@freecodecamp/universe-cli 0.7.2 → 0.8.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
@@ -39,6 +39,9 @@ var StorageError = class extends CliError {
39
39
  var GitError = class extends CliError {
40
40
  exitCode = EXIT_GIT;
41
41
  };
42
+ var ConfirmError = class extends CliError {
43
+ exitCode = EXIT_CONFIRM;
44
+ };
42
45
  var PartialUploadError = class extends CliError {
43
46
  exitCode = EXIT_PARTIAL;
44
47
  };
@@ -51,8 +54,8 @@ import { execSync } from "child_process";
51
54
  function getGitState() {
52
55
  try {
53
56
  const hash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
54
- const status = execSync("git status --porcelain", { encoding: "utf-8" });
55
- return { hash, dirty: status.length > 0 };
57
+ const status2 = execSync("git status --porcelain", { encoding: "utf-8" });
58
+ return { hash, dirty: status2.length > 0 };
56
59
  } catch {
57
60
  return { hash: null, dirty: false, error: "not a git repository" };
58
61
  }
@@ -101,13 +104,13 @@ import { spawn } from "child_process";
101
104
  import { stat } from "fs/promises";
102
105
  import { isAbsolute, resolve } from "path";
103
106
  var defaultExec = async (req) => {
104
- return new Promise((resolveExit, reject) => {
107
+ return new Promise((resolveExit, reject2) => {
105
108
  const child = spawn(req.command, {
106
109
  cwd: req.cwd,
107
110
  shell: true,
108
111
  stdio: "inherit"
109
112
  });
110
- child.on("error", (err) => reject(err));
113
+ child.on("error", (err) => reject2(err));
111
114
  child.on("exit", (code) => resolveExit(code ?? 1));
112
115
  });
113
116
  };
@@ -428,6 +431,52 @@ function suggest(target, candidates, threshold = 2) {
428
431
  return bestD <= threshold ? best : null;
429
432
  }
430
433
 
434
+ // src/commands/repo/schema.ts
435
+ import { z as z2 } from "zod";
436
+ var REPO_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/;
437
+ var repoName = z2.string().regex(
438
+ REPO_NAME_RE,
439
+ "must start with a letter or digit, then letters, digits, '.', '_' or '-' (max 100 chars)"
440
+ );
441
+ var visibilitySchema = z2.enum(["public", "private"]);
442
+ var repoStatusSchema = z2.enum([
443
+ "pending",
444
+ "approved",
445
+ "active",
446
+ "rejected",
447
+ "failed"
448
+ ]);
449
+ var createRepoRequestSchema = z2.object({
450
+ name: repoName,
451
+ visibility: visibilitySchema.default("private"),
452
+ description: z2.string().max(350).optional(),
453
+ template: repoName.optional()
454
+ }).strict();
455
+ var repoRowSchema = z2.object({
456
+ id: z2.string(),
457
+ name: z2.string(),
458
+ owner: z2.string(),
459
+ visibility: visibilitySchema,
460
+ description: z2.string().optional(),
461
+ template: z2.string().optional(),
462
+ status: repoStatusSchema,
463
+ url: z2.string().optional(),
464
+ error: z2.string().optional(),
465
+ requestedBy: z2.string(),
466
+ approver: z2.string().optional(),
467
+ rejectReason: z2.string().optional(),
468
+ createdAt: z2.string(),
469
+ updatedAt: z2.string()
470
+ });
471
+ var repoRowArraySchema = z2.array(repoRowSchema);
472
+ var repoApproveResultSchema = z2.object({
473
+ outcome: z2.enum(["ok", "approved_failed"]),
474
+ request: repoRowSchema
475
+ });
476
+ var repoTemplatesResponseSchema = z2.object({
477
+ templates: z2.array(z2.string())
478
+ });
479
+
431
480
  // src/lib/proxy-client.ts
432
481
  var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
433
482
  function parseFetchTimeoutMs(env) {
@@ -441,11 +490,13 @@ var ProxyError = class extends CliError {
441
490
  exitCode;
442
491
  status;
443
492
  code;
444
- constructor(status, code, message) {
493
+ requestId;
494
+ constructor(status2, code, message, requestId) {
445
495
  super(message);
446
- this.status = status;
496
+ this.status = status2;
447
497
  this.code = code;
448
- this.exitCode = mapExitCode(status);
498
+ this.exitCode = mapExitCode(status2);
499
+ this.requestId = requestId;
449
500
  }
450
501
  };
451
502
  var AliasDriftError = class extends ProxyError {
@@ -455,16 +506,22 @@ var AliasDriftError = class extends ProxyError {
455
506
  this.current = current;
456
507
  }
457
508
  };
458
- function mapExitCode(status) {
459
- if (status === 401 || status === 403) return EXIT_CREDENTIALS;
460
- if (status === 422 || status === 0 || status >= 500) return EXIT_STORAGE;
509
+ function mapExitCode(status2) {
510
+ if (status2 === 401 || status2 === 403) return EXIT_CREDENTIALS;
511
+ if (status2 === 422 || status2 === 0 || status2 >= 500) return EXIT_STORAGE;
461
512
  return EXIT_USAGE;
462
513
  }
463
514
  function wrapProxyError(command, err) {
464
515
  if (err instanceof ProxyError) {
516
+ let message = `${command} failed (${err.code}): ${err.message}`;
517
+ if (err.code === "user_unauthorized") {
518
+ message += "\n hint: the active GitHub token may lack the read:org scope or SSO authorization for the org. $GITHUB_TOKEN / $GH_TOKEN override `gh auth token` \u2014 run `universe whoami` to check the active identity source, then unset them or re-authorize the token (Configure SSO).";
519
+ }
465
520
  return {
466
521
  code: err.exitCode,
467
- message: `${command} failed (${err.code}): ${err.message}`
522
+ message,
523
+ kind: err.code,
524
+ requestId: err.requestId
468
525
  };
469
526
  }
470
527
  if (err instanceof CliError) {
@@ -479,34 +536,34 @@ function isProxyErrorEnvelope(value) {
479
536
  return typeof value === "object" && value !== null && !Array.isArray(value) && "error" in value;
480
537
  }
481
538
  async function readErrorEnvelope(response) {
482
- const status = response.status;
539
+ const status2 = response.status;
483
540
  let raw;
484
541
  try {
485
542
  raw = await response.json();
486
543
  } catch {
487
544
  return {
488
- code: `http_${status}`,
545
+ code: `http_${status2}`,
489
546
  message: response.statusText || "request failed"
490
547
  };
491
548
  }
492
549
  if (isProxyErrorEnvelope(raw) && raw.error) {
493
550
  const current = typeof raw.current === "string" ? raw.current : void 0;
494
551
  return {
495
- code: raw.error.code ?? `http_${status}`,
552
+ code: raw.error.code ?? `http_${status2}`,
496
553
  message: raw.error.message ?? response.statusText ?? "request failed",
497
554
  ...current === void 0 ? {} : { current }
498
555
  };
499
556
  }
500
557
  return {
501
- code: `http_${status}`,
558
+ code: `http_${status2}`,
502
559
  message: response.statusText || "request failed"
503
560
  };
504
561
  }
505
- function throwProxyError(status, env) {
506
- if (status === 409 && env.code === "alias_drift") {
562
+ function throwProxyError(status2, env, requestId) {
563
+ if (status2 === 409 && env.code === "alias_drift") {
507
564
  throw new AliasDriftError(env.message, env.current ?? "");
508
565
  }
509
- throw new ProxyError(status, env.code, env.message);
566
+ throw new ProxyError(status2, env.code, env.message, requestId);
510
567
  }
511
568
  function stripTrailingSlash(url) {
512
569
  return url.endsWith("/") ? url.slice(0, -1) : url;
@@ -515,6 +572,7 @@ function createProxyClient(cfg) {
515
572
  const base = stripTrailingSlash(cfg.baseUrl);
516
573
  const fetchImpl = cfg.fetch ?? globalThis.fetch.bind(globalThis);
517
574
  const timeoutMs = cfg.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
575
+ const debug = cfg.debug ?? false;
518
576
  function withTimeoutSignal(init) {
519
577
  if (timeoutMs <= 0) return init;
520
578
  const timeoutSignal = AbortSignal.timeout(timeoutMs);
@@ -526,31 +584,65 @@ function createProxyClient(cfg) {
526
584
  throw new ProxyError(
527
585
  0,
528
586
  "timeout",
529
- `proxy timed out after ${timeoutMs}ms`
587
+ `proxy timed out after ${timeoutMs}ms (${base})`
530
588
  );
531
589
  }
532
590
  const message = err instanceof Error ? err.message : String(err);
533
- throw new ProxyError(0, "network_error", `proxy unreachable: ${message}`);
591
+ throw new ProxyError(
592
+ 0,
593
+ "network_error",
594
+ `proxy unreachable at ${base}: ${message}`
595
+ );
534
596
  }
535
597
  async function userBearer() {
536
598
  const tok = await cfg.getAuthToken();
537
599
  return `Bearer ${tok}`;
538
600
  }
539
- async function call(url, init) {
601
+ async function call(url, init, validate) {
602
+ const startedAt = debug ? Date.now() : 0;
540
603
  let response;
541
604
  try {
542
605
  response = await fetchImpl(url, withTimeoutSignal(init));
543
606
  } catch (err) {
544
607
  translateFetchError(err);
545
608
  }
609
+ if (debug) {
610
+ process.stderr.write(
611
+ `[universe] ${init.method ?? "GET"} ${url} -> ${response.status} (${Date.now() - startedAt}ms)
612
+ `
613
+ );
614
+ }
546
615
  if (!response.ok) {
616
+ const requestId = response.headers.get("x-request-id") ?? void 0;
547
617
  const env = await readErrorEnvelope(response);
548
- throwProxyError(response.status, env);
618
+ throwProxyError(response.status, env, requestId);
549
619
  }
550
620
  if (response.status === 204) {
551
621
  return void 0;
552
622
  }
553
- return await response.json();
623
+ let raw;
624
+ try {
625
+ raw = await response.json();
626
+ } catch {
627
+ throw new ProxyError(
628
+ 0,
629
+ "malformed_response",
630
+ "proxy returned a non-JSON response body"
631
+ );
632
+ }
633
+ if (validate) {
634
+ try {
635
+ validate(raw);
636
+ } catch (err) {
637
+ const detail = err instanceof Error ? err.message : "invalid shape";
638
+ throw new ProxyError(
639
+ 0,
640
+ "malformed_response",
641
+ `proxy returned an unexpected response shape: ${detail}`
642
+ );
643
+ }
644
+ }
645
+ return raw;
554
646
  }
555
647
  return {
556
648
  async whoami() {
@@ -711,6 +803,105 @@ function createProxyClient(cfg) {
711
803
  Accept: "application/json"
712
804
  }
713
805
  });
806
+ },
807
+ async createRepoRequest(req) {
808
+ const body = { name: req.name };
809
+ if (req.visibility !== void 0) body.visibility = req.visibility;
810
+ if (req.description !== void 0) body.description = req.description;
811
+ if (req.template !== void 0 && req.template !== "") {
812
+ body.template = req.template;
813
+ }
814
+ return call(
815
+ `${base}/api/repo`,
816
+ {
817
+ method: "POST",
818
+ headers: {
819
+ Authorization: await userBearer(),
820
+ Accept: "application/json",
821
+ "Content-Type": "application/json"
822
+ },
823
+ body: JSON.stringify(body)
824
+ },
825
+ (raw) => repoRowSchema.parse(raw)
826
+ );
827
+ },
828
+ async listRepoRequests(req) {
829
+ const params = new URLSearchParams();
830
+ if (req?.status) params.set("status", req.status);
831
+ if (req?.mine) params.set("mine", "1");
832
+ const qs = params.toString();
833
+ const url = `${base}/api/repos${qs ? `?${qs}` : ""}`;
834
+ return call(
835
+ url,
836
+ {
837
+ method: "GET",
838
+ headers: {
839
+ Authorization: await userBearer(),
840
+ Accept: "application/json"
841
+ }
842
+ },
843
+ (raw) => repoRowArraySchema.parse(raw)
844
+ );
845
+ },
846
+ async getRepoRequest(id) {
847
+ const url = `${base}/api/repo/${encodeURIComponent(id)}`;
848
+ return call(
849
+ url,
850
+ {
851
+ method: "GET",
852
+ headers: {
853
+ Authorization: await userBearer(),
854
+ Accept: "application/json"
855
+ }
856
+ },
857
+ (raw) => repoRowSchema.parse(raw)
858
+ );
859
+ },
860
+ async approveRepoRequest(req) {
861
+ const url = `${base}/api/repo/${encodeURIComponent(req.id)}/approve`;
862
+ return call(
863
+ url,
864
+ {
865
+ method: "POST",
866
+ headers: {
867
+ Authorization: await userBearer(),
868
+ Accept: "application/json"
869
+ }
870
+ },
871
+ (raw) => repoApproveResultSchema.parse(raw)
872
+ );
873
+ },
874
+ async rejectRepoRequest(req) {
875
+ const url = `${base}/api/repo/${encodeURIComponent(req.id)}/reject`;
876
+ const body = {};
877
+ if (req.reason !== void 0) body.reason = req.reason;
878
+ return call(
879
+ url,
880
+ {
881
+ method: "POST",
882
+ headers: {
883
+ Authorization: await userBearer(),
884
+ Accept: "application/json",
885
+ "Content-Type": "application/json"
886
+ },
887
+ body: JSON.stringify(body)
888
+ },
889
+ (raw) => repoRowSchema.parse(raw)
890
+ );
891
+ },
892
+ async listRepoTemplates() {
893
+ const res = await call(
894
+ `${base}/api/repo/templates`,
895
+ {
896
+ method: "GET",
897
+ headers: {
898
+ Authorization: await userBearer(),
899
+ Accept: "application/json"
900
+ }
901
+ },
902
+ (raw) => repoTemplatesResponseSchema.parse(raw)
903
+ );
904
+ return res.templates ?? [];
714
905
  }
715
906
  };
716
907
  }
@@ -851,11 +1042,17 @@ function buildEnvelope(command, success, data) {
851
1042
  ...data
852
1043
  };
853
1044
  }
854
- function buildErrorEnvelope(command, code, message, issues) {
1045
+ function buildErrorEnvelope(command, code, message, issues, kind, requestId) {
855
1046
  const error = {
856
1047
  code,
857
1048
  message
858
1049
  };
1050
+ if (kind !== void 0) {
1051
+ error.kind = kind;
1052
+ }
1053
+ if (requestId !== void 0) {
1054
+ error.requestId = requestId;
1055
+ }
859
1056
  if (issues !== void 0) {
860
1057
  error.issues = issues;
861
1058
  }
@@ -959,12 +1156,14 @@ function outputError(ctx, code, message, optsOrIssues) {
959
1156
  ctx.command,
960
1157
  code,
961
1158
  redactedMessage,
962
- redactedIssues
1159
+ redactedIssues,
1160
+ opts.kind,
1161
+ opts.requestId
963
1162
  );
964
1163
  const payload = opts.extras ? { ...envelope, ...redactObject(opts.extras) } : envelope;
965
1164
  process.stdout.write(JSON.stringify(payload) + "\n");
966
1165
  } else {
967
- (opts.logError ?? ((m) => log.error(m)))(redactedMessage);
1166
+ (opts.logError ?? ((m) => log.error(m, { output: process.stderr })))(redactedMessage);
968
1167
  }
969
1168
  }
970
1169
 
@@ -1885,6 +2084,7 @@ async function whoami(options, deps = {}) {
1885
2084
  buildEnvelope("whoami", true, {
1886
2085
  login: result.login,
1887
2086
  identitySource: identity.source,
2087
+ proxyUrl: baseUrl,
1888
2088
  authorizedSitesCount: count
1889
2089
  })
1890
2090
  );
@@ -1894,6 +2094,7 @@ async function whoami(options, deps = {}) {
1894
2094
  [
1895
2095
  `Logged in as: ${result.login}`,
1896
2096
  `Identity source: ${identity.source}`,
2097
+ `Proxy: ${baseUrl}`,
1897
2098
  sitesLine
1898
2099
  ].join("\n")
1899
2100
  );
@@ -2130,6 +2331,508 @@ async function update(options, deps = {}) {
2130
2331
  }
2131
2332
  }
2132
2333
 
2334
+ // src/commands/repo/approve.ts
2335
+ import { log as log13 } from "@clack/prompts";
2336
+
2337
+ // src/commands/repo/_shared.ts
2338
+ import {
2339
+ confirm as clackConfirm,
2340
+ isCancel as clackIsCancel,
2341
+ select as clackSelect,
2342
+ text as clackText
2343
+ } from "@clack/prompts";
2344
+ var defaultRepoPrompts = {
2345
+ text: clackText,
2346
+ select: clackSelect,
2347
+ confirm: clackConfirm,
2348
+ isCancel: clackIsCancel
2349
+ };
2350
+ function emitJson9(envelope) {
2351
+ process.stdout.write(JSON.stringify(envelope) + "\n");
2352
+ }
2353
+ async function setupClient2(deps) {
2354
+ const env = deps.env ?? process.env;
2355
+ const resolveId = deps.resolveIdentity ?? resolveIdentity;
2356
+ const mkClient = deps.createProxyClient ?? createProxyClient;
2357
+ const identity = await resolveId({ env });
2358
+ if (!identity) {
2359
+ throw new CredentialError(
2360
+ "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
2361
+ );
2362
+ }
2363
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
2364
+ const client = mkClient({
2365
+ baseUrl,
2366
+ getAuthToken: () => identity.token,
2367
+ timeoutMs: parseFetchTimeoutMs(env),
2368
+ debug: Boolean(env["UNIVERSE_DEBUG"])
2369
+ });
2370
+ return { client, identitySource: identity.source };
2371
+ }
2372
+ function formatRepoTable(rows, emptyMsg = "No repo requests.") {
2373
+ if (rows.length === 0) return emptyMsg;
2374
+ const headers = [
2375
+ "ID",
2376
+ "REPO",
2377
+ "VIS",
2378
+ "STATUS",
2379
+ "REQUESTED BY",
2380
+ "REQUESTED AT"
2381
+ ];
2382
+ const cells = rows.map((r) => [
2383
+ r.id,
2384
+ r.name,
2385
+ r.visibility,
2386
+ r.status,
2387
+ r.requestedBy,
2388
+ r.createdAt
2389
+ ]);
2390
+ const widths = headers.map(
2391
+ (h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
2392
+ );
2393
+ const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
2394
+ return [fmt(headers), ...cells.map(fmt)].join("\n");
2395
+ }
2396
+
2397
+ // src/commands/repo/approve.ts
2398
+ async function approve(options, deps = {}) {
2399
+ const command = "repo approve";
2400
+ const success = deps.logSuccess ?? ((s) => log13.success(s));
2401
+ const error = deps.logError ?? ((s) => log13.error(s));
2402
+ const exit = deps.exit ?? exitWithCode;
2403
+ const prompts = deps.prompts ?? defaultRepoPrompts;
2404
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
2405
+ let creationFailure;
2406
+ let identitySource;
2407
+ try {
2408
+ if (!options.id || options.id.trim().length === 0) {
2409
+ throw new UsageError("request id is required (positional argument)");
2410
+ }
2411
+ const setup = await setupClient2(deps);
2412
+ const client = setup.client;
2413
+ identitySource = setup.identitySource;
2414
+ if (!options.json && !options.yes) {
2415
+ if (!isTTY) {
2416
+ throw new UsageError(
2417
+ "non-interactive session: pass --yes to approve without confirmation"
2418
+ );
2419
+ }
2420
+ const cur = await client.getRepoRequest(options.id);
2421
+ const ok = await prompts.confirm({
2422
+ message: `Approve ${cur.visibility} repo "${cur.name}" requested by ${cur.requestedBy}? This creates the repository.`
2423
+ });
2424
+ if (prompts.isCancel(ok) || ok === false) {
2425
+ throw new ConfirmError("repo approve cancelled");
2426
+ }
2427
+ }
2428
+ const res = await client.approveRepoRequest({ id: options.id });
2429
+ const row = res.request;
2430
+ if (res.outcome === "approved_failed") {
2431
+ if (!options.json) {
2432
+ throw new StorageError(
2433
+ `approved, but repository creation failed: ${row.error ?? "unknown"} (${row.owner}/${row.name}, requested by ${row.requestedBy})`
2434
+ );
2435
+ }
2436
+ creationFailure = {
2437
+ outcome: res.outcome,
2438
+ id: row.id,
2439
+ repo: `${row.owner}/${row.name}`,
2440
+ status: row.status,
2441
+ creationError: row.error ?? "unknown",
2442
+ requestedBy: row.requestedBy,
2443
+ identitySource
2444
+ };
2445
+ } else if (options.json) {
2446
+ emitJson9(
2447
+ buildEnvelope(command, true, {
2448
+ id: row.id,
2449
+ outcome: res.outcome,
2450
+ repo: `${row.owner}/${row.name}`,
2451
+ url: row.url,
2452
+ visibility: row.visibility,
2453
+ approver: row.approver,
2454
+ identitySource
2455
+ })
2456
+ );
2457
+ } else {
2458
+ success(
2459
+ [
2460
+ `Approved ${row.name}`,
2461
+ ``,
2462
+ ` Repository: ${row.url ?? `${row.owner}/${row.name}`}`,
2463
+ ` Visibility: ${row.visibility}`,
2464
+ ` Approved by: ${row.approver ?? "you"}`
2465
+ ].join("\n")
2466
+ );
2467
+ }
2468
+ } catch (err) {
2469
+ const { code, message, kind, requestId } = wrapProxyError(command, err);
2470
+ outputError({ json: options.json, command }, code, message, {
2471
+ logError: error,
2472
+ kind,
2473
+ requestId,
2474
+ extras: identitySource ? { identitySource } : void 0
2475
+ });
2476
+ exit(code);
2477
+ return;
2478
+ }
2479
+ if (creationFailure) {
2480
+ outputError(
2481
+ { json: true, command },
2482
+ EXIT_STORAGE,
2483
+ "approved, but repository creation failed",
2484
+ { logError: error, extras: creationFailure }
2485
+ );
2486
+ exit(EXIT_STORAGE);
2487
+ }
2488
+ }
2489
+
2490
+ // src/commands/repo/create.ts
2491
+ import { log as log14 } from "@clack/prompts";
2492
+ function blankToUndefined(s) {
2493
+ if (s === void 0 || s === null) return void 0;
2494
+ const t = String(s).trim();
2495
+ return t === "" ? void 0 : t;
2496
+ }
2497
+ async function promptTemplate(client, prompts) {
2498
+ let templates = [];
2499
+ try {
2500
+ templates = await client.listRepoTemplates();
2501
+ } catch {
2502
+ templates = [];
2503
+ }
2504
+ if (templates.length === 0) {
2505
+ const v2 = await prompts.text({
2506
+ message: "Template (optional)",
2507
+ placeholder: "name of an org template repo; blank for an empty repo"
2508
+ });
2509
+ if (prompts.isCancel(v2)) throw new ConfirmError("repo create cancelled");
2510
+ return blankToUndefined(String(v2));
2511
+ }
2512
+ const v = await prompts.select({
2513
+ message: "Template",
2514
+ options: [
2515
+ { value: "", label: "None (blank repo)" },
2516
+ ...templates.map((t) => ({ value: t, label: t }))
2517
+ ],
2518
+ initialValue: ""
2519
+ });
2520
+ if (prompts.isCancel(v)) throw new ConfirmError("repo create cancelled");
2521
+ return blankToUndefined(String(v));
2522
+ }
2523
+ async function create(options, deps = {}) {
2524
+ const command = "repo create";
2525
+ const success = deps.logSuccess ?? ((s) => log14.success(s));
2526
+ const error = deps.logError ?? ((s) => log14.error(s));
2527
+ const exit = deps.exit ?? exitWithCode;
2528
+ const prompts = deps.prompts ?? defaultRepoPrompts;
2529
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
2530
+ const canPrompt = !options.json && !options.yes && isTTY;
2531
+ let identitySource = "";
2532
+ try {
2533
+ let client;
2534
+ const ensureClient = async () => {
2535
+ if (!client) {
2536
+ const setup = await setupClient2(deps);
2537
+ client = setup.client;
2538
+ identitySource = setup.identitySource;
2539
+ }
2540
+ return client;
2541
+ };
2542
+ let name = blankToUndefined(options.name) ?? "";
2543
+ let visibility = options.visibility;
2544
+ let description = options.description;
2545
+ let template = options.template;
2546
+ if (canPrompt) {
2547
+ if (!name) {
2548
+ const v = await prompts.text({
2549
+ message: "Repository name",
2550
+ placeholder: "learn-python-rpg"
2551
+ });
2552
+ if (prompts.isCancel(v))
2553
+ throw new ConfirmError("repo create cancelled");
2554
+ name = String(v).trim();
2555
+ }
2556
+ if (visibility === void 0) {
2557
+ const v = await prompts.select({
2558
+ message: "Visibility",
2559
+ options: [
2560
+ { value: "private", label: "Private" },
2561
+ { value: "public", label: "Public" }
2562
+ ],
2563
+ initialValue: "private"
2564
+ });
2565
+ if (prompts.isCancel(v))
2566
+ throw new ConfirmError("repo create cancelled");
2567
+ visibility = String(v);
2568
+ }
2569
+ if (description === void 0) {
2570
+ const v = await prompts.text({
2571
+ message: "Description (optional)",
2572
+ placeholder: "What is this project about?"
2573
+ });
2574
+ if (prompts.isCancel(v))
2575
+ throw new ConfirmError("repo create cancelled");
2576
+ description = String(v);
2577
+ }
2578
+ if (template === void 0) {
2579
+ template = await promptTemplate(await ensureClient(), prompts);
2580
+ }
2581
+ }
2582
+ if (!name) {
2583
+ throw new UsageError("repo name is required");
2584
+ }
2585
+ const candidate = { name };
2586
+ if (visibility !== void 0 && visibility !== "") {
2587
+ candidate.visibility = visibility;
2588
+ }
2589
+ const desc = blankToUndefined(description);
2590
+ if (desc !== void 0) candidate.description = desc;
2591
+ const tmpl = blankToUndefined(template);
2592
+ if (tmpl !== void 0) candidate.template = tmpl;
2593
+ const parsed = createRepoRequestSchema.safeParse(candidate);
2594
+ if (!parsed.success) {
2595
+ const issues = parsed.error.issues.map(
2596
+ (i) => `${i.path.join(".") || "input"}: ${i.message}`
2597
+ );
2598
+ throw new UsageError(issues.join("; "));
2599
+ }
2600
+ const body = parsed.data;
2601
+ if (!options.json && !options.yes) {
2602
+ if (!isTTY) {
2603
+ throw new UsageError(
2604
+ "non-interactive session: pass --yes to submit without confirmation (or --json)"
2605
+ );
2606
+ }
2607
+ const ok = await prompts.confirm({
2608
+ message: `Submit request to create ${body.visibility} repo "${body.name}"${body.template ? ` from template ${body.template}` : ""}?`
2609
+ });
2610
+ if (prompts.isCancel(ok) || ok === false) {
2611
+ throw new ConfirmError("repo create cancelled");
2612
+ }
2613
+ }
2614
+ const activeClient = await ensureClient();
2615
+ const row = await activeClient.createRepoRequest({
2616
+ name: body.name,
2617
+ visibility: body.visibility,
2618
+ description: body.description,
2619
+ template: body.template
2620
+ });
2621
+ if (options.json) {
2622
+ emitJson9(
2623
+ buildEnvelope(command, true, {
2624
+ id: row.id,
2625
+ name: row.name,
2626
+ owner: row.owner,
2627
+ visibility: row.visibility,
2628
+ template: row.template,
2629
+ status: row.status,
2630
+ identitySource
2631
+ })
2632
+ );
2633
+ } else {
2634
+ success(
2635
+ [
2636
+ `Request submitted`,
2637
+ ``,
2638
+ ` Request id: ${row.id}`,
2639
+ ` Repository: ${row.owner}/${row.name}`,
2640
+ ` Visibility: ${row.visibility}`,
2641
+ ...row.template ? [` Template: ${row.template}`] : [],
2642
+ ` Status: ${row.status} \u2014 run \`universe repo ls\` to review`
2643
+ ].join("\n")
2644
+ );
2645
+ }
2646
+ } catch (err) {
2647
+ const { code, message, kind, requestId } = wrapProxyError(command, err);
2648
+ outputError({ json: options.json, command }, code, message, {
2649
+ logError: error,
2650
+ kind,
2651
+ requestId,
2652
+ extras: identitySource ? { identitySource } : void 0
2653
+ });
2654
+ exit(code);
2655
+ }
2656
+ }
2657
+
2658
+ // src/commands/repo/ls.ts
2659
+ import { log as log15 } from "@clack/prompts";
2660
+ var LS_STATUSES = [...repoStatusSchema.options, "all"];
2661
+ async function ls3(options, deps = {}) {
2662
+ const command = "repo ls";
2663
+ const message = deps.logMessage ?? ((s) => log15.message(s));
2664
+ const error = deps.logError ?? ((s) => log15.error(s));
2665
+ const exit = deps.exit ?? exitWithCode;
2666
+ let identitySource;
2667
+ try {
2668
+ if (options.status !== void 0 && !LS_STATUSES.includes(options.status)) {
2669
+ throw new UsageError(
2670
+ `invalid --status "${options.status}": must be one of ${LS_STATUSES.join(", ")}`
2671
+ );
2672
+ }
2673
+ const setup = await setupClient2(deps);
2674
+ const client = setup.client;
2675
+ identitySource = setup.identitySource;
2676
+ const rows = await client.listRepoRequests({
2677
+ status: options.status,
2678
+ mine: options.mine ?? false
2679
+ });
2680
+ const status2 = options.status ?? "pending";
2681
+ if (options.json) {
2682
+ emitJson9(
2683
+ buildEnvelope(command, true, {
2684
+ count: rows.length,
2685
+ status: status2,
2686
+ mine: options.mine ?? false,
2687
+ requests: rows,
2688
+ identitySource
2689
+ })
2690
+ );
2691
+ } else {
2692
+ const empty = status2 === "all" ? "No repo requests." : `No ${status2} repo requests.`;
2693
+ message(formatRepoTable(rows, empty));
2694
+ }
2695
+ } catch (err) {
2696
+ const {
2697
+ code,
2698
+ message: msg,
2699
+ kind,
2700
+ requestId
2701
+ } = wrapProxyError(command, err);
2702
+ outputError({ json: options.json, command }, code, msg, {
2703
+ logError: error,
2704
+ kind,
2705
+ requestId,
2706
+ extras: identitySource ? { identitySource } : void 0
2707
+ });
2708
+ exit(code);
2709
+ }
2710
+ }
2711
+
2712
+ // src/commands/repo/reject.ts
2713
+ import { log as log16 } from "@clack/prompts";
2714
+ async function reject(options, deps = {}) {
2715
+ const command = "repo reject";
2716
+ const success = deps.logSuccess ?? ((s) => log16.success(s));
2717
+ const error = deps.logError ?? ((s) => log16.error(s));
2718
+ const exit = deps.exit ?? exitWithCode;
2719
+ const prompts = deps.prompts ?? defaultRepoPrompts;
2720
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
2721
+ let identitySource;
2722
+ try {
2723
+ if (!options.id || options.id.trim().length === 0) {
2724
+ throw new UsageError("request id is required (positional argument)");
2725
+ }
2726
+ const setup = await setupClient2(deps);
2727
+ const client = setup.client;
2728
+ identitySource = setup.identitySource;
2729
+ if (!options.json && !options.yes) {
2730
+ if (!isTTY) {
2731
+ throw new UsageError(
2732
+ "non-interactive session: pass --yes to reject without confirmation"
2733
+ );
2734
+ }
2735
+ const cur = await client.getRepoRequest(options.id);
2736
+ const ok = await prompts.confirm({
2737
+ message: `Reject the request for "${cur.name}" by ${cur.requestedBy}?`
2738
+ });
2739
+ if (prompts.isCancel(ok) || ok === false) {
2740
+ throw new ConfirmError("repo reject cancelled");
2741
+ }
2742
+ }
2743
+ const reason = options.reason === void 0 ? void 0 : String(options.reason).trim() || void 0;
2744
+ const row = await client.rejectRepoRequest({
2745
+ id: options.id,
2746
+ reason
2747
+ });
2748
+ if (options.json) {
2749
+ emitJson9(
2750
+ buildEnvelope(command, true, {
2751
+ id: row.id,
2752
+ status: row.status,
2753
+ repo: `${row.owner}/${row.name}`,
2754
+ rejectReason: row.rejectReason,
2755
+ identitySource
2756
+ })
2757
+ );
2758
+ } else {
2759
+ success(
2760
+ [
2761
+ `Rejected ${row.name}`,
2762
+ ``,
2763
+ ` Repository: ${row.owner}/${row.name}`,
2764
+ ...row.rejectReason ? [` Reason: ${row.rejectReason}`] : []
2765
+ ].join("\n")
2766
+ );
2767
+ }
2768
+ } catch (err) {
2769
+ const { code, message, kind, requestId } = wrapProxyError(command, err);
2770
+ outputError({ json: options.json, command }, code, message, {
2771
+ logError: error,
2772
+ kind,
2773
+ requestId,
2774
+ extras: identitySource ? { identitySource } : void 0
2775
+ });
2776
+ exit(code);
2777
+ }
2778
+ }
2779
+
2780
+ // src/commands/repo/status.ts
2781
+ import { log as log17 } from "@clack/prompts";
2782
+ function humanRow(row) {
2783
+ const lines = [
2784
+ `Request ${row.id}`,
2785
+ ``,
2786
+ ` Repository: ${row.owner}/${row.name}`,
2787
+ ` Visibility: ${row.visibility}`,
2788
+ ` Status: ${row.status}`,
2789
+ ` Requested by: ${row.requestedBy}`
2790
+ ];
2791
+ if (row.template) lines.push(` Template: ${row.template}`);
2792
+ if (row.url) lines.push(` URL: ${row.url}`);
2793
+ if (row.approver) lines.push(` Approver: ${row.approver}`);
2794
+ if (row.rejectReason) lines.push(` Reason: ${row.rejectReason}`);
2795
+ if (row.error) lines.push(` Error: ${row.error}`);
2796
+ lines.push(` Created: ${row.createdAt}`);
2797
+ lines.push(` Updated: ${row.updatedAt}`);
2798
+ return lines.join("\n");
2799
+ }
2800
+ async function status(options, deps = {}) {
2801
+ const command = "repo status";
2802
+ const message = deps.logMessage ?? ((s) => log17.message(s));
2803
+ const error = deps.logError ?? ((s) => log17.error(s));
2804
+ const exit = deps.exit ?? exitWithCode;
2805
+ let identitySource;
2806
+ try {
2807
+ if (!options.id || options.id.trim().length === 0) {
2808
+ throw new UsageError("request id is required (positional argument)");
2809
+ }
2810
+ const setup = await setupClient2(deps);
2811
+ const client = setup.client;
2812
+ identitySource = setup.identitySource;
2813
+ const row = await client.getRepoRequest(options.id);
2814
+ if (options.json) {
2815
+ emitJson9(buildEnvelope(command, true, { request: row, identitySource }));
2816
+ } else {
2817
+ message(humanRow(row));
2818
+ }
2819
+ } catch (err) {
2820
+ const {
2821
+ code,
2822
+ message: msg,
2823
+ kind,
2824
+ requestId
2825
+ } = wrapProxyError(command, err);
2826
+ outputError({ json: options.json, command }, code, msg, {
2827
+ logError: error,
2828
+ kind,
2829
+ requestId,
2830
+ extras: identitySource ? { identitySource } : void 0
2831
+ });
2832
+ exit(code);
2833
+ }
2834
+ }
2835
+
2133
2836
  // src/lib/update-notifier.ts
2134
2837
  import { readFileSync } from "fs";
2135
2838
  import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
@@ -2139,7 +2842,7 @@ var APP_DIR2 = "universe-cli";
2139
2842
  var CACHE_FILE = "update-check.json";
2140
2843
  var PKG_NAME = "@freecodecamp/universe-cli";
2141
2844
  var NPM_LATEST_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
2142
- var TTL_MS = 24 * 60 * 60 * 1e3;
2845
+ var TTL_MS = 6 * 60 * 60 * 1e3;
2143
2846
  var FETCH_TIMEOUT_MS = 3e3;
2144
2847
  function configBase2() {
2145
2848
  const xdg = process.env["XDG_CONFIG_HOME"];
@@ -2227,10 +2930,12 @@ function parseVersion(s) {
2227
2930
  if (nums.some((n) => Number.isNaN(n))) return null;
2228
2931
  return [nums[0], nums[1], nums[2]];
2229
2932
  }
2230
- async function refreshIfStale(now = Date.now()) {
2933
+ async function refreshIfStale(now = Date.now(), options = {}) {
2231
2934
  if (isDisabled()) return;
2232
- const cache = await readCache();
2233
- if (cache !== null && now - cache.lastCheck < TTL_MS) return;
2935
+ if (!options.force) {
2936
+ const cache = await readCache();
2937
+ if (cache !== null && now - cache.lastCheck < TTL_MS) return;
2938
+ }
2234
2939
  const latest = await fetchLatest();
2235
2940
  if (latest === null) return;
2236
2941
  try {
@@ -2285,7 +2990,7 @@ function installExitNotice(current) {
2285
2990
  }
2286
2991
 
2287
2992
  // src/cli.ts
2288
- var version = true ? "0.7.2" : "0.0.0";
2993
+ var version = true ? "0.8.1" : "0.0.0";
2289
2994
  function handleActionError(command, json, err) {
2290
2995
  const ctx = { json, command };
2291
2996
  const message = err instanceof Error ? err.message : "unknown error";
@@ -2300,14 +3005,22 @@ function findFirstPositional(args) {
2300
3005
  }
2301
3006
  return -1;
2302
3007
  }
2303
- function run(argv = process.argv) {
3008
+ function isVersionRequest(args) {
3009
+ return args.includes("--version") || args.includes("-v");
3010
+ }
3011
+ async function run(argv = process.argv) {
2304
3012
  installExitNotice(version);
2305
- void refreshIfStale();
2306
3013
  const args = argv.slice(2);
3014
+ const versionRequested = isVersionRequest(args);
3015
+ if (!versionRequested) {
3016
+ void refreshIfStale().catch(() => {
3017
+ });
3018
+ }
2307
3019
  const firstPosIdx = findFirstPositional(args);
2308
3020
  const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
2309
3021
  const isStatic = namespace === "static";
2310
3022
  const isSites = namespace === "sites";
3023
+ const isRepo = namespace === "repo";
2311
3024
  if (isSites) {
2312
3025
  const sitesArgs = [
2313
3026
  ...args.slice(0, firstPosIdx),
@@ -2374,6 +3087,136 @@ function run(argv = process.argv) {
2374
3087
  sitesCli.parse(["node", "universe-sites", ...sitesArgs]);
2375
3088
  return;
2376
3089
  }
3090
+ if (isRepo) {
3091
+ const repoArgs = [
3092
+ ...args.slice(0, firstPosIdx),
3093
+ ...args.slice(firstPosIdx + 1)
3094
+ ];
3095
+ const repoCli = cac("universe repo");
3096
+ repoCli.command(
3097
+ "create [name]",
3098
+ "Request a new repository under freeCodeCamp-Universe (staff only)"
3099
+ ).option("--json", "Output as JSON").option("--visibility <vis>", "public or private (default: private)").option("--description <text>", "Repository description").option(
3100
+ "--template <name>",
3101
+ "Org template repo to generate from; omit for a blank repo"
3102
+ ).option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").example("universe repo create my-app --visibility private --yes").action(
3103
+ async (name, flags) => {
3104
+ try {
3105
+ await create({
3106
+ json: flags.json ?? false,
3107
+ name,
3108
+ visibility: flags.visibility,
3109
+ description: flags.description,
3110
+ template: flags.template,
3111
+ yes: flags.yes ?? false
3112
+ });
3113
+ } catch (err) {
3114
+ handleActionError("repo create", flags.json ?? false, err);
3115
+ }
3116
+ }
3117
+ );
3118
+ repoCli.command("ls", "List repo requests (default: pending)").option("--json", "Output as JSON").option(
3119
+ "--status <status>",
3120
+ "pending | approved | active | rejected | failed | all"
3121
+ ).option("--mine", "Only requests you submitted").action(
3122
+ async (flags) => {
3123
+ try {
3124
+ await ls3({
3125
+ json: flags.json ?? false,
3126
+ status: flags.status,
3127
+ mine: flags.mine ?? false
3128
+ });
3129
+ } catch (err) {
3130
+ handleActionError("repo ls", flags.json ?? false, err);
3131
+ }
3132
+ }
3133
+ );
3134
+ repoCli.command(
3135
+ "approve <id>",
3136
+ "Approve a pending request \u2014 creates the repo (admin only)"
3137
+ ).option("--json", "Output as JSON").option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").example("universe repo approve req_abc123 --yes --json").action(async (id, flags) => {
3138
+ try {
3139
+ await approve({
3140
+ json: flags.json ?? false,
3141
+ id,
3142
+ yes: flags.yes ?? false
3143
+ });
3144
+ } catch (err) {
3145
+ handleActionError("repo approve", flags.json ?? false, err);
3146
+ }
3147
+ });
3148
+ repoCli.command("reject <id>", "Reject a pending request (admin only)").option("--json", "Output as JSON").option("--reason <text>", "Reason shown to the requester").option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").example('universe repo reject req_abc123 --reason "out of scope" --yes').action(
3149
+ async (id, flags) => {
3150
+ try {
3151
+ await reject({
3152
+ json: flags.json ?? false,
3153
+ id,
3154
+ reason: flags.reason,
3155
+ yes: flags.yes ?? false
3156
+ });
3157
+ } catch (err) {
3158
+ handleActionError("repo reject", flags.json ?? false, err);
3159
+ }
3160
+ }
3161
+ );
3162
+ repoCli.command("status <id>", "Show a request's current state").option("--json", "Output as JSON").action(async (id, flags) => {
3163
+ try {
3164
+ await status({ json: flags.json ?? false, id });
3165
+ } catch (err) {
3166
+ handleActionError("repo status", flags.json ?? false, err);
3167
+ }
3168
+ });
3169
+ repoCli.help();
3170
+ repoCli.version(version);
3171
+ const knownRepoSubs = /* @__PURE__ */ new Set([
3172
+ "create",
3173
+ "ls",
3174
+ "approve",
3175
+ "reject",
3176
+ "status"
3177
+ ]);
3178
+ const repoValueFlags = /* @__PURE__ */ new Set([
3179
+ "--visibility",
3180
+ "--description",
3181
+ "--template",
3182
+ "--status",
3183
+ "--reason"
3184
+ ]);
3185
+ let repoSub;
3186
+ for (let i = 0; i < repoArgs.length; i += 1) {
3187
+ const a = repoArgs[i];
3188
+ if (a === void 0) continue;
3189
+ if (repoValueFlags.has(a)) {
3190
+ i += 1;
3191
+ continue;
3192
+ }
3193
+ if (!a.startsWith("-")) {
3194
+ repoSub = a;
3195
+ break;
3196
+ }
3197
+ }
3198
+ const repoJson = repoArgs.includes("--json");
3199
+ const repoWantsHelp = repoArgs.includes("--help") || repoArgs.includes("-h") || repoArgs.includes("--version");
3200
+ if (repoSub === void 0 ? !repoWantsHelp : !knownRepoSubs.has(repoSub)) {
3201
+ if (repoSub === void 0 && !repoJson) {
3202
+ repoCli.outputHelp();
3203
+ } else {
3204
+ outputError(
3205
+ { json: repoJson, command: "repo" },
3206
+ EXIT_USAGE,
3207
+ repoSub === void 0 ? "missing repo subcommand \u2014 run `universe repo --help`" : `unknown repo subcommand "${repoSub}" \u2014 run \`universe repo --help\``
3208
+ );
3209
+ }
3210
+ exitWithCode(EXIT_USAGE);
3211
+ return;
3212
+ }
3213
+ try {
3214
+ repoCli.parse(["node", "universe-repo", ...repoArgs]);
3215
+ } catch (err) {
3216
+ handleActionError("repo", repoJson, err);
3217
+ }
3218
+ return;
3219
+ }
2377
3220
  if (isStatic) {
2378
3221
  const staticArgs = [
2379
3222
  ...args.slice(0, firstPosIdx),
@@ -2457,11 +3300,33 @@ function run(argv = process.argv) {
2457
3300
  });
2458
3301
  cli.command("static <subcommand>", "Static site deployment commands");
2459
3302
  cli.command("sites <subcommand>", "Static site registry commands");
3303
+ cli.command(
3304
+ "repo <subcommand>",
3305
+ "Repository creation + approval queue commands"
3306
+ );
2460
3307
  cli.help();
2461
3308
  cli.version(version);
2462
3309
  cli.parse(argv);
2463
3310
  }
3311
+ if (versionRequested) {
3312
+ await refreshIfStale(Date.now(), { force: true });
3313
+ }
3314
+ }
3315
+
3316
+ // src/lib/fatal.ts
3317
+ function formatFatal(err) {
3318
+ const message = err instanceof Error ? err.message : String(err);
3319
+ return `universe: ${redact(message)}`;
3320
+ }
3321
+ function defaultOnFatal(err) {
3322
+ process.stderr.write(formatFatal(err) + "\n");
3323
+ process.exit(EXIT_USAGE);
3324
+ }
3325
+ function installFatalHandlers(onFatal = defaultOnFatal) {
3326
+ process.on("unhandledRejection", onFatal);
3327
+ process.on("uncaughtException", onFatal);
2464
3328
  }
2465
3329
 
2466
3330
  // src/index.ts
2467
- run();
3331
+ installFatalHandlers();
3332
+ void run();