opencode-rules-md 0.8.5 → 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 +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/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/cli/update.ts
CHANGED
|
@@ -1,85 +1,183 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// src/cli/update.ts — `omd update` command implementation.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
4
|
+
// Like install, update is now a thin wrapper around OpenCode's own CLI.
|
|
5
|
+
// We compare the installed version (read from `data['plugin']` — singular —
|
|
6
|
+
// with a backward-compat fallback to the legacy `data['plugins']`) against
|
|
7
|
+
// the latest npm version. When stale we purge the on-disk cache under
|
|
8
|
+
// ~/.cache/opencode/packages/ (the actual location OpenCode uses) and
|
|
9
|
+
// invoke `opencode plugin opencode-rules-md --global --force` to refresh
|
|
10
|
+
// the registration.
|
|
11
|
+
//
|
|
12
|
+
// The cache purge matters because OpenCode caches the resolved package
|
|
13
|
+
// by specifier — once a cached copy exists, `--force` re-registers without
|
|
14
|
+
// re-fetching unless we first remove the stale entry.
|
|
7
15
|
// ---------------------------------------------------------------------------
|
|
8
16
|
|
|
9
17
|
import { homedir } from 'os';
|
|
10
18
|
import { join } from 'path';
|
|
11
19
|
import { fetchLatestVersion, isStale } from './registry.js';
|
|
12
|
-
import {
|
|
13
|
-
|
|
20
|
+
import {
|
|
21
|
+
loadGlobalConfig,
|
|
22
|
+
matchesPlugin,
|
|
23
|
+
readInstalledPlugins,
|
|
24
|
+
type CliFs,
|
|
25
|
+
type LoadedConfig,
|
|
26
|
+
} from './config.js';
|
|
27
|
+
import { spawnOpencodePlugin } from './spawn.js';
|
|
28
|
+
|
|
29
|
+
const CONFIG_BASENAMES = ['opencode', 'tui'] as const;
|
|
30
|
+
|
|
31
|
+
/** Package directory used by OpenCode to cache plugin installs. */
|
|
32
|
+
export const PACKAGES_DIR_BASENAME = ['.cache', 'opencode', 'packages'] as const;
|
|
33
|
+
|
|
34
|
+
/** Exact cache directory name we look for (bare specifier, no version suffix). */
|
|
35
|
+
export const CACHE_DIR_BASENAME = 'opencode-rules-md';
|
|
14
36
|
|
|
15
|
-
|
|
37
|
+
/**
|
|
38
|
+
* Resolve the user's home directory, honoring a custom HOME env var.
|
|
39
|
+
* Used by both resolveCachePaths and the config loader.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveHome(env: NodeJS.ProcessEnv = process.env): string {
|
|
42
|
+
return env.HOME ?? homedir();
|
|
43
|
+
}
|
|
16
44
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
);
|
|
45
|
+
/**
|
|
46
|
+
* Return the absolute path of the OpenCode packages cache directory.
|
|
47
|
+
*/
|
|
48
|
+
export function resolvePackagesDir(env: NodeJS.ProcessEnv = process.env): string {
|
|
49
|
+
return join(resolveHome(env), ...PACKAGES_DIR_BASENAME);
|
|
50
|
+
}
|
|
24
51
|
|
|
25
|
-
|
|
52
|
+
/**
|
|
53
|
+
* Return the cache directories that match the opencode-rules-md prefix.
|
|
54
|
+
*
|
|
55
|
+
* Real-world layout under ~/.cache/opencode/packages/ looks like:
|
|
56
|
+
* opencode-rules-md/
|
|
57
|
+
* opencode-rules-md@latest/
|
|
58
|
+
* some-other-plugin/
|
|
59
|
+
*
|
|
60
|
+
* We glob-match anything starting with `opencode-rules-md` (exact or with
|
|
61
|
+
* an `@<version>` suffix). When an injected fs is provided we list the
|
|
62
|
+
* directory and filter; without one we return the two most common shapes
|
|
63
|
+
* so callers can pre-check existence cheaply.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveCachePaths(
|
|
66
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
67
|
+
fs?: CliFs,
|
|
68
|
+
): string[] {
|
|
69
|
+
const packagesDir = resolvePackagesDir(env);
|
|
70
|
+
if (fs && fs.existsSync(packagesDir)) {
|
|
71
|
+
try {
|
|
72
|
+
const entries = fs.readdirSync(packagesDir);
|
|
73
|
+
return entries
|
|
74
|
+
.filter(
|
|
75
|
+
(name) => name === CACHE_DIR_BASENAME || name.startsWith(`${CACHE_DIR_BASENAME}@`),
|
|
76
|
+
)
|
|
77
|
+
.map((name) => join(packagesDir, name));
|
|
78
|
+
} catch {
|
|
79
|
+
return [];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// No fs injection — return the conventional candidates. Callers that care
|
|
83
|
+
// about existence should still check with existsSync().
|
|
84
|
+
return [
|
|
85
|
+
join(packagesDir, CACHE_DIR_BASENAME),
|
|
86
|
+
join(packagesDir, `${CACHE_DIR_BASENAME}@latest`),
|
|
87
|
+
];
|
|
88
|
+
}
|
|
26
89
|
|
|
27
90
|
export interface UpdateOptions {
|
|
91
|
+
/** Injected latest version (test seam). Defaults to a real npm lookup. */
|
|
28
92
|
latestVersion?: string | null | undefined;
|
|
93
|
+
/** Print the plan without writing or spawning. */
|
|
29
94
|
dryRun?: boolean;
|
|
95
|
+
/** Injected spawn function (test seam). Defaults to spawnOpencodePlugin. */
|
|
96
|
+
spawn?: typeof spawnOpencodePlugin;
|
|
30
97
|
}
|
|
31
98
|
|
|
32
99
|
export interface UpdateResult {
|
|
33
100
|
status: 'stale' | 'current' | 'unreachable';
|
|
34
|
-
|
|
101
|
+
cachePaths: string[];
|
|
102
|
+
/** Re-install instruction; empty when status === 'current'. */
|
|
35
103
|
instruction: string;
|
|
36
104
|
}
|
|
37
105
|
|
|
38
|
-
|
|
106
|
+
/**
|
|
107
|
+
* Look up the installed specifier across both config files, returning the
|
|
108
|
+
* first one we find. Returns null when neither file registers the plugin.
|
|
109
|
+
*/
|
|
110
|
+
function findInstalledSpecifier(loaded: readonly LoadedConfig[]): string | null {
|
|
111
|
+
for (const cfg of loaded) {
|
|
112
|
+
const plugins = readInstalledPlugins(cfg);
|
|
113
|
+
const match = plugins.find((p) => matchesPlugin(p));
|
|
114
|
+
if (match) return match;
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
39
118
|
|
|
40
119
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
* resolveConfigDir fallback strategy.
|
|
120
|
+
* Extract the version portion of an `opencode-rules-md@<version>` specifier.
|
|
121
|
+
* Returns null when the specifier has no `@version` segment.
|
|
44
122
|
*/
|
|
45
|
-
export function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'opencode',
|
|
50
|
-
'node_modules',
|
|
51
|
-
'opencode-rules-md',
|
|
52
|
-
);
|
|
123
|
+
export function extractVersion(specifier: string): string | null {
|
|
124
|
+
const at = specifier.lastIndexOf('@');
|
|
125
|
+
if (at < 0 || at === specifier.length - 1) return null;
|
|
126
|
+
return specifier.slice(at + 1);
|
|
53
127
|
}
|
|
54
128
|
|
|
55
129
|
/**
|
|
56
|
-
*
|
|
57
|
-
*
|
|
130
|
+
* Recursively delete a directory and all its contents using the injected fs.
|
|
131
|
+
* Best-effort — a failed purge is not fatal; we want the update to keep going.
|
|
58
132
|
*/
|
|
59
|
-
function
|
|
60
|
-
|
|
133
|
+
export function purgeDirectory(fs: CliFs, dirPath: string): void {
|
|
134
|
+
let entries: string[] = [];
|
|
61
135
|
try {
|
|
62
|
-
|
|
63
|
-
return null;
|
|
64
|
-
}
|
|
65
|
-
const pkg = JSON.parse(fs.readFileSync(pkgPath)) as { version?: unknown };
|
|
66
|
-
return typeof pkg.version === 'string' ? pkg.version : null;
|
|
136
|
+
entries = fs.readdirSync(dirPath);
|
|
67
137
|
} catch {
|
|
68
|
-
return
|
|
138
|
+
return;
|
|
69
139
|
}
|
|
70
|
-
}
|
|
71
140
|
|
|
72
|
-
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
const entryPath = join(dirPath, entry);
|
|
143
|
+
try {
|
|
144
|
+
if (!fs.existsSync(entryPath)) continue;
|
|
145
|
+
// Probe to differentiate empty dirs / non-empty dirs / files. The injected
|
|
146
|
+
// fs interface does not expose stat, so we try readdir first and fall back
|
|
147
|
+
// to unlink for leaves.
|
|
148
|
+
try {
|
|
149
|
+
const subEntries = fs.readdirSync(entryPath);
|
|
150
|
+
if (subEntries.length === 0) {
|
|
151
|
+
fs.rmdirSync(entryPath);
|
|
152
|
+
} else {
|
|
153
|
+
purgeDirectory(fs, entryPath);
|
|
154
|
+
fs.rmdirSync(entryPath);
|
|
155
|
+
}
|
|
156
|
+
} catch {
|
|
157
|
+
// Not a directory (or unreadable) — best-effort unlink.
|
|
158
|
+
fs.unlinkSync(entryPath);
|
|
159
|
+
}
|
|
160
|
+
} catch {
|
|
161
|
+
// best-effort per entry
|
|
162
|
+
}
|
|
163
|
+
}
|
|
73
164
|
|
|
74
|
-
|
|
165
|
+
try {
|
|
166
|
+
fs.rmdirSync(dirPath);
|
|
167
|
+
} catch {
|
|
168
|
+
// best-effort
|
|
169
|
+
}
|
|
170
|
+
}
|
|
75
171
|
|
|
76
172
|
/**
|
|
77
|
-
* Run the update command
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
173
|
+
* Run the update command.
|
|
174
|
+
*
|
|
175
|
+
* 1. Resolve the latest published version from npm (or use the injected mock).
|
|
176
|
+
* 2. Read the installed version from the user's opencode config.
|
|
177
|
+
* 3. If unreachable, log and return.
|
|
178
|
+
* 4. If current, log and return.
|
|
179
|
+
* 5. Otherwise: purge stale cache directories, then spawn
|
|
180
|
+
* `opencode plugin opencode-rules-md --global --force`.
|
|
83
181
|
*/
|
|
84
182
|
export const runUpdate = async (
|
|
85
183
|
fs: CliFs,
|
|
@@ -88,133 +186,69 @@ export const runUpdate = async (
|
|
|
88
186
|
_error: (s: string) => void,
|
|
89
187
|
opts: UpdateOptions = {},
|
|
90
188
|
): Promise<UpdateResult> => {
|
|
91
|
-
//
|
|
92
|
-
const latest = opts.latestVersion !== undefined
|
|
93
|
-
? opts.latestVersion
|
|
94
|
-
: await fetchLatestVersion();
|
|
189
|
+
// 1. Resolve latest.
|
|
190
|
+
const latest = opts.latestVersion !== undefined ? opts.latestVersion : await fetchLatestVersion();
|
|
95
191
|
|
|
96
|
-
const
|
|
192
|
+
const cachePaths = resolveCachePaths(env, fs);
|
|
193
|
+
const instruction = 'opencode plugin opencode-rules-md --global --force';
|
|
194
|
+
const spawnFn = opts.spawn ?? spawnOpencodePlugin;
|
|
97
195
|
|
|
196
|
+
// 2. Unreachable.
|
|
98
197
|
if (latest === null) {
|
|
99
198
|
log('omd: could not determine latest version from npm registry');
|
|
100
|
-
return {
|
|
101
|
-
status: 'unreachable',
|
|
102
|
-
cachePath,
|
|
103
|
-
instruction: 'npx opencode-rules-md@latest install',
|
|
104
|
-
};
|
|
199
|
+
return { status: 'unreachable', cachePaths, instruction };
|
|
105
200
|
}
|
|
106
201
|
|
|
107
|
-
//
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const plugins = normalizePlugin(loaded.data['plugins']);
|
|
112
|
-
const match = plugins.find(p => matchesPlugin(p));
|
|
113
|
-
if (match) {
|
|
114
|
-
// Extract version from specifier (e.g. 'opencode-rules-md@2.0.0' -> '2.0.0')
|
|
115
|
-
const atIndex = match.lastIndexOf('@');
|
|
116
|
-
installedVersion = atIndex >= 0 ? match.slice(atIndex + 1) : null;
|
|
117
|
-
break; // use first config that has it
|
|
118
|
-
}
|
|
119
|
-
}
|
|
202
|
+
// 3. Installed version (read both configs).
|
|
203
|
+
const configs = CONFIG_BASENAMES.map((basename) => loadGlobalConfig(fs, env, basename));
|
|
204
|
+
const installedSpecifier = findInstalledSpecifier(configs);
|
|
205
|
+
const installedVersion = installedSpecifier ? extractVersion(installedSpecifier) : null;
|
|
120
206
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
// Check staleness: only stale if installed version differs from latest
|
|
207
|
+
// 4. Current.
|
|
124
208
|
if (installedVersion !== null && !isStale(installedVersion, latest)) {
|
|
125
209
|
log(`omd: opencode-rules-md@${installedVersion} is already the latest`);
|
|
126
|
-
return {
|
|
127
|
-
status: 'current',
|
|
128
|
-
cachePath,
|
|
129
|
-
instruction,
|
|
130
|
-
};
|
|
210
|
+
return { status: 'current', cachePaths, instruction: '' };
|
|
131
211
|
}
|
|
132
212
|
|
|
133
|
-
//
|
|
134
|
-
// version is already equal to the latest resolved version, skip the purge
|
|
135
|
-
// so we do not delete a valid cache. If the version cannot be determined,
|
|
136
|
-
// fall back to purging to be safe.
|
|
137
|
-
const cacheVersion = readCacheVersion(fs, cachePath);
|
|
138
|
-
const shouldPurge = cacheVersion === null || cacheVersion !== latest;
|
|
139
|
-
|
|
213
|
+
// 5. Stale — describe plan.
|
|
140
214
|
if (opts.dryRun) {
|
|
141
|
-
log(
|
|
215
|
+
log('omd: update check (dry-run)');
|
|
142
216
|
log(` latest version: ${latest}`);
|
|
143
217
|
log(` installed version: ${installedVersion ?? 'not installed'}`);
|
|
144
|
-
log(` would purge: ${
|
|
218
|
+
log(` would purge: ${cachePaths.join(', ') || '(none found)'}`);
|
|
145
219
|
log(` would instruct: ${instruction}`);
|
|
146
|
-
return {
|
|
147
|
-
status: 'stale',
|
|
148
|
-
cachePath,
|
|
149
|
-
instruction,
|
|
150
|
-
};
|
|
220
|
+
return { status: 'stale', cachePaths, instruction };
|
|
151
221
|
}
|
|
152
222
|
|
|
153
|
-
|
|
154
|
-
|
|
223
|
+
log(
|
|
224
|
+
installedVersion === null
|
|
225
|
+
? `omd: opencode-rules-md is not installed; registering latest (${latest})`
|
|
226
|
+
: `omd: opencode-rules-md is stale (installed ${installedVersion}, latest ${latest})`,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
// Purge each matching cache directory. Best-effort per path.
|
|
230
|
+
for (const cachePath of cachePaths) {
|
|
155
231
|
try {
|
|
156
232
|
if (fs.existsSync(cachePath)) {
|
|
157
|
-
// Recursively remove contents then the dir itself
|
|
158
233
|
purgeDirectory(fs, cachePath);
|
|
234
|
+
log(`omd: purged cache ${cachePath}`);
|
|
159
235
|
}
|
|
160
236
|
} catch {
|
|
161
|
-
//
|
|
237
|
+
// ignore individual purge failures
|
|
162
238
|
}
|
|
163
239
|
}
|
|
164
240
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
return {
|
|
171
|
-
status: 'stale',
|
|
172
|
-
cachePath,
|
|
173
|
-
instruction,
|
|
174
|
-
};
|
|
175
|
-
};
|
|
176
|
-
|
|
177
|
-
// ─── purgeDirectory ────────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Recursively delete a directory and all its contents using the given fs.
|
|
181
|
-
*/
|
|
182
|
-
export function purgeDirectory(fs: CliFs, dirPath: string): void {
|
|
183
|
-
let entries: string[] = [];
|
|
184
|
-
try {
|
|
185
|
-
entries = fs.readdirSync(dirPath);
|
|
186
|
-
} catch {
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
241
|
+
// Re-register via OpenCode's CLI. Failure here is fatal — surface it.
|
|
242
|
+
const result = await spawnFn(['opencode-rules-md', '--global', '--force'], {
|
|
243
|
+
env: process.env,
|
|
244
|
+
stdio: 'inherit',
|
|
245
|
+
});
|
|
189
246
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Check if it's a directory by trying to read it
|
|
195
|
-
try {
|
|
196
|
-
const subEntries = fs.readdirSync(entryPath);
|
|
197
|
-
if (subEntries.length === 0) {
|
|
198
|
-
// Empty directory
|
|
199
|
-
fs.rmdirSync(entryPath);
|
|
200
|
-
} else {
|
|
201
|
-
// Non-empty directory — recurse
|
|
202
|
-
purgeDirectory(fs, entryPath);
|
|
203
|
-
fs.rmdirSync(entryPath);
|
|
204
|
-
}
|
|
205
|
-
} catch {
|
|
206
|
-
// It's a file — unlink it
|
|
207
|
-
fs.unlinkSync(entryPath);
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
} catch {
|
|
211
|
-
// best-effort per entry
|
|
212
|
-
}
|
|
247
|
+
if ((result.status ?? 0) !== 0) {
|
|
248
|
+
throw new Error(
|
|
249
|
+
`opencode plugin opencode-rules-md --global --force exited with status ${String(result.status)}`,
|
|
250
|
+
);
|
|
213
251
|
}
|
|
214
252
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
} catch {
|
|
218
|
-
// best-effort
|
|
219
|
-
}
|
|
220
|
-
}
|
|
253
|
+
return { status: 'stale', cachePaths, instruction: '' };
|
|
254
|
+
};
|