specrails-desktop 2.11.0 → 2.11.2
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/package.json +1 -1
- package/server/dist/core-update-manager.js +4 -0
- package/server/dist/framework-manager.js +16 -3
- package/server/dist/openspec-shim.js +9 -4
- package/server/dist/setup-manager.js +11 -6
- package/server/dist/setup-prerequisites.js +112 -41
- package/server/dist/util/win-spawn.js +46 -2
package/package.json
CHANGED
|
@@ -10,6 +10,7 @@ const os_1 = __importDefault(require("os"));
|
|
|
10
10
|
const path_1 = __importDefault(require("path"));
|
|
11
11
|
const framework_manager_1 = require("./framework-manager");
|
|
12
12
|
const semver_lite_1 = require("./semver-lite");
|
|
13
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
13
14
|
/**
|
|
14
15
|
* CoreUpdateManager — the voluntary, app-global specrails-core update channel.
|
|
15
16
|
*
|
|
@@ -215,5 +216,8 @@ function defaultNpmInstall(spec, cwd) {
|
|
|
215
216
|
stdio: ['ignore', 'inherit', 'inherit'],
|
|
216
217
|
timeout: INSTALL_TIMEOUT_MS,
|
|
217
218
|
shell: process.platform === 'win32',
|
|
219
|
+
// SystemRoot/ComSpec so npm.cmd's cmd.exe can start even if the packaged
|
|
220
|
+
// sidecar inherited a stripped Windows environment.
|
|
221
|
+
env: (0, win_spawn_1.windowsSpawnEnv)(),
|
|
218
222
|
});
|
|
219
223
|
}
|
|
@@ -13,6 +13,19 @@ const bundled_core_1 = require("./bundled-core");
|
|
|
13
13
|
const artifact_registry_1 = require("./artifact-registry");
|
|
14
14
|
Object.defineProperty(exports, "atomicWrite", { enumerable: true, get: function () { return artifact_registry_1.atomicWrite; } });
|
|
15
15
|
const semver_lite_1 = require("./semver-lite");
|
|
16
|
+
const path_resolver_1 = require("./path-resolver");
|
|
17
|
+
/**
|
|
18
|
+
* The node interpreter to run the core CLI with. In the PACKAGED app
|
|
19
|
+
* `process.execPath` is the `specrails-server` pkg binary — NOT node — so
|
|
20
|
+
* `spawnSync(process.execPath, [cli, …])` would re-launch the server instead of
|
|
21
|
+
* running `cli.js`, silently breaking every framework materialize/swap/assemble.
|
|
22
|
+
* Use the bundled real node when present (it always is when a bundled core is —
|
|
23
|
+
* they ship together), and fall back to `process.execPath` only in dev/tests
|
|
24
|
+
* where `process.execPath` IS node.
|
|
25
|
+
*/
|
|
26
|
+
function nodeInterpreter() {
|
|
27
|
+
return (0, path_resolver_1.resolveBundledNodeExe)() ?? process.execPath;
|
|
28
|
+
}
|
|
16
29
|
/** `~/.specrails/framework` — same home as the registry. */
|
|
17
30
|
function frameworkRoot(home) {
|
|
18
31
|
return path_1.default.join((0, artifact_registry_1.resolveHome)(home), '.specrails', 'framework');
|
|
@@ -114,7 +127,7 @@ class FrameworkManager {
|
|
|
114
127
|
const errors = [];
|
|
115
128
|
const done = [];
|
|
116
129
|
for (const provider of dedupe(providers)) {
|
|
117
|
-
const res = (0, child_process_1.spawnSync)(
|
|
130
|
+
const res = (0, child_process_1.spawnSync)(nodeInterpreter(), [
|
|
118
131
|
cli,
|
|
119
132
|
'install-framework',
|
|
120
133
|
'--framework-dir',
|
|
@@ -153,7 +166,7 @@ class FrameworkManager {
|
|
|
153
166
|
if (readCurrentFrameworkVersion(this.home) === version)
|
|
154
167
|
return true;
|
|
155
168
|
const fwDir = frameworkRoot(this.home);
|
|
156
|
-
const res = (0, child_process_1.spawnSync)(
|
|
169
|
+
const res = (0, child_process_1.spawnSync)(nodeInterpreter(), [cli, 'swap-current', '--framework-dir', fwDir, '--version', version], { env: this.childEnv(), encoding: 'utf-8', timeout: 120_000 });
|
|
157
170
|
if (res.error || (res.status ?? 1) !== 0)
|
|
158
171
|
return false;
|
|
159
172
|
return readCurrentFrameworkVersion(this.home) === version;
|
|
@@ -245,7 +258,7 @@ class FrameworkManager {
|
|
|
245
258
|
if (!ver)
|
|
246
259
|
return { ran: false };
|
|
247
260
|
const fwDir = frameworkRoot(this.home);
|
|
248
|
-
const res = (0, child_process_1.spawnSync)(
|
|
261
|
+
const res = (0, child_process_1.spawnSync)(nodeInterpreter(), [
|
|
249
262
|
cli,
|
|
250
263
|
'assemble',
|
|
251
264
|
'--workspace',
|
|
@@ -12,6 +12,7 @@ const fs_1 = __importDefault(require("fs"));
|
|
|
12
12
|
const os_1 = __importDefault(require("os"));
|
|
13
13
|
const path_1 = __importDefault(require("path"));
|
|
14
14
|
const bundled_openspec_1 = require("./bundled-openspec");
|
|
15
|
+
const path_resolver_1 = require("./path-resolver");
|
|
15
16
|
/**
|
|
16
17
|
* openspec PATH shim — the robustness backstop for relocated Claude rails.
|
|
17
18
|
*
|
|
@@ -54,11 +55,15 @@ function openspecShimDir(slug, jobId, home = os_1.default.homedir()) {
|
|
|
54
55
|
function realOpenspecInvocation() {
|
|
55
56
|
const cli = (0, bundled_openspec_1.getBundledOpenspecCli)();
|
|
56
57
|
if (cli) {
|
|
57
|
-
// node "<cli>" — invoke the bundled openspec as a node script (Tauri strips
|
|
58
|
-
// exec bits from bundled resources, so it can't be run as a binary).
|
|
58
|
+
// <node> "<cli>" — invoke the bundled openspec as a node script (Tauri strips
|
|
59
|
+
// exec bits from bundled resources, so it can't be run as a binary). Anchor to
|
|
60
|
+
// the absolute bundled node when present rather than a bare `node` (which is
|
|
61
|
+
// PATH-dependent and may be absent / a mismatched system node in a partial
|
|
62
|
+
// bundle); fall back to bare `node` only when no bundled runtimes ship.
|
|
63
|
+
const node = (0, path_resolver_1.resolveBundledNodeExe)();
|
|
59
64
|
return {
|
|
60
|
-
posix:
|
|
61
|
-
windows:
|
|
65
|
+
posix: `${node ? shq(node) : 'node'} ${shq(cli)}`,
|
|
66
|
+
windows: `${node ? winq(node) : 'node'} ${winq(cli)}`,
|
|
62
67
|
};
|
|
63
68
|
}
|
|
64
69
|
// Legacy fallback: npx resolves + caches openspec. `-y` skips the install
|
|
@@ -44,7 +44,7 @@ function resolveCoreBinary(bin) {
|
|
|
44
44
|
if (bin.includes('/') || bin.includes('\\'))
|
|
45
45
|
return (0, path_1.resolve)(bin);
|
|
46
46
|
const result = (0, child_process_1.spawnSync)(WHICH_CMD, [bin], {
|
|
47
|
-
env:
|
|
47
|
+
env: (0, win_spawn_1.windowsSpawnEnv)(),
|
|
48
48
|
shell: process.platform === 'win32',
|
|
49
49
|
encoding: 'utf-8',
|
|
50
50
|
timeout: 5_000,
|
|
@@ -82,7 +82,7 @@ function spawnCoreInit(args, cwd) {
|
|
|
82
82
|
// repo). The desktop manages the cwd (spawns rails with cwd=workspace), so
|
|
83
83
|
// it MUST relocate to keep the user's imported repo pristine — independent of
|
|
84
84
|
// whether the registry mirror already ran. See specrails-core init's gate.
|
|
85
|
-
env: { ...
|
|
85
|
+
env: { ...(0, win_spawn_1.windowsSpawnEnv)(), SPECRAILS_RELOCATE: '1' },
|
|
86
86
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
87
87
|
});
|
|
88
88
|
}
|
|
@@ -105,7 +105,7 @@ function spawnBundledCoreInit(args, cwd) {
|
|
|
105
105
|
const fullArgs = [cli, 'init', ...args];
|
|
106
106
|
// Force RELOCATION (core installs in-repo by default; the desktop keeps the
|
|
107
107
|
// user's repo pristine + manages the spawn cwd). See spawnCoreInit.
|
|
108
|
-
const env = { ...
|
|
108
|
+
const env = { ...(0, win_spawn_1.windowsSpawnEnv)(), SPECRAILS_CORE_SCRIPT_DIR: coreRoot, SPECRAILS_RELOCATE: '1' };
|
|
109
109
|
// Bundled openspec (offline) — the LAST network step of project-add. When the
|
|
110
110
|
// app ships @fission-ai/openspec, point specrails-core's `installOpenSpecProject`
|
|
111
111
|
// at the bundled CLI and the SAME node we run the bundled core with. Tauri
|
|
@@ -125,9 +125,14 @@ function spawnBundledCoreInit(args, cwd) {
|
|
|
125
125
|
// bundled node bin in desktop mode, so PATH `node` is the bundled node too).
|
|
126
126
|
env.SPECRAILS_OPENSPEC_NODE = (0, path_resolver_1.resolveBundledNodeExe)() ?? 'node';
|
|
127
127
|
}
|
|
128
|
-
|
|
128
|
+
// Run the core CLI with the REAL bundled node — NOT process.execPath, which in
|
|
129
|
+
// the packaged app is the `specrails-server` pkg binary and would re-launch the
|
|
130
|
+
// server instead of running cli.js (same trap as SPECRAILS_OPENSPEC_NODE above).
|
|
131
|
+
// Fall back to process.execPath only in dev/tests where it IS node.
|
|
132
|
+
const nodeBin = (0, path_resolver_1.resolveBundledNodeExe)() ?? process.execPath;
|
|
133
|
+
console.log(`[SetupManager] spawning BUNDLED core: ${nodeBin} ${fullArgs.join(' ')} (cwd=${cwd})` +
|
|
129
134
|
(openspecCli ? ' [bundled openspec: offline]' : ' [openspec: npx fallback]'));
|
|
130
|
-
return (0, win_spawn_1.spawnCli)(
|
|
135
|
+
return (0, win_spawn_1.spawnCli)(nodeBin, fullArgs, {
|
|
131
136
|
cwd,
|
|
132
137
|
env,
|
|
133
138
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -144,7 +149,7 @@ function probeCoreRuntimeVersion(cwd) {
|
|
|
144
149
|
const { bin, fullArgs } = buildCoreArgs(['version']);
|
|
145
150
|
const result = (0, child_process_1.spawnSync)(bin, fullArgs, {
|
|
146
151
|
cwd,
|
|
147
|
-
env:
|
|
152
|
+
env: (0, win_spawn_1.windowsSpawnEnv)(),
|
|
148
153
|
shell: process.platform === 'win32',
|
|
149
154
|
encoding: 'utf-8',
|
|
150
155
|
timeout: CORE_PROBE_TIMEOUT_MS,
|
|
@@ -10,10 +10,31 @@ exports.getSetupPrerequisitesStatus = getSetupPrerequisitesStatus;
|
|
|
10
10
|
exports.__resetSetupPrerequisitesCacheForTest = __resetSetupPrerequisitesCacheForTest;
|
|
11
11
|
exports.formatMissingSetupPrerequisites = formatMissingSetupPrerequisites;
|
|
12
12
|
const child_process_1 = require("child_process");
|
|
13
|
+
const cross_spawn_1 = __importDefault(require("cross-spawn"));
|
|
13
14
|
const fs_1 = __importDefault(require("fs"));
|
|
14
15
|
const path_1 = __importDefault(require("path"));
|
|
15
16
|
const providers_1 = require("./providers");
|
|
17
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
18
|
+
const bundled_core_1 = require("./bundled-core");
|
|
19
|
+
const bundled_openspec_1 = require("./bundled-openspec");
|
|
16
20
|
const WHICH_CMD = process.platform === 'win32' ? 'where' : 'which';
|
|
21
|
+
/**
|
|
22
|
+
* Run `<cmd> <args>` synchronously. On Windows use cross-spawn (NOT `shell:true`):
|
|
23
|
+
* it spawns a real `.exe` directly (no `cmd.exe` interposition — pkg-safe and
|
|
24
|
+
* immune to the GUI-launch env stripping), and runs `.cmd`/`.bat` shims through
|
|
25
|
+
* `cmd.exe` with correct quoting (Node refuses to spawn `.cmd` without a shell
|
|
26
|
+
* since CVE-2024-27980). On POSIX it is a plain `spawnSync`. `windowsSpawnEnv()`
|
|
27
|
+
* guarantees `cmd.exe` can start (SystemRoot/ComSpec present even if the sidecar
|
|
28
|
+
* inherited a stripped env) — without it every bundled probe failed with a bogus
|
|
29
|
+
* "bundle corrupted — reinstall the app".
|
|
30
|
+
*/
|
|
31
|
+
function runVersionSpawn(cmd, args) {
|
|
32
|
+
const opts = { env: (0, win_spawn_1.windowsSpawnEnv)(), encoding: 'utf-8', timeout: 5_000 };
|
|
33
|
+
if (process.platform === 'win32') {
|
|
34
|
+
return cross_spawn_1.default.sync(cmd, args, opts);
|
|
35
|
+
}
|
|
36
|
+
return (0, child_process_1.spawnSync)(cmd, args, opts);
|
|
37
|
+
}
|
|
17
38
|
exports.MIN_VERSIONS = {
|
|
18
39
|
node: '18.0.0',
|
|
19
40
|
npm: '9.0.0',
|
|
@@ -21,37 +42,18 @@ exports.MIN_VERSIONS = {
|
|
|
21
42
|
uv: '0.1.0',
|
|
22
43
|
};
|
|
23
44
|
function locateCommand(command) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
45
|
+
// cross-spawn (via runVersionSpawn) resolves the `where`/`which` executable
|
|
46
|
+
// and PATHEXT itself — no `shell:true` needed — and runs under an env that
|
|
47
|
+
// includes SystemRoot so `where` can actually start in the packaged sidecar.
|
|
48
|
+
const result = runVersionSpawn(WHICH_CMD, [command]);
|
|
29
49
|
if (result.error || (result.status ?? 1) !== 0)
|
|
30
50
|
return { found: false };
|
|
31
51
|
const resolvedPath = `${result.stdout ?? ''}`.trim().split(/\r?\n/)[0]?.trim() || undefined;
|
|
32
52
|
return { found: true, resolvedPath };
|
|
33
53
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
// intercepted by pkg's child_process patch and redirected to `process.execPath`
|
|
38
|
-
// (the server binary itself) — the child then crashes with
|
|
39
|
-
// `Cannot find module '/--version'` from pkg/prelude/bootstrap.js. Passing
|
|
40
|
-
// an absolute path bypasses this interception.
|
|
41
|
-
const target = resolvedPath || command;
|
|
42
|
-
// On Windows we must use `shell: true` to execute `.cmd` shims (npm, npx) — Node
|
|
43
|
-
// refuses to spawn them directly since CVE-2024-27980. But cmd.exe splits the
|
|
44
|
-
// command line on whitespace, so an absolute path like
|
|
45
|
-
// `C:\Program Files\Git\cmd\git.exe` becomes `C:\Program` + arg `Files\Git\...`.
|
|
46
|
-
// Wrap the target in double quotes when it contains a space.
|
|
47
|
-
const isWin = process.platform === 'win32';
|
|
48
|
-
const quotedTarget = isWin && /\s/.test(target) ? `"${target}"` : target;
|
|
49
|
-
const result = (0, child_process_1.spawnSync)(quotedTarget, ['--version'], {
|
|
50
|
-
env: process.env,
|
|
51
|
-
shell: isWin,
|
|
52
|
-
encoding: 'utf-8',
|
|
53
|
-
timeout: 5_000,
|
|
54
|
-
});
|
|
54
|
+
/** Run `<bin> <args>` and interpret the result as a `--version` probe. */
|
|
55
|
+
function runProbe(bin, args) {
|
|
56
|
+
const result = runVersionSpawn(bin, args);
|
|
55
57
|
if (result.error) {
|
|
56
58
|
const err = result.error;
|
|
57
59
|
return { executed: false, error: `${err.code ?? 'ERR'}: ${err.message}` };
|
|
@@ -65,6 +67,23 @@ function probeVersion(command, resolvedPath) {
|
|
|
65
67
|
const version = output.split(/\r?\n/)[0]?.trim() || undefined;
|
|
66
68
|
return { executed: true, version };
|
|
67
69
|
}
|
|
70
|
+
function probeVersion(command, resolvedPath) {
|
|
71
|
+
// IMPORTANT: prefer the absolute path returned by `which`. When the server is
|
|
72
|
+
// bundled with `pkg`, calling spawn with the bare command name `'node'` is
|
|
73
|
+
// intercepted by pkg's child_process patch and redirected to `process.execPath`
|
|
74
|
+
// (the server binary itself) — the child then crashes with
|
|
75
|
+
// `Cannot find module '/--version'` from pkg/prelude/bootstrap.js. Passing
|
|
76
|
+
// an absolute path bypasses this interception.
|
|
77
|
+
const target = resolvedPath || command;
|
|
78
|
+
// Spawn via cross-spawn on Windows (NOT shell:true). A bundled `.exe`
|
|
79
|
+
// (node.exe, git.exe) is launched DIRECTLY — no cmd.exe interposition — so a
|
|
80
|
+
// path with spaces needs no manual quoting and pkg's bare-name redirect never
|
|
81
|
+
// applies. A `.cmd` shim (npm.cmd, npx.cmd) is run through cmd.exe by
|
|
82
|
+
// cross-spawn with correct escaping. The env carries SystemRoot so cmd.exe can
|
|
83
|
+
// start. This replaces the old `shell:true` path that made every bundled probe
|
|
84
|
+
// fail with a bogus "corrupted-bundle" when the sidecar env lacked SystemRoot.
|
|
85
|
+
return runProbe(target, ['--version']);
|
|
86
|
+
}
|
|
68
87
|
/** Extracts the first `major.minor.patch` triple from a version string.
|
|
69
88
|
* Tolerates prefixes like `v`, `git version `, `node `. */
|
|
70
89
|
function parseSemver(raw) {
|
|
@@ -131,6 +150,41 @@ function fileExists(p) {
|
|
|
131
150
|
return false;
|
|
132
151
|
}
|
|
133
152
|
}
|
|
153
|
+
/** The bundled REAL node executable inside the runtimes tree, or null. */
|
|
154
|
+
function bundledNodeExe(runtimesBase) {
|
|
155
|
+
return getBundledToolCandidates(runtimesBase, 'node').find(fileExists) ?? null;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Absolute path to the bundled npm/npx CLI JS (`npm-cli.js` / `npx-cli.js`), or
|
|
159
|
+
* null. The npm package ships INSIDE the node distribution: at
|
|
160
|
+
* `node/node_modules/npm/bin/` on Windows and `node/lib/node_modules/npm/bin/`
|
|
161
|
+
* on POSIX. Both `npm` and `npx` are served by the npm package (npx-cli.js lives
|
|
162
|
+
* there too).
|
|
163
|
+
*/
|
|
164
|
+
function bundledNpmCliJs(runtimesBase, tool) {
|
|
165
|
+
const file = tool === 'npx' ? 'npx-cli.js' : 'npm-cli.js';
|
|
166
|
+
const candidates = process.platform === 'win32'
|
|
167
|
+
? [path_1.default.join(runtimesBase, 'node', 'node_modules', 'npm', 'bin', file)]
|
|
168
|
+
: [path_1.default.join(runtimesBase, 'node', 'lib', 'node_modules', 'npm', 'bin', file)];
|
|
169
|
+
return candidates.find(fileExists) ?? null;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Probe a bundled npm/npx by running the bundled node DIRECTLY against the npm
|
|
173
|
+
* package's CLI JS, instead of executing the `.cmd`/wrapper shim. The shims route
|
|
174
|
+
* through `cmd.exe` (Windows) / a wrapper script and proved fragile in the
|
|
175
|
+
* packaged app (a stripped sidecar env / cmd.exe quirks made `npm.cmd --version`
|
|
176
|
+
* fail with a bogus "corrupted-bundle" even though node.exe itself runs fine).
|
|
177
|
+
* Running `node npm-cli.js --version` is deterministic, shell-free and pkg-safe.
|
|
178
|
+
* Returns null when the bundled node or the CLI JS isn't present (caller falls
|
|
179
|
+
* back to probing the shim, then to the system tool).
|
|
180
|
+
*/
|
|
181
|
+
function probeBundledNpmLike(runtimesBase, tool) {
|
|
182
|
+
const nodeExe = bundledNodeExe(runtimesBase);
|
|
183
|
+
const cliJs = bundledNpmCliJs(runtimesBase, tool);
|
|
184
|
+
if (!nodeExe || !cliJs)
|
|
185
|
+
return null;
|
|
186
|
+
return { probe: runProbe(nodeExe, [cliJs, '--version']), resolvedPath: cliJs };
|
|
187
|
+
}
|
|
134
188
|
// Short-lived memo: probing involves up to ~12 synchronous spawnSync calls that
|
|
135
189
|
// block the single Express event loop. The endpoint only fires from low-freq
|
|
136
190
|
// setup flows, so a 30s TTL eliminates repeated probe storms (cold-cache
|
|
@@ -154,6 +208,13 @@ function __resetSetupPrerequisitesCacheForTest() {
|
|
|
154
208
|
function computeSetupPrerequisitesStatus(options = {}) {
|
|
155
209
|
const isDesktop = process.env.SPECRAILS_IS_DESKTOP === '1';
|
|
156
210
|
const runtimesBase = process.env.SPECRAILS_BUNDLED_RUNTIMES_PATH ?? '';
|
|
211
|
+
// When the app ships a bundled core AND bundled openspec, project-add is fully
|
|
212
|
+
// OFFLINE: it runs `node cli.js` / `node openspec.js` directly and NEVER spawns
|
|
213
|
+
// npm or npx. So npm/npx are advisory (not required) in that mode — a broken or
|
|
214
|
+
// unprobeable npm/npx must NOT dead-end Add Project for tools the install flow
|
|
215
|
+
// never invokes. Outside the bundled-offline path (dev / no bundle) npx IS used
|
|
216
|
+
// (npx specrails-core), so npm/npx stay required there.
|
|
217
|
+
const bundledOfflineSetup = isDesktop && (0, bundled_core_1.getBundledCoreCli)() !== null && (0, bundled_openspec_1.getBundledOpenspecCli)() !== null;
|
|
157
218
|
const platform = process.platform === 'darwin'
|
|
158
219
|
? 'darwin'
|
|
159
220
|
: process.platform === 'win32'
|
|
@@ -177,7 +238,7 @@ function computeSetupPrerequisitesStatus(options = {}) {
|
|
|
177
238
|
kind: 'tool',
|
|
178
239
|
label: 'npm',
|
|
179
240
|
command: 'npm',
|
|
180
|
-
required:
|
|
241
|
+
required: !bundledOfflineSetup,
|
|
181
242
|
minVersion: exports.MIN_VERSIONS.npm,
|
|
182
243
|
installUrl: 'https://nodejs.org/en/download',
|
|
183
244
|
installHint: 'npm ships with Node.js LTS.',
|
|
@@ -187,7 +248,7 @@ function computeSetupPrerequisitesStatus(options = {}) {
|
|
|
187
248
|
kind: 'tool',
|
|
188
249
|
label: 'npx',
|
|
189
250
|
command: 'npx',
|
|
190
|
-
required:
|
|
251
|
+
required: !bundledOfflineSetup,
|
|
191
252
|
installUrl: 'https://nodejs.org/en/download',
|
|
192
253
|
installHint: 'npx ships with npm and is required to run specrails-core.',
|
|
193
254
|
},
|
|
@@ -241,17 +302,27 @@ function computeSetupPrerequisitesStatus(options = {}) {
|
|
|
241
302
|
// Desktop mode: probe bundled absolute paths for node/npm/npx/git.
|
|
242
303
|
// Provider CLIs (claude, codex) are always probed via system PATH regardless of mode.
|
|
243
304
|
if (isDesktop && definition.kind === 'tool' && isBundledTool(definition.key)) {
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
//
|
|
305
|
+
const tool = definition.key;
|
|
306
|
+
// npm/npx: probe by running the bundled node DIRECTLY against the npm
|
|
307
|
+
// package's CLI JS — robust where the `.cmd`/wrapper shim proved fragile in
|
|
308
|
+
// the packaged app (cmd.exe / stripped-env quirks made `npm.cmd --version`
|
|
309
|
+
// fail with a bogus "corrupted-bundle" even though node.exe runs fine).
|
|
310
|
+
let effective = tool === 'npm' || tool === 'npx' ? probeBundledNpmLike(runtimesBase, tool) : null;
|
|
311
|
+
// Otherwise (node/git, or npm/npx whose CLI JS wasn't found) probe the
|
|
312
|
+
// bundled binary/shim directly. Only treat as bundled when the FILE exists;
|
|
313
|
+
// a missing file means this build shipped no runtimes for this platform/arch
|
|
249
314
|
// — fall through to the system probe so a system-installed tool still
|
|
250
|
-
// satisfies the requirement
|
|
251
|
-
//
|
|
252
|
-
//
|
|
253
|
-
if (
|
|
254
|
-
const
|
|
315
|
+
// satisfies the requirement instead of dead-ending Add Project with a futile
|
|
316
|
+
// "reinstall the app". 'corrupted-bundle' is reserved for file-EXISTS-but-
|
|
317
|
+
// fails-its-probe.
|
|
318
|
+
if (!effective) {
|
|
319
|
+
const bundledPath = getBundledToolCandidates(runtimesBase, tool).find(fileExists);
|
|
320
|
+
if (bundledPath) {
|
|
321
|
+
effective = { probe: probeVersion(tool, bundledPath), resolvedPath: bundledPath };
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
if (effective) {
|
|
325
|
+
const { probe, resolvedPath } = effective;
|
|
255
326
|
if (!probe.executed) {
|
|
256
327
|
return {
|
|
257
328
|
...definition,
|
|
@@ -259,7 +330,7 @@ function computeSetupPrerequisitesStatus(options = {}) {
|
|
|
259
330
|
executable: false,
|
|
260
331
|
bundled: true,
|
|
261
332
|
error: 'corrupted-bundle',
|
|
262
|
-
resolvedPath
|
|
333
|
+
resolvedPath,
|
|
263
334
|
executionError: probe.error,
|
|
264
335
|
meetsMinimum: false,
|
|
265
336
|
installHint: 'Bundle corrupted — reinstall the Specrails app.',
|
|
@@ -271,12 +342,12 @@ function computeSetupPrerequisitesStatus(options = {}) {
|
|
|
271
342
|
executable: true,
|
|
272
343
|
bundled: true,
|
|
273
344
|
version: probe.version,
|
|
274
|
-
resolvedPath
|
|
345
|
+
resolvedPath,
|
|
275
346
|
meetsMinimum: meetsMinimumVersion(probe.version, definition.minVersion),
|
|
276
347
|
installHint: '',
|
|
277
348
|
};
|
|
278
349
|
}
|
|
279
|
-
//
|
|
350
|
+
// No bundled binary/CLI found → fall through to the system probe below.
|
|
280
351
|
}
|
|
281
352
|
// Non-desktop (or provider CLI in any mode, or desktop with bundle absent): system probe.
|
|
282
353
|
const lookup = locateCommand(definition.command);
|
|
@@ -25,16 +25,60 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
25
25
|
};
|
|
26
26
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
27
|
exports.spawnCli = spawnCli;
|
|
28
|
+
exports.windowsSpawnEnv = windowsSpawnEnv;
|
|
28
29
|
exports.resolveWindowsBinary = resolveWindowsBinary;
|
|
29
30
|
const child_process_1 = require("child_process");
|
|
30
31
|
const cross_spawn_1 = __importDefault(require("cross-spawn"));
|
|
31
32
|
function spawnCli(binary, args, options = {}) {
|
|
32
|
-
/* c8 ignore next
|
|
33
|
+
/* c8 ignore next 5 -- Windows-only branch; coverage runs on Linux/macOS */
|
|
33
34
|
if (process.platform === 'win32') {
|
|
34
|
-
|
|
35
|
+
// Guarantee SystemRoot/ComSpec so cmd.exe (which cross-spawn uses to run
|
|
36
|
+
// `.cmd` shims like claude.cmd/npm.cmd) can start even when the packaged
|
|
37
|
+
// sidecar inherited a stripped environment. Chokepoint for EVERY Windows
|
|
38
|
+
// spawn — protects rails, chat, setup and probes uniformly.
|
|
39
|
+
return (0, cross_spawn_1.default)(binary, args, { ...options, env: windowsSpawnEnv(options.env) });
|
|
35
40
|
}
|
|
36
41
|
return (0, child_process_1.spawn)(binary, args, options);
|
|
37
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Return an environment safe for spawning children on Windows. The desktop
|
|
45
|
+
* server runs as a pkg-packaged sidecar launched by the Tauri host, and that
|
|
46
|
+
* spawn can deliver a STRIPPED environment missing `SystemRoot` / `windir` /
|
|
47
|
+
* `ComSpec`. Without `SystemRoot`, `cmd.exe` (used to run `.cmd` shims like
|
|
48
|
+
* `npm.cmd`/`npx.cmd`, and by cross-spawn) fails to initialise — which silently
|
|
49
|
+
* broke every bundled-tool `--version` probe and any npx/npm spawn during setup.
|
|
50
|
+
* Reconstructs the canonical values when absent. No-op (returns `base`
|
|
51
|
+
* unchanged) on POSIX. Pass a base env to layer extra vars on top.
|
|
52
|
+
*/
|
|
53
|
+
function windowsSpawnEnv(base = process.env) {
|
|
54
|
+
if (process.platform !== 'win32')
|
|
55
|
+
return base;
|
|
56
|
+
const env = { ...base };
|
|
57
|
+
const systemRoot = (env.SystemRoot || env.windir || 'C:\\Windows').replace(/[\\/]$/, '');
|
|
58
|
+
env.SystemRoot = env.SystemRoot || systemRoot;
|
|
59
|
+
env.windir = env.windir || systemRoot;
|
|
60
|
+
env.ComSpec = env.ComSpec || `${systemRoot}\\System32\\cmd.exe`;
|
|
61
|
+
// npm/npx load @npmcli/config on startup (even `npm --version` and the inner
|
|
62
|
+
// npm-prefix.js resolver), which reads the user profile via USERPROFILE /
|
|
63
|
+
// APPDATA / HOMEDRIVE+HOMEPATH / TEMP. A GUI-launched, env-stripped sidecar can
|
|
64
|
+
// lack these → `node npm-cli.js --version` fails the same way the .cmd shim did.
|
|
65
|
+
// Backfill canonical Windows defaults when absent (node.exe itself needs none
|
|
66
|
+
// of these, which is why node/git probed fine while npm/npx did not).
|
|
67
|
+
const userProfile = env.USERPROFILE ||
|
|
68
|
+
(env.HOMEDRIVE && env.HOMEPATH ? `${env.HOMEDRIVE}${env.HOMEPATH}` : 'C:\\Users\\Default');
|
|
69
|
+
env.USERPROFILE = userProfile;
|
|
70
|
+
const drive = /^([A-Za-z]:)(.*)$/.exec(userProfile);
|
|
71
|
+
if (drive) {
|
|
72
|
+
env.HOMEDRIVE = env.HOMEDRIVE || drive[1];
|
|
73
|
+
env.HOMEPATH = env.HOMEPATH || (drive[2] || '\\');
|
|
74
|
+
}
|
|
75
|
+
env.APPDATA = env.APPDATA || `${userProfile}\\AppData\\Roaming`;
|
|
76
|
+
env.LOCALAPPDATA = env.LOCALAPPDATA || `${userProfile}\\AppData\\Local`;
|
|
77
|
+
const temp = env.TEMP || env.TMP || `${env.LOCALAPPDATA}\\Temp`;
|
|
78
|
+
env.TEMP = env.TEMP || temp;
|
|
79
|
+
env.TMP = env.TMP || temp;
|
|
80
|
+
return env;
|
|
81
|
+
}
|
|
38
82
|
// Back-compat for callsites that only need the resolved binary
|
|
39
83
|
// (e.g. logging). Kept as a no-op identity on POSIX; on Windows
|
|
40
84
|
// `where`-based resolution lives inside cross-spawn now.
|