@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,1189 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { delimiter, dirname, extname, join, posix } from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
chmodSync,
|
|
5
|
+
closeSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
fstatSync,
|
|
8
|
+
lstatSync,
|
|
9
|
+
mkdirSync,
|
|
10
|
+
openSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
readSync,
|
|
14
|
+
renameSync,
|
|
15
|
+
rmdirSync,
|
|
16
|
+
statSync,
|
|
17
|
+
type Stats,
|
|
18
|
+
unlinkSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} from "node:fs";
|
|
21
|
+
import { getConfigDir } from "../config";
|
|
22
|
+
import { durableBunPath } from "../lib/bun-runtime";
|
|
23
|
+
import { isProcessAlive } from "../lib/process-control";
|
|
24
|
+
import { serviceApiTokenFilePath } from "../lib/service-secrets";
|
|
25
|
+
import { recordOwnedConfigPath } from "../lib/config-ownership";
|
|
26
|
+
import { windowsEnvIndirectBatchValue } from "../lib/win-paths";
|
|
27
|
+
import { isWslRuntime, wslAutomountRoot } from "./home";
|
|
28
|
+
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
29
|
+
|
|
30
|
+
const SHIM_MARKER = "opencodex codex autostart shim";
|
|
31
|
+
const CODEX_SHIM_PROBE_BYTES = 16 * 1024;
|
|
32
|
+
export const CODEX_SHIM_REPLACEMENT_STABLE_MS = 100;
|
|
33
|
+
export const CODEX_SHIM_STATE_MAX_BYTES = 1024 * 1024;
|
|
34
|
+
const CODEX_SHIM_RESTORE_LOCK_STALE_MS = 30_000;
|
|
35
|
+
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
36
|
+
let lastShimDiscoveryError: string | null = null;
|
|
37
|
+
/** Last human-readable reason discovery returned null (exposed for doctor/tests). */
|
|
38
|
+
export function lastCodexDiscoveryError(): string | null {
|
|
39
|
+
return lastShimDiscoveryError;
|
|
40
|
+
}
|
|
41
|
+
const CODEX_INTERNAL_COMMANDS = [
|
|
42
|
+
"app-server",
|
|
43
|
+
"archive",
|
|
44
|
+
"apply",
|
|
45
|
+
"cloud",
|
|
46
|
+
"completion",
|
|
47
|
+
"debug",
|
|
48
|
+
"delete",
|
|
49
|
+
"doctor",
|
|
50
|
+
"exec-server",
|
|
51
|
+
"features",
|
|
52
|
+
"fork",
|
|
53
|
+
"help",
|
|
54
|
+
"login",
|
|
55
|
+
"logout",
|
|
56
|
+
"mcp",
|
|
57
|
+
"plugin",
|
|
58
|
+
"sandbox",
|
|
59
|
+
"unarchive",
|
|
60
|
+
"update",
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Codex accepts global options before a subcommand. The shim must skip the value belonging to
|
|
64
|
+
// these options before it decides which first positional token is the real subcommand. Keep this
|
|
65
|
+
// list aligned with `codex --help`; `--option=value` and attached short forms stay one token.
|
|
66
|
+
const CODEX_GLOBAL_OPTIONS_WITH_VALUE = [
|
|
67
|
+
"-c", "--config",
|
|
68
|
+
"--enable", "--disable",
|
|
69
|
+
"--remote", "--remote-auth-token-env",
|
|
70
|
+
"-i", "--image",
|
|
71
|
+
"-m", "--model",
|
|
72
|
+
"--local-provider",
|
|
73
|
+
"-p", "--profile",
|
|
74
|
+
"-s", "--sandbox",
|
|
75
|
+
"-C", "--cd",
|
|
76
|
+
"--add-dir",
|
|
77
|
+
"-a", "--ask-for-approval",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
interface ShimState {
|
|
81
|
+
platform: NodeJS.Platform;
|
|
82
|
+
wrapperPath: string;
|
|
83
|
+
originalPath: string;
|
|
84
|
+
backupPath: string;
|
|
85
|
+
wrappers?: ShimFileState[];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
interface ShimFileState {
|
|
89
|
+
wrapperPath: string;
|
|
90
|
+
originalPath: string;
|
|
91
|
+
backupPath: string;
|
|
92
|
+
realPath?: string;
|
|
93
|
+
preserveOnly?: boolean;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
interface ShimPathFingerprint {
|
|
97
|
+
dev: number;
|
|
98
|
+
ino: number;
|
|
99
|
+
kind: "file" | "symlink";
|
|
100
|
+
mode: number;
|
|
101
|
+
size: number;
|
|
102
|
+
mtimeMs: number;
|
|
103
|
+
ctimeMs: number;
|
|
104
|
+
target?: Omit<ShimPathFingerprint, "target">;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
interface StableShimPathProbe {
|
|
108
|
+
fingerprint: ShimPathFingerprint;
|
|
109
|
+
prefix: string;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
interface InstallCodexShimInternalOptions {
|
|
113
|
+
expectedReplacements?: ReadonlyMap<string, ShimPathFingerprint>;
|
|
114
|
+
allowFreshInstall: boolean;
|
|
115
|
+
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export type CodexShimAutoRestoreResult =
|
|
119
|
+
| { status: "not-installed" | "healthy" | "disabled" }
|
|
120
|
+
| { status: "ineligible" | "deferred"; message?: string }
|
|
121
|
+
| { status: "restored"; message: string };
|
|
122
|
+
|
|
123
|
+
function cliEntry(): { bun: string; cli: string } {
|
|
124
|
+
// Bundled Bun path (survives `ocx update`); all three shim builders
|
|
125
|
+
// (Unix / Windows cmd / Windows PowerShell) receive it via this entry.
|
|
126
|
+
// This module lives in src/codex/, the CLI entry in src/cli/index.ts.
|
|
127
|
+
return { bun: durableBunPath(), cli: join(import.meta.dir, "..", "cli", "index.ts") };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function commandNames(name: string): string[] {
|
|
131
|
+
if (process.platform !== "win32") return [name];
|
|
132
|
+
const exts = (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD;.PS1").split(";").filter(Boolean);
|
|
133
|
+
return [name, ...exts.flatMap(ext => [`${name}${ext.toLowerCase()}`, `${name}${ext.toUpperCase()}`])];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function isShim(path: string): boolean {
|
|
137
|
+
try {
|
|
138
|
+
return readFileSync(path, "utf8").includes(SHIM_MARKER);
|
|
139
|
+
} catch {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function isHealthyShim(path: string, platform: NodeJS.Platform): boolean {
|
|
145
|
+
try {
|
|
146
|
+
const content = readFileSync(path, "utf8");
|
|
147
|
+
if (content.length < 180 || !content.includes(SHIM_MARKER) || !content.includes("ensure")) return false;
|
|
148
|
+
if (platform !== "win32" && (lstatSync(path).mode & 0o111) === 0) return false;
|
|
149
|
+
return true;
|
|
150
|
+
} catch {
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readShimProbePrefix(path: string): string {
|
|
156
|
+
const fd = openSync(path, "r");
|
|
157
|
+
try {
|
|
158
|
+
const buffer = Buffer.allocUnsafe(CODEX_SHIM_PROBE_BYTES);
|
|
159
|
+
const bytesRead = readSync(fd, buffer, 0, buffer.length, 0);
|
|
160
|
+
return buffer.toString("utf8", 0, bytesRead);
|
|
161
|
+
} finally {
|
|
162
|
+
closeSync(fd);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function statFingerprint(path: string, follow: boolean): Omit<ShimPathFingerprint, "target"> | null {
|
|
167
|
+
try {
|
|
168
|
+
const stat = follow ? statSync(path) : lstatSync(path);
|
|
169
|
+
if (follow ? !stat.isFile() : (!stat.isFile() && !stat.isSymbolicLink())) return null;
|
|
170
|
+
return {
|
|
171
|
+
dev: stat.dev,
|
|
172
|
+
ino: stat.ino,
|
|
173
|
+
kind: stat.isSymbolicLink() ? "symlink" : "file",
|
|
174
|
+
mode: stat.mode,
|
|
175
|
+
size: stat.size,
|
|
176
|
+
mtimeMs: stat.mtimeMs,
|
|
177
|
+
ctimeMs: stat.ctimeMs,
|
|
178
|
+
};
|
|
179
|
+
} catch {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sameFingerprint(
|
|
185
|
+
left: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
|
|
186
|
+
right: ShimPathFingerprint | Omit<ShimPathFingerprint, "target">,
|
|
187
|
+
): boolean {
|
|
188
|
+
return left.dev === right.dev
|
|
189
|
+
&& left.ino === right.ino
|
|
190
|
+
&& left.kind === right.kind
|
|
191
|
+
&& left.mode === right.mode
|
|
192
|
+
&& left.size === right.size
|
|
193
|
+
&& left.mtimeMs === right.mtimeMs
|
|
194
|
+
&& left.ctimeMs === right.ctimeMs
|
|
195
|
+
&& (!("target" in left) || !("target" in right)
|
|
196
|
+
? true
|
|
197
|
+
: left.target === undefined && right.target === undefined
|
|
198
|
+
? true
|
|
199
|
+
: left.target !== undefined && right.target !== undefined
|
|
200
|
+
? sameFingerprint(left.target, right.target)
|
|
201
|
+
: false);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function stableShimPathProbe(path: string): StableShimPathProbe | null {
|
|
205
|
+
const before = statFingerprint(path, false);
|
|
206
|
+
if (!before) return null;
|
|
207
|
+
const targetBefore = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
|
|
208
|
+
if (before.kind === "symlink" && !targetBefore) return null;
|
|
209
|
+
let prefix: string;
|
|
210
|
+
try {
|
|
211
|
+
prefix = readShimProbePrefix(path);
|
|
212
|
+
} catch {
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
const targetAfter = before.kind === "symlink" ? statFingerprint(path, true) : undefined;
|
|
216
|
+
const after = statFingerprint(path, false);
|
|
217
|
+
if (!after || !sameFingerprint(before, after)) return null;
|
|
218
|
+
if (before.kind === "symlink") {
|
|
219
|
+
if (!targetBefore || !targetAfter || !sameFingerprint(targetBefore, targetAfter)) return null;
|
|
220
|
+
}
|
|
221
|
+
const fingerprint: ShimPathFingerprint = {
|
|
222
|
+
...before,
|
|
223
|
+
...(targetBefore ? { target: targetBefore } : {}),
|
|
224
|
+
};
|
|
225
|
+
const contentSize = fingerprint.target?.size ?? fingerprint.size;
|
|
226
|
+
return contentSize > 0 ? { fingerprint, prefix } : null;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function sameStableShimPathProbe(left: StableShimPathProbe, right: StableShimPathProbe): boolean {
|
|
230
|
+
return left.prefix === right.prefix && sameFingerprint(left.fingerprint, right.fingerprint);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isHealthyShimProbe(probe: StableShimPathProbe, platform: NodeJS.Platform): boolean {
|
|
234
|
+
if (probe.prefix.length < 180 || !probe.prefix.includes(SHIM_MARKER) || !probe.prefix.includes("ensure")) return false;
|
|
235
|
+
const mode = probe.fingerprint.target?.mode ?? probe.fingerprint.mode;
|
|
236
|
+
return platform === "win32" || (mode & 0o111) !== 0;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function hasUsableBackingPath(file: ShimFileState): boolean {
|
|
240
|
+
return [existsSync(file.backupPath) ? file.backupPath : undefined, file.realPath]
|
|
241
|
+
.some(path => {
|
|
242
|
+
if (!path) return false;
|
|
243
|
+
const fingerprint = statFingerprint(path, true);
|
|
244
|
+
return fingerprint !== null && fingerprint.size > 0;
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* A PATH entry that reaches Windows through WSL drive interop
|
|
250
|
+
* (`<automount-root>/<drive>/...`; root defaults to /mnt, configurable via
|
|
251
|
+
* /etc/wsl.conf [automount] root).
|
|
252
|
+
*/
|
|
253
|
+
export function isWindowsInteropDir(dir: string, automountRoot = "/mnt"): boolean {
|
|
254
|
+
const root = automountRoot.replace(/\/+$/, "");
|
|
255
|
+
const escaped = root.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
256
|
+
return new RegExp(`^${escaped}/[a-z](/|$)`, "i").test(dir);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export type CodexPathScanDeps = {
|
|
260
|
+
pathValue?: string;
|
|
261
|
+
wsl?: boolean;
|
|
262
|
+
/** Treat PATH entries as POSIX paths (WSL context). Defaults to wsl || non-win32. */
|
|
263
|
+
posixPaths?: boolean;
|
|
264
|
+
automountRoot?: string;
|
|
265
|
+
exists?: (path: string) => boolean;
|
|
266
|
+
isShimFile?: (path: string) => boolean;
|
|
267
|
+
isDirectory?: (path: string) => boolean;
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
function realIsDirectory(path: string): boolean {
|
|
271
|
+
try {
|
|
272
|
+
return lstatSync(path).isDirectory();
|
|
273
|
+
} catch {
|
|
274
|
+
return true; // unreadable -> treat as unusable
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
export function findCodexOnPath(deps: CodexPathScanDeps = {}): string | null {
|
|
279
|
+
lastShimDiscoveryError = null;
|
|
280
|
+
const exists = deps.exists ?? existsSync;
|
|
281
|
+
const shimFile = deps.isShimFile ?? isShim;
|
|
282
|
+
const isDir = deps.isDirectory ?? realIsDirectory;
|
|
283
|
+
const wsl = deps.wsl ?? (process.platform === "linux" && isWslRuntime());
|
|
284
|
+
const usePosix = deps.posixPaths ?? (wsl || process.platform !== "win32");
|
|
285
|
+
const joinPath = usePosix ? posix.join : join;
|
|
286
|
+
const pathSep = usePosix ? ":" : delimiter;
|
|
287
|
+
const automountRoot = deps.automountRoot ?? (wsl ? wslAutomountRoot() : "/mnt");
|
|
288
|
+
// Windows npm prefixes ship codex.exe/codex.cmd next to the extensionless sh launcher.
|
|
289
|
+
const interopNames = ["codex", "codex.exe", "codex.cmd", "codex.ps1"];
|
|
290
|
+
let skippedInterop: string | null = null;
|
|
291
|
+
|
|
292
|
+
for (const dir of (deps.pathValue ?? process.env.PATH ?? "").split(pathSep).filter(Boolean)) {
|
|
293
|
+
if (wsl && isWindowsInteropDir(dir, automountRoot)) {
|
|
294
|
+
// A Windows-side codex reached through WSL PATH interop: a Unix shim written
|
|
295
|
+
// here would embed WSL-only paths and break every Windows-side invocation.
|
|
296
|
+
if (!skippedInterop) {
|
|
297
|
+
for (const name of interopNames) {
|
|
298
|
+
const path = joinPath(dir, name);
|
|
299
|
+
if (exists(path) && !shimFile(path) && !isDir(path)) { skippedInterop = path; break; }
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
// Interop dirs carry Windows launcher names even when the scan is not skipping them.
|
|
305
|
+
const names = isWindowsInteropDir(dir, automountRoot) ? interopNames : commandNames("codex");
|
|
306
|
+
for (const name of names) {
|
|
307
|
+
const path = joinPath(dir, name);
|
|
308
|
+
if (!exists(path) || shimFile(path)) continue;
|
|
309
|
+
if (!isDir(path)) return path;
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (skippedInterop) {
|
|
314
|
+
lastShimDiscoveryError = truncateRetainedUtf8(
|
|
315
|
+
`Found a Windows codex at ${skippedInterop} via WSL PATH interop, but no Linux-side codex. ` +
|
|
316
|
+
"Refusing to shim a Windows launcher from WSL (a WSL shim breaks Windows invocations). " +
|
|
317
|
+
"Install codex inside WSL (npm i -g @openai/codex), or run 'ocx ensure' from Windows to shim the Windows side.",
|
|
318
|
+
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
319
|
+
);
|
|
320
|
+
}
|
|
321
|
+
return null;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
function findWindowsCodexTargets(): ShimFileState[] | null {
|
|
325
|
+
lastShimDiscoveryError = null;
|
|
326
|
+
for (const dir of (process.env.PATH ?? "").split(delimiter).filter(Boolean)) {
|
|
327
|
+
const exe = join(dir, "codex.exe");
|
|
328
|
+
if (existsSync(exe) && !isShim(exe)) {
|
|
329
|
+
try {
|
|
330
|
+
if (!lstatSync(exe).isDirectory()) {
|
|
331
|
+
lastShimDiscoveryError = truncateRetainedUtf8(
|
|
332
|
+
`Found codex.exe at ${exe}. Refusing to rename a real .exe because exact codex.exe invocations would break; ` +
|
|
333
|
+
"install a codex.cmd/codex.ps1 launcher or use `ocx service install` for autostart.",
|
|
334
|
+
MAX_DIAGNOSTIC_VALUE_BYTES,
|
|
335
|
+
);
|
|
336
|
+
return null;
|
|
337
|
+
}
|
|
338
|
+
} catch { /* keep scanning */ }
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const cmd = join(dir, "codex.cmd");
|
|
342
|
+
const ps1 = join(dir, "codex.ps1");
|
|
343
|
+
// npm also installs an extensionless `codex` sh launcher for Git-Bash/MSYS shells;
|
|
344
|
+
// leaving it unshimmed means Git-Bash users silently get no autostart.
|
|
345
|
+
const gitBashLauncher = join(dir, "codex");
|
|
346
|
+
const targets: ShimFileState[] = [];
|
|
347
|
+
for (const path of [cmd, ps1, gitBashLauncher]) {
|
|
348
|
+
if (!existsSync(path) || isShim(path)) continue;
|
|
349
|
+
try {
|
|
350
|
+
if (!lstatSync(path).isDirectory()) {
|
|
351
|
+
targets.push({ wrapperPath: path, originalPath: path, backupPath: backupPathFor(path) });
|
|
352
|
+
}
|
|
353
|
+
} catch { /* keep scanning */ }
|
|
354
|
+
}
|
|
355
|
+
if (targets.length > 0) return targets;
|
|
356
|
+
}
|
|
357
|
+
return null;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function backupPathFor(path: string): string {
|
|
361
|
+
const ext = extname(path);
|
|
362
|
+
return ext ? `${path.slice(0, -ext.length)}.opencodex-real${ext}` : `${path}.opencodex-real`;
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function shQuote(value: string): string {
|
|
366
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
export function buildUnixCodexShim(realCodexPath: string, bunPath: string, cliPath: string, tokenFile = serviceApiTokenFilePath()): string {
|
|
370
|
+
const internalCommands = CODEX_INTERNAL_COMMANDS.join("|");
|
|
371
|
+
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.join("|");
|
|
372
|
+
return `#!/usr/bin/env sh
|
|
373
|
+
# ${SHIM_MARKER}
|
|
374
|
+
if [ -z "$OPENCODEX_API_AUTH_TOKEN" ] && [ -f ${shQuote(tokenFile)} ]; then
|
|
375
|
+
OPENCODEX_API_AUTH_TOKEN="$(cat ${shQuote(tokenFile)})"
|
|
376
|
+
export OPENCODEX_API_AUTH_TOKEN
|
|
377
|
+
fi
|
|
378
|
+
ocx_subcommand=""
|
|
379
|
+
ocx_skip_next=0
|
|
380
|
+
for ocx_arg in "$@"; do
|
|
381
|
+
if [ "$ocx_skip_next" -eq 1 ]; then
|
|
382
|
+
ocx_skip_next=0
|
|
383
|
+
continue
|
|
384
|
+
fi
|
|
385
|
+
case "$ocx_arg" in
|
|
386
|
+
--)
|
|
387
|
+
break
|
|
388
|
+
;;
|
|
389
|
+
${valueOptions})
|
|
390
|
+
ocx_skip_next=1
|
|
391
|
+
;;
|
|
392
|
+
--help|-h|--version|-V)
|
|
393
|
+
ocx_subcommand="$ocx_arg"
|
|
394
|
+
break
|
|
395
|
+
;;
|
|
396
|
+
-*)
|
|
397
|
+
;;
|
|
398
|
+
*)
|
|
399
|
+
ocx_subcommand="$ocx_arg"
|
|
400
|
+
break
|
|
401
|
+
;;
|
|
402
|
+
esac
|
|
403
|
+
done
|
|
404
|
+
case "$ocx_subcommand" in
|
|
405
|
+
${internalCommands}|--help|-h|--version|-V)
|
|
406
|
+
;;
|
|
407
|
+
*)
|
|
408
|
+
if [ -z "$OCX_SHIM_BYPASS" ]; then
|
|
409
|
+
${shQuote(bunPath)} ${shQuote(cliPath)} ensure >/dev/null 2>&1 || true
|
|
410
|
+
fi
|
|
411
|
+
;;
|
|
412
|
+
esac
|
|
413
|
+
exec ${shQuote(realCodexPath)} "$@"
|
|
414
|
+
`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function windowsBatchValue(value: string): string {
|
|
418
|
+
return value
|
|
419
|
+
.replace(/%/g, "%%")
|
|
420
|
+
.replace(/\^/g, "^^")
|
|
421
|
+
.replace(/"/g, "")
|
|
422
|
+
.replace(/[\r\n]/g, "");
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function windowsBatchSet(name: string, value: string): string {
|
|
426
|
+
// Paths are rewritten to %USERPROFILE%-style env indirection: cmd.exe parses .cmd
|
|
427
|
+
// files in the OEM codepage, so a literal non-ASCII profile prefix (Korean/Chinese
|
|
428
|
+
// usernames) written as UTF-8 turns to mojibake. The env token expands natively in
|
|
429
|
+
// the right codepage at parse time; no `chcp` here — this shim runs in the USER's
|
|
430
|
+
// console and must not leak a codepage change into it.
|
|
431
|
+
return `set "${name}=${windowsEnvIndirectBatchValue(value, windowsBatchValue)}"`;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
export function buildWindowsCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
435
|
+
const internalCommandChecks = CODEX_INTERNAL_COMMANDS.map(command => `if /I "%~1"=="${command}" goto run_codex`).join("\r\n");
|
|
436
|
+
const valueOptionChecks = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => `if /I "%~1"=="${option}" goto skip_option_value`).join("\r\n");
|
|
437
|
+
return `@echo off\r
|
|
438
|
+
rem ${SHIM_MARKER}\r
|
|
439
|
+
${windowsBatchSet("OCX_REAL_CODEX", realCodexPath)}\r
|
|
440
|
+
${windowsBatchSet("OCX_BUN", bunPath)}\r
|
|
441
|
+
${windowsBatchSet("OCX_CLI", cliPath)}\r
|
|
442
|
+
${windowsBatchSet("OCX_API_TOKEN_FILE", serviceApiTokenFilePath())}\r
|
|
443
|
+
if "%OPENCODEX_API_AUTH_TOKEN%"=="" if exist "%OCX_API_TOKEN_FILE%" set /p OPENCODEX_API_AUTH_TOKEN=<"%OCX_API_TOKEN_FILE%"\r
|
|
444
|
+
if not "%OCX_SHIM_BYPASS%"=="" goto run_codex\r
|
|
445
|
+
goto scan_codex_args\r
|
|
446
|
+
:scan_codex_args\r
|
|
447
|
+
if "%~1"=="" goto ensure_ocx\r
|
|
448
|
+
if "%~1"=="--" goto ensure_ocx\r
|
|
449
|
+
${valueOptionChecks}\r
|
|
450
|
+
${internalCommandChecks}\r
|
|
451
|
+
if /I "%~1"=="--help" goto run_codex\r
|
|
452
|
+
if /I "%~1"=="-h" goto run_codex\r
|
|
453
|
+
if /I "%~1"=="--version" goto run_codex\r
|
|
454
|
+
if /I "%~1"=="-V" goto run_codex\r
|
|
455
|
+
set "OCX_SCAN_ARG=%~1"\r
|
|
456
|
+
if "%OCX_SCAN_ARG:~0,1%"=="-" goto shift_codex_arg\r
|
|
457
|
+
goto ensure_ocx\r
|
|
458
|
+
:skip_option_value\r
|
|
459
|
+
shift\r
|
|
460
|
+
if "%~1"=="" goto ensure_ocx\r
|
|
461
|
+
:shift_codex_arg\r
|
|
462
|
+
shift\r
|
|
463
|
+
goto scan_codex_args\r
|
|
464
|
+
:ensure_ocx\r
|
|
465
|
+
"%OCX_BUN%" "%OCX_CLI%" ensure >nul 2>nul\r
|
|
466
|
+
:run_codex\r
|
|
467
|
+
"%OCX_REAL_CODEX%" %*\r
|
|
468
|
+
`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
function psString(value: string): string {
|
|
472
|
+
return `'${value.replace(/'/g, "''")}'`;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
export function buildWindowsPowerShellCodexShim(realCodexPath: string, bunPath: string, cliPath: string): string {
|
|
476
|
+
const internalCommands = CODEX_INTERNAL_COMMANDS.map(command => psString(command)).join(", ");
|
|
477
|
+
const valueOptions = CODEX_GLOBAL_OPTIONS_WITH_VALUE.map(option => psString(option)).join(", ");
|
|
478
|
+
const tokenFile = serviceApiTokenFilePath();
|
|
479
|
+
return `#!/usr/bin/env pwsh
|
|
480
|
+
# ${SHIM_MARKER}
|
|
481
|
+
if (-not $env:OPENCODEX_API_AUTH_TOKEN -and (Test-Path -LiteralPath ${psString(tokenFile)})) {
|
|
482
|
+
$env:OPENCODEX_API_AUTH_TOKEN = (Get-Content -Raw -LiteralPath ${psString(tokenFile)}).Trim()
|
|
483
|
+
}
|
|
484
|
+
$internalCommands = @(${internalCommands})
|
|
485
|
+
$valueOptions = @(${valueOptions})
|
|
486
|
+
$subcommand = ""
|
|
487
|
+
$skipNext = $false
|
|
488
|
+
foreach ($argValue in $args) {
|
|
489
|
+
$argText = [string]$argValue
|
|
490
|
+
if ($skipNext) { $skipNext = $false; continue }
|
|
491
|
+
if ($argText -eq "--") { break }
|
|
492
|
+
if ($valueOptions -contains $argText) { $skipNext = $true; continue }
|
|
493
|
+
if (@("--help", "-h", "--version", "-V") -contains $argText) { $subcommand = $argText; break }
|
|
494
|
+
if ($argText.StartsWith("-")) { continue }
|
|
495
|
+
$subcommand = $argText
|
|
496
|
+
break
|
|
497
|
+
}
|
|
498
|
+
$skipEnsure = $env:OCX_SHIM_BYPASS -or $internalCommands -contains $subcommand -or @("--help", "-h", "--version", "-V") -contains $subcommand
|
|
499
|
+
if (-not $skipEnsure) {
|
|
500
|
+
& ${psString(bunPath)} ${psString(cliPath)} ensure *> $null
|
|
501
|
+
}
|
|
502
|
+
& ${psString(realCodexPath)} @args
|
|
503
|
+
exit $LASTEXITCODE
|
|
504
|
+
`;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
interface ShimStateReadResult {
|
|
508
|
+
state: ShimState | null;
|
|
509
|
+
warning?: string;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function fileErrorCode(error: unknown): string | undefined {
|
|
513
|
+
return error && typeof error === "object" && "code" in error
|
|
514
|
+
? String((error as { code?: unknown }).code)
|
|
515
|
+
: undefined;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function readBoundedRegularFile(path: string, maxBytes: number): { content: string } | { warning: string } | null {
|
|
519
|
+
let fd: number;
|
|
520
|
+
try {
|
|
521
|
+
fd = openSync(path, "r");
|
|
522
|
+
} catch (error) {
|
|
523
|
+
if (fileErrorCode(error) === "ENOENT") return null;
|
|
524
|
+
return { warning: `Codex shim state could not be opened as a regular file at ${path}.` };
|
|
525
|
+
}
|
|
526
|
+
try {
|
|
527
|
+
const before = fstatSync(fd);
|
|
528
|
+
if (!before.isFile()) return { warning: `Codex shim state is not a regular file at ${path}; auto-restore skipped.` };
|
|
529
|
+
if (before.size > maxBytes) {
|
|
530
|
+
return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
|
|
531
|
+
}
|
|
532
|
+
const buffer = Buffer.allocUnsafe(before.size);
|
|
533
|
+
let offset = 0;
|
|
534
|
+
while (offset < buffer.length) {
|
|
535
|
+
const bytesRead = readSync(fd, buffer, offset, buffer.length - offset, offset);
|
|
536
|
+
if (bytesRead === 0) return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
537
|
+
offset += bytesRead;
|
|
538
|
+
}
|
|
539
|
+
const extra = Buffer.allocUnsafe(1);
|
|
540
|
+
if (readSync(fd, extra, 0, 1, offset) !== 0) {
|
|
541
|
+
return { warning: `Codex shim state exceeds the 1 MiB startup limit at ${path}; auto-restore skipped.` };
|
|
542
|
+
}
|
|
543
|
+
const after = fstatSync(fd);
|
|
544
|
+
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size
|
|
545
|
+
|| before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs) {
|
|
546
|
+
return { warning: `Codex shim state changed while being read at ${path}; auto-restore skipped.` };
|
|
547
|
+
}
|
|
548
|
+
return { content: buffer.toString("utf8") };
|
|
549
|
+
} finally {
|
|
550
|
+
closeSync(fd);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
function readStateResult(): ShimStateReadResult {
|
|
555
|
+
const bounded = readBoundedRegularFile(statePath(), CODEX_SHIM_STATE_MAX_BYTES);
|
|
556
|
+
if (!bounded) return { state: null };
|
|
557
|
+
if ("warning" in bounded) return { state: null, warning: bounded.warning };
|
|
558
|
+
try {
|
|
559
|
+
const value = JSON.parse(bounded.content) as unknown;
|
|
560
|
+
if (!value || typeof value !== "object") return { state: null };
|
|
561
|
+
const state = value as Record<string, unknown>;
|
|
562
|
+
if (typeof state.platform !== "string") return { state: null };
|
|
563
|
+
const validFile = (item: unknown): item is ShimFileState => {
|
|
564
|
+
if (!item || typeof item !== "object") return false;
|
|
565
|
+
const file = item as Record<string, unknown>;
|
|
566
|
+
return typeof file.wrapperPath === "string"
|
|
567
|
+
&& typeof file.originalPath === "string"
|
|
568
|
+
&& typeof file.backupPath === "string"
|
|
569
|
+
&& (file.realPath === undefined || typeof file.realPath === "string")
|
|
570
|
+
&& (file.preserveOnly === undefined || typeof file.preserveOnly === "boolean");
|
|
571
|
+
};
|
|
572
|
+
if (state.wrappers !== undefined) {
|
|
573
|
+
if (!Array.isArray(state.wrappers) || state.wrappers.length === 0 || !state.wrappers.every(validFile)) return { state: null };
|
|
574
|
+
} else if (!validFile(state)) {
|
|
575
|
+
return { state: null };
|
|
576
|
+
}
|
|
577
|
+
return { state: state as unknown as ShimState };
|
|
578
|
+
} catch {
|
|
579
|
+
return { state: null };
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function readState(): ShimState | null {
|
|
584
|
+
return readStateResult().state;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
function statePath(): string {
|
|
588
|
+
return join(getConfigDir(), "codex-shim.json");
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function writeState(state: ShimState): void {
|
|
592
|
+
const path = statePath();
|
|
593
|
+
recordOwnedConfigPath(getConfigDir(), path);
|
|
594
|
+
if (!existsSync(getConfigDir())) mkdirSync(getConfigDir(), { recursive: true });
|
|
595
|
+
writeFileSync(path, JSON.stringify(state, null, 2) + "\n", "utf8");
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/** Git-Bash accepts `C:/...` but not backslashed paths inside sh scripts. */
|
|
599
|
+
function gitBashPath(path: string): string {
|
|
600
|
+
return path.replace(/\\/g, "/");
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
function writeShim(wrapperPath: string, realCodexPath: string): void {
|
|
604
|
+
const { bun, cli } = cliEntry();
|
|
605
|
+
if (process.platform === "win32") {
|
|
606
|
+
const lower = wrapperPath.toLowerCase();
|
|
607
|
+
if (lower.endsWith(".ps1")) {
|
|
608
|
+
// UTF-8 BOM: Windows PowerShell 5.1 decodes BOM-less .ps1 files in the ANSI
|
|
609
|
+
// codepage, which mangles non-ASCII paths embedded in the shim.
|
|
610
|
+
writeFileSync(wrapperPath, `\uFEFF${buildWindowsPowerShellCodexShim(realCodexPath, bun, cli)}`, "utf8");
|
|
611
|
+
} else if (lower.endsWith(".cmd") || lower.endsWith(".bat")) {
|
|
612
|
+
writeFileSync(wrapperPath, buildWindowsCodexShim(realCodexPath, bun, cli), "utf8");
|
|
613
|
+
} else {
|
|
614
|
+
// Extensionless Git-Bash sh launcher: sh shim with forward-slash paths.
|
|
615
|
+
writeFileSync(
|
|
616
|
+
wrapperPath,
|
|
617
|
+
buildUnixCodexShim(gitBashPath(realCodexPath), gitBashPath(bun), gitBashPath(cli), gitBashPath(serviceApiTokenFilePath())),
|
|
618
|
+
"utf8",
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
} else {
|
|
622
|
+
writeFileSync(wrapperPath, buildUnixCodexShim(realCodexPath, bun, cli), "utf8");
|
|
623
|
+
chmodSync(wrapperPath, 0o755);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
function stateFiles(state: ShimState): ShimFileState[] {
|
|
628
|
+
return state.wrappers?.length
|
|
629
|
+
? state.wrappers
|
|
630
|
+
: [{ wrapperPath: state.wrapperPath, originalPath: state.originalPath, backupPath: state.backupPath }];
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function primaryState(files: ShimFileState[]): ShimState {
|
|
634
|
+
const first = files[0];
|
|
635
|
+
return { platform: process.platform, ...first, wrappers: files };
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
function replaceOwnedBackup(sourcePath: string, backupPath: string): void {
|
|
639
|
+
const oldBackupPath = `${backupPath}.old-${process.pid}`;
|
|
640
|
+
if (existsSync(oldBackupPath)) unlinkSync(oldBackupPath);
|
|
641
|
+
if (existsSync(backupPath)) renameSync(backupPath, oldBackupPath);
|
|
642
|
+
try {
|
|
643
|
+
renameSync(sourcePath, backupPath);
|
|
644
|
+
if (existsSync(oldBackupPath)) unlinkSync(oldBackupPath);
|
|
645
|
+
} catch (error) {
|
|
646
|
+
if (!existsSync(backupPath) && existsSync(oldBackupPath)) renameSync(oldBackupPath, backupPath);
|
|
647
|
+
throw error;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function refreshShimFile(file: ShimFileState): boolean {
|
|
652
|
+
if (file.preserveOnly) {
|
|
653
|
+
if (existsSync(file.originalPath) && !isShim(file.originalPath)) {
|
|
654
|
+
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
655
|
+
return true;
|
|
656
|
+
}
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
if (existsSync(file.wrapperPath) && !isShim(file.wrapperPath)) {
|
|
660
|
+
if (file.wrapperPath !== file.originalPath) return false;
|
|
661
|
+
replaceOwnedBackup(file.wrapperPath, file.backupPath);
|
|
662
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
663
|
+
return true;
|
|
664
|
+
}
|
|
665
|
+
if (!existsSync(file.wrapperPath) && existsSync(file.backupPath)) {
|
|
666
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
667
|
+
return true;
|
|
668
|
+
}
|
|
669
|
+
if (file.originalPath !== file.wrapperPath && existsSync(file.originalPath) && existsSync(file.wrapperPath) && isShim(file.wrapperPath)) {
|
|
670
|
+
replaceOwnedBackup(file.originalPath, file.backupPath);
|
|
671
|
+
writeShim(file.wrapperPath, file.realPath ?? file.backupPath);
|
|
672
|
+
return true;
|
|
673
|
+
}
|
|
674
|
+
return false;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
interface GuardedRefreshOperation {
|
|
678
|
+
file: ShimFileState;
|
|
679
|
+
expectedReplacement: ShimPathFingerprint;
|
|
680
|
+
sourcePath: string;
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
interface GuardedRefreshJournalEntry {
|
|
684
|
+
operation: GuardedRefreshOperation;
|
|
685
|
+
stagedOldBackupPath?: string;
|
|
686
|
+
replacementMovedToBackup: boolean;
|
|
687
|
+
wrapperWriteStarted: boolean;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
let guardedRefreshTransactionId = 0;
|
|
691
|
+
|
|
692
|
+
interface ShimRestoreLock {
|
|
693
|
+
release(): void;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
interface ShimRestoreLockRecord {
|
|
697
|
+
version: 1;
|
|
698
|
+
token: string;
|
|
699
|
+
pid: number;
|
|
700
|
+
createdAt: number;
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
interface ShimRestoreLockSnapshot {
|
|
704
|
+
record: ShimRestoreLockRecord;
|
|
705
|
+
ownerPath: string;
|
|
706
|
+
lockIdentity: Pick<Stats, "dev" | "ino">;
|
|
707
|
+
fingerprint: ShimPathFingerprint;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
function restoreLockPath(): string {
|
|
711
|
+
return join(getConfigDir(), "codex-shim.autorestore.lock");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function sameFileIdentity(left: Pick<Stats, "dev" | "ino">, right: Pick<Stats, "dev" | "ino">): boolean {
|
|
715
|
+
return left.dev === right.dev && left.ino === right.ino;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
function readShimRestoreLockSnapshot(path: string): ShimRestoreLockSnapshot | null {
|
|
719
|
+
let lockIdentity: Stats;
|
|
720
|
+
let entries: string[];
|
|
721
|
+
try {
|
|
722
|
+
lockIdentity = lstatSync(path);
|
|
723
|
+
if (!lockIdentity.isDirectory()) return null;
|
|
724
|
+
entries = readdirSync(path);
|
|
725
|
+
} catch {
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
if (entries.length !== 1 || !entries[0].endsWith(".json")) return null;
|
|
729
|
+
const ownerPath = join(path, entries[0]);
|
|
730
|
+
const probe = stableShimPathProbe(ownerPath);
|
|
731
|
+
if (!probe || probe.fingerprint.kind !== "file" || probe.fingerprint.size > 4096) return null;
|
|
732
|
+
try {
|
|
733
|
+
const value = JSON.parse(probe.prefix) as Partial<ShimRestoreLockRecord>;
|
|
734
|
+
if (value.version !== 1 || typeof value.token !== "string" || value.token.length === 0
|
|
735
|
+
|| typeof value.pid !== "number" || !Number.isSafeInteger(value.pid) || value.pid <= 0
|
|
736
|
+
|| typeof value.createdAt !== "number" || !Number.isFinite(value.createdAt)) return null;
|
|
737
|
+
if (entries[0] !== `${value.token}.json`) return null;
|
|
738
|
+
const currentLockIdentity = lstatSync(path);
|
|
739
|
+
if (!currentLockIdentity.isDirectory() || !sameFileIdentity(lockIdentity, currentLockIdentity)) return null;
|
|
740
|
+
return {
|
|
741
|
+
record: value as ShimRestoreLockRecord,
|
|
742
|
+
ownerPath,
|
|
743
|
+
lockIdentity,
|
|
744
|
+
fingerprint: probe.fingerprint,
|
|
745
|
+
};
|
|
746
|
+
} catch {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
function sameShimRestoreLock(left: ShimRestoreLockSnapshot, right: ShimRestoreLockSnapshot): boolean {
|
|
752
|
+
return left.record.token === right.record.token
|
|
753
|
+
&& sameFileIdentity(left.lockIdentity, right.lockIdentity)
|
|
754
|
+
&& sameFingerprint(left.fingerprint, right.fingerprint);
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
function reclaimStaleRestoreLock(path: string, beforeDelete?: () => void): boolean {
|
|
758
|
+
const observed = readShimRestoreLockSnapshot(path);
|
|
759
|
+
if (!observed) return false;
|
|
760
|
+
const createdAt = Math.max(observed.record.createdAt, observed.fingerprint.mtimeMs);
|
|
761
|
+
if (Date.now() - createdAt <= CODEX_SHIM_RESTORE_LOCK_STALE_MS) return false;
|
|
762
|
+
if (isProcessAlive(observed.record.pid)) return false;
|
|
763
|
+
const current = readShimRestoreLockSnapshot(path);
|
|
764
|
+
if (!current || !sameShimRestoreLock(observed, current)) return false;
|
|
765
|
+
beforeDelete?.();
|
|
766
|
+
try {
|
|
767
|
+
// The token is part of the owner filename. Even if the lock directory is
|
|
768
|
+
// replaced after the comparison, this unlink cannot target a successor's
|
|
769
|
+
// differently named owner record.
|
|
770
|
+
unlinkSync(observed.ownerPath);
|
|
771
|
+
rmdirSync(path);
|
|
772
|
+
return true;
|
|
773
|
+
} catch {
|
|
774
|
+
return false;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
function tryAcquireShimRestoreLock(beforeStaleDelete?: () => void): ShimRestoreLock | null {
|
|
779
|
+
const dir = getConfigDir();
|
|
780
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
781
|
+
const path = restoreLockPath();
|
|
782
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
783
|
+
let fd: number | null = null;
|
|
784
|
+
let identity: Stats | null = null;
|
|
785
|
+
let createdDirectory = false;
|
|
786
|
+
const record: ShimRestoreLockRecord = {
|
|
787
|
+
version: 1,
|
|
788
|
+
token: `${process.pid}-${Date.now()}-${randomUUID()}`,
|
|
789
|
+
pid: process.pid,
|
|
790
|
+
createdAt: Date.now(),
|
|
791
|
+
};
|
|
792
|
+
const ownerPath = join(path, `${record.token}.json`);
|
|
793
|
+
try {
|
|
794
|
+
mkdirSync(path, { mode: 0o700 });
|
|
795
|
+
createdDirectory = true;
|
|
796
|
+
fd = openSync(ownerPath, "wx", 0o600);
|
|
797
|
+
identity = fstatSync(fd);
|
|
798
|
+
writeFileSync(fd, `${JSON.stringify(record)}\n`, "utf8");
|
|
799
|
+
identity = fstatSync(fd);
|
|
800
|
+
let released = false;
|
|
801
|
+
return {
|
|
802
|
+
release(): void {
|
|
803
|
+
if (released) return;
|
|
804
|
+
released = true;
|
|
805
|
+
try { closeSync(fd!); } catch { /* stale recovery handles an uncertain lock */ }
|
|
806
|
+
try {
|
|
807
|
+
const current = readShimRestoreLockSnapshot(path);
|
|
808
|
+
if (identity && current && current.record.token === record.token
|
|
809
|
+
&& sameFileIdentity(identity, current.fingerprint)) {
|
|
810
|
+
unlinkSync(ownerPath);
|
|
811
|
+
rmdirSync(path);
|
|
812
|
+
}
|
|
813
|
+
} catch { /* stale recovery handles release failures */ }
|
|
814
|
+
},
|
|
815
|
+
};
|
|
816
|
+
} catch (error) {
|
|
817
|
+
if (fd !== null) {
|
|
818
|
+
try { closeSync(fd); } catch { /* best-effort close before ownership cleanup */ }
|
|
819
|
+
try {
|
|
820
|
+
const current = readShimRestoreLockSnapshot(path);
|
|
821
|
+
if (identity && current && current.record.token === record.token
|
|
822
|
+
&& sameFileIdentity(identity, current.fingerprint)) {
|
|
823
|
+
unlinkSync(ownerPath);
|
|
824
|
+
rmdirSync(path);
|
|
825
|
+
}
|
|
826
|
+
} catch { /* leave an uncertain lock for stale recovery */ }
|
|
827
|
+
} else if (createdDirectory) {
|
|
828
|
+
try { rmdirSync(path); } catch { /* another owner exists or cleanup is uncertain */ }
|
|
829
|
+
}
|
|
830
|
+
if (fileErrorCode(error) !== "EEXIST") throw error;
|
|
831
|
+
if (attempt === 0 && reclaimStaleRestoreLock(path, beforeStaleDelete)) continue;
|
|
832
|
+
return null;
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
return null;
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
function planGuardedRefreshTransaction(
|
|
839
|
+
files: readonly ShimFileState[],
|
|
840
|
+
expectedReplacements: ReadonlyMap<string, ShimPathFingerprint>,
|
|
841
|
+
): GuardedRefreshOperation[] | null {
|
|
842
|
+
const operations: GuardedRefreshOperation[] = [];
|
|
843
|
+
const seen = new Set<string>();
|
|
844
|
+
for (const file of files) {
|
|
845
|
+
if (seen.has(file.wrapperPath)) return null;
|
|
846
|
+
seen.add(file.wrapperPath);
|
|
847
|
+
if (file.preserveOnly) {
|
|
848
|
+
if (!existsSync(file.backupPath) || existsSync(file.originalPath)) return null;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
if (!hasUsableBackingPath(file)) return null;
|
|
852
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
853
|
+
if (!probe) return null;
|
|
854
|
+
const expectedReplacement = expectedReplacements.get(file.wrapperPath);
|
|
855
|
+
if (!expectedReplacement) {
|
|
856
|
+
if (!isHealthyShimProbe(probe, process.platform)) return null;
|
|
857
|
+
continue;
|
|
858
|
+
}
|
|
859
|
+
if (file.wrapperPath !== file.originalPath
|
|
860
|
+
|| probe.prefix.includes(SHIM_MARKER)
|
|
861
|
+
|| !sameFingerprint(probe.fingerprint, expectedReplacement)) return null;
|
|
862
|
+
operations.push({ file, expectedReplacement, sourcePath: file.wrapperPath });
|
|
863
|
+
}
|
|
864
|
+
if (operations.length !== expectedReplacements.size) return null;
|
|
865
|
+
return operations;
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
function rollbackGuardedRefresh(journal: readonly GuardedRefreshJournalEntry[]): Error[] {
|
|
869
|
+
const rollbackErrors: Error[] = [];
|
|
870
|
+
const attempt = (operation: () => void): void => {
|
|
871
|
+
try {
|
|
872
|
+
operation();
|
|
873
|
+
} catch (error) {
|
|
874
|
+
rollbackErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
875
|
+
}
|
|
876
|
+
};
|
|
877
|
+
for (const entry of [...journal].reverse()) {
|
|
878
|
+
attempt(() => {
|
|
879
|
+
if (entry.wrapperWriteStarted && existsSync(entry.operation.file.wrapperPath)) {
|
|
880
|
+
unlinkSync(entry.operation.file.wrapperPath);
|
|
881
|
+
}
|
|
882
|
+
});
|
|
883
|
+
attempt(() => {
|
|
884
|
+
if (entry.replacementMovedToBackup && existsSync(entry.operation.file.backupPath)) {
|
|
885
|
+
renameSync(entry.operation.file.backupPath, entry.operation.sourcePath);
|
|
886
|
+
}
|
|
887
|
+
});
|
|
888
|
+
attempt(() => {
|
|
889
|
+
if (entry.stagedOldBackupPath && existsSync(entry.stagedOldBackupPath)) {
|
|
890
|
+
renameSync(entry.stagedOldBackupPath, entry.operation.file.backupPath);
|
|
891
|
+
}
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
return rollbackErrors;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function applyGuardedRefreshTransaction(
|
|
898
|
+
operations: readonly GuardedRefreshOperation[],
|
|
899
|
+
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void,
|
|
900
|
+
commitState?: () => void,
|
|
901
|
+
): boolean {
|
|
902
|
+
const journal: GuardedRefreshJournalEntry[] = [];
|
|
903
|
+
let applyError: Error | null = null;
|
|
904
|
+
let fingerprintMismatch = false;
|
|
905
|
+
const transactionId = `${process.pid}-${++guardedRefreshTransactionId}`;
|
|
906
|
+
|
|
907
|
+
for (const [index, operation] of operations.entries()) {
|
|
908
|
+
beforeGuardedRefresh?.(operation.sourcePath, index);
|
|
909
|
+
const probe = stableShimPathProbe(operation.sourcePath);
|
|
910
|
+
if (!probe || !sameFingerprint(probe.fingerprint, operation.expectedReplacement)) {
|
|
911
|
+
fingerprintMismatch = true;
|
|
912
|
+
break;
|
|
913
|
+
}
|
|
914
|
+
const entry: GuardedRefreshJournalEntry = {
|
|
915
|
+
operation,
|
|
916
|
+
replacementMovedToBackup: false,
|
|
917
|
+
wrapperWriteStarted: false,
|
|
918
|
+
};
|
|
919
|
+
journal.push(entry);
|
|
920
|
+
try {
|
|
921
|
+
if (existsSync(operation.file.backupPath)) {
|
|
922
|
+
entry.stagedOldBackupPath = `${operation.file.backupPath}.autorestore-${transactionId}-${index}`;
|
|
923
|
+
if (existsSync(entry.stagedOldBackupPath)) unlinkSync(entry.stagedOldBackupPath);
|
|
924
|
+
renameSync(operation.file.backupPath, entry.stagedOldBackupPath);
|
|
925
|
+
}
|
|
926
|
+
renameSync(operation.sourcePath, operation.file.backupPath);
|
|
927
|
+
entry.replacementMovedToBackup = true;
|
|
928
|
+
entry.wrapperWriteStarted = true;
|
|
929
|
+
writeShim(operation.file.wrapperPath, operation.file.realPath ?? operation.file.backupPath);
|
|
930
|
+
} catch (error) {
|
|
931
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
932
|
+
break;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
if (!fingerprintMismatch && !applyError && commitState) {
|
|
937
|
+
try {
|
|
938
|
+
commitState();
|
|
939
|
+
} catch (error) {
|
|
940
|
+
applyError = error instanceof Error ? error : new Error(String(error));
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
|
|
944
|
+
if (fingerprintMismatch || applyError) {
|
|
945
|
+
const rollbackErrors = rollbackGuardedRefresh(journal);
|
|
946
|
+
if (applyError || rollbackErrors.length > 0) {
|
|
947
|
+
throw new AggregateError(
|
|
948
|
+
[...(applyError ? [applyError] : []), ...rollbackErrors],
|
|
949
|
+
"Codex shim guarded refresh failed",
|
|
950
|
+
);
|
|
951
|
+
}
|
|
952
|
+
return false;
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
const cleanupErrors: Error[] = [];
|
|
956
|
+
for (const entry of journal) {
|
|
957
|
+
try {
|
|
958
|
+
if (entry.stagedOldBackupPath && existsSync(entry.stagedOldBackupPath)) unlinkSync(entry.stagedOldBackupPath);
|
|
959
|
+
} catch (error) {
|
|
960
|
+
cleanupErrors.push(error instanceof Error ? error : new Error(String(error)));
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Codex shim guarded refresh cleanup failed");
|
|
964
|
+
return true;
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
function installCodexShimInternal(options: InstallCodexShimInternalOptions): { installed: boolean; message: string } {
|
|
968
|
+
const existing = readState();
|
|
969
|
+
if (existing) {
|
|
970
|
+
const files = stateFiles(existing);
|
|
971
|
+
if (options.expectedReplacements) {
|
|
972
|
+
const operations = planGuardedRefreshTransaction(files, options.expectedReplacements);
|
|
973
|
+
if (!operations || operations.length === 0) {
|
|
974
|
+
return { installed: false, message: "Codex shim auto-restore deferred because tracked launchers changed." };
|
|
975
|
+
}
|
|
976
|
+
const originalStateBytes = readFileSync(statePath());
|
|
977
|
+
const commitState = (): void => {
|
|
978
|
+
try {
|
|
979
|
+
writeState(primaryState(files));
|
|
980
|
+
} catch (writeError) {
|
|
981
|
+
try {
|
|
982
|
+
writeFileSync(statePath(), originalStateBytes);
|
|
983
|
+
} catch (restoreError) {
|
|
984
|
+
throw new AggregateError(
|
|
985
|
+
[writeError, restoreError],
|
|
986
|
+
"Codex shim state commit and restoration failed",
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
throw writeError;
|
|
990
|
+
}
|
|
991
|
+
};
|
|
992
|
+
if (!applyGuardedRefreshTransaction(operations, options.beforeGuardedRefresh, commitState)) {
|
|
993
|
+
return { installed: false, message: "Codex shim auto-restore deferred because tracked launchers changed." };
|
|
994
|
+
}
|
|
995
|
+
return {
|
|
996
|
+
installed: true,
|
|
997
|
+
message: `Codex update detected. Backed up new launcher and refreshed shim at ${files.map(f => f.wrapperPath).join(", ")}.`,
|
|
998
|
+
};
|
|
999
|
+
}
|
|
1000
|
+
let refreshed = false;
|
|
1001
|
+
for (const file of files) refreshed = refreshShimFile(file) || refreshed;
|
|
1002
|
+
const allInstalled = files.every(file => file.preserveOnly
|
|
1003
|
+
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
1004
|
+
: existsSync(file.wrapperPath)
|
|
1005
|
+
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
1006
|
+
&& isShim(file.wrapperPath));
|
|
1007
|
+
if (refreshed || allInstalled) {
|
|
1008
|
+
writeState(primaryState(files));
|
|
1009
|
+
if (refreshed) {
|
|
1010
|
+
return {
|
|
1011
|
+
installed: true,
|
|
1012
|
+
message: `Codex update detected. Backed up new launcher and refreshed shim at ${files.map(f => f.wrapperPath).join(", ")}.`,
|
|
1013
|
+
};
|
|
1014
|
+
}
|
|
1015
|
+
return {
|
|
1016
|
+
installed: false,
|
|
1017
|
+
message: `Codex autostart shim already installed at ${files.map(f => f.wrapperPath).join(", ")}.`,
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
|
|
1022
|
+
if (!options.allowFreshInstall) {
|
|
1023
|
+
return { installed: false, message: "Codex shim auto-restore requires a valid prior installation." };
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
const targets: ShimFileState[] | null = process.platform === "win32"
|
|
1027
|
+
? findWindowsCodexTargets()
|
|
1028
|
+
: (() => {
|
|
1029
|
+
const originalPath = findCodexOnPath();
|
|
1030
|
+
return originalPath ? [{ wrapperPath: originalPath, originalPath, backupPath: backupPathFor(originalPath) }] : null;
|
|
1031
|
+
})();
|
|
1032
|
+
if (!targets) return { installed: false, message: lastShimDiscoveryError ?? "Could not find a codex executable on PATH." };
|
|
1033
|
+
|
|
1034
|
+
for (const target of targets) {
|
|
1035
|
+
if (existsSync(target.backupPath)) return { installed: false, message: `Refusing to overwrite existing backup: ${target.backupPath}` };
|
|
1036
|
+
}
|
|
1037
|
+
for (const target of targets) {
|
|
1038
|
+
if (existsSync(target.originalPath)) renameSync(target.originalPath, target.backupPath);
|
|
1039
|
+
if (!target.preserveOnly) writeShim(target.wrapperPath, target.realPath ?? target.backupPath);
|
|
1040
|
+
}
|
|
1041
|
+
writeState(primaryState(targets));
|
|
1042
|
+
return {
|
|
1043
|
+
installed: true,
|
|
1044
|
+
message: `Codex autostart shim installed at ${targets.map(t => t.wrapperPath).join(", ")}. Original saved at ${targets.map(t => t.backupPath).join(", ")}.`,
|
|
1045
|
+
};
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
export function installCodexShim(): { installed: boolean; message: string } {
|
|
1049
|
+
return installCodexShimInternal({ allowFreshInstall: true });
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
export function autoRestoreCodexShim(options: {
|
|
1053
|
+
enabled: () => boolean;
|
|
1054
|
+
stabilitySleep?: (ms: number) => void;
|
|
1055
|
+
/** Narrow deterministic seam used to hold the interprocess lock in tests. */
|
|
1056
|
+
afterRestoreLockAcquired?: () => void;
|
|
1057
|
+
/** Narrow deterministic seam for stale-lock compare-and-delete tests. */
|
|
1058
|
+
beforeStaleRestoreLockDelete?: () => void;
|
|
1059
|
+
/** Narrow deterministic race seam for the guarded transaction tests. */
|
|
1060
|
+
beforeGuardedRefresh?: (wrapperPath: string, index: number) => void;
|
|
1061
|
+
}): CodexShimAutoRestoreResult {
|
|
1062
|
+
const stateRead = readStateResult();
|
|
1063
|
+
const state = stateRead.state;
|
|
1064
|
+
if (!state) {
|
|
1065
|
+
if (stateRead.warning) return { status: "ineligible", message: stateRead.warning };
|
|
1066
|
+
return { status: existsSync(statePath()) ? "ineligible" : "not-installed" };
|
|
1067
|
+
}
|
|
1068
|
+
if (state.platform !== process.platform) return { status: "ineligible" };
|
|
1069
|
+
|
|
1070
|
+
const files = stateFiles(state);
|
|
1071
|
+
const replacementProbes = new Map<string, StableShimPathProbe>();
|
|
1072
|
+
const seen = new Set<string>();
|
|
1073
|
+
let healthyCount = 0;
|
|
1074
|
+
for (const file of files) {
|
|
1075
|
+
if (seen.has(file.wrapperPath)) return { status: "ineligible" };
|
|
1076
|
+
seen.add(file.wrapperPath);
|
|
1077
|
+
if (file.preserveOnly) {
|
|
1078
|
+
if (!existsSync(file.backupPath) || existsSync(file.originalPath)) return { status: "ineligible" };
|
|
1079
|
+
continue;
|
|
1080
|
+
}
|
|
1081
|
+
if (!existsSync(file.wrapperPath) || !hasUsableBackingPath(file)) return { status: "ineligible" };
|
|
1082
|
+
const probe = stableShimPathProbe(file.wrapperPath);
|
|
1083
|
+
if (!probe) return { status: "deferred" };
|
|
1084
|
+
if (probe.prefix.includes(SHIM_MARKER)) {
|
|
1085
|
+
if (!isHealthyShimProbe(probe, state.platform)) return { status: "ineligible" };
|
|
1086
|
+
healthyCount += 1;
|
|
1087
|
+
continue;
|
|
1088
|
+
}
|
|
1089
|
+
replacementProbes.set(file.wrapperPath, probe);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
if (replacementProbes.size === 0) return { status: "healthy" };
|
|
1093
|
+
if (!options.enabled()) return { status: "disabled" };
|
|
1094
|
+
if (files.length > 1 && healthyCount > 0) {
|
|
1095
|
+
return {
|
|
1096
|
+
status: "deferred",
|
|
1097
|
+
message: "Codex shim auto-restore deferred because tracked launcher siblings are in a mixed shim/replacement state.",
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
const lock = tryAcquireShimRestoreLock(options.beforeStaleRestoreLockDelete);
|
|
1102
|
+
if (!lock) return { status: "deferred" };
|
|
1103
|
+
try {
|
|
1104
|
+
options.afterRestoreLockAcquired?.();
|
|
1105
|
+
(options.stabilitySleep ?? Bun.sleepSync)(CODEX_SHIM_REPLACEMENT_STABLE_MS);
|
|
1106
|
+
const expectedReplacements = new Map<string, ShimPathFingerprint>();
|
|
1107
|
+
for (const [path, firstProbe] of replacementProbes) {
|
|
1108
|
+
const secondProbe = stableShimPathProbe(path);
|
|
1109
|
+
if (!secondProbe || secondProbe.prefix.includes(SHIM_MARKER)
|
|
1110
|
+
|| !sameStableShimPathProbe(firstProbe, secondProbe)) return { status: "deferred" };
|
|
1111
|
+
expectedReplacements.set(path, secondProbe.fingerprint);
|
|
1112
|
+
}
|
|
1113
|
+
const result = installCodexShimInternal({
|
|
1114
|
+
allowFreshInstall: false,
|
|
1115
|
+
expectedReplacements,
|
|
1116
|
+
beforeGuardedRefresh: options.beforeGuardedRefresh,
|
|
1117
|
+
});
|
|
1118
|
+
return result.installed
|
|
1119
|
+
? { status: "restored", message: result.message }
|
|
1120
|
+
: { status: "deferred" };
|
|
1121
|
+
} finally {
|
|
1122
|
+
lock.release();
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
export function uninstallCodexShim(): { removed: boolean; message: string } {
|
|
1127
|
+
const state = readState();
|
|
1128
|
+
if (!state) return { removed: false, message: "Codex autostart shim is not installed." };
|
|
1129
|
+
const files = stateFiles(state);
|
|
1130
|
+
for (const file of files) {
|
|
1131
|
+
if (file.preserveOnly) continue;
|
|
1132
|
+
if (existsSync(file.wrapperPath) && isShim(file.wrapperPath)) unlinkSync(file.wrapperPath);
|
|
1133
|
+
}
|
|
1134
|
+
for (const file of files) {
|
|
1135
|
+
if (existsSync(file.backupPath) && !existsSync(file.originalPath)) renameSync(file.backupPath, file.originalPath);
|
|
1136
|
+
}
|
|
1137
|
+
if (existsSync(statePath())) unlinkSync(statePath());
|
|
1138
|
+
return { removed: true, message: `Codex autostart shim removed. Restored ${files.map(f => f.originalPath).join(", ")}.` };
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** True if a Codex autostart shim is currently installed (state file present). */
|
|
1142
|
+
export function isCodexShimInstalled(): boolean {
|
|
1143
|
+
return diagnoseCodexShim().installed;
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
export interface CodexShimDiagnostic {
|
|
1147
|
+
installed: boolean;
|
|
1148
|
+
healthy: boolean;
|
|
1149
|
+
summary: string;
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
/** Structured, secret-free shim state for CLI/GUI lifecycle diagnostics. */
|
|
1153
|
+
export function diagnoseCodexShim(): CodexShimDiagnostic {
|
|
1154
|
+
const state = readState();
|
|
1155
|
+
if (!state) {
|
|
1156
|
+
if (existsSync(statePath())) {
|
|
1157
|
+
return {
|
|
1158
|
+
installed: true,
|
|
1159
|
+
healthy: false,
|
|
1160
|
+
summary: `Codex autostart shim state is invalid or corrupt at ${statePath()}. Reinstall or remove the shim.`,
|
|
1161
|
+
};
|
|
1162
|
+
}
|
|
1163
|
+
return {
|
|
1164
|
+
installed: false,
|
|
1165
|
+
healthy: false,
|
|
1166
|
+
summary: "Codex autostart shim is not installed.",
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
const files = stateFiles(state);
|
|
1170
|
+
const healthy = files.length > 0 && files.every(file => file.preserveOnly
|
|
1171
|
+
? existsSync(file.backupPath) && !existsSync(file.originalPath)
|
|
1172
|
+
: existsSync(file.wrapperPath)
|
|
1173
|
+
&& (existsSync(file.backupPath) || (file.realPath ? existsSync(file.realPath) : false))
|
|
1174
|
+
&& isHealthyShim(file.wrapperPath, state.platform));
|
|
1175
|
+
const summary = files.map(file => {
|
|
1176
|
+
const wrapper = existsSync(file.wrapperPath)
|
|
1177
|
+
? isShim(file.wrapperPath)
|
|
1178
|
+
? "shim present"
|
|
1179
|
+
: "present but not an opencodex shim"
|
|
1180
|
+
: "missing";
|
|
1181
|
+
const backup = existsSync(file.backupPath) ? "present" : "missing";
|
|
1182
|
+
return `Codex autostart shim: wrapper ${wrapper} at ${file.wrapperPath}; original backup ${backup} at ${file.backupPath}.`;
|
|
1183
|
+
}).join("\n");
|
|
1184
|
+
return { installed: true, healthy, summary };
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
export function codexShimStatus(): string {
|
|
1188
|
+
return diagnoseCodexShim().summary;
|
|
1189
|
+
}
|