specrails-desktop 2.62.0 → 2.63.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/cli/dist/specrails-desktop.js +39 -5
- package/client/dist/assets/{ActivityFeedPage-C_pqc281.js → ActivityFeedPage-B1bpTy7O.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-vz6rbDtu.js → AgentBrowserCapture-BussBbN3.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BiT1n3BU.js +2 -0
- package/client/dist/assets/AgentModeCodePane-CUjNfeKa.js +2 -0
- package/client/dist/assets/{AgentModeJobsPane-99fBBLri.js → AgentModeJobsPane-RVEwyS59.js} +1 -1
- package/client/dist/assets/AgentsPage-DKE-eMC5.js +118 -0
- package/client/dist/assets/AnalyticsPage-Bser2Ly3.js +1 -0
- package/client/dist/assets/BarChart-BDMqVFJy.js +55 -0
- package/client/dist/assets/CodePage-BK0grm6e.js +3 -0
- package/client/dist/assets/DesktopAnalyticsPage-DEBidZbs.js +1 -0
- package/client/dist/assets/{DocsDialog-XUCYKoq0.js → DocsDialog-Dhyo57n9.js} +1 -1
- package/client/dist/assets/{DocsPage-dOLC8bXA.js → DocsPage-BcEkPXd6.js} +1 -1
- package/client/dist/assets/{ExportDropdown-BvKQgwM9.js → ExportDropdown-mRmqdkjr.js} +1 -1
- package/client/dist/assets/IntegrationsPage-EAipiDWm.js +1 -0
- package/client/dist/assets/InteractiveJobComposer-BlCe3vrU.js +19 -0
- package/client/dist/assets/{JobDetailModal-CkiubrkZ.js → JobDetailModal-CkAejD1F.js} +1 -1
- package/client/dist/assets/{JobDetailPage-CNe4Wn7l.js → JobDetailPage-P-FS_lKz.js} +1 -1
- package/client/dist/assets/JobsPage-CoUXgYse.js +1 -0
- package/client/dist/assets/LoopBuilderPage-iFylNk68.js +1 -0
- package/client/dist/assets/{LoopPreviewModal-Dv3dxHr_.js → LoopPreviewModal-4obS_Ptg.js} +1 -1
- package/client/dist/assets/{LoopWindowSurface-DN2lbSnL.js → LoopWindowSurface-CLr5OPqj.js} +1 -1
- package/client/dist/assets/LoopsPage-eKpWFtCp.js +2 -0
- package/client/dist/assets/MinimizedChatsContext-BHIUPVmC.js +1 -0
- package/client/dist/assets/PluginsPage-BgTC2CQx.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-B4chTfU8.js +1 -0
- package/client/dist/assets/RepositoryDeliveries-DAIxIYsl.js +5 -0
- package/client/dist/assets/{RepositoryScopeSelector-DrtPqvc_.js → RepositoryScopeSelector-DeCoUsRN.js} +1 -1
- package/client/dist/assets/ReviewPacketPage-Cktv0-9b.js +4 -0
- package/client/dist/assets/{TemplatePreviewModal-DcFlhUxy.js → TemplatePreviewModal-BZTV1TM1.js} +1 -1
- package/client/dist/assets/TicketDetailModalContext-DiPngt2g.js +16 -0
- package/client/dist/assets/Trans-ZiXIfj50.js +1 -0
- package/client/dist/assets/{agentRuntime-TKQyg137.js → agentRuntime-BDvbJMmS.js} +1 -1
- package/client/dist/assets/{agentRuntime-DvhEe1um.js → agentRuntime-BKJfv5YD.js} +1 -1
- package/client/dist/assets/{agentRuntime-DxC6-9rN.js → agentRuntime-BxF81z52.js} +1 -1
- package/client/dist/assets/{agentRuntime-C_yLPKDy.js → agentRuntime-BxYS5V7u.js} +1 -1
- package/client/dist/assets/{agentRuntime-WSv8G0pW.js → agentRuntime-BzuwQQgn.js} +1 -1
- package/client/dist/assets/{agentRuntime-D2kYQqNO.js → agentRuntime-Dc9HG3BA.js} +1 -1
- package/client/dist/assets/{agentRuntime-Bjt7Ij6_.js → agentRuntime-DiCldqGG.js} +1 -1
- package/client/dist/assets/{agentRuntime-B-myNBUP.js → agentRuntime-es2ymSl-.js} +1 -1
- package/client/dist/assets/ban-wqfdzoXt.js +1 -0
- package/client/dist/assets/{button-D1xExSTF.js → button-4FS1ZDTp.js} +1 -1
- package/client/dist/assets/chevron-up-6EFqEHBj.js +1 -0
- package/client/dist/assets/chunk-OB3PAWPO-B1EMk45A.js +3 -0
- package/client/dist/assets/circle-x-D2ttKotB.js +1 -0
- package/client/dist/assets/createLucideIcon-DtODIU64.js +1 -0
- package/client/dist/assets/{dialog-CMgps2MY.js → dialog-DtdR05v1.js} +1 -1
- package/client/dist/assets/download-C9wP_42h.js +1 -0
- package/client/dist/assets/external-link-CZ6Q-v1-.js +1 -0
- package/client/dist/assets/file-text-C0blZHLh.js +1 -0
- package/client/dist/assets/{formatDistanceToNow-DlQY1Lm_.js → formatDistanceToNow-CdN5lTyD.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-COUeq65G.js → getTimezoneOffsetInMilliseconds-62h40I4t.js} +1 -1
- package/client/dist/assets/git-branch-yf6LE1Bw.js +1 -0
- package/client/dist/assets/index-BgbCbIua.css +2 -0
- package/client/dist/assets/{index-CONHvEJw.js → index-CECj3joE.js} +40 -40
- package/client/dist/assets/jira-api-C_D_RDhl.js +1 -0
- package/client/dist/assets/jsx-runtime-BbYvbtY8.js +1 -0
- package/client/dist/assets/layers-BCLTgHXO.js +1 -0
- package/client/dist/assets/{lib-C7Mhm-gT.js → lib-CQJT8S6Q.js} +1 -1
- package/client/dist/assets/{lib-CGdyxIWg.js → lib-DWJfei6y.js} +1 -1
- package/client/dist/assets/loader-circle-BqV9xYGf.js +1 -0
- package/client/dist/assets/{loop-layout-BF8xMiNv.js → loop-layout-CyXj9223.js} +1 -1
- package/client/dist/assets/pencil-CxrRbDwb.js +1 -0
- package/client/dist/assets/plus-fHDdjBk1.js +1 -0
- package/client/dist/assets/{project-repositories-Cc3oNv8X.js → project-repositories-Be0CWklo.js} +1 -1
- package/client/dist/assets/react-dom-DDD8FYaU.js +1 -0
- package/client/dist/assets/rotate-ccw-DalJAQQX.js +1 -0
- package/client/dist/assets/save-LmPSfsvZ.js +1 -0
- package/client/dist/assets/search-BDVp3fkH.js +1 -0
- package/client/dist/assets/{settings-BKJZ2KEM.js → settings-BO2N-oPf.js} +1 -1
- package/client/dist/assets/{settings-LfFnl1UH.js → settings-BQChAs_w.js} +1 -1
- package/client/dist/assets/{settings-BV3jocIx.js → settings-BzmYlcQB.js} +1 -1
- package/client/dist/assets/{settings-BjM2Dw8_.js → settings-CGjXb6TX.js} +1 -1
- package/client/dist/assets/{settings-DA77M48m.js → settings-CMxEWY46.js} +1 -1
- package/client/dist/assets/{settings-P1IQvhnL.js → settings-C_dRSgI2.js} +1 -1
- package/client/dist/assets/{settings-eIMJjyK_.js → settings-Hw7av-B1.js} +1 -1
- package/client/dist/assets/{settings-CKu0LTib.js → settings-nryN6wuS.js} +1 -1
- package/client/dist/assets/sparkles-CCfUdFGY.js +1 -0
- package/client/dist/assets/spending-eWiZGhC_.js +1 -0
- package/client/dist/assets/square-D0JTbRSR.js +1 -0
- package/client/dist/assets/trash-D8zYUmTg.js +1 -0
- package/client/dist/assets/triangle-alert-Cxpn8bqr.js +1 -0
- package/client/dist/assets/upload-CCHvUkRs.js +1 -0
- package/client/dist/assets/{useDesktop-9Lwoel_8.js → useDesktop-C0mRIlHT.js} +1 -1
- package/client/dist/assets/{useRuntimeRuns-DL0xYoZ2.js → useRuntimeRuns-B1a6GChu.js} +1 -1
- package/client/dist/assets/useSharedWebSocket-CcsE5986.js +2 -0
- package/client/dist/assets/{value-s-gzBeyF.js → value-DtOOK0wQ.js} +1 -1
- package/client/dist/assets/x-Bu7X_eR2.js +1 -0
- package/client/dist/assets/zap-bugHyWzQ.js +1 -0
- package/client/dist/index.html +44 -45
- package/docs/ci-cd.md +8 -1
- package/docs/customizing.md +1 -0
- package/docs/getting-started.md +1 -1
- package/docs/guide/de/settings/5-project-environment.md +27 -0
- package/docs/guide/en/settings/5-project-environment.md +27 -0
- package/docs/guide/es/settings/5-project-environment.md +27 -0
- package/docs/guide/fr/settings/5-project-environment.md +27 -0
- package/docs/guide/it/settings/5-project-environment.md +27 -0
- package/docs/guide/ja/settings/5-project-environment.md +27 -0
- package/docs/guide/pt/settings/5-project-environment.md +27 -0
- package/docs/guide/zh/settings/5-project-environment.md +27 -0
- package/docs/internals/api-reference.md +3 -1
- package/docs/internals/configuration.md +46 -5
- package/docs/internals/core-runtime-updates.md +3 -3
- package/docs/internals/desktop-owned-workflows.md +10 -0
- package/docs/internals/programmatic-agent-runtime.md +16 -0
- package/docs/internals/safe-pr-review-flow.md +15 -0
- package/docs/internals/source-map.md +6 -1
- package/docs/mcp.md +1 -1
- package/docs/platforms/macos.md +2 -0
- package/docs/platforms/windows-parity.md +4 -4
- package/docs/platforms/windows.md +9 -9
- package/local-runner/dist/specrails-local-runner.js +1 -1
- package/package.json +8 -9
- package/server/dist/mcp/tools/env.js +38 -3
- package/server/dist/mcp/tools/loops.js +1 -1
- package/server/dist/modules/agent-runtime/runtime/agent-runtime-bridge.js +13 -3
- package/server/dist/modules/delivery/runtime/rail-isolated-launch.js +1 -1
- package/server/dist/modules/execution/runtime/queue-manager.js +8 -0
- package/server/dist/modules/loops/runtime/loop-executors.js +1 -0
- package/server/dist/modules/loops/runtime/loop-implement-recipe.js +13 -6
- package/server/dist/modules/loops/runtime/loop-run-manager.js +7 -0
- package/server/dist/modules/loops/runtime/loop-turn-limits.js +64 -0
- package/server/dist/modules/loops/runtime/loops-router.js +3 -1
- package/server/dist/modules/missions/runtime/agent-operator-prompt.js +8 -0
- package/server/dist/modules/project-settings/adapters/http.js +24 -0
- package/server/dist/path-resolver.js +135 -20
- package/server/dist/project-env.js +265 -17
- package/server/dist/project-registry.js +14 -0
- package/server/dist/project-router-settings.js +17 -0
- package/server/dist/schemas/agent-runtime.schema.json +1 -1
- package/server/dist/worktree-node-modules.js +95 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BHCcUahS.js +0 -2
- package/client/dist/assets/AgentModeCodePane-DhmxgG3W.js +0 -2
- package/client/dist/assets/AgentsPage-CTwm_6T8.js +0 -118
- package/client/dist/assets/AnalyticsPage-YD8MLoPa.js +0 -1
- package/client/dist/assets/BarChart-nBOMNfiy.js +0 -33
- package/client/dist/assets/CodePage-CLJn2F2e.js +0 -3
- package/client/dist/assets/DesktopAnalyticsPage-MWGQVbwp.js +0 -1
- package/client/dist/assets/IntegrationsPage-Bhy58lnQ.js +0 -1
- package/client/dist/assets/InteractiveJobComposer-DxOvwmBF.js +0 -19
- package/client/dist/assets/JobsPage-DSts_L_d.js +0 -1
- package/client/dist/assets/LoopBuilderPage-C9D9eer5.js +0 -1
- package/client/dist/assets/LoopsPage-DHgIreEW.js +0 -2
- package/client/dist/assets/MinimizedChatsContext-Ds2-R6qJ.js +0 -1
- package/client/dist/assets/PluginsPage-D1EJ9oja.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-DKpa1wKk.js +0 -1
- package/client/dist/assets/RepositoryDeliveries-CW4eKG3x.js +0 -5
- package/client/dist/assets/ReviewPacketPage-BtjPI0Cc.js +0 -4
- package/client/dist/assets/TicketDetailModalContext-Dea22CI-.js +0 -16
- package/client/dist/assets/Trans-CAY2Rd2f.js +0 -1
- package/client/dist/assets/ban-E4EaZe3a.js +0 -1
- package/client/dist/assets/chevron-up-CwGUHd6D.js +0 -1
- package/client/dist/assets/chunk-KS7C4IRE-DEKu087k.js +0 -3
- package/client/dist/assets/circle-x-CMYsN4ZH.js +0 -1
- package/client/dist/assets/createLucideIcon-Bjbbp_KS.js +0 -1
- package/client/dist/assets/dist-Bq10nS2m.js +0 -1
- package/client/dist/assets/download-CFHyyHcw.js +0 -1
- package/client/dist/assets/external-link-D6XxC_Cu.js +0 -1
- package/client/dist/assets/file-text-BcLCfcWZ.js +0 -1
- package/client/dist/assets/git-branch-BRM0F5GN.js +0 -1
- package/client/dist/assets/index-BacrO_ub.css +0 -2
- package/client/dist/assets/jira-api-C94VNHNY.js +0 -1
- package/client/dist/assets/jsx-runtime-zu2_FqZY.js +0 -1
- package/client/dist/assets/layers-eDMjG41I.js +0 -1
- package/client/dist/assets/loader-circle-DcygOQpC.js +0 -1
- package/client/dist/assets/pencil-C9RkrIPZ.js +0 -1
- package/client/dist/assets/plus-CsnjfkmX.js +0 -1
- package/client/dist/assets/react-dom-DiWzBDP9.js +0 -1
- package/client/dist/assets/rotate-ccw-Wh4id13q.js +0 -1
- package/client/dist/assets/save-B6wGWwBR.js +0 -1
- package/client/dist/assets/search-Cq_gE3Ik.js +0 -1
- package/client/dist/assets/sparkles-DrY8hb0X.js +0 -1
- package/client/dist/assets/spending-DemeFfrH.js +0 -1
- package/client/dist/assets/square-Ci-PFcFe.js +0 -1
- package/client/dist/assets/trash-2-BOU3WdZZ.js +0 -1
- package/client/dist/assets/triangle-alert-H9T4jAmh.js +0 -1
- package/client/dist/assets/upload-DYMjAQ6M.js +0 -1
- package/client/dist/assets/useSharedWebSocket-BEHPeU1Y.js +0 -2
- package/client/dist/assets/x-CBMI9ejn.js +0 -1
- package/client/dist/assets/zap-CHo3aCD1.js +0 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.registerProjectSettingsHttp = registerProjectSettingsHttp;
|
|
4
|
+
exports.registerEnvPassthroughStatusHttp = registerEnvPassthroughStatusHttp;
|
|
4
5
|
const __1 = require("..");
|
|
5
6
|
/** Inbound adapter: translate HTTP into use cases and domain errors into HTTP. */
|
|
6
7
|
function registerProjectSettingsHttp(router, serviceForRequest,
|
|
@@ -34,3 +35,26 @@ onUpdated) {
|
|
|
34
35
|
}
|
|
35
36
|
});
|
|
36
37
|
}
|
|
38
|
+
/** Inbound adapter for `GET /:projectId/env-passthrough/status` and
|
|
39
|
+
* `POST /:projectId/env-passthrough/recheck`. Responses carry names and
|
|
40
|
+
* states only; values never reach this adapter. */
|
|
41
|
+
function registerEnvPassthroughStatusHttp(router, port) {
|
|
42
|
+
router.get('/:projectId/env-passthrough/status', async (req, res) => {
|
|
43
|
+
try {
|
|
44
|
+
res.json(await port.read(req));
|
|
45
|
+
}
|
|
46
|
+
catch (error) {
|
|
47
|
+
console.error('[project-router] env status error:', error);
|
|
48
|
+
res.status(500).json({ error: 'Failed to read environment status' });
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
router.post('/:projectId/env-passthrough/recheck', async (req, res) => {
|
|
52
|
+
try {
|
|
53
|
+
res.json(await port.recheck(req));
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
console.error('[project-router] env recheck error:', error);
|
|
57
|
+
res.status(500).json({ error: 'Failed to recheck environment' });
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -3,7 +3,7 @@ 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.AUTH_ENV_VARS = void 0;
|
|
6
|
+
exports.LOGIN_SHELL_PROBE_TIMEOUT_MS = exports.AUTH_ENV_VARS = void 0;
|
|
7
7
|
exports.ensureWindowsBaseEnv = ensureWindowsBaseEnv;
|
|
8
8
|
exports.resolveBundledRuntimePath = resolveBundledRuntimePath;
|
|
9
9
|
exports.resolveBundledNodeExe = resolveBundledNodeExe;
|
|
@@ -12,6 +12,10 @@ exports.parseLoginShellOutput = parseLoginShellOutput;
|
|
|
12
12
|
exports.augmentPathFromLoginShell = augmentPathFromLoginShell;
|
|
13
13
|
exports.parseLoginShellEnv = parseLoginShellEnv;
|
|
14
14
|
exports.augmentEnvFromLoginShell = augmentEnvFromLoginShell;
|
|
15
|
+
exports.resolveLoginShellProbeTimeoutMs = resolveLoginShellProbeTimeoutMs;
|
|
16
|
+
exports.hasCompleteLoginShellEnvBlock = hasCompleteLoginShellEnvBlock;
|
|
17
|
+
exports.readEnvFromLoginShell = readEnvFromLoginShell;
|
|
18
|
+
exports.readEnvFromLoginShellSyncDetailed = readEnvFromLoginShellSyncDetailed;
|
|
15
19
|
exports.readEnvFromLoginShellSync = readEnvFromLoginShellSync;
|
|
16
20
|
exports.augmentEnvFromLoginShellSync = augmentEnvFromLoginShellSync;
|
|
17
21
|
exports.augmentAuthEnvFromLoginShell = augmentAuthEnvFromLoginShell;
|
|
@@ -580,17 +584,130 @@ async function augmentEnvFromLoginShell(names, opts = {}) {
|
|
|
580
584
|
});
|
|
581
585
|
});
|
|
582
586
|
}
|
|
583
|
-
/**
|
|
584
|
-
*
|
|
585
|
-
|
|
587
|
+
/** Default budget for the asynchronous project-env probe. Real interactive
|
|
588
|
+
* profiles (oh-my-zsh, nvm, powerlevel10k, conda) routinely exceed 1.5 s. */
|
|
589
|
+
exports.LOGIN_SHELL_PROBE_TIMEOUT_MS = 10_000;
|
|
590
|
+
const LOGIN_SHELL_PROBE_MAX_TIMEOUT_MS = 120_000;
|
|
591
|
+
const LOGIN_SHELL_OUTPUT_CAP = 1024 * 1024;
|
|
592
|
+
/** `SPECRAILS_LOGIN_SHELL_TIMEOUT_MS` overrides the asynchronous probe budget.
|
|
593
|
+
* Invalid or non-positive values fall back to the 10 s default; values are
|
|
594
|
+
* capped at 2 minutes so a typo cannot park a probe forever. */
|
|
595
|
+
function resolveLoginShellProbeTimeoutMs(env = process.env) {
|
|
596
|
+
const raw = env.SPECRAILS_LOGIN_SHELL_TIMEOUT_MS?.trim();
|
|
597
|
+
if (!raw || !/^\d+$/.test(raw))
|
|
598
|
+
return exports.LOGIN_SHELL_PROBE_TIMEOUT_MS;
|
|
599
|
+
const value = Number(raw);
|
|
600
|
+
if (!Number.isFinite(value) || value <= 0)
|
|
601
|
+
return exports.LOGIN_SHELL_PROBE_TIMEOUT_MS;
|
|
602
|
+
return Math.min(value, LOGIN_SHELL_PROBE_MAX_TIMEOUT_MS);
|
|
603
|
+
}
|
|
604
|
+
/** True when the probe output contains the complete env sentinel block. The
|
|
605
|
+
* block is the proof that the printf ran, independent of the shell's status. */
|
|
606
|
+
function hasCompleteLoginShellEnvBlock(stdout) {
|
|
607
|
+
const begin = stdout.indexOf(ENV_BEGIN);
|
|
608
|
+
return begin !== -1 && stdout.indexOf(ENV_END, begin + ENV_BEGIN.length) !== -1;
|
|
609
|
+
}
|
|
610
|
+
function loginShellProbeSkipped(names = []) {
|
|
611
|
+
return { values: {}, status: 'skipped', exitCode: null, shell: null, names };
|
|
612
|
+
}
|
|
613
|
+
function pickRecovered(stdout, wanted) {
|
|
614
|
+
const recovered = parseLoginShellEnv(stdout);
|
|
615
|
+
const out = {};
|
|
616
|
+
for (const key of wanted) {
|
|
617
|
+
const val = recovered[key];
|
|
618
|
+
if (val)
|
|
619
|
+
out[key] = val;
|
|
620
|
+
}
|
|
621
|
+
return out;
|
|
622
|
+
}
|
|
623
|
+
function settleLoginShellProbe(stdout, wanted, shell, exitCode, timedOut) {
|
|
624
|
+
if (hasCompleteLoginShellEnvBlock(stdout)) {
|
|
625
|
+
return { values: pickRecovered(stdout, wanted), status: 'ok', exitCode, shell, names: wanted };
|
|
626
|
+
}
|
|
627
|
+
return { values: {}, status: timedOut ? 'timeout' : 'failed', exitCode, shell, names: wanted };
|
|
628
|
+
}
|
|
629
|
+
function wantedLoginShellNames(names, env) {
|
|
586
630
|
if (process.platform === 'win32')
|
|
587
|
-
return
|
|
631
|
+
return null;
|
|
588
632
|
if (process.env.NODE_ENV === 'test' || process.env.VITEST === 'true')
|
|
589
|
-
return
|
|
633
|
+
return null;
|
|
634
|
+
return normalizeLoginShellEnvNames(names).filter((name) => !env[name]);
|
|
635
|
+
}
|
|
636
|
+
/** Asynchronously read missing configured names from the login shell without
|
|
637
|
+
* blocking the event loop or mutating any environment. Values from a complete
|
|
638
|
+
* sentinel block are accepted regardless of the shell's exit status. Output is
|
|
639
|
+
* capped at 1 MiB; the child is killed at the timeout. Callers own any cache. */
|
|
640
|
+
function readEnvFromLoginShell(names, opts = {}) {
|
|
590
641
|
const env = opts.env ?? process.env;
|
|
591
|
-
const wanted =
|
|
642
|
+
const wanted = wantedLoginShellNames(names, env);
|
|
643
|
+
if (wanted === null)
|
|
644
|
+
return Promise.resolve(loginShellProbeSkipped());
|
|
592
645
|
if (wanted.length === 0)
|
|
593
|
-
return
|
|
646
|
+
return Promise.resolve(loginShellProbeSkipped(wanted));
|
|
647
|
+
const spawnFn = opts.spawnFn ?? child_process_1.spawn;
|
|
648
|
+
const timeoutMs = opts.timeoutMs ?? resolveLoginShellProbeTimeoutMs(env);
|
|
649
|
+
const shell = resolveLoginShell(env);
|
|
650
|
+
const command = buildLoginShellEnvCommand(wanted);
|
|
651
|
+
return new Promise((resolve) => {
|
|
652
|
+
let child;
|
|
653
|
+
try {
|
|
654
|
+
child = spawnFn(shell, ['-l', '-i', '-c', command], { env, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
resolve({ values: {}, status: 'failed', exitCode: null, shell, names: wanted });
|
|
658
|
+
return;
|
|
659
|
+
}
|
|
660
|
+
let stdout = '';
|
|
661
|
+
let timedOut = false;
|
|
662
|
+
let settled = false;
|
|
663
|
+
const kill = () => { try {
|
|
664
|
+
child.kill('SIGKILL');
|
|
665
|
+
}
|
|
666
|
+
catch { /* ignore */ } };
|
|
667
|
+
const timer = setTimeout(() => {
|
|
668
|
+
timedOut = true;
|
|
669
|
+
kill();
|
|
670
|
+
// Settle now: a background job started by the profile can keep the pipes
|
|
671
|
+
// (and so 'close') open long after the shell itself was killed. A complete
|
|
672
|
+
// block printed before a hung exit hook is still a success.
|
|
673
|
+
finish(null);
|
|
674
|
+
}, timeoutMs);
|
|
675
|
+
const finish = (exitCode) => {
|
|
676
|
+
if (settled)
|
|
677
|
+
return;
|
|
678
|
+
settled = true;
|
|
679
|
+
clearTimeout(timer);
|
|
680
|
+
resolve(settleLoginShellProbe(stdout, wanted, shell, exitCode, timedOut));
|
|
681
|
+
};
|
|
682
|
+
child.stdout?.on('data', (chunk) => {
|
|
683
|
+
if (stdout.length >= LOGIN_SHELL_OUTPUT_CAP)
|
|
684
|
+
return;
|
|
685
|
+
stdout += chunk.toString('utf-8');
|
|
686
|
+
if (stdout.length >= LOGIN_SHELL_OUTPUT_CAP) {
|
|
687
|
+
stdout = stdout.slice(0, LOGIN_SHELL_OUTPUT_CAP);
|
|
688
|
+
if (!hasCompleteLoginShellEnvBlock(stdout))
|
|
689
|
+
kill();
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
child.stderr?.on('data', () => { });
|
|
693
|
+
child.on('error', () => finish(null));
|
|
694
|
+
child.on('close', (code) => finish(typeof code === 'number' ? code : null));
|
|
695
|
+
});
|
|
696
|
+
}
|
|
697
|
+
function isSpawnSyncTimeout(res) {
|
|
698
|
+
const error = res.error;
|
|
699
|
+
return Boolean(error && (error.code === 'ETIMEDOUT' || /ETIMEDOUT/.test(error.message)));
|
|
700
|
+
}
|
|
701
|
+
/** Synchronous, detailed variant used only as the cold fallback at spawn time.
|
|
702
|
+
* Keeps the short 1.5 s budget so a cold spawn never freezes the server for
|
|
703
|
+
* long, and accepts a complete sentinel block regardless of exit status. */
|
|
704
|
+
function readEnvFromLoginShellSyncDetailed(names, opts = {}) {
|
|
705
|
+
const env = opts.env ?? process.env;
|
|
706
|
+
const wanted = wantedLoginShellNames(names, env);
|
|
707
|
+
if (wanted === null)
|
|
708
|
+
return loginShellProbeSkipped();
|
|
709
|
+
if (wanted.length === 0)
|
|
710
|
+
return loginShellProbeSkipped(wanted);
|
|
594
711
|
const spawnSyncFn = opts.spawnSyncFn ?? child_process_1.spawnSync;
|
|
595
712
|
const timeoutMs = opts.timeoutMs ?? LOGIN_SHELL_TIMEOUT_MS;
|
|
596
713
|
const shell = resolveLoginShell(env);
|
|
@@ -602,23 +719,21 @@ function readEnvFromLoginShellSync(names, opts = {}) {
|
|
|
602
719
|
encoding: 'utf8',
|
|
603
720
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
604
721
|
timeout: timeoutMs,
|
|
605
|
-
|
|
722
|
+
// An interactive shell can ignore SIGTERM; the cold fallback must stay bounded.
|
|
723
|
+
killSignal: 'SIGKILL',
|
|
724
|
+
maxBuffer: LOGIN_SHELL_OUTPUT_CAP,
|
|
606
725
|
});
|
|
607
726
|
}
|
|
608
727
|
catch {
|
|
609
|
-
return {};
|
|
728
|
+
return { values: {}, status: 'failed', exitCode: null, shell, names: wanted };
|
|
610
729
|
}
|
|
611
|
-
if (res.error || res.status !== 0)
|
|
612
|
-
return {};
|
|
613
730
|
const stdout = typeof res.stdout === 'string' ? res.stdout : res.stdout?.toString('utf8') ?? '';
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
}
|
|
621
|
-
return out;
|
|
731
|
+
return settleLoginShellProbe(stdout, wanted, shell, typeof res.status === 'number' ? res.status : null, isSpawnSyncTimeout(res));
|
|
732
|
+
}
|
|
733
|
+
/** Read missing configured names without mutating the caller's or global env.
|
|
734
|
+
* Callers own any cache so project credentials cannot leak across scopes. */
|
|
735
|
+
function readEnvFromLoginShellSync(names, opts = {}) {
|
|
736
|
+
return readEnvFromLoginShellSyncDetailed(names, opts).values;
|
|
622
737
|
}
|
|
623
738
|
/** Compatibility backfill for callers that intentionally augment process.env.
|
|
624
739
|
* Failed probes remain retryable; successful names are already present. */
|
|
@@ -1,37 +1,237 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PROJECT_ENV_FAILURE_TTL_MS = exports.PROJECT_ENV_SUCCESS_TTL_MS = void 0;
|
|
4
|
+
exports.refreshProjectEnv = refreshProjectEnv;
|
|
5
|
+
exports.ensureProjectEnvFresh = ensureProjectEnvFresh;
|
|
6
|
+
exports.startProjectEnvWarmup = startProjectEnvWarmup;
|
|
7
|
+
exports.stopProjectEnvWarmup = stopProjectEnvWarmup;
|
|
3
8
|
exports.resolveWorktreeEnvPassthrough = resolveWorktreeEnvPassthrough;
|
|
4
9
|
exports.applyWorktreeEnvPassthrough = applyWorktreeEnvPassthrough;
|
|
10
|
+
exports.getProjectEnvStatus = getProjectEnvStatus;
|
|
11
|
+
exports.worktreeEnvPassthroughWarning = worktreeEnvPassthroughWarning;
|
|
5
12
|
const db_1 = require("./db");
|
|
6
13
|
const path_resolver_1 = require("./path-resolver");
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
14
|
+
/** Successful recoveries stay valid for 10 minutes; the background refresh
|
|
15
|
+
* rotates them well inside that window. */
|
|
16
|
+
exports.PROJECT_ENV_SUCCESS_TTL_MS = 10 * 60_000;
|
|
17
|
+
/** Failed or empty lookups are retried after 30 seconds. */
|
|
18
|
+
exports.PROJECT_ENV_FAILURE_TTL_MS = 30_000;
|
|
19
|
+
/** The background refresh runs this long before a success expires. */
|
|
20
|
+
const PROJECT_ENV_REFRESH_LEAD_MS = 60_000;
|
|
21
|
+
const states = new WeakMap();
|
|
22
|
+
function shellIdentity() {
|
|
23
|
+
return JSON.stringify([process.env.SHELL, process.env.HOME, process.env.ZDOTDIR]);
|
|
24
|
+
}
|
|
25
|
+
function stateFor(db) {
|
|
26
|
+
const identity = shellIdentity();
|
|
27
|
+
let state = states.get(db);
|
|
28
|
+
if (!state) {
|
|
29
|
+
state = { identity, records: new Map(), values: new Map(), inflight: null, queued: null, timer: null, active: false };
|
|
30
|
+
states.set(db, state);
|
|
31
|
+
}
|
|
32
|
+
else if (state.identity !== identity) {
|
|
33
|
+
state.identity = identity;
|
|
34
|
+
state.records.clear();
|
|
35
|
+
state.values.clear();
|
|
36
|
+
}
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
function configuredNames(db) {
|
|
40
|
+
try {
|
|
41
|
+
return (0, db_1.getProjectSettings)(db).worktreeEnvPassthrough;
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// A closed/unloaded project connection has no configured names.
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function missingNames(names) {
|
|
49
|
+
return names.filter(name => !process.env[name]);
|
|
50
|
+
}
|
|
51
|
+
/** Drop records for names no longer configured or now inherited. */
|
|
52
|
+
function prune(state, names) {
|
|
53
|
+
for (const name of [...state.records.keys()]) {
|
|
54
|
+
if (!names.includes(name) || process.env[name]) {
|
|
55
|
+
state.records.delete(name);
|
|
56
|
+
state.values.delete(name);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function isValid(record, now) {
|
|
61
|
+
return Boolean(record && now < record.expiresAt);
|
|
62
|
+
}
|
|
63
|
+
function statusFromProbe(probe, value) {
|
|
64
|
+
if (value)
|
|
65
|
+
return 'recovered';
|
|
66
|
+
if (probe.status === 'timeout')
|
|
67
|
+
return 'probe-timeout';
|
|
68
|
+
if (probe.status === 'failed')
|
|
69
|
+
return 'probe-failed';
|
|
70
|
+
// `ok` without a value, or `skipped` (Windows / no login-shell recovery).
|
|
71
|
+
return 'not-defined';
|
|
72
|
+
}
|
|
73
|
+
function recordProbe(state, probe, names, now) {
|
|
74
|
+
for (const name of names) {
|
|
75
|
+
const value = probe.values[name];
|
|
76
|
+
const status = statusFromProbe(probe, value);
|
|
77
|
+
const previous = state.records.get(name);
|
|
78
|
+
// A transient probe failure never discards a still-valid recovered value.
|
|
79
|
+
if (status !== 'recovered' && previous?.status === 'recovered' && isValid(previous, now) && probe.status !== 'ok')
|
|
80
|
+
continue;
|
|
81
|
+
state.records.set(name, {
|
|
82
|
+
status,
|
|
83
|
+
shell: probe.shell,
|
|
84
|
+
checkedAt: now,
|
|
85
|
+
expiresAt: now + (status === 'recovered' ? exports.PROJECT_ENV_SUCCESS_TTL_MS : exports.PROJECT_ENV_FAILURE_TTL_MS),
|
|
86
|
+
exitCode: probe.exitCode,
|
|
87
|
+
});
|
|
88
|
+
if (value)
|
|
89
|
+
state.values.set(name, value);
|
|
90
|
+
else
|
|
91
|
+
state.values.delete(name);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function scheduleRefresh(db, state) {
|
|
95
|
+
if (state.timer) {
|
|
96
|
+
clearTimeout(state.timer);
|
|
97
|
+
state.timer = null;
|
|
98
|
+
}
|
|
99
|
+
if (!state.active || states.get(db) !== state)
|
|
100
|
+
return;
|
|
101
|
+
let earliest = Infinity;
|
|
102
|
+
for (const record of state.records.values()) {
|
|
103
|
+
if (record.status === 'recovered')
|
|
104
|
+
earliest = Math.min(earliest, record.expiresAt);
|
|
105
|
+
}
|
|
106
|
+
if (earliest === Infinity)
|
|
107
|
+
return;
|
|
108
|
+
const now = Date.now();
|
|
109
|
+
const at = Math.max(now + exports.PROJECT_ENV_FAILURE_TTL_MS, earliest - PROJECT_ENV_REFRESH_LEAD_MS);
|
|
110
|
+
state.timer = setTimeout(() => {
|
|
111
|
+
state.timer = null;
|
|
112
|
+
if (state.active && states.get(db) === state)
|
|
113
|
+
void refreshProjectEnv(db);
|
|
114
|
+
}, at - now);
|
|
115
|
+
state.timer.unref?.();
|
|
116
|
+
}
|
|
117
|
+
/** Probe the login shell asynchronously for every configured name missing from
|
|
118
|
+
* the process environment and update this project's records. De-duplicated
|
|
119
|
+
* while a probe is in flight: a call during a probe resolves after one more
|
|
120
|
+
* probe that starts when the current one ends, so a settings change or an
|
|
121
|
+
* explicit recheck is never answered with stale results. Never rejects. */
|
|
122
|
+
function refreshProjectEnv(db) {
|
|
123
|
+
const state = stateFor(db);
|
|
124
|
+
if (state.inflight) {
|
|
125
|
+
// A request during a probe (settings change, recheck) gets a fresh probe
|
|
126
|
+
// after the current one; concurrent requests share that single follow-up.
|
|
127
|
+
if (!state.queued) {
|
|
128
|
+
state.queued = state.inflight.then(() => {
|
|
129
|
+
state.queued = null;
|
|
130
|
+
return refreshProjectEnv(db);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
return state.queued;
|
|
134
|
+
}
|
|
135
|
+
const names = configuredNames(db);
|
|
136
|
+
prune(state, names);
|
|
137
|
+
const missing = missingNames(names);
|
|
138
|
+
if (missing.length === 0) {
|
|
139
|
+
scheduleRefresh(db, state);
|
|
140
|
+
return Promise.resolve();
|
|
141
|
+
}
|
|
142
|
+
const run = (0, path_resolver_1.readEnvFromLoginShell)(missing, { timeoutMs: (0, path_resolver_1.resolveLoginShellProbeTimeoutMs)() })
|
|
143
|
+
.then(probe => {
|
|
144
|
+
if (states.get(db) !== state)
|
|
145
|
+
return;
|
|
146
|
+
const current = configuredNames(db);
|
|
147
|
+
prune(state, current);
|
|
148
|
+
// Only names still configured and still missing are recorded.
|
|
149
|
+
const stillMissing = missingNames(current);
|
|
150
|
+
recordProbe(state, probe, missing.filter(name => stillMissing.includes(name)), Date.now());
|
|
151
|
+
})
|
|
152
|
+
.catch(() => { })
|
|
153
|
+
.finally(() => {
|
|
154
|
+
state.inflight = null;
|
|
155
|
+
scheduleRefresh(db, state);
|
|
156
|
+
});
|
|
157
|
+
state.inflight = run;
|
|
158
|
+
return run;
|
|
159
|
+
}
|
|
160
|
+
/** Probe only when a configured missing name has no valid record yet. */
|
|
161
|
+
function ensureProjectEnvFresh(db) {
|
|
162
|
+
const state = stateFor(db);
|
|
163
|
+
if (state.inflight)
|
|
164
|
+
return state.inflight;
|
|
165
|
+
const names = configuredNames(db);
|
|
166
|
+
prune(state, names);
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
const stale = missingNames(names).some(name => !isValid(state.records.get(name), now));
|
|
169
|
+
return stale ? refreshProjectEnv(db) : Promise.resolve();
|
|
170
|
+
}
|
|
171
|
+
/** Start background warm-up for a loaded project: probe now, then refresh
|
|
172
|
+
* before successful results expire, until `stopProjectEnvWarmup`. */
|
|
173
|
+
function startProjectEnvWarmup(db) {
|
|
174
|
+
const state = stateFor(db);
|
|
175
|
+
state.active = true;
|
|
176
|
+
void refreshProjectEnv(db);
|
|
177
|
+
}
|
|
178
|
+
/** Stop the background refresh and forget this project's recovered values. */
|
|
179
|
+
function stopProjectEnvWarmup(db) {
|
|
180
|
+
const state = states.get(db);
|
|
181
|
+
if (!state)
|
|
182
|
+
return;
|
|
183
|
+
state.active = false;
|
|
184
|
+
if (state.timer) {
|
|
185
|
+
clearTimeout(state.timer);
|
|
186
|
+
state.timer = null;
|
|
187
|
+
}
|
|
188
|
+
states.delete(db);
|
|
189
|
+
}
|
|
11
190
|
function recoverProjectEnv(db, names) {
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
191
|
+
const state = stateFor(db);
|
|
192
|
+
prune(state, names);
|
|
193
|
+
const missing = missingNames(names);
|
|
194
|
+
if (!missing.length)
|
|
15
195
|
return {};
|
|
196
|
+
const now = Date.now();
|
|
197
|
+
// Cold fallback: a name with no record (or an expired success) gets the
|
|
198
|
+
// short synchronous probe. Expired failures are retried asynchronously.
|
|
199
|
+
const cold = missing.filter(name => {
|
|
200
|
+
const record = state.records.get(name);
|
|
201
|
+
return !record || (record.status === 'recovered' && !isValid(record, now));
|
|
202
|
+
});
|
|
203
|
+
let retry = missing.some(name => {
|
|
204
|
+
const record = state.records.get(name);
|
|
205
|
+
return record !== undefined && record.status !== 'recovered' && !isValid(record, now);
|
|
206
|
+
});
|
|
207
|
+
if (cold.length) {
|
|
208
|
+
const probe = (0, path_resolver_1.readEnvFromLoginShellSyncDetailed)(cold);
|
|
209
|
+
recordProbe(state, probe, cold, now);
|
|
210
|
+
// The async probe has a larger budget than the cold sync one.
|
|
211
|
+
if (probe.status === 'timeout' || probe.status === 'failed')
|
|
212
|
+
retry = true;
|
|
213
|
+
}
|
|
214
|
+
if (retry)
|
|
215
|
+
void refreshProjectEnv(db);
|
|
216
|
+
const out = {};
|
|
217
|
+
for (const name of missing) {
|
|
218
|
+
const value = state.values.get(name);
|
|
219
|
+
if (value && isValid(state.records.get(name), now))
|
|
220
|
+
out[name] = value;
|
|
16
221
|
}
|
|
17
|
-
|
|
18
|
-
const cached = shellValues.get(db);
|
|
19
|
-
if (cached?.identity === identity && Date.now() < cached.expiresAt)
|
|
20
|
-
return cached.env;
|
|
21
|
-
const env = (0, path_resolver_1.readEnvFromLoginShellSync)(missing);
|
|
22
|
-
shellValues.set(db, { identity, expiresAt: Date.now() + LOGIN_SHELL_CACHE_MS, env });
|
|
23
|
-
return env;
|
|
222
|
+
return out;
|
|
24
223
|
}
|
|
25
224
|
/** Resolve the per-project env passthrough overlay for a spawn.
|
|
26
225
|
*
|
|
27
226
|
* The project setting stores names only. Values are read from `sourceEnv` at
|
|
28
227
|
* spawn time so credentials can rotate without touching SQLite. When the
|
|
29
|
-
* source is `process.env`, missing configured names
|
|
30
|
-
*
|
|
228
|
+
* source is `process.env`, missing configured names come from the project's
|
|
229
|
+
* warm login-shell cache; the short synchronous probe runs only when cold.
|
|
230
|
+
* Unresolved names remain absent.
|
|
31
231
|
*/
|
|
32
232
|
function resolveWorktreeEnvPassthrough(db, sourceEnv = process.env) {
|
|
33
233
|
const out = {};
|
|
34
|
-
const names = (
|
|
234
|
+
const names = configuredNames(db);
|
|
35
235
|
const recovered = sourceEnv === process.env ? recoverProjectEnv(db, names) : {};
|
|
36
236
|
for (const name of names) {
|
|
37
237
|
const value = sourceEnv[name] || recovered[name] || sourceEnv[name];
|
|
@@ -44,3 +244,51 @@ function applyWorktreeEnvPassthrough(db, env, sourceEnv = process.env) {
|
|
|
44
244
|
const passthrough = resolveWorktreeEnvPassthrough(db, sourceEnv);
|
|
45
245
|
return Object.keys(passthrough).length === 0 ? env : { ...env, ...passthrough };
|
|
46
246
|
}
|
|
247
|
+
/** Value-free status of every configured name for this project. */
|
|
248
|
+
function getProjectEnvStatus(db) {
|
|
249
|
+
const state = stateFor(db);
|
|
250
|
+
const names = configuredNames(db);
|
|
251
|
+
prune(state, names);
|
|
252
|
+
const nowIso = new Date(Date.now()).toISOString();
|
|
253
|
+
return {
|
|
254
|
+
loginShellRecovery: process.platform !== 'win32',
|
|
255
|
+
timeoutMs: (0, path_resolver_1.resolveLoginShellProbeTimeoutMs)(),
|
|
256
|
+
checking: state.inflight !== null,
|
|
257
|
+
names: names.map((name) => {
|
|
258
|
+
if (process.env[name])
|
|
259
|
+
return { name, status: 'inherited', shell: null, checkedAt: nowIso, exitCode: null };
|
|
260
|
+
const record = state.records.get(name);
|
|
261
|
+
if (!record)
|
|
262
|
+
return { name, status: 'pending', shell: null, checkedAt: null, exitCode: null };
|
|
263
|
+
return {
|
|
264
|
+
name,
|
|
265
|
+
status: record.status,
|
|
266
|
+
shell: record.shell,
|
|
267
|
+
checkedAt: new Date(record.checkedAt).toISOString(),
|
|
268
|
+
exitCode: record.exitCode,
|
|
269
|
+
};
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
/** One aggregated, value-free run-log line naming every configured name that
|
|
274
|
+
* is absent from `env`, or null when all configured names resolved. */
|
|
275
|
+
function worktreeEnvPassthroughWarning(db, env) {
|
|
276
|
+
const names = configuredNames(db);
|
|
277
|
+
const unresolved = names.filter(name => !env[name]);
|
|
278
|
+
if (!unresolved.length)
|
|
279
|
+
return null;
|
|
280
|
+
const state = states.get(db);
|
|
281
|
+
const now = Date.now();
|
|
282
|
+
const parts = unresolved.map(name => {
|
|
283
|
+
const record = state?.records.get(name);
|
|
284
|
+
// An expired success no longer counts as recovered; failures keep their cause.
|
|
285
|
+
const status = !record ? 'not-defined' : record.status === 'recovered' && now >= record.expiresAt ? 'not-defined' : record.status;
|
|
286
|
+
return { name, status };
|
|
287
|
+
});
|
|
288
|
+
const hint = process.platform === 'win32'
|
|
289
|
+
? 'set it in the environment Specrails starts with'
|
|
290
|
+
: 'configure it in the login shell or launch from a terminal';
|
|
291
|
+
if (parts.length === 1)
|
|
292
|
+
return `[environment] ${parts[0].name} not available (${parts[0].status}); ${hint}`;
|
|
293
|
+
return `[environment] ${parts.map(p => `${p.name} (${p.status})`).join(', ')} not available; ${hint.replace(' it ', ' them ')}`;
|
|
294
|
+
}
|
|
@@ -390,6 +390,11 @@ class ProjectRegistry {
|
|
|
390
390
|
(0, telemetry_receiver_1.dropBlobStatesForProject)(id);
|
|
391
391
|
}
|
|
392
392
|
catch { /* ignore */ }
|
|
393
|
+
// Stop the login-shell env refresh and forget recovered values.
|
|
394
|
+
try {
|
|
395
|
+
(0, project_env_1.stopProjectEnvWarmup)(ctx.db);
|
|
396
|
+
}
|
|
397
|
+
catch { /* ignore */ }
|
|
393
398
|
// Close the DB connection BEFORE removing the project's data dir below.
|
|
394
399
|
try {
|
|
395
400
|
ctx.db.close();
|
|
@@ -639,6 +644,10 @@ class ProjectRegistry {
|
|
|
639
644
|
ctx.fileSummaryManager.dispose();
|
|
640
645
|
}
|
|
641
646
|
catch { /* ignore */ }
|
|
647
|
+
try {
|
|
648
|
+
(0, project_env_1.stopProjectEnvWarmup)(ctx.db);
|
|
649
|
+
}
|
|
650
|
+
catch { /* ignore */ }
|
|
642
651
|
ctx.ticketWatcher.close().catch(() => { });
|
|
643
652
|
}
|
|
644
653
|
// Close the shared browser context once, after every per-project manager has
|
|
@@ -1152,6 +1161,8 @@ class ProjectRegistry {
|
|
|
1152
1161
|
sourcePath: () => (0, project_repositories_1.resolveProjectRepository)(project).path,
|
|
1153
1162
|
workspacePaths: () => { const primary = (0, project_repositories_1.resolveProjectRepository)(project); return primary.workspacePaths ?? (primary.workspacePath ? [primary.workspacePath] : undefined); },
|
|
1154
1163
|
env: () => (0, workspace_resolution_1.resolveLoopBaseEnv)({ slug: project.slug, path: project.path }, undefined, (0, project_env_1.applyWorktreeEnvPassthrough)(db, process.env)),
|
|
1164
|
+
// One value-free run-log line when configured names are unresolved.
|
|
1165
|
+
environmentNotice: () => (0, project_env_1.worktreeEnvPassthroughWarning)(db, (0, project_env_1.applyWorktreeEnvPassthrough)(db, process.env)),
|
|
1155
1166
|
// Named rail profiles and global per-agent defaults ride the same
|
|
1156
1167
|
// SPECRAILS_PROFILE_PATH snapshot in both AI transports. An explicit null
|
|
1157
1168
|
// rail selection opts out; omitted selections keep Core's default rules.
|
|
@@ -1486,6 +1497,9 @@ class ProjectRegistry {
|
|
|
1486
1497
|
const ctx = { project, db, queueManager, chatManager, setupManager, proposalManager, agentRefineManager, fileSummaryManager, specLauncherManager, ticketWatcher, browserCaptureManager, jiraSyncManager, stuckRunDetector, broadcast: boundBroadcast, railJobs, loopRunManager, railLoopRuns, onLoopRunFinished, getTicketSpec, desktopDb: this._desktopDb, milestoneProgress };
|
|
1487
1498
|
ctx.milestoneChains = milestoneChains;
|
|
1488
1499
|
this._contexts.set(project.id, ctx);
|
|
1500
|
+
// Warm the project's login-shell env cache off the event loop so the first
|
|
1501
|
+
// rail/loop spawn reads recovered credentials instead of a cold 1.5 s probe.
|
|
1502
|
+
(0, project_env_1.startProjectEnvWarmup)(db);
|
|
1489
1503
|
const recoverAfterProbe = (definitionStates) => {
|
|
1490
1504
|
if (this._contexts.get(project.id) !== ctx)
|
|
1491
1505
|
return;
|
|
@@ -9,6 +9,7 @@ exports.registerSettingsRoutes = registerSettingsRoutes;
|
|
|
9
9
|
const project_settings_1 = require("./modules/project-settings");
|
|
10
10
|
const sqlite_1 = require("./modules/project-settings/adapters/sqlite");
|
|
11
11
|
const http_1 = require("./modules/project-settings/adapters/http");
|
|
12
|
+
const project_env_1 = require("./project-env");
|
|
12
13
|
const fs_1 = __importDefault(require("fs"));
|
|
13
14
|
const path_1 = __importDefault(require("path"));
|
|
14
15
|
const db_1 = require("./db");
|
|
@@ -35,6 +36,22 @@ function registerSettingsRoutes(deps) {
|
|
|
35
36
|
const runtimeChanged = JSON.stringify(settings.subagentRuntime) !== JSON.stringify(previous.subagentRuntime);
|
|
36
37
|
if (settings.allowSubagents !== previous.allowSubagents || runtimeChanged)
|
|
37
38
|
registry.notifySettingsChanged({ projectId: String(req.params.projectId) });
|
|
39
|
+
// Configured env names changed: re-warm this project's login-shell cache.
|
|
40
|
+
if (JSON.stringify(settings.worktreeEnvPassthrough) !== JSON.stringify(previous.worktreeEnvPassthrough))
|
|
41
|
+
void (0, project_env_1.refreshProjectEnv)(ctx(req).db);
|
|
42
|
+
});
|
|
43
|
+
// Value-free resolution status of the configured env names (+ recheck).
|
|
44
|
+
(0, http_1.registerEnvPassthroughStatusHttp)(router, {
|
|
45
|
+
read: async (req) => {
|
|
46
|
+
const { db } = ctx(req);
|
|
47
|
+
await (0, project_env_1.ensureProjectEnvFresh)(db);
|
|
48
|
+
return (0, project_env_1.getProjectEnvStatus)(db);
|
|
49
|
+
},
|
|
50
|
+
recheck: async (req) => {
|
|
51
|
+
const { db } = ctx(req);
|
|
52
|
+
await (0, project_env_1.refreshProjectEnv)(db);
|
|
53
|
+
return (0, project_env_1.getProjectEnvStatus)(db);
|
|
54
|
+
},
|
|
38
55
|
});
|
|
39
56
|
// ─── Per-project Quick mode Contract Refine last-used value ─────────────────
|
|
40
57
|
router.get('/:projectId/add-spec-quick-contract-refine-last', (req, res) => {
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
"planning": { "enum": ["full", "proportional"] }, "acceptDeveloperChecks": { "type": "boolean" },
|
|
13
13
|
"verification": { "type": "object", "additionalProperties": false, "properties": { "maxConcurrency": { "type": "integer", "minimum": 1, "maximum": 4 } } }
|
|
14
14
|
} },
|
|
15
|
-
"guardrails": { "type": "object", "additionalProperties": false, "properties": { "plan-validation": { "type": "boolean" }, "genuine-blocking-question": { "type": "boolean" }, "plan-language-hint": { "type": "boolean" }, "frozen-plan-writes": { "type": "boolean" }, "primary-language": { "type": "boolean" }, "duplicate-sibling": { "type": "boolean" }, "one-test-per-module": { "type": "boolean" }, "empty-write": { "type": "boolean" }, "evidence-gated-ticking": { "type": "boolean" }, "inventory-retry": { "type": "boolean" }, "synthetic-corrections": { "type": "boolean" }, "silent-group-closure": { "type": "boolean" }, "verify-per-group": { "type": "boolean" }, "test-reachability": { "type": "boolean" }, "exit-code-honesty": { "type": "boolean" }, "environment-repair": { "type": "boolean" }, "lockfile-repair": { "type": "boolean" }, "verify-idle-timeout": { "type": "boolean" } } },
|
|
15
|
+
"guardrails": { "type": "object", "additionalProperties": false, "properties": { "plan-validation": { "type": "boolean" }, "genuine-blocking-question": { "type": "boolean" }, "plan-language-hint": { "type": "boolean" }, "frozen-plan-writes": { "type": "boolean" }, "primary-language": { "type": "boolean" }, "duplicate-sibling": { "type": "boolean" }, "one-test-per-module": { "type": "boolean" }, "empty-write": { "type": "boolean" }, "evidence-gated-ticking": { "type": "boolean" }, "inventory-retry": { "type": "boolean" }, "synthetic-corrections": { "type": "boolean" }, "silent-group-closure": { "type": "boolean" }, "verify-per-group": { "type": "boolean" }, "test-reachability": { "type": "boolean" }, "exit-code-honesty": { "type": "boolean" }, "environment-repair": { "type": "boolean" }, "lockfile-repair": { "type": "boolean" }, "verify-idle-timeout": { "type": "boolean" }, "verification-output-adoption": { "type": "boolean" } } },
|
|
16
16
|
"rolePrompts": { "type": "object", "propertyNames": { "pattern": "^[a-z][a-z0-9-]{0,63}$" }, "additionalProperties": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" }, "properties": {
|
|
17
17
|
"architect": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" },
|
|
18
18
|
"developer": { "type": "string", "minLength": 1, "maxLength": 20000, "pattern": "\\S" },
|