@freecodecamp/universe-cli 0.7.0 → 0.7.2

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,
@@ -24572,8 +24621,162 @@ async function update(options, deps = {}) {
24572
24621
  }
24573
24622
  }
24574
24623
 
24624
+ // src/lib/update-notifier.ts
24625
+ var import_node_fs3 = require("fs");
24626
+ var import_promises8 = require("fs/promises");
24627
+ var import_node_os2 = require("os");
24628
+ var import_node_path9 = require("path");
24629
+ var APP_DIR2 = "universe-cli";
24630
+ var CACHE_FILE = "update-check.json";
24631
+ var PKG_NAME = "@freecodecamp/universe-cli";
24632
+ var NPM_LATEST_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
24633
+ var TTL_MS = 24 * 60 * 60 * 1e3;
24634
+ var FETCH_TIMEOUT_MS = 3e3;
24635
+ function configBase2() {
24636
+ const xdg = process.env["XDG_CONFIG_HOME"];
24637
+ if (xdg && xdg.length > 0) return xdg;
24638
+ return (0, import_node_path9.join)((0, import_node_os2.homedir)(), ".config");
24639
+ }
24640
+ function cachePath() {
24641
+ return (0, import_node_path9.join)(configBase2(), APP_DIR2, CACHE_FILE);
24642
+ }
24643
+ function isDisabled() {
24644
+ const v = process.env["UNIVERSE_NO_UPDATE_CHECK"];
24645
+ return v === "1" || v === "true";
24646
+ }
24647
+ function parseCache(raw) {
24648
+ let parsed;
24649
+ try {
24650
+ parsed = JSON.parse(raw);
24651
+ } catch {
24652
+ return null;
24653
+ }
24654
+ if (typeof parsed !== "object" || parsed === null || !("latest" in parsed) || !("lastCheck" in parsed)) {
24655
+ return null;
24656
+ }
24657
+ const { latest, lastCheck } = parsed;
24658
+ if (typeof latest !== "string" || typeof lastCheck !== "number") {
24659
+ return null;
24660
+ }
24661
+ return { latest, lastCheck };
24662
+ }
24663
+ async function readCache() {
24664
+ try {
24665
+ const raw = await (0, import_promises8.readFile)(cachePath(), "utf-8");
24666
+ return parseCache(raw);
24667
+ } catch {
24668
+ return null;
24669
+ }
24670
+ }
24671
+ function readCacheSync() {
24672
+ try {
24673
+ const raw = (0, import_node_fs3.readFileSync)(cachePath(), "utf-8");
24674
+ return parseCache(raw);
24675
+ } catch {
24676
+ return null;
24677
+ }
24678
+ }
24679
+ async function writeCache(c2) {
24680
+ const path = cachePath();
24681
+ await (0, import_promises8.mkdir)((0, import_node_path9.dirname)(path), { recursive: true, mode: 448 });
24682
+ await (0, import_promises8.writeFile)(path, JSON.stringify(c2), { mode: 420 });
24683
+ }
24684
+ async function fetchLatest() {
24685
+ const ctl = new AbortController();
24686
+ const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
24687
+ try {
24688
+ const res = await fetch(NPM_LATEST_URL, {
24689
+ signal: ctl.signal,
24690
+ headers: { accept: "application/json" }
24691
+ });
24692
+ if (!res.ok) return null;
24693
+ const body = await res.json();
24694
+ return typeof body.version === "string" ? body.version : null;
24695
+ } catch {
24696
+ return null;
24697
+ } finally {
24698
+ clearTimeout(timer);
24699
+ }
24700
+ }
24701
+ function compareVersions(a, b) {
24702
+ const pa = parseVersion(a);
24703
+ const pb = parseVersion(b);
24704
+ if (pa === null || pb === null) return 0;
24705
+ for (let i = 0; i < 3; i += 1) {
24706
+ const ai = pa[i] ?? 0;
24707
+ const bi = pb[i] ?? 0;
24708
+ if (ai < bi) return -1;
24709
+ if (ai > bi) return 1;
24710
+ }
24711
+ return 0;
24712
+ }
24713
+ function parseVersion(s) {
24714
+ const core = s.split("-")[0] ?? "";
24715
+ const parts = core.split(".");
24716
+ if (parts.length !== 3) return null;
24717
+ const nums = parts.map((p2) => Number.parseInt(p2, 10));
24718
+ if (nums.some((n) => Number.isNaN(n))) return null;
24719
+ return [nums[0], nums[1], nums[2]];
24720
+ }
24721
+ async function refreshIfStale(now = Date.now()) {
24722
+ if (isDisabled()) return;
24723
+ const cache = await readCache();
24724
+ if (cache !== null && now - cache.lastCheck < TTL_MS) return;
24725
+ const latest = await fetchLatest();
24726
+ if (latest === null) return;
24727
+ try {
24728
+ await writeCache({ latest, lastCheck: now });
24729
+ } catch {
24730
+ }
24731
+ }
24732
+ function getNoticeSync(current) {
24733
+ if (isDisabled()) return null;
24734
+ const cache = readCacheSync();
24735
+ if (cache === null) return null;
24736
+ if (compareVersions(current, cache.latest) >= 0) return null;
24737
+ return { current, latest: cache.latest };
24738
+ }
24739
+ function useColor() {
24740
+ if (process.env["NO_COLOR"] && process.env["NO_COLOR"].length > 0) {
24741
+ return false;
24742
+ }
24743
+ return process.stderr.isTTY === true;
24744
+ }
24745
+ function paint(s, code, color) {
24746
+ if (!color) return s;
24747
+ return `\x1B[${code}m${s}\x1B[0m`;
24748
+ }
24749
+ function formatNotice(n, color = useColor()) {
24750
+ const dim = (s) => paint(s, "2", color);
24751
+ const yellow = (s) => paint(s, "33", color);
24752
+ const cyan = (s) => paint(s, "36", color);
24753
+ const bar = dim("\u2502");
24754
+ const lines = [
24755
+ "",
24756
+ bar,
24757
+ `${yellow("\u25B2")} Update available: ${dim(n.current)} \u2192 ${cyan(n.latest)}`,
24758
+ `${bar} Run ${cyan(`npm i -g ${PKG_NAME}`)} to upgrade`,
24759
+ dim("\u2514"),
24760
+ ""
24761
+ ];
24762
+ return lines.join("\n");
24763
+ }
24764
+ function installExitNotice(current) {
24765
+ if (isDisabled()) return;
24766
+ let printed = false;
24767
+ const emit = () => {
24768
+ if (printed) return;
24769
+ printed = true;
24770
+ const n = getNoticeSync(current);
24771
+ if (n === null) return;
24772
+ process.stderr.write(formatNotice(n));
24773
+ };
24774
+ process.on("beforeExit", emit);
24775
+ process.on("exit", emit);
24776
+ }
24777
+
24575
24778
  // src/cli.ts
24576
- var version2 = true ? "0.7.0" : "0.0.0";
24779
+ var version2 = true ? "0.7.2" : "0.0.0";
24577
24780
  function handleActionError(command, json2, err) {
24578
24781
  const ctx = { json: json2, command };
24579
24782
  const message = err instanceof Error ? err.message : "unknown error";
@@ -24589,6 +24792,8 @@ function findFirstPositional(args) {
24589
24792
  return -1;
24590
24793
  }
24591
24794
  function run(argv = process.argv) {
24795
+ installExitNotice(version2);
24796
+ void refreshIfStale();
24592
24797
  const args = argv.slice(2);
24593
24798
  const firstPosIdx = findFirstPositional(args);
24594
24799
  const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
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,
@@ -2081,8 +2130,162 @@ async function update(options, deps = {}) {
2081
2130
  }
2082
2131
  }
2083
2132
 
2133
+ // src/lib/update-notifier.ts
2134
+ import { readFileSync } from "fs";
2135
+ import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2136
+ import { homedir as homedir2 } from "os";
2137
+ import { dirname as dirname2, join as join3 } from "path";
2138
+ var APP_DIR2 = "universe-cli";
2139
+ var CACHE_FILE = "update-check.json";
2140
+ var PKG_NAME = "@freecodecamp/universe-cli";
2141
+ var NPM_LATEST_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
2142
+ var TTL_MS = 24 * 60 * 60 * 1e3;
2143
+ var FETCH_TIMEOUT_MS = 3e3;
2144
+ function configBase2() {
2145
+ const xdg = process.env["XDG_CONFIG_HOME"];
2146
+ if (xdg && xdg.length > 0) return xdg;
2147
+ return join3(homedir2(), ".config");
2148
+ }
2149
+ function cachePath() {
2150
+ return join3(configBase2(), APP_DIR2, CACHE_FILE);
2151
+ }
2152
+ function isDisabled() {
2153
+ const v = process.env["UNIVERSE_NO_UPDATE_CHECK"];
2154
+ return v === "1" || v === "true";
2155
+ }
2156
+ function parseCache(raw) {
2157
+ let parsed;
2158
+ try {
2159
+ parsed = JSON.parse(raw);
2160
+ } catch {
2161
+ return null;
2162
+ }
2163
+ if (typeof parsed !== "object" || parsed === null || !("latest" in parsed) || !("lastCheck" in parsed)) {
2164
+ return null;
2165
+ }
2166
+ const { latest, lastCheck } = parsed;
2167
+ if (typeof latest !== "string" || typeof lastCheck !== "number") {
2168
+ return null;
2169
+ }
2170
+ return { latest, lastCheck };
2171
+ }
2172
+ async function readCache() {
2173
+ try {
2174
+ const raw = await readFile6(cachePath(), "utf-8");
2175
+ return parseCache(raw);
2176
+ } catch {
2177
+ return null;
2178
+ }
2179
+ }
2180
+ function readCacheSync() {
2181
+ try {
2182
+ const raw = readFileSync(cachePath(), "utf-8");
2183
+ return parseCache(raw);
2184
+ } catch {
2185
+ return null;
2186
+ }
2187
+ }
2188
+ async function writeCache(c) {
2189
+ const path = cachePath();
2190
+ await mkdir2(dirname2(path), { recursive: true, mode: 448 });
2191
+ await writeFile2(path, JSON.stringify(c), { mode: 420 });
2192
+ }
2193
+ async function fetchLatest() {
2194
+ const ctl = new AbortController();
2195
+ const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
2196
+ try {
2197
+ const res = await fetch(NPM_LATEST_URL, {
2198
+ signal: ctl.signal,
2199
+ headers: { accept: "application/json" }
2200
+ });
2201
+ if (!res.ok) return null;
2202
+ const body = await res.json();
2203
+ return typeof body.version === "string" ? body.version : null;
2204
+ } catch {
2205
+ return null;
2206
+ } finally {
2207
+ clearTimeout(timer);
2208
+ }
2209
+ }
2210
+ function compareVersions(a, b) {
2211
+ const pa = parseVersion(a);
2212
+ const pb = parseVersion(b);
2213
+ if (pa === null || pb === null) return 0;
2214
+ for (let i = 0; i < 3; i += 1) {
2215
+ const ai = pa[i] ?? 0;
2216
+ const bi = pb[i] ?? 0;
2217
+ if (ai < bi) return -1;
2218
+ if (ai > bi) return 1;
2219
+ }
2220
+ return 0;
2221
+ }
2222
+ function parseVersion(s) {
2223
+ const core = s.split("-")[0] ?? "";
2224
+ const parts = core.split(".");
2225
+ if (parts.length !== 3) return null;
2226
+ const nums = parts.map((p) => Number.parseInt(p, 10));
2227
+ if (nums.some((n) => Number.isNaN(n))) return null;
2228
+ return [nums[0], nums[1], nums[2]];
2229
+ }
2230
+ async function refreshIfStale(now = Date.now()) {
2231
+ if (isDisabled()) return;
2232
+ const cache = await readCache();
2233
+ if (cache !== null && now - cache.lastCheck < TTL_MS) return;
2234
+ const latest = await fetchLatest();
2235
+ if (latest === null) return;
2236
+ try {
2237
+ await writeCache({ latest, lastCheck: now });
2238
+ } catch {
2239
+ }
2240
+ }
2241
+ function getNoticeSync(current) {
2242
+ if (isDisabled()) return null;
2243
+ const cache = readCacheSync();
2244
+ if (cache === null) return null;
2245
+ if (compareVersions(current, cache.latest) >= 0) return null;
2246
+ return { current, latest: cache.latest };
2247
+ }
2248
+ function useColor() {
2249
+ if (process.env["NO_COLOR"] && process.env["NO_COLOR"].length > 0) {
2250
+ return false;
2251
+ }
2252
+ return process.stderr.isTTY === true;
2253
+ }
2254
+ function paint(s, code, color) {
2255
+ if (!color) return s;
2256
+ return `\x1B[${code}m${s}\x1B[0m`;
2257
+ }
2258
+ function formatNotice(n, color = useColor()) {
2259
+ const dim = (s) => paint(s, "2", color);
2260
+ const yellow = (s) => paint(s, "33", color);
2261
+ const cyan = (s) => paint(s, "36", color);
2262
+ const bar = dim("\u2502");
2263
+ const lines = [
2264
+ "",
2265
+ bar,
2266
+ `${yellow("\u25B2")} Update available: ${dim(n.current)} \u2192 ${cyan(n.latest)}`,
2267
+ `${bar} Run ${cyan(`npm i -g ${PKG_NAME}`)} to upgrade`,
2268
+ dim("\u2514"),
2269
+ ""
2270
+ ];
2271
+ return lines.join("\n");
2272
+ }
2273
+ function installExitNotice(current) {
2274
+ if (isDisabled()) return;
2275
+ let printed = false;
2276
+ const emit = () => {
2277
+ if (printed) return;
2278
+ printed = true;
2279
+ const n = getNoticeSync(current);
2280
+ if (n === null) return;
2281
+ process.stderr.write(formatNotice(n));
2282
+ };
2283
+ process.on("beforeExit", emit);
2284
+ process.on("exit", emit);
2285
+ }
2286
+
2084
2287
  // src/cli.ts
2085
- var version = true ? "0.7.0" : "0.0.0";
2288
+ var version = true ? "0.7.2" : "0.0.0";
2086
2289
  function handleActionError(command, json, err) {
2087
2290
  const ctx = { json, command };
2088
2291
  const message = err instanceof Error ? err.message : "unknown error";
@@ -2098,6 +2301,8 @@ function findFirstPositional(args) {
2098
2301
  return -1;
2099
2302
  }
2100
2303
  function run(argv = process.argv) {
2304
+ installExitNotice(version);
2305
+ void refreshIfStale();
2101
2306
  const args = argv.slice(2);
2102
2307
  const firstPosIdx = findFirstPositional(args);
2103
2308
  const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
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.2",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "test": "vitest run",