@freecodecamp/universe-cli 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -0
- package/dist/index.cjs +1086 -76
- package/dist/index.js +882 -28
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -39,6 +39,9 @@ var StorageError = class extends CliError {
|
|
|
39
39
|
var GitError = class extends CliError {
|
|
40
40
|
exitCode = EXIT_GIT;
|
|
41
41
|
};
|
|
42
|
+
var ConfirmError = class extends CliError {
|
|
43
|
+
exitCode = EXIT_CONFIRM;
|
|
44
|
+
};
|
|
42
45
|
var PartialUploadError = class extends CliError {
|
|
43
46
|
exitCode = EXIT_PARTIAL;
|
|
44
47
|
};
|
|
@@ -51,8 +54,8 @@ import { execSync } from "child_process";
|
|
|
51
54
|
function getGitState() {
|
|
52
55
|
try {
|
|
53
56
|
const hash = execSync("git rev-parse HEAD", { encoding: "utf-8" }).trim();
|
|
54
|
-
const
|
|
55
|
-
return { hash, dirty:
|
|
57
|
+
const status2 = execSync("git status --porcelain", { encoding: "utf-8" });
|
|
58
|
+
return { hash, dirty: status2.length > 0 };
|
|
56
59
|
} catch {
|
|
57
60
|
return { hash: null, dirty: false, error: "not a git repository" };
|
|
58
61
|
}
|
|
@@ -101,13 +104,13 @@ import { spawn } from "child_process";
|
|
|
101
104
|
import { stat } from "fs/promises";
|
|
102
105
|
import { isAbsolute, resolve } from "path";
|
|
103
106
|
var defaultExec = async (req) => {
|
|
104
|
-
return new Promise((resolveExit,
|
|
107
|
+
return new Promise((resolveExit, reject2) => {
|
|
105
108
|
const child = spawn(req.command, {
|
|
106
109
|
cwd: req.cwd,
|
|
107
110
|
shell: true,
|
|
108
111
|
stdio: "inherit"
|
|
109
112
|
});
|
|
110
|
-
child.on("error", (err) =>
|
|
113
|
+
child.on("error", (err) => reject2(err));
|
|
111
114
|
child.on("exit", (code) => resolveExit(code ?? 1));
|
|
112
115
|
});
|
|
113
116
|
};
|
|
@@ -428,6 +431,52 @@ function suggest(target, candidates, threshold = 2) {
|
|
|
428
431
|
return bestD <= threshold ? best : null;
|
|
429
432
|
}
|
|
430
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
|
+
|
|
431
480
|
// src/lib/proxy-client.ts
|
|
432
481
|
var DEFAULT_FETCH_TIMEOUT_MS = 3e4;
|
|
433
482
|
function parseFetchTimeoutMs(env) {
|
|
@@ -441,11 +490,13 @@ var ProxyError = class extends CliError {
|
|
|
441
490
|
exitCode;
|
|
442
491
|
status;
|
|
443
492
|
code;
|
|
444
|
-
|
|
493
|
+
requestId;
|
|
494
|
+
constructor(status2, code, message, requestId) {
|
|
445
495
|
super(message);
|
|
446
|
-
this.status =
|
|
496
|
+
this.status = status2;
|
|
447
497
|
this.code = code;
|
|
448
|
-
this.exitCode = mapExitCode(
|
|
498
|
+
this.exitCode = mapExitCode(status2);
|
|
499
|
+
this.requestId = requestId;
|
|
449
500
|
}
|
|
450
501
|
};
|
|
451
502
|
var AliasDriftError = class extends ProxyError {
|
|
@@ -455,16 +506,22 @@ var AliasDriftError = class extends ProxyError {
|
|
|
455
506
|
this.current = current;
|
|
456
507
|
}
|
|
457
508
|
};
|
|
458
|
-
function mapExitCode(
|
|
459
|
-
if (
|
|
460
|
-
if (
|
|
509
|
+
function mapExitCode(status2) {
|
|
510
|
+
if (status2 === 401 || status2 === 403) return EXIT_CREDENTIALS;
|
|
511
|
+
if (status2 === 422 || status2 === 0 || status2 >= 500) return EXIT_STORAGE;
|
|
461
512
|
return EXIT_USAGE;
|
|
462
513
|
}
|
|
463
514
|
function wrapProxyError(command, err) {
|
|
464
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
|
+
}
|
|
465
520
|
return {
|
|
466
521
|
code: err.exitCode,
|
|
467
|
-
message
|
|
522
|
+
message,
|
|
523
|
+
kind: err.code,
|
|
524
|
+
requestId: err.requestId
|
|
468
525
|
};
|
|
469
526
|
}
|
|
470
527
|
if (err instanceof CliError) {
|
|
@@ -479,34 +536,34 @@ function isProxyErrorEnvelope(value) {
|
|
|
479
536
|
return typeof value === "object" && value !== null && !Array.isArray(value) && "error" in value;
|
|
480
537
|
}
|
|
481
538
|
async function readErrorEnvelope(response) {
|
|
482
|
-
const
|
|
539
|
+
const status2 = response.status;
|
|
483
540
|
let raw;
|
|
484
541
|
try {
|
|
485
542
|
raw = await response.json();
|
|
486
543
|
} catch {
|
|
487
544
|
return {
|
|
488
|
-
code: `http_${
|
|
545
|
+
code: `http_${status2}`,
|
|
489
546
|
message: response.statusText || "request failed"
|
|
490
547
|
};
|
|
491
548
|
}
|
|
492
549
|
if (isProxyErrorEnvelope(raw) && raw.error) {
|
|
493
550
|
const current = typeof raw.current === "string" ? raw.current : void 0;
|
|
494
551
|
return {
|
|
495
|
-
code: raw.error.code ?? `http_${
|
|
552
|
+
code: raw.error.code ?? `http_${status2}`,
|
|
496
553
|
message: raw.error.message ?? response.statusText ?? "request failed",
|
|
497
554
|
...current === void 0 ? {} : { current }
|
|
498
555
|
};
|
|
499
556
|
}
|
|
500
557
|
return {
|
|
501
|
-
code: `http_${
|
|
558
|
+
code: `http_${status2}`,
|
|
502
559
|
message: response.statusText || "request failed"
|
|
503
560
|
};
|
|
504
561
|
}
|
|
505
|
-
function throwProxyError(
|
|
506
|
-
if (
|
|
562
|
+
function throwProxyError(status2, env, requestId) {
|
|
563
|
+
if (status2 === 409 && env.code === "alias_drift") {
|
|
507
564
|
throw new AliasDriftError(env.message, env.current ?? "");
|
|
508
565
|
}
|
|
509
|
-
throw new ProxyError(
|
|
566
|
+
throw new ProxyError(status2, env.code, env.message, requestId);
|
|
510
567
|
}
|
|
511
568
|
function stripTrailingSlash(url) {
|
|
512
569
|
return url.endsWith("/") ? url.slice(0, -1) : url;
|
|
@@ -515,6 +572,7 @@ function createProxyClient(cfg) {
|
|
|
515
572
|
const base = stripTrailingSlash(cfg.baseUrl);
|
|
516
573
|
const fetchImpl = cfg.fetch ?? globalThis.fetch.bind(globalThis);
|
|
517
574
|
const timeoutMs = cfg.timeoutMs ?? DEFAULT_FETCH_TIMEOUT_MS;
|
|
575
|
+
const debug = cfg.debug ?? false;
|
|
518
576
|
function withTimeoutSignal(init) {
|
|
519
577
|
if (timeoutMs <= 0) return init;
|
|
520
578
|
const timeoutSignal = AbortSignal.timeout(timeoutMs);
|
|
@@ -526,31 +584,65 @@ function createProxyClient(cfg) {
|
|
|
526
584
|
throw new ProxyError(
|
|
527
585
|
0,
|
|
528
586
|
"timeout",
|
|
529
|
-
`proxy timed out after ${timeoutMs}ms`
|
|
587
|
+
`proxy timed out after ${timeoutMs}ms (${base})`
|
|
530
588
|
);
|
|
531
589
|
}
|
|
532
590
|
const message = err instanceof Error ? err.message : String(err);
|
|
533
|
-
throw new ProxyError(
|
|
591
|
+
throw new ProxyError(
|
|
592
|
+
0,
|
|
593
|
+
"network_error",
|
|
594
|
+
`proxy unreachable at ${base}: ${message}`
|
|
595
|
+
);
|
|
534
596
|
}
|
|
535
597
|
async function userBearer() {
|
|
536
598
|
const tok = await cfg.getAuthToken();
|
|
537
599
|
return `Bearer ${tok}`;
|
|
538
600
|
}
|
|
539
|
-
async function call(url, init) {
|
|
601
|
+
async function call(url, init, validate) {
|
|
602
|
+
const startedAt = debug ? Date.now() : 0;
|
|
540
603
|
let response;
|
|
541
604
|
try {
|
|
542
605
|
response = await fetchImpl(url, withTimeoutSignal(init));
|
|
543
606
|
} catch (err) {
|
|
544
607
|
translateFetchError(err);
|
|
545
608
|
}
|
|
609
|
+
if (debug) {
|
|
610
|
+
process.stderr.write(
|
|
611
|
+
`[universe] ${init.method ?? "GET"} ${url} -> ${response.status} (${Date.now() - startedAt}ms)
|
|
612
|
+
`
|
|
613
|
+
);
|
|
614
|
+
}
|
|
546
615
|
if (!response.ok) {
|
|
616
|
+
const requestId = response.headers.get("x-request-id") ?? void 0;
|
|
547
617
|
const env = await readErrorEnvelope(response);
|
|
548
|
-
throwProxyError(response.status, env);
|
|
618
|
+
throwProxyError(response.status, env, requestId);
|
|
549
619
|
}
|
|
550
620
|
if (response.status === 204) {
|
|
551
621
|
return void 0;
|
|
552
622
|
}
|
|
553
|
-
|
|
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;
|
|
554
646
|
}
|
|
555
647
|
return {
|
|
556
648
|
async whoami() {
|
|
@@ -711,6 +803,105 @@ function createProxyClient(cfg) {
|
|
|
711
803
|
Accept: "application/json"
|
|
712
804
|
}
|
|
713
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 listRepoTemplates() {
|
|
893
|
+
const res = await call(
|
|
894
|
+
`${base}/api/repo/templates`,
|
|
895
|
+
{
|
|
896
|
+
method: "GET",
|
|
897
|
+
headers: {
|
|
898
|
+
Authorization: await userBearer(),
|
|
899
|
+
Accept: "application/json"
|
|
900
|
+
}
|
|
901
|
+
},
|
|
902
|
+
(raw) => repoTemplatesResponseSchema.parse(raw)
|
|
903
|
+
);
|
|
904
|
+
return res.templates ?? [];
|
|
714
905
|
}
|
|
715
906
|
};
|
|
716
907
|
}
|
|
@@ -851,11 +1042,17 @@ function buildEnvelope(command, success, data) {
|
|
|
851
1042
|
...data
|
|
852
1043
|
};
|
|
853
1044
|
}
|
|
854
|
-
function buildErrorEnvelope(command, code, message, issues) {
|
|
1045
|
+
function buildErrorEnvelope(command, code, message, issues, kind, requestId) {
|
|
855
1046
|
const error = {
|
|
856
1047
|
code,
|
|
857
1048
|
message
|
|
858
1049
|
};
|
|
1050
|
+
if (kind !== void 0) {
|
|
1051
|
+
error.kind = kind;
|
|
1052
|
+
}
|
|
1053
|
+
if (requestId !== void 0) {
|
|
1054
|
+
error.requestId = requestId;
|
|
1055
|
+
}
|
|
859
1056
|
if (issues !== void 0) {
|
|
860
1057
|
error.issues = issues;
|
|
861
1058
|
}
|
|
@@ -959,12 +1156,14 @@ function outputError(ctx, code, message, optsOrIssues) {
|
|
|
959
1156
|
ctx.command,
|
|
960
1157
|
code,
|
|
961
1158
|
redactedMessage,
|
|
962
|
-
redactedIssues
|
|
1159
|
+
redactedIssues,
|
|
1160
|
+
opts.kind,
|
|
1161
|
+
opts.requestId
|
|
963
1162
|
);
|
|
964
1163
|
const payload = opts.extras ? { ...envelope, ...redactObject(opts.extras) } : envelope;
|
|
965
1164
|
process.stdout.write(JSON.stringify(payload) + "\n");
|
|
966
1165
|
} else {
|
|
967
|
-
(opts.logError ?? ((m) => log.error(m)))(redactedMessage);
|
|
1166
|
+
(opts.logError ?? ((m) => log.error(m, { output: process.stderr })))(redactedMessage);
|
|
968
1167
|
}
|
|
969
1168
|
}
|
|
970
1169
|
|
|
@@ -1885,6 +2084,7 @@ async function whoami(options, deps = {}) {
|
|
|
1885
2084
|
buildEnvelope("whoami", true, {
|
|
1886
2085
|
login: result.login,
|
|
1887
2086
|
identitySource: identity.source,
|
|
2087
|
+
proxyUrl: baseUrl,
|
|
1888
2088
|
authorizedSitesCount: count
|
|
1889
2089
|
})
|
|
1890
2090
|
);
|
|
@@ -1894,6 +2094,7 @@ async function whoami(options, deps = {}) {
|
|
|
1894
2094
|
[
|
|
1895
2095
|
`Logged in as: ${result.login}`,
|
|
1896
2096
|
`Identity source: ${identity.source}`,
|
|
2097
|
+
`Proxy: ${baseUrl}`,
|
|
1897
2098
|
sitesLine
|
|
1898
2099
|
].join("\n")
|
|
1899
2100
|
);
|
|
@@ -2130,6 +2331,508 @@ async function update(options, deps = {}) {
|
|
|
2130
2331
|
}
|
|
2131
2332
|
}
|
|
2132
2333
|
|
|
2334
|
+
// src/commands/repo/approve.ts
|
|
2335
|
+
import { log as log13 } from "@clack/prompts";
|
|
2336
|
+
|
|
2337
|
+
// src/commands/repo/_shared.ts
|
|
2338
|
+
import {
|
|
2339
|
+
confirm as clackConfirm,
|
|
2340
|
+
isCancel as clackIsCancel,
|
|
2341
|
+
select as clackSelect,
|
|
2342
|
+
text as clackText
|
|
2343
|
+
} from "@clack/prompts";
|
|
2344
|
+
var defaultRepoPrompts = {
|
|
2345
|
+
text: clackText,
|
|
2346
|
+
select: clackSelect,
|
|
2347
|
+
confirm: clackConfirm,
|
|
2348
|
+
isCancel: clackIsCancel
|
|
2349
|
+
};
|
|
2350
|
+
function emitJson9(envelope) {
|
|
2351
|
+
process.stdout.write(JSON.stringify(envelope) + "\n");
|
|
2352
|
+
}
|
|
2353
|
+
async function setupClient2(deps) {
|
|
2354
|
+
const env = deps.env ?? process.env;
|
|
2355
|
+
const resolveId = deps.resolveIdentity ?? resolveIdentity;
|
|
2356
|
+
const mkClient = deps.createProxyClient ?? createProxyClient;
|
|
2357
|
+
const identity = await resolveId({ env });
|
|
2358
|
+
if (!identity) {
|
|
2359
|
+
throw new CredentialError(
|
|
2360
|
+
"No GitHub identity available. Run `universe login`, set $GITHUB_TOKEN, or install the gh CLI."
|
|
2361
|
+
);
|
|
2362
|
+
}
|
|
2363
|
+
const baseUrl = env["UNIVERSE_PROXY_URL"] ?? DEFAULT_PROXY_URL;
|
|
2364
|
+
const client = mkClient({
|
|
2365
|
+
baseUrl,
|
|
2366
|
+
getAuthToken: () => identity.token,
|
|
2367
|
+
timeoutMs: parseFetchTimeoutMs(env),
|
|
2368
|
+
debug: Boolean(env["UNIVERSE_DEBUG"])
|
|
2369
|
+
});
|
|
2370
|
+
return { client, identitySource: identity.source };
|
|
2371
|
+
}
|
|
2372
|
+
function formatRepoTable(rows, emptyMsg = "No repo requests.") {
|
|
2373
|
+
if (rows.length === 0) return emptyMsg;
|
|
2374
|
+
const headers = [
|
|
2375
|
+
"ID",
|
|
2376
|
+
"REPO",
|
|
2377
|
+
"VIS",
|
|
2378
|
+
"STATUS",
|
|
2379
|
+
"REQUESTED BY",
|
|
2380
|
+
"REQUESTED AT"
|
|
2381
|
+
];
|
|
2382
|
+
const cells = rows.map((r) => [
|
|
2383
|
+
r.id,
|
|
2384
|
+
r.name,
|
|
2385
|
+
r.visibility,
|
|
2386
|
+
r.status,
|
|
2387
|
+
r.requestedBy,
|
|
2388
|
+
r.createdAt
|
|
2389
|
+
]);
|
|
2390
|
+
const widths = headers.map(
|
|
2391
|
+
(h, i) => Math.max(h.length, ...cells.map((row) => row[i]?.length ?? 0))
|
|
2392
|
+
);
|
|
2393
|
+
const fmt = (row) => row.map((c, i) => c.padEnd(widths[i] ?? 0)).join(" ");
|
|
2394
|
+
return [fmt(headers), ...cells.map(fmt)].join("\n");
|
|
2395
|
+
}
|
|
2396
|
+
|
|
2397
|
+
// src/commands/repo/approve.ts
|
|
2398
|
+
async function approve(options, deps = {}) {
|
|
2399
|
+
const command = "repo approve";
|
|
2400
|
+
const success = deps.logSuccess ?? ((s) => log13.success(s));
|
|
2401
|
+
const error = deps.logError ?? ((s) => log13.error(s));
|
|
2402
|
+
const exit = deps.exit ?? exitWithCode;
|
|
2403
|
+
const prompts = deps.prompts ?? defaultRepoPrompts;
|
|
2404
|
+
const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
|
|
2405
|
+
let creationFailure;
|
|
2406
|
+
let identitySource;
|
|
2407
|
+
try {
|
|
2408
|
+
if (!options.id || options.id.trim().length === 0) {
|
|
2409
|
+
throw new UsageError("request id is required (positional argument)");
|
|
2410
|
+
}
|
|
2411
|
+
const setup = await setupClient2(deps);
|
|
2412
|
+
const client = setup.client;
|
|
2413
|
+
identitySource = setup.identitySource;
|
|
2414
|
+
if (!options.json && !options.yes) {
|
|
2415
|
+
if (!isTTY) {
|
|
2416
|
+
throw new UsageError(
|
|
2417
|
+
"non-interactive session: pass --yes to approve without confirmation"
|
|
2418
|
+
);
|
|
2419
|
+
}
|
|
2420
|
+
const cur = await client.getRepoRequest(options.id);
|
|
2421
|
+
const ok = await prompts.confirm({
|
|
2422
|
+
message: `Approve ${cur.visibility} repo "${cur.name}" requested by ${cur.requestedBy}? This creates the repository.`
|
|
2423
|
+
});
|
|
2424
|
+
if (prompts.isCancel(ok) || ok === false) {
|
|
2425
|
+
throw new ConfirmError("repo approve cancelled");
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
const res = await client.approveRepoRequest({ id: options.id });
|
|
2429
|
+
const row = res.request;
|
|
2430
|
+
if (res.outcome === "approved_failed") {
|
|
2431
|
+
if (!options.json) {
|
|
2432
|
+
throw new StorageError(
|
|
2433
|
+
`approved, but repository creation failed: ${row.error ?? "unknown"} (${row.owner}/${row.name}, requested by ${row.requestedBy})`
|
|
2434
|
+
);
|
|
2435
|
+
}
|
|
2436
|
+
creationFailure = {
|
|
2437
|
+
outcome: res.outcome,
|
|
2438
|
+
id: row.id,
|
|
2439
|
+
repo: `${row.owner}/${row.name}`,
|
|
2440
|
+
status: row.status,
|
|
2441
|
+
creationError: row.error ?? "unknown",
|
|
2442
|
+
requestedBy: row.requestedBy,
|
|
2443
|
+
identitySource
|
|
2444
|
+
};
|
|
2445
|
+
} else if (options.json) {
|
|
2446
|
+
emitJson9(
|
|
2447
|
+
buildEnvelope(command, true, {
|
|
2448
|
+
id: row.id,
|
|
2449
|
+
outcome: res.outcome,
|
|
2450
|
+
repo: `${row.owner}/${row.name}`,
|
|
2451
|
+
url: row.url,
|
|
2452
|
+
visibility: row.visibility,
|
|
2453
|
+
approver: row.approver,
|
|
2454
|
+
identitySource
|
|
2455
|
+
})
|
|
2456
|
+
);
|
|
2457
|
+
} else {
|
|
2458
|
+
success(
|
|
2459
|
+
[
|
|
2460
|
+
`Approved ${row.name}`,
|
|
2461
|
+
``,
|
|
2462
|
+
` Repository: ${row.url ?? `${row.owner}/${row.name}`}`,
|
|
2463
|
+
` Visibility: ${row.visibility}`,
|
|
2464
|
+
` Approved by: ${row.approver ?? "you"}`
|
|
2465
|
+
].join("\n")
|
|
2466
|
+
);
|
|
2467
|
+
}
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
const { code, message, kind, requestId } = wrapProxyError(command, err);
|
|
2470
|
+
outputError({ json: options.json, command }, code, message, {
|
|
2471
|
+
logError: error,
|
|
2472
|
+
kind,
|
|
2473
|
+
requestId,
|
|
2474
|
+
extras: identitySource ? { identitySource } : void 0
|
|
2475
|
+
});
|
|
2476
|
+
exit(code);
|
|
2477
|
+
return;
|
|
2478
|
+
}
|
|
2479
|
+
if (creationFailure) {
|
|
2480
|
+
outputError(
|
|
2481
|
+
{ json: true, command },
|
|
2482
|
+
EXIT_STORAGE,
|
|
2483
|
+
"approved, but repository creation failed",
|
|
2484
|
+
{ logError: error, extras: creationFailure }
|
|
2485
|
+
);
|
|
2486
|
+
exit(EXIT_STORAGE);
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// src/commands/repo/create.ts
|
|
2491
|
+
import { log as log14 } from "@clack/prompts";
|
|
2492
|
+
function blankToUndefined(s) {
|
|
2493
|
+
if (s === void 0 || s === null) return void 0;
|
|
2494
|
+
const t = String(s).trim();
|
|
2495
|
+
return t === "" ? void 0 : t;
|
|
2496
|
+
}
|
|
2497
|
+
async function promptTemplate(client, prompts) {
|
|
2498
|
+
let templates = [];
|
|
2499
|
+
try {
|
|
2500
|
+
templates = await client.listRepoTemplates();
|
|
2501
|
+
} catch {
|
|
2502
|
+
templates = [];
|
|
2503
|
+
}
|
|
2504
|
+
if (templates.length === 0) {
|
|
2505
|
+
const v2 = await prompts.text({
|
|
2506
|
+
message: "Template (optional)",
|
|
2507
|
+
placeholder: "name of an org template repo; blank for an empty repo"
|
|
2508
|
+
});
|
|
2509
|
+
if (prompts.isCancel(v2)) throw new ConfirmError("repo create cancelled");
|
|
2510
|
+
return blankToUndefined(String(v2));
|
|
2511
|
+
}
|
|
2512
|
+
const v = await prompts.select({
|
|
2513
|
+
message: "Template",
|
|
2514
|
+
options: [
|
|
2515
|
+
{ value: "", label: "None (blank repo)" },
|
|
2516
|
+
...templates.map((t) => ({ value: t, label: t }))
|
|
2517
|
+
],
|
|
2518
|
+
initialValue: ""
|
|
2519
|
+
});
|
|
2520
|
+
if (prompts.isCancel(v)) throw new ConfirmError("repo create cancelled");
|
|
2521
|
+
return blankToUndefined(String(v));
|
|
2522
|
+
}
|
|
2523
|
+
async function create(options, deps = {}) {
|
|
2524
|
+
const command = "repo create";
|
|
2525
|
+
const success = deps.logSuccess ?? ((s) => log14.success(s));
|
|
2526
|
+
const error = deps.logError ?? ((s) => log14.error(s));
|
|
2527
|
+
const exit = deps.exit ?? exitWithCode;
|
|
2528
|
+
const prompts = deps.prompts ?? defaultRepoPrompts;
|
|
2529
|
+
const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
|
|
2530
|
+
const canPrompt = !options.json && !options.yes && isTTY;
|
|
2531
|
+
let identitySource = "";
|
|
2532
|
+
try {
|
|
2533
|
+
let client;
|
|
2534
|
+
const ensureClient = async () => {
|
|
2535
|
+
if (!client) {
|
|
2536
|
+
const setup = await setupClient2(deps);
|
|
2537
|
+
client = setup.client;
|
|
2538
|
+
identitySource = setup.identitySource;
|
|
2539
|
+
}
|
|
2540
|
+
return client;
|
|
2541
|
+
};
|
|
2542
|
+
let name = blankToUndefined(options.name) ?? "";
|
|
2543
|
+
let visibility = options.visibility;
|
|
2544
|
+
let description = options.description;
|
|
2545
|
+
let template = options.template;
|
|
2546
|
+
if (canPrompt) {
|
|
2547
|
+
if (!name) {
|
|
2548
|
+
const v = await prompts.text({
|
|
2549
|
+
message: "Repository name",
|
|
2550
|
+
placeholder: "learn-python-rpg"
|
|
2551
|
+
});
|
|
2552
|
+
if (prompts.isCancel(v))
|
|
2553
|
+
throw new ConfirmError("repo create cancelled");
|
|
2554
|
+
name = String(v).trim();
|
|
2555
|
+
}
|
|
2556
|
+
if (visibility === void 0) {
|
|
2557
|
+
const v = await prompts.select({
|
|
2558
|
+
message: "Visibility",
|
|
2559
|
+
options: [
|
|
2560
|
+
{ value: "private", label: "Private" },
|
|
2561
|
+
{ value: "public", label: "Public" }
|
|
2562
|
+
],
|
|
2563
|
+
initialValue: "private"
|
|
2564
|
+
});
|
|
2565
|
+
if (prompts.isCancel(v))
|
|
2566
|
+
throw new ConfirmError("repo create cancelled");
|
|
2567
|
+
visibility = String(v);
|
|
2568
|
+
}
|
|
2569
|
+
if (description === void 0) {
|
|
2570
|
+
const v = await prompts.text({
|
|
2571
|
+
message: "Description (optional)",
|
|
2572
|
+
placeholder: "What is this project about?"
|
|
2573
|
+
});
|
|
2574
|
+
if (prompts.isCancel(v))
|
|
2575
|
+
throw new ConfirmError("repo create cancelled");
|
|
2576
|
+
description = String(v);
|
|
2577
|
+
}
|
|
2578
|
+
if (template === void 0) {
|
|
2579
|
+
template = await promptTemplate(await ensureClient(), prompts);
|
|
2580
|
+
}
|
|
2581
|
+
}
|
|
2582
|
+
if (!name) {
|
|
2583
|
+
throw new UsageError("repo name is required");
|
|
2584
|
+
}
|
|
2585
|
+
const candidate = { name };
|
|
2586
|
+
if (visibility !== void 0 && visibility !== "") {
|
|
2587
|
+
candidate.visibility = visibility;
|
|
2588
|
+
}
|
|
2589
|
+
const desc = blankToUndefined(description);
|
|
2590
|
+
if (desc !== void 0) candidate.description = desc;
|
|
2591
|
+
const tmpl = blankToUndefined(template);
|
|
2592
|
+
if (tmpl !== void 0) candidate.template = tmpl;
|
|
2593
|
+
const parsed = createRepoRequestSchema.safeParse(candidate);
|
|
2594
|
+
if (!parsed.success) {
|
|
2595
|
+
const issues = parsed.error.issues.map(
|
|
2596
|
+
(i) => `${i.path.join(".") || "input"}: ${i.message}`
|
|
2597
|
+
);
|
|
2598
|
+
throw new UsageError(issues.join("; "));
|
|
2599
|
+
}
|
|
2600
|
+
const body = parsed.data;
|
|
2601
|
+
if (!options.json && !options.yes) {
|
|
2602
|
+
if (!isTTY) {
|
|
2603
|
+
throw new UsageError(
|
|
2604
|
+
"non-interactive session: pass --yes to submit without confirmation (or --json)"
|
|
2605
|
+
);
|
|
2606
|
+
}
|
|
2607
|
+
const ok = await prompts.confirm({
|
|
2608
|
+
message: `Submit request to create ${body.visibility} repo "${body.name}"${body.template ? ` from template ${body.template}` : ""}?`
|
|
2609
|
+
});
|
|
2610
|
+
if (prompts.isCancel(ok) || ok === false) {
|
|
2611
|
+
throw new ConfirmError("repo create cancelled");
|
|
2612
|
+
}
|
|
2613
|
+
}
|
|
2614
|
+
const activeClient = await ensureClient();
|
|
2615
|
+
const row = await activeClient.createRepoRequest({
|
|
2616
|
+
name: body.name,
|
|
2617
|
+
visibility: body.visibility,
|
|
2618
|
+
description: body.description,
|
|
2619
|
+
template: body.template
|
|
2620
|
+
});
|
|
2621
|
+
if (options.json) {
|
|
2622
|
+
emitJson9(
|
|
2623
|
+
buildEnvelope(command, true, {
|
|
2624
|
+
id: row.id,
|
|
2625
|
+
name: row.name,
|
|
2626
|
+
owner: row.owner,
|
|
2627
|
+
visibility: row.visibility,
|
|
2628
|
+
template: row.template,
|
|
2629
|
+
status: row.status,
|
|
2630
|
+
identitySource
|
|
2631
|
+
})
|
|
2632
|
+
);
|
|
2633
|
+
} else {
|
|
2634
|
+
success(
|
|
2635
|
+
[
|
|
2636
|
+
`Request submitted`,
|
|
2637
|
+
``,
|
|
2638
|
+
` Request id: ${row.id}`,
|
|
2639
|
+
` Repository: ${row.owner}/${row.name}`,
|
|
2640
|
+
` Visibility: ${row.visibility}`,
|
|
2641
|
+
...row.template ? [` Template: ${row.template}`] : [],
|
|
2642
|
+
` Status: ${row.status} \u2014 run \`universe repo ls\` to review`
|
|
2643
|
+
].join("\n")
|
|
2644
|
+
);
|
|
2645
|
+
}
|
|
2646
|
+
} catch (err) {
|
|
2647
|
+
const { code, message, kind, requestId } = wrapProxyError(command, err);
|
|
2648
|
+
outputError({ json: options.json, command }, code, message, {
|
|
2649
|
+
logError: error,
|
|
2650
|
+
kind,
|
|
2651
|
+
requestId,
|
|
2652
|
+
extras: identitySource ? { identitySource } : void 0
|
|
2653
|
+
});
|
|
2654
|
+
exit(code);
|
|
2655
|
+
}
|
|
2656
|
+
}
|
|
2657
|
+
|
|
2658
|
+
// src/commands/repo/ls.ts
|
|
2659
|
+
import { log as log15 } from "@clack/prompts";
|
|
2660
|
+
var LS_STATUSES = [...repoStatusSchema.options, "all"];
|
|
2661
|
+
async function ls3(options, deps = {}) {
|
|
2662
|
+
const command = "repo ls";
|
|
2663
|
+
const message = deps.logMessage ?? ((s) => log15.message(s));
|
|
2664
|
+
const error = deps.logError ?? ((s) => log15.error(s));
|
|
2665
|
+
const exit = deps.exit ?? exitWithCode;
|
|
2666
|
+
let identitySource;
|
|
2667
|
+
try {
|
|
2668
|
+
if (options.status !== void 0 && !LS_STATUSES.includes(options.status)) {
|
|
2669
|
+
throw new UsageError(
|
|
2670
|
+
`invalid --status "${options.status}": must be one of ${LS_STATUSES.join(", ")}`
|
|
2671
|
+
);
|
|
2672
|
+
}
|
|
2673
|
+
const setup = await setupClient2(deps);
|
|
2674
|
+
const client = setup.client;
|
|
2675
|
+
identitySource = setup.identitySource;
|
|
2676
|
+
const rows = await client.listRepoRequests({
|
|
2677
|
+
status: options.status,
|
|
2678
|
+
mine: options.mine ?? false
|
|
2679
|
+
});
|
|
2680
|
+
const status2 = options.status ?? "pending";
|
|
2681
|
+
if (options.json) {
|
|
2682
|
+
emitJson9(
|
|
2683
|
+
buildEnvelope(command, true, {
|
|
2684
|
+
count: rows.length,
|
|
2685
|
+
status: status2,
|
|
2686
|
+
mine: options.mine ?? false,
|
|
2687
|
+
requests: rows,
|
|
2688
|
+
identitySource
|
|
2689
|
+
})
|
|
2690
|
+
);
|
|
2691
|
+
} else {
|
|
2692
|
+
const empty = status2 === "all" ? "No repo requests." : `No ${status2} repo requests.`;
|
|
2693
|
+
message(formatRepoTable(rows, empty));
|
|
2694
|
+
}
|
|
2695
|
+
} catch (err) {
|
|
2696
|
+
const {
|
|
2697
|
+
code,
|
|
2698
|
+
message: msg,
|
|
2699
|
+
kind,
|
|
2700
|
+
requestId
|
|
2701
|
+
} = wrapProxyError(command, err);
|
|
2702
|
+
outputError({ json: options.json, command }, code, msg, {
|
|
2703
|
+
logError: error,
|
|
2704
|
+
kind,
|
|
2705
|
+
requestId,
|
|
2706
|
+
extras: identitySource ? { identitySource } : void 0
|
|
2707
|
+
});
|
|
2708
|
+
exit(code);
|
|
2709
|
+
}
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
// src/commands/repo/reject.ts
|
|
2713
|
+
import { log as log16 } from "@clack/prompts";
|
|
2714
|
+
async function reject(options, deps = {}) {
|
|
2715
|
+
const command = "repo reject";
|
|
2716
|
+
const success = deps.logSuccess ?? ((s) => log16.success(s));
|
|
2717
|
+
const error = deps.logError ?? ((s) => log16.error(s));
|
|
2718
|
+
const exit = deps.exit ?? exitWithCode;
|
|
2719
|
+
const prompts = deps.prompts ?? defaultRepoPrompts;
|
|
2720
|
+
const isTTY = deps.isTTY ?? Boolean(process.stdout.isTTY);
|
|
2721
|
+
let identitySource;
|
|
2722
|
+
try {
|
|
2723
|
+
if (!options.id || options.id.trim().length === 0) {
|
|
2724
|
+
throw new UsageError("request id is required (positional argument)");
|
|
2725
|
+
}
|
|
2726
|
+
const setup = await setupClient2(deps);
|
|
2727
|
+
const client = setup.client;
|
|
2728
|
+
identitySource = setup.identitySource;
|
|
2729
|
+
if (!options.json && !options.yes) {
|
|
2730
|
+
if (!isTTY) {
|
|
2731
|
+
throw new UsageError(
|
|
2732
|
+
"non-interactive session: pass --yes to reject without confirmation"
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
const cur = await client.getRepoRequest(options.id);
|
|
2736
|
+
const ok = await prompts.confirm({
|
|
2737
|
+
message: `Reject the request for "${cur.name}" by ${cur.requestedBy}?`
|
|
2738
|
+
});
|
|
2739
|
+
if (prompts.isCancel(ok) || ok === false) {
|
|
2740
|
+
throw new ConfirmError("repo reject cancelled");
|
|
2741
|
+
}
|
|
2742
|
+
}
|
|
2743
|
+
const reason = options.reason === void 0 ? void 0 : String(options.reason).trim() || void 0;
|
|
2744
|
+
const row = await client.rejectRepoRequest({
|
|
2745
|
+
id: options.id,
|
|
2746
|
+
reason
|
|
2747
|
+
});
|
|
2748
|
+
if (options.json) {
|
|
2749
|
+
emitJson9(
|
|
2750
|
+
buildEnvelope(command, true, {
|
|
2751
|
+
id: row.id,
|
|
2752
|
+
status: row.status,
|
|
2753
|
+
repo: `${row.owner}/${row.name}`,
|
|
2754
|
+
rejectReason: row.rejectReason,
|
|
2755
|
+
identitySource
|
|
2756
|
+
})
|
|
2757
|
+
);
|
|
2758
|
+
} else {
|
|
2759
|
+
success(
|
|
2760
|
+
[
|
|
2761
|
+
`Rejected ${row.name}`,
|
|
2762
|
+
``,
|
|
2763
|
+
` Repository: ${row.owner}/${row.name}`,
|
|
2764
|
+
...row.rejectReason ? [` Reason: ${row.rejectReason}`] : []
|
|
2765
|
+
].join("\n")
|
|
2766
|
+
);
|
|
2767
|
+
}
|
|
2768
|
+
} catch (err) {
|
|
2769
|
+
const { code, message, kind, requestId } = wrapProxyError(command, err);
|
|
2770
|
+
outputError({ json: options.json, command }, code, message, {
|
|
2771
|
+
logError: error,
|
|
2772
|
+
kind,
|
|
2773
|
+
requestId,
|
|
2774
|
+
extras: identitySource ? { identitySource } : void 0
|
|
2775
|
+
});
|
|
2776
|
+
exit(code);
|
|
2777
|
+
}
|
|
2778
|
+
}
|
|
2779
|
+
|
|
2780
|
+
// src/commands/repo/status.ts
|
|
2781
|
+
import { log as log17 } from "@clack/prompts";
|
|
2782
|
+
function humanRow(row) {
|
|
2783
|
+
const lines = [
|
|
2784
|
+
`Request ${row.id}`,
|
|
2785
|
+
``,
|
|
2786
|
+
` Repository: ${row.owner}/${row.name}`,
|
|
2787
|
+
` Visibility: ${row.visibility}`,
|
|
2788
|
+
` Status: ${row.status}`,
|
|
2789
|
+
` Requested by: ${row.requestedBy}`
|
|
2790
|
+
];
|
|
2791
|
+
if (row.template) lines.push(` Template: ${row.template}`);
|
|
2792
|
+
if (row.url) lines.push(` URL: ${row.url}`);
|
|
2793
|
+
if (row.approver) lines.push(` Approver: ${row.approver}`);
|
|
2794
|
+
if (row.rejectReason) lines.push(` Reason: ${row.rejectReason}`);
|
|
2795
|
+
if (row.error) lines.push(` Error: ${row.error}`);
|
|
2796
|
+
lines.push(` Created: ${row.createdAt}`);
|
|
2797
|
+
lines.push(` Updated: ${row.updatedAt}`);
|
|
2798
|
+
return lines.join("\n");
|
|
2799
|
+
}
|
|
2800
|
+
async function status(options, deps = {}) {
|
|
2801
|
+
const command = "repo status";
|
|
2802
|
+
const message = deps.logMessage ?? ((s) => log17.message(s));
|
|
2803
|
+
const error = deps.logError ?? ((s) => log17.error(s));
|
|
2804
|
+
const exit = deps.exit ?? exitWithCode;
|
|
2805
|
+
let identitySource;
|
|
2806
|
+
try {
|
|
2807
|
+
if (!options.id || options.id.trim().length === 0) {
|
|
2808
|
+
throw new UsageError("request id is required (positional argument)");
|
|
2809
|
+
}
|
|
2810
|
+
const setup = await setupClient2(deps);
|
|
2811
|
+
const client = setup.client;
|
|
2812
|
+
identitySource = setup.identitySource;
|
|
2813
|
+
const row = await client.getRepoRequest(options.id);
|
|
2814
|
+
if (options.json) {
|
|
2815
|
+
emitJson9(buildEnvelope(command, true, { request: row, identitySource }));
|
|
2816
|
+
} else {
|
|
2817
|
+
message(humanRow(row));
|
|
2818
|
+
}
|
|
2819
|
+
} catch (err) {
|
|
2820
|
+
const {
|
|
2821
|
+
code,
|
|
2822
|
+
message: msg,
|
|
2823
|
+
kind,
|
|
2824
|
+
requestId
|
|
2825
|
+
} = wrapProxyError(command, err);
|
|
2826
|
+
outputError({ json: options.json, command }, code, msg, {
|
|
2827
|
+
logError: error,
|
|
2828
|
+
kind,
|
|
2829
|
+
requestId,
|
|
2830
|
+
extras: identitySource ? { identitySource } : void 0
|
|
2831
|
+
});
|
|
2832
|
+
exit(code);
|
|
2833
|
+
}
|
|
2834
|
+
}
|
|
2835
|
+
|
|
2133
2836
|
// src/lib/update-notifier.ts
|
|
2134
2837
|
import { readFileSync } from "fs";
|
|
2135
2838
|
import { mkdir as mkdir2, readFile as readFile6, writeFile as writeFile2 } from "fs/promises";
|
|
@@ -2285,7 +2988,7 @@ function installExitNotice(current) {
|
|
|
2285
2988
|
}
|
|
2286
2989
|
|
|
2287
2990
|
// src/cli.ts
|
|
2288
|
-
var version = true ? "0.
|
|
2991
|
+
var version = true ? "0.8.0" : "0.0.0";
|
|
2289
2992
|
function handleActionError(command, json, err) {
|
|
2290
2993
|
const ctx = { json, command };
|
|
2291
2994
|
const message = err instanceof Error ? err.message : "unknown error";
|
|
@@ -2302,12 +3005,14 @@ function findFirstPositional(args) {
|
|
|
2302
3005
|
}
|
|
2303
3006
|
function run(argv = process.argv) {
|
|
2304
3007
|
installExitNotice(version);
|
|
2305
|
-
void refreshIfStale()
|
|
3008
|
+
void refreshIfStale().catch(() => {
|
|
3009
|
+
});
|
|
2306
3010
|
const args = argv.slice(2);
|
|
2307
3011
|
const firstPosIdx = findFirstPositional(args);
|
|
2308
3012
|
const namespace = firstPosIdx >= 0 ? args[firstPosIdx] : void 0;
|
|
2309
3013
|
const isStatic = namespace === "static";
|
|
2310
3014
|
const isSites = namespace === "sites";
|
|
3015
|
+
const isRepo = namespace === "repo";
|
|
2311
3016
|
if (isSites) {
|
|
2312
3017
|
const sitesArgs = [
|
|
2313
3018
|
...args.slice(0, firstPosIdx),
|
|
@@ -2374,6 +3079,136 @@ function run(argv = process.argv) {
|
|
|
2374
3079
|
sitesCli.parse(["node", "universe-sites", ...sitesArgs]);
|
|
2375
3080
|
return;
|
|
2376
3081
|
}
|
|
3082
|
+
if (isRepo) {
|
|
3083
|
+
const repoArgs = [
|
|
3084
|
+
...args.slice(0, firstPosIdx),
|
|
3085
|
+
...args.slice(firstPosIdx + 1)
|
|
3086
|
+
];
|
|
3087
|
+
const repoCli = cac("universe repo");
|
|
3088
|
+
repoCli.command(
|
|
3089
|
+
"create [name]",
|
|
3090
|
+
"Request a new repository under freeCodeCamp-Universe (staff only)"
|
|
3091
|
+
).option("--json", "Output as JSON").option("--visibility <vis>", "public or private (default: private)").option("--description <text>", "Repository description").option(
|
|
3092
|
+
"--template <name>",
|
|
3093
|
+
"Org template repo to generate from; omit for a blank repo"
|
|
3094
|
+
).option("--yes", "Skip confirmation prompts (required for non-TTY/CI)").example("universe repo create my-app --visibility private --yes").action(
|
|
3095
|
+
async (name, flags) => {
|
|
3096
|
+
try {
|
|
3097
|
+
await create({
|
|
3098
|
+
json: flags.json ?? false,
|
|
3099
|
+
name,
|
|
3100
|
+
visibility: flags.visibility,
|
|
3101
|
+
description: flags.description,
|
|
3102
|
+
template: flags.template,
|
|
3103
|
+
yes: flags.yes ?? false
|
|
3104
|
+
});
|
|
3105
|
+
} catch (err) {
|
|
3106
|
+
handleActionError("repo create", flags.json ?? false, err);
|
|
3107
|
+
}
|
|
3108
|
+
}
|
|
3109
|
+
);
|
|
3110
|
+
repoCli.command("ls", "List repo requests (default: pending)").option("--json", "Output as JSON").option(
|
|
3111
|
+
"--status <status>",
|
|
3112
|
+
"pending | approved | active | rejected | failed | all"
|
|
3113
|
+
).option("--mine", "Only requests you submitted").action(
|
|
3114
|
+
async (flags) => {
|
|
3115
|
+
try {
|
|
3116
|
+
await ls3({
|
|
3117
|
+
json: flags.json ?? false,
|
|
3118
|
+
status: flags.status,
|
|
3119
|
+
mine: flags.mine ?? false
|
|
3120
|
+
});
|
|
3121
|
+
} catch (err) {
|
|
3122
|
+
handleActionError("repo ls", flags.json ?? false, err);
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
);
|
|
3126
|
+
repoCli.command(
|
|
3127
|
+
"approve <id>",
|
|
3128
|
+
"Approve a pending request \u2014 creates the repo (admin only)"
|
|
3129
|
+
).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) => {
|
|
3130
|
+
try {
|
|
3131
|
+
await approve({
|
|
3132
|
+
json: flags.json ?? false,
|
|
3133
|
+
id,
|
|
3134
|
+
yes: flags.yes ?? false
|
|
3135
|
+
});
|
|
3136
|
+
} catch (err) {
|
|
3137
|
+
handleActionError("repo approve", flags.json ?? false, err);
|
|
3138
|
+
}
|
|
3139
|
+
});
|
|
3140
|
+
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(
|
|
3141
|
+
async (id, flags) => {
|
|
3142
|
+
try {
|
|
3143
|
+
await reject({
|
|
3144
|
+
json: flags.json ?? false,
|
|
3145
|
+
id,
|
|
3146
|
+
reason: flags.reason,
|
|
3147
|
+
yes: flags.yes ?? false
|
|
3148
|
+
});
|
|
3149
|
+
} catch (err) {
|
|
3150
|
+
handleActionError("repo reject", flags.json ?? false, err);
|
|
3151
|
+
}
|
|
3152
|
+
}
|
|
3153
|
+
);
|
|
3154
|
+
repoCli.command("status <id>", "Show a request's current state").option("--json", "Output as JSON").action(async (id, flags) => {
|
|
3155
|
+
try {
|
|
3156
|
+
await status({ json: flags.json ?? false, id });
|
|
3157
|
+
} catch (err) {
|
|
3158
|
+
handleActionError("repo status", flags.json ?? false, err);
|
|
3159
|
+
}
|
|
3160
|
+
});
|
|
3161
|
+
repoCli.help();
|
|
3162
|
+
repoCli.version(version);
|
|
3163
|
+
const knownRepoSubs = /* @__PURE__ */ new Set([
|
|
3164
|
+
"create",
|
|
3165
|
+
"ls",
|
|
3166
|
+
"approve",
|
|
3167
|
+
"reject",
|
|
3168
|
+
"status"
|
|
3169
|
+
]);
|
|
3170
|
+
const repoValueFlags = /* @__PURE__ */ new Set([
|
|
3171
|
+
"--visibility",
|
|
3172
|
+
"--description",
|
|
3173
|
+
"--template",
|
|
3174
|
+
"--status",
|
|
3175
|
+
"--reason"
|
|
3176
|
+
]);
|
|
3177
|
+
let repoSub;
|
|
3178
|
+
for (let i = 0; i < repoArgs.length; i += 1) {
|
|
3179
|
+
const a = repoArgs[i];
|
|
3180
|
+
if (a === void 0) continue;
|
|
3181
|
+
if (repoValueFlags.has(a)) {
|
|
3182
|
+
i += 1;
|
|
3183
|
+
continue;
|
|
3184
|
+
}
|
|
3185
|
+
if (!a.startsWith("-")) {
|
|
3186
|
+
repoSub = a;
|
|
3187
|
+
break;
|
|
3188
|
+
}
|
|
3189
|
+
}
|
|
3190
|
+
const repoJson = repoArgs.includes("--json");
|
|
3191
|
+
const repoWantsHelp = repoArgs.includes("--help") || repoArgs.includes("-h") || repoArgs.includes("--version");
|
|
3192
|
+
if (repoSub === void 0 ? !repoWantsHelp : !knownRepoSubs.has(repoSub)) {
|
|
3193
|
+
if (repoSub === void 0 && !repoJson) {
|
|
3194
|
+
repoCli.outputHelp();
|
|
3195
|
+
} else {
|
|
3196
|
+
outputError(
|
|
3197
|
+
{ json: repoJson, command: "repo" },
|
|
3198
|
+
EXIT_USAGE,
|
|
3199
|
+
repoSub === void 0 ? "missing repo subcommand \u2014 run `universe repo --help`" : `unknown repo subcommand "${repoSub}" \u2014 run \`universe repo --help\``
|
|
3200
|
+
);
|
|
3201
|
+
}
|
|
3202
|
+
exitWithCode(EXIT_USAGE);
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
try {
|
|
3206
|
+
repoCli.parse(["node", "universe-repo", ...repoArgs]);
|
|
3207
|
+
} catch (err) {
|
|
3208
|
+
handleActionError("repo", repoJson, err);
|
|
3209
|
+
}
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
2377
3212
|
if (isStatic) {
|
|
2378
3213
|
const staticArgs = [
|
|
2379
3214
|
...args.slice(0, firstPosIdx),
|
|
@@ -2457,11 +3292,30 @@ function run(argv = process.argv) {
|
|
|
2457
3292
|
});
|
|
2458
3293
|
cli.command("static <subcommand>", "Static site deployment commands");
|
|
2459
3294
|
cli.command("sites <subcommand>", "Static site registry commands");
|
|
3295
|
+
cli.command(
|
|
3296
|
+
"repo <subcommand>",
|
|
3297
|
+
"Repository creation + approval queue commands"
|
|
3298
|
+
);
|
|
2460
3299
|
cli.help();
|
|
2461
3300
|
cli.version(version);
|
|
2462
3301
|
cli.parse(argv);
|
|
2463
3302
|
}
|
|
2464
3303
|
}
|
|
2465
3304
|
|
|
3305
|
+
// src/lib/fatal.ts
|
|
3306
|
+
function formatFatal(err) {
|
|
3307
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3308
|
+
return `universe: ${redact(message)}`;
|
|
3309
|
+
}
|
|
3310
|
+
function defaultOnFatal(err) {
|
|
3311
|
+
process.stderr.write(formatFatal(err) + "\n");
|
|
3312
|
+
process.exit(EXIT_USAGE);
|
|
3313
|
+
}
|
|
3314
|
+
function installFatalHandlers(onFatal = defaultOnFatal) {
|
|
3315
|
+
process.on("unhandledRejection", onFatal);
|
|
3316
|
+
process.on("uncaughtException", onFatal);
|
|
3317
|
+
}
|
|
3318
|
+
|
|
2466
3319
|
// src/index.ts
|
|
3320
|
+
installFatalHandlers();
|
|
2467
3321
|
run();
|