@thaut/claude-ide 0.0.0-stage → 1.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/bin/claude-ide.js +23 -0
- package/package.json +75 -4
- package/server/add-dirs.ts +126 -0
- package/server/auth.test.ts +148 -0
- package/server/auth.ts +235 -0
- package/server/bang.ts +54 -0
- package/server/btw-instructions.ts +19 -0
- package/server/btw-policy.ts +17 -0
- package/server/btw.ts +143 -0
- package/server/child-env.ts +29 -0
- package/server/cli-control.ts +255 -0
- package/server/cli-files.ts +55 -0
- package/server/cli-info.ts +73 -0
- package/server/config/account.ts +65 -0
- package/server/config/agents.ts +186 -0
- package/server/config/mcp.ts +86 -0
- package/server/config/memory.ts +193 -0
- package/server/config/paths.ts +156 -0
- package/server/config/plugins.ts +204 -0
- package/server/config/routes.ts +95 -0
- package/server/config/settings.ts +246 -0
- package/server/config.ts +46 -0
- package/server/db.ts +121 -0
- package/server/directories.test.ts +180 -0
- package/server/directories.ts +87 -0
- package/server/dist-version.ts +69 -0
- package/server/durable-tasks.ts +61 -0
- package/server/file-transfers.ts +79 -0
- package/server/files.test.ts +127 -0
- package/server/files.ts +386 -0
- package/server/fs-queue.ts +14 -0
- package/server/fsutil.ts +144 -0
- package/server/git.ts +86 -0
- package/server/goal-state.test.ts +64 -0
- package/server/goal-state.ts +78 -0
- package/server/history-pages.test.ts +619 -0
- package/server/history-pages.ts +513 -0
- package/server/http-channel-manager.ts +468 -0
- package/server/http-channel-pull.test.ts +314 -0
- package/server/http-channel-push.test.ts +364 -0
- package/server/http-channel-registration.test.ts +291 -0
- package/server/http-channel-routes.ts +73 -0
- package/server/http-channel-store.ts +142 -0
- package/server/http-channel-test-fixture.ts +103 -0
- package/server/ide-messages-mcp.test.ts +82 -0
- package/server/ide-messages-mcp.ts +47 -0
- package/server/idecron-mcp.ts +65 -0
- package/server/idecron.test.ts +79 -0
- package/server/idecron.ts +271 -0
- package/server/index.ts +336 -0
- package/server/live-session.test.ts +1126 -0
- package/server/live-session.ts +1165 -0
- package/server/project-routes.ts +46 -0
- package/server/projects.test.ts +28 -0
- package/server/push.ts +315 -0
- package/server/report-cache.ts +28 -0
- package/server/session-aliases.test.ts +57 -0
- package/server/session-aliases.ts +50 -0
- package/server/session-branch.ts +71 -0
- package/server/session-dirs.ts +189 -0
- package/server/session-file.test.ts +134 -0
- package/server/session-file.ts +113 -0
- package/server/session-messaging.test.ts +297 -0
- package/server/session-messaging.ts +165 -0
- package/server/sessions.ts +638 -0
- package/server/subagents.test.ts +43 -0
- package/server/subagents.ts +58 -0
- package/server/tool-images.test.ts +76 -0
- package/server/tool-images.ts +45 -0
- package/server/turn-changes.ts +308 -0
- package/server/worktree-git.ts +87 -0
- package/server/worktrees.ts +138 -0
- package/server/ws.test.ts +195 -0
- package/server/ws.ts +197 -0
- package/shared/agent-model.ts +15 -0
- package/shared/agent-tasks.ts +12 -0
- package/shared/config.ts +175 -0
- package/shared/directories.ts +12 -0
- package/shared/files.ts +51 -0
- package/shared/http-channels.ts +63 -0
- package/shared/message-window.ts +189 -0
- package/shared/project.ts +77 -0
- package/shared/protocol.ts +203 -0
- package/shared/task-types.ts +4 -0
- package/shared/todos.ts +40 -0
- package/shared/write-policy.ts +9 -0
- package/web/dist/assets/AgentView-wrTKgcJ3.js +1 -0
- package/web/dist/assets/AnnotationSelection-BZmNmwVW.js +7 -0
- package/web/dist/assets/ChangesView-G1WTzh4q.js +2 -0
- package/web/dist/assets/FilesView-DPgAbYt_.js +6 -0
- package/web/dist/assets/Form-BKTwiYyw.js +1 -0
- package/web/dist/assets/HomePage-Dq2apjBe.js +1 -0
- package/web/dist/assets/Markdown-IwP-xF29.js +67 -0
- package/web/dist/assets/MessageList-DD9GypSy.js +15 -0
- package/web/dist/assets/Modal-g2rv8I_n.js +1 -0
- package/web/dist/assets/NotificationSettings-DpuLO_Bv.js +1 -0
- package/web/dist/assets/OverviewPage-Dx6miabE.js +1 -0
- package/web/dist/assets/ProjectHome-CousyTLL.js +1 -0
- package/web/dist/assets/SessionView-CPVL7_HO.js +2 -0
- package/web/dist/assets/SettingsPage-D_oYSnW2.js +7 -0
- package/web/dist/assets/WorktreesView-C7e0HV__.js +1 -0
- package/web/dist/assets/abap-CLvhMVsD.js +1 -0
- package/web/dist/assets/actionscript-3-Cs6JJXT5.js +1 -0
- package/web/dist/assets/ada-X3Y2mlq2.js +1 -0
- package/web/dist/assets/ahk-DwADkxyd.js +1 -0
- package/web/dist/assets/ahk2-_6rzW9BS.js +1 -0
- package/web/dist/assets/andromeeda-vGVdxbeo.js +1 -0
- package/web/dist/assets/angular-html-Cw29lun8.js +1 -0
- package/web/dist/assets/angular-ts-BXz-O3SJ.js +1 -0
- package/web/dist/assets/apache-U0d_L8uA.js +1 -0
- package/web/dist/assets/apex-KFGFyKwG.js +1 -0
- package/web/dist/assets/apl-xBchdRhy.js +1 -0
- package/web/dist/assets/applescript-CCn79oCD.js +1 -0
- package/web/dist/assets/ara-4CJ0cIlV.js +1 -0
- package/web/dist/assets/asciidoc-SCjQUq34.js +1 -0
- package/web/dist/assets/asm-Cmm7eHzH.js +1 -0
- package/web/dist/assets/astro-1q6DGuN3.js +1 -0
- package/web/dist/assets/aurora-x-CDeNXAV0.js +1 -0
- package/web/dist/assets/awk-BWXHIvNe.js +1 -0
- package/web/dist/assets/ayu-dark-DluEY0Gj.js +1 -0
- package/web/dist/assets/ayu-light-C3h-C4tm.js +1 -0
- package/web/dist/assets/ayu-mirage-Bqwy1Gya.js +1 -0
- package/web/dist/assets/ballerina-B7ZEbQpA.js +1 -0
- package/web/dist/assets/bat-qdxAdBWQ.js +1 -0
- package/web/dist/assets/beancount-C2IN4Vk8.js +1 -0
- package/web/dist/assets/berry-DKpUyyne.js +1 -0
- package/web/dist/assets/bibtex-Ci_nEsc7.js +1 -0
- package/web/dist/assets/bicep-CUHmPFLl.js +1 -0
- package/web/dist/assets/bird2-CYIUgYqB.js +1 -0
- package/web/dist/assets/blade-Cqdv4oZN.js +1 -0
- package/web/dist/assets/bsl-CVED6lJ1.js +1 -0
- package/web/dist/assets/c-D9gIxSfx.js +1 -0
- package/web/dist/assets/c3-D0apG41V.js +1 -0
- package/web/dist/assets/cadence-CQ2zXKGN.js +1 -0
- package/web/dist/assets/cairo-DRAZz75c.js +1 -0
- package/web/dist/assets/catppuccin-frappe-C9Q5zVZY.js +1 -0
- package/web/dist/assets/catppuccin-latte-CtsyrtPq.js +1 -0
- package/web/dist/assets/catppuccin-macchiato-R8nAf1vR.js +1 -0
- package/web/dist/assets/catppuccin-mocha-DjuaAJKi.js +1 -0
- package/web/dist/assets/chapel-CuseGurG.js +1 -0
- package/web/dist/assets/clarity-CtfBBspg.js +1 -0
- package/web/dist/assets/clojure-DqKBuwfJ.js +1 -0
- package/web/dist/assets/cmake-Bj61d0ZC.js +1 -0
- package/web/dist/assets/cobol-Bdpj3Qoh.js +1 -0
- package/web/dist/assets/codeowners-C8r90Shi.js +1 -0
- package/web/dist/assets/codeql-oeQT6MSM.js +1 -0
- package/web/dist/assets/coffee-Sk8pHFjs.js +1 -0
- package/web/dist/assets/common-lisp-Cv5bFMCO.js +1 -0
- package/web/dist/assets/coq-CGaSfhwR.js +1 -0
- package/web/dist/assets/core-BxI4V4-T.js +12 -0
- package/web/dist/assets/cpp-BQfBYKx3.js +1 -0
- package/web/dist/assets/crystal-B3j7DoOZ.js +1 -0
- package/web/dist/assets/csharp-dmMxLVLW.js +1 -0
- package/web/dist/assets/css-DpFbYmRh.js +1 -0
- package/web/dist/assets/csv-Dx-8-gkx.js +1 -0
- package/web/dist/assets/cue-CE9AQfxI.js +1 -0
- package/web/dist/assets/cypher-ClKdZ_lG.js +1 -0
- package/web/dist/assets/d-qD-0Kul2.js +1 -0
- package/web/dist/assets/dark-plus-Cs2F2srj.js +1 -0
- package/web/dist/assets/dart-CHZY9Xhz.js +1 -0
- package/web/dist/assets/dax-BkyTk9wS.js +1 -0
- package/web/dist/assets/desktop-Dlh5hvp9.js +1 -0
- package/web/dist/assets/diff-woXpYk--.js +1 -0
- package/web/dist/assets/docker-IyjqRm3v.js +1 -0
- package/web/dist/assets/dotenv-CuH5my32.js +1 -0
- package/web/dist/assets/dracula-BHWKrbxM.js +1 -0
- package/web/dist/assets/dracula-soft-5eyTD99u.js +1 -0
- package/web/dist/assets/dream-maker-DW3nJb8Q.js +1 -0
- package/web/dist/assets/edge-DXQWmXTB.js +1 -0
- package/web/dist/assets/elixir-CrDIY2Vo.js +1 -0
- package/web/dist/assets/elm-BslFEg8K.js +1 -0
- package/web/dist/assets/emacs-lisp-DR2vtWaj.js +1 -0
- package/web/dist/assets/engine-javascript-DJg3-Vu2.js +141 -0
- package/web/dist/assets/erb-CUYhg0Ws.js +1 -0
- package/web/dist/assets/erlang-BXdgHVfi.js +1 -0
- package/web/dist/assets/everforest-dark-sB-x3p7T.js +1 -0
- package/web/dist/assets/everforest-light-Df2xbC6M.js +1 -0
- package/web/dist/assets/fennel-DQxkIbk2.js +1 -0
- package/web/dist/assets/fish-BJitypiv.js +1 -0
- package/web/dist/assets/fluent-C03EYrpw.js +1 -0
- package/web/dist/assets/format-DrBC7w0A.js +1 -0
- package/web/dist/assets/fortran-fixed-form-D6mN0kjy.js +1 -0
- package/web/dist/assets/fortran-free-form-CYNrtFtB.js +1 -0
- package/web/dist/assets/fsharp-CwoEF39o.js +1 -0
- package/web/dist/assets/gdresource-CZkFGo3B.js +1 -0
- package/web/dist/assets/gdscript-DYvRD3hg.js +1 -0
- package/web/dist/assets/gdshader-CBce3t8t.js +1 -0
- package/web/dist/assets/genie-CV2tkWYe.js +1 -0
- package/web/dist/assets/gherkin-DExj1W_8.js +1 -0
- package/web/dist/assets/git-commit-dUGrvm6Q.js +1 -0
- package/web/dist/assets/git-rebase-4zCRqF28.js +1 -0
- package/web/dist/assets/github-dark-C-LZuMrd.js +1 -0
- package/web/dist/assets/github-dark-default-DXG-b-1a.js +1 -0
- package/web/dist/assets/github-dark-dimmed-Bx1FflLF.js +1 -0
- package/web/dist/assets/github-dark-high-contrast-B_tTalzw.js +1 -0
- package/web/dist/assets/github-light-EUqPIrTm.js +1 -0
- package/web/dist/assets/github-light-default-BXViO-2h.js +1 -0
- package/web/dist/assets/github-light-high-contrast-B68TUdTA.js +1 -0
- package/web/dist/assets/gleam-CSRkHgEL.js +1 -0
- package/web/dist/assets/glimmer-js-7adTRvH4.js +1 -0
- package/web/dist/assets/glimmer-ts-IdSskDYw.js +1 -0
- package/web/dist/assets/glsl-Qzy53KDt.js +1 -0
- package/web/dist/assets/gn-CAXYAgV-.js +1 -0
- package/web/dist/assets/gnuplot-7GGW24-e.js +1 -0
- package/web/dist/assets/go-rLFTqkRN.js +1 -0
- package/web/dist/assets/graphql-CV_puxbB.js +1 -0
- package/web/dist/assets/groovy-CacY0gHj.js +1 -0
- package/web/dist/assets/gruvbox-dark-hard-C820rvS2.js +1 -0
- package/web/dist/assets/gruvbox-dark-medium-BPjhmG05.js +1 -0
- package/web/dist/assets/gruvbox-dark-soft-MrdJrrXF.js +1 -0
- package/web/dist/assets/gruvbox-light-hard-BC_s9l72.js +1 -0
- package/web/dist/assets/gruvbox-light-medium-BAWPOn9u.js +1 -0
- package/web/dist/assets/gruvbox-light-soft-BSMLrYjP.js +1 -0
- package/web/dist/assets/hack-C5GPjiLV.js +1 -0
- package/web/dist/assets/haml-BQ5YR1DQ.js +1 -0
- package/web/dist/assets/handlebars-DnhRK9A1.js +1 -0
- package/web/dist/assets/haskell-D8IpX4py.js +1 -0
- package/web/dist/assets/haxe-C8oCnwJJ.js +1 -0
- package/web/dist/assets/hcl-Dh228itO.js +1 -0
- package/web/dist/assets/highlight.worker-CY-ZeDAl.js +1 -0
- package/web/dist/assets/hjson-CxZEssPk.js +1 -0
- package/web/dist/assets/hlsl-Cvrh5tZx.js +1 -0
- package/web/dist/assets/horizon-CE9ld1lL.js +1 -0
- package/web/dist/assets/horizon-bright-DSNQnXHK.js +1 -0
- package/web/dist/assets/houston-CsvMBhTu.js +1 -0
- package/web/dist/assets/html-CmHiEKV9.js +1 -0
- package/web/dist/assets/html-derivative-BXE-rYIK.js +1 -0
- package/web/dist/assets/http-DPjjqtcL.js +1 -0
- package/web/dist/assets/hurl-AMBpSloO.js +1 -0
- package/web/dist/assets/hxml-D7Y24wKF.js +1 -0
- package/web/dist/assets/hy-CZbG8q4J.js +1 -0
- package/web/dist/assets/imba-DsUTQ-LC.js +1 -0
- package/web/dist/assets/index-DNPnl4sh.css +2 -0
- package/web/dist/assets/index-DQh72lEx.js +14 -0
- package/web/dist/assets/ini-B5eOa1yu.js +1 -0
- package/web/dist/assets/java-CEM7Tdw_.js +1 -0
- package/web/dist/assets/javascript-CU3PLowd.js +1 -0
- package/web/dist/assets/jinja-fR0x_9Xy.js +1 -0
- package/web/dist/assets/jison-BoKsxs5M.js +1 -0
- package/web/dist/assets/json-BM2FWsRB.js +1 -0
- package/web/dist/assets/json5-BR5RXkoi.js +1 -0
- package/web/dist/assets/jsonc-CYpm1nAK.js +1 -0
- package/web/dist/assets/jsonl-CmCQp5Yx.js +1 -0
- package/web/dist/assets/jsonnet-CJTPZ8u_.js +1 -0
- package/web/dist/assets/jssm-DXw9l8Rf.js +1 -0
- package/web/dist/assets/jsx-B5fTecQP.js +1 -0
- package/web/dist/assets/julia-DGyDG39_.js +1 -0
- package/web/dist/assets/just-B-RyLcSR.js +1 -0
- package/web/dist/assets/kanagawa-dragon-Bozd7klA.js +1 -0
- package/web/dist/assets/kanagawa-lotus-BmuOG9ZW.js +1 -0
- package/web/dist/assets/kanagawa-wave-BML5mF2J.js +1 -0
- package/web/dist/assets/kdl-CsD5j6eV.js +1 -0
- package/web/dist/assets/kotlin-DhhofPvG.js +1 -0
- package/web/dist/assets/kusto-C7mF5XQf.js +1 -0
- package/web/dist/assets/langs-CciPKqaF.js +1 -0
- package/web/dist/assets/laserwave-C_8bwKvT.js +1 -0
- package/web/dist/assets/latex-CV1pQelm.js +1 -0
- package/web/dist/assets/lean-CewbzKMR.js +1 -0
- package/web/dist/assets/less-DVTAwKKz.js +1 -0
- package/web/dist/assets/lib-DlcVMUqT.js +1 -0
- package/web/dist/assets/light-plus-DVQuIRkW.js +1 -0
- package/web/dist/assets/liquid-9p05RSzQ.js +1 -0
- package/web/dist/assets/llvm-DqHOMS-s.js +1 -0
- package/web/dist/assets/log-BNLmms1o.js +1 -0
- package/web/dist/assets/logo-Cluzi2Zq.js +1 -0
- package/web/dist/assets/lua-CIPVh0S-.js +1 -0
- package/web/dist/assets/luau-VGWVLoqX.js +1 -0
- package/web/dist/assets/make-Dixweg8N.js +1 -0
- package/web/dist/assets/markdown-BYOwaDjH.js +1 -0
- package/web/dist/assets/marko-h2h6YUMS.js +1 -0
- package/web/dist/assets/material-theme-Bm3Qr25_.js +1 -0
- package/web/dist/assets/material-theme-darker-2IIEA8gg.js +1 -0
- package/web/dist/assets/material-theme-lighter-uhdI0v04.js +1 -0
- package/web/dist/assets/material-theme-ocean-CHQ94UKr.js +1 -0
- package/web/dist/assets/material-theme-palenight-B5W6OYN7.js +1 -0
- package/web/dist/assets/matlab-D7qyCx1q.js +1 -0
- package/web/dist/assets/mdc-yAyZMdoC.js +1 -0
- package/web/dist/assets/mdx-DQZ5AkYe.js +1 -0
- package/web/dist/assets/mermaid-BxpfTIzt.js +1 -0
- package/web/dist/assets/message-font-size-DoaZfwkT.js +1 -0
- package/web/dist/assets/min-dark-BSWPekZh.js +1 -0
- package/web/dist/assets/min-light-DDpmG2fV.js +1 -0
- package/web/dist/assets/mipsasm-BMqwQI7S.js +1 -0
- package/web/dist/assets/mojo-fg40_ntQ.js +1 -0
- package/web/dist/assets/monokai-BscAjiQZ.js +1 -0
- package/web/dist/assets/moonbit-D9W784E_.js +1 -0
- package/web/dist/assets/move-BYnGhZoM.js +1 -0
- package/web/dist/assets/narrat-_X_XdTYD.js +1 -0
- package/web/dist/assets/nextflow-BoTnSBQ_.js +1 -0
- package/web/dist/assets/nextflow-groovy-D7IRt6Pp.js +1 -0
- package/web/dist/assets/nginx-jWINucNT.js +1 -0
- package/web/dist/assets/night-owl-DhmEMT88.js +1 -0
- package/web/dist/assets/night-owl-light-eJ-hLW7d.js +1 -0
- package/web/dist/assets/nim-Dtt9DZ0t.js +1 -0
- package/web/dist/assets/nix-Cl_7024t.js +1 -0
- package/web/dist/assets/nord-Cb4Vim4T.js +1 -0
- package/web/dist/assets/nsis-CjNLYCLg.js +1 -0
- package/web/dist/assets/nushell--0fT3PI1.js +1 -0
- package/web/dist/assets/objective-c-D1A_Heim.js +1 -0
- package/web/dist/assets/objective-cpp-BsSzOQcm.js +1 -0
- package/web/dist/assets/ocaml-O90oeIOV.js +1 -0
- package/web/dist/assets/odin-B1RWQWA5.js +1 -0
- package/web/dist/assets/one-dark-pro-C6YyR8OD.js +1 -0
- package/web/dist/assets/one-light-D7Lr4KcI.js +1 -0
- package/web/dist/assets/openscad-BUDT5pXO.js +1 -0
- package/web/dist/assets/org-ClD5huOa.js +1 -0
- package/web/dist/assets/pascal-4ZHwLPI5.js +1 -0
- package/web/dist/assets/perl-BFegxyeJ.js +1 -0
- package/web/dist/assets/php-B-qOA3Y4.js +1 -0
- package/web/dist/assets/pkl-ot-7Btpt.js +1 -0
- package/web/dist/assets/plastic-DQwYfKfQ.js +1 -0
- package/web/dist/assets/plsql-DGHpHOYJ.js +1 -0
- package/web/dist/assets/po-BiJDBrnU.js +1 -0
- package/web/dist/assets/poimandres-DRFjx7u4.js +1 -0
- package/web/dist/assets/polar-C7UOKdEL.js +1 -0
- package/web/dist/assets/postcss-BXeXVLqQ.js +1 -0
- package/web/dist/assets/powerquery-DNMTfnFr.js +1 -0
- package/web/dist/assets/powershell-QtSnmXcu.js +1 -0
- package/web/dist/assets/prisma-DMgNhnYS.js +1 -0
- package/web/dist/assets/prolog-iXnhIJG7.js +1 -0
- package/web/dist/assets/proto-DB4EqR-F.js +1 -0
- package/web/dist/assets/pug-Cq0NBPu-.js +1 -0
- package/web/dist/assets/puppet-CDv2pdJW.js +1 -0
- package/web/dist/assets/purescript-9MfHhQsQ.js +1 -0
- package/web/dist/assets/python-gzcpVVnB.js +1 -0
- package/web/dist/assets/qml-CYUsa87k.js +1 -0
- package/web/dist/assets/qmldir-DCQb3MpD.js +1 -0
- package/web/dist/assets/qss-Fe1Jh2GI.js +1 -0
- package/web/dist/assets/r-4xJhgbOf.js +1 -0
- package/web/dist/assets/racket-DcIDlBhZ.js +1 -0
- package/web/dist/assets/raku-B3gFvitq.js +1 -0
- package/web/dist/assets/razor-BlHaOHx8.js +1 -0
- package/web/dist/assets/rbs-C0MpOrj4.js +1 -0
- package/web/dist/assets/react-B4UGpcWC.js +1 -0
- package/web/dist/assets/red-CYlAq2b0.js +1 -0
- package/web/dist/assets/reg-CRGYupPL.js +1 -0
- package/web/dist/assets/regexp-BUSxjv2o.js +1 -0
- package/web/dist/assets/rel-BtDbiS_P.js +1 -0
- package/web/dist/assets/riscv-Ckw8ddFX.js +1 -0
- package/web/dist/assets/ron-VUp2lXgN.js +1 -0
- package/web/dist/assets/rose-pine-aMsaMD1D.js +1 -0
- package/web/dist/assets/rose-pine-dawn-8PwUyIlP.js +1 -0
- package/web/dist/assets/rose-pine-moon-DO-qXg-I.js +1 -0
- package/web/dist/assets/rosmsg-CAekHB0j.js +1 -0
- package/web/dist/assets/rst-zjfguEh7.js +1 -0
- package/web/dist/assets/ruby-4oR9waBK.js +1 -0
- package/web/dist/assets/rust-DpiIf5JO.js +1 -0
- package/web/dist/assets/sas-Do4V_2i9.js +1 -0
- package/web/dist/assets/sass-DXrisJhu.js +1 -0
- package/web/dist/assets/scala-BiFt_7mu.js +1 -0
- package/web/dist/assets/scheme-DQCgrYNe.js +1 -0
- package/web/dist/assets/scss-CW6JPtag.js +1 -0
- package/web/dist/assets/sdbl-bTVj8UrX.js +1 -0
- package/web/dist/assets/shaderlab-C4rnI7x0.js +1 -0
- package/web/dist/assets/shellscript-zBmOMFJh.js +1 -0
- package/web/dist/assets/shellsession-DxGUTWaf.js +1 -0
- package/web/dist/assets/slack-dark-DnToyrRv.js +1 -0
- package/web/dist/assets/slack-ochin-B2OO5cIa.js +1 -0
- package/web/dist/assets/smalltalk-BHpQb5nM.js +1 -0
- package/web/dist/assets/smithy-Dgaz7aur.js +1 -0
- package/web/dist/assets/snazzy-light-4G7pJPwS.js +1 -0
- package/web/dist/assets/solarized-dark-b97czJOk.js +1 -0
- package/web/dist/assets/solarized-light-DFPxpIYQ.js +1 -0
- package/web/dist/assets/solidity-Bj7E1mRn.js +1 -0
- package/web/dist/assets/soy-DVtw20Yn.js +1 -0
- package/web/dist/assets/sparql-C0ia5K60.js +1 -0
- package/web/dist/assets/splunk-BC2Px7Mm.js +1 -0
- package/web/dist/assets/sql-DGCM-jag.js +1 -0
- package/web/dist/assets/ssh-config-BgfXC-Er.js +1 -0
- package/web/dist/assets/stata-DfuMOXna.js +1 -0
- package/web/dist/assets/stylus-B6D30XZt.js +1 -0
- package/web/dist/assets/surrealql-CBoRWxfR.js +1 -0
- package/web/dist/assets/svelte-g_uCUem5.js +1 -0
- package/web/dist/assets/swift-CyEgAFGc.js +1 -0
- package/web/dist/assets/synthwave-84-nFMaYfgc.js +1 -0
- package/web/dist/assets/system-verilog-Dh5ihWbf.js +1 -0
- package/web/dist/assets/systemd-BxMlprV5.js +1 -0
- package/web/dist/assets/talonscript-CohzipZa.js +1 -0
- package/web/dist/assets/tasl-DMoTqEGO.js +1 -0
- package/web/dist/assets/tcl-CZd0xW_V.js +1 -0
- package/web/dist/assets/templ-N5C1hJ4Y.js +1 -0
- package/web/dist/assets/terraform-DswuEJGm.js +1 -0
- package/web/dist/assets/tex-mx6xDwuK.js +1 -0
- package/web/dist/assets/themes-CNElGaXw.js +1 -0
- package/web/dist/assets/tokyo-night-oM2G3aXe.js +1 -0
- package/web/dist/assets/toml-CcmNWLt0.js +1 -0
- package/web/dist/assets/ts-tags-C-j71FRb.js +1 -0
- package/web/dist/assets/tsv-sltzmVWM.js +1 -0
- package/web/dist/assets/tsx-z04y7qR9.js +1 -0
- package/web/dist/assets/turtle-ByJddavk.js +1 -0
- package/web/dist/assets/twig-Bz57G_nl.js +1 -0
- package/web/dist/assets/typescript-RMqVTmzR.js +1 -0
- package/web/dist/assets/typespec-BGhR2SqG.js +1 -0
- package/web/dist/assets/typst-BgkqA2Pg.js +1 -0
- package/web/dist/assets/v-DBj4K9d_.js +1 -0
- package/web/dist/assets/vala-zf12oZj6.js +1 -0
- package/web/dist/assets/vb-DOwzaHZ0.js +1 -0
- package/web/dist/assets/verilog-B2gwz-sJ.js +1 -0
- package/web/dist/assets/vesper-DdrHHSXu.js +1 -0
- package/web/dist/assets/vhdl-BroJfC0k.js +1 -0
- package/web/dist/assets/viml-DvXPmvsu.js +1 -0
- package/web/dist/assets/vitesse-black-fwtXNY1n.js +1 -0
- package/web/dist/assets/vitesse-dark-BZCL-v6S.js +1 -0
- package/web/dist/assets/vitesse-light-VbXTXTou.js +1 -0
- package/web/dist/assets/vue-CQgvQytb.js +1 -0
- package/web/dist/assets/vue-html-Dhl6fh9l.js +1 -0
- package/web/dist/assets/vue-vine-Dw_uU59K.js +1 -0
- package/web/dist/assets/vyper-CgoNMtux.js +1 -0
- package/web/dist/assets/wasm-ByWQv1Qj.js +1 -0
- package/web/dist/assets/wenyan-C8pVoKbM.js +1 -0
- package/web/dist/assets/wgsl-BsKzXJz4.js +1 -0
- package/web/dist/assets/wikitext-ClFFjSW2.js +1 -0
- package/web/dist/assets/wit-DdvCle-K.js +1 -0
- package/web/dist/assets/wolfram-DLL8P-h_.js +1 -0
- package/web/dist/assets/xml-B49klYQ3.js +1 -0
- package/web/dist/assets/xsl-D7tnD3qq.js +1 -0
- package/web/dist/assets/yaml-DNQ5_xkL.js +1 -0
- package/web/dist/assets/zenscript-BnlCZFoB.js +1 -0
- package/web/dist/assets/zig-CMLA9XwU.js +1 -0
- package/web/dist/icons/apple-touch-icon.png +0 -0
- package/web/dist/icons/badge-96.png +0 -0
- package/web/dist/icons/icon-192.png +0 -0
- package/web/dist/icons/icon-512.png +0 -0
- package/web/dist/icons/icon.svg +4 -0
- package/web/dist/icons/maskable-192.png +0 -0
- package/web/dist/icons/maskable-512.png +0 -0
- package/web/dist/index.html +30 -0
- package/web/dist/manifest.webmanifest +20 -0
- package/web/dist/sw.js +176 -0
- package/web/dist/version.json +1 -0
- package/web/src/App.tsx +97 -0
- package/web/src/agent-history-flow.test.ts +191 -0
- package/web/src/agent-history-flow.ts +58 -0
- package/web/src/agent-owners.ts +30 -0
- package/web/src/annotation-selection.test.ts +36 -0
- package/web/src/annotation-selection.ts +123 -0
- package/web/src/annotations.test.ts +279 -0
- package/web/src/annotations.ts +191 -0
- package/web/src/api.ts +27 -0
- package/web/src/auth-events.ts +11 -0
- package/web/src/auth-lifecycle.test.ts +105 -0
- package/web/src/clipboard.ts +32 -0
- package/web/src/components/ActivityNotifier.tsx +22 -0
- package/web/src/components/AnnotationEditor.tsx +135 -0
- package/web/src/components/AnnotationSelection.tsx +122 -0
- package/web/src/components/Automation.tsx +390 -0
- package/web/src/components/CodeView.tsx +121 -0
- package/web/src/components/Composer.tsx +552 -0
- package/web/src/components/ComposerAnnotations.tsx +45 -0
- package/web/src/components/Confirmation.tsx +91 -0
- package/web/src/components/CopyButton.tsx +23 -0
- package/web/src/components/DiffView.tsx +94 -0
- package/web/src/components/DirectoryPicker.tsx +230 -0
- package/web/src/components/FileCopyMenu.tsx +58 -0
- package/web/src/components/FileEntryActions.tsx +95 -0
- package/web/src/components/ImagePreview.tsx +68 -0
- package/web/src/components/Items.tsx +608 -0
- package/web/src/components/Markdown.tsx +73 -0
- package/web/src/components/MessageList.tsx +458 -0
- package/web/src/components/Modal.tsx +72 -0
- package/web/src/components/NotificationEntry.tsx +31 -0
- package/web/src/components/NotificationSettings.tsx +226 -0
- package/web/src/components/NotificationToasts.tsx +59 -0
- package/web/src/components/OpenTabs.tsx +112 -0
- package/web/src/components/Pending.tsx +175 -0
- package/web/src/components/ProjectExtras.tsx +299 -0
- package/web/src/components/ProjectNav.tsx +47 -0
- package/web/src/components/SessionActions.tsx +271 -0
- package/web/src/components/SessionChannels.tsx +288 -0
- package/web/src/components/SessionExtras.tsx +263 -0
- package/web/src/components/SettingsLink.tsx +21 -0
- package/web/src/components/Shell.tsx +320 -0
- package/web/src/components/SvgPreview.tsx +18 -0
- package/web/src/components/TodoPanel.tsx +37 -0
- package/web/src/components/VersionNotice.tsx +40 -0
- package/web/src/components/icons.tsx +43 -0
- package/web/src/diff.ts +62 -0
- package/web/src/drafts.test.ts +80 -0
- package/web/src/drafts.ts +64 -0
- package/web/src/format.ts +5 -0
- package/web/src/highlight.worker.ts +49 -0
- package/web/src/hooks.ts +59 -0
- package/web/src/item-view-state.test.ts +51 -0
- package/web/src/item-view-state.ts +43 -0
- package/web/src/live-history-flow.test.ts +77 -0
- package/web/src/live-merge.test.ts +64 -0
- package/web/src/live-merge.ts +50 -0
- package/web/src/live-message-hydration.test.ts +253 -0
- package/web/src/live.ts +404 -0
- package/web/src/main.tsx +27 -0
- package/web/src/markdown-assets.test.ts +70 -0
- package/web/src/markdown-assets.ts +78 -0
- package/web/src/markdown-html.test.ts +84 -0
- package/web/src/markdown-html.ts +60 -0
- package/web/src/markdown-svg.test.ts +37 -0
- package/web/src/markdown-svg.ts +42 -0
- package/web/src/markdown-tables.test.ts +34 -0
- package/web/src/markdown-tables.ts +17 -0
- package/web/src/message-bodies.ts +117 -0
- package/web/src/message-cache.test.ts +208 -0
- package/web/src/message-cache.ts +158 -0
- package/web/src/message-font-size.test.ts +41 -0
- package/web/src/message-font-size.ts +57 -0
- package/web/src/message-time.test.ts +32 -0
- package/web/src/message-time.ts +26 -0
- package/web/src/notify.ts +132 -0
- package/web/src/open-tabs.test.ts +69 -0
- package/web/src/open-tabs.ts +110 -0
- package/web/src/pages/AgentView.tsx +138 -0
- package/web/src/pages/ChangesView.tsx +193 -0
- package/web/src/pages/FilesView.tsx +406 -0
- package/web/src/pages/HomePage.tsx +96 -0
- package/web/src/pages/LoginPage.tsx +56 -0
- package/web/src/pages/OverviewPage.tsx +124 -0
- package/web/src/pages/ProjectHome.tsx +136 -0
- package/web/src/pages/SessionView.tsx +238 -0
- package/web/src/pages/WorktreesView.tsx +197 -0
- package/web/src/pages/settings/AccountPage.tsx +149 -0
- package/web/src/pages/settings/AgentsPage.tsx +182 -0
- package/web/src/pages/settings/ConfigPage.tsx +181 -0
- package/web/src/pages/settings/GeneralPage.tsx +27 -0
- package/web/src/pages/settings/HooksPage.tsx +244 -0
- package/web/src/pages/settings/McpPage.tsx +310 -0
- package/web/src/pages/settings/MemoryPage.tsx +309 -0
- package/web/src/pages/settings/PermissionsPage.tsx +160 -0
- package/web/src/pages/settings/PluginsPage.tsx +251 -0
- package/web/src/pages/settings/SettingsPage.tsx +141 -0
- package/web/src/pages/settings/SkillsPage.tsx +142 -0
- package/web/src/pages/settings/ui.tsx +165 -0
- package/web/src/preference-document.ts +133 -0
- package/web/src/project-actions.ts +18 -0
- package/web/src/project-files-state.test.ts +49 -0
- package/web/src/project-files-state.ts +64 -0
- package/web/src/project-images.ts +13 -0
- package/web/src/pwa.ts +33 -0
- package/web/src/queryClient.ts +5 -0
- package/web/src/reasoning-preference.test.ts +39 -0
- package/web/src/reasoning-preference.ts +42 -0
- package/web/src/session-confirmations.ts +36 -0
- package/web/src/stream-replay.test.ts +163 -0
- package/web/src/styles.css +231 -0
- package/web/src/tool-result-body.test.ts +115 -0
- package/web/src/transcript.test.ts +186 -0
- package/web/src/transcript.ts +583 -0
- package/web/src/turns.test.ts +338 -0
- package/web/src/turns.ts +178 -0
- package/web/src/ui/Button.tsx +60 -0
- package/web/src/ui/Controls.tsx +227 -0
- package/web/src/ui/Form.tsx +203 -0
- package/web/src/ui/MenuPopover.tsx +144 -0
- package/web/src/ui/Popover.tsx +147 -0
- package/web/src/ui/ResizablePane.tsx +110 -0
- package/web/src/ui/Select.tsx +150 -0
- package/web/src/ui/icons.tsx +270 -0
- package/web/src/unsaved.tsx +99 -0
- package/web/src/user-text.test.ts +60 -0
- package/web/src/user-text.ts +35 -0
- package/web/src/uuid.ts +12 -0
- package/README.md +0 -3
package/server/db.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { DatabaseSync } from "node:sqlite";
|
|
3
|
+
import { config } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* IDE-owned state (views, authentication, HTTP registrations) lives here.
|
|
7
|
+
* The CLI remains the source of truth for conversations (see GOAL.md "CLI 是唯一的真相源").
|
|
8
|
+
* Session content, titles, tags and fork origin are read from the CLI's own files.
|
|
9
|
+
*/
|
|
10
|
+
export const db = new DatabaseSync(path.join(config.dataDir, "ide.sqlite"));
|
|
11
|
+
|
|
12
|
+
db.exec(`PRAGMA journal_mode = WAL;`);
|
|
13
|
+
|
|
14
|
+
const migrations: string[] = [
|
|
15
|
+
`CREATE TABLE auth_tokens (
|
|
16
|
+
token TEXT PRIMARY KEY,
|
|
17
|
+
created_at INTEGER NOT NULL,
|
|
18
|
+
last_seen_at INTEGER NOT NULL
|
|
19
|
+
);
|
|
20
|
+
CREATE TABLE projects (
|
|
21
|
+
path TEXT PRIMARY KEY,
|
|
22
|
+
added_at INTEGER NOT NULL,
|
|
23
|
+
sort_order INTEGER NOT NULL DEFAULT 0
|
|
24
|
+
);
|
|
25
|
+
CREATE TABLE session_view (
|
|
26
|
+
session_id TEXT PRIMARY KEY,
|
|
27
|
+
project_path TEXT NOT NULL,
|
|
28
|
+
pinned INTEGER NOT NULL DEFAULT 0,
|
|
29
|
+
archived INTEGER NOT NULL DEFAULT 0,
|
|
30
|
+
btw_parent TEXT
|
|
31
|
+
);`,
|
|
32
|
+
// A conversation rewind waiting for the next message (applied with resumeSessionAt).
|
|
33
|
+
`ALTER TABLE session_view ADD COLUMN rewind_anchor TEXT;
|
|
34
|
+
ALTER TABLE session_view ADD COLUMN rewind_text TEXT;`,
|
|
35
|
+
// /btw: the side question and the uuid of the prompt that asked it in the forked session.
|
|
36
|
+
`ALTER TABLE session_view ADD COLUMN btw_question TEXT;
|
|
37
|
+
ALTER TABLE session_view ADD COLUMN btw_prompt_uuid TEXT;`,
|
|
38
|
+
// Web Push subscriptions, one per device, with per-kind switches (server/push.ts).
|
|
39
|
+
`CREATE TABLE push_subscriptions (
|
|
40
|
+
id TEXT PRIMARY KEY,
|
|
41
|
+
endpoint TEXT NOT NULL UNIQUE,
|
|
42
|
+
p256dh TEXT NOT NULL,
|
|
43
|
+
auth TEXT NOT NULL,
|
|
44
|
+
device TEXT,
|
|
45
|
+
user_agent TEXT,
|
|
46
|
+
created_at INTEGER NOT NULL,
|
|
47
|
+
last_success_at INTEGER,
|
|
48
|
+
notify_done INTEGER NOT NULL DEFAULT 1,
|
|
49
|
+
notify_attention INTEGER NOT NULL DEFAULT 1,
|
|
50
|
+
notify_error INTEGER NOT NULL DEFAULT 1
|
|
51
|
+
);`,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
migrations.push(`ALTER TABLE session_view ADD COLUMN rewind_id TEXT;`);
|
|
55
|
+
migrations.push(`
|
|
56
|
+
CREATE TABLE http_sources (
|
|
57
|
+
id TEXT PRIMARY KEY,
|
|
58
|
+
name TEXT NOT NULL,
|
|
59
|
+
url TEXT NOT NULL,
|
|
60
|
+
headers_json TEXT NOT NULL DEFAULT '{}',
|
|
61
|
+
timeout_ms INTEGER NOT NULL DEFAULT 120000
|
|
62
|
+
);
|
|
63
|
+
CREATE TABLE session_http_bindings (
|
|
64
|
+
id TEXT PRIMARY KEY,
|
|
65
|
+
session_id TEXT NOT NULL,
|
|
66
|
+
source_id TEXT NOT NULL,
|
|
67
|
+
direction TEXT NOT NULL CHECK(direction IN ('subscribe', 'push')),
|
|
68
|
+
active INTEGER NOT NULL DEFAULT 0,
|
|
69
|
+
activated_at INTEGER,
|
|
70
|
+
inactive_reason TEXT,
|
|
71
|
+
created_at INTEGER NOT NULL,
|
|
72
|
+
UNIQUE(session_id, source_id, direction)
|
|
73
|
+
);
|
|
74
|
+
CREATE INDEX http_bindings_session ON session_http_bindings(session_id);
|
|
75
|
+
CREATE INDEX http_bindings_active ON session_http_bindings(active, activated_at);
|
|
76
|
+
`);
|
|
77
|
+
migrations.push(`ALTER TABLE http_sources ADD COLUMN extra_body_json TEXT NOT NULL DEFAULT '{}';`);
|
|
78
|
+
migrations.push(`ALTER TABLE http_sources ADD COLUMN subscribe_method TEXT NOT NULL DEFAULT 'GET' CHECK(subscribe_method IN ('GET', 'POST'));`);
|
|
79
|
+
migrations.push(`
|
|
80
|
+
ALTER TABLE http_sources ADD COLUMN kind TEXT NOT NULL DEFAULT 'http' CHECK(kind IN ('http', 'brain-api'));
|
|
81
|
+
ALTER TABLE http_sources ADD COLUMN brain_id TEXT;
|
|
82
|
+
`);
|
|
83
|
+
migrations.push(`ALTER TABLE session_view ADD COLUMN ask_questions_enabled INTEGER NOT NULL DEFAULT 1 CHECK(ask_questions_enabled IN (0, 1));`);
|
|
84
|
+
migrations.push(`ALTER TABLE http_sources ADD COLUMN brain_voice TEXT;`);
|
|
85
|
+
migrations.push(`ALTER TABLE session_http_bindings ADD COLUMN brain_id TEXT;
|
|
86
|
+
ALTER TABLE session_http_bindings ADD COLUMN brain_voice TEXT;`);
|
|
87
|
+
migrations.push(`ALTER TABLE session_view ADD COLUMN write_enabled INTEGER NOT NULL DEFAULT 1 CHECK(write_enabled IN (0, 1));
|
|
88
|
+
UPDATE session_view SET write_enabled = 0 WHERE btw_parent IS NOT NULL;`);
|
|
89
|
+
migrations.push(`ALTER TABLE session_view ADD COLUMN btw_goal_isolated INTEGER NOT NULL DEFAULT 0 CHECK(btw_goal_isolated IN (0, 1));`);
|
|
90
|
+
migrations.push(`ALTER TABLE session_view ADD COLUMN btw_goal_isolation_uuid TEXT;`);
|
|
91
|
+
|
|
92
|
+
// A stable verifier binds persisted login tokens to the effective password.
|
|
93
|
+
// auth.ts performs the first initialization and legacy-token revocation atomically.
|
|
94
|
+
migrations.push(`CREATE TABLE auth_credential (
|
|
95
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
96
|
+
salt TEXT NOT NULL,
|
|
97
|
+
verifier TEXT NOT NULL
|
|
98
|
+
);`);
|
|
99
|
+
|
|
100
|
+
// Aliases belong to the naming session, like a private address book.
|
|
101
|
+
migrations.push(`CREATE TABLE session_aliases (
|
|
102
|
+
owner_session_id TEXT NOT NULL,
|
|
103
|
+
target_session_id TEXT NOT NULL,
|
|
104
|
+
alias TEXT NOT NULL,
|
|
105
|
+
PRIMARY KEY (owner_session_id, target_session_id),
|
|
106
|
+
UNIQUE (owner_session_id, alias)
|
|
107
|
+
);
|
|
108
|
+
CREATE INDEX session_aliases_target ON session_aliases(target_session_id);`);
|
|
109
|
+
|
|
110
|
+
const { user_version: current } = db.prepare("PRAGMA user_version").get() as { user_version: number };
|
|
111
|
+
for (let v = current; v < migrations.length; v++) {
|
|
112
|
+
db.exec("BEGIN");
|
|
113
|
+
try {
|
|
114
|
+
db.exec(migrations[v]!);
|
|
115
|
+
db.exec(`PRAGMA user_version = ${v + 1}`);
|
|
116
|
+
db.exec("COMMIT");
|
|
117
|
+
} catch (err) {
|
|
118
|
+
db.exec("ROLLBACK");
|
|
119
|
+
throw err;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { test, after, beforeEach } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import fs from "node:fs/promises";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import os from "node:os";
|
|
6
|
+
import type { DirectoryListing } from "../shared/directories.ts";
|
|
7
|
+
import { channelFixture } from "./http-channel-test-fixture.ts";
|
|
8
|
+
|
|
9
|
+
// Set isolated DB/config paths before importing routes (fsutil imports sessions).
|
|
10
|
+
const fixture = await channelFixture();
|
|
11
|
+
const { registerDirectoryRoutes } = await import("./directories.ts");
|
|
12
|
+
registerDirectoryRoutes(fixture.app);
|
|
13
|
+
after(() => fixture.close());
|
|
14
|
+
|
|
15
|
+
let root: string;
|
|
16
|
+
beforeEach(async () => {
|
|
17
|
+
root = await fs.mkdtemp(path.join(fixture.tmp, "directories-"));
|
|
18
|
+
await Promise.all([
|
|
19
|
+
fs.mkdir(path.join(root, "project")),
|
|
20
|
+
fs.mkdir(path.join(root, "project-other")),
|
|
21
|
+
fs.mkdir(path.join(root, "unrelated")),
|
|
22
|
+
fs.mkdir(path.join(root, ".private")),
|
|
23
|
+
fs.writeFile(path.join(root, "project.txt"), "file"),
|
|
24
|
+
]);
|
|
25
|
+
await fs.mkdir(path.join(root, "project", "child"));
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
function request(input: string, options: { complete?: boolean; hidden?: boolean } = {}) {
|
|
29
|
+
const query = new URLSearchParams({ path: input });
|
|
30
|
+
if (options.complete !== false) query.set("complete", "1");
|
|
31
|
+
if (options.hidden) query.set("hidden", "1");
|
|
32
|
+
return fixture.app.request(`/api/directories?${query}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function listing(input: string, options?: { complete?: boolean; hidden?: boolean }): Promise<DirectoryListing> {
|
|
36
|
+
const response = await request(input, options);
|
|
37
|
+
assert.equal(response.status, 200, await response.clone().text());
|
|
38
|
+
return response.json();
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const names = (result: DirectoryListing) => result.entries.map((entry) => entry.name).sort();
|
|
42
|
+
|
|
43
|
+
test("an unfinished absolute path returns matching directories and files from its existing parent", async () => {
|
|
44
|
+
const input = path.join(root, "proj");
|
|
45
|
+
const result = await listing(input);
|
|
46
|
+
assert.equal(result.path, root);
|
|
47
|
+
assert.equal(result.requestedPath, input);
|
|
48
|
+
assert.equal(result.requestedType, "missing");
|
|
49
|
+
assert.equal(result.prefix, "proj");
|
|
50
|
+
assert.deepEqual(result.entries, [
|
|
51
|
+
{ name: "project", path: path.join(root, "project"), type: "dir" },
|
|
52
|
+
{ name: "project-other", path: path.join(root, "project-other"), type: "dir" },
|
|
53
|
+
{ name: "project.txt", path: path.join(root, "project.txt"), type: "file" },
|
|
54
|
+
]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("a complete directory name remains a prefix until a trailing slash is entered", async () => {
|
|
58
|
+
const input = path.join(root, "project");
|
|
59
|
+
const result = await listing(input);
|
|
60
|
+
assert.equal(result.path, root);
|
|
61
|
+
assert.equal(result.requestedType, "dir");
|
|
62
|
+
assert.equal(result.prefix, "project");
|
|
63
|
+
assert.deepEqual(names(result), ["project", "project-other", "project.txt"]);
|
|
64
|
+
|
|
65
|
+
const inside = await listing(`${input}/`);
|
|
66
|
+
assert.equal(inside.path, input);
|
|
67
|
+
assert.equal(inside.requestedPath, input);
|
|
68
|
+
assert.equal(inside.requestedType, "dir");
|
|
69
|
+
assert.equal(inside.prefix, "");
|
|
70
|
+
assert.deepEqual(inside.entries, [{ name: "child", path: path.join(input, "child"), type: "dir" }]);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("multiple nonexistent components use the nearest existing directory and first unfinished component", async () => {
|
|
74
|
+
for (const suffix of ["", "/"]) {
|
|
75
|
+
const input = path.join(root, "proj", "missing", "leaf");
|
|
76
|
+
const result = await listing(input + suffix);
|
|
77
|
+
assert.equal(result.path, root);
|
|
78
|
+
assert.equal(result.requestedPath, input);
|
|
79
|
+
assert.equal(result.requestedType, "missing");
|
|
80
|
+
assert.equal(result.prefix, "proj");
|
|
81
|
+
assert.deepEqual(names(result), ["project", "project-other", "project.txt"]);
|
|
82
|
+
}
|
|
83
|
+
const nested = await listing(path.join(root, "project", "chi", "missing"));
|
|
84
|
+
assert.equal(nested.path, path.join(root, "project"));
|
|
85
|
+
assert.equal(nested.prefix, "chi");
|
|
86
|
+
assert.deepEqual(names(nested), ["child"]);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("an unmatched prefix returns an empty list without losing the typed creation target", async () => {
|
|
90
|
+
const input = path.join(root, "new", "nested");
|
|
91
|
+
const result = await listing(input);
|
|
92
|
+
assert.equal(result.path, root);
|
|
93
|
+
assert.equal(result.requestedPath, input);
|
|
94
|
+
assert.equal(result.requestedType, "missing");
|
|
95
|
+
assert.equal(result.prefix, "new");
|
|
96
|
+
assert.deepEqual(result.entries, []);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test("files are identified as files and never browsed as directories, including with trailing slash", async () => {
|
|
100
|
+
const input = path.join(root, "project.txt");
|
|
101
|
+
for (const suffix of ["", "/"]) {
|
|
102
|
+
const result = await listing(input + suffix);
|
|
103
|
+
assert.equal(result.path, root);
|
|
104
|
+
assert.equal(result.requestedType, "file");
|
|
105
|
+
assert.deepEqual(result.entries, [{ name: "project.txt", path: input, type: "file" }]);
|
|
106
|
+
}
|
|
107
|
+
const blocked = await listing(path.join(input, "missing", "child"));
|
|
108
|
+
assert.equal(blocked.path, root);
|
|
109
|
+
assert.equal(blocked.requestedType, "missing");
|
|
110
|
+
assert.equal(blocked.prefix, "project.txt");
|
|
111
|
+
assert.deepEqual(blocked.entries, [{ name: "project.txt", path: input, type: "file" }]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("completion is case insensitive and explicitly typed dot prefixes reveal hidden entries", async () => {
|
|
115
|
+
assert.deepEqual(names(await listing(path.join(root, "PROJ"))), ["project", "project-other", "project.txt"]);
|
|
116
|
+
assert.deepEqual(names(await listing(path.join(root, ".pri"))), [".private"]);
|
|
117
|
+
assert.ok(!names(await listing(`${root}/`)).includes(".private"));
|
|
118
|
+
assert.ok(names(await listing(`${root}/`, { hidden: true })).includes(".private"));
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("completion classifies symlink targets and omits broken symlinks", async () => {
|
|
122
|
+
await fs.symlink(path.join(root, "project"), path.join(root, "link-dir"));
|
|
123
|
+
await fs.symlink(path.join(root, "project.txt"), path.join(root, "link-file"));
|
|
124
|
+
await fs.symlink(path.join(root, "absent"), path.join(root, "link-broken"));
|
|
125
|
+
const result = await listing(path.join(root, "link-"));
|
|
126
|
+
assert.deepEqual(result.entries, [
|
|
127
|
+
{ name: "link-dir", path: path.join(root, "link-dir"), type: "dir" },
|
|
128
|
+
{ name: "link-file", path: path.join(root, "link-file"), type: "file" },
|
|
129
|
+
]);
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("the default endpoint retains its directory-only response and missing/file error behavior", async () => {
|
|
133
|
+
await fs.symlink(path.join(root, "project"), path.join(root, "link-dir"));
|
|
134
|
+
await fs.symlink(path.join(root, "project.txt"), path.join(root, "link-file"));
|
|
135
|
+
const result = await listing(root, { complete: false });
|
|
136
|
+
assert.deepEqual(Object.keys(result).sort(), ["entries", "home", "parent", "path", "truncated"]);
|
|
137
|
+
assert.equal(result.parent, path.dirname(root));
|
|
138
|
+
assert.equal(result.home, os.homedir());
|
|
139
|
+
assert.equal(result.truncated, false);
|
|
140
|
+
assert.deepEqual(result.entries, ["link-dir", "project", "project-other", "unrelated"].map((name) => ({ name, path: path.join(root, name) })));
|
|
141
|
+
assert.ok(names(await listing(root, { complete: false, hidden: true })).includes(".private"));
|
|
142
|
+
assert.equal((await request(path.join(root, "proj"), { complete: false })).status, 404);
|
|
143
|
+
assert.equal((await request(path.join(root, "project.txt"), { complete: false })).status, 400);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("recursive directory creation remains available for the completion target", async () => {
|
|
147
|
+
const input = path.join(root, "new", "nested");
|
|
148
|
+
const completion = await listing(input);
|
|
149
|
+
const response = await fixture.app.request("/api/directories", {
|
|
150
|
+
method: "POST",
|
|
151
|
+
headers: { "content-type": "application/json" },
|
|
152
|
+
body: JSON.stringify({ path: completion.requestedPath }),
|
|
153
|
+
});
|
|
154
|
+
assert.equal(response.status, 201);
|
|
155
|
+
const result = await response.json() as DirectoryListing;
|
|
156
|
+
assert.equal(result.path, input);
|
|
157
|
+
assert.deepEqual(result.entries, []);
|
|
158
|
+
assert.equal(result.requestedPath, undefined);
|
|
159
|
+
assert.ok((await fs.stat(input)).isDirectory());
|
|
160
|
+
assert.equal((await listing(`${input}/`)).requestedType, "dir");
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
test("creating over a file or through a file fails without changing that file", async () => {
|
|
164
|
+
for (const [input, status] of [[path.join(root, "project.txt"), 409], [path.join(root, "project.txt", "child"), 400]] as const) {
|
|
165
|
+
const response = await fixture.app.request("/api/directories", {
|
|
166
|
+
method: "POST",
|
|
167
|
+
headers: { "content-type": "application/json" },
|
|
168
|
+
body: JSON.stringify({ path: input }),
|
|
169
|
+
});
|
|
170
|
+
assert.equal(response.status, status);
|
|
171
|
+
}
|
|
172
|
+
assert.equal(await fs.readFile(path.join(root, "project.txt"), "utf8"), "file");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("both modes reject relative and NUL-containing paths", async () => {
|
|
176
|
+
for (const complete of [true, false]) {
|
|
177
|
+
assert.equal((await request("relative/path", { complete })).status, 400);
|
|
178
|
+
assert.equal((await request(`${root}/bad\0path`, { complete })).status, 400);
|
|
179
|
+
}
|
|
180
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import type { Hono } from "hono";
|
|
5
|
+
import type { DirectoryListing } from "../shared/directories.ts";
|
|
6
|
+
import { FsHttpError, route } from "./fsutil.ts";
|
|
7
|
+
|
|
8
|
+
/** Registered after requireAuth: selecting a new project must also browse unregistered directories. */
|
|
9
|
+
export function registerDirectoryRoutes(app: Hono) {
|
|
10
|
+
app.get("/api/directories", route(async (c) => c.json(c.req.query("complete") === "1"
|
|
11
|
+
? await completeDirectory(c.req.query("path"), c.req.query("hidden") === "1")
|
|
12
|
+
: await listDirectories(c.req.query("path"), c.req.query("hidden") === "1"))));
|
|
13
|
+
app.post("/api/directories", route(async (c) => {
|
|
14
|
+
const body = await c.req.json().catch(() => null);
|
|
15
|
+
if (typeof body?.path !== "string" || !body.path.trim()) throw new FsHttpError(400, "请填写要创建的目录路径");
|
|
16
|
+
const directory = resolveDirectory(body.path);
|
|
17
|
+
try {
|
|
18
|
+
await fs.mkdir(directory, { recursive: true });
|
|
19
|
+
} catch (err) {
|
|
20
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
21
|
+
if (code === "EACCES" || code === "EPERM" || code === "EROFS") throw new FsHttpError(403, "没有权限创建这个目录");
|
|
22
|
+
if (code === "EEXIST") throw new FsHttpError(409, "这个路径已存在且不是目录");
|
|
23
|
+
if (code === "ENOTDIR") throw new FsHttpError(400, "路径中包含文件,无法创建目录");
|
|
24
|
+
if (code === "ENAMETOOLONG") throw new FsHttpError(400, "目录路径过长");
|
|
25
|
+
throw err;
|
|
26
|
+
}
|
|
27
|
+
return c.json(await listDirectories(directory, c.req.query("hidden") === "1"), 201);
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function resolveDirectory(input: string | undefined) {
|
|
32
|
+
const home = os.homedir();
|
|
33
|
+
const raw = input || home;
|
|
34
|
+
if (raw.includes("\0")) throw new FsHttpError(400, "目录路径无效");
|
|
35
|
+
const expanded = raw === "~" ? home : raw.startsWith("~/") ? path.join(home, raw.slice(2)) : raw;
|
|
36
|
+
if (!path.isAbsolute(expanded)) throw new FsHttpError(400, "请填写绝对路径");
|
|
37
|
+
return path.resolve(expanded);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function completeDirectory(input: string | undefined, hidden: boolean): Promise<DirectoryListing> {
|
|
41
|
+
const requestedPath = resolveDirectory(input);
|
|
42
|
+
const statPath = (directory: string) => fs.stat(directory).catch((error: NodeJS.ErrnoException) => {
|
|
43
|
+
if (error.code === "ENOENT" || error.code === "ENOTDIR") return undefined;
|
|
44
|
+
if (error.code === "EACCES" || error.code === "EPERM") throw new FsHttpError(403, "没有权限读取这个目录");
|
|
45
|
+
throw error;
|
|
46
|
+
});
|
|
47
|
+
const target = await statPath(requestedPath);
|
|
48
|
+
const requestedType = !target ? "missing" : target.isDirectory() ? "dir" : "file";
|
|
49
|
+
const exact = !input || input === "~" || /[\\/]$/.test(input) || path.dirname(requestedPath) === requestedPath;
|
|
50
|
+
let parent = exact && target?.isDirectory() ? requestedPath : path.dirname(requestedPath);
|
|
51
|
+
// Only climb missing/non-directory components. Permission errors must remain
|
|
52
|
+
// errors, rather than silently suggesting a different location.
|
|
53
|
+
while (true) {
|
|
54
|
+
const stat = await statPath(parent);
|
|
55
|
+
if (stat?.isDirectory() || path.dirname(parent) === parent) break;
|
|
56
|
+
parent = path.dirname(parent);
|
|
57
|
+
}
|
|
58
|
+
const prefix = parent === requestedPath ? "" : path.relative(parent, requestedPath).split(path.sep)[0]!;
|
|
59
|
+
const listing = await listDirectories(parent, hidden || prefix.startsWith("."), { prefix });
|
|
60
|
+
return { ...listing, requestedPath, requestedType, prefix };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function listDirectories(input: string | undefined, hidden: boolean, completion?: { prefix: string }): Promise<DirectoryListing> {
|
|
64
|
+
const cwd = resolveDirectory(input);
|
|
65
|
+
try {
|
|
66
|
+
const dirents = await fs.readdir(cwd, { withFileTypes: true });
|
|
67
|
+
const candidates = dirents
|
|
68
|
+
.filter((entry) => (hidden || !entry.name.startsWith(".")) && (completion || entry.isDirectory() || entry.isSymbolicLink())
|
|
69
|
+
&& (!completion || entry.name.toLocaleLowerCase().startsWith(completion.prefix.toLocaleLowerCase())))
|
|
70
|
+
.sort((a, b) => a.name.localeCompare(b.name, undefined, { numeric: true, sensitivity: "base" }));
|
|
71
|
+
const entries = await Promise.all(candidates.slice(0, 5000).map(async (entry) => {
|
|
72
|
+
const child = path.join(cwd, entry.name);
|
|
73
|
+
const stat = entry.isSymbolicLink() ? await fs.stat(child).catch(() => undefined) : undefined;
|
|
74
|
+
const directory = entry.isDirectory() || !!stat?.isDirectory();
|
|
75
|
+
if (!directory && (!completion || !entry.isFile() && !stat?.isFile())) return undefined;
|
|
76
|
+
return { name: entry.name, path: child, ...(completion ? { type: directory ? "dir" as const : "file" as const } : {}) };
|
|
77
|
+
}));
|
|
78
|
+
const parent = path.dirname(cwd);
|
|
79
|
+
return { path: cwd, parent: parent === cwd ? null : parent, home: os.homedir(), entries: entries.filter((entry) => entry !== undefined), truncated: candidates.length > 5000 };
|
|
80
|
+
} catch (err) {
|
|
81
|
+
const code = (err as NodeJS.ErrnoException).code;
|
|
82
|
+
if (code === "EACCES" || code === "EPERM") throw new FsHttpError(403, "没有权限读取这个目录");
|
|
83
|
+
if (code === "ENOENT") throw new FsHttpError(404, "目录不存在");
|
|
84
|
+
if (code === "ENOTDIR") throw new FsHttpError(400, "这个路径不是目录");
|
|
85
|
+
throw err;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { config } from "./config.ts";
|
|
4
|
+
|
|
5
|
+
type Manifest = { version: string; files: { path: string; size: number }[] };
|
|
6
|
+
let validatedVersion: string | undefined;
|
|
7
|
+
const listeners = new Set<(version: string) => void>();
|
|
8
|
+
|
|
9
|
+
export function onDistVersion(fn: (version: string) => void) {
|
|
10
|
+
listeners.add(fn);
|
|
11
|
+
return () => listeners.delete(fn);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** The index and all emitted assets must match the deployment manifest before it is usable. */
|
|
15
|
+
export async function deployedDistVersion(): Promise<string | undefined> {
|
|
16
|
+
if (!config.production) return;
|
|
17
|
+
try {
|
|
18
|
+
const manifest = JSON.parse(await fs.readFile(path.join(config.webDist, "version.json"), "utf8")) as Manifest;
|
|
19
|
+
if (typeof manifest.version !== "string" || !manifest.version || !Array.isArray(manifest.files) || !manifest.files.length) return;
|
|
20
|
+
const html = await fs.readFile(path.join(config.webDist, "index.html"), "utf8");
|
|
21
|
+
const htmlVersion = /<meta\b[^>]*\bname=["']cide-build["'][^>]*\bcontent=["']([^"']+)["']/.exec(html)?.[1];
|
|
22
|
+
if (htmlVersion !== manifest.version) return;
|
|
23
|
+
if (manifest.version !== validatedVersion) {
|
|
24
|
+
const ready = await Promise.all(manifest.files.map(async (file) => {
|
|
25
|
+
if (typeof file.path !== "string" || path.isAbsolute(file.path) || file.path.split(/[\\/]/).includes("..") || !Number.isFinite(file.size)) return false;
|
|
26
|
+
const stat = await fs.stat(path.join(config.webDist, file.path)).catch(() => undefined);
|
|
27
|
+
return stat?.isFile() && stat.size === file.size;
|
|
28
|
+
}));
|
|
29
|
+
if (!ready.every(Boolean)) return;
|
|
30
|
+
validatedVersion = manifest.version;
|
|
31
|
+
}
|
|
32
|
+
return manifest.version;
|
|
33
|
+
} catch {
|
|
34
|
+
// Missing or partly-written files during a build/deploy are expected.
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Polling survives rsync replacing files or the entire dist directory. */
|
|
40
|
+
export function watchDistVersion(): () => void {
|
|
41
|
+
if (!config.production) return () => {};
|
|
42
|
+
let checking = false;
|
|
43
|
+
let stopped = false;
|
|
44
|
+
let published: string | undefined;
|
|
45
|
+
let candidate: string | undefined;
|
|
46
|
+
let candidateSince = 0;
|
|
47
|
+
const check = async () => {
|
|
48
|
+
if (checking || stopped) return;
|
|
49
|
+
checking = true;
|
|
50
|
+
try {
|
|
51
|
+
const version = await deployedDistVersion();
|
|
52
|
+
if (!version || stopped) return;
|
|
53
|
+
if (version !== candidate) {
|
|
54
|
+
candidate = version;
|
|
55
|
+
candidateSince = Date.now();
|
|
56
|
+
}
|
|
57
|
+
// Let a deployment burst settle before suggesting a refresh to an already open page.
|
|
58
|
+
if (published && version !== published && Date.now() - candidateSince < 1500) return;
|
|
59
|
+
published = version;
|
|
60
|
+
for (const listener of listeners) listener(version);
|
|
61
|
+
} finally {
|
|
62
|
+
checking = false;
|
|
63
|
+
}
|
|
64
|
+
};
|
|
65
|
+
void check();
|
|
66
|
+
const timer = setInterval(() => void check(), 2000);
|
|
67
|
+
timer.unref();
|
|
68
|
+
return () => { stopped = true; clearInterval(timer); };
|
|
69
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A durable scheduled task written by the CLI to `<cwd>/.claude/scheduled_tasks.json`.
|
|
6
|
+
* This file is CLI-owned; parse defensively and never write to it from here.
|
|
7
|
+
*/
|
|
8
|
+
export type DurableTask = {
|
|
9
|
+
id: string;
|
|
10
|
+
cron: string;
|
|
11
|
+
prompt: string;
|
|
12
|
+
recurring: boolean;
|
|
13
|
+
createdBySessionId?: string;
|
|
14
|
+
createdAt?: number;
|
|
15
|
+
lastFiredAt?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function task(value: unknown): DurableTask | undefined {
|
|
19
|
+
if (!value || typeof value !== "object") return undefined;
|
|
20
|
+
const t = value as Record<string, unknown>;
|
|
21
|
+
if (typeof t.id !== "string" || typeof t.cron !== "string" || typeof t.prompt !== "string") return undefined;
|
|
22
|
+
return {
|
|
23
|
+
id: t.id,
|
|
24
|
+
cron: t.cron,
|
|
25
|
+
prompt: t.prompt,
|
|
26
|
+
recurring: t.recurring === true,
|
|
27
|
+
...(typeof t.createdBySessionId === "string" ? { createdBySessionId: t.createdBySessionId } : {}),
|
|
28
|
+
...(typeof t.createdAt === "number" ? { createdAt: t.createdAt } : {}),
|
|
29
|
+
...(typeof t.lastFiredAt === "number" ? { lastFiredAt: t.lastFiredAt } : {}),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Read the durable task file for a session's working directory. Missing or malformed files mean no tasks. */
|
|
34
|
+
export function durableTasks(cwd: string): DurableTask[] {
|
|
35
|
+
const file = path.join(cwd, ".claude", "scheduled_tasks.json");
|
|
36
|
+
let raw: string;
|
|
37
|
+
try {
|
|
38
|
+
raw = fs.readFileSync(file, "utf8");
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(raw) as { tasks?: unknown };
|
|
44
|
+
if (!Array.isArray(parsed.tasks)) return [];
|
|
45
|
+
return parsed.tasks.flatMap((t) => {
|
|
46
|
+
const parsedTask = task(t);
|
|
47
|
+
return parsedTask ? [parsedTask] : [];
|
|
48
|
+
});
|
|
49
|
+
} catch {
|
|
50
|
+
return [];
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Durable tasks belonging to one session. Tasks without a creator predate the ownership field;
|
|
56
|
+
* show those too rather than hiding them. Other sessions' tasks are skipped: the panel is per
|
|
57
|
+
* session, and the CLI will run them from whichever process owns the scheduler lock.
|
|
58
|
+
*/
|
|
59
|
+
export function durableTasksForSession(cwd: string, sessionId: string): DurableTask[] {
|
|
60
|
+
return durableTasks(cwd).filter((t) => !t.createdBySessionId || t.createdBySessionId === sessionId);
|
|
61
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Readable } from "node:stream";
|
|
5
|
+
import type { Hono } from "hono";
|
|
6
|
+
import { bodyLimit } from "hono/body-limit";
|
|
7
|
+
import { MAX_UPLOAD_BYTES, MAX_UPLOAD_FILES, type UploadResult } from "../shared/files.ts";
|
|
8
|
+
import { FsHttpError, projectRoot, resolveSafe, route } from "./fsutil.ts";
|
|
9
|
+
|
|
10
|
+
export function registerFileTransferRoutes(app: Hono) {
|
|
11
|
+
app.get("/api/projects/:pid/file/download", route(async (c) => {
|
|
12
|
+
const root = projectRoot(c), target = await resolveSafe(root, c.req.query("path"));
|
|
13
|
+
const real = await resolveSafe(root, await fs.realpath(target.abs));
|
|
14
|
+
const handle = await fs.open(real.abs, constants.O_RDONLY | constants.O_NONBLOCK | constants.O_NOFOLLOW);
|
|
15
|
+
try {
|
|
16
|
+
const stat = await handle.stat();
|
|
17
|
+
if (!stat.isFile()) throw new FsHttpError(400, "只能下载普通文件");
|
|
18
|
+
const filename = encodeURIComponent(path.basename(target.abs)).replace(/['()*]/g, (character) => `%${character.charCodeAt(0).toString(16)}`);
|
|
19
|
+
return new Response(Readable.toWeb(handle.createReadStream()) as ReadableStream<Uint8Array>, { headers: {
|
|
20
|
+
"content-type": "application/octet-stream", "content-length": String(stat.size),
|
|
21
|
+
"content-disposition": `attachment; filename*=UTF-8''${filename}`,
|
|
22
|
+
"cache-control": "no-store", "x-content-type-options": "nosniff",
|
|
23
|
+
} });
|
|
24
|
+
} catch (error) { await handle.close(); throw error; }
|
|
25
|
+
}));
|
|
26
|
+
app.post("/api/projects/:pid/files/upload", bodyLimit({
|
|
27
|
+
maxSize: MAX_UPLOAD_BYTES + 1024 * 1024,
|
|
28
|
+
onError: (c) => c.json({ error: "单次上传最多 50 MiB" }, 413),
|
|
29
|
+
}), route(async (c) => {
|
|
30
|
+
const root = projectRoot(c), selected = await resolveSafe(root, c.req.query("path"));
|
|
31
|
+
const target = await resolveSafe(root, await fs.realpath(selected.abs));
|
|
32
|
+
if (!(await fs.stat(target.abs)).isDirectory()) throw new FsHttpError(400, "上传目标不是文件夹");
|
|
33
|
+
let form: FormData;
|
|
34
|
+
try { form = await c.req.formData(); } catch { throw new FsHttpError(400, "上传格式无效"); }
|
|
35
|
+
const files = form.getAll("files");
|
|
36
|
+
if (!files.length || files.length > MAX_UPLOAD_FILES || !files.every((file) => file instanceof File)) throw new FsHttpError(400, "请选择 1 至 100 个文件");
|
|
37
|
+
if (files.reduce((bytes, file) => bytes + file.size, 0) > MAX_UPLOAD_BYTES) throw new FsHttpError(413, "单次上传最多 50 MiB");
|
|
38
|
+
const names = new Set<string>();
|
|
39
|
+
for (const file of files) {
|
|
40
|
+
if (!file.name || /[\/\\\0]/.test(file.name) || file.name === "." || file.name === ".." || names.has(file.name)) throw new FsHttpError(400, "文件名无效或重复");
|
|
41
|
+
names.add(file.name);
|
|
42
|
+
}
|
|
43
|
+
const overwrite = new Set(form.getAll("overwrite").filter((name): name is string => typeof name === "string" && names.has(name)));
|
|
44
|
+
const result: UploadResult = { files: [], conflicts: [] };
|
|
45
|
+
const planned = await Promise.all(files.map(async (file) => {
|
|
46
|
+
const dest = await resolveSafe(root, path.join(target.rel, file.name), { mustExist: false });
|
|
47
|
+
const stat = await fs.lstat(dest.abs).catch((error: NodeJS.ErrnoException) => { if (error.code === "ENOENT") return undefined; throw error; });
|
|
48
|
+
if (stat && !stat.isFile() && !stat.isSymbolicLink()) throw new FsHttpError(409, `“${file.name}”不是可覆盖的文件`);
|
|
49
|
+
if (stat && !overwrite.has(file.name)) result.conflicts.push(file.name);
|
|
50
|
+
return { file, dest };
|
|
51
|
+
}));
|
|
52
|
+
if (result.conflicts.length) return c.json(result, 409);
|
|
53
|
+
// Stage complete files in the destination filesystem, then publish atomically.
|
|
54
|
+
const staging = await fs.mkdtemp(path.join(target.abs, ".claude-ide-upload-"));
|
|
55
|
+
try {
|
|
56
|
+
for (const [index, item] of planned.entries()) await fs.writeFile(path.join(staging, String(index)), new Uint8Array(await item.file.arrayBuffer()), { flag: "wx" });
|
|
57
|
+
for (const [index, item] of planned.entries()) {
|
|
58
|
+
const destination = await resolveSafe(root, item.dest.rel, { mustExist: false });
|
|
59
|
+
const staged = path.join(staging, String(index));
|
|
60
|
+
if (overwrite.has(item.file.name)) {
|
|
61
|
+
const existing = await fs.lstat(destination.abs).catch((error: NodeJS.ErrnoException) => { if (error.code === "ENOENT") return undefined; throw error; });
|
|
62
|
+
// Apply the target's ordinary permissions before publishing, so a
|
|
63
|
+
// private file never becomes temporarily readable with the umask default.
|
|
64
|
+
if (existing?.isFile()) await fs.chmod(staged, existing.mode & 0o777);
|
|
65
|
+
await fs.rename(staged, destination.abs);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
try { await fs.link(staged, destination.abs); }
|
|
69
|
+
catch (error) {
|
|
70
|
+
if ((error as NodeJS.ErrnoException).code === "EEXIST") { result.conflicts.push(item.file.name); continue; }
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
result.files.push(path.posix.join(selected.rel, item.file.name));
|
|
75
|
+
}
|
|
76
|
+
return c.json(result, result.conflicts.length ? 409 : 200);
|
|
77
|
+
} finally { await fs.rm(staging, { recursive: true, force: true }); }
|
|
78
|
+
}));
|
|
79
|
+
}
|