@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 +29 -13
- package/README.md +12 -71
- package/dist/index.cjs +155 -20
- package/dist/index.js +160 -24
- package/package.json +3 -3
package/LICENSE
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
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
|
|
88
|
-
|
|
89
|
-
universe
|
|
90
|
-
universe
|
|
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
|
-
|
|
118
|
-
|
|
119
|
-
No credential fields. The proxy holds the R2 admin key; the CLI never reads or writes one.
|
|
64
|
+
## Docs
|
|
120
65
|
|
|
121
|
-
|
|
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
|
-
|
|
68
|
+
## License
|
|
124
69
|
|
|
125
|
-
|
|
70
|
+
Copyright © 2014 freeCodeCamp.org
|
|
126
71
|
|
|
127
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "${
|
|
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:
|
|
25339
|
+
status: requestedStatus,
|
|
25327
25340
|
mine: options.mine ?? false
|
|
25328
25341
|
});
|
|
25329
|
-
const status2 =
|
|
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
|
|
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(
|
|
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
|
-
|
|
25583
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
25968
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 "${
|
|
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:
|
|
2690
|
+
status: requestedStatus,
|
|
2678
2691
|
mine: options.mine ?? false
|
|
2679
2692
|
});
|
|
2680
|
-
const status2 =
|
|
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/
|
|
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) =>
|
|
2803
|
-
const error = deps.logError ?? ((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 {
|
|
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
|
|
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(
|
|
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
|
-
|
|
2936
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
3321
|
-
|
|
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.
|
|
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": "
|
|
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
|
+
}
|