Skip to content

Commit ff12aa4

Browse files
authored
feat: add OpenCode V2 plugin support with dual V1+V2 entry (#138)
V2 setup via provider transform with V2 model inventory; workit-style packageRoot catalog resolution; @/ import aliases for tests/scripts
1 parent d5110b3 commit ff12aa4

34 files changed

Lines changed: 511 additions & 126 deletions

‎index.ts‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import { resolveApiKey } from "./src/auth.js";
22
import { CommandCodeLanguageModel } from "./src/model.js";
33

4+
// Re-export the plugin definition so a directory plugin entry (<dir>/index.ts)
5+
// loads the dual V1+V2 plugin. No cycle: plugin.ts only imports from src/.
6+
export { default, server } from "./plugin.js";
7+
48
export interface CommandCodeProviderOptions {
59
name?: string;
610
apiKey?: string;

‎package.json‎

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,34 @@
2929
"url": "git+https://github.com/BrainerVirus/opencode-commandcode.git"
3030
},
3131
"files": [
32-
"index.ts",
33-
"plugin.ts",
32+
"dist/",
3433
"models.json",
3534
"manifest.json",
3635
"_version.txt",
3736
"README.md",
3837
"CHANGELOG.md",
38+
"index.ts",
39+
"plugin.ts",
3940
"src/"
4041
],
4142
"type": "module",
42-
"main": "index.ts",
43-
"types": "index.ts",
43+
"main": "./dist/plugin.js",
44+
"types": "./dist/index.d.ts",
4445
"exports": {
4546
".": {
46-
"import": "./index.ts"
47+
"types": "./dist/index.d.ts",
48+
"import": "./dist/plugin.js"
4749
},
4850
"./server": {
49-
"import": "./plugin.ts"
51+
"types": "./dist/plugin.d.ts",
52+
"import": "./dist/plugin.js"
5053
}
5154
},
5255
"publishConfig": {
5356
"access": "public"
5457
},
5558
"scripts": {
59+
"build": "bun scripts/build-plugin.ts && bunx tsc -p tsconfig.json --emitDeclarationOnly",
5660
"lint": "oxlint --deny-warnings src scripts plugin.ts index.ts tests",
5761
"lint:fix": "oxlint --fix src scripts plugin.ts index.ts tests",
5862
"format": "oxfmt src scripts plugin.ts index.ts tests package.json tsconfig.json release.config.cjs",

‎plugin.ts‎

Lines changed: 139 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { readFileSync, existsSync } from "fs";
22
import { homedir } from "os";
3-
import { join, dirname } from "path";
4-
import { fileURLToPath } from "url";
3+
import { join } from "path";
54
import {
65
generateOpencodeModels,
76
loadCatalogFromLocalCommandCode,
@@ -14,11 +13,10 @@ import {
1413
pluginStateDir,
1514
} from "./src/startup.js";
1615
import type { CatalogManifest } from "./src/manifest.js";
16+
import { generateV2Models } from "./src/v2models.js";
17+
import { catalogPaths } from "./src/paths.js";
1718

18-
const __dirname = dirname(fileURLToPath(import.meta.url));
19-
const MODELS_PATH = join(__dirname, "models.json");
20-
const VERSION_PATH = join(__dirname, "_version.txt");
21-
const MANIFEST_PATH = join(__dirname, "manifest.json");
19+
const { models: MODELS_PATH, manifest: MANIFEST_PATH, version: VERSION_PATH } = catalogPaths();
2220

2321
interface PluginFileConfig {
2422
disableModelSync?: boolean;
@@ -72,7 +70,94 @@ function readBundledVersion(): string | null {
7270
}
7371
}
7472

75-
export default async function commandcodePlugin() {
73+
type CatalogLoad = {
74+
models: ModelEntry[];
75+
catalogSource: "bundled" | "cache" | "opt-in-local";
76+
commandCodeVersion: string | null;
77+
degraded: boolean;
78+
degradedReason: string | null;
79+
};
80+
81+
function loadCatalogEntries(): CatalogLoad {
82+
const pluginCfg = loadPluginConfig();
83+
const override =
84+
pluginCfg.commandCodePackagePath?.trim() || process.env.COMMANDCODE_PACKAGE_PATH?.trim() || "";
85+
86+
let models: ModelEntry[] = [];
87+
let catalogSource: CatalogLoad["catalogSource"] = "bundled";
88+
let commandCodeVersion: string | null = null;
89+
let degraded = false;
90+
let degradedReason: string | null = null;
91+
92+
if (override) {
93+
const localCatalog = loadCatalogFromLocalCommandCode({ packagePath: override });
94+
if (localCatalog && localCatalog.models.length > 0) {
95+
models = localCatalog.models;
96+
catalogSource = "opt-in-local";
97+
commandCodeVersion = localCatalog.version;
98+
}
99+
}
100+
101+
if (models.length === 0) {
102+
const bundled = loadBundledModels();
103+
if (bundled) {
104+
models = bundled;
105+
catalogSource = "bundled";
106+
commandCodeVersion = readBundledVersion();
107+
const manifest = readBundledManifest();
108+
if (manifest?.status === "degraded" || manifest?.status === "broken") {
109+
degraded = true;
110+
degradedReason =
111+
manifest.status === "broken"
112+
? "bundled catalog marked broken"
113+
: "bundled catalog has models with no listed price";
114+
}
115+
} else {
116+
const cached = readCatalogCache();
117+
if (cached) {
118+
models = cached;
119+
catalogSource = "cache";
120+
degraded = true;
121+
degradedReason = "bundled models.json unreadable; using last-good cache";
122+
} else {
123+
degraded = true;
124+
degradedReason = "no bundled catalog and no cache";
125+
}
126+
}
127+
}
128+
129+
return { models, catalogSource, commandCodeVersion, degraded, degradedReason };
130+
}
131+
132+
function persistCatalogSideEffects(load: CatalogLoad, debug: boolean): void {
133+
if (load.models.length > 0) {
134+
try {
135+
writeCatalogCache(pluginStateDir(), load.models);
136+
} catch {
137+
// ignore cache write
138+
}
139+
}
140+
141+
const summary = {
142+
catalogSource: load.catalogSource,
143+
commandCodeVersion: load.commandCodeVersion,
144+
modelCount: load.models.length,
145+
reasoningModelCount: load.models.filter((m) => m.reasoning).length,
146+
degraded: load.degraded,
147+
degradedReason: load.degradedReason,
148+
};
149+
try {
150+
writeStartupSummary(pluginStateDir(), summary);
151+
} catch {
152+
// ignore
153+
}
154+
if (debug) {
155+
console.warn("[commandcode]", JSON.stringify(summary));
156+
}
157+
}
158+
159+
/** V1 entrypoint (OpenCode 1.18.29+ also accepts it via `server`). */
160+
export async function server() {
76161
return {
77162
config: async (config: Record<string, unknown>) => {
78163
if (!(config as Record<string, unknown>).provider) {
@@ -85,86 +170,16 @@ export default async function commandcodePlugin() {
85170

86171
const pluginCfg = loadPluginConfig();
87172
const debug = pluginCfg.debugStartupLogs === true;
88-
const override =
89-
pluginCfg.commandCodePackagePath?.trim() ||
90-
process.env.COMMANDCODE_PACKAGE_PATH?.trim() ||
91-
"";
92173

93174
if (!cc.npm) cc.npm = "commandcode-go-opencode-provider";
94175
if (!cc.name) cc.name = "Command Code";
95176
if (!cc.env) cc.env = ["COMMANDCODE_API_KEY"];
96177

97178
if (cc.models) return;
98179

99-
let models: ModelEntry[] = [];
100-
let catalogSource: "bundled" | "cache" | "opt-in-local" = "bundled";
101-
let commandCodeVersion: string | null = null;
102-
let degraded = false;
103-
let degradedReason: string | null = null;
104-
105-
if (override) {
106-
const localCatalog = loadCatalogFromLocalCommandCode({ packagePath: override });
107-
if (localCatalog && localCatalog.models.length > 0) {
108-
models = localCatalog.models;
109-
catalogSource = "opt-in-local";
110-
commandCodeVersion = localCatalog.version;
111-
}
112-
}
113-
114-
if (models.length === 0) {
115-
const bundled = loadBundledModels();
116-
if (bundled) {
117-
models = bundled;
118-
catalogSource = "bundled";
119-
commandCodeVersion = readBundledVersion();
120-
const manifest = readBundledManifest();
121-
if (manifest?.status === "degraded" || manifest?.status === "broken") {
122-
degraded = true;
123-
degradedReason =
124-
manifest.status === "broken"
125-
? "bundled catalog marked broken"
126-
: "bundled catalog has models with no listed price";
127-
}
128-
} else {
129-
const cached = readCatalogCache();
130-
if (cached) {
131-
models = cached;
132-
catalogSource = "cache";
133-
degraded = true;
134-
degradedReason = "bundled models.json unreadable; using last-good cache";
135-
} else {
136-
degraded = true;
137-
degradedReason = "no bundled catalog and no cache";
138-
}
139-
}
140-
}
141-
142-
if (models.length > 0) {
143-
try {
144-
writeCatalogCache(pluginStateDir(), models);
145-
} catch {
146-
// ignore cache write
147-
}
148-
}
149-
150-
cc.models = generateOpencodeModels(models);
151-
152-
const summary = {
153-
catalogSource,
154-
commandCodeVersion,
155-
modelCount: models.length,
156-
reasoningModelCount: models.filter((m) => m.reasoning).length,
157-
degraded,
158-
degradedReason,
159-
};
160-
try {
161-
writeStartupSummary(pluginStateDir(), summary);
162-
} catch {
163-
// ignore
164-
}
165-
if (debug) {
166-
console.warn("[commandcode]", JSON.stringify(summary));
167-
}
180+
const load = loadCatalogEntries();
181+
persistCatalogSideEffects(load, debug);
182+
cc.models = generateOpencodeModels(load.models);
168183
},
169184

170185
auth: {
@@ -195,3 +210,47 @@ export default async function commandcodePlugin() {
195210
},
196211
};
197212
}
213+
214+
/** V2 setup: provider inventory + models through transforms. Auth stays V1-only for now. */
215+
async function setup(ctx: any): Promise<void> {
216+
const pluginCfg = loadPluginConfig();
217+
const debug = pluginCfg.debugStartupLogs === true;
218+
const load = loadCatalogEntries();
219+
persistCatalogSideEffects(load, debug);
220+
const v2models = generateV2Models(load.models);
221+
222+
await ctx.provider.transform((editor: any) => {
223+
const existing = editor.get("commandcode");
224+
if (!existing) {
225+
editor.add({
226+
info: {
227+
id: "commandcode",
228+
name: "Command Code",
229+
activation: "enabled",
230+
package: "aisdk:@ai-sdk/openai-compatible",
231+
settings: {
232+
baseURL: "https://api.commandcode.ai/provider/v1",
233+
apiKey: "{env:COMMANDCODE_API_KEY}",
234+
},
235+
},
236+
models: v2models,
237+
});
238+
return;
239+
}
240+
editor.update("commandcode", (provider: any) => {
241+
provider.name = provider.name ?? "Command Code";
242+
provider.settings = provider.settings ?? {};
243+
if (typeof provider.settings === "object") {
244+
if (!provider.settings.baseURL)
245+
provider.settings.baseURL = "https://api.commandcode.ai/provider/v1";
246+
if (!provider.settings.apiKey) provider.settings.apiKey = "{env:COMMANDCODE_API_KEY}";
247+
}
248+
});
249+
editor.models.set("commandcode", v2models);
250+
});
251+
}
252+
253+
const definition = { id: "commandcode", setup };
254+
255+
// Dual entry: V2 reads default.id/setup, V1 (>=1.18.29) calls default.server().
256+
export default { ...definition, server };

‎scripts/build-plugin.ts‎

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
#!/usr/bin/env bun
2+
// Build the self-contained OpenCode plugin entry (dist/plugin.js).
3+
// Bundles src/entry.ts (plugin default + V1 server + AI-SDK factory) for
4+
// node ESM; the `ai` package stays external and node builtins stay external
5+
// via --target node. All other imports are type-only (erased at build).
6+
import { spawnSync } from "node:child_process";
7+
import { mkdirSync } from "node:fs";
8+
import { join } from "node:path";
9+
10+
const ROOT = join(import.meta.dir, "..");
11+
12+
mkdirSync(join(ROOT, "dist"), { recursive: true });
13+
14+
const build = spawnSync(
15+
"bun",
16+
[
17+
"build",
18+
join(ROOT, "src/entry.ts"),
19+
"--outfile",
20+
join(ROOT, "dist/plugin.js"),
21+
"--target",
22+
"node",
23+
"--format",
24+
"esm",
25+
"--external",
26+
"ai",
27+
],
28+
{ encoding: "utf8", cwd: ROOT },
29+
);
30+
if (build.stdout) process.stdout.write(build.stdout);
31+
if (build.stderr) process.stderr.write(build.stderr);
32+
if (build.status !== 0) {
33+
throw new Error(`bun build src/entry.ts failed (exit ${build.status ?? "unknown"})`);
34+
}
35+
console.log("opencode: built dist/plugin.js");

‎scripts/catalog-release-notes.ts‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
#!/usr/bin/env bun
22
import { readFileSync } from "fs";
33
import { join } from "path";
4-
import { catalogReleaseNotesFromFiles } from "../src/catalog-release-notes.js";
5-
import type { CatalogManifest } from "../src/manifest.js";
6-
import type { PricedModel } from "../src/catalog-release-notes.js";
4+
import { catalogReleaseNotesFromFiles } from "@/src/catalog-release-notes.js";
5+
import type { CatalogManifest } from "@/src/manifest.js";
6+
import type { PricedModel } from "@/src/catalog-release-notes.js";
77

88
const ROOT = join(import.meta.dir, "..");
99
const manifest = JSON.parse(readFileSync(join(ROOT, "manifest.json"), "utf-8")) as CatalogManifest;

‎scripts/catalog-sync-ci.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@
22
import { existsSync, readFileSync } from "fs";
33
import { join } from "path";
44
import { execSync } from "child_process";
5-
import { catalogBreakTitle, renderCatalogBreakBody } from "../src/catalog-break.js";
6-
import { decideCatalogSync } from "../src/publish-policy.js";
5+
import { catalogBreakTitle, renderCatalogBreakBody } from "@/src/catalog-break.js";
6+
import { decideCatalogSync } from "@/src/publish-policy.js";
77
import { npmLatestVersion, npmPackageVersions } from "./npm-registry.js";
88

99
const ROOT = join(import.meta.dir, "..");

‎scripts/cut-changelog.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#!/usr/bin/env bun
22
import { readFileSync, writeFileSync } from "node:fs";
33
import { join } from "node:path";
4-
import { cutKeepAChangelog } from "../src/cut-changelog.ts";
4+
import { cutKeepAChangelog } from "@/src/cut-changelog.ts";
55

66
const version = process.argv[2];
77
const date = process.argv[3] ?? new Date().toISOString().slice(0, 10);

‎scripts/prepare-release-manifest.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
// untouched; the post-release sync PR commits the stamped versions to main.
55
import { existsSync, readFileSync } from "fs";
66
import { join } from "path";
7-
import { withPluginVersion, writeManifest, type CatalogManifest } from "../src/manifest.js";
7+
import { withPluginVersion, writeManifest, type CatalogManifest } from "@/src/manifest.js";
88

99
const ROOT = join(import.meta.dir, "..");
1010

0 commit comments

Comments
 (0)