@formigio/fazemos-cli 0.10.14 → 0.10.16
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 +345 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -10,6 +10,7 @@ import { isProjectConnectionUnavailable, renderProjectConnectionUnavailableCopy,
|
|
|
10
10
|
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
|
+
import { findLocalRegistry, resolveRole, buildInboxFile, writeInboxFile, buildNotificationPayload, gitCommitInboxFile, } from './dispatch.js';
|
|
13
14
|
import { execSync } from 'child_process';
|
|
14
15
|
import { readFileSync, readdirSync, writeFileSync, mkdirSync, existsSync, statSync } from 'fs';
|
|
15
16
|
import { fileURLToPath } from 'url';
|
|
@@ -1390,27 +1391,34 @@ const connections = program.command('connections').alias('conn').description('Gi
|
|
|
1390
1391
|
*/
|
|
1391
1392
|
connections
|
|
1392
1393
|
.command('list')
|
|
1393
|
-
.description('List
|
|
1394
|
+
.description('List VCS Connections (GitHub + BitBucket) in the active organization')
|
|
1394
1395
|
.option('-s, --status <status>', 'Filter: active (default), all, pending, suspended, revoked, uninstalled', 'active')
|
|
1395
1396
|
.action(async (opts) => {
|
|
1396
1397
|
try {
|
|
1397
1398
|
const orgId = requireActiveOrgOrExit();
|
|
1398
|
-
|
|
1399
|
-
|
|
1399
|
+
// F22 — call the provider-agnostic /vcs/connections endpoint.
|
|
1400
|
+
// The legacy /github/connections alias still works during the
|
|
1401
|
+
// dual-write phase, but `vcs/` returns BOTH providers' rows so
|
|
1402
|
+
// the user sees their full Org Connection list.
|
|
1403
|
+
const data = await api('GET', `/api/organizations/${orgId}/vcs/connections?status=${encodeURIComponent(opts.status)}`, undefined, { noProjectHeader: true });
|
|
1404
|
+
const list = (data.connections ?? []).map(normalizeConnection);
|
|
1400
1405
|
if (list.length === 0) {
|
|
1401
1406
|
const orgName = findOrgById(orgId)?.name ?? orgId;
|
|
1402
|
-
console.log(chalk.yellow(`No
|
|
1407
|
+
console.log(chalk.yellow(`No code platform connections in ${orgName}.`));
|
|
1403
1408
|
console.log(chalk.gray('Add one with: fazemos connections install'));
|
|
1404
1409
|
return;
|
|
1405
1410
|
}
|
|
1406
1411
|
const orgName = findOrgById(orgId)?.name ?? orgId;
|
|
1407
|
-
console.log(chalk.cyan(`
|
|
1412
|
+
console.log(chalk.cyan(`Code platform connections in ${orgName}:`));
|
|
1408
1413
|
console.log('');
|
|
1409
1414
|
for (const c of list) {
|
|
1410
1415
|
const statusColor = pickStatusColor(c.status);
|
|
1411
1416
|
const projects = c.projectCount == null ? '—' : `${c.projectCount} ${c.projectCount === 1 ? 'project' : 'projects'}`;
|
|
1412
|
-
const login = c.
|
|
1413
|
-
|
|
1417
|
+
const login = c.accountLogin ?? chalk.gray('—');
|
|
1418
|
+
const providerLabel = formatProviderLabel(c.provider);
|
|
1419
|
+
// F22 §8.3 item 1: provider column. Renders as a prefix tag so
|
|
1420
|
+
// mixed-provider lists are scannable at a glance.
|
|
1421
|
+
console.log(` ${chalk.magenta(providerLabel)} ${chalk.cyan(c.name)} ${chalk.gray(login)} ${statusColor(c.status)} ${chalk.gray(projects)}`);
|
|
1414
1422
|
console.log(chalk.gray(` ID: ${c.id}`));
|
|
1415
1423
|
}
|
|
1416
1424
|
}
|
|
@@ -1425,40 +1433,56 @@ connections
|
|
|
1425
1433
|
*/
|
|
1426
1434
|
connections
|
|
1427
1435
|
.command('show')
|
|
1428
|
-
.description('Show a
|
|
1436
|
+
.description('Show a VCS Connection in detail')
|
|
1429
1437
|
.argument('<id>', 'Connection ID')
|
|
1430
1438
|
.action(async (id) => {
|
|
1431
1439
|
try {
|
|
1432
1440
|
const orgId = requireActiveOrgOrExit();
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1441
|
+
// F22 — provider-agnostic detail endpoint. The serialized shape
|
|
1442
|
+
// includes `provider`, `account_login`, `supports_contents_api`
|
|
1443
|
+
// and (for GitHub) `installation_id` / (BitBucket) `workspace_uuid`.
|
|
1444
|
+
const data = await api('GET', `/api/organizations/${orgId}/vcs/connections/${id}`, undefined, { noProjectHeader: true });
|
|
1445
|
+
const raw = data.connection ?? data;
|
|
1446
|
+
const c = normalizeConnection(raw);
|
|
1447
|
+
console.log(chalk.cyan(c.name ?? '(unnamed connection)'));
|
|
1436
1448
|
console.log(` ID: ${c.id}`);
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1449
|
+
// F22 §8.3 item 2 — provider line above account info.
|
|
1450
|
+
console.log(` Provider: ${formatProviderLabel(c.provider)}`);
|
|
1451
|
+
console.log(` Account: ${c.accountLogin ?? chalk.gray('— (pending)')}`);
|
|
1452
|
+
if (c.accountType)
|
|
1453
|
+
console.log(` Account type: ${c.accountType}`);
|
|
1440
1454
|
const statusColor = pickStatusColor(c.status);
|
|
1441
1455
|
console.log(` Status: ${statusColor(c.status)}`);
|
|
1442
|
-
|
|
1456
|
+
if (c.installedAt) {
|
|
1457
|
+
console.log(` Installed: ${new Date(c.installedAt).toLocaleString()}`);
|
|
1458
|
+
}
|
|
1443
1459
|
if (c.lastHealthCheckAt) {
|
|
1444
1460
|
console.log(` Last checked: ${new Date(c.lastHealthCheckAt).toLocaleString()}`);
|
|
1445
1461
|
}
|
|
1446
1462
|
if (c.lastUsedAt) {
|
|
1447
1463
|
console.log(` Last used: ${new Date(c.lastUsedAt).toLocaleString()}`);
|
|
1448
1464
|
}
|
|
1449
|
-
|
|
1450
|
-
|
|
1465
|
+
// F22 §6.4 — supports_contents_api is the canonical signal for
|
|
1466
|
+
// "can this Connection back F18 docs surfaces?" Surface it so
|
|
1467
|
+
// users understand why docs may be unavailable on BitBucket.
|
|
1468
|
+
if (typeof c.supportsContentsApi === 'boolean') {
|
|
1469
|
+
const docs = c.supportsContentsApi ? 'yes' : chalk.yellow('no');
|
|
1470
|
+
console.log(` Docs API: ${docs}`);
|
|
1471
|
+
}
|
|
1472
|
+
if (raw.repositorySelection === 'all') {
|
|
1473
|
+
console.log(` Repos: All repositories${c.accountLogin ? ` in ${c.accountLogin}` : ''}`);
|
|
1451
1474
|
}
|
|
1452
|
-
else if (Array.isArray(
|
|
1453
|
-
console.log(` Repos: ${
|
|
1454
|
-
for (const r of
|
|
1455
|
-
console.log(chalk.gray(` · ${r.fullName}`));
|
|
1475
|
+
else if (Array.isArray(raw.repositories)) {
|
|
1476
|
+
console.log(` Repos: ${raw.repositories.length}${raw.repositoriesTruncated ? ' (showing 100)' : ''}`);
|
|
1477
|
+
for (const r of raw.repositories) {
|
|
1478
|
+
console.log(chalk.gray(` · ${r.fullName ?? r.full_name ?? r.name}`));
|
|
1456
1479
|
}
|
|
1457
1480
|
}
|
|
1458
|
-
|
|
1481
|
+
const boundProjects = raw.boundProjects ?? raw.bound_projects ?? [];
|
|
1482
|
+
if (Array.isArray(boundProjects) && boundProjects.length > 0) {
|
|
1459
1483
|
console.log('');
|
|
1460
1484
|
console.log(chalk.cyan('Projects using this connection:'));
|
|
1461
|
-
for (const p of
|
|
1485
|
+
for (const p of boundProjects) {
|
|
1462
1486
|
console.log(` · ${p.name} (${p.slug})`);
|
|
1463
1487
|
}
|
|
1464
1488
|
}
|
|
@@ -1479,12 +1503,55 @@ connections
|
|
|
1479
1503
|
*/
|
|
1480
1504
|
connections
|
|
1481
1505
|
.command('install')
|
|
1482
|
-
.description('Add a GitHub
|
|
1483
|
-
.
|
|
1506
|
+
.description('Add a VCS Connection (GitHub or BitBucket). Prints an install URL and (for GitHub) waits for a confirmation code.')
|
|
1507
|
+
.option('--provider <provider>', 'VCS provider: github | bitbucket')
|
|
1508
|
+
.action(async (opts) => {
|
|
1484
1509
|
try {
|
|
1485
1510
|
const orgId = requireActiveOrgOrExit();
|
|
1486
1511
|
const orgName = findOrgById(orgId)?.name ?? orgId;
|
|
1487
|
-
|
|
1512
|
+
// F22 §8.3 item 3 — provider selection matrix.
|
|
1513
|
+
// 1) `--provider github|bitbucket` supplied: use directly.
|
|
1514
|
+
// 2) No flag: prompt the user interactively (provider-picker).
|
|
1515
|
+
// The CLI cannot probe server-side provider configuration, so the
|
|
1516
|
+
// "exactly one provider configured" auto-select branch of the spec
|
|
1517
|
+
// is implemented as "user always confirms" — narrower than the
|
|
1518
|
+
// spec wording but strictly safer (no silent provider choice).
|
|
1519
|
+
let provider = opts.provider?.toLowerCase();
|
|
1520
|
+
if (!provider) {
|
|
1521
|
+
provider = await promptProviderChoice();
|
|
1522
|
+
}
|
|
1523
|
+
if (provider !== 'github' && provider !== 'bitbucket') {
|
|
1524
|
+
console.error(chalk.red(`Invalid --provider: ${provider}. Use 'github' or 'bitbucket'.`));
|
|
1525
|
+
process.exit(1);
|
|
1526
|
+
}
|
|
1527
|
+
// F22 prefers the new provider-agnostic /vcs/connections route, which
|
|
1528
|
+
// expects `provider` in the body. The github-only endpoint is kept
|
|
1529
|
+
// alive but not used here so all new connections flow through one path.
|
|
1530
|
+
const mintData = await api('POST', `/api/organizations/${orgId}/vcs/connections/install-url`, { provider, source: 'cli', returnTo: null }, { noProjectHeader: true });
|
|
1531
|
+
// F22 §8.3 item 8 — BitBucket install is browser-only in v1 (no CLI
|
|
1532
|
+
// confirmation-code exchange counterpart). Print the literal copy
|
|
1533
|
+
// block specified in the spec verbatim, then exit successfully.
|
|
1534
|
+
// The user completes consent in the browser and the /api/bitbucket/
|
|
1535
|
+
// oauth/callback handler finishes the Connection server-side.
|
|
1536
|
+
//
|
|
1537
|
+
// Dex nit N4 — output literal MUST match the spec block exactly so
|
|
1538
|
+
// the acceptance test can assert it.
|
|
1539
|
+
if (provider === 'bitbucket') {
|
|
1540
|
+
console.log('');
|
|
1541
|
+
console.log('BitBucket Connection install — browser required');
|
|
1542
|
+
console.log('');
|
|
1543
|
+
console.log('Open this URL in your browser to authorize Fazemos:');
|
|
1544
|
+
console.log('');
|
|
1545
|
+
console.log(` ${mintData.url}`);
|
|
1546
|
+
console.log('');
|
|
1547
|
+
console.log('After you authorize, BitBucket will redirect to Fazemos and complete the Connection.');
|
|
1548
|
+
console.log('You can then list your connections with:');
|
|
1549
|
+
console.log('');
|
|
1550
|
+
console.log(' fazemos connections list');
|
|
1551
|
+
console.log('');
|
|
1552
|
+
console.log('This URL expires in 10 minutes.');
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1488
1555
|
console.log('');
|
|
1489
1556
|
console.log(`To add a GitHub connection to ${chalk.cyan(orgName)}:`);
|
|
1490
1557
|
console.log('');
|
|
@@ -1573,18 +1640,20 @@ connections
|
|
|
1573
1640
|
connections
|
|
1574
1641
|
.command('revoke')
|
|
1575
1642
|
.alias('disconnect')
|
|
1576
|
-
.description('Disconnect a
|
|
1643
|
+
.description('Disconnect a VCS Connection (Fazemos-side; does not uninstall the App from GitHub / revoke the OAuth grant on BitBucket).')
|
|
1577
1644
|
.argument('<id>', 'Connection ID')
|
|
1578
1645
|
.option('-f, --force', 'Skip the confirmation prompt', false)
|
|
1579
1646
|
.action(async (id, opts) => {
|
|
1580
1647
|
try {
|
|
1581
1648
|
const orgId = requireActiveOrgOrExit();
|
|
1582
1649
|
// Fetch the Connection so we can show what we're about to revoke
|
|
1583
|
-
// and how many projects it affects (
|
|
1584
|
-
|
|
1650
|
+
// and how many projects it affects (UX §8.3 confirmation copy).
|
|
1651
|
+
// F22 — use the provider-agnostic /vcs/ endpoint so both provider
|
|
1652
|
+
// rows are reachable.
|
|
1653
|
+
let raw;
|
|
1585
1654
|
try {
|
|
1586
|
-
const detail = await api('GET', `/api/organizations/${orgId}/
|
|
1587
|
-
|
|
1655
|
+
const detail = await api('GET', `/api/organizations/${orgId}/vcs/connections/${id}`, undefined, { noProjectHeader: true });
|
|
1656
|
+
raw = detail.connection ?? detail;
|
|
1588
1657
|
}
|
|
1589
1658
|
catch (err) {
|
|
1590
1659
|
if (err instanceof ApiError && err.code === 'CONNECTION_NOT_FOUND') {
|
|
@@ -1593,15 +1662,17 @@ connections
|
|
|
1593
1662
|
}
|
|
1594
1663
|
throw err;
|
|
1595
1664
|
}
|
|
1596
|
-
const
|
|
1665
|
+
const connection = normalizeConnection(raw);
|
|
1666
|
+
const boundProjects = raw.boundProjects ?? raw.bound_projects ?? [];
|
|
1667
|
+
const providerLabelStr = formatProviderLabel(connection.provider);
|
|
1597
1668
|
if (!opts.force) {
|
|
1598
1669
|
console.log('');
|
|
1599
|
-
console.log(`Disconnect ${chalk.cyan(connection.name)}?`);
|
|
1670
|
+
console.log(`Disconnect ${chalk.magenta(providerLabelStr)} ${chalk.cyan(connection.name ?? id)}?`);
|
|
1600
1671
|
console.log('');
|
|
1601
1672
|
if (boundProjects.length > 0) {
|
|
1602
1673
|
const projectList = boundProjects.map(p => p.name).join(', ');
|
|
1603
1674
|
console.log(` This connection is used by ${boundProjects.length} ${boundProjects.length === 1 ? 'project' : 'projects'}: ${projectList}.`);
|
|
1604
|
-
console.log(
|
|
1675
|
+
console.log(` Pipelines in those projects will fail on ${providerLabelStr} steps until`);
|
|
1605
1676
|
console.log(' they are bound to a different connection.');
|
|
1606
1677
|
console.log('');
|
|
1607
1678
|
}
|
|
@@ -1609,7 +1680,13 @@ connections
|
|
|
1609
1680
|
console.log(' No projects are using this connection.');
|
|
1610
1681
|
console.log('');
|
|
1611
1682
|
}
|
|
1612
|
-
|
|
1683
|
+
// Provider-aware caveat: GitHub App vs BitBucket OAuth grant.
|
|
1684
|
+
if (connection.provider === 'bitbucket') {
|
|
1685
|
+
console.log(chalk.gray(' This does not revoke the OAuth grant on BitBucket.'));
|
|
1686
|
+
}
|
|
1687
|
+
else {
|
|
1688
|
+
console.log(chalk.gray(' This does not uninstall the Fazemos App from GitHub.'));
|
|
1689
|
+
}
|
|
1613
1690
|
console.log('');
|
|
1614
1691
|
const answer = await promptLine('Disconnect? [y/N]: ');
|
|
1615
1692
|
if (!answer || !/^y/i.test(answer.trim())) {
|
|
@@ -1617,11 +1694,14 @@ connections
|
|
|
1617
1694
|
return;
|
|
1618
1695
|
}
|
|
1619
1696
|
}
|
|
1620
|
-
const data = await api('DELETE', `/api/organizations/${orgId}/
|
|
1621
|
-
console.log(chalk.green(`Disconnected: ${connection.name}`));
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1697
|
+
const data = await api('DELETE', `/api/organizations/${orgId}/vcs/connections/${id}`, undefined, { noProjectHeader: true });
|
|
1698
|
+
console.log(chalk.green(`Disconnected: ${providerLabelStr} ${connection.name ?? id}`));
|
|
1699
|
+
// F22 response shape: { revoked, secrets_deleted, projects_unbound }.
|
|
1700
|
+
// F16 legacy shape: { affectedProjects: [...] }. Handle both during
|
|
1701
|
+
// the dual-write rollout.
|
|
1702
|
+
const unbound = data.projects_unbound ?? (data.affectedProjects ?? []).length ?? 0;
|
|
1703
|
+
if (unbound > 0) {
|
|
1704
|
+
console.log(chalk.gray(` ${unbound} project${unbound === 1 ? '' : 's'} unbound.`));
|
|
1625
1705
|
}
|
|
1626
1706
|
invalidateAuthMeCache();
|
|
1627
1707
|
}
|
|
@@ -1636,19 +1716,23 @@ connections
|
|
|
1636
1716
|
*/
|
|
1637
1717
|
connections
|
|
1638
1718
|
.command('health-check')
|
|
1639
|
-
.description('Verify a Connection is still healthy on GitHub')
|
|
1719
|
+
.description('Verify a Connection is still healthy on its code platform (GitHub or BitBucket)')
|
|
1640
1720
|
.argument('<id>', 'Connection ID')
|
|
1641
1721
|
.action(async (id) => {
|
|
1642
1722
|
try {
|
|
1643
1723
|
const orgId = requireActiveOrgOrExit();
|
|
1644
|
-
|
|
1645
|
-
|
|
1724
|
+
// F22 — provider-aware health-check route on the /vcs/ alias.
|
|
1725
|
+
// The handler dispatches to the right provider (GitHub installation
|
|
1726
|
+
// probe / BitBucket OAuth-refresh + workspace probe).
|
|
1727
|
+
const data = await api('POST', `/api/organizations/${orgId}/vcs/connections/${id}/health-check`, {}, { noProjectHeader: true });
|
|
1728
|
+
const c = normalizeConnection(data.connection ?? data);
|
|
1646
1729
|
const statusColor = pickStatusColor(c.status);
|
|
1730
|
+
const label = `${formatProviderLabel(c.provider)} ${c.name ?? id}`;
|
|
1647
1731
|
if (data.changed) {
|
|
1648
|
-
console.log(chalk.yellow(`Status changed: ${
|
|
1732
|
+
console.log(chalk.yellow(`Status changed: ${label} → ${statusColor(c.status)}`));
|
|
1649
1733
|
}
|
|
1650
1734
|
else {
|
|
1651
|
-
console.log(chalk.green(`✓ ${
|
|
1735
|
+
console.log(chalk.green(`✓ ${label} — ${statusColor(c.status)}`));
|
|
1652
1736
|
}
|
|
1653
1737
|
}
|
|
1654
1738
|
catch (err) {
|
|
@@ -1660,6 +1744,53 @@ connections
|
|
|
1660
1744
|
process.exit(1);
|
|
1661
1745
|
}
|
|
1662
1746
|
});
|
|
1747
|
+
export function normalizeConnection(raw) {
|
|
1748
|
+
if (!raw || typeof raw !== 'object') {
|
|
1749
|
+
return {
|
|
1750
|
+
id: '',
|
|
1751
|
+
provider: null,
|
|
1752
|
+
name: null,
|
|
1753
|
+
accountLogin: null,
|
|
1754
|
+
accountType: null,
|
|
1755
|
+
status: 'unknown',
|
|
1756
|
+
installedAt: null,
|
|
1757
|
+
lastHealthCheckAt: null,
|
|
1758
|
+
lastUsedAt: null,
|
|
1759
|
+
supportsContentsApi: null,
|
|
1760
|
+
projectCount: null,
|
|
1761
|
+
};
|
|
1762
|
+
}
|
|
1763
|
+
// Provider — F22 new field. Older /github/ rows always meant github;
|
|
1764
|
+
// default there for back-compat.
|
|
1765
|
+
const provider = raw.provider ?? (raw.githubAccountLogin !== undefined ? 'github' : null);
|
|
1766
|
+
return {
|
|
1767
|
+
id: String(raw.id ?? ''),
|
|
1768
|
+
provider,
|
|
1769
|
+
name: raw.name ?? null,
|
|
1770
|
+
accountLogin: raw.accountLogin ?? raw.account_login ?? raw.githubAccountLogin ?? null,
|
|
1771
|
+
accountType: raw.accountType ?? raw.account_type ?? raw.githubAccountType ?? null,
|
|
1772
|
+
status: raw.status ?? 'unknown',
|
|
1773
|
+
installedAt: raw.installedAt ?? raw.installed_at ?? raw.created_at ?? null,
|
|
1774
|
+
lastHealthCheckAt: raw.lastHealthCheckAt ?? raw.last_health_check_at ?? null,
|
|
1775
|
+
lastUsedAt: raw.lastUsedAt ?? raw.last_used_at ?? null,
|
|
1776
|
+
supportsContentsApi: typeof raw.supportsContentsApi === 'boolean'
|
|
1777
|
+
? raw.supportsContentsApi
|
|
1778
|
+
: typeof raw.supports_contents_api === 'boolean'
|
|
1779
|
+
? raw.supports_contents_api
|
|
1780
|
+
: null,
|
|
1781
|
+
projectCount: raw.projectCount ?? raw.project_count ?? null,
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
/**
|
|
1785
|
+
* F22 — Human-friendly provider label used in CLI columns and lines.
|
|
1786
|
+
*/
|
|
1787
|
+
export function formatProviderLabel(provider) {
|
|
1788
|
+
if (provider === 'github')
|
|
1789
|
+
return 'GitHub';
|
|
1790
|
+
if (provider === 'bitbucket')
|
|
1791
|
+
return 'BitBucket';
|
|
1792
|
+
return '—';
|
|
1793
|
+
}
|
|
1663
1794
|
/**
|
|
1664
1795
|
* Helper — color a status string per the Sage §2.2 taxonomy.
|
|
1665
1796
|
*/
|
|
@@ -1679,6 +1810,30 @@ function pickStatusColor(status) {
|
|
|
1679
1810
|
return chalk.white;
|
|
1680
1811
|
}
|
|
1681
1812
|
}
|
|
1813
|
+
/**
|
|
1814
|
+
* F22 §8.3 item 3 — interactive provider picker. Used by
|
|
1815
|
+
* `fazemos connections install` when no `--provider` flag is supplied
|
|
1816
|
+
* so the user explicitly chooses GitHub or BitBucket before the URL
|
|
1817
|
+
* mint hits the API. Mirrors the web `ProviderPickerModal` choice
|
|
1818
|
+
* without the modal chrome.
|
|
1819
|
+
*
|
|
1820
|
+
* Returns 'github' or 'bitbucket'. Repeats on invalid input.
|
|
1821
|
+
*/
|
|
1822
|
+
export async function promptProviderChoice() {
|
|
1823
|
+
console.log('');
|
|
1824
|
+
console.log('Which code platform do you want to connect?');
|
|
1825
|
+
console.log(' 1. GitHub');
|
|
1826
|
+
console.log(' 2. BitBucket');
|
|
1827
|
+
console.log('');
|
|
1828
|
+
while (true) {
|
|
1829
|
+
const ans = (await promptLine(' Choose [1=GitHub / 2=BitBucket]: ')).trim().toLowerCase();
|
|
1830
|
+
if (ans === '1' || ans === 'github' || ans === 'gh')
|
|
1831
|
+
return 'github';
|
|
1832
|
+
if (ans === '2' || ans === 'bitbucket' || ans === 'bb')
|
|
1833
|
+
return 'bitbucket';
|
|
1834
|
+
console.log(chalk.yellow(' Please answer 1 (GitHub) or 2 (BitBucket).'));
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1682
1837
|
/**
|
|
1683
1838
|
* Read a single line of input from stdin. No fancy framing — `readline`
|
|
1684
1839
|
* is sufficient for the install confirmation-code prompt and the
|
|
@@ -7898,6 +8053,151 @@ Examples:
|
|
|
7898
8053
|
if (summary.errors > 0)
|
|
7899
8054
|
process.exit(1);
|
|
7900
8055
|
});
|
|
8056
|
+
// ── `fazemos dispatch` — role-to-role dispatch ─────────────────────
|
|
8057
|
+
//
|
|
8058
|
+
// Writes an inbox markdown file in the recipient role's operating dir and
|
|
8059
|
+
// calls POST /api/notifications/dispatch so the API can fire any
|
|
8060
|
+
// configured human-filler notifications (Slack today).
|
|
8061
|
+
//
|
|
8062
|
+
// Recipients are resolved via `.fazemos/roles.json` walked up from cwd.
|
|
8063
|
+
// Cross-workspace roles are followed via the local registry's
|
|
8064
|
+
// `cross_workspace_roles` block.
|
|
8065
|
+
//
|
|
8066
|
+
// Example:
|
|
8067
|
+
// fazemos dispatch founder question --from business-strategist \
|
|
8068
|
+
// --body "Should we ship F19 before F7?" --priority high --commit
|
|
8069
|
+
program
|
|
8070
|
+
.command('dispatch <to> <type>')
|
|
8071
|
+
.description(`Write an inbox markdown file in <to>'s operating dir and (optionally) fire
|
|
8072
|
+
notifications via the API.
|
|
8073
|
+
|
|
8074
|
+
Recipients are resolved via the nearest .fazemos/roles.json registry, walking
|
|
8075
|
+
up from the current directory. Cross-workspace recipients are followed via
|
|
8076
|
+
the local registry's cross_workspace_roles block.
|
|
8077
|
+
|
|
8078
|
+
Types: question | task | signal | response | flag | decision | direction`)
|
|
8079
|
+
.requiredOption('--from <role>', 'sender role-slug (required)')
|
|
8080
|
+
.option('--body <text>', 'markdown body of the dispatch (or use --body-file)')
|
|
8081
|
+
.option('--body-file <path>', 'read body from a file')
|
|
8082
|
+
.option('--priority <level>', 'low | normal | high', 'normal')
|
|
8083
|
+
.option('--re <ref>', 'optional reference (worksheet id, file path, etc.)')
|
|
8084
|
+
.option('--thread <id>', 'optional prior item id this responds to')
|
|
8085
|
+
.option('--expires-at <iso>', 'optional deadline (ISO 8601)')
|
|
8086
|
+
.option('--commit', 'git add + commit the inbox file after writing')
|
|
8087
|
+
.option('--no-notify', 'skip the API notification call (file-only)')
|
|
8088
|
+
.action(async (to, type, opts) => {
|
|
8089
|
+
try {
|
|
8090
|
+
// Validate type
|
|
8091
|
+
const allowedTypes = ['question', 'task', 'signal', 'response', 'flag', 'decision', 'direction'];
|
|
8092
|
+
if (!allowedTypes.includes(type)) {
|
|
8093
|
+
throw new Error(`Invalid type "${type}". Allowed: ${allowedTypes.join(', ')}`);
|
|
8094
|
+
}
|
|
8095
|
+
// Resolve body
|
|
8096
|
+
let body = opts.body;
|
|
8097
|
+
if (!body && opts.bodyFile) {
|
|
8098
|
+
body = readFileSync(opts.bodyFile, 'utf-8').trim();
|
|
8099
|
+
}
|
|
8100
|
+
if (!body) {
|
|
8101
|
+
throw new Error('--body or --body-file is required');
|
|
8102
|
+
}
|
|
8103
|
+
// Find local registry
|
|
8104
|
+
const localRegistry = findLocalRegistry(process.cwd());
|
|
8105
|
+
if (!localRegistry) {
|
|
8106
|
+
throw new Error(`No .fazemos/roles.json found in cwd or any parent. ` +
|
|
8107
|
+
`Run from inside a Fazemos-aware workspace.`);
|
|
8108
|
+
}
|
|
8109
|
+
// Resolve recipient
|
|
8110
|
+
const resolved = resolveRole(to, localRegistry);
|
|
8111
|
+
if (!resolved) {
|
|
8112
|
+
throw new Error(`Role "${to}" not found in local registry or any cross-workspace ref. ` +
|
|
8113
|
+
`Check .fazemos/roles.json.`);
|
|
8114
|
+
}
|
|
8115
|
+
const { role, registry } = resolved;
|
|
8116
|
+
const input = {
|
|
8117
|
+
to,
|
|
8118
|
+
from: opts.from,
|
|
8119
|
+
type: type,
|
|
8120
|
+
priority: (opts.priority ?? 'normal'),
|
|
8121
|
+
body,
|
|
8122
|
+
re: opts.re,
|
|
8123
|
+
thread: opts.thread,
|
|
8124
|
+
expiresAt: opts.expiresAt,
|
|
8125
|
+
};
|
|
8126
|
+
// Build + write file
|
|
8127
|
+
const { filename, content, summary } = buildInboxFile(input);
|
|
8128
|
+
const fullPath = writeInboxFile(registry._workspaceRoot, role, filename, content);
|
|
8129
|
+
const relPath = fullPath.startsWith(registry._workspaceRoot)
|
|
8130
|
+
? fullPath.slice(registry._workspaceRoot.length + 1)
|
|
8131
|
+
: fullPath;
|
|
8132
|
+
console.log(chalk.green(`✓ Wrote inbox file:`));
|
|
8133
|
+
console.log(` ${chalk.cyan(fullPath)}`);
|
|
8134
|
+
console.log(` Workspace: ${registry.workspace} Filler: ${role.filler.identity} (${role.filler.type})`);
|
|
8135
|
+
// Notify (unless --no-notify)
|
|
8136
|
+
if (opts.notify !== false) {
|
|
8137
|
+
try {
|
|
8138
|
+
const payload = buildNotificationPayload(input, role, relPath, summary);
|
|
8139
|
+
const resp = await api('POST', '/api/notifications/dispatch', payload);
|
|
8140
|
+
if (resp.notified) {
|
|
8141
|
+
console.log(chalk.green(`✓ Notification fired: ${resp.channel}`));
|
|
8142
|
+
}
|
|
8143
|
+
else {
|
|
8144
|
+
console.log(chalk.gray(` (no notification fired — ${resp.reason ?? 'unknown'})`));
|
|
8145
|
+
}
|
|
8146
|
+
}
|
|
8147
|
+
catch (err) {
|
|
8148
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8149
|
+
console.log(chalk.yellow(` warning: notification call failed — ${msg}`));
|
|
8150
|
+
console.log(chalk.yellow(` (inbox file was still written successfully)`));
|
|
8151
|
+
}
|
|
8152
|
+
}
|
|
8153
|
+
else {
|
|
8154
|
+
console.log(chalk.gray(' (--no-notify; API not called)'));
|
|
8155
|
+
}
|
|
8156
|
+
// Commit if requested
|
|
8157
|
+
if (opts.commit) {
|
|
8158
|
+
try {
|
|
8159
|
+
gitCommitInboxFile(registry._workspaceRoot, relPath, `dispatch(${opts.from} → ${to}): ${type}`);
|
|
8160
|
+
console.log(chalk.green('✓ Committed.'));
|
|
8161
|
+
}
|
|
8162
|
+
catch (err) {
|
|
8163
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
8164
|
+
console.log(chalk.yellow(` warning: commit failed — ${msg}`));
|
|
8165
|
+
}
|
|
8166
|
+
}
|
|
8167
|
+
}
|
|
8168
|
+
catch (err) {
|
|
8169
|
+
console.error(chalk.red(err instanceof Error ? err.message : String(err)));
|
|
8170
|
+
process.exit(1);
|
|
8171
|
+
}
|
|
8172
|
+
});
|
|
8173
|
+
program
|
|
8174
|
+
.command('dispatch-list-roles')
|
|
8175
|
+
.description('List roles available in the nearest .fazemos/roles.json registry')
|
|
8176
|
+
.action(() => {
|
|
8177
|
+
const reg = findLocalRegistry(process.cwd());
|
|
8178
|
+
if (!reg) {
|
|
8179
|
+
console.error(chalk.red('No .fazemos/roles.json found in cwd or any parent.'));
|
|
8180
|
+
process.exit(1);
|
|
8181
|
+
}
|
|
8182
|
+
console.log(chalk.cyan(`Registry: ${reg._registryPath}`));
|
|
8183
|
+
console.log(chalk.cyan(`Workspace: ${reg.workspace} Org: ${reg.org}${reg.project ? ' Project: ' + reg.project : ''}`));
|
|
8184
|
+
console.log();
|
|
8185
|
+
console.log(chalk.bold('Local roles:'));
|
|
8186
|
+
for (const [slug, role] of Object.entries(reg.roles)) {
|
|
8187
|
+
const fillerTag = role.filler.type === 'human'
|
|
8188
|
+
? chalk.yellow(`${role.filler.identity} (human)`)
|
|
8189
|
+
: chalk.gray(`${role.filler.identity} (agent)`);
|
|
8190
|
+
const notifyTag = role.notification ? chalk.green(' [notify]') : '';
|
|
8191
|
+
console.log(` ${chalk.cyan(slug.padEnd(32))} ${fillerTag}${notifyTag}`);
|
|
8192
|
+
}
|
|
8193
|
+
if (reg.cross_workspace_roles) {
|
|
8194
|
+
console.log();
|
|
8195
|
+
console.log(chalk.bold('Cross-workspace roles:'));
|
|
8196
|
+
for (const [slug, xref] of Object.entries(reg.cross_workspace_roles)) {
|
|
8197
|
+
console.log(` ${chalk.cyan(slug.padEnd(32))} → ${xref.workspace_path}`);
|
|
8198
|
+
}
|
|
8199
|
+
}
|
|
8200
|
+
});
|
|
7901
8201
|
// Skip auto-parse only when running under Vitest (which sets process.env.VITEST).
|
|
7902
8202
|
// Tests import `program` and drive it via `program.parseAsync(...)` after mocking
|
|
7903
8203
|
// `./api.js`. In every other context — direct invocation, npx tsx, OR the bin
|