@formigio/fazemos-cli 0.10.34 → 0.10.37
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/connectionErrorCopy.d.ts +5 -1
- package/dist/connectionErrorCopy.js +15 -2
- package/dist/connectionErrorCopy.js.map +1 -1
- package/dist/dispatch.d.ts +34 -0
- package/dist/dispatch.js +107 -0
- package/dist/dispatch.js.map +1 -1
- package/dist/index.js +515 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { loadYaml, summarize } from './yaml/load.js';
|
|
|
11
11
|
import { printFindings, printJson } from './yaml/format.js';
|
|
12
12
|
import { validateManifest } from './manifest/checks.js';
|
|
13
13
|
import { validatePath as validateRunbookPath } from './runbook/checks.js';
|
|
14
|
-
import { findLocalRegistry, resolveRole, buildInboxFile, writeInboxFile, writeInboxFileAtPath, buildRolesSyncPayload, findUnprocessedInboxFiles, computeFileHash, gitCommitInboxFile, computeChildDispatchDepth, shouldFallBackToFile, } from './dispatch.js';
|
|
14
|
+
import { findLocalRegistry, resolveRole, buildInboxFile, writeInboxFile, writeInboxFileAtPath, buildRolesSyncPayload, findUnprocessedInboxFiles, computeFileHash, gitCommitInboxFile, gitCommitAndPushInboxFile, isAgentExecution, computeRegistryInboxPath, computeChildDispatchDepth, shouldFallBackToFile, } from './dispatch.js';
|
|
15
15
|
import { execSync } from 'child_process';
|
|
16
16
|
import { registerPauseCommands } from './pause.js';
|
|
17
17
|
import { registerBudgetCommands } from './budget.js';
|
|
@@ -110,6 +110,32 @@ function parseJson(val, flag) {
|
|
|
110
110
|
process.exit(1);
|
|
111
111
|
}
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Parse JSON from an inline string or `@filepath`. Empty string returns null (clears the field).
|
|
115
|
+
* Used by --verification and --uat flags.
|
|
116
|
+
*/
|
|
117
|
+
function parseJsonOrFile(val, flag) {
|
|
118
|
+
if (val === '')
|
|
119
|
+
return null;
|
|
120
|
+
let raw = val;
|
|
121
|
+
if (val.startsWith('@')) {
|
|
122
|
+
const filePath = resolve(val.slice(1));
|
|
123
|
+
try {
|
|
124
|
+
raw = readFileSync(filePath, 'utf-8');
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
console.error(chalk.red(`Cannot read ${flag} file "${filePath}": ${err.message}`));
|
|
128
|
+
process.exit(1);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
try {
|
|
132
|
+
return JSON.parse(raw);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
console.error(chalk.red(`Invalid JSON for ${flag}. Check quoting and syntax.`));
|
|
136
|
+
process.exit(1);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
113
139
|
/** Resolve a value that may be `@filepath` (reads file contents) or inline text. */
|
|
114
140
|
function resolveFileOrInline(val) {
|
|
115
141
|
if (val.startsWith('@')) {
|
|
@@ -1108,7 +1134,8 @@ projects
|
|
|
1108
1134
|
});
|
|
1109
1135
|
projects
|
|
1110
1136
|
.command('create')
|
|
1111
|
-
.description('Create a new project in the active organization. Owner/admin only. Slug is immutable after creation — pick carefully
|
|
1137
|
+
.description('Create a new project in the active organization. Owner/admin only. Slug is immutable after creation — pick carefully.\n\n' +
|
|
1138
|
+
'Next: `fazemos projects setup <slug>` to bind docs and connection.')
|
|
1112
1139
|
.argument('<slug>', 'URL slug (lowercase letters, numbers, hyphens; 1-32 chars). Used in URLs and CLI config — cannot be changed.')
|
|
1113
1140
|
.requiredOption('-n, --name <name>', 'Human-readable name (max 200 chars)')
|
|
1114
1141
|
.option('-d, --description <desc>', 'Optional description (max 2000 chars)')
|
|
@@ -1337,7 +1364,8 @@ projects
|
|
|
1337
1364
|
// ── F16 — projects set-connection (binds a Connection to a project) ─
|
|
1338
1365
|
projects
|
|
1339
1366
|
.command('set-connection')
|
|
1340
|
-
.description('
|
|
1367
|
+
.description('If this is a fresh project, use `fazemos projects setup` instead — that command binds docs + connection together.\n\n' +
|
|
1368
|
+
'Bind a GitHub Connection to a project. Pass "none" to unbind. Owner/admin only.')
|
|
1341
1369
|
.argument('<slug>', 'Project slug')
|
|
1342
1370
|
.argument('<connection>', 'Connection ID, or "none" to unbind')
|
|
1343
1371
|
.action(async (slug, connection) => {
|
|
@@ -1390,6 +1418,200 @@ projects
|
|
|
1390
1418
|
process.exit(1);
|
|
1391
1419
|
}
|
|
1392
1420
|
});
|
|
1421
|
+
// ── F42 — projects setup (binds docs + connection in one call) ──────────────
|
|
1422
|
+
projects
|
|
1423
|
+
.command('setup')
|
|
1424
|
+
.description('Bind docs repo, docs root, and/or VCS connection to an existing project in one audited admin call. ' +
|
|
1425
|
+
'Owner/admin only — agent identities are rejected. At least one flag required.')
|
|
1426
|
+
.argument('<slug>', 'Project slug')
|
|
1427
|
+
.option('--docs-repo <owner/repo>', 'Docs repository in <owner>/<repo> format (omit to leave unchanged)')
|
|
1428
|
+
.option('--docs-root <ROOT|path>', 'Docs root path. Pass ROOT (case-insensitive) for repo root. Omit to leave unchanged.')
|
|
1429
|
+
.option('--vcs-connection-id <uuid>', 'VCS connection UUID to bind (omit to leave unchanged)')
|
|
1430
|
+
.option('--json', 'Emit JSON instead of human-readable output')
|
|
1431
|
+
.action(async (slug, opts) => {
|
|
1432
|
+
// ── Client-side validation ──────────────────────────────────
|
|
1433
|
+
const hasDocsRepo = opts.docsRepo !== undefined;
|
|
1434
|
+
const hasDocsRoot = opts.docsRoot !== undefined;
|
|
1435
|
+
const hasVcsConnectionId = opts.vcsConnectionId !== undefined;
|
|
1436
|
+
if (!hasDocsRepo && !hasDocsRoot && !hasVcsConnectionId) {
|
|
1437
|
+
const msg = 'No flags passed. Provide at least one of --docs-repo, --docs-root, --vcs-connection-id.';
|
|
1438
|
+
if (opts.json) {
|
|
1439
|
+
console.log(JSON.stringify({ error: { code: 'NO_FLAGS_PASSED', message: msg } }));
|
|
1440
|
+
}
|
|
1441
|
+
else {
|
|
1442
|
+
console.error(chalk.red(msg));
|
|
1443
|
+
console.error(chalk.gray('Run: fazemos projects setup --help'));
|
|
1444
|
+
}
|
|
1445
|
+
process.exit(1);
|
|
1446
|
+
return; // guard: process.exit is mocked in tests
|
|
1447
|
+
}
|
|
1448
|
+
if (hasDocsRoot) {
|
|
1449
|
+
const rawRoot = opts.docsRoot;
|
|
1450
|
+
if (rawRoot.trim() === '') {
|
|
1451
|
+
const msg = 'docs_root_empty: --docs-root cannot be an empty string or whitespace. Pass ROOT for repo root or a valid subpath.';
|
|
1452
|
+
if (opts.json) {
|
|
1453
|
+
console.log(JSON.stringify({ error: { code: 'docs_root_empty', message: msg } }));
|
|
1454
|
+
}
|
|
1455
|
+
else {
|
|
1456
|
+
console.error(chalk.red(msg));
|
|
1457
|
+
console.error(chalk.gray('See: docs/architecture/repo-root-vs-subdir-docs.md'));
|
|
1458
|
+
}
|
|
1459
|
+
process.exit(1);
|
|
1460
|
+
return; // guard: process.exit is mocked in tests
|
|
1461
|
+
}
|
|
1462
|
+
if (rawRoot.length > 500) {
|
|
1463
|
+
const msg = `docs_root_too_long: --docs-root exceeds 500 characters (got ${rawRoot.length}).`;
|
|
1464
|
+
if (opts.json) {
|
|
1465
|
+
console.log(JSON.stringify({ error: { code: 'docs_root_too_long', message: msg } }));
|
|
1466
|
+
}
|
|
1467
|
+
else {
|
|
1468
|
+
console.error(chalk.red(msg));
|
|
1469
|
+
}
|
|
1470
|
+
process.exit(1);
|
|
1471
|
+
return; // guard: process.exit is mocked in tests
|
|
1472
|
+
}
|
|
1473
|
+
}
|
|
1474
|
+
if (hasDocsRepo && opts.docsRepo !== null) {
|
|
1475
|
+
const repoPattern = /^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/;
|
|
1476
|
+
if (!repoPattern.test(opts.docsRepo)) {
|
|
1477
|
+
const msg = `docs_repo_shape: --docs-repo must be in <owner>/<repo> format (got: ${opts.docsRepo}).`;
|
|
1478
|
+
if (opts.json) {
|
|
1479
|
+
console.log(JSON.stringify({ error: { code: 'docs_repo_shape', message: msg } }));
|
|
1480
|
+
}
|
|
1481
|
+
else {
|
|
1482
|
+
console.error(chalk.red(msg));
|
|
1483
|
+
}
|
|
1484
|
+
process.exit(1);
|
|
1485
|
+
return; // guard: process.exit is mocked in tests
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
// ── Resolve slug → project ID ──────────────────────────────
|
|
1489
|
+
const orgId = requireActiveOrgOrExit();
|
|
1490
|
+
let project = findProjectBySlug(orgId, slug);
|
|
1491
|
+
if (!project) {
|
|
1492
|
+
await refreshAuthMeCache();
|
|
1493
|
+
project = findProjectBySlug(orgId, slug);
|
|
1494
|
+
}
|
|
1495
|
+
if (!project) {
|
|
1496
|
+
const msg = `Unknown project: ${slug}`;
|
|
1497
|
+
if (opts.json) {
|
|
1498
|
+
console.log(JSON.stringify({ error: { code: 'PROJECT_NOT_FOUND', message: msg } }));
|
|
1499
|
+
}
|
|
1500
|
+
else {
|
|
1501
|
+
console.error(chalk.red(msg));
|
|
1502
|
+
console.log(chalk.gray('Run: fazemos projects list'));
|
|
1503
|
+
}
|
|
1504
|
+
process.exit(1);
|
|
1505
|
+
return; // guard: process.exit is mocked in tests
|
|
1506
|
+
}
|
|
1507
|
+
// ── Build request body ─────────────────────────────────────
|
|
1508
|
+
// OQ-3: ROOT (case-insensitive) → null (means "repo root" at API)
|
|
1509
|
+
const body = {};
|
|
1510
|
+
if (hasDocsRepo)
|
|
1511
|
+
body.docsRepo = opts.docsRepo ?? null;
|
|
1512
|
+
if (hasDocsRoot) {
|
|
1513
|
+
body.docsRoot = opts.docsRoot.toUpperCase() === 'ROOT' ? null : opts.docsRoot;
|
|
1514
|
+
}
|
|
1515
|
+
if (hasVcsConnectionId)
|
|
1516
|
+
body.vcsConnectionId = opts.vcsConnectionId ?? null;
|
|
1517
|
+
// ── API call ───────────────────────────────────────────────
|
|
1518
|
+
try {
|
|
1519
|
+
const data = await api('POST', `/api/admin/projects/${project.id}/setup`, body, { noProjectHeader: true });
|
|
1520
|
+
if (opts.json) {
|
|
1521
|
+
console.log(JSON.stringify({
|
|
1522
|
+
status: 'ok',
|
|
1523
|
+
project: data.project,
|
|
1524
|
+
auditLogId: data.auditLogId,
|
|
1525
|
+
changes: data.changes,
|
|
1526
|
+
}));
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
// ── Human-readable success output (Sage UX §"Success output") ──
|
|
1530
|
+
const p = data.project;
|
|
1531
|
+
const changes = data.changes ?? {};
|
|
1532
|
+
const changedFields = Object.keys(changes);
|
|
1533
|
+
const passedFields = Object.keys(body);
|
|
1534
|
+
const unchangedFields = passedFields.filter(f => !changedFields.includes(f));
|
|
1535
|
+
console.log(chalk.green(`✓ ${p.name} (${p.slug}) — onboarded`));
|
|
1536
|
+
if (changedFields.includes('docsRepo') || passedFields.includes('docsRepo')) {
|
|
1537
|
+
const val = p.docsRepo ?? '(cleared)';
|
|
1538
|
+
const marker = changedFields.includes('docsRepo') ? '' : chalk.gray(' (unchanged)');
|
|
1539
|
+
console.log(` docs_repo: ${val}${marker}`);
|
|
1540
|
+
}
|
|
1541
|
+
if (changedFields.includes('docsRoot') || passedFields.includes('docsRoot')) {
|
|
1542
|
+
const val = p.docsRoot === null ? 'ROOT (repo root — no subdirectory)' : p.docsRoot;
|
|
1543
|
+
const marker = changedFields.includes('docsRoot') ? '' : chalk.gray(' (unchanged)');
|
|
1544
|
+
console.log(` docs_root: ${val}${marker}`);
|
|
1545
|
+
}
|
|
1546
|
+
if (changedFields.includes('vcsConnectionId') || passedFields.includes('vcsConnectionId')) {
|
|
1547
|
+
const val = p.vcsConnectionId ?? '(cleared)';
|
|
1548
|
+
const marker = changedFields.includes('vcsConnectionId') ? '' : chalk.gray(' (unchanged)');
|
|
1549
|
+
console.log(` vcs_connection_id: ${val}${marker}`);
|
|
1550
|
+
}
|
|
1551
|
+
if (unchangedFields.length > 0) {
|
|
1552
|
+
console.log(chalk.gray(` (unchanged: ${unchangedFields.join(', ')})`));
|
|
1553
|
+
}
|
|
1554
|
+
console.log(` Audit: logged as project_setup (id: ${data.auditLogId})`);
|
|
1555
|
+
}
|
|
1556
|
+
catch (err) {
|
|
1557
|
+
if (opts.json) {
|
|
1558
|
+
console.log(JSON.stringify({ error: { code: err.code ?? 'ERROR', message: err.message } }));
|
|
1559
|
+
process.exit(1);
|
|
1560
|
+
}
|
|
1561
|
+
if (err instanceof ApiError) {
|
|
1562
|
+
switch (err.code) {
|
|
1563
|
+
case 'PROJECT_NOT_FOUND':
|
|
1564
|
+
console.error(chalk.red(`Project not found: ${slug}`));
|
|
1565
|
+
console.log(chalk.gray('Run: fazemos projects list'));
|
|
1566
|
+
break;
|
|
1567
|
+
case 'FORBIDDEN_ROLE':
|
|
1568
|
+
console.error(chalk.red('Access denied. Owner or admin role required. Agent identities are not permitted.'));
|
|
1569
|
+
break;
|
|
1570
|
+
case 'AT_LEAST_ONE_FIELD_REQUIRED':
|
|
1571
|
+
console.error(chalk.red('At least one of --docs-repo, --docs-root, --vcs-connection-id must be provided.'));
|
|
1572
|
+
break;
|
|
1573
|
+
case 'VALIDATION_ERROR': {
|
|
1574
|
+
const body2 = (err.body ?? {});
|
|
1575
|
+
const sub = body2.subCode ?? '';
|
|
1576
|
+
if (sub === 'docs_root_empty') {
|
|
1577
|
+
console.error(chalk.red('docs_root_empty: --docs-root cannot be empty or whitespace. Pass ROOT for repo root.'));
|
|
1578
|
+
console.error(chalk.gray('See: docs/architecture/repo-root-vs-subdir-docs.md'));
|
|
1579
|
+
}
|
|
1580
|
+
else if (sub === 'docs_root_too_long') {
|
|
1581
|
+
console.error(chalk.red('docs_root_too_long: --docs-root exceeds 500 characters.'));
|
|
1582
|
+
}
|
|
1583
|
+
else if (sub === 'docs_repo_shape') {
|
|
1584
|
+
console.error(chalk.red('docs_repo_shape: --docs-repo must be in <owner>/<repo> format.'));
|
|
1585
|
+
}
|
|
1586
|
+
else if (sub === 'vcs_connection_uuid') {
|
|
1587
|
+
console.error(chalk.red('vcs_connection_uuid: --vcs-connection-id must be a valid UUID.'));
|
|
1588
|
+
}
|
|
1589
|
+
else {
|
|
1590
|
+
console.error(chalk.red(`Validation error: ${err.message}`));
|
|
1591
|
+
}
|
|
1592
|
+
break;
|
|
1593
|
+
}
|
|
1594
|
+
case 'CONNECTION_NOT_FOUND':
|
|
1595
|
+
console.error(chalk.red('VCS connection not found.'));
|
|
1596
|
+
console.log(chalk.gray('List your connections: fazemos connections list'));
|
|
1597
|
+
break;
|
|
1598
|
+
case 'CONNECTION_CROSS_ORG':
|
|
1599
|
+
console.error(chalk.red('That VCS connection belongs to a different organization.'));
|
|
1600
|
+
break;
|
|
1601
|
+
case 'CONNECTION_NOT_ACTIVE':
|
|
1602
|
+
console.error(chalk.red('VCS connection is not active and cannot be bound.'));
|
|
1603
|
+
console.log(chalk.gray('Pick an active connection: fazemos connections list'));
|
|
1604
|
+
break;
|
|
1605
|
+
default:
|
|
1606
|
+
console.error(chalk.red(err.message));
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
else {
|
|
1610
|
+
console.error(chalk.red(err.message));
|
|
1611
|
+
}
|
|
1612
|
+
process.exit(1);
|
|
1613
|
+
}
|
|
1614
|
+
});
|
|
1393
1615
|
// ── F16 — Connections ───────────────────────────────────────
|
|
1394
1616
|
const connections = program.command('connections').alias('conn').description('GitHub Connection commands');
|
|
1395
1617
|
/**
|
|
@@ -3039,10 +3261,17 @@ templates
|
|
|
3039
3261
|
.command('show')
|
|
3040
3262
|
.description('Show template detail including phases, steps, I/O declarations, pipeline inputs, and revision number. Use this to inspect the full structure of a template and discover phase/step IDs needed by other commands.')
|
|
3041
3263
|
.argument('<id>', 'Template ID (use "tpl list" to find IDs)')
|
|
3042
|
-
.
|
|
3264
|
+
.option('--json', 'Output the template definition as JSON (same as tpl export). Useful for piping/inspecting without a temp file.')
|
|
3265
|
+
.option('--show-sections', 'Print full sections content for all steps (default: truncated at 200 chars)')
|
|
3266
|
+
.action(async (id, opts) => {
|
|
3043
3267
|
try {
|
|
3044
3268
|
const data = await api('GET', `/api/pipeline-templates/${id}`);
|
|
3045
3269
|
const t = data.template;
|
|
3270
|
+
// --json mode: emit definition JSON (same as tpl export)
|
|
3271
|
+
if (opts.json) {
|
|
3272
|
+
console.log(JSON.stringify(t.definition, null, 2));
|
|
3273
|
+
return;
|
|
3274
|
+
}
|
|
3046
3275
|
console.log(chalk.cyan(t.name));
|
|
3047
3276
|
console.log(` ID: ${t.id}`);
|
|
3048
3277
|
console.log(` Status: ${t.status}`);
|
|
@@ -3063,7 +3292,9 @@ templates
|
|
|
3063
3292
|
for (const step of phase.steps || []) {
|
|
3064
3293
|
const reviewer = step.reviewer ? ` reviewer:${step.reviewer}` : '';
|
|
3065
3294
|
const cycles = step.reviewer && step.max_review_cycles ? ` max-cycles:${step.max_review_cycles}` : '';
|
|
3066
|
-
|
|
3295
|
+
// gate and human_approval are equivalent; normalise display
|
|
3296
|
+
const displayType = step.step_type || step.stepType || 'human';
|
|
3297
|
+
console.log(` Step: ${step.name} [${step.role || 'unassigned'}] (${displayType})${reviewer}${cycles}`);
|
|
3067
3298
|
if (step.outputs?.length) {
|
|
3068
3299
|
console.log(' Outputs:');
|
|
3069
3300
|
for (const o of step.outputs) {
|
|
@@ -3125,9 +3356,101 @@ templates
|
|
|
3125
3356
|
if (ac.repos)
|
|
3126
3357
|
console.log(` repos: ${JSON.stringify(ac.repos)}`);
|
|
3127
3358
|
}
|
|
3359
|
+
// sections (agent instructions) — truncated at 200 chars unless --show-sections
|
|
3360
|
+
if (step.sections) {
|
|
3361
|
+
const full = step.sections;
|
|
3362
|
+
if (opts.showSections || full.length <= 200) {
|
|
3363
|
+
console.log(` Sections:\n${full.split('\n').map((l) => ` ${l}`).join('\n')}`);
|
|
3364
|
+
}
|
|
3365
|
+
else {
|
|
3366
|
+
console.log(` Sections: ${full.slice(0, 200)}... (${full.length} chars total)`);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
// verification block
|
|
3370
|
+
if (step.verification) {
|
|
3371
|
+
console.log(` Verification: ${JSON.stringify(step.verification)}`);
|
|
3372
|
+
}
|
|
3373
|
+
// uat block
|
|
3374
|
+
if (step.uat) {
|
|
3375
|
+
console.log(` UAT: ${JSON.stringify(step.uat)}`);
|
|
3376
|
+
}
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
}
|
|
3380
|
+
}
|
|
3381
|
+
catch (err) {
|
|
3382
|
+
console.error(chalk.red(err.message));
|
|
3383
|
+
process.exit(1);
|
|
3384
|
+
}
|
|
3385
|
+
});
|
|
3386
|
+
templates
|
|
3387
|
+
.command('export')
|
|
3388
|
+
.description('Export a template\'s full definition JSON to stdout or a file. Includes all phases, steps, sections, verification, uat, agent_config, I/O wires, and pipeline inputs. The exported JSON is the canonical input to "tpl update-definition". Round-trip: tpl export <id> > def.json && tpl update-definition <id> def.json is a no-op.')
|
|
3389
|
+
.argument('<id>', 'Template ID (use "tpl list" to find IDs)')
|
|
3390
|
+
.option('--out <file>', 'Write output to file instead of stdout')
|
|
3391
|
+
.action(async (id, opts) => {
|
|
3392
|
+
try {
|
|
3393
|
+
const data = await api('GET', `/api/pipeline-templates/${id}`);
|
|
3394
|
+
const t = data.template;
|
|
3395
|
+
const json = JSON.stringify(t.definition, null, 2);
|
|
3396
|
+
if (opts.out) {
|
|
3397
|
+
const outPath = resolve(opts.out);
|
|
3398
|
+
writeFileSync(outPath, json, 'utf-8');
|
|
3399
|
+
console.log(chalk.green(`Definition written to ${outPath}`));
|
|
3400
|
+
}
|
|
3401
|
+
else {
|
|
3402
|
+
console.log(json);
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
catch (err) {
|
|
3406
|
+
console.error(chalk.red(err.message));
|
|
3407
|
+
process.exit(1);
|
|
3408
|
+
}
|
|
3409
|
+
});
|
|
3410
|
+
templates
|
|
3411
|
+
.command('clone')
|
|
3412
|
+
.description('Clone a template into a new draft. The cloned template has the same definition as the source (all phases, steps, sections, verification, uat, I/O wires) but fresh UUIDs for all step/phase IDs. The source template is unchanged. After cloning, use "tpl validate <new-id>" to verify, then "tpl activate" when ready.')
|
|
3413
|
+
.argument('<sourceId>', 'Source template ID to clone from')
|
|
3414
|
+
.requiredOption('--name <name>', 'Name for the new draft template')
|
|
3415
|
+
.option('--description <desc>', 'Description for the new draft template')
|
|
3416
|
+
.action(async (sourceId, opts) => {
|
|
3417
|
+
try {
|
|
3418
|
+
// Fetch source definition
|
|
3419
|
+
const data = await api('GET', `/api/pipeline-templates/${sourceId}`);
|
|
3420
|
+
const src = data.template;
|
|
3421
|
+
// Deep-clone and remap all step/phase UUIDs so no shared IDs exist
|
|
3422
|
+
const srcDef = JSON.parse(JSON.stringify(src.definition));
|
|
3423
|
+
// Build a UUID remap table: old id → new id for phases and steps
|
|
3424
|
+
const idMap = new Map();
|
|
3425
|
+
for (const phase of srcDef.phases || []) {
|
|
3426
|
+
idMap.set(phase.id, crypto.randomUUID());
|
|
3427
|
+
for (const step of phase.steps || []) {
|
|
3428
|
+
idMap.set(step.id, crypto.randomUUID());
|
|
3429
|
+
}
|
|
3430
|
+
}
|
|
3431
|
+
// Apply remapping throughout the definition
|
|
3432
|
+
for (const phase of srcDef.phases || []) {
|
|
3433
|
+
phase.id = idMap.get(phase.id);
|
|
3434
|
+
for (const step of phase.steps || []) {
|
|
3435
|
+
step.id = idMap.get(step.id);
|
|
3436
|
+
// Remap step input source_step_id references
|
|
3437
|
+
for (const inp of step.inputs || []) {
|
|
3438
|
+
if (inp.source_step_id && idMap.has(inp.source_step_id)) {
|
|
3439
|
+
inp.source_step_id = idMap.get(inp.source_step_id);
|
|
3440
|
+
}
|
|
3128
3441
|
}
|
|
3129
3442
|
}
|
|
3130
3443
|
}
|
|
3444
|
+
// Create new draft template
|
|
3445
|
+
const body = { name: opts.name, definition: srcDef };
|
|
3446
|
+
if (opts.description)
|
|
3447
|
+
body.description = opts.description;
|
|
3448
|
+
const created = await api('POST', '/api/pipeline-templates', body);
|
|
3449
|
+
const newTpl = created.template;
|
|
3450
|
+
console.log(chalk.green(`Cloned: ${newTpl.name}`));
|
|
3451
|
+
console.log(` ID: ${newTpl.id}`);
|
|
3452
|
+
console.log(` Source: ${src.name} (${sourceId})`);
|
|
3453
|
+
console.log(chalk.dim(` Run "fazemos tpl validate ${newTpl.id}" to verify the clone.`));
|
|
3131
3454
|
}
|
|
3132
3455
|
catch (err) {
|
|
3133
3456
|
console.error(chalk.red(err.message));
|
|
@@ -3432,7 +3755,7 @@ templates
|
|
|
3432
3755
|
.argument('<templateId>', 'Template ID')
|
|
3433
3756
|
.requiredOption('--phase <phaseId>', 'Phase ID (from "tpl phases", "tpl show", or "tpl add-phase" output)')
|
|
3434
3757
|
.requiredOption('--name <name>', 'Step name (must be unique within the phase)')
|
|
3435
|
-
.option('--type <type>', 'Step type: human (manual task), agent (AI agent), script (automated), gate (approval checkpoint)', 'human')
|
|
3758
|
+
.option('--type <type>', 'Step type: human (manual task), agent (AI agent), script (automated), gate/human_approval (approval checkpoint — gate and human_approval are equivalent aliases)', 'human')
|
|
3436
3759
|
.option('--role <role>', 'Role or agent name (e.g., "kate", "marco", "dev-team")')
|
|
3437
3760
|
.option('--description <desc>', 'Step description')
|
|
3438
3761
|
.option('--reviewer <reviewer>', 'Reviewer role for review steps')
|
|
@@ -3449,6 +3772,8 @@ templates
|
|
|
3449
3772
|
.option('--agent-config <json>', 'Per-step agent config overrides as JSON (e.g., \'{"model":"opus","maxBudgetUsd":20}\'). Overrides agent member defaults for: model, maxBudgetUsd, maxTurns, timeoutMs, cwd, repos')
|
|
3450
3773
|
.option('--stream-silence-abort-ms <ms>', 'Stream silence abort threshold in milliseconds (30000-1800000)', parseStreamSilenceAbortMs)
|
|
3451
3774
|
.option('--eval-max-turns <n>', 'Evaluator turn budget for tool-enabled (agent) evals on this step (8-30). Has no effect on script-typed steps (rejected by API).', parseEvalMaxTurns)
|
|
3775
|
+
.option('--verification <json>', 'Step-level verification config as JSON or @filepath (e.g., \'{"type":"endpoint","target":"{{pipeline.backend_endpoint_target}}",...}\'). Used for TOOL-3 Tooth-1 deploy-guard verification.')
|
|
3776
|
+
.option('--uat <json>', 'Step-level UAT config as JSON or @filepath (e.g., \'{"primary_path":"{{pipeline.uat_primary_path}}","reachable_env":"dev",...}\'). Used for TOOL-3 Tooth-3 UAT replay.')
|
|
3452
3777
|
.action(async (templateId, opts) => {
|
|
3453
3778
|
try {
|
|
3454
3779
|
if (!VALID_STEP_TYPES.includes(opts.type)) {
|
|
@@ -3512,6 +3837,16 @@ templates
|
|
|
3512
3837
|
step.stream_silence_abort_ms = opts.streamSilenceAbortMs;
|
|
3513
3838
|
if (opts.evalMaxTurns !== undefined)
|
|
3514
3839
|
step.eval_max_turns = opts.evalMaxTurns;
|
|
3840
|
+
if (opts.verification != null) {
|
|
3841
|
+
const v = parseJsonOrFile(opts.verification, '--verification');
|
|
3842
|
+
if (v !== null)
|
|
3843
|
+
step.verification = v;
|
|
3844
|
+
}
|
|
3845
|
+
if (opts.uat != null) {
|
|
3846
|
+
const u = parseJsonOrFile(opts.uat, '--uat');
|
|
3847
|
+
if (u !== null)
|
|
3848
|
+
step.uat = u;
|
|
3849
|
+
}
|
|
3515
3850
|
// Positioning: --after or --sort-order
|
|
3516
3851
|
if (opts.after && opts.sortOrder !== undefined) {
|
|
3517
3852
|
console.error(chalk.red('Cannot use both --after and --sort-order'));
|
|
@@ -3598,7 +3933,7 @@ templates
|
|
|
3598
3933
|
.argument('<templateId>', 'Template ID')
|
|
3599
3934
|
.requiredOption('--step <stepId>', 'Step ID (from "tpl steps" output)')
|
|
3600
3935
|
.option('--name <name>', 'New step name (must be unique within the phase)')
|
|
3601
|
-
.option('--type <type>', 'Step type: human, agent, script, gate')
|
|
3936
|
+
.option('--type <type>', 'Step type: human, agent, script, gate/human_approval (gate and human_approval are equivalent aliases for approval checkpoint steps)')
|
|
3602
3937
|
.option('--role <role>', 'Role or agent name')
|
|
3603
3938
|
.option('--description <desc>', 'Description')
|
|
3604
3939
|
.option('--reviewer <reviewer>', 'Reviewer role (empty string to clear)')
|
|
@@ -3614,6 +3949,8 @@ templates
|
|
|
3614
3949
|
.option('--stream-silence-abort-ms <ms>', 'Stream silence abort threshold in milliseconds (30000-1800000)', parseStreamSilenceAbortMs)
|
|
3615
3950
|
.option('--eval-max-turns <n>', 'Evaluator turn budget for tool-enabled (agent) evals on this step (8-30). Mutually exclusive with --clear-eval-max-turns.', parseEvalMaxTurns)
|
|
3616
3951
|
.option('--clear-eval-max-turns', 'Remove a previously-set eval_max_turns from this step, reverting to runner default. Mutually exclusive with --eval-max-turns.')
|
|
3952
|
+
.option('--verification <json>', 'Step-level verification config as JSON or @filepath. Empty string clears the field. (e.g., \'{"type":"endpoint","target":"{{pipeline.backend_endpoint_target}}",...}\')')
|
|
3953
|
+
.option('--uat <json>', 'Step-level UAT config as JSON or @filepath. Empty string clears the field. (e.g., \'{"primary_path":"{{pipeline.uat_primary_path}}","reachable_env":"dev",...}\')')
|
|
3617
3954
|
.action(async (templateId, opts) => {
|
|
3618
3955
|
try {
|
|
3619
3956
|
if (opts.evalMaxTurns !== undefined && opts.clearEvalMaxTurns) {
|
|
@@ -3625,7 +3962,8 @@ templates
|
|
|
3625
3962
|
|| opts.reviewer != null || opts.maxReviewCycles != null || opts.parallelGroup != null
|
|
3626
3963
|
|| opts.image || opts.command || opts.timeout !== undefined || opts.workingDir || opts.env
|
|
3627
3964
|
|| opts.agentConfig || opts.sections != null || opts.streamSilenceAbortMs !== undefined
|
|
3628
|
-
|| opts.evalMaxTurns !== undefined || opts.clearEvalMaxTurns
|
|
3965
|
+
|| opts.evalMaxTurns !== undefined || opts.clearEvalMaxTurns
|
|
3966
|
+
|| opts.verification != null || opts.uat != null;
|
|
3629
3967
|
if (!hasUpdate) {
|
|
3630
3968
|
console.error(chalk.red('Provide at least one field to update'));
|
|
3631
3969
|
process.exit(1);
|
|
@@ -3705,6 +4043,26 @@ templates
|
|
|
3705
4043
|
else if (opts.clearEvalMaxTurns) {
|
|
3706
4044
|
delete step.eval_max_turns;
|
|
3707
4045
|
}
|
|
4046
|
+
// verification block — empty string clears, any other value replaces
|
|
4047
|
+
if (opts.verification != null) {
|
|
4048
|
+
const v = parseJsonOrFile(opts.verification, '--verification');
|
|
4049
|
+
if (v === null) {
|
|
4050
|
+
delete step.verification;
|
|
4051
|
+
}
|
|
4052
|
+
else {
|
|
4053
|
+
step.verification = v;
|
|
4054
|
+
}
|
|
4055
|
+
}
|
|
4056
|
+
// uat block — empty string clears, any other value replaces
|
|
4057
|
+
if (opts.uat != null) {
|
|
4058
|
+
const u = parseJsonOrFile(opts.uat, '--uat');
|
|
4059
|
+
if (u === null) {
|
|
4060
|
+
delete step.uat;
|
|
4061
|
+
}
|
|
4062
|
+
else {
|
|
4063
|
+
step.uat = u;
|
|
4064
|
+
}
|
|
4065
|
+
}
|
|
3708
4066
|
await api('PUT', `/api/pipeline-templates/${templateId}`, { definition: t.definition });
|
|
3709
4067
|
console.log(chalk.green(`Updated step: ${step.name}`));
|
|
3710
4068
|
}
|
|
@@ -5828,25 +6186,127 @@ async function resolveAgentId(nameOrId) {
|
|
|
5828
6186
|
}
|
|
5829
6187
|
agentsCmd
|
|
5830
6188
|
.command('register')
|
|
5831
|
-
.description('Register an agent'
|
|
6189
|
+
.description('Register an agent and optionally provision an API credential (fzm_ key) in the same call. ' +
|
|
6190
|
+
'Credential provisioning is on by default (pass --no-provision-credential to opt out). ' +
|
|
6191
|
+
'Owner/admin only when provisioning credentials; agent identities are rejected.')
|
|
5832
6192
|
.requiredOption('-n, --name <name>', 'Agent display name')
|
|
5833
6193
|
.option('-r, --roles <roles>', 'Comma-separated roles', (v) => v.split(','))
|
|
5834
6194
|
.option('--model <model>', 'Model (e.g., opus, sonnet, system)', 'sonnet')
|
|
6195
|
+
.option('--project <slug>', 'Project slug to scope the credential to (defaults to active project). Required when provisioning a credential.')
|
|
6196
|
+
.option('--no-provision-credential', 'Skip credential provisioning. Default: credential IS provisioned (pass this flag to opt out).')
|
|
6197
|
+
.option('--force-rotate', 'Rotate an existing credential — revoke the old key and mint a new one in the same transaction.')
|
|
6198
|
+
.option('--json', 'Emit JSON instead of human-readable output')
|
|
6199
|
+
// NOTE: --no-provision-credential is the sole option definition; Commander v12 automatically
|
|
6200
|
+
// creates --provision-credential as the positive form and defaults provisionCredential to true.
|
|
5835
6201
|
.action(async (opts) => {
|
|
5836
6202
|
try {
|
|
6203
|
+
// F42 — CLI defaults --provision-credential to true (Sage UX lock).
|
|
6204
|
+
// opts.provisionCredential is `true` unless --no-provision-credential was passed (sets it to false).
|
|
6205
|
+
const provisionCredential = opts.provisionCredential !== false;
|
|
6206
|
+
// Resolve credentialProjectId from --project or active project when provisioning.
|
|
6207
|
+
let credentialProjectId;
|
|
6208
|
+
if (provisionCredential) {
|
|
6209
|
+
if (opts.project) {
|
|
6210
|
+
// Resolve slug → id via cache; refresh once on miss.
|
|
6211
|
+
const orgId = getActiveOrgId();
|
|
6212
|
+
if (orgId) {
|
|
6213
|
+
let proj = findProjectBySlug(orgId, opts.project);
|
|
6214
|
+
if (!proj) {
|
|
6215
|
+
await refreshAuthMeCache();
|
|
6216
|
+
proj = findProjectBySlug(orgId, opts.project);
|
|
6217
|
+
}
|
|
6218
|
+
if (proj) {
|
|
6219
|
+
credentialProjectId = proj.id;
|
|
6220
|
+
}
|
|
6221
|
+
else {
|
|
6222
|
+
const msg = `Unknown project: ${opts.project}`;
|
|
6223
|
+
if (opts.json) {
|
|
6224
|
+
console.log(JSON.stringify({ error: { code: 'UNKNOWN_PROJECT', message: msg } }));
|
|
6225
|
+
}
|
|
6226
|
+
else {
|
|
6227
|
+
console.error(chalk.red(msg));
|
|
6228
|
+
console.log(chalk.gray('Run: fazemos projects list'));
|
|
6229
|
+
}
|
|
6230
|
+
process.exit(1);
|
|
6231
|
+
return; // guard: process.exit is mocked in tests
|
|
6232
|
+
}
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
else {
|
|
6236
|
+
credentialProjectId = getActiveProjectId() ?? undefined;
|
|
6237
|
+
}
|
|
6238
|
+
if (!credentialProjectId) {
|
|
6239
|
+
const msg = 'No active Project. Pass --project <slug> or fazemos projects switch <slug>.';
|
|
6240
|
+
if (opts.json) {
|
|
6241
|
+
console.log(JSON.stringify({ error: { code: 'NO_ACTIVE_PROJECT', message: msg } }));
|
|
6242
|
+
}
|
|
6243
|
+
else {
|
|
6244
|
+
console.error(chalk.red(msg));
|
|
6245
|
+
}
|
|
6246
|
+
process.exit(1);
|
|
6247
|
+
return; // guard: process.exit is mocked in tests
|
|
6248
|
+
}
|
|
6249
|
+
}
|
|
6250
|
+
const agentEntry = {
|
|
6251
|
+
displayName: opts.name,
|
|
6252
|
+
roles: opts.roles || [opts.name],
|
|
6253
|
+
config: { model: opts.model },
|
|
6254
|
+
provisionCredential,
|
|
6255
|
+
};
|
|
6256
|
+
if (provisionCredential) {
|
|
6257
|
+
agentEntry.credentialProjectId = credentialProjectId;
|
|
6258
|
+
if (opts.forceRotate)
|
|
6259
|
+
agentEntry.forceRotate = true;
|
|
6260
|
+
}
|
|
5837
6261
|
const data = await api('POST', '/api/agents/register', {
|
|
5838
|
-
agents: [
|
|
5839
|
-
displayName: opts.name,
|
|
5840
|
-
roles: opts.roles || [opts.name],
|
|
5841
|
-
config: { model: opts.model },
|
|
5842
|
-
}],
|
|
6262
|
+
agents: [agentEntry],
|
|
5843
6263
|
});
|
|
6264
|
+
if (opts.json) {
|
|
6265
|
+
console.log(JSON.stringify({ agents: data.agents }));
|
|
6266
|
+
return;
|
|
6267
|
+
}
|
|
5844
6268
|
for (const a of data.agents) {
|
|
5845
6269
|
const icon = a.status === 'created' ? chalk.green('✓') : a.status === 'updated' ? chalk.yellow('↻') : '○';
|
|
5846
6270
|
console.log(` ${icon} ${a.display_name} (${a.status}) — ${a.member_id}`);
|
|
6271
|
+
// F42 — credential block (matches `fazemos api-keys create` rendering per Sage UX)
|
|
6272
|
+
if (a.credential) {
|
|
6273
|
+
const cred = a.credential;
|
|
6274
|
+
if (cred.status === 'minted' || cred.status === 'rotated') {
|
|
6275
|
+
console.log('');
|
|
6276
|
+
console.log(chalk.green(` API credential ${cred.status}: ${opts.name}-key`));
|
|
6277
|
+
console.log(` Key: ${chalk.yellow(cred.rawKey)}`);
|
|
6278
|
+
console.log(` ID: ${cred.apiKeyId}`);
|
|
6279
|
+
console.log('');
|
|
6280
|
+
console.log(chalk.red(' ⚠ Save this key now — it will not be shown again.'));
|
|
6281
|
+
}
|
|
6282
|
+
else if (cred.status === 'exists') {
|
|
6283
|
+
console.log(chalk.gray(` (credential already provisioned — use --force-rotate to rotate)`));
|
|
6284
|
+
}
|
|
6285
|
+
}
|
|
6286
|
+
if (a.auditLogId) {
|
|
6287
|
+
console.log(chalk.gray(` Audit: ${a.auditLogId}`));
|
|
6288
|
+
}
|
|
5847
6289
|
}
|
|
5848
6290
|
}
|
|
5849
6291
|
catch (err) {
|
|
6292
|
+
if (opts.json) {
|
|
6293
|
+
console.log(JSON.stringify({ error: { code: err.code ?? 'ERROR', message: err.message } }));
|
|
6294
|
+
process.exit(1);
|
|
6295
|
+
}
|
|
6296
|
+
if (err instanceof ApiError) {
|
|
6297
|
+
if (err.code === 'FORBIDDEN_ROLE') {
|
|
6298
|
+
console.error(chalk.red('Access denied. Owner or admin role required to provision credentials. Agent identities are not permitted.'));
|
|
6299
|
+
process.exit(1);
|
|
6300
|
+
}
|
|
6301
|
+
if (err.code === 'CREDENTIAL_PROJECT_REQUIRED') {
|
|
6302
|
+
console.error(chalk.red('credentialProjectId is required when provisionCredential=true. Pass --project <slug>.'));
|
|
6303
|
+
process.exit(1);
|
|
6304
|
+
}
|
|
6305
|
+
if (err.code === 'CREDENTIAL_PROVISION_FAILED') {
|
|
6306
|
+
console.error(chalk.red('Credential provisioning failed — transaction rolled back. Try again or contact your admin.'));
|
|
6307
|
+
process.exit(1);
|
|
6308
|
+
}
|
|
6309
|
+
}
|
|
5850
6310
|
console.error(chalk.red(err.message));
|
|
5851
6311
|
process.exit(1);
|
|
5852
6312
|
}
|
|
@@ -8508,10 +8968,23 @@ Types: question | task | signal | response | flag | decision | direction`)
|
|
|
8508
8968
|
}
|
|
8509
8969
|
return;
|
|
8510
8970
|
}
|
|
8511
|
-
// API returned file_path — CLI writes the actual file (Dex S-B)
|
|
8971
|
+
// API returned file_path — CLI writes the actual file (Dex S-B).
|
|
8972
|
+
//
|
|
8973
|
+
// rul_registry_inbox_field_wins_regardless_of_scope (BUG64 Fix):
|
|
8974
|
+
// The write path is always built from the registry-declared `role.inbox`,
|
|
8975
|
+
// regardless of what `scope` the role carries or what the API's
|
|
8976
|
+
// `file_path` returns. This ensures org-scoped roles (e.g.
|
|
8977
|
+
// `business-operations`, `chief-of-staff`) that declare an explicit
|
|
8978
|
+
// `inbox` in .fazemos/roles.json write to that path, not to the
|
|
8979
|
+
// org-layout root (`roles/<slug>/inbox/`) that the API may return for
|
|
8980
|
+
// roles with `project_id IS NULL` in role_registrations.
|
|
8981
|
+
//
|
|
8982
|
+
// The API's `file_path` is preserved as `relPath` for reference/logging
|
|
8983
|
+
// but the on-disk write uses computeRegistryInboxPath(role, filename).
|
|
8512
8984
|
if (apiResp.file_path) {
|
|
8513
|
-
|
|
8514
|
-
relPath =
|
|
8985
|
+
// Prefer registry-declared path; use API path only when no explicit inbox.
|
|
8986
|
+
relPath = computeRegistryInboxPath(role, to, filename);
|
|
8987
|
+
fullPath = writeInboxFileAtPath(registry._workspaceRoot, relPath, content);
|
|
8515
8988
|
}
|
|
8516
8989
|
else {
|
|
8517
8990
|
// API didn't return a file_path (e.g. cross-workspace agent recipient) — write locally
|
|
@@ -8527,8 +9000,31 @@ Types: question | task | signal | response | flag | decision | direction`)
|
|
|
8527
9000
|
console.log(` Thread: ${apiResp.thread_id}`);
|
|
8528
9001
|
}
|
|
8529
9002
|
}
|
|
8530
|
-
//
|
|
8531
|
-
|
|
9003
|
+
// rul_dispatch_cli_pushes_inbox_file_on_agent_execution (BUG66 Fix B bridge):
|
|
9004
|
+
// When running inside a Fargate agent task (FAZEMOS_AGENT_EXECUTION=1),
|
|
9005
|
+
// commit + push the inbox file to origin/main so the recipient's next
|
|
9006
|
+
// dispatch_wake clone sees it. Scoped to short-execution dispatchers only.
|
|
9007
|
+
// Long dispatchers route through Fix A (DB-drain on wake); they must NOT
|
|
9008
|
+
// push (BUG46 token-expiry risk on long runs is out of scope for this bridge).
|
|
9009
|
+
//
|
|
9010
|
+
// --commit is still honored for human-workstation operators (AC3).
|
|
9011
|
+
// Agent-mode push is additive; it does not suppress the --commit path.
|
|
9012
|
+
if (isAgentExecution() && !opts.fileOnly && relPath) {
|
|
9013
|
+
try {
|
|
9014
|
+
gitCommitAndPushInboxFile(registry._workspaceRoot, relPath, `dispatch(${opts.from} → ${to}): ${type}`);
|
|
9015
|
+
console.log(chalk.green('✓ Committed and pushed to origin/main (agent-mode).'));
|
|
9016
|
+
}
|
|
9017
|
+
catch (err) {
|
|
9018
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
9019
|
+
// DISPATCH_PUSH_CONFLICT — loud fail. The DB dispatch row is durable;
|
|
9020
|
+
// the recipient will drain via Fix A (wake-task DB body embed).
|
|
9021
|
+
console.error(chalk.red(`✗ Agent push failed: ${msg}`));
|
|
9022
|
+
process.exitCode = 1;
|
|
9023
|
+
return;
|
|
9024
|
+
}
|
|
9025
|
+
}
|
|
9026
|
+
else if (opts.commit) {
|
|
9027
|
+
// Human-operator --commit path (AC3: unchanged from pre-fix behavior)
|
|
8532
9028
|
try {
|
|
8533
9029
|
gitCommitInboxFile(registry._workspaceRoot, relPath, `dispatch(${opts.from} → ${to}): ${type}`);
|
|
8534
9030
|
console.log(chalk.green('✓ Committed.'));
|