specrails-desktop 2.34.0 → 2.34.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/client/dist/assets/{ActivityFeedPage-DIDxNsDj.js → ActivityFeedPage-B4yaoZsD.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-7pJ2G5VY.js → AgentBrowserCapture-Bhykbzog.js} +1 -1
- package/client/dist/assets/{AgentModeAnalyticsPane-BSSyTnTM.js → AgentModeAnalyticsPane-prvv23b7.js} +2 -2
- package/client/dist/assets/{AgentModeCodePane-DzW6gvDw.js → AgentModeCodePane-BmxySwPP.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-9wJff15f.js → AgentModeJobsPane-BvW6tTmU.js} +1 -1
- package/client/dist/assets/{AgentsPage-CIA67psb.js → AgentsPage-DinMMQPm.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-C0LwYELz.js → AnalyticsPage-d5GmoZY2.js} +1 -1
- package/client/dist/assets/{CodePage-RtKvHmAO.js → CodePage-Cgbj4lpK.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-BU8Y_Ts1.js → DesktopAnalyticsPage-DbbZi6fw.js} +1 -1
- package/client/dist/assets/{DocsDialog-DIpoaGlQ.js → DocsDialog-B87oF3aC.js} +1 -1
- package/client/dist/assets/{DocsPage-ME47KZpK.js → DocsPage-CqJ-yRkh.js} +1 -1
- package/client/dist/assets/{InteractiveJobComposer-BGjIe9R6.js → InteractiveJobComposer-nPGDKuCk.js} +1 -1
- package/client/dist/assets/{JobDetailModal-1UZkqH4I.js → JobDetailModal-duouGMzi.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CN65sAf1.js → JobDetailPage-Cw3Hu579.js} +1 -1
- package/client/dist/assets/{JobsPage-f-4dqys7.js → JobsPage-DYyV-qWT.js} +1 -1
- package/client/dist/assets/{LoopBuilderPage-WeiofeL6.js → LoopBuilderPage-B1GJJ0QZ.js} +1 -1
- package/client/dist/assets/{LoopPreviewModal-DDTnTyH0.js → LoopPreviewModal-Md6KF7rU.js} +1 -1
- package/client/dist/assets/{LoopsPage-BVNchIex.js → LoopsPage-BKNiwsy1.js} +1 -1
- package/client/dist/assets/{PluginsPage-CdFzGEfQ.js → PluginsPage-r682uE6n.js} +1 -1
- package/client/dist/assets/{ProjectSettingsDialog-BkCXBQWk.js → ProjectSettingsDialog-D6Vxjpv5.js} +1 -1
- package/client/dist/assets/{TemplatePreviewModal-RDi0khFG.js → TemplatePreviewModal-DcgWnn23.js} +1 -1
- package/client/dist/assets/{TicketDetailModal-MhJzrqbi.js → TicketDetailModal-BPaqyvrs.js} +2 -2
- package/client/dist/assets/{dist-js-BmsFWRly.js → dist-js-B1s_CM_v.js} +1 -1
- package/client/dist/assets/{dist-js-J5W_N5ng.js → dist-js-BNETKUa_.js} +1 -1
- package/client/dist/assets/{index-C_sdsSZt.js → index-DdC_faQv.js} +21 -21
- package/client/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/dist/providers/claude-adapter.js +21 -1
- package/server/dist/rail-isolated-launch.js +18 -3
- package/server/dist/rail-worktree-release.js +17 -1
- package/server/dist/worktree-node-modules.js +138 -16
package/client/dist/index.html
CHANGED
|
@@ -204,7 +204,7 @@
|
|
|
204
204
|
.specrails-splash__mark .pill { opacity: 1; }
|
|
205
205
|
}
|
|
206
206
|
</style>
|
|
207
|
-
<script type="module" crossorigin src="/assets/index-
|
|
207
|
+
<script type="module" crossorigin src="/assets/index-DdC_faQv.js"></script>
|
|
208
208
|
<link rel="modulepreload" crossorigin href="/assets/rolldown-runtime-CNC7AqOf.js">
|
|
209
209
|
<link rel="modulepreload" crossorigin href="/assets/jsx-runtime-zu2_FqZY.js">
|
|
210
210
|
<link rel="modulepreload" crossorigin href="/assets/react-dom-DiWzBDP9.js">
|
|
@@ -277,7 +277,7 @@
|
|
|
277
277
|
<link rel="modulepreload" crossorigin href="/assets/getTimezoneOffsetInMilliseconds-EjGOpn4b.js">
|
|
278
278
|
<link rel="modulepreload" crossorigin href="/assets/formatDistanceToNow-BOuXgebD.js">
|
|
279
279
|
<link rel="modulepreload" crossorigin href="/assets/spending-BpYPsevQ.js">
|
|
280
|
-
<link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-
|
|
280
|
+
<link rel="modulepreload" crossorigin href="/assets/TicketDetailModal-BPaqyvrs.js">
|
|
281
281
|
<link rel="stylesheet" crossorigin href="/assets/index-DCN5QbDm.css">
|
|
282
282
|
</head>
|
|
283
283
|
<body>
|
package/package.json
CHANGED
|
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
12
12
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
13
13
|
exports.claudeAdapter = void 0;
|
|
14
14
|
exports._normaliseClaudeModel = normaliseModel;
|
|
15
|
+
exports._resolveClaudeSpawnModel = resolveSpawnModel;
|
|
15
16
|
const child_process_1 = require("child_process");
|
|
16
17
|
const path_1 = __importDefault(require("path"));
|
|
17
18
|
const WHICH_CMD = process.platform === 'win32' ? 'where' : 'which';
|
|
@@ -33,6 +34,7 @@ function normaliseModel(model) {
|
|
|
33
34
|
return 'sonnet';
|
|
34
35
|
case 'claude-fable-5':
|
|
35
36
|
return 'fable';
|
|
37
|
+
case 'claude-opus-5':
|
|
36
38
|
case 'claude-opus-4-8':
|
|
37
39
|
case 'claude-opus-4-5':
|
|
38
40
|
case 'claude-opus-4-1-20250805':
|
|
@@ -46,6 +48,24 @@ function normaliseModel(model) {
|
|
|
46
48
|
return model || 'sonnet';
|
|
47
49
|
}
|
|
48
50
|
}
|
|
51
|
+
/**
|
|
52
|
+
* Catalog aliases whose GENERATION Specrails pins explicitly.
|
|
53
|
+
*
|
|
54
|
+
* The catalog value (`opus`) stays the stored, validated and displayed identity
|
|
55
|
+
* — profiles, project settings, conversation rows and analytics all keep using
|
|
56
|
+
* it, and `normaliseModel` collapses the pinned id back to it. Only the spawn
|
|
57
|
+
* argument is expanded, so picking "Claude Opus" is a product decision about
|
|
58
|
+
* WHICH Opus runs instead of delegating that to whatever generation the CLI's
|
|
59
|
+
* bare `opus` alias currently points at.
|
|
60
|
+
*/
|
|
61
|
+
const PINNED_ALIAS_MODEL_IDS = {
|
|
62
|
+
opus: 'claude-opus-5',
|
|
63
|
+
};
|
|
64
|
+
/** Catalog value (or concrete id) in, spawn model id out. */
|
|
65
|
+
function resolveSpawnModel(model) {
|
|
66
|
+
const alias = normaliseModel(model);
|
|
67
|
+
return PINNED_ALIAS_MODEL_IDS[alias] ?? alias;
|
|
68
|
+
}
|
|
49
69
|
/**
|
|
50
70
|
* Common flags + the effective tool boundary for this spawn.
|
|
51
71
|
*
|
|
@@ -95,7 +115,7 @@ function commonFlagsFor(opts) {
|
|
|
95
115
|
}
|
|
96
116
|
function buildClaudeArgs(action, opts) {
|
|
97
117
|
const args = [];
|
|
98
|
-
const model =
|
|
118
|
+
const model = resolveSpawnModel(opts.model);
|
|
99
119
|
// Titles are unconditionally pure-output. Centralising the policy here keeps
|
|
100
120
|
// both ChatManager and AgentChatManager from accidentally regaining tools.
|
|
101
121
|
const commonFlags = commonFlagsFor(action === 'auto-title' ? { ...opts, toolPolicy: 'none' } : opts);
|
|
@@ -301,7 +301,7 @@ function branchRecords(results) {
|
|
|
301
301
|
branchOwnership: result.run.branchOwnership,
|
|
302
302
|
worktreePath: result.run.handle.worktreePath,
|
|
303
303
|
overlayExcludes: result.run.overlayExcludes,
|
|
304
|
-
overlayCleanupEvidence: result.run.overlayCleanupEvidence,
|
|
304
|
+
overlayCleanupEvidence: [...result.run.overlayCleanupEvidence, ...result.run.warmLinkEvidence],
|
|
305
305
|
settlementIgnoredPaths: result.run.settlementIgnoredPaths ?? null,
|
|
306
306
|
})));
|
|
307
307
|
}
|
|
@@ -695,10 +695,17 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
695
695
|
// into the fresh worktree (best-effort; a failure just leaves the run
|
|
696
696
|
// on the legacy cold start). Linked paths join the overlay exclusion
|
|
697
697
|
// list so they inherit the commit-time guarantees.
|
|
698
|
+
let warmLinkEvidence = [];
|
|
698
699
|
try {
|
|
699
700
|
const nm = (io.linkNodeModules ?? worktree_node_modules_1.linkNodeModulesIntoWorktree)(baseRepo, handle.worktreePath);
|
|
700
|
-
|
|
701
|
-
|
|
701
|
+
// `authenticated` (not `linked`) is the authorized set: it also covers
|
|
702
|
+
// links a previous pass created, which a resumed launch would
|
|
703
|
+
// otherwise leave both un-excluded from the commit and unauthorized
|
|
704
|
+
// for release.
|
|
705
|
+
const authorized = nm.authenticated ?? nm.linked ?? [];
|
|
706
|
+
if (authorized.length > 0)
|
|
707
|
+
overlayExcludes = overlayExcludes.concat(authorized);
|
|
708
|
+
warmLinkEvidence = nm.evidence ?? [];
|
|
702
709
|
if (nm.warnings.length > 0)
|
|
703
710
|
notifyOverlayDegraded(unit.ticketId, nm.warnings);
|
|
704
711
|
}
|
|
@@ -732,6 +739,7 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
732
739
|
handle,
|
|
733
740
|
overlayExcludes,
|
|
734
741
|
overlayCleanupEvidence,
|
|
742
|
+
warmLinkEvidence,
|
|
735
743
|
provenanceSnapshot,
|
|
736
744
|
continuationTarget,
|
|
737
745
|
baseRef: continuationTarget?.baseRef ?? worktreeBaseRef.baseRef,
|
|
@@ -876,6 +884,13 @@ async function launchIsolatedRail(input, io = {}) {
|
|
|
876
884
|
instructionsFilename: overlayInstructions,
|
|
877
885
|
}, a.overlayCleanupEvidence);
|
|
878
886
|
}
|
|
887
|
+
// Warm-dependency links are re-proven against the base repo (their source),
|
|
888
|
+
// not the framework overlay source. A link the engine replaced with a real
|
|
889
|
+
// install loses its authorization here and correctly preserves the worktree.
|
|
890
|
+
if (a.warmLinkEvidence.length > 0) {
|
|
891
|
+
a.warmLinkEvidence = (0, worktree_node_modules_1.authenticateWarmNodeModulesLinks)(baseRepo, a.handle.worktreePath)
|
|
892
|
+
.filter((live) => a.warmLinkEvidence.some((prior) => prior.path === live.path && prior.kind === live.kind && prior.digest === live.digest));
|
|
893
|
+
}
|
|
879
894
|
let commit;
|
|
880
895
|
try {
|
|
881
896
|
commit = await (0, worktree_manager_1.commitWorktreeAndVerify)(git, a.handle.worktreePath, worktreeCommitMessage(ctx, a.ticketId, a.runId, implementationOutcome === 'failed'), a.overlayExcludes);
|
|
@@ -42,6 +42,7 @@ const worktree_manager_1 = require("./worktree-manager");
|
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const fs = __importStar(require("fs"));
|
|
44
44
|
const worktree_overlay_1 = require("./worktree-overlay");
|
|
45
|
+
const worktree_node_modules_1 = require("./worktree-node-modules");
|
|
45
46
|
const COMMIT_SHA_RE = /^[0-9a-f]{40,64}$/i;
|
|
46
47
|
/** Collapse per-unit settlement evidence into one unambiguous immutable HEAD per
|
|
47
48
|
* branch. Conflicting or malformed records intentionally yield no authorization. */
|
|
@@ -157,8 +158,23 @@ async function verifyReleaseEvidence(input, worktree) {
|
|
|
157
158
|
if (!expected || !COMMIT_SHA_RE.test(expected)) {
|
|
158
159
|
return { failure: 'no durable settled HEAD is recorded for its branch', overlayEntries: [] };
|
|
159
160
|
}
|
|
160
|
-
const
|
|
161
|
+
const persistedEntries = (input.overlayEvidenceByBranch.get(worktree.branch) ?? [])
|
|
161
162
|
.filter((entry) => (0, worktree_overlay_1.matchesOverlayCleanupEvidence)(worktree.worktree_path, entry));
|
|
163
|
+
// Warm-dependency links are authenticated LIVE against the base repo, on top
|
|
164
|
+
// of whatever was persisted. Two reasons this is derived and not persisted-
|
|
165
|
+
// only: a repo's ordinary `node_modules/` DIRECTORY ignore pattern does not
|
|
166
|
+
// ignore a SYMLINK of that name, so the link reads as untracked dirt and
|
|
167
|
+
// permanently blocked release (and therefore branch checkout); and worktrees
|
|
168
|
+
// that settled before the links carried evidence are stuck at `needs-review`
|
|
169
|
+
// where no persisted fix could ever reach them. The proof is identical to the
|
|
170
|
+
// persisted one — a symlink resolving to the base checkout's identically-
|
|
171
|
+
// named dependency directory — so nothing weaker is being accepted here.
|
|
172
|
+
const warmEntries = (input.authenticateWarmLinks ?? worktree_node_modules_1.authenticateWarmNodeModulesLinks)(input.repoDir, worktree.worktree_path);
|
|
173
|
+
const seenPaths = new Set(persistedEntries.map((entry) => entry.path));
|
|
174
|
+
const overlayEntries = [
|
|
175
|
+
...persistedEntries,
|
|
176
|
+
...warmEntries.filter((entry) => !seenPaths.has(entry.path)),
|
|
177
|
+
];
|
|
162
178
|
const overlayExcludes = overlayEntries.map((entry) => entry.path);
|
|
163
179
|
try {
|
|
164
180
|
const status = await input.git.run([
|
|
@@ -34,6 +34,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.isWorktreeNodeModulesEnabled = isWorktreeNodeModulesEnabled;
|
|
37
|
+
exports.authenticateWarmNodeModulesLinks = authenticateWarmNodeModulesLinks;
|
|
37
38
|
exports.linkNodeModulesIntoWorktree = linkNodeModulesIntoWorktree;
|
|
38
39
|
/**
|
|
39
40
|
* Warm-dependency reuse for isolated rail worktrees.
|
|
@@ -57,10 +58,26 @@ exports.linkNodeModulesIntoWorktree = linkNodeModulesIntoWorktree;
|
|
|
57
58
|
* prior link, anything) is left untouched, so resume passes are idempotent
|
|
58
59
|
* and an agent-made install is never clobbered.
|
|
59
60
|
*
|
|
60
|
-
* The returned `
|
|
61
|
-
* list, so a linked `node_modules` inherits the same commit-time
|
|
62
|
-
* overlay scaffolding (excluded pathspecs + index audit) even if
|
|
63
|
-
* `.gitignore` were ever missing the entry.
|
|
61
|
+
* The returned `authenticated` paths are appended to the launch's overlay-
|
|
62
|
+
* exclusion list, so a linked `node_modules` inherits the same commit-time
|
|
63
|
+
* guarantees as overlay scaffolding (excluded pathspecs + index audit) even if
|
|
64
|
+
* the repo's `.gitignore` were ever missing the entry.
|
|
65
|
+
*
|
|
66
|
+
* Release safety (why `authenticated` exists next to `linked`): a repo whose
|
|
67
|
+
* `.gitignore` carries the ordinary `node_modules/` DIRECTORY pattern does not
|
|
68
|
+
* ignore a SYMLINK named `node_modules`, so the link surfaces as an untracked
|
|
69
|
+
* entry in `git status`. Without evidence authorizing it, worktree release read
|
|
70
|
+
* that as "the worktree contains changes made after settlement", parked the row
|
|
71
|
+
* at `needs-review` forever and permanently blocked checkout of the branch. The
|
|
72
|
+
* links therefore also carry `OverlayCleanupEvidence`, and a link created by an
|
|
73
|
+
* EARLIER pass (resume) is re-authenticated instead of being ignored — the old
|
|
74
|
+
* `linked`-only contract silently dropped both guarantees on resume.
|
|
75
|
+
*
|
|
76
|
+
* Authentication is deliberately narrow and source-anchored, never name-based:
|
|
77
|
+
* the worktree entry must be a symlink whose resolved target is exactly the base
|
|
78
|
+
* checkout's identically-named dependency directory. A real directory, a copy,
|
|
79
|
+
* or a link pointing anywhere else stays unauthorized and preserves the
|
|
80
|
+
* worktree, as `Recoverable work is never removed automatically` requires.
|
|
64
81
|
*
|
|
65
82
|
* Honest caveat (documented, accepted): the link SHARES the dependency tree
|
|
66
83
|
* with the base checkout. Reads (tests, builds) are safe under concurrency; an
|
|
@@ -72,8 +89,11 @@ exports.linkNodeModulesIntoWorktree = linkNodeModulesIntoWorktree;
|
|
|
72
89
|
*/
|
|
73
90
|
const fs = __importStar(require("fs"));
|
|
74
91
|
const path = __importStar(require("path"));
|
|
92
|
+
const worktree_overlay_1 = require("./worktree-overlay");
|
|
75
93
|
/** Package-dir discovery depth: 0 = repo root, 1 = `client/`-style children. */
|
|
76
94
|
const MAX_DEPTH = 2;
|
|
95
|
+
/** Name of the dependency directory this module links. */
|
|
96
|
+
const DEPS_DIR = 'node_modules';
|
|
77
97
|
function isWorktreeNodeModulesEnabled() {
|
|
78
98
|
return (process.env.SPECRAILS_WORKTREE_NODE_MODULES ?? '').toLowerCase() !== 'false';
|
|
79
99
|
}
|
|
@@ -127,31 +147,133 @@ function discoverPackageDirs(baseRepo) {
|
|
|
127
147
|
walk('', 0);
|
|
128
148
|
return found;
|
|
129
149
|
}
|
|
150
|
+
/** Resolve a path through symlinks when possible; the literal path otherwise
|
|
151
|
+
* (a nonexistent target must still compare, and must still compare EQUAL to an
|
|
152
|
+
* equally-nonexistent expectation rather than silently authorizing). */
|
|
153
|
+
function resolveRealPath(target) {
|
|
154
|
+
try {
|
|
155
|
+
return fs.realpathSync(target);
|
|
156
|
+
}
|
|
157
|
+
catch {
|
|
158
|
+
return path.resolve(target);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Prove one worktree-relative path is an app-created warm-dependency link and
|
|
163
|
+
* fingerprint it. Returns null for anything that is not EXACTLY a symlink whose
|
|
164
|
+
* target resolves to the base checkout's identically-named directory — a real
|
|
165
|
+
* directory, a dereferencing copy, a dangling link, or a link into some other
|
|
166
|
+
* tree are all unauthorized and must keep preserving the worktree.
|
|
167
|
+
*/
|
|
168
|
+
function authenticateWarmLink(baseRepo, worktreePath, rel) {
|
|
169
|
+
const dest = path.join(worktreePath, ...rel.split('/'));
|
|
170
|
+
try {
|
|
171
|
+
if (!fs.lstatSync(dest).isSymbolicLink())
|
|
172
|
+
return null;
|
|
173
|
+
const target = path.resolve(path.dirname(dest), fs.readlinkSync(dest));
|
|
174
|
+
const expected = path.join(baseRepo, ...rel.split('/'));
|
|
175
|
+
if (resolveRealPath(target) !== resolveRealPath(expected))
|
|
176
|
+
return null;
|
|
177
|
+
// A link to something that is not a live directory proves nothing about the
|
|
178
|
+
// base checkout's dependency tree.
|
|
179
|
+
if (!isDir(target))
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return null;
|
|
184
|
+
}
|
|
185
|
+
const fingerprint = (0, worktree_overlay_1.fingerprintOverlayCleanupPath)(dest);
|
|
186
|
+
return fingerprint ? { path: rel, ...fingerprint } : null;
|
|
187
|
+
}
|
|
188
|
+
/** Worktree-relative POSIX paths of every `node_modules` entry the worktree
|
|
189
|
+
* holds at depth ≤ MAX_DEPTH. Mirrors the linker's own discovery depth, so it
|
|
190
|
+
* can only ever see paths this module could have created. Recursion uses lstat
|
|
191
|
+
* so a symlinked directory is never walked into. */
|
|
192
|
+
function discoverWorktreeDependencyPaths(worktreePath) {
|
|
193
|
+
const found = [];
|
|
194
|
+
const walk = (rel, depth) => {
|
|
195
|
+
const abs = rel === '' ? worktreePath : path.join(worktreePath, rel);
|
|
196
|
+
let names;
|
|
197
|
+
try {
|
|
198
|
+
names = fs.readdirSync(abs);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
for (const name of names) {
|
|
204
|
+
const childRel = rel === '' ? name : `${rel}/${name}`;
|
|
205
|
+
if (name === DEPS_DIR) {
|
|
206
|
+
found.push(childRel);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (depth >= MAX_DEPTH || name.startsWith('.'))
|
|
210
|
+
continue;
|
|
211
|
+
try {
|
|
212
|
+
if (!fs.lstatSync(path.join(abs, name)).isDirectory())
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
walk(childRel, depth + 1);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
walk('', 0);
|
|
222
|
+
return found;
|
|
223
|
+
}
|
|
224
|
+
/**
|
|
225
|
+
* Live authentication of the warm-dependency links a worktree currently holds.
|
|
226
|
+
*
|
|
227
|
+
* Deriving this from the filesystem (instead of only from persisted evidence)
|
|
228
|
+
* is what heals worktrees that settled BEFORE the links carried evidence: they
|
|
229
|
+
* are stuck at `needs-review` with the link as their only "dirt", and a
|
|
230
|
+
* persisted-only fix would never reach them. The proof is identical either way
|
|
231
|
+
* — the link target anchored to a directory the app controls.
|
|
232
|
+
*/
|
|
233
|
+
function authenticateWarmNodeModulesLinks(baseRepo, worktreePath) {
|
|
234
|
+
const evidence = [];
|
|
235
|
+
for (const rel of discoverWorktreeDependencyPaths(worktreePath)) {
|
|
236
|
+
const entry = authenticateWarmLink(baseRepo, worktreePath, rel);
|
|
237
|
+
if (entry)
|
|
238
|
+
evidence.push(entry);
|
|
239
|
+
}
|
|
240
|
+
return evidence;
|
|
241
|
+
}
|
|
130
242
|
/**
|
|
131
243
|
* Link the base checkout's installed `node_modules` trees into a fresh
|
|
132
244
|
* worktree. Best-effort and side-effect-transparent: never throws, never
|
|
133
|
-
* overwrites, returns what it created
|
|
134
|
-
*
|
|
245
|
+
* overwrites, and returns both what it created and what it can PROVE is a warm
|
|
246
|
+
* link (including links a previous pass created), so the caller can exclude
|
|
247
|
+
* those paths from commits and authorize them for release.
|
|
135
248
|
*/
|
|
136
249
|
function linkNodeModulesIntoWorktree(baseRepo, worktreePath) {
|
|
137
|
-
const result = { linked: [], warnings: [] };
|
|
250
|
+
const result = { linked: [], authenticated: [], evidence: [], warnings: [] };
|
|
138
251
|
if (!isWorktreeNodeModulesEnabled())
|
|
139
252
|
return result;
|
|
140
253
|
for (const pkgRel of discoverPackageDirs(baseRepo)) {
|
|
141
|
-
const rel = pkgRel === '' ?
|
|
254
|
+
const rel = pkgRel === '' ? DEPS_DIR : `${pkgRel}/${DEPS_DIR}`;
|
|
142
255
|
const src = path.join(baseRepo, ...rel.split('/'));
|
|
143
256
|
if (!isDir(src))
|
|
144
257
|
continue;
|
|
145
258
|
const dest = path.join(worktreePath, ...rel.split('/'));
|
|
146
|
-
if (exists(dest))
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
259
|
+
if (!exists(dest)) {
|
|
260
|
+
try {
|
|
261
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
262
|
+
fs.symlinkSync(src, dest, process.platform === 'win32' ? 'junction' : undefined);
|
|
263
|
+
result.linked.push(rel);
|
|
264
|
+
}
|
|
265
|
+
catch (err) {
|
|
266
|
+
result.warnings.push(`failed to link ${rel}: ${errMsg(err)}`);
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
152
269
|
}
|
|
153
|
-
|
|
154
|
-
|
|
270
|
+
// Authenticate what is actually on disk NOW — a link we just made, or one a
|
|
271
|
+
// previous pass made. An entry that cannot prove itself (a real dir, an
|
|
272
|
+
// agent-made install) is deliberately left unauthorized.
|
|
273
|
+
const entry = authenticateWarmLink(baseRepo, worktreePath, rel);
|
|
274
|
+
if (entry) {
|
|
275
|
+
result.authenticated.push(rel);
|
|
276
|
+
result.evidence.push(entry);
|
|
155
277
|
}
|
|
156
278
|
}
|
|
157
279
|
return result;
|