@opencode-compat/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/setup.js ADDED
@@ -0,0 +1,333 @@
1
+ /**
2
+ * Layer A setup — write `@opencode-ai/{plugin,sdk}` install-tree overrides
3
+ * that resolve to `@opencode-compat/facade-*`, then Option B provider shims.
4
+ */
5
+ import { detect, facadeOverrides, } from "@opencode-compat/profile";
6
+ import { spawnSync } from "node:child_process";
7
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { dirname, join, resolve } from "node:path";
9
+ import { fileURLToPath } from "node:url";
10
+ import { setupProviderShims, } from "./provider-shim";
11
+ function packagesRootFromHere() {
12
+ // packages/cli/src → packages/
13
+ return resolve(dirname(fileURLToPath(import.meta.url)), "../..");
14
+ }
15
+ function resolveFileOverrides() {
16
+ const root = packagesRootFromHere();
17
+ const plugin = join(root, "facade-plugin");
18
+ const sdk = join(root, "facade-sdk");
19
+ if (!existsSync(join(plugin, "package.json")))
20
+ return undefined;
21
+ if (!existsSync(join(sdk, "package.json")))
22
+ return undefined;
23
+ return {
24
+ "@opencode-ai/plugin": `file:${plugin}`,
25
+ "@opencode-ai/sdk": `file:${sdk}`,
26
+ };
27
+ }
28
+ function resolveMode(requested, version) {
29
+ if (requested === "npm") {
30
+ return { mode: "npm", overrides: facadeOverrides(version) };
31
+ }
32
+ if (requested === "file") {
33
+ const file = resolveFileOverrides();
34
+ if (!file) {
35
+ throw new Error("setup --mode file: could not locate sibling facade-plugin / facade-sdk packages");
36
+ }
37
+ return { mode: "file", overrides: file };
38
+ }
39
+ // auto: prefer local file: when running from a checkout / packed sibling layout
40
+ const file = resolveFileOverrides();
41
+ if (file)
42
+ return { mode: "file", overrides: file };
43
+ return { mode: "npm", overrides: facadeOverrides(version) };
44
+ }
45
+ function readPackageJson(path) {
46
+ if (!existsSync(path))
47
+ return undefined;
48
+ try {
49
+ return JSON.parse(readFileSync(path, "utf8"));
50
+ }
51
+ catch (err) {
52
+ const message = err instanceof Error ? err.message : String(err);
53
+ throw new Error(`invalid package.json at ${path}: ${message}`);
54
+ }
55
+ }
56
+ function mergeOverrides(pkg, overrides) {
57
+ const next = { ...pkg };
58
+ const prev = { ...(pkg.overrides ?? {}) };
59
+ let changed = false;
60
+ for (const [key, value] of Object.entries(overrides)) {
61
+ if (prev[key] !== value) {
62
+ prev[key] = value;
63
+ changed = true;
64
+ }
65
+ }
66
+ next.overrides = prev;
67
+ if (!next.name) {
68
+ next.name = "opencode-compat-overrides";
69
+ changed = true;
70
+ }
71
+ if (next.private !== true) {
72
+ next.private = true;
73
+ changed = true;
74
+ }
75
+ return { pkg: next, changed };
76
+ }
77
+ function writePackageJson(path, pkg, dryRun) {
78
+ if (dryRun)
79
+ return;
80
+ mkdirSync(dirname(path), { recursive: true });
81
+ writeFileSync(path, `${JSON.stringify(pkg, null, 2)}\n`, "utf8");
82
+ }
83
+ function listChildPackageJson(dir) {
84
+ if (!existsSync(dir))
85
+ return [];
86
+ const out = [];
87
+ for (const ent of readdirSync(dir, { withFileTypes: true })) {
88
+ if (!ent.isDirectory())
89
+ continue;
90
+ if (ent.name === "node_modules" || ent.name.startsWith("."))
91
+ continue;
92
+ const pkgPath = join(dir, ent.name, "package.json");
93
+ if (existsSync(pkgPath))
94
+ out.push(pkgPath);
95
+ }
96
+ return out;
97
+ }
98
+ function shouldReify(pkgDir, target, reify) {
99
+ if (reify === false)
100
+ return false;
101
+ if (!target.changed)
102
+ return false;
103
+ if (reify === "force" || reify === true)
104
+ return true;
105
+ // auto: only when the tree was already installed (common after `mimo plugin` / `kilo plugin`)
106
+ return existsSync(join(pkgDir, "node_modules"));
107
+ }
108
+ function reifyInstallTree(pkgDir, dryRun) {
109
+ if (dryRun)
110
+ return { ok: true };
111
+ const result = spawnSync("npm", ["install", "--ignore-scripts", "--no-fund", "--no-audit"], {
112
+ cwd: pkgDir,
113
+ encoding: "utf8",
114
+ env: process.env,
115
+ });
116
+ if (result.status === 0)
117
+ return { ok: true };
118
+ const detail = [result.stderr, result.stdout]
119
+ .filter(Boolean)
120
+ .join("\n")
121
+ .trim();
122
+ return {
123
+ ok: false,
124
+ error: detail || `npm install exited ${result.status ?? "unknown"}`,
125
+ };
126
+ }
127
+ /** Apply Layer A overrides into one or more package.json files under an install tree. */
128
+ export function setup(options = {}) {
129
+ const version = options.version ?? "0.1.0";
130
+ const dryRun = options.dryRun ?? false;
131
+ const deep = options.deep ?? true;
132
+ const reify = options.reify ?? "auto";
133
+ const providerShim = options.providerShim ?? true;
134
+ const env = options.host
135
+ ? {
136
+ ...(options.detectOptions?.env ?? process.env),
137
+ OPENCODE_COMPAT_HOST: options.host,
138
+ }
139
+ : options.detectOptions?.env;
140
+ const detected = detect({
141
+ ...options.detectOptions,
142
+ env,
143
+ });
144
+ const { mode, overrides } = resolveMode(options.mode, version);
145
+ const dir = options.dir?.trim() ||
146
+ detected.profile.paths.pluginInstallDir ||
147
+ undefined;
148
+ if (!dir) {
149
+ return {
150
+ ok: false,
151
+ host: detected.id,
152
+ source: detected.source,
153
+ dir: "",
154
+ mode,
155
+ overrides,
156
+ targets: [],
157
+ message: detected.message ??
158
+ `setup: could not resolve plugin install dir for host=${detected.id}; pass --dir`,
159
+ };
160
+ }
161
+ if (!detected.supported && !options.dir?.trim()) {
162
+ return {
163
+ ok: false,
164
+ host: detected.id,
165
+ source: detected.source,
166
+ dir,
167
+ mode,
168
+ overrides,
169
+ targets: [],
170
+ message: detected.message ??
171
+ `setup: host=${detected.id} is unsupported; pass --dir to write overrides anyway`,
172
+ };
173
+ }
174
+ const resolvedDir = resolve(dir);
175
+ const targets = [];
176
+ const rootPkgPath = join(resolvedDir, "package.json");
177
+ const existingRoot = readPackageJson(rootPkgPath);
178
+ const createdRoot = !existingRoot;
179
+ const rootBase = existingRoot ?? {
180
+ name: "opencode-compat-overrides",
181
+ private: true,
182
+ };
183
+ const mergedRoot = mergeOverrides(rootBase, overrides);
184
+ if (mergedRoot.changed || createdRoot) {
185
+ writePackageJson(rootPkgPath, mergedRoot.pkg, dryRun);
186
+ targets.push({
187
+ path: rootPkgPath,
188
+ created: createdRoot,
189
+ changed: true,
190
+ });
191
+ }
192
+ else {
193
+ targets.push({ path: rootPkgPath, created: false, changed: false });
194
+ }
195
+ if (deep) {
196
+ for (const childPath of listChildPackageJson(resolvedDir)) {
197
+ const child = readPackageJson(childPath);
198
+ if (!child)
199
+ continue;
200
+ const merged = mergeOverrides(child, overrides);
201
+ if (!merged.changed) {
202
+ targets.push({ path: childPath, created: false, changed: false });
203
+ continue;
204
+ }
205
+ writePackageJson(childPath, merged.pkg, dryRun);
206
+ targets.push({ path: childPath, created: false, changed: true });
207
+ }
208
+ }
209
+ // MiMo/Kilo install each npm plugin in an isolated child dir
210
+ // (`name@version/`). Parent overrides alone do not apply — reify children
211
+ // after patching so `@opencode-ai/*` links to facades.
212
+ for (const target of targets) {
213
+ const pkgDir = dirname(target.path);
214
+ if (!shouldReify(pkgDir, target, reify))
215
+ continue;
216
+ // Skip empty root marker trees with no dependencies to install.
217
+ const pkg = readPackageJson(target.path);
218
+ const hasDeps = pkg &&
219
+ Object.keys({
220
+ ...pkg.dependencies,
221
+ ...pkg.devDependencies,
222
+ ...pkg.optionalDependencies,
223
+ ...pkg.peerDependencies,
224
+ }).length > 0;
225
+ if (!hasDeps && !existsSync(join(pkgDir, "node_modules")))
226
+ continue;
227
+ const outcome = reifyInstallTree(pkgDir, dryRun);
228
+ target.reified = outcome.ok;
229
+ if (!outcome.ok)
230
+ target.reifyError = outcome.error;
231
+ }
232
+ // Option B must run *after* reify — npm install restores stock package
233
+ // files from the tarball and would wipe in-place entry shims.
234
+ let providerShimResult;
235
+ if (providerShim) {
236
+ providerShimResult = setupProviderShims({
237
+ dir: resolvedDir,
238
+ hostHint: detected.id,
239
+ dryRun,
240
+ });
241
+ }
242
+ const changed = targets.filter((t) => t.changed).length;
243
+ const reified = targets.filter((t) => t.reified).length;
244
+ const reifyFailed = targets.filter((t) => t.reifyError);
245
+ const action = dryRun ? "dry-run" : "wrote";
246
+ const message = [
247
+ `setup ${action}: host=${detected.id} mode=${mode} dir=${resolvedDir}`,
248
+ `overrides: ${Object.keys(overrides).join(", ")}`,
249
+ `targets: ${changed} changed / ${targets.length} scanned` +
250
+ (reified || reifyFailed.length
251
+ ? `; reified ${reified}` +
252
+ (reifyFailed.length ? `, ${reifyFailed.length} reify failed` : "")
253
+ : ""),
254
+ providerShimResult ? providerShimResult.message : "provider-shim: skipped (--no-provider-shim)",
255
+ "Note: listing OCP in host plugin config alone does not intercept @opencode-ai/* imports.",
256
+ "Note: on MiMo/Kilo, re-run `ocp setup` after installing plugins (isolated per-plugin trees).",
257
+ "Note: provider shims are install-tree only (in-place entry); re-apply after plugin upgrade/reify.",
258
+ ...reifyFailed.map((t) => `reify failed: ${dirname(t.path)} — ${t.reifyError}`),
259
+ ].join("\n");
260
+ return {
261
+ ok: reifyFailed.length === 0 && (providerShimResult?.ok ?? true),
262
+ host: detected.id,
263
+ source: detected.source,
264
+ dir: resolvedDir,
265
+ mode,
266
+ overrides,
267
+ targets,
268
+ providerShim: providerShimResult,
269
+ message,
270
+ };
271
+ }
272
+ export function parseSetupArgs(rest) {
273
+ const opts = {
274
+ dryRun: false,
275
+ deep: true,
276
+ reify: "auto",
277
+ providerShim: true,
278
+ help: false,
279
+ };
280
+ for (let i = 0; i < rest.length; i++) {
281
+ const arg = rest[i];
282
+ if (arg === "--help" || arg === "-h")
283
+ opts.help = true;
284
+ else if (arg === "--dir")
285
+ opts.dir = rest[++i];
286
+ else if (arg?.startsWith("--dir="))
287
+ opts.dir = arg.slice("--dir=".length);
288
+ else if (arg === "--host")
289
+ opts.host = rest[++i];
290
+ else if (arg?.startsWith("--host="))
291
+ opts.host = arg.slice("--host=".length);
292
+ else if (arg === "--version")
293
+ opts.version = rest[++i];
294
+ else if (arg?.startsWith("--version=")) {
295
+ opts.version = arg.slice("--version=".length);
296
+ }
297
+ else if (arg === "--mode") {
298
+ const value = rest[++i];
299
+ if (value === "npm" || value === "file" || value === "auto")
300
+ opts.mode = value;
301
+ }
302
+ else if (arg?.startsWith("--mode=")) {
303
+ const value = arg.slice("--mode=".length);
304
+ if (value === "npm" || value === "file" || value === "auto")
305
+ opts.mode = value;
306
+ }
307
+ else if (arg === "--dry-run")
308
+ opts.dryRun = true;
309
+ else if (arg === "--deep")
310
+ opts.deep = true;
311
+ else if (arg === "--no-deep")
312
+ opts.deep = false;
313
+ else if (arg === "--reify")
314
+ opts.reify = "force";
315
+ else if (arg === "--no-reify")
316
+ opts.reify = false;
317
+ else if (arg?.startsWith("--reify=")) {
318
+ const value = arg.slice("--reify=".length);
319
+ if (value === "auto" || value === "force")
320
+ opts.reify = value;
321
+ else if (value === "false" || value === "0" || value === "no")
322
+ opts.reify = false;
323
+ else if (value === "true" || value === "1" || value === "yes")
324
+ opts.reify = "force";
325
+ }
326
+ else if (arg === "--provider-shim")
327
+ opts.providerShim = true;
328
+ else if (arg === "--no-provider-shim")
329
+ opts.providerShim = false;
330
+ }
331
+ return opts;
332
+ }
333
+ //# sourceMappingURL=setup.js.map
package/package.json ADDED
@@ -0,0 +1,61 @@
1
+ {
2
+ "name": "@opencode-compat/cli",
3
+ "version": "0.1.0",
4
+ "description": "OCP compat doctor, Plugin×Host×Tier matrix runner, Layer A setup/overrides, and migrate-zcode companion CLI",
5
+ "type": "module",
6
+ "license": "MPL-2.0",
7
+ "author": "oakimov",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/oakimov/opencode-plugin-compat.git",
11
+ "directory": "packages/cli"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "main": "./dist/index.js",
17
+ "types": "./dist/index.d.ts",
18
+ "bin": {
19
+ "opencode-compat": "./bin/compat.ts",
20
+ "compat": "./bin/compat.ts"
21
+ },
22
+ "exports": {
23
+ ".": {
24
+ "types": "./dist/index.d.ts",
25
+ "import": "./dist/index.js"
26
+ }
27
+ },
28
+ "files": [
29
+ "bin",
30
+ "dist",
31
+ "LICENSE",
32
+ "README.md",
33
+ "!dist/**/*.map"
34
+ ],
35
+ "scripts": {
36
+ "build": "bunx tsc -p tsconfig.json",
37
+ "typecheck": "bunx tsc -p tsconfig.json --noEmit",
38
+ "prepack": "bunx tsc -p tsconfig.json"
39
+ },
40
+ "dependencies": {
41
+ "@opencode-compat/profile": "0.1.0",
42
+ "@opencode-compat/adapter": "0.1.0",
43
+ "@opencode-compat/migrate-zcode": "0.1.0"
44
+ },
45
+ "devDependencies": {
46
+ "typescript": "^5.8.3"
47
+ },
48
+ "bugs": {
49
+ "url": "https://github.com/oakimov/opencode-plugin-compat/issues"
50
+ },
51
+ "homepage": "https://github.com/oakimov/opencode-plugin-compat/tree/main/packages/cli#readme",
52
+ "keywords": [
53
+ "opencode",
54
+ "ocp",
55
+ "cli",
56
+ "doctor"
57
+ ],
58
+ "engines": {
59
+ "bun": ">=1.2.0"
60
+ }
61
+ }