@aopslabs/aops 0.3.11 → 0.3.13

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 (36) hide show
  1. package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/aops-cli.md +17 -0
  2. package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/aops-system.md +24 -0
  3. package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/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 +26 -24
  8. package/assets/agent-assets/core/user-guides/aops-cli.md +17 -0
  9. package/assets/agent-assets/core/user-guides/aops-system.md +24 -0
  10. package/dist/commands/global-update.js +623 -0
  11. package/dist/lib/agent-assets/native-fs.js +2 -2
  12. package/dist/main.js +2 -0
  13. package/native/manifest.json +1 -1
  14. package/native/qualifications/win32-x64.json +1 -1
  15. package/native/tui/darwin-arm64/aops-tui +0 -0
  16. package/native/tui/win32-x64/aops-tui.exe +0 -0
  17. package/package.json +5 -4
  18. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/SKILL.md +0 -0
  19. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/agentspace/SKILL.md +0 -0
  20. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/aops-cli-core/SKILL.md +0 -0
  21. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/chatv3/SKILL.md +0 -0
  22. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/collaborative-work/SKILL.md +0 -0
  23. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/discuss/SKILL.md +0 -0
  24. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/docman/SKILL.md +0 -0
  25. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/mission/SKILL.md +0 -0
  26. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/projectman/SKILL.md +0 -0
  27. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/sys/SKILL.md +0 -0
  28. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/view/SKILL.md +0 -0
  29. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/references/working-disciplines/SKILL.md +0 -0
  30. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/agent-assets.md +0 -0
  31. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/agentspace.md +0 -0
  32. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/chatv3.md +0 -0
  33. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/docman.md +0 -0
  34. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/projectman.md +0 -0
  35. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/sys.md +0 -0
  36. /package/agent-assets-release/agent-assets/core/{070b0d3126c645890845791b67157fdd9f43a482fda66583e84fb159b30689a5 → ec1a663c171f7b847980a06dc84ce045493880f8a71d9d2e992d9bcaa31407ff}/files/user-guides/working-disciplines.md +0 -0
@@ -0,0 +1,623 @@
1
+ import assert from 'node:assert/strict';
2
+ import { spawnSync } from 'node:child_process';
3
+ import { createHash } from 'node:crypto';
4
+ import { existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, writeFileSync, } from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+ import { compareCanonicalText } from '@aopslabs/artifact-trust-contracts';
8
+ import { Command } from 'commander';
9
+ const CHECK_CONTRACT = 'aops.global-update-check/v1';
10
+ const PLAN_CONTRACT = 'aops.global-update-plan/v1';
11
+ const RESULT_CONTRACT = 'aops.global-update-result/v1';
12
+ const PUBLIC_REGISTRY = 'https://registry.npmjs.org';
13
+ const EXACT_STABLE_VERSION = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/u;
14
+ const PLAN_ID = /^sha256:[a-f0-9]{64}$/u;
15
+ const SHA256 = /^[a-f0-9]{64}$/u;
16
+ const SHA512_INTEGRITY = /^sha512-[A-Za-z0-9+/]+={0,2}$/u;
17
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
18
+ const PACKAGE_NAMES = ['@aopslabs/aops', '@aopslabs/aops-server'];
19
+ const HEALTH_SAMPLES = 4;
20
+ const HEALTH_INTERVAL_MS = 3_000;
21
+ const HEALTH_STARTUP_ATTEMPTS = 30;
22
+ const HEALTH_STARTUP_INTERVAL_MS = 2_000;
23
+ function cleanEnvironment() {
24
+ return {
25
+ ...process.env,
26
+ NPM_TOKEN: '',
27
+ NODE_AUTH_TOKEN: '',
28
+ GH_TOKEN: '',
29
+ GITHUB_TOKEN: '',
30
+ NPM_CONFIG_REGISTRY: PUBLIC_REGISTRY,
31
+ NPM_CONFIG_USERCONFIG: os.devNull,
32
+ npm_config_registry: PUBLIC_REGISTRY,
33
+ npm_config_userconfig: os.devNull,
34
+ };
35
+ }
36
+ function defaultRunner(command, args, cwd = process.cwd()) {
37
+ const result = spawnSync(command, args, {
38
+ cwd,
39
+ encoding: 'utf8',
40
+ shell: false,
41
+ windowsHide: true,
42
+ maxBuffer: 32 * 1024 * 1024,
43
+ env: cleanEnvironment(),
44
+ });
45
+ if (result.error)
46
+ throw result.error;
47
+ assert.equal(result.status, 0, `${command} ${args.join(' ')} failed\n${result.stdout}\n${result.stderr}`);
48
+ return result.stdout;
49
+ }
50
+ export function resolveNpmInvocation(options = {}) {
51
+ const platform = options.platform ?? process.platform;
52
+ if (platform !== 'win32')
53
+ return { command: 'npm', prefixArgs: [] };
54
+ const nodeExecutable = path.resolve(options.nodeExecutable ?? process.execPath);
55
+ const environment = options.environment ?? process.env;
56
+ const fileExists = options.fileExists ?? existsSync;
57
+ const candidates = [
58
+ path.join(path.dirname(nodeExecutable), 'node_modules', 'npm', 'bin', 'npm-cli.js'),
59
+ ];
60
+ const npmExecPath = environment.npm_execpath?.trim();
61
+ if (npmExecPath && /^npm-cli\.(?:c?js)$/iu.test(path.basename(npmExecPath))) {
62
+ candidates.push(path.resolve(npmExecPath));
63
+ }
64
+ for (const entry of String(environment.PATH ?? '').split(path.delimiter)) {
65
+ if (!entry.trim())
66
+ continue;
67
+ candidates.push(path.resolve(entry, 'node_modules', 'npm', 'bin', 'npm-cli.js'));
68
+ }
69
+ const npmCli = [...new Set(candidates)].find((candidate) => fileExists(candidate));
70
+ assert.ok(npmCli, 'windows_npm_cli_not_found');
71
+ return { command: nodeExecutable, prefixArgs: [npmCli] };
72
+ }
73
+ function runNpm(run, args, cwd) {
74
+ const invocation = resolveNpmInvocation();
75
+ return run(invocation.command, [...invocation.prefixArgs, ...args], cwd);
76
+ }
77
+ export function publicNpmArgs(args) {
78
+ return [
79
+ ...args,
80
+ `--registry=${PUBLIC_REGISTRY}`,
81
+ `--@aopslabs:registry=${PUBLIC_REGISTRY}`,
82
+ `--userconfig=${os.devNull}`,
83
+ ];
84
+ }
85
+ function parseJson(value, label) {
86
+ try {
87
+ return JSON.parse(value);
88
+ }
89
+ catch {
90
+ throw new Error(`${label}_invalid_json`);
91
+ }
92
+ }
93
+ function exactVersion(value, label) {
94
+ if (typeof value !== 'string')
95
+ throw new Error(`${label} must be a string`);
96
+ assert.match(value, EXACT_STABLE_VERSION, `${label} must be an exact stable version`);
97
+ return value;
98
+ }
99
+ export function compareStableVersions(left, right) {
100
+ const leftParts = exactVersion(left, 'left version').split('.').map(Number);
101
+ const rightParts = exactVersion(right, 'right version').split('.').map(Number);
102
+ for (let index = 0; index < 3; index += 1) {
103
+ if (leftParts[index] !== rightParts[index])
104
+ return leftParts[index] - rightParts[index];
105
+ }
106
+ return 0;
107
+ }
108
+ function sha256(file) {
109
+ return createHash('sha256').update(readFileSync(file)).digest('hex');
110
+ }
111
+ function plainDirectory(candidate, label) {
112
+ const absolute = path.resolve(candidate);
113
+ const stat = lstatSync(absolute);
114
+ assert.equal(stat.isDirectory() && !stat.isSymbolicLink(), true, `${label} must be a plain directory`);
115
+ return realpathSync.native(absolute);
116
+ }
117
+ function plainFile(candidate, label) {
118
+ const absolute = path.resolve(candidate);
119
+ const stat = lstatSync(absolute);
120
+ assert.equal(stat.isFile() && !stat.isSymbolicLink(), true, `${label} must be a plain file`);
121
+ return realpathSync.native(absolute);
122
+ }
123
+ function ensureWithin(parent, candidate, label) {
124
+ const relative = path.relative(parent, candidate);
125
+ assert.equal(Boolean(relative) && !relative.startsWith('..') && !path.isAbsolute(relative), true, `${label} must stay inside ${parent}`);
126
+ return candidate;
127
+ }
128
+ function packageManifest(packageRoot, expectedName) {
129
+ const manifestPath = plainFile(path.join(packageRoot, 'package.json'), `${expectedName} manifest`);
130
+ const manifest = parseJson(readFileSync(manifestPath, 'utf8'), `${expectedName}_manifest`);
131
+ assert.equal(manifest.name, expectedName, `${expectedName} package identity mismatch`);
132
+ exactVersion(manifest.version, `${expectedName} version`);
133
+ return manifest;
134
+ }
135
+ function inspectGlobalInstall(run) {
136
+ const globalPrefix = plainDirectory(runNpm(run, ['prefix', '--global']).trim(), 'global npm prefix');
137
+ const globalRoot = plainDirectory(runNpm(run, ['root', '--global']).trim(), 'global npm root');
138
+ ensureWithin(globalPrefix, globalRoot, 'global npm root');
139
+ const cliRoot = path.join(globalRoot, '@aopslabs', 'aops');
140
+ const cli = packageManifest(cliRoot, '@aopslabs/aops');
141
+ const nestedServerRoot = path.join(cliRoot, 'node_modules', '@aopslabs', 'aops-server');
142
+ const dependencies = cli.dependencies;
143
+ const exactServerDependency = exactVersion(dependencies?.['@aopslabs/aops-server'], 'installed CLI Server dependency');
144
+ const topLevelServerRoot = path.join(globalRoot, '@aopslabs', 'aops-server');
145
+ const nestedServer = existsSync(path.join(nestedServerRoot, 'package.json'));
146
+ const topLevelServer = existsSync(path.join(topLevelServerRoot, 'package.json'));
147
+ assert.equal(nestedServer || topLevelServer, true, 'global AOPS Server package was not found');
148
+ for (const [serverRoot, placement] of [
149
+ [nestedServerRoot, 'nested'],
150
+ [topLevelServerRoot, 'top-level'],
151
+ ]) {
152
+ if (!existsSync(path.join(serverRoot, 'package.json')))
153
+ continue;
154
+ const server = packageManifest(serverRoot, '@aopslabs/aops-server');
155
+ assert.equal(exactVersion(server.version, `installed ${placement} Server version`), exactServerDependency, `installed ${placement} Server does not match the CLI exact dependency`);
156
+ }
157
+ const cliPath = plainFile(path.join(cliRoot, 'dist', 'main.js'), 'global AOPS CLI entrypoint');
158
+ const serverPath = plainDirectory(nestedServer ? nestedServerRoot : topLevelServerRoot, 'global AOPS Server root');
159
+ return {
160
+ globalPrefix,
161
+ globalRoot,
162
+ cliPath,
163
+ serverPath,
164
+ packages: {
165
+ cli: exactVersion(cli.version, 'installed CLI version'),
166
+ server: exactServerDependency,
167
+ },
168
+ topology: { nestedServer, topLevelServer },
169
+ };
170
+ }
171
+ function npmViewString(run, selector, field) {
172
+ const value = parseJson(runNpm(run, publicNpmArgs(['view', selector, field, '--json'])).trim(), `npm_view_${field}`);
173
+ if (typeof value !== 'string')
174
+ throw new Error(`${selector} ${field} must be a string`);
175
+ return value;
176
+ }
177
+ function availableClosure(run) {
178
+ const cli = exactVersion(npmViewString(run, '@aopslabs/aops', 'dist-tags.latest'), 'latest CLI version');
179
+ const server = exactVersion(npmViewString(run, `@aopslabs/aops@${cli}`, 'dependencies.@aopslabs/aops-server'), 'CLI Server dependency');
180
+ return { cli, server };
181
+ }
182
+ export function inspectGlobalUpdate(run = defaultRunner) {
183
+ const installation = inspectGlobalInstall(run);
184
+ const installed = installation.packages;
185
+ const available = availableClosure(run);
186
+ assert.equal(compareStableVersions(available.cli, installed.cli) >= 0, true, 'public npm latest must not downgrade the installed CLI');
187
+ const cliVersionDelta = compareStableVersions(available.cli, installed.cli);
188
+ const cliUpdateAvailable = cliVersionDelta > 0;
189
+ const updateAvailable = cliUpdateAvailable ||
190
+ !installation.topology.nestedServer ||
191
+ installation.topology.topLevelServer;
192
+ assert.equal(compareStableVersions(available.server, installed.server) >= 0 || cliUpdateAvailable, true, 'latest CLI closure would unexpectedly downgrade the installed Server without a CLI update');
193
+ return {
194
+ schemaVersion: 1,
195
+ contract: CHECK_CONTRACT,
196
+ status: updateAvailable ? 'update-available' : 'current',
197
+ installed,
198
+ available,
199
+ updateAvailable,
200
+ installedTopology: installation.topology,
201
+ registry: PUBLIC_REGISTRY,
202
+ };
203
+ }
204
+ function runInstalledJson(run, cliPath, args) {
205
+ return parseJson(run(process.execPath, [cliPath, ...args]).trim(), args.join('_'));
206
+ }
207
+ function serviceSnapshot(run, cliPath) {
208
+ return {
209
+ server: runInstalledJson(run, cliPath, ['server', 'status', '--json']),
210
+ cockpit: runInstalledJson(run, cliPath, ['cockpit', 'status', '--json']),
211
+ };
212
+ }
213
+ function assertHealthyServices(services) {
214
+ assert.equal(services.server.runtimeState, 'running', 'AOPS Server must be running before update');
215
+ assert.equal(services.server.liveness?.health, 'healthy', 'AOPS Server must be healthy before update');
216
+ assert.equal(services.cockpit.runtimeState, 'running', 'AOPS Cockpit must be running before update');
217
+ assert.equal(services.cockpit.health, 'healthy', 'AOPS Cockpit must be healthy before update');
218
+ }
219
+ function packRegistryPackage(run, destination, name, version) {
220
+ const packed = parseJson(runNpm(run, publicNpmArgs([
221
+ 'pack',
222
+ `${name}@${version}`,
223
+ '--ignore-scripts',
224
+ '--json',
225
+ '--pack-destination',
226
+ destination,
227
+ ])), 'npm_pack');
228
+ assert.equal(packed.length, 1, `${name}@${version} must produce one tarball`);
229
+ assert.equal(packed[0].name, name);
230
+ assert.equal(packed[0].version, version);
231
+ const registryIntegrity = npmViewString(run, `${name}@${version}`, 'dist.integrity');
232
+ assert.equal(packed[0].integrity, registryIntegrity, `${name}@${version} integrity mismatch`);
233
+ const file = plainFile(path.join(destination, packed[0].filename), `${name}@${version} tarball`);
234
+ return { name, version, path: file, integrity: registryIntegrity, sha256: sha256(file) };
235
+ }
236
+ function canonicalJson(value) {
237
+ if (Array.isArray(value))
238
+ return value.map((entry) => canonicalJson(entry));
239
+ if (value !== null && typeof value === 'object') {
240
+ return Object.fromEntries(Object.entries(value)
241
+ .sort(([left], [right]) => compareCanonicalText(left, right))
242
+ .map(([key, entry]) => [key, canonicalJson(entry)]));
243
+ }
244
+ return value;
245
+ }
246
+ function canonicalPlanBody(plan) {
247
+ return JSON.stringify(canonicalJson(plan));
248
+ }
249
+ export function computeGlobalUpdatePlanId(plan) {
250
+ return `sha256:${createHash('sha256').update(canonicalPlanBody(plan)).digest('hex')}`;
251
+ }
252
+ function defaultUpdateRoot(configRoot) {
253
+ const root = configRoot?.trim()
254
+ ? path.resolve(configRoot)
255
+ : path.join(os.homedir(), '.aops');
256
+ if (!existsSync(root))
257
+ mkdirSync(root, { recursive: true, mode: 0o700 });
258
+ return plainDirectory(root, 'AOPS config root');
259
+ }
260
+ export function prepareGlobalUpdate(options = {}, run = defaultRunner) {
261
+ const check = inspectGlobalUpdate(run);
262
+ assert.equal(check.updateAvailable, true, 'aops_global_update_not_available');
263
+ const installed = inspectGlobalInstall(run);
264
+ const services = serviceSnapshot(run, installed.cliPath);
265
+ assertHealthyServices(services);
266
+ const configRoot = defaultUpdateRoot(options.configRoot);
267
+ const updatesRoot = path.join(configRoot, 'global-updates');
268
+ mkdirSync(updatesRoot, { recursive: true, mode: 0o700 });
269
+ const operationRoot = path.join(updatesRoot, `${Date.now()}-${process.pid}`);
270
+ mkdirSync(operationRoot, { recursive: false, mode: 0o700 });
271
+ const candidateRoot = path.join(operationRoot, 'candidate');
272
+ const restoreRoot = path.join(operationRoot, 'restore');
273
+ const cacheRoot = path.join(operationRoot, 'cache');
274
+ mkdirSync(candidateRoot, { mode: 0o700 });
275
+ mkdirSync(restoreRoot, { mode: 0o700 });
276
+ mkdirSync(cacheRoot, { mode: 0o700 });
277
+ const candidates = [
278
+ packRegistryPackage(run, candidateRoot, '@aopslabs/aops', check.available.cli),
279
+ packRegistryPackage(run, candidateRoot, '@aopslabs/aops-server', check.available.server),
280
+ ];
281
+ const restorePackages = [
282
+ packRegistryPackage(run, restoreRoot, '@aopslabs/aops', check.installed.cli),
283
+ packRegistryPackage(run, restoreRoot, '@aopslabs/aops-server', check.installed.server),
284
+ ];
285
+ const body = {
286
+ schemaVersion: 1,
287
+ contract: PLAN_CONTRACT,
288
+ createdAt: new Date().toISOString(),
289
+ globalPrefix: installed.globalPrefix,
290
+ globalRoot: installed.globalRoot,
291
+ cliPath: installed.cliPath,
292
+ operationRoot,
293
+ prior: { packages: installed.packages, topology: installed.topology, services },
294
+ target: {
295
+ packages: check.available,
296
+ topology: { nestedServer: true, topLevelServer: false },
297
+ },
298
+ candidates,
299
+ restorePackages,
300
+ safety: {
301
+ candidateIntegrity: 'verified-from-public-registry',
302
+ restoreIntegrity: 'verified-from-public-registry',
303
+ automaticRollback: true,
304
+ requiresExactConfirmation: true,
305
+ healthSamples: HEALTH_SAMPLES,
306
+ healthIntervalMs: HEALTH_INTERVAL_MS,
307
+ healthStartupAttempts: HEALTH_STARTUP_ATTEMPTS,
308
+ healthStartupIntervalMs: HEALTH_STARTUP_INTERVAL_MS,
309
+ },
310
+ };
311
+ const plan = { ...body, planId: computeGlobalUpdatePlanId(body) };
312
+ const planPath = path.join(operationRoot, 'update-plan.json');
313
+ writeFileSync(planPath, `${JSON.stringify(plan, null, 2)}\n`, { flag: 'wx', mode: 0o600 });
314
+ return { check, plan, planPath };
315
+ }
316
+ function packageRecordByName(records, name) {
317
+ const matches = records.filter((record) => record.name === name);
318
+ assert.equal(matches.length, 1, `update plan must contain exactly one ${name} tarball`);
319
+ return matches[0];
320
+ }
321
+ function validateRecordSet(records, expected, root, label) {
322
+ assert.equal(records.length, PACKAGE_NAMES.length, `${label} must contain exactly two packages`);
323
+ for (const [name, version] of [
324
+ ['@aopslabs/aops', expected.cli],
325
+ ['@aopslabs/aops-server', expected.server],
326
+ ]) {
327
+ const record = packageRecordByName(records, name);
328
+ assert.equal(exactVersion(record.version, `${name} ${label} version`), version);
329
+ assert.match(record.integrity, SHA512_INTEGRITY, `${name} ${label} integrity is invalid`);
330
+ assert.match(record.sha256, SHA256, `${name} ${label} sha256 is invalid`);
331
+ const file = plainFile(record.path, `${name} ${label} tarball`);
332
+ ensureWithin(root, file, `${name} ${label} tarball`);
333
+ assert.equal(file, record.path, `${name} ${label} tarball must be canonical`);
334
+ assert.equal(sha256(file), record.sha256, `${name} tarball changed after planning`);
335
+ }
336
+ }
337
+ export function readGlobalUpdatePlan(planPath) {
338
+ const resolved = plainFile(planPath, 'global update plan');
339
+ const plan = parseJson(readFileSync(resolved, 'utf8'), 'global_update_plan');
340
+ assert.equal(plan.schemaVersion, 1);
341
+ assert.equal(plan.contract, PLAN_CONTRACT);
342
+ assert.match(plan.planId, PLAN_ID);
343
+ const { planId, ...body } = plan;
344
+ assert.equal(planId, computeGlobalUpdatePlanId(body), 'global update plan identity mismatch');
345
+ plainDirectory(plan.globalPrefix, 'planned global npm prefix');
346
+ plainDirectory(plan.globalRoot, 'planned global npm root');
347
+ ensureWithin(plan.globalPrefix, plan.globalRoot, 'planned global npm root');
348
+ const operationRoot = plainDirectory(plan.operationRoot, 'global update operation root');
349
+ assert.equal(operationRoot, path.dirname(resolved), 'global update plan must stay at its operation root');
350
+ assert.deepEqual(plan.target.topology, { nestedServer: true, topLevelServer: false });
351
+ exactVersion(plan.prior.packages.cli, 'prior CLI version');
352
+ exactVersion(plan.prior.packages.server, 'prior Server version');
353
+ exactVersion(plan.target.packages.cli, 'target CLI version');
354
+ exactVersion(plan.target.packages.server, 'target Server version');
355
+ validateRecordSet(plan.candidates, plan.target.packages, operationRoot, 'candidate set');
356
+ validateRecordSet(plan.restorePackages, plan.prior.packages, operationRoot, 'restore set');
357
+ assert.equal(plan.safety.candidateIntegrity, 'verified-from-public-registry');
358
+ assert.equal(plan.safety.restoreIntegrity, 'verified-from-public-registry');
359
+ assert.equal(plan.safety.automaticRollback, true);
360
+ assert.equal(plan.safety.requiresExactConfirmation, true);
361
+ assert.equal(plan.safety.healthSamples, HEALTH_SAMPLES);
362
+ assert.equal(plan.safety.healthIntervalMs, HEALTH_INTERVAL_MS);
363
+ assert.equal(plan.safety.healthStartupAttempts, HEALTH_STARTUP_ATTEMPTS);
364
+ assert.equal(plan.safety.healthStartupIntervalMs, HEALTH_STARTUP_INTERVAL_MS);
365
+ return plan;
366
+ }
367
+ export function installGlobalUpdateClosure(run, plan, records, topology) {
368
+ const cli = packageRecordByName(records, '@aopslabs/aops');
369
+ const server = packageRecordByName(records, '@aopslabs/aops-server');
370
+ const cacheRoot = path.join(plan.operationRoot, 'cache');
371
+ mkdirSync(cacheRoot, { recursive: true, mode: 0o700 });
372
+ runNpm(run, publicNpmArgs(['cache', 'add', server.path, `--cache=${cacheRoot}`]));
373
+ const topLevelServerRoot = path.join(plan.globalRoot, '@aopslabs', 'aops-server');
374
+ if (existsSync(path.join(topLevelServerRoot, 'package.json'))) {
375
+ runNpm(run, publicNpmArgs([
376
+ '--prefix',
377
+ plan.globalPrefix,
378
+ 'uninstall',
379
+ '--global',
380
+ '--ignore-scripts',
381
+ '--fund=false',
382
+ '--audit=false',
383
+ '@aopslabs/aops-server',
384
+ ]));
385
+ }
386
+ const commonInstallArgs = [
387
+ '--prefix',
388
+ plan.globalPrefix,
389
+ 'install',
390
+ '--global',
391
+ '--ignore-scripts',
392
+ '--prefer-offline',
393
+ `--cache=${cacheRoot}`,
394
+ '--fund=false',
395
+ '--audit=false',
396
+ ];
397
+ if (topology.topLevelServer && !topology.nestedServer) {
398
+ runNpm(run, publicNpmArgs([...commonInstallArgs, cli.path, server.path]));
399
+ return;
400
+ }
401
+ runNpm(run, publicNpmArgs([...commonInstallArgs, cli.path]));
402
+ if (topology.topLevelServer) {
403
+ runNpm(run, publicNpmArgs([
404
+ ...commonInstallArgs,
405
+ server.path,
406
+ ]));
407
+ }
408
+ }
409
+ function stopService(run, cliPath, service, bestEffort = false) {
410
+ try {
411
+ run(process.execPath, [cliPath, service, 'stop', '--json']);
412
+ }
413
+ catch (error) {
414
+ if (!bestEffort)
415
+ throw error;
416
+ }
417
+ }
418
+ function startService(run, cliPath, service) {
419
+ run(process.execPath, [cliPath, service, 'start', '--json']);
420
+ }
421
+ function updateServerApplication(run, cliPath, expected) {
422
+ const result = runInstalledJson(run, cliPath, ['server', 'update', '--detach', '--json']);
423
+ assert.equal(result.status, 'community-native-application-updated');
424
+ assert.equal(result.priorReleaseVersion, expected.prior);
425
+ assert.equal(result.releaseVersion, expected.target);
426
+ assert.equal(typeof result.updateId, 'string');
427
+ assert.match(result.updateId, UUID);
428
+ return result.updateId;
429
+ }
430
+ function rollbackServerApplication(run, cliPath, updateId, serverPath, expectedRelease) {
431
+ const result = runInstalledJson(run, cliPath, [
432
+ 'server',
433
+ 'rollback',
434
+ 'app',
435
+ '--update-id',
436
+ updateId,
437
+ '--source-root',
438
+ serverPath,
439
+ '--detach',
440
+ '--json',
441
+ ]);
442
+ assert.equal(result.status, 'community-native-application-rolled-back');
443
+ assert.equal(result.updateId, updateId);
444
+ assert.equal(result.releaseVersion, expectedRelease);
445
+ }
446
+ const defaultExecutionDependencies = {
447
+ fetch: globalThis.fetch.bind(globalThis),
448
+ sleep: (milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)),
449
+ };
450
+ class StartupHealthPending extends Error {
451
+ constructor(cause) {
452
+ super('global_update_health_startup_pending', { cause });
453
+ }
454
+ }
455
+ async function assertStableHealth(run, plan, execution) {
456
+ const evidence = [];
457
+ const collectSample = async (sample, startup) => {
458
+ const installed = inspectGlobalInstall(run);
459
+ let services;
460
+ try {
461
+ services = serviceSnapshot(run, installed.cliPath);
462
+ assertHealthyServices(services);
463
+ }
464
+ catch (error) {
465
+ if (startup)
466
+ throw new StartupHealthPending(error);
467
+ throw error;
468
+ }
469
+ const serverOrigin = validateLoopbackOrigin(services.server.origin, 'AOPS Server origin');
470
+ const cockpitOrigin = validateLoopbackOrigin(services.cockpit.origin, 'AOPS Cockpit origin');
471
+ const endpoints = [];
472
+ for (const [name, url] of [
473
+ ['server', `${serverOrigin}/api/health`],
474
+ ['cockpit', cockpitOrigin],
475
+ ['cockpit-proxy', `${cockpitOrigin}/api/health`],
476
+ ]) {
477
+ let response;
478
+ try {
479
+ response = await execution.fetch(url, { cache: 'no-store', signal: AbortSignal.timeout(15_000) });
480
+ }
481
+ catch (error) {
482
+ if (startup)
483
+ throw new StartupHealthPending(error);
484
+ throw error;
485
+ }
486
+ if (response.status !== 200 && startup) {
487
+ throw new StartupHealthPending(new Error(`${name} health returned ${response.status}`));
488
+ }
489
+ assert.equal(response.status, 200, `${name} health returned ${response.status}`);
490
+ const body = await response.arrayBuffer();
491
+ endpoints.push({ name, url, status: response.status, bytes: body.byteLength });
492
+ }
493
+ return { sample, observedAt: new Date().toISOString(), endpoints };
494
+ };
495
+ let startupError;
496
+ for (let attempt = 1; attempt <= plan.safety.healthStartupAttempts; attempt += 1) {
497
+ try {
498
+ evidence.push(await collectSample(1, true));
499
+ startupError = undefined;
500
+ break;
501
+ }
502
+ catch (error) {
503
+ if (!(error instanceof StartupHealthPending))
504
+ throw error;
505
+ startupError = error.cause ?? error;
506
+ if (attempt < plan.safety.healthStartupAttempts) {
507
+ await execution.sleep(plan.safety.healthStartupIntervalMs);
508
+ }
509
+ }
510
+ }
511
+ if (startupError)
512
+ throw startupError;
513
+ for (let sample = 2; sample <= plan.safety.healthSamples; sample += 1) {
514
+ await execution.sleep(plan.safety.healthIntervalMs);
515
+ evidence.push(await collectSample(sample, false));
516
+ }
517
+ return evidence;
518
+ }
519
+ export function validateLoopbackOrigin(value, label) {
520
+ assert.equal(typeof value, 'string', `${label} must be a string`);
521
+ const url = new URL(value);
522
+ assert.equal(url.protocol, 'http:', `${label} must use http`);
523
+ assert.equal(url.username, '', `${label} must not include credentials`);
524
+ assert.equal(url.password, '', `${label} must not include credentials`);
525
+ assert.equal(['127.0.0.1', 'localhost', '[::1]'].includes(url.hostname), true, `${label} must be loopback`);
526
+ assert.equal(url.pathname, '/', `${label} must not include a path`);
527
+ assert.equal(url.search, '', `${label} must not include a query`);
528
+ assert.equal(url.hash, '', `${label} must not include a fragment`);
529
+ return url.origin;
530
+ }
531
+ export async function applyGlobalUpdate(options, run = defaultRunner, execution = defaultExecutionDependencies) {
532
+ const plan = readGlobalUpdatePlan(options.plan);
533
+ assert.equal(options.confirm, plan.planId, 'global update confirmation does not match the exact plan');
534
+ const current = inspectGlobalInstall(run);
535
+ assert.deepEqual(current.packages, plan.prior.packages, 'global packages changed after update planning');
536
+ assert.deepEqual(current.topology, plan.prior.topology, 'global package topology changed after update planning');
537
+ assert.equal(current.globalPrefix, plan.globalPrefix, 'global npm prefix changed after update planning');
538
+ assertHealthyServices(serviceSnapshot(run, current.cliPath));
539
+ const events = [];
540
+ let applicationUpdateId = null;
541
+ try {
542
+ stopService(run, current.cliPath, 'cockpit');
543
+ events.push('stopped:cockpit');
544
+ stopService(run, current.cliPath, 'server');
545
+ events.push('stopped:server');
546
+ installGlobalUpdateClosure(run, plan, plan.candidates, plan.target.topology);
547
+ events.push('installed:candidate');
548
+ const updated = inspectGlobalInstall(run);
549
+ assert.deepEqual(updated.packages, plan.target.packages, 'installed candidate closure is not exact');
550
+ assert.deepEqual(updated.topology, plan.target.topology, 'installed candidate topology is not canonical');
551
+ applicationUpdateId = updateServerApplication(run, updated.cliPath, {
552
+ prior: plan.prior.packages.server,
553
+ target: plan.target.packages.server,
554
+ });
555
+ events.push('updated:server');
556
+ startService(run, updated.cliPath, 'cockpit');
557
+ events.push('started:cockpit');
558
+ const healthEvidence = await assertStableHealth(run, plan, execution);
559
+ return {
560
+ schemaVersion: 1,
561
+ contract: RESULT_CONTRACT,
562
+ status: 'updated',
563
+ planId: plan.planId,
564
+ installed: updated.packages,
565
+ rollbackPerformed: false,
566
+ events,
567
+ healthEvidence,
568
+ next: 'Exit and reopen the AOPS TUI to run the newly installed binary.',
569
+ };
570
+ }
571
+ catch (updateError) {
572
+ events.push('rollback:started');
573
+ try {
574
+ stopService(run, plan.cliPath, 'cockpit', true);
575
+ stopService(run, plan.cliPath, 'server', true);
576
+ installGlobalUpdateClosure(run, plan, plan.restorePackages, plan.prior.topology);
577
+ events.push('installed:restore');
578
+ const restored = inspectGlobalInstall(run);
579
+ assert.deepEqual(restored.packages, plan.prior.packages, 'rollback package closure is not exact');
580
+ assert.deepEqual(restored.topology, plan.prior.topology, 'rollback package topology is not exact');
581
+ if (applicationUpdateId) {
582
+ rollbackServerApplication(run, restored.cliPath, applicationUpdateId, restored.serverPath, plan.prior.packages.server);
583
+ events.push('rolled-back:server-application');
584
+ }
585
+ else {
586
+ startService(run, restored.cliPath, 'server');
587
+ events.push('started:server-restore');
588
+ }
589
+ startService(run, restored.cliPath, 'cockpit');
590
+ await assertStableHealth(run, plan, execution);
591
+ events.push('rollback:healthy');
592
+ }
593
+ catch (rollbackError) {
594
+ throw new AggregateError([updateError, rollbackError], `global update and automatic rollback both failed; recovery artifacts remain at ${plan.operationRoot}`);
595
+ }
596
+ throw new Error('global update failed; the prior exact packages were restored and are healthy', { cause: updateError });
597
+ }
598
+ }
599
+ function output(value, json) {
600
+ process.stdout.write(`${JSON.stringify(value, null, json ? 0 : 2)}\n`);
601
+ }
602
+ export function makeGlobalUpdateCommand() {
603
+ const command = new Command('update').description('Check, prepare, and apply a guarded global AOPS npm update');
604
+ command
605
+ .command('check')
606
+ .description('Compare the installed global AOPS closure with the public npm latest tag')
607
+ .option('--json', 'Output JSON')
608
+ .action((options) => output(inspectGlobalUpdate(), options.json === true));
609
+ command
610
+ .command('prepare')
611
+ .description('Download and verify exact candidate and rollback tarballs without changing the installation')
612
+ .option('--config-root <path>', 'AOPS config root for the immutable update plan')
613
+ .option('--json', 'Output JSON')
614
+ .action((options) => output(prepareGlobalUpdate(options), options.json === true));
615
+ command
616
+ .command('apply')
617
+ .description('Apply one exact prepared plan with automatic rollback and health verification')
618
+ .requiredOption('--plan <path>', 'Exact update-plan.json path returned by prepare')
619
+ .requiredOption('--confirm <plan-id>', 'Exact sha256 plan id returned by prepare')
620
+ .option('--json', 'Output JSON')
621
+ .action(async (options) => output(await applyGlobalUpdate(options), options.json === true));
622
+ return command;
623
+ }
@@ -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 "4667a6c7f4b2f6261035103dbfed6e6117a0aba6b5a7d9cd85750a028b6052d6" === 'undefined'
546
+ return typeof "9644b89d6936c82a514f32f74d2c65324d45079255cebc642a4af2e36d482964" === 'undefined'
547
547
  ? undefined
548
- : "4667a6c7f4b2f6261035103dbfed6e6117a0aba6b5a7d9cd85750a028b6052d6";
548
+ : "9644b89d6936c82a514f32f74d2c65324d45079255cebc642a4af2e36d482964";
549
549
  }
550
550
  function sha256(bytes) {
551
551
  return createHash('sha256').update(bytes).digest('hex');
package/dist/main.js CHANGED
@@ -37,6 +37,7 @@ import { makeCommunityCockpitCommand } from './commands/community-cockpit.js';
37
37
  import { makeCommunityDoctorCommand } from './commands/community-doctor.js';
38
38
  import { makeTargetCommand } from './commands/target.js';
39
39
  import { makeVersionCommand } from './commands/version.js';
40
+ import { makeGlobalUpdateCommand } from './commands/global-update.js';
40
41
  import { launchBundledTui, shouldLaunchBundledTui } from './lib/tui-launcher.js';
41
42
  import { CommunityDiagnosticError, formatCommunityDiagnostic, } from './lib/community-diagnostic.js';
42
43
  import { resolveCliApiBaseUrl } from './utils/api.js';
@@ -152,6 +153,7 @@ export function buildCommunityProgram() {
152
153
  program.addCommand(makeCommunityDoctorCommand());
153
154
  program.addCommand(makeTargetCommand());
154
155
  program.addCommand(makeVersionCommand());
156
+ program.addCommand(makeGlobalUpdateCommand());
155
157
  program.addHelpText('after', `
156
158
  AOPS Community is a single-user, self-hosted/local-trusted distribution.
157
159
  Canonical writes go to the local AOPS server; .aops/** remains a read-only cache.
@@ -42,7 +42,7 @@
42
42
  "capabilityClass": "windows-ntfs-crash-recoverable-v1",
43
43
  "qualification": {
44
44
  "relativePath": "qualifications/win32-x64.json",
45
- "sha256": "3c4edb8aa2e3c74627ca16fe9b42a746f11555d5b137abf560eaceb5e939865d"
45
+ "sha256": "f589981718dff79e64aa1171acf66abcb86a662b94015592b22217911752ed58"
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": "69bdba89959089af51117f1d2e5398078c1e4ae1bd850efb1ecb557a58b2fb43",
10
+ "evidenceSha256": "0c390c3a76decbc63fcae8e7d84b56322bf25d2fae728406d8a3dda91f09585e",
11
11
  "crashInjectionPoints": [
12
12
  "authority-history",
13
13
  "authority-current",
Binary file
Binary file