@zq-silk/yui 0.8.7 → 0.8.9
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/dist/cli.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { join, resolve } from "node:path";
|
|
4
5
|
import { createInterface } from "node:readline/promises";
|
|
@@ -48,7 +49,7 @@ import { runWorkflowCommandAsync } from "./commands/workflowCommands.js";
|
|
|
48
49
|
import { createUpdatePorts } from "./cli/updatePorts.js";
|
|
49
50
|
import { createReleaseWorkflowPorts } from "./release/releaseWorkflowPorts.js";
|
|
50
51
|
import { acquireHandoverLock, readRuntimeIdentity } from "./release/runtimeRelease.js";
|
|
51
|
-
import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
|
|
52
|
+
import { renderReleaseActivateResult, renderReleaseInstallResult, renderReleaseList, resolveReleaseActivationDriver, runReleaseActivate, runReleaseInstall, runReleaseList } from "./commands/releaseCommands.js";
|
|
52
53
|
import { reconcileTaskRemoteBaselines, verifyTaskCompletionPublishedTree } from "./commands/taskCompletionGate.js";
|
|
53
54
|
import { runTaskBaseStatusCommand } from "./commands/taskBaseCommands.js";
|
|
54
55
|
import { assertTaskBaseFreshnessForCompletion, inspectTaskBaseFreshness } from "./repository/taskBaseFreshness.js";
|
|
@@ -112,6 +113,22 @@ void main().catch((error) => {
|
|
|
112
113
|
process.exitCode = 5;
|
|
113
114
|
});
|
|
114
115
|
export async function main() {
|
|
116
|
+
const delegatedDriver = explicitReleaseActivationDriver();
|
|
117
|
+
if (delegatedDriver !== null) {
|
|
118
|
+
const delegated = spawnSync(process.execPath, [delegatedDriver, ...process.argv.slice(2)], {
|
|
119
|
+
cwd: process.cwd(),
|
|
120
|
+
env: process.env,
|
|
121
|
+
stdio: "inherit"
|
|
122
|
+
});
|
|
123
|
+
if (delegated.error !== undefined) {
|
|
124
|
+
throw runtimeError(`Target release activation driver could not start: ${delegated.error.message}`);
|
|
125
|
+
}
|
|
126
|
+
if (delegated.signal !== null) {
|
|
127
|
+
throw runtimeError(`Target release activation driver stopped by signal ${delegated.signal}.`);
|
|
128
|
+
}
|
|
129
|
+
process.exitCode = delegated.status ?? 5;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
115
132
|
const { contract: taskFinalReviewContract, verifiedStore, controlPlane: exactCurrentControlPlane } = await preflightManagedTaskControlPlane();
|
|
116
133
|
if (args.length === 0) {
|
|
117
134
|
emit(renderCommandHelp((await import("./cli/commandCatalog.js")).ROOT_COMMAND, VERSION));
|
|
@@ -1360,6 +1377,15 @@ export async function main() {
|
|
|
1360
1377
|
}
|
|
1361
1378
|
throw usageError(`Command is not connected to the restored FileTaskStore framework yet: ${resolved[0]}.`, renderCommandHelp(invocation.node, VERSION));
|
|
1362
1379
|
}
|
|
1380
|
+
function explicitReleaseActivationDriver() {
|
|
1381
|
+
if (args.length !== 3
|
|
1382
|
+
|| args[0] !== "release"
|
|
1383
|
+
|| args[1] !== "activate"
|
|
1384
|
+
|| args[2].startsWith("-")) {
|
|
1385
|
+
return null;
|
|
1386
|
+
}
|
|
1387
|
+
return resolveReleaseActivationDriver(resolveYuiHome(process.env), args[2], fileURLToPath(import.meta.url));
|
|
1388
|
+
}
|
|
1363
1389
|
async function preflightManagedTaskControlPlane() {
|
|
1364
1390
|
if (exactControlInvocation.error !== undefined) {
|
|
1365
1391
|
throw new Error(exactControlInvocation.error);
|
|
@@ -8,7 +8,7 @@ import { join, resolve } from "node:path";
|
|
|
8
8
|
import { callController, ControllerClientError } from "../core/controllerClient.js";
|
|
9
9
|
import { readLinuxProcessStartIdentity } from "../controller/domainIdentity.js";
|
|
10
10
|
import { activateRelease } from "../release/releaseHandover.js";
|
|
11
|
-
import { assertReleaseIsNotWorktreeOrLinked, readActiveReleasePointer, readReleaseManifest, releasesDirectory, releaseDirectoryFor, verifyReleaseIntegrity, writeSmokeReceipt } from "../release/runtimeRelease.js";
|
|
11
|
+
import { assertReleaseIsNotWorktreeOrLinked, detectRunningRelease, readActiveReleasePointer, readReleaseManifest, readSmokeReceipt, releasesDirectory, releaseDirectoryFor, verifyReleaseIntegrity, writeSmokeReceipt } from "../release/runtimeRelease.js";
|
|
12
12
|
/**
|
|
13
13
|
* Installs a runtime package directory into the Home's immutable release
|
|
14
14
|
* tree. The source must carry a release manifest, must not be a Git worktree
|
|
@@ -123,7 +123,7 @@ export function runReleaseList(home) {
|
|
|
123
123
|
version: manifest.version,
|
|
124
124
|
buildId: manifest.buildId,
|
|
125
125
|
packageDigest: manifest.packageDigest,
|
|
126
|
-
smoke:
|
|
126
|
+
smoke: hasMatchingSmokeReceipt(releaseDir, manifest)
|
|
127
127
|
};
|
|
128
128
|
}
|
|
129
129
|
catch {
|
|
@@ -142,6 +142,8 @@ export function runReleaseList(home) {
|
|
|
142
142
|
/** Resolves an installed release by directory name or build ID. */
|
|
143
143
|
export function resolveInstalledRelease(home, releaseId) {
|
|
144
144
|
const directory = releasesDirectory(home);
|
|
145
|
+
if (!existsSync(directory))
|
|
146
|
+
return null;
|
|
145
147
|
for (const entry of readdirSync(directory, { withFileTypes: true })) {
|
|
146
148
|
if (!entry.isDirectory())
|
|
147
149
|
continue;
|
|
@@ -156,18 +158,45 @@ export function resolveInstalledRelease(home, releaseId) {
|
|
|
156
158
|
+ "Reinstall it with `yui release install <source-dir>` before activating.", { cause: error });
|
|
157
159
|
}
|
|
158
160
|
}
|
|
161
|
+
const releaseDir = join(directory, entry.name);
|
|
162
|
+
let manifest;
|
|
159
163
|
try {
|
|
160
|
-
|
|
161
|
-
if (manifest.buildId === releaseId) {
|
|
162
|
-
return { releaseDir: join(directory, entry.name), manifest };
|
|
163
|
-
}
|
|
164
|
+
manifest = readReleaseManifest(releaseDir);
|
|
164
165
|
}
|
|
165
166
|
catch {
|
|
166
167
|
continue;
|
|
167
168
|
}
|
|
169
|
+
if (manifest.buildId !== releaseId)
|
|
170
|
+
continue;
|
|
171
|
+
try {
|
|
172
|
+
return { releaseDir, manifest: verifyReleaseIntegrity(releaseDir) };
|
|
173
|
+
}
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw new Error(`Release ${releaseId} failed integrity verification and may have drifted: `
|
|
176
|
+
+ `${error instanceof Error ? error.message : String(error)}. `
|
|
177
|
+
+ "Reinstall it with `yui release install <source-dir>` before activating.", { cause: error });
|
|
178
|
+
}
|
|
168
179
|
}
|
|
169
180
|
return null;
|
|
170
181
|
}
|
|
182
|
+
/**
|
|
183
|
+
* Selects the verified target release CLI as the activation driver. Ordinary
|
|
184
|
+
* commands remain on the globally installed CLI; only an explicit target
|
|
185
|
+
* activation crosses into the target package so its handover deadlines and
|
|
186
|
+
* protocol own the complete operation.
|
|
187
|
+
*/
|
|
188
|
+
export function resolveReleaseActivationDriver(home, releaseId, currentCliPath) {
|
|
189
|
+
const resolved = resolveInstalledRelease(home, releaseId);
|
|
190
|
+
if (resolved === null)
|
|
191
|
+
return null;
|
|
192
|
+
if (!hasMatchingSmokeReceipt(resolved.releaseDir, resolved.manifest))
|
|
193
|
+
return null;
|
|
194
|
+
const running = detectRunningRelease(currentCliPath);
|
|
195
|
+
if (running !== null && resolve(running.releaseDir) === resolve(resolved.releaseDir)) {
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
return join(resolved.releaseDir, "dist", "cli.js");
|
|
199
|
+
}
|
|
171
200
|
/** Runs the atomic Controller handover for one installed release. */
|
|
172
201
|
export async function runReleaseActivate(home, releaseId, options = {}) {
|
|
173
202
|
let resolved;
|
|
@@ -192,11 +221,11 @@ export async function runReleaseActivate(home, releaseId, options = {}) {
|
|
|
192
221
|
recoverable: true
|
|
193
222
|
};
|
|
194
223
|
}
|
|
195
|
-
if (!
|
|
224
|
+
if (!hasMatchingSmokeReceipt(resolved.releaseDir, resolved.manifest)) {
|
|
196
225
|
return {
|
|
197
226
|
outcome: "aborted",
|
|
198
227
|
phase: "resolve",
|
|
199
|
-
message: `Release has no smoke receipt: ${releaseId}.`,
|
|
228
|
+
message: `Release has no matching smoke receipt: ${releaseId}.`,
|
|
200
229
|
action: "Reinstall the release; the smoke receipt is written at install time.",
|
|
201
230
|
recoverable: true
|
|
202
231
|
};
|
|
@@ -217,6 +246,13 @@ export async function runReleaseActivate(home, releaseId, options = {}) {
|
|
|
217
246
|
: { dualOwnerGraceMs: options.dualOwnerGraceMs })
|
|
218
247
|
});
|
|
219
248
|
}
|
|
249
|
+
function hasMatchingSmokeReceipt(releaseDir, manifest) {
|
|
250
|
+
const receipt = readSmokeReceipt(releaseDir);
|
|
251
|
+
return receipt !== null
|
|
252
|
+
&& receipt.version === manifest.version
|
|
253
|
+
&& receipt.buildId === manifest.buildId
|
|
254
|
+
&& receipt.packageDigest === manifest.packageDigest;
|
|
255
|
+
}
|
|
220
256
|
/** Renders the install result as concise CLI text. */
|
|
221
257
|
export function renderReleaseInstallResult(result) {
|
|
222
258
|
switch (result.outcome) {
|
|
@@ -79,10 +79,21 @@ export function createProductionRuntimeIdentityPorts(packageRoot, entryPath, env
|
|
|
79
79
|
}
|
|
80
80
|
function readGitHead(packageRoot) {
|
|
81
81
|
try {
|
|
82
|
-
const
|
|
83
|
-
|
|
82
|
+
const resolvedPackageRoot = realpathSync(packageRoot);
|
|
83
|
+
const rootResult = spawnSync("git", ["-C", resolvedPackageRoot, "rev-parse", "--show-toplevel"], { encoding: "utf8", timeout: 1_500, stdio: ["ignore", "pipe", "ignore"] });
|
|
84
|
+
if (rootResult.status !== 0)
|
|
84
85
|
return null;
|
|
85
|
-
const
|
|
86
|
+
const repositoryRoot = realpathSync(rootResult.stdout.trim());
|
|
87
|
+
// `git -C` searches parent directories. A globally installed package may
|
|
88
|
+
// therefore appear to belong to an unrelated repository that happens to
|
|
89
|
+
// contain the package manager directory (for example ~/.nvm). Only the
|
|
90
|
+
// package's own checkout is valid source identity evidence.
|
|
91
|
+
if (repositoryRoot !== resolvedPackageRoot)
|
|
92
|
+
return null;
|
|
93
|
+
const headResult = spawnSync("git", ["-C", resolvedPackageRoot, "rev-parse", "HEAD"], { encoding: "utf8", timeout: 1_500, stdio: ["ignore", "pipe", "ignore"] });
|
|
94
|
+
if (headResult.status !== 0)
|
|
95
|
+
return null;
|
|
96
|
+
const head = headResult.stdout.trim();
|
|
86
97
|
return /^[a-f0-9]{7,40}$/u.test(head) ? head : null;
|
|
87
98
|
}
|
|
88
99
|
catch {
|
|
@@ -135,8 +135,8 @@ async function activateLocked(ports, locked) {
|
|
|
135
135
|
};
|
|
136
136
|
}
|
|
137
137
|
// 6) Switch the active release pointer. This is the atomic commit point:
|
|
138
|
-
// after it succeeds,
|
|
139
|
-
// release.
|
|
138
|
+
// after it succeeds, Controller lifecycle operations resolve the new
|
|
139
|
+
// release. The ordinary CLI and managed Session wrappers stay on `yui`.
|
|
140
140
|
const pointer = Object.freeze({
|
|
141
141
|
schemaVersion: 1,
|
|
142
142
|
releaseId,
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
*
|
|
5
5
|
* A release is a self-contained, content-addressed runtime package unpacked
|
|
6
6
|
* below `runtime/releases/<version>-<package-sha256>/`. The Home's active
|
|
7
|
-
* release pointer (`runtime/active-release.json`) names exactly one
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* release pointer (`runtime/active-release.json`) names exactly one Controller
|
|
8
|
+
* release. The ordinary CLI remains the globally installed package; only an
|
|
9
|
+
* explicit target activation delegates to the verified target release CLI so
|
|
10
|
+
* that release owns its handover protocol and deadlines.
|
|
10
11
|
*
|
|
11
12
|
* The Controller writes its provenance to `runtime/runtime-identity.json` on
|
|
12
13
|
* every start, and a versioned handover fence (`runtime/handover-fence.json`)
|