specrails-desktop 2.8.0 → 2.9.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/README.md +23 -19
- package/client/dist/assets/{ActivityFeedPage-LKqd18-G.js → ActivityFeedPage-DNqnf1fZ.js} +1 -1
- package/client/dist/assets/{AgentsPage-Cb-b-6Ot.js → AgentsPage-vmNIEbGM.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-HVxQQ1wy.js → AnalyticsPage-CdfN0ofZ.js} +1 -1
- package/client/dist/assets/{BarChart-BOyHB0dw.js → BarChart-CIkopHjl.js} +1 -1
- package/client/dist/assets/{CodePage-DnOnwKGB.js → CodePage-DDRNU5FN.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-D2auU39x.js → DesktopAnalyticsPage-Cl3sKKSG.js} +1 -1
- package/client/dist/assets/{DocsDialog-CTuDX3GK.js → DocsDialog-BGrBOfUr.js} +2 -2
- package/client/dist/assets/{DocsPage-DRyMmu0Z.js → DocsPage-CY-2SSzw.js} +2 -2
- package/client/dist/assets/{ExportDropdown-DO-GGiMh.js → ExportDropdown-BRHcvP0r.js} +1 -1
- package/client/dist/assets/{IntegrationsPage-BhbO4jFT.js → IntegrationsPage-nKdLB4Ub.js} +1 -1
- package/client/dist/assets/{JobDetailPage-DJooEg1s.js → JobDetailPage-Bf0A6WWQ.js} +1 -1
- package/client/dist/assets/{JobsPage-BbaC-YOg.js → JobsPage-Vg4nXPdL.js} +1 -1
- package/client/dist/assets/{dist-js-CiIVMsx3.js → dist-js-0i_klubI.js} +1 -1
- package/client/dist/assets/{dist-js-Xc2lRKp2.js → dist-js-CUs5GjwA.js} +1 -1
- package/client/dist/assets/{index-DK214dak.js → index-BXoHFtfG.js} +8 -8
- package/client/dist/assets/index-D6BaYRRU.css +2 -0
- package/client/dist/assets/{integrations-2C7MkGT0.js → integrations-7YyTBuU9.js} +1 -1
- package/client/dist/assets/{integrations-CX4p_bij.js → integrations-B9CEpNF0.js} +1 -1
- package/client/dist/assets/{integrations-C2jQtv-s.js → integrations-BlvAdewo.js} +1 -1
- package/client/dist/assets/{integrations-eQPHAYsE.js → integrations-Bw8UM9Xd.js} +1 -1
- package/client/dist/assets/{integrations-BDC670cg.js → integrations-C5SxNKnG.js} +1 -1
- package/client/dist/assets/{integrations-BqUmRUef.js → integrations-CJQKMmdW.js} +1 -1
- package/client/dist/assets/{integrations-CB98NeH5.js → integrations-DWz1eU_K.js} +1 -1
- package/client/dist/assets/{integrations-_SuVeQIG.js → integrations-DiPR8Fzp.js} +1 -1
- package/client/dist/assets/{lib-Bo5s6xpe.js → lib-D6M_MvoC.js} +1 -1
- package/client/dist/assets/setup-B6egeeTM.js +1 -0
- package/client/dist/assets/setup-BHroXlke.js +1 -0
- package/client/dist/assets/setup-BIXsWUp1.js +1 -0
- package/client/dist/assets/setup-BJRdg1iE.js +1 -0
- package/client/dist/assets/setup-C0rVGnCy.js +1 -0
- package/client/dist/assets/setup-Cpu17hJv.js +1 -0
- package/client/dist/assets/setup-D-1r0uSx.js +1 -0
- package/client/dist/assets/setup-Dn2-veYO.js +1 -0
- package/client/dist/assets/{useProjectCache-DVNypkmR.js → useProjectCache-BeyBSNpD.js} +1 -1
- package/client/dist/index.html +4 -4
- package/docs/README.md +5 -2
- package/docs/agy-cli-provider-study.md +78 -0
- package/docs/cli.md +23 -4
- package/docs/codex.md +116 -58
- package/docs/creating-specs.md +19 -5
- package/docs/customizing.md +27 -6
- package/docs/gemini.md +225 -73
- package/docs/getting-started.md +18 -9
- package/docs/guide/de/agents/1-meet-the-agents.md +38 -0
- package/docs/guide/de/agents/2-profiles-and-the-balanced-default.md +45 -0
- package/docs/guide/de/agents/3-customizing-models-per-agent.md +60 -0
- package/docs/guide/de/agents/4-custom-agents-catalog.md +43 -0
- package/docs/guide/de/getting-started/1-what-is-specrails.md +49 -0
- package/docs/guide/de/getting-started/2-installing-and-first-run.md +42 -0
- package/docs/guide/de/getting-started/3-adding-your-first-project.md +58 -0
- package/docs/guide/de/getting-started/4-the-dashboard-tour.md +53 -0
- package/docs/guide/de/insights/1-analytics-and-cost-tracking.md +78 -0
- package/docs/guide/de/insights/2-the-integrated-terminal.md +46 -0
- package/docs/guide/de/insights/3-code-explorer.md +50 -0
- package/docs/guide/de/integrations/1-ai-providers.md +64 -0
- package/docs/guide/de/integrations/2-plugins.md +44 -0
- package/docs/guide/de/integrations/3-jira-integration.md +71 -0
- package/docs/guide/de/integrations/4-mobile-companion.md +38 -0
- package/docs/guide/de/pipeline/1-rails-and-jobs.md +94 -0
- package/docs/guide/de/pipeline/2-the-job-detail-view.md +90 -0
- package/docs/guide/de/pipeline/3-batch-implement-and-multi-feature.md +78 -0
- package/docs/guide/de/pipeline/4-picking-an-engine-per-rail.md +60 -0
- package/docs/guide/de/settings/1-themes.md +37 -0
- package/docs/guide/de/settings/2-language.md +39 -0
- package/docs/guide/de/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
- package/docs/guide/de/settings/4-where-your-data-lives.md +48 -0
- package/docs/guide/de/specs/1-specs-and-the-backlog.md +52 -0
- package/docs/guide/de/specs/2-add-spec-quick-mode.md +45 -0
- package/docs/guide/de/specs/3-add-spec-explore-mode.md +68 -0
- package/docs/guide/de/specs/4-drafts-and-contract-layer.md +81 -0
- package/docs/guide/en/agents/1-meet-the-agents.md +38 -0
- package/docs/guide/en/agents/2-profiles-and-the-balanced-default.md +45 -0
- package/docs/guide/en/agents/3-customizing-models-per-agent.md +60 -0
- package/docs/guide/en/agents/4-custom-agents-catalog.md +43 -0
- package/docs/guide/en/getting-started/1-what-is-specrails.md +49 -0
- package/docs/guide/en/getting-started/2-installing-and-first-run.md +42 -0
- package/docs/guide/en/getting-started/3-adding-your-first-project.md +58 -0
- package/docs/guide/en/getting-started/4-the-dashboard-tour.md +53 -0
- package/docs/guide/en/insights/1-analytics-and-cost-tracking.md +78 -0
- package/docs/guide/en/insights/2-the-integrated-terminal.md +46 -0
- package/docs/guide/en/insights/3-code-explorer.md +50 -0
- package/docs/guide/en/integrations/1-ai-providers.md +64 -0
- package/docs/guide/en/integrations/2-plugins.md +44 -0
- package/docs/guide/en/integrations/3-jira-integration.md +71 -0
- package/docs/guide/en/integrations/4-mobile-companion.md +38 -0
- package/docs/guide/en/pipeline/1-rails-and-jobs.md +94 -0
- package/docs/guide/en/pipeline/2-the-job-detail-view.md +90 -0
- package/docs/guide/en/pipeline/3-batch-implement-and-multi-feature.md +78 -0
- package/docs/guide/en/pipeline/4-picking-an-engine-per-rail.md +60 -0
- package/docs/guide/en/settings/1-themes.md +37 -0
- package/docs/guide/en/settings/2-language.md +39 -0
- package/docs/guide/en/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
- package/docs/guide/en/settings/4-where-your-data-lives.md +48 -0
- package/docs/guide/en/specs/1-specs-and-the-backlog.md +52 -0
- package/docs/guide/en/specs/2-add-spec-quick-mode.md +45 -0
- package/docs/guide/en/specs/3-add-spec-explore-mode.md +68 -0
- package/docs/guide/en/specs/4-drafts-and-contract-layer.md +81 -0
- package/docs/guide/es/agents/1-meet-the-agents.md +38 -0
- package/docs/guide/es/agents/2-profiles-and-the-balanced-default.md +45 -0
- package/docs/guide/es/agents/3-customizing-models-per-agent.md +60 -0
- package/docs/guide/es/agents/4-custom-agents-catalog.md +43 -0
- package/docs/guide/es/getting-started/1-what-is-specrails.md +49 -0
- package/docs/guide/es/getting-started/2-installing-and-first-run.md +42 -0
- package/docs/guide/es/getting-started/3-adding-your-first-project.md +58 -0
- package/docs/guide/es/getting-started/4-the-dashboard-tour.md +53 -0
- package/docs/guide/es/insights/1-analytics-and-cost-tracking.md +78 -0
- package/docs/guide/es/insights/2-the-integrated-terminal.md +46 -0
- package/docs/guide/es/insights/3-code-explorer.md +50 -0
- package/docs/guide/es/integrations/1-ai-providers.md +64 -0
- package/docs/guide/es/integrations/2-plugins.md +44 -0
- package/docs/guide/es/integrations/3-jira-integration.md +71 -0
- package/docs/guide/es/integrations/4-mobile-companion.md +38 -0
- package/docs/guide/es/pipeline/1-rails-and-jobs.md +94 -0
- package/docs/guide/es/pipeline/2-the-job-detail-view.md +90 -0
- package/docs/guide/es/pipeline/3-batch-implement-and-multi-feature.md +78 -0
- package/docs/guide/es/pipeline/4-picking-an-engine-per-rail.md +60 -0
- package/docs/guide/es/settings/1-themes.md +37 -0
- package/docs/guide/es/settings/2-language.md +39 -0
- package/docs/guide/es/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
- package/docs/guide/es/settings/4-where-your-data-lives.md +48 -0
- package/docs/guide/es/specs/1-specs-and-the-backlog.md +52 -0
- package/docs/guide/es/specs/2-add-spec-quick-mode.md +45 -0
- package/docs/guide/es/specs/3-add-spec-explore-mode.md +68 -0
- package/docs/guide/es/specs/4-drafts-and-contract-layer.md +81 -0
- package/docs/guide/fr/agents/1-meet-the-agents.md +38 -0
- package/docs/guide/fr/agents/2-profiles-and-the-balanced-default.md +45 -0
- package/docs/guide/fr/agents/3-customizing-models-per-agent.md +60 -0
- package/docs/guide/fr/agents/4-custom-agents-catalog.md +43 -0
- package/docs/guide/fr/getting-started/1-what-is-specrails.md +49 -0
- package/docs/guide/fr/getting-started/2-installing-and-first-run.md +42 -0
- package/docs/guide/fr/getting-started/3-adding-your-first-project.md +58 -0
- package/docs/guide/fr/getting-started/4-the-dashboard-tour.md +53 -0
- package/docs/guide/fr/insights/1-analytics-and-cost-tracking.md +78 -0
- package/docs/guide/fr/insights/2-the-integrated-terminal.md +46 -0
- package/docs/guide/fr/insights/3-code-explorer.md +50 -0
- package/docs/guide/fr/integrations/1-ai-providers.md +64 -0
- package/docs/guide/fr/integrations/2-plugins.md +44 -0
- package/docs/guide/fr/integrations/3-jira-integration.md +71 -0
- package/docs/guide/fr/integrations/4-mobile-companion.md +38 -0
- package/docs/guide/fr/pipeline/1-rails-and-jobs.md +94 -0
- package/docs/guide/fr/pipeline/2-the-job-detail-view.md +90 -0
- package/docs/guide/fr/pipeline/3-batch-implement-and-multi-feature.md +78 -0
- package/docs/guide/fr/pipeline/4-picking-an-engine-per-rail.md +60 -0
- package/docs/guide/fr/settings/1-themes.md +37 -0
- package/docs/guide/fr/settings/2-language.md +39 -0
- package/docs/guide/fr/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
- package/docs/guide/fr/settings/4-where-your-data-lives.md +48 -0
- package/docs/guide/fr/specs/1-specs-and-the-backlog.md +52 -0
- package/docs/guide/fr/specs/2-add-spec-quick-mode.md +45 -0
- package/docs/guide/fr/specs/3-add-spec-explore-mode.md +68 -0
- package/docs/guide/fr/specs/4-drafts-and-contract-layer.md +81 -0
- package/docs/guide/it/agents/1-meet-the-agents.md +38 -0
- package/docs/guide/it/agents/2-profiles-and-the-balanced-default.md +45 -0
- package/docs/guide/it/agents/3-customizing-models-per-agent.md +60 -0
- package/docs/guide/it/agents/4-custom-agents-catalog.md +43 -0
- package/docs/guide/it/getting-started/1-what-is-specrails.md +49 -0
- package/docs/guide/it/getting-started/2-installing-and-first-run.md +42 -0
- package/docs/guide/it/getting-started/3-adding-your-first-project.md +58 -0
- package/docs/guide/it/getting-started/4-the-dashboard-tour.md +53 -0
- package/docs/guide/it/insights/1-analytics-and-cost-tracking.md +78 -0
- package/docs/guide/it/insights/2-the-integrated-terminal.md +46 -0
- package/docs/guide/it/insights/3-code-explorer.md +50 -0
- package/docs/guide/it/integrations/1-ai-providers.md +64 -0
- package/docs/guide/it/integrations/2-plugins.md +44 -0
- package/docs/guide/it/integrations/3-jira-integration.md +71 -0
- package/docs/guide/it/integrations/4-mobile-companion.md +38 -0
- package/docs/guide/it/pipeline/1-rails-and-jobs.md +94 -0
- package/docs/guide/it/pipeline/2-the-job-detail-view.md +90 -0
- package/docs/guide/it/pipeline/3-batch-implement-and-multi-feature.md +78 -0
- package/docs/guide/it/pipeline/4-picking-an-engine-per-rail.md +60 -0
- package/docs/guide/it/settings/1-themes.md +37 -0
- package/docs/guide/it/settings/2-language.md +39 -0
- package/docs/guide/it/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
- package/docs/guide/it/settings/4-where-your-data-lives.md +48 -0
- package/docs/guide/it/specs/1-specs-and-the-backlog.md +52 -0
- package/docs/guide/it/specs/2-add-spec-quick-mode.md +45 -0
- package/docs/guide/it/specs/3-add-spec-explore-mode.md +68 -0
- package/docs/guide/it/specs/4-drafts-and-contract-layer.md +81 -0
- package/docs/guide/ja/agents/1-meet-the-agents.md +38 -0
- package/docs/guide/ja/agents/2-profiles-and-the-balanced-default.md +45 -0
- package/docs/guide/ja/agents/3-customizing-models-per-agent.md +60 -0
- package/docs/guide/ja/agents/4-custom-agents-catalog.md +43 -0
- package/docs/guide/ja/getting-started/1-what-is-specrails.md +49 -0
- package/docs/guide/ja/getting-started/2-installing-and-first-run.md +42 -0
- package/docs/guide/ja/getting-started/3-adding-your-first-project.md +58 -0
- package/docs/guide/ja/getting-started/4-the-dashboard-tour.md +53 -0
- package/docs/guide/ja/insights/1-analytics-and-cost-tracking.md +78 -0
- package/docs/guide/ja/insights/2-the-integrated-terminal.md +46 -0
- package/docs/guide/ja/insights/3-code-explorer.md +50 -0
- package/docs/guide/ja/integrations/1-ai-providers.md +64 -0
- package/docs/guide/ja/integrations/2-plugins.md +44 -0
- package/docs/guide/ja/integrations/3-jira-integration.md +71 -0
- package/docs/guide/ja/integrations/4-mobile-companion.md +38 -0
- package/docs/guide/ja/pipeline/1-rails-and-jobs.md +94 -0
- package/docs/guide/ja/pipeline/2-the-job-detail-view.md +90 -0
- package/docs/guide/ja/pipeline/3-batch-implement-and-multi-feature.md +78 -0
- package/docs/guide/ja/pipeline/4-picking-an-engine-per-rail.md +60 -0
- package/docs/guide/ja/settings/1-themes.md +37 -0
- package/docs/guide/ja/settings/2-language.md +39 -0
- package/docs/guide/ja/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
- package/docs/guide/ja/settings/4-where-your-data-lives.md +48 -0
- package/docs/guide/ja/specs/1-specs-and-the-backlog.md +52 -0
- package/docs/guide/ja/specs/2-add-spec-quick-mode.md +45 -0
- package/docs/guide/ja/specs/3-add-spec-explore-mode.md +68 -0
- package/docs/guide/ja/specs/4-drafts-and-contract-layer.md +81 -0
- package/docs/guide/pt/agents/1-meet-the-agents.md +38 -0
- package/docs/guide/pt/agents/2-profiles-and-the-balanced-default.md +45 -0
- package/docs/guide/pt/agents/3-customizing-models-per-agent.md +60 -0
- package/docs/guide/pt/agents/4-custom-agents-catalog.md +43 -0
- package/docs/guide/pt/getting-started/1-what-is-specrails.md +49 -0
- package/docs/guide/pt/getting-started/2-installing-and-first-run.md +42 -0
- package/docs/guide/pt/getting-started/3-adding-your-first-project.md +58 -0
- package/docs/guide/pt/getting-started/4-the-dashboard-tour.md +53 -0
- package/docs/guide/pt/insights/1-analytics-and-cost-tracking.md +78 -0
- package/docs/guide/pt/insights/2-the-integrated-terminal.md +46 -0
- package/docs/guide/pt/insights/3-code-explorer.md +50 -0
- package/docs/guide/pt/integrations/1-ai-providers.md +64 -0
- package/docs/guide/pt/integrations/2-plugins.md +44 -0
- package/docs/guide/pt/integrations/3-jira-integration.md +71 -0
- package/docs/guide/pt/integrations/4-mobile-companion.md +38 -0
- package/docs/guide/pt/pipeline/1-rails-and-jobs.md +94 -0
- package/docs/guide/pt/pipeline/2-the-job-detail-view.md +90 -0
- package/docs/guide/pt/pipeline/3-batch-implement-and-multi-feature.md +78 -0
- package/docs/guide/pt/pipeline/4-picking-an-engine-per-rail.md +60 -0
- package/docs/guide/pt/settings/1-themes.md +37 -0
- package/docs/guide/pt/settings/2-language.md +39 -0
- package/docs/guide/pt/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
- package/docs/guide/pt/settings/4-where-your-data-lives.md +48 -0
- package/docs/guide/pt/specs/1-specs-and-the-backlog.md +52 -0
- package/docs/guide/pt/specs/2-add-spec-quick-mode.md +45 -0
- package/docs/guide/pt/specs/3-add-spec-explore-mode.md +68 -0
- package/docs/guide/pt/specs/4-drafts-and-contract-layer.md +81 -0
- package/docs/guide/zh/agents/1-meet-the-agents.md +38 -0
- package/docs/guide/zh/agents/2-profiles-and-the-balanced-default.md +45 -0
- package/docs/guide/zh/agents/3-customizing-models-per-agent.md +60 -0
- package/docs/guide/zh/agents/4-custom-agents-catalog.md +43 -0
- package/docs/guide/zh/getting-started/1-what-is-specrails.md +49 -0
- package/docs/guide/zh/getting-started/2-installing-and-first-run.md +42 -0
- package/docs/guide/zh/getting-started/3-adding-your-first-project.md +58 -0
- package/docs/guide/zh/getting-started/4-the-dashboard-tour.md +53 -0
- package/docs/guide/zh/insights/1-analytics-and-cost-tracking.md +78 -0
- package/docs/guide/zh/insights/2-the-integrated-terminal.md +46 -0
- package/docs/guide/zh/insights/3-code-explorer.md +50 -0
- package/docs/guide/zh/integrations/1-ai-providers.md +64 -0
- package/docs/guide/zh/integrations/2-plugins.md +44 -0
- package/docs/guide/zh/integrations/3-jira-integration.md +71 -0
- package/docs/guide/zh/integrations/4-mobile-companion.md +38 -0
- package/docs/guide/zh/pipeline/1-rails-and-jobs.md +94 -0
- package/docs/guide/zh/pipeline/2-the-job-detail-view.md +90 -0
- package/docs/guide/zh/pipeline/3-batch-implement-and-multi-feature.md +78 -0
- package/docs/guide/zh/pipeline/4-picking-an-engine-per-rail.md +60 -0
- package/docs/guide/zh/settings/1-themes.md +37 -0
- package/docs/guide/zh/settings/2-language.md +39 -0
- package/docs/guide/zh/settings/3-pipeline-telemetry-and-diagnostics.md +46 -0
- package/docs/guide/zh/settings/4-where-your-data-lives.md +48 -0
- package/docs/guide/zh/specs/1-specs-and-the-backlog.md +52 -0
- package/docs/guide/zh/specs/2-add-spec-quick-mode.md +45 -0
- package/docs/guide/zh/specs/3-add-spec-explore-mode.md +68 -0
- package/docs/guide/zh/specs/4-drafts-and-contract-layer.md +81 -0
- package/docs/internals/README.md +1 -1
- package/docs/internals/adding-a-provider.md +192 -59
- package/docs/internals/api-reference.md +130 -21
- package/docs/internals/architecture.md +22 -9
- package/docs/internals/bundled-framework-build-plan.md +264 -0
- package/docs/internals/configuration.md +33 -8
- package/docs/internals/global-artifacts-alignment-contract.md +486 -0
- package/docs/internals/global-artifacts-relocation-evaluation.md +294 -0
- package/docs/internals/operations-runbook.md +16 -5
- package/docs/internals/profiles.md +42 -14
- package/docs/platforms/macos.md +27 -8
- package/docs/platforms/windows.md +20 -6
- package/docs/running-pipelines.md +17 -9
- package/docs/terminal.md +9 -3
- package/docs/tracking-cost.md +17 -11
- package/package.json +1 -1
- package/server/dist/agent-refine-manager.js +20 -5
- package/server/dist/artifact-registry.js +468 -0
- package/server/dist/attachment-manager.js +5 -8
- package/server/dist/browser-capture-manager.js +4 -4
- package/server/dist/bundled-core.js +72 -0
- package/server/dist/bundled-openspec.js +58 -0
- package/server/dist/chat-manager.js +42 -5
- package/server/dist/code-explorer-router.js +10 -7
- package/server/dist/config.js +7 -2
- package/server/dist/context-budget.js +17 -6
- package/server/dist/context-scope.js +6 -2
- package/server/dist/contract-refine-runner.js +31 -9
- package/server/dist/desktop-router.js +24 -1
- package/server/dist/docs-router.js +210 -132
- package/server/dist/file-summary-manager.js +41 -16
- package/server/dist/framework-manager.js +248 -0
- package/server/dist/framework-migration.js +308 -0
- package/server/dist/index.js +30 -0
- package/server/dist/install-config-path.js +73 -0
- package/server/dist/jira/jira-sync-manager.js +23 -11
- package/server/dist/openspec-shim.js +153 -0
- package/server/dist/plugins-router.js +19 -8
- package/server/dist/profiles-router.js +38 -16
- package/server/dist/project-registry.js +101 -3
- package/server/dist/project-router-chat.js +1 -1
- package/server/dist/project-router-helpers.js +25 -12
- package/server/dist/project-router-jobs.js +3 -3
- package/server/dist/project-router-settings.js +8 -6
- package/server/dist/project-router-setup.js +27 -10
- package/server/dist/project-router-spending.js +6 -1
- package/server/dist/project-router-tickets.js +30 -10
- package/server/dist/project-router.js +16 -1
- package/server/dist/queue-manager.js +149 -18
- package/server/dist/setup-manager.js +131 -29
- package/server/dist/smash-runner.js +21 -6
- package/server/dist/ticket-store.js +6 -2
- package/server/dist/ticket-watcher.js +5 -1
- package/server/dist/vitest-setup.js +25 -0
- package/server/dist/workspace-manager.js +199 -0
- package/server/dist/workspace-resolution.js +147 -0
- package/client/dist/assets/index-DgKfQFcf.css +0 -2
- package/client/dist/assets/setup-BIIkb-_K.js +0 -1
- package/client/dist/assets/setup-BeQxu9kD.js +0 -1
- package/client/dist/assets/setup-CPa6GnlI.js +0 -1
- package/client/dist/assets/setup-CZl4OEJx.js +0 -1
- package/client/dist/assets/setup-ChpodNfn.js +0 -1
- package/client/dist/assets/setup-D_fjJH6u.js +0 -1
- package/client/dist/assets/setup-YzD8DX4O.js +0 -1
- package/client/dist/assets/setup-fRpDozmq.js +0 -1
- package/docs/adding-a-provider.md +0 -107
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Shared artifact registry — the cross-tool contract that lets specrails-core
|
|
4
|
+
* place, and specrails-desktop write/read, a repo's relocated artifacts OUTSIDE
|
|
5
|
+
* the repo, under `$HOME/.specrails/projects/<slug>/workspace`.
|
|
6
|
+
*
|
|
7
|
+
* Single source of truth: `$HOME/.specrails/registry.json`, an inspectable,
|
|
8
|
+
* schema-versioned JSON file keyed by the **canonical realpath of the repo**.
|
|
9
|
+
* specrails-desktop is the PRIMARY writer (a projection of its `desktop.sqlite`);
|
|
10
|
+
* specrails-core reads it and, when run standalone, allocates its own entry.
|
|
11
|
+
*
|
|
12
|
+
* This module is a port of specrails-core's `src/installer/util/registry.ts`.
|
|
13
|
+
* The slug algorithm, canonical-key rule, atomic-write rule, lock protocol and
|
|
14
|
+
* workspace layout here MUST stay byte-identical to core — the correctness of
|
|
15
|
+
* the cross-tool contract depends on both tools resolving the same repo to the
|
|
16
|
+
* same paths. Everything is synchronous to match the installer's sync flow and
|
|
17
|
+
* to keep desktop's startup reconcile cheap.
|
|
18
|
+
*
|
|
19
|
+
* Beyond the ported core surface, this module adds the desktop-only writers that
|
|
20
|
+
* project `desktop.sqlite` into the registry: `mirrorProjectEntry`,
|
|
21
|
+
* `removeRegistryEntry`, and `reconcileFromProjects`.
|
|
22
|
+
*/
|
|
23
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
24
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
25
|
+
};
|
|
26
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
27
|
+
exports.REGISTRY_SCHEMA_VERSION = void 0;
|
|
28
|
+
exports.slugify = slugify;
|
|
29
|
+
exports.resolveHome = resolveHome;
|
|
30
|
+
exports.registryPath = registryPath;
|
|
31
|
+
exports.lockPath = lockPath;
|
|
32
|
+
exports.realpathSafe = realpathSafe;
|
|
33
|
+
exports.normalizeKey = normalizeKey;
|
|
34
|
+
exports.canonicalizeRepoPath = canonicalizeRepoPath;
|
|
35
|
+
exports.workspaceLayout = workspaceLayout;
|
|
36
|
+
exports.isCompleteEntry = isCompleteEntry;
|
|
37
|
+
exports.readRegistryOrEmpty = readRegistryOrEmpty;
|
|
38
|
+
exports.atomicWrite = atomicWrite;
|
|
39
|
+
exports.withFileLock = withFileLock;
|
|
40
|
+
exports.mirrorProjectEntry = mirrorProjectEntry;
|
|
41
|
+
exports.resolveArtifacts = resolveArtifacts;
|
|
42
|
+
exports.removeRegistryEntry = removeRegistryEntry;
|
|
43
|
+
exports.reconcileFromProjects = reconcileFromProjects;
|
|
44
|
+
const fs_1 = require("fs");
|
|
45
|
+
const os_1 = __importDefault(require("os"));
|
|
46
|
+
const path_1 = __importDefault(require("path"));
|
|
47
|
+
/** Registry schema version. A reader that sees a higher value MUST treat all
|
|
48
|
+
* entries as absent (legacy fallback), never mis-parse. */
|
|
49
|
+
exports.REGISTRY_SCHEMA_VERSION = 1;
|
|
50
|
+
/**
|
|
51
|
+
* Slug derivation — byte-identical to `slugify` in `server/desktop-router.ts`
|
|
52
|
+
* (and to specrails-core's port). Do NOT "improve" it; parity is the contract.
|
|
53
|
+
*/
|
|
54
|
+
function slugify(name) {
|
|
55
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
56
|
+
}
|
|
57
|
+
/** `$HOME` for the registry, overridable for tests. Honors the
|
|
58
|
+
* `SPECRAILS_REGISTRY_HOME` env var (mirrors core) when no explicit override
|
|
59
|
+
* is passed, so the desktop-side wiring can be redirected in tests without
|
|
60
|
+
* threading a `home` argument through every call site. */
|
|
61
|
+
function resolveHome(home) {
|
|
62
|
+
return home ?? process.env.SPECRAILS_REGISTRY_HOME ?? os_1.default.homedir();
|
|
63
|
+
}
|
|
64
|
+
/** Absolute path to `registry.json`. */
|
|
65
|
+
function registryPath(home) {
|
|
66
|
+
return path_1.default.join(resolveHome(home), '.specrails', 'registry.json');
|
|
67
|
+
}
|
|
68
|
+
/** Absolute path to the advisory lock file. */
|
|
69
|
+
function lockPath(home) {
|
|
70
|
+
return registryPath(home) + '.lock';
|
|
71
|
+
}
|
|
72
|
+
/** `fs.realpathSync` that falls back to the resolved-but-unreal path on error
|
|
73
|
+
* (the path may not exist yet, or be on a volume that rejects realpath). */
|
|
74
|
+
function realpathSafe(abs) {
|
|
75
|
+
try {
|
|
76
|
+
return (0, fs_1.realpathSync)(abs);
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return abs;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Case-fold the key on case-insensitive platforms (macOS, Windows) so two
|
|
83
|
+
* spellings of the same path map to one entry. The stored `repoPath` keeps
|
|
84
|
+
* its canonical case; only the index key is folded. */
|
|
85
|
+
function normalizeKey(canon) {
|
|
86
|
+
if (process.platform === 'darwin' || process.platform === 'win32') {
|
|
87
|
+
return canon.toLowerCase();
|
|
88
|
+
}
|
|
89
|
+
return canon;
|
|
90
|
+
}
|
|
91
|
+
/** Canonical repo path: resolve to absolute, then realpath (collapses symlinks). */
|
|
92
|
+
function canonicalizeRepoPath(repoPathInput) {
|
|
93
|
+
return realpathSafe(path_1.default.resolve(repoPathInput));
|
|
94
|
+
}
|
|
95
|
+
/** The per-project sub-path layout under a workspace dir. Single source of the
|
|
96
|
+
* layout so writer and (allocation) reader never disagree. */
|
|
97
|
+
function workspaceLayout(home, slug, canon) {
|
|
98
|
+
const workspaceDir = path_1.default.join(resolveHome(home), '.specrails', 'projects', slug, 'workspace');
|
|
99
|
+
const specrailsDir = path_1.default.join(workspaceDir, '.specrails');
|
|
100
|
+
return {
|
|
101
|
+
repoPath: canon,
|
|
102
|
+
slug,
|
|
103
|
+
workspaceDir,
|
|
104
|
+
artifactRoot: workspaceDir,
|
|
105
|
+
codeRoot: canon,
|
|
106
|
+
stateDir: path_1.default.join(workspaceDir, '.claude'),
|
|
107
|
+
ticketsPath: path_1.default.join(specrailsDir, 'local-tickets.json'),
|
|
108
|
+
backlogConfigPath: path_1.default.join(specrailsDir, 'backlog-config.json'),
|
|
109
|
+
profilesDir: path_1.default.join(specrailsDir, 'profiles'),
|
|
110
|
+
pluginsStateDir: path_1.default.join(specrailsDir, 'plugins'),
|
|
111
|
+
fileSummariesDir: path_1.default.join(specrailsDir, 'file-summaries'),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The path/identity fields a usable entry MUST carry. A hand-edited or
|
|
115
|
+
* partially-written entry missing ANY of these is treated as ABSENT (legacy
|
|
116
|
+
* fallback) rather than returned with `undefined` path fields that would crash
|
|
117
|
+
* a downstream `fs` call or silently point at `undefined/...`. */
|
|
118
|
+
const REQUIRED_ENTRY_FIELDS = [
|
|
119
|
+
'repoPath',
|
|
120
|
+
'slug',
|
|
121
|
+
'workspaceDir',
|
|
122
|
+
'artifactRoot',
|
|
123
|
+
'codeRoot',
|
|
124
|
+
'stateDir',
|
|
125
|
+
'ticketsPath',
|
|
126
|
+
'backlogConfigPath',
|
|
127
|
+
'profilesDir',
|
|
128
|
+
'pluginsStateDir',
|
|
129
|
+
'fileSummariesDir',
|
|
130
|
+
];
|
|
131
|
+
/**
|
|
132
|
+
* True only when `entry` carries every required path/identity field as a
|
|
133
|
+
* non-empty string. Guards against a hand-edited registry.json with a missing
|
|
134
|
+
* key (which would otherwise surface as `ticketsPath: undefined`).
|
|
135
|
+
*/
|
|
136
|
+
function isCompleteEntry(entry) {
|
|
137
|
+
if (!entry || typeof entry !== 'object')
|
|
138
|
+
return false;
|
|
139
|
+
const e = entry;
|
|
140
|
+
for (const f of REQUIRED_ENTRY_FIELDS) {
|
|
141
|
+
const v = e[f];
|
|
142
|
+
if (typeof v !== 'string' || v.length === 0)
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Total, fail-open read. A missing file, a parse error, or a `schemaVersion`
|
|
149
|
+
* greater than we understand all yield an empty registry, so a caller treats
|
|
150
|
+
* it as "no entry" and (if writing) writes a fresh, understood entry rather
|
|
151
|
+
* than crashing. Biases toward availability over strict consistency — correct
|
|
152
|
+
* for a local, inspectable file.
|
|
153
|
+
*/
|
|
154
|
+
function readRegistryOrEmpty(home) {
|
|
155
|
+
const empty = { schemaVersion: exports.REGISTRY_SCHEMA_VERSION, projects: {} };
|
|
156
|
+
const p = registryPath(home);
|
|
157
|
+
if (!(0, fs_1.existsSync)(p))
|
|
158
|
+
return empty;
|
|
159
|
+
try {
|
|
160
|
+
const parsed = JSON.parse((0, fs_1.readFileSync)(p, 'utf8'));
|
|
161
|
+
if (!parsed ||
|
|
162
|
+
typeof parsed !== 'object' ||
|
|
163
|
+
typeof parsed.schemaVersion !== 'number' ||
|
|
164
|
+
parsed.schemaVersion > exports.REGISTRY_SCHEMA_VERSION ||
|
|
165
|
+
typeof parsed.projects !== 'object' ||
|
|
166
|
+
parsed.projects === null) {
|
|
167
|
+
return empty;
|
|
168
|
+
}
|
|
169
|
+
return parsed;
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
return empty;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
/** Write `data` to `filePath` atomically: temp file in the same dir, fsync,
|
|
176
|
+
* rename. A reader (even without the lock) only ever sees a complete old or
|
|
177
|
+
* new file — the lock serialises writers, the rename protects readers. */
|
|
178
|
+
function atomicWrite(filePath, data) {
|
|
179
|
+
const dir = path_1.default.dirname(filePath);
|
|
180
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
181
|
+
// Deterministic-but-unique temp name (no Math.random/Date dependency for the
|
|
182
|
+
// name itself); collisions are impossible because the lock serialises writers.
|
|
183
|
+
const tmp = path_1.default.join(dir, `.${path_1.default.basename(filePath)}.tmp-${process.pid}`);
|
|
184
|
+
const fd = (0, fs_1.openSync)(tmp, 'w');
|
|
185
|
+
try {
|
|
186
|
+
(0, fs_1.writeFileSync)(fd, data);
|
|
187
|
+
(0, fs_1.fsyncSync)(fd);
|
|
188
|
+
}
|
|
189
|
+
finally {
|
|
190
|
+
(0, fs_1.closeSync)(fd);
|
|
191
|
+
}
|
|
192
|
+
(0, fs_1.renameSync)(tmp, filePath);
|
|
193
|
+
}
|
|
194
|
+
/** Synchronous sleep without busy-spinning the CPU. */
|
|
195
|
+
function syncSleep(ms) {
|
|
196
|
+
const shared = new Int32Array(new SharedArrayBuffer(4));
|
|
197
|
+
Atomics.wait(shared, 0, 0, ms);
|
|
198
|
+
}
|
|
199
|
+
const LOCK_STALE_MS = 30_000;
|
|
200
|
+
const LOCK_SPIN_MS = 50;
|
|
201
|
+
const LOCK_MAX_WAIT_MS = 2_000;
|
|
202
|
+
/**
|
|
203
|
+
* Run `fn` while holding an advisory lock over the registry. Mutual exclusion
|
|
204
|
+
* is an exclusive-create lock file (`open(..., 'wx')`) with bounded spin-retry
|
|
205
|
+
* and stale-lock breaking (a lock whose mtime exceeds the TTL is reclaimed —
|
|
206
|
+
* covers a crashed writer). Read-only callers never take the lock.
|
|
207
|
+
*/
|
|
208
|
+
function withFileLock(home, fn) {
|
|
209
|
+
const lp = lockPath(home);
|
|
210
|
+
(0, fs_1.mkdirSync)(path_1.default.dirname(lp), { recursive: true });
|
|
211
|
+
let fd;
|
|
212
|
+
// Unique per-acquisition token written INTO the lock file. On release we
|
|
213
|
+
// re-read the file and only unlink it when the token still matches OURS — so a
|
|
214
|
+
// writer whose lock was stale-broken (TTL-reclaimed) by another writer never
|
|
215
|
+
// deletes the NEW owner's lock out from under it (which would let a third
|
|
216
|
+
// writer acquire concurrently and race the read-modify-write).
|
|
217
|
+
const ourToken = `${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}`;
|
|
218
|
+
const deadline = Date.now() + LOCK_MAX_WAIT_MS;
|
|
219
|
+
for (;;) {
|
|
220
|
+
try {
|
|
221
|
+
fd = (0, fs_1.openSync)(lp, 'wx');
|
|
222
|
+
// Write our token + fsync so a concurrent reader sees a complete token.
|
|
223
|
+
try {
|
|
224
|
+
(0, fs_1.writeFileSync)(fd, ourToken);
|
|
225
|
+
(0, fs_1.fsyncSync)(fd);
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
/* best-effort — the lock still serialises via exclusive-create */
|
|
229
|
+
}
|
|
230
|
+
break;
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Lock held — break it if stale, otherwise spin until the deadline.
|
|
234
|
+
try {
|
|
235
|
+
const age = Date.now() - (0, fs_1.statSync)(lp).mtimeMs;
|
|
236
|
+
if (age > LOCK_STALE_MS) {
|
|
237
|
+
(0, fs_1.unlinkSync)(lp);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
// lock vanished between open and stat — retry immediately
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
if (Date.now() >= deadline) {
|
|
246
|
+
// FAIL-CLOSED (parity with specrails-core): rather than proceed WITHOUT
|
|
247
|
+
// the lock — which would let two writers race a read-modify-write and
|
|
248
|
+
// clobber each other's entries — throw. Every caller wraps this in a
|
|
249
|
+
// non-fatal try/catch (a missing/un-updated entry is recreated on the
|
|
250
|
+
// next reconcile), so failing closed loses nothing while preserving the
|
|
251
|
+
// single-writer invariant.
|
|
252
|
+
throw new Error(`artifact-registry: could not acquire lock ${lp} within ${LOCK_MAX_WAIT_MS}ms`);
|
|
253
|
+
}
|
|
254
|
+
syncSleep(LOCK_SPIN_MS);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
try {
|
|
258
|
+
return fn();
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
if (fd !== undefined) {
|
|
262
|
+
try {
|
|
263
|
+
(0, fs_1.closeSync)(fd);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
/* already closed */
|
|
267
|
+
}
|
|
268
|
+
// Only unlink the lock when it still carries OUR token. If we were
|
|
269
|
+
// stale-broken and another writer now owns the file, its token differs and
|
|
270
|
+
// we leave it alone (a missing/unreadable file ⇒ already gone ⇒ no-op).
|
|
271
|
+
try {
|
|
272
|
+
const onDisk = (0, fs_1.readFileSync)(lp, 'utf8');
|
|
273
|
+
if (onDisk === ourToken) {
|
|
274
|
+
(0, fs_1.unlinkSync)(lp);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
/* already removed / unreadable — nothing to clean up */
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
function normalizeProviders(providers, primary) {
|
|
284
|
+
let list = providers && providers.length > 0 ? providers.slice() : undefined;
|
|
285
|
+
if (!list || list.length === 0) {
|
|
286
|
+
list = [primary && primary.length > 0 ? primary : 'claude'];
|
|
287
|
+
}
|
|
288
|
+
const primaryProvider = primary && primary.length > 0 ? primary : list[0];
|
|
289
|
+
return { providers: list, primaryProvider };
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Build (or refresh) a desktop-owned entry for a repo, preserving artifact-
|
|
293
|
+
* location identity.
|
|
294
|
+
*
|
|
295
|
+
* INVARIANT: a `slug` + `workspaceDir` are **immutable once an entry exists**
|
|
296
|
+
* (the contract's "el slug queda congelado para siempre"). When ANY entry
|
|
297
|
+
* already exists for this repo — whether a prior desktop entry OR a
|
|
298
|
+
* `core-standalone` entry being adopted (possibly under a core-allocated slug
|
|
299
|
+
* that differs from desktop's) — we preserve EVERY path field verbatim and
|
|
300
|
+
* refresh only the mutable projection fields (providers, coreVersion, source,
|
|
301
|
+
* timestamps, desktopProjectId). Re-deriving the layout from `desiredSlug`
|
|
302
|
+
* would re-home an adopted/renamed entry and STRAND its on-disk artifacts.
|
|
303
|
+
*
|
|
304
|
+
* Only when no entry exists do we allocate fresh paths from `desiredSlug`.
|
|
305
|
+
*
|
|
306
|
+
* `touchInstall` = true for an install event (addProject) ⇒ bump
|
|
307
|
+
* `lastInstallAt`; false for a startup reconcile ⇒ leave it.
|
|
308
|
+
*/
|
|
309
|
+
function buildMirroredEntry(existing, canon, desiredSlug, input, now, touchInstall, home) {
|
|
310
|
+
let entry;
|
|
311
|
+
if (existing) {
|
|
312
|
+
entry = {
|
|
313
|
+
...existing,
|
|
314
|
+
providers: input.providers,
|
|
315
|
+
primaryProvider: input.primaryProvider,
|
|
316
|
+
coreVersion: input.coreVersion ?? existing.coreVersion,
|
|
317
|
+
source: 'desktop',
|
|
318
|
+
createdAt: existing.createdAt ?? now,
|
|
319
|
+
lastInstallAt: touchInstall ? now : (existing.lastInstallAt ?? now),
|
|
320
|
+
updatedAt: now,
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
const layout = workspaceLayout(home ?? resolveHome(home), desiredSlug, canon);
|
|
325
|
+
entry = {
|
|
326
|
+
...layout,
|
|
327
|
+
providers: input.providers,
|
|
328
|
+
primaryProvider: input.primaryProvider,
|
|
329
|
+
coreVersion: input.coreVersion,
|
|
330
|
+
source: 'desktop',
|
|
331
|
+
createdAt: now,
|
|
332
|
+
lastInstallAt: now,
|
|
333
|
+
updatedAt: now,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
if (input.desktopProjectId)
|
|
337
|
+
entry.desktopProjectId = input.desktopProjectId;
|
|
338
|
+
return entry;
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Upsert a desktop-owned entry for a repo, keyed by the canonical repo path.
|
|
342
|
+
*
|
|
343
|
+
* Behaviour:
|
|
344
|
+
* - new key ⇒ insert a fresh `source:'desktop'` entry, computing every sub-path
|
|
345
|
+
* from `workspaceLayout` with the desktop slug.
|
|
346
|
+
* - existing `source:'desktop'` entry ⇒ update providers/coreVersion/paths,
|
|
347
|
+
* preserve `createdAt`, refresh `lastInstallAt`/`updatedAt`.
|
|
348
|
+
* - existing `source:'core-standalone'` entry ⇒ ADOPT it: keep its slug +
|
|
349
|
+
* workspaceDir (core already materialised artifacts there), flip
|
|
350
|
+
* `source:'desktop'`, attach `desktopProjectId`. The workspace location is
|
|
351
|
+
* immutable post-adoption so we never strand on-disk artifacts.
|
|
352
|
+
*
|
|
353
|
+
* Runs under the file lock with a re-read so concurrent writers serialise.
|
|
354
|
+
*/
|
|
355
|
+
function mirrorProjectEntry(input, home) {
|
|
356
|
+
const canon = canonicalizeRepoPath(input.repoPath);
|
|
357
|
+
const key = normalizeKey(canon);
|
|
358
|
+
const { providers, primaryProvider } = normalizeProviders(input.providers, input.primaryProvider);
|
|
359
|
+
return withFileLock(home, () => {
|
|
360
|
+
const reg = readRegistryOrEmpty(home);
|
|
361
|
+
const now = new Date().toISOString();
|
|
362
|
+
const existing = reg.projects[key];
|
|
363
|
+
const entry = buildMirroredEntry(existing, canon, input.slug, { providers, primaryProvider, coreVersion: input.coreVersion, desktopProjectId: input.desktopProjectId }, now, true, home);
|
|
364
|
+
reg.projects[key] = entry;
|
|
365
|
+
reg.schemaVersion = exports.REGISTRY_SCHEMA_VERSION;
|
|
366
|
+
reg.generator = 'specrails-desktop';
|
|
367
|
+
reg.updatedAt = now;
|
|
368
|
+
atomicWrite(registryPath(home), JSON.stringify(reg, null, 2) + '\n');
|
|
369
|
+
return entry;
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Resolve a repo's artifact locations from the registry (read-only). Returns a
|
|
374
|
+
* legacy (repo-relative) resolution when no entry exists, so callers can treat
|
|
375
|
+
* the result uniformly. NEVER mutates the registry.
|
|
376
|
+
*/
|
|
377
|
+
function resolveArtifacts(repoPath, _options, home) {
|
|
378
|
+
const canon = canonicalizeRepoPath(repoPath);
|
|
379
|
+
const key = normalizeKey(canon);
|
|
380
|
+
const reg = readRegistryOrEmpty(home);
|
|
381
|
+
const rawEntry = reg.projects[key];
|
|
382
|
+
// A hand-edited / partially-written entry missing a path field must fall back
|
|
383
|
+
// to legacy (repo-relative) — never return `ticketsPath: undefined`.
|
|
384
|
+
const entry = isCompleteEntry(rawEntry) ? rawEntry : undefined;
|
|
385
|
+
if (!entry) {
|
|
386
|
+
// Legacy: every artifact lives under the repo's own `.specrails`.
|
|
387
|
+
const specrailsDir = path_1.default.join(canon, '.specrails');
|
|
388
|
+
return {
|
|
389
|
+
isLegacy: true,
|
|
390
|
+
repoPath: canon,
|
|
391
|
+
workspaceDir: canon,
|
|
392
|
+
specrailsDir,
|
|
393
|
+
ticketsPath: path_1.default.join(specrailsDir, 'local-tickets.json'),
|
|
394
|
+
backlogConfigPath: path_1.default.join(specrailsDir, 'backlog-config.json'),
|
|
395
|
+
profilesDir: path_1.default.join(specrailsDir, 'profiles'),
|
|
396
|
+
pluginsStateDir: path_1.default.join(specrailsDir, 'plugins'),
|
|
397
|
+
fileSummariesDir: path_1.default.join(specrailsDir, 'file-summaries'),
|
|
398
|
+
stateDir: path_1.default.join(canon, '.claude'),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
return {
|
|
402
|
+
isLegacy: false,
|
|
403
|
+
repoPath: entry.codeRoot || canon,
|
|
404
|
+
workspaceDir: entry.workspaceDir,
|
|
405
|
+
specrailsDir: path_1.default.join(entry.workspaceDir, '.specrails'),
|
|
406
|
+
ticketsPath: entry.ticketsPath,
|
|
407
|
+
backlogConfigPath: entry.backlogConfigPath,
|
|
408
|
+
profilesDir: entry.profilesDir,
|
|
409
|
+
pluginsStateDir: entry.pluginsStateDir,
|
|
410
|
+
fileSummariesDir: entry.fileSummariesDir,
|
|
411
|
+
stateDir: entry.stateDir,
|
|
412
|
+
entry,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/** Delete a repo's registry entry (no-op if absent). Keyed by canonical path. */
|
|
416
|
+
function removeRegistryEntry(repoPath, home) {
|
|
417
|
+
const canon = canonicalizeRepoPath(repoPath);
|
|
418
|
+
const key = normalizeKey(canon);
|
|
419
|
+
withFileLock(home, () => {
|
|
420
|
+
const reg = readRegistryOrEmpty(home);
|
|
421
|
+
if (!reg.projects[key])
|
|
422
|
+
return;
|
|
423
|
+
delete reg.projects[key];
|
|
424
|
+
reg.schemaVersion = exports.REGISTRY_SCHEMA_VERSION;
|
|
425
|
+
reg.generator = 'specrails-desktop';
|
|
426
|
+
reg.updatedAt = new Date().toISOString();
|
|
427
|
+
atomicWrite(registryPath(home), JSON.stringify(reg, null, 2) + '\n');
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* MERGE-upsert one entry per desktop project, leaving entries NOT owned by
|
|
432
|
+
* desktop (`source:'core-standalone'` for repos desktop doesn't track)
|
|
433
|
+
* UNTOUCHED. Never wholesale-wipes the registry. Run once at startup to
|
|
434
|
+
* self-heal a hand-edited / partially-written registry, and to backfill
|
|
435
|
+
* entries for projects added before the mirror existed.
|
|
436
|
+
*
|
|
437
|
+
* Adoption of a core-standalone entry happens exactly as in `mirrorProjectEntry`
|
|
438
|
+
* when a desktop project's canonical repo path matches a core-standalone key.
|
|
439
|
+
*
|
|
440
|
+
* All upserts happen under a single lock so the file is read + rewritten once.
|
|
441
|
+
*/
|
|
442
|
+
function reconcileFromProjects(projects, home) {
|
|
443
|
+
if (projects.length === 0)
|
|
444
|
+
return;
|
|
445
|
+
withFileLock(home, () => {
|
|
446
|
+
const reg = readRegistryOrEmpty(home);
|
|
447
|
+
const now = new Date().toISOString();
|
|
448
|
+
let changed = false;
|
|
449
|
+
for (const p of projects) {
|
|
450
|
+
const canon = canonicalizeRepoPath(p.repoPath);
|
|
451
|
+
const key = normalizeKey(canon);
|
|
452
|
+
const { providers, primaryProvider } = normalizeProviders(p.providers, p.primaryProvider);
|
|
453
|
+
const existing = reg.projects[key];
|
|
454
|
+
// Startup reconcile (touchInstall=false): preserve the immutable
|
|
455
|
+
// slug/workspace of any existing entry — never re-home an adopted or
|
|
456
|
+
// prior-desktop entry (that would strand on-disk artifacts every boot).
|
|
457
|
+
const entry = buildMirroredEntry(existing, canon, p.slug, { providers, primaryProvider, coreVersion: p.coreVersion, desktopProjectId: p.desktopProjectId }, now, false, home);
|
|
458
|
+
reg.projects[key] = entry;
|
|
459
|
+
changed = true;
|
|
460
|
+
}
|
|
461
|
+
if (!changed)
|
|
462
|
+
return;
|
|
463
|
+
reg.schemaVersion = exports.REGISTRY_SCHEMA_VERSION;
|
|
464
|
+
reg.generator = 'specrails-desktop';
|
|
465
|
+
reg.updatedAt = now;
|
|
466
|
+
atomicWrite(registryPath(home), JSON.stringify(reg, null, 2) + '\n');
|
|
467
|
+
});
|
|
468
|
+
}
|
|
@@ -110,9 +110,8 @@ class AttachmentManager {
|
|
|
110
110
|
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
111
111
|
fs_1.default.writeFileSync(path_1.default.join(dir, storedName), opts.file.buffer);
|
|
112
112
|
fs_1.default.writeFileSync(this.sidecarPath(opts.slug, opts.ticketKey, id), JSON.stringify(attachment, null, 2), 'utf-8');
|
|
113
|
-
if (opts.
|
|
114
|
-
|
|
115
|
-
(0, ticket_store_1.mutateStore)(ticketFile, (store) => {
|
|
113
|
+
if (opts.ticketStorePath) {
|
|
114
|
+
(0, ticket_store_1.mutateStore)(opts.ticketStorePath, (store) => {
|
|
116
115
|
const ticket = store.tickets[String(opts.ticketKey)];
|
|
117
116
|
if (ticket) {
|
|
118
117
|
ticket.attachments = [...(ticket.attachments ?? []), attachment];
|
|
@@ -160,9 +159,8 @@ class AttachmentManager {
|
|
|
160
159
|
const side = this.sidecarPath(opts.slug, opts.ticketKey, opts.attachmentId);
|
|
161
160
|
if (fs_1.default.existsSync(side))
|
|
162
161
|
fs_1.default.unlinkSync(side);
|
|
163
|
-
if (opts.
|
|
164
|
-
|
|
165
|
-
(0, ticket_store_1.mutateStore)(ticketFile, (store) => {
|
|
162
|
+
if (opts.ticketStorePath) {
|
|
163
|
+
(0, ticket_store_1.mutateStore)(opts.ticketStorePath, (store) => {
|
|
166
164
|
const ticket = store.tickets[String(opts.ticketKey)];
|
|
167
165
|
if (ticket?.attachments) {
|
|
168
166
|
ticket.attachments = ticket.attachments.filter((a) => a.id !== opts.attachmentId);
|
|
@@ -189,8 +187,7 @@ class AttachmentManager {
|
|
|
189
187
|
fs_1.default.mkdirSync(path_1.default.dirname(dst), { recursive: true });
|
|
190
188
|
fs_1.default.renameSync(src, dst);
|
|
191
189
|
const list = this.list(opts.slug, opts.realTicketId);
|
|
192
|
-
|
|
193
|
-
(0, ticket_store_1.mutateStore)(ticketFile, (store) => {
|
|
190
|
+
(0, ticket_store_1.mutateStore)(opts.ticketStorePath, (store) => {
|
|
194
191
|
const ticket = store.tickets[String(opts.realTicketId)];
|
|
195
192
|
if (ticket) {
|
|
196
193
|
const existing = ticket.attachments ?? [];
|
|
@@ -340,7 +340,7 @@ class BrowserCaptureManager {
|
|
|
340
340
|
const screenshot = await this.attachments.upload({
|
|
341
341
|
slug: this.projectSlug,
|
|
342
342
|
ticketKey: pendingSpecId,
|
|
343
|
-
|
|
343
|
+
ticketStorePath: null,
|
|
344
344
|
file: {
|
|
345
345
|
buffer: png,
|
|
346
346
|
originalname: `screen-capture-${stamp}.png`,
|
|
@@ -352,7 +352,7 @@ class BrowserCaptureManager {
|
|
|
352
352
|
const domAttachment = await this.attachments.upload({
|
|
353
353
|
slug: this.projectSlug,
|
|
354
354
|
ticketKey: pendingSpecId,
|
|
355
|
-
|
|
355
|
+
ticketStorePath: null,
|
|
356
356
|
file: {
|
|
357
357
|
buffer: domJson,
|
|
358
358
|
originalname: `page-dom-${stamp}.json`,
|
|
@@ -471,7 +471,7 @@ class BrowserCaptureManager {
|
|
|
471
471
|
const attachment = await this.attachments.upload({
|
|
472
472
|
slug: this.projectSlug,
|
|
473
473
|
ticketKey: pendingSpecId,
|
|
474
|
-
|
|
474
|
+
ticketStorePath: null,
|
|
475
475
|
file: { buffer: png, originalname: `screen-capture-${key}-${stamp}.png`, mimetype: 'image/png', size: png.length },
|
|
476
476
|
});
|
|
477
477
|
breakpoints[key] = { attachment, dataUrl: `data:image/png;base64,${png.toString('base64')}`, viewport: dims[key] };
|
|
@@ -484,7 +484,7 @@ class BrowserCaptureManager {
|
|
|
484
484
|
const domAttachment = await this.attachments.upload({
|
|
485
485
|
slug: this.projectSlug,
|
|
486
486
|
ticketKey: pendingSpecId,
|
|
487
|
-
|
|
487
|
+
ticketStorePath: null,
|
|
488
488
|
file: { buffer: domJson, originalname: `page-dom-${stamp}.json`, mimetype: 'application/json', size: domJson.length },
|
|
489
489
|
});
|
|
490
490
|
return {
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getBundledCoreRoot = getBundledCoreRoot;
|
|
7
|
+
exports.getBundledCoreCli = getBundledCoreCli;
|
|
8
|
+
exports.getBundledCoreVersion = getBundledCoreVersion;
|
|
9
|
+
exports.hasBundledCore = hasBundledCore;
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
/**
|
|
13
|
+
* Bundled specrails-core resolution.
|
|
14
|
+
*
|
|
15
|
+
* The Tauri host (src-tauri/src/lib.rs) sets `SPECRAILS_BUNDLED_CORE_PATH` to
|
|
16
|
+
* `<resource_dir>/core` — but ONLY when the compiled CLI actually exists on disk
|
|
17
|
+
* (existence-gated, mirroring `SPECRAILS_BUNDLED_RUNTIMES_PATH`). When the env is
|
|
18
|
+
* absent (a build that shipped no core, a partial extraction, or any non-desktop
|
|
19
|
+
* mode like `npm run dev:server`) these helpers return null and EVERY caller
|
|
20
|
+
* falls back to the legacy `npx specrails-core` path — byte-identical to today.
|
|
21
|
+
*
|
|
22
|
+
* This is the single chokepoint: do NOT read `SPECRAILS_BUNDLED_CORE_PATH`
|
|
23
|
+
* anywhere else; resolve through `getBundledCoreCli()` / `getBundledCoreRoot()`.
|
|
24
|
+
*/
|
|
25
|
+
/** The bundled core package root (`<resource_dir>/core`) or null when absent. */
|
|
26
|
+
function getBundledCoreRoot() {
|
|
27
|
+
const p = process.env.SPECRAILS_BUNDLED_CORE_PATH;
|
|
28
|
+
if (!p || p.length === 0)
|
|
29
|
+
return null;
|
|
30
|
+
// Existence-gate (defence-in-depth — lib.rs already gates, but a stale env
|
|
31
|
+
// from a partial uninstall must never make us shell out to a missing file).
|
|
32
|
+
if (!fs_1.default.existsSync(p))
|
|
33
|
+
return null;
|
|
34
|
+
return p;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Absolute path to the bundled core CLI entry
|
|
38
|
+
* (`<core>/dist/installer/cli.js`), or null when no bundled core is present (→
|
|
39
|
+
* caller falls back to `npx specrails-core`).
|
|
40
|
+
*/
|
|
41
|
+
function getBundledCoreCli() {
|
|
42
|
+
const root = getBundledCoreRoot();
|
|
43
|
+
if (!root)
|
|
44
|
+
return null;
|
|
45
|
+
const cli = path_1.default.join(root, 'dist', 'installer', 'cli.js');
|
|
46
|
+
if (!fs_1.default.existsSync(cli))
|
|
47
|
+
return null;
|
|
48
|
+
return cli;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The version of the bundled core (from `<core>/package.json`), or null when no
|
|
52
|
+
* bundled core / unreadable. FrameworkManager.versionCheck compares this against
|
|
53
|
+
* the materialized `framework/current` to decide whether to re-materialize.
|
|
54
|
+
*/
|
|
55
|
+
function getBundledCoreVersion() {
|
|
56
|
+
const root = getBundledCoreRoot();
|
|
57
|
+
if (!root)
|
|
58
|
+
return null;
|
|
59
|
+
const pkgPath = path_1.default.join(root, 'package.json');
|
|
60
|
+
try {
|
|
61
|
+
const pkg = JSON.parse(fs_1.default.readFileSync(pkgPath, 'utf8'));
|
|
62
|
+
const v = pkg.version?.trim();
|
|
63
|
+
return v && v.length > 0 ? v : null;
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** True when a usable bundled core (env + CLI on disk) is present. */
|
|
70
|
+
function hasBundledCore() {
|
|
71
|
+
return getBundledCoreCli() !== null;
|
|
72
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getBundledOpenspecCli = getBundledOpenspecCli;
|
|
7
|
+
exports.hasBundledOpenspec = hasBundledOpenspec;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
/**
|
|
11
|
+
* Bundled `@fission-ai/openspec` resolution.
|
|
12
|
+
*
|
|
13
|
+
* The Tauri host (src-tauri/src/lib.rs) sets `SPECRAILS_BUNDLED_OPENSPEC_PATH` to
|
|
14
|
+
* `<resource_dir>/openspec` — but ONLY when the openspec CLI entry actually
|
|
15
|
+
* exists on disk (existence-gated, mirroring `SPECRAILS_BUNDLED_CORE_PATH`).
|
|
16
|
+
* When the env is absent (a build that shipped no openspec, a partial
|
|
17
|
+
* extraction, or any non-desktop mode like `npm run dev:server`) these helpers
|
|
18
|
+
* return null and the bundled-core init falls back to the legacy
|
|
19
|
+
* `npx @fission-ai/openspec` path — byte-identical to today.
|
|
20
|
+
*
|
|
21
|
+
* openspec is bundled as a `npm install`ed tree (it has runtime deps), so the
|
|
22
|
+
* CLI lives at:
|
|
23
|
+
* <openspec>/node_modules/@fission-ai/openspec/bin/openspec.js
|
|
24
|
+
*
|
|
25
|
+
* This is the single chokepoint: do NOT read `SPECRAILS_BUNDLED_OPENSPEC_PATH`
|
|
26
|
+
* anywhere else; resolve through `getBundledOpenspecCli()`.
|
|
27
|
+
*/
|
|
28
|
+
/** Relative path (from the bundle root) to the openspec CLI node entry. */
|
|
29
|
+
const OPENSPEC_CLI_REL = path_1.default.join('node_modules', '@fission-ai', 'openspec', 'bin', 'openspec.js');
|
|
30
|
+
/** The bundled openspec root (`<resource_dir>/openspec`) or null when absent. */
|
|
31
|
+
function getBundledOpenspecRoot() {
|
|
32
|
+
const p = process.env.SPECRAILS_BUNDLED_OPENSPEC_PATH;
|
|
33
|
+
if (!p || p.length === 0)
|
|
34
|
+
return null;
|
|
35
|
+
// Existence-gate (defence-in-depth — lib.rs already gates, but a stale env
|
|
36
|
+
// from a partial uninstall must never make us point at a missing file).
|
|
37
|
+
if (!fs_1.default.existsSync(p))
|
|
38
|
+
return null;
|
|
39
|
+
return p;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Absolute path to the bundled openspec CLI node entry
|
|
43
|
+
* (`<openspec>/node_modules/@fission-ai/openspec/bin/openspec.js`), or null when
|
|
44
|
+
* no bundled openspec is present (→ bundled-core init falls back to npx openspec).
|
|
45
|
+
*/
|
|
46
|
+
function getBundledOpenspecCli() {
|
|
47
|
+
const root = getBundledOpenspecRoot();
|
|
48
|
+
if (!root)
|
|
49
|
+
return null;
|
|
50
|
+
const cli = path_1.default.join(root, OPENSPEC_CLI_REL);
|
|
51
|
+
if (!fs_1.default.existsSync(cli))
|
|
52
|
+
return null;
|
|
53
|
+
return cli;
|
|
54
|
+
}
|
|
55
|
+
/** True when a usable bundled openspec (env + CLI on disk) is present. */
|
|
56
|
+
function hasBundledOpenspec() {
|
|
57
|
+
return getBundledOpenspecCli() !== null;
|
|
58
|
+
}
|