@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,399 @@
|
|
|
1
|
+
export const registerProjectIconRoutes = (app, dependencies) => {
|
|
2
|
+
const {
|
|
3
|
+
fsPromises,
|
|
4
|
+
path,
|
|
5
|
+
crypto,
|
|
6
|
+
vinciDataDir,
|
|
7
|
+
sanitizeProjects,
|
|
8
|
+
readSettingsFromDiskMigrated,
|
|
9
|
+
persistSettings,
|
|
10
|
+
createFsSearchRuntime,
|
|
11
|
+
spawn,
|
|
12
|
+
resolveGitBinaryForSpawn,
|
|
13
|
+
} = dependencies;
|
|
14
|
+
|
|
15
|
+
const projectIconsDirPath = path.join(vinciDataDir, 'project-icons');
|
|
16
|
+
const projectIconMimeToExtension = {
|
|
17
|
+
'image/png': 'png',
|
|
18
|
+
'image/jpeg': 'jpg',
|
|
19
|
+
'image/svg+xml': 'svg',
|
|
20
|
+
'image/webp': 'webp',
|
|
21
|
+
'image/x-icon': 'ico',
|
|
22
|
+
};
|
|
23
|
+
const projectIconExtensionToMime = Object.fromEntries(
|
|
24
|
+
Object.entries(projectIconMimeToExtension).map(([mime, ext]) => [ext, mime])
|
|
25
|
+
);
|
|
26
|
+
const projectIconSupportedMimes = new Set(Object.keys(projectIconMimeToExtension));
|
|
27
|
+
const projectIconMaxBytes = 5 * 1024 * 1024;
|
|
28
|
+
const projectIconThemeColors = {
|
|
29
|
+
light: '#111111',
|
|
30
|
+
dark: '#f5f5f5',
|
|
31
|
+
};
|
|
32
|
+
const projectIconHexColorPattern = /^#(?:[\da-fA-F]{3}|[\da-fA-F]{4}|[\da-fA-F]{6}|[\da-fA-F]{8})$/;
|
|
33
|
+
|
|
34
|
+
const normalizeProjectIconMime = (value) => {
|
|
35
|
+
if (typeof value !== 'string') {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const normalized = value.trim().toLowerCase();
|
|
40
|
+
if (normalized === 'image/jpg') {
|
|
41
|
+
return 'image/jpeg';
|
|
42
|
+
}
|
|
43
|
+
if (projectIconSupportedMimes.has(normalized)) {
|
|
44
|
+
return normalized;
|
|
45
|
+
}
|
|
46
|
+
return null;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const projectIconBaseName = (projectId) => {
|
|
50
|
+
const hash = crypto.createHash('sha1').update(projectId).digest('hex');
|
|
51
|
+
return `project-${hash}`;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const projectIconPathForMime = (projectId, mime) => {
|
|
55
|
+
const normalizedMime = normalizeProjectIconMime(mime);
|
|
56
|
+
if (!normalizedMime) {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
const ext = projectIconMimeToExtension[normalizedMime];
|
|
60
|
+
return path.join(projectIconsDirPath, `${projectIconBaseName(projectId)}.${ext}`);
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const projectIconPathCandidates = (projectId) => {
|
|
64
|
+
const base = projectIconBaseName(projectId);
|
|
65
|
+
return Object.values(projectIconMimeToExtension).map((ext) => path.join(projectIconsDirPath, `${base}.${ext}`));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const removeProjectIconFiles = async (projectId, keepPath) => {
|
|
69
|
+
const candidates = projectIconPathCandidates(projectId);
|
|
70
|
+
await Promise.all(candidates.map(async (candidatePath) => {
|
|
71
|
+
if (keepPath && candidatePath === keepPath) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
await fsPromises.unlink(candidatePath);
|
|
76
|
+
} catch (error) {
|
|
77
|
+
if (!error || typeof error !== 'object' || error.code !== 'ENOENT') {
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}));
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const parseProjectIconDataUrl = (value) => {
|
|
85
|
+
if (typeof value !== 'string') {
|
|
86
|
+
return { ok: false, error: 'dataUrl is required' };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const trimmed = value.trim();
|
|
90
|
+
const match = trimmed.match(/^data:([^;,]+);base64,([A-Za-z0-9+/=\s]+)$/i);
|
|
91
|
+
if (!match) {
|
|
92
|
+
return { ok: false, error: 'Invalid dataUrl format' };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const mime = normalizeProjectIconMime(match[1]);
|
|
96
|
+
if (!mime || !['image/png', 'image/jpeg', 'image/svg+xml'].includes(mime)) {
|
|
97
|
+
return { ok: false, error: 'Icon must be PNG, JPEG, or SVG' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const base64 = match[2].replace(/\s+/g, '');
|
|
102
|
+
const bytes = Buffer.from(base64, 'base64');
|
|
103
|
+
if (bytes.length === 0) {
|
|
104
|
+
return { ok: false, error: 'Icon content is empty' };
|
|
105
|
+
}
|
|
106
|
+
if (bytes.length > projectIconMaxBytes) {
|
|
107
|
+
return { ok: false, error: 'Icon exceeds size limit (5 MB)' };
|
|
108
|
+
}
|
|
109
|
+
return { ok: true, mime, bytes };
|
|
110
|
+
} catch {
|
|
111
|
+
return { ok: false, error: 'Failed to decode icon data' };
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
const normalizeProjectIconThemeVariant = (value) => {
|
|
116
|
+
if (typeof value !== 'string') {
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const normalized = value.trim().toLowerCase();
|
|
121
|
+
if (normalized === 'light' || normalized === 'dark') {
|
|
122
|
+
return normalized;
|
|
123
|
+
}
|
|
124
|
+
return null;
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
const normalizeProjectIconColor = (value) => {
|
|
128
|
+
if (typeof value !== 'string') {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const normalized = value.trim();
|
|
133
|
+
if (!projectIconHexColorPattern.test(normalized)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
return normalized;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
const applyProjectIconSvgTheme = (svgMarkup, themeVariant, iconColor) => {
|
|
140
|
+
if (typeof svgMarkup !== 'string') {
|
|
141
|
+
return svgMarkup;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const color = iconColor || projectIconThemeColors[themeVariant];
|
|
145
|
+
if (!color) {
|
|
146
|
+
return svgMarkup;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const svgTagIndex = svgMarkup.search(/<svg\b/i);
|
|
150
|
+
if (svgTagIndex === -1) {
|
|
151
|
+
return svgMarkup;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const svgOpenTagEndIndex = svgMarkup.indexOf('>', svgTagIndex);
|
|
155
|
+
if (svgOpenTagEndIndex === -1) {
|
|
156
|
+
return svgMarkup;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const overrideStyle = `<style data-vinci-theme-icon="1">:root{color:${color}!important;}</style>`;
|
|
160
|
+
return `${svgMarkup.slice(0, svgOpenTagEndIndex + 1)}${overrideStyle}${svgMarkup.slice(svgOpenTagEndIndex + 1)}`;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const findProjectById = (settings, projectId) => {
|
|
164
|
+
const projects = sanitizeProjects(settings?.projects) || [];
|
|
165
|
+
const index = projects.findIndex((project) => project.id === projectId);
|
|
166
|
+
if (index === -1) {
|
|
167
|
+
return { projects, index: -1, project: null };
|
|
168
|
+
}
|
|
169
|
+
return { projects, index, project: projects[index] };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const fsSearchRuntime = createFsSearchRuntime({
|
|
173
|
+
fsPromises,
|
|
174
|
+
path,
|
|
175
|
+
spawn,
|
|
176
|
+
resolveGitBinaryForSpawn,
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
app.get('/api/projects/:projectId/icon', async (req, res) => {
|
|
180
|
+
const projectId = typeof req.params.projectId === 'string' ? req.params.projectId.trim() : '';
|
|
181
|
+
if (!projectId) {
|
|
182
|
+
return res.status(400).json({ error: 'projectId is required' });
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
187
|
+
const { project } = findProjectById(settings, projectId);
|
|
188
|
+
if (!project) {
|
|
189
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const metadataMime = normalizeProjectIconMime(project.iconImage?.mime);
|
|
193
|
+
const preferredPath = metadataMime ? projectIconPathForMime(projectId, metadataMime) : null;
|
|
194
|
+
const candidates = preferredPath
|
|
195
|
+
? [preferredPath, ...projectIconPathCandidates(projectId).filter((candidate) => candidate !== preferredPath)]
|
|
196
|
+
: projectIconPathCandidates(projectId);
|
|
197
|
+
|
|
198
|
+
const themeQuery = Array.isArray(req.query?.theme) ? req.query.theme[0] : req.query?.theme;
|
|
199
|
+
const requestedThemeVariant = normalizeProjectIconThemeVariant(themeQuery);
|
|
200
|
+
const iconColorQuery = Array.isArray(req.query?.iconColor) ? req.query.iconColor[0] : req.query?.iconColor;
|
|
201
|
+
const requestedIconColor = normalizeProjectIconColor(iconColorQuery);
|
|
202
|
+
|
|
203
|
+
for (const iconPath of candidates) {
|
|
204
|
+
try {
|
|
205
|
+
const data = await fsPromises.readFile(iconPath);
|
|
206
|
+
const ext = path.extname(iconPath).slice(1).toLowerCase();
|
|
207
|
+
const resolvedMime = iconPath === preferredPath && metadataMime
|
|
208
|
+
? metadataMime
|
|
209
|
+
: projectIconExtensionToMime[ext] || 'application/octet-stream';
|
|
210
|
+
const contentType = resolvedMime === 'image/svg+xml' ? 'image/svg+xml; charset=utf-8' : resolvedMime;
|
|
211
|
+
|
|
212
|
+
if (resolvedMime === 'image/svg+xml' && requestedThemeVariant) {
|
|
213
|
+
const svgMarkup = data.toString('utf8');
|
|
214
|
+
const themedSvgMarkup = applyProjectIconSvgTheme(svgMarkup, requestedThemeVariant, requestedIconColor);
|
|
215
|
+
res.setHeader('Content-Type', contentType);
|
|
216
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
217
|
+
return res.send(themedSvgMarkup);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (resolvedMime === 'image/svg+xml' && requestedIconColor) {
|
|
221
|
+
const svgMarkup = data.toString('utf8');
|
|
222
|
+
const themedSvgMarkup = applyProjectIconSvgTheme(svgMarkup, requestedThemeVariant, requestedIconColor);
|
|
223
|
+
res.setHeader('Content-Type', contentType);
|
|
224
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
225
|
+
return res.send(themedSvgMarkup);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
res.setHeader('Content-Type', contentType);
|
|
229
|
+
res.setHeader('Cache-Control', 'public, max-age=31536000, immutable');
|
|
230
|
+
return res.send(data);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
if (!error || typeof error !== 'object' || error.code !== 'ENOENT') {
|
|
233
|
+
console.warn('Failed to read project icon:', error);
|
|
234
|
+
return res.status(500).json({ error: 'Failed to read project icon' });
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return res.status(404).json({ error: 'Project icon not found' });
|
|
240
|
+
} catch (error) {
|
|
241
|
+
console.warn('Failed to load project icon:', error);
|
|
242
|
+
return res.status(500).json({ error: 'Failed to load project icon' });
|
|
243
|
+
}
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
app.put('/api/projects/:projectId/icon', async (req, res) => {
|
|
247
|
+
const projectId = typeof req.params.projectId === 'string' ? req.params.projectId.trim() : '';
|
|
248
|
+
if (!projectId) {
|
|
249
|
+
return res.status(400).json({ error: 'projectId is required' });
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
const parsed = parseProjectIconDataUrl(req.body?.dataUrl);
|
|
253
|
+
if (!parsed.ok) {
|
|
254
|
+
return res.status(400).json({ error: parsed.error });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
try {
|
|
258
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
259
|
+
const { projects, project } = findProjectById(settings, projectId);
|
|
260
|
+
if (!project) {
|
|
261
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const iconPath = projectIconPathForMime(projectId, parsed.mime);
|
|
265
|
+
if (!iconPath) {
|
|
266
|
+
return res.status(400).json({ error: 'Unsupported icon format' });
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
await fsPromises.mkdir(projectIconsDirPath, { recursive: true });
|
|
270
|
+
await fsPromises.writeFile(iconPath, parsed.bytes);
|
|
271
|
+
await removeProjectIconFiles(projectId, iconPath);
|
|
272
|
+
|
|
273
|
+
const updatedAt = Date.now();
|
|
274
|
+
const nextProjects = projects.map((entry) => (
|
|
275
|
+
entry.id === projectId
|
|
276
|
+
? { ...entry, iconImage: { mime: parsed.mime, updatedAt, source: 'custom' } }
|
|
277
|
+
: entry
|
|
278
|
+
));
|
|
279
|
+
const updatedSettings = await persistSettings({ projects: nextProjects });
|
|
280
|
+
const updatedProject = (updatedSettings.projects || []).find((entry) => entry.id === projectId) || null;
|
|
281
|
+
|
|
282
|
+
return res.json({ project: updatedProject, settings: updatedSettings });
|
|
283
|
+
} catch (error) {
|
|
284
|
+
console.warn('Failed to upload project icon:', error);
|
|
285
|
+
return res.status(500).json({ error: 'Failed to upload project icon' });
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
app.delete('/api/projects/:projectId/icon', async (req, res) => {
|
|
290
|
+
const projectId = typeof req.params.projectId === 'string' ? req.params.projectId.trim() : '';
|
|
291
|
+
if (!projectId) {
|
|
292
|
+
return res.status(400).json({ error: 'projectId is required' });
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
try {
|
|
296
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
297
|
+
const { projects, project } = findProjectById(settings, projectId);
|
|
298
|
+
if (!project) {
|
|
299
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
await removeProjectIconFiles(projectId);
|
|
303
|
+
|
|
304
|
+
const nextProjects = projects.map((entry) => (
|
|
305
|
+
entry.id === projectId
|
|
306
|
+
? { ...entry, iconImage: null }
|
|
307
|
+
: entry
|
|
308
|
+
));
|
|
309
|
+
const updatedSettings = await persistSettings({ projects: nextProjects });
|
|
310
|
+
const updatedProject = (updatedSettings.projects || []).find((entry) => entry.id === projectId) || null;
|
|
311
|
+
|
|
312
|
+
return res.json({ project: updatedProject, settings: updatedSettings });
|
|
313
|
+
} catch (error) {
|
|
314
|
+
console.warn('Failed to remove project icon:', error);
|
|
315
|
+
return res.status(500).json({ error: 'Failed to remove project icon' });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
app.post('/api/projects/:projectId/icon/discover', async (req, res) => {
|
|
320
|
+
const projectId = typeof req.params.projectId === 'string' ? req.params.projectId.trim() : '';
|
|
321
|
+
if (!projectId) {
|
|
322
|
+
return res.status(400).json({ error: 'projectId is required' });
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
try {
|
|
326
|
+
const settings = await readSettingsFromDiskMigrated();
|
|
327
|
+
const { projects, project } = findProjectById(settings, projectId);
|
|
328
|
+
if (!project) {
|
|
329
|
+
return res.status(404).json({ error: 'Project not found' });
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
const force = req.body?.force === true;
|
|
333
|
+
if (project.iconImage?.source === 'custom' && !force) {
|
|
334
|
+
return res.json({
|
|
335
|
+
project,
|
|
336
|
+
skipped: true,
|
|
337
|
+
reason: 'custom-icon-present',
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const faviconCandidates = await fsSearchRuntime.searchFilesystemFiles(project.path, {
|
|
342
|
+
limit: 200,
|
|
343
|
+
query: 'favicon',
|
|
344
|
+
includeHidden: true,
|
|
345
|
+
respectGitignore: false,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
const filtered = faviconCandidates
|
|
349
|
+
.filter((entry) => /(^|\/)favicon\.(ico|png|svg|jpg|jpeg|webp)$/i.test(entry.path))
|
|
350
|
+
.sort((a, b) => a.path.length - b.path.length);
|
|
351
|
+
|
|
352
|
+
const selected = filtered[0];
|
|
353
|
+
if (!selected) {
|
|
354
|
+
return res.status(404).json({ error: 'No favicon found in project' });
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const ext = path.extname(selected.path).slice(1).toLowerCase();
|
|
358
|
+
const mime = projectIconExtensionToMime[ext] || null;
|
|
359
|
+
if (!mime) {
|
|
360
|
+
return res.status(415).json({ error: 'Unsupported favicon format' });
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
const bytes = await fsPromises.readFile(selected.path);
|
|
364
|
+
if (bytes.length === 0) {
|
|
365
|
+
return res.status(400).json({ error: 'Discovered icon is empty' });
|
|
366
|
+
}
|
|
367
|
+
if (bytes.length > projectIconMaxBytes) {
|
|
368
|
+
return res.status(400).json({ error: 'Discovered icon exceeds size limit (5 MB)' });
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const iconPath = projectIconPathForMime(projectId, mime);
|
|
372
|
+
if (!iconPath) {
|
|
373
|
+
return res.status(415).json({ error: 'Unsupported favicon format' });
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
await fsPromises.mkdir(projectIconsDirPath, { recursive: true });
|
|
377
|
+
await fsPromises.writeFile(iconPath, bytes);
|
|
378
|
+
await removeProjectIconFiles(projectId, iconPath);
|
|
379
|
+
|
|
380
|
+
const updatedAt = Date.now();
|
|
381
|
+
const nextProjects = projects.map((entry) => (
|
|
382
|
+
entry.id === projectId
|
|
383
|
+
? { ...entry, iconImage: { mime, updatedAt, source: 'auto' } }
|
|
384
|
+
: entry
|
|
385
|
+
));
|
|
386
|
+
const updatedSettings = await persistSettings({ projects: nextProjects });
|
|
387
|
+
const updatedProject = (updatedSettings.projects || []).find((entry) => entry.id === projectId) || null;
|
|
388
|
+
|
|
389
|
+
return res.json({
|
|
390
|
+
project: updatedProject,
|
|
391
|
+
settings: updatedSettings,
|
|
392
|
+
discoveredPath: selected.path,
|
|
393
|
+
});
|
|
394
|
+
} catch (error) {
|
|
395
|
+
console.warn('Failed to discover project icon:', error);
|
|
396
|
+
return res.status(500).json({ error: 'Failed to discover project icon' });
|
|
397
|
+
}
|
|
398
|
+
});
|
|
399
|
+
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import crypto from 'crypto';
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { registerProjectIconRoutes } from './project-icon-routes.js';
|
|
6
|
+
|
|
7
|
+
const createRouteRegistry = () => {
|
|
8
|
+
const routes = new Map();
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
app: {
|
|
12
|
+
get(routePath, handler) {
|
|
13
|
+
routes.set(`GET ${routePath}`, handler);
|
|
14
|
+
},
|
|
15
|
+
post(routePath, handler) {
|
|
16
|
+
routes.set(`POST ${routePath}`, handler);
|
|
17
|
+
},
|
|
18
|
+
put(routePath, handler) {
|
|
19
|
+
routes.set(`PUT ${routePath}`, handler);
|
|
20
|
+
},
|
|
21
|
+
delete(routePath, handler) {
|
|
22
|
+
routes.set(`DELETE ${routePath}`, handler);
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
getRoute(method, routePath) {
|
|
26
|
+
return routes.get(`${method} ${routePath}`);
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const createMockResponse = () => {
|
|
32
|
+
const headers = new Map();
|
|
33
|
+
let statusCode = 200;
|
|
34
|
+
let body = null;
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
setHeader(name, value) {
|
|
38
|
+
headers.set(name.toLowerCase(), value);
|
|
39
|
+
},
|
|
40
|
+
getHeader(name) {
|
|
41
|
+
return headers.get(name.toLowerCase());
|
|
42
|
+
},
|
|
43
|
+
status(code) {
|
|
44
|
+
statusCode = code;
|
|
45
|
+
return this;
|
|
46
|
+
},
|
|
47
|
+
json(payload) {
|
|
48
|
+
body = payload;
|
|
49
|
+
return this;
|
|
50
|
+
},
|
|
51
|
+
send(payload) {
|
|
52
|
+
body = payload;
|
|
53
|
+
return this;
|
|
54
|
+
},
|
|
55
|
+
get statusCode() {
|
|
56
|
+
return statusCode;
|
|
57
|
+
},
|
|
58
|
+
get body() {
|
|
59
|
+
return body;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
describe('project icon routes', () => {
|
|
65
|
+
it('uses fallback file extension MIME when metadata points to a missing icon', async () => {
|
|
66
|
+
const { app, getRoute } = createRouteRegistry();
|
|
67
|
+
const jpgBytes = Buffer.from('jpg-bytes');
|
|
68
|
+
const enoent = Object.assign(new Error('missing'), { code: 'ENOENT' });
|
|
69
|
+
const fsPromises = {
|
|
70
|
+
readFile: vi.fn(async (iconPath) => {
|
|
71
|
+
if (iconPath.endsWith('.jpg')) {
|
|
72
|
+
return jpgBytes;
|
|
73
|
+
}
|
|
74
|
+
throw enoent;
|
|
75
|
+
}),
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
registerProjectIconRoutes(app, {
|
|
79
|
+
fsPromises,
|
|
80
|
+
path,
|
|
81
|
+
crypto,
|
|
82
|
+
vinciDataDir: '/tmp/vinci-test',
|
|
83
|
+
sanitizeProjects: (projects) => projects,
|
|
84
|
+
readSettingsFromDiskMigrated: async () => ({
|
|
85
|
+
projects: [{
|
|
86
|
+
id: 'proj-1',
|
|
87
|
+
path: '/repo',
|
|
88
|
+
iconImage: { mime: 'image/png', updatedAt: 1, source: 'custom' },
|
|
89
|
+
}],
|
|
90
|
+
}),
|
|
91
|
+
persistSettings: async () => ({}),
|
|
92
|
+
createFsSearchRuntime: () => ({ searchFilesystemFiles: async () => [] }),
|
|
93
|
+
spawn: vi.fn(),
|
|
94
|
+
resolveGitBinaryForSpawn: vi.fn(),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const res = createMockResponse();
|
|
98
|
+
await getRoute('GET', '/api/projects/:projectId/icon')({
|
|
99
|
+
params: { projectId: 'proj-1' },
|
|
100
|
+
query: {},
|
|
101
|
+
}, res);
|
|
102
|
+
|
|
103
|
+
expect(res.statusCode).toBe(200);
|
|
104
|
+
expect(res.getHeader('Content-Type')).toBe('image/jpeg');
|
|
105
|
+
expect(res.body).toBe(jpgBytes);
|
|
106
|
+
});
|
|
107
|
+
});
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CONFIG_FILE,
|
|
3
|
+
readConfigLayers,
|
|
4
|
+
isPlainObject,
|
|
5
|
+
getConfigForPath,
|
|
6
|
+
writeConfig,
|
|
7
|
+
} from './shared.js';
|
|
8
|
+
|
|
9
|
+
function getProviderSources(providerId, workingDirectory) {
|
|
10
|
+
const layers = readConfigLayers(workingDirectory);
|
|
11
|
+
const { userConfig, projectConfig, customConfig, paths } = layers;
|
|
12
|
+
|
|
13
|
+
const customProviders = isPlainObject(customConfig?.provider) ? customConfig.provider : {};
|
|
14
|
+
const customProvidersAlias = isPlainObject(customConfig?.providers) ? customConfig.providers : {};
|
|
15
|
+
const projectProviders = isPlainObject(projectConfig?.provider) ? projectConfig.provider : {};
|
|
16
|
+
const projectProvidersAlias = isPlainObject(projectConfig?.providers) ? projectConfig.providers : {};
|
|
17
|
+
const userProviders = isPlainObject(userConfig?.provider) ? userConfig.provider : {};
|
|
18
|
+
const userProvidersAlias = isPlainObject(userConfig?.providers) ? userConfig.providers : {};
|
|
19
|
+
|
|
20
|
+
const customExists =
|
|
21
|
+
Object.prototype.hasOwnProperty.call(customProviders, providerId) ||
|
|
22
|
+
Object.prototype.hasOwnProperty.call(customProvidersAlias, providerId);
|
|
23
|
+
const projectExists =
|
|
24
|
+
Object.prototype.hasOwnProperty.call(projectProviders, providerId) ||
|
|
25
|
+
Object.prototype.hasOwnProperty.call(projectProvidersAlias, providerId);
|
|
26
|
+
const userExists =
|
|
27
|
+
Object.prototype.hasOwnProperty.call(userProviders, providerId) ||
|
|
28
|
+
Object.prototype.hasOwnProperty.call(userProvidersAlias, providerId);
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
sources: {
|
|
32
|
+
auth: { exists: false },
|
|
33
|
+
user: { exists: userExists, path: paths.userPath },
|
|
34
|
+
project: { exists: projectExists, path: paths.projectPath || null },
|
|
35
|
+
custom: { exists: customExists, path: paths.customPath }
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function removeProviderConfig(providerId, workingDirectory, scope = 'user') {
|
|
41
|
+
if (!providerId || typeof providerId !== 'string') {
|
|
42
|
+
throw new Error('Provider ID is required');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const layers = readConfigLayers(workingDirectory);
|
|
46
|
+
let targetPath = layers.paths.userPath;
|
|
47
|
+
|
|
48
|
+
if (scope === 'project') {
|
|
49
|
+
if (!workingDirectory) {
|
|
50
|
+
throw new Error('Working directory is required for project scope');
|
|
51
|
+
}
|
|
52
|
+
targetPath = layers.paths.projectPath || targetPath;
|
|
53
|
+
} else if (scope === 'custom') {
|
|
54
|
+
if (!layers.paths.customPath) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
targetPath = layers.paths.customPath;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const targetConfig = getConfigForPath(layers, targetPath);
|
|
61
|
+
const providerConfig = isPlainObject(targetConfig.provider) ? targetConfig.provider : {};
|
|
62
|
+
const providersConfig = isPlainObject(targetConfig.providers) ? targetConfig.providers : {};
|
|
63
|
+
const removedProvider = Object.prototype.hasOwnProperty.call(providerConfig, providerId);
|
|
64
|
+
const removedProviders = Object.prototype.hasOwnProperty.call(providersConfig, providerId);
|
|
65
|
+
|
|
66
|
+
if (!removedProvider && !removedProviders) {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (removedProvider) {
|
|
71
|
+
delete providerConfig[providerId];
|
|
72
|
+
if (Object.keys(providerConfig).length === 0) {
|
|
73
|
+
delete targetConfig.provider;
|
|
74
|
+
} else {
|
|
75
|
+
targetConfig.provider = providerConfig;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (removedProviders) {
|
|
80
|
+
delete providersConfig[providerId];
|
|
81
|
+
if (Object.keys(providersConfig).length === 0) {
|
|
82
|
+
delete targetConfig.providers;
|
|
83
|
+
} else {
|
|
84
|
+
targetConfig.providers = providersConfig;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
writeConfig(targetConfig, targetPath || CONFIG_FILE);
|
|
89
|
+
console.log(`Removed provider ${providerId} from config: ${targetPath}`);
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export {
|
|
94
|
+
getProviderSources,
|
|
95
|
+
removeProviderConfig,
|
|
96
|
+
};
|