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
@@ -0,0 +1,146 @@
1
+ // ---------------------------------------------------------------------------
2
+ // src/cli/uninstall.ts — `omd uninstall` command implementation.
3
+ //
4
+ // Dual-config uninstall: removes opencode-rules-md entries from both configs.
5
+ // --purge removes ONLY ~/.cache/opencode/node_modules/opencode-rules-md and
6
+ // NEVER touches rule directories.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import { dirname, join } from 'path';
10
+ import { homedir } from 'os';
11
+ import {
12
+ loadGlobalConfig,
13
+ matchesPlugin,
14
+ normalizePlugin,
15
+ backupIfWritable,
16
+ rotateBackups,
17
+ writeAtomically,
18
+ type CliFs,
19
+ } from './config.js';
20
+
21
+ export const CONFIG_BASENAMES = ['opencode', 'tui'] as const;
22
+
23
+ export interface UninstallOptions {
24
+ purge?: boolean;
25
+ dryRun?: boolean;
26
+ yes?: boolean;
27
+ }
28
+
29
+ export interface UninstallResultPerFile {
30
+ path: string;
31
+ status: 'wrote' | 'skipped' | 'error';
32
+ backup: string | null;
33
+ }
34
+
35
+ export interface UninstallResult {
36
+ status: 'wrote' | 'skipped';
37
+ results: UninstallResultPerFile[];
38
+ purged: boolean;
39
+ }
40
+
41
+ /**
42
+ * Uninstall opencode-rules-md from both opencode.json and tui.json configs.
43
+ *
44
+ * Options:
45
+ * purge — also remove ~/.cache/opencode/node_modules/opencode-rules-md
46
+ * dryRun — run full pipeline without writing to disk
47
+ * yes — accepted for future prompts, no-op here
48
+ */
49
+ export const runUninstall = (
50
+ opts: UninstallOptions = {},
51
+ fs: CliFs,
52
+ env: NodeJS.ProcessEnv,
53
+ ): UninstallResult => {
54
+ const results: UninstallResultPerFile[] = [];
55
+ let anyProcessed = false;
56
+ let purged = false;
57
+
58
+ // ── Purge cache if requested ────────────────────────────────────────────
59
+ if (opts.purge) {
60
+ const cachePath = join(
61
+ homedir(),
62
+ '.cache',
63
+ 'opencode',
64
+ 'node_modules',
65
+ 'opencode-rules-md',
66
+ );
67
+ if (fs.existsSync(cachePath)) {
68
+ try {
69
+ // Check if it's a directory or a file
70
+ const entries = fs.readdirSync(cachePath);
71
+ if (entries.length === 0) {
72
+ // Empty directory — remove it
73
+ fs.rmdirSync(cachePath);
74
+ } else {
75
+ // Has contents — remove contents then the dir
76
+ for (const entry of entries) {
77
+ const entryPath = join(cachePath, entry);
78
+ if (fs.existsSync(entryPath)) {
79
+ try {
80
+ fs.unlinkSync(entryPath);
81
+ } catch {
82
+ // ignore individual file failures
83
+ }
84
+ }
85
+ }
86
+ fs.rmdirSync(cachePath);
87
+ }
88
+ purged = true;
89
+ } catch {
90
+ // best-effort — cache purge failure is non-fatal
91
+ }
92
+ }
93
+ }
94
+
95
+ // ── Remove plugin from both configs ─────────────────────────────────────
96
+ for (const basename of CONFIG_BASENAMES) {
97
+ const loaded = loadGlobalConfig(fs, env, basename);
98
+ const plugins = normalizePlugin(loaded.data['plugins']);
99
+
100
+ // Filter out all opencode-rules-md entries
101
+ const remaining = plugins.filter(p => !matchesPlugin(p));
102
+
103
+ if (remaining.length === plugins.length) {
104
+ // Nothing to remove — no-op for this file
105
+ results.push({ path: loaded.path, status: 'skipped', backup: null });
106
+ continue;
107
+ }
108
+
109
+ anyProcessed = true;
110
+
111
+ const newData = { ...loaded.data, plugins: remaining };
112
+ const newContent = JSON.stringify(newData, null, 2) + '\n';
113
+
114
+ if (opts.dryRun) {
115
+ results.push({ path: loaded.path, status: 'wrote', backup: null });
116
+ continue;
117
+ }
118
+
119
+ // Backup the existing file if it exists
120
+ let backup: string | undefined;
121
+ if (loaded.exists) {
122
+ backup = backupIfWritable(fs, loaded.path);
123
+ if (backup !== undefined) {
124
+ const dir = dirname(loaded.path);
125
+ const segs = loaded.path.replace(/\\/g, '/').split('/');
126
+ const base = segs[segs.length - 1] ?? loaded.path;
127
+ const dot = base.lastIndexOf('.');
128
+ const name = dot >= 0 ? base.slice(0, dot) : base;
129
+ rotateBackups(fs, dir, name, 3);
130
+ }
131
+ }
132
+
133
+ writeAtomically(fs, loaded.path, newContent);
134
+ results.push({
135
+ path: loaded.path,
136
+ status: 'wrote',
137
+ backup: backup ?? null,
138
+ });
139
+ }
140
+
141
+ return {
142
+ status: anyProcessed || purged ? 'wrote' : 'skipped',
143
+ results,
144
+ purged,
145
+ };
146
+ };
@@ -0,0 +1,220 @@
1
+ // ---------------------------------------------------------------------------
2
+ // src/cli/update.ts — `omd update` command implementation.
3
+ //
4
+ // Fetches latest from npm, compares to installed version, purges the
5
+ // ~/.cache/opencode/node_modules/opencode-rules-md cache, and prints the
6
+ // reinstall instruction when stale. No auto-reinstall.
7
+ // ---------------------------------------------------------------------------
8
+
9
+ import { homedir } from 'os';
10
+ import { join } from 'path';
11
+ import { fetchLatestVersion, isStale } from './registry.js';
12
+ import { loadGlobalConfig, matchesPlugin, normalizePlugin } from './config.js';
13
+ import type { CliFs } from './config.js';
14
+
15
+ // ─── Constants ───────────────────────────────────────────────────────────────
16
+
17
+ export const CACHE_PATH = join(
18
+ homedir(),
19
+ '.cache',
20
+ 'opencode',
21
+ 'node_modules',
22
+ 'opencode-rules-md',
23
+ );
24
+
25
+ // ─── Types ───────────────────────────────────────────────────────────────────
26
+
27
+ export interface UpdateOptions {
28
+ latestVersion?: string | null | undefined;
29
+ dryRun?: boolean;
30
+ }
31
+
32
+ export interface UpdateResult {
33
+ status: 'stale' | 'current' | 'unreachable';
34
+ cachePath: string;
35
+ instruction: string;
36
+ }
37
+
38
+ // ─── Cache path helpers ───────────────────────────────────────────────────────
39
+
40
+ /**
41
+ * Resolve the local cache directory, honoring a custom HOME environment.
42
+ * Falls back to os.homedir() when HOME is not set, matching the
43
+ * resolveConfigDir fallback strategy.
44
+ */
45
+ export function resolveCachePath(env: NodeJS.ProcessEnv = process.env): string {
46
+ return join(
47
+ env.HOME ?? homedir(),
48
+ '.cache',
49
+ 'opencode',
50
+ 'node_modules',
51
+ 'opencode-rules-md',
52
+ );
53
+ }
54
+
55
+ /**
56
+ * Read the cached package.json version, if determinable.
57
+ * Returns null when the cache is missing or unreadable.
58
+ */
59
+ function readCacheVersion(fs: CliFs, cachePath: string): string | null {
60
+ const pkgPath = join(cachePath, 'package.json');
61
+ try {
62
+ if (!fs.existsSync(pkgPath)) {
63
+ return null;
64
+ }
65
+ const pkg = JSON.parse(fs.readFileSync(pkgPath)) as { version?: unknown };
66
+ return typeof pkg.version === 'string' ? pkg.version : null;
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+
72
+ // ─── runUpdate ────────────────────────────────────────────────────────────────
73
+
74
+ const CONFIG_BASENAMES = ['opencode', 'tui'] as const;
75
+
76
+ /**
77
+ * Run the update command:
78
+ * 1. Fetch latest version from npm (or use provided mock)
79
+ * 2. Compare to installed version in config
80
+ * 3. If stale: purge cache, print reinstall instruction
81
+ * 4. If current: print "already current"
82
+ * 5. If unreachable: noop
83
+ */
84
+ export const runUpdate = async (
85
+ fs: CliFs,
86
+ env: NodeJS.ProcessEnv,
87
+ log: (s: string) => void,
88
+ _error: (s: string) => void,
89
+ opts: UpdateOptions = {},
90
+ ): Promise<UpdateResult> => {
91
+ // Use provided latestVersion for testing, otherwise fetch from npm
92
+ const latest = opts.latestVersion !== undefined
93
+ ? opts.latestVersion
94
+ : await fetchLatestVersion();
95
+
96
+ const cachePath = resolveCachePath(env);
97
+
98
+ if (latest === null) {
99
+ log('omd: could not determine latest version from npm registry');
100
+ return {
101
+ status: 'unreachable',
102
+ cachePath,
103
+ instruction: 'npx opencode-rules-md@latest install',
104
+ };
105
+ }
106
+
107
+ // Determine installed version from config (check both configs)
108
+ let installedVersion: string | null = null;
109
+ for (const basename of CONFIG_BASENAMES) {
110
+ const loaded = loadGlobalConfig(fs, env, basename);
111
+ const plugins = normalizePlugin(loaded.data['plugins']);
112
+ const match = plugins.find(p => matchesPlugin(p));
113
+ if (match) {
114
+ // Extract version from specifier (e.g. 'opencode-rules-md@2.0.0' -> '2.0.0')
115
+ const atIndex = match.lastIndexOf('@');
116
+ installedVersion = atIndex >= 0 ? match.slice(atIndex + 1) : null;
117
+ break; // use first config that has it
118
+ }
119
+ }
120
+
121
+ const instruction = `npx opencode-rules-md@latest install`;
122
+
123
+ // Check staleness: only stale if installed version differs from latest
124
+ if (installedVersion !== null && !isStale(installedVersion, latest)) {
125
+ log(`omd: opencode-rules-md@${installedVersion} is already the latest`);
126
+ return {
127
+ status: 'current',
128
+ cachePath,
129
+ instruction,
130
+ };
131
+ }
132
+
133
+ // Before purging, decide whether the cache is actually stale. When the cache
134
+ // version is already equal to the latest resolved version, skip the purge
135
+ // so we do not delete a valid cache. If the version cannot be determined,
136
+ // fall back to purging to be safe.
137
+ const cacheVersion = readCacheVersion(fs, cachePath);
138
+ const shouldPurge = cacheVersion === null || cacheVersion !== latest;
139
+
140
+ if (opts.dryRun) {
141
+ log(`omd: update check (dry-run)`);
142
+ log(` latest version: ${latest}`);
143
+ log(` installed version: ${installedVersion ?? 'not installed'}`);
144
+ log(` would purge: ${shouldPurge ? cachePath : '(nothing to purge)'}`);
145
+ log(` would instruct: ${instruction}`);
146
+ return {
147
+ status: 'stale',
148
+ cachePath,
149
+ instruction,
150
+ };
151
+ }
152
+
153
+ // Purge the cache directory using the injected fs (allows test faking)
154
+ if (shouldPurge) {
155
+ try {
156
+ if (fs.existsSync(cachePath)) {
157
+ // Recursively remove contents then the dir itself
158
+ purgeDirectory(fs, cachePath);
159
+ }
160
+ } catch {
161
+ // best-effort — purge failure is non-fatal
162
+ }
163
+ }
164
+
165
+ log(`omd: opencode-rules-md is stale (latest: ${latest})`);
166
+ log(`omd: cache purged at ${cachePath}`);
167
+ log(`omd: to reinstall, run:`);
168
+ log(` ${instruction}`);
169
+
170
+ return {
171
+ status: 'stale',
172
+ cachePath,
173
+ instruction,
174
+ };
175
+ };
176
+
177
+ // ─── purgeDirectory ────────────────────────────────────────────────────────────
178
+
179
+ /**
180
+ * Recursively delete a directory and all its contents using the given fs.
181
+ */
182
+ export function purgeDirectory(fs: CliFs, dirPath: string): void {
183
+ let entries: string[] = [];
184
+ try {
185
+ entries = fs.readdirSync(dirPath);
186
+ } catch {
187
+ return;
188
+ }
189
+
190
+ for (const entry of entries) {
191
+ const entryPath = join(dirPath, entry);
192
+ try {
193
+ if (fs.existsSync(entryPath)) {
194
+ // Check if it's a directory by trying to read it
195
+ try {
196
+ const subEntries = fs.readdirSync(entryPath);
197
+ if (subEntries.length === 0) {
198
+ // Empty directory
199
+ fs.rmdirSync(entryPath);
200
+ } else {
201
+ // Non-empty directory — recurse
202
+ purgeDirectory(fs, entryPath);
203
+ fs.rmdirSync(entryPath);
204
+ }
205
+ } catch {
206
+ // It's a file — unlink it
207
+ fs.unlinkSync(entryPath);
208
+ }
209
+ }
210
+ } catch {
211
+ // best-effort per entry
212
+ }
213
+ }
214
+
215
+ try {
216
+ fs.rmdirSync(dirPath);
217
+ } catch {
218
+ // best-effort
219
+ }
220
+ }