@cloudcli-ai/cloudcli 1.37.2 → 1.37.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +269 -0
- package/dist/assets/{abnfDiagram-N423BO3Z-C4UAoHJb.js → abnfDiagram-N423BO3Z-Dmkwx38H.js} +1 -1
- package/dist/assets/{arc-B8jF-ja9.js → arc-DLe9AYuO.js} +1 -1
- package/dist/assets/{architectureDiagram-T3A2C74G-C_Wur_CU.js → architectureDiagram-T3A2C74G-CBbbbNUW.js} +1 -1
- package/dist/assets/{blockDiagram-VBNYF7ZC-Cq4P1i8t.js → blockDiagram-VBNYF7ZC-CaMEaWu4.js} +1 -1
- package/dist/assets/{c4Diagram-5PPSVZJV-C1wG4eUj.js → c4Diagram-5PPSVZJV-DhjMgNnR.js} +1 -1
- package/dist/assets/channel-CWrmwsir.js +1 -0
- package/dist/assets/{chunk-2GRJ4B5K-B8fnBG6E.js → chunk-2GRJ4B5K-yjl3-F-i.js} +1 -1
- package/dist/assets/{chunk-2Q5K7J3B-WdfPooPJ.js → chunk-2Q5K7J3B-DsAYaWEP.js} +1 -1
- package/dist/assets/{chunk-5RXB4S5H-DhZJuG2E.js → chunk-5RXB4S5H-Bt5qmqOX.js} +1 -1
- package/dist/assets/{chunk-5VM5RSS4-B0rjslal.js → chunk-5VM5RSS4-yWrWVvUt.js} +1 -1
- package/dist/assets/{chunk-6Q2QTUOP-CG2Oj4jL.js → chunk-6Q2QTUOP-Dt66K5xb.js} +1 -1
- package/dist/assets/{chunk-GF5L2VYU-rrl2lRxY.js → chunk-GF5L2VYU-DA0vzaj4.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CjVbrVMA.js → chunk-JWPE2WC7-CUkbx96V.js} +1 -1
- package/dist/assets/{chunk-KBJHAD2P-CRzKgU8S.js → chunk-KBJHAD2P-DWKxx1WH.js} +1 -1
- package/dist/assets/{chunk-RYQCIY6F-DBz4GTZF.js → chunk-RYQCIY6F-BqluplN0.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-wHxalsXk.js → chunk-XXDRQBXY-DelKg3Xf.js} +1 -1
- package/dist/assets/classDiagram-JCYQIIEL-COAGCC6w.js +1 -0
- package/dist/assets/classDiagram-v2-OCEON4UE-COAGCC6w.js +1 -0
- package/dist/assets/{cose-bilkent-JH36ORCC-s1hygY8e.js → cose-bilkent-JH36ORCC-DHV93-Em.js} +1 -1
- package/dist/assets/{cynefin-VYW2F7L2-C4LGV15i.js → cynefin-VYW2F7L2-B9HZgoGX.js} +1 -1
- package/dist/assets/{cynefinDiagram-MW4NZA55-C5uhu7B3.js → cynefinDiagram-MW4NZA55-hFjjEQg8.js} +1 -1
- package/dist/assets/{dagre-VZM6K2ZE-B-OBTewm.js → dagre-VZM6K2ZE-CpeVOC7V.js} +1 -1
- package/dist/assets/{diagram-7IWD3JNH-BjGKQxih.js → diagram-7IWD3JNH-D0H1UoRA.js} +1 -1
- package/dist/assets/{diagram-B4RE2ZJO-BMKz1X2L.js → diagram-B4RE2ZJO-6Y7BQUup.js} +1 -1
- package/dist/assets/{diagram-LBJQPF4R-DNh-GhKC.js → diagram-LBJQPF4R-C0nNOwht.js} +1 -1
- package/dist/assets/{diagram-Q27KOJAE-BaTG9jPT.js → diagram-Q27KOJAE-En6QlOVX.js} +1 -1
- package/dist/assets/{diagram-UB23O5K3-BUwo2OvJ.js → diagram-UB23O5K3-CmPiNeGD.js} +1 -1
- package/dist/assets/{ebnfDiagram-BXEA7PRR-DAZgfzX2.js → ebnfDiagram-BXEA7PRR-Bygzp1Qt.js} +1 -1
- package/dist/assets/{erDiagram-JOGREHBK-1w1CcGEb.js → erDiagram-JOGREHBK-CwScRfzl.js} +1 -1
- package/dist/assets/{flowDiagram-UKHOOZJN-D4EII8Jj.js → flowDiagram-UKHOOZJN-bKX-0xP6.js} +1 -1
- package/dist/assets/{ganttDiagram-PKOTCBZU-ChvJZAgB.js → ganttDiagram-PKOTCBZU-DtZPKXGd.js} +1 -1
- package/dist/assets/{gitGraphDiagram-DS77QQ5N-DtOn41S8.js → gitGraphDiagram-DS77QQ5N-CUOtdL1o.js} +1 -1
- package/dist/assets/index-CdjIFxM2.js +5004 -0
- package/dist/assets/index-DgafhGTF.css +32 -0
- package/dist/assets/{infoDiagram-6WML65LV-DN_PVEJZ.js → infoDiagram-6WML65LV-ciRQTS0h.js} +1 -1
- package/dist/assets/{ishikawaDiagram-WSZJBQD7-DbkF1kXb.js → ishikawaDiagram-WSZJBQD7-CgjhQZ84.js} +1 -1
- package/dist/assets/{journeyDiagram-NVQOT4AX-pJwmhddE.js → journeyDiagram-NVQOT4AX-BsbQSeIW.js} +1 -1
- package/dist/assets/{kanban-definition-27J2QSJJ-PPAPgNp5.js → kanban-definition-27J2QSJJ-CrwzY9Jw.js} +1 -1
- package/dist/assets/{linear-iPR-J9f7.js → linear-BUaCDr6l.js} +1 -1
- package/dist/assets/{mermaid.core-BEUL86Z6.js → mermaid.core-ClgdUU0j.js} +5 -5
- package/dist/assets/{mindmap-definition-FAOFIHXS-BvZoBib7.js → mindmap-definition-FAOFIHXS-BictljY1.js} +1 -1
- package/dist/assets/{pegDiagram-VL7TDLO6-DhYreaUd.js → pegDiagram-VL7TDLO6-BXdByy_U.js} +1 -1
- package/dist/assets/{pieDiagram-7S7Q4E2Y-CrOHlyD0.js → pieDiagram-7S7Q4E2Y-uZsMfkRX.js} +1 -1
- package/dist/assets/{quadrantDiagram-CIZ2JOQS-D4g5yH3t.js → quadrantDiagram-CIZ2JOQS-7lCv4QIj.js} +1 -1
- package/dist/assets/{railroadDiagram-AXF67PYL-Dj39lHlf.js → railroadDiagram-AXF67PYL-07dxIWbi.js} +1 -1
- package/dist/assets/{requirementDiagram-LRYGKXZP-1AS4Zgm_.js → requirementDiagram-LRYGKXZP-Cb31Yv1_.js} +1 -1
- package/dist/assets/{sankeyDiagram-W5VNT64P-CKlMn19P.js → sankeyDiagram-W5VNT64P-X0YPSyF9.js} +1 -1
- package/dist/assets/{sequenceDiagram-SI44F4Z6-B_j3qyvo.js → sequenceDiagram-SI44F4Z6-BdBxSIlP.js} +1 -1
- package/dist/assets/server.browser-CtX3Hdl5.js +27 -0
- package/dist/assets/{sizeCapture-X5ZJPWSS-Bp2Op16v.js → sizeCapture-X5ZJPWSS-DQM5xAyD.js} +1 -1
- package/dist/assets/{stateDiagram-OKZ733FA-C0cN6DoY.js → stateDiagram-OKZ733FA-DJ3xkLUF.js} +1 -1
- package/dist/assets/stateDiagram-v2-UEYNNEHI-vAaE4zNo.js +1 -0
- package/dist/assets/{swimlanes-SLNWSIFB-BEvXOV9b.js → swimlanes-SLNWSIFB-rjfYdW0w.js} +2 -2
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-TV2IF4Nq.js +8 -0
- package/dist/assets/{timeline-definition-Z64GVDOM-zdJQinvZ.js → timeline-definition-Z64GVDOM-DZMra5do.js} +1 -1
- package/dist/assets/{vendor-codemirror-OwyKSvPE.js → vendor-codemirror-CYrSt7-U.js} +18 -18
- package/dist/assets/{vennDiagram-T6HMQDX7-DuF1uZMY.js → vennDiagram-T6HMQDX7-BevD8uu0.js} +1 -1
- package/dist/assets/{wardleyDiagram-T6FBY63Y-B9AlLQup.js → wardleyDiagram-T6FBY63Y-DsIyXB7z.js} +1 -1
- package/dist/assets/{xychartDiagram-ELKLHX3M-DTJX48Qg.js → xychartDiagram-ELKLHX3M-G3PlF3ko.js} +1 -1
- package/dist/index.html +3 -3
- package/dist-server/server/index.js +11 -4
- package/dist-server/server/index.js.map +1 -1
- package/dist-server/server/modules/agent/agent.module.js +9 -2
- package/dist-server/server/modules/agent/agent.module.js.map +1 -1
- package/dist-server/server/modules/agent/agent.routes.js +177 -130
- package/dist-server/server/modules/agent/agent.routes.js.map +1 -1
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js +306 -0
- package/dist-server/server/modules/agent/tests/agent-run-registration.test.js.map +1 -0
- package/dist-server/server/modules/agent/tests/agent.routes.test.js +48 -2
- package/dist-server/server/modules/agent/tests/agent.routes.test.js.map +1 -1
- package/dist-server/server/modules/browser-use/browser-use.routes.js +39 -33
- package/dist-server/server/modules/browser-use/browser-use.routes.js.map +1 -1
- package/dist-server/server/modules/database/connection.js +1 -1
- package/dist-server/server/modules/database/connection.js.map +1 -1
- package/dist-server/server/modules/database/index.js +5 -0
- package/dist-server/server/modules/database/index.js.map +1 -1
- package/dist-server/server/modules/database/migrations.js +45 -1
- package/dist-server/server/modules/database/migrations.js.map +1 -1
- package/dist-server/server/modules/database/repositories/provider-models.js +60 -6
- package/dist-server/server/modules/database/repositories/provider-models.js.map +1 -1
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js +71 -0
- package/dist-server/server/modules/database/repositories/scheduled-messages.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js +105 -0
- package/dist-server/server/modules/database/repositories/session-drafts.db.js.map +1 -0
- package/dist-server/server/modules/database/repositories/sessions.db.js +144 -5
- package/dist-server/server/modules/database/repositories/sessions.db.js.map +1 -1
- package/dist-server/server/modules/database/repositories/user-preferences.db.js +65 -0
- package/dist-server/server/modules/database/repositories/user-preferences.db.js.map +1 -0
- package/dist-server/server/modules/database/schema.js +99 -0
- package/dist-server/server/modules/database/schema.js.map +1 -1
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js +99 -0
- package/dist-server/server/modules/database/tests/provider-models.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js +39 -0
- package/dist-server/server/modules/database/tests/sessions.db.integration.test.js.map +1 -1
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js +105 -0
- package/dist-server/server/modules/database/tests/user-preferences.db.integration.test.js.map +1 -0
- package/dist-server/server/modules/file-tree/file-tree.module.js +2 -1
- package/dist-server/server/modules/file-tree/file-tree.module.js.map +1 -1
- package/dist-server/server/modules/file-tree/file-tree.service.js +44 -7
- package/dist-server/server/modules/file-tree/file-tree.service.js.map +1 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js +126 -1
- package/dist-server/server/modules/file-tree/tests/file-tree.service.test.js.map +1 -1
- package/dist-server/server/modules/git/git-branch-diff.service.js +142 -0
- package/dist-server/server/modules/git/git-branch-diff.service.js.map +1 -0
- package/dist-server/server/modules/git/git-branch.service.js.map +1 -1
- package/dist-server/server/modules/git/git.routes.js +92 -5
- package/dist-server/server/modules/git/git.routes.js.map +1 -1
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js +160 -0
- package/dist-server/server/modules/git/tests/git-branch-diff.service.test.js.map +1 -0
- package/dist-server/server/modules/projects/project-clone.routes.js +108 -0
- package/dist-server/server/modules/projects/project-clone.routes.js.map +1 -0
- package/dist-server/server/modules/projects/projects.routes.js +13 -66
- package/dist-server/server/modules/projects/projects.routes.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-clone-request.service.js +36 -0
- package/dist-server/server/modules/projects/services/project-clone-request.service.js.map +1 -0
- package/dist-server/server/modules/projects/services/project-clone.service.js +86 -32
- package/dist-server/server/modules/projects/services/project-clone.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js +12 -1
- package/dist-server/server/modules/projects/services/project-delete.service.js.map +1 -1
- package/dist-server/server/modules/projects/services/project-star.service.js +2 -2
- package/dist-server/server/modules/projects/services/project-star.service.js.map +1 -1
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js +191 -0
- package/dist-server/server/modules/projects/tests/project-clone.routes.test.js.map +1 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js +275 -0
- package/dist-server/server/modules/projects/tests/project-clone.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/index.js +3 -0
- package/dist-server/server/modules/providers/index.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js +62 -9
- package/dist-server/server/modules/providers/list/claude/claude-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js +46 -0
- package/dist-server/server/modules/providers/list/claude/claude-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js +31 -12
- package/dist-server/server/modules/providers/list/claude/claude-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js +472 -53
- package/dist-server/server/modules/providers/list/claude/claude-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js +38 -11
- package/dist-server/server/modules/providers/list/claude/claude-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js +1007 -60
- package/dist-server/server/modules/providers/list/claude/claude-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js +4 -1
- package/dist-server/server/modules/providers/list/claude/claude-skills.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/claude/claude.provider.js +2 -0
- package/dist-server/server/modules/providers/list/claude/claude.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js +204 -0
- package/dist-server/server/modules/providers/list/codex/codex-app-server.client.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js +32 -0
- package/dist-server/server/modules/providers/list/codex/codex-fork.provider.js.map +1 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js +47 -0
- package/dist-server/server/modules/providers/list/codex/codex-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js +71 -53
- package/dist-server/server/modules/providers/list/codex/codex-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js +8 -0
- package/dist-server/server/modules/providers/list/codex/codex-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js +1621 -431
- package/dist-server/server/modules/providers/list/codex/codex-sessions.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/codex/codex.provider.js +2 -0
- package/dist-server/server/modules/providers/list/codex/codex.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js +9 -2
- package/dist-server/server/modules/providers/list/cursor/cursor-auth.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js +16 -8
- package/dist-server/server/modules/providers/list/cursor/cursor-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js +274 -9
- package/dist-server/server/modules/providers/list/opencode/opencode-models.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-runtime.provider.js.map +1 -1
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js +19 -2
- package/dist-server/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.js.map +1 -1
- package/dist-server/server/modules/providers/provider.routes.js +134 -2
- package/dist-server/server/modules/providers/provider.routes.js.map +1 -1
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js +100 -0
- package/dist-server/server/modules/providers/services/claude-cli-liveness.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js +97 -0
- package/dist-server/server/modules/providers/services/claude-settings-permissions.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js +23 -1
- package/dist-server/server/modules/providers/services/provider-capabilities.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-models.service.js +59 -5
- package/dist-server/server/modules/providers/services/provider-models.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-runtime.service.js +8 -0
- package/dist-server/server/modules/providers/services/provider-runtime.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js +139 -35
- package/dist-server/server/modules/providers/services/provider-token-usage.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js +2 -4
- package/dist-server/server/modules/providers/services/session-conversations-search.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/session-history-cache.service.js +91 -0
- package/dist-server/server/modules/providers/services/session-history-cache.service.js.map +1 -0
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js +105 -32
- package/dist-server/server/modules/providers/services/session-synchronizer.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js +3 -60
- package/dist-server/server/modules/providers/services/sessions-watcher.service.js.map +1 -1
- package/dist-server/server/modules/providers/services/sessions.service.js +252 -12
- package/dist-server/server/modules/providers/services/sessions.service.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-auth.test.js +183 -0
- package/dist-server/server/modules/providers/tests/claude-auth.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js +172 -0
- package/dist-server/server/modules/providers/tests/claude-background-work.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js +84 -0
- package/dist-server/server/modules/providers/tests/claude-compaction.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js +123 -0
- package/dist-server/server/modules/providers/tests/claude-message-model.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-models.test.js +39 -0
- package/dist-server/server/modules/providers/tests/claude-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js +265 -0
- package/dist-server/server/modules/providers/tests/claude-runtime-hold.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js +1815 -33
- package/dist-server/server/modules/providers/tests/claude-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js +228 -0
- package/dist-server/server/modules/providers/tests/claude-settings-permissions.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js +16 -0
- package/dist-server/server/modules/providers/tests/claude-subagent-echo.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js +85 -0
- package/dist-server/server/modules/providers/tests/claude-token-budget.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js +338 -0
- package/dist-server/server/modules/providers/tests/codex-message-editing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js +25 -0
- package/dist-server/server/modules/providers/tests/codex-models.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js +83 -0
- package/dist-server/server/modules/providers/tests/codex-runtime.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js +54 -0
- package/dist-server/server/modules/providers/tests/codex-session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js +354 -15
- package/dist-server/server/modules/providers/tests/codex-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js +167 -0
- package/dist-server/server/modules/providers/tests/cursor-auth.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/opencode-models.test.js +196 -4
- package/dist-server/server/modules/providers/tests/opencode-models.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js +19 -1
- package/dist-server/server/modules/providers/tests/opencode-sessions.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js +30 -0
- package/dist-server/server/modules/providers/tests/provider-capabilities.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js +79 -1
- package/dist-server/server/modules/providers/tests/provider-models.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js +164 -1
- package/dist-server/server/modules/providers/tests/provider-token-usage.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js +190 -1
- package/dist-server/server/modules/providers/tests/provider.routes.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/session-fork.test.js +113 -0
- package/dist-server/server/modules/providers/tests/session-fork.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js +139 -0
- package/dist-server/server/modules/providers/tests/session-history-cache.service.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js +90 -0
- package/dist-server/server/modules/providers/tests/session-orphan-prune.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js +122 -0
- package/dist-server/server/modules/providers/tests/session-synchronizer-coalescing.test.js.map +1 -0
- package/dist-server/server/modules/providers/tests/sessions.service.test.js +219 -1
- package/dist-server/server/modules/providers/tests/sessions.service.test.js.map +1 -1
- package/dist-server/server/modules/providers/tests/skills.test.js +15 -3
- package/dist-server/server/modules/providers/tests/skills.test.js.map +1 -1
- package/dist-server/server/modules/scheduled-messages/index.js +5 -0
- package/dist-server/server/modules/scheduled-messages/index.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js +47 -0
- package/dist-server/server/modules/scheduled-messages/scheduled-messages.routes.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js +175 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js +95 -0
- package/dist-server/server/modules/scheduled-messages/services/scheduled-messages.service.js.map +1 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js +290 -0
- package/dist-server/server/modules/scheduled-messages/tests/scheduled-messages.test.js.map +1 -0
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js +23 -23
- package/dist-server/server/modules/taskmaster/taskmaster.routes.js.map +1 -1
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js +48 -0
- package/dist-server/server/modules/taskmaster/tests/taskmaster.routes.test.js.map +1 -1
- package/dist-server/server/modules/user/tests/user.service.test.js +70 -0
- package/dist-server/server/modules/user/tests/user.service.test.js.map +1 -1
- package/dist-server/server/modules/user/user.module.js +10 -1
- package/dist-server/server/modules/user/user.module.js.map +1 -1
- package/dist-server/server/modules/user/user.routes.js +44 -0
- package/dist-server/server/modules/user/user.routes.js.map +1 -1
- package/dist-server/server/modules/user/user.service.js +75 -0
- package/dist-server/server/modules/user/user.service.js.map +1 -1
- package/dist-server/server/modules/voice/tests/voice.service.test.js +3 -2
- package/dist-server/server/modules/voice/tests/voice.service.test.js.map +1 -1
- package/dist-server/server/modules/websocket/index.js +7 -0
- package/dist-server/server/modules/websocket/index.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js +37 -49
- package/dist-server/server/modules/websocket/services/chat-run-registry.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js +35 -5
- package/dist-server/server/modules/websocket/services/chat-session-writer.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js +204 -10
- package/dist-server/server/modules/websocket/services/chat-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js +94 -0
- package/dist-server/server/modules/websocket/services/session-upsert-broadcast.service.js.map +1 -0
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js +236 -20
- package/dist-server/server/modules/websocket/services/shell-websocket.service.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js +137 -0
- package/dist-server/server/modules/websocket/tests/chat-background-subscribe.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js +263 -0
- package/dist-server/server/modules/websocket/tests/chat-edit-send.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js +29 -3
- package/dist-server/server/modules/websocket/tests/chat-run-registry.test.js.map +1 -1
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js +96 -0
- package/dist-server/server/modules/websocket/tests/chat-stop-task.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js +106 -0
- package/dist-server/server/modules/websocket/tests/session-upsert-broadcast.test.js.map +1 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js +500 -0
- package/dist-server/server/modules/websocket/tests/shell-websocket.service.test.js.map +1 -1
- package/dist-server/server/shared/claude-cli-path.js +22 -5
- package/dist-server/server/shared/claude-cli-path.js.map +1 -1
- package/dist-server/server/shared/index.js +3 -0
- package/dist-server/server/shared/index.js.map +1 -0
- package/dist-server/server/shared/message-unification.js +376 -0
- package/dist-server/server/shared/message-unification.js.map +1 -0
- package/dist-server/server/shared/tests/claude-cli-path.test.js +45 -1
- package/dist-server/server/shared/tests/claude-cli-path.test.js.map +1 -1
- package/dist-server/server/shared/tests/message-unification.test.js +174 -0
- package/dist-server/server/shared/tests/message-unification.test.js.map +1 -0
- package/dist-server/server/shared/tests/read-only-roots.test.js +124 -0
- package/dist-server/server/shared/tests/read-only-roots.test.js.map +1 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js +31 -0
- package/dist-server/server/shared/tests/strip-ansi-sequences.test.js.map +1 -0
- package/dist-server/server/shared/utils.js +160 -0
- package/dist-server/server/shared/utils.js.map +1 -1
- package/electron/desktopWindow.js +6 -0
- package/electron/viewHost.js +25 -2
- package/package.json +20 -17
- package/server/index.ts +16 -5
- package/server/modules/agent/agent.module.ts +11 -1
- package/server/modules/agent/agent.routes.ts +200 -142
- package/server/modules/agent/tests/agent-run-registration.test.ts +348 -0
- package/server/modules/agent/tests/agent.routes.test.ts +51 -2
- package/server/modules/browser-use/browser-use.routes.ts +96 -33
- package/server/modules/database/connection.ts +2 -1
- package/server/modules/database/index.ts +10 -0
- package/server/modules/database/migrations.ts +51 -0
- package/server/modules/database/repositories/provider-models.ts +79 -6
- package/server/modules/database/repositories/scheduled-messages.db.ts +127 -0
- package/server/modules/database/repositories/session-drafts.db.ts +171 -0
- package/server/modules/database/repositories/sessions.db.ts +204 -3
- package/server/modules/database/repositories/user-preferences.db.ts +80 -0
- package/server/modules/database/schema.ts +103 -0
- package/server/modules/database/tests/provider-models.db.integration.test.ts +113 -0
- package/server/modules/database/tests/sessions.db.integration.test.ts +52 -0
- package/server/modules/database/tests/user-preferences.db.integration.test.ts +135 -0
- package/server/modules/file-tree/file-tree.module.ts +2 -1
- package/server/modules/file-tree/file-tree.service.ts +49 -7
- package/server/modules/file-tree/tests/file-tree.service.test.ts +163 -1
- package/server/modules/git/git-branch-diff.service.ts +204 -0
- package/server/modules/git/git-branch.service.ts +2 -12
- package/server/modules/git/git.routes.ts +100 -6
- package/server/modules/git/tests/git-branch-diff.service.test.ts +210 -0
- package/server/modules/projects/project-clone.routes.ts +142 -0
- package/server/modules/projects/projects.routes.ts +16 -81
- package/server/modules/projects/services/project-clone-request.service.ts +56 -0
- package/server/modules/projects/services/project-clone.service.ts +102 -34
- package/server/modules/projects/services/project-delete.service.ts +13 -1
- package/server/modules/projects/services/project-star.service.ts +2 -2
- package/server/modules/projects/tests/project-clone.routes.test.ts +246 -0
- package/server/modules/projects/tests/project-clone.service.test.ts +376 -0
- package/server/modules/providers/index.ts +4 -0
- package/server/modules/providers/list/claude/claude-auth.provider.ts +81 -11
- package/server/modules/providers/list/claude/claude-fork.provider.ts +57 -0
- package/server/modules/providers/list/claude/claude-models.provider.ts +37 -17
- package/server/modules/providers/list/claude/claude-runtime.provider.js +510 -54
- package/server/modules/providers/list/claude/claude-session-synchronizer.provider.ts +40 -14
- package/server/modules/providers/list/claude/claude-sessions.provider.ts +1195 -63
- package/server/modules/providers/list/claude/claude-skills.provider.ts +4 -1
- package/server/modules/providers/list/claude/claude.provider.ts +3 -0
- package/server/modules/providers/list/codex/codex-app-server.client.ts +243 -0
- package/server/modules/providers/list/codex/codex-fork.provider.ts +40 -0
- package/server/modules/providers/list/codex/codex-models.provider.ts +47 -0
- package/server/modules/providers/list/codex/{codex-runtime.provider.js → codex-runtime.provider.ts} +105 -70
- package/server/modules/providers/list/codex/codex-session-synchronizer.provider.ts +9 -0
- package/server/modules/providers/list/codex/codex-sessions.provider.ts +1847 -455
- package/server/modules/providers/list/codex/codex.provider.ts +3 -0
- package/server/modules/providers/list/cursor/cursor-auth.provider.ts +10 -2
- package/server/modules/providers/list/cursor/cursor-runtime.provider.js +17 -8
- package/server/modules/providers/list/cursor/cursor-session-synchronizer.provider.ts +0 -1
- package/server/modules/providers/list/opencode/opencode-models.provider.ts +297 -9
- package/server/modules/providers/list/opencode/opencode-runtime.provider.js +5 -2
- package/server/modules/providers/list/opencode/opencode-session-synchronizer.provider.ts +30 -2
- package/server/modules/providers/provider.routes.ts +162 -2
- package/server/modules/providers/services/claude-cli-liveness.service.ts +121 -0
- package/server/modules/providers/services/claude-settings-permissions.service.ts +127 -0
- package/server/modules/providers/services/provider-capabilities.service.ts +40 -1
- package/server/modules/providers/services/provider-models.service.ts +71 -4
- package/server/modules/providers/services/provider-runtime.service.ts +11 -0
- package/server/modules/providers/services/provider-token-usage.service.ts +166 -39
- package/server/modules/providers/services/session-conversations-search.service.ts +2 -5
- package/server/modules/providers/services/session-history-cache.service.ts +150 -0
- package/server/modules/providers/services/session-synchronizer.service.ts +125 -38
- package/server/modules/providers/services/sessions-watcher.service.ts +3 -64
- package/server/modules/providers/services/sessions.service.ts +309 -17
- package/server/modules/providers/tests/claude-auth.test.ts +208 -0
- package/server/modules/providers/tests/claude-background-work.test.ts +213 -0
- package/server/modules/providers/tests/claude-compaction.test.ts +101 -0
- package/server/modules/providers/tests/claude-message-model.test.ts +160 -0
- package/server/modules/providers/tests/claude-models.test.ts +87 -0
- package/server/modules/providers/tests/claude-runtime-hold.test.ts +309 -0
- package/server/modules/providers/tests/claude-sessions.test.ts +2167 -37
- package/server/modules/providers/tests/claude-settings-permissions.test.ts +271 -0
- package/server/modules/providers/tests/claude-subagent-echo.test.ts +25 -0
- package/server/modules/providers/tests/claude-token-budget.test.ts +104 -0
- package/server/modules/providers/tests/codex-message-editing.test.ts +419 -0
- package/server/modules/providers/tests/codex-models.test.ts +37 -0
- package/server/modules/providers/tests/codex-runtime.test.ts +94 -0
- package/server/modules/providers/tests/codex-session-fork.test.ts +59 -0
- package/server/modules/providers/tests/codex-sessions.test.ts +396 -15
- package/server/modules/providers/tests/cursor-auth.test.ts +187 -0
- package/server/modules/providers/tests/opencode-models.test.ts +266 -8
- package/server/modules/providers/tests/opencode-sessions.test.ts +40 -1
- package/server/modules/providers/tests/provider-capabilities.service.test.ts +37 -0
- package/server/modules/providers/tests/provider-models.service.test.ts +99 -1
- package/server/modules/providers/tests/provider-token-usage.service.test.ts +188 -1
- package/server/modules/providers/tests/provider.routes.test.ts +230 -1
- package/server/modules/providers/tests/session-fork.test.ts +173 -0
- package/server/modules/providers/tests/session-history-cache.service.test.ts +165 -0
- package/server/modules/providers/tests/session-orphan-prune.test.ts +107 -0
- package/server/modules/providers/tests/session-synchronizer-coalescing.test.ts +136 -0
- package/server/modules/providers/tests/sessions.service.test.ts +303 -1
- package/server/modules/providers/tests/skills.test.ts +21 -5
- package/server/modules/scheduled-messages/index.ts +8 -0
- package/server/modules/scheduled-messages/scheduled-messages.routes.ts +68 -0
- package/server/modules/scheduled-messages/services/scheduled-message-dispatcher.service.ts +216 -0
- package/server/modules/scheduled-messages/services/scheduled-messages.service.ts +121 -0
- package/server/modules/scheduled-messages/tests/scheduled-messages.test.ts +354 -0
- package/server/modules/taskmaster/taskmaster.routes.ts +23 -33
- package/server/modules/taskmaster/tests/taskmaster.routes.test.ts +57 -0
- package/server/modules/user/tests/user.service.test.ts +90 -0
- package/server/modules/user/user.module.ts +10 -1
- package/server/modules/user/user.routes.ts +44 -0
- package/server/modules/user/user.service.ts +105 -0
- package/server/modules/voice/tests/voice.service.test.ts +3 -2
- package/server/modules/websocket/README.md +4 -2
- package/server/modules/websocket/index.ts +8 -0
- package/server/modules/websocket/services/chat-run-registry.service.ts +45 -54
- package/server/modules/websocket/services/chat-session-writer.service.ts +36 -6
- package/server/modules/websocket/services/chat-websocket.service.ts +302 -16
- package/server/modules/websocket/services/session-upsert-broadcast.service.ts +108 -0
- package/server/modules/websocket/services/shell-websocket.service.ts +276 -22
- package/server/modules/websocket/tests/chat-background-subscribe.test.ts +167 -0
- package/server/modules/websocket/tests/chat-edit-send.test.ts +309 -0
- package/server/modules/websocket/tests/chat-run-registry.test.ts +34 -3
- package/server/modules/websocket/tests/chat-stop-task.test.ts +127 -0
- package/server/modules/websocket/tests/session-upsert-broadcast.test.ts +138 -0
- package/server/modules/websocket/tests/shell-websocket.service.test.ts +629 -0
- package/server/shared/claude-cli-path.ts +24 -6
- package/server/shared/index.ts +3 -0
- package/server/shared/interfaces.ts +91 -0
- package/server/shared/message-unification.ts +449 -0
- package/server/shared/tests/claude-cli-path.test.ts +58 -1
- package/server/shared/tests/message-unification.test.ts +218 -0
- package/server/shared/tests/read-only-roots.test.ts +139 -0
- package/server/shared/tests/strip-ansi-sequences.test.ts +42 -0
- package/server/shared/types.ts +382 -7
- package/server/shared/utils.ts +176 -0
- package/dist/assets/channel-_falYbU1.js +0 -1
- package/dist/assets/classDiagram-JCYQIIEL-Dn-3CfpR.js +0 -1
- package/dist/assets/classDiagram-v2-OCEON4UE-Dn-3CfpR.js +0 -1
- package/dist/assets/index-CzPLBvXT.css +0 -32
- package/dist/assets/index-DFkBBuSA.js +0 -1549
- package/dist/assets/stateDiagram-v2-UEYNNEHI-BvBxUUrZ.js +0 -1
- package/dist/assets/swimlanesDiagram-ULZ7WXOC-CsE-x4YK.js +0 -8
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import test from 'node:test';
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
closeConnection,
|
|
9
|
+
getConnection,
|
|
10
|
+
initializeDatabase,
|
|
11
|
+
sessionDraftsDb,
|
|
12
|
+
userPreferencesDb,
|
|
13
|
+
} from '@/modules/database/index.js';
|
|
14
|
+
|
|
15
|
+
const USER_ID = 1;
|
|
16
|
+
|
|
17
|
+
async function withDatabase(runTest: () => void | Promise<void>): Promise<void> {
|
|
18
|
+
const previousDatabasePath = process.env.DATABASE_PATH;
|
|
19
|
+
const tempDirectory = await mkdtemp(path.join(os.tmpdir(), 'user-prefs-db-'));
|
|
20
|
+
const databasePath = path.join(tempDirectory, 'auth.db');
|
|
21
|
+
|
|
22
|
+
closeConnection();
|
|
23
|
+
process.env.DATABASE_PATH = databasePath;
|
|
24
|
+
await writeFile(databasePath, '');
|
|
25
|
+
await initializeDatabase();
|
|
26
|
+
|
|
27
|
+
// Both tables cascade from users(id), so a row has to exist to write against.
|
|
28
|
+
getConnection()
|
|
29
|
+
.prepare('INSERT INTO users (id, username, password_hash) VALUES (?, ?, ?)')
|
|
30
|
+
.run(USER_ID, 'tester', 'hash');
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
await runTest();
|
|
34
|
+
} finally {
|
|
35
|
+
closeConnection();
|
|
36
|
+
if (previousDatabasePath === undefined) {
|
|
37
|
+
delete process.env.DATABASE_PATH;
|
|
38
|
+
} else {
|
|
39
|
+
process.env.DATABASE_PATH = previousDatabasePath;
|
|
40
|
+
}
|
|
41
|
+
await rm(tempDirectory, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('preferences round-trip every JSON shape a setting can take', async () => {
|
|
46
|
+
await withDatabase(() => {
|
|
47
|
+
userPreferencesDb.savePreferences(USER_ID, {
|
|
48
|
+
theme: 'dark',
|
|
49
|
+
tasksEnabled: false,
|
|
50
|
+
claudePermissions: { allowedTools: ['Read'], skipPermissions: true },
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), {
|
|
54
|
+
theme: 'dark',
|
|
55
|
+
tasksEnabled: false,
|
|
56
|
+
claudePermissions: { allowedTools: ['Read'], skipPermissions: true },
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('saving preferences merge-patches instead of replacing the whole set', async () => {
|
|
62
|
+
await withDatabase(() => {
|
|
63
|
+
userPreferencesDb.savePreferences(USER_ID, { theme: 'dark', userLanguage: 'de' });
|
|
64
|
+
userPreferencesDb.savePreferences(USER_ID, { theme: 'light' });
|
|
65
|
+
|
|
66
|
+
assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), {
|
|
67
|
+
theme: 'light',
|
|
68
|
+
userLanguage: 'de',
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('a preference set to undefined is removed', async () => {
|
|
74
|
+
await withDatabase(() => {
|
|
75
|
+
userPreferencesDb.savePreferences(USER_ID, { theme: 'dark', userLanguage: 'de' });
|
|
76
|
+
userPreferencesDb.savePreferences(USER_ID, { userLanguage: undefined });
|
|
77
|
+
|
|
78
|
+
assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), { theme: 'dark' });
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('one unreadable preference value does not cost the user the others', async () => {
|
|
83
|
+
await withDatabase(() => {
|
|
84
|
+
userPreferencesDb.savePreferences(USER_ID, { theme: 'dark' });
|
|
85
|
+
getConnection()
|
|
86
|
+
.prepare(
|
|
87
|
+
`INSERT INTO user_preferences (user_id, preference_key, preference_value)
|
|
88
|
+
VALUES (?, ?, ?)`
|
|
89
|
+
)
|
|
90
|
+
.run(USER_ID, 'broken', 'not json');
|
|
91
|
+
|
|
92
|
+
assert.deepEqual(userPreferencesDb.getPreferences(USER_ID), { theme: 'dark' });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test('drafts round-trip text and queued message per scope', async () => {
|
|
97
|
+
await withDatabase(() => {
|
|
98
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'half typed', queuedMessage: null });
|
|
99
|
+
sessionDraftsDb.saveDraft(USER_ID, 'project:p1', {
|
|
100
|
+
text: '',
|
|
101
|
+
queuedMessage: { content: 'run it' },
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const drafts = sessionDraftsDb.getDrafts(USER_ID);
|
|
105
|
+
const byScope = new Map(drafts.map((draft) => [draft.scope, draft]));
|
|
106
|
+
|
|
107
|
+
assert.equal(byScope.get('session-a')?.text, 'half typed');
|
|
108
|
+
assert.equal(byScope.get('session-a')?.queuedMessage, null);
|
|
109
|
+
assert.equal(byScope.get('project:p1')?.text, '');
|
|
110
|
+
assert.deepEqual(byScope.get('project:p1')?.queuedMessage, { content: 'run it' });
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test('saving an empty draft deletes the row rather than keeping a blank one', async () => {
|
|
115
|
+
await withDatabase(() => {
|
|
116
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'typed', queuedMessage: null });
|
|
117
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: '', queuedMessage: null });
|
|
118
|
+
|
|
119
|
+
assert.deepEqual(sessionDraftsDb.getDrafts(USER_ID), []);
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('deleteDraft removes only the named scope', async () => {
|
|
124
|
+
await withDatabase(() => {
|
|
125
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-a', { text: 'a', queuedMessage: null });
|
|
126
|
+
sessionDraftsDb.saveDraft(USER_ID, 'session-b', { text: 'b', queuedMessage: null });
|
|
127
|
+
|
|
128
|
+
sessionDraftsDb.deleteDraft(USER_ID, 'session-a');
|
|
129
|
+
|
|
130
|
+
assert.deepEqual(
|
|
131
|
+
sessionDraftsDb.getDrafts(USER_ID).map((draft) => draft.scope),
|
|
132
|
+
['session-b'],
|
|
133
|
+
);
|
|
134
|
+
});
|
|
135
|
+
});
|
|
@@ -14,7 +14,7 @@ import type {
|
|
|
14
14
|
FileTreeProjectGateway,
|
|
15
15
|
FileTreeWorkspaceGateway,
|
|
16
16
|
} from '@/shared/types.js';
|
|
17
|
-
import { WORKSPACES_ROOT, validateWorkspacePath } from '@/shared/utils.js';
|
|
17
|
+
import { WORKSPACES_ROOT, resolveReadOnlyRootPath, validateWorkspacePath } from '@/shared/utils.js';
|
|
18
18
|
|
|
19
19
|
const MAXIMUM_UPLOAD_SIZE_MEGABYTES = 200;
|
|
20
20
|
const MAXIMUM_UPLOAD_SIZE_BYTES = MAXIMUM_UPLOAD_SIZE_MEGABYTES * 1024 * 1024;
|
|
@@ -73,6 +73,7 @@ const fileTreeProjects: FileTreeProjectGateway = {
|
|
|
73
73
|
const fileTreeWorkspace: FileTreeWorkspaceGateway = {
|
|
74
74
|
rootPath: WORKSPACES_ROOT,
|
|
75
75
|
validatePath: (candidatePath) => validateWorkspacePath(candidatePath),
|
|
76
|
+
resolveReadOnlyRootPath: (candidatePath) => resolveReadOnlyRootPath(candidatePath),
|
|
76
77
|
};
|
|
77
78
|
|
|
78
79
|
const fileTreeLogger: FileTreeLogger = {
|
|
@@ -312,6 +312,51 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
|
|
|
312
312
|
});
|
|
313
313
|
}
|
|
314
314
|
|
|
315
|
+
/**
|
|
316
|
+
* Resolves a directory the browser may list.
|
|
317
|
+
*
|
|
318
|
+
* The read-only roots (the system temp directory and the Claude projects
|
|
319
|
+
* directory, see `READ_ONLY_ROOTS` in shared/utils) are browsable but never
|
|
320
|
+
* writable, so they are checked before the workspace policy rejects them.
|
|
321
|
+
* `createWorkspaceFolder` deliberately does not do this, which is what keeps
|
|
322
|
+
* them read-only.
|
|
323
|
+
*/
|
|
324
|
+
async function resolveBrowsablePath(targetPath: string): Promise<string> {
|
|
325
|
+
const readOnlyPath = await dependencies.workspace.resolveReadOnlyRootPath(targetPath);
|
|
326
|
+
if (readOnlyPath) {
|
|
327
|
+
return readOnlyPath;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const validation = await dependencies.workspace.validatePath(targetPath);
|
|
331
|
+
if (!validation.valid) {
|
|
332
|
+
throw createFileTreeError(validation.error ?? 'Path is outside the workspace root', 403, 'INVALID_WORKSPACE_PATH');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
return validation.resolvedPath || targetPath;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Resolves a path the viewer is allowed to read.
|
|
340
|
+
*
|
|
341
|
+
* A file reference in a transcript is normally inside the project, but a
|
|
342
|
+
* background command's log lives in the system temp directory and a
|
|
343
|
+
* background agent's output file is a link from there into the Claude
|
|
344
|
+
* projects directory, and the transcript quotes those paths verbatim. Those
|
|
345
|
+
* resolve through the read-only root policy instead of the project root; the
|
|
346
|
+
* writing endpoints keep using `resolvePathInsideProject` alone, so nothing
|
|
347
|
+
* outside the project can be changed.
|
|
348
|
+
*/
|
|
349
|
+
async function resolveReadablePath(projectRoot: string, targetPath: string): Promise<string> {
|
|
350
|
+
if (path.isAbsolute(targetPath)) {
|
|
351
|
+
const readOnlyPath = await dependencies.workspace.resolveReadOnlyRootPath(targetPath);
|
|
352
|
+
if (readOnlyPath) {
|
|
353
|
+
return readOnlyPath;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return resolvePathInsideProject(projectRoot, targetPath);
|
|
358
|
+
}
|
|
359
|
+
|
|
315
360
|
async function cleanupTemporaryFiles(files: FileTreeUploadedFile[]): Promise<void> {
|
|
316
361
|
await Promise.all(files.map(async (file) => {
|
|
317
362
|
try {
|
|
@@ -328,12 +373,8 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
|
|
|
328
373
|
? expandWorkspacePath(dependencies.workspace.rootPath, inputPath)
|
|
329
374
|
: dependencies.workspace.rootPath;
|
|
330
375
|
const targetPath = path.resolve(requestedPath);
|
|
331
|
-
const
|
|
332
|
-
if (!validation.valid) {
|
|
333
|
-
throw createFileTreeError(validation.error ?? 'Path is outside the workspace root', 403, 'INVALID_WORKSPACE_PATH');
|
|
334
|
-
}
|
|
376
|
+
const resolvedPath = await resolveBrowsablePath(targetPath);
|
|
335
377
|
|
|
336
|
-
const resolvedPath = validation.resolvedPath || targetPath;
|
|
337
378
|
try {
|
|
338
379
|
await fileSystem.access(resolvedPath);
|
|
339
380
|
const stats = await fileSystem.stat(resolvedPath);
|
|
@@ -409,7 +450,7 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
|
|
|
409
450
|
|
|
410
451
|
async readTextFile(projectId, filePath) {
|
|
411
452
|
const projectRoot = await resolveProjectRoot(projectId);
|
|
412
|
-
const resolvedPath =
|
|
453
|
+
const resolvedPath = await resolveReadablePath(projectRoot, filePath);
|
|
413
454
|
try {
|
|
414
455
|
const content = await fileSystem.readTextFile(resolvedPath);
|
|
415
456
|
return { content, path: resolvedPath };
|
|
@@ -417,13 +458,14 @@ export function createFileTreeService(dependencies: FileTreeServiceDependencies)
|
|
|
417
458
|
mapFileSystemError(error, {
|
|
418
459
|
ENOENT: { message: 'File not found', statusCode: 404 },
|
|
419
460
|
EACCES: { message: 'Permission denied', statusCode: 403 },
|
|
461
|
+
EISDIR: { message: 'Path is a directory, not a file', statusCode: 400 },
|
|
420
462
|
});
|
|
421
463
|
}
|
|
422
464
|
},
|
|
423
465
|
|
|
424
466
|
async openFile(projectId, filePath) {
|
|
425
467
|
const projectRoot = await resolveProjectRoot(projectId);
|
|
426
|
-
const resolvedPath =
|
|
468
|
+
const resolvedPath = await resolveReadablePath(projectRoot, filePath);
|
|
427
469
|
try {
|
|
428
470
|
await fileSystem.access(resolvedPath);
|
|
429
471
|
} catch {
|
|
@@ -1,16 +1,23 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
|
+
import { createReadStream } from 'node:fs';
|
|
3
|
+
import fsPromises from 'node:fs/promises';
|
|
4
|
+
import os from 'node:os';
|
|
2
5
|
import path from 'node:path';
|
|
3
6
|
import { Readable } from 'node:stream';
|
|
4
7
|
import test from 'node:test';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
5
9
|
|
|
6
10
|
import { createFileTreeService } from '@/modules/file-tree/file-tree.service.js';
|
|
7
11
|
import type {
|
|
8
12
|
FileTreeDirectoryEntry,
|
|
9
13
|
FileTreeFileSystem,
|
|
10
14
|
FileTreeServiceDependencies,
|
|
15
|
+
FileTreeServices,
|
|
11
16
|
FileTreeStats,
|
|
12
17
|
} from '@/shared/types.js';
|
|
13
|
-
import { AppError } from '@/shared/utils.js';
|
|
18
|
+
import { AppError, resolveReadOnlyRootPath, validateWorkspacePath } from '@/shared/utils.js';
|
|
19
|
+
|
|
20
|
+
const testDirectory = path.dirname(fileURLToPath(import.meta.url));
|
|
14
21
|
|
|
15
22
|
function createDirectoryEntry(name: string, directory: boolean): FileTreeDirectoryEntry {
|
|
16
23
|
return {
|
|
@@ -80,6 +87,7 @@ function createDependencies(
|
|
|
80
87
|
workspace: {
|
|
81
88
|
rootPath: projectRoot,
|
|
82
89
|
validatePath: async (candidatePath) => ({ valid: true, resolvedPath: candidatePath }),
|
|
90
|
+
resolveReadOnlyRootPath: async () => null,
|
|
83
91
|
},
|
|
84
92
|
resolveMimeType: () => 'text/plain',
|
|
85
93
|
fileSystemConcurrency: 4,
|
|
@@ -343,6 +351,26 @@ test('readTextFile rejects traversal before invoking the filesystem adapter', as
|
|
|
343
351
|
assert.deepEqual(readPaths, []);
|
|
344
352
|
});
|
|
345
353
|
|
|
354
|
+
test('readTextFile reports a directory as a client error, not a 500', async () => {
|
|
355
|
+
const projectRoot = path.resolve('file-tree-test-project');
|
|
356
|
+
const fileSystem = createFakeFileSystem({
|
|
357
|
+
readTextFile: async () => {
|
|
358
|
+
const error = new Error('EISDIR: illegal operation on a directory, read');
|
|
359
|
+
(error as NodeJS.ErrnoException).code = 'EISDIR';
|
|
360
|
+
throw error;
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
const service = createFileTreeService(createDependencies(fileSystem, projectRoot));
|
|
364
|
+
|
|
365
|
+
await assert.rejects(
|
|
366
|
+
service.readTextFile('project-1', 'decisions'),
|
|
367
|
+
(error: unknown) => error instanceof AppError
|
|
368
|
+
&& error.code === 'EISDIR'
|
|
369
|
+
&& error.statusCode === 400
|
|
370
|
+
&& error.message === 'Path is a directory, not a file',
|
|
371
|
+
);
|
|
372
|
+
});
|
|
373
|
+
|
|
346
374
|
test('createEntry performs filesystem mutation only through the injected adapter', async () => {
|
|
347
375
|
const projectRoot = path.resolve('file-tree-test-project');
|
|
348
376
|
const targetPath = path.join(projectRoot, 'notes.txt');
|
|
@@ -369,3 +397,137 @@ test('createEntry performs filesystem mutation only through the injected adapter
|
|
|
369
397
|
assert.equal(result.path, targetPath);
|
|
370
398
|
assert.deepEqual(writtenFiles, [{ filePath: targetPath, content: '' }]);
|
|
371
399
|
});
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Builds the service against the real filesystem and the real workspace policy,
|
|
403
|
+
* which is the only way to exercise the read-only roots: the whole guarantee
|
|
404
|
+
* rests on `realpath` resolving symlinks before the comparison.
|
|
405
|
+
*/
|
|
406
|
+
function createRealFileSystemService(projectRoot: string): FileTreeServices {
|
|
407
|
+
return createFileTreeService({
|
|
408
|
+
fileSystem: {
|
|
409
|
+
access: (candidatePath) => fsPromises.access(candidatePath),
|
|
410
|
+
stat: (candidatePath) => fsPromises.stat(candidatePath),
|
|
411
|
+
lstat: (candidatePath) => fsPromises.lstat(candidatePath),
|
|
412
|
+
openDirectory: async function* (directoryPath) {
|
|
413
|
+
yield* await fsPromises.opendir(directoryPath);
|
|
414
|
+
},
|
|
415
|
+
realpath: (candidatePath) => fsPromises.realpath(candidatePath),
|
|
416
|
+
readTextFile: (filePath) => fsPromises.readFile(filePath, 'utf8'),
|
|
417
|
+
writeTextFile: (filePath, content) => fsPromises.writeFile(filePath, content, 'utf8'),
|
|
418
|
+
async makeDirectory(directoryPath, recursive) {
|
|
419
|
+
await fsPromises.mkdir(directoryPath, { recursive });
|
|
420
|
+
},
|
|
421
|
+
rename: (oldPath, newPath) => fsPromises.rename(oldPath, newPath),
|
|
422
|
+
async removeDirectory(directoryPath) {
|
|
423
|
+
await fsPromises.rm(directoryPath, { recursive: true, force: true });
|
|
424
|
+
},
|
|
425
|
+
unlink: (filePath) => fsPromises.unlink(filePath),
|
|
426
|
+
copyFile: (source, destination) => fsPromises.copyFile(source, destination),
|
|
427
|
+
createReadStream: (filePath) => createReadStream(filePath),
|
|
428
|
+
},
|
|
429
|
+
projects: { getProjectPathById: async () => projectRoot },
|
|
430
|
+
workspace: {
|
|
431
|
+
rootPath: projectRoot,
|
|
432
|
+
validatePath: (candidatePath) => validateWorkspacePath(candidatePath),
|
|
433
|
+
resolveReadOnlyRootPath: (candidatePath) => resolveReadOnlyRootPath(candidatePath),
|
|
434
|
+
},
|
|
435
|
+
resolveMimeType: () => 'text/plain',
|
|
436
|
+
fileSystemConcurrency: 4,
|
|
437
|
+
logger: { error: () => undefined },
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
test('the temp directory can be browsed and read, but never written to', async () => {
|
|
442
|
+
const temporaryDirectory = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'file-tree-tmp-'));
|
|
443
|
+
const projectRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'file-tree-project-'));
|
|
444
|
+
|
|
445
|
+
try {
|
|
446
|
+
// The shape a task notification quotes: an agent's output file written to
|
|
447
|
+
// the system temp directory, outside every project.
|
|
448
|
+
await fsPromises.mkdir(path.join(temporaryDirectory, 'tasks'));
|
|
449
|
+
const outputPath = path.join(temporaryDirectory, 'tasks', 'agent.output');
|
|
450
|
+
await fsPromises.writeFile(outputPath, 'what the agent found', 'utf8');
|
|
451
|
+
|
|
452
|
+
const service = createRealFileSystemService(projectRoot);
|
|
453
|
+
|
|
454
|
+
const browsed = await service.browseWorkspace(temporaryDirectory);
|
|
455
|
+
assert.deepEqual(browsed.suggestions.map((entry) => entry.name), ['tasks']);
|
|
456
|
+
|
|
457
|
+
const opened = await service.readTextFile('project-1', outputPath);
|
|
458
|
+
assert.equal(opened.content, 'what the agent found');
|
|
459
|
+
|
|
460
|
+
const streamed = await service.openFile('project-1', outputPath);
|
|
461
|
+
streamed.stream.destroy();
|
|
462
|
+
|
|
463
|
+
// Read-only means read-only: nothing may be created or changed there.
|
|
464
|
+
await assert.rejects(
|
|
465
|
+
service.saveTextFile('project-1', outputPath, 'overwritten'),
|
|
466
|
+
(error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
|
|
467
|
+
);
|
|
468
|
+
await assert.rejects(
|
|
469
|
+
service.createWorkspaceFolder(path.join(temporaryDirectory, 'new-folder')),
|
|
470
|
+
(error: unknown) => (error as AppError).code === 'INVALID_WORKSPACE_PATH',
|
|
471
|
+
);
|
|
472
|
+
await assert.rejects(
|
|
473
|
+
service.createEntry({
|
|
474
|
+
projectId: 'project-1',
|
|
475
|
+
parentPath: path.join(temporaryDirectory, 'tasks'),
|
|
476
|
+
type: 'file',
|
|
477
|
+
name: 'planted.txt',
|
|
478
|
+
}),
|
|
479
|
+
(error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
|
|
480
|
+
);
|
|
481
|
+
await assert.rejects(
|
|
482
|
+
service.renameEntry({ projectId: 'project-1', oldPath: outputPath, newName: 'renamed.output' }),
|
|
483
|
+
(error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
|
|
484
|
+
);
|
|
485
|
+
const uploadedTemporaryPath = path.join(projectRoot, 'upload.tmp');
|
|
486
|
+
await fsPromises.writeFile(uploadedTemporaryPath, 'upload', 'utf8');
|
|
487
|
+
await assert.rejects(
|
|
488
|
+
service.storeUploadedFiles({
|
|
489
|
+
projectId: 'project-1',
|
|
490
|
+
targetPath: path.join(temporaryDirectory, 'tasks'),
|
|
491
|
+
relativePaths: [],
|
|
492
|
+
requestedFileCount: 1,
|
|
493
|
+
files: [{ originalName: 'upload.txt', temporaryPath: uploadedTemporaryPath, size: 6, mimeType: 'text/plain' }],
|
|
494
|
+
}),
|
|
495
|
+
(error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
|
|
496
|
+
);
|
|
497
|
+
await assert.rejects(
|
|
498
|
+
service.deleteEntry({ projectId: 'project-1', targetPath: outputPath }),
|
|
499
|
+
(error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
|
|
500
|
+
);
|
|
501
|
+
assert.deepEqual(await fsPromises.readdir(path.join(temporaryDirectory, 'tasks')), ['agent.output']);
|
|
502
|
+
assert.equal(await fsPromises.readFile(outputPath, 'utf8'), 'what the agent found');
|
|
503
|
+
} finally {
|
|
504
|
+
await fsPromises.rm(temporaryDirectory, { recursive: true, force: true });
|
|
505
|
+
await fsPromises.rm(projectRoot, { recursive: true, force: true });
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
test('reading through a symlink out of the temp directory is still refused', async () => {
|
|
510
|
+
const temporaryDirectory = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'file-tree-tmp-'));
|
|
511
|
+
const projectRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'file-tree-project-'));
|
|
512
|
+
// The link target has to sit under no read-only root. Beside this file is
|
|
513
|
+
// deterministic; a directory under `$HOME` lands under `/tmp` whenever a
|
|
514
|
+
// test run isolates its home there, and the Claude projects directory is a
|
|
515
|
+
// read-only root too.
|
|
516
|
+
const outsideDirectory = await fsPromises.mkdtemp(path.join(testDirectory, 'file-tree-outside-'));
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
assert.equal(await resolveReadOnlyRootPath(outsideDirectory), null);
|
|
520
|
+
await fsPromises.writeFile(path.join(outsideDirectory, 'secret.txt'), 'secret', 'utf8');
|
|
521
|
+
await fsPromises.symlink(outsideDirectory, path.join(temporaryDirectory, 'escape'));
|
|
522
|
+
|
|
523
|
+
const service = createRealFileSystemService(projectRoot);
|
|
524
|
+
await assert.rejects(
|
|
525
|
+
service.readTextFile('project-1', path.join(temporaryDirectory, 'escape', 'secret.txt')),
|
|
526
|
+
(error: unknown) => (error as AppError).code === 'PATH_OUTSIDE_PROJECT',
|
|
527
|
+
);
|
|
528
|
+
} finally {
|
|
529
|
+
await fsPromises.rm(temporaryDirectory, { recursive: true, force: true });
|
|
530
|
+
await fsPromises.rm(projectRoot, { recursive: true, force: true });
|
|
531
|
+
await fsPromises.rm(outsideDirectory, { recursive: true, force: true });
|
|
532
|
+
}
|
|
533
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import type { GitProcessRunner } from '@/shared/types.js';
|
|
4
|
+
import { AppError, getGitErrorDetails } from '@/shared/utils.js';
|
|
5
|
+
|
|
6
|
+
// Single-letter status the Compare tab renders as a badge. Git's own
|
|
7
|
+
// `--name-status` letters are folded into this set (see `toBranchDiffStatus`).
|
|
8
|
+
type BranchDiffFileStatus = 'M' | 'A' | 'D' | 'R' | 'U';
|
|
9
|
+
|
|
10
|
+
type BranchDiffFile = {
|
|
11
|
+
path: string;
|
|
12
|
+
/** Pre-rename path, only present for `R` entries. */
|
|
13
|
+
oldPath?: string;
|
|
14
|
+
status: BranchDiffFileStatus;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type BranchDiffInput = {
|
|
18
|
+
/** Repository root — every reported path is relative to it. */
|
|
19
|
+
projectPath: string;
|
|
20
|
+
/** Branch or remote-tracking ref the working copy is compared against. */
|
|
21
|
+
base: string;
|
|
22
|
+
runCommand: GitProcessRunner;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
type BranchDiffFileInput = BranchDiffInput & {
|
|
26
|
+
/** Repository-relative path of the file to diff. */
|
|
27
|
+
file: string;
|
|
28
|
+
/**
|
|
29
|
+
* Pre-rename path for `R` entries. Diffing only the new path would make git
|
|
30
|
+
* report the whole file as added, so both paths are handed to git together
|
|
31
|
+
* and it pairs them into a rename with just the real content changes.
|
|
32
|
+
*/
|
|
33
|
+
oldPath?: string;
|
|
34
|
+
/** Reads an untracked file so its content can be rendered as an all-additions diff. */
|
|
35
|
+
readFile: (absolutePath: string) => Promise<string>;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
function toBranchDiffStatus(rawStatus: string): BranchDiffFileStatus {
|
|
39
|
+
// `--name-status` emits R<score>/C<score> for renames and copies; a copy is
|
|
40
|
+
// a brand-new file from the reader's point of view, and type changes (T) are
|
|
41
|
+
// modifications.
|
|
42
|
+
switch (rawStatus.charAt(0)) {
|
|
43
|
+
case 'A':
|
|
44
|
+
case 'C':
|
|
45
|
+
return 'A';
|
|
46
|
+
case 'D':
|
|
47
|
+
return 'D';
|
|
48
|
+
case 'R':
|
|
49
|
+
return 'R';
|
|
50
|
+
default:
|
|
51
|
+
return 'M';
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseNameStatus(output: string): BranchDiffFile[] {
|
|
56
|
+
// `-z` output is `<status>\0<path>\0`, with renames/copies contributing a
|
|
57
|
+
// second path: `R100\0<old>\0<new>\0`. A trailing NUL leaves an empty token.
|
|
58
|
+
const tokens = output.split('\0');
|
|
59
|
+
const files: BranchDiffFile[] = [];
|
|
60
|
+
|
|
61
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
62
|
+
const rawStatus = tokens[index];
|
|
63
|
+
if (!rawStatus) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const status = toBranchDiffStatus(rawStatus);
|
|
68
|
+
const isTwoPathEntry = rawStatus.startsWith('R') || rawStatus.startsWith('C');
|
|
69
|
+
const firstPath = tokens[index + 1];
|
|
70
|
+
const secondPath = isTwoPathEntry ? tokens[index + 2] : undefined;
|
|
71
|
+
index += isTwoPathEntry ? 2 : 1;
|
|
72
|
+
|
|
73
|
+
if (!firstPath) {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isTwoPathEntry) {
|
|
78
|
+
if (!secondPath) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
files.push(status === 'R' ? { path: secondPath, oldPath: firstPath, status } : { path: secondPath, status });
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
files.push({ path: firstPath, status });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return files;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseUntrackedPaths(output: string): string[] {
|
|
92
|
+
return output.split('\0').filter(Boolean);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Turns a user-chosen base branch into the merge-base commit shared with HEAD.
|
|
97
|
+
* Rejects refs that would parse as git options and refs git cannot resolve
|
|
98
|
+
* before running `merge-base`, so the routes answer with a 400 and a stable
|
|
99
|
+
* code instead of a raw git failure.
|
|
100
|
+
*/
|
|
101
|
+
async function resolveMergeBase(input: BranchDiffInput): Promise<string> {
|
|
102
|
+
const { projectPath, base, runCommand } = input;
|
|
103
|
+
const cwd = { cwd: projectPath };
|
|
104
|
+
|
|
105
|
+
// validateBranchName in the routes accepts a leading '-', which git would
|
|
106
|
+
// otherwise read as an option (e.g. `--output=file`).
|
|
107
|
+
if (!base || base.startsWith('-')) {
|
|
108
|
+
throw new AppError(`Invalid base branch "${base}"`, {
|
|
109
|
+
code: 'GIT_INVALID_BASE_REF',
|
|
110
|
+
statusCode: 400,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
await runCommand('git', ['rev-parse', '--verify', '--quiet', '--end-of-options', `${base}^{commit}`], cwd);
|
|
116
|
+
} catch {
|
|
117
|
+
throw new AppError(`Unknown branch or ref "${base}"`, {
|
|
118
|
+
code: 'GIT_UNKNOWN_BASE_REF',
|
|
119
|
+
statusCode: 400,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
let mergeBaseOutput: string;
|
|
124
|
+
try {
|
|
125
|
+
({ stdout: mergeBaseOutput } = await runCommand('git', ['merge-base', '--', base, 'HEAD'], cwd));
|
|
126
|
+
} catch (error) {
|
|
127
|
+
throw new AppError(`"${base}" and the current branch share no common history`, {
|
|
128
|
+
code: 'GIT_NO_MERGE_BASE',
|
|
129
|
+
statusCode: 400,
|
|
130
|
+
details: getGitErrorDetails(error),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const mergeBase = mergeBaseOutput.trim();
|
|
135
|
+
if (!mergeBase) {
|
|
136
|
+
throw new AppError(`"${base}" and the current branch share no common history`, {
|
|
137
|
+
code: 'GIT_NO_MERGE_BASE',
|
|
138
|
+
statusCode: 400,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return mergeBase;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Used by the Git routes module for `GET /branch-diff`: lists every file the
|
|
147
|
+
* working copy (committed, staged, unstaged and untracked) differs in from the
|
|
148
|
+
* merge base of `base` and HEAD — the PR-style "what this branch changed".
|
|
149
|
+
* Results are sorted by path.
|
|
150
|
+
*/
|
|
151
|
+
export async function listBranchDiffFiles(input: BranchDiffInput): Promise<{
|
|
152
|
+
base: string;
|
|
153
|
+
mergeBase: string;
|
|
154
|
+
files: BranchDiffFile[];
|
|
155
|
+
}> {
|
|
156
|
+
const { projectPath, base, runCommand } = input;
|
|
157
|
+
const cwd = { cwd: projectPath };
|
|
158
|
+
const mergeBase = await resolveMergeBase(input);
|
|
159
|
+
|
|
160
|
+
// `git diff <commit>` compares the working tree (index + unstaged edits)
|
|
161
|
+
// against the commit, which is exactly the "everything so far" view.
|
|
162
|
+
const [{ stdout: nameStatusOutput }, { stdout: untrackedOutput }] = await Promise.all([
|
|
163
|
+
runCommand('git', ['diff', '--name-status', '-M', '-z', mergeBase, '--'], cwd),
|
|
164
|
+
runCommand('git', ['ls-files', '--others', '--exclude-standard', '-z'], cwd),
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
const files = [
|
|
168
|
+
...parseNameStatus(nameStatusOutput),
|
|
169
|
+
...parseUntrackedPaths(untrackedOutput).map((path): BranchDiffFile => ({ path, status: 'U' })),
|
|
170
|
+
].sort((left, right) => left.path.localeCompare(right.path));
|
|
171
|
+
|
|
172
|
+
return { base, mergeBase, files };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Used by the Git routes module for `GET /branch-diff/file`: the raw unified
|
|
177
|
+
* diff of one file between the merge base and the working copy. Untracked
|
|
178
|
+
* files are not visible to `git diff`, so their content is synthesized into
|
|
179
|
+
* the same all-additions shape the `/diff` route produces.
|
|
180
|
+
*/
|
|
181
|
+
export async function getBranchDiffForFile(input: BranchDiffFileInput): Promise<string> {
|
|
182
|
+
const { projectPath, file, oldPath, runCommand, readFile } = input;
|
|
183
|
+
const cwd = { cwd: projectPath };
|
|
184
|
+
const mergeBase = await resolveMergeBase(input);
|
|
185
|
+
|
|
186
|
+
// Ask git (not the filesystem) whether the path is untracked so a path that
|
|
187
|
+
// escapes the repository is rejected by git before anything is read.
|
|
188
|
+
const { stdout: untrackedOutput } = await runCommand(
|
|
189
|
+
'git',
|
|
190
|
+
['ls-files', '--others', '--exclude-standard', '-z', '--', file],
|
|
191
|
+
cwd,
|
|
192
|
+
);
|
|
193
|
+
const isUntracked = parseUntrackedPaths(untrackedOutput).includes(file);
|
|
194
|
+
|
|
195
|
+
if (isUntracked) {
|
|
196
|
+
const content = await readFile(path.join(projectPath, file));
|
|
197
|
+
const lines = content.split('\n');
|
|
198
|
+
return `--- /dev/null\n+++ b/${file}\n@@ -0,0 +1,${lines.length} @@\n${lines.map((line) => `+${line}`).join('\n')}`;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const pathspecs = oldPath && oldPath !== file ? [oldPath, file] : [file];
|
|
202
|
+
const { stdout: diff } = await runCommand('git', ['diff', '-M', mergeBase, '--', ...pathspecs], cwd);
|
|
203
|
+
return diff;
|
|
204
|
+
}
|
|
@@ -1,21 +1,11 @@
|
|
|
1
|
+
import type { GitProcessRunner } from '@/shared/types.js';
|
|
1
2
|
import { AppError } from '@/shared/utils.js';
|
|
2
3
|
|
|
3
|
-
type GitCommandResult = {
|
|
4
|
-
stdout: string;
|
|
5
|
-
stderr: string;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
type GitCommandRunner = (
|
|
9
|
-
command: string,
|
|
10
|
-
args: string[],
|
|
11
|
-
options: { cwd: string },
|
|
12
|
-
) => Promise<GitCommandResult>;
|
|
13
|
-
|
|
14
4
|
type DeleteLocalBranchInput = {
|
|
15
5
|
projectPath: string;
|
|
16
6
|
branch: string;
|
|
17
7
|
force: boolean;
|
|
18
|
-
runCommand:
|
|
8
|
+
runCommand: GitProcessRunner;
|
|
19
9
|
};
|
|
20
10
|
|
|
21
11
|
/** Used by the Git routes module to safely delete a non-current local branch. */
|