@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.cjs CHANGED
@@ -8106,10 +8106,7 @@ var EXIT_STORAGE = 13;
8106
8106
  var EXIT_GIT = 15;
8107
8107
  var EXIT_CONFIRM = 18;
8108
8108
  var EXIT_PARTIAL = 19;
8109
- function exitWithCode(code, message) {
8110
- if (message !== void 0) {
8111
- process.stderr.write(message + "\n");
8112
- }
8109
+ function exitWithCode(code) {
8113
8110
  process.exit(code);
8114
8111
  }
8115
8112
 
@@ -22241,6 +22238,53 @@ See docs/platform-yaml.md.`
22241
22238
  return { ok: true, value: result.data };
22242
22239
  }
22243
22240
 
22241
+ // src/lib/similarity.ts
22242
+ function editDistance(a, b) {
22243
+ const m = a.length;
22244
+ const n = b.length;
22245
+ if (m === 0) return n;
22246
+ if (n === 0) return m;
22247
+ const d2 = Array.from(
22248
+ { length: m + 1 },
22249
+ () => new Array(n + 1).fill(0)
22250
+ );
22251
+ for (let i = 0; i <= m; i++) d2[i][0] = i;
22252
+ for (let j = 0; j <= n; j++) d2[0][j] = j;
22253
+ for (let i = 1; i <= m; i++) {
22254
+ for (let j = 1; j <= n; j++) {
22255
+ const cost = a[i - 1] === b[j - 1] ? 0 : 1;
22256
+ d2[i][j] = Math.min(
22257
+ d2[i - 1][j] + 1,
22258
+ d2[i][j - 1] + 1,
22259
+ d2[i - 1][j - 1] + cost
22260
+ );
22261
+ if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
22262
+ d2[i][j] = Math.min(d2[i][j], d2[i - 2][j - 2] + 1);
22263
+ }
22264
+ }
22265
+ }
22266
+ return d2[m][n];
22267
+ }
22268
+ function suggest(target, candidates, threshold = 2) {
22269
+ if (candidates.length === 0) return null;
22270
+ const lc = target.toLowerCase();
22271
+ const sub = candidates.find((c2) => {
22272
+ const clc = c2.toLowerCase();
22273
+ return clc.includes(lc) || lc.includes(clc);
22274
+ });
22275
+ if (sub) return sub;
22276
+ let best = null;
22277
+ let bestD = threshold + 1;
22278
+ for (const c2 of candidates) {
22279
+ const d2 = editDistance(lc, c2.toLowerCase());
22280
+ if (d2 < bestD) {
22281
+ bestD = d2;
22282
+ best = c2;
22283
+ }
22284
+ }
22285
+ return bestD <= threshold ? best : null;
22286
+ }
22287
+
22244
22288
  // src/lib/proxy-client.ts
22245
22289
  var ProxyError = class extends CliError {
22246
22290
  exitCode;
@@ -22403,6 +22447,52 @@ function createProxyClient(cfg) {
22403
22447
  },
22404
22448
  body: JSON.stringify({ to: req.to })
22405
22449
  });
22450
+ },
22451
+ async registerSite(req) {
22452
+ const body = { slug: req.slug };
22453
+ if (req.teams && req.teams.length > 0) {
22454
+ body.teams = req.teams;
22455
+ }
22456
+ return call(`${base}/api/site/register`, {
22457
+ method: "POST",
22458
+ headers: {
22459
+ Authorization: await userBearer(),
22460
+ Accept: "application/json",
22461
+ "Content-Type": "application/json"
22462
+ },
22463
+ body: JSON.stringify(body)
22464
+ });
22465
+ },
22466
+ async listSites() {
22467
+ return call(`${base}/api/sites`, {
22468
+ method: "GET",
22469
+ headers: {
22470
+ Authorization: await userBearer(),
22471
+ Accept: "application/json"
22472
+ }
22473
+ });
22474
+ },
22475
+ async updateSite(req) {
22476
+ const url2 = `${base}/api/site/${encodeURIComponent(req.slug)}`;
22477
+ return call(url2, {
22478
+ method: "PATCH",
22479
+ headers: {
22480
+ Authorization: await userBearer(),
22481
+ Accept: "application/json",
22482
+ "Content-Type": "application/json"
22483
+ },
22484
+ body: JSON.stringify({ teams: req.teams })
22485
+ });
22486
+ },
22487
+ async deleteSite(req) {
22488
+ const url2 = `${base}/api/site/${encodeURIComponent(req.slug)}`;
22489
+ return call(url2, {
22490
+ method: "DELETE",
22491
+ headers: {
22492
+ Authorization: await userBearer(),
22493
+ Accept: "application/json"
22494
+ }
22495
+ });
22406
22496
  }
22407
22497
  };
22408
22498
  }
@@ -22597,6 +22687,52 @@ function rethrowProxy(prefix, err) {
22597
22687
  if (err instanceof Error) throw new StorageError(`${prefix}: ${err.message}`);
22598
22688
  throw new StorageError(`${prefix}: ${String(err)}`);
22599
22689
  }
22690
+ var PREFLIGHT_INLINE_LIST_CAP = 10;
22691
+ function formatUnauthorizedSiteError(a) {
22692
+ const lines = [
22693
+ `Site '${a.attempted}' is not registered for your GitHub identity.`,
22694
+ ``,
22695
+ ` You are: ${a.login}`,
22696
+ ``
22697
+ ];
22698
+ if (a.authorized.length === 0) {
22699
+ lines.push(
22700
+ ` Your identity is authorized for no sites yet.`,
22701
+ ``,
22702
+ ` Likely causes:`,
22703
+ ` 1. The '${a.attempted}' slug is not registered.`,
22704
+ ` Admin (staff): universe sites register ${a.attempted} --team <team>`,
22705
+ ` 2. You are not in any team listed on any registered site.`,
22706
+ ` Admin (staff): universe sites update <slug> --team +<your-team>`
22707
+ );
22708
+ return lines.join("\n");
22709
+ }
22710
+ const hint = suggest(a.attempted, a.authorized);
22711
+ if (hint) {
22712
+ lines.push(` Did you mean: ${hint}?`, ``);
22713
+ }
22714
+ lines.push(
22715
+ ` Likely causes (most common first):`,
22716
+ ` 1. Typo in platform.yaml \`site:\` \u2014 check the spelling above.`,
22717
+ ` 2. The '${a.attempted}' slug is not registered yet.`,
22718
+ ` Admin (staff): universe sites register ${a.attempted} --team <team>`,
22719
+ ` 3. You are not in any team authorized for '${a.attempted}'.`,
22720
+ ` Admin (staff): universe sites update ${a.attempted} --team +<your-team>`,
22721
+ ``
22722
+ );
22723
+ if (a.authorized.length <= PREFLIGHT_INLINE_LIST_CAP) {
22724
+ lines.push(
22725
+ ` Your authorized sites (${a.authorized.length}):`,
22726
+ ...[...a.authorized].sort().map((s) => ` - ${s}`)
22727
+ );
22728
+ } else {
22729
+ lines.push(
22730
+ ` You have ${a.authorized.length} authorized sites \u2014 too many to inline.`,
22731
+ ` Run \`universe sites ls --mine\` to inspect the full list.`
22732
+ );
22733
+ }
22734
+ return lines.join("\n");
22735
+ }
22600
22736
  async function deploy(options, deps = {}) {
22601
22737
  const cwd = deps.cwd ?? process.cwd();
22602
22738
  const env = deps.env ?? process.env;
@@ -22632,22 +22768,12 @@ async function deploy(options, deps = {}) {
22632
22768
  rethrowProxy("whoami preflight failed", err);
22633
22769
  }
22634
22770
  if (!me2.authorizedSites.includes(config2.site)) {
22635
- const sitesLine = me2.authorizedSites.length > 0 ? me2.authorizedSites.join(", ") : "(no sites authorized)";
22636
22771
  throw new CredentialError(
22637
- [
22638
- `Site '${config2.site}' is not registered for your GitHub identity.`,
22639
- ``,
22640
- ` You are: ${me2.login}`,
22641
- ` Authorized sites: ${sitesLine}`,
22642
- ``,
22643
- `Likely causes (most common first):`,
22644
- ` 1. Platform admin has not added '${config2.site}' to artemis`,
22645
- ` 'config/sites.yaml' yet (one-time, per site).`,
22646
- ` 2. You are not in any GitHub team listed for '${config2.site}'.`,
22647
- ``,
22648
- `Runbook:`,
22649
- ` https://github.com/freeCodeCamp/infra/blob/main/docs/runbooks/01-deploy-new-constellation-site.md`
22650
- ].join("\n")
22772
+ formatUnauthorizedSiteError({
22773
+ attempted: config2.site,
22774
+ login: me2.login,
22775
+ authorized: me2.authorizedSites
22776
+ })
22651
22777
  );
22652
22778
  }
22653
22779
  const git = gitState();
@@ -22758,7 +22884,7 @@ async function deploy(options, deps = {}) {
22758
22884
  } else {
22759
22885
  error48(message);
22760
22886
  }
22761
- exit(code, message);
22887
+ exit(code);
22762
22888
  }
22763
22889
  }
22764
22890
 
@@ -22866,7 +22992,7 @@ async function login(options, deps = {}) {
22866
22992
  } else {
22867
22993
  error48(msg);
22868
22994
  }
22869
- exit(EXIT_CONFIRM, msg);
22995
+ exit(EXIT_CONFIRM);
22870
22996
  return;
22871
22997
  }
22872
22998
  }
@@ -22909,7 +23035,7 @@ async function login(options, deps = {}) {
22909
23035
  } else {
22910
23036
  error48(message);
22911
23037
  }
22912
- exit(EXIT_CREDENTIALS, message);
23038
+ exit(EXIT_CREDENTIALS);
22913
23039
  return;
22914
23040
  }
22915
23041
  await save(token);
@@ -23043,7 +23169,7 @@ async function ls(options, deps = {}) {
23043
23169
  } else {
23044
23170
  error48(message);
23045
23171
  }
23046
- exit(code, message);
23172
+ exit(code);
23047
23173
  }
23048
23174
  }
23049
23175
 
@@ -23130,7 +23256,7 @@ async function promote(options, deps = {}) {
23130
23256
  } else {
23131
23257
  error48(message);
23132
23258
  }
23133
- exit(code, message);
23259
+ exit(code);
23134
23260
  }
23135
23261
  }
23136
23262
 
@@ -23217,7 +23343,7 @@ async function rollback(options, deps = {}) {
23217
23343
  } else {
23218
23344
  error48(message);
23219
23345
  }
23220
- exit(code, message);
23346
+ exit(code);
23221
23347
  }
23222
23348
  }
23223
23349
 
@@ -23241,7 +23367,7 @@ async function whoami(options, deps = {}) {
23241
23367
  } else {
23242
23368
  error48(msg);
23243
23369
  }
23244
- exit(EXIT_CREDENTIALS, msg);
23370
+ exit(EXIT_CREDENTIALS);
23245
23371
  return;
23246
23372
  }
23247
23373
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
@@ -23251,21 +23377,22 @@ async function whoami(options, deps = {}) {
23251
23377
  });
23252
23378
  try {
23253
23379
  const result = await client.whoami();
23380
+ const count = result.authorizedSites.length;
23254
23381
  if (options.json) {
23255
23382
  emitJson7(
23256
23383
  buildEnvelope("whoami", true, {
23257
23384
  login: result.login,
23258
- authorizedSites: result.authorizedSites,
23259
- identitySource: identity.source
23385
+ identitySource: identity.source,
23386
+ authorizedSitesCount: count
23260
23387
  })
23261
23388
  );
23262
23389
  } else {
23263
- const sites = result.authorizedSites.length > 0 ? result.authorizedSites.join(", ") : "(no sites authorized)";
23390
+ const sitesLine = count === 0 ? "Authorized for 0 sites." : `Authorized for ${count} site${count === 1 ? "" : "s"} \u2014 run \`universe sites ls --mine\``;
23264
23391
  success2(
23265
23392
  [
23266
23393
  `Logged in as: ${result.login}`,
23267
- `Authorized sites: ${sites}`,
23268
- `Identity source: ${identity.source}`
23394
+ `Identity source: ${identity.source}`,
23395
+ sitesLine
23269
23396
  ].join("\n")
23270
23397
  );
23271
23398
  }
@@ -23277,7 +23404,226 @@ async function whoami(options, deps = {}) {
23277
23404
  } else {
23278
23405
  error48(message);
23279
23406
  }
23280
- exit(exitCode, message);
23407
+ exit(exitCode);
23408
+ }
23409
+ }
23410
+
23411
+ // src/commands/sites/_shared.ts
23412
+ function emitJson8(envelope) {
23413
+ process.stdout.write(JSON.stringify(envelope) + "\n");
23414
+ }
23415
+ function parseTeamsFlag(raw) {
23416
+ if (raw === void 0 || raw === null) return [];
23417
+ const tokens = Array.isArray(raw) ? raw : [raw];
23418
+ return tokens.flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
23419
+ }
23420
+ async function setupClient(deps) {
23421
+ const env = deps.env ?? process.env;
23422
+ const resolveId = deps.resolveIdentity ?? resolveIdentity;
23423
+ const mkClient = deps.createProxyClient ?? createProxyClient;
23424
+ const identity = await resolveId({ env });
23425
+ if (!identity) {
23426
+ throw new CredentialError(
23427
+ "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
23428
+ );
23429
+ }
23430
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
23431
+ const client = mkClient({
23432
+ baseUrl,
23433
+ getAuthToken: () => identity.token
23434
+ });
23435
+ return { client, identitySource: identity.source };
23436
+ }
23437
+
23438
+ // src/commands/sites/ls.ts
23439
+ function formatTable2(rows) {
23440
+ if (rows.length === 0) return "No registered sites.";
23441
+ const headers = ["SLUG", "TEAMS", "CREATED BY", "CREATED AT"];
23442
+ const cells = rows.map((r) => [
23443
+ r.slug,
23444
+ r.teams.join(","),
23445
+ r.createdBy,
23446
+ r.createdAt
23447
+ ]);
23448
+ const widths = headers.map(
23449
+ (h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
23450
+ );
23451
+ const fmt = (row) => row.map((c2, i) => c2.padEnd(widths[i] ?? 0)).join(" ");
23452
+ return [fmt(headers), ...cells.map(fmt)].join("\n");
23453
+ }
23454
+ async function ls2(options, deps = {}) {
23455
+ const command = "sites ls";
23456
+ const success2 = deps.logSuccess ?? ((s) => O2.message(s));
23457
+ const error48 = deps.logError ?? ((s) => O2.error(s));
23458
+ const exit = deps.exit ?? exitWithCode;
23459
+ try {
23460
+ const { client } = await setupClient(deps);
23461
+ let rows = await client.listSites();
23462
+ let scope = "all";
23463
+ if (options.mine) {
23464
+ const me2 = await client.whoami();
23465
+ const allowed = new Set(me2.authorizedSites);
23466
+ rows = rows.filter((r) => allowed.has(r.slug));
23467
+ scope = "mine";
23468
+ }
23469
+ if (options.json) {
23470
+ emitJson8(
23471
+ buildEnvelope(command, true, {
23472
+ count: rows.length,
23473
+ scope,
23474
+ sites: rows
23475
+ })
23476
+ );
23477
+ } else {
23478
+ success2(formatTable2(rows));
23479
+ }
23480
+ } catch (err) {
23481
+ const { code, message } = wrapProxyError(command, err);
23482
+ if (options.json) {
23483
+ emitJson8(buildErrorEnvelope(command, code, message));
23484
+ } else {
23485
+ error48(message);
23486
+ }
23487
+ exit(code);
23488
+ }
23489
+ }
23490
+
23491
+ // src/commands/sites/register.ts
23492
+ async function register(options, deps = {}) {
23493
+ const command = "sites register";
23494
+ const success2 = deps.logSuccess ?? ((s) => O2.success(s));
23495
+ const error48 = deps.logError ?? ((s) => O2.error(s));
23496
+ const exit = deps.exit ?? exitWithCode;
23497
+ try {
23498
+ if (!options.slug || options.slug.trim().length === 0) {
23499
+ throw new UsageError("slug is required (positional argument)");
23500
+ }
23501
+ const teams = parseTeamsFlag(options.team);
23502
+ const { client } = await setupClient(deps);
23503
+ const row = await client.registerSite({
23504
+ slug: options.slug,
23505
+ teams: teams.length > 0 ? teams : void 0
23506
+ });
23507
+ if (options.json) {
23508
+ emitJson8(
23509
+ buildEnvelope(command, true, {
23510
+ slug: row.slug,
23511
+ teams: row.teams,
23512
+ createdAt: row.createdAt,
23513
+ createdBy: row.createdBy
23514
+ })
23515
+ );
23516
+ } else {
23517
+ success2(
23518
+ [
23519
+ `Registered ${row.slug}`,
23520
+ ``,
23521
+ ` Slug: ${row.slug}`,
23522
+ ` Teams: ${row.teams.join(", ")}`,
23523
+ ` Created by: ${row.createdBy}`,
23524
+ ` Created at: ${row.createdAt}`
23525
+ ].join("\n")
23526
+ );
23527
+ }
23528
+ } catch (err) {
23529
+ const { code, message } = wrapProxyError(command, err);
23530
+ if (options.json) {
23531
+ emitJson8(buildErrorEnvelope(command, code, message));
23532
+ } else {
23533
+ error48(message);
23534
+ }
23535
+ exit(code);
23536
+ }
23537
+ }
23538
+
23539
+ // src/commands/sites/rm.ts
23540
+ async function rm2(options, deps = {}) {
23541
+ const command = "sites rm";
23542
+ const success2 = deps.logSuccess ?? ((s) => O2.success(s));
23543
+ const error48 = deps.logError ?? ((s) => O2.error(s));
23544
+ const exit = deps.exit ?? exitWithCode;
23545
+ try {
23546
+ if (!options.slug || options.slug.trim().length === 0) {
23547
+ throw new UsageError("slug is required (positional argument)");
23548
+ }
23549
+ const { client } = await setupClient(deps);
23550
+ await client.deleteSite({ slug: options.slug });
23551
+ if (options.json) {
23552
+ emitJson8(
23553
+ buildEnvelope(command, true, {
23554
+ slug: options.slug,
23555
+ deleted: true
23556
+ })
23557
+ );
23558
+ } else {
23559
+ success2(
23560
+ [
23561
+ `Deleted ${options.slug}`,
23562
+ ``,
23563
+ ` Note: R2 deploy bytes are NOT removed; they age out via the`,
23564
+ ` post-GA cleanup cron.`
23565
+ ].join("\n")
23566
+ );
23567
+ }
23568
+ } catch (err) {
23569
+ const { code, message } = wrapProxyError(command, err);
23570
+ if (options.json) {
23571
+ emitJson8(buildErrorEnvelope(command, code, message));
23572
+ } else {
23573
+ error48(message);
23574
+ }
23575
+ exit(code);
23576
+ }
23577
+ }
23578
+
23579
+ // src/commands/sites/update.ts
23580
+ async function update(options, deps = {}) {
23581
+ const command = "sites update";
23582
+ const success2 = deps.logSuccess ?? ((s) => O2.success(s));
23583
+ const error48 = deps.logError ?? ((s) => O2.error(s));
23584
+ const exit = deps.exit ?? exitWithCode;
23585
+ try {
23586
+ if (!options.slug || options.slug.trim().length === 0) {
23587
+ throw new UsageError("slug is required (positional argument)");
23588
+ }
23589
+ const teams = parseTeamsFlag(options.team);
23590
+ if (teams.length === 0) {
23591
+ throw new UsageError(
23592
+ "--team is required with at least one slug; use `sites rm` to remove a site"
23593
+ );
23594
+ }
23595
+ const { client } = await setupClient(deps);
23596
+ const row = await client.updateSite({
23597
+ slug: options.slug,
23598
+ teams
23599
+ });
23600
+ if (options.json) {
23601
+ emitJson8(
23602
+ buildEnvelope(command, true, {
23603
+ slug: row.slug,
23604
+ teams: row.teams,
23605
+ updatedAt: row.updatedAt
23606
+ })
23607
+ );
23608
+ } else {
23609
+ success2(
23610
+ [
23611
+ `Updated ${row.slug}`,
23612
+ ``,
23613
+ ` Slug: ${row.slug}`,
23614
+ ` Teams: ${row.teams.join(", ")}`,
23615
+ ` Updated at: ${row.updatedAt}`
23616
+ ].join("\n")
23617
+ );
23618
+ }
23619
+ } catch (err) {
23620
+ const { code, message } = wrapProxyError(command, err);
23621
+ if (options.json) {
23622
+ emitJson8(buildErrorEnvelope(command, code, message));
23623
+ } else {
23624
+ error48(message);
23625
+ }
23626
+ exit(code);
23281
23627
  }
23282
23628
  }
23283
23629
 
@@ -23343,13 +23689,13 @@ function outputError(ctx, code, message, issues) {
23343
23689
  }
23344
23690
 
23345
23691
  // src/cli.ts
23346
- var version2 = true ? "0.4.0" : "0.0.0";
23692
+ var version2 = true ? "0.5.0" : "0.0.0";
23347
23693
  function handleActionError(command, json2, err) {
23348
23694
  const ctx = { json: json2, command };
23349
23695
  const message = err instanceof Error ? err.message : "unknown error";
23350
23696
  const code = err instanceof CliError ? err.exitCode : EXIT_USAGE;
23351
23697
  outputError(ctx, code, message);
23352
- exitWithCode(code, message);
23698
+ exitWithCode(code);
23353
23699
  }
23354
23700
  function findFirstPositional(args) {
23355
23701
  for (let i = 0; i < args.length; i += 1) {
@@ -23361,7 +23707,75 @@ function findFirstPositional(args) {
23361
23707
  function run(argv = process.argv) {
23362
23708
  const args = argv.slice(2);
23363
23709
  const firstPosIdx = findFirstPositional(args);
23364
- const isStatic = firstPosIdx >= 0 && args[firstPosIdx] === "static";
23710
+ const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
23711
+ const isStatic = namespace === "static";
23712
+ const isSites = namespace === "sites";
23713
+ if (isSites) {
23714
+ const sitesArgs = [
23715
+ ...args.slice(0, firstPosIdx),
23716
+ ...args.slice(firstPosIdx + 1)
23717
+ ];
23718
+ const sitesCli = cac("universe sites");
23719
+ sitesCli.command("register <slug>", "Register a new static site (staff only)").option("--json", "Output as JSON").option(
23720
+ "--team <name>",
23721
+ "GitHub team slug (repeatable, or comma-separated). Defaults to staff."
23722
+ ).action(
23723
+ async (slug, flags) => {
23724
+ try {
23725
+ await register({
23726
+ json: flags.json ?? false,
23727
+ slug,
23728
+ team: flags.team
23729
+ });
23730
+ } catch (err) {
23731
+ handleActionError("sites register", flags.json ?? false, err);
23732
+ }
23733
+ }
23734
+ );
23735
+ sitesCli.command("ls", "List sites in the registry").option("--json", "Output as JSON").option(
23736
+ "--mine",
23737
+ "Filter to sites your GitHub identity is authorized for"
23738
+ ).action(async (flags) => {
23739
+ try {
23740
+ await ls2({
23741
+ json: flags.json ?? false,
23742
+ mine: flags.mine ?? false
23743
+ });
23744
+ } catch (err) {
23745
+ handleActionError("sites ls", flags.json ?? false, err);
23746
+ }
23747
+ });
23748
+ sitesCli.command(
23749
+ "update <slug>",
23750
+ "Replace the teams list for an existing site (staff only)"
23751
+ ).option("--json", "Output as JSON").option(
23752
+ "--team <name>",
23753
+ "GitHub team slug (repeatable, or comma-separated). Required."
23754
+ ).action(
23755
+ async (slug, flags) => {
23756
+ try {
23757
+ await update({
23758
+ json: flags.json ?? false,
23759
+ slug,
23760
+ team: flags.team
23761
+ });
23762
+ } catch (err) {
23763
+ handleActionError("sites update", flags.json ?? false, err);
23764
+ }
23765
+ }
23766
+ );
23767
+ sitesCli.command("rm <slug>", "Remove a site from the registry (staff only)").option("--json", "Output as JSON").action(async (slug, flags) => {
23768
+ try {
23769
+ await rm2({ json: flags.json ?? false, slug });
23770
+ } catch (err) {
23771
+ handleActionError("sites rm", flags.json ?? false, err);
23772
+ }
23773
+ });
23774
+ sitesCli.help();
23775
+ sitesCli.version(version2);
23776
+ sitesCli.parse(["node", "universe-sites", ...sitesArgs]);
23777
+ return;
23778
+ }
23365
23779
  if (isStatic) {
23366
23780
  const staticArgs = [
23367
23781
  ...args.slice(0, firstPosIdx),
@@ -23444,6 +23858,7 @@ function run(argv = process.argv) {
23444
23858
  }
23445
23859
  });
23446
23860
  cli.command("static <subcommand>", "Static site deployment commands");
23861
+ cli.command("sites <subcommand>", "Static site registry commands");
23447
23862
  cli.help();
23448
23863
  cli.version(version2);
23449
23864
  cli.parse(argv);