@magland/mochi 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +108 -0
- package/dist/ansi.js +174 -0
- package/dist/api/admin.js +416 -0
- package/dist/api/auth.js +166 -0
- package/dist/api/backup.js +598 -0
- package/dist/api/ci.js +336 -0
- package/dist/api/contents.js +339 -0
- package/dist/api/issues.js +165 -0
- package/dist/api/pulls.js +244 -0
- package/dist/api/releases.js +83 -0
- package/dist/api/repos.js +156 -0
- package/dist/api/write.js +518 -0
- package/dist/api.js +326 -0
- package/dist/assets.js +29 -0
- package/dist/atom.js +32 -0
- package/dist/atomic.js +171 -0
- package/dist/avatar.js +81 -0
- package/dist/browse.js +630 -0
- package/dist/build-info.json +4 -0
- package/dist/ci/actionref.js +86 -0
- package/dist/ci/api.js +829 -0
- package/dist/ci/artifacts.js +201 -0
- package/dist/ci/dispatch.js +30 -0
- package/dist/ci/engine.js +1321 -0
- package/dist/ci/expr.js +526 -0
- package/dist/ci/manual.js +199 -0
- package/dist/ci/present.js +82 -0
- package/dist/ci/protocol.js +6 -0
- package/dist/ci/runners.js +256 -0
- package/dist/ci/runs.js +208 -0
- package/dist/ci/trigger.js +28 -0
- package/dist/ci/views.js +441 -0
- package/dist/ci/wake.js +194 -0
- package/dist/ci/web.js +617 -0
- package/dist/ci/workflow.js +436 -0
- package/dist/cli/admin-cmd.js +324 -0
- package/dist/cli/api-cmd.js +128 -0
- package/dist/cli/backup-cmd.js +1500 -0
- package/dist/cli/exit.js +69 -0
- package/dist/cli/input.js +64 -0
- package/dist/cli/issue-cmd.js +243 -0
- package/dist/cli/output.js +93 -0
- package/dist/cli/parse.js +317 -0
- package/dist/cli/pr-cmd.js +289 -0
- package/dist/cli/release-cmd.js +171 -0
- package/dist/cli/repo-cmd.js +763 -0
- package/dist/cli/repo.js +101 -0
- package/dist/cli/run-cmd.js +438 -0
- package/dist/cli/target.js +54 -0
- package/dist/cli-api.js +84 -0
- package/dist/compare.js +111 -0
- package/dist/config.js +212 -0
- package/dist/credentials.js +235 -0
- package/dist/deploy-cli.js +859 -0
- package/dist/deploy-runner-cli.js +592 -0
- package/dist/diff.js +171 -0
- package/dist/discussion.js +253 -0
- package/dist/egress.js +559 -0
- package/dist/filecache.js +68 -0
- package/dist/find.js +162 -0
- package/dist/forms.js +737 -0
- package/dist/git.js +547 -0
- package/dist/githttp.js +428 -0
- package/dist/html.js +87 -0
- package/dist/icons.js +101 -0
- package/dist/import-cli.js +316 -0
- package/dist/index.js +752 -0
- package/dist/issues.js +308 -0
- package/dist/issueweb.js +447 -0
- package/dist/job-cli.js +197 -0
- package/dist/jobtoken.js +96 -0
- package/dist/languages.js +383 -0
- package/dist/layout.js +100 -0
- package/dist/lfs.js +438 -0
- package/dist/lfsstore.js +425 -0
- package/dist/limit.js +259 -0
- package/dist/logo.js +61 -0
- package/dist/markdown.js +382 -0
- package/dist/migrate.js +334 -0
- package/dist/multipart.js +90 -0
- package/dist/ops.js +869 -0
- package/dist/pagescript.js +465 -0
- package/dist/perms.js +370 -0
- package/dist/pointer.js +55 -0
- package/dist/profile.js +106 -0
- package/dist/pulls.js +320 -0
- package/dist/pullweb.js +461 -0
- package/dist/redirects.js +455 -0
- package/dist/releases.js +435 -0
- package/dist/render.js +233 -0
- package/dist/runner/actions.js +448 -0
- package/dist/runner/client.js +428 -0
- package/dist/runner/context.js +247 -0
- package/dist/runner/docker.js +197 -0
- package/dist/runner/externals.js +175 -0
- package/dist/runner/job.js +290 -0
- package/dist/runner/manual-run.js +272 -0
- package/dist/runner/overrides.js +554 -0
- package/dist/runner/steps.js +571 -0
- package/dist/runner/wake.js +84 -0
- package/dist/runner-cli.js +405 -0
- package/dist/scan.js +231 -0
- package/dist/server.js +424 -0
- package/dist/session.js +267 -0
- package/dist/site.js +259 -0
- package/dist/siteshost.js +94 -0
- package/dist/source.js +90 -0
- package/dist/style.js +1295 -0
- package/dist/themes.js +369 -0
- package/dist/vault.js +442 -0
- package/dist/version.js +88 -0
- package/dist/views.js +1007 -0
- package/dist/web.js +182 -0
- package/dist/webops.js +1402 -0
- package/package.json +71 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.commandHelp = commandHelp;
|
|
4
|
+
exports.groupHelp = groupHelp;
|
|
5
|
+
exports.rootHelp = rootHelp;
|
|
6
|
+
exports.registryJson = registryJson;
|
|
7
|
+
exports.dispatch = dispatch;
|
|
8
|
+
const exit_1 = require("./exit");
|
|
9
|
+
function usageError(message) {
|
|
10
|
+
throw new exit_1.CliError(message, exit_1.EXIT_USAGE);
|
|
11
|
+
}
|
|
12
|
+
// ---- suggestions ----
|
|
13
|
+
function editDistance(a, b) {
|
|
14
|
+
const prev = new Array(b.length + 1).fill(0).map((_, i) => i);
|
|
15
|
+
const cur = new Array(b.length + 1).fill(0);
|
|
16
|
+
for (let i = 1; i <= a.length; i++) {
|
|
17
|
+
cur[0] = i;
|
|
18
|
+
for (let j = 1; j <= b.length; j++) {
|
|
19
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
20
|
+
}
|
|
21
|
+
for (let j = 0; j <= b.length; j++)
|
|
22
|
+
prev[j] = cur[j];
|
|
23
|
+
}
|
|
24
|
+
return prev[b.length];
|
|
25
|
+
}
|
|
26
|
+
/** The closest of `among` to `word`, when something is within two edits of it. */
|
|
27
|
+
function nearest(word, among) {
|
|
28
|
+
let best = null;
|
|
29
|
+
let bestD = 3;
|
|
30
|
+
for (const c of among) {
|
|
31
|
+
const d = editDistance(word, c);
|
|
32
|
+
if (d < bestD) {
|
|
33
|
+
bestD = d;
|
|
34
|
+
best = c;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return best;
|
|
38
|
+
}
|
|
39
|
+
// ---- option lookup ----
|
|
40
|
+
function findOption(cmd, name) {
|
|
41
|
+
return (cmd.options ?? []).find((o) => o.name === name) ?? null;
|
|
42
|
+
}
|
|
43
|
+
function findShort(cmd, letter) {
|
|
44
|
+
return (cmd.options ?? []).find((o) => o.short === letter) ?? null;
|
|
45
|
+
}
|
|
46
|
+
// A field list is how a 'string?' option's value is recognised when it is not
|
|
47
|
+
// attached with '='. Requiring a comma is what keeps `repo list --json mycoll`
|
|
48
|
+
// from swallowing the collection: a single field must be written
|
|
49
|
+
// `--json=title`, which the option's help says.
|
|
50
|
+
function looksLikeFieldList(token) {
|
|
51
|
+
return /^[A-Za-z][A-Za-z0-9_]*(,[A-Za-z][A-Za-z0-9_]*)+$/.test(token);
|
|
52
|
+
}
|
|
53
|
+
function parseOptions(cmd, argv) {
|
|
54
|
+
const values = new Map();
|
|
55
|
+
const args = [];
|
|
56
|
+
const names = (cmd.options ?? []).filter((o) => !o.hidden).map((o) => o.name);
|
|
57
|
+
const take = (spec, inline, argv, i) => {
|
|
58
|
+
let value;
|
|
59
|
+
if (inline !== undefined)
|
|
60
|
+
value = inline;
|
|
61
|
+
else if (i + 1 < argv.length)
|
|
62
|
+
value = argv[++i];
|
|
63
|
+
else
|
|
64
|
+
usageError(`--${spec.name} needs a value${spec.value ? `: --${spec.name} ${spec.value}` : ''}`);
|
|
65
|
+
if (spec.type === 'string[]') {
|
|
66
|
+
const list = values.get(spec.name) ?? [];
|
|
67
|
+
list.push(value);
|
|
68
|
+
values.set(spec.name, list);
|
|
69
|
+
}
|
|
70
|
+
else if (spec.type === 'int') {
|
|
71
|
+
const n = Number(value);
|
|
72
|
+
if (!Number.isInteger(n))
|
|
73
|
+
usageError(`--${spec.name} takes a whole number, not '${value}'`);
|
|
74
|
+
values.set(spec.name, n);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
values.set(spec.name, value);
|
|
78
|
+
}
|
|
79
|
+
return i;
|
|
80
|
+
};
|
|
81
|
+
for (let i = 0; i < argv.length; i++) {
|
|
82
|
+
const a = argv[i];
|
|
83
|
+
if (a === '--') {
|
|
84
|
+
args.push(...argv.slice(i + 1));
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
if (a.startsWith('--')) {
|
|
88
|
+
const eq = a.indexOf('=');
|
|
89
|
+
const name = eq === -1 ? a.slice(2) : a.slice(2, eq);
|
|
90
|
+
const inline = eq === -1 ? undefined : a.slice(eq + 1);
|
|
91
|
+
const spec = findOption(cmd, name);
|
|
92
|
+
if (!spec) {
|
|
93
|
+
const near = nearest(name, names);
|
|
94
|
+
usageError(`unknown option --${name} for '${cmd.path.join(' ')}'` +
|
|
95
|
+
(near ? `; did you mean --${near}?` : `. Run 'mochi ${cmd.path.join(' ')} --help'.`));
|
|
96
|
+
}
|
|
97
|
+
if (spec.type === 'boolean') {
|
|
98
|
+
if (inline !== undefined)
|
|
99
|
+
usageError(`--${name} takes no value`);
|
|
100
|
+
values.set(name, true);
|
|
101
|
+
}
|
|
102
|
+
else if (spec.type === 'string?') {
|
|
103
|
+
if (inline !== undefined)
|
|
104
|
+
values.set(name, inline);
|
|
105
|
+
else if (i + 1 < argv.length && !argv[i + 1].startsWith('-') && looksLikeFieldList(argv[i + 1])) {
|
|
106
|
+
values.set(name, argv[++i]);
|
|
107
|
+
}
|
|
108
|
+
else
|
|
109
|
+
values.set(name, true);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
i = take(spec, inline, argv, i);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
// A lone '-' is a positional: several options take it to mean stdin.
|
|
117
|
+
if (a.startsWith('-') && a.length > 1) {
|
|
118
|
+
const spec = findShort(cmd, a.slice(1));
|
|
119
|
+
if (!spec)
|
|
120
|
+
usageError(`unknown option ${a} for '${cmd.path.join(' ')}'. Run 'mochi ${cmd.path.join(' ')} --help'.`);
|
|
121
|
+
if (spec.type === 'boolean')
|
|
122
|
+
values.set(spec.name, true);
|
|
123
|
+
else
|
|
124
|
+
i = take(spec, undefined, argv, i);
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
args.push(a);
|
|
128
|
+
}
|
|
129
|
+
return { values, args };
|
|
130
|
+
}
|
|
131
|
+
function checkArgs(cmd, args) {
|
|
132
|
+
const specs = cmd.args ?? [];
|
|
133
|
+
const required = specs.filter((s) => s.required).length;
|
|
134
|
+
if (args.length < required) {
|
|
135
|
+
usageError(`'mochi ${cmd.path.join(' ')}' needs ${usageLine(cmd)}`);
|
|
136
|
+
}
|
|
137
|
+
const variadic = specs.some((s) => s.variadic);
|
|
138
|
+
if (!variadic && args.length > specs.length) {
|
|
139
|
+
usageError(`unexpected argument '${args[specs.length]}' to 'mochi ${cmd.path.join(' ')}'`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function invocationFor(cli, cmd, argv) {
|
|
143
|
+
const { values, args } = cmd.raw ? { values: new Map(), args: [] } : parseOptions(cmd, argv);
|
|
144
|
+
if (!cmd.raw)
|
|
145
|
+
checkArgs(cmd, args);
|
|
146
|
+
if (values.has('json'))
|
|
147
|
+
(0, exit_1.setJsonErrors)(true);
|
|
148
|
+
return {
|
|
149
|
+
args,
|
|
150
|
+
argv,
|
|
151
|
+
help: () => {
|
|
152
|
+
process.stdout.write(commandHelp(cli, cmd));
|
|
153
|
+
process.exit(0);
|
|
154
|
+
},
|
|
155
|
+
str: (name) => {
|
|
156
|
+
const v = values.get(name);
|
|
157
|
+
return typeof v === 'string' ? v : null;
|
|
158
|
+
},
|
|
159
|
+
int: (name) => {
|
|
160
|
+
const v = values.get(name);
|
|
161
|
+
return typeof v === 'number' ? v : null;
|
|
162
|
+
},
|
|
163
|
+
bool: (name) => values.get(name) === true,
|
|
164
|
+
list: (name) => {
|
|
165
|
+
const v = values.get(name);
|
|
166
|
+
return Array.isArray(v) ? v : [];
|
|
167
|
+
},
|
|
168
|
+
optional: (name) => {
|
|
169
|
+
const v = values.get(name);
|
|
170
|
+
if (typeof v === 'string')
|
|
171
|
+
return v;
|
|
172
|
+
return v === true;
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
// ---- help ----
|
|
177
|
+
function usageLine(cmd) {
|
|
178
|
+
return (cmd.args ?? [])
|
|
179
|
+
.map((a) => (a.required ? `<${a.name}>` : `[<${a.name}>]`) + (a.variadic ? '...' : ''))
|
|
180
|
+
.join(' ');
|
|
181
|
+
}
|
|
182
|
+
function optionLine(o) {
|
|
183
|
+
const short = o.short ? `-${o.short}, ` : '';
|
|
184
|
+
const value = o.type === 'boolean' ? '' : ` ${o.value ?? '<value>'}`;
|
|
185
|
+
const repeat = o.type === 'string[]' ? '...' : '';
|
|
186
|
+
return `${short}--${o.name}${value}${repeat}`;
|
|
187
|
+
}
|
|
188
|
+
function pad(rows, indent = ' ') {
|
|
189
|
+
const width = rows.reduce((w, [left]) => Math.max(w, left.length), 0);
|
|
190
|
+
return rows.map(([left, right]) => `${indent}${left.padEnd(width)} ${right}\n`).join('');
|
|
191
|
+
}
|
|
192
|
+
function commandHelp(cli, cmd) {
|
|
193
|
+
const args = usageLine(cmd);
|
|
194
|
+
let out = `Usage: ${cli.name} ${cmd.path.join(' ')}${args ? ' ' + args : ''}${cmd.options?.length ? ' [options]' : ''}\n`;
|
|
195
|
+
out += `\n${cmd.summary}\n`;
|
|
196
|
+
if (cmd.description)
|
|
197
|
+
out += `\n${cmd.description.trim()}\n`;
|
|
198
|
+
const shown = (cmd.options ?? []).filter((o) => !o.hidden);
|
|
199
|
+
if (shown.length) {
|
|
200
|
+
out += '\nOptions:\n';
|
|
201
|
+
out += pad(shown.map((o) => [optionLine(o), o.summary]));
|
|
202
|
+
}
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
function groupHelp(cli, group) {
|
|
206
|
+
const commands = cli.commands.filter((c) => c.path.length > 1 && c.path[0] === group);
|
|
207
|
+
const summary = cli.groups.find((g) => g.name === group)?.summary ?? '';
|
|
208
|
+
let out = `Usage: ${cli.name} ${group} <command>\n`;
|
|
209
|
+
if (summary)
|
|
210
|
+
out += `\n${summary}\n`;
|
|
211
|
+
out += '\nCommands:\n';
|
|
212
|
+
out += pad(commands.map((c) => [c.path.slice(1).join(' '), c.summary]));
|
|
213
|
+
out += `\nRun '${cli.name} ${group} <command> --help' for one command's options.\n`;
|
|
214
|
+
return out;
|
|
215
|
+
}
|
|
216
|
+
function rootHelp(cli) {
|
|
217
|
+
let out = `Usage: ${cli.name} <command> [options]\n`;
|
|
218
|
+
const groups = cli.groups.filter((g) => cli.commands.some((c) => c.path.length > 1 && c.path[0] === g.name));
|
|
219
|
+
const singles = cli.commands.filter((c) => c.path.length === 1);
|
|
220
|
+
if (singles.length) {
|
|
221
|
+
out += '\nCommands:\n';
|
|
222
|
+
out += pad(singles.map((c) => [c.path[0], c.summary]));
|
|
223
|
+
}
|
|
224
|
+
if (groups.length) {
|
|
225
|
+
out += '\nCommand groups:\n';
|
|
226
|
+
out += pad(groups.map((g) => [g.name, g.summary]));
|
|
227
|
+
}
|
|
228
|
+
out += `\nRun '${cli.name} <group> --help' for a group's commands, or '${cli.name} <command> --help'\n`;
|
|
229
|
+
out += `for one command's options. '${cli.name} commands --json' dumps the whole command set.\n`;
|
|
230
|
+
if (cli.footer)
|
|
231
|
+
out += `\n${cli.footer.trim()}\n`;
|
|
232
|
+
return out;
|
|
233
|
+
}
|
|
234
|
+
/** The registry as data, for a caller that would rather not read help text. */
|
|
235
|
+
function registryJson(cli) {
|
|
236
|
+
return {
|
|
237
|
+
name: cli.name,
|
|
238
|
+
commands: cli.commands.map((c) => ({
|
|
239
|
+
path: c.path,
|
|
240
|
+
summary: c.summary,
|
|
241
|
+
args: (c.args ?? []).map((a) => ({ name: a.name, required: !!a.required, variadic: !!a.variadic })),
|
|
242
|
+
options: (c.options ?? [])
|
|
243
|
+
.filter((o) => !o.hidden)
|
|
244
|
+
.map((o) => ({ name: o.name, type: o.type, summary: o.summary, short: o.short ?? null })),
|
|
245
|
+
raw: !!c.raw,
|
|
246
|
+
})),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
// ---- dispatch ----
|
|
250
|
+
const HELP_FLAGS = new Set(['-h', '--help', 'help']);
|
|
251
|
+
function wantsHelp(argv) {
|
|
252
|
+
for (const a of argv) {
|
|
253
|
+
if (a === '--')
|
|
254
|
+
return false;
|
|
255
|
+
if (a === '-h' || a === '--help')
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Resolve argv against the registry and run what it names. Every failure is a
|
|
262
|
+
* CliError, so the caller decides how to report it; nothing here writes to
|
|
263
|
+
* stderr or exits, apart from the help paths.
|
|
264
|
+
*/
|
|
265
|
+
async function dispatch(cli, argv) {
|
|
266
|
+
if (argv.length === 0 || (argv.length === 1 && HELP_FLAGS.has(argv[0]))) {
|
|
267
|
+
process.stdout.write(rootHelp(cli));
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
// The longest matching path wins, so a two-word command is found before the
|
|
271
|
+
// one-word command that shares its first word could shadow it.
|
|
272
|
+
let found = null;
|
|
273
|
+
let depth = 0;
|
|
274
|
+
for (const cmd of cli.commands) {
|
|
275
|
+
if (cmd.path.length > argv.length)
|
|
276
|
+
continue;
|
|
277
|
+
if (cmd.path.every((seg, i) => argv[i] === seg) && cmd.path.length > depth) {
|
|
278
|
+
found = cmd;
|
|
279
|
+
depth = cmd.path.length;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
if (!found) {
|
|
283
|
+
const head = argv[0];
|
|
284
|
+
if (cli.groups.some((g) => g.name === head) || cli.commands.some((c) => c.path[0] === head)) {
|
|
285
|
+
// A known group with an unknown or missing subcommand. Listed by their
|
|
286
|
+
// whole path below the group, since a group may nest ('deploy fly show'),
|
|
287
|
+
// and deduplicated: several such commands share one second word.
|
|
288
|
+
const under = cli.commands.filter((c) => c.path[0] === head && c.path.length > 1);
|
|
289
|
+
const subs = [...new Set(under.map((c) => c.path.slice(1).join(' ')))];
|
|
290
|
+
const sub = argv[1];
|
|
291
|
+
if (sub === undefined || HELP_FLAGS.has(sub)) {
|
|
292
|
+
process.stdout.write(groupHelp(cli, head));
|
|
293
|
+
if (sub === undefined)
|
|
294
|
+
throw new exit_1.CliError(`'mochi ${head}' needs a command: ${subs.join(', ')}`, exit_1.EXIT_USAGE);
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
// A group may nest twice ('user token list'), so a second word that names
|
|
298
|
+
// one of those is a group in its own right rather than a misspelling.
|
|
299
|
+
const deeper = under.filter((c) => c.path.length > 2 && c.path[1] === sub);
|
|
300
|
+
if (deeper.length && argv[2] === undefined) {
|
|
301
|
+
throw new exit_1.CliError(`'mochi ${head} ${sub}' needs a command: ${[...new Set(deeper.map((c) => c.path[2]))].join(', ')}`, exit_1.EXIT_USAGE);
|
|
302
|
+
}
|
|
303
|
+
// What was typed is one word, so it is compared against single words.
|
|
304
|
+
const near = nearest(sub, [...new Set(under.map((c) => c.path[1]))]);
|
|
305
|
+
throw new exit_1.CliError(`unknown command 'mochi ${head} ${sub}'` + (near ? `; did you mean '${head} ${near}'?` : `. One of: ${subs.join(', ')}`), exit_1.EXIT_USAGE);
|
|
306
|
+
}
|
|
307
|
+
const tops = [...new Set([...cli.groups.map((g) => g.name), ...cli.commands.map((c) => c.path[0])])];
|
|
308
|
+
const near = nearest(head, tops);
|
|
309
|
+
throw new exit_1.CliError(`unknown command '${head}'` + (near ? `; did you mean '${near}'?` : ". Run 'mochi --help'."), exit_1.EXIT_USAGE);
|
|
310
|
+
}
|
|
311
|
+
const rest = argv.slice(depth);
|
|
312
|
+
if (wantsHelp(rest)) {
|
|
313
|
+
process.stdout.write(commandHelp(cli, found));
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
await found.run(invocationFor(cli, found, rest));
|
|
317
|
+
}
|
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.prCommands = void 0;
|
|
4
|
+
const cli_api_1 = require("../cli-api");
|
|
5
|
+
const exit_1 = require("./exit");
|
|
6
|
+
const input_1 = require("./input");
|
|
7
|
+
const output_1 = require("./output");
|
|
8
|
+
const repo_1 = require("./repo");
|
|
9
|
+
const target_1 = require("./target");
|
|
10
|
+
// The pull request commands. `pr merge` and `pr checks` are the two with
|
|
11
|
+
// something to say beyond the shape of the API underneath.
|
|
12
|
+
const COMMON = [repo_1.REPO_OPTION, ...target_1.TARGET_OPTIONS];
|
|
13
|
+
const BODY_OPTIONS = [
|
|
14
|
+
{ name: 'body', type: 'string', value: '<b>', summary: 'Body text' },
|
|
15
|
+
{ name: 'body-file', type: 'string', value: '<f>', summary: "Read the body from a file, or '-' for stdin" },
|
|
16
|
+
];
|
|
17
|
+
async function bodyOf(inv, required = false) {
|
|
18
|
+
const inline = inv.str('body');
|
|
19
|
+
const file = inv.str('body-file');
|
|
20
|
+
if (inline !== null && file !== null)
|
|
21
|
+
throw new exit_1.CliError('Pass either --body or --body-file, not both.', exit_1.EXIT_USAGE);
|
|
22
|
+
if (file !== null)
|
|
23
|
+
return await (0, input_1.readFileArg)(file);
|
|
24
|
+
if (inline !== null)
|
|
25
|
+
return inline;
|
|
26
|
+
if (required)
|
|
27
|
+
throw new exit_1.CliError('A body is required: pass --body <text> or --body-file <path>.', exit_1.EXIT_USAGE);
|
|
28
|
+
return undefined;
|
|
29
|
+
}
|
|
30
|
+
function pullRef(inv) {
|
|
31
|
+
const raw = inv.args[0] ?? '';
|
|
32
|
+
if (!/^[1-9][0-9]*$/.test(raw))
|
|
33
|
+
throw new exit_1.CliError(`'${raw}' is not a pull request number`, exit_1.EXIT_USAGE);
|
|
34
|
+
return parseInt(raw, 10);
|
|
35
|
+
}
|
|
36
|
+
exports.prCommands = [
|
|
37
|
+
{
|
|
38
|
+
path: ['pr', 'list'],
|
|
39
|
+
summary: 'List pull requests',
|
|
40
|
+
options: [
|
|
41
|
+
{ name: 'state', type: 'string', value: '<s>', summary: 'open (default), closed, merged, or all' },
|
|
42
|
+
{ name: 'limit', type: 'int', value: '<n>', summary: 'At most this many' },
|
|
43
|
+
output_1.JSON_OPTION,
|
|
44
|
+
...COMMON,
|
|
45
|
+
],
|
|
46
|
+
async run(inv) {
|
|
47
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
48
|
+
const ref = await (0, repo_1.resolveRepo)(inv, target);
|
|
49
|
+
const query = new URLSearchParams();
|
|
50
|
+
const state = inv.str('state');
|
|
51
|
+
if (state !== null)
|
|
52
|
+
query.set('state', state);
|
|
53
|
+
const limit = inv.int('limit');
|
|
54
|
+
if (limit !== null)
|
|
55
|
+
query.set('limit', String(limit));
|
|
56
|
+
const data = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(ref)}/pulls?${query.toString()}`);
|
|
57
|
+
const pulls = (data.pulls ?? []);
|
|
58
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
59
|
+
if (json.enabled) {
|
|
60
|
+
(0, output_1.printJson)({ pulls: (0, output_1.pickFields)(pulls, json.fields) });
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (pulls.length === 0) {
|
|
64
|
+
console.log(`No pull requests in ${ref.collection}/${ref.repo}`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
(0, output_1.printTable)(pulls.map((p) => [
|
|
68
|
+
`#${p.number}`,
|
|
69
|
+
String(p.state),
|
|
70
|
+
String(p.title),
|
|
71
|
+
`${p.head} -> ${p.base}`,
|
|
72
|
+
String(p.author),
|
|
73
|
+
(0, output_1.shortDate)(p.updated),
|
|
74
|
+
]));
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
path: ['pr', 'view'],
|
|
79
|
+
summary: 'Show one pull request, with its comments',
|
|
80
|
+
args: [{ name: 'number', required: true }],
|
|
81
|
+
options: [
|
|
82
|
+
{ name: 'comments', type: 'boolean', summary: 'Print the comments in full (implied by --json)' },
|
|
83
|
+
output_1.JSON_OPTION,
|
|
84
|
+
...COMMON,
|
|
85
|
+
],
|
|
86
|
+
async run(inv) {
|
|
87
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
88
|
+
const ref = await (0, repo_1.resolveRepo)(inv, target);
|
|
89
|
+
const data = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(ref)}/pulls/${pullRef(inv)}`);
|
|
90
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
91
|
+
if (json.enabled) {
|
|
92
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
console.log(`#${data.number} ${data.title}`);
|
|
96
|
+
console.log(`${data.state} - ${data.head} into ${data.base}, opened by ${data.author} on ${(0, output_1.shortDate)(data.created)}`);
|
|
97
|
+
if (data.mergeSha)
|
|
98
|
+
console.log(`merged by ${data.mergedBy} as ${String(data.mergeSha).slice(0, 10)}`);
|
|
99
|
+
console.log('');
|
|
100
|
+
console.log(String(data.body ?? '').trimEnd());
|
|
101
|
+
const comments = (data.commentList ?? []);
|
|
102
|
+
if (comments.length && inv.bool('comments')) {
|
|
103
|
+
for (const c of comments) {
|
|
104
|
+
console.log('');
|
|
105
|
+
console.log(`--- ${c.author} on ${(0, output_1.shortDate)(c.created)}`);
|
|
106
|
+
console.log(String(c.body ?? '').trimEnd());
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
else if (comments.length) {
|
|
110
|
+
console.log('');
|
|
111
|
+
console.log(`(${comments.length} comment${comments.length === 1 ? '' : 's'}; --comments shows them)`);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
path: ['pr', 'create'],
|
|
117
|
+
summary: 'Open a pull request',
|
|
118
|
+
options: [
|
|
119
|
+
{ name: 'base', type: 'string', value: '<b>', summary: 'Branch to merge into' },
|
|
120
|
+
{ name: 'head', type: 'string', value: '<h>', summary: 'Branch to merge from' },
|
|
121
|
+
{ name: 'title', type: 'string', value: '<t>', summary: 'Title' },
|
|
122
|
+
...BODY_OPTIONS,
|
|
123
|
+
output_1.JSON_OPTION,
|
|
124
|
+
...COMMON,
|
|
125
|
+
],
|
|
126
|
+
async run(inv) {
|
|
127
|
+
const base = inv.str('base');
|
|
128
|
+
const head = inv.str('head');
|
|
129
|
+
const title = inv.str('title');
|
|
130
|
+
if (base === null || head === null || title === null) {
|
|
131
|
+
throw new exit_1.CliError('--base, --head, and --title are all required', exit_1.EXIT_USAGE);
|
|
132
|
+
}
|
|
133
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
134
|
+
const ref = await (0, repo_1.resolveRepo)(inv, target);
|
|
135
|
+
const data = await (0, cli_api_1.api)(target, 'POST', `${(0, repo_1.repoPath)(ref)}/pulls`, {
|
|
136
|
+
base,
|
|
137
|
+
head,
|
|
138
|
+
title,
|
|
139
|
+
body: (await bodyOf(inv)) ?? '',
|
|
140
|
+
});
|
|
141
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
142
|
+
if (json.enabled) {
|
|
143
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
console.log(`${target.host}/${ref.collection}/${ref.repo}/pulls/${data.number}`);
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
path: ['pr', 'diff'],
|
|
151
|
+
summary: 'Print the diff a pull request proposes',
|
|
152
|
+
args: [{ name: 'number', required: true }],
|
|
153
|
+
options: [output_1.JSON_OPTION, ...COMMON],
|
|
154
|
+
async run(inv) {
|
|
155
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
156
|
+
const ref = await (0, repo_1.resolveRepo)(inv, target);
|
|
157
|
+
const data = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(ref)}/pulls/${pullRef(inv)}/diff`);
|
|
158
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
159
|
+
if (json.enabled) {
|
|
160
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
process.stdout.write(String(data.patch ?? ''));
|
|
164
|
+
},
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
path: ['pr', 'comment'],
|
|
168
|
+
summary: 'Add a comment to a pull request',
|
|
169
|
+
args: [{ name: 'number', required: true }],
|
|
170
|
+
options: [...BODY_OPTIONS, output_1.JSON_OPTION, ...COMMON],
|
|
171
|
+
async run(inv) {
|
|
172
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
173
|
+
const ref = await (0, repo_1.resolveRepo)(inv, target);
|
|
174
|
+
const n = pullRef(inv);
|
|
175
|
+
const data = await (0, cli_api_1.api)(target, 'POST', `${(0, repo_1.repoPath)(ref)}/pulls/${n}/comments`, { body: await bodyOf(inv, true) });
|
|
176
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
177
|
+
if (json.enabled) {
|
|
178
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
console.log(`Commented on #${n}`);
|
|
182
|
+
},
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
path: ['pr', 'merge'],
|
|
186
|
+
summary: 'Merge a pull request',
|
|
187
|
+
description: `Without --squash the merge keeps both parents, which is the record of where the
|
|
188
|
+
work came from. A conflict is reported with the conflicting paths and exits 5, so
|
|
189
|
+
a caller can tell "it does not apply" from "it went wrong".
|
|
190
|
+
|
|
191
|
+
Whether it would apply can be asked without merging anything, which is the read
|
|
192
|
+
worth making first:
|
|
193
|
+
|
|
194
|
+
mochi api repos/<collection>/<repo>/pulls/<n>/merge`,
|
|
195
|
+
args: [{ name: 'number', required: true }],
|
|
196
|
+
options: [
|
|
197
|
+
{ name: 'squash', type: 'boolean', summary: 'One commit on the base rather than a merge commit' },
|
|
198
|
+
{ name: 'delete-branch', type: 'boolean', summary: 'Delete the head branch afterwards' },
|
|
199
|
+
{ name: 'message', type: 'string', value: '<m>', summary: 'Commit message; a default is built if omitted' },
|
|
200
|
+
output_1.JSON_OPTION,
|
|
201
|
+
...COMMON,
|
|
202
|
+
],
|
|
203
|
+
async run(inv) {
|
|
204
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
205
|
+
const ref = await (0, repo_1.resolveRepo)(inv, target);
|
|
206
|
+
const n = pullRef(inv);
|
|
207
|
+
const path = `${(0, repo_1.repoPath)(ref)}/pulls/${n}/merge`;
|
|
208
|
+
const r = await (0, cli_api_1.apiTry)(target, 'POST', path, {
|
|
209
|
+
method: inv.bool('squash') ? 'squash' : 'merge',
|
|
210
|
+
message: inv.str('message') ?? undefined,
|
|
211
|
+
deleteBranch: inv.bool('delete-branch'),
|
|
212
|
+
});
|
|
213
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
214
|
+
if (!r.ok) {
|
|
215
|
+
// The conflicting paths are the useful part of a refusal, so they are
|
|
216
|
+
// printed rather than swallowed by a one-line error.
|
|
217
|
+
if (json.enabled) {
|
|
218
|
+
process.stderr.write(JSON.stringify(r.data) + '\n');
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
console.error(String(r.data.error ?? `HTTP ${r.status}`));
|
|
222
|
+
for (const p of (r.data.conflicts ?? []))
|
|
223
|
+
console.error(` conflict: ${p}`);
|
|
224
|
+
}
|
|
225
|
+
process.exit(Array.isArray(r.data.conflicts) ? exit_1.EXIT_CONFLICT : (0, exit_1.exitCodeForStatus)(r.status));
|
|
226
|
+
}
|
|
227
|
+
if (json.enabled) {
|
|
228
|
+
(0, output_1.printJson)((0, output_1.pickObject)(r.data, json.fields));
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
console.log(`Merged #${n} as ${String(r.data.sha).slice(0, 10)}`);
|
|
232
|
+
if (r.data.branchDeleted)
|
|
233
|
+
console.log('and deleted the head branch');
|
|
234
|
+
},
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
path: ['pr', 'checks'],
|
|
238
|
+
summary: 'Workflow runs whose commit is this pull request head',
|
|
239
|
+
description: `mochi has no equivalent of a check suite or a commit status, so there is
|
|
240
|
+
nothing behind this but the runs whose sha matches the pull request's head
|
|
241
|
+
commit. That is what this reports, and it is worth knowing that is what it means.`,
|
|
242
|
+
args: [{ name: 'number', required: true }],
|
|
243
|
+
options: [output_1.JSON_OPTION, ...COMMON],
|
|
244
|
+
async run(inv) {
|
|
245
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
246
|
+
const ref = await (0, repo_1.resolveRepo)(inv, target);
|
|
247
|
+
const n = pullRef(inv);
|
|
248
|
+
const commits = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(ref)}/pulls/${n}/commits`);
|
|
249
|
+
const head = (commits.commits ?? [])[0];
|
|
250
|
+
const sha = head ? String(head.sha) : null;
|
|
251
|
+
const runs = await (0, cli_api_1.api)(target, 'GET', `${(0, repo_1.repoPath)(ref)}/runs?limit=100`);
|
|
252
|
+
const matching = (runs.runs ?? []).filter((r) => sha !== null && r.sha === sha);
|
|
253
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
254
|
+
if (json.enabled) {
|
|
255
|
+
(0, output_1.printJson)({ sha, runs: (0, output_1.pickFields)(matching, json.fields) });
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
if (!sha) {
|
|
259
|
+
console.log('This pull request has no commits, so there is nothing to have run.');
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (matching.length === 0) {
|
|
263
|
+
console.log(`No workflow runs for ${sha.slice(0, 10)}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
(0, output_1.printTable)(matching.map((r) => [`#${r.number}`, String(r.status), String(r.conclusion ?? ''), String(r.workflowName ?? r.workflow ?? '')]));
|
|
267
|
+
},
|
|
268
|
+
},
|
|
269
|
+
...['close', 'reopen'].map((verb) => ({
|
|
270
|
+
path: ['pr', verb],
|
|
271
|
+
summary: verb === 'close' ? 'Close a pull request without merging it' : 'Reopen a closed pull request',
|
|
272
|
+
args: [{ name: 'number', required: true }],
|
|
273
|
+
options: [output_1.JSON_OPTION, ...COMMON],
|
|
274
|
+
async run(inv) {
|
|
275
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
276
|
+
const ref = await (0, repo_1.resolveRepo)(inv, target);
|
|
277
|
+
const n = pullRef(inv);
|
|
278
|
+
const data = await (0, cli_api_1.api)(target, 'POST', `${(0, repo_1.repoPath)(ref)}/pulls/${n}/state`, {
|
|
279
|
+
state: verb === 'close' ? 'closed' : 'open',
|
|
280
|
+
});
|
|
281
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
282
|
+
if (json.enabled) {
|
|
283
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
console.log(`#${n} is now ${data.state}`);
|
|
287
|
+
},
|
|
288
|
+
})),
|
|
289
|
+
];
|