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,788 @@
|
|
|
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.createCodeExplorerRouter = createCodeExplorerRouter;
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const express_1 = require("express");
|
|
11
|
+
const feature_flags_1 = require("./feature-flags");
|
|
12
|
+
const build_dirs_1 = require("./build-dirs");
|
|
13
|
+
const file_provenance_1 = require("./file-provenance");
|
|
14
|
+
const file_summary_manager_1 = require("./file-summary-manager");
|
|
15
|
+
const MAX_TREE_PAGE = 2000;
|
|
16
|
+
const MAX_FILE_BYTES = 2 * 1024 * 1024;
|
|
17
|
+
const BINARY_PROBE_BYTES = 8 * 1024;
|
|
18
|
+
// Hard-coded app deny-list (mirrors design D8). Dotfiles are excluded by name
|
|
19
|
+
// prefix; build/dep dirs come from the shared BUILD_DIRS set (node_modules, dist,
|
|
20
|
+
// build, out, coverage, target, vendor) so the on-demand tree walk skips the same
|
|
21
|
+
// heavy trees the file-summary watcher prunes; extensions handled below.
|
|
22
|
+
const DENY_EXTS = new Set(['.lock', '.log']);
|
|
23
|
+
// Secret-bearing extensions/names blocked as defense-in-depth so credentials are
|
|
24
|
+
// never served to the (non-developer) reader even if .gitignore is missing or git
|
|
25
|
+
// is unavailable. Kept conservative to avoid hiding ordinary source files.
|
|
26
|
+
const SECRET_EXTS = new Set(['.pem', '.key', '.p12', '.pfx', '.keystore', '.jks']);
|
|
27
|
+
const SECRET_NAMES = new Set(['id_rsa', 'id_dsa', 'id_ecdsa', 'id_ed25519']);
|
|
28
|
+
const DENY_NAMES = new Set(['package-lock.json', 'pnpm-lock.yaml', 'yarn.lock']);
|
|
29
|
+
function isDenied(entryName) {
|
|
30
|
+
// Dotfiles (.env, .npmrc, .netrc, .git, …) are denied wholesale by prefix.
|
|
31
|
+
if (entryName.startsWith('.'))
|
|
32
|
+
return true;
|
|
33
|
+
// Case-insensitive for dir/lockfile names: macOS (APFS) and Windows (NTFS) are
|
|
34
|
+
// case-insensitive, so `Node_Modules` / `Package-Lock.json` resolve to the same
|
|
35
|
+
// denied path on disk and must not slip past the policy.
|
|
36
|
+
const lower = entryName.toLowerCase();
|
|
37
|
+
if (build_dirs_1.BUILD_DIRS.has(lower))
|
|
38
|
+
return true;
|
|
39
|
+
const ext = path_1.default.extname(lower);
|
|
40
|
+
if (DENY_EXTS.has(ext) || SECRET_EXTS.has(ext))
|
|
41
|
+
return true;
|
|
42
|
+
if (DENY_NAMES.has(lower) || SECRET_NAMES.has(lower))
|
|
43
|
+
return true;
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
// Apply the deny-list to ANY segment of a relative path so the policy is the
|
|
47
|
+
// single source of truth across every surface (tree walk, touched-by-ai list,
|
|
48
|
+
// and the content endpoints) — not just the top-level `all` walk.
|
|
49
|
+
function isDeniedRelPath(rel) {
|
|
50
|
+
return rel.split(/[\\/]/).filter(Boolean).some(isDenied);
|
|
51
|
+
}
|
|
52
|
+
// Normalize a client-supplied relative path to POSIX separators so summary
|
|
53
|
+
// (sha256 of relPath), provenance (git always emits '/'), and content lookups
|
|
54
|
+
// all key off ONE canonical form regardless of the request's separator style.
|
|
55
|
+
function normalizeRel(rel) {
|
|
56
|
+
return rel.split(/[\\/]/).filter((seg) => seg.length > 0).join('/');
|
|
57
|
+
}
|
|
58
|
+
// Return the subset of `relPaths` that git considers ignored (honours nested
|
|
59
|
+
// .gitignore, excludes tracked files — exactly the set we must hide). One batched
|
|
60
|
+
// `git check-ignore` spawn. Best-effort: any git failure (no repo, no git) → no
|
|
61
|
+
// paths reported, so the deny-list remains the only filter. `check-ignore` exits
|
|
62
|
+
// 1 ("none ignored") which execFileSync throws on — the matched list still lands
|
|
63
|
+
// on stdout, so both branches read stdout.
|
|
64
|
+
function gitIgnoredSet(projectPath, relPaths) {
|
|
65
|
+
if (relPaths.length === 0)
|
|
66
|
+
return new Set();
|
|
67
|
+
let out = '';
|
|
68
|
+
try {
|
|
69
|
+
out = (0, child_process_1.execFileSync)('git', ['check-ignore', '--stdin', '-z'], {
|
|
70
|
+
cwd: projectPath,
|
|
71
|
+
input: relPaths.join('\0') + '\0',
|
|
72
|
+
encoding: 'utf8',
|
|
73
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
74
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
75
|
+
timeout: 15_000,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
out = (err.stdout ?? '').toString();
|
|
80
|
+
}
|
|
81
|
+
return new Set(out.split('\0').filter((p) => p.length > 0));
|
|
82
|
+
}
|
|
83
|
+
function isGitIgnored(projectPath, relPath) {
|
|
84
|
+
return gitIgnoredSet(projectPath, [relPath]).has(relPath);
|
|
85
|
+
}
|
|
86
|
+
function languageForExt(ext) {
|
|
87
|
+
const e = ext.toLowerCase();
|
|
88
|
+
switch (e) {
|
|
89
|
+
case '.ts':
|
|
90
|
+
case '.tsx':
|
|
91
|
+
case '.cts':
|
|
92
|
+
case '.mts': return 'typescript';
|
|
93
|
+
case '.js':
|
|
94
|
+
case '.jsx':
|
|
95
|
+
case '.mjs':
|
|
96
|
+
case '.cjs': return 'javascript';
|
|
97
|
+
case '.json': return 'json';
|
|
98
|
+
case '.md': return 'markdown';
|
|
99
|
+
case '.py': return 'python';
|
|
100
|
+
case '.rs': return 'rust';
|
|
101
|
+
case '.go': return 'go';
|
|
102
|
+
case '.css': return 'css';
|
|
103
|
+
case '.html': return 'html';
|
|
104
|
+
case '.yml':
|
|
105
|
+
case '.yaml': return 'yaml';
|
|
106
|
+
case '.sh': return 'shell';
|
|
107
|
+
case '.sql': return 'sql';
|
|
108
|
+
case '.toml': return 'toml';
|
|
109
|
+
default: return 'plaintext';
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function decodeCursor(raw) {
|
|
113
|
+
if (!raw)
|
|
114
|
+
return { skip: 0 };
|
|
115
|
+
try {
|
|
116
|
+
const json = Buffer.from(raw, 'base64').toString('utf8');
|
|
117
|
+
const parsed = JSON.parse(json);
|
|
118
|
+
if (typeof parsed.skip === 'number' && parsed.skip >= 0)
|
|
119
|
+
return { skip: parsed.skip };
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// fall through to default
|
|
123
|
+
}
|
|
124
|
+
return { skip: 0 };
|
|
125
|
+
}
|
|
126
|
+
function encodeCursor(skip) {
|
|
127
|
+
return Buffer.from(JSON.stringify({ skip }), 'utf8').toString('base64');
|
|
128
|
+
}
|
|
129
|
+
function rollupProvenance(rows) {
|
|
130
|
+
let createdByTicketId = null;
|
|
131
|
+
const modifiedSet = new Set();
|
|
132
|
+
// `rows` arrives ordered by `at DESC`. Walk oldest → newest so the earliest
|
|
133
|
+
// 'created' wins for createdByTicketId.
|
|
134
|
+
for (const r of [...rows].reverse()) {
|
|
135
|
+
if (r.ticket_id == null)
|
|
136
|
+
continue;
|
|
137
|
+
if (r.kind === 'created' && createdByTicketId == null) {
|
|
138
|
+
createdByTicketId = r.ticket_id;
|
|
139
|
+
}
|
|
140
|
+
else if (r.kind === 'modified') {
|
|
141
|
+
modifiedSet.add(r.ticket_id);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
// Don't double-count the creating ticket in the modified chips list.
|
|
145
|
+
if (createdByTicketId != null)
|
|
146
|
+
modifiedSet.delete(createdByTicketId);
|
|
147
|
+
return {
|
|
148
|
+
createdByTicketId,
|
|
149
|
+
modifiedByTicketIds: [...modifiedSet],
|
|
150
|
+
latest: rows[0] ?? null,
|
|
151
|
+
touchedFileCount: 0,
|
|
152
|
+
rows,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function rollupDirectoryProvenance(rowsByPath, dirPath) {
|
|
156
|
+
const prefix = `${dirPath}/`;
|
|
157
|
+
const childRows = [];
|
|
158
|
+
let touchedFileCount = 0;
|
|
159
|
+
for (const [filePath, rows] of rowsByPath) {
|
|
160
|
+
if (!filePath.startsWith(prefix))
|
|
161
|
+
continue;
|
|
162
|
+
touchedFileCount += 1;
|
|
163
|
+
childRows.push(...rows);
|
|
164
|
+
}
|
|
165
|
+
childRows.sort((a, b) => b.at - a.at);
|
|
166
|
+
return {
|
|
167
|
+
...rollupProvenance(childRows),
|
|
168
|
+
touchedFileCount,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
function provenanceToJson(row) {
|
|
172
|
+
if (!row)
|
|
173
|
+
return null;
|
|
174
|
+
return {
|
|
175
|
+
path: row.file_path,
|
|
176
|
+
ticketId: row.ticket_id,
|
|
177
|
+
jobId: row.job_id,
|
|
178
|
+
kind: row.kind,
|
|
179
|
+
at: row.at,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
function provenanceRowsToJson(rows) {
|
|
183
|
+
return rows.map((row) => ({
|
|
184
|
+
path: row.file_path,
|
|
185
|
+
ticketId: row.ticket_id,
|
|
186
|
+
jobId: row.job_id,
|
|
187
|
+
kind: row.kind,
|
|
188
|
+
at: row.at,
|
|
189
|
+
}));
|
|
190
|
+
}
|
|
191
|
+
function treeProvenanceToJson(provenance) {
|
|
192
|
+
return {
|
|
193
|
+
createdByTicketId: provenance.createdByTicketId,
|
|
194
|
+
modifiedByTicketIds: provenance.modifiedByTicketIds,
|
|
195
|
+
latest: provenanceToJson(provenance.latest),
|
|
196
|
+
touchedFileCount: provenance.touchedFileCount,
|
|
197
|
+
rows: provenanceRowsToJson(provenance.rows),
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function parsePositiveInt(raw) {
|
|
201
|
+
if (typeof raw !== 'string' || raw.trim() === '')
|
|
202
|
+
return null;
|
|
203
|
+
const n = Number(raw);
|
|
204
|
+
return Number.isInteger(n) && n > 0 ? n : null;
|
|
205
|
+
}
|
|
206
|
+
function parseNonEmptyString(raw) {
|
|
207
|
+
return typeof raw === 'string' && raw.trim() !== '' ? raw.trim() : null;
|
|
208
|
+
}
|
|
209
|
+
function listTouchedRows(db, filters) {
|
|
210
|
+
const where = [];
|
|
211
|
+
const args = [];
|
|
212
|
+
if (filters.ticketId != null) {
|
|
213
|
+
where.push('ticket_id = ?');
|
|
214
|
+
args.push(filters.ticketId);
|
|
215
|
+
}
|
|
216
|
+
if (filters.jobId) {
|
|
217
|
+
where.push('job_id = ?');
|
|
218
|
+
args.push(filters.jobId);
|
|
219
|
+
}
|
|
220
|
+
if (filters.path) {
|
|
221
|
+
where.push('file_path = ?');
|
|
222
|
+
args.push(filters.path);
|
|
223
|
+
}
|
|
224
|
+
const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
|
225
|
+
return db.prepare(`SELECT id, file_path, ticket_id, job_id, kind, at
|
|
226
|
+
FROM file_provenance ${whereSql}
|
|
227
|
+
ORDER BY file_path ASC, at DESC`).all(...args);
|
|
228
|
+
}
|
|
229
|
+
function listAllEntries(projectPath) {
|
|
230
|
+
const out = [];
|
|
231
|
+
const stack = [projectPath];
|
|
232
|
+
while (stack.length > 0) {
|
|
233
|
+
const dir = stack.pop();
|
|
234
|
+
let entries;
|
|
235
|
+
try {
|
|
236
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
237
|
+
}
|
|
238
|
+
catch {
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
for (const entry of entries) {
|
|
242
|
+
if (isDenied(entry.name))
|
|
243
|
+
continue;
|
|
244
|
+
const abs = path_1.default.join(dir, entry.name);
|
|
245
|
+
// Normalize to POSIX so provenance/summary lookups (which are '/'-keyed)
|
|
246
|
+
// match on Windows, where path.relative emits backslashes.
|
|
247
|
+
const rel = normalizeRel(path_1.default.relative(projectPath, abs));
|
|
248
|
+
if (entry.isDirectory()) {
|
|
249
|
+
out.push({ rel, isDir: true, size: null, mtime: null });
|
|
250
|
+
stack.push(abs);
|
|
251
|
+
}
|
|
252
|
+
else if (entry.isFile()) {
|
|
253
|
+
let size = null;
|
|
254
|
+
let mtime = null;
|
|
255
|
+
try {
|
|
256
|
+
const st = fs_1.default.statSync(abs);
|
|
257
|
+
size = st.size;
|
|
258
|
+
mtime = st.mtimeMs;
|
|
259
|
+
}
|
|
260
|
+
catch {
|
|
261
|
+
// ignore
|
|
262
|
+
}
|
|
263
|
+
out.push({ rel, isDir: false, size, mtime });
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Drop gitignored files (honours the documented .gitignore-respect contract).
|
|
268
|
+
// Directories are kept — git can't report an ignored dir without its files, and
|
|
269
|
+
// an empty dir node is harmless; its ignored children are already filtered.
|
|
270
|
+
const files = out.filter((e) => !e.isDir).map((e) => e.rel);
|
|
271
|
+
const ignored = gitIgnoredSet(projectPath, files);
|
|
272
|
+
const filtered = ignored.size > 0 ? out.filter((e) => e.isDir || !ignored.has(e.rel)) : out;
|
|
273
|
+
filtered.sort((a, b) => a.rel.localeCompare(b.rel));
|
|
274
|
+
return filtered;
|
|
275
|
+
}
|
|
276
|
+
function listTouchedEntries(projectPath, rowsByPath) {
|
|
277
|
+
const seen = new Set();
|
|
278
|
+
const out = [];
|
|
279
|
+
for (const filePath of rowsByPath.keys()) {
|
|
280
|
+
// Keep touched-by-ai consistent with the `all` tree (and never surface
|
|
281
|
+
// secrets like .env that an AI job happened to touch).
|
|
282
|
+
if (isDeniedRelPath(filePath))
|
|
283
|
+
continue;
|
|
284
|
+
const parts = filePath.split('/').filter(Boolean);
|
|
285
|
+
for (let i = 1; i < parts.length; i += 1) {
|
|
286
|
+
const dirRel = parts.slice(0, i).join('/');
|
|
287
|
+
if (!seen.has(dirRel)) {
|
|
288
|
+
seen.add(dirRel);
|
|
289
|
+
out.push({ rel: dirRel, isDir: true, size: null, mtime: null });
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
if (seen.has(filePath))
|
|
293
|
+
continue;
|
|
294
|
+
seen.add(filePath);
|
|
295
|
+
const abs = path_1.default.join(projectPath, filePath);
|
|
296
|
+
let size = null;
|
|
297
|
+
let mtime = null;
|
|
298
|
+
try {
|
|
299
|
+
const st = fs_1.default.statSync(abs);
|
|
300
|
+
size = st.size;
|
|
301
|
+
mtime = st.mtimeMs;
|
|
302
|
+
}
|
|
303
|
+
catch {
|
|
304
|
+
// file may have been deleted after provenance was recorded
|
|
305
|
+
}
|
|
306
|
+
out.push({ rel: filePath, isDir: false, size, mtime });
|
|
307
|
+
}
|
|
308
|
+
out.sort((a, b) => {
|
|
309
|
+
const byPath = a.rel.localeCompare(b.rel);
|
|
310
|
+
if (byPath !== 0)
|
|
311
|
+
return byPath;
|
|
312
|
+
return Number(b.isDir) - Number(a.isDir);
|
|
313
|
+
});
|
|
314
|
+
return out;
|
|
315
|
+
}
|
|
316
|
+
// Set of summary file basenames (without `.json`), i.e. the path-hash of every
|
|
317
|
+
// file that currently has a summary on disk. One readdir replaces a per-entry
|
|
318
|
+
// readSummary disk hit during the tree walk.
|
|
319
|
+
function readSummaryHashSet(projectPath) {
|
|
320
|
+
const set = new Set();
|
|
321
|
+
let files;
|
|
322
|
+
try {
|
|
323
|
+
files = fs_1.default.readdirSync((0, file_summary_manager_1.summariesDir)(projectPath));
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
return set;
|
|
327
|
+
}
|
|
328
|
+
for (const f of files) {
|
|
329
|
+
if (f.endsWith('.json'))
|
|
330
|
+
set.add(f.slice(0, -'.json'.length));
|
|
331
|
+
}
|
|
332
|
+
return set;
|
|
333
|
+
}
|
|
334
|
+
function createCodeExplorerRouter(deps) {
|
|
335
|
+
const router = (0, express_1.Router)({ mergeParams: true });
|
|
336
|
+
const listByPath = deps.listProvenanceByPath ?? file_provenance_1.listProvenanceByPath;
|
|
337
|
+
const listByTicket = deps.listProvenanceByTicket ?? file_provenance_1.listProvenanceByTicket;
|
|
338
|
+
// Short-TTL per-project cache so paginating a large `all` tree reuses ONE
|
|
339
|
+
// synchronous filesystem walk instead of re-walking (and re-statting) on every
|
|
340
|
+
// page — the cursor only slices an already-materialized array. Also caches the
|
|
341
|
+
// one-readdir summary-hash set. 5s is long enough for a pagination burst and
|
|
342
|
+
// short enough that tree edits surface promptly.
|
|
343
|
+
const WALK_CACHE_TTL_MS = 5000;
|
|
344
|
+
let allEntriesCache = null;
|
|
345
|
+
let summaryHashCache = null;
|
|
346
|
+
const nowMs = () => Date.now();
|
|
347
|
+
function getAllEntriesCached() {
|
|
348
|
+
if (allEntriesCache && nowMs() - allEntriesCache.at < WALK_CACHE_TTL_MS)
|
|
349
|
+
return allEntriesCache.entries;
|
|
350
|
+
const entries = listAllEntries(deps.projectPath);
|
|
351
|
+
allEntriesCache = { at: nowMs(), entries };
|
|
352
|
+
return entries;
|
|
353
|
+
}
|
|
354
|
+
function getSummaryHashesCached() {
|
|
355
|
+
if (summaryHashCache && nowMs() - summaryHashCache.at < WALK_CACHE_TTL_MS)
|
|
356
|
+
return summaryHashCache.set;
|
|
357
|
+
const set = readSummaryHashSet(deps.projectPath);
|
|
358
|
+
summaryHashCache = { at: nowMs(), set };
|
|
359
|
+
return set;
|
|
360
|
+
}
|
|
361
|
+
// Feature-flag gate — entire prefix returns 404 when disabled.
|
|
362
|
+
router.use((_req, res, next) => {
|
|
363
|
+
if (!(0, feature_flags_1.isCodeExplorerEnabled)()) {
|
|
364
|
+
res.status(404).end();
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
// Lazily attach the file-summary watcher on first Code-Explorer use. It is
|
|
368
|
+
// not attached at registry load (that recursive watcher caused the fd leak
|
|
369
|
+
// that broke terminals); attachWatcher is idempotent, so this is cheap on
|
|
370
|
+
// every subsequent request.
|
|
371
|
+
try {
|
|
372
|
+
deps.fileSummaryManager.attachWatcher(deps.projectId, deps.projectPath);
|
|
373
|
+
}
|
|
374
|
+
catch { /* non-fatal */ }
|
|
375
|
+
next();
|
|
376
|
+
});
|
|
377
|
+
router.get('/tree', (req, res) => {
|
|
378
|
+
const filter = req.query.filter ?? 'touched-by-ai';
|
|
379
|
+
const withProvenance = req.query.withProvenance === '1' || req.query.withProvenance === 'true';
|
|
380
|
+
const { skip } = decodeCursor(req.query.cursor);
|
|
381
|
+
const ticketId = parsePositiveInt(req.query.ticketId);
|
|
382
|
+
const jobId = parseNonEmptyString(req.query.jobId);
|
|
383
|
+
let entries;
|
|
384
|
+
const touchedRowsByPath = new Map();
|
|
385
|
+
if (filter === 'touched-by-ai') {
|
|
386
|
+
const rows = listTouchedRows(deps.db, { ticketId, jobId });
|
|
387
|
+
for (const row of rows) {
|
|
388
|
+
const existing = touchedRowsByPath.get(row.file_path);
|
|
389
|
+
if (existing)
|
|
390
|
+
existing.push(row);
|
|
391
|
+
else
|
|
392
|
+
touchedRowsByPath.set(row.file_path, [row]);
|
|
393
|
+
}
|
|
394
|
+
entries = listTouchedEntries(deps.projectPath, touchedRowsByPath);
|
|
395
|
+
}
|
|
396
|
+
else {
|
|
397
|
+
entries = getAllEntriesCached();
|
|
398
|
+
// Batch-load ALL provenance once instead of a per-entry SQL query (N+1).
|
|
399
|
+
if (withProvenance) {
|
|
400
|
+
for (const row of listTouchedRows(deps.db, {})) {
|
|
401
|
+
const existing = touchedRowsByPath.get(row.file_path);
|
|
402
|
+
if (existing)
|
|
403
|
+
existing.push(row);
|
|
404
|
+
else
|
|
405
|
+
touchedRowsByPath.set(row.file_path, [row]);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
const page = entries.slice(skip, skip + MAX_TREE_PAGE);
|
|
410
|
+
const nextCursor = skip + page.length < entries.length ? encodeCursor(skip + page.length) : null;
|
|
411
|
+
// Read the summaries dir ONCE into a Set of path-hashes instead of opening +
|
|
412
|
+
// parsing a JSON file per entry just to test existence (cached per project).
|
|
413
|
+
const summaryHashes = getSummaryHashesCached();
|
|
414
|
+
const out = page.map((e) => {
|
|
415
|
+
const isTouchedDir = filter === 'touched-by-ai' && e.isDir;
|
|
416
|
+
const rawRows = withProvenance && !isTouchedDir ? (touchedRowsByPath.get(e.rel) ?? []) : [];
|
|
417
|
+
const provenance = withProvenance && isTouchedDir
|
|
418
|
+
? rollupDirectoryProvenance(touchedRowsByPath, e.rel)
|
|
419
|
+
: rollupProvenance(rawRows);
|
|
420
|
+
return {
|
|
421
|
+
path: e.rel,
|
|
422
|
+
kind: e.isDir ? 'dir' : 'file',
|
|
423
|
+
sizeBytes: e.size,
|
|
424
|
+
hasSummary: !e.isDir && summaryHashes.has((0, file_summary_manager_1.pathHash)(e.rel)),
|
|
425
|
+
provenance,
|
|
426
|
+
lastModifiedAt: e.mtime,
|
|
427
|
+
};
|
|
428
|
+
});
|
|
429
|
+
res.json({
|
|
430
|
+
entries: out.map((entry) => ({
|
|
431
|
+
...entry,
|
|
432
|
+
provenance: treeProvenanceToJson(entry.provenance),
|
|
433
|
+
})),
|
|
434
|
+
nextCursor,
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
router.get('/file', async (req, res) => {
|
|
438
|
+
const relRaw = req.query.path;
|
|
439
|
+
if (!relRaw || typeof relRaw !== 'string') {
|
|
440
|
+
res.status(400).json({ error: 'path query parameter is required' });
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
const guard = resolveSafePath(deps.projectPath, relRaw);
|
|
444
|
+
if (!guard) {
|
|
445
|
+
res.status(400).json({ error: 'path traversal not allowed' });
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
if (isDeniedRelPath(relRaw)) {
|
|
449
|
+
res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
// Canonical POSIX form for all summary/provenance/hash lookups.
|
|
453
|
+
const rel = normalizeRel(relRaw);
|
|
454
|
+
if (isGitIgnored(deps.projectPath, rel)) {
|
|
455
|
+
res.status(403).json({ error: 'path is gitignored' });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const abs = guard;
|
|
459
|
+
let stat;
|
|
460
|
+
try {
|
|
461
|
+
stat = fs_1.default.statSync(abs);
|
|
462
|
+
}
|
|
463
|
+
catch {
|
|
464
|
+
// Honour the staleness scenario: even if content is unavailable, return
|
|
465
|
+
// the existing summary so the client can render a "not found" banner.
|
|
466
|
+
const summary = (0, file_summary_manager_1.readSummary)(deps.projectPath, rel);
|
|
467
|
+
const provenance = listByPath(deps.db, deps.projectId, rel);
|
|
468
|
+
if (summary || provenance.length > 0) {
|
|
469
|
+
res.json({
|
|
470
|
+
content: null,
|
|
471
|
+
reason: 'not-found',
|
|
472
|
+
summary,
|
|
473
|
+
summaryStale: true,
|
|
474
|
+
provenance: provenanceRowsToJson(provenance),
|
|
475
|
+
});
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
res.status(404).json({ error: 'file not found' });
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
if (!stat.isFile()) {
|
|
482
|
+
res.status(400).json({ error: 'path is not a regular file' });
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
486
|
+
res.json({
|
|
487
|
+
tooLarge: true,
|
|
488
|
+
sizeBytes: stat.size,
|
|
489
|
+
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
|
|
490
|
+
summary: (0, file_summary_manager_1.readSummary)(deps.projectPath, rel),
|
|
491
|
+
absolutePath: abs,
|
|
492
|
+
});
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// Binary detection: read first 8 KB, scan for NUL.
|
|
496
|
+
let head;
|
|
497
|
+
try {
|
|
498
|
+
const fd = fs_1.default.openSync(abs, 'r');
|
|
499
|
+
try {
|
|
500
|
+
head = Buffer.alloc(Math.min(BINARY_PROBE_BYTES, stat.size));
|
|
501
|
+
fs_1.default.readSync(fd, head, 0, head.length, 0);
|
|
502
|
+
}
|
|
503
|
+
finally {
|
|
504
|
+
fs_1.default.closeSync(fd);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
res.status(500).json({ error: 'failed to read file' });
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
if (head.includes(0)) {
|
|
512
|
+
res.json({
|
|
513
|
+
binary: true,
|
|
514
|
+
sizeBytes: stat.size,
|
|
515
|
+
mime: 'application/octet-stream',
|
|
516
|
+
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
|
|
517
|
+
summary: (0, file_summary_manager_1.readSummary)(deps.projectPath, rel),
|
|
518
|
+
absolutePath: abs,
|
|
519
|
+
});
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
let content;
|
|
523
|
+
try {
|
|
524
|
+
content = fs_1.default.readFileSync(abs, 'utf8');
|
|
525
|
+
}
|
|
526
|
+
catch {
|
|
527
|
+
res.status(500).json({ error: 'failed to read file' });
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
const summary = (0, file_summary_manager_1.readSummary)(deps.projectPath, rel);
|
|
531
|
+
const summaryStale = await computeStaleness(abs, summary);
|
|
532
|
+
res.json({
|
|
533
|
+
content,
|
|
534
|
+
encoding: 'utf-8',
|
|
535
|
+
language: languageForExt(path_1.default.extname(rel)),
|
|
536
|
+
provenance: provenanceRowsToJson(listByPath(deps.db, deps.projectId, rel)),
|
|
537
|
+
summary,
|
|
538
|
+
summaryStale,
|
|
539
|
+
absolutePath: abs,
|
|
540
|
+
});
|
|
541
|
+
});
|
|
542
|
+
router.get('/summary', async (req, res) => {
|
|
543
|
+
const relRaw = req.query.path;
|
|
544
|
+
if (!relRaw || typeof relRaw !== 'string') {
|
|
545
|
+
res.status(400).json({ error: 'path query parameter is required' });
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
const guard = resolveSafePath(deps.projectPath, relRaw);
|
|
549
|
+
if (!guard) {
|
|
550
|
+
res.status(400).json({ error: 'path traversal not allowed' });
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
553
|
+
if (isDeniedRelPath(relRaw)) {
|
|
554
|
+
res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
const rel = normalizeRel(relRaw);
|
|
558
|
+
if (isGitIgnored(deps.projectPath, rel)) {
|
|
559
|
+
res.status(403).json({ error: 'path is gitignored' });
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
const summary = (0, file_summary_manager_1.readSummary)(deps.projectPath, rel);
|
|
563
|
+
if (!summary) {
|
|
564
|
+
res.json({ summary: null });
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
let summaryStale = false;
|
|
568
|
+
try {
|
|
569
|
+
summaryStale = await computeStaleness(guard, summary);
|
|
570
|
+
}
|
|
571
|
+
catch {
|
|
572
|
+
summaryStale = true;
|
|
573
|
+
}
|
|
574
|
+
res.json({ summary, summaryStale });
|
|
575
|
+
});
|
|
576
|
+
router.post('/file/regenerate-summary', async (req, res) => {
|
|
577
|
+
const relRaw = req.query.path;
|
|
578
|
+
if (!relRaw || typeof relRaw !== 'string') {
|
|
579
|
+
res.status(400).json({ error: 'path query parameter is required' });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
const guard = resolveSafePath(deps.projectPath, relRaw);
|
|
583
|
+
if (!guard) {
|
|
584
|
+
res.status(400).json({ error: 'path traversal not allowed' });
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
if (isDeniedRelPath(relRaw)) {
|
|
588
|
+
res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
const rel = normalizeRel(relRaw);
|
|
592
|
+
if (isGitIgnored(deps.projectPath, rel)) {
|
|
593
|
+
res.status(403).json({ error: 'path is gitignored' });
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
596
|
+
let stat;
|
|
597
|
+
try {
|
|
598
|
+
stat = fs_1.default.statSync(guard);
|
|
599
|
+
}
|
|
600
|
+
catch {
|
|
601
|
+
res.status(404).json({ skipped: 'not-found' });
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
if (!stat.isFile()) {
|
|
605
|
+
res.status(400).json({ skipped: 'not-file' });
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
if (stat.size > MAX_FILE_BYTES) {
|
|
609
|
+
res.status(413).json({ skipped: 'too-large' });
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
try {
|
|
613
|
+
const fd = fs_1.default.openSync(guard, 'r');
|
|
614
|
+
try {
|
|
615
|
+
const head = Buffer.alloc(Math.min(BINARY_PROBE_BYTES, stat.size));
|
|
616
|
+
fs_1.default.readSync(fd, head, 0, head.length, 0);
|
|
617
|
+
if (head.includes(0)) {
|
|
618
|
+
res.status(415).json({ skipped: 'binary' });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
finally {
|
|
623
|
+
fs_1.default.closeSync(fd);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
res.status(500).json({ error: 'failed to inspect file' });
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
const body = (req.body ?? {});
|
|
631
|
+
try {
|
|
632
|
+
// force: true — an explicit "Regenerate" click should re-summarise even if
|
|
633
|
+
// the content hash is unchanged (e.g. after an app language switch).
|
|
634
|
+
const result = await deps.fileSummaryManager.enqueue({
|
|
635
|
+
projectPath: deps.projectPath,
|
|
636
|
+
projectId: deps.projectId,
|
|
637
|
+
projectSlug: deps.projectId,
|
|
638
|
+
relPath: rel,
|
|
639
|
+
triggeredBy: { kind: 'user', id: 'manual', ticketId: null },
|
|
640
|
+
overrideBudget: body.overrideBudget === true,
|
|
641
|
+
force: true,
|
|
642
|
+
});
|
|
643
|
+
// Surface the enqueue outcome so the client's budget-override prompt is
|
|
644
|
+
// reachable. 200 (not 4xx) keeps res.ok true so the client reads `skipped`.
|
|
645
|
+
if (result === 'skipped:budget') {
|
|
646
|
+
res.status(200).json({ skipped: 'budget' });
|
|
647
|
+
return;
|
|
648
|
+
}
|
|
649
|
+
if (result === 'skipped:per-job-cap') {
|
|
650
|
+
res.status(200).json({ skipped: 'per-job-cap' });
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
// TTL-dropped (queue saturated >5min) and not-found (file vanished between
|
|
654
|
+
// the stat above and the worker) must NOT masquerade as a 202 success —
|
|
655
|
+
// surface them so the client toasts "try again" instead of silently
|
|
656
|
+
// clearing the spinner with no summary.
|
|
657
|
+
if (result === 'skipped:ttl') {
|
|
658
|
+
res.status(200).json({ skipped: 'ttl' });
|
|
659
|
+
return;
|
|
660
|
+
}
|
|
661
|
+
if (result === 'skipped:not-found') {
|
|
662
|
+
res.status(200).json({ skipped: 'not-found' });
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
if (result === 'failed') {
|
|
666
|
+
res.status(500).json({ error: 'summary generation failed' });
|
|
667
|
+
return;
|
|
668
|
+
}
|
|
669
|
+
res.status(202).json({ enqueued: true });
|
|
670
|
+
}
|
|
671
|
+
catch (err) {
|
|
672
|
+
console.error('[code-explorer-router] enqueue failed:', err);
|
|
673
|
+
res.status(500).json({ error: 'enqueue failed', message: err.message });
|
|
674
|
+
}
|
|
675
|
+
});
|
|
676
|
+
router.get('/provenance', (req, res) => {
|
|
677
|
+
const ticketId = parsePositiveInt(req.query.ticketId);
|
|
678
|
+
const jobId = parseNonEmptyString(req.query.jobId);
|
|
679
|
+
const relPath = parseNonEmptyString(req.query.path);
|
|
680
|
+
if (relPath) {
|
|
681
|
+
const guard = resolveSafePath(deps.projectPath, relPath);
|
|
682
|
+
if (!guard) {
|
|
683
|
+
res.status(400).json({ error: 'path traversal not allowed' });
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
// Mirror the content endpoints: never leak even the metadata (which ticket/
|
|
687
|
+
// job touched it, when) of a denied/secret file.
|
|
688
|
+
if (isDeniedRelPath(relPath)) {
|
|
689
|
+
res.status(403).json({ error: 'path is excluded by the code-explorer deny-list' });
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
if (ticketId == null && !jobId && !relPath) {
|
|
694
|
+
res.status(400).json({ error: 'ticketId, jobId, or path query parameter is required' });
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (req.query.ticketId != null && ticketId == null) {
|
|
698
|
+
res.status(400).json({ error: 'ticketId must be a positive integer' });
|
|
699
|
+
return;
|
|
700
|
+
}
|
|
701
|
+
const rows = ticketId != null && !jobId && !relPath
|
|
702
|
+
? listByTicket(deps.db, deps.projectId, ticketId)
|
|
703
|
+
: listTouchedRows(deps.db, { ticketId, jobId, path: relPath ? normalizeRel(relPath) : relPath });
|
|
704
|
+
res.json(provenanceRowsToJson(rows));
|
|
705
|
+
});
|
|
706
|
+
router.get('/diff', (req, res) => {
|
|
707
|
+
const jobId = parseNonEmptyString(req.query.jobId);
|
|
708
|
+
const relPath = parseNonEmptyString(req.query.path);
|
|
709
|
+
if (!jobId || !relPath) {
|
|
710
|
+
res.status(400).json({ error: 'jobId and path query parameters are required' });
|
|
711
|
+
return;
|
|
712
|
+
}
|
|
713
|
+
const guard = resolveSafePath(deps.projectPath, relPath);
|
|
714
|
+
if (!guard) {
|
|
715
|
+
res.status(400).json({ error: 'path traversal not allowed' });
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
const diff = (0, file_provenance_1.getProvenanceDiff)(deps.db, deps.projectId, jobId, normalizeRel(relPath));
|
|
719
|
+
if (!diff) {
|
|
720
|
+
res.status(404).json({ error: 'diff not available' });
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
res.json(diff);
|
|
724
|
+
});
|
|
725
|
+
return router;
|
|
726
|
+
}
|
|
727
|
+
function resolveSafePath(projectPath, relPath) {
|
|
728
|
+
// Reject absolute paths and any path with explicit traversal segments before
|
|
729
|
+
// we ever hit the filesystem. resolve() can collapse `..` legally so we still
|
|
730
|
+
// verify the prefix below.
|
|
731
|
+
if (path_1.default.isAbsolute(relPath))
|
|
732
|
+
return null;
|
|
733
|
+
const resolved = path_1.default.resolve(projectPath, relPath);
|
|
734
|
+
const root = projectPath.endsWith(path_1.default.sep) ? projectPath : projectPath + path_1.default.sep;
|
|
735
|
+
if (resolved !== projectPath && !resolved.startsWith(root))
|
|
736
|
+
return null;
|
|
737
|
+
// Symlink hardening: the lexical check above is defeated by an in-tree symlink
|
|
738
|
+
// whose target escapes the project (e.g. `link -> /etc/passwd`). Verify the
|
|
739
|
+
// REAL path stays under the REAL project root. Walk up to the nearest existing
|
|
740
|
+
// ancestor (so not-yet-created paths — used by the not-found banner and the
|
|
741
|
+
// regenerate endpoint — still validate), realpath it, then re-append the
|
|
742
|
+
// missing suffix.
|
|
743
|
+
let realRoot;
|
|
744
|
+
try {
|
|
745
|
+
realRoot = fs_1.default.realpathSync.native(projectPath);
|
|
746
|
+
}
|
|
747
|
+
catch {
|
|
748
|
+
// Project root itself is uncanonicalisable — fail CLOSED. Returning the
|
|
749
|
+
// lexical path here would silently drop the symlink-escape hardening (the
|
|
750
|
+
// lexical check alone is defeatable by an in-tree symlink pointing outside
|
|
751
|
+
// the project). Reading any file is pointless if the root can't resolve.
|
|
752
|
+
return null;
|
|
753
|
+
}
|
|
754
|
+
const realRootWithSep = realRoot.endsWith(path_1.default.sep) ? realRoot : realRoot + path_1.default.sep;
|
|
755
|
+
let probe = resolved;
|
|
756
|
+
const suffix = [];
|
|
757
|
+
for (;;) {
|
|
758
|
+
try {
|
|
759
|
+
const realProbe = fs_1.default.realpathSync.native(probe);
|
|
760
|
+
const realFull = suffix.length > 0
|
|
761
|
+
? path_1.default.join(realProbe, ...suffix.slice().reverse())
|
|
762
|
+
: realProbe;
|
|
763
|
+
if (realFull !== realRoot && !realFull.startsWith(realRootWithSep))
|
|
764
|
+
return null;
|
|
765
|
+
return resolved;
|
|
766
|
+
}
|
|
767
|
+
catch (err) {
|
|
768
|
+
if (err.code !== 'ENOENT')
|
|
769
|
+
return null;
|
|
770
|
+
const parent = path_1.default.dirname(probe);
|
|
771
|
+
if (parent === probe)
|
|
772
|
+
return null; // hit filesystem root without resolving
|
|
773
|
+
suffix.push(path_1.default.basename(probe));
|
|
774
|
+
probe = parent;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
async function computeStaleness(abs, summary) {
|
|
779
|
+
if (!summary)
|
|
780
|
+
return false;
|
|
781
|
+
try {
|
|
782
|
+
const currentHash = await (0, file_summary_manager_1.computeFileHash)(abs);
|
|
783
|
+
return currentHash !== summary.fileHash;
|
|
784
|
+
}
|
|
785
|
+
catch {
|
|
786
|
+
return true;
|
|
787
|
+
}
|
|
788
|
+
}
|