specrails-desktop 2.10.1 → 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 +18 -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,
|
|
@@ -77,7 +77,12 @@ function spawnCoreInit(args, cwd) {
|
|
|
77
77
|
// .cmd shim AND quotes each arg, so spaces (and newlines) survive intact.
|
|
78
78
|
return (0, win_spawn_1.spawnCli)(bin, fullArgs, {
|
|
79
79
|
cwd,
|
|
80
|
-
|
|
80
|
+
// Force RELOCATION: specrails-core installs in-repo by DEFAULT now (so a
|
|
81
|
+
// standalone `npx specrails-core init` user's CLI finds the agents in their
|
|
82
|
+
// repo). The desktop manages the cwd (spawns rails with cwd=workspace), so
|
|
83
|
+
// it MUST relocate to keep the user's imported repo pristine — independent of
|
|
84
|
+
// whether the registry mirror already ran. See specrails-core init's gate.
|
|
85
|
+
env: { ...(0, win_spawn_1.windowsSpawnEnv)(), SPECRAILS_RELOCATE: '1' },
|
|
81
86
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
82
87
|
});
|
|
83
88
|
}
|
|
@@ -98,7 +103,9 @@ function spawnBundledCoreInit(args, cwd) {
|
|
|
98
103
|
if (!cli || !coreRoot)
|
|
99
104
|
return null;
|
|
100
105
|
const fullArgs = [cli, 'init', ...args];
|
|
101
|
-
|
|
106
|
+
// Force RELOCATION (core installs in-repo by default; the desktop keeps the
|
|
107
|
+
// user's repo pristine + manages the spawn cwd). See spawnCoreInit.
|
|
108
|
+
const env = { ...(0, win_spawn_1.windowsSpawnEnv)(), SPECRAILS_CORE_SCRIPT_DIR: coreRoot, SPECRAILS_RELOCATE: '1' };
|
|
102
109
|
// Bundled openspec (offline) — the LAST network step of project-add. When the
|
|
103
110
|
// app ships @fission-ai/openspec, point specrails-core's `installOpenSpecProject`
|
|
104
111
|
// at the bundled CLI and the SAME node we run the bundled core with. Tauri
|
|
@@ -118,9 +125,14 @@ function spawnBundledCoreInit(args, cwd) {
|
|
|
118
125
|
// bundled node bin in desktop mode, so PATH `node` is the bundled node too).
|
|
119
126
|
env.SPECRAILS_OPENSPEC_NODE = (0, path_resolver_1.resolveBundledNodeExe)() ?? 'node';
|
|
120
127
|
}
|
|
121
|
-
|
|
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})` +
|
|
122
134
|
(openspecCli ? ' [bundled openspec: offline]' : ' [openspec: npx fallback]'));
|
|
123
|
-
return (0, win_spawn_1.spawnCli)(
|
|
135
|
+
return (0, win_spawn_1.spawnCli)(nodeBin, fullArgs, {
|
|
124
136
|
cwd,
|
|
125
137
|
env,
|
|
126
138
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
@@ -137,7 +149,7 @@ function probeCoreRuntimeVersion(cwd) {
|
|
|
137
149
|
const { bin, fullArgs } = buildCoreArgs(['version']);
|
|
138
150
|
const result = (0, child_process_1.spawnSync)(bin, fullArgs, {
|
|
139
151
|
cwd,
|
|
140
|
-
env:
|
|
152
|
+
env: (0, win_spawn_1.windowsSpawnEnv)(),
|
|
141
153
|
shell: process.platform === 'win32',
|
|
142
154
|
encoding: 'utf-8',
|
|
143
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.
|