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.
Files changed (47) hide show
  1. package/dist/cli.mjs +906 -0
  2. package/dist/src/cli/config.d.ts +104 -0
  3. package/dist/src/cli/config.d.ts.map +1 -0
  4. package/dist/src/cli/config.js +397 -0
  5. package/dist/src/cli/config.js.map +1 -0
  6. package/dist/src/cli/install.d.ts +50 -0
  7. package/dist/src/cli/install.d.ts.map +1 -0
  8. package/dist/src/cli/install.js +67 -0
  9. package/dist/src/cli/install.js.map +1 -0
  10. package/dist/src/cli/main.d.ts +19 -0
  11. package/dist/src/cli/main.d.ts.map +1 -0
  12. package/dist/src/cli/main.js +247 -0
  13. package/dist/src/cli/main.js.map +1 -0
  14. package/dist/src/cli/real-fs.d.ts +3 -0
  15. package/dist/src/cli/real-fs.d.ts.map +1 -0
  16. package/dist/src/cli/real-fs.js +32 -0
  17. package/dist/src/cli/real-fs.js.map +1 -0
  18. package/dist/src/cli/registry.d.ts +12 -0
  19. package/dist/src/cli/registry.d.ts.map +1 -0
  20. package/dist/src/cli/registry.js +34 -0
  21. package/dist/src/cli/registry.js.map +1 -0
  22. package/dist/src/cli/spawn.d.ts +29 -0
  23. package/dist/src/cli/spawn.d.ts.map +1 -0
  24. package/dist/src/cli/spawn.js +48 -0
  25. package/dist/src/cli/spawn.js.map +1 -0
  26. package/dist/src/cli/status.d.ts +51 -0
  27. package/dist/src/cli/status.d.ts.map +1 -0
  28. package/dist/src/cli/status.js +214 -0
  29. package/dist/src/cli/status.js.map +1 -0
  30. package/dist/src/cli/uninstall.d.ts +30 -0
  31. package/dist/src/cli/uninstall.d.ts.map +1 -0
  32. package/dist/src/cli/uninstall.js +128 -0
  33. package/dist/src/cli/uninstall.js.map +1 -0
  34. package/dist/src/cli/update.d.ts +65 -0
  35. package/dist/src/cli/update.d.ts.map +1 -0
  36. package/dist/src/cli/update.js +205 -0
  37. package/dist/src/cli/update.js.map +1 -0
  38. package/package.json +7 -2
  39. package/src/cli/config.ts +480 -0
  40. package/src/cli/install.ts +103 -0
  41. package/src/cli/main.ts +308 -0
  42. package/src/cli/real-fs.ts +44 -0
  43. package/src/cli/registry.ts +36 -0
  44. package/src/cli/spawn.ts +76 -0
  45. package/src/cli/status.ts +293 -0
  46. package/src/cli/uninstall.ts +177 -0
  47. package/src/cli/update.ts +254 -0
package/dist/cli.mjs ADDED
@@ -0,0 +1,906 @@
1
+ #!/usr/bin/env node
2
+ import { createRequire } from "node:module";
3
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
4
+
5
+ // src/cli/main.ts
6
+ import { parseArgs } from "node:util";
7
+
8
+ // src/cli/spawn.ts
9
+ import { createRequire as createRequire2 } from "node:module";
10
+ var require2 = createRequire2(import.meta.url);
11
+ async function spawnOpencodePlugin(args, opts = {}) {
12
+ const env = opts.env ?? process.env;
13
+ const stdio = opts.stdio ?? "inherit";
14
+ const spawnFn = opts.spawn ?? defaultSpawn;
15
+ return spawnFn("opencode", ["plugin", ...args], { env, stdio });
16
+ }
17
+ function defaultSpawn(command, args, options) {
18
+ const cp = require2("node:child_process");
19
+ const result = cp.spawnSync(command, args, {
20
+ env: options.env,
21
+ stdio: options.stdio
22
+ });
23
+ return {
24
+ status: result.status,
25
+ stdout: result.stdout?.toString() ?? "",
26
+ stderr: result.stderr?.toString() ?? ""
27
+ };
28
+ }
29
+
30
+ // src/cli/config.ts
31
+ import { join, dirname } from "path";
32
+ import { homedir } from "os";
33
+ var PLUGIN_NAME = "opencode-rules-md";
34
+ var BACKUP_LIMIT = 3;
35
+ var OPENCODE_CONFIG_SUBDIR = "opencode";
36
+ function parseJsonc(content) {
37
+ if (content.trim() === "") {
38
+ return {};
39
+ }
40
+ let out = "";
41
+ let i = 0;
42
+ let depth = 0;
43
+ while (i < content.length) {
44
+ const ch = content[i];
45
+ if (ch === "/" && content[i + 1] === "/" && !isInsideString(out)) {
46
+ let j = i + 2;
47
+ while (j < content.length && content[j] !== `
48
+ `) {
49
+ j++;
50
+ }
51
+ out += " ";
52
+ if (j < content.length && content[j] === `
53
+ `) {
54
+ i = j;
55
+ continue;
56
+ }
57
+ if (j >= content.length) {
58
+ const last = content[content.length - 1];
59
+ if (last === "}" || last === "]") {
60
+ out += last;
61
+ }
62
+ i = j;
63
+ } else {
64
+ i = j - 1;
65
+ }
66
+ continue;
67
+ }
68
+ if (ch === "/" && content[i + 1] === "*" && !isInsideString(out)) {
69
+ let j = i + 2;
70
+ while (j < content.length - 1) {
71
+ if (content[j] === "*" && content[j + 1] === "/") {
72
+ j += 2;
73
+ break;
74
+ }
75
+ j++;
76
+ }
77
+ out += " ";
78
+ i = j;
79
+ continue;
80
+ }
81
+ if (!isInsideString(out)) {
82
+ if (ch === "{")
83
+ depth++;
84
+ else if (ch === "}")
85
+ depth = Math.max(0, depth - 1);
86
+ else if (ch === "[")
87
+ depth++;
88
+ else if (ch === "]")
89
+ depth = Math.max(0, depth - 1);
90
+ }
91
+ out += ch;
92
+ i++;
93
+ }
94
+ let s = out.replace(/,(\s*[}\]])/g, "$1");
95
+ if (depth > 0 && !/[}\]]/.test(s)) {
96
+ s = s.replace(/,(\s*$)/, "") + "}";
97
+ depth = 0;
98
+ }
99
+ if (s.trim() === "") {
100
+ return {};
101
+ }
102
+ try {
103
+ return JSON.parse(s);
104
+ } catch (err) {
105
+ const msg = err.message;
106
+ throw new Error("parseJsonc: invalid JSON after stripping comments - " + msg);
107
+ }
108
+ }
109
+ function isInsideString(s) {
110
+ let inStr = false;
111
+ let strChar = "";
112
+ for (let i = 0;i < s.length; i++) {
113
+ const ch = s[i];
114
+ if (!inStr && (ch === '"' || ch === "'")) {
115
+ inStr = true;
116
+ strChar = ch;
117
+ } else if (inStr && ch === "\\") {
118
+ i++;
119
+ } else if (inStr && ch === strChar) {
120
+ inStr = false;
121
+ strChar = "";
122
+ }
123
+ }
124
+ return inStr;
125
+ }
126
+ function resolveConfigDir(env) {
127
+ const custom = env.OPENCODE_CONFIG_DIR;
128
+ if (custom && custom.trim() !== "") {
129
+ return custom;
130
+ }
131
+ const xdg = env.XDG_CONFIG_HOME;
132
+ if (xdg && xdg.trim() !== "") {
133
+ return join(xdg, OPENCODE_CONFIG_SUBDIR);
134
+ }
135
+ const home = env.HOME;
136
+ if (home && home.trim() !== "") {
137
+ return join(home, ".config", OPENCODE_CONFIG_SUBDIR);
138
+ }
139
+ return join(homedir(), ".config", OPENCODE_CONFIG_SUBDIR);
140
+ }
141
+ function resolveConfigPath(fs, env, basename = "opencode") {
142
+ const dir = resolveConfigDir(env);
143
+ const jsonPath = join(dir, basename + ".json");
144
+ const jsoncPath = join(dir, basename + ".jsonc");
145
+ if (fs.existsSync(jsonPath)) {
146
+ return { path: jsonPath, exists: true };
147
+ }
148
+ if (fs.existsSync(jsoncPath)) {
149
+ return { path: jsoncPath, exists: true };
150
+ }
151
+ return { path: jsonPath, exists: false };
152
+ }
153
+ function normalizePlugin(raw) {
154
+ if (raw == null) {
155
+ return [];
156
+ }
157
+ if (Array.isArray(raw)) {
158
+ return raw.filter((v) => typeof v === "string");
159
+ }
160
+ if (typeof raw === "object") {
161
+ const obj = raw;
162
+ return Object.keys(obj).filter((k) => obj[k] != null && obj[k] !== false);
163
+ }
164
+ if (typeof raw === "string") {
165
+ return [raw];
166
+ }
167
+ return [];
168
+ }
169
+ function readInstalledPlugins(config) {
170
+ const modern = config.data["plugin"];
171
+ if (modern !== undefined) {
172
+ return normalizePlugin(modern);
173
+ }
174
+ return normalizePlugin(config.data["plugins"]);
175
+ }
176
+ function escapeRegex(s) {
177
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
178
+ }
179
+ function matchesPlugin(entry, name = PLUGIN_NAME) {
180
+ if (!entry || typeof entry !== "string")
181
+ return false;
182
+ if (entry === name)
183
+ return true;
184
+ const pattern = new RegExp("^" + escapeRegex(name) + "@");
185
+ return pattern.test(entry);
186
+ }
187
+ function loadGlobalConfig(fs, env, basename = "opencode") {
188
+ const resolved = resolveConfigPath(fs, env, basename);
189
+ if (!resolved.exists) {
190
+ return { path: resolved.path, exists: false, data: {} };
191
+ }
192
+ const raw = fs.readFileSync(resolved.path);
193
+ try {
194
+ const data = parseJsonc(raw);
195
+ return { path: resolved.path, exists: true, data };
196
+ } catch (err) {
197
+ throw new Error(`config file at ${resolved.path} is malformed JSON
198
+ ` + `Fix the JSON error, or delete the file and re-run.
199
+ ` + ` error: ${err.message}`);
200
+ }
201
+ }
202
+ function timestamp() {
203
+ const now = new Date;
204
+ const y = now.getUTCFullYear();
205
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
206
+ const d = String(now.getUTCDate()).padStart(2, "0");
207
+ const hh = String(now.getUTCHours()).padStart(2, "0");
208
+ const mm = String(now.getUTCMinutes()).padStart(2, "0");
209
+ const ss = String(now.getUTCSeconds()).padStart(2, "0");
210
+ return "" + y + m + d + "T" + hh + mm + ss;
211
+ }
212
+ function backupIfWritable(fs, path) {
213
+ if (!fs.existsSync(path)) {
214
+ return;
215
+ }
216
+ try {
217
+ const content = fs.readFileSync(path);
218
+ const dir = dirname(path);
219
+ const segs = path.replace(/\\/g, "/").split("/");
220
+ const base = segs[segs.length - 1] ?? path;
221
+ const dot = base.lastIndexOf(".");
222
+ const name = dot >= 0 ? base.slice(0, dot) : base;
223
+ const backupPath = join(dir, name + ".bak." + timestamp());
224
+ fs.writeFileSync(backupPath, content);
225
+ return backupPath;
226
+ } catch {
227
+ return;
228
+ }
229
+ }
230
+ function rotateBackups(fs, dir, basename, limit = BACKUP_LIMIT) {
231
+ let entries = [];
232
+ try {
233
+ entries = fs.readdirSync(dir);
234
+ } catch {
235
+ return;
236
+ }
237
+ const prefix = basename + ".bak.";
238
+ const backups = entries.filter((e) => e.startsWith(prefix)).map((e) => ({ name: e, path: join(dir, e) })).filter(({ path }) => fs.existsSync(path)).sort((a, b) => a.name.localeCompare(b.name));
239
+ if (backups.length <= limit) {
240
+ return;
241
+ }
242
+ const toDelete = backups.slice(0, backups.length - limit);
243
+ for (const { path } of toDelete) {
244
+ try {
245
+ fs.unlinkSync(path);
246
+ } catch {}
247
+ }
248
+ }
249
+ function writeAtomically(fs, path, content) {
250
+ const dir = dirname(path);
251
+ try {
252
+ fs.mkdirSync(dir, { recursive: true });
253
+ } catch {}
254
+ const segs = path.replace(/\\/g, "/").split("/");
255
+ const base = segs[segs.length - 1] ?? path;
256
+ const dot = base.lastIndexOf(".");
257
+ const name = dot >= 0 ? base.slice(0, dot) : base;
258
+ const ext = dot >= 0 ? base.slice(dot) : "";
259
+ const tmpPath = join(dir, ".tmp." + name + ext);
260
+ let tempCreated = false;
261
+ try {
262
+ fs.writeFileSync(tmpPath, content);
263
+ tempCreated = true;
264
+ fs.renameSync(tmpPath, path);
265
+ } catch (err) {
266
+ if (tempCreated) {
267
+ try {
268
+ fs.unlinkSync(tmpPath);
269
+ } catch {}
270
+ }
271
+ throw err;
272
+ }
273
+ }
274
+
275
+ // src/cli/install.ts
276
+ var DEFAULT_SPECIFIER = PLUGIN_NAME;
277
+ function buildSpecifier(version) {
278
+ const trimmed = version?.trim() ?? "";
279
+ if (!trimmed || trimmed === "latest") {
280
+ return DEFAULT_SPECIFIER;
281
+ }
282
+ return `${PLUGIN_NAME}@${trimmed}`;
283
+ }
284
+ var runInstall = async (opts = {}, _fs, env) => {
285
+ const specifier = buildSpecifier(opts.version);
286
+ const spawnFn = opts.spawn ?? spawnOpencodePlugin;
287
+ const targetEnv = env ?? process.env;
288
+ if (opts.dryRun) {
289
+ console.log(`omd: would run: opencode plugin ${specifier} --global`);
290
+ return { status: "skipped", specifier };
291
+ }
292
+ const result = await spawnFn([specifier, "--global"], { env: targetEnv, stdio: "inherit" });
293
+ if ((result.status ?? 0) !== 0) {
294
+ throw new Error(`opencode plugin ${specifier} --global exited with status ${String(result.status)}`);
295
+ }
296
+ return { status: "wrote", specifier };
297
+ };
298
+
299
+ // src/cli/uninstall.ts
300
+ import { join as join3 } from "path";
301
+
302
+ // src/cli/update.ts
303
+ import { homedir as homedir2 } from "os";
304
+ import { join as join2 } from "path";
305
+
306
+ // src/cli/registry.ts
307
+ var REGISTRY_URL = "https://registry.npmjs.org/opencode-rules-md/latest";
308
+ var fetchLatestVersion = async () => {
309
+ try {
310
+ const res = await fetch(REGISTRY_URL);
311
+ if (!res.ok)
312
+ return null;
313
+ const data = await res.json();
314
+ return data.version ?? null;
315
+ } catch {
316
+ return null;
317
+ }
318
+ };
319
+ var isStale = (installed, latest) => {
320
+ if (!installed || !latest)
321
+ return false;
322
+ return installed !== latest;
323
+ };
324
+
325
+ // src/cli/update.ts
326
+ var CONFIG_BASENAMES = ["opencode", "tui"];
327
+ var PACKAGES_DIR_BASENAME = [".cache", "opencode", "packages"];
328
+ var CACHE_DIR_BASENAME = "opencode-rules-md";
329
+ function resolveHome(env = process.env) {
330
+ return env.HOME ?? homedir2();
331
+ }
332
+ function resolvePackagesDir(env = process.env) {
333
+ return join2(resolveHome(env), ...PACKAGES_DIR_BASENAME);
334
+ }
335
+ function resolveCachePaths(env = process.env, fs) {
336
+ const packagesDir = resolvePackagesDir(env);
337
+ if (fs && fs.existsSync(packagesDir)) {
338
+ try {
339
+ const entries = fs.readdirSync(packagesDir);
340
+ return entries.filter((name) => name === CACHE_DIR_BASENAME || name.startsWith(`${CACHE_DIR_BASENAME}@`)).map((name) => join2(packagesDir, name));
341
+ } catch {
342
+ return [];
343
+ }
344
+ }
345
+ return [
346
+ join2(packagesDir, CACHE_DIR_BASENAME),
347
+ join2(packagesDir, `${CACHE_DIR_BASENAME}@latest`)
348
+ ];
349
+ }
350
+ function findInstalledSpecifier(loaded) {
351
+ for (const cfg of loaded) {
352
+ const plugins = readInstalledPlugins(cfg);
353
+ const match = plugins.find((p) => matchesPlugin(p));
354
+ if (match)
355
+ return match;
356
+ }
357
+ return null;
358
+ }
359
+ function extractVersion(specifier) {
360
+ const at = specifier.lastIndexOf("@");
361
+ if (at < 0 || at === specifier.length - 1)
362
+ return null;
363
+ return specifier.slice(at + 1);
364
+ }
365
+ function purgeDirectory(fs, dirPath) {
366
+ let entries = [];
367
+ try {
368
+ entries = fs.readdirSync(dirPath);
369
+ } catch {
370
+ return;
371
+ }
372
+ for (const entry of entries) {
373
+ const entryPath = join2(dirPath, entry);
374
+ try {
375
+ if (!fs.existsSync(entryPath))
376
+ continue;
377
+ try {
378
+ const subEntries = fs.readdirSync(entryPath);
379
+ if (subEntries.length === 0) {
380
+ fs.rmdirSync(entryPath);
381
+ } else {
382
+ purgeDirectory(fs, entryPath);
383
+ fs.rmdirSync(entryPath);
384
+ }
385
+ } catch {
386
+ fs.unlinkSync(entryPath);
387
+ }
388
+ } catch {}
389
+ }
390
+ try {
391
+ fs.rmdirSync(dirPath);
392
+ } catch {}
393
+ }
394
+ var runUpdate = async (fs, env, log, _error, opts = {}) => {
395
+ const latest = opts.latestVersion !== undefined ? opts.latestVersion : await fetchLatestVersion();
396
+ const cachePaths = resolveCachePaths(env, fs);
397
+ const instruction = "opencode plugin opencode-rules-md --global --force";
398
+ const spawnFn = opts.spawn ?? spawnOpencodePlugin;
399
+ if (latest === null) {
400
+ log("omd: could not determine latest version from npm registry");
401
+ return { status: "unreachable", cachePaths, instruction };
402
+ }
403
+ const configs = CONFIG_BASENAMES.map((basename) => loadGlobalConfig(fs, env, basename));
404
+ const installedSpecifier = findInstalledSpecifier(configs);
405
+ const installedVersion = installedSpecifier ? extractVersion(installedSpecifier) : null;
406
+ if (installedVersion !== null && !isStale(installedVersion, latest)) {
407
+ log(`omd: opencode-rules-md@${installedVersion} is already the latest`);
408
+ return { status: "current", cachePaths, instruction: "" };
409
+ }
410
+ if (opts.dryRun) {
411
+ log("omd: update check (dry-run)");
412
+ log(` latest version: ${latest}`);
413
+ log(` installed version: ${installedVersion ?? "not installed"}`);
414
+ log(` would purge: ${cachePaths.join(", ") || "(none found)"}`);
415
+ log(` would instruct: ${instruction}`);
416
+ return { status: "stale", cachePaths, instruction };
417
+ }
418
+ log(installedVersion === null ? `omd: opencode-rules-md is not installed; registering latest (${latest})` : `omd: opencode-rules-md is stale (installed ${installedVersion}, latest ${latest})`);
419
+ for (const cachePath of cachePaths) {
420
+ try {
421
+ if (fs.existsSync(cachePath)) {
422
+ purgeDirectory(fs, cachePath);
423
+ log(`omd: purged cache ${cachePath}`);
424
+ }
425
+ } catch {}
426
+ }
427
+ const result = await spawnFn(["opencode-rules-md", "--global", "--force"], {
428
+ env: process.env,
429
+ stdio: "inherit"
430
+ });
431
+ if ((result.status ?? 0) !== 0) {
432
+ throw new Error(`opencode plugin opencode-rules-md --global --force exited with status ${String(result.status)}`);
433
+ }
434
+ return { status: "stale", cachePaths, instruction: "" };
435
+ };
436
+
437
+ // src/cli/uninstall.ts
438
+ var CONFIG_BASENAMES2 = ["opencode", "tui"];
439
+ function stripFromData(data) {
440
+ let removed = 0;
441
+ const next = { ...data };
442
+ const currentRaw = next["plugin"] ?? next["plugins"];
443
+ if (currentRaw !== undefined) {
444
+ if (typeof currentRaw === "string") {
445
+ if (currentRaw.startsWith("opencode-rules-md")) {
446
+ delete next["plugin"];
447
+ delete next["plugins"];
448
+ removed += 1;
449
+ }
450
+ } else if (Array.isArray(currentRaw)) {
451
+ const filtered = currentRaw.filter((p) => !(typeof p === "string" && p.startsWith("opencode-rules-md")));
452
+ if (filtered.length !== currentRaw.length) {
453
+ removed += currentRaw.length - filtered.length;
454
+ if (filtered.length === 0) {
455
+ delete next["plugin"];
456
+ delete next["plugins"];
457
+ } else {
458
+ next["plugin"] = filtered;
459
+ delete next["plugins"];
460
+ }
461
+ }
462
+ }
463
+ }
464
+ return { next, removed };
465
+ }
466
+ var runUninstall = (opts = {}, fs, env) => {
467
+ const results = [];
468
+ let anyProcessed = false;
469
+ let purged = false;
470
+ if (opts.purge) {
471
+ const cachePaths = resolveCachePaths(env, fs);
472
+ for (const cachePath of cachePaths) {
473
+ try {
474
+ if (fs.existsSync(cachePath)) {
475
+ purgeDirectory(fs, cachePath);
476
+ purged = true;
477
+ }
478
+ } catch {}
479
+ }
480
+ }
481
+ for (const basename of CONFIG_BASENAMES2) {
482
+ const loaded = loadGlobalConfig(fs, env, basename);
483
+ if (!loaded.exists) {
484
+ results.push({ path: loaded.path, status: "skipped", backup: null });
485
+ continue;
486
+ }
487
+ const { next, removed } = stripFromData(loaded.data);
488
+ if (removed === 0) {
489
+ results.push({ path: loaded.path, status: "skipped", backup: null });
490
+ continue;
491
+ }
492
+ anyProcessed = true;
493
+ const newContent = JSON.stringify(next, null, 2) + `
494
+ `;
495
+ if (opts.dryRun) {
496
+ results.push({ path: loaded.path, status: "wrote", backup: null });
497
+ continue;
498
+ }
499
+ const backup = backupIfWritable(fs, loaded.path);
500
+ if (backup !== undefined) {
501
+ const segs = loaded.path.replace(/\\/g, "/").split("/");
502
+ const base = segs[segs.length - 1] ?? loaded.path;
503
+ const dot = base.lastIndexOf(".");
504
+ const name = dot >= 0 ? base.slice(0, dot) : base;
505
+ const dir = join3(...segs.slice(0, -1));
506
+ rotateBackups(fs, dir, name, 3);
507
+ }
508
+ writeAtomically(fs, loaded.path, newContent);
509
+ results.push({
510
+ path: loaded.path,
511
+ status: "wrote",
512
+ backup: backup ?? null
513
+ });
514
+ }
515
+ return {
516
+ status: anyProcessed || purged ? "wrote" : "skipped",
517
+ results,
518
+ purged
519
+ };
520
+ };
521
+
522
+ // src/cli/status.ts
523
+ import { join as join4, dirname as dirname2, extname } from "path";
524
+ import { homedir as homedir3 } from "os";
525
+ var RULE_DIR_NAME = "opencode-rules-md";
526
+ var MIN_NODE_VERSION = 20;
527
+ var CONFIG_BASENAMES3 = ["opencode", "tui"];
528
+ var runStatus = async (fs, env, log, opts = {}) => {
529
+ const configs = [];
530
+ const latest = opts.latestVersion !== undefined ? opts.latestVersion : await fetchLatestVersion();
531
+ for (const basename of CONFIG_BASENAMES3) {
532
+ const loaded = loadGlobalConfig(fs, env, basename);
533
+ const format = extname(loaded.path);
534
+ const plugins = readInstalledPlugins(loaded);
535
+ const match = plugins.find((p) => matchesPlugin(p)) ?? null;
536
+ const entry = {
537
+ basename,
538
+ path: loaded.path,
539
+ format,
540
+ installed: match,
541
+ notInstalled: !loaded.exists || match === null,
542
+ otherPlugins: plugins.filter((p) => !matchesPlugin(p)),
543
+ latest,
544
+ isLatest: match !== null && latest !== null ? match === `opencode-rules-md@${latest}` : null
545
+ };
546
+ configs.push(entry);
547
+ if (!loaded.exists) {
548
+ log(`omd: ${basename}.json — config not found at ${loaded.path}`);
549
+ continue;
550
+ }
551
+ log(`omd: ${basename}${format} — ${loaded.path}`);
552
+ if (match) {
553
+ const isLatestLabel = entry.isLatest === true ? " (latest)" : entry.isLatest === false ? ` (behind: latest is ${latest ?? "unknown"})` : "";
554
+ log(` opencode-rules-md: ${match}${isLatestLabel}`);
555
+ } else {
556
+ log(" opencode-rules-md: not installed");
557
+ }
558
+ if (entry.otherPlugins.length > 0) {
559
+ log(` other plugins: ${entry.otherPlugins.join(", ")}`);
560
+ }
561
+ }
562
+ return { configs };
563
+ };
564
+ var runDoctor = async (fs, env, log, error, opts = {}) => {
565
+ const issues = [];
566
+ const warnings = [];
567
+ const info = [];
568
+ const nodeVersion = opts.nodeVersion ?? process.version.slice(1);
569
+ const ruleDirExists = opts.ruleDirExists ?? true;
570
+ let hasBun = opts.hasBun;
571
+ if (hasBun === undefined) {
572
+ const pathEnv = (env.PATH ?? "").split(":");
573
+ hasBun = pathEnv.some((p) => {
574
+ try {
575
+ const { existsSync } = __require("node:fs");
576
+ return existsSync(p + "/bun");
577
+ } catch {
578
+ return false;
579
+ }
580
+ });
581
+ }
582
+ log("Checking Node version...");
583
+ const nodeMajor = parseInt(nodeVersion.split(".")[0] ?? "0", 10);
584
+ if (nodeMajor < MIN_NODE_VERSION) {
585
+ issues.push(`Node.js ${nodeVersion} is too old; requires Node >= ${MIN_NODE_VERSION}`);
586
+ error(`[ISSUE] Node.js ${nodeVersion} — requires >= ${MIN_NODE_VERSION}. Download latest from https://nodejs.org`);
587
+ } else {
588
+ log(` Node.js ${nodeVersion} — OK`);
589
+ }
590
+ log("Checking for Bun...");
591
+ if (!hasBun) {
592
+ issues.push("Bun is not on PATH — install from https://bun.sh");
593
+ error("[ISSUE] Bun not found on PATH — install Bun for best performance");
594
+ } else {
595
+ log(" Bun — found on PATH");
596
+ }
597
+ for (const basename of CONFIG_BASENAMES3) {
598
+ log(`Checking ${basename} config...`);
599
+ const loaded = loadGlobalConfig(fs, env, basename);
600
+ if (!loaded.exists) {
601
+ warnings.push(`${basename} config not found at ${loaded.path}`);
602
+ log(` ${basename}: not found (will be created on first install)`);
603
+ continue;
604
+ }
605
+ log(` ${basename}${extname(loaded.path)}: ${loaded.path}`);
606
+ const plugins = readInstalledPlugins(loaded);
607
+ const match = plugins.find((p) => matchesPlugin(p)) ?? null;
608
+ if (match) {
609
+ info.push(`${basename}: opencode-rules-md installed (${match})`);
610
+ log(` opencode-rules-md: ${match}`);
611
+ } else {
612
+ warnings.push(`${basename}: plugin not installed`);
613
+ log(" opencode-rules-md: not installed");
614
+ }
615
+ if (loaded.data["plugins"] !== undefined && loaded.data["plugin"] === undefined) {
616
+ warnings.push(`${basename}: legacy "plugins" field present (should be "plugin") — run "omd uninstall && omd install" to migrate`);
617
+ log(` [WARN] ${basename}: legacy "plugins" field present (should be "plugin")`);
618
+ }
619
+ const pluginField = loaded.data["plugin"] ?? loaded.data["plugins"];
620
+ if (pluginField !== undefined && !Array.isArray(pluginField) && typeof pluginField !== "object" && typeof pluginField !== "string") {
621
+ issues.push(`${basename}: plugin field has invalid type — expected array, object, or string`);
622
+ error(`[ISSUE] ${basename}: invalid plugin shape`);
623
+ } else {
624
+ log(" plugin field: valid");
625
+ }
626
+ }
627
+ log("Checking rule directory...");
628
+ const ruleBase = join4(homedir3(), ".local", "share", RULE_DIR_NAME);
629
+ if (!ruleDirExists) {
630
+ warnings.push(`Rule directory not found at ${ruleBase}`);
631
+ log(` ${ruleBase}: not found (plugin rules not installed — this is optional)`);
632
+ } else {
633
+ log(` ${ruleBase}: exists`);
634
+ }
635
+ log("Checking OpenCode package cache...");
636
+ const cachePaths = resolveCachePaths(env, fs);
637
+ if (cachePaths.length === 0) {
638
+ info.push("No opencode-rules-md cache found under ~/.cache/opencode/packages/");
639
+ log(" no cache entries match opencode-rules-md*");
640
+ } else {
641
+ log(` cache entries: ${cachePaths.join(", ")}`);
642
+ }
643
+ const configDir = env.OPENCODE_CONFIG_DIR ?? join4(homedir3(), ".config", "opencode");
644
+ log("Checking config directory write access...");
645
+ const parentDir = dirname2(configDir);
646
+ if (!fs.existsSync(parentDir)) {
647
+ issues.push(`Config parent directory does not exist: ${parentDir}`);
648
+ error(`[ISSUE] Config dir parent missing: ${parentDir}`);
649
+ } else {
650
+ log(` ${parentDir}: writable`);
651
+ }
652
+ log("");
653
+ if (issues.length === 0) {
654
+ log("omd doctor: all checks passed ✓");
655
+ } else {
656
+ error(`omd doctor: ${issues.length} issue(s) found — fix before using the plugin`);
657
+ error(`Run 'omd --help' for usage information`);
658
+ }
659
+ return {
660
+ ok: issues.length === 0,
661
+ issues,
662
+ warnings,
663
+ info
664
+ };
665
+ };
666
+
667
+ // src/cli/real-fs.ts
668
+ import {
669
+ copyFileSync,
670
+ existsSync,
671
+ mkdirSync,
672
+ readdirSync,
673
+ readFileSync,
674
+ renameSync,
675
+ rmdirSync,
676
+ unlinkSync,
677
+ writeFileSync
678
+ } from "node:fs";
679
+ var createRealFs = () => ({
680
+ readFileSync: (path) => readFileSync(path, "utf8"),
681
+ writeFileSync: (path, content) => {
682
+ writeFileSync(path, content);
683
+ },
684
+ renameSync: (from, to) => {
685
+ renameSync(from, to);
686
+ },
687
+ copyFileSync: (from, to) => {
688
+ copyFileSync(from, to);
689
+ },
690
+ unlinkSync: (path) => {
691
+ unlinkSync(path);
692
+ },
693
+ mkdirSync: (path, opts) => {
694
+ mkdirSync(path, opts);
695
+ },
696
+ readdirSync: (path) => readdirSync(path),
697
+ existsSync: (path) => existsSync(path),
698
+ rmdirSync: (path) => {
699
+ rmdirSync(path);
700
+ }
701
+ });
702
+
703
+ // src/cli/main.ts
704
+ var USAGE = `omd — opencode-rules-md plugin manager
705
+
706
+ Usage:
707
+ omd [command] [options]
708
+
709
+ Commands:
710
+ install Register opencode-rules-md via OpenCode's plugin command
711
+ uninstall Remove opencode-rules-md from both configs and cache
712
+ status Show installed plugin state for each config
713
+ doctor Run health checks for the plugin environment
714
+ update Check for new versions and refresh the install via OpenCode
715
+
716
+ Options:
717
+ --dry-run Show what would be changed without writing
718
+ --version Pin to a specific version (install only)
719
+ --latest Use the latest version (install only)
720
+ --purge Also remove ~/.cache/opencode/packages/opencode-rules-md* (uninstall only)
721
+ --yes Accept all prompts automatically
722
+
723
+ Examples:
724
+ omd # install with defaults (latest)
725
+ omd install # same as bare omd
726
+ omd install --dry-run
727
+ omd install --version 2.0.0
728
+ omd uninstall --purge
729
+ `.trim();
730
+ var printUsage = (stdout) => {
731
+ stdout(USAGE);
732
+ };
733
+ var KNOWN_FLAGS = new Set([
734
+ "--help",
735
+ "--dry-run",
736
+ "--latest",
737
+ "--purge",
738
+ "--yes",
739
+ "--version",
740
+ "-h",
741
+ "-V"
742
+ ]);
743
+ var SHORT_OPTIONS_WITH_VALUE = new Set(["v"]);
744
+ var LONG_OPTIONS_WITH_VALUE = new Set(["version"]);
745
+ function extractCommand(argv) {
746
+ const unknownFlags = [];
747
+ let command = null;
748
+ for (let i = 0;i < argv.length; i++) {
749
+ const arg = argv[i];
750
+ if (arg === "--") {
751
+ break;
752
+ }
753
+ if (!arg.startsWith("-")) {
754
+ if (command === null) {
755
+ command = arg;
756
+ }
757
+ continue;
758
+ }
759
+ if (!KNOWN_FLAGS.has(arg)) {
760
+ unknownFlags.push(arg);
761
+ }
762
+ const flagName = arg.startsWith("--") ? arg.slice(2) : arg.startsWith("-") ? arg.slice(1) : "";
763
+ if (LONG_OPTIONS_WITH_VALUE.has(flagName) || SHORT_OPTIONS_WITH_VALUE.has(flagName)) {
764
+ if (i + 1 < argv.length && !argv[i + 1].startsWith("-")) {
765
+ i++;
766
+ }
767
+ }
768
+ }
769
+ return { command, unknownFlags };
770
+ }
771
+ var runMain = async (opts, argv) => {
772
+ const {
773
+ fs = createRealFs(),
774
+ env = process.env,
775
+ stdout = (s) => console.log(s),
776
+ stderr = (s) => console.error(s)
777
+ } = opts;
778
+ if (argv.includes("--help") || argv.includes("-h")) {
779
+ printUsage(stdout);
780
+ return 0;
781
+ }
782
+ const { command, unknownFlags } = extractCommand(argv);
783
+ if (unknownFlags.length > 0) {
784
+ stderr(`omd: unknown option ${unknownFlags[0]}`);
785
+ stderr("Run 'omd --help' for usage.");
786
+ return 2;
787
+ }
788
+ const resolvedCommand = command ?? "install";
789
+ try {
790
+ switch (resolvedCommand) {
791
+ case "install": {
792
+ const remaining = argv.slice(argv.indexOf("install") + 1 || argv.indexOf(resolvedCommand) + 1);
793
+ const { values } = parseArgs({
794
+ args: remaining,
795
+ allowPositionals: true,
796
+ strict: false,
797
+ options: {
798
+ "dry-run": { type: "boolean", default: false },
799
+ version: { type: "string", default: "" },
800
+ latest: { type: "boolean", default: false },
801
+ yes: { type: "boolean", default: false }
802
+ }
803
+ });
804
+ const installOpts = {
805
+ version: values["latest"] ? "latest" : String(values["version"] ?? ""),
806
+ dryRun: Boolean(values["dry-run"]),
807
+ yes: Boolean(values["yes"]),
808
+ latestVersion: opts.latestVersion,
809
+ ...opts.spawn ? { spawn: opts.spawn } : {}
810
+ };
811
+ const result = await runInstall(installOpts, fs, env);
812
+ if (result.status === "skipped") {
813
+ stdout(`omd: install skipped (dry-run) — would run: opencode plugin ${result.specifier} --global`);
814
+ return 0;
815
+ }
816
+ stdout(`omd: installed via opencode plugin ${result.specifier} --global`);
817
+ return 0;
818
+ }
819
+ case "uninstall": {
820
+ const remaining = argv.slice(argv.indexOf(resolvedCommand) + 1);
821
+ const { values } = parseArgs({
822
+ args: remaining,
823
+ allowPositionals: true,
824
+ strict: false,
825
+ options: {
826
+ "dry-run": { type: "boolean", default: false },
827
+ purge: { type: "boolean", default: false },
828
+ yes: { type: "boolean", default: false }
829
+ }
830
+ });
831
+ const uninstallOpts = {
832
+ purge: Boolean(values["purge"]),
833
+ dryRun: Boolean(values["dry-run"]),
834
+ yes: Boolean(values["yes"])
835
+ };
836
+ const result = runUninstall(uninstallOpts, fs, env);
837
+ if (result.status === "skipped") {
838
+ stdout("omd: not installed (no changes needed)");
839
+ return 0;
840
+ }
841
+ for (const r of result.results) {
842
+ if (r.status === "wrote") {
843
+ stdout(`omd: removed from ${r.path}`);
844
+ } else if (r.status === "skipped") {
845
+ stdout(`omd: ${r.path} — not present`);
846
+ }
847
+ }
848
+ if (result.purged) {
849
+ stdout("omd: cache purged");
850
+ }
851
+ return 0;
852
+ }
853
+ case "status": {
854
+ await runStatus(fs, env, stdout, opts.latestVersion !== undefined ? { latestVersion: opts.latestVersion } : {});
855
+ return 0;
856
+ }
857
+ case "doctor": {
858
+ const docResult = await runDoctor(fs, env, stdout, stderr);
859
+ return docResult.ok ? 0 : 1;
860
+ }
861
+ case "update": {
862
+ const remaining = argv.slice(argv.indexOf(resolvedCommand) + 1);
863
+ const { values } = parseArgs({
864
+ args: remaining,
865
+ allowPositionals: true,
866
+ strict: false,
867
+ options: {
868
+ "dry-run": { type: "boolean", default: false }
869
+ }
870
+ });
871
+ const dryRun = Boolean(values["dry-run"]);
872
+ const updateResult = await runUpdate(fs, env, stdout, stderr, {
873
+ dryRun,
874
+ latestVersion: opts.latestVersion,
875
+ ...opts.spawn ? { spawn: opts.spawn } : {}
876
+ });
877
+ switch (updateResult.status) {
878
+ case "current":
879
+ stdout("omd: already at latest version");
880
+ break;
881
+ case "unreachable":
882
+ stderr("omd: could not reach npm registry — try again later");
883
+ return 1;
884
+ case "stale":
885
+ break;
886
+ }
887
+ return 0;
888
+ }
889
+ default:
890
+ stderr(`omd: unknown command '${resolvedCommand}'`);
891
+ stderr("Run 'omd --help' for usage.");
892
+ return 2;
893
+ }
894
+ } catch (err) {
895
+ stderr(`omd: ${err.message}`);
896
+ return 1;
897
+ }
898
+ };
899
+ var isMainModule = import.meta.url === `file://${process.argv[1]?.replace(/\\/g, "/")}`;
900
+ if (isMainModule) {
901
+ const exitCode = await runMain({}, process.argv.slice(2));
902
+ process.exit(exitCode);
903
+ }
904
+ export {
905
+ runMain
906
+ };