specrails-desktop 2.0.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/.claude/commands/specrails/batch-implement.md +287 -0
- package/.claude/commands/specrails/compat-check.md +271 -0
- package/.claude/commands/specrails/doctor.md +62 -0
- package/.claude/commands/specrails/enrich.md +1635 -0
- package/.claude/commands/specrails/explore-spec.md +173 -0
- package/.claude/commands/specrails/health-check.md +527 -0
- package/.claude/commands/specrails/implement.md +1457 -0
- package/.claude/commands/specrails/memory-inspect.md +259 -0
- package/.claude/commands/specrails/opsx-diff.md +419 -0
- package/.claude/commands/specrails/propose-spec.md +102 -0
- package/.claude/commands/specrails/reconfig.md +89 -0
- package/.claude/commands/specrails/refactor-recommender.md +212 -0
- package/.claude/commands/specrails/retry.md +363 -0
- package/.claude/commands/specrails/telemetry.md +552 -0
- package/.claude/commands/specrails/why.md +96 -0
- package/LICENSE +21 -0
- package/README.md +290 -0
- package/cli/dist/specrails-desktop.js +1098 -0
- package/client/dist/assets/ActivityFeedPage-Gy4x8dBt.js +1 -0
- package/client/dist/assets/AgentsPage-CPgu--Fb.js +86 -0
- package/client/dist/assets/AnalyticsPage-B5sJEee2.js +1 -0
- package/client/dist/assets/BarChart-7IMQ8HY1.js +33 -0
- package/client/dist/assets/CodePage-CBdFvbwe.js +2 -0
- package/client/dist/assets/DesktopAnalyticsPage-w0rdTq4w.js +1 -0
- package/client/dist/assets/DocsDialog-BZUYM7wm.js +11 -0
- package/client/dist/assets/DocsPage-9QglWl46.js +11 -0
- package/client/dist/assets/ExportDropdown-BLZFXtNi.js +1 -0
- package/client/dist/assets/IntegrationsPage-BxBE4y99.js +3 -0
- package/client/dist/assets/JobDetailPage-DydWx_5S.js +16 -0
- package/client/dist/assets/JobsPage-20ibw0IO.js +1 -0
- package/client/dist/assets/abap-Bw6f2wDG.js +1 -0
- package/client/dist/assets/activity-BEIp_Y1A.js +1 -0
- package/client/dist/assets/activity-BdrPln96.js +1 -0
- package/client/dist/assets/activity-CpkRS8Sx.js +1 -0
- package/client/dist/assets/activity-DKCpESPt.js +1 -0
- package/client/dist/assets/activity-DOUVEjJi.js +1 -0
- package/client/dist/assets/activity-DRwkql_y.js +1 -0
- package/client/dist/assets/activity-DcDQ7tjw.js +1 -0
- package/client/dist/assets/activity-Dv6H7wEr.js +1 -0
- package/client/dist/assets/addon-image-3WCl5Vhd.js +1 -0
- package/client/dist/assets/addon-ligatures-C5OdliKs.js +2 -0
- package/client/dist/assets/addon-webgl-BbX6pSjl.js +44 -0
- package/client/dist/assets/addspec-B5yl4Loj.js +1 -0
- package/client/dist/assets/addspec-BEeF5-zc.js +1 -0
- package/client/dist/assets/addspec-D33ocMxf.js +1 -0
- package/client/dist/assets/addspec-DFswZ0jK.js +1 -0
- package/client/dist/assets/addspec-DRE-jZv7.js +1 -0
- package/client/dist/assets/addspec-DVZ15Jp8.js +1 -0
- package/client/dist/assets/addspec-Fkv91Opc.js +1 -0
- package/client/dist/assets/addspec-GWm4ffKl.js +1 -0
- package/client/dist/assets/agents-1nCDWRmP.js +1 -0
- package/client/dist/assets/agents-Bm9rPqnt.js +1 -0
- package/client/dist/assets/agents-CMxtJMLD.js +1 -0
- package/client/dist/assets/agents-DK-Dlc0i.js +1 -0
- package/client/dist/assets/agents-Q6Ldfpxx.js +1 -0
- package/client/dist/assets/agents-TeOSy-ax.js +1 -0
- package/client/dist/assets/agents-iTqjRajS.js +1 -0
- package/client/dist/assets/agents-s87sMGzL.js +1 -0
- package/client/dist/assets/agentstudio-B6Wb59E7.js +1 -0
- package/client/dist/assets/agentstudio-BADhZ41e.js +1 -0
- package/client/dist/assets/agentstudio-BSnWLR63.js +1 -0
- package/client/dist/assets/agentstudio-BdidyBzZ.js +1 -0
- package/client/dist/assets/agentstudio-CxlUllqI.js +1 -0
- package/client/dist/assets/agentstudio-D3I62TLJ.js +1 -0
- package/client/dist/assets/agentstudio-DuH9TogZ.js +1 -0
- package/client/dist/assets/agentstudio-Kw88_dUF.js +1 -0
- package/client/dist/assets/aiedit-BWxHGsYA.js +1 -0
- package/client/dist/assets/aiedit-D2ji6Qy0.js +1 -0
- package/client/dist/assets/aiedit-DAhZTvtk.js +1 -0
- package/client/dist/assets/aiedit-DJMny-D5.js +1 -0
- package/client/dist/assets/aiedit-DOcxERkU.js +1 -0
- package/client/dist/assets/aiedit-DvrcbwGv.js +1 -0
- package/client/dist/assets/aiedit-TTwzL1TS.js +1 -0
- package/client/dist/assets/aiedit-WBSjT_C1.js +1 -0
- package/client/dist/assets/analytics-BIdr0YfL.js +1 -0
- package/client/dist/assets/analytics-C6EzgtdE.js +1 -0
- package/client/dist/assets/analytics-C9Zc-rkM.js +1 -0
- package/client/dist/assets/analytics-CVx3YOc0.js +1 -0
- package/client/dist/assets/analytics-CYj0tfj7.js +1 -0
- package/client/dist/assets/analytics-CnY4kNG3.js +1 -0
- package/client/dist/assets/analytics-CrPCZRJ-.js +1 -0
- package/client/dist/assets/analytics-DMCto-TF.js +1 -0
- package/client/dist/assets/apex-Cw8_REBo.js +1 -0
- package/client/dist/assets/atom-one-dark-B-oHczHB.css +1 -0
- package/client/dist/assets/attachments-BIsSSnHJ.js +1 -0
- package/client/dist/assets/attachments-BW4L3l2L.js +1 -0
- package/client/dist/assets/attachments-Bcf6BG6V.js +1 -0
- package/client/dist/assets/attachments-Bke8sCU4.js +1 -0
- package/client/dist/assets/attachments-COcrGRFz.js +1 -0
- package/client/dist/assets/attachments-DYHGA2Dj.js +1 -0
- package/client/dist/assets/attachments-Dd92KpUH.js +1 -0
- package/client/dist/assets/attachments-DzdU6DV6.js +1 -0
- package/client/dist/assets/azcli-Cz6HAoOw.js +1 -0
- package/client/dist/assets/bat-CcJ-xyqL.js +1 -0
- package/client/dist/assets/bicep-z1WDCKYz.js +2 -0
- package/client/dist/assets/browser-5ErDlJoR.js +1 -0
- package/client/dist/assets/browser-Bc-YdlVg.js +1 -0
- package/client/dist/assets/browser-BlYF4OOq.js +1 -0
- package/client/dist/assets/browser-CT-ReZGt.js +1 -0
- package/client/dist/assets/browser-DGITz3fC.js +1 -0
- package/client/dist/assets/browser-JsAIGCEW.js +1 -0
- package/client/dist/assets/browser-M5-rbPlw.js +1 -0
- package/client/dist/assets/browser-Qya9cARy.js +1 -0
- package/client/dist/assets/cameligo-BRewOpfa.js +1 -0
- package/client/dist/assets/chat-BEGuC03z.js +1 -0
- package/client/dist/assets/chat-BEW60P_u.js +1 -0
- package/client/dist/assets/chat-BQNMD0PL.js +1 -0
- package/client/dist/assets/chat-BsbNGPW9.js +1 -0
- package/client/dist/assets/chat-CboQguCi.js +1 -0
- package/client/dist/assets/chat-DRCa9pOt.js +1 -0
- package/client/dist/assets/chat-DwUm6W9z.js +1 -0
- package/client/dist/assets/chat-yoXwguQu.js +1 -0
- package/client/dist/assets/chunk-CilyBKbf.js +1 -0
- package/client/dist/assets/clojure-DBjRWN6g.js +1 -0
- package/client/dist/assets/clsx-DnqN-uhr.js +1 -0
- package/client/dist/assets/code-AL1rVIMb.js +1 -0
- package/client/dist/assets/code-C0BKpkht.js +1 -0
- package/client/dist/assets/code-C0FTS3ew.js +1 -0
- package/client/dist/assets/code-CPcHxzxw.js +1 -0
- package/client/dist/assets/code-D3ryDniw.js +1 -0
- package/client/dist/assets/code-D3zVVQTj.js +1 -0
- package/client/dist/assets/code-PCmfS3dn.js +1 -0
- package/client/dist/assets/code-exI0G5Wd.js +1 -0
- package/client/dist/assets/codicon-ngg6Pgfi.ttf +0 -0
- package/client/dist/assets/coffee-Cfk_XHGR.js +1 -0
- package/client/dist/assets/commands-B772IyDa.js +1 -0
- package/client/dist/assets/commands-BDDp6xFG.js +1 -0
- package/client/dist/assets/commands-CJxCry-o.js +1 -0
- package/client/dist/assets/commands-CfgY-_of.js +1 -0
- package/client/dist/assets/commands-DLrvnPNg.js +1 -0
- package/client/dist/assets/commands-IXMOKBYt.js +1 -0
- package/client/dist/assets/commands-UD1NzmwX.js +1 -0
- package/client/dist/assets/commands-sqrqsxyE.js +1 -0
- package/client/dist/assets/common-DCr6VzJ7.js +1 -0
- package/client/dist/assets/common-Dard9UNH.js +1 -0
- package/client/dist/assets/common-DeDELLZJ.js +1 -0
- package/client/dist/assets/common-DltqHaAe.js +1 -0
- package/client/dist/assets/common-Dmm1GhdD.js +1 -0
- package/client/dist/assets/common-DnjcgkPH.js +1 -0
- package/client/dist/assets/common-GbpxfPG8.js +1 -0
- package/client/dist/assets/common-wA36jmj1.js +1 -0
- package/client/dist/assets/cpp-BVob6BaP.js +1 -0
- package/client/dist/assets/csharp-C4fbRuOu.js +1 -0
- package/client/dist/assets/csp-DthFP_vT.js +1 -0
- package/client/dist/assets/css-CGMH0hcW.js +3 -0
- package/client/dist/assets/css.worker-Wv5dxAWO.js +89 -0
- package/client/dist/assets/cssMode-Cc6ozl-J.js +1 -0
- package/client/dist/assets/cypher-Pnf68BRV.js +1 -0
- package/client/dist/assets/dart-PMMOtxZX.js +1 -0
- package/client/dist/assets/dashboard-B4ixDVk8.js +1 -0
- package/client/dist/assets/dashboard-BZBADHSj.js +1 -0
- package/client/dist/assets/dashboard-C1MfeUHs.js +1 -0
- package/client/dist/assets/dashboard-C7SK6xu5.js +1 -0
- package/client/dist/assets/dashboard-CB6Le1yN.js +1 -0
- package/client/dist/assets/dashboard-CoTpMOBM.js +1 -0
- package/client/dist/assets/dashboard-Duo4DDCW.js +1 -0
- package/client/dist/assets/dashboard-I19DXBxw.js +1 -0
- package/client/dist/assets/dist-js-BY-Fv_fg.js +1 -0
- package/client/dist/assets/dist-js-Bakc4uxT.js +1 -0
- package/client/dist/assets/dockerfile-di1nsJCc.js +1 -0
- package/client/dist/assets/ecl-D_WVcB5M.js +1 -0
- package/client/dist/assets/editor-Br_kD0ds.css +1 -0
- package/client/dist/assets/editor.api2-XLGzZfbc.js +872 -0
- package/client/dist/assets/editor.main-CfXxHimg.js +6 -0
- package/client/dist/assets/editor.worker-Bd9IXS8d.js +26 -0
- package/client/dist/assets/elixir-OAdJEMOn.js +1 -0
- package/client/dist/assets/explore-4mFpnrKU.js +1 -0
- package/client/dist/assets/explore-A8Ltoblq.js +1 -0
- package/client/dist/assets/explore-B9A3iN2W.js +1 -0
- package/client/dist/assets/explore-BV5Xxlsn.js +1 -0
- package/client/dist/assets/explore-BrBJvfjP.js +1 -0
- package/client/dist/assets/explore-C3FSE42C.js +1 -0
- package/client/dist/assets/explore-D2EFgt8J.js +1 -0
- package/client/dist/assets/explore-hFc3HFcp.js +1 -0
- package/client/dist/assets/flow9-D3QEZjgn.js +1 -0
- package/client/dist/assets/format-command-CwGuwzGA.js +1 -0
- package/client/dist/assets/freemarker2-DP7J1gG3.js +3 -0
- package/client/dist/assets/fsharp-BF0k_8N8.js +1 -0
- package/client/dist/assets/go-BAQO5Jsz.js +1 -0
- package/client/dist/assets/graphql-hdFVFkiV.js +1 -0
- package/client/dist/assets/handlebars-BjRlucw6.js +1 -0
- package/client/dist/assets/hcl-DWnl1o-X.js +1 -0
- package/client/dist/assets/html-OumBQJ-U.js +1 -0
- package/client/dist/assets/html.worker-CQP8QQsS.js +502 -0
- package/client/dist/assets/htmlMode-CStc3zXM.js +1 -0
- package/client/dist/assets/index-CimDRRi7.css +2 -0
- package/client/dist/assets/index-XGZaKl_u.js +142 -0
- package/client/dist/assets/ini-CB-6OVu3.js +1 -0
- package/client/dist/assets/integrations-C3p12Ms6.js +1 -0
- package/client/dist/assets/integrations-Cr6hH7XR.js +1 -0
- package/client/dist/assets/integrations-Cublz3m6.js +1 -0
- package/client/dist/assets/integrations-D28q1kF6.js +1 -0
- package/client/dist/assets/integrations-DRdbki5W.js +1 -0
- package/client/dist/assets/integrations-DaC4SzzL.js +1 -0
- package/client/dist/assets/integrations-DmQYCUvN.js +1 -0
- package/client/dist/assets/integrations-HIlUxXVs.js +1 -0
- package/client/dist/assets/java-d1CmfiHX.js +1 -0
- package/client/dist/assets/javascript-CMk--e7g.js +1 -0
- package/client/dist/assets/jobs-BE1siB0M.js +1 -0
- package/client/dist/assets/jobs-BHcQ_Faf.js +1 -0
- package/client/dist/assets/jobs-CFfc2dNX.js +1 -0
- package/client/dist/assets/jobs-CSi5n8X_.js +1 -0
- package/client/dist/assets/jobs-Dc3X86PY.js +1 -0
- package/client/dist/assets/jobs-De5tASex.js +1 -0
- package/client/dist/assets/jobs-DsoXEdo7.js +1 -0
- package/client/dist/assets/jobs-Wl-ApPMb.js +1 -0
- package/client/dist/assets/json.worker-DzV-CpCQ.js +58 -0
- package/client/dist/assets/jsonMode-C2h3ZcjZ.js +7 -0
- package/client/dist/assets/julia-Bgv08lKa.js +1 -0
- package/client/dist/assets/kotlin-u98kaVTf.js +1 -0
- package/client/dist/assets/less-CjYwpgg5.js +2 -0
- package/client/dist/assets/lexon-YTjaAFBB.js +1 -0
- package/client/dist/assets/lib-CPxTMOAq.js +7 -0
- package/client/dist/assets/liquid-mI3KJrBE.js +1 -0
- package/client/dist/assets/lspLanguageFeatures-DU09ggWi.js +4 -0
- package/client/dist/assets/lua-BzmkWv27.js +1 -0
- package/client/dist/assets/m3-CFwk9fw0.js +1 -0
- package/client/dist/assets/markdown-CR5iMpSZ.js +1 -0
- package/client/dist/assets/mdx-C41VDTR_.js +1 -0
- package/client/dist/assets/mips-CcEalc17.js +1 -0
- package/client/dist/assets/monaco.contribution-CPObAXMC.js +2 -0
- package/client/dist/assets/msdax-BQbkawnr.js +1 -0
- package/client/dist/assets/mysql-GTlaaW_P.js +1 -0
- package/client/dist/assets/nav-0fwkrgHt.js +1 -0
- package/client/dist/assets/nav-BEL3MTwK.js +1 -0
- package/client/dist/assets/nav-B_G-TJDW.js +1 -0
- package/client/dist/assets/nav-C2YXcbZS.js +1 -0
- package/client/dist/assets/nav-ClzOE4mA.js +1 -0
- package/client/dist/assets/nav-CtYwmMgu.js +1 -0
- package/client/dist/assets/nav-D2bOGSEg.js +1 -0
- package/client/dist/assets/nav-iH1V5j6o.js +1 -0
- package/client/dist/assets/objective-c-Byu1T5if.js +1 -0
- package/client/dist/assets/pascal-BrfzBfRm.js +1 -0
- package/client/dist/assets/pascaligo-BXXKFUeo.js +1 -0
- package/client/dist/assets/perl-B3OikKq-.js +1 -0
- package/client/dist/assets/pgsql-CTsa0Acc.js +1 -0
- package/client/dist/assets/php-DiQh3FUW.js +1 -0
- package/client/dist/assets/pla-92uH8Fzm.js +1 -0
- package/client/dist/assets/postiats-BbeWkKUr.js +1 -0
- package/client/dist/assets/powerquery-DgDMzpsm.js +1 -0
- package/client/dist/assets/powershell-BfdUUzaG.js +1 -0
- package/client/dist/assets/preload-helper-DSXbuxSR.js +1 -0
- package/client/dist/assets/protobuf-BojW2ftW.js +2 -0
- package/client/dist/assets/pug-BxqTg3IU.js +1 -0
- package/client/dist/assets/python-Y27rKQtk.js +1 -0
- package/client/dist/assets/qsharp-BX_A-MW9.js +1 -0
- package/client/dist/assets/r-D9BMnxvJ.js +1 -0
- package/client/dist/assets/razor-Cd5-q9Bp.js +1 -0
- package/client/dist/assets/redis-5cJqEQJJ.js +1 -0
- package/client/dist/assets/redshift-d8BBqiwb.js +1 -0
- package/client/dist/assets/restructuredtext-C8a6yIcZ.js +1 -0
- package/client/dist/assets/ruby-egeh-6KX.js +1 -0
- package/client/dist/assets/rust-a3r9IInB.js +1 -0
- package/client/dist/assets/sb-y8iRIDei.js +1 -0
- package/client/dist/assets/scala-BPDK2AmK.js +1 -0
- package/client/dist/assets/scheme-BIWUEoOs.js +1 -0
- package/client/dist/assets/scss-CA-PSzwg.js +3 -0
- package/client/dist/assets/settings-55oDcbSh.js +1 -0
- package/client/dist/assets/settings-Bd4Tq1RB.js +1 -0
- package/client/dist/assets/settings-CCSM-Fhn.js +1 -0
- package/client/dist/assets/settings-D3e_bDoW.js +1 -0
- package/client/dist/assets/settings-DKbTkbn7.js +1 -0
- package/client/dist/assets/settings-Dxpo6_w7.js +1 -0
- package/client/dist/assets/settings-bt84e3Aa.js +1 -0
- package/client/dist/assets/settings-nu68QukM.js +1 -0
- package/client/dist/assets/setup-BMqwfbW9.js +1 -0
- package/client/dist/assets/setup-Bb5LcG28.js +1 -0
- package/client/dist/assets/setup-BeEx2_da.js +1 -0
- package/client/dist/assets/setup-CCCrB53Q.js +1 -0
- package/client/dist/assets/setup-CJA0ATmd.js +1 -0
- package/client/dist/assets/setup-CeiDbZcb.js +1 -0
- package/client/dist/assets/setup-Cus7TApA.js +1 -0
- package/client/dist/assets/setup-D9qOs2Xo.js +1 -0
- package/client/dist/assets/shell--LiT1Bja.js +1 -0
- package/client/dist/assets/solidity-DdqZccZg.js +1 -0
- package/client/dist/assets/sophia-S6-YxNG_.js +1 -0
- package/client/dist/assets/sparql-BSf5kMp2.js +1 -0
- package/client/dist/assets/specs-BFfu3u-a.js +1 -0
- package/client/dist/assets/specs-B__C8-8a.js +1 -0
- package/client/dist/assets/specs-CZ1PsXsC.js +1 -0
- package/client/dist/assets/specs-D2FzlLn9.js +1 -0
- package/client/dist/assets/specs-DaUTrNF9.js +1 -0
- package/client/dist/assets/specs-Dyc5hYeE.js +1 -0
- package/client/dist/assets/specs-cKEh2LXt.js +1 -0
- package/client/dist/assets/specs-k0PyLDVt.js +1 -0
- package/client/dist/assets/sql-D7KgjR8G.js +1 -0
- package/client/dist/assets/st-BnoDa-Ml.js +1 -0
- package/client/dist/assets/swift-DEUHTkUX.js +1 -0
- package/client/dist/assets/systemverilog-Tqb_KPnW.js +1 -0
- package/client/dist/assets/tcl-BmBFS2qq.js +1 -0
- package/client/dist/assets/terminal-80yDMgMF.js +1 -0
- package/client/dist/assets/terminal-Bje4ziIa.js +1 -0
- package/client/dist/assets/terminal-C2WYcFHF.js +1 -0
- package/client/dist/assets/terminal-CSONJOex.js +1 -0
- package/client/dist/assets/terminal-DEqzGtcr.js +1 -0
- package/client/dist/assets/terminal-DeWzh6ys.js +1 -0
- package/client/dist/assets/terminal-YOlsJCQj.js +1 -0
- package/client/dist/assets/terminal-lkZYR4wJ.js +1 -0
- package/client/dist/assets/tickets-CB7N30gm.js +1 -0
- package/client/dist/assets/tickets-CF2PYelu.js +1 -0
- package/client/dist/assets/tickets-DNOANUXr.js +1 -0
- package/client/dist/assets/tickets-DU1aqsbr.js +1 -0
- package/client/dist/assets/tickets-DYvafSaY.js +1 -0
- package/client/dist/assets/tickets-DlpC_iTg.js +1 -0
- package/client/dist/assets/tickets-DucYgtdl.js +1 -0
- package/client/dist/assets/tickets-clefmXLv.js +1 -0
- package/client/dist/assets/ts.worker-METxwbDZ.js +67719 -0
- package/client/dist/assets/tsMode-B0y_xEci.js +11 -0
- package/client/dist/assets/twig-BQV8igWC.js +1 -0
- package/client/dist/assets/typescript-BzK0OgwW.js +1 -0
- package/client/dist/assets/typespec-DlFroUGY.js +1 -0
- package/client/dist/assets/useProjectCache-DSaiGFjV.js +1 -0
- package/client/dist/assets/vb-BlrJpIMX.js +1 -0
- package/client/dist/assets/wgsl-BWgIc6FZ.js +298 -0
- package/client/dist/assets/workers-rt--R2Qy.js +1 -0
- package/client/dist/assets/xml-eX9QXAmI.js +1 -0
- package/client/dist/assets/yaml-fcsNkpOt.js +1 -0
- package/client/dist/index.html +246 -0
- package/docs/README.md +54 -0
- package/docs/cli.md +198 -0
- package/docs/codex.md +210 -0
- package/docs/creating-specs.md +197 -0
- package/docs/customizing.md +197 -0
- package/docs/getting-started.md +140 -0
- package/docs/internals/README.md +25 -0
- package/docs/internals/adding-a-provider.md +238 -0
- package/docs/internals/api-reference.md +634 -0
- package/docs/internals/architecture.md +332 -0
- package/docs/internals/configuration.md +172 -0
- package/docs/internals/openspec-workflow.md +282 -0
- package/docs/internals/operations-runbook.md +198 -0
- package/docs/internals/profiles.md +152 -0
- package/docs/platforms/macos.md +130 -0
- package/docs/platforms/windows.md +81 -0
- package/docs/running-pipelines.md +240 -0
- package/docs/terminal.md +138 -0
- package/docs/tracking-cost.md +155 -0
- package/package.json +82 -0
- package/server/dist/agent-generator.js +232 -0
- package/server/dist/agent-refine-db.js +124 -0
- package/server/dist/agent-refine-manager.js +526 -0
- package/server/dist/ai-invocations.js +111 -0
- package/server/dist/attachment-manager.js +299 -0
- package/server/dist/auth.js +207 -0
- package/server/dist/binary-probe.js +35 -0
- package/server/dist/browser-capture-manager.js +576 -0
- package/server/dist/browser-capture-types.js +28 -0
- package/server/dist/browser-network.js +149 -0
- package/server/dist/browser-playwright.js +888 -0
- package/server/dist/build-dirs.js +44 -0
- package/server/dist/changes-reader.js +120 -0
- package/server/dist/chat-manager.js +1060 -0
- package/server/dist/chromium-resolver.js +311 -0
- package/server/dist/code-explorer-router.js +788 -0
- package/server/dist/codex-otel-bridge.js +235 -0
- package/server/dist/command-resolver.js +102 -0
- package/server/dist/config.js +306 -0
- package/server/dist/context-budget.js +113 -0
- package/server/dist/context-scope.js +279 -0
- package/server/dist/contract-refine-runner.js +521 -0
- package/server/dist/core-compat.js +207 -0
- package/server/dist/core-package.js +14 -0
- package/server/dist/db.js +1034 -0
- package/server/dist/desktop-analytics.js +156 -0
- package/server/dist/desktop-db.js +456 -0
- package/server/dist/desktop-router.js +735 -0
- package/server/dist/docs-router.js +207 -0
- package/server/dist/explore-contract-refine.js +421 -0
- package/server/dist/explore-cwd-manager.js +242 -0
- package/server/dist/explore-draft-title.js +47 -0
- package/server/dist/explore-smash.js +450 -0
- package/server/dist/feature-flags.js +17 -0
- package/server/dist/file-provenance.js +382 -0
- package/server/dist/file-summary-generator.js +221 -0
- package/server/dist/file-summary-manager.js +689 -0
- package/server/dist/hooks.js +102 -0
- package/server/dist/ids.js +7 -0
- package/server/dist/index.js +586 -0
- package/server/dist/metrics.js +136 -0
- package/server/dist/mobile/index.js +16 -0
- package/server/dist/mobile/mobile-admin-router.js +84 -0
- package/server/dist/mobile/mobile-auth.js +67 -0
- package/server/dist/mobile/mobile-devices.js +80 -0
- package/server/dist/mobile/mobile-event-bus.js +39 -0
- package/server/dist/mobile/mobile-gateway.js +285 -0
- package/server/dist/mobile/mobile-mdns.js +81 -0
- package/server/dist/mobile/mobile-pairing.js +179 -0
- package/server/dist/mobile/mobile-redact.js +53 -0
- package/server/dist/mobile/mobile-router.js +411 -0
- package/server/dist/mobile/mobile-tls.js +86 -0
- package/server/dist/mobile/mobile-types.js +9 -0
- package/server/dist/mobile/mobile-ws.js +275 -0
- package/server/dist/path-resolver.js +298 -0
- package/server/dist/plugin-manager.js +617 -0
- package/server/dist/plugins/claude-approval.js +179 -0
- package/server/dist/plugins/claude-md-mutation.js +146 -0
- package/server/dist/plugins/codex-mcp.js +108 -0
- package/server/dist/plugins/contributors.js +72 -0
- package/server/dist/plugins/drift.js +58 -0
- package/server/dist/plugins/index.js +14 -0
- package/server/dist/plugins/json-mutation.js +120 -0
- package/server/dist/plugins/manager.js +32 -0
- package/server/dist/plugins/ownership.js +86 -0
- package/server/dist/plugins/paths.js +37 -0
- package/server/dist/plugins/prereq-installer.js +104 -0
- package/server/dist/plugins/rail-integration.js +79 -0
- package/server/dist/plugins/serena/index.js +13 -0
- package/server/dist/plugins/serena/install.js +91 -0
- package/server/dist/plugins/serena/instructions-content.js +21 -0
- package/server/dist/plugins/serena/manifest.js +111 -0
- package/server/dist/plugins/serena/verify.js +78 -0
- package/server/dist/plugins-router.js +215 -0
- package/server/dist/pricing.js +89 -0
- package/server/dist/profile-manager.js +310 -0
- package/server/dist/profiles-router.js +759 -0
- package/server/dist/project-registry.js +443 -0
- package/server/dist/project-router.js +4016 -0
- package/server/dist/proposal-manager.js +291 -0
- package/server/dist/provider-selection.js +69 -0
- package/server/dist/providers/claude-adapter.js +281 -0
- package/server/dist/providers/codex-adapter.js +264 -0
- package/server/dist/providers/index.js +23 -0
- package/server/dist/providers/registry.js +37 -0
- package/server/dist/providers/types.js +22 -0
- package/server/dist/queue-manager.js +1511 -0
- package/server/dist/rails-router.js +362 -0
- package/server/dist/rails-store.js +116 -0
- package/server/dist/result-event.js +106 -0
- package/server/dist/schemas/profile.v1.json +151 -0
- package/server/dist/setup-manager.js +1165 -0
- package/server/dist/setup-prerequisites.js +372 -0
- package/server/dist/smash-runner.js +663 -0
- package/server/dist/spec-draft-parser.js +133 -0
- package/server/dist/spec-launcher-manager.js +174 -0
- package/server/dist/spec-models.js +32 -0
- package/server/dist/specrails-tech-client.js +82 -0
- package/server/dist/spending.js +448 -0
- package/server/dist/telemetry-compactor.js +180 -0
- package/server/dist/telemetry-export.js +317 -0
- package/server/dist/telemetry-receiver.js +224 -0
- package/server/dist/terminal-manager.js +633 -0
- package/server/dist/terminal-marks-store.js +117 -0
- package/server/dist/terminal-osc-parser.js +159 -0
- package/server/dist/terminal-settings.js +282 -0
- package/server/dist/terminal-shell-integration.js +196 -0
- package/server/dist/ticket-broadcast.js +47 -0
- package/server/dist/ticket-store.js +397 -0
- package/server/dist/ticket-watcher.js +117 -0
- package/server/dist/types.js +10 -0
- package/server/dist/user-mcp-config.js +117 -0
- package/server/dist/util/cli-prompt.js +181 -0
- package/server/dist/util/secure-fs.js +50 -0
- package/server/dist/util/win-spawn.js +43 -0
- package/server/dist/webhook-manager.js +89 -0
- package/server/dist/ws-routing.js +47 -0
|
@@ -0,0 +1,1165 @@
|
|
|
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.SetupManager = exports.EMPTY_SUMMARY = exports.QUICK_CHECKPOINTS = exports.CHECKPOINTS = void 0;
|
|
7
|
+
exports.detectCheckpointFromText = detectCheckpointFromText;
|
|
8
|
+
exports.validateInstalledCore = validateInstalledCore;
|
|
9
|
+
exports.computeSummary = computeSummary;
|
|
10
|
+
exports.sweepLegacySrCommands = sweepLegacySrCommands;
|
|
11
|
+
const child_process_1 = require("child_process");
|
|
12
|
+
const readline_1 = require("readline");
|
|
13
|
+
const fs_1 = require("fs");
|
|
14
|
+
const path_1 = require("path");
|
|
15
|
+
const os_1 = require("os");
|
|
16
|
+
const tree_kill_1 = __importDefault(require("tree-kill"));
|
|
17
|
+
const core_compat_1 = require("./core-compat");
|
|
18
|
+
const cli_prompt_1 = require("./util/cli-prompt");
|
|
19
|
+
const win_spawn_1 = require("./util/win-spawn");
|
|
20
|
+
const setup_prerequisites_1 = require("./setup-prerequisites");
|
|
21
|
+
const core_package_1 = require("./core-package");
|
|
22
|
+
const providers_1 = require("./providers");
|
|
23
|
+
/**
|
|
24
|
+
* specrails-core's installer (Node-native from v4.2.0 onward, bash
|
|
25
|
+
* prior) always scaffolds into `.claude/` regardless of which AI
|
|
26
|
+
* CLI the project uses. The provider choice affects which binary
|
|
27
|
+
* runs (claude vs codex), not where the framework files live.
|
|
28
|
+
*/
|
|
29
|
+
const SPECRAILS_DIR = '.claude';
|
|
30
|
+
// ─── specrails-core binary resolution ────────────────────────────────────────
|
|
31
|
+
// Default: npx CORE_PACKAGE_SPEC (major-pinned range, see core-package.ts)
|
|
32
|
+
// Override: set SPECRAILS_CORE_BIN to use a local/linked version, e.g.
|
|
33
|
+
// SPECRAILS_CORE_BIN=specrails-core npm run dev
|
|
34
|
+
const WHICH_CMD = process.platform === 'win32' ? 'where' : 'which';
|
|
35
|
+
function resolveCoreBinary(bin) {
|
|
36
|
+
if ((0, path_1.isAbsolute)(bin))
|
|
37
|
+
return bin;
|
|
38
|
+
if (bin.includes('/') || bin.includes('\\'))
|
|
39
|
+
return (0, path_1.resolve)(bin);
|
|
40
|
+
const result = (0, child_process_1.spawnSync)(WHICH_CMD, [bin], {
|
|
41
|
+
env: process.env,
|
|
42
|
+
shell: process.platform === 'win32',
|
|
43
|
+
encoding: 'utf-8',
|
|
44
|
+
timeout: 5_000,
|
|
45
|
+
});
|
|
46
|
+
if (result.error || (result.status ?? 1) !== 0)
|
|
47
|
+
return bin;
|
|
48
|
+
const first = `${result.stdout ?? ''}`.trim().split(/\r?\n/)[0]?.trim();
|
|
49
|
+
return first && first.length > 0 ? first : bin;
|
|
50
|
+
}
|
|
51
|
+
function getCoreCommand() {
|
|
52
|
+
const override = process.env.SPECRAILS_CORE_BIN;
|
|
53
|
+
if (override) {
|
|
54
|
+
return { bin: resolveCoreBinary(override), pkg: '' };
|
|
55
|
+
}
|
|
56
|
+
return { bin: 'npx', pkg: core_package_1.CORE_PACKAGE_SPEC };
|
|
57
|
+
}
|
|
58
|
+
function buildCoreArgs(args) {
|
|
59
|
+
const { bin, pkg } = getCoreCommand();
|
|
60
|
+
const fullArgs = pkg ? ['--yes', '--prefer-online', pkg, ...args] : args;
|
|
61
|
+
return { bin, fullArgs };
|
|
62
|
+
}
|
|
63
|
+
function spawnCoreInit(args, cwd) {
|
|
64
|
+
const { bin, fullArgs } = buildCoreArgs(['init', ...args]);
|
|
65
|
+
console.log(`[SetupManager] spawning core: ${bin} ${fullArgs.join(' ')} (cwd=${cwd}) (SPECRAILS_CORE_BIN=${process.env.SPECRAILS_CORE_BIN ?? '<unset>'})`);
|
|
66
|
+
// M15: use the cross-spawn wrapper instead of `spawn(..., { shell: win32 })`.
|
|
67
|
+
// With shell:true, Node concatenates the argv into one cmd.exe command line and
|
|
68
|
+
// does NOT quote individual args, so `--from-config C:\Users\John Doe\...yaml`
|
|
69
|
+
// (and `--root-dir <path with spaces>`) split on the space and break install on
|
|
70
|
+
// any Windows account/project path containing a space. cross-spawn resolves the
|
|
71
|
+
// .cmd shim AND quotes each arg, so spaces (and newlines) survive intact.
|
|
72
|
+
return (0, win_spawn_1.spawnCli)(bin, fullArgs, {
|
|
73
|
+
cwd,
|
|
74
|
+
env: process.env,
|
|
75
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// H19: hard cap on the runtime probe. `npx --yes --prefer-online
|
|
79
|
+
// <CORE_PACKAGE_SPEC> version` does a network round-trip to the npm
|
|
80
|
+
// registry, and spawnSync blocks the single event loop — without a timeout a
|
|
81
|
+
// hung network froze the WHOLE app indefinitely during Add Project. On
|
|
82
|
+
// timeout the probe degrades to ok:false, which the install paths surface as
|
|
83
|
+
// a setup_error instead of hanging.
|
|
84
|
+
const CORE_PROBE_TIMEOUT_MS = 60_000;
|
|
85
|
+
function probeCoreRuntimeVersion(cwd) {
|
|
86
|
+
const { bin, fullArgs } = buildCoreArgs(['version']);
|
|
87
|
+
const result = (0, child_process_1.spawnSync)(bin, fullArgs, {
|
|
88
|
+
cwd,
|
|
89
|
+
env: process.env,
|
|
90
|
+
shell: process.platform === 'win32',
|
|
91
|
+
encoding: 'utf-8',
|
|
92
|
+
timeout: CORE_PROBE_TIMEOUT_MS,
|
|
93
|
+
});
|
|
94
|
+
if (result.error) {
|
|
95
|
+
const timedOut = result.error.code === 'ETIMEDOUT';
|
|
96
|
+
return {
|
|
97
|
+
ok: false,
|
|
98
|
+
bin,
|
|
99
|
+
error: timedOut
|
|
100
|
+
? `probe timed out after ${CORE_PROBE_TIMEOUT_MS / 1000}s — npm registry unreachable?`
|
|
101
|
+
: result.error.message,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
if ((result.status ?? 1) !== 0) {
|
|
105
|
+
const stderr = typeof result.stderr === 'string' ? result.stderr.trim() : '';
|
|
106
|
+
const stdout = typeof result.stdout === 'string' ? result.stdout.trim() : '';
|
|
107
|
+
return { ok: false, bin, error: stderr || stdout || `exit code ${result.status ?? 'unknown'}` };
|
|
108
|
+
}
|
|
109
|
+
const output = `${result.stdout ?? ''}`.trim();
|
|
110
|
+
const match = output.match(/\d+\.\d+\.\d+/);
|
|
111
|
+
if (!match) {
|
|
112
|
+
return { ok: false, bin, error: `could not parse version from output: ${output}` };
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, bin, version: match[0] };
|
|
115
|
+
}
|
|
116
|
+
// ─── YAML helpers ─────────────────────────────────────────────────────────────
|
|
117
|
+
function writeSpawnInstallConfig(projectId, yamlText) {
|
|
118
|
+
const tmpDir = (0, os_1.tmpdir)();
|
|
119
|
+
const tempPath = (0, path_1.join)(tmpDir, `specrails-desktop-install-config-${projectId}-${Date.now()}.yaml`);
|
|
120
|
+
(0, fs_1.writeFileSync)(tempPath, yamlText, 'utf-8');
|
|
121
|
+
return tempPath;
|
|
122
|
+
}
|
|
123
|
+
function readInstallConfig(projectPath) {
|
|
124
|
+
const configPath = (0, path_1.join)(projectPath, '.specrails', 'install-config.yaml');
|
|
125
|
+
try {
|
|
126
|
+
const text = (0, fs_1.readFileSync)(configPath, 'utf-8');
|
|
127
|
+
const tierMatch = text.match(/^tier:\s*(\w+)/m);
|
|
128
|
+
const tier = (tierMatch?.[1] === 'quick' ? 'quick' : 'full');
|
|
129
|
+
let selectedAgents = [];
|
|
130
|
+
// Inline format: selected: [a, b, c]
|
|
131
|
+
const inlineMatch = text.match(/selected:\s*\[([^\]]*)\]/);
|
|
132
|
+
if (inlineMatch) {
|
|
133
|
+
selectedAgents = inlineMatch[1].split(',').map((s) => s.trim()).filter(Boolean);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
// Multi-line format: selected:\n - a\n - b
|
|
137
|
+
const multilineMatch = text.match(/selected:\s*\n((?:\s+-\s+\S+\n?)+)/);
|
|
138
|
+
if (multilineMatch) {
|
|
139
|
+
selectedAgents = multilineMatch[1].match(/- (\S+)/g)?.map((m) => m.replace('- ', '')) ?? [];
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return { tier, selectedAgents };
|
|
143
|
+
}
|
|
144
|
+
catch {
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
// ─── Template deployment (post-install) ──────────────────────────────────────
|
|
149
|
+
function deployTemplates(projectPath, selectedAgents) {
|
|
150
|
+
const templatesDir = (0, path_1.join)(projectPath, '.specrails', 'setup-templates');
|
|
151
|
+
const targetDir = (0, path_1.join)(projectPath, SPECRAILS_DIR);
|
|
152
|
+
let agents = 0, commands = 0, personas = 0;
|
|
153
|
+
// Deploy selected agent templates
|
|
154
|
+
const agentTemplatesDir = (0, path_1.join)(templatesDir, 'agents');
|
|
155
|
+
const agentTargetDir = (0, path_1.join)(targetDir, 'agents');
|
|
156
|
+
if ((0, fs_1.existsSync)(agentTemplatesDir)) {
|
|
157
|
+
(0, fs_1.mkdirSync)(agentTargetDir, { recursive: true });
|
|
158
|
+
for (const file of (0, fs_1.readdirSync)(agentTemplatesDir)) {
|
|
159
|
+
if (!file.endsWith('.md'))
|
|
160
|
+
continue;
|
|
161
|
+
const agentId = file.replace(/\.md$/, '');
|
|
162
|
+
if (selectedAgents.length > 0 && !selectedAgents.includes(agentId))
|
|
163
|
+
continue;
|
|
164
|
+
(0, fs_1.copyFileSync)((0, path_1.join)(agentTemplatesDir, file), (0, path_1.join)(agentTargetDir, file));
|
|
165
|
+
agents++;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
// Deploy persona templates
|
|
169
|
+
const personaTemplatesDir = (0, path_1.join)(templatesDir, 'personas');
|
|
170
|
+
const personaTargetDir = (0, path_1.join)(agentTargetDir, 'personas');
|
|
171
|
+
if ((0, fs_1.existsSync)(personaTemplatesDir)) {
|
|
172
|
+
(0, fs_1.mkdirSync)(personaTargetDir, { recursive: true });
|
|
173
|
+
for (const file of (0, fs_1.readdirSync)(personaTemplatesDir)) {
|
|
174
|
+
if (!file.endsWith('.md'))
|
|
175
|
+
continue;
|
|
176
|
+
(0, fs_1.copyFileSync)((0, path_1.join)(personaTemplatesDir, file), (0, path_1.join)(personaTargetDir, file));
|
|
177
|
+
personas++;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
// Deploy command templates
|
|
181
|
+
const cmdTemplatesDir = (0, path_1.join)(templatesDir, 'commands', 'specrails');
|
|
182
|
+
const cmdTargetDir = (0, path_1.join)(targetDir, 'commands', 'specrails');
|
|
183
|
+
if ((0, fs_1.existsSync)(cmdTemplatesDir)) {
|
|
184
|
+
(0, fs_1.mkdirSync)(cmdTargetDir, { recursive: true });
|
|
185
|
+
for (const file of (0, fs_1.readdirSync)(cmdTemplatesDir)) {
|
|
186
|
+
if (!file.endsWith('.md'))
|
|
187
|
+
continue;
|
|
188
|
+
(0, fs_1.copyFileSync)((0, path_1.join)(cmdTemplatesDir, file), (0, path_1.join)(cmdTargetDir, file));
|
|
189
|
+
commands++;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
return { agents, commands, personas };
|
|
193
|
+
}
|
|
194
|
+
// Full install: 7-phase enrichment flow (claude /specrails:enrich)
|
|
195
|
+
exports.CHECKPOINTS = [
|
|
196
|
+
{ key: 'base_install', name: 'Base installation' },
|
|
197
|
+
{ key: 'repo_analysis', name: 'Repository analysis' },
|
|
198
|
+
{ key: 'stack_conventions', name: 'Stack & conventions' },
|
|
199
|
+
{ key: 'product_discovery', name: 'Product discovery' },
|
|
200
|
+
{ key: 'agent_generation', name: 'Agent generation' },
|
|
201
|
+
{ key: 'command_config', name: 'Command configuration' },
|
|
202
|
+
{ key: 'final_verification', name: 'Final verification' },
|
|
203
|
+
];
|
|
204
|
+
// Quick install: 3-phase non-interactive flow (npx init --from-config)
|
|
205
|
+
exports.QUICK_CHECKPOINTS = [
|
|
206
|
+
{ key: 'config_written', name: 'Config written' },
|
|
207
|
+
{ key: 'base_install', name: 'Base installation' },
|
|
208
|
+
{ key: 'quick_complete', name: 'Quick install complete' },
|
|
209
|
+
];
|
|
210
|
+
function checkFilesystem(projectPath) {
|
|
211
|
+
const dir = SPECRAILS_DIR;
|
|
212
|
+
const hasBaseInstall = (0, fs_1.existsSync)((0, path_1.join)(projectPath, '.specrails', 'specrails-version')) ||
|
|
213
|
+
(0, fs_1.existsSync)((0, path_1.join)(projectPath, '.specrails-version'));
|
|
214
|
+
const hasSetupTemplates = (0, fs_1.existsSync)((0, path_1.join)(projectPath, '.specrails', 'setup-templates')) ||
|
|
215
|
+
(0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'setup-templates'));
|
|
216
|
+
const hasRules = (0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'rules')) &&
|
|
217
|
+
hasFiles((0, path_1.join)(projectPath, dir, 'rules'), /\.md$/);
|
|
218
|
+
const hasPersonas = (0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'agents', 'personas')) &&
|
|
219
|
+
hasFiles((0, path_1.join)(projectPath, dir, 'agents', 'personas'), /\.md$/);
|
|
220
|
+
const hasAgents = (0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'agents')) &&
|
|
221
|
+
hasFiles((0, path_1.join)(projectPath, dir, 'agents'), /^sr-.*\.md$/);
|
|
222
|
+
const hasCommands = (((0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'commands', 'sr')) && hasFiles((0, path_1.join)(projectPath, dir, 'commands', 'sr'), /\.md$/)) ||
|
|
223
|
+
((0, fs_1.existsSync)((0, path_1.join)(projectPath, dir, 'commands', 'specrails')) && hasFiles((0, path_1.join)(projectPath, dir, 'commands', 'specrails'), /\.md$/)));
|
|
224
|
+
const hasCLAUDE = (0, fs_1.existsSync)((0, path_1.join)(projectPath, 'CLAUDE.md'));
|
|
225
|
+
return {
|
|
226
|
+
base_install: hasBaseInstall,
|
|
227
|
+
// repo_analysis: detected when setup templates exist and CLAUDE.md is written
|
|
228
|
+
// (Claude writes CLAUDE.md after analyzing the repo)
|
|
229
|
+
repo_analysis: hasBaseInstall && (hasCLAUDE || hasSetupTemplates),
|
|
230
|
+
// stack_conventions: detected when rules files are generated
|
|
231
|
+
stack_conventions: hasRules,
|
|
232
|
+
product_discovery: hasPersonas,
|
|
233
|
+
agent_generation: hasAgents,
|
|
234
|
+
command_config: hasCommands,
|
|
235
|
+
// Final verification: agents + commands must exist (manifest from install.sh is unreliable —
|
|
236
|
+
// it's created during scaffolding before /setup generates the actual artifacts)
|
|
237
|
+
final_verification: hasAgents && hasCommands,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
function hasFiles(dir, pattern) {
|
|
241
|
+
try {
|
|
242
|
+
return (0, fs_1.readdirSync)(dir).some((f) => pattern.test(f));
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
// ─── Enrich.md content resolver (shared by start + resume enrich paths) ─────
|
|
249
|
+
/**
|
|
250
|
+
* Reads the enrich command's body from the project's specrails dir, falling
|
|
251
|
+
* back across the three known locations:
|
|
252
|
+
* 1. `.claude/commands/sr/enrich.md` (modern; written by core ≥ 4.2)
|
|
253
|
+
* 2. `.claude/commands/specrails/enrich.md` (legacy; written by core 4.1.x)
|
|
254
|
+
* 3. `.claude/commands/setup.md` (very legacy; before enrich rename)
|
|
255
|
+
*
|
|
256
|
+
* For codex projects the .codex/skills/<name>/SKILL.md layout is read by the
|
|
257
|
+
* codex CLI directly when the slash command is forwarded — when the legacy
|
|
258
|
+
* codex flow needs the literal content (synthetic-session resume), the same
|
|
259
|
+
* .claude/ paths are consulted because specrails-core scaffolds both trees.
|
|
260
|
+
*
|
|
261
|
+
* Returns an empty string when no file is found; callers fall back to passing
|
|
262
|
+
* the literal slash command and let the CLI surface the missing-command error.
|
|
263
|
+
*/
|
|
264
|
+
function readEnrichMdContent(projectPath) {
|
|
265
|
+
const enrichMdPathSr = (0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'sr', 'enrich.md');
|
|
266
|
+
const enrichMdPathSpecrails = (0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'specrails', 'enrich.md');
|
|
267
|
+
const legacyMdPath = (0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'setup.md');
|
|
268
|
+
for (const p of [enrichMdPathSr, enrichMdPathSpecrails, legacyMdPath]) {
|
|
269
|
+
try {
|
|
270
|
+
return (0, fs_1.readFileSync)(p, 'utf-8');
|
|
271
|
+
}
|
|
272
|
+
catch { /* try next */ }
|
|
273
|
+
}
|
|
274
|
+
return '';
|
|
275
|
+
}
|
|
276
|
+
// ─── Stream-based checkpoint detection ───────────────────────────────────────
|
|
277
|
+
function detectCheckpointFromText(text) {
|
|
278
|
+
const hits = [];
|
|
279
|
+
// Match phase headers from Claude's /specrails:enrich output (and legacy /setup)
|
|
280
|
+
if (/phase\s*1|codebase\s*analysis|repository\s*analysis/i.test(text)) {
|
|
281
|
+
hits.push({ key: 'repo_analysis', detail: 'Analyzing codebase...' });
|
|
282
|
+
}
|
|
283
|
+
if (/phase\s*2|user\s*personas|product\s*discovery/i.test(text)) {
|
|
284
|
+
hits.push({ key: 'product_discovery', detail: 'Generating personas...' });
|
|
285
|
+
}
|
|
286
|
+
if (/phase\s*3|configuration|agent\s*selection|backlog\s*provider/i.test(text)) {
|
|
287
|
+
hits.push({ key: 'stack_conventions', detail: 'Configuring stack...' });
|
|
288
|
+
}
|
|
289
|
+
if (/generating\s*all\s*files|writing.*agent|sr-architect|sr-developer|sr-reviewer/i.test(text)) {
|
|
290
|
+
hits.push({ key: 'agent_generation', detail: 'Generating agents...' });
|
|
291
|
+
}
|
|
292
|
+
if (/command\s*selection|installing.*commands|\.claude\/commands\/(sr|specrails)/i.test(text)) {
|
|
293
|
+
hits.push({ key: 'command_config', detail: 'Configuring commands...' });
|
|
294
|
+
}
|
|
295
|
+
// TUI output patterns from specrails-core init --from-config.
|
|
296
|
+
// Covers both the retired bash installer (✓ config loaded, reading
|
|
297
|
+
// install-config.yaml) and the Node installer ≥ v4.2.0 (Loaded
|
|
298
|
+
// install config from <path>, Phase 1 / 2 / 3 step headers, final
|
|
299
|
+
// `init complete` sentinel).
|
|
300
|
+
if (/✓\s*config\s*loaded|reading.*install-config|loaded\s*install\s*config|from-config/i.test(text)) {
|
|
301
|
+
hits.push({ key: 'config_written' });
|
|
302
|
+
}
|
|
303
|
+
if (/installing\s*specrails|phase\s*2\s*&\s*3|placing\s*agents/i.test(text)) {
|
|
304
|
+
hits.push({ key: 'agent_generation', detail: 'Installing specrails artefacts...' });
|
|
305
|
+
}
|
|
306
|
+
if (/writing\s*manifest|wrote\s+.*specrails-manifest/i.test(text)) {
|
|
307
|
+
hits.push({ key: 'final_verification' });
|
|
308
|
+
}
|
|
309
|
+
if (/✓\s*installed|installation\s*complete|init\s*complete|update\s*complete/i.test(text)) {
|
|
310
|
+
hits.push({ key: 'quick_complete' });
|
|
311
|
+
}
|
|
312
|
+
// File path detection in tool_use events
|
|
313
|
+
if (text.includes('.specrails-version') || text.includes('specrails/specrails-version'))
|
|
314
|
+
hits.push({ key: 'base_install' });
|
|
315
|
+
if (text.includes('/agents/personas/') && text.includes('.md')) {
|
|
316
|
+
hits.push({ key: 'product_discovery', detail: 'Writing personas...' });
|
|
317
|
+
}
|
|
318
|
+
// Claude path: .claude/agents/sr-<name>.md
|
|
319
|
+
if (/\/agents\/sr-[^/]+\.md/.test(text)) {
|
|
320
|
+
hits.push({ key: 'agent_generation', detail: 'Writing agents...' });
|
|
321
|
+
}
|
|
322
|
+
// Codex path: .codex/skills/sr-<name>/SKILL.md (rail skills ship in
|
|
323
|
+
// specrails-core 4.6.0+ — see openspec/.../specs/setup-wizard… for the
|
|
324
|
+
// checkpoint protocol shared across providers).
|
|
325
|
+
if (/\.codex\/skills\/sr-[^/]+\/SKILL\.md/.test(text)) {
|
|
326
|
+
hits.push({ key: 'agent_generation', detail: 'Writing agent skills...' });
|
|
327
|
+
}
|
|
328
|
+
if ((text.includes('/commands/sr/') || text.includes('/commands/specrails/')) && text.includes('.md')) {
|
|
329
|
+
hits.push({ key: 'command_config', detail: 'Writing commands...' });
|
|
330
|
+
}
|
|
331
|
+
// Codex enrich/doctor skills (the non-rail commands) also indicate
|
|
332
|
+
// command_config progress.
|
|
333
|
+
if (/\.codex\/skills\/(enrich|doctor)\/SKILL\.md/.test(text)) {
|
|
334
|
+
hits.push({ key: 'command_config', detail: 'Writing codex command skills...' });
|
|
335
|
+
}
|
|
336
|
+
if (text.includes('/rules/') && text.includes('.md')) {
|
|
337
|
+
hits.push({ key: 'stack_conventions', detail: 'Writing conventions...' });
|
|
338
|
+
}
|
|
339
|
+
// Codex sandbox / approval policy lives inside .codex/config.toml
|
|
340
|
+
// (top-level `sandbox_mode` + `approval_policy` keys, per codex
|
|
341
|
+
// 0.128.0+). There is no separate Starlark rules file.
|
|
342
|
+
if (/\.codex\/config\.toml/.test(text)) {
|
|
343
|
+
hits.push({ key: 'stack_conventions', detail: 'Writing codex sandbox config...' });
|
|
344
|
+
}
|
|
345
|
+
if (text.includes('.specrails-manifest.json') || text.includes('specrails/specrails-manifest.json')) {
|
|
346
|
+
hits.push({ key: 'final_verification' });
|
|
347
|
+
}
|
|
348
|
+
return hits;
|
|
349
|
+
}
|
|
350
|
+
exports.EMPTY_SUMMARY = {
|
|
351
|
+
agents: 0,
|
|
352
|
+
specrailsCommands: 0,
|
|
353
|
+
opsxCommands: 0,
|
|
354
|
+
personas: 0,
|
|
355
|
+
legacySrRemoved: 0,
|
|
356
|
+
tier: 'quick',
|
|
357
|
+
provider: 'claude',
|
|
358
|
+
};
|
|
359
|
+
const MIN_NODE_NATIVE_CORE_VERSION = '4.1.0';
|
|
360
|
+
function compareSemver(a, b) {
|
|
361
|
+
const aParts = a.trim().split('.').map((n) => parseInt(n, 10));
|
|
362
|
+
const bParts = b.trim().split('.').map((n) => parseInt(n, 10));
|
|
363
|
+
if (aParts.length < 3 || bParts.length < 3)
|
|
364
|
+
return null;
|
|
365
|
+
if ([...aParts, ...bParts].some((n) => Number.isNaN(n)))
|
|
366
|
+
return null;
|
|
367
|
+
for (let i = 0; i < 3; i++) {
|
|
368
|
+
if (aParts[i] > bParts[i])
|
|
369
|
+
return 1;
|
|
370
|
+
if (aParts[i] < bParts[i])
|
|
371
|
+
return -1;
|
|
372
|
+
}
|
|
373
|
+
return 0;
|
|
374
|
+
}
|
|
375
|
+
function validateInstalledCore(projectPath) {
|
|
376
|
+
const reasons = [];
|
|
377
|
+
const versionCandidates = [
|
|
378
|
+
(0, path_1.join)(projectPath, '.specrails', 'specrails-version'),
|
|
379
|
+
(0, path_1.join)(projectPath, '.specrails-version'),
|
|
380
|
+
];
|
|
381
|
+
for (const candidate of versionCandidates) {
|
|
382
|
+
if (!(0, fs_1.existsSync)(candidate))
|
|
383
|
+
continue;
|
|
384
|
+
try {
|
|
385
|
+
const raw = (0, fs_1.readFileSync)(candidate, 'utf-8').trim();
|
|
386
|
+
const cmp = compareSemver(raw, MIN_NODE_NATIVE_CORE_VERSION);
|
|
387
|
+
if (cmp !== null && cmp < 0) {
|
|
388
|
+
reasons.push(`installed specrails-core version ${raw} is older than required ${MIN_NODE_NATIVE_CORE_VERSION}`);
|
|
389
|
+
}
|
|
390
|
+
break;
|
|
391
|
+
}
|
|
392
|
+
catch {
|
|
393
|
+
reasons.push(`failed to read installed specrails-core version from ${candidate}`);
|
|
394
|
+
break;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const legacyMarkers = [
|
|
398
|
+
{ path: (0, path_1.join)(projectPath, '.specrails', 'bin', 'doctor.sh'), reason: 'legacy bash doctor detected' },
|
|
399
|
+
{
|
|
400
|
+
path: (0, path_1.join)(projectPath, '.specrails', 'setup-templates', 'settings', 'integration-contract.json'),
|
|
401
|
+
reason: 'legacy integration-contract copy detected in setup-templates',
|
|
402
|
+
},
|
|
403
|
+
];
|
|
404
|
+
for (const marker of legacyMarkers) {
|
|
405
|
+
if ((0, fs_1.existsSync)(marker.path))
|
|
406
|
+
reasons.push(marker.reason);
|
|
407
|
+
}
|
|
408
|
+
return { ok: reasons.length === 0, reasons };
|
|
409
|
+
}
|
|
410
|
+
function formatLegacyInstallError(reasons) {
|
|
411
|
+
return [
|
|
412
|
+
'Installed specrails-core is legacy; expected the Node-native installer.',
|
|
413
|
+
'',
|
|
414
|
+
...reasons.map((reason) => `- ${reason}`),
|
|
415
|
+
].join('\n');
|
|
416
|
+
}
|
|
417
|
+
function computeSummary(projectPath, tier, provider = 'claude') {
|
|
418
|
+
let agents = 0;
|
|
419
|
+
let personas = 0;
|
|
420
|
+
let specrailsCommands = 0;
|
|
421
|
+
let opsxCommands = 0;
|
|
422
|
+
try {
|
|
423
|
+
if (provider === 'codex') {
|
|
424
|
+
// Codex layout: every artefact ships as a SKILL under `.codex/skills/`.
|
|
425
|
+
// - agents = rail personas (`skills/rails/sr-*/SKILL.md`) + orchestrator
|
|
426
|
+
// skills at the root with an `sr-` prefix (sr-implement,
|
|
427
|
+
// sr-batch-implement, …)
|
|
428
|
+
// - opsxCommands = `skills/openspec-*/SKILL.md`
|
|
429
|
+
// - specrailsCommands = everything else under `skills/` (ported claude
|
|
430
|
+
// slash commands like propose-spec, explore-spec, retry, doctor,
|
|
431
|
+
// enrich, vpc-drift, …)
|
|
432
|
+
// - personas = 0 today; codex VPC pass not implemented yet.
|
|
433
|
+
const skillsDir = (0, path_1.join)(projectPath, '.codex', 'skills');
|
|
434
|
+
if ((0, fs_1.existsSync)(skillsDir)) {
|
|
435
|
+
// Rails (always counted as agents).
|
|
436
|
+
const railsDir = (0, path_1.join)(skillsDir, 'rails');
|
|
437
|
+
if ((0, fs_1.existsSync)(railsDir)) {
|
|
438
|
+
for (const entry of (0, fs_1.readdirSync)(railsDir)) {
|
|
439
|
+
if ((0, fs_1.existsSync)((0, path_1.join)(railsDir, entry, 'SKILL.md')))
|
|
440
|
+
agents++;
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
// Top-level skill dirs.
|
|
444
|
+
for (const entry of (0, fs_1.readdirSync)(skillsDir)) {
|
|
445
|
+
if (entry === 'rails')
|
|
446
|
+
continue;
|
|
447
|
+
if (!(0, fs_1.existsSync)((0, path_1.join)(skillsDir, entry, 'SKILL.md')))
|
|
448
|
+
continue;
|
|
449
|
+
if (/^sr-/.test(entry))
|
|
450
|
+
agents++;
|
|
451
|
+
else if (/^openspec-/.test(entry))
|
|
452
|
+
opsxCommands++;
|
|
453
|
+
else
|
|
454
|
+
specrailsCommands++;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
else {
|
|
459
|
+
// Claude layout (unchanged).
|
|
460
|
+
const dir = SPECRAILS_DIR;
|
|
461
|
+
const agentsDir = (0, path_1.join)(projectPath, dir, 'agents');
|
|
462
|
+
if ((0, fs_1.existsSync)(agentsDir)) {
|
|
463
|
+
const files = (0, fs_1.readdirSync)(agentsDir);
|
|
464
|
+
agents = files.filter((f) => /^sr-.*\.md$/.test(f)).length;
|
|
465
|
+
const personasDir = (0, path_1.join)(agentsDir, 'personas');
|
|
466
|
+
if ((0, fs_1.existsSync)(personasDir)) {
|
|
467
|
+
personas = (0, fs_1.readdirSync)(personasDir).filter((f) => f.endsWith('.md')).length;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
const commandsDirSpecrails = (0, path_1.join)(projectPath, dir, 'commands', 'specrails');
|
|
471
|
+
const commandsDirOpsx = (0, path_1.join)(projectPath, dir, 'commands', 'opsx');
|
|
472
|
+
if ((0, fs_1.existsSync)(commandsDirSpecrails)) {
|
|
473
|
+
specrailsCommands = (0, fs_1.readdirSync)(commandsDirSpecrails).filter((f) => f.endsWith('.md')).length;
|
|
474
|
+
}
|
|
475
|
+
if ((0, fs_1.existsSync)(commandsDirOpsx)) {
|
|
476
|
+
opsxCommands = (0, fs_1.readdirSync)(commandsDirOpsx).filter((f) => f.endsWith('.md')).length;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
catch {
|
|
481
|
+
// non-fatal
|
|
482
|
+
}
|
|
483
|
+
return { agents, specrailsCommands, opsxCommands, personas, legacySrRemoved: 0, tier, provider };
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* Deletes the deprecated `.claude/commands/sr/` directory (if present) and returns
|
|
487
|
+
* the number of `.md` files that were removed. Safe to call even if the directory
|
|
488
|
+
* does not exist. Never throws — errors are logged at info level.
|
|
489
|
+
*/
|
|
490
|
+
function sweepLegacySrCommands(projectPath) {
|
|
491
|
+
const srDir = (0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'sr');
|
|
492
|
+
try {
|
|
493
|
+
if (!(0, fs_1.existsSync)(srDir))
|
|
494
|
+
return 0;
|
|
495
|
+
const files = (0, fs_1.readdirSync)(srDir).filter((f) => f.endsWith('.md'));
|
|
496
|
+
const count = files.length;
|
|
497
|
+
(0, fs_1.rmSync)(srDir, { recursive: true, force: true });
|
|
498
|
+
console.info(`[SetupManager] Swept ${count} legacy /specrails:* command(s) from ${srDir}`);
|
|
499
|
+
return count;
|
|
500
|
+
}
|
|
501
|
+
catch (err) {
|
|
502
|
+
console.info(`[SetupManager] sweepLegacySrCommands failed (non-fatal): ${err}`);
|
|
503
|
+
return 0;
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
// ─── Core contract validation ────────────────────────────────────────────────
|
|
507
|
+
async function validateCoreContract() {
|
|
508
|
+
const contractPath = await (0, core_compat_1.findCoreContract)();
|
|
509
|
+
if (!contractPath) {
|
|
510
|
+
// specrails-core does not yet ship integration-contract.json (planned in RFC-003).
|
|
511
|
+
// Fall back silently to runtime defaults — Specrails works fine without the contract.
|
|
512
|
+
console.debug('[Specrails] integration-contract.json not found — using runtime defaults');
|
|
513
|
+
return;
|
|
514
|
+
}
|
|
515
|
+
let contract;
|
|
516
|
+
try {
|
|
517
|
+
const raw = require('fs').readFileSync(contractPath, 'utf-8');
|
|
518
|
+
contract = JSON.parse(raw);
|
|
519
|
+
}
|
|
520
|
+
catch {
|
|
521
|
+
console.debug('[Specrails] integration-contract.json failed to parse — using runtime defaults');
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
if (contract.checkpoints) {
|
|
525
|
+
const missingCheckpoints = contract.checkpoints.filter((c) => !exports.CHECKPOINTS.some((cp) => cp.key === c));
|
|
526
|
+
const extraCheckpoints = exports.CHECKPOINTS
|
|
527
|
+
.filter((cp) => !contract.checkpoints.includes(cp.key))
|
|
528
|
+
.map((cp) => cp.key);
|
|
529
|
+
if (missingCheckpoints.length > 0 || extraCheckpoints.length > 0) {
|
|
530
|
+
console.warn('[Specrails] ⚠️ specrails-core contract checkpoint mismatch:');
|
|
531
|
+
if (missingCheckpoints.length > 0)
|
|
532
|
+
console.warn(` Checkpoints in Core but not in the app: ${missingCheckpoints.join(', ')}`);
|
|
533
|
+
if (extraCheckpoints.length > 0)
|
|
534
|
+
console.warn(` Checkpoints in the app but not in Core: ${extraCheckpoints.join(', ')}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
// ─── SetupManager ─────────────────────────────────────────────────────────────
|
|
539
|
+
const INSTALL_LOG_BUFFER_MAX = 2000;
|
|
540
|
+
function formatBufferedInstallError(baseMessage, logBuffer) {
|
|
541
|
+
const recentLines = logBuffer
|
|
542
|
+
.map((line) => line.trim())
|
|
543
|
+
.filter(Boolean)
|
|
544
|
+
.slice(-8);
|
|
545
|
+
if (recentLines.length === 0)
|
|
546
|
+
return baseMessage;
|
|
547
|
+
return [
|
|
548
|
+
baseMessage,
|
|
549
|
+
'',
|
|
550
|
+
'Recent output:',
|
|
551
|
+
...recentLines.map((line) => `- ${line}`),
|
|
552
|
+
].join('\n');
|
|
553
|
+
}
|
|
554
|
+
class SetupManager {
|
|
555
|
+
_broadcast;
|
|
556
|
+
_onSessionCaptured;
|
|
557
|
+
_onSetupDone;
|
|
558
|
+
// Map from projectId → active child processes
|
|
559
|
+
_installProcesses;
|
|
560
|
+
_setupProcesses;
|
|
561
|
+
// Track checkpoint states per project
|
|
562
|
+
_checkpoints;
|
|
563
|
+
// Track checkpoint start times
|
|
564
|
+
_checkpointStart;
|
|
565
|
+
// Ring buffer for install log lines — allows clients to recover log on reconnect
|
|
566
|
+
_installLogBuffer;
|
|
567
|
+
// Track each project's chosen AI provider for binary selection
|
|
568
|
+
_projectProviders;
|
|
569
|
+
// Track each project's install tier (quick vs full)
|
|
570
|
+
_projectTiers;
|
|
571
|
+
// Track project names for codex context header injection
|
|
572
|
+
_projectNames;
|
|
573
|
+
constructor(broadcast, onSessionCaptured, onSetupDone) {
|
|
574
|
+
this._broadcast = broadcast;
|
|
575
|
+
this._onSessionCaptured = onSessionCaptured;
|
|
576
|
+
this._onSetupDone = onSetupDone;
|
|
577
|
+
this._installProcesses = new Map();
|
|
578
|
+
this._setupProcesses = new Map();
|
|
579
|
+
this._checkpoints = new Map();
|
|
580
|
+
this._checkpointStart = new Map();
|
|
581
|
+
this._pollTimers = new Map();
|
|
582
|
+
this._installLogBuffer = new Map();
|
|
583
|
+
this._projectProviders = new Map();
|
|
584
|
+
this._projectTiers = new Map();
|
|
585
|
+
this._projectNames = new Map();
|
|
586
|
+
}
|
|
587
|
+
// ─── Full Install: TUI installer (npx specrails-core) ────────────────────────
|
|
588
|
+
startInstall(projectId, projectPath) {
|
|
589
|
+
if (this._installProcesses.has(projectId)) {
|
|
590
|
+
console.warn(`[SetupManager] install already running for ${projectId}`);
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
const configPath = (0, path_1.join)(projectPath, '.specrails', 'install-config.yaml');
|
|
594
|
+
const hasConfig = (0, fs_1.existsSync)(configPath);
|
|
595
|
+
const parsedConfig = hasConfig ? readInstallConfig(projectPath) : null;
|
|
596
|
+
const tier = parsedConfig?.tier ?? 'full';
|
|
597
|
+
this._projectTiers.set(projectId, tier);
|
|
598
|
+
// Pull provider out of the just-written install-config.yaml so the
|
|
599
|
+
// completion-summary path can label tiles correctly (codex → "Skills"
|
|
600
|
+
// etc.). Without this, summary.provider stays undefined and the client
|
|
601
|
+
// renders the claude labels with 0/0/0 counts because the codex skill
|
|
602
|
+
// walker never gets selected.
|
|
603
|
+
if (hasConfig) {
|
|
604
|
+
try {
|
|
605
|
+
const text = (0, fs_1.readFileSync)(configPath, 'utf-8');
|
|
606
|
+
const m = text.match(/^provider:\s*(\w+)/m);
|
|
607
|
+
if (m && (m[1] === 'claude' || m[1] === 'codex')) {
|
|
608
|
+
this._projectProviders.set(projectId, m[1]);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
catch {
|
|
612
|
+
// Ignore — falls back to claude default downstream.
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
this._initCheckpoints(projectId);
|
|
616
|
+
const missingPrerequisites = (0, setup_prerequisites_1.formatMissingSetupPrerequisites)();
|
|
617
|
+
if (missingPrerequisites) {
|
|
618
|
+
this._broadcast({
|
|
619
|
+
type: 'setup_error',
|
|
620
|
+
projectId,
|
|
621
|
+
error: missingPrerequisites,
|
|
622
|
+
});
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
const probe = probeCoreRuntimeVersion(projectPath);
|
|
626
|
+
if (!probe.ok) {
|
|
627
|
+
this._broadcast({
|
|
628
|
+
type: 'setup_error',
|
|
629
|
+
projectId,
|
|
630
|
+
error: `Failed to verify specrails-core runtime before install: ${probe.error ?? 'unknown error'}`,
|
|
631
|
+
});
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
console.log(`[SetupManager] core runtime probe: ${probe.bin} -> ${probe.version}`);
|
|
635
|
+
const probeCmp = compareSemver(probe.version, MIN_NODE_NATIVE_CORE_VERSION);
|
|
636
|
+
if (probeCmp !== null && probeCmp < 0) {
|
|
637
|
+
this._broadcast({
|
|
638
|
+
type: 'setup_error',
|
|
639
|
+
projectId,
|
|
640
|
+
error: `Resolved specrails-core@${probe.version} is legacy; expected Node-native >= ${MIN_NODE_NATIVE_CORE_VERSION}.`,
|
|
641
|
+
});
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
644
|
+
let spawnConfigPath = null;
|
|
645
|
+
if (hasConfig) {
|
|
646
|
+
try {
|
|
647
|
+
spawnConfigPath = writeSpawnInstallConfig(projectId, (0, fs_1.readFileSync)(configPath, 'utf-8'));
|
|
648
|
+
}
|
|
649
|
+
catch (err) {
|
|
650
|
+
console.warn(`[SetupManager] Failed to write temp install-config.yaml: ${err}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
const initArgs = hasConfig
|
|
654
|
+
? ['--yes', '--from-config', spawnConfigPath ?? configPath]
|
|
655
|
+
: ['--yes', '--root-dir', projectPath];
|
|
656
|
+
const child = spawnCoreInit(initArgs, projectPath);
|
|
657
|
+
this._installProcesses.set(projectId, child);
|
|
658
|
+
this._installLogBuffer.set(projectId, []);
|
|
659
|
+
// spawnCoreInit uses shell:false on POSIX, so a spawn failure emits 'error'
|
|
660
|
+
// (and NOT 'close') — without this handler the temp config file leaks and
|
|
661
|
+
// the unhandled 'error' event would crash the app.
|
|
662
|
+
/* c8 ignore start -- spawn-failure path; exercised manually, not in CI */
|
|
663
|
+
child.on('error', (err) => {
|
|
664
|
+
console.error(`[SetupManager] core spawn failed for ${projectId}: ${err.message}`);
|
|
665
|
+
this._installProcesses.delete(projectId);
|
|
666
|
+
if (spawnConfigPath) {
|
|
667
|
+
try {
|
|
668
|
+
(0, fs_1.rmSync)(spawnConfigPath, { force: true });
|
|
669
|
+
}
|
|
670
|
+
catch { /* non-fatal */ }
|
|
671
|
+
}
|
|
672
|
+
this._broadcast({
|
|
673
|
+
type: 'setup_error',
|
|
674
|
+
projectId,
|
|
675
|
+
error: `Failed to launch specrails-core: ${err.message}`,
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
/* c8 ignore stop */
|
|
679
|
+
const stdoutReader = (0, readline_1.createInterface)({ input: child.stdout, crlfDelay: Infinity });
|
|
680
|
+
const stderrReader = (0, readline_1.createInterface)({ input: child.stderr, crlfDelay: Infinity });
|
|
681
|
+
const appendLog = (line) => {
|
|
682
|
+
const buf = this._installLogBuffer.get(projectId) ?? [];
|
|
683
|
+
buf.push(line);
|
|
684
|
+
if (buf.length > INSTALL_LOG_BUFFER_MAX)
|
|
685
|
+
buf.splice(0, buf.length - INSTALL_LOG_BUFFER_MAX);
|
|
686
|
+
this._installLogBuffer.set(projectId, buf);
|
|
687
|
+
};
|
|
688
|
+
stdoutReader.on('line', (line) => {
|
|
689
|
+
appendLog(line);
|
|
690
|
+
this._broadcast({ type: 'setup_log', projectId, line, stream: 'stdout' });
|
|
691
|
+
const hits = detectCheckpointFromText(line);
|
|
692
|
+
for (const hit of hits) {
|
|
693
|
+
this._advanceCheckpoint(projectId, hit.key, hit.detail);
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
stderrReader.on('line', (line) => {
|
|
697
|
+
appendLog(line);
|
|
698
|
+
this._broadcast({ type: 'setup_log', projectId, line, stream: 'stderr' });
|
|
699
|
+
});
|
|
700
|
+
child.on('close', (code) => {
|
|
701
|
+
if (spawnConfigPath) {
|
|
702
|
+
try {
|
|
703
|
+
(0, fs_1.rmSync)(spawnConfigPath, { force: true });
|
|
704
|
+
}
|
|
705
|
+
catch { /* non-fatal */ }
|
|
706
|
+
}
|
|
707
|
+
this._installProcesses.delete(projectId);
|
|
708
|
+
if (code === 0) {
|
|
709
|
+
const validation = validateInstalledCore(projectPath);
|
|
710
|
+
if (!validation.ok) {
|
|
711
|
+
this._broadcast({
|
|
712
|
+
type: 'setup_error',
|
|
713
|
+
projectId,
|
|
714
|
+
error: formatLegacyInstallError(validation.reasons),
|
|
715
|
+
});
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
this._advanceCheckpoint(projectId, 'base_install');
|
|
719
|
+
this._completeCheckpoint(projectId, 'base_install');
|
|
720
|
+
const legacySrRemoved = sweepLegacySrCommands(projectPath);
|
|
721
|
+
const summary = { ...computeSummary(projectPath, tier, this._projectProviders.get(projectId) ?? 'claude'), legacySrRemoved };
|
|
722
|
+
this._broadcast({
|
|
723
|
+
type: 'setup_install_done',
|
|
724
|
+
projectId,
|
|
725
|
+
timestamp: new Date().toISOString(),
|
|
726
|
+
summary,
|
|
727
|
+
});
|
|
728
|
+
validateCoreContract().catch(() => { });
|
|
729
|
+
}
|
|
730
|
+
else {
|
|
731
|
+
const logBuffer = this._installLogBuffer.get(projectId) ?? [];
|
|
732
|
+
this._broadcast({
|
|
733
|
+
type: 'setup_error',
|
|
734
|
+
projectId,
|
|
735
|
+
error: formatBufferedInstallError(`npx specrails-core exited with code ${code ?? 'unknown'}`, logBuffer),
|
|
736
|
+
});
|
|
737
|
+
}
|
|
738
|
+
});
|
|
739
|
+
}
|
|
740
|
+
// ─── Enrich: claude -p "/specrails:enrich --from-config" ────────────────────
|
|
741
|
+
startEnrich(projectId, projectPath, provider, projectName) {
|
|
742
|
+
if (this._setupProcesses.has(projectId)) {
|
|
743
|
+
console.warn(`[SetupManager] enrich already running for ${projectId}`);
|
|
744
|
+
return;
|
|
745
|
+
}
|
|
746
|
+
if (provider)
|
|
747
|
+
this._projectProviders.set(projectId, provider);
|
|
748
|
+
if (projectName)
|
|
749
|
+
this._projectNames.set(projectId, projectName);
|
|
750
|
+
this._projectTiers.set(projectId, 'full');
|
|
751
|
+
this._initCheckpoints(projectId);
|
|
752
|
+
// Pre-create the directory structure that /specrails:enrich will write to.
|
|
753
|
+
// Claude Code's Write tool does not create parent directories automatically —
|
|
754
|
+
// if a target directory doesn't exist the write fails and Claude reports a
|
|
755
|
+
// misleading "write permissions aren't enabled" error. Creating the dirs
|
|
756
|
+
// here ensures enrich runs transparently without any user intervention.
|
|
757
|
+
try {
|
|
758
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(projectPath, SPECRAILS_DIR, 'agents', 'personas'), { recursive: true });
|
|
759
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'sr'), { recursive: true });
|
|
760
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'specrails'), { recursive: true });
|
|
761
|
+
(0, fs_1.mkdirSync)((0, path_1.join)(projectPath, SPECRAILS_DIR, 'rules'), { recursive: true });
|
|
762
|
+
}
|
|
763
|
+
catch (err) {
|
|
764
|
+
console.warn(`[SetupManager] Failed to pre-create enrich directories: ${err}`);
|
|
765
|
+
}
|
|
766
|
+
const configPath = (0, path_1.join)(projectPath, '.specrails', 'install-config.yaml');
|
|
767
|
+
const hasConfig = (0, fs_1.existsSync)(configPath);
|
|
768
|
+
const enrichCmd = hasConfig ? '/specrails:enrich --from-config' : '/specrails:enrich';
|
|
769
|
+
this._spawnSetupWithAdapter(projectId, projectPath, {
|
|
770
|
+
action: 'setup-enrich',
|
|
771
|
+
prompt: enrichCmd,
|
|
772
|
+
provider,
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
/** @deprecated Use startEnrich() instead */
|
|
776
|
+
startSetup(projectId, projectPath, provider) {
|
|
777
|
+
return this.startEnrich(projectId, projectPath, provider);
|
|
778
|
+
}
|
|
779
|
+
resumeEnrich(projectId, projectPath, sessionId, userMessage, provider) {
|
|
780
|
+
if (this._setupProcesses.has(projectId)) {
|
|
781
|
+
console.warn(`[SetupManager] enrich already running for ${projectId}`);
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
if (provider)
|
|
785
|
+
this._projectProviders.set(projectId, provider);
|
|
786
|
+
const resolvedProvider = (provider ?? this._projectProviders.get(projectId));
|
|
787
|
+
const adapter = (0, providers_1.getAdapter)(resolvedProvider ?? 'claude');
|
|
788
|
+
// Synthetic codex session ids (from before §10) can't be resumed against
|
|
789
|
+
// a real codex thread — detect and fall back to a fresh enrich respawn
|
|
790
|
+
// that folds enrich.md content + the user reply, matching the legacy UX.
|
|
791
|
+
const isSyntheticSession = sessionId.startsWith('codex-') && !/^[0-9a-f]{8}-[0-9a-f]{4}/i.test(sessionId);
|
|
792
|
+
if (adapter.id === 'codex' && isSyntheticSession) {
|
|
793
|
+
const enrichContent = readEnrichMdContent(projectPath);
|
|
794
|
+
const prompt = enrichContent
|
|
795
|
+
? `${enrichContent}\n\n---\nIMPORTANT: This is a continuation of a previous enrich run. Check which artifacts already exist in the project before regenerating anything. The user responded to your question with:\n\n${userMessage}`
|
|
796
|
+
: userMessage;
|
|
797
|
+
this._spawnSetupWithAdapter(projectId, projectPath, {
|
|
798
|
+
action: 'setup-enrich',
|
|
799
|
+
prompt,
|
|
800
|
+
provider: resolvedProvider,
|
|
801
|
+
});
|
|
802
|
+
return;
|
|
803
|
+
}
|
|
804
|
+
// Modern path: real session id (claude or post-§10 codex) — use the
|
|
805
|
+
// adapter's resume action.
|
|
806
|
+
this._spawnSetupWithAdapter(projectId, projectPath, {
|
|
807
|
+
action: 'setup-enrich-resume',
|
|
808
|
+
prompt: userMessage,
|
|
809
|
+
sessionId,
|
|
810
|
+
provider: resolvedProvider,
|
|
811
|
+
});
|
|
812
|
+
}
|
|
813
|
+
/** @deprecated Use resumeEnrich() instead */
|
|
814
|
+
resumeSetup(projectId, projectPath, sessionId, userMessage, provider) {
|
|
815
|
+
return this.resumeEnrich(projectId, projectPath, sessionId, userMessage, provider);
|
|
816
|
+
}
|
|
817
|
+
// Active filesystem poll timers per project
|
|
818
|
+
_pollTimers;
|
|
819
|
+
_startFilesystemPoll(projectId, projectPath) {
|
|
820
|
+
this._stopFilesystemPoll(projectId);
|
|
821
|
+
const timer = setInterval(() => {
|
|
822
|
+
this._syncFilesystemCheckpoints(projectId, projectPath);
|
|
823
|
+
}, 3000);
|
|
824
|
+
this._pollTimers.set(projectId, timer);
|
|
825
|
+
}
|
|
826
|
+
_stopFilesystemPoll(projectId) {
|
|
827
|
+
const timer = this._pollTimers.get(projectId);
|
|
828
|
+
if (timer) {
|
|
829
|
+
clearInterval(timer);
|
|
830
|
+
this._pollTimers.delete(projectId);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Adapter-driven enrich spawn. Provider-aware prompt resolution
|
|
835
|
+
* (slash command for claude vs file-content fold for codex), real
|
|
836
|
+
* thread_id capture from `session-started` events (no more synthetic
|
|
837
|
+
* `codex-<id>-<ts>` ids), uniform stream parsing via
|
|
838
|
+
* `adapter.parseStreamLine`.
|
|
839
|
+
*/
|
|
840
|
+
_spawnSetupWithAdapter(projectId, projectPath, opts) {
|
|
841
|
+
const resolvedProvider = opts.provider ?? (0, core_compat_1.detectCLISync)();
|
|
842
|
+
if (resolvedProvider === null) {
|
|
843
|
+
console.warn('[SetupManager] No AI CLI detected. Falling back to claude.');
|
|
844
|
+
}
|
|
845
|
+
const adapter = (0, providers_1.getAdapter)(resolvedProvider ?? 'claude');
|
|
846
|
+
// Provider-aware prompt resolution:
|
|
847
|
+
// - claude: pass the slash command unresolved so the CLI looks up
|
|
848
|
+
// `.claude/commands/specrails/enrich.md` natively. Honours the
|
|
849
|
+
// skills-resolution priority over CLAUDE.md.
|
|
850
|
+
// - codex: no slash-command support; fold the enrich.md content into
|
|
851
|
+
// the prompt with the PROJECT context header so codex knows the cwd.
|
|
852
|
+
let effectivePrompt = opts.prompt;
|
|
853
|
+
if (!adapter.capabilities.systemPromptArg &&
|
|
854
|
+
(opts.prompt === '/specrails:enrich' || opts.prompt === '/specrails:enrich --from-config')) {
|
|
855
|
+
const enrichContent = readEnrichMdContent(projectPath);
|
|
856
|
+
if (enrichContent) {
|
|
857
|
+
const projectName = this._projectNames.get(projectId);
|
|
858
|
+
effectivePrompt = projectName
|
|
859
|
+
? `PROJECT: ${projectName}\nCWD: ${projectPath}\n\n---\n\n${enrichContent}`
|
|
860
|
+
: enrichContent;
|
|
861
|
+
}
|
|
862
|
+
else {
|
|
863
|
+
console.warn(`[SetupManager] Could not read enrich.md or setup.md — falling back to literal prompt`);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const args = adapter.buildArgs(opts.action, {
|
|
867
|
+
prompt: effectivePrompt,
|
|
868
|
+
model: adapter.defaultModel(),
|
|
869
|
+
sessionId: opts.sessionId,
|
|
870
|
+
});
|
|
871
|
+
// No OTEL env injection here — SetupManager spawns drive the initial project
|
|
872
|
+
// setup wizard, not repeatable pipeline jobs. Telemetry is scoped to
|
|
873
|
+
// QueueManager pipeline runs only.
|
|
874
|
+
const child = (0, cli_prompt_1.spawnAiCli)(adapter.binary, args, {
|
|
875
|
+
cwd: projectPath,
|
|
876
|
+
env: process.env,
|
|
877
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
878
|
+
});
|
|
879
|
+
this._setupProcesses.set(projectId, child);
|
|
880
|
+
/* c8 ignore start -- spawn-failure path; exercised manually, not in CI */
|
|
881
|
+
child.on('error', (err) => {
|
|
882
|
+
console.error(`[SetupManager] ${adapter.binary} spawn failed for ${projectId}: ${err.message}`);
|
|
883
|
+
this._setupProcesses.delete(projectId);
|
|
884
|
+
this._stopFilesystemPoll(projectId);
|
|
885
|
+
this._broadcast({
|
|
886
|
+
type: 'setup_error',
|
|
887
|
+
projectId,
|
|
888
|
+
error: `Failed to launch ${adapter.binary}: ${err.message}`,
|
|
889
|
+
});
|
|
890
|
+
});
|
|
891
|
+
/* c8 ignore stop */
|
|
892
|
+
// Start periodic filesystem polling for checkpoint detection
|
|
893
|
+
this._startFilesystemPoll(projectId, projectPath);
|
|
894
|
+
let capturedSessionId = null;
|
|
895
|
+
const stdoutReader = (0, readline_1.createInterface)({ input: child.stdout, crlfDelay: Infinity });
|
|
896
|
+
const stderrReader = (0, readline_1.createInterface)({ input: child.stderr, crlfDelay: Infinity });
|
|
897
|
+
stdoutReader.on('line', (line) => {
|
|
898
|
+
const ev = adapter.parseStreamLine(line);
|
|
899
|
+
if (!ev) {
|
|
900
|
+
// Non-parseable line — emit as raw log.
|
|
901
|
+
if (line)
|
|
902
|
+
this._broadcast({ type: 'setup_log', projectId, line, stream: 'stdout' });
|
|
903
|
+
return;
|
|
904
|
+
}
|
|
905
|
+
switch (ev.kind) {
|
|
906
|
+
case 'session-started': {
|
|
907
|
+
if (!capturedSessionId) {
|
|
908
|
+
capturedSessionId = ev.sessionId;
|
|
909
|
+
this._onSessionCaptured?.(projectId, ev.sessionId);
|
|
910
|
+
}
|
|
911
|
+
break;
|
|
912
|
+
}
|
|
913
|
+
case 'text-delta': {
|
|
914
|
+
// Run checkpoint detection over the assistant text and surface it
|
|
915
|
+
// both to the collapsible log viewer and the wizard chat panel.
|
|
916
|
+
this._broadcast({ type: 'setup_log', projectId, line: ev.text, stream: 'stdout' });
|
|
917
|
+
this._broadcast({ type: 'setup_chat', projectId, text: ev.text, role: 'assistant' });
|
|
918
|
+
const hits = detectCheckpointFromText(ev.text);
|
|
919
|
+
for (const hit of hits) {
|
|
920
|
+
this._advanceCheckpoint(projectId, hit.key, hit.detail);
|
|
921
|
+
}
|
|
922
|
+
// Also sync filesystem (cheap mtime/exists checks, see helper).
|
|
923
|
+
this._syncFilesystemCheckpoints(projectId, projectPath);
|
|
924
|
+
break;
|
|
925
|
+
}
|
|
926
|
+
case 'tool-use': {
|
|
927
|
+
this._broadcast({ type: 'setup_log', projectId, line: `[tool] ${ev.name}`, stream: 'stdout' });
|
|
928
|
+
// Tool inputs commonly mention the paths being written — feed the
|
|
929
|
+
// input preview into the checkpoint detector so writes to
|
|
930
|
+
// .claude/agents/sr-*.md or .codex/skills/sr-*/SKILL.md advance
|
|
931
|
+
// the checkpoint state immediately.
|
|
932
|
+
const hits = detectCheckpointFromText(ev.inputPreview);
|
|
933
|
+
for (const hit of hits) {
|
|
934
|
+
this._advanceCheckpoint(projectId, hit.key, hit.detail);
|
|
935
|
+
}
|
|
936
|
+
break;
|
|
937
|
+
}
|
|
938
|
+
case 'result': {
|
|
939
|
+
// Claude's `result` event also carries session_id; use it as a
|
|
940
|
+
// backstop if `session-started` wasn't observed earlier.
|
|
941
|
+
const sid = ev.payload.session_id;
|
|
942
|
+
if (sid && !capturedSessionId) {
|
|
943
|
+
capturedSessionId = sid;
|
|
944
|
+
this._onSessionCaptured?.(projectId, sid);
|
|
945
|
+
}
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
case 'other':
|
|
949
|
+
// Other event types (system progress markers) — already broadcast
|
|
950
|
+
// implicitly through the line itself if useful. Skip.
|
|
951
|
+
break;
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
stderrReader.on('line', (line) => {
|
|
955
|
+
this._broadcast({ type: 'setup_log', projectId, line, stream: 'stderr' });
|
|
956
|
+
});
|
|
957
|
+
child.on('close', (code) => {
|
|
958
|
+
this._setupProcesses.delete(projectId);
|
|
959
|
+
this._stopFilesystemPoll(projectId);
|
|
960
|
+
// Final filesystem sync
|
|
961
|
+
this._syncFilesystemCheckpoints(projectId, projectPath);
|
|
962
|
+
if (code === 0) {
|
|
963
|
+
// Sync filesystem checkpoints
|
|
964
|
+
this._syncFilesystemCheckpoints(projectId, projectPath);
|
|
965
|
+
// Check if setup is truly complete — real artifacts must exist
|
|
966
|
+
const hasAgents = (0, fs_1.existsSync)((0, path_1.join)(projectPath, SPECRAILS_DIR, 'agents')) &&
|
|
967
|
+
hasFiles((0, path_1.join)(projectPath, SPECRAILS_DIR, 'agents'), /^sr-.*\.md$/);
|
|
968
|
+
const hasCommands = (((0, fs_1.existsSync)((0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'sr')) && hasFiles((0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'sr'), /\.md$/)) ||
|
|
969
|
+
((0, fs_1.existsSync)((0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'specrails')) && hasFiles((0, path_1.join)(projectPath, SPECRAILS_DIR, 'commands', 'specrails'), /\.md$/)));
|
|
970
|
+
const isComplete = hasAgents && hasCommands;
|
|
971
|
+
if (isComplete) {
|
|
972
|
+
const legacySrRemoved = sweepLegacySrCommands(projectPath);
|
|
973
|
+
const tier = this._projectTiers.get(projectId) ?? 'full';
|
|
974
|
+
const summary = { ...computeSummary(projectPath, tier, this._projectProviders.get(projectId) ?? 'claude'), legacySrRemoved };
|
|
975
|
+
this._onSetupDone?.(projectId);
|
|
976
|
+
this._broadcast({
|
|
977
|
+
type: 'setup_complete',
|
|
978
|
+
projectId,
|
|
979
|
+
sessionId: capturedSessionId ?? undefined,
|
|
980
|
+
summary,
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
else {
|
|
984
|
+
// Claude finished one turn but setup isn't done yet.
|
|
985
|
+
// Emit turn_done so the wizard knows to wait for user input.
|
|
986
|
+
this._broadcast({
|
|
987
|
+
type: 'setup_turn_done',
|
|
988
|
+
projectId,
|
|
989
|
+
sessionId: capturedSessionId ?? undefined,
|
|
990
|
+
});
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
else {
|
|
994
|
+
this._onSetupDone?.(projectId);
|
|
995
|
+
this._broadcast({
|
|
996
|
+
type: 'setup_error',
|
|
997
|
+
projectId,
|
|
998
|
+
error: `${adapter.binary} enrich exited with code ${code ?? 'unknown'}`,
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
_initCheckpoints(projectId) {
|
|
1004
|
+
const tier = this._projectTiers.get(projectId) ?? 'full';
|
|
1005
|
+
const defs = tier === 'quick' ? exports.QUICK_CHECKPOINTS : exports.CHECKPOINTS;
|
|
1006
|
+
const statuses = new Map();
|
|
1007
|
+
const starts = new Map();
|
|
1008
|
+
for (const def of defs) {
|
|
1009
|
+
statuses.set(def.key, { key: def.key, name: def.name, status: 'pending' });
|
|
1010
|
+
}
|
|
1011
|
+
this._checkpoints.set(projectId, statuses);
|
|
1012
|
+
this._checkpointStart.set(projectId, starts);
|
|
1013
|
+
}
|
|
1014
|
+
_advanceCheckpoint(projectId, key, detail) {
|
|
1015
|
+
const statuses = this._checkpoints.get(projectId);
|
|
1016
|
+
if (!statuses)
|
|
1017
|
+
return;
|
|
1018
|
+
const checkpoint = statuses.get(key);
|
|
1019
|
+
if (!checkpoint || checkpoint.status === 'done')
|
|
1020
|
+
return;
|
|
1021
|
+
const starts = this._checkpointStart.get(projectId);
|
|
1022
|
+
// When a later checkpoint starts, auto-complete all earlier ones
|
|
1023
|
+
const tier = this._projectTiers.get(projectId) ?? 'full';
|
|
1024
|
+
const checkpointDefs = tier === 'quick' ? exports.QUICK_CHECKPOINTS : exports.CHECKPOINTS;
|
|
1025
|
+
const checkpointKeys = checkpointDefs.map((c) => c.key);
|
|
1026
|
+
const targetIdx = checkpointKeys.indexOf(key);
|
|
1027
|
+
for (let i = 0; i < targetIdx; i++) {
|
|
1028
|
+
const prevKey = checkpointKeys[i];
|
|
1029
|
+
const prev = statuses.get(prevKey);
|
|
1030
|
+
if (prev && prev.status !== 'done') {
|
|
1031
|
+
this._completeCheckpoint(projectId, prevKey);
|
|
1032
|
+
}
|
|
1033
|
+
}
|
|
1034
|
+
if (checkpoint.status === 'pending') {
|
|
1035
|
+
checkpoint.status = 'running';
|
|
1036
|
+
starts.set(key, Date.now());
|
|
1037
|
+
if (detail)
|
|
1038
|
+
checkpoint.detail = detail;
|
|
1039
|
+
this._broadcast({ type: 'setup_checkpoint', projectId, checkpoint: key, status: 'running', detail });
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
_completeCheckpoint(projectId, key) {
|
|
1043
|
+
const statuses = this._checkpoints.get(projectId);
|
|
1044
|
+
if (!statuses)
|
|
1045
|
+
return;
|
|
1046
|
+
const checkpoint = statuses.get(key);
|
|
1047
|
+
if (!checkpoint || checkpoint.status === 'done')
|
|
1048
|
+
return;
|
|
1049
|
+
const starts = this._checkpointStart.get(projectId);
|
|
1050
|
+
const startTime = starts.get(key) ?? Date.now();
|
|
1051
|
+
const duration_ms = Date.now() - startTime;
|
|
1052
|
+
starts.delete(key);
|
|
1053
|
+
checkpoint.status = 'done';
|
|
1054
|
+
checkpoint.duration_ms = duration_ms;
|
|
1055
|
+
this._broadcast({ type: 'setup_checkpoint', projectId, checkpoint: key, status: 'done', duration_ms });
|
|
1056
|
+
}
|
|
1057
|
+
_syncFilesystemCheckpoints(projectId, projectPath) {
|
|
1058
|
+
const statuses = this._checkpoints.get(projectId);
|
|
1059
|
+
if (!statuses)
|
|
1060
|
+
return;
|
|
1061
|
+
const fsChecks = checkFilesystem(projectPath);
|
|
1062
|
+
for (const [key, exists] of Object.entries(fsChecks)) {
|
|
1063
|
+
if (!exists)
|
|
1064
|
+
continue;
|
|
1065
|
+
const cp = statuses.get(key);
|
|
1066
|
+
if (!cp)
|
|
1067
|
+
continue;
|
|
1068
|
+
if (cp.status === 'pending') {
|
|
1069
|
+
// Fast-path: mark running then done immediately
|
|
1070
|
+
this._advanceCheckpoint(projectId, key);
|
|
1071
|
+
this._completeCheckpoint(projectId, key);
|
|
1072
|
+
}
|
|
1073
|
+
else if (cp.status === 'running') {
|
|
1074
|
+
this._completeCheckpoint(projectId, key);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
// ─── Checkpoint poll endpoint ─────────────────────────────────────────────────
|
|
1079
|
+
getCheckpointStatus(projectId, projectPath) {
|
|
1080
|
+
// Sync from filesystem before returning
|
|
1081
|
+
this._syncFilesystemCheckpoints(projectId, projectPath);
|
|
1082
|
+
const tier = this._projectTiers.get(projectId) ?? 'full';
|
|
1083
|
+
const defs = tier === 'quick' ? exports.QUICK_CHECKPOINTS : exports.CHECKPOINTS;
|
|
1084
|
+
const statuses = this._checkpoints.get(projectId);
|
|
1085
|
+
if (!statuses) {
|
|
1086
|
+
// Return all-pending if install hasn't started
|
|
1087
|
+
return defs.map((def) => ({ key: def.key, name: def.name, status: 'pending' }));
|
|
1088
|
+
}
|
|
1089
|
+
return defs.map((def) => statuses.get(def.key) ?? { key: def.key, name: def.name, status: 'pending' });
|
|
1090
|
+
}
|
|
1091
|
+
getInstallLog(projectId) {
|
|
1092
|
+
return this._installLogBuffer.get(projectId) ?? [];
|
|
1093
|
+
}
|
|
1094
|
+
// ─── Abort ────────────────────────────────────────────────────────────────────
|
|
1095
|
+
abort(projectId) {
|
|
1096
|
+
this._stopFilesystemPoll(projectId);
|
|
1097
|
+
this._projectProviders.delete(projectId);
|
|
1098
|
+
this._projectTiers.delete(projectId);
|
|
1099
|
+
this._onSetupDone?.(projectId);
|
|
1100
|
+
const installChild = this._installProcesses.get(projectId);
|
|
1101
|
+
if (installChild?.pid) {
|
|
1102
|
+
this._terminateWithEscalation(installChild.pid);
|
|
1103
|
+
this._installProcesses.delete(projectId);
|
|
1104
|
+
}
|
|
1105
|
+
const setupChild = this._setupProcesses.get(projectId);
|
|
1106
|
+
if (setupChild?.pid) {
|
|
1107
|
+
this._terminateWithEscalation(setupChild.pid);
|
|
1108
|
+
this._setupProcesses.delete(projectId);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
/**
|
|
1112
|
+
* SIGTERM a process tree, then escalate to SIGKILL after a grace window if it
|
|
1113
|
+
* is still alive — mirroring QueueManager._kill. The Map entry is deleted
|
|
1114
|
+
* immediately by the caller, so the pid is captured locally here; without the
|
|
1115
|
+
* escalation a child that ignores SIGTERM (npm/npx scaffolding, a hung CLI)
|
|
1116
|
+
* would be orphaned for the host's lifetime with no remaining handle.
|
|
1117
|
+
*/
|
|
1118
|
+
_terminateWithEscalation(pid) {
|
|
1119
|
+
try {
|
|
1120
|
+
(0, tree_kill_1.default)(pid, 'SIGTERM');
|
|
1121
|
+
}
|
|
1122
|
+
catch { /* best-effort */ }
|
|
1123
|
+
const grace = setTimeout(() => {
|
|
1124
|
+
try {
|
|
1125
|
+
(0, tree_kill_1.default)(pid, 'SIGKILL', () => { });
|
|
1126
|
+
}
|
|
1127
|
+
catch { /* best-effort */ }
|
|
1128
|
+
}, 5000);
|
|
1129
|
+
if (typeof grace.unref === 'function')
|
|
1130
|
+
grace.unref();
|
|
1131
|
+
}
|
|
1132
|
+
isInstalling(projectId) {
|
|
1133
|
+
return this._installProcesses.has(projectId);
|
|
1134
|
+
}
|
|
1135
|
+
isEnriching(projectId) {
|
|
1136
|
+
return this._setupProcesses.has(projectId);
|
|
1137
|
+
}
|
|
1138
|
+
/** @deprecated Use isEnriching() instead */
|
|
1139
|
+
isSettingUp(projectId) {
|
|
1140
|
+
return this.isEnriching(projectId);
|
|
1141
|
+
}
|
|
1142
|
+
getInstallTier(projectId) {
|
|
1143
|
+
return this._projectTiers.get(projectId);
|
|
1144
|
+
}
|
|
1145
|
+
getSummary(projectPath) {
|
|
1146
|
+
const config = readInstallConfig(projectPath);
|
|
1147
|
+
const tier = config?.tier ?? 'quick';
|
|
1148
|
+
// Provider is authoritative from install-config.yaml when present; we
|
|
1149
|
+
// do NOT fall back to filesystem heuristics because both `.codex/` and
|
|
1150
|
+
// `.claude/` can legitimately coexist (e.g. a project that's been
|
|
1151
|
+
// re-init'd) and a generic `existsSync` probe would mis-route.
|
|
1152
|
+
let provider = 'claude';
|
|
1153
|
+
try {
|
|
1154
|
+
const text = (0, fs_1.readFileSync)((0, path_1.join)(projectPath, '.specrails', 'install-config.yaml'), 'utf-8');
|
|
1155
|
+
const m = text.match(/^provider:\s*(\w+)/m);
|
|
1156
|
+
if (m && m[1] === 'codex')
|
|
1157
|
+
provider = 'codex';
|
|
1158
|
+
}
|
|
1159
|
+
catch {
|
|
1160
|
+
// Missing install-config — stay on claude default.
|
|
1161
|
+
}
|
|
1162
|
+
return computeSummary(projectPath, tier, provider);
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
exports.SetupManager = SetupManager;
|