@freecodecamp/universe-cli 0.4.0 → 0.5.0

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,22 +911,12 @@ 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();
@@ -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);
@@ -1191,7 +1317,7 @@ async function ls(options, deps = {}) {
1191
1317
  } else {
1192
1318
  error(message);
1193
1319
  }
1194
- exit(code, message);
1320
+ exit(code);
1195
1321
  }
1196
1322
  }
1197
1323
 
@@ -1279,7 +1405,7 @@ async function promote(options, deps = {}) {
1279
1405
  } else {
1280
1406
  error(message);
1281
1407
  }
1282
- exit(code, message);
1408
+ exit(code);
1283
1409
  }
1284
1410
  }
1285
1411
 
@@ -1367,7 +1493,7 @@ async function rollback(options, deps = {}) {
1367
1493
  } else {
1368
1494
  error(message);
1369
1495
  }
1370
- exit(code, message);
1496
+ exit(code);
1371
1497
  }
1372
1498
  }
1373
1499
 
@@ -1392,7 +1518,7 @@ async function whoami(options, deps = {}) {
1392
1518
  } else {
1393
1519
  error(msg);
1394
1520
  }
1395
- exit(EXIT_CREDENTIALS, msg);
1521
+ exit(EXIT_CREDENTIALS);
1396
1522
  return;
1397
1523
  }
1398
1524
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
@@ -1402,21 +1528,22 @@ async function whoami(options, deps = {}) {
1402
1528
  });
1403
1529
  try {
1404
1530
  const result = await client.whoami();
1531
+ const count = result.authorizedSites.length;
1405
1532
  if (options.json) {
1406
1533
  emitJson7(
1407
1534
  buildEnvelope("whoami", true, {
1408
1535
  login: result.login,
1409
- authorizedSites: result.authorizedSites,
1410
- identitySource: identity.source
1536
+ identitySource: identity.source,
1537
+ authorizedSitesCount: count
1411
1538
  })
1412
1539
  );
1413
1540
  } else {
1414
- const sites = result.authorizedSites.length > 0 ? result.authorizedSites.join(", ") : "(no sites authorized)";
1541
+ const sitesLine = count === 0 ? "Authorized for 0 sites." : `Authorized for ${count} site${count === 1 ? "" : "s"} \u2014 run \`universe sites ls --mine\``;
1415
1542
  success(
1416
1543
  [
1417
1544
  `Logged in as: ${result.login}`,
1418
- `Authorized sites: ${sites}`,
1419
- `Identity source: ${identity.source}`
1545
+ `Identity source: ${identity.source}`,
1546
+ sitesLine
1420
1547
  ].join("\n")
1421
1548
  );
1422
1549
  }
@@ -1428,13 +1555,238 @@ async function whoami(options, deps = {}) {
1428
1555
  } else {
1429
1556
  error(message);
1430
1557
  }
1431
- exit(exitCode, message);
1558
+ exit(exitCode);
1432
1559
  }
1433
1560
  }
1434
1561
 
1435
- // src/output/format.ts
1562
+ // src/commands/sites/ls.ts
1436
1563
  import { log as log8 } from "@clack/prompts";
1437
1564
 
1565
+ // src/commands/sites/_shared.ts
1566
+ function emitJson8(envelope) {
1567
+ process.stdout.write(JSON.stringify(envelope) + "\n");
1568
+ }
1569
+ function parseTeamsFlag(raw) {
1570
+ if (raw === void 0 || raw === null) return [];
1571
+ const tokens = Array.isArray(raw) ? raw : [raw];
1572
+ return tokens.flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
1573
+ }
1574
+ async function setupClient(deps) {
1575
+ const env = deps.env ?? process.env;
1576
+ const resolveId = deps.resolveIdentity ?? resolveIdentity;
1577
+ const mkClient = deps.createProxyClient ?? createProxyClient;
1578
+ const identity = await resolveId({ env });
1579
+ if (!identity) {
1580
+ throw new CredentialError(
1581
+ "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
1582
+ );
1583
+ }
1584
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1585
+ const client = mkClient({
1586
+ baseUrl,
1587
+ getAuthToken: () => identity.token
1588
+ });
1589
+ return { client, identitySource: identity.source };
1590
+ }
1591
+
1592
+ // src/commands/sites/ls.ts
1593
+ function formatTable2(rows) {
1594
+ if (rows.length === 0) return "No registered sites.";
1595
+ const headers = ["SLUG", "TEAMS", "CREATED BY", "CREATED AT"];
1596
+ const cells = rows.map((r) => [
1597
+ r.slug,
1598
+ r.teams.join(","),
1599
+ r.createdBy,
1600
+ r.createdAt
1601
+ ]);
1602
+ const widths = headers.map(
1603
+ (h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
1604
+ );
1605
+ const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
1606
+ return [fmt(headers), ...cells.map(fmt)].join("\n");
1607
+ }
1608
+ async function ls2(options, deps = {}) {
1609
+ const command = "sites ls";
1610
+ const success = deps.logSuccess ?? ((s) => log8.message(s));
1611
+ const error = deps.logError ?? ((s) => log8.error(s));
1612
+ const exit = deps.exit ?? exitWithCode;
1613
+ try {
1614
+ const { client } = await setupClient(deps);
1615
+ let rows = await client.listSites();
1616
+ let scope = "all";
1617
+ if (options.mine) {
1618
+ const me = await client.whoami();
1619
+ const allowed = new Set(me.authorizedSites);
1620
+ rows = rows.filter((r) => allowed.has(r.slug));
1621
+ scope = "mine";
1622
+ }
1623
+ if (options.json) {
1624
+ emitJson8(
1625
+ buildEnvelope(command, true, {
1626
+ count: rows.length,
1627
+ scope,
1628
+ sites: rows
1629
+ })
1630
+ );
1631
+ } else {
1632
+ success(formatTable2(rows));
1633
+ }
1634
+ } catch (err) {
1635
+ const { code, message } = wrapProxyError(command, err);
1636
+ if (options.json) {
1637
+ emitJson8(buildErrorEnvelope(command, code, message));
1638
+ } else {
1639
+ error(message);
1640
+ }
1641
+ exit(code);
1642
+ }
1643
+ }
1644
+
1645
+ // src/commands/sites/register.ts
1646
+ import { log as log9 } from "@clack/prompts";
1647
+ async function register(options, deps = {}) {
1648
+ const command = "sites register";
1649
+ const success = deps.logSuccess ?? ((s) => log9.success(s));
1650
+ const error = deps.logError ?? ((s) => log9.error(s));
1651
+ const exit = deps.exit ?? exitWithCode;
1652
+ try {
1653
+ if (!options.slug || options.slug.trim().length === 0) {
1654
+ throw new UsageError("slug is required (positional argument)");
1655
+ }
1656
+ const teams = parseTeamsFlag(options.team);
1657
+ const { client } = await setupClient(deps);
1658
+ const row = await client.registerSite({
1659
+ slug: options.slug,
1660
+ teams: teams.length > 0 ? teams : void 0
1661
+ });
1662
+ if (options.json) {
1663
+ emitJson8(
1664
+ buildEnvelope(command, true, {
1665
+ slug: row.slug,
1666
+ teams: row.teams,
1667
+ createdAt: row.createdAt,
1668
+ createdBy: row.createdBy
1669
+ })
1670
+ );
1671
+ } else {
1672
+ success(
1673
+ [
1674
+ `Registered ${row.slug}`,
1675
+ ``,
1676
+ ` Slug: ${row.slug}`,
1677
+ ` Teams: ${row.teams.join(", ")}`,
1678
+ ` Created by: ${row.createdBy}`,
1679
+ ` Created at: ${row.createdAt}`
1680
+ ].join("\n")
1681
+ );
1682
+ }
1683
+ } catch (err) {
1684
+ const { code, message } = wrapProxyError(command, err);
1685
+ if (options.json) {
1686
+ emitJson8(buildErrorEnvelope(command, code, message));
1687
+ } else {
1688
+ error(message);
1689
+ }
1690
+ exit(code);
1691
+ }
1692
+ }
1693
+
1694
+ // src/commands/sites/rm.ts
1695
+ import { log as log10 } from "@clack/prompts";
1696
+ async function rm2(options, deps = {}) {
1697
+ const command = "sites rm";
1698
+ const success = deps.logSuccess ?? ((s) => log10.success(s));
1699
+ const error = deps.logError ?? ((s) => log10.error(s));
1700
+ const exit = deps.exit ?? exitWithCode;
1701
+ try {
1702
+ if (!options.slug || options.slug.trim().length === 0) {
1703
+ throw new UsageError("slug is required (positional argument)");
1704
+ }
1705
+ const { client } = await setupClient(deps);
1706
+ await client.deleteSite({ slug: options.slug });
1707
+ if (options.json) {
1708
+ emitJson8(
1709
+ buildEnvelope(command, true, {
1710
+ slug: options.slug,
1711
+ deleted: true
1712
+ })
1713
+ );
1714
+ } else {
1715
+ success(
1716
+ [
1717
+ `Deleted ${options.slug}`,
1718
+ ``,
1719
+ ` Note: R2 deploy bytes are NOT removed; they age out via the`,
1720
+ ` post-GA cleanup cron.`
1721
+ ].join("\n")
1722
+ );
1723
+ }
1724
+ } catch (err) {
1725
+ const { code, message } = wrapProxyError(command, err);
1726
+ if (options.json) {
1727
+ emitJson8(buildErrorEnvelope(command, code, message));
1728
+ } else {
1729
+ error(message);
1730
+ }
1731
+ exit(code);
1732
+ }
1733
+ }
1734
+
1735
+ // src/commands/sites/update.ts
1736
+ import { log as log11 } from "@clack/prompts";
1737
+ async function update(options, deps = {}) {
1738
+ const command = "sites update";
1739
+ const success = deps.logSuccess ?? ((s) => log11.success(s));
1740
+ const error = deps.logError ?? ((s) => log11.error(s));
1741
+ const exit = deps.exit ?? exitWithCode;
1742
+ try {
1743
+ if (!options.slug || options.slug.trim().length === 0) {
1744
+ throw new UsageError("slug is required (positional argument)");
1745
+ }
1746
+ const teams = parseTeamsFlag(options.team);
1747
+ if (teams.length === 0) {
1748
+ throw new UsageError(
1749
+ "--team is required with at least one slug; use `sites rm` to remove a site"
1750
+ );
1751
+ }
1752
+ const { client } = await setupClient(deps);
1753
+ const row = await client.updateSite({
1754
+ slug: options.slug,
1755
+ teams
1756
+ });
1757
+ if (options.json) {
1758
+ emitJson8(
1759
+ buildEnvelope(command, true, {
1760
+ slug: row.slug,
1761
+ teams: row.teams,
1762
+ updatedAt: row.updatedAt
1763
+ })
1764
+ );
1765
+ } else {
1766
+ success(
1767
+ [
1768
+ `Updated ${row.slug}`,
1769
+ ``,
1770
+ ` Slug: ${row.slug}`,
1771
+ ` Teams: ${row.teams.join(", ")}`,
1772
+ ` Updated at: ${row.updatedAt}`
1773
+ ].join("\n")
1774
+ );
1775
+ }
1776
+ } catch (err) {
1777
+ const { code, message } = wrapProxyError(command, err);
1778
+ if (options.json) {
1779
+ emitJson8(buildErrorEnvelope(command, code, message));
1780
+ } else {
1781
+ error(message);
1782
+ }
1783
+ exit(code);
1784
+ }
1785
+ }
1786
+
1787
+ // src/output/format.ts
1788
+ import { log as log12 } from "@clack/prompts";
1789
+
1438
1790
  // src/output/redact.ts
1439
1791
  var AWS_KEY_PREFIX_PATTERN = /(?:AKIA|ASIA|AROA|AIDA|ACCA|ANPA|ABIA|AGPA)[A-Z0-9]{12,}/g;
1440
1792
  var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
@@ -1492,18 +1844,18 @@ function outputError(ctx, code, message, issues) {
1492
1844
  );
1493
1845
  process.stdout.write(JSON.stringify(envelope) + "\n");
1494
1846
  } else {
1495
- log8.error(redactedMessage);
1847
+ log12.error(redactedMessage);
1496
1848
  }
1497
1849
  }
1498
1850
 
1499
1851
  // src/cli.ts
1500
- var version = true ? "0.4.0" : "0.0.0";
1852
+ var version = true ? "0.5.0" : "0.0.0";
1501
1853
  function handleActionError(command, json, err) {
1502
1854
  const ctx = { json, command };
1503
1855
  const message = err instanceof Error ? err.message : "unknown error";
1504
1856
  const code = err instanceof CliError ? err.exitCode : EXIT_USAGE;
1505
1857
  outputError(ctx, code, message);
1506
- exitWithCode(code, message);
1858
+ exitWithCode(code);
1507
1859
  }
1508
1860
  function findFirstPositional(args) {
1509
1861
  for (let i = 0; i < args.length; i += 1) {
@@ -1515,7 +1867,75 @@ function findFirstPositional(args) {
1515
1867
  function run(argv = process.argv) {
1516
1868
  const args = argv.slice(2);
1517
1869
  const firstPosIdx = findFirstPositional(args);
1518
- const isStatic = firstPosIdx >= 0 && args[firstPosIdx] === "static";
1870
+ const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
1871
+ const isStatic = namespace === "static";
1872
+ const isSites = namespace === "sites";
1873
+ if (isSites) {
1874
+ const sitesArgs = [
1875
+ ...args.slice(0, firstPosIdx),
1876
+ ...args.slice(firstPosIdx + 1)
1877
+ ];
1878
+ const sitesCli = cac("universe sites");
1879
+ sitesCli.command("register <slug>", "Register a new static site (staff only)").option("--json", "Output as JSON").option(
1880
+ "--team <name>",
1881
+ "GitHub team slug (repeatable, or comma-separated). Defaults to staff."
1882
+ ).action(
1883
+ async (slug, flags) => {
1884
+ try {
1885
+ await register({
1886
+ json: flags.json ?? false,
1887
+ slug,
1888
+ team: flags.team
1889
+ });
1890
+ } catch (err) {
1891
+ handleActionError("sites register", flags.json ?? false, err);
1892
+ }
1893
+ }
1894
+ );
1895
+ sitesCli.command("ls", "List sites in the registry").option("--json", "Output as JSON").option(
1896
+ "--mine",
1897
+ "Filter to sites your GitHub identity is authorized for"
1898
+ ).action(async (flags) => {
1899
+ try {
1900
+ await ls2({
1901
+ json: flags.json ?? false,
1902
+ mine: flags.mine ?? false
1903
+ });
1904
+ } catch (err) {
1905
+ handleActionError("sites ls", flags.json ?? false, err);
1906
+ }
1907
+ });
1908
+ sitesCli.command(
1909
+ "update <slug>",
1910
+ "Replace the teams list for an existing site (staff only)"
1911
+ ).option("--json", "Output as JSON").option(
1912
+ "--team <name>",
1913
+ "GitHub team slug (repeatable, or comma-separated). Required."
1914
+ ).action(
1915
+ async (slug, flags) => {
1916
+ try {
1917
+ await update({
1918
+ json: flags.json ?? false,
1919
+ slug,
1920
+ team: flags.team
1921
+ });
1922
+ } catch (err) {
1923
+ handleActionError("sites update", flags.json ?? false, err);
1924
+ }
1925
+ }
1926
+ );
1927
+ sitesCli.command("rm <slug>", "Remove a site from the registry (staff only)").option("--json", "Output as JSON").action(async (slug, flags) => {
1928
+ try {
1929
+ await rm2({ json: flags.json ?? false, slug });
1930
+ } catch (err) {
1931
+ handleActionError("sites rm", flags.json ?? false, err);
1932
+ }
1933
+ });
1934
+ sitesCli.help();
1935
+ sitesCli.version(version);
1936
+ sitesCli.parse(["node", "universe-sites", ...sitesArgs]);
1937
+ return;
1938
+ }
1519
1939
  if (isStatic) {
1520
1940
  const staticArgs = [
1521
1941
  ...args.slice(0, firstPosIdx),
@@ -1598,6 +2018,7 @@ function run(argv = process.argv) {
1598
2018
  }
1599
2019
  });
1600
2020
  cli.command("static <subcommand>", "Static site deployment commands");
2021
+ cli.command("sites <subcommand>", "Static site registry commands");
1601
2022
  cli.help();
1602
2023
  cli.version(version);
1603
2024
  cli.parse(argv);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freecodecamp/universe-cli",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "test": "vitest run",
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "packageManager": "pnpm@10.33.0",
46
46
  "engines": {
47
- "node": ">=22.11.0"
47
+ "node": ">=24.0.0"
48
48
  },
49
49
  "description": "Static site deployment CLI for the freeCodeCamp Universe platform",
50
50
  "homepage": "https://github.com/freeCodeCamp-Universe/universe-cli#readme",