skalpel 3.0.8 → 3.0.10

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 CHANGED
@@ -71,11 +71,26 @@ A user who has rolled back to a previous version (by running `npm install -g ska
71
71
 
72
72
  ## Uninstall
73
73
 
74
- `skalpel uninstall` is the one-liner for a clean removal. It unregisters the per-OS service entry (launchd / systemd user / Task Scheduler), removes the managed shell-rc block, and deletes `auth.json`, `config.toml`, `skalpeld.lock`, and `logs/` from the per-OS configuration directory. To preserve user data (engine toggles, active org, cached auth), pass `--keep-data`. `--dry-run` previews every action without writing anything. The command does not remove the npm package itself; after running `skalpel uninstall`, finish with `npm uninstall -g skalpel`.
74
+ ```
75
+ npx skalpel uninstall
76
+ ```
75
77
 
76
- `npm uninstall -g skalpel` on its own removes both binaries. There is no separate uninstall command for `skalpeld`; the package is the unit of removal. Before npm removes the binaries it fires the package's `preuninstall` hook, which invokes `node postinstall/index.js --uninstall`; that pass removes the rc-file managed-block injected on install (so the user's shell stops pointing at the now-defunct local proxy port) and unregisters the per-OS service entry (so `launchctl`, `systemctl --user`, or `schtasks` stops trying to start a daemon whose binary is about to vanish). The shim and registration cleanup happen before the binaries themselves are removed; an interrupted uninstall leaves a coherent intermediate state. The `preuninstall` hook deliberately preserves user data on disk so that `npm uninstall -g skalpel` followed by `npm install -g skalpel` is a non-destructive reinstall; the `skalpel uninstall` one-liner above is the path for users who want a full wipe.
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:
77
79
 
78
- `npx skalpel` users have no global install to uninstall; the npx cache is cleaned by npm on its own schedule. For an `npx`-only user who wants to clean up shell rc-file and service registration without waiting on cache eviction, `skalpel uninstall` performs the same cleanup the `preuninstall` hook would, plus the user-data wipe.
80
+ 1. Unregisters the per-OS service entry (launchd on macOS, systemd user unit on Linux, Task Scheduler entry on Windows).
81
+ 2. Removes the managed shell-rc block injected on install.
82
+ 3. Deletes user state under the per-OS configuration directory: `auth.json`, `config.toml`, `skalpeld.lock`, `logs/`, and `cache/`.
83
+ 4. Auto-detects a global npm install (via `npm prefix -g`) and removes it via `npm uninstall -g skalpel`, so the `skalpel` binary leaves your `PATH` in the same command.
84
+
85
+ Flags: `--keep-data` preserves user-data files (engine toggles, cached auth) for a future reinstall; `--keep-package` preserves the global npm package (only the state cleanup runs); `--dry-run` previews every action without writing anything.
86
+
87
+ `skalpel uninstall` from a global install behaves identically — same flags, same auto-removal of the npm package. Pick whichever is convenient: `npx skalpel uninstall` requires no prior install; `skalpel uninstall` is faster on a machine that already has the binary on `PATH`.
88
+
89
+ ### Why `npm uninstall -g skalpel` is not the documented path
90
+
91
+ npm 7+ removed the `preuninstall`/`uninstall`/`postuninstall` lifecycle scripts; the `npm uninstall` command runs Arborist's `reify` which simply deletes files from the global `node_modules` and returns. There is no hook left for skalpel to clean up the rc-file managed block, the service entry, or user data on `npm uninstall -g skalpel` alone. Running `npm uninstall -g skalpel` without first running `skalpel uninstall` (or `npx skalpel uninstall`) leaves orphaned shell-rc env vars pointing at a dead proxy port and an orphaned launchd/systemd/Task Scheduler entry trying to start a binary that no longer exists. The `npx skalpel uninstall` one-liner above does both the state cleanup and the package removal in a single command — that's why it's the documented path.
92
+
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`.
79
94
 
80
95
  ## Version coupling
81
96
 
@@ -189,6 +189,7 @@ function resolveBinary(name, argv) {
189
189
  // only place this command can live.
190
190
  function runUninstall(rest) {
191
191
  let cleanupData = true;
192
+ let removePackage = true;
192
193
  let dryRun = false;
193
194
  let showHelp = false;
194
195
  for (const a of rest) {
@@ -196,6 +197,9 @@ function runUninstall(rest) {
196
197
  case '--keep-data':
197
198
  cleanupData = false;
198
199
  break;
200
+ case '--keep-package':
201
+ removePackage = false;
202
+ break;
199
203
  case '--dry-run':
200
204
  dryRun = true;
201
205
  break;
@@ -212,14 +216,16 @@ function runUninstall(rest) {
212
216
  const head = colored ? theme.bold(theme.lilac('skalpel uninstall')) : 'skalpel uninstall';
213
217
  const dim = colored ? theme.dim : (s) => s;
214
218
  process.stdout.write(
215
- `${head} — clean up skalpel state on this machine\n\n` +
216
- `usage: skalpel uninstall [--keep-data] [--dry-run]\n\n` +
219
+ `${head} — fully remove skalpel from this machine\n\n` +
220
+ `usage: skalpel uninstall [--keep-data] [--keep-package] [--dry-run]\n\n` +
217
221
  `By default, removes:\n` +
218
222
  ` • per-OS service entry (launchd / systemd user / Task Scheduler)\n` +
219
223
  ` • managed shell-rc block injected on install\n` +
220
- ` • auth.json, config.toml, skalpeld.lock, and logs/ (use --keep-data to preserve)\n\n` +
221
- `${dim('Does not remove the npm package itself. To finish removal, run:')}\n` +
222
- ` npm uninstall -g skalpel\n`
224
+ ` • auth.json, config.toml, skalpeld.lock, logs/ and cache/ (use --keep-data to preserve)\n` +
225
+ ` • the global npm package itself (use --keep-package to preserve the binary)\n\n` +
226
+ `${dim('Equivalent invocations (both do the same thing):')}\n` +
227
+ ` skalpel uninstall ${dim('(from a global install)')}\n` +
228
+ ` npx skalpel uninstall ${dim('(no global install needed)')}\n`
223
229
  );
224
230
  return 0;
225
231
  }
@@ -298,11 +304,108 @@ function runUninstall(rest) {
298
304
  // Fallback to the old unconditional message if no summary made it back.
299
305
  process.stdout.write(`\n${ok} skalpel state removed from this machine.\n`);
300
306
  }
301
- process.stdout.write(`${dim('To finish removal, also run:')}\n`);
302
- process.stdout.write(` npm uninstall -g skalpel\n`);
307
+
308
+ // Only emit the "running agents still have stale env" hint when
309
+ // something was actually removed (or in dry-run); on already-clean
310
+ // re-runs the hint adds noise without value.
311
+ const ranAnyCleanup = summary && !(
312
+ summary.rcBlocksRemoved === 0 &&
313
+ !summary.serviceFileRemoved &&
314
+ summary.userDataFilesRemoved === 0
315
+ );
316
+ if (!summary || ranAnyCleanup) {
317
+ process.stdout.write(
318
+ `${dim('Note: any open shells or running coding agents (Claude Code, Codex, ' +
319
+ 'Cursor, etc.) still have the skalpel proxy env vars cached in their ' +
320
+ 'environment. Open a fresh shell and restart any active agents to drop ' +
321
+ 'them.')}\n`
322
+ );
323
+ }
324
+
325
+ // True-uninstall: auto-detect a global npm install of `skalpel` and
326
+ // remove it via `npm uninstall -g skalpel`. This makes
327
+ // `npx skalpel uninstall` (or `skalpel uninstall` from a global
328
+ // install) a single one-stop true uninstall on every OS.
329
+ //
330
+ // Safety notes:
331
+ // - When invoked from a global install (not via npx), we're about
332
+ // to delete the files Node loaded our shim from. Node read the
333
+ // script and closed the handle, so deletion is safe cross-OS.
334
+ // - When invoked from npx, the global install (if any) is at a
335
+ // different prefix than the npx cache, so no self-deletion.
336
+ // - We skip when --keep-package is passed or when the postinstall
337
+ // summary indicates nothing was removed and no global install
338
+ // exists (i.e. user has nothing to uninstall).
339
+ let globalRemoved = false;
340
+ if (removePackage && !dryRun) {
341
+ const detected = detectGlobalInstall();
342
+ if (detected) {
343
+ const r = removeGlobalInstall();
344
+ if (r.removed) {
345
+ globalRemoved = true;
346
+ process.stdout.write(`${ok} Removed global npm package skalpel (${detected.pkgRoot}).\n`);
347
+ } else {
348
+ process.stdout.write(
349
+ `\n${dim('Could not auto-remove the global skalpel package ')}` +
350
+ `${dim('(' + r.reason + ').')}\n` +
351
+ `Run manually:\n npm uninstall -g skalpel\n` +
352
+ dim(' (sudo may be required on some setups)\n')
353
+ );
354
+ }
355
+ }
356
+ } else if (removePackage && dryRun) {
357
+ const detected = detectGlobalInstall();
358
+ if (detected) {
359
+ process.stdout.write(`${dim('[dry-run] would run: npm uninstall -g skalpel')}\n`);
360
+ process.stdout.write(`${dim(' (' + detected.pkgRoot + ')')}\n`);
361
+ }
362
+ }
363
+
364
+ if (globalRemoved || (summary && (summary.rcBlocksRemoved || summary.serviceFileRemoved || summary.userDataFilesRemoved))) {
365
+ process.stdout.write(`\n${ok} skalpel fully removed from this machine.\n`);
366
+ }
303
367
  return 0;
304
368
  }
305
369
 
370
+ // detectGlobalInstall returns { prefix, pkgRoot } if a global npm
371
+ // install of `skalpel` exists, or null. Uses `npm prefix -g` which is
372
+ // the cross-OS canonical way to find the global root. We don't trust
373
+ // `which skalpel` because PATH ordering can hide a global install
374
+ // behind another shim.
375
+ function detectGlobalInstall() {
376
+ const r = spawnSync('npm', ['prefix', '-g'], {
377
+ encoding: 'utf8',
378
+ shell: process.platform === 'win32',
379
+ });
380
+ if (r.status !== 0 || !r.stdout) return null;
381
+ const prefix = r.stdout.trim();
382
+ if (!prefix) return null;
383
+ // On Unix, `npm prefix -g` returns e.g. /usr/local; the package is
384
+ // at lib/node_modules/skalpel. On Windows, prefix is the AppData
385
+ // dir itself; package is at node_modules/skalpel.
386
+ const candidate = process.platform === 'win32'
387
+ ? path.join(prefix, 'node_modules', 'skalpel')
388
+ : path.join(prefix, 'lib', 'node_modules', 'skalpel');
389
+ // Avoid false positive when we ARE the global install we just
390
+ // checked (npx cache lives elsewhere, so this is fine; only matters
391
+ // if some non-npx, non-global path resolves to the same dir).
392
+ if (!fs.existsSync(candidate)) return null;
393
+ return { prefix, pkgRoot: candidate };
394
+ }
395
+
396
+ // removeGlobalInstall shells out to `npm uninstall -g skalpel`. Uses
397
+ // shell: true on Windows so the .cmd shim resolves; harmless on
398
+ // Unix. Returns { removed: bool, reason: string }.
399
+ function removeGlobalInstall() {
400
+ const r = spawnSync('npm', ['uninstall', '-g', 'skalpel'], {
401
+ stdio: 'inherit',
402
+ shell: process.platform === 'win32',
403
+ });
404
+ if (r.error) return { removed: false, reason: r.error.message };
405
+ if (r.status !== 0) return { removed: false, reason: `npm exited ${r.status}` };
406
+ return { removed: true };
407
+ }
408
+
306
409
  if (require.main === module) {
307
410
  const argv = process.argv.slice(2);
308
411
  if (argv[0] === 'uninstall') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "3.0.8",
3
+ "version": "3.0.10",
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",
@@ -29,7 +29,6 @@
29
29
  },
30
30
  "scripts": {
31
31
  "postinstall": "node postinstall/index.js",
32
- "preuninstall": "node postinstall/index.js --uninstall",
33
32
  "test": "echo 'no top-level tests; run make test or npm run test:rc-edit' && exit 0",
34
33
  "test:rc-edit": "node postinstall/lib/rc-edit.test.js"
35
34
  },
@@ -54,10 +53,10 @@
54
53
  "x64"
55
54
  ],
56
55
  "optionalDependencies": {
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"
56
+ "@skalpelai/skalpel-darwin-arm64": "3.0.10",
57
+ "@skalpelai/skalpel-darwin-x64": "3.0.10",
58
+ "@skalpelai/skalpel-linux-arm64": "3.0.10",
59
+ "@skalpelai/skalpel-linux-x64": "3.0.10",
60
+ "@skalpelai/skalpel-win32-x64": "3.0.10"
62
61
  }
63
62
  }
@@ -226,12 +226,35 @@ function main(argv) {
226
226
  return 0;
227
227
  }
228
228
 
229
- // B32: refuse to run under `npx skalpel`.
230
- if (paths.isNpxInvocation()) {
231
- process.stderr.write(
232
- 'Skalpel does not support `npx skalpel` please run `npm i -g @skalpelai/skalpel` instead.\n'
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).'
233
252
  );
234
- return 1;
253
+ log.info(
254
+ 'For a persistent install with daemon-on-boot and shell env vars, ' +
255
+ 'run: `npm install -g skalpel`'
256
+ );
257
+ return 0;
235
258
  }
236
259
 
237
260
  const total = 5;