@freecodecamp/universe-cli 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/index.cjs +305 -164
  2. package/dist/index.js +270 -204
  3. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -6,7 +6,7 @@ import { cac } from "cac";
6
6
  // src/commands/deploy.ts
7
7
  import { readFile as readFile2 } from "fs/promises";
8
8
  import { resolve as resolve2 } from "path";
9
- import { log } from "@clack/prompts";
9
+ import { log as log2, spinner } from "@clack/prompts";
10
10
 
11
11
  // src/output/exit-codes.ts
12
12
  var EXIT_USAGE = 10;
@@ -429,6 +429,14 @@ function suggest(target, candidates, threshold = 2) {
429
429
  }
430
430
 
431
431
  // src/lib/proxy-client.ts
432
+ var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
433
+ function parseFetchTimeoutMs(env) {
434
+ const raw = env["UNIVERSE_FETCH_TIMEOUT_MS"];
435
+ if (raw === void 0 || raw === "") return void 0;
436
+ const n = Number(raw);
437
+ if (!Number.isFinite(n) || n < 0) return void 0;
438
+ return Math.floor(n);
439
+ }
432
440
  var ProxyError = class extends CliError {
433
441
  exitCode;
434
442
  status;
@@ -506,6 +514,24 @@ function stripTrailingSlash(url) {
506
514
  function createProxyClient(cfg) {
507
515
  const base = stripTrailingSlash(cfg.baseUrl);
508
516
  const fetchImpl = cfg.fetch ?? globalThis.fetch.bind(globalThis);
517
+ const timeoutMs = cfg.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
518
+ function withTimeoutSignal(init) {
519
+ if (timeoutMs <= 0) return init;
520
+ const timeoutSignal = AbortSignal.timeout(timeoutMs);
521
+ const merged = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
522
+ return { ...init, signal: merged };
523
+ }
524
+ function translateFetchError(err) {
525
+ if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) {
526
+ throw new ProxyError(
527
+ 0,
528
+ "timeout",
529
+ `proxy timed out after ${timeoutMs}ms`
530
+ );
531
+ }
532
+ const message = err instanceof Error ? err.message : String(err);
533
+ throw new ProxyError(0, "network_error", `proxy unreachable: ${message}`);
534
+ }
509
535
  async function userBearer() {
510
536
  const tok = await cfg.getAuthToken();
511
537
  return `Bearer ${tok}`;
@@ -513,10 +539,9 @@ function createProxyClient(cfg) {
513
539
  async function call(url, init) {
514
540
  let response;
515
541
  try {
516
- response = await fetchImpl(url, init);
542
+ response = await fetchImpl(url, withTimeoutSignal(init));
517
543
  } catch (err) {
518
- const message = err instanceof Error ? err.message : String(err);
519
- throw new ProxyError(0, "network_error", `proxy unreachable: ${message}`);
544
+ translateFetchError(err);
520
545
  }
521
546
  if (!response.ok) {
522
547
  const env = await readErrorEnvelope(response);
@@ -588,20 +613,18 @@ function createProxyClient(cfg) {
588
613
  const url = `${base}/api/site/${encodeURIComponent(req.site)}/alias/${encodeURIComponent(req.mode)}`;
589
614
  let response;
590
615
  try {
591
- response = await fetchImpl(url, {
592
- method: "GET",
593
- headers: {
594
- Authorization: await userBearer(),
595
- Accept: "application/json"
596
- }
597
- });
598
- } catch (err) {
599
- const message = err instanceof Error ? err.message : String(err);
600
- throw new ProxyError(
601
- 0,
602
- "network_error",
603
- `proxy unreachable: ${message}`
616
+ response = await fetchImpl(
617
+ url,
618
+ withTimeoutSignal({
619
+ method: "GET",
620
+ headers: {
621
+ Authorization: await userBearer(),
622
+ Accept: "application/json"
623
+ }
624
+ })
604
625
  );
626
+ } catch (err) {
627
+ translateFetchError(err);
605
628
  }
606
629
  if (response.status === 404) return null;
607
630
  if (!response.ok) {
@@ -845,6 +868,106 @@ function buildErrorEnvelope(command, code, message, issues) {
845
868
  };
846
869
  }
847
870
 
871
+ // src/output/format.ts
872
+ import { log } from "@clack/prompts";
873
+
874
+ // src/output/redact.ts
875
+ var AWS_KEY_PREFIX_PATTERN = /(?:AKIA|ASIA|AROA|AIDA|ACCA|ANPA|ABIA|AGPA)[A-Z0-9]{12,}/g;
876
+ var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
877
+ 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;
878
+ var HEX_CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key)\s*[=:]\s*([a-f0-9]{32,})/gi;
879
+ var JSON_CREDENTIAL_PATTERN = /"(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key|accessKeyId|secretAccessKey)"\s*:\s*"[^"]+"/gi;
880
+ var BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
881
+ var CREDENTIAL_KEY_PATTERN = /(?:secret|password|token|key|credential|auth)/i;
882
+ var EXACT_CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
883
+ "accesskeyid",
884
+ "secretaccesskey",
885
+ "access_key_id",
886
+ "secret_access_key"
887
+ ]);
888
+ var STANDALONE_LONG_SECRET = /^[A-Za-z0-9/+=]{21,}$/;
889
+ function maskAwsKey(match) {
890
+ return match.slice(0, 4) + "****" + match.slice(-4);
891
+ }
892
+ function maskUrlCreds(match) {
893
+ const atIndex = match.lastIndexOf("@");
894
+ const protocolEnd = match.indexOf("://") + 3;
895
+ return match.slice(0, protocolEnd) + "****:****@" + match.slice(atIndex + 1);
896
+ }
897
+ function redact(value) {
898
+ let result = value;
899
+ result = result.replace(URL_CREDS_PATTERN, maskUrlCreds);
900
+ result = result.replace(AWS_KEY_PREFIX_PATTERN, maskAwsKey);
901
+ result = result.replace(BEARER_PATTERN, "Bearer ****");
902
+ result = result.replace(JSON_CREDENTIAL_PATTERN, (match) => {
903
+ const colonIndex = match.indexOf(":");
904
+ return match.slice(0, colonIndex + 1) + '"****"';
905
+ });
906
+ result = result.replace(
907
+ CREDENTIAL_CONTEXT_PATTERN,
908
+ (_match, _secret, _offset, _full) => {
909
+ const eqIndex = _match.indexOf("=");
910
+ const colonIndex = _match.indexOf(":");
911
+ const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
912
+ const prefix = _match.slice(0, sepIndex + 1);
913
+ return prefix + "****";
914
+ }
915
+ );
916
+ result = result.replace(HEX_CREDENTIAL_CONTEXT_PATTERN, (_match, _secret) => {
917
+ const eqIndex = _match.indexOf("=");
918
+ const colonIndex = _match.indexOf(":");
919
+ const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
920
+ const prefix = _match.slice(0, sepIndex + 1);
921
+ return prefix + "****";
922
+ });
923
+ return result;
924
+ }
925
+ function redactValue(value, keyName) {
926
+ if (typeof value === "string") {
927
+ if (keyName && EXACT_CREDENTIAL_KEYS.has(keyName.toLowerCase())) {
928
+ return "****";
929
+ }
930
+ const redacted = redact(value);
931
+ if (redacted === value && keyName && CREDENTIAL_KEY_PATTERN.test(keyName) && STANDALONE_LONG_SECRET.test(value)) {
932
+ return "****";
933
+ }
934
+ return redacted;
935
+ }
936
+ if (Array.isArray(value)) {
937
+ return value.map((v) => redactValue(v, keyName));
938
+ }
939
+ if (value !== null && typeof value === "object") {
940
+ return redactObject(value);
941
+ }
942
+ return value;
943
+ }
944
+ function redactObject(obj) {
945
+ const result = {};
946
+ for (const [key, value] of Object.entries(obj)) {
947
+ result[key] = redactValue(value, key);
948
+ }
949
+ return result;
950
+ }
951
+
952
+ // src/output/format.ts
953
+ function outputError(ctx, code, message, optsOrIssues) {
954
+ const opts = Array.isArray(optsOrIssues) ? { issues: optsOrIssues } : optsOrIssues ?? {};
955
+ const redactedMessage = redact(message);
956
+ const redactedIssues = opts.issues?.map(redact);
957
+ if (ctx.json) {
958
+ const envelope = buildErrorEnvelope(
959
+ ctx.command,
960
+ code,
961
+ redactedMessage,
962
+ redactedIssues
963
+ );
964
+ const payload = opts.extras ? { ...envelope, ...redactObject(opts.extras) } : envelope;
965
+ process.stdout.write(JSON.stringify(payload) + "\n");
966
+ } else {
967
+ (opts.logError ?? ((m) => log.error(m)))(redactedMessage);
968
+ }
969
+ }
970
+
848
971
  // src/commands/deploy.ts
849
972
  var defaultReadPlatformYaml = async (cwd) => {
850
973
  return readFile2(resolve2(cwd, "platform.yaml"), "utf-8");
@@ -938,10 +1061,11 @@ async function deploy(options, deps = {}) {
938
1061
  const build = deps.runBuild ?? runBuild;
939
1062
  const walk = deps.walkFiles ?? walkFiles;
940
1063
  const upload = deps.uploadFiles ?? uploadFiles;
941
- const success = deps.logSuccess ?? ((s) => log.success(s));
942
- const info = deps.logInfo ?? ((s) => log.info(s));
943
- const warn = deps.logWarn ?? ((s) => log.warn(s));
944
- const error = deps.logError ?? ((s) => log.error(s));
1064
+ const mkSpinner = deps.createSpinner ?? (() => spinner());
1065
+ const success = deps.logSuccess ?? ((s) => log2.success(s));
1066
+ const info = deps.logInfo ?? ((s) => log2.info(s));
1067
+ const warn = deps.logWarn ?? ((s) => log2.warn(s));
1068
+ const error = deps.logError ?? ((s) => log2.error(s));
945
1069
  const exit = deps.exit ?? exitWithCode;
946
1070
  try {
947
1071
  const identity = await resolveId({ env });
@@ -954,7 +1078,8 @@ async function deploy(options, deps = {}) {
954
1078
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
955
1079
  const client = mkClient({
956
1080
  baseUrl,
957
- getAuthToken: () => identity.token
1081
+ getAuthToken: () => identity.token,
1082
+ timeoutMs: parseFetchTimeoutMs(env)
958
1083
  });
959
1084
  let me;
960
1085
  try {
@@ -1005,17 +1130,22 @@ async function deploy(options, deps = {}) {
1005
1130
  } catch (err) {
1006
1131
  rethrowProxy("deploy init failed", err);
1007
1132
  }
1133
+ const spin = options.json ? null : mkSpinner();
1134
+ spin?.start(`Uploading 0/${filtered.length} files`);
1008
1135
  const uploadResult = await upload({
1009
1136
  client,
1010
1137
  deployId: initResult.deployId,
1011
1138
  jwt: initResult.jwt,
1012
- files: filtered
1139
+ files: filtered,
1140
+ onProgress: spin ? (p) => spin.message(`Uploading ${p.uploaded}/${p.total} \u2014 ${p.current}`) : void 0
1013
1141
  });
1014
1142
  if (uploadResult.errors.length > 0) {
1143
+ spin?.error(`Upload failed: ${uploadResult.errors.length} file(s)`);
1015
1144
  const message = `Upload partially failed: ${uploadResult.errors.length} file(s) failed:
1016
1145
  - ${uploadResult.errors.join("\n - ")}`;
1017
1146
  throw new PartialUploadError(message);
1018
1147
  }
1148
+ spin?.stop(`Uploaded ${uploadResult.fileCount} files`);
1019
1149
  const mode = options.promote ? "production" : "preview";
1020
1150
  let finalizeResult;
1021
1151
  try {
@@ -1028,6 +1158,25 @@ async function deploy(options, deps = {}) {
1028
1158
  } catch (err) {
1029
1159
  rethrowProxy("deploy finalize failed", err);
1030
1160
  }
1161
+ if (options.promote && !options.json) {
1162
+ try {
1163
+ const preview = await client.getAlias({
1164
+ site: config.site,
1165
+ mode: "preview"
1166
+ });
1167
+ if (preview && preview.deployId !== finalizeResult.deployId) {
1168
+ warn(
1169
+ `Preview alias still points to ${preview.deployId}; it will not auto-update. Run \`universe static deploy\` (without --promote) to refresh preview.`
1170
+ );
1171
+ }
1172
+ } catch (err) {
1173
+ if (err instanceof ProxyError && (err.status === 401 || err.status === 403)) {
1174
+ warn(
1175
+ `Preview alias probe got ${err.status} (${err.code}) \u2014 token may need rotation: ${err.message}`
1176
+ );
1177
+ }
1178
+ }
1179
+ }
1031
1180
  if (options.json) {
1032
1181
  emitJson(
1033
1182
  buildEnvelope("deploy", true, {
@@ -1074,17 +1223,15 @@ async function deploy(options, deps = {}) {
1074
1223
  code = EXIT_USAGE;
1075
1224
  message = String(err);
1076
1225
  }
1077
- if (options.json) {
1078
- emitJson(buildErrorEnvelope("deploy", code, message));
1079
- } else {
1080
- error(message);
1081
- }
1226
+ outputError({ json: options.json, command: "deploy" }, code, message, {
1227
+ logError: error
1228
+ });
1082
1229
  exit(code);
1083
1230
  }
1084
1231
  }
1085
1232
 
1086
1233
  // src/commands/login.ts
1087
- import { log as log2 } from "@clack/prompts";
1234
+ import { log as log3 } from "@clack/prompts";
1088
1235
 
1089
1236
  // src/lib/device-flow.ts
1090
1237
  var DEVICE_CODE_URL = "https://github.com/login/device/code";
@@ -1169,9 +1316,9 @@ async function login(options, deps = {}) {
1169
1316
  const runFlow = deps.runDeviceFlow ?? runDeviceFlow;
1170
1317
  const save = deps.saveToken ?? saveToken;
1171
1318
  const load = deps.loadToken ?? loadToken;
1172
- const success = deps.logSuccess ?? ((s) => log2.success(s));
1173
- const info = deps.logInfo ?? ((s) => log2.info(s));
1174
- const error = deps.logError ?? ((s) => log2.error(s));
1319
+ const success = deps.logSuccess ?? ((s) => log3.success(s));
1320
+ const info = deps.logInfo ?? ((s) => log3.info(s));
1321
+ const error = deps.logError ?? ((s) => log3.error(s));
1175
1322
  const exit = deps.exit ?? exitWithCode;
1176
1323
  const envClientId = env["UNIVERSE_GH_CLIENT_ID"];
1177
1324
  const clientId = envClientId && envClientId.trim().length > 0 ? envClientId : DEFAULT_GH_CLIENT_ID;
@@ -1222,17 +1369,12 @@ async function login(options, deps = {}) {
1222
1369
  });
1223
1370
  } catch (err) {
1224
1371
  const message = err instanceof Error ? err.message : String(err);
1225
- if (options.json) {
1226
- emitJson2({
1227
- schemaVersion: "1",
1228
- command: "login",
1229
- success: false,
1230
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1231
- error: { code: EXIT_CREDENTIALS, message }
1232
- });
1233
- } else {
1234
- error(message);
1235
- }
1372
+ outputError(
1373
+ { json: options.json, command: "login" },
1374
+ EXIT_CREDENTIALS,
1375
+ message,
1376
+ { logError: error }
1377
+ );
1236
1378
  exit(EXIT_CREDENTIALS);
1237
1379
  return;
1238
1380
  }
@@ -1245,15 +1387,15 @@ async function login(options, deps = {}) {
1245
1387
  }
1246
1388
 
1247
1389
  // src/commands/logout.ts
1248
- import { log as log3 } from "@clack/prompts";
1390
+ import { log as log4 } from "@clack/prompts";
1249
1391
  function emitJson3(envelope) {
1250
1392
  process.stdout.write(JSON.stringify(envelope) + "\n");
1251
1393
  }
1252
1394
  async function logout(options, deps = {}) {
1253
1395
  const load = deps.loadToken ?? loadToken;
1254
1396
  const del = deps.deleteToken ?? deleteToken;
1255
- const success = deps.logSuccess ?? ((s) => log3.success(s));
1256
- const info = deps.logInfo ?? ((s) => log3.info(s));
1397
+ const success = deps.logSuccess ?? ((s) => log4.success(s));
1398
+ const info = deps.logInfo ?? ((s) => log4.info(s));
1257
1399
  const existing = await load();
1258
1400
  await del();
1259
1401
  if (options.json) {
@@ -1270,7 +1412,7 @@ async function logout(options, deps = {}) {
1270
1412
  // src/commands/ls.ts
1271
1413
  import { readFile as readFile3 } from "fs/promises";
1272
1414
  import { resolve as resolve3 } from "path";
1273
- import { log as log4 } from "@clack/prompts";
1415
+ import { log as log5 } from "@clack/prompts";
1274
1416
  var defaultReadPlatformYaml2 = async (cwd) => {
1275
1417
  return readFile3(resolve3(cwd, "platform.yaml"), "utf-8");
1276
1418
  };
@@ -1320,9 +1462,9 @@ async function ls(options, deps = {}) {
1320
1462
  const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml2;
1321
1463
  const resolveId = deps.resolveIdentity ?? resolveIdentity;
1322
1464
  const mkClient = deps.createProxyClient ?? createProxyClient;
1323
- const success = deps.logSuccess ?? ((s) => log4.success(s));
1324
- const info = deps.logInfo ?? ((s) => log4.info(s));
1325
- const error = deps.logError ?? ((s) => log4.error(s));
1465
+ const success = deps.logSuccess ?? ((s) => log5.success(s));
1466
+ const info = deps.logInfo ?? ((s) => log5.info(s));
1467
+ const error = deps.logError ?? ((s) => log5.error(s));
1326
1468
  const exit = deps.exit ?? exitWithCode;
1327
1469
  try {
1328
1470
  const identity = await resolveId({ env });
@@ -1343,7 +1485,8 @@ async function ls(options, deps = {}) {
1343
1485
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1344
1486
  const client = mkClient({
1345
1487
  baseUrl,
1346
- getAuthToken: () => identity.token
1488
+ getAuthToken: () => identity.token,
1489
+ timeoutMs: parseFetchTimeoutMs(env)
1347
1490
  });
1348
1491
  const raw = await client.siteDeploys({ site });
1349
1492
  const sorted = [...raw].sort(
@@ -1367,11 +1510,9 @@ async function ls(options, deps = {}) {
1367
1510
  success(formatTable(deploys));
1368
1511
  } catch (err) {
1369
1512
  const { code, message } = wrapProxyError("ls", err);
1370
- if (options.json) {
1371
- emitJson4(buildErrorEnvelope("ls", code, message));
1372
- } else {
1373
- error(message);
1374
- }
1513
+ outputError({ json: options.json, command: "ls" }, code, message, {
1514
+ logError: error
1515
+ });
1375
1516
  exit(code);
1376
1517
  }
1377
1518
  }
@@ -1379,7 +1520,7 @@ async function ls(options, deps = {}) {
1379
1520
  // src/commands/promote.ts
1380
1521
  import { readFile as readFile4 } from "fs/promises";
1381
1522
  import { resolve as resolve4 } from "path";
1382
- import { confirm, isCancel, log as log5 } from "@clack/prompts";
1523
+ import { confirm, isCancel, log as log6 } from "@clack/prompts";
1383
1524
  var defaultPromptConfirm = async (msg) => {
1384
1525
  const r = await confirm({ message: msg, initialValue: false });
1385
1526
  if (isCancel(r)) return false;
@@ -1413,8 +1554,8 @@ async function promote(options, deps = {}) {
1413
1554
  const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml3;
1414
1555
  const resolveId = deps.resolveIdentity ?? resolveIdentity;
1415
1556
  const mkClient = deps.createProxyClient ?? createProxyClient;
1416
- const success = deps.logSuccess ?? ((s) => log5.success(s));
1417
- const error = deps.logError ?? ((s) => log5.error(s));
1557
+ const success = deps.logSuccess ?? ((s) => log6.success(s));
1558
+ const error = deps.logError ?? ((s) => log6.error(s));
1418
1559
  const exit = deps.exit ?? exitWithCode;
1419
1560
  const promptConfirm = deps.promptConfirm ?? defaultPromptConfirm;
1420
1561
  try {
@@ -1428,7 +1569,8 @@ async function promote(options, deps = {}) {
1428
1569
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1429
1570
  const client = mkClient({
1430
1571
  baseUrl,
1431
- getAuthToken: () => identity.token
1572
+ getAuthToken: () => identity.token,
1573
+ timeoutMs: parseFetchTimeoutMs(env)
1432
1574
  });
1433
1575
  let result;
1434
1576
  if (options.from) {
@@ -1526,16 +1668,11 @@ async function promote(options, deps = {}) {
1526
1668
  }
1527
1669
  } catch (err) {
1528
1670
  const { code, message } = wrapProxyError("promote", err);
1529
- if (options.json) {
1530
- const envelope = buildErrorEnvelope("promote", code, message);
1531
- if (err instanceof AliasDriftError) {
1532
- emitJson5({ ...envelope, current: err.current });
1533
- } else {
1534
- emitJson5(envelope);
1535
- }
1536
- } else {
1537
- error(message);
1538
- }
1671
+ const extras = err instanceof AliasDriftError ? { current: err.current } : void 0;
1672
+ outputError({ json: options.json, command: "promote" }, code, message, {
1673
+ logError: error,
1674
+ extras
1675
+ });
1539
1676
  exit(code);
1540
1677
  }
1541
1678
  }
@@ -1543,7 +1680,7 @@ async function promote(options, deps = {}) {
1543
1680
  // src/commands/rollback.ts
1544
1681
  import { readFile as readFile5 } from "fs/promises";
1545
1682
  import { resolve as resolve5 } from "path";
1546
- import { confirm as confirm2, isCancel as isCancel2, log as log6 } from "@clack/prompts";
1683
+ import { confirm as confirm2, isCancel as isCancel2, log as log7 } from "@clack/prompts";
1547
1684
  var defaultPromptConfirm2 = async (msg) => {
1548
1685
  const r = await confirm2({ message: msg, initialValue: false });
1549
1686
  if (isCancel2(r)) return false;
@@ -1577,8 +1714,8 @@ async function rollback(options, deps = {}) {
1577
1714
  const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml4;
1578
1715
  const resolveId = deps.resolveIdentity ?? resolveIdentity;
1579
1716
  const mkClient = deps.createProxyClient ?? createProxyClient;
1580
- const success = deps.logSuccess ?? ((s) => log6.success(s));
1581
- const error = deps.logError ?? ((s) => log6.error(s));
1717
+ const success = deps.logSuccess ?? ((s) => log7.success(s));
1718
+ const error = deps.logError ?? ((s) => log7.error(s));
1582
1719
  const exit = deps.exit ?? exitWithCode;
1583
1720
  const promptConfirm = deps.promptConfirm ?? defaultPromptConfirm2;
1584
1721
  try {
@@ -1597,7 +1734,8 @@ async function rollback(options, deps = {}) {
1597
1734
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1598
1735
  const client = mkClient({
1599
1736
  baseUrl,
1600
- getAuthToken: () => identity.token
1737
+ getAuthToken: () => identity.token,
1738
+ timeoutMs: parseFetchTimeoutMs(env)
1601
1739
  });
1602
1740
  const to = options.to.trim();
1603
1741
  const prod = await client.getAlias({
@@ -1650,22 +1788,17 @@ async function rollback(options, deps = {}) {
1650
1788
  }
1651
1789
  } catch (err) {
1652
1790
  const { code, message } = wrapProxyError("rollback", err);
1653
- if (options.json) {
1654
- const envelope = buildErrorEnvelope("rollback", code, message);
1655
- if (err instanceof AliasDriftError) {
1656
- emitJson6({ ...envelope, current: err.current });
1657
- } else {
1658
- emitJson6(envelope);
1659
- }
1660
- } else {
1661
- error(message);
1662
- }
1791
+ const extras = err instanceof AliasDriftError ? { current: err.current } : void 0;
1792
+ outputError({ json: options.json, command: "rollback" }, code, message, {
1793
+ logError: error,
1794
+ extras
1795
+ });
1663
1796
  exit(code);
1664
1797
  }
1665
1798
  }
1666
1799
 
1667
1800
  // src/commands/whoami.ts
1668
- import { log as log7 } from "@clack/prompts";
1801
+ import { log as log8 } from "@clack/prompts";
1669
1802
  var DEFAULT_PROXY_URL2 = "https://uploads.freecode.camp";
1670
1803
  function emitJson7(envelope) {
1671
1804
  process.stdout.write(JSON.stringify(envelope) + "\n");
@@ -1674,24 +1807,26 @@ async function whoami(options, deps = {}) {
1674
1807
  const env = deps.env ?? process.env;
1675
1808
  const resolve6 = deps.resolveIdentity ?? resolveIdentity;
1676
1809
  const mkClient = deps.createProxyClient ?? createProxyClient;
1677
- const success = deps.logSuccess ?? ((s) => log7.success(s));
1678
- const error = deps.logError ?? ((s) => log7.error(s));
1810
+ const success = deps.logSuccess ?? ((s) => log8.success(s));
1811
+ const error = deps.logError ?? ((s) => log8.error(s));
1679
1812
  const exit = deps.exit ?? exitWithCode;
1680
1813
  const identity = await resolve6({ env });
1681
1814
  if (!identity) {
1682
1815
  const msg = "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI.";
1683
- if (options.json) {
1684
- emitJson7(buildErrorEnvelope("whoami", EXIT_CREDENTIALS, msg));
1685
- } else {
1686
- error(msg);
1687
- }
1816
+ outputError(
1817
+ { json: options.json, command: "whoami" },
1818
+ EXIT_CREDENTIALS,
1819
+ msg,
1820
+ { logError: error }
1821
+ );
1688
1822
  exit(EXIT_CREDENTIALS);
1689
1823
  return;
1690
1824
  }
1691
1825
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
1692
1826
  const client = mkClient({
1693
1827
  baseUrl,
1694
- getAuthToken: () => identity.token
1828
+ getAuthToken: () => identity.token,
1829
+ timeoutMs: parseFetchTimeoutMs(env)
1695
1830
  });
1696
1831
  try {
1697
1832
  const result = await client.whoami();
@@ -1717,17 +1852,15 @@ async function whoami(options, deps = {}) {
1717
1852
  } catch (err) {
1718
1853
  const exitCode = err instanceof CliError ? err.exitCode : EXIT_CREDENTIALS;
1719
1854
  const message = err instanceof ProxyError ? `whoami failed (${err.code}): ${err.message}` : err instanceof Error ? err.message : String(err);
1720
- if (options.json) {
1721
- emitJson7(buildErrorEnvelope("whoami", exitCode, message));
1722
- } else {
1723
- error(message);
1724
- }
1855
+ outputError({ json: options.json, command: "whoami" }, exitCode, message, {
1856
+ logError: error
1857
+ });
1725
1858
  exit(exitCode);
1726
1859
  }
1727
1860
  }
1728
1861
 
1729
1862
  // src/commands/sites/ls.ts
1730
- import { log as log8 } from "@clack/prompts";
1863
+ import { log as log9 } from "@clack/prompts";
1731
1864
 
1732
1865
  // src/commands/sites/_shared.ts
1733
1866
  function emitJson8(envelope) {
@@ -1751,7 +1884,8 @@ async function setupClient(deps) {
1751
1884
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1752
1885
  const client = mkClient({
1753
1886
  baseUrl,
1754
- getAuthToken: () => identity.token
1887
+ getAuthToken: () => identity.token,
1888
+ timeoutMs: parseFetchTimeoutMs(env)
1755
1889
  });
1756
1890
  return { client, identitySource: identity.source };
1757
1891
  }
@@ -1774,11 +1908,11 @@ function formatTable2(rows) {
1774
1908
  }
1775
1909
  async function ls2(options, deps = {}) {
1776
1910
  const command = "sites ls";
1777
- const success = deps.logSuccess ?? ((s) => log8.message(s));
1778
- const error = deps.logError ?? ((s) => log8.error(s));
1911
+ const success = deps.logSuccess ?? ((s) => log9.message(s));
1912
+ const error = deps.logError ?? ((s) => log9.error(s));
1779
1913
  const exit = deps.exit ?? exitWithCode;
1780
1914
  try {
1781
- const { client } = await setupClient(deps);
1915
+ const { client, identitySource } = await setupClient(deps);
1782
1916
  let rows = await client.listSites();
1783
1917
  let scope = "all";
1784
1918
  if (options.mine) {
@@ -1792,7 +1926,8 @@ async function ls2(options, deps = {}) {
1792
1926
  buildEnvelope(command, true, {
1793
1927
  count: rows.length,
1794
1928
  scope,
1795
- sites: rows
1929
+ sites: rows,
1930
+ identitySource
1796
1931
  })
1797
1932
  );
1798
1933
  } else {
@@ -1800,28 +1935,26 @@ async function ls2(options, deps = {}) {
1800
1935
  }
1801
1936
  } catch (err) {
1802
1937
  const { code, message } = wrapProxyError(command, err);
1803
- if (options.json) {
1804
- emitJson8(buildErrorEnvelope(command, code, message));
1805
- } else {
1806
- error(message);
1807
- }
1938
+ outputError({ json: options.json, command }, code, message, {
1939
+ logError: error
1940
+ });
1808
1941
  exit(code);
1809
1942
  }
1810
1943
  }
1811
1944
 
1812
1945
  // src/commands/sites/register.ts
1813
- import { log as log9 } from "@clack/prompts";
1946
+ import { log as log10 } from "@clack/prompts";
1814
1947
  async function register(options, deps = {}) {
1815
1948
  const command = "sites register";
1816
- const success = deps.logSuccess ?? ((s) => log9.success(s));
1817
- const error = deps.logError ?? ((s) => log9.error(s));
1949
+ const success = deps.logSuccess ?? ((s) => log10.success(s));
1950
+ const error = deps.logError ?? ((s) => log10.error(s));
1818
1951
  const exit = deps.exit ?? exitWithCode;
1819
1952
  try {
1820
1953
  if (!options.slug || options.slug.trim().length === 0) {
1821
1954
  throw new UsageError("slug is required (positional argument)");
1822
1955
  }
1823
1956
  const teams = parseTeamsFlag(options.team);
1824
- const { client } = await setupClient(deps);
1957
+ const { client, identitySource } = await setupClient(deps);
1825
1958
  const row = await client.registerSite({
1826
1959
  slug: options.slug,
1827
1960
  teams: teams.length > 0 ? teams : void 0
@@ -1832,7 +1965,8 @@ async function register(options, deps = {}) {
1832
1965
  slug: row.slug,
1833
1966
  teams: row.teams,
1834
1967
  createdAt: row.createdAt,
1835
- createdBy: row.createdBy
1968
+ createdBy: row.createdBy,
1969
+ identitySource
1836
1970
  })
1837
1971
  );
1838
1972
  } else {
@@ -1849,33 +1983,32 @@ async function register(options, deps = {}) {
1849
1983
  }
1850
1984
  } catch (err) {
1851
1985
  const { code, message } = wrapProxyError(command, err);
1852
- if (options.json) {
1853
- emitJson8(buildErrorEnvelope(command, code, message));
1854
- } else {
1855
- error(message);
1856
- }
1986
+ outputError({ json: options.json, command }, code, message, {
1987
+ logError: error
1988
+ });
1857
1989
  exit(code);
1858
1990
  }
1859
1991
  }
1860
1992
 
1861
1993
  // src/commands/sites/rm.ts
1862
- import { log as log10 } from "@clack/prompts";
1994
+ import { log as log11 } from "@clack/prompts";
1863
1995
  async function rm2(options, deps = {}) {
1864
1996
  const command = "sites rm";
1865
- const success = deps.logSuccess ?? ((s) => log10.success(s));
1866
- const error = deps.logError ?? ((s) => log10.error(s));
1997
+ const success = deps.logSuccess ?? ((s) => log11.success(s));
1998
+ const error = deps.logError ?? ((s) => log11.error(s));
1867
1999
  const exit = deps.exit ?? exitWithCode;
1868
2000
  try {
1869
2001
  if (!options.slug || options.slug.trim().length === 0) {
1870
2002
  throw new UsageError("slug is required (positional argument)");
1871
2003
  }
1872
- const { client } = await setupClient(deps);
2004
+ const { client, identitySource } = await setupClient(deps);
1873
2005
  await client.deleteSite({ slug: options.slug });
1874
2006
  if (options.json) {
1875
2007
  emitJson8(
1876
2008
  buildEnvelope(command, true, {
1877
2009
  slug: options.slug,
1878
- deleted: true
2010
+ deleted: true,
2011
+ identitySource
1879
2012
  })
1880
2013
  );
1881
2014
  } else {
@@ -1890,21 +2023,19 @@ async function rm2(options, deps = {}) {
1890
2023
  }
1891
2024
  } catch (err) {
1892
2025
  const { code, message } = wrapProxyError(command, err);
1893
- if (options.json) {
1894
- emitJson8(buildErrorEnvelope(command, code, message));
1895
- } else {
1896
- error(message);
1897
- }
2026
+ outputError({ json: options.json, command }, code, message, {
2027
+ logError: error
2028
+ });
1898
2029
  exit(code);
1899
2030
  }
1900
2031
  }
1901
2032
 
1902
2033
  // src/commands/sites/update.ts
1903
- import { log as log11 } from "@clack/prompts";
2034
+ import { log as log12 } from "@clack/prompts";
1904
2035
  async function update(options, deps = {}) {
1905
2036
  const command = "sites update";
1906
- const success = deps.logSuccess ?? ((s) => log11.success(s));
1907
- const error = deps.logError ?? ((s) => log11.error(s));
2037
+ const success = deps.logSuccess ?? ((s) => log12.success(s));
2038
+ const error = deps.logError ?? ((s) => log12.error(s));
1908
2039
  const exit = deps.exit ?? exitWithCode;
1909
2040
  try {
1910
2041
  if (!options.slug || options.slug.trim().length === 0) {
@@ -1916,7 +2047,7 @@ async function update(options, deps = {}) {
1916
2047
  "--team is required with at least one slug; use `sites rm` to remove a site"
1917
2048
  );
1918
2049
  }
1919
- const { client } = await setupClient(deps);
2050
+ const { client, identitySource } = await setupClient(deps);
1920
2051
  const row = await client.updateSite({
1921
2052
  slug: options.slug,
1922
2053
  teams
@@ -1926,7 +2057,8 @@ async function update(options, deps = {}) {
1926
2057
  buildEnvelope(command, true, {
1927
2058
  slug: row.slug,
1928
2059
  teams: row.teams,
1929
- updatedAt: row.updatedAt
2060
+ updatedAt: row.updatedAt,
2061
+ identitySource
1930
2062
  })
1931
2063
  );
1932
2064
  } else {
@@ -1942,81 +2074,15 @@ async function update(options, deps = {}) {
1942
2074
  }
1943
2075
  } catch (err) {
1944
2076
  const { code, message } = wrapProxyError(command, err);
1945
- if (options.json) {
1946
- emitJson8(buildErrorEnvelope(command, code, message));
1947
- } else {
1948
- error(message);
1949
- }
2077
+ outputError({ json: options.json, command }, code, message, {
2078
+ logError: error
2079
+ });
1950
2080
  exit(code);
1951
2081
  }
1952
2082
  }
1953
2083
 
1954
- // src/output/format.ts
1955
- import { log as log12 } from "@clack/prompts";
1956
-
1957
- // src/output/redact.ts
1958
- var AWS_KEY_PREFIX_PATTERN = /(?:AKIA|ASIA|AROA|AIDA|ACCA|ANPA|ABIA|AGPA)[A-Z0-9]{12,}/g;
1959
- var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
1960
- 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;
1961
- var HEX_CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key)\s*[=:]\s*([a-f0-9]{32,})/gi;
1962
- var JSON_CREDENTIAL_PATTERN = /"(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key|accessKeyId|secretAccessKey)"\s*:\s*"[^"]+"/gi;
1963
- var BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
1964
- function maskAwsKey(match) {
1965
- return match.slice(0, 4) + "****" + match.slice(-4);
1966
- }
1967
- function maskUrlCreds(match) {
1968
- const atIndex = match.lastIndexOf("@");
1969
- const protocolEnd = match.indexOf("://") + 3;
1970
- return match.slice(0, protocolEnd) + "****:****@" + match.slice(atIndex + 1);
1971
- }
1972
- function redact(value) {
1973
- let result = value;
1974
- result = result.replace(URL_CREDS_PATTERN, maskUrlCreds);
1975
- result = result.replace(AWS_KEY_PREFIX_PATTERN, maskAwsKey);
1976
- result = result.replace(BEARER_PATTERN, "Bearer ****");
1977
- result = result.replace(JSON_CREDENTIAL_PATTERN, (match) => {
1978
- const colonIndex = match.indexOf(":");
1979
- return match.slice(0, colonIndex + 1) + '"****"';
1980
- });
1981
- result = result.replace(
1982
- CREDENTIAL_CONTEXT_PATTERN,
1983
- (_match, _secret, _offset, _full) => {
1984
- const eqIndex = _match.indexOf("=");
1985
- const colonIndex = _match.indexOf(":");
1986
- const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
1987
- const prefix = _match.slice(0, sepIndex + 1);
1988
- return prefix + "****";
1989
- }
1990
- );
1991
- result = result.replace(HEX_CREDENTIAL_CONTEXT_PATTERN, (_match, _secret) => {
1992
- const eqIndex = _match.indexOf("=");
1993
- const colonIndex = _match.indexOf(":");
1994
- const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
1995
- const prefix = _match.slice(0, sepIndex + 1);
1996
- return prefix + "****";
1997
- });
1998
- return result;
1999
- }
2000
-
2001
- // src/output/format.ts
2002
- function outputError(ctx, code, message, issues) {
2003
- const redactedMessage = redact(message);
2004
- const redactedIssues = issues?.map(redact);
2005
- if (ctx.json) {
2006
- const envelope = buildErrorEnvelope(
2007
- ctx.command,
2008
- code,
2009
- redactedMessage,
2010
- redactedIssues
2011
- );
2012
- process.stdout.write(JSON.stringify(envelope) + "\n");
2013
- } else {
2014
- log12.error(redactedMessage);
2015
- }
2016
- }
2017
-
2018
2084
  // src/cli.ts
2019
- var version = true ? "0.6.0" : "0.0.0";
2085
+ var version = true ? "0.7.0" : "0.0.0";
2020
2086
  function handleActionError(command, json, err) {
2021
2087
  const ctx = { json, command };
2022
2088
  const message = err instanceof Error ? err.message : "unknown error";