specrails-desktop 2.24.0 → 2.24.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/claude-trust.js +32 -25
- package/server/dist/integration-branch.js +80 -0
- package/server/dist/pr-publisher.js +40 -2
- package/server/dist/rail-isolated-launch.js +26 -1
- package/server/dist/vitest-setup.js +28 -0
- package/server/dist/worktree-manager.js +5 -1
package/package.json
CHANGED
|
@@ -36,26 +36,35 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.claudeConfigPath = claudeConfigPath;
|
|
37
37
|
exports.markProjectsTrusted = markProjectsTrusted;
|
|
38
38
|
exports.ensureClaudeTrusted = ensureClaudeTrusted;
|
|
39
|
-
exports.__resetClaudeTrustMemoForTest = __resetClaudeTrustMemoForTest;
|
|
40
39
|
/**
|
|
41
40
|
* Pre-trust specrails-managed spawn directories in the user's `~/.claude.json`
|
|
42
|
-
* so a HEADLESS claude rail/job spawn
|
|
43
|
-
* `permissions.allow` list the framework overlay places in the worktree.
|
|
44
|
-
*
|
|
45
|
-
* Why: claude ignores a workspace's `permissions.allow` entries until that
|
|
46
|
-
* project directory has been "trusted" (the interactive trust dialog, or
|
|
47
|
-
* `projects[<dir>].hasTrustDialogAccepted: true` in `~/.claude.json`). specrails
|
|
48
|
-
* spawns claude headlessly in FRESH per-run worktrees / workspaces that were
|
|
49
|
-
* never opened interactively, so every isolated rail run logged
|
|
41
|
+
* so a HEADLESS claude rail/job spawn does NOT log the noisy
|
|
50
42
|
* "Ignoring N permissions.allow entries … this workspace has not been trusted"
|
|
51
|
-
*
|
|
52
|
-
*
|
|
43
|
+
* warning.
|
|
44
|
+
*
|
|
45
|
+
* IMPORTANT — this is cosmetic, not load-bearing. Every claude spawn already
|
|
46
|
+
* carries `--dangerously-skip-permissions` (see `claude-adapter.ts` COMMON_FLAGS),
|
|
47
|
+
* which bypasses the permission engine entirely — so a workspace's
|
|
48
|
+
* `.claude/settings.json` `permissions.allow` list is moot on these spawns
|
|
49
|
+
* whether the dir is trusted or not. Marking the dir trusted only silences the
|
|
50
|
+
* warning. (It WOULD become functionally load-bearing if we ever dropped
|
|
51
|
+
* `--dangerously-skip-permissions` and relied on the allow-list.)
|
|
52
|
+
*
|
|
53
|
+
* Why re-assert on EVERY spawn (no persistent memo): `~/.claude.json` is a
|
|
54
|
+
* single ~200KB file holding ALL projects that many concurrent claude processes
|
|
55
|
+
* rewrite wholesale (each carries its own in-memory snapshot). Our surgical
|
|
56
|
+
* `true` for one dir is routinely clobbered by another process writing the whole
|
|
57
|
+
* file back with a stale `false` — a lost update. A one-shot per-process memo
|
|
58
|
+
* (the old design) therefore left the flag stuck `false` for the rest of the
|
|
59
|
+
* process lifetime after the first clobber. Re-asserting immediately before each
|
|
60
|
+
* spawn re-flips a clobbered `false → true` so this spawn reads `true` at
|
|
61
|
+
* startup. It cannot fully win the race against concurrent whole-file writers,
|
|
62
|
+
* but the failure mode is only a cosmetic warning, so best-effort is enough.
|
|
53
63
|
*
|
|
54
64
|
* Surgical + best-effort: read → set only `hasTrustDialogAccepted` on the
|
|
55
65
|
* relevant `projects[<realpath>]` keys → atomic temp+rename. Never throws, never
|
|
56
66
|
* touches any other field, and only writes when something actually changed.
|
|
57
|
-
* claude-only (the trust/allow model is a claude concept).
|
|
58
|
-
* a multi-step run does the I/O once, before the first spawn in that dir.
|
|
67
|
+
* claude-only (the trust/allow model is a claude concept).
|
|
59
68
|
*/
|
|
60
69
|
const fs = __importStar(require("fs"));
|
|
61
70
|
const path = __importStar(require("path"));
|
|
@@ -122,17 +131,21 @@ function markProjectsTrusted(configPath, dirs) {
|
|
|
122
131
|
}
|
|
123
132
|
return changed;
|
|
124
133
|
}
|
|
125
|
-
// Per-path memo so a multi-step run writes at most once per unique dir.
|
|
126
|
-
const _trusted = new Set();
|
|
127
134
|
/**
|
|
128
|
-
* Ensure the claude spawn directories are trusted,
|
|
129
|
-
*
|
|
130
|
-
*
|
|
135
|
+
* Ensure the claude spawn directories are trusted, RE-ASSERTED on every call
|
|
136
|
+
* (no persistent memo — see the module header for why: concurrent whole-file
|
|
137
|
+
* writers clobber our flag back to `false`, and a one-shot memo would leave it
|
|
138
|
+
* stuck). No-op for non-claude providers and best-effort otherwise (a failure
|
|
139
|
+
* only leaves the cosmetic trust warning — never blocks a spawn).
|
|
140
|
+
*
|
|
141
|
+
* `markProjectsTrusted` reads the current on-disk value and writes ONLY when a
|
|
142
|
+
* dir is missing / `false`, so a call where the flag is already `true` is a
|
|
143
|
+
* cheap read with no write.
|
|
131
144
|
*/
|
|
132
145
|
function ensureClaudeTrusted(provider, dirs, home) {
|
|
133
146
|
if (provider !== 'claude')
|
|
134
147
|
return;
|
|
135
|
-
const todo = dirs.filter((d) => !!d
|
|
148
|
+
const todo = dirs.filter((d) => !!d);
|
|
136
149
|
if (todo.length === 0)
|
|
137
150
|
return;
|
|
138
151
|
try {
|
|
@@ -141,10 +154,4 @@ function ensureClaudeTrusted(provider, dirs, home) {
|
|
|
141
154
|
catch {
|
|
142
155
|
/* best-effort */
|
|
143
156
|
}
|
|
144
|
-
for (const d of todo)
|
|
145
|
-
_trusted.add(canonical(d));
|
|
146
|
-
}
|
|
147
|
-
/** Test-only: clear the per-path memo. */
|
|
148
|
-
function __resetClaudeTrustMemoForTest() {
|
|
149
|
-
_trusted.clear();
|
|
150
157
|
}
|
|
@@ -1,9 +1,57 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FETCH_ORIGIN_TTL_MS = void 0;
|
|
4
|
+
exports.__resetFetchOriginCache = __resetFetchOriginCache;
|
|
5
|
+
exports.fetchOrigin = fetchOrigin;
|
|
3
6
|
exports.repoDefaultBranch = repoDefaultBranch;
|
|
4
7
|
exports.currentBranch = currentBranch;
|
|
5
8
|
exports.isValidBranchName = isValidBranchName;
|
|
6
9
|
exports.resolveIntegrationBranch = resolveIntegrationBranch;
|
|
10
|
+
exports.resolveWorktreeBaseRef = resolveWorktreeBaseRef;
|
|
11
|
+
/** How long a fetch outcome (success OR failure) is reused for the same repo
|
|
12
|
+
* before a fresh `git fetch origin` is attempted again. Exists so a burst of
|
|
13
|
+
* near-simultaneous launches against the SAME repo — e.g. a "Launch all"
|
|
14
|
+
* batch, which is N independent HTTP requests with no shared server-side
|
|
15
|
+
* transaction (see design.md Decision 2) — performs one real fetch instead of
|
|
16
|
+
* one per rail. */
|
|
17
|
+
exports.FETCH_ORIGIN_TTL_MS = 15_000;
|
|
18
|
+
const fetchCache = new Map();
|
|
19
|
+
/** Test-only: clear the fetch dedup cache so tests never leak state across
|
|
20
|
+
* cases (mirrors `__resetRepoLocks` in repo-lock.ts). */
|
|
21
|
+
function __resetFetchOriginCache() {
|
|
22
|
+
fetchCache.clear();
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* `git fetch origin` against `repoDir` — updates ONLY `refs/remotes/origin/*`,
|
|
26
|
+
* never the checked-out local branch or working tree (Git itself refuses to
|
|
27
|
+
* touch either via a plain fetch). Never throws: any non-zero exit or runner
|
|
28
|
+
* rejection resolves to `{ ok: false, error }` so callers can degrade
|
|
29
|
+
* gracefully instead of failing the launch.
|
|
30
|
+
*
|
|
31
|
+
* De-duped per `repoDir` for `FETCH_ORIGIN_TTL_MS`: a call within the window
|
|
32
|
+
* of the last attempt (success OR failure) for the same repo reuses that
|
|
33
|
+
* outcome instead of spawning a new `git fetch` process. `now` is injectable
|
|
34
|
+
* so tests can control TTL expiry without real timers.
|
|
35
|
+
*/
|
|
36
|
+
async function fetchOrigin(git, repoDir, now = Date.now) {
|
|
37
|
+
const cached = fetchCache.get(repoDir);
|
|
38
|
+
const nowMs = now();
|
|
39
|
+
if (cached && nowMs - cached.at < exports.FETCH_ORIGIN_TTL_MS)
|
|
40
|
+
return cached.result;
|
|
41
|
+
const result = (async () => {
|
|
42
|
+
try {
|
|
43
|
+
const r = await git.run(['fetch', 'origin'], repoDir);
|
|
44
|
+
if (r.code === 0)
|
|
45
|
+
return { ok: true };
|
|
46
|
+
return { ok: false, error: r.stderr.trim() || r.stdout.trim() || `exit ${r.code}` };
|
|
47
|
+
}
|
|
48
|
+
catch (err) {
|
|
49
|
+
return { ok: false, error: err instanceof Error ? err.message : String(err) };
|
|
50
|
+
}
|
|
51
|
+
})();
|
|
52
|
+
fetchCache.set(repoDir, { at: nowMs, result });
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
7
55
|
/** The repo's default branch via `origin/HEAD` → the bare branch name, or null. */
|
|
8
56
|
async function repoDefaultBranch(git, repoDir) {
|
|
9
57
|
const r = await git.run(['symbolic-ref', '--quiet', 'refs/remotes/origin/HEAD'], repoDir);
|
|
@@ -61,3 +109,35 @@ async function resolveIntegrationBranch(git, input) {
|
|
|
61
109
|
// Detached / no remote / unresolvable → the literal HEAD (legacy-identical).
|
|
62
110
|
return { branch: 'HEAD', source: 'head-fallback' };
|
|
63
111
|
}
|
|
112
|
+
/**
|
|
113
|
+
* Decide the actual `baseRef` a worktree should branch from, given the
|
|
114
|
+
* already-resolved integration branch and whether `fetchOrigin` succeeded.
|
|
115
|
+
*
|
|
116
|
+
* `explicit` is a launch-time override the caller chose on purpose — it is
|
|
117
|
+
* NEVER remote-prefixed and NEVER existence-checked (see proposal.md Out of
|
|
118
|
+
* Scope). Every other source (`repo-default`, `project-setting`,
|
|
119
|
+
* `head-fallback`) uses the fetched remote-tracking ref `origin/<branch>`
|
|
120
|
+
* ONLY when the fetch succeeded AND that remote branch actually exists —
|
|
121
|
+
* guards a `project-setting` branch that was never pushed, which would
|
|
122
|
+
* otherwise turn a working local-only setup into a broken `git worktree add`.
|
|
123
|
+
* Any failure of either check falls back to today's bare local branch name,
|
|
124
|
+
* with a human-readable `warning` the caller can log/broadcast.
|
|
125
|
+
*/
|
|
126
|
+
async function resolveWorktreeBaseRef(git, input) {
|
|
127
|
+
const { repoDir, integration, fetchOk } = input;
|
|
128
|
+
if (integration.source === 'explicit') {
|
|
129
|
+
return { baseRef: integration.branch, usedRemote: false };
|
|
130
|
+
}
|
|
131
|
+
if (!fetchOk) {
|
|
132
|
+
return { baseRef: integration.branch, usedRemote: false, warning: 'git fetch origin failed; using local ref' };
|
|
133
|
+
}
|
|
134
|
+
const exists = await git.run(['rev-parse', '--verify', '--quiet', `refs/remotes/origin/${integration.branch}`], repoDir);
|
|
135
|
+
if (exists.code === 0) {
|
|
136
|
+
return { baseRef: `origin/${integration.branch}`, usedRemote: true };
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
baseRef: integration.branch,
|
|
140
|
+
usedRemote: false,
|
|
141
|
+
warning: `origin/${integration.branch} not found; using local ref`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
@@ -23,10 +23,18 @@ exports.publishDraftPr = publishDraftPr;
|
|
|
23
23
|
*/
|
|
24
24
|
const child_process_1 = require("child_process");
|
|
25
25
|
const git_guardrails_1 = require("./git-guardrails");
|
|
26
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
26
27
|
exports.defaultExec = {
|
|
27
28
|
run(cmd, args, cwd) {
|
|
28
29
|
return new Promise((resolve) => {
|
|
29
|
-
(
|
|
30
|
+
// `windowsSpawnEnv()` backfills SystemRoot / ComSpec / USERPROFILE, which a
|
|
31
|
+
// GUI-launched / pkg-stripped Windows sidecar can lack — without them the
|
|
32
|
+
// `git`/`gh` child (and the `sh -c` the `!gh …` credential helper runs) can
|
|
33
|
+
// fail to start. NOT `GIT_EXEC_ENV`: that hardened env disables credentials
|
|
34
|
+
// (GIT_ASKPASS=echo / GIT_TERMINAL_PROMPT=0), which the push must keep. No-op
|
|
35
|
+
// on POSIX (returns process.env), so mac/Linux behaviour is byte-identical.
|
|
36
|
+
const env = (0, win_spawn_1.windowsSpawnEnv)();
|
|
37
|
+
(0, child_process_1.execFile)(cmd, args, { cwd, env, maxBuffer: 16 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
|
|
30
38
|
const code = err && typeof err.code === 'number' ? err.code : err ? 1 : 0;
|
|
31
39
|
resolve({ code, stdout: stdout?.toString() ?? '', stderr: stderr?.toString() ?? '' });
|
|
32
40
|
});
|
|
@@ -45,7 +53,22 @@ async function publishDraftPr(exec, input) {
|
|
|
45
53
|
// Guardrail: never force-push, never push the integration branch itself.
|
|
46
54
|
const pushArgs = ['push', '-u', remote, branch];
|
|
47
55
|
(0, git_guardrails_1.assertGitAllowed)('git', pushArgs, { protectedBranch: baseBranch });
|
|
48
|
-
|
|
56
|
+
let push = await exec.run('git', pushArgs, repoDir);
|
|
57
|
+
if (push.code !== 0 && isCredentialFailure(push)) {
|
|
58
|
+
// The push failed authenticating over HTTPS — typically the machine's
|
|
59
|
+
// configured credential helper (e.g. git-credential-osxkeychain) is not on
|
|
60
|
+
// the app's PATH (GUI-launch PATH divergence: the sidecar inherits launchd's
|
|
61
|
+
// PATH, not the user's login shell). Retry borrowing gh's credentials — the
|
|
62
|
+
// SAME auth we use for `gh pr create` below, so if the PR can be created the
|
|
63
|
+
// push can succeed — instead of depending on a machine credential helper.
|
|
64
|
+
// `-c credential.helper=` first RESETS the (broken) inherited helper list,
|
|
65
|
+
// then adds gh's. Harmless for SSH remotes (git ignores credential.helper).
|
|
66
|
+
// The semantic push (refspec/remote) is unchanged, so the guardrail asserted
|
|
67
|
+
// above still holds; no-op when gh is absent (retry fails the same way).
|
|
68
|
+
const retry = await exec.run('git', ['-c', 'credential.helper=', '-c', 'credential.helper=!gh auth git-credential', ...pushArgs], repoDir);
|
|
69
|
+
if (retry.code === 0)
|
|
70
|
+
push = retry;
|
|
71
|
+
}
|
|
49
72
|
if (push.code !== 0) {
|
|
50
73
|
return { state: 'local-only', branch, reason: reasonFrom(push) };
|
|
51
74
|
}
|
|
@@ -66,3 +89,18 @@ function reasonFrom(r) {
|
|
|
66
89
|
const msg = (r.stderr.trim() || r.stdout.trim()).split('\n')[0];
|
|
67
90
|
return msg || `exit ${r.code}`;
|
|
68
91
|
}
|
|
92
|
+
/**
|
|
93
|
+
* True when a failed `git push` looks like an HTTPS credential/authentication
|
|
94
|
+
* problem (as opposed to a rejected ref, missing remote, network, etc.) — the
|
|
95
|
+
* only class worth retrying through gh's credential helper. Matches the
|
|
96
|
+
* canonical git strings, including the "'credential-<helper>' is not a git
|
|
97
|
+
* command" case that happens when the configured helper binary isn't on PATH.
|
|
98
|
+
*/
|
|
99
|
+
function isCredentialFailure(r) {
|
|
100
|
+
const s = `${r.stderr}\n${r.stdout}`.toLowerCase();
|
|
101
|
+
return (s.includes('could not read username') ||
|
|
102
|
+
s.includes('could not read password') ||
|
|
103
|
+
s.includes('authentication failed') ||
|
|
104
|
+
s.includes('credential-') ||
|
|
105
|
+
s.includes('terminal prompts disabled'));
|
|
106
|
+
}
|
|
@@ -211,6 +211,17 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
211
211
|
let integration;
|
|
212
212
|
const allocated = [];
|
|
213
213
|
await (0, repo_lock_1.withRepoLock)(baseRepo, async () => {
|
|
214
|
+
// Bring the repo's remote-tracking refs up to date BEFORE resolving the
|
|
215
|
+
// integration branch or allocating any worktree — otherwise `git worktree
|
|
216
|
+
// add -b <branch> <path> <bare-name>` resolves against whatever (possibly
|
|
217
|
+
// stale) commit the user's LOCAL branch happens to be at. `fetchOrigin` only
|
|
218
|
+
// ever touches `refs/remotes/origin/*`; it never mutates the checked-out
|
|
219
|
+
// branch/working tree. De-duped per repo for a short TTL so a "Launch all"
|
|
220
|
+
// batch (N independent launch requests for the same repo, serialized by
|
|
221
|
+
// this same withRepoLock) performs one real fetch, not one per rail. A
|
|
222
|
+
// failed fetch (no network / no remote / auth error) never blocks the
|
|
223
|
+
// launch — resolveWorktreeBaseRef below degrades to the local ref.
|
|
224
|
+
const fetchResult = await (0, integration_branch_1.fetchOrigin)(git, baseRepo);
|
|
214
225
|
// Resolve the project's designated integration branch ONCE, and branch every
|
|
215
226
|
// ticket's worktree off it (not the ambient HEAD). Empty setting → auto-resolve
|
|
216
227
|
// (repo default → HEAD fallback). See server/integration-branch.ts.
|
|
@@ -218,6 +229,20 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
218
229
|
repoDir: baseRepo,
|
|
219
230
|
projectSetting: (0, db_1.getProjectSettings)(ctx.db).integrationBranch,
|
|
220
231
|
});
|
|
232
|
+
// Prefer the freshly-fetched remote-tracking ref (origin/<branch>) over the
|
|
233
|
+
// bare local name for repo-default/project-setting sources — see
|
|
234
|
+
// resolveWorktreeBaseRef for the exact fallback policy. `explicit` is left
|
|
235
|
+
// completely untouched (rare, launch-time-chosen override).
|
|
236
|
+
const worktreeBaseRef = await (0, integration_branch_1.resolveWorktreeBaseRef)(git, {
|
|
237
|
+
repoDir: baseRepo, integration, fetchOk: fetchResult.ok,
|
|
238
|
+
});
|
|
239
|
+
if (worktreeBaseRef.warning) {
|
|
240
|
+
console.warn(`[rail-isolated] ${worktreeBaseRef.warning} (repo ${baseRepo})`);
|
|
241
|
+
try {
|
|
242
|
+
ctx.broadcast({ type: 'rail.fetch_degraded', projectId: ctx.project.id, railIndex, warning: worktreeBaseRef.warning });
|
|
243
|
+
}
|
|
244
|
+
catch { /* non-fatal, mirrors notifyOverlayDegraded's broadcast guard below */ }
|
|
245
|
+
}
|
|
221
246
|
if (prMode) {
|
|
222
247
|
prDeliveryId = (0, rail_pr_store_1.createPrDelivery)(ctx.db, {
|
|
223
248
|
railIndex,
|
|
@@ -251,7 +276,7 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
251
276
|
};
|
|
252
277
|
try {
|
|
253
278
|
for (const unit of units) {
|
|
254
|
-
const handle = await create(git, { repoDir: baseRepo, worktreesRoot, slug, ticketId: unit.ticketId, baseRef:
|
|
279
|
+
const handle = await create(git, { repoDir: baseRepo, worktreesRoot, slug, ticketId: unit.ticketId, baseRef: worktreeBaseRef.baseRef, branch: unitBranchName(unit.ticketId) });
|
|
255
280
|
// Per-run overlay: merge-link the framework surface the checkout didn't
|
|
256
281
|
// bring into the worktree (idempotent; resume-safe via its manifest).
|
|
257
282
|
let overlayExcludes = [];
|
|
@@ -23,3 +23,31 @@ const path_1 = __importDefault(require("path"));
|
|
|
23
23
|
if (!process.env.SPECRAILS_REGISTRY_HOME) {
|
|
24
24
|
process.env.SPECRAILS_REGISTRY_HOME = (0, fs_1.mkdtempSync)(path_1.default.join(os_1.default.tmpdir(), 'specrails-desktop-test-home-'));
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Hermetic env: a test run must NEVER inherit relocation / desktop-mode env
|
|
28
|
+
* vars from the host process. When the suite runs INSIDE a Specrails rail spawn
|
|
29
|
+
* (e.g. a `/specrails:implement` job running the verification step), that parent
|
|
30
|
+
* process exports `SPECRAILS_REPO_DIR` etc., which leak straight through the
|
|
31
|
+
* legacy (non-relocated) spawn path — the managers spread `process.env` into the
|
|
32
|
+
* child env — and break the "LEGACY: no relocation env" assertions in
|
|
33
|
+
* chat-manager / queue-manager tests. No test relies on ambient inheritance: the
|
|
34
|
+
* relocated-branch tests activate relocation via the registry + populated
|
|
35
|
+
* workspace gate, and the path-resolver / setup-prerequisites tests set their
|
|
36
|
+
* own vars per-case in `beforeEach` (which run AFTER this file), so a
|
|
37
|
+
* blanket-delete here is safe and any per-case setter still wins. Only
|
|
38
|
+
* `SPECRAILS_REPO_DIR` breaks a test today; the rest are defense-in-depth (they
|
|
39
|
+
* would leak identically the moment a future test asserts on them). Do NOT
|
|
40
|
+
* delete `SPECRAILS_REGISTRY_HOME` — the block above sets it intentionally.
|
|
41
|
+
*/
|
|
42
|
+
for (const k of [
|
|
43
|
+
'SPECRAILS_REPO_DIR',
|
|
44
|
+
'SPECRAILS_IS_DESKTOP',
|
|
45
|
+
'SPECRAILS_BUNDLED_RUNTIMES_PATH',
|
|
46
|
+
'SPECRAILS_TICKETS_PATH',
|
|
47
|
+
'SPECRAILS_BACKLOG_CONFIG_PATH',
|
|
48
|
+
'SPECRAILS_WORKSPACE_DIR',
|
|
49
|
+
'SPECRAILS_PROFILES_DIR',
|
|
50
|
+
'SPECRAILS_STATE_DIR',
|
|
51
|
+
]) {
|
|
52
|
+
delete process.env[k];
|
|
53
|
+
}
|
|
@@ -59,10 +59,14 @@ exports.listWorktrees = listWorktrees;
|
|
|
59
59
|
*/
|
|
60
60
|
const child_process_1 = require("child_process");
|
|
61
61
|
const path = __importStar(require("path"));
|
|
62
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
62
63
|
exports.defaultGitRunner = {
|
|
63
64
|
run(args, cwd) {
|
|
64
65
|
return new Promise((resolve) => {
|
|
65
|
-
|
|
66
|
+
// SystemRoot/ComSpec backfill so worktree + PR-decision git ops don't fail
|
|
67
|
+
// to start under a pkg-stripped Windows sidecar env. No-op on POSIX.
|
|
68
|
+
const env = (0, win_spawn_1.windowsSpawnEnv)();
|
|
69
|
+
(0, child_process_1.execFile)('git', args, { cwd, env, maxBuffer: 16 * 1024 * 1024, windowsHide: true }, (err, stdout, stderr) => {
|
|
66
70
|
const code = err && typeof err.code === 'number' ? err.code : err ? 1 : 0;
|
|
67
71
|
resolve({ code, stdout: stdout?.toString() ?? '', stderr: stderr?.toString() ?? '' });
|
|
68
72
|
});
|