skalpel 2.0.23 → 3.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/INSTALL.md +103 -0
- package/LICENSE +201 -21
- package/README.md +12 -174
- package/design-tokens.json +51 -0
- package/npm-bin/colors.js +125 -0
- package/npm-bin/skalpel.js +200 -0
- package/npm-bin/skalpeld.js +20 -0
- package/package.json +50 -68
- package/postinstall/index.js +294 -0
- package/postinstall/launchd/com.skalpel.skalpeld.plist.tmpl +41 -0
- package/postinstall/lib/detect-prior.js +51 -0
- package/postinstall/lib/env-inject.js +121 -0
- package/postinstall/lib/launch.js +28 -0
- package/postinstall/lib/log.js +31 -0
- package/postinstall/lib/paths.js +186 -0
- package/postinstall/lib/rc-edit.js +167 -0
- package/postinstall/lib/rc-edit.test.js +196 -0
- package/postinstall/lib/service-register.js +293 -0
- package/postinstall/lib/sign-in.js +98 -0
- package/postinstall/lib/template.js +36 -0
- package/postinstall/snippets/bash.sh.tmpl +12 -0
- package/postinstall/snippets/fish.fish.tmpl +11 -0
- package/postinstall/snippets/powershell.ps1.tmpl +12 -0
- package/postinstall/snippets/zsh.sh.tmpl +13 -0
- package/postinstall/systemd/skalpeld.service.tmpl +33 -0
- package/postinstall/windows/Task.xml.tmpl +42 -0
- package/postinstall/windows/register-task.ps1.tmpl +45 -0
- package/dist/cli/index.js +0 -2899
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/proxy-runner.js +0 -1649
- package/dist/cli/proxy-runner.js.map +0 -1
- package/dist/index.cjs +0 -2333
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.cts +0 -165
- package/dist/index.d.ts +0 -165
- package/dist/index.js +0 -2287
- package/dist/index.js.map +0 -1
- package/dist/proxy/index.cjs +0 -1782
- package/dist/proxy/index.cjs.map +0 -1
- package/dist/proxy/index.d.cts +0 -39
- package/dist/proxy/index.d.ts +0 -39
- package/dist/proxy/index.js +0 -1748
- package/dist/proxy/index.js.map +0 -1
|
@@ -0,0 +1,294 @@
|
|
|
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
|
+
|
|
31
|
+
function printStyledSuccess() {
|
|
32
|
+
if (!process.stdout.isTTY) return;
|
|
33
|
+
const colored = !process.env.NO_COLOR;
|
|
34
|
+
const platformLabel = `${process.platform}-${process.arch}`;
|
|
35
|
+
const check = colored ? colors.theme.green('✓') : '✓';
|
|
36
|
+
const text = colored
|
|
37
|
+
? colors.theme.text(` skalpel installed for ${platformLabel}`)
|
|
38
|
+
: ` skalpel installed for ${platformLabel}`;
|
|
39
|
+
process.stdout.write(`${check}${text}\n`);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function parseArgs(argv) {
|
|
43
|
+
const out = {
|
|
44
|
+
dryRun: false,
|
|
45
|
+
skipBundle: false,
|
|
46
|
+
verbose: false,
|
|
47
|
+
uninstall: false,
|
|
48
|
+
cleanupData: 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 '--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
|
+
case '--help':
|
|
70
|
+
case '-h':
|
|
71
|
+
out.help = true;
|
|
72
|
+
break;
|
|
73
|
+
default:
|
|
74
|
+
// Unknown args are ignored for npm-postinstall robustness;
|
|
75
|
+
// the user's npm CLI may pass extra flags.
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
if (process.env.SKALPEL_INSTALL_DRY_RUN === '1') {
|
|
80
|
+
out.dryRun = true;
|
|
81
|
+
}
|
|
82
|
+
if (process.env.SKALPEL_UNINSTALL_CLEAN === '1') {
|
|
83
|
+
out.cleanupData = true;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function helpText() {
|
|
89
|
+
return [
|
|
90
|
+
'skalpel postinstall wizard',
|
|
91
|
+
'',
|
|
92
|
+
'usage: node postinstall/index.js [--dry-run] [--verbose] [--uninstall] [--cleanup-data]',
|
|
93
|
+
'',
|
|
94
|
+
'Run automatically by npm install. The wizard performs:',
|
|
95
|
+
' 1. detect-prior probe configDir for an existing install',
|
|
96
|
+
' 2. sign-in start `skalpel login` (or hint at it)',
|
|
97
|
+
' 3. service-register register the per-OS daemon entry',
|
|
98
|
+
' 4. env-inject update shell rc managed-block',
|
|
99
|
+
' 5. launch print the run-skalpel hint',
|
|
100
|
+
'',
|
|
101
|
+
'--uninstall reverses steps 3 and 4: unregisters the daemon and',
|
|
102
|
+
' removes the managed block from each rc file.',
|
|
103
|
+
'--cleanup-data (with --uninstall) also deletes auth.json,',
|
|
104
|
+
' config.toml, skalpeld.lock, and logs/. Equivalent env var:',
|
|
105
|
+
' SKALPEL_UNINSTALL_CLEAN=1.',
|
|
106
|
+
'',
|
|
107
|
+
'Dry-run mode (SKALPEL_INSTALL_DRY_RUN=1 or --dry-run) makes every',
|
|
108
|
+
'step log what it would do and write nothing.',
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// B29: best-effort delete of user data on --uninstall --cleanup-data.
|
|
113
|
+
// Skipped silently when files / dirs are absent.
|
|
114
|
+
function cleanupUserData({ dryRun }) {
|
|
115
|
+
const removed = [];
|
|
116
|
+
const targets = [
|
|
117
|
+
paths.authFile(),
|
|
118
|
+
paths.configToml(),
|
|
119
|
+
paths.lockFile(),
|
|
120
|
+
];
|
|
121
|
+
for (const t of targets) {
|
|
122
|
+
if (dryRun) {
|
|
123
|
+
log.dryRun(`uninstall-cleanup: would rm ${t}`);
|
|
124
|
+
removed.push(t);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
try {
|
|
128
|
+
if (fs.existsSync(t)) {
|
|
129
|
+
fs.rmSync(t, { force: true });
|
|
130
|
+
log.info(`uninstall-cleanup: removed ${t}`);
|
|
131
|
+
removed.push(t);
|
|
132
|
+
}
|
|
133
|
+
} catch (err) {
|
|
134
|
+
log.warn(`uninstall-cleanup: rm ${t} failed: ${err.message}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const lDir = paths.logsDir();
|
|
138
|
+
if (dryRun) {
|
|
139
|
+
log.dryRun(`uninstall-cleanup: would rm -r ${lDir}`);
|
|
140
|
+
removed.push(lDir);
|
|
141
|
+
} else {
|
|
142
|
+
try {
|
|
143
|
+
if (fs.existsSync(lDir)) {
|
|
144
|
+
fs.rmSync(lDir, { recursive: true, force: true });
|
|
145
|
+
log.info(`uninstall-cleanup: removed ${lDir}`);
|
|
146
|
+
removed.push(lDir);
|
|
147
|
+
}
|
|
148
|
+
} catch (err) {
|
|
149
|
+
log.warn(`uninstall-cleanup: rm -r ${lDir} failed: ${err.message}`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Try to remove the (now-empty) configDir; ignore ENOTEMPTY which
|
|
153
|
+
// means the user has other state under it we don't own.
|
|
154
|
+
const cfg = paths.configDir();
|
|
155
|
+
if (!dryRun) {
|
|
156
|
+
try {
|
|
157
|
+
fs.rmdirSync(cfg);
|
|
158
|
+
log.info(`uninstall-cleanup: removed empty ${cfg}`);
|
|
159
|
+
removed.push(cfg);
|
|
160
|
+
} catch (_) {
|
|
161
|
+
// non-empty or missing — leave alone.
|
|
162
|
+
}
|
|
163
|
+
} else {
|
|
164
|
+
log.dryRun(`uninstall-cleanup: would rmdir (if empty) ${cfg}`);
|
|
165
|
+
}
|
|
166
|
+
return { removed };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function main(argv) {
|
|
170
|
+
const opts = parseArgs(argv);
|
|
171
|
+
if (opts.help) {
|
|
172
|
+
process.stdout.write(`${helpText()}\n`);
|
|
173
|
+
return 0;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// B32: refuse to run under `npx skalpel`.
|
|
177
|
+
if (paths.isNpxInvocation()) {
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
'Skalpel does not support `npx skalpel` — please run `npm i -g @skalpelai/skalpel` instead.\n'
|
|
180
|
+
);
|
|
181
|
+
return 1;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const total = 5;
|
|
185
|
+
const mode = opts.dryRun ? 'dry-run' : 'live';
|
|
186
|
+
const action = opts.uninstall ? 'uninstall' : 'install';
|
|
187
|
+
log.info(`postinstall wizard starting (${mode} ${action}) on ${process.platform}`);
|
|
188
|
+
|
|
189
|
+
// B28: classify failures. Critical = re-throw and exit 0 (we do
|
|
190
|
+
// not want npm install to abort, but the operator should know).
|
|
191
|
+
// Warning = collect and log at end.
|
|
192
|
+
const allWarnings = [];
|
|
193
|
+
|
|
194
|
+
if (opts.uninstall) {
|
|
195
|
+
try {
|
|
196
|
+
const stepCount = opts.cleanupData ? 3 : 2;
|
|
197
|
+
log.step(1, stepCount, 'env-uninject', 'remove managed-block from rc files');
|
|
198
|
+
envInject.uninject({ dryRun: opts.dryRun });
|
|
199
|
+
|
|
200
|
+
log.step(2, stepCount, 'service-unregister', `OS=${process.platform}`);
|
|
201
|
+
const ur = serviceRegister.unregister({ dryRun: opts.dryRun }) || {};
|
|
202
|
+
if (Array.isArray(ur.errors) && ur.errors.length) {
|
|
203
|
+
allWarnings.push(...ur.errors);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// B29: optional user-data cleanup.
|
|
207
|
+
if (opts.cleanupData) {
|
|
208
|
+
log.step(3, stepCount, 'cleanup-user-data', 'delete auth.json/config.toml/lock/logs');
|
|
209
|
+
cleanupUserData({ dryRun: opts.dryRun });
|
|
210
|
+
} else {
|
|
211
|
+
log.info(
|
|
212
|
+
'uninstall: user data preserved (auth.json/config.toml/lock/logs). ' +
|
|
213
|
+
'Pass --cleanup-data or set SKALPEL_UNINSTALL_CLEAN=1 to remove.'
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
} catch (err) {
|
|
217
|
+
log.error(`uninstall failed: ${err.message}`);
|
|
218
|
+
if (opts.verbose) {
|
|
219
|
+
process.stderr.write(`${err.stack}\n`);
|
|
220
|
+
}
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
if (allWarnings.length) {
|
|
224
|
+
log.warn(`postinstall wizard finished (${mode} uninstall) with ${allWarnings.length} warning(s)`);
|
|
225
|
+
} else {
|
|
226
|
+
log.info(`postinstall wizard finished (${mode} uninstall)`);
|
|
227
|
+
}
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
let prior = false;
|
|
232
|
+
let critical = null;
|
|
233
|
+
try {
|
|
234
|
+
log.step(1, total, 'detect-prior', 'probing configDir');
|
|
235
|
+
const r = detectPrior.run({ dryRun: opts.dryRun });
|
|
236
|
+
prior = r.prior;
|
|
237
|
+
|
|
238
|
+
log.step(2, total, 'sign-in', prior ? 'skipping (prior auth)' : 'browser flow');
|
|
239
|
+
// sign-in is non-critical: a missing dispatcher / login failure
|
|
240
|
+
// is recoverable — user can run `skalpel login` later.
|
|
241
|
+
try {
|
|
242
|
+
const sr = signIn.run({ dryRun: opts.dryRun, prior });
|
|
243
|
+
if (sr && sr.error) {
|
|
244
|
+
allWarnings.push(`sign-in: ${sr.error}`);
|
|
245
|
+
}
|
|
246
|
+
} catch (err) {
|
|
247
|
+
log.warn(`sign-in: ${err.message}; continuing`);
|
|
248
|
+
allWarnings.push(`sign-in: ${err.message}`);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
log.step(3, total, 'service-register', `OS=${process.platform}`);
|
|
252
|
+
// service-register is critical: a stale service unit is a real
|
|
253
|
+
// problem at boot. Errors propagate to the catch block below.
|
|
254
|
+
const sr = serviceRegister.run({ dryRun: opts.dryRun }) || {};
|
|
255
|
+
if (Array.isArray(sr.warnings) && sr.warnings.length) {
|
|
256
|
+
allWarnings.push(...sr.warnings);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
log.step(4, total, 'env-inject', 'managed-block edit per shell');
|
|
260
|
+
// env-inject is critical: missing managed block means agents
|
|
261
|
+
// never see the proxy URL. Errors propagate.
|
|
262
|
+
envInject.run({ dryRun: opts.dryRun });
|
|
263
|
+
|
|
264
|
+
log.step(5, total, 'launch', 'next-step hint');
|
|
265
|
+
launch.run({ dryRun: opts.dryRun });
|
|
266
|
+
} catch (err) {
|
|
267
|
+
critical = err;
|
|
268
|
+
log.error(`postinstall failed: ${err.message}`);
|
|
269
|
+
if (opts.verbose) {
|
|
270
|
+
process.stderr.write(`${err.stack}\n`);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
if (allWarnings.length) {
|
|
275
|
+
log.warn(`postinstall wizard finished (${mode}) with ${allWarnings.length} warning(s):`);
|
|
276
|
+
for (const w of allWarnings) {
|
|
277
|
+
log.warn(` - ${w}`);
|
|
278
|
+
}
|
|
279
|
+
} else if (!critical) {
|
|
280
|
+
log.info(`postinstall wizard finished (${mode})`);
|
|
281
|
+
if (!opts.dryRun) {
|
|
282
|
+
printStyledSuccess();
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
// Always exit 0 so npm install does not abort. The user can
|
|
286
|
+
// re-run `skalpel install` if a critical step failed.
|
|
287
|
+
return 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (require.main === module) {
|
|
291
|
+
process.exit(main(process.argv));
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
module.exports = { main, parseArgs, cleanupUserData };
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
<key>KeepAlive</key>
|
|
16
|
+
<dict>
|
|
17
|
+
<key>SuccessfulExit</key> <false/>
|
|
18
|
+
<key>Crashed</key> <true/>
|
|
19
|
+
</dict>
|
|
20
|
+
|
|
21
|
+
<key>ThrottleInterval</key> <integer>10</integer>
|
|
22
|
+
|
|
23
|
+
<key>StandardOutPath</key> <string>{{CONFIG_DIR}}/logs/skalpeld.log</string>
|
|
24
|
+
<key>StandardErrorPath</key> <string>{{CONFIG_DIR}}/logs/skalpeld.log</string>
|
|
25
|
+
|
|
26
|
+
<key>WorkingDirectory</key> <string>{{HOME}}</string>
|
|
27
|
+
|
|
28
|
+
<key>EnvironmentVariables</key>
|
|
29
|
+
<dict>
|
|
30
|
+
<key>SKALPEL_CONFIG_DIR</key> <string>{{CONFIG_DIR}}</string>
|
|
31
|
+
<key>HOME</key> <string>{{HOME}}</string>
|
|
32
|
+
<key>USER</key> <string>{{USER}}</string>
|
|
33
|
+
</dict>
|
|
34
|
+
|
|
35
|
+
<key>ProcessType</key> <string>Interactive</string>
|
|
36
|
+
<!-- audit-2026-05-09 §6 LOW: dropped LimitLoadToSessionType=Aqua so
|
|
37
|
+
the daemon also loads under headless macOS sessions (CI runners
|
|
38
|
+
driving via SSH land in LoginWindow / Background sessions; the
|
|
39
|
+
Aqua restriction blocked load there). -->
|
|
40
|
+
</dict>
|
|
41
|
+
</plist>
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// Step 1: detect prior install.
|
|
2
|
+
//
|
|
3
|
+
// SPEC.md §9.6 — read <configDir>/auth.json existence, config.toml
|
|
4
|
+
// existence, and presence of LaunchAgent / systemd unit / Task. If any
|
|
5
|
+
// of these are present, the wizard short-circuits and the TUI is
|
|
6
|
+
// launched directly.
|
|
7
|
+
//
|
|
8
|
+
// We also check the lock file (which the daemon writes) so an active
|
|
9
|
+
// install with running daemon is detected even if config.toml hasn't
|
|
10
|
+
// been written yet.
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const log = require('./log');
|
|
16
|
+
const paths = require('./paths');
|
|
17
|
+
|
|
18
|
+
function fileExists(p) {
|
|
19
|
+
try {
|
|
20
|
+
fs.accessSync(p, fs.constants.F_OK);
|
|
21
|
+
return true;
|
|
22
|
+
} catch (_e) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function run({ dryRun }) {
|
|
28
|
+
const probes = [
|
|
29
|
+
{ label: 'auth.json', path: paths.authFile() },
|
|
30
|
+
{ label: 'config.toml', path: paths.configToml() },
|
|
31
|
+
{ label: 'lock file', path: paths.lockFile() },
|
|
32
|
+
{ label: 'service registration', path: paths.servicePath() },
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const findings = probes.map((p) => ({ ...p, exists: fileExists(p.path) }));
|
|
36
|
+
const prior = findings.some((f) => f.exists);
|
|
37
|
+
|
|
38
|
+
if (dryRun) {
|
|
39
|
+
log.dryRun(`step 1 detect-prior: would probe ${findings.length} paths`);
|
|
40
|
+
}
|
|
41
|
+
for (const f of findings) {
|
|
42
|
+
log.info(
|
|
43
|
+
` ${f.exists ? 'present' : 'absent '} — ${f.label}: ${f.path}`
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
log.info(prior ? 'detected prior install' : 'no prior install detected');
|
|
48
|
+
return { prior, findings };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { run };
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// Step 4: environment-variable injection into shell rc files.
|
|
2
|
+
//
|
|
3
|
+
// Delegates to lib/rc-edit.js for the idempotent fenced-block edit.
|
|
4
|
+
// The set of files we touch is paths.rcFiles(); each file that
|
|
5
|
+
// already exists gets its managed block written (or replaced).
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const fs = require('fs');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
const log = require('./log');
|
|
13
|
+
const paths = require('./paths');
|
|
14
|
+
const rcEdit = require('./rc-edit');
|
|
15
|
+
|
|
16
|
+
// B21: when none of the per-shell candidates exist, fall back to
|
|
17
|
+
// creating a fresh ~/.profile (POSIX) or ~/.bash_profile (the
|
|
18
|
+
// macOS bash-login default). Without this, a fresh-install user
|
|
19
|
+
// who has no rc files yet ends up with no managed block at all
|
|
20
|
+
// and the daemon's env vars never reach their shells.
|
|
21
|
+
function fallbackRc() {
|
|
22
|
+
if (process.platform === 'win32') {
|
|
23
|
+
// PowerShell candidates are listed even when absent; rc-edit
|
|
24
|
+
// creates the path on first apply. No fallback needed.
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
// We pick ~/.profile because it is sourced by sh / bash-login /
|
|
28
|
+
// ksh / dash and is the only POSIX-portable file. macOS users
|
|
29
|
+
// who exclusively use Terminal.app's bash-login may also want
|
|
30
|
+
// ~/.bash_profile, but that is already in paths.rcFiles() on
|
|
31
|
+
// darwin.
|
|
32
|
+
return {
|
|
33
|
+
shell: 'profile',
|
|
34
|
+
path: path.join(require('os').homedir(), '.profile'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function run({ dryRun, port }) {
|
|
39
|
+
const targetPort = port || 7878;
|
|
40
|
+
const env = rcEdit.envBlockValues(targetPort);
|
|
41
|
+
const rcs = paths.rcFiles();
|
|
42
|
+
const touched = [];
|
|
43
|
+
const present = [];
|
|
44
|
+
|
|
45
|
+
for (const rc of rcs) {
|
|
46
|
+
const exists = fs.existsSync(rc.path);
|
|
47
|
+
if (!exists) {
|
|
48
|
+
log.info(`env-inject: ${rc.shell} rc absent (${rc.path}) — skip`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
present.push(rc);
|
|
52
|
+
if (dryRun) {
|
|
53
|
+
log.dryRun(`step 4 env-inject: would update managed block in ${rc.path} (${rc.shell})`);
|
|
54
|
+
touched.push(rc.path);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
rcEdit.applyBlock({ shell: rc.shell, file: rc.path, env });
|
|
58
|
+
log.info(`env-inject: updated managed block in ${rc.path} (${rc.shell})`);
|
|
59
|
+
touched.push(rc.path);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// B21: no candidates existed — bootstrap a fresh ~/.profile so
|
|
63
|
+
// the env vars are reachable from a freshly-provisioned account.
|
|
64
|
+
if (present.length === 0) {
|
|
65
|
+
const fallback = fallbackRc();
|
|
66
|
+
if (fallback) {
|
|
67
|
+
if (dryRun) {
|
|
68
|
+
log.dryRun(
|
|
69
|
+
`step 4 env-inject: no rc files present — would create ${fallback.path} ` +
|
|
70
|
+
`with the managed block (${fallback.shell})`
|
|
71
|
+
);
|
|
72
|
+
touched.push(fallback.path);
|
|
73
|
+
} else {
|
|
74
|
+
log.info(
|
|
75
|
+
`env-inject: no rc files present — creating ${fallback.path} ` +
|
|
76
|
+
`with the managed block (${fallback.shell})`
|
|
77
|
+
);
|
|
78
|
+
// Write a small header so the user knows we created the file.
|
|
79
|
+
const header = '# Created by skalpel install — feel free to add your own lines below.\n';
|
|
80
|
+
fs.mkdirSync(path.dirname(fallback.path), { recursive: true });
|
|
81
|
+
fs.writeFileSync(fallback.path, header);
|
|
82
|
+
rcEdit.applyBlock({ shell: fallback.shell, file: fallback.path, env });
|
|
83
|
+
touched.push(fallback.path);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (touched.length === 0) {
|
|
89
|
+
log.info('env-inject: no shell rc files present — user can re-run after creating one');
|
|
90
|
+
} else {
|
|
91
|
+
log.info(
|
|
92
|
+
'env-inject: restart your shell or `source` the rc file to pick up the variables'
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
return { touched };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function uninject({ dryRun }) {
|
|
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
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return { removed };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
module.exports = { run, uninject, fallbackRc };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Step 5: launch.
|
|
2
|
+
//
|
|
3
|
+
// SPEC.md §9.6 step 6 — launch the TUI; if no agents are configured
|
|
4
|
+
// yet, the empty-state from spec §3.2 is shown.
|
|
5
|
+
//
|
|
6
|
+
// During npm postinstall we don't actually launch the TUI (it would
|
|
7
|
+
// wedge the install behind a long-running interactive process).
|
|
8
|
+
// Instead we print the one-line "next-step" hint. Real TUI launch
|
|
9
|
+
// happens when the user types `skalpel` themselves.
|
|
10
|
+
//
|
|
11
|
+
// In dry-run we log the same hint marked [dry-run].
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const log = require('./log');
|
|
17
|
+
|
|
18
|
+
function run({ dryRun }) {
|
|
19
|
+
const hint = 'all set — run `skalpel` to open the TUI';
|
|
20
|
+
if (dryRun) {
|
|
21
|
+
log.dryRun(`step 5 launch: would print: ${hint}`);
|
|
22
|
+
return { launched: false, dryRun: true };
|
|
23
|
+
}
|
|
24
|
+
log.info(hint);
|
|
25
|
+
return { launched: false, deferred: true };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = { run };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
// Tiny logger for the postinstall wizard.
|
|
2
|
+
//
|
|
3
|
+
// No third-party deps: stdlib only. The wizard runs as part of
|
|
4
|
+
// `npm install` so its output is interleaved with npm's own; we
|
|
5
|
+
// prefix every line with `skalpel install:` to make it easy to grep.
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const PREFIX = 'skalpel install:';
|
|
10
|
+
|
|
11
|
+
function info(msg) {
|
|
12
|
+
process.stdout.write(`${PREFIX} ${msg}\n`);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function warn(msg) {
|
|
16
|
+
process.stderr.write(`${PREFIX} WARN ${msg}\n`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function error(msg) {
|
|
20
|
+
process.stderr.write(`${PREFIX} ERROR ${msg}\n`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function dryRun(msg) {
|
|
24
|
+
process.stdout.write(`${PREFIX} [dry-run] ${msg}\n`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function step(n, total, name, msg) {
|
|
28
|
+
process.stdout.write(`${PREFIX} (${n}/${total}) ${name}: ${msg}\n`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = { info, warn, error, dryRun, step };
|