specrails-desktop 2.42.1 → 2.42.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.
@@ -26,7 +26,7 @@ This audit covers the `codex/multi-repo-projects` branch, including mission stee
26
26
 
27
27
  ## Automated gates
28
28
 
29
- `.github/workflows/windows-parity.yml` runs a `windows-parity` job on `windows-latest` (x64) for every push and pull request, and adds `windows-11-arm` on pushes to `main`. It installs dependencies, checks TypeScript, runs release/PTY helper regression tests, native filesystem/process tests (retried once, they drive real processes), the application build, native host build/tests, and four real native fixtures: WebView2 capture/selection, authentication popups, mission window handoff, and browser multiwindow transfer/parking. It is deliberately a separate workflow from `ci.yml`: `release.yml` waits for CI only, so a slow or flaky Windows run never delays the release PR, while Desktop Release still builds and installs on Windows before publication. The jsdom client suite is not repeated on Windows; it runs on Linux in CI. The bundled-core matrix in `ci.yml` exercises Windows junction/copy relocation for providers.
29
+ `.github/workflows/windows-parity.yml` runs a `windows-parity` job on `windows-latest` (x64) for every pull request and push to `main`, adding `windows-11-arm` on the `main` pushes. It installs dependencies, checks TypeScript, runs release/PTY helper regression tests, native filesystem/process tests (retried once, they drive real processes), the application build, native host build/tests, and four real native fixtures: WebView2 capture/selection, authentication popups, mission window handoff, and browser multiwindow transfer/parking. It is deliberately a separate workflow from `ci.yml`: `release.yml` waits for CI only, so a slow or flaky Windows run never delays the release PR, while Desktop Release still builds and installs on Windows before publication. The jsdom client suite is not repeated on Windows; it runs on Linux in CI. The bundled-core matrix in `ci.yml` exercises Windows junction/copy relocation for providers.
30
30
 
31
31
  `.github/workflows/desktop-release.yml` builds the real installers and runs `scripts/smoke-windows-installers.ps1` before publishing artifacts. For each NSIS and MSI package, the script installs into a temporary path containing spaces and drives `scripts/smoke-installed-windows.mjs` with the installed Node runtime. The driver uses an isolated user profile and tests:
32
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "specrails-desktop",
3
- "version": "2.42.1",
3
+ "version": "2.42.2",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -139,7 +139,7 @@ async function gitIgnoredSet(projectPath, relPaths, maxDurationMs = Number.POSIT
139
139
  let stdout = '';
140
140
  let settled = false;
141
141
  let overflow = false;
142
- const child = (0, child_process_1.spawn)('git', ['check-ignore', '--stdin', '-z'], { cwd: projectPath, stdio: ['pipe', 'pipe', 'ignore'], env: file_provenance_1.GIT_EXEC_ENV });
142
+ const child = (0, child_process_1.spawn)('git', ['check-ignore', '--stdin', '-z'], { cwd: projectPath, stdio: ['pipe', 'pipe', 'ignore'], env: (0, file_provenance_1.gitExecEnv)() });
143
143
  const finish = (complete) => { if (!settled) {
144
144
  settled = true;
145
145
  resolve({ stdout, complete: complete && !overflow });
@@ -3,7 +3,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.STORY_EXCERPT_MAX_BYTES = exports.GIT_EXEC_ENV = void 0;
6
+ exports.STORY_EXCERPT_MAX_BYTES = void 0;
7
+ exports.gitExecEnv = gitExecEnv;
7
8
  exports.resolveHeadSha = resolveHeadSha;
8
9
  exports.listUntracked = listUntracked;
9
10
  exports.snapshotWorkingTree = snapshotWorkingTree;
@@ -34,9 +35,13 @@ const GIT_MAX_BUFFER = 16 * 1024 * 1024;
34
35
  // the on-demand diff patches beyond this cap are skipped (the UI shows "diff
35
36
  // unavailable" for them). Mirrors the existing large-job warn threshold (50).
36
37
  const MAX_PATCH_FILES = 50;
37
- // Exported: project-git.ts (the Agent-Mode git bar) runs the same cwd-scoped
38
- // git calls and needs the identical hostile-repo hardening.
39
- exports.GIT_EXEC_ENV = (() => {
38
+ // Exported: project-git.ts (the Agent-Mode git bar), the code explorer and the
39
+ // git diagnostics run the same cwd-scoped git calls and need the identical
40
+ // hostile-repo hardening. Computed PER CALL, never at module load: the startup
41
+ // path resolver prepends the bundled git directory to process.env.PATH after
42
+ // this module was imported, and a snapshot taken earlier could not find git in
43
+ // a packaged app without a system git — every guard then failed closed.
44
+ function gitExecEnv() {
40
45
  // Inherit the parent env but STRIP git-location vars. If the app process (or a
41
46
  // parent) ever exports GIT_DIR / GIT_WORK_TREE / GIT_INDEX_FILE, every cwd-scoped
42
47
  // git call below would silently operate on that repo instead of the project —
@@ -61,7 +66,7 @@ exports.GIT_EXEC_ENV = (() => {
61
66
  env.GIT_CONFIG_NOSYSTEM = '1';
62
67
  env.GIT_CONFIG_PARAMETERS = "'core.fsmonitor=false' 'core.hooksPath=/dev/null' 'protocol.ext.allow=user'";
63
68
  return env;
64
- })();
69
+ }
65
70
  /** Resolve the current HEAD commit SHA. Best-effort: '' on any git failure
66
71
  * (no repo, unborn HEAD, no git, timeout). */
67
72
  function resolveHeadSha(cwd) {
@@ -72,7 +77,7 @@ function resolveHeadSha(cwd) {
72
77
  stdio: ['ignore', 'pipe', 'pipe'],
73
78
  timeout: GIT_TIMEOUT_MS,
74
79
  maxBuffer: GIT_MAX_BUFFER,
75
- env: exports.GIT_EXEC_ENV,
80
+ env: gitExecEnv(),
76
81
  });
77
82
  return out.trim();
78
83
  }
@@ -90,7 +95,7 @@ function listUntracked(cwd) {
90
95
  stdio: ['ignore', 'pipe', 'pipe'],
91
96
  timeout: GIT_TIMEOUT_MS,
92
97
  maxBuffer: GIT_MAX_BUFFER,
93
- env: exports.GIT_EXEC_ENV,
98
+ env: gitExecEnv(),
94
99
  });
95
100
  return out.split('\0').filter((p) => p.length > 0);
96
101
  }
@@ -107,7 +112,7 @@ function snapshotWorkingTree(cwd) {
107
112
  stdio: ['ignore', 'pipe', 'pipe'],
108
113
  timeout: GIT_TIMEOUT_MS,
109
114
  maxBuffer: GIT_MAX_BUFFER,
110
- env: exports.GIT_EXEC_ENV,
115
+ env: gitExecEnv(),
111
116
  });
112
117
  ref = out.trim();
113
118
  }
@@ -141,7 +146,7 @@ function diffAgainstSnapshot(cwd, snapshotRef, untrackedBefore, baseSha) {
141
146
  stdio: ['ignore', 'pipe', 'pipe'],
142
147
  timeout: GIT_TIMEOUT_MS,
143
148
  maxBuffer: GIT_MAX_BUFFER,
144
- env: exports.GIT_EXEC_ENV,
149
+ env: gitExecEnv(),
145
150
  });
146
151
  }
147
152
  catch {
@@ -309,7 +314,7 @@ function collectDiffPatches(cwd, snapshotRef, diff, baseSha) {
309
314
  stdio: ['ignore', 'pipe', 'pipe'],
310
315
  maxBuffer: MAX_PATCH_BYTES + 64 * 1024,
311
316
  timeout: GIT_TIMEOUT_MS,
312
- env: exports.GIT_EXEC_ENV,
317
+ env: gitExecEnv(),
313
318
  });
314
319
  }
315
320
  catch (err) {
@@ -17,7 +17,7 @@ const win_spawn_1 = require("./util/win-spawn");
17
17
  * SAFETY: the command list is a FIXED ALLOWLIST — the caller only picks an
18
18
  * action name, never arguments, so no flag/ref/path can be smuggled in. Every
19
19
  * command is read-only (no push/commit/checkout/pr-create). git runs with the
20
- * hardened `GIT_EXEC_ENV` (hostile-repo config stripped, prompts disabled); gh
20
+ * hardened `gitExecEnv()` (hostile-repo config stripped, prompts disabled); gh
21
21
  * runs with the normal env so it finds the user's `~/.config/gh` auth and the
22
22
  * bundled `gh` on PATH.
23
23
  */
@@ -43,7 +43,7 @@ const OUTPUT_CAP = 8_000;
43
43
  const defaultExec = (cmd, args, cwd) => new Promise((resolve) => {
44
44
  // git: hardened, cwd-pinned env. gh: user env (needs ~/.config/gh auth) with
45
45
  // the Windows shell-critical vars guaranteed present.
46
- const env = cmd === 'gh' ? (0, win_spawn_1.windowsSpawnEnv)() : file_provenance_1.GIT_EXEC_ENV;
46
+ const env = cmd === 'gh' ? (0, win_spawn_1.windowsSpawnEnv)() : (0, file_provenance_1.gitExecEnv)();
47
47
  (0, child_process_1.execFile)(cmd, args, { cwd, env, timeout: 15_000, maxBuffer: 8 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
48
48
  const code = err && typeof err.code === 'number'
49
49
  ? err.code
@@ -35,7 +35,7 @@ function createBoundedExec(timeoutMs = exports.PR_COMMAND_TIMEOUT_MS) {
35
35
  // `windowsSpawnEnv()` backfills SystemRoot / ComSpec / USERPROFILE, which a
36
36
  // GUI-launched / pkg-stripped Windows sidecar can lack — without them the
37
37
  // `git`/`gh` child (and the `sh -c` the `!gh …` credential helper runs) can
38
- // fail to start. NOT `GIT_EXEC_ENV`: that hardened env disables credentials
38
+ // fail to start. NOT `gitExecEnv()`: that hardened env disables credentials
39
39
  // (GIT_ASKPASS=echo / GIT_TERMINAL_PROMPT=0), which the push must keep. No-op
40
40
  // on POSIX (returns process.env), so mac/Linux behaviour is byte-identical.
41
41
  const env = (0, win_spawn_1.windowsSpawnEnv)();
@@ -21,7 +21,7 @@ const GIT_MAX_BUFFER = 4 * 1024 * 1024;
21
21
  const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
22
22
  function git(repoDir, args) {
23
23
  return new Promise((resolve, reject) => {
24
- (0, child_process_1.execFile)('git', args, { cwd: repoDir, env: file_provenance_1.GIT_EXEC_ENV, timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true }, (err, stdout, stderr) => {
24
+ (0, child_process_1.execFile)('git', args, { cwd: repoDir, env: (0, file_provenance_1.gitExecEnv)(), timeout: GIT_TIMEOUT_MS, maxBuffer: GIT_MAX_BUFFER, windowsHide: true }, (err, stdout, stderr) => {
25
25
  if (err) {
26
26
  const detail = String(stderr || err.message).trim();
27
27
  reject(new Error(detail));
@@ -772,7 +772,13 @@ class SetupManager {
772
772
  providers: list,
773
773
  continueOnError: true,
774
774
  onProviderStart: (p) => emit(p, 'running'),
775
- onProviderResult: (r) => emit(r.provider, r.ok ? 'done' : 'failed', r.error),
775
+ onProviderResult: (r) => {
776
+ // The WS event carries the reason to the app; the server log must too,
777
+ // or a packaged-app failure (installer smoke, support bundle) is blind.
778
+ if (!r.ok)
779
+ console.warn(`[SetupManager] silent assemble: ${r.provider} failed for ${projectId}: ${r.error ?? 'unknown error'}`);
780
+ emit(r.provider, r.ok ? 'done' : 'failed', r.error);
781
+ },
776
782
  })
777
783
  .then((results) => {
778
784
  const failed = results.filter((r) => !r.ok).map((r) => r.provider);
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.TerminalManager = exports.TERMINAL_EARLY_EXIT_MS = exports.TerminalSpawnError = exports.TerminalNameInvalidError = exports.TerminalNotFoundError = exports.TerminalLimitExceededError = exports.RingBuffer = exports.TERMINAL_DEFAULT_ROWS = exports.TERMINAL_DEFAULT_COLS = exports.TERMINAL_NAME_MAX = exports.TERMINAL_MAX_PER_PROJECT = exports.TERMINAL_KILL_GRACE_MS = exports.TERMINAL_SCROLLBACK_BYTES = void 0;
7
+ exports.killTerminalTree = killTerminalTree;
7
8
  exports.resolveShell = resolveShell;
8
9
  exports.resolveShellFor = resolveShellFor;
9
10
  exports.shellArgs = shellArgs;
@@ -20,6 +21,21 @@ const terminal_marks_store_1 = require("./terminal-marks-store");
20
21
  const terminal_shell_integration_1 = require("./terminal-shell-integration");
21
22
  exports.TERMINAL_SCROLLBACK_BYTES = 262_144;
22
23
  exports.TERMINAL_KILL_GRACE_MS = 2_000;
24
+ /**
25
+ * Windows: node-pty ends a ConPTY session by enumerating the pseudo console's
26
+ * processes through a helper it starts with `process.execPath`. In the
27
+ * packaged sidecar that path is the sidecar binary itself, so the helper never
28
+ * runs, only the shell is terminated and whatever the shell started (a dev
29
+ * server, a watcher) survives the closed terminal. Kill the shell's process
30
+ * tree ourselves (`taskkill /T /F` through tree-kill). No-op elsewhere: the
31
+ * POSIX pty closes its process group.
32
+ */
33
+ function killTerminalTree(pid, platform = process.platform, kill = win_spawn_1.treeKillSafe) {
34
+ if (platform !== 'win32' || !pid || !Number.isInteger(pid) || pid <= 0)
35
+ return false;
36
+ kill(pid, 'SIGKILL');
37
+ return true;
38
+ }
23
39
  exports.TERMINAL_MAX_PER_PROJECT = 10;
24
40
  exports.TERMINAL_NAME_MAX = 64;
25
41
  exports.TERMINAL_DEFAULT_COLS = 80;
@@ -455,6 +471,7 @@ class TerminalManager {
455
471
  async shutdown() {
456
472
  const all = Array.from(this.sessions.values());
457
473
  for (const s of all) {
474
+ killTerminalTree(s.pty.pid);
458
475
  try {
459
476
  s.pty.kill('SIGTERM');
460
477
  }
@@ -462,6 +479,7 @@ class TerminalManager {
462
479
  }
463
480
  await new Promise((r) => setTimeout(r, exports.TERMINAL_KILL_GRACE_MS));
464
481
  for (const s of Array.from(this.sessions.values())) {
482
+ killTerminalTree(s.pty.pid);
465
483
  try {
466
484
  s.pty.kill('SIGKILL');
467
485
  }
@@ -540,12 +558,14 @@ class TerminalManager {
540
558
  return;
541
559
  this.detachFromRegistry(s);
542
560
  if (!s.exited) {
561
+ killTerminalTree(s.pty.pid);
543
562
  try {
544
563
  s.pty.kill('SIGTERM');
545
564
  }
546
565
  catch { /* ignore */ }
547
566
  s.killTimer = setTimeout(() => {
548
567
  if (!s.exited) {
568
+ killTerminalTree(s.pty.pid);
549
569
  try {
550
570
  s.pty.kill('SIGKILL');
551
571
  }