agentic-workflow-manager 9.3.0 → 9.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/commands/plan/index.js +98 -0
- package/dist/src/commands/preflight/checks.js +64 -4
- package/dist/src/commands/preflight/index.js +5 -1
- package/dist/src/core/currentness/check.js +224 -0
- package/dist/src/core/currentness/types.js +2 -0
- package/dist/src/core/plan/json.js +111 -0
- package/dist/src/core/plan/types.js +2 -0
- package/dist/src/core/plan/validate.js +393 -0
- package/dist/src/index.js +7 -1
- package/dist/tests/commands/plan/index.test.js +154 -0
- package/dist/tests/commands/preflight/preflight.test.js +93 -2
- package/dist/tests/core/currentness/check.test.js +293 -0
- package/dist/tests/core/plan/validate.test.js +437 -0
- package/dist/tests/integration/preflight-json-pipe.e2e.test.js +38 -0
- package/dist/tests/structural/r4-plan-currentness-docs.test.js +37 -0
- package/package.json +1 -1
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.exitCodeFor = exitCodeFor;
|
|
4
|
+
exports.formatReport = formatReport;
|
|
5
|
+
exports.registerPlanCommand = registerPlanCommand;
|
|
6
|
+
const validate_1 = require("../../core/plan/validate");
|
|
7
|
+
const SUPPORTED_SCHEMA = 'compact-slices/v1';
|
|
8
|
+
const MAX_PATH_LENGTH = 4096;
|
|
9
|
+
const MAX_DIAGNOSTICS = 20;
|
|
10
|
+
const MAX_DIAGNOSTIC_LENGTH = 4096;
|
|
11
|
+
function assertText(value, name) {
|
|
12
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > MAX_PATH_LENGTH || value.startsWith('--') || /[\u0000-\u001F\u007F-\u009F]/.test(value)) {
|
|
13
|
+
throw new Error(`${name} must be a non-empty path without control characters`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
function terminalSafe(value) {
|
|
17
|
+
return value.replace(/[\u0000-\u001F\u007F-\u009F]/g, character => `\\u${character.codePointAt(0).toString(16).padStart(4, '0')}`);
|
|
18
|
+
}
|
|
19
|
+
function assertDependencies(deps) {
|
|
20
|
+
if (!deps || typeof deps.validatePlanFile !== 'function')
|
|
21
|
+
throw new Error('validatePlanFile must be a function');
|
|
22
|
+
}
|
|
23
|
+
function boundedDiagnostics(diagnostics) {
|
|
24
|
+
if (!Array.isArray(diagnostics))
|
|
25
|
+
throw new Error('plan validator returned invalid diagnostics');
|
|
26
|
+
return diagnostics.slice(0, MAX_DIAGNOSTICS).map((diagnostic) => {
|
|
27
|
+
if (!diagnostic || typeof diagnostic.code !== 'string' || typeof diagnostic.message !== 'string') {
|
|
28
|
+
throw new Error('plan validator returned an invalid diagnostic');
|
|
29
|
+
}
|
|
30
|
+
return {
|
|
31
|
+
code: diagnostic.code.slice(0, MAX_DIAGNOSTIC_LENGTH),
|
|
32
|
+
message: diagnostic.message.slice(0, MAX_DIAGNOSTIC_LENGTH),
|
|
33
|
+
...(typeof diagnostic.field === 'string' ? { field: diagnostic.field.slice(0, MAX_DIAGNOSTIC_LENGTH) } : {}),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function reportPayload(report, planPath) {
|
|
38
|
+
switch (report.state) {
|
|
39
|
+
case 'valid':
|
|
40
|
+
return {
|
|
41
|
+
state: report.state, path: planPath, schema: report.schema, planId: report.manifest.planId,
|
|
42
|
+
requirements: report.manifest.requirements.length, sources: report.manifest.sources.length,
|
|
43
|
+
commands: report.manifest.commands.length, slices: report.manifest.slices.length, completeOwnership: true,
|
|
44
|
+
};
|
|
45
|
+
case 'legacy':
|
|
46
|
+
return { state: report.state, path: planPath, message: 'existing full-quality path applies; compact optimization was not requested.' };
|
|
47
|
+
case 'invalid':
|
|
48
|
+
return { state: report.state, path: planPath, diagnostics: boundedDiagnostics(report.diagnostics) };
|
|
49
|
+
case 'unsupported':
|
|
50
|
+
return {
|
|
51
|
+
state: report.state, path: planPath, schema: report.schema, supportedSchema: SUPPORTED_SCHEMA,
|
|
52
|
+
diagnostics: boundedDiagnostics(report.diagnostics), remedy: 'update AWM to support this compact plan schema.',
|
|
53
|
+
};
|
|
54
|
+
default:
|
|
55
|
+
throw new Error('plan validator returned an unknown report state');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function exitCodeFor(report) {
|
|
59
|
+
return report.state === 'valid' || report.state === 'legacy' ? 0 : 2;
|
|
60
|
+
}
|
|
61
|
+
function formatReport(report, planPath) {
|
|
62
|
+
const payload = reportPayload(report, planPath);
|
|
63
|
+
switch (report.state) {
|
|
64
|
+
case 'valid':
|
|
65
|
+
return `Plan validation: valid "${terminalSafe(planPath)}" (${terminalSafe(report.schema)}; ${payload.slices} slices; ${payload.requirements} requirements; complete ownership)\n`;
|
|
66
|
+
case 'legacy':
|
|
67
|
+
return `Plan validation: legacy "${terminalSafe(planPath)}" — existing full-quality path applies; compact optimization was not requested.\n`;
|
|
68
|
+
case 'invalid':
|
|
69
|
+
return `Plan validation: invalid "${terminalSafe(planPath)}"\n${payload.diagnostics.map(diagnostic => `- ${terminalSafe(diagnostic.code)}: ${terminalSafe(diagnostic.message)}${diagnostic.field ? ` (${terminalSafe(diagnostic.field)})` : ''}`).join('\n')}\n`;
|
|
70
|
+
case 'unsupported':
|
|
71
|
+
return `Plan validation: unsupported "${terminalSafe(planPath)}" (${terminalSafe(report.schema)}; supported: ${SUPPORTED_SCHEMA})\nUpdate AWM to support this compact plan schema.\n`;
|
|
72
|
+
default:
|
|
73
|
+
throw new Error('plan validator returned an unknown report state');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function registerPlanCommand(program, deps = { validatePlanFile: validate_1.validatePlanFile }) {
|
|
77
|
+
if (!program || typeof program.command !== 'function')
|
|
78
|
+
throw new Error('program must be a Commander command');
|
|
79
|
+
assertDependencies(deps);
|
|
80
|
+
const plan = program.command('plan').description('inspect plan contracts');
|
|
81
|
+
plan
|
|
82
|
+
.command('validate <plan-path>')
|
|
83
|
+
.description('validate a compact plan without modifying it')
|
|
84
|
+
.option('--json', 'emit one stable JSON report')
|
|
85
|
+
.option('--cwd <path>', 'repository root for plan containment and source resolution')
|
|
86
|
+
.action((planPath, options) => {
|
|
87
|
+
assertText(planPath, 'plan path');
|
|
88
|
+
const cwd = options.cwd ?? process.cwd();
|
|
89
|
+
assertText(cwd, '--cwd');
|
|
90
|
+
const report = deps.validatePlanFile(planPath, cwd);
|
|
91
|
+
const output = options.json === true
|
|
92
|
+
? `${JSON.stringify(reportPayload(report, planPath))}\n`
|
|
93
|
+
: formatReport(report, planPath);
|
|
94
|
+
process.stdout.write(output);
|
|
95
|
+
const code = exitCodeFor(report);
|
|
96
|
+
process.exitCode = code;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
@@ -15,6 +15,8 @@ const run_1 = require("../sensors/run");
|
|
|
15
15
|
const paths_1 = require("../../core/paths");
|
|
16
16
|
const registries_1 = require("../../core/registries");
|
|
17
17
|
const inspect_1 = require("../../core/context-kernel/inspect");
|
|
18
|
+
const registries_2 = require("../../core/registries");
|
|
19
|
+
const check_1 = require("../../core/currentness/check");
|
|
18
20
|
const MANIFEST = path_1.default.join('.awm', 'sensors.json');
|
|
19
21
|
/**
|
|
20
22
|
* The agent needs project context delivered every session. A repo with neither file
|
|
@@ -34,7 +36,17 @@ function checkContext(cwd) {
|
|
|
34
36
|
return { id: 'context', ok: true, detail: present.join(', ') };
|
|
35
37
|
}
|
|
36
38
|
function checkContextKernel(cwd) {
|
|
37
|
-
|
|
39
|
+
let resolution;
|
|
40
|
+
try {
|
|
41
|
+
resolution = (0, registries_1.resolveProjectContextSchema)();
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return {
|
|
45
|
+
id: 'context-kernel', ok: false, advisory: false,
|
|
46
|
+
detail: 'configured registry inventory could not be read',
|
|
47
|
+
remedy: 'Repair the local registry inventory and rerun preflight.',
|
|
48
|
+
};
|
|
49
|
+
}
|
|
38
50
|
if (resolution.diagnostics.length > 0) {
|
|
39
51
|
return {
|
|
40
52
|
id: 'context-kernel',
|
|
@@ -379,9 +391,46 @@ function checkHost(cwd) {
|
|
|
379
391
|
// Bitbucket, Azure DevOps, an internal git server, etc. — don't overclaim support.
|
|
380
392
|
return { id: 'host', ok: true, detail: 'git host not recognized (github/gitlab) — PR/MR automation not applicable' };
|
|
381
393
|
}
|
|
394
|
+
function bounded(value, limit = 512) {
|
|
395
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
396
|
+
}
|
|
397
|
+
function checkCompatibility() {
|
|
398
|
+
let failures;
|
|
399
|
+
try {
|
|
400
|
+
failures = (0, registries_2.verifyMinCliVersions)();
|
|
401
|
+
}
|
|
402
|
+
catch {
|
|
403
|
+
return {
|
|
404
|
+
id: 'compatibility', ok: false,
|
|
405
|
+
detail: 'configured registry inventory could not be read',
|
|
406
|
+
remedy: 'Repair the local registry inventory and rerun strict preflight.',
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
if (failures.length === 0)
|
|
410
|
+
return { id: 'compatibility', ok: true, detail: 'all configured registry minCliVersion requirements are satisfied' };
|
|
411
|
+
return {
|
|
412
|
+
id: 'compatibility', ok: false,
|
|
413
|
+
detail: bounded(failures.map(failure => `${failure.name} requires CLI >= ${failure.min}`).join('; ')),
|
|
414
|
+
remedy: 'npm i -g agentic-workflow-manager@latest && rerun in a fresh process',
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
function checkAuthoritativeCurrentness(report) {
|
|
418
|
+
const failing = report.components.filter(component => component.status !== 'current');
|
|
419
|
+
const components = report.components.map(component => bounded(`${component.component}: installed=${component.installed ?? 'unknown'} latest=${component.latest ?? 'unknown'} `
|
|
420
|
+
+ `channel=${component.channel} source=${component.source}${component.pin ? ` pin=${component.pin}` : ''} `
|
|
421
|
+
+ `checkedAt=${component.checkedAt} status=${component.status}`));
|
|
422
|
+
return failing.length === 0
|
|
423
|
+
? { id: 'currentness', ok: true, detail: `Currentness: current. ${components.join('; ')}` }
|
|
424
|
+
: {
|
|
425
|
+
id: 'currentness', ok: false, detail: `Currentness: ${failing.map(component => component.status).join(', ')}. ${components.join('; ')}`,
|
|
426
|
+
remedy: bounded(failing.map(component => component.remedy).join('; ')),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
382
429
|
async function preflight(cwd = process.cwd(), opts = {}) {
|
|
383
|
-
if (!opts || typeof opts !== 'object' || Array.isArray(opts)
|
|
384
|
-
|
|
430
|
+
if (!opts || typeof opts !== 'object' || Array.isArray(opts)
|
|
431
|
+
|| (opts.verifySensors !== undefined && typeof opts.verifySensors !== 'boolean')
|
|
432
|
+
|| (opts.requireCurrent !== undefined && typeof opts.requireCurrent !== 'boolean')) {
|
|
433
|
+
throw new Error('preflight options must contain optional boolean verifySensors and requireCurrent');
|
|
385
434
|
}
|
|
386
435
|
const manifest = readManifest(cwd);
|
|
387
436
|
const manifestExists = fs_1.default.existsSync(path_1.default.join(cwd, MANIFEST));
|
|
@@ -397,6 +446,8 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
397
446
|
// The empirical mode is intentionally opt-in: ordinary preflight remains a
|
|
398
447
|
// quick static inspection and must not dispatch project software.
|
|
399
448
|
...(opts.verifySensors === true ? [await checkSensorExecution(cwd)] : []),
|
|
449
|
+
// Strict mode is the only path that contacts authoritative npm/Git sources.
|
|
450
|
+
...(opts.requireCurrent === true ? [checkCompatibility()] : []),
|
|
400
451
|
// Runs unconditionally — orthogonal to sensor configuration entirely, this is
|
|
401
452
|
// about PR/MR tooling, not sensors.
|
|
402
453
|
checkHost(cwd),
|
|
@@ -407,5 +458,14 @@ async function preflight(cwd = process.cwd(), opts = {}) {
|
|
|
407
458
|
: !manifestExists ? 'not_configured'
|
|
408
459
|
: checks.some(blockingFailure) ? 'degraded'
|
|
409
460
|
: 'ready';
|
|
410
|
-
|
|
461
|
+
const currentness = opts.requireCurrent === true ? await (0, check_1.checkCurrentness)(cwd) : undefined;
|
|
462
|
+
if (currentness) {
|
|
463
|
+
checks.splice(checks.length - 1, 0, checkAuthoritativeCurrentness(currentness));
|
|
464
|
+
}
|
|
465
|
+
const strictFailure = checks.some(blockingFailure);
|
|
466
|
+
const strictStatus = invalidContextKernel ? 'degraded'
|
|
467
|
+
: !manifestExists ? 'not_configured'
|
|
468
|
+
: strictFailure ? 'degraded'
|
|
469
|
+
: 'ready';
|
|
470
|
+
return currentness ? { status: strictStatus, checks, currentness } : { status, checks };
|
|
411
471
|
}
|
|
@@ -44,9 +44,13 @@ function registerPreflightCommand(program) {
|
|
|
44
44
|
.description('verify the project harness can actually gate before development starts')
|
|
45
45
|
.option('--json', 'emit the report as JSON')
|
|
46
46
|
.option('--verify-sensors', 'run the complete sensor gate before unattended execution')
|
|
47
|
+
.option('--require-current', 'require authoritative CLI and registry currentness before unattended execution')
|
|
47
48
|
.option('--cwd <path>', 'project directory to check (default: current)')
|
|
48
49
|
.action(async (opts) => {
|
|
49
|
-
const report = await (0, checks_1.preflight)(opts.cwd ?? process.cwd(), {
|
|
50
|
+
const report = await (0, checks_1.preflight)(opts.cwd ?? process.cwd(), {
|
|
51
|
+
verifySensors: opts.verifySensors === true,
|
|
52
|
+
requireCurrent: opts.requireCurrent === true,
|
|
53
|
+
});
|
|
50
54
|
process.stdout.write(opts.json ? JSON.stringify(report, null, 2) + '\n' : formatReport(report));
|
|
51
55
|
const code = exitCodeFor(report);
|
|
52
56
|
// `process.exit()` may truncate the JSON written immediately above when
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.checkCurrentness = checkCurrentness;
|
|
4
|
+
exports.sanitizeSource = sanitizeSource;
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const util_1 = require("util");
|
|
7
|
+
const cli_version_1 = require("../cli-version");
|
|
8
|
+
const registries_1 = require("../registries");
|
|
9
|
+
const config_1 = require("../../utils/config");
|
|
10
|
+
const execFileAsync = (0, util_1.promisify)(child_process_1.execFile);
|
|
11
|
+
const TIMEOUT_MS = 2_000;
|
|
12
|
+
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
13
|
+
const NPM_SOURCE = `https://registry.npmjs.org/${cli_version_1.CLI_PACKAGE_NAME}`;
|
|
14
|
+
const STRICT_SEMVER = /^v?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/;
|
|
15
|
+
function deadline(operation, timeoutMs = TIMEOUT_MS) {
|
|
16
|
+
let timer;
|
|
17
|
+
const timeout = new Promise((_resolve, reject) => {
|
|
18
|
+
timer = setTimeout(() => reject(new Error('currentness transport timed out')), timeoutMs);
|
|
19
|
+
});
|
|
20
|
+
return Promise.race([operation, timeout]).finally(() => {
|
|
21
|
+
if (timer)
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
function bounded(value, limit = 512) {
|
|
26
|
+
return value.length <= limit ? value : `${value.slice(0, limit - 1)}…`;
|
|
27
|
+
}
|
|
28
|
+
function authoritativeSource(value) {
|
|
29
|
+
try {
|
|
30
|
+
const url = new URL(value);
|
|
31
|
+
// HTTPS userinfo is a credential-bearing transport URL. Reject it before
|
|
32
|
+
// constructing any Git argv, rather than merely redacting it for output.
|
|
33
|
+
if (url.protocol === 'https:' && url.hostname && !url.username && !url.password && !url.search && !url.hash) {
|
|
34
|
+
return bounded(`https://${url.host}${url.pathname}`);
|
|
35
|
+
}
|
|
36
|
+
if (url.protocol === 'ssh:' && url.hostname && !url.password && !url.search && !url.hash) {
|
|
37
|
+
return bounded(`ssh://${url.host}${url.pathname}`);
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// SCP-style Git remotes have no URL parser representation. Permit only an
|
|
43
|
+
// intentionally narrow grammar and remove the optional userinfo; all other
|
|
44
|
+
// schemes fail closed.
|
|
45
|
+
const match = /^(?:[A-Za-z0-9._-]+@)?([A-Za-z0-9.-]+):([A-Za-z0-9._/-]+)$/.exec(value);
|
|
46
|
+
return match
|
|
47
|
+
? bounded(`${match[1]}:${match[2]}`)
|
|
48
|
+
: null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function sanitizeSource(value) {
|
|
52
|
+
return authoritativeSource(value) ?? '[configured remote]';
|
|
53
|
+
}
|
|
54
|
+
function parseVersion(value) {
|
|
55
|
+
const match = STRICT_SEMVER.exec(value.trim());
|
|
56
|
+
if (!match)
|
|
57
|
+
return null;
|
|
58
|
+
return [match[1], match[2], match[3]];
|
|
59
|
+
}
|
|
60
|
+
function compareNumericIdentifier(a, b) {
|
|
61
|
+
if (a.length !== b.length)
|
|
62
|
+
return a.length < b.length ? -1 : 1;
|
|
63
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
64
|
+
}
|
|
65
|
+
function compareStrict(a, b) {
|
|
66
|
+
const av = parseVersion(a);
|
|
67
|
+
const bv = parseVersion(b);
|
|
68
|
+
if (!av || !bv)
|
|
69
|
+
return null;
|
|
70
|
+
return compareNumericIdentifier(av[0], bv[0])
|
|
71
|
+
|| compareNumericIdentifier(av[1], bv[1])
|
|
72
|
+
|| compareNumericIdentifier(av[2], bv[2]);
|
|
73
|
+
}
|
|
74
|
+
function iso(now) {
|
|
75
|
+
const value = now();
|
|
76
|
+
if (!Number.isFinite(value))
|
|
77
|
+
throw new Error('currentness clock must return a finite timestamp');
|
|
78
|
+
return new Date(value).toISOString();
|
|
79
|
+
}
|
|
80
|
+
const defaultGit = async (cwd, args, options) => {
|
|
81
|
+
const result = await execFileAsync('git', args, {
|
|
82
|
+
cwd,
|
|
83
|
+
timeout: options.timeoutMs,
|
|
84
|
+
maxBuffer: options.maxOutputBytes,
|
|
85
|
+
windowsHide: true,
|
|
86
|
+
});
|
|
87
|
+
return result.stdout;
|
|
88
|
+
};
|
|
89
|
+
function unavailable(component, source, checkedAt, installed, pin) {
|
|
90
|
+
return {
|
|
91
|
+
component, installed, latest: null, channel: 'stable', source, ...(pin ? { pin } : {}), checkedAt,
|
|
92
|
+
status: 'unverifiable', detail: 'Authoritative currentness could not be verified.',
|
|
93
|
+
remedy: 'Restore source access and rerun strict preflight.',
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
async function latestCli(fetchImpl) {
|
|
97
|
+
const controller = new AbortController();
|
|
98
|
+
const timer = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
|
99
|
+
try {
|
|
100
|
+
const response = await deadline(fetchImpl(NPM_SOURCE, { signal: controller.signal }));
|
|
101
|
+
if (!response.ok)
|
|
102
|
+
return null;
|
|
103
|
+
const text = await deadline(response.text());
|
|
104
|
+
if (Buffer.byteLength(text, 'utf8') > MAX_OUTPUT_BYTES)
|
|
105
|
+
return null;
|
|
106
|
+
const body = JSON.parse(text);
|
|
107
|
+
if (!body || typeof body !== 'object')
|
|
108
|
+
return null;
|
|
109
|
+
const tags = body['dist-tags'];
|
|
110
|
+
const latest = tags && typeof tags === 'object' ? tags.latest : undefined;
|
|
111
|
+
return typeof latest === 'string' && parseVersion(latest) ? latest : null;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
finally {
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
function latestStableTag(output) {
|
|
121
|
+
if (Buffer.byteLength(output, 'utf8') > MAX_OUTPUT_BYTES)
|
|
122
|
+
return null;
|
|
123
|
+
const tags = new Map();
|
|
124
|
+
for (const line of output.split(/\r?\n/)) {
|
|
125
|
+
const match = /^([0-9a-fA-F]{40,64})\trefs\/tags\/(v?\d+\.\d+\.\d+)(\^\{\})?$/.exec(line);
|
|
126
|
+
if (!match)
|
|
127
|
+
continue;
|
|
128
|
+
const tag = tags.get(match[2]) ?? { sha: '' };
|
|
129
|
+
if (match[3])
|
|
130
|
+
tag.peeled = match[1];
|
|
131
|
+
else
|
|
132
|
+
tag.sha = match[1];
|
|
133
|
+
tags.set(match[2], tag);
|
|
134
|
+
}
|
|
135
|
+
let latest = null;
|
|
136
|
+
for (const [tag, ref] of tags) {
|
|
137
|
+
if (!ref.sha)
|
|
138
|
+
continue;
|
|
139
|
+
if (!latest || (compareStrict(tag, latest.tag) ?? -1) > 0)
|
|
140
|
+
latest = { sha: ref.peeled ?? ref.sha, tag };
|
|
141
|
+
}
|
|
142
|
+
return latest;
|
|
143
|
+
}
|
|
144
|
+
async function registryComponent(registry, git, prefs, checkedAt) {
|
|
145
|
+
const authorizedSource = authoritativeSource(registry.remote);
|
|
146
|
+
const source = authorizedSource ?? '[configured remote]';
|
|
147
|
+
const pin = prefs.pins?.[registry.name];
|
|
148
|
+
if (!authorizedSource)
|
|
149
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, null, pin);
|
|
150
|
+
let origin;
|
|
151
|
+
let head;
|
|
152
|
+
let exactTag;
|
|
153
|
+
let remoteTags;
|
|
154
|
+
try {
|
|
155
|
+
[origin, head, exactTag, remoteTags] = await Promise.all([
|
|
156
|
+
deadline(git(registry.contentRoot, ['remote', 'get-url', 'origin'], { timeoutMs: TIMEOUT_MS, maxOutputBytes: MAX_OUTPUT_BYTES })),
|
|
157
|
+
deadline(git(registry.contentRoot, ['rev-parse', 'HEAD'], { timeoutMs: TIMEOUT_MS, maxOutputBytes: MAX_OUTPUT_BYTES })),
|
|
158
|
+
deadline(git(registry.contentRoot, ['describe', '--tags', '--exact-match', 'HEAD'], { timeoutMs: TIMEOUT_MS, maxOutputBytes: MAX_OUTPUT_BYTES })),
|
|
159
|
+
deadline(git(registry.contentRoot, ['ls-remote', '--tags', registry.remote], { timeoutMs: TIMEOUT_MS, maxOutputBytes: MAX_OUTPUT_BYTES })),
|
|
160
|
+
]);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, null, pin);
|
|
164
|
+
}
|
|
165
|
+
const installed = exactTag.trim();
|
|
166
|
+
const latest = latestStableTag(remoteTags);
|
|
167
|
+
const localHead = head.trim();
|
|
168
|
+
const installedVersion = parseVersion(installed);
|
|
169
|
+
if (!installedVersion || !latest || !/^[0-9a-fA-F]{40,64}$/.test(localHead) || origin.trim() !== registry.remote) {
|
|
170
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, installedVersion ? installed : null, pin);
|
|
171
|
+
}
|
|
172
|
+
const relation = compareStrict(installed, latest.tag);
|
|
173
|
+
if (relation === null)
|
|
174
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, installed, pin);
|
|
175
|
+
if (relation === 0 && localHead === latest.sha) {
|
|
176
|
+
return { component: `registry:${registry.name}`, installed, latest: latest.tag, channel: 'stable', source, ...(pin ? { pin } : {}), checkedAt, status: 'current', detail: 'Exact stable tag and configured origin match the authoritative remote.', remedy: 'No action required.' };
|
|
177
|
+
}
|
|
178
|
+
if (relation < 0) {
|
|
179
|
+
return {
|
|
180
|
+
component: `registry:${registry.name}`, installed, latest: latest.tag, channel: 'stable', source, ...(pin ? { pin } : {}), checkedAt,
|
|
181
|
+
status: pin ? 'pinned-behind' : 'stale', detail: 'Installed stable tag is behind the authoritative remote.',
|
|
182
|
+
remedy: pin ? `awm unpin ${registry.name} && awm update --yes` : 'awm update --yes',
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
return unavailable(`registry:${registry.name}`, source, checkedAt, installed, pin);
|
|
186
|
+
}
|
|
187
|
+
/** Strict, read-only remote currentness check. It intentionally never reads/writes update cache. */
|
|
188
|
+
async function checkCurrentness(cwd, deps = {}) {
|
|
189
|
+
if (!cwd || typeof cwd !== 'string')
|
|
190
|
+
throw new Error('currentness cwd must be a non-empty string');
|
|
191
|
+
const now = deps.now ?? Date.now;
|
|
192
|
+
const checkedAt = iso(now);
|
|
193
|
+
const getCliVersion = deps.cliVersion ?? cli_version_1.cliVersion;
|
|
194
|
+
const fetchImpl = deps.fetch ?? fetch;
|
|
195
|
+
const git = deps.git ?? defaultGit;
|
|
196
|
+
const preferences = (deps.readPreferences ?? config_1.readPreferences)();
|
|
197
|
+
const installedCli = getCliVersion();
|
|
198
|
+
const latest = await latestCli(fetchImpl);
|
|
199
|
+
const cli = !parseVersion(installedCli) || !latest || compareStrict(installedCli, latest) === null
|
|
200
|
+
? unavailable('cli', NPM_SOURCE, checkedAt, parseVersion(installedCli) ? installedCli : null)
|
|
201
|
+
: compareStrict(installedCli, latest) === 0
|
|
202
|
+
? { component: 'cli', installed: installedCli, latest, channel: 'stable', source: NPM_SOURCE, checkedAt, status: 'current', detail: 'Installed version equals npm dist-tags.latest.', remedy: 'No action required.' }
|
|
203
|
+
: { component: 'cli', installed: installedCli, latest, channel: 'stable', source: NPM_SOURCE, checkedAt, status: 'stale', detail: 'Installed version is behind npm dist-tags.latest.', remedy: `npm i -g ${cli_version_1.CLI_PACKAGE_NAME}@latest && rerun in a fresh process` };
|
|
204
|
+
const components = [cli];
|
|
205
|
+
let registries;
|
|
206
|
+
try {
|
|
207
|
+
registries = (deps.listRegistries ?? registries_1.listRegistries)();
|
|
208
|
+
if (!Array.isArray(registries))
|
|
209
|
+
throw new Error('currentness registry inventory must be an array');
|
|
210
|
+
}
|
|
211
|
+
catch {
|
|
212
|
+
components.push({
|
|
213
|
+
...unavailable('registry:inventory', '[configured registry inventory]', checkedAt, null),
|
|
214
|
+
remedy: 'Repair the local registry inventory and rerun strict preflight.',
|
|
215
|
+
});
|
|
216
|
+
return { checkedAt, components, compatibility: { status: 'not-checked' } };
|
|
217
|
+
}
|
|
218
|
+
if (registries.length === 0)
|
|
219
|
+
components.push(unavailable('registry:none', '[configured remote]', checkedAt, null));
|
|
220
|
+
else
|
|
221
|
+
for (const registry of registries)
|
|
222
|
+
components.push(await registryComponent(registry, git, preferences, checkedAt));
|
|
223
|
+
return { checkedAt, components, compatibility: { status: 'not-checked' } };
|
|
224
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseJsonNoDuplicate = parseJsonNoDuplicate;
|
|
4
|
+
/** Parses JSON after rejecting duplicate object keys without runtime dependencies. */
|
|
5
|
+
function parseJsonNoDuplicate(text) {
|
|
6
|
+
if (typeof text !== 'string')
|
|
7
|
+
throw new Error('JSON input must be a string');
|
|
8
|
+
let index = 0;
|
|
9
|
+
const fail = (message) => { throw new Error(`invalid JSON: ${message}`); };
|
|
10
|
+
const whitespace = () => { while (/\s/.test(text[index] ?? ''))
|
|
11
|
+
index++; };
|
|
12
|
+
const string = () => {
|
|
13
|
+
if (text[index++] !== '"')
|
|
14
|
+
fail('expected string');
|
|
15
|
+
let out = '';
|
|
16
|
+
while (index < text.length) {
|
|
17
|
+
const char = text[index++];
|
|
18
|
+
if (char === '"')
|
|
19
|
+
return out;
|
|
20
|
+
if (char === '\\') {
|
|
21
|
+
const escape = text[index++];
|
|
22
|
+
if (!escape)
|
|
23
|
+
fail('unterminated escape');
|
|
24
|
+
if (escape === 'u') {
|
|
25
|
+
const hex = text.slice(index, index + 4);
|
|
26
|
+
if (!/^[0-9a-fA-F]{4}$/.test(hex))
|
|
27
|
+
fail('invalid unicode escape');
|
|
28
|
+
out += String.fromCharCode(Number.parseInt(hex, 16));
|
|
29
|
+
index += 4;
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
const decoded = { '"': '"', '\\': '\\', '/': '/', b: '\b', f: '\f', n: '\n', r: '\r', t: '\t' };
|
|
33
|
+
if (!(escape in decoded))
|
|
34
|
+
fail('invalid escape');
|
|
35
|
+
out += decoded[escape];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
if (char < ' ')
|
|
40
|
+
fail('control character in string');
|
|
41
|
+
out += char;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return fail('unterminated string');
|
|
45
|
+
};
|
|
46
|
+
const value = () => {
|
|
47
|
+
whitespace();
|
|
48
|
+
const char = text[index];
|
|
49
|
+
if (char === '"') {
|
|
50
|
+
string();
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
if (char === '{') {
|
|
54
|
+
object();
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (char === '[') {
|
|
58
|
+
array();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (text.startsWith('true', index) || text.startsWith('false', index) || text.startsWith('null', index)) {
|
|
62
|
+
index += text.startsWith('true', index) ? 4 : text.startsWith('false', index) ? 5 : 4;
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
const match = text.slice(index).match(/^-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]?\d+)?/);
|
|
66
|
+
if (!match)
|
|
67
|
+
return fail('expected value');
|
|
68
|
+
index += match[0].length;
|
|
69
|
+
};
|
|
70
|
+
const array = () => { index++; whitespace(); if (text[index] === ']') {
|
|
71
|
+
index++;
|
|
72
|
+
return;
|
|
73
|
+
} while (true) {
|
|
74
|
+
value();
|
|
75
|
+
whitespace();
|
|
76
|
+
if (text[index] === ']') {
|
|
77
|
+
index++;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (text[index++] !== ',')
|
|
81
|
+
fail('expected comma');
|
|
82
|
+
} };
|
|
83
|
+
const object = () => { index++; const keys = new Set(); whitespace(); if (text[index] === '}') {
|
|
84
|
+
index++;
|
|
85
|
+
return;
|
|
86
|
+
} while (true) {
|
|
87
|
+
whitespace();
|
|
88
|
+
if (text[index] !== '"')
|
|
89
|
+
fail('expected object key');
|
|
90
|
+
const key = string();
|
|
91
|
+
if (keys.has(key))
|
|
92
|
+
fail(`duplicate key ${JSON.stringify(key)}`);
|
|
93
|
+
keys.add(key);
|
|
94
|
+
whitespace();
|
|
95
|
+
if (text[index++] !== ':')
|
|
96
|
+
fail('expected colon');
|
|
97
|
+
value();
|
|
98
|
+
whitespace();
|
|
99
|
+
if (text[index] === '}') {
|
|
100
|
+
index++;
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
if (text[index++] !== ',')
|
|
104
|
+
fail('expected comma');
|
|
105
|
+
} };
|
|
106
|
+
value();
|
|
107
|
+
whitespace();
|
|
108
|
+
if (index !== text.length)
|
|
109
|
+
fail('trailing content');
|
|
110
|
+
return JSON.parse(text);
|
|
111
|
+
}
|