skalpel 3.4.19 → 4.0.0
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/README.md +29 -37
- package/auth.mjs +179 -0
- package/{prosumer-hooks/bootstrap.mjs → bootstrap.mjs} +14 -8
- package/{prosumer-hooks/incremental-ingest.mjs → incremental-ingest.mjs} +5 -10
- package/{prosumer-hooks/install.mjs → install.mjs} +6 -9
- package/login.mjs +164 -0
- package/package.json +17 -57
- package/postinstall.mjs +68 -0
- package/skalpel-setup.mjs +896 -0
- package/{prosumer-hooks/skalpel-statusline.mjs → skalpel-statusline.mjs} +1 -2
- package/INSTALL.md +0 -250
- package/LICENSE +0 -201
- package/design-tokens.json +0 -52
- package/npm-bin/colors.js +0 -125
- package/npm-bin/skalpel.bug-0039.test.js +0 -232
- package/npm-bin/skalpel.js +0 -517
- package/npm-bin/skalpeld.js +0 -20
- package/postinstall/index.js +0 -277
- package/postinstall/index.test.js +0 -147
- package/postinstall/launchd/com.skalpel.skalpeld.plist.tmpl +0 -46
- package/postinstall/lib/ca-install.js +0 -268
- package/postinstall/lib/ca-install.test.js +0 -327
- package/postinstall/lib/detect-prior.js +0 -62
- package/postinstall/lib/env-inject.js +0 -105
- package/postinstall/lib/integrations.js +0 -207
- package/postinstall/lib/launch.js +0 -38
- package/postinstall/lib/log.js +0 -31
- package/postinstall/lib/paths.js +0 -236
- package/postinstall/lib/prosumer-hooks.js +0 -146
- package/postinstall/lib/prosumer-hooks.test.js +0 -278
- package/postinstall/lib/rc-edit.js +0 -168
- package/postinstall/lib/rc-edit.test.js +0 -243
- package/postinstall/lib/run-tests.js +0 -59
- package/postinstall/lib/service-register.js +0 -248
- package/postinstall/lib/sign-in.js +0 -80
- package/postinstall/lib/template.js +0 -36
- package/postinstall/preinstall.js +0 -111
- package/postinstall/preinstall.test.js +0 -47
- package/postinstall/snippets/bash.sh.tmpl +0 -12
- package/postinstall/snippets/fish.fish.tmpl +0 -11
- package/postinstall/snippets/powershell.ps1.tmpl +0 -12
- package/postinstall/snippets/zsh.sh.tmpl +0 -13
- package/postinstall/systemd/skalpeld.service.tmpl +0 -34
- package/postinstall/uninstall.js +0 -98
- package/postinstall/windows/Task.xml.tmpl +0 -42
- package/postinstall/windows/register-task.ps1.tmpl +0 -45
- package/prosumer-hooks/auth.mjs +0 -93
- package/prosumer-hooks/bootstrap.test.mjs +0 -93
- package/prosumer-hooks/incremental-ingest.test.mjs +0 -320
- /package/{prosumer-hooks/insights.mjs → insights.mjs} +0 -0
- /package/{prosumer-hooks/metrics.mjs → metrics.mjs} +0 -0
- /package/{prosumer-hooks/skalpel-hook-session-end.mjs → skalpel-hook-session-end.mjs} +0 -0
- /package/{prosumer-hooks/skalpel-hook-session.mjs → skalpel-hook-session.mjs} +0 -0
- /package/{prosumer-hooks/skalpel-hook.mjs → skalpel-hook.mjs} +0 -0
- /package/{prosumer-hooks/stats.mjs → stats.mjs} +0 -0
package/postinstall/index.js
DELETED
|
@@ -1,277 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Postinstall wizard.
|
|
3
|
-
//
|
|
4
|
-
// 5-step flow per SPEC.md §9.6:
|
|
5
|
-
// 1. detect-prior — probe configDir for prior install
|
|
6
|
-
// 2. sign-in — `skalpel login` (or hint to run it)
|
|
7
|
-
// 3. service-register — render + load launchd / systemd / Task
|
|
8
|
-
// 4. env-inject — managed-block edit of shell rc files
|
|
9
|
-
// 5. launch — print next-step hint
|
|
10
|
-
//
|
|
11
|
-
// Dry-run: SKALPEL_INSTALL_DRY_RUN=1 (env) or --dry-run (argv) makes
|
|
12
|
-
// the wizard log every action it would take and write nothing.
|
|
13
|
-
//
|
|
14
|
-
// Idempotence: the wizard is safe to re-run. Each step replaces or
|
|
15
|
-
// skips rather than duplicating.
|
|
16
|
-
|
|
17
|
-
'use strict';
|
|
18
|
-
|
|
19
|
-
const fs = require('fs');
|
|
20
|
-
const path = require('path');
|
|
21
|
-
|
|
22
|
-
const colors = require('../npm-bin/colors');
|
|
23
|
-
const log = require('./lib/log');
|
|
24
|
-
const paths = require('./lib/paths');
|
|
25
|
-
const detectPrior = require('./lib/detect-prior');
|
|
26
|
-
const signIn = require('./lib/sign-in');
|
|
27
|
-
const serviceRegister = require('./lib/service-register');
|
|
28
|
-
const envInject = require('./lib/env-inject');
|
|
29
|
-
const launch = require('./lib/launch');
|
|
30
|
-
const caInstall = require('./lib/ca-install');
|
|
31
|
-
const prosumerHooks = require('./lib/prosumer-hooks');
|
|
32
|
-
|
|
33
|
-
function printStyledSuccess() {
|
|
34
|
-
if (!process.stdout.isTTY) return;
|
|
35
|
-
const colored = !process.env.NO_COLOR;
|
|
36
|
-
const platformLabel = `${process.platform}-${process.arch}`;
|
|
37
|
-
const check = colored ? colors.theme.mint('✓') : '✓';
|
|
38
|
-
const text = colored
|
|
39
|
-
? colors.theme.text(` skalpel installed for ${platformLabel}`)
|
|
40
|
-
: ` skalpel installed for ${platformLabel}`;
|
|
41
|
-
process.stdout.write(`${check}${text}\n`);
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function parseArgs(argv) {
|
|
45
|
-
const out = {
|
|
46
|
-
dryRun: false,
|
|
47
|
-
skipBundle: false,
|
|
48
|
-
verbose: false,
|
|
49
|
-
};
|
|
50
|
-
for (const a of argv.slice(2)) {
|
|
51
|
-
switch (a) {
|
|
52
|
-
case '--dry-run':
|
|
53
|
-
out.dryRun = true;
|
|
54
|
-
break;
|
|
55
|
-
case '--verbose':
|
|
56
|
-
out.verbose = true;
|
|
57
|
-
break;
|
|
58
|
-
case '--skip-bundle':
|
|
59
|
-
out.skipBundle = true;
|
|
60
|
-
break;
|
|
61
|
-
case '--help':
|
|
62
|
-
case '-h':
|
|
63
|
-
out.help = true;
|
|
64
|
-
break;
|
|
65
|
-
default:
|
|
66
|
-
// Unknown args are ignored for npm-postinstall robustness;
|
|
67
|
-
// the user's npm CLI may pass extra flags.
|
|
68
|
-
break;
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
if (process.env.SKALPEL_INSTALL_DRY_RUN === '1') {
|
|
72
|
-
out.dryRun = true;
|
|
73
|
-
}
|
|
74
|
-
return out;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function helpText() {
|
|
78
|
-
return [
|
|
79
|
-
'skalpel postinstall wizard',
|
|
80
|
-
'',
|
|
81
|
-
'usage: node postinstall/index.js [--dry-run] [--verbose]',
|
|
82
|
-
'',
|
|
83
|
-
'Run automatically by npm install. The wizard performs:',
|
|
84
|
-
' 1. detect-prior probe configDir for an existing install',
|
|
85
|
-
' 2. behavior-hooks stage the local graph hook runtime',
|
|
86
|
-
' 3. sign-in start `skalpel login` (or hint at it)',
|
|
87
|
-
' 4. service-register register the per-OS daemon entry',
|
|
88
|
-
' 5. ca-install install the local interception CA',
|
|
89
|
-
' 6. shell-activation defer wrapping until `skalpel setup`',
|
|
90
|
-
' 7. launch print the run-skalpel hint',
|
|
91
|
-
'',
|
|
92
|
-
'Uninstall is owned by the Go binary now — run `skalpel uninstall`',
|
|
93
|
-
'or `npx skalpel uninstall` to reverse this wizard.',
|
|
94
|
-
'',
|
|
95
|
-
'Dry-run mode (SKALPEL_INSTALL_DRY_RUN=1 or --dry-run) makes every',
|
|
96
|
-
'step log what it would do and write nothing.',
|
|
97
|
-
].join('\n');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function runCAInstallStep(opts) {
|
|
101
|
-
// Resolve CA path by spawning the just-installed `skalpel ca-path`.
|
|
102
|
-
// (The Go binary is on PATH by the time postinstall runs; see
|
|
103
|
-
// npm-bin/skalpel.js for the resolver.)
|
|
104
|
-
const cp = require('child_process');
|
|
105
|
-
let caPath = '';
|
|
106
|
-
try {
|
|
107
|
-
const out = cp.spawnSync('skalpel', ['ca-path'], {
|
|
108
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
109
|
-
timeout: 5_000,
|
|
110
|
-
});
|
|
111
|
-
if (out.status === 0) {
|
|
112
|
-
caPath = String(out.stdout || '').trim();
|
|
113
|
-
}
|
|
114
|
-
} catch (_) {
|
|
115
|
-
// ignored — fall through to deferred-no-ca branch
|
|
116
|
-
}
|
|
117
|
-
if (!caPath) {
|
|
118
|
-
log.warn('ca-install: could not resolve `skalpel ca-path`; deferring trust-store install to first login');
|
|
119
|
-
return { ok: false, reason: 'no-binary' };
|
|
120
|
-
}
|
|
121
|
-
if (opts.dryRun) {
|
|
122
|
-
log.info(`ca-install: dry-run; would install ${caPath} into OS trust store`);
|
|
123
|
-
return { ok: true, skipped: true, reason: 'dry-run' };
|
|
124
|
-
}
|
|
125
|
-
return caInstall.installDaemonCA(caPath);
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function main(argv) {
|
|
129
|
-
const opts = parseArgs(argv);
|
|
130
|
-
if (opts.help) {
|
|
131
|
-
process.stdout.write(`${helpText()}\n`);
|
|
132
|
-
return 0;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
// npx mode: skip the install wizard. The wizard's service-register
|
|
136
|
-
// step would bake the npx cache path (~/.npm/_npx/<hash>/...) into
|
|
137
|
-
// the OS service unit, and that path is evicted by npx's cache GC.
|
|
138
|
-
// Exit 0 instead of 1 so npm install completes and the binary
|
|
139
|
-
// becomes invokable. (Uninstall is owned by the Go binary now and
|
|
140
|
-
// is unaffected.)
|
|
141
|
-
if (paths.isNpxInvocation()) {
|
|
142
|
-
log.info(
|
|
143
|
-
'npx mode detected — skipping install wizard (service-register / ' +
|
|
144
|
-
'env-inject would bake transient npx cache paths into your system).'
|
|
145
|
-
);
|
|
146
|
-
log.info(
|
|
147
|
-
'For a persistent install with daemon-on-boot and shell env vars, ' +
|
|
148
|
-
'run: `npm install -g skalpel`'
|
|
149
|
-
);
|
|
150
|
-
return 0;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
const total = 7;
|
|
154
|
-
const mode = opts.dryRun ? 'dry-run' : 'live';
|
|
155
|
-
log.info(`postinstall wizard starting (${mode} install) on ${process.platform}`);
|
|
156
|
-
|
|
157
|
-
// B28: classify failures. Critical = re-throw and exit 0 (we do
|
|
158
|
-
// not want npm install to abort, but the operator should know).
|
|
159
|
-
// Warning = collect and log at end.
|
|
160
|
-
const allWarnings = [];
|
|
161
|
-
|
|
162
|
-
let prior = false;
|
|
163
|
-
let critical = null;
|
|
164
|
-
try {
|
|
165
|
-
log.step(1, total, 'detect-prior', 'probing configDir');
|
|
166
|
-
const r = detectPrior.run({ dryRun: opts.dryRun });
|
|
167
|
-
prior = r.prior;
|
|
168
|
-
|
|
169
|
-
log.step(2, total, 'behavior-hooks', 'staging CLI-owned runtime');
|
|
170
|
-
try {
|
|
171
|
-
const hooks = prosumerHooks.stageAndHeal({ dryRun: opts.dryRun });
|
|
172
|
-
if (hooks.healed) {
|
|
173
|
-
log.info('behavior-hooks: refreshed an existing skalpel hook installation');
|
|
174
|
-
} else if (hooks.statuslineManaged) {
|
|
175
|
-
log.info(
|
|
176
|
-
'behavior-hooks: runtime staged; Claude status line enabled at 0.0h; ' +
|
|
177
|
-
'prompt hooks activate automatically after `skalpel login`'
|
|
178
|
-
);
|
|
179
|
-
} else {
|
|
180
|
-
log.info('behavior-hooks: runtime staged; hooks activate automatically after `skalpel login`');
|
|
181
|
-
}
|
|
182
|
-
} catch (err) {
|
|
183
|
-
log.warn(`behavior-hooks: ${err.message}; continuing`);
|
|
184
|
-
allWarnings.push(`behavior-hooks: ${err.message}`);
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
log.step(3, total, 'sign-in', prior ? 'skipping (prior auth)' : 'browser flow');
|
|
188
|
-
// sign-in is non-critical: a missing dispatcher / login failure
|
|
189
|
-
// is recoverable — user can run `skalpel login` later.
|
|
190
|
-
try {
|
|
191
|
-
const sr = signIn.run({ dryRun: opts.dryRun, prior });
|
|
192
|
-
if (sr && sr.error) {
|
|
193
|
-
allWarnings.push(`sign-in: ${sr.error}`);
|
|
194
|
-
}
|
|
195
|
-
} catch (err) {
|
|
196
|
-
log.warn(`sign-in: ${err.message}; continuing`);
|
|
197
|
-
allWarnings.push(`sign-in: ${err.message}`);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
log.step(4, total, 'service-register', `OS=${process.platform}`);
|
|
201
|
-
// service-register is critical: a stale service unit is a real
|
|
202
|
-
// problem at boot. Errors propagate to the catch block below.
|
|
203
|
-
const sr = serviceRegister.run({ dryRun: opts.dryRun }) || {};
|
|
204
|
-
if (Array.isArray(sr.warnings) && sr.warnings.length) {
|
|
205
|
-
allWarnings.push(...sr.warnings);
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
log.step(5, total, 'ca-install', `trust store on ${process.platform}`);
|
|
209
|
-
// ca-install is NON-CRITICAL: declined prompts or missing sudo must NOT
|
|
210
|
-
// fail postinstall. The deferred-install sentinel ensures first
|
|
211
|
-
// `skalpel login` / `codex` invocation retries the trust install.
|
|
212
|
-
try {
|
|
213
|
-
const caRes = await runCAInstallStep(opts);
|
|
214
|
-
if (caRes && !caRes.ok && caRes.reason) {
|
|
215
|
-
allWarnings.push(`ca-install: ${caRes.reason}`);
|
|
216
|
-
}
|
|
217
|
-
} catch (err) {
|
|
218
|
-
log.warn(`ca-install: ${err.message}; continuing`);
|
|
219
|
-
allWarnings.push(`ca-install: ${err.message}`);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Step 5 — shell activation is OPT-IN (incident 2026-07-05). A global
|
|
223
|
-
// `npm install -g skalpel` must NOT modify the user's shell rc files. That
|
|
224
|
-
// was the blast radius of the incident: the managed block exported
|
|
225
|
-
// ANTHROPIC_BASE_URL into every shell and broke Claude Code, and even a
|
|
226
|
-
// poison-free wrapper is too invasive to write into a user's shell without
|
|
227
|
-
// consent for an agent harness. The daemon registered in step 3 sits IDLE
|
|
228
|
-
// and intercepts nothing until the user explicitly opts in with
|
|
229
|
-
// `skalpel setup`, which wraps the detected agents (setupCore -> the same
|
|
230
|
-
// managed block, now poison-free). Set SKALPEL_ACTIVATE_SHELL=1 to restore
|
|
231
|
-
// the old auto-wrap-on-install behavior.
|
|
232
|
-
if (process.env.SKALPEL_ACTIVATE_SHELL === '1') {
|
|
233
|
-
log.step(6, total, 'env-inject', 'managed-block edit per shell (SKALPEL_ACTIVATE_SHELL=1)');
|
|
234
|
-
envInject.run({ dryRun: opts.dryRun });
|
|
235
|
-
} else {
|
|
236
|
-
log.step(6, total, 'shell-activation', 'deferred — opt-in via `skalpel setup`');
|
|
237
|
-
log.info('Shell integration is opt-in. Run `skalpel setup` to route Claude/Codex through skalpel.');
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
log.step(7, total, 'launch', 'next-step hint');
|
|
241
|
-
launch.run({ dryRun: opts.dryRun });
|
|
242
|
-
} catch (err) {
|
|
243
|
-
critical = err;
|
|
244
|
-
log.error(`postinstall failed: ${err.message}`);
|
|
245
|
-
if (opts.verbose) {
|
|
246
|
-
process.stderr.write(`${err.stack}\n`);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
if (allWarnings.length) {
|
|
251
|
-
log.warn(`postinstall wizard finished (${mode}) with ${allWarnings.length} warning(s):`);
|
|
252
|
-
for (const w of allWarnings) {
|
|
253
|
-
log.warn(` - ${w}`);
|
|
254
|
-
}
|
|
255
|
-
} else if (!critical) {
|
|
256
|
-
log.info(`postinstall wizard finished (${mode})`);
|
|
257
|
-
if (!opts.dryRun) {
|
|
258
|
-
printStyledSuccess();
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
// Always exit 0 so npm install does not abort. The user can
|
|
262
|
-
// re-run `skalpel install` if a critical step failed.
|
|
263
|
-
return 0;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
if (require.main === module) {
|
|
267
|
-
Promise.resolve(main(process.argv)).then(
|
|
268
|
-
(code) => process.exit(code || 0),
|
|
269
|
-
(err) => {
|
|
270
|
-
log.error(`postinstall crashed: ${err && err.stack ? err.stack : err}`);
|
|
271
|
-
// Wizard contract: exit 0 even on crash so npm install never aborts.
|
|
272
|
-
process.exit(0);
|
|
273
|
-
}
|
|
274
|
-
);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
module.exports = { main, parseArgs };
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// Postinstall wizard regression — exercises the entry-point shipped
|
|
3
|
-
// to every `npm install skalpel` user. Tests run under Node's built-in
|
|
4
|
-
// `node --test` runner (stdlib only; matches the convention set by
|
|
5
|
-
// postinstall/lib/rc-edit.test.js).
|
|
6
|
-
//
|
|
7
|
-
// Scope (closes GAP-5):
|
|
8
|
-
// - parseArgs maps flags, env-var dry-run, and help correctly
|
|
9
|
-
// - dry-run install completes with exit 0 and writes nothing
|
|
10
|
-
// - re-running dry-run install is idempotent (no observable drift)
|
|
11
|
-
// - --help returns 0 and prints usage
|
|
12
|
-
//
|
|
13
|
-
// We exercise the wizard as a child process so the existing entry-
|
|
14
|
-
// point IIFE in index.js does not need to be refactored; tests stay
|
|
15
|
-
// hermetic by setting SKALPEL_INSTALL_DRY_RUN=1 and forcing all
|
|
16
|
-
// service / rc paths to writable t.TempDir() so a regression cannot
|
|
17
|
-
// touch a developer's real shell rc.
|
|
18
|
-
|
|
19
|
-
'use strict';
|
|
20
|
-
|
|
21
|
-
const { test } = require('node:test');
|
|
22
|
-
const assert = require('node:assert/strict');
|
|
23
|
-
const { spawnSync } = require('node:child_process');
|
|
24
|
-
const fs = require('node:fs');
|
|
25
|
-
const os = require('node:os');
|
|
26
|
-
const path = require('node:path');
|
|
27
|
-
|
|
28
|
-
const indexPath = path.join(__dirname, 'index.js');
|
|
29
|
-
|
|
30
|
-
function mkTempEnv() {
|
|
31
|
-
const home = fs.mkdtempSync(path.join(os.tmpdir(), 'skalpel-postinstall-test-'));
|
|
32
|
-
const xdgConfig = path.join(home, '.config');
|
|
33
|
-
fs.mkdirSync(xdgConfig, { recursive: true });
|
|
34
|
-
return {
|
|
35
|
-
HOME: home,
|
|
36
|
-
USERPROFILE: home,
|
|
37
|
-
XDG_CONFIG_HOME: xdgConfig,
|
|
38
|
-
SKALPEL_INSTALL_DRY_RUN: '1',
|
|
39
|
-
SKALPEL_BIN_DIR: path.join(home, 'bin'),
|
|
40
|
-
NO_COLOR: '1',
|
|
41
|
-
PATH: process.env.PATH || '',
|
|
42
|
-
// Force npx detection off so the wizard reaches its full step
|
|
43
|
-
// sequence rather than short-circuiting.
|
|
44
|
-
npm_config_user_agent: 'npm/test',
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function runWizard(extraArgs = []) {
|
|
49
|
-
const env = mkTempEnv();
|
|
50
|
-
return spawnSync(process.execPath, [indexPath, ...extraArgs], {
|
|
51
|
-
env,
|
|
52
|
-
encoding: 'utf8',
|
|
53
|
-
timeout: 15_000,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
test('parseArgs maps short and long flags', () => {
|
|
58
|
-
// require()-time evaluation must not run the IIFE: index.js gates
|
|
59
|
-
// it with `if (require.main === module)`, so require() is safe.
|
|
60
|
-
const mod = require('./index');
|
|
61
|
-
assert.equal(typeof mod.parseArgs, 'function', 'parseArgs export missing');
|
|
62
|
-
const dry = mod.parseArgs(['node', 'index.js', '--dry-run']);
|
|
63
|
-
assert.equal(dry.dryRun, true);
|
|
64
|
-
const help = mod.parseArgs(['node', 'index.js', '-h']);
|
|
65
|
-
assert.equal(help.help, true);
|
|
66
|
-
const verbose = mod.parseArgs(['node', 'index.js', '--verbose']);
|
|
67
|
-
assert.equal(verbose.verbose, true);
|
|
68
|
-
const skipBundle = mod.parseArgs(['node', 'index.js', '--skip-bundle']);
|
|
69
|
-
assert.equal(skipBundle.skipBundle, true);
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
test('SKALPEL_INSTALL_DRY_RUN=1 forces dry-run even without --dry-run', () => {
|
|
73
|
-
const prev = process.env.SKALPEL_INSTALL_DRY_RUN;
|
|
74
|
-
process.env.SKALPEL_INSTALL_DRY_RUN = '1';
|
|
75
|
-
try {
|
|
76
|
-
const mod = require('./index');
|
|
77
|
-
const opts = mod.parseArgs(['node', 'index.js']);
|
|
78
|
-
assert.equal(opts.dryRun, true);
|
|
79
|
-
} finally {
|
|
80
|
-
if (prev === undefined) delete process.env.SKALPEL_INSTALL_DRY_RUN;
|
|
81
|
-
else process.env.SKALPEL_INSTALL_DRY_RUN = prev;
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
test('--help exits 0 and prints usage', () => {
|
|
86
|
-
const r = runWizard(['--help']);
|
|
87
|
-
assert.equal(r.status, 0, `stderr: ${r.stderr}`);
|
|
88
|
-
assert.match(r.stdout, /postinstall wizard/i);
|
|
89
|
-
assert.match(r.stdout, /usage:/i);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
test('dry-run install completes with exit 0', () => {
|
|
93
|
-
const r = runWizard(['--dry-run']);
|
|
94
|
-
// Wizard contract: even on critical step failure, return 0 so npm
|
|
95
|
-
// install never aborts. Tests assert the *invariant*, not the path.
|
|
96
|
-
assert.equal(r.status, 0, `non-zero exit: stderr=${r.stderr}\nstdout=${r.stdout}`);
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
test('dry-run install is idempotent: second run mirrors first', () => {
|
|
100
|
-
// Same temp HOME both runs; the wizard should not leave behind
|
|
101
|
-
// state that perturbs the second run. We compare exit codes; output
|
|
102
|
-
// diffs are tolerated because timestamps / random suffixes vary.
|
|
103
|
-
const env = mkTempEnv();
|
|
104
|
-
function once() {
|
|
105
|
-
return spawnSync(process.execPath, [indexPath, '--dry-run'], {
|
|
106
|
-
env,
|
|
107
|
-
encoding: 'utf8',
|
|
108
|
-
timeout: 15_000,
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
const r1 = once();
|
|
112
|
-
const r2 = once();
|
|
113
|
-
assert.equal(r1.status, 0, `first run: stderr=${r1.stderr}`);
|
|
114
|
-
assert.equal(r2.status, 0, `second run: stderr=${r2.stderr}`);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
test('dry-run install does not write to HOME', () => {
|
|
118
|
-
const env = mkTempEnv();
|
|
119
|
-
const home = env.HOME;
|
|
120
|
-
// Capture initial file inventory.
|
|
121
|
-
function snapshot(dir) {
|
|
122
|
-
const acc = [];
|
|
123
|
-
(function walk(p) {
|
|
124
|
-
let entries;
|
|
125
|
-
try { entries = fs.readdirSync(p, { withFileTypes: true }); }
|
|
126
|
-
catch { return; }
|
|
127
|
-
for (const e of entries) {
|
|
128
|
-
const full = path.join(p, e.name);
|
|
129
|
-
acc.push(full);
|
|
130
|
-
if (e.isDirectory()) walk(full);
|
|
131
|
-
}
|
|
132
|
-
})(dir);
|
|
133
|
-
return acc.sort();
|
|
134
|
-
}
|
|
135
|
-
const before = snapshot(home);
|
|
136
|
-
spawnSync(process.execPath, [indexPath, '--dry-run'], {
|
|
137
|
-
env,
|
|
138
|
-
encoding: 'utf8',
|
|
139
|
-
timeout: 15_000,
|
|
140
|
-
});
|
|
141
|
-
const after = snapshot(home);
|
|
142
|
-
// Dry-run must not create *new* files in HOME beyond what mkTempEnv
|
|
143
|
-
// pre-seeded (.config). Any drift is a regression on the dry-run
|
|
144
|
-
// contract.
|
|
145
|
-
const added = after.filter((p) => !before.includes(p));
|
|
146
|
-
assert.deepEqual(added, [], `dry-run wrote files: ${added.join(', ')}`);
|
|
147
|
-
});
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
|
|
3
|
-
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
4
|
-
<plist version="1.0">
|
|
5
|
-
<dict>
|
|
6
|
-
<key>Label</key> <string>ai.skalpel.daemon</string>
|
|
7
|
-
|
|
8
|
-
<key>ProgramArguments</key>
|
|
9
|
-
<array>
|
|
10
|
-
<string>{{BIN}}/skalpeld</string>
|
|
11
|
-
<string>--service-mode</string>
|
|
12
|
-
</array>
|
|
13
|
-
|
|
14
|
-
<key>RunAtLoad</key> <true/>
|
|
15
|
-
<!-- Unconditional KeepAlive: launchd restarts the daemon after any
|
|
16
|
-
exit (clean or otherwise), respecting ThrottleInterval. The
|
|
17
|
-
previous conditional form (SuccessfulExit=false + Crashed=true)
|
|
18
|
-
did NOT restart after a graceful SIGTERM drain that exits with
|
|
19
|
-
status 0 — the daemon stayed dead until some unrelated XPC nudge
|
|
20
|
-
eventually woke launchd up (observed gaps of 1-25 min on the
|
|
21
|
-
test machine), breaking Claude Code sessions routed through the
|
|
22
|
-
proxy for that whole window. Uninstall continues to work via
|
|
23
|
-
`launchctl unload`, which bypasses KeepAlive entirely. -->
|
|
24
|
-
<key>KeepAlive</key> <true/>
|
|
25
|
-
|
|
26
|
-
<key>ThrottleInterval</key> <integer>10</integer>
|
|
27
|
-
|
|
28
|
-
<key>StandardOutPath</key> <string>{{CONFIG_DIR}}/logs/skalpeld.log</string>
|
|
29
|
-
<key>StandardErrorPath</key> <string>{{CONFIG_DIR}}/logs/skalpeld.log</string>
|
|
30
|
-
|
|
31
|
-
<key>WorkingDirectory</key> <string>{{HOME}}</string>
|
|
32
|
-
|
|
33
|
-
<key>EnvironmentVariables</key>
|
|
34
|
-
<dict>
|
|
35
|
-
<key>SKALPEL_CONFIG_DIR</key> <string>{{CONFIG_DIR}}</string>
|
|
36
|
-
<key>HOME</key> <string>{{HOME}}</string>
|
|
37
|
-
<key>USER</key> <string>{{USER}}</string>
|
|
38
|
-
</dict>
|
|
39
|
-
|
|
40
|
-
<key>ProcessType</key> <string>Interactive</string>
|
|
41
|
-
<!-- audit-2026-05-09 §6 LOW: dropped LimitLoadToSessionType=Aqua so
|
|
42
|
-
the daemon also loads under headless macOS sessions (CI runners
|
|
43
|
-
driving via SSH land in LoginWindow / Background sessions; the
|
|
44
|
-
Aqua restriction blocked load there). -->
|
|
45
|
-
</dict>
|
|
46
|
-
</plist>
|