@thevinci/web 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/README.md +197 -0
- package/bin/cli-entry.js +55 -0
- package/bin/cli-output.js +145 -0
- package/bin/cli.js +4887 -0
- package/bin/cli.test.js +64 -0
- package/dist/apple-touch-icon-120x120.png +0 -0
- package/dist/apple-touch-icon-152x152.png +0 -0
- package/dist/apple-touch-icon-167x167.png +0 -0
- package/dist/apple-touch-icon-180x180.png +0 -0
- package/dist/apple-touch-icon.png +0 -0
- package/dist/apple-touch-icon.svg +528 -0
- package/dist/assets/JsonTreeView-CSm9OzXG.js +1 -0
- package/dist/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
- package/dist/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
- package/dist/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
- package/dist/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
- package/dist/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
- package/dist/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
- package/dist/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
- package/dist/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
- package/dist/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
- package/dist/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
- package/dist/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
- package/dist/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
- package/dist/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
- package/dist/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
- package/dist/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
- package/dist/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
- package/dist/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
- package/dist/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
- package/dist/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
- package/dist/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
- package/dist/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
- package/dist/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
- package/dist/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
- package/dist/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
- package/dist/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
- package/dist/assets/MarkdownRendererImpl-DensKOLc.js +6 -0
- package/dist/assets/MultiRunWindow-Bo7THayo.js +1 -0
- package/dist/assets/OnboardingScreen-BDqmzTVR.js +2 -0
- package/dist/assets/SettingsWindow-coz__Ykw.js +1 -0
- package/dist/assets/TerminalView-DrZ-i3Dr.js +1 -0
- package/dist/assets/ToolOutputDialog-Eglzslt3.js +16 -0
- package/dist/assets/es-4o9ciP61.js +15 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-CvHOgSBP.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-400-normal-DMJ8VG8y.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-CB9ihrfo.woff +0 -0
- package/dist/assets/ibm-plex-mono-latin-500-normal-DSY6xOcd.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-BgSNZQsw.woff2 +0 -0
- package/dist/assets/ibm-plex-mono-latin-600-normal-DWFSQ4vo.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CDDApCn2.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-400-normal-CYLoc0-x.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-6ng42L7E.woff2 +0 -0
- package/dist/assets/ibm-plex-sans-latin-500-normal-BgVn5rGT.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-Cu4Hd6ag.woff +0 -0
- package/dist/assets/ibm-plex-sans-latin-600-normal-CuJfVYMP.woff2 +0 -0
- package/dist/assets/index-DLTDToSP.css +1 -0
- package/dist/assets/index-DgiFEKGN.js +1 -0
- package/dist/assets/ko-B20imCHE.js +15 -0
- package/dist/assets/main-BV3KOtdA.css +1 -0
- package/dist/assets/main-CDKJj0sH.js +226 -0
- package/dist/assets/main-LC-PSNVM.js +2 -0
- package/dist/assets/miniChat-CQUiG_cr.js +2 -0
- package/dist/assets/modelPrefsAutoSave-Dm799vzR.js +6986 -0
- package/dist/assets/pl-DQJ7LSzj.js +15 -0
- package/dist/assets/pt-BR-OmjHUz9y.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-CARbeW0G.js +2 -0
- package/dist/assets/uk-BNFxOlO4.js +15 -0
- package/dist/assets/vendor--DBfsbEis.css +1 -0
- package/dist/assets/vendor-.bun-B9l0ZNi2.js +4094 -0
- package/dist/assets/wasm-CG6Dc4jp.js +1 -0
- package/dist/assets/wasmSttWorker-Dtlxac_K.js +1 -0
- package/dist/assets/wasmSttWorker-oo7Dm_jy.js +1806 -0
- package/dist/assets/worker-CbT6TVo7.js +155 -0
- package/dist/assets/zh-CN-C6T-Ac7F.js +15 -0
- package/dist/favicon-16.png +0 -0
- package/dist/favicon-32.png +0 -0
- package/dist/favicon.png +0 -0
- package/dist/favicon.svg +528 -0
- package/dist/index.html +607 -0
- package/dist/logo-dark-192x192.png +0 -0
- package/dist/logo-dark-512x512.png +0 -0
- package/dist/logo-dark-512x512.svg +528 -0
- package/dist/logo-light-192x192.png +0 -0
- package/dist/logo-light-512x512.png +0 -0
- package/dist/logo-light-512x512.svg +528 -0
- package/dist/mini-chat.html +16 -0
- package/dist/pwa-192.png +0 -0
- package/dist/pwa-512.png +0 -0
- package/dist/pwa-maskable-192.png +0 -0
- package/dist/pwa-maskable-512.png +0 -0
- package/dist/site.webmanifest +21 -0
- package/dist/sw.js +1 -0
- package/package.json +118 -0
- package/public/apple-touch-icon-120x120.png +0 -0
- package/public/apple-touch-icon-152x152.png +0 -0
- package/public/apple-touch-icon-167x167.png +0 -0
- package/public/apple-touch-icon-180x180.png +0 -0
- package/public/apple-touch-icon.png +0 -0
- package/public/apple-touch-icon.svg +528 -0
- package/public/favicon-16.png +0 -0
- package/public/favicon-32.png +0 -0
- package/public/favicon.png +0 -0
- package/public/favicon.svg +528 -0
- package/public/logo-dark-192x192.png +0 -0
- package/public/logo-dark-512x512.png +0 -0
- package/public/logo-dark-512x512.svg +528 -0
- package/public/logo-light-192x192.png +0 -0
- package/public/logo-light-512x512.png +0 -0
- package/public/logo-light-512x512.svg +528 -0
- package/public/pwa-192.png +0 -0
- package/public/pwa-512.png +0 -0
- package/public/pwa-maskable-192.png +0 -0
- package/public/pwa-maskable-512.png +0 -0
- package/public/site.webmanifest +21 -0
- package/server/TERMINAL_WS_PROTOCOL.md +48 -0
- package/server/index.d.ts +39 -0
- package/server/index.js +1311 -0
- package/server/lib/cloudflare-tunnel.js +650 -0
- package/server/lib/event-stream/DOCUMENTATION.md +61 -0
- package/server/lib/event-stream/directory-ws-bridge.js +185 -0
- package/server/lib/event-stream/global-hub.js +158 -0
- package/server/lib/event-stream/global-hub.test.js +140 -0
- package/server/lib/event-stream/global-ws-bridge.js +206 -0
- package/server/lib/event-stream/index.js +25 -0
- package/server/lib/event-stream/protocol.js +131 -0
- package/server/lib/event-stream/protocol.test.js +182 -0
- package/server/lib/event-stream/runtime.js +180 -0
- package/server/lib/event-stream/runtime.test.js +512 -0
- package/server/lib/event-stream/upstream-reader.js +226 -0
- package/server/lib/event-stream/upstream-reader.test.js +276 -0
- package/server/lib/fs/DOCUMENTATION.md +36 -0
- package/server/lib/fs/routes.js +1040 -0
- package/server/lib/fs/search.js +238 -0
- package/server/lib/git/DOCUMENTATION.md +152 -0
- package/server/lib/git/credentials.js +74 -0
- package/server/lib/git/identity-storage.js +112 -0
- package/server/lib/git/index.js +6 -0
- package/server/lib/git/routes.js +972 -0
- package/server/lib/git/service.js +3432 -0
- package/server/lib/git/service.test.js +39 -0
- package/server/lib/github/DOCUMENTATION.md +171 -0
- package/server/lib/github/auth.js +307 -0
- package/server/lib/github/device-flow.js +50 -0
- package/server/lib/github/index.js +24 -0
- package/server/lib/github/octokit.js +10 -0
- package/server/lib/github/pr-status.js +519 -0
- package/server/lib/github/repo/fork-detection.js +102 -0
- package/server/lib/github/repo/index.js +55 -0
- package/server/lib/github/routes.js +1560 -0
- package/server/lib/magic-prompts/routes.js +63 -0
- package/server/lib/magic-prompts/runtime.js +119 -0
- package/server/lib/notifications/DOCUMENTATION.md +122 -0
- package/server/lib/notifications/emitter-runtime.js +102 -0
- package/server/lib/notifications/index.js +4 -0
- package/server/lib/notifications/message.js +52 -0
- package/server/lib/notifications/message.test.js +34 -0
- package/server/lib/notifications/push-runtime.js +304 -0
- package/server/lib/notifications/routes.js +315 -0
- package/server/lib/notifications/runtime.js +566 -0
- package/server/lib/notifications/template-runtime.js +349 -0
- package/server/lib/notifications/template-runtime.test.js +26 -0
- package/server/lib/opencode/DOCUMENTATION.md +362 -0
- package/server/lib/opencode/agents.js +634 -0
- package/server/lib/opencode/auth-state-runtime.js +88 -0
- package/server/lib/opencode/auth.js +83 -0
- package/server/lib/opencode/bootstrap-runtime.js +131 -0
- package/server/lib/opencode/cli-entry-runtime.js +43 -0
- package/server/lib/opencode/cli-options.js +128 -0
- package/server/lib/opencode/commands.js +339 -0
- package/server/lib/opencode/config-entity-routes.js +370 -0
- package/server/lib/opencode/core-routes.js +500 -0
- package/server/lib/opencode/core-routes.test.js +26 -0
- package/server/lib/opencode/env-config.js +74 -0
- package/server/lib/opencode/env-keys.js +68 -0
- package/server/lib/opencode/env-runtime.js +1162 -0
- package/server/lib/opencode/env-runtime.test.js +116 -0
- package/server/lib/opencode/feature-routes-runtime.js +244 -0
- package/server/lib/opencode/hmr-state-runtime.js +85 -0
- package/server/lib/opencode/index.js +66 -0
- package/server/lib/opencode/lifecycle.js +1019 -0
- package/server/lib/opencode/lifecycle.test.js +240 -0
- package/server/lib/opencode/mcp.js +278 -0
- package/server/lib/opencode/network-runtime.js +104 -0
- package/server/lib/opencode/network-runtime.test.js +37 -0
- package/server/lib/opencode/opencode-resolution-runtime.js +71 -0
- package/server/lib/opencode/path-utils.js +100 -0
- package/server/lib/opencode/path-utils.test.js +71 -0
- package/server/lib/opencode/project-directory-runtime.js +124 -0
- package/server/lib/opencode/project-icon-routes.js +399 -0
- package/server/lib/opencode/project-icon-routes.test.js +107 -0
- package/server/lib/opencode/providers.js +96 -0
- package/server/lib/opencode/proxy.js +445 -0
- package/server/lib/opencode/pwa-manifest-routes.js +257 -0
- package/server/lib/opencode/pwa-manifest-routes.test.js +133 -0
- package/server/lib/opencode/routes.js +541 -0
- package/server/lib/opencode/server-startup-runtime.js +156 -0
- package/server/lib/opencode/server-utils-runtime.js +168 -0
- package/server/lib/opencode/server-utils-runtime.test.js +135 -0
- package/server/lib/opencode/session-runtime.js +356 -0
- package/server/lib/opencode/session-runtime.test.js +151 -0
- package/server/lib/opencode/settings-helpers.js +770 -0
- package/server/lib/opencode/settings-helpers.test.js +109 -0
- package/server/lib/opencode/settings-normalization-runtime.js +428 -0
- package/server/lib/opencode/settings-runtime.js +826 -0
- package/server/lib/opencode/settings-runtime.test.js +85 -0
- package/server/lib/opencode/shared.js +615 -0
- package/server/lib/opencode/shutdown-runtime.js +139 -0
- package/server/lib/opencode/shutdown-runtime.test.js +58 -0
- package/server/lib/opencode/skill-routes.js +701 -0
- package/server/lib/opencode/skills.js +548 -0
- package/server/lib/opencode/startup-pipeline-runtime.js +130 -0
- package/server/lib/opencode/static-routes-runtime.js +65 -0
- package/server/lib/opencode/theme-runtime.js +167 -0
- package/server/lib/opencode/tunnel-auth.js +591 -0
- package/server/lib/opencode/tunnel-wiring-runtime.js +94 -0
- package/server/lib/opencode/vinci-routes.js +76 -0
- package/server/lib/opencode/watcher.js +115 -0
- package/server/lib/opencode/watcher.test.js +239 -0
- package/server/lib/preview/proxy-runtime.js +1333 -0
- package/server/lib/preview/proxy-runtime.test.js +144 -0
- package/server/lib/projects/project-config.js +567 -0
- package/server/lib/projects/project-config.test.js +175 -0
- package/server/lib/projects/project-id.js +13 -0
- package/server/lib/quota/DOCUMENTATION.md +58 -0
- package/server/lib/quota/index.js +25 -0
- package/server/lib/quota/providers/claude.js +107 -0
- package/server/lib/quota/providers/codex.js +113 -0
- package/server/lib/quota/providers/copilot.js +165 -0
- package/server/lib/quota/providers/google/api.js +92 -0
- package/server/lib/quota/providers/google/auth.js +108 -0
- package/server/lib/quota/providers/google/index.js +124 -0
- package/server/lib/quota/providers/google/transforms.js +109 -0
- package/server/lib/quota/providers/index.js +168 -0
- package/server/lib/quota/providers/interface.js +55 -0
- package/server/lib/quota/providers/kimi.js +108 -0
- package/server/lib/quota/providers/minimax-cn-coding-plan.js +140 -0
- package/server/lib/quota/providers/minimax-coding-plan.js +139 -0
- package/server/lib/quota/providers/nanogpt.js +124 -0
- package/server/lib/quota/providers/ollama-cloud.js +112 -0
- package/server/lib/quota/providers/openai.js +91 -0
- package/server/lib/quota/providers/openrouter.js +92 -0
- package/server/lib/quota/providers/zai.js +91 -0
- package/server/lib/quota/providers/zhipuai-coding-plan.js +133 -0
- package/server/lib/quota/providers/zhipuai.js +114 -0
- package/server/lib/quota/routes.js +27 -0
- package/server/lib/quota/utils/auth.js +50 -0
- package/server/lib/quota/utils/formatters.js +85 -0
- package/server/lib/quota/utils/formatters.test.js +54 -0
- package/server/lib/quota/utils/index.js +10 -0
- package/server/lib/quota/utils/transformers.js +55 -0
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +44 -0
- package/server/lib/scheduled-tasks/routes.js +235 -0
- package/server/lib/scheduled-tasks/runtime.js +773 -0
- package/server/lib/scheduled-tasks/runtime.test.js +100 -0
- package/server/lib/security/request-security.js +115 -0
- package/server/lib/session-folders/routes.js +63 -0
- package/server/lib/session-folders/routes.test.js +102 -0
- package/server/lib/skills-catalog/DOCUMENTATION.md +178 -0
- package/server/lib/skills-catalog/cache.js +29 -0
- package/server/lib/skills-catalog/clawdhub/api.js +158 -0
- package/server/lib/skills-catalog/clawdhub/index.js +30 -0
- package/server/lib/skills-catalog/clawdhub/install.js +238 -0
- package/server/lib/skills-catalog/clawdhub/scan.js +113 -0
- package/server/lib/skills-catalog/curated-sources.js +21 -0
- package/server/lib/skills-catalog/git.js +77 -0
- package/server/lib/skills-catalog/index.js +42 -0
- package/server/lib/skills-catalog/install.js +294 -0
- package/server/lib/skills-catalog/scan.js +221 -0
- package/server/lib/skills-catalog/source.js +87 -0
- package/server/lib/terminal/DOCUMENTATION.md +76 -0
- package/server/lib/terminal/index.js +31 -0
- package/server/lib/terminal/output-replay-buffer.js +78 -0
- package/server/lib/terminal/output-replay-buffer.test.js +75 -0
- package/server/lib/terminal/runtime.js +850 -0
- package/server/lib/terminal/runtime.test.js +96 -0
- package/server/lib/terminal/terminal-ws-protocol.js +68 -0
- package/server/lib/terminal/terminal-ws-protocol.test.js +145 -0
- package/server/lib/text/DOCUMENTATION.md +35 -0
- package/server/lib/text/summarization.js +138 -0
- package/server/lib/text/summarization.test.js +34 -0
- package/server/lib/tts/DOCUMENTATION.md +146 -0
- package/server/lib/tts/base-url.js +62 -0
- package/server/lib/tts/capability-runtime.js +31 -0
- package/server/lib/tts/index.js +19 -0
- package/server/lib/tts/routes.js +261 -0
- package/server/lib/tts/routes.test.js +53 -0
- package/server/lib/tts/service.js +178 -0
- package/server/lib/tts/stt.js +75 -0
- package/server/lib/tunnels/DOCUMENTATION.md +18 -0
- package/server/lib/tunnels/index.js +166 -0
- package/server/lib/tunnels/managed-config.js +201 -0
- package/server/lib/tunnels/providers/cloudflare.js +260 -0
- package/server/lib/tunnels/registry.js +51 -0
- package/server/lib/tunnels/routes.js +605 -0
- package/server/lib/tunnels/types.js +219 -0
- package/server/lib/ui-auth/DOCUMENTATION.md +38 -0
- package/server/lib/ui-auth/ui-auth.js +673 -0
- package/server/lib/ui-auth/ui-passkeys.js +545 -0
- package/server/opencode-proxy.test.js +151 -0
- package/server/proxy-headers.js +61 -0
- package/server/proxy-headers.test.js +58 -0
- package/server/sse-routes.test.js +152 -0
|
@@ -0,0 +1,1040 @@
|
|
|
1
|
+
const EXEC_JOB_TTL_MS = 30 * 60 * 1000;
|
|
2
|
+
|
|
3
|
+
const createCommandTimeoutMs = () => {
|
|
4
|
+
const raw = Number(process.env.VINCI_FS_EXEC_TIMEOUT_MS);
|
|
5
|
+
if (Number.isFinite(raw) && raw > 0) return raw;
|
|
6
|
+
return 5 * 60 * 1000;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
const isPathWithinRoot = (resolvedPath, rootPath, path, os) => {
|
|
10
|
+
const resolvedRoot = path.resolve(rootPath || os.homedir());
|
|
11
|
+
const relative = path.relative(resolvedRoot, resolvedPath);
|
|
12
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
return true;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const resolveWorkspacePath = ({ targetPath, baseDirectory, path, os, normalizeDirectoryPath, vinciUserConfigRoot }) => {
|
|
19
|
+
const normalized = normalizeDirectoryPath(targetPath);
|
|
20
|
+
if (!normalized || typeof normalized !== 'string') {
|
|
21
|
+
return { ok: false, error: 'Path is required' };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const resolved = path.resolve(normalized);
|
|
25
|
+
const resolvedBase = path.resolve(baseDirectory || os.homedir());
|
|
26
|
+
|
|
27
|
+
if (isPathWithinRoot(resolved, resolvedBase, path, os)) {
|
|
28
|
+
return { ok: true, base: resolvedBase, resolved };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (isPathWithinRoot(resolved, vinciUserConfigRoot, path, os)) {
|
|
32
|
+
return { ok: true, base: path.resolve(vinciUserConfigRoot), resolved };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { ok: false, error: 'Path is outside of active workspace' };
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const resolveWorkspacePathFromWorktrees = async ({ targetPath, baseDirectory, path, os, normalizeDirectoryPath }) => {
|
|
39
|
+
const normalized = normalizeDirectoryPath(targetPath);
|
|
40
|
+
if (!normalized || typeof normalized !== 'string') {
|
|
41
|
+
return { ok: false, error: 'Path is required' };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const resolved = path.resolve(normalized);
|
|
45
|
+
const resolvedBase = path.resolve(baseDirectory || os.homedir());
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const { getWorktrees } = await import('../git/index.js');
|
|
49
|
+
const worktrees = await getWorktrees(resolvedBase);
|
|
50
|
+
|
|
51
|
+
for (const worktree of worktrees) {
|
|
52
|
+
const candidatePath = typeof worktree?.path === 'string'
|
|
53
|
+
? worktree.path
|
|
54
|
+
: (typeof worktree?.worktree === 'string' ? worktree.worktree : '');
|
|
55
|
+
const candidate = normalizeDirectoryPath(candidatePath);
|
|
56
|
+
if (!candidate) {
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const candidateResolved = path.resolve(candidate);
|
|
60
|
+
if (isPathWithinRoot(resolved, candidateResolved, path, os)) {
|
|
61
|
+
return { ok: true, base: candidateResolved, resolved };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.warn('Failed to resolve worktree roots:', error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return { ok: false, error: 'Path is outside of active workspace' };
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const resolveWorkspacePathFromContext = async ({ req, targetPath, resolveProjectDirectory, path, os, normalizeDirectoryPath, vinciUserConfigRoot }) => {
|
|
72
|
+
const resolvedProject = await resolveProjectDirectory(req);
|
|
73
|
+
if (!resolvedProject.directory) {
|
|
74
|
+
return { ok: false, error: resolvedProject.error || 'Active workspace is required' };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const resolved = resolveWorkspacePath({
|
|
78
|
+
targetPath,
|
|
79
|
+
baseDirectory: resolvedProject.directory,
|
|
80
|
+
path,
|
|
81
|
+
os,
|
|
82
|
+
normalizeDirectoryPath,
|
|
83
|
+
vinciUserConfigRoot,
|
|
84
|
+
});
|
|
85
|
+
if (resolved.ok || resolved.error !== 'Path is outside of active workspace') {
|
|
86
|
+
return resolved;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return resolveWorkspacePathFromWorktrees({
|
|
90
|
+
targetPath,
|
|
91
|
+
baseDirectory: resolvedProject.directory,
|
|
92
|
+
path,
|
|
93
|
+
os,
|
|
94
|
+
normalizeDirectoryPath,
|
|
95
|
+
});
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const deriveCloneDirectoryName = (remoteUrl) => {
|
|
99
|
+
const remote = typeof remoteUrl === 'string' ? remoteUrl.trim() : '';
|
|
100
|
+
if (!remote) return '';
|
|
101
|
+
const withoutQuery = remote.split(/[?#]/, 1)[0] || remote;
|
|
102
|
+
const match = withoutQuery.match(/([^/:]+?)(?:\.git)?\/?$/);
|
|
103
|
+
return match?.[1]?.trim() || '';
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const resolveCloneGitIdentity = async (gitIdentityId) => {
|
|
107
|
+
const id = typeof gitIdentityId === 'string' ? gitIdentityId.trim() : '';
|
|
108
|
+
if (!id) return null;
|
|
109
|
+
const { getProfile, getGlobalIdentity } = await import('../git/index.js');
|
|
110
|
+
if (id === 'global') {
|
|
111
|
+
const globalIdentity = await getGlobalIdentity();
|
|
112
|
+
if (!globalIdentity?.userName || !globalIdentity?.userEmail) return null;
|
|
113
|
+
return {
|
|
114
|
+
id: 'global',
|
|
115
|
+
name: 'Global Identity',
|
|
116
|
+
userName: globalIdentity.userName,
|
|
117
|
+
userEmail: globalIdentity.userEmail,
|
|
118
|
+
sshKey: globalIdentity.sshCommand ? globalIdentity.sshCommand.replace('ssh -i ', '') : null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return getProfile(id) || null;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
const escapeCloneSshKeyPath = (sshKeyPath) => {
|
|
125
|
+
const raw = String(sshKeyPath || '').trim();
|
|
126
|
+
if (!raw) return '';
|
|
127
|
+
const normalized = process.platform === 'win32' ? raw.replace(/\\/g, '/') : raw;
|
|
128
|
+
const dangerousChars = /[`$!"';&|<>(){}[\]*?#~]/;
|
|
129
|
+
if (dangerousChars.test(normalized)) {
|
|
130
|
+
throw new Error(`SSH key path contains invalid characters: ${raw}`);
|
|
131
|
+
}
|
|
132
|
+
if (process.platform === 'win32') {
|
|
133
|
+
const driveMatch = normalized.match(/^([A-Za-z]):\//);
|
|
134
|
+
const unixPath = driveMatch ? `/${driveMatch[1].toLowerCase()}${normalized.slice(2)}` : normalized;
|
|
135
|
+
return `'${unixPath}'`;
|
|
136
|
+
}
|
|
137
|
+
return `'${normalized.replace(/'/g, "'\\''")}'`;
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const resolveReadPathFromContext = async ({ req, targetPath, resolveProjectDirectory, path, os, normalizeDirectoryPath, vinciUserConfigRoot }) => {
|
|
141
|
+
if (req.query?.allowOutsideWorkspace === 'true') {
|
|
142
|
+
const normalized = normalizeDirectoryPath(targetPath);
|
|
143
|
+
if (!normalized || typeof normalized !== 'string') {
|
|
144
|
+
return { ok: false, error: 'Path is required' };
|
|
145
|
+
}
|
|
146
|
+
const resolved = path.resolve(normalized);
|
|
147
|
+
return { ok: true, base: path.dirname(resolved), resolved };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return resolveWorkspacePathFromContext({
|
|
151
|
+
req,
|
|
152
|
+
targetPath,
|
|
153
|
+
resolveProjectDirectory,
|
|
154
|
+
path,
|
|
155
|
+
os,
|
|
156
|
+
normalizeDirectoryPath,
|
|
157
|
+
vinciUserConfigRoot,
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
const runCommandInDirectory = ({ shell, shellFlag, command, resolvedCwd, spawn, buildAugmentedPath, commandTimeoutMs }) => {
|
|
162
|
+
return new Promise((resolve) => {
|
|
163
|
+
let stdout = '';
|
|
164
|
+
let stderr = '';
|
|
165
|
+
let timedOut = false;
|
|
166
|
+
|
|
167
|
+
const envPath = buildAugmentedPath();
|
|
168
|
+
const execEnv = { ...process.env, PATH: envPath };
|
|
169
|
+
|
|
170
|
+
const child = spawn(shell, [shellFlag, command], {
|
|
171
|
+
cwd: resolvedCwd,
|
|
172
|
+
env: execEnv,
|
|
173
|
+
windowsHide: true,
|
|
174
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
const timeout = setTimeout(() => {
|
|
178
|
+
timedOut = true;
|
|
179
|
+
try {
|
|
180
|
+
child.kill('SIGKILL');
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
}, commandTimeoutMs);
|
|
184
|
+
|
|
185
|
+
child.stdout?.on('data', (chunk) => {
|
|
186
|
+
stdout += chunk.toString();
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
child.stderr?.on('data', (chunk) => {
|
|
190
|
+
stderr += chunk.toString();
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
child.on('error', (error) => {
|
|
194
|
+
clearTimeout(timeout);
|
|
195
|
+
resolve({
|
|
196
|
+
command,
|
|
197
|
+
success: false,
|
|
198
|
+
exitCode: undefined,
|
|
199
|
+
stdout: stdout.trim(),
|
|
200
|
+
stderr: stderr.trim(),
|
|
201
|
+
error: (error && error.message) || 'Command execution failed',
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
child.on('close', (code, signal) => {
|
|
206
|
+
clearTimeout(timeout);
|
|
207
|
+
const exitCode = typeof code === 'number' ? code : undefined;
|
|
208
|
+
const base = {
|
|
209
|
+
command,
|
|
210
|
+
success: exitCode === 0 && !timedOut,
|
|
211
|
+
exitCode,
|
|
212
|
+
stdout: stdout.trim(),
|
|
213
|
+
stderr: stderr.trim(),
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
if (timedOut) {
|
|
217
|
+
resolve({
|
|
218
|
+
...base,
|
|
219
|
+
success: false,
|
|
220
|
+
error: `Command timed out after ${commandTimeoutMs}ms` + (signal ? ` (${signal})` : ''),
|
|
221
|
+
});
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
resolve(base);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export const registerFsRoutes = (app, dependencies) => {
|
|
231
|
+
const {
|
|
232
|
+
os,
|
|
233
|
+
path,
|
|
234
|
+
fsPromises,
|
|
235
|
+
spawn,
|
|
236
|
+
crypto,
|
|
237
|
+
normalizeDirectoryPath,
|
|
238
|
+
resolveProjectDirectory,
|
|
239
|
+
buildAugmentedPath,
|
|
240
|
+
resolveGitBinaryForSpawn,
|
|
241
|
+
vinciUserConfigRoot,
|
|
242
|
+
} = dependencies;
|
|
243
|
+
|
|
244
|
+
const execJobs = new Map();
|
|
245
|
+
const commandTimeoutMs = createCommandTimeoutMs();
|
|
246
|
+
|
|
247
|
+
const pruneExecJobs = () => {
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
for (const [jobId, job] of execJobs.entries()) {
|
|
250
|
+
if (!job || typeof job !== 'object') {
|
|
251
|
+
execJobs.delete(jobId);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
const updatedAt = typeof job.updatedAt === 'number' ? job.updatedAt : 0;
|
|
255
|
+
if (updatedAt && now - updatedAt > EXEC_JOB_TTL_MS) {
|
|
256
|
+
execJobs.delete(jobId);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const runExecJob = async (job) => {
|
|
262
|
+
job.status = 'running';
|
|
263
|
+
job.updatedAt = Date.now();
|
|
264
|
+
|
|
265
|
+
const results = [];
|
|
266
|
+
for (const command of job.commands) {
|
|
267
|
+
if (typeof command !== 'string' || !command.trim()) {
|
|
268
|
+
results.push({ command, success: false, error: 'Invalid command' });
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try {
|
|
273
|
+
const result = await runCommandInDirectory({
|
|
274
|
+
shell: job.shell,
|
|
275
|
+
shellFlag: job.shellFlag,
|
|
276
|
+
command,
|
|
277
|
+
resolvedCwd: job.resolvedCwd,
|
|
278
|
+
spawn,
|
|
279
|
+
buildAugmentedPath,
|
|
280
|
+
commandTimeoutMs,
|
|
281
|
+
});
|
|
282
|
+
results.push(result);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
results.push({
|
|
285
|
+
command,
|
|
286
|
+
success: false,
|
|
287
|
+
error: (error && error.message) || 'Command execution failed',
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
job.results = results;
|
|
292
|
+
job.updatedAt = Date.now();
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
job.results = results;
|
|
296
|
+
job.success = results.every((r) => r.success);
|
|
297
|
+
job.status = 'done';
|
|
298
|
+
job.finishedAt = Date.now();
|
|
299
|
+
job.updatedAt = Date.now();
|
|
300
|
+
};
|
|
301
|
+
|
|
302
|
+
app.get('/api/fs/home', (_req, res) => {
|
|
303
|
+
try {
|
|
304
|
+
const home = os.homedir();
|
|
305
|
+
if (!home || typeof home !== 'string' || home.length === 0) {
|
|
306
|
+
return res.status(500).json({ error: 'Failed to resolve home directory' });
|
|
307
|
+
}
|
|
308
|
+
return res.json({ home });
|
|
309
|
+
} catch (error) {
|
|
310
|
+
console.error('Failed to resolve home directory:', error);
|
|
311
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to resolve home directory' });
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
app.post('/api/fs/mkdir', async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const { path: dirPath, allowOutsideWorkspace, initializeTemplate } = req.body ?? {};
|
|
318
|
+
if (typeof dirPath !== 'string' || !dirPath.trim()) {
|
|
319
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
let resolvedPath = '';
|
|
323
|
+
if (allowOutsideWorkspace) {
|
|
324
|
+
resolvedPath = path.resolve(normalizeDirectoryPath(dirPath));
|
|
325
|
+
} else {
|
|
326
|
+
const resolved = await resolveWorkspacePathFromContext({
|
|
327
|
+
req,
|
|
328
|
+
targetPath: dirPath,
|
|
329
|
+
resolveProjectDirectory,
|
|
330
|
+
path,
|
|
331
|
+
os,
|
|
332
|
+
normalizeDirectoryPath,
|
|
333
|
+
vinciUserConfigRoot,
|
|
334
|
+
});
|
|
335
|
+
if (!resolved.ok) {
|
|
336
|
+
return res.status(400).json({ error: resolved.error });
|
|
337
|
+
}
|
|
338
|
+
resolvedPath = resolved.resolved;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
await fsPromises.mkdir(resolvedPath, { recursive: true });
|
|
342
|
+
|
|
343
|
+
if (initializeTemplate) {
|
|
344
|
+
const templateSrcDir = path.join(vinciUserConfigRoot, 'template');
|
|
345
|
+
const templateExists = await fsPromises.access(templateSrcDir).then(() => true).catch(() => false);
|
|
346
|
+
if (templateExists) {
|
|
347
|
+
const copyDir = async (src, dest) => {
|
|
348
|
+
await fsPromises.mkdir(dest, { recursive: true });
|
|
349
|
+
const entries = await fsPromises.readdir(src, { withFileTypes: true });
|
|
350
|
+
for (const entry of entries) {
|
|
351
|
+
const name = entry.name;
|
|
352
|
+
if (entry.isDirectory()) {
|
|
353
|
+
if (['.venv', 'node_modules', '__pycache__', '.git'].includes(name)) {
|
|
354
|
+
console.log(`[mkdir] skipping directory ${name} from template`);
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
const sPath = path.join(src, name);
|
|
358
|
+
const dPath = path.join(dest, name);
|
|
359
|
+
await copyDir(sPath, dPath);
|
|
360
|
+
} else if (entry.isFile()) {
|
|
361
|
+
if (['.version'].includes(name)) {
|
|
362
|
+
console.log(`[mkdir] skipping file ${name} from template`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const sPath = path.join(src, name);
|
|
366
|
+
const dPath = path.join(dest, name);
|
|
367
|
+
await fsPromises.copyFile(sPath, dPath);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
};
|
|
371
|
+
await copyDir(templateSrcDir, resolvedPath);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return res.json({ success: true, path: resolvedPath });
|
|
376
|
+
} catch (error) {
|
|
377
|
+
console.error('Failed to create directory:', error);
|
|
378
|
+
return res.status(500).json({ error: error.message || 'Failed to create directory' });
|
|
379
|
+
}
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
app.post('/api/fs/clone', async (req, res) => {
|
|
383
|
+
try {
|
|
384
|
+
const { remoteUrl, destinationPath, gitIdentityId } = req.body ?? {};
|
|
385
|
+
const remote = typeof remoteUrl === 'string' ? remoteUrl.trim() : '';
|
|
386
|
+
const destination = typeof destinationPath === 'string' ? destinationPath.trim() : '';
|
|
387
|
+
if (!remote) {
|
|
388
|
+
return res.status(400).json({ error: 'Repository URL is required' });
|
|
389
|
+
}
|
|
390
|
+
if (!destination) {
|
|
391
|
+
return res.status(400).json({ error: 'Destination path is required' });
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
let resolvedDestination = path.resolve(normalizeDirectoryPath(destination));
|
|
395
|
+
let parentPath = path.dirname(resolvedDestination);
|
|
396
|
+
let directoryName = path.basename(resolvedDestination);
|
|
397
|
+
|
|
398
|
+
const cloneIntoDestinationDirectory = destination.endsWith('/') || destination.endsWith('\\');
|
|
399
|
+
if (cloneIntoDestinationDirectory) {
|
|
400
|
+
const inferredName = deriveCloneDirectoryName(remote);
|
|
401
|
+
if (!inferredName) {
|
|
402
|
+
return res.status(400).json({ error: 'Could not infer repository directory name from URL' });
|
|
403
|
+
}
|
|
404
|
+
parentPath = resolvedDestination;
|
|
405
|
+
directoryName = inferredName;
|
|
406
|
+
resolvedDestination = path.join(parentPath, directoryName);
|
|
407
|
+
} else {
|
|
408
|
+
try {
|
|
409
|
+
const stat = await fsPromises.stat(resolvedDestination);
|
|
410
|
+
if (stat.isDirectory()) {
|
|
411
|
+
const inferredName = deriveCloneDirectoryName(remote);
|
|
412
|
+
if (!inferredName) {
|
|
413
|
+
return res.status(400).json({ error: 'Could not infer repository directory name from URL' });
|
|
414
|
+
}
|
|
415
|
+
parentPath = resolvedDestination;
|
|
416
|
+
directoryName = inferredName;
|
|
417
|
+
resolvedDestination = path.join(parentPath, directoryName);
|
|
418
|
+
}
|
|
419
|
+
} catch (error) {
|
|
420
|
+
if (!error || error.code !== 'ENOENT') {
|
|
421
|
+
throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (!directoryName || directoryName === '.' || directoryName === '..') {
|
|
426
|
+
return res.status(400).json({ error: 'Destination path must include a directory name' });
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const identity = await resolveCloneGitIdentity(gitIdentityId);
|
|
430
|
+
const gitArgs = ['clone', '--', remote, directoryName];
|
|
431
|
+
const sshKeyPath = typeof identity?.sshKey === 'string' ? identity.sshKey.trim() : '';
|
|
432
|
+
if (sshKeyPath) {
|
|
433
|
+
gitArgs.unshift(`core.sshCommand=ssh -i ${escapeCloneSshKeyPath(sshKeyPath)} -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new`);
|
|
434
|
+
gitArgs.unshift('-c');
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
await fsPromises.mkdir(parentPath, { recursive: true });
|
|
438
|
+
try {
|
|
439
|
+
await fsPromises.access(resolvedDestination);
|
|
440
|
+
return res.status(409).json({ error: 'Destination path already exists' });
|
|
441
|
+
} catch (error) {
|
|
442
|
+
if (!error || error.code !== 'ENOENT') {
|
|
443
|
+
throw error;
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const output = await new Promise((resolve, reject) => {
|
|
448
|
+
const child = spawn(resolveGitBinaryForSpawn(), gitArgs, {
|
|
449
|
+
cwd: parentPath,
|
|
450
|
+
windowsHide: true,
|
|
451
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
452
|
+
env: {
|
|
453
|
+
...process.env,
|
|
454
|
+
PATH: buildAugmentedPath ? buildAugmentedPath(process.env.PATH || '') : process.env.PATH,
|
|
455
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
456
|
+
},
|
|
457
|
+
});
|
|
458
|
+
|
|
459
|
+
let stdout = '';
|
|
460
|
+
let stderr = '';
|
|
461
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
462
|
+
child.stderr.on('data', (data) => { stderr += data.toString(); });
|
|
463
|
+
child.on('error', reject);
|
|
464
|
+
child.on('close', (code) => {
|
|
465
|
+
const combined = `${stdout}\n${stderr}`.trim();
|
|
466
|
+
if (code === 0) {
|
|
467
|
+
resolve(combined);
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const message = combined || `git clone failed with exit code ${code}`;
|
|
471
|
+
reject(new Error(message));
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
if (identity?.userName && identity?.userEmail) {
|
|
476
|
+
try {
|
|
477
|
+
const { setLocalIdentity } = await import('../git/index.js');
|
|
478
|
+
await setLocalIdentity(resolvedDestination, identity);
|
|
479
|
+
} catch (error) {
|
|
480
|
+
console.warn('Failed to apply git identity after clone:', error);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
return res.json({ success: true, path: resolvedDestination, output });
|
|
485
|
+
} catch (error) {
|
|
486
|
+
console.error('Failed to clone repository:', error);
|
|
487
|
+
return res.status(500).json({ error: error.message || 'Failed to clone repository' });
|
|
488
|
+
}
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
app.get('/api/fs/stat', async (req, res) => {
|
|
492
|
+
const filePath = typeof req.query.path === 'string' ? req.query.path.trim() : '';
|
|
493
|
+
if (!filePath) {
|
|
494
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
try {
|
|
498
|
+
const resolved = await resolveReadPathFromContext({
|
|
499
|
+
req,
|
|
500
|
+
targetPath: filePath,
|
|
501
|
+
resolveProjectDirectory,
|
|
502
|
+
path,
|
|
503
|
+
os,
|
|
504
|
+
normalizeDirectoryPath,
|
|
505
|
+
vinciUserConfigRoot,
|
|
506
|
+
});
|
|
507
|
+
if (!resolved.ok) {
|
|
508
|
+
return res.status(400).json({ error: resolved.error });
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const [canonicalPath, canonicalBase] = await Promise.all([
|
|
512
|
+
fsPromises.realpath(resolved.resolved),
|
|
513
|
+
fsPromises.realpath(resolved.base).catch(() => path.resolve(resolved.base)),
|
|
514
|
+
]);
|
|
515
|
+
|
|
516
|
+
if (!isPathWithinRoot(canonicalPath, canonicalBase, path, os)) {
|
|
517
|
+
return res.status(403).json({ error: 'Access to file denied' });
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const stats = await fsPromises.stat(canonicalPath);
|
|
521
|
+
if (!stats.isFile()) {
|
|
522
|
+
return res.status(400).json({ error: 'Specified path is not a file' });
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
return res.json({ path: canonicalPath, isFile: true, size: stats.size, mtimeMs: stats.mtimeMs });
|
|
526
|
+
} catch (error) {
|
|
527
|
+
const err = error;
|
|
528
|
+
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
529
|
+
return res.status(404).json({ error: 'File not found' });
|
|
530
|
+
}
|
|
531
|
+
if (err && typeof err === 'object' && err.code === 'EACCES') {
|
|
532
|
+
return res.status(403).json({ error: 'Access to file denied' });
|
|
533
|
+
}
|
|
534
|
+
console.error('Failed to stat file:', error);
|
|
535
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to stat file' });
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
|
|
539
|
+
app.get('/api/fs/read', async (req, res) => {
|
|
540
|
+
const filePath = typeof req.query.path === 'string' ? req.query.path.trim() : '';
|
|
541
|
+
const optional = req.query.optional === 'true';
|
|
542
|
+
if (!filePath) {
|
|
543
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
try {
|
|
547
|
+
const resolved = await resolveReadPathFromContext({
|
|
548
|
+
req,
|
|
549
|
+
targetPath: filePath,
|
|
550
|
+
resolveProjectDirectory,
|
|
551
|
+
path,
|
|
552
|
+
os,
|
|
553
|
+
normalizeDirectoryPath,
|
|
554
|
+
vinciUserConfigRoot,
|
|
555
|
+
});
|
|
556
|
+
if (!resolved.ok) {
|
|
557
|
+
return res.status(400).json({ error: resolved.error });
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
const [canonicalPath, canonicalBase] = await Promise.all([
|
|
561
|
+
fsPromises.realpath(resolved.resolved),
|
|
562
|
+
fsPromises.realpath(resolved.base).catch(() => path.resolve(resolved.base)),
|
|
563
|
+
]);
|
|
564
|
+
|
|
565
|
+
if (!isPathWithinRoot(canonicalPath, canonicalBase, path, os)) {
|
|
566
|
+
return res.status(403).json({ error: 'Access to file denied' });
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const stats = await fsPromises.stat(canonicalPath);
|
|
570
|
+
if (!stats.isFile()) {
|
|
571
|
+
return res.status(400).json({ error: 'Specified path is not a file' });
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
const content = await fsPromises.readFile(canonicalPath, 'utf8');
|
|
575
|
+
return res.type('text/plain').send(content);
|
|
576
|
+
} catch (error) {
|
|
577
|
+
const err = error;
|
|
578
|
+
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
579
|
+
if (optional) {
|
|
580
|
+
return res.type('text/plain').send('');
|
|
581
|
+
}
|
|
582
|
+
return res.status(404).json({ error: 'File not found' });
|
|
583
|
+
}
|
|
584
|
+
if (err && typeof err === 'object' && err.code === 'EACCES') {
|
|
585
|
+
return res.status(403).json({ error: 'Access to file denied' });
|
|
586
|
+
}
|
|
587
|
+
console.error('Failed to read file:', error);
|
|
588
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to read file' });
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
app.get('/api/fs/raw', async (req, res) => {
|
|
593
|
+
const filePath = typeof req.query.path === 'string' ? req.query.path.trim() : '';
|
|
594
|
+
if (!filePath) {
|
|
595
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const resolved = await resolveReadPathFromContext({
|
|
600
|
+
req,
|
|
601
|
+
targetPath: filePath,
|
|
602
|
+
resolveProjectDirectory,
|
|
603
|
+
path,
|
|
604
|
+
os,
|
|
605
|
+
normalizeDirectoryPath,
|
|
606
|
+
vinciUserConfigRoot,
|
|
607
|
+
});
|
|
608
|
+
if (!resolved.ok) {
|
|
609
|
+
return res.status(400).json({ error: resolved.error });
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const [canonicalPath, canonicalBase] = await Promise.all([
|
|
613
|
+
fsPromises.realpath(resolved.resolved),
|
|
614
|
+
fsPromises.realpath(resolved.base).catch(() => path.resolve(resolved.base)),
|
|
615
|
+
]);
|
|
616
|
+
|
|
617
|
+
if (!isPathWithinRoot(canonicalPath, canonicalBase, path, os)) {
|
|
618
|
+
return res.status(403).json({ error: 'Access to file denied' });
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
const stats = await fsPromises.stat(canonicalPath);
|
|
622
|
+
if (!stats.isFile()) {
|
|
623
|
+
return res.status(400).json({ error: 'Specified path is not a file' });
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const ext = path.extname(canonicalPath).toLowerCase();
|
|
627
|
+
const mimeMap = {
|
|
628
|
+
'.png': 'image/png',
|
|
629
|
+
'.jpg': 'image/jpeg',
|
|
630
|
+
'.jpeg': 'image/jpeg',
|
|
631
|
+
'.gif': 'image/gif',
|
|
632
|
+
'.svg': 'image/svg+xml',
|
|
633
|
+
'.webp': 'image/webp',
|
|
634
|
+
'.ico': 'image/x-icon',
|
|
635
|
+
'.bmp': 'image/bmp',
|
|
636
|
+
'.avif': 'image/avif',
|
|
637
|
+
};
|
|
638
|
+
const mimeType = mimeMap[ext] || 'application/octet-stream';
|
|
639
|
+
|
|
640
|
+
const download = req.query.download === 'true';
|
|
641
|
+
if (download) {
|
|
642
|
+
const fileName = path.basename(canonicalPath);
|
|
643
|
+
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const content = await fsPromises.readFile(canonicalPath);
|
|
647
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
648
|
+
return res.type(mimeType).send(content);
|
|
649
|
+
} catch (error) {
|
|
650
|
+
const err = error;
|
|
651
|
+
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
652
|
+
return res.status(404).json({ error: 'File not found' });
|
|
653
|
+
}
|
|
654
|
+
if (err && typeof err === 'object' && err.code === 'EACCES') {
|
|
655
|
+
return res.status(403).json({ error: 'Access to file denied' });
|
|
656
|
+
}
|
|
657
|
+
console.error('Failed to read raw file:', error);
|
|
658
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to read file' });
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
app.post('/api/fs/write', async (req, res) => {
|
|
663
|
+
const { path: filePath, content } = req.body || {};
|
|
664
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
665
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
666
|
+
}
|
|
667
|
+
if (typeof content !== 'string') {
|
|
668
|
+
return res.status(400).json({ error: 'Content is required' });
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
try {
|
|
672
|
+
const resolved = await resolveWorkspacePathFromContext({
|
|
673
|
+
req,
|
|
674
|
+
targetPath: filePath,
|
|
675
|
+
resolveProjectDirectory,
|
|
676
|
+
path,
|
|
677
|
+
os,
|
|
678
|
+
normalizeDirectoryPath,
|
|
679
|
+
vinciUserConfigRoot,
|
|
680
|
+
});
|
|
681
|
+
if (!resolved.ok) {
|
|
682
|
+
return res.status(400).json({ error: resolved.error });
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
await fsPromises.mkdir(path.dirname(resolved.resolved), { recursive: true });
|
|
686
|
+
await fsPromises.writeFile(resolved.resolved, content, 'utf8');
|
|
687
|
+
return res.json({ success: true, path: resolved.resolved });
|
|
688
|
+
} catch (error) {
|
|
689
|
+
const err = error;
|
|
690
|
+
if (err && typeof err === 'object' && err.code === 'EACCES') {
|
|
691
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
692
|
+
}
|
|
693
|
+
console.error('Failed to write file:', error);
|
|
694
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to write file' });
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
app.post('/api/fs/delete', async (req, res) => {
|
|
699
|
+
const { path: targetPath } = req.body || {};
|
|
700
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
701
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
try {
|
|
705
|
+
const resolved = await resolveWorkspacePathFromContext({
|
|
706
|
+
req,
|
|
707
|
+
targetPath,
|
|
708
|
+
resolveProjectDirectory,
|
|
709
|
+
path,
|
|
710
|
+
os,
|
|
711
|
+
normalizeDirectoryPath,
|
|
712
|
+
vinciUserConfigRoot,
|
|
713
|
+
});
|
|
714
|
+
if (!resolved.ok) {
|
|
715
|
+
return res.status(400).json({ error: resolved.error });
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
await fsPromises.rm(resolved.resolved, { recursive: true, force: true });
|
|
719
|
+
return res.json({ success: true, path: resolved.resolved });
|
|
720
|
+
} catch (error) {
|
|
721
|
+
const err = error;
|
|
722
|
+
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
723
|
+
return res.status(404).json({ error: 'File or directory not found' });
|
|
724
|
+
}
|
|
725
|
+
if (err && typeof err === 'object' && err.code === 'EACCES') {
|
|
726
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
727
|
+
}
|
|
728
|
+
console.error('Failed to delete path:', error);
|
|
729
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to delete path' });
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
733
|
+
app.post('/api/fs/rename', async (req, res) => {
|
|
734
|
+
const { oldPath, newPath } = req.body || {};
|
|
735
|
+
if (!oldPath || typeof oldPath !== 'string') {
|
|
736
|
+
return res.status(400).json({ error: 'oldPath is required' });
|
|
737
|
+
}
|
|
738
|
+
if (!newPath || typeof newPath !== 'string') {
|
|
739
|
+
return res.status(400).json({ error: 'newPath is required' });
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
try {
|
|
743
|
+
const resolvedOld = await resolveWorkspacePathFromContext({
|
|
744
|
+
req,
|
|
745
|
+
targetPath: oldPath,
|
|
746
|
+
resolveProjectDirectory,
|
|
747
|
+
path,
|
|
748
|
+
os,
|
|
749
|
+
normalizeDirectoryPath,
|
|
750
|
+
vinciUserConfigRoot,
|
|
751
|
+
});
|
|
752
|
+
if (!resolvedOld.ok) {
|
|
753
|
+
return res.status(400).json({ error: resolvedOld.error });
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
const resolvedNew = await resolveWorkspacePathFromContext({
|
|
757
|
+
req,
|
|
758
|
+
targetPath: newPath,
|
|
759
|
+
resolveProjectDirectory,
|
|
760
|
+
path,
|
|
761
|
+
os,
|
|
762
|
+
normalizeDirectoryPath,
|
|
763
|
+
vinciUserConfigRoot,
|
|
764
|
+
});
|
|
765
|
+
if (!resolvedNew.ok) {
|
|
766
|
+
return res.status(400).json({ error: resolvedNew.error });
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
if (resolvedOld.base !== resolvedNew.base) {
|
|
770
|
+
return res.status(400).json({ error: 'Source and destination must share the same workspace root' });
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
await fsPromises.rename(resolvedOld.resolved, resolvedNew.resolved);
|
|
774
|
+
return res.json({ success: true, path: resolvedNew.resolved });
|
|
775
|
+
} catch (error) {
|
|
776
|
+
const err = error;
|
|
777
|
+
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
778
|
+
return res.status(404).json({ error: 'Source path not found' });
|
|
779
|
+
}
|
|
780
|
+
if (err && typeof err === 'object' && err.code === 'EACCES') {
|
|
781
|
+
return res.status(403).json({ error: 'Access denied' });
|
|
782
|
+
}
|
|
783
|
+
console.error('Failed to rename path:', error);
|
|
784
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to rename path' });
|
|
785
|
+
}
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
app.post('/api/fs/reveal', async (req, res) => {
|
|
789
|
+
const { path: targetPath } = req.body || {};
|
|
790
|
+
if (!targetPath || typeof targetPath !== 'string') {
|
|
791
|
+
return res.status(400).json({ error: 'Path is required' });
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
try {
|
|
795
|
+
const resolved = path.resolve(targetPath.trim());
|
|
796
|
+
await fsPromises.access(resolved);
|
|
797
|
+
|
|
798
|
+
const platform = process.platform;
|
|
799
|
+
if (platform === 'darwin') {
|
|
800
|
+
const stat = await fsPromises.stat(resolved);
|
|
801
|
+
if (stat.isDirectory()) {
|
|
802
|
+
spawn('open', [resolved], { windowsHide: true, stdio: 'ignore', detached: true }).unref();
|
|
803
|
+
} else {
|
|
804
|
+
spawn('open', ['-R', resolved], { windowsHide: true, stdio: 'ignore', detached: true }).unref();
|
|
805
|
+
}
|
|
806
|
+
} else if (platform === 'win32') {
|
|
807
|
+
const stat = await fsPromises.stat(resolved);
|
|
808
|
+
const escapedPath = resolved.replace(/'/g, "''");
|
|
809
|
+
const explorerArg = stat.isDirectory() ? escapedPath : `/select,${escapedPath}`;
|
|
810
|
+
const command = `Start-Process -FilePath explorer.exe -ArgumentList '${explorerArg}'`;
|
|
811
|
+
await new Promise((resolve, reject) => {
|
|
812
|
+
const child = spawn('powershell.exe', ['-NoProfile', '-NonInteractive', '-Command', command], {
|
|
813
|
+
windowsHide: true,
|
|
814
|
+
stdio: 'ignore',
|
|
815
|
+
});
|
|
816
|
+
child.once('error', reject);
|
|
817
|
+
child.once('exit', (code) => {
|
|
818
|
+
if (code === 0) {
|
|
819
|
+
resolve();
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
reject(new Error(`Explorer launch failed with code ${code ?? 'unknown'}`));
|
|
823
|
+
});
|
|
824
|
+
});
|
|
825
|
+
} else {
|
|
826
|
+
const stat = await fsPromises.stat(resolved);
|
|
827
|
+
const dir = stat.isDirectory() ? resolved : path.dirname(resolved);
|
|
828
|
+
spawn('xdg-open', [dir], { windowsHide: true, stdio: 'ignore', detached: true }).unref();
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
return res.json({ success: true, path: resolved });
|
|
832
|
+
} catch (error) {
|
|
833
|
+
const err = error;
|
|
834
|
+
if (err && typeof err === 'object' && err.code === 'ENOENT') {
|
|
835
|
+
return res.status(404).json({ error: 'Path not found' });
|
|
836
|
+
}
|
|
837
|
+
console.error('Failed to reveal path:', error);
|
|
838
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to reveal path' });
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
|
|
842
|
+
app.post('/api/fs/exec', async (req, res) => {
|
|
843
|
+
const { commands, cwd, background } = req.body || {};
|
|
844
|
+
if (!Array.isArray(commands) || commands.length === 0) {
|
|
845
|
+
return res.status(400).json({ error: 'Commands array is required' });
|
|
846
|
+
}
|
|
847
|
+
if (!cwd || typeof cwd !== 'string') {
|
|
848
|
+
return res.status(400).json({ error: 'Working directory (cwd) is required' });
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
pruneExecJobs();
|
|
852
|
+
|
|
853
|
+
try {
|
|
854
|
+
const resolvedCwd = path.resolve(normalizeDirectoryPath(cwd));
|
|
855
|
+
const stats = await fsPromises.stat(resolvedCwd);
|
|
856
|
+
if (!stats.isDirectory()) {
|
|
857
|
+
return res.status(400).json({ error: 'Specified cwd is not a directory' });
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
const shell = process.env.SHELL || (process.platform === 'win32' ? 'cmd.exe' : '/bin/sh');
|
|
861
|
+
const shellFlag = process.platform === 'win32' ? '/c' : '-c';
|
|
862
|
+
|
|
863
|
+
const jobId = crypto.randomUUID();
|
|
864
|
+
const job = {
|
|
865
|
+
jobId,
|
|
866
|
+
status: 'queued',
|
|
867
|
+
success: null,
|
|
868
|
+
commands,
|
|
869
|
+
resolvedCwd,
|
|
870
|
+
shell,
|
|
871
|
+
shellFlag,
|
|
872
|
+
results: [],
|
|
873
|
+
startedAt: Date.now(),
|
|
874
|
+
finishedAt: null,
|
|
875
|
+
updatedAt: Date.now(),
|
|
876
|
+
};
|
|
877
|
+
|
|
878
|
+
execJobs.set(jobId, job);
|
|
879
|
+
|
|
880
|
+
const isBackground = background === true;
|
|
881
|
+
if (isBackground) {
|
|
882
|
+
void runExecJob(job).catch((error) => {
|
|
883
|
+
job.status = 'done';
|
|
884
|
+
job.success = false;
|
|
885
|
+
job.results = Array.isArray(job.results) ? job.results : [];
|
|
886
|
+
job.results.push({
|
|
887
|
+
command: '',
|
|
888
|
+
success: false,
|
|
889
|
+
error: (error && error.message) || 'Command execution failed',
|
|
890
|
+
});
|
|
891
|
+
job.finishedAt = Date.now();
|
|
892
|
+
job.updatedAt = Date.now();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
return res.status(202).json({
|
|
896
|
+
jobId,
|
|
897
|
+
status: 'running',
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
await runExecJob(job);
|
|
902
|
+
return res.json({
|
|
903
|
+
jobId,
|
|
904
|
+
status: job.status,
|
|
905
|
+
success: job.success === true,
|
|
906
|
+
results: job.results,
|
|
907
|
+
});
|
|
908
|
+
} catch (error) {
|
|
909
|
+
console.error('Failed to execute commands:', error);
|
|
910
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to execute commands' });
|
|
911
|
+
}
|
|
912
|
+
});
|
|
913
|
+
|
|
914
|
+
app.get('/api/fs/exec/:jobId', (req, res) => {
|
|
915
|
+
const jobId = typeof req.params?.jobId === 'string' ? req.params.jobId : '';
|
|
916
|
+
if (!jobId) {
|
|
917
|
+
return res.status(400).json({ error: 'Job id is required' });
|
|
918
|
+
}
|
|
919
|
+
|
|
920
|
+
pruneExecJobs();
|
|
921
|
+
|
|
922
|
+
const job = execJobs.get(jobId);
|
|
923
|
+
if (!job) {
|
|
924
|
+
return res.status(404).json({ error: 'Job not found' });
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
job.updatedAt = Date.now();
|
|
928
|
+
return res.json({
|
|
929
|
+
jobId: job.jobId,
|
|
930
|
+
status: job.status,
|
|
931
|
+
success: job.success === true,
|
|
932
|
+
results: Array.isArray(job.results) ? job.results : [],
|
|
933
|
+
});
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
app.get('/api/fs/list', async (req, res) => {
|
|
937
|
+
const rawPath = typeof req.query.path === 'string' && req.query.path.trim().length > 0
|
|
938
|
+
? req.query.path.trim()
|
|
939
|
+
: os.homedir();
|
|
940
|
+
const respectGitignore = req.query.respectGitignore === 'true';
|
|
941
|
+
let resolvedPath = '';
|
|
942
|
+
|
|
943
|
+
const isPlansDirectory = (value) => {
|
|
944
|
+
if (!value || typeof value !== 'string') return false;
|
|
945
|
+
const normalized = value.replace(/\\/g, '/').replace(/\/+$/, '');
|
|
946
|
+
return normalized.endsWith('/.opencode/plans') || normalized.endsWith('.opencode/plans');
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
try {
|
|
950
|
+
resolvedPath = path.resolve(normalizeDirectoryPath(rawPath));
|
|
951
|
+
|
|
952
|
+
const stats = await fsPromises.stat(resolvedPath);
|
|
953
|
+
if (!stats.isDirectory()) {
|
|
954
|
+
return res.status(400).json({ error: 'Specified path is not a directory' });
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
const dirents = await fsPromises.readdir(resolvedPath, { withFileTypes: true });
|
|
958
|
+
let ignoredPaths = new Set();
|
|
959
|
+
if (respectGitignore) {
|
|
960
|
+
try {
|
|
961
|
+
const pathsToCheck = dirents.map((d) => d.name);
|
|
962
|
+
if (pathsToCheck.length > 0) {
|
|
963
|
+
try {
|
|
964
|
+
const result = await new Promise((resolve) => {
|
|
965
|
+
const child = spawn(resolveGitBinaryForSpawn(), ['check-ignore', '--', ...pathsToCheck], {
|
|
966
|
+
cwd: resolvedPath,
|
|
967
|
+
windowsHide: true,
|
|
968
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
969
|
+
});
|
|
970
|
+
|
|
971
|
+
let stdout = '';
|
|
972
|
+
child.stdout.on('data', (data) => { stdout += data.toString(); });
|
|
973
|
+
child.on('close', () => resolve(stdout));
|
|
974
|
+
child.on('error', () => resolve(''));
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
result.split('\n').filter(Boolean).forEach((name) => {
|
|
978
|
+
const fullPath = path.join(resolvedPath, name.trim());
|
|
979
|
+
ignoredPaths.add(fullPath);
|
|
980
|
+
});
|
|
981
|
+
} catch {
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
} catch {
|
|
985
|
+
}
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
const entries = await Promise.all(
|
|
989
|
+
dirents.map(async (dirent) => {
|
|
990
|
+
const entryPath = path.join(resolvedPath, dirent.name);
|
|
991
|
+
if (respectGitignore && ignoredPaths.has(entryPath)) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
let isDirectory = dirent.isDirectory();
|
|
996
|
+
const isSymbolicLink = dirent.isSymbolicLink();
|
|
997
|
+
|
|
998
|
+
if (!isDirectory && isSymbolicLink) {
|
|
999
|
+
try {
|
|
1000
|
+
const linkStats = await fsPromises.stat(entryPath);
|
|
1001
|
+
isDirectory = linkStats.isDirectory();
|
|
1002
|
+
} catch {
|
|
1003
|
+
isDirectory = false;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
return {
|
|
1008
|
+
name: dirent.name,
|
|
1009
|
+
path: entryPath,
|
|
1010
|
+
isDirectory,
|
|
1011
|
+
isFile: dirent.isFile(),
|
|
1012
|
+
isSymbolicLink,
|
|
1013
|
+
};
|
|
1014
|
+
})
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
return res.json({
|
|
1018
|
+
path: resolvedPath,
|
|
1019
|
+
entries: entries.filter(Boolean),
|
|
1020
|
+
});
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
const err = error;
|
|
1023
|
+
const code = err && typeof err === 'object' && 'code' in err ? err.code : undefined;
|
|
1024
|
+
const isPlansPath = code === 'ENOENT' && (isPlansDirectory(resolvedPath) || isPlansDirectory(rawPath));
|
|
1025
|
+
if (code !== 'ENOENT') {
|
|
1026
|
+
console.error('Failed to list directory:', error);
|
|
1027
|
+
}
|
|
1028
|
+
if (code === 'ENOENT') {
|
|
1029
|
+
if (isPlansPath) {
|
|
1030
|
+
return res.json({ path: resolvedPath || rawPath, entries: [] });
|
|
1031
|
+
}
|
|
1032
|
+
return res.status(404).json({ error: 'Directory not found' });
|
|
1033
|
+
}
|
|
1034
|
+
if (code === 'EACCES') {
|
|
1035
|
+
return res.status(403).json({ error: 'Access to directory denied' });
|
|
1036
|
+
}
|
|
1037
|
+
return res.status(500).json({ error: (error && error.message) || 'Failed to list directory' });
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
};
|