|
|
@@ -2,10 +2,18 @@
|
|
|
* opencode target.
|
|
|
*
|
|
|
* - MCP server entry to `~/.config/opencode/opencode.jsonc` (global,
|
|
|
- * XDG-style; `%APPDATA%/opencode/opencode.jsonc` on Windows) or
|
|
|
+ * XDG-style on EVERY platform, Windows included — see below) or
|
|
|
* `./opencode.jsonc` (local). Falls back to `opencode.json` when a
|
|
|
* `.json` file already exists; defaults new installs to `.jsonc`
|
|
|
* because that's what opencode itself creates on first run.
|
|
|
+ *
|
|
|
+ * opencode resolves its config dir with the `xdg-basedir` package
|
|
|
+ * (sst/opencode `packages/core/src/global.ts`): `XDG_CONFIG_HOME`
|
|
|
+ * if set, else `~/.config` — unconditionally, on all platforms. It
|
|
|
+ * never reads `%APPDATA%`; that layout belonged to the discontinued
|
|
|
+ * Go fork. We previously wrote there on Windows, so opencode never
|
|
|
+ * saw the entry (#535) — install/uninstall now also sweep a stale
|
|
|
+ * codegraph entry out of the legacy `%APPDATA%/opencode` location.
|
|
|
* - Instructions to `~/.config/opencode/AGENTS.md` (global) or
|
|
|
* `./AGENTS.md` (local). opencode reads AGENTS.md for agent
|
|
|
* instructions — same convention Codex CLI uses.
|
|
|
@@ -49,17 +57,29 @@ import {
|
|
|
} from '../instructions-template';
|
|
|
|
|
|
function globalConfigDir(): string {
|
|
|
- if (process.platform === 'win32') {
|
|
|
- const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
|
|
|
- return path.join(appData, 'opencode');
|
|
|
- }
|
|
|
- // XDG_CONFIG_HOME if set, else ~/.config — matches opencode's docs.
|
|
|
+ // XDG_CONFIG_HOME if set, else ~/.config — on every platform, matching
|
|
|
+ // opencode's own `xdg-basedir` resolution (no Windows special case; #535).
|
|
|
const xdg = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim().length > 0
|
|
|
? process.env.XDG_CONFIG_HOME
|
|
|
: path.join(os.homedir(), '.config');
|
|
|
return path.join(xdg, 'opencode');
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Pre-#535 installs wrote the global entry to `%APPDATA%/opencode` — a dir
|
|
|
+ * today's opencode never reads. Returns that legacy dir when it could hold
|
|
|
+ * stale state (APPDATA set and resolving somewhere other than the real config
|
|
|
+ * dir). Gated on the env var rather than `process.platform` so the cleanup
|
|
|
+ * logic runs under the cross-platform test suite; on POSIX, APPDATA is unset
|
|
|
+ * in real life and this is a no-op.
|
|
|
+ */
|
|
|
+function legacyWindowsConfigDir(): string | null {
|
|
|
+ const appData = process.env.APPDATA;
|
|
|
+ if (!appData || !appData.trim()) return null;
|
|
|
+ const legacy = path.join(appData, 'opencode');
|
|
|
+ return path.resolve(legacy) === path.resolve(globalConfigDir()) ? null : legacy;
|
|
|
+}
|
|
|
+
|
|
|
function configBaseDir(loc: Location): string {
|
|
|
return loc === 'global' ? globalConfigDir() : process.cwd();
|
|
|
}
|
|
|
@@ -118,8 +138,12 @@ class OpencodeTarget implements AgentTarget {
|
|
|
const file = configPath(loc);
|
|
|
const config = parseConfig(readConfigText(file));
|
|
|
const alreadyConfigured = !!config.mcp?.codegraph;
|
|
|
+ // Global: the XDG dir is what current opencode creates on first run; the
|
|
|
+ // legacy %APPDATA% dir still counts as "opencode present" so a re-install
|
|
|
+ // can sweep the stale pre-#535 entry out of it.
|
|
|
+ const legacy = legacyWindowsConfigDir();
|
|
|
const installed = loc === 'global'
|
|
|
- ? fs.existsSync(globalConfigDir())
|
|
|
+ ? fs.existsSync(globalConfigDir()) || (!!legacy && fs.existsSync(legacy))
|
|
|
: fs.existsSync(file);
|
|
|
return { installed, alreadyConfigured, configPath: file };
|
|
|
}
|
|
|
@@ -133,42 +157,18 @@ class OpencodeTarget implements AgentTarget {
|
|
|
// initialize instructions. Upsert self-heals a stale pre-#529 block.
|
|
|
files.push(upsertInstructionsEntry(instructionsPath(loc)));
|
|
|
|
|
|
+ // Self-heal a pre-#535 install that wrote to %APPDATA%/opencode —
|
|
|
+ // opencode never reads it, so anything of ours there is stale.
|
|
|
+ if (loc === 'global') files.push(...cleanupLegacyWindowsState());
|
|
|
+
|
|
|
return { files };
|
|
|
}
|
|
|
|
|
|
uninstall(loc: Location): WriteResult {
|
|
|
const files: WriteResult['files'] = [];
|
|
|
- const file = configPath(loc);
|
|
|
-
|
|
|
- if (!fs.existsSync(file)) {
|
|
|
- files.push({ path: file, action: 'not-found' });
|
|
|
- } else {
|
|
|
- const text = readConfigText(file);
|
|
|
- const config = parseConfig(text);
|
|
|
- if (!config.mcp?.codegraph) {
|
|
|
- files.push({ path: file, action: 'not-found' });
|
|
|
- } else {
|
|
|
- // Drop our key surgically. Leaves siblings + comments untouched.
|
|
|
- let edits = modify(text, ['mcp', 'codegraph'], undefined, {
|
|
|
- formattingOptions: FORMATTING,
|
|
|
- });
|
|
|
- let updated = applyEdits(text, edits);
|
|
|
-
|
|
|
- // If `mcp` is now an empty object, drop the wrapper too.
|
|
|
- const afterParsed = parseConfig(updated);
|
|
|
- if (afterParsed.mcp && typeof afterParsed.mcp === 'object' &&
|
|
|
- Object.keys(afterParsed.mcp).length === 0) {
|
|
|
- edits = modify(updated, ['mcp'], undefined, { formattingOptions: FORMATTING });
|
|
|
- updated = applyEdits(updated, edits);
|
|
|
- }
|
|
|
-
|
|
|
- atomicWriteFileSync(file, updated);
|
|
|
- files.push({ path: file, action: 'removed' });
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
+ files.push(removeMcpEntryAt(configPath(loc)));
|
|
|
files.push(removeInstructionsEntry(loc));
|
|
|
-
|
|
|
+ if (loc === 'global') files.push(...cleanupLegacyWindowsState());
|
|
|
return { files };
|
|
|
}
|
|
|
|
|
|
@@ -225,6 +225,55 @@ function writeMcpEntry(loc: Location): WriteResult['files'][number] {
|
|
|
return { path: file, action: existed ? 'updated' : 'created' };
|
|
|
}
|
|
|
|
|
|
+/**
|
|
|
+ * Surgically drop `mcp.codegraph` from one config file. Leaves sibling
|
|
|
+ * servers, comments, and formatting untouched; drops an emptied `mcp`
|
|
|
+ * wrapper too. Shared by uninstall and the legacy-%APPDATA% sweep.
|
|
|
+ */
|
|
|
+function removeMcpEntryAt(file: string): WriteResult['files'][number] {
|
|
|
+ if (!fs.existsSync(file)) return { path: file, action: 'not-found' };
|
|
|
+ const text = readConfigText(file);
|
|
|
+ const config = parseConfig(text);
|
|
|
+ if (!config.mcp?.codegraph) return { path: file, action: 'not-found' };
|
|
|
+
|
|
|
+ let edits = modify(text, ['mcp', 'codegraph'], undefined, {
|
|
|
+ formattingOptions: FORMATTING,
|
|
|
+ });
|
|
|
+ let updated = applyEdits(text, edits);
|
|
|
+
|
|
|
+ // If `mcp` is now an empty object, drop the wrapper too.
|
|
|
+ const afterParsed = parseConfig(updated);
|
|
|
+ if (afterParsed.mcp && typeof afterParsed.mcp === 'object' &&
|
|
|
+ Object.keys(afterParsed.mcp).length === 0) {
|
|
|
+ edits = modify(updated, ['mcp'], undefined, { formattingOptions: FORMATTING });
|
|
|
+ updated = applyEdits(updated, edits);
|
|
|
+ }
|
|
|
+
|
|
|
+ atomicWriteFileSync(file, updated);
|
|
|
+ return { path: file, action: 'removed' };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * Remove whatever a pre-#535 install left in `%APPDATA%/opencode` — an MCP
|
|
|
+ * entry opencode never reads, plus our marker-fenced AGENTS.md block. Returns
|
|
|
+ * only files actually changed, so install output stays quiet when there is
|
|
|
+ * nothing to heal. Never touches anything else in the legacy dir: a user may
|
|
|
+ * genuinely keep other tools' state under %APPDATA%.
|
|
|
+ */
|
|
|
+function cleanupLegacyWindowsState(): WriteResult['files'] {
|
|
|
+ const dir = legacyWindowsConfigDir();
|
|
|
+ if (!dir || !fs.existsSync(dir)) return [];
|
|
|
+ const out: WriteResult['files'] = [];
|
|
|
+ for (const name of ['opencode.jsonc', 'opencode.json']) {
|
|
|
+ const res = removeMcpEntryAt(path.join(dir, name));
|
|
|
+ if (res.action === 'removed') out.push(res);
|
|
|
+ }
|
|
|
+ const agents = path.join(dir, 'AGENTS.md');
|
|
|
+ const action = removeMarkedSection(agents, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
|
|
+ if (action === 'removed') out.push({ path: agents, action });
|
|
|
+ return out;
|
|
|
+}
|
|
|
+
|
|
|
/**
|
|
|
* Strip the marker-delimited CodeGraph block from AGENTS.md if a prior
|
|
|
* install wrote one. Used by both install (self-heal on upgrade) and
|