opencode-rules-md 0.8.3 → 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.
- package/dist/cli.mjs +915 -0
- package/dist/src/cli/config.d.ts +93 -0
- package/dist/src/cli/config.d.ts.map +1 -0
- package/dist/src/cli/config.js +382 -0
- package/dist/src/cli/config.js.map +1 -0
- package/dist/src/cli/install.d.ts +35 -0
- package/dist/src/cli/install.d.ts.map +1 -0
- package/dist/src/cli/install.js +148 -0
- package/dist/src/cli/install.js.map +1 -0
- package/dist/src/cli/main.d.ts +14 -0
- package/dist/src/cli/main.d.ts.map +1 -0
- package/dist/src/cli/main.js +244 -0
- package/dist/src/cli/main.js.map +1 -0
- package/dist/src/cli/real-fs.d.ts +3 -0
- package/dist/src/cli/real-fs.d.ts.map +1 -0
- package/dist/src/cli/real-fs.js +32 -0
- package/dist/src/cli/real-fs.js.map +1 -0
- package/dist/src/cli/registry.d.ts +12 -0
- package/dist/src/cli/registry.d.ts.map +1 -0
- package/dist/src/cli/registry.js +34 -0
- package/dist/src/cli/registry.js.map +1 -0
- package/dist/src/cli/status.d.ts +44 -0
- package/dist/src/cli/status.d.ts.map +1 -0
- package/dist/src/cli/status.js +182 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/uninstall.d.ts +27 -0
- package/dist/src/cli/uninstall.d.ts.map +1 -0
- package/dist/src/cli/uninstall.js +101 -0
- package/dist/src/cli/uninstall.js.map +1 -0
- package/dist/src/cli/update.d.ts +31 -0
- package/dist/src/cli/update.d.ts.map +1 -0
- package/dist/src/cli/update.js +173 -0
- package/dist/src/cli/update.js.map +1 -0
- package/package.json +11 -15
- package/src/cli/config.ts +461 -0
- package/src/cli/install.ts +211 -0
- package/src/cli/main.ts +299 -0
- package/src/cli/real-fs.ts +44 -0
- package/src/cli/registry.ts +36 -0
- package/src/cli/status.ts +247 -0
- package/src/cli/uninstall.ts +146 -0
- 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
|
+
}
|