skalpel 3.0.9 → 3.0.11

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
@@ -47,7 +47,7 @@ The first-run flow assumes the user has obtained an `sk-skalpel-*` API key on We
47
47
 
48
48
  This flow corresponds to Journey 1 — First launch, fresh authentication — in `SPEC.md`. That narrative describes what the user feels at each step; this document describes what the install machinery actually does.
49
49
 
50
- If the user runs `skalpel` without first obtaining an API key, the TUI exits per spec §8.4 with a stderr pointer at `skalpel login`. The wizard above is the path for users who arrived through the post-signup install hub on Web; users with broken or missing auth state are returned to the shell.
50
+ If the user runs `skalpel` without first obtaining an API key, the TUI exits per spec §8.4 with a stderr pointer at `skalpel login`. The wizard above is the path for users who arrived through the post-signup install hub on Web; users with broken or missing auth state are returned to the shell. To sign out of an existing account on this machine, run `skalpel logout` from a shell — the CLI revokes the backend session (best-effort) and removes `auth.json` via `internal/auth.Delete`.
51
51
 
52
52
  An alternative first-run flow uses `skalpel login` from the shell directly. That command runs a device-code flow against Cognito (per the Auth handoff section of the cross-surface contract) and writes `auth.json` without the user having to paste an API key. The two flows produce equivalent on-disk state; the API-key-paste flow is the one a user lands in when arriving from the Web post-signup install hub, and the device-code flow is the one a user runs when reauthenticating an account that has already been signed in on this machine before.
53
53
 
@@ -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
 
package/README.md CHANGED
@@ -18,6 +18,16 @@ npm install -g skalpel
18
18
 
19
19
  Both `skalpel` and `skalpeld` are placed on your `PATH`. For details on what gets installed where, per-OS service registration, updates, and uninstall, see [INSTALL.md](./INSTALL.md).
20
20
 
21
+ ## Subcommands
22
+
23
+ `skalpel` accepts a small set of subcommands:
24
+
25
+ - `skalpel` — launch the Bubble Tea TUI (default).
26
+ - `skalpel login` — browser-loopback sign-in; writes `auth.json`.
27
+ - `skalpel logout` — revoke the active session (best-effort) and delete `auth.json`; supports `--yes` to skip the `[y/N]` confirm.
28
+ - `skalpel uninstall` — clean up skalpel state on this machine.
29
+ - `skalpel --version` / `skalpel --help` — utilities.
30
+
21
31
  ## Links
22
32
 
23
33
  - Homepage: https://skalpel.ai
@@ -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
  }
@@ -316,11 +322,90 @@ function runUninstall(rest) {
316
322
  );
317
323
  }
318
324
 
319
- process.stdout.write(`${dim('To finish removal, also run:')}\n`);
320
- process.stdout.write(` npm uninstall -g skalpel\n`);
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
+ }
321
367
  return 0;
322
368
  }
323
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
+
324
409
  if (require.main === module) {
325
410
  const argv = process.argv.slice(2);
326
411
  if (argv[0] === 'uninstall') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skalpel",
3
- "version": "3.0.9",
3
+ "version": "3.0.11",
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.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"
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
  }