@aopslabs/aops 0.3.16 → 0.3.18
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/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/aops-cli.md +13 -0
- package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/aops-system.md +15 -0
- package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/chatv3.md +16 -1
- package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/manifest.json +9 -9
- package/agent-assets-release/agent-assets/inventory.json +5 -5
- package/agent-assets-release/agent-assets/projection.json +1 -1
- package/agent-assets-release/agent-assets-release.json +40 -40
- package/agent-assets-release/agent-assets-release.sigstore.json +26 -29
- package/assets/agent-assets/core/user-guides/aops-cli.md +13 -0
- package/assets/agent-assets/core/user-guides/aops-system.md +15 -0
- package/assets/agent-assets/core/user-guides/chatv3.md +16 -1
- package/dist/commands/chatv3.js +297 -8
- package/dist/commands/community-server.js +20 -0
- package/dist/commands/global-update.js +353 -9
- package/dist/lib/agent-assets/native-fs.js +2 -2
- package/dist/lib/cli-error-format.js +13 -0
- package/dist/lib/community-native-lifecycle.js +1 -1
- package/dist/main.js +2 -1
- package/dist/utils/chatv3-session-store.js +476 -47
- package/native/manifest.json +1 -1
- package/native/qualifications/win32-x64.json +1 -1
- package/native/tui/darwin-arm64/aops-tui +0 -0
- package/native/tui/win32-x64/aops-tui.exe +0 -0
- package/package.json +6 -5
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/agentspace/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/aops-cli-core/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/chatv3/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/collaborative-work/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/discuss/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/docman/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/mission/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/projectman/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/sys/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/view/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/working-disciplines/SKILL.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/agent-assets.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/agentspace.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/docman.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/projectman.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/sys.md +0 -0
- /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/working-disciplines.md +0 -0
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import assert from 'node:assert/strict';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
3
|
import { createHash } from 'node:crypto';
|
|
4
|
-
import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readlinkSync, readFileSync, realpathSync, writeFileSync, } from 'node:fs';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { compareCanonicalText } from '@aopslabs/artifact-trust-contracts';
|
|
8
|
-
import { Command } from 'commander';
|
|
8
|
+
import { Command, Option } from 'commander';
|
|
9
|
+
import { isCommunityDiagnosticV1 } from '../lib/community-diagnostic.js';
|
|
9
10
|
const CHECK_CONTRACT = 'aops.global-update-check/v1';
|
|
10
11
|
const PLAN_CONTRACT = 'aops.global-update-plan/v1';
|
|
11
12
|
const RESULT_CONTRACT = 'aops.global-update-result/v1';
|
|
13
|
+
const EXECUTOR_CONTRACT = 'aops.global-update-executor/v1';
|
|
14
|
+
const RESULT_FILE = 'update-result.json';
|
|
15
|
+
const EXECUTOR_RECEIPT_FILE = 'executor-receipt.json';
|
|
12
16
|
const PUBLIC_REGISTRY = 'https://registry.npmjs.org';
|
|
13
17
|
const EXACT_STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
|
|
14
18
|
const PLAN_ID = /^sha256:[a-f0-9]{64}$/u;
|
|
@@ -20,6 +24,16 @@ const HEALTH_SAMPLES = 4;
|
|
|
20
24
|
const HEALTH_INTERVAL_MS = 3_000;
|
|
21
25
|
const HEALTH_STARTUP_ATTEMPTS = 30;
|
|
22
26
|
const HEALTH_STARTUP_INTERVAL_MS = 2_000;
|
|
27
|
+
class ChildCommandError extends Error {
|
|
28
|
+
status;
|
|
29
|
+
diagnostic;
|
|
30
|
+
constructor(command, status, diagnostic) {
|
|
31
|
+
super(`${path.basename(command)} failed with status ${status ?? 'unknown'}`);
|
|
32
|
+
this.name = 'ChildCommandError';
|
|
33
|
+
this.status = status;
|
|
34
|
+
this.diagnostic = diagnostic;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
23
37
|
function cleanEnvironment() {
|
|
24
38
|
return {
|
|
25
39
|
...process.env,
|
|
@@ -44,7 +58,17 @@ function defaultRunner(command, args, cwd = process.cwd()) {
|
|
|
44
58
|
});
|
|
45
59
|
if (result.error)
|
|
46
60
|
throw result.error;
|
|
47
|
-
|
|
61
|
+
if (result.status !== 0) {
|
|
62
|
+
let diagnostic;
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(result.stderr.trim());
|
|
65
|
+
if (isCommunityDiagnosticV1(parsed.error))
|
|
66
|
+
diagnostic = parsed.error;
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
}
|
|
70
|
+
throw new ChildCommandError(command, result.status, diagnostic);
|
|
71
|
+
}
|
|
48
72
|
return result.stdout;
|
|
49
73
|
}
|
|
50
74
|
export function resolveNpmInvocation(options = {}) {
|
|
@@ -54,13 +78,12 @@ export function resolveNpmInvocation(options = {}) {
|
|
|
54
78
|
const nodeExecutable = path.resolve(options.nodeExecutable ?? process.execPath);
|
|
55
79
|
const environment = options.environment ?? process.env;
|
|
56
80
|
const fileExists = options.fileExists ?? existsSync;
|
|
57
|
-
const candidates = [
|
|
58
|
-
path.join(path.dirname(nodeExecutable), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
|
|
59
|
-
];
|
|
81
|
+
const candidates = [];
|
|
60
82
|
const npmExecPath = environment.npm_execpath?.trim();
|
|
61
83
|
if (npmExecPath && /^npm-cli\.(?:c?js)$/iu.test(path.basename(npmExecPath))) {
|
|
62
84
|
candidates.push(path.resolve(npmExecPath));
|
|
63
85
|
}
|
|
86
|
+
candidates.push(path.join(path.dirname(nodeExecutable), 'node_modules', 'npm', 'bin', 'npm-cli.js'));
|
|
64
87
|
for (const entry of String(environment.PATH ?? '').split(path.delimiter)) {
|
|
65
88
|
if (!entry.trim())
|
|
66
89
|
continue;
|
|
@@ -108,6 +131,57 @@ export function compareStableVersions(left, right) {
|
|
|
108
131
|
function sha256(file) {
|
|
109
132
|
return createHash('sha256').update(readFileSync(file)).digest('hex');
|
|
110
133
|
}
|
|
134
|
+
function executorTreeInventory(executorRoot) {
|
|
135
|
+
const root = plainDirectory(executorRoot, 'candidate executor root');
|
|
136
|
+
const records = [];
|
|
137
|
+
let totalBytes = 0;
|
|
138
|
+
const visit = (directory, prefix) => {
|
|
139
|
+
for (const entry of readdirSync(directory, { withFileTypes: true })
|
|
140
|
+
.sort((left, right) => compareCanonicalText(left.name, right.name))) {
|
|
141
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
142
|
+
const absolute = path.join(directory, entry.name);
|
|
143
|
+
const stat = lstatSync(absolute);
|
|
144
|
+
const normalizedRelative = relative.replace(/\\/gu, '/');
|
|
145
|
+
if (stat.isSymbolicLink()) {
|
|
146
|
+
const target = readlinkSync(absolute);
|
|
147
|
+
const resolved = realpathSync.native(absolute);
|
|
148
|
+
ensureWithin(root, resolved, `candidate executor link ${normalizedRelative}`);
|
|
149
|
+
records.push({
|
|
150
|
+
path: normalizedRelative,
|
|
151
|
+
type: 'link',
|
|
152
|
+
target: target.replace(/\\/gu, '/'),
|
|
153
|
+
resolved: path.relative(root, resolved).replace(/\\/gu, '/'),
|
|
154
|
+
});
|
|
155
|
+
assert.equal(records.length <= 20_000, true, 'candidate executor entry bound exceeded');
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (stat.isDirectory()) {
|
|
159
|
+
records.push({ path: normalizedRelative, type: 'directory', mode: stat.mode & 0o777 });
|
|
160
|
+
assert.equal(records.length <= 20_000, true, 'candidate executor entry bound exceeded');
|
|
161
|
+
visit(absolute, relative);
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
assert.equal(stat.isFile(), true, `candidate executor entry invalid: ${normalizedRelative}`);
|
|
165
|
+
const canonical = plainFile(absolute, `candidate executor file ${normalizedRelative}`);
|
|
166
|
+
ensureWithin(root, canonical, `candidate executor file ${normalizedRelative}`);
|
|
167
|
+
totalBytes += stat.size;
|
|
168
|
+
records.push({
|
|
169
|
+
path: normalizedRelative,
|
|
170
|
+
type: 'file',
|
|
171
|
+
mode: stat.mode & 0o777,
|
|
172
|
+
bytes: stat.size,
|
|
173
|
+
sha256: sha256(canonical),
|
|
174
|
+
});
|
|
175
|
+
assert.equal(records.length <= 20_000, true, 'candidate executor entry bound exceeded');
|
|
176
|
+
assert.equal(totalBytes <= 512 * 1024 * 1024, true, 'candidate executor package byte bound exceeded');
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
visit(root, '');
|
|
180
|
+
return {
|
|
181
|
+
entryCount: records.length,
|
|
182
|
+
sha256: createHash('sha256').update(JSON.stringify(records)).digest('hex'),
|
|
183
|
+
};
|
|
184
|
+
}
|
|
111
185
|
function plainDirectory(candidate, label) {
|
|
112
186
|
const absolute = path.resolve(candidate);
|
|
113
187
|
const stat = lstatSync(absolute);
|
|
@@ -364,6 +438,136 @@ export function readGlobalUpdatePlan(planPath) {
|
|
|
364
438
|
assert.equal(plan.safety.healthStartupIntervalMs, HEALTH_STARTUP_INTERVAL_MS);
|
|
365
439
|
return plan;
|
|
366
440
|
}
|
|
441
|
+
function readCandidateExecutorReceipt(plan, receiptPath) {
|
|
442
|
+
const operationRoot = plainDirectory(plan.operationRoot, 'global update operation root');
|
|
443
|
+
const resolved = plainFile(receiptPath, 'global update executor receipt');
|
|
444
|
+
assert.equal(resolved, path.join(operationRoot, EXECUTOR_RECEIPT_FILE), 'global update executor receipt must stay at the operation root');
|
|
445
|
+
const receipt = parseJson(readFileSync(resolved, 'utf8'), 'global_update_executor_receipt');
|
|
446
|
+
assert.equal(receipt.schemaVersion, 1);
|
|
447
|
+
assert.equal(receipt.contract, EXECUTOR_CONTRACT);
|
|
448
|
+
assert.equal(receipt.planId, plan.planId, 'global update executor plan identity mismatch');
|
|
449
|
+
const executorRoot = plainDirectory(receipt.executorRoot, 'global update executor root');
|
|
450
|
+
ensureWithin(operationRoot, executorRoot, 'global update executor root');
|
|
451
|
+
assert.equal(executorRoot, path.join(operationRoot, 'executor'));
|
|
452
|
+
const entrypoint = plainFile(receipt.entrypoint, 'global update executor entrypoint');
|
|
453
|
+
ensureWithin(executorRoot, entrypoint, 'global update executor entrypoint');
|
|
454
|
+
assert.equal(entrypoint, receipt.entrypoint, 'global update executor entrypoint must be canonical');
|
|
455
|
+
assert.match(receipt.entrypointSha256, SHA256);
|
|
456
|
+
assert.equal(sha256(entrypoint), receipt.entrypointSha256, 'global update executor entrypoint changed');
|
|
457
|
+
const candidate = packageRecordByName(plan.candidates, '@aopslabs/aops');
|
|
458
|
+
assert.equal(receipt.candidate.version, candidate.version);
|
|
459
|
+
assert.equal(receipt.candidate.sha256, candidate.sha256);
|
|
460
|
+
const executorPackageRoot = path.join(executorRoot, 'node_modules', '@aopslabs', 'aops');
|
|
461
|
+
const executorManifest = packageManifest(executorPackageRoot, '@aopslabs/aops');
|
|
462
|
+
assert.equal(executorManifest.version, plan.target.packages.cli);
|
|
463
|
+
const dependencies = executorManifest.dependencies;
|
|
464
|
+
assert.equal(dependencies?.['@aopslabs/aops-server'], plan.target.packages.server);
|
|
465
|
+
const executorServer = packageManifest(path.join(executorRoot, 'node_modules', '@aopslabs', 'aops-server'), '@aopslabs/aops-server');
|
|
466
|
+
assert.equal(executorServer.version, plan.target.packages.server);
|
|
467
|
+
assert.equal(Number.isSafeInteger(receipt.executorEntryCount), true);
|
|
468
|
+
assert.match(receipt.executorContentSha256, SHA256);
|
|
469
|
+
const inventory = executorTreeInventory(executorRoot);
|
|
470
|
+
assert.equal(inventory.entryCount, receipt.executorEntryCount, 'candidate executor entry count changed');
|
|
471
|
+
assert.equal(inventory.sha256, receipt.executorContentSha256, 'candidate executor content changed');
|
|
472
|
+
return receipt;
|
|
473
|
+
}
|
|
474
|
+
export function stageCandidateGlobalUpdateExecutor(plan, run = defaultRunner) {
|
|
475
|
+
const receiptPath = path.join(plan.operationRoot, EXECUTOR_RECEIPT_FILE);
|
|
476
|
+
if (existsSync(receiptPath)) {
|
|
477
|
+
return { receipt: readCandidateExecutorReceipt(plan, receiptPath), receiptPath };
|
|
478
|
+
}
|
|
479
|
+
const executorRoot = path.join(plan.operationRoot, 'executor');
|
|
480
|
+
assert.equal(existsSync(executorRoot), false, 'unverified global update executor directory already exists');
|
|
481
|
+
mkdirSync(executorRoot, { mode: 0o700 });
|
|
482
|
+
const cacheRoot = path.join(plan.operationRoot, 'cache');
|
|
483
|
+
mkdirSync(cacheRoot, { recursive: true, mode: 0o700 });
|
|
484
|
+
const cli = packageRecordByName(plan.candidates, '@aopslabs/aops');
|
|
485
|
+
const server = packageRecordByName(plan.candidates, '@aopslabs/aops-server');
|
|
486
|
+
runNpm(run, publicNpmArgs([
|
|
487
|
+
'--prefix',
|
|
488
|
+
executorRoot,
|
|
489
|
+
'install',
|
|
490
|
+
'--ignore-scripts',
|
|
491
|
+
'--omit=dev',
|
|
492
|
+
'--prefer-offline',
|
|
493
|
+
`--cache=${cacheRoot}`,
|
|
494
|
+
'--fund=false',
|
|
495
|
+
'--audit=false',
|
|
496
|
+
cli.path,
|
|
497
|
+
server.path,
|
|
498
|
+
]));
|
|
499
|
+
const entrypoint = plainFile(path.join(executorRoot, 'node_modules', '@aopslabs', 'aops', 'dist', 'main.js'), 'global update executor entrypoint');
|
|
500
|
+
const inventory = executorTreeInventory(executorRoot);
|
|
501
|
+
const receipt = {
|
|
502
|
+
schemaVersion: 1,
|
|
503
|
+
contract: EXECUTOR_CONTRACT,
|
|
504
|
+
planId: plan.planId,
|
|
505
|
+
executorRoot: plainDirectory(executorRoot, 'global update executor root'),
|
|
506
|
+
entrypoint,
|
|
507
|
+
entrypointSha256: sha256(entrypoint),
|
|
508
|
+
executorEntryCount: inventory.entryCount,
|
|
509
|
+
executorContentSha256: inventory.sha256,
|
|
510
|
+
candidate: { version: cli.version, sha256: cli.sha256 },
|
|
511
|
+
};
|
|
512
|
+
writeFileSync(receiptPath, `${JSON.stringify(receipt, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
513
|
+
return { receipt: readCandidateExecutorReceipt(plan, receiptPath), receiptPath };
|
|
514
|
+
}
|
|
515
|
+
function validateCandidateExecutorInvocation(plan, receiptPath) {
|
|
516
|
+
const receipt = readCandidateExecutorReceipt(plan, receiptPath);
|
|
517
|
+
const invokedEntrypoint = plainFile(process.argv[1], 'invoked AOPS CLI entrypoint');
|
|
518
|
+
assert.equal(invokedEntrypoint, receipt.entrypoint, 'candidate update execution must run from the verified staged entrypoint');
|
|
519
|
+
return receipt;
|
|
520
|
+
}
|
|
521
|
+
export function validateLegacyGlobalUpdateBridgeInvocation(plan, invokedEntrypoint = process.argv[1]) {
|
|
522
|
+
const invoked = plainFile(invokedEntrypoint, 'legacy update bridge candidate entrypoint');
|
|
523
|
+
const globalCliRoot = plainDirectory(path.dirname(path.dirname(plan.cliPath)), 'planned global CLI root');
|
|
524
|
+
const relativeToGlobal = path.relative(globalCliRoot, invoked);
|
|
525
|
+
assert.equal(relativeToGlobal.startsWith('..') || path.isAbsolute(relativeToGlobal), true, 'legacy update bridge must run from an external exact candidate, not the installed old CLI');
|
|
526
|
+
const candidateRoot = plainDirectory(path.dirname(path.dirname(invoked)), 'legacy update bridge candidate root');
|
|
527
|
+
const manifest = packageManifest(candidateRoot, '@aopslabs/aops');
|
|
528
|
+
const version = exactVersion(manifest.version, 'legacy update bridge candidate version');
|
|
529
|
+
assert.equal(version, plan.target.packages.cli, 'legacy update bridge candidate must match the plan target');
|
|
530
|
+
return { candidateRoot, version };
|
|
531
|
+
}
|
|
532
|
+
export function dispatchGlobalUpdate(options, run = defaultRunner) {
|
|
533
|
+
const plan = readGlobalUpdatePlan(options.plan);
|
|
534
|
+
assert.equal(options.confirm, plan.planId, 'global update confirmation does not match the exact plan');
|
|
535
|
+
const resultPath = path.join(plan.operationRoot, RESULT_FILE);
|
|
536
|
+
assert.equal(existsSync(resultPath), false, 'global_update_plan_already_terminal');
|
|
537
|
+
let executorStarted = false;
|
|
538
|
+
try {
|
|
539
|
+
const { receipt, receiptPath } = stageCandidateGlobalUpdateExecutor(plan, run);
|
|
540
|
+
executorStarted = true;
|
|
541
|
+
return parseJson(run(process.execPath, [
|
|
542
|
+
receipt.entrypoint,
|
|
543
|
+
'update',
|
|
544
|
+
'apply',
|
|
545
|
+
'--plan',
|
|
546
|
+
options.plan,
|
|
547
|
+
'--confirm',
|
|
548
|
+
options.confirm,
|
|
549
|
+
'--executor-receipt',
|
|
550
|
+
receiptPath,
|
|
551
|
+
'--json',
|
|
552
|
+
]).trim(), 'global_update_candidate_result');
|
|
553
|
+
}
|
|
554
|
+
catch (error) {
|
|
555
|
+
if (!existsSync(resultPath)) {
|
|
556
|
+
writeTerminalUpdateResult(plan, {
|
|
557
|
+
schemaVersion: 1,
|
|
558
|
+
contract: RESULT_CONTRACT,
|
|
559
|
+
status: executorStarted ? 'executor-result-missing' : 'executor-staging-failed',
|
|
560
|
+
planId: plan.planId,
|
|
561
|
+
failedStage: executorStarted ? 'candidate-executor' : 'stage-candidate-executor',
|
|
562
|
+
globalState: executorStarted ? 'unknown-requires-inspection' : 'unchanged',
|
|
563
|
+
rollbackPerformed: false,
|
|
564
|
+
error: safeUpdateError(error),
|
|
565
|
+
events: [],
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
throw new Error(`global update candidate executor failed; diagnostic record: ${resultPath}`, { cause: error });
|
|
569
|
+
}
|
|
570
|
+
}
|
|
367
571
|
export function installGlobalUpdateClosure(run, plan, records, topology) {
|
|
368
572
|
const cli = packageRecordByName(records, '@aopslabs/aops');
|
|
369
573
|
const server = packageRecordByName(records, '@aopslabs/aops-server');
|
|
@@ -528,9 +732,61 @@ export function validateLoopbackOrigin(value, label) {
|
|
|
528
732
|
assert.equal(url.hash, '', `${label} must not include a fragment`);
|
|
529
733
|
return url.origin;
|
|
530
734
|
}
|
|
735
|
+
function nestedCommunityDiagnostic(error) {
|
|
736
|
+
const pending = [error];
|
|
737
|
+
const visited = new Set();
|
|
738
|
+
while (pending.length > 0) {
|
|
739
|
+
const candidate = pending.shift();
|
|
740
|
+
if (!candidate || visited.has(candidate))
|
|
741
|
+
continue;
|
|
742
|
+
visited.add(candidate);
|
|
743
|
+
if (typeof candidate === 'object') {
|
|
744
|
+
const record = candidate;
|
|
745
|
+
if (isCommunityDiagnosticV1(record.diagnostic))
|
|
746
|
+
return record.diagnostic;
|
|
747
|
+
if (record.cause)
|
|
748
|
+
pending.push(record.cause);
|
|
749
|
+
if (candidate instanceof AggregateError)
|
|
750
|
+
pending.push(...candidate.errors);
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return undefined;
|
|
754
|
+
}
|
|
755
|
+
function safeUpdateError(error) {
|
|
756
|
+
const diagnostic = nestedCommunityDiagnostic(error);
|
|
757
|
+
if (diagnostic) {
|
|
758
|
+
const details = diagnostic.details;
|
|
759
|
+
return Object.fromEntries(Object.entries({
|
|
760
|
+
code: diagnostic.code,
|
|
761
|
+
causeCode: diagnostic.cause.code,
|
|
762
|
+
phase: diagnostic.phase,
|
|
763
|
+
operationId: typeof details.operationId === 'string' && UUID.test(details.operationId)
|
|
764
|
+
? details.operationId
|
|
765
|
+
: undefined,
|
|
766
|
+
operation: typeof details.operation === 'string' ? details.operation : undefined,
|
|
767
|
+
journalPhase: typeof details.journalPhase === 'string' ? details.journalPhase : undefined,
|
|
768
|
+
journalStep: typeof details.journalStep === 'string' ? details.journalStep : undefined,
|
|
769
|
+
}).filter(([, value]) => value !== undefined));
|
|
770
|
+
}
|
|
771
|
+
const original = error instanceof Error ? error.message : String(error ?? 'global update failed');
|
|
772
|
+
const message = original
|
|
773
|
+
.split(/\r?\n/u, 1)[0]
|
|
774
|
+
.replace(/https?:\/\/\S+/giu, '[REDACTED_URL]')
|
|
775
|
+
.replace(/((?:password|passwd|secret|token|api[_-]?key|authorization)\s*[=:]\s*)\S+/giu, '$1[REDACTED]')
|
|
776
|
+
.replace(/(?:[A-Za-z]:\\|\/)\S+/gu, '[REDACTED_PATH]')
|
|
777
|
+
.slice(0, 500);
|
|
778
|
+
const code = /\b(?:aops|community|global_update)_[a-z0-9_]+\b/iu.exec(original)?.[0]?.toLowerCase()
|
|
779
|
+
?? 'global_update_failed';
|
|
780
|
+
return { code, message };
|
|
781
|
+
}
|
|
782
|
+
function writeTerminalUpdateResult(plan, result) {
|
|
783
|
+
const resultPath = path.join(plan.operationRoot, RESULT_FILE);
|
|
784
|
+
writeFileSync(resultPath, `${JSON.stringify(result, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
|
|
785
|
+
}
|
|
531
786
|
export async function applyGlobalUpdate(options, run = defaultRunner, execution = defaultExecutionDependencies) {
|
|
532
787
|
const plan = readGlobalUpdatePlan(options.plan);
|
|
533
788
|
assert.equal(options.confirm, plan.planId, 'global update confirmation does not match the exact plan');
|
|
789
|
+
assert.equal(existsSync(path.join(plan.operationRoot, RESULT_FILE)), false, 'global_update_plan_already_terminal');
|
|
534
790
|
const current = inspectGlobalInstall(run);
|
|
535
791
|
assert.deepEqual(current.packages, plan.prior.packages, 'global packages changed after update planning');
|
|
536
792
|
assert.deepEqual(current.topology, plan.prior.topology, 'global package topology changed after update planning');
|
|
@@ -538,31 +794,39 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
|
|
|
538
794
|
assertHealthyServices(serviceSnapshot(run, current.cliPath));
|
|
539
795
|
const events = [];
|
|
540
796
|
let applicationUpdateId = null;
|
|
797
|
+
let failedStage = 'stop-cockpit';
|
|
541
798
|
try {
|
|
542
799
|
stopService(run, current.cliPath, 'cockpit');
|
|
543
800
|
events.push('stopped:cockpit');
|
|
801
|
+
failedStage = 'stop-server';
|
|
544
802
|
stopService(run, current.cliPath, 'server');
|
|
545
803
|
events.push('stopped:server');
|
|
804
|
+
failedStage = 'install-candidate';
|
|
546
805
|
installGlobalUpdateClosure(run, plan, plan.candidates, plan.target.topology);
|
|
547
806
|
events.push('installed:candidate');
|
|
807
|
+
failedStage = 'verify-candidate';
|
|
548
808
|
const updated = inspectGlobalInstall(run);
|
|
549
809
|
assert.deepEqual(updated.packages, plan.target.packages, 'installed candidate closure is not exact');
|
|
550
810
|
assert.deepEqual(updated.topology, plan.target.topology, 'installed candidate topology is not canonical');
|
|
551
811
|
if (plan.prior.packages.server === plan.target.packages.server) {
|
|
812
|
+
failedStage = 'start-unchanged-server';
|
|
552
813
|
startService(run, updated.cliPath, 'server');
|
|
553
814
|
events.push('started:server-unchanged');
|
|
554
815
|
}
|
|
555
816
|
else {
|
|
817
|
+
failedStage = 'update-server-application';
|
|
556
818
|
applicationUpdateId = updateServerApplication(run, updated.cliPath, {
|
|
557
819
|
prior: plan.prior.packages.server,
|
|
558
820
|
target: plan.target.packages.server,
|
|
559
821
|
});
|
|
560
822
|
events.push('updated:server');
|
|
561
823
|
}
|
|
824
|
+
failedStage = 'start-cockpit';
|
|
562
825
|
startService(run, updated.cliPath, 'cockpit');
|
|
563
826
|
events.push('started:cockpit');
|
|
827
|
+
failedStage = 'stable-health';
|
|
564
828
|
const healthEvidence = await assertStableHealth(run, plan, execution);
|
|
565
|
-
|
|
829
|
+
const result = {
|
|
566
830
|
schemaVersion: 1,
|
|
567
831
|
contract: RESULT_CONTRACT,
|
|
568
832
|
status: 'updated',
|
|
@@ -573,9 +837,13 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
|
|
|
573
837
|
healthEvidence,
|
|
574
838
|
next: 'Exit and reopen the AOPS TUI to run the newly installed binary.',
|
|
575
839
|
};
|
|
840
|
+
writeTerminalUpdateResult(plan, result);
|
|
841
|
+
return result;
|
|
576
842
|
}
|
|
577
843
|
catch (updateError) {
|
|
844
|
+
const updateStage = failedStage;
|
|
578
845
|
events.push('rollback:started');
|
|
846
|
+
let rollbackError;
|
|
579
847
|
try {
|
|
580
848
|
stopService(run, plan.cliPath, 'cockpit', true);
|
|
581
849
|
stopService(run, plan.cliPath, 'server', true);
|
|
@@ -597,8 +865,64 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
|
|
|
597
865
|
events.push('rollback:healthy');
|
|
598
866
|
}
|
|
599
867
|
catch (rollbackError) {
|
|
600
|
-
|
|
868
|
+
try {
|
|
869
|
+
const restoredForRecovery = inspectGlobalInstall(run);
|
|
870
|
+
assert.deepEqual(restoredForRecovery.packages, plan.prior.packages, 'service recovery package closure is not exact');
|
|
871
|
+
assert.deepEqual(restoredForRecovery.topology, plan.prior.topology, 'service recovery package topology is not exact');
|
|
872
|
+
stopService(run, restoredForRecovery.cliPath, 'cockpit', true);
|
|
873
|
+
stopService(run, restoredForRecovery.cliPath, 'server', true);
|
|
874
|
+
startService(run, restoredForRecovery.cliPath, 'server');
|
|
875
|
+
events.push('started:server-recovery');
|
|
876
|
+
startService(run, restoredForRecovery.cliPath, 'cockpit');
|
|
877
|
+
events.push('started:cockpit-recovery');
|
|
878
|
+
await assertStableHealth(run, plan, execution);
|
|
879
|
+
events.push('rollback:services-recovered');
|
|
880
|
+
}
|
|
881
|
+
catch (serviceRecoveryError) {
|
|
882
|
+
writeTerminalUpdateResult(plan, {
|
|
883
|
+
schemaVersion: 1,
|
|
884
|
+
contract: RESULT_CONTRACT,
|
|
885
|
+
status: 'recovery-required',
|
|
886
|
+
planId: plan.planId,
|
|
887
|
+
failedStage: updateStage,
|
|
888
|
+
rollbackPerformed: true,
|
|
889
|
+
rollback: { status: 'failed', error: safeUpdateError(rollbackError) },
|
|
890
|
+
serviceRecovery: { status: 'failed', error: safeUpdateError(serviceRecoveryError) },
|
|
891
|
+
error: safeUpdateError(updateError),
|
|
892
|
+
events,
|
|
893
|
+
});
|
|
894
|
+
throw new AggregateError([updateError, rollbackError, serviceRecoveryError], `global update, automatic rollback, and service recovery failed; services may remain stopped; recovery artifacts remain at ${plan.operationRoot}`);
|
|
895
|
+
}
|
|
896
|
+
writeTerminalUpdateResult(plan, {
|
|
897
|
+
schemaVersion: 1,
|
|
898
|
+
contract: RESULT_CONTRACT,
|
|
899
|
+
status: 'failed-restored',
|
|
900
|
+
planId: plan.planId,
|
|
901
|
+
failedStage: updateStage,
|
|
902
|
+
installed: plan.prior.packages,
|
|
903
|
+
rollbackPerformed: true,
|
|
904
|
+
rollback: {
|
|
905
|
+
status: 'healthy',
|
|
906
|
+
recovery: 'services-recovered',
|
|
907
|
+
applicationRollbackError: safeUpdateError(rollbackError),
|
|
908
|
+
},
|
|
909
|
+
error: safeUpdateError(updateError),
|
|
910
|
+
events,
|
|
911
|
+
});
|
|
912
|
+
throw new AggregateError([updateError, rollbackError], `global update and application rollback failed; prior exact packages and healthy services were restored; recovery artifacts remain at ${plan.operationRoot}`);
|
|
601
913
|
}
|
|
914
|
+
writeTerminalUpdateResult(plan, {
|
|
915
|
+
schemaVersion: 1,
|
|
916
|
+
contract: RESULT_CONTRACT,
|
|
917
|
+
status: 'failed-restored',
|
|
918
|
+
planId: plan.planId,
|
|
919
|
+
failedStage: updateStage,
|
|
920
|
+
installed: plan.prior.packages,
|
|
921
|
+
rollbackPerformed: true,
|
|
922
|
+
rollback: { status: 'healthy' },
|
|
923
|
+
error: safeUpdateError(updateError),
|
|
924
|
+
events,
|
|
925
|
+
});
|
|
602
926
|
throw new Error('global update failed; the prior exact packages were restored and are healthy', { cause: updateError });
|
|
603
927
|
}
|
|
604
928
|
}
|
|
@@ -623,7 +947,27 @@ export function makeGlobalUpdateCommand() {
|
|
|
623
947
|
.description('Apply one exact prepared plan with automatic rollback and health verification')
|
|
624
948
|
.requiredOption('--plan <path>', 'Exact update-plan.json path returned by prepare')
|
|
625
949
|
.requiredOption('--confirm <plan-id>', 'Exact sha256 plan id returned by prepare')
|
|
950
|
+
.addOption(new Option('--executor-receipt <path>').hideHelp())
|
|
626
951
|
.option('--json', 'Output JSON')
|
|
627
|
-
.action(async (options) =>
|
|
952
|
+
.action(async (options) => {
|
|
953
|
+
if (typeof options.executorReceipt === 'string') {
|
|
954
|
+
const plan = readGlobalUpdatePlan(options.plan);
|
|
955
|
+
validateCandidateExecutorInvocation(plan, options.executorReceipt);
|
|
956
|
+
output(await applyGlobalUpdate(options), options.json === true);
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
output(dispatchGlobalUpdate(options), options.json === true);
|
|
960
|
+
});
|
|
961
|
+
command
|
|
962
|
+
.command('bridge')
|
|
963
|
+
.description('Apply an old-CLI plan from an externally launched exact public candidate')
|
|
964
|
+
.requiredOption('--plan <path>', 'Exact update-plan.json path returned by the installed old CLI')
|
|
965
|
+
.requiredOption('--confirm <plan-id>', 'Exact sha256 plan id returned by prepare')
|
|
966
|
+
.option('--json', 'Output JSON')
|
|
967
|
+
.action((options) => {
|
|
968
|
+
const plan = readGlobalUpdatePlan(options.plan);
|
|
969
|
+
validateLegacyGlobalUpdateBridgeInvocation(plan);
|
|
970
|
+
output(dispatchGlobalUpdate(options), options.json === true);
|
|
971
|
+
});
|
|
628
972
|
return command;
|
|
629
973
|
}
|
|
@@ -543,9 +543,9 @@ function defaultNativeRoot() {
|
|
|
543
543
|
return moduleDirectory.endsWith(`${path.sep}dist`) ? bundled : compiledOrSource;
|
|
544
544
|
}
|
|
545
545
|
function injectedNativeManifestSha256() {
|
|
546
|
-
return typeof "
|
|
546
|
+
return typeof "859b976094fe8cde402fd4ff8b48716df0360ce7e30e698f43b6515216f4c95f" === 'undefined'
|
|
547
547
|
? undefined
|
|
548
|
-
: "
|
|
548
|
+
: "859b976094fe8cde402fd4ff8b48716df0360ce7e30e698f43b6515216f4c95f";
|
|
549
549
|
}
|
|
550
550
|
function sha256(bytes) {
|
|
551
551
|
return createHash('sha256').update(bytes).digest('hex');
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
function messageOf(error) {
|
|
2
|
+
return error instanceof Error ? error.message : String(error);
|
|
3
|
+
}
|
|
4
|
+
export function formatCliError(error, depth = 0) {
|
|
5
|
+
const message = messageOf(error);
|
|
6
|
+
if (!(error instanceof AggregateError) || error.errors.length === 0 || depth >= 3)
|
|
7
|
+
return message;
|
|
8
|
+
const details = error.errors.map((cause, index) => {
|
|
9
|
+
const rendered = formatCliError(cause, depth + 1).replaceAll('\n', '\n ');
|
|
10
|
+
return ` ${index + 1}. ${rendered}`;
|
|
11
|
+
});
|
|
12
|
+
return `${message}\n${details.join('\n')}`;
|
|
13
|
+
}
|
|
@@ -23,7 +23,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-
|
|
|
23
23
|
const MAX_CONFIG_BYTES = 65_536;
|
|
24
24
|
const MAX_TLS_ROOT_CERT_BYTES = 1_048_576;
|
|
25
25
|
const MIN_EXACT_SECRET_REDACTION_LENGTH = 8;
|
|
26
|
-
const DEFAULT_READY_TIMEOUT_MS =
|
|
26
|
+
const DEFAULT_READY_TIMEOUT_MS = 120_000;
|
|
27
27
|
const DEFAULT_STOP_TIMEOUT_MS = 15_000;
|
|
28
28
|
const READY_POLL_MS = 250;
|
|
29
29
|
const MAX_LOG_TAIL_BYTES = 4 * 1024 * 1024;
|
package/dist/main.js
CHANGED
|
@@ -41,6 +41,7 @@ import { makeGlobalUpdateCommand } from './commands/global-update.js';
|
|
|
41
41
|
import { launchBundledTui, shouldLaunchBundledTui } from './lib/tui-launcher.js';
|
|
42
42
|
import { CommunityDiagnosticError, formatCommunityDiagnostic, } from './lib/community-diagnostic.js';
|
|
43
43
|
import { resolveCliApiBaseUrl } from './utils/api.js';
|
|
44
|
+
import { formatCliError } from './lib/cli-error-format.js';
|
|
44
45
|
const KERNEL_COMMANDS_REPLACED_BY_AOPS = new Set(['agent', 'api', 'auth', 'client', 'host']);
|
|
45
46
|
function removeReplacedKernelCommands(program) {
|
|
46
47
|
const mutable = program;
|
|
@@ -204,7 +205,7 @@ async function main() {
|
|
|
204
205
|
}
|
|
205
206
|
}
|
|
206
207
|
else {
|
|
207
|
-
logError(
|
|
208
|
+
logError(formatCliError(error));
|
|
208
209
|
}
|
|
209
210
|
process.exitCode = 1;
|
|
210
211
|
}
|