specrails-desktop 2.32.0 → 2.34.0
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-DvnFkjVG.js → ActivityFeedPage-DIDxNsDj.js} +1 -1
- package/client/dist/assets/{AgentBrowserCapture-CBjcliD7.js → AgentBrowserCapture-7pJ2G5VY.js} +1 -1
- package/client/dist/assets/AgentModeAnalyticsPane-BSSyTnTM.js +2 -0
- package/client/dist/assets/{AgentModeCodePane-CokuFVdV.js → AgentModeCodePane-DzW6gvDw.js} +2 -2
- package/client/dist/assets/{AgentModeJobsPane-rf_DlWNo.js → AgentModeJobsPane-9wJff15f.js} +1 -1
- package/client/dist/assets/AgentsPage-CIA67psb.js +87 -0
- package/client/dist/assets/AnalyticsPage-C0LwYELz.js +1 -0
- package/client/dist/assets/CodePage-RtKvHmAO.js +2 -0
- package/client/dist/assets/{DesktopAnalyticsPage-gBRVqiw6.js → DesktopAnalyticsPage-BU8Y_Ts1.js} +1 -1
- package/client/dist/assets/{DocsDialog-BzO5dg2B.js → DocsDialog-DIpoaGlQ.js} +1 -1
- package/client/dist/assets/{DocsPage-Db_uxr1j.js → DocsPage-ME47KZpK.js} +1 -1
- package/client/dist/assets/InteractiveJobComposer-BGjIe9R6.js +19 -0
- package/client/dist/assets/{JobDetailModal-DPHh2WOS.js → JobDetailModal-1UZkqH4I.js} +1 -1
- package/client/dist/assets/JobDetailPage-CN65sAf1.js +1 -0
- package/client/dist/assets/JobsPage-f-4dqys7.js +1 -0
- package/client/dist/assets/LoopBuilderPage-WeiofeL6.js +7 -0
- package/client/dist/assets/LoopPreviewModal-DDTnTyH0.js +1 -0
- package/client/dist/assets/LoopsPage-BVNchIex.js +1 -0
- package/client/dist/assets/MinimizedChatsContext-CHsu3NCX.js +1 -0
- package/client/dist/assets/PluginsPage-CdFzGEfQ.js +2 -0
- package/client/dist/assets/ProjectSettingsDialog-BkCXBQWk.js +1 -0
- package/client/dist/assets/TemplatePreviewModal-RDi0khFG.js +1 -0
- package/client/dist/assets/TicketDetailModal-MhJzrqbi.js +14 -0
- package/client/dist/assets/{Trans-B2i5HVcM.js → Trans-CTqw6rr9.js} +1 -1
- package/client/dist/assets/{addspec-DhmnXER8.js → addspec-A1lDQa4M.js} +1 -1
- package/client/dist/assets/{addspec-BRlk8jON.js → addspec-BHZd0N5F.js} +1 -1
- package/client/dist/assets/{addspec-CHW9xf_X.js → addspec-BPpZ_olp.js} +1 -1
- package/client/dist/assets/{addspec-Di41DpYV.js → addspec-B_TPJt-R.js} +1 -1
- package/client/dist/assets/{addspec-4WmLJmBG.js → addspec-CLYLvaVP.js} +1 -1
- package/client/dist/assets/{addspec-C7XRejU4.js → addspec-CQBjzt24.js} +1 -1
- package/client/dist/assets/{addspec-x3mizDxc.js → addspec-D2YIQ0GU.js} +1 -1
- package/client/dist/assets/{addspec-DTmRrWrF.js → addspec-iFdQ6D99.js} +1 -1
- package/client/dist/assets/agent-Bd-q3Wqp.js +1 -0
- package/client/dist/assets/agent-BlvdW4Hv.js +1 -0
- package/client/dist/assets/agent-C-KQhxeM.js +1 -0
- package/client/dist/assets/agent-Ca15w5eu.js +1 -0
- package/client/dist/assets/agent-D6fKaoMd.js +1 -0
- package/client/dist/assets/agent-HFMYMTbB.js +1 -0
- package/client/dist/assets/agent-QIEbkBjg.js +1 -0
- package/client/dist/assets/agent-mTCsAbCl.js +1 -0
- package/client/dist/assets/agentstudio-BNUth0uj.js +1 -0
- package/client/dist/assets/agentstudio-BNdB5hkr.js +1 -0
- package/client/dist/assets/agentstudio-C7-bt8FO.js +1 -0
- package/client/dist/assets/agentstudio-C8uNchpV.js +1 -0
- package/client/dist/assets/agentstudio-CKabyMlg.js +1 -0
- package/client/dist/assets/agentstudio-CN2pokQu.js +1 -0
- package/client/dist/assets/agentstudio-DR7doNBV.js +1 -0
- package/client/dist/assets/agentstudio-y2dVDXeM.js +1 -0
- package/client/dist/assets/{analytics-C9RaQcE3.js → analytics-47pYygtV.js} +1 -1
- package/client/dist/assets/{analytics-B8Qtptbl.js → analytics-B0vt1g5U.js} +1 -1
- package/client/dist/assets/analytics-BMzCvrCU.js +1 -0
- package/client/dist/assets/{analytics-BPptRY4f.js → analytics-BlFN3Zwx.js} +1 -1
- package/client/dist/assets/{analytics-BxDGOIwf.js → analytics-CZYzL-nR.js} +1 -1
- package/client/dist/assets/{analytics-D_OZdQ9O.js → analytics-DAsb9MJ8.js} +1 -1
- package/client/dist/assets/{analytics-CJIwqrEl.js → analytics-FXQlQ2Yt.js} +1 -1
- package/client/dist/assets/{analytics-We6lUMI8.js → analytics-RnFCNkah.js} +1 -1
- package/client/dist/assets/{builder-CWYPw7Cw.js → builder-0IWtosUm.js} +1 -1
- package/client/dist/assets/{builder-CX1437hF.js → builder-Bf2N6XWn.js} +1 -1
- package/client/dist/assets/{builder-BiUG9Z5K.js → builder-BnqaaBkT.js} +1 -1
- package/client/dist/assets/{builder-CGRz_-zP.js → builder-BxxfGgAO.js} +1 -1
- package/client/dist/assets/{builder-DwDexYRE.js → builder-C5IIgWsq.js} +1 -1
- package/client/dist/assets/builder-CHuahItu.js +1 -0
- package/client/dist/assets/{builder-Cowk9jtL.js → builder-CjWosXL_.js} +1 -1
- package/client/dist/assets/{builder-3bygQtZi.js → builder-CpCIOoKj.js} +1 -1
- package/client/dist/assets/dashboard-BHRqsGCV.js +1 -0
- package/client/dist/assets/dashboard-Bfr90vOl.js +1 -0
- package/client/dist/assets/dashboard-CTsM8x3m.js +1 -0
- package/client/dist/assets/dashboard-CqC6wX0h.js +1 -0
- package/client/dist/assets/dashboard-CuZtAszc.js +1 -0
- package/client/dist/assets/dashboard-DAcqIc-n.js +1 -0
- package/client/dist/assets/dashboard-DekiV-86.js +1 -0
- package/client/dist/assets/dashboard-DoDczc5S.js +1 -0
- package/client/dist/assets/{dist-js-CkTZKBF9.js → dist-js-BmsFWRly.js} +1 -1
- package/client/dist/assets/{dist-js-ClShHcGa.js → dist-js-J5W_N5ng.js} +1 -1
- package/client/dist/assets/{formatDistanceToNow-BjGwPf6I.js → formatDistanceToNow-BOuXgebD.js} +1 -1
- package/client/dist/assets/{getTimezoneOffsetInMilliseconds-CSCbUI4W.js → getTimezoneOffsetInMilliseconds-EjGOpn4b.js} +1 -1
- package/client/dist/assets/{i18n-Ch-Db_x2.js → i18n-nJQvD5zJ.js} +2 -2
- package/client/dist/assets/index-C_sdsSZt.js +63 -0
- package/client/dist/assets/index-DCN5QbDm.css +2 -0
- package/client/dist/assets/{jira-BySfUuQZ.js → jira-BNZu8f9O.js} +1 -1
- package/client/dist/assets/{jira-gAvOyije.js → jira-BOoaUXbb.js} +1 -1
- package/client/dist/assets/{jira-pRaL7JQo.js → jira-BYVN4L2u.js} +1 -1
- package/client/dist/assets/jira-CKJg3HZd.js +1 -0
- package/client/dist/assets/{jira-Byw2C0F8.js → jira-DDDsZ4ub.js} +1 -1
- package/client/dist/assets/{jira-D38Gj7AT.js → jira-DXwYjVE4.js} +1 -1
- package/client/dist/assets/{jira-D11gPhxz.js → jira-Dn6U2nRW.js} +1 -1
- package/client/dist/assets/{jira-api-BgFIr8eD.js → jira-api-Bhixwn-d.js} +1 -1
- package/client/dist/assets/{jira-CNeYi06w.js → jira-j7hVolR5.js} +1 -1
- package/client/dist/assets/nav-ApJVg3xu.js +1 -0
- package/client/dist/assets/nav-BKNky1Bg.js +1 -0
- package/client/dist/assets/nav-C6vWWKYx.js +1 -0
- package/client/dist/assets/nav-CXJ6Aw89.js +1 -0
- package/client/dist/assets/nav-CsmFBIDf.js +1 -0
- package/client/dist/assets/nav-DjkmQJjd.js +1 -0
- package/client/dist/assets/nav-iwjCg5FR.js +1 -0
- package/client/dist/assets/nav-pYuXq7sf.js +1 -0
- package/client/dist/assets/{provider-capabilities-BvoExILy.js → provider-capabilities-Doej9lfa.js} +1 -1
- package/client/dist/assets/setup-3jWaoVUO.js +1 -0
- package/client/dist/assets/setup-BjgRneb2.js +1 -0
- package/client/dist/assets/setup-BzFIjv-f.js +1 -0
- package/client/dist/assets/setup-C4VfQsxE.js +1 -0
- package/client/dist/assets/setup-DfUkpDbN.js +1 -0
- package/client/dist/assets/setup-ECe90Pmu.js +1 -0
- package/client/dist/assets/setup-dlqC4Jmm.js +1 -0
- package/client/dist/assets/setup-jwfLMiM2.js +1 -0
- package/client/dist/assets/{spending-Dq43JYmD.js → spending-BpYPsevQ.js} +1 -1
- package/client/dist/assets/tickets-B4rjuSYi.js +1 -0
- package/client/dist/assets/tickets-Bc71ce1C.js +1 -0
- package/client/dist/assets/tickets-CLg7VTJ8.js +1 -0
- package/client/dist/assets/tickets-CP9U1GJt.js +1 -0
- package/client/dist/assets/tickets-CZNBiHbq.js +1 -0
- package/client/dist/assets/tickets-DlX2mEkD.js +1 -0
- package/client/dist/assets/tickets-W6pBmbrX.js +1 -0
- package/client/dist/assets/tickets-_yKvCt4Y.js +1 -0
- package/client/dist/assets/{useBrowserCaptureSession-TTO8vHtd.js → useBrowserCaptureSession-BBXBN0Na.js} +1 -1
- package/client/dist/assets/useDesktop-B6Du2N83.js +1 -0
- package/client/dist/assets/{useSharedWebSocket-DaPh7QBL.js → useSharedWebSocket-DK96ctb7.js} +1 -1
- package/client/dist/index.html +24 -24
- package/docs/guide/de/getting-started/3-adding-your-first-project.md +13 -32
- package/docs/guide/de/integrations/1-ai-providers.md +27 -16
- package/docs/guide/en/getting-started/3-adding-your-first-project.md +8 -27
- package/docs/guide/en/integrations/1-ai-providers.md +21 -18
- package/docs/guide/es/getting-started/3-adding-your-first-project.md +12 -31
- package/docs/guide/es/integrations/1-ai-providers.md +24 -16
- package/docs/guide/fr/getting-started/3-adding-your-first-project.md +15 -34
- package/docs/guide/fr/integrations/1-ai-providers.md +25 -16
- package/docs/guide/it/getting-started/3-adding-your-first-project.md +16 -35
- package/docs/guide/it/integrations/1-ai-providers.md +25 -16
- package/docs/guide/ja/getting-started/3-adding-your-first-project.md +16 -35
- package/docs/guide/ja/integrations/1-ai-providers.md +7 -12
- package/docs/guide/pt/getting-started/3-adding-your-first-project.md +15 -34
- package/docs/guide/pt/integrations/1-ai-providers.md +24 -16
- package/docs/guide/zh/getting-started/3-adding-your-first-project.md +18 -37
- package/docs/guide/zh/integrations/1-ai-providers.md +7 -12
- package/docs/internals/project-builder.md +11 -6
- package/package.json +1 -1
- package/server/dist/ai-invocations.js +35 -0
- package/server/dist/blueprint-commit.js +1 -1
- package/server/dist/desktop-db.js +22 -1
- package/server/dist/desktop-router.js +132 -43
- package/server/dist/framework-reseed.js +144 -0
- package/server/dist/index.js +41 -2
- package/server/dist/jira/jira-sync-manager.js +49 -2
- package/server/dist/jira-router.js +21 -0
- package/server/dist/job-phase-breakdown.js +132 -0
- package/server/dist/legacy-migration.js +395 -0
- package/server/dist/loop-executors.js +5 -1
- package/server/dist/offline-assemble.js +32 -4
- package/server/dist/pr-body.js +13 -0
- package/server/dist/project-router-jobs.js +16 -0
- package/server/dist/project-router-spending.js +11 -1
- package/server/dist/provider-detection.js +222 -0
- package/server/dist/provider-selection.js +71 -19
- package/server/dist/queue-manager.js +4 -0
- package/server/dist/rail-pr-decision.js +10 -0
- package/server/dist/rails-router.js +11 -0
- package/server/dist/rails-store.js +5 -1
- package/server/dist/repo-map.js +293 -0
- package/server/dist/setup-manager.js +57 -0
- package/server/dist/spending.js +45 -0
- package/client/dist/assets/AgentsPage-B648IzGI.js +0 -87
- package/client/dist/assets/AnalyticsPage-0rBoULPX.js +0 -1
- package/client/dist/assets/CodePage-ju302Vl7.js +0 -2
- package/client/dist/assets/InteractiveJobComposer-_jmsE_Az.js +0 -19
- package/client/dist/assets/JobDetailPage-7nwHyHGV.js +0 -1
- package/client/dist/assets/JobsPage-C_6YrcmE.js +0 -1
- package/client/dist/assets/LoopBuilderPage-DJ1UPDwL.js +0 -7
- package/client/dist/assets/LoopPreviewModal-12cUnljK.js +0 -1
- package/client/dist/assets/LoopsPage-CHYK0Xjr.js +0 -1
- package/client/dist/assets/MinimizedChatsContext-Gt6KQySM.js +0 -1
- package/client/dist/assets/PluginsPage-E59oQmwc.js +0 -2
- package/client/dist/assets/ProjectSettingsDialog-DQTLQ7ie.js +0 -1
- package/client/dist/assets/TemplatePreviewModal-nXUcDgfk.js +0 -1
- package/client/dist/assets/TicketDetailModal-C-tHW-2s.js +0 -14
- package/client/dist/assets/agent-BKsuIgIT.js +0 -1
- package/client/dist/assets/agent-Bs9NilfI.js +0 -1
- package/client/dist/assets/agent-C23h684q.js +0 -1
- package/client/dist/assets/agent-CqEjy3Z2.js +0 -1
- package/client/dist/assets/agent-D5DZTks1.js +0 -1
- package/client/dist/assets/agent-DE8hUYPl.js +0 -1
- package/client/dist/assets/agent-DsTuJGSc.js +0 -1
- package/client/dist/assets/agent-DwZM05-x.js +0 -1
- package/client/dist/assets/agentstudio-8TQqBenW.js +0 -1
- package/client/dist/assets/agentstudio-B8Z_mmEX.js +0 -1
- package/client/dist/assets/agentstudio-BZR9YmLs.js +0 -1
- package/client/dist/assets/agentstudio-Bge9pAWb.js +0 -1
- package/client/dist/assets/agentstudio-Bz169Vzt.js +0 -1
- package/client/dist/assets/agentstudio-D6L2jpNi.js +0 -1
- package/client/dist/assets/agentstudio-K2-Z7CL0.js +0 -1
- package/client/dist/assets/agentstudio-Zmgzthv8.js +0 -1
- package/client/dist/assets/analytics-BHqUazhP.js +0 -1
- package/client/dist/assets/builder-skfnZ1uC.js +0 -1
- package/client/dist/assets/dashboard-BVq6kRJe.js +0 -1
- package/client/dist/assets/dashboard-BaKPPmLH.js +0 -1
- package/client/dist/assets/dashboard-Bm8e4zYG.js +0 -1
- package/client/dist/assets/dashboard-Bt6IesnM.js +0 -1
- package/client/dist/assets/dashboard-C_NQZle_.js +0 -1
- package/client/dist/assets/dashboard-CyZKoGoI.js +0 -1
- package/client/dist/assets/dashboard-HIUdUBQ0.js +0 -1
- package/client/dist/assets/dashboard-pm4Mr229.js +0 -1
- package/client/dist/assets/index-Bz0TeGRs.css +0 -2
- package/client/dist/assets/index-kCUcnT8G.js +0 -77
- package/client/dist/assets/jira-CmKWERE0.js +0 -1
- package/client/dist/assets/nav-BHDiSIVu.js +0 -1
- package/client/dist/assets/nav-CMiYAq4i.js +0 -1
- package/client/dist/assets/nav-CqS7NrDW.js +0 -1
- package/client/dist/assets/nav-D-0M7xfv.js +0 -1
- package/client/dist/assets/nav-D65O6u9I.js +0 -1
- package/client/dist/assets/nav-D_LKYvBr.js +0 -1
- package/client/dist/assets/nav-FDTJmU79.js +0 -1
- package/client/dist/assets/nav-VaB-C4k5.js +0 -1
- package/client/dist/assets/setup-BBiHEO7h.js +0 -1
- package/client/dist/assets/setup-BlTWhNIQ.js +0 -1
- package/client/dist/assets/setup-Bp2L_oiP.js +0 -1
- package/client/dist/assets/setup-CInYXp1-.js +0 -1
- package/client/dist/assets/setup-CYH3bP6K.js +0 -1
- package/client/dist/assets/setup-DwcQ921P.js +0 -1
- package/client/dist/assets/setup-PBLjuDSs.js +0 -1
- package/client/dist/assets/setup-bx6FtoBp.js +0 -1
- package/client/dist/assets/tickets-BL9r1G0e.js +0 -1
- package/client/dist/assets/tickets-B_NyHJ8f.js +0 -1
- package/client/dist/assets/tickets-BarF1wuG.js +0 -1
- package/client/dist/assets/tickets-CclVhpQA.js +0 -1
- package/client/dist/assets/tickets-CqqOHbli.js +0 -1
- package/client/dist/assets/tickets-DCkFSiqt.js +0 -1
- package/client/dist/assets/tickets-Djn3ccfT.js +0 -1
- package/client/dist/assets/tickets-MiHBPsy_.js +0 -1
- package/client/dist/assets/useDesktop-DUvS4fuP.js +0 -1
|
@@ -16,6 +16,9 @@ const core_update_manager_1 = require("./core-update-manager");
|
|
|
16
16
|
const specrails_tech_client_1 = require("./specrails-tech-client");
|
|
17
17
|
const core_compat_1 = require("./core-compat");
|
|
18
18
|
const providers_1 = require("./providers");
|
|
19
|
+
const provider_detection_1 = require("./provider-detection");
|
|
20
|
+
const workspace_manager_1 = require("./workspace-manager");
|
|
21
|
+
const workspace_resolution_1 = require("./workspace-resolution");
|
|
19
22
|
const desktop_analytics_1 = require("./desktop-analytics");
|
|
20
23
|
const setup_prerequisites_1 = require("./setup-prerequisites");
|
|
21
24
|
const path_resolver_1 = require("./path-resolver");
|
|
@@ -44,24 +47,9 @@ function allocateSlug(name, existing) {
|
|
|
44
47
|
n += 1;
|
|
45
48
|
return `${base}-${n}`;
|
|
46
49
|
}
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
// var is unset (legacy fallback — do not remove while old installs exist).
|
|
51
|
-
function isCodexBetaDisabled() {
|
|
52
|
-
const v = process.env.SPECRAILS_CODEX_BETA ?? process.env.SPECRAILS_HUB_CODEX_BETA;
|
|
53
|
-
return v === '0';
|
|
54
|
-
}
|
|
55
|
-
// Gemini is enabled by DEFAULT now that its stream-json schema and the full rails
|
|
56
|
-
// pipeline (architect→developer→reviewer delegation, headless agent loading, the
|
|
57
|
-
// MAX_TURNS resume + reviewer gate) are validated against the live binary
|
|
58
|
-
// (0.46/0.47). Emergency rollback: SPECRAILS_GEMINI_BETA=0 forces it back to
|
|
59
|
-
// "unavailable" without redeploying — parity with codex's SPECRAILS_CODEX_BETA.
|
|
60
|
-
// The adapter is always registered (pricing/getAdapter work); only project
|
|
61
|
-
// selection is gated.
|
|
62
|
-
function isGeminiBetaDisabled() {
|
|
63
|
-
return process.env.SPECRAILS_GEMINI_BETA === '0';
|
|
64
|
-
}
|
|
50
|
+
// Beta kill switches live with the app-level detection singleton now
|
|
51
|
+
// (provider-detection.ts) so detection and route gating can never disagree.
|
|
52
|
+
// Re-imported here under the original names.
|
|
65
53
|
// Theme allow-list. Mirror of THEME_IDS in `client/src/lib/themes.ts` —
|
|
66
54
|
// kept duplicated to avoid pulling client code into the server bundle.
|
|
67
55
|
const THEME_ID_ALLOWLIST = new Set(['dracula', 'aurora-light', 'obsidian-dark', 'code-rain', 'specrails', 'galaxy']);
|
|
@@ -332,25 +320,64 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
332
320
|
db: registry.desktopDb,
|
|
333
321
|
isLoopRunning: (loopId) => registry.listContexts().some((c) => (0, loop_runs_store_1.countRunningForLoop)(c.db, loopId) > 0),
|
|
334
322
|
});
|
|
335
|
-
//
|
|
323
|
+
// Lazy per-provider workspace assembly (provider-auto-detection spec): when
|
|
324
|
+
// the usable set gains a provider, every RELOCATED project whose workspace
|
|
325
|
+
// lacks that provider's surface assembles it in the background — installing
|
|
326
|
+
// codex on Tuesday makes it usable everywhere without user action.
|
|
327
|
+
function assembleNewlyDetected(detected) {
|
|
328
|
+
for (const ctx of registry.listContexts()) {
|
|
329
|
+
try {
|
|
330
|
+
const workspace = (0, workspace_manager_1.workspacePathFor)(ctx.project.slug);
|
|
331
|
+
if (!(0, workspace_resolution_1.isWorkspacePopulated)(workspace))
|
|
332
|
+
continue; // legacy → migration owns it
|
|
333
|
+
const missing = detected.filter((p) => {
|
|
334
|
+
try {
|
|
335
|
+
return (0, providers_1.hasAdapter)(p) && !fs_1.default.existsSync(path_1.default.join(workspace, (0, providers_1.getAdapter)(p).projectDirName));
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
});
|
|
341
|
+
if (missing.length === 0)
|
|
342
|
+
continue;
|
|
343
|
+
ctx.setupManager.startSilentAssemble(ctx.project.id, ctx.project.path, ctx.project.slug, missing);
|
|
344
|
+
}
|
|
345
|
+
catch (err) {
|
|
346
|
+
console.warn(`[desktop] lazy assemble check failed for ${ctx.project.id} (non-fatal):`, err);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
// GET /api/projects — list all registered projects. The old wizard-restore
|
|
351
|
+
// `setupProjectIds` field is retired (silent-project-add): kept as an empty
|
|
352
|
+
// array for wire compat with clients that still destructure it.
|
|
336
353
|
router.get('/projects', (_req, res) => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
354
|
+
res.json({ projects: (0, desktop_db_1.listProjects)(registry.desktopDb), setupProjectIds: [] });
|
|
355
|
+
});
|
|
356
|
+
// GET /api/providers/detected — the app-level detection snapshot (machine
|
|
357
|
+
// property, not project property). `?refresh=1` bypasses the 60s cache; a
|
|
358
|
+
// changed usable set broadcasts `providers.detected_changed` app-globally.
|
|
359
|
+
router.get('/providers/detected', async (req, res) => {
|
|
360
|
+
try {
|
|
361
|
+
if (req.query.refresh === '1') {
|
|
362
|
+
const { snapshot, changed } = await (0, provider_detection_1.refreshDetection)();
|
|
363
|
+
if (changed) {
|
|
364
|
+
broadcast({
|
|
365
|
+
type: 'providers.detected_changed',
|
|
366
|
+
detected: snapshot.detected,
|
|
367
|
+
providers: snapshot.providers,
|
|
368
|
+
timestamp: new Date().toISOString(),
|
|
369
|
+
});
|
|
370
|
+
assembleNewlyDetected(snapshot.detected);
|
|
371
|
+
}
|
|
372
|
+
res.json(snapshot);
|
|
373
|
+
return;
|
|
351
374
|
}
|
|
375
|
+
res.json(await (0, provider_detection_1.getDetectionSnapshot)());
|
|
376
|
+
}
|
|
377
|
+
catch (err) {
|
|
378
|
+
console.error('[provider-detection] snapshot error:', err);
|
|
379
|
+
res.status(500).json({ error: 'provider detection failed' });
|
|
352
380
|
}
|
|
353
|
-
res.json({ projects, setupProjectIds });
|
|
354
381
|
});
|
|
355
382
|
// GET /api/available-providers — which AI CLIs are installed, plus supported install tiers
|
|
356
383
|
//
|
|
@@ -364,11 +391,11 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
364
391
|
// provider surfaces here with no edit. Apply per-provider beta gates: codex
|
|
365
392
|
// is forced unavailable when SPECRAILS_CODEX_BETA=0 (emergency rollback).
|
|
366
393
|
const gated = { ...providers };
|
|
367
|
-
if (isCodexBetaDisabled())
|
|
394
|
+
if ((0, provider_detection_1.isCodexBetaDisabled)())
|
|
368
395
|
gated.codex = false;
|
|
369
396
|
// Gemini: enabled by default; forced unavailable only when
|
|
370
397
|
// SPECRAILS_GEMINI_BETA=0 (emergency rollback, parity with codex).
|
|
371
|
-
if (isGeminiBetaDisabled())
|
|
398
|
+
if ((0, provider_detection_1.isGeminiBetaDisabled)())
|
|
372
399
|
gated.gemini = false;
|
|
373
400
|
const providerIssues = {};
|
|
374
401
|
if (gated.kimi) {
|
|
@@ -429,10 +456,28 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
429
456
|
res.status(400).json({ error: 'path is required' });
|
|
430
457
|
return;
|
|
431
458
|
}
|
|
432
|
-
//
|
|
433
|
-
//
|
|
434
|
-
//
|
|
435
|
-
//
|
|
459
|
+
// Project registration is a detection refresh trigger (spec:
|
|
460
|
+
// provider-auto-detection). The refresh runs in the BACKGROUND so
|
|
461
|
+
// registration latency never pays for CLI probes; the defaulting below uses
|
|
462
|
+
// the last cached snapshot (null before the first startup detection).
|
|
463
|
+
(0, provider_detection_1.refreshDetection)()
|
|
464
|
+
.then(({ snapshot, changed }) => {
|
|
465
|
+
if (!changed)
|
|
466
|
+
return;
|
|
467
|
+
broadcast({
|
|
468
|
+
type: 'providers.detected_changed',
|
|
469
|
+
detected: snapshot.detected,
|
|
470
|
+
providers: snapshot.providers,
|
|
471
|
+
timestamp: new Date().toISOString(),
|
|
472
|
+
});
|
|
473
|
+
assembleNewlyDetected(snapshot.detected);
|
|
474
|
+
})
|
|
475
|
+
.catch(() => { });
|
|
476
|
+
const detectedNow = (0, provider_detection_1.getDetectedIdsSync)() ?? [];
|
|
477
|
+
// Normalise to a providers list. Wire compat: `providers`/`provider` are
|
|
478
|
+
// still accepted and recorded, but the DETECTED set is authoritative for
|
|
479
|
+
// what the project offers (delta spec: multi-provider-architecture).
|
|
480
|
+
// Omitting both registers with the detected set.
|
|
436
481
|
let providers;
|
|
437
482
|
if (Array.isArray(providersRaw) && providersRaw.length > 0) {
|
|
438
483
|
providers = providersRaw;
|
|
@@ -441,7 +486,7 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
441
486
|
providers = [provider];
|
|
442
487
|
}
|
|
443
488
|
else {
|
|
444
|
-
providers = ['claude'];
|
|
489
|
+
providers = detectedNow.length > 0 ? [...detectedNow] : ['claude'];
|
|
445
490
|
}
|
|
446
491
|
// De-duplicate while preserving order (primary stays first).
|
|
447
492
|
providers = providers.filter((p, i) => providers.indexOf(p) === i);
|
|
@@ -458,13 +503,13 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
458
503
|
}
|
|
459
504
|
// Beta-gate parity: if codex beta is forced off via env, refuse codex
|
|
460
505
|
// selections too (consistency with /available-providers).
|
|
461
|
-
if (providers.includes('codex') && isCodexBetaDisabled()) {
|
|
506
|
+
if (providers.includes('codex') && (0, provider_detection_1.isCodexBetaDisabled)()) {
|
|
462
507
|
res.status(400).json({
|
|
463
508
|
error: 'Codex provider is currently disabled (SPECRAILS_CODEX_BETA=0). Unset or set to 1 to enable.',
|
|
464
509
|
});
|
|
465
510
|
return;
|
|
466
511
|
}
|
|
467
|
-
if (providers.includes('gemini') && isGeminiBetaDisabled()) {
|
|
512
|
+
if (providers.includes('gemini') && (0, provider_detection_1.isGeminiBetaDisabled)()) {
|
|
468
513
|
res.status(400).json({
|
|
469
514
|
error: 'Gemini provider is currently disabled (SPECRAILS_GEMINI_BETA=0). Unset or set to 1 to enable.',
|
|
470
515
|
});
|
|
@@ -527,6 +572,20 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
527
572
|
project: ctx.project,
|
|
528
573
|
timestamp: new Date().toISOString(),
|
|
529
574
|
});
|
|
575
|
+
// Silent add (global-core-zero-friction): registration is already done —
|
|
576
|
+
// assemble the relocated workspace in the BACKGROUND for every detected
|
|
577
|
+
// provider. Never blocks or rolls back registration. Skipped when the
|
|
578
|
+
// workspace is already populated (re-register of a relocated repo).
|
|
579
|
+
try {
|
|
580
|
+
const workspace = (0, workspace_manager_1.workspacePathFor)(slug);
|
|
581
|
+
const effectiveProviders = detectedNow.length > 0 ? detectedNow : providers;
|
|
582
|
+
if (!(0, workspace_resolution_1.isWorkspacePopulated)(workspace)) {
|
|
583
|
+
ctx.setupManager.startSilentAssemble(id, canonicalPath, slug, effectiveProviders);
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
catch (err) {
|
|
587
|
+
console.warn('[desktop] silent assemble launch failed (non-fatal):', err);
|
|
588
|
+
}
|
|
530
589
|
res.status(201).json({ project: ctx.project, has_specrails: specrailsInstalled });
|
|
531
590
|
}
|
|
532
591
|
catch (err) {
|
|
@@ -541,6 +600,36 @@ function createDesktopRouter(registry, broadcast) {
|
|
|
541
600
|
}
|
|
542
601
|
}
|
|
543
602
|
});
|
|
603
|
+
// POST /api/projects/:id/assemble-retry — re-run the silent workspace
|
|
604
|
+
// assemble for the providers that failed (or every detected provider when
|
|
605
|
+
// nothing is recorded as failed). 202; progress rides project.assemble_progress.
|
|
606
|
+
router.post('/projects/:id/assemble-retry', async (req, res) => {
|
|
607
|
+
const ctx = registry.getContext(req.params.id);
|
|
608
|
+
if (!ctx) {
|
|
609
|
+
res.status(404).json({ error: 'unknown project' });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const state = ctx.setupManager.silentAssembleState(ctx.project.id);
|
|
613
|
+
if (state.running) {
|
|
614
|
+
res.status(202).json({ alreadyRunning: true });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
let providers = state.failed;
|
|
618
|
+
if (providers.length === 0) {
|
|
619
|
+
try {
|
|
620
|
+
providers = (await (0, provider_detection_1.getDetectionSnapshot)()).detected;
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
providers = [];
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
if (providers.length === 0) {
|
|
627
|
+
res.status(409).json({ error: 'no providers detected to assemble' });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
ctx.setupManager.startSilentAssemble(ctx.project.id, ctx.project.path, ctx.project.slug, providers);
|
|
631
|
+
res.status(202).json({ retrying: providers });
|
|
632
|
+
});
|
|
544
633
|
// DELETE /api/projects/:id — unregister a project
|
|
545
634
|
router.delete('/projects/:id', (req, res) => {
|
|
546
635
|
const { id } = req.params;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Post-swap workspace re-seed (global-core-zero-friction / framework-auto-update).
|
|
3
|
+
//
|
|
4
|
+
// A `current` swap updates every SYMLINKED workspace surface for free, but the
|
|
5
|
+
// COPIED files (instruction files with the project name, Windows copy-fallback
|
|
6
|
+
// subtrees, Kimi per-child skill links) stay frozen at the old version. This
|
|
7
|
+
// pass re-runs the offline assemble for every relocated workspace whose
|
|
8
|
+
// recorded framework version differs from `current` — which also repairs
|
|
9
|
+
// machines that were off during a swap (missed-swap scenario).
|
|
10
|
+
//
|
|
11
|
+
// `.mcp.json` guarantee: the framework owns NO mcp keys today, so the surgical
|
|
12
|
+
// "update only framework-owned keys" contract degenerates to full preservation
|
|
13
|
+
// — the file is snapshotted before the assemble and restored byte-identically
|
|
14
|
+
// if the assemble touched it (plugin/user keys can never be clobbered).
|
|
15
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
18
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
19
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
20
|
+
}
|
|
21
|
+
Object.defineProperty(o, k2, desc);
|
|
22
|
+
}) : (function(o, m, k, k2) {
|
|
23
|
+
if (k2 === undefined) k2 = k;
|
|
24
|
+
o[k2] = m[k];
|
|
25
|
+
}));
|
|
26
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
27
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
28
|
+
}) : function(o, v) {
|
|
29
|
+
o["default"] = v;
|
|
30
|
+
});
|
|
31
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
32
|
+
var ownKeys = function(o) {
|
|
33
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
34
|
+
var ar = [];
|
|
35
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
36
|
+
return ar;
|
|
37
|
+
};
|
|
38
|
+
return ownKeys(o);
|
|
39
|
+
};
|
|
40
|
+
return function (mod) {
|
|
41
|
+
if (mod && mod.__esModule) return mod;
|
|
42
|
+
var result = {};
|
|
43
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
44
|
+
__setModuleDefault(result, mod);
|
|
45
|
+
return result;
|
|
46
|
+
};
|
|
47
|
+
})();
|
|
48
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
49
|
+
exports.isFrameworkAutoswapEnabled = isFrameworkAutoswapEnabled;
|
|
50
|
+
exports.readWorkspaceFrameworkVersion = readWorkspaceFrameworkVersion;
|
|
51
|
+
exports.reseedStaleWorkspaces = reseedStaleWorkspaces;
|
|
52
|
+
const fs = __importStar(require("fs"));
|
|
53
|
+
const path = __importStar(require("path"));
|
|
54
|
+
const providers_1 = require("./providers");
|
|
55
|
+
const workspace_manager_1 = require("./workspace-manager");
|
|
56
|
+
const workspace_resolution_1 = require("./workspace-resolution");
|
|
57
|
+
const offline_assemble_1 = require("./offline-assemble");
|
|
58
|
+
function isFrameworkAutoswapEnabled() {
|
|
59
|
+
const v = (process.env.SPECRAILS_FRAMEWORK_AUTOSWAP ?? '').toLowerCase();
|
|
60
|
+
return v !== 'false' && v !== '0' && v !== 'off';
|
|
61
|
+
}
|
|
62
|
+
/** The framework version this workspace was last assembled against. */
|
|
63
|
+
function readWorkspaceFrameworkVersion(slug) {
|
|
64
|
+
try {
|
|
65
|
+
const v = fs.readFileSync(path.join((0, workspace_manager_1.workspacePathFor)(slug), '.specrails', 'specrails-version'), 'utf-8').trim();
|
|
66
|
+
return v.length > 0 ? v : null;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/** Providers whose surface exists in this workspace (assembled at some point). */
|
|
73
|
+
function workspaceProviders(workspace) {
|
|
74
|
+
return (0, providers_1.listAdapters)()
|
|
75
|
+
.filter((a) => fs.existsSync(path.join(workspace, a.projectDirName)))
|
|
76
|
+
.map((a) => a.id);
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Re-seed every relocated workspace whose recorded framework version differs
|
|
80
|
+
* from `currentVersion`. Idempotent (an up-to-date workspace is skipped);
|
|
81
|
+
* serialized; never throws — per-project failures are reported and logged.
|
|
82
|
+
*/
|
|
83
|
+
async function reseedStaleWorkspaces(projects, currentVersion, io) {
|
|
84
|
+
const results = [];
|
|
85
|
+
if (!currentVersion)
|
|
86
|
+
return results;
|
|
87
|
+
const assemble = io?.assemble
|
|
88
|
+
?? (async (p, providers) => {
|
|
89
|
+
await (0, offline_assemble_1.assembleProjectOffline)({
|
|
90
|
+
projectPath: p.path,
|
|
91
|
+
slug: p.slug,
|
|
92
|
+
desktopProjectId: p.id,
|
|
93
|
+
providers,
|
|
94
|
+
continueOnError: true,
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
for (const project of projects) {
|
|
98
|
+
const workspace = (0, workspace_manager_1.workspacePathFor)(project.slug);
|
|
99
|
+
if (!(0, workspace_resolution_1.isWorkspacePopulated)(workspace)) {
|
|
100
|
+
results.push({ projectId: project.id, reseeded: false, skippedReason: 'not-relocated' });
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const recorded = readWorkspaceFrameworkVersion(project.slug);
|
|
104
|
+
if (recorded === currentVersion) {
|
|
105
|
+
results.push({ projectId: project.id, reseeded: false, skippedReason: 'up-to-date' });
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const providers = workspaceProviders(workspace);
|
|
109
|
+
if (providers.length === 0) {
|
|
110
|
+
results.push({ projectId: project.id, reseeded: false, skippedReason: 'not-relocated' });
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// `.mcp.json` preservation snapshot (see module header).
|
|
114
|
+
const mcpPath = path.join(workspace, '.mcp.json');
|
|
115
|
+
let mcpBefore = null;
|
|
116
|
+
try {
|
|
117
|
+
mcpBefore = fs.readFileSync(mcpPath, 'utf-8');
|
|
118
|
+
}
|
|
119
|
+
catch { /* absent */ }
|
|
120
|
+
try {
|
|
121
|
+
await assemble(project, providers);
|
|
122
|
+
if (mcpBefore !== null) {
|
|
123
|
+
let mcpAfter = null;
|
|
124
|
+
try {
|
|
125
|
+
mcpAfter = fs.readFileSync(mcpPath, 'utf-8');
|
|
126
|
+
}
|
|
127
|
+
catch { /* deleted by assemble */ }
|
|
128
|
+
if (mcpAfter !== mcpBefore)
|
|
129
|
+
fs.writeFileSync(mcpPath, mcpBefore, 'utf-8');
|
|
130
|
+
}
|
|
131
|
+
results.push({ projectId: project.id, reseeded: true });
|
|
132
|
+
console.log(`[framework-reseed] ${project.slug}: ${recorded ?? '(none)'} → ${currentVersion}`);
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
results.push({
|
|
136
|
+
projectId: project.id,
|
|
137
|
+
reseeded: false,
|
|
138
|
+
error: err instanceof Error ? err.message : String(err),
|
|
139
|
+
});
|
|
140
|
+
console.warn(`[framework-reseed] ${project.slug} failed (non-fatal):`, err);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return results;
|
|
144
|
+
}
|
package/server/dist/index.js
CHANGED
|
@@ -15,6 +15,11 @@ const express_1 = __importDefault(require("express"));
|
|
|
15
15
|
const ws_1 = require("ws");
|
|
16
16
|
const project_registry_1 = require("./project-registry");
|
|
17
17
|
const desktop_router_1 = require("./desktop-router");
|
|
18
|
+
const provider_detection_1 = require("./provider-detection");
|
|
19
|
+
const provider_selection_1 = require("./provider-selection");
|
|
20
|
+
const desktop_db_1 = require("./desktop-db");
|
|
21
|
+
const legacy_migration_1 = require("./legacy-migration");
|
|
22
|
+
const framework_reseed_1 = require("./framework-reseed");
|
|
18
23
|
const project_router_1 = require("./project-router");
|
|
19
24
|
const docs_router_1 = require("./docs-router");
|
|
20
25
|
const auth_1 = require("./auth");
|
|
@@ -522,9 +527,20 @@ function applyPtyWsRateLimiting(ws) {
|
|
|
522
527
|
// WsMessage union; cast at this single boundary.
|
|
523
528
|
broadcast: (msg) => broadcast(msg),
|
|
524
529
|
});
|
|
525
|
-
|
|
530
|
+
// Kill switch (framework-auto-update spec): SPECRAILS_FRAMEWORK_AUTOSWAP=false
|
|
531
|
+
// restores the manual-only flow (GlobalSettings "check now").
|
|
532
|
+
if (framework.isAvailable() && (0, framework_reseed_1.isFrameworkAutoswapEnabled)()) {
|
|
526
533
|
const providers = registry.installedProvidersUnion();
|
|
527
|
-
(0, artifact_registry_1.withFileLock)(undefined, () => framework.versionCheck(providers));
|
|
534
|
+
const check = (0, artifact_registry_1.withFileLock)(undefined, () => framework.versionCheck(providers));
|
|
535
|
+
// Post-swap re-seed: refresh COPIED workspace files (instruction files,
|
|
536
|
+
// Windows copy-fallback, Kimi skill links) for every workspace whose
|
|
537
|
+
// recorded framework version ≠ current — also repairs missed swaps.
|
|
538
|
+
const reseedProjects = registry.listContexts().map((c) => ({
|
|
539
|
+
id: c.project.id,
|
|
540
|
+
slug: c.project.slug,
|
|
541
|
+
path: c.project.path,
|
|
542
|
+
}));
|
|
543
|
+
void (0, framework_reseed_1.reseedStaleWorkspaces)(reseedProjects, check.version);
|
|
528
544
|
}
|
|
529
545
|
}
|
|
530
546
|
catch (err) {
|
|
@@ -542,6 +558,29 @@ function applyPtyWsRateLimiting(ws) {
|
|
|
542
558
|
(0, telemetry_compactor_1.runCompactionForAll)(registry).catch((err) => {
|
|
543
559
|
console.error('[telemetry-compactor] startup compaction error:', err);
|
|
544
560
|
});
|
|
561
|
+
// ─── App-level provider detection (global-core-zero-friction) ───────────────
|
|
562
|
+
// Providers are a machine property: one detection snapshot feeds
|
|
563
|
+
// provider-selection (per-invocation validation) and desktop-db's project-row
|
|
564
|
+
// mirror (`providers` reads as the detected set). Startup is a refresh
|
|
565
|
+
// trigger; window-focus and project-add refreshes ride the REST route.
|
|
566
|
+
(0, provider_selection_1.setDetectedProvidersSupplier)(provider_detection_1.getDetectedIdsSync);
|
|
567
|
+
(0, desktop_db_1.setProjectProvidersMirror)(provider_detection_1.getDetectedIdsSync);
|
|
568
|
+
(0, provider_detection_1.refreshDetection)()
|
|
569
|
+
.then(({ snapshot }) => {
|
|
570
|
+
// ─── Forced legacy migration (global-core-zero-friction) ────────────────
|
|
571
|
+
// After the first detection lands, migrate repo-resident core installs to
|
|
572
|
+
// the relocated workspace in the background — serialized, fail-open,
|
|
573
|
+
// journaled. Kill switch: SPECRAILS_LEGACY_MIGRATION=false.
|
|
574
|
+
const projects = registry.listContexts().map((c) => ({
|
|
575
|
+
id: c.project.id,
|
|
576
|
+
slug: c.project.slug,
|
|
577
|
+
path: c.project.path,
|
|
578
|
+
}));
|
|
579
|
+
void (0, legacy_migration_1.runLegacyMigrationSweep)(projects, { providers: snapshot.detected });
|
|
580
|
+
})
|
|
581
|
+
.catch((err) => {
|
|
582
|
+
console.error('[provider-detection] startup detection failed (non-fatal):', err);
|
|
583
|
+
});
|
|
545
584
|
// ─── Mobile companion gateway (off by default; boot if previously enabled) ──
|
|
546
585
|
// Second HTTPS+WSS listener in THIS process; the main server stays loopback.
|
|
547
586
|
// The control plane is desktop-only: loopback + (via /api below) requireAuth.
|
|
@@ -687,6 +687,49 @@ class JiraSyncManager {
|
|
|
687
687
|
* reconciles it to the issue's real status (protected meanwhile by the
|
|
688
688
|
* pending-transition frozen guard).
|
|
689
689
|
*/
|
|
690
|
+
/**
|
|
691
|
+
* Manual "move to <Jira status>" from the spec detail's status selector
|
|
692
|
+
* (global-core-zero-friction follow-up): enqueue an explicit-target
|
|
693
|
+
* transition to the NAMED raw Jira status. The logical state is derived by
|
|
694
|
+
* reverse statusMap lookup (falling back to `in_progress` — the resolver
|
|
695
|
+
* honours the explicit target regardless), and the local cache is
|
|
696
|
+
* optimistically flipped so the board reflects the move immediately; the
|
|
697
|
+
* inbound poll reconciles to the issue's real status afterwards.
|
|
698
|
+
*/
|
|
699
|
+
moveSpecToStatus(localId, targetStatus) {
|
|
700
|
+
if (!this.isActive())
|
|
701
|
+
return { ok: false, reason: 'not-active' };
|
|
702
|
+
const link = (0, jira_db_1.getLinkByLocalId)(this.db, localId);
|
|
703
|
+
if (!link || link.tombstoned)
|
|
704
|
+
return { ok: false, reason: 'no-link' };
|
|
705
|
+
const conn = (0, jira_db_1.getConnection)(this.db, this.projectId);
|
|
706
|
+
const map = conn?.statusMap ?? null;
|
|
707
|
+
let logical = 'in_progress';
|
|
708
|
+
if (map) {
|
|
709
|
+
for (const [state, name] of Object.entries(map)) {
|
|
710
|
+
if (typeof name === 'string' && name.toLowerCase() === targetStatus.toLowerCase()) {
|
|
711
|
+
logical = state;
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
const nonce = Date.now().toString(36);
|
|
717
|
+
(0, jira_db_1.enqueueMany)(this.db, [{
|
|
718
|
+
jiraIssueId: link.jiraIssueId,
|
|
719
|
+
opType: 'transition',
|
|
720
|
+
idempotencyKey: `manual-move:${localId}:${nonce}`,
|
|
721
|
+
payload: { localId, jiraIssueId: link.jiraIssueId, logicalState: logical, targetStatus },
|
|
722
|
+
}]);
|
|
723
|
+
const localStatus = logical === 'todo' ? 'todo'
|
|
724
|
+
: logical === 'done' ? 'done'
|
|
725
|
+
: logical === 'cancelled' ? 'cancelled'
|
|
726
|
+
: logical === 'on_review' ? 'on_review'
|
|
727
|
+
: 'in_progress';
|
|
728
|
+
this.writeLocalStatus([localId], localStatus, targetStatus);
|
|
729
|
+
this.broadcastOutboxState();
|
|
730
|
+
void this.drainOnce().catch(() => undefined);
|
|
731
|
+
return { ok: true };
|
|
732
|
+
}
|
|
690
733
|
discardSpec(localId, comment) {
|
|
691
734
|
if (!this.isActive())
|
|
692
735
|
return { ok: false, reason: 'not-active' };
|
|
@@ -1156,7 +1199,7 @@ class JiraSyncManager {
|
|
|
1156
1199
|
void this.drainOnce().catch(() => undefined);
|
|
1157
1200
|
}
|
|
1158
1201
|
// ─── Local cache helpers ───────────────────────────────────────────────────
|
|
1159
|
-
writeLocalStatus(localIds, status) {
|
|
1202
|
+
writeLocalStatus(localIds, status, rawJiraStatus) {
|
|
1160
1203
|
if (localIds.length === 0)
|
|
1161
1204
|
return;
|
|
1162
1205
|
try {
|
|
@@ -1166,8 +1209,12 @@ class JiraSyncManager {
|
|
|
1166
1209
|
const store = (0, ticket_store_1.mutateStore)(file, (s) => {
|
|
1167
1210
|
for (const id of ids) {
|
|
1168
1211
|
const t = s.tickets[id];
|
|
1169
|
-
if (t && t.status !== status) {
|
|
1212
|
+
if (t && (t.status !== status || (rawJiraStatus !== undefined && t.jira_status !== rawJiraStatus))) {
|
|
1170
1213
|
t.status = status;
|
|
1214
|
+
// Manual move-to-status: reflect the RAW target immediately so the
|
|
1215
|
+
// Jira-status filter dimension updates without waiting for the poll.
|
|
1216
|
+
if (rawJiraStatus !== undefined)
|
|
1217
|
+
t.jira_status = rawJiraStatus;
|
|
1171
1218
|
t.updated_at = now;
|
|
1172
1219
|
}
|
|
1173
1220
|
}
|
|
@@ -245,6 +245,27 @@ function createJiraRouter() {
|
|
|
245
245
|
}
|
|
246
246
|
res.status(201).json({ localId: result.localId, jiraKey: result.jiraKey });
|
|
247
247
|
});
|
|
248
|
+
// POST /specs/:localId/move-to-status — manual transition to a NAMED raw Jira
|
|
249
|
+
// status from the spec detail's status selector. Body: { status: string }.
|
|
250
|
+
router.post('/specs/:localId/move-to-status', (req, res) => {
|
|
251
|
+
const c = ctx(req);
|
|
252
|
+
const localId = parseInt(req.params.localId, 10);
|
|
253
|
+
if (Number.isNaN(localId)) {
|
|
254
|
+
res.status(400).json({ error: 'Invalid spec id' });
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const status = req.body?.status;
|
|
258
|
+
if (!isNonEmptyString(status)) {
|
|
259
|
+
res.status(400).json({ error: 'status is required' });
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const result = c.jiraSyncManager.moveSpecToStatus(localId, status.trim());
|
|
263
|
+
if (!result.ok) {
|
|
264
|
+
res.status(result.reason === 'no-link' ? 404 : 409).json({ error: result.reason });
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
res.status(202).json({ ok: true });
|
|
268
|
+
});
|
|
248
269
|
// POST /specs/:localId/discard — "Move to <discard status>": transition the
|
|
249
270
|
// linked issue to the configured discard status (+ optional reason comment)
|
|
250
271
|
// instead of a destructive delete. Body: { comment?: string }.
|