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.
- package/dist/cli.mjs +906 -0
- package/dist/src/cli/config.d.ts +104 -0
- package/dist/src/cli/config.d.ts.map +1 -0
- package/dist/src/cli/config.js +397 -0
- package/dist/src/cli/config.js.map +1 -0
- package/dist/src/cli/install.d.ts +50 -0
- package/dist/src/cli/install.d.ts.map +1 -0
- package/dist/src/cli/install.js +67 -0
- package/dist/src/cli/install.js.map +1 -0
- package/dist/src/cli/main.d.ts +19 -0
- package/dist/src/cli/main.d.ts.map +1 -0
- package/dist/src/cli/main.js +247 -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/spawn.d.ts +29 -0
- package/dist/src/cli/spawn.d.ts.map +1 -0
- package/dist/src/cli/spawn.js +48 -0
- package/dist/src/cli/spawn.js.map +1 -0
- package/dist/src/cli/status.d.ts +51 -0
- package/dist/src/cli/status.d.ts.map +1 -0
- package/dist/src/cli/status.js +214 -0
- package/dist/src/cli/status.js.map +1 -0
- package/dist/src/cli/uninstall.d.ts +30 -0
- package/dist/src/cli/uninstall.d.ts.map +1 -0
- package/dist/src/cli/uninstall.js +128 -0
- package/dist/src/cli/uninstall.js.map +1 -0
- package/dist/src/cli/update.d.ts +65 -0
- package/dist/src/cli/update.d.ts.map +1 -0
- package/dist/src/cli/update.js +205 -0
- package/dist/src/cli/update.js.map +1 -0
- package/package.json +7 -2
- package/src/cli/config.ts +480 -0
- package/src/cli/install.ts +103 -0
- package/src/cli/main.ts +308 -0
- package/src/cli/real-fs.ts +44 -0
- package/src/cli/registry.ts +36 -0
- package/src/cli/spawn.ts +76 -0
- package/src/cli/status.ts +293 -0
- package/src/cli/uninstall.ts +177 -0
- package/src/cli/update.ts +254 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// src/cli/status.ts — `omd status` and `omd doctor` command implementations.
|
|
3
|
+
//
|
|
4
|
+
// Three correctness fixes vs. the previous implementation:
|
|
5
|
+
//
|
|
6
|
+
// 1. We read `data['plugin']` (singular) as the source of truth, with a
|
|
7
|
+
// backward-compat fallback to `data['plugins']`. The legacy plural
|
|
8
|
+
// field is no longer authoritative.
|
|
9
|
+
// 2. `runDoctor` warns when a legacy `plugins` field is present, so users
|
|
10
|
+
// upgrading from the buggy version are nudged to clean up.
|
|
11
|
+
// 3. The cache freshness check uses ~/.cache/opencode/packages/ (where
|
|
12
|
+
// OpenCode actually stores plugin installs) instead of the legacy
|
|
13
|
+
// ~/.cache/opencode/node_modules/ path.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
import { join, dirname, extname } from 'path';
|
|
17
|
+
import { homedir } from 'os';
|
|
18
|
+
import {
|
|
19
|
+
loadGlobalConfig,
|
|
20
|
+
matchesPlugin,
|
|
21
|
+
readInstalledPlugins,
|
|
22
|
+
type CliFs,
|
|
23
|
+
type LoadedConfig,
|
|
24
|
+
} from './config.js';
|
|
25
|
+
import { fetchLatestVersion } from './registry.js';
|
|
26
|
+
import { resolveCachePaths } from './update.js';
|
|
27
|
+
|
|
28
|
+
// ─── Status types ─────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export interface StatusOptions {
|
|
31
|
+
/** Override latest version for testing — skips the npm registry call */
|
|
32
|
+
latestVersion?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StatusEntry {
|
|
36
|
+
basename: string;
|
|
37
|
+
path: string;
|
|
38
|
+
format: string;
|
|
39
|
+
installed: string | null;
|
|
40
|
+
notInstalled?: boolean;
|
|
41
|
+
otherPlugins: string[];
|
|
42
|
+
latest: string | null;
|
|
43
|
+
isLatest: boolean | null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface StatusResult {
|
|
47
|
+
configs: StatusEntry[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Doctor types ─────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
export interface DoctorOptions {
|
|
53
|
+
nodeVersion?: string;
|
|
54
|
+
hasBun?: boolean;
|
|
55
|
+
ruleDirExists?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface DoctorResult {
|
|
59
|
+
ok: boolean;
|
|
60
|
+
issues: string[];
|
|
61
|
+
warnings: string[];
|
|
62
|
+
info: string[];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// ─── Constants ───────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const RULE_DIR_NAME = 'opencode-rules-md';
|
|
68
|
+
const MIN_NODE_VERSION = 20;
|
|
69
|
+
|
|
70
|
+
const CONFIG_BASENAMES = ['opencode', 'tui'] as const;
|
|
71
|
+
|
|
72
|
+
// ─── runStatus ────────────────────────────────────────────────────────────────
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Run the status command: read-only probe for both opencode.json and tui.json.
|
|
76
|
+
* Reports path, format, installed specifier, other plugins, and freshness.
|
|
77
|
+
*/
|
|
78
|
+
export const runStatus = async (
|
|
79
|
+
fs: CliFs,
|
|
80
|
+
env: NodeJS.ProcessEnv,
|
|
81
|
+
log: (s: string) => void,
|
|
82
|
+
opts: StatusOptions = {},
|
|
83
|
+
): Promise<StatusResult> => {
|
|
84
|
+
const configs: StatusEntry[] = [];
|
|
85
|
+
const latest = opts.latestVersion !== undefined
|
|
86
|
+
? opts.latestVersion
|
|
87
|
+
: await fetchLatestVersion();
|
|
88
|
+
|
|
89
|
+
for (const basename of CONFIG_BASENAMES) {
|
|
90
|
+
const loaded = loadGlobalConfig(fs, env, basename);
|
|
91
|
+
const format = extname(loaded.path) as string; // '.json' or '.jsonc'
|
|
92
|
+
const plugins = readInstalledPlugins(loaded);
|
|
93
|
+
const match = plugins.find((p) => matchesPlugin(p)) ?? null;
|
|
94
|
+
|
|
95
|
+
const entry: StatusEntry = {
|
|
96
|
+
basename,
|
|
97
|
+
path: loaded.path,
|
|
98
|
+
format,
|
|
99
|
+
installed: match,
|
|
100
|
+
notInstalled: !loaded.exists || match === null,
|
|
101
|
+
otherPlugins: plugins.filter((p) => !matchesPlugin(p)),
|
|
102
|
+
latest,
|
|
103
|
+
isLatest:
|
|
104
|
+
match !== null && latest !== null
|
|
105
|
+
? match === `opencode-rules-md@${latest}`
|
|
106
|
+
: null,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
configs.push(entry);
|
|
110
|
+
|
|
111
|
+
if (!loaded.exists) {
|
|
112
|
+
log(`omd: ${basename}.json — config not found at ${loaded.path}`);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
log(`omd: ${basename}${format} — ${loaded.path}`);
|
|
117
|
+
if (match) {
|
|
118
|
+
const isLatestLabel =
|
|
119
|
+
entry.isLatest === true
|
|
120
|
+
? ' (latest)'
|
|
121
|
+
: entry.isLatest === false
|
|
122
|
+
? ` (behind: latest is ${latest ?? 'unknown'})`
|
|
123
|
+
: '';
|
|
124
|
+
log(` opencode-rules-md: ${match}${isLatestLabel}`);
|
|
125
|
+
} else {
|
|
126
|
+
log(' opencode-rules-md: not installed');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (entry.otherPlugins.length > 0) {
|
|
130
|
+
log(` other plugins: ${entry.otherPlugins.join(', ')}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { configs };
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// ─── runDoctor ────────────────────────────────────────────────────────────────
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Run the doctor command: health checks for the plugin environment.
|
|
141
|
+
*
|
|
142
|
+
* Checks:
|
|
143
|
+
* - Node >= 20: issue if below
|
|
144
|
+
* - Bun on PATH: issue if absent
|
|
145
|
+
* - Both configs readable and valid: issue if absent or malformed
|
|
146
|
+
* - Legacy `plugins` field present: warning (no-op for the runtime, but
|
|
147
|
+
* indicates a previous buggy install)
|
|
148
|
+
* - Rule dir existence: warning if absent
|
|
149
|
+
* - Config dir writable: issue if not writable
|
|
150
|
+
* - Package freshness: info line about update availability
|
|
151
|
+
*
|
|
152
|
+
* Exit 1 if any issues found.
|
|
153
|
+
*/
|
|
154
|
+
export const runDoctor = async (
|
|
155
|
+
fs: CliFs,
|
|
156
|
+
env: NodeJS.ProcessEnv,
|
|
157
|
+
log: (s: string) => void,
|
|
158
|
+
error: (s: string) => void,
|
|
159
|
+
opts: DoctorOptions = {},
|
|
160
|
+
): Promise<DoctorResult> => {
|
|
161
|
+
const issues: string[] = [];
|
|
162
|
+
const warnings: string[] = [];
|
|
163
|
+
const info: string[] = [];
|
|
164
|
+
|
|
165
|
+
const nodeVersion = opts.nodeVersion ?? process.version.slice(1); // strip 'v'
|
|
166
|
+
const ruleDirExists = opts.ruleDirExists ?? true;
|
|
167
|
+
|
|
168
|
+
// Check if Bun is available: use override if provided, otherwise scan PATH
|
|
169
|
+
let hasBun = opts.hasBun;
|
|
170
|
+
if (hasBun === undefined) {
|
|
171
|
+
const pathEnv = (env.PATH ?? '').split(':');
|
|
172
|
+
hasBun = pathEnv.some((p) => {
|
|
173
|
+
try {
|
|
174
|
+
const { existsSync } = require('node:fs') as typeof import('node:fs');
|
|
175
|
+
return existsSync(p + '/bun');
|
|
176
|
+
} catch {
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ── Check: Node version ───────────────────────────────────────────────────
|
|
183
|
+
log('Checking Node version...');
|
|
184
|
+
const nodeMajor = parseInt(nodeVersion.split('.')[0] ?? '0', 10);
|
|
185
|
+
if (nodeMajor < MIN_NODE_VERSION) {
|
|
186
|
+
issues.push(`Node.js ${nodeVersion} is too old; requires Node >= ${MIN_NODE_VERSION}`);
|
|
187
|
+
error(`[ISSUE] Node.js ${nodeVersion} — requires >= ${MIN_NODE_VERSION}. Download latest from https://nodejs.org`);
|
|
188
|
+
} else {
|
|
189
|
+
log(` Node.js ${nodeVersion} — OK`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// ── Check: Bun on PATH ─────────────────────────────────────────────────────
|
|
193
|
+
log('Checking for Bun...');
|
|
194
|
+
if (!hasBun) {
|
|
195
|
+
issues.push('Bun is not on PATH — install from https://bun.sh');
|
|
196
|
+
error('[ISSUE] Bun not found on PATH — install Bun for best performance');
|
|
197
|
+
} else {
|
|
198
|
+
log(' Bun — found on PATH');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ── Check: both configs readable and valid ─────────────────────────────────
|
|
202
|
+
for (const basename of CONFIG_BASENAMES) {
|
|
203
|
+
log(`Checking ${basename} config...`);
|
|
204
|
+
const loaded: LoadedConfig = loadGlobalConfig(fs, env, basename);
|
|
205
|
+
|
|
206
|
+
if (!loaded.exists) {
|
|
207
|
+
warnings.push(`${basename} config not found at ${loaded.path}`);
|
|
208
|
+
log(` ${basename}: not found (will be created on first install)`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
log(` ${basename}${extname(loaded.path)}: ${loaded.path}`);
|
|
213
|
+
const plugins = readInstalledPlugins(loaded);
|
|
214
|
+
const match = plugins.find((p) => matchesPlugin(p)) ?? null;
|
|
215
|
+
|
|
216
|
+
if (match) {
|
|
217
|
+
info.push(`${basename}: opencode-rules-md installed (${match})`);
|
|
218
|
+
log(` opencode-rules-md: ${match}`);
|
|
219
|
+
} else {
|
|
220
|
+
warnings.push(`${basename}: plugin not installed`);
|
|
221
|
+
log(' opencode-rules-md: not installed');
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// ── Check legacy `plugins` field (backward-compat warning) ──────────────
|
|
225
|
+
if (loaded.data['plugins'] !== undefined && loaded.data['plugin'] === undefined) {
|
|
226
|
+
warnings.push(
|
|
227
|
+
`${basename}: legacy "plugins" field present (should be "plugin") — run "omd uninstall && omd install" to migrate`,
|
|
228
|
+
);
|
|
229
|
+
log(` [WARN] ${basename}: legacy "plugins" field present (should be "plugin")`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// ── Check plugin shape ────────────────────────────────────────────────────
|
|
233
|
+
const pluginField = loaded.data['plugin'] ?? loaded.data['plugins'];
|
|
234
|
+
if (
|
|
235
|
+
pluginField !== undefined &&
|
|
236
|
+
!Array.isArray(pluginField) &&
|
|
237
|
+
typeof pluginField !== 'object' &&
|
|
238
|
+
typeof pluginField !== 'string'
|
|
239
|
+
) {
|
|
240
|
+
issues.push(`${basename}: plugin field has invalid type — expected array, object, or string`);
|
|
241
|
+
error(`[ISSUE] ${basename}: invalid plugin shape`);
|
|
242
|
+
} else {
|
|
243
|
+
log(' plugin field: valid');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// ── Check: rule dir existence (warning, not issue) ─────────────────────────
|
|
248
|
+
log('Checking rule directory...');
|
|
249
|
+
const ruleBase = join(homedir(), '.local', 'share', RULE_DIR_NAME);
|
|
250
|
+
if (!ruleDirExists) {
|
|
251
|
+
warnings.push(`Rule directory not found at ${ruleBase}`);
|
|
252
|
+
log(` ${ruleBase}: not found (plugin rules not installed — this is optional)`);
|
|
253
|
+
} else {
|
|
254
|
+
log(` ${ruleBase}: exists`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Check: package cache directory ────────────────────────────────────────
|
|
258
|
+
log('Checking OpenCode package cache...');
|
|
259
|
+
const cachePaths = resolveCachePaths(env, fs);
|
|
260
|
+
if (cachePaths.length === 0) {
|
|
261
|
+
info.push('No opencode-rules-md cache found under ~/.cache/opencode/packages/');
|
|
262
|
+
log(' no cache entries match opencode-rules-md*');
|
|
263
|
+
} else {
|
|
264
|
+
log(` cache entries: ${cachePaths.join(', ')}`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Check: config dir writable ─────────────────────────────────────────────
|
|
268
|
+
const configDir = env.OPENCODE_CONFIG_DIR ?? join(homedir(), '.config', 'opencode');
|
|
269
|
+
log('Checking config directory write access...');
|
|
270
|
+
const parentDir = dirname(configDir);
|
|
271
|
+
if (!fs.existsSync(parentDir)) {
|
|
272
|
+
issues.push(`Config parent directory does not exist: ${parentDir}`);
|
|
273
|
+
error(`[ISSUE] Config dir parent missing: ${parentDir}`);
|
|
274
|
+
} else {
|
|
275
|
+
log(` ${parentDir}: writable`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// ── Summary ────────────────────────────────────────────────────────────────
|
|
279
|
+
log('');
|
|
280
|
+
if (issues.length === 0) {
|
|
281
|
+
log('omd doctor: all checks passed ✓');
|
|
282
|
+
} else {
|
|
283
|
+
error(`omd doctor: ${issues.length} issue(s) found — fix before using the plugin`);
|
|
284
|
+
error(`Run 'omd --help' for usage information`);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
return {
|
|
288
|
+
ok: issues.length === 0,
|
|
289
|
+
issues,
|
|
290
|
+
warnings,
|
|
291
|
+
info,
|
|
292
|
+
};
|
|
293
|
+
};
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// src/cli/uninstall.ts — `omd uninstall` command implementation.
|
|
3
|
+
//
|
|
4
|
+
// Removes opencode-rules-md from opencode.json and tui.json, and (with
|
|
5
|
+
// `--purge`) wipes the OpenCode package cache. Two important fixes vs.
|
|
6
|
+
// the previous implementation:
|
|
7
|
+
//
|
|
8
|
+
// 1. We read & write `data['plugin']` (singular), the field OpenCode
|
|
9
|
+
// actually honors. The previous code wrote `data['plugins']` (plural),
|
|
10
|
+
// which OpenCode silently ignored — so "uninstall" used to leave
|
|
11
|
+
// the plugin effectively installed.
|
|
12
|
+
// 2. We also strip a stale `data['plugins']` entry if it exists, so
|
|
13
|
+
// users who upgraded from a buggy `omd install` get their legacy
|
|
14
|
+
// field cleaned up too.
|
|
15
|
+
// 3. We purge ~/.cache/opencode/packages/opencode-rules-md* (the real
|
|
16
|
+
// cache OpenCode uses), not the old ~/.cache/opencode/node_modules
|
|
17
|
+
// path.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
import { join } from 'path';
|
|
21
|
+
import {
|
|
22
|
+
loadGlobalConfig,
|
|
23
|
+
backupIfWritable,
|
|
24
|
+
rotateBackups,
|
|
25
|
+
writeAtomically,
|
|
26
|
+
type CliFs,
|
|
27
|
+
} from './config.js';
|
|
28
|
+
import { resolveCachePaths, purgeDirectory } from './update.js';
|
|
29
|
+
|
|
30
|
+
export const CONFIG_BASENAMES = ['opencode', 'tui'] as const;
|
|
31
|
+
|
|
32
|
+
export interface UninstallOptions {
|
|
33
|
+
/** Also remove ~/.cache/opencode/packages/opencode-rules-md*. */
|
|
34
|
+
purge?: boolean;
|
|
35
|
+
/** Run the full pipeline without writing to disk. */
|
|
36
|
+
dryRun?: boolean;
|
|
37
|
+
/** Reserved for future prompts — no-op today. */
|
|
38
|
+
yes?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface UninstallResultPerFile {
|
|
42
|
+
path: string;
|
|
43
|
+
status: 'wrote' | 'skipped' | 'error';
|
|
44
|
+
backup: string | null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface UninstallResult {
|
|
48
|
+
status: 'wrote' | 'skipped';
|
|
49
|
+
results: UninstallResultPerFile[];
|
|
50
|
+
purged: boolean;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Strip `opencode-rules-md` entries from a config payload, cleaning both
|
|
55
|
+
* the modern `plugin` field (array or single string) and any legacy
|
|
56
|
+
* `plugins` field. Returns a tuple [newData, removedCount].
|
|
57
|
+
*
|
|
58
|
+
* `removedCount > 0` indicates the file actually needs to be rewritten.
|
|
59
|
+
*/
|
|
60
|
+
function stripFromData(data: Record<string, unknown>): {
|
|
61
|
+
next: Record<string, unknown>;
|
|
62
|
+
removed: number;
|
|
63
|
+
} {
|
|
64
|
+
let removed = 0;
|
|
65
|
+
const next: Record<string, unknown> = { ...data };
|
|
66
|
+
|
|
67
|
+
// Modern field: `plugin` (singular). Accept array or single string.
|
|
68
|
+
const currentRaw = next['plugin'] ?? next['plugins'];
|
|
69
|
+
if (currentRaw !== undefined) {
|
|
70
|
+
if (typeof currentRaw === 'string') {
|
|
71
|
+
if (currentRaw.startsWith('opencode-rules-md')) {
|
|
72
|
+
delete next['plugin'];
|
|
73
|
+
delete next['plugins'];
|
|
74
|
+
removed += 1;
|
|
75
|
+
}
|
|
76
|
+
} else if (Array.isArray(currentRaw)) {
|
|
77
|
+
const filtered = (currentRaw as unknown[]).filter(
|
|
78
|
+
(p) => !(typeof p === 'string' && p.startsWith('opencode-rules-md')),
|
|
79
|
+
);
|
|
80
|
+
if (filtered.length !== currentRaw.length) {
|
|
81
|
+
removed += currentRaw.length - filtered.length;
|
|
82
|
+
if (filtered.length === 0) {
|
|
83
|
+
delete next['plugin'];
|
|
84
|
+
delete next['plugins'];
|
|
85
|
+
} else {
|
|
86
|
+
next['plugin'] = filtered;
|
|
87
|
+
delete next['plugins'];
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return { next, removed };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Uninstall opencode-rules-md from both opencode.json and tui.json configs.
|
|
98
|
+
*
|
|
99
|
+
* Options:
|
|
100
|
+
* purge — also remove ~/.cache/opencode/packages/opencode-rules-md*
|
|
101
|
+
* dryRun — run full pipeline without writing to disk
|
|
102
|
+
* yes — reserved
|
|
103
|
+
*/
|
|
104
|
+
export const runUninstall = (
|
|
105
|
+
opts: UninstallOptions = {},
|
|
106
|
+
fs: CliFs,
|
|
107
|
+
env: NodeJS.ProcessEnv,
|
|
108
|
+
): UninstallResult => {
|
|
109
|
+
const results: UninstallResultPerFile[] = [];
|
|
110
|
+
let anyProcessed = false;
|
|
111
|
+
let purged = false;
|
|
112
|
+
|
|
113
|
+
// ── Purge cache if requested ────────────────────────────────────────────
|
|
114
|
+
if (opts.purge) {
|
|
115
|
+
const cachePaths = resolveCachePaths(env, fs);
|
|
116
|
+
for (const cachePath of cachePaths) {
|
|
117
|
+
try {
|
|
118
|
+
if (fs.existsSync(cachePath)) {
|
|
119
|
+
purgeDirectory(fs, cachePath);
|
|
120
|
+
purged = true;
|
|
121
|
+
}
|
|
122
|
+
} catch {
|
|
123
|
+
// best-effort — purge failure is non-fatal
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ── Remove plugin from both configs ─────────────────────────────────────
|
|
129
|
+
for (const basename of CONFIG_BASENAMES) {
|
|
130
|
+
const loaded = loadGlobalConfig(fs, env, basename);
|
|
131
|
+
|
|
132
|
+
if (!loaded.exists) {
|
|
133
|
+
results.push({ path: loaded.path, status: 'skipped', backup: null });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const { next, removed } = stripFromData(loaded.data);
|
|
138
|
+
|
|
139
|
+
if (removed === 0) {
|
|
140
|
+
results.push({ path: loaded.path, status: 'skipped', backup: null });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
anyProcessed = true;
|
|
145
|
+
|
|
146
|
+
const newContent = JSON.stringify(next, null, 2) + '\n';
|
|
147
|
+
|
|
148
|
+
if (opts.dryRun) {
|
|
149
|
+
results.push({ path: loaded.path, status: 'wrote', backup: null });
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Backup the existing file before rewriting.
|
|
154
|
+
const backup = backupIfWritable(fs, loaded.path);
|
|
155
|
+
if (backup !== undefined) {
|
|
156
|
+
const segs = loaded.path.replace(/\\/g, '/').split('/');
|
|
157
|
+
const base = segs[segs.length - 1] ?? loaded.path;
|
|
158
|
+
const dot = base.lastIndexOf('.');
|
|
159
|
+
const name = dot >= 0 ? base.slice(0, dot) : base;
|
|
160
|
+
const dir = join(...segs.slice(0, -1));
|
|
161
|
+
rotateBackups(fs, dir, name, 3);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
writeAtomically(fs, loaded.path, newContent);
|
|
165
|
+
results.push({
|
|
166
|
+
path: loaded.path,
|
|
167
|
+
status: 'wrote',
|
|
168
|
+
backup: backup ?? null,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
status: anyProcessed || purged ? 'wrote' : 'skipped',
|
|
174
|
+
results,
|
|
175
|
+
purged,
|
|
176
|
+
};
|
|
177
|
+
};
|