skalpel 3.0.6 → 3.0.8
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 +46 -1
- package/package.json +6 -6
- package/postinstall/index.js +107 -28
- 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,66 @@ 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
|
+
}
|
|
256
301
|
process.stdout.write(`${dim('To finish removal, also run:')}\n`);
|
|
257
302
|
process.stdout.write(` npm uninstall -g skalpel\n`);
|
|
258
303
|
return 0;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skalpel",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.8",
|
|
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.8",
|
|
58
|
+
"@skalpelai/skalpel-darwin-x64": "3.0.8",
|
|
59
|
+
"@skalpelai/skalpel-linux-arm64": "3.0.8",
|
|
60
|
+
"@skalpelai/skalpel-linux-x64": "3.0.8",
|
|
61
|
+
"@skalpelai/skalpel-win32-x64": "3.0.8"
|
|
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) {
|
|
@@ -192,21 +245,44 @@ function main(argv) {
|
|
|
192
245
|
const allWarnings = [];
|
|
193
246
|
|
|
194
247
|
if (opts.uninstall) {
|
|
248
|
+
// X4 fix: count what was actually removed in each phase so the
|
|
249
|
+
// shim can print a truthful summary instead of a blanket "✓ state
|
|
250
|
+
// removed" that lies when the system was already clean.
|
|
251
|
+
const summary = {
|
|
252
|
+
rcBlocksRemoved: 0,
|
|
253
|
+
serviceFileRemoved: false,
|
|
254
|
+
serviceUnloadSucceeded: 0,
|
|
255
|
+
serviceUnloadFailedAllowed: 0,
|
|
256
|
+
serviceUnloadSkipped: false,
|
|
257
|
+
userDataFilesRemoved: 0,
|
|
258
|
+
userDataFilesSkipped: 0,
|
|
259
|
+
cleanupDataRequested: !!opts.cleanupData,
|
|
260
|
+
dryRun: !!opts.dryRun,
|
|
261
|
+
errors: [],
|
|
262
|
+
};
|
|
195
263
|
try {
|
|
196
264
|
const stepCount = opts.cleanupData ? 3 : 2;
|
|
197
265
|
log.step(1, stepCount, 'env-uninject', 'remove managed-block from rc files');
|
|
198
|
-
envInject.uninject({ dryRun: opts.dryRun });
|
|
266
|
+
const ei = envInject.uninject({ dryRun: opts.dryRun }) || {};
|
|
267
|
+
summary.rcBlocksRemoved = Array.isArray(ei.removed) ? ei.removed.length : 0;
|
|
199
268
|
|
|
200
269
|
log.step(2, stepCount, 'service-unregister', `OS=${process.platform}`);
|
|
201
270
|
const ur = serviceRegister.unregister({ dryRun: opts.dryRun }) || {};
|
|
202
271
|
if (Array.isArray(ur.errors) && ur.errors.length) {
|
|
203
272
|
allWarnings.push(...ur.errors);
|
|
273
|
+
summary.errors.push(...ur.errors);
|
|
204
274
|
}
|
|
275
|
+
summary.serviceFileRemoved = !!ur.serviceFileRemoved;
|
|
276
|
+
summary.serviceUnloadSucceeded = ur.succeeded || 0;
|
|
277
|
+
summary.serviceUnloadFailedAllowed = ur.failedAllowed || 0;
|
|
278
|
+
summary.serviceUnloadSkipped = !!ur.skipped;
|
|
205
279
|
|
|
206
280
|
// B29: optional user-data cleanup.
|
|
207
281
|
if (opts.cleanupData) {
|
|
208
|
-
log.step(3, stepCount, 'cleanup-user-data', 'delete auth.json/config.toml/lock/logs');
|
|
209
|
-
cleanupUserData({ dryRun: opts.dryRun });
|
|
282
|
+
log.step(3, stepCount, 'cleanup-user-data', 'delete auth.json/config.toml/lock/logs/cache');
|
|
283
|
+
const cd = cleanupUserData({ dryRun: opts.dryRun }) || {};
|
|
284
|
+
summary.userDataFilesRemoved = Array.isArray(cd.removed) ? cd.removed.length : 0;
|
|
285
|
+
summary.userDataFilesSkipped = Array.isArray(cd.skipped) ? cd.skipped.length : 0;
|
|
210
286
|
} else {
|
|
211
287
|
log.info(
|
|
212
288
|
'uninstall: user data preserved (auth.json/config.toml/lock/logs). ' +
|
|
@@ -218,8 +294,11 @@ function main(argv) {
|
|
|
218
294
|
if (opts.verbose) {
|
|
219
295
|
process.stderr.write(`${err.stack}\n`);
|
|
220
296
|
}
|
|
297
|
+
summary.errors.push(err.message);
|
|
298
|
+
writeSummaryFile(summary);
|
|
221
299
|
return 0;
|
|
222
300
|
}
|
|
301
|
+
writeSummaryFile(summary);
|
|
223
302
|
if (allWarnings.length) {
|
|
224
303
|
log.warn(`postinstall wizard finished (${mode} uninstall) with ${allWarnings.length} warning(s)`);
|
|
225
304
|
} 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 };
|