skalpel 3.0.11 → 3.0.13
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/INSTALL.md +19 -1
- package/npm-bin/skalpel.js +37 -15
- package/package.json +6 -6
- package/postinstall/index.js +12 -215
- package/postinstall/lib/env-inject.js +1 -31
- package/postinstall/lib/rc-edit.js +49 -18
- package/postinstall/lib/rc-edit.test.js +7 -5
- package/postinstall/lib/service-register.js +1 -139
- package/postinstall/lib/sign-in.js +24 -42
- package/postinstall/systemd/skalpeld.service.tmpl +2 -1
- package/postinstall/windows/Task.xml.tmpl +1 -1
package/INSTALL.md
CHANGED
|
@@ -75,7 +75,7 @@ A user who has rolled back to a previous version (by running `npm install -g ska
|
|
|
75
75
|
npx skalpel uninstall
|
|
76
76
|
```
|
|
77
77
|
|
|
78
|
-
is the canonical one-liner for a clean, full removal on every supported OS. It works whether or not skalpel was previously installed globally:
|
|
78
|
+
is the canonical one-liner for a clean, full removal on every supported OS. The Go binary owns the state cleanup; the Node shim owns `npm uninstall -g`. The watchdog catches `npm uninstall -g skalpel` alone within ~60s. It works whether or not skalpel was previously installed globally:
|
|
79
79
|
|
|
80
80
|
1. Unregisters the per-OS service entry (launchd on macOS, systemd user unit on Linux, Task Scheduler entry on Windows).
|
|
81
81
|
2. Removes the managed shell-rc block injected on install.
|
|
@@ -92,6 +92,24 @@ npm 7+ removed the `preuninstall`/`uninstall`/`postuninstall` lifecycle scripts;
|
|
|
92
92
|
|
|
93
93
|
The configuration directory itself (auth.json + config.toml + cache + logs) is deliberately wiped by the default uninstall so a reinstalled skalpel starts from a known state. Users who want to reinstall and keep their auth + engine toggles should pass `--keep-data`.
|
|
94
94
|
|
|
95
|
+
### Watchdog and cleanup.lock
|
|
96
|
+
|
|
97
|
+
`skalpel uninstall` is one path to a clean machine; `npm uninstall -g skalpel` followed by no further action is another. The daemon's **watchdog** subsystem closes the loop on the second path. Every ~30s, `skalpeld` polls its own executable path; when the binary disappears (because npm just nuked the global `node_modules`), it self-runs the same cleanup the Go CLI does, then exits.
|
|
98
|
+
|
|
99
|
+
To opt out (e.g. on shared hosts where the daemon is centrally managed), set `[daemon] watchdog_enabled = false` in `~/.config/skalpel/config.toml`:
|
|
100
|
+
|
|
101
|
+
```toml
|
|
102
|
+
[daemon]
|
|
103
|
+
watchdog_enabled = false
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`cleanup.lock` is a small marker file `<configDir>/cleanup.lock` that `skalpel uninstall` writes before any destructive step. It contains `<pid>\n<unix_ms>\n`. The watchdog reads it every tick:
|
|
107
|
+
|
|
108
|
+
- If the lock's PID is alive AND its timestamp is fresher than 10 minutes → stand down (a user-initiated uninstall owns this window).
|
|
109
|
+
- Else (dead PID or stale timestamp) → ignore the lock and proceed as if it weren't there.
|
|
110
|
+
|
|
111
|
+
`cleanup.lock` is safe to delete manually if you see one outside an active uninstall — the next watchdog tick will recover.
|
|
112
|
+
|
|
95
113
|
## Version coupling
|
|
96
114
|
|
|
97
115
|
`skalpel` and `skalpeld` carry the same version string at all times. The release workflow produces both from the same git tag; the npm package's `version` field, the binaries' embedded version metadata, and the cosign signatures all reference the same value. There is no path by which the two could diverge on a machine that received either via a supported install path.
|
package/npm-bin/skalpel.js
CHANGED
|
@@ -143,6 +143,24 @@ function isInfoArg(argv) {
|
|
|
143
143
|
function resolveBinary(name, argv) {
|
|
144
144
|
const infoOnly = isInfoArg(argv || []);
|
|
145
145
|
const failExit = infoOnly ? 0 : 1;
|
|
146
|
+
// Local-dev override: SKALPEL_BIN_DIR points at a directory holding
|
|
147
|
+
// freshly-built `skalpel` / `skalpeld` binaries (e.g. ./bin from a
|
|
148
|
+
// `make build`). When set we skip the optionalDependencies lookup
|
|
149
|
+
// entirely so `npm link` users can iterate without rebuilding the
|
|
150
|
+
// platform tarballs each cycle.
|
|
151
|
+
const overrideDir = process.env.SKALPEL_BIN_DIR;
|
|
152
|
+
if (overrideDir) {
|
|
153
|
+
const exeOverride = process.platform === 'win32' ? `${name}.exe` : name;
|
|
154
|
+
const candidate = path.join(overrideDir, exeOverride);
|
|
155
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
156
|
+
emitError(
|
|
157
|
+
process.stderr,
|
|
158
|
+
'SKALPEL_BIN_DIR override missing binary',
|
|
159
|
+
`Expected ${candidate} but it does not exist.`,
|
|
160
|
+
`→ Run \`make build\` in the repo root, or unset SKALPEL_BIN_DIR.`
|
|
161
|
+
);
|
|
162
|
+
process.exit(failExit);
|
|
163
|
+
}
|
|
146
164
|
const key = `${process.platform}-${process.arch}`;
|
|
147
165
|
const pkg = PLATFORM_PACKAGES[key];
|
|
148
166
|
if (!pkg) {
|
|
@@ -230,22 +248,26 @@ function runUninstall(rest) {
|
|
|
230
248
|
return 0;
|
|
231
249
|
}
|
|
232
250
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
return 1;
|
|
251
|
+
// Phase-11 (Go port): the install-side Node postinstall used to
|
|
252
|
+
// own uninstall as well; the cleanup is now a Go subcommand on the
|
|
253
|
+
// platform binary. The shim still owns the npm-package removal
|
|
254
|
+
// below (detectGlobalInstall → npm uninstall -g), but the
|
|
255
|
+
// state-tearing is dispatched to the binary.
|
|
256
|
+
const goBinPath = resolveBinary('skalpel', null);
|
|
257
|
+
if (!goBinPath) {
|
|
258
|
+
return 1; // resolveBinary already emitted a structured error.
|
|
242
259
|
}
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
260
|
+
const goArgs = ['uninstall'];
|
|
261
|
+
// Flag inversion: the shim's defaults (cleanupData=true,
|
|
262
|
+
// removePackage=true, dryRun=false) become the Go binary's *implicit*
|
|
263
|
+
// defaults, so we only forward an inverted flag when the user opted
|
|
264
|
+
// out (or, for dryRun, opted in).
|
|
265
|
+
if (!cleanupData) goArgs.push('--keep-data');
|
|
266
|
+
if (!removePackage) goArgs.push('--keep-package');
|
|
267
|
+
if (dryRun) goArgs.push('--dry-run');
|
|
246
268
|
|
|
247
|
-
// X4 fix: pass a temp summary file path;
|
|
248
|
-
// structure with per-phase counts so we can print a truthful
|
|
269
|
+
// X4 fix: pass a temp summary file path; the Go binary writes a
|
|
270
|
+
// JSON structure with per-phase counts so we can print a truthful
|
|
249
271
|
// message instead of unconditionally claiming success.
|
|
250
272
|
const summaryFile = path.join(
|
|
251
273
|
os.tmpdir(),
|
|
@@ -255,7 +277,7 @@ function runUninstall(rest) {
|
|
|
255
277
|
SKALPEL_UNINSTALL_SUMMARY_FILE: summaryFile,
|
|
256
278
|
});
|
|
257
279
|
|
|
258
|
-
const result = spawnSync(
|
|
280
|
+
const result = spawnSync(goBinPath, goArgs, {
|
|
259
281
|
stdio: 'inherit',
|
|
260
282
|
env: childEnv,
|
|
261
283
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skalpel",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.13",
|
|
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",
|
|
@@ -53,10 +53,10 @@
|
|
|
53
53
|
"x64"
|
|
54
54
|
],
|
|
55
55
|
"optionalDependencies": {
|
|
56
|
-
"@skalpelai/skalpel-darwin-arm64": "3.0.
|
|
57
|
-
"@skalpelai/skalpel-darwin-x64": "3.0.
|
|
58
|
-
"@skalpelai/skalpel-linux-arm64": "3.0.
|
|
59
|
-
"@skalpelai/skalpel-linux-x64": "3.0.
|
|
60
|
-
"@skalpelai/skalpel-win32-x64": "3.0.
|
|
56
|
+
"@skalpelai/skalpel-darwin-arm64": "3.0.13",
|
|
57
|
+
"@skalpelai/skalpel-darwin-x64": "3.0.13",
|
|
58
|
+
"@skalpelai/skalpel-linux-arm64": "3.0.13",
|
|
59
|
+
"@skalpelai/skalpel-linux-x64": "3.0.13",
|
|
60
|
+
"@skalpelai/skalpel-win32-x64": "3.0.13"
|
|
61
61
|
}
|
|
62
62
|
}
|
package/postinstall/index.js
CHANGED
|
@@ -44,8 +44,6 @@ function parseArgs(argv) {
|
|
|
44
44
|
dryRun: false,
|
|
45
45
|
skipBundle: false,
|
|
46
46
|
verbose: false,
|
|
47
|
-
uninstall: false,
|
|
48
|
-
cleanupData: false,
|
|
49
47
|
};
|
|
50
48
|
for (const a of argv.slice(2)) {
|
|
51
49
|
switch (a) {
|
|
@@ -58,14 +56,6 @@ function parseArgs(argv) {
|
|
|
58
56
|
case '--skip-bundle':
|
|
59
57
|
out.skipBundle = true;
|
|
60
58
|
break;
|
|
61
|
-
case '--uninstall':
|
|
62
|
-
out.uninstall = true;
|
|
63
|
-
break;
|
|
64
|
-
case '--cleanup-data':
|
|
65
|
-
// B29: opt-in flag to delete auth.json / config.toml /
|
|
66
|
-
// skalpeld.lock / logs/ during --uninstall.
|
|
67
|
-
out.cleanupData = true;
|
|
68
|
-
break;
|
|
69
59
|
case '--help':
|
|
70
60
|
case '-h':
|
|
71
61
|
out.help = true;
|
|
@@ -79,9 +69,6 @@ function parseArgs(argv) {
|
|
|
79
69
|
if (process.env.SKALPEL_INSTALL_DRY_RUN === '1') {
|
|
80
70
|
out.dryRun = true;
|
|
81
71
|
}
|
|
82
|
-
if (process.env.SKALPEL_UNINSTALL_CLEAN === '1') {
|
|
83
|
-
out.cleanupData = true;
|
|
84
|
-
}
|
|
85
72
|
return out;
|
|
86
73
|
}
|
|
87
74
|
|
|
@@ -89,7 +76,7 @@ function helpText() {
|
|
|
89
76
|
return [
|
|
90
77
|
'skalpel postinstall wizard',
|
|
91
78
|
'',
|
|
92
|
-
'usage: node postinstall/index.js [--dry-run] [--verbose]
|
|
79
|
+
'usage: node postinstall/index.js [--dry-run] [--verbose]',
|
|
93
80
|
'',
|
|
94
81
|
'Run automatically by npm install. The wizard performs:',
|
|
95
82
|
' 1. detect-prior probe configDir for an existing install',
|
|
@@ -98,127 +85,14 @@ function helpText() {
|
|
|
98
85
|
' 4. env-inject update shell rc managed-block',
|
|
99
86
|
' 5. launch print the run-skalpel hint',
|
|
100
87
|
'',
|
|
101
|
-
'
|
|
102
|
-
'
|
|
103
|
-
'--cleanup-data (with --uninstall) also deletes auth.json,',
|
|
104
|
-
' config.toml, skalpeld.lock, and logs/. Equivalent env var:',
|
|
105
|
-
' SKALPEL_UNINSTALL_CLEAN=1.',
|
|
88
|
+
'Uninstall is owned by the Go binary now — run `skalpel uninstall`',
|
|
89
|
+
'or `npx skalpel uninstall` to reverse this wizard.',
|
|
106
90
|
'',
|
|
107
91
|
'Dry-run mode (SKALPEL_INSTALL_DRY_RUN=1 or --dry-run) makes every',
|
|
108
92
|
'step log what it would do and write nothing.',
|
|
109
93
|
].join('\n');
|
|
110
94
|
}
|
|
111
95
|
|
|
112
|
-
// B29: best-effort delete of user data on --uninstall --cleanup-data.
|
|
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.
|
|
118
|
-
function cleanupUserData({ dryRun }) {
|
|
119
|
-
const removed = [];
|
|
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
|
|
135
|
-
];
|
|
136
|
-
|
|
137
|
-
for (const t of fileTargets) {
|
|
138
|
-
const present = fs.existsSync(t);
|
|
139
|
-
if (dryRun) {
|
|
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
|
-
}
|
|
147
|
-
continue;
|
|
148
|
-
}
|
|
149
|
-
try {
|
|
150
|
-
if (present) {
|
|
151
|
-
fs.rmSync(t, { force: true });
|
|
152
|
-
log.info(`uninstall-cleanup: removed ${t}`);
|
|
153
|
-
removed.push(t);
|
|
154
|
-
} else {
|
|
155
|
-
log.info(`uninstall-cleanup: ${t} absent — skip`);
|
|
156
|
-
skipped.push(t);
|
|
157
|
-
}
|
|
158
|
-
} catch (err) {
|
|
159
|
-
log.warn(`uninstall-cleanup: rm ${t} failed: ${err.message}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
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
|
-
}
|
|
174
|
-
try {
|
|
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);
|
|
182
|
-
}
|
|
183
|
-
} catch (err) {
|
|
184
|
-
log.warn(`uninstall-cleanup: rm -r ${d} failed: ${err.message}`);
|
|
185
|
-
}
|
|
186
|
-
}
|
|
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.
|
|
190
|
-
if (!dryRun) {
|
|
191
|
-
try {
|
|
192
|
-
fs.rmdirSync(cfg);
|
|
193
|
-
log.info(`uninstall-cleanup: removed empty ${cfg}`);
|
|
194
|
-
removed.push(cfg);
|
|
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
|
-
}
|
|
201
|
-
}
|
|
202
|
-
} else {
|
|
203
|
-
log.dryRun(`uninstall-cleanup: would rmdir (if empty) ${cfg}`);
|
|
204
|
-
}
|
|
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
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
96
|
function main(argv) {
|
|
223
97
|
const opts = parseArgs(argv);
|
|
224
98
|
if (opts.help) {
|
|
@@ -226,26 +100,13 @@ function main(argv) {
|
|
|
226
100
|
return 0;
|
|
227
101
|
}
|
|
228
102
|
|
|
229
|
-
// npx mode:
|
|
230
|
-
//
|
|
231
|
-
//
|
|
232
|
-
//
|
|
233
|
-
//
|
|
234
|
-
//
|
|
235
|
-
|
|
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) {
|
|
103
|
+
// npx mode: skip the install wizard. The wizard's service-register
|
|
104
|
+
// step would bake the npx cache path (~/.npm/_npx/<hash>/...) into
|
|
105
|
+
// the OS service unit, and that path is evicted by npx's cache GC.
|
|
106
|
+
// Exit 0 instead of 1 so npm install completes and the binary
|
|
107
|
+
// becomes invokable. (Uninstall is owned by the Go binary now and
|
|
108
|
+
// is unaffected.)
|
|
109
|
+
if (paths.isNpxInvocation()) {
|
|
249
110
|
log.info(
|
|
250
111
|
'npx mode detected — skipping install wizard (service-register / ' +
|
|
251
112
|
'env-inject would bake transient npx cache paths into your system).'
|
|
@@ -259,77 +120,13 @@ function main(argv) {
|
|
|
259
120
|
|
|
260
121
|
const total = 5;
|
|
261
122
|
const mode = opts.dryRun ? 'dry-run' : 'live';
|
|
262
|
-
|
|
263
|
-
log.info(`postinstall wizard starting (${mode} ${action}) on ${process.platform}`);
|
|
123
|
+
log.info(`postinstall wizard starting (${mode} install) on ${process.platform}`);
|
|
264
124
|
|
|
265
125
|
// B28: classify failures. Critical = re-throw and exit 0 (we do
|
|
266
126
|
// not want npm install to abort, but the operator should know).
|
|
267
127
|
// Warning = collect and log at end.
|
|
268
128
|
const allWarnings = [];
|
|
269
129
|
|
|
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
|
-
};
|
|
286
|
-
try {
|
|
287
|
-
const stepCount = opts.cleanupData ? 3 : 2;
|
|
288
|
-
log.step(1, stepCount, 'env-uninject', 'remove managed-block from rc files');
|
|
289
|
-
const ei = envInject.uninject({ dryRun: opts.dryRun }) || {};
|
|
290
|
-
summary.rcBlocksRemoved = Array.isArray(ei.removed) ? ei.removed.length : 0;
|
|
291
|
-
|
|
292
|
-
log.step(2, stepCount, 'service-unregister', `OS=${process.platform}`);
|
|
293
|
-
const ur = serviceRegister.unregister({ dryRun: opts.dryRun }) || {};
|
|
294
|
-
if (Array.isArray(ur.errors) && ur.errors.length) {
|
|
295
|
-
allWarnings.push(...ur.errors);
|
|
296
|
-
summary.errors.push(...ur.errors);
|
|
297
|
-
}
|
|
298
|
-
summary.serviceFileRemoved = !!ur.serviceFileRemoved;
|
|
299
|
-
summary.serviceUnloadSucceeded = ur.succeeded || 0;
|
|
300
|
-
summary.serviceUnloadFailedAllowed = ur.failedAllowed || 0;
|
|
301
|
-
summary.serviceUnloadSkipped = !!ur.skipped;
|
|
302
|
-
|
|
303
|
-
// B29: optional user-data cleanup.
|
|
304
|
-
if (opts.cleanupData) {
|
|
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;
|
|
309
|
-
} else {
|
|
310
|
-
log.info(
|
|
311
|
-
'uninstall: user data preserved (auth.json/config.toml/lock/logs). ' +
|
|
312
|
-
'Pass --cleanup-data or set SKALPEL_UNINSTALL_CLEAN=1 to remove.'
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
} catch (err) {
|
|
316
|
-
log.error(`uninstall failed: ${err.message}`);
|
|
317
|
-
if (opts.verbose) {
|
|
318
|
-
process.stderr.write(`${err.stack}\n`);
|
|
319
|
-
}
|
|
320
|
-
summary.errors.push(err.message);
|
|
321
|
-
writeSummaryFile(summary);
|
|
322
|
-
return 0;
|
|
323
|
-
}
|
|
324
|
-
writeSummaryFile(summary);
|
|
325
|
-
if (allWarnings.length) {
|
|
326
|
-
log.warn(`postinstall wizard finished (${mode} uninstall) with ${allWarnings.length} warning(s)`);
|
|
327
|
-
} else {
|
|
328
|
-
log.info(`postinstall wizard finished (${mode} uninstall)`);
|
|
329
|
-
}
|
|
330
|
-
return 0;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
130
|
let prior = false;
|
|
334
131
|
let critical = null;
|
|
335
132
|
try {
|
|
@@ -393,4 +190,4 @@ if (require.main === module) {
|
|
|
393
190
|
process.exit(main(process.argv));
|
|
394
191
|
}
|
|
395
192
|
|
|
396
|
-
module.exports = { main, parseArgs
|
|
193
|
+
module.exports = { main, parseArgs };
|
|
@@ -95,34 +95,4 @@ function run({ dryRun, port }) {
|
|
|
95
95
|
return { touched };
|
|
96
96
|
}
|
|
97
97
|
|
|
98
|
-
|
|
99
|
-
const rcs = paths.rcFiles();
|
|
100
|
-
const removed = [];
|
|
101
|
-
for (const rc of rcs) {
|
|
102
|
-
const exists = fs.existsSync(rc.path);
|
|
103
|
-
if (!exists) {
|
|
104
|
-
log.info(`uninstall: ${rc.shell} rc absent (${rc.path}) — skip`);
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
if (dryRun) {
|
|
108
|
-
log.dryRun(`uninstall: would remove managed block from ${rc.path} (${rc.shell})`);
|
|
109
|
-
removed.push(rc.path);
|
|
110
|
-
continue;
|
|
111
|
-
}
|
|
112
|
-
const r = rcEdit.removeBlock({ shell: rc.shell, file: rc.path });
|
|
113
|
-
if (r.changed) {
|
|
114
|
-
log.info(`uninstall: removed managed block from ${rc.path} (${rc.shell})`);
|
|
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`);
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
return { removed };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
module.exports = { run, uninject, fallbackRc };
|
|
98
|
+
module.exports = { run, fallbackRc };
|
|
@@ -48,6 +48,52 @@ function shellEscapePsSQ(v) {
|
|
|
48
48
|
return String(v).replace(/'/g, "''");
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// agentWrapPosix is a tiny bash/zsh-portable shim that prints a
|
|
52
|
+
// one-line "skalpel active/inactive" hint right before launching
|
|
53
|
+
// `claude`. The status is computed by `skalpel status` which checks
|
|
54
|
+
// both the daemon socket AND the compressor toggle in config.toml so
|
|
55
|
+
// the line accurately reflects whether anything will happen on the
|
|
56
|
+
// next request.
|
|
57
|
+
//
|
|
58
|
+
// Self-disabling knobs:
|
|
59
|
+
// - SKALPEL_NO_AGENT_WRAP=1 → operator opt-out (env var, anywhere).
|
|
60
|
+
// - existing `claude` alias → we don't override aliases.
|
|
61
|
+
// - `skalpel` not on PATH → wrapper installs nothing.
|
|
62
|
+
const agentWrapPosix = `
|
|
63
|
+
# Pre-launch status hint for coding agents launched in this shell.
|
|
64
|
+
# Status comes from \`skalpel status\` (checks daemon + engine config).
|
|
65
|
+
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
66
|
+
if [ -z "\${SKALPEL_NO_AGENT_WRAP:-}" ] && ! alias claude >/dev/null 2>&1 && command -v skalpel >/dev/null 2>&1; then
|
|
67
|
+
claude() { skalpel status >&2; command claude "$@"; }
|
|
68
|
+
fi`;
|
|
69
|
+
|
|
70
|
+
// agentWrapFish is the fish-shell port of agentWrapPosix.
|
|
71
|
+
const agentWrapFish = `
|
|
72
|
+
# Pre-launch status hint for coding agents launched in this shell.
|
|
73
|
+
# Status comes from \`skalpel status\` (checks daemon + engine config).
|
|
74
|
+
# Set SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
75
|
+
if not set -q SKALPEL_NO_AGENT_WRAP; and not functions -q claude; and command -q skalpel
|
|
76
|
+
function claude
|
|
77
|
+
skalpel status 1>&2
|
|
78
|
+
command claude $argv
|
|
79
|
+
end
|
|
80
|
+
end`;
|
|
81
|
+
|
|
82
|
+
// agentWrapPosh is the PowerShell port. Same delegation: ask the
|
|
83
|
+
// `skalpel` binary for the status, write it to stderr, then forward.
|
|
84
|
+
const agentWrapPosh = `
|
|
85
|
+
# Pre-launch status hint for coding agents launched in this shell.
|
|
86
|
+
# Status comes from \`skalpel status\` (checks daemon + engine config).
|
|
87
|
+
# Set $env:SKALPEL_NO_AGENT_WRAP=1 to disable.
|
|
88
|
+
$_skalpelOrigClaude = Get-Command claude.exe -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
89
|
+
$_skalpelStatusBin = Get-Command skalpel -CommandType Application -ErrorAction SilentlyContinue | Select-Object -First 1
|
|
90
|
+
if (-not $env:SKALPEL_NO_AGENT_WRAP -and $_skalpelOrigClaude -and $_skalpelStatusBin) {
|
|
91
|
+
function global:claude {
|
|
92
|
+
& $script:_skalpelStatusBin.Source status 1>&2
|
|
93
|
+
& $script:_skalpelOrigClaude.Source @args
|
|
94
|
+
}
|
|
95
|
+
}`;
|
|
96
|
+
|
|
51
97
|
function bodyFor(shell, env) {
|
|
52
98
|
const note =
|
|
53
99
|
'This block is managed by skalpel install. Do not edit by hand;\n' +
|
|
@@ -59,6 +105,7 @@ function bodyFor(shell, env) {
|
|
|
59
105
|
...Object.entries(env).map(
|
|
60
106
|
([k, v]) => `set -gx ${k} "${shellEscapePosixDQ(v)}"`
|
|
61
107
|
),
|
|
108
|
+
agentWrapFish,
|
|
62
109
|
].join('\n');
|
|
63
110
|
case 'powershell':
|
|
64
111
|
case 'powershell-legacy':
|
|
@@ -67,6 +114,7 @@ function bodyFor(shell, env) {
|
|
|
67
114
|
...Object.entries(env).map(
|
|
68
115
|
([k, v]) => `$env:${k} = '${shellEscapePsSQ(v)}'`
|
|
69
116
|
),
|
|
117
|
+
agentWrapPosh,
|
|
70
118
|
].join('\n');
|
|
71
119
|
default:
|
|
72
120
|
return [
|
|
@@ -74,6 +122,7 @@ function bodyFor(shell, env) {
|
|
|
74
122
|
...Object.entries(env).map(
|
|
75
123
|
([k, v]) => `export ${k}="${shellEscapePosixDQ(v)}"`
|
|
76
124
|
),
|
|
125
|
+
agentWrapPosix,
|
|
77
126
|
].join('\n');
|
|
78
127
|
}
|
|
79
128
|
}
|
|
@@ -122,23 +171,6 @@ function applyBlock({ shell, file, env, dryRun }) {
|
|
|
122
171
|
return { changed: true };
|
|
123
172
|
}
|
|
124
173
|
|
|
125
|
-
function removeBlock({ shell, file, dryRun }) {
|
|
126
|
-
if (!fs.existsSync(file)) {
|
|
127
|
-
return { changed: false, missing: true };
|
|
128
|
-
}
|
|
129
|
-
const current = fs.readFileSync(file, 'utf8');
|
|
130
|
-
const re = blockRegex(shell);
|
|
131
|
-
const next = current.replace(re, '');
|
|
132
|
-
if (current === next) {
|
|
133
|
-
return { changed: false };
|
|
134
|
-
}
|
|
135
|
-
if (dryRun) {
|
|
136
|
-
return { changed: true, dryRun: true };
|
|
137
|
-
}
|
|
138
|
-
fs.writeFileSync(file, next);
|
|
139
|
-
return { changed: true };
|
|
140
|
-
}
|
|
141
|
-
|
|
142
174
|
// xmlEscape: B47 — used by Task.xml renderer.
|
|
143
175
|
function xmlEscape(v) {
|
|
144
176
|
return String(v)
|
|
@@ -154,7 +186,6 @@ module.exports = {
|
|
|
154
186
|
buildBlock,
|
|
155
187
|
rewrite,
|
|
156
188
|
applyBlock,
|
|
157
|
-
removeBlock,
|
|
158
189
|
fenceFor,
|
|
159
190
|
bodyFor,
|
|
160
191
|
shellEscapePosixDQ,
|
|
@@ -145,15 +145,17 @@ function run() {
|
|
|
145
145
|
}
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
-
test('
|
|
149
|
-
|
|
148
|
+
test('TestRcEdit_ApplyBlock_Leaves_User_Content_Untouched', () => {
|
|
149
|
+
// Install-side counterpart to the old removeBlock test: apply the
|
|
150
|
+
// managed block and confirm the user's own export line still
|
|
151
|
+
// exists in the rewritten file. (Removal lives in Go now;
|
|
152
|
+
// see internal/cleanup/rcedit for its parity tests.)
|
|
153
|
+
const file = tmpFile('rcedit-apply-user');
|
|
150
154
|
fs.writeFileSync(file, 'export USER_VAR=1\n');
|
|
151
155
|
try {
|
|
152
156
|
rc.applyBlock({ shell: 'bash', file, env: rc.envBlockValues(7878) });
|
|
153
|
-
const r = rc.removeBlock({ shell: 'bash', file });
|
|
154
|
-
assert.strictEqual(r.changed, true);
|
|
155
157
|
const after = fs.readFileSync(file, 'utf8');
|
|
156
|
-
assert.ok(
|
|
158
|
+
assert.ok(after.includes(rc.FENCE_BEGIN_POSIX), 'managed block applied');
|
|
157
159
|
assert.ok(after.includes('export USER_VAR=1'), 'user var preserved');
|
|
158
160
|
} finally {
|
|
159
161
|
fs.unlinkSync(file);
|
|
@@ -99,29 +99,6 @@ function loadCommands() {
|
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
function unloadCommands() {
|
|
103
|
-
switch (process.platform) {
|
|
104
|
-
case 'darwin': {
|
|
105
|
-
const target = launchdTarget();
|
|
106
|
-
const fullTarget =
|
|
107
|
-
target === 'system' ? 'system/ai.skalpel.daemon' : `${target}/ai.skalpel.daemon`;
|
|
108
|
-
return [
|
|
109
|
-
[LAUNCHCTL_PATH, ['bootout', fullTarget], { allowFail: true }],
|
|
110
|
-
];
|
|
111
|
-
}
|
|
112
|
-
case 'win32': {
|
|
113
|
-
const ps1 = renderedPowerShellPath();
|
|
114
|
-
return [
|
|
115
|
-
['powershell.exe', ['-NoProfile', '-File', ps1, '-Action', 'unregister'], { allowFail: true }],
|
|
116
|
-
];
|
|
117
|
-
}
|
|
118
|
-
default:
|
|
119
|
-
return [
|
|
120
|
-
['systemctl', ['--user', 'disable', '--now', 'skalpel-daemon.service'], { allowFail: true }],
|
|
121
|
-
];
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
|
|
125
102
|
// B42: validate the daemon binary exists at the path the service
|
|
126
103
|
// unit will exec. SHA256 verification is deferred (TODO below) until
|
|
127
104
|
// dist.shasum is populated in package.json. The existence check is
|
|
@@ -161,121 +138,6 @@ function verifyBinary() {
|
|
|
161
138
|
return { bin, stat };
|
|
162
139
|
}
|
|
163
140
|
|
|
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.
|
|
194
|
-
function unregister({ dryRun }) {
|
|
195
|
-
const dest = paths.servicePath();
|
|
196
|
-
const ps1 = renderedPowerShellPath();
|
|
197
|
-
const servicePresent = fs.existsSync(dest);
|
|
198
|
-
const ps1Present = fs.existsSync(ps1);
|
|
199
|
-
|
|
200
|
-
if (dryRun) {
|
|
201
|
-
log.dryRun(`uninstall: would unregister service at ${dest}`);
|
|
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
|
-
}
|
|
211
|
-
}
|
|
212
|
-
log.dryRun(` would rm ${dest}`);
|
|
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();
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
let succeeded = 0;
|
|
231
|
-
let failedAllowed = 0;
|
|
232
|
-
const errors = [];
|
|
233
|
-
|
|
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}`;
|
|
244
|
-
errors.push(msg);
|
|
245
|
-
log.warn(`${msg}; rolling forward to file removal`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
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) {
|
|
261
|
-
fs.unlinkSync(dest);
|
|
262
|
-
log.info(`uninstall: removed ${dest}`);
|
|
263
|
-
serviceFileRemoved = true;
|
|
264
|
-
}
|
|
265
|
-
if (process.platform === 'win32' && ps1Present) {
|
|
266
|
-
fs.unlinkSync(ps1);
|
|
267
|
-
log.info(`uninstall: removed ${ps1}`);
|
|
268
|
-
}
|
|
269
|
-
return {
|
|
270
|
-
removed: serviceFileRemoved,
|
|
271
|
-
serviceFileRemoved,
|
|
272
|
-
succeeded,
|
|
273
|
-
skipped: cmds.length === 0 ? 1 : 0,
|
|
274
|
-
failedAllowed,
|
|
275
|
-
errors,
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
141
|
function run({ dryRun }) {
|
|
280
142
|
const tmpl = templatePath();
|
|
281
143
|
const dest = paths.servicePath();
|
|
@@ -351,4 +213,4 @@ function run({ dryRun }) {
|
|
|
351
213
|
return { skipped: false, registered: true, warnings };
|
|
352
214
|
}
|
|
353
215
|
|
|
354
|
-
module.exports = { run,
|
|
216
|
+
module.exports = { run, templatePath, loadCommands, launchdTarget };
|
|
@@ -46,53 +46,35 @@ function resolveDispatcher() {
|
|
|
46
46
|
|
|
47
47
|
function run({ dryRun, prior }) {
|
|
48
48
|
if (prior) {
|
|
49
|
-
log.info('sign-in: prior auth.json detected — skipping
|
|
49
|
+
log.info('sign-in: prior auth.json detected — skipping');
|
|
50
50
|
return { skipped: true };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
// Per UX request 2026-05-14: postinstall must NOT pop a browser
|
|
54
|
+
// unprompted. The first-time-user contract is now:
|
|
55
|
+
// 1. npm install completes silently (no auto-launched browser).
|
|
56
|
+
// 2. Postinstall prints "all set — run `skalpel` to open the TUI".
|
|
57
|
+
// 3. On first `skalpel` invocation, the TUI detects missing
|
|
58
|
+
// auth.json and renders an in-TUI sign-in gate (press Enter
|
|
59
|
+
// to open the browser, q to quit). See the runTUI auth gate
|
|
60
|
+
// in cmd/skalpel/main.go.
|
|
61
|
+
//
|
|
62
|
+
// We keep this module's surface unchanged so postinstall/index.js
|
|
63
|
+
// does not need to special-case the deferred path; we just return
|
|
64
|
+
// `skipped: true` whether or not auth exists.
|
|
65
|
+
log.info('sign-in: deferred — first `skalpel` launch will gate sign-in');
|
|
55
66
|
if (dryRun) {
|
|
56
|
-
log.dryRun(
|
|
57
|
-
|
|
58
|
-
'(loopback browser flow with manual-paste fallback)'
|
|
59
|
-
);
|
|
60
|
-
return { skipped: false, dryRun: true };
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
if (!dispatcher) {
|
|
64
|
-
log.warn(
|
|
65
|
-
'sign-in: dispatch shim not resolvable; user must run `skalpel login` ' +
|
|
66
|
-
'manually after install'
|
|
67
|
-
);
|
|
68
|
-
return { skipped: true, reason: 'no-dispatcher' };
|
|
67
|
+
log.dryRun('step 2 sign-in: would defer (no postinstall browser pop)');
|
|
68
|
+
return { skipped: true, dryRun: true, deferred: true };
|
|
69
69
|
}
|
|
70
|
-
|
|
71
|
-
// B6: actually spawn `skalpel login`. The subprocess opens the
|
|
72
|
-
// browser, waits for the loopback callback, and writes auth.json.
|
|
73
|
-
// We propagate stdio so the user can see the prompt; we cap at
|
|
74
|
-
// SIGN_IN_TIMEOUT_MS so a runaway hang does not block npm install.
|
|
75
|
-
log.info(
|
|
76
|
-
'sign-in: launching `skalpel login` (loopback browser flow); ' +
|
|
77
|
-
`timeout ${Math.round(SIGN_IN_TIMEOUT_MS / 1000)}s`
|
|
78
|
-
);
|
|
79
|
-
const r = spawnSync(process.execPath, [dispatcher, 'login'], {
|
|
80
|
-
stdio: 'inherit',
|
|
81
|
-
timeout: SIGN_IN_TIMEOUT_MS,
|
|
82
|
-
});
|
|
83
|
-
if (r.error) {
|
|
84
|
-
log.warn(`sign-in: spawn error: ${r.error.message}; user can re-run \`skalpel login\``);
|
|
85
|
-
return { skipped: false, error: r.error.message };
|
|
86
|
-
}
|
|
87
|
-
if (r.status !== 0) {
|
|
88
|
-
log.warn(
|
|
89
|
-
`sign-in: \`skalpel login\` exited ${r.status}; user can re-run after install ` +
|
|
90
|
-
`(config dir: ${paths.configDir()})`
|
|
91
|
-
);
|
|
92
|
-
return { skipped: false, exitCode: r.status };
|
|
93
|
-
}
|
|
94
|
-
log.info(`sign-in: ok (config dir: ${paths.configDir()})`);
|
|
95
|
-
return { skipped: false, ok: true };
|
|
70
|
+
return { skipped: true, deferred: true };
|
|
96
71
|
}
|
|
97
72
|
|
|
73
|
+
// resolveDispatcher and SIGN_IN_TIMEOUT_MS are retained at module
|
|
74
|
+
// scope (used by no callers today) so the spawn-on-install behaviour
|
|
75
|
+
// can be reinstated with a single edit if the deferred-auth UX needs
|
|
76
|
+
// to be reverted.
|
|
77
|
+
void resolveDispatcher;
|
|
78
|
+
void SIGN_IN_TIMEOUT_MS;
|
|
79
|
+
|
|
98
80
|
module.exports = { run, resolveDispatcher };
|