specrails-desktop 2.8.0 → 2.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -19
- package/client/dist/assets/{ActivityFeedPage-LKqd18-G.js → ActivityFeedPage-DpQzYMBz.js} +1 -1
- package/client/dist/assets/{AgentsPage-Cb-b-6Ot.js → AgentsPage-29fCY8qV.js} +1 -1
- package/client/dist/assets/{AnalyticsPage-HVxQQ1wy.js → AnalyticsPage-BwGtS6Hf.js} +1 -1
- package/client/dist/assets/{BarChart-BOyHB0dw.js → BarChart-CTR97DVC.js} +1 -1
- package/client/dist/assets/{CodePage-DnOnwKGB.js → CodePage-yAAxKasA.js} +1 -1
- package/client/dist/assets/{DesktopAnalyticsPage-D2auU39x.js → DesktopAnalyticsPage-BdK_XpsD.js} +1 -1
- package/client/dist/assets/{DocsDialog-CTuDX3GK.js → DocsDialog-BaE0cLlL.js} +2 -2
- package/client/dist/assets/{DocsPage-DRyMmu0Z.js → DocsPage-c1FgZX8_.js} +2 -2
- package/client/dist/assets/{ExportDropdown-DO-GGiMh.js → ExportDropdown-lPv_yDen.js} +1 -1
- package/client/dist/assets/{IntegrationsPage-BhbO4jFT.js → IntegrationsPage-DOpxRe7G.js} +1 -1
- package/client/dist/assets/{JobDetailPage-DJooEg1s.js → JobDetailPage-5ExzXY-F.js} +1 -1
- package/client/dist/assets/{JobsPage-BbaC-YOg.js → JobsPage-iW7WuPAc.js} +1 -1
- package/client/dist/assets/{dist-js-Xc2lRKp2.js → dist-js-A8aSaLng.js} +1 -1
- package/client/dist/assets/{dist-js-CiIVMsx3.js → dist-js-CD_m3Xj5.js} +1 -1
- package/client/dist/assets/index-D6BaYRRU.css +2 -0
- package/client/dist/assets/{index-DK214dak.js → index-DRhFPNAv.js} +44 -44
- 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-1vkTuLY7.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/{tickets-9kdPXInd.js → tickets-CG_mo-Bg.js} +1 -1
- package/client/dist/assets/{tickets-n23kDqJT.js → tickets-CVJQ-vRm.js} +1 -1
- package/client/dist/assets/{tickets-tGx5AR5b.js → tickets-D5MSAPe_.js} +1 -1
- package/client/dist/assets/{tickets-1UIGf_oA.js → tickets-DBV3wgQZ.js} +1 -1
- package/client/dist/assets/{tickets-DNmXcAwu.js → tickets-Q0_pONEh.js} +1 -1
- package/client/dist/assets/{tickets-C6pwZwt4.js → tickets-RZ0LyeQe.js} +1 -1
- package/client/dist/assets/{tickets-DAjtxAVb.js → tickets-d1A6EOHa.js} +1 -1
- package/client/dist/assets/{tickets-0rM0lIXd.js → tickets-r4-oNV0R.js} +1 -1
- package/client/dist/assets/{useProjectCache-DVNypkmR.js → useProjectCache-CSi2xHri.js} +1 -1
- package/client/dist/index.html +5 -5
- 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
|
@@ -8,68 +8,116 @@ const express_1 = require("express");
|
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const path_1 = __importDefault(require("path"));
|
|
10
10
|
const os_1 = __importDefault(require("os"));
|
|
11
|
-
// ───
|
|
12
|
-
//
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
11
|
+
// ─── User-guide docs ────────────────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// The Documentation panel serves a dedicated, language-aware USER-GUIDE tree —
|
|
14
|
+
// NOT the developer docs (docs/internals, docs/platforms, top-level dev .md).
|
|
15
|
+
// Layout on disk:
|
|
16
|
+
//
|
|
17
|
+
// docs/guide/<lang>/<category>/<order>-<slug>.md
|
|
18
|
+
//
|
|
19
|
+
// where <lang> is an i18n language id (`en`, `es`, …). English (`en`) is the
|
|
20
|
+
// source-of-truth locale and is always present; any missing language OR missing
|
|
21
|
+
// page falls back to the English file, so an untranslated language still shows
|
|
22
|
+
// real content instead of an empty panel.
|
|
23
|
+
//
|
|
24
|
+
// Resolution precedence for the guide ROOT (the dir that holds <lang>/):
|
|
25
|
+
// 1. ~/.specrails/docs/guide/ (user-editable override — wins when present)
|
|
26
|
+
// 2. bundled Tauri resource (<resource_dir>/docs/guide in the packaged app)
|
|
27
|
+
// 3. repo / compiled dist (docs/guide relative to __dirname)
|
|
28
|
+
// All candidates are existence-gated; a missing tree never crashes — the panel
|
|
29
|
+
// just shows no categories.
|
|
30
|
+
// ─── Default language + validation ──────────────────────────────────────────
|
|
31
|
+
const DEFAULT_LANG = 'en';
|
|
32
|
+
// Mirror of client/src/lib/i18n.ts LANGUAGE_IDS. Kept here (not imported — the
|
|
33
|
+
// server is CommonJS and must not depend on the ESM client) so an unknown / bogus
|
|
34
|
+
// `?lang=` value can never be turned into a filesystem path.
|
|
35
|
+
const SUPPORTED_LANGS = new Set(['en', 'es', 'fr', 'de', 'pt', 'it', 'zh', 'ja']);
|
|
36
|
+
function normalizeLang(raw) {
|
|
37
|
+
if (typeof raw !== 'string')
|
|
38
|
+
return DEFAULT_LANG;
|
|
39
|
+
const base = raw.toLowerCase().split('-')[0].trim();
|
|
40
|
+
return SUPPORTED_LANGS.has(base) ? base : DEFAULT_LANG;
|
|
41
|
+
}
|
|
42
|
+
// ─── Guide-root resolution ──────────────────────────────────────────────────
|
|
43
|
+
/**
|
|
44
|
+
* The bundled (packaged-app) guide root, or null when not running inside a
|
|
45
|
+
* Tauri bundle / the dir is absent. The Tauri host exports
|
|
46
|
+
* `SPECRAILS_BUNDLED_DOCS_PATH=<resource_dir>/docs` (existence-gated, mirroring
|
|
47
|
+
* the runtimes/core envs); as a defence-in-depth fallback we also derive
|
|
48
|
+
* `<resource_dir>/docs` from `SPECRAILS_BUNDLED_RUNTIMES_PATH` (whose parent IS
|
|
49
|
+
* the resource dir) so a build that forgot the docs env still resolves.
|
|
50
|
+
*/
|
|
51
|
+
function resolveBundledGuideRoot() {
|
|
52
|
+
const direct = process.env.SPECRAILS_BUNDLED_DOCS_PATH;
|
|
53
|
+
if (direct && direct.length > 0) {
|
|
54
|
+
const guide = path_1.default.join(direct, 'guide');
|
|
55
|
+
if (fs_1.default.existsSync(guide))
|
|
56
|
+
return guide;
|
|
17
57
|
}
|
|
18
|
-
|
|
58
|
+
const runtimes = process.env.SPECRAILS_BUNDLED_RUNTIMES_PATH;
|
|
59
|
+
if (runtimes && runtimes.length > 0) {
|
|
60
|
+
// <resource_dir>/runtimes → <resource_dir>/docs/guide
|
|
61
|
+
const guide = path_1.default.join(path_1.default.dirname(runtimes), 'docs', 'guide');
|
|
62
|
+
if (fs_1.default.existsSync(guide))
|
|
63
|
+
return guide;
|
|
64
|
+
}
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Resolve the guide ROOT directory (the one containing <lang>/ subdirs).
|
|
69
|
+
* Returns null when no guide tree can be found anywhere (the panel then shows
|
|
70
|
+
* an empty index rather than crashing).
|
|
71
|
+
*/
|
|
72
|
+
function resolveGuideRoot() {
|
|
73
|
+
// 1. User override: ~/.specrails/docs/guide
|
|
74
|
+
const userGuide = path_1.default.join(os_1.default.homedir(), '.specrails', 'docs', 'guide');
|
|
75
|
+
if (fs_1.default.existsSync(userGuide))
|
|
76
|
+
return userGuide;
|
|
77
|
+
// 2. Bundled app resource
|
|
78
|
+
const bundled = resolveBundledGuideRoot();
|
|
79
|
+
if (bundled)
|
|
80
|
+
return bundled;
|
|
81
|
+
// 3. Repo / compiled dist (relative to this file)
|
|
19
82
|
const candidates = [
|
|
20
|
-
path_1.default.resolve(__dirname, '../docs'), // dev: server/ -> ../docs
|
|
21
|
-
path_1.default.resolve(__dirname, '../../docs'), // compiled: server/dist/ -> ../../docs
|
|
83
|
+
path_1.default.resolve(__dirname, '../docs/guide'), // dev: server/ -> ../docs/guide
|
|
84
|
+
path_1.default.resolve(__dirname, '../../docs/guide'), // compiled: server/dist/ -> ../../docs/guide
|
|
22
85
|
];
|
|
23
86
|
for (const candidate of candidates) {
|
|
24
|
-
if (fs_1.default.existsSync(candidate))
|
|
87
|
+
if (fs_1.default.existsSync(candidate))
|
|
25
88
|
return candidate;
|
|
26
|
-
}
|
|
27
89
|
}
|
|
28
|
-
|
|
29
|
-
return userDocsDir;
|
|
90
|
+
return null;
|
|
30
91
|
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*/
|
|
37
|
-
const TOP_LEVEL_CATEGORY_SLUG = 'guides';
|
|
92
|
+
/** The localized directory for a given language under the guide root. */
|
|
93
|
+
function langDir(guideRoot, lang) {
|
|
94
|
+
return path_1.default.join(guideRoot, lang);
|
|
95
|
+
}
|
|
96
|
+
// ─── Category configuration ─────────────────────────────────────────────────
|
|
38
97
|
const CATEGORY_LABELS = {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
operations: 'Operations',
|
|
98
|
+
'getting-started': 'Getting started',
|
|
99
|
+
specs: 'Specs',
|
|
100
|
+
pipeline: 'Pipeline',
|
|
101
|
+
agents: 'Agents',
|
|
102
|
+
insights: 'Insights',
|
|
103
|
+
integrations: 'Integrations',
|
|
104
|
+
settings: 'Settings',
|
|
47
105
|
};
|
|
48
106
|
/**
|
|
49
|
-
* Preferred display order. Categories not in this list come after,
|
|
50
|
-
* order `fs.readdirSync` returns them.
|
|
51
|
-
*/
|
|
52
|
-
const CATEGORY_ORDER = ['guides', 'platforms', 'internals', 'general', 'product', 'engineering', 'operations'];
|
|
53
|
-
/**
|
|
54
|
-
* Preferred document order within the "guides" category. Files not listed
|
|
55
|
-
* here are appended in alphabetical order so adding a new doc never breaks
|
|
56
|
-
* the build.
|
|
107
|
+
* Preferred display order. Categories not in this list come after, alphabetically.
|
|
57
108
|
*/
|
|
58
|
-
const
|
|
109
|
+
const CATEGORY_ORDER = [
|
|
59
110
|
'getting-started',
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'
|
|
63
|
-
'
|
|
64
|
-
'
|
|
65
|
-
'
|
|
66
|
-
'codex',
|
|
111
|
+
'specs',
|
|
112
|
+
'pipeline',
|
|
113
|
+
'agents',
|
|
114
|
+
'insights',
|
|
115
|
+
'integrations',
|
|
116
|
+
'settings',
|
|
67
117
|
];
|
|
68
|
-
// ─── Helpers
|
|
118
|
+
// ─── Helpers ────────────────────────────────────────────────────────────────
|
|
69
119
|
function slugToTitle(slug) {
|
|
70
|
-
return slug
|
|
71
|
-
.replace(/-/g, ' ')
|
|
72
|
-
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
120
|
+
return slug.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
|
|
73
121
|
}
|
|
74
122
|
function categoryLabel(slug) {
|
|
75
123
|
return CATEGORY_LABELS[slug] ?? slugToTitle(slug);
|
|
@@ -78,130 +126,160 @@ function extractTitle(content, slug) {
|
|
|
78
126
|
const match = content.match(/^#\s+(.+)$/m);
|
|
79
127
|
return match ? match[1].trim() : slugToTitle(slug);
|
|
80
128
|
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return [
|
|
87
|
-
const aKey = keyFn(a);
|
|
88
|
-
const bKey = keyFn(b);
|
|
89
|
-
const ai = preferredIndex(aKey);
|
|
90
|
-
const bi = preferredIndex(bKey);
|
|
91
|
-
if (ai !== bi)
|
|
92
|
-
return ai - bi;
|
|
93
|
-
return aKey.localeCompare(bKey);
|
|
94
|
-
});
|
|
129
|
+
/** Parse an `<order>-<slug>.md` filename into { order, slug }. */
|
|
130
|
+
function parseGuideFile(file) {
|
|
131
|
+
const m = file.match(/^(\d+)-(.+)\.md$/);
|
|
132
|
+
if (!m)
|
|
133
|
+
return null;
|
|
134
|
+
return { order: parseInt(m[1], 10), slug: m[2] };
|
|
95
135
|
}
|
|
96
|
-
function
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
catch {
|
|
101
|
-
return [];
|
|
102
|
-
}
|
|
136
|
+
function categoryRank(slug) {
|
|
137
|
+
const i = CATEGORY_ORDER.indexOf(slug);
|
|
138
|
+
return i === -1 ? Number.MAX_SAFE_INTEGER : i;
|
|
103
139
|
}
|
|
104
|
-
|
|
105
|
-
|
|
140
|
+
/** List the category subdirectories of a localized lang dir (existence-safe). */
|
|
141
|
+
function listCategories(dir) {
|
|
106
142
|
try {
|
|
107
|
-
|
|
108
|
-
|
|
143
|
+
return fs_1.default
|
|
144
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
145
|
+
.filter((e) => e.isDirectory())
|
|
146
|
+
.map((e) => e.name);
|
|
109
147
|
}
|
|
110
148
|
catch {
|
|
111
|
-
return
|
|
149
|
+
return [];
|
|
112
150
|
}
|
|
113
151
|
}
|
|
114
|
-
|
|
115
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Build the full category/doc index for a language, falling back to English
|
|
154
|
+
* per-category and per-page. The English tree defines the canonical set of
|
|
155
|
+
* categories and slugs; a translated language contributes localized titles for
|
|
156
|
+
* the pages it has, and English fills the gaps.
|
|
157
|
+
*/
|
|
158
|
+
function buildCategories(guideRoot, lang) {
|
|
159
|
+
const enDir = langDir(guideRoot, DEFAULT_LANG);
|
|
160
|
+
if (!fs_1.default.existsSync(enDir))
|
|
116
161
|
return [];
|
|
162
|
+
const localizedDir = langDir(guideRoot, lang);
|
|
117
163
|
const categories = [];
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
164
|
+
for (const cat of listCategories(enDir)) {
|
|
165
|
+
const enCatDir = path_1.default.join(enDir, cat);
|
|
166
|
+
const localizedCatDir = path_1.default.join(localizedDir, cat);
|
|
167
|
+
let files;
|
|
168
|
+
try {
|
|
169
|
+
files = fs_1.default.readdirSync(enCatDir).filter((f) => f.endsWith('.md'));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
files = [];
|
|
173
|
+
}
|
|
174
|
+
const parsed = files
|
|
175
|
+
.map((f) => ({ file: f, meta: parseGuideFile(f) }))
|
|
176
|
+
.filter((x) => x.meta !== null)
|
|
177
|
+
.sort((a, b) => a.meta.order - b.meta.order || a.meta.slug.localeCompare(b.meta.slug));
|
|
178
|
+
const docs = parsed.map(({ file, meta }) => {
|
|
179
|
+
// Prefer the localized file's title; fall back to the English file.
|
|
180
|
+
const localizedPath = path_1.default.join(localizedCatDir, file);
|
|
181
|
+
const enPath = path_1.default.join(enCatDir, file);
|
|
182
|
+
const readPath = fs_1.default.existsSync(localizedPath) ? localizedPath : enPath;
|
|
183
|
+
let title = slugToTitle(meta.slug);
|
|
184
|
+
try {
|
|
185
|
+
title = extractTitle(fs_1.default.readFileSync(readPath, 'utf-8'), meta.slug);
|
|
186
|
+
}
|
|
187
|
+
catch {
|
|
188
|
+
/* fall back to slug-derived title */
|
|
189
|
+
}
|
|
190
|
+
return { title, slug: meta.slug };
|
|
191
|
+
});
|
|
192
|
+
if (docs.length > 0) {
|
|
193
|
+
categories.push({ name: categoryLabel(cat), slug: cat, docs });
|
|
194
|
+
}
|
|
124
195
|
}
|
|
125
|
-
|
|
126
|
-
|
|
196
|
+
return categories.sort((a, b) => categoryRank(a.slug) - categoryRank(b.slug) || a.slug.localeCompare(b.slug));
|
|
197
|
+
}
|
|
198
|
+
// ─── Path-safety guards ─────────────────────────────────────────────────────
|
|
199
|
+
function isSafeSegment(seg) {
|
|
200
|
+
return (typeof seg === 'string' &&
|
|
201
|
+
seg.length > 0 &&
|
|
202
|
+
seg !== '.' &&
|
|
203
|
+
seg !== '..' &&
|
|
204
|
+
seg === path_1.default.basename(seg) &&
|
|
205
|
+
!seg.includes('/') &&
|
|
206
|
+
!seg.includes('\\'));
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Resolve the on-disk file for a (lang, category, slug). Tries the localized
|
|
210
|
+
* file first, then English. Returns null when neither exists or a path-safety
|
|
211
|
+
* check fails. The slug in the URL is the *bare* slug (no order prefix); we map
|
|
212
|
+
* it to the `<order>-<slug>.md` file by scanning the English category dir
|
|
213
|
+
* (English is canonical for the set of slugs).
|
|
214
|
+
*/
|
|
215
|
+
function resolveDocFile(guideRoot, lang, category, slug) {
|
|
216
|
+
if (!isSafeSegment(category) || !isSafeSegment(slug))
|
|
217
|
+
return null;
|
|
218
|
+
const enCatDir = path_1.default.join(langDir(guideRoot, DEFAULT_LANG), category);
|
|
219
|
+
let files;
|
|
127
220
|
try {
|
|
128
|
-
|
|
221
|
+
files = fs_1.default.readdirSync(enCatDir).filter((f) => f.endsWith('.md'));
|
|
129
222
|
}
|
|
130
223
|
catch {
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
for (const entry of entries) {
|
|
134
|
-
if (!entry.isDirectory())
|
|
135
|
-
continue;
|
|
136
|
-
// Avoid colliding with synthetic top-level slug
|
|
137
|
-
if (entry.name === TOP_LEVEL_CATEGORY_SLUG)
|
|
138
|
-
continue;
|
|
139
|
-
const catDir = path_1.default.join(docsDir, entry.name);
|
|
140
|
-
const files = readMarkdownFiles(catDir);
|
|
141
|
-
const docs = files.map((f) => readDocEntry(f, path_1.default.join(catDir, f)));
|
|
142
|
-
// Sort docs within unknown categories alphabetically.
|
|
143
|
-
docs.sort((a, b) => a.slug.localeCompare(b.slug));
|
|
144
|
-
categories.push({ name: categoryLabel(entry.name), slug: entry.name, docs });
|
|
224
|
+
return null;
|
|
145
225
|
}
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
226
|
+
const match = files.find((f) => {
|
|
227
|
+
const meta = parseGuideFile(f);
|
|
228
|
+
return meta?.slug === slug;
|
|
229
|
+
});
|
|
230
|
+
if (!match)
|
|
231
|
+
return null;
|
|
232
|
+
const localizedPath = path_1.default.join(langDir(guideRoot, lang), category, match);
|
|
233
|
+
const enPath = path_1.default.join(enCatDir, match);
|
|
234
|
+
const filePath = fs_1.default.existsSync(localizedPath) ? localizedPath : enPath;
|
|
235
|
+
// Defence-in-depth: the resolved file must live inside the guide root.
|
|
236
|
+
const rel = path_1.default.relative(path_1.default.resolve(guideRoot), path_1.default.resolve(filePath));
|
|
237
|
+
if (rel.startsWith('..') || path_1.default.isAbsolute(rel))
|
|
238
|
+
return null;
|
|
239
|
+
if (!fs_1.default.existsSync(filePath))
|
|
240
|
+
return null;
|
|
241
|
+
return { filePath };
|
|
158
242
|
}
|
|
159
|
-
// ─── Router
|
|
243
|
+
// ─── Router ─────────────────────────────────────────────────────────────────
|
|
160
244
|
function createDocsRouter() {
|
|
161
245
|
const router = (0, express_1.Router)();
|
|
162
|
-
router.get('/', (
|
|
163
|
-
const
|
|
164
|
-
const
|
|
246
|
+
router.get('/', (req, res) => {
|
|
247
|
+
const lang = normalizeLang(req.query.lang);
|
|
248
|
+
const guideRoot = resolveGuideRoot();
|
|
249
|
+
const categories = guideRoot ? buildCategories(guideRoot, lang) : [];
|
|
165
250
|
res.json({ categories });
|
|
166
251
|
});
|
|
167
252
|
router.get('/:category/:slug', (req, res) => {
|
|
168
253
|
const { category, slug } = req.params;
|
|
169
|
-
|
|
254
|
+
const lang = normalizeLang(req.query.lang);
|
|
255
|
+
if (!isSafeSegment(category)) {
|
|
170
256
|
res.status(404).json({ error: 'Category not found' });
|
|
171
257
|
return;
|
|
172
258
|
}
|
|
173
|
-
|
|
174
|
-
const safeSlug = path_1.default.basename(slug);
|
|
175
|
-
if (safeSlug !== slug || slug.includes('/') || slug.includes('\\')) {
|
|
259
|
+
if (!isSafeSegment(slug)) {
|
|
176
260
|
res.status(400).json({ error: 'Invalid slug' });
|
|
177
261
|
return;
|
|
178
262
|
}
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
const filePath = category === TOP_LEVEL_CATEGORY_SLUG
|
|
182
|
-
? path_1.default.join(docsDir, `${safeSlug}.md`)
|
|
183
|
-
: path_1.default.join(docsDir, category, `${safeSlug}.md`);
|
|
184
|
-
// B4: defence-in-depth — never serve a file resolved outside docsDir, even
|
|
185
|
-
// if a future change loosens the slug/category validation above.
|
|
186
|
-
const rel = path_1.default.relative(path_1.default.resolve(docsDir), path_1.default.resolve(filePath));
|
|
187
|
-
if (rel.startsWith('..') || path_1.default.isAbsolute(rel)) {
|
|
263
|
+
const guideRoot = resolveGuideRoot();
|
|
264
|
+
if (!guideRoot) {
|
|
188
265
|
res.status(404).json({ error: 'Document not found' });
|
|
189
266
|
return;
|
|
190
267
|
}
|
|
191
|
-
|
|
268
|
+
const resolved = resolveDocFile(guideRoot, lang, category, slug);
|
|
269
|
+
if (!resolved) {
|
|
192
270
|
res.status(404).json({ error: 'Document not found' });
|
|
193
271
|
return;
|
|
194
272
|
}
|
|
195
273
|
let content;
|
|
196
274
|
try {
|
|
197
|
-
content = fs_1.default.readFileSync(filePath, 'utf-8');
|
|
275
|
+
content = fs_1.default.readFileSync(resolved.filePath, 'utf-8');
|
|
198
276
|
}
|
|
199
277
|
catch {
|
|
200
278
|
res.status(500).json({ error: 'Failed to read document' });
|
|
201
279
|
return;
|
|
202
280
|
}
|
|
203
|
-
const title = extractTitle(content,
|
|
204
|
-
res.json({ title, content, category, slug
|
|
281
|
+
const title = extractTitle(content, slug);
|
|
282
|
+
res.json({ title, content, category, slug });
|
|
205
283
|
});
|
|
206
284
|
return router;
|
|
207
285
|
}
|
|
@@ -95,20 +95,23 @@ function readSummary(projectPath, relPath) {
|
|
|
95
95
|
return null;
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
|
-
function writeSummary(
|
|
99
|
-
|
|
98
|
+
function writeSummary(summaryRoot, relPath, payload,
|
|
99
|
+
/** When false (relocated: summaryRoot is the app-owned workspace), skip the
|
|
100
|
+
* `.gitignore` append — the workspace `.gitignore` is not the user's repo. */
|
|
101
|
+
appendGitignore = true) {
|
|
102
|
+
const dir = summariesDir(summaryRoot);
|
|
100
103
|
const firstWrite = !fs.existsSync(dir);
|
|
101
104
|
fs.mkdirSync(dir, { recursive: true });
|
|
102
|
-
const final = summaryFilePath(
|
|
105
|
+
const final = summaryFilePath(summaryRoot, relPath);
|
|
103
106
|
// Atomic write: temp file in the same directory, then rename.
|
|
104
107
|
const tmp = `${final}.tmp.${(0, crypto_1.randomBytes)(6).toString('hex')}`;
|
|
105
108
|
fs.writeFileSync(tmp, JSON.stringify(payload), { encoding: 'utf8', mode: 0o600 });
|
|
106
109
|
fs.renameSync(tmp, final);
|
|
107
|
-
if (firstWrite) {
|
|
110
|
+
if (firstWrite && appendGitignore) {
|
|
108
111
|
// The app appends `.specrails/file-summaries/` to the project `.gitignore`
|
|
109
112
|
// on first write. Idempotent: only appends when the line is absent.
|
|
110
113
|
try {
|
|
111
|
-
ensureGitignoreLine(
|
|
114
|
+
ensureGitignoreLine(summaryRoot, '.specrails/file-summaries/');
|
|
112
115
|
}
|
|
113
116
|
catch { /* non-fatal */ }
|
|
114
117
|
}
|
|
@@ -130,8 +133,13 @@ function ensureGitignoreLine(projectPath, line) {
|
|
|
130
133
|
fs.writeFileSync(gi, `${existing}${sep}${line}\n`, 'utf8');
|
|
131
134
|
return true;
|
|
132
135
|
}
|
|
133
|
-
function sweepOrphans(
|
|
134
|
-
|
|
136
|
+
function sweepOrphans(summaryRoot, cap = 200,
|
|
137
|
+
/** Where to resolve source files (the repo). Relocate-artifacts: summaries
|
|
138
|
+
* live under `summaryRoot` (workspace) but source is under `sourceRoot`
|
|
139
|
+
* (repo). Defaults to `summaryRoot` (legacy, byte-identical). */
|
|
140
|
+
sourceRoot) {
|
|
141
|
+
const dir = summariesDir(summaryRoot);
|
|
142
|
+
const srcRoot = sourceRoot ?? summaryRoot;
|
|
135
143
|
let deleted = 0;
|
|
136
144
|
let remaining = 0;
|
|
137
145
|
let entries;
|
|
@@ -154,7 +162,7 @@ function sweepOrphans(projectPath, cap = 200) {
|
|
|
154
162
|
catch {
|
|
155
163
|
continue;
|
|
156
164
|
}
|
|
157
|
-
const sourceAbs = path.join(
|
|
165
|
+
const sourceAbs = path.join(srcRoot, payload.path);
|
|
158
166
|
if (fs.existsSync(sourceAbs))
|
|
159
167
|
continue;
|
|
160
168
|
if (deleted >= cap) {
|
|
@@ -266,7 +274,8 @@ class FileSummaryManager {
|
|
|
266
274
|
// summaries; without `force` an explicit "Regenerate" of an unchanged file
|
|
267
275
|
// would be a silent no-op.
|
|
268
276
|
const currentLang = this.deps.language?.() ?? 'en';
|
|
269
|
-
const
|
|
277
|
+
const summaryRoot = req.summaryRoot ?? req.projectPath;
|
|
278
|
+
const existing = readSummary(summaryRoot, req.relPath);
|
|
270
279
|
if (!req.force &&
|
|
271
280
|
existing &&
|
|
272
281
|
existing.fileHash === newHash &&
|
|
@@ -442,7 +451,11 @@ class FileSummaryManager {
|
|
|
442
451
|
generatedBy: { model: out.model, promptVersion: CURRENT_PROMPT_VERSION, truncated },
|
|
443
452
|
triggeredBy: req.triggeredBy,
|
|
444
453
|
};
|
|
445
|
-
|
|
454
|
+
// Relocate-artifacts: summaries OUTPUT to the workspace when relocated
|
|
455
|
+
// (summaryRoot ≠ projectPath ⇒ skip the repo .gitignore append); source
|
|
456
|
+
// was read above from req.projectPath (the repo). Legacy ⇒ both equal.
|
|
457
|
+
const summaryRoot = req.summaryRoot ?? req.projectPath;
|
|
458
|
+
writeSummary(summaryRoot, req.relPath, payload, summaryRoot === req.projectPath);
|
|
446
459
|
// Keep the watcher's negative-cache a correct superset.
|
|
447
460
|
this.knownSummaries.get(req.projectId)?.add(req.relPath);
|
|
448
461
|
try {
|
|
@@ -531,22 +544,34 @@ class FileSummaryManager {
|
|
|
531
544
|
this.activeControllers.delete(controller);
|
|
532
545
|
}
|
|
533
546
|
}
|
|
534
|
-
|
|
535
|
-
|
|
547
|
+
/** `summaryRoot` (relocate-artifacts) is where the summary JSON lives — the
|
|
548
|
+
* workspace when relocated, else === projectPath. */
|
|
549
|
+
markStale(projectPath, projectId, relPath, summaryRoot) {
|
|
550
|
+
const existing = readSummary(summaryRoot ?? projectPath, relPath);
|
|
536
551
|
if (!existing)
|
|
537
552
|
return;
|
|
538
553
|
this.deps.broadcast(buildSummaryUpdated(projectId, existing, true));
|
|
539
554
|
}
|
|
540
|
-
|
|
555
|
+
/**
|
|
556
|
+
* Watch the repo SOURCE tree (`projectPath`) for edits and mark the
|
|
557
|
+
* corresponding summary stale. Relocate-artifacts: `summaryRoot` is where the
|
|
558
|
+
* summary JSON lives (workspace when relocated) — source is watched at
|
|
559
|
+
* `projectPath`, summaries are scanned/swept/marked at `summaryRoot`. When
|
|
560
|
+
* omitted `summaryRoot` defaults to `projectPath` (legacy, byte-identical).
|
|
561
|
+
*/
|
|
562
|
+
attachWatcher(projectId, projectPath, summaryRoot) {
|
|
541
563
|
if (this.watchers.has(projectId))
|
|
542
564
|
return;
|
|
543
|
-
|
|
565
|
+
const sumRoot = summaryRoot ?? projectPath;
|
|
566
|
+
this.knownSummaries.set(projectId, this.scanKnownSummaries(sumRoot));
|
|
544
567
|
// Reclaim summary JSON files whose source file was renamed/deleted since the
|
|
545
568
|
// last session. Runs once per project per session (attachWatcher is
|
|
546
569
|
// idempotent) and is capped at 200/pass inside sweepOrphans. The chokidar
|
|
547
570
|
// watcher only sees 'change', never 'unlink', so this is the only reaper.
|
|
571
|
+
// sweepOrphans resolves source files against the repo, so it must know both:
|
|
572
|
+
// summaries under sumRoot, source under projectPath.
|
|
548
573
|
try {
|
|
549
|
-
sweepOrphans(projectPath);
|
|
574
|
+
sweepOrphans(sumRoot, undefined, projectPath);
|
|
550
575
|
}
|
|
551
576
|
catch { /* best effort */ }
|
|
552
577
|
// CRITICAL: prune build/dep trees (node_modules, dist, target, src-tauri/target,
|
|
@@ -574,7 +599,7 @@ class FileSummaryManager {
|
|
|
574
599
|
const known = this.knownSummaries.get(projectId);
|
|
575
600
|
if (known && !known.has(rel))
|
|
576
601
|
return;
|
|
577
|
-
this.markStale(projectPath, projectId, rel);
|
|
602
|
+
this.markStale(projectPath, projectId, rel, sumRoot);
|
|
578
603
|
});
|
|
579
604
|
this.watchers.set(projectId, { projectPath, watcher });
|
|
580
605
|
}
|