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,735 @@
|
|
|
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.createDesktopRouter = createDesktopRouter;
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const crypto_1 = require("crypto");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const fs_1 = __importDefault(require("fs"));
|
|
11
|
+
const net_1 = __importDefault(require("net"));
|
|
12
|
+
const desktop_db_1 = require("./desktop-db");
|
|
13
|
+
const webhook_manager_1 = require("./webhook-manager");
|
|
14
|
+
const specrails_tech_client_1 = require("./specrails-tech-client");
|
|
15
|
+
const core_compat_1 = require("./core-compat");
|
|
16
|
+
const providers_1 = require("./providers");
|
|
17
|
+
const desktop_analytics_1 = require("./desktop-analytics");
|
|
18
|
+
const setup_prerequisites_1 = require("./setup-prerequisites");
|
|
19
|
+
const path_resolver_1 = require("./path-resolver");
|
|
20
|
+
const terminal_settings_1 = require("./terminal-settings");
|
|
21
|
+
function slugify(name) {
|
|
22
|
+
return name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
23
|
+
}
|
|
24
|
+
// Emergency rollback for the codex provider: SPECRAILS_CODEX_BETA=0 forces
|
|
25
|
+
// codex back to "unavailable" without redeploying. The pre-rebrand
|
|
26
|
+
// SPECRAILS_HUB_CODEX_BETA name is read as a legacy fallback when the new
|
|
27
|
+
// var is unset (legacy fallback — do not remove while old installs exist).
|
|
28
|
+
function isCodexBetaDisabled() {
|
|
29
|
+
const v = process.env.SPECRAILS_CODEX_BETA ?? process.env.SPECRAILS_HUB_CODEX_BETA;
|
|
30
|
+
return v === '0';
|
|
31
|
+
}
|
|
32
|
+
// Theme allow-list. Mirror of THEME_IDS in `client/src/lib/themes.ts` —
|
|
33
|
+
// kept duplicated to avoid pulling client code into the server bundle.
|
|
34
|
+
const THEME_ID_ALLOWLIST = new Set(['dracula', 'aurora-light', 'obsidian-dark', 'matrix', 'specrails']);
|
|
35
|
+
// Language allow-list. Mirror of LANGUAGE_IDS in `client/src/lib/i18n.ts` —
|
|
36
|
+
// kept duplicated to avoid pulling client code into the server bundle.
|
|
37
|
+
const LANGUAGE_ID_ALLOWLIST = new Set(['en', 'es', 'fr', 'de', 'pt', 'it', 'zh', 'ja']);
|
|
38
|
+
// LOW-04: Deny registration of system-critical directory paths.
|
|
39
|
+
const DENIED_PATH_PREFIXES = [
|
|
40
|
+
'/etc', '/usr', '/bin', '/sbin', '/lib', '/lib64',
|
|
41
|
+
'/sys', '/proc', '/dev', '/boot', '/run',
|
|
42
|
+
];
|
|
43
|
+
function isPathSafe(resolvedPath) {
|
|
44
|
+
const normalized = resolvedPath.endsWith('/') ? resolvedPath : resolvedPath + '/';
|
|
45
|
+
return !DENIED_PATH_PREFIXES.some((prefix) => normalized.startsWith(prefix + '/') || normalized === prefix + '/');
|
|
46
|
+
}
|
|
47
|
+
function deriveProjectName(projectPath) {
|
|
48
|
+
return path_1.default.basename(projectPath);
|
|
49
|
+
}
|
|
50
|
+
function hasCommandFiles(dir) {
|
|
51
|
+
try {
|
|
52
|
+
return fs_1.default.readdirSync(dir).some((f) => f.endsWith('.md'));
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function hasSpecrails(projectPath) {
|
|
59
|
+
return hasCommandFiles(path_1.default.join(projectPath, '.claude', 'commands', 'sr'))
|
|
60
|
+
|| hasCommandFiles(path_1.default.join(projectPath, '.claude', 'commands', 'specrails'));
|
|
61
|
+
}
|
|
62
|
+
function canonicalizePath(resolvedPath) {
|
|
63
|
+
try {
|
|
64
|
+
return fs_1.default.realpathSync(resolvedPath);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return resolvedPath;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
function isLoopbackHost(hostname) {
|
|
71
|
+
const host = hostname.toLowerCase();
|
|
72
|
+
return host === 'localhost' || host === '127.0.0.1' || host === '::1';
|
|
73
|
+
}
|
|
74
|
+
function isPrivateIp(hostname) {
|
|
75
|
+
const ipVersion = net_1.default.isIP(hostname);
|
|
76
|
+
if (ipVersion === 0)
|
|
77
|
+
return false;
|
|
78
|
+
if (ipVersion === 6) {
|
|
79
|
+
const host = hostname.toLowerCase();
|
|
80
|
+
return host === '::1' || host.startsWith('fc') || host.startsWith('fd') || host.startsWith('fe80:');
|
|
81
|
+
}
|
|
82
|
+
const parts = hostname.split('.').map((p) => Number.parseInt(p, 10));
|
|
83
|
+
const [a, b] = parts;
|
|
84
|
+
return a === 0 ||
|
|
85
|
+
a === 10 ||
|
|
86
|
+
a === 127 ||
|
|
87
|
+
(a === 169 && b === 254) ||
|
|
88
|
+
(a === 172 && b >= 16 && b <= 31) ||
|
|
89
|
+
(a === 192 && b === 168);
|
|
90
|
+
}
|
|
91
|
+
function validateHttpUrl(raw, opts) {
|
|
92
|
+
let parsed;
|
|
93
|
+
try {
|
|
94
|
+
parsed = new URL(raw);
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
100
|
+
return null;
|
|
101
|
+
if (opts.requireHttps && parsed.protocol !== 'https:') {
|
|
102
|
+
if (!opts.allowLoopback || !isLoopbackHost(parsed.hostname))
|
|
103
|
+
return null;
|
|
104
|
+
}
|
|
105
|
+
if (!opts.allowLoopback && (isLoopbackHost(parsed.hostname) || isPrivateIp(parsed.hostname)))
|
|
106
|
+
return null;
|
|
107
|
+
return parsed.toString().replace(/\/$/, '');
|
|
108
|
+
}
|
|
109
|
+
function publicWebhook(row) {
|
|
110
|
+
if (!row)
|
|
111
|
+
return row;
|
|
112
|
+
const { secret: _secret, ...rest } = row;
|
|
113
|
+
return { ...rest, hasSecret: row.secret.length > 0 };
|
|
114
|
+
}
|
|
115
|
+
function createDesktopRouter(registry, broadcast) {
|
|
116
|
+
const router = (0, express_1.Router)();
|
|
117
|
+
// GET /api/projects — list all registered projects
|
|
118
|
+
router.get('/projects', (_req, res) => {
|
|
119
|
+
const projects = (0, desktop_db_1.listProjects)(registry.desktopDb);
|
|
120
|
+
// Detect projects that are currently in the setup wizard so the client
|
|
121
|
+
// can restore the wizard after a page refresh.
|
|
122
|
+
const setupProjectIds = [];
|
|
123
|
+
for (const p of projects) {
|
|
124
|
+
const ctx = registry.getContext(p.id);
|
|
125
|
+
if (!ctx)
|
|
126
|
+
continue;
|
|
127
|
+
const installing = ctx.setupManager.isInstalling(p.id);
|
|
128
|
+
const settingUp = ctx.setupManager.isSettingUp(p.id);
|
|
129
|
+
const hasSession = !!(0, desktop_db_1.getProjectSetupSession)(registry.desktopDb, p.id);
|
|
130
|
+
const specrailsInstalled = hasSpecrails(p.path);
|
|
131
|
+
if (installing || settingUp || (hasSession && !specrailsInstalled)) {
|
|
132
|
+
setupProjectIds.push(p.id);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
res.json({ projects, setupProjectIds });
|
|
136
|
+
});
|
|
137
|
+
// GET /api/available-providers — which AI CLIs are installed, plus supported install tiers
|
|
138
|
+
//
|
|
139
|
+
// Codex (OpenAI) is supported as a first-class provider as of Stage C of
|
|
140
|
+
// the multi-provider work. The `SPECRAILS_CODEX_BETA=0` env var is honoured
|
|
141
|
+
// as an emergency rollback (forces codex back to "unavailable" in the UI
|
|
142
|
+
// without redeploying) — unset or `1` reports the real detection.
|
|
143
|
+
router.get('/available-providers', (_req, res) => {
|
|
144
|
+
const providers = (0, core_compat_1.detectAvailableCLIs)();
|
|
145
|
+
// tiers: quick install is always available (app-driven config); full requires an AI CLI
|
|
146
|
+
const tiers = ['quick'];
|
|
147
|
+
if (providers.claude || providers.codex)
|
|
148
|
+
tiers.push('full');
|
|
149
|
+
const codexBetaOff = isCodexBetaDisabled();
|
|
150
|
+
res.json({
|
|
151
|
+
claude: providers.claude,
|
|
152
|
+
codex: codexBetaOff ? false : providers.codex,
|
|
153
|
+
tiers,
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
router.get('/setup-prerequisites', (req, res) => {
|
|
157
|
+
const status = (0, setup_prerequisites_1.getSetupPrerequisitesStatus)();
|
|
158
|
+
if (req.query.diagnostic === '1') {
|
|
159
|
+
const diag = (0, path_resolver_1.getPathDiagnostic)();
|
|
160
|
+
const whichResults = {};
|
|
161
|
+
for (const item of status.prerequisites) {
|
|
162
|
+
whichResults[item.command] = item.resolvedPath ?? null;
|
|
163
|
+
}
|
|
164
|
+
res.json({
|
|
165
|
+
...status,
|
|
166
|
+
diagnostic: {
|
|
167
|
+
pathSegments: diag.pathSegments,
|
|
168
|
+
pathSources: diag.pathSources,
|
|
169
|
+
loginShellStatus: diag.loginShellStatus,
|
|
170
|
+
whichResults,
|
|
171
|
+
nodeEnv: process.env.NODE_ENV ?? null,
|
|
172
|
+
platform: status.platform,
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
res.json(status);
|
|
178
|
+
});
|
|
179
|
+
// POST /api/projects — register a new project by path
|
|
180
|
+
router.post('/projects', (req, res) => {
|
|
181
|
+
const { path: projectPath, name, provider, providers: providersRaw } = req.body ?? {};
|
|
182
|
+
if (!projectPath || typeof projectPath !== 'string') {
|
|
183
|
+
res.status(400).json({ error: 'path is required' });
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
// Normalise to a providers list. New multi-provider clients send
|
|
187
|
+
// `providers: ['claude','codex']`; legacy clients send a single
|
|
188
|
+
// `provider`; omitting both defaults to ['claude']. The first entry is the
|
|
189
|
+
// primary/default provider.
|
|
190
|
+
let providers;
|
|
191
|
+
if (Array.isArray(providersRaw) && providersRaw.length > 0) {
|
|
192
|
+
providers = providersRaw;
|
|
193
|
+
}
|
|
194
|
+
else if (typeof provider === 'string') {
|
|
195
|
+
providers = [provider];
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
providers = ['claude'];
|
|
199
|
+
}
|
|
200
|
+
// De-duplicate while preserving order (primary stays first).
|
|
201
|
+
providers = providers.filter((p, i) => providers.indexOf(p) === i);
|
|
202
|
+
// Provider validation walks the registry — `claude` and `codex` are
|
|
203
|
+
// both accepted as of Stage C; future providers register one adapter
|
|
204
|
+
// file and become acceptable here without further changes.
|
|
205
|
+
for (const p of providers) {
|
|
206
|
+
if (!(0, providers_1.hasAdapter)(p)) {
|
|
207
|
+
res.status(400).json({
|
|
208
|
+
error: `provider must be one of: ${[...(0, providers_1.listAdapters)().map((a) => a.id)].join(', ')}`,
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
// Beta-gate parity: if codex beta is forced off via env, refuse codex
|
|
214
|
+
// selections too (consistency with /available-providers).
|
|
215
|
+
if (providers.includes('codex') && isCodexBetaDisabled()) {
|
|
216
|
+
res.status(400).json({
|
|
217
|
+
error: 'Codex provider is currently disabled (SPECRAILS_CODEX_BETA=0). Unset or set to 1 to enable.',
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const resolvedPath = path_1.default.resolve(projectPath);
|
|
222
|
+
// Validate path exists
|
|
223
|
+
if (!fs_1.default.existsSync(resolvedPath)) {
|
|
224
|
+
res.status(400).json({ error: `Path does not exist: ${resolvedPath}` });
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
const canonicalPath = canonicalizePath(resolvedPath);
|
|
228
|
+
// LOW-04: Reject registration of system-critical directories
|
|
229
|
+
if (!isPathSafe(canonicalPath)) {
|
|
230
|
+
res.status(400).json({ error: 'Registering system directories is not allowed' });
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
const derivedName = (name && typeof name === 'string' && name.trim())
|
|
234
|
+
? name.trim()
|
|
235
|
+
: deriveProjectName(canonicalPath);
|
|
236
|
+
const slug = slugify(derivedName);
|
|
237
|
+
const id = (0, crypto_1.randomUUID)();
|
|
238
|
+
const specrailsInstalled = hasSpecrails(canonicalPath);
|
|
239
|
+
try {
|
|
240
|
+
const ctx = registry.addProject({
|
|
241
|
+
id,
|
|
242
|
+
slug,
|
|
243
|
+
name: derivedName,
|
|
244
|
+
path: canonicalPath,
|
|
245
|
+
provider: providers[0],
|
|
246
|
+
providers: providers,
|
|
247
|
+
});
|
|
248
|
+
broadcast({
|
|
249
|
+
type: 'desktop.project_added',
|
|
250
|
+
project: ctx.project,
|
|
251
|
+
timestamp: new Date().toISOString(),
|
|
252
|
+
});
|
|
253
|
+
res.status(201).json({ project: ctx.project, has_specrails: specrailsInstalled });
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
const message = err.message ?? '';
|
|
257
|
+
// SQLite UNIQUE constraint violation means path or slug already registered
|
|
258
|
+
if (message.includes('UNIQUE')) {
|
|
259
|
+
res.status(409).json({ error: 'A project with this path is already registered' });
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
console.error('[desktop] add project error:', err);
|
|
263
|
+
res.status(500).json({ error: 'Failed to register project' });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
// DELETE /api/projects/:id — unregister a project
|
|
268
|
+
router.delete('/projects/:id', (req, res) => {
|
|
269
|
+
const { id } = req.params;
|
|
270
|
+
const ctx = registry.getContext(id);
|
|
271
|
+
if (!ctx) {
|
|
272
|
+
res.status(404).json({ error: 'Project not found' });
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
registry.removeProject(id);
|
|
276
|
+
broadcast({
|
|
277
|
+
type: 'desktop.project_removed',
|
|
278
|
+
projectId: id,
|
|
279
|
+
timestamp: new Date().toISOString(),
|
|
280
|
+
});
|
|
281
|
+
res.json({ ok: true });
|
|
282
|
+
});
|
|
283
|
+
// GET /api/state — app-level state summary
|
|
284
|
+
router.get('/state', (_req, res) => {
|
|
285
|
+
const projects = (0, desktop_db_1.listProjects)(registry.desktopDb);
|
|
286
|
+
const todayStats = (0, desktop_analytics_1.getDesktopTodayStats)(registry);
|
|
287
|
+
res.json({
|
|
288
|
+
projects,
|
|
289
|
+
projectCount: projects.length,
|
|
290
|
+
...todayStats,
|
|
291
|
+
});
|
|
292
|
+
});
|
|
293
|
+
// GET /api/analytics?period= — cross-project aggregated analytics
|
|
294
|
+
router.get('/analytics', (req, res) => {
|
|
295
|
+
const period = req.query.period ?? '7d';
|
|
296
|
+
const from = req.query.from;
|
|
297
|
+
const to = req.query.to;
|
|
298
|
+
const opts = { period, from, to };
|
|
299
|
+
const result = (0, desktop_analytics_1.getDesktopAnalytics)(registry, opts);
|
|
300
|
+
res.json(result);
|
|
301
|
+
});
|
|
302
|
+
// GET /api/recent-jobs?limit= — last N jobs across all projects
|
|
303
|
+
router.get('/recent-jobs', (req, res) => {
|
|
304
|
+
const limit = Math.min(Math.max(parseInt(req.query.limit ?? '10', 10) || 10, 1), 50);
|
|
305
|
+
const jobs = (0, desktop_analytics_1.getDesktopRecentJobs)(registry, limit);
|
|
306
|
+
res.json({ jobs });
|
|
307
|
+
});
|
|
308
|
+
// GET /api/resolve?path=<cwd> — resolve a project from a filesystem path
|
|
309
|
+
router.get('/resolve', (req, res) => {
|
|
310
|
+
const queryPath = req.query.path;
|
|
311
|
+
if (!queryPath) {
|
|
312
|
+
res.status(400).json({ error: 'path query parameter is required' });
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
const resolvedPath = canonicalizePath(path_1.default.resolve(queryPath));
|
|
316
|
+
const ctx = registry.getContextByPath(resolvedPath);
|
|
317
|
+
if (!ctx) {
|
|
318
|
+
res.status(404).json({ error: 'No project registered for this path' });
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
registry.touchProject(ctx.project.id);
|
|
322
|
+
res.json({ project: ctx.project });
|
|
323
|
+
});
|
|
324
|
+
// GET /api/settings — get app-level settings
|
|
325
|
+
router.get('/settings', (_req, res) => {
|
|
326
|
+
const port = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'port') ?? '4200';
|
|
327
|
+
const specrailsTechUrl = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'specrails_tech_url') ??
|
|
328
|
+
process.env.SPECRAILS_TECH_URL ??
|
|
329
|
+
'http://localhost:3000';
|
|
330
|
+
const costAlertThresholdRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'cost_alert_threshold_usd');
|
|
331
|
+
const costAlertThresholdUsd = costAlertThresholdRaw != null ? parseFloat(costAlertThresholdRaw) : null;
|
|
332
|
+
res.json({ port: parseInt(port, 10), specrailsTechUrl, costAlertThresholdUsd });
|
|
333
|
+
});
|
|
334
|
+
// PUT /api/settings — update app-level settings
|
|
335
|
+
router.put('/settings', (req, res) => {
|
|
336
|
+
const { port, specrailsTechUrl, costAlertThresholdUsd } = req.body ?? {};
|
|
337
|
+
if (port !== undefined) {
|
|
338
|
+
const n = Number(port);
|
|
339
|
+
if (!Number.isInteger(n) || n < 1 || n > 65535) {
|
|
340
|
+
res.status(400).json({ error: 'port must be an integer between 1 and 65535' });
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'port', String(n));
|
|
344
|
+
}
|
|
345
|
+
if (specrailsTechUrl !== undefined && typeof specrailsTechUrl === 'string') {
|
|
346
|
+
const normalized = validateHttpUrl(specrailsTechUrl.trim(), {
|
|
347
|
+
allowLoopback: true,
|
|
348
|
+
requireHttps: false,
|
|
349
|
+
});
|
|
350
|
+
if (!normalized) {
|
|
351
|
+
res.status(400).json({ error: 'specrailsTechUrl must be a valid http(s) URL' });
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'specrails_tech_url', normalized);
|
|
355
|
+
}
|
|
356
|
+
if (costAlertThresholdUsd !== undefined) {
|
|
357
|
+
if (costAlertThresholdUsd === null) {
|
|
358
|
+
registry.desktopDb.prepare('DELETE FROM desktop_settings WHERE key = ?').run('cost_alert_threshold_usd');
|
|
359
|
+
}
|
|
360
|
+
else if (typeof costAlertThresholdUsd === 'number' && costAlertThresholdUsd > 0) {
|
|
361
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'cost_alert_threshold_usd', String(costAlertThresholdUsd));
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
res.json({ ok: true });
|
|
365
|
+
});
|
|
366
|
+
// ─── Budget routes ────────────────────────────────────────────────────────────
|
|
367
|
+
// GET /api/budget — get app-level budget status
|
|
368
|
+
router.get('/budget', (_req, res) => {
|
|
369
|
+
const desktopDailyBudgetRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'desktop_daily_budget_usd');
|
|
370
|
+
const desktopDailyBudgetUsd = desktopDailyBudgetRaw != null ? parseFloat(desktopDailyBudgetRaw) : null;
|
|
371
|
+
const costAlertRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'cost_alert_threshold_usd');
|
|
372
|
+
const costAlertThresholdUsd = costAlertRaw != null ? parseFloat(costAlertRaw) : null;
|
|
373
|
+
const { costToday } = (0, desktop_analytics_1.getDesktopTodayStats)(registry);
|
|
374
|
+
const budgetUtilizationPct = desktopDailyBudgetUsd != null && desktopDailyBudgetUsd > 0
|
|
375
|
+
? (costToday / desktopDailyBudgetUsd) * 100
|
|
376
|
+
: null;
|
|
377
|
+
res.json({ desktopDailyBudgetUsd, costAlertThresholdUsd, costToday, budgetUtilizationPct });
|
|
378
|
+
});
|
|
379
|
+
// PATCH /api/budget — update app-level budget settings
|
|
380
|
+
router.patch('/budget', (req, res) => {
|
|
381
|
+
const { desktopDailyBudgetUsd, costAlertThresholdUsd } = req.body ?? {};
|
|
382
|
+
if (desktopDailyBudgetUsd !== undefined) {
|
|
383
|
+
if (desktopDailyBudgetUsd === null) {
|
|
384
|
+
registry.desktopDb.prepare('DELETE FROM desktop_settings WHERE key = ?').run('desktop_daily_budget_usd');
|
|
385
|
+
}
|
|
386
|
+
else if (typeof desktopDailyBudgetUsd === 'number' && desktopDailyBudgetUsd > 0) {
|
|
387
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'desktop_daily_budget_usd', String(desktopDailyBudgetUsd));
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
if (costAlertThresholdUsd !== undefined) {
|
|
391
|
+
if (costAlertThresholdUsd === null) {
|
|
392
|
+
registry.desktopDb.prepare('DELETE FROM desktop_settings WHERE key = ?').run('cost_alert_threshold_usd');
|
|
393
|
+
}
|
|
394
|
+
else if (typeof costAlertThresholdUsd === 'number' && costAlertThresholdUsd > 0) {
|
|
395
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'cost_alert_threshold_usd', String(costAlertThresholdUsd));
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
res.json({ ok: true });
|
|
399
|
+
});
|
|
400
|
+
// ─── Agent routes ────────────────────────────────────────────────────────────
|
|
401
|
+
// GET /api/agents — list all registered agents
|
|
402
|
+
router.get('/agents', (_req, res) => {
|
|
403
|
+
res.json({ agents: (0, desktop_db_1.listAgents)(registry.desktopDb) });
|
|
404
|
+
});
|
|
405
|
+
// GET /api/agents/:id — get agent by ID
|
|
406
|
+
router.get('/agents/:id', (req, res) => {
|
|
407
|
+
const agent = (0, desktop_db_1.getAgent)(registry.desktopDb, req.params.id);
|
|
408
|
+
if (!agent) {
|
|
409
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
res.json({ agent });
|
|
413
|
+
});
|
|
414
|
+
// POST /api/agents — register a new agent
|
|
415
|
+
router.post('/agents', (req, res) => {
|
|
416
|
+
const { slug, name, role, config } = req.body ?? {};
|
|
417
|
+
if (!slug || typeof slug !== 'string') {
|
|
418
|
+
res.status(400).json({ error: 'slug is required' });
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (!name || typeof name !== 'string') {
|
|
422
|
+
res.status(400).json({ error: 'name is required' });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const id = (0, crypto_1.randomUUID)();
|
|
426
|
+
try {
|
|
427
|
+
const agent = (0, desktop_db_1.addAgent)(registry.desktopDb, { id, slug, name, role, config });
|
|
428
|
+
res.status(201).json({ agent });
|
|
429
|
+
}
|
|
430
|
+
catch (err) {
|
|
431
|
+
const message = err.message ?? '';
|
|
432
|
+
if (message.includes('UNIQUE')) {
|
|
433
|
+
res.status(409).json({ error: 'An agent with this slug already exists' });
|
|
434
|
+
}
|
|
435
|
+
else {
|
|
436
|
+
console.error('[desktop] add agent error:', err);
|
|
437
|
+
res.status(500).json({ error: 'Failed to register agent' });
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
});
|
|
441
|
+
// PATCH /api/agents/:id — update agent fields
|
|
442
|
+
router.patch('/agents/:id', (req, res) => {
|
|
443
|
+
const agent = (0, desktop_db_1.getAgent)(registry.desktopDb, req.params.id);
|
|
444
|
+
if (!agent) {
|
|
445
|
+
res.status(404).json({ error: 'Agent not found' });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
const { name, role, status, current_job_id, last_heartbeat_at, config } = req.body ?? {};
|
|
449
|
+
const updates = {};
|
|
450
|
+
if (name !== undefined)
|
|
451
|
+
updates.name = name;
|
|
452
|
+
if (role !== undefined)
|
|
453
|
+
updates.role = role;
|
|
454
|
+
if (status !== undefined)
|
|
455
|
+
updates.status = status;
|
|
456
|
+
if (current_job_id !== undefined)
|
|
457
|
+
updates.current_job_id = current_job_id;
|
|
458
|
+
if (last_heartbeat_at !== undefined)
|
|
459
|
+
updates.last_heartbeat_at = last_heartbeat_at;
|
|
460
|
+
if (config !== undefined)
|
|
461
|
+
updates.config = config;
|
|
462
|
+
const updated = (0, desktop_db_1.updateAgent)(registry.desktopDb, req.params.id, updates);
|
|
463
|
+
res.json({ agent: updated });
|
|
464
|
+
});
|
|
465
|
+
// GET /api/core-compat — compatibility status between the app and specrails-core
|
|
466
|
+
router.get('/core-compat', async (_req, res) => {
|
|
467
|
+
const result = await (0, core_compat_1.checkCoreCompat)();
|
|
468
|
+
res.json(result);
|
|
469
|
+
});
|
|
470
|
+
// GET /api/cli-status — detected AI CLI provider and version
|
|
471
|
+
router.get('/cli-status', (_req, res) => {
|
|
472
|
+
res.json((0, core_compat_1.getCLIStatus)());
|
|
473
|
+
});
|
|
474
|
+
// ─── specrails-tech proxy routes ────────────────────────────────────────────
|
|
475
|
+
function getSpecrailsTechClient() {
|
|
476
|
+
const url = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'specrails_tech_url') ??
|
|
477
|
+
process.env.SPECRAILS_TECH_URL ??
|
|
478
|
+
'http://localhost:3000';
|
|
479
|
+
return (0, specrails_tech_client_1.createSpecrailsTechClient)(url);
|
|
480
|
+
}
|
|
481
|
+
// GET /api/specrails-tech/status — health + connected flag
|
|
482
|
+
router.get('/specrails-tech/status', async (_req, res) => {
|
|
483
|
+
const client = getSpecrailsTechClient();
|
|
484
|
+
const result = await client.health();
|
|
485
|
+
if (!result.connected) {
|
|
486
|
+
res.json({ connected: false, error: result.error });
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
res.json({ connected: true, data: result.data });
|
|
490
|
+
});
|
|
491
|
+
// GET /api/specrails-tech/agents — list agents
|
|
492
|
+
router.get('/specrails-tech/agents', async (_req, res) => {
|
|
493
|
+
const client = getSpecrailsTechClient();
|
|
494
|
+
const result = await client.listAgents();
|
|
495
|
+
if (!result.connected) {
|
|
496
|
+
res.json({ connected: false, error: result.error, data: [] });
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
res.json({ connected: true, data: result.data });
|
|
500
|
+
});
|
|
501
|
+
// GET /api/specrails-tech/agents/:slug — agent detail
|
|
502
|
+
router.get('/specrails-tech/agents/:slug', async (req, res) => {
|
|
503
|
+
const client = getSpecrailsTechClient();
|
|
504
|
+
const result = await client.getAgent(req.params.slug);
|
|
505
|
+
if (!result.connected) {
|
|
506
|
+
res.status(503).json({ connected: false, error: result.error });
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
res.json({ connected: true, data: result.data });
|
|
510
|
+
});
|
|
511
|
+
// GET /api/specrails-tech/docs — list docs
|
|
512
|
+
router.get('/specrails-tech/docs', async (_req, res) => {
|
|
513
|
+
const client = getSpecrailsTechClient();
|
|
514
|
+
const result = await client.listDocs();
|
|
515
|
+
if (!result.connected) {
|
|
516
|
+
res.json({ connected: false, error: result.error, data: [] });
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
res.json({ connected: true, data: result.data });
|
|
520
|
+
});
|
|
521
|
+
// GET /api/specrails-tech/docs/:page — doc page detail
|
|
522
|
+
router.get('/specrails-tech/docs/:page', async (req, res) => {
|
|
523
|
+
const client = getSpecrailsTechClient();
|
|
524
|
+
const result = await client.getDoc(req.params.page);
|
|
525
|
+
if (!result.connected) {
|
|
526
|
+
res.status(503).json({ connected: false, error: result.error });
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
res.json({ connected: true, data: result.data });
|
|
530
|
+
});
|
|
531
|
+
// ─── Webhook routes ──────────────────────────────────────────────────────────
|
|
532
|
+
const webhookManager = new webhook_manager_1.WebhookManager(registry.desktopDb);
|
|
533
|
+
// GET /api/webhooks — list all webhooks
|
|
534
|
+
router.get('/webhooks', (_req, res) => {
|
|
535
|
+
res.json({ webhooks: (0, desktop_db_1.listWebhooks)(registry.desktopDb).map(publicWebhook) });
|
|
536
|
+
});
|
|
537
|
+
// POST /api/webhooks — create a webhook
|
|
538
|
+
router.post('/webhooks', (req, res) => {
|
|
539
|
+
const { url, secret, events, projectId } = req.body ?? {};
|
|
540
|
+
if (!url || typeof url !== 'string') {
|
|
541
|
+
res.status(400).json({ error: 'url is required' });
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
const validEvents = ['job.completed', 'job.failed', 'job.canceled', 'daily_budget_exceeded', 'desktop_daily_budget_exceeded'];
|
|
545
|
+
const parsedEvents = Array.isArray(events)
|
|
546
|
+
? events.filter((e) => validEvents.includes(e))
|
|
547
|
+
: ['job.completed', 'job.failed', 'job.canceled'];
|
|
548
|
+
if (parsedEvents.length === 0) {
|
|
549
|
+
res.status(400).json({ error: 'at least one valid event is required' });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
if (projectId != null) {
|
|
553
|
+
const ctx = registry.getContext(projectId);
|
|
554
|
+
if (!ctx) {
|
|
555
|
+
res.status(400).json({ error: 'project not found' });
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
const normalizedUrl = validateHttpUrl(url.trim(), {
|
|
560
|
+
allowLoopback: process.env.SPECRAILS_ALLOW_LOCAL_WEBHOOKS === '1',
|
|
561
|
+
requireHttps: true,
|
|
562
|
+
});
|
|
563
|
+
if (!normalizedUrl) {
|
|
564
|
+
res.status(400).json({ error: 'webhook url must be https and must not target localhost/private IPs' });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const webhook = (0, desktop_db_1.addWebhook)(registry.desktopDb, {
|
|
568
|
+
id: (0, crypto_1.randomUUID)(),
|
|
569
|
+
projectId: projectId ?? null,
|
|
570
|
+
url: normalizedUrl,
|
|
571
|
+
secret: typeof secret === 'string' ? secret.trim() : '',
|
|
572
|
+
events: parsedEvents,
|
|
573
|
+
});
|
|
574
|
+
res.status(201).json({ webhook: publicWebhook(webhook) });
|
|
575
|
+
});
|
|
576
|
+
// PATCH /api/webhooks/:id — update a webhook
|
|
577
|
+
router.patch('/webhooks/:id', (req, res) => {
|
|
578
|
+
const existing = (0, desktop_db_1.getWebhook)(registry.desktopDb, req.params.id);
|
|
579
|
+
if (!existing) {
|
|
580
|
+
res.status(404).json({ error: 'Webhook not found' });
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const { url, secret, events, enabled } = req.body ?? {};
|
|
584
|
+
const validEvents = ['job.completed', 'job.failed', 'job.canceled', 'daily_budget_exceeded', 'desktop_daily_budget_exceeded'];
|
|
585
|
+
const parsedEvents = Array.isArray(events)
|
|
586
|
+
? events.filter((e) => validEvents.includes(e))
|
|
587
|
+
: undefined;
|
|
588
|
+
let normalizedUrl;
|
|
589
|
+
if (typeof url === 'string') {
|
|
590
|
+
const candidate = validateHttpUrl(url.trim(), {
|
|
591
|
+
allowLoopback: process.env.SPECRAILS_ALLOW_LOCAL_WEBHOOKS === '1',
|
|
592
|
+
requireHttps: true,
|
|
593
|
+
});
|
|
594
|
+
if (!candidate) {
|
|
595
|
+
res.status(400).json({ error: 'webhook url must be https and must not target localhost/private IPs' });
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
normalizedUrl = candidate;
|
|
599
|
+
}
|
|
600
|
+
const updated = (0, desktop_db_1.updateWebhook)(registry.desktopDb, req.params.id, {
|
|
601
|
+
url: normalizedUrl,
|
|
602
|
+
secret: typeof secret === 'string' ? secret.trim() : undefined,
|
|
603
|
+
events: parsedEvents,
|
|
604
|
+
enabled: typeof enabled === 'boolean' ? enabled : undefined,
|
|
605
|
+
});
|
|
606
|
+
res.json({ webhook: publicWebhook(updated) });
|
|
607
|
+
});
|
|
608
|
+
// DELETE /api/webhooks/:id — delete a webhook
|
|
609
|
+
router.delete('/webhooks/:id', (req, res) => {
|
|
610
|
+
const existing = (0, desktop_db_1.getWebhook)(registry.desktopDb, req.params.id);
|
|
611
|
+
if (!existing) {
|
|
612
|
+
res.status(404).json({ error: 'Webhook not found' });
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
(0, desktop_db_1.removeWebhook)(registry.desktopDb, req.params.id);
|
|
616
|
+
res.json({ ok: true });
|
|
617
|
+
});
|
|
618
|
+
// POST /api/webhooks/:id/test — send a test ping
|
|
619
|
+
router.post('/webhooks/:id/test', (req, res) => {
|
|
620
|
+
const webhook = (0, desktop_db_1.getWebhook)(registry.desktopDb, req.params.id);
|
|
621
|
+
if (!webhook) {
|
|
622
|
+
res.status(404).json({ error: 'Webhook not found' });
|
|
623
|
+
return;
|
|
624
|
+
}
|
|
625
|
+
webhookManager.deliverTest(webhook);
|
|
626
|
+
res.json({ ok: true, message: 'Test ping queued' });
|
|
627
|
+
});
|
|
628
|
+
// GET /api/terminal-settings — Desktop-wide terminal defaults
|
|
629
|
+
router.get('/terminal-settings', (_req, res) => {
|
|
630
|
+
res.json((0, terminal_settings_1.getDesktopTerminalSettings)(registry.desktopDb));
|
|
631
|
+
});
|
|
632
|
+
// PATCH /api/terminal-settings — partial update of Desktop-wide defaults
|
|
633
|
+
router.patch('/terminal-settings', (req, res) => {
|
|
634
|
+
if (!req.body || typeof req.body !== 'object' || Array.isArray(req.body)) {
|
|
635
|
+
res.status(400).json({ error: 'invalid body' });
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
try {
|
|
639
|
+
const updated = (0, terminal_settings_1.patchDesktopTerminalSettings)(registry.desktopDb, req.body);
|
|
640
|
+
res.json(updated);
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
if (err instanceof terminal_settings_1.TerminalSettingsValidationError) {
|
|
644
|
+
res.status(400).json({ error: 'validation_failed', field: err.field, message: err.message });
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
throw err;
|
|
648
|
+
}
|
|
649
|
+
});
|
|
650
|
+
// ─── Theme (app-wide UI theme) ────────────────────────────────────────────
|
|
651
|
+
// Allow-list synchronized with `client/src/lib/themes.ts THEME_IDS`.
|
|
652
|
+
// Persisted under desktop_settings key `ui_theme`. Default seeded by migration 8.
|
|
653
|
+
router.get('/theme', (_req, res) => {
|
|
654
|
+
const stored = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'ui_theme');
|
|
655
|
+
const theme = stored && THEME_ID_ALLOWLIST.has(stored) ? stored : 'specrails';
|
|
656
|
+
res.json({ theme });
|
|
657
|
+
});
|
|
658
|
+
router.patch('/theme', (req, res) => {
|
|
659
|
+
const next = req.body?.theme;
|
|
660
|
+
if (typeof next !== 'string' || !THEME_ID_ALLOWLIST.has(next)) {
|
|
661
|
+
res.status(400).json({
|
|
662
|
+
error: 'invalid_theme',
|
|
663
|
+
message: `theme must be one of: ${[...THEME_ID_ALLOWLIST].join(', ')}`,
|
|
664
|
+
});
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'ui_theme', next);
|
|
668
|
+
res.json({ theme: next });
|
|
669
|
+
});
|
|
670
|
+
// ─── Language (app-wide UI language) ──────────────────────────────────────
|
|
671
|
+
// Allow-list synchronized with `client/src/lib/i18n.ts LANGUAGE_IDS`.
|
|
672
|
+
// Persisted under desktop_settings key `ui_language`. No default is seeded:
|
|
673
|
+
// `language: null` means "user never chose" and the client keeps following
|
|
674
|
+
// the OS/browser language until an explicit choice is PATCHed.
|
|
675
|
+
router.get('/language', (_req, res) => {
|
|
676
|
+
const stored = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'ui_language');
|
|
677
|
+
const language = stored && LANGUAGE_ID_ALLOWLIST.has(stored) ? stored : null;
|
|
678
|
+
res.json({ language });
|
|
679
|
+
});
|
|
680
|
+
router.patch('/language', (req, res) => {
|
|
681
|
+
const next = req.body?.language;
|
|
682
|
+
if (typeof next !== 'string' || !LANGUAGE_ID_ALLOWLIST.has(next)) {
|
|
683
|
+
res.status(400).json({
|
|
684
|
+
error: 'invalid_language',
|
|
685
|
+
message: `language must be one of: ${[...LANGUAGE_ID_ALLOWLIST].join(', ')}`,
|
|
686
|
+
});
|
|
687
|
+
return;
|
|
688
|
+
}
|
|
689
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'ui_language', next);
|
|
690
|
+
res.json({ language: next });
|
|
691
|
+
});
|
|
692
|
+
// ─── Code Explorer settings (summary language + monthly budget) ───────────
|
|
693
|
+
router.get('/code-explorer-settings', (_req, res) => {
|
|
694
|
+
const langRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'summary_language');
|
|
695
|
+
const language = langRaw === 'es' ? 'es' : 'en';
|
|
696
|
+
const budgetRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'summary_monthly_budget_usd');
|
|
697
|
+
const parsed = budgetRaw !== undefined ? Number(budgetRaw) : NaN;
|
|
698
|
+
const monthlyBudgetUsd = Number.isFinite(parsed) && parsed >= 0 ? parsed : 5.0;
|
|
699
|
+
res.json({ language, monthlyBudgetUsd });
|
|
700
|
+
});
|
|
701
|
+
router.patch('/code-explorer-settings', (req, res) => {
|
|
702
|
+
const body = (req.body ?? {});
|
|
703
|
+
if (body.language !== undefined) {
|
|
704
|
+
if (body.language !== 'en' && body.language !== 'es') {
|
|
705
|
+
res.status(400).json({
|
|
706
|
+
error: 'invalid_language',
|
|
707
|
+
message: "language must be one of: 'en', 'es'",
|
|
708
|
+
});
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
if (body.monthlyBudgetUsd !== undefined) {
|
|
713
|
+
if (typeof body.monthlyBudgetUsd !== 'number' || !Number.isFinite(body.monthlyBudgetUsd) || body.monthlyBudgetUsd < 0) {
|
|
714
|
+
res.status(400).json({
|
|
715
|
+
error: 'invalid_monthly_budget_usd',
|
|
716
|
+
message: 'monthlyBudgetUsd must be a non-negative number',
|
|
717
|
+
});
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
if (body.language !== undefined) {
|
|
722
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'summary_language', body.language);
|
|
723
|
+
}
|
|
724
|
+
if (body.monthlyBudgetUsd !== undefined) {
|
|
725
|
+
(0, desktop_db_1.setDesktopSetting)(registry.desktopDb, 'summary_monthly_budget_usd', String(body.monthlyBudgetUsd));
|
|
726
|
+
}
|
|
727
|
+
const langRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'summary_language');
|
|
728
|
+
const language = langRaw === 'es' ? 'es' : 'en';
|
|
729
|
+
const budgetRaw = (0, desktop_db_1.getDesktopSetting)(registry.desktopDb, 'summary_monthly_budget_usd');
|
|
730
|
+
const parsed = budgetRaw !== undefined ? Number(budgetRaw) : NaN;
|
|
731
|
+
const monthlyBudgetUsd = Number.isFinite(parsed) && parsed >= 0 ? parsed : 5.0;
|
|
732
|
+
res.json({ language, monthlyBudgetUsd });
|
|
733
|
+
});
|
|
734
|
+
return router;
|
|
735
|
+
}
|