@freecodecamp/universe-cli 0.4.0 → 0.5.1

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
@@ -16,10 +16,7 @@ var EXIT_STORAGE = 13;
16
16
  var EXIT_GIT = 15;
17
17
  var EXIT_CONFIRM = 18;
18
18
  var EXIT_PARTIAL = 19;
19
- function exitWithCode(code, message) {
20
- if (message !== void 0) {
21
- process.stderr.write(message + "\n");
22
- }
19
+ function exitWithCode(code) {
23
20
  process.exit(code);
24
21
  }
25
22
 
@@ -384,6 +381,53 @@ See docs/platform-yaml.md.`
384
381
  return { ok: true, value: result.data };
385
382
  }
386
383
 
384
+ // src/lib/similarity.ts
385
+ function editDistance(a, b) {
386
+ const m = a.length;
387
+ const n = b.length;
388
+ if (m === 0) return n;
389
+ if (n === 0) return m;
390
+ const d = Array.from(
391
+ { length: m + 1 },
392
+ () => new Array(n + 1).fill(0)
393
+ );
394
+ for (let i = 0; i <= m; i++) d[i][0] = i;
395
+ for (let j = 0; j <= n; j++) d[0][j] = j;
396
+ for (let i = 1; i <= m; i++) {
397
+ for (let j = 1; j <= n; j++) {
398
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
399
+ d[i][j] = Math.min(
400
+ d[i - 1][j] + 1,
401
+ d[i][j - 1] + 1,
402
+ d[i - 1][j - 1] + cost
403
+ );
404
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
405
+ d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
406
+ }
407
+ }
408
+ }
409
+ return d[m][n];
410
+ }
411
+ function suggest(target, candidates, threshold = 2) {
412
+ if (candidates.length === 0) return null;
413
+ const lc = target.toLowerCase();
414
+ const sub = candidates.find((c) => {
415
+ const clc = c.toLowerCase();
416
+ return clc.includes(lc) || lc.includes(clc);
417
+ });
418
+ if (sub) return sub;
419
+ let best = null;
420
+ let bestD = threshold + 1;
421
+ for (const c of candidates) {
422
+ const d = editDistance(lc, c.toLowerCase());
423
+ if (d < bestD) {
424
+ bestD = d;
425
+ best = c;
426
+ }
427
+ }
428
+ return bestD <= threshold ? best : null;
429
+ }
430
+
387
431
  // src/lib/proxy-client.ts
388
432
  var ProxyError = class extends CliError {
389
433
  exitCode;
@@ -546,6 +590,52 @@ function createProxyClient(cfg) {
546
590
  },
547
591
  body: JSON.stringify({ to: req.to })
548
592
  });
593
+ },
594
+ async registerSite(req) {
595
+ const body = { slug: req.slug };
596
+ if (req.teams && req.teams.length > 0) {
597
+ body.teams = req.teams;
598
+ }
599
+ return call(`${base}/api/site/register`, {
600
+ method: "POST",
601
+ headers: {
602
+ Authorization: await userBearer(),
603
+ Accept: "application/json",
604
+ "Content-Type": "application/json"
605
+ },
606
+ body: JSON.stringify(body)
607
+ });
608
+ },
609
+ async listSites() {
610
+ return call(`${base}/api/sites`, {
611
+ method: "GET",
612
+ headers: {
613
+ Authorization: await userBearer(),
614
+ Accept: "application/json"
615
+ }
616
+ });
617
+ },
618
+ async updateSite(req) {
619
+ const url = `${base}/api/site/${encodeURIComponent(req.slug)}`;
620
+ return call(url, {
621
+ method: "PATCH",
622
+ headers: {
623
+ Authorization: await userBearer(),
624
+ Accept: "application/json",
625
+ "Content-Type": "application/json"
626
+ },
627
+ body: JSON.stringify({ teams: req.teams })
628
+ });
629
+ },
630
+ async deleteSite(req) {
631
+ const url = `${base}/api/site/${encodeURIComponent(req.slug)}`;
632
+ return call(url, {
633
+ method: "DELETE",
634
+ headers: {
635
+ Authorization: await userBearer(),
636
+ Accept: "application/json"
637
+ }
638
+ });
549
639
  }
550
640
  };
551
641
  }
@@ -740,6 +830,52 @@ function rethrowProxy(prefix, err) {
740
830
  if (err instanceof Error) throw new StorageError(`${prefix}: ${err.message}`);
741
831
  throw new StorageError(`${prefix}: ${String(err)}`);
742
832
  }
833
+ var PREFLIGHT_INLINE_LIST_CAP = 10;
834
+ function formatUnauthorizedSiteError(a) {
835
+ const lines = [
836
+ `Site '${a.attempted}' is not registered for your GitHub identity.`,
837
+ ``,
838
+ ` You are: ${a.login}`,
839
+ ``
840
+ ];
841
+ if (a.authorized.length === 0) {
842
+ lines.push(
843
+ ` Your identity is authorized for no sites yet.`,
844
+ ``,
845
+ ` Likely causes:`,
846
+ ` 1. The '${a.attempted}' slug is not registered.`,
847
+ ` Admin (staff): universe sites register ${a.attempted} --team <team>`,
848
+ ` 2. You are not in any team listed on any registered site.`,
849
+ ` Admin (staff): universe sites update <slug> --team +<your-team>`
850
+ );
851
+ return lines.join("\n");
852
+ }
853
+ const hint = suggest(a.attempted, a.authorized);
854
+ if (hint) {
855
+ lines.push(` Did you mean: ${hint}?`, ``);
856
+ }
857
+ lines.push(
858
+ ` Likely causes (most common first):`,
859
+ ` 1. Typo in platform.yaml \`site:\` \u2014 check the spelling above.`,
860
+ ` 2. The '${a.attempted}' slug is not registered yet.`,
861
+ ` Admin (staff): universe sites register ${a.attempted} --team <team>`,
862
+ ` 3. You are not in any team authorized for '${a.attempted}'.`,
863
+ ` Admin (staff): universe sites update ${a.attempted} --team +<your-team>`,
864
+ ``
865
+ );
866
+ if (a.authorized.length <= PREFLIGHT_INLINE_LIST_CAP) {
867
+ lines.push(
868
+ ` Your authorized sites (${a.authorized.length}):`,
869
+ ...[...a.authorized].sort().map((s) => ` - ${s}`)
870
+ );
871
+ } else {
872
+ lines.push(
873
+ ` You have ${a.authorized.length} authorized sites \u2014 too many to inline.`,
874
+ ` Run \`universe sites ls --mine\` to inspect the full list.`
875
+ );
876
+ }
877
+ return lines.join("\n");
878
+ }
743
879
  async function deploy(options, deps = {}) {
744
880
  const cwd = deps.cwd ?? process.cwd();
745
881
  const env = deps.env ?? process.env;
@@ -775,26 +911,16 @@ async function deploy(options, deps = {}) {
775
911
  rethrowProxy("whoami preflight failed", err);
776
912
  }
777
913
  if (!me.authorizedSites.includes(config.site)) {
778
- const sitesLine = me.authorizedSites.length > 0 ? me.authorizedSites.join(", ") : "(no sites authorized)";
779
914
  throw new CredentialError(
780
- [
781
- `Site '${config.site}' is not registered for your GitHub identity.`,
782
- ``,
783
- ` You are: ${me.login}`,
784
- ` Authorized sites: ${sitesLine}`,
785
- ``,
786
- `Likely causes (most common first):`,
787
- ` 1. Platform admin has not added '${config.site}' to artemis`,
788
- ` 'config/sites.yaml' yet (one-time, per site).`,
789
- ` 2. You are not in any GitHub team listed for '${config.site}'.`,
790
- ``,
791
- `Runbook:`,
792
- ` https://github.com/freeCodeCamp/infra/blob/main/docs/runbooks/01-deploy-new-constellation-site.md`
793
- ].join("\n")
915
+ formatUnauthorizedSiteError({
916
+ attempted: config.site,
917
+ login: me.login,
918
+ authorized: me.authorizedSites
919
+ })
794
920
  );
795
921
  }
796
922
  const git = gitState();
797
- if (git.dirty) {
923
+ if (git.dirty && !options.json) {
798
924
  warn(
799
925
  "git working tree is dirty \u2014 uncommitted changes will not be reflected."
800
926
  );
@@ -806,7 +932,7 @@ async function deploy(options, deps = {}) {
806
932
  cwd,
807
933
  outputDir
808
934
  });
809
- if (buildResult.skipped) {
935
+ if (buildResult.skipped && !options.json) {
810
936
  info("build.command not set \u2014 using pre-built output.");
811
937
  }
812
938
  const resolvedOutputDir = buildResult.outputDir;
@@ -865,7 +991,7 @@ async function deploy(options, deps = {}) {
865
991
  );
866
992
  } else {
867
993
  const sizeKB = (uploadResult.totalSize / 1024).toFixed(1);
868
- const nextLine = mode === "preview" ? "Next: universe static promote" : "Promoted to production.";
994
+ const nextLine = mode === "preview" ? `Next: universe static promote --from ${finalizeResult.deployId}` : "Promoted to production.\nPreview alias unchanged.";
869
995
  success(
870
996
  [
871
997
  `Deployed ${finalizeResult.deployId}`,
@@ -901,7 +1027,7 @@ async function deploy(options, deps = {}) {
901
1027
  } else {
902
1028
  error(message);
903
1029
  }
904
- exit(code, message);
1030
+ exit(code);
905
1031
  }
906
1032
  }
907
1033
 
@@ -1012,7 +1138,7 @@ async function login(options, deps = {}) {
1012
1138
  } else {
1013
1139
  error(msg);
1014
1140
  }
1015
- exit(EXIT_CONFIRM, msg);
1141
+ exit(EXIT_CONFIRM);
1016
1142
  return;
1017
1143
  }
1018
1144
  }
@@ -1055,7 +1181,7 @@ async function login(options, deps = {}) {
1055
1181
  } else {
1056
1182
  error(message);
1057
1183
  }
1058
- exit(EXIT_CREDENTIALS, message);
1184
+ exit(EXIT_CREDENTIALS);
1059
1185
  return;
1060
1186
  }
1061
1187
  await save(token);
@@ -1168,7 +1294,10 @@ async function ls(options, deps = {}) {
1168
1294
  getAuthToken: () => identity.token
1169
1295
  });
1170
1296
  const raw = await client.siteDeploys({ site });
1171
- const deploys = raw.map((d) => parseDeployId(d.deployId));
1297
+ const sorted = [...raw].sort(
1298
+ (a, b) => b.deployId.localeCompare(a.deployId)
1299
+ );
1300
+ const deploys = sorted.map((d) => parseDeployId(d.deployId));
1172
1301
  if (options.json) {
1173
1302
  emitJson4(
1174
1303
  buildEnvelope("ls", true, {
@@ -1191,7 +1320,7 @@ async function ls(options, deps = {}) {
1191
1320
  } else {
1192
1321
  error(message);
1193
1322
  }
1194
- exit(code, message);
1323
+ exit(code);
1195
1324
  }
1196
1325
  }
1197
1326
 
@@ -1262,15 +1391,17 @@ async function promote(options, deps = {}) {
1262
1391
  })
1263
1392
  );
1264
1393
  } else {
1265
- success(
1266
- [
1267
- `Promoted ${result.deployId} to production`,
1268
- ``,
1269
- ` Site: ${config.site}`,
1270
- ` Deploy: ${result.deployId}`,
1271
- ` Production: ${result.url}`
1272
- ].join("\n")
1273
- );
1394
+ const lines = [
1395
+ `Promoted ${result.deployId} to production`,
1396
+ ``,
1397
+ ` Site: ${config.site}`,
1398
+ ` Deploy: ${result.deployId}`,
1399
+ ` Production: ${result.url}`
1400
+ ];
1401
+ if (options.from) {
1402
+ lines.push(``, "Preview alias unchanged.");
1403
+ }
1404
+ success(lines.join("\n"));
1274
1405
  }
1275
1406
  } catch (err) {
1276
1407
  const { code, message } = wrapProxyError("promote", err);
@@ -1279,7 +1410,7 @@ async function promote(options, deps = {}) {
1279
1410
  } else {
1280
1411
  error(message);
1281
1412
  }
1282
- exit(code, message);
1413
+ exit(code);
1283
1414
  }
1284
1415
  }
1285
1416
 
@@ -1367,7 +1498,7 @@ async function rollback(options, deps = {}) {
1367
1498
  } else {
1368
1499
  error(message);
1369
1500
  }
1370
- exit(code, message);
1501
+ exit(code);
1371
1502
  }
1372
1503
  }
1373
1504
 
@@ -1392,7 +1523,7 @@ async function whoami(options, deps = {}) {
1392
1523
  } else {
1393
1524
  error(msg);
1394
1525
  }
1395
- exit(EXIT_CREDENTIALS, msg);
1526
+ exit(EXIT_CREDENTIALS);
1396
1527
  return;
1397
1528
  }
1398
1529
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
@@ -1402,21 +1533,22 @@ async function whoami(options, deps = {}) {
1402
1533
  });
1403
1534
  try {
1404
1535
  const result = await client.whoami();
1536
+ const count = result.authorizedSites.length;
1405
1537
  if (options.json) {
1406
1538
  emitJson7(
1407
1539
  buildEnvelope("whoami", true, {
1408
1540
  login: result.login,
1409
- authorizedSites: result.authorizedSites,
1410
- identitySource: identity.source
1541
+ identitySource: identity.source,
1542
+ authorizedSitesCount: count
1411
1543
  })
1412
1544
  );
1413
1545
  } else {
1414
- const sites = result.authorizedSites.length > 0 ? result.authorizedSites.join(", ") : "(no sites authorized)";
1546
+ const sitesLine = count === 0 ? "Authorized for 0 sites." : `Authorized for ${count} site${count === 1 ? "" : "s"} \u2014 run \`universe sites ls --mine\``;
1415
1547
  success(
1416
1548
  [
1417
1549
  `Logged in as: ${result.login}`,
1418
- `Authorized sites: ${sites}`,
1419
- `Identity source: ${identity.source}`
1550
+ `Identity source: ${identity.source}`,
1551
+ sitesLine
1420
1552
  ].join("\n")
1421
1553
  );
1422
1554
  }
@@ -1428,13 +1560,238 @@ async function whoami(options, deps = {}) {
1428
1560
  } else {
1429
1561
  error(message);
1430
1562
  }
1431
- exit(exitCode, message);
1563
+ exit(exitCode);
1432
1564
  }
1433
1565
  }
1434
1566
 
1435
- // src/output/format.ts
1567
+ // src/commands/sites/ls.ts
1436
1568
  import { log as log8 } from "@clack/prompts";
1437
1569
 
1570
+ // src/commands/sites/_shared.ts
1571
+ function emitJson8(envelope) {
1572
+ process.stdout.write(JSON.stringify(envelope) + "\n");
1573
+ }
1574
+ function parseTeamsFlag(raw) {
1575
+ if (raw === void 0 || raw === null) return [];
1576
+ const tokens = Array.isArray(raw) ? raw : [raw];
1577
+ return tokens.flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
1578
+ }
1579
+ async function setupClient(deps) {
1580
+ const env = deps.env ?? process.env;
1581
+ const resolveId = deps.resolveIdentity ?? resolveIdentity;
1582
+ const mkClient = deps.createProxyClient ?? createProxyClient;
1583
+ const identity = await resolveId({ env });
1584
+ if (!identity) {
1585
+ throw new CredentialError(
1586
+ "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
1587
+ );
1588
+ }
1589
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1590
+ const client = mkClient({
1591
+ baseUrl,
1592
+ getAuthToken: () => identity.token
1593
+ });
1594
+ return { client, identitySource: identity.source };
1595
+ }
1596
+
1597
+ // src/commands/sites/ls.ts
1598
+ function formatTable2(rows) {
1599
+ if (rows.length === 0) return "No registered sites.";
1600
+ const headers = ["SLUG", "TEAMS", "CREATED BY", "CREATED AT"];
1601
+ const cells = rows.map((r) => [
1602
+ r.slug,
1603
+ r.teams.join(","),
1604
+ r.createdBy,
1605
+ r.createdAt
1606
+ ]);
1607
+ const widths = headers.map(
1608
+ (h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
1609
+ );
1610
+ const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
1611
+ return [fmt(headers), ...cells.map(fmt)].join("\n");
1612
+ }
1613
+ async function ls2(options, deps = {}) {
1614
+ const command = "sites ls";
1615
+ const success = deps.logSuccess ?? ((s) => log8.message(s));
1616
+ const error = deps.logError ?? ((s) => log8.error(s));
1617
+ const exit = deps.exit ?? exitWithCode;
1618
+ try {
1619
+ const { client } = await setupClient(deps);
1620
+ let rows = await client.listSites();
1621
+ let scope = "all";
1622
+ if (options.mine) {
1623
+ const me = await client.whoami();
1624
+ const allowed = new Set(me.authorizedSites);
1625
+ rows = rows.filter((r) => allowed.has(r.slug));
1626
+ scope = "mine";
1627
+ }
1628
+ if (options.json) {
1629
+ emitJson8(
1630
+ buildEnvelope(command, true, {
1631
+ count: rows.length,
1632
+ scope,
1633
+ sites: rows
1634
+ })
1635
+ );
1636
+ } else {
1637
+ success(formatTable2(rows));
1638
+ }
1639
+ } catch (err) {
1640
+ const { code, message } = wrapProxyError(command, err);
1641
+ if (options.json) {
1642
+ emitJson8(buildErrorEnvelope(command, code, message));
1643
+ } else {
1644
+ error(message);
1645
+ }
1646
+ exit(code);
1647
+ }
1648
+ }
1649
+
1650
+ // src/commands/sites/register.ts
1651
+ import { log as log9 } from "@clack/prompts";
1652
+ async function register(options, deps = {}) {
1653
+ const command = "sites register";
1654
+ const success = deps.logSuccess ?? ((s) => log9.success(s));
1655
+ const error = deps.logError ?? ((s) => log9.error(s));
1656
+ const exit = deps.exit ?? exitWithCode;
1657
+ try {
1658
+ if (!options.slug || options.slug.trim().length === 0) {
1659
+ throw new UsageError("slug is required (positional argument)");
1660
+ }
1661
+ const teams = parseTeamsFlag(options.team);
1662
+ const { client } = await setupClient(deps);
1663
+ const row = await client.registerSite({
1664
+ slug: options.slug,
1665
+ teams: teams.length > 0 ? teams : void 0
1666
+ });
1667
+ if (options.json) {
1668
+ emitJson8(
1669
+ buildEnvelope(command, true, {
1670
+ slug: row.slug,
1671
+ teams: row.teams,
1672
+ createdAt: row.createdAt,
1673
+ createdBy: row.createdBy
1674
+ })
1675
+ );
1676
+ } else {
1677
+ success(
1678
+ [
1679
+ `Registered ${row.slug}`,
1680
+ ``,
1681
+ ` Slug: ${row.slug}`,
1682
+ ` Teams: ${row.teams.join(", ")}`,
1683
+ ` Created by: ${row.createdBy}`,
1684
+ ` Created at: ${row.createdAt}`
1685
+ ].join("\n")
1686
+ );
1687
+ }
1688
+ } catch (err) {
1689
+ const { code, message } = wrapProxyError(command, err);
1690
+ if (options.json) {
1691
+ emitJson8(buildErrorEnvelope(command, code, message));
1692
+ } else {
1693
+ error(message);
1694
+ }
1695
+ exit(code);
1696
+ }
1697
+ }
1698
+
1699
+ // src/commands/sites/rm.ts
1700
+ import { log as log10 } from "@clack/prompts";
1701
+ async function rm2(options, deps = {}) {
1702
+ const command = "sites rm";
1703
+ const success = deps.logSuccess ?? ((s) => log10.success(s));
1704
+ const error = deps.logError ?? ((s) => log10.error(s));
1705
+ const exit = deps.exit ?? exitWithCode;
1706
+ try {
1707
+ if (!options.slug || options.slug.trim().length === 0) {
1708
+ throw new UsageError("slug is required (positional argument)");
1709
+ }
1710
+ const { client } = await setupClient(deps);
1711
+ await client.deleteSite({ slug: options.slug });
1712
+ if (options.json) {
1713
+ emitJson8(
1714
+ buildEnvelope(command, true, {
1715
+ slug: options.slug,
1716
+ deleted: true
1717
+ })
1718
+ );
1719
+ } else {
1720
+ success(
1721
+ [
1722
+ `Deleted ${options.slug}`,
1723
+ ``,
1724
+ ` Note: R2 deploy bytes are NOT removed; they age out via the`,
1725
+ ` post-GA cleanup cron.`
1726
+ ].join("\n")
1727
+ );
1728
+ }
1729
+ } catch (err) {
1730
+ const { code, message } = wrapProxyError(command, err);
1731
+ if (options.json) {
1732
+ emitJson8(buildErrorEnvelope(command, code, message));
1733
+ } else {
1734
+ error(message);
1735
+ }
1736
+ exit(code);
1737
+ }
1738
+ }
1739
+
1740
+ // src/commands/sites/update.ts
1741
+ import { log as log11 } from "@clack/prompts";
1742
+ async function update(options, deps = {}) {
1743
+ const command = "sites update";
1744
+ const success = deps.logSuccess ?? ((s) => log11.success(s));
1745
+ const error = deps.logError ?? ((s) => log11.error(s));
1746
+ const exit = deps.exit ?? exitWithCode;
1747
+ try {
1748
+ if (!options.slug || options.slug.trim().length === 0) {
1749
+ throw new UsageError("slug is required (positional argument)");
1750
+ }
1751
+ const teams = parseTeamsFlag(options.team);
1752
+ if (teams.length === 0) {
1753
+ throw new UsageError(
1754
+ "--team is required with at least one slug; use `sites rm` to remove a site"
1755
+ );
1756
+ }
1757
+ const { client } = await setupClient(deps);
1758
+ const row = await client.updateSite({
1759
+ slug: options.slug,
1760
+ teams
1761
+ });
1762
+ if (options.json) {
1763
+ emitJson8(
1764
+ buildEnvelope(command, true, {
1765
+ slug: row.slug,
1766
+ teams: row.teams,
1767
+ updatedAt: row.updatedAt
1768
+ })
1769
+ );
1770
+ } else {
1771
+ success(
1772
+ [
1773
+ `Updated ${row.slug}`,
1774
+ ``,
1775
+ ` Slug: ${row.slug}`,
1776
+ ` Teams: ${row.teams.join(", ")}`,
1777
+ ` Updated at: ${row.updatedAt}`
1778
+ ].join("\n")
1779
+ );
1780
+ }
1781
+ } catch (err) {
1782
+ const { code, message } = wrapProxyError(command, err);
1783
+ if (options.json) {
1784
+ emitJson8(buildErrorEnvelope(command, code, message));
1785
+ } else {
1786
+ error(message);
1787
+ }
1788
+ exit(code);
1789
+ }
1790
+ }
1791
+
1792
+ // src/output/format.ts
1793
+ import { log as log12 } from "@clack/prompts";
1794
+
1438
1795
  // src/output/redact.ts
1439
1796
  var AWS_KEY_PREFIX_PATTERN = /(?:AKIA|ASIA|AROA|AIDA|ACCA|ANPA|ABIA|AGPA)[A-Z0-9]{12,}/g;
1440
1797
  var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
@@ -1492,18 +1849,18 @@ function outputError(ctx, code, message, issues) {
1492
1849
  );
1493
1850
  process.stdout.write(JSON.stringify(envelope) + "\n");
1494
1851
  } else {
1495
- log8.error(redactedMessage);
1852
+ log12.error(redactedMessage);
1496
1853
  }
1497
1854
  }
1498
1855
 
1499
1856
  // src/cli.ts
1500
- var version = true ? "0.4.0" : "0.0.0";
1857
+ var version = true ? "0.5.1" : "0.0.0";
1501
1858
  function handleActionError(command, json, err) {
1502
1859
  const ctx = { json, command };
1503
1860
  const message = err instanceof Error ? err.message : "unknown error";
1504
1861
  const code = err instanceof CliError ? err.exitCode : EXIT_USAGE;
1505
1862
  outputError(ctx, code, message);
1506
- exitWithCode(code, message);
1863
+ exitWithCode(code);
1507
1864
  }
1508
1865
  function findFirstPositional(args) {
1509
1866
  for (let i = 0; i < args.length; i += 1) {
@@ -1515,7 +1872,75 @@ function findFirstPositional(args) {
1515
1872
  function run(argv = process.argv) {
1516
1873
  const args = argv.slice(2);
1517
1874
  const firstPosIdx = findFirstPositional(args);
1518
- const isStatic = firstPosIdx >= 0 && args[firstPosIdx] === "static";
1875
+ const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
1876
+ const isStatic = namespace === "static";
1877
+ const isSites = namespace === "sites";
1878
+ if (isSites) {
1879
+ const sitesArgs = [
1880
+ ...args.slice(0, firstPosIdx),
1881
+ ...args.slice(firstPosIdx + 1)
1882
+ ];
1883
+ const sitesCli = cac("universe sites");
1884
+ sitesCli.command("register <slug>", "Register a new static site (staff only)").option("--json", "Output as JSON").option(
1885
+ "--team <name>",
1886
+ "GitHub team slug (repeatable, or comma-separated). Defaults to staff."
1887
+ ).action(
1888
+ async (slug, flags) => {
1889
+ try {
1890
+ await register({
1891
+ json: flags.json ?? false,
1892
+ slug,
1893
+ team: flags.team
1894
+ });
1895
+ } catch (err) {
1896
+ handleActionError("sites register", flags.json ?? false, err);
1897
+ }
1898
+ }
1899
+ );
1900
+ sitesCli.command("ls", "List sites in the registry").option("--json", "Output as JSON").option(
1901
+ "--mine",
1902
+ "Filter to sites your GitHub identity is authorized for"
1903
+ ).action(async (flags) => {
1904
+ try {
1905
+ await ls2({
1906
+ json: flags.json ?? false,
1907
+ mine: flags.mine ?? false
1908
+ });
1909
+ } catch (err) {
1910
+ handleActionError("sites ls", flags.json ?? false, err);
1911
+ }
1912
+ });
1913
+ sitesCli.command(
1914
+ "update <slug>",
1915
+ "Replace the teams list for an existing site (staff only)"
1916
+ ).option("--json", "Output as JSON").option(
1917
+ "--team <name>",
1918
+ "GitHub team slug (repeatable, or comma-separated). Required."
1919
+ ).action(
1920
+ async (slug, flags) => {
1921
+ try {
1922
+ await update({
1923
+ json: flags.json ?? false,
1924
+ slug,
1925
+ team: flags.team
1926
+ });
1927
+ } catch (err) {
1928
+ handleActionError("sites update", flags.json ?? false, err);
1929
+ }
1930
+ }
1931
+ );
1932
+ sitesCli.command("rm <slug>", "Remove a site from the registry (staff only)").option("--json", "Output as JSON").action(async (slug, flags) => {
1933
+ try {
1934
+ await rm2({ json: flags.json ?? false, slug });
1935
+ } catch (err) {
1936
+ handleActionError("sites rm", flags.json ?? false, err);
1937
+ }
1938
+ });
1939
+ sitesCli.help();
1940
+ sitesCli.version(version);
1941
+ sitesCli.parse(["node", "universe-sites", ...sitesArgs]);
1942
+ return;
1943
+ }
1519
1944
  if (isStatic) {
1520
1945
  const staticArgs = [
1521
1946
  ...args.slice(0, firstPosIdx),
@@ -1598,6 +2023,7 @@ function run(argv = process.argv) {
1598
2023
  }
1599
2024
  });
1600
2025
  cli.command("static <subcommand>", "Static site deployment commands");
2026
+ cli.command("sites <subcommand>", "Static site registry commands");
1601
2027
  cli.help();
1602
2028
  cli.version(version);
1603
2029
  cli.parse(argv);