opencode-rules-md 0.8.5 → 0.9.0
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 +252 -261
- package/dist/src/cli/config.d.ts +11 -0
- package/dist/src/cli/config.d.ts.map +1 -1
- package/dist/src/cli/config.js +15 -0
- package/dist/src/cli/config.js.map +1 -1
- package/dist/src/cli/install.d.ts +35 -20
- package/dist/src/cli/install.d.ts.map +1 -1
- package/dist/src/cli/install.js +50 -131
- package/dist/src/cli/install.js.map +1 -1
- package/dist/src/cli/main.d.ts +5 -0
- package/dist/src/cli/main.d.ts.map +1 -1
- package/dist/src/cli/main.js +29 -26
- package/dist/src/cli/main.js.map +1 -1
- 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 +15 -8
- package/dist/src/cli/status.d.ts.map +1 -1
- package/dist/src/cli/status.js +63 -31
- package/dist/src/cli/status.js.map +1 -1
- package/dist/src/cli/uninstall.d.ts +5 -2
- package/dist/src/cli/uninstall.d.ts.map +1 -1
- package/dist/src/cli/uninstall.js +78 -51
- package/dist/src/cli/uninstall.js.map +1 -1
- package/dist/src/cli/update.d.ts +50 -16
- package/dist/src/cli/update.d.ts.map +1 -1
- package/dist/src/cli/update.js +156 -124
- package/dist/src/cli/update.js.map +1 -1
- package/dist/src/rule-discovery.d.ts.map +1 -1
- package/dist/src/rule-discovery.js +18 -10
- package/dist/src/rule-discovery.js.map +1 -1
- package/dist/src/rule-filter.d.ts +9 -0
- package/dist/src/rule-filter.d.ts.map +1 -1
- package/dist/src/rule-filter.js +48 -11
- package/dist/src/rule-filter.js.map +1 -1
- package/dist/src/rule-metadata.d.ts +2 -0
- package/dist/src/rule-metadata.d.ts.map +1 -1
- package/dist/src/rule-metadata.js +4 -0
- package/dist/src/rule-metadata.js.map +1 -1
- package/dist/src/runtime-context.d.ts +6 -0
- package/dist/src/runtime-context.d.ts.map +1 -1
- package/dist/src/runtime-context.js +16 -0
- package/dist/src/runtime-context.js.map +1 -1
- package/dist/src/session-store.d.ts.map +1 -1
- package/dist/src/session-store.js +1 -0
- package/dist/src/session-store.js.map +1 -1
- package/dist/tui/index.js +13 -9
- package/dist/tui/index.js.map +4 -4
- package/package.json +1 -1
- package/src/cli/config.ts +19 -0
- package/src/cli/install.ts +66 -174
- package/src/cli/main.ts +34 -25
- package/src/cli/spawn.ts +76 -0
- package/src/cli/status.ts +83 -37
- package/src/cli/uninstall.ts +88 -57
- package/src/cli/update.ts +182 -148
- package/src/rule-discovery.ts +22 -10
- package/src/rule-filter.ts +73 -12
- package/src/rule-metadata.ts +8 -0
- package/src/runtime-context.ts +16 -0
- package/src/session-store.ts +1 -0
package/src/cli/status.ts
CHANGED
|
@@ -1,24 +1,37 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// src/cli/status.ts — `omd status` and `omd doctor` command implementations.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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.
|
|
7
14
|
// ---------------------------------------------------------------------------
|
|
8
15
|
|
|
9
|
-
import { join, dirname } from 'path';
|
|
16
|
+
import { join, dirname, extname } from 'path';
|
|
10
17
|
import { homedir } from 'os';
|
|
11
|
-
import { extname } from 'path';
|
|
12
18
|
import {
|
|
13
19
|
loadGlobalConfig,
|
|
14
20
|
matchesPlugin,
|
|
15
|
-
|
|
21
|
+
readInstalledPlugins,
|
|
16
22
|
type CliFs,
|
|
23
|
+
type LoadedConfig,
|
|
17
24
|
} from './config.js';
|
|
18
25
|
import { fetchLatestVersion } from './registry.js';
|
|
26
|
+
import { resolveCachePaths } from './update.js';
|
|
19
27
|
|
|
20
28
|
// ─── Status types ─────────────────────────────────────────────────────────────
|
|
21
29
|
|
|
30
|
+
export interface StatusOptions {
|
|
31
|
+
/** Override latest version for testing — skips the npm registry call */
|
|
32
|
+
latestVersion?: string | null;
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
export interface StatusEntry {
|
|
23
36
|
basename: string;
|
|
24
37
|
path: string;
|
|
@@ -66,30 +79,35 @@ export const runStatus = async (
|
|
|
66
79
|
fs: CliFs,
|
|
67
80
|
env: NodeJS.ProcessEnv,
|
|
68
81
|
log: (s: string) => void,
|
|
82
|
+
opts: StatusOptions = {},
|
|
69
83
|
): Promise<StatusResult> => {
|
|
70
84
|
const configs: StatusEntry[] = [];
|
|
71
|
-
const latest =
|
|
85
|
+
const latest = opts.latestVersion !== undefined
|
|
86
|
+
? opts.latestVersion
|
|
87
|
+
: await fetchLatestVersion();
|
|
72
88
|
|
|
73
89
|
for (const basename of CONFIG_BASENAMES) {
|
|
74
90
|
const loaded = loadGlobalConfig(fs, env, basename);
|
|
75
91
|
const format = extname(loaded.path) as string; // '.json' or '.jsonc'
|
|
76
|
-
const plugins =
|
|
77
|
-
const match = plugins.find(p => matchesPlugin(p));
|
|
92
|
+
const plugins = readInstalledPlugins(loaded);
|
|
93
|
+
const match = plugins.find((p) => matchesPlugin(p)) ?? null;
|
|
78
94
|
|
|
79
95
|
const entry: StatusEntry = {
|
|
80
96
|
basename,
|
|
81
97
|
path: loaded.path,
|
|
82
98
|
format,
|
|
83
|
-
installed: match
|
|
84
|
-
notInstalled: !loaded.exists
|
|
85
|
-
otherPlugins: plugins.filter(p => !matchesPlugin(p)),
|
|
99
|
+
installed: match,
|
|
100
|
+
notInstalled: !loaded.exists || match === null,
|
|
101
|
+
otherPlugins: plugins.filter((p) => !matchesPlugin(p)),
|
|
86
102
|
latest,
|
|
87
|
-
isLatest:
|
|
103
|
+
isLatest:
|
|
104
|
+
match !== null && latest !== null
|
|
105
|
+
? match === `opencode-rules-md@${latest}`
|
|
106
|
+
: null,
|
|
88
107
|
};
|
|
89
108
|
|
|
90
109
|
configs.push(entry);
|
|
91
110
|
|
|
92
|
-
// ── Print status lines ──────────────────────────────────────────────────
|
|
93
111
|
if (!loaded.exists) {
|
|
94
112
|
log(`omd: ${basename}.json — config not found at ${loaded.path}`);
|
|
95
113
|
continue;
|
|
@@ -97,14 +115,15 @@ export const runStatus = async (
|
|
|
97
115
|
|
|
98
116
|
log(`omd: ${basename}${format} — ${loaded.path}`);
|
|
99
117
|
if (match) {
|
|
100
|
-
const isLatestLabel =
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
118
|
+
const isLatestLabel =
|
|
119
|
+
entry.isLatest === true
|
|
120
|
+
? ' (latest)'
|
|
121
|
+
: entry.isLatest === false
|
|
122
|
+
? ` (behind: latest is ${latest ?? 'unknown'})`
|
|
123
|
+
: '';
|
|
105
124
|
log(` opencode-rules-md: ${match}${isLatestLabel}`);
|
|
106
125
|
} else {
|
|
107
|
-
log(
|
|
126
|
+
log(' opencode-rules-md: not installed');
|
|
108
127
|
}
|
|
109
128
|
|
|
110
129
|
if (entry.otherPlugins.length > 0) {
|
|
@@ -119,13 +138,16 @@ export const runStatus = async (
|
|
|
119
138
|
|
|
120
139
|
/**
|
|
121
140
|
* Run the doctor command: health checks for the plugin environment.
|
|
122
|
-
*
|
|
123
|
-
*
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
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
|
|
129
151
|
*
|
|
130
152
|
* Exit 1 if any issues found.
|
|
131
153
|
*/
|
|
@@ -147,7 +169,7 @@ export const runDoctor = async (
|
|
|
147
169
|
let hasBun = opts.hasBun;
|
|
148
170
|
if (hasBun === undefined) {
|
|
149
171
|
const pathEnv = (env.PATH ?? '').split(':');
|
|
150
|
-
hasBun = pathEnv.some(p => {
|
|
172
|
+
hasBun = pathEnv.some((p) => {
|
|
151
173
|
try {
|
|
152
174
|
const { existsSync } = require('node:fs') as typeof import('node:fs');
|
|
153
175
|
return existsSync(p + '/bun');
|
|
@@ -179,7 +201,7 @@ export const runDoctor = async (
|
|
|
179
201
|
// ── Check: both configs readable and valid ─────────────────────────────────
|
|
180
202
|
for (const basename of CONFIG_BASENAMES) {
|
|
181
203
|
log(`Checking ${basename} config...`);
|
|
182
|
-
const loaded = loadGlobalConfig(fs, env, basename);
|
|
204
|
+
const loaded: LoadedConfig = loadGlobalConfig(fs, env, basename);
|
|
183
205
|
|
|
184
206
|
if (!loaded.exists) {
|
|
185
207
|
warnings.push(`${basename} config not found at ${loaded.path}`);
|
|
@@ -188,23 +210,37 @@ export const runDoctor = async (
|
|
|
188
210
|
}
|
|
189
211
|
|
|
190
212
|
log(` ${basename}${extname(loaded.path)}: ${loaded.path}`);
|
|
191
|
-
const plugins =
|
|
192
|
-
const match = plugins.find(p => matchesPlugin(p));
|
|
213
|
+
const plugins = readInstalledPlugins(loaded);
|
|
214
|
+
const match = plugins.find((p) => matchesPlugin(p)) ?? null;
|
|
193
215
|
|
|
194
216
|
if (match) {
|
|
195
|
-
info.push(`${basename}: opencode-rules-md
|
|
217
|
+
info.push(`${basename}: opencode-rules-md installed (${match})`);
|
|
196
218
|
log(` opencode-rules-md: ${match}`);
|
|
197
219
|
} else {
|
|
198
220
|
warnings.push(`${basename}: plugin not installed`);
|
|
199
|
-
log(
|
|
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")`);
|
|
200
230
|
}
|
|
201
231
|
|
|
202
232
|
// ── Check plugin shape ────────────────────────────────────────────────────
|
|
203
|
-
|
|
204
|
-
|
|
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`);
|
|
205
241
|
error(`[ISSUE] ${basename}: invalid plugin shape`);
|
|
206
242
|
} else {
|
|
207
|
-
log(
|
|
243
|
+
log(' plugin field: valid');
|
|
208
244
|
}
|
|
209
245
|
}
|
|
210
246
|
|
|
@@ -218,6 +254,16 @@ export const runDoctor = async (
|
|
|
218
254
|
log(` ${ruleBase}: exists`);
|
|
219
255
|
}
|
|
220
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
|
+
|
|
221
267
|
// ── Check: config dir writable ─────────────────────────────────────────────
|
|
222
268
|
const configDir = env.OPENCODE_CONFIG_DIR ?? join(homedir(), '.config', 'opencode');
|
|
223
269
|
log('Checking config directory write access...');
|
|
@@ -244,4 +290,4 @@ export const runDoctor = async (
|
|
|
244
290
|
warnings,
|
|
245
291
|
info,
|
|
246
292
|
};
|
|
247
|
-
};
|
|
293
|
+
};
|
package/src/cli/uninstall.ts
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
2
|
// src/cli/uninstall.ts — `omd uninstall` command implementation.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
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.
|
|
7
18
|
// ---------------------------------------------------------------------------
|
|
8
19
|
|
|
9
|
-
import {
|
|
10
|
-
import { homedir } from 'os';
|
|
20
|
+
import { join } from 'path';
|
|
11
21
|
import {
|
|
12
22
|
loadGlobalConfig,
|
|
13
|
-
matchesPlugin,
|
|
14
|
-
normalizePlugin,
|
|
15
23
|
backupIfWritable,
|
|
16
24
|
rotateBackups,
|
|
17
25
|
writeAtomically,
|
|
18
26
|
type CliFs,
|
|
19
27
|
} from './config.js';
|
|
28
|
+
import { resolveCachePaths, purgeDirectory } from './update.js';
|
|
20
29
|
|
|
21
30
|
export const CONFIG_BASENAMES = ['opencode', 'tui'] as const;
|
|
22
31
|
|
|
23
32
|
export interface UninstallOptions {
|
|
33
|
+
/** Also remove ~/.cache/opencode/packages/opencode-rules-md*. */
|
|
24
34
|
purge?: boolean;
|
|
35
|
+
/** Run the full pipeline without writing to disk. */
|
|
25
36
|
dryRun?: boolean;
|
|
37
|
+
/** Reserved for future prompts — no-op today. */
|
|
26
38
|
yes?: boolean;
|
|
27
39
|
}
|
|
28
40
|
|
|
@@ -38,13 +50,56 @@ export interface UninstallResult {
|
|
|
38
50
|
purged: boolean;
|
|
39
51
|
}
|
|
40
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
|
+
|
|
41
96
|
/**
|
|
42
97
|
* Uninstall opencode-rules-md from both opencode.json and tui.json configs.
|
|
43
98
|
*
|
|
44
99
|
* Options:
|
|
45
|
-
* purge — also remove ~/.cache/opencode/
|
|
100
|
+
* purge — also remove ~/.cache/opencode/packages/opencode-rules-md*
|
|
46
101
|
* dryRun — run full pipeline without writing to disk
|
|
47
|
-
* yes —
|
|
102
|
+
* yes — reserved
|
|
48
103
|
*/
|
|
49
104
|
export const runUninstall = (
|
|
50
105
|
opts: UninstallOptions = {},
|
|
@@ -57,37 +112,15 @@ export const runUninstall = (
|
|
|
57
112
|
|
|
58
113
|
// ── Purge cache if requested ────────────────────────────────────────────
|
|
59
114
|
if (opts.purge) {
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
'.cache',
|
|
63
|
-
'opencode',
|
|
64
|
-
'node_modules',
|
|
65
|
-
'opencode-rules-md',
|
|
66
|
-
);
|
|
67
|
-
if (fs.existsSync(cachePath)) {
|
|
115
|
+
const cachePaths = resolveCachePaths(env, fs);
|
|
116
|
+
for (const cachePath of cachePaths) {
|
|
68
117
|
try {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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);
|
|
118
|
+
if (fs.existsSync(cachePath)) {
|
|
119
|
+
purgeDirectory(fs, cachePath);
|
|
120
|
+
purged = true;
|
|
87
121
|
}
|
|
88
|
-
purged = true;
|
|
89
122
|
} catch {
|
|
90
|
-
// best-effort —
|
|
123
|
+
// best-effort — purge failure is non-fatal
|
|
91
124
|
}
|
|
92
125
|
}
|
|
93
126
|
}
|
|
@@ -95,39 +128,37 @@ export const runUninstall = (
|
|
|
95
128
|
// ── Remove plugin from both configs ─────────────────────────────────────
|
|
96
129
|
for (const basename of CONFIG_BASENAMES) {
|
|
97
130
|
const loaded = loadGlobalConfig(fs, env, basename);
|
|
98
|
-
const plugins = normalizePlugin(loaded.data['plugins']);
|
|
99
131
|
|
|
100
|
-
|
|
101
|
-
|
|
132
|
+
if (!loaded.exists) {
|
|
133
|
+
results.push({ path: loaded.path, status: 'skipped', backup: null });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
102
136
|
|
|
103
|
-
|
|
104
|
-
|
|
137
|
+
const { next, removed } = stripFromData(loaded.data);
|
|
138
|
+
|
|
139
|
+
if (removed === 0) {
|
|
105
140
|
results.push({ path: loaded.path, status: 'skipped', backup: null });
|
|
106
141
|
continue;
|
|
107
142
|
}
|
|
108
143
|
|
|
109
144
|
anyProcessed = true;
|
|
110
145
|
|
|
111
|
-
const
|
|
112
|
-
const newContent = JSON.stringify(newData, null, 2) + '\n';
|
|
146
|
+
const newContent = JSON.stringify(next, null, 2) + '\n';
|
|
113
147
|
|
|
114
148
|
if (opts.dryRun) {
|
|
115
149
|
results.push({ path: loaded.path, status: 'wrote', backup: null });
|
|
116
150
|
continue;
|
|
117
151
|
}
|
|
118
152
|
|
|
119
|
-
// Backup the existing file
|
|
120
|
-
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const name = dot >= 0 ? base.slice(0, dot) : base;
|
|
129
|
-
rotateBackups(fs, dir, name, 3);
|
|
130
|
-
}
|
|
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);
|
|
131
162
|
}
|
|
132
163
|
|
|
133
164
|
writeAtomically(fs, loaded.path, newContent);
|
|
@@ -143,4 +174,4 @@ export const runUninstall = (
|
|
|
143
174
|
results,
|
|
144
175
|
purged,
|
|
145
176
|
};
|
|
146
|
-
};
|
|
177
|
+
};
|