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 CHANGED
@@ -5,8 +5,27 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
5
  // src/cli/main.ts
6
6
  import { parseArgs } from "node:util";
7
7
 
8
- // src/cli/install.ts
9
- import { dirname as dirname2, join as join3 } from "path";
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
+ }
10
29
 
11
30
  // src/cli/config.ts
12
31
  import { join, dirname } from "path";
@@ -147,6 +166,13 @@ function normalizePlugin(raw) {
147
166
  }
148
167
  return [];
149
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
+ }
150
176
  function escapeRegex(s) {
151
177
  return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
152
178
  }
@@ -246,6 +272,37 @@ function writeAtomically(fs, path, content) {
246
272
  }
247
273
  }
248
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
+
249
306
  // src/cli/registry.ts
250
307
  var REGISTRY_URL = "https://registry.npmjs.org/opencode-rules-md/latest";
251
308
  var fetchLatestVersion = async () => {
@@ -266,87 +323,45 @@ var isStale = (installed, latest) => {
266
323
  };
267
324
 
268
325
  // 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");
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();
274
331
  }
275
- function readCacheVersion(fs, cachePath) {
276
- const pkgPath = join2(cachePath, "package.json");
277
- try {
278
- if (!fs.existsSync(pkgPath)) {
279
- return null;
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 [];
280
343
  }
281
- const pkg = JSON.parse(fs.readFileSync(pkgPath));
282
- return typeof pkg.version === "string" ? pkg.version : null;
283
- } catch {
284
- return null;
285
344
  }
345
+ return [
346
+ join2(packagesDir, CACHE_DIR_BASENAME),
347
+ join2(packagesDir, `${CACHE_DIR_BASENAME}@latest`)
348
+ ];
286
349
  }
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"]);
350
+ function findInstalledSpecifier(loaded) {
351
+ for (const cfg of loaded) {
352
+ const plugins = readInstalledPlugins(cfg);
303
353
  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
- }
354
+ if (match)
355
+ return match;
309
356
  }
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
- };
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
+ }
350
365
  function purgeDirectory(fs, dirPath) {
351
366
  let entries = [];
352
367
  try {
@@ -357,18 +372,18 @@ function purgeDirectory(fs, dirPath) {
357
372
  for (const entry of entries) {
358
373
  const entryPath = join2(dirPath, entry);
359
374
  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);
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);
371
384
  }
385
+ } catch {
386
+ fs.unlinkSync(entryPath);
372
387
  }
373
388
  } catch {}
374
389
  }
@@ -376,158 +391,119 @@ function purgeDirectory(fs, dirPath) {
376
391
  fs.rmdirSync(dirPath);
377
392
  } catch {}
378
393
  }
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;
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 };
409
402
  }
410
- const cachePath = resolveCachePath(env);
411
- if (!fs.existsSync(cachePath)) {
412
- return false;
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: "" };
413
409
  }
414
- const cacheVersion = readCacheVersion2(fs, cachePath);
415
- if (cacheVersion === null || cacheVersion !== resolvedVersion) {
416
- purgeDirectory(fs, cachePath);
417
- return true;
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 };
418
417
  }
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);
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}`);
454
424
  }
455
- }
456
- writeAtomically(fs, loaded.path, newContent);
457
- results.push({
458
- path: loaded.path,
459
- status: "wrote",
460
- backup: backup ?? null
461
- });
425
+ } catch {}
462
426
  }
463
- let purged = false;
464
- if (!opts.dryRun) {
465
- purged = purgeCacheIfStale(fs, env, resolvedVersion);
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)}`);
466
433
  }
467
- return {
468
- status: anyProcessed ? "wrote" : "skipped",
469
- results,
470
- purged
471
- };
434
+ return { status: "stale", cachePaths, instruction: "" };
472
435
  };
473
436
 
474
437
  // 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"];
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
+ }
478
466
  var runUninstall = (opts = {}, fs, env) => {
479
467
  const results = [];
480
468
  let anyProcessed = false;
481
469
  let purged = false;
482
470
  if (opts.purge) {
483
- const cachePath = join4(homedir3(), ".cache", "opencode", "node_modules", "opencode-rules-md");
484
- if (fs.existsSync(cachePath)) {
471
+ const cachePaths = resolveCachePaths(env, fs);
472
+ for (const cachePath of cachePaths) {
485
473
  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);
474
+ if (fs.existsSync(cachePath)) {
475
+ purgeDirectory(fs, cachePath);
476
+ purged = true;
499
477
  }
500
- purged = true;
501
478
  } catch {}
502
479
  }
503
480
  }
504
- for (const basename of CONFIG_BASENAMES3) {
481
+ for (const basename of CONFIG_BASENAMES2) {
505
482
  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) {
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) {
509
489
  results.push({ path: loaded.path, status: "skipped", backup: null });
510
490
  continue;
511
491
  }
512
492
  anyProcessed = true;
513
- const newData = { ...loaded.data, plugins: remaining };
514
- const newContent = JSON.stringify(newData, null, 2) + `
493
+ const newContent = JSON.stringify(next, null, 2) + `
515
494
  `;
516
495
  if (opts.dryRun) {
517
496
  results.push({ path: loaded.path, status: "wrote", backup: null });
518
497
  continue;
519
498
  }
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
- }
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);
531
507
  }
532
508
  writeAtomically(fs, loaded.path, newContent);
533
509
  results.push({
@@ -544,29 +520,28 @@ var runUninstall = (opts = {}, fs, env) => {
544
520
  };
545
521
 
546
522
  // 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";
523
+ import { join as join4, dirname as dirname2, extname } from "path";
524
+ import { homedir as homedir3 } from "os";
550
525
  var RULE_DIR_NAME = "opencode-rules-md";
551
526
  var MIN_NODE_VERSION = 20;
552
- var CONFIG_BASENAMES4 = ["opencode", "tui"];
553
- var runStatus = async (fs, env, log) => {
527
+ var CONFIG_BASENAMES3 = ["opencode", "tui"];
528
+ var runStatus = async (fs, env, log, opts = {}) => {
554
529
  const configs = [];
555
- const latest = await fetchLatestVersion();
556
- for (const basename of CONFIG_BASENAMES4) {
530
+ const latest = opts.latestVersion !== undefined ? opts.latestVersion : await fetchLatestVersion();
531
+ for (const basename of CONFIG_BASENAMES3) {
557
532
  const loaded = loadGlobalConfig(fs, env, basename);
558
533
  const format = extname(loaded.path);
559
- const plugins = normalizePlugin(loaded.data["plugins"]);
560
- const match = plugins.find((p) => matchesPlugin(p));
534
+ const plugins = readInstalledPlugins(loaded);
535
+ const match = plugins.find((p) => matchesPlugin(p)) ?? null;
561
536
  const entry = {
562
537
  basename,
563
538
  path: loaded.path,
564
539
  format,
565
- installed: match ?? null,
566
- notInstalled: !loaded.exists ? true : match === undefined || match === null,
540
+ installed: match,
541
+ notInstalled: !loaded.exists || match === null,
567
542
  otherPlugins: plugins.filter((p) => !matchesPlugin(p)),
568
543
  latest,
569
- isLatest: match !== undefined && latest !== null ? match === `opencode-rules-md@${latest}` : null
544
+ isLatest: match !== null && latest !== null ? match === `opencode-rules-md@${latest}` : null
570
545
  };
571
546
  configs.push(entry);
572
547
  if (!loaded.exists) {
@@ -578,7 +553,7 @@ var runStatus = async (fs, env, log) => {
578
553
  const isLatestLabel = entry.isLatest === true ? " (latest)" : entry.isLatest === false ? ` (behind: latest is ${latest ?? "unknown"})` : "";
579
554
  log(` opencode-rules-md: ${match}${isLatestLabel}`);
580
555
  } else {
581
- log(` opencode-rules-md: not installed`);
556
+ log(" opencode-rules-md: not installed");
582
557
  }
583
558
  if (entry.otherPlugins.length > 0) {
584
559
  log(` other plugins: ${entry.otherPlugins.join(", ")}`);
@@ -619,7 +594,7 @@ var runDoctor = async (fs, env, log, error, opts = {}) => {
619
594
  } else {
620
595
  log(" Bun — found on PATH");
621
596
  }
622
- for (const basename of CONFIG_BASENAMES4) {
597
+ for (const basename of CONFIG_BASENAMES3) {
623
598
  log(`Checking ${basename} config...`);
624
599
  const loaded = loadGlobalConfig(fs, env, basename);
625
600
  if (!loaded.exists) {
@@ -628,33 +603,46 @@ var runDoctor = async (fs, env, log, error, opts = {}) => {
628
603
  continue;
629
604
  }
630
605
  log(` ${basename}${extname(loaded.path)}: ${loaded.path}`);
631
- const plugins = normalizePlugin(loaded.data["plugins"]);
632
- const match = plugins.find((p) => matchesPlugin(p));
606
+ const plugins = readInstalledPlugins(loaded);
607
+ const match = plugins.find((p) => matchesPlugin(p)) ?? null;
633
608
  if (match) {
634
- info.push(`${basename}: opencode-rules-md@${match ?? "unknown"} installed`);
609
+ info.push(`${basename}: opencode-rules-md installed (${match})`);
635
610
  log(` opencode-rules-md: ${match}`);
636
611
  } else {
637
612
  warnings.push(`${basename}: plugin not installed`);
638
- log(` opencode-rules-md: not installed`);
613
+ log(" opencode-rules-md: not installed");
639
614
  }
640
- if (loaded.data["plugins"] !== undefined && !Array.isArray(loaded.data["plugins"]) && typeof loaded.data["plugins"] !== "object") {
641
- issues.push(`${basename}: plugins field has invalid typeexpected array or object`);
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`);
642
622
  error(`[ISSUE] ${basename}: invalid plugin shape`);
643
623
  } else {
644
- log(` plugins field: valid`);
624
+ log(" plugin field: valid");
645
625
  }
646
626
  }
647
627
  log("Checking rule directory...");
648
- const ruleBase = join5(homedir4(), ".local", "share", RULE_DIR_NAME);
628
+ const ruleBase = join4(homedir3(), ".local", "share", RULE_DIR_NAME);
649
629
  if (!ruleDirExists) {
650
630
  warnings.push(`Rule directory not found at ${ruleBase}`);
651
631
  log(` ${ruleBase}: not found (plugin rules not installed — this is optional)`);
652
632
  } else {
653
633
  log(` ${ruleBase}: exists`);
654
634
  }
655
- const configDir = env.OPENCODE_CONFIG_DIR ?? join5(homedir4(), ".config", "opencode");
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");
656
644
  log("Checking config directory write access...");
657
- const parentDir = dirname4(configDir);
645
+ const parentDir = dirname2(configDir);
658
646
  if (!fs.existsSync(parentDir)) {
659
647
  issues.push(`Config parent directory does not exist: ${parentDir}`);
660
648
  error(`[ISSUE] Config dir parent missing: ${parentDir}`);
@@ -719,17 +707,17 @@ Usage:
719
707
  omd [command] [options]
720
708
 
721
709
  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
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
727
715
 
728
716
  Options:
729
717
  --dry-run Show what would be changed without writing
730
718
  --version Pin to a specific version (install only)
731
719
  --latest Use the latest version (install only)
732
- --purge Also remove ~/.cache/opencode/node_modules/opencode-rules-md (uninstall only)
720
+ --purge Also remove ~/.cache/opencode/packages/opencode-rules-md* (uninstall only)
733
721
  --yes Accept all prompts automatically
734
722
 
735
723
  Examples:
@@ -803,12 +791,12 @@ var runMain = async (opts, argv) => {
803
791
  case "install": {
804
792
  const remaining = argv.slice(argv.indexOf("install") + 1 || argv.indexOf(resolvedCommand) + 1);
805
793
  const { values } = parseArgs({
806
- argv: remaining,
794
+ args: remaining,
807
795
  allowPositionals: true,
808
796
  strict: false,
809
797
  options: {
810
798
  "dry-run": { type: "boolean", default: false },
811
- version: { type: "string", default: undefined },
799
+ version: { type: "string", default: "" },
812
800
  latest: { type: "boolean", default: false },
813
801
  yes: { type: "boolean", default: false }
814
802
  }
@@ -817,29 +805,21 @@ var runMain = async (opts, argv) => {
817
805
  version: values["latest"] ? "latest" : String(values["version"] ?? ""),
818
806
  dryRun: Boolean(values["dry-run"]),
819
807
  yes: Boolean(values["yes"]),
820
- latestVersion: opts.latestVersion
808
+ latestVersion: opts.latestVersion,
809
+ ...opts.spawn ? { spawn: opts.spawn } : {}
821
810
  };
822
811
  const result = await runInstall(installOpts, fs, env);
823
812
  if (result.status === "skipped") {
824
- stdout("omd: already installed (no changes needed)");
813
+ stdout(`omd: install skipped (dry-run) would run: opencode plugin ${result.specifier} --global`);
825
814
  return 0;
826
815
  }
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
- }
816
+ stdout(`omd: installed via opencode plugin ${result.specifier} --global`);
837
817
  return 0;
838
818
  }
839
819
  case "uninstall": {
840
820
  const remaining = argv.slice(argv.indexOf(resolvedCommand) + 1);
841
821
  const { values } = parseArgs({
842
- argv: remaining,
822
+ args: remaining,
843
823
  allowPositionals: true,
844
824
  strict: false,
845
825
  options: {
@@ -871,7 +851,7 @@ var runMain = async (opts, argv) => {
871
851
  return 0;
872
852
  }
873
853
  case "status": {
874
- await runStatus(fs, env, stdout);
854
+ await runStatus(fs, env, stdout, opts.latestVersion !== undefined ? { latestVersion: opts.latestVersion } : {});
875
855
  return 0;
876
856
  }
877
857
  case "doctor": {
@@ -881,7 +861,7 @@ var runMain = async (opts, argv) => {
881
861
  case "update": {
882
862
  const remaining = argv.slice(argv.indexOf(resolvedCommand) + 1);
883
863
  const { values } = parseArgs({
884
- argv: remaining,
864
+ args: remaining,
885
865
  allowPositionals: true,
886
866
  strict: false,
887
867
  options: {
@@ -889,9 +869,20 @@ var runMain = async (opts, argv) => {
889
869
  }
890
870
  });
891
871
  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");
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;
895
886
  }
896
887
  return 0;
897
888
  }