specrails-desktop 2.11.0 → 2.11.1
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 +31 -18
- package/server/dist/util/win-spawn.js +27 -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,29 @@ 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");
|
|
16
18
|
const WHICH_CMD = process.platform === 'win32' ? 'where' : 'which';
|
|
19
|
+
/**
|
|
20
|
+
* Run `<cmd> <args>` synchronously. On Windows use cross-spawn (NOT `shell:true`):
|
|
21
|
+
* it spawns a real `.exe` directly (no `cmd.exe` interposition — pkg-safe and
|
|
22
|
+
* immune to the GUI-launch env stripping), and runs `.cmd`/`.bat` shims through
|
|
23
|
+
* `cmd.exe` with correct quoting (Node refuses to spawn `.cmd` without a shell
|
|
24
|
+
* since CVE-2024-27980). On POSIX it is a plain `spawnSync`. `windowsSpawnEnv()`
|
|
25
|
+
* guarantees `cmd.exe` can start (SystemRoot/ComSpec present even if the sidecar
|
|
26
|
+
* inherited a stripped env) — without it every bundled probe failed with a bogus
|
|
27
|
+
* "bundle corrupted — reinstall the app".
|
|
28
|
+
*/
|
|
29
|
+
function runVersionSpawn(cmd, args) {
|
|
30
|
+
const opts = { env: (0, win_spawn_1.windowsSpawnEnv)(), encoding: 'utf-8', timeout: 5_000 };
|
|
31
|
+
if (process.platform === 'win32') {
|
|
32
|
+
return cross_spawn_1.default.sync(cmd, args, opts);
|
|
33
|
+
}
|
|
34
|
+
return (0, child_process_1.spawnSync)(cmd, args, opts);
|
|
35
|
+
}
|
|
17
36
|
exports.MIN_VERSIONS = {
|
|
18
37
|
node: '18.0.0',
|
|
19
38
|
npm: '9.0.0',
|
|
@@ -21,11 +40,10 @@ exports.MIN_VERSIONS = {
|
|
|
21
40
|
uv: '0.1.0',
|
|
22
41
|
};
|
|
23
42
|
function locateCommand(command) {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
});
|
|
43
|
+
// cross-spawn (via runVersionSpawn) resolves the `where`/`which` executable
|
|
44
|
+
// and PATHEXT itself — no `shell:true` needed — and runs under an env that
|
|
45
|
+
// includes SystemRoot so `where` can actually start in the packaged sidecar.
|
|
46
|
+
const result = runVersionSpawn(WHICH_CMD, [command]);
|
|
29
47
|
if (result.error || (result.status ?? 1) !== 0)
|
|
30
48
|
return { found: false };
|
|
31
49
|
const resolvedPath = `${result.stdout ?? ''}`.trim().split(/\r?\n/)[0]?.trim() || undefined;
|
|
@@ -39,19 +57,14 @@ function probeVersion(command, resolvedPath) {
|
|
|
39
57
|
// `Cannot find module '/--version'` from pkg/prelude/bootstrap.js. Passing
|
|
40
58
|
// an absolute path bypasses this interception.
|
|
41
59
|
const target = resolvedPath || command;
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// `
|
|
46
|
-
//
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const result = (
|
|
50
|
-
env: process.env,
|
|
51
|
-
shell: isWin,
|
|
52
|
-
encoding: 'utf-8',
|
|
53
|
-
timeout: 5_000,
|
|
54
|
-
});
|
|
60
|
+
// Spawn via cross-spawn on Windows (NOT shell:true). A bundled `.exe`
|
|
61
|
+
// (node.exe, git.exe) is launched DIRECTLY — no cmd.exe interposition — so a
|
|
62
|
+
// path with spaces needs no manual quoting and pkg's bare-name redirect never
|
|
63
|
+
// applies. A `.cmd` shim (npm.cmd, npx.cmd) is run through cmd.exe by
|
|
64
|
+
// cross-spawn with correct escaping. The env carries SystemRoot so cmd.exe can
|
|
65
|
+
// start. This replaces the old `shell:true` path that made every bundled probe
|
|
66
|
+
// fail with a bogus "corrupted-bundle" when the sidecar env lacked SystemRoot.
|
|
67
|
+
const result = runVersionSpawn(target, ['--version']);
|
|
55
68
|
if (result.error) {
|
|
56
69
|
const err = result.error;
|
|
57
70
|
return { executed: false, error: `${err.code ?? 'ERR'}: ${err.message}` };
|
|
@@ -25,16 +25,41 @@ 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';
|
|
58
|
+
env.SystemRoot = systemRoot;
|
|
59
|
+
env.windir = env.windir || systemRoot;
|
|
60
|
+
env.ComSpec = env.ComSpec || `${systemRoot.replace(/[\\/]$/, '')}\\System32\\cmd.exe`;
|
|
61
|
+
return env;
|
|
62
|
+
}
|
|
38
63
|
// Back-compat for callsites that only need the resolved binary
|
|
39
64
|
// (e.g. logging). Kept as a no-op identity on POSIX; on Windows
|
|
40
65
|
// `where`-based resolution lives inside cross-spawn now.
|