mirror of
https://github.com/mksglu/context-mode.git
synced 2026-10-02 12:25:33 +08:00
feat(opencode): add JSONC config support without external dependency (#205)
OpenCode supports opencode.jsonc as config format. Added .jsonc paths to config detection and a 3-line stripJsonComments function instead of the jsonc-parser dependency (208KB). Priority: .json before .jsonc at each level. writeSettings writes back to the same file it read. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
c2db524509
commit
7189ed6c0f
@@ -17,6 +17,14 @@
|
||||
*/
|
||||
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
/** Strip JSONC comments (// and /* */) and trailing commas for JSON.parse. */
|
||||
function stripJsonComments(str: string): string {
|
||||
return str
|
||||
.replace(/\/\/.*$/gm, "")
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/,(\s*[}\]])/g, "$1");
|
||||
}
|
||||
import {
|
||||
readFileSync,
|
||||
writeFileSync,
|
||||
@@ -230,8 +238,11 @@ export class OpenCodeAdapter implements HookAdapter {
|
||||
}
|
||||
return [
|
||||
resolve("opencode.json"),
|
||||
resolve("opencode.jsonc"),
|
||||
resolve(".opencode", "opencode.json"),
|
||||
resolve(".opencode", "opencode.jsonc"),
|
||||
join(homedir(), ".config", "opencode", "opencode.json"),
|
||||
join(homedir(), ".config", "opencode", "opencode.jsonc"),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -307,7 +318,8 @@ export class OpenCodeAdapter implements HookAdapter {
|
||||
try {
|
||||
const raw = readFileSync(configPath, "utf-8");
|
||||
this.settingsPath = configPath;
|
||||
return JSON.parse(raw) as Record<string, unknown>;
|
||||
const text = configPath.endsWith(".jsonc") ? stripJsonComments(raw) : raw;
|
||||
return JSON.parse(text) as Record<string, unknown>;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
@@ -334,7 +346,7 @@ export class OpenCodeAdapter implements HookAdapter {
|
||||
results.push({
|
||||
check: "Plugin configuration",
|
||||
status: "fail",
|
||||
message: "Could not read opencode.json",
|
||||
message: `Could not read ${this.platform}.json or ${this.platform}.jsonc`,
|
||||
fix: "context-mode upgrade",
|
||||
});
|
||||
return results;
|
||||
@@ -380,7 +392,7 @@ export class OpenCodeAdapter implements HookAdapter {
|
||||
return {
|
||||
check: "Plugin registration",
|
||||
status: "warn",
|
||||
message: "Could not read opencode.json",
|
||||
message: `Could not read ${this.platform}.json or ${this.platform}.jsonc`,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -257,6 +257,113 @@ describe("OpenCodeAdapter", () => {
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("readSettings reads opencode.jsonc with comments stripped", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-adapter-"));
|
||||
const dir = join(root, "project");
|
||||
const src = resolve(process.cwd(), "src", "adapters", "opencode", "index.ts");
|
||||
const tsx = resolve(process.cwd(), "node_modules", "tsx", "dist", "cli.mjs");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, "opencode.jsonc"),
|
||||
`{
|
||||
// This is a line comment
|
||||
"plugin": ["context-mode"],
|
||||
/* Block comment */
|
||||
"version": "1.0"
|
||||
}
|
||||
`,
|
||||
);
|
||||
const run = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
tsx,
|
||||
"-e",
|
||||
`import { OpenCodeAdapter } from ${JSON.stringify(src)};const a=new OpenCodeAdapter();console.log(JSON.stringify(a.readSettings()))`,
|
||||
],
|
||||
{ cwd: dir, env: env(join(root, "home")), encoding: "utf-8" },
|
||||
);
|
||||
expect(run.status).toBe(0);
|
||||
expect(JSON.parse(run.stdout)).toEqual({ plugin: ["context-mode"], version: "1.0" });
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("prefers opencode.json over opencode.jsonc when both exist", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-adapter-"));
|
||||
const dir = join(root, "project");
|
||||
const src = resolve(process.cwd(), "src", "adapters", "opencode", "index.ts");
|
||||
const tsx = resolve(process.cwd(), "node_modules", "tsx", "dist", "cli.mjs");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(join(dir, "opencode.json"), JSON.stringify({ from: "json" }));
|
||||
writeFileSync(join(dir, "opencode.jsonc"), `{ /* comment */ "from": "jsonc" }`);
|
||||
const run = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
tsx,
|
||||
"-e",
|
||||
`import { OpenCodeAdapter } from ${JSON.stringify(src)};const a=new OpenCodeAdapter();console.log(JSON.stringify(a.readSettings()))`,
|
||||
],
|
||||
{ cwd: dir, env: env(join(root, "home")), encoding: "utf-8" },
|
||||
);
|
||||
expect(run.status).toBe(0);
|
||||
expect(JSON.parse(run.stdout)).toEqual({ from: "json" });
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("configureAllHooks works with opencode.jsonc", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-adapter-"));
|
||||
const dir = join(root, "project");
|
||||
const src = resolve(process.cwd(), "src", "adapters", "opencode", "index.ts");
|
||||
const tsx = resolve(process.cwd(), "node_modules", "tsx", "dist", "cli.mjs");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
writeFileSync(
|
||||
join(dir, "opencode.jsonc"),
|
||||
`{
|
||||
// My config
|
||||
"plugin": []
|
||||
}
|
||||
`,
|
||||
);
|
||||
const run = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
tsx,
|
||||
"-e",
|
||||
`import { OpenCodeAdapter } from ${JSON.stringify(src)};const a=new OpenCodeAdapter();console.log(JSON.stringify(a.configureAllHooks('/tmp/plugin')))`,
|
||||
],
|
||||
{ cwd: dir, env: env(join(root, "home")), encoding: "utf-8" },
|
||||
);
|
||||
expect(run.status).toBe(0);
|
||||
expect(JSON.parse(run.stdout)).toEqual(["Added context-mode to plugin array"]);
|
||||
// Should write back to .jsonc (same file it read)
|
||||
expect(JSON.parse(readFileSync(join(dir, "opencode.jsonc"), "utf-8"))).toEqual({
|
||||
plugin: ["context-mode"],
|
||||
});
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("validates hooks with jsonc config shows correct error message", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-adapter-"));
|
||||
const dir = join(root, "project");
|
||||
const src = resolve(process.cwd(), "src", "adapters", "opencode", "index.ts");
|
||||
const tsx = resolve(process.cwd(), "node_modules", "tsx", "dist", "cli.mjs");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
// No config file at all
|
||||
const run = spawnSync(
|
||||
process.execPath,
|
||||
[
|
||||
tsx,
|
||||
"-e",
|
||||
`import { OpenCodeAdapter } from ${JSON.stringify(src)};const a=new OpenCodeAdapter();console.log(JSON.stringify(a.validateHooks('/tmp')))`,
|
||||
],
|
||||
{ cwd: dir, env: env(join(root, "home")), encoding: "utf-8" },
|
||||
);
|
||||
expect(run.status).toBe(0);
|
||||
const results = JSON.parse(run.stdout);
|
||||
const pluginCheck = results.find((r: { check: string }) => r.check === "Plugin configuration");
|
||||
expect(pluginCheck.message).toContain("jsonc");
|
||||
rmSync(root, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it("configureAllHooks writes back to .opencode/opencode.json when that is the selected config", () => {
|
||||
const root = mkdtempSync(join(tmpdir(), "opencode-adapter-"));
|
||||
const dir = join(root, "project");
|
||||
|
||||
Reference in New Issue
Block a user