@bitkyc08/opencodex 2.7.43-preview.20260728 → 2.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -1
- package/bin/ocx.mjs +47 -22
- package/gui/dist/assets/index-BDjpkcRN.js +67 -0
- package/gui/dist/assets/index-BHsKRFh9.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/AGENTS.md +28 -0
- package/src/adapters/anthropic.ts +15 -6
- package/src/adapters/cursor/discovery.ts +4 -1
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/native-exec-shell.ts +18 -6
- package/src/adapters/cursor/protobuf-events.ts +24 -2
- package/src/adapters/cursor/protobuf-request.ts +1 -2
- package/src/adapters/cursor/tool-definitions.ts +68 -29
- package/src/adapters/google-wire-compiler.ts +4 -0
- package/src/adapters/google.ts +128 -2
- package/src/adapters/identity.ts +12 -2
- package/src/adapters/kiro.ts +64 -7
- package/src/adapters/mimo-free.ts +2 -0
- package/src/adapters/openai-responses.ts +246 -59
- package/src/claude/agents-inject.ts +5 -0
- package/src/claude/alias.ts +94 -14
- package/src/claude/inbound.ts +26 -9
- package/src/claude/outbound.ts +6 -3
- package/src/cli/account-auth.ts +1 -1
- package/src/cli/agent-driven.ts +37 -0
- package/src/cli/catalog-prewarm.ts +24 -0
- package/src/cli/claude.ts +35 -10
- package/src/cli/doctor.ts +71 -19
- package/src/cli/help.ts +42 -6
- package/src/cli/index.ts +93 -19
- package/src/cli/interactive-confirm.ts +133 -0
- package/src/cli/opencode.ts +701 -0
- package/src/cli/provider-runtime.ts +3 -0
- package/src/cli/provider.ts +31 -10
- package/src/cli/star-prompt.ts +79 -18
- package/src/cli/status.ts +47 -13
- package/src/cli/v2.ts +10 -1
- package/src/codex/account-id.ts +34 -0
- package/src/codex/account-lifecycle.ts +4 -1
- 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-store.ts +2 -0
- package/src/codex/account-usability.ts +6 -1
- package/src/codex/app-server-processes.ts +511 -0
- package/src/codex/auth-api.ts +293 -34
- package/src/codex/auth-collision.ts +2 -1
- package/src/codex/auth-context.ts +60 -17
- package/src/codex/catalog/bundled.ts +9 -2
- package/src/codex/catalog/parsing.ts +42 -2
- package/src/codex/catalog/provider-fetch.ts +264 -70
- package/src/codex/catalog/sync.ts +45 -8
- package/src/codex/catalog.ts +2 -2
- package/src/codex/features.ts +524 -5
- package/src/codex/history-provider.ts +145 -1
- package/src/codex/inject.ts +114 -14
- package/src/codex/main-account.ts +2 -8
- package/src/codex/pool-rotation.ts +186 -0
- package/src/codex/quota.ts +92 -2
- package/src/codex/routing.ts +695 -106
- package/src/codex/runtime.ts +10 -1
- package/src/codex/shim.ts +4 -1
- package/src/codex/subagent-defaults.ts +550 -0
- package/src/codex/subagent-model-fallback.ts +2 -0
- package/src/codex/sync.ts +3 -0
- package/src/config.ts +574 -25
- package/src/generated/jawcode-model-metadata.ts +12 -12
- package/src/github/star-state.ts +191 -0
- package/src/images/artifacts.ts +516 -0
- package/src/images/fulfill-video.ts +163 -0
- package/src/images/fulfill.ts +111 -0
- package/src/images/index.ts +4 -0
- package/src/images/loop.ts +789 -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/lib/admin-secrets.ts +25 -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 +6 -20
- package/src/lib/config-ownership.ts +327 -0
- package/src/lib/crash-guard.ts +2 -0
- package/src/lib/destination-policy.ts +132 -7
- package/src/lib/pinned-http.ts +151 -0
- package/src/lib/process-control.ts +2 -2
- package/src/lib/provider-outbound.ts +167 -0
- package/src/lib/provider-url.ts +14 -0
- package/src/lib/proxy-env.ts +18 -0
- package/src/lib/shadow-call.ts +30 -0
- package/src/lib/test-home-guard.ts +90 -0
- package/src/lib/win-exec.ts +12 -2
- package/src/lib/windows-elevation.ts +81 -3
- package/src/lib/windows-secret-acl.ts +189 -12
- package/src/lib/winsw.ts +2 -0
- package/src/oauth/anthropic-routing.ts +570 -0
- package/src/oauth/health.ts +6 -0
- package/src/oauth/index.ts +310 -75
- package/src/oauth/key-providers.ts +38 -8
- package/src/oauth/kimi.ts +2 -0
- package/src/oauth/kiro-credentials.ts +373 -12
- package/src/oauth/kiro.ts +424 -43
- package/src/oauth/login-cli.ts +33 -6
- package/src/oauth/store.ts +56 -4
- package/src/oauth/types.ts +11 -0
- package/src/providers/alibaba-region-migration.ts +16 -3
- package/src/providers/antigravity-models.ts +3 -0
- package/src/providers/api-keys.ts +13 -6
- package/src/providers/derive.ts +8 -2
- package/src/providers/key-failover.ts +24 -4
- package/src/providers/model-discovery.ts +356 -0
- package/src/providers/quota.ts +233 -29
- package/src/providers/registry.ts +125 -3
- package/src/responses/parser.ts +11 -0
- package/src/responses/state.ts +22 -8
- package/src/responses/tool-groups.ts +19 -0
- package/src/router.ts +19 -7
- package/src/server/auth-cors.ts +114 -24
- package/src/server/claude-messages.ts +8 -1
- package/src/server/gui-static.ts +30 -6
- package/src/server/images.ts +303 -9
- package/src/server/index.ts +77 -9
- package/src/server/lifecycle.ts +25 -1
- package/src/server/live.ts +75 -25
- package/src/server/management/agent-settings-routes.ts +106 -8
- package/src/server/management/combo-routes.ts +7 -0
- package/src/server/management/config-routes.ts +22 -7
- package/src/server/management/context.ts +11 -1
- package/src/server/management/logs-usage-routes.ts +167 -3
- package/src/server/management/model-routes.ts +46 -13
- package/src/server/management/oauth-account-routes.ts +163 -17
- package/src/server/management/provider-routes.ts +73 -10
- package/src/server/management/shared.ts +2 -2
- package/src/server/management/sidebar-routes.ts +39 -0
- package/src/server/management/system-restart.ts +172 -0
- package/src/server/management/system-routes.ts +33 -10
- package/src/server/management-api.ts +5 -3
- package/src/server/management-auth.ts +216 -0
- package/src/server/proxy-liveness.ts +14 -3
- package/src/server/responses/compact.ts +21 -13
- package/src/server/responses/core.ts +614 -172
- package/src/server/responses/upstream-error.ts +48 -0
- package/src/server/responses-image-gen-repair.ts +118 -0
- package/src/server/responses-item-id-repair.ts +10 -85
- package/src/server/sse-payload-rewrite.ts +116 -0
- package/src/server/startup-action-control.ts +30 -14
- package/src/server/system-env.ts +28 -10
- package/src/service.ts +284 -19
- package/src/storage/cleanup-job.ts +57 -0
- package/src/storage/cleanup.ts +1504 -28
- package/src/storage/policy-job.ts +387 -0
- package/src/storage/policy-scheduler.ts +40 -0
- package/src/storage/policy-worker.ts +53 -0
- package/src/storage/policy.ts +522 -0
- package/src/storage/restore-job.ts +253 -0
- package/src/storage/restore-worker.ts +52 -0
- package/src/storage/storage-mutation-coordinator.ts +109 -0
- package/src/storage/worker-lifecycle.ts +81 -0
- package/src/tray/windows.ts +34 -4
- package/src/types.ts +107 -1
- package/src/update/badge.ts +72 -0
- package/src/update/index.ts +36 -18
- package/src/update/job.ts +111 -16
- package/src/update/npm-invocation.d.mts +23 -0
- package/src/update/npm-invocation.mjs +94 -0
- package/src/usage/debug.ts +2 -0
- package/src/usage/expected-prices.ts +6 -5
- package/src/usage/log.ts +12 -0
- package/src/web-search/loop.ts +57 -16
- package/gui/dist/assets/index-CjKFJHSC.js +0 -65
- package/gui/dist/assets/index-DfVGuN88.css +0 -1
package/src/types.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import type { KiroOAuthMetadata } from "./oauth/types";
|
|
2
|
+
|
|
1
3
|
export interface OcxParsedRequest {
|
|
2
4
|
modelId: string;
|
|
3
5
|
previousResponseId?: string;
|
|
@@ -23,6 +25,8 @@ export interface OcxParsedRequest {
|
|
|
23
25
|
* derived from the parent thread id.
|
|
24
26
|
*/
|
|
25
27
|
_cursorIsolateConversation?: boolean;
|
|
28
|
+
/** Account-scoped, non-secret Kiro request metadata selected with the OAuth access token. */
|
|
29
|
+
_kiroAuthContext?: Pick<KiroOAuthMetadata, "profileArn" | "apiRegion" | "ssoRegion">;
|
|
26
30
|
/** Provider-private continuation metadata resolved from the Responses previous_response_id chain. */
|
|
27
31
|
_providerContinuation?: OcxProviderContinuationState;
|
|
28
32
|
/**
|
|
@@ -31,6 +35,8 @@ export interface OcxParsedRequest {
|
|
|
31
35
|
* executes searches via the gpt-5.4-mini sidecar (see src/web-search). Absent when not requested.
|
|
32
36
|
*/
|
|
33
37
|
_webSearch?: Record<string, unknown>;
|
|
38
|
+
/** Hosted image_generation tool config stashed for the image bridge sidecar (see src/images). */
|
|
39
|
+
_imageGeneration?: { toolNames: Set<string>; originalTool?: Record<string, unknown> };
|
|
34
40
|
/**
|
|
35
41
|
* True when Codex requested structured output (`text.format` = json_schema/json_object). The
|
|
36
42
|
* web-search tool_result is then rendered as compact JSON instead of markdown prose, so its
|
|
@@ -152,6 +158,10 @@ export interface OcxTool {
|
|
|
152
158
|
loadedFromToolSearch?: boolean;
|
|
153
159
|
/** Synthetic web_search tool: the model's call is executed by the gpt-5.4-mini sidecar, not relayed to Codex. */
|
|
154
160
|
webSearch?: boolean;
|
|
161
|
+
/** Synthetic image_gen tool: the model's call is executed by the xAI image bridge sidecar, not relayed to Codex. */
|
|
162
|
+
imageGeneration?: boolean;
|
|
163
|
+
/** Synthetic video_gen tool: executed by the xAI video bridge sidecar. */
|
|
164
|
+
videoGeneration?: boolean;
|
|
155
165
|
}
|
|
156
166
|
|
|
157
167
|
/**
|
|
@@ -432,6 +442,11 @@ export interface OcxClaudeCodeConfig {
|
|
|
432
442
|
* free. Only ocx-*.md files are owned/pruned. Default: enabled.
|
|
433
443
|
*/
|
|
434
444
|
injectAgents?: boolean;
|
|
445
|
+
/**
|
|
446
|
+
* Optional Claude Code effort pinned in every generated ocx-* subagent
|
|
447
|
+
* definition. Unset inherits the parent session effort.
|
|
448
|
+
*/
|
|
449
|
+
subagentEffort?: "low" | "medium" | "high" | "xhigh" | "max";
|
|
435
450
|
/** Claude-originated web-search override. Unset fields inherit the global sidecar settings. */
|
|
436
451
|
webSearchSidecar?: { backend?: "openai" | "anthropic"; model?: string };
|
|
437
452
|
/** Claude-originated vision override. Unset fields inherit the global sidecar settings. */
|
|
@@ -459,6 +474,25 @@ export interface OcxClaudeDesktopProfile {
|
|
|
459
474
|
appliedAt?: string;
|
|
460
475
|
}
|
|
461
476
|
|
|
477
|
+
/**
|
|
478
|
+
* Opt-in archived-session auto-cleanup policy (issue #42 Phase 3).
|
|
479
|
+
* Persisted under `OcxConfig.storageCleanupPolicy`. Default `enabled: false`.
|
|
480
|
+
*/
|
|
481
|
+
export interface StorageCleanupPolicy {
|
|
482
|
+
/** When false/unset, the engine never mutates. Default false. */
|
|
483
|
+
enabled: boolean;
|
|
484
|
+
/** Run when archived session bytes exceed this threshold. */
|
|
485
|
+
trigger: { archivedBytesOver: number };
|
|
486
|
+
/** Either shrink archives toward a byte floor, or remove the oldest N%. */
|
|
487
|
+
target: { reduceToBytes?: number } | { removeOldestPercent?: number };
|
|
488
|
+
schedule: "startup" | "daily" | "weekly" | "manual";
|
|
489
|
+
/** Default quarantine. Permanent only when explicitly set. */
|
|
490
|
+
mode: "quarantine" | "permanent";
|
|
491
|
+
lastRun?: { at: number; freedBytes: number; removed: number };
|
|
492
|
+
/** Epoch ms when the next scheduled evaluation is due. */
|
|
493
|
+
nextRun?: number;
|
|
494
|
+
}
|
|
495
|
+
|
|
462
496
|
/** 사용자가 대시보드에서 직접 추가한 커스텀 모델 정의. */
|
|
463
497
|
export interface OcxCustomModel {
|
|
464
498
|
/** 고유 ID (crypto.randomUUID()) */
|
|
@@ -501,12 +535,28 @@ export interface OcxConfig {
|
|
|
501
535
|
*/
|
|
502
536
|
subagentModelFallbackPollMs?: number;
|
|
503
537
|
injectionModel?: string;
|
|
538
|
+
/**
|
|
539
|
+
* Opt in to synchronizing the selected injection model into Codex's native
|
|
540
|
+
* sub-agent defaults. Only meaningful while `injectionModel` is set.
|
|
541
|
+
*/
|
|
542
|
+
syncCodexSubagentDefaults?: boolean;
|
|
504
543
|
/**
|
|
505
544
|
* Optional reasoning effort the delegation prompt tells the agent to pass in spawn_agent calls
|
|
506
545
|
* (`reasoning_effort` argument). Only meaningful while `injectionModel` is set; validated against
|
|
507
546
|
* the Codex ladder (src/reasoning-effort.ts CODEX_REASONING_LEVELS) at the API boundary.
|
|
508
547
|
*/
|
|
509
548
|
injectionEffort?: string;
|
|
549
|
+
/**
|
|
550
|
+
* Explicit sideband websocket base for realtime/live joins, mirroring upstream's
|
|
551
|
+
* `experimental_realtime_ws_base_url`. The value is a ROOT (or a recognized
|
|
552
|
+
* `/realtime`, `/realtime/calls/<id>`, `/live/<id>` endpoint form, which is
|
|
553
|
+
* stripped back to the root); `/v1` is appended during normalization. Intended
|
|
554
|
+
* for local development against a fake realtime server — plaintext `http`/`ws`
|
|
555
|
+
* is accepted only for loopback hosts, and URL userinfo is rejected; both
|
|
556
|
+
* failures close to the canonical `https://api.openai.com/v1`. Configured by
|
|
557
|
+
* editing this file; there is deliberately no management-API or GUI surface.
|
|
558
|
+
*/
|
|
559
|
+
experimentalRealtimeWsBaseUrl?: string;
|
|
510
560
|
/**
|
|
511
561
|
* Model ids the user has EXCLUDED from the Grok Build managed block. Absent or empty
|
|
512
562
|
* means "everything visible", which is the historical behaviour — so an existing
|
|
@@ -613,6 +663,12 @@ export interface OcxConfig {
|
|
|
613
663
|
shutdownTimeoutMs?: number;
|
|
614
664
|
/** Advertise supports_websockets so Codex opens the WS endpoint. Default false; set true to opt in. */
|
|
615
665
|
websockets?: boolean;
|
|
666
|
+
/**
|
|
667
|
+
* Opt-in auto-cleanup policy for archived Codex sessions (issue #42 Phase 3).
|
|
668
|
+
* Default OFF (`enabled` false / unset). Never enabled implicitly.
|
|
669
|
+
* See `src/storage/policy.ts`.
|
|
670
|
+
*/
|
|
671
|
+
storageCleanupPolicy?: StorageCleanupPolicy;
|
|
616
672
|
/** Generated API keys for external access to the proxy's /v1/responses endpoint. */
|
|
617
673
|
apiKeys?: Array<{ id: string; name: string; key: string; createdAt: string }>;
|
|
618
674
|
/** Auto-start/sync the proxy from the Codex shim before launching Codex. Default true. */
|
|
@@ -640,12 +696,38 @@ export interface OcxConfig {
|
|
|
640
696
|
search?: OcxSearchConfig;
|
|
641
697
|
/** Codex multi-account pool. */
|
|
642
698
|
codexAccounts?: CodexAccount[];
|
|
699
|
+
/** Account ids administratively excluded from future pool selection until resumed. */
|
|
700
|
+
pausedCodexAccountIds?: string[];
|
|
701
|
+
/**
|
|
702
|
+
* Public model-selector namespaces bound to one Codex account. Values are stored account ids;
|
|
703
|
+
* `"@main"` selects the Codex Desktop/main auth.json account. Account display aliases
|
|
704
|
+
* are intentionally separate from these selectors.
|
|
705
|
+
*/
|
|
706
|
+
codexAccountNamespaces?: Record<string, string>;
|
|
643
707
|
/** Active pool account id for next session. undefined = main (passthrough as-is). */
|
|
644
708
|
activeCodexAccountId?: string;
|
|
645
709
|
/** Auto-switch threshold (0-100). Default 80. 0 = disabled. */
|
|
646
710
|
autoSwitchThreshold?: number;
|
|
711
|
+
/** New-session account rotation strategy for the Codex pool. Default quota (today's behaviour). */
|
|
712
|
+
accountPoolStrategy?: OcxAccountPoolRotationStrategy;
|
|
713
|
+
/** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
|
|
714
|
+
accountPoolStickyLimit?: number;
|
|
647
715
|
/** Consecutive non-2xx upstream responses before switching future new threads. Default 3. 0 = disabled. */
|
|
648
716
|
upstreamFailoverThreshold?: number;
|
|
717
|
+
/**
|
|
718
|
+
* Opt-in Anthropic OAuth account pool (#294). Default OFF.
|
|
719
|
+
* Failover on 429 + sticky affinity; new sessions may pick lowest known 5h usage.
|
|
720
|
+
* Experimental — see docs and GUI warning before enabling.
|
|
721
|
+
*/
|
|
722
|
+
anthropicAccountPool?: {
|
|
723
|
+
enabled?: boolean;
|
|
724
|
+
/** Usage % threshold for new-session auto-pick. Default 80. 0 = disabled (affinity/active only). */
|
|
725
|
+
autoSwitchThreshold?: number;
|
|
726
|
+
/** New-session rotation strategy. Default quota (today's behaviour). */
|
|
727
|
+
strategy?: OcxAccountPoolRotationStrategy;
|
|
728
|
+
/** Successful new-session binds retained on one round-robin selection. Default 1; range 1..100. */
|
|
729
|
+
stickyLimit?: number;
|
|
730
|
+
};
|
|
649
731
|
/** Virtual `combo/<id>` models spanning concrete provider/model targets (issue #133). */
|
|
650
732
|
combos?: Record<string, OcxComboConfig>;
|
|
651
733
|
/** Background proactive token refresh ("Token Guardian"). Off by default; see OcxTokenGuardianConfig. */
|
|
@@ -654,6 +736,8 @@ export interface OcxConfig {
|
|
|
654
736
|
corsAllowOrigins?: string[];
|
|
655
737
|
}
|
|
656
738
|
|
|
739
|
+
export type OcxAccountPoolRotationStrategy = "quota" | "round-robin" | "fill-first";
|
|
740
|
+
|
|
657
741
|
export type OcxComboStrategy = "failover" | "round-robin";
|
|
658
742
|
export type OcxComboDefaultEffort = "low" | "medium" | "high" | "xhigh" | "max" | "ultra";
|
|
659
743
|
|
|
@@ -714,8 +798,24 @@ export interface OcxTokenGuardianConfig {
|
|
|
714
798
|
export interface OcxImagesConfig {
|
|
715
799
|
/** Optional custom API-key provider for /v1/images relays. Built-in OpenAI tiers remain automatic. */
|
|
716
800
|
provider?: string;
|
|
717
|
-
/** Upstream timeout (ms) for one /v1/images relay. Default
|
|
801
|
+
/** Upstream timeout (ms) for one image generation/edit call (bridge xAI + /v1/images relay). Default 60000 for the bridge; relay may use a higher default (300000). */
|
|
718
802
|
timeoutMs?: number;
|
|
803
|
+
/** Master switch for the image bridge. Default false — set true to enable paid xAI Grok Imagine generation. */
|
|
804
|
+
bridgeEnabled?: boolean;
|
|
805
|
+
/** xAI image model id. Default "grok-imagine-image-quality" (see DEFAULT_MODEL in images/plan.ts). */
|
|
806
|
+
bridgeModel?: string;
|
|
807
|
+
/** Max image-generation loop iterations before forced-final. Default 3; clamped to [0, 10]. */
|
|
808
|
+
maxRounds?: number;
|
|
809
|
+
/** Max files retained under artifacts/. Oldest deleted when exceeded. Default 200. */
|
|
810
|
+
artifactsKeepCount?: number;
|
|
811
|
+
/** Master switch for the video bridge. Default false — must be explicitly opted in. */
|
|
812
|
+
videoBridgeEnabled?: boolean;
|
|
813
|
+
/** Model for xAI video generation. Default "grok-imagine-video". */
|
|
814
|
+
videoBridgeModel?: string;
|
|
815
|
+
/** Max video-gen rounds before forced-final. Default 2 (video is slower than image). */
|
|
816
|
+
videoMaxRounds?: number;
|
|
817
|
+
/** Per-video generation timeout (ms) including polling. Default 300000 (5 min). */
|
|
818
|
+
videoTimeoutMs?: number;
|
|
719
819
|
}
|
|
720
820
|
|
|
721
821
|
export interface OcxSearchConfig {
|
|
@@ -817,6 +917,12 @@ export interface OcxProviderConfig {
|
|
|
817
917
|
*/
|
|
818
918
|
codexAccountMode?: CodexAccountMode;
|
|
819
919
|
apiKey?: string;
|
|
920
|
+
/**
|
|
921
|
+
* Key-auth header style for Anthropic-compatible providers.
|
|
922
|
+
* Defaults to the native Anthropic `x-api-key`; gateways may require
|
|
923
|
+
* `Authorization: Bearer <key>` instead.
|
|
924
|
+
*/
|
|
925
|
+
apiKeyTransport?: "x-api-key" | "bearer";
|
|
820
926
|
/**
|
|
821
927
|
* Multi-key pool (API-key twin of OAuth multiauth). `apiKey` always mirrors the ACTIVE
|
|
822
928
|
* entry so routing stays single-key; managed via /api/providers/keys. A legacy bare
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cached "is an update available?" answer for the GUI sidebar badge.
|
|
3
|
+
*
|
|
4
|
+
* `/api/update/check` spawns `npm view` on every call (~1s, network-bound), so a
|
|
5
|
+
* sidebar that polls it would spawn a process per tick on every page of the GUI.
|
|
6
|
+
* The badge instead READS the 20h version cache the CLI update prompt already
|
|
7
|
+
* maintains (`~/.opencodex/version.json`).
|
|
8
|
+
*
|
|
9
|
+
* This is deliberately read-only: it must never trigger a registry refresh. The GUI
|
|
10
|
+
* polls it, so a refresh-on-read would let repeated polls launch repeated `npm view`
|
|
11
|
+
* helpers with no coalescing. Cache warming stays with `ocx start`
|
|
12
|
+
* (`triggerBackgroundRefreshIfStale` in `src/update/notify.ts`) and with the explicit
|
|
13
|
+
* `/api/update/check` the user reaches by clicking the sidebar update button.
|
|
14
|
+
*/
|
|
15
|
+
import { currentVersion, defaultUpdateTag, detectInstall, type Channel } from "./index";
|
|
16
|
+
import { isNewer, isSourceBuildVersion, readVersionCache } from "./notify";
|
|
17
|
+
|
|
18
|
+
export interface UpdateBadge {
|
|
19
|
+
/** True only when a newer version exists on the current channel. */
|
|
20
|
+
updateAvailable: boolean;
|
|
21
|
+
currentVersion: string;
|
|
22
|
+
latestVersion: string | null;
|
|
23
|
+
channel: Channel;
|
|
24
|
+
/** False for source checkouts, where the GUI cannot offer a one-click update. */
|
|
25
|
+
canUpdate: boolean;
|
|
26
|
+
/** True when no cached registry answer exists yet, so "no update" is unproven. */
|
|
27
|
+
unknown: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface UpdateBadgeDeps {
|
|
31
|
+
currentVersion: () => string;
|
|
32
|
+
detectInstall: () => ReturnType<typeof detectInstall>;
|
|
33
|
+
readCache: (channel: Channel) => ReturnType<typeof readVersionCache>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const defaultDeps: UpdateBadgeDeps = {
|
|
37
|
+
currentVersion,
|
|
38
|
+
detectInstall,
|
|
39
|
+
readCache: readVersionCache,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read-only badge state. Source checkouts and unknown versions report no update
|
|
44
|
+
* rather than a dead badge the user cannot act on.
|
|
45
|
+
*/
|
|
46
|
+
export function readUpdateBadge(deps: UpdateBadgeDeps = defaultDeps): UpdateBadge {
|
|
47
|
+
const current = deps.currentVersion();
|
|
48
|
+
const installer = deps.detectInstall();
|
|
49
|
+
const channel = defaultUpdateTag(current);
|
|
50
|
+
const base: UpdateBadge = {
|
|
51
|
+
updateAvailable: false,
|
|
52
|
+
currentVersion: current,
|
|
53
|
+
latestVersion: null,
|
|
54
|
+
channel,
|
|
55
|
+
canUpdate: installer !== "source",
|
|
56
|
+
unknown: true,
|
|
57
|
+
};
|
|
58
|
+
// A source checkout has nothing to compare against, so "unknown" is not useful there.
|
|
59
|
+
if (installer === "source" || current === "?" || isSourceBuildVersion(current)) {
|
|
60
|
+
return { ...base, canUpdate: false, unknown: false };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const cache = deps.readCache(channel);
|
|
64
|
+
if (!cache) return base;
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
...base,
|
|
68
|
+
latestVersion: cache.latest_version,
|
|
69
|
+
updateAvailable: isNewer(cache.latest_version, current, channel),
|
|
70
|
+
unknown: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
package/src/update/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { readFileSync, readdirSync } from "node:fs";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
|
|
6
|
+
import { npmInvocation } from "./npm-invocation.mjs";
|
|
6
7
|
import { handoffWindowsTrayForUpdate, planWindowsTrayUpdate } from "./tray-update-plan.mjs";
|
|
7
8
|
|
|
8
9
|
/**
|
|
@@ -50,19 +51,24 @@ export function updateTag(current: string): Channel {
|
|
|
50
51
|
return defaultUpdateTag(current);
|
|
51
52
|
}
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
function npmSpawnTarget(args: readonly string[]): { bin: string; args: string[]; options: { windowsVerbatimArguments?: boolean } } | null {
|
|
55
|
+
const invocation = npmInvocation(args);
|
|
56
|
+
if (!invocation) return null;
|
|
57
|
+
return { bin: invocation.file, args: invocation.args, options: invocation.options };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function updateSpawnTarget(bin: string, args: readonly string[]): { bin: string; args: string[]; options: { windowsVerbatimArguments?: boolean } } | null {
|
|
61
|
+
if (bin === "npm") return npmSpawnTarget(args);
|
|
62
|
+
if (process.platform === "win32" && bin === "bun") {
|
|
63
|
+
return { bin: process.execPath, args: [...args], options: {} };
|
|
64
|
+
}
|
|
65
|
+
return { bin, args: [...args], options: {} };
|
|
60
66
|
}
|
|
61
67
|
|
|
62
68
|
/**
|
|
63
69
|
* The GUI update worker sets OCX_SERVICE=1 and has stdio ignored — inheriting that for
|
|
64
|
-
*
|
|
65
|
-
* relay bounded output after the child exits. (Ported from PR #167.)
|
|
70
|
+
* Background package-manager children can open stacked visible consoles on Windows.
|
|
71
|
+
* Pipe instead and relay bounded output after the child exits. (Ported from PR #167.)
|
|
66
72
|
*/
|
|
67
73
|
function updateChildStdio(): "inherit" | "pipe" {
|
|
68
74
|
if (process.env.OCX_SERVICE === "1") return "pipe";
|
|
@@ -79,8 +85,14 @@ function logSpawnOutput(label: string, result: { stdout?: string | Buffer | null
|
|
|
79
85
|
|
|
80
86
|
/** Latest published version from the registry (best-effort; null if npm isn't available). */
|
|
81
87
|
export function latestVersion(tag: string): string | null {
|
|
82
|
-
const npm = npmSpawnTarget("
|
|
83
|
-
|
|
88
|
+
const npm = npmSpawnTarget(["view", `${PKG}@${tag}`, "version"]);
|
|
89
|
+
if (!npm) return null;
|
|
90
|
+
const r = spawnSync(npm.bin, npm.args, {
|
|
91
|
+
encoding: "utf8",
|
|
92
|
+
timeout: 12000,
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
...npm.options,
|
|
95
|
+
});
|
|
84
96
|
return r.status === 0 ? (r.stdout.trim() || null) : null;
|
|
85
97
|
}
|
|
86
98
|
|
|
@@ -116,11 +128,12 @@ export function checkUpdatePackageIntegrity(
|
|
|
116
128
|
spawn: typeof spawnSync = spawnSync,
|
|
117
129
|
): { ok: true; integrity: string } | { ok: false; reason: string } | { ok: "skipped"; reason: string } {
|
|
118
130
|
if (!version) return { ok: "skipped", reason: "no resolved version (registry unavailable)" };
|
|
119
|
-
const npm = npmSpawnTarget("
|
|
131
|
+
const npm = npmSpawnTarget(["view", `${PKG}@${version}`, "dist.integrity"]);
|
|
132
|
+
if (!npm) return { ok: "skipped", reason: "npm executable was not found on a trusted PATH entry" };
|
|
120
133
|
const r = spawn(
|
|
121
134
|
npm.bin,
|
|
122
|
-
|
|
123
|
-
{ encoding: "utf8", timeout: 12000, windowsHide: true,
|
|
135
|
+
npm.args,
|
|
136
|
+
{ encoding: "utf8", timeout: 12000, windowsHide: true, ...npm.options },
|
|
124
137
|
);
|
|
125
138
|
// status !== 0 covers nonzero exits AND timeouts (status === null).
|
|
126
139
|
if (r.status !== 0) return { ok: "skipped", reason: `registry integrity query failed (status ${r.status ?? "timeout"})` };
|
|
@@ -164,6 +177,13 @@ export async function runUpdate(): Promise<void> {
|
|
|
164
177
|
console.log(`Verified ${PKG}@${latest} integrity metadata ${integrity.integrity.slice(0, 24)}…`);
|
|
165
178
|
}
|
|
166
179
|
|
|
180
|
+
const { bin, args: cmdArgs } = updateCommand(installer, tag, latest);
|
|
181
|
+
const target = updateSpawnTarget(bin, cmdArgs);
|
|
182
|
+
if (!target) {
|
|
183
|
+
console.error("⚠️ Could not resolve npm from a trusted absolute PATH entry; aborting before stopping the proxy.");
|
|
184
|
+
process.exit(1);
|
|
185
|
+
}
|
|
186
|
+
|
|
167
187
|
// Remember whether a background service manages the proxy BEFORE stopping — `ocx stop`
|
|
168
188
|
// unloads it permanently, so a successful update must reinstall/restart it afterwards.
|
|
169
189
|
let serviceWasInstalled = false;
|
|
@@ -240,17 +260,15 @@ export async function runUpdate(): Promise<void> {
|
|
|
240
260
|
}
|
|
241
261
|
}
|
|
242
262
|
|
|
243
|
-
const { bin, args: cmdArgs } = updateCommand(installer, tag, latest);
|
|
244
263
|
console.log(`Updating${latest ? ` to v${latest}` : ""}…\n$ ${bin} ${cmdArgs.join(" ")}`);
|
|
245
264
|
|
|
246
|
-
const target = npmSpawnTarget(bin);
|
|
247
265
|
const installStdio = updateChildStdio();
|
|
248
|
-
const r = spawnSync(target.bin,
|
|
266
|
+
const r = spawnSync(target.bin, target.args, {
|
|
249
267
|
stdio: installStdio,
|
|
250
268
|
encoding: installStdio === "pipe" ? "utf8" : undefined,
|
|
251
269
|
timeout: 180000,
|
|
252
270
|
windowsHide: true,
|
|
253
|
-
|
|
271
|
+
...target.options,
|
|
254
272
|
});
|
|
255
273
|
if (installStdio === "pipe") logSpawnOutput("", r);
|
|
256
274
|
if (r.status === 0) {
|
package/src/update/job.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
|
3
3
|
import { dirname, join } from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { atomicWriteFile, getConfigDir, loadConfig, readPid, readRuntimePort } from "../config";
|
|
6
|
-
import { killProxy } from "../lib/process-control";
|
|
6
|
+
import { isProcessAlive, killProxy } from "../lib/process-control";
|
|
7
7
|
import { reclaimListenPort } from "../server/port-reclaim";
|
|
8
8
|
import { isOpencodexHealthz, probeHostname, proxyIdentityAt, type HealthzIdentity } from "../server/proxy-liveness";
|
|
9
9
|
import { isServiceInstalled } from "../service";
|
|
@@ -26,8 +26,10 @@ const RELEASE_NOTES_URL = "https://github.com/lidge-jun/opencodex/releases/lates
|
|
|
26
26
|
const UPDATE_JOB_FILENAME = "update-job.json";
|
|
27
27
|
const UPDATE_TIMEOUT_MS = 180_000;
|
|
28
28
|
const RESTART_TIMEOUT_MS = 60_000;
|
|
29
|
-
const RESTART_HEALTH_TIMEOUT_MS =
|
|
29
|
+
const RESTART_HEALTH_TIMEOUT_MS = 30_000;
|
|
30
30
|
const RESTART_STABILITY_WINDOW_MS = 15_000;
|
|
31
|
+
/** Legacy active records did not persist a worker PID, so age is their only safe recovery signal. */
|
|
32
|
+
export const UPDATE_JOB_LEGACY_STALE_MS = 10 * 60_000;
|
|
31
33
|
/** How long update restart waits for the captured port to become bindable after stop. */
|
|
32
34
|
export const RESTART_PORT_RECLAIM_MS = 30_000;
|
|
33
35
|
|
|
@@ -77,6 +79,19 @@ export interface UpdateCheckDeps {
|
|
|
77
79
|
latestVersion: (tag: Channel) => string | null;
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
interface UpdateWorkerProcess {
|
|
83
|
+
pid?: number;
|
|
84
|
+
unref(): void;
|
|
85
|
+
once(event: "error", listener: (error: Error) => void): unknown;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export interface StartUpdateJobDeps {
|
|
89
|
+
checkForUpdateFn: (channel: Channel) => UpdateCheckResult;
|
|
90
|
+
spawnWorkerFn: (jobId: string, channel: Channel, restart: boolean) => UpdateWorkerProcess;
|
|
91
|
+
isProcessAliveFn: (pid: number) => boolean;
|
|
92
|
+
nowMs: () => number;
|
|
93
|
+
}
|
|
94
|
+
|
|
80
95
|
const defaultCheckDeps: UpdateCheckDeps = {
|
|
81
96
|
currentVersion,
|
|
82
97
|
detectInstall,
|
|
@@ -155,7 +170,9 @@ export function updateExecutionCommand(
|
|
|
155
170
|
return { bin, args, display: formatCommand(bin, args) };
|
|
156
171
|
}
|
|
157
172
|
if (installer === "bun") {
|
|
158
|
-
const
|
|
173
|
+
const command = updateCommand(installer, channel, resolvedVersion);
|
|
174
|
+
const bin = process.platform === "win32" ? process.execPath : command.bin;
|
|
175
|
+
const { args } = command;
|
|
159
176
|
return { bin, args, display: updateCommandStr(installer, channel, resolvedVersion) };
|
|
160
177
|
}
|
|
161
178
|
return { bin: "sh", args: ["-lc", manualSourceCommand()], display: manualSourceCommand() };
|
|
@@ -225,19 +242,77 @@ function newJobId(): string {
|
|
|
225
242
|
return `${Date.now()}-${Math.random().toString(36).slice(2, 10)}`;
|
|
226
243
|
}
|
|
227
244
|
|
|
228
|
-
|
|
245
|
+
/**
|
|
246
|
+
* [Decision Log]
|
|
247
|
+
* - Purpose: recover dashboard updates after a detached worker dies without unlocking concurrent live updates.
|
|
248
|
+
* - Existing constraints: legacy records have no PID, while a healthy update may legitimately run for minutes.
|
|
249
|
+
* - Alternatives considered: clear every active record by age, or require operators to delete the file manually.
|
|
250
|
+
* - Chosen approach: trust PID liveness first and use a conservative age limit only for legacy no-PID records.
|
|
251
|
+
* - Why: age-only recovery can start two installers, while never recovering leaves the dashboard permanently blocked.
|
|
252
|
+
* - Impact: live PID records remain locked regardless of age; dead PIDs recover immediately; legacy records recover after ten minutes.
|
|
253
|
+
*/
|
|
254
|
+
export function staleActiveUpdateJobReason(
|
|
255
|
+
job: Pick<UpdateJobState, "status" | "pid" | "updatedAt">,
|
|
256
|
+
now = Date.now(),
|
|
257
|
+
isAlive: (pid: number) => boolean = isProcessAlive,
|
|
258
|
+
): string | null {
|
|
259
|
+
if (job.status !== "running" && job.status !== "restarting") return null;
|
|
260
|
+
if (typeof job.pid === "number" && Number.isSafeInteger(job.pid) && job.pid > 0) {
|
|
261
|
+
return isAlive(job.pid) ? null : `update worker PID ${job.pid} is no longer running`;
|
|
262
|
+
}
|
|
263
|
+
const updatedAt = Date.parse(job.updatedAt);
|
|
264
|
+
if (Number.isFinite(updatedAt) && now - updatedAt >= UPDATE_JOB_LEGACY_STALE_MS) {
|
|
265
|
+
return "legacy active update record has no worker PID and exceeded the stale window";
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const defaultStartUpdateJobDeps: StartUpdateJobDeps = {
|
|
271
|
+
checkForUpdateFn: channel => checkForUpdate(channel),
|
|
272
|
+
spawnWorkerFn: (jobId, channel, restart) => spawn(
|
|
273
|
+
process.execPath,
|
|
274
|
+
[process.argv[1], "__gui-update-worker", jobId, channel, restart ? "restart" : "no-restart"],
|
|
275
|
+
{
|
|
276
|
+
detached: true,
|
|
277
|
+
stdio: "ignore",
|
|
278
|
+
windowsHide: true,
|
|
279
|
+
env: { ...process.env, OCX_SERVICE: "1" },
|
|
280
|
+
},
|
|
281
|
+
),
|
|
282
|
+
isProcessAliveFn: isProcessAlive,
|
|
283
|
+
nowMs: Date.now,
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
export function startUpdateJob(
|
|
287
|
+
channel: Channel,
|
|
288
|
+
restart: boolean,
|
|
289
|
+
deps: Partial<StartUpdateJobDeps> = {},
|
|
290
|
+
): UpdateJobState {
|
|
291
|
+
const resolvedDeps = { ...defaultStartUpdateJobDeps, ...deps };
|
|
229
292
|
const running = readUpdateJob();
|
|
230
293
|
if (running?.status === "running" || running?.status === "restarting") {
|
|
231
|
-
|
|
294
|
+
const staleReason = staleActiveUpdateJobReason(
|
|
295
|
+
running,
|
|
296
|
+
resolvedDeps.nowMs(),
|
|
297
|
+
resolvedDeps.isProcessAliveFn,
|
|
298
|
+
);
|
|
299
|
+
if (!staleReason) {
|
|
300
|
+
throw new UpdateJobError("An update job is already running", 409, "update_already_running");
|
|
301
|
+
}
|
|
302
|
+
updateJob(
|
|
303
|
+
running,
|
|
304
|
+
{ status: "failed", error: `Recovered stale update job: ${staleReason}.`, exitCode: null },
|
|
305
|
+
`Recovered stale update job: ${staleReason}.`,
|
|
306
|
+
);
|
|
232
307
|
}
|
|
233
308
|
|
|
234
|
-
const check =
|
|
309
|
+
const check = resolvedDeps.checkForUpdateFn(channel);
|
|
235
310
|
if (!check.canUpdate) {
|
|
236
311
|
throw new UpdateJobError(check.reason ?? "No update is available", 409, check.reason ?? "update_unavailable");
|
|
237
312
|
}
|
|
238
313
|
|
|
239
314
|
const id = newJobId();
|
|
240
|
-
const now = new Date().toISOString();
|
|
315
|
+
const now = new Date(resolvedDeps.nowMs()).toISOString();
|
|
241
316
|
const job: UpdateJobState = {
|
|
242
317
|
id,
|
|
243
318
|
status: "running",
|
|
@@ -254,14 +329,30 @@ export function startUpdateJob(channel: Channel, restart: boolean): UpdateJobSta
|
|
|
254
329
|
};
|
|
255
330
|
writeJob(job);
|
|
256
331
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
332
|
+
let child: UpdateWorkerProcess;
|
|
333
|
+
try {
|
|
334
|
+
child = resolvedDeps.spawnWorkerFn(id, channel, restart);
|
|
335
|
+
} catch (error) {
|
|
336
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
337
|
+
updateJob(job, { status: "failed", error: `Could not start update worker: ${message}` }, "Update worker failed to start.");
|
|
338
|
+
throw new UpdateJobError("Could not start update worker", 500, "update_worker_start_failed");
|
|
339
|
+
}
|
|
340
|
+
if (typeof child.pid !== "number" || !Number.isSafeInteger(child.pid) || child.pid <= 0) {
|
|
341
|
+
updateJob(job, { status: "failed", error: "Could not start update worker: no worker PID was returned." }, "Update worker failed to start.");
|
|
342
|
+
throw new UpdateJobError("Could not start update worker", 500, "update_worker_start_failed");
|
|
343
|
+
}
|
|
344
|
+
const startedJob = updateJob(job, { pid: child.pid }, `Update worker started as PID ${child.pid}.`);
|
|
345
|
+
child.once("error", error => {
|
|
346
|
+
const current = readUpdateJob(id);
|
|
347
|
+
if (!current || current.pid !== child.pid || (current.status !== "running" && current.status !== "restarting")) return;
|
|
348
|
+
updateJob(
|
|
349
|
+
current,
|
|
350
|
+
{ status: "failed", error: `Update worker failed to start: ${error.message}` },
|
|
351
|
+
"Update worker emitted a startup error.",
|
|
352
|
+
);
|
|
262
353
|
});
|
|
263
354
|
child.unref();
|
|
264
|
-
return
|
|
355
|
+
return startedJob;
|
|
265
356
|
}
|
|
266
357
|
|
|
267
358
|
function runLoggedCommand(job: UpdateJobState, bin: string, args: string[], timeout: number): { status: number | null; signal: NodeJS.Signals | null } {
|
|
@@ -442,7 +533,10 @@ async function awaitRestartedProxyHealthy(
|
|
|
442
533
|
const hostname = captured.hostname;
|
|
443
534
|
const startDeadline = now() + RESTART_HEALTH_TIMEOUT_MS;
|
|
444
535
|
|
|
445
|
-
while (
|
|
536
|
+
while (true) {
|
|
537
|
+
// Always make one identity-aware probe at or after the boundary. A replacement
|
|
538
|
+
// becoming healthy on the final tick must not be mistaken for a timeout.
|
|
539
|
+
const finalProbe = now() >= startDeadline;
|
|
446
540
|
if (await probe(port, hostname)) {
|
|
447
541
|
updateJob(job, {}, `Proxy reported healthy on ${hostname}:${port}; confirming it stays up...`);
|
|
448
542
|
const stableUntil = now() + RESTART_STABILITY_WINDOW_MS;
|
|
@@ -456,7 +550,8 @@ async function awaitRestartedProxyHealthy(
|
|
|
456
550
|
updateJob(job, {}, `Proxy stayed healthy for ${Math.trunc(RESTART_STABILITY_WINDOW_MS / 1000)}s after restart.`);
|
|
457
551
|
return { ok: true };
|
|
458
552
|
}
|
|
459
|
-
|
|
553
|
+
if (finalProbe) break;
|
|
554
|
+
await sleep(Math.min(250, Math.max(0, startDeadline - now())));
|
|
460
555
|
}
|
|
461
556
|
|
|
462
557
|
return { ok: false, reason: "timeout" };
|
|
@@ -479,7 +574,7 @@ async function confirmRestartedProxy(
|
|
|
479
574
|
- 검토한 주요 대안: (1) 포트 점유만 확인 — 외부 프로세스/죽기 직전 프로세스를 성공으로 오인할 수 있다. (2) 무기한 /healthz 폴링 — UX가 느려지고 worker 종료 시점이 불명확하다. (3) 짧은 healthy 등장 + 안정성 창 확인 — 실제 복귀를 확인하면서도 대기 시간을 제한할 수 있다.
|
|
480
575
|
- 선택한 방식: identity-aware /healthz probe가 일정 시간 안에 나타나고, 추가 안정성 창 동안 유지되는지 확인한다.
|
|
481
576
|
- 다른 대안 대신 이 방식을 선택한 이유: GUI는 "업데이트가 설치됐지만 재시작은 실패"를 분리해 알려줘야 하며, 이 방식이 가장 적은 오탐으로 그 경계를 만든다.
|
|
482
|
-
- 장점, 단점 및 영향: 장점은 silent restart failure가 update-job 상태로 드러난다는 점이다. 단점은 성공
|
|
577
|
+
- 장점, 단점 및 영향: 장점은 silent restart failure가 update-job 상태로 드러난다는 점이다. 단점은 설정상 성공 판정 창이 30초 도착 + 15초 안정성으로 늘어나고 경계 probe 지연이 추가될 수 있다는 점이며, 대신 실제 복귀를 더 정확히 반영한다.
|
|
483
578
|
*/
|
|
484
579
|
const result = await awaitRestartedProxyHealthy(job, captured, io);
|
|
485
580
|
if (result.ok) return true;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface NpmInvocationDeps {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
exists?: (path: string) => boolean;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface NpmInvocation {
|
|
7
|
+
file: string;
|
|
8
|
+
args: string[];
|
|
9
|
+
options: { windowsVerbatimArguments?: boolean };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export declare function resolveNpmCommand(
|
|
13
|
+
platform?: NodeJS.Platform,
|
|
14
|
+
env?: Record<string, string | undefined>,
|
|
15
|
+
deps?: NpmInvocationDeps,
|
|
16
|
+
): string | null;
|
|
17
|
+
|
|
18
|
+
export declare function npmInvocation(
|
|
19
|
+
args: readonly string[],
|
|
20
|
+
platform?: NodeJS.Platform,
|
|
21
|
+
env?: Record<string, string | undefined>,
|
|
22
|
+
deps?: NpmInvocationDeps,
|
|
23
|
+
): NpmInvocation | null;
|