opencode-rules-md 0.8.4 → 0.8.6
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/cli.mjs +906 -0
- package/dist/src/cli/config.d.ts +104 -0
- package/dist/src/cli/config.d.ts.map +1 -0
- package/dist/src/cli/config.js +397 -0
- package/dist/src/cli/config.js.map +1 -0
- package/dist/src/cli/install.d.ts +50 -0
- package/dist/src/cli/install.d.ts.map +1 -0
- package/dist/src/cli/install.js +67 -0
- package/dist/src/cli/install.js.map +1 -0
- package/dist/src/cli/main.d.ts +19 -0
- package/dist/src/cli/main.d.ts.map +1 -0
- package/dist/src/cli/main.js +247 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/real-fs.d.ts +3 -0
- package/dist/src/cli/real-fs.d.ts.map +1 -0
- package/dist/src/cli/real-fs.js +32 -0
- package/dist/src/cli/real-fs.js.map +1 -0
- package/dist/src/cli/registry.d.ts +12 -0
- package/dist/src/cli/registry.d.ts.map +1 -0
- package/dist/src/cli/registry.js +34 -0
- package/dist/src/cli/registry.js.map +1 -0
- package/dist/src/cli/spawn.d.ts +29 -0
- package/dist/src/cli/spawn.d.ts.map +1 -0
- package/dist/src/cli/spawn.js +48 -0
- package/dist/src/cli/spawn.js.map +1 -0
- package/dist/src/cli/status.d.ts +51 -0
- package/dist/src/cli/status.d.ts.map +1 -0
- package/dist/src/cli/status.js +214 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/uninstall.d.ts +30 -0
- package/dist/src/cli/uninstall.d.ts.map +1 -0
- package/dist/src/cli/uninstall.js +128 -0
- package/dist/src/cli/uninstall.js.map +1 -0
- package/dist/src/cli/update.d.ts +65 -0
- package/dist/src/cli/update.d.ts.map +1 -0
- package/dist/src/cli/update.js +205 -0
- package/dist/src/cli/update.js.map +1 -0
- package/package.json +7 -2
- package/src/cli/config.ts +480 -0
- package/src/cli/install.ts +103 -0
- package/src/cli/main.ts +308 -0
- package/src/cli/real-fs.ts +44 -0
- package/src/cli/registry.ts +36 -0
- package/src/cli/spawn.ts +76 -0
- package/src/cli/status.ts +293 -0
- package/src/cli/uninstall.ts +177 -0
- package/src/cli/update.ts +254 -0
package/src/cli/main.ts
ADDED
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// src/cli/main.ts — `omd` CLI entry point.
|
|
3
|
+
//
|
|
4
|
+
// Command dispatch using node:util.parseArgs.
|
|
5
|
+
// Bare `omd` → install (default).
|
|
6
|
+
// Exit codes: 0 success/no-op, 1 health failure, 2 usage error.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import { parseArgs } from 'node:util';
|
|
10
|
+
import { runInstall, type InstallOptions } from './install.js';
|
|
11
|
+
import { runUninstall, type UninstallOptions } from './uninstall.js';
|
|
12
|
+
import { runStatus, runDoctor } from './status.js';
|
|
13
|
+
import { runUpdate } from './update.js';
|
|
14
|
+
import { createRealFs } from './real-fs.js';
|
|
15
|
+
import type { CliFs } from './config.js';
|
|
16
|
+
|
|
17
|
+
// ─── Usage ───────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
const USAGE = `omd — opencode-rules-md plugin manager
|
|
20
|
+
|
|
21
|
+
Usage:
|
|
22
|
+
omd [command] [options]
|
|
23
|
+
|
|
24
|
+
Commands:
|
|
25
|
+
install Register opencode-rules-md via OpenCode's plugin command
|
|
26
|
+
uninstall Remove opencode-rules-md from both configs and cache
|
|
27
|
+
status Show installed plugin state for each config
|
|
28
|
+
doctor Run health checks for the plugin environment
|
|
29
|
+
update Check for new versions and refresh the install via OpenCode
|
|
30
|
+
|
|
31
|
+
Options:
|
|
32
|
+
--dry-run Show what would be changed without writing
|
|
33
|
+
--version Pin to a specific version (install only)
|
|
34
|
+
--latest Use the latest version (install only)
|
|
35
|
+
--purge Also remove ~/.cache/opencode/packages/opencode-rules-md* (uninstall only)
|
|
36
|
+
--yes Accept all prompts automatically
|
|
37
|
+
|
|
38
|
+
Examples:
|
|
39
|
+
omd # install with defaults (latest)
|
|
40
|
+
omd install # same as bare omd
|
|
41
|
+
omd install --dry-run
|
|
42
|
+
omd install --version 2.0.0
|
|
43
|
+
omd uninstall --purge
|
|
44
|
+
`.trim();
|
|
45
|
+
|
|
46
|
+
const printUsage = (stdout: (s: string) => void): void => {
|
|
47
|
+
stdout(USAGE);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// ─── Manual argv parsing — extract command before passing to parseArgs ────────
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Known option flags (long-form).
|
|
54
|
+
* We detect unknown flags by scanning argv for anything starting with `-` or `--`
|
|
55
|
+
* that is NOT in this set.
|
|
56
|
+
*/
|
|
57
|
+
const KNOWN_FLAGS = new Set([
|
|
58
|
+
'--help', '--dry-run', '--latest', '--purge', '--yes', '--version',
|
|
59
|
+
'-h', '-V',
|
|
60
|
+
]);
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Known short options that take a value.
|
|
64
|
+
* e.g. `--version 2.0.0` → values.version = '2.0.0'
|
|
65
|
+
*/
|
|
66
|
+
const SHORT_OPTIONS_WITH_VALUE = new Set(['v']);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Known long options that take a value.
|
|
70
|
+
*/
|
|
71
|
+
const LONG_OPTIONS_WITH_VALUE = new Set(['version']);
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Extract the command name (first non-option positional) from argv.
|
|
75
|
+
* Also returns unknown flags found before the command.
|
|
76
|
+
*
|
|
77
|
+
* Examples:
|
|
78
|
+
* [] → { command: 'install', args: [], unknownFlags: [] }
|
|
79
|
+
* ['install'] → { command: 'install', args: [], unknownFlags: [] }
|
|
80
|
+
* ['notacommand'] → { command: 'notacommand', args: [], unknownFlags: [] }
|
|
81
|
+
* ['install', '--dry-run'] → { command: 'install', args: ['--dry-run'], unknownFlags: [] }
|
|
82
|
+
* ['install', '--unknown-opt'] → { command: 'install', args: ['--unknown-opt'], unknownFlags: ['--unknown-opt'] }
|
|
83
|
+
*/
|
|
84
|
+
function extractCommand(
|
|
85
|
+
argv: string[],
|
|
86
|
+
): { command: string | null; unknownFlags: string[] } {
|
|
87
|
+
const unknownFlags: string[] = [];
|
|
88
|
+
let command: string | null = null;
|
|
89
|
+
|
|
90
|
+
for (let i = 0; i < argv.length; i++) {
|
|
91
|
+
const arg = argv[i]!;
|
|
92
|
+
|
|
93
|
+
if (arg === '--') {
|
|
94
|
+
// Argument separator — remaining args are positional
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (!arg.startsWith('-')) {
|
|
99
|
+
// First non-option is the command
|
|
100
|
+
if (command === null) {
|
|
101
|
+
command = arg;
|
|
102
|
+
}
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// It's an option — check if known
|
|
107
|
+
if (!KNOWN_FLAGS.has(arg)) {
|
|
108
|
+
unknownFlags.push(arg);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Skip the value for options that take one
|
|
112
|
+
const flagName = arg.startsWith('--')
|
|
113
|
+
? arg.slice(2)
|
|
114
|
+
: arg.startsWith('-')
|
|
115
|
+
? arg.slice(1)
|
|
116
|
+
: '';
|
|
117
|
+
|
|
118
|
+
if (
|
|
119
|
+
LONG_OPTIONS_WITH_VALUE.has(flagName) ||
|
|
120
|
+
SHORT_OPTIONS_WITH_VALUE.has(flagName)
|
|
121
|
+
) {
|
|
122
|
+
// Skip the next argv element if it's not an option itself
|
|
123
|
+
if (i + 1 < argv.length && !argv[i + 1]!.startsWith('-')) {
|
|
124
|
+
i++;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { command, unknownFlags };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ─── Main dispatch ───────────────────────────────────────────────────────────
|
|
133
|
+
|
|
134
|
+
export interface MainOptions {
|
|
135
|
+
fs?: CliFs;
|
|
136
|
+
env?: NodeJS.ProcessEnv;
|
|
137
|
+
stdout?: (s: string) => void;
|
|
138
|
+
stderr?: (s: string) => void;
|
|
139
|
+
/**
|
|
140
|
+
* Optional injection point for tests: pretend the npm registry returned this
|
|
141
|
+
* version when "latest" is requested by install or update.
|
|
142
|
+
*/
|
|
143
|
+
latestVersion?: string;
|
|
144
|
+
/**
|
|
145
|
+
* Test seam: replace the `spawnOpencodePlugin` function. Lets tests verify
|
|
146
|
+
* which CLI args the installer would have invoked without touching disk.
|
|
147
|
+
*/
|
|
148
|
+
spawn?: typeof import('./spawn.js').spawnOpencodePlugin;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export const runMain = async (
|
|
152
|
+
opts: MainOptions,
|
|
153
|
+
argv: string[],
|
|
154
|
+
): Promise<number> => {
|
|
155
|
+
const {
|
|
156
|
+
fs = createRealFs(),
|
|
157
|
+
env = process.env,
|
|
158
|
+
stdout = (s: string) => console.log(s),
|
|
159
|
+
stderr = (s: string) => console.error(s),
|
|
160
|
+
} = opts;
|
|
161
|
+
|
|
162
|
+
// Intercept --help/-h before anything else
|
|
163
|
+
if (argv.includes('--help') || argv.includes('-h')) {
|
|
164
|
+
printUsage(stdout);
|
|
165
|
+
return 0;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Extract command and detect unknown flags before parseArgs
|
|
169
|
+
const { command, unknownFlags } = extractCommand(argv);
|
|
170
|
+
|
|
171
|
+
if (unknownFlags.length > 0) {
|
|
172
|
+
stderr(`omd: unknown option ${unknownFlags[0]}`);
|
|
173
|
+
stderr("Run 'omd --help' for usage.");
|
|
174
|
+
return 2;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Default to install for bare `omd`
|
|
178
|
+
const resolvedCommand = command ?? 'install';
|
|
179
|
+
|
|
180
|
+
try {
|
|
181
|
+
switch (resolvedCommand) {
|
|
182
|
+
case 'install': {
|
|
183
|
+
// Remaining argv for parseArgs to extract options
|
|
184
|
+
const remaining = argv.slice(
|
|
185
|
+
argv.indexOf('install') + 1 || argv.indexOf(resolvedCommand) + 1,
|
|
186
|
+
);
|
|
187
|
+
const { values } = parseArgs({
|
|
188
|
+
args: remaining,
|
|
189
|
+
allowPositionals: true,
|
|
190
|
+
strict: false,
|
|
191
|
+
options: {
|
|
192
|
+
'dry-run': { type: 'boolean', default: false },
|
|
193
|
+
version: { type: 'string', default: '' },
|
|
194
|
+
latest: { type: 'boolean', default: false },
|
|
195
|
+
yes: { type: 'boolean', default: false },
|
|
196
|
+
},
|
|
197
|
+
});
|
|
198
|
+
const installOpts: InstallOptions = {
|
|
199
|
+
version: values['latest'] ? 'latest' : String(values['version'] ?? ''),
|
|
200
|
+
dryRun: Boolean(values['dry-run']),
|
|
201
|
+
yes: Boolean(values['yes']),
|
|
202
|
+
latestVersion: opts.latestVersion,
|
|
203
|
+
...(opts.spawn ? { spawn: opts.spawn } : {}),
|
|
204
|
+
};
|
|
205
|
+
const result = await runInstall(installOpts, fs, env);
|
|
206
|
+
if (result.status === 'skipped') {
|
|
207
|
+
stdout(`omd: install skipped (dry-run) — would run: opencode plugin ${result.specifier} --global`);
|
|
208
|
+
return 0;
|
|
209
|
+
}
|
|
210
|
+
stdout(`omd: installed via opencode plugin ${result.specifier} --global`);
|
|
211
|
+
return 0;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case 'uninstall': {
|
|
215
|
+
const remaining = argv.slice(argv.indexOf(resolvedCommand) + 1);
|
|
216
|
+
const { values } = parseArgs({
|
|
217
|
+
args: remaining,
|
|
218
|
+
allowPositionals: true,
|
|
219
|
+
strict: false,
|
|
220
|
+
options: {
|
|
221
|
+
'dry-run': { type: 'boolean', default: false },
|
|
222
|
+
purge: { type: 'boolean', default: false },
|
|
223
|
+
yes: { type: 'boolean', default: false },
|
|
224
|
+
},
|
|
225
|
+
});
|
|
226
|
+
const uninstallOpts: UninstallOptions = {
|
|
227
|
+
purge: Boolean(values['purge']),
|
|
228
|
+
dryRun: Boolean(values['dry-run']),
|
|
229
|
+
yes: Boolean(values['yes']),
|
|
230
|
+
};
|
|
231
|
+
const result = runUninstall(uninstallOpts, fs, env);
|
|
232
|
+
if (result.status === 'skipped') {
|
|
233
|
+
stdout('omd: not installed (no changes needed)');
|
|
234
|
+
return 0;
|
|
235
|
+
}
|
|
236
|
+
for (const r of result.results) {
|
|
237
|
+
if (r.status === 'wrote') {
|
|
238
|
+
stdout(`omd: removed from ${r.path}`);
|
|
239
|
+
} else if (r.status === 'skipped') {
|
|
240
|
+
stdout(`omd: ${r.path} — not present`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (result.purged) {
|
|
244
|
+
stdout('omd: cache purged');
|
|
245
|
+
}
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
case 'status': {
|
|
250
|
+
await runStatus(fs, env, stdout, opts.latestVersion !== undefined ? { latestVersion: opts.latestVersion } : {});
|
|
251
|
+
return 0;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
case 'doctor': {
|
|
255
|
+
const docResult = await runDoctor(fs, env, stdout, stderr);
|
|
256
|
+
return docResult.ok ? 0 : 1;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
case 'update': {
|
|
260
|
+
const remaining = argv.slice(argv.indexOf(resolvedCommand) + 1);
|
|
261
|
+
const { values } = parseArgs({
|
|
262
|
+
args: remaining,
|
|
263
|
+
allowPositionals: true,
|
|
264
|
+
strict: false,
|
|
265
|
+
options: {
|
|
266
|
+
'dry-run': { type: 'boolean', default: false },
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
const dryRun = Boolean(values['dry-run']);
|
|
270
|
+
const updateResult = await runUpdate(fs, env, stdout, stderr, {
|
|
271
|
+
dryRun,
|
|
272
|
+
latestVersion: opts.latestVersion,
|
|
273
|
+
...(opts.spawn ? { spawn: opts.spawn } : {}),
|
|
274
|
+
});
|
|
275
|
+
switch (updateResult.status) {
|
|
276
|
+
case 'current':
|
|
277
|
+
stdout('omd: already at latest version');
|
|
278
|
+
break;
|
|
279
|
+
case 'unreachable':
|
|
280
|
+
stderr('omd: could not reach npm registry — try again later');
|
|
281
|
+
return 1;
|
|
282
|
+
case 'stale':
|
|
283
|
+
// runUpdate already logged the purge / reinstall outcome.
|
|
284
|
+
break;
|
|
285
|
+
}
|
|
286
|
+
return 0;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
default:
|
|
290
|
+
stderr(`omd: unknown command '${resolvedCommand}'`);
|
|
291
|
+
stderr("Run 'omd --help' for usage.");
|
|
292
|
+
return 2;
|
|
293
|
+
}
|
|
294
|
+
} catch (err) {
|
|
295
|
+
stderr(`omd: ${(err as Error).message}`);
|
|
296
|
+
return 1;
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
|
|
300
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
301
|
+
// Only run when executed directly (not imported as a module in tests).
|
|
302
|
+
const isMainModule =
|
|
303
|
+
import.meta.url === `file://${process.argv[1]?.replace(/\\/g, '/')}`;
|
|
304
|
+
|
|
305
|
+
if (isMainModule) {
|
|
306
|
+
const exitCode = await runMain({}, process.argv.slice(2));
|
|
307
|
+
process.exit(exitCode);
|
|
308
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// src/cli/real-fs.ts — Default `CliFs` adapter backed by `node:fs`.
|
|
3
|
+
//
|
|
4
|
+
// The CLI commands default to the real filesystem in production. Tests inject
|
|
5
|
+
// an in-memory adapter to keep everything deterministic and fast. All methods
|
|
6
|
+
// are sync — the CLI is short-lived and never benefits from async I/O.
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
copyFileSync,
|
|
11
|
+
existsSync,
|
|
12
|
+
mkdirSync,
|
|
13
|
+
readdirSync,
|
|
14
|
+
readFileSync,
|
|
15
|
+
renameSync,
|
|
16
|
+
rmdirSync,
|
|
17
|
+
unlinkSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import type { CliFs } from "./config";
|
|
21
|
+
|
|
22
|
+
export const createRealFs = (): CliFs => ({
|
|
23
|
+
readFileSync: (path) => readFileSync(path, "utf8"),
|
|
24
|
+
writeFileSync: (path, content) => {
|
|
25
|
+
writeFileSync(path, content);
|
|
26
|
+
},
|
|
27
|
+
renameSync: (from, to) => {
|
|
28
|
+
renameSync(from, to);
|
|
29
|
+
},
|
|
30
|
+
copyFileSync: (from, to) => {
|
|
31
|
+
copyFileSync(from, to);
|
|
32
|
+
},
|
|
33
|
+
unlinkSync: (path) => {
|
|
34
|
+
unlinkSync(path);
|
|
35
|
+
},
|
|
36
|
+
mkdirSync: (path, opts) => {
|
|
37
|
+
mkdirSync(path, opts);
|
|
38
|
+
},
|
|
39
|
+
readdirSync: (path) => readdirSync(path),
|
|
40
|
+
existsSync: (path) => existsSync(path),
|
|
41
|
+
rmdirSync: (path) => {
|
|
42
|
+
rmdirSync(path);
|
|
43
|
+
},
|
|
44
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// src/cli/registry.ts — npm registry helpers for `omd`.
|
|
3
|
+
//
|
|
4
|
+
// Fetches the latest published version of `opencode-rules-md` from the npm
|
|
5
|
+
// registry. Uses the native `fetch()` API (Node 20+) — no extra dependencies.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
|
|
8
|
+
const REGISTRY_URL = "https://registry.npmjs.org/opencode-rules-md/latest";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Fetch the latest version string from the npm registry.
|
|
12
|
+
* Returns `null` when the registry is unreachable or the response is
|
|
13
|
+
* malformed — callers treat `null` as "can't determine, don't block".
|
|
14
|
+
*/
|
|
15
|
+
export const fetchLatestVersion = async (): Promise<string | null> => {
|
|
16
|
+
try {
|
|
17
|
+
const res = await fetch(REGISTRY_URL);
|
|
18
|
+
if (!res.ok) return null;
|
|
19
|
+
const data = (await res.json()) as { version?: string };
|
|
20
|
+
return data.version ?? null;
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Return whether the installed version is older than the latest.
|
|
28
|
+
* Unknown versions (null on either side) are never treated as stale.
|
|
29
|
+
*/
|
|
30
|
+
export const isStale = (
|
|
31
|
+
installed: string | null,
|
|
32
|
+
latest: string | null,
|
|
33
|
+
): boolean => {
|
|
34
|
+
if (!installed || !latest) return false;
|
|
35
|
+
return installed !== latest;
|
|
36
|
+
};
|
package/src/cli/spawn.ts
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// src/cli/spawn.ts — thin wrapper around `opencode plugin`.
|
|
3
|
+
//
|
|
4
|
+
// Why a wrapper? Two reasons:
|
|
5
|
+
// 1. OpenCode owns the schema for `data['plugin']` (singular) and the cache
|
|
6
|
+
// layout under ~/.cache/opencode/packages/. Re-implementing that logic
|
|
7
|
+
// drift-prone (the bug we are fixing). Delegating to OpenCode's own CLI
|
|
8
|
+
// keeps us correct by construction.
|
|
9
|
+
// 2. Tests need a deterministic, non-blocking seam. Defaulting to
|
|
10
|
+
// spawnSync with `stdio: 'inherit'` lets the real CLI talk directly to
|
|
11
|
+
// the user's terminal; tests inject a stub that returns canned output.
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
15
|
+
|
|
16
|
+
const require = createRequire(import.meta.url);
|
|
17
|
+
|
|
18
|
+
export interface SpawnResult {
|
|
19
|
+
status: number | null;
|
|
20
|
+
stdout: string;
|
|
21
|
+
stderr: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface SpawnOptions {
|
|
25
|
+
env: NodeJS.ProcessEnv;
|
|
26
|
+
stdio?: 'pipe' | 'inherit';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type SpawnFn = (command: string, args: string[], options: SpawnOptions) => SpawnResult;
|
|
30
|
+
|
|
31
|
+
export interface SpawnOpencodePluginOptions {
|
|
32
|
+
/** Injected spawn function for tests. Defaults to node:child_process.spawnSync. */
|
|
33
|
+
spawn?: SpawnFn;
|
|
34
|
+
/** Environment variables passed to the child process. Defaults to process.env. */
|
|
35
|
+
env?: NodeJS.ProcessEnv;
|
|
36
|
+
/** stdio mode for the child process. 'inherit' forwards output to the parent. */
|
|
37
|
+
stdio?: 'pipe' | 'inherit';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Run `opencode plugin <args...>` and return the exit status plus captured
|
|
42
|
+
* stdout/stderr (only populated when `stdio: 'pipe'`).
|
|
43
|
+
*
|
|
44
|
+
* The default implementation uses spawnSync so the call blocks until the
|
|
45
|
+
* child process exits. This keeps `omd install` simple — the user's shell
|
|
46
|
+
* stays put until registration completes, then returns control with a
|
|
47
|
+
* non-zero status on failure.
|
|
48
|
+
*/
|
|
49
|
+
export async function spawnOpencodePlugin(
|
|
50
|
+
args: string[],
|
|
51
|
+
opts: SpawnOpencodePluginOptions = {},
|
|
52
|
+
): Promise<SpawnResult> {
|
|
53
|
+
const env = opts.env ?? process.env;
|
|
54
|
+
const stdio = opts.stdio ?? 'inherit';
|
|
55
|
+
const spawnFn = opts.spawn ?? defaultSpawn;
|
|
56
|
+
return spawnFn('opencode', ['plugin', ...args], { env, stdio });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Default spawn implementation backed by node:child_process.spawnSync.
|
|
61
|
+
*
|
|
62
|
+
* The require is loaded lazily so test doubles can replace the spawn
|
|
63
|
+
* function entirely without ever touching node:child_process.
|
|
64
|
+
*/
|
|
65
|
+
function defaultSpawn(command: string, args: string[], options: SpawnOptions): SpawnResult {
|
|
66
|
+
const cp = require('node:child_process') as typeof import('node:child_process');
|
|
67
|
+
const result = cp.spawnSync(command, args, {
|
|
68
|
+
env: options.env,
|
|
69
|
+
stdio: options.stdio,
|
|
70
|
+
});
|
|
71
|
+
return {
|
|
72
|
+
status: result.status,
|
|
73
|
+
stdout: result.stdout?.toString() ?? '',
|
|
74
|
+
stderr: result.stderr?.toString() ?? '',
|
|
75
|
+
};
|
|
76
|
+
}
|