@formigio/fazemos-cli 0.10.14 → 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/index.js CHANGED
@@ -1390,27 +1390,34 @@ const connections = program.command('connections').alias('conn').description('Gi
1390
1390
  */
1391
1391
  connections
1392
1392
  .command('list')
1393
- .description('List GitHub Connections in the active organization')
1393
+ .description('List VCS Connections (GitHub + BitBucket) in the active organization')
1394
1394
  .option('-s, --status <status>', 'Filter: active (default), all, pending, suspended, revoked, uninstalled', 'active')
1395
1395
  .action(async (opts) => {
1396
1396
  try {
1397
1397
  const orgId = requireActiveOrgOrExit();
1398
- const data = await api('GET', `/api/organizations/${orgId}/github/connections?status=${encodeURIComponent(opts.status)}`, undefined, { noProjectHeader: true });
1399
- const list = data.connections ?? [];
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);
1400
1404
  if (list.length === 0) {
1401
1405
  const orgName = findOrgById(orgId)?.name ?? orgId;
1402
- console.log(chalk.yellow(`No GitHub connections in ${orgName}.`));
1406
+ console.log(chalk.yellow(`No code platform connections in ${orgName}.`));
1403
1407
  console.log(chalk.gray('Add one with: fazemos connections install'));
1404
1408
  return;
1405
1409
  }
1406
1410
  const orgName = findOrgById(orgId)?.name ?? orgId;
1407
- console.log(chalk.cyan(`GitHub connections in ${orgName}:`));
1411
+ console.log(chalk.cyan(`Code platform connections in ${orgName}:`));
1408
1412
  console.log('');
1409
1413
  for (const c of list) {
1410
1414
  const statusColor = pickStatusColor(c.status);
1411
1415
  const projects = c.projectCount == null ? '—' : `${c.projectCount} ${c.projectCount === 1 ? 'project' : 'projects'}`;
1412
- const login = c.githubAccountLogin ?? chalk.gray('—');
1413
- console.log(` ${chalk.cyan(c.name)} ${chalk.gray(login)} ${statusColor(c.status)} ${chalk.gray(projects)}`);
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)}`);
1414
1421
  console.log(chalk.gray(` ID: ${c.id}`));
1415
1422
  }
1416
1423
  }
@@ -1425,40 +1432,56 @@ connections
1425
1432
  */
1426
1433
  connections
1427
1434
  .command('show')
1428
- .description('Show a GitHub Connection in detail')
1435
+ .description('Show a VCS Connection in detail')
1429
1436
  .argument('<id>', 'Connection ID')
1430
1437
  .action(async (id) => {
1431
1438
  try {
1432
1439
  const orgId = requireActiveOrgOrExit();
1433
- const data = await api('GET', `/api/organizations/${orgId}/github/connections/${id}`, undefined, { noProjectHeader: true });
1434
- const c = data.connection;
1435
- console.log(chalk.cyan(c.name));
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)'));
1436
1447
  console.log(` ID: ${c.id}`);
1437
- console.log(` GitHub: ${c.githubAccountLogin ?? chalk.gray('(pending)')}`);
1438
- if (c.githubAccountType)
1439
- console.log(` Account type: ${c.githubAccountType}`);
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}`);
1440
1453
  const statusColor = pickStatusColor(c.status);
1441
1454
  console.log(` Status: ${statusColor(c.status)}`);
1442
- console.log(` Installed: ${c.installedAt ? new Date(c.installedAt).toLocaleString() : '(unknown)'}`);
1455
+ if (c.installedAt) {
1456
+ console.log(` Installed: ${new Date(c.installedAt).toLocaleString()}`);
1457
+ }
1443
1458
  if (c.lastHealthCheckAt) {
1444
1459
  console.log(` Last checked: ${new Date(c.lastHealthCheckAt).toLocaleString()}`);
1445
1460
  }
1446
1461
  if (c.lastUsedAt) {
1447
1462
  console.log(` Last used: ${new Date(c.lastUsedAt).toLocaleString()}`);
1448
1463
  }
1449
- if (c.repositorySelection === 'all') {
1450
- console.log(` Repos: All repositories${c.githubAccountLogin ? ` in ${c.githubAccountLogin}` : ''}`);
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}`);
1470
+ }
1471
+ if (raw.repositorySelection === 'all') {
1472
+ console.log(` Repos: All repositories${c.accountLogin ? ` in ${c.accountLogin}` : ''}`);
1451
1473
  }
1452
- else if (Array.isArray(c.repositories)) {
1453
- console.log(` Repos: ${c.repositories.length}${c.repositoriesTruncated ? ' (showing 100)' : ''}`);
1454
- for (const r of c.repositories) {
1455
- console.log(chalk.gray(` · ${r.fullName}`));
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}`));
1456
1478
  }
1457
1479
  }
1458
- if (Array.isArray(c.boundProjects) && c.boundProjects.length > 0) {
1480
+ const boundProjects = raw.boundProjects ?? raw.bound_projects ?? [];
1481
+ if (Array.isArray(boundProjects) && boundProjects.length > 0) {
1459
1482
  console.log('');
1460
1483
  console.log(chalk.cyan('Projects using this connection:'));
1461
- for (const p of c.boundProjects) {
1484
+ for (const p of boundProjects) {
1462
1485
  console.log(` · ${p.name} (${p.slug})`);
1463
1486
  }
1464
1487
  }
@@ -1479,12 +1502,55 @@ connections
1479
1502
  */
1480
1503
  connections
1481
1504
  .command('install')
1482
- .description('Add a GitHub Connection. Prints an install URL and waits for a confirmation code.')
1483
- .action(async () => {
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) => {
1484
1508
  try {
1485
1509
  const orgId = requireActiveOrgOrExit();
1486
1510
  const orgName = findOrgById(orgId)?.name ?? orgId;
1487
- const mintData = await api('POST', `/api/organizations/${orgId}/github/connections/install-url`, { source: 'cli', returnTo: null }, { noProjectHeader: true });
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
+ }
1488
1554
  console.log('');
1489
1555
  console.log(`To add a GitHub connection to ${chalk.cyan(orgName)}:`);
1490
1556
  console.log('');
@@ -1573,18 +1639,20 @@ connections
1573
1639
  connections
1574
1640
  .command('revoke')
1575
1641
  .alias('disconnect')
1576
- .description('Disconnect a GitHub Connection (Fazemos-side; does not uninstall the App from GitHub).')
1642
+ .description('Disconnect a VCS Connection (Fazemos-side; does not uninstall the App from GitHub / revoke the OAuth grant on BitBucket).')
1577
1643
  .argument('<id>', 'Connection ID')
1578
1644
  .option('-f, --force', 'Skip the confirmation prompt', false)
1579
1645
  .action(async (id, opts) => {
1580
1646
  try {
1581
1647
  const orgId = requireActiveOrgOrExit();
1582
1648
  // Fetch the Connection so we can show what we're about to revoke
1583
- // and how many projects it affects (Sage §8.3 confirmation copy).
1584
- let connection;
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;
1585
1653
  try {
1586
- const detail = await api('GET', `/api/organizations/${orgId}/github/connections/${id}`, undefined, { noProjectHeader: true });
1587
- connection = detail.connection;
1654
+ const detail = await api('GET', `/api/organizations/${orgId}/vcs/connections/${id}`, undefined, { noProjectHeader: true });
1655
+ raw = detail.connection ?? detail;
1588
1656
  }
1589
1657
  catch (err) {
1590
1658
  if (err instanceof ApiError && err.code === 'CONNECTION_NOT_FOUND') {
@@ -1593,15 +1661,17 @@ connections
1593
1661
  }
1594
1662
  throw err;
1595
1663
  }
1596
- const boundProjects = connection.boundProjects ?? [];
1664
+ const connection = normalizeConnection(raw);
1665
+ const boundProjects = raw.boundProjects ?? raw.bound_projects ?? [];
1666
+ const providerLabelStr = formatProviderLabel(connection.provider);
1597
1667
  if (!opts.force) {
1598
1668
  console.log('');
1599
- console.log(`Disconnect ${chalk.cyan(connection.name)}?`);
1669
+ console.log(`Disconnect ${chalk.magenta(providerLabelStr)} ${chalk.cyan(connection.name ?? id)}?`);
1600
1670
  console.log('');
1601
1671
  if (boundProjects.length > 0) {
1602
1672
  const projectList = boundProjects.map(p => p.name).join(', ');
1603
1673
  console.log(` This connection is used by ${boundProjects.length} ${boundProjects.length === 1 ? 'project' : 'projects'}: ${projectList}.`);
1604
- console.log(' Pipelines in those projects will fail on GitHub steps until');
1674
+ console.log(` Pipelines in those projects will fail on ${providerLabelStr} steps until`);
1605
1675
  console.log(' they are bound to a different connection.');
1606
1676
  console.log('');
1607
1677
  }
@@ -1609,7 +1679,13 @@ connections
1609
1679
  console.log(' No projects are using this connection.');
1610
1680
  console.log('');
1611
1681
  }
1612
- console.log(chalk.gray(' This does not uninstall the Fazemos App from GitHub.'));
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
+ }
1613
1689
  console.log('');
1614
1690
  const answer = await promptLine('Disconnect? [y/N]: ');
1615
1691
  if (!answer || !/^y/i.test(answer.trim())) {
@@ -1617,11 +1693,14 @@ connections
1617
1693
  return;
1618
1694
  }
1619
1695
  }
1620
- const data = await api('DELETE', `/api/organizations/${orgId}/github/connections/${id}`, undefined, { noProjectHeader: true });
1621
- console.log(chalk.green(`Disconnected: ${connection.name}`));
1622
- const affected = data.affectedProjects ?? [];
1623
- if (affected.length > 0) {
1624
- console.log(chalk.gray(` ${affected.length} project${affected.length === 1 ? '' : 's'} unbound.`));
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.`));
1625
1704
  }
1626
1705
  invalidateAuthMeCache();
1627
1706
  }
@@ -1636,19 +1715,23 @@ connections
1636
1715
  */
1637
1716
  connections
1638
1717
  .command('health-check')
1639
- .description('Verify a Connection is still healthy on GitHub')
1718
+ .description('Verify a Connection is still healthy on its code platform (GitHub or BitBucket)')
1640
1719
  .argument('<id>', 'Connection ID')
1641
1720
  .action(async (id) => {
1642
1721
  try {
1643
1722
  const orgId = requireActiveOrgOrExit();
1644
- const data = await api('POST', `/api/organizations/${orgId}/github/connections/${id}/health-check`, {}, { noProjectHeader: true });
1645
- const c = data.connection;
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);
1646
1728
  const statusColor = pickStatusColor(c.status);
1729
+ const label = `${formatProviderLabel(c.provider)} ${c.name ?? id}`;
1647
1730
  if (data.changed) {
1648
- console.log(chalk.yellow(`Status changed: ${c.name} → ${statusColor(c.status)}`));
1731
+ console.log(chalk.yellow(`Status changed: ${label} → ${statusColor(c.status)}`));
1649
1732
  }
1650
1733
  else {
1651
- console.log(chalk.green(`✓ ${c.name} — ${statusColor(c.status)}`));
1734
+ console.log(chalk.green(`✓ ${label} — ${statusColor(c.status)}`));
1652
1735
  }
1653
1736
  }
1654
1737
  catch (err) {
@@ -1660,6 +1743,53 @@ connections
1660
1743
  process.exit(1);
1661
1744
  }
1662
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
+ }
1663
1793
  /**
1664
1794
  * Helper — color a status string per the Sage §2.2 taxonomy.
1665
1795
  */
@@ -1679,6 +1809,30 @@ function pickStatusColor(status) {
1679
1809
  return chalk.white;
1680
1810
  }
1681
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
+ }
1682
1836
  /**
1683
1837
  * Read a single line of input from stdin. No fancy framing — `readline`
1684
1838
  * is sufficient for the install confirmation-code prompt and the