@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.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;
@@ -221,14 +221,14 @@ async function resolveIdentity(opts = {}) {
221
221
  if (isNonEmpty(ghTokenEnv)) {
222
222
  return { token: ghTokenEnv.trim(), source: "env_GH_TOKEN" };
223
223
  }
224
- const ghCli = await execGh();
225
- if (isNonEmpty(ghCli)) {
226
- return { token: ghCli.trim(), source: "gh_cli" };
227
- }
228
224
  const stored = await loadStored();
229
225
  if (isNonEmpty(stored)) {
230
226
  return { token: stored.trim(), source: "device_flow" };
231
227
  }
228
+ const ghCli = await execGh();
229
+ if (isNonEmpty(ghCli)) {
230
+ return { token: ghCli.trim(), source: "gh_cli" };
231
+ }
232
232
  return null;
233
233
  }
234
234
 
@@ -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";
@@ -1161,6 +1308,19 @@ async function runDeviceFlow(opts) {
1161
1308
 
1162
1309
  // src/commands/login.ts
1163
1310
  var DEFAULT_SCOPE = "read:org user:email";
1311
+ var DEFAULT_PROXY_URL2 = "https://uploads.freecode.camp";
1312
+ var NO_SITES_WARNING = [
1313
+ "Logged in, but the proxy reports 0 authorized sites for your account.",
1314
+ "This usually means the Universe CLI GitHub App is not installed on the org",
1315
+ "that owns the registry-authz team (production: `freeCodeCamp-Universe`), or",
1316
+ "your account is not on a team granted access to any site.",
1317
+ "",
1318
+ "Next steps:",
1319
+ " 1. Run `universe whoami` to confirm the identity that resolved.",
1320
+ " 2. Ask an org owner to install the Universe CLI GitHub App on the org.",
1321
+ " 3. Confirm your team membership at",
1322
+ " https://github.com/orgs/freeCodeCamp-Universe/teams."
1323
+ ].join("\n");
1164
1324
  function emitJson2(envelope) {
1165
1325
  process.stdout.write(JSON.stringify(envelope) + "\n");
1166
1326
  }
@@ -1169,9 +1329,9 @@ async function login(options, deps = {}) {
1169
1329
  const runFlow = deps.runDeviceFlow ?? runDeviceFlow;
1170
1330
  const save = deps.saveToken ?? saveToken;
1171
1331
  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));
1332
+ const success = deps.logSuccess ?? ((s) => log3.success(s));
1333
+ const info = deps.logInfo ?? ((s) => log3.info(s));
1334
+ const error = deps.logError ?? ((s) => log3.error(s));
1175
1335
  const exit = deps.exit ?? exitWithCode;
1176
1336
  const envClientId = env["UNIVERSE_GH_CLIENT_ID"];
1177
1337
  const clientId = envClientId && envClientId.trim().length > 0 ? envClientId : DEFAULT_GH_CLIENT_ID;
@@ -1222,38 +1382,69 @@ async function login(options, deps = {}) {
1222
1382
  });
1223
1383
  } catch (err) {
1224
1384
  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
- }
1385
+ outputError(
1386
+ { json: options.json, command: "login" },
1387
+ EXIT_CREDENTIALS,
1388
+ message,
1389
+ { logError: error }
1390
+ );
1236
1391
  exit(EXIT_CREDENTIALS);
1237
1392
  return;
1238
1393
  }
1239
1394
  await save(token);
1395
+ const selfCheck = await postLoginSelfCheck(token, env, deps);
1240
1396
  if (options.json) {
1241
- emitJson2(buildEnvelope("login", true, { stored: true }));
1397
+ emitJson2(
1398
+ buildEnvelope("login", true, {
1399
+ stored: true,
1400
+ ...selfCheck.checked ? {
1401
+ authorizedSitesCount: selfCheck.authorizedSitesCount,
1402
+ ...selfCheck.warning ? { warning: selfCheck.warning } : {}
1403
+ } : {}
1404
+ })
1405
+ );
1242
1406
  } else {
1243
1407
  success("Logged in. Token stored at ~/.config/universe-cli/token.");
1408
+ if (selfCheck.checked && selfCheck.warning) {
1409
+ const warn = deps.logWarn ?? ((s) => log3.warn(s));
1410
+ warn(selfCheck.warning);
1411
+ }
1412
+ }
1413
+ }
1414
+ async function postLoginSelfCheck(token, env, deps) {
1415
+ const mkClient = deps.createProxyClient ?? createProxyClient;
1416
+ try {
1417
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
1418
+ const client = mkClient({
1419
+ baseUrl,
1420
+ getAuthToken: () => token,
1421
+ timeoutMs: parseFetchTimeoutMs(env)
1422
+ });
1423
+ const result = await client.whoami();
1424
+ const count = result.authorizedSites.length;
1425
+ if (count === 0) {
1426
+ return {
1427
+ checked: true,
1428
+ authorizedSitesCount: 0,
1429
+ warning: NO_SITES_WARNING
1430
+ };
1431
+ }
1432
+ return { checked: true, authorizedSitesCount: count };
1433
+ } catch {
1434
+ return { checked: false, authorizedSitesCount: 0 };
1244
1435
  }
1245
1436
  }
1246
1437
 
1247
1438
  // src/commands/logout.ts
1248
- import { log as log3 } from "@clack/prompts";
1439
+ import { log as log4 } from "@clack/prompts";
1249
1440
  function emitJson3(envelope) {
1250
1441
  process.stdout.write(JSON.stringify(envelope) + "\n");
1251
1442
  }
1252
1443
  async function logout(options, deps = {}) {
1253
1444
  const load = deps.loadToken ?? loadToken;
1254
1445
  const del = deps.deleteToken ?? deleteToken;
1255
- const success = deps.logSuccess ?? ((s) => log3.success(s));
1256
- const info = deps.logInfo ?? ((s) => log3.info(s));
1446
+ const success = deps.logSuccess ?? ((s) => log4.success(s));
1447
+ const info = deps.logInfo ?? ((s) => log4.info(s));
1257
1448
  const existing = await load();
1258
1449
  await del();
1259
1450
  if (options.json) {
@@ -1270,7 +1461,7 @@ async function logout(options, deps = {}) {
1270
1461
  // src/commands/ls.ts
1271
1462
  import { readFile as readFile3 } from "fs/promises";
1272
1463
  import { resolve as resolve3 } from "path";
1273
- import { log as log4 } from "@clack/prompts";
1464
+ import { log as log5 } from "@clack/prompts";
1274
1465
  var defaultReadPlatformYaml2 = async (cwd) => {
1275
1466
  return readFile3(resolve3(cwd, "platform.yaml"), "utf-8");
1276
1467
  };
@@ -1320,9 +1511,9 @@ async function ls(options, deps = {}) {
1320
1511
  const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml2;
1321
1512
  const resolveId = deps.resolveIdentity ?? resolveIdentity;
1322
1513
  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));
1514
+ const success = deps.logSuccess ?? ((s) => log5.success(s));
1515
+ const info = deps.logInfo ?? ((s) => log5.info(s));
1516
+ const error = deps.logError ?? ((s) => log5.error(s));
1326
1517
  const exit = deps.exit ?? exitWithCode;
1327
1518
  try {
1328
1519
  const identity = await resolveId({ env });
@@ -1343,7 +1534,8 @@ async function ls(options, deps = {}) {
1343
1534
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1344
1535
  const client = mkClient({
1345
1536
  baseUrl,
1346
- getAuthToken: () => identity.token
1537
+ getAuthToken: () => identity.token,
1538
+ timeoutMs: parseFetchTimeoutMs(env)
1347
1539
  });
1348
1540
  const raw = await client.siteDeploys({ site });
1349
1541
  const sorted = [...raw].sort(
@@ -1367,11 +1559,9 @@ async function ls(options, deps = {}) {
1367
1559
  success(formatTable(deploys));
1368
1560
  } catch (err) {
1369
1561
  const { code, message } = wrapProxyError("ls", err);
1370
- if (options.json) {
1371
- emitJson4(buildErrorEnvelope("ls", code, message));
1372
- } else {
1373
- error(message);
1374
- }
1562
+ outputError({ json: options.json, command: "ls" }, code, message, {
1563
+ logError: error
1564
+ });
1375
1565
  exit(code);
1376
1566
  }
1377
1567
  }
@@ -1379,7 +1569,7 @@ async function ls(options, deps = {}) {
1379
1569
  // src/commands/promote.ts
1380
1570
  import { readFile as readFile4 } from "fs/promises";
1381
1571
  import { resolve as resolve4 } from "path";
1382
- import { confirm, isCancel, log as log5 } from "@clack/prompts";
1572
+ import { confirm, isCancel, log as log6 } from "@clack/prompts";
1383
1573
  var defaultPromptConfirm = async (msg) => {
1384
1574
  const r = await confirm({ message: msg, initialValue: false });
1385
1575
  if (isCancel(r)) return false;
@@ -1413,8 +1603,8 @@ async function promote(options, deps = {}) {
1413
1603
  const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml3;
1414
1604
  const resolveId = deps.resolveIdentity ?? resolveIdentity;
1415
1605
  const mkClient = deps.createProxyClient ?? createProxyClient;
1416
- const success = deps.logSuccess ?? ((s) => log5.success(s));
1417
- const error = deps.logError ?? ((s) => log5.error(s));
1606
+ const success = deps.logSuccess ?? ((s) => log6.success(s));
1607
+ const error = deps.logError ?? ((s) => log6.error(s));
1418
1608
  const exit = deps.exit ?? exitWithCode;
1419
1609
  const promptConfirm = deps.promptConfirm ?? defaultPromptConfirm;
1420
1610
  try {
@@ -1428,7 +1618,8 @@ async function promote(options, deps = {}) {
1428
1618
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1429
1619
  const client = mkClient({
1430
1620
  baseUrl,
1431
- getAuthToken: () => identity.token
1621
+ getAuthToken: () => identity.token,
1622
+ timeoutMs: parseFetchTimeoutMs(env)
1432
1623
  });
1433
1624
  let result;
1434
1625
  if (options.from) {
@@ -1526,16 +1717,11 @@ async function promote(options, deps = {}) {
1526
1717
  }
1527
1718
  } catch (err) {
1528
1719
  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
- }
1720
+ const extras = err instanceof AliasDriftError ? { current: err.current } : void 0;
1721
+ outputError({ json: options.json, command: "promote" }, code, message, {
1722
+ logError: error,
1723
+ extras
1724
+ });
1539
1725
  exit(code);
1540
1726
  }
1541
1727
  }
@@ -1543,7 +1729,7 @@ async function promote(options, deps = {}) {
1543
1729
  // src/commands/rollback.ts
1544
1730
  import { readFile as readFile5 } from "fs/promises";
1545
1731
  import { resolve as resolve5 } from "path";
1546
- import { confirm as confirm2, isCancel as isCancel2, log as log6 } from "@clack/prompts";
1732
+ import { confirm as confirm2, isCancel as isCancel2, log as log7 } from "@clack/prompts";
1547
1733
  var defaultPromptConfirm2 = async (msg) => {
1548
1734
  const r = await confirm2({ message: msg, initialValue: false });
1549
1735
  if (isCancel2(r)) return false;
@@ -1577,8 +1763,8 @@ async function rollback(options, deps = {}) {
1577
1763
  const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml4;
1578
1764
  const resolveId = deps.resolveIdentity ?? resolveIdentity;
1579
1765
  const mkClient = deps.createProxyClient ?? createProxyClient;
1580
- const success = deps.logSuccess ?? ((s) => log6.success(s));
1581
- const error = deps.logError ?? ((s) => log6.error(s));
1766
+ const success = deps.logSuccess ?? ((s) => log7.success(s));
1767
+ const error = deps.logError ?? ((s) => log7.error(s));
1582
1768
  const exit = deps.exit ?? exitWithCode;
1583
1769
  const promptConfirm = deps.promptConfirm ?? defaultPromptConfirm2;
1584
1770
  try {
@@ -1597,7 +1783,8 @@ async function rollback(options, deps = {}) {
1597
1783
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1598
1784
  const client = mkClient({
1599
1785
  baseUrl,
1600
- getAuthToken: () => identity.token
1786
+ getAuthToken: () => identity.token,
1787
+ timeoutMs: parseFetchTimeoutMs(env)
1601
1788
  });
1602
1789
  const to = options.to.trim();
1603
1790
  const prod = await client.getAlias({
@@ -1650,23 +1837,18 @@ async function rollback(options, deps = {}) {
1650
1837
  }
1651
1838
  } catch (err) {
1652
1839
  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
- }
1840
+ const extras = err instanceof AliasDriftError ? { current: err.current } : void 0;
1841
+ outputError({ json: options.json, command: "rollback" }, code, message, {
1842
+ logError: error,
1843
+ extras
1844
+ });
1663
1845
  exit(code);
1664
1846
  }
1665
1847
  }
1666
1848
 
1667
1849
  // src/commands/whoami.ts
1668
- import { log as log7 } from "@clack/prompts";
1669
- var DEFAULT_PROXY_URL2 = "https://uploads.freecode.camp";
1850
+ import { log as log8 } from "@clack/prompts";
1851
+ var DEFAULT_PROXY_URL3 = "https://uploads.freecode.camp";
1670
1852
  function emitJson7(envelope) {
1671
1853
  process.stdout.write(JSON.stringify(envelope) + "\n");
1672
1854
  }
@@ -1674,24 +1856,26 @@ async function whoami(options, deps = {}) {
1674
1856
  const env = deps.env ?? process.env;
1675
1857
  const resolve6 = deps.resolveIdentity ?? resolveIdentity;
1676
1858
  const mkClient = deps.createProxyClient ?? createProxyClient;
1677
- const success = deps.logSuccess ?? ((s) => log7.success(s));
1678
- const error = deps.logError ?? ((s) => log7.error(s));
1859
+ const success = deps.logSuccess ?? ((s) => log8.success(s));
1860
+ const error = deps.logError ?? ((s) => log8.error(s));
1679
1861
  const exit = deps.exit ?? exitWithCode;
1680
1862
  const identity = await resolve6({ env });
1681
1863
  if (!identity) {
1682
1864
  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
- }
1865
+ outputError(
1866
+ { json: options.json, command: "whoami" },
1867
+ EXIT_CREDENTIALS,
1868
+ msg,
1869
+ { logError: error }
1870
+ );
1688
1871
  exit(EXIT_CREDENTIALS);
1689
1872
  return;
1690
1873
  }
1691
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
1874
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL3;
1692
1875
  const client = mkClient({
1693
1876
  baseUrl,
1694
- getAuthToken: () => identity.token
1877
+ getAuthToken: () => identity.token,
1878
+ timeoutMs: parseFetchTimeoutMs(env)
1695
1879
  });
1696
1880
  try {
1697
1881
  const result = await client.whoami();
@@ -1717,17 +1901,15 @@ async function whoami(options, deps = {}) {
1717
1901
  } catch (err) {
1718
1902
  const exitCode = err instanceof CliError ? err.exitCode : EXIT_CREDENTIALS;
1719
1903
  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
- }
1904
+ outputError({ json: options.json, command: "whoami" }, exitCode, message, {
1905
+ logError: error
1906
+ });
1725
1907
  exit(exitCode);
1726
1908
  }
1727
1909
  }
1728
1910
 
1729
1911
  // src/commands/sites/ls.ts
1730
- import { log as log8 } from "@clack/prompts";
1912
+ import { log as log9 } from "@clack/prompts";
1731
1913
 
1732
1914
  // src/commands/sites/_shared.ts
1733
1915
  function emitJson8(envelope) {
@@ -1751,7 +1933,8 @@ async function setupClient(deps) {
1751
1933
  const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1752
1934
  const client = mkClient({
1753
1935
  baseUrl,
1754
- getAuthToken: () => identity.token
1936
+ getAuthToken: () => identity.token,
1937
+ timeoutMs: parseFetchTimeoutMs(env)
1755
1938
  });
1756
1939
  return { client, identitySource: identity.source };
1757
1940
  }
@@ -1774,11 +1957,11 @@ function formatTable2(rows) {
1774
1957
  }
1775
1958
  async function ls2(options, deps = {}) {
1776
1959
  const command = "sites ls";
1777
- const success = deps.logSuccess ?? ((s) => log8.message(s));
1778
- const error = deps.logError ?? ((s) => log8.error(s));
1960
+ const success = deps.logSuccess ?? ((s) => log9.message(s));
1961
+ const error = deps.logError ?? ((s) => log9.error(s));
1779
1962
  const exit = deps.exit ?? exitWithCode;
1780
1963
  try {
1781
- const { client } = await setupClient(deps);
1964
+ const { client, identitySource } = await setupClient(deps);
1782
1965
  let rows = await client.listSites();
1783
1966
  let scope = "all";
1784
1967
  if (options.mine) {
@@ -1792,7 +1975,8 @@ async function ls2(options, deps = {}) {
1792
1975
  buildEnvelope(command, true, {
1793
1976
  count: rows.length,
1794
1977
  scope,
1795
- sites: rows
1978
+ sites: rows,
1979
+ identitySource
1796
1980
  })
1797
1981
  );
1798
1982
  } else {
@@ -1800,28 +1984,26 @@ async function ls2(options, deps = {}) {
1800
1984
  }
1801
1985
  } catch (err) {
1802
1986
  const { code, message } = wrapProxyError(command, err);
1803
- if (options.json) {
1804
- emitJson8(buildErrorEnvelope(command, code, message));
1805
- } else {
1806
- error(message);
1807
- }
1987
+ outputError({ json: options.json, command }, code, message, {
1988
+ logError: error
1989
+ });
1808
1990
  exit(code);
1809
1991
  }
1810
1992
  }
1811
1993
 
1812
1994
  // src/commands/sites/register.ts
1813
- import { log as log9 } from "@clack/prompts";
1995
+ import { log as log10 } from "@clack/prompts";
1814
1996
  async function register(options, deps = {}) {
1815
1997
  const command = "sites register";
1816
- const success = deps.logSuccess ?? ((s) => log9.success(s));
1817
- const error = deps.logError ?? ((s) => log9.error(s));
1998
+ const success = deps.logSuccess ?? ((s) => log10.success(s));
1999
+ const error = deps.logError ?? ((s) => log10.error(s));
1818
2000
  const exit = deps.exit ?? exitWithCode;
1819
2001
  try {
1820
2002
  if (!options.slug || options.slug.trim().length === 0) {
1821
2003
  throw new UsageError("slug is required (positional argument)");
1822
2004
  }
1823
2005
  const teams = parseTeamsFlag(options.team);
1824
- const { client } = await setupClient(deps);
2006
+ const { client, identitySource } = await setupClient(deps);
1825
2007
  const row = await client.registerSite({
1826
2008
  slug: options.slug,
1827
2009
  teams: teams.length > 0 ? teams : void 0
@@ -1832,7 +2014,8 @@ async function register(options, deps = {}) {
1832
2014
  slug: row.slug,
1833
2015
  teams: row.teams,
1834
2016
  createdAt: row.createdAt,
1835
- createdBy: row.createdBy
2017
+ createdBy: row.createdBy,
2018
+ identitySource
1836
2019
  })
1837
2020
  );
1838
2021
  } else {
@@ -1849,33 +2032,32 @@ async function register(options, deps = {}) {
1849
2032
  }
1850
2033
  } catch (err) {
1851
2034
  const { code, message } = wrapProxyError(command, err);
1852
- if (options.json) {
1853
- emitJson8(buildErrorEnvelope(command, code, message));
1854
- } else {
1855
- error(message);
1856
- }
2035
+ outputError({ json: options.json, command }, code, message, {
2036
+ logError: error
2037
+ });
1857
2038
  exit(code);
1858
2039
  }
1859
2040
  }
1860
2041
 
1861
2042
  // src/commands/sites/rm.ts
1862
- import { log as log10 } from "@clack/prompts";
2043
+ import { log as log11 } from "@clack/prompts";
1863
2044
  async function rm2(options, deps = {}) {
1864
2045
  const command = "sites rm";
1865
- const success = deps.logSuccess ?? ((s) => log10.success(s));
1866
- const error = deps.logError ?? ((s) => log10.error(s));
2046
+ const success = deps.logSuccess ?? ((s) => log11.success(s));
2047
+ const error = deps.logError ?? ((s) => log11.error(s));
1867
2048
  const exit = deps.exit ?? exitWithCode;
1868
2049
  try {
1869
2050
  if (!options.slug || options.slug.trim().length === 0) {
1870
2051
  throw new UsageError("slug is required (positional argument)");
1871
2052
  }
1872
- const { client } = await setupClient(deps);
2053
+ const { client, identitySource } = await setupClient(deps);
1873
2054
  await client.deleteSite({ slug: options.slug });
1874
2055
  if (options.json) {
1875
2056
  emitJson8(
1876
2057
  buildEnvelope(command, true, {
1877
2058
  slug: options.slug,
1878
- deleted: true
2059
+ deleted: true,
2060
+ identitySource
1879
2061
  })
1880
2062
  );
1881
2063
  } else {
@@ -1890,21 +2072,19 @@ async function rm2(options, deps = {}) {
1890
2072
  }
1891
2073
  } catch (err) {
1892
2074
  const { code, message } = wrapProxyError(command, err);
1893
- if (options.json) {
1894
- emitJson8(buildErrorEnvelope(command, code, message));
1895
- } else {
1896
- error(message);
1897
- }
2075
+ outputError({ json: options.json, command }, code, message, {
2076
+ logError: error
2077
+ });
1898
2078
  exit(code);
1899
2079
  }
1900
2080
  }
1901
2081
 
1902
2082
  // src/commands/sites/update.ts
1903
- import { log as log11 } from "@clack/prompts";
2083
+ import { log as log12 } from "@clack/prompts";
1904
2084
  async function update(options, deps = {}) {
1905
2085
  const command = "sites update";
1906
- const success = deps.logSuccess ?? ((s) => log11.success(s));
1907
- const error = deps.logError ?? ((s) => log11.error(s));
2086
+ const success = deps.logSuccess ?? ((s) => log12.success(s));
2087
+ const error = deps.logError ?? ((s) => log12.error(s));
1908
2088
  const exit = deps.exit ?? exitWithCode;
1909
2089
  try {
1910
2090
  if (!options.slug || options.slug.trim().length === 0) {
@@ -1916,7 +2096,7 @@ async function update(options, deps = {}) {
1916
2096
  "--team is required with at least one slug; use `sites rm` to remove a site"
1917
2097
  );
1918
2098
  }
1919
- const { client } = await setupClient(deps);
2099
+ const { client, identitySource } = await setupClient(deps);
1920
2100
  const row = await client.updateSite({
1921
2101
  slug: options.slug,
1922
2102
  teams
@@ -1926,7 +2106,8 @@ async function update(options, deps = {}) {
1926
2106
  buildEnvelope(command, true, {
1927
2107
  slug: row.slug,
1928
2108
  teams: row.teams,
1929
- updatedAt: row.updatedAt
2109
+ updatedAt: row.updatedAt,
2110
+ identitySource
1930
2111
  })
1931
2112
  );
1932
2113
  } else {
@@ -1942,81 +2123,15 @@ async function update(options, deps = {}) {
1942
2123
  }
1943
2124
  } catch (err) {
1944
2125
  const { code, message } = wrapProxyError(command, err);
1945
- if (options.json) {
1946
- emitJson8(buildErrorEnvelope(command, code, message));
1947
- } else {
1948
- error(message);
1949
- }
2126
+ outputError({ json: options.json, command }, code, message, {
2127
+ logError: error
2128
+ });
1950
2129
  exit(code);
1951
2130
  }
1952
2131
  }
1953
2132
 
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
2133
  // src/cli.ts
2019
- var version = true ? "0.6.0" : "0.0.0";
2134
+ var version = true ? "0.7.1" : "0.0.0";
2020
2135
  function handleActionError(command, json, err) {
2021
2136
  const ctx = { json, command };
2022
2137
  const message = err instanceof Error ? err.message : "unknown error";