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,586 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Note: the pkg-binary native-addon hijacks (better_sqlite3.node, node-pty, pty.node)
|
|
3
|
+
// used to live here but had to move to an esbuild `banner.js` in scripts/build-sidecar.mjs
|
|
4
|
+
// so they run BEFORE esbuild's top-of-bundle `require('node-pty')` statement.
|
|
5
|
+
// See the banner in that script for the actual patches.
|
|
6
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
7
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
8
|
+
};
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
const http_1 = __importDefault(require("http"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
12
|
+
const fs_1 = __importDefault(require("fs"));
|
|
13
|
+
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const express_1 = __importDefault(require("express"));
|
|
15
|
+
const ws_1 = require("ws");
|
|
16
|
+
const project_registry_1 = require("./project-registry");
|
|
17
|
+
const desktop_router_1 = require("./desktop-router");
|
|
18
|
+
const project_router_1 = require("./project-router");
|
|
19
|
+
const docs_router_1 = require("./docs-router");
|
|
20
|
+
const auth_1 = require("./auth");
|
|
21
|
+
const ws_routing_1 = require("./ws-routing");
|
|
22
|
+
const terminal_manager_1 = require("./terminal-manager");
|
|
23
|
+
const terminal_shell_integration_1 = require("./terminal-shell-integration");
|
|
24
|
+
const feature_flags_1 = require("./feature-flags");
|
|
25
|
+
const mobile_1 = require("./mobile");
|
|
26
|
+
const telemetry_receiver_1 = require("./telemetry-receiver");
|
|
27
|
+
const telemetry_compactor_1 = require("./telemetry-compactor");
|
|
28
|
+
const path_resolver_1 = require("./path-resolver");
|
|
29
|
+
// Side-effect import: registers every bundled ProviderAdapter (claude, codex,
|
|
30
|
+
// future providers) so `getAdapter`/`hasAdapter`/`listAdapters` are populated
|
|
31
|
+
// before any manager constructs a project context. See
|
|
32
|
+
// openspec/changes/add-multi-provider-support/specs/multi-provider-architecture/spec.md.
|
|
33
|
+
require("./providers");
|
|
34
|
+
const inheritedPathBeforeResolve = (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean).length;
|
|
35
|
+
(0, path_resolver_1.resolveStartupPath)();
|
|
36
|
+
const TERMINAL_PANEL_ENABLED = process.env.SPECRAILS_TERMINAL_PANEL !== 'false';
|
|
37
|
+
const BROWSER_CAPTURE_ENABLED = (0, feature_flags_1.isBrowserCaptureEnabled)();
|
|
38
|
+
// Read package.json version once at startup
|
|
39
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
40
|
+
const PKG_VERSION = (() => {
|
|
41
|
+
try {
|
|
42
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
43
|
+
return require('../package.json').version ?? '0.0.0';
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
return '0.0.0';
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
// ─── Desktop app watchdog ─────────────────────────────────────────────────────
|
|
50
|
+
// When running as a Tauri sidecar, the parent Tauri process passes its PID via
|
|
51
|
+
// --parent-pid=<pid>. We poll every 3 seconds and exit if the parent is gone,
|
|
52
|
+
// preventing orphaned server processes after an app crash.
|
|
53
|
+
const parentPidArg = process.argv.find((a) => a.startsWith('--parent-pid='));
|
|
54
|
+
if (parentPidArg) {
|
|
55
|
+
const parentPid = parseInt(parentPidArg.split('=')[1], 10);
|
|
56
|
+
if (!isNaN(parentPid)) {
|
|
57
|
+
// Poll at 1s (not 3s): on a self-update relaunch the Tauri host exits and the
|
|
58
|
+
// freshly-launched instance needs port 4200 freed fast. The old 3s latency
|
|
59
|
+
// raced the new instance's startup port check and produced "port already in
|
|
60
|
+
// use". Faster detection + a graceful exit shrinks that window.
|
|
61
|
+
const watchdog = setInterval(() => {
|
|
62
|
+
try {
|
|
63
|
+
// signal 0 = existence check only, does not actually send a signal
|
|
64
|
+
process.kill(parentPid, 0);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
// Parent process is gone — shut down GRACEFULLY (tree-kill child rails,
|
|
68
|
+
// PTYs, remove the PID file, release the port) instead of a bare
|
|
69
|
+
// process.exit(0) that orphans children and leaks the PID file.
|
|
70
|
+
clearInterval(watchdog);
|
|
71
|
+
void shutdown();
|
|
72
|
+
}
|
|
73
|
+
}, 1000);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
// ─── Parse CLI args ───────────────────────────────────────────────────────────
|
|
77
|
+
let port = 4200;
|
|
78
|
+
for (let i = 2; i < process.argv.length; i++) {
|
|
79
|
+
if (process.argv[i] === '--port' && process.argv[i + 1]) {
|
|
80
|
+
port = parseInt(process.argv[++i], 10);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
// ─── PID file management ──────────────────────────────────────────────────────
|
|
84
|
+
const PID_DIR = path_1.default.join(os_1.default.homedir(), '.specrails');
|
|
85
|
+
const PID_FILE = path_1.default.join(PID_DIR, 'manager.pid');
|
|
86
|
+
function writePidFile() {
|
|
87
|
+
try {
|
|
88
|
+
fs_1.default.mkdirSync(PID_DIR, { recursive: true });
|
|
89
|
+
fs_1.default.writeFileSync(PID_FILE, String(process.pid), 'utf-8');
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
// Non-fatal
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function removePidFile() {
|
|
96
|
+
try {
|
|
97
|
+
fs_1.default.unlinkSync(PID_FILE);
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// Non-fatal
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
// ─── Express + WebSocket setup ────────────────────────────────────────────────
|
|
104
|
+
const app = (0, express_1.default)();
|
|
105
|
+
// Host-header validation (H-08) — first barrier, anti DNS-rebinding.
|
|
106
|
+
// Implementation in auth.ts (unit-tested there); index.ts is coverage-excluded.
|
|
107
|
+
app.use(auth_1.hostValidationMiddleware);
|
|
108
|
+
// ─── CORS — allow only localhost origins (CRIT-02) ────────────────────────────
|
|
109
|
+
// Tauri's desktop WebView exposes two different origin formats:
|
|
110
|
+
// - macOS / Linux: tauri://localhost
|
|
111
|
+
// - Windows WebView2: http://tauri.localhost (virtual-host mapping on the
|
|
112
|
+
// custom scheme; shows up as a regular http origin from the fetch layer)
|
|
113
|
+
const ALLOWED_ORIGIN_PATTERN = /^(https?:\/\/(localhost|127\.0\.0\.1|tauri\.localhost)(:\d+)?|tauri:\/\/localhost)$/;
|
|
114
|
+
function isAllowedBrowserOrigin(origin) {
|
|
115
|
+
return origin === undefined || ALLOWED_ORIGIN_PATTERN.test(origin);
|
|
116
|
+
}
|
|
117
|
+
function corsMiddleware(req, res, next) {
|
|
118
|
+
const origin = req.headers['origin'];
|
|
119
|
+
if (origin) {
|
|
120
|
+
if (ALLOWED_ORIGIN_PATTERN.test(origin)) {
|
|
121
|
+
res.setHeader('Access-Control-Allow-Origin', origin);
|
|
122
|
+
res.setHeader('Vary', 'Origin');
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
// Non-localhost origin — reject with 403
|
|
126
|
+
res.status(403).json({ error: 'Forbidden: cross-origin requests not allowed' });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
131
|
+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Desktop-Token');
|
|
132
|
+
res.setHeader('Access-Control-Max-Age', '600');
|
|
133
|
+
if (req.method === 'OPTIONS') {
|
|
134
|
+
res.sendStatus(204);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
next();
|
|
138
|
+
}
|
|
139
|
+
app.use(corsMiddleware);
|
|
140
|
+
// ─── Body size limit (MED-02) ─────────────────────────────────────────────────
|
|
141
|
+
app.use(express_1.default.json({ limit: '1mb' }));
|
|
142
|
+
const server = http_1.default.createServer(app);
|
|
143
|
+
const wsServerOptions = {
|
|
144
|
+
noServer: true,
|
|
145
|
+
// Cap inbound frame size (H-10). Shared by the main, terminal and browser WS
|
|
146
|
+
// servers. Terminal/browser input frames (keystrokes, control JSON, pastes)
|
|
147
|
+
// are tiny; 1 MB tolerates a large bracketed paste while bounding the memory
|
|
148
|
+
// a single malicious frame can force the sidecar to buffer.
|
|
149
|
+
maxPayload: 1024 * 1024,
|
|
150
|
+
handleProtocols: (protocols) => {
|
|
151
|
+
if (protocols.has('specrails-desktop'))
|
|
152
|
+
return 'specrails-desktop';
|
|
153
|
+
// Backward compatibility for clients that only offer the auth carrier.
|
|
154
|
+
for (const protocol of protocols) {
|
|
155
|
+
if (protocol.startsWith('desktop-token.'))
|
|
156
|
+
return protocol;
|
|
157
|
+
}
|
|
158
|
+
return false;
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
const wss = new ws_1.WebSocketServer(wsServerOptions);
|
|
162
|
+
const terminalWss = new ws_1.WebSocketServer(wsServerOptions);
|
|
163
|
+
const browserWss = new ws_1.WebSocketServer(wsServerOptions);
|
|
164
|
+
const clients = new Map();
|
|
165
|
+
// 8 MB: a healthy client drains far faster than we produce; crossing this means
|
|
166
|
+
// the socket is stalled. Dropping an event is safe — clients reconcile via the
|
|
167
|
+
// REST polling paths — and far better than an unbounded memory leak.
|
|
168
|
+
const WS_BACKPRESSURE_LIMIT_BYTES = 8 * 1024 * 1024;
|
|
169
|
+
const TERMINAL_WS_RE = /^\/ws\/terminal\/([0-9a-f-]+)$/i;
|
|
170
|
+
const BROWSER_WS_RE = /^\/ws\/browser\/([0-9a-f-]+)$/i;
|
|
171
|
+
function rejectUpgrade(socket, status, reason) {
|
|
172
|
+
socket.write(`HTTP/1.1 ${status} ${reason}\r\nConnection: close\r\n\r\n`);
|
|
173
|
+
socket.destroy();
|
|
174
|
+
}
|
|
175
|
+
function authorizeUpgrade(request) {
|
|
176
|
+
const origin = request.headers.origin;
|
|
177
|
+
if (!isAllowedBrowserOrigin(origin))
|
|
178
|
+
return 'forbidden';
|
|
179
|
+
const provided = (0, auth_1.tokenFromUpgradeRequest)(request);
|
|
180
|
+
if (!provided || !(0, auth_1.safeEqual)(provided, (0, auth_1.loadOrGenerateToken)()))
|
|
181
|
+
return 'unauthorized';
|
|
182
|
+
return 'ok';
|
|
183
|
+
}
|
|
184
|
+
function broadcast(msg) {
|
|
185
|
+
const data = JSON.stringify(msg);
|
|
186
|
+
// Project-scoped messages carry a projectId; app-level messages do not.
|
|
187
|
+
const msgProjectId = msg.projectId;
|
|
188
|
+
for (const [client, state] of clients) {
|
|
189
|
+
if (client.readyState !== ws_1.WebSocket.OPEN)
|
|
190
|
+
continue;
|
|
191
|
+
// H-09: route project-scoped messages only to matching/undeclared subscribers.
|
|
192
|
+
if (!(0, ws_routing_1.shouldDeliverToSubscriber)(msgProjectId, state.subscribedProjectId))
|
|
193
|
+
continue;
|
|
194
|
+
// H-10: drop for a back-pressured client instead of growing its buffer.
|
|
195
|
+
if (client.bufferedAmount > WS_BACKPRESSURE_LIMIT_BYTES)
|
|
196
|
+
continue;
|
|
197
|
+
client.send(data);
|
|
198
|
+
}
|
|
199
|
+
// Fan a copy to the Mobile Gateway's in-process bus (no-op when no phone is
|
|
200
|
+
// attached). publish() swallows mobile-side errors so the main loop is safe.
|
|
201
|
+
(0, mobile_1.getMobileEventBus)().publish(msg);
|
|
202
|
+
}
|
|
203
|
+
// ─── Health endpoint state (populated by the Super-mode bootstrap below) ─────
|
|
204
|
+
let _getProjectCount = () => 0;
|
|
205
|
+
/** Captured by the Super-mode bootstrap block so graceful shutdown can tear down every
|
|
206
|
+
* project's spawners (rail/chat children) instead of orphaning them. */
|
|
207
|
+
let _registry = null;
|
|
208
|
+
/** The mobile companion gateway (off by default); torn down on shutdown. */
|
|
209
|
+
let _mobileGateway = null;
|
|
210
|
+
server.on('upgrade', (request, socket, head) => {
|
|
211
|
+
const urlStr = request.url ?? '/';
|
|
212
|
+
const auth = authorizeUpgrade(request);
|
|
213
|
+
if (auth === 'forbidden')
|
|
214
|
+
return rejectUpgrade(socket, 403, 'Forbidden');
|
|
215
|
+
if (auth === 'unauthorized')
|
|
216
|
+
return rejectUpgrade(socket, 401, 'Unauthorized');
|
|
217
|
+
// Terminal PTY WebSocket endpoint: /ws/terminal/:id?projectId=...
|
|
218
|
+
const pathOnly = urlStr.split('?')[0];
|
|
219
|
+
const termMatch = pathOnly.match(TERMINAL_WS_RE);
|
|
220
|
+
if (termMatch) {
|
|
221
|
+
if (!TERMINAL_PANEL_ENABLED)
|
|
222
|
+
return rejectUpgrade(socket, 404, 'Not Found');
|
|
223
|
+
let parsed;
|
|
224
|
+
try {
|
|
225
|
+
parsed = new URL(urlStr, 'http://localhost');
|
|
226
|
+
}
|
|
227
|
+
catch {
|
|
228
|
+
return rejectUpgrade(socket, 400, 'Bad Request');
|
|
229
|
+
}
|
|
230
|
+
const projectId = parsed.searchParams.get('projectId');
|
|
231
|
+
const sessionId = termMatch[1];
|
|
232
|
+
const tm = (0, terminal_manager_1.getTerminalManager)();
|
|
233
|
+
const session = tm.getUnsafe(sessionId);
|
|
234
|
+
if (!session) {
|
|
235
|
+
// The session may have died right after POST /terminals returned (e.g. a
|
|
236
|
+
// shell that failed to acquire a controlling tty). If we have a tombstone,
|
|
237
|
+
// upgrade just long enough to tell the client WHY, then close — otherwise
|
|
238
|
+
// the client sees a bare 404 and a silent dead terminal.
|
|
239
|
+
if (projectId) {
|
|
240
|
+
const tomb = tm.getTombstone(sessionId, projectId);
|
|
241
|
+
if (tomb) {
|
|
242
|
+
terminalWss.handleUpgrade(request, socket, head, (ws) => {
|
|
243
|
+
try {
|
|
244
|
+
ws.send(JSON.stringify({ type: 'exit', code: tomb.code, signal: tomb.signal, early: tomb.early }));
|
|
245
|
+
}
|
|
246
|
+
catch { /* ignore */ }
|
|
247
|
+
try {
|
|
248
|
+
ws.close(1000, tomb.early ? 'pty_exit_early' : 'pty_exit');
|
|
249
|
+
}
|
|
250
|
+
catch { /* ignore */ }
|
|
251
|
+
});
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
return rejectUpgrade(socket, 404, 'Not Found');
|
|
256
|
+
}
|
|
257
|
+
if (!projectId || session.projectId !== projectId)
|
|
258
|
+
return rejectUpgrade(socket, 403, 'Forbidden');
|
|
259
|
+
terminalWss.handleUpgrade(request, socket, head, (ws) => {
|
|
260
|
+
const meta = tm.attach(sessionId, ws);
|
|
261
|
+
if (!meta) {
|
|
262
|
+
// Lost the race: the pty exited between the getUnsafe check and attach.
|
|
263
|
+
const tomb = tm.getTombstone(sessionId, projectId);
|
|
264
|
+
if (tomb) {
|
|
265
|
+
try {
|
|
266
|
+
ws.send(JSON.stringify({ type: 'exit', code: tomb.code, signal: tomb.signal, early: tomb.early }));
|
|
267
|
+
}
|
|
268
|
+
catch { /* ignore */ }
|
|
269
|
+
}
|
|
270
|
+
try {
|
|
271
|
+
ws.close(1000, tomb?.early ? 'pty_exit_early' : 'pty_exit');
|
|
272
|
+
}
|
|
273
|
+
catch { /* ignore */ }
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
ws.on('message', (data, isBinary) => {
|
|
277
|
+
if (isBinary) {
|
|
278
|
+
tm.write(sessionId, data);
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
try {
|
|
282
|
+
const txt = data.toString('utf8');
|
|
283
|
+
const msg = JSON.parse(txt);
|
|
284
|
+
if (msg?.type === 'resize' && typeof msg.cols === 'number' && typeof msg.rows === 'number') {
|
|
285
|
+
tm.resize(sessionId, msg.cols, msg.rows);
|
|
286
|
+
}
|
|
287
|
+
else if (msg?.type === 'write' && typeof msg.data === 'string') {
|
|
288
|
+
tm.write(sessionId, msg.data);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
catch { /* ignore malformed control */ }
|
|
292
|
+
});
|
|
293
|
+
ws.on('close', () => tm.detach(sessionId, ws));
|
|
294
|
+
ws.on('error', () => tm.detach(sessionId, ws));
|
|
295
|
+
});
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
// Embedded-browser screencast WebSocket: /ws/browser/:id?projectId=...
|
|
299
|
+
// Frames stream server→client as binary; client→server are JSON control
|
|
300
|
+
// messages (input/navigate). Kept off the shared /ws so high-rate screencast
|
|
301
|
+
// throughput can't starve the project event stream (mirrors the terminal WS).
|
|
302
|
+
const browserMatch = pathOnly.match(BROWSER_WS_RE);
|
|
303
|
+
if (browserMatch) {
|
|
304
|
+
if (!BROWSER_CAPTURE_ENABLED)
|
|
305
|
+
return rejectUpgrade(socket, 404, 'Not Found');
|
|
306
|
+
let parsed;
|
|
307
|
+
try {
|
|
308
|
+
parsed = new URL(urlStr, 'http://localhost');
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
return rejectUpgrade(socket, 400, 'Bad Request');
|
|
312
|
+
}
|
|
313
|
+
const projectId = parsed.searchParams.get('projectId');
|
|
314
|
+
const sessionId = browserMatch[1];
|
|
315
|
+
if (!projectId)
|
|
316
|
+
return rejectUpgrade(socket, 400, 'Bad Request');
|
|
317
|
+
const mgr = _registry?.getContext(projectId)?.browserCaptureManager;
|
|
318
|
+
const session = mgr?.getSession(sessionId);
|
|
319
|
+
if (!mgr || !session)
|
|
320
|
+
return rejectUpgrade(socket, 404, 'Not Found');
|
|
321
|
+
browserWss.handleUpgrade(request, socket, head, (ws) => {
|
|
322
|
+
const client = ws;
|
|
323
|
+
void mgr.attach(sessionId, client).then((meta) => {
|
|
324
|
+
if (!meta) {
|
|
325
|
+
// Session died between the upgrade check and attach — tell the client
|
|
326
|
+
// explicitly instead of leaving a silent, frame-less socket open.
|
|
327
|
+
try {
|
|
328
|
+
ws.close(1000, 'session_closed');
|
|
329
|
+
}
|
|
330
|
+
catch { /* ignore */ }
|
|
331
|
+
}
|
|
332
|
+
});
|
|
333
|
+
ws.on('message', (data, isBinary) => {
|
|
334
|
+
if (isBinary)
|
|
335
|
+
return; // browser inbound is JSON control only
|
|
336
|
+
try {
|
|
337
|
+
const msg = JSON.parse(data.toString('utf8'));
|
|
338
|
+
if (msg.type === 'input' && msg.event) {
|
|
339
|
+
void mgr.handleInput(sessionId, msg.event);
|
|
340
|
+
}
|
|
341
|
+
else if (msg.type === 'navigate') {
|
|
342
|
+
void mgr.navigate(sessionId, msg.action ?? 'goto', msg.url);
|
|
343
|
+
}
|
|
344
|
+
else if (msg.type === 'probe' && Number.isFinite(msg.x) && Number.isFinite(msg.y) && msg.x >= 0 && msg.y >= 0) {
|
|
345
|
+
// Hover-to-select: resolve the element under the cursor and reply with
|
|
346
|
+
// its rect so the client can draw a highlight box.
|
|
347
|
+
void mgr.probeElement(sessionId, { x: msg.x, y: msg.y }).then((probe) => {
|
|
348
|
+
try {
|
|
349
|
+
ws.send(JSON.stringify({ type: 'hover', rect: probe?.rect ?? null, selector: probe?.selector ?? null, path: probe?.path ?? null }));
|
|
350
|
+
}
|
|
351
|
+
catch { /* drop */ }
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
catch { /* ignore malformed control */ }
|
|
356
|
+
});
|
|
357
|
+
ws.on('close', () => mgr.detach(sessionId, client));
|
|
358
|
+
ws.on('error', () => mgr.detach(sessionId, client));
|
|
359
|
+
});
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
// Default: main event WebSocket.
|
|
363
|
+
wss.handleUpgrade(request, socket, head, (ws) => {
|
|
364
|
+
wss.emit('connection', ws, request);
|
|
365
|
+
});
|
|
366
|
+
});
|
|
367
|
+
// ─── Docs portal (available in all modes) ────────────────────────────────────
|
|
368
|
+
// Loopback-only (H-04): docs are unauthenticated by design (no token needed to
|
|
369
|
+
// read them), so a loopback guard is the only thing standing between them and
|
|
370
|
+
// the network the day the bind changes.
|
|
371
|
+
app.use('/api/docs', auth_1.requireLoopback, (0, docs_router_1.createDocsRouter)());
|
|
372
|
+
// ─── Auth — protect all /api/* except /api/health and /api/token ─────────────
|
|
373
|
+
// (CRIT-01) Token is served publicly so the local client can bootstrap itself.
|
|
374
|
+
app.get('/api/health', (_req, res) => {
|
|
375
|
+
res.json({
|
|
376
|
+
status: 'ok',
|
|
377
|
+
version: PKG_VERSION,
|
|
378
|
+
uptime: Math.floor(process.uptime()),
|
|
379
|
+
projects: _getProjectCount(),
|
|
380
|
+
mode: 'super',
|
|
381
|
+
});
|
|
382
|
+
});
|
|
383
|
+
// Loopback-only (H-03): this is the most sensitive endpoint — it hands out the
|
|
384
|
+
// master token, which grants terminals/spawn/fs/admin. It must stay public (no
|
|
385
|
+
// token) so the local client can bootstrap. Two complementary guards protect it:
|
|
386
|
+
// `requireLoopback` rejects a non-local PEER (matters if the bind ever changes
|
|
387
|
+
// from 127.0.0.1), and the Host-header guard above rejects DNS-rebinding (where
|
|
388
|
+
// the peer IS loopback — the victim's own browser — but the Host is the
|
|
389
|
+
// attacker's domain). Neither alone is sufficient; together they close both.
|
|
390
|
+
app.get('/api/token', auth_1.requireLoopback, (_req, res) => {
|
|
391
|
+
res.json({ token: (0, auth_1.loadOrGenerateToken)() });
|
|
392
|
+
});
|
|
393
|
+
app.use('/api', auth_1.requireAuth);
|
|
394
|
+
// ─── WebSocket rate limiting helper (LOW-03) ──────────────────────────────────
|
|
395
|
+
const WS_MAX_MESSAGES_PER_MINUTE = 120;
|
|
396
|
+
const WS_MAX_MESSAGE_BYTES = 65_536; // 64 KB
|
|
397
|
+
function applyWsRateLimiting(ws) {
|
|
398
|
+
let messageCount = 0;
|
|
399
|
+
const resetTimer = setInterval(() => { messageCount = 0; }, 60_000);
|
|
400
|
+
ws.on('message', (data) => {
|
|
401
|
+
const size = typeof data === 'string' ? Buffer.byteLength(data) : data.byteLength;
|
|
402
|
+
if (size > WS_MAX_MESSAGE_BYTES) {
|
|
403
|
+
ws.close(1009, 'Message too large');
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
messageCount++;
|
|
407
|
+
if (messageCount > WS_MAX_MESSAGES_PER_MINUTE) {
|
|
408
|
+
ws.close(1008, 'Rate limit exceeded');
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
ws.on('close', () => {
|
|
412
|
+
clearInterval(resetTimer);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
// ─── Super-mode bootstrap ─────────────────────────────────────────────────────
|
|
416
|
+
{
|
|
417
|
+
const registry = new project_registry_1.ProjectRegistry(broadcast, undefined, port);
|
|
418
|
+
registry.loadAll();
|
|
419
|
+
_registry = registry;
|
|
420
|
+
_getProjectCount = () => registry.listContexts().length;
|
|
421
|
+
// OTLP/JSON receiver — INTENTIONALLY UNAUTHENTICATED (H-01/H-02). The spawned
|
|
422
|
+
// claude/codex CLIs post telemetry here with no auth header (queue-manager sets
|
|
423
|
+
// OTEL_EXPORTER_OTLP_ENDPOINT but no OTEL_EXPORTER_OTLP_HEADERS), so it cannot
|
|
424
|
+
// be put behind requireAuth. It is also NOT covered by `app.use('/api', ...)`
|
|
425
|
+
// — that middleware is path-scoped to /api, and /otlp is a sibling path (the
|
|
426
|
+
// old comment here claiming otherwise was wrong). It is protected instead by
|
|
427
|
+
// `requireLoopback` (children always connect via 127.0.0.1) + the loopback bind.
|
|
428
|
+
app.use('/otlp', auth_1.requireLoopback, (0, telemetry_receiver_1.createTelemetryRouter)(registry));
|
|
429
|
+
// Run telemetry compaction at startup after registry is hydrated
|
|
430
|
+
(0, telemetry_compactor_1.runCompactionForAll)(registry).catch((err) => {
|
|
431
|
+
console.error('[telemetry-compactor] startup compaction error:', err);
|
|
432
|
+
});
|
|
433
|
+
// ─── Mobile companion gateway (off by default; boot if previously enabled) ──
|
|
434
|
+
// Second HTTPS+WSS listener in THIS process; the main server stays loopback.
|
|
435
|
+
// The control plane is desktop-only: loopback + (via /api below) requireAuth.
|
|
436
|
+
// Mounted before the /api desktop router so its sub-path is unambiguous.
|
|
437
|
+
const mobileGateway = new mobile_1.MobileGateway({ desktopDb: registry.desktopDb, desktopPort: port, broadcast });
|
|
438
|
+
_mobileGateway = mobileGateway;
|
|
439
|
+
app.use('/api/mobile', auth_1.requireLoopback, (0, mobile_1.createMobileAdminRouter)({
|
|
440
|
+
gateway: mobileGateway,
|
|
441
|
+
desktopDb: registry.desktopDb,
|
|
442
|
+
broadcast,
|
|
443
|
+
}));
|
|
444
|
+
if (mobileGateway.isEnabledSetting()) {
|
|
445
|
+
mobileGateway.start().catch((err) => console.error('[mobile-gateway] boot start failed:', err));
|
|
446
|
+
}
|
|
447
|
+
// App-level routes. CRITICAL mount order: the desktop router is mounted at
|
|
448
|
+
// '/api' BEFORE the project router below so its exact routes (e.g.
|
|
449
|
+
// GET /api/projects, DELETE /api/projects/:id) are handled here, while
|
|
450
|
+
// everything else under /api/projects/:projectId/* falls through to the
|
|
451
|
+
// project router.
|
|
452
|
+
app.use('/api', (0, desktop_router_1.createDesktopRouter)(registry, broadcast));
|
|
453
|
+
// Per-project routes under /api/projects/:projectId/*
|
|
454
|
+
app.use('/api/projects', (0, project_router_1.createProjectRouter)(registry));
|
|
455
|
+
// Return 410 Gone for the old per-project hook endpoint in Super mode
|
|
456
|
+
app.post('/hooks/events', (_req, res) => {
|
|
457
|
+
res.status(410).json({
|
|
458
|
+
error: 'In Super mode, use /api/projects/:projectId/hooks/events',
|
|
459
|
+
});
|
|
460
|
+
});
|
|
461
|
+
wss.on('connection', (ws) => {
|
|
462
|
+
const state = { subscribedProjectId: null };
|
|
463
|
+
clients.set(ws, state);
|
|
464
|
+
applyWsRateLimiting(ws);
|
|
465
|
+
// H-09: honor an optional `{ type: 'subscribe', projectId }` control frame so
|
|
466
|
+
// a connection can scope itself to one project's events. Anything else on the
|
|
467
|
+
// inbound channel is ignored (the main event WS is otherwise server→client).
|
|
468
|
+
ws.on('message', (data) => {
|
|
469
|
+
const txt = typeof data === 'string' ? data : data.toString('utf8');
|
|
470
|
+
const frame = (0, ws_routing_1.parseSubscribeFrame)(txt);
|
|
471
|
+
if (frame.subscribe)
|
|
472
|
+
state.subscribedProjectId = frame.projectId;
|
|
473
|
+
});
|
|
474
|
+
// Send app state init
|
|
475
|
+
const projects = registry.listContexts().map((ctx) => ctx.project);
|
|
476
|
+
ws.send(JSON.stringify({
|
|
477
|
+
type: 'desktop.projects',
|
|
478
|
+
projects,
|
|
479
|
+
timestamp: new Date().toISOString(),
|
|
480
|
+
}));
|
|
481
|
+
ws.on('close', () => {
|
|
482
|
+
clients.delete(ws);
|
|
483
|
+
});
|
|
484
|
+
});
|
|
485
|
+
}
|
|
486
|
+
// ─── Global async error handler ────────────────────────────────────────────────
|
|
487
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
488
|
+
app.use((err, _req, res, _next) => {
|
|
489
|
+
console.error('[unhandled error]', err);
|
|
490
|
+
if (!res.headersSent) {
|
|
491
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
// ─── Serve built React client (production) ────────────────────────────────────
|
|
495
|
+
const clientDist = path_1.default.resolve(__dirname, '../../client/dist');
|
|
496
|
+
if (fs_1.default.existsSync(clientDist)) {
|
|
497
|
+
app.use(express_1.default.static(clientDist));
|
|
498
|
+
app.get(/^(?!\/api|\/hooks).*/, (_req, res) => {
|
|
499
|
+
res.sendFile(path_1.default.join(clientDist, 'index.html'));
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
// ─── Start server ─────────────────────────────────────────────────────────────
|
|
503
|
+
server.on('error', (err) => {
|
|
504
|
+
if (err.code === 'EADDRINUSE') {
|
|
505
|
+
console.error(`[error] Port ${port} is already in use. Is another manager instance running?`);
|
|
506
|
+
console.error(`[error] Try stopping it first: specrails-desktop stop`);
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
throw err;
|
|
510
|
+
});
|
|
511
|
+
server.listen(port, '127.0.0.1', () => {
|
|
512
|
+
console.log(`specrails web manager running on http://127.0.0.1:${port}`);
|
|
513
|
+
writePidFile();
|
|
514
|
+
// Sweep stale shell-integration shim directories left behind by previous runs.
|
|
515
|
+
try {
|
|
516
|
+
const removed = (0, terminal_shell_integration_1.cleanupStaleShimDirs)();
|
|
517
|
+
if (removed > 0)
|
|
518
|
+
console.log(`[terminal-shell-integration] cleaned ${removed} stale shim dirs`);
|
|
519
|
+
}
|
|
520
|
+
catch { /* best effort */ }
|
|
521
|
+
void (0, path_resolver_1.augmentPathFromLoginShell)().then(() => {
|
|
522
|
+
const diag = (0, path_resolver_1.getPathDiagnostic)();
|
|
523
|
+
const augmented = diag.pathSegments.length - inheritedPathBeforeResolve;
|
|
524
|
+
const source = process.stdin.isTTY ? 'terminal' : 'gui';
|
|
525
|
+
console.log(`[path-resolver] inherited=${inheritedPathBeforeResolve} augmented=${Math.max(0, augmented)} loginShell=${diag.loginShellStatus} source=${source}`);
|
|
526
|
+
});
|
|
527
|
+
});
|
|
528
|
+
// ─── Clean shutdown ───────────────────────────────────────────────────────────
|
|
529
|
+
let shuttingDown = false;
|
|
530
|
+
async function shutdown() {
|
|
531
|
+
// Idempotent: the watchdog, SIGTERM and SIGINT can all race into here.
|
|
532
|
+
if (shuttingDown)
|
|
533
|
+
return;
|
|
534
|
+
shuttingDown = true;
|
|
535
|
+
removePidFile();
|
|
536
|
+
try {
|
|
537
|
+
_registry?.shutdown();
|
|
538
|
+
}
|
|
539
|
+
catch { /* ignore */ }
|
|
540
|
+
try {
|
|
541
|
+
await (0, terminal_manager_1.getTerminalManager)().shutdown();
|
|
542
|
+
}
|
|
543
|
+
catch { /* ignore */ }
|
|
544
|
+
try {
|
|
545
|
+
await _mobileGateway?.stop();
|
|
546
|
+
}
|
|
547
|
+
catch { /* ignore */ }
|
|
548
|
+
try {
|
|
549
|
+
wss.close();
|
|
550
|
+
}
|
|
551
|
+
catch { /* ignore */ }
|
|
552
|
+
try {
|
|
553
|
+
terminalWss.close();
|
|
554
|
+
}
|
|
555
|
+
catch { /* ignore */ }
|
|
556
|
+
try {
|
|
557
|
+
browserWss.close();
|
|
558
|
+
}
|
|
559
|
+
catch { /* ignore */ }
|
|
560
|
+
// Force-close lingering keep-alive / WebSocket sockets so server.close()'s
|
|
561
|
+
// callback actually fires. The persistent /ws client connection and terminal
|
|
562
|
+
// sockets would otherwise hold the server open, stalling the port release that
|
|
563
|
+
// a relaunching desktop instance is waiting on. (Node 18.2+.)
|
|
564
|
+
try {
|
|
565
|
+
;
|
|
566
|
+
server.closeAllConnections?.();
|
|
567
|
+
}
|
|
568
|
+
catch { /* ignore */ }
|
|
569
|
+
// Hard-exit fallback in case server.close() still hangs.
|
|
570
|
+
const forceExit = setTimeout(() => process.exit(0), 3000);
|
|
571
|
+
forceExit.unref?.();
|
|
572
|
+
server.close(() => {
|
|
573
|
+
clearTimeout(forceExit);
|
|
574
|
+
process.exit(0);
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
process.on('SIGTERM', () => { void shutdown(); });
|
|
578
|
+
process.on('SIGINT', () => { void shutdown(); });
|
|
579
|
+
// Last-resort PID-file cleanup for paths that bypass shutdown() (hard crash,
|
|
580
|
+
// uncaught exception). 'exit' handlers must be synchronous.
|
|
581
|
+
process.on('exit', () => {
|
|
582
|
+
try {
|
|
583
|
+
fs_1.default.unlinkSync(PID_FILE);
|
|
584
|
+
}
|
|
585
|
+
catch { /* best effort */ }
|
|
586
|
+
});
|