Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
},
"metadata": {
"description": "GitHits plugins for Claude Code - code examples from global open source",
"version": "0.2.1"
"version": "0.2.2"
},
"plugins": [
{
Expand Down
2 changes: 1 addition & 1 deletion .claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "githits",
"version": "0.2.1",
"version": "0.2.2",
"description": "Code examples from global open source for developers and AI assistants",
"author": {
"name": "GitHits"
Expand Down
2 changes: 1 addition & 1 deletion .plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "githits",
"version": "0.2.1",
"version": "0.2.2",
"description": "Code examples from global open source for developers and AI assistants",
"author": {
"name": "GitHits"
Expand Down
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions docs/implementation/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,14 @@ Commands receive the full `Dependencies` object. Services receive only what they
- **Custom environment not working** — Make sure both `GITHITS_MCP_URL` and `GITHITS_API_URL` are set. They point to different services.
- **Tokens from wrong environment** — Tokens are stored per MCP URL. If you switched `GITHITS_MCP_URL`, you need to re-authenticate for the new URL.

### Init config parsing behavior

`githits init` now accepts both strict JSON and JSONC-style config files when reading agent MCP config files (for example files containing comments or trailing commas).

- Parsing flow first attempts strict JSON, then falls back to JSONC parsing.
- If parsing still fails, setup reports a parse error and leaves the file unchanged.
- Successful writes are still emitted as canonical JSON with 2-space indentation and a trailing newline.

## Key Reference Files

| File | What it demonstrates |
Expand Down
2 changes: 1 addition & 1 deletion gemini-extension.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "githits",
"version": "0.2.1",
"version": "0.2.2",
"description": "Code examples from global open source for developers and AI assistants.",
"mcpServers": {
"githits": {
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "githits",
"description": "CLI companion for GitHits - code examples from global open source for developers and AI assistants",
"version": "0.2.1",
"version": "0.2.2",
"type": "module",
"files": [
"dist",
Expand Down Expand Up @@ -72,6 +72,7 @@
"@modelcontextprotocol/sdk": "^1.23.0",
"@napi-rs/keyring": "^1.2.0",
"commander": "^14.0.2",
"jsonc-parser": "^3.3.1",
"open": "^11.0.0",
"zod": "^4.1.13"
},
Expand Down
2 changes: 1 addition & 1 deletion plugins/claude/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "githits",
"version": "0.2.1",
"version": "0.2.2",
"description": "Code examples from global open source for developers and AI assistants",
"author": {
"name": "GitHits"
Expand Down
199 changes: 190 additions & 9 deletions src/commands/init/agent-definitions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,10 +86,105 @@ describe("detection configuration", () => {
expect(paths).toEqual(["/home/test/.gemini/antigravity"]);
});

it("opencode uses binary detection only", () => {
it("opencode uses hybrid detection", () => {
const agent = agentDefinitions.find((a) => a.id === "opencode")!;
expect(agent.detectBinary).toBeDefined();
expect(agent.detectPaths).toBeUndefined();
expect(agent.detectPaths).toBeDefined();
});

it("opencode includes desktop and config detection paths on linux", () => {
const originalPlatform = process.platform;
const originalXdgDataHome = process.env.XDG_DATA_HOME;
Object.defineProperty(process, "platform", {
value: "linux",
configurable: true,
});
process.env.XDG_DATA_HOME = "/home/test/.local/share";
try {
const fs = createMockFileSystemService({
getHomeDir: mock(() => "/home/test"),
joinPath: mock((...segments: string[]) => segments.join("/")),
});
const agent = agentDefinitions.find((a) => a.id === "opencode")!;
const paths = agent.detectPaths?.(fs);
expect(paths).toEqual([
"/home/test/.local/share/ai.opencode.desktop",
"/home/test/.local/share/ai.opencode.desktop.beta",
"/home/test/.local/share/ai.opencode.desktop.dev",
"/home/test/.config/opencode",
]);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
configurable: true,
});
if (originalXdgDataHome !== undefined) {
process.env.XDG_DATA_HOME = originalXdgDataHome;
} else {
delete process.env.XDG_DATA_HOME;
}
}
});

it("opencode includes desktop and config detection paths on darwin", () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
value: "darwin",
configurable: true,
});
try {
const fs = createMockFileSystemService({
getHomeDir: mock(() => "/home/test"),
joinPath: mock((...segments: string[]) => segments.join("/")),
});
const agent = agentDefinitions.find((a) => a.id === "opencode")!;
const paths = agent.detectPaths?.(fs);
expect(paths).toEqual([
"/home/test/Library/Application Support/ai.opencode.desktop",
"/home/test/Library/Application Support/ai.opencode.desktop.beta",
"/home/test/Library/Application Support/ai.opencode.desktop.dev",
"/home/test/.config/opencode",
]);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
configurable: true,
});
}
});

it("opencode includes desktop and config detection paths on win32", () => {
const originalPlatform = process.platform;
const originalAppdata = process.env.APPDATA;
Object.defineProperty(process, "platform", {
value: "win32",
configurable: true,
});
process.env.APPDATA = "C:\\Users\\test\\AppData\\Roaming";
try {
const fs = createMockFileSystemService({
getHomeDir: mock(() => "C:\\Users\\test"),
joinPath: mock((...segments: string[]) => segments.join("/")),
});
const agent = agentDefinitions.find((a) => a.id === "opencode")!;
const paths = agent.detectPaths?.(fs);
expect(paths).toEqual([
"C:\\Users\\test\\AppData\\Roaming/ai.opencode.desktop",
"C:\\Users\\test\\AppData\\Roaming/ai.opencode.desktop.beta",
"C:\\Users\\test\\AppData\\Roaming/ai.opencode.desktop.dev",
"C:\\Users\\test\\AppData\\Roaming/opencode",
]);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
configurable: true,
});
if (originalAppdata !== undefined) {
process.env.APPDATA = originalAppdata;
} else {
delete process.env.APPDATA;
}
}
});

it("claude-desktop checks multiple Windows paths on win32", () => {
Expand Down Expand Up @@ -311,14 +406,16 @@ describe("detection configuration", () => {
expect(await agent.detectBinary!(exec)).toBe(false);
});

it("path-detected agents use FileSystemService.getHomeDir (not hardcoded)", () => {
it("path/hybrid-detected agents use FileSystemService.getHomeDir (not hardcoded)", () => {
const originalPlatform = process.platform;
const originalAppdata = process.env.APPDATA;
const originalXdgDataHome = process.env.XDG_DATA_HOME;
Object.defineProperty(process, "platform", {
value: "linux",
configurable: true,
});
delete process.env.APPDATA;
delete process.env.XDG_DATA_HOME;
try {
const fs = createMockFileSystemService({
getHomeDir: mock(() => "/custom/home"),
Expand All @@ -343,6 +440,11 @@ describe("detection configuration", () => {
} else {
delete process.env.APPDATA;
}
if (originalXdgDataHome !== undefined) {
process.env.XDG_DATA_HOME = originalXdgDataHome;
} else {
delete process.env.XDG_DATA_HOME;
}
}
});
});
Expand Down Expand Up @@ -769,12 +871,12 @@ describe("detectAgents", () => {
isDirectory: mock(() => Promise.resolve(true)),
});
const detected = await detectAgents(agentDefinitions, fs);
// detectAgents (deprecated) only checks path-based agents
expect(detected).toHaveLength(6);
// detectAgents (deprecated) checks path and hybrid agents
expect(detected).toHaveLength(7);
expect(detected).not.toContain("claude-code");
expect(detected).not.toContain("codex-cli");
expect(detected).not.toContain("gemini-cli");
expect(detected).not.toContain("opencode");
expect(detected).toContain("opencode");
});
});

Expand Down Expand Up @@ -978,7 +1080,7 @@ describe("scanAgents", () => {
expect(result.notDetected.some((a) => a.id === "codex-cli")).toBe(false);
});

it("does not detect opencode from config directory when binary is missing", async () => {
it("detects opencode from config directory when binary is missing", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
value: "linux",
Expand All @@ -994,8 +1096,59 @@ describe("scanAgents", () => {
},
});
const result = await scanAgents(agentDefinitions, fs, execService);
expect(result.notDetected.some((a) => a.id === "opencode")).toBe(true);
expect(result.needsSetup.some((a) => a.id === "opencode")).toBe(false);
expect(result.notDetected.some((a) => a.id === "opencode")).toBe(false);
expect(result.needsSetup.some((a) => a.id === "opencode")).toBe(true);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
configurable: true,
});
}
});

it("detects opencode from desktop app data directory when binary is missing", async () => {
const originalPlatform = process.platform;
const originalXdgDataHome = process.env.XDG_DATA_HOME;
Object.defineProperty(process, "platform", {
value: "linux",
configurable: true,
});
process.env.XDG_DATA_HOME = "/home/test/.local/share";
try {
const { fs, execService } = createScanMocks({
detectedDirs: ["/home/test/.local/share/ai.opencode.desktop"],
});
const result = await scanAgents(agentDefinitions, fs, execService);
expect(result.notDetected.some((a) => a.id === "opencode")).toBe(false);
expect(result.needsSetup.some((a) => a.id === "opencode")).toBe(true);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
configurable: true,
});
if (originalXdgDataHome !== undefined) {
process.env.XDG_DATA_HOME = originalXdgDataHome;
} else {
delete process.env.XDG_DATA_HOME;
}
}
});

it("detects opencode from desktop app data directory on darwin when binary is missing", async () => {
const originalPlatform = process.platform;
Object.defineProperty(process, "platform", {
value: "darwin",
configurable: true,
});
try {
const { fs, execService } = createScanMocks({
detectedDirs: [
"/home/test/Library/Application Support/ai.opencode.desktop",
],
});
const result = await scanAgents(agentDefinitions, fs, execService);
expect(result.notDetected.some((a) => a.id === "opencode")).toBe(false);
expect(result.needsSetup.some((a) => a.id === "opencode")).toBe(true);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
Expand All @@ -1004,6 +1157,34 @@ describe("scanAgents", () => {
}
});

it("detects opencode from desktop app data directory on win32 when binary is missing", async () => {
const originalPlatform = process.platform;
const originalAppdata = process.env.APPDATA;
Object.defineProperty(process, "platform", {
value: "win32",
configurable: true,
});
process.env.APPDATA = "C:\\Users\\test\\AppData\\Roaming";
try {
const { fs, execService } = createScanMocks({
detectedDirs: ["C:\\Users\\test\\AppData\\Roaming/ai.opencode.desktop"],
});
const result = await scanAgents(agentDefinitions, fs, execService);
expect(result.notDetected.some((a) => a.id === "opencode")).toBe(false);
expect(result.needsSetup.some((a) => a.id === "opencode")).toBe(true);
} finally {
Object.defineProperty(process, "platform", {
value: originalPlatform,
configurable: true,
});
if (originalAppdata !== undefined) {
process.env.APPDATA = originalAppdata;
} else {
delete process.env.APPDATA;
}
}
});

it("categorizes undetected agent as notDetected", async () => {
const { fs, execService } = createScanMocks({ detectedDirs: [] });
const result = await scanAgents(agentDefinitions, fs, execService);
Expand Down
Loading
Loading