@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.
Files changed (42) hide show
  1. package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/aops-cli.md +13 -0
  2. package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/aops-system.md +15 -0
  3. package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/chatv3.md +16 -1
  4. package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/manifest.json +9 -9
  5. package/agent-assets-release/agent-assets/inventory.json +5 -5
  6. package/agent-assets-release/agent-assets/projection.json +1 -1
  7. package/agent-assets-release/agent-assets-release.json +40 -40
  8. package/agent-assets-release/agent-assets-release.sigstore.json +26 -29
  9. package/assets/agent-assets/core/user-guides/aops-cli.md +13 -0
  10. package/assets/agent-assets/core/user-guides/aops-system.md +15 -0
  11. package/assets/agent-assets/core/user-guides/chatv3.md +16 -1
  12. package/dist/commands/chatv3.js +297 -8
  13. package/dist/commands/community-server.js +20 -0
  14. package/dist/commands/global-update.js +353 -9
  15. package/dist/lib/agent-assets/native-fs.js +2 -2
  16. package/dist/lib/cli-error-format.js +13 -0
  17. package/dist/lib/community-native-lifecycle.js +1 -1
  18. package/dist/main.js +2 -1
  19. package/dist/utils/chatv3-session-store.js +476 -47
  20. package/native/manifest.json +1 -1
  21. package/native/qualifications/win32-x64.json +1 -1
  22. package/native/tui/darwin-arm64/aops-tui +0 -0
  23. package/native/tui/win32-x64/aops-tui.exe +0 -0
  24. package/package.json +6 -5
  25. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/SKILL.md +0 -0
  26. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/agentspace/SKILL.md +0 -0
  27. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/aops-cli-core/SKILL.md +0 -0
  28. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/chatv3/SKILL.md +0 -0
  29. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/collaborative-work/SKILL.md +0 -0
  30. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/discuss/SKILL.md +0 -0
  31. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/docman/SKILL.md +0 -0
  32. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/mission/SKILL.md +0 -0
  33. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/projectman/SKILL.md +0 -0
  34. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/sys/SKILL.md +0 -0
  35. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/view/SKILL.md +0 -0
  36. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/working-disciplines/SKILL.md +0 -0
  37. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/agent-assets.md +0 -0
  38. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/agentspace.md +0 -0
  39. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/docman.md +0 -0
  40. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/projectman.md +0 -0
  41. /package/agent-assets-release/agent-assets/core/{ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/sys.md +0 -0
  42. /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
- assert.equal(result.status, 0, `${command} ${args.join(' ')} failed\n${result.stdout}\n${result.stderr}`);
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
- return {
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
- throw new AggregateError([updateError, rollbackError], `global update and automatic rollback both failed; recovery artifacts remain at ${plan.operationRoot}`);
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) => output(await applyGlobalUpdate(options), options.json === true));
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 "4adb33acb603e59a7468a00116b92ef15500fe8831a079235708261636780360" === 'undefined'
546
+ return typeof "859b976094fe8cde402fd4ff8b48716df0360ce7e30e698f43b6515216f4c95f" === 'undefined'
547
547
  ? undefined
548
- : "4adb33acb603e59a7468a00116b92ef15500fe8831a079235708261636780360";
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 = 30_000;
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(String(error?.message ?? error));
208
+ logError(formatCliError(error));
208
209
  }
209
210
  process.exitCode = 1;
210
211
  }