agentic-workflow-manager 9.17.0 → 9.18.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/env/index.js +192 -0
- package/dist/src/commands/registry/doctor.js +428 -0
- package/dist/src/commands/registry/index.js +52 -0
- package/dist/src/commands/registry/scaffold.js +140 -0
- package/dist/src/commands/watch/index.js +3 -0
- package/dist/src/core/environments/resolve.js +122 -0
- package/dist/src/core/environments/store.js +323 -0
- package/dist/src/core/facts/catalog.js +138 -0
- package/dist/src/core/facts/needs.js +57 -0
- package/dist/src/core/facts/types.js +21 -0
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/env/index.test.js +261 -0
- package/dist/tests/commands/registry/doctor.test.js +283 -0
- package/dist/tests/commands/registry/scaffold.test.js +186 -0
- package/dist/tests/commands/watch/reopened-196.test.js +9 -2
- package/dist/tests/core/environments/resolve.test.js +347 -0
- package/dist/tests/core/environments/store.test.js +248 -0
- package/dist/tests/core/facts/catalog.test.js +179 -0
- package/dist/tests/core/facts/needs.test.js +88 -0
- package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
- package/package.json +1 -1
- package/prebuilds/darwin-arm64/secure_fs.node +0 -0
- package/prebuilds/darwin-x64/secure_fs.node +0 -0
- package/prebuilds/win32-arm64/secure_fs.node +0 -0
- package/prebuilds/win32-x64/secure_fs.node +0 -0
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.mergeFactDefinitions = mergeFactDefinitions;
|
|
7
|
+
exports.registerEnvCommand = registerEnvCommand;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const readline_1 = __importDefault(require("readline"));
|
|
11
|
+
const catalog_1 = require("../../core/facts/catalog");
|
|
12
|
+
const registries_1 = require("../../core/registries");
|
|
13
|
+
const store_1 = require("../../core/environments/store");
|
|
14
|
+
const resolve_1 = require("../../core/environments/resolve");
|
|
15
|
+
/**
|
|
16
|
+
* Merge facts catalogs from installed registry roots.
|
|
17
|
+
* Later roots may refine a fact, but must not weaken durability: volatile wins
|
|
18
|
+
* over durable so an expired secret cannot be revived by a durable override.
|
|
19
|
+
*/
|
|
20
|
+
function mergeFactDefinitions(into, from) {
|
|
21
|
+
for (const [id, next] of Object.entries(from)) {
|
|
22
|
+
const prev = Object.hasOwn(into, id) ? into[id] : undefined;
|
|
23
|
+
if (!prev) {
|
|
24
|
+
into[id] = next;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (prev.durability === 'volatile' && next.durability === 'durable') {
|
|
28
|
+
const kept = { ...next, durability: 'volatile' };
|
|
29
|
+
if (prev.ttl !== undefined)
|
|
30
|
+
kept.ttl = prev.ttl;
|
|
31
|
+
into[id] = kept;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
into[id] = next;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
function loadCatalogFromInstalledRoots() {
|
|
38
|
+
const merged = { schemaVersion: 1, facts: {} };
|
|
39
|
+
for (const root of (0, registries_1.contentRoots)()) {
|
|
40
|
+
const factsPath = path_1.default.join(root, 'facts.json');
|
|
41
|
+
if (!fs_1.default.existsSync(factsPath))
|
|
42
|
+
continue;
|
|
43
|
+
const catalog = (0, catalog_1.parseFactsCatalog)(factsPath);
|
|
44
|
+
mergeFactDefinitions(merged.facts, catalog.facts);
|
|
45
|
+
}
|
|
46
|
+
return merged;
|
|
47
|
+
}
|
|
48
|
+
function defaultConsultedRoots() {
|
|
49
|
+
return (0, registries_1.contentRoots)();
|
|
50
|
+
}
|
|
51
|
+
function requireValue(value, flagHint) {
|
|
52
|
+
if (value === undefined) {
|
|
53
|
+
throw new store_1.EnvironmentStoreError(`${flagHint} requires --value <text> in non-interactive mode (prompt / editable store / --value only; RF-3.3)`);
|
|
54
|
+
}
|
|
55
|
+
return value;
|
|
56
|
+
}
|
|
57
|
+
async function defaultPromptValue(question) {
|
|
58
|
+
const rl = readline_1.default.createInterface({ input: process.stdin, output: process.stdout });
|
|
59
|
+
try {
|
|
60
|
+
return await new Promise((resolve) => {
|
|
61
|
+
rl.question(question, (answer) => resolve(answer));
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
finally {
|
|
65
|
+
rl.close();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
async function resolveValue(optsValue, flagHint, factId, deps) {
|
|
69
|
+
if (optsValue !== undefined)
|
|
70
|
+
return optsValue;
|
|
71
|
+
if (!deps.stdinIsTTY()) {
|
|
72
|
+
return requireValue(undefined, flagHint);
|
|
73
|
+
}
|
|
74
|
+
return Promise.resolve(deps.promptValue(`Value for ${factId}: `));
|
|
75
|
+
}
|
|
76
|
+
function setFact(factId, rawValue, deps) {
|
|
77
|
+
const catalog = deps.loadCatalog();
|
|
78
|
+
const name = (0, store_1.getActiveEnvironmentName)();
|
|
79
|
+
(0, store_1.writeFact)(name, factId, {
|
|
80
|
+
value: rawValue,
|
|
81
|
+
status: 'fresh',
|
|
82
|
+
updatedAt: new Date().toISOString(),
|
|
83
|
+
}, {
|
|
84
|
+
catalog,
|
|
85
|
+
consultedRoots: deps.consultedRoots(),
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
function registerEnvCommand(program, deps = {}) {
|
|
89
|
+
const loadCatalog = deps.loadCatalog ?? loadCatalogFromInstalledRoots;
|
|
90
|
+
const consultedRoots = deps.consultedRoots ?? defaultConsultedRoots;
|
|
91
|
+
const now = deps.now ?? (() => new Date());
|
|
92
|
+
const stdinIsTTY = deps.stdinIsTTY ?? (() => Boolean(process.stdin.isTTY));
|
|
93
|
+
const promptValue = deps.promptValue ?? defaultPromptValue;
|
|
94
|
+
const env = program.command('env').description('manage machine-local operator environments (facts store)');
|
|
95
|
+
env.command('status')
|
|
96
|
+
.description('show active environment and fact statuses vs catalog')
|
|
97
|
+
.action(() => {
|
|
98
|
+
const name = (0, store_1.getActiveEnvironmentName)();
|
|
99
|
+
const catalog = loadCatalog();
|
|
100
|
+
const store = (0, store_1.readValues)(name);
|
|
101
|
+
const statuses = (0, resolve_1.statusFor)(catalog, store, now());
|
|
102
|
+
const lines = [
|
|
103
|
+
`Active environment: ${name}`,
|
|
104
|
+
`Store: ${(0, store_1.valuesPath)(name)}`,
|
|
105
|
+
'',
|
|
106
|
+
'Facts:',
|
|
107
|
+
];
|
|
108
|
+
const ids = Object.keys(catalog.facts).sort();
|
|
109
|
+
if (ids.length === 0) {
|
|
110
|
+
lines.push(' (no facts in consulted catalogs)');
|
|
111
|
+
}
|
|
112
|
+
else {
|
|
113
|
+
for (const id of ids) {
|
|
114
|
+
lines.push(` ${id}: ${statuses[id]}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
process.stdout.write(lines.join('\n') + '\n');
|
|
118
|
+
});
|
|
119
|
+
env.command('set <factId>')
|
|
120
|
+
.description('set a fact value in the active environment (--value for non-interactive)')
|
|
121
|
+
.option('--value <text>', 'fact value (required non-interactive; never runs registry commands)')
|
|
122
|
+
.action(async (factId, opts) => {
|
|
123
|
+
const value = await resolveValue(opts.value, 'awm env set', factId, {
|
|
124
|
+
stdinIsTTY,
|
|
125
|
+
promptValue,
|
|
126
|
+
});
|
|
127
|
+
setFact(factId, value, { loadCatalog, consultedRoots });
|
|
128
|
+
process.stdout.write(`Set ${factId} in environment ${(0, store_1.getActiveEnvironmentName)()}\n`);
|
|
129
|
+
});
|
|
130
|
+
env.command('edit')
|
|
131
|
+
.description('print the absolute path of the active environment values.json')
|
|
132
|
+
.action(() => {
|
|
133
|
+
const name = (0, store_1.getActiveEnvironmentName)();
|
|
134
|
+
const file = (0, store_1.valuesPath)(name);
|
|
135
|
+
process.stdout.write(`${file}\n`);
|
|
136
|
+
});
|
|
137
|
+
env.command('refresh [factId]')
|
|
138
|
+
.description('re-set a fact value (prompt / --value only; no registry commands)')
|
|
139
|
+
.option('--value <text>', 'new value (required non-interactive)')
|
|
140
|
+
.option('--all', 'refresh every fact present in the store (interactive prompts, or pass factId)')
|
|
141
|
+
.action(async (factId, opts) => {
|
|
142
|
+
if (opts.all && !factId) {
|
|
143
|
+
if (!stdinIsTTY()) {
|
|
144
|
+
throw new store_1.EnvironmentStoreError('awm env refresh --all requires a TTY (or pass <factId> with --value in non-interactive mode)');
|
|
145
|
+
}
|
|
146
|
+
const name = (0, store_1.getActiveEnvironmentName)();
|
|
147
|
+
const ids = Object.keys((0, store_1.readValues)(name));
|
|
148
|
+
if (ids.length === 0) {
|
|
149
|
+
throw new store_1.EnvironmentStoreError('awm env refresh --all: active store has no facts');
|
|
150
|
+
}
|
|
151
|
+
for (const id of ids) {
|
|
152
|
+
const value = await Promise.resolve(promptValue(`Value for ${id}: `));
|
|
153
|
+
setFact(id, value, { loadCatalog, consultedRoots });
|
|
154
|
+
process.stdout.write(`Refreshed ${id} in environment ${(0, store_1.getActiveEnvironmentName)()}\n`);
|
|
155
|
+
}
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
if (!factId) {
|
|
159
|
+
throw new store_1.EnvironmentStoreError('awm env refresh requires <factId> (or interactive --all)');
|
|
160
|
+
}
|
|
161
|
+
const value = await resolveValue(opts.value, 'awm env refresh', factId, {
|
|
162
|
+
stdinIsTTY,
|
|
163
|
+
promptValue,
|
|
164
|
+
});
|
|
165
|
+
setFact(factId, value, { loadCatalog, consultedRoots });
|
|
166
|
+
process.stdout.write(`Refreshed ${factId} in environment ${(0, store_1.getActiveEnvironmentName)()}\n`);
|
|
167
|
+
});
|
|
168
|
+
env.command('use <name>')
|
|
169
|
+
.description('switch the active environment (one active per machine)')
|
|
170
|
+
.action((name) => {
|
|
171
|
+
(0, store_1.setActiveEnvironmentName)(name);
|
|
172
|
+
process.stdout.write(`Active environment: ${name}\n`);
|
|
173
|
+
});
|
|
174
|
+
env.command('inject')
|
|
175
|
+
.description('print JSON injection payload for the active environment (default mode: reference)')
|
|
176
|
+
.option('--mode <mode>', 'injection mode: reference|context', 'reference')
|
|
177
|
+
.action((opts) => {
|
|
178
|
+
const rawMode = opts.mode ?? 'reference';
|
|
179
|
+
const mode = rawMode === 'reference' || rawMode === 'context' ? rawMode : 'reference';
|
|
180
|
+
const name = (0, store_1.getActiveEnvironmentName)();
|
|
181
|
+
const catalog = loadCatalog();
|
|
182
|
+
const store = (0, store_1.readValues)(name);
|
|
183
|
+
const result = (0, resolve_1.buildInjection)({
|
|
184
|
+
catalog,
|
|
185
|
+
store,
|
|
186
|
+
storePath: (0, store_1.valuesPath)(name),
|
|
187
|
+
inject: mode,
|
|
188
|
+
now: now(),
|
|
189
|
+
});
|
|
190
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
191
|
+
});
|
|
192
|
+
}
|
|
@@ -0,0 +1,428 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.doctorExitCode = doctorExitCode;
|
|
7
|
+
exports.doctorRegistry = doctorRegistry;
|
|
8
|
+
const fs_1 = __importDefault(require("fs"));
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const catalog_1 = require("../../core/facts/catalog");
|
|
11
|
+
const needs_1 = require("../../core/facts/needs");
|
|
12
|
+
const types_1 = require("../../core/facts/types");
|
|
13
|
+
const bundles_1 = require("../../core/bundles");
|
|
14
|
+
const registries_1 = require("../../core/registries");
|
|
15
|
+
const versioning_1 = require("../../core/versioning");
|
|
16
|
+
/** Exit code for CLI: structural failures → 1; advisories alone → 0. */
|
|
17
|
+
function doctorExitCode(report) {
|
|
18
|
+
return report.structural.length ? 1 : 0;
|
|
19
|
+
}
|
|
20
|
+
/** Capability ids the doctor understands in v1; others are advisory (forward-compatible). */
|
|
21
|
+
const KNOWN_CAPABILITY_IDS = new Set(['facts']);
|
|
22
|
+
const CAPABILITIES_HEADER_RE = /^\|\s*id\s*\|\s*status\s*\|\s*notes\s*\|$/i;
|
|
23
|
+
const HOME_PATH_RE = /(^|[^\w])~(\/|\\)/;
|
|
24
|
+
const USERS_PATH_RE = /\/Users\/[^/\s]+/;
|
|
25
|
+
const ADVISORY_CAP = 50;
|
|
26
|
+
const STORE_BASENAMES = new Set(['values.json', 'meta.json']);
|
|
27
|
+
function isMissing(error) {
|
|
28
|
+
return error?.code === 'ENOENT';
|
|
29
|
+
}
|
|
30
|
+
function walkRegularFiles(root, visit) {
|
|
31
|
+
const stack = [root];
|
|
32
|
+
while (stack.length > 0) {
|
|
33
|
+
const dir = stack.pop();
|
|
34
|
+
let entries;
|
|
35
|
+
try {
|
|
36
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
for (const entry of entries) {
|
|
42
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
43
|
+
continue;
|
|
44
|
+
const abs = path_1.default.join(dir, entry.name);
|
|
45
|
+
let stat;
|
|
46
|
+
try {
|
|
47
|
+
stat = fs_1.default.lstatSync(abs);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
if (stat.isSymbolicLink())
|
|
53
|
+
continue;
|
|
54
|
+
if (stat.isDirectory()) {
|
|
55
|
+
stack.push(abs);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (stat.isFile()) {
|
|
59
|
+
visit(abs, path_1.default.relative(root, abs));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function parseCapabilitiesIndex(markdown) {
|
|
65
|
+
const lines = markdown.split(/\r?\n/);
|
|
66
|
+
const headingIdx = lines.findIndex((l) => /^##\s+Capabilities\s*$/i.test(l.trim()));
|
|
67
|
+
if (headingIdx === -1) {
|
|
68
|
+
return { rows: [], errors: ['REGISTRY.md missing ## Capabilities section'] };
|
|
69
|
+
}
|
|
70
|
+
let headerIdx = -1;
|
|
71
|
+
for (let i = headingIdx + 1; i < lines.length; i++) {
|
|
72
|
+
const line = lines[i].trim();
|
|
73
|
+
if (/^##\s+/.test(line))
|
|
74
|
+
break;
|
|
75
|
+
if (CAPABILITIES_HEADER_RE.test(line)) {
|
|
76
|
+
headerIdx = i;
|
|
77
|
+
break;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (headerIdx === -1) {
|
|
81
|
+
return {
|
|
82
|
+
rows: [],
|
|
83
|
+
errors: ['REGISTRY.md Capabilities index missing | id | status | notes | header'],
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const rows = [];
|
|
87
|
+
const errors = [];
|
|
88
|
+
const seen = new Set();
|
|
89
|
+
for (let i = headerIdx + 1; i < lines.length; i++) {
|
|
90
|
+
const line = lines[i].trim();
|
|
91
|
+
if (line === '')
|
|
92
|
+
continue;
|
|
93
|
+
if (/^##\s+/.test(line))
|
|
94
|
+
break;
|
|
95
|
+
if (!line.startsWith('|'))
|
|
96
|
+
continue;
|
|
97
|
+
if (/^\|\s*-+/.test(line))
|
|
98
|
+
continue; // separator row
|
|
99
|
+
const cells = line
|
|
100
|
+
.replace(/^\|/, '')
|
|
101
|
+
.replace(/\|$/, '')
|
|
102
|
+
.split('|')
|
|
103
|
+
.map((c) => c.trim());
|
|
104
|
+
if (cells.length < 3) {
|
|
105
|
+
errors.push(`Capabilities row malformed: ${line}`);
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
const id = cells[0];
|
|
109
|
+
if (id === '') {
|
|
110
|
+
errors.push('Capabilities table has empty id');
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (seen.has(id)) {
|
|
114
|
+
errors.push(`Capabilities table duplicate id "${id}"`);
|
|
115
|
+
continue;
|
|
116
|
+
}
|
|
117
|
+
seen.add(id);
|
|
118
|
+
rows.push({ id, status: cells[1] ?? '', notes: cells[2] ?? '' });
|
|
119
|
+
}
|
|
120
|
+
return { rows, errors };
|
|
121
|
+
}
|
|
122
|
+
/** Collect readable regular SKILL.md paths; symlink / non-regular / unreadable → structural. */
|
|
123
|
+
function collectSkillMdPaths(root, structural) {
|
|
124
|
+
const skillsRoot = path_1.default.join(root, 'skills');
|
|
125
|
+
const out = [];
|
|
126
|
+
if (!fs_1.default.existsSync(skillsRoot))
|
|
127
|
+
return out;
|
|
128
|
+
let entries;
|
|
129
|
+
try {
|
|
130
|
+
entries = fs_1.default.readdirSync(skillsRoot, { withFileTypes: true });
|
|
131
|
+
}
|
|
132
|
+
catch (e) {
|
|
133
|
+
structural.push(`Cannot read skills/: ${e instanceof Error ? e.message : String(e)}`);
|
|
134
|
+
return out;
|
|
135
|
+
}
|
|
136
|
+
for (const entry of entries) {
|
|
137
|
+
const skillDir = path_1.default.join(skillsRoot, entry.name);
|
|
138
|
+
const relDir = path_1.default.relative(root, skillDir);
|
|
139
|
+
let dirStat;
|
|
140
|
+
try {
|
|
141
|
+
dirStat = fs_1.default.lstatSync(skillDir);
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
structural.push(`Cannot inspect skill path ${relDir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (dirStat.isSymbolicLink()) {
|
|
148
|
+
structural.push(`Skill path ${relDir} must not be a symbolic link (fail closed)`);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!dirStat.isDirectory()) {
|
|
152
|
+
// Scaffold tracks empty skills/ with .gitkeep so git clone keeps the
|
|
153
|
+
// content dir (RF-4.1). Other loose files remain structural.
|
|
154
|
+
if (entry.name === '.gitkeep' && dirStat.isFile())
|
|
155
|
+
continue;
|
|
156
|
+
structural.push(`Skill path ${relDir} must be a regular directory (fail closed)`);
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
const skillMd = path_1.default.join(skillDir, 'SKILL.md');
|
|
160
|
+
const relMd = path_1.default.relative(root, skillMd);
|
|
161
|
+
let mdStat;
|
|
162
|
+
try {
|
|
163
|
+
mdStat = fs_1.default.lstatSync(skillMd);
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
if (isMissing(error))
|
|
167
|
+
continue;
|
|
168
|
+
structural.push(`Cannot inspect ${relMd}: ${error instanceof Error ? error.message : String(error)}`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (mdStat.isSymbolicLink()) {
|
|
172
|
+
structural.push(`${relMd} must not be a symbolic link (fail closed)`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
if (!mdStat.isFile()) {
|
|
176
|
+
structural.push(`${relMd} must be a regular file (fail closed)`);
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
try {
|
|
180
|
+
fs_1.default.accessSync(skillMd, fs_1.default.constants.R_OK);
|
|
181
|
+
}
|
|
182
|
+
catch (e) {
|
|
183
|
+
structural.push(`Unreadable ${relMd}: ${e instanceof Error ? e.message : String(e)} (fail closed)`);
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
out.push(skillMd);
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* RF-4.5: store files under the registry tree are structural.
|
|
192
|
+
* Symlink entries named `environments`, or basename `values.json`/`meta.json`, are structural
|
|
193
|
+
* even when the walk does not follow the link.
|
|
194
|
+
*/
|
|
195
|
+
function scanEnvStoreInTree(root, structural) {
|
|
196
|
+
const stack = [root];
|
|
197
|
+
while (stack.length > 0) {
|
|
198
|
+
const dir = stack.pop();
|
|
199
|
+
let entries;
|
|
200
|
+
try {
|
|
201
|
+
entries = fs_1.default.readdirSync(dir, { withFileTypes: true });
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
for (const entry of entries) {
|
|
207
|
+
if (entry.name === '.git' || entry.name === 'node_modules')
|
|
208
|
+
continue;
|
|
209
|
+
const abs = path_1.default.join(dir, entry.name);
|
|
210
|
+
const rel = path_1.default.relative(root, abs);
|
|
211
|
+
let stat;
|
|
212
|
+
try {
|
|
213
|
+
stat = fs_1.default.lstatSync(abs);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (stat.isSymbolicLink()) {
|
|
219
|
+
if (entry.name === 'environments' || STORE_BASENAMES.has(entry.name)) {
|
|
220
|
+
structural.push(`Environment store symlink must not live in the registry tree: ${rel} (RF-4.5)`);
|
|
221
|
+
}
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
if (stat.isDirectory()) {
|
|
225
|
+
stack.push(abs);
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
if (stat.isFile()) {
|
|
229
|
+
const parts = rel.split(/[/\\]/);
|
|
230
|
+
if (parts.includes('environments') && STORE_BASENAMES.has(entry.name)) {
|
|
231
|
+
structural.push(`Environment store file must not live in the registry tree: ${rel} (RF-4.5)`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
function scanProsePathHints(root, advisories) {
|
|
238
|
+
const skillsRoot = path_1.default.join(root, 'skills');
|
|
239
|
+
if (!fs_1.default.existsSync(skillsRoot))
|
|
240
|
+
return;
|
|
241
|
+
walkRegularFiles(skillsRoot, (abs, rel) => {
|
|
242
|
+
if (!abs.endsWith('.md'))
|
|
243
|
+
return;
|
|
244
|
+
if (advisories.length >= ADVISORY_CAP)
|
|
245
|
+
return;
|
|
246
|
+
let text;
|
|
247
|
+
try {
|
|
248
|
+
text = fs_1.default.readFileSync(abs, 'utf-8');
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
for (const line of text.split(/\r?\n/)) {
|
|
254
|
+
if (advisories.length >= ADVISORY_CAP)
|
|
255
|
+
return;
|
|
256
|
+
if (HOME_PATH_RE.test(line) || USERS_PATH_RE.test(line)) {
|
|
257
|
+
advisories.push(`Possible machine path in ${rel}: prefer a fact (e.g. ~/Developments or /Users/…)`);
|
|
258
|
+
break;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
function checkVersionCoherence(root, structural) {
|
|
264
|
+
let entries;
|
|
265
|
+
try {
|
|
266
|
+
entries = (0, bundles_1.readCatalog)(root);
|
|
267
|
+
}
|
|
268
|
+
catch (e) {
|
|
269
|
+
structural.push(`catalog.json unreadable: ${e instanceof Error ? e.message : String(e)}`);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for (const entry of entries) {
|
|
273
|
+
if (typeof entry.version !== 'string')
|
|
274
|
+
continue;
|
|
275
|
+
const bundlePath = path_1.default.join(root, entry.source, 'bundle.json');
|
|
276
|
+
let stat;
|
|
277
|
+
try {
|
|
278
|
+
stat = fs_1.default.lstatSync(bundlePath);
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
if (isMissing(error))
|
|
282
|
+
continue;
|
|
283
|
+
structural.push(`Cannot inspect ${path_1.default.relative(root, bundlePath)}`);
|
|
284
|
+
continue;
|
|
285
|
+
}
|
|
286
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
287
|
+
continue;
|
|
288
|
+
let raw;
|
|
289
|
+
try {
|
|
290
|
+
raw = JSON.parse(fs_1.default.readFileSync(bundlePath, 'utf-8'));
|
|
291
|
+
}
|
|
292
|
+
catch (e) {
|
|
293
|
+
structural.push(`bundle.json invalid at ${path_1.default.relative(root, bundlePath)}: ${e instanceof Error ? e.message : String(e)}`);
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
const bundleVersion = raw !== null && typeof raw === 'object' && typeof raw.version === 'string'
|
|
297
|
+
? raw.version
|
|
298
|
+
: undefined;
|
|
299
|
+
if (bundleVersion !== undefined && bundleVersion !== entry.version) {
|
|
300
|
+
structural.push(`Bundle "${entry.name}" version skew: catalog=${entry.version} bundle.json=${bundleVersion} (RF-4.7)`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Structural + advisory audit of a registry tree. Path-only API (RF-5.1) — schema/doctor are the gates.
|
|
306
|
+
*/
|
|
307
|
+
function doctorRegistry(root) {
|
|
308
|
+
const structural = [];
|
|
309
|
+
const advisories = [];
|
|
310
|
+
if (typeof root !== 'string' || root.trim() === '') {
|
|
311
|
+
return {
|
|
312
|
+
structural: ['Registry root must be a non-empty string'],
|
|
313
|
+
advisories,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
const resolvedRoot = path_1.default.resolve(root);
|
|
317
|
+
if (!fs_1.default.existsSync(resolvedRoot) || !fs_1.default.statSync(resolvedRoot).isDirectory()) {
|
|
318
|
+
return { structural: [`Registry root does not exist or is not a directory: ${resolvedRoot}`], advisories };
|
|
319
|
+
}
|
|
320
|
+
let manifestMin;
|
|
321
|
+
try {
|
|
322
|
+
const manifest = (0, registries_1.readRegistryManifest)(resolvedRoot);
|
|
323
|
+
manifestMin = manifest.minCliVersion;
|
|
324
|
+
}
|
|
325
|
+
catch (e) {
|
|
326
|
+
structural.push(`awm-registry.json invalid: ${e instanceof Error ? e.message : String(e)}`);
|
|
327
|
+
}
|
|
328
|
+
const handbookPath = path_1.default.join(resolvedRoot, 'REGISTRY.md');
|
|
329
|
+
if (!fs_1.default.existsSync(handbookPath)) {
|
|
330
|
+
structural.push('missing REGISTRY.md');
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
let handbook;
|
|
334
|
+
try {
|
|
335
|
+
handbook = fs_1.default.readFileSync(handbookPath, 'utf-8');
|
|
336
|
+
}
|
|
337
|
+
catch (e) {
|
|
338
|
+
structural.push(`Cannot read REGISTRY.md: ${e instanceof Error ? e.message : String(e)}`);
|
|
339
|
+
handbook = '';
|
|
340
|
+
}
|
|
341
|
+
if (handbook) {
|
|
342
|
+
const caps = parseCapabilitiesIndex(handbook);
|
|
343
|
+
structural.push(...caps.errors);
|
|
344
|
+
for (const row of caps.rows) {
|
|
345
|
+
if (!KNOWN_CAPABILITY_IDS.has(row.id)) {
|
|
346
|
+
if (advisories.length < ADVISORY_CAP) {
|
|
347
|
+
advisories.push(`Unknown capability id "${row.id}" in REGISTRY.md (advisory; RF-4.6)`);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
checkVersionCoherence(resolvedRoot, structural);
|
|
354
|
+
scanEnvStoreInTree(resolvedRoot, structural);
|
|
355
|
+
const factsPath = path_1.default.join(resolvedRoot, 'facts.json');
|
|
356
|
+
let catalog = null;
|
|
357
|
+
let factsPresent = false;
|
|
358
|
+
try {
|
|
359
|
+
const factsStat = fs_1.default.lstatSync(factsPath);
|
|
360
|
+
factsPresent = true;
|
|
361
|
+
if (factsStat.isSymbolicLink()) {
|
|
362
|
+
structural.push('facts.json must not be a symbolic link (regular-file guard)');
|
|
363
|
+
}
|
|
364
|
+
else if (!factsStat.isFile()) {
|
|
365
|
+
structural.push('facts.json must be a regular file');
|
|
366
|
+
}
|
|
367
|
+
else {
|
|
368
|
+
try {
|
|
369
|
+
catalog = (0, catalog_1.parseFactsCatalog)(factsPath);
|
|
370
|
+
}
|
|
371
|
+
catch (e) {
|
|
372
|
+
structural.push(`facts.json invalid: ${e instanceof Error ? e.message : String(e)}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
catch (error) {
|
|
377
|
+
if (!isMissing(error)) {
|
|
378
|
+
structural.push(`Cannot inspect facts.json: ${error instanceof Error ? error.message : String(error)}`);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
const skillPaths = collectSkillMdPaths(resolvedRoot, structural);
|
|
382
|
+
let anyNeeds = false;
|
|
383
|
+
for (const skillMd of skillPaths) {
|
|
384
|
+
let needs;
|
|
385
|
+
try {
|
|
386
|
+
needs = (0, needs_1.readSkillNeeds)(skillMd);
|
|
387
|
+
}
|
|
388
|
+
catch (e) {
|
|
389
|
+
if (e instanceof types_1.SkillNeedsError) {
|
|
390
|
+
structural.push(e.message);
|
|
391
|
+
continue;
|
|
392
|
+
}
|
|
393
|
+
structural.push(`Cannot read needs from ${path_1.default.relative(resolvedRoot, skillMd)}`);
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if (needs.length === 0)
|
|
397
|
+
continue;
|
|
398
|
+
anyNeeds = true;
|
|
399
|
+
if (!catalog) {
|
|
400
|
+
const reason = factsPresent
|
|
401
|
+
? 'facts.json is invalid/unreadable'
|
|
402
|
+
: 'facts.json is missing';
|
|
403
|
+
structural.push(`Skill ${path_1.default.relative(resolvedRoot, skillMd)} declares needs but ${reason} (fail closed)`);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const orphans = (0, needs_1.findOrphanNeeds)(needs, catalog);
|
|
407
|
+
for (const id of orphans) {
|
|
408
|
+
structural.push(`Orphan need "${id}" in ${path_1.default.relative(resolvedRoot, skillMd)} — not in facts.json (RF-1.3)`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
if (factsPresent || anyNeeds) {
|
|
412
|
+
if (!manifestMin) {
|
|
413
|
+
structural.push(`minCliVersion required when facts/needs are present (must be >= ${types_1.FACTS_MIN_CLI_VERSION}) (RF-4.8)`);
|
|
414
|
+
}
|
|
415
|
+
else {
|
|
416
|
+
try {
|
|
417
|
+
if ((0, versioning_1.compareSemver)(manifestMin, types_1.FACTS_MIN_CLI_VERSION) < 0) {
|
|
418
|
+
structural.push(`minCliVersion ${manifestMin} is below FACTS_MIN_CLI_VERSION ${types_1.FACTS_MIN_CLI_VERSION} (RF-4.8)`);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
catch (e) {
|
|
422
|
+
structural.push(`minCliVersion unreadable for facts gate: ${e instanceof Error ? e.message : String(e)}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
scanProsePathHints(resolvedRoot, advisories);
|
|
427
|
+
return { structural, advisories };
|
|
428
|
+
}
|
|
@@ -8,6 +8,7 @@ exports.registerRegistryCommand = registerRegistryCommand;
|
|
|
8
8
|
const fs_1 = __importDefault(require("fs"));
|
|
9
9
|
const prompts_1 = require("@clack/prompts");
|
|
10
10
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
11
|
+
const path_1 = __importDefault(require("path"));
|
|
11
12
|
const registries_1 = require("../../core/registries");
|
|
12
13
|
const discovery_1 = require("../../core/discovery");
|
|
13
14
|
const bundles_1 = require("../../core/bundles");
|
|
@@ -16,10 +17,22 @@ const regenerate_1 = require("../../core/context/regenerate");
|
|
|
16
17
|
const add_1 = require("./add");
|
|
17
18
|
const remove_1 = require("./remove");
|
|
18
19
|
const status_1 = require("./status");
|
|
20
|
+
const doctor_1 = require("./doctor");
|
|
21
|
+
const scaffold_1 = require("./scaffold");
|
|
19
22
|
const config_1 = require("../../utils/config");
|
|
20
23
|
const profile_1 = require("../../core/profile");
|
|
21
24
|
const providers_1 = require("../../providers");
|
|
22
25
|
const install_bundles_1 = require("./install-bundles");
|
|
26
|
+
function resolveDoctorTarget(nameOrPath) {
|
|
27
|
+
if (!nameOrPath) {
|
|
28
|
+
throw new Error('Specify a registry name or path: awm registry doctor <name|path>');
|
|
29
|
+
}
|
|
30
|
+
const asPath = path_1.default.resolve(nameOrPath);
|
|
31
|
+
if (fs_1.default.existsSync(asPath) && fs_1.default.statSync(asPath).isDirectory()) {
|
|
32
|
+
return asPath;
|
|
33
|
+
}
|
|
34
|
+
return (0, registries_1.registryContentRoot)(nameOrPath);
|
|
35
|
+
}
|
|
23
36
|
function registerRegistryCommand(program) {
|
|
24
37
|
const reg = program.command('registry').description('manage additional content registries (team/personal)');
|
|
25
38
|
reg.command('add <remote>')
|
|
@@ -173,4 +186,43 @@ function registerRegistryCommand(program) {
|
|
|
173
186
|
}
|
|
174
187
|
(0, prompts_1.outro)(`✅ Registry ${picocolors_1.default.cyan(name)} removed.`);
|
|
175
188
|
});
|
|
189
|
+
reg.command('scaffold <dir>')
|
|
190
|
+
.description('create a minimal registry layout (handbook + facts) that passes doctor')
|
|
191
|
+
.action((dir) => {
|
|
192
|
+
(0, prompts_1.intro)(picocolors_1.default.bgCyan(picocolors_1.default.black(' AWM - Scaffold Registry ')));
|
|
193
|
+
const result = (0, scaffold_1.scaffoldRegistry)(dir);
|
|
194
|
+
if (!result.ok) {
|
|
195
|
+
console.error(picocolors_1.default.red(result.error));
|
|
196
|
+
process.exit(1);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
(0, prompts_1.outro)(`✅ Registry scaffolded at ${picocolors_1.default.cyan(result.dir)}. Run ${picocolors_1.default.cyan('awm registry doctor ' + result.dir)} to audit.`);
|
|
200
|
+
});
|
|
201
|
+
reg.command('doctor [nameOrPath]')
|
|
202
|
+
.description('audit a registry tree (structural failures vs advisories)')
|
|
203
|
+
.action((nameOrPath) => {
|
|
204
|
+
let root;
|
|
205
|
+
try {
|
|
206
|
+
root = resolveDoctorTarget(nameOrPath);
|
|
207
|
+
}
|
|
208
|
+
catch (e) {
|
|
209
|
+
console.error(picocolors_1.default.red(e instanceof Error ? e.message : String(e)));
|
|
210
|
+
process.exit(1);
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const report = (0, doctor_1.doctorRegistry)(root);
|
|
214
|
+
for (const msg of report.structural) {
|
|
215
|
+
console.error(picocolors_1.default.red(`structural: ${msg}`));
|
|
216
|
+
}
|
|
217
|
+
for (const msg of report.advisories) {
|
|
218
|
+
console.warn(picocolors_1.default.yellow(`advisory: ${msg}`));
|
|
219
|
+
}
|
|
220
|
+
if (report.structural.length === 0 && report.advisories.length === 0) {
|
|
221
|
+
console.log(picocolors_1.default.green(`Registry doctor: ok (${root})`));
|
|
222
|
+
}
|
|
223
|
+
else if (report.structural.length === 0) {
|
|
224
|
+
console.log(picocolors_1.default.green(`Registry doctor: ok with ${report.advisories.length} advisory(ies)`));
|
|
225
|
+
}
|
|
226
|
+
process.exit((0, doctor_1.doctorExitCode)(report));
|
|
227
|
+
});
|
|
176
228
|
}
|