@freecodecamp/universe-cli 0.6.0 → 0.7.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
@@ -8421,6 +8421,27 @@ function w(r, t2) {
8421
8421
  const e = r;
8422
8422
  e.isTTY && e.setRawMode(t2);
8423
8423
  }
8424
+ function z({ input: r = import_node_process.stdin, output: t2 = import_node_process.stdout, overwrite: e = true, hideCursor: s = true } = {}) {
8425
+ const i = _.createInterface({ input: r, output: t2, prompt: "", tabSize: 1 });
8426
+ _.emitKeypressEvents(r, i), r instanceof import_node_tty.ReadStream && r.isTTY && r.setRawMode(true);
8427
+ const n = (o, { name: a, sequence: h }) => {
8428
+ const l = String(o);
8429
+ if (V([l, a, h], "cancel")) {
8430
+ s && t2.write(import_sisteransi.cursor.show), process.exit(0);
8431
+ return;
8432
+ }
8433
+ if (!e) return;
8434
+ const f = a === "return" ? 0 : -1, v = a === "return" ? -1 : 0;
8435
+ _.moveCursor(t2, f, v, () => {
8436
+ _.clearLine(t2, 1, () => {
8437
+ r.once("keypress", n);
8438
+ });
8439
+ });
8440
+ };
8441
+ return s && t2.write(import_sisteransi.cursor.hide), r.once("keypress", n), () => {
8442
+ r.off("keypress", n), s && t2.write(import_sisteransi.cursor.show), r instanceof import_node_tty.ReadStream && r.isTTY && !Y && r.setRawMode(false), i.terminal = false, i.close();
8443
+ };
8444
+ }
8424
8445
  var O = (r) => "columns" in r && typeof r.columns == "number" ? r.columns : 80;
8425
8446
  var A = (r) => "rows" in r && typeof r.rows == "number" ? r.rows : 20;
8426
8447
  function R(r, t2, e, s = e) {
@@ -8579,6 +8600,7 @@ function Ze() {
8579
8600
  return import_node_process2.default.platform !== "win32" ? import_node_process2.default.env.TERM !== "linux" : !!import_node_process2.default.env.CI || !!import_node_process2.default.env.WT_SESSION || !!import_node_process2.default.env.TERMINUS_SUBLIME || import_node_process2.default.env.ConEmuTask === "{cmd::Cmder}" || import_node_process2.default.env.TERM_PROGRAM === "Terminus-Sublime" || import_node_process2.default.env.TERM_PROGRAM === "vscode" || import_node_process2.default.env.TERM === "xterm-256color" || import_node_process2.default.env.TERM === "alacritty" || import_node_process2.default.env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
8580
8601
  }
8581
8602
  var ee = Ze();
8603
+ var ae = () => process.env.CI === "true";
8582
8604
  var w2 = (e, i) => ee ? e : i;
8583
8605
  var _e = w2("\u25C6", "*");
8584
8606
  var oe = w2("\u25A0", "x");
@@ -8671,6 +8693,59 @@ var O2 = { message: (e = [], { symbol: i = (0, import_node_util2.styleText)("gra
8671
8693
  }, error: (e, i) => {
8672
8694
  O2.message(e, { ...i, symbol: (0, import_node_util2.styleText)("red", ge) });
8673
8695
  } };
8696
+ var Ct = (e) => (0, import_node_util2.styleText)("magenta", e);
8697
+ var fe = ({ indicator: e = "dots", onCancel: i, output: s = process.stdout, cancelMessage: r, errorMessage: u2, frames: n = ee ? ["\u25D2", "\u25D0", "\u25D3", "\u25D1"] : ["\u2022", "o", "O", "0"], delay: o = ee ? 80 : 120, signal: c2, ...a } = {}) => {
8698
+ const l = ae();
8699
+ let $2, y2, p2 = false, m = false, g = "", S2, h = performance.now();
8700
+ const f = O(s), v = a?.styleFrame ?? Ct, T = (_2) => {
8701
+ const A2 = _2 > 1 ? u2 ?? u.messages.error : r ?? u.messages.cancel;
8702
+ m = _2 === 1, p2 && (W(A2, _2), m && typeof i == "function" && i());
8703
+ }, C2 = () => T(2), b = () => T(1), x = () => {
8704
+ process.on("uncaughtExceptionMonitor", C2), process.on("unhandledRejection", C2), process.on("SIGINT", b), process.on("SIGTERM", b), process.on("exit", T), c2 && c2.addEventListener("abort", b);
8705
+ }, G2 = () => {
8706
+ process.removeListener("uncaughtExceptionMonitor", C2), process.removeListener("unhandledRejection", C2), process.removeListener("SIGINT", b), process.removeListener("SIGTERM", b), process.removeListener("exit", T), c2 && c2.removeEventListener("abort", b);
8707
+ }, M2 = () => {
8708
+ if (S2 === void 0) return;
8709
+ l && s.write(`
8710
+ `);
8711
+ const _2 = wrapAnsi(S2, f, { hard: true, trim: false }).split(`
8712
+ `);
8713
+ _2.length > 1 && s.write(import_sisteransi2.cursor.up(_2.length - 1)), s.write(import_sisteransi2.cursor.to(0)), s.write(import_sisteransi2.erase.down());
8714
+ }, R2 = (_2) => _2.replace(/\.+$/, ""), j2 = (_2) => {
8715
+ const A2 = (performance.now() - _2) / 1e3, k = Math.floor(A2 / 60), L = Math.floor(A2 % 60);
8716
+ return k > 0 ? `[${k}m ${L}s]` : `[${L}s]`;
8717
+ }, D2 = a.withGuide ?? u.withGuide, ie = (_2 = "") => {
8718
+ p2 = true, $2 = z({ output: s }), g = R2(_2), h = performance.now(), D2 && s.write(`${(0, import_node_util2.styleText)("gray", d)}
8719
+ `);
8720
+ let A2 = 0, k = 0;
8721
+ x(), y2 = setInterval(() => {
8722
+ if (l && g === S2) return;
8723
+ M2(), S2 = g;
8724
+ const L = v(n[A2]);
8725
+ let Z;
8726
+ if (l) Z = `${L} ${g}...`;
8727
+ else if (e === "timer") Z = `${L} ${g} ${j2(h)}`;
8728
+ else {
8729
+ const Be = ".".repeat(Math.floor(k)).slice(0, 3);
8730
+ Z = `${L} ${g}${Be}`;
8731
+ }
8732
+ const Ne = wrapAnsi(Z, f, { hard: true, trim: false });
8733
+ s.write(Ne), A2 = A2 + 1 < n.length ? A2 + 1 : 0, k = k < 4 ? k + 0.125 : 0;
8734
+ }, o);
8735
+ }, W = (_2 = "", A2 = 0, k = false) => {
8736
+ if (!p2) return;
8737
+ p2 = false, clearInterval(y2), M2();
8738
+ const L = A2 === 0 ? (0, import_node_util2.styleText)("green", F) : A2 === 1 ? (0, import_node_util2.styleText)("red", oe) : (0, import_node_util2.styleText)("red", ue);
8739
+ g = _2 ?? g, k || (e === "timer" ? s.write(`${L} ${g} ${j2(h)}
8740
+ `) : s.write(`${L} ${g}
8741
+ `)), G2(), $2();
8742
+ };
8743
+ return { start: ie, stop: (_2 = "") => W(_2, 0), message: (_2 = "") => {
8744
+ g = R2(_2 ?? g);
8745
+ }, cancel: (_2 = "") => W(_2, 1), error: (_2 = "") => W(_2, 2), clear: () => W("", 0, true), get isCancelled() {
8746
+ return m;
8747
+ } };
8748
+ };
8674
8749
  var Ve = { light: w2("\u2500", "-"), heavy: w2("\u2501", "="), block: w2("\u2588", "#") };
8675
8750
  var je = `${(0, import_node_util2.styleText)("gray", d)} `;
8676
8751
 
@@ -8887,14 +8962,14 @@ async function resolveIdentity(opts = {}) {
8887
8962
  if (isNonEmpty(ghTokenEnv)) {
8888
8963
  return { token: ghTokenEnv.trim(), source: "env_GH_TOKEN" };
8889
8964
  }
8890
- const ghCli = await execGh();
8891
- if (isNonEmpty(ghCli)) {
8892
- return { token: ghCli.trim(), source: "gh_cli" };
8893
- }
8894
8965
  const stored = await loadStored();
8895
8966
  if (isNonEmpty(stored)) {
8896
8967
  return { token: stored.trim(), source: "device_flow" };
8897
8968
  }
8969
+ const ghCli = await execGh();
8970
+ if (isNonEmpty(ghCli)) {
8971
+ return { token: ghCli.trim(), source: "gh_cli" };
8972
+ }
8898
8973
  return null;
8899
8974
  }
8900
8975
 
@@ -22862,6 +22937,14 @@ function suggest(target, candidates, threshold = 2) {
22862
22937
  }
22863
22938
 
22864
22939
  // src/lib/proxy-client.ts
22940
+ var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
22941
+ function parseFetchTimeoutMs(env) {
22942
+ const raw = env["UNIVERSE_FETCH_TIMEOUT_MS"];
22943
+ if (raw === void 0 || raw === "") return void 0;
22944
+ const n = Number(raw);
22945
+ if (!Number.isFinite(n) || n < 0) return void 0;
22946
+ return Math.floor(n);
22947
+ }
22865
22948
  var ProxyError = class extends CliError {
22866
22949
  exitCode;
22867
22950
  status;
@@ -22939,6 +23022,24 @@ function stripTrailingSlash(url2) {
22939
23022
  function createProxyClient(cfg) {
22940
23023
  const base = stripTrailingSlash(cfg.baseUrl);
22941
23024
  const fetchImpl = cfg.fetch ?? globalThis.fetch.bind(globalThis);
23025
+ const timeoutMs = cfg.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
23026
+ function withTimeoutSignal(init) {
23027
+ if (timeoutMs <= 0) return init;
23028
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
23029
+ const merged = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
23030
+ return { ...init, signal: merged };
23031
+ }
23032
+ function translateFetchError(err) {
23033
+ if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) {
23034
+ throw new ProxyError(
23035
+ 0,
23036
+ "timeout",
23037
+ `proxy timed out after ${timeoutMs}ms`
23038
+ );
23039
+ }
23040
+ const message = err instanceof Error ? err.message : String(err);
23041
+ throw new ProxyError(0, "network_error", `proxy unreachable: ${message}`);
23042
+ }
22942
23043
  async function userBearer() {
22943
23044
  const tok = await cfg.getAuthToken();
22944
23045
  return `Bearer ${tok}`;
@@ -22946,10 +23047,9 @@ function createProxyClient(cfg) {
22946
23047
  async function call(url2, init) {
22947
23048
  let response;
22948
23049
  try {
22949
- response = await fetchImpl(url2, init);
23050
+ response = await fetchImpl(url2, withTimeoutSignal(init));
22950
23051
  } catch (err) {
22951
- const message = err instanceof Error ? err.message : String(err);
22952
- throw new ProxyError(0, "network_error", `proxy unreachable: ${message}`);
23052
+ translateFetchError(err);
22953
23053
  }
22954
23054
  if (!response.ok) {
22955
23055
  const env = await readErrorEnvelope(response);
@@ -23021,20 +23121,18 @@ function createProxyClient(cfg) {
23021
23121
  const url2 = `${base}/api/site/${encodeURIComponent(req.site)}/alias/${encodeURIComponent(req.mode)}`;
23022
23122
  let response;
23023
23123
  try {
23024
- response = await fetchImpl(url2, {
23025
- method: "GET",
23026
- headers: {
23027
- Authorization: await userBearer(),
23028
- Accept: "application/json"
23029
- }
23030
- });
23031
- } catch (err) {
23032
- const message = err instanceof Error ? err.message : String(err);
23033
- throw new ProxyError(
23034
- 0,
23035
- "network_error",
23036
- `proxy unreachable: ${message}`
23124
+ response = await fetchImpl(
23125
+ url2,
23126
+ withTimeoutSignal({
23127
+ method: "GET",
23128
+ headers: {
23129
+ Authorization: await userBearer(),
23130
+ Accept: "application/json"
23131
+ }
23132
+ })
23037
23133
  );
23134
+ } catch (err) {
23135
+ translateFetchError(err);
23038
23136
  }
23039
23137
  if (response.status === 404) return null;
23040
23138
  if (!response.ok) {
@@ -23278,6 +23376,103 @@ function buildErrorEnvelope(command, code, message, issues) {
23278
23376
  };
23279
23377
  }
23280
23378
 
23379
+ // src/output/redact.ts
23380
+ var AWS_KEY_PREFIX_PATTERN = /(?:AKIA|ASIA|AROA|AIDA|ACCA|ANPA|ABIA|AGPA)[A-Z0-9]{12,}/g;
23381
+ var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
23382
+ var CREDENTIAL_CONTEXT_PATTERN = /(?:access_key_id|secret_access_key|accessKeyId|secretAccessKey|secret|password|token|key|credential|auth)\s*[=:]\s*([A-Za-z0-9/+=]{21,})/gi;
23383
+ var HEX_CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key)\s*[=:]\s*([a-f0-9]{32,})/gi;
23384
+ var JSON_CREDENTIAL_PATTERN = /"(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key|accessKeyId|secretAccessKey)"\s*:\s*"[^"]+"/gi;
23385
+ var BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
23386
+ var CREDENTIAL_KEY_PATTERN = /(?:secret|password|token|key|credential|auth)/i;
23387
+ var EXACT_CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
23388
+ "accesskeyid",
23389
+ "secretaccesskey",
23390
+ "access_key_id",
23391
+ "secret_access_key"
23392
+ ]);
23393
+ var STANDALONE_LONG_SECRET = /^[A-Za-z0-9/+=]{21,}$/;
23394
+ function maskAwsKey(match) {
23395
+ return match.slice(0, 4) + "****" + match.slice(-4);
23396
+ }
23397
+ function maskUrlCreds(match) {
23398
+ const atIndex = match.lastIndexOf("@");
23399
+ const protocolEnd = match.indexOf("://") + 3;
23400
+ return match.slice(0, protocolEnd) + "****:****@" + match.slice(atIndex + 1);
23401
+ }
23402
+ function redact(value) {
23403
+ let result = value;
23404
+ result = result.replace(URL_CREDS_PATTERN, maskUrlCreds);
23405
+ result = result.replace(AWS_KEY_PREFIX_PATTERN, maskAwsKey);
23406
+ result = result.replace(BEARER_PATTERN, "Bearer ****");
23407
+ result = result.replace(JSON_CREDENTIAL_PATTERN, (match) => {
23408
+ const colonIndex = match.indexOf(":");
23409
+ return match.slice(0, colonIndex + 1) + '"****"';
23410
+ });
23411
+ result = result.replace(
23412
+ CREDENTIAL_CONTEXT_PATTERN,
23413
+ (_match, _secret, _offset, _full) => {
23414
+ const eqIndex = _match.indexOf("=");
23415
+ const colonIndex = _match.indexOf(":");
23416
+ const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
23417
+ const prefix = _match.slice(0, sepIndex + 1);
23418
+ return prefix + "****";
23419
+ }
23420
+ );
23421
+ result = result.replace(HEX_CREDENTIAL_CONTEXT_PATTERN, (_match, _secret) => {
23422
+ const eqIndex = _match.indexOf("=");
23423
+ const colonIndex = _match.indexOf(":");
23424
+ const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
23425
+ const prefix = _match.slice(0, sepIndex + 1);
23426
+ return prefix + "****";
23427
+ });
23428
+ return result;
23429
+ }
23430
+ function redactValue(value, keyName) {
23431
+ if (typeof value === "string") {
23432
+ if (keyName && EXACT_CREDENTIAL_KEYS.has(keyName.toLowerCase())) {
23433
+ return "****";
23434
+ }
23435
+ const redacted = redact(value);
23436
+ if (redacted === value && keyName && CREDENTIAL_KEY_PATTERN.test(keyName) && STANDALONE_LONG_SECRET.test(value)) {
23437
+ return "****";
23438
+ }
23439
+ return redacted;
23440
+ }
23441
+ if (Array.isArray(value)) {
23442
+ return value.map((v) => redactValue(v, keyName));
23443
+ }
23444
+ if (value !== null && typeof value === "object") {
23445
+ return redactObject(value);
23446
+ }
23447
+ return value;
23448
+ }
23449
+ function redactObject(obj) {
23450
+ const result = {};
23451
+ for (const [key, value] of Object.entries(obj)) {
23452
+ result[key] = redactValue(value, key);
23453
+ }
23454
+ return result;
23455
+ }
23456
+
23457
+ // src/output/format.ts
23458
+ function outputError(ctx, code, message, optsOrIssues) {
23459
+ const opts = Array.isArray(optsOrIssues) ? { issues: optsOrIssues } : optsOrIssues ?? {};
23460
+ const redactedMessage = redact(message);
23461
+ const redactedIssues = opts.issues?.map(redact);
23462
+ if (ctx.json) {
23463
+ const envelope = buildErrorEnvelope(
23464
+ ctx.command,
23465
+ code,
23466
+ redactedMessage,
23467
+ redactedIssues
23468
+ );
23469
+ const payload = opts.extras ? { ...envelope, ...redactObject(opts.extras) } : envelope;
23470
+ process.stdout.write(JSON.stringify(payload) + "\n");
23471
+ } else {
23472
+ (opts.logError ?? ((m) => O2.error(m)))(redactedMessage);
23473
+ }
23474
+ }
23475
+
23281
23476
  // src/commands/deploy.ts
23282
23477
  var defaultReadPlatformYaml = async (cwd) => {
23283
23478
  return (0, import_promises4.readFile)((0, import_node_path5.resolve)(cwd, "platform.yaml"), "utf-8");
@@ -23371,6 +23566,7 @@ async function deploy(options, deps = {}) {
23371
23566
  const build = deps.runBuild ?? runBuild;
23372
23567
  const walk = deps.walkFiles ?? walkFiles;
23373
23568
  const upload = deps.uploadFiles ?? uploadFiles;
23569
+ const mkSpinner = deps.createSpinner ?? (() => fe());
23374
23570
  const success2 = deps.logSuccess ?? ((s) => O2.success(s));
23375
23571
  const info = deps.logInfo ?? ((s) => O2.info(s));
23376
23572
  const warn = deps.logWarn ?? ((s) => O2.warn(s));
@@ -23387,7 +23583,8 @@ async function deploy(options, deps = {}) {
23387
23583
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
23388
23584
  const client = mkClient({
23389
23585
  baseUrl,
23390
- getAuthToken: () => identity.token
23586
+ getAuthToken: () => identity.token,
23587
+ timeoutMs: parseFetchTimeoutMs(env)
23391
23588
  });
23392
23589
  let me2;
23393
23590
  try {
@@ -23438,17 +23635,22 @@ async function deploy(options, deps = {}) {
23438
23635
  } catch (err) {
23439
23636
  rethrowProxy("deploy init failed", err);
23440
23637
  }
23638
+ const spin = options.json ? null : mkSpinner();
23639
+ spin?.start(`Uploading 0/${filtered.length} files`);
23441
23640
  const uploadResult = await upload({
23442
23641
  client,
23443
23642
  deployId: initResult.deployId,
23444
23643
  jwt: initResult.jwt,
23445
- files: filtered
23644
+ files: filtered,
23645
+ onProgress: spin ? (p2) => spin.message(`Uploading ${p2.uploaded}/${p2.total} \u2014 ${p2.current}`) : void 0
23446
23646
  });
23447
23647
  if (uploadResult.errors.length > 0) {
23648
+ spin?.error(`Upload failed: ${uploadResult.errors.length} file(s)`);
23448
23649
  const message = `Upload partially failed: ${uploadResult.errors.length} file(s) failed:
23449
23650
  - ${uploadResult.errors.join("\n - ")}`;
23450
23651
  throw new PartialUploadError(message);
23451
23652
  }
23653
+ spin?.stop(`Uploaded ${uploadResult.fileCount} files`);
23452
23654
  const mode = options.promote ? "production" : "preview";
23453
23655
  let finalizeResult;
23454
23656
  try {
@@ -23461,6 +23663,25 @@ async function deploy(options, deps = {}) {
23461
23663
  } catch (err) {
23462
23664
  rethrowProxy("deploy finalize failed", err);
23463
23665
  }
23666
+ if (options.promote && !options.json) {
23667
+ try {
23668
+ const preview = await client.getAlias({
23669
+ site: config2.site,
23670
+ mode: "preview"
23671
+ });
23672
+ if (preview && preview.deployId !== finalizeResult.deployId) {
23673
+ warn(
23674
+ `Preview alias still points to ${preview.deployId}; it will not auto-update. Run \`universe static deploy\` (without --promote) to refresh preview.`
23675
+ );
23676
+ }
23677
+ } catch (err) {
23678
+ if (err instanceof ProxyError && (err.status === 401 || err.status === 403)) {
23679
+ warn(
23680
+ `Preview alias probe got ${err.status} (${err.code}) \u2014 token may need rotation: ${err.message}`
23681
+ );
23682
+ }
23683
+ }
23684
+ }
23464
23685
  if (options.json) {
23465
23686
  emitJson(
23466
23687
  buildEnvelope("deploy", true, {
@@ -23507,11 +23728,9 @@ async function deploy(options, deps = {}) {
23507
23728
  code = EXIT_USAGE;
23508
23729
  message = String(err);
23509
23730
  }
23510
- if (options.json) {
23511
- emitJson(buildErrorEnvelope("deploy", code, message));
23512
- } else {
23513
- error48(message);
23514
- }
23731
+ outputError({ json: options.json, command: "deploy" }, code, message, {
23732
+ logError: error48
23733
+ });
23515
23734
  exit(code);
23516
23735
  }
23517
23736
  }
@@ -23591,6 +23810,19 @@ async function runDeviceFlow(opts) {
23591
23810
 
23592
23811
  // src/commands/login.ts
23593
23812
  var DEFAULT_SCOPE = "read:org user:email";
23813
+ var DEFAULT_PROXY_URL2 = "https://uploads.freecode.camp";
23814
+ var NO_SITES_WARNING = [
23815
+ "Logged in, but the proxy reports 0 authorized sites for your account.",
23816
+ "This usually means the Universe CLI GitHub App is not installed on the org",
23817
+ "that owns the registry-authz team (production: `freeCodeCamp-Universe`), or",
23818
+ "your account is not on a team granted access to any site.",
23819
+ "",
23820
+ "Next steps:",
23821
+ " 1. Run `universe whoami` to confirm the identity that resolved.",
23822
+ " 2. Ask an org owner to install the Universe CLI GitHub App on the org.",
23823
+ " 3. Confirm your team membership at",
23824
+ " https://github.com/orgs/freeCodeCamp-Universe/teams."
23825
+ ].join("\n");
23594
23826
  function emitJson2(envelope) {
23595
23827
  process.stdout.write(JSON.stringify(envelope) + "\n");
23596
23828
  }
@@ -23652,25 +23884,56 @@ async function login(options, deps = {}) {
23652
23884
  });
23653
23885
  } catch (err) {
23654
23886
  const message = err instanceof Error ? err.message : String(err);
23655
- if (options.json) {
23656
- emitJson2({
23657
- schemaVersion: "1",
23658
- command: "login",
23659
- success: false,
23660
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
23661
- error: { code: EXIT_CREDENTIALS, message }
23662
- });
23663
- } else {
23664
- error48(message);
23665
- }
23887
+ outputError(
23888
+ { json: options.json, command: "login" },
23889
+ EXIT_CREDENTIALS,
23890
+ message,
23891
+ { logError: error48 }
23892
+ );
23666
23893
  exit(EXIT_CREDENTIALS);
23667
23894
  return;
23668
23895
  }
23669
23896
  await save(token);
23897
+ const selfCheck = await postLoginSelfCheck(token, env, deps);
23670
23898
  if (options.json) {
23671
- emitJson2(buildEnvelope("login", true, { stored: true }));
23899
+ emitJson2(
23900
+ buildEnvelope("login", true, {
23901
+ stored: true,
23902
+ ...selfCheck.checked ? {
23903
+ authorizedSitesCount: selfCheck.authorizedSitesCount,
23904
+ ...selfCheck.warning ? { warning: selfCheck.warning } : {}
23905
+ } : {}
23906
+ })
23907
+ );
23672
23908
  } else {
23673
23909
  success2("Logged in. Token stored at ~/.config/universe-cli/token.");
23910
+ if (selfCheck.checked && selfCheck.warning) {
23911
+ const warn = deps.logWarn ?? ((s) => O2.warn(s));
23912
+ warn(selfCheck.warning);
23913
+ }
23914
+ }
23915
+ }
23916
+ async function postLoginSelfCheck(token, env, deps) {
23917
+ const mkClient = deps.createProxyClient ?? createProxyClient;
23918
+ try {
23919
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
23920
+ const client = mkClient({
23921
+ baseUrl,
23922
+ getAuthToken: () => token,
23923
+ timeoutMs: parseFetchTimeoutMs(env)
23924
+ });
23925
+ const result = await client.whoami();
23926
+ const count = result.authorizedSites.length;
23927
+ if (count === 0) {
23928
+ return {
23929
+ checked: true,
23930
+ authorizedSitesCount: 0,
23931
+ warning: NO_SITES_WARNING
23932
+ };
23933
+ }
23934
+ return { checked: true, authorizedSitesCount: count };
23935
+ } catch {
23936
+ return { checked: false, authorizedSitesCount: 0 };
23674
23937
  }
23675
23938
  }
23676
23939
 
@@ -23771,7 +24034,8 @@ async function ls(options, deps = {}) {
23771
24034
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
23772
24035
  const client = mkClient({
23773
24036
  baseUrl,
23774
- getAuthToken: () => identity.token
24037
+ getAuthToken: () => identity.token,
24038
+ timeoutMs: parseFetchTimeoutMs(env)
23775
24039
  });
23776
24040
  const raw = await client.siteDeploys({ site });
23777
24041
  const sorted = [...raw].sort(
@@ -23795,11 +24059,9 @@ async function ls(options, deps = {}) {
23795
24059
  success2(formatTable(deploys));
23796
24060
  } catch (err) {
23797
24061
  const { code, message } = wrapProxyError("ls", err);
23798
- if (options.json) {
23799
- emitJson4(buildErrorEnvelope("ls", code, message));
23800
- } else {
23801
- error48(message);
23802
- }
24062
+ outputError({ json: options.json, command: "ls" }, code, message, {
24063
+ logError: error48
24064
+ });
23803
24065
  exit(code);
23804
24066
  }
23805
24067
  }
@@ -23855,7 +24117,8 @@ async function promote(options, deps = {}) {
23855
24117
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
23856
24118
  const client = mkClient({
23857
24119
  baseUrl,
23858
- getAuthToken: () => identity.token
24120
+ getAuthToken: () => identity.token,
24121
+ timeoutMs: parseFetchTimeoutMs(env)
23859
24122
  });
23860
24123
  let result;
23861
24124
  if (options.from) {
@@ -23953,16 +24216,11 @@ async function promote(options, deps = {}) {
23953
24216
  }
23954
24217
  } catch (err) {
23955
24218
  const { code, message } = wrapProxyError("promote", err);
23956
- if (options.json) {
23957
- const envelope = buildErrorEnvelope("promote", code, message);
23958
- if (err instanceof AliasDriftError) {
23959
- emitJson5({ ...envelope, current: err.current });
23960
- } else {
23961
- emitJson5(envelope);
23962
- }
23963
- } else {
23964
- error48(message);
23965
- }
24219
+ const extras = err instanceof AliasDriftError ? { current: err.current } : void 0;
24220
+ outputError({ json: options.json, command: "promote" }, code, message, {
24221
+ logError: error48,
24222
+ extras
24223
+ });
23966
24224
  exit(code);
23967
24225
  }
23968
24226
  }
@@ -24023,7 +24281,8 @@ async function rollback(options, deps = {}) {
24023
24281
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
24024
24282
  const client = mkClient({
24025
24283
  baseUrl,
24026
- getAuthToken: () => identity.token
24284
+ getAuthToken: () => identity.token,
24285
+ timeoutMs: parseFetchTimeoutMs(env)
24027
24286
  });
24028
24287
  const to = options.to.trim();
24029
24288
  const prod = await client.getAlias({
@@ -24076,22 +24335,17 @@ async function rollback(options, deps = {}) {
24076
24335
  }
24077
24336
  } catch (err) {
24078
24337
  const { code, message } = wrapProxyError("rollback", err);
24079
- if (options.json) {
24080
- const envelope = buildErrorEnvelope("rollback", code, message);
24081
- if (err instanceof AliasDriftError) {
24082
- emitJson6({ ...envelope, current: err.current });
24083
- } else {
24084
- emitJson6(envelope);
24085
- }
24086
- } else {
24087
- error48(message);
24088
- }
24338
+ const extras = err instanceof AliasDriftError ? { current: err.current } : void 0;
24339
+ outputError({ json: options.json, command: "rollback" }, code, message, {
24340
+ logError: error48,
24341
+ extras
24342
+ });
24089
24343
  exit(code);
24090
24344
  }
24091
24345
  }
24092
24346
 
24093
24347
  // src/commands/whoami.ts
24094
- var DEFAULT_PROXY_URL2 = "https://uploads.freecode.camp";
24348
+ var DEFAULT_PROXY_URL3 = "https://uploads.freecode.camp";
24095
24349
  function emitJson7(envelope) {
24096
24350
  process.stdout.write(JSON.stringify(envelope) + "\n");
24097
24351
  }
@@ -24105,18 +24359,20 @@ async function whoami(options, deps = {}) {
24105
24359
  const identity = await resolve6({ env });
24106
24360
  if (!identity) {
24107
24361
  const msg = "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI.";
24108
- if (options.json) {
24109
- emitJson7(buildErrorEnvelope("whoami", EXIT_CREDENTIALS, msg));
24110
- } else {
24111
- error48(msg);
24112
- }
24362
+ outputError(
24363
+ { json: options.json, command: "whoami" },
24364
+ EXIT_CREDENTIALS,
24365
+ msg,
24366
+ { logError: error48 }
24367
+ );
24113
24368
  exit(EXIT_CREDENTIALS);
24114
24369
  return;
24115
24370
  }
24116
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
24371
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL3;
24117
24372
  const client = mkClient({
24118
24373
  baseUrl,
24119
- getAuthToken: () => identity.token
24374
+ getAuthToken: () => identity.token,
24375
+ timeoutMs: parseFetchTimeoutMs(env)
24120
24376
  });
24121
24377
  try {
24122
24378
  const result = await client.whoami();
@@ -24142,11 +24398,9 @@ async function whoami(options, deps = {}) {
24142
24398
  } catch (err) {
24143
24399
  const exitCode = err instanceof CliError ? err.exitCode : EXIT_CREDENTIALS;
24144
24400
  const message = err instanceof ProxyError ? `whoami failed (${err.code}): ${err.message}` : err instanceof Error ? err.message : String(err);
24145
- if (options.json) {
24146
- emitJson7(buildErrorEnvelope("whoami", exitCode, message));
24147
- } else {
24148
- error48(message);
24149
- }
24401
+ outputError({ json: options.json, command: "whoami" }, exitCode, message, {
24402
+ logError: error48
24403
+ });
24150
24404
  exit(exitCode);
24151
24405
  }
24152
24406
  }
@@ -24173,7 +24427,8 @@ async function setupClient(deps) {
24173
24427
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
24174
24428
  const client = mkClient({
24175
24429
  baseUrl,
24176
- getAuthToken: () => identity.token
24430
+ getAuthToken: () => identity.token,
24431
+ timeoutMs: parseFetchTimeoutMs(env)
24177
24432
  });
24178
24433
  return { client, identitySource: identity.source };
24179
24434
  }
@@ -24200,7 +24455,7 @@ async function ls2(options, deps = {}) {
24200
24455
  const error48 = deps.logError ?? ((s) => O2.error(s));
24201
24456
  const exit = deps.exit ?? exitWithCode;
24202
24457
  try {
24203
- const { client } = await setupClient(deps);
24458
+ const { client, identitySource } = await setupClient(deps);
24204
24459
  let rows = await client.listSites();
24205
24460
  let scope = "all";
24206
24461
  if (options.mine) {
@@ -24214,7 +24469,8 @@ async function ls2(options, deps = {}) {
24214
24469
  buildEnvelope(command, true, {
24215
24470
  count: rows.length,
24216
24471
  scope,
24217
- sites: rows
24472
+ sites: rows,
24473
+ identitySource
24218
24474
  })
24219
24475
  );
24220
24476
  } else {
@@ -24222,11 +24478,9 @@ async function ls2(options, deps = {}) {
24222
24478
  }
24223
24479
  } catch (err) {
24224
24480
  const { code, message } = wrapProxyError(command, err);
24225
- if (options.json) {
24226
- emitJson8(buildErrorEnvelope(command, code, message));
24227
- } else {
24228
- error48(message);
24229
- }
24481
+ outputError({ json: options.json, command }, code, message, {
24482
+ logError: error48
24483
+ });
24230
24484
  exit(code);
24231
24485
  }
24232
24486
  }
@@ -24242,7 +24496,7 @@ async function register(options, deps = {}) {
24242
24496
  throw new UsageError("slug is required (positional argument)");
24243
24497
  }
24244
24498
  const teams = parseTeamsFlag(options.team);
24245
- const { client } = await setupClient(deps);
24499
+ const { client, identitySource } = await setupClient(deps);
24246
24500
  const row = await client.registerSite({
24247
24501
  slug: options.slug,
24248
24502
  teams: teams.length > 0 ? teams : void 0
@@ -24253,7 +24507,8 @@ async function register(options, deps = {}) {
24253
24507
  slug: row.slug,
24254
24508
  teams: row.teams,
24255
24509
  createdAt: row.createdAt,
24256
- createdBy: row.createdBy
24510
+ createdBy: row.createdBy,
24511
+ identitySource
24257
24512
  })
24258
24513
  );
24259
24514
  } else {
@@ -24270,11 +24525,9 @@ async function register(options, deps = {}) {
24270
24525
  }
24271
24526
  } catch (err) {
24272
24527
  const { code, message } = wrapProxyError(command, err);
24273
- if (options.json) {
24274
- emitJson8(buildErrorEnvelope(command, code, message));
24275
- } else {
24276
- error48(message);
24277
- }
24528
+ outputError({ json: options.json, command }, code, message, {
24529
+ logError: error48
24530
+ });
24278
24531
  exit(code);
24279
24532
  }
24280
24533
  }
@@ -24289,13 +24542,14 @@ async function rm2(options, deps = {}) {
24289
24542
  if (!options.slug || options.slug.trim().length === 0) {
24290
24543
  throw new UsageError("slug is required (positional argument)");
24291
24544
  }
24292
- const { client } = await setupClient(deps);
24545
+ const { client, identitySource } = await setupClient(deps);
24293
24546
  await client.deleteSite({ slug: options.slug });
24294
24547
  if (options.json) {
24295
24548
  emitJson8(
24296
24549
  buildEnvelope(command, true, {
24297
24550
  slug: options.slug,
24298
- deleted: true
24551
+ deleted: true,
24552
+ identitySource
24299
24553
  })
24300
24554
  );
24301
24555
  } else {
@@ -24310,11 +24564,9 @@ async function rm2(options, deps = {}) {
24310
24564
  }
24311
24565
  } catch (err) {
24312
24566
  const { code, message } = wrapProxyError(command, err);
24313
- if (options.json) {
24314
- emitJson8(buildErrorEnvelope(command, code, message));
24315
- } else {
24316
- error48(message);
24317
- }
24567
+ outputError({ json: options.json, command }, code, message, {
24568
+ logError: error48
24569
+ });
24318
24570
  exit(code);
24319
24571
  }
24320
24572
  }
@@ -24335,7 +24587,7 @@ async function update(options, deps = {}) {
24335
24587
  "--team is required with at least one slug; use `sites rm` to remove a site"
24336
24588
  );
24337
24589
  }
24338
- const { client } = await setupClient(deps);
24590
+ const { client, identitySource } = await setupClient(deps);
24339
24591
  const row = await client.updateSite({
24340
24592
  slug: options.slug,
24341
24593
  teams
@@ -24345,7 +24597,8 @@ async function update(options, deps = {}) {
24345
24597
  buildEnvelope(command, true, {
24346
24598
  slug: row.slug,
24347
24599
  teams: row.teams,
24348
- updatedAt: row.updatedAt
24600
+ updatedAt: row.updatedAt,
24601
+ identitySource
24349
24602
  })
24350
24603
  );
24351
24604
  } else {
@@ -24361,78 +24614,15 @@ async function update(options, deps = {}) {
24361
24614
  }
24362
24615
  } catch (err) {
24363
24616
  const { code, message } = wrapProxyError(command, err);
24364
- if (options.json) {
24365
- emitJson8(buildErrorEnvelope(command, code, message));
24366
- } else {
24367
- error48(message);
24368
- }
24617
+ outputError({ json: options.json, command }, code, message, {
24618
+ logError: error48
24619
+ });
24369
24620
  exit(code);
24370
24621
  }
24371
24622
  }
24372
24623
 
24373
- // src/output/redact.ts
24374
- var AWS_KEY_PREFIX_PATTERN = /(?:AKIA|ASIA|AROA|AIDA|ACCA|ANPA|ABIA|AGPA)[A-Z0-9]{12,}/g;
24375
- var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
24376
- var CREDENTIAL_CONTEXT_PATTERN = /(?:access_key_id|secret_access_key|accessKeyId|secretAccessKey|secret|password|token|key|credential|auth)\s*[=:]\s*([A-Za-z0-9/+=]{21,})/gi;
24377
- var HEX_CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key)\s*[=:]\s*([a-f0-9]{32,})/gi;
24378
- var JSON_CREDENTIAL_PATTERN = /"(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key|accessKeyId|secretAccessKey)"\s*:\s*"[^"]+"/gi;
24379
- var BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
24380
- function maskAwsKey(match) {
24381
- return match.slice(0, 4) + "****" + match.slice(-4);
24382
- }
24383
- function maskUrlCreds(match) {
24384
- const atIndex = match.lastIndexOf("@");
24385
- const protocolEnd = match.indexOf("://") + 3;
24386
- return match.slice(0, protocolEnd) + "****:****@" + match.slice(atIndex + 1);
24387
- }
24388
- function redact(value) {
24389
- let result = value;
24390
- result = result.replace(URL_CREDS_PATTERN, maskUrlCreds);
24391
- result = result.replace(AWS_KEY_PREFIX_PATTERN, maskAwsKey);
24392
- result = result.replace(BEARER_PATTERN, "Bearer ****");
24393
- result = result.replace(JSON_CREDENTIAL_PATTERN, (match) => {
24394
- const colonIndex = match.indexOf(":");
24395
- return match.slice(0, colonIndex + 1) + '"****"';
24396
- });
24397
- result = result.replace(
24398
- CREDENTIAL_CONTEXT_PATTERN,
24399
- (_match, _secret, _offset, _full) => {
24400
- const eqIndex = _match.indexOf("=");
24401
- const colonIndex = _match.indexOf(":");
24402
- const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
24403
- const prefix = _match.slice(0, sepIndex + 1);
24404
- return prefix + "****";
24405
- }
24406
- );
24407
- result = result.replace(HEX_CREDENTIAL_CONTEXT_PATTERN, (_match, _secret) => {
24408
- const eqIndex = _match.indexOf("=");
24409
- const colonIndex = _match.indexOf(":");
24410
- const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
24411
- const prefix = _match.slice(0, sepIndex + 1);
24412
- return prefix + "****";
24413
- });
24414
- return result;
24415
- }
24416
-
24417
- // src/output/format.ts
24418
- function outputError(ctx, code, message, issues) {
24419
- const redactedMessage = redact(message);
24420
- const redactedIssues = issues?.map(redact);
24421
- if (ctx.json) {
24422
- const envelope = buildErrorEnvelope(
24423
- ctx.command,
24424
- code,
24425
- redactedMessage,
24426
- redactedIssues
24427
- );
24428
- process.stdout.write(JSON.stringify(envelope) + "\n");
24429
- } else {
24430
- O2.error(redactedMessage);
24431
- }
24432
- }
24433
-
24434
24624
  // src/cli.ts
24435
- var version2 = true ? "0.6.0" : "0.0.0";
24625
+ var version2 = true ? "0.7.1" : "0.0.0";
24436
24626
  function handleActionError(command, json2, err) {
24437
24627
  const ctx = { json: json2, command };
24438
24628
  const message = err instanceof Error ? err.message : "unknown error";