opencode-rules-md 0.8.5 → 0.9.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/cli.mjs +252 -261
- package/dist/src/cli/config.d.ts +11 -0
- package/dist/src/cli/config.d.ts.map +1 -1
- package/dist/src/cli/config.js +15 -0
- package/dist/src/cli/config.js.map +1 -1
- package/dist/src/cli/install.d.ts +35 -20
- package/dist/src/cli/install.d.ts.map +1 -1
- package/dist/src/cli/install.js +50 -131
- package/dist/src/cli/install.js.map +1 -1
- package/dist/src/cli/main.d.ts +5 -0
- package/dist/src/cli/main.d.ts.map +1 -1
- package/dist/src/cli/main.js +29 -26
- package/dist/src/cli/main.js.map +1 -1
- 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 +15 -8
- package/dist/src/cli/status.d.ts.map +1 -1
- package/dist/src/cli/status.js +63 -31
- package/dist/src/cli/status.js.map +1 -1
- package/dist/src/cli/uninstall.d.ts +5 -2
- package/dist/src/cli/uninstall.d.ts.map +1 -1
- package/dist/src/cli/uninstall.js +78 -51
- package/dist/src/cli/uninstall.js.map +1 -1
- package/dist/src/cli/update.d.ts +50 -16
- package/dist/src/cli/update.d.ts.map +1 -1
- package/dist/src/cli/update.js +156 -124
- package/dist/src/cli/update.js.map +1 -1
- package/dist/src/rule-discovery.d.ts.map +1 -1
- package/dist/src/rule-discovery.js +18 -10
- package/dist/src/rule-discovery.js.map +1 -1
- package/dist/src/rule-filter.d.ts +9 -0
- package/dist/src/rule-filter.d.ts.map +1 -1
- package/dist/src/rule-filter.js +48 -11
- package/dist/src/rule-filter.js.map +1 -1
- package/dist/src/rule-metadata.d.ts +2 -0
- package/dist/src/rule-metadata.d.ts.map +1 -1
- package/dist/src/rule-metadata.js +4 -0
- package/dist/src/rule-metadata.js.map +1 -1
- package/dist/src/runtime-context.d.ts +6 -0
- package/dist/src/runtime-context.d.ts.map +1 -1
- package/dist/src/runtime-context.js +16 -0
- package/dist/src/runtime-context.js.map +1 -1
- package/dist/src/session-store.d.ts.map +1 -1
- package/dist/src/session-store.js +1 -0
- package/dist/src/session-store.js.map +1 -1
- package/dist/tui/index.js +13 -9
- package/dist/tui/index.js.map +4 -4
- package/package.json +1 -1
- package/src/cli/config.ts +19 -0
- package/src/cli/install.ts +66 -174
- package/src/cli/main.ts +34 -25
- package/src/cli/spawn.ts +76 -0
- package/src/cli/status.ts +83 -37
- package/src/cli/uninstall.ts +88 -57
- package/src/cli/update.ts +182 -148
- package/src/rule-discovery.ts +22 -10
- package/src/rule-filter.ts +73 -12
- package/src/rule-metadata.ts +8 -0
- package/src/runtime-context.ts +16 -0
- package/src/session-store.ts +1 -0
package/src/cli/install.ts
CHANGED
|
@@ -1,211 +1,103 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// src/cli/install.ts — `omd install` command implementation.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// `omd install` is a thin convenience wrapper around OpenCode's own
|
|
5
|
+
// `opencode plugin <specifier> --global` command. OpenCode handles the
|
|
6
|
+
// correct config field name (`plugin`, singular), manifest reading, and
|
|
7
|
+
// cache placement under ~/.cache/opencode/packages/. We no longer edit
|
|
8
|
+
// opencode.json or tui.json directly — duplicating that logic was the
|
|
9
|
+
// source of the original bug (we wrote `plugins` plural, which OpenCode
|
|
10
|
+
// silently ignored).
|
|
11
|
+
//
|
|
12
|
+
// The bare specifier `opencode-rules-md` (no `@latest`) is intentional:
|
|
13
|
+
// it lets OpenCode resolve and refresh the package on every invocation
|
|
14
|
+
// without us pinning to a stale version literal.
|
|
10
15
|
// ---------------------------------------------------------------------------
|
|
11
16
|
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
|
|
15
|
-
loadGlobalConfig,
|
|
16
|
-
matchesPlugin,
|
|
17
|
-
normalizePlugin,
|
|
18
|
-
backupIfWritable,
|
|
19
|
-
rotateBackups,
|
|
20
|
-
writeAtomically,
|
|
21
|
-
type CliFs,
|
|
22
|
-
} from './config.js';
|
|
23
|
-
import { fetchLatestVersion } from './registry.js';
|
|
24
|
-
import { purgeDirectory, resolveCachePath } from './update.js';
|
|
17
|
+
import { spawnOpencodePlugin } from './spawn.js';
|
|
18
|
+
import type { CliFs } from './config.js';
|
|
19
|
+
import { PLUGIN_NAME } from './config.js';
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
/** Default base specifier used when the caller does not pin a version. */
|
|
22
|
+
export const DEFAULT_SPECIFIER = PLUGIN_NAME;
|
|
27
23
|
|
|
28
24
|
export interface InstallOptions {
|
|
25
|
+
/** Pin to a specific version, e.g. `"2.0.0"`. Falsy means "use the bare specifier". */
|
|
29
26
|
version?: string;
|
|
27
|
+
/** Run the full pipeline without spawning the child process. */
|
|
30
28
|
dryRun?: boolean;
|
|
29
|
+
/** Reserved for future prompts — currently a no-op. */
|
|
31
30
|
yes?: boolean;
|
|
31
|
+
/** Test hook for resolving the latest version (kept for API compatibility). */
|
|
32
|
+
latestVersion?: string | undefined;
|
|
32
33
|
/**
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* `fetchLatestVersion()` is called against the real npm registry.
|
|
34
|
+
* Test seam: replaces spawnOpencodePlugin. Defaults to the real CLI wrapper.
|
|
35
|
+
* Pass an object with a compatible signature to assert on calls.
|
|
36
36
|
*/
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface InstallResultPerFile {
|
|
41
|
-
path: string;
|
|
42
|
-
status: 'wrote' | 'skipped' | 'error';
|
|
43
|
-
backup: string | null;
|
|
37
|
+
spawn?: typeof spawnOpencodePlugin;
|
|
44
38
|
}
|
|
45
39
|
|
|
46
40
|
export interface InstallResult {
|
|
41
|
+
/** Whether we actually invoked the opencode CLI. */
|
|
47
42
|
status: 'wrote' | 'skipped';
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
purged?: boolean;
|
|
43
|
+
/** The specifier passed to `opencode plugin`. Useful for logging. */
|
|
44
|
+
specifier: string;
|
|
51
45
|
}
|
|
52
46
|
|
|
53
47
|
/**
|
|
54
|
-
*
|
|
48
|
+
* Build the specifier to pass to `opencode plugin`.
|
|
55
49
|
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
* - If the registry is unreachable, fall back to the literal "@latest" tag so
|
|
60
|
-
* the install command never hard-fails due to network issues. This preserves
|
|
61
|
-
* the previous behavior while still allowing the cache check to compare when
|
|
62
|
-
* a concrete version is known.
|
|
63
|
-
*/
|
|
64
|
-
async function resolveVersion(opts: InstallOptions): Promise<string> {
|
|
65
|
-
const raw = opts.version?.trim() ?? '';
|
|
66
|
-
const wantsLatest = raw === '' || raw === 'latest';
|
|
67
|
-
|
|
68
|
-
if (!wantsLatest) {
|
|
69
|
-
return raw;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// Tests can short-circuit the network call via latestVersion.
|
|
73
|
-
if (opts.latestVersion !== undefined) {
|
|
74
|
-
return opts.latestVersion;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
const latest = await fetchLatestVersion();
|
|
78
|
-
return latest ?? 'latest';
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Read the version field from a cached package.json, if present.
|
|
83
|
-
* Returns null when the file is missing or unreadable.
|
|
50
|
+
* Rules:
|
|
51
|
+
* - Empty / unset version → bare `opencode-rules-md` (lets OpenCode refresh).
|
|
52
|
+
* - Any other value → `opencode-rules-md@<version>` (pins the install).
|
|
84
53
|
*/
|
|
85
|
-
function
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
return null;
|
|
90
|
-
}
|
|
91
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath)) as { version?: unknown };
|
|
92
|
-
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
93
|
-
} catch {
|
|
94
|
-
// A broken cache is treated as "version unknown" so it gets purged.
|
|
95
|
-
return null;
|
|
54
|
+
export function buildSpecifier(version: string | undefined): string {
|
|
55
|
+
const trimmed = version?.trim() ?? '';
|
|
56
|
+
if (!trimmed || trimmed === 'latest') {
|
|
57
|
+
return DEFAULT_SPECIFIER;
|
|
96
58
|
}
|
|
59
|
+
return `${PLUGIN_NAME}@${trimmed}`;
|
|
97
60
|
}
|
|
98
61
|
|
|
99
62
|
/**
|
|
100
|
-
*
|
|
101
|
-
* just installed. If the cache version cannot be determined, we err on the
|
|
102
|
-
* side of purging so the next npx invocation fetches a fresh copy.
|
|
103
|
-
*/
|
|
104
|
-
function purgeCacheIfStale(
|
|
105
|
-
fs: CliFs,
|
|
106
|
-
env: NodeJS.ProcessEnv,
|
|
107
|
-
resolvedVersion: string,
|
|
108
|
-
): boolean {
|
|
109
|
-
// The literal "latest" tag is not a comparable version; we only purge when
|
|
110
|
-
// we have a concrete resolved version to compare against.
|
|
111
|
-
if (resolvedVersion === 'latest') {
|
|
112
|
-
return false;
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const cachePath = resolveCachePath(env);
|
|
116
|
-
if (!fs.existsSync(cachePath)) {
|
|
117
|
-
return false;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
const cacheVersion = readCacheVersion(fs, cachePath);
|
|
121
|
-
if (cacheVersion === null || cacheVersion !== resolvedVersion) {
|
|
122
|
-
purgeDirectory(fs, cachePath);
|
|
123
|
-
return true;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
return false;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Install opencode-rules-md into both opencode.json and tui.json configs.
|
|
63
|
+
* Install opencode-rules-md via OpenCode's own plugin command.
|
|
131
64
|
*
|
|
132
65
|
* Options:
|
|
133
|
-
* version — npm version
|
|
134
|
-
* dryRun —
|
|
135
|
-
* yes —
|
|
136
|
-
*
|
|
66
|
+
* version — optional npm version pin
|
|
67
|
+
* dryRun — print the would-be command and skip the spawn
|
|
68
|
+
* yes — reserved
|
|
69
|
+
*
|
|
70
|
+
* Returns:
|
|
71
|
+
* { status: 'skipped', specifier } when dryRun is true
|
|
72
|
+
* { status: 'wrote', specifier } on a clean exit
|
|
73
|
+
*
|
|
74
|
+
* Throws on non-zero exit, so the caller (main.ts) can map it to a CLI
|
|
75
|
+
* failure without us swallowing the error.
|
|
137
76
|
*/
|
|
138
77
|
export const runInstall = async (
|
|
139
78
|
opts: InstallOptions = {},
|
|
140
|
-
|
|
141
|
-
|
|
79
|
+
// The next two parameters are kept for API compatibility with main.ts.
|
|
80
|
+
// `omd install` no longer reads or writes the user's config files directly,
|
|
81
|
+
// so fs/env are no longer consulted here.
|
|
82
|
+
_fs?: CliFs,
|
|
83
|
+
env?: NodeJS.ProcessEnv,
|
|
142
84
|
): Promise<InstallResult> => {
|
|
143
|
-
const
|
|
144
|
-
const
|
|
145
|
-
const
|
|
146
|
-
let anyProcessed = false;
|
|
147
|
-
|
|
148
|
-
for (const basename of CONFIG_BASENAMES) {
|
|
149
|
-
const loaded = loadGlobalConfig(fs, env, basename);
|
|
150
|
-
const plugins = normalizePlugin(loaded.data['plugins']);
|
|
85
|
+
const specifier = buildSpecifier(opts.version);
|
|
86
|
+
const spawnFn = opts.spawn ?? spawnOpencodePlugin;
|
|
87
|
+
const targetEnv = env ?? process.env;
|
|
151
88
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
// No-op if the same specifier is already installed
|
|
156
|
-
if (existingEntry === freshEntry) {
|
|
157
|
-
results.push({ path: loaded.path, status: 'skipped', backup: null });
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
anyProcessed = true;
|
|
162
|
-
|
|
163
|
-
// Build the new plugin list: remove all matching entries, append fresh specifier.
|
|
164
|
-
// Existing non-matching plugins are preserved in their original order.
|
|
165
|
-
const withoutStale = plugins.filter(p => !matchesPlugin(p));
|
|
166
|
-
const newPlugins = [...withoutStale, freshEntry];
|
|
167
|
-
|
|
168
|
-
const newData = { ...loaded.data, plugins: newPlugins };
|
|
169
|
-
const newContent = JSON.stringify(newData, null, 2) + '\n';
|
|
170
|
-
|
|
171
|
-
if (opts.dryRun) {
|
|
172
|
-
results.push({ path: loaded.path, status: 'wrote', backup: null });
|
|
173
|
-
continue;
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
// Backup the existing file if it exists
|
|
177
|
-
let backup: string | undefined;
|
|
178
|
-
if (loaded.exists) {
|
|
179
|
-
backup = backupIfWritable(fs, loaded.path);
|
|
180
|
-
if (backup !== undefined) {
|
|
181
|
-
const dir = dirname(loaded.path);
|
|
182
|
-
const segs = loaded.path.replace(/\\/g, '/').split('/');
|
|
183
|
-
const base = segs[segs.length - 1] ?? loaded.path;
|
|
184
|
-
const dot = base.lastIndexOf('.');
|
|
185
|
-
const name = dot >= 0 ? base.slice(0, dot) : base;
|
|
186
|
-
rotateBackups(fs, dir, name, 3);
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
writeAtomically(fs, loaded.path, newContent);
|
|
191
|
-
results.push({
|
|
192
|
-
path: loaded.path,
|
|
193
|
-
status: 'wrote',
|
|
194
|
-
backup: backup ?? null,
|
|
195
|
-
});
|
|
89
|
+
if (opts.dryRun) {
|
|
90
|
+
console.log(`omd: would run: opencode plugin ${specifier} --global`);
|
|
91
|
+
return { status: 'skipped', specifier };
|
|
196
92
|
}
|
|
197
93
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
94
|
+
const result = await spawnFn([specifier, '--global'], { env: targetEnv, stdio: 'inherit' });
|
|
95
|
+
|
|
96
|
+
if ((result.status ?? 0) !== 0) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`opencode plugin ${specifier} --global exited with status ${String(result.status)}`,
|
|
99
|
+
);
|
|
204
100
|
}
|
|
205
101
|
|
|
206
|
-
return {
|
|
207
|
-
|
|
208
|
-
results,
|
|
209
|
-
purged,
|
|
210
|
-
};
|
|
211
|
-
};
|
|
102
|
+
return { status: 'wrote', specifier };
|
|
103
|
+
};
|
package/src/cli/main.ts
CHANGED
|
@@ -22,17 +22,17 @@ Usage:
|
|
|
22
22
|
omd [command] [options]
|
|
23
23
|
|
|
24
24
|
Commands:
|
|
25
|
-
install
|
|
26
|
-
uninstall
|
|
27
|
-
status
|
|
28
|
-
doctor
|
|
29
|
-
update
|
|
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
30
|
|
|
31
31
|
Options:
|
|
32
32
|
--dry-run Show what would be changed without writing
|
|
33
33
|
--version Pin to a specific version (install only)
|
|
34
34
|
--latest Use the latest version (install only)
|
|
35
|
-
--purge Also remove ~/.cache/opencode/
|
|
35
|
+
--purge Also remove ~/.cache/opencode/packages/opencode-rules-md* (uninstall only)
|
|
36
36
|
--yes Accept all prompts automatically
|
|
37
37
|
|
|
38
38
|
Examples:
|
|
@@ -141,6 +141,11 @@ export interface MainOptions {
|
|
|
141
141
|
* version when "latest" is requested by install or update.
|
|
142
142
|
*/
|
|
143
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;
|
|
144
149
|
}
|
|
145
150
|
|
|
146
151
|
export const runMain = async (
|
|
@@ -180,12 +185,12 @@ export const runMain = async (
|
|
|
180
185
|
argv.indexOf('install') + 1 || argv.indexOf(resolvedCommand) + 1,
|
|
181
186
|
);
|
|
182
187
|
const { values } = parseArgs({
|
|
183
|
-
|
|
188
|
+
args: remaining,
|
|
184
189
|
allowPositionals: true,
|
|
185
190
|
strict: false,
|
|
186
191
|
options: {
|
|
187
192
|
'dry-run': { type: 'boolean', default: false },
|
|
188
|
-
version: { type: 'string', default:
|
|
193
|
+
version: { type: 'string', default: '' },
|
|
189
194
|
latest: { type: 'boolean', default: false },
|
|
190
195
|
yes: { type: 'boolean', default: false },
|
|
191
196
|
},
|
|
@@ -195,29 +200,21 @@ export const runMain = async (
|
|
|
195
200
|
dryRun: Boolean(values['dry-run']),
|
|
196
201
|
yes: Boolean(values['yes']),
|
|
197
202
|
latestVersion: opts.latestVersion,
|
|
203
|
+
...(opts.spawn ? { spawn: opts.spawn } : {}),
|
|
198
204
|
};
|
|
199
205
|
const result = await runInstall(installOpts, fs, env);
|
|
200
206
|
if (result.status === 'skipped') {
|
|
201
|
-
stdout(
|
|
207
|
+
stdout(`omd: install skipped (dry-run) — would run: opencode plugin ${result.specifier} --global`);
|
|
202
208
|
return 0;
|
|
203
209
|
}
|
|
204
|
-
|
|
205
|
-
if (r.status === 'wrote') {
|
|
206
|
-
stdout(`omd: registered in ${r.path}`);
|
|
207
|
-
} else if (r.status === 'skipped') {
|
|
208
|
-
stdout(`omd: ${r.path} — already up to date`);
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
if (result.purged) {
|
|
212
|
-
stdout('omd: cache purged');
|
|
213
|
-
}
|
|
210
|
+
stdout(`omd: installed via opencode plugin ${result.specifier} --global`);
|
|
214
211
|
return 0;
|
|
215
212
|
}
|
|
216
213
|
|
|
217
214
|
case 'uninstall': {
|
|
218
215
|
const remaining = argv.slice(argv.indexOf(resolvedCommand) + 1);
|
|
219
216
|
const { values } = parseArgs({
|
|
220
|
-
|
|
217
|
+
args: remaining,
|
|
221
218
|
allowPositionals: true,
|
|
222
219
|
strict: false,
|
|
223
220
|
options: {
|
|
@@ -250,7 +247,7 @@ export const runMain = async (
|
|
|
250
247
|
}
|
|
251
248
|
|
|
252
249
|
case 'status': {
|
|
253
|
-
await runStatus(fs, env, stdout);
|
|
250
|
+
await runStatus(fs, env, stdout, opts.latestVersion !== undefined ? { latestVersion: opts.latestVersion } : {});
|
|
254
251
|
return 0;
|
|
255
252
|
}
|
|
256
253
|
|
|
@@ -262,7 +259,7 @@ export const runMain = async (
|
|
|
262
259
|
case 'update': {
|
|
263
260
|
const remaining = argv.slice(argv.indexOf(resolvedCommand) + 1);
|
|
264
261
|
const { values } = parseArgs({
|
|
265
|
-
|
|
262
|
+
args: remaining,
|
|
266
263
|
allowPositionals: true,
|
|
267
264
|
strict: false,
|
|
268
265
|
options: {
|
|
@@ -270,9 +267,21 @@ export const runMain = async (
|
|
|
270
267
|
},
|
|
271
268
|
});
|
|
272
269
|
const dryRun = Boolean(values['dry-run']);
|
|
273
|
-
const updateResult = await runUpdate(fs, env, stdout, stderr, {
|
|
274
|
-
|
|
275
|
-
|
|
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;
|
|
276
285
|
}
|
|
277
286
|
return 0;
|
|
278
287
|
}
|
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
|
+
}
|