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/npm-bin/colors.js
DELETED
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
|
|
3
|
-
// colors.js — Catppuccin Frappé palette wrapper for the npm-side
|
|
4
|
-
// surfaces (npx wizard banner, postinstall success line). Reads the
|
|
5
|
-
// canonical hex values from design-tokens.json (Wave 1 artifact) and
|
|
6
|
-
// wraps Picocolors so NO_COLOR is honoured automatically. The box()
|
|
7
|
-
// helper draws a rounded Unicode-art frame around content; structure
|
|
8
|
-
// chars are emitted unconditionally, only the ANSI sequences are
|
|
9
|
-
// suppressed under NO_COLOR.
|
|
10
|
-
|
|
11
|
-
const path = require('path');
|
|
12
|
-
const pc = require('picocolors');
|
|
13
|
-
|
|
14
|
-
const tokens = require(path.resolve(__dirname, '..', 'design-tokens.json'));
|
|
15
|
-
|
|
16
|
-
const palette = (tokens.themes && tokens.themes['tokyo-night-storm']) || {};
|
|
17
|
-
|
|
18
|
-
function rgbFromHex(hex) {
|
|
19
|
-
const m = String(hex || '').replace('#', '').match(/^([0-9a-fA-F]{6})$/);
|
|
20
|
-
if (!m) return null;
|
|
21
|
-
const v = parseInt(m[1], 16);
|
|
22
|
-
return [(v >> 16) & 0xff, (v >> 8) & 0xff, v & 0xff];
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
function colorize(hex) {
|
|
26
|
-
const rgb = rgbFromHex(hex);
|
|
27
|
-
if (!rgb) {
|
|
28
|
-
return (s) => String(s);
|
|
29
|
-
}
|
|
30
|
-
return (s) => {
|
|
31
|
-
if (process.env.NO_COLOR) return String(s);
|
|
32
|
-
return `\x1b[38;2;${rgb[0]};${rgb[1]};${rgb[2]}m${s}\x1b[39m`;
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const theme = {};
|
|
37
|
-
for (const name of Object.keys(palette)) {
|
|
38
|
-
theme[name] = colorize(palette[name]);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function attr(open, close) {
|
|
42
|
-
return (s) => {
|
|
43
|
-
if (process.env.NO_COLOR) return String(s);
|
|
44
|
-
return `\x1b[${open}m${s}\x1b[${close}m`;
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
theme.bold = attr(1, 22);
|
|
49
|
-
theme.italic = attr(3, 23);
|
|
50
|
-
theme.dim = attr(2, 22);
|
|
51
|
-
|
|
52
|
-
const semantic = (tokens.semantic) || {};
|
|
53
|
-
|
|
54
|
-
function semanticHelper(key) {
|
|
55
|
-
const ref = semantic[key];
|
|
56
|
-
if (!ref) return (s) => String(s);
|
|
57
|
-
const parts = ref.split('.');
|
|
58
|
-
if (parts.length !== 2) return (s) => String(s);
|
|
59
|
-
const colorName = parts[1];
|
|
60
|
-
return theme[colorName] || ((s) => String(s));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function semanticKey(name) {
|
|
64
|
-
return name
|
|
65
|
-
.replace(/[^a-zA-Z0-9]+([a-zA-Z0-9])/g, (_, c) => c.toUpperCase())
|
|
66
|
-
.replace(/^([A-Z])/, (m) => m.toLowerCase());
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
for (const k of Object.keys(semantic)) {
|
|
70
|
-
const camel = semanticKey(k);
|
|
71
|
-
theme[camel] = semanticHelper(k);
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const BORDERS = {
|
|
75
|
-
topLeft: '╭',
|
|
76
|
-
topRight: '╮',
|
|
77
|
-
bottomLeft: '╰',
|
|
78
|
-
bottomRight: '╯',
|
|
79
|
-
horizontal: '─',
|
|
80
|
-
vertical: '│',
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
function visibleLength(s) {
|
|
84
|
-
return String(s).replace(/\x1b\[[0-9;]*m/g, '').length;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function box(content, borderColor, options) {
|
|
88
|
-
const opts = options || {};
|
|
89
|
-
const padding = typeof opts.padding === 'number' ? opts.padding : 1;
|
|
90
|
-
const lines = String(content).split('\n');
|
|
91
|
-
|
|
92
|
-
let inner;
|
|
93
|
-
if (opts.width === 'fit' || opts.width == null) {
|
|
94
|
-
inner = 0;
|
|
95
|
-
for (const ln of lines) {
|
|
96
|
-
const len = visibleLength(ln);
|
|
97
|
-
if (len > inner) inner = len;
|
|
98
|
-
}
|
|
99
|
-
inner += padding * 2;
|
|
100
|
-
} else {
|
|
101
|
-
inner = Math.max(0, Number(opts.width) - 2);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const colorOK = pc.isColorSupported && !process.env.NO_COLOR;
|
|
105
|
-
const colorFn = (colorOK && typeof borderColor === 'string' && theme[borderColor])
|
|
106
|
-
? theme[borderColor]
|
|
107
|
-
: (s) => String(s);
|
|
108
|
-
|
|
109
|
-
const horiz = BORDERS.horizontal.repeat(inner);
|
|
110
|
-
const top = colorFn(BORDERS.topLeft + horiz + BORDERS.topRight);
|
|
111
|
-
const bottom = colorFn(BORDERS.bottomLeft + horiz + BORDERS.bottomRight);
|
|
112
|
-
const v = colorFn(BORDERS.vertical);
|
|
113
|
-
const pad = ' '.repeat(padding);
|
|
114
|
-
|
|
115
|
-
const rendered = [top];
|
|
116
|
-
for (const ln of lines) {
|
|
117
|
-
const visLen = visibleLength(ln);
|
|
118
|
-
const fill = ' '.repeat(Math.max(0, inner - padding * 2 - visLen));
|
|
119
|
-
rendered.push(`${v}${pad}${ln}${fill}${pad}${v}`);
|
|
120
|
-
}
|
|
121
|
-
rendered.push(bottom);
|
|
122
|
-
return rendered.join('\n');
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
module.exports = { theme, box, BORDERS, palette };
|
|
@@ -1,232 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// BUG-0039 — Windows daemon missing → Defender-aware warning test.
|
|
3
|
-
//
|
|
4
|
-
// What we're testing: the sibling-check branch in resolveBinary. When
|
|
5
|
-
// the user runs `skalpel <anything>` on Windows AND skalpeld.exe is
|
|
6
|
-
// missing from the platform package's bin/, the CLI must:
|
|
7
|
-
// - emit a clear actionable warning to stderr
|
|
8
|
-
// - NOT exit (so --version / --help still work)
|
|
9
|
-
// - include the PowerShell Add-MpPreference command
|
|
10
|
-
// - use single-quoted path (neutralises NTFS-legal $ / backtick / ')
|
|
11
|
-
//
|
|
12
|
-
// Stdlib-only, no test framework. Builds a fake @skalpelai/skalpel-*
|
|
13
|
-
// package layout in a tmpdir inside the repo's node_modules so Node's
|
|
14
|
-
// resolver finds it, then patches Module._resolveLookupPaths to prefer
|
|
15
|
-
// the sandbox.
|
|
16
|
-
//
|
|
17
|
-
// Run: node npm-bin/skalpel.bug-0039.test.js
|
|
18
|
-
'use strict';
|
|
19
|
-
|
|
20
|
-
const assert = require('assert');
|
|
21
|
-
const fs = require('fs');
|
|
22
|
-
const os = require('os');
|
|
23
|
-
const path = require('path');
|
|
24
|
-
|
|
25
|
-
let pass = 0;
|
|
26
|
-
let fail = 0;
|
|
27
|
-
|
|
28
|
-
function t(name, fn) {
|
|
29
|
-
try {
|
|
30
|
-
fn();
|
|
31
|
-
process.stdout.write(` PASS ${name}\n`);
|
|
32
|
-
pass += 1;
|
|
33
|
-
} catch (err) {
|
|
34
|
-
process.stderr.write(` FAIL ${name}\n ${err && err.stack ? err.stack : err}\n`);
|
|
35
|
-
fail += 1;
|
|
36
|
-
}
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
// Capture stderr + intercepted process.exit code.
|
|
40
|
-
function captureStderr(fn) {
|
|
41
|
-
const original = process.stderr.write.bind(process.stderr);
|
|
42
|
-
let buf = '';
|
|
43
|
-
process.stderr.write = (chunk) => {
|
|
44
|
-
buf += String(chunk);
|
|
45
|
-
return true;
|
|
46
|
-
};
|
|
47
|
-
const origExit = process.exit;
|
|
48
|
-
let exitCode = null;
|
|
49
|
-
process.exit = (code) => {
|
|
50
|
-
exitCode = code;
|
|
51
|
-
throw new Error('__BUG0039_TEST_EXIT__');
|
|
52
|
-
};
|
|
53
|
-
try {
|
|
54
|
-
try { fn(); } catch (e) {
|
|
55
|
-
if (e && e.message !== '__BUG0039_TEST_EXIT__') throw e;
|
|
56
|
-
}
|
|
57
|
-
} finally {
|
|
58
|
-
process.stderr.write = original;
|
|
59
|
-
process.exit = origExit;
|
|
60
|
-
}
|
|
61
|
-
return { stderr: buf, exitCode };
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function withFakeWin32Package(opts) {
|
|
65
|
-
// Use os.tmpdir() (NOT in-repo node_modules) so a test crash between
|
|
66
|
-
// mkdir and finally-cleanup can't leave a stub that shadows the real
|
|
67
|
-
// platform package on subsequent dev runs.
|
|
68
|
-
const sandboxRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'bug0039-'));
|
|
69
|
-
const platDir = path.join(sandboxRoot, 'node_modules', '@skalpelai', 'skalpel-win32-x64');
|
|
70
|
-
const binDir = path.join(platDir, 'bin');
|
|
71
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
72
|
-
fs.writeFileSync(
|
|
73
|
-
path.join(platDir, 'package.json'),
|
|
74
|
-
JSON.stringify({ name: '@skalpelai/skalpel-win32-x64', version: '3.2.2-test' })
|
|
75
|
-
);
|
|
76
|
-
fs.writeFileSync(path.join(binDir, 'skalpel.exe'), 'stub');
|
|
77
|
-
if (opts.daemonPresent) fs.writeFileSync(path.join(binDir, 'skalpeld.exe'), 'stub');
|
|
78
|
-
return {
|
|
79
|
-
sandboxRoot,
|
|
80
|
-
cleanup: () => fs.rmSync(sandboxRoot, { recursive: true, force: true }),
|
|
81
|
-
};
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function withResolveOverride(sandboxRoot, fn) {
|
|
85
|
-
const Module = require('module');
|
|
86
|
-
const orig = Module._resolveLookupPaths;
|
|
87
|
-
Module._resolveLookupPaths = function (request, parent) {
|
|
88
|
-
const base = orig.call(this, request, parent);
|
|
89
|
-
if (request && request.startsWith('@skalpelai/skalpel-')) {
|
|
90
|
-
const inject = path.join(sandboxRoot, 'node_modules');
|
|
91
|
-
return base && Array.isArray(base) ? [inject, ...base] : [inject];
|
|
92
|
-
}
|
|
93
|
-
return base;
|
|
94
|
-
};
|
|
95
|
-
try { return fn(); } finally { Module._resolveLookupPaths = orig; }
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
const skalpel = require('./skalpel.js');
|
|
99
|
-
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
100
|
-
Object.defineProperty(process, 'arch', { value: 'x64', configurable: true });
|
|
101
|
-
|
|
102
|
-
// ── Test 1: skalpel-resolve + missing sibling daemon → warning, no exit
|
|
103
|
-
t('resolveBinary(\'skalpel\') with missing skalpeld.exe emits warning and returns', () => {
|
|
104
|
-
const fake = withFakeWin32Package({ daemonPresent: false });
|
|
105
|
-
try {
|
|
106
|
-
let returned = null;
|
|
107
|
-
const { stderr, exitCode } = captureStderr(() => {
|
|
108
|
-
withResolveOverride(fake.sandboxRoot, () => {
|
|
109
|
-
returned = skalpel.resolveBinary('skalpel', []);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
// MUST return the resolved skalpel.exe path (no exit)
|
|
113
|
-
assert.ok(returned, `expected non-null return, got ${returned}`);
|
|
114
|
-
assert.ok(returned.endsWith('skalpel.exe'), `expected skalpel.exe, got ${returned}`);
|
|
115
|
-
assert.strictEqual(exitCode, null, `expected NO process.exit, got code=${exitCode}`);
|
|
116
|
-
// MUST emit Defender-aware warning
|
|
117
|
-
assert.ok(stderr.includes('skalpel daemon missing'), `expected daemon-missing title:\n${stderr}`);
|
|
118
|
-
assert.ok(stderr.includes('Defender'), `expected Defender mention:\n${stderr}`);
|
|
119
|
-
assert.ok(stderr.includes('Add-MpPreference'), `expected PowerShell command:\n${stderr}`);
|
|
120
|
-
assert.ok(stderr.includes('Authenticode'), `expected code-sign mention:\n${stderr}`);
|
|
121
|
-
assert.ok(stderr.includes('Restore'), `expected restore path:\n${stderr}`);
|
|
122
|
-
// PowerShell path is single-quoted (the critical quoting fix)
|
|
123
|
-
const dirLine = stderr.split('\n').find((l) => l.includes('$dir ='));
|
|
124
|
-
assert.ok(dirLine, `expected $dir = line, got:\n${stderr}`);
|
|
125
|
-
assert.ok(/\$dir = '/.test(dirLine), `expected single-quoted path in $dir line: ${dirLine}`);
|
|
126
|
-
// The Add-MpPreference line should reference $dir (not embed the path)
|
|
127
|
-
const mpLine = stderr.split('\n').find((l) => l.includes('Add-MpPreference'));
|
|
128
|
-
assert.ok(mpLine, `expected Add-MpPreference line:\n${stderr}`);
|
|
129
|
-
assert.ok(/-ExclusionPath \$dir/.test(mpLine), `expected -ExclusionPath $dir: ${mpLine}`);
|
|
130
|
-
} finally { fake.cleanup(); }
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
// ── Test 2: happy path (daemon present) → returns path, NO warning
|
|
134
|
-
t('resolveBinary(\'skalpel\') with daemon present: silent, returns path', () => {
|
|
135
|
-
const fake = withFakeWin32Package({ daemonPresent: true });
|
|
136
|
-
try {
|
|
137
|
-
let returned = null;
|
|
138
|
-
const { stderr, exitCode } = captureStderr(() => {
|
|
139
|
-
withResolveOverride(fake.sandboxRoot, () => {
|
|
140
|
-
returned = skalpel.resolveBinary('skalpel', []);
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
assert.ok(returned && returned.endsWith('skalpel.exe'), `expected skalpel.exe, got ${returned}`);
|
|
144
|
-
assert.strictEqual(exitCode, null);
|
|
145
|
-
assert.ok(!stderr.includes('Defender'), `expected no Defender warning on happy path, got:\n${stderr}`);
|
|
146
|
-
assert.ok(!stderr.includes('quarantine'), `expected no quarantine mention on happy path:\n${stderr}`);
|
|
147
|
-
} finally { fake.cleanup(); }
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
// ── Test 3: skalpel.exe itself missing → generic Binary missing error
|
|
151
|
-
t('resolveBinary(\'skalpel\') with skalpel.exe missing: generic error, NOT Defender block', () => {
|
|
152
|
-
const fake = withFakeWin32Package({ daemonPresent: false });
|
|
153
|
-
// Also remove skalpel.exe
|
|
154
|
-
fs.unlinkSync(path.join(fake.sandboxRoot, 'node_modules', '@skalpelai', 'skalpel-win32-x64', 'bin', 'skalpel.exe'));
|
|
155
|
-
try {
|
|
156
|
-
const { stderr, exitCode } = captureStderr(() => {
|
|
157
|
-
withResolveOverride(fake.sandboxRoot, () => {
|
|
158
|
-
skalpel.resolveBinary('skalpel', []);
|
|
159
|
-
});
|
|
160
|
-
});
|
|
161
|
-
assert.strictEqual(exitCode, 1, 'expected exit 1 on missing CLI binary');
|
|
162
|
-
assert.ok(stderr.includes('Binary missing'), `expected generic title:\n${stderr}`);
|
|
163
|
-
// The Defender block is gated on a successful candidate-exists check,
|
|
164
|
-
// so it should NOT fire when skalpel.exe itself is missing.
|
|
165
|
-
assert.ok(!stderr.includes('Defender'), `Defender warning should NOT fire when CLI itself missing:\n${stderr}`);
|
|
166
|
-
} finally { fake.cleanup(); }
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
// ── Test 4: non-Windows platform → no sibling check fires
|
|
170
|
-
t('resolveBinary(\'skalpel\') on linux: no sibling check, silent on success', () => {
|
|
171
|
-
// Temporarily flip to linux + reset PLATFORM_PACKAGES key check
|
|
172
|
-
Object.defineProperty(process, 'platform', { value: 'linux', configurable: true });
|
|
173
|
-
try {
|
|
174
|
-
const sandboxRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'bug0039-linux-'));
|
|
175
|
-
const platDir = path.join(sandboxRoot, 'node_modules', '@skalpelai', 'skalpel-linux-x64');
|
|
176
|
-
const binDir = path.join(platDir, 'bin');
|
|
177
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
178
|
-
fs.writeFileSync(path.join(platDir, 'package.json'), JSON.stringify({ name: '@skalpelai/skalpel-linux-x64', version: '3.2.2-test' }));
|
|
179
|
-
fs.writeFileSync(path.join(binDir, 'skalpel'), 'stub');
|
|
180
|
-
// daemon intentionally missing
|
|
181
|
-
try {
|
|
182
|
-
const { stderr, exitCode } = captureStderr(() => {
|
|
183
|
-
withResolveOverride(sandboxRoot, () => {
|
|
184
|
-
skalpel.resolveBinary('skalpel', []);
|
|
185
|
-
});
|
|
186
|
-
});
|
|
187
|
-
assert.strictEqual(exitCode, null, 'expected no exit on linux happy path');
|
|
188
|
-
assert.ok(!stderr.includes('Defender'), `linux must NOT emit Defender warning:\n${stderr}`);
|
|
189
|
-
} finally { fs.rmSync(sandboxRoot, { recursive: true, force: true }); }
|
|
190
|
-
} finally {
|
|
191
|
-
Object.defineProperty(process, 'platform', { value: 'win32', configurable: true });
|
|
192
|
-
}
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
// ── Test 5: PowerShell escape of literal apostrophe in path
|
|
196
|
-
t("PowerShell '' escape activates when path contains a literal apostrophe", () => {
|
|
197
|
-
// Build a sandbox under a parent dir whose NAME contains an apostrophe.
|
|
198
|
-
// NTFS allows ' in filenames, and POSIX paths do too. The Defender
|
|
199
|
-
// exclusion path will be path.dirname(siblingDaemon) — which on this
|
|
200
|
-
// sandbox includes a "with'apos" segment. The PowerShell single-quote
|
|
201
|
-
// literal MUST escape the inner ' as '' to remain valid PS syntax.
|
|
202
|
-
const parent = fs.mkdtempSync(path.join(os.tmpdir(), "bug0039-apos-"));
|
|
203
|
-
const aposDir = path.join(parent, "with'apos");
|
|
204
|
-
const platDir = path.join(aposDir, 'node_modules', '@skalpelai', 'skalpel-win32-x64');
|
|
205
|
-
const binDir = path.join(platDir, 'bin');
|
|
206
|
-
fs.mkdirSync(binDir, { recursive: true });
|
|
207
|
-
fs.writeFileSync(path.join(platDir, 'package.json'), JSON.stringify({ name: '@skalpelai/skalpel-win32-x64', version: '3.2.2-test' }));
|
|
208
|
-
fs.writeFileSync(path.join(binDir, 'skalpel.exe'), 'stub');
|
|
209
|
-
// skalpeld.exe intentionally missing
|
|
210
|
-
try {
|
|
211
|
-
const { stderr } = captureStderr(() => {
|
|
212
|
-
withResolveOverride(aposDir, () => {
|
|
213
|
-
skalpel.resolveBinary('skalpel', []);
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
const dirLine = stderr.split('\n').find((l) => l.includes('$dir ='));
|
|
217
|
-
assert.ok(dirLine, `expected $dir = line, got:\n${stderr}`);
|
|
218
|
-
// The literal ' in the path must be DOUBLED inside the single-quoted PS literal
|
|
219
|
-
assert.ok(
|
|
220
|
-
dirLine.includes("with''apos"),
|
|
221
|
-
`expected '' escape for literal apostrophe, got: ${dirLine}`
|
|
222
|
-
);
|
|
223
|
-
// And the path must still be wrapped in single quotes
|
|
224
|
-
assert.ok(/\$dir = '.*'$/.test(dirLine.trim()), `expected single-quoted: ${dirLine}`);
|
|
225
|
-
} finally {
|
|
226
|
-
fs.rmSync(parent, { recursive: true, force: true });
|
|
227
|
-
}
|
|
228
|
-
});
|
|
229
|
-
|
|
230
|
-
// ── summary
|
|
231
|
-
process.stdout.write(`\n results: ${pass} passed, ${fail} failed\n`);
|
|
232
|
-
if (fail > 0) process.exit(1);
|