@aopslabs/aops 0.3.17 → 0.3.19

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 (39) hide show
  1. package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/aops-cli.md +13 -0
  2. package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/aops-system.md +15 -0
  3. package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/manifest.json +7 -7
  4. package/agent-assets-release/agent-assets/inventory.json +5 -5
  5. package/agent-assets-release/agent-assets/projection.json +1 -1
  6. package/agent-assets-release/agent-assets-release.json +38 -38
  7. package/agent-assets-release/agent-assets-release.sigstore.json +24 -28
  8. package/assets/agent-assets/core/user-guides/aops-cli.md +13 -0
  9. package/assets/agent-assets/core/user-guides/aops-system.md +15 -0
  10. package/dist/commands/community-cockpit.js +24 -4
  11. package/dist/commands/community-server.js +1 -0
  12. package/dist/commands/global-update.js +336 -8
  13. package/dist/lib/agent-assets/native-fs.js +2 -2
  14. package/dist/lib/community-cockpit-lifecycle.js +32 -2
  15. package/native/manifest.json +1 -1
  16. package/native/qualifications/win32-x64.json +1 -1
  17. package/native/tui/darwin-arm64/aops-tui +0 -0
  18. package/native/tui/linux-x64/aops-tui +0 -0
  19. package/native/tui/win32-x64/aops-tui.exe +0 -0
  20. package/package.json +2 -1
  21. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/SKILL.md +0 -0
  22. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/agentspace/SKILL.md +0 -0
  23. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/aops-cli-core/SKILL.md +0 -0
  24. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/chatv3/SKILL.md +0 -0
  25. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/collaborative-work/SKILL.md +0 -0
  26. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/discuss/SKILL.md +0 -0
  27. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/docman/SKILL.md +0 -0
  28. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/mission/SKILL.md +0 -0
  29. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/projectman/SKILL.md +0 -0
  30. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/sys/SKILL.md +0 -0
  31. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/view/SKILL.md +0 -0
  32. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/references/working-disciplines/SKILL.md +0 -0
  33. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/agent-assets.md +0 -0
  34. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/agentspace.md +0 -0
  35. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/chatv3.md +0 -0
  36. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/docman.md +0 -0
  37. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/projectman.md +0 -0
  38. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 387548b990d0cfef6a92805f39d18c2ae6b4bb6b1e42ea42e6c597ea7d207447}/files/user-guides/sys.md +0 -0
  39. /package/agent-assets-release/agent-assets/core/{e66703ed1e78896b46a4124b8c26f2972a504900425517ea51f1dcc484d411a1 → 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);
@@ -611,10 +879,50 @@ export async function applyGlobalUpdate(options, run = defaultRunner, execution
611
879
  events.push('rollback:services-recovered');
612
880
  }
613
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
+ });
614
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}`);
615
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
+ });
616
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}`);
617
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
+ });
618
926
  throw new Error('global update failed; the prior exact packages were restored and are healthy', { cause: updateError });
619
927
  }
620
928
  }
@@ -639,7 +947,27 @@ export function makeGlobalUpdateCommand() {
639
947
  .description('Apply one exact prepared plan with automatic rollback and health verification')
640
948
  .requiredOption('--plan <path>', 'Exact update-plan.json path returned by prepare')
641
949
  .requiredOption('--confirm <plan-id>', 'Exact sha256 plan id returned by prepare')
950
+ .addOption(new Option('--executor-receipt <path>').hideHelp())
642
951
  .option('--json', 'Output JSON')
643
- .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
+ });
644
972
  return command;
645
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 "6164b5c4d71eb6eed7d12b49faf10cd9f7c5324de4e893f50087e7089b5483b3" === 'undefined'
546
+ return typeof "bfaede623bfbea984d9711454380227e4f4e7a2bc816dee2b47002065209c741" === 'undefined'
547
547
  ? undefined
548
- : "6164b5c4d71eb6eed7d12b49faf10cd9f7c5324de4e893f50087e7089b5483b3";
548
+ : "bfaede623bfbea984d9711454380227e4f4e7a2bc816dee2b47002065209c741";
549
549
  }
550
550
  function sha256(bytes) {
551
551
  return createHash('sha256').update(bytes).digest('hex');
@@ -187,6 +187,24 @@ function statusBase(state, port, serverPort = state.server.publicPort) {
187
187
  port,
188
188
  };
189
189
  }
190
+ function terminalObservation(record, observedAt, disappeared = false) {
191
+ const classification = disappeared
192
+ ? 'disappeared'
193
+ : record.signal
194
+ ? 'signal'
195
+ : record.status === 'exited' && record.exitCode === 0
196
+ ? 'clean-exit'
197
+ : record.exitCode !== undefined && record.exitCode !== null
198
+ ? 'failed-exit'
199
+ : 'launch-failure';
200
+ return {
201
+ classification,
202
+ lastSeenAt: record.updatedAt,
203
+ observedAt,
204
+ exitCode: record.exitCode ?? null,
205
+ signal: record.signal ?? null,
206
+ };
207
+ }
190
208
  async function observeCockpit(params) {
191
209
  const record = params.processRecord;
192
210
  const identityPresent = existsSync(params.paths.identityPath);
@@ -203,6 +221,7 @@ async function observeCockpit(params) {
203
221
  hostAlive: false,
204
222
  health: 'not-checked',
205
223
  reason: identityPresent ? 'community_cockpit_identity_without_process_record' : null,
224
+ terminalObservation: null,
206
225
  };
207
226
  }
208
227
  const supervisorAlive = params.runtime.processExists(record.pid);
@@ -218,6 +237,7 @@ async function observeCockpit(params) {
218
237
  supervisorAlive, hostAlive: record.hostPid ? params.runtime.processExists(record.hostPid) : false,
219
238
  health: 'not-checked',
220
239
  reason: error instanceof Error ? error.message : 'community_cockpit_identity_invalid',
240
+ terminalObservation: null,
221
241
  };
222
242
  }
223
243
  }
@@ -231,6 +251,7 @@ async function observeCockpit(params) {
231
251
  runtimeState: 'stopped', recoverable: true, process: record, identity,
232
252
  supervisorAlive: false, hostAlive: false, health: 'not-checked',
233
253
  reason: identity ? 'community_cockpit_terminal_identity_stale' : null,
254
+ terminalObservation: terminalObservation(record, record.updatedAt),
234
255
  };
235
256
  }
236
257
  return {
@@ -238,6 +259,7 @@ async function observeCockpit(params) {
238
259
  runtimeState: 'identity-conflict', recoverable: false, process: record, identity,
239
260
  supervisorAlive, hostAlive, health: 'not-checked',
240
261
  reason: 'community_cockpit_terminal_record_with_live_or_unowned_process',
262
+ terminalObservation: null,
241
263
  };
242
264
  }
243
265
  if (!supervisorAlive && !hostAlive) {
@@ -246,6 +268,7 @@ async function observeCockpit(params) {
246
268
  runtimeState: 'crashed', recoverable: true, process: record, identity,
247
269
  supervisorAlive: false, hostAlive: false, health: 'not-checked',
248
270
  reason: 'community_cockpit_process_disappeared',
271
+ terminalObservation: terminalObservation(record, params.now().toISOString(), true),
249
272
  };
250
273
  }
251
274
  if (record.instanceName !== params.state.instanceName) {
@@ -254,6 +277,7 @@ async function observeCockpit(params) {
254
277
  runtimeState: 'identity-conflict', recoverable: false, process: record, identity,
255
278
  supervisorAlive, hostAlive, health: 'not-checked',
256
279
  reason: 'community_cockpit_process_instance_mismatch',
280
+ terminalObservation: null,
257
281
  };
258
282
  }
259
283
  if (!identity) {
@@ -262,6 +286,7 @@ async function observeCockpit(params) {
262
286
  runtimeState: 'identity-conflict', recoverable: false, process: record, identity: null,
263
287
  supervisorAlive, hostAlive, health: 'not-checked',
264
288
  reason: 'community_cockpit_live_process_without_matching_identity',
289
+ terminalObservation: null,
265
290
  };
266
291
  }
267
292
  if (!supervisorAlive && hostAlive) {
@@ -269,6 +294,7 @@ async function observeCockpit(params) {
269
294
  ...base,
270
295
  runtimeState: 'orphaned', recoverable: false, process: record, identity,
271
296
  supervisorAlive, hostAlive, health: 'not-checked', reason: 'community_cockpit_host_orphaned',
297
+ terminalObservation: null,
272
298
  };
273
299
  }
274
300
  if (supervisorAlive && !hostAlive) {
@@ -276,6 +302,7 @@ async function observeCockpit(params) {
276
302
  ...base,
277
303
  runtimeState: 'unhealthy', recoverable: false, process: record, identity,
278
304
  supervisorAlive, hostAlive, health: 'unhealthy', reason: 'community_cockpit_host_process_missing',
305
+ terminalObservation: null,
279
306
  };
280
307
  }
281
308
  const healthy = await params.runtime.health(`${base.origin}/_aops/cockpit-health`);
@@ -289,6 +316,7 @@ async function observeCockpit(params) {
289
316
  hostAlive,
290
317
  health: healthy ? 'healthy' : 'unhealthy',
291
318
  reason: healthy ? null : 'community_cockpit_health_failed',
319
+ terminalObservation: null,
292
320
  };
293
321
  }
294
322
  export async function inspectCommunityNativeCockpit(params = {}) {
@@ -302,6 +330,7 @@ export async function inspectCommunityNativeCockpit(params = {}) {
302
330
  paths,
303
331
  processRecord: readProcess(paths),
304
332
  runtime: params.runtime ?? communityNativeRuntime,
333
+ now: params.now ?? (() => new Date()),
305
334
  });
306
335
  }
307
336
  function cockpitEnvironment(params) {
@@ -409,8 +438,9 @@ async function startCommunityNativeCockpitUnlocked(params = {}) {
409
438
  const state = install.state;
410
439
  const paths = resolveCommunityCockpitPaths(params);
411
440
  const runtime = params.runtime ?? communityNativeRuntime;
441
+ const now = params.now ?? (() => new Date());
412
442
  const prior = readProcess(paths);
413
- const observed = await observeCockpit({ state, paths, processRecord: prior, runtime });
443
+ const observed = await observeCockpit({ state, paths, processRecord: prior, runtime, now });
414
444
  if (observed.runtimeState === 'running') {
415
445
  return {
416
446
  action: 'already-running', state, paths, process: observed.process,
@@ -426,7 +456,6 @@ async function startCommunityNativeCockpitUnlocked(params = {}) {
426
456
  removeControlIfOwned(paths, prior.launchId);
427
457
  mkdirSync(paths.logRoot, { recursive: true, mode: 0o700 });
428
458
  assertPaths(paths);
429
- const now = params.now ?? (() => new Date());
430
459
  const launchId = (params.createId ?? randomUUID)();
431
460
  const startedAt = now().toISOString();
432
461
  const handle = await runtime.spawnChild({
@@ -507,6 +536,7 @@ async function stopCommunityNativeCockpitUnlocked(params = {}) {
507
536
  paths,
508
537
  processRecord: readProcess(paths),
509
538
  runtime,
539
+ now,
510
540
  });
511
541
  if (observed.runtimeState === 'stopped' || observed.runtimeState === 'crashed') {
512
542
  return { status: 'already-stopped', state: install.state, process: observed.process };
@@ -42,7 +42,7 @@
42
42
  "capabilityClass": "windows-ntfs-crash-recoverable-v1",
43
43
  "qualification": {
44
44
  "relativePath": "qualifications/win32-x64.json",
45
- "sha256": "08d7ccc099bcc86e1e4a8cea6ea0d7b50eee76f3a126d6020bf11b2b2edacb3c"
45
+ "sha256": "fdbdfc275dd6053bb6609a9d2aaad6728fee6f7777802ae08c3cb77e2b0d5d50"
46
46
  }
47
47
  }
48
48
  ]
@@ -7,7 +7,7 @@
7
7
  "filesystem": "NTFS",
8
8
  "primitiveProfile": "windows-createfile-flush-movefileex-v1",
9
9
  "capabilityClass": "windows-ntfs-crash-recoverable-v1",
10
- "evidenceSha256": "dd80c815d10feb9499911bc5eb9c3bf56964afe66faa894ebc9de478cb0e5311",
10
+ "evidenceSha256": "76057d52ed4b1f01ed24fbbaa1014b6be4d1db044bb2f5cdc0e6c77524f7a28c",
11
11
  "crashInjectionPoints": [
12
12
  "authority-history",
13
13
  "authority-current",
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aopslabs/aops",
3
- "version": "0.3.17",
3
+ "version": "0.3.19",
4
4
  "type": "module",
5
5
  "description": "AOPS CLI and terminal setup application.",
6
6
  "aopsDockerServerVersion": "0.2.15",
@@ -44,6 +44,7 @@
44
44
  "test:package-identity": "node --test test/package-identity.test.mjs test/npm-tar-entry-mode.test.mjs",
45
45
  "test:tui-launcher": "node --test test/tui-launcher.test.mjs",
46
46
  "test:global-update": "node --test test/global-update.test.mjs test/cli-error-format.test.mjs",
47
+ "test:global-update-public-bridge": "node scripts/verify-global-update-public-bridge.mjs",
47
48
  "test:skill-discovery": "node --test test/skill-discovery.test.mjs",
48
49
  "test:target-auth": "node --test test/target-auth.test.mjs test/device-login.test.mjs",
49
50
  "test:target-transport": "node --test test/target-transport.test.mjs",