@timurproko/a1 0.1.8-dev.508 → 0.1.8-dev.512
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/bin/activate.js +8 -0
- package/bin/sync-pi-tui-proxy.js +7 -0
- package/dist/foundation/release/index.d.ts +2 -0
- package/dist/foundation/release/index.js +1 -0
- package/dist/foundation/release/update-activation.d.ts +72 -0
- package/dist/foundation/release/update-activation.js +204 -0
- package/dist/foundation/release/update.d.ts +5 -10
- package/dist/foundation/release/update.js +39 -38
- package/dist/native/darwin-arm64/manifest.json +1 -1
- package/dist/native/linux-x64/manifest.json +1 -1
- package/dist/native/win32-x64/manifest.json +2 -2
- package/dist/native/win32-x64/process-guardian.exe +0 -0
- package/dist/runtime-payload-inventory.json +1 -0
- package/docs/architecture/internal-naming.md +2 -0
- package/package.json +4 -1
package/bin/activate.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Protocol: the updater that installed this tree starts this entry with --data-dir and
|
|
4
|
+
// --target-version and reads one JSON progress event per line. The tree activates itself
|
|
5
|
+
// with its own release code, so the updater needs to know nothing else about its layout.
|
|
6
|
+
const { runActivationEntry } = await import("../dist/foundation/release/update-activation.js");
|
|
7
|
+
|
|
8
|
+
process.exitCode = await runActivationEntry(process.argv.slice(2), import.meta.url);
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// Compatibility: updaters older than 0.1.8-dev.479 run this entry from the tree they just
|
|
4
|
+
// installed to point a terminal-package proxy that no longer exists at that tree. Those
|
|
5
|
+
// updaters treat a missing file as a failure worth a stack trace on the user's terminal,
|
|
6
|
+
// so the entry stays, does nothing, and exits successfully. Newer updaters never run it.
|
|
7
|
+
process.exitCode = 0;
|
|
@@ -22,6 +22,8 @@ export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistr
|
|
|
22
22
|
export type { StableRegistryState, StableRegistryVerificationOptions, StableReleaseEvidence, StableReleaseEvidenceInput } from "./stable-release.js";
|
|
23
23
|
export { PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
|
|
24
24
|
export type { ProcessRequest, ProcessResult, SelfUpdateOptions, UpdateActivationPhase, UpdateChannel, UpdateFileSystem, UpdateLifecycleCoordinator, UpdateMaterializationProgress, UpdateMeasuredPhase, UpdateOutput, UpdateOwnershipAction, UpdatePackageReplacementInput, UpdatePerformanceEvidence, UpdatePhaseTimingEvent, UpdateProcessRunner, UpdateTransactionJournal, } from "./update.js";
|
|
25
|
+
export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
|
|
26
|
+
export type { UpdateActivationCallbacks, UpdateActivationEvent, UpdateActivationRequest } from "./update-activation.js";
|
|
25
27
|
export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
|
|
26
28
|
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, } from "./update-recovery.js";
|
|
27
29
|
export type { ProtectedPackageReplacementOptions, ProtectedPackageReplacementResult, UpdateRecoveryCapsule, UpdateRecoveryResult } from "./update-recovery.js";
|
|
@@ -10,6 +10,7 @@ export { RELEASE_MANIFEST_FILENAME, assertImmutableExecutionRoot, consumeMateria
|
|
|
10
10
|
export { createRestartSeal, readRestartCertifiedRelease, releaseCertificationDocument, restartSealDigest } from "./restart-certification.js";
|
|
11
11
|
export { STABLE_RELEASE_SCHEMA, createStableReleaseEvidence, verifyStableRegistry } from "./stable-release.js";
|
|
12
12
|
export { PRODUCT_PACKAGE, assertUpdatePerformanceBudget, createNpmProcessRunner, createUpdateLifecycleCoordinator, planUpdateOwnership, renderUpdateProgressBar, runSelfUpdate, } from "./update.js";
|
|
13
|
+
export { UPDATE_ACTIVATION_CONTRACT, UPDATE_ACTIVATION_ENTRY, UPDATE_ACTIVATION_MANIFEST_FIELD, activateInstalledRelease, delegateActivation, readActivationContracts, runActivationEntry, } from "./update-activation.js";
|
|
13
14
|
export { selectSupervisorLaunchReleaseId, selectUpdateLaunchRelease } from "./update-launch.js";
|
|
14
15
|
export { UPDATE_RECOVERY_SCHEMA, cleanupUpdateRecoveryCapsules, inspectUpdateLauncherSet, prepareUpdateRecoveryCapsule, readUpdateRecoveryCapsule, removeUpdateRecoveryCapsule, runProtectedPackageReplacement, updateLauncherPaths, } from "./update-recovery.js";
|
|
15
16
|
export { UPDATE_JOURNAL_SCHEMA, UpdateTransactionStore } from "./update-transaction.js";
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one activation contract an installed tree can offer the updater that installed it.
|
|
3
|
+
* The updater knows nothing else about the tree: the manifest names the contracts the tree
|
|
4
|
+
* serves, and this entry drives its activation with the tree's own release code.
|
|
5
|
+
*/
|
|
6
|
+
export declare const UPDATE_ACTIVATION_CONTRACT = "activate-v1";
|
|
7
|
+
export declare const UPDATE_ACTIVATION_ENTRY = "bin/activate.js";
|
|
8
|
+
/** Manifest field listing the activation contracts an installed tree serves. */
|
|
9
|
+
export declare const UPDATE_ACTIVATION_MANIFEST_FIELD = "updateActivationContracts";
|
|
10
|
+
export type UpdateActivationPhase = "materialized" | "certified" | "active-reference-committed";
|
|
11
|
+
/**
|
|
12
|
+
* Copying the release is the longest step with nothing to say for itself, so it
|
|
13
|
+
* reports the files it has written against the files it must write. A caller that
|
|
14
|
+
* shows progress can then move with the work instead of guessing at it.
|
|
15
|
+
*/
|
|
16
|
+
export interface UpdateMaterializationProgress {
|
|
17
|
+
readonly completed: number;
|
|
18
|
+
readonly total: number;
|
|
19
|
+
}
|
|
20
|
+
export interface UpdateActivationCallbacks {
|
|
21
|
+
readonly phase: (phase: UpdateActivationPhase) => Promise<void>;
|
|
22
|
+
readonly onMaterializing?: (progress: UpdateMaterializationProgress) => void;
|
|
23
|
+
readonly onWarmup?: (state: "started" | "completed") => void;
|
|
24
|
+
}
|
|
25
|
+
export interface UpdateActivationRequest {
|
|
26
|
+
readonly packageRoot: string;
|
|
27
|
+
readonly dataDir: string;
|
|
28
|
+
readonly targetVersion: string;
|
|
29
|
+
readonly environment: NodeJS.ProcessEnv;
|
|
30
|
+
}
|
|
31
|
+
/** One line of the activator's progress stream; `failed` carries the reason and ends it. */
|
|
32
|
+
export type UpdateActivationEvent = {
|
|
33
|
+
readonly event: "materializing";
|
|
34
|
+
readonly completed: number;
|
|
35
|
+
readonly total: number;
|
|
36
|
+
} | {
|
|
37
|
+
readonly event: "phase";
|
|
38
|
+
readonly phase: UpdateActivationPhase;
|
|
39
|
+
} | {
|
|
40
|
+
readonly event: "warmup";
|
|
41
|
+
readonly state: "started" | "completed";
|
|
42
|
+
} | {
|
|
43
|
+
readonly event: "completed";
|
|
44
|
+
} | {
|
|
45
|
+
readonly event: "failed";
|
|
46
|
+
readonly message: string;
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Activate the installed tree at `packageRoot` with this process's own release code:
|
|
50
|
+
* materialize it into the immutable store, certify, warm, verify supervision, and only
|
|
51
|
+
* then move the active reference. This is what the installed tree runs for itself through
|
|
52
|
+
* its activation entry, and what an updater runs in-process for a tree older than the contract.
|
|
53
|
+
*/
|
|
54
|
+
export declare function activateInstalledRelease(request: UpdateActivationRequest, callbacks: UpdateActivationCallbacks): Promise<void>;
|
|
55
|
+
/**
|
|
56
|
+
* Which activation contracts the installed tree serves, read from its manifest. A tree
|
|
57
|
+
* without the field, or without a readable manifest, serves none and is activated in-process.
|
|
58
|
+
*/
|
|
59
|
+
export declare function readActivationContracts(packageRoot: string, read?: (path: string) => Promise<string>): Promise<readonly string[]>;
|
|
60
|
+
/**
|
|
61
|
+
* Drive the installed tree's own activator and relay its progress. The updater passes
|
|
62
|
+
* the tree what the tree cannot know (the data directory, the target it must match) and
|
|
63
|
+
* learns from it only the phases it reached; every path inside the tree is the tree's business.
|
|
64
|
+
*/
|
|
65
|
+
export declare function delegateActivation(request: UpdateActivationRequest, callbacks: UpdateActivationCallbacks): Promise<void>;
|
|
66
|
+
/**
|
|
67
|
+
* Body of the installed tree's activation entry: activate the tree this code ships in,
|
|
68
|
+
* writing one progress event per line so the updater that started it can follow along.
|
|
69
|
+
*/
|
|
70
|
+
export declare function runActivationEntry(argv: readonly string[], entryUrl: string, io?: {
|
|
71
|
+
write(line: string): void;
|
|
72
|
+
}, environment?: NodeJS.ProcessEnv): Promise<number>;
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { readFile } from "node:fs/promises";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { PRODUCT_TEXT } from "../../product-identity.js";
|
|
6
|
+
import { certifyMaterializedRelease, ensureSupervisor } from "./bootstrap.js";
|
|
7
|
+
import { CohortStateStore } from "./cohort-state.js";
|
|
8
|
+
import { encodeFrame, LineFrameDecoder } from "../protocol/index.js";
|
|
9
|
+
import { resolveProductPaths } from "../lifecycle/index.js";
|
|
10
|
+
import { materializeRelease } from "./release-store.js";
|
|
11
|
+
import { warmMaterializedRelease } from "./warmup.js";
|
|
12
|
+
/**
|
|
13
|
+
* The one activation contract an installed tree can offer the updater that installed it.
|
|
14
|
+
* The updater knows nothing else about the tree: the manifest names the contracts the tree
|
|
15
|
+
* serves, and this entry drives its activation with the tree's own release code.
|
|
16
|
+
*/
|
|
17
|
+
export const UPDATE_ACTIVATION_CONTRACT = "activate-v1";
|
|
18
|
+
export const UPDATE_ACTIVATION_ENTRY = "bin/activate.js";
|
|
19
|
+
/** Manifest field listing the activation contracts an installed tree serves. */
|
|
20
|
+
export const UPDATE_ACTIVATION_MANIFEST_FIELD = "updateActivationContracts";
|
|
21
|
+
const ACTIVATION_DIAGNOSTIC_LIMIT = 2_000;
|
|
22
|
+
/**
|
|
23
|
+
* Activate the installed tree at `packageRoot` with this process's own release code:
|
|
24
|
+
* materialize it into the immutable store, certify, warm, verify supervision, and only
|
|
25
|
+
* then move the active reference. This is what the installed tree runs for itself through
|
|
26
|
+
* its activation entry, and what an updater runs in-process for a tree older than the contract.
|
|
27
|
+
*/
|
|
28
|
+
export async function activateInstalledRelease(request, callbacks) {
|
|
29
|
+
const stateStore = new CohortStateStore(request.dataDir);
|
|
30
|
+
let total = 0;
|
|
31
|
+
let completed = 0;
|
|
32
|
+
const candidate = await materializeRelease(request.packageRoot, request.dataDir, {
|
|
33
|
+
onProgress: event => {
|
|
34
|
+
total = event.fileCount;
|
|
35
|
+
callbacks.onMaterializing?.({ completed, total });
|
|
36
|
+
},
|
|
37
|
+
onOperation: event => {
|
|
38
|
+
if (event.operation !== "candidate-write" && event.operation !== "layer-write")
|
|
39
|
+
return;
|
|
40
|
+
completed += 1;
|
|
41
|
+
callbacks.onMaterializing?.({ completed, total });
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
if (candidate.packageVersion !== request.targetVersion)
|
|
45
|
+
throw new Error(`installed ${PRODUCT_TEXT.displayName} version ${candidate.packageVersion} does not match target ${request.targetVersion}`);
|
|
46
|
+
await stateStore.recordCandidate(candidate);
|
|
47
|
+
await callbacks.phase("materialized");
|
|
48
|
+
const diagnostics = await certifyMaterializedRelease(candidate, request.dataDir);
|
|
49
|
+
await stateStore.approve(candidate.releaseId, diagnostics);
|
|
50
|
+
await callbacks.phase("certified");
|
|
51
|
+
callbacks.onWarmup?.("started");
|
|
52
|
+
await warmMaterializedRelease(candidate, request.environment);
|
|
53
|
+
callbacks.onWarmup?.("completed");
|
|
54
|
+
await ensureSupervisor(candidate, request.environment);
|
|
55
|
+
// Invariant: warmup and authenticated readiness precede changing the active reference.
|
|
56
|
+
await stateStore.activate(candidate.releaseId);
|
|
57
|
+
await callbacks.phase("active-reference-committed");
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Which activation contracts the installed tree serves, read from its manifest. A tree
|
|
61
|
+
* without the field, or without a readable manifest, serves none and is activated in-process.
|
|
62
|
+
*/
|
|
63
|
+
export async function readActivationContracts(packageRoot, read = path => readFile(path, "utf8")) {
|
|
64
|
+
try {
|
|
65
|
+
const manifest = JSON.parse(await read(resolve(packageRoot, "package.json")));
|
|
66
|
+
const declared = manifest[UPDATE_ACTIVATION_MANIFEST_FIELD];
|
|
67
|
+
return Array.isArray(declared) ? declared.filter((value) => typeof value === "string") : [];
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
return [];
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Drive the installed tree's own activator and relay its progress. The updater passes
|
|
75
|
+
* the tree what the tree cannot know (the data directory, the target it must match) and
|
|
76
|
+
* learns from it only the phases it reached; every path inside the tree is the tree's business.
|
|
77
|
+
*/
|
|
78
|
+
export async function delegateActivation(request, callbacks) {
|
|
79
|
+
const entry = resolve(request.packageRoot, UPDATE_ACTIVATION_ENTRY);
|
|
80
|
+
const child = spawn(process.execPath, [entry, "--data-dir", request.dataDir, "--target-version", request.targetVersion], {
|
|
81
|
+
env: request.environment,
|
|
82
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
83
|
+
windowsHide: true,
|
|
84
|
+
});
|
|
85
|
+
const decoder = new LineFrameDecoder();
|
|
86
|
+
let diagnostics = "";
|
|
87
|
+
let failure = null;
|
|
88
|
+
let completed = false;
|
|
89
|
+
// Concurrency: callbacks are awaited in order so a phase is journaled before the next one is read.
|
|
90
|
+
let relay = Promise.resolve();
|
|
91
|
+
child.stderr?.setEncoding("utf8");
|
|
92
|
+
child.stderr?.on("data", chunk => { diagnostics = `${diagnostics}${chunk}`.slice(-ACTIVATION_DIAGNOSTIC_LIMIT); });
|
|
93
|
+
child.stdout?.on("data", chunk => {
|
|
94
|
+
let events;
|
|
95
|
+
try {
|
|
96
|
+
events = decoder.push(chunk);
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
failure ??= errorMessage(error);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const event of events) {
|
|
103
|
+
relay = relay.then(async () => {
|
|
104
|
+
const parsed = parseActivationEvent(event);
|
|
105
|
+
if (!parsed) {
|
|
106
|
+
failure ??= `unrecognized activation event ${JSON.stringify(event).slice(0, 200)}`;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (parsed.event === "materializing")
|
|
110
|
+
callbacks.onMaterializing?.({ completed: parsed.completed, total: parsed.total });
|
|
111
|
+
else if (parsed.event === "phase")
|
|
112
|
+
await callbacks.phase(parsed.phase);
|
|
113
|
+
else if (parsed.event === "warmup")
|
|
114
|
+
callbacks.onWarmup?.(parsed.state);
|
|
115
|
+
else if (parsed.event === "completed")
|
|
116
|
+
completed = true;
|
|
117
|
+
else
|
|
118
|
+
failure ??= parsed.message;
|
|
119
|
+
}).catch(error => { failure ??= errorMessage(error); });
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
const exit = await new Promise((resolvePromise, rejectPromise) => {
|
|
123
|
+
child.once("error", rejectPromise);
|
|
124
|
+
child.once("close", (code, signal) => resolvePromise({ code, signal }));
|
|
125
|
+
});
|
|
126
|
+
await relay;
|
|
127
|
+
if (failure)
|
|
128
|
+
throw new Error(failure);
|
|
129
|
+
if (exit.code !== 0 || !completed) {
|
|
130
|
+
const status = exit.code === null ? exit.signal ?? "unknown status" : `status ${exit.code}`;
|
|
131
|
+
throw new Error(`installed release activation exited with ${status}${summarize(diagnostics)}`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Body of the installed tree's activation entry: activate the tree this code ships in,
|
|
136
|
+
* writing one progress event per line so the updater that started it can follow along.
|
|
137
|
+
*/
|
|
138
|
+
export async function runActivationEntry(argv, entryUrl, io = { write: line => process.stdout.write(line) }, environment = process.env) {
|
|
139
|
+
const emit = (event) => io.write(encodeFrame(event));
|
|
140
|
+
try {
|
|
141
|
+
const arguments_ = parseEntryArguments(argv);
|
|
142
|
+
const packageRoot = resolve(fileURLToPath(new URL("..", entryUrl)));
|
|
143
|
+
await activateInstalledRelease({
|
|
144
|
+
packageRoot,
|
|
145
|
+
dataDir: arguments_.dataDir ?? resolveProductPaths(environment).dataDir,
|
|
146
|
+
targetVersion: arguments_.targetVersion,
|
|
147
|
+
environment,
|
|
148
|
+
}, {
|
|
149
|
+
phase: async (phase) => emit({ event: "phase", phase }),
|
|
150
|
+
onMaterializing: progress => emit({ event: "materializing", ...progress }),
|
|
151
|
+
onWarmup: state => emit({ event: "warmup", state }),
|
|
152
|
+
});
|
|
153
|
+
emit({ event: "completed" });
|
|
154
|
+
return 0;
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
emit({ event: "failed", message: errorMessage(error) });
|
|
158
|
+
return 1;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function parseEntryArguments(argv) {
|
|
162
|
+
let dataDir = null;
|
|
163
|
+
let targetVersion = null;
|
|
164
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
165
|
+
const value = argv[index + 1];
|
|
166
|
+
if (argv[index] === "--data-dir" && value !== undefined) {
|
|
167
|
+
dataDir = value;
|
|
168
|
+
index += 1;
|
|
169
|
+
}
|
|
170
|
+
else if (argv[index] === "--target-version" && value !== undefined) {
|
|
171
|
+
targetVersion = value;
|
|
172
|
+
index += 1;
|
|
173
|
+
}
|
|
174
|
+
else
|
|
175
|
+
throw new Error(`unexpected activation argument ${argv[index]}`);
|
|
176
|
+
}
|
|
177
|
+
if (targetVersion === null)
|
|
178
|
+
throw new Error("activation requires --target-version");
|
|
179
|
+
return { dataDir, targetVersion };
|
|
180
|
+
}
|
|
181
|
+
function parseActivationEvent(value) {
|
|
182
|
+
if (typeof value !== "object" || value === null || !("event" in value))
|
|
183
|
+
return null;
|
|
184
|
+
const record = value;
|
|
185
|
+
switch (record.event) {
|
|
186
|
+
case "materializing":
|
|
187
|
+
return typeof record.completed === "number" && typeof record.total === "number" ? { event: "materializing", completed: record.completed, total: record.total } : null;
|
|
188
|
+
case "phase":
|
|
189
|
+
return record.phase === "materialized" || record.phase === "certified" || record.phase === "active-reference-committed" ? { event: "phase", phase: record.phase } : null;
|
|
190
|
+
case "warmup":
|
|
191
|
+
return record.state === "started" || record.state === "completed" ? { event: "warmup", state: record.state } : null;
|
|
192
|
+
case "completed":
|
|
193
|
+
return { event: "completed" };
|
|
194
|
+
case "failed":
|
|
195
|
+
return typeof record.message === "string" ? { event: "failed", message: record.message } : null;
|
|
196
|
+
default:
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
function summarize(text) {
|
|
201
|
+
const lines = text.split(/\r?\n/).map(line => line.trim()).filter(line => line.length > 0);
|
|
202
|
+
return lines.length === 0 ? "" : `: ${lines.slice(0, 4).join(" | ")}`;
|
|
203
|
+
}
|
|
204
|
+
function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { UpdateChannel } from "./types.js";
|
|
2
|
+
import { type UpdateActivationPhase, type UpdateMaterializationProgress } from "./update-activation.js";
|
|
2
3
|
import { type UpdateRecoveryState, type UpdateTransaction, type UpdateTransactionPhase } from "./update-transaction.js";
|
|
3
4
|
import { type ProtectedPackageReplacementResult } from "./update-recovery.js";
|
|
4
5
|
export declare const PRODUCT_PACKAGE: string;
|
|
@@ -9,6 +10,7 @@ export interface ProcessRequest {
|
|
|
9
10
|
export interface ProcessResult {
|
|
10
11
|
code: number | null;
|
|
11
12
|
stdout: string;
|
|
13
|
+
stderr?: string;
|
|
12
14
|
}
|
|
13
15
|
export type UpdateProcessRunner = (command: string, arguments_: readonly string[], request: ProcessRequest) => Promise<ProcessResult>;
|
|
14
16
|
export interface UpdateFileSystem {
|
|
@@ -57,16 +59,7 @@ export interface SelfUpdateOptions {
|
|
|
57
59
|
onPhaseTiming?: (event: UpdatePhaseTimingEvent) => void;
|
|
58
60
|
now?: () => number;
|
|
59
61
|
}
|
|
60
|
-
export type UpdateActivationPhase
|
|
61
|
-
/**
|
|
62
|
-
* Copying the release is the longest step with nothing to say for itself, so it
|
|
63
|
-
* reports the files it has written against the files it must write. A caller that
|
|
64
|
-
* shows progress can then move with the work instead of guessing at it.
|
|
65
|
-
*/
|
|
66
|
-
export interface UpdateMaterializationProgress {
|
|
67
|
-
readonly completed: number;
|
|
68
|
-
readonly total: number;
|
|
69
|
-
}
|
|
62
|
+
export type { UpdateActivationPhase, UpdateMaterializationProgress } from "./update-activation.js";
|
|
70
63
|
export interface UpdateLifecycleCoordinator {
|
|
71
64
|
targetIsActive(targetVersion: string): Promise<boolean>;
|
|
72
65
|
shutdownVerifiedOwners(targetVersion: string): Promise<{
|
|
@@ -103,6 +96,8 @@ export interface UpdatePackageReplacementInput {
|
|
|
103
96
|
readonly environment: NodeJS.ProcessEnv;
|
|
104
97
|
readonly onRecoveryState: (state: UpdateRecoveryState) => Promise<void>;
|
|
105
98
|
}
|
|
99
|
+
/** Bound on the child diagnostics an update keeps: enough for npm's failure reason, never a log. */
|
|
100
|
+
export declare const CHILD_DIAGNOSTIC_LIMIT = 8000;
|
|
106
101
|
export declare function createNpmProcessRunner(platform?: NodeJS.Platform): UpdateProcessRunner;
|
|
107
102
|
export type UpdateOwnershipAction = "clean-dead-record" | "leave-running" | "end-session";
|
|
108
103
|
/**
|
|
@@ -13,7 +13,7 @@ import { CohortStateStore } from "./cohort-state.js";
|
|
|
13
13
|
import { cleanupVerifiedOwner, processIsAlive } from "./process-cleanup.js";
|
|
14
14
|
import { materializeRelease, readMaterializedRelease } from "./release-store.js";
|
|
15
15
|
import { scheduleReleaseCleanup } from "./release-gc.js";
|
|
16
|
-
import {
|
|
16
|
+
import { UPDATE_ACTIVATION_CONTRACT, activateInstalledRelease, delegateActivation, readActivationContracts, } from "./update-activation.js";
|
|
17
17
|
import { UpdateTransactionStore } from "./update-transaction.js";
|
|
18
18
|
import { removeUpdateRecoveryCapsule, runProtectedPackageReplacement } from "./update-recovery.js";
|
|
19
19
|
export const PRODUCT_PACKAGE = PRODUCT_TEXT.packageName;
|
|
@@ -27,12 +27,19 @@ const defaultOutput = {
|
|
|
27
27
|
stdout(message) { process.stdout.write(message); },
|
|
28
28
|
stderr(message) { process.stderr.write(message); },
|
|
29
29
|
};
|
|
30
|
+
/** Bound on the child diagnostics an update keeps: enough for npm's failure reason, never a log. */
|
|
31
|
+
export const CHILD_DIAGNOSTIC_LIMIT = 8_000;
|
|
30
32
|
export function createNpmProcessRunner(platform = process.platform) {
|
|
31
33
|
return async (command, arguments_, request) => await new Promise((resolvePromise, rejectPromise) => {
|
|
32
|
-
|
|
34
|
+
// Invariant: no child of the updater shares the terminal. The progress bar is the only
|
|
35
|
+
// live output; a child's own text is kept, bounded, and shown with a failure or not at all.
|
|
36
|
+
const stdio = ["ignore", request.captureStdout ? "pipe" : "ignore", "pipe"];
|
|
33
37
|
const child = platform === "win32" ? crossSpawn(command, [...arguments_], { stdio }) : spawn(command, [...arguments_], { stdio });
|
|
34
38
|
const stdout = [];
|
|
39
|
+
let stderr = "";
|
|
35
40
|
child.stdout?.on("data", chunk => stdout.push(Buffer.from(chunk)));
|
|
41
|
+
child.stderr?.setEncoding("utf8");
|
|
42
|
+
child.stderr?.on("data", chunk => { stderr = `${stderr}${chunk}`.slice(-CHILD_DIAGNOSTIC_LIMIT); });
|
|
36
43
|
let settled = false;
|
|
37
44
|
child.once("error", error => { if (!settled) {
|
|
38
45
|
settled = true;
|
|
@@ -40,7 +47,7 @@ export function createNpmProcessRunner(platform = process.platform) {
|
|
|
40
47
|
} });
|
|
41
48
|
child.once("close", code => { if (!settled) {
|
|
42
49
|
settled = true;
|
|
43
|
-
resolvePromise({ code, stdout: Buffer.concat(stdout).toString("utf8") });
|
|
50
|
+
resolvePromise({ code, stdout: Buffer.concat(stdout).toString("utf8"), stderr });
|
|
44
51
|
} });
|
|
45
52
|
});
|
|
46
53
|
}
|
|
@@ -143,34 +150,20 @@ export function createUpdateLifecycleCoordinator(environment = process.env, file
|
|
|
143
150
|
}
|
|
144
151
|
},
|
|
145
152
|
async activateInstalled(packageRoot, targetVersion, phase, onMaterializing, onWarmup) {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
if (candidate.packageVersion !== targetVersion)
|
|
161
|
-
throw new Error(`installed ${PRODUCT_TEXT.displayName} version ${candidate.packageVersion} does not match target ${targetVersion}`);
|
|
162
|
-
await stateStore.recordCandidate(candidate);
|
|
163
|
-
await phase("materialized");
|
|
164
|
-
const diagnostics = await certifyMaterializedRelease(candidate, paths.dataDir);
|
|
165
|
-
await stateStore.approve(candidate.releaseId, diagnostics);
|
|
166
|
-
await phase("certified");
|
|
167
|
-
onWarmup?.("started");
|
|
168
|
-
await warmMaterializedRelease(candidate, environment);
|
|
169
|
-
onWarmup?.("completed");
|
|
170
|
-
await ensureSupervisor(candidate, environment);
|
|
171
|
-
// Invariant: warmup and authenticated readiness precede changing the active reference.
|
|
172
|
-
await stateStore.activate(candidate.releaseId);
|
|
173
|
-
await phase("active-reference-committed");
|
|
153
|
+
const request = { packageRoot, dataDir: paths.dataDir, targetVersion, environment };
|
|
154
|
+
const callbacks = {
|
|
155
|
+
phase,
|
|
156
|
+
...(onMaterializing ? { onMaterializing } : {}),
|
|
157
|
+
...(onWarmup ? { onWarmup } : {}),
|
|
158
|
+
};
|
|
159
|
+
// Compatibility: the tree npm just installed is newer than this updater, so its layout is
|
|
160
|
+
// its own to know. A tree that serves the contract activates itself with its own code;
|
|
161
|
+
// a tree from before the contract is activated here, with the layout it had then.
|
|
162
|
+
const contracts = await readActivationContracts(packageRoot, fileSystem.readFile);
|
|
163
|
+
if (contracts.includes(UPDATE_ACTIVATION_CONTRACT))
|
|
164
|
+
await delegateActivation(request, callbacks);
|
|
165
|
+
else
|
|
166
|
+
await activateInstalledRelease(request, callbacks);
|
|
174
167
|
},
|
|
175
168
|
};
|
|
176
169
|
}
|
|
@@ -444,10 +437,10 @@ export async function runSelfUpdate(options) {
|
|
|
444
437
|
transaction = await transactionStore.setRecovery(recovery) ?? transaction;
|
|
445
438
|
},
|
|
446
439
|
}));
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
if (replacement.
|
|
450
|
-
output
|
|
440
|
+
// Rationale: npm's own text explains a failed replacement and says nothing about a
|
|
441
|
+
// successful one, so it reaches the terminal only when the installation did not happen.
|
|
442
|
+
if (replacement.outcome !== "installed")
|
|
443
|
+
reportChildDiagnostics(output, replacement);
|
|
451
444
|
if (replacement.outcome === "installed")
|
|
452
445
|
transaction = await transactionStore.advance("package-installed");
|
|
453
446
|
if (replacement.cancelled) {
|
|
@@ -464,8 +457,7 @@ export async function runSelfUpdate(options) {
|
|
|
464
457
|
if (installation.result === null)
|
|
465
458
|
throw new UpdateFailure(installation.exitCode, "npm process failed");
|
|
466
459
|
if (installation.result.code !== 0) {
|
|
467
|
-
|
|
468
|
-
output.stderr(`${installation.result.stdout.trimEnd()}\n`);
|
|
460
|
+
reportChildDiagnostics(output, installation.result);
|
|
469
461
|
throw new UpdateFailure(unsuccessfulCode(installation.result.code), `npm exited with status ${formatExitCode(installation.result.code)}`);
|
|
470
462
|
}
|
|
471
463
|
transaction = await transactionStore.advance("package-installed");
|
|
@@ -581,11 +573,20 @@ async function runNpm(runner, arguments_, captureStdout, output, action, reportN
|
|
|
581
573
|
return { result: null, exitCode: 1 };
|
|
582
574
|
}
|
|
583
575
|
if (result.code !== 0 && reportNonzero) {
|
|
584
|
-
|
|
576
|
+
const explained = reportChildDiagnostics(output, result);
|
|
577
|
+
output.stderr(`${PRODUCT_TEXT.diagnostic(`could not ${action}; npm exited with status ${formatExitCode(result.code)}.${explained ? " Review npm's diagnostics above." : ""}`)}\n`);
|
|
585
578
|
return { result: null, exitCode: unsuccessfulCode(result.code) };
|
|
586
579
|
}
|
|
587
580
|
return { result, exitCode: 0 };
|
|
588
581
|
}
|
|
582
|
+
/** Prints what a failed child said, bounded, and reports whether there was anything to print. */
|
|
583
|
+
function reportChildDiagnostics(output, result) {
|
|
584
|
+
const text = [result.stdout, result.stderr ?? ""].map(part => part.trim()).filter(part => part.length > 0).join("\n").slice(-CHILD_DIAGNOSTIC_LIMIT);
|
|
585
|
+
if (text.length === 0)
|
|
586
|
+
return false;
|
|
587
|
+
output.stderr(`${text}\n`);
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
589
590
|
async function rollbackPriorCohort(dataDir, environment, priorReleaseId) {
|
|
590
591
|
if (!priorReleaseId)
|
|
591
592
|
return "no prior cohort was available for rollback";
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "darwin",
|
|
6
6
|
"architecture": "arm64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-19T13:30:30.983Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "9db1726bbe3fc2e8292f2ead1e7e9d0fd4d7d0dc9217b372582bf354b0f565dc",
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
"platform": "linux",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-19T13:30:39.829Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian",
|
|
11
11
|
"sha256": "d8cda6b0c7cb36c0cc41e90802aceebf6c02eaebbc08a83d7d963d2e918dbd7a",
|
|
@@ -5,10 +5,10 @@
|
|
|
5
5
|
"platform": "win32",
|
|
6
6
|
"architecture": "x64",
|
|
7
7
|
"capability": "supported",
|
|
8
|
-
"builtAt": "2026-09-
|
|
8
|
+
"builtAt": "2026-09-19T13:31:06.678Z",
|
|
9
9
|
"artifact": {
|
|
10
10
|
"filename": "process-guardian.exe",
|
|
11
|
-
"sha256": "
|
|
11
|
+
"sha256": "39cc0b1caeb6d009b118f40db5a66aa58d84e7f6b33443e97670bd3708a83a0e",
|
|
12
12
|
"size": 177664
|
|
13
13
|
},
|
|
14
14
|
"provenance": {
|
|
Binary file
|
|
@@ -77,6 +77,8 @@ An update is performed by the release that is already installed, so the contract
|
|
|
77
77
|
|
|
78
78
|
Metadata that predates contract declaration is stale, never fatal. A pre-cutover active record does not stop a launch: it is ignored, the installed payload is materialized, and ordinary cohort selection activates it. A launch that cannot take the installed payload at all — an installation being replaced right now — starts the retained active release instead of failing, and leaves activation to the next launch. What still requires `neutral-launch-v1` outright is narrow and never blocks a launch: the installed package's own manifest must declare it, and an update recovery capsule is never translated.
|
|
79
79
|
|
|
80
|
+
Activation after the global installation is handed over the same way. The package manifest declares `updateActivationContracts` (`activate-v1`) and ships `bin/activate.js`; an updater that serves a declared contract starts that entry with the data directory and the target version and relays its line-delimited progress events (`materializing`, `phase`, `warmup`, `completed`, `failed`) into its own journal and progress bar, while the entry materializes, certifies, warms, and supervises the tree it ships in with that tree's own code. The updater resolves nothing inside the installed tree but `package.json` and that entry; `test/repository-governance/update-activation-contract.test.ts` pins this. A tree that declares no contract the updater serves is activated in-process with the layout it had, which keeps an updater older than the contract and a downgrade to an older preview working.
|
|
81
|
+
|
|
80
82
|
`test/foundation/release/update-predecessor.integration.test.ts` is the gate for this. It installs each of the most recent published releases and drives the candidate through **that release's own** materialization and warmup, because a fixture built from the candidate would only prove the candidate agrees with itself. No reset or migration runs automatically. If disposable state blocks installation, inspect its resolved paths and obtain separate approval before removal.
|
|
81
83
|
|
|
82
84
|
| State | Treatment |
|
package/package.json
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@timurproko/a1",
|
|
3
|
-
"version": "0.1.8-dev.
|
|
3
|
+
"version": "0.1.8-dev.512",
|
|
4
4
|
"description": "Standalone terminal workspace for supervised native and managed agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"privateLaunchContract": "neutral-launch-v1",
|
|
7
|
+
"updateActivationContracts": [
|
|
8
|
+
"activate-v1"
|
|
9
|
+
],
|
|
7
10
|
"packageManager": "npm@11.13.0",
|
|
8
11
|
"bin": {
|
|
9
12
|
"a1": "bin/cli.js"
|