skalpel 3.0.7 → 3.0.9
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/npm-bin/skalpel.js +64 -1
- package/package.json +6 -6
- package/postinstall/index.js +135 -33
- package/postinstall/lib/env-inject.js +7 -0
- package/postinstall/lib/service-register.js +93 -32
package/npm-bin/skalpel.js
CHANGED
|
@@ -238,21 +238,84 @@ function runUninstall(rest) {
|
|
|
238
238
|
if (cleanupData) args.push('--cleanup-data');
|
|
239
239
|
if (dryRun) args.push('--dry-run');
|
|
240
240
|
|
|
241
|
+
// X4 fix: pass a temp summary file path; postinstall writes a JSON
|
|
242
|
+
// structure with per-phase counts so we can print a truthful
|
|
243
|
+
// message instead of unconditionally claiming success.
|
|
244
|
+
const summaryFile = path.join(
|
|
245
|
+
os.tmpdir(),
|
|
246
|
+
`skalpel-uninstall-summary-${process.pid}-${Date.now()}.json`
|
|
247
|
+
);
|
|
248
|
+
const childEnv = Object.assign({}, process.env, {
|
|
249
|
+
SKALPEL_UNINSTALL_SUMMARY_FILE: summaryFile,
|
|
250
|
+
});
|
|
251
|
+
|
|
241
252
|
const result = spawnSync(process.execPath, [postinstall, ...args], {
|
|
242
253
|
stdio: 'inherit',
|
|
254
|
+
env: childEnv,
|
|
243
255
|
});
|
|
244
256
|
if (result.error) {
|
|
257
|
+
try { fs.rmSync(summaryFile, { force: true }); } catch (_) {}
|
|
245
258
|
emitError(process.stderr, 'Uninstall failed', result.error.message, '→ Try `npm uninstall -g skalpel`.');
|
|
246
259
|
return 1;
|
|
247
260
|
}
|
|
248
261
|
if (result.status !== 0) {
|
|
262
|
+
try { fs.rmSync(summaryFile, { force: true }); } catch (_) {}
|
|
249
263
|
return result.status === null ? 1 : result.status;
|
|
250
264
|
}
|
|
251
265
|
|
|
266
|
+
let summary = null;
|
|
267
|
+
try {
|
|
268
|
+
summary = JSON.parse(fs.readFileSync(summaryFile, 'utf8'));
|
|
269
|
+
} catch (_) {
|
|
270
|
+
// Older postinstall didn't write one, or file removed mid-flight.
|
|
271
|
+
} finally {
|
|
272
|
+
try { fs.rmSync(summaryFile, { force: true }); } catch (_) {}
|
|
273
|
+
}
|
|
274
|
+
|
|
252
275
|
const colored = process.stdout.isTTY && !process.env.NO_COLOR;
|
|
253
276
|
const dim = colored ? theme.dim : (s) => s;
|
|
254
277
|
const ok = colored ? theme.mint('✓') : '✓';
|
|
255
|
-
|
|
278
|
+
|
|
279
|
+
if (summary) {
|
|
280
|
+
const rc = summary.rcBlocksRemoved | 0;
|
|
281
|
+
const svc = summary.serviceFileRemoved ? 1 : 0;
|
|
282
|
+
const data = summary.userDataFilesRemoved | 0;
|
|
283
|
+
const total = rc + svc + data;
|
|
284
|
+
if (total === 0 && !summary.dryRun) {
|
|
285
|
+
process.stdout.write(`\n${ok} skalpel state was already clean — nothing to remove.\n`);
|
|
286
|
+
} else {
|
|
287
|
+
const parts = [
|
|
288
|
+
`${rc} shell-rc block${rc === 1 ? '' : 's'}`,
|
|
289
|
+
`${svc} service entr${svc === 1 ? 'y' : 'ies'}`,
|
|
290
|
+
];
|
|
291
|
+
if (summary.cleanupDataRequested) {
|
|
292
|
+
parts.push(`${data} user-data file${data === 1 ? '' : 's'}`);
|
|
293
|
+
}
|
|
294
|
+
const prefix = summary.dryRun ? '[dry-run] would remove' : 'Removed';
|
|
295
|
+
process.stdout.write(`\n${ok} ${prefix}: ${parts.join(', ')}.\n`);
|
|
296
|
+
}
|
|
297
|
+
} else {
|
|
298
|
+
// Fallback to the old unconditional message if no summary made it back.
|
|
299
|
+
process.stdout.write(`\n${ok} skalpel state removed from this machine.\n`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Only emit the "running agents still have stale env" hint when
|
|
303
|
+
// something was actually removed (or in dry-run); on already-clean
|
|
304
|
+
// re-runs the hint adds noise without value.
|
|
305
|
+
const ranAnyCleanup = summary && !(
|
|
306
|
+
summary.rcBlocksRemoved === 0 &&
|
|
307
|
+
!summary.serviceFileRemoved &&
|
|
308
|
+
summary.userDataFilesRemoved === 0
|
|
309
|
+
);
|
|
310
|
+
if (!summary || ranAnyCleanup) {
|
|
311
|
+
process.stdout.write(
|
|
312
|
+
`${dim('Note: any open shells or running coding agents (Claude Code, Codex, ' +
|
|
313
|
+
'Cursor, etc.) still have the skalpel proxy env vars cached in their ' +
|
|
314
|
+
'environment. Open a fresh shell and restart any active agents to drop ' +
|
|
315
|
+
'them.')}\n`
|
|
316
|
+
);
|
|
317
|
+
}
|
|
318
|
+
|
|
256
319
|
process.stdout.write(`${dim('To finish removal, also run:')}\n`);
|
|
257
320
|
process.stdout.write(` npm uninstall -g skalpel\n`);
|
|
258
321
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skalpel",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.9",
|
|
4
4
|
"description": "Skalpel — local proxy and TUI for coding agents (skalpel + skalpeld bundle).",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"homepage": "https://skalpel.ai",
|
|
@@ -54,10 +54,10 @@
|
|
|
54
54
|
"x64"
|
|
55
55
|
],
|
|
56
56
|
"optionalDependencies": {
|
|
57
|
-
"@skalpelai/skalpel-darwin-arm64": "3.0.
|
|
58
|
-
"@skalpelai/skalpel-darwin-x64": "3.0.
|
|
59
|
-
"@skalpelai/skalpel-linux-arm64": "3.0.
|
|
60
|
-
"@skalpelai/skalpel-linux-x64": "3.0.
|
|
61
|
-
"@skalpelai/skalpel-win32-x64": "3.0.
|
|
57
|
+
"@skalpelai/skalpel-darwin-arm64": "3.0.9",
|
|
58
|
+
"@skalpelai/skalpel-darwin-x64": "3.0.9",
|
|
59
|
+
"@skalpelai/skalpel-linux-arm64": "3.0.9",
|
|
60
|
+
"@skalpelai/skalpel-linux-x64": "3.0.9",
|
|
61
|
+
"@skalpelai/skalpel-win32-x64": "3.0.9"
|
|
62
62
|
}
|
|
63
63
|
}
|
package/postinstall/index.js
CHANGED
|
@@ -110,60 +110,113 @@ function helpText() {
|
|
|
110
110
|
}
|
|
111
111
|
|
|
112
112
|
// B29: best-effort delete of user data on --uninstall --cleanup-data.
|
|
113
|
-
//
|
|
113
|
+
// X5 fix: previously silent on absent files. Now logs "absent — skip"
|
|
114
|
+
// for every checked path so the user can see what was inspected.
|
|
115
|
+
// X7 fix: also clean cache/, skalpel-tui.log, skalpeld.log, socket
|
|
116
|
+
// markers, and stats.cache — the pre-X7 list was just 4 paths and
|
|
117
|
+
// left those behind, defeating the "full wipe" promise.
|
|
114
118
|
function cleanupUserData({ dryRun }) {
|
|
115
119
|
const removed = [];
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
paths.
|
|
120
|
+
const skipped = [];
|
|
121
|
+
const cfg = paths.configDir();
|
|
122
|
+
const fileTargets = [
|
|
123
|
+
paths.authFile(), // auth.json
|
|
124
|
+
paths.configToml(), // config.toml
|
|
125
|
+
paths.lockFile(), // skalpeld.lock
|
|
126
|
+
path.join(cfg, 'skalpel-tui.log'), // X7: TUI logger sink
|
|
127
|
+
path.join(cfg, 'skalpeld.log'), // X7: daemon log
|
|
128
|
+
path.join(cfg, 'skalpeld.sock'), // X7: daemon socket
|
|
129
|
+
path.join(cfg, 'skalpeld.sock.path'), // X7: socket path marker
|
|
130
|
+
path.join(cfg, 'stats.cache'), // X7: stats cache
|
|
131
|
+
];
|
|
132
|
+
const dirTargets = [
|
|
133
|
+
paths.logsDir(), // logs/
|
|
134
|
+
path.join(cfg, 'cache'), // X7: model-fingerprint cache
|
|
120
135
|
];
|
|
121
|
-
|
|
136
|
+
|
|
137
|
+
for (const t of fileTargets) {
|
|
138
|
+
const present = fs.existsSync(t);
|
|
122
139
|
if (dryRun) {
|
|
123
|
-
|
|
124
|
-
|
|
140
|
+
if (present) {
|
|
141
|
+
log.dryRun(`uninstall-cleanup: would rm ${t}`);
|
|
142
|
+
removed.push(t);
|
|
143
|
+
} else {
|
|
144
|
+
log.dryRun(`uninstall-cleanup: ${t} absent — would skip`);
|
|
145
|
+
skipped.push(t);
|
|
146
|
+
}
|
|
125
147
|
continue;
|
|
126
148
|
}
|
|
127
149
|
try {
|
|
128
|
-
if (
|
|
150
|
+
if (present) {
|
|
129
151
|
fs.rmSync(t, { force: true });
|
|
130
152
|
log.info(`uninstall-cleanup: removed ${t}`);
|
|
131
153
|
removed.push(t);
|
|
154
|
+
} else {
|
|
155
|
+
log.info(`uninstall-cleanup: ${t} absent — skip`);
|
|
156
|
+
skipped.push(t);
|
|
132
157
|
}
|
|
133
158
|
} catch (err) {
|
|
134
159
|
log.warn(`uninstall-cleanup: rm ${t} failed: ${err.message}`);
|
|
135
160
|
}
|
|
136
161
|
}
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
for (const d of dirTargets) {
|
|
163
|
+
const present = fs.existsSync(d);
|
|
164
|
+
if (dryRun) {
|
|
165
|
+
if (present) {
|
|
166
|
+
log.dryRun(`uninstall-cleanup: would rm -r ${d}`);
|
|
167
|
+
removed.push(d);
|
|
168
|
+
} else {
|
|
169
|
+
log.dryRun(`uninstall-cleanup: ${d} absent — would skip`);
|
|
170
|
+
skipped.push(d);
|
|
171
|
+
}
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
142
174
|
try {
|
|
143
|
-
if (
|
|
144
|
-
fs.rmSync(
|
|
145
|
-
log.info(`uninstall-cleanup: removed ${
|
|
146
|
-
removed.push(
|
|
175
|
+
if (present) {
|
|
176
|
+
fs.rmSync(d, { recursive: true, force: true });
|
|
177
|
+
log.info(`uninstall-cleanup: removed ${d}`);
|
|
178
|
+
removed.push(d);
|
|
179
|
+
} else {
|
|
180
|
+
log.info(`uninstall-cleanup: ${d} absent — skip`);
|
|
181
|
+
skipped.push(d);
|
|
147
182
|
}
|
|
148
183
|
} catch (err) {
|
|
149
|
-
log.warn(`uninstall-cleanup: rm -r ${
|
|
184
|
+
log.warn(`uninstall-cleanup: rm -r ${d} failed: ${err.message}`);
|
|
150
185
|
}
|
|
151
186
|
}
|
|
152
|
-
// Try to remove the (now-empty) configDir;
|
|
153
|
-
//
|
|
154
|
-
|
|
187
|
+
// Try to remove the (now-empty) configDir; ENOTEMPTY means the
|
|
188
|
+
// user has third-party state under it we deliberately did not
|
|
189
|
+
// touch — leave it alone.
|
|
155
190
|
if (!dryRun) {
|
|
156
191
|
try {
|
|
157
192
|
fs.rmdirSync(cfg);
|
|
158
193
|
log.info(`uninstall-cleanup: removed empty ${cfg}`);
|
|
159
194
|
removed.push(cfg);
|
|
160
|
-
} catch (
|
|
161
|
-
|
|
195
|
+
} catch (err) {
|
|
196
|
+
if (err.code === 'ENOTEMPTY') {
|
|
197
|
+
log.info(`uninstall-cleanup: ${cfg} not empty (third-party files preserved) — skip rmdir`);
|
|
198
|
+
} else if (err.code !== 'ENOENT') {
|
|
199
|
+
log.warn(`uninstall-cleanup: rmdir ${cfg} failed: ${err.message}`);
|
|
200
|
+
}
|
|
162
201
|
}
|
|
163
202
|
} else {
|
|
164
203
|
log.dryRun(`uninstall-cleanup: would rmdir (if empty) ${cfg}`);
|
|
165
204
|
}
|
|
166
|
-
return { removed };
|
|
205
|
+
return { removed, skipped };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// X4 fix: write a JSON summary of what the uninstall actually did to
|
|
209
|
+
// the path in SKALPEL_UNINSTALL_SUMMARY_FILE, so npm-bin/skalpel.js
|
|
210
|
+
// (which inherited stdio to this child) can derive a truthful count
|
|
211
|
+
// summary without parsing log lines.
|
|
212
|
+
function writeSummaryFile(summary) {
|
|
213
|
+
const out = process.env.SKALPEL_UNINSTALL_SUMMARY_FILE;
|
|
214
|
+
if (!out) return;
|
|
215
|
+
try {
|
|
216
|
+
fs.writeFileSync(out, JSON.stringify(summary), { mode: 0o600 });
|
|
217
|
+
} catch (err) {
|
|
218
|
+
log.warn(`uninstall: failed to write summary to ${out}: ${err.message}`);
|
|
219
|
+
}
|
|
167
220
|
}
|
|
168
221
|
|
|
169
222
|
function main(argv) {
|
|
@@ -173,12 +226,35 @@ function main(argv) {
|
|
|
173
226
|
return 0;
|
|
174
227
|
}
|
|
175
228
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
229
|
+
// npx mode: prior to v3.0.9 we returned exit 1 here, which caused
|
|
230
|
+
// npm to roll back the cached package install — leaving an empty
|
|
231
|
+
// ~/.npm/_npx/<hash>/ dir and a silent failure that re-prompted on
|
|
232
|
+
// every subsequent `npx skalpel` call. The original reason for the
|
|
233
|
+
// gate (B32) was that paths.binPath() couldn't locate the platform
|
|
234
|
+
// binary under npx's directory layout. That was fixed in v3.0.1
|
|
235
|
+
// when binPath was rewritten to use require.resolve against the
|
|
236
|
+
// platform sub-package (see paths.js:99-116) — which works under
|
|
237
|
+
// npx, global, or local install paths uniformly.
|
|
238
|
+
//
|
|
239
|
+
// We still skip the install wizard under npx because the wizard's
|
|
240
|
+
// service-register step would bake the npx cache path
|
|
241
|
+
// (~/.npm/_npx/<hash>/...) into the OS service unit, and that path
|
|
242
|
+
// is evicted by npx's cache GC. Exit 0 instead of 1 so npm install
|
|
243
|
+
// completes and the binary becomes invokable.
|
|
244
|
+
//
|
|
245
|
+
// Uninstall, however, MUST run under npx: a user running
|
|
246
|
+
// `npx skalpel uninstall` to clean up a prior `npm install -g`
|
|
247
|
+
// would otherwise hit the same skip and have nothing happen.
|
|
248
|
+
if (paths.isNpxInvocation() && !opts.uninstall) {
|
|
249
|
+
log.info(
|
|
250
|
+
'npx mode detected — skipping install wizard (service-register / ' +
|
|
251
|
+
'env-inject would bake transient npx cache paths into your system).'
|
|
252
|
+
);
|
|
253
|
+
log.info(
|
|
254
|
+
'For a persistent install with daemon-on-boot and shell env vars, ' +
|
|
255
|
+
'run: `npm install -g skalpel`'
|
|
180
256
|
);
|
|
181
|
-
return
|
|
257
|
+
return 0;
|
|
182
258
|
}
|
|
183
259
|
|
|
184
260
|
const total = 5;
|
|
@@ -192,21 +268,44 @@ function main(argv) {
|
|
|
192
268
|
const allWarnings = [];
|
|
193
269
|
|
|
194
270
|
if (opts.uninstall) {
|
|
271
|
+
// X4 fix: count what was actually removed in each phase so the
|
|
272
|
+
// shim can print a truthful summary instead of a blanket "✓ state
|
|
273
|
+
// removed" that lies when the system was already clean.
|
|
274
|
+
const summary = {
|
|
275
|
+
rcBlocksRemoved: 0,
|
|
276
|
+
serviceFileRemoved: false,
|
|
277
|
+
serviceUnloadSucceeded: 0,
|
|
278
|
+
serviceUnloadFailedAllowed: 0,
|
|
279
|
+
serviceUnloadSkipped: false,
|
|
280
|
+
userDataFilesRemoved: 0,
|
|
281
|
+
userDataFilesSkipped: 0,
|
|
282
|
+
cleanupDataRequested: !!opts.cleanupData,
|
|
283
|
+
dryRun: !!opts.dryRun,
|
|
284
|
+
errors: [],
|
|
285
|
+
};
|
|
195
286
|
try {
|
|
196
287
|
const stepCount = opts.cleanupData ? 3 : 2;
|
|
197
288
|
log.step(1, stepCount, 'env-uninject', 'remove managed-block from rc files');
|
|
198
|
-
envInject.uninject({ dryRun: opts.dryRun });
|
|
289
|
+
const ei = envInject.uninject({ dryRun: opts.dryRun }) || {};
|
|
290
|
+
summary.rcBlocksRemoved = Array.isArray(ei.removed) ? ei.removed.length : 0;
|
|
199
291
|
|
|
200
292
|
log.step(2, stepCount, 'service-unregister', `OS=${process.platform}`);
|
|
201
293
|
const ur = serviceRegister.unregister({ dryRun: opts.dryRun }) || {};
|
|
202
294
|
if (Array.isArray(ur.errors) && ur.errors.length) {
|
|
203
295
|
allWarnings.push(...ur.errors);
|
|
296
|
+
summary.errors.push(...ur.errors);
|
|
204
297
|
}
|
|
298
|
+
summary.serviceFileRemoved = !!ur.serviceFileRemoved;
|
|
299
|
+
summary.serviceUnloadSucceeded = ur.succeeded || 0;
|
|
300
|
+
summary.serviceUnloadFailedAllowed = ur.failedAllowed || 0;
|
|
301
|
+
summary.serviceUnloadSkipped = !!ur.skipped;
|
|
205
302
|
|
|
206
303
|
// B29: optional user-data cleanup.
|
|
207
304
|
if (opts.cleanupData) {
|
|
208
|
-
log.step(3, stepCount, 'cleanup-user-data', 'delete auth.json/config.toml/lock/logs');
|
|
209
|
-
cleanupUserData({ dryRun: opts.dryRun });
|
|
305
|
+
log.step(3, stepCount, 'cleanup-user-data', 'delete auth.json/config.toml/lock/logs/cache');
|
|
306
|
+
const cd = cleanupUserData({ dryRun: opts.dryRun }) || {};
|
|
307
|
+
summary.userDataFilesRemoved = Array.isArray(cd.removed) ? cd.removed.length : 0;
|
|
308
|
+
summary.userDataFilesSkipped = Array.isArray(cd.skipped) ? cd.skipped.length : 0;
|
|
210
309
|
} else {
|
|
211
310
|
log.info(
|
|
212
311
|
'uninstall: user data preserved (auth.json/config.toml/lock/logs). ' +
|
|
@@ -218,8 +317,11 @@ function main(argv) {
|
|
|
218
317
|
if (opts.verbose) {
|
|
219
318
|
process.stderr.write(`${err.stack}\n`);
|
|
220
319
|
}
|
|
320
|
+
summary.errors.push(err.message);
|
|
321
|
+
writeSummaryFile(summary);
|
|
221
322
|
return 0;
|
|
222
323
|
}
|
|
324
|
+
writeSummaryFile(summary);
|
|
223
325
|
if (allWarnings.length) {
|
|
224
326
|
log.warn(`postinstall wizard finished (${mode} uninstall) with ${allWarnings.length} warning(s)`);
|
|
225
327
|
} else {
|
|
@@ -113,6 +113,13 @@ function uninject({ dryRun }) {
|
|
|
113
113
|
if (r.changed) {
|
|
114
114
|
log.info(`uninstall: removed managed block from ${rc.path} (${rc.shell})`);
|
|
115
115
|
removed.push(rc.path);
|
|
116
|
+
} else {
|
|
117
|
+
// X6 fix: previously silent. Surface "checked, nothing to remove"
|
|
118
|
+
// so the user can tell the rc file was inspected — Owen's #discord
|
|
119
|
+
// 2026-05-12 Windows report only logged "powershell-legacy absent"
|
|
120
|
+
// and never mentioned the modern powershell rc which existed but
|
|
121
|
+
// had no managed block.
|
|
122
|
+
log.info(`uninstall: no managed block in ${rc.path} (${rc.shell}) — skip`);
|
|
116
123
|
}
|
|
117
124
|
}
|
|
118
125
|
return { removed };
|
|
@@ -161,58 +161,119 @@ function verifyBinary() {
|
|
|
161
161
|
return { bin, stat };
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
//
|
|
165
|
-
//
|
|
166
|
-
//
|
|
167
|
-
//
|
|
164
|
+
// fallbackUnloadCommands returns commands that do not depend on
|
|
165
|
+
// installed-state files. Used when the per-OS primary unload path
|
|
166
|
+
// requires a precondition (rendered PS1, etc.) that may be missing —
|
|
167
|
+
// e.g. an aborted install or a prior uninstall already deleted it.
|
|
168
|
+
function fallbackUnloadCommands() {
|
|
169
|
+
switch (process.platform) {
|
|
170
|
+
case 'win32':
|
|
171
|
+
// schtasks.exe lives in System32 and is always present; the
|
|
172
|
+
// PS1 helper only existed to template params, so we invoke
|
|
173
|
+
// schtasks directly when the helper is gone.
|
|
174
|
+
return [
|
|
175
|
+
['schtasks.exe', ['/End', '/TN', 'Skalpel\\skalpel-daemon'], { allowFail: true }],
|
|
176
|
+
['schtasks.exe', ['/Delete', '/TN', 'Skalpel\\skalpel-daemon', '/F'], { allowFail: true }],
|
|
177
|
+
];
|
|
178
|
+
default:
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Preflight + three-state outcome tracking. We distinguish:
|
|
184
|
+
// - succeeded: command exited 0
|
|
185
|
+
// - skipped-precondition: a file the command needs is absent
|
|
186
|
+
// - failed-allowed: command exited non-0, allowFail=true
|
|
187
|
+
// - failed-hard: command exited non-0, allowFail=false
|
|
188
|
+
// The pre-X1 code lumped succeeded + failed-allowed into "ok",
|
|
189
|
+
// producing "1/1 ok" reports when an unregister silently failed
|
|
190
|
+
// because the service was never registered to begin with. Owen
|
|
191
|
+
// (#discord, 2026-05-12) saw this on Windows when the rendered PS1
|
|
192
|
+
// helper was gone and PowerShell errored loudly while the wizard
|
|
193
|
+
// still reported success.
|
|
168
194
|
function unregister({ dryRun }) {
|
|
169
195
|
const dest = paths.servicePath();
|
|
196
|
+
const ps1 = renderedPowerShellPath();
|
|
197
|
+
const servicePresent = fs.existsSync(dest);
|
|
198
|
+
const ps1Present = fs.existsSync(ps1);
|
|
199
|
+
|
|
170
200
|
if (dryRun) {
|
|
171
201
|
log.dryRun(`uninstall: would unregister service at ${dest}`);
|
|
172
|
-
|
|
173
|
-
log.dryRun(`
|
|
202
|
+
if (!servicePresent) {
|
|
203
|
+
log.dryRun(` service file absent at ${dest} — would skip unload commands`);
|
|
204
|
+
} else {
|
|
205
|
+
const cmds = (process.platform === 'win32' && !ps1Present)
|
|
206
|
+
? fallbackUnloadCommands()
|
|
207
|
+
: unloadCommands();
|
|
208
|
+
for (const [bin, args] of cmds) {
|
|
209
|
+
log.dryRun(` would run: ${bin} ${args.join(' ')}`);
|
|
210
|
+
}
|
|
174
211
|
}
|
|
175
212
|
log.dryRun(` would rm ${dest}`);
|
|
176
|
-
|
|
213
|
+
if (process.platform === 'win32') {
|
|
214
|
+
log.dryRun(` would rm ${ps1}`);
|
|
215
|
+
}
|
|
216
|
+
return { dryRun: true, succeeded: 0, skipped: 0, failedAllowed: 0, serviceFileRemoved: false };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let cmds;
|
|
220
|
+
if (!servicePresent) {
|
|
221
|
+
log.info(`uninstall: service file absent at ${dest} — nothing registered to unload`);
|
|
222
|
+
cmds = [];
|
|
223
|
+
} else if (process.platform === 'win32' && !ps1Present) {
|
|
224
|
+
log.warn(`uninstall: PowerShell helper absent at ${ps1} — using direct schtasks fallback`);
|
|
225
|
+
cmds = fallbackUnloadCommands();
|
|
226
|
+
} else {
|
|
227
|
+
cmds = unloadCommands();
|
|
177
228
|
}
|
|
178
229
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
bin,
|
|
182
|
-
args,
|
|
183
|
-
allowFail: !!(opts && opts.allowFail),
|
|
184
|
-
done: false,
|
|
185
|
-
}));
|
|
230
|
+
let succeeded = 0;
|
|
231
|
+
let failedAllowed = 0;
|
|
186
232
|
const errors = [];
|
|
187
233
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
234
|
+
for (const [bin, args, opts] of cmds) {
|
|
235
|
+
const allowFail = !!(opts && opts.allowFail);
|
|
236
|
+
const r = spawnSync(bin, args, { stdio: 'inherit' });
|
|
237
|
+
if (r.status === 0) {
|
|
238
|
+
succeeded += 1;
|
|
239
|
+
} else if (allowFail) {
|
|
240
|
+
failedAllowed += 1;
|
|
241
|
+
log.info(`uninstall: ${bin} ${args.join(' ')} exited ${r.status} (allowed)`);
|
|
242
|
+
} else {
|
|
243
|
+
const msg = `uninstall: ${bin} ${args.join(' ')} exited ${r.status}`;
|
|
194
244
|
errors.push(msg);
|
|
195
245
|
log.warn(`${msg}; rolling forward to file removal`);
|
|
196
|
-
} else {
|
|
197
|
-
step.done = true;
|
|
198
246
|
}
|
|
199
247
|
}
|
|
200
|
-
log.info(
|
|
201
|
-
`uninstall: phase 2 complete: ${steps.filter((s) => s.done).length}/${steps.length} ok` +
|
|
202
|
-
(errors.length ? ` (${errors.length} errors)` : '')
|
|
203
|
-
);
|
|
204
248
|
|
|
205
|
-
if (
|
|
249
|
+
if (cmds.length === 0) {
|
|
250
|
+
log.info('uninstall: phase 2 skipped — service was not registered');
|
|
251
|
+
} else {
|
|
252
|
+
log.info(
|
|
253
|
+
`uninstall: phase 2 complete: ${succeeded} succeeded, ` +
|
|
254
|
+
`${failedAllowed} failed (allowed)` +
|
|
255
|
+
(errors.length ? `, ${errors.length} hard errors` : '')
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
let serviceFileRemoved = false;
|
|
260
|
+
if (servicePresent) {
|
|
206
261
|
fs.unlinkSync(dest);
|
|
207
262
|
log.info(`uninstall: removed ${dest}`);
|
|
263
|
+
serviceFileRemoved = true;
|
|
208
264
|
}
|
|
209
|
-
|
|
210
|
-
const ps1 = renderedPowerShellPath();
|
|
211
|
-
if (process.platform === 'win32' && fs.existsSync(ps1)) {
|
|
265
|
+
if (process.platform === 'win32' && ps1Present) {
|
|
212
266
|
fs.unlinkSync(ps1);
|
|
213
267
|
log.info(`uninstall: removed ${ps1}`);
|
|
214
268
|
}
|
|
215
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
removed: serviceFileRemoved,
|
|
271
|
+
serviceFileRemoved,
|
|
272
|
+
succeeded,
|
|
273
|
+
skipped: cmds.length === 0 ? 1 : 0,
|
|
274
|
+
failedAllowed,
|
|
275
|
+
errors,
|
|
276
|
+
};
|
|
216
277
|
}
|
|
217
278
|
|
|
218
279
|
function run({ dryRun }) {
|
|
@@ -290,4 +351,4 @@ function run({ dryRun }) {
|
|
|
290
351
|
return { skipped: false, registered: true, warnings };
|
|
291
352
|
}
|
|
292
353
|
|
|
293
|
-
module.exports = { run, unregister, templatePath, loadCommands, unloadCommands, launchdTarget };
|
|
354
|
+
module.exports = { run, unregister, templatePath, loadCommands, unloadCommands, fallbackUnloadCommands, launchdTarget };
|