@freecodecamp/universe-cli 0.8.0 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE CHANGED
@@ -1,13 +1,29 @@
1
- Copyright (c) freeCodeCamp
2
-
3
- Permission to use, copy, modify, and/or distribute this software for any
4
- purpose with or without fee is hereby granted, provided that the above
5
- copyright notice and this permission notice appear in all copies.
6
-
7
- THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
8
- REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
9
- AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
10
- INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
11
- LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
12
- OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
13
- PERFORMANCE OF THIS SOFTWARE.
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2014, freeCodeCamp.
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+
12
+ 2. Redistributions in binary form must reproduce the above copyright notice,
13
+ this list of conditions and the following disclaimer in the documentation
14
+ and/or other materials provided with the distribution.
15
+
16
+ 3. Neither the name of the copyright holder nor the names of its
17
+ contributors may be used to endorse or promote products derived from
18
+ this software without specific prior written permission.
19
+
20
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21
+ AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22
+ IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24
+ FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25
+ DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26
+ SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27
+ CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28
+ OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29
+ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Universe CLI
2
2
 
3
- Static site deployment for the freeCodeCamp Universe platform.
3
+ A CLI for freeCodeCamp staff and operators to deploy, manage and maintain constellation apps on the freeCodeCamp Universe Platform.
4
4
 
5
5
  ## Install
6
6
 
@@ -52,82 +52,23 @@ Verify:
52
52
  universe --version
53
53
  ```
54
54
 
55
- ## CLI surface
56
-
57
- Top-level (cross-cutting):
58
-
59
- ```sh
60
- universe login # GitHub OAuth device flow → ~/.config/universe-cli/token
61
- universe logout # delete stored token
62
- universe whoami # echo current login + authorized-sites count
63
- universe --version # CLI version
64
- ```
65
-
66
- Static-site verbs (namespaced under `static`):
67
-
68
- ```sh
69
- universe static deploy [--promote] [--dir <path>]
70
- universe static promote [--from <deployId>]
71
- universe static rollback --to <deployId>
72
- universe static ls [--site <site>]
73
- ```
74
-
75
- Static-app registry (namespaced under `sites`, staff-gated writes):
76
-
77
- ```sh
78
- universe sites ls [--mine] # list registered sites; `--mine` filters to your authorized set
79
- universe sites register <slug> [--team=<name>...] # create new entry (staff; defaults --team to staff)
80
- universe sites update <slug> --team=<name>... # replace teams list (staff)
81
- universe sites rm <slug> # delete entry (staff; R2 deploy bytes untouched)
82
- ```
83
-
84
- Repository creation + approval queue (namespaced under `repo`):
55
+ ## Quickstart
85
56
 
86
57
  ```sh
87
- universe repo create [name] [--visibility public|private] [--template <repo>] [--description <text>] [--yes]
88
- # request a repo in freeCodeCamp-Universe (staff; prompts when run bare)
89
- universe repo ls [--status <state>] [--mine] # list requests (default: pending; state ∈ pending|approved|active|rejected|failed|all)
90
- universe repo status <id> # show one request's lifecycle state
91
- universe repo approve <id> [--yes] # approve → creates the repo via the Apollo-11 App (admin)
92
- universe repo reject <id> [--reason <text>] [--yes] # reject a pending request (admin)
93
- ```
94
-
95
- Repo requests enter an artemis-owned approval queue (independent of the legacy Windmill flow). The Apollo-11 GitHub App key lives in the cluster, never the CLI — the CLI only carries your GitHub bearer.
96
-
97
- All commands support `--json` for CI integration.
98
-
99
- ## Identity (priority chain)
100
-
101
- The CLI resolves a GitHub identity in this order — first match wins:
102
-
103
- 1. `$GITHUB_TOKEN` / `$GH_TOKEN` env (CI explicit)
104
- 1. Device-flow stored token at `~/.config/universe-cli/token` (`universe login`)
105
- 1. `gh auth token` shell-out (laptop fallback when no `universe login` token)
106
-
107
- 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`.
108
-
109
- ## Configuration (`platform.yaml`)
110
-
111
- Every site has a `platform.yaml` at its repo root. Minimal valid file:
112
-
113
- ```yaml
114
- site: my-site
58
+ universe login # GitHub OAuth device flow
59
+ # add a platform.yaml at your repo root with: site: my-site
60
+ universe static deploy # upload the build to a new preview deploy
61
+ universe static promote # point production at that deploy
115
62
  ```
116
63
 
117
- Full schema reference (every field, defaults, validation rules): [`docs/platform-yaml.md`](docs/platform-yaml.md).
118
-
119
- No credential fields. The proxy holds the R2 admin key; the CLI never reads or writes one.
64
+ ## Docs
120
65
 
121
- ## Common flows
66
+ Start with the [Staff Guide](docs/STAFF-GUIDE.md). See the [command reference](docs/reference.md), the [`platform.yaml` schema](docs/platform-yaml.md), or [architecture & internals](docs/README.md).
122
67
 
123
- Full operator walkthrough (login → deploy → promote → rollback, CI shape, registry admin, troubleshooting) lives in [`docs/STAFF-GUIDE.md`](docs/STAFF-GUIDE.md).
68
+ ## License
124
69
 
125
- ## Environment overrides
70
+ Copyright © 2014 freeCodeCamp.org
126
71
 
127
- | Env | Default | Purpose |
128
- | --------------------------- | ---------------------------------------------- | ---------------------------------------------------------- |
129
- | `UNIVERSE_PROXY_URL` | `https://uploads.freecode.camp` | Override proxy host (staging etc.) |
130
- | `UNIVERSE_GH_CLIENT_ID` | _baked-in freeCodeCamp-Universe GitHub App id_ | Override GitHub App client id (fork tenants, `login` only) |
131
- | `GITHUB_TOKEN` / `GH_TOKEN` | — | Slot 1 of identity chain |
72
+ The content of this repository is bound by the following license:
132
73
 
133
- 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.
74
+ - The computer software is licensed under the [BSD-3-Clause](LICENSE) license.
package/dist/index.cjs CHANGED
@@ -23566,6 +23566,16 @@ function createProxyClient(cfg) {
23566
23566
  (raw) => repoRowSchema.parse(raw)
23567
23567
  );
23568
23568
  },
23569
+ async deleteRepoRequest(req) {
23570
+ const url2 = `${base}/api/repo/${encodeURIComponent(req.id)}`;
23571
+ return call(url2, {
23572
+ method: "DELETE",
23573
+ headers: {
23574
+ Authorization: await userBearer(),
23575
+ Accept: "application/json"
23576
+ }
23577
+ });
23578
+ },
23569
23579
  async listRepoTemplates() {
23570
23580
  const res = await call(
23571
23581
  `${base}/api/repo/templates`,
@@ -25295,7 +25305,9 @@ async function create(options, deps = {}) {
25295
25305
  }
25296
25306
  } catch (err) {
25297
25307
  const { code, message, kind, requestId } = wrapProxyError(command, err);
25298
- outputError({ json: options.json, command }, code, message, {
25308
+ const display = kind === "already_exists" && !options.json ? `${message}
25309
+ \u2192 run \`universe repo ls --status all\` to find the existing request (it may be active or failed)` : message;
25310
+ outputError({ json: options.json, command }, code, display, {
25299
25311
  logError: error48,
25300
25312
  kind,
25301
25313
  requestId,
@@ -25314,19 +25326,20 @@ async function ls3(options, deps = {}) {
25314
25326
  const exit = deps.exit ?? exitWithCode;
25315
25327
  let identitySource;
25316
25328
  try {
25317
- if (options.status !== void 0 && !LS_STATUSES.includes(options.status)) {
25329
+ const requestedStatus = options.all ? "all" : options.status;
25330
+ if (requestedStatus !== void 0 && !LS_STATUSES.includes(requestedStatus)) {
25318
25331
  throw new UsageError(
25319
- `invalid --status "${options.status}": must be one of ${LS_STATUSES.join(", ")}`
25332
+ `invalid --status "${requestedStatus}": must be one of ${LS_STATUSES.join(", ")}`
25320
25333
  );
25321
25334
  }
25322
25335
  const setup = await setupClient2(deps);
25323
25336
  const client = setup.client;
25324
25337
  identitySource = setup.identitySource;
25325
25338
  const rows = await client.listRepoRequests({
25326
- status: options.status,
25339
+ status: requestedStatus,
25327
25340
  mine: options.mine ?? false
25328
25341
  });
25329
- const status2 = options.status ?? "pending";
25342
+ const status2 = requestedStatus ?? "pending";
25330
25343
  if (options.json) {
25331
25344
  emitJson9(
25332
25345
  buildEnvelope(command, true, {
@@ -25425,6 +25438,62 @@ async function reject(options, deps = {}) {
25425
25438
  }
25426
25439
  }
25427
25440
 
25441
+ // src/commands/repo/rm.ts
25442
+ async function rm3(options, deps = {}) {
25443
+ const command = "repo rm";
25444
+ const success2 = deps.logSuccess ?? ((s) => O2.success(s));
25445
+ const error48 = deps.logError ?? ((s) => O2.error(s));
25446
+ const exit = deps.exit ?? exitWithCode;
25447
+ const prompts = deps.prompts ?? defaultRepoPrompts;
25448
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
25449
+ let identitySource;
25450
+ try {
25451
+ if (!options.id || options.id.trim().length === 0) {
25452
+ throw new UsageError("request id is required (positional argument)");
25453
+ }
25454
+ const setup = await setupClient2(deps);
25455
+ const client = setup.client;
25456
+ identitySource = setup.identitySource;
25457
+ if (!options.json && !options.yes) {
25458
+ if (!isTTY) {
25459
+ throw new UsageError(
25460
+ "non-interactive session: pass --yes to delete without confirmation"
25461
+ );
25462
+ }
25463
+ const cur = await client.getRepoRequest(options.id);
25464
+ const ok = await prompts.confirm({
25465
+ message: `Delete the ${cur.status} request for "${cur.name}" (${cur.id})? This frees the repo name.`
25466
+ });
25467
+ if (prompts.isCancel(ok) || ok === false) {
25468
+ throw new ConfirmError("repo rm cancelled");
25469
+ }
25470
+ }
25471
+ await client.deleteRepoRequest({ id: options.id });
25472
+ if (options.json) {
25473
+ emitJson9(
25474
+ buildEnvelope(command, true, {
25475
+ id: options.id,
25476
+ deleted: true,
25477
+ identitySource
25478
+ })
25479
+ );
25480
+ } else {
25481
+ success2(
25482
+ `Deleted request ${options.id} \u2014 the repo name is free to request again`
25483
+ );
25484
+ }
25485
+ } catch (err) {
25486
+ const { code, message, kind, requestId } = wrapProxyError(command, err);
25487
+ outputError({ json: options.json, command }, code, message, {
25488
+ logError: error48,
25489
+ kind,
25490
+ requestId,
25491
+ extras: identitySource ? { identitySource } : void 0
25492
+ });
25493
+ exit(code);
25494
+ }
25495
+ }
25496
+
25428
25497
  // src/commands/repo/status.ts
25429
25498
  function humanRow(row) {
25430
25499
  const lines = [
@@ -25481,6 +25550,7 @@ async function status(options, deps = {}) {
25481
25550
  }
25482
25551
 
25483
25552
  // src/lib/update-notifier.ts
25553
+ var import_node_child_process4 = require("child_process");
25484
25554
  var import_node_fs3 = require("fs");
25485
25555
  var import_promises8 = require("fs/promises");
25486
25556
  var import_node_os2 = require("os");
@@ -25489,8 +25559,21 @@ var APP_DIR2 = "universe-cli";
25489
25559
  var CACHE_FILE = "update-check.json";
25490
25560
  var PKG_NAME = "@freecodecamp/universe-cli";
25491
25561
  var NPM_LATEST_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
25492
- var TTL_MS = 24 * 60 * 60 * 1e3;
25562
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
25493
25563
  var FETCH_TIMEOUT_MS = 3e3;
25564
+ var REFRESH_ENV = "UNIVERSE_REFRESH_WORKER";
25565
+ function ttlMs() {
25566
+ const raw = process.env["UNIVERSE_UPDATE_TTL_MS"];
25567
+ if (raw !== void 0) {
25568
+ const n = Number.parseInt(raw, 10);
25569
+ if (Number.isFinite(n) && n >= 0) return n;
25570
+ }
25571
+ return DEFAULT_TTL_MS;
25572
+ }
25573
+ function latestUrl() {
25574
+ const override = process.env["UNIVERSE_UPDATE_URL"];
25575
+ return override && override.length > 0 ? override : NPM_LATEST_URL;
25576
+ }
25494
25577
  function configBase2() {
25495
25578
  const xdg = process.env["XDG_CONFIG_HOME"];
25496
25579
  if (xdg && xdg.length > 0) return xdg;
@@ -25544,7 +25627,7 @@ async function fetchLatest() {
25544
25627
  const ctl = new AbortController();
25545
25628
  const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
25546
25629
  try {
25547
- const res = await fetch(NPM_LATEST_URL, {
25630
+ const res = await fetch(latestUrl(), {
25548
25631
  signal: ctl.signal,
25549
25632
  headers: { accept: "application/json" }
25550
25633
  });
@@ -25577,10 +25660,12 @@ function parseVersion(s) {
25577
25660
  if (nums.some((n) => Number.isNaN(n))) return null;
25578
25661
  return [nums[0], nums[1], nums[2]];
25579
25662
  }
25580
- async function refreshIfStale(now = Date.now()) {
25663
+ async function refreshIfStale(now = Date.now(), options = {}) {
25581
25664
  if (isDisabled()) return;
25582
- const cache = await readCache();
25583
- if (cache !== null && now - cache.lastCheck < TTL_MS) return;
25665
+ if (!options.force) {
25666
+ const cache = await readCache();
25667
+ if (cache !== null && now - cache.lastCheck < ttlMs()) return;
25668
+ }
25584
25669
  const latest = await fetchLatest();
25585
25670
  if (latest === null) return;
25586
25671
  try {
@@ -25588,6 +25673,25 @@ async function refreshIfStale(now = Date.now()) {
25588
25673
  } catch {
25589
25674
  }
25590
25675
  }
25676
+ function spawnRefresh(now = Date.now()) {
25677
+ if (isDisabled()) return;
25678
+ const cache = readCacheSync();
25679
+ if (cache !== null && now - cache.lastCheck < ttlMs()) return;
25680
+ try {
25681
+ const entry = process.argv[1];
25682
+ const args = entry ? [entry] : [];
25683
+ const child = (0, import_node_child_process4.spawn)(process.execPath, args, {
25684
+ detached: true,
25685
+ stdio: "ignore",
25686
+ env: { ...process.env, [REFRESH_ENV]: "1" }
25687
+ });
25688
+ child.unref();
25689
+ } catch {
25690
+ }
25691
+ }
25692
+ async function runRefreshWorker() {
25693
+ await refreshIfStale(Date.now(), { force: true });
25694
+ }
25591
25695
  function getNoticeSync(current) {
25592
25696
  if (isDisabled()) return null;
25593
25697
  const cache = readCacheSync();
@@ -25628,14 +25732,17 @@ function installExitNotice(current) {
25628
25732
  printed = true;
25629
25733
  const n = getNoticeSync(current);
25630
25734
  if (n === null) return;
25631
- process.stderr.write(formatNotice(n));
25735
+ try {
25736
+ (0, import_node_fs3.writeSync)(2, formatNotice(n));
25737
+ } catch {
25738
+ }
25632
25739
  };
25633
25740
  process.on("beforeExit", emit);
25634
25741
  process.on("exit", emit);
25635
25742
  }
25636
25743
 
25637
25744
  // src/cli.ts
25638
- var version2 = true ? "0.8.0" : "0.0.0";
25745
+ var version2 = true ? "0.9.0" : "0.0.0";
25639
25746
  function handleActionError(command, json2, err) {
25640
25747
  const ctx = { json: json2, command };
25641
25748
  const message = err instanceof Error ? err.message : "unknown error";
@@ -25650,11 +25757,16 @@ function findFirstPositional(args) {
25650
25757
  }
25651
25758
  return -1;
25652
25759
  }
25653
- function run(argv = process.argv) {
25760
+ function isVersionRequest(args) {
25761
+ return args.includes("--version") || args.includes("-v");
25762
+ }
25763
+ async function run(argv = process.argv) {
25654
25764
  installExitNotice(version2);
25655
- void refreshIfStale().catch(() => {
25656
- });
25657
25765
  const args = argv.slice(2);
25766
+ const versionRequested = isVersionRequest(args);
25767
+ if (!versionRequested) {
25768
+ spawnRefresh();
25769
+ }
25658
25770
  const firstPosIdx = findFirstPositional(args);
25659
25771
  const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
25660
25772
  const isStatic = namespace === "static";
@@ -25757,13 +25869,14 @@ function run(argv = process.argv) {
25757
25869
  repoCli.command("ls", "List repo requests (default: pending)").option("--json", "Output as JSON").option(
25758
25870
  "--status <status>",
25759
25871
  "pending | approved | active | rejected | failed | all"
25760
- ).option("--mine", "Only requests you submitted").action(
25872
+ ).option("--mine", "Only requests you submitted").option("--all", "Show every state (shorthand for --status all)").action(
25761
25873
  async (flags) => {
25762
25874
  try {
25763
25875
  await ls3({
25764
25876
  json: flags.json ?? false,
25765
25877
  status: flags.status,
25766
- mine: flags.mine ?? false
25878
+ mine: flags.mine ?? false,
25879
+ all: flags.all ?? false
25767
25880
  });
25768
25881
  } catch (err) {
25769
25882
  handleActionError("repo ls", flags.json ?? false, err);
@@ -25805,6 +25918,20 @@ function run(argv = process.argv) {
25805
25918
  handleActionError("repo status", flags.json ?? false, err);
25806
25919
  }
25807
25920
  });
25921
+ repoCli.command(
25922
+ "rm <id>",
25923
+ "Delete a request, freeing its repo name (admin only)"
25924
+ ).option("--json", "Output as JSON").option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").action(async (id, flags) => {
25925
+ try {
25926
+ await rm3({
25927
+ json: flags.json ?? false,
25928
+ id,
25929
+ yes: flags.yes ?? false
25930
+ });
25931
+ } catch (err) {
25932
+ handleActionError("repo rm", flags.json ?? false, err);
25933
+ }
25934
+ });
25808
25935
  repoCli.help();
25809
25936
  repoCli.version(version2);
25810
25937
  const knownRepoSubs = /* @__PURE__ */ new Set([
@@ -25812,7 +25939,8 @@ function run(argv = process.argv) {
25812
25939
  "ls",
25813
25940
  "approve",
25814
25941
  "reject",
25815
- "status"
25942
+ "status",
25943
+ "rm"
25816
25944
  ]);
25817
25945
  const repoValueFlags = /* @__PURE__ */ new Set([
25818
25946
  "--visibility",
@@ -25947,6 +26075,9 @@ function run(argv = process.argv) {
25947
26075
  cli.version(version2);
25948
26076
  cli.parse(argv);
25949
26077
  }
26078
+ if (versionRequested) {
26079
+ await refreshIfStale(Date.now(), { force: true });
26080
+ }
25950
26081
  }
25951
26082
 
25952
26083
  // src/lib/fatal.ts
@@ -25964,5 +26095,9 @@ function installFatalHandlers(onFatal = defaultOnFatal) {
25964
26095
  }
25965
26096
 
25966
26097
  // src/index.ts
25967
- installFatalHandlers();
25968
- run();
26098
+ if (process.env[REFRESH_ENV] === "1") {
26099
+ void runRefreshWorker();
26100
+ } else {
26101
+ installFatalHandlers();
26102
+ void run();
26103
+ }
package/dist/index.js CHANGED
@@ -889,6 +889,16 @@ function createProxyClient(cfg) {
889
889
  (raw) => repoRowSchema.parse(raw)
890
890
  );
891
891
  },
892
+ async deleteRepoRequest(req) {
893
+ const url = `${base}/api/repo/${encodeURIComponent(req.id)}`;
894
+ return call(url, {
895
+ method: "DELETE",
896
+ headers: {
897
+ Authorization: await userBearer(),
898
+ Accept: "application/json"
899
+ }
900
+ });
901
+ },
892
902
  async listRepoTemplates() {
893
903
  const res = await call(
894
904
  `${base}/api/repo/templates`,
@@ -2645,7 +2655,9 @@ async function create(options, deps = {}) {
2645
2655
  }
2646
2656
  } catch (err) {
2647
2657
  const { code, message, kind, requestId } = wrapProxyError(command, err);
2648
- outputError({ json: options.json, command }, code, message, {
2658
+ const display = kind === "already_exists" && !options.json ? `${message}
2659
+ \u2192 run \`universe repo ls --status all\` to find the existing request (it may be active or failed)` : message;
2660
+ outputError({ json: options.json, command }, code, display, {
2649
2661
  logError: error,
2650
2662
  kind,
2651
2663
  requestId,
@@ -2665,19 +2677,20 @@ async function ls3(options, deps = {}) {
2665
2677
  const exit = deps.exit ?? exitWithCode;
2666
2678
  let identitySource;
2667
2679
  try {
2668
- if (options.status !== void 0 && !LS_STATUSES.includes(options.status)) {
2680
+ const requestedStatus = options.all ? "all" : options.status;
2681
+ if (requestedStatus !== void 0 && !LS_STATUSES.includes(requestedStatus)) {
2669
2682
  throw new UsageError(
2670
- `invalid --status "${options.status}": must be one of ${LS_STATUSES.join(", ")}`
2683
+ `invalid --status "${requestedStatus}": must be one of ${LS_STATUSES.join(", ")}`
2671
2684
  );
2672
2685
  }
2673
2686
  const setup = await setupClient2(deps);
2674
2687
  const client = setup.client;
2675
2688
  identitySource = setup.identitySource;
2676
2689
  const rows = await client.listRepoRequests({
2677
- status: options.status,
2690
+ status: requestedStatus,
2678
2691
  mine: options.mine ?? false
2679
2692
  });
2680
- const status2 = options.status ?? "pending";
2693
+ const status2 = requestedStatus ?? "pending";
2681
2694
  if (options.json) {
2682
2695
  emitJson9(
2683
2696
  buildEnvelope(command, true, {
@@ -2777,8 +2790,65 @@ async function reject(options, deps = {}) {
2777
2790
  }
2778
2791
  }
2779
2792
 
2780
- // src/commands/repo/status.ts
2793
+ // src/commands/repo/rm.ts
2781
2794
  import { log as log17 } from "@clack/prompts";
2795
+ async function rm3(options, deps = {}) {
2796
+ const command = "repo rm";
2797
+ const success = deps.logSuccess ?? ((s) => log17.success(s));
2798
+ const error = deps.logError ?? ((s) => log17.error(s));
2799
+ const exit = deps.exit ?? exitWithCode;
2800
+ const prompts = deps.prompts ?? defaultRepoPrompts;
2801
+ const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
2802
+ let identitySource;
2803
+ try {
2804
+ if (!options.id || options.id.trim().length === 0) {
2805
+ throw new UsageError("request id is required (positional argument)");
2806
+ }
2807
+ const setup = await setupClient2(deps);
2808
+ const client = setup.client;
2809
+ identitySource = setup.identitySource;
2810
+ if (!options.json && !options.yes) {
2811
+ if (!isTTY) {
2812
+ throw new UsageError(
2813
+ "non-interactive session: pass --yes to delete without confirmation"
2814
+ );
2815
+ }
2816
+ const cur = await client.getRepoRequest(options.id);
2817
+ const ok = await prompts.confirm({
2818
+ message: `Delete the ${cur.status} request for "${cur.name}" (${cur.id})? This frees the repo name.`
2819
+ });
2820
+ if (prompts.isCancel(ok) || ok === false) {
2821
+ throw new ConfirmError("repo rm cancelled");
2822
+ }
2823
+ }
2824
+ await client.deleteRepoRequest({ id: options.id });
2825
+ if (options.json) {
2826
+ emitJson9(
2827
+ buildEnvelope(command, true, {
2828
+ id: options.id,
2829
+ deleted: true,
2830
+ identitySource
2831
+ })
2832
+ );
2833
+ } else {
2834
+ success(
2835
+ `Deleted request ${options.id} \u2014 the repo name is free to request again`
2836
+ );
2837
+ }
2838
+ } catch (err) {
2839
+ const { code, message, kind, requestId } = wrapProxyError(command, err);
2840
+ outputError({ json: options.json, command }, code, message, {
2841
+ logError: error,
2842
+ kind,
2843
+ requestId,
2844
+ extras: identitySource ? { identitySource } : void 0
2845
+ });
2846
+ exit(code);
2847
+ }
2848
+ }
2849
+
2850
+ // src/commands/repo/status.ts
2851
+ import { log as log18 } from "@clack/prompts";
2782
2852
  function humanRow(row) {
2783
2853
  const lines = [
2784
2854
  `Request ${row.id}`,
@@ -2799,8 +2869,8 @@ function humanRow(row) {
2799
2869
  }
2800
2870
  async function status(options, deps = {}) {
2801
2871
  const command = "repo status";
2802
- const message = deps.logMessage ?? ((s) => log17.message(s));
2803
- const error = deps.logError ?? ((s) => log17.error(s));
2872
+ const message = deps.logMessage ?? ((s) => log18.message(s));
2873
+ const error = deps.logError ?? ((s) => log18.error(s));
2804
2874
  const exit = deps.exit ?? exitWithCode;
2805
2875
  let identitySource;
2806
2876
  try {
@@ -2834,7 +2904,8 @@ async function status(options, deps = {}) {
2834
2904
  }
2835
2905
 
2836
2906
  // src/lib/update-notifier.ts
2837
- import { readFileSync } from "fs";
2907
+ import { spawn as spawn2 } from "child_process";
2908
+ import { readFileSync, writeSync } from "fs";
2838
2909
  import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2839
2910
  import { homedir as homedir2 } from "os";
2840
2911
  import { dirname as dirname2, join as join3 } from "path";
@@ -2842,8 +2913,21 @@ var APP_DIR2 = "universe-cli";
2842
2913
  var CACHE_FILE = "update-check.json";
2843
2914
  var PKG_NAME = "@freecodecamp/universe-cli";
2844
2915
  var NPM_LATEST_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
2845
- var TTL_MS = 24 * 60 * 60 * 1e3;
2916
+ var DEFAULT_TTL_MS = 60 * 60 * 1e3;
2846
2917
  var FETCH_TIMEOUT_MS = 3e3;
2918
+ var REFRESH_ENV = "UNIVERSE_REFRESH_WORKER";
2919
+ function ttlMs() {
2920
+ const raw = process.env["UNIVERSE_UPDATE_TTL_MS"];
2921
+ if (raw !== void 0) {
2922
+ const n = Number.parseInt(raw, 10);
2923
+ if (Number.isFinite(n) && n >= 0) return n;
2924
+ }
2925
+ return DEFAULT_TTL_MS;
2926
+ }
2927
+ function latestUrl() {
2928
+ const override = process.env["UNIVERSE_UPDATE_URL"];
2929
+ return override && override.length > 0 ? override : NPM_LATEST_URL;
2930
+ }
2847
2931
  function configBase2() {
2848
2932
  const xdg = process.env["XDG_CONFIG_HOME"];
2849
2933
  if (xdg && xdg.length > 0) return xdg;
@@ -2897,7 +2981,7 @@ async function fetchLatest() {
2897
2981
  const ctl = new AbortController();
2898
2982
  const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
2899
2983
  try {
2900
- const res = await fetch(NPM_LATEST_URL, {
2984
+ const res = await fetch(latestUrl(), {
2901
2985
  signal: ctl.signal,
2902
2986
  headers: { accept: "application/json" }
2903
2987
  });
@@ -2930,10 +3014,12 @@ function parseVersion(s) {
2930
3014
  if (nums.some((n) => Number.isNaN(n))) return null;
2931
3015
  return [nums[0], nums[1], nums[2]];
2932
3016
  }
2933
- async function refreshIfStale(now = Date.now()) {
3017
+ async function refreshIfStale(now = Date.now(), options = {}) {
2934
3018
  if (isDisabled()) return;
2935
- const cache = await readCache();
2936
- if (cache !== null && now - cache.lastCheck < TTL_MS) return;
3019
+ if (!options.force) {
3020
+ const cache = await readCache();
3021
+ if (cache !== null && now - cache.lastCheck < ttlMs()) return;
3022
+ }
2937
3023
  const latest = await fetchLatest();
2938
3024
  if (latest === null) return;
2939
3025
  try {
@@ -2941,6 +3027,25 @@ async function refreshIfStale(now = Date.now()) {
2941
3027
  } catch {
2942
3028
  }
2943
3029
  }
3030
+ function spawnRefresh(now = Date.now()) {
3031
+ if (isDisabled()) return;
3032
+ const cache = readCacheSync();
3033
+ if (cache !== null && now - cache.lastCheck < ttlMs()) return;
3034
+ try {
3035
+ const entry = process.argv[1];
3036
+ const args = entry ? [entry] : [];
3037
+ const child = spawn2(process.execPath, args, {
3038
+ detached: true,
3039
+ stdio: "ignore",
3040
+ env: { ...process.env, [REFRESH_ENV]: "1" }
3041
+ });
3042
+ child.unref();
3043
+ } catch {
3044
+ }
3045
+ }
3046
+ async function runRefreshWorker() {
3047
+ await refreshIfStale(Date.now(), { force: true });
3048
+ }
2944
3049
  function getNoticeSync(current) {
2945
3050
  if (isDisabled()) return null;
2946
3051
  const cache = readCacheSync();
@@ -2981,14 +3086,17 @@ function installExitNotice(current) {
2981
3086
  printed = true;
2982
3087
  const n = getNoticeSync(current);
2983
3088
  if (n === null) return;
2984
- process.stderr.write(formatNotice(n));
3089
+ try {
3090
+ writeSync(2, formatNotice(n));
3091
+ } catch {
3092
+ }
2985
3093
  };
2986
3094
  process.on("beforeExit", emit);
2987
3095
  process.on("exit", emit);
2988
3096
  }
2989
3097
 
2990
3098
  // src/cli.ts
2991
- var version = true ? "0.8.0" : "0.0.0";
3099
+ var version = true ? "0.9.0" : "0.0.0";
2992
3100
  function handleActionError(command, json, err) {
2993
3101
  const ctx = { json, command };
2994
3102
  const message = err instanceof Error ? err.message : "unknown error";
@@ -3003,11 +3111,16 @@ function findFirstPositional(args) {
3003
3111
  }
3004
3112
  return -1;
3005
3113
  }
3006
- function run(argv = process.argv) {
3114
+ function isVersionRequest(args) {
3115
+ return args.includes("--version") || args.includes("-v");
3116
+ }
3117
+ async function run(argv = process.argv) {
3007
3118
  installExitNotice(version);
3008
- void refreshIfStale().catch(() => {
3009
- });
3010
3119
  const args = argv.slice(2);
3120
+ const versionRequested = isVersionRequest(args);
3121
+ if (!versionRequested) {
3122
+ spawnRefresh();
3123
+ }
3011
3124
  const firstPosIdx = findFirstPositional(args);
3012
3125
  const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
3013
3126
  const isStatic = namespace === "static";
@@ -3110,13 +3223,14 @@ function run(argv = process.argv) {
3110
3223
  repoCli.command("ls", "List repo requests (default: pending)").option("--json", "Output as JSON").option(
3111
3224
  "--status <status>",
3112
3225
  "pending | approved | active | rejected | failed | all"
3113
- ).option("--mine", "Only requests you submitted").action(
3226
+ ).option("--mine", "Only requests you submitted").option("--all", "Show every state (shorthand for --status all)").action(
3114
3227
  async (flags) => {
3115
3228
  try {
3116
3229
  await ls3({
3117
3230
  json: flags.json ?? false,
3118
3231
  status: flags.status,
3119
- mine: flags.mine ?? false
3232
+ mine: flags.mine ?? false,
3233
+ all: flags.all ?? false
3120
3234
  });
3121
3235
  } catch (err) {
3122
3236
  handleActionError("repo ls", flags.json ?? false, err);
@@ -3158,6 +3272,20 @@ function run(argv = process.argv) {
3158
3272
  handleActionError("repo status", flags.json ?? false, err);
3159
3273
  }
3160
3274
  });
3275
+ repoCli.command(
3276
+ "rm <id>",
3277
+ "Delete a request, freeing its repo name (admin only)"
3278
+ ).option("--json", "Output as JSON").option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").action(async (id, flags) => {
3279
+ try {
3280
+ await rm3({
3281
+ json: flags.json ?? false,
3282
+ id,
3283
+ yes: flags.yes ?? false
3284
+ });
3285
+ } catch (err) {
3286
+ handleActionError("repo rm", flags.json ?? false, err);
3287
+ }
3288
+ });
3161
3289
  repoCli.help();
3162
3290
  repoCli.version(version);
3163
3291
  const knownRepoSubs = /* @__PURE__ */ new Set([
@@ -3165,7 +3293,8 @@ function run(argv = process.argv) {
3165
3293
  "ls",
3166
3294
  "approve",
3167
3295
  "reject",
3168
- "status"
3296
+ "status",
3297
+ "rm"
3169
3298
  ]);
3170
3299
  const repoValueFlags = /* @__PURE__ */ new Set([
3171
3300
  "--visibility",
@@ -3300,6 +3429,9 @@ function run(argv = process.argv) {
3300
3429
  cli.version(version);
3301
3430
  cli.parse(argv);
3302
3431
  }
3432
+ if (versionRequested) {
3433
+ await refreshIfStale(Date.now(), { force: true });
3434
+ }
3303
3435
  }
3304
3436
 
3305
3437
  // src/lib/fatal.ts
@@ -3317,5 +3449,9 @@ function installFatalHandlers(onFatal = defaultOnFatal) {
3317
3449
  }
3318
3450
 
3319
3451
  // src/index.ts
3320
- installFatalHandlers();
3321
- run();
3452
+ if (process.env[REFRESH_ENV] === "1") {
3453
+ void runRefreshWorker();
3454
+ } else {
3455
+ installFatalHandlers();
3456
+ void run();
3457
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freecodecamp/universe-cli",
3
- "version": "0.8.0",
3
+ "version": "0.9.0",
4
4
  "main": "dist/index.js",
5
5
  "scripts": {
6
6
  "test": "vitest run",
@@ -11,7 +11,7 @@
11
11
  "prepare": "husky"
12
12
  },
13
13
  "author": "Mrugesh Mohapatra <noreply@mrugesh.dev> (https://mrugesh.dev/)",
14
- "license": "ISC",
14
+ "license": "BSD-3-Clause",
15
15
  "repository": {
16
16
  "type": "git",
17
17
  "url": "git+https://github.com/freeCodeCamp-Universe/universe-cli.git"
@@ -58,4 +58,4 @@
58
58
  "deployment",
59
59
  "freecodecamp"
60
60
  ]
61
- }
61
+ }