@formigio/fazemos-cli 0.10.66 → 0.10.74
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/commands/preflight.d.ts +41 -0
- package/dist/commands/preflight.js +706 -0
- package/dist/commands/preflight.js.map +1 -0
- package/dist/commands/provision.d.ts +29 -0
- package/dist/commands/provision.js +382 -0
- package/dist/commands/provision.js.map +1 -0
- package/dist/index.js +173 -14
- package/dist/index.js.map +1 -1
- package/dist/preflight/repoRead.d.ts +65 -0
- package/dist/preflight/repoRead.js +194 -0
- package/dist/preflight/repoRead.js.map +1 -0
- package/dist/preflight/roster.d.ts +65 -0
- package/dist/preflight/roster.js +153 -0
- package/dist/preflight/roster.js.map +1 -0
- package/dist/preflight/row1_member.d.ts +8 -0
- package/dist/preflight/row1_member.js +64 -0
- package/dist/preflight/row1_member.js.map +1 -0
- package/dist/preflight/row2_roleRegistration.d.ts +18 -0
- package/dist/preflight/row2_roleRegistration.js +156 -0
- package/dist/preflight/row2_roleRegistration.js.map +1 -0
- package/dist/preflight/row3_persona.d.ts +24 -0
- package/dist/preflight/row3_persona.js +92 -0
- package/dist/preflight/row3_persona.js.map +1 -0
- package/dist/preflight/row3_playbook.d.ts +30 -0
- package/dist/preflight/row3_playbook.js +142 -0
- package/dist/preflight/row3_playbook.js.map +1 -0
- package/dist/preflight/row3_rolesJson.d.ts +12 -0
- package/dist/preflight/row3_rolesJson.js +38 -0
- package/dist/preflight/row3_rolesJson.js.map +1 -0
- package/dist/preflight/row3_workspace.d.ts +24 -0
- package/dist/preflight/row3_workspace.js +253 -0
- package/dist/preflight/row3_workspace.js.map +1 -0
- package/dist/preflight/row4_ssm.d.ts +57 -0
- package/dist/preflight/row4_ssm.js +235 -0
- package/dist/preflight/row4_ssm.js.map +1 -0
- package/dist/preflight/row5_workerLambdaWakeConfig.d.ts +20 -0
- package/dist/preflight/row5_workerLambdaWakeConfig.js +113 -0
- package/dist/preflight/row5_workerLambdaWakeConfig.js.map +1 -0
- package/dist/preflight/row6_ingest.d.ts +20 -0
- package/dist/preflight/row6_ingest.js +178 -0
- package/dist/preflight/row6_ingest.js.map +1 -0
- package/dist/preflight/schema.d.ts +175 -0
- package/dist/preflight/schema.js +62 -0
- package/dist/preflight/schema.js.map +1 -0
- package/dist/preflight/workerLambdas.d.ts +22 -0
- package/dist/preflight/workerLambdas.js +25 -0
- package/dist/preflight/workerLambdas.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,706 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* F53 — `fazemos agents preflight <target>`
|
|
3
|
+
*
|
|
4
|
+
* 6-row PASS/FAIL/SKIP/TOOLING-ERROR/XWS_DEFERRED verifier for agent provisioning.
|
|
5
|
+
* Non-zero exit on FAIL (1), TOOLING-ERROR (2), or XWS_DEFERRED (3).
|
|
6
|
+
*
|
|
7
|
+
* Target forms:
|
|
8
|
+
* <org>/<project>/<role> — single project row-set
|
|
9
|
+
* <org>/<project> — fan-out all declared roles for a project
|
|
10
|
+
* <org>/<role> — fan-out all declared projects for a role
|
|
11
|
+
*
|
|
12
|
+
* Spec: projects/fazemos/specs/tech/platform/F53-agent-provision-tooling-tech-spec.md
|
|
13
|
+
*/
|
|
14
|
+
import chalk from 'chalk';
|
|
15
|
+
import { api, resolveOrgIdMaybeSlug } from '../api.js';
|
|
16
|
+
import { findLocalRegistry } from '../dispatch.js';
|
|
17
|
+
import { getEnv } from '../config.js';
|
|
18
|
+
import { checkGhAvailable, getWorkspaceGitHubRepo, listDirectoryFilenamesOnGitHub, } from '../preflight/repoRead.js';
|
|
19
|
+
import { loadRoster, classifyRoleForProject, getProjectsForRole, getRolesForProject, RosterLoadError, } from '../preflight/roster.js';
|
|
20
|
+
import { checkRow1Member } from '../preflight/row1_member.js';
|
|
21
|
+
import { checkRow2RoleRegistration } from '../preflight/row2_roleRegistration.js';
|
|
22
|
+
import { checkRow3aPersona } from '../preflight/row3_persona.js';
|
|
23
|
+
import { checkRow3bPlaybook } from '../preflight/row3_playbook.js';
|
|
24
|
+
import { checkRow3cRolesJson } from '../preflight/row3_rolesJson.js';
|
|
25
|
+
import { checkRow4Ssm } from '../preflight/row4_ssm.js';
|
|
26
|
+
import { checkRow5WorkerLambdaWakeConfig } from '../preflight/row5_workerLambdaWakeConfig.js';
|
|
27
|
+
import { checkRow6Ingest } from '../preflight/row6_ingest.js';
|
|
28
|
+
import { summarizeRows, computeExitCode, mergeExitCodes, } from '../preflight/schema.js';
|
|
29
|
+
// ── Target parsing ─────────────────────────────────────────────────────────────
|
|
30
|
+
/**
|
|
31
|
+
* Parse <target> into its components.
|
|
32
|
+
* Disambiguation of 2-segment form uses the roster:
|
|
33
|
+
* - If segment[1] matches a role_slug in the roster → fan-out-role
|
|
34
|
+
* - Otherwise → fan-out-project
|
|
35
|
+
*/
|
|
36
|
+
export function parseTarget(target, roster) {
|
|
37
|
+
const parts = target.split('/');
|
|
38
|
+
if (parts.length === 3) {
|
|
39
|
+
return {
|
|
40
|
+
form: 'single',
|
|
41
|
+
orgSlug: parts[0],
|
|
42
|
+
projectSlug: parts[1],
|
|
43
|
+
roleSlug: parts[2],
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
if (parts.length === 2) {
|
|
47
|
+
const orgSlug = parts[0];
|
|
48
|
+
const second = parts[1];
|
|
49
|
+
// Disambiguate: is the second segment a role slug or a project slug?
|
|
50
|
+
if (roster) {
|
|
51
|
+
const allRoles = new Set([
|
|
52
|
+
...roster.declared_here.map((e) => e.role_slug),
|
|
53
|
+
...roster.declared_cross_workspace.map((e) => e.role_slug),
|
|
54
|
+
]);
|
|
55
|
+
if (allRoles.has(second)) {
|
|
56
|
+
return { form: 'fan-out-role', orgSlug, roleSlug: second };
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
// Default: treat as <org>/<project>
|
|
60
|
+
return { form: 'fan-out-project', orgSlug, projectSlug: second };
|
|
61
|
+
}
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
// ── Playbook pre-fetch (shared data for rows 3b and 4c) ───────────────────────
|
|
65
|
+
/**
|
|
66
|
+
* Pre-fetch the playbook-*.md filenames for a role from the workspace GitHub repo.
|
|
67
|
+
*
|
|
68
|
+
* Row 4c needs this to classify SKIP vs FAIL when the role is absent from the
|
|
69
|
+
* triggers allowlist — symmetric with row 3b receiving `triggersAllowlist` from row 4c.
|
|
70
|
+
* Both rows read both inputs; neither verdict depends on the other's verdict.
|
|
71
|
+
*
|
|
72
|
+
* Returns:
|
|
73
|
+
* string[] — playbook-*.md files found (may be empty if none / dir missing)
|
|
74
|
+
* 'unavailable' — gh CLI not installed, not authenticated, or workspace not on GitHub
|
|
75
|
+
*/
|
|
76
|
+
function fetchPlaybookFilesForRole(workspaceRoot, roleSlug, roleEntry) {
|
|
77
|
+
try {
|
|
78
|
+
checkGhAvailable();
|
|
79
|
+
const repoRef = getWorkspaceGitHubRepo(workspaceRoot);
|
|
80
|
+
if (!repoRef)
|
|
81
|
+
return 'unavailable';
|
|
82
|
+
// Derive role directory path (mirrors deriveRoleDir in row3_playbook.ts).
|
|
83
|
+
let roleDir;
|
|
84
|
+
if (roleEntry?.inbox) {
|
|
85
|
+
const trimmed = roleEntry.inbox.replace(/\/+$/, '');
|
|
86
|
+
const parts = trimmed.split('/');
|
|
87
|
+
roleDir = parts[parts.length - 1] === 'inbox'
|
|
88
|
+
? parts.slice(0, -1).join('/')
|
|
89
|
+
: trimmed;
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
roleDir = `roles/${roleSlug}`;
|
|
93
|
+
}
|
|
94
|
+
const files = listDirectoryFilenamesOnGitHub(repoRef, roleDir);
|
|
95
|
+
if (files === null)
|
|
96
|
+
return []; // directory absent → no playbooks
|
|
97
|
+
return files.filter((f) => /^playbook-.+\.md$/.test(f));
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return 'unavailable';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
async function runRowSet(opts) {
|
|
104
|
+
const { orgId, orgSlug, projectId, projectSlug, roleSlug, agentName, workspace, workspaceRoot, registry, env, region, } = opts;
|
|
105
|
+
const roleEntry = registry?.roles[roleSlug];
|
|
106
|
+
// r3.3: Pre-fetch playbook files (sync via gh CLI) so row 4c can classify SKIP vs FAIL
|
|
107
|
+
// for dispatch-driven roles — symmetric with row 3b receiving triggersAllowlist from row 4c.
|
|
108
|
+
// Both rows receive both data inputs; neither verdict depends on the other row's verdict.
|
|
109
|
+
const playbookFiles = fetchPlaybookFilesForRole(workspaceRoot, roleSlug, roleEntry);
|
|
110
|
+
// r3.2: Run row 4 first so row 3b can reuse the SSM triggers allowlist
|
|
111
|
+
// without a second SSM call (rul_f53_row3b_classifies_by_cadence_signal).
|
|
112
|
+
const { rows: rows4, triggersAllowlist } = await checkRow4Ssm({
|
|
113
|
+
env, agentName, roleSlug, region,
|
|
114
|
+
playbookFiles, // r3.3: row 4c uses this to classify SKIP vs FAIL
|
|
115
|
+
});
|
|
116
|
+
// Run remaining rows in parallel, passing SSM context to row 3b.
|
|
117
|
+
const [row1, row2, row3a, row3b, row3c, row5, rows6] = await Promise.all([
|
|
118
|
+
checkRow1Member(orgId, agentName, roleSlug),
|
|
119
|
+
checkRow2RoleRegistration({
|
|
120
|
+
workspace,
|
|
121
|
+
roleSlug,
|
|
122
|
+
orgId,
|
|
123
|
+
projectId,
|
|
124
|
+
orgSlug,
|
|
125
|
+
projectSlug,
|
|
126
|
+
env,
|
|
127
|
+
}),
|
|
128
|
+
checkRow3aPersona({ agentName, orgId, projectId }),
|
|
129
|
+
checkRow3bPlaybook({
|
|
130
|
+
workspaceRoot,
|
|
131
|
+
roleSlug,
|
|
132
|
+
roleEntry,
|
|
133
|
+
ssmContext: { triggers_allowlist: triggersAllowlist },
|
|
134
|
+
}),
|
|
135
|
+
Promise.resolve(checkRow3cRolesJson({ roleSlug, registry })),
|
|
136
|
+
checkRow5WorkerLambdaWakeConfig({ env, region }),
|
|
137
|
+
checkRow6Ingest({ orgId, projectId, projectSlug }),
|
|
138
|
+
]);
|
|
139
|
+
return [row1, row2, row3a, row3b, row3c, ...rows4, row5, ...rows6];
|
|
140
|
+
}
|
|
141
|
+
// ── Human rendering ────────────────────────────────────────────────────────────
|
|
142
|
+
function statusIcon(status) {
|
|
143
|
+
switch (status) {
|
|
144
|
+
case 'pass': return chalk.green('✓ PASS');
|
|
145
|
+
case 'fail': return chalk.red('✗ FAIL');
|
|
146
|
+
case 'skip': return chalk.gray('⊘ SKIP');
|
|
147
|
+
case 'tooling_error': return chalk.yellow('? TOOLING-ERROR');
|
|
148
|
+
case 'xws_deferred': return chalk.cyan('⌛ XWS_DEFERRED');
|
|
149
|
+
case 'indeterminate': return chalk.magenta('⚠ INDETERMINATE');
|
|
150
|
+
default: return String(status).toUpperCase();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function renderRows(rows) {
|
|
154
|
+
for (const row of rows) {
|
|
155
|
+
const icon = statusIcon(row.status);
|
|
156
|
+
console.log(` ${icon} [${row.id}] ${row.name}`);
|
|
157
|
+
console.log(` ${chalk.gray(row.detail)}`);
|
|
158
|
+
if (row.fix_command) {
|
|
159
|
+
console.log(` ${chalk.yellow('fix:')} ${row.fix_command}`);
|
|
160
|
+
}
|
|
161
|
+
else if (row.fix_instruction) {
|
|
162
|
+
console.log(` ${chalk.yellow('fix:')} ${row.fix_instruction}`);
|
|
163
|
+
}
|
|
164
|
+
// Row 5 enumeration note
|
|
165
|
+
if (row.enumerated) {
|
|
166
|
+
console.log(chalk.gray(` (${row.enumerated.count} worker(s), last audited ${row.enumerated.last_audited})`));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function renderSummary(summary) {
|
|
171
|
+
const parts = [`${summary.pass} PASS`, `${summary.fail} FAIL`, `${summary.skip} SKIP`];
|
|
172
|
+
if (summary.tooling_error > 0)
|
|
173
|
+
parts.push(`${summary.tooling_error} TOOLING-ERROR`);
|
|
174
|
+
if (summary.xws_deferred > 0)
|
|
175
|
+
parts.push(`${summary.xws_deferred} XWS_DEFERRED`);
|
|
176
|
+
if (summary.indeterminate > 0)
|
|
177
|
+
parts.push(`${summary.indeterminate} INDETERMINATE`);
|
|
178
|
+
return parts.join(', ');
|
|
179
|
+
}
|
|
180
|
+
function renderXwsDeferred(xws) {
|
|
181
|
+
console.log(chalk.cyan(`⌛ XWS_DEFERRED ${xws.role_slug}: declared cross-workspace; verify from the owning workspace.`));
|
|
182
|
+
console.log(` ${chalk.yellow('fix:')} ${xws.fix_instruction}`);
|
|
183
|
+
}
|
|
184
|
+
// ── Org/project resolution helpers ────────────────────────────────────────────
|
|
185
|
+
async function resolveOrgAndProjects(orgSlug) {
|
|
186
|
+
const orgId = await resolveOrgIdMaybeSlug(orgSlug);
|
|
187
|
+
const data = (await api('GET', `/api/organizations/${orgId}/projects?status=active`, undefined, { noProjectHeader: true }));
|
|
188
|
+
return { orgId, projects: data.projects ?? [] };
|
|
189
|
+
}
|
|
190
|
+
async function resolveProject(orgId, orgSlug, projectSlug) {
|
|
191
|
+
const data = (await api('GET', `/api/organizations/${orgId}/projects?status=active`, undefined, { noProjectHeader: true }));
|
|
192
|
+
const project = (data.projects ?? []).find((p) => p.slug === projectSlug);
|
|
193
|
+
if (!project) {
|
|
194
|
+
throw new Error(`Project '${projectSlug}' not found in org '${orgSlug}'.`);
|
|
195
|
+
}
|
|
196
|
+
return project;
|
|
197
|
+
}
|
|
198
|
+
// ── Main preflight runner ──────────────────────────────────────────────────────
|
|
199
|
+
export async function runPreflight(target, opts) {
|
|
200
|
+
const envConfig = getEnv();
|
|
201
|
+
const envName = opts.env ?? envConfig.name;
|
|
202
|
+
// ── Find workspace root + load roster ────────────────────────────────────
|
|
203
|
+
const startDir = opts.workspace ?? process.cwd();
|
|
204
|
+
const registry = findLocalRegistry(startDir);
|
|
205
|
+
if (!registry) {
|
|
206
|
+
const msg = 'No .fazemos/roles.json found in cwd or any parent. ' +
|
|
207
|
+
'Run from inside a workspace repo or specify --workspace <path>.';
|
|
208
|
+
if (opts.json) {
|
|
209
|
+
process.stdout.write(JSON.stringify({
|
|
210
|
+
schema_version: '1.2',
|
|
211
|
+
target,
|
|
212
|
+
env: envName,
|
|
213
|
+
exit_code: 2,
|
|
214
|
+
error: msg,
|
|
215
|
+
}) + '\n');
|
|
216
|
+
}
|
|
217
|
+
else {
|
|
218
|
+
console.error(chalk.red(`⚠ TOOLING-ERROR: ${msg}`));
|
|
219
|
+
}
|
|
220
|
+
return 2;
|
|
221
|
+
}
|
|
222
|
+
let roster;
|
|
223
|
+
try {
|
|
224
|
+
roster = loadRoster(registry._workspaceRoot, registry);
|
|
225
|
+
}
|
|
226
|
+
catch (err) {
|
|
227
|
+
const msg = err instanceof RosterLoadError ? err.message : String(err);
|
|
228
|
+
if (opts.json) {
|
|
229
|
+
process.stdout.write(JSON.stringify({ schema_version: '1.2', target, env: envName, exit_code: 2, error: msg }) + '\n');
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
console.error(chalk.red(`⚠ TOOLING-ERROR: ${msg}`));
|
|
233
|
+
}
|
|
234
|
+
return 2;
|
|
235
|
+
}
|
|
236
|
+
// ── Parse target ─────────────────────────────────────────────────────────
|
|
237
|
+
const parsed = parseTarget(target, roster);
|
|
238
|
+
if (!parsed) {
|
|
239
|
+
const msg = `Invalid target '${target}'. Use <org>/<project>/<role>, <org>/<project>, or <org>/<role>.`;
|
|
240
|
+
if (opts.json) {
|
|
241
|
+
process.stdout.write(JSON.stringify({ schema_version: '1.2', target, env: envName, exit_code: 2, error: msg }) + '\n');
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
console.error(chalk.red(msg));
|
|
245
|
+
}
|
|
246
|
+
return 2;
|
|
247
|
+
}
|
|
248
|
+
// ── Dispatch to the right form ────────────────────────────────────────────
|
|
249
|
+
if (parsed.form === 'single') {
|
|
250
|
+
return runSingleProject(parsed, registry, roster, opts, envName);
|
|
251
|
+
}
|
|
252
|
+
if (parsed.form === 'fan-out-role') {
|
|
253
|
+
return runFanOutRole(parsed, registry, roster, opts, envName);
|
|
254
|
+
}
|
|
255
|
+
return runFanOutProject(parsed, registry, roster, opts, envName);
|
|
256
|
+
}
|
|
257
|
+
// ── Single-project form ────────────────────────────────────────────────────────
|
|
258
|
+
async function runSingleProject(parsed, registry, roster, opts, envName) {
|
|
259
|
+
const { orgSlug, projectSlug, roleSlug } = parsed;
|
|
260
|
+
// Check roster classification
|
|
261
|
+
const classification = classifyRoleForProject(roster, projectSlug, roleSlug);
|
|
262
|
+
// ── XWS_DEFERRED: role is cross-workspace ─────────────────────────────────
|
|
263
|
+
if (classification === 'declared_cross_workspace') {
|
|
264
|
+
const xwsEntry = roster.declared_cross_workspace.find((e) => e.role_slug === roleSlug);
|
|
265
|
+
const xwsResult = {
|
|
266
|
+
verdict: 'xws_deferred',
|
|
267
|
+
role_slug: roleSlug,
|
|
268
|
+
cross_workspace_source: {
|
|
269
|
+
workspace_path: xwsEntry.workspace_path,
|
|
270
|
+
registry_path: xwsEntry.registry_path,
|
|
271
|
+
},
|
|
272
|
+
fix_instruction: `cd ${xwsEntry.workspace_path} && fazemos agents preflight ${orgSlug}/${projectSlug}/${roleSlug}`,
|
|
273
|
+
};
|
|
274
|
+
if (opts.json) {
|
|
275
|
+
const out = {
|
|
276
|
+
schema_version: '1.2',
|
|
277
|
+
target: `${orgSlug}/${projectSlug}/${roleSlug}`,
|
|
278
|
+
env: envName,
|
|
279
|
+
exit_code: 3,
|
|
280
|
+
org: { slug: orgSlug, id: '' },
|
|
281
|
+
project: { slug: projectSlug, id: '' },
|
|
282
|
+
role: roleSlug,
|
|
283
|
+
xws_deferred: xwsResult,
|
|
284
|
+
summary: { pass: 0, fail: 0, skip: 0, tooling_error: 0, xws_deferred: 1, indeterminate: 0, ok: false },
|
|
285
|
+
};
|
|
286
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
287
|
+
}
|
|
288
|
+
else {
|
|
289
|
+
console.log(chalk.cyan(`\nRoster classification: role '${roleSlug}' is declared cross-workspace in this workspace.`));
|
|
290
|
+
console.log(` Source: ${xwsEntry.workspace_path}/${xwsEntry.registry_path} (cross_workspace_roles block)\n`);
|
|
291
|
+
renderXwsDeferred(xwsResult);
|
|
292
|
+
console.log(`\nResult: 0 PASS, 0 FAIL, 0 SKIP, 1 XWS_DEFERRED.`);
|
|
293
|
+
console.log(`Exit 3.`);
|
|
294
|
+
}
|
|
295
|
+
return 3;
|
|
296
|
+
}
|
|
297
|
+
// Resolve org + project IDs
|
|
298
|
+
let orgId;
|
|
299
|
+
let projectId;
|
|
300
|
+
try {
|
|
301
|
+
orgId = await resolveOrgIdMaybeSlug(orgSlug);
|
|
302
|
+
const project = await resolveProject(orgId, orgSlug, projectSlug);
|
|
303
|
+
projectId = project.id;
|
|
304
|
+
}
|
|
305
|
+
catch (err) {
|
|
306
|
+
const msg = err?.message ?? String(err);
|
|
307
|
+
if (opts.json) {
|
|
308
|
+
process.stdout.write(JSON.stringify({ schema_version: '1.2', target: `${orgSlug}/${projectSlug}/${roleSlug}`, env: envName, exit_code: 2, error: msg }) + '\n');
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
console.error(chalk.red(`⚠ TOOLING-ERROR: ${msg}`));
|
|
312
|
+
}
|
|
313
|
+
return 2;
|
|
314
|
+
}
|
|
315
|
+
// Determine agent name from roster or registry
|
|
316
|
+
const rosterEntry = roster.declared_here.find((e) => e.project_slug === projectSlug && e.role_slug === roleSlug);
|
|
317
|
+
const agentName = rosterEntry?.filler?.identity ??
|
|
318
|
+
registry.roles[roleSlug]?.filler?.identity ??
|
|
319
|
+
roleSlug;
|
|
320
|
+
if (!opts.json) {
|
|
321
|
+
console.log(`\nPreflight: ${orgSlug} / ${projectSlug} / ${roleSlug} (agent: ${agentName})\n`);
|
|
322
|
+
console.log(` Workspace: ${registry.workspace} @ ${registry._registryPath}`);
|
|
323
|
+
console.log(` Checking project '${projectSlug}' — this output is scoped to this project only.\n`);
|
|
324
|
+
}
|
|
325
|
+
const rows = await runRowSet({
|
|
326
|
+
orgId,
|
|
327
|
+
orgSlug,
|
|
328
|
+
projectId,
|
|
329
|
+
projectSlug,
|
|
330
|
+
roleSlug,
|
|
331
|
+
agentName,
|
|
332
|
+
workspace: registry.workspace,
|
|
333
|
+
workspaceRoot: registry._workspaceRoot,
|
|
334
|
+
registry,
|
|
335
|
+
env: envName,
|
|
336
|
+
region: opts.region,
|
|
337
|
+
});
|
|
338
|
+
const summary = summarizeRows(rows);
|
|
339
|
+
const exitCode = computeExitCode(rows, false);
|
|
340
|
+
if (opts.json) {
|
|
341
|
+
const out = {
|
|
342
|
+
schema_version: '1.2',
|
|
343
|
+
target: `${orgSlug}/${projectSlug}/${roleSlug}`,
|
|
344
|
+
env: envName,
|
|
345
|
+
exit_code: exitCode,
|
|
346
|
+
org: { slug: orgSlug, id: orgId },
|
|
347
|
+
project: { slug: projectSlug, id: projectId },
|
|
348
|
+
role: roleSlug,
|
|
349
|
+
agent: agentName,
|
|
350
|
+
workspace_repo: registry.workspace,
|
|
351
|
+
workspace_ref: 'main',
|
|
352
|
+
rows,
|
|
353
|
+
summary,
|
|
354
|
+
};
|
|
355
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
356
|
+
}
|
|
357
|
+
else {
|
|
358
|
+
renderRows(rows);
|
|
359
|
+
console.log(`\n Result: ${renderSummary(summary)}. Exit ${exitCode}.`);
|
|
360
|
+
}
|
|
361
|
+
return exitCode;
|
|
362
|
+
}
|
|
363
|
+
// ── Fan-out: <org>/<role> — all declared projects for a role ──────────────────
|
|
364
|
+
async function runFanOutRole(parsed, registry, roster, opts, envName) {
|
|
365
|
+
const { orgSlug, roleSlug } = parsed;
|
|
366
|
+
const { declared_here: declaredProjects, declared_cross_workspace: xwsEntry } = getProjectsForRole(roster, roleSlug);
|
|
367
|
+
// Empty roster for this role — TOOLING-ERROR (exit 2)
|
|
368
|
+
if (declaredProjects.length === 0 && !xwsEntry) {
|
|
369
|
+
const msg = `Roster: 0 declared projects for role '${roleSlug}'. ` +
|
|
370
|
+
`Add a filler for '${roleSlug}' in .fazemos/roles.json (or projects/<slug>/.fazemos/roles.json), then re-run.`;
|
|
371
|
+
if (opts.json) {
|
|
372
|
+
process.stdout.write(JSON.stringify({
|
|
373
|
+
schema_version: '1.2',
|
|
374
|
+
target: `${orgSlug}/${roleSlug}`,
|
|
375
|
+
env: envName,
|
|
376
|
+
exit_code: 2,
|
|
377
|
+
error: msg,
|
|
378
|
+
}) + '\n');
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
console.error(chalk.red(`⚠ TOOLING-ERROR: ${msg}`));
|
|
382
|
+
}
|
|
383
|
+
return 2;
|
|
384
|
+
}
|
|
385
|
+
// If only cross-workspace — XWS_DEFERRED immediately
|
|
386
|
+
if (declaredProjects.length === 0 && xwsEntry) {
|
|
387
|
+
const fixInstruction = `cd ${xwsEntry.workspace_path} && fazemos agents preflight ${orgSlug}/${roleSlug}`;
|
|
388
|
+
if (opts.json) {
|
|
389
|
+
const out = {
|
|
390
|
+
schema_version: '1.2',
|
|
391
|
+
target: `${orgSlug}/${roleSlug}`,
|
|
392
|
+
env: envName,
|
|
393
|
+
exit_code: 3,
|
|
394
|
+
org: { slug: orgSlug, id: '' },
|
|
395
|
+
role: roleSlug,
|
|
396
|
+
roster: {
|
|
397
|
+
source_files: roster.source_files,
|
|
398
|
+
declared_projects: [],
|
|
399
|
+
cross_workspace_roles: [{
|
|
400
|
+
role_slug: roleSlug,
|
|
401
|
+
workspace_path: xwsEntry.workspace_path,
|
|
402
|
+
registry_path: xwsEntry.registry_path,
|
|
403
|
+
fix_instruction: fixInstruction,
|
|
404
|
+
}],
|
|
405
|
+
},
|
|
406
|
+
org_summary: { declared_projects: 0, xws_deferred_count: xwsEntry ? 1 : 0 },
|
|
407
|
+
};
|
|
408
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
409
|
+
}
|
|
410
|
+
else {
|
|
411
|
+
console.log(chalk.cyan(`\nRoster classification: role '${roleSlug}' is declared cross-workspace in this workspace.`));
|
|
412
|
+
console.log(` Source: ${xwsEntry.workspace_path}/${xwsEntry.registry_path}\n`);
|
|
413
|
+
renderXwsDeferred({
|
|
414
|
+
verdict: 'xws_deferred',
|
|
415
|
+
role_slug: roleSlug,
|
|
416
|
+
cross_workspace_source: { workspace_path: xwsEntry.workspace_path, registry_path: xwsEntry.registry_path },
|
|
417
|
+
fix_instruction: fixInstruction,
|
|
418
|
+
});
|
|
419
|
+
const xwsCount = xwsEntry ? 1 : 0;
|
|
420
|
+
console.log(`\nOrg summary: 0 of 0 declared-here projects processed. ${xwsCount} cross-workspace role(s) declared (unverified).`);
|
|
421
|
+
console.log('Exit 3.');
|
|
422
|
+
}
|
|
423
|
+
return 3;
|
|
424
|
+
}
|
|
425
|
+
if (!opts.json) {
|
|
426
|
+
console.log(`\nRoster: ${declaredProjects.length} declared project(s) for role '${roleSlug}' (from ${roster.source_files.join(', ')}).`);
|
|
427
|
+
if (xwsEntry) {
|
|
428
|
+
console.log(chalk.cyan(` + 1 cross-workspace declaration in ${xwsEntry.workspace_path} (XWS_DEFERRED — not fanned out).`));
|
|
429
|
+
}
|
|
430
|
+
console.log('');
|
|
431
|
+
}
|
|
432
|
+
// Resolve org + active projects
|
|
433
|
+
let orgId;
|
|
434
|
+
let allProjects;
|
|
435
|
+
try {
|
|
436
|
+
const resolved = await resolveOrgAndProjects(orgSlug);
|
|
437
|
+
orgId = resolved.orgId;
|
|
438
|
+
allProjects = resolved.projects;
|
|
439
|
+
}
|
|
440
|
+
catch (err) {
|
|
441
|
+
const msg = err?.message ?? String(err);
|
|
442
|
+
if (opts.json) {
|
|
443
|
+
process.stdout.write(JSON.stringify({ schema_version: '1.2', target: `${orgSlug}/${roleSlug}`, env: envName, exit_code: 2, error: msg }) + '\n');
|
|
444
|
+
}
|
|
445
|
+
else {
|
|
446
|
+
console.error(chalk.red(`⚠ TOOLING-ERROR: ${msg}`));
|
|
447
|
+
}
|
|
448
|
+
return 2;
|
|
449
|
+
}
|
|
450
|
+
const declaredSlugs = new Set(declaredProjects.map((e) => e.project_slug));
|
|
451
|
+
const projectResults = [];
|
|
452
|
+
const exitCodes = [];
|
|
453
|
+
// Declared projects (full row-set each)
|
|
454
|
+
for (let i = 0; i < declaredProjects.length; i++) {
|
|
455
|
+
const dp = declaredProjects[i];
|
|
456
|
+
const proj = allProjects.find((p) => p.slug === dp.project_slug);
|
|
457
|
+
if (!proj) {
|
|
458
|
+
if (!opts.json) {
|
|
459
|
+
console.log(chalk.yellow(`WARNING: roles.json declares '${roleSlug}' for project '${dp.project_slug}' but no active project by that slug exists in org '${orgSlug}'. Skipping.`));
|
|
460
|
+
}
|
|
461
|
+
continue;
|
|
462
|
+
}
|
|
463
|
+
if (!opts.json) {
|
|
464
|
+
console.log(`Preflight (project ${i + 1}/${declaredProjects.length} declared): ${orgSlug} / ${dp.project_slug} / ${roleSlug} (agent: ${dp.filler.identity})`);
|
|
465
|
+
}
|
|
466
|
+
const rows = await runRowSet({
|
|
467
|
+
orgId,
|
|
468
|
+
orgSlug,
|
|
469
|
+
projectId: proj.id,
|
|
470
|
+
projectSlug: dp.project_slug,
|
|
471
|
+
roleSlug: roleSlug,
|
|
472
|
+
agentName: dp.filler.identity,
|
|
473
|
+
workspace: registry.workspace,
|
|
474
|
+
workspaceRoot: registry._workspaceRoot,
|
|
475
|
+
registry,
|
|
476
|
+
env: envName,
|
|
477
|
+
region: opts.region,
|
|
478
|
+
});
|
|
479
|
+
const summary = summarizeRows(rows);
|
|
480
|
+
const code = computeExitCode(rows, false);
|
|
481
|
+
exitCodes.push(code);
|
|
482
|
+
if (!opts.json) {
|
|
483
|
+
renderRows(rows);
|
|
484
|
+
console.log(` Result: ${renderSummary(summary)}. Exit contribution: ${code}.\n`);
|
|
485
|
+
}
|
|
486
|
+
projectResults.push({
|
|
487
|
+
project: { slug: dp.project_slug, id: proj.id },
|
|
488
|
+
declared: true,
|
|
489
|
+
role: roleSlug,
|
|
490
|
+
agent: dp.filler.identity,
|
|
491
|
+
rows,
|
|
492
|
+
summary,
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
// XWS_DEFERRED for cross-workspace role
|
|
496
|
+
let hasXwsDeferred = false;
|
|
497
|
+
if (xwsEntry) {
|
|
498
|
+
hasXwsDeferred = true;
|
|
499
|
+
exitCodes.push(3);
|
|
500
|
+
const fixInstruction = `cd ${xwsEntry.workspace_path} && fazemos agents preflight ${orgSlug}/${roleSlug}`;
|
|
501
|
+
if (!opts.json) {
|
|
502
|
+
console.log(chalk.cyan(`⌛ XWS_DEFERRED ${roleSlug}: declared cross-workspace in ${xwsEntry.workspace_path}`));
|
|
503
|
+
console.log(` ${chalk.yellow('fix:')} ${fixInstruction}`);
|
|
504
|
+
}
|
|
505
|
+
projectResults.push({
|
|
506
|
+
project: { slug: '(cross-workspace)', id: '' },
|
|
507
|
+
declared: true,
|
|
508
|
+
declared_cross_workspace: true,
|
|
509
|
+
fix_instruction: fixInstruction,
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
// Undeclared projects (SKIP — not FAIL)
|
|
513
|
+
const undeclaredProjects = allProjects.filter((p) => !declaredSlugs.has(p.slug));
|
|
514
|
+
if (!opts.json && undeclaredProjects.length > 0) {
|
|
515
|
+
console.log('Undeclared projects (SKIP — not FAIL):');
|
|
516
|
+
for (const p of undeclaredProjects) {
|
|
517
|
+
console.log(chalk.gray(` ⊘ SKIP ${p.slug}: role '${roleSlug}' not declared for this project.`));
|
|
518
|
+
}
|
|
519
|
+
console.log('');
|
|
520
|
+
}
|
|
521
|
+
for (const p of undeclaredProjects) {
|
|
522
|
+
projectResults.push({
|
|
523
|
+
project: { slug: p.slug, id: p.id },
|
|
524
|
+
declared: false,
|
|
525
|
+
reason: `role '${roleSlug}' not declared for this project`,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
const finalCode = mergeExitCodes(exitCodes.length > 0 ? exitCodes : [0]);
|
|
529
|
+
const failingProjects = projectResults
|
|
530
|
+
.filter((p) => p.declared && !p.declared_cross_workspace && p.summary && !p.summary.ok)
|
|
531
|
+
.map((p) => p.project.slug);
|
|
532
|
+
if (!opts.json) {
|
|
533
|
+
const allDeclaredPass = failingProjects.length === 0;
|
|
534
|
+
console.log(`Org summary: ${declaredProjects.length - (allProjects.filter(p => !declaredSlugs.has(p.slug)).length)} of ${declaredProjects.length} declared projects PASS.` +
|
|
535
|
+
(undeclaredProjects.length > 0 ? ` ${undeclaredProjects.length} undeclared project(s) SKIPPED.` : '') +
|
|
536
|
+
(failingProjects.length > 0 ? ` FAIL projects: [${failingProjects.join(', ')}].` : '') +
|
|
537
|
+
(xwsEntry ? ` ${xwsEntry ? 1 : 0} cross-workspace role(s) XWS_DEFERRED.` : ''));
|
|
538
|
+
console.log(`Exit ${finalCode}.`);
|
|
539
|
+
}
|
|
540
|
+
else {
|
|
541
|
+
const out = {
|
|
542
|
+
schema_version: '1.2',
|
|
543
|
+
target: `${orgSlug}/${roleSlug}`,
|
|
544
|
+
env: envName,
|
|
545
|
+
exit_code: finalCode,
|
|
546
|
+
org: { slug: orgSlug, id: orgId },
|
|
547
|
+
role: roleSlug,
|
|
548
|
+
roster: {
|
|
549
|
+
source_files: roster.source_files,
|
|
550
|
+
declared_projects: declaredProjects.map((e) => e.project_slug),
|
|
551
|
+
undeclared_projects: undeclaredProjects.map((p) => p.slug),
|
|
552
|
+
...(xwsEntry ? {
|
|
553
|
+
cross_workspace_roles: [{
|
|
554
|
+
role_slug: roleSlug,
|
|
555
|
+
workspace_path: xwsEntry.workspace_path,
|
|
556
|
+
registry_path: xwsEntry.registry_path,
|
|
557
|
+
fix_instruction: `cd ${xwsEntry.workspace_path} && fazemos agents preflight ${orgSlug}/${roleSlug}`,
|
|
558
|
+
}],
|
|
559
|
+
} : {}),
|
|
560
|
+
},
|
|
561
|
+
projects: projectResults,
|
|
562
|
+
org_summary: {
|
|
563
|
+
total_projects: allProjects.length,
|
|
564
|
+
declared_projects: declaredProjects.length,
|
|
565
|
+
undeclared_projects: undeclaredProjects.length,
|
|
566
|
+
all_declared_pass: failingProjects.length === 0 && !hasXwsDeferred,
|
|
567
|
+
failing_projects: failingProjects,
|
|
568
|
+
xws_deferred_count: xwsEntry ? 1 : 0,
|
|
569
|
+
},
|
|
570
|
+
};
|
|
571
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
572
|
+
}
|
|
573
|
+
return finalCode;
|
|
574
|
+
}
|
|
575
|
+
// ── Fan-out: <org>/<project> — all declared roles for a project ───────────────
|
|
576
|
+
async function runFanOutProject(parsed, registry, roster, opts, envName) {
|
|
577
|
+
const { orgSlug, projectSlug } = parsed;
|
|
578
|
+
const { declared_here: declaredRoles, declared_cross_workspace: xwsEntries } = getRolesForProject(roster, projectSlug);
|
|
579
|
+
if (declaredRoles.length === 0 && xwsEntries.length === 0) {
|
|
580
|
+
const msg = `Roster: 0 declared roles for project '${projectSlug}' in this workspace.`;
|
|
581
|
+
if (opts.json) {
|
|
582
|
+
process.stdout.write(JSON.stringify({ schema_version: '1.2', target: `${orgSlug}/${projectSlug}`, env: envName, exit_code: 2, error: msg }) + '\n');
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
console.error(chalk.red(`⚠ TOOLING-ERROR: ${msg}`));
|
|
586
|
+
}
|
|
587
|
+
return 2;
|
|
588
|
+
}
|
|
589
|
+
// Resolve org + project IDs
|
|
590
|
+
let orgId;
|
|
591
|
+
let projectId;
|
|
592
|
+
try {
|
|
593
|
+
orgId = await resolveOrgIdMaybeSlug(orgSlug);
|
|
594
|
+
const project = await resolveProject(orgId, orgSlug, projectSlug);
|
|
595
|
+
projectId = project.id;
|
|
596
|
+
}
|
|
597
|
+
catch (err) {
|
|
598
|
+
const msg = err?.message ?? String(err);
|
|
599
|
+
if (opts.json) {
|
|
600
|
+
process.stdout.write(JSON.stringify({ schema_version: '1.2', target: `${orgSlug}/${projectSlug}`, env: envName, exit_code: 2, error: msg }) + '\n');
|
|
601
|
+
}
|
|
602
|
+
else {
|
|
603
|
+
console.error(chalk.red(`⚠ TOOLING-ERROR: ${msg}`));
|
|
604
|
+
}
|
|
605
|
+
return 2;
|
|
606
|
+
}
|
|
607
|
+
if (!opts.json) {
|
|
608
|
+
console.log(`\nRoster for project '${projectSlug}': ${declaredRoles.length} locally-declared role(s)` +
|
|
609
|
+
(xwsEntries.length > 0 ? ` + ${xwsEntries.length} cross-workspace role(s) (${xwsEntries.map((e) => e.role_slug).join(', ')}).` : '.'));
|
|
610
|
+
console.log('');
|
|
611
|
+
}
|
|
612
|
+
const roleResults = [];
|
|
613
|
+
const exitCodes = [];
|
|
614
|
+
// Run row-sets for locally-declared roles
|
|
615
|
+
for (let i = 0; i < declaredRoles.length; i++) {
|
|
616
|
+
const dr = declaredRoles[i];
|
|
617
|
+
const agentName = dr.filler.identity;
|
|
618
|
+
if (!opts.json) {
|
|
619
|
+
console.log(`Preflight (role ${i + 1}/${declaredRoles.length} declared-here): ${dr.role_slug} (agent: ${agentName})`);
|
|
620
|
+
}
|
|
621
|
+
const rows = await runRowSet({
|
|
622
|
+
orgId,
|
|
623
|
+
orgSlug,
|
|
624
|
+
projectId,
|
|
625
|
+
projectSlug: projectSlug,
|
|
626
|
+
roleSlug: dr.role_slug,
|
|
627
|
+
agentName,
|
|
628
|
+
workspace: registry.workspace,
|
|
629
|
+
workspaceRoot: registry._workspaceRoot,
|
|
630
|
+
registry,
|
|
631
|
+
env: envName,
|
|
632
|
+
region: opts.region,
|
|
633
|
+
});
|
|
634
|
+
const summary = summarizeRows(rows);
|
|
635
|
+
const code = computeExitCode(rows, false);
|
|
636
|
+
exitCodes.push(code);
|
|
637
|
+
if (!opts.json) {
|
|
638
|
+
renderRows(rows);
|
|
639
|
+
console.log(` Result: ${renderSummary(summary)}. Exit contribution: ${code}.\n`);
|
|
640
|
+
}
|
|
641
|
+
roleResults.push({ role: dr.role_slug, declared: true, agent: agentName, rows, summary });
|
|
642
|
+
}
|
|
643
|
+
// XWS_DEFERRED for cross-workspace roles
|
|
644
|
+
if (xwsEntries.length > 0) {
|
|
645
|
+
if (!opts.json) {
|
|
646
|
+
console.log('Cross-workspace roles (XWS_DEFERRED — verify from owning workspace):');
|
|
647
|
+
}
|
|
648
|
+
for (const xws of xwsEntries) {
|
|
649
|
+
exitCodes.push(3);
|
|
650
|
+
const fixInstruction = `cd ${xws.workspace_path} && fazemos agents preflight ${orgSlug}/${xws.role_slug}`;
|
|
651
|
+
if (!opts.json) {
|
|
652
|
+
console.log(chalk.cyan(` ⌛ ${xws.role_slug}: fix: ${fixInstruction}`));
|
|
653
|
+
}
|
|
654
|
+
roleResults.push({
|
|
655
|
+
role: xws.role_slug,
|
|
656
|
+
declared: true,
|
|
657
|
+
declared_cross_workspace: true,
|
|
658
|
+
fix_instruction: fixInstruction,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (!opts.json)
|
|
662
|
+
console.log('');
|
|
663
|
+
}
|
|
664
|
+
const finalCode = mergeExitCodes(exitCodes.length > 0 ? exitCodes : [0]);
|
|
665
|
+
const failingRoles = roleResults
|
|
666
|
+
.filter((r) => r.declared && !r.declared_cross_workspace && r.summary && !r.summary.ok)
|
|
667
|
+
.map((r) => r.role);
|
|
668
|
+
const hasXwsDeferred = xwsEntries.length > 0;
|
|
669
|
+
if (!opts.json) {
|
|
670
|
+
console.log(`Project summary: ${declaredRoles.length} of ${declaredRoles.length} declared-here roles` +
|
|
671
|
+
(failingRoles.length === 0 ? ' PASS.' : ` — ${failingRoles.length} FAIL: [${failingRoles.join(', ')}].`) +
|
|
672
|
+
(hasXwsDeferred ? ` ${xwsEntries.length} cross-workspace role(s) XWS_DEFERRED.` : ''));
|
|
673
|
+
console.log(`Exit ${finalCode}.`);
|
|
674
|
+
}
|
|
675
|
+
else {
|
|
676
|
+
const out = {
|
|
677
|
+
schema_version: '1.2',
|
|
678
|
+
target: `${orgSlug}/${projectSlug}`,
|
|
679
|
+
env: envName,
|
|
680
|
+
exit_code: finalCode,
|
|
681
|
+
org: { slug: orgSlug, id: orgId },
|
|
682
|
+
project: { slug: projectSlug, id: projectId },
|
|
683
|
+
roster: {
|
|
684
|
+
source_files: roster.source_files,
|
|
685
|
+
...(xwsEntries.length > 0 ? {
|
|
686
|
+
cross_workspace_roles: xwsEntries.map((e) => ({
|
|
687
|
+
role_slug: e.role_slug,
|
|
688
|
+
workspace_path: e.workspace_path,
|
|
689
|
+
registry_path: e.registry_path,
|
|
690
|
+
fix_instruction: `cd ${e.workspace_path} && fazemos agents preflight ${orgSlug}/${e.role_slug}`,
|
|
691
|
+
})),
|
|
692
|
+
} : {}),
|
|
693
|
+
},
|
|
694
|
+
roles: roleResults,
|
|
695
|
+
project_summary: {
|
|
696
|
+
total_roles_declared_here: declaredRoles.length,
|
|
697
|
+
cross_workspace_count: xwsEntries.length,
|
|
698
|
+
all_declared_here_pass: failingRoles.length === 0,
|
|
699
|
+
failing_roles: failingRoles,
|
|
700
|
+
},
|
|
701
|
+
};
|
|
702
|
+
process.stdout.write(JSON.stringify(out) + '\n');
|
|
703
|
+
}
|
|
704
|
+
return finalCode;
|
|
705
|
+
}
|
|
706
|
+
//# sourceMappingURL=preflight.js.map
|