synabun 2.0.0 → 2.0.7
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/CHANGELOG.md +97 -0
- package/NOTICE +5 -0
- package/README.md +2 -1
- package/THIRD-PARTY-LICENSES.md +59 -0
- package/benchmarks/fixtures/browser/README.md +1 -1
- package/benchmarks/fixtures/desktop/README.md +1 -1
- package/docs/whatsapp.md +153 -18
- package/hooks/claude-code/pre-compact.mjs +11 -0
- package/hooks/claude-code/prompt-submit.mjs +60 -184
- package/hooks/claude-code/session-start.mjs +73 -16
- package/hooks/claude-code/shared.mjs +26 -3
- package/hooks/claude-code/stop.mjs +5 -5
- package/lib/child-path.js +303 -0
- package/lib/claude-hooks.js +2 -1
- package/lib/data-home-migration.js +2 -0
- package/lib/desktop-app.js +265 -0
- package/lib/external-tools.js +7 -3
- package/lib/log-redact.js +102 -0
- package/lib/packaged-runtime.js +138 -11
- package/lib/start-launcher.js +88 -1
- package/mcp-server/dist/http.d.ts +3 -1
- package/mcp-server/dist/http.d.ts.map +1 -1
- package/mcp-server/dist/http.js +15 -4
- package/mcp-server/dist/http.js.map +1 -1
- package/mcp-server/dist/index.d.ts.map +1 -1
- package/mcp-server/dist/index.js +9 -6
- package/mcp-server/dist/index.js.map +1 -1
- package/mcp-server/dist/services/budget-judgments.d.ts +108 -0
- package/mcp-server/dist/services/budget-judgments.d.ts.map +1 -0
- package/mcp-server/dist/services/budget-judgments.js +153 -0
- package/mcp-server/dist/services/budget-judgments.js.map +1 -0
- package/mcp-server/dist/services/codex-config-heal.d.ts +5 -1
- package/mcp-server/dist/services/codex-config-heal.d.ts.map +1 -1
- package/mcp-server/dist/services/codex-config-heal.js +41 -4
- package/mcp-server/dist/services/codex-config-heal.js.map +1 -1
- package/mcp-server/dist/services/identity.d.ts +11 -0
- package/mcp-server/dist/services/identity.d.ts.map +1 -1
- package/mcp-server/dist/services/identity.js +24 -0
- package/mcp-server/dist/services/identity.js.map +1 -1
- package/mcp-server/dist/services/neural-interface.d.ts +13 -0
- package/mcp-server/dist/services/neural-interface.d.ts.map +1 -1
- package/mcp-server/dist/services/neural-interface.js +34 -2
- package/mcp-server/dist/services/neural-interface.js.map +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts +1 -1
- package/mcp-server/dist/services/typesafe-config.d.ts.map +1 -1
- package/mcp-server/dist/services/typesafe-config.js +4 -1
- package/mcp-server/dist/services/typesafe-config.js.map +1 -1
- package/mcp-server/dist/tools/agents.d.ts +10 -7
- package/mcp-server/dist/tools/agents.d.ts.map +1 -1
- package/mcp-server/dist/tools/agents.js +34 -13
- package/mcp-server/dist/tools/agents.js.map +1 -1
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts +17 -0
- package/mcp-server/dist/tools/whatsapp-send-file.d.ts.map +1 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js +44 -0
- package/mcp-server/dist/tools/whatsapp-send-file.js.map +1 -0
- package/mcp-server/dist/tui.js +2 -1
- package/mcp-server/dist/tui.js.map +1 -1
- package/mcp-server/dist/version.d.ts +5 -0
- package/mcp-server/dist/version.d.ts.map +1 -0
- package/mcp-server/dist/version.js +21 -0
- package/mcp-server/dist/version.js.map +1 -0
- package/mcp-server/package-lock.json +2 -2
- package/mcp-server/package.json +1 -3
- package/mcp-server/run.mjs +15 -9
- package/mcp-server/src/http.ts +18 -3
- package/mcp-server/src/index.ts +9 -6
- package/mcp-server/src/services/budget-judgments.ts +183 -0
- package/mcp-server/src/services/codex-config-heal.ts +35 -4
- package/mcp-server/src/services/identity.ts +27 -0
- package/mcp-server/src/services/neural-interface.ts +34 -2
- package/mcp-server/src/services/typesafe-config.ts +4 -1
- package/mcp-server/src/tools/agents.ts +33 -14
- package/mcp-server/src/tools/whatsapp-send-file.ts +51 -0
- package/mcp-server/src/tui.ts +2 -1
- package/mcp-server/src/version.ts +22 -0
- package/neural-interface/i18n/en.json +2126 -209
- package/neural-interface/i18n/pt-BR.json +2123 -207
- package/neural-interface/lib/account-usage.js +163 -0
- package/neural-interface/lib/assistant-api.js +316 -57
- package/neural-interface/lib/assistant-brains/claude.js +19 -2
- package/neural-interface/lib/assistant-brains/codex.js +3 -0
- package/neural-interface/lib/assistant-brains/opencode.js +17 -4
- package/neural-interface/lib/assistant-budget-planner.js +390 -0
- package/neural-interface/lib/assistant-budget-split.js +408 -0
- package/neural-interface/lib/assistant-budget.js +94 -95
- package/neural-interface/lib/assistant-catalog.js +179 -27
- package/neural-interface/lib/assistant-config.js +91 -14
- package/neural-interface/lib/assistant-dispatch.js +494 -138
- package/neural-interface/lib/assistant-hidden-models.js +393 -66
- package/neural-interface/lib/assistant-jev.js +26 -3
- package/neural-interface/lib/assistant-memory.js +24 -10
- package/neural-interface/lib/assistant-persona.js +73 -12
- package/neural-interface/lib/assistant-plan-permissions.js +8 -1
- package/neural-interface/lib/assistant-pricing.js +21 -6
- package/neural-interface/lib/assistant-route-accounts.js +14 -0
- package/neural-interface/lib/assistant-route-gate.js +19 -3
- package/neural-interface/lib/assistant-router.js +373 -42
- package/neural-interface/lib/assistant-runtime.js +488 -59
- package/neural-interface/lib/assistant-task-prompt.js +13 -3
- package/neural-interface/lib/assistant-test-density.js +142 -0
- package/neural-interface/lib/assistant-user-context-budgets.js +123 -0
- package/neural-interface/lib/assistant-user-context.js +908 -0
- package/neural-interface/lib/assistant-user-knowledge.js +328 -0
- package/neural-interface/lib/assistant-user-learn.js +404 -0
- package/neural-interface/lib/assistant-user-threads.js +226 -0
- package/neural-interface/lib/assistant-user-writer.js +183 -0
- package/neural-interface/lib/augmented-path.js +317 -53
- package/neural-interface/lib/browser-background-tab.js +134 -0
- package/neural-interface/lib/browser-screenshot.js +678 -61
- package/neural-interface/lib/claude-accounts.js +342 -31
- package/neural-interface/lib/claude-agent-bridge.js +123 -9
- package/neural-interface/lib/claude-executable.js +6 -1
- package/neural-interface/lib/claude-model-catalog.js +33 -5
- package/neural-interface/lib/claude-panel-auth.js +221 -0
- package/neural-interface/lib/claude-profile-api-key.js +62 -0
- package/neural-interface/lib/claude-temporary.js +46 -7
- package/neural-interface/lib/cli-install-api.js +137 -0
- package/neural-interface/lib/cli-install.js +797 -0
- package/neural-interface/lib/cli-path.js +130 -0
- package/neural-interface/lib/cli-resolver.js +744 -0
- package/neural-interface/lib/cli-update-planner.js +143 -7
- package/neural-interface/lib/cloudflared/install.js +533 -0
- package/neural-interface/lib/cloudflared/manager.js +545 -0
- package/neural-interface/lib/cloudflared/pins.js +115 -0
- package/neural-interface/lib/cloudflared/pins.json +63 -0
- package/neural-interface/lib/cloudflared/tunnel.js +265 -0
- package/neural-interface/lib/codex-account-auth.js +178 -0
- package/neural-interface/lib/codex-accounts.js +489 -0
- package/neural-interface/lib/codex-capabilities.js +2 -1
- package/neural-interface/lib/codex-compat-rpc.js +8 -2
- package/neural-interface/lib/codex-mcp-approvals.js +5 -1
- package/neural-interface/lib/codex-panel-auth.js +62 -0
- package/neural-interface/lib/codex-session-settings.js +1 -1
- package/neural-interface/lib/codex-settings-schema.js +120 -0
- package/neural-interface/lib/desktop/config.js +3 -0
- package/neural-interface/lib/editor-completion-endpoint.js +264 -0
- package/neural-interface/lib/editor-completion-opencode.js +436 -0
- package/neural-interface/lib/editor-completion.js +714 -0
- package/neural-interface/lib/editor-config.js +149 -0
- package/neural-interface/lib/editor-files-api.js +268 -0
- package/neural-interface/lib/editor-git-api.js +134 -0
- package/neural-interface/lib/file-explorer-api.js +1039 -0
- package/neural-interface/lib/file-explorer-system.js +355 -0
- package/neural-interface/lib/file-explorer-trash.ps1 +85 -0
- package/neural-interface/lib/file-explorer-watch.js +239 -0
- package/neural-interface/lib/git-bash/bzip2.js +339 -0
- package/neural-interface/lib/git-bash/claude-settings.js +73 -0
- package/neural-interface/lib/git-bash/extract-worker.js +34 -0
- package/neural-interface/lib/git-bash/install.js +529 -0
- package/neural-interface/lib/git-bash/licenses/COPYING +360 -0
- package/neural-interface/lib/git-bash/licenses/LGPL-2.1 +511 -0
- package/neural-interface/lib/git-bash/manager.js +268 -0
- package/neural-interface/lib/git-bash/pins.js +206 -0
- package/neural-interface/lib/git-bash/pins.json +34 -0
- package/neural-interface/lib/git-bash/resolver.js +194 -0
- package/neural-interface/lib/git-diff-api.js +103 -0
- package/neural-interface/lib/git-read.js +234 -0
- package/neural-interface/lib/greeting/api.js +89 -0
- package/neural-interface/lib/greeting/budgets.js +90 -0
- package/neural-interface/lib/greeting/engine.js +461 -0
- package/neural-interface/lib/greeting/facts.js +320 -0
- package/neural-interface/lib/greeting/sources.js +252 -0
- package/neural-interface/lib/greeting/writer.js +139 -0
- package/neural-interface/lib/http-guards.js +189 -13
- package/neural-interface/lib/install-details.js +145 -0
- package/neural-interface/lib/machine-names.js +88 -0
- package/neural-interface/lib/mcp-installer.js +14 -2
- package/neural-interface/lib/memory-api.js +31 -7
- package/neural-interface/lib/native-loop-providers.js +26 -8
- package/neural-interface/lib/native-loop-runtime.js +9 -4
- package/neural-interface/lib/os-path.js +319 -0
- package/neural-interface/lib/plugin-installer.js +4 -2
- package/neural-interface/lib/project-storage.js +5 -7
- package/neural-interface/lib/provider-panel-api-key-auth.js +92 -0
- package/neural-interface/lib/provider-panel-terminal-auth.js +95 -0
- package/neural-interface/lib/pty-host/core.js +2 -1
- package/neural-interface/lib/pty-host/manager.js +27 -2
- package/neural-interface/lib/remote-policy.js +20 -11
- package/neural-interface/lib/request-gate.js +589 -0
- package/neural-interface/lib/session-title-generator.js +4 -3
- package/neural-interface/lib/system-about.js +340 -0
- package/neural-interface/lib/system-activity.js +66 -0
- package/neural-interface/lib/terminal-env.js +11 -3
- package/neural-interface/lib/terminal-path.js +55 -0
- package/neural-interface/lib/ui-blob-api.js +108 -0
- package/neural-interface/lib/ui-blob-store.js +68 -0
- package/neural-interface/lib/virtual-socket.js +12 -1
- package/neural-interface/lib/whatsapp/baileys-adapter.js +165 -45
- package/neural-interface/lib/whatsapp/bridge.js +78 -34
- package/neural-interface/lib/whatsapp/commands.js +13 -14
- package/neural-interface/lib/whatsapp/config.js +134 -20
- package/neural-interface/lib/whatsapp/fake-baileys.js +99 -14
- package/neural-interface/lib/whatsapp/host-core.js +168 -35
- package/neural-interface/lib/whatsapp/inbound.js +5 -3
- package/neural-interface/lib/whatsapp/local-file.js +524 -0
- package/neural-interface/lib/whatsapp/manager.js +92 -32
- package/neural-interface/lib/whatsapp/paths.js +11 -1
- package/neural-interface/lib/whatsapp/protocol.js +65 -5
- package/neural-interface/lib/whatsapp/redact.js +63 -0
- package/neural-interface/lib/whatsapp/service.js +343 -91
- package/neural-interface/lib/whatsapp/transcription.js +463 -0
- package/neural-interface/package-lock.json +701 -2
- package/neural-interface/package.json +13 -2
- package/neural-interface/public/index.html +2 -0
- package/neural-interface/public/index2d.html +2 -0
- package/neural-interface/public/install-app.html +9 -6
- package/neural-interface/public/offline.html +19 -3
- package/neural-interface/public/onboarding.html +107 -2
- package/neural-interface/public/shared/account-i18n.js +7 -0
- package/neural-interface/public/shared/account-menu.js +539 -0
- package/neural-interface/public/shared/account-usage.js +19 -0
- package/neural-interface/public/shared/account-view.js +335 -0
- package/neural-interface/public/shared/agent-runtime-options.js +50 -14
- package/neural-interface/public/shared/assistant/asst-accounts.js +182 -184
- package/neural-interface/public/shared/assistant/asst-api.js +32 -2
- package/neural-interface/public/shared/assistant/asst-automation.js +286 -0
- package/neural-interface/public/shared/assistant/asst-brain-picker.js +430 -81
- package/neural-interface/public/shared/assistant/asst-budget.js +259 -303
- package/neural-interface/public/shared/assistant/asst-composer.js +11 -1
- package/neural-interface/public/shared/assistant/asst-mascot.js +140 -61
- package/neural-interface/public/shared/assistant/asst-menu.js +27 -11
- package/neural-interface/public/shared/assistant/asst-panel.js +198 -42
- package/neural-interface/public/shared/assistant/asst-render.js +197 -23
- package/neural-interface/public/shared/assistant/asst-route-models.js +108 -0
- package/neural-interface/public/shared/assistant/asst-route.js +95 -343
- package/neural-interface/public/shared/assistant/asst-routes-editor.js +534 -0
- package/neural-interface/public/shared/assistant/asst-sanitize.js +8 -1
- package/neural-interface/public/shared/assistant/asst-sidepanel.js +31 -9
- package/neural-interface/public/shared/assistant/asst-slash.js +1 -1
- package/neural-interface/public/shared/assistant/asst-state.js +186 -2
- package/neural-interface/public/shared/assistant/asst-styles.js +247 -11
- package/neural-interface/public/shared/blob-store.js +111 -22
- package/neural-interface/public/shared/cdx/cdx-commands.js +4 -1
- package/neural-interface/public/shared/cdx/cdx-features.js +22 -14
- package/neural-interface/public/shared/cdx/cdx-icons.js +2 -0
- package/neural-interface/public/shared/cdx/cdx-panel.js +80 -118
- package/neural-interface/public/shared/cdx/cdx-render.js +25 -89
- package/neural-interface/public/shared/cdx/cdx-settings.js +231 -0
- package/neural-interface/public/shared/cdx/cdx-styles.js +164 -107
- package/neural-interface/public/shared/cdx/cdx-tabs.js +348 -668
- package/neural-interface/public/shared/cdx/cdx-ws.js +2 -0
- package/neural-interface/public/shared/claude-account-view.js +3 -0
- package/neural-interface/public/shared/cli-install-view.js +392 -0
- package/neural-interface/public/shared/cli-status.js +282 -22
- package/neural-interface/public/shared/constants.js +6 -1
- package/neural-interface/public/shared/cp/cp-events.js +2 -1
- package/neural-interface/public/shared/cp/cp-rehydrate.js +2 -1
- package/neural-interface/public/shared/cp/cp-styles.js +61 -0
- package/neural-interface/public/shared/desktop-bridge.js +39 -0
- package/neural-interface/public/shared/desktop-titlebar-page.js +59 -0
- package/neural-interface/public/shared/desktop-titlebar-view.js +122 -0
- package/neural-interface/public/shared/desktop-titlebar.js +153 -0
- package/neural-interface/public/shared/desktop-update-view.js +462 -0
- package/neural-interface/public/shared/editor/editor-ai-logic.js +409 -0
- package/neural-interface/public/shared/editor/editor-ai.js +619 -0
- package/neural-interface/public/shared/editor/editor-commands.js +212 -0
- package/neural-interface/public/shared/editor/editor-compare.js +103 -0
- package/neural-interface/public/shared/editor/editor-completion-list.js +172 -0
- package/neural-interface/public/shared/editor/editor-doc.js +257 -0
- package/neural-interface/public/shared/editor/editor-git-logic.js +131 -0
- package/neural-interface/public/shared/editor/editor-git.js +95 -0
- package/neural-interface/public/shared/editor/editor-languages.js +156 -0
- package/neural-interface/public/shared/editor/editor-lint.js +229 -0
- package/neural-interface/public/shared/editor/editor-loader.js +51 -0
- package/neural-interface/public/shared/editor/editor-markdown.js +90 -0
- package/neural-interface/public/shared/editor/editor-menu.js +233 -0
- package/neural-interface/public/shared/editor/editor-minimap.js +371 -0
- package/neural-interface/public/shared/editor/editor-palette-logic.js +192 -0
- package/neural-interface/public/shared/editor/editor-palette.js +125 -0
- package/neural-interface/public/shared/editor/editor-panel-logic.js +288 -0
- package/neural-interface/public/shared/editor/editor-panel.js +2099 -0
- package/neural-interface/public/shared/editor/editor-prefs.js +224 -0
- package/neural-interface/public/shared/editor/editor-safety.js +500 -0
- package/neural-interface/public/shared/editor/editor-search.js +352 -0
- package/neural-interface/public/shared/editor/editor-statusbar.js +144 -0
- package/neural-interface/public/shared/editor/editor-tabs-logic.js +331 -0
- package/neural-interface/public/shared/editor/editor-tabs.js +233 -0
- package/neural-interface/public/shared/editor/editor-theme.js +244 -0
- package/neural-interface/public/shared/editor/editor-view.js +332 -0
- package/neural-interface/public/shared/explorer/fx-api.js +216 -0
- package/neural-interface/public/shared/explorer/fx-links.js +204 -0
- package/neural-interface/public/shared/explorer/fx-logic.js +537 -0
- package/neural-interface/public/shared/explorer/fx-main.js +865 -0
- package/neural-interface/public/shared/explorer/fx-names.js +87 -0
- package/neural-interface/public/shared/explorer/fx-ops.js +370 -0
- package/neural-interface/public/shared/explorer/fx-popups.js +490 -0
- package/neural-interface/public/shared/explorer/fx-tree.js +790 -0
- package/neural-interface/public/shared/file-links.js +339 -0
- package/neural-interface/public/shared/git-bash-view.js +145 -0
- package/neural-interface/public/shared/greeting/greeting-card.js +178 -0
- package/neural-interface/public/shared/greeting/greeting-view.js +148 -0
- package/neural-interface/public/shared/html-shell.js +161 -165
- package/neural-interface/public/shared/i18n.js +19 -1
- package/neural-interface/public/shared/install-details-view.js +86 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-childpanel.js +4 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-greeting.js +47 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-panel.js +131 -17
- package/neural-interface/public/shared/ocp-v2/ocp-v2-render.js +7 -1
- package/neural-interface/public/shared/ocp-v2/ocp-v2-send.js +22 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-styles.js +158 -0
- package/neural-interface/public/shared/ocp-v2/ocp-v2-ws.js +3 -2
- package/neural-interface/public/shared/pwa-install-page.js +32 -2
- package/neural-interface/public/shared/pwa-install.css +3 -3
- package/neural-interface/public/shared/pwa-install.js +100 -1
- package/neural-interface/public/shared/settings/settings-about-pane.js +271 -0
- package/neural-interface/public/shared/settings/settings-about.js +254 -0
- package/neural-interface/public/shared/settings/settings-code-editor-pane.js +412 -0
- package/neural-interface/public/shared/settings/settings-code-editor.js +328 -0
- package/neural-interface/public/shared/settings/settings-greeting.js +133 -0
- package/neural-interface/public/shared/settings/settings-ia.js +33 -4
- package/neural-interface/public/shared/settings/settings-user-context.js +154 -0
- package/neural-interface/public/shared/showcase-privacy.js +489 -0
- package/neural-interface/public/shared/start-bridge.js +238 -19
- package/neural-interface/public/shared/storage.js +30 -3
- package/neural-interface/public/shared/styles.css +1710 -770
- package/neural-interface/public/shared/synabun-mascot.js +68 -58
- package/neural-interface/public/shared/tunnel-view.js +167 -0
- package/neural-interface/public/shared/ui-claude-panel.js +369 -229
- package/neural-interface/public/shared/ui-cli-banner.js +355 -0
- package/neural-interface/public/shared/ui-detail.js +15 -0
- package/neural-interface/public/shared/ui-explorer.js +22 -0
- package/neural-interface/public/shared/ui-file-explorer.js +481 -2496
- package/neural-interface/public/shared/ui-file-links.js +103 -0
- package/neural-interface/public/shared/ui-git-bash.js +124 -0
- package/neural-interface/public/shared/ui-help.js +26 -0
- package/neural-interface/public/shared/ui-invite.js +126 -116
- package/neural-interface/public/shared/ui-judgments.js +10 -1
- package/neural-interface/public/shared/ui-keybinds.js +4 -0
- package/neural-interface/public/shared/ui-loading.js +187 -19
- package/neural-interface/public/shared/ui-menubar.js +25 -33
- package/neural-interface/public/shared/ui-native-loop-router-state.js +11 -0
- package/neural-interface/public/shared/ui-native-loop-router.js +15 -2
- package/neural-interface/public/shared/ui-navbar.js +48 -22
- package/neural-interface/public/shared/ui-provider-auth.js +104 -0
- package/neural-interface/public/shared/ui-provider-terminal-auth.js +223 -0
- package/neural-interface/public/shared/ui-sessions.js +9 -3
- package/neural-interface/public/shared/ui-settings.js +509 -490
- package/neural-interface/public/shared/ui-sidepanel-runtime.js +2 -1
- package/neural-interface/public/shared/ui-sidepanel-sessions.js +3 -1
- package/neural-interface/public/shared/ui-sync.js +22 -0
- package/neural-interface/public/shared/ui-terminal.js +40 -5
- package/neural-interface/public/shared/ui-tool-updates.js +5 -0
- package/neural-interface/public/shared/ui-tunnel.js +130 -0
- package/neural-interface/public/shared/ui-tutorial-steps.js +6 -2
- package/neural-interface/public/shared/ui-update.js +594 -5
- package/neural-interface/public/shared/ui-whatsapp.js +21 -6
- package/neural-interface/public/shared/ui-whiteboard.js +12 -0
- package/neural-interface/public/shared/ui-worker-end.js +245 -0
- package/neural-interface/public/shared/ui-workspaces.js +41 -27
- package/neural-interface/public/shared/whatsapp/wa-view.js +51 -8
- package/neural-interface/public/shared/worker-end.js +253 -0
- package/neural-interface/public/sw.js +2 -2
- package/neural-interface/public/variant/2d/main.js +10 -1
- package/neural-interface/public/variant/3d/main.js +10 -1
- package/neural-interface/public/vendor/codemirror/chunks/apl-CSDKT72Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asciiarmor-JYYPU5BM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asn1-NRWCZDA6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/asterisk-5OU6D4ZQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/brainfuck-ID4N4SYC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-4RZWGCS2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-ACTJM3JR.js +11 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-S6BMAXLK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-TO2RDOHQ.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-US2GMZZQ.js +13 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-WYYO5ZAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-XALZTDJC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-YRNUTRDV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/chunk-Z3QOKH62.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clike-OWQBSOGL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/clojure-KV75UQWC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cmake-GTEP5Y3C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cobol-WCZ7QEGJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/coffeescript-TGLBOT6B.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/commonlisp-WVVI432V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/crystal-S6R6HLKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/css-PDA2RINP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/cypher-HL5QPL4C.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/d-HHJH2HAJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/diff-MN3A35BY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-25SQLBVS.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-27ZGGJJS.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-4E52OJIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-6FT73XDC.js +2 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-BE6YGRMW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CJSGKW2O.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CRPH2WLI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-CTG7JEQN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-I3FEVELX.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-IM5BBSWZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-KERUKTIF.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-LXTMWZAM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MABASSET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MJVBUZLL.js +3 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-MNB3WZVU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-QYWXOE3U.js +7 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-TEBOFWLZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-UNXFROM3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XL2SLQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-XO42Y3AP.js +6 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-Y2WWDWJK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-YFEZKWMU.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dist-ZY7SWQO6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dockerfile-3Y4R4NCH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dtd-5343GJ4P.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/dylan-FI2CXSW2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ebnf-GNQ77YWA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ecl-MRPUAE2L.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/eiffel-DZROES27.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/elm-4KICRFKQ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/erlang-3IHYC263.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/factor-C6D3E5UK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fcl-VYTWUJMI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/forth-ZBO2EH5M.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/fortran-5NDCRWIK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gas-SKRMOXFP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/gherkin-L45YXZLW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/groovy-GLPTLN44.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haskell-E7NF7P4Y.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/haxe-K2NVDOB6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/http-Q6DRQIJB.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/idl-MZYNJVLN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/javascript-6YRYK2HR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/julia-HXEL2T5G.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/livescript-MV7CI2JZ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/lua-E36NHOOD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mathematica-3XOZZCMC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mbox-ODYGFKS7.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mirc-WB43AWF5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mllike-ZT6ET7ZH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/modelica-LLMZPFGA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mscgen-2OPNNYFH.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/mumps-6P3HWJH6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nginx-TCGKQTRW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/nsis-ATIGIXTJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ntriples-5PLBI5OV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/octave-QF2S3NL6.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/oz-JFIVFFH2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pascal-JG6SW76V.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/perl-4B3IWVVO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pig-ABEBH4MI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/powershell-H32XE7TL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/properties-NN644MMA.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/protobuf-YKWQK2J3.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/pug-RIYXEKFW.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/puppet-JZQ5WPEV.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/python-EBCGGHAC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/q-5UMH2YQY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/r-7XMGG453.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/rpm-IEV7TSLR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ruby-T4SPFPNM.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sas-GEDX67ET.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/scheme-FTLOWKPC.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/shell-HUFRKFSJ.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sieve-P5QUDVYO.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/smalltalk-2SORJNQI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/solr-M22TGAR5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sparql-4M3KL752.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/spreadsheet-3K23LA4N.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/sql-VHRFMUBI.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stex-QZDAQUY2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/stylus-7ZDKAKBD.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/swift-WCLCLGMR.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tcl-JLMU6WOK.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/textile-4CHDKZ4T.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiddlywiki-DKMCZZQ4.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/tiki-F4JIG36Q.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/toml-BHMHTIM2.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/troff-YQ2KV5NT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-ZHRCVFJY.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/ttcn-cfg-KJOVTY4X.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/turtle-535P5N6I.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vb-76UMQHOT.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vbscript-CGNDTK6Z.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/velocity-VSJDO5T5.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/verilog-4T6ETXIP.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/vhdl-T7FNAXUN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/webidl-P6MJSJ6J.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/xquery-UOVILRGN.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/yacas-CV2APROL.js +1 -0
- package/neural-interface/public/vendor/codemirror/chunks/z80-3KFNVM4M.js +1 -0
- package/neural-interface/public/vendor/codemirror/codemirror-6.43.14-75f05e67.min.js +2 -0
- package/neural-interface/public/vendor/codemirror/manifest.json +60 -0
- package/neural-interface/scripts/build-vendor.mjs +78 -2
- package/neural-interface/scripts/vendor-codemirror-entry.js +121 -0
- package/neural-interface/server.js +2085 -1629
- package/neural-interface/templates/rulesets/README.md +1 -1
- package/package.json +5 -8
- package/setup.js +53 -15
- package/benchmarks/browser-live-results.json +0 -79
- package/benchmarks/browser-v2-results.json +0 -258
- package/neural-interface/lib/codex-capabilities.d.ts +0 -41
- package/neural-interface/public/category-logos/claude.svg +0 -1
- package/neural-interface/public/category-logos/gemini-color.svg +0 -1
- package/neural-interface/public/category-logos/openai.svg +0 -1
- package/neural-interface/public/category-logos/synabun.png +0 -0
- package/neural-interface/public/claude.svg +0 -1
- package/neural-interface/public/gallery.svg +0 -6
- package/neural-interface/public/leonardoai.svg +0 -1
- package/neural-interface/public/mcp.svg +0 -4
- package/neural-interface/public/opencode-logo-dark-square.svg +0 -1
- package/neural-interface/public/pin-svgrepo-com.svg +0 -25
- package/neural-interface/public/sessionicon.svg +0 -8
- package/neural-interface/public/toolbar-album.svg +0 -8
- package/neural-interface/public/toolbar-question.svg +0 -6
- package/neural-interface/public/toolbar-sessions.svg +0 -6
- package/neural-interface/scripts/codex-protocol-fixture.mjs +0 -83
- package/skills/synabun/synabunicon.png +0 -0
|
@@ -0,0 +1,797 @@
|
|
|
1
|
+
// ═══════════════════════════════════════════
|
|
2
|
+
// SynaBun — install a missing CLI from its vendor
|
|
3
|
+
// ═══════════════════════════════════════════
|
|
4
|
+
//
|
|
5
|
+
// What the side panels' Install button runs (POST /api/cli/install/:tool,
|
|
6
|
+
// lib/cli-install-api.js) when Claude Code, Codex or OpenCode is not installed.
|
|
7
|
+
// Each tool is installed the way its vendor documents for the system, from the
|
|
8
|
+
// vendor's own source, with nothing else needed (no Node, npm, Homebrew or
|
|
9
|
+
// Scoop). The sources are fixed here; a request only names the tool.
|
|
10
|
+
//
|
|
11
|
+
// Claude Code Anthropic's native installer: https://claude.ai/install.sh
|
|
12
|
+
// (bash) on macOS and Linux, https://claude.ai/install.ps1
|
|
13
|
+
// (powershell.exe -NoProfile -ExecutionPolicy Bypass -File) on
|
|
14
|
+
// Windows. Both check the binary they download against the
|
|
15
|
+
// SHA-256 of Anthropic's signed release manifest.
|
|
16
|
+
// Codex OpenAI's standalone installer (learn.chatgpt.com/docs/codex/cli),
|
|
17
|
+
// the install.sh / install.ps1 published with every
|
|
18
|
+
// openai/codex release: taken from the latest release and
|
|
19
|
+
// checked against the SHA-256 GitHub publishes for it, or, when
|
|
20
|
+
// GitHub's API does not answer, from https://chatgpt.com/codex/
|
|
21
|
+
// (the same file). The installer checks the package it fetches.
|
|
22
|
+
// OpenCode the install script https://opencode.ai/install (bash) on
|
|
23
|
+
// macOS and Linux. On Windows, where that script does not run:
|
|
24
|
+
// the newest OpenCode 1 release archive for the processor
|
|
25
|
+
// (opencode-windows-x64-baseline.zip, which runs on every x64
|
|
26
|
+
// CPU, or opencode-windows-arm64.zip) from the anomalyco/opencode
|
|
27
|
+
// GitHub releases, checked against its published SHA-256, its
|
|
28
|
+
// opencode.exe read out without an unzip program (the backups'
|
|
29
|
+
// streaming reader) and put in %USERPROFILE%\.opencode\bin, the
|
|
30
|
+
// folder the official installer uses (and the resolver looks in).
|
|
31
|
+
//
|
|
32
|
+
// Every download is HTTPS, every redirect too, and only to the vendor's own
|
|
33
|
+
// hosts. Nothing runs as administrator; everything lands in the user's own
|
|
34
|
+
// folders. Install consents to adding the resolved CLI's folder to persistent
|
|
35
|
+
// PATH when the vendor did not do so. That is a visible final step, using the
|
|
36
|
+
// same writer as Add to PATH; a failed PATH write leaves the CLI installed and
|
|
37
|
+
// reports a warning. Installers run without a terminal and without questions
|
|
38
|
+
// (CODEX_NON_INTERACTIVE=1, PowerShell -NonInteractive).
|
|
39
|
+
//
|
|
40
|
+
// One install per tool at a time. Every failure is a CliInstallError with a
|
|
41
|
+
// `code` the page has words for. Nothing here throws to a caller of start().
|
|
42
|
+
|
|
43
|
+
import { spawn } from 'node:child_process';
|
|
44
|
+
import { createHash, randomBytes } from 'node:crypto';
|
|
45
|
+
import { createWriteStream, existsSync, mkdirSync, rmSync, renameSync, unlinkSync } from 'node:fs';
|
|
46
|
+
import { mkdtemp, open } from 'node:fs/promises';
|
|
47
|
+
import os from 'node:os';
|
|
48
|
+
import path from 'node:path';
|
|
49
|
+
import { openZipArchive } from './backup-zip-reader.js';
|
|
50
|
+
import { buildInstallCommand } from './cli-update-planner.js';
|
|
51
|
+
import { packagedPathLast, rememberedPackagedPath, withPathEnv } from './augmented-path.js';
|
|
52
|
+
|
|
53
|
+
export const CLI_INSTALL_ERROR_CODES = Object.freeze([
|
|
54
|
+
'unsupported', // no official one-click method for this system
|
|
55
|
+
'busy', // an install of this tool is already running
|
|
56
|
+
'offline', // the vendor's server was not reached
|
|
57
|
+
'http', // it answered, but not with the file
|
|
58
|
+
'checksum', // what arrived is not the published file
|
|
59
|
+
'interrupted', // the transfer ended early
|
|
60
|
+
'timeout', // nothing arrived for too long, or the installer ran too long
|
|
61
|
+
'installer-failed', // the vendor's installer exited with an error
|
|
62
|
+
'not-found-after-install', // it finished, but no working CLI is found
|
|
63
|
+
'unpack', // the archive did not hold the program
|
|
64
|
+
'disk', // a file could not be written
|
|
65
|
+
'cancelled', // stopped (SynaBun shutting down)
|
|
66
|
+
'internal', // anything else
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
export class CliInstallError extends Error {
|
|
70
|
+
constructor(code, message, { cause, detail } = {}) {
|
|
71
|
+
super(message, cause ? { cause } : undefined);
|
|
72
|
+
this.name = 'CliInstallError';
|
|
73
|
+
this.code = CLI_INSTALL_ERROR_CODES.includes(code) ? code : 'internal';
|
|
74
|
+
if (detail !== undefined) this.detail = detail;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The vendor's install page for each tool (the banner's "Install guide"). */
|
|
79
|
+
export const CLI_INSTALL_GUIDES = Object.freeze({
|
|
80
|
+
'claude-code': 'https://code.claude.com/docs/en/setup',
|
|
81
|
+
codex: 'https://learn.chatgpt.com/docs/codex/cli',
|
|
82
|
+
opencode: 'https://opencode.ai/docs/',
|
|
83
|
+
gemini: 'https://github.com/google-gemini/gemini-cli',
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const GITHUB_DOWNLOAD_HOSTS = ['github.com', 'objects.githubusercontent.com', 'release-assets.githubusercontent.com', 'github-releases.githubusercontent.com'];
|
|
87
|
+
const MAX_SCRIPT_BYTES = 2 * 1024 * 1024;
|
|
88
|
+
const MAX_ARCHIVE_BYTES = 600 * 1024 * 1024;
|
|
89
|
+
const MAX_JSON_BYTES = 8 * 1024 * 1024;
|
|
90
|
+
const MAX_REDIRECTS = 6;
|
|
91
|
+
const LOG_KEEP = 200;
|
|
92
|
+
const SUPPORTED = { darwin: ['x64', 'arm64'], linux: ['x64', 'arm64'], win32: ['x64', 'arm64'] };
|
|
93
|
+
|
|
94
|
+
const hostIs = (...hosts) => (url) => hosts.includes(url.hostname);
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The one-click method for a tool on this system, or null when there is none
|
|
98
|
+
* (Gemini CLI, a 32-bit or other system). Pure: tests ask it for every OS.
|
|
99
|
+
*/
|
|
100
|
+
export function installPlanFor(toolKey, { platform = process.platform, arch = process.arch, env = process.env, home } = {}) {
|
|
101
|
+
if (!(SUPPORTED[platform] || []).includes(arch)) return null;
|
|
102
|
+
const windows = platform === 'win32';
|
|
103
|
+
const homeDir = home || (windows ? env.USERPROFILE : env.HOME) || os.homedir();
|
|
104
|
+
switch (toolKey) {
|
|
105
|
+
case 'claude-code':
|
|
106
|
+
return {
|
|
107
|
+
tool: toolKey,
|
|
108
|
+
kind: 'script',
|
|
109
|
+
interpreter: windows ? 'powershell' : 'bash',
|
|
110
|
+
url: windows ? 'https://claude.ai/install.ps1' : 'https://claude.ai/install.sh',
|
|
111
|
+
allow: hostIs('claude.ai', 'downloads.claude.ai'),
|
|
112
|
+
source: windows ? 'https://claude.ai/install.ps1' : 'https://claude.ai/install.sh',
|
|
113
|
+
label: 'Anthropic native installer',
|
|
114
|
+
env: {},
|
|
115
|
+
};
|
|
116
|
+
case 'codex':
|
|
117
|
+
return {
|
|
118
|
+
tool: toolKey,
|
|
119
|
+
kind: 'script',
|
|
120
|
+
interpreter: windows ? 'powershell' : 'sh',
|
|
121
|
+
url: windows ? 'https://chatgpt.com/codex/install.ps1' : 'https://chatgpt.com/codex/install.sh',
|
|
122
|
+
allow: hostIs('chatgpt.com', 'releases.openai.com'),
|
|
123
|
+
release: { repo: 'openai/codex', asset: windows ? 'install.ps1' : 'install.sh' },
|
|
124
|
+
source: windows ? 'https://chatgpt.com/codex/install.ps1' : 'https://chatgpt.com/codex/install.sh',
|
|
125
|
+
label: 'OpenAI standalone installer',
|
|
126
|
+
env: { CODEX_NON_INTERACTIVE: '1' },
|
|
127
|
+
};
|
|
128
|
+
case 'opencode':
|
|
129
|
+
if (windows) {
|
|
130
|
+
return {
|
|
131
|
+
tool: toolKey,
|
|
132
|
+
kind: 'archive',
|
|
133
|
+
interpreter: null,
|
|
134
|
+
repo: 'anomalyco/opencode',
|
|
135
|
+
tag: /^v1\.\d+\.\d+$/,
|
|
136
|
+
assets: arch === 'arm64' ? ['opencode-windows-arm64.zip'] : ['opencode-windows-x64-baseline.zip', 'opencode-windows-x64.zip'],
|
|
137
|
+
member: 'opencode.exe',
|
|
138
|
+
// The official installer's folder; $OPENCODE_INSTALL_DIR, which older installers honoured, when set.
|
|
139
|
+
dir: env.OPENCODE_INSTALL_DIR || path.win32.join(homeDir, '.opencode', 'bin'),
|
|
140
|
+
source: 'https://github.com/anomalyco/opencode/releases',
|
|
141
|
+
label: 'OpenCode release archive',
|
|
142
|
+
env: {},
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
tool: toolKey,
|
|
147
|
+
kind: 'script',
|
|
148
|
+
interpreter: 'bash',
|
|
149
|
+
url: 'https://opencode.ai/install',
|
|
150
|
+
allow: (url) => url.hostname === 'opencode.ai' || (url.hostname === 'raw.githubusercontent.com' && url.pathname.startsWith('/anomalyco/opencode/')),
|
|
151
|
+
source: 'https://opencode.ai/install',
|
|
152
|
+
label: 'OpenCode install script',
|
|
153
|
+
env: {},
|
|
154
|
+
};
|
|
155
|
+
default:
|
|
156
|
+
return null;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** What the status of a tool says about its one-click install. */
|
|
161
|
+
export function describeInstall(toolKey, options) {
|
|
162
|
+
const plan = installPlanFor(toolKey, options);
|
|
163
|
+
return plan
|
|
164
|
+
? { available: true, method: plan.kind, source: plan.source, interpreter: plan.interpreter, label: plan.label }
|
|
165
|
+
: { available: false, method: null, source: null, interpreter: null, label: null };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// ── Transfers ───────────────────────────────────────────────────────────────
|
|
169
|
+
|
|
170
|
+
function errorCode(error) {
|
|
171
|
+
return error?.code || error?.cause?.code || null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const DISK_CODES = new Set(['ENOSPC', 'EACCES', 'EPERM', 'EROFS', 'EDQUOT', 'EEXIST', 'EISDIR', 'ENOTDIR']);
|
|
175
|
+
|
|
176
|
+
function asInstallError(error, code, message) {
|
|
177
|
+
if (error instanceof CliInstallError) return error;
|
|
178
|
+
return new CliInstallError(code, message, { cause: error, detail: errorCode(error) || error?.message });
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function nextHop(response, from, allow) {
|
|
182
|
+
const location = response.headers.get('location');
|
|
183
|
+
let next = null;
|
|
184
|
+
try { next = new URL(location, from); } catch { next = null; }
|
|
185
|
+
if (!location || !next) throw new CliInstallError('http', 'The download server sent a redirect without an address.');
|
|
186
|
+
if (next.protocol !== 'https:') throw new CliInstallError('http', `The download server redirected to an address that is not HTTPS (${next.protocol}); it was not followed.`);
|
|
187
|
+
if (!allow(next)) throw new CliInstallError('http', `The download server redirected to ${next.hostname}, which is not one of the vendor's own; it was not followed.`);
|
|
188
|
+
return next;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
async function writeAll(handle, buffer) {
|
|
192
|
+
let at = 0;
|
|
193
|
+
while (at < buffer.length) at += (await handle.write(buffer, at, buffer.length - at)).bytesWritten;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Download `url` into `destination` (a new file), HTTPS only and only through
|
|
198
|
+
* hosts `allow` accepts. Checks the size and, when given, the SHA-256.
|
|
199
|
+
* Resolves { sha256, size, url } (the address the file came from).
|
|
200
|
+
*/
|
|
201
|
+
export async function downloadFile({
|
|
202
|
+
url,
|
|
203
|
+
allow,
|
|
204
|
+
destination,
|
|
205
|
+
maxBytes,
|
|
206
|
+
expectedSize = null,
|
|
207
|
+
sha256 = null,
|
|
208
|
+
fetchImpl = fetch,
|
|
209
|
+
signal,
|
|
210
|
+
onProgress = () => {},
|
|
211
|
+
stallMs = 60_000,
|
|
212
|
+
accept = 'application/octet-stream',
|
|
213
|
+
}) {
|
|
214
|
+
let current;
|
|
215
|
+
try { current = new URL(url); } catch { throw new CliInstallError('internal', `Not an address: ${url}`); }
|
|
216
|
+
if (current.protocol !== 'https:' || !allow(current)) throw new CliInstallError('internal', `${url} is not one of the vendor's HTTPS addresses.`);
|
|
217
|
+
const stop = new AbortController();
|
|
218
|
+
const onAbort = () => stop.abort(new CliInstallError('cancelled', 'The download was stopped.'));
|
|
219
|
+
if (signal?.aborted) onAbort(); else signal?.addEventListener('abort', onAbort, { once: true });
|
|
220
|
+
let timer = null;
|
|
221
|
+
const alive = () => {
|
|
222
|
+
clearTimeout(timer);
|
|
223
|
+
timer = setTimeout(() => stop.abort(new CliInstallError('timeout', `Nothing arrived from ${current.hostname} for ${Math.round(stallMs / 1000)} seconds.`)), stallMs);
|
|
224
|
+
timer.unref?.();
|
|
225
|
+
};
|
|
226
|
+
let handle = null;
|
|
227
|
+
let made = false;
|
|
228
|
+
let answered = false;
|
|
229
|
+
let received = 0;
|
|
230
|
+
const hash = createHash('sha256');
|
|
231
|
+
try {
|
|
232
|
+
alive();
|
|
233
|
+
let response = null;
|
|
234
|
+
for (let hop = 0; ; hop++) {
|
|
235
|
+
response = await fetchImpl(current, { redirect: 'manual', signal: stop.signal, headers: { accept, 'accept-encoding': 'identity', 'user-agent': 'synabun' } });
|
|
236
|
+
if (response.status < 300 || response.status >= 400) break;
|
|
237
|
+
const next = nextHop(response, current, allow);
|
|
238
|
+
try { await response.body?.cancel(); } catch { /* nothing to drain */ }
|
|
239
|
+
if (hop >= MAX_REDIRECTS) throw new CliInstallError('http', 'The download server redirected too many times.');
|
|
240
|
+
current = next;
|
|
241
|
+
alive();
|
|
242
|
+
}
|
|
243
|
+
if (response.status !== 200 || !response.body) {
|
|
244
|
+
try { await response.body?.cancel(); } catch { /* nothing to drain */ }
|
|
245
|
+
throw new CliInstallError('http', `${current.hostname} answered ${response.status} instead of the file.`, { detail: String(response.status) });
|
|
246
|
+
}
|
|
247
|
+
const announced = Number(response.headers.get('content-length'));
|
|
248
|
+
if (Number.isFinite(announced) && announced > 0 && ((expectedSize && announced !== expectedSize) || announced > maxBytes)) {
|
|
249
|
+
try { await response.body.cancel(); } catch { /* nothing to drain */ }
|
|
250
|
+
throw new CliInstallError('checksum', `${current.hostname} offered ${announced} bytes${expectedSize ? `; the published file has ${expectedSize}` : ''}. It was not downloaded.`);
|
|
251
|
+
}
|
|
252
|
+
answered = true;
|
|
253
|
+
handle = await open(destination, 'wx', 0o600);
|
|
254
|
+
made = true;
|
|
255
|
+
const total = expectedSize || (Number.isFinite(announced) && announced > 0 ? announced : 0);
|
|
256
|
+
onProgress({ received: 0, total });
|
|
257
|
+
// Read through a reader of our own, cancelled on a stall or a stop: the transfer ends then whatever the
|
|
258
|
+
// transport does with the signal.
|
|
259
|
+
const reader = response.body.getReader();
|
|
260
|
+
const onStop = () => { reader.cancel(stop.signal.reason).catch(() => {}); };
|
|
261
|
+
stop.signal.addEventListener('abort', onStop, { once: true });
|
|
262
|
+
try {
|
|
263
|
+
for (;;) {
|
|
264
|
+
const { done, value } = await reader.read();
|
|
265
|
+
if (stop.signal.aborted) throw stop.signal.reason;
|
|
266
|
+
if (done) break;
|
|
267
|
+
const chunk = Buffer.from(value.buffer, value.byteOffset, value.byteLength);
|
|
268
|
+
alive();
|
|
269
|
+
received += chunk.length;
|
|
270
|
+
if (received > (expectedSize || maxBytes)) throw new CliInstallError('checksum', 'The download is larger than the published file. It was stopped and removed.');
|
|
271
|
+
hash.update(chunk);
|
|
272
|
+
await writeAll(handle, chunk);
|
|
273
|
+
onProgress({ received, total });
|
|
274
|
+
}
|
|
275
|
+
} finally {
|
|
276
|
+
stop.signal.removeEventListener('abort', onStop);
|
|
277
|
+
}
|
|
278
|
+
if (expectedSize && received !== expectedSize) throw new CliInstallError('interrupted', `The download ended after ${received} of ${expectedSize} bytes.`);
|
|
279
|
+
const digest = hash.digest('hex');
|
|
280
|
+
if (sha256 && digest !== String(sha256).toLowerCase()) {
|
|
281
|
+
throw new CliInstallError('checksum', 'The download does not match the SHA-256 its vendor published. It was removed and never run.', { detail: digest });
|
|
282
|
+
}
|
|
283
|
+
await handle.sync();
|
|
284
|
+
await handle.close();
|
|
285
|
+
handle = null;
|
|
286
|
+
return { sha256: digest, size: received, url: current.href };
|
|
287
|
+
} catch (error) {
|
|
288
|
+
try { await handle?.close(); } catch { /* closed */ }
|
|
289
|
+
handle = null;
|
|
290
|
+
if (made) rmSync(destination, { force: true });
|
|
291
|
+
if (stop.signal.aborted && stop.signal.reason instanceof CliInstallError) throw stop.signal.reason;
|
|
292
|
+
if (error instanceof CliInstallError) throw error;
|
|
293
|
+
if (DISK_CODES.has(errorCode(error))) throw asInstallError(error, 'disk', `The download could not be saved (${errorCode(error)}).`);
|
|
294
|
+
if (answered) throw new CliInstallError('interrupted', `The download from ${current.hostname} ended after ${received} bytes.`, { cause: error, detail: errorCode(error) || error?.message });
|
|
295
|
+
throw asInstallError(error, 'offline', `${current.hostname} could not be reached.`);
|
|
296
|
+
} finally {
|
|
297
|
+
clearTimeout(timer);
|
|
298
|
+
signal?.removeEventListener('abort', onAbort);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** GET a GitHub API document (api.github.com only, followed redirects stay there). */
|
|
303
|
+
export async function fetchGithubJson(url, { fetchImpl = fetch, signal, timeoutMs = 20_000 } = {}) {
|
|
304
|
+
const stop = new AbortController();
|
|
305
|
+
const timer = setTimeout(() => stop.abort(), timeoutMs);
|
|
306
|
+
timer.unref?.();
|
|
307
|
+
const onAbort = () => stop.abort();
|
|
308
|
+
signal?.addEventListener('abort', onAbort, { once: true });
|
|
309
|
+
try {
|
|
310
|
+
const response = await fetchImpl(url, { signal: stop.signal, headers: { accept: 'application/vnd.github+json', 'x-github-api-version': '2022-11-28', 'user-agent': 'synabun' } });
|
|
311
|
+
const final = new URL(response.url || url);
|
|
312
|
+
if (final.protocol !== 'https:' || final.hostname !== 'api.github.com') throw new CliInstallError('http', 'GitHub\'s API answered from another address.');
|
|
313
|
+
if (!response.ok) throw new CliInstallError('http', `GitHub's API answered ${response.status}${response.status === 403 || response.status === 429 ? ' (rate limited)' : ''}.`, { detail: String(response.status) });
|
|
314
|
+
const text = await response.text();
|
|
315
|
+
if (text.length > MAX_JSON_BYTES) throw new CliInstallError('http', 'GitHub\'s API answered with too much.');
|
|
316
|
+
return JSON.parse(text);
|
|
317
|
+
} catch (error) {
|
|
318
|
+
if (error instanceof CliInstallError) throw error;
|
|
319
|
+
if (signal?.aborted) throw new CliInstallError('cancelled', 'Stopped.');
|
|
320
|
+
if (error instanceof SyntaxError) throw new CliInstallError('http', 'GitHub\'s API answered with something that is not JSON.');
|
|
321
|
+
throw asInstallError(error, 'offline', 'GitHub\'s API could not be reached.');
|
|
322
|
+
} finally {
|
|
323
|
+
clearTimeout(timer);
|
|
324
|
+
signal?.removeEventListener('abort', onAbort);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** "sha256:abc…" (GitHub's asset digest) → "abc…", or null. */
|
|
329
|
+
export function sha256OfDigest(digest) {
|
|
330
|
+
const match = String(digest || '').match(/^sha256:([0-9a-f]{64})$/i);
|
|
331
|
+
return match ? match[1].toLowerCase() : null;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/** The named asset of a release, when it is downloaded from that repository's own releases. */
|
|
335
|
+
export function releaseAsset(release, name, repo) {
|
|
336
|
+
const asset = (release?.assets || []).find(item => item?.name === name);
|
|
337
|
+
if (!asset) return null;
|
|
338
|
+
const url = String(asset.browser_download_url || '');
|
|
339
|
+
if (!url.startsWith(`https://github.com/${repo}/releases/download/`)) return null;
|
|
340
|
+
return { name, url, size: Number(asset.size) || null, sha256: sha256OfDigest(asset.digest) };
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* The newest OpenCode 1 release (not a draft, not a prerelease; OpenCode 2 is
|
|
345
|
+
* a different program SynaBun's panel does not drive) that carries one of
|
|
346
|
+
* `assets`, the first listed preferred.
|
|
347
|
+
*/
|
|
348
|
+
export function pickOpencodeRelease(releases, { assets, repo = 'anomalyco/opencode', tag = /^v1\.\d+\.\d+$/ } = {}) {
|
|
349
|
+
for (const release of Array.isArray(releases) ? releases : []) {
|
|
350
|
+
if (!release || release.draft || release.prerelease || !tag.test(String(release.tag_name || ''))) continue;
|
|
351
|
+
for (const name of assets || []) {
|
|
352
|
+
const asset = releaseAsset(release, name, repo);
|
|
353
|
+
if (asset) return { tag: release.tag_name, version: String(release.tag_name).replace(/^v/, ''), asset };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// ── Zip ─────────────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
const unpack = (message) => new CliInstallError('unpack', message);
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Read one member out of a zip archive into `destination` (a new file),
|
|
365
|
+
* without an unzip program and without using a name of the archive as a path:
|
|
366
|
+
* only the entry called exactly `member` is read, through the streaming
|
|
367
|
+
* reader of backups (backup-zip-reader.js: ZIP64, stored or deflated, checked
|
|
368
|
+
* against its size and CRC-32).
|
|
369
|
+
*/
|
|
370
|
+
export async function extractZipMember({ archive, member, destination, maxSize = 1024 ** 3, mode = 0o755 }) {
|
|
371
|
+
let zip;
|
|
372
|
+
try { zip = await openZipArchive(archive); } catch (error) { throw unpack(`The download is not a readable zip archive (${error?.message || error}).`); }
|
|
373
|
+
let opened = false;
|
|
374
|
+
try {
|
|
375
|
+
const entry = zip.getEntry(member);
|
|
376
|
+
if (!entry) throw unpack(`The archive holds no ${member}.`);
|
|
377
|
+
if (entry.uncompressedSize > maxSize) throw unpack(`The archive's ${member} is larger than expected.`);
|
|
378
|
+
const out = createWriteStream(destination, { flags: 'wx', mode });
|
|
379
|
+
out.on('open', () => { opened = true; });
|
|
380
|
+
await zip.pipeEntry(entry, out);
|
|
381
|
+
return { size: entry.uncompressedSize };
|
|
382
|
+
} catch (error) {
|
|
383
|
+
if (opened) rmSync(destination, { force: true });
|
|
384
|
+
if (error instanceof CliInstallError) throw error;
|
|
385
|
+
if (DISK_CODES.has(errorCode(error))) throw asInstallError(error, 'disk', `The program could not be written (${errorCode(error)}).`);
|
|
386
|
+
throw unpack(`The archive's ${member} could not be read (${error?.message || error}).`);
|
|
387
|
+
} finally {
|
|
388
|
+
try { await zip.close(); } catch { /* closed */ }
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ── Output ──────────────────────────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
const ANSI = /\x1b\[[0-9;?]*[ -\/]*[@-~]|\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)|\x1b[()][A-Za-z0-9]|\x1b[=>78DEHMNOZc]/g;
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Turns an installer's output into lines: escapes stripped, a carriage return
|
|
398
|
+
* (a progress bar redrawing itself) keeping only what came after it, empty
|
|
399
|
+
* lines dropped, each line at most 500 characters.
|
|
400
|
+
*/
|
|
401
|
+
export function createLineSplitter(onLine) {
|
|
402
|
+
let pending = '';
|
|
403
|
+
const emit = (raw) => {
|
|
404
|
+
// A line's own CR (Windows' CRLF) ends it; a CR inside it is a redraw.
|
|
405
|
+
const segments = raw.replace(/\r+$/, '').split('\r');
|
|
406
|
+
const text = segments[segments.length - 1].replace(ANSI, '').replace(/[\x00-\x08\x0b-\x1f\x7f]/g, '').trimEnd();
|
|
407
|
+
if (text.trim()) onLine(text.slice(0, 500));
|
|
408
|
+
};
|
|
409
|
+
return {
|
|
410
|
+
push(chunk) {
|
|
411
|
+
pending += String(chunk);
|
|
412
|
+
let index;
|
|
413
|
+
while ((index = pending.indexOf('\n')) >= 0) {
|
|
414
|
+
emit(pending.slice(0, index));
|
|
415
|
+
pending = pending.slice(index + 1);
|
|
416
|
+
}
|
|
417
|
+
if (pending.length > 16_384) { emit(pending); pending = ''; }
|
|
418
|
+
},
|
|
419
|
+
end() {
|
|
420
|
+
if (pending) emit(pending);
|
|
421
|
+
pending = '';
|
|
422
|
+
},
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function sameDir(a, b, platform) {
|
|
427
|
+
const clean = (value) => {
|
|
428
|
+
const text = String(value || '').replace(/[\\/]+$/, '');
|
|
429
|
+
return platform === 'win32' ? text.toLowerCase().replace(/\//g, '\\') : text;
|
|
430
|
+
};
|
|
431
|
+
return clean(a) === clean(b);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/** Whether `dir` is on a PATH list. */
|
|
435
|
+
export function onPathList(dir, entries, platform = process.platform) {
|
|
436
|
+
return (entries || []).some(entry => sameDir(entry, dir, platform));
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// ── The installer ───────────────────────────────────────────────────────────
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* @param {object} deps
|
|
443
|
+
* @param {{ resolve: Function }} deps.resolver cli-resolver.js: the final check (force)
|
|
444
|
+
* @param {() => Promise<string[]>} [deps.readOsPath] the OS PATH a new terminal gets, read afresh
|
|
445
|
+
* @param {(tool: string, ok: boolean, status: object|null) => void} [deps.onFinished]
|
|
446
|
+
*/
|
|
447
|
+
export function createCliInstaller({
|
|
448
|
+
resolver,
|
|
449
|
+
platform = process.platform,
|
|
450
|
+
arch = process.arch,
|
|
451
|
+
env = process.env,
|
|
452
|
+
home = (platform === 'win32' ? env.USERPROFILE : env.HOME) || os.homedir(),
|
|
453
|
+
tmpRoot = os.tmpdir(),
|
|
454
|
+
fetchImpl = fetch,
|
|
455
|
+
spawnImpl = spawn,
|
|
456
|
+
readOsPath = async () => [],
|
|
457
|
+
addToPath = null,
|
|
458
|
+
installerPath = null,
|
|
459
|
+
onFinished = () => {},
|
|
460
|
+
log = () => {},
|
|
461
|
+
timeoutMs = 15 * 60_000,
|
|
462
|
+
stallMs = 60_000,
|
|
463
|
+
} = {}) {
|
|
464
|
+
if (!resolver?.resolve) throw new Error('createCliInstaller needs the CLI resolver');
|
|
465
|
+
const jobs = new Map();
|
|
466
|
+
const windows = platform === 'win32';
|
|
467
|
+
const planFor = (toolKey) => installPlanFor(toolKey, { platform, arch, env, home });
|
|
468
|
+
const guideOf = (toolKey) => CLI_INSTALL_GUIDES[toolKey] || null;
|
|
469
|
+
const manualOf = (toolKey) => buildInstallCommand(toolKey, 'unknown', { platform });
|
|
470
|
+
|
|
471
|
+
function publicJob(job) {
|
|
472
|
+
return {
|
|
473
|
+
tool: job.tool,
|
|
474
|
+
state: job.state,
|
|
475
|
+
startedAt: job.startedAt,
|
|
476
|
+
endedAt: job.endedAt,
|
|
477
|
+
step: job.step,
|
|
478
|
+
code: job.code,
|
|
479
|
+
error: job.error,
|
|
480
|
+
method: job.plan.kind,
|
|
481
|
+
source: job.source || job.plan.source,
|
|
482
|
+
log: job.log.slice(-40),
|
|
483
|
+
path: job.path || null,
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function emit(job, event) {
|
|
488
|
+
if (event.type === 'done' && event.path) job.path = event.path;
|
|
489
|
+
if (event.type === 'log') {
|
|
490
|
+
job.log.push(event.line);
|
|
491
|
+
if (job.log.length > LOG_KEEP) job.log.splice(0, job.log.length - LOG_KEEP);
|
|
492
|
+
}
|
|
493
|
+
if (event.type === 'step') job.step = event.step;
|
|
494
|
+
for (const listener of [...job.listeners]) {
|
|
495
|
+
try { listener(event); } catch { /* a closed stream */ }
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
function stepper(job) {
|
|
500
|
+
return {
|
|
501
|
+
step: (step) => emit(job, { type: 'step', step }),
|
|
502
|
+
line: (line) => emit(job, { type: 'log', line }),
|
|
503
|
+
progress: (() => {
|
|
504
|
+
let last = 0;
|
|
505
|
+
return ({ received, total }) => {
|
|
506
|
+
const now = Date.now();
|
|
507
|
+
if (received !== total && now - last < 250) return;
|
|
508
|
+
last = now;
|
|
509
|
+
emit(job, { type: 'progress', received, total });
|
|
510
|
+
};
|
|
511
|
+
})(),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function interpreterFor(plan) {
|
|
516
|
+
if (plan.interpreter === 'powershell') {
|
|
517
|
+
const root = env.SystemRoot || env.SYSTEMROOT || 'C:\\Windows';
|
|
518
|
+
return { file: path.win32.join(root, 'System32', 'WindowsPowerShell', 'v1.0', 'powershell.exe'), args: (script) => ['-NoProfile', '-NonInteractive', '-ExecutionPolicy', 'Bypass', '-File', script] };
|
|
519
|
+
}
|
|
520
|
+
const candidates = plan.interpreter === 'sh' ? ['/bin/sh', '/usr/bin/sh'] : ['/bin/bash', '/usr/bin/bash', '/usr/local/bin/bash', '/opt/homebrew/bin/bash'];
|
|
521
|
+
const file = candidates.find(candidate => existsSync(candidate));
|
|
522
|
+
if (!file) throw new CliInstallError('unsupported', `${plan.interpreter} is not installed on this computer; the ${plan.label} needs it.`);
|
|
523
|
+
return { file, args: (script) => [script] };
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
/** PATH for an installer: the one a new terminal has (so its own "is it on PATH?" checks see the truth), plus the system's folders. */
|
|
527
|
+
function installerPathValue(osEntries) {
|
|
528
|
+
if (installerPath) return installerPath;
|
|
529
|
+
const delimiter = windows ? ';' : ':';
|
|
530
|
+
const base = osEntries.length ? osEntries : String(env.PATH || env.Path || '').split(delimiter);
|
|
531
|
+
const system = windows
|
|
532
|
+
? [path.win32.join(env.SystemRoot || 'C:\\Windows', 'System32'), path.win32.join(env.SystemRoot || 'C:\\Windows', 'System32', 'WindowsPowerShell', 'v1.0'), env.SystemRoot || 'C:\\Windows']
|
|
533
|
+
: ['/usr/bin', '/bin', '/usr/sbin', '/sbin'];
|
|
534
|
+
const seen = new Set();
|
|
535
|
+
const value = [...base, ...system].filter((entry) => {
|
|
536
|
+
const key = windows ? String(entry).toLowerCase() : String(entry);
|
|
537
|
+
if (!entry || seen.has(key)) return false;
|
|
538
|
+
seen.add(key);
|
|
539
|
+
return true;
|
|
540
|
+
}).join(delimiter);
|
|
541
|
+
// A packaged application's own Node runtime and npm, after everything of the user's: an installer that looks for
|
|
542
|
+
// node or npm finds the user's own first, and these on a machine that has none (lib/augmented-path.js).
|
|
543
|
+
const packaged = rememberedPackagedPath();
|
|
544
|
+
return packaged.last.length || packaged.extra.length ? packagedPathLast(value, { platform, ...packaged }) : value;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function fetchScript(job, plan, workDir, { step, line, progress }) {
|
|
548
|
+
const file = path.join(workDir, windows ? 'install.ps1' : 'install.sh');
|
|
549
|
+
step('download');
|
|
550
|
+
let source = plan.url;
|
|
551
|
+
let expected = null;
|
|
552
|
+
let allow = plan.allow;
|
|
553
|
+
if (plan.release) {
|
|
554
|
+
try {
|
|
555
|
+
const release = await fetchGithubJson(`https://api.github.com/repos/${plan.release.repo}/releases/latest`, { fetchImpl, signal: job.abort.signal });
|
|
556
|
+
const asset = releaseAsset(release, plan.release.asset, plan.release.repo);
|
|
557
|
+
if (asset?.sha256) {
|
|
558
|
+
source = asset.url;
|
|
559
|
+
expected = asset.sha256;
|
|
560
|
+
allow = (url) => GITHUB_DOWNLOAD_HOSTS.includes(url.hostname);
|
|
561
|
+
line(`${plan.label}: ${plan.release.asset} of ${plan.release.repo} ${release.tag_name || ''}`.trim());
|
|
562
|
+
}
|
|
563
|
+
} catch (error) {
|
|
564
|
+
if (error?.code === 'cancelled') throw error;
|
|
565
|
+
line(`GitHub's release list did not answer (${error?.message || error}); downloading ${plan.url} instead.`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
job.source = source;
|
|
569
|
+
const got = await downloadFile({ url: source, allow, destination: file, maxBytes: MAX_SCRIPT_BYTES, sha256: expected, fetchImpl, signal: job.abort.signal, onProgress: progress, stallMs, accept: '*/*' });
|
|
570
|
+
step('verify');
|
|
571
|
+
if (expected) line(`SHA-256 matches the published one (${got.sha256.slice(0, 12)}…).`);
|
|
572
|
+
const handle = await open(file, 'r');
|
|
573
|
+
const head = Buffer.alloc(64);
|
|
574
|
+
try { await handle.read(head, 0, 64, 0); } finally { await handle.close(); }
|
|
575
|
+
const start = head.toString('utf8').replace(/^\uFEFF/, '').trimStart();
|
|
576
|
+
// A region the vendor does not serve, a portal or a proxy answers with a page: never run that.
|
|
577
|
+
if (start.startsWith('<') || (!windows && !start.startsWith('#!'))) {
|
|
578
|
+
throw new CliInstallError('http', `${new URL(got.url).hostname} answered with a web page, not the installer (the download service may not be available in this region).`);
|
|
579
|
+
}
|
|
580
|
+
return file;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function runScript(job, plan, scriptFile, workDir, osEntries, { line }) {
|
|
584
|
+
const interpreter = interpreterFor(plan);
|
|
585
|
+
const childEnv = { ...withPathEnv({ ...env, ...plan.env }, installerPathValue(osEntries), platform), TERM: 'dumb', NO_COLOR: '1' };
|
|
586
|
+
delete childEnv.FORCE_COLOR;
|
|
587
|
+
return new Promise((resolve, reject) => {
|
|
588
|
+
let child;
|
|
589
|
+
try {
|
|
590
|
+
child = spawnImpl(interpreter.file, interpreter.args(scriptFile), {
|
|
591
|
+
cwd: workDir,
|
|
592
|
+
env: childEnv,
|
|
593
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
594
|
+
windowsHide: true,
|
|
595
|
+
// POSIX: a session of its own, without a terminal, so nothing can ask on /dev/tty, and the
|
|
596
|
+
// whole group can be stopped.
|
|
597
|
+
detached: !windows,
|
|
598
|
+
});
|
|
599
|
+
} catch (error) {
|
|
600
|
+
reject(asInstallError(error, 'installer-failed', `${interpreter.file} could not be started.`));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
job.child = child;
|
|
604
|
+
const out = createLineSplitter(line);
|
|
605
|
+
const err = createLineSplitter(line);
|
|
606
|
+
child.stdout?.on('data', chunk => out.push(chunk.toString('utf8')));
|
|
607
|
+
child.stderr?.on('data', chunk => err.push(chunk.toString('utf8')));
|
|
608
|
+
let settled = false;
|
|
609
|
+
const timer = setTimeout(() => {
|
|
610
|
+
stopChild(child, windows, spawnImpl);
|
|
611
|
+
finish(new CliInstallError('timeout', `The installer was still running after ${Math.round(timeoutMs / 60_000)} minutes and was stopped.`));
|
|
612
|
+
}, timeoutMs);
|
|
613
|
+
timer.unref?.();
|
|
614
|
+
const onAbort = () => { stopChild(child, windows, spawnImpl); finish(new CliInstallError('cancelled', 'The install was stopped.')); };
|
|
615
|
+
job.abort.signal.addEventListener('abort', onAbort, { once: true });
|
|
616
|
+
function finish(error, code) {
|
|
617
|
+
if (settled) return;
|
|
618
|
+
settled = true;
|
|
619
|
+
clearTimeout(timer);
|
|
620
|
+
job.abort.signal.removeEventListener('abort', onAbort);
|
|
621
|
+
out.end();
|
|
622
|
+
err.end();
|
|
623
|
+
job.child = null;
|
|
624
|
+
if (error) reject(error);
|
|
625
|
+
else if (code !== 0) reject(new CliInstallError('installer-failed', `The ${plan.label} stopped with exit code ${code}.`, { detail: code }));
|
|
626
|
+
else resolve();
|
|
627
|
+
}
|
|
628
|
+
child.on('error', error => finish(asInstallError(error, 'installer-failed', `${interpreter.file} could not be started (${errorCode(error) || error?.message}).`)));
|
|
629
|
+
child.on('close', (code, signal) => finish(null, code ?? (signal ? 128 : 1)));
|
|
630
|
+
});
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
async function installArchive(job, plan, workDir, { step, line, progress }) {
|
|
634
|
+
step('download');
|
|
635
|
+
const releases = await fetchGithubJson(`https://api.github.com/repos/${plan.repo}/releases?per_page=20`, { fetchImpl, signal: job.abort.signal });
|
|
636
|
+
const picked = pickOpencodeRelease(releases, { assets: plan.assets, repo: plan.repo, tag: plan.tag });
|
|
637
|
+
if (!picked) throw new CliInstallError('http', `No OpenCode 1 release of ${plan.repo} carries ${plan.assets.join(' or ')}.`);
|
|
638
|
+
job.source = picked.asset.url;
|
|
639
|
+
line(`OpenCode ${picked.version}: ${picked.asset.name}`);
|
|
640
|
+
const archive = path.join(workDir, picked.asset.name);
|
|
641
|
+
const got = await downloadFile({
|
|
642
|
+
url: picked.asset.url,
|
|
643
|
+
allow: (url) => GITHUB_DOWNLOAD_HOSTS.includes(url.hostname),
|
|
644
|
+
destination: archive,
|
|
645
|
+
maxBytes: MAX_ARCHIVE_BYTES,
|
|
646
|
+
expectedSize: picked.asset.size,
|
|
647
|
+
sha256: picked.asset.sha256,
|
|
648
|
+
fetchImpl,
|
|
649
|
+
signal: job.abort.signal,
|
|
650
|
+
onProgress: progress,
|
|
651
|
+
stallMs,
|
|
652
|
+
});
|
|
653
|
+
step('verify');
|
|
654
|
+
line(picked.asset.sha256 ? `SHA-256 matches the published one (${got.sha256.slice(0, 12)}…).` : 'GitHub publishes no SHA-256 for this file; its size matches.');
|
|
655
|
+
step('extract');
|
|
656
|
+
try { mkdirSync(plan.dir, { recursive: true }); } catch (error) { throw asInstallError(error, 'disk', `${plan.dir} could not be created (${errorCode(error)}).`); }
|
|
657
|
+
const part = path.join(plan.dir, `.${plan.member}.${randomBytes(4).toString('hex')}.part`);
|
|
658
|
+
await extractZipMember({ archive, member: plan.member, destination: part });
|
|
659
|
+
step('place');
|
|
660
|
+
const head = Buffer.alloc(2);
|
|
661
|
+
const handle = await open(part, 'r');
|
|
662
|
+
try { await handle.read(head, 0, 2, 0); } finally { await handle.close(); }
|
|
663
|
+
if (head[0] !== 0x4d || head[1] !== 0x5a) {
|
|
664
|
+
rmSync(part, { force: true });
|
|
665
|
+
throw new CliInstallError('unpack', `The archive's ${plan.member} is not a Windows program.`);
|
|
666
|
+
}
|
|
667
|
+
const target = path.join(plan.dir, plan.member);
|
|
668
|
+
try {
|
|
669
|
+
renameSync(part, target);
|
|
670
|
+
} catch (error) {
|
|
671
|
+
rmSync(part, { force: true });
|
|
672
|
+
throw asInstallError(error, 'disk', `${target} could not be written (${errorCode(error)}: is an OpenCode still running from there?).`);
|
|
673
|
+
}
|
|
674
|
+
// Files SynaBun writes carry no Mark of the Web; a copy that does (a mirror, an antivirus) is cleared so it can start.
|
|
675
|
+
try { unlinkSync(`${target}:Zone.Identifier`); } catch { /* none */ }
|
|
676
|
+
line(`Placed ${target}`);
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
async function run(job) {
|
|
680
|
+
const { plan, tool } = job;
|
|
681
|
+
const io = stepper(job);
|
|
682
|
+
let workDir = null;
|
|
683
|
+
let before = [];
|
|
684
|
+
try {
|
|
685
|
+
emit(job, { type: 'start', tool, method: plan.kind, source: plan.source, label: plan.label, platform, arch });
|
|
686
|
+
try { before = await readOsPath(); } catch { before = []; }
|
|
687
|
+
workDir = await mkdtemp(path.join(tmpRoot, `synabun-cli-install-${tool}-`));
|
|
688
|
+
if (plan.kind === 'script') {
|
|
689
|
+
const script = await fetchScript(job, plan, workDir, io);
|
|
690
|
+
io.step('run');
|
|
691
|
+
await runScript(job, plan, script, workDir, before, io);
|
|
692
|
+
} else {
|
|
693
|
+
await installArchive(job, plan, workDir, io);
|
|
694
|
+
}
|
|
695
|
+
io.step('check');
|
|
696
|
+
const status = await resolver.resolve(tool, { force: true });
|
|
697
|
+
if (status?.state !== 'installed') {
|
|
698
|
+
const where = status?.path ? ` ${status.path}: ${status.error || 'it does not answer --version'}` : ' no working copy was found in any known place';
|
|
699
|
+
throw new CliInstallError('not-found-after-install', `The installer finished, but${where}.`);
|
|
700
|
+
}
|
|
701
|
+
let after = [];
|
|
702
|
+
try { after = await readOsPath(); } catch { after = []; }
|
|
703
|
+
const dir = status.path ? path[windows ? 'win32' : 'posix'].dirname(status.path) : null;
|
|
704
|
+
let onPath = dir ? onPathList(dir, after, platform) : false;
|
|
705
|
+
let added = onPath && !onPathList(dir, before, platform);
|
|
706
|
+
let warning = null;
|
|
707
|
+
if (!onPath && addToPath) {
|
|
708
|
+
io.step('path');
|
|
709
|
+
try {
|
|
710
|
+
const result = await addToPath(status);
|
|
711
|
+
onPath = result.onPath;
|
|
712
|
+
added = result.added;
|
|
713
|
+
warning = result.warning || null;
|
|
714
|
+
if (added) emit(job, { type: 'path', dir, added: true });
|
|
715
|
+
if (warning) io.line(warning);
|
|
716
|
+
} catch (error) {
|
|
717
|
+
warning = error.message;
|
|
718
|
+
io.line(`PATH could not be updated: ${warning}`);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
job.state = 'done';
|
|
722
|
+
job.endedAt = new Date().toISOString();
|
|
723
|
+
emit(job, { type: 'done', ok: true, status, path: { dir, onPath, added, ...(warning ? { warning } : {}) } });
|
|
724
|
+
try { onFinished(tool, true, status); } catch { /* the caller's */ }
|
|
725
|
+
} catch (failure) {
|
|
726
|
+
const error = failure instanceof CliInstallError ? failure : asInstallError(failure, DISK_CODES.has(errorCode(failure)) ? 'disk' : 'internal', String(failure?.message || failure));
|
|
727
|
+
job.state = 'failed';
|
|
728
|
+
job.endedAt = new Date().toISOString();
|
|
729
|
+
job.code = error.code;
|
|
730
|
+
job.error = error.message;
|
|
731
|
+
log(`[cli-install] ${tool} failed (${error.code}): ${error.message}`);
|
|
732
|
+
emit(job, { type: 'done', ok: false, code: error.code, error: error.message, manualCommand: manualOf(tool), guideUrl: guideOf(tool), log: job.log.slice(-12) });
|
|
733
|
+
try { onFinished(tool, false, null); } catch { /* the caller's */ }
|
|
734
|
+
} finally {
|
|
735
|
+
job.listeners.clear();
|
|
736
|
+
if (workDir) { try { rmSync(workDir, { recursive: true, force: true }); } catch { /* temp */ } }
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
return {
|
|
741
|
+
describe: (toolKey) => describeInstall(toolKey, { platform, arch, env, home }),
|
|
742
|
+
guideUrl: guideOf,
|
|
743
|
+
manualCommand: manualOf,
|
|
744
|
+
jobs() {
|
|
745
|
+
return Object.fromEntries([...jobs.entries()].map(([tool, job]) => [tool, publicJob(job)]));
|
|
746
|
+
},
|
|
747
|
+
running(toolKey) {
|
|
748
|
+
return jobs.get(toolKey)?.state === 'running';
|
|
749
|
+
},
|
|
750
|
+
/**
|
|
751
|
+
* Start installing `toolKey`; `onEvent` hears every event of this run
|
|
752
|
+
* (start, step, progress, log, done). Throws CliInstallError 'busy' /
|
|
753
|
+
* 'unsupported' before anything starts.
|
|
754
|
+
*/
|
|
755
|
+
start(toolKey, { onEvent = null } = {}) {
|
|
756
|
+
const plan = planFor(toolKey);
|
|
757
|
+
if (!plan) throw new CliInstallError('unsupported', `SynaBun has no one-click install of ${toolKey} for ${platform}-${arch}.`);
|
|
758
|
+
if (jobs.get(toolKey)?.state === 'running') throw new CliInstallError('busy', 'An install of this CLI is already running.');
|
|
759
|
+
const job = {
|
|
760
|
+
tool: toolKey,
|
|
761
|
+
plan,
|
|
762
|
+
state: 'running',
|
|
763
|
+
startedAt: new Date().toISOString(),
|
|
764
|
+
endedAt: null,
|
|
765
|
+
step: null,
|
|
766
|
+
code: null,
|
|
767
|
+
error: null,
|
|
768
|
+
source: null,
|
|
769
|
+
log: [],
|
|
770
|
+
listeners: new Set(onEvent ? [onEvent] : []),
|
|
771
|
+
abort: new AbortController(),
|
|
772
|
+
child: null,
|
|
773
|
+
};
|
|
774
|
+
jobs.set(toolKey, job);
|
|
775
|
+
job.done = run(job);
|
|
776
|
+
return { job: publicJob(job), done: job.done, unsubscribe: () => { if (onEvent) job.listeners.delete(onEvent); } };
|
|
777
|
+
},
|
|
778
|
+
/** Stop every running install (SynaBun is shutting down). */
|
|
779
|
+
stopAll() {
|
|
780
|
+
for (const job of jobs.values()) if (job.state === 'running') job.abort.abort();
|
|
781
|
+
},
|
|
782
|
+
};
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/** End an installer and everything it started. */
|
|
786
|
+
function stopChild(child, windows, spawnImpl) {
|
|
787
|
+
if (!child?.pid) return;
|
|
788
|
+
if (windows) {
|
|
789
|
+
try { spawnImpl('taskkill', ['/pid', String(child.pid), '/T', '/F'], { windowsHide: true, stdio: 'ignore' }); } catch { /* gone */ }
|
|
790
|
+
return;
|
|
791
|
+
}
|
|
792
|
+
try { process.kill(-child.pid, 'SIGTERM'); } catch { try { child.kill('SIGTERM'); } catch { /* gone */ } }
|
|
793
|
+
const hard = setTimeout(() => {
|
|
794
|
+
try { process.kill(-child.pid, 'SIGKILL'); } catch { try { child.kill('SIGKILL'); } catch { /* gone */ } }
|
|
795
|
+
}, 5000);
|
|
796
|
+
hard.unref?.();
|
|
797
|
+
}
|