@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.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,26 +22768,16 @@ 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();
22654
- if (git.dirty) {
22780
+ if (git.dirty && !options.json) {
22655
22781
  warn(
22656
22782
  "git working tree is dirty \u2014 uncommitted changes will not be reflected."
22657
22783
  );
@@ -22663,7 +22789,7 @@ async function deploy(options, deps = {}) {
22663
22789
  cwd,
22664
22790
  outputDir
22665
22791
  });
22666
- if (buildResult.skipped) {
22792
+ if (buildResult.skipped && !options.json) {
22667
22793
  info("build.command not set \u2014 using pre-built output.");
22668
22794
  }
22669
22795
  const resolvedOutputDir = buildResult.outputDir;
@@ -22722,7 +22848,7 @@ async function deploy(options, deps = {}) {
22722
22848
  );
22723
22849
  } else {
22724
22850
  const sizeKB = (uploadResult.totalSize / 1024).toFixed(1);
22725
- const nextLine = mode === "preview" ? "Next: universe static promote" : "Promoted to production.";
22851
+ const nextLine = mode === "preview" ? `Next: universe static promote --from ${finalizeResult.deployId}` : "Promoted to production.\nPreview alias unchanged.";
22726
22852
  success2(
22727
22853
  [
22728
22854
  `Deployed ${finalizeResult.deployId}`,
@@ -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);
@@ -23020,7 +23146,10 @@ async function ls(options, deps = {}) {
23020
23146
  getAuthToken: () => identity.token
23021
23147
  });
23022
23148
  const raw = await client.siteDeploys({ site });
23023
- const deploys = raw.map((d2) => parseDeployId(d2.deployId));
23149
+ const sorted = [...raw].sort(
23150
+ (a, b) => b.deployId.localeCompare(a.deployId)
23151
+ );
23152
+ const deploys = sorted.map((d2) => parseDeployId(d2.deployId));
23024
23153
  if (options.json) {
23025
23154
  emitJson4(
23026
23155
  buildEnvelope("ls", true, {
@@ -23043,7 +23172,7 @@ async function ls(options, deps = {}) {
23043
23172
  } else {
23044
23173
  error48(message);
23045
23174
  }
23046
- exit(code, message);
23175
+ exit(code);
23047
23176
  }
23048
23177
  }
23049
23178
 
@@ -23113,15 +23242,17 @@ async function promote(options, deps = {}) {
23113
23242
  })
23114
23243
  );
23115
23244
  } else {
23116
- success2(
23117
- [
23118
- `Promoted ${result.deployId} to production`,
23119
- ``,
23120
- ` Site: ${config2.site}`,
23121
- ` Deploy: ${result.deployId}`,
23122
- ` Production: ${result.url}`
23123
- ].join("\n")
23124
- );
23245
+ const lines = [
23246
+ `Promoted ${result.deployId} to production`,
23247
+ ``,
23248
+ ` Site: ${config2.site}`,
23249
+ ` Deploy: ${result.deployId}`,
23250
+ ` Production: ${result.url}`
23251
+ ];
23252
+ if (options.from) {
23253
+ lines.push(``, "Preview alias unchanged.");
23254
+ }
23255
+ success2(lines.join("\n"));
23125
23256
  }
23126
23257
  } catch (err) {
23127
23258
  const { code, message } = wrapProxyError("promote", err);
@@ -23130,7 +23261,7 @@ async function promote(options, deps = {}) {
23130
23261
  } else {
23131
23262
  error48(message);
23132
23263
  }
23133
- exit(code, message);
23264
+ exit(code);
23134
23265
  }
23135
23266
  }
23136
23267
 
@@ -23217,7 +23348,7 @@ async function rollback(options, deps = {}) {
23217
23348
  } else {
23218
23349
  error48(message);
23219
23350
  }
23220
- exit(code, message);
23351
+ exit(code);
23221
23352
  }
23222
23353
  }
23223
23354
 
@@ -23241,7 +23372,7 @@ async function whoami(options, deps = {}) {
23241
23372
  } else {
23242
23373
  error48(msg);
23243
23374
  }
23244
- exit(EXIT_CREDENTIALS, msg);
23375
+ exit(EXIT_CREDENTIALS);
23245
23376
  return;
23246
23377
  }
23247
23378
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
@@ -23251,21 +23382,22 @@ async function whoami(options, deps = {}) {
23251
23382
  });
23252
23383
  try {
23253
23384
  const result = await client.whoami();
23385
+ const count = result.authorizedSites.length;
23254
23386
  if (options.json) {
23255
23387
  emitJson7(
23256
23388
  buildEnvelope("whoami", true, {
23257
23389
  login: result.login,
23258
- authorizedSites: result.authorizedSites,
23259
- identitySource: identity.source
23390
+ identitySource: identity.source,
23391
+ authorizedSitesCount: count
23260
23392
  })
23261
23393
  );
23262
23394
  } else {
23263
- const sites = result.authorizedSites.length > 0 ? result.authorizedSites.join(", ") : "(no sites authorized)";
23395
+ const sitesLine = count === 0 ? "Authorized for 0 sites." : `Authorized for ${count} site${count === 1 ? "" : "s"} \u2014 run \`universe sites ls --mine\``;
23264
23396
  success2(
23265
23397
  [
23266
23398
  `Logged in as: ${result.login}`,
23267
- `Authorized sites: ${sites}`,
23268
- `Identity source: ${identity.source}`
23399
+ `Identity source: ${identity.source}`,
23400
+ sitesLine
23269
23401
  ].join("\n")
23270
23402
  );
23271
23403
  }
@@ -23277,7 +23409,226 @@ async function whoami(options, deps = {}) {
23277
23409
  } else {
23278
23410
  error48(message);
23279
23411
  }
23280
- exit(exitCode, message);
23412
+ exit(exitCode);
23413
+ }
23414
+ }
23415
+
23416
+ // src/commands/sites/_shared.ts
23417
+ function emitJson8(envelope) {
23418
+ process.stdout.write(JSON.stringify(envelope) + "\n");
23419
+ }
23420
+ function parseTeamsFlag(raw) {
23421
+ if (raw === void 0 || raw === null) return [];
23422
+ const tokens = Array.isArray(raw) ? raw : [raw];
23423
+ return tokens.flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
23424
+ }
23425
+ async function setupClient(deps) {
23426
+ const env = deps.env ?? process.env;
23427
+ const resolveId = deps.resolveIdentity ?? resolveIdentity;
23428
+ const mkClient = deps.createProxyClient ?? createProxyClient;
23429
+ const identity = await resolveId({ env });
23430
+ if (!identity) {
23431
+ throw new CredentialError(
23432
+ "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
23433
+ );
23434
+ }
23435
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
23436
+ const client = mkClient({
23437
+ baseUrl,
23438
+ getAuthToken: () => identity.token
23439
+ });
23440
+ return { client, identitySource: identity.source };
23441
+ }
23442
+
23443
+ // src/commands/sites/ls.ts
23444
+ function formatTable2(rows) {
23445
+ if (rows.length === 0) return "No registered sites.";
23446
+ const headers = ["SLUG", "TEAMS", "CREATED BY", "CREATED AT"];
23447
+ const cells = rows.map((r) => [
23448
+ r.slug,
23449
+ r.teams.join(","),
23450
+ r.createdBy,
23451
+ r.createdAt
23452
+ ]);
23453
+ const widths = headers.map(
23454
+ (h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
23455
+ );
23456
+ const fmt = (row) => row.map((c2, i) => c2.padEnd(widths[i] ?? 0)).join(" ");
23457
+ return [fmt(headers), ...cells.map(fmt)].join("\n");
23458
+ }
23459
+ async function ls2(options, deps = {}) {
23460
+ const command = "sites ls";
23461
+ const success2 = deps.logSuccess ?? ((s) => O2.message(s));
23462
+ const error48 = deps.logError ?? ((s) => O2.error(s));
23463
+ const exit = deps.exit ?? exitWithCode;
23464
+ try {
23465
+ const { client } = await setupClient(deps);
23466
+ let rows = await client.listSites();
23467
+ let scope = "all";
23468
+ if (options.mine) {
23469
+ const me2 = await client.whoami();
23470
+ const allowed = new Set(me2.authorizedSites);
23471
+ rows = rows.filter((r) => allowed.has(r.slug));
23472
+ scope = "mine";
23473
+ }
23474
+ if (options.json) {
23475
+ emitJson8(
23476
+ buildEnvelope(command, true, {
23477
+ count: rows.length,
23478
+ scope,
23479
+ sites: rows
23480
+ })
23481
+ );
23482
+ } else {
23483
+ success2(formatTable2(rows));
23484
+ }
23485
+ } catch (err) {
23486
+ const { code, message } = wrapProxyError(command, err);
23487
+ if (options.json) {
23488
+ emitJson8(buildErrorEnvelope(command, code, message));
23489
+ } else {
23490
+ error48(message);
23491
+ }
23492
+ exit(code);
23493
+ }
23494
+ }
23495
+
23496
+ // src/commands/sites/register.ts
23497
+ async function register(options, deps = {}) {
23498
+ const command = "sites register";
23499
+ const success2 = deps.logSuccess ?? ((s) => O2.success(s));
23500
+ const error48 = deps.logError ?? ((s) => O2.error(s));
23501
+ const exit = deps.exit ?? exitWithCode;
23502
+ try {
23503
+ if (!options.slug || options.slug.trim().length === 0) {
23504
+ throw new UsageError("slug is required (positional argument)");
23505
+ }
23506
+ const teams = parseTeamsFlag(options.team);
23507
+ const { client } = await setupClient(deps);
23508
+ const row = await client.registerSite({
23509
+ slug: options.slug,
23510
+ teams: teams.length > 0 ? teams : void 0
23511
+ });
23512
+ if (options.json) {
23513
+ emitJson8(
23514
+ buildEnvelope(command, true, {
23515
+ slug: row.slug,
23516
+ teams: row.teams,
23517
+ createdAt: row.createdAt,
23518
+ createdBy: row.createdBy
23519
+ })
23520
+ );
23521
+ } else {
23522
+ success2(
23523
+ [
23524
+ `Registered ${row.slug}`,
23525
+ ``,
23526
+ ` Slug: ${row.slug}`,
23527
+ ` Teams: ${row.teams.join(", ")}`,
23528
+ ` Created by: ${row.createdBy}`,
23529
+ ` Created at: ${row.createdAt}`
23530
+ ].join("\n")
23531
+ );
23532
+ }
23533
+ } catch (err) {
23534
+ const { code, message } = wrapProxyError(command, err);
23535
+ if (options.json) {
23536
+ emitJson8(buildErrorEnvelope(command, code, message));
23537
+ } else {
23538
+ error48(message);
23539
+ }
23540
+ exit(code);
23541
+ }
23542
+ }
23543
+
23544
+ // src/commands/sites/rm.ts
23545
+ async function rm2(options, deps = {}) {
23546
+ const command = "sites rm";
23547
+ const success2 = deps.logSuccess ?? ((s) => O2.success(s));
23548
+ const error48 = deps.logError ?? ((s) => O2.error(s));
23549
+ const exit = deps.exit ?? exitWithCode;
23550
+ try {
23551
+ if (!options.slug || options.slug.trim().length === 0) {
23552
+ throw new UsageError("slug is required (positional argument)");
23553
+ }
23554
+ const { client } = await setupClient(deps);
23555
+ await client.deleteSite({ slug: options.slug });
23556
+ if (options.json) {
23557
+ emitJson8(
23558
+ buildEnvelope(command, true, {
23559
+ slug: options.slug,
23560
+ deleted: true
23561
+ })
23562
+ );
23563
+ } else {
23564
+ success2(
23565
+ [
23566
+ `Deleted ${options.slug}`,
23567
+ ``,
23568
+ ` Note: R2 deploy bytes are NOT removed; they age out via the`,
23569
+ ` post-GA cleanup cron.`
23570
+ ].join("\n")
23571
+ );
23572
+ }
23573
+ } catch (err) {
23574
+ const { code, message } = wrapProxyError(command, err);
23575
+ if (options.json) {
23576
+ emitJson8(buildErrorEnvelope(command, code, message));
23577
+ } else {
23578
+ error48(message);
23579
+ }
23580
+ exit(code);
23581
+ }
23582
+ }
23583
+
23584
+ // src/commands/sites/update.ts
23585
+ async function update(options, deps = {}) {
23586
+ const command = "sites update";
23587
+ const success2 = deps.logSuccess ?? ((s) => O2.success(s));
23588
+ const error48 = deps.logError ?? ((s) => O2.error(s));
23589
+ const exit = deps.exit ?? exitWithCode;
23590
+ try {
23591
+ if (!options.slug || options.slug.trim().length === 0) {
23592
+ throw new UsageError("slug is required (positional argument)");
23593
+ }
23594
+ const teams = parseTeamsFlag(options.team);
23595
+ if (teams.length === 0) {
23596
+ throw new UsageError(
23597
+ "--team is required with at least one slug; use `sites rm` to remove a site"
23598
+ );
23599
+ }
23600
+ const { client } = await setupClient(deps);
23601
+ const row = await client.updateSite({
23602
+ slug: options.slug,
23603
+ teams
23604
+ });
23605
+ if (options.json) {
23606
+ emitJson8(
23607
+ buildEnvelope(command, true, {
23608
+ slug: row.slug,
23609
+ teams: row.teams,
23610
+ updatedAt: row.updatedAt
23611
+ })
23612
+ );
23613
+ } else {
23614
+ success2(
23615
+ [
23616
+ `Updated ${row.slug}`,
23617
+ ``,
23618
+ ` Slug: ${row.slug}`,
23619
+ ` Teams: ${row.teams.join(", ")}`,
23620
+ ` Updated at: ${row.updatedAt}`
23621
+ ].join("\n")
23622
+ );
23623
+ }
23624
+ } catch (err) {
23625
+ const { code, message } = wrapProxyError(command, err);
23626
+ if (options.json) {
23627
+ emitJson8(buildErrorEnvelope(command, code, message));
23628
+ } else {
23629
+ error48(message);
23630
+ }
23631
+ exit(code);
23281
23632
  }
23282
23633
  }
23283
23634
 
@@ -23343,13 +23694,13 @@ function outputError(ctx, code, message, issues) {
23343
23694
  }
23344
23695
 
23345
23696
  // src/cli.ts
23346
- var version2 = true ? "0.4.0" : "0.0.0";
23697
+ var version2 = true ? "0.5.1" : "0.0.0";
23347
23698
  function handleActionError(command, json2, err) {
23348
23699
  const ctx = { json: json2, command };
23349
23700
  const message = err instanceof Error ? err.message : "unknown error";
23350
23701
  const code = err instanceof CliError ? err.exitCode : EXIT_USAGE;
23351
23702
  outputError(ctx, code, message);
23352
- exitWithCode(code, message);
23703
+ exitWithCode(code);
23353
23704
  }
23354
23705
  function findFirstPositional(args) {
23355
23706
  for (let i = 0; i < args.length; i += 1) {
@@ -23361,7 +23712,75 @@ function findFirstPositional(args) {
23361
23712
  function run(argv = process.argv) {
23362
23713
  const args = argv.slice(2);
23363
23714
  const firstPosIdx = findFirstPositional(args);
23364
- const isStatic = firstPosIdx >= 0 && args[firstPosIdx] === "static";
23715
+ const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
23716
+ const isStatic = namespace === "static";
23717
+ const isSites = namespace === "sites";
23718
+ if (isSites) {
23719
+ const sitesArgs = [
23720
+ ...args.slice(0, firstPosIdx),
23721
+ ...args.slice(firstPosIdx + 1)
23722
+ ];
23723
+ const sitesCli = cac("universe sites");
23724
+ sitesCli.command("register <slug>", "Register a new static site (staff only)").option("--json", "Output as JSON").option(
23725
+ "--team <name>",
23726
+ "GitHub team slug (repeatable, or comma-separated). Defaults to staff."
23727
+ ).action(
23728
+ async (slug, flags) => {
23729
+ try {
23730
+ await register({
23731
+ json: flags.json ?? false,
23732
+ slug,
23733
+ team: flags.team
23734
+ });
23735
+ } catch (err) {
23736
+ handleActionError("sites register", flags.json ?? false, err);
23737
+ }
23738
+ }
23739
+ );
23740
+ sitesCli.command("ls", "List sites in the registry").option("--json", "Output as JSON").option(
23741
+ "--mine",
23742
+ "Filter to sites your GitHub identity is authorized for"
23743
+ ).action(async (flags) => {
23744
+ try {
23745
+ await ls2({
23746
+ json: flags.json ?? false,
23747
+ mine: flags.mine ?? false
23748
+ });
23749
+ } catch (err) {
23750
+ handleActionError("sites ls", flags.json ?? false, err);
23751
+ }
23752
+ });
23753
+ sitesCli.command(
23754
+ "update <slug>",
23755
+ "Replace the teams list for an existing site (staff only)"
23756
+ ).option("--json", "Output as JSON").option(
23757
+ "--team <name>",
23758
+ "GitHub team slug (repeatable, or comma-separated). Required."
23759
+ ).action(
23760
+ async (slug, flags) => {
23761
+ try {
23762
+ await update({
23763
+ json: flags.json ?? false,
23764
+ slug,
23765
+ team: flags.team
23766
+ });
23767
+ } catch (err) {
23768
+ handleActionError("sites update", flags.json ?? false, err);
23769
+ }
23770
+ }
23771
+ );
23772
+ sitesCli.command("rm <slug>", "Remove a site from the registry (staff only)").option("--json", "Output as JSON").action(async (slug, flags) => {
23773
+ try {
23774
+ await rm2({ json: flags.json ?? false, slug });
23775
+ } catch (err) {
23776
+ handleActionError("sites rm", flags.json ?? false, err);
23777
+ }
23778
+ });
23779
+ sitesCli.help();
23780
+ sitesCli.version(version2);
23781
+ sitesCli.parse(["node", "universe-sites", ...sitesArgs]);
23782
+ return;
23783
+ }
23365
23784
  if (isStatic) {
23366
23785
  const staticArgs = [
23367
23786
  ...args.slice(0, firstPosIdx),
@@ -23444,6 +23863,7 @@ function run(argv = process.argv) {
23444
23863
  }
23445
23864
  });
23446
23865
  cli.command("static <subcommand>", "Static site deployment commands");
23866
+ cli.command("sites <subcommand>", "Static site registry commands");
23447
23867
  cli.help();
23448
23868
  cli.version(version2);
23449
23869
  cli.parse(argv);