opencode-rules-md 0.8.4 → 0.8.5

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