@freecodecamp/universe-cli 0.7.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/README.md CHANGED
@@ -88,8 +88,8 @@ All commands support `--json` for CI integration.
88
88
  The CLI resolves a GitHub identity in this order — first match wins:
89
89
 
90
90
  1. `$GITHUB_TOKEN` / `$GH_TOKEN` env (CI explicit)
91
- 1. `gh auth token` shell-out (laptop with `gh` installed)
92
- 1. Device-flow stored token at `~/.config/universe-cli/token`
91
+ 1. Device-flow stored token at `~/.config/universe-cli/token` (`universe login`)
92
+ 1. `gh auth token` shell-out (laptop fallback when no `universe login` token)
93
93
 
94
94
  CI runners must export `$GITHUB_TOKEN` explicitly. artemis validates the bearer via GitHub `GET /user`, then authorizes server-side against the Valkey-backed registry. Run `universe whoami` to see which slot resolved; inspect the sites you can deploy to with `universe sites ls --mine`.
95
95
 
@@ -111,10 +111,10 @@ Full operator walkthrough (login → deploy → promote → rollback, CI shape,
111
111
 
112
112
  ## Environment overrides
113
113
 
114
- | Env | Default | Purpose |
115
- | --------------------------- | -------------------------------- | --------------------------------------------------------- |
116
- | `UNIVERSE_PROXY_URL` | `https://uploads.freecode.camp` | Override proxy host (staging etc.) |
117
- | `UNIVERSE_GH_CLIENT_ID` | _baked-in freeCodeCamp OAuth id_ | Override GitHub OAuth App id (fork tenants, `login` only) |
118
- | `GITHUB_TOKEN` / `GH_TOKEN` | — | Slot 1 of identity chain |
114
+ | Env | Default | Purpose |
115
+ | --------------------------- | ---------------------------------------------- | ---------------------------------------------------------- |
116
+ | `UNIVERSE_PROXY_URL` | `https://uploads.freecode.camp` | Override proxy host (staging etc.) |
117
+ | `UNIVERSE_GH_CLIENT_ID` | _baked-in freeCodeCamp-Universe GitHub App id_ | Override GitHub App client id (fork tenants, `login` only) |
118
+ | `GITHUB_TOKEN` / `GH_TOKEN` | — | Slot 1 of identity chain |
119
119
 
120
- The shipped binary embeds the `freeCodeCamp` GitHub OAuth App client id (public; device flow uses no `client_secret`), so `universe login` works out of the box for staff. Fork operators and self-hosted mirror tenants set `UNIVERSE_GH_CLIENT_ID` to their own OAuth App's id — env value wins when set.
120
+ The shipped binary embeds the `freeCodeCamp-Universe` GitHub App client id (public; device flow uses no `client_secret`), so `universe login` works out of the box for staff once the App is installed on their org. Fork operators and self-hosted mirror tenants set `UNIVERSE_GH_CLIENT_ID` to their own GitHub App's id — env value wins when set.
package/dist/index.cjs CHANGED
@@ -8962,14 +8962,14 @@ async function resolveIdentity(opts = {}) {
8962
8962
  if (isNonEmpty(ghTokenEnv)) {
8963
8963
  return { token: ghTokenEnv.trim(), source: "env_GH_TOKEN" };
8964
8964
  }
8965
- const ghCli = await execGh();
8966
- if (isNonEmpty(ghCli)) {
8967
- return { token: ghCli.trim(), source: "gh_cli" };
8968
- }
8969
8965
  const stored = await loadStored();
8970
8966
  if (isNonEmpty(stored)) {
8971
8967
  return { token: stored.trim(), source: "device_flow" };
8972
8968
  }
8969
+ const ghCli = await execGh();
8970
+ if (isNonEmpty(ghCli)) {
8971
+ return { token: ghCli.trim(), source: "gh_cli" };
8972
+ }
8973
8973
  return null;
8974
8974
  }
8975
8975
 
@@ -23810,6 +23810,19 @@ async function runDeviceFlow(opts) {
23810
23810
 
23811
23811
  // src/commands/login.ts
23812
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");
23813
23826
  function emitJson2(envelope) {
23814
23827
  process.stdout.write(JSON.stringify(envelope) + "\n");
23815
23828
  }
@@ -23881,10 +23894,46 @@ async function login(options, deps = {}) {
23881
23894
  return;
23882
23895
  }
23883
23896
  await save(token);
23897
+ const selfCheck = await postLoginSelfCheck(token, env, deps);
23884
23898
  if (options.json) {
23885
- 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
+ );
23886
23908
  } else {
23887
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 };
23888
23937
  }
23889
23938
  }
23890
23939
 
@@ -24296,7 +24345,7 @@ async function rollback(options, deps = {}) {
24296
24345
  }
24297
24346
 
24298
24347
  // src/commands/whoami.ts
24299
- var DEFAULT_PROXY_URL2 = "https://uploads.freecode.camp";
24348
+ var DEFAULT_PROXY_URL3 = "https://uploads.freecode.camp";
24300
24349
  function emitJson7(envelope) {
24301
24350
  process.stdout.write(JSON.stringify(envelope) + "\n");
24302
24351
  }
@@ -24319,7 +24368,7 @@ async function whoami(options, deps = {}) {
24319
24368
  exit(EXIT_CREDENTIALS);
24320
24369
  return;
24321
24370
  }
24322
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
24371
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL3;
24323
24372
  const client = mkClient({
24324
24373
  baseUrl,
24325
24374
  getAuthToken: () => identity.token,
@@ -24573,7 +24622,7 @@ async function update(options, deps = {}) {
24573
24622
  }
24574
24623
 
24575
24624
  // src/cli.ts
24576
- var version2 = true ? "0.7.0" : "0.0.0";
24625
+ var version2 = true ? "0.7.1" : "0.0.0";
24577
24626
  function handleActionError(command, json2, err) {
24578
24627
  const ctx = { json: json2, command };
24579
24628
  const message = err instanceof Error ? err.message : "unknown error";
package/dist/index.js CHANGED
@@ -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
 
@@ -1308,6 +1308,19 @@ async function runDeviceFlow(opts) {
1308
1308
 
1309
1309
  // src/commands/login.ts
1310
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");
1311
1324
  function emitJson2(envelope) {
1312
1325
  process.stdout.write(JSON.stringify(envelope) + "\n");
1313
1326
  }
@@ -1379,10 +1392,46 @@ async function login(options, deps = {}) {
1379
1392
  return;
1380
1393
  }
1381
1394
  await save(token);
1395
+ const selfCheck = await postLoginSelfCheck(token, env, deps);
1382
1396
  if (options.json) {
1383
- 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
+ );
1384
1406
  } else {
1385
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 };
1386
1435
  }
1387
1436
  }
1388
1437
 
@@ -1799,7 +1848,7 @@ async function rollback(options, deps = {}) {
1799
1848
 
1800
1849
  // src/commands/whoami.ts
1801
1850
  import { log as log8 } from "@clack/prompts";
1802
- var DEFAULT_PROXY_URL2 = "https://uploads.freecode.camp";
1851
+ var DEFAULT_PROXY_URL3 = "https://uploads.freecode.camp";
1803
1852
  function emitJson7(envelope) {
1804
1853
  process.stdout.write(JSON.stringify(envelope) + "\n");
1805
1854
  }
@@ -1822,7 +1871,7 @@ async function whoami(options, deps = {}) {
1822
1871
  exit(EXIT_CREDENTIALS);
1823
1872
  return;
1824
1873
  }
1825
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
1874
+ const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL3;
1826
1875
  const client = mkClient({
1827
1876
  baseUrl,
1828
1877
  getAuthToken: () => identity.token,
@@ -2082,7 +2131,7 @@ async function update(options, deps = {}) {
2082
2131
  }
2083
2132
 
2084
2133
  // src/cli.ts
2085
- var version = true ? "0.7.0" : "0.0.0";
2134
+ var version = true ? "0.7.1" : "0.0.0";
2086
2135
  function handleActionError(command, json, err) {
2087
2136
  const ctx = { json, command };
2088
2137
  const message = err instanceof Error ? err.message : "unknown error";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freecodecamp/universe-cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "test": "vitest run",