@formigio/fazemos-cli 0.10.13 → 0.10.15
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 +27 -4
- package/dist/connectionErrorCopy.js +67 -9
- package/dist/connectionErrorCopy.js.map +1 -1
- package/dist/dispatch.d.ts +96 -0
- package/dist/dispatch.js +169 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +419 -46
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -22,6 +22,56 @@ function parseNumber(val) {
|
|
|
22
22
|
throw new Error(`Invalid number: ${val}`);
|
|
23
23
|
return n;
|
|
24
24
|
}
|
|
25
|
+
/**
|
|
26
|
+
* OPS-2 — Manual Interventions KPI cell formatter.
|
|
27
|
+
*
|
|
28
|
+
* Single source of CLI truth for the tri-state coloring rule encoded in
|
|
29
|
+
* rul_intervention_chip_thresholds_v1. Mirrors the web InterventionChip
|
|
30
|
+
* thresholds so the CLI MI column on `pl list`, the header line on
|
|
31
|
+
* `pl show`, and the `ops interventions` table all render the same way:
|
|
32
|
+
*
|
|
33
|
+
* null → `—` (no color) — pre-migration row or unknown state
|
|
34
|
+
* 0 → green — clean run
|
|
35
|
+
* 1-3 → amber — some operator help
|
|
36
|
+
* ≥4 → red — carried
|
|
37
|
+
* >999 → `999+` — cap per rul_intervention_chip_count_cap_999plus
|
|
38
|
+
*
|
|
39
|
+
* Width: 5 chars (right-padded inside the chalk color call so colors don't
|
|
40
|
+
* count against the visible width). Callers can wrap with additional text
|
|
41
|
+
* (e.g. `MI:${cell}`) without breaking alignment.
|
|
42
|
+
*/
|
|
43
|
+
function formatManualInterventionCell(count) {
|
|
44
|
+
if (count === null || count === undefined) {
|
|
45
|
+
return chalk.gray('—'.padEnd(5));
|
|
46
|
+
}
|
|
47
|
+
const display = count > 999 ? '999+' : String(count);
|
|
48
|
+
const padded = display.padEnd(5);
|
|
49
|
+
if (count === 0)
|
|
50
|
+
return chalk.green(padded);
|
|
51
|
+
if (count <= 3)
|
|
52
|
+
return chalk.yellow(padded);
|
|
53
|
+
return chalk.red(padded);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* OPS-2 — parse a duration string ('24h', '7d', '30d', '90d', etc.) into
|
|
57
|
+
* an ISO-8601 datetime `since` value for the GET /api/ops/manual-interventions
|
|
58
|
+
* `since` query param.
|
|
59
|
+
*
|
|
60
|
+
* Accepted units: `h` (hours), `d` (days), `w` (weeks). Unrecognised values
|
|
61
|
+
* throw and surface to the operator via the standard error path.
|
|
62
|
+
*/
|
|
63
|
+
function parseSinceDuration(val) {
|
|
64
|
+
const m = /^(\d+)([hdw])$/.exec(val.trim());
|
|
65
|
+
if (!m) {
|
|
66
|
+
throw new Error(`Invalid --since "${val}". Expected forms like 24h, 7d, 30d, 4w.`);
|
|
67
|
+
}
|
|
68
|
+
const n = Number(m[1]);
|
|
69
|
+
const unit = m[2];
|
|
70
|
+
const ms = unit === 'h' ? n * 3_600_000
|
|
71
|
+
: unit === 'd' ? n * 86_400_000
|
|
72
|
+
: n * 604_800_000;
|
|
73
|
+
return new Date(Date.now() - ms).toISOString();
|
|
74
|
+
}
|
|
25
75
|
function parseStreamSilenceAbortMs(val) {
|
|
26
76
|
const n = Number(val);
|
|
27
77
|
if (!Number.isInteger(n) || n < 30000 || n > 1800000) {
|
|
@@ -1340,27 +1390,34 @@ const connections = program.command('connections').alias('conn').description('Gi
|
|
|
1340
1390
|
*/
|
|
1341
1391
|
connections
|
|
1342
1392
|
.command('list')
|
|
1343
|
-
.description('List
|
|
1393
|
+
.description('List VCS Connections (GitHub + BitBucket) in the active organization')
|
|
1344
1394
|
.option('-s, --status <status>', 'Filter: active (default), all, pending, suspended, revoked, uninstalled', 'active')
|
|
1345
1395
|
.action(async (opts) => {
|
|
1346
1396
|
try {
|
|
1347
1397
|
const orgId = requireActiveOrgOrExit();
|
|
1348
|
-
|
|
1349
|
-
|
|
1398
|
+
// F22 — call the provider-agnostic /vcs/connections endpoint.
|
|
1399
|
+
// The legacy /github/connections alias still works during the
|
|
1400
|
+
// dual-write phase, but `vcs/` returns BOTH providers' rows so
|
|
1401
|
+
// the user sees their full Org Connection list.
|
|
1402
|
+
const data = await api('GET', `/api/organizations/${orgId}/vcs/connections?status=${encodeURIComponent(opts.status)}`, undefined, { noProjectHeader: true });
|
|
1403
|
+
const list = (data.connections ?? []).map(normalizeConnection);
|
|
1350
1404
|
if (list.length === 0) {
|
|
1351
1405
|
const orgName = findOrgById(orgId)?.name ?? orgId;
|
|
1352
|
-
console.log(chalk.yellow(`No
|
|
1406
|
+
console.log(chalk.yellow(`No code platform connections in ${orgName}.`));
|
|
1353
1407
|
console.log(chalk.gray('Add one with: fazemos connections install'));
|
|
1354
1408
|
return;
|
|
1355
1409
|
}
|
|
1356
1410
|
const orgName = findOrgById(orgId)?.name ?? orgId;
|
|
1357
|
-
console.log(chalk.cyan(`
|
|
1411
|
+
console.log(chalk.cyan(`Code platform connections in ${orgName}:`));
|
|
1358
1412
|
console.log('');
|
|
1359
1413
|
for (const c of list) {
|
|
1360
1414
|
const statusColor = pickStatusColor(c.status);
|
|
1361
1415
|
const projects = c.projectCount == null ? '—' : `${c.projectCount} ${c.projectCount === 1 ? 'project' : 'projects'}`;
|
|
1362
|
-
const login = c.
|
|
1363
|
-
|
|
1416
|
+
const login = c.accountLogin ?? chalk.gray('—');
|
|
1417
|
+
const providerLabel = formatProviderLabel(c.provider);
|
|
1418
|
+
// F22 §8.3 item 1: provider column. Renders as a prefix tag so
|
|
1419
|
+
// mixed-provider lists are scannable at a glance.
|
|
1420
|
+
console.log(` ${chalk.magenta(providerLabel)} ${chalk.cyan(c.name)} ${chalk.gray(login)} ${statusColor(c.status)} ${chalk.gray(projects)}`);
|
|
1364
1421
|
console.log(chalk.gray(` ID: ${c.id}`));
|
|
1365
1422
|
}
|
|
1366
1423
|
}
|
|
@@ -1375,40 +1432,56 @@ connections
|
|
|
1375
1432
|
*/
|
|
1376
1433
|
connections
|
|
1377
1434
|
.command('show')
|
|
1378
|
-
.description('Show a
|
|
1435
|
+
.description('Show a VCS Connection in detail')
|
|
1379
1436
|
.argument('<id>', 'Connection ID')
|
|
1380
1437
|
.action(async (id) => {
|
|
1381
1438
|
try {
|
|
1382
1439
|
const orgId = requireActiveOrgOrExit();
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1440
|
+
// F22 — provider-agnostic detail endpoint. The serialized shape
|
|
1441
|
+
// includes `provider`, `account_login`, `supports_contents_api`
|
|
1442
|
+
// and (for GitHub) `installation_id` / (BitBucket) `workspace_uuid`.
|
|
1443
|
+
const data = await api('GET', `/api/organizations/${orgId}/vcs/connections/${id}`, undefined, { noProjectHeader: true });
|
|
1444
|
+
const raw = data.connection ?? data;
|
|
1445
|
+
const c = normalizeConnection(raw);
|
|
1446
|
+
console.log(chalk.cyan(c.name ?? '(unnamed connection)'));
|
|
1386
1447
|
console.log(` ID: ${c.id}`);
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1448
|
+
// F22 §8.3 item 2 — provider line above account info.
|
|
1449
|
+
console.log(` Provider: ${formatProviderLabel(c.provider)}`);
|
|
1450
|
+
console.log(` Account: ${c.accountLogin ?? chalk.gray('— (pending)')}`);
|
|
1451
|
+
if (c.accountType)
|
|
1452
|
+
console.log(` Account type: ${c.accountType}`);
|
|
1390
1453
|
const statusColor = pickStatusColor(c.status);
|
|
1391
1454
|
console.log(` Status: ${statusColor(c.status)}`);
|
|
1392
|
-
|
|
1455
|
+
if (c.installedAt) {
|
|
1456
|
+
console.log(` Installed: ${new Date(c.installedAt).toLocaleString()}`);
|
|
1457
|
+
}
|
|
1393
1458
|
if (c.lastHealthCheckAt) {
|
|
1394
1459
|
console.log(` Last checked: ${new Date(c.lastHealthCheckAt).toLocaleString()}`);
|
|
1395
1460
|
}
|
|
1396
1461
|
if (c.lastUsedAt) {
|
|
1397
1462
|
console.log(` Last used: ${new Date(c.lastUsedAt).toLocaleString()}`);
|
|
1398
1463
|
}
|
|
1399
|
-
|
|
1400
|
-
|
|
1464
|
+
// F22 §6.4 — supports_contents_api is the canonical signal for
|
|
1465
|
+
// "can this Connection back F18 docs surfaces?" Surface it so
|
|
1466
|
+
// users understand why docs may be unavailable on BitBucket.
|
|
1467
|
+
if (typeof c.supportsContentsApi === 'boolean') {
|
|
1468
|
+
const docs = c.supportsContentsApi ? 'yes' : chalk.yellow('no');
|
|
1469
|
+
console.log(` Docs API: ${docs}`);
|
|
1401
1470
|
}
|
|
1402
|
-
|
|
1403
|
-
console.log(` Repos:
|
|
1404
|
-
|
|
1405
|
-
|
|
1471
|
+
if (raw.repositorySelection === 'all') {
|
|
1472
|
+
console.log(` Repos: All repositories${c.accountLogin ? ` in ${c.accountLogin}` : ''}`);
|
|
1473
|
+
}
|
|
1474
|
+
else if (Array.isArray(raw.repositories)) {
|
|
1475
|
+
console.log(` Repos: ${raw.repositories.length}${raw.repositoriesTruncated ? ' (showing 100)' : ''}`);
|
|
1476
|
+
for (const r of raw.repositories) {
|
|
1477
|
+
console.log(chalk.gray(` · ${r.fullName ?? r.full_name ?? r.name}`));
|
|
1406
1478
|
}
|
|
1407
1479
|
}
|
|
1408
|
-
|
|
1480
|
+
const boundProjects = raw.boundProjects ?? raw.bound_projects ?? [];
|
|
1481
|
+
if (Array.isArray(boundProjects) && boundProjects.length > 0) {
|
|
1409
1482
|
console.log('');
|
|
1410
1483
|
console.log(chalk.cyan('Projects using this connection:'));
|
|
1411
|
-
for (const p of
|
|
1484
|
+
for (const p of boundProjects) {
|
|
1412
1485
|
console.log(` · ${p.name} (${p.slug})`);
|
|
1413
1486
|
}
|
|
1414
1487
|
}
|
|
@@ -1429,12 +1502,55 @@ connections
|
|
|
1429
1502
|
*/
|
|
1430
1503
|
connections
|
|
1431
1504
|
.command('install')
|
|
1432
|
-
.description('Add a GitHub
|
|
1433
|
-
.
|
|
1505
|
+
.description('Add a VCS Connection (GitHub or BitBucket). Prints an install URL and (for GitHub) waits for a confirmation code.')
|
|
1506
|
+
.option('--provider <provider>', 'VCS provider: github | bitbucket')
|
|
1507
|
+
.action(async (opts) => {
|
|
1434
1508
|
try {
|
|
1435
1509
|
const orgId = requireActiveOrgOrExit();
|
|
1436
1510
|
const orgName = findOrgById(orgId)?.name ?? orgId;
|
|
1437
|
-
|
|
1511
|
+
// F22 §8.3 item 3 — provider selection matrix.
|
|
1512
|
+
// 1) `--provider github|bitbucket` supplied: use directly.
|
|
1513
|
+
// 2) No flag: prompt the user interactively (provider-picker).
|
|
1514
|
+
// The CLI cannot probe server-side provider configuration, so the
|
|
1515
|
+
// "exactly one provider configured" auto-select branch of the spec
|
|
1516
|
+
// is implemented as "user always confirms" — narrower than the
|
|
1517
|
+
// spec wording but strictly safer (no silent provider choice).
|
|
1518
|
+
let provider = opts.provider?.toLowerCase();
|
|
1519
|
+
if (!provider) {
|
|
1520
|
+
provider = await promptProviderChoice();
|
|
1521
|
+
}
|
|
1522
|
+
if (provider !== 'github' && provider !== 'bitbucket') {
|
|
1523
|
+
console.error(chalk.red(`Invalid --provider: ${provider}. Use 'github' or 'bitbucket'.`));
|
|
1524
|
+
process.exit(1);
|
|
1525
|
+
}
|
|
1526
|
+
// F22 prefers the new provider-agnostic /vcs/connections route, which
|
|
1527
|
+
// expects `provider` in the body. The github-only endpoint is kept
|
|
1528
|
+
// alive but not used here so all new connections flow through one path.
|
|
1529
|
+
const mintData = await api('POST', `/api/organizations/${orgId}/vcs/connections/install-url`, { provider, source: 'cli', returnTo: null }, { noProjectHeader: true });
|
|
1530
|
+
// F22 §8.3 item 8 — BitBucket install is browser-only in v1 (no CLI
|
|
1531
|
+
// confirmation-code exchange counterpart). Print the literal copy
|
|
1532
|
+
// block specified in the spec verbatim, then exit successfully.
|
|
1533
|
+
// The user completes consent in the browser and the /api/bitbucket/
|
|
1534
|
+
// oauth/callback handler finishes the Connection server-side.
|
|
1535
|
+
//
|
|
1536
|
+
// Dex nit N4 — output literal MUST match the spec block exactly so
|
|
1537
|
+
// the acceptance test can assert it.
|
|
1538
|
+
if (provider === 'bitbucket') {
|
|
1539
|
+
console.log('');
|
|
1540
|
+
console.log('BitBucket Connection install — browser required');
|
|
1541
|
+
console.log('');
|
|
1542
|
+
console.log('Open this URL in your browser to authorize Fazemos:');
|
|
1543
|
+
console.log('');
|
|
1544
|
+
console.log(` ${mintData.url}`);
|
|
1545
|
+
console.log('');
|
|
1546
|
+
console.log('After you authorize, BitBucket will redirect to Fazemos and complete the Connection.');
|
|
1547
|
+
console.log('You can then list your connections with:');
|
|
1548
|
+
console.log('');
|
|
1549
|
+
console.log(' fazemos connections list');
|
|
1550
|
+
console.log('');
|
|
1551
|
+
console.log('This URL expires in 10 minutes.');
|
|
1552
|
+
return;
|
|
1553
|
+
}
|
|
1438
1554
|
console.log('');
|
|
1439
1555
|
console.log(`To add a GitHub connection to ${chalk.cyan(orgName)}:`);
|
|
1440
1556
|
console.log('');
|
|
@@ -1523,18 +1639,20 @@ connections
|
|
|
1523
1639
|
connections
|
|
1524
1640
|
.command('revoke')
|
|
1525
1641
|
.alias('disconnect')
|
|
1526
|
-
.description('Disconnect a
|
|
1642
|
+
.description('Disconnect a VCS Connection (Fazemos-side; does not uninstall the App from GitHub / revoke the OAuth grant on BitBucket).')
|
|
1527
1643
|
.argument('<id>', 'Connection ID')
|
|
1528
1644
|
.option('-f, --force', 'Skip the confirmation prompt', false)
|
|
1529
1645
|
.action(async (id, opts) => {
|
|
1530
1646
|
try {
|
|
1531
1647
|
const orgId = requireActiveOrgOrExit();
|
|
1532
1648
|
// Fetch the Connection so we can show what we're about to revoke
|
|
1533
|
-
// and how many projects it affects (
|
|
1534
|
-
|
|
1649
|
+
// and how many projects it affects (UX §8.3 confirmation copy).
|
|
1650
|
+
// F22 — use the provider-agnostic /vcs/ endpoint so both provider
|
|
1651
|
+
// rows are reachable.
|
|
1652
|
+
let raw;
|
|
1535
1653
|
try {
|
|
1536
|
-
const detail = await api('GET', `/api/organizations/${orgId}/
|
|
1537
|
-
|
|
1654
|
+
const detail = await api('GET', `/api/organizations/${orgId}/vcs/connections/${id}`, undefined, { noProjectHeader: true });
|
|
1655
|
+
raw = detail.connection ?? detail;
|
|
1538
1656
|
}
|
|
1539
1657
|
catch (err) {
|
|
1540
1658
|
if (err instanceof ApiError && err.code === 'CONNECTION_NOT_FOUND') {
|
|
@@ -1543,15 +1661,17 @@ connections
|
|
|
1543
1661
|
}
|
|
1544
1662
|
throw err;
|
|
1545
1663
|
}
|
|
1546
|
-
const
|
|
1664
|
+
const connection = normalizeConnection(raw);
|
|
1665
|
+
const boundProjects = raw.boundProjects ?? raw.bound_projects ?? [];
|
|
1666
|
+
const providerLabelStr = formatProviderLabel(connection.provider);
|
|
1547
1667
|
if (!opts.force) {
|
|
1548
1668
|
console.log('');
|
|
1549
|
-
console.log(`Disconnect ${chalk.cyan(connection.name)}?`);
|
|
1669
|
+
console.log(`Disconnect ${chalk.magenta(providerLabelStr)} ${chalk.cyan(connection.name ?? id)}?`);
|
|
1550
1670
|
console.log('');
|
|
1551
1671
|
if (boundProjects.length > 0) {
|
|
1552
1672
|
const projectList = boundProjects.map(p => p.name).join(', ');
|
|
1553
1673
|
console.log(` This connection is used by ${boundProjects.length} ${boundProjects.length === 1 ? 'project' : 'projects'}: ${projectList}.`);
|
|
1554
|
-
console.log(
|
|
1674
|
+
console.log(` Pipelines in those projects will fail on ${providerLabelStr} steps until`);
|
|
1555
1675
|
console.log(' they are bound to a different connection.');
|
|
1556
1676
|
console.log('');
|
|
1557
1677
|
}
|
|
@@ -1559,7 +1679,13 @@ connections
|
|
|
1559
1679
|
console.log(' No projects are using this connection.');
|
|
1560
1680
|
console.log('');
|
|
1561
1681
|
}
|
|
1562
|
-
|
|
1682
|
+
// Provider-aware caveat: GitHub App vs BitBucket OAuth grant.
|
|
1683
|
+
if (connection.provider === 'bitbucket') {
|
|
1684
|
+
console.log(chalk.gray(' This does not revoke the OAuth grant on BitBucket.'));
|
|
1685
|
+
}
|
|
1686
|
+
else {
|
|
1687
|
+
console.log(chalk.gray(' This does not uninstall the Fazemos App from GitHub.'));
|
|
1688
|
+
}
|
|
1563
1689
|
console.log('');
|
|
1564
1690
|
const answer = await promptLine('Disconnect? [y/N]: ');
|
|
1565
1691
|
if (!answer || !/^y/i.test(answer.trim())) {
|
|
@@ -1567,11 +1693,14 @@ connections
|
|
|
1567
1693
|
return;
|
|
1568
1694
|
}
|
|
1569
1695
|
}
|
|
1570
|
-
const data = await api('DELETE', `/api/organizations/${orgId}/
|
|
1571
|
-
console.log(chalk.green(`Disconnected: ${connection.name}`));
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1696
|
+
const data = await api('DELETE', `/api/organizations/${orgId}/vcs/connections/${id}`, undefined, { noProjectHeader: true });
|
|
1697
|
+
console.log(chalk.green(`Disconnected: ${providerLabelStr} ${connection.name ?? id}`));
|
|
1698
|
+
// F22 response shape: { revoked, secrets_deleted, projects_unbound }.
|
|
1699
|
+
// F16 legacy shape: { affectedProjects: [...] }. Handle both during
|
|
1700
|
+
// the dual-write rollout.
|
|
1701
|
+
const unbound = data.projects_unbound ?? (data.affectedProjects ?? []).length ?? 0;
|
|
1702
|
+
if (unbound > 0) {
|
|
1703
|
+
console.log(chalk.gray(` ${unbound} project${unbound === 1 ? '' : 's'} unbound.`));
|
|
1575
1704
|
}
|
|
1576
1705
|
invalidateAuthMeCache();
|
|
1577
1706
|
}
|
|
@@ -1586,19 +1715,23 @@ connections
|
|
|
1586
1715
|
*/
|
|
1587
1716
|
connections
|
|
1588
1717
|
.command('health-check')
|
|
1589
|
-
.description('Verify a Connection is still healthy on GitHub')
|
|
1718
|
+
.description('Verify a Connection is still healthy on its code platform (GitHub or BitBucket)')
|
|
1590
1719
|
.argument('<id>', 'Connection ID')
|
|
1591
1720
|
.action(async (id) => {
|
|
1592
1721
|
try {
|
|
1593
1722
|
const orgId = requireActiveOrgOrExit();
|
|
1594
|
-
|
|
1595
|
-
|
|
1723
|
+
// F22 — provider-aware health-check route on the /vcs/ alias.
|
|
1724
|
+
// The handler dispatches to the right provider (GitHub installation
|
|
1725
|
+
// probe / BitBucket OAuth-refresh + workspace probe).
|
|
1726
|
+
const data = await api('POST', `/api/organizations/${orgId}/vcs/connections/${id}/health-check`, {}, { noProjectHeader: true });
|
|
1727
|
+
const c = normalizeConnection(data.connection ?? data);
|
|
1596
1728
|
const statusColor = pickStatusColor(c.status);
|
|
1729
|
+
const label = `${formatProviderLabel(c.provider)} ${c.name ?? id}`;
|
|
1597
1730
|
if (data.changed) {
|
|
1598
|
-
console.log(chalk.yellow(`Status changed: ${
|
|
1731
|
+
console.log(chalk.yellow(`Status changed: ${label} → ${statusColor(c.status)}`));
|
|
1599
1732
|
}
|
|
1600
1733
|
else {
|
|
1601
|
-
console.log(chalk.green(`✓ ${
|
|
1734
|
+
console.log(chalk.green(`✓ ${label} — ${statusColor(c.status)}`));
|
|
1602
1735
|
}
|
|
1603
1736
|
}
|
|
1604
1737
|
catch (err) {
|
|
@@ -1610,6 +1743,53 @@ connections
|
|
|
1610
1743
|
process.exit(1);
|
|
1611
1744
|
}
|
|
1612
1745
|
});
|
|
1746
|
+
export function normalizeConnection(raw) {
|
|
1747
|
+
if (!raw || typeof raw !== 'object') {
|
|
1748
|
+
return {
|
|
1749
|
+
id: '',
|
|
1750
|
+
provider: null,
|
|
1751
|
+
name: null,
|
|
1752
|
+
accountLogin: null,
|
|
1753
|
+
accountType: null,
|
|
1754
|
+
status: 'unknown',
|
|
1755
|
+
installedAt: null,
|
|
1756
|
+
lastHealthCheckAt: null,
|
|
1757
|
+
lastUsedAt: null,
|
|
1758
|
+
supportsContentsApi: null,
|
|
1759
|
+
projectCount: null,
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
// Provider — F22 new field. Older /github/ rows always meant github;
|
|
1763
|
+
// default there for back-compat.
|
|
1764
|
+
const provider = raw.provider ?? (raw.githubAccountLogin !== undefined ? 'github' : null);
|
|
1765
|
+
return {
|
|
1766
|
+
id: String(raw.id ?? ''),
|
|
1767
|
+
provider,
|
|
1768
|
+
name: raw.name ?? null,
|
|
1769
|
+
accountLogin: raw.accountLogin ?? raw.account_login ?? raw.githubAccountLogin ?? null,
|
|
1770
|
+
accountType: raw.accountType ?? raw.account_type ?? raw.githubAccountType ?? null,
|
|
1771
|
+
status: raw.status ?? 'unknown',
|
|
1772
|
+
installedAt: raw.installedAt ?? raw.installed_at ?? raw.created_at ?? null,
|
|
1773
|
+
lastHealthCheckAt: raw.lastHealthCheckAt ?? raw.last_health_check_at ?? null,
|
|
1774
|
+
lastUsedAt: raw.lastUsedAt ?? raw.last_used_at ?? null,
|
|
1775
|
+
supportsContentsApi: typeof raw.supportsContentsApi === 'boolean'
|
|
1776
|
+
? raw.supportsContentsApi
|
|
1777
|
+
: typeof raw.supports_contents_api === 'boolean'
|
|
1778
|
+
? raw.supports_contents_api
|
|
1779
|
+
: null,
|
|
1780
|
+
projectCount: raw.projectCount ?? raw.project_count ?? null,
|
|
1781
|
+
};
|
|
1782
|
+
}
|
|
1783
|
+
/**
|
|
1784
|
+
* F22 — Human-friendly provider label used in CLI columns and lines.
|
|
1785
|
+
*/
|
|
1786
|
+
export function formatProviderLabel(provider) {
|
|
1787
|
+
if (provider === 'github')
|
|
1788
|
+
return 'GitHub';
|
|
1789
|
+
if (provider === 'bitbucket')
|
|
1790
|
+
return 'BitBucket';
|
|
1791
|
+
return '—';
|
|
1792
|
+
}
|
|
1613
1793
|
/**
|
|
1614
1794
|
* Helper — color a status string per the Sage §2.2 taxonomy.
|
|
1615
1795
|
*/
|
|
@@ -1629,6 +1809,30 @@ function pickStatusColor(status) {
|
|
|
1629
1809
|
return chalk.white;
|
|
1630
1810
|
}
|
|
1631
1811
|
}
|
|
1812
|
+
/**
|
|
1813
|
+
* F22 §8.3 item 3 — interactive provider picker. Used by
|
|
1814
|
+
* `fazemos connections install` when no `--provider` flag is supplied
|
|
1815
|
+
* so the user explicitly chooses GitHub or BitBucket before the URL
|
|
1816
|
+
* mint hits the API. Mirrors the web `ProviderPickerModal` choice
|
|
1817
|
+
* without the modal chrome.
|
|
1818
|
+
*
|
|
1819
|
+
* Returns 'github' or 'bitbucket'. Repeats on invalid input.
|
|
1820
|
+
*/
|
|
1821
|
+
export async function promptProviderChoice() {
|
|
1822
|
+
console.log('');
|
|
1823
|
+
console.log('Which code platform do you want to connect?');
|
|
1824
|
+
console.log(' 1. GitHub');
|
|
1825
|
+
console.log(' 2. BitBucket');
|
|
1826
|
+
console.log('');
|
|
1827
|
+
while (true) {
|
|
1828
|
+
const ans = (await promptLine(' Choose [1=GitHub / 2=BitBucket]: ')).trim().toLowerCase();
|
|
1829
|
+
if (ans === '1' || ans === 'github' || ans === 'gh')
|
|
1830
|
+
return 'github';
|
|
1831
|
+
if (ans === '2' || ans === 'bitbucket' || ans === 'bb')
|
|
1832
|
+
return 'bitbucket';
|
|
1833
|
+
console.log(chalk.yellow(' Please answer 1 (GitHub) or 2 (BitBucket).'));
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1632
1836
|
/**
|
|
1633
1837
|
* Read a single line of input from stdin. No fancy framing — `readline`
|
|
1634
1838
|
* is sufficient for the install confirmation-code prompt and the
|
|
@@ -4000,7 +4204,18 @@ pipelines
|
|
|
4000
4204
|
}
|
|
4001
4205
|
for (const inst of data.instances) {
|
|
4002
4206
|
const stepInfo = inst.total_steps ? ` — step ${inst.current_step ?? '?'}/${inst.total_steps}` : '';
|
|
4003
|
-
|
|
4207
|
+
// OPS-2 — Manual Interventions KPI. The MI column renders inline next
|
|
4208
|
+
// to the instance status with the prescribed tri-state coloring:
|
|
4209
|
+
// null → `—` (no color; pre-migration row or unknown)
|
|
4210
|
+
// 0 → green
|
|
4211
|
+
// 1-3 → amber
|
|
4212
|
+
// ≥4 → red
|
|
4213
|
+
// Capped per rul_intervention_chip_count_cap_999plus: values > 999
|
|
4214
|
+
// collapse to `999+` while the underlying count remains intact in
|
|
4215
|
+
// the API response and JSON output for scripts.
|
|
4216
|
+
const miCell = formatManualInterventionCell(inst.manual_intervention_count);
|
|
4217
|
+
const envCell = inst.env_tag ? chalk.gray(` [${inst.env_tag}]`) : '';
|
|
4218
|
+
console.log(` ${chalk.cyan(inst.name)} (${inst.status}) MI:${miCell}${envCell}${stepInfo}`);
|
|
4004
4219
|
console.log(` ID: ${inst.id}`);
|
|
4005
4220
|
if (opts.expand && inst.steps?.length) {
|
|
4006
4221
|
for (const s of inst.steps) {
|
|
@@ -4029,6 +4244,14 @@ pipelines
|
|
|
4029
4244
|
console.log(` ID: ${inst.id}`);
|
|
4030
4245
|
console.log(` Status: ${inst.status}`);
|
|
4031
4246
|
console.log(` Template: ${inst.template_name || inst.template_id}`);
|
|
4247
|
+
// OPS-2 — Manual Interventions KPI header line. Same coloring rule as
|
|
4248
|
+
// `pl list` (null=—, 0=green, 1-3=amber, 4+=red). env_tag is surfaced
|
|
4249
|
+
// alongside so operators can read prod-vs-dev at a glance (the cycle
|
|
4250
|
+
// exit criterion is prod-only).
|
|
4251
|
+
console.log(` Manual Interventions: ${formatManualInterventionCell(inst.manual_intervention_count)}`);
|
|
4252
|
+
if (inst.env_tag) {
|
|
4253
|
+
console.log(` Env Tag: ${inst.env_tag}`);
|
|
4254
|
+
}
|
|
4032
4255
|
if (inst.phases?.length) {
|
|
4033
4256
|
for (const phase of inst.phases) {
|
|
4034
4257
|
console.log(chalk.cyan(`\n Phase: ${phase.name}`));
|
|
@@ -6995,6 +7218,156 @@ ops
|
|
|
6995
7218
|
process.exit(1);
|
|
6996
7219
|
}
|
|
6997
7220
|
});
|
|
7221
|
+
// ── OPS-2 — Manual Interventions KPI ─────────────────────────────────
|
|
7222
|
+
// Spec: specs/tech/platform/OPS-2-manual-intervention-kpi-tech-spec.md
|
|
7223
|
+
//
|
|
7224
|
+
// `fazemos ops interventions` has three operating modes:
|
|
7225
|
+
//
|
|
7226
|
+
// 1. bare — windowed summary table + current zero-intervention
|
|
7227
|
+
// streak. Calls GET /api/ops/manual-interventions/summary.
|
|
7228
|
+
// Prod-only by default (cycle exit criterion is prod);
|
|
7229
|
+
// --include-dev inverts the env_tag filter for parity
|
|
7230
|
+
// with the API.
|
|
7231
|
+
// 2. --pipeline — per-pipeline drill. Calls GET /api/pipeline-instances/:id
|
|
7232
|
+
// (response now carries manual_intervention_count +
|
|
7233
|
+
// env_tag) and surfaces the count + the env_tag fence so
|
|
7234
|
+
// operators can read the chip the same way the web does.
|
|
7235
|
+
// Forensic per-event detail lives behind `fazemos agents
|
|
7236
|
+
// events --type manual_intervention` (existing surface);
|
|
7237
|
+
// we print the hint so the operator doesn't have to
|
|
7238
|
+
// rediscover it.
|
|
7239
|
+
// 3. --since — windowed list. Calls GET /api/ops/manual-interventions
|
|
7240
|
+
// with the resolved `since` timestamp. Defaults to
|
|
7241
|
+
// include-dev (false) → prod-only-NO; the GET list
|
|
7242
|
+
// endpoint defaults exclude_dev=false (the broader view
|
|
7243
|
+
// is more useful when scanning recent activity). The
|
|
7244
|
+
// --exclude-dev flag tightens to prod-only.
|
|
7245
|
+
//
|
|
7246
|
+
// Admin/owner gating is enforced server-side (403 FORBIDDEN); the CLI
|
|
7247
|
+
// surfaces the API's error verbatim so non-admin operators see the
|
|
7248
|
+
// authoritative reason rather than a stale local guess.
|
|
7249
|
+
ops
|
|
7250
|
+
.command('interventions')
|
|
7251
|
+
.description('Manual-intervention KPI. Bare mode shows the windowed summary table + '
|
|
7252
|
+
+ 'current zero-intervention streak (prod-only by default). '
|
|
7253
|
+
+ '--pipeline <id> drills into one pipeline. '
|
|
7254
|
+
+ '--since <duration> lists pipelines started in the window.')
|
|
7255
|
+
.option('-w, --window <window>', 'Summary window: 24h, 7d, 30d', '7d')
|
|
7256
|
+
.option('-p, --pipeline <id>', 'Drill into one pipeline_instance by ID')
|
|
7257
|
+
.option('--since <duration>', 'List mode: filter by started_at >= now - duration (e.g. 24h, 7d, 30d)')
|
|
7258
|
+
.option('-l, --limit <n>', 'List mode: max rows (default 50, max 200)', '50')
|
|
7259
|
+
.option('--include-dev', 'Include dev-tagged pipelines in the summary / list (default: exclude)', false)
|
|
7260
|
+
.option('--exclude-dev', 'Force exclude dev-tagged pipelines (default for summary; opt-in for list)', false)
|
|
7261
|
+
.option('--json', 'Print the raw API response as JSON (machine-readable)')
|
|
7262
|
+
.action(async (opts) => {
|
|
7263
|
+
try {
|
|
7264
|
+
// ── Mode 1: --pipeline <id> drill ────────────────────────────
|
|
7265
|
+
if (opts.pipeline) {
|
|
7266
|
+
const data = await api('GET', `/api/pipeline-instances/${opts.pipeline}`);
|
|
7267
|
+
if (opts.json) {
|
|
7268
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7269
|
+
return;
|
|
7270
|
+
}
|
|
7271
|
+
const inst = data.instance;
|
|
7272
|
+
if (!inst) {
|
|
7273
|
+
console.error(chalk.red(`Pipeline ${opts.pipeline} not found`));
|
|
7274
|
+
process.exit(1);
|
|
7275
|
+
}
|
|
7276
|
+
console.log(chalk.cyan(`Manual interventions on ${inst.name}`));
|
|
7277
|
+
console.log(` ID: ${inst.id}`);
|
|
7278
|
+
console.log(` Status: ${inst.status}`);
|
|
7279
|
+
console.log(` Env Tag: ${inst.env_tag ?? chalk.gray('—')}`);
|
|
7280
|
+
console.log(` Count: ${formatManualInterventionCell(inst.manual_intervention_count)}`);
|
|
7281
|
+
if (inst.started_at) {
|
|
7282
|
+
console.log(` Started: ${new Date(inst.started_at).toLocaleString()}`);
|
|
7283
|
+
}
|
|
7284
|
+
if (inst.completed_at) {
|
|
7285
|
+
console.log(` Completed: ${new Date(inst.completed_at).toLocaleString()}`);
|
|
7286
|
+
}
|
|
7287
|
+
console.log('');
|
|
7288
|
+
console.log(chalk.gray(' Forensic per-event detail: query agent_events for the assignee/reviewer'));
|
|
7289
|
+
console.log(chalk.gray(' identities that drove each increment. Example:'));
|
|
7290
|
+
console.log(chalk.gray(` fazemos agents events <agent-id> --type manual_intervention`));
|
|
7291
|
+
return;
|
|
7292
|
+
}
|
|
7293
|
+
// ── Mode 2: --since <duration> windowed list ─────────────────
|
|
7294
|
+
if (opts.since) {
|
|
7295
|
+
const since = parseSinceDuration(opts.since);
|
|
7296
|
+
const params = [`since=${encodeURIComponent(since)}`];
|
|
7297
|
+
// For the list endpoint, the API default is exclude_dev=false (a
|
|
7298
|
+
// broader scan-recent-activity surface). --exclude-dev tightens
|
|
7299
|
+
// to prod-only. --include-dev is a no-op here (the default
|
|
7300
|
+
// already includes dev) but accepted so the flag is symmetric
|
|
7301
|
+
// across modes.
|
|
7302
|
+
if (opts.excludeDev)
|
|
7303
|
+
params.push('exclude_dev=true');
|
|
7304
|
+
const limitN = Math.max(1, Math.min(200, Number(opts.limit) || 50));
|
|
7305
|
+
params.push(`limit=${limitN}`);
|
|
7306
|
+
const qs = `?${params.join('&')}`;
|
|
7307
|
+
const data = await api('GET', `/api/ops/manual-interventions${qs}`);
|
|
7308
|
+
if (opts.json) {
|
|
7309
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7310
|
+
return;
|
|
7311
|
+
}
|
|
7312
|
+
const rows = data.pipelines || [];
|
|
7313
|
+
if (!rows.length) {
|
|
7314
|
+
console.log(chalk.green(`No interventions in window since ${since}`));
|
|
7315
|
+
return;
|
|
7316
|
+
}
|
|
7317
|
+
const scope = opts.excludeDev ? 'prod-only' : 'all envs';
|
|
7318
|
+
console.log(chalk.cyan(`Manual interventions since ${since} (${scope}):`));
|
|
7319
|
+
console.log('');
|
|
7320
|
+
// Columns: MI | Env | Status | Started | Name (ID)
|
|
7321
|
+
for (const r of rows) {
|
|
7322
|
+
const mi = formatManualInterventionCell(r.manual_intervention_count);
|
|
7323
|
+
const env = (r.env_tag ?? '—').padEnd(4);
|
|
7324
|
+
const status = (r.status ?? '').padEnd(10);
|
|
7325
|
+
const started = r.started_at ? new Date(r.started_at).toLocaleString() : '';
|
|
7326
|
+
console.log(` MI:${mi} ${chalk.gray(env)} ${status} ${chalk.gray(started)}`);
|
|
7327
|
+
console.log(` ${chalk.cyan(r.name)} ${chalk.gray(r.pipeline_instance_id)}`);
|
|
7328
|
+
}
|
|
7329
|
+
return;
|
|
7330
|
+
}
|
|
7331
|
+
// ── Mode 3: bare summary ─────────────────────────────────────
|
|
7332
|
+
// The summary endpoint defaults exclude_dev=true (cycle exit
|
|
7333
|
+
// criterion is prod-only by spec). --include-dev inverts. The CLI
|
|
7334
|
+
// is explicit either way so log lines are unambiguous.
|
|
7335
|
+
const excludeDev = opts.includeDev ? false : true;
|
|
7336
|
+
const window = opts.window || '7d';
|
|
7337
|
+
const validWindows = ['24h', '7d', '30d'];
|
|
7338
|
+
if (!validWindows.includes(window)) {
|
|
7339
|
+
console.error(chalk.red(`Invalid --window "${window}". Must be one of: ${validWindows.join(', ')}`));
|
|
7340
|
+
process.exit(1);
|
|
7341
|
+
}
|
|
7342
|
+
const qs = `?window=${window}&exclude_dev=${excludeDev}`;
|
|
7343
|
+
const data = await api('GET', `/api/ops/manual-interventions/summary${qs}`);
|
|
7344
|
+
if (opts.json) {
|
|
7345
|
+
console.log(JSON.stringify(data, null, 2));
|
|
7346
|
+
return;
|
|
7347
|
+
}
|
|
7348
|
+
const scope = excludeDev ? 'prod-only' : 'all envs';
|
|
7349
|
+
console.log(chalk.cyan(`Manual interventions — ${data.window} (${scope})`));
|
|
7350
|
+
console.log('');
|
|
7351
|
+
console.log(` Total runs: ${data.total_runs}`);
|
|
7352
|
+
console.log(` Total interventions: ${data.total_interventions}`);
|
|
7353
|
+
console.log(` Avg per run: ${typeof data.avg_per_run === 'number' ? data.avg_per_run.toFixed(2) : data.avg_per_run}`);
|
|
7354
|
+
console.log(` p95 per run: ${data.p95_per_run}`);
|
|
7355
|
+
console.log(` Zero-intervention: ${data.zero_intervention_runs} of ${data.total_runs} runs`);
|
|
7356
|
+
// Streak gets the same green/amber/grey emphasis as the web chip:
|
|
7357
|
+
// ≥5 is a celebration moment per Sage's spec; 1-4 amber; 0 grey.
|
|
7358
|
+
const streak = data.zero_intervention_streak ?? 0;
|
|
7359
|
+
const streakDisplay = streak >= 5
|
|
7360
|
+
? chalk.green(`${streak} 🎉`)
|
|
7361
|
+
: streak >= 1
|
|
7362
|
+
? chalk.yellow(String(streak))
|
|
7363
|
+
: chalk.gray('0');
|
|
7364
|
+
console.log(` Current streak: ${streakDisplay}`);
|
|
7365
|
+
}
|
|
7366
|
+
catch (err) {
|
|
7367
|
+
console.error(chalk.red(err.message));
|
|
7368
|
+
process.exit(1);
|
|
7369
|
+
}
|
|
7370
|
+
});
|
|
6998
7371
|
// ── F18 — Project-scoped docs surface ────────────────────────────────
|
|
6999
7372
|
// Spec: specs/tech/platform/F18-project-scoped-docs-surface-tech-spec.md §8
|
|
7000
7373
|
//
|