agentic-workflow-manager 9.16.1 → 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/preflight/checks.js +63 -13
- 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/sensors/bootstrap.js +2 -1
- package/dist/src/commands/sensors/compatibility/contract.js +30 -26
- package/dist/src/commands/sensors/compatibility/discovery.js +21 -0
- package/dist/src/commands/sensors/compatibility/manifest.js +169 -17
- package/dist/src/commands/sensors/compatibility/materialize.js +30 -5
- package/dist/src/commands/sensors/compatibility/source.js +3 -2
- package/dist/src/commands/sensors/coverage/render.js +8 -2
- package/dist/src/commands/sensors/coverage/resolve.js +7 -0
- package/dist/src/commands/sensors/init.js +15 -2
- package/dist/src/commands/sensors/migrate.js +13 -10
- package/dist/src/commands/sensors/prepare.js +53 -6
- package/dist/src/commands/sensors/project.js +60 -6
- package/dist/src/commands/sensors/result.js +18 -3
- package/dist/src/commands/sensors/run.js +124 -16
- package/dist/src/commands/sensors/status.js +163 -18
- 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/core/init/steps.js +5 -1
- package/dist/src/index.js +2 -0
- package/dist/tests/commands/env/index.test.js +261 -0
- package/dist/tests/commands/preflight/preflight.test.js +121 -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/sensors/bootstrap.test.js +10 -0
- package/dist/tests/commands/sensors/compatibility/manifest.test.js +204 -0
- package/dist/tests/commands/sensors/init.test.js +107 -0
- package/dist/tests/commands/sensors/migrate.test.js +4 -3
- package/dist/tests/commands/sensors/partial-pack-bootstrap.test.js +1 -1
- package/dist/tests/commands/sensors/prepare.test.js +62 -0
- package/dist/tests/commands/sensors/result.test.js +66 -0
- package/dist/tests/commands/sensors/run.test.js +331 -0
- package/dist/tests/commands/sensors/status.test.js +204 -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/native-release-artifacts.test.js +4 -2
- package/dist/tests/structural/operator-environments-docs-contract.test.js +70 -0
- package/dist/tests/structural/project-declared-sensors-docs-contract.test.js +104 -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,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
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
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.scaffoldRegistry = scaffoldRegistry;
|
|
7
|
+
// cli/src/commands/registry/scaffold.ts
|
|
8
|
+
// Logic for `awm registry scaffold`, separated from commander wiring.
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const types_1 = require("../../core/facts/types");
|
|
12
|
+
function writeJson(write, file, value) {
|
|
13
|
+
write(file, JSON.stringify(value, null, 2) + '\n');
|
|
14
|
+
}
|
|
15
|
+
function handbookFor(name) {
|
|
16
|
+
return `# ${name}
|
|
17
|
+
|
|
18
|
+
## Capabilities
|
|
19
|
+
| id | status | notes |
|
|
20
|
+
|----|--------|-------|
|
|
21
|
+
| facts | active | see ## Capability: facts |
|
|
22
|
+
|
|
23
|
+
## Capability: facts
|
|
24
|
+
|
|
25
|
+
Declare operator/machine facts in \`facts.json\`. Skills may list \`needs\` in
|
|
26
|
+
frontmatter; values live in the machine-local environment store
|
|
27
|
+
(\`AWM_HOME/environments/\`), never in this registry tree.
|
|
28
|
+
|
|
29
|
+
- Add fact definitions under \`facts\` in \`facts.json\` (\`schemaVersion: 1\`).
|
|
30
|
+
- Keep \`minCliVersion\` in \`awm-registry.json\` at or above the CLI version that
|
|
31
|
+
understands facts.
|
|
32
|
+
- Run \`awm registry doctor\` before publishing — structural failures block; advisories do not.
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
function writeScaffoldTree(root, name, write) {
|
|
36
|
+
writeJson(write, path_1.default.join(root, 'awm-registry.json'), {
|
|
37
|
+
overrides: [],
|
|
38
|
+
minCliVersion: types_1.FACTS_MIN_CLI_VERSION,
|
|
39
|
+
});
|
|
40
|
+
writeJson(write, path_1.default.join(root, 'catalog.json'), {
|
|
41
|
+
version: 1,
|
|
42
|
+
bundles: [],
|
|
43
|
+
});
|
|
44
|
+
const skillsDir = path_1.default.join(root, 'skills');
|
|
45
|
+
fs_1.default.mkdirSync(skillsDir, { recursive: true });
|
|
46
|
+
// Track skills/ in git — an empty dir is dropped on clone and then
|
|
47
|
+
// validateRegistryLayout / awm registry add reject the tree (RF-4.1).
|
|
48
|
+
write(path_1.default.join(skillsDir, '.gitkeep'), '');
|
|
49
|
+
writeJson(write, path_1.default.join(root, 'facts.json'), {
|
|
50
|
+
schemaVersion: 1,
|
|
51
|
+
facts: {},
|
|
52
|
+
});
|
|
53
|
+
write(path_1.default.join(root, 'REGISTRY.md'), handbookFor(name));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Create a minimal registry tree that passes structural `doctorRegistry`.
|
|
57
|
+
* Atomic (RF-4.3): write into a temp sibling, then rename into place; on
|
|
58
|
+
* failure remove only the staging dir so an empty preexisting target stays
|
|
59
|
+
* empty (retryable) and a missing target stays absent.
|
|
60
|
+
*/
|
|
61
|
+
function scaffoldRegistry(dir, opts = {}) {
|
|
62
|
+
if (typeof dir !== 'string' || dir.trim() === '') {
|
|
63
|
+
return { ok: false, error: 'scaffold target directory is required' };
|
|
64
|
+
}
|
|
65
|
+
const abs = path_1.default.resolve(dir);
|
|
66
|
+
const write = opts.writeFileSync ?? ((file, data) => fs_1.default.writeFileSync(file, data));
|
|
67
|
+
let targetExistsEmpty = false;
|
|
68
|
+
if (fs_1.default.existsSync(abs)) {
|
|
69
|
+
let stat;
|
|
70
|
+
try {
|
|
71
|
+
stat = fs_1.default.lstatSync(abs);
|
|
72
|
+
}
|
|
73
|
+
catch (e) {
|
|
74
|
+
return {
|
|
75
|
+
ok: false,
|
|
76
|
+
error: `Cannot inspect target ${abs}: ${e instanceof Error ? e.message : String(e)}`,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) {
|
|
80
|
+
return { ok: false, error: `Target exists and is not a directory: ${abs}` };
|
|
81
|
+
}
|
|
82
|
+
if (fs_1.default.readdirSync(abs).length > 0) {
|
|
83
|
+
return {
|
|
84
|
+
ok: false,
|
|
85
|
+
error: `Target directory is not empty — refuse to overwrite: ${abs}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
targetExistsEmpty = true;
|
|
89
|
+
}
|
|
90
|
+
const parent = path_1.default.dirname(abs);
|
|
91
|
+
let staging;
|
|
92
|
+
try {
|
|
93
|
+
fs_1.default.mkdirSync(parent, { recursive: true });
|
|
94
|
+
staging = fs_1.default.mkdtempSync(path_1.default.join(parent, '.awm-scaffold-'));
|
|
95
|
+
const name = path_1.default.basename(abs) || 'registry';
|
|
96
|
+
// Always stage first so mid-fail never leaves a partial target (RF-4.3).
|
|
97
|
+
writeScaffoldTree(staging, name, write);
|
|
98
|
+
if (targetExistsEmpty) {
|
|
99
|
+
// Never rmdir the empty shell before contents land. Write into abs
|
|
100
|
+
// directly from the validated staging pattern, then drop staging.
|
|
101
|
+
try {
|
|
102
|
+
writeScaffoldTree(abs, name, write);
|
|
103
|
+
}
|
|
104
|
+
catch (writeErr) {
|
|
105
|
+
// Best-effort: remove what we wrote into the preexisting empty
|
|
106
|
+
// dir without deleting the empty shell itself.
|
|
107
|
+
try {
|
|
108
|
+
for (const entry of fs_1.default.readdirSync(abs)) {
|
|
109
|
+
fs_1.default.rmSync(path_1.default.join(abs, entry), { recursive: true, force: true });
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// best-effort
|
|
114
|
+
}
|
|
115
|
+
throw writeErr;
|
|
116
|
+
}
|
|
117
|
+
fs_1.default.rmSync(staging, { recursive: true, force: true });
|
|
118
|
+
staging = undefined;
|
|
119
|
+
return { ok: true, dir: abs };
|
|
120
|
+
}
|
|
121
|
+
// Target does not exist: atomic rename of staging into place.
|
|
122
|
+
fs_1.default.renameSync(staging, abs);
|
|
123
|
+
staging = undefined;
|
|
124
|
+
return { ok: true, dir: abs };
|
|
125
|
+
}
|
|
126
|
+
catch (e) {
|
|
127
|
+
if (staging) {
|
|
128
|
+
try {
|
|
129
|
+
fs_1.default.rmSync(staging, { recursive: true, force: true });
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// best-effort cleanup; surface the original write error
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
ok: false,
|
|
137
|
+
error: `Scaffold failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
}
|
|
@@ -209,7 +209,8 @@ function applySensorBootstrap(plan) {
|
|
|
209
209
|
if (plan.manifest.mode === 'project-sensors') {
|
|
210
210
|
if (!plan.source)
|
|
211
211
|
throw new Error('project-sensors bootstrap plan requires its resolved source');
|
|
212
|
-
(0,
|
|
212
|
+
const packBound = (0, manifest_1.asPackBoundProjectSensors)(plan.manifest);
|
|
213
|
+
(0, materialize_1.materializePortableSensors)({ projectRoot: plan.projectRoot, pack: packBound.pack, source: plan.source, ...(packBound.packageRoot ? { packageRoot: packBound.packageRoot } : {}), sensors: packBound.sensors, configure: plan.configure !== false });
|
|
213
214
|
return 'created';
|
|
214
215
|
}
|
|
215
216
|
const parsed = (0, manifest_1.parseSensorManifest)(plan.manifest, 'bootstrap declaration');
|