@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 +8 -8
- package/dist/index.cjs +213 -8
- package/dist/index.js +213 -8
- package/package.json +1 -1
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.
|
|
92
|
-
1.
|
|
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
|
|
115
|
-
| --------------------------- |
|
|
116
|
-
| `UNIVERSE_PROXY_URL` | `https://uploads.freecode.camp`
|
|
117
|
-
| `UNIVERSE_GH_CLIENT_ID` | _baked-in freeCodeCamp
|
|
118
|
-
| `GITHUB_TOKEN` / `GH_TOKEN` | —
|
|
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
|
|
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(
|
|
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
|
|
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"] ??
|
|
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.
|
|
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(
|
|
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
|
|
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"] ??
|
|
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.
|
|
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;
|