@gajae-code/coding-agent 0.16.7 → 0.17.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +180 -1
- package/dist/types/capability/fs.d.ts +37 -4
- package/dist/types/capability/index.d.ts +19 -0
- package/dist/types/capability/types.d.ts +9 -0
- package/dist/types/cli/doctor/args.d.ts +36 -0
- package/dist/types/cli/doctor/checks.d.ts +18 -0
- package/dist/types/cli/doctor/config-repairs.d.ts +38 -0
- package/dist/types/cli/doctor/context.d.ts +89 -0
- package/dist/types/cli/doctor/files.d.ts +27 -0
- package/dist/types/cli/doctor/ids.d.ts +20 -0
- package/dist/types/cli/doctor/install-repairs.d.ts +94 -0
- package/dist/types/cli/doctor/journal.d.ts +43 -0
- package/dist/types/cli/doctor/managed-link.d.ts +39 -0
- package/dist/types/cli/doctor/native-probe.d.ts +2 -0
- package/dist/types/cli/doctor/permission-repairs.d.ts +25 -0
- package/dist/types/cli/doctor/plugin-quarantine.d.ts +77 -0
- package/dist/types/cli/doctor/plugin-restore.d.ts +40 -0
- package/dist/types/cli/doctor/plugin-targets.d.ts +11 -0
- package/dist/types/cli/doctor/probe-client.d.ts +9 -0
- package/dist/types/cli/doctor/probe-types.d.ts +18 -0
- package/dist/types/cli/doctor/probe.d.ts +3 -0
- package/dist/types/cli/doctor/projection-probe.d.ts +2 -0
- package/dist/types/cli/doctor/repairs.d.ts +28 -0
- package/dist/types/cli/doctor/report.d.ts +27 -0
- package/dist/types/cli/doctor/runner.d.ts +4 -0
- package/dist/types/cli/doctor/safety.d.ts +10 -0
- package/dist/types/cli/doctor/service-repairs.d.ts +8 -0
- package/dist/types/cli/doctor/service-targets.d.ts +41 -0
- package/dist/types/cli/doctor/types.d.ts +145 -0
- package/dist/types/cli/doctor-cli.d.ts +7 -0
- package/dist/types/cli/doctor-supervisor.d.ts +121 -0
- package/dist/types/cli/doctor-worker.d.ts +29 -0
- package/dist/types/cli/install-activation.d.ts +158 -0
- package/dist/types/cli/macos-community-app.d.ts +41 -0
- package/dist/types/cli/mcp-cli.d.ts +0 -11
- package/dist/types/cli/update-cli.d.ts +41 -4
- package/dist/types/cli-main.d.ts +174 -0
- package/dist/types/cli.d.ts +1 -171
- package/dist/types/commands/doctor.d.ts +7 -0
- package/dist/types/commands/sdk.d.ts +17 -8
- package/dist/types/config/autorouting-tier-map.d.ts +19 -0
- package/dist/types/config/file-lock.d.ts +27 -3
- package/dist/types/config/model-preset-registry.d.ts +1 -0
- package/dist/types/config/model-registry.d.ts +12 -5
- package/dist/types/config/model-resolver.d.ts +16 -3
- package/dist/types/config/models-config-schema.d.ts +2 -0
- package/dist/types/config/settings-schema.d.ts +1 -1
- package/dist/types/coordinator-mcp/question-gate-codec.d.ts +1 -1
- package/dist/types/coordinator-mcp/session-reaper.d.ts +15 -5
- package/dist/types/cursor.d.ts +9 -9
- package/dist/types/discovery/agents-md.d.ts +6 -1
- package/dist/types/discovery/helpers.d.ts +36 -7
- package/dist/types/discovery/index.d.ts +1 -1
- package/dist/types/discovery/mcp-json.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +22 -1
- package/dist/types/extensibility/gjc-plugins/lifecycle.d.ts +14 -1
- package/dist/types/extensibility/gjc-plugins/paths.d.ts +2 -1
- package/dist/types/extensibility/gjc-plugins/types.d.ts +25 -0
- package/dist/types/extensibility/plugins/manager.d.ts +33 -4
- package/dist/types/extensibility/plugins/marketplace/cache.d.ts +6 -0
- package/dist/types/extensibility/plugins/marketplace/manager.d.ts +186 -0
- package/dist/types/extensibility/plugins/marketplace/registry.d.ts +27 -0
- package/dist/types/extensibility/plugins/marketplace/source-resolver.d.ts +16 -1
- package/dist/types/extensibility/runtime-skill-discovery.d.ts +16 -3
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +6 -0
- package/dist/types/gjc-runtime/launch-worktree.d.ts +53 -0
- package/dist/types/gjc-runtime/session-layout.d.ts +8 -0
- package/dist/types/gjc-runtime/session-state-lock.d.ts +15 -0
- package/dist/types/gjc-runtime/session-state-sidecar.d.ts +13 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +6 -0
- package/dist/types/main.d.ts +19 -1
- package/dist/types/modes/acp/acp-agent.d.ts +40 -0
- package/dist/types/modes/components/agent-dashboard.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/interactive-mode.d.ts +1 -1
- package/dist/types/runtime-mcp/config-writer.d.ts +47 -0
- package/dist/types/runtime-mcp/policy-json-edit.d.ts +60 -0
- package/dist/types/runtime-mcp/startup-policy.d.ts +4 -0
- package/dist/types/runtime-mcp/types.d.ts +2 -2
- package/dist/types/sdk/acp/adapter.d.ts +5 -1
- package/dist/types/sdk/broker/broker.d.ts +51 -0
- package/dist/types/sdk/broker/daemon-entry.d.ts +39 -0
- package/dist/types/sdk/broker/discovery.d.ts +38 -0
- package/dist/types/sdk/broker/doctor-restart.d.ts +79 -0
- package/dist/types/sdk/broker/endpoint-authority.d.ts +7 -0
- package/dist/types/sdk/broker/ensure.d.ts +14 -0
- package/dist/types/sdk/broker/lifecycle.d.ts +2 -0
- package/dist/types/sdk/broker/process-incarnation.d.ts +50 -0
- package/dist/types/sdk/broker/runtime.d.ts +26 -0
- package/dist/types/sdk/broker/session-index.d.ts +13 -4
- package/dist/types/sdk/broker/worktree-occupancy.d.ts +14 -0
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +16 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +101 -6
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +16 -0
- package/dist/types/sdk/bus/daemon-startup-exclusion.d.ts +43 -0
- package/dist/types/sdk/bus/doctor-daemon-restart-client.d.ts +51 -0
- package/dist/types/sdk/bus/doctor-daemon-restart.d.ts +39 -0
- package/dist/types/sdk/bus/index.d.ts +2 -0
- package/dist/types/sdk/bus/notification-service.d.ts +3 -0
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +11 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +1 -1
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-owner-registry.d.ts +1 -0
- package/dist/types/sdk/bus/telegram-daemon.d.ts +36 -5
- package/dist/types/sdk/cli/session-cli.d.ts +1 -0
- package/dist/types/sdk/host/session-runtime.d.ts +4 -0
- package/dist/types/sdk/host/surface-policy.d.ts +3 -1
- package/dist/types/sdk/lifecycle/service.d.ts +44 -10
- package/dist/types/sdk/prompt-deadline-lease.d.ts +6 -3
- package/dist/types/sdk/prompt-deadline-manager.d.ts +2 -2
- package/dist/types/sdk/prompt-failure.d.ts +47 -0
- package/dist/types/sdk/prompt-status.d.ts +19 -0
- package/dist/types/sdk/router/session-router.d.ts +3 -4
- package/dist/types/sdk/service-artifact-paths.d.ts +16 -0
- package/dist/types/sdk/session-authority.d.ts +9 -0
- package/dist/types/sdk/session.d.ts +3 -1
- package/dist/types/session/agent-session.d.ts +1 -1
- package/dist/types/session/blob-store.d.ts +1 -1
- package/dist/types/session/fallback-chain-controller.d.ts +3 -6
- package/dist/types/session/internal/managed-session-scope.d.ts +44 -0
- package/dist/types/session/primary-control-surface.d.ts +4 -0
- package/dist/types/session/session-manager.d.ts +6 -0
- package/dist/types/system-prompt.d.ts +6 -0
- package/dist/types/tools/ask.d.ts +25 -0
- package/dist/types/tools/image-gen.d.ts +0 -4
- package/dist/types/tools/skill-discovery.d.ts +7 -5
- package/dist/types/tools/ultragoal-ask-guard.d.ts +7 -4
- package/dist/types/utils/changelog.d.ts +0 -9
- package/dist/types/utils/mupdf-wasm.d.ts +1 -0
- package/package.json +9 -8
- package/scripts/compile-args.ts +8 -4
- package/src/capability/fs.ts +395 -17
- package/src/capability/index.ts +273 -10
- package/src/capability/types.ts +9 -0
- package/src/cli/customize-doctor.ts +18 -12
- package/src/cli/doctor/args.ts +236 -0
- package/src/cli/doctor/checks.ts +431 -0
- package/src/cli/doctor/config-repairs.ts +517 -0
- package/src/cli/doctor/context.ts +149 -0
- package/src/cli/doctor/files.ts +149 -0
- package/src/cli/doctor/ids.ts +115 -0
- package/src/cli/doctor/install-repairs.ts +335 -0
- package/src/cli/doctor/journal.ts +423 -0
- package/src/cli/doctor/managed-link.ts +514 -0
- package/src/cli/doctor/native-probe.ts +13 -0
- package/src/cli/doctor/permission-repairs.ts +242 -0
- package/src/cli/doctor/plugin-quarantine.ts +377 -0
- package/src/cli/doctor/plugin-restore.ts +59 -0
- package/src/cli/doctor/plugin-targets.ts +373 -0
- package/src/cli/doctor/probe-client.ts +195 -0
- package/src/cli/doctor/probe-types.ts +21 -0
- package/src/cli/doctor/probe.ts +42 -0
- package/src/cli/doctor/projection-probe.ts +249 -0
- package/src/cli/doctor/repairs.ts +1157 -0
- package/src/cli/doctor/report.ts +176 -0
- package/src/cli/doctor/runner.ts +342 -0
- package/src/cli/doctor/safety.ts +40 -0
- package/src/cli/doctor/service-repairs.ts +452 -0
- package/src/cli/doctor/service-targets.ts +274 -0
- package/src/cli/doctor/types.ts +164 -0
- package/src/cli/doctor-cli.ts +62 -0
- package/src/cli/doctor-supervisor.ts +571 -0
- package/src/cli/doctor-worker.ts +181 -0
- package/src/cli/install-activation.ts +666 -0
- package/src/cli/macos-community-app.ts +1449 -0
- package/src/cli/mcp-cli.ts +11 -21
- package/src/cli/nofile-limit.ts +5 -1
- package/src/cli/plugin-cli.ts +6 -1
- package/src/cli/update-cli.ts +362 -264
- package/src/cli-main.ts +517 -0
- package/src/cli.ts +64 -433
- package/src/commands/autoresearch.ts +11 -5
- package/src/commands/completion.ts +1 -1
- package/src/commands/doctor.ts +13 -0
- package/src/commands/sdk.ts +58 -10
- package/src/commands/ultragoal.ts +11 -5
- package/src/config/autorouting-tier-map.ts +7 -0
- package/src/config/file-lock.ts +736 -71
- package/src/config/mcp-schema.json +3 -3
- package/src/config/model-preset-registry.ts +1 -0
- package/src/config/model-registry.ts +80 -27
- package/src/config/model-resolver.ts +33 -4
- package/src/config/models-config-schema.ts +2 -0
- package/src/config/settings-schema.ts +6 -2
- package/src/coordinator-mcp/question-gate-codec.ts +1 -0
- package/src/coordinator-mcp/server.ts +534 -51
- package/src/coordinator-mcp/session-reaper.ts +64 -8
- package/src/cursor.ts +223 -53
- package/src/discovery/agents-md.ts +74 -8
- package/src/discovery/agents.ts +32 -7
- package/src/discovery/builtin.ts +184 -75
- package/src/discovery/claude-plugins.ts +85 -17
- package/src/discovery/cline.ts +38 -10
- package/src/discovery/cursor.ts +19 -6
- package/src/discovery/gemini.ts +62 -22
- package/src/discovery/github.ts +13 -3
- package/src/discovery/helpers.ts +483 -89
- package/src/discovery/index.ts +1 -0
- package/src/discovery/mcp-json.ts +36 -10
- package/src/discovery/opencode.ts +36 -19
- package/src/discovery/ssh.ts +11 -8
- package/src/discovery/windsurf.ts +12 -3
- package/src/exec/bash-executor.ts +10 -3
- package/src/exec/isolated-shell.ts +15 -2
- package/src/extensibility/extensions/loader.ts +88 -26
- package/src/extensibility/gjc-plugins/lifecycle.ts +189 -0
- package/src/extensibility/gjc-plugins/paths.ts +4 -1
- package/src/extensibility/gjc-plugins/types.ts +27 -0
- package/src/extensibility/plugins/manager.ts +200 -22
- package/src/extensibility/plugins/marketplace/cache.ts +55 -2
- package/src/extensibility/plugins/marketplace/manager.ts +1262 -162
- package/src/extensibility/plugins/marketplace/registry.ts +153 -0
- package/src/extensibility/plugins/marketplace/source-resolver.ts +116 -10
- package/src/extensibility/runtime-skill-discovery.ts +61 -9
- package/src/extensibility/skill-management.ts +7 -1
- package/src/gjc-runtime/autoresearch-runtime.ts +10 -1
- package/src/gjc-runtime/launch-worktree.test.ts +248 -0
- package/src/gjc-runtime/launch-worktree.ts +275 -38
- package/src/gjc-runtime/repository-binding.ts +26 -1
- package/src/gjc-runtime/session-layout.ts +10 -0
- package/src/gjc-runtime/session-state-lock.ts +330 -23
- package/src/gjc-runtime/session-state-sidecar.ts +328 -80
- package/src/gjc-runtime/ultragoal-runtime.ts +10 -1
- package/src/harness-control-plane/session-lease.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +28 -26
- package/src/main.ts +150 -29
- package/src/memories/index.ts +24 -2
- package/src/modes/acp/acp-agent.ts +522 -68
- package/src/modes/components/agent-dashboard.ts +3 -1
- package/src/modes/components/model-selector.ts +28 -19
- package/src/modes/components/plugin-settings.ts +2 -1
- package/src/modes/controllers/selector-controller.ts +15 -9
- package/src/modes/interactive-mode.ts +21 -3
- package/src/prompts/system/system-prompt.md +5 -1
- package/src/prompts/tools/browser.md +1 -1
- package/src/prompts/tools/move-session.md +4 -0
- package/src/prompts/tools/read.md +2 -1
- package/src/prompts/tools/skill-discovery.md +6 -5
- package/src/prompts/tools/task.md +1 -0
- package/src/runtime-mcp/config-writer.ts +228 -0
- package/src/runtime-mcp/oauth-discovery.ts +114 -14
- package/src/runtime-mcp/policy-json-edit.ts +129 -0
- package/src/runtime-mcp/startup-policy.ts +14 -0
- package/src/runtime-mcp/types.ts +2 -2
- package/src/sdk/acp/adapter.ts +27 -12
- package/src/sdk/broker/broker.ts +273 -44
- package/src/sdk/broker/daemon-entry.ts +119 -0
- package/src/sdk/broker/discovery.ts +173 -20
- package/src/sdk/broker/doctor-restart.ts +206 -0
- package/src/sdk/broker/endpoint-authority.ts +36 -0
- package/src/sdk/broker/ensure.ts +49 -14
- package/src/sdk/broker/lifecycle.ts +174 -42
- package/src/sdk/broker/process-incarnation.ts +86 -2
- package/src/sdk/broker/runtime.ts +145 -21
- package/src/sdk/broker/session-index.ts +136 -22
- package/src/sdk/broker/spawn-substrate.ts +4 -1
- package/src/sdk/broker/transport.ts +5 -0
- package/src/sdk/broker/worktree-occupancy.ts +21 -1
- package/src/sdk/bus/chat-daemon-cli.ts +63 -0
- package/src/sdk/bus/chat-daemon-control.ts +260 -64
- package/src/sdk/bus/chat-daemon-runtime.ts +141 -25
- package/src/sdk/bus/daemon-startup-exclusion.ts +119 -0
- package/src/sdk/bus/doctor-daemon-restart-client.ts +291 -0
- package/src/sdk/bus/doctor-daemon-restart.ts +92 -0
- package/src/sdk/bus/index.ts +252 -51
- package/src/sdk/bus/kind-aware-reconciliation.ts +76 -41
- package/src/sdk/bus/notification-service.ts +21 -3
- package/src/sdk/bus/reconciliation-store.ts +17 -11
- package/src/sdk/bus/telegram-daemon-cli.ts +91 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +1 -1
- package/src/sdk/bus/telegram-daemon-control.ts +47 -0
- package/src/sdk/bus/telegram-daemon-owner-registry.ts +1 -1
- package/src/sdk/bus/telegram-daemon.ts +178 -13
- package/src/sdk/cli/session-cli.ts +100 -12
- package/src/sdk/host/host.ts +2 -7
- package/src/sdk/host/session-runtime.test.ts +329 -8
- package/src/sdk/host/session-runtime.ts +202 -50
- package/src/sdk/host/surface-policy.ts +8 -1
- package/src/sdk/lifecycle/service.ts +199 -22
- package/src/sdk/mcp/server.ts +1 -1
- package/src/sdk/prompt-deadline-lease.ts +12 -4
- package/src/sdk/prompt-deadline-manager.ts +5 -7
- package/src/sdk/prompt-failure.ts +148 -0
- package/src/sdk/prompt-status.ts +30 -1
- package/src/sdk/prompt-watchdog.ts +11 -3
- package/src/sdk/protocol/operation-inventory.generated.json +18 -0
- package/src/sdk/protocol/operation-registry.ts +20 -8
- package/src/sdk/router/session-router.ts +8 -3
- package/src/sdk/service-artifact-paths.ts +26 -0
- package/src/sdk/session-authority.ts +10 -0
- package/src/sdk/session.ts +361 -194
- package/src/session/agent-session.ts +398 -93
- package/src/session/blob-store.ts +9 -9
- package/src/session/fallback-chain-controller.ts +3 -7
- package/src/session/internal/managed-session-scope.ts +445 -29
- package/src/session/internal/managed-session-storage.ts +38 -6
- package/src/session/primary-control-surface.ts +12 -0
- package/src/session/session-manager.ts +11 -3
- package/src/session/session-storage.ts +1 -1
- package/src/session-import/codex.ts +6 -1
- package/src/session-import/redact.ts +7 -0
- package/src/skill-state/workflow-mutation-guard.ts +645 -39
- package/src/slash-commands/builtin-registry.ts +7 -3
- package/src/system-prompt.ts +8 -0
- package/src/task/executor.ts +36 -3
- package/src/task/types.ts +27 -5
- package/src/tools/ask.ts +114 -34
- package/src/tools/browser/actions.ts +2 -2
- package/src/tools/browser/tab-worker.ts +11 -8
- package/src/tools/descriptors.ts +6 -1
- package/src/tools/fetch.ts +7 -2
- package/src/tools/image-gen.ts +33 -34
- package/src/tools/read.ts +78 -32
- package/src/tools/skill-discovery.ts +17 -7
- package/src/tools/tool-catalog.generated.ts +8 -8
- package/src/tools/ultragoal-ask-guard.ts +16 -26
- package/src/utils/changelog.ts +7 -1
- package/src/utils/git.ts +3 -1
- package/src/utils/markit.ts +24 -2
- package/src/utils/mupdf-wasm.ts +26 -0
- package/src/web/search/providers/openai-compatible.ts +191 -14
package/src/cli/update-cli.ts
CHANGED
|
@@ -5,13 +5,16 @@
|
|
|
5
5
|
* binaries. Package-manager installs are migrated to a user binary path
|
|
6
6
|
* rather than overwritten. Source checkouts and dev-links are never replaced.
|
|
7
7
|
*/
|
|
8
|
+
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
8
10
|
import * as fs from "node:fs";
|
|
9
11
|
import * as os from "node:os";
|
|
10
12
|
import * as path from "node:path";
|
|
11
|
-
import {
|
|
13
|
+
import { exactReplaceRetained, exactUnlink } from "@gajae-code/natives";
|
|
12
14
|
import { $which, APP_NAME, isCompiledBinary, isEnoent, redactCrashSecrets, VERSION } from "@gajae-code/utils";
|
|
13
15
|
import { $ } from "bun";
|
|
14
16
|
import chalk from "chalk";
|
|
17
|
+
import { acquireFileLock } from "../config/file-lock";
|
|
15
18
|
import { Settings } from "../config/settings";
|
|
16
19
|
import { isUpdateChannel, UPDATE_CHANNELS, type UpdateChannel } from "../config/update-channel";
|
|
17
20
|
import { installDefaultGjcDefinitions } from "../defaults/gjc-defaults";
|
|
@@ -29,6 +32,22 @@ import {
|
|
|
29
32
|
verifyDownloadedBinaryChecksum,
|
|
30
33
|
versionFromTag,
|
|
31
34
|
} from "./github-release";
|
|
35
|
+
import {
|
|
36
|
+
type ActivationRecordV1,
|
|
37
|
+
buildActivationRecord,
|
|
38
|
+
createActivationRecordFile,
|
|
39
|
+
type DirectoryIdentity,
|
|
40
|
+
type FileIdentity,
|
|
41
|
+
readActivationRecord,
|
|
42
|
+
reconcileActivationRecord,
|
|
43
|
+
sameDirectoryIdentity,
|
|
44
|
+
sameFileIdentity,
|
|
45
|
+
snapshotDirectory,
|
|
46
|
+
snapshotRegularFile,
|
|
47
|
+
toNativeIdentity,
|
|
48
|
+
updateActivationRecordFile,
|
|
49
|
+
} from "./install-activation";
|
|
50
|
+
import { COMMUNITY_APP_REPOSITORY, offerMacosCommunityApp } from "./macos-community-app";
|
|
32
51
|
import { runNotifyCommand } from "./notify-cli";
|
|
33
52
|
|
|
34
53
|
const PACKAGE = "@gajae-code/coding-agent";
|
|
@@ -82,6 +101,10 @@ export interface BinaryReplacementOptions {
|
|
|
82
101
|
tempPath: string;
|
|
83
102
|
backupPath: string;
|
|
84
103
|
expectedVersion: string;
|
|
104
|
+
originalTarget: FileIdentity | undefined;
|
|
105
|
+
originalParent: DirectoryIdentity;
|
|
106
|
+
candidate?: { readonly channel: string; readonly ref: string; readonly sha256: string };
|
|
107
|
+
verifyStagedVersion?: (stagingPath: string, expectedVersion: string) => Promise<void>;
|
|
85
108
|
verifyInstalledVersion: (expectedVersion: string) => Promise<InstalledVersionVerification>;
|
|
86
109
|
}
|
|
87
110
|
|
|
@@ -299,6 +322,10 @@ export function isProtectedSourcePathForTest(filePath: string): boolean {
|
|
|
299
322
|
return isProtectedSourcePath(filePath);
|
|
300
323
|
}
|
|
301
324
|
|
|
325
|
+
export function isProtectedSourcePathForInstall(filePath: string): boolean {
|
|
326
|
+
return isProtectedSourcePath(filePath);
|
|
327
|
+
}
|
|
328
|
+
|
|
302
329
|
export function defaultUserBinaryPathForTest(
|
|
303
330
|
platform: NodeJS.Platform = process.platform,
|
|
304
331
|
env: NodeJS.ProcessEnv = process.env,
|
|
@@ -387,7 +414,10 @@ export function compareVersionsForTest(a: string, b: string): number {
|
|
|
387
414
|
/**
|
|
388
415
|
* Get the appropriate binary name for this platform.
|
|
389
416
|
*/
|
|
390
|
-
function
|
|
417
|
+
export function getBinaryNameForPlatform(
|
|
418
|
+
platform: NodeJS.Platform = process.platform,
|
|
419
|
+
arch: string = process.arch,
|
|
420
|
+
): string {
|
|
391
421
|
let os: string;
|
|
392
422
|
switch (platform) {
|
|
393
423
|
case "linux":
|
|
@@ -423,6 +453,7 @@ function getBinaryName(platform: NodeJS.Platform = process.platform, arch: strin
|
|
|
423
453
|
}
|
|
424
454
|
return `${APP_NAME}-${os}-${archName}`;
|
|
425
455
|
}
|
|
456
|
+
const getBinaryName = getBinaryNameForPlatform;
|
|
426
457
|
|
|
427
458
|
/**
|
|
428
459
|
* Resolve the running GJC image. Compiled binaries update themselves via
|
|
@@ -741,176 +772,169 @@ export function formatVerificationFailureForTest(
|
|
|
741
772
|
return formatVerificationFailure(result, expectedVersion);
|
|
742
773
|
}
|
|
743
774
|
|
|
744
|
-
async function unlinkIfExists(filePath: string): Promise<void> {
|
|
745
|
-
try {
|
|
746
|
-
await fs.promises.unlink(filePath);
|
|
747
|
-
} catch (err) {
|
|
748
|
-
if (!isEnoent(err)) throw err;
|
|
749
|
-
}
|
|
750
|
-
}
|
|
751
|
-
|
|
752
|
-
function formatBackupCleanupWarning(backupPath: string, err: unknown): string {
|
|
753
|
-
return `Installed update, but could not remove backup file ${backupPath}: ${err}. You can delete it manually after closing shells or antivirus processes that may still hold it.`;
|
|
754
|
-
}
|
|
755
|
-
|
|
756
|
-
async function cleanupVerifiedBackup(backupPath: string): Promise<string | undefined> {
|
|
757
|
-
try {
|
|
758
|
-
await unlinkIfExists(backupPath);
|
|
759
|
-
return undefined;
|
|
760
|
-
} catch (err) {
|
|
761
|
-
return formatBackupCleanupWarning(backupPath, err);
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
775
|
export async function recoverWindowsUpdateJournal(journalPath: string): Promise<void> {
|
|
766
|
-
let raw: string;
|
|
767
776
|
try {
|
|
768
|
-
|
|
769
|
-
} catch (
|
|
770
|
-
if (isEnoent(
|
|
771
|
-
throw
|
|
772
|
-
}
|
|
773
|
-
let target = "";
|
|
774
|
-
let backup = "";
|
|
775
|
-
let next = "";
|
|
776
|
-
try {
|
|
777
|
-
const parsed: unknown = JSON.parse(raw);
|
|
778
|
-
if (parsed && typeof parsed === "object") {
|
|
779
|
-
const record = parsed as { target?: unknown; backup?: unknown; next?: unknown };
|
|
780
|
-
if (typeof record.target === "string") target = record.target;
|
|
781
|
-
if (typeof record.backup === "string") backup = record.backup;
|
|
782
|
-
if (typeof record.next === "string") next = record.next;
|
|
783
|
-
}
|
|
784
|
-
} catch {
|
|
785
|
-
await unlinkIfExists(journalPath);
|
|
786
|
-
return;
|
|
787
|
-
}
|
|
788
|
-
const exists = async (p: string): Promise<boolean> => {
|
|
789
|
-
try {
|
|
790
|
-
await fs.promises.lstat(p);
|
|
791
|
-
return true;
|
|
792
|
-
} catch (err) {
|
|
793
|
-
if (isEnoent(err)) return false;
|
|
794
|
-
throw err;
|
|
795
|
-
}
|
|
796
|
-
};
|
|
797
|
-
if (target && next && (await exists(next))) {
|
|
798
|
-
if (!(await exists(target))) {
|
|
799
|
-
await fs.promises.rename(next, target);
|
|
800
|
-
await unlinkIfExists(journalPath);
|
|
801
|
-
return;
|
|
802
|
-
}
|
|
803
|
-
try {
|
|
804
|
-
const recoverBackup = `${target}.bak.recover.${process.pid}.${Date.now().toString(16)}`;
|
|
805
|
-
await fs.promises.rename(target, recoverBackup);
|
|
806
|
-
try {
|
|
807
|
-
await fs.promises.rename(next, target);
|
|
808
|
-
} catch (promoteErr) {
|
|
809
|
-
try {
|
|
810
|
-
await fs.promises.rename(recoverBackup, target);
|
|
811
|
-
} catch {
|
|
812
|
-
throw promoteErr;
|
|
813
|
-
}
|
|
814
|
-
throw promoteErr;
|
|
815
|
-
}
|
|
816
|
-
await unlinkIfExists(recoverBackup);
|
|
817
|
-
await unlinkIfExists(journalPath);
|
|
818
|
-
return;
|
|
819
|
-
} catch {
|
|
820
|
-
return;
|
|
821
|
-
}
|
|
822
|
-
}
|
|
823
|
-
if (target && backup && !(await exists(target))) {
|
|
824
|
-
try {
|
|
825
|
-
await fs.promises.rename(backup, target);
|
|
826
|
-
} catch (restoreErr) {
|
|
827
|
-
if (!isEnoent(restoreErr)) throw restoreErr;
|
|
828
|
-
}
|
|
777
|
+
await fs.promises.lstat(journalPath);
|
|
778
|
+
} catch (error) {
|
|
779
|
+
if (isEnoent(error)) return;
|
|
780
|
+
throw error;
|
|
829
781
|
}
|
|
830
|
-
|
|
782
|
+
throw new Error("legacy_update_journal_requires_manual_review");
|
|
831
783
|
}
|
|
832
784
|
|
|
833
785
|
/**
|
|
834
786
|
* Atomically replace the installed binary and roll back if version verification fails.
|
|
835
787
|
*/
|
|
836
788
|
export async function replaceBinaryForUpdate(options: BinaryReplacementOptions): Promise<InstalledVersionVerification> {
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
789
|
+
const targetPath = path.resolve(options.targetPath);
|
|
790
|
+
const parentPath = path.dirname(targetPath);
|
|
791
|
+
if (
|
|
792
|
+
path.dirname(path.resolve(options.tempPath)) !== parentPath ||
|
|
793
|
+
path.dirname(path.resolve(options.backupPath)) !== parentPath
|
|
794
|
+
)
|
|
795
|
+
throw new Error("installation_staging_parent_mismatch");
|
|
796
|
+
await recoverWindowsUpdateJournal(`${targetPath}.update-journal`);
|
|
797
|
+
const parent = await snapshotDirectory(parentPath);
|
|
798
|
+
const current = await snapshotRegularFile(targetPath);
|
|
799
|
+
if (
|
|
800
|
+
!sameDirectoryIdentity(options.originalParent, parent) ||
|
|
801
|
+
(options.originalTarget
|
|
802
|
+
? !current || !sameFileIdentity(options.originalTarget, current.identity)
|
|
803
|
+
: current !== undefined)
|
|
804
|
+
)
|
|
805
|
+
throw new Error("installation_original_target_changed");
|
|
806
|
+
const existing = await readActivationRecord(targetPath);
|
|
807
|
+
if (existing.status === "malformed" || existing.status === "foreign") throw new Error("activation_record_untrusted");
|
|
808
|
+
const resumePinned =
|
|
809
|
+
existing.status === "valid" &&
|
|
810
|
+
existing.record.phase !== "verified" &&
|
|
811
|
+
existing.record.phase !== "rolled_back" &&
|
|
812
|
+
options.candidate !== undefined;
|
|
813
|
+
const staged = resumePinned ? undefined : await snapshotRegularFile(options.tempPath);
|
|
814
|
+
if (!resumePinned) {
|
|
815
|
+
if (
|
|
816
|
+
!staged ||
|
|
817
|
+
staged.identity.parentDev !== options.originalParent.dev ||
|
|
818
|
+
staged.identity.parentIno !== options.originalParent.ino
|
|
819
|
+
)
|
|
820
|
+
throw new Error("installation_staging_unavailable");
|
|
821
|
+
if (options.candidate && options.candidate.sha256.toLowerCase() !== staged.identity.sha256)
|
|
822
|
+
throw new Error("candidate_digest_mismatch");
|
|
823
|
+
await (options.verifyStagedVersion ?? (async (file, version) => smokeTestPinnedCandidate({ version }, file)))(
|
|
824
|
+
options.tempPath,
|
|
825
|
+
options.expectedVersion,
|
|
826
|
+
);
|
|
827
|
+
const stagedAfter = await snapshotRegularFile(options.tempPath);
|
|
828
|
+
if (!stagedAfter || !sameFileIdentity(staged.identity, stagedAfter.identity))
|
|
829
|
+
throw new Error("installation_candidate_changed");
|
|
830
|
+
}
|
|
831
|
+
let record: ActivationRecordV1;
|
|
832
|
+
let recordIdentity: FileIdentity;
|
|
833
|
+
if (existing.status === "valid" && existing.record.phase !== "verified" && existing.record.phase !== "rolled_back") {
|
|
834
|
+
if (
|
|
835
|
+
existing.record.candidate.digest !== (options.candidate?.sha256.toLowerCase() ?? staged?.identity.sha256) ||
|
|
836
|
+
existing.record.candidate.version !== options.expectedVersion ||
|
|
837
|
+
(options.candidate &&
|
|
838
|
+
(existing.record.candidate.ref !== options.candidate.ref ||
|
|
839
|
+
existing.record.candidate.channel !== options.candidate.channel))
|
|
840
|
+
)
|
|
841
|
+
throw new Error("activation_transaction_requires_reconciliation");
|
|
842
|
+
record = existing.record;
|
|
843
|
+
recordIdentity = existing.identity;
|
|
844
|
+
} else {
|
|
845
|
+
if (!staged) throw new Error("installation_staging_unavailable");
|
|
846
|
+
record = {
|
|
847
|
+
...buildActivationRecord({
|
|
848
|
+
targetPath,
|
|
849
|
+
targetIdentity: options.originalTarget,
|
|
850
|
+
originalTargetAbsent: options.originalTarget === undefined,
|
|
851
|
+
parentIdentity: options.originalParent,
|
|
852
|
+
baselineDigest: options.originalTarget?.sha256,
|
|
853
|
+
baselineVersion: VERSION,
|
|
854
|
+
stagingPath: path.resolve(options.tempPath),
|
|
855
|
+
stagingIdentity: staged.identity,
|
|
856
|
+
candidate: {
|
|
857
|
+
digest: staged.identity.sha256,
|
|
858
|
+
version: options.expectedVersion,
|
|
859
|
+
ref: options.candidate?.ref ?? `v${options.expectedVersion}`,
|
|
860
|
+
channel: options.candidate?.channel,
|
|
861
|
+
os: process.platform,
|
|
862
|
+
arch: process.arch,
|
|
863
|
+
},
|
|
864
|
+
}),
|
|
865
|
+
backupName: path.basename(options.backupPath),
|
|
866
|
+
};
|
|
867
|
+
recordIdentity =
|
|
868
|
+
existing.status === "valid"
|
|
869
|
+
? await updateActivationRecordFile(record, existing.identity)
|
|
870
|
+
: await createActivationRecordFile(record);
|
|
871
|
+
}
|
|
872
|
+
const promoted = await reconcileActivationRecord(targetPath, {
|
|
873
|
+
allowPromotion: true,
|
|
874
|
+
expectedRecordIdentity: recordIdentity,
|
|
875
|
+
});
|
|
876
|
+
if (promoted.status === "pending_activation") throw new Error("installation_pending_activation");
|
|
877
|
+
if (
|
|
878
|
+
(promoted.status !== "reconciled" &&
|
|
879
|
+
promoted.status !== "applied_unverified" &&
|
|
880
|
+
promoted.status !== "verified") ||
|
|
881
|
+
!promoted.record ||
|
|
882
|
+
!promoted.recordIdentity
|
|
883
|
+
)
|
|
884
|
+
throw new Error(promoted.reason ?? "installation_publication_unverified");
|
|
885
|
+
record = promoted.record;
|
|
886
|
+
recordIdentity = promoted.recordIdentity;
|
|
841
887
|
try {
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
888
|
+
const verification = await options.verifyInstalledVersion(options.expectedVersion);
|
|
889
|
+
if (!verification.ok) throw new Error(formatVerificationFailure(verification, options.expectedVersion));
|
|
890
|
+
const after = await snapshotRegularFile(targetPath);
|
|
891
|
+
if (!after || !record.targetIdentity || !sameFileIdentity(record.targetIdentity, after.identity))
|
|
892
|
+
throw new Error("installation_postcheck_identity_changed");
|
|
893
|
+
await updateActivationRecordFile({ ...record, phase: "verified" }, recordIdentity);
|
|
894
|
+
return verification;
|
|
895
|
+
} catch (primary) {
|
|
845
896
|
try {
|
|
846
|
-
const
|
|
847
|
-
if (
|
|
848
|
-
throw new Error(
|
|
849
|
-
|
|
897
|
+
const after = await snapshotRegularFile(targetPath);
|
|
898
|
+
if (!after || !record.targetIdentity || !sameFileIdentity(record.targetIdentity, after.identity))
|
|
899
|
+
throw new Error("rollback_conflict_target_changed");
|
|
900
|
+
recordIdentity = await updateActivationRecordFile({ ...record, phase: "uncertain" }, recordIdentity);
|
|
901
|
+
const failedName = `.gjc-update-failed-${record.transactionId}`;
|
|
902
|
+
if (record.baseline.exists) {
|
|
903
|
+
const backupPath = path.join(parentPath, record.backupName);
|
|
904
|
+
const backup = await snapshotRegularFile(backupPath);
|
|
905
|
+
if (!backup || !record.backupIdentity || !sameFileIdentity(record.backupIdentity, backup.identity))
|
|
906
|
+
throw new Error("rollback_backup_changed");
|
|
907
|
+
const restored = exactReplaceRetained(
|
|
908
|
+
backupPath,
|
|
909
|
+
targetPath,
|
|
910
|
+
failedName,
|
|
911
|
+
toNativeIdentity(record.backupIdentity),
|
|
912
|
+
toNativeIdentity(record.targetIdentity),
|
|
913
|
+
);
|
|
914
|
+
const observed = await snapshotRegularFile(targetPath);
|
|
915
|
+
if (!restored.ok || !observed || !sameFileIdentity(record.backupIdentity, observed.identity))
|
|
916
|
+
throw new Error("rollback_unverified");
|
|
917
|
+
await updateActivationRecordFile(
|
|
918
|
+
{ ...record, targetIdentity: observed.identity, phase: "rolled_back" },
|
|
919
|
+
recordIdentity,
|
|
920
|
+
);
|
|
921
|
+
} else {
|
|
922
|
+
const removed = exactUnlink(targetPath, {
|
|
923
|
+
...toNativeIdentity(record.targetIdentity, failedName),
|
|
924
|
+
detachOnly: true,
|
|
925
|
+
});
|
|
926
|
+
const absent = await snapshotRegularFile(targetPath);
|
|
927
|
+
if ((!removed.ok && removed.code !== "cleanup_pending") || absent !== undefined)
|
|
928
|
+
throw new Error("rollback_unverified");
|
|
929
|
+
await updateActivationRecordFile(
|
|
930
|
+
{ ...record, targetIdentity: undefined, phase: "rolled_back" },
|
|
931
|
+
recordIdentity,
|
|
850
932
|
);
|
|
851
933
|
}
|
|
852
|
-
} catch (
|
|
853
|
-
|
|
854
|
-
}
|
|
855
|
-
await unlinkIfExists(options.backupPath);
|
|
856
|
-
if (process.platform === "win32") {
|
|
857
|
-
const nextPath = `${options.targetPath}.next`;
|
|
858
|
-
await fs.promises.writeFile(
|
|
859
|
-
journalPath,
|
|
860
|
-
JSON.stringify({
|
|
861
|
-
target: options.targetPath,
|
|
862
|
-
backup: options.backupPath,
|
|
863
|
-
temp: options.tempPath,
|
|
864
|
-
next: nextPath,
|
|
865
|
-
}),
|
|
866
|
-
"utf8",
|
|
867
|
-
);
|
|
868
|
-
try {
|
|
869
|
-
await fs.promises.rename(options.targetPath, options.backupPath);
|
|
870
|
-
backupReady = true;
|
|
871
|
-
} catch (err) {
|
|
872
|
-
if (isEnoent(err)) {
|
|
873
|
-
backupReady = false;
|
|
874
|
-
} else {
|
|
875
|
-
await fs.promises.copyFile(options.tempPath, nextPath);
|
|
876
|
-
stagedNext = true;
|
|
877
|
-
throw new Error(
|
|
878
|
-
`Running Windows image ${options.targetPath} could not be replaced in-process (${err}). Staged ${nextPath}. Close running gjc.exe and re-run gjc update.`,
|
|
879
|
-
);
|
|
880
|
-
}
|
|
881
|
-
}
|
|
882
|
-
} else {
|
|
883
|
-
try {
|
|
884
|
-
await fs.promises.copyFile(options.targetPath, options.backupPath);
|
|
885
|
-
backupReady = true;
|
|
886
|
-
} catch (err) {
|
|
887
|
-
if (!isEnoent(err)) throw err;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
await fs.promises.rename(options.tempPath, options.targetPath);
|
|
891
|
-
published = true;
|
|
892
|
-
|
|
893
|
-
const verification = await options.verifyInstalledVersion(options.expectedVersion);
|
|
894
|
-
if (!verification.ok) {
|
|
895
|
-
throw new Error(
|
|
896
|
-
`${formatVerificationFailure(verification, options.expectedVersion)}; restored previous ${APP_NAME} binary`,
|
|
897
|
-
);
|
|
898
|
-
}
|
|
899
|
-
|
|
900
|
-
backupReady = false;
|
|
901
|
-
if (process.platform === "win32") await unlinkIfExists(journalPath);
|
|
902
|
-
const cleanupWarning = await cleanupVerifiedBackup(options.backupPath);
|
|
903
|
-
return cleanupWarning ? { ...verification, cleanupWarning } : verification;
|
|
904
|
-
} catch (err) {
|
|
905
|
-
if (backupReady) {
|
|
906
|
-
await unlinkIfExists(options.targetPath);
|
|
907
|
-
await fs.promises.rename(options.backupPath, options.targetPath);
|
|
908
|
-
} else if (published) {
|
|
909
|
-
await unlinkIfExists(options.targetPath);
|
|
934
|
+
} catch (rollback) {
|
|
935
|
+
throw new AggregateError([primary, rollback], "installation_rollback_unverified");
|
|
910
936
|
}
|
|
911
|
-
|
|
912
|
-
if (process.platform === "win32" && !stagedNext) await unlinkIfExists(journalPath);
|
|
913
|
-
throw err;
|
|
937
|
+
throw primary;
|
|
914
938
|
}
|
|
915
939
|
}
|
|
916
940
|
|
|
@@ -1004,7 +1028,7 @@ async function downloadBinaryTo(
|
|
|
1004
1028
|
expectedVersion?: string,
|
|
1005
1029
|
): Promise<void> {
|
|
1006
1030
|
const response = await fetch(url, { redirect: "follow" });
|
|
1007
|
-
if (!response.ok || !response.body)
|
|
1031
|
+
if (!response.ok || !response.body)
|
|
1008
1032
|
throw new Error(
|
|
1009
1033
|
formatBinaryDownloadFailureMessage(
|
|
1010
1034
|
binaryName,
|
|
@@ -1014,41 +1038,93 @@ async function downloadBinaryTo(
|
|
|
1014
1038
|
registryNote,
|
|
1015
1039
|
),
|
|
1016
1040
|
);
|
|
1041
|
+
const limit = 512 * 1024 * 1024;
|
|
1042
|
+
const declared = response.headers.get("content-length");
|
|
1043
|
+
if (declared && /^\d+$/.test(declared) && Number(declared) > limit) {
|
|
1044
|
+
await response.body.cancel();
|
|
1045
|
+
throw new Error("candidate_size_limit");
|
|
1017
1046
|
}
|
|
1047
|
+
const handle = await fs.promises.open(
|
|
1048
|
+
tempPath,
|
|
1049
|
+
fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_NOFOLLOW,
|
|
1050
|
+
0o600,
|
|
1051
|
+
);
|
|
1052
|
+
const reader = response.body.getReader();
|
|
1018
1053
|
try {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1054
|
+
let size = 0;
|
|
1055
|
+
for (;;) {
|
|
1056
|
+
const next = await reader.read();
|
|
1057
|
+
if (next.done) break;
|
|
1058
|
+
size += next.value.byteLength;
|
|
1059
|
+
if (size > limit) {
|
|
1060
|
+
await reader.cancel();
|
|
1061
|
+
throw new Error("candidate_size_limit");
|
|
1062
|
+
}
|
|
1063
|
+
await handle.writeFile(next.value);
|
|
1024
1064
|
}
|
|
1025
|
-
|
|
1026
|
-
await
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
if (expectedVersion) {
|
|
1030
|
-
const tag = expectedVersion.startsWith("v") ? expectedVersion : `v${expectedVersion}`;
|
|
1031
|
-
try {
|
|
1065
|
+
if (size === 0) throw new Error("candidate_empty");
|
|
1066
|
+
await handle.sync();
|
|
1067
|
+
const beforeVerification = await handle.stat({ bigint: true });
|
|
1068
|
+
if (expectedVersion)
|
|
1032
1069
|
await verifyDownloadedBinaryChecksum({
|
|
1033
|
-
tag
|
|
1070
|
+
tag: expectedVersion.startsWith("v") ? expectedVersion : `v${expectedVersion}`,
|
|
1034
1071
|
assetName: binaryName,
|
|
1035
1072
|
filePath: tempPath,
|
|
1036
1073
|
});
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1074
|
+
const current = await fs.promises.lstat(tempPath, { bigint: true });
|
|
1075
|
+
if (
|
|
1076
|
+
!current.isFile() ||
|
|
1077
|
+
current.isSymbolicLink() ||
|
|
1078
|
+
current.dev !== beforeVerification.dev ||
|
|
1079
|
+
current.ino !== beforeVerification.ino ||
|
|
1080
|
+
current.size !== beforeVerification.size ||
|
|
1081
|
+
current.mtimeNs !== beforeVerification.mtimeNs
|
|
1082
|
+
)
|
|
1083
|
+
throw new Error("candidate_changed_during_verification");
|
|
1084
|
+
await handle.chmod(0o755);
|
|
1085
|
+
await handle.sync();
|
|
1086
|
+
} finally {
|
|
1087
|
+
reader.releaseLock();
|
|
1088
|
+
await handle.close();
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
/** Production adapter used by doctor restore: official release origin, checksum manifest, and runtime smoke. */
|
|
1093
|
+
export async function fetchAndVerifyOfficialPinnedCandidate(
|
|
1094
|
+
candidate: { ref: string; version: string; os: NodeJS.Platform; arch: string; sha256: string },
|
|
1095
|
+
stagingPath: string,
|
|
1096
|
+
): Promise<void> {
|
|
1097
|
+
if (candidate.os !== process.platform || candidate.arch !== process.arch)
|
|
1098
|
+
throw new Error("candidate_platform_or_arch_mismatch");
|
|
1099
|
+
const expectedTag = candidate.ref.startsWith("v") ? candidate.ref : `v${candidate.ref}`;
|
|
1100
|
+
const expectedVersion = versionFromTag(expectedTag);
|
|
1101
|
+
if (expectedVersion !== candidate.version) throw new Error("candidate_ref_version_mismatch");
|
|
1102
|
+
await downloadBinaryTo(
|
|
1103
|
+
buildReleaseBinaryUrl(candidate.version, candidate.os, candidate.arch),
|
|
1104
|
+
stagingPath,
|
|
1105
|
+
getBinaryNameForPlatform(candidate.os, candidate.arch),
|
|
1106
|
+
undefined,
|
|
1107
|
+
candidate.version,
|
|
1108
|
+
);
|
|
1109
|
+
const staged = await snapshotRegularFile(stagingPath);
|
|
1110
|
+
if (!staged) throw new Error("candidate_missing");
|
|
1111
|
+
const digest = staged.identity.sha256;
|
|
1112
|
+
if (digest.toLowerCase() !== candidate.sha256.toLowerCase()) {
|
|
1113
|
+
throw new Error("candidate_digest_mismatch");
|
|
1041
1114
|
}
|
|
1042
1115
|
}
|
|
1043
1116
|
|
|
1117
|
+
export async function smokeTestPinnedCandidate(candidate: { version: string }, stagingPath: string): Promise<void> {
|
|
1118
|
+
const verification = await verifyInstalledRuntime(candidate.version, stagingPath);
|
|
1119
|
+
if (!verification.ok) throw new Error(formatVerificationFailure(verification, candidate.version));
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1044
1122
|
/** Injectable steps of the binary update flow (seams for testing ordering). */
|
|
1045
1123
|
export interface BinaryUpdateFlow {
|
|
1046
1124
|
download(url: string, tempPath: string): Promise<void>;
|
|
1047
1125
|
fsync(filePath: string): Promise<void>;
|
|
1048
1126
|
replace(options: BinaryReplacementOptions): Promise<InstalledVersionVerification>;
|
|
1049
1127
|
verifyInstalledVersion(expectedVersion: string): Promise<InstalledVersionVerification>;
|
|
1050
|
-
/** Best-effort cleanup of the temp file when the flow aborts before replace. */
|
|
1051
|
-
removeTemp?(filePath: string): Promise<void>;
|
|
1052
1128
|
/** Called once fsync has succeeded, right before replacement begins. */
|
|
1053
1129
|
beforeReplace?(): void;
|
|
1054
1130
|
}
|
|
@@ -1059,8 +1135,8 @@ export interface BinaryUpdateFlow {
|
|
|
1059
1135
|
* before it is published (renamed into place) or exec'd for verification.
|
|
1060
1136
|
*
|
|
1061
1137
|
* If fsync fails the temp bytes are not durable, so we abort before
|
|
1062
|
-
* replacement/verification and
|
|
1063
|
-
* possibly-truncated binary.
|
|
1138
|
+
* replacement/verification and retain the staging artifact rather than risk
|
|
1139
|
+
* deleting a substituted pathname or installing a possibly-truncated binary.
|
|
1064
1140
|
*/
|
|
1065
1141
|
export async function runBinaryUpdateFlow(
|
|
1066
1142
|
targetPath: string,
|
|
@@ -1068,18 +1144,16 @@ export async function runBinaryUpdateFlow(
|
|
|
1068
1144
|
expectedVersion: string,
|
|
1069
1145
|
flow: BinaryUpdateFlow,
|
|
1070
1146
|
): Promise<InstalledVersionVerification> {
|
|
1071
|
-
const stamp =
|
|
1147
|
+
const stamp = randomUUID();
|
|
1072
1148
|
const tempPath = `${targetPath}.new.${stamp}`;
|
|
1073
1149
|
const backupPath = `${targetPath}.bak.${stamp}`;
|
|
1074
1150
|
const releaseLock = await acquireBinaryUpdateLock(targetPath);
|
|
1075
1151
|
try {
|
|
1152
|
+
const originalTarget = await snapshotRegularFile(targetPath);
|
|
1153
|
+
const originalParent = await snapshotDirectory(path.dirname(targetPath));
|
|
1154
|
+
if (!originalParent) throw new Error("installation_parent_unavailable");
|
|
1076
1155
|
await flow.download(url, tempPath);
|
|
1077
|
-
|
|
1078
|
-
await flow.fsync(tempPath);
|
|
1079
|
-
} catch (err) {
|
|
1080
|
-
if (flow.removeTemp) await flow.removeTemp(tempPath);
|
|
1081
|
-
throw err;
|
|
1082
|
-
}
|
|
1156
|
+
await flow.fsync(tempPath);
|
|
1083
1157
|
|
|
1084
1158
|
flow.beforeReplace?.();
|
|
1085
1159
|
return await flow.replace({
|
|
@@ -1087,6 +1161,8 @@ export async function runBinaryUpdateFlow(
|
|
|
1087
1161
|
tempPath,
|
|
1088
1162
|
backupPath,
|
|
1089
1163
|
expectedVersion,
|
|
1164
|
+
originalTarget: originalTarget?.identity,
|
|
1165
|
+
originalParent,
|
|
1090
1166
|
verifyInstalledVersion: flow.verifyInstalledVersion,
|
|
1091
1167
|
});
|
|
1092
1168
|
} finally {
|
|
@@ -1094,46 +1170,14 @@ export async function runBinaryUpdateFlow(
|
|
|
1094
1170
|
}
|
|
1095
1171
|
}
|
|
1096
1172
|
|
|
1097
|
-
async function acquireBinaryUpdateLock(targetPath: string): Promise<() => Promise<void>> {
|
|
1098
|
-
const
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
} finally {
|
|
1106
|
-
await handle.close();
|
|
1107
|
-
}
|
|
1108
|
-
};
|
|
1109
|
-
const ownsClaim = async (): Promise<boolean> => {
|
|
1110
|
-
try {
|
|
1111
|
-
return (await fs.promises.readFile(lockFile, "utf8")) === claim;
|
|
1112
|
-
} catch {
|
|
1113
|
-
return false;
|
|
1114
|
-
}
|
|
1115
|
-
};
|
|
1116
|
-
const release = async (): Promise<void> => {
|
|
1117
|
-
try {
|
|
1118
|
-
if (!(await ownsClaim())) return;
|
|
1119
|
-
await unlinkIfExists(lockFile);
|
|
1120
|
-
} catch {
|
|
1121
|
-
// Best-effort lock release after a verified or failed update.
|
|
1122
|
-
}
|
|
1123
|
-
};
|
|
1124
|
-
try {
|
|
1125
|
-
await publish();
|
|
1126
|
-
return release;
|
|
1127
|
-
} catch (err) {
|
|
1128
|
-
const code = (err as NodeJS.ErrnoException).code;
|
|
1129
|
-
if (code === "ENOENT") return async () => {};
|
|
1130
|
-
if (code === "EEXIST") {
|
|
1131
|
-
throw new Error(
|
|
1132
|
-
`Another ${APP_NAME} update is already running for ${targetPath}. Remove ${lockFile} only after confirming no installer or update is running.`,
|
|
1133
|
-
);
|
|
1134
|
-
}
|
|
1135
|
-
throw err;
|
|
1136
|
-
}
|
|
1173
|
+
export async function acquireBinaryUpdateLock(targetPath: string): Promise<() => Promise<void>> {
|
|
1174
|
+
const parent = await fs.promises.lstat(path.dirname(path.resolve(targetPath))).catch(() => undefined);
|
|
1175
|
+
if (!parent?.isDirectory()) throw new Error("installation_directory_unavailable");
|
|
1176
|
+
return await acquireFileLock(path.join(path.dirname(path.resolve(targetPath)), ".gjc-install"), {
|
|
1177
|
+
signal: AbortSignal.timeout(10_000),
|
|
1178
|
+
retries: 200,
|
|
1179
|
+
retryDelayMs: 50,
|
|
1180
|
+
});
|
|
1137
1181
|
}
|
|
1138
1182
|
|
|
1139
1183
|
/**
|
|
@@ -1233,13 +1277,10 @@ async function updateViaBinaryAt(
|
|
|
1233
1277
|
fsync: fsyncFile,
|
|
1234
1278
|
replace: replaceBinaryForUpdate,
|
|
1235
1279
|
verifyInstalledVersion: version => verifyInstalledRuntime(version, targetPath),
|
|
1236
|
-
removeTemp: unlinkIfExists,
|
|
1237
1280
|
beforeReplace: () => console.log(chalk.dim("Installing update...")),
|
|
1238
1281
|
});
|
|
1239
1282
|
|
|
1240
|
-
printVerifiedVersion(expectedVersion);
|
|
1241
1283
|
if (verification.cleanupWarning) console.warn(chalk.yellow(verification.cleanupWarning));
|
|
1242
|
-
printRestartGuidance();
|
|
1243
1284
|
return verification;
|
|
1244
1285
|
}
|
|
1245
1286
|
|
|
@@ -1247,6 +1288,8 @@ async function updateViaBinaryAt(
|
|
|
1247
1288
|
* Run the update command.
|
|
1248
1289
|
*/
|
|
1249
1290
|
export interface UpdateCommandDependencies {
|
|
1291
|
+
/** User-visible migration guidance owned by the standalone update command, not the TUI logger. */
|
|
1292
|
+
writeStdout?: (text: string) => void;
|
|
1250
1293
|
getLatestRelease?: (options?: LatestReleaseLookupOptions) => Promise<ReleaseInfo>;
|
|
1251
1294
|
resolveUpdateTarget?: () => Promise<UpdateTarget>;
|
|
1252
1295
|
verifyMigrationTarget?: (release: ReleaseInfo, runtimePath: string) => Promise<InstalledVersionVerification>;
|
|
@@ -1261,6 +1304,8 @@ export interface UpdateCommandDependencies {
|
|
|
1261
1304
|
restartDaemon?: (settings: Settings) => Promise<void>;
|
|
1262
1305
|
recoverNotifications?: (settings: Settings) => Promise<void>;
|
|
1263
1306
|
runPostUpdateRecovery?: (runtimePath: string) => Promise<void>;
|
|
1307
|
+
platform?: NodeJS.Platform;
|
|
1308
|
+
offerMacosCommunityApp?: typeof offerMacosCommunityApp;
|
|
1264
1309
|
recordTelemetryEvent?: (event: TelemetryEventName, details: TelemetryDetails) => unknown;
|
|
1265
1310
|
exit?: (code: number) => never;
|
|
1266
1311
|
}
|
|
@@ -1269,6 +1314,24 @@ export type PostUpdateRecoverySpawn = (argv: string[]) => Promise<number>;
|
|
|
1269
1314
|
export type PostUpdateRecoverySupportCheck = (runtimePath: string) => Promise<boolean>;
|
|
1270
1315
|
export type LegacyRecoveryDaemonKinds = () => Promise<NotificationProvider[]>;
|
|
1271
1316
|
|
|
1317
|
+
async function offerCommunityAppAfterUpdate(deps: UpdateCommandDependencies): Promise<void> {
|
|
1318
|
+
const platform = deps.platform ?? process.platform;
|
|
1319
|
+
if (platform !== "darwin") return;
|
|
1320
|
+
try {
|
|
1321
|
+
await (deps.offerMacosCommunityApp ?? offerMacosCommunityApp)({
|
|
1322
|
+
platform,
|
|
1323
|
+
log: message => {
|
|
1324
|
+
process.stderr.write(`${message}\n`);
|
|
1325
|
+
},
|
|
1326
|
+
});
|
|
1327
|
+
} catch (error) {
|
|
1328
|
+
const reason = sanitizeVerificationOutput(error instanceof Error ? error.message : String(error));
|
|
1329
|
+
process.stderr.write(
|
|
1330
|
+
`Optional community app offer failed: ${reason ?? "unknown error"}. GJC remains installed. https://github.com/${COMMUNITY_APP_REPOSITORY}\n`,
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1272
1335
|
/**
|
|
1273
1336
|
* A complete, non-quarantined provider with provider-level desired intent is a
|
|
1274
1337
|
* durable managed-notify setup. The global switch is deliberately excluded:
|
|
@@ -1394,17 +1457,11 @@ async function performUpdate(
|
|
|
1394
1457
|
if (target.previousPath) {
|
|
1395
1458
|
console.log(
|
|
1396
1459
|
chalk.yellow(
|
|
1397
|
-
`Current ${APP_NAME} at ${target.previousPath} is a package-manager shim or wrapper; installing a standalone binary at ${target.path} without overwriting the shim.`,
|
|
1460
|
+
`Current ${APP_NAME} at ${sanitizeVerificationOutput(target.previousPath)} is a package-manager shim or wrapper; installing a standalone binary at ${sanitizeVerificationOutput(target.path)} without overwriting the shim.`,
|
|
1398
1461
|
),
|
|
1399
1462
|
);
|
|
1400
1463
|
}
|
|
1401
|
-
|
|
1402
|
-
console.log(
|
|
1403
|
-
chalk.cyan(
|
|
1404
|
-
`Put ${path.dirname(target.path)} first on PATH so the standalone binary wins over any leftover Bun/npm shim.`,
|
|
1405
|
-
),
|
|
1406
|
-
);
|
|
1407
|
-
return verification;
|
|
1464
|
+
return await updateViaBinaryAt(target.path, expectedVersion, registry);
|
|
1408
1465
|
}
|
|
1409
1466
|
if (target.method === "binary") {
|
|
1410
1467
|
return await updateViaBinaryAt(target.path, expectedVersion, registry);
|
|
@@ -1454,22 +1511,43 @@ export function resolveUpdateDecision(options: {
|
|
|
1454
1511
|
return { install: true, kind: options.comparison > 0 ? "new-version" : "force" };
|
|
1455
1512
|
}
|
|
1456
1513
|
|
|
1457
|
-
function
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
)
|
|
1514
|
+
export function formatVerifiedBinaryInvocation(runtimePath: string, platform: NodeJS.Platform): string {
|
|
1515
|
+
// PowerShell treats smart single quotes as delimiters too; preserve each by doubling it.
|
|
1516
|
+
return platform === "win32"
|
|
1517
|
+
? `& '${runtimePath.replace(/['‘’‚‛]/g, quote => quote + quote)}'`
|
|
1518
|
+
: `'${runtimePath.replace(/'/g, "'\\''")}'`;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1521
|
+
function printVerifiedMigrationTarget(
|
|
1522
|
+
target: MigrationUpdateTarget,
|
|
1523
|
+
version: string,
|
|
1524
|
+
writeStdout: (text: string) => void,
|
|
1525
|
+
alreadyInstalled = true,
|
|
1526
|
+
): void {
|
|
1527
|
+
const displayPath = sanitizeVerificationOutput(target.path);
|
|
1528
|
+
const directory = sanitizeVerificationOutput(path.dirname(target.path));
|
|
1529
|
+
const quotedPath = formatVerifiedBinaryInvocation(target.path, process.platform);
|
|
1530
|
+
const invocation =
|
|
1531
|
+
displayPath === target.path
|
|
1532
|
+
? `Run the verified binary directly: ${quotedPath} --version (omit --version to launch).`
|
|
1533
|
+
: "Run the verified binary directly using its exact local path (displayed path was sanitized).";
|
|
1534
|
+
writeStdout(
|
|
1535
|
+
`${chalk.green(
|
|
1536
|
+
`${theme.status.success} Standalone ${APP_NAME} ${version} is ${alreadyInstalled ? "already installed" : "installed"} and verified at ${displayPath}.${version === VERSION ? " Version unchanged; this is an installation migration, not a version update." : ""}`,
|
|
1537
|
+
)}\n`,
|
|
1538
|
+
);
|
|
1539
|
+
writeStdout(
|
|
1540
|
+
`${chalk.cyan(
|
|
1541
|
+
[
|
|
1542
|
+
"Shell activation is not verified; package-manager shims were not overwritten or uninstalled.",
|
|
1543
|
+
invocation,
|
|
1544
|
+
process.platform === "win32"
|
|
1545
|
+
? "Check resolution in PowerShell: Get-Command gjc -All; where.exe gjc."
|
|
1546
|
+
: "In your current shell, check: type -a gjc; command -v gjc. Clear cached commands with hash -r (Bash) or rehash (zsh), then repeat the checks and run gjc --version.",
|
|
1547
|
+
`Only if resolution still selects another install, ensure ${directory} is on PATH before the shim directory; also check shell aliases/functions.`,
|
|
1548
|
+
].join("\n"),
|
|
1549
|
+
)}\n`,
|
|
1462
1550
|
);
|
|
1463
|
-
if (target.previousPath) {
|
|
1464
|
-
console.log(chalk.yellow(`${target.previousPath} shadows it on PATH.`));
|
|
1465
|
-
console.log(
|
|
1466
|
-
chalk.cyan(
|
|
1467
|
-
`The standalone directory ${path.dirname(target.path)} must precede the shim directory ${path.dirname(target.previousPath)} on PATH.`,
|
|
1468
|
-
),
|
|
1469
|
-
);
|
|
1470
|
-
return;
|
|
1471
|
-
}
|
|
1472
|
-
console.log(chalk.cyan(`Ensure the standalone directory ${path.dirname(target.path)} is on PATH.`));
|
|
1473
1551
|
}
|
|
1474
1552
|
|
|
1475
1553
|
export async function runUpdateCommand(
|
|
@@ -1483,6 +1561,7 @@ export async function runUpdateCommand(
|
|
|
1483
1561
|
const update = deps.performUpdate ?? performUpdate;
|
|
1484
1562
|
const refreshDefaults = deps.refreshInstalledDefaultSkills ?? refreshInstalledDefaultSkills;
|
|
1485
1563
|
const exit = deps.exit ?? process.exit;
|
|
1564
|
+
const writeStdout = deps.writeStdout ?? (text => process.stdout.write(text));
|
|
1486
1565
|
const recordEvent = deps.recordTelemetryEvent ?? ((event, details) => recordTelemetryEvent(event, details));
|
|
1487
1566
|
const pendingTelemetry = new Set<Promise<void>>();
|
|
1488
1567
|
const record = (event: TelemetryEventName, details: TelemetryDetails): void => {
|
|
@@ -1553,18 +1632,31 @@ export async function runUpdateCommand(
|
|
|
1553
1632
|
});
|
|
1554
1633
|
|
|
1555
1634
|
if (target.method === "migrate" && decision.install && !opts.force) {
|
|
1556
|
-
|
|
1635
|
+
// Check mode is read-only, including when another installer holds the lock.
|
|
1636
|
+
// A first-time migration may intentionally target a path whose parent does
|
|
1637
|
+
// not exist yet; the real install creates it before entering the locked
|
|
1638
|
+
// replacement flow. There is no existing lock to contend with in that
|
|
1639
|
+
// state, so preflight remains read-only and lock-free until installation.
|
|
1640
|
+
const targetParent = path.dirname(path.resolve(target.path));
|
|
1641
|
+
const parent = await fs.promises.lstat(targetParent).catch(() => undefined);
|
|
1642
|
+
const releaseLock = opts.check || !parent?.isDirectory() ? undefined : await acquireBinaryUpdateLock(target.path);
|
|
1643
|
+
let verified = false;
|
|
1557
1644
|
try {
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1645
|
+
verified = (await verifyTarget(release, target.path)).ok;
|
|
1646
|
+
} finally {
|
|
1647
|
+
await releaseLock?.();
|
|
1648
|
+
}
|
|
1649
|
+
if (verified) {
|
|
1650
|
+
record("update_check_completed", { channel, result: "available" });
|
|
1651
|
+
printVerifiedMigrationTarget(target, release.version, writeStdout);
|
|
1652
|
+
if (opts.check) {
|
|
1653
|
+
record("update_install_completed", { channel, result: "skipped" });
|
|
1564
1654
|
return;
|
|
1565
1655
|
}
|
|
1566
|
-
|
|
1567
|
-
|
|
1656
|
+
record("update_install_started", { channel, installMethod: target.method });
|
|
1657
|
+
record("update_install_completed", { channel, result: "installed", installMethod: target.method });
|
|
1658
|
+
await offerCommunityAppAfterUpdate(deps);
|
|
1659
|
+
return;
|
|
1568
1660
|
}
|
|
1569
1661
|
}
|
|
1570
1662
|
|
|
@@ -1595,14 +1687,19 @@ export async function runUpdateCommand(
|
|
|
1595
1687
|
try {
|
|
1596
1688
|
const resolved = target ?? (await resolveTarget());
|
|
1597
1689
|
const verification = await update(resolved, release.version, release.registry);
|
|
1598
|
-
if (verification?.path) {
|
|
1690
|
+
if (verification?.ok && verification.path) {
|
|
1599
1691
|
installedVersion = release.version;
|
|
1692
|
+
if (resolved.method === "migrate") {
|
|
1693
|
+
printVerifiedMigrationTarget({ ...resolved, path: verification.path }, release.version, writeStdout, false);
|
|
1694
|
+
} else {
|
|
1695
|
+
printSuccessfulVerification(release.version);
|
|
1696
|
+
}
|
|
1600
1697
|
await (deps.runPostUpdateRecovery ?? runPostUpdateRecovery)(verification.path);
|
|
1601
1698
|
} else if (!deps.performUpdate) throw new Error("verified installed runtime path is unavailable");
|
|
1602
1699
|
} catch (err) {
|
|
1603
1700
|
record("update_install_failed", { channel, result: "failed", installMethod: target.method });
|
|
1604
1701
|
const prefix = installedVersion
|
|
1605
|
-
?
|
|
1702
|
+
? `${target.method === "migrate" ? "Standalone binary installed and verified at version" : "Updated to"} ${installedVersion}, but post-update recovery failed`
|
|
1606
1703
|
: "Update failed";
|
|
1607
1704
|
console.error(chalk.red(`${prefix}: ${err}`));
|
|
1608
1705
|
return flushTelemetryBeforeExit();
|
|
@@ -1612,6 +1709,7 @@ export async function runUpdateCommand(
|
|
|
1612
1709
|
// refreshes opt-in local definitions, avoiding stale-module daemon control.
|
|
1613
1710
|
await refreshDefaults();
|
|
1614
1711
|
record("update_install_completed", { channel, result: "installed", installMethod: target.method });
|
|
1712
|
+
await offerCommunityAppAfterUpdate(deps);
|
|
1615
1713
|
}
|
|
1616
1714
|
|
|
1617
1715
|
/**
|