← back to Kickbacks Ai Upstream
src/adapters/registry.ts
120 lines
import { homedir } from "node:os";
import { join } from "node:path";
import { readdirSync, existsSync } from "node:fs";
import type { TargetAdapter } from "./types";
import { ClaudeCodeAdapter } from "./claude-code/adapter";
import { CodexAdapter } from "./codex/adapter";
import { compareClaudeCodeInstall } from "../util/claudeCodeVersion";
// Same host roots locate.ts scans for Claude Code (keep the lists in sync):
// local (.vscode/.vscode-insiders/.cursor) AND remote/server hosts
// (Remote-SSH, dev containers, vscode.dev) where extensions live under
// *-server/.
const ROOTS = [".vscode", ".vscode-insiders", ".vscode-server",
".vscode-server-insiders", ".cursor", ".cursor-server"]
.map((d) => join(homedir(), d, "extensions"));
// An env override is AUTHORITATIVE when set (non-empty): return it iff it
// exists, else treat the target as absent — never silently fall back to a
// machine scan. Keeps the discovery matrix hermetic and makes an explicit
// S5/manual-smoke override predictable. `undefined` => env unset => scan.
function envTarget(name: string): string | null | undefined {
const v = process.env[name];
if (!v) return undefined; // unset/empty => fall through to scan
return existsSync(v) ? v : null; // set => authoritative (exists or absent)
}
// Newest `<root>/<prefix>*/<...sub>` across all host roots. Highest semantic
// Claude Code version wins; lexical ordering is only a fallback for unexpected
// names. Never throws.
function newestUnder(prefix: string, sub: string[]): string | null {
for (const root of ROOTS) {
try {
if (!existsSync(root)) continue;
const hits: string[] = [];
for (const name of readdirSync(root)) {
if (!name.startsWith(prefix)) continue;
const p = join(root, name, ...sub);
if (existsSync(p)) hits.push(p);
}
hits.sort(compareClaudeCodeInstall);
if (hits.length) return hits[hits.length - 1];
} catch { /* ignore this root */ }
}
return null;
}
// Codex ships the entry component in a content-hashed chunk:
// openai.chatgpt-<ver>/webview/assets/thinking-shimmer-<hash>.js — glob the
// assets dir for the prefix (newest extension dir, first matching chunk).
function newestCodexChunk(): string | null {
for (const root of ROOTS) {
try {
if (!existsSync(root)) continue;
const ext = readdirSync(root)
.filter((n) => n.startsWith("openai.chatgpt-")).sort();
for (let i = ext.length - 1; i >= 0; i--) {
const assets = join(root, ext[i], "webview", "assets");
if (!existsSync(assets)) continue;
const cf = readdirSync(assets)
.filter((n) => /^thinking-shimmer-.*\.js$/.test(n)).sort();
if (cf.length) return join(assets, cf[0]);
}
} catch { /* ignore this root */ }
}
return null;
}
export interface TargetEntry {
id: string;
locate(): string | null;
make(target: string): TargetAdapter;
}
// Order = precedence: claude-code is the "primary" (spec S9-5) — its version
// is the one reported to the backend / status bar.
export const REGISTRY: TargetEntry[] = [
{
id: "claude-code",
locate: () => {
const ev = envTarget("KICKBACKS_CC_TARGET")
?? envTarget("VIBE_ADS_CC_TARGET");
if (ev !== undefined) return ev; // authoritative when set
return newestUnder("anthropic.claude-code-", ["webview", "index.js"]);
},
make: (t) => new ClaudeCodeAdapter(t),
},
{
id: "codex",
locate: () => {
const ev = envTarget("KICKBACKS_CODEX_TARGET")
?? envTarget("VIBE_ADS_CODEX_TARGET");
if (ev !== undefined) return ev; // authoritative when set
return newestCodexChunk();
},
make: (t) => new CodexAdapter(t),
},
];
/** The Codex chunk target on this host (env override authoritative when set,
* else newest install), or null. Never throws — lets extension.ts wire Codex
* as a guarded, additive second target without duplicating the scan. */
export function locateCodexTarget(): string | null {
try {
return REGISTRY.find((e) => e.id === "codex")!.locate();
} catch { return null; }
}
/** Every target present on this host, in registry (precedence) order. One bad
* locator never blocks the others (each guarded). */
export function discover(): { id: string; adapter: TargetAdapter }[] {
const out: { id: string; adapter: TargetAdapter }[] = [];
for (const e of REGISTRY) {
try {
const t = e.locate();
if (t) out.push({ id: e.id, adapter: e.make(t) });
} catch { /* one bad locator never blocks the other */ }
}
return out;
}