@iislee/opencodex 2.11.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/LICENSE +21 -0
- package/README.md +225 -0
- package/assets/architecture.png +0 -0
- package/assets/banner.png +0 -0
- package/assets/claude-code-models.gif +0 -0
- package/assets/codex-app-picker.png +0 -0
- package/bin/ocx.mjs +451 -0
- package/bin/package-main.mjs +9 -0
- package/gui/dist/assets/index-DTpMHS4F.js +67 -0
- package/gui/dist/assets/index-ZNVDE3C7.css +1 -0
- package/gui/dist/favicon.png +0 -0
- package/gui/dist/icons.svg +24 -0
- package/gui/dist/index.html +25 -0
- package/gui/dist/logo.png +0 -0
- package/gui/dist/provider-icons/alibaba-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity-color.svg +1 -0
- package/gui/dist/provider-icons/antigravity.svg +1 -0
- package/gui/dist/provider-icons/claude-color.svg +1 -0
- package/gui/dist/provider-icons/claude.svg +1 -0
- package/gui/dist/provider-icons/cloudflare-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/copilot-color.svg +1 -0
- package/gui/dist/provider-icons/copilot.svg +1 -0
- package/gui/dist/provider-icons/cursor-color.svg +2 -0
- package/gui/dist/provider-icons/cursor.svg +2 -0
- package/gui/dist/provider-icons/deepseek-color.svg +1 -0
- package/gui/dist/provider-icons/discord.svg +1 -0
- package/gui/dist/provider-icons/firepass-color.svg +1 -0
- package/gui/dist/provider-icons/fireworks-color.svg +1 -0
- package/gui/dist/provider-icons/gemini-color.svg +1 -0
- package/gui/dist/provider-icons/gemini.svg +1 -0
- package/gui/dist/provider-icons/github-copilot-color.svg +1 -0
- package/gui/dist/provider-icons/gitlab-duo-color.svg +1 -0
- package/gui/dist/provider-icons/grok-color.svg +1 -0
- package/gui/dist/provider-icons/grok.svg +1 -0
- package/gui/dist/provider-icons/groq-color.svg +1 -0
- package/gui/dist/provider-icons/huggingface-color.svg +1 -0
- package/gui/dist/provider-icons/kimi-color.svg +1 -0
- package/gui/dist/provider-icons/kiro-color.svg +15 -0
- package/gui/dist/provider-icons/kiro.svg +14 -0
- package/gui/dist/provider-icons/lm-studio-color.svg +1 -0
- package/gui/dist/provider-icons/mistral-color.svg +1 -0
- package/gui/dist/provider-icons/moonshot-color.svg +1 -0
- package/gui/dist/provider-icons/nvidia-color.svg +1 -0
- package/gui/dist/provider-icons/ollama-color.svg +1 -0
- package/gui/dist/provider-icons/openai.svg +1 -0
- package/gui/dist/provider-icons/opencode.svg +1 -0
- package/gui/dist/provider-icons/openrouter-color.svg +1 -0
- package/gui/dist/provider-icons/pi.svg +21 -0
- package/gui/dist/provider-icons/qianfan-color.svg +1 -0
- package/gui/dist/provider-icons/qwen-portal-color.svg +1 -0
- package/gui/dist/provider-icons/telegram.svg +1 -0
- package/gui/dist/provider-icons/vercel-ai-gateway-color.svg +1 -0
- package/gui/dist/provider-icons/vllm-color.svg +1 -0
- package/gui/dist/provider-icons/xiaomi-color.svg +1 -0
- package/package.json +102 -0
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic-image-guard.ts +251 -0
- package/src/adapters/anthropic-image-normalize.ts +518 -0
- package/src/adapters/anthropic.ts +1003 -0
- package/src/adapters/azure.ts +36 -0
- package/src/adapters/base.ts +72 -0
- package/src/adapters/client-fingerprint.ts +59 -0
- package/src/adapters/cursor/arg-codec.ts +38 -0
- package/src/adapters/cursor/arg-normalize.ts +104 -0
- package/src/adapters/cursor/cursor-errors.ts +165 -0
- package/src/adapters/cursor/discovery.ts +276 -0
- package/src/adapters/cursor/effort-map.ts +127 -0
- package/src/adapters/cursor/exec-policy.ts +88 -0
- package/src/adapters/cursor/framing.ts +211 -0
- package/src/adapters/cursor/gen/agent_pb.ts +15274 -0
- package/src/adapters/cursor/kv-store.ts +52 -0
- package/src/adapters/cursor/live-models.ts +153 -0
- package/src/adapters/cursor/live-smoke-gate.ts +41 -0
- package/src/adapters/cursor/live-transport.ts +1214 -0
- package/src/adapters/cursor/mcp-config.ts +42 -0
- package/src/adapters/cursor/mcp-manager.ts +333 -0
- package/src/adapters/cursor/message-mapper.ts +49 -0
- package/src/adapters/cursor/native-exec-common.ts +55 -0
- package/src/adapters/cursor/native-exec-desktop.ts +184 -0
- package/src/adapters/cursor/native-exec-fs.ts +329 -0
- package/src/adapters/cursor/native-exec-mcp.ts +153 -0
- package/src/adapters/cursor/native-exec-network.ts +43 -0
- package/src/adapters/cursor/native-exec-shell.ts +548 -0
- package/src/adapters/cursor/native-exec-tools.ts +118 -0
- package/src/adapters/cursor/native-exec.ts +576 -0
- package/src/adapters/cursor/protobuf-events.ts +563 -0
- package/src/adapters/cursor/protobuf-request.ts +714 -0
- package/src/adapters/cursor/request-builder.ts +255 -0
- package/src/adapters/cursor/thread-continuity.ts +67 -0
- package/src/adapters/cursor/tool-definitions.ts +505 -0
- package/src/adapters/cursor/transport-retry.ts +132 -0
- package/src/adapters/cursor/transport.ts +57 -0
- package/src/adapters/cursor/types.ts +52 -0
- package/src/adapters/cursor.ts +196 -0
- package/src/adapters/google-antigravity-replay.ts +303 -0
- package/src/adapters/google-antigravity-wire.ts +108 -0
- package/src/adapters/google-errors.ts +85 -0
- package/src/adapters/google-http.ts +100 -0
- package/src/adapters/google-tool-schema.ts +173 -0
- package/src/adapters/google-truncation.ts +13 -0
- package/src/adapters/google-wire-compiler.ts +232 -0
- package/src/adapters/google.ts +758 -0
- package/src/adapters/identity.ts +44 -0
- package/src/adapters/image.ts +23 -0
- package/src/adapters/kiro-constants.ts +16 -0
- package/src/adapters/kiro-errors.ts +197 -0
- package/src/adapters/kiro-events.ts +179 -0
- package/src/adapters/kiro-images.ts +129 -0
- package/src/adapters/kiro-retry.ts +312 -0
- package/src/adapters/kiro-thinking.ts +96 -0
- package/src/adapters/kiro-tool-fallback.ts +36 -0
- package/src/adapters/kiro-tools.ts +215 -0
- package/src/adapters/kiro-truncation.ts +33 -0
- package/src/adapters/kiro-wire.ts +129 -0
- package/src/adapters/kiro.ts +1898 -0
- package/src/adapters/mimo-free.ts +263 -0
- package/src/adapters/openai-chat.ts +1005 -0
- package/src/adapters/openai-responses.ts +1137 -0
- package/src/adapters/run-turn-queue.ts +114 -0
- package/src/adapters/tool-catalog-nudge.ts +71 -0
- package/src/adapters/upstream-http-error.ts +48 -0
- package/src/bridge.ts +1619 -0
- package/src/chat/inbound.ts +295 -0
- package/src/chat/outbound.ts +765 -0
- package/src/claude/agents-inject.ts +243 -0
- package/src/claude/alias.ts +149 -0
- package/src/claude/auth-detect.ts +229 -0
- package/src/claude/auth-mode-migration.ts +32 -0
- package/src/claude/auth-mode.ts +62 -0
- package/src/claude/context-windows.ts +189 -0
- package/src/claude/desktop-3p-guard.ts +35 -0
- package/src/claude/desktop-3p-paths.ts +84 -0
- package/src/claude/desktop-3p.ts +381 -0
- package/src/claude/desktop-health.ts +26 -0
- package/src/claude/desktop-profile.ts +263 -0
- package/src/claude/gateway-cache.ts +70 -0
- package/src/claude/inbound-debug.ts +163 -0
- package/src/claude/inbound.ts +509 -0
- package/src/claude/model-info.ts +151 -0
- package/src/claude/outbound.ts +872 -0
- package/src/cli/access.ts +108 -0
- package/src/cli/account-api.ts +268 -0
- package/src/cli/account-auth.ts +223 -0
- package/src/cli/account-extended.ts +350 -0
- package/src/cli/account.ts +275 -0
- package/src/cli/agent-driven.ts +70 -0
- package/src/cli/agent.ts +184 -0
- package/src/cli/catalog-prewarm.ts +27 -0
- package/src/cli/claude-desktop.ts +188 -0
- package/src/cli/claude.ts +286 -0
- package/src/cli/codex-shim-autorestore.ts +45 -0
- package/src/cli/combo.ts +119 -0
- package/src/cli/config-command.ts +145 -0
- package/src/cli/debug.ts +228 -0
- package/src/cli/doctor.ts +930 -0
- package/src/cli/export-command.ts +187 -0
- package/src/cli/help.ts +354 -0
- package/src/cli/index.ts +1113 -0
- package/src/cli/init.ts +224 -0
- package/src/cli/integrations.ts +142 -0
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/internal-dispatch.ts +20 -0
- package/src/cli/models-runtime.ts +212 -0
- package/src/cli/models.ts +336 -0
- package/src/cli/observe.ts +117 -0
- package/src/cli/opencode.ts +586 -0
- package/src/cli/pi.ts +188 -0
- package/src/cli/provider-runtime.ts +162 -0
- package/src/cli/provider.ts +463 -0
- package/src/cli/runtime-api.ts +325 -0
- package/src/cli/star-prompt.ts +155 -0
- package/src/cli/status-oauth.ts +78 -0
- package/src/cli/status.ts +321 -0
- package/src/cli/sync-cloud.ts +283 -0
- package/src/cli/system-command.ts +112 -0
- package/src/cli/tray-proxy.ts +52 -0
- package/src/cli/v2.ts +173 -0
- package/src/cli.ts +10 -0
- package/src/clients/config-export.ts +377 -0
- package/src/clients/effective-status.ts +385 -0
- package/src/clients/probes/agy.ts +55 -0
- package/src/clients/probes/cc-switch.ts +110 -0
- package/src/clients/probes/claude.ts +90 -0
- package/src/clients/probes/codex.ts +125 -0
- package/src/clients/probes/grok.ts +29 -0
- package/src/clients/probes/opencode.ts +109 -0
- package/src/clients/probes/paseo.ts +55 -0
- package/src/clients/probes/pi.ts +55 -0
- package/src/cloud/onedrive-auth.ts +666 -0
- package/src/cloud/onedrive-graph.ts +108 -0
- package/src/cloud/settings.ts +75 -0
- package/src/cloud/sync.ts +212 -0
- package/src/cloud/types.ts +56 -0
- package/src/cloud/vault.ts +89 -0
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-label.ts +34 -0
- package/src/codex/account-lifecycle.ts +55 -0
- package/src/codex/account-namespace-match.ts +63 -0
- package/src/codex/account-namespaces.ts +149 -0
- package/src/codex/account-pause.ts +20 -0
- package/src/codex/account-runtime-state.ts +31 -0
- package/src/codex/account-store.ts +517 -0
- package/src/codex/account-usability.ts +20 -0
- package/src/codex/app-server-processes.ts +756 -0
- package/src/codex/auth-api.ts +1540 -0
- package/src/codex/auth-collision.ts +107 -0
- package/src/codex/auth-context.ts +352 -0
- package/src/codex/autostart-health.ts +149 -0
- package/src/codex/catalog/aggregation.ts +378 -0
- package/src/codex/catalog/bundled.ts +251 -0
- package/src/codex/catalog/effort.ts +355 -0
- package/src/codex/catalog/metadata.ts +180 -0
- package/src/codex/catalog/parsing.ts +456 -0
- package/src/codex/catalog/provider-fetch.ts +902 -0
- package/src/codex/catalog/sync.ts +620 -0
- package/src/codex/catalog.ts +12 -0
- package/src/codex/data/upstream-models.json +830 -0
- package/src/codex/exec-invocation.ts +22 -0
- package/src/codex/features.ts +969 -0
- package/src/codex/history-migration-guardian.ts +102 -0
- package/src/codex/history-provider.ts +776 -0
- package/src/codex/home.ts +206 -0
- package/src/codex/inject.ts +799 -0
- package/src/codex/injected-marker.ts +72 -0
- package/src/codex/journal.ts +163 -0
- package/src/codex/main-account-cache.ts +32 -0
- package/src/codex/main-account.ts +40 -0
- package/src/codex/model-cache.ts +227 -0
- package/src/codex/paths.ts +65 -0
- package/src/codex/plugins-doctor.ts +242 -0
- package/src/codex/pool-rotation.ts +225 -0
- package/src/codex/project-config-warnings.ts +411 -0
- package/src/codex/quota.ts +411 -0
- package/src/codex/refresh.ts +53 -0
- package/src/codex/routing.ts +1477 -0
- package/src/codex/runtime.ts +538 -0
- package/src/codex/shim.ts +1189 -0
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +469 -0
- package/src/codex/sync.ts +130 -0
- package/src/codex/warmup.ts +192 -0
- package/src/codex/websocket-registry.ts +100 -0
- package/src/combos/failover.ts +140 -0
- package/src/combos/index.ts +41 -0
- package/src/combos/request.ts +62 -0
- package/src/combos/resolve.ts +232 -0
- package/src/combos/types.ts +326 -0
- package/src/config.ts +2356 -0
- package/src/generated/jawcode-model-metadata.ts +104 -0
- package/src/github/star-state.ts +203 -0
- package/src/grok/inject.ts +545 -0
- package/src/grok/status.ts +121 -0
- package/src/grok/sync.ts +103 -0
- package/src/grok/usage-hook/report.mjs +348 -0
- package/src/grok/usage-hook.ts +278 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +149 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +829 -0
- package/src/images/plan.ts +133 -0
- package/src/images/synthetic-tool.ts +133 -0
- package/src/images/types.ts +41 -0
- package/src/images/xai-client.ts +141 -0
- package/src/images/xai-video-client.ts +163 -0
- package/src/index.ts +22 -0
- package/src/lib/abort.ts +146 -0
- package/src/lib/admin-secrets.ts +25 -0
- package/src/lib/admission.ts +83 -0
- package/src/lib/app-owned-memory-stores.ts +173 -0
- package/src/lib/app-owned-memory.ts +265 -0
- package/src/lib/bounded-body.ts +202 -0
- package/src/lib/bun-binary-validator.d.mts +3 -0
- package/src/lib/bun-binary-validator.mjs +18 -0
- package/src/lib/bun-runtime.ts +71 -0
- package/src/lib/bun-stream-caps.ts +126 -0
- package/src/lib/config-ownership.ts +360 -0
- package/src/lib/crash-guard.ts +344 -0
- package/src/lib/debug-log-buffer.ts +83 -0
- package/src/lib/debug-settings.ts +108 -0
- package/src/lib/debug.ts +31 -0
- package/src/lib/destination-policy.ts +316 -0
- package/src/lib/errors.ts +364 -0
- package/src/lib/eventstream-decoder.ts +253 -0
- package/src/lib/gcp-adc.ts +341 -0
- package/src/lib/injection-debug-log.ts +58 -0
- package/src/lib/open-url.ts +25 -0
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/privacy.ts +20 -0
- package/src/lib/process-control.ts +165 -0
- package/src/lib/provider-outbound.ts +170 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/redact.ts +105 -0
- package/src/lib/retry-after.ts +55 -0
- package/src/lib/service-secrets.ts +25 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/sidecar-tracker.ts +52 -0
- package/src/lib/sse-decoder.ts +323 -0
- package/src/lib/state-store-registrations.ts +109 -0
- package/src/lib/state-store-sweeper.ts +184 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/token-estimate.ts +69 -0
- package/src/lib/translator-budget.ts +356 -0
- package/src/lib/upstream-retry.ts +239 -0
- package/src/lib/win-exec.ts +115 -0
- package/src/lib/win-paths.ts +68 -0
- package/src/lib/windows-elevation.ts +705 -0
- package/src/lib/windows-secret-acl.ts +514 -0
- package/src/lib/winsw.ts +375 -0
- package/src/oauth/anthropic-routing.ts +594 -0
- package/src/oauth/anthropic.ts +177 -0
- package/src/oauth/callback-server.ts +294 -0
- package/src/oauth/chatgpt.ts +150 -0
- package/src/oauth/cursor.ts +211 -0
- package/src/oauth/github-copilot.ts +428 -0
- package/src/oauth/google-antigravity.ts +230 -0
- package/src/oauth/health.ts +399 -0
- package/src/oauth/index.ts +1174 -0
- package/src/oauth/key-providers.ts +108 -0
- package/src/oauth/kimi.ts +213 -0
- package/src/oauth/kiro-credentials.ts +726 -0
- package/src/oauth/kiro.ts +577 -0
- package/src/oauth/local-token-detect.ts +121 -0
- package/src/oauth/log.ts +48 -0
- package/src/oauth/login-cli.ts +163 -0
- package/src/oauth/pkce.ts +15 -0
- package/src/oauth/store.ts +630 -0
- package/src/oauth/token-guardian.ts +303 -0
- package/src/oauth/types.ts +62 -0
- package/src/oauth/xai.ts +241 -0
- package/src/pi/extensions.ts +72 -0
- package/src/pi/home.ts +42 -0
- package/src/pi/index.ts +40 -0
- package/src/pi/models.ts +278 -0
- package/src/pi/packages.ts +219 -0
- package/src/pi/settings.ts +365 -0
- package/src/pi/status.ts +68 -0
- package/src/pi/sync.ts +75 -0
- package/src/providers/alibaba-region-backup.ts +75 -0
- package/src/providers/alibaba-region-migration.ts +156 -0
- package/src/providers/alibaba-region-startup.ts +36 -0
- package/src/providers/antigravity-models.ts +205 -0
- package/src/providers/api-keys.ts +140 -0
- package/src/providers/base-url-choices.ts +64 -0
- package/src/providers/context-cap.ts +65 -0
- package/src/providers/derive.ts +339 -0
- package/src/providers/free-directory.ts +184 -0
- package/src/providers/github-copilot-transport.ts +56 -0
- package/src/providers/key-failover.ts +203 -0
- package/src/providers/kiro-models.ts +67 -0
- package/src/providers/label.ts +19 -0
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/openai-sidecar.ts +175 -0
- package/src/providers/openai-tier-startup.ts +27 -0
- package/src/providers/openai-tiers.ts +301 -0
- package/src/providers/openai-virtual-models.ts +82 -0
- package/src/providers/openrouter-routing.ts +102 -0
- package/src/providers/provider-id-rewrite.ts +150 -0
- package/src/providers/quota.ts +1260 -0
- package/src/providers/registry.ts +1600 -0
- package/src/providers/slug-codec.ts +67 -0
- package/src/providers/xai-transport.ts +141 -0
- package/src/reasoning-effort.ts +135 -0
- package/src/responses/compaction.ts +117 -0
- package/src/responses/parser.ts +656 -0
- package/src/responses/reasoning-envelope.ts +52 -0
- package/src/responses/schema.ts +159 -0
- package/src/responses/spill-store.ts +394 -0
- package/src/responses/state.ts +895 -0
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +425 -0
- package/src/server/adapter-resolve.ts +80 -0
- package/src/server/auth-cors.ts +530 -0
- package/src/server/chat-completions.ts +368 -0
- package/src/server/claude-messages.ts +914 -0
- package/src/server/effort-policy.ts +172 -0
- package/src/server/gui-static.ts +123 -0
- package/src/server/image-retry.ts +42 -0
- package/src/server/images.ts +476 -0
- package/src/server/index.ts +1126 -0
- package/src/server/lifecycle.ts +227 -0
- package/src/server/live.ts +598 -0
- package/src/server/management/agent-settings-routes.ts +1169 -0
- package/src/server/management/api-access.ts +141 -0
- package/src/server/management/api-key-usage.ts +167 -0
- package/src/server/management/body.ts +35 -0
- package/src/server/management/clients-routes.ts +63 -0
- package/src/server/management/cloud-sync-routes.ts +266 -0
- package/src/server/management/combo-routes.ts +220 -0
- package/src/server/management/config-routes.ts +422 -0
- package/src/server/management/context.ts +31 -0
- package/src/server/management/logs-usage-routes.ts +707 -0
- package/src/server/management/model-routes.ts +525 -0
- package/src/server/management/oauth-account-routes.ts +563 -0
- package/src/server/management/provider-routes.ts +556 -0
- package/src/server/management/shared.ts +277 -0
- package/src/server/management/sidebar-routes.ts +90 -0
- package/src/server/management/system-restart.ts +179 -0
- package/src/server/management/system-routes.ts +117 -0
- package/src/server/management/usage-summary-cache.ts +86 -0
- package/src/server/management-api.ts +215 -0
- package/src/server/management-auth.ts +267 -0
- package/src/server/memory-watchdog.ts +156 -0
- package/src/server/port-reclaim.ts +307 -0
- package/src/server/ports.ts +116 -0
- package/src/server/proxy-liveness.ts +201 -0
- package/src/server/relay-eager.ts +313 -0
- package/src/server/relay.ts +1049 -0
- package/src/server/request-decompress.ts +132 -0
- package/src/server/request-log-conversation.ts +168 -0
- package/src/server/request-log.ts +1046 -0
- package/src/server/responses/collaboration.ts +354 -0
- package/src/server/responses/compact.ts +384 -0
- package/src/server/responses/core.ts +2758 -0
- package/src/server/responses/encrypted-payload.ts +308 -0
- package/src/server/responses/fetch-helpers.ts +157 -0
- package/src/server/responses/passthrough-error.ts +78 -0
- package/src/server/responses/terminal-guard.ts +230 -0
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +132 -0
- package/src/server/responses-item-id-repair.ts +224 -0
- package/src/server/responses.ts +9 -0
- package/src/server/search.ts +136 -0
- package/src/server/sse-payload-rewrite.ts +175 -0
- package/src/server/startup-action-control.ts +308 -0
- package/src/server/startup-health-cache.ts +113 -0
- package/src/server/system-env.ts +413 -0
- package/src/server/windows-tcp-drop.ts +184 -0
- package/src/server/windows-tray-control.ts +41 -0
- package/src/server/ws-bridge.ts +471 -0
- package/src/service.ts +2554 -0
- package/src/stall-timeout.ts +20 -0
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +3085 -0
- package/src/storage/policy-job.ts +457 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +59 -0
- package/src/storage/policy.ts +527 -0
- package/src/storage/restore-job.ts +299 -0
- package/src/storage/restore-worker.ts +58 -0
- package/src/storage/scanner.ts +238 -0
- package/src/storage/storage-mutation-coordinator.ts +139 -0
- package/src/storage/worker-lifecycle.ts +215 -0
- package/src/tray/assets/opencodex-tray-offline.ico +0 -0
- package/src/tray/assets/opencodex-tray-online.ico +0 -0
- package/src/tray/assets/opencodex-tray-warning.ico +0 -0
- package/src/tray/assets/opencodex-tray.png +0 -0
- package/src/tray/windows-tray.ps1 +290 -0
- package/src/tray/windows.ts +730 -0
- package/src/types.ts +1237 -0
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +407 -0
- package/src/update/job.ts +1520 -0
- package/src/update/notify.ts +257 -0
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/update/tray-update-plan.d.mts +18 -0
- package/src/update/tray-update-plan.mjs +38 -0
- package/src/usage/cost.ts +0 -0
- package/src/usage/debug.ts +97 -0
- package/src/usage/expected-prices.ts +164 -0
- package/src/usage/log.ts +658 -0
- package/src/usage/summary.ts +585 -0
- package/src/usage/totals.ts +14 -0
- package/src/vision/anthropic-describe.ts +185 -0
- package/src/vision/describe.ts +125 -0
- package/src/vision/index.ts +467 -0
- package/src/web-search/anthropic-executor.ts +189 -0
- package/src/web-search/executor.ts +105 -0
- package/src/web-search/format-result.ts +89 -0
- package/src/web-search/index.ts +196 -0
- package/src/web-search/loop.ts +664 -0
- package/src/web-search/parse.ts +220 -0
- package/src/web-search/progress-stream.ts +342 -0
- package/src/web-search/synthetic-tool.ts +47 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retry guard for upstream fetches that die on stale pooled keep-alive sockets.
|
|
3
|
+
*
|
|
4
|
+
* chatgpt.com (Cloudflare) closes idle keep-alive connections server-side; Bun's fetch pool
|
|
5
|
+
* reuses the half-closed socket and the request write fails with ECONNRESET before any
|
|
6
|
+
* response bytes arrive. Retrying on a fresh connection is safe for our replayable
|
|
7
|
+
* (string-body) upstream requests, because fetch() rejects only before response headers —
|
|
8
|
+
* a caught error here means no response was ever received.
|
|
9
|
+
*
|
|
10
|
+
* Deliberately narrow: timeouts, aborts, ECONNREFUSED/DNS/TLS failures, and HTTP error
|
|
11
|
+
* statuses (returned as Response, never thrown) are NOT retried. Mid-stream SSE resets are
|
|
12
|
+
* out of scope — the response has already resolved by then.
|
|
13
|
+
*
|
|
14
|
+
* MUST stay a leaf module: imports nothing from server.ts or adapters (kiro-retry imports
|
|
15
|
+
* the shared abort helpers from here).
|
|
16
|
+
*/
|
|
17
|
+
import { clearableDeadline } from "./abort";
|
|
18
|
+
|
|
19
|
+
// 1 initial + 2 retries: the pool may hold more than one stale socket.
|
|
20
|
+
const RESET_RETRY_MAX_ATTEMPTS = 3;
|
|
21
|
+
const RESET_RETRY_BASE_DELAY_MS = 150;
|
|
22
|
+
const RESET_RETRY_MAX_DELAY_MS = 1_000;
|
|
23
|
+
|
|
24
|
+
// Transient-5xx status retry layer (pre-stream only; devlog/_plan/260716_claudecode_hardening/010).
|
|
25
|
+
const TRANSIENT_RETRY_MAX_ATTEMPTS = 3; // 1 initial + 2 retries
|
|
26
|
+
const TRANSIENT_RETRY_BASE_DELAY_MS = 400;
|
|
27
|
+
const TRANSIENT_RETRY_MAX_DELAY_MS = 5_000;
|
|
28
|
+
// A failed attempt slower than this is the "slow 502" incident shape (191s observed on
|
|
29
|
+
// 2026-07-15): retrying it only duplicates upstream load past client timeouts — return it.
|
|
30
|
+
const TRANSIENT_RETRY_SLOW_ATTEMPT_MS = 15_000;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Upstream statuses treated as transient: gateway errors and Cloudflare 52x.
|
|
34
|
+
* 500 is included per the OpenAI SDK default (auto-retries >=500; Tier-2 proven in
|
|
35
|
+
* devlog/260716_ocx_claude_sol_502_midstream/02). 507 was observed in the 48h ledger
|
|
36
|
+
* but is deliberately excluded (storage-class, not gateway-transient).
|
|
37
|
+
*/
|
|
38
|
+
export function isTransientUpstreamStatus(status: number): boolean {
|
|
39
|
+
return status === 500 || status === 502 || status === 503 || status === 504
|
|
40
|
+
|| status === 520 || status === 521 || status === 522;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface RetryBackoffOptions {
|
|
44
|
+
baseDelayMs: number;
|
|
45
|
+
maxDelayMs: number;
|
|
46
|
+
headers?: Headers;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function abortError(signal?: AbortSignal): unknown {
|
|
50
|
+
return signal?.reason ?? new DOMException("The operation was aborted", "AbortError");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function sleepWithAbort(ms: number, signal?: AbortSignal): Promise<void> {
|
|
54
|
+
if (ms <= 0) return;
|
|
55
|
+
if (signal?.aborted) throw abortError(signal);
|
|
56
|
+
await new Promise<void>((resolve, reject) => {
|
|
57
|
+
let timer: ReturnType<typeof setTimeout>;
|
|
58
|
+
const cleanup = () => {
|
|
59
|
+
clearTimeout(timer);
|
|
60
|
+
signal?.removeEventListener("abort", onAbort);
|
|
61
|
+
};
|
|
62
|
+
const onAbort = () => {
|
|
63
|
+
cleanup();
|
|
64
|
+
reject(abortError(signal));
|
|
65
|
+
};
|
|
66
|
+
timer = setTimeout(() => {
|
|
67
|
+
cleanup();
|
|
68
|
+
resolve();
|
|
69
|
+
}, ms);
|
|
70
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function isConnectionResetError(err: unknown): boolean {
|
|
75
|
+
if (!(err instanceof Error)) return false;
|
|
76
|
+
// Aborts and timeouts are caller decisions / honest failures — never retryable.
|
|
77
|
+
if (err.name === "AbortError" || err.name === "TimeoutError") return false;
|
|
78
|
+
const code = (err as { code?: unknown }).code;
|
|
79
|
+
if (code === "ECONNRESET" || code === "EPIPE") return true;
|
|
80
|
+
const msg = err.message.toLowerCase();
|
|
81
|
+
return msg.includes("socket connection was closed unexpectedly")
|
|
82
|
+
|| msg.includes("connection reset by peer");
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function retryAfterDelayMs(headers: Headers): number | undefined {
|
|
86
|
+
const raw = headers.get("retry-after")?.trim();
|
|
87
|
+
if (!raw) return undefined;
|
|
88
|
+
const seconds = Number(raw);
|
|
89
|
+
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1000);
|
|
90
|
+
const dateMs = Date.parse(raw);
|
|
91
|
+
if (!Number.isFinite(dateMs)) return undefined;
|
|
92
|
+
return Math.max(0, dateMs - Date.now());
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function retryBackoffDelayMs(attempt: number, opts: RetryBackoffOptions): number {
|
|
96
|
+
const retryAfter = opts.headers ? retryAfterDelayMs(opts.headers) : undefined;
|
|
97
|
+
if (retryAfter !== undefined) return Math.min(retryAfter, opts.maxDelayMs);
|
|
98
|
+
const exp = Math.min(opts.baseDelayMs * (2 ** attempt), opts.maxDelayMs);
|
|
99
|
+
return Math.floor(exp * (0.8 + Math.random() * 0.4));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export function cancelResponseBodyBestEffort(res: Response): void {
|
|
103
|
+
try {
|
|
104
|
+
const cancellation = res.body?.cancel();
|
|
105
|
+
if (cancellation) void cancellation.catch(() => {});
|
|
106
|
+
} catch {
|
|
107
|
+
// Cancellation is cleanup only; retries must not wait for or fail because of it.
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export async function fetchWithAttemptDeadline(
|
|
112
|
+
url: string,
|
|
113
|
+
init: RequestInit,
|
|
114
|
+
timeoutMs: number,
|
|
115
|
+
abortSignal?: AbortSignal,
|
|
116
|
+
preferIdentityEncoding = false,
|
|
117
|
+
): Promise<Response> {
|
|
118
|
+
const attemptTimeout = clearableDeadline(timeoutMs, abortSignal);
|
|
119
|
+
const headers = new Headers(init.headers);
|
|
120
|
+
if (preferIdentityEncoding && !headers.has("accept-encoding")) {
|
|
121
|
+
headers.set("accept-encoding", "identity");
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
return await fetch(url, {
|
|
125
|
+
...init,
|
|
126
|
+
headers,
|
|
127
|
+
signal: attemptTimeout.signal,
|
|
128
|
+
});
|
|
129
|
+
} finally {
|
|
130
|
+
// Only the header timer is cleared. The composed signal still contains the parent, so a
|
|
131
|
+
// caller abort after headers continue to cancel consumption of the returned response body.
|
|
132
|
+
attemptTimeout.clear();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export interface ResetRetryOptions {
|
|
137
|
+
abortSignal?: AbortSignal;
|
|
138
|
+
/** Short host/path label for the retry warn log (no secrets/query strings). */
|
|
139
|
+
label?: string;
|
|
140
|
+
attempts?: number;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface TransientRetryOptions extends ResetRetryOptions {
|
|
144
|
+
/** Test seam: per-attempt slow budget override (defaults to TRANSIENT_RETRY_SLOW_ATTEMPT_MS). */
|
|
145
|
+
slowAttemptMs?: number;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export type UpstreamSendRecovery = "connection-reset" | "transient-5xx";
|
|
149
|
+
type ReplayableFetch = (recovery?: UpstreamSendRecovery) => Promise<Response>;
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Opt out of Bun's keep-alive pool after a connection-reset retry.
|
|
153
|
+
*
|
|
154
|
+
* Prefer the Bun fetch extension `keepalive: false` (transport-level) over
|
|
155
|
+
* relying on the hop-by-hop `Connection: close` header alone — Bun has ignored
|
|
156
|
+
* that header in past releases (oven-sh/bun#20492), so a header-only retry can
|
|
157
|
+
* still reuse the same half-closed pooled socket. Still set Connection: close
|
|
158
|
+
* as a belt-and-suspenders signal for intermediaries that honor it.
|
|
159
|
+
*/
|
|
160
|
+
export function applyUpstreamRecoveryInit<T extends RequestInit>(
|
|
161
|
+
init: T,
|
|
162
|
+
recovery?: UpstreamSendRecovery,
|
|
163
|
+
): T & { headers: Headers } {
|
|
164
|
+
const headers = new Headers(init.headers);
|
|
165
|
+
if (recovery !== "connection-reset") {
|
|
166
|
+
return { ...init, headers };
|
|
167
|
+
}
|
|
168
|
+
headers.set("connection", "close");
|
|
169
|
+
return { ...init, headers, keepalive: false };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Run `doFetch`, retrying only connection-reset-shaped rejections (see
|
|
174
|
+
* isConnectionResetError) with jittered backoff. The caller's thunk must be replay-safe
|
|
175
|
+
* (string body); every retry is logged so persistent resets stay visible.
|
|
176
|
+
*/
|
|
177
|
+
export async function fetchWithResetRetry(
|
|
178
|
+
doFetch: ReplayableFetch,
|
|
179
|
+
opts: ResetRetryOptions = {},
|
|
180
|
+
firstRecovery?: UpstreamSendRecovery,
|
|
181
|
+
): Promise<Response> {
|
|
182
|
+
const attempts = Math.max(1, opts.attempts ?? RESET_RETRY_MAX_ATTEMPTS);
|
|
183
|
+
let lastError: unknown;
|
|
184
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
185
|
+
if (opts.abortSignal?.aborted) throw abortError(opts.abortSignal);
|
|
186
|
+
try {
|
|
187
|
+
return await doFetch(attempt === 0 ? firstRecovery : "connection-reset");
|
|
188
|
+
} catch (err) {
|
|
189
|
+
if (opts.abortSignal?.aborted || !isConnectionResetError(err) || attempt === attempts - 1) throw err;
|
|
190
|
+
lastError = err;
|
|
191
|
+
console.warn(
|
|
192
|
+
`[upstream-retry] connection reset${opts.label ? ` (${opts.label})` : ""} — retrying (${attempt + 2}/${attempts})`,
|
|
193
|
+
);
|
|
194
|
+
await sleepWithAbort(retryBackoffDelayMs(attempt, {
|
|
195
|
+
baseDelayMs: RESET_RETRY_BASE_DELAY_MS,
|
|
196
|
+
maxDelayMs: RESET_RETRY_MAX_DELAY_MS,
|
|
197
|
+
}), opts.abortSignal);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
throw lastError ?? new Error("upstream fetch failed");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* fetchWithResetRetry plus a transient-5xx status retry layer, PRE-STREAM only: a
|
|
205
|
+
* returned Response has by definition not been relayed to the client yet, so replaying
|
|
206
|
+
* the (string-body) request is safe. The failed attempt's body is cancelled before the
|
|
207
|
+
* retry; every returned response (ok, non-transient, aborted, slow, exhausted) keeps
|
|
208
|
+
* its body intact. Honors Retry-After via retryBackoffDelayMs.
|
|
209
|
+
*
|
|
210
|
+
* A failed attempt slower than the slow budget is returned as-is (slow-502 shape);
|
|
211
|
+
* note `opts.attempts` is shared with the inner reset layer (no caller passes it today).
|
|
212
|
+
*/
|
|
213
|
+
export async function fetchWithTransientRetry(
|
|
214
|
+
doFetch: ReplayableFetch,
|
|
215
|
+
opts: TransientRetryOptions = {},
|
|
216
|
+
): Promise<Response> {
|
|
217
|
+
const attempts = Math.max(1, opts.attempts ?? TRANSIENT_RETRY_MAX_ATTEMPTS);
|
|
218
|
+
const slowAttemptMs = opts.slowAttemptMs ?? TRANSIENT_RETRY_SLOW_ATTEMPT_MS;
|
|
219
|
+
let attemptStart = Date.now();
|
|
220
|
+
let res = await fetchWithResetRetry(doFetch, opts);
|
|
221
|
+
for (let attempt = 0; attempt < attempts - 1; attempt++) {
|
|
222
|
+
if (res.ok || !isTransientUpstreamStatus(res.status)) return res;
|
|
223
|
+
if (opts.abortSignal?.aborted) return res;
|
|
224
|
+
if (Date.now() - attemptStart > slowAttemptMs) return res;
|
|
225
|
+
console.warn(
|
|
226
|
+
`[upstream-retry] transient ${res.status}${opts.label ? ` (${opts.label})` : ""} — retrying (${attempt + 2}/${attempts})`,
|
|
227
|
+
);
|
|
228
|
+
const delay = retryBackoffDelayMs(attempt, {
|
|
229
|
+
baseDelayMs: TRANSIENT_RETRY_BASE_DELAY_MS,
|
|
230
|
+
maxDelayMs: TRANSIENT_RETRY_MAX_DELAY_MS,
|
|
231
|
+
headers: res.headers,
|
|
232
|
+
});
|
|
233
|
+
cancelResponseBodyBestEffort(res);
|
|
234
|
+
await sleepWithAbort(delay, opts.abortSignal);
|
|
235
|
+
attemptStart = Date.now();
|
|
236
|
+
res = await fetchWithResetRetry(doFetch, opts, "transient-5xx");
|
|
237
|
+
}
|
|
238
|
+
return res;
|
|
239
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform command launching (devlog 260715_cross_platform_audit/020).
|
|
3
|
+
*
|
|
4
|
+
* Windows npm installs expose CLIs as `.cmd` shims, and Node/Bun refuse shell-less
|
|
5
|
+
* `.cmd` spawns (CVE-2024-27980 hardening). Bare names like `spawn("claude")` also
|
|
6
|
+
* skip PATHEXT resolution entirely, so they ENOENT even when `claude.cmd` is on PATH.
|
|
7
|
+
* This module mirrors the battle-tested cross-spawn approach: resolve the real target
|
|
8
|
+
* via PATH×PATHEXT, launch `.exe` targets directly (argument boundaries preserved by
|
|
9
|
+
* the normal shell-less spawn), and route `.cmd`/`.bat` targets through
|
|
10
|
+
* `cmd.exe /d /s /c "<escaped line>"` with `windowsVerbatimArguments: true`.
|
|
11
|
+
*/
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import { win32 } from "node:path";
|
|
14
|
+
|
|
15
|
+
const CMD_META = /([()\][%!^"`<>&|;, *?])/g;
|
|
16
|
+
/** cross-spawn parse.js: only npm local-bin shims get double escaping. */
|
|
17
|
+
const IS_CMD_SHIM = /node_modules[\\/].bin[\\/][^\\/]+\.cmd$/i;
|
|
18
|
+
|
|
19
|
+
/** cross-spawn escape.js argument(): quote + escape one argument for cmd.exe /d /s /c. */
|
|
20
|
+
export function escapeCmdArg(arg: string, doubleEscape = false): string {
|
|
21
|
+
let out = String(arg).replace(/(\\*)"/g, '$1$1\\"').replace(/(\\*)$/, "$1$1");
|
|
22
|
+
out = `"${out}"`.replace(CMD_META, "^$1");
|
|
23
|
+
return doubleEscape ? out.replace(CMD_META, "^$1") : out;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** cross-spawn escape.js command(): escape the command token itself (no quoting). */
|
|
27
|
+
export function escapeCmdCommand(command: string): string {
|
|
28
|
+
return command.replace(CMD_META, "^$1");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResolveDeps {
|
|
32
|
+
env?: Record<string, string | undefined>;
|
|
33
|
+
exists?: (path: string) => boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Resolve a bare command name to its first PATH×PATHEXT hit (win32 semantics).
|
|
38
|
+
* Commands that already carry an extension, a separator, or an absolute prefix are
|
|
39
|
+
* returned unchanged; unresolvable names fall back unchanged (spawn will surface it).
|
|
40
|
+
*/
|
|
41
|
+
export function resolveWindowsCommand(command: string, deps: ResolveDeps = {}): string {
|
|
42
|
+
const env = deps.env ?? process.env;
|
|
43
|
+
const exists = deps.exists ?? existsSync;
|
|
44
|
+
if (win32.extname(command) || command.includes("\\") || command.includes("/") || win32.isAbsolute(command)) {
|
|
45
|
+
return command;
|
|
46
|
+
}
|
|
47
|
+
// Windows environment variables are case-insensitive, and a spawned child can
|
|
48
|
+
// arrive with `Path`, `PATH`, or both depending on who built its env. Reading
|
|
49
|
+
// only two fixed spellings silently resolved against the wrong list once a
|
|
50
|
+
// caller added a second casing, so match however the key is spelled.
|
|
51
|
+
const lookup = (name: string): string | undefined => {
|
|
52
|
+
const direct = env[name] ?? env[name.toUpperCase()] ?? env[name.toLowerCase()];
|
|
53
|
+
if (direct !== undefined) return direct;
|
|
54
|
+
const key = Object.keys(env).find(k => k.toLowerCase() === name.toLowerCase());
|
|
55
|
+
return key ? env[key] : undefined;
|
|
56
|
+
};
|
|
57
|
+
const exts = (lookup("PATHEXT") ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean);
|
|
58
|
+
for (const dir of (lookup("PATH") ?? "").split(win32.delimiter).filter(Boolean)) {
|
|
59
|
+
for (const ext of exts) {
|
|
60
|
+
const candidate = win32.join(dir, command + ext.toLowerCase());
|
|
61
|
+
if (exists(candidate)) return candidate;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return command;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface SpawnInvocation {
|
|
68
|
+
file: string;
|
|
69
|
+
args: string[];
|
|
70
|
+
options: { windowsVerbatimArguments?: boolean };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Platform-safe invocation preserving argument boundaries (cross-spawn parse.js).
|
|
75
|
+
* POSIX: passthrough. win32 `.exe`: resolved direct spawn. win32 `.cmd`/`.bat`:
|
|
76
|
+
* `ComSpec /d /s /c "<escaped command line>"` with verbatim args; npm local-bin
|
|
77
|
+
* shims get cross-spawn's double escaping, all other batch targets single.
|
|
78
|
+
*/
|
|
79
|
+
export function commandInvocation(
|
|
80
|
+
command: string,
|
|
81
|
+
args: readonly string[],
|
|
82
|
+
platform: NodeJS.Platform = process.platform,
|
|
83
|
+
deps: ResolveDeps = {},
|
|
84
|
+
): SpawnInvocation {
|
|
85
|
+
if (platform !== "win32") return { file: command, args: [...args], options: {} };
|
|
86
|
+
const resolved = resolveWindowsCommand(command, deps);
|
|
87
|
+
if (!/\.(cmd|bat)$/i.test(resolved)) return { file: resolved, args: [...args], options: {} };
|
|
88
|
+
const env = deps.env ?? process.env;
|
|
89
|
+
const doubleEscape = IS_CMD_SHIM.test(resolved);
|
|
90
|
+
const line = [escapeCmdCommand(resolved), ...args.map(a => escapeCmdArg(a, doubleEscape))].join(" ");
|
|
91
|
+
return {
|
|
92
|
+
file: env.ComSpec ?? "cmd.exe",
|
|
93
|
+
args: ["/d", "/s", "/c", `"${line}"`],
|
|
94
|
+
options: { windowsVerbatimArguments: true },
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* `sh -c <command>` analog per platform. The configured command string is passed
|
|
100
|
+
* VERBATIM in content; on win32 it gets the outer quotes `/s` requires, so
|
|
101
|
+
* `"C:\Program Files\x.exe" --json` runs as `cmd.exe /d /s /c ""C:\Program Files\x.exe" --json"`.
|
|
102
|
+
* Contract: the command is platform-native shell syntax (sh on POSIX, CMD on Windows).
|
|
103
|
+
*/
|
|
104
|
+
export function shellInvocation(
|
|
105
|
+
command: string,
|
|
106
|
+
platform: NodeJS.Platform = process.platform,
|
|
107
|
+
env: Record<string, string | undefined> = process.env,
|
|
108
|
+
): SpawnInvocation {
|
|
109
|
+
if (platform !== "win32") return { file: "sh", args: ["-c", command], options: {} };
|
|
110
|
+
return {
|
|
111
|
+
file: env.ComSpec ?? "cmd.exe",
|
|
112
|
+
args: ["/d", "/s", "/c", `"${command}"`],
|
|
113
|
+
options: { windowsVerbatimArguments: true },
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Windows path → batch-safe env indirection.
|
|
3
|
+
*
|
|
4
|
+
* cmd.exe parses .cmd files in the console's OEM codepage (CP949/GBK/CP437…), NOT UTF-8.
|
|
5
|
+
* Any absolute path we bake into a generated batch file breaks for users whose profile
|
|
6
|
+
* directory contains non-ASCII characters (e.g. Korean/Chinese usernames), because the
|
|
7
|
+
* UTF-8 bytes we wrote are decoded as mojibake at parse time. Instead of baking the
|
|
8
|
+
* profile prefix literally, rewrite it as a `%USERPROFILE%`-style token: cmd expands the
|
|
9
|
+
* variable natively at parse time in the correct codepage, so the non-ASCII prefix never
|
|
10
|
+
* has to survive a file-encoding round trip.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
export type WindowsEnvMap = Record<string, string | undefined>;
|
|
14
|
+
|
|
15
|
+
/** Ordered by specificity; longest resolved prefix wins regardless of this order. */
|
|
16
|
+
const INDIRECTION_VARS = ["LOCALAPPDATA", "APPDATA", "USERPROFILE"] as const;
|
|
17
|
+
|
|
18
|
+
/** True when `prefix` matches a leading whole-component span of `path` (case-insensitive). */
|
|
19
|
+
function isComponentPrefix(path: string, prefix: string): boolean {
|
|
20
|
+
if (path.length < prefix.length) return false;
|
|
21
|
+
if (path.slice(0, prefix.length).toLowerCase() !== prefix.toLowerCase()) return false;
|
|
22
|
+
if (path.length === prefix.length) return true;
|
|
23
|
+
const next = path[prefix.length];
|
|
24
|
+
return next === "\\" || next === "/";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Split a Windows path into an env-token prefix and the remaining literal suffix.
|
|
29
|
+
* Returns `token: ""` (and the full path as `rest`) when no known prefix applies.
|
|
30
|
+
*/
|
|
31
|
+
export function splitWindowsEnvPrefix(path: string, env: WindowsEnvMap = process.env): { token: string; rest: string } {
|
|
32
|
+
let best: { name: string; value: string } | null = null;
|
|
33
|
+
for (const name of INDIRECTION_VARS) {
|
|
34
|
+
const raw = env[name]?.trim();
|
|
35
|
+
if (!raw) continue;
|
|
36
|
+
const value = raw.replace(/[\\/]+$/, "");
|
|
37
|
+
if (!value || !isComponentPrefix(path, value)) continue;
|
|
38
|
+
if (!best || value.length > best.value.length) best = { name, value };
|
|
39
|
+
}
|
|
40
|
+
if (!best) return { token: "", rest: path };
|
|
41
|
+
return { token: `%${best.name}%`, rest: path.slice(best.value.length) };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Render a single path for embedding in a batch `set "X=…"` line: env-token prefix kept
|
|
46
|
+
* verbatim (cmd must expand it), remaining literal suffix run through the caller's batch
|
|
47
|
+
* escaping (which doubles `%` and would otherwise destroy the token itself).
|
|
48
|
+
*/
|
|
49
|
+
export function windowsEnvIndirectBatchValue(
|
|
50
|
+
path: string,
|
|
51
|
+
escape: (value: string) => string,
|
|
52
|
+
env: WindowsEnvMap = process.env,
|
|
53
|
+
): string {
|
|
54
|
+
const { token, rest } = splitWindowsEnvPrefix(path, env);
|
|
55
|
+
return token + escape(rest);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Same as {@link windowsEnvIndirectBatchValue} for `;`-separated path lists (PATH). */
|
|
59
|
+
export function windowsEnvIndirectBatchPathList(
|
|
60
|
+
value: string,
|
|
61
|
+
escape: (value: string) => string,
|
|
62
|
+
env: WindowsEnvMap = process.env,
|
|
63
|
+
): string {
|
|
64
|
+
return value
|
|
65
|
+
.split(";")
|
|
66
|
+
.map(part => (part ? windowsEnvIndirectBatchValue(part, escape, env) : part))
|
|
67
|
+
.join(";");
|
|
68
|
+
}
|