@freecodecamp/universe-cli 0.9.0 → 0.10.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.
Files changed (3) hide show
  1. package/dist/index.cjs +17204 -25798
  2. package/package.json +10 -8
  3. package/dist/index.js +0 -3457
package/dist/index.js DELETED
@@ -1,3457 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- // src/cli.ts
4
- import { cac } from "cac";
5
-
6
- // src/commands/deploy.ts
7
- import { readFile as readFile2 } from "fs/promises";
8
- import { resolve as resolve2 } from "path";
9
- import { log as log2, spinner } from "@clack/prompts";
10
-
11
- // src/output/exit-codes.ts
12
- var EXIT_USAGE = 10;
13
- var EXIT_CONFIG = 11;
14
- var EXIT_CREDENTIALS = 12;
15
- var EXIT_STORAGE = 13;
16
- var EXIT_GIT = 15;
17
- var EXIT_CONFIRM = 18;
18
- var EXIT_PARTIAL = 19;
19
- function exitWithCode(code) {
20
- process.exit(code);
21
- }
22
-
23
- // src/errors.ts
24
- var CliError = class extends Error {
25
- constructor(message) {
26
- super(message);
27
- this.name = new.target.name;
28
- }
29
- };
30
- var ConfigError = class extends CliError {
31
- exitCode = EXIT_CONFIG;
32
- };
33
- var CredentialError = class extends CliError {
34
- exitCode = EXIT_CREDENTIALS;
35
- };
36
- var StorageError = class extends CliError {
37
- exitCode = EXIT_STORAGE;
38
- };
39
- var GitError = class extends CliError {
40
- exitCode = EXIT_GIT;
41
- };
42
- var ConfirmError = class extends CliError {
43
- exitCode = EXIT_CONFIRM;
44
- };
45
- var PartialUploadError = class extends CliError {
46
- exitCode = EXIT_PARTIAL;
47
- };
48
- var UsageError = class extends CliError {
49
- exitCode = EXIT_USAGE;
50
- };
51
-
52
- // src/deploy/git.ts
53
- import { execSync } from "child_process";
54
- function getGitState() {
55
- try {
56
- const hash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
57
- const status2 = execSync("git status --porcelain", { encoding: "utf-8" });
58
- return { hash, dirty: status2.length > 0 };
59
- } catch {
60
- return { hash: null, dirty: false, error: "not a git repository" };
61
- }
62
- }
63
-
64
- // src/deploy/walk.ts
65
- import { readdirSync, realpathSync, statSync } from "fs";
66
- import { join, relative, sep } from "path";
67
- function walkFiles(base) {
68
- const baseReal = realpathSync(base);
69
- const entries = readdirSync(base, { recursive: true, withFileTypes: true });
70
- const files = [];
71
- for (const entry of entries) {
72
- const full = join(entry.parentPath, entry.name);
73
- const relPath = relative(base, full);
74
- let targetIsFile;
75
- try {
76
- targetIsFile = statSync(full).isFile();
77
- } catch {
78
- throw new StorageError(
79
- `"${relPath}" could not be stat'd (dangling symlink?)`
80
- );
81
- }
82
- if (!targetIsFile) continue;
83
- let resolved;
84
- try {
85
- resolved = realpathSync(full);
86
- } catch {
87
- throw new StorageError(
88
- `"${relPath}" could not be resolved (dangling symlink?)`
89
- );
90
- }
91
- const rel = relative(baseReal, resolved);
92
- if (rel === ".." || rel.startsWith(`..${sep}`)) {
93
- throw new StorageError(
94
- `"${relPath}" resolves outside the output directory (symlink escape). Resolved to: ${resolved}`
95
- );
96
- }
97
- files.push({ relPath, absPath: full });
98
- }
99
- return files;
100
- }
101
-
102
- // src/lib/build.ts
103
- import { spawn } from "child_process";
104
- import { stat } from "fs/promises";
105
- import { isAbsolute, resolve } from "path";
106
- var defaultExec = async (req) => {
107
- return new Promise((resolveExit, reject2) => {
108
- const child = spawn(req.command, {
109
- cwd: req.cwd,
110
- shell: true,
111
- stdio: "inherit"
112
- });
113
- child.on("error", (err) => reject2(err));
114
- child.on("exit", (code) => resolveExit(code ?? 1));
115
- });
116
- };
117
- async function ensureDirectory(absPath) {
118
- let st;
119
- try {
120
- st = await stat(absPath);
121
- } catch {
122
- throw new ConfigError(`output directory missing after build: ${absPath}`);
123
- }
124
- if (!st.isDirectory()) {
125
- throw new ConfigError(
126
- `output path is not a directory after build: ${absPath}`
127
- );
128
- }
129
- }
130
- async function runBuild(options, deps = {}) {
131
- const exec = deps.exec ?? defaultExec;
132
- const absCwd = isAbsolute(options.cwd) ? options.cwd : resolve(options.cwd);
133
- const absOutput = isAbsolute(options.outputDir) ? options.outputDir : resolve(absCwd, options.outputDir);
134
- if (!options.command) {
135
- await ensureDirectory(absOutput);
136
- return { skipped: true, outputDir: absOutput };
137
- }
138
- const code = await exec({ command: options.command, cwd: absCwd });
139
- if (code !== 0) {
140
- throw new ConfigError(
141
- `build command failed with exit code ${code}: ${options.command}`
142
- );
143
- }
144
- await ensureDirectory(absOutput);
145
- return { skipped: false, outputDir: absOutput };
146
- }
147
-
148
- // src/lib/constants.ts
149
- var DEFAULT_GH_CLIENT_ID = "Iv23liIuGmZRyPd5wUeN";
150
- var DEFAULT_PROXY_URL = "https://uploads.freecode.camp";
151
-
152
- // src/lib/identity.ts
153
- import { execFile } from "child_process";
154
- import { promisify } from "util";
155
-
156
- // src/lib/token-store.ts
157
- import { chmod, mkdir, readFile, rm, writeFile } from "fs/promises";
158
- import { homedir } from "os";
159
- import { dirname, join as join2 } from "path";
160
- var APP_DIR = "universe-cli";
161
- var TOKEN_FILE = "token";
162
- function configBase() {
163
- const xdg = process.env["XDG_CONFIG_HOME"];
164
- if (xdg && xdg.length > 0) return xdg;
165
- return join2(homedir(), ".config");
166
- }
167
- function tokenPath() {
168
- return join2(configBase(), APP_DIR, TOKEN_FILE);
169
- }
170
- async function saveToken(token) {
171
- const trimmed = token.trim();
172
- if (trimmed.length === 0) {
173
- throw new Error("refusing to save empty token");
174
- }
175
- const path = tokenPath();
176
- const dir = dirname(path);
177
- await mkdir(dir, { recursive: true, mode: 448 });
178
- await chmod(dir, 448);
179
- await writeFile(path, trimmed, { mode: 384 });
180
- await chmod(path, 384);
181
- }
182
- async function loadToken() {
183
- let raw;
184
- try {
185
- raw = await readFile(tokenPath(), "utf-8");
186
- } catch (err) {
187
- if (isFileNotFound(err)) return null;
188
- throw err;
189
- }
190
- const trimmed = raw.trim();
191
- return trimmed.length === 0 ? null : trimmed;
192
- }
193
- async function deleteToken() {
194
- await rm(tokenPath(), { force: true });
195
- }
196
- function isFileNotFound(err) {
197
- return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
198
- }
199
-
200
- // src/lib/identity.ts
201
- var execFileP = promisify(execFile);
202
- function isNonEmpty(s) {
203
- return typeof s === "string" && s.trim().length > 0;
204
- }
205
- async function defaultExecGhAuthToken() {
206
- try {
207
- const { stdout } = await execFileP("gh", ["auth", "token"], {
208
- timeout: 5e3
209
- });
210
- return stdout;
211
- } catch {
212
- return null;
213
- }
214
- }
215
- async function resolveIdentity(opts = {}) {
216
- const env = opts.env ?? process.env;
217
- const execGh = opts.execGhAuthToken ?? defaultExecGhAuthToken;
218
- const loadStored = opts.loadStoredToken ?? loadToken;
219
- const ghEnv = env["GITHUB_TOKEN"];
220
- if (isNonEmpty(ghEnv)) {
221
- return { token: ghEnv.trim(), source: "env_GITHUB_TOKEN" };
222
- }
223
- const ghTokenEnv = env["GH_TOKEN"];
224
- if (isNonEmpty(ghTokenEnv)) {
225
- return { token: ghTokenEnv.trim(), source: "env_GH_TOKEN" };
226
- }
227
- const stored = await loadStored();
228
- if (isNonEmpty(stored)) {
229
- return { token: stored.trim(), source: "device_flow" };
230
- }
231
- const ghCli = await execGh();
232
- if (isNonEmpty(ghCli)) {
233
- return { token: ghCli.trim(), source: "gh_cli" };
234
- }
235
- return null;
236
- }
237
-
238
- // src/lib/ignore.ts
239
- var SPECIAL = /[.+^${}()|[\]\\]/g;
240
- function globToRegexBody(pattern) {
241
- let out = "";
242
- let i = 0;
243
- while (i < pattern.length) {
244
- const ch = pattern[i] ?? "";
245
- if (ch === "*") {
246
- const next = pattern[i + 1];
247
- if (next === "*") {
248
- out += ".*";
249
- i += 2;
250
- continue;
251
- }
252
- out += "[^/]*";
253
- i += 1;
254
- continue;
255
- }
256
- if (ch === "?") {
257
- out += "[^/]";
258
- i += 1;
259
- continue;
260
- }
261
- out += ch.replace(SPECIAL, "\\$&");
262
- i += 1;
263
- }
264
- return out;
265
- }
266
- function compilePattern(pattern) {
267
- const anchored = pattern.includes("/");
268
- const body = globToRegexBody(pattern);
269
- if (anchored) {
270
- const re2 = new RegExp(`^${body}$`);
271
- return { test: (rel) => re2.test(rel) };
272
- }
273
- const re = new RegExp(`^${body}$`);
274
- return {
275
- test: (rel) => {
276
- const idx = rel.lastIndexOf("/");
277
- const base = idx === -1 ? rel : rel.slice(idx + 1);
278
- return re.test(base);
279
- }
280
- };
281
- }
282
- function normalize(rel) {
283
- let out = rel.replace(/\\/g, "/");
284
- if (out.startsWith("./")) out = out.slice(2);
285
- return out;
286
- }
287
- function createIgnoreFilter(patterns) {
288
- const compiled = patterns.filter((p) => p.length > 0).map(compilePattern);
289
- return (relPath) => {
290
- const norm = normalize(relPath);
291
- for (const c of compiled) {
292
- if (c.test(norm)) return true;
293
- }
294
- return false;
295
- };
296
- }
297
-
298
- // src/lib/platform-yaml.ts
299
- import { parse as parseYaml } from "yaml";
300
-
301
- // src/lib/platform-yaml.schema.ts
302
- import { z } from "zod";
303
- var SITE_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
304
- var siteName = z.string().min(1, "site is required").max(63, "site must be at most 63 characters").regex(
305
- SITE_NAME_PATTERN,
306
- "site must be lowercase letters, digits, and single hyphens; no leading/trailing/consecutive hyphens"
307
- );
308
- var buildSchema = z.object({
309
- command: z.string().min(1).optional(),
310
- output: z.string().min(1).default("dist")
311
- }).strict();
312
- var DEFAULT_DEPLOY_IGNORE = Object.freeze([
313
- "*.map",
314
- "node_modules/**",
315
- ".git/**",
316
- ".env*"
317
- ]);
318
- var deploySchema = z.object({
319
- preview: z.boolean().default(true),
320
- ignore: z.array(z.string()).default([...DEFAULT_DEPLOY_IGNORE])
321
- }).strict().prefault({});
322
- var platformYamlSchemaV2 = z.object({
323
- site: siteName,
324
- build: buildSchema.prefault({}),
325
- deploy: deploySchema
326
- }).strict();
327
-
328
- // src/lib/platform-yaml.ts
329
- var V1_MARKERS = ["r2", "stack", "domain", "static", "name"];
330
- var MIGRATION_HINT = "platform.yaml v1 detected. v0.4 removes credential paths (r2, bucket, region) and per-site team declarations. See docs/platform-yaml.md for the v0.3 \u2192 v0.4 migration.";
331
- function isPlainObject(value) {
332
- return typeof value === "object" && value !== null && !Array.isArray(value) && Object.getPrototypeOf(value) === Object.prototype;
333
- }
334
- function detectV1(parsed) {
335
- for (const marker of V1_MARKERS) {
336
- if (Object.prototype.hasOwnProperty.call(parsed, marker)) {
337
- return marker;
338
- }
339
- }
340
- return void 0;
341
- }
342
- function formatZodIssues(issues) {
343
- return issues.map((issue) => {
344
- const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
345
- return ` - ${path}: ${issue.message}`;
346
- }).join("\n");
347
- }
348
- function parsePlatformYaml(text) {
349
- let parsed;
350
- try {
351
- parsed = parseYaml(text);
352
- } catch (err) {
353
- const message = err instanceof Error ? err.message : String(err);
354
- return { ok: false, error: `platform.yaml is not valid YAML: ${message}` };
355
- }
356
- if (parsed === null || parsed === void 0) {
357
- return {
358
- ok: false,
359
- error: "platform.yaml is empty. Required field: `site`. See docs/platform-yaml.md."
360
- };
361
- }
362
- if (!isPlainObject(parsed)) {
363
- return {
364
- ok: false,
365
- error: "platform.yaml must be a YAML mapping at the root. See docs/platform-yaml.md."
366
- };
367
- }
368
- const v1Marker = detectV1(parsed);
369
- if (v1Marker) {
370
- return {
371
- ok: false,
372
- error: `${MIGRATION_HINT} (v1 marker detected: \`${v1Marker}\`)`
373
- };
374
- }
375
- const result = platformYamlSchemaV2.safeParse(parsed);
376
- if (!result.success) {
377
- return {
378
- ok: false,
379
- error: `platform.yaml is invalid:
380
- ${formatZodIssues(result.error.issues)}
381
- See docs/platform-yaml.md.`
382
- };
383
- }
384
- return { ok: true, value: result.data };
385
- }
386
-
387
- // src/lib/similarity.ts
388
- function editDistance(a, b) {
389
- const m = a.length;
390
- const n = b.length;
391
- if (m === 0) return n;
392
- if (n === 0) return m;
393
- const d = Array.from(
394
- { length: m + 1 },
395
- () => new Array(n + 1).fill(0)
396
- );
397
- for (let i = 0; i <= m; i++) d[i][0] = i;
398
- for (let j = 0; j <= n; j++) d[0][j] = j;
399
- for (let i = 1; i <= m; i++) {
400
- for (let j = 1; j <= n; j++) {
401
- const cost = a[i - 1] === b[j - 1] ? 0 : 1;
402
- d[i][j] = Math.min(
403
- d[i - 1][j] + 1,
404
- d[i][j - 1] + 1,
405
- d[i - 1][j - 1] + cost
406
- );
407
- if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) {
408
- d[i][j] = Math.min(d[i][j], d[i - 2][j - 2] + 1);
409
- }
410
- }
411
- }
412
- return d[m][n];
413
- }
414
- function suggest(target, candidates, threshold = 2) {
415
- if (candidates.length === 0) return null;
416
- const lc = target.toLowerCase();
417
- const sub = candidates.find((c) => {
418
- const clc = c.toLowerCase();
419
- return clc.includes(lc) || lc.includes(clc);
420
- });
421
- if (sub) return sub;
422
- let best = null;
423
- let bestD = threshold + 1;
424
- for (const c of candidates) {
425
- const d = editDistance(lc, c.toLowerCase());
426
- if (d < bestD) {
427
- bestD = d;
428
- best = c;
429
- }
430
- }
431
- return bestD <= threshold ? best : null;
432
- }
433
-
434
- // src/commands/repo/schema.ts
435
- import { z as z2 } from "zod";
436
- var REPO_NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]{0,99}$/;
437
- var repoName = z2.string().regex(
438
- REPO_NAME_RE,
439
- "must start with a letter or digit, then letters, digits, '.', '_' or '-' (max 100 chars)"
440
- );
441
- var visibilitySchema = z2.enum(["public", "private"]);
442
- var repoStatusSchema = z2.enum([
443
- "pending",
444
- "approved",
445
- "active",
446
- "rejected",
447
- "failed"
448
- ]);
449
- var createRepoRequestSchema = z2.object({
450
- name: repoName,
451
- visibility: visibilitySchema.default("private"),
452
- description: z2.string().max(350).optional(),
453
- template: repoName.optional()
454
- }).strict();
455
- var repoRowSchema = z2.object({
456
- id: z2.string(),
457
- name: z2.string(),
458
- owner: z2.string(),
459
- visibility: visibilitySchema,
460
- description: z2.string().optional(),
461
- template: z2.string().optional(),
462
- status: repoStatusSchema,
463
- url: z2.string().optional(),
464
- error: z2.string().optional(),
465
- requestedBy: z2.string(),
466
- approver: z2.string().optional(),
467
- rejectReason: z2.string().optional(),
468
- createdAt: z2.string(),
469
- updatedAt: z2.string()
470
- });
471
- var repoRowArraySchema = z2.array(repoRowSchema);
472
- var repoApproveResultSchema = z2.object({
473
- outcome: z2.enum(["ok", "approved_failed"]),
474
- request: repoRowSchema
475
- });
476
- var repoTemplatesResponseSchema = z2.object({
477
- templates: z2.array(z2.string())
478
- });
479
-
480
- // src/lib/proxy-client.ts
481
- var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
482
- function parseFetchTimeoutMs(env) {
483
- const raw = env["UNIVERSE_FETCH_TIMEOUT_MS"];
484
- if (raw === void 0 || raw === "") return void 0;
485
- const n = Number(raw);
486
- if (!Number.isFinite(n) || n < 0) return void 0;
487
- return Math.floor(n);
488
- }
489
- var ProxyError = class extends CliError {
490
- exitCode;
491
- status;
492
- code;
493
- requestId;
494
- constructor(status2, code, message, requestId) {
495
- super(message);
496
- this.status = status2;
497
- this.code = code;
498
- this.exitCode = mapExitCode(status2);
499
- this.requestId = requestId;
500
- }
501
- };
502
- var AliasDriftError = class extends ProxyError {
503
- current;
504
- constructor(message, current) {
505
- super(409, "alias_drift", message);
506
- this.current = current;
507
- }
508
- };
509
- function mapExitCode(status2) {
510
- if (status2 === 401 || status2 === 403) return EXIT_CREDENTIALS;
511
- if (status2 === 422 || status2 === 0 || status2 >= 500) return EXIT_STORAGE;
512
- return EXIT_USAGE;
513
- }
514
- function wrapProxyError(command, err) {
515
- if (err instanceof ProxyError) {
516
- let message = `${command} failed (${err.code}): ${err.message}`;
517
- if (err.code === "user_unauthorized") {
518
- message += "\n hint: the active GitHub token may lack the read:org scope or SSO authorization for the org. $GITHUB_TOKEN / $GH_TOKEN override `gh auth token` \u2014 run `universe whoami` to check the active identity source, then unset them or re-authorize the token (Configure SSO).";
519
- }
520
- return {
521
- code: err.exitCode,
522
- message,
523
- kind: err.code,
524
- requestId: err.requestId
525
- };
526
- }
527
- if (err instanceof CliError) {
528
- return { code: err.exitCode, message: err.message };
529
- }
530
- if (err instanceof Error) {
531
- return { code: EXIT_USAGE, message: err.message };
532
- }
533
- return { code: EXIT_USAGE, message: String(err) };
534
- }
535
- function isProxyErrorEnvelope(value) {
536
- return typeof value === "object" && value !== null && !Array.isArray(value) && "error" in value;
537
- }
538
- async function readErrorEnvelope(response) {
539
- const status2 = response.status;
540
- let raw;
541
- try {
542
- raw = await response.json();
543
- } catch {
544
- return {
545
- code: `http_${status2}`,
546
- message: response.statusText || "request failed"
547
- };
548
- }
549
- if (isProxyErrorEnvelope(raw) && raw.error) {
550
- const current = typeof raw.current === "string" ? raw.current : void 0;
551
- return {
552
- code: raw.error.code ?? `http_${status2}`,
553
- message: raw.error.message ?? response.statusText ?? "request failed",
554
- ...current === void 0 ? {} : { current }
555
- };
556
- }
557
- return {
558
- code: `http_${status2}`,
559
- message: response.statusText || "request failed"
560
- };
561
- }
562
- function throwProxyError(status2, env, requestId) {
563
- if (status2 === 409 && env.code === "alias_drift") {
564
- throw new AliasDriftError(env.message, env.current ?? "");
565
- }
566
- throw new ProxyError(status2, env.code, env.message, requestId);
567
- }
568
- function stripTrailingSlash(url) {
569
- return url.endsWith("/") ? url.slice(0, -1) : url;
570
- }
571
- function createProxyClient(cfg) {
572
- const base = stripTrailingSlash(cfg.baseUrl);
573
- const fetchImpl = cfg.fetch ?? globalThis.fetch.bind(globalThis);
574
- const timeoutMs = cfg.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
575
- const debug = cfg.debug ?? false;
576
- function withTimeoutSignal(init) {
577
- if (timeoutMs <= 0) return init;
578
- const timeoutSignal = AbortSignal.timeout(timeoutMs);
579
- const merged = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal;
580
- return { ...init, signal: merged };
581
- }
582
- function translateFetchError(err) {
583
- if (err instanceof DOMException && (err.name === "TimeoutError" || err.name === "AbortError")) {
584
- throw new ProxyError(
585
- 0,
586
- "timeout",
587
- `proxy timed out after ${timeoutMs}ms (${base})`
588
- );
589
- }
590
- const message = err instanceof Error ? err.message : String(err);
591
- throw new ProxyError(
592
- 0,
593
- "network_error",
594
- `proxy unreachable at ${base}: ${message}`
595
- );
596
- }
597
- async function userBearer() {
598
- const tok = await cfg.getAuthToken();
599
- return `Bearer ${tok}`;
600
- }
601
- async function call(url, init, validate) {
602
- const startedAt = debug ? Date.now() : 0;
603
- let response;
604
- try {
605
- response = await fetchImpl(url, withTimeoutSignal(init));
606
- } catch (err) {
607
- translateFetchError(err);
608
- }
609
- if (debug) {
610
- process.stderr.write(
611
- `[universe] ${init.method ?? "GET"} ${url} -> ${response.status} (${Date.now() - startedAt}ms)
612
- `
613
- );
614
- }
615
- if (!response.ok) {
616
- const requestId = response.headers.get("x-request-id") ?? void 0;
617
- const env = await readErrorEnvelope(response);
618
- throwProxyError(response.status, env, requestId);
619
- }
620
- if (response.status === 204) {
621
- return void 0;
622
- }
623
- let raw;
624
- try {
625
- raw = await response.json();
626
- } catch {
627
- throw new ProxyError(
628
- 0,
629
- "malformed_response",
630
- "proxy returned a non-JSON response body"
631
- );
632
- }
633
- if (validate) {
634
- try {
635
- validate(raw);
636
- } catch (err) {
637
- const detail = err instanceof Error ? err.message : "invalid shape";
638
- throw new ProxyError(
639
- 0,
640
- "malformed_response",
641
- `proxy returned an unexpected response shape: ${detail}`
642
- );
643
- }
644
- }
645
- return raw;
646
- }
647
- return {
648
- async whoami() {
649
- return call(`${base}/api/whoami`, {
650
- method: "GET",
651
- headers: {
652
- Authorization: await userBearer(),
653
- Accept: "application/json"
654
- }
655
- });
656
- },
657
- async deployInit(req) {
658
- return call(`${base}/api/deploy/init`, {
659
- method: "POST",
660
- headers: {
661
- Authorization: await userBearer(),
662
- Accept: "application/json",
663
- "Content-Type": "application/json"
664
- },
665
- body: JSON.stringify(req)
666
- });
667
- },
668
- async deployUpload(req) {
669
- const url = `${base}/api/deploy/${encodeURIComponent(
670
- req.deployId
671
- )}/upload?path=${encodeURIComponent(req.path)}`;
672
- return call(url, {
673
- method: "PUT",
674
- headers: {
675
- Authorization: `Bearer ${req.jwt}`,
676
- Accept: "application/json",
677
- "Content-Type": req.contentType ?? "application/octet-stream"
678
- },
679
- body: req.body
680
- });
681
- },
682
- async deployFinalize(req) {
683
- const url = `${base}/api/deploy/${encodeURIComponent(req.deployId)}/finalize`;
684
- return call(url, {
685
- method: "POST",
686
- headers: {
687
- Authorization: `Bearer ${req.jwt}`,
688
- Accept: "application/json",
689
- "Content-Type": "application/json"
690
- },
691
- body: JSON.stringify({ mode: req.mode, files: req.files })
692
- });
693
- },
694
- async siteDeploys(req) {
695
- const url = `${base}/api/site/${encodeURIComponent(req.site)}/deploys`;
696
- return call(url, {
697
- method: "GET",
698
- headers: {
699
- Authorization: await userBearer(),
700
- Accept: "application/json"
701
- }
702
- });
703
- },
704
- async getAlias(req) {
705
- const url = `${base}/api/site/${encodeURIComponent(req.site)}/alias/${encodeURIComponent(req.mode)}`;
706
- let response;
707
- try {
708
- response = await fetchImpl(
709
- url,
710
- withTimeoutSignal({
711
- method: "GET",
712
- headers: {
713
- Authorization: await userBearer(),
714
- Accept: "application/json"
715
- }
716
- })
717
- );
718
- } catch (err) {
719
- translateFetchError(err);
720
- }
721
- if (response.status === 404) return null;
722
- if (!response.ok) {
723
- const env = await readErrorEnvelope(response);
724
- throwProxyError(response.status, env);
725
- }
726
- return await response.json();
727
- },
728
- async sitePromote(req) {
729
- const url = `${base}/api/site/${encodeURIComponent(req.site)}/promote`;
730
- const headers = {
731
- Authorization: await userBearer(),
732
- Accept: "application/json"
733
- };
734
- const body = {};
735
- if (req.deployId !== void 0) body.deployId = req.deployId;
736
- if (req.expectedCurrent !== void 0)
737
- body.expectedCurrent = req.expectedCurrent;
738
- const hasBody = Object.keys(body).length > 0;
739
- if (hasBody) headers["Content-Type"] = "application/json";
740
- return call(url, {
741
- method: "POST",
742
- headers,
743
- ...hasBody ? { body: JSON.stringify(body) } : {}
744
- });
745
- },
746
- async siteRollback(req) {
747
- const url = `${base}/api/site/${encodeURIComponent(req.site)}/rollback`;
748
- const body = { to: req.to };
749
- if (req.expectedCurrent !== void 0)
750
- body.expectedCurrent = req.expectedCurrent;
751
- return call(url, {
752
- method: "POST",
753
- headers: {
754
- Authorization: await userBearer(),
755
- Accept: "application/json",
756
- "Content-Type": "application/json"
757
- },
758
- body: JSON.stringify(body)
759
- });
760
- },
761
- async registerSite(req) {
762
- const body = { slug: req.slug };
763
- if (req.teams && req.teams.length > 0) {
764
- body.teams = req.teams;
765
- }
766
- return call(`${base}/api/site/register`, {
767
- method: "POST",
768
- headers: {
769
- Authorization: await userBearer(),
770
- Accept: "application/json",
771
- "Content-Type": "application/json"
772
- },
773
- body: JSON.stringify(body)
774
- });
775
- },
776
- async listSites() {
777
- return call(`${base}/api/sites`, {
778
- method: "GET",
779
- headers: {
780
- Authorization: await userBearer(),
781
- Accept: "application/json"
782
- }
783
- });
784
- },
785
- async updateSite(req) {
786
- const url = `${base}/api/site/${encodeURIComponent(req.slug)}`;
787
- return call(url, {
788
- method: "PATCH",
789
- headers: {
790
- Authorization: await userBearer(),
791
- Accept: "application/json",
792
- "Content-Type": "application/json"
793
- },
794
- body: JSON.stringify({ teams: req.teams })
795
- });
796
- },
797
- async deleteSite(req) {
798
- const url = `${base}/api/site/${encodeURIComponent(req.slug)}`;
799
- return call(url, {
800
- method: "DELETE",
801
- headers: {
802
- Authorization: await userBearer(),
803
- Accept: "application/json"
804
- }
805
- });
806
- },
807
- async createRepoRequest(req) {
808
- const body = { name: req.name };
809
- if (req.visibility !== void 0) body.visibility = req.visibility;
810
- if (req.description !== void 0) body.description = req.description;
811
- if (req.template !== void 0 && req.template !== "") {
812
- body.template = req.template;
813
- }
814
- return call(
815
- `${base}/api/repo`,
816
- {
817
- method: "POST",
818
- headers: {
819
- Authorization: await userBearer(),
820
- Accept: "application/json",
821
- "Content-Type": "application/json"
822
- },
823
- body: JSON.stringify(body)
824
- },
825
- (raw) => repoRowSchema.parse(raw)
826
- );
827
- },
828
- async listRepoRequests(req) {
829
- const params = new URLSearchParams();
830
- if (req?.status) params.set("status", req.status);
831
- if (req?.mine) params.set("mine", "1");
832
- const qs = params.toString();
833
- const url = `${base}/api/repos${qs ? `?${qs}` : ""}`;
834
- return call(
835
- url,
836
- {
837
- method: "GET",
838
- headers: {
839
- Authorization: await userBearer(),
840
- Accept: "application/json"
841
- }
842
- },
843
- (raw) => repoRowArraySchema.parse(raw)
844
- );
845
- },
846
- async getRepoRequest(id) {
847
- const url = `${base}/api/repo/${encodeURIComponent(id)}`;
848
- return call(
849
- url,
850
- {
851
- method: "GET",
852
- headers: {
853
- Authorization: await userBearer(),
854
- Accept: "application/json"
855
- }
856
- },
857
- (raw) => repoRowSchema.parse(raw)
858
- );
859
- },
860
- async approveRepoRequest(req) {
861
- const url = `${base}/api/repo/${encodeURIComponent(req.id)}/approve`;
862
- return call(
863
- url,
864
- {
865
- method: "POST",
866
- headers: {
867
- Authorization: await userBearer(),
868
- Accept: "application/json"
869
- }
870
- },
871
- (raw) => repoApproveResultSchema.parse(raw)
872
- );
873
- },
874
- async rejectRepoRequest(req) {
875
- const url = `${base}/api/repo/${encodeURIComponent(req.id)}/reject`;
876
- const body = {};
877
- if (req.reason !== void 0) body.reason = req.reason;
878
- return call(
879
- url,
880
- {
881
- method: "POST",
882
- headers: {
883
- Authorization: await userBearer(),
884
- Accept: "application/json",
885
- "Content-Type": "application/json"
886
- },
887
- body: JSON.stringify(body)
888
- },
889
- (raw) => repoRowSchema.parse(raw)
890
- );
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
- },
902
- async listRepoTemplates() {
903
- const res = await call(
904
- `${base}/api/repo/templates`,
905
- {
906
- method: "GET",
907
- headers: {
908
- Authorization: await userBearer(),
909
- Accept: "application/json"
910
- }
911
- },
912
- (raw) => repoTemplatesResponseSchema.parse(raw)
913
- );
914
- return res.templates ?? [];
915
- }
916
- };
917
- }
918
-
919
- // src/lib/upload.ts
920
- import { readFile as defaultReadFile } from "fs/promises";
921
- var DEFAULT_CONCURRENCY = 6;
922
- var MIME_BY_EXT = Object.freeze({
923
- // text
924
- html: "text/html",
925
- htm: "text/html",
926
- css: "text/css",
927
- js: "text/javascript",
928
- mjs: "text/javascript",
929
- cjs: "text/javascript",
930
- json: "application/json",
931
- txt: "text/plain",
932
- md: "text/markdown",
933
- xml: "application/xml",
934
- csv: "text/csv",
935
- // images
936
- svg: "image/svg+xml",
937
- png: "image/png",
938
- jpg: "image/jpeg",
939
- jpeg: "image/jpeg",
940
- gif: "image/gif",
941
- webp: "image/webp",
942
- avif: "image/avif",
943
- ico: "image/x-icon",
944
- bmp: "image/bmp",
945
- // fonts
946
- woff: "font/woff",
947
- woff2: "font/woff2",
948
- ttf: "font/ttf",
949
- otf: "font/otf",
950
- eot: "application/vnd.ms-fontobject",
951
- // a/v
952
- mp4: "video/mp4",
953
- webm: "video/webm",
954
- ogg: "audio/ogg",
955
- mp3: "audio/mpeg",
956
- wav: "audio/wav",
957
- // other
958
- pdf: "application/pdf",
959
- wasm: "application/wasm"
960
- });
961
- function getContentType(filename) {
962
- const dot = filename.lastIndexOf(".");
963
- if (dot < 0 || dot === filename.length - 1) {
964
- return "application/octet-stream";
965
- }
966
- const ext = filename.slice(dot + 1).toLowerCase();
967
- return MIME_BY_EXT[ext] ?? "application/octet-stream";
968
- }
969
- function createLimit(max) {
970
- let active = 0;
971
- const queue = [];
972
- const acquire = () => {
973
- if (active < max) {
974
- active += 1;
975
- return Promise.resolve();
976
- }
977
- return new Promise((resolve6) => {
978
- queue.push(() => {
979
- active += 1;
980
- resolve6();
981
- });
982
- });
983
- };
984
- const release = () => {
985
- active -= 1;
986
- const next = queue.shift();
987
- if (next) next();
988
- };
989
- return async (fn) => {
990
- await acquire();
991
- try {
992
- return await fn();
993
- } finally {
994
- release();
995
- }
996
- };
997
- }
998
- async function uploadFiles(options, deps = {}) {
999
- const read = deps.readFile ?? defaultReadFile;
1000
- const concurrency = options.concurrency ?? DEFAULT_CONCURRENCY;
1001
- const limit = createLimit(concurrency);
1002
- const total = options.files.length;
1003
- const uploaded = [];
1004
- const errors = [];
1005
- let totalSize = 0;
1006
- let done = 0;
1007
- const tasks = options.files.map(
1008
- (file) => limit(async () => {
1009
- try {
1010
- const body = await read(file.absPath);
1011
- const bodyAsBodyInit = body;
1012
- await options.client.deployUpload({
1013
- deployId: options.deployId,
1014
- jwt: options.jwt,
1015
- path: file.relPath,
1016
- body: bodyAsBodyInit,
1017
- contentType: getContentType(file.relPath)
1018
- });
1019
- uploaded.push(file.relPath);
1020
- totalSize += body.byteLength;
1021
- } catch (err) {
1022
- const message = err instanceof Error ? err.message : "unknown upload error";
1023
- errors.push(`${file.relPath}: ${message}`);
1024
- } finally {
1025
- done += 1;
1026
- if (options.onProgress) {
1027
- options.onProgress({
1028
- uploaded: done,
1029
- total,
1030
- current: file.relPath
1031
- });
1032
- }
1033
- }
1034
- })
1035
- );
1036
- await Promise.all(tasks);
1037
- return {
1038
- fileCount: uploaded.length,
1039
- totalSize,
1040
- uploaded,
1041
- errors
1042
- };
1043
- }
1044
-
1045
- // src/output/envelope.ts
1046
- function buildEnvelope(command, success, data) {
1047
- return {
1048
- schemaVersion: "1",
1049
- command,
1050
- success,
1051
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1052
- ...data
1053
- };
1054
- }
1055
- function buildErrorEnvelope(command, code, message, issues, kind, requestId) {
1056
- const error = {
1057
- code,
1058
- message
1059
- };
1060
- if (kind !== void 0) {
1061
- error.kind = kind;
1062
- }
1063
- if (requestId !== void 0) {
1064
- error.requestId = requestId;
1065
- }
1066
- if (issues !== void 0) {
1067
- error.issues = issues;
1068
- }
1069
- return {
1070
- schemaVersion: "1",
1071
- command,
1072
- success: false,
1073
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1074
- error
1075
- };
1076
- }
1077
-
1078
- // src/output/format.ts
1079
- import { log } from "@clack/prompts";
1080
-
1081
- // src/output/redact.ts
1082
- var AWS_KEY_PREFIX_PATTERN = /(?:AKIA|ASIA|AROA|AIDA|ACCA|ANPA|ABIA|AGPA)[A-Z0-9]{12,}/g;
1083
- var URL_CREDS_PATTERN = /https?:\/\/[^@\s]+@/g;
1084
- var CREDENTIAL_CONTEXT_PATTERN = /(?:access_key_id|secret_access_key|accessKeyId|secretAccessKey|secret|password|token|key|credential|auth)\s*[=:]\s*([A-Za-z0-9/+=]{21,})/gi;
1085
- var HEX_CREDENTIAL_CONTEXT_PATTERN = /(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key)\s*[=:]\s*([a-f0-9]{32,})/gi;
1086
- var JSON_CREDENTIAL_PATTERN = /"(?:secret|password|token|key|credential|auth|access_key_id|secret_access_key|accessKeyId|secretAccessKey)"\s*:\s*"[^"]+"/gi;
1087
- var BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]+/gi;
1088
- var CREDENTIAL_KEY_PATTERN = /(?:secret|password|token|key|credential|auth)/i;
1089
- var EXACT_CREDENTIAL_KEYS = /* @__PURE__ */ new Set([
1090
- "accesskeyid",
1091
- "secretaccesskey",
1092
- "access_key_id",
1093
- "secret_access_key"
1094
- ]);
1095
- var STANDALONE_LONG_SECRET = /^[A-Za-z0-9/+=]{21,}$/;
1096
- function maskAwsKey(match) {
1097
- return match.slice(0, 4) + "****" + match.slice(-4);
1098
- }
1099
- function maskUrlCreds(match) {
1100
- const atIndex = match.lastIndexOf("@");
1101
- const protocolEnd = match.indexOf("://") + 3;
1102
- return match.slice(0, protocolEnd) + "****:****@" + match.slice(atIndex + 1);
1103
- }
1104
- function redact(value) {
1105
- let result = value;
1106
- result = result.replace(URL_CREDS_PATTERN, maskUrlCreds);
1107
- result = result.replace(AWS_KEY_PREFIX_PATTERN, maskAwsKey);
1108
- result = result.replace(BEARER_PATTERN, "Bearer ****");
1109
- result = result.replace(JSON_CREDENTIAL_PATTERN, (match) => {
1110
- const colonIndex = match.indexOf(":");
1111
- return match.slice(0, colonIndex + 1) + '"****"';
1112
- });
1113
- result = result.replace(
1114
- CREDENTIAL_CONTEXT_PATTERN,
1115
- (_match, _secret, _offset, _full) => {
1116
- const eqIndex = _match.indexOf("=");
1117
- const colonIndex = _match.indexOf(":");
1118
- const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
1119
- const prefix = _match.slice(0, sepIndex + 1);
1120
- return prefix + "****";
1121
- }
1122
- );
1123
- result = result.replace(HEX_CREDENTIAL_CONTEXT_PATTERN, (_match, _secret) => {
1124
- const eqIndex = _match.indexOf("=");
1125
- const colonIndex = _match.indexOf(":");
1126
- const sepIndex = eqIndex >= 0 && colonIndex >= 0 ? Math.min(eqIndex, colonIndex) : eqIndex >= 0 ? eqIndex : colonIndex;
1127
- const prefix = _match.slice(0, sepIndex + 1);
1128
- return prefix + "****";
1129
- });
1130
- return result;
1131
- }
1132
- function redactValue(value, keyName) {
1133
- if (typeof value === "string") {
1134
- if (keyName && EXACT_CREDENTIAL_KEYS.has(keyName.toLowerCase())) {
1135
- return "****";
1136
- }
1137
- const redacted = redact(value);
1138
- if (redacted === value && keyName && CREDENTIAL_KEY_PATTERN.test(keyName) && STANDALONE_LONG_SECRET.test(value)) {
1139
- return "****";
1140
- }
1141
- return redacted;
1142
- }
1143
- if (Array.isArray(value)) {
1144
- return value.map((v) => redactValue(v, keyName));
1145
- }
1146
- if (value !== null && typeof value === "object") {
1147
- return redactObject(value);
1148
- }
1149
- return value;
1150
- }
1151
- function redactObject(obj) {
1152
- const result = {};
1153
- for (const [key, value] of Object.entries(obj)) {
1154
- result[key] = redactValue(value, key);
1155
- }
1156
- return result;
1157
- }
1158
-
1159
- // src/output/format.ts
1160
- function outputError(ctx, code, message, optsOrIssues) {
1161
- const opts = Array.isArray(optsOrIssues) ? { issues: optsOrIssues } : optsOrIssues ?? {};
1162
- const redactedMessage = redact(message);
1163
- const redactedIssues = opts.issues?.map(redact);
1164
- if (ctx.json) {
1165
- const envelope = buildErrorEnvelope(
1166
- ctx.command,
1167
- code,
1168
- redactedMessage,
1169
- redactedIssues,
1170
- opts.kind,
1171
- opts.requestId
1172
- );
1173
- const payload = opts.extras ? { ...envelope, ...redactObject(opts.extras) } : envelope;
1174
- process.stdout.write(JSON.stringify(payload) + "\n");
1175
- } else {
1176
- (opts.logError ?? ((m) => log.error(m, { output: process.stderr })))(redactedMessage);
1177
- }
1178
- }
1179
-
1180
- // src/commands/deploy.ts
1181
- var defaultReadPlatformYaml = async (cwd) => {
1182
- return readFile2(resolve2(cwd, "platform.yaml"), "utf-8");
1183
- };
1184
- function emitJson(envelope) {
1185
- process.stdout.write(JSON.stringify(envelope) + "\n");
1186
- }
1187
- async function readAndParseConfig(cwd, read) {
1188
- let raw;
1189
- try {
1190
- raw = await read(cwd);
1191
- } catch (err) {
1192
- if (err instanceof Error && err.code === "ENOENT") {
1193
- throw new ConfigError(
1194
- `platform.yaml not found in ${cwd}. See docs/platform-yaml.md.`
1195
- );
1196
- }
1197
- throw err;
1198
- }
1199
- const r = parsePlatformYaml(raw);
1200
- if (!r.ok) throw new ConfigError(r.error);
1201
- return r.value;
1202
- }
1203
- function syntheticSha() {
1204
- return `nogit-${Date.now().toString(36)}`;
1205
- }
1206
- function rethrowProxy(prefix, err) {
1207
- if (err instanceof ProxyError) {
1208
- throw new ProxyError(
1209
- err.status,
1210
- err.code,
1211
- `${prefix} (${err.code}): ${err.message}`
1212
- );
1213
- }
1214
- if (err instanceof Error) throw new StorageError(`${prefix}: ${err.message}`);
1215
- throw new StorageError(`${prefix}: ${String(err)}`);
1216
- }
1217
- var PREFLIGHT_INLINE_LIST_CAP = 10;
1218
- function formatUnauthorizedSiteError(a) {
1219
- const lines = [
1220
- `Site '${a.attempted}' is not registered for your GitHub identity.`,
1221
- ``,
1222
- ` You are: ${a.login}`,
1223
- ``
1224
- ];
1225
- if (a.authorized.length === 0) {
1226
- lines.push(
1227
- ` Your identity is authorized for no sites yet.`,
1228
- ``,
1229
- ` Likely causes:`,
1230
- ` 1. The '${a.attempted}' slug is not registered.`,
1231
- ` Admin (staff): universe sites register ${a.attempted} --team <team>`,
1232
- ` 2. You are not in any team listed on any registered site.`,
1233
- ` Admin (staff): universe sites update <slug> --team +<your-team>`
1234
- );
1235
- return lines.join("\n");
1236
- }
1237
- const hint = suggest(a.attempted, a.authorized);
1238
- if (hint) {
1239
- lines.push(` Did you mean: ${hint}?`, ``);
1240
- }
1241
- lines.push(
1242
- ` Likely causes (most common first):`,
1243
- ` 1. Typo in platform.yaml \`site:\` \u2014 check the spelling above.`,
1244
- ` 2. The '${a.attempted}' slug is not registered yet.`,
1245
- ` Admin (staff): universe sites register ${a.attempted} --team <team>`,
1246
- ` 3. You are not in any team authorized for '${a.attempted}'.`,
1247
- ` Admin (staff): universe sites update ${a.attempted} --team +<your-team>`,
1248
- ``
1249
- );
1250
- if (a.authorized.length <= PREFLIGHT_INLINE_LIST_CAP) {
1251
- lines.push(
1252
- ` Your authorized sites (${a.authorized.length}):`,
1253
- ...[...a.authorized].sort().map((s) => ` - ${s}`)
1254
- );
1255
- } else {
1256
- lines.push(
1257
- ` You have ${a.authorized.length} authorized sites \u2014 too many to inline.`,
1258
- ` Run \`universe sites ls --mine\` to inspect the full list.`
1259
- );
1260
- }
1261
- return lines.join("\n");
1262
- }
1263
- async function deploy(options, deps = {}) {
1264
- const cwd = deps.cwd ?? process.cwd();
1265
- const env = deps.env ?? process.env;
1266
- const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml;
1267
- const resolveId = deps.resolveIdentity ?? resolveIdentity;
1268
- const mkClient = deps.createProxyClient ?? createProxyClient;
1269
- const gitState = deps.getGitState ?? getGitState;
1270
- const build = deps.runBuild ?? runBuild;
1271
- const walk = deps.walkFiles ?? walkFiles;
1272
- const upload = deps.uploadFiles ?? uploadFiles;
1273
- const mkSpinner = deps.createSpinner ?? (() => spinner());
1274
- const success = deps.logSuccess ?? ((s) => log2.success(s));
1275
- const info = deps.logInfo ?? ((s) => log2.info(s));
1276
- const warn = deps.logWarn ?? ((s) => log2.warn(s));
1277
- const error = deps.logError ?? ((s) => log2.error(s));
1278
- const exit = deps.exit ?? exitWithCode;
1279
- try {
1280
- const identity = await resolveId({ env });
1281
- if (!identity) {
1282
- throw new CredentialError(
1283
- "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
1284
- );
1285
- }
1286
- const config = await readAndParseConfig(cwd, readYaml);
1287
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1288
- const client = mkClient({
1289
- baseUrl,
1290
- getAuthToken: () => identity.token,
1291
- timeoutMs: parseFetchTimeoutMs(env)
1292
- });
1293
- let me;
1294
- try {
1295
- me = await client.whoami();
1296
- } catch (err) {
1297
- rethrowProxy("whoami preflight failed", err);
1298
- }
1299
- if (!me.authorizedSites.includes(config.site)) {
1300
- throw new CredentialError(
1301
- formatUnauthorizedSiteError({
1302
- attempted: config.site,
1303
- login: me.login,
1304
- authorized: me.authorizedSites
1305
- })
1306
- );
1307
- }
1308
- const git = gitState();
1309
- if (git.dirty && !options.json) {
1310
- warn(
1311
- "git working tree is dirty \u2014 uncommitted changes will not be reflected."
1312
- );
1313
- }
1314
- const sha = git.hash ?? syntheticSha();
1315
- const outputDir = options.dir ?? config.build.output;
1316
- const buildResult = await build({
1317
- command: config.build.command,
1318
- cwd,
1319
- outputDir
1320
- });
1321
- if (buildResult.skipped && !options.json) {
1322
- info("build.command not set \u2014 using pre-built output.");
1323
- }
1324
- const resolvedOutputDir = buildResult.outputDir;
1325
- const walked = walk(resolvedOutputDir);
1326
- const ignore = createIgnoreFilter(config.deploy.ignore);
1327
- const filtered = walked.filter((f) => !ignore(f.relPath));
1328
- if (filtered.length === 0) {
1329
- throw new GitError(`No files to deploy under ${resolvedOutputDir}.`);
1330
- }
1331
- const fileList = filtered.map((f) => f.relPath);
1332
- let initResult;
1333
- try {
1334
- initResult = await client.deployInit({
1335
- site: config.site,
1336
- sha,
1337
- files: fileList
1338
- });
1339
- } catch (err) {
1340
- rethrowProxy("deploy init failed", err);
1341
- }
1342
- const spin = options.json ? null : mkSpinner();
1343
- spin?.start(`Uploading 0/${filtered.length} files`);
1344
- const uploadResult = await upload({
1345
- client,
1346
- deployId: initResult.deployId,
1347
- jwt: initResult.jwt,
1348
- files: filtered,
1349
- onProgress: spin ? (p) => spin.message(`Uploading ${p.uploaded}/${p.total} \u2014 ${p.current}`) : void 0
1350
- });
1351
- if (uploadResult.errors.length > 0) {
1352
- spin?.error(`Upload failed: ${uploadResult.errors.length} file(s)`);
1353
- const message = `Upload partially failed: ${uploadResult.errors.length} file(s) failed:
1354
- - ${uploadResult.errors.join("\n - ")}`;
1355
- throw new PartialUploadError(message);
1356
- }
1357
- spin?.stop(`Uploaded ${uploadResult.fileCount} files`);
1358
- const mode = options.promote ? "production" : "preview";
1359
- let finalizeResult;
1360
- try {
1361
- finalizeResult = await client.deployFinalize({
1362
- deployId: initResult.deployId,
1363
- jwt: initResult.jwt,
1364
- mode,
1365
- files: fileList
1366
- });
1367
- } catch (err) {
1368
- rethrowProxy("deploy finalize failed", err);
1369
- }
1370
- if (options.promote && !options.json) {
1371
- try {
1372
- const preview = await client.getAlias({
1373
- site: config.site,
1374
- mode: "preview"
1375
- });
1376
- if (preview && preview.deployId !== finalizeResult.deployId) {
1377
- warn(
1378
- `Preview alias still points to ${preview.deployId}; it will not auto-update. Run \`universe static deploy\` (without --promote) to refresh preview.`
1379
- );
1380
- }
1381
- } catch (err) {
1382
- if (err instanceof ProxyError && (err.status === 401 || err.status === 403)) {
1383
- warn(
1384
- `Preview alias probe got ${err.status} (${err.code}) \u2014 token may need rotation: ${err.message}`
1385
- );
1386
- }
1387
- }
1388
- }
1389
- if (options.json) {
1390
- emitJson(
1391
- buildEnvelope("deploy", true, {
1392
- deployId: finalizeResult.deployId,
1393
- url: finalizeResult.url,
1394
- mode: finalizeResult.mode,
1395
- site: config.site,
1396
- sha,
1397
- fileCount: uploadResult.fileCount,
1398
- totalSize: uploadResult.totalSize,
1399
- identitySource: identity.source
1400
- })
1401
- );
1402
- } else {
1403
- const sizeKB = (uploadResult.totalSize / 1024).toFixed(1);
1404
- const nextLine = mode === "preview" ? `Next: universe static promote --from ${finalizeResult.deployId}` : "Promoted to production.\nPreview alias unchanged.";
1405
- success(
1406
- [
1407
- `Deployed ${finalizeResult.deployId}`,
1408
- ``,
1409
- ` Site: ${config.site}`,
1410
- ` Files: ${uploadResult.fileCount}`,
1411
- ` Size: ${sizeKB} KB`,
1412
- ` Mode: ${mode}`,
1413
- ` URL: ${finalizeResult.url}`,
1414
- ``,
1415
- nextLine
1416
- ].join("\n")
1417
- );
1418
- }
1419
- } catch (err) {
1420
- let code;
1421
- let message;
1422
- if (err instanceof ProxyError) {
1423
- code = err.exitCode;
1424
- message = err.message;
1425
- } else if (err instanceof CliError) {
1426
- code = err.exitCode;
1427
- message = err.message;
1428
- } else if (err instanceof Error) {
1429
- code = EXIT_USAGE;
1430
- message = err.message;
1431
- } else {
1432
- code = EXIT_USAGE;
1433
- message = String(err);
1434
- }
1435
- outputError({ json: options.json, command: "deploy" }, code, message, {
1436
- logError: error
1437
- });
1438
- exit(code);
1439
- }
1440
- }
1441
-
1442
- // src/commands/login.ts
1443
- import { log as log3 } from "@clack/prompts";
1444
-
1445
- // src/lib/device-flow.ts
1446
- var DEVICE_CODE_URL = "https://github.com/login/device/code";
1447
- var ACCESS_TOKEN_URL = "https://github.com/login/oauth/access_token";
1448
- var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
1449
- function isAccessTokenSuccess(body) {
1450
- return "access_token" in body && typeof body.access_token === "string";
1451
- }
1452
- var defaultSleep = (ms) => new Promise((resolve6) => setTimeout(resolve6, ms));
1453
- async function runDeviceFlow(opts) {
1454
- const fetchImpl = opts.fetch ?? globalThis.fetch.bind(globalThis);
1455
- const sleep = opts.sleep ?? defaultSleep;
1456
- const startResp = await fetchImpl(DEVICE_CODE_URL, {
1457
- method: "POST",
1458
- headers: {
1459
- Accept: "application/json",
1460
- "Content-Type": "application/json"
1461
- },
1462
- body: JSON.stringify({
1463
- client_id: opts.clientId,
1464
- ...opts.scope ? { scope: opts.scope } : {}
1465
- })
1466
- });
1467
- if (!startResp.ok) {
1468
- throw new Error(`device code request failed: HTTP ${startResp.status}`);
1469
- }
1470
- const start = await startResp.json();
1471
- await opts.onPrompt({
1472
- userCode: start.user_code,
1473
- verificationUri: start.verification_uri,
1474
- expiresIn: start.expires_in
1475
- });
1476
- let intervalSec = start.interval > 0 ? start.interval : 5;
1477
- while (true) {
1478
- await sleep(intervalSec * 1e3);
1479
- const pollResp = await fetchImpl(ACCESS_TOKEN_URL, {
1480
- method: "POST",
1481
- headers: {
1482
- Accept: "application/json",
1483
- "Content-Type": "application/json"
1484
- },
1485
- body: JSON.stringify({
1486
- client_id: opts.clientId,
1487
- device_code: start.device_code,
1488
- grant_type: DEVICE_CODE_GRANT
1489
- })
1490
- });
1491
- if (!pollResp.ok) {
1492
- throw new Error(`device flow poll failed: HTTP ${pollResp.status}`);
1493
- }
1494
- const body = await pollResp.json();
1495
- if (isAccessTokenSuccess(body)) {
1496
- return body.access_token;
1497
- }
1498
- switch (body.error) {
1499
- case "authorization_pending":
1500
- continue;
1501
- case "slow_down":
1502
- intervalSec += 5;
1503
- continue;
1504
- case "expired_token":
1505
- throw new Error(
1506
- "device flow expired before authorization. Run `universe login` again."
1507
- );
1508
- case "access_denied":
1509
- throw new Error("device flow access denied by user.");
1510
- default:
1511
- throw new Error(
1512
- body.error_description ? `device flow error: ${body.error}: ${body.error_description}` : `device flow error: ${body.error}`
1513
- );
1514
- }
1515
- }
1516
- }
1517
-
1518
- // src/commands/login.ts
1519
- var DEFAULT_SCOPE = "read:org user:email";
1520
- var DEFAULT_PROXY_URL2 = "https://uploads.freecode.camp";
1521
- var NO_SITES_WARNING = [
1522
- "Logged in, but the proxy reports 0 authorized sites for your account.",
1523
- "This usually means the Universe CLI GitHub App is not installed on the org",
1524
- "that owns the registry-authz team (production: `freeCodeCamp-Universe`), or",
1525
- "your account is not on a team granted access to any site.",
1526
- "",
1527
- "Next steps:",
1528
- " 1. Run `universe whoami` to confirm the identity that resolved.",
1529
- " 2. Ask an org owner to install the Universe CLI GitHub App on the org.",
1530
- " 3. Confirm your team membership at",
1531
- " https://github.com/orgs/freeCodeCamp-Universe/teams."
1532
- ].join("\n");
1533
- function emitJson2(envelope) {
1534
- process.stdout.write(JSON.stringify(envelope) + "\n");
1535
- }
1536
- async function login(options, deps = {}) {
1537
- const env = deps.env ?? process.env;
1538
- const runFlow = deps.runDeviceFlow ?? runDeviceFlow;
1539
- const save = deps.saveToken ?? saveToken;
1540
- const load = deps.loadToken ?? loadToken;
1541
- const success = deps.logSuccess ?? ((s) => log3.success(s));
1542
- const info = deps.logInfo ?? ((s) => log3.info(s));
1543
- const error = deps.logError ?? ((s) => log3.error(s));
1544
- const exit = deps.exit ?? exitWithCode;
1545
- const envClientId = env["UNIVERSE_GH_CLIENT_ID"];
1546
- const clientId = envClientId && envClientId.trim().length > 0 ? envClientId : DEFAULT_GH_CLIENT_ID;
1547
- if (!options.force) {
1548
- const existing = await load();
1549
- if (existing) {
1550
- const msg = "Already logged in. Run `universe logout` first or pass --force to replace the stored token.";
1551
- if (options.json) {
1552
- emitJson2({
1553
- schemaVersion: "1",
1554
- command: "login",
1555
- success: false,
1556
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
1557
- error: { code: EXIT_CONFIRM, message: msg }
1558
- });
1559
- } else {
1560
- error(msg);
1561
- }
1562
- exit(EXIT_CONFIRM);
1563
- return;
1564
- }
1565
- }
1566
- let token;
1567
- try {
1568
- token = await runFlow({
1569
- clientId,
1570
- scope: DEFAULT_SCOPE,
1571
- onPrompt: ({ userCode, verificationUri, expiresIn }) => {
1572
- if (options.json) {
1573
- emitJson2(
1574
- buildEnvelope("login", true, {
1575
- userCode,
1576
- verificationUri,
1577
- expiresIn,
1578
- stored: false
1579
- })
1580
- );
1581
- } else {
1582
- info(
1583
- [
1584
- `Open ${verificationUri} in your browser`,
1585
- `and enter code: ${userCode}`,
1586
- `(code expires in ${Math.round(expiresIn / 60)} min)`
1587
- ].join("\n")
1588
- );
1589
- }
1590
- }
1591
- });
1592
- } catch (err) {
1593
- const message = err instanceof Error ? err.message : String(err);
1594
- outputError(
1595
- { json: options.json, command: "login" },
1596
- EXIT_CREDENTIALS,
1597
- message,
1598
- { logError: error }
1599
- );
1600
- exit(EXIT_CREDENTIALS);
1601
- return;
1602
- }
1603
- await save(token);
1604
- const selfCheck = await postLoginSelfCheck(token, env, deps);
1605
- if (options.json) {
1606
- emitJson2(
1607
- buildEnvelope("login", true, {
1608
- stored: true,
1609
- ...selfCheck.checked ? {
1610
- authorizedSitesCount: selfCheck.authorizedSitesCount,
1611
- ...selfCheck.warning ? { warning: selfCheck.warning } : {}
1612
- } : {}
1613
- })
1614
- );
1615
- } else {
1616
- success("Logged in. Token stored at ~/.config/universe-cli/token.");
1617
- if (selfCheck.checked && selfCheck.warning) {
1618
- const warn = deps.logWarn ?? ((s) => log3.warn(s));
1619
- warn(selfCheck.warning);
1620
- }
1621
- }
1622
- }
1623
- async function postLoginSelfCheck(token, env, deps) {
1624
- const mkClient = deps.createProxyClient ?? createProxyClient;
1625
- try {
1626
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL2;
1627
- const client = mkClient({
1628
- baseUrl,
1629
- getAuthToken: () => token,
1630
- timeoutMs: parseFetchTimeoutMs(env)
1631
- });
1632
- const result = await client.whoami();
1633
- const count = result.authorizedSites.length;
1634
- if (count === 0) {
1635
- return {
1636
- checked: true,
1637
- authorizedSitesCount: 0,
1638
- warning: NO_SITES_WARNING
1639
- };
1640
- }
1641
- return { checked: true, authorizedSitesCount: count };
1642
- } catch {
1643
- return { checked: false, authorizedSitesCount: 0 };
1644
- }
1645
- }
1646
-
1647
- // src/commands/logout.ts
1648
- import { log as log4 } from "@clack/prompts";
1649
- function emitJson3(envelope) {
1650
- process.stdout.write(JSON.stringify(envelope) + "\n");
1651
- }
1652
- async function logout(options, deps = {}) {
1653
- const load = deps.loadToken ?? loadToken;
1654
- const del = deps.deleteToken ?? deleteToken;
1655
- const success = deps.logSuccess ?? ((s) => log4.success(s));
1656
- const info = deps.logInfo ?? ((s) => log4.info(s));
1657
- const existing = await load();
1658
- await del();
1659
- if (options.json) {
1660
- emitJson3(buildEnvelope("logout", true, { removed: existing !== null }));
1661
- return;
1662
- }
1663
- if (existing) {
1664
- success("Logged out. Stored token removed.");
1665
- } else {
1666
- info("No token was stored. Nothing to remove.");
1667
- }
1668
- }
1669
-
1670
- // src/commands/ls.ts
1671
- import { readFile as readFile3 } from "fs/promises";
1672
- import { resolve as resolve3 } from "path";
1673
- import { log as log5 } from "@clack/prompts";
1674
- var defaultReadPlatformYaml2 = async (cwd) => {
1675
- return readFile3(resolve3(cwd, "platform.yaml"), "utf-8");
1676
- };
1677
- function emitJson4(envelope) {
1678
- process.stdout.write(JSON.stringify(envelope) + "\n");
1679
- }
1680
- var DEPLOY_ID_RE = /^(\d{8})-(\d{6})-(\S+)$/;
1681
- function parseDeployId(deployId) {
1682
- const m = DEPLOY_ID_RE.exec(deployId);
1683
- if (!m) return { deployId, timestamp: null, sha: null };
1684
- const [, ymd, hms, sha] = m;
1685
- if (!ymd || !hms || !sha) return { deployId, timestamp: null, sha: null };
1686
- const iso = `${ymd.slice(0, 4)}-${ymd.slice(4, 6)}-${ymd.slice(6, 8)}T${hms.slice(0, 2)}:${hms.slice(2, 4)}:${hms.slice(4, 6)}Z`;
1687
- return { deployId, timestamp: iso, sha };
1688
- }
1689
- async function readSiteFromYaml(cwd, read) {
1690
- let raw;
1691
- try {
1692
- raw = await read(cwd);
1693
- } catch (err) {
1694
- if (err instanceof Error && err.code === "ENOENT") {
1695
- return null;
1696
- }
1697
- throw err;
1698
- }
1699
- const r = parsePlatformYaml(raw);
1700
- if (!r.ok) throw new ConfigError(r.error);
1701
- const config = r.value;
1702
- return config.site;
1703
- }
1704
- function formatTable(deploys) {
1705
- const header = ["DEPLOY ID", "TIMESTAMP", "SHA"];
1706
- const rows = deploys.map((d) => [
1707
- d.deployId,
1708
- d.timestamp ? d.timestamp.replace("T", " ").replace("Z", "") : "\u2014",
1709
- d.sha ?? "\u2014"
1710
- ]);
1711
- const widths = header.map(
1712
- (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
1713
- );
1714
- const fmt = (cells) => cells.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
1715
- return [fmt(header), ...rows.map(fmt)].join("\n");
1716
- }
1717
- async function ls(options, deps = {}) {
1718
- const cwd = deps.cwd ?? process.cwd();
1719
- const env = deps.env ?? process.env;
1720
- const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml2;
1721
- const resolveId = deps.resolveIdentity ?? resolveIdentity;
1722
- const mkClient = deps.createProxyClient ?? createProxyClient;
1723
- const success = deps.logSuccess ?? ((s) => log5.success(s));
1724
- const info = deps.logInfo ?? ((s) => log5.info(s));
1725
- const error = deps.logError ?? ((s) => log5.error(s));
1726
- const exit = deps.exit ?? exitWithCode;
1727
- try {
1728
- const identity = await resolveId({ env });
1729
- if (!identity) {
1730
- throw new CredentialError(
1731
- "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
1732
- );
1733
- }
1734
- let site = options.site?.trim() || null;
1735
- if (!site) {
1736
- site = await readSiteFromYaml(cwd, readYaml);
1737
- }
1738
- if (!site) {
1739
- throw new ConfigError(
1740
- "No site to list. Run from a directory with `platform.yaml`, or pass `--site <name>`."
1741
- );
1742
- }
1743
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1744
- const client = mkClient({
1745
- baseUrl,
1746
- getAuthToken: () => identity.token,
1747
- timeoutMs: parseFetchTimeoutMs(env)
1748
- });
1749
- const raw = await client.siteDeploys({ site });
1750
- const sorted = [...raw].sort(
1751
- (a, b) => b.deployId.localeCompare(a.deployId)
1752
- );
1753
- const deploys = sorted.map((d) => parseDeployId(d.deployId));
1754
- if (options.json) {
1755
- emitJson4(
1756
- buildEnvelope("ls", true, {
1757
- site,
1758
- deploys,
1759
- identitySource: identity.source
1760
- })
1761
- );
1762
- return;
1763
- }
1764
- if (deploys.length === 0) {
1765
- info(`(no deploys for ${site})`);
1766
- return;
1767
- }
1768
- success(formatTable(deploys));
1769
- } catch (err) {
1770
- const { code, message } = wrapProxyError("ls", err);
1771
- outputError({ json: options.json, command: "ls" }, code, message, {
1772
- logError: error
1773
- });
1774
- exit(code);
1775
- }
1776
- }
1777
-
1778
- // src/commands/promote.ts
1779
- import { readFile as readFile4 } from "fs/promises";
1780
- import { resolve as resolve4 } from "path";
1781
- import { confirm, isCancel, log as log6 } from "@clack/prompts";
1782
- var defaultPromptConfirm = async (msg) => {
1783
- const r = await confirm({ message: msg, initialValue: false });
1784
- if (isCancel(r)) return false;
1785
- return r === true;
1786
- };
1787
- var defaultReadPlatformYaml3 = async (cwd) => {
1788
- return readFile4(resolve4(cwd, "platform.yaml"), "utf-8");
1789
- };
1790
- function emitJson5(envelope) {
1791
- process.stdout.write(JSON.stringify(envelope) + "\n");
1792
- }
1793
- async function readAndParseConfig2(cwd, read) {
1794
- let raw;
1795
- try {
1796
- raw = await read(cwd);
1797
- } catch (err) {
1798
- if (err instanceof Error && err.code === "ENOENT") {
1799
- throw new ConfigError(
1800
- `platform.yaml not found in ${cwd}. See docs/platform-yaml.md.`
1801
- );
1802
- }
1803
- throw err;
1804
- }
1805
- const r = parsePlatformYaml(raw);
1806
- if (!r.ok) throw new ConfigError(r.error);
1807
- return r.value;
1808
- }
1809
- async function promote(options, deps = {}) {
1810
- const cwd = deps.cwd ?? process.cwd();
1811
- const env = deps.env ?? process.env;
1812
- const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml3;
1813
- const resolveId = deps.resolveIdentity ?? resolveIdentity;
1814
- const mkClient = deps.createProxyClient ?? createProxyClient;
1815
- const success = deps.logSuccess ?? ((s) => log6.success(s));
1816
- const error = deps.logError ?? ((s) => log6.error(s));
1817
- const exit = deps.exit ?? exitWithCode;
1818
- const promptConfirm = deps.promptConfirm ?? defaultPromptConfirm;
1819
- try {
1820
- const identity = await resolveId({ env });
1821
- if (!identity) {
1822
- throw new CredentialError(
1823
- "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
1824
- );
1825
- }
1826
- const config = await readAndParseConfig2(cwd, readYaml);
1827
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1828
- const client = mkClient({
1829
- baseUrl,
1830
- getAuthToken: () => identity.token,
1831
- timeoutMs: parseFetchTimeoutMs(env)
1832
- });
1833
- let result;
1834
- if (options.from) {
1835
- const prod = await client.getAlias({
1836
- site: config.site,
1837
- mode: "production"
1838
- });
1839
- const initialExpected = prod?.deployId ?? "";
1840
- try {
1841
- result = await client.siteRollback({
1842
- site: config.site,
1843
- to: options.from,
1844
- expectedCurrent: initialExpected
1845
- });
1846
- } catch (err) {
1847
- if (!(err instanceof AliasDriftError)) throw err;
1848
- if (options.json) throw err;
1849
- error(
1850
- `drift: production moved to ${err.current}, expected ${initialExpected}`
1851
- );
1852
- const ok = await promptConfirm(
1853
- `Retry promote --from with expectedCurrent='${err.current}'?`
1854
- );
1855
- if (!ok) throw err;
1856
- result = await client.siteRollback({
1857
- site: config.site,
1858
- to: options.from,
1859
- expectedCurrent: err.current
1860
- });
1861
- }
1862
- } else {
1863
- const preview = await client.getAlias({
1864
- site: config.site,
1865
- mode: "preview"
1866
- });
1867
- if (preview === null) {
1868
- throw new ConfigError(
1869
- "no preview alias to promote \u2014 run `universe static deploy` first"
1870
- );
1871
- }
1872
- const prod = await client.getAlias({
1873
- site: config.site,
1874
- mode: "production"
1875
- });
1876
- if (!options.json) {
1877
- success(
1878
- `Promoting ${preview.deployId} \u2192 ${prod?.deployId ?? "<none>"}`
1879
- );
1880
- }
1881
- const initialExpected = prod?.deployId ?? "";
1882
- try {
1883
- result = await client.sitePromote({
1884
- site: config.site,
1885
- deployId: preview.deployId,
1886
- expectedCurrent: initialExpected
1887
- });
1888
- } catch (err) {
1889
- if (!(err instanceof AliasDriftError)) throw err;
1890
- if (options.json) throw err;
1891
- error(
1892
- `drift: production moved to ${err.current}, expected ${initialExpected}`
1893
- );
1894
- const ok = await promptConfirm(
1895
- `Retry promote with expectedCurrent='${err.current}'?`
1896
- );
1897
- if (!ok) throw err;
1898
- result = await client.sitePromote({
1899
- site: config.site,
1900
- deployId: preview.deployId,
1901
- expectedCurrent: err.current
1902
- });
1903
- }
1904
- }
1905
- if (options.json) {
1906
- emitJson5(
1907
- buildEnvelope("promote", true, {
1908
- deployId: result.deployId,
1909
- url: result.url,
1910
- site: config.site,
1911
- identitySource: identity.source
1912
- })
1913
- );
1914
- } else {
1915
- const lines = [
1916
- `Promoted ${result.deployId} to production`,
1917
- ``,
1918
- ` Site: ${config.site}`,
1919
- ` Deploy: ${result.deployId}`,
1920
- ` Production: ${result.url}`
1921
- ];
1922
- if (options.from) {
1923
- lines.push(``, "Preview alias unchanged.");
1924
- }
1925
- success(lines.join("\n"));
1926
- }
1927
- } catch (err) {
1928
- const { code, message } = wrapProxyError("promote", err);
1929
- const extras = err instanceof AliasDriftError ? { current: err.current } : void 0;
1930
- outputError({ json: options.json, command: "promote" }, code, message, {
1931
- logError: error,
1932
- extras
1933
- });
1934
- exit(code);
1935
- }
1936
- }
1937
-
1938
- // src/commands/rollback.ts
1939
- import { readFile as readFile5 } from "fs/promises";
1940
- import { resolve as resolve5 } from "path";
1941
- import { confirm as confirm2, isCancel as isCancel2, log as log7 } from "@clack/prompts";
1942
- var defaultPromptConfirm2 = async (msg) => {
1943
- const r = await confirm2({ message: msg, initialValue: false });
1944
- if (isCancel2(r)) return false;
1945
- return r === true;
1946
- };
1947
- var defaultReadPlatformYaml4 = async (cwd) => {
1948
- return readFile5(resolve5(cwd, "platform.yaml"), "utf-8");
1949
- };
1950
- function emitJson6(envelope) {
1951
- process.stdout.write(JSON.stringify(envelope) + "\n");
1952
- }
1953
- async function readAndParseConfig3(cwd, read) {
1954
- let raw;
1955
- try {
1956
- raw = await read(cwd);
1957
- } catch (err) {
1958
- if (err instanceof Error && err.code === "ENOENT") {
1959
- throw new ConfigError(
1960
- `platform.yaml not found in ${cwd}. See docs/platform-yaml.md.`
1961
- );
1962
- }
1963
- throw err;
1964
- }
1965
- const r = parsePlatformYaml(raw);
1966
- if (!r.ok) throw new ConfigError(r.error);
1967
- return r.value;
1968
- }
1969
- async function rollback(options, deps = {}) {
1970
- const cwd = deps.cwd ?? process.cwd();
1971
- const env = deps.env ?? process.env;
1972
- const readYaml = deps.readPlatformYaml ?? defaultReadPlatformYaml4;
1973
- const resolveId = deps.resolveIdentity ?? resolveIdentity;
1974
- const mkClient = deps.createProxyClient ?? createProxyClient;
1975
- const success = deps.logSuccess ?? ((s) => log7.success(s));
1976
- const error = deps.logError ?? ((s) => log7.error(s));
1977
- const exit = deps.exit ?? exitWithCode;
1978
- const promptConfirm = deps.promptConfirm ?? defaultPromptConfirm2;
1979
- try {
1980
- if (!options.to || options.to.trim().length === 0) {
1981
- throw new UsageError(
1982
- "rollback requires --to <deployId>. Run `universe static ls` to list past deploys."
1983
- );
1984
- }
1985
- const identity = await resolveId({ env });
1986
- if (!identity) {
1987
- throw new CredentialError(
1988
- "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
1989
- );
1990
- }
1991
- const config = await readAndParseConfig3(cwd, readYaml);
1992
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
1993
- const client = mkClient({
1994
- baseUrl,
1995
- getAuthToken: () => identity.token,
1996
- timeoutMs: parseFetchTimeoutMs(env)
1997
- });
1998
- const to = options.to.trim();
1999
- const prod = await client.getAlias({
2000
- site: config.site,
2001
- mode: "production"
2002
- });
2003
- const initialExpected = prod?.deployId ?? "";
2004
- let result;
2005
- try {
2006
- result = await client.siteRollback({
2007
- site: config.site,
2008
- to,
2009
- expectedCurrent: initialExpected
2010
- });
2011
- } catch (err) {
2012
- if (!(err instanceof AliasDriftError)) throw err;
2013
- if (options.json) throw err;
2014
- error(
2015
- `drift: production moved to ${err.current}, expected ${initialExpected}`
2016
- );
2017
- const ok = await promptConfirm(
2018
- `Retry rollback with expectedCurrent='${err.current}'?`
2019
- );
2020
- if (!ok) throw err;
2021
- result = await client.siteRollback({
2022
- site: config.site,
2023
- to,
2024
- expectedCurrent: err.current
2025
- });
2026
- }
2027
- if (options.json) {
2028
- emitJson6(
2029
- buildEnvelope("rollback", true, {
2030
- deployId: result.deployId,
2031
- url: result.url,
2032
- site: config.site,
2033
- identitySource: identity.source
2034
- })
2035
- );
2036
- } else {
2037
- success(
2038
- [
2039
- `Rolled production back to ${result.deployId}`,
2040
- ``,
2041
- ` Site: ${config.site}`,
2042
- ` Deploy: ${result.deployId}`,
2043
- ` Production: ${result.url}`
2044
- ].join("\n")
2045
- );
2046
- }
2047
- } catch (err) {
2048
- const { code, message } = wrapProxyError("rollback", err);
2049
- const extras = err instanceof AliasDriftError ? { current: err.current } : void 0;
2050
- outputError({ json: options.json, command: "rollback" }, code, message, {
2051
- logError: error,
2052
- extras
2053
- });
2054
- exit(code);
2055
- }
2056
- }
2057
-
2058
- // src/commands/whoami.ts
2059
- import { log as log8 } from "@clack/prompts";
2060
- var DEFAULT_PROXY_URL3 = "https://uploads.freecode.camp";
2061
- function emitJson7(envelope) {
2062
- process.stdout.write(JSON.stringify(envelope) + "\n");
2063
- }
2064
- async function whoami(options, deps = {}) {
2065
- const env = deps.env ?? process.env;
2066
- const resolve6 = deps.resolveIdentity ?? resolveIdentity;
2067
- const mkClient = deps.createProxyClient ?? createProxyClient;
2068
- const success = deps.logSuccess ?? ((s) => log8.success(s));
2069
- const error = deps.logError ?? ((s) => log8.error(s));
2070
- const exit = deps.exit ?? exitWithCode;
2071
- const identity = await resolve6({ env });
2072
- if (!identity) {
2073
- const msg = "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI.";
2074
- outputError(
2075
- { json: options.json, command: "whoami" },
2076
- EXIT_CREDENTIALS,
2077
- msg,
2078
- { logError: error }
2079
- );
2080
- exit(EXIT_CREDENTIALS);
2081
- return;
2082
- }
2083
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL3;
2084
- const client = mkClient({
2085
- baseUrl,
2086
- getAuthToken: () => identity.token,
2087
- timeoutMs: parseFetchTimeoutMs(env)
2088
- });
2089
- try {
2090
- const result = await client.whoami();
2091
- const count = result.authorizedSites.length;
2092
- if (options.json) {
2093
- emitJson7(
2094
- buildEnvelope("whoami", true, {
2095
- login: result.login,
2096
- identitySource: identity.source,
2097
- proxyUrl: baseUrl,
2098
- authorizedSitesCount: count
2099
- })
2100
- );
2101
- } else {
2102
- const sitesLine = count === 0 ? "Authorized for 0 sites." : `Authorized for ${count} site${count === 1 ? "" : "s"} \u2014 run \`universe sites ls --mine\``;
2103
- success(
2104
- [
2105
- `Logged in as: ${result.login}`,
2106
- `Identity source: ${identity.source}`,
2107
- `Proxy: ${baseUrl}`,
2108
- sitesLine
2109
- ].join("\n")
2110
- );
2111
- }
2112
- } catch (err) {
2113
- const exitCode = err instanceof CliError ? err.exitCode : EXIT_CREDENTIALS;
2114
- const message = err instanceof ProxyError ? `whoami failed (${err.code}): ${err.message}` : err instanceof Error ? err.message : String(err);
2115
- outputError({ json: options.json, command: "whoami" }, exitCode, message, {
2116
- logError: error
2117
- });
2118
- exit(exitCode);
2119
- }
2120
- }
2121
-
2122
- // src/commands/sites/ls.ts
2123
- import { log as log9 } from "@clack/prompts";
2124
-
2125
- // src/commands/sites/_shared.ts
2126
- function emitJson8(envelope) {
2127
- process.stdout.write(JSON.stringify(envelope) + "\n");
2128
- }
2129
- function parseTeamsFlag(raw) {
2130
- if (raw === void 0 || raw === null) return [];
2131
- const tokens = Array.isArray(raw) ? raw : [raw];
2132
- return tokens.flatMap((s) => s.split(",")).map((s) => s.trim()).filter((s) => s.length > 0);
2133
- }
2134
- async function setupClient(deps) {
2135
- const env = deps.env ?? process.env;
2136
- const resolveId = deps.resolveIdentity ?? resolveIdentity;
2137
- const mkClient = deps.createProxyClient ?? createProxyClient;
2138
- const identity = await resolveId({ env });
2139
- if (!identity) {
2140
- throw new CredentialError(
2141
- "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
2142
- );
2143
- }
2144
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
2145
- const client = mkClient({
2146
- baseUrl,
2147
- getAuthToken: () => identity.token,
2148
- timeoutMs: parseFetchTimeoutMs(env)
2149
- });
2150
- return { client, identitySource: identity.source };
2151
- }
2152
-
2153
- // src/commands/sites/ls.ts
2154
- function formatTable2(rows) {
2155
- if (rows.length === 0) return "No registered sites.";
2156
- const headers = ["SLUG", "TEAMS", "CREATED BY", "CREATED AT"];
2157
- const cells = rows.map((r) => [
2158
- r.slug,
2159
- r.teams.join(","),
2160
- r.createdBy,
2161
- r.createdAt
2162
- ]);
2163
- const widths = headers.map(
2164
- (h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
2165
- );
2166
- const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
2167
- return [fmt(headers), ...cells.map(fmt)].join("\n");
2168
- }
2169
- async function ls2(options, deps = {}) {
2170
- const command = "sites ls";
2171
- const success = deps.logSuccess ?? ((s) => log9.message(s));
2172
- const error = deps.logError ?? ((s) => log9.error(s));
2173
- const exit = deps.exit ?? exitWithCode;
2174
- try {
2175
- const { client, identitySource } = await setupClient(deps);
2176
- let rows = await client.listSites();
2177
- let scope = "all";
2178
- if (options.mine) {
2179
- const me = await client.whoami();
2180
- const allowed = new Set(me.authorizedSites);
2181
- rows = rows.filter((r) => allowed.has(r.slug));
2182
- scope = "mine";
2183
- }
2184
- if (options.json) {
2185
- emitJson8(
2186
- buildEnvelope(command, true, {
2187
- count: rows.length,
2188
- scope,
2189
- sites: rows,
2190
- identitySource
2191
- })
2192
- );
2193
- } else {
2194
- success(formatTable2(rows));
2195
- }
2196
- } catch (err) {
2197
- const { code, message } = wrapProxyError(command, err);
2198
- outputError({ json: options.json, command }, code, message, {
2199
- logError: error
2200
- });
2201
- exit(code);
2202
- }
2203
- }
2204
-
2205
- // src/commands/sites/register.ts
2206
- import { log as log10 } from "@clack/prompts";
2207
- async function register(options, deps = {}) {
2208
- const command = "sites register";
2209
- const success = deps.logSuccess ?? ((s) => log10.success(s));
2210
- const error = deps.logError ?? ((s) => log10.error(s));
2211
- const exit = deps.exit ?? exitWithCode;
2212
- try {
2213
- if (!options.slug || options.slug.trim().length === 0) {
2214
- throw new UsageError("slug is required (positional argument)");
2215
- }
2216
- const teams = parseTeamsFlag(options.team);
2217
- const { client, identitySource } = await setupClient(deps);
2218
- const row = await client.registerSite({
2219
- slug: options.slug,
2220
- teams: teams.length > 0 ? teams : void 0
2221
- });
2222
- if (options.json) {
2223
- emitJson8(
2224
- buildEnvelope(command, true, {
2225
- slug: row.slug,
2226
- teams: row.teams,
2227
- createdAt: row.createdAt,
2228
- createdBy: row.createdBy,
2229
- identitySource
2230
- })
2231
- );
2232
- } else {
2233
- success(
2234
- [
2235
- `Registered ${row.slug}`,
2236
- ``,
2237
- ` Slug: ${row.slug}`,
2238
- ` Teams: ${row.teams.join(", ")}`,
2239
- ` Created by: ${row.createdBy}`,
2240
- ` Created at: ${row.createdAt}`
2241
- ].join("\n")
2242
- );
2243
- }
2244
- } catch (err) {
2245
- const { code, message } = wrapProxyError(command, err);
2246
- outputError({ json: options.json, command }, code, message, {
2247
- logError: error
2248
- });
2249
- exit(code);
2250
- }
2251
- }
2252
-
2253
- // src/commands/sites/rm.ts
2254
- import { log as log11 } from "@clack/prompts";
2255
- async function rm2(options, deps = {}) {
2256
- const command = "sites rm";
2257
- const success = deps.logSuccess ?? ((s) => log11.success(s));
2258
- const error = deps.logError ?? ((s) => log11.error(s));
2259
- const exit = deps.exit ?? exitWithCode;
2260
- try {
2261
- if (!options.slug || options.slug.trim().length === 0) {
2262
- throw new UsageError("slug is required (positional argument)");
2263
- }
2264
- const { client, identitySource } = await setupClient(deps);
2265
- await client.deleteSite({ slug: options.slug });
2266
- if (options.json) {
2267
- emitJson8(
2268
- buildEnvelope(command, true, {
2269
- slug: options.slug,
2270
- deleted: true,
2271
- identitySource
2272
- })
2273
- );
2274
- } else {
2275
- success(
2276
- [
2277
- `Deleted ${options.slug}`,
2278
- ``,
2279
- ` Note: R2 deploy bytes are NOT removed; they age out via the`,
2280
- ` post-GA cleanup cron.`
2281
- ].join("\n")
2282
- );
2283
- }
2284
- } catch (err) {
2285
- const { code, message } = wrapProxyError(command, err);
2286
- outputError({ json: options.json, command }, code, message, {
2287
- logError: error
2288
- });
2289
- exit(code);
2290
- }
2291
- }
2292
-
2293
- // src/commands/sites/update.ts
2294
- import { log as log12 } from "@clack/prompts";
2295
- async function update(options, deps = {}) {
2296
- const command = "sites update";
2297
- const success = deps.logSuccess ?? ((s) => log12.success(s));
2298
- const error = deps.logError ?? ((s) => log12.error(s));
2299
- const exit = deps.exit ?? exitWithCode;
2300
- try {
2301
- if (!options.slug || options.slug.trim().length === 0) {
2302
- throw new UsageError("slug is required (positional argument)");
2303
- }
2304
- const teams = parseTeamsFlag(options.team);
2305
- if (teams.length === 0) {
2306
- throw new UsageError(
2307
- "--team is required with at least one slug; use `sites rm` to remove a site"
2308
- );
2309
- }
2310
- const { client, identitySource } = await setupClient(deps);
2311
- const row = await client.updateSite({
2312
- slug: options.slug,
2313
- teams
2314
- });
2315
- if (options.json) {
2316
- emitJson8(
2317
- buildEnvelope(command, true, {
2318
- slug: row.slug,
2319
- teams: row.teams,
2320
- updatedAt: row.updatedAt,
2321
- identitySource
2322
- })
2323
- );
2324
- } else {
2325
- success(
2326
- [
2327
- `Updated ${row.slug}`,
2328
- ``,
2329
- ` Slug: ${row.slug}`,
2330
- ` Teams: ${row.teams.join(", ")}`,
2331
- ` Updated at: ${row.updatedAt}`
2332
- ].join("\n")
2333
- );
2334
- }
2335
- } catch (err) {
2336
- const { code, message } = wrapProxyError(command, err);
2337
- outputError({ json: options.json, command }, code, message, {
2338
- logError: error
2339
- });
2340
- exit(code);
2341
- }
2342
- }
2343
-
2344
- // src/commands/repo/approve.ts
2345
- import { log as log13 } from "@clack/prompts";
2346
-
2347
- // src/commands/repo/_shared.ts
2348
- import {
2349
- confirm as clackConfirm,
2350
- isCancel as clackIsCancel,
2351
- select as clackSelect,
2352
- text as clackText
2353
- } from "@clack/prompts";
2354
- var defaultRepoPrompts = {
2355
- text: clackText,
2356
- select: clackSelect,
2357
- confirm: clackConfirm,
2358
- isCancel: clackIsCancel
2359
- };
2360
- function emitJson9(envelope) {
2361
- process.stdout.write(JSON.stringify(envelope) + "\n");
2362
- }
2363
- async function setupClient2(deps) {
2364
- const env = deps.env ?? process.env;
2365
- const resolveId = deps.resolveIdentity ?? resolveIdentity;
2366
- const mkClient = deps.createProxyClient ?? createProxyClient;
2367
- const identity = await resolveId({ env });
2368
- if (!identity) {
2369
- throw new CredentialError(
2370
- "No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
2371
- );
2372
- }
2373
- const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
2374
- const client = mkClient({
2375
- baseUrl,
2376
- getAuthToken: () => identity.token,
2377
- timeoutMs: parseFetchTimeoutMs(env),
2378
- debug: Boolean(env["UNIVERSE_DEBUG"])
2379
- });
2380
- return { client, identitySource: identity.source };
2381
- }
2382
- function formatRepoTable(rows, emptyMsg = "No repo requests.") {
2383
- if (rows.length === 0) return emptyMsg;
2384
- const headers = [
2385
- "ID",
2386
- "REPO",
2387
- "VIS",
2388
- "STATUS",
2389
- "REQUESTED BY",
2390
- "REQUESTED AT"
2391
- ];
2392
- const cells = rows.map((r) => [
2393
- r.id,
2394
- r.name,
2395
- r.visibility,
2396
- r.status,
2397
- r.requestedBy,
2398
- r.createdAt
2399
- ]);
2400
- const widths = headers.map(
2401
- (h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
2402
- );
2403
- const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
2404
- return [fmt(headers), ...cells.map(fmt)].join("\n");
2405
- }
2406
-
2407
- // src/commands/repo/approve.ts
2408
- async function approve(options, deps = {}) {
2409
- const command = "repo approve";
2410
- const success = deps.logSuccess ?? ((s) => log13.success(s));
2411
- const error = deps.logError ?? ((s) => log13.error(s));
2412
- const exit = deps.exit ?? exitWithCode;
2413
- const prompts = deps.prompts ?? defaultRepoPrompts;
2414
- const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
2415
- let creationFailure;
2416
- let identitySource;
2417
- try {
2418
- if (!options.id || options.id.trim().length === 0) {
2419
- throw new UsageError("request id is required (positional argument)");
2420
- }
2421
- const setup = await setupClient2(deps);
2422
- const client = setup.client;
2423
- identitySource = setup.identitySource;
2424
- if (!options.json && !options.yes) {
2425
- if (!isTTY) {
2426
- throw new UsageError(
2427
- "non-interactive session: pass --yes to approve without confirmation"
2428
- );
2429
- }
2430
- const cur = await client.getRepoRequest(options.id);
2431
- const ok = await prompts.confirm({
2432
- message: `Approve ${cur.visibility} repo "${cur.name}" requested by ${cur.requestedBy}? This creates the repository.`
2433
- });
2434
- if (prompts.isCancel(ok) || ok === false) {
2435
- throw new ConfirmError("repo approve cancelled");
2436
- }
2437
- }
2438
- const res = await client.approveRepoRequest({ id: options.id });
2439
- const row = res.request;
2440
- if (res.outcome === "approved_failed") {
2441
- if (!options.json) {
2442
- throw new StorageError(
2443
- `approved, but repository creation failed: ${row.error ?? "unknown"} (${row.owner}/${row.name}, requested by ${row.requestedBy})`
2444
- );
2445
- }
2446
- creationFailure = {
2447
- outcome: res.outcome,
2448
- id: row.id,
2449
- repo: `${row.owner}/${row.name}`,
2450
- status: row.status,
2451
- creationError: row.error ?? "unknown",
2452
- requestedBy: row.requestedBy,
2453
- identitySource
2454
- };
2455
- } else if (options.json) {
2456
- emitJson9(
2457
- buildEnvelope(command, true, {
2458
- id: row.id,
2459
- outcome: res.outcome,
2460
- repo: `${row.owner}/${row.name}`,
2461
- url: row.url,
2462
- visibility: row.visibility,
2463
- approver: row.approver,
2464
- identitySource
2465
- })
2466
- );
2467
- } else {
2468
- success(
2469
- [
2470
- `Approved ${row.name}`,
2471
- ``,
2472
- ` Repository: ${row.url ?? `${row.owner}/${row.name}`}`,
2473
- ` Visibility: ${row.visibility}`,
2474
- ` Approved by: ${row.approver ?? "you"}`
2475
- ].join("\n")
2476
- );
2477
- }
2478
- } catch (err) {
2479
- const { code, message, kind, requestId } = wrapProxyError(command, err);
2480
- outputError({ json: options.json, command }, code, message, {
2481
- logError: error,
2482
- kind,
2483
- requestId,
2484
- extras: identitySource ? { identitySource } : void 0
2485
- });
2486
- exit(code);
2487
- return;
2488
- }
2489
- if (creationFailure) {
2490
- outputError(
2491
- { json: true, command },
2492
- EXIT_STORAGE,
2493
- "approved, but repository creation failed",
2494
- { logError: error, extras: creationFailure }
2495
- );
2496
- exit(EXIT_STORAGE);
2497
- }
2498
- }
2499
-
2500
- // src/commands/repo/create.ts
2501
- import { log as log14 } from "@clack/prompts";
2502
- function blankToUndefined(s) {
2503
- if (s === void 0 || s === null) return void 0;
2504
- const t = String(s).trim();
2505
- return t === "" ? void 0 : t;
2506
- }
2507
- async function promptTemplate(client, prompts) {
2508
- let templates = [];
2509
- try {
2510
- templates = await client.listRepoTemplates();
2511
- } catch {
2512
- templates = [];
2513
- }
2514
- if (templates.length === 0) {
2515
- const v2 = await prompts.text({
2516
- message: "Template (optional)",
2517
- placeholder: "name of an org template repo; blank for an empty repo"
2518
- });
2519
- if (prompts.isCancel(v2)) throw new ConfirmError("repo create cancelled");
2520
- return blankToUndefined(String(v2));
2521
- }
2522
- const v = await prompts.select({
2523
- message: "Template",
2524
- options: [
2525
- { value: "", label: "None (blank repo)" },
2526
- ...templates.map((t) => ({ value: t, label: t }))
2527
- ],
2528
- initialValue: ""
2529
- });
2530
- if (prompts.isCancel(v)) throw new ConfirmError("repo create cancelled");
2531
- return blankToUndefined(String(v));
2532
- }
2533
- async function create(options, deps = {}) {
2534
- const command = "repo create";
2535
- const success = deps.logSuccess ?? ((s) => log14.success(s));
2536
- const error = deps.logError ?? ((s) => log14.error(s));
2537
- const exit = deps.exit ?? exitWithCode;
2538
- const prompts = deps.prompts ?? defaultRepoPrompts;
2539
- const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
2540
- const canPrompt = !options.json && !options.yes && isTTY;
2541
- let identitySource = "";
2542
- try {
2543
- let client;
2544
- const ensureClient = async () => {
2545
- if (!client) {
2546
- const setup = await setupClient2(deps);
2547
- client = setup.client;
2548
- identitySource = setup.identitySource;
2549
- }
2550
- return client;
2551
- };
2552
- let name = blankToUndefined(options.name) ?? "";
2553
- let visibility = options.visibility;
2554
- let description = options.description;
2555
- let template = options.template;
2556
- if (canPrompt) {
2557
- if (!name) {
2558
- const v = await prompts.text({
2559
- message: "Repository name",
2560
- placeholder: "learn-python-rpg"
2561
- });
2562
- if (prompts.isCancel(v))
2563
- throw new ConfirmError("repo create cancelled");
2564
- name = String(v).trim();
2565
- }
2566
- if (visibility === void 0) {
2567
- const v = await prompts.select({
2568
- message: "Visibility",
2569
- options: [
2570
- { value: "private", label: "Private" },
2571
- { value: "public", label: "Public" }
2572
- ],
2573
- initialValue: "private"
2574
- });
2575
- if (prompts.isCancel(v))
2576
- throw new ConfirmError("repo create cancelled");
2577
- visibility = String(v);
2578
- }
2579
- if (description === void 0) {
2580
- const v = await prompts.text({
2581
- message: "Description (optional)",
2582
- placeholder: "What is this project about?"
2583
- });
2584
- if (prompts.isCancel(v))
2585
- throw new ConfirmError("repo create cancelled");
2586
- description = String(v);
2587
- }
2588
- if (template === void 0) {
2589
- template = await promptTemplate(await ensureClient(), prompts);
2590
- }
2591
- }
2592
- if (!name) {
2593
- throw new UsageError("repo name is required");
2594
- }
2595
- const candidate = { name };
2596
- if (visibility !== void 0 && visibility !== "") {
2597
- candidate.visibility = visibility;
2598
- }
2599
- const desc = blankToUndefined(description);
2600
- if (desc !== void 0) candidate.description = desc;
2601
- const tmpl = blankToUndefined(template);
2602
- if (tmpl !== void 0) candidate.template = tmpl;
2603
- const parsed = createRepoRequestSchema.safeParse(candidate);
2604
- if (!parsed.success) {
2605
- const issues = parsed.error.issues.map(
2606
- (i) => `${i.path.join(".") || "input"}: ${i.message}`
2607
- );
2608
- throw new UsageError(issues.join("; "));
2609
- }
2610
- const body = parsed.data;
2611
- if (!options.json && !options.yes) {
2612
- if (!isTTY) {
2613
- throw new UsageError(
2614
- "non-interactive session: pass --yes to submit without confirmation (or --json)"
2615
- );
2616
- }
2617
- const ok = await prompts.confirm({
2618
- message: `Submit request to create ${body.visibility} repo "${body.name}"${body.template ? ` from template ${body.template}` : ""}?`
2619
- });
2620
- if (prompts.isCancel(ok) || ok === false) {
2621
- throw new ConfirmError("repo create cancelled");
2622
- }
2623
- }
2624
- const activeClient = await ensureClient();
2625
- const row = await activeClient.createRepoRequest({
2626
- name: body.name,
2627
- visibility: body.visibility,
2628
- description: body.description,
2629
- template: body.template
2630
- });
2631
- if (options.json) {
2632
- emitJson9(
2633
- buildEnvelope(command, true, {
2634
- id: row.id,
2635
- name: row.name,
2636
- owner: row.owner,
2637
- visibility: row.visibility,
2638
- template: row.template,
2639
- status: row.status,
2640
- identitySource
2641
- })
2642
- );
2643
- } else {
2644
- success(
2645
- [
2646
- `Request submitted`,
2647
- ``,
2648
- ` Request id: ${row.id}`,
2649
- ` Repository: ${row.owner}/${row.name}`,
2650
- ` Visibility: ${row.visibility}`,
2651
- ...row.template ? [` Template: ${row.template}`] : [],
2652
- ` Status: ${row.status} \u2014 run \`universe repo ls\` to review`
2653
- ].join("\n")
2654
- );
2655
- }
2656
- } catch (err) {
2657
- const { code, message, kind, requestId } = wrapProxyError(command, err);
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, {
2661
- logError: error,
2662
- kind,
2663
- requestId,
2664
- extras: identitySource ? { identitySource } : void 0
2665
- });
2666
- exit(code);
2667
- }
2668
- }
2669
-
2670
- // src/commands/repo/ls.ts
2671
- import { log as log15 } from "@clack/prompts";
2672
- var LS_STATUSES = [...repoStatusSchema.options, "all"];
2673
- async function ls3(options, deps = {}) {
2674
- const command = "repo ls";
2675
- const message = deps.logMessage ?? ((s) => log15.message(s));
2676
- const error = deps.logError ?? ((s) => log15.error(s));
2677
- const exit = deps.exit ?? exitWithCode;
2678
- let identitySource;
2679
- try {
2680
- const requestedStatus = options.all ? "all" : options.status;
2681
- if (requestedStatus !== void 0 && !LS_STATUSES.includes(requestedStatus)) {
2682
- throw new UsageError(
2683
- `invalid --status "${requestedStatus}": must be one of ${LS_STATUSES.join(", ")}`
2684
- );
2685
- }
2686
- const setup = await setupClient2(deps);
2687
- const client = setup.client;
2688
- identitySource = setup.identitySource;
2689
- const rows = await client.listRepoRequests({
2690
- status: requestedStatus,
2691
- mine: options.mine ?? false
2692
- });
2693
- const status2 = requestedStatus ?? "pending";
2694
- if (options.json) {
2695
- emitJson9(
2696
- buildEnvelope(command, true, {
2697
- count: rows.length,
2698
- status: status2,
2699
- mine: options.mine ?? false,
2700
- requests: rows,
2701
- identitySource
2702
- })
2703
- );
2704
- } else {
2705
- const empty = status2 === "all" ? "No repo requests." : `No ${status2} repo requests.`;
2706
- message(formatRepoTable(rows, empty));
2707
- }
2708
- } catch (err) {
2709
- const {
2710
- code,
2711
- message: msg,
2712
- kind,
2713
- requestId
2714
- } = wrapProxyError(command, err);
2715
- outputError({ json: options.json, command }, code, msg, {
2716
- logError: error,
2717
- kind,
2718
- requestId,
2719
- extras: identitySource ? { identitySource } : void 0
2720
- });
2721
- exit(code);
2722
- }
2723
- }
2724
-
2725
- // src/commands/repo/reject.ts
2726
- import { log as log16 } from "@clack/prompts";
2727
- async function reject(options, deps = {}) {
2728
- const command = "repo reject";
2729
- const success = deps.logSuccess ?? ((s) => log16.success(s));
2730
- const error = deps.logError ?? ((s) => log16.error(s));
2731
- const exit = deps.exit ?? exitWithCode;
2732
- const prompts = deps.prompts ?? defaultRepoPrompts;
2733
- const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
2734
- let identitySource;
2735
- try {
2736
- if (!options.id || options.id.trim().length === 0) {
2737
- throw new UsageError("request id is required (positional argument)");
2738
- }
2739
- const setup = await setupClient2(deps);
2740
- const client = setup.client;
2741
- identitySource = setup.identitySource;
2742
- if (!options.json && !options.yes) {
2743
- if (!isTTY) {
2744
- throw new UsageError(
2745
- "non-interactive session: pass --yes to reject without confirmation"
2746
- );
2747
- }
2748
- const cur = await client.getRepoRequest(options.id);
2749
- const ok = await prompts.confirm({
2750
- message: `Reject the request for "${cur.name}" by ${cur.requestedBy}?`
2751
- });
2752
- if (prompts.isCancel(ok) || ok === false) {
2753
- throw new ConfirmError("repo reject cancelled");
2754
- }
2755
- }
2756
- const reason = options.reason === void 0 ? void 0 : String(options.reason).trim() || void 0;
2757
- const row = await client.rejectRepoRequest({
2758
- id: options.id,
2759
- reason
2760
- });
2761
- if (options.json) {
2762
- emitJson9(
2763
- buildEnvelope(command, true, {
2764
- id: row.id,
2765
- status: row.status,
2766
- repo: `${row.owner}/${row.name}`,
2767
- rejectReason: row.rejectReason,
2768
- identitySource
2769
- })
2770
- );
2771
- } else {
2772
- success(
2773
- [
2774
- `Rejected ${row.name}`,
2775
- ``,
2776
- ` Repository: ${row.owner}/${row.name}`,
2777
- ...row.rejectReason ? [` Reason: ${row.rejectReason}`] : []
2778
- ].join("\n")
2779
- );
2780
- }
2781
- } catch (err) {
2782
- const { code, message, kind, requestId } = wrapProxyError(command, err);
2783
- outputError({ json: options.json, command }, code, message, {
2784
- logError: error,
2785
- kind,
2786
- requestId,
2787
- extras: identitySource ? { identitySource } : void 0
2788
- });
2789
- exit(code);
2790
- }
2791
- }
2792
-
2793
- // src/commands/repo/rm.ts
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";
2852
- function humanRow(row) {
2853
- const lines = [
2854
- `Request ${row.id}`,
2855
- ``,
2856
- ` Repository: ${row.owner}/${row.name}`,
2857
- ` Visibility: ${row.visibility}`,
2858
- ` Status: ${row.status}`,
2859
- ` Requested by: ${row.requestedBy}`
2860
- ];
2861
- if (row.template) lines.push(` Template: ${row.template}`);
2862
- if (row.url) lines.push(` URL: ${row.url}`);
2863
- if (row.approver) lines.push(` Approver: ${row.approver}`);
2864
- if (row.rejectReason) lines.push(` Reason: ${row.rejectReason}`);
2865
- if (row.error) lines.push(` Error: ${row.error}`);
2866
- lines.push(` Created: ${row.createdAt}`);
2867
- lines.push(` Updated: ${row.updatedAt}`);
2868
- return lines.join("\n");
2869
- }
2870
- async function status(options, deps = {}) {
2871
- const command = "repo status";
2872
- const message = deps.logMessage ?? ((s) => log18.message(s));
2873
- const error = deps.logError ?? ((s) => log18.error(s));
2874
- const exit = deps.exit ?? exitWithCode;
2875
- let identitySource;
2876
- try {
2877
- if (!options.id || options.id.trim().length === 0) {
2878
- throw new UsageError("request id is required (positional argument)");
2879
- }
2880
- const setup = await setupClient2(deps);
2881
- const client = setup.client;
2882
- identitySource = setup.identitySource;
2883
- const row = await client.getRepoRequest(options.id);
2884
- if (options.json) {
2885
- emitJson9(buildEnvelope(command, true, { request: row, identitySource }));
2886
- } else {
2887
- message(humanRow(row));
2888
- }
2889
- } catch (err) {
2890
- const {
2891
- code,
2892
- message: msg,
2893
- kind,
2894
- requestId
2895
- } = wrapProxyError(command, err);
2896
- outputError({ json: options.json, command }, code, msg, {
2897
- logError: error,
2898
- kind,
2899
- requestId,
2900
- extras: identitySource ? { identitySource } : void 0
2901
- });
2902
- exit(code);
2903
- }
2904
- }
2905
-
2906
- // src/lib/update-notifier.ts
2907
- import { spawn as spawn2 } from "child_process";
2908
- import { readFileSync, writeSync } from "fs";
2909
- import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
2910
- import { homedir as homedir2 } from "os";
2911
- import { dirname as dirname2, join as join3 } from "path";
2912
- var APP_DIR2 = "universe-cli";
2913
- var CACHE_FILE = "update-check.json";
2914
- var PKG_NAME = "@freecodecamp/universe-cli";
2915
- var NPM_LATEST_URL = `https://registry.npmjs.org/${PKG_NAME}/latest`;
2916
- var DEFAULT_TTL_MS = 60 * 60 * 1e3;
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
- }
2931
- function configBase2() {
2932
- const xdg = process.env["XDG_CONFIG_HOME"];
2933
- if (xdg && xdg.length > 0) return xdg;
2934
- return join3(homedir2(), ".config");
2935
- }
2936
- function cachePath() {
2937
- return join3(configBase2(), APP_DIR2, CACHE_FILE);
2938
- }
2939
- function isDisabled() {
2940
- const v = process.env["UNIVERSE_NO_UPDATE_CHECK"];
2941
- return v === "1" || v === "true";
2942
- }
2943
- function parseCache(raw) {
2944
- let parsed;
2945
- try {
2946
- parsed = JSON.parse(raw);
2947
- } catch {
2948
- return null;
2949
- }
2950
- if (typeof parsed !== "object" || parsed === null || !("latest" in parsed) || !("lastCheck" in parsed)) {
2951
- return null;
2952
- }
2953
- const { latest, lastCheck } = parsed;
2954
- if (typeof latest !== "string" || typeof lastCheck !== "number") {
2955
- return null;
2956
- }
2957
- return { latest, lastCheck };
2958
- }
2959
- async function readCache() {
2960
- try {
2961
- const raw = await readFile6(cachePath(), "utf-8");
2962
- return parseCache(raw);
2963
- } catch {
2964
- return null;
2965
- }
2966
- }
2967
- function readCacheSync() {
2968
- try {
2969
- const raw = readFileSync(cachePath(), "utf-8");
2970
- return parseCache(raw);
2971
- } catch {
2972
- return null;
2973
- }
2974
- }
2975
- async function writeCache(c) {
2976
- const path = cachePath();
2977
- await mkdir2(dirname2(path), { recursive: true, mode: 448 });
2978
- await writeFile2(path, JSON.stringify(c), { mode: 420 });
2979
- }
2980
- async function fetchLatest() {
2981
- const ctl = new AbortController();
2982
- const timer = setTimeout(() => ctl.abort(), FETCH_TIMEOUT_MS);
2983
- try {
2984
- const res = await fetch(latestUrl(), {
2985
- signal: ctl.signal,
2986
- headers: { accept: "application/json" }
2987
- });
2988
- if (!res.ok) return null;
2989
- const body = await res.json();
2990
- return typeof body.version === "string" ? body.version : null;
2991
- } catch {
2992
- return null;
2993
- } finally {
2994
- clearTimeout(timer);
2995
- }
2996
- }
2997
- function compareVersions(a, b) {
2998
- const pa = parseVersion(a);
2999
- const pb = parseVersion(b);
3000
- if (pa === null || pb === null) return 0;
3001
- for (let i = 0; i < 3; i += 1) {
3002
- const ai = pa[i] ?? 0;
3003
- const bi = pb[i] ?? 0;
3004
- if (ai < bi) return -1;
3005
- if (ai > bi) return 1;
3006
- }
3007
- return 0;
3008
- }
3009
- function parseVersion(s) {
3010
- const core = s.split("-")[0] ?? "";
3011
- const parts = core.split(".");
3012
- if (parts.length !== 3) return null;
3013
- const nums = parts.map((p) => Number.parseInt(p, 10));
3014
- if (nums.some((n) => Number.isNaN(n))) return null;
3015
- return [nums[0], nums[1], nums[2]];
3016
- }
3017
- async function refreshIfStale(now = Date.now(), options = {}) {
3018
- if (isDisabled()) return;
3019
- if (!options.force) {
3020
- const cache = await readCache();
3021
- if (cache !== null && now - cache.lastCheck < ttlMs()) return;
3022
- }
3023
- const latest = await fetchLatest();
3024
- if (latest === null) return;
3025
- try {
3026
- await writeCache({ latest, lastCheck: now });
3027
- } catch {
3028
- }
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
- }
3049
- function getNoticeSync(current) {
3050
- if (isDisabled()) return null;
3051
- const cache = readCacheSync();
3052
- if (cache === null) return null;
3053
- if (compareVersions(current, cache.latest) >= 0) return null;
3054
- return { current, latest: cache.latest };
3055
- }
3056
- function useColor() {
3057
- if (process.env["NO_COLOR"] && process.env["NO_COLOR"].length > 0) {
3058
- return false;
3059
- }
3060
- return process.stderr.isTTY === true;
3061
- }
3062
- function paint(s, code, color) {
3063
- if (!color) return s;
3064
- return `\x1B[${code}m${s}\x1B[0m`;
3065
- }
3066
- function formatNotice(n, color = useColor()) {
3067
- const dim = (s) => paint(s, "2", color);
3068
- const yellow = (s) => paint(s, "33", color);
3069
- const cyan = (s) => paint(s, "36", color);
3070
- const bar = dim("\u2502");
3071
- const lines = [
3072
- "",
3073
- bar,
3074
- `${yellow("\u25B2")} Update available: ${dim(n.current)} \u2192 ${cyan(n.latest)}`,
3075
- `${bar} Run ${cyan(`npm i -g ${PKG_NAME}`)} to upgrade`,
3076
- dim("\u2514"),
3077
- ""
3078
- ];
3079
- return lines.join("\n");
3080
- }
3081
- function installExitNotice(current) {
3082
- if (isDisabled()) return;
3083
- let printed = false;
3084
- const emit = () => {
3085
- if (printed) return;
3086
- printed = true;
3087
- const n = getNoticeSync(current);
3088
- if (n === null) return;
3089
- try {
3090
- writeSync(2, formatNotice(n));
3091
- } catch {
3092
- }
3093
- };
3094
- process.on("beforeExit", emit);
3095
- process.on("exit", emit);
3096
- }
3097
-
3098
- // src/cli.ts
3099
- var version = true ? "0.9.0" : "0.0.0";
3100
- function handleActionError(command, json, err) {
3101
- const ctx = { json, command };
3102
- const message = err instanceof Error ? err.message : "unknown error";
3103
- const code = err instanceof CliError ? err.exitCode : EXIT_USAGE;
3104
- outputError(ctx, code, message);
3105
- exitWithCode(code);
3106
- }
3107
- function findFirstPositional(args) {
3108
- for (let i = 0; i < args.length; i += 1) {
3109
- const a = args[i];
3110
- if (typeof a === "string" && !a.startsWith("-")) return i;
3111
- }
3112
- return -1;
3113
- }
3114
- function isVersionRequest(args) {
3115
- return args.includes("--version") || args.includes("-v");
3116
- }
3117
- async function run(argv = process.argv) {
3118
- installExitNotice(version);
3119
- const args = argv.slice(2);
3120
- const versionRequested = isVersionRequest(args);
3121
- if (!versionRequested) {
3122
- spawnRefresh();
3123
- }
3124
- const firstPosIdx = findFirstPositional(args);
3125
- const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
3126
- const isStatic = namespace === "static";
3127
- const isSites = namespace === "sites";
3128
- const isRepo = namespace === "repo";
3129
- if (isSites) {
3130
- const sitesArgs = [
3131
- ...args.slice(0, firstPosIdx),
3132
- ...args.slice(firstPosIdx + 1)
3133
- ];
3134
- const sitesCli = cac("universe sites");
3135
- sitesCli.command("register <slug>", "Register a new static site (staff only)").option("--json", "Output as JSON").option(
3136
- "--team <name>",
3137
- "GitHub team slug (repeatable, or comma-separated). Defaults to staff."
3138
- ).action(
3139
- async (slug, flags) => {
3140
- try {
3141
- await register({
3142
- json: flags.json ?? false,
3143
- slug,
3144
- team: flags.team
3145
- });
3146
- } catch (err) {
3147
- handleActionError("sites register", flags.json ?? false, err);
3148
- }
3149
- }
3150
- );
3151
- sitesCli.command("ls", "List sites in the registry").option("--json", "Output as JSON").option(
3152
- "--mine",
3153
- "Filter to sites your GitHub identity is authorized for"
3154
- ).action(async (flags) => {
3155
- try {
3156
- await ls2({
3157
- json: flags.json ?? false,
3158
- mine: flags.mine ?? false
3159
- });
3160
- } catch (err) {
3161
- handleActionError("sites ls", flags.json ?? false, err);
3162
- }
3163
- });
3164
- sitesCli.command(
3165
- "update <slug>",
3166
- "Replace the teams list for an existing site (staff only)"
3167
- ).option("--json", "Output as JSON").option(
3168
- "--team <name>",
3169
- "GitHub team slug (repeatable, or comma-separated). Required."
3170
- ).action(
3171
- async (slug, flags) => {
3172
- try {
3173
- await update({
3174
- json: flags.json ?? false,
3175
- slug,
3176
- team: flags.team
3177
- });
3178
- } catch (err) {
3179
- handleActionError("sites update", flags.json ?? false, err);
3180
- }
3181
- }
3182
- );
3183
- sitesCli.command("rm <slug>", "Remove a site from the registry (staff only)").option("--json", "Output as JSON").action(async (slug, flags) => {
3184
- try {
3185
- await rm2({ json: flags.json ?? false, slug });
3186
- } catch (err) {
3187
- handleActionError("sites rm", flags.json ?? false, err);
3188
- }
3189
- });
3190
- sitesCli.help();
3191
- sitesCli.version(version);
3192
- sitesCli.parse(["node", "universe-sites", ...sitesArgs]);
3193
- return;
3194
- }
3195
- if (isRepo) {
3196
- const repoArgs = [
3197
- ...args.slice(0, firstPosIdx),
3198
- ...args.slice(firstPosIdx + 1)
3199
- ];
3200
- const repoCli = cac("universe repo");
3201
- repoCli.command(
3202
- "create [name]",
3203
- "Request a new repository under freeCodeCamp-Universe (staff only)"
3204
- ).option("--json", "Output as JSON").option("--visibility <vis>", "public or private (default: private)").option("--description <text>", "Repository description").option(
3205
- "--template <name>",
3206
- "Org template repo to generate from; omit for a blank repo"
3207
- ).option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").example("universe repo create my-app --visibility private --yes").action(
3208
- async (name, flags) => {
3209
- try {
3210
- await create({
3211
- json: flags.json ?? false,
3212
- name,
3213
- visibility: flags.visibility,
3214
- description: flags.description,
3215
- template: flags.template,
3216
- yes: flags.yes ?? false
3217
- });
3218
- } catch (err) {
3219
- handleActionError("repo create", flags.json ?? false, err);
3220
- }
3221
- }
3222
- );
3223
- repoCli.command("ls", "List repo requests (default: pending)").option("--json", "Output as JSON").option(
3224
- "--status <status>",
3225
- "pending | approved | active | rejected | failed | all"
3226
- ).option("--mine", "Only requests you submitted").option("--all", "Show every state (shorthand for --status all)").action(
3227
- async (flags) => {
3228
- try {
3229
- await ls3({
3230
- json: flags.json ?? false,
3231
- status: flags.status,
3232
- mine: flags.mine ?? false,
3233
- all: flags.all ?? false
3234
- });
3235
- } catch (err) {
3236
- handleActionError("repo ls", flags.json ?? false, err);
3237
- }
3238
- }
3239
- );
3240
- repoCli.command(
3241
- "approve <id>",
3242
- "Approve a pending request \u2014 creates the repo (admin only)"
3243
- ).option("--json", "Output as JSON").option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").example("universe repo approve req_abc123 --yes --json").action(async (id, flags) => {
3244
- try {
3245
- await approve({
3246
- json: flags.json ?? false,
3247
- id,
3248
- yes: flags.yes ?? false
3249
- });
3250
- } catch (err) {
3251
- handleActionError("repo approve", flags.json ?? false, err);
3252
- }
3253
- });
3254
- repoCli.command("reject <id>", "Reject a pending request (admin only)").option("--json", "Output as JSON").option("--reason <text>", "Reason shown to the requester").option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").example('universe repo reject req_abc123 --reason "out of scope" --yes').action(
3255
- async (id, flags) => {
3256
- try {
3257
- await reject({
3258
- json: flags.json ?? false,
3259
- id,
3260
- reason: flags.reason,
3261
- yes: flags.yes ?? false
3262
- });
3263
- } catch (err) {
3264
- handleActionError("repo reject", flags.json ?? false, err);
3265
- }
3266
- }
3267
- );
3268
- repoCli.command("status <id>", "Show a request's current state").option("--json", "Output as JSON").action(async (id, flags) => {
3269
- try {
3270
- await status({ json: flags.json ?? false, id });
3271
- } catch (err) {
3272
- handleActionError("repo status", flags.json ?? false, err);
3273
- }
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
- });
3289
- repoCli.help();
3290
- repoCli.version(version);
3291
- const knownRepoSubs = /* @__PURE__ */ new Set([
3292
- "create",
3293
- "ls",
3294
- "approve",
3295
- "reject",
3296
- "status",
3297
- "rm"
3298
- ]);
3299
- const repoValueFlags = /* @__PURE__ */ new Set([
3300
- "--visibility",
3301
- "--description",
3302
- "--template",
3303
- "--status",
3304
- "--reason"
3305
- ]);
3306
- let repoSub;
3307
- for (let i = 0; i < repoArgs.length; i += 1) {
3308
- const a = repoArgs[i];
3309
- if (a === void 0) continue;
3310
- if (repoValueFlags.has(a)) {
3311
- i += 1;
3312
- continue;
3313
- }
3314
- if (!a.startsWith("-")) {
3315
- repoSub = a;
3316
- break;
3317
- }
3318
- }
3319
- const repoJson = repoArgs.includes("--json");
3320
- const repoWantsHelp = repoArgs.includes("--help") || repoArgs.includes("-h") || repoArgs.includes("--version");
3321
- if (repoSub === void 0 ? !repoWantsHelp : !knownRepoSubs.has(repoSub)) {
3322
- if (repoSub === void 0 && !repoJson) {
3323
- repoCli.outputHelp();
3324
- } else {
3325
- outputError(
3326
- { json: repoJson, command: "repo" },
3327
- EXIT_USAGE,
3328
- repoSub === void 0 ? "missing repo subcommand \u2014 run `universe repo --help`" : `unknown repo subcommand "${repoSub}" \u2014 run \`universe repo --help\``
3329
- );
3330
- }
3331
- exitWithCode(EXIT_USAGE);
3332
- return;
3333
- }
3334
- try {
3335
- repoCli.parse(["node", "universe-repo", ...repoArgs]);
3336
- } catch (err) {
3337
- handleActionError("repo", repoJson, err);
3338
- }
3339
- return;
3340
- }
3341
- if (isStatic) {
3342
- const staticArgs = [
3343
- ...args.slice(0, firstPosIdx),
3344
- ...args.slice(firstPosIdx + 1)
3345
- ];
3346
- const staticCli = cac("universe static");
3347
- staticCli.command("deploy", "Deploy static site via the artemis proxy").option("--json", "Output as JSON").option("--promote", "Finalize as production (default: preview)").option("--dir <path>", "Override build.output dir from platform.yaml").action(
3348
- async (flags) => {
3349
- try {
3350
- await deploy({
3351
- json: flags.json ?? false,
3352
- promote: flags.promote ?? false,
3353
- dir: flags.dir
3354
- });
3355
- } catch (err) {
3356
- handleActionError("deploy", flags.json ?? false, err);
3357
- }
3358
- }
3359
- );
3360
- staticCli.command("promote", "Promote the current preview to production").option("--json", "Output as JSON").option(
3361
- "--from <deployId>",
3362
- "Promote a specific past deploy id (alias rewrite)"
3363
- ).action(async (flags) => {
3364
- try {
3365
- await promote({
3366
- json: flags.json ?? false,
3367
- from: flags.from
3368
- });
3369
- } catch (err) {
3370
- handleActionError("promote", flags.json ?? false, err);
3371
- }
3372
- });
3373
- staticCli.command("rollback", "Rewrite production alias to a past deploy").option("--json", "Output as JSON").option("--to <deployId>", "Target deploy id (required)").action(async (flags) => {
3374
- try {
3375
- await rollback({
3376
- json: flags.json ?? false,
3377
- to: flags.to
3378
- });
3379
- } catch (err) {
3380
- handleActionError("rollback", flags.json ?? false, err);
3381
- }
3382
- });
3383
- staticCli.command("ls", "List recent deploys for a site").option("--json", "Output as JSON").option("--site <site>", "Override site from platform.yaml").action(async (flags) => {
3384
- try {
3385
- await ls({
3386
- json: flags.json ?? false,
3387
- site: flags.site
3388
- });
3389
- } catch (err) {
3390
- handleActionError("ls", flags.json ?? false, err);
3391
- }
3392
- });
3393
- staticCli.help();
3394
- staticCli.version(version);
3395
- staticCli.parse(["node", "universe-static", ...staticArgs]);
3396
- } else {
3397
- const cli = cac("universe");
3398
- cli.command("login", "Authenticate with GitHub via OAuth device flow").option("--json", "Output as JSON").option("--force", "Replace any existing stored token").action(async (flags) => {
3399
- try {
3400
- await login({
3401
- json: flags.json ?? false,
3402
- force: flags.force ?? false
3403
- });
3404
- } catch (err) {
3405
- handleActionError("login", flags.json ?? false, err);
3406
- }
3407
- });
3408
- cli.command("logout", "Remove the stored GitHub device-flow token").option("--json", "Output as JSON").action(async (flags) => {
3409
- try {
3410
- await logout({ json: flags.json ?? false });
3411
- } catch (err) {
3412
- handleActionError("logout", flags.json ?? false, err);
3413
- }
3414
- });
3415
- cli.command("whoami", "Show resolved GitHub identity and authorized sites").option("--json", "Output as JSON").action(async (flags) => {
3416
- try {
3417
- await whoami({ json: flags.json ?? false });
3418
- } catch (err) {
3419
- handleActionError("whoami", flags.json ?? false, err);
3420
- }
3421
- });
3422
- cli.command("static <subcommand>", "Static site deployment commands");
3423
- cli.command("sites <subcommand>", "Static site registry commands");
3424
- cli.command(
3425
- "repo <subcommand>",
3426
- "Repository creation + approval queue commands"
3427
- );
3428
- cli.help();
3429
- cli.version(version);
3430
- cli.parse(argv);
3431
- }
3432
- if (versionRequested) {
3433
- await refreshIfStale(Date.now(), { force: true });
3434
- }
3435
- }
3436
-
3437
- // src/lib/fatal.ts
3438
- function formatFatal(err) {
3439
- const message = err instanceof Error ? err.message : String(err);
3440
- return `universe: ${redact(message)}`;
3441
- }
3442
- function defaultOnFatal(err) {
3443
- process.stderr.write(formatFatal(err) + "\n");
3444
- process.exit(EXIT_USAGE);
3445
- }
3446
- function installFatalHandlers(onFatal = defaultOnFatal) {
3447
- process.on("unhandledRejection", onFatal);
3448
- process.on("uncaughtException", onFatal);
3449
- }
3450
-
3451
- // src/index.ts
3452
- if (process.env[REFRESH_ENV] === "1") {
3453
- void runRefreshWorker();
3454
- } else {
3455
- installFatalHandlers();
3456
- void run();
3457
- }