skalpel 2.0.24 → 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 -2888
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/proxy-runner.js +0 -1598
- package/dist/cli/proxy-runner.js.map +0 -1
- package/dist/index.cjs +0 -2282
- 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 -2236
- package/dist/index.js.map +0 -1
- package/dist/proxy/index.cjs +0 -1731
- 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 -1697
- package/dist/proxy/index.js.map +0 -1
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
// Step 3: per-OS service registration.
|
|
2
|
+
//
|
|
3
|
+
// SPEC.md §9.2 (launchd), §9.3 (systemd), §9.4 (Task Scheduler).
|
|
4
|
+
//
|
|
5
|
+
// On every supported OS we:
|
|
6
|
+
// 1. Render the template with paths.templateValues().
|
|
7
|
+
// 2. Write the rendered file to its OS-specific destination.
|
|
8
|
+
// 3. Invoke the OS tool (launchctl / systemctl / schtasks) to load
|
|
9
|
+
// the unit.
|
|
10
|
+
//
|
|
11
|
+
// Idempotence: writing replaces any prior file at the same path.
|
|
12
|
+
// launchctl bootout / systemctl daemon-reload / schtasks /Delete are
|
|
13
|
+
// run before the new file is loaded so a stale unit is replaced.
|
|
14
|
+
//
|
|
15
|
+
// Dry-run: no filesystem writes, no exec — only log lines describing
|
|
16
|
+
// what would happen.
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
const fs = require('fs');
|
|
21
|
+
const path = require('path');
|
|
22
|
+
const crypto = require('crypto');
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const log = require('./log');
|
|
26
|
+
const paths = require('./paths');
|
|
27
|
+
const template = require('./template');
|
|
28
|
+
const rcEdit = require('./rc-edit');
|
|
29
|
+
|
|
30
|
+
// B14: launchctl absolute path. SSH'd users may have a stripped PATH
|
|
31
|
+
// that does not include /bin; fail fast with a real path so the
|
|
32
|
+
// spawn error is "binary not found at known location" rather than
|
|
33
|
+
// "PATH lookup failed".
|
|
34
|
+
const LAUNCHCTL_PATH = '/bin/launchctl';
|
|
35
|
+
|
|
36
|
+
// B10: launchd target (gui/<uid> vs system/) depends on whether a
|
|
37
|
+
// graphical session is attached. Headless macOS (SSH login, CI box)
|
|
38
|
+
// must use system/ because gui/<uid> requires Aqua.
|
|
39
|
+
function launchdTarget() {
|
|
40
|
+
const headless = !!process.env.SSH_CLIENT || !process.env.DISPLAY;
|
|
41
|
+
if (headless) {
|
|
42
|
+
return 'system';
|
|
43
|
+
}
|
|
44
|
+
return `gui/${process.getuid()}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function templatePath() {
|
|
48
|
+
switch (process.platform) {
|
|
49
|
+
case 'darwin':
|
|
50
|
+
return path.join(template.templateDir('launchd'), 'com.skalpel.skalpeld.plist.tmpl');
|
|
51
|
+
case 'win32':
|
|
52
|
+
return path.join(template.templateDir('windows'), 'Task.xml.tmpl');
|
|
53
|
+
default:
|
|
54
|
+
return path.join(template.templateDir('systemd'), 'skalpeld.service.tmpl');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// renderedPowerShellPath is where service-register stages the rendered
|
|
59
|
+
// register-task.ps1 (B8). The .tmpl on disk is never executed directly.
|
|
60
|
+
function renderedPowerShellPath() {
|
|
61
|
+
return path.join(paths.configDir(), 'register-task.ps1');
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function xmlEscapedValues(values) {
|
|
65
|
+
const out = {};
|
|
66
|
+
for (const [k, v] of Object.entries(values)) {
|
|
67
|
+
out[k] = rcEdit.xmlEscape(v);
|
|
68
|
+
}
|
|
69
|
+
return out;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function loadCommands() {
|
|
73
|
+
const dest = paths.servicePath();
|
|
74
|
+
switch (process.platform) {
|
|
75
|
+
case 'darwin': {
|
|
76
|
+
const target = launchdTarget(); // B10
|
|
77
|
+
const fullTarget =
|
|
78
|
+
target === 'system' ? 'system/ai.skalpel.daemon' : `${target}/ai.skalpel.daemon`;
|
|
79
|
+
const bootstrap = target === 'system' ? 'system' : target;
|
|
80
|
+
return [
|
|
81
|
+
[LAUNCHCTL_PATH, ['bootout', fullTarget], { allowFail: true }],
|
|
82
|
+
[LAUNCHCTL_PATH, ['bootstrap', bootstrap, dest]],
|
|
83
|
+
[LAUNCHCTL_PATH, ['enable', fullTarget]],
|
|
84
|
+
[LAUNCHCTL_PATH, ['kickstart', '-k', fullTarget]],
|
|
85
|
+
];
|
|
86
|
+
}
|
|
87
|
+
case 'win32': {
|
|
88
|
+
const ps1 = renderedPowerShellPath();
|
|
89
|
+
return [
|
|
90
|
+
['powershell.exe', ['-NoProfile', '-File', ps1, '-Action', 'register', '-XmlPath', dest]],
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
default:
|
|
94
|
+
return [
|
|
95
|
+
['systemctl', ['--user', 'daemon-reload']],
|
|
96
|
+
['systemctl', ['--user', 'enable', 'skalpel-daemon.service']],
|
|
97
|
+
['systemctl', ['--user', 'start', 'skalpel-daemon.service']],
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
}
|
|
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
|
+
// B42: validate the daemon binary exists at the path the service
|
|
126
|
+
// unit will exec. SHA256 verification is deferred (TODO below) until
|
|
127
|
+
// dist.shasum is populated in package.json. The existence check is
|
|
128
|
+
// not deferred — running a service unit pointing at a missing binary
|
|
129
|
+
// guarantees a startup failure.
|
|
130
|
+
function verifyBinary() {
|
|
131
|
+
const bin = paths.binPath('skalpeld');
|
|
132
|
+
let stat;
|
|
133
|
+
try {
|
|
134
|
+
stat = fs.statSync(bin);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
throw new Error(`service-register: skalpeld binary missing at ${bin}: ${err.message}`);
|
|
137
|
+
}
|
|
138
|
+
if (!stat.isFile()) {
|
|
139
|
+
throw new Error(`service-register: ${bin} is not a regular file`);
|
|
140
|
+
}
|
|
141
|
+
// TODO(B42): wire shasum verify when package.json/dist.shasum is populated.
|
|
142
|
+
// Compute SHA-256 over the binary and compare to the published shasum
|
|
143
|
+
// so an attacker cannot swap the binary post-install.
|
|
144
|
+
let pkg;
|
|
145
|
+
try {
|
|
146
|
+
pkg = require(path.join(__dirname, '..', '..', 'package.json'));
|
|
147
|
+
} catch (_) {
|
|
148
|
+
pkg = {};
|
|
149
|
+
}
|
|
150
|
+
const expected = pkg && pkg.dist && pkg.dist.shasum;
|
|
151
|
+
if (expected) {
|
|
152
|
+
const h = crypto.createHash('sha256');
|
|
153
|
+
h.update(fs.readFileSync(bin));
|
|
154
|
+
const got = h.digest('hex');
|
|
155
|
+
if (got !== expected) {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`service-register: skalpeld shasum mismatch (got ${got}, want ${expected})`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return { bin, stat };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// B50: two-phase unregister with rollback markers. Phase 1 collects
|
|
165
|
+
// what would be undone; phase 2 executes each step and on failure
|
|
166
|
+
// runs the prior-step undo so the system does not end up in a
|
|
167
|
+
// half-detached state.
|
|
168
|
+
function unregister({ dryRun }) {
|
|
169
|
+
const dest = paths.servicePath();
|
|
170
|
+
if (dryRun) {
|
|
171
|
+
log.dryRun(`uninstall: would unregister service at ${dest}`);
|
|
172
|
+
for (const [bin, args] of unloadCommands()) {
|
|
173
|
+
log.dryRun(` would run: ${bin} ${args.join(' ')}`);
|
|
174
|
+
}
|
|
175
|
+
log.dryRun(` would rm ${dest}`);
|
|
176
|
+
return { dryRun: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Phase 1: plan the steps.
|
|
180
|
+
const steps = unloadCommands().map(([bin, args, opts]) => ({
|
|
181
|
+
bin,
|
|
182
|
+
args,
|
|
183
|
+
allowFail: !!(opts && opts.allowFail),
|
|
184
|
+
done: false,
|
|
185
|
+
}));
|
|
186
|
+
const errors = [];
|
|
187
|
+
|
|
188
|
+
// Phase 2: execute. On hard failure, log every prior step (we do not
|
|
189
|
+
// attempt to re-bootstrap because that would re-install the daemon).
|
|
190
|
+
for (const step of steps) {
|
|
191
|
+
const r = spawnSync(step.bin, step.args, { stdio: 'inherit' });
|
|
192
|
+
if (r.status !== 0 && !step.allowFail) {
|
|
193
|
+
const msg = `uninstall: ${step.bin} ${step.args.join(' ')} exited ${r.status}`;
|
|
194
|
+
errors.push(msg);
|
|
195
|
+
log.warn(`${msg}; rolling forward to file removal`);
|
|
196
|
+
} else {
|
|
197
|
+
step.done = true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
log.info(
|
|
201
|
+
`uninstall: phase 2 complete: ${steps.filter((s) => s.done).length}/${steps.length} ok` +
|
|
202
|
+
(errors.length ? ` (${errors.length} errors)` : '')
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
if (fs.existsSync(dest)) {
|
|
206
|
+
fs.unlinkSync(dest);
|
|
207
|
+
log.info(`uninstall: removed ${dest}`);
|
|
208
|
+
}
|
|
209
|
+
// Also remove the rendered helper PS1 on Windows.
|
|
210
|
+
const ps1 = renderedPowerShellPath();
|
|
211
|
+
if (process.platform === 'win32' && fs.existsSync(ps1)) {
|
|
212
|
+
fs.unlinkSync(ps1);
|
|
213
|
+
log.info(`uninstall: removed ${ps1}`);
|
|
214
|
+
}
|
|
215
|
+
return { removed: true, errors };
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function run({ dryRun }) {
|
|
219
|
+
const tmpl = templatePath();
|
|
220
|
+
const dest = paths.servicePath();
|
|
221
|
+
const values = paths.templateValues();
|
|
222
|
+
const warnings = [];
|
|
223
|
+
|
|
224
|
+
if (dryRun) {
|
|
225
|
+
log.dryRun(`step 3 service-register: would render ${tmpl} → ${dest}`);
|
|
226
|
+
for (const [bin, args] of loadCommands()) {
|
|
227
|
+
log.dryRun(` would run: ${bin} ${args.join(' ')}`);
|
|
228
|
+
}
|
|
229
|
+
return { skipped: false, dryRun: true, warnings };
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!fs.existsSync(tmpl)) {
|
|
233
|
+
log.warn(
|
|
234
|
+
`service-register: template ${tmpl} missing — skipping service ` +
|
|
235
|
+
'registration. Daemon will still launch on first TUI invocation.'
|
|
236
|
+
);
|
|
237
|
+
return { skipped: true, reason: 'no-template', warnings };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// B42: verify the daemon binary is present. Log + record a
|
|
241
|
+
// warning when missing but continue writing the service file so
|
|
242
|
+
// the wizard remains best-effort under npm postinstall. The
|
|
243
|
+
// service unit itself will fail to start at boot, which is the
|
|
244
|
+
// signal an operator notices — better than silently writing a
|
|
245
|
+
// unit that may have a stale path baked in.
|
|
246
|
+
try {
|
|
247
|
+
verifyBinary();
|
|
248
|
+
} catch (err) {
|
|
249
|
+
log.warn(`service-register: ${err.message}; continuing — service unit will surface this at boot`);
|
|
250
|
+
warnings.push(err.message);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
254
|
+
// B49: explicit chmod after recursive mkdir — Node's mkdirSync
|
|
255
|
+
// honours mode only for the *leaf* directory, not the intermediate
|
|
256
|
+
// ones. The configDir + logsDir need a final chmod to land at
|
|
257
|
+
// 0o700 reliably across platforms.
|
|
258
|
+
fs.mkdirSync(paths.configDir(), { recursive: true, mode: 0o700 });
|
|
259
|
+
fs.chmodSync(paths.configDir(), 0o700);
|
|
260
|
+
fs.mkdirSync(paths.logsDir(), { recursive: true, mode: 0o700 });
|
|
261
|
+
fs.chmodSync(paths.logsDir(), 0o700);
|
|
262
|
+
|
|
263
|
+
const renderValues = process.platform === 'win32' ? xmlEscapedValues(values) : values;
|
|
264
|
+
const rendered = template.render(tmpl, renderValues);
|
|
265
|
+
fs.writeFileSync(dest, rendered, { mode: 0o644 });
|
|
266
|
+
log.info(`service-register: wrote ${dest}`);
|
|
267
|
+
|
|
268
|
+
if (process.platform === 'win32') {
|
|
269
|
+
const ps1Tmpl = path.join(template.templateDir('windows'), 'register-task.ps1.tmpl');
|
|
270
|
+
const ps1Path = renderedPowerShellPath();
|
|
271
|
+
const ps1Body = template.render(ps1Tmpl, values);
|
|
272
|
+
fs.writeFileSync(ps1Path, ps1Body, { mode: 0o755 });
|
|
273
|
+
log.info(`service-register: wrote ${ps1Path}`);
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// B13: collect warnings on non-fatal failures and return them so
|
|
277
|
+
// the caller (postinstall/index.js) can decide whether to treat
|
|
278
|
+
// the wizard run as ok-with-warnings or fail.
|
|
279
|
+
for (const [bin, args, opts] of loadCommands()) {
|
|
280
|
+
const r = spawnSync(bin, args, { stdio: 'inherit' });
|
|
281
|
+
if (r.status !== 0 && !(opts && opts.allowFail)) {
|
|
282
|
+
const msg =
|
|
283
|
+
`service-register: ${bin} ${args.join(' ')} exited ${r.status}; ` +
|
|
284
|
+
'continuing without daemon-on-boot';
|
|
285
|
+
log.warn(msg);
|
|
286
|
+
warnings.push(msg);
|
|
287
|
+
return { skipped: false, registered: false, warnings };
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { skipped: false, registered: true, warnings };
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { run, unregister, templatePath, loadCommands, unloadCommands, launchdTarget };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// Step 2: sign in.
|
|
2
|
+
//
|
|
3
|
+
// SPEC.md §9.6 step 3 — run `skalpel login` in browser-loopback mode
|
|
4
|
+
// inline. Manual-paste fallback if loopback bind fails.
|
|
5
|
+
//
|
|
6
|
+
// During npm postinstall the user's shell has not yet been re-sourced
|
|
7
|
+
// so the `skalpel` command may not be on PATH. We resolve the
|
|
8
|
+
// dispatch shim via require.resolve() (B7) so a renamed top-level
|
|
9
|
+
// directory does not break the lookup, then invoke it via `node`.
|
|
10
|
+
//
|
|
11
|
+
// In dry-run mode we never spawn anything; we only log the action.
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const { spawnSync } = require('child_process');
|
|
18
|
+
|
|
19
|
+
const log = require('./log');
|
|
20
|
+
const paths = require('./paths');
|
|
21
|
+
|
|
22
|
+
// SIGN_IN_TIMEOUT_MS caps how long we wait for the spawned `skalpel
|
|
23
|
+
// login` subprocess. SPEC.md §4.5 sets the loopback timeout at 5 min;
|
|
24
|
+
// we add a small slack so the user has time to complete the browser
|
|
25
|
+
// flow.
|
|
26
|
+
const SIGN_IN_TIMEOUT_MS = 5 * 60 * 1000 + 30 * 1000;
|
|
27
|
+
|
|
28
|
+
// resolveDispatcher returns the path to the dispatch shim. B7:
|
|
29
|
+
// require.resolve('../../npm-bin/skalpel.js') is anchored to the
|
|
30
|
+
// installed package layout — works whether the package is installed
|
|
31
|
+
// globally or linked locally.
|
|
32
|
+
function resolveDispatcher() {
|
|
33
|
+
// Try require.resolve first (handles symlinked / deduped layouts).
|
|
34
|
+
try {
|
|
35
|
+
return require.resolve('../../npm-bin/skalpel.js');
|
|
36
|
+
} catch (_) {
|
|
37
|
+
// Fall back to a path-join lookup so dev environments without a
|
|
38
|
+
// resolvable layout still work.
|
|
39
|
+
const guess = path.join(__dirname, '..', '..', 'npm-bin', 'skalpel.js');
|
|
40
|
+
if (fs.existsSync(guess)) {
|
|
41
|
+
return guess;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function run({ dryRun, prior }) {
|
|
48
|
+
if (prior) {
|
|
49
|
+
log.info('sign-in: prior auth.json detected — skipping browser flow');
|
|
50
|
+
return { skipped: true };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const dispatcher = resolveDispatcher();
|
|
54
|
+
|
|
55
|
+
if (dryRun) {
|
|
56
|
+
log.dryRun(
|
|
57
|
+
`step 2 sign-in: would exec \`node ${dispatcher || '<dispatcher-missing>'} login\` ` +
|
|
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' };
|
|
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 };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = { run, resolveDispatcher };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// Minimal placeholder substitution for postinstall templates.
|
|
2
|
+
//
|
|
3
|
+
// Templates live alongside this file (postinstall/launchd/...,
|
|
4
|
+
// postinstall/systemd/..., postinstall/windows/...). Each template
|
|
5
|
+
// uses {{NAME}} placeholders; we replace them with the values from
|
|
6
|
+
// paths.templateValues().
|
|
7
|
+
//
|
|
8
|
+
// We deliberately do not pull in handlebars/mustache: stdlib only
|
|
9
|
+
// keeps the npm install footprint at zero, which matters for a
|
|
10
|
+
// postinstall hook.
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
|
|
17
|
+
function render(templatePath, values) {
|
|
18
|
+
const raw = fs.readFileSync(templatePath, 'utf8');
|
|
19
|
+
return substitute(raw, values);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function substitute(body, values) {
|
|
23
|
+
return body.replace(/\{\{([A-Z_][A-Z0-9_]*)\}\}/g, (m, key) => {
|
|
24
|
+
if (Object.prototype.hasOwnProperty.call(values, key)) {
|
|
25
|
+
return String(values[key]);
|
|
26
|
+
}
|
|
27
|
+
// Leave unknown placeholders untouched so the operator sees them.
|
|
28
|
+
return m;
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function templateDir(subdir) {
|
|
33
|
+
return path.join(__dirname, '..', subdir);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = { render, substitute, templateDir };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Reference snippet — bash form of the managed block. Identical body
|
|
2
|
+
# to zsh.sh.tmpl per SPEC.md §9.5; the only difference is the rc-file
|
|
3
|
+
# path the editor writes to (~/.bashrc or ~/.bash_profile).
|
|
4
|
+
# >>> @skalpelai/skalpel begin (managed)
|
|
5
|
+
# This block is managed by skalpel install. Do not edit by hand;
|
|
6
|
+
# re-run `skalpel install` or `npx skalpel` to update.
|
|
7
|
+
export ANTHROPIC_API_URL="http://127.0.0.1:7878"
|
|
8
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:7878"
|
|
9
|
+
export OPENAI_BASE_URL="http://127.0.0.1:7878/v1"
|
|
10
|
+
export OPENAI_API_BASE="http://127.0.0.1:7878/v1"
|
|
11
|
+
export SKALPEL_PROXY_URL="http://127.0.0.1:7878"
|
|
12
|
+
# <<< @skalpelai/skalpel end (managed)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# Reference snippet — fish form of the managed block. SPEC.md §9.5
|
|
2
|
+
# also allows the conf.d variant at ~/.config/fish/conf.d/skalpel.fish;
|
|
3
|
+
# the editor uses the rc-file form to keep the four shells uniform.
|
|
4
|
+
# >>> @skalpelai/skalpel begin (managed)
|
|
5
|
+
# Managed by skalpel install; safe to delete.
|
|
6
|
+
set -gx ANTHROPIC_API_URL "http://127.0.0.1:7878"
|
|
7
|
+
set -gx ANTHROPIC_BASE_URL "http://127.0.0.1:7878"
|
|
8
|
+
set -gx OPENAI_BASE_URL "http://127.0.0.1:7878/v1"
|
|
9
|
+
set -gx OPENAI_API_BASE "http://127.0.0.1:7878/v1"
|
|
10
|
+
set -gx SKALPEL_PROXY_URL "http://127.0.0.1:7878"
|
|
11
|
+
# <<< @skalpelai/skalpel end (managed)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Reference snippet — PowerShell form of the managed block. The fence
|
|
2
|
+
# uses the trailing >>> / <<< sigils per SPEC.md §9.5 so the marker
|
|
3
|
+
# parses cleanly against PowerShell's $env:NAME prefix style.
|
|
4
|
+
# >>> @skalpelai/skalpel begin (managed) >>>
|
|
5
|
+
# This block is managed by skalpel install. Do not edit by hand;
|
|
6
|
+
# re-run `skalpel install` or `npx skalpel` to update.
|
|
7
|
+
$env:ANTHROPIC_API_URL = 'http://127.0.0.1:7878'
|
|
8
|
+
$env:ANTHROPIC_BASE_URL = 'http://127.0.0.1:7878'
|
|
9
|
+
$env:OPENAI_BASE_URL = 'http://127.0.0.1:7878/v1'
|
|
10
|
+
$env:OPENAI_API_BASE = 'http://127.0.0.1:7878/v1'
|
|
11
|
+
$env:SKALPEL_PROXY_URL = 'http://127.0.0.1:7878'
|
|
12
|
+
# <<< @skalpelai/skalpel end (managed) <<<
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Reference snippet — zsh form of the managed block emitted by
|
|
2
|
+
# postinstall/lib/rc-edit.js. The runtime body is computed in code
|
|
3
|
+
# (so the active port is filled in from <CONFIG_DIR>/skalpeld.lock);
|
|
4
|
+
# this template is for documentation/diff comparisons.
|
|
5
|
+
# >>> @skalpelai/skalpel begin (managed)
|
|
6
|
+
# This block is managed by skalpel install. Do not edit by hand;
|
|
7
|
+
# re-run `skalpel install` or `npx skalpel` to update.
|
|
8
|
+
export ANTHROPIC_API_URL="http://127.0.0.1:7878"
|
|
9
|
+
export ANTHROPIC_BASE_URL="http://127.0.0.1:7878"
|
|
10
|
+
export OPENAI_BASE_URL="http://127.0.0.1:7878/v1"
|
|
11
|
+
export OPENAI_API_BASE="http://127.0.0.1:7878/v1"
|
|
12
|
+
export SKALPEL_PROXY_URL="http://127.0.0.1:7878"
|
|
13
|
+
# <<< @skalpelai/skalpel end (managed)
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# systemd user unit for the Skalpel local proxy daemon.
|
|
2
|
+
# SPEC.md §9.3 — installed at ~/.config/systemd/user/skalpel-daemon.service
|
|
3
|
+
# Postinstall placeholders: {{USER}} {{HOME}} {{BIN}} {{CONFIG_DIR}}.
|
|
4
|
+
[Unit]
|
|
5
|
+
Description=Skalpel daemon (local proxy) — user={{USER}}
|
|
6
|
+
Documentation=https://skalpel.ai/docs/cli
|
|
7
|
+
After=default.target
|
|
8
|
+
|
|
9
|
+
[Service]
|
|
10
|
+
Type=simple
|
|
11
|
+
ExecStart="{{BIN}}/skalpeld" --service-mode
|
|
12
|
+
Restart=on-failure
|
|
13
|
+
RestartSec=10s
|
|
14
|
+
|
|
15
|
+
# Process hygiene
|
|
16
|
+
StandardOutput=append:{{CONFIG_DIR}}/logs/skalpeld.log
|
|
17
|
+
StandardError=append:{{CONFIG_DIR}}/logs/skalpeld.log
|
|
18
|
+
WorkingDirectory={{HOME}}
|
|
19
|
+
|
|
20
|
+
# Sandboxing (best-effort; user units can't enforce all of these).
|
|
21
|
+
PrivateTmp=true
|
|
22
|
+
ProtectSystem=strict
|
|
23
|
+
ReadWritePaths={{CONFIG_DIR}}
|
|
24
|
+
NoNewPrivileges=true
|
|
25
|
+
|
|
26
|
+
# Allow bind to 127.0.0.1:<port> (privileged binds not needed; ports >= 1024).
|
|
27
|
+
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6
|
|
28
|
+
|
|
29
|
+
Environment="SKALPEL_CONFIG_DIR={{CONFIG_DIR}}"
|
|
30
|
+
Environment="HOME={{HOME}}"
|
|
31
|
+
|
|
32
|
+
[Install]
|
|
33
|
+
WantedBy=default.target
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<Task version="1.4" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
3
|
+
<RegistrationInfo>
|
|
4
|
+
<Date>2026-05-03T10:00:00</Date>
|
|
5
|
+
<Author>skalpel</Author>
|
|
6
|
+
<Description>Skalpel daemon (local proxy) — user={{USER}}</Description>
|
|
7
|
+
<URI>\Skalpel\skalpel-daemon</URI>
|
|
8
|
+
</RegistrationInfo>
|
|
9
|
+
<Triggers>
|
|
10
|
+
<LogonTrigger>
|
|
11
|
+
<Enabled>true</Enabled>
|
|
12
|
+
<UserId>{{USER}}</UserId>
|
|
13
|
+
</LogonTrigger>
|
|
14
|
+
</Triggers>
|
|
15
|
+
<Principals>
|
|
16
|
+
<Principal id="Author">
|
|
17
|
+
<UserId>{{USER}}</UserId>
|
|
18
|
+
<LogonType>InteractiveToken</LogonType>
|
|
19
|
+
<RunLevel>LeastPrivilege</RunLevel>
|
|
20
|
+
</Principal>
|
|
21
|
+
</Principals>
|
|
22
|
+
<Settings>
|
|
23
|
+
<MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy>
|
|
24
|
+
<DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries>
|
|
25
|
+
<StopIfGoingOnBatteries>false</StopIfGoingOnBatteries>
|
|
26
|
+
<AllowHardTerminate>false</AllowHardTerminate>
|
|
27
|
+
<RestartOnFailure>
|
|
28
|
+
<Interval>PT10S</Interval>
|
|
29
|
+
<Count>3</Count>
|
|
30
|
+
</RestartOnFailure>
|
|
31
|
+
<Hidden>true</Hidden>
|
|
32
|
+
<ExecutionTimeLimit>PT0S</ExecutionTimeLimit>
|
|
33
|
+
</Settings>
|
|
34
|
+
<Actions Context="Author">
|
|
35
|
+
<Exec>
|
|
36
|
+
<Command>{{BIN}}\skalpeld.exe</Command>
|
|
37
|
+
<Arguments>--service-mode</Arguments>
|
|
38
|
+
<WorkingDirectory>{{CONFIG_DIR}}</WorkingDirectory>
|
|
39
|
+
</Exec>
|
|
40
|
+
</Actions>
|
|
41
|
+
<!-- Home reference for postinstall renderer: {{HOME}} -->
|
|
42
|
+
</Task>
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# register-task.ps1 — register or unregister the Skalpel daemon Scheduled Task.
|
|
2
|
+
#
|
|
3
|
+
# Invoked by postinstall/lib/service-register.js. Runs as the current
|
|
4
|
+
# user (no elevation); the task itself runs as the same user at logon.
|
|
5
|
+
#
|
|
6
|
+
# Placeholders rendered by postinstall/lib/template.js:
|
|
7
|
+
# {{USER}} — interactive user (also written into Task.xml)
|
|
8
|
+
# {{HOME}} — user's home directory
|
|
9
|
+
# {{BIN}} — directory containing skalpeld.exe
|
|
10
|
+
# {{CONFIG_DIR}} — %APPDATA%\skalpel
|
|
11
|
+
#
|
|
12
|
+
# This file is a TEMPLATE; the renderer substitutes the placeholders
|
|
13
|
+
# above before the script is invoked.
|
|
14
|
+
|
|
15
|
+
[CmdletBinding()]
|
|
16
|
+
param(
|
|
17
|
+
[Parameter(Mandatory = $true)]
|
|
18
|
+
[ValidateSet('register', 'unregister')]
|
|
19
|
+
[string]$Action,
|
|
20
|
+
|
|
21
|
+
[Parameter()]
|
|
22
|
+
[string]$XmlPath = '{{CONFIG_DIR}}\Task.xml',
|
|
23
|
+
|
|
24
|
+
[Parameter()]
|
|
25
|
+
[string]$TaskName = 'Skalpel\skalpel-daemon'
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
$ErrorActionPreference = 'Stop'
|
|
29
|
+
|
|
30
|
+
Write-Host "skalpel install: register-task.ps1 user={{USER}} home={{HOME}} bin={{BIN}}"
|
|
31
|
+
|
|
32
|
+
switch ($Action) {
|
|
33
|
+
'register' {
|
|
34
|
+
if (-not (Test-Path -LiteralPath $XmlPath)) {
|
|
35
|
+
Write-Error "Task XML not found at $XmlPath"
|
|
36
|
+
exit 1
|
|
37
|
+
}
|
|
38
|
+
& schtasks.exe /Create /XML $XmlPath /TN $TaskName /F | Out-Host
|
|
39
|
+
& schtasks.exe /Run /TN $TaskName | Out-Host
|
|
40
|
+
}
|
|
41
|
+
'unregister' {
|
|
42
|
+
& schtasks.exe /End /TN $TaskName 2>$null | Out-Host
|
|
43
|
+
& schtasks.exe /Delete /TN $TaskName /F | Out-Host
|
|
44
|
+
}
|
|
45
|
+
}
|