@timurproko/a1 0.2.7-dev.820 → 0.2.7-dev.828
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 +7 -1
- package/bin/automatic-update.js +7 -0
- package/bin/tab-launch-context.d.ts +1 -0
- package/bin/tab-launch-context.js +3 -3
- package/bin/ui.js +96 -66
- package/dist/app/session-shell/session-shell-root.d.ts +13 -6
- package/dist/app/session-shell/session-shell-root.js +9 -5
- package/dist/app/session-shell/session-shell.js +68 -18
- package/dist/app/session-shell/shell-host.js +6 -3
- package/dist/app/session-shell/transcript-document.js +1 -1
- package/dist/cli/tabs.d.ts +1 -1
- package/dist/cli/tabs.js +10 -14
- package/dist/composition/owned-settings.d.ts +12 -0
- package/dist/composition/owned-settings.js +36 -0
- package/dist/composition/owned-ui.d.ts +5 -14
- package/dist/composition/owned-ui.js +93 -63
- package/dist/composition/resident-tab-bridge.d.ts +10 -1
- package/dist/composition/resident-tab-bridge.js +21 -0
- package/dist/composition/settings-route-host.d.ts +12 -2
- package/dist/composition/settings-route-host.js +27 -9
- package/dist/composition/settings-search-input.d.ts +9 -0
- package/dist/composition/settings-search-input.js +58 -0
- package/dist/composition/utility-tab.d.ts +28 -0
- package/dist/composition/utility-tab.js +242 -0
- package/dist/contracts/owned-ui/session-backend.d.ts +2 -0
- package/dist/contracts/owned-ui/session-workflows.d.ts +7 -3
- package/dist/contracts/presentation/index.d.ts +11 -0
- package/dist/contracts/presentation/index.js +12 -0
- package/dist/features/launch/development-launch.js +6 -0
- package/dist/features/launch/profile-paths.js +4 -0
- package/dist/features/owned-ui/plus-menu-dialog.d.ts +22 -0
- package/dist/features/owned-ui/plus-menu-dialog.js +76 -0
- package/dist/features/owned-ui/project-trust-dialog.d.ts +1 -1
- package/dist/features/owned-ui/project-trust-dialog.js +6 -8
- package/dist/features/owned-ui/project-trust-prompt.d.ts +1 -0
- package/dist/features/owned-ui/project-trust-prompt.js +7 -3
- package/dist/features/owned-ui/reference-screen-app.d.ts +2 -0
- package/dist/features/owned-ui/reference-screen-app.js +12 -2
- package/dist/features/owned-ui/settings-app.d.ts +10 -3
- package/dist/features/owned-ui/settings-app.js +98 -45
- package/dist/features/owned-ui/settings-search-input.d.ts +29 -0
- package/dist/features/owned-ui/settings-search-input.js +1 -0
- package/dist/features/prompt-history/index.d.ts +1 -1
- package/dist/features/prompt-history/paths.d.ts +2 -1
- package/dist/features/prompt-history/paths.js +10 -2
- package/dist/features/prompt-history/service.d.ts +5 -0
- package/dist/features/prompt-history/service.js +1 -1
- package/dist/features/resident-tab/shortcut-declarations.d.ts +1 -1
- package/dist/features/resident-tab/shortcut-declarations.js +2 -0
- package/dist/foundation/launch-context/index.d.ts +2 -0
- package/dist/foundation/launch-context/index.js +1 -0
- package/dist/foundation/lifecycle/session-repository-context.js +16 -2
- package/dist/foundation/release/automatic-update.d.ts +37 -0
- package/dist/foundation/release/automatic-update.js +265 -0
- package/dist/foundation/release/bootstrap.d.ts +1 -9
- package/dist/foundation/release/bootstrap.js +7 -3
- package/dist/foundation/release/early-accent.d.ts +12 -0
- package/dist/foundation/release/early-accent.js +59 -0
- package/dist/foundation/release/index.d.ts +1 -1
- package/dist/foundation/release/index.js +1 -1
- package/dist/foundation/release/latest-release.d.ts +3 -2
- package/dist/foundation/release/latest-release.js +1 -1
- package/dist/foundation/release/progress-palette.generated.d.ts +137 -3
- package/dist/foundation/release/progress-palette.generated.js +137 -3
- package/dist/foundation/release/release-gc.js +19 -4
- package/dist/foundation/release/update-execution-lease.d.ts +36 -0
- package/dist/foundation/release/update-execution-lease.js +145 -0
- package/dist/foundation/release/update.d.ts +9 -7
- package/dist/foundation/release/update.js +244 -175
- package/dist/foundation/resident-tabs/bridge-client.d.ts +9 -2
- package/dist/foundation/resident-tabs/bridge-client.js +19 -7
- package/dist/foundation/resident-tabs/maintenance.d.ts +2 -6
- package/dist/foundation/resident-tabs/maintenance.js +5 -23
- package/dist/foundation/resident-tabs/progress-observation-schema.d.ts +34 -0
- package/dist/foundation/resident-tabs/progress-observation-schema.js +1 -0
- package/dist/foundation/resident-tabs/progress-observation.d.ts +3 -6
- package/dist/foundation/resident-tabs/progress-observation.js +43 -12
- package/dist/foundation/resident-tabs/release-holds.js +3 -14
- package/dist/foundation/resident-tabs/routes.d.ts +5 -0
- package/dist/foundation/resident-tabs/routes.js +5 -0
- package/dist/integrations/pi/components/editor-interaction.d.ts +2 -0
- package/dist/integrations/pi/components/modal-frame.d.ts +17 -1
- package/dist/integrations/pi/components/modal-frame.js +48 -9
- package/dist/integrations/pi/components/models-dialog.js +4 -52
- package/dist/integrations/pi/components/owned-editor-ux.d.ts +3 -0
- package/dist/integrations/pi/components/owned-editor-ux.js +75 -4
- package/dist/integrations/pi/components/shell-editor-autocomplete.js +57 -37
- package/dist/integrations/pi/components/shell-footer-status.js +19 -3
- package/dist/integrations/pi/components/shell-presenters-transcript.js +4 -1
- package/dist/integrations/pi/components/shell-selectors-dialogs.js +4 -1
- package/dist/integrations/pi/components/shell-shared-facade.d.ts +9 -2
- package/dist/integrations/pi/components/skills-dialog.js +2 -2
- package/dist/integrations/pi/components/upstream/components/owned-editor.d.ts +7 -5
- package/dist/integrations/pi/components/upstream/components/owned-editor.js +15 -8
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.d.ts +3 -3
- package/dist/integrations/pi/components/upstream/components/scoped-models-selector.js +2 -2
- package/dist/integrations/pi/components/upstream/components/session-footer.js +26 -23
- package/dist/integrations/pi/components/upstream/components/session-selector.js +7 -6
- package/dist/integrations/pi/components/upstream/components/thinking-selector.d.ts +7 -6
- package/dist/integrations/pi/components/upstream/components/thinking-selector.js +9 -8
- package/dist/integrations/pi/components/upstream/components/tool-execution.js +6 -4
- package/dist/integrations/pi/components/upstream/components/tree-selector.d.ts +8 -8
- package/dist/integrations/pi/components/upstream/components/tree-selector.js +10 -10
- package/dist/integrations/pi/components/upstream/history/editor-core.js +2 -0
- package/dist/integrations/pi/components/upstream/theme/theme.d.ts +2 -0
- package/dist/integrations/pi/components/upstream/theme/theme.js +5 -1
- package/dist/integrations/pi/engine/adapter.js +7 -2
- package/dist/integrations/pi/engine/offline-settings-port.d.ts +23 -0
- package/dist/integrations/pi/engine/offline-settings-port.js +53 -0
- package/dist/integrations/pi/engine/ports.d.ts +1 -1
- package/dist/integrations/pi/engine/ports.js +4 -0
- package/dist/integrations/pi/engine/settings-bridge.d.ts +5 -0
- package/dist/integrations/pi/engine/settings-bridge.js +13 -0
- package/dist/integrations/pi/engine/settings-port.js +1 -14
- package/dist/integrations/pi/engine/workflow-runner.js +9 -2
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/darwin-arm64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-arm64/terminal-host.node +0 -0
- package/dist/native/darwin-x64/manifest.json +1 -1
- package/dist/native/darwin-x64/terminal-host.manifest.json +3 -3
- package/dist/native/darwin-x64/terminal-host.node +0 -0
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/linux-x64/terminal-host.manifest.json +3 -3
- package/dist/native/linux-x64/terminal-host.node +0 -0
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.node +0 -0
- package/dist/native/win32-x64/terminal-host.manifest.json +3 -3
- package/dist/native/win32-x64/terminal-host.node +0 -0
- package/dist/product-identity.d.ts +1 -1
- package/dist/product-identity.js +2 -2
- package/dist/product-identity.json +2 -0
- package/dist/ui/apps/host.d.ts +2 -0
- package/dist/ui/apps/host.js +7 -1
- package/dist/ui/components/dialog-panel.js +35 -30
- package/dist/ui/components/line-input.d.ts +3 -0
- package/dist/ui/components/line-input.js +3 -1
- package/dist/ui/components/prompt-input.d.ts +3 -2
- package/dist/ui/components/prompt-input.js +6 -4
- package/dist/ui/components/spans.js +2 -2
- package/dist/ui/components/surface.js +4 -2
- package/dist/ui/components/text.d.ts +3 -6
- package/dist/ui/components/text.js +6 -8
- package/dist/ui/settings/declarations.d.ts +56 -10
- package/dist/ui/settings/declarations.js +38 -9
- package/dist/ui/settings/early-accent.d.ts +81 -0
- package/dist/ui/settings/early-accent.js +50 -0
- package/dist/ui/settings/manager.js +7 -3
- package/dist/ui/settings/migrations.js +15 -0
- package/dist/ui/settings/progress-palette.generated.d.ts +139 -0
- package/dist/ui/settings/progress-palette.generated.js +139 -0
- package/dist/ui/settings/sections.d.ts +4 -0
- package/dist/ui/settings/sections.js +9 -3
- package/docs/architecture/resident-tabs.md +48 -17
- package/docs/architecture/tool-image-presentation.md +2 -1
- package/docs/features/prompt-history.md +41 -20
- package/docs/validation.md +25 -5
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@ import { dirname, isAbsolute, relative, resolve, sep } from "node:path";
|
|
|
5
5
|
import crossSpawn from "cross-spawn";
|
|
6
6
|
import { valid as validSemver } from "semver";
|
|
7
7
|
import { PRODUCT_IDENTITY, PRODUCT_TEXT } from "../../product-identity.js";
|
|
8
|
+
import { earlyAccentPalette, earlyTerminalAppearance, earlyTerminalColorMode, resolveEarlyAccentColor } from "./early-accent.js";
|
|
8
9
|
import { SHARED_PROGRESS_ACCENT_ANSI } from "./progress-palette.generated.js";
|
|
9
10
|
import { isNewerRelease, releaseChannelOf } from "./latest-release.js";
|
|
10
11
|
import { certifyMaterializedRelease, ensureSupervisor, waitForProcessExit, } from "./bootstrap.js";
|
|
@@ -17,6 +18,7 @@ import { materializeRelease, readMaterializedRelease } from "./release-store.js"
|
|
|
17
18
|
import { scheduleReleaseCleanup } from "./release-gc.js";
|
|
18
19
|
import { UPDATE_ACTIVATION_CONTRACT, activateInstalledRelease, delegateActivation, readActivationContracts, } from "./update-activation.js";
|
|
19
20
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
21
|
+
import { acquireUpdateExecutionLease } from "./update-execution-lease.js";
|
|
20
22
|
import { npmPrefixForGlobalRoot, removeUpdateRecoveryCapsule, resolveNpmCli, runProtectedPackageReplacement, updateLauncherPaths, updateNpmInstallArguments, } from "./update-recovery.js";
|
|
21
23
|
export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
|
|
22
24
|
const UPDATE_DIST_TAGS = { stable: "latest", next: "next" };
|
|
@@ -34,6 +36,7 @@ const defaultFileSystem = {
|
|
|
34
36
|
* The update therefore keeps checking for this long before it calls the package locked.
|
|
35
37
|
*/
|
|
36
38
|
export const PACKAGE_UNLOCK_PATIENCE_MS = 15_000;
|
|
39
|
+
export const UPDATE_ALREADY_RUNNING_EXIT_CODE = 75;
|
|
37
40
|
const PACKAGE_UNLOCK_INITIAL_DELAY_MS = 100;
|
|
38
41
|
const PACKAGE_UNLOCK_MAX_DELAY_MS = 1_000;
|
|
39
42
|
// Platform: Windows reports a held directory as EPERM or EBUSY; EACCES and ENOTEMPTY are the other
|
|
@@ -85,6 +88,21 @@ export function createNpmProcessRunner(platform = process.platform) {
|
|
|
85
88
|
* its own endpoint. A cohort running from the mutable installation is the one exception,
|
|
86
89
|
* because that installation is exactly what is about to be replaced under it.
|
|
87
90
|
*/
|
|
91
|
+
async function verifyAutomaticUpdateSafety(paths, stateStore, environment) {
|
|
92
|
+
const state = await stateStore.read();
|
|
93
|
+
const owner = await readActiveEndpoint(paths, state.references.active, environment);
|
|
94
|
+
if (!owner)
|
|
95
|
+
return;
|
|
96
|
+
const ownership = await probeOwnership(owner.metadata);
|
|
97
|
+
if (ownership === "dead")
|
|
98
|
+
return;
|
|
99
|
+
if (ownership !== "live-verified") {
|
|
100
|
+
throw new UpdateAutomaticDeferred(PRODUCT_TEXT.diagnostic(`automatic update deferred because supervisor ownership is ${ownership}`));
|
|
101
|
+
}
|
|
102
|
+
if (!await canonicalImmutableRoot(paths.dataDir, owner.metadata.releaseRoot)) {
|
|
103
|
+
throw new UpdateAutomaticDeferred(PRODUCT_TEXT.diagnostic("automatic update deferred because the running session is not using a retained immutable release; run the update command manually"));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
88
106
|
export function planUpdateOwnership(ownership, runsFromRetainedRelease) {
|
|
89
107
|
if (ownership === "dead")
|
|
90
108
|
return "clean-dead-record";
|
|
@@ -111,13 +129,13 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
111
129
|
const stateStore = new CohortStateStore(paths.dataDir);
|
|
112
130
|
return {
|
|
113
131
|
async targetIsActive(targetVersion) {
|
|
114
|
-
const state = await stateStore.read();
|
|
115
|
-
const activeId = state.references.active;
|
|
132
|
+
const state = await stateStore.read(), activeId = state.references.active;
|
|
116
133
|
if (!activeId || state.releases[activeId]?.packageVersion !== targetVersion)
|
|
117
134
|
return false;
|
|
118
135
|
const endpoint = await readActiveEndpoint(paths, activeId, environment);
|
|
119
136
|
return endpoint?.metadata.releaseId === activeId && await probeOwnership(endpoint.metadata) === "live-verified";
|
|
120
137
|
},
|
|
138
|
+
async verifyAutomaticSafety() { await verifyAutomaticUpdateSafety(paths, stateStore, environment); },
|
|
121
139
|
async shutdownVerifiedOwners(targetVersion) {
|
|
122
140
|
const state = await stateStore.read();
|
|
123
141
|
const activeId = state.references.active;
|
|
@@ -233,16 +251,19 @@ const ACTIVATION_PROGRESS = {
|
|
|
233
251
|
"active-reference-committed": { at: 96, creepTo: 99 },
|
|
234
252
|
};
|
|
235
253
|
/** Renders the terminal-only update meter; the visual preview shares this exact frame. */
|
|
236
|
-
export function renderUpdateProgressBar(percent) {
|
|
254
|
+
export function renderUpdateProgressBar(percent, accent = SHARED_PROGRESS_ACCENT_ANSI) {
|
|
237
255
|
const bounded = Math.min(100, Math.max(0, Math.round(percent)));
|
|
238
256
|
const filled = Math.round((bounded / 100) * PROGRESS_BAR_WIDTH);
|
|
239
|
-
// Rationale: the completed run follows the
|
|
240
|
-
//
|
|
257
|
+
// Rationale: the completed run follows the A1 profile accent; the track and
|
|
258
|
+
// percentage retain the update meter's neutral treatment.
|
|
241
259
|
const gray = "\u001b[38;2;128;128;128m";
|
|
242
260
|
const track = "\u001b[38;2;102;102;102m";
|
|
243
|
-
return `${
|
|
261
|
+
return `${accent}${"━".repeat(filled)}${track}${"─".repeat(PROGRESS_BAR_WIDTH - filled)}${gray} ${bounded}%\u001b[39m`;
|
|
262
|
+
}
|
|
263
|
+
function updateProgressAccent(environment) {
|
|
264
|
+
return earlyAccentPalette(resolveEarlyAccentColor(environment), earlyTerminalAppearance(environment), earlyTerminalColorMode(environment)).accent;
|
|
244
265
|
}
|
|
245
|
-
function createUpdateProgress(output, enabled) {
|
|
266
|
+
function createUpdateProgress(output, enabled, accent = SHARED_PROGRESS_ACCENT_ANSI) {
|
|
246
267
|
let visible = false;
|
|
247
268
|
let current = 0;
|
|
248
269
|
let shown = -1;
|
|
@@ -253,7 +274,7 @@ function createUpdateProgress(output, enabled) {
|
|
|
253
274
|
return;
|
|
254
275
|
shown = rounded;
|
|
255
276
|
visible = true;
|
|
256
|
-
output.stdout(`\r${renderUpdateProgressBar(rounded)}`);
|
|
277
|
+
output.stdout(`\r${renderUpdateProgressBar(rounded, accent)}`);
|
|
257
278
|
};
|
|
258
279
|
const stopCreep = () => {
|
|
259
280
|
if (timer !== null) {
|
|
@@ -455,9 +476,58 @@ async function launchersOwnPackage(globalRoot, fileSystem) {
|
|
|
455
476
|
return false;
|
|
456
477
|
}
|
|
457
478
|
}
|
|
479
|
+
async function resolveSelfUpdateTarget(input) {
|
|
480
|
+
if (input.exactTarget !== undefined && input.exactTarget.length > 0) {
|
|
481
|
+
return resolveExactAutomaticTarget(input.exactTarget, input.channel, input.output);
|
|
482
|
+
}
|
|
483
|
+
return input.requested === undefined || input.requested.length === 0
|
|
484
|
+
? await resolveChannelHead(input.runner, input.distTag, input.output)
|
|
485
|
+
: await resolveRequestedPreview(input.runner, input.requested, input.output);
|
|
486
|
+
}
|
|
487
|
+
function resolveExactAutomaticTarget(target, channel, output) {
|
|
488
|
+
const version = validSemver(target);
|
|
489
|
+
const expected = channel === "stable" ? "stable" : "development";
|
|
490
|
+
if (version === null || releaseChannelOf(version) !== expected) {
|
|
491
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`refused invalid automatic ${expected} update target ${JSON.stringify(target)}`)}\n`);
|
|
492
|
+
return { version: null, exitCode: 1 };
|
|
493
|
+
}
|
|
494
|
+
return { version, exitCode: 0 };
|
|
495
|
+
}
|
|
458
496
|
function reportUnmanagedInstallation(output, packageRoot, activeGlobalRoot) {
|
|
459
497
|
output.stderr(`${PRODUCT_TEXT.diagnostic(`refused to update automatically because ${packageRoot} could not be verified as an npm-managed ${PRODUCT_PACKAGE} installation; npm's active global package root is ${activeGlobalRoot}. Run npm install --global ${PRODUCT_PACKAGE} to install it manually.`)}\n`);
|
|
460
498
|
}
|
|
499
|
+
async function withUpdateExecutionLease(input, options, output, operation) {
|
|
500
|
+
const lease = await acquireUpdateExecutionLease(input);
|
|
501
|
+
if (lease.status === "busy") {
|
|
502
|
+
options.onLeaseBusy?.(lease.owner);
|
|
503
|
+
if ((options.mode ?? "manual") === "manual") {
|
|
504
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`update already in progress for ${lease.owner.targetVersion}`)}\n`);
|
|
505
|
+
}
|
|
506
|
+
return UPDATE_ALREADY_RUNNING_EXIT_CODE;
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
options.onLeaseAcquired?.(lease.owner);
|
|
510
|
+
return await operation();
|
|
511
|
+
}
|
|
512
|
+
finally {
|
|
513
|
+
await lease.release();
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
async function readRunningPackageVersion(packageRoot, fileSystem, output) {
|
|
517
|
+
try {
|
|
518
|
+
const packageJson = JSON.parse(await fileSystem.readFile(resolve(packageRoot, "package.json")));
|
|
519
|
+
const version = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
|
|
520
|
+
if (packageJson.name !== PRODUCT_PACKAGE)
|
|
521
|
+
throw new Error(`package.json does not identify ${PRODUCT_PACKAGE}`);
|
|
522
|
+
if (version === null)
|
|
523
|
+
throw new Error("package.json does not contain a valid semantic version");
|
|
524
|
+
return version;
|
|
525
|
+
}
|
|
526
|
+
catch (error) {
|
|
527
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
}
|
|
461
531
|
export async function runSelfUpdate(options) {
|
|
462
532
|
const fileSystem = options.fileSystem ?? defaultFileSystem;
|
|
463
533
|
const output = options.output ?? defaultOutput;
|
|
@@ -474,24 +544,14 @@ export async function runSelfUpdate(options) {
|
|
|
474
544
|
options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - startedAt) });
|
|
475
545
|
}
|
|
476
546
|
};
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
const packageJson = JSON.parse(await measure("package-version", async () => await fileSystem.readFile(resolve(options.packageRoot, "package.json"))));
|
|
480
|
-
const parsedVersion = typeof packageJson.version === "string" ? validSemver(packageJson.version) : null;
|
|
481
|
-
if (packageJson.name !== PRODUCT_PACKAGE)
|
|
482
|
-
throw new Error(`package.json does not identify ${PRODUCT_PACKAGE}`);
|
|
483
|
-
if (parsedVersion === null)
|
|
484
|
-
throw new Error("package.json does not contain a valid semantic version");
|
|
485
|
-
runningVersion = parsedVersion;
|
|
486
|
-
}
|
|
487
|
-
catch (error) {
|
|
488
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not read its running package version: ${errorMessage(error)}`)}\n`);
|
|
547
|
+
const runningVersion = await measure("package-version", async () => await readRunningPackageVersion(options.packageRoot, fileSystem, output));
|
|
548
|
+
if (runningVersion === null)
|
|
489
549
|
return 1;
|
|
490
|
-
}
|
|
491
550
|
const requested = options.target?.trim();
|
|
492
|
-
const
|
|
493
|
-
|
|
494
|
-
|
|
551
|
+
const exactTarget = options.resolvedTarget?.trim();
|
|
552
|
+
const resolved = await measure("target-resolution", async () => await resolveSelfUpdateTarget({
|
|
553
|
+
runner, output, channel, distTag, requested, exactTarget,
|
|
554
|
+
}));
|
|
495
555
|
if (resolved.version === null)
|
|
496
556
|
return resolved.exitCode;
|
|
497
557
|
const targetVersion = resolved.version;
|
|
@@ -506,178 +566,185 @@ export async function runSelfUpdate(options) {
|
|
|
506
566
|
// Rationale: no full stop after a version: it already ends in a dot-separated identifier,
|
|
507
567
|
// and a trailing one reads as part of the version rather than as punctuation.
|
|
508
568
|
output.stdout(`${PRODUCT_TEXT.commandName} update: ${runningVersion} → ${targetVersion}\n`);
|
|
509
|
-
const
|
|
569
|
+
const environment = options.environment ?? process.env;
|
|
570
|
+
const progress = createUpdateProgress(output, options.progress ?? (options.output === undefined && process.stdout.isTTY === true), updateProgressAccent(environment));
|
|
510
571
|
const managed = await measure("global-root", async () => await resolveManagedNpmInstallation(options.packageRoot, fileSystem, runner, output));
|
|
511
572
|
if (managed.installation === null)
|
|
512
573
|
return managed.exitCode;
|
|
513
574
|
const { packageRoot, globalRoot, npmCliRoot } = managed.installation;
|
|
514
|
-
const environment = options.environment ?? process.env;
|
|
515
575
|
const paths = resolveProductPaths(environment);
|
|
516
576
|
const lifecycle = options.lifecycle ?? createUpdateLifecycleCoordinator(environment, fileSystem, output);
|
|
517
577
|
const transactionStore = options.transactionStore ?? new UpdateTransactionStore(paths.dataDir);
|
|
578
|
+
const mode = options.mode ?? "manual";
|
|
518
579
|
const maintenance = options.maintenance ?? (options.lifecycle
|
|
519
580
|
? async () => { }
|
|
520
581
|
: async () => await scheduleReleaseCleanup(paths.dataDir, paths));
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
if (preflightRequired) {
|
|
533
|
-
try {
|
|
534
|
-
npmCli = await (options.npmCliResolver ?? resolveNpmCli)(npmCliRoot, environment);
|
|
582
|
+
return await withUpdateExecutionLease({ dataDir: paths.dataDir, packageRoot, channel, targetVersion, mode }, options, output, async () => {
|
|
583
|
+
let transaction = await transactionStore.read();
|
|
584
|
+
let npmCli;
|
|
585
|
+
try {
|
|
586
|
+
if (mode === "automatic")
|
|
587
|
+
await lifecycle.verifyAutomaticSafety?.();
|
|
588
|
+
if ((!transaction || transaction.status === "completed") && await lifecycle.targetIsActive(targetVersion)) {
|
|
589
|
+
await maintenance();
|
|
590
|
+
await transactionStore.clearCompleted();
|
|
591
|
+
output.stdout(`${PRODUCT_TEXT.commandName} is up to date — no update needed.\n`);
|
|
592
|
+
return 0;
|
|
535
593
|
}
|
|
536
|
-
|
|
537
|
-
|
|
594
|
+
const preflightRequired = options.npmCliResolver !== undefined
|
|
595
|
+
|| options.packageReplacement === undefined && options.runner === undefined;
|
|
596
|
+
if (preflightRequired) {
|
|
597
|
+
try {
|
|
598
|
+
npmCli = await (options.npmCliResolver ?? resolveNpmCli)(npmCliRoot, environment);
|
|
599
|
+
}
|
|
600
|
+
catch (error) {
|
|
601
|
+
throw new UpdatePreflightFailure(errorMessage(error));
|
|
602
|
+
}
|
|
538
603
|
}
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
priorActiveReleaseId,
|
|
560
|
-
});
|
|
561
|
-
if (phaseBefore(transaction.phase, "ownership-released")) {
|
|
562
|
-
await measure("ownership-release", async () => {
|
|
563
|
-
await lifecycle.shutdownVerifiedOwners(targetVersion);
|
|
564
|
-
await lifecycle.verifyPackageUnlocked(packageRoot);
|
|
604
|
+
// Rationale: the bar first appears only after every non-mutating replacement prerequisite passes.
|
|
605
|
+
progress.set(3, 15);
|
|
606
|
+
const cohortStore = new CohortStateStore(paths.dataDir);
|
|
607
|
+
let cohortState = await cohortStore.read();
|
|
608
|
+
let priorActiveReleaseId = cohortState.references.active;
|
|
609
|
+
if (options.runner === undefined && (priorActiveReleaseId === null || cohortState.releases[priorActiveReleaseId]?.approval !== "approved")) {
|
|
610
|
+
// Invariant: protected replacement needs an immutable launch authority that npm cannot rename.
|
|
611
|
+
const recoveryRelease = await materializeRelease(packageRoot, paths.dataDir);
|
|
612
|
+
await cohortStore.recordCandidate(recoveryRelease);
|
|
613
|
+
const diagnostics = await certifyMaterializedRelease(recoveryRelease, paths.dataDir);
|
|
614
|
+
await cohortStore.approve(recoveryRelease.releaseId, diagnostics);
|
|
615
|
+
await cohortStore.activate(recoveryRelease.releaseId);
|
|
616
|
+
cohortState = await cohortStore.read();
|
|
617
|
+
priorActiveReleaseId = recoveryRelease.releaseId;
|
|
618
|
+
}
|
|
619
|
+
transaction = await transactionStore.begin({
|
|
620
|
+
channel,
|
|
621
|
+
targetVersion,
|
|
622
|
+
packageRoot,
|
|
623
|
+
priorActiveReleaseId,
|
|
565
624
|
});
|
|
566
|
-
transaction
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
625
|
+
if (phaseBefore(transaction.phase, "ownership-released")) {
|
|
626
|
+
await measure("ownership-release", async () => {
|
|
627
|
+
await lifecycle.shutdownVerifiedOwners(targetVersion);
|
|
628
|
+
await lifecycle.verifyPackageUnlocked(packageRoot);
|
|
629
|
+
});
|
|
630
|
+
transaction = await transactionStore.advance("ownership-released");
|
|
631
|
+
}
|
|
632
|
+
progress.set(15, 70);
|
|
633
|
+
if (phaseBefore(transaction.phase, "package-installed")) {
|
|
634
|
+
if (options.packageReplacement || options.runner === undefined) {
|
|
635
|
+
const priorReleaseId = transaction.priorActiveReleaseId ?? priorActiveReleaseId;
|
|
636
|
+
const recordedPrior = priorReleaseId === null ? undefined : cohortState.releases[priorReleaseId];
|
|
637
|
+
if ((!recordedPrior || recordedPrior.approval !== "approved") && !options.packageReplacement) {
|
|
638
|
+
throw new Error(PRODUCT_TEXT.diagnostic("cannot protect package replacement because no approved prior release is available"));
|
|
639
|
+
}
|
|
640
|
+
const priorRelease = recordedPrior ?? {
|
|
641
|
+
releaseId: priorReleaseId ?? "injected-prior-release",
|
|
642
|
+
releaseRoot: packageRoot,
|
|
643
|
+
contentDigest: "0".repeat(64),
|
|
644
|
+
};
|
|
645
|
+
const replacementTransaction = transaction;
|
|
646
|
+
const replacement = await measure("npm-install", async () => await (options.packageReplacement ?? runProtectedPackageReplacement)({
|
|
647
|
+
dataDir: paths.dataDir,
|
|
648
|
+
globalRoot,
|
|
649
|
+
npmCliRoot,
|
|
650
|
+
...(npmCli === undefined ? {} : { npmCli }),
|
|
651
|
+
packageRoot,
|
|
652
|
+
transaction: replacementTransaction,
|
|
653
|
+
priorRelease,
|
|
654
|
+
output,
|
|
655
|
+
environment,
|
|
656
|
+
onRecoveryState: async (recovery) => {
|
|
657
|
+
if (transactionStore.setRecovery)
|
|
658
|
+
transaction = await transactionStore.setRecovery(recovery) ?? transaction;
|
|
659
|
+
},
|
|
660
|
+
}));
|
|
661
|
+
// Rationale: npm's own text explains a failed replacement and says nothing about a
|
|
662
|
+
// successful one, so it reaches the terminal only when the installation did not happen.
|
|
663
|
+
if (replacement.outcome !== "installed")
|
|
664
|
+
reportChildDiagnostics(output, replacement);
|
|
665
|
+
if (replacement.outcome === "installed")
|
|
666
|
+
transaction = await transactionStore.advance("package-installed");
|
|
667
|
+
if (replacement.cancelled) {
|
|
668
|
+
progress.clear();
|
|
669
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic("update cancelled safely; the launcher is available and the transaction can be resumed.")}\n`);
|
|
670
|
+
return 130;
|
|
671
|
+
}
|
|
672
|
+
if (replacement.outcome !== "installed") {
|
|
673
|
+
throw new UpdateFailure(unsuccessfulCode(replacement.npmExitCode), `npm exited with status ${formatExitCode(replacement.npmExitCode)}`);
|
|
674
|
+
}
|
|
575
675
|
}
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
globalRoot,
|
|
585
|
-
npmCliRoot,
|
|
586
|
-
...(npmCli === undefined ? {} : { npmCli }),
|
|
587
|
-
packageRoot,
|
|
588
|
-
transaction: replacementTransaction,
|
|
589
|
-
priorRelease,
|
|
590
|
-
output,
|
|
591
|
-
environment,
|
|
592
|
-
onRecoveryState: async (recovery) => {
|
|
593
|
-
if (transactionStore.setRecovery)
|
|
594
|
-
transaction = await transactionStore.setRecovery(recovery) ?? transaction;
|
|
595
|
-
},
|
|
596
|
-
}));
|
|
597
|
-
// Rationale: npm's own text explains a failed replacement and says nothing about a
|
|
598
|
-
// successful one, so it reaches the terminal only when the installation did not happen.
|
|
599
|
-
if (replacement.outcome !== "installed")
|
|
600
|
-
reportChildDiagnostics(output, replacement);
|
|
601
|
-
if (replacement.outcome === "installed")
|
|
676
|
+
else {
|
|
677
|
+
const installation = await measure("npm-install", async () => await runNpm(runner, updateNpmInstallArguments(globalRoot, targetVersion), true, output, "start the global npm installation", false));
|
|
678
|
+
if (installation.result === null)
|
|
679
|
+
throw new UpdateFailure(installation.exitCode, "npm process failed");
|
|
680
|
+
if (installation.result.code !== 0) {
|
|
681
|
+
reportChildDiagnostics(output, installation.result);
|
|
682
|
+
throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
|
|
683
|
+
}
|
|
602
684
|
transaction = await transactionStore.advance("package-installed");
|
|
603
|
-
if (replacement.cancelled) {
|
|
604
|
-
progress.clear();
|
|
605
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic("update cancelled safely; the launcher is available and the transaction can be resumed.")}\n`);
|
|
606
|
-
return 130;
|
|
607
|
-
}
|
|
608
|
-
if (replacement.outcome !== "installed") {
|
|
609
|
-
throw new UpdateFailure(unsuccessfulCode(replacement.npmExitCode), `npm exited with status ${formatExitCode(replacement.npmExitCode)}`);
|
|
610
685
|
}
|
|
611
686
|
}
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
687
|
+
progress.set(70, 75);
|
|
688
|
+
// Concurrency: ownership can be reacquired after an interrupted installation (for
|
|
689
|
+
// example, if bare A1 is launched before the update is resumed). Recheck
|
|
690
|
+
// immediately before activation so recovery cannot start a second cohort.
|
|
691
|
+
await measure("ownership-release", async () => { await lifecycle.shutdownVerifiedOwners(targetVersion); });
|
|
692
|
+
progress.set(75, MATERIALIZE_PROGRESS.from);
|
|
693
|
+
let activationPhaseStartedAt = now();
|
|
694
|
+
await lifecycle.activateInstalled(packageRoot, targetVersion, async (phase) => {
|
|
695
|
+
options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
696
|
+
transaction = await transactionStore.advance(phase);
|
|
697
|
+
progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
|
|
698
|
+
activationPhaseStartedAt = now();
|
|
699
|
+
}, ({ completed, total }) => {
|
|
700
|
+
const span = MATERIALIZE_PROGRESS.to - MATERIALIZE_PROGRESS.from;
|
|
701
|
+
const done = total > 0 ? Math.min(1, completed / total) : 0;
|
|
702
|
+
progress.set(MATERIALIZE_PROGRESS.from + span * done);
|
|
703
|
+
}, state => {
|
|
704
|
+
if (state === "started") {
|
|
705
|
+
activationPhaseStartedAt = now();
|
|
706
|
+
progress.set(98, 99);
|
|
619
707
|
}
|
|
620
|
-
|
|
621
|
-
|
|
708
|
+
else {
|
|
709
|
+
options.onPhaseTiming?.({ phase: "warmup", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
710
|
+
activationPhaseStartedAt = now();
|
|
711
|
+
progress.set(99);
|
|
712
|
+
}
|
|
713
|
+
});
|
|
714
|
+
options.onPhaseTiming?.({ phase: "supervisor-verified", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
715
|
+
const transactionStartedAt = now();
|
|
716
|
+
await transactionStore.advance("supervisor-verified");
|
|
717
|
+
if (transaction.recovery?.capsulePath)
|
|
718
|
+
await removeUpdateRecoveryCapsule(paths.dataDir, transaction.transactionId);
|
|
719
|
+
// Invariant: successful output follows the durable cleanup disposition. Slow recursive
|
|
720
|
+
// removal belongs to the detached worker started by this maintenance coordinator.
|
|
721
|
+
await maintenance();
|
|
722
|
+
// Concurrency: this is the launch cutover. No failure-prone update work follows success.
|
|
723
|
+
await transactionStore.finish("completed");
|
|
724
|
+
await transactionStore.clearCompleted().catch(() => undefined);
|
|
725
|
+
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
726
|
+
progress.finish();
|
|
727
|
+
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully to ${targetVersion}\n`);
|
|
728
|
+
return 0;
|
|
622
729
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
options.onPhaseTiming?.({ phase, durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
632
|
-
transaction = await transactionStore.advance(phase);
|
|
633
|
-
progress.set(ACTIVATION_PROGRESS[phase].at, ACTIVATION_PROGRESS[phase].creepTo);
|
|
634
|
-
activationPhaseStartedAt = now();
|
|
635
|
-
}, ({ completed, total }) => {
|
|
636
|
-
const span = MATERIALIZE_PROGRESS.to - MATERIALIZE_PROGRESS.from;
|
|
637
|
-
const done = total > 0 ? Math.min(1, completed / total) : 0;
|
|
638
|
-
progress.set(MATERIALIZE_PROGRESS.from + span * done);
|
|
639
|
-
}, state => {
|
|
640
|
-
if (state === "started") {
|
|
641
|
-
activationPhaseStartedAt = now();
|
|
642
|
-
progress.set(98, 99);
|
|
643
|
-
}
|
|
644
|
-
else {
|
|
645
|
-
options.onPhaseTiming?.({ phase: "warmup", durationMs: Math.max(0, now() - activationPhaseStartedAt) });
|
|
646
|
-
activationPhaseStartedAt = now();
|
|
647
|
-
progress.set(99);
|
|
730
|
+
catch (error) {
|
|
731
|
+
progress.clear();
|
|
732
|
+
const message = errorMessage(error);
|
|
733
|
+
if (error instanceof UpdateAutomaticDeferred)
|
|
734
|
+
return 1;
|
|
735
|
+
if (error instanceof UpdatePreflightFailure) {
|
|
736
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`update could not start safely: ${message}. Nothing was changed. Retry with npm x -y -- ${PRODUCT_PACKAGE}-install`)}\n`);
|
|
737
|
+
return 1;
|
|
648
738
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
// removal belongs to the detached worker started by this maintenance coordinator.
|
|
657
|
-
await maintenance();
|
|
658
|
-
// Concurrency: this is the launch cutover. No failure-prone update work follows success.
|
|
659
|
-
await transactionStore.finish("completed");
|
|
660
|
-
await transactionStore.clearCompleted().catch(() => undefined);
|
|
661
|
-
options.onPhaseTiming?.({ phase: "transaction-complete", durationMs: Math.max(0, now() - transactionStartedAt) });
|
|
662
|
-
progress.finish();
|
|
663
|
-
output.stdout(`${PRODUCT_TEXT.commandName} updated successfully to ${targetVersion}\n`);
|
|
664
|
-
return 0;
|
|
665
|
-
}
|
|
666
|
-
catch (error) {
|
|
667
|
-
progress.clear();
|
|
668
|
-
const message = errorMessage(error);
|
|
669
|
-
if (error instanceof UpdatePreflightFailure) {
|
|
670
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic(`update could not start safely: ${message}. Nothing was changed. Retry with npm x -y -- ${PRODUCT_PACKAGE}-install`)}\n`);
|
|
671
|
-
return 1;
|
|
739
|
+
const rollback = options.lifecycle
|
|
740
|
+
? "previous test lifecycle retained"
|
|
741
|
+
: await rollbackPriorCohort(paths.dataDir, environment, transaction?.priorActiveReleaseId ?? null).catch(rollbackError => `rollback failed: ${errorMessage(rollbackError)}`);
|
|
742
|
+
if (transaction)
|
|
743
|
+
await transactionStore.finish(rollback === "rolled back" ? "rolled-back" : "failed", `${message}; ${rollback}`);
|
|
744
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`update failed: ${message}. ${rollback}. Diagnostics: ${transactionStore.path}`)}\n`);
|
|
745
|
+
return error instanceof UpdateFailure ? error.exitCode : 1;
|
|
672
746
|
}
|
|
673
|
-
|
|
674
|
-
? "previous test lifecycle retained"
|
|
675
|
-
: await rollbackPriorCohort(paths.dataDir, environment, transaction?.priorActiveReleaseId ?? null).catch(rollbackError => `rollback failed: ${errorMessage(rollbackError)}`);
|
|
676
|
-
if (transaction)
|
|
677
|
-
await transactionStore.finish(rollback === "rolled back" ? "rolled-back" : "failed", `${message}; ${rollback}`);
|
|
678
|
-
output.stderr(`${PRODUCT_TEXT.diagnostic(`update failed: ${message}. ${rollback}. Diagnostics: ${transactionStore.path}`)}\n`);
|
|
679
|
-
return error instanceof UpdateFailure ? error.exitCode : 1;
|
|
680
|
-
}
|
|
747
|
+
});
|
|
681
748
|
}
|
|
682
749
|
export function assertUpdatePerformanceBudget(evidence, maximumPostNpmDurationMs = 30_000) {
|
|
683
750
|
const failures = [];
|
|
@@ -777,6 +844,8 @@ class UpdateFailure extends Error {
|
|
|
777
844
|
}
|
|
778
845
|
class UpdatePreflightFailure extends Error {
|
|
779
846
|
}
|
|
847
|
+
class UpdateAutomaticDeferred extends Error {
|
|
848
|
+
}
|
|
780
849
|
async function canonicalImmutableRoot(dataDir, releaseRoot) {
|
|
781
850
|
try {
|
|
782
851
|
const [store, selected] = await Promise.all([realpath(resolve(dataDir, "releases")), realpath(releaseRoot)]);
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { type ResidentTabRoute } from "./routes.js";
|
|
1
2
|
export type ResidentTabStatus = "starting" | "working" | "needs-input" | "done-unseen" | "done" | "aborted" | "error" | "idle" | "unresponsive" | "crashed" | "restarting" | "failed";
|
|
3
|
+
export { residentUtilityRoute, type ResidentTabRoute } from "./routes.js";
|
|
2
4
|
export interface TabBridgeCredentials {
|
|
3
5
|
readonly tabId: string;
|
|
4
6
|
readonly token: string;
|
|
5
7
|
readonly endpoint: string;
|
|
8
|
+
readonly releaseId?: string;
|
|
6
9
|
}
|
|
7
10
|
export interface TabAttachment {
|
|
8
11
|
readonly clientId: string;
|
|
@@ -17,7 +20,7 @@ export type TabBridgeCommand = {
|
|
|
17
20
|
readonly nameSource: "default" | "user" | "auto";
|
|
18
21
|
} | {
|
|
19
22
|
readonly type: "route";
|
|
20
|
-
readonly route:
|
|
23
|
+
readonly route: ResidentTabRoute;
|
|
21
24
|
} | {
|
|
22
25
|
readonly type: "stop";
|
|
23
26
|
readonly deadlineMs: number;
|
|
@@ -59,5 +62,9 @@ export declare class TabBridgeClient {
|
|
|
59
62
|
requestClose(origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
60
63
|
consumeControllerBoundaryInput(data: string): string;
|
|
61
64
|
close(): void;
|
|
65
|
+
/**
|
|
66
|
+
* Asks the server to act for the client that controls this tab, as `origin`: leave it, restart or close the tab,
|
|
67
|
+
* or open the tab bar's Plus Menu dialog (`configure-plus`).
|
|
68
|
+
*/
|
|
69
|
+
requestAction(action: "detach" | "restart" | "close" | "configure-plus", origin?: TabAttachment | null, timeoutMs?: number): Promise<boolean>;
|
|
62
70
|
}
|
|
63
|
-
export {};
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { connect } from "node:net";
|
|
2
2
|
import { encodeResidentFrame, ResidentFrameDecoder } from "./protocol.js";
|
|
3
|
+
import { RESIDENT_TAB_ROUTES } from "./routes.js";
|
|
4
|
+
export { residentUtilityRoute } from "./routes.js";
|
|
5
|
+
const RESIDENT_TAB_ROUTE_SET = new Set(RESIDENT_TAB_ROUTES);
|
|
3
6
|
/** Metadata-only advisory bridge. Terminal input and transcript bytes have no API here. */
|
|
4
7
|
export class TabBridgeClient {
|
|
5
8
|
#options;
|
|
@@ -67,7 +70,7 @@ export class TabBridgeClient {
|
|
|
67
70
|
this.#startWaiter = finish;
|
|
68
71
|
transport.once("connect", () => {
|
|
69
72
|
this.#send("hello", {
|
|
70
|
-
role: "bridge", generation: 1, build: this.#options.build ?? "a1",
|
|
73
|
+
role: "bridge", generation: 1, build: this.#options.build ?? "a1", releaseId: this.#options.credentials.releaseId,
|
|
71
74
|
features: ["bridge-status", "bridge-detach", "bridge-heartbeat", "bridge-restart", "bridge-visibility", "bridge-rename", "bridge-route", "bridge-stop", "bridge-shortcuts", "controller-boundary", "recovery-journal"],
|
|
72
75
|
token: [...Buffer.from(this.#options.credentials.token, "base64")], tabId: this.#options.credentials.tabId,
|
|
73
76
|
pid: this.#options.pid ?? process.pid, startIdentity: this.#options.startIdentity ?? `${process.pid}:node`,
|
|
@@ -118,13 +121,13 @@ export class TabBridgeClient {
|
|
|
118
121
|
this.#send("bridge.shortcuts", { assignments: this.#latestShortcuts.map(([action, key]) => [action, key]) });
|
|
119
122
|
}
|
|
120
123
|
requestDetach(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
121
|
-
return this
|
|
124
|
+
return this.requestAction("detach", origin, timeoutMs);
|
|
122
125
|
}
|
|
123
126
|
requestRestart(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
124
|
-
return this
|
|
127
|
+
return this.requestAction("restart", origin, timeoutMs);
|
|
125
128
|
}
|
|
126
129
|
requestClose(origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
127
|
-
return this
|
|
130
|
+
return this.requestAction("close", origin, timeoutMs);
|
|
128
131
|
}
|
|
129
132
|
consumeControllerBoundaryInput(data) {
|
|
130
133
|
const prefix = "\x1bP+a1;controller=";
|
|
@@ -205,6 +208,11 @@ export class TabBridgeClient {
|
|
|
205
208
|
}
|
|
206
209
|
#receive(message) {
|
|
207
210
|
if (message.type === "welcome" && message.body["generation"] === 1) {
|
|
211
|
+
const id = this.#options.credentials.releaseId;
|
|
212
|
+
if (id !== undefined && message.body["releaseId"] !== id) {
|
|
213
|
+
this.#fail(false);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
208
216
|
const advertised = message.body["features"];
|
|
209
217
|
this.#features = new Set(Array.isArray(advertised) ? advertised.filter((feature) => typeof feature === "string") : []);
|
|
210
218
|
this.#connected = true;
|
|
@@ -232,8 +240,8 @@ export class TabBridgeClient {
|
|
|
232
240
|
this.#emitCommand({ type: "rename", name: message.body["name"], nameSource: source === "auto" || source === "default" ? source : "user" });
|
|
233
241
|
return;
|
|
234
242
|
}
|
|
235
|
-
if (message.type === "bridge.route" && message.body["route"]
|
|
236
|
-
this.#emitCommand({ type: "route", route: "
|
|
243
|
+
if (message.type === "bridge.route" && RESIDENT_TAB_ROUTE_SET.has(message.body["route"])) {
|
|
244
|
+
this.#emitCommand({ type: "route", route: message.body["route"] });
|
|
237
245
|
return;
|
|
238
246
|
}
|
|
239
247
|
if (message.type === "bridge.stop" && typeof message.body["deadlineMs"] === "number") {
|
|
@@ -255,7 +263,11 @@ export class TabBridgeClient {
|
|
|
255
263
|
}
|
|
256
264
|
}
|
|
257
265
|
}
|
|
258
|
-
|
|
266
|
+
/**
|
|
267
|
+
* Asks the server to act for the client that controls this tab, as `origin`: leave it, restart or close the tab,
|
|
268
|
+
* or open the tab bar's Plus Menu dialog (`configure-plus`).
|
|
269
|
+
*/
|
|
270
|
+
async requestAction(action, origin = this.captureAttachment(), timeoutMs = 1_000) {
|
|
259
271
|
if (!this.available || origin === null || (action === "restart" && !this.#supports("bridge-restart")))
|
|
260
272
|
return false;
|
|
261
273
|
const requestId = ++this.#request;
|