@bitkyc08/opencodex 2.63.0-preview.20260923 → 2.64.0-preview.20260923
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/AGENTS_INSTALL.md +4 -3
- package/README.md +46 -33
- package/assets/download-linux.svg +10 -0
- package/assets/download-macos.svg +10 -0
- package/assets/download-windows.svg +10 -0
- package/bin/ocx.mjs +12 -4
- package/gui/dist/assets/App-D5eN1TID.js +50 -0
- package/gui/dist/assets/App-I5AnaSLh.css +1 -0
- package/gui/dist/assets/{Tray-B03pW-Uf.js → Tray-Bh_ErQDh.js} +1 -1
- package/gui/dist/assets/index-BEq4OCOz.js +86 -0
- package/gui/dist/assets/index-DiBRuK-d.css +1 -0
- package/gui/dist/assets/usage-companion-chart-DBG_37kQ.js +1 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/src/protocol.rs +5 -0
- package/package.json +4 -1
- package/src/adapters/anthropic.ts +66 -7
- package/src/adapters/codebuddy/adapter.ts +112 -16
- package/src/adapters/codebuddy/mcp-server.ts +180 -0
- package/src/adapters/codebuddy/scaffold-guard.ts +25 -8
- package/src/adapters/codebuddy/tool-bridge.ts +597 -0
- package/src/adapters/coding-agent/protocol.ts +123 -10
- package/src/adapters/coding-agent/turn.ts +324 -2
- package/src/adapters/command-code-restored-schema.ts +112 -0
- package/src/adapters/command-code-tool-text.ts +589 -0
- package/src/adapters/command-code.ts +80 -6
- package/src/adapters/cursor/catalog.ts +12 -0
- package/src/adapters/cursor/current-request.ts +46 -0
- package/src/adapters/cursor/discovery.ts +1 -1
- package/src/adapters/cursor/effort-map.ts +4 -0
- package/src/adapters/cursor/native-exec.ts +15 -0
- package/src/adapters/cursor/protobuf-events.ts +40 -12
- package/src/adapters/cursor/protobuf-request.ts +83 -18
- package/src/adapters/cursor/request-builder.ts +5 -4
- package/src/adapters/cursor/tool-guidance.ts +1 -1
- package/src/adapters/devin/live-models.ts +7 -0
- package/src/adapters/inline-think-tags.ts +251 -0
- package/src/adapters/kiro/adapter.ts +1 -0
- package/src/adapters/kiro/stream.ts +5 -3
- package/src/adapters/kiro/usage.ts +4 -3
- package/src/adapters/mimo-free.ts +37 -18
- package/src/adapters/openai-chat/messages.ts +5 -5
- package/src/adapters/openai-chat/tool-schema.ts +124 -2
- package/src/adapters/openai-chat.ts +13 -3
- package/src/adapters/openai-responses/passthrough.ts +15 -2
- package/src/adapters/openai-responses/reasoning.ts +14 -1
- package/src/adapters/openai-responses/request-strips.ts +29 -3
- package/src/adapters/openai-responses/tool-output-recovery.ts +9 -3
- package/src/adapters/qoder/adapter.ts +15 -12
- package/src/adapters/qoder/scaffold-guard.ts +45 -13
- package/src/bridge/internal.ts +20 -0
- package/src/bridge/sse.ts +14 -5
- package/src/claude/agents-inject.ts +2 -1
- package/src/claude/desktop-applied-marker.ts +44 -0
- package/src/claude/desktop-profile.ts +22 -12
- package/src/claude/inbound.ts +8 -3
- package/src/cli/access.ts +22 -5
- package/src/cli/account-api.ts +6 -0
- package/src/cli/account-auth.ts +5 -2
- package/src/cli/account.ts +5 -6
- package/src/cli/aside-profiles.ts +61 -1
- package/src/cli/claude-desktop.ts +3 -2
- package/src/cli/claude.ts +3 -1
- package/src/cli/codex-cli-update.ts +2 -2
- package/src/cli/codex-shim-autorestore.ts +3 -0
- package/src/cli/dispatch.ts +12 -4
- package/src/cli/index.ts +98 -10
- package/src/cli/registry.ts +1 -1
- package/src/cli/resolve.ts +113 -1
- package/src/cli/root.ts +5 -0
- package/src/cli/stop-approval.ts +186 -0
- package/src/cli/stop-report.ts +1 -1
- package/src/cli/system-restart-client.ts +19 -4
- package/src/client/hub-client.ts +25 -18
- package/src/clients/config-export.ts +12 -4
- package/src/codex/account-auto-switch.ts +59 -0
- package/src/codex/account-lifecycle.ts +7 -0
- package/src/codex/account-priority.ts +10 -0
- package/src/codex/account-usability.ts +9 -0
- package/src/codex/auth-api/account-list.ts +5 -0
- package/src/codex/auth-api/login-flow.ts +15 -5
- package/src/codex/auth-api/login-state.ts +5 -16
- package/src/codex/auth-api/pool-mode-gate.ts +30 -7
- package/src/codex/auth-api/routes.ts +39 -3
- package/src/codex/auth-context.ts +145 -17
- package/src/codex/catalog/build-entries.ts +2 -1
- package/src/codex/catalog/effort.ts +8 -2
- package/src/codex/catalog/metadata.ts +33 -7
- package/src/codex/catalog/native-models.ts +102 -5
- package/src/codex/catalog/parsing.ts +2 -0
- package/src/codex/catalog/provider-models.ts +19 -6
- package/src/codex/cli-installation-targets.ts +44 -40
- package/src/codex/codex-write-lock.ts +32 -2
- package/src/codex/inject/multi-agent-v2.ts +90 -0
- package/src/codex/inject/plan.ts +365 -0
- package/src/codex/inject.ts +237 -362
- package/src/codex/log-guard/maintenance.ts +30 -13
- package/src/codex/project-config-warnings.ts +47 -7
- package/src/codex/prompt-layers/encoding.ts +41 -0
- package/src/codex/prompt-layers/toml-read.ts +1 -1
- package/src/codex/prompt-layers.ts +17 -13
- package/src/codex/prompt-text-probe.ts +20 -10
- package/src/codex/quota-observation-freshness.ts +34 -0
- package/src/codex/quota-rejection.ts +8 -4
- package/src/codex/quota.ts +4 -2
- package/src/codex/routing/selection.ts +32 -9
- package/src/codex/routing.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +4 -3
- package/src/codex/sync.ts +11 -0
- package/src/codex/windows-installation-files.ts +33 -11
- package/src/codex/write-coordination.ts +4 -0
- package/src/combos/request.ts +6 -2
- package/src/companion/settings.ts +6 -1
- package/src/config/atomic-write.ts +12 -1
- package/src/config/derived-registries.ts +29 -0
- package/src/config/diagnostics.ts +17 -0
- package/src/config/live-reconcile.ts +11 -2
- package/src/config/load-degrade.ts +9 -2
- package/src/config/persist-unlocked.ts +4 -3
- package/src/config/persisted-mutation.ts +94 -0
- package/src/config/provider-validation.ts +1 -1
- package/src/config/proxy-env.ts +62 -12
- package/src/config/rebase-provenance.ts +80 -1
- package/src/config/schema/config-schema.ts +5 -0
- package/src/config/schema/leaf-validators.ts +50 -1
- package/src/config/subagent-models.ts +41 -15
- package/src/config.ts +18 -95
- package/src/generated/compatibility-version.json +330 -218
- package/src/generated/model-metadata.ts +8 -5
- package/src/github/star-state.ts +46 -6
- package/src/grok/inject.ts +74 -40
- package/src/grok/status.ts +11 -4
- package/src/integrations/cursor-effort-table.ts +50 -13
- package/src/integrations/raycast-detect.ts +19 -4
- package/src/integrations/serialize.ts +11 -1
- package/src/lib/bounded-body.ts +30 -0
- package/src/lib/crash-guard.ts +52 -4
- package/src/lib/local-aside-sync-contract.ts +41 -0
- package/src/lib/package-tree-integrity.ts +181 -9
- package/src/lib/proxy-env.ts +18 -1
- package/src/lib/request-failure-attribution.ts +1 -0
- package/src/lib/request-failure-model.ts +3 -0
- package/src/lib/service-secrets.ts +99 -2
- package/src/lib/socks5-fetch.ts +43 -14
- package/src/lib/token-estimate.ts +17 -2
- package/src/oauth/command-code.ts +3 -2
- package/src/oauth/generic-account-failover.ts +29 -1
- package/src/oauth/index.ts +24 -16
- package/src/oauth/login-flow-state.ts +5 -5
- package/src/oauth/meta-muse-device.ts +49 -7
- package/src/oauth/store.ts +74 -9
- package/src/providers/alibaba-region-backup.ts +16 -1
- package/src/providers/anthropic-fast.ts +89 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +288 -0
- package/src/providers/anthropic-reset-grants.ts +329 -0
- package/src/providers/claude-cli-identity.ts +12 -0
- package/src/providers/command-code-efforts.ts +184 -102
- package/src/providers/derive.ts +11 -3
- package/src/providers/fastwire.ts +16 -3
- package/src/providers/label.ts +8 -3
- package/src/providers/model-rename-fields.ts +1 -0
- package/src/providers/openai-virtual-models.ts +1 -0
- package/src/providers/quota/vendor-probes-oauth.ts +2 -1
- package/src/providers/reasoning-metadata.ts +38 -15
- package/src/providers/registry/entries-core.ts +95 -7
- package/src/providers/registry/entries-extended.ts +21 -10
- package/src/providers/registry/model-ids.ts +1 -0
- package/src/providers/registry/model-seeds.ts +31 -3
- package/src/providers/registry/types.ts +3 -1
- package/src/providers/resolved-model-policy-merge.ts +38 -8
- package/src/providers/resolved-model-policy.ts +4 -2
- package/src/providers/service-tier.ts +3 -1
- package/src/reasoning-effort.ts +6 -5
- package/src/responses/code-mode-helper-compat.ts +14 -4
- package/src/responses/code-mode-shell-input.ts +54 -0
- package/src/responses/custom-tool-compat.ts +173 -5
- package/src/responses/parser.ts +38 -2
- package/src/responses/reasoning-replay-cache.ts +26 -0
- package/src/router.ts +11 -1
- package/src/routing/history/indexer.ts +7 -2
- package/src/routing/history/schema.ts +3 -1
- package/src/server/adapter-resolve.ts +2 -2
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-completions.ts +14 -1
- package/src/server/chat-native.ts +17 -0
- package/src/server/claude-messages.ts +4 -3
- package/src/server/direct-local-http.ts +45 -19
- package/src/server/gui-session.ts +6 -15
- package/src/server/index/package-tree-guard.ts +53 -0
- package/src/server/index/serve-options.ts +17 -3
- package/src/server/index/startup-warnings.ts +17 -0
- package/src/server/index.ts +15 -17
- package/src/server/lifecycle.ts +8 -0
- package/src/server/live.ts +54 -4
- package/src/server/management/agent-settings-routes.ts +44 -8
- package/src/server/management/anthropic-reset-grant-routes.ts +252 -0
- package/src/server/management/codex-prompt-routes.ts +5 -1
- package/src/server/management/config-routes.ts +14 -4
- package/src/server/management/oauth-account-routes.ts +17 -1
- package/src/server/management/provider-overwrite-carry.ts +164 -0
- package/src/server/management/provider-routes.ts +36 -6
- package/src/server/management/remote-workspace-routes.ts +2 -2
- package/src/server/management/route-registry.ts +2 -0
- package/src/server/management/shadow-call-validation.ts +39 -0
- package/src/server/management/system-restart.ts +72 -6
- package/src/server/management-api.ts +16 -2
- package/src/server/management-auth.ts +52 -8
- package/src/server/proxy-liveness.ts +110 -4
- package/src/server/request-log.ts +26 -9
- package/src/server/request-metrics.ts +5 -0
- package/src/server/responses/adapter-continuation.ts +6 -3
- package/src/server/responses/adapter-dispatch.ts +65 -1
- package/src/server/responses/compact.ts +31 -1
- package/src/server/responses/compaction-routing.ts +28 -3
- package/src/server/responses/core-codex-account.ts +85 -26
- package/src/server/responses/core-combo-failure.ts +16 -15
- package/src/server/responses/core-normalize.ts +5 -1
- package/src/server/responses/core-opaque-recovery.ts +50 -2
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core-replay.ts +7 -0
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/encrypted-payload.ts +23 -9
- package/src/server/responses/passthrough-delivery.ts +55 -35
- package/src/server/responses/passthrough-dispatch.ts +12 -13
- package/src/server/responses/policy-fallback.ts +12 -1
- package/src/server/responses/request-prepare.ts +46 -7
- package/src/server/responses/request-spend.ts +31 -15
- package/src/server/responses/run-turn-execution.ts +6 -5
- package/src/server/responses/shadow-target-availability.ts +61 -0
- package/src/server/responses-custom-tool-repair.ts +2 -0
- package/src/service/claim.ts +185 -0
- package/src/service/cli.ts +10 -1
- package/src/service/guarded-manager-target.ts +151 -0
- package/src/service/guards.ts +47 -48
- package/src/service/managing-cli.ts +170 -0
- package/src/service/orchestration.ts +3 -1
- package/src/service/state.ts +4 -0
- package/src/service/systemd.ts +16 -1
- package/src/service.ts +1 -1
- package/src/types/config.ts +8 -0
- package/src/types/provider.ts +16 -0
- package/src/types/request.ts +10 -0
- package/src/types/tools.ts +9 -1
- package/src/types/wire.ts +68 -4
- package/src/types.ts +1 -0
- package/src/update/job.ts +10 -16
- package/src/update/npm-invocation.mjs +17 -16
- package/src/update/transactional-install.d.mts +22 -1
- package/src/update/transactional-install.mjs +176 -19
- package/src/update/update-failure-guidance.d.mts +8 -0
- package/src/update/update-failure-guidance.mjs +47 -0
- package/src/usage/expected-prices.ts +54 -12
- package/src/usage/log.ts +117 -3
- package/src/usage/telemetry-contract.ts +1 -0
- package/src/usage/timeline.ts +34 -6
- package/src/usage/user-cost-overlay-reconciler.ts +3 -3
- package/src/vision/reasoning.ts +2 -4
- package/src/web-search/xai-executor.ts +14 -4
- package/gui/dist/assets/App-EJxiUFMq.js +0 -50
- package/gui/dist/assets/index-BmJwNBHL.js +0 -86
- package/gui/dist/assets/index-DdDunwDb.css +0 -1
- package/gui/dist/assets/usage-companion-chart-IftE60UK.js +0 -1
- package/src/adapters/kiro-thinking.ts +0 -112
package/src/types/wire.ts
CHANGED
|
@@ -70,6 +70,67 @@ export function captureWireAdapterHardPins(providerName: string): Readonly<Recor
|
|
|
70
70
|
return Object.freeze(Object.fromEntries([...models].map(modelId => [modelId, "anthropic"])));
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
interface WirePinPrefixRule {
|
|
74
|
+
/** The registry endpoint the rule describes; another destination under the same name is not covered. */
|
|
75
|
+
readonly endpoint: string;
|
|
76
|
+
readonly prefixes: Readonly<Record<string, string>>;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Provider-local model-id prefixes whose upstream accepts only one wire, bound to the endpoint that
|
|
81
|
+
* behaves that way. Command Code's Provider API serves `claude-*` ids only on `/provider/v1/messages`
|
|
82
|
+
* (live `supported_endpoints` on 2026-09-23 list `/messages` alone; `/chat/completions` answers 400
|
|
83
|
+
* "must be called via /provider/v1/messages"). A prefix covers Claude ids Command Code adds later.
|
|
84
|
+
*/
|
|
85
|
+
const WIRE_ADAPTER_PIN_PREFIXES: Readonly<Record<string, WirePinPrefixRule>> = Object.freeze({
|
|
86
|
+
commandcode: Object.freeze({
|
|
87
|
+
endpoint: "https://api.commandcode.ai/provider/v1",
|
|
88
|
+
prefixes: Object.freeze({ "claude-": "anthropic" }),
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
/** Just enough of a provider config to tell whether it still points at the pinned endpoint. */
|
|
93
|
+
export interface WirePinProvider {
|
|
94
|
+
readonly baseUrl?: unknown;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizedEndpoint(value: unknown): string | undefined {
|
|
98
|
+
if (typeof value !== "string") return undefined;
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(value.trim());
|
|
101
|
+
return `${url.protocol}//${url.host}${url.pathname.replace(/\/+$/, "")}`.toLowerCase();
|
|
102
|
+
} catch {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function prefixPinsFor(providerName: string, provider: WirePinProvider | undefined): Readonly<Record<string, string>> | undefined {
|
|
108
|
+
if (!provider || !Object.hasOwn(WIRE_ADAPTER_PIN_PREFIXES, providerName)) return undefined;
|
|
109
|
+
const rule = WIRE_ADAPTER_PIN_PREFIXES[providerName]!;
|
|
110
|
+
return normalizedEndpoint(provider.baseUrl) === rule.endpoint ? rule.prefixes : undefined;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Detached provider-local prefix pins for pure wire-policy resolution. Empty unless the provider
|
|
115
|
+
* still points at the endpoint the rule describes.
|
|
116
|
+
*/
|
|
117
|
+
export function captureWireAdapterHardPinPrefixes(
|
|
118
|
+
providerName: string,
|
|
119
|
+
provider: WirePinProvider | undefined,
|
|
120
|
+
): Readonly<Record<string, string>> {
|
|
121
|
+
return Object.freeze({ ...(prefixPinsFor(providerName, provider) ?? {}) });
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function prefixedWireAdapter(providerName: string, modelId: string, provider: WirePinProvider | undefined): string | undefined {
|
|
125
|
+
const prefixes = prefixPinsFor(providerName, provider);
|
|
126
|
+
if (!prefixes) return undefined;
|
|
127
|
+
const folded = modelId.toLowerCase();
|
|
128
|
+
for (const [prefix, adapter] of Object.entries(prefixes)) {
|
|
129
|
+
if (folded.startsWith(prefix)) return adapter;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
73
134
|
/**
|
|
74
135
|
* True when the upstream speaks exactly one wire for this model, so a configured
|
|
75
136
|
* override must not apply.
|
|
@@ -78,11 +139,14 @@ export function captureWireAdapterHardPins(providerName: string): Readonly<Recor
|
|
|
78
139
|
* more than once per request, and a check phrased as "pin differs from the current
|
|
79
140
|
* adapter" would pass on the first pass and then let the override win on the second.
|
|
80
141
|
*/
|
|
81
|
-
export function isWirePinnedModel(providerName: string, modelId: string): boolean {
|
|
82
|
-
return anthropicWireModelsForProvider(providerName)?.has(modelId) ?? false
|
|
142
|
+
export function isWirePinnedModel(providerName: string, modelId: string, provider?: WirePinProvider): boolean {
|
|
143
|
+
return (anthropicWireModelsForProvider(providerName)?.has(modelId) ?? false)
|
|
144
|
+
|| prefixedWireAdapter(providerName, modelId, provider) !== undefined;
|
|
83
145
|
}
|
|
84
146
|
|
|
85
147
|
/** The wire a pinned model must use, or undefined when the model is not pinned. */
|
|
86
|
-
export function pinnedWireAdapter(providerName: string, modelId: string): string | undefined {
|
|
87
|
-
return
|
|
148
|
+
export function pinnedWireAdapter(providerName: string, modelId: string, provider?: WirePinProvider): string | undefined {
|
|
149
|
+
return anthropicWireModelsForProvider(providerName)?.has(modelId)
|
|
150
|
+
? "anthropic"
|
|
151
|
+
: prefixedWireAdapter(providerName, modelId, provider);
|
|
88
152
|
}
|
package/src/types.ts
CHANGED
package/src/update/job.ts
CHANGED
|
@@ -24,7 +24,13 @@ import {
|
|
|
24
24
|
import { stopWinswService } from "../lib/winsw";
|
|
25
25
|
import { listListenPids, reclaimListenPort, scanListenPids, type ListenPidScan } from "../server/port-reclaim";
|
|
26
26
|
import { dropWindowsTcpRowsForLocalPort } from "../server/windows-tcp-drop";
|
|
27
|
-
import {
|
|
27
|
+
import {
|
|
28
|
+
isHealthzVersion,
|
|
29
|
+
isOpencodexHealthz,
|
|
30
|
+
probeHostname,
|
|
31
|
+
proxyIdentityAt,
|
|
32
|
+
type HealthzIdentity,
|
|
33
|
+
} from "../server/proxy-liveness";
|
|
28
34
|
import { isServiceInstalled, isServiceViable, readServiceBackend, stopWindows } from "../service";
|
|
29
35
|
import { runUpdateRestartWithOwnershipLease, type ServiceOwnershipResolution } from "./restart-ownership";
|
|
30
36
|
import {
|
|
@@ -45,6 +51,7 @@ import type { PnpmGlobalOwner } from "./pnpm-global-install.mjs";
|
|
|
45
51
|
import { isNewer } from "./notify";
|
|
46
52
|
import { isRealBunBinary } from "../lib/bun-binary-validator.mjs";
|
|
47
53
|
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
|
|
54
|
+
import { GUI_UPDATE_FAILURE_NEXT_STEP } from "./update-failure-guidance.mjs";
|
|
48
55
|
import {
|
|
49
56
|
npmCachePreflightFailureMessage,
|
|
50
57
|
runNpmCachePreflight,
|
|
@@ -268,19 +275,6 @@ function ensureJobDir(): void {
|
|
|
268
275
|
* TYPE and size — enough to tell a reader what class of failure occurred — and never its text,
|
|
269
276
|
* which is where the paths and account names live.
|
|
270
277
|
*/
|
|
271
|
-
/**
|
|
272
|
-
* A version string we are willing to repeat in a persisted field.
|
|
273
|
-
*
|
|
274
|
-
* Semver plus an optional prerelease/build tail, capped in length. Anything else is dropped
|
|
275
|
-
* rather than logged: `/healthz` is answered by whatever holds the port, so its `version` is
|
|
276
|
-
* external input on the same footing as an error message.
|
|
277
|
-
*/
|
|
278
|
-
function isVersionLike(value: unknown): value is string {
|
|
279
|
-
return typeof value === "string"
|
|
280
|
-
&& value.length <= 64
|
|
281
|
-
&& /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.test(value);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
278
|
function withheldSummary(error: unknown): string {
|
|
285
279
|
// `error.name` is writable, so it is external text like the message. A fixed classification
|
|
286
280
|
// is the only part of an unknown error we can state without repeating something we were
|
|
@@ -1589,7 +1583,7 @@ async function defaultProbeProxyIdentity(
|
|
|
1589
1583
|
// `/healthz` is answered by whatever is listening on that port, so a hostile or confused
|
|
1590
1584
|
// responder can return any string here — and the restart-evidence reasons below
|
|
1591
1585
|
// interpolate it into a persisted field. A version is a version or it is nothing.
|
|
1592
|
-
...(
|
|
1586
|
+
...(isHealthzVersion(body?.version) ? { version: body.version } : {}),
|
|
1593
1587
|
};
|
|
1594
1588
|
} catch {
|
|
1595
1589
|
return null;
|
|
@@ -1949,7 +1943,7 @@ export async function runGuiUpdateWorker(
|
|
|
1949
1943
|
status: "failed",
|
|
1950
1944
|
exitCode: result.status,
|
|
1951
1945
|
signal: result.signal,
|
|
1952
|
-
error: `update command failed (${result.status ?? "?"})`,
|
|
1946
|
+
error: `update command failed (${result.status ?? "?"}). ${GUI_UPDATE_FAILURE_NEXT_STEP}`,
|
|
1953
1947
|
});
|
|
1954
1948
|
return;
|
|
1955
1949
|
}
|
|
@@ -12,21 +12,17 @@ function escapeCmdCommand(command) {
|
|
|
12
12
|
return command.replace(CMD_META, "^$1");
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
function isCurrentDirectory(cwd, entry) {
|
|
27
|
-
const left = win32.resolve(entry);
|
|
28
|
-
const right = win32.resolve(cwd);
|
|
29
|
-
return left.toLowerCase() === right.toLowerCase();
|
|
15
|
+
function isInside(root, candidate) {
|
|
16
|
+
const relative = win32.relative(win32.resolve(root), win32.resolve(candidate));
|
|
17
|
+
return relative === "" || (
|
|
18
|
+
relative !== ".."
|
|
19
|
+
&& !relative.startsWith(`..${win32.sep}`)
|
|
20
|
+
&& !win32.isAbsolute(relative)
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function isSamePath(left, right) {
|
|
25
|
+
return win32.resolve(left).toLowerCase() === win32.resolve(right).toLowerCase();
|
|
30
26
|
}
|
|
31
27
|
|
|
32
28
|
function cleanPathEntry(entry) {
|
|
@@ -43,6 +39,10 @@ export function resolveNpmCommand(
|
|
|
43
39
|
if (platform !== "win32") return "npm";
|
|
44
40
|
const exists = deps.exists ?? existsSync;
|
|
45
41
|
const cwd = deps.cwd ?? process.cwd();
|
|
42
|
+
const trustedRoots = [env.APPDATA, env.LOCALAPPDATA, env.ProgramFiles, env["ProgramFiles(x86)"],
|
|
43
|
+
env.USERPROFILE && win32.join(env.USERPROFILE, "scoop", "shims")]
|
|
44
|
+
.filter(root => typeof root === "string" && win32.isAbsolute(root));
|
|
45
|
+
const trustedEntry = entry => trustedRoots.some(root => isInside(root, entry) && !isInside(root, cwd));
|
|
46
46
|
const extensions = (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD")
|
|
47
47
|
.split(";")
|
|
48
48
|
.filter(Boolean);
|
|
@@ -53,7 +53,8 @@ export function resolveNpmCommand(
|
|
|
53
53
|
|
|
54
54
|
for (const entry of pathEntries) {
|
|
55
55
|
if (!win32.isAbsolute(entry)) continue;
|
|
56
|
-
if (
|
|
56
|
+
if (isSamePath(entry, cwd)) continue;
|
|
57
|
+
if (isInside(cwd, entry) && !trustedEntry(entry)) continue;
|
|
57
58
|
for (const extension of extensions) {
|
|
58
59
|
const candidate = win32.join(entry, `npm${extension.toLowerCase()}`);
|
|
59
60
|
if (exists(candidate)) return win32.resolve(candidate);
|
|
@@ -5,6 +5,27 @@ export function bootRestoreProbe(
|
|
|
5
5
|
packageDir: string,
|
|
6
6
|
deps?: { rename?: (from: string, to: string) => void },
|
|
7
7
|
): { action: "none" | "reaped" | "restored" | "failed"; count?: number; from?: string; error?: string };
|
|
8
|
+
export type UpdateFsDeps = {
|
|
9
|
+
rename?: (from: string, to: string) => void;
|
|
10
|
+
mkdir?: (path: string) => void;
|
|
11
|
+
rm?: (path: string, options: { recursive: true; force: true }) => void;
|
|
12
|
+
now?: () => number;
|
|
13
|
+
};
|
|
14
|
+
export const UPDATE_OWNER_MARKER: string;
|
|
15
|
+
export const STALE_STAGE_MIN_AGE_MS: number;
|
|
16
|
+
export function removeOwnedStage(stageRoot: string, deps?: UpdateFsDeps): { removed: boolean; code?: string };
|
|
17
|
+
export function sweepUpdateLeftovers(args: {
|
|
18
|
+
packageDir: string;
|
|
19
|
+
pkgName: string;
|
|
20
|
+
log?: (line: string) => void;
|
|
21
|
+
deps?: UpdateFsDeps;
|
|
22
|
+
}): {
|
|
23
|
+
removed: string[];
|
|
24
|
+
inUse: Array<{ path: string; code: string }>;
|
|
25
|
+
recent: string[];
|
|
26
|
+
notOwned: string[];
|
|
27
|
+
};
|
|
28
|
+
export function launcherUsableAfterNpmUpdate(tx: { ok: boolean; phase: string; rolledBack?: boolean } | null | undefined): boolean;
|
|
8
29
|
export function transactionalNpmUpdate(args: {
|
|
9
30
|
packageDir: string;
|
|
10
31
|
pkgName: string;
|
|
@@ -12,7 +33,7 @@ export function transactionalNpmUpdate(args: {
|
|
|
12
33
|
tag: string;
|
|
13
34
|
runNpm: (args: string[]) => { status: number | null };
|
|
14
35
|
log?: (line: string) => void;
|
|
15
|
-
deps?:
|
|
36
|
+
deps?: UpdateFsDeps;
|
|
16
37
|
}): {
|
|
17
38
|
ok: boolean;
|
|
18
39
|
phase: "stage" | "verify" | "swap-backup" | "swap-live" | "post-verify" | "double-fault" | "done";
|
|
@@ -15,9 +15,15 @@
|
|
|
15
15
|
* <scopeDir>/.ocx-staging-<ts>/ npm --prefix root (contains node_modules/...)
|
|
16
16
|
* <scopeDir>/.ocx-backup-<ts>/opencodex previous live tree during/after the swap
|
|
17
17
|
* <scopeDir>/.ocx-recovery.json double-fault marker with a one-line restore
|
|
18
|
+
*
|
|
19
|
+
* A staging directory is created exclusively and immediately carries an ownership marker
|
|
20
|
+
* (`.ocx-update-owner.json`). Only a marked staging directory is ever swept by a later
|
|
21
|
+
* update; anything else next to the package — an unmarked stage from an older updater, npm's
|
|
22
|
+
* own `.<name>-<random>` rename-aside, a link — is reported and left alone (#5624).
|
|
18
23
|
*/
|
|
19
24
|
import { spawnSync } from "node:child_process";
|
|
20
|
-
import {
|
|
25
|
+
import { randomBytes } from "node:crypto";
|
|
26
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
21
27
|
import { basename, dirname, join } from "node:path";
|
|
22
28
|
|
|
23
29
|
/**
|
|
@@ -160,6 +166,146 @@ function stampedName(prefix) {
|
|
|
160
166
|
return prefix + "-" + new Date().toISOString().replace(/[:.]/g, "-");
|
|
161
167
|
}
|
|
162
168
|
|
|
169
|
+
export const UPDATE_OWNER_MARKER = ".ocx-update-owner.json";
|
|
170
|
+
const STAGE_PREFIX = ".ocx-staging-";
|
|
171
|
+
/**
|
|
172
|
+
* A marked stage younger than this is left alone by the sweep. npm's staging install is bounded
|
|
173
|
+
* at three minutes by the launcher; the floor is far above it so an update running from another
|
|
174
|
+
* home against the same global prefix never loses its in-flight stage.
|
|
175
|
+
*/
|
|
176
|
+
export const STALE_STAGE_MIN_AGE_MS = 30 * 60 * 1000;
|
|
177
|
+
const RETRYABLE_REMOVE_CODES = new Set(["EPERM", "EBUSY", "EACCES", "ENOTEMPTY"]);
|
|
178
|
+
|
|
179
|
+
/** Create a fresh staging directory that did not exist before, and mark it as ours. */
|
|
180
|
+
function createOwnedStage(scopeDir, pkgName, deps = {}) {
|
|
181
|
+
const mkdir = deps.mkdir ?? mkdirSync;
|
|
182
|
+
const stageRoot = join(scopeDir, stampedName(".ocx-staging") + "-" + randomBytes(4).toString("hex"));
|
|
183
|
+
// Not recursive: an existing directory of the same name must fail, never be reused.
|
|
184
|
+
mkdir(stageRoot);
|
|
185
|
+
try {
|
|
186
|
+
writeFileSync(join(stageRoot, UPDATE_OWNER_MARKER), JSON.stringify({
|
|
187
|
+
schema: 1,
|
|
188
|
+
kind: "staging",
|
|
189
|
+
pkgName,
|
|
190
|
+
pid: process.pid,
|
|
191
|
+
createdAt: (deps.now ?? Date.now)(),
|
|
192
|
+
}), { flag: "wx" });
|
|
193
|
+
} catch (error) {
|
|
194
|
+
// Still empty and created by this call: remove it rather than leave an unmarked stage.
|
|
195
|
+
try { rmSync(stageRoot, { recursive: true, force: true }); } catch { /* reported by the caller */ }
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
return stageRoot;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** The marker of a real (non-link) staging directory this updater created, or null. */
|
|
202
|
+
function readOwnedStageMarker(dir, pkgName) {
|
|
203
|
+
try {
|
|
204
|
+
const dirStat = lstatSync(dir);
|
|
205
|
+
// A symlink or junction is never ours to delete: removing through it reaches its target.
|
|
206
|
+
if (!dirStat.isDirectory() || dirStat.isSymbolicLink()) return null;
|
|
207
|
+
const markerPath = join(dir, UPDATE_OWNER_MARKER);
|
|
208
|
+
if (!lstatSync(markerPath).isFile()) return null;
|
|
209
|
+
const marker = JSON.parse(readFileSync(markerPath, "utf8"));
|
|
210
|
+
if (marker?.schema !== 1 || marker.kind !== "staging" || marker.pkgName !== pkgName) return null;
|
|
211
|
+
if (typeof marker.createdAt !== "number" || !Number.isFinite(marker.createdAt)) return null;
|
|
212
|
+
return marker;
|
|
213
|
+
} catch {
|
|
214
|
+
return null;
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function removeWithRetry(rm, target, attempts = 3) {
|
|
219
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
220
|
+
try {
|
|
221
|
+
rm(target, { recursive: true, force: true });
|
|
222
|
+
return null;
|
|
223
|
+
} catch (error) {
|
|
224
|
+
const code = typeof error?.code === "string" ? error.code : "EUNKNOWN";
|
|
225
|
+
if (!RETRYABLE_REMOVE_CODES.has(code) || attempt >= attempts - 1) return code;
|
|
226
|
+
const until = Date.now() + 100 * (attempt + 1);
|
|
227
|
+
while (Date.now() < until) { /* brief synchronous backoff, as renameWithRetry */ }
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Remove an owned staging directory. The marker goes last and only once everything else is
|
|
234
|
+
* gone, so a tree held open by a running executable (the reported locked `bunx.exe`) stays
|
|
235
|
+
* provably ours and the next update can finish the job.
|
|
236
|
+
*/
|
|
237
|
+
export function removeOwnedStage(stageRoot, deps = {}) {
|
|
238
|
+
const rm = deps.rm ?? rmSync;
|
|
239
|
+
let names;
|
|
240
|
+
try {
|
|
241
|
+
names = readdirSync(stageRoot);
|
|
242
|
+
} catch (error) {
|
|
243
|
+
return error?.code === "ENOENT" ? { removed: true } : { removed: false, code: error?.code ?? "EUNKNOWN" };
|
|
244
|
+
}
|
|
245
|
+
let failure = null;
|
|
246
|
+
for (const name of names) {
|
|
247
|
+
if (name === UPDATE_OWNER_MARKER) continue;
|
|
248
|
+
const code = removeWithRetry(rm, join(stageRoot, name));
|
|
249
|
+
if (code && !failure) failure = code;
|
|
250
|
+
}
|
|
251
|
+
if (failure) return { removed: false, code: failure };
|
|
252
|
+
const markerCode = removeWithRetry(rm, join(stageRoot, UPDATE_OWNER_MARKER));
|
|
253
|
+
if (markerCode) return { removed: false, code: markerCode };
|
|
254
|
+
const dirCode = removeWithRetry(rm, stageRoot);
|
|
255
|
+
return dirCode ? { removed: false, code: dirCode } : { removed: true };
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Clear what earlier update attempts left next to the package, without ever deleting anything
|
|
260
|
+
* this updater cannot prove it created. Never throws and never fails the update: every stage is
|
|
261
|
+
* a fresh, uniquely named directory, so a leftover can only cost disk space, not block staging.
|
|
262
|
+
*/
|
|
263
|
+
export function sweepUpdateLeftovers({ packageDir, pkgName, log = () => {}, deps = {} }) {
|
|
264
|
+
const scopeDir = dirname(packageDir);
|
|
265
|
+
const now = (deps.now ?? Date.now)();
|
|
266
|
+
const result = { removed: [], inUse: [], recent: [], notOwned: [] };
|
|
267
|
+
let names = [];
|
|
268
|
+
try {
|
|
269
|
+
names = readdirSync(scopeDir);
|
|
270
|
+
} catch {
|
|
271
|
+
return result;
|
|
272
|
+
}
|
|
273
|
+
// npm renames a package it is replacing to ".<name>-<random>" during a direct global install.
|
|
274
|
+
const renameAsidePrefix = "." + basename(packageDir) + "-";
|
|
275
|
+
for (const name of names.sort()) {
|
|
276
|
+
const full = join(scopeDir, name);
|
|
277
|
+
if (name.startsWith(STAGE_PREFIX)) {
|
|
278
|
+
const marker = readOwnedStageMarker(full, pkgName);
|
|
279
|
+
if (!marker) {
|
|
280
|
+
result.notOwned.push(full);
|
|
281
|
+
} else if (now - marker.createdAt < STALE_STAGE_MIN_AGE_MS) {
|
|
282
|
+
result.recent.push(full);
|
|
283
|
+
} else {
|
|
284
|
+
const removal = removeOwnedStage(full, deps);
|
|
285
|
+
if (removal.removed) result.removed.push(full);
|
|
286
|
+
else result.inUse.push({ path: full, code: removal.code });
|
|
287
|
+
}
|
|
288
|
+
} else if (name.startsWith(renameAsidePrefix)) {
|
|
289
|
+
result.notOwned.push(full);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// Names only: the full path under a user-scoped npm prefix carries the account name, and
|
|
293
|
+
// this logger is the launcher's console.
|
|
294
|
+
for (const path of result.removed) log("Removed a staging directory left by an earlier update: " + basename(path));
|
|
295
|
+
for (const entry of result.inUse) {
|
|
296
|
+
log("Left an earlier update's staging directory in place (" + entry.code + "; a file inside is still in use); the next update retries it: " + basename(entry.path));
|
|
297
|
+
}
|
|
298
|
+
for (const path of result.notOwned) {
|
|
299
|
+
log("Not removing " + basename(path) + " next to the package: this updater did not create it. Delete it by hand once no OpenCodex process is running from it.");
|
|
300
|
+
}
|
|
301
|
+
return result;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
/** Whether the launcher that ran this npm update can still drive service/tray recovery. */
|
|
305
|
+
export function launcherUsableAfterNpmUpdate(tx) {
|
|
306
|
+
return Boolean(tx?.ok || tx?.rolledBack === true || ["stage", "verify", "swap-backup"].includes(tx?.phase));
|
|
307
|
+
}
|
|
308
|
+
|
|
163
309
|
/** Largest regular file under a directory tree (bounded depth) — locates the Bun binary. */
|
|
164
310
|
function findLargestFile(root, depth = 3) {
|
|
165
311
|
let best;
|
|
@@ -247,29 +393,37 @@ export function transactionalNpmUpdate({
|
|
|
247
393
|
}) {
|
|
248
394
|
const rename = deps.rename ?? renameSync;
|
|
249
395
|
const scopeDir = dirname(packageDir);
|
|
250
|
-
|
|
396
|
+
// Leftovers from earlier attempts are cleared or stepped around first; this never fails.
|
|
397
|
+
try { sweepUpdateLeftovers({ packageDir, pkgName, log, deps }); } catch { /* report-only */ }
|
|
398
|
+
let stageRoot;
|
|
251
399
|
// GLOBAL-style staging (-g --prefix): npm nests the package's dependencies INSIDE the
|
|
252
400
|
// package dir, exactly like the live global tree this stage will replace. A local-style
|
|
253
401
|
// install would hoist bun/zod to stageRoot/node_modules — siblings that the swap would
|
|
254
402
|
// leave behind, shipping a dependency-less live tree (release-audit blocker).
|
|
255
403
|
// Layout: <stageRoot>/lib/node_modules/<pkg> on POSIX, <stageRoot>/node_modules/<pkg>
|
|
256
404
|
// on Windows.
|
|
257
|
-
const stagedCandidates = [
|
|
258
|
-
join(stageRoot, "lib", "node_modules", ...pkgName.split("/")),
|
|
259
|
-
join(stageRoot, "node_modules", ...pkgName.split("/")),
|
|
260
|
-
];
|
|
261
405
|
|
|
262
406
|
// D1: stage to the side. --prefix keeps npm entirely inside stageRoot; the live tree
|
|
263
407
|
// and the npm bin shims are untouched until the swap. A failure HERE (mkdir EACCES,
|
|
264
408
|
// ENOSPC) must NOT fall back to the destructive legacy install (review High 4): the
|
|
265
409
|
// caller sees a normal phase failure with live untouched.
|
|
266
410
|
try {
|
|
267
|
-
|
|
411
|
+
stageRoot = createOwnedStage(scopeDir, pkgName, deps);
|
|
268
412
|
} catch (error) {
|
|
269
413
|
return { ok: false, phase: "stage", error: "could not create staging directory: " + (error?.message ?? String(error)) };
|
|
270
414
|
}
|
|
415
|
+
const stagedCandidatesIn = root => [
|
|
416
|
+
join(root, "lib", "node_modules", ...pkgName.split("/")),
|
|
417
|
+
join(root, "node_modules", ...pkgName.split("/")),
|
|
418
|
+
];
|
|
419
|
+
const discardStage = () => {
|
|
420
|
+
const removal = removeOwnedStage(stageRoot, deps);
|
|
421
|
+
if (!removal.removed) {
|
|
422
|
+
log("Left this update's staging directory in place (" + removal.code + "; a file inside is still in use); the next update removes it: " + basename(stageRoot));
|
|
423
|
+
}
|
|
424
|
+
};
|
|
271
425
|
const spec = pkgName + "@" + (targetVersion || tag);
|
|
272
|
-
log("Staging " + spec + " into " + stageRoot);
|
|
426
|
+
log("Staging " + spec + " into " + basename(stageRoot) + " next to the package");
|
|
273
427
|
// npm 12 blocks lifecycle scripts by default. Bun's postinstall copies the selected
|
|
274
428
|
// @oven/bun-* executable into bun/bin, so a successful npm exit without this narrow
|
|
275
429
|
// approval leaves the staged tree intentionally incomplete. Allow only the package
|
|
@@ -279,19 +433,19 @@ export function transactionalNpmUpdate({
|
|
|
279
433
|
"--allow-scripts=bun", "--no-audit", "--no-fund", spec,
|
|
280
434
|
]);
|
|
281
435
|
if (install.status !== 0) {
|
|
282
|
-
|
|
436
|
+
discardStage();
|
|
283
437
|
return { ok: false, phase: "stage", error: "npm staging install failed (" + (install.status ?? "?") + ")" };
|
|
284
438
|
}
|
|
285
|
-
const stagedPackage =
|
|
439
|
+
const stagedPackage = stagedCandidatesIn(stageRoot).find(dir => existsSync(join(dir, "package.json")));
|
|
286
440
|
if (!stagedPackage) {
|
|
287
|
-
|
|
441
|
+
discardStage();
|
|
288
442
|
return { ok: false, phase: "verify", error: "staged package directory not found under " + stageRoot };
|
|
289
443
|
}
|
|
290
444
|
|
|
291
445
|
// D2: verify INSIDE the stage. Live is still untouched on any failure here.
|
|
292
446
|
const staged = verifyInstallTree(stagedPackage, targetVersion || undefined);
|
|
293
447
|
if (!staged.ok) {
|
|
294
|
-
|
|
448
|
+
discardStage();
|
|
295
449
|
return { ok: false, phase: "verify", error: "staged tree failed verification: " + staged.failures.join("; ") };
|
|
296
450
|
}
|
|
297
451
|
|
|
@@ -301,13 +455,13 @@ export function transactionalNpmUpdate({
|
|
|
301
455
|
try {
|
|
302
456
|
mkdirSync(backupRoot, { recursive: true });
|
|
303
457
|
} catch (error) {
|
|
304
|
-
|
|
458
|
+
discardStage();
|
|
305
459
|
return { ok: false, phase: "swap-backup", error: "could not create backup directory: " + (error?.message ?? String(error)) };
|
|
306
460
|
}
|
|
307
461
|
try {
|
|
308
462
|
renameWithRetry(rename, packageDir, backupPackage);
|
|
309
463
|
} catch (error) {
|
|
310
|
-
|
|
464
|
+
discardStage();
|
|
311
465
|
try { rmSync(backupRoot, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
312
466
|
return { ok: false, phase: "swap-backup", error: "could not move live tree aside: " + (error?.message ?? String(error)) };
|
|
313
467
|
}
|
|
@@ -317,7 +471,7 @@ export function transactionalNpmUpdate({
|
|
|
317
471
|
// Rollback: reverse the first rename. Double fault leaves the recovery marker.
|
|
318
472
|
try {
|
|
319
473
|
renameWithRetry(rename, backupPackage, packageDir);
|
|
320
|
-
|
|
474
|
+
discardStage();
|
|
321
475
|
try { rmSync(backupRoot, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
322
476
|
return { ok: false, phase: "swap-live", rolledBack: true, error: "could not place staged tree: " + (error?.message ?? String(error)) };
|
|
323
477
|
} catch (rollbackError) {
|
|
@@ -335,9 +489,12 @@ export function transactionalNpmUpdate({
|
|
|
335
489
|
const liveCheck = verifyInstallTree(packageDir, targetVersion || undefined);
|
|
336
490
|
if (!liveCheck.ok) {
|
|
337
491
|
try {
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
492
|
+
// Move the rejected tree into our own stage instead of deleting it in place: a file held
|
|
493
|
+
// open there must not turn a clean rollback into a double fault. The stage's marker keeps
|
|
494
|
+
// it sweepable by the next update.
|
|
495
|
+
renameWithRetry(rename, packageDir, join(stageRoot, "rejected"));
|
|
496
|
+
renameWithRetry(rename, backupPackage, packageDir);
|
|
497
|
+
discardStage();
|
|
341
498
|
try { rmSync(backupRoot, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
342
499
|
return { ok: false, phase: "post-verify", rolledBack: true, error: "live tree failed post-swap verification: " + liveCheck.failures.join("; ") };
|
|
343
500
|
} catch (rollbackError) {
|
|
@@ -355,6 +512,6 @@ export function transactionalNpmUpdate({
|
|
|
355
512
|
// Success: stage scaffolding is disposable now; the backup stays until the next
|
|
356
513
|
// healthy boot reaps it (bootRestoreProbe) — the process that spawned this update may
|
|
357
514
|
// still hold the old cwd.
|
|
358
|
-
|
|
515
|
+
discardStage();
|
|
359
516
|
return { ok: true, phase: "done", backup: backupPackage };
|
|
360
517
|
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What to tell the operator after a failed npm self-update (#5624).
|
|
3
|
+
*
|
|
4
|
+
* The transactional updater (`transactional-install.mjs`) either leaves the live package
|
|
5
|
+
* untouched, rolls it back, or — only on a double fault — leaves it moved aside with a recovery
|
|
6
|
+
* marker. Each of those needs a different next step, and the one command a user most often
|
|
7
|
+
* reaches for after a failed update, a bare `npm install -g` under a live proxy, is the one that
|
|
8
|
+
* fences the proxy (#5496). So the guidance stops the proxy first.
|
|
9
|
+
*
|
|
10
|
+
* Plain ESM with no Bun APIs: the Node launcher (`bin/ocx.mjs`) imports it.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Phases in which the live package was never touched. */
|
|
14
|
+
const UNTOUCHED_PHASES = new Set(["stage", "verify", "swap-backup"]);
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {{ phase?: string; rolledBack?: boolean; pkgName: string; version?: string; tag?: string }} failure
|
|
18
|
+
* @returns {{ previousVersionKept: boolean; lines: string[] }}
|
|
19
|
+
*/
|
|
20
|
+
export function npmUpdateFailureGuidance({ phase, rolledBack, pkgName, version, tag }) {
|
|
21
|
+
const reinstall = "npm install -g --allow-scripts=bun " + pkgName + "@" + (version || tag || "latest");
|
|
22
|
+
const kept = UNTOUCHED_PHASES.has(phase ?? "") || rolledBack === true;
|
|
23
|
+
if (!kept) {
|
|
24
|
+
return {
|
|
25
|
+
previousVersionKept: false,
|
|
26
|
+
lines: [
|
|
27
|
+
"The previous version was moved aside and could not be put back automatically.",
|
|
28
|
+
"Next: run the \"restore\" command recorded in .ocx-recovery.json next to the package, or reinstall with '" + reinstall + "'.",
|
|
29
|
+
],
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
previousVersionKept: true,
|
|
34
|
+
lines: [
|
|
35
|
+
"The previous version is still installed.",
|
|
36
|
+
"Next: run 'ocx update' again. If it fails the same way, run 'ocx stop', then '" + reinstall
|
|
37
|
+
+ "', then 'ocx service restart' ('ocx start' when no background service is installed).",
|
|
38
|
+
],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The dashboard job only sees the launcher's exit status; its output is withheld because it can
|
|
44
|
+
* carry local paths. Point at the terminal, where the launcher prints the phase-specific step.
|
|
45
|
+
*/
|
|
46
|
+
export const GUI_UPDATE_FAILURE_NEXT_STEP =
|
|
47
|
+
"Run 'ocx update' in a terminal to see the reason and the exact recovery step.";
|