* feat(#1914): OpenCode native plugin integration (Option 1 file-copy) Ship a native OpenCode plugin (.opencode/plugins/gsd-core.js) plus the installer step that delivers it, so GSD's lifecycle hooks run on OpenCode. OpenCode declares hooksSurface:'none', so GSD's hook scripts already ship to <configDir>/hooks/ but nothing invokes them; the plugin bridges OpenCode's event bus onto those scripts as subprocesses (prompt/read/worktree/workflow guards, injection scanner, context monitor). Distribution is Option 1 (file copy) per the #1914 triage decision: no scripts.build rename, no prepare/prepack removal. package.json gains main + .opencode in files[] for discovery. Corrected against OpenCode's docs + loader source (not the reference branch): - Auto-discovery globs {plugin,plugins}/*.{ts,js} — .cjs is never matched, so the installed adapter must be .js (config dir carries {"type":"commonjs"}). - No opencode.json plugin-array patch — that array is npm-only; local files are auto-discovered. - REPO_ROOT is resolved by walking up to the dir holding hooks/ + gsd-core/, correct for package tree, global install, and local install. - Config-hook registration is gated (IS_PACKAGE_TREE) so it never double-registers commands/agents/skills already delivered by native copy. Also fixes an incidental .gitignore drift: 8 ADR-1239 .cts-generated .cjs artifacts were untracked-and-not-ignored (leak risk) — now ignored. Tests: tests/opencode-plugin-adapter.test.cjs (14, pure helpers + real subprocess bridge against stub hooks); golden-install-parity regenerated. Green on Mac + Linux (gsd-test): 0 failures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1914): harden OpenCode plugin export shape + advisory accumulation (adversarial findings) Address Codex adversarial-review findings: - HIGH: export `{ id, server }` could trip OpenCode's loader (`for (entry of Object.values(mod)) getServerPlugin(entry)` throws on a non-extractable value). Make `id` NON-ENUMERABLE and assign module.exports from a variable (not a literal) so no string `id` is ever iterated — verified loader-safe under real import(pathToFileURL) (default + module.exports alias, both objects with .server; no bare id string). - MEDIUM: sequential advisory hooks clobbered output.metadata._gsdAdvisory; now accumulate into an array. - LOW: resolveRepoRoot fallback returned ".." while the comment said "../.." — aligned to "../.." (package-tree depth). Tests: added a faithful loader-loop emulation (raw CJS + ESM namespace views), advisory-accumulation, and a real bin/install.js copy→manifest→uninstall integration test. golden regenerated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs(#1914): add changeset fragment for OpenCode plugin integration (PR #1923) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(#1914): Windows — assert rewritten path with string include, not path-regex The Read content-rewrite test built a RegExp from `path.join(root,'gsd-core')`. On Windows the backslashes in the path are interpreted as regex escapes, so the assertion never matched and `test (windows-latest, 24)` failed — even though the adapter rewrote the path correctly. Replace the RegExp with a separator-agnostic `String.includes` check (the repo's no-path-literal-in-assert concern). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7316,6 +7316,22 @@ function uninstall(isGlobal, runtime = 'claude') {
|
||||
}
|
||||
}
|
||||
|
||||
// 4z. Remove the OpenCode native plugin adapter (#1914). Only GSD's own
|
||||
// plugin file is removed; the plugins/ dir is pruned only if it becomes
|
||||
// empty, preserving any user-authored OpenCode plugins.
|
||||
if (isOpencode) {
|
||||
const pluginsDir = path.join(targetDir, 'plugins');
|
||||
const pluginPath = path.join(pluginsDir, 'gsd-core.js');
|
||||
if (fs.existsSync(pluginPath)) {
|
||||
try {
|
||||
fs.unlinkSync(pluginPath);
|
||||
removedCount++;
|
||||
console.log(` ${green}✓${reset} Removed OpenCode plugin`);
|
||||
} catch (_) { /* best-effort */ }
|
||||
try { fs.rmdirSync(pluginsDir); } catch (_) { /* not empty — user plugins present */ }
|
||||
}
|
||||
}
|
||||
|
||||
// 4a. Remove scripts/changeset/ and scripts/lib/ (#935)
|
||||
// GSD-managed files only: enumerate the exact set the installer writes.
|
||||
// Any file NOT in this set is user-owned and must survive uninstall.
|
||||
@@ -8064,6 +8080,15 @@ function writeManifest(configDir, runtime = 'claude', options = {}) {
|
||||
manifest.files['scripts/fix-slash-commands.cjs'] = fileHash(fixSlashInstallPath);
|
||||
}
|
||||
|
||||
// Track the OpenCode native plugin adapter (#1914) so update/drift detection
|
||||
// and uninstall can account for it.
|
||||
if (isOpencode) {
|
||||
const pluginInstallPath = path.join(configDir, 'plugins', 'gsd-core.js');
|
||||
if (fs.existsSync(pluginInstallPath)) {
|
||||
manifest.files['plugins/gsd-core.js'] = fileHash(pluginInstallPath);
|
||||
}
|
||||
}
|
||||
|
||||
fs.writeFileSync(path.join(configDir, MANIFEST_NAME), JSON.stringify(manifest, null, 2));
|
||||
return manifest;
|
||||
}
|
||||
@@ -8999,6 +9024,39 @@ function install(isGlobal, runtime = 'claude', options = {}) {
|
||||
} else {
|
||||
failures.push('skills/gsd-*');
|
||||
}
|
||||
|
||||
// OpenCode-only: install the native plugin adapter (#1914). OpenCode
|
||||
// declares hooksSurface: 'none', so GSD's lifecycle hooks are never
|
||||
// registered as settings.json hooks the way Claude Code does — the hook
|
||||
// *scripts* ship to <configDir>/hooks/ but nothing invokes them. This
|
||||
// plugin bridges OpenCode's event bus onto those existing hook scripts
|
||||
// (prompt guard, read guard, injection scanner, context monitor, ...),
|
||||
// spawning them as subprocesses. OpenCode auto-discovers plugin files under
|
||||
// <configDir>/plugins/ at startup — no opencode.json registration needed
|
||||
// (its `plugin` array is for npm packages, not local file paths).
|
||||
//
|
||||
// The file MUST land as `.js`: OpenCode's loader globs
|
||||
// `{plugin,plugins}/*.{ts,js}` (verified against its source) — a `.cjs`
|
||||
// extension would never be discovered. The config dir carries a
|
||||
// `{"type":"commonjs"}` package.json (written above), so the `.js` file is
|
||||
// interpreted as CommonJS, matching the adapter's module.exports/require.
|
||||
// Kilo has no plugin surface, so this is gated to OpenCode only.
|
||||
if (isOpencode) {
|
||||
const pluginSrc = path.join(src, '.opencode', 'plugins', 'gsd-core.js');
|
||||
const pluginDestDir = path.join(targetDir, 'plugins');
|
||||
const pluginDest = path.join(pluginDestDir, 'gsd-core.js');
|
||||
if (fs.existsSync(pluginSrc)) {
|
||||
fs.mkdirSync(pluginDestDir, { recursive: true });
|
||||
fs.copyFileSync(pluginSrc, pluginDest);
|
||||
if (fs.existsSync(pluginDest)) {
|
||||
console.log(` ${green}✓${reset} Installed OpenCode plugin (bridges GSD hooks)`);
|
||||
} else {
|
||||
failures.push('plugins/gsd-core.js');
|
||||
}
|
||||
} else {
|
||||
failures.push('plugins/gsd-core.js');
|
||||
}
|
||||
}
|
||||
} else if (isCline) {
|
||||
// Cline local install: rules-based only — commands are embedded in .clinerules (generated below).
|
||||
// No skills/commands directory needed for local installs.
|
||||
|
||||
Reference in New Issue
Block a user