@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,324 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.adminCommands = void 0;
|
|
4
|
+
const cli_api_1 = require("../cli-api");
|
|
5
|
+
const exit_1 = require("./exit");
|
|
6
|
+
const output_1 = require("./output");
|
|
7
|
+
const target_1 = require("./target");
|
|
8
|
+
// The rest of administration: reading and removing users, listing and revoking
|
|
9
|
+
// their tokens, removing an empty collection, and the vault's own settings.
|
|
10
|
+
const YES_OPTION = {
|
|
11
|
+
name: 'yes',
|
|
12
|
+
type: 'boolean',
|
|
13
|
+
summary: 'Required: confirm that this cannot be undone',
|
|
14
|
+
};
|
|
15
|
+
exports.adminCommands = [
|
|
16
|
+
{
|
|
17
|
+
path: ['user', 'view'],
|
|
18
|
+
summary: "Show one user's standing and the tokens they hold",
|
|
19
|
+
description: `Never a token, and never a token's hash: only a SHA-256 hash is stored, so there
|
|
20
|
+
is nothing to show even if it were a good idea. What comes back is the id
|
|
21
|
+
revocation takes, when the token was minted, and any scope of its own.`,
|
|
22
|
+
args: [{ name: 'username', required: true }],
|
|
23
|
+
options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
24
|
+
async run(inv) {
|
|
25
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
26
|
+
const data = await (0, cli_api_1.api)(target, 'GET', `/api/users/${encodeURIComponent(inv.args[0])}`);
|
|
27
|
+
const tokens = (data.tokens ?? []);
|
|
28
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
29
|
+
if (json.enabled) {
|
|
30
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
console.log(`${data.name} @ ${target.host}`);
|
|
34
|
+
console.log(` ${data.siteAdmin ? 'site admin' : `owns collection '${data.name}' by name`}`);
|
|
35
|
+
console.log('');
|
|
36
|
+
if (tokens.length === 0) {
|
|
37
|
+
console.log('No tokens, so this user cannot sign in or push.');
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
(0, output_1.printTable)(tokens.map((t) => [
|
|
41
|
+
String(t.id),
|
|
42
|
+
(0, output_1.shortDate)(t.created) || '(unknown date)',
|
|
43
|
+
t.scope ? `restricted to: ${t.scope.join(', ')}` : '',
|
|
44
|
+
]));
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
path: ['user', 'delete'],
|
|
49
|
+
summary: 'Remove a user and every token they hold',
|
|
50
|
+
args: [{ name: 'username', required: true }],
|
|
51
|
+
options: [YES_OPTION, output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
52
|
+
async run(inv) {
|
|
53
|
+
if (!inv.bool('yes'))
|
|
54
|
+
throw new exit_1.CliError('Removing a user cannot be undone. Pass --yes.', exit_1.EXIT_USAGE);
|
|
55
|
+
const name = inv.args[0];
|
|
56
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
57
|
+
const data = await (0, cli_api_1.api)(target, 'DELETE', `/api/users/${encodeURIComponent(name)}?confirm=${encodeURIComponent(name)}`);
|
|
58
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
59
|
+
if (json.enabled) {
|
|
60
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
console.log(`Removed ${data.deleted}`);
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
path: ['user', 'token', 'list'],
|
|
68
|
+
summary: "List a user's tokens, by the id revocation takes",
|
|
69
|
+
args: [{ name: 'username', required: true }],
|
|
70
|
+
options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
71
|
+
async run(inv) {
|
|
72
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
73
|
+
const data = await (0, cli_api_1.api)(target, 'GET', `/api/users/${encodeURIComponent(inv.args[0])}/tokens`);
|
|
74
|
+
const tokens = (data.tokens ?? []);
|
|
75
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
76
|
+
if (json.enabled) {
|
|
77
|
+
(0, output_1.printJson)({ tokens: (0, output_1.pickFields)(tokens, json.fields) });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
if (tokens.length === 0) {
|
|
81
|
+
console.log('No tokens');
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
(0, output_1.printTable)(tokens.map((t) => [
|
|
85
|
+
String(t.id),
|
|
86
|
+
(0, output_1.shortDate)(t.created) || '(unknown date)',
|
|
87
|
+
t.scope ? `restricted to: ${t.scope.join(', ')}` : '',
|
|
88
|
+
]));
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
path: ['user', 'token', 'revoke'],
|
|
93
|
+
summary: 'Revoke one token, leaving the user and their other tokens',
|
|
94
|
+
description: `Revoking the token you are using is allowed and is reported rather than refused:
|
|
95
|
+
locking yourself out is your business, and vault.json remains hand-editable.`,
|
|
96
|
+
args: [
|
|
97
|
+
{ name: 'username', required: true },
|
|
98
|
+
{ name: 'token-id', required: true },
|
|
99
|
+
],
|
|
100
|
+
options: [YES_OPTION, output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
101
|
+
async run(inv) {
|
|
102
|
+
if (!inv.bool('yes'))
|
|
103
|
+
throw new exit_1.CliError('Revoking a token cannot be undone. Pass --yes.', exit_1.EXIT_USAGE);
|
|
104
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
105
|
+
const data = await (0, cli_api_1.api)(target, 'DELETE', `/api/users/${encodeURIComponent(inv.args[0])}/tokens/${encodeURIComponent(inv.args[1])}`);
|
|
106
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
107
|
+
if (json.enabled) {
|
|
108
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
console.log(`Revoked ${data.revoked}; ${data.remaining} token${data.remaining === 1 ? '' : 's'} left.`);
|
|
112
|
+
if (data.wasThisToken)
|
|
113
|
+
console.log('That was the token this command authenticated with, so it will not work again.');
|
|
114
|
+
},
|
|
115
|
+
},
|
|
116
|
+
{
|
|
117
|
+
path: ['collection', 'rename'],
|
|
118
|
+
summary: 'Rename a collection, with every repository in it',
|
|
119
|
+
description: `Everything the collection holds moves with it: the repositories, their issues,
|
|
120
|
+
pull requests, releases, sites, run histories, and LFS objects. Requests for
|
|
121
|
+
the old address are redirected to the new one, so links and existing clones
|
|
122
|
+
keep working, until something else is created under that name. Token scopes
|
|
123
|
+
naming the old collection are the exception: they cover nothing afterwards and
|
|
124
|
+
have to be granted again under the new name.
|
|
125
|
+
|
|
126
|
+
Takes ownership of the collection; the owners travel with it. No --yes: a
|
|
127
|
+
rename is undone by renaming back.`,
|
|
128
|
+
args: [
|
|
129
|
+
{ name: 'name', required: true },
|
|
130
|
+
{ name: 'new-name', required: true },
|
|
131
|
+
],
|
|
132
|
+
options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
133
|
+
async run(inv) {
|
|
134
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
135
|
+
const data = await (0, cli_api_1.api)(target, 'POST', `/api/collections/${encodeURIComponent(inv.args[0])}/rename`, {
|
|
136
|
+
name: inv.args[1],
|
|
137
|
+
});
|
|
138
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
139
|
+
if (json.enabled) {
|
|
140
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
// A rename to the name it already has is a success with nothing done,
|
|
144
|
+
// which the API answers as changed:false rather than as an error.
|
|
145
|
+
if (data.changed === false) {
|
|
146
|
+
console.log(String(data.message ?? 'Nothing changed.'));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const repos = Number(data.repos ?? 0);
|
|
150
|
+
console.log(`Now ${data.name}, with ${repos} ${repos === 1 ? 'repository' : 'repositories'} in it`);
|
|
151
|
+
console.log('Remotes pointing at the old name, and token scopes naming it, need changing.');
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
path: ['collection', 'delete'],
|
|
156
|
+
summary: 'Remove an empty collection',
|
|
157
|
+
description: `A collection is a directory, so this is an rmdir: one holding anything at all is
|
|
158
|
+
refused rather than emptied.`,
|
|
159
|
+
args: [{ name: 'name', required: true }],
|
|
160
|
+
options: [YES_OPTION, output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
161
|
+
async run(inv) {
|
|
162
|
+
if (!inv.bool('yes'))
|
|
163
|
+
throw new exit_1.CliError('Removing a collection cannot be undone. Pass --yes.', exit_1.EXIT_USAGE);
|
|
164
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
165
|
+
const data = await (0, cli_api_1.api)(target, 'DELETE', `/api/collections/${encodeURIComponent(inv.args[0])}`);
|
|
166
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
167
|
+
if (json.enabled) {
|
|
168
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
console.log(`Removed ${data.deleted}`);
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
{
|
|
175
|
+
path: ['config', 'view'],
|
|
176
|
+
summary: "Show the vault's settings",
|
|
177
|
+
options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
178
|
+
async run(inv) {
|
|
179
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
180
|
+
const data = await (0, cli_api_1.api)(target, 'GET', '/api/config');
|
|
181
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
182
|
+
if (json.enabled) {
|
|
183
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const ci = (data.ci ?? {});
|
|
187
|
+
const sites = (data.sites ?? {});
|
|
188
|
+
const network = (data.network ?? {});
|
|
189
|
+
const limits = (data.limits ?? {});
|
|
190
|
+
(0, output_1.printTable)([
|
|
191
|
+
['theme', String(data.theme)],
|
|
192
|
+
['ci.runs', String(ci.runs)],
|
|
193
|
+
['ci.days', String(ci.days)],
|
|
194
|
+
['ci.artifactMb', String(ci.artifactMb)],
|
|
195
|
+
['sites.host', String(sites.host || '(none; sites are sandboxed on the forge host)')],
|
|
196
|
+
['network.trustProxy', String(network.trustProxy)],
|
|
197
|
+
['limits.requestsPerMinute', String(limits.requestsPerMinute)],
|
|
198
|
+
['limits.authFailures', String(limits.authFailures)],
|
|
199
|
+
[
|
|
200
|
+
'limits.egressGbPerDay',
|
|
201
|
+
Number(limits.egressGbPerDay) > 0 ? String(limits.egressGbPerDay) : '0 (no daily limit on outgoing bytes)',
|
|
202
|
+
],
|
|
203
|
+
]);
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log(`themes: ${data.themes.join(', ')}`);
|
|
206
|
+
// Saying which of these a write can reach saves a caller discovering it by
|
|
207
|
+
// being refused.
|
|
208
|
+
console.log('theme, ci, sites.host, and limits.egressGbPerDay can be set with `mochi config set`.');
|
|
209
|
+
console.log('network and the rest of limits are read once at startup: edit config.json in the vault and restart.');
|
|
210
|
+
console.log('`mochi api /api/egress` shows what has gone out today, per repository.');
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
path: ['config', 'set'],
|
|
215
|
+
summary: 'Change the theme, the sites hostname, or the CI retention settings',
|
|
216
|
+
description: `Writes config.json in the vault, so a hosted vault is configured the same way as
|
|
217
|
+
one on your desk and there is no reason to reach its disk by hand.
|
|
218
|
+
|
|
219
|
+
--sites-host gives each repository's static site an origin of its own at
|
|
220
|
+
<repo>--<collection>.<host>, in place of the sandbox they are served under on the
|
|
221
|
+
vault's own hostname. Set it only once that hostname resolves to this vault and a
|
|
222
|
+
wildcard certificate covers it, since sites stop being served anywhere else the
|
|
223
|
+
moment it is set. --sites-host '' puts them back.
|
|
224
|
+
|
|
225
|
+
--egress-gb-per-day caps the bytes the vault may send in one UTC day, over
|
|
226
|
+
everything. Once a day's total reaches it, ordinary requests are refused with a
|
|
227
|
+
503 until 00:00 UTC; administration and signing in keep working, so the cap can
|
|
228
|
+
be raised from the vault itself. 0 sends without a limit. It is read per request,
|
|
229
|
+
so a change is in force immediately.
|
|
230
|
+
|
|
231
|
+
network.trustProxy and the rest of the limits block are not here. They are read
|
|
232
|
+
once when the server starts, so a command that changed them would report a change
|
|
233
|
+
the running server had not made. Edit config.json in the vault and restart.`,
|
|
234
|
+
options: [
|
|
235
|
+
{ name: 'theme', type: 'string', value: '<t>', summary: 'Theme name' },
|
|
236
|
+
{
|
|
237
|
+
name: 'sites-host',
|
|
238
|
+
type: 'string',
|
|
239
|
+
value: '<host>',
|
|
240
|
+
summary: "Hostname whose subdomains serve sites; '' to serve them sandboxed on the forge host",
|
|
241
|
+
},
|
|
242
|
+
{ name: 'ci-runs', type: 'int', value: '<n>', summary: 'Completed runs to keep per repository' },
|
|
243
|
+
{ name: 'ci-days', type: 'int', value: '<n>', summary: 'Also drop runs older than this; 0 disables' },
|
|
244
|
+
{ name: 'ci-artifact-mb', type: 'int', value: '<n>', summary: 'Largest artifact a job may upload' },
|
|
245
|
+
{
|
|
246
|
+
name: 'egress-gb-per-day',
|
|
247
|
+
type: 'string',
|
|
248
|
+
value: '<gb>',
|
|
249
|
+
summary: 'Gigabytes the vault may send per UTC day; 0 for no limit',
|
|
250
|
+
},
|
|
251
|
+
output_1.JSON_OPTION,
|
|
252
|
+
...target_1.TARGET_OPTIONS,
|
|
253
|
+
],
|
|
254
|
+
async run(inv) {
|
|
255
|
+
const theme = inv.str('theme');
|
|
256
|
+
// An empty string is a value here rather than an absence: it is how the
|
|
257
|
+
// sites hostname is cleared, and str() returns null only when the option
|
|
258
|
+
// was not given at all.
|
|
259
|
+
const sitesHost = inv.str('sites-host');
|
|
260
|
+
const runs = inv.int('ci-runs');
|
|
261
|
+
const days = inv.int('ci-days');
|
|
262
|
+
const artifactMb = inv.int('ci-artifact-mb');
|
|
263
|
+
// Taken as a string and parsed here rather than declared as an int: half a
|
|
264
|
+
// gigabyte is a reasonable budget to give a vault, and the option parser
|
|
265
|
+
// has no float type.
|
|
266
|
+
const egressRaw = inv.str('egress-gb-per-day');
|
|
267
|
+
let egressGbPerDay = null;
|
|
268
|
+
if (egressRaw !== null) {
|
|
269
|
+
const gb = Number(egressRaw.trim());
|
|
270
|
+
if (egressRaw.trim() === '' || !Number.isFinite(gb) || gb < 0) {
|
|
271
|
+
throw new exit_1.CliError('--egress-gb-per-day takes a number of gigabytes, or 0 for no limit.', exit_1.EXIT_USAGE);
|
|
272
|
+
}
|
|
273
|
+
egressGbPerDay = gb;
|
|
274
|
+
}
|
|
275
|
+
if (theme === null &&
|
|
276
|
+
sitesHost === null &&
|
|
277
|
+
runs === null &&
|
|
278
|
+
days === null &&
|
|
279
|
+
artifactMb === null &&
|
|
280
|
+
egressGbPerDay === null) {
|
|
281
|
+
throw new exit_1.CliError('Nothing to change. Pass --theme, --sites-host, --ci-runs, --ci-days, --ci-artifact-mb, or --egress-gb-per-day.', exit_1.EXIT_USAGE);
|
|
282
|
+
}
|
|
283
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
284
|
+
// The CI block is written whole, so the fields not named have to come from
|
|
285
|
+
// what is there now rather than from a default.
|
|
286
|
+
let ci;
|
|
287
|
+
if (runs !== null || days !== null || artifactMb !== null) {
|
|
288
|
+
const current = (await (0, cli_api_1.api)(target, 'GET', '/api/config')).ci;
|
|
289
|
+
ci = {
|
|
290
|
+
runs: runs ?? current.runs,
|
|
291
|
+
days: days ?? current.days,
|
|
292
|
+
artifactMb: artifactMb ?? current.artifactMb,
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
const data = await (0, cli_api_1.api)(target, 'PATCH', '/api/config', {
|
|
296
|
+
theme: theme ?? undefined,
|
|
297
|
+
ci,
|
|
298
|
+
sites: sitesHost === null ? undefined : { host: sitesHost },
|
|
299
|
+
// The route merges this into the block it is part of, since the rest of
|
|
300
|
+
// it is not writable from here.
|
|
301
|
+
limits: egressGbPerDay === null ? undefined : { egressGbPerDay },
|
|
302
|
+
});
|
|
303
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
304
|
+
if (json.enabled) {
|
|
305
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
console.log(`theme: ${data.theme}`);
|
|
309
|
+
const saved = data.ci;
|
|
310
|
+
console.log(`ci: keep ${saved.runs} runs, ${saved.days} days, artifacts up to ${saved.artifactMb} MB`);
|
|
311
|
+
const host = String((data.sites ?? {}).host ?? '');
|
|
312
|
+
console.log(host
|
|
313
|
+
? `sites: served from <repo>--<collection>.${host}`
|
|
314
|
+
: "sites: served sandboxed on the vault's own hostname");
|
|
315
|
+
// Every reader of this setting calls loadConfig per request, so there is
|
|
316
|
+
// nothing further to do; saying so is worth a line, because the settings
|
|
317
|
+
// next to it in the same file do need a restart.
|
|
318
|
+
const gb = Number((data.limits ?? {}).egressGbPerDay ?? 0);
|
|
319
|
+
console.log(gb > 0 ? `egress: up to ${gb} GB per UTC day` : 'egress: no daily limit');
|
|
320
|
+
if (sitesHost !== null || egressGbPerDay !== null)
|
|
321
|
+
console.log('In effect now; no restart needed.');
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
];
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.apiCommand = void 0;
|
|
4
|
+
exports.normalizeApiPath = normalizeApiPath;
|
|
5
|
+
const cli_api_1 = require("../cli-api");
|
|
6
|
+
const exit_1 = require("./exit");
|
|
7
|
+
const input_1 = require("./input");
|
|
8
|
+
const target_1 = require("./target");
|
|
9
|
+
// The escape hatch. Every route the vault has is reachable the day it lands,
|
|
10
|
+
// whether or not a typed command has been written for it, which is what keeps a
|
|
11
|
+
// gap in the command set an inconvenience rather than a blocker. The shape
|
|
12
|
+
// follows `gh api` closely enough that someone who knows that one needs no
|
|
13
|
+
// second explanation.
|
|
14
|
+
const FIELD_OPTIONS = [
|
|
15
|
+
{
|
|
16
|
+
name: 'method',
|
|
17
|
+
type: 'string',
|
|
18
|
+
value: '<m>',
|
|
19
|
+
short: 'X',
|
|
20
|
+
summary: 'HTTP method; defaults to GET, or POST when a body is given',
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
name: 'field',
|
|
24
|
+
type: 'string[]',
|
|
25
|
+
value: '<k=v>',
|
|
26
|
+
summary: 'JSON body field; true, false, null, and whole numbers are coerced',
|
|
27
|
+
},
|
|
28
|
+
{ name: 'raw-field', type: 'string[]', value: '<k=v>', summary: 'JSON body field, always a string' },
|
|
29
|
+
{ name: 'input', type: 'string', value: '<f>', summary: "Send this file as the body; '-' reads stdin" },
|
|
30
|
+
{ name: 'include', type: 'boolean', short: 'i', summary: 'Print the status line and headers before the body' },
|
|
31
|
+
];
|
|
32
|
+
/**
|
|
33
|
+
* A path as typed becomes a path as sent. All four of these name the same
|
|
34
|
+
* route, because remembering which one this CLI wanted is not a good use of
|
|
35
|
+
* anyone's attention:
|
|
36
|
+
*
|
|
37
|
+
* repos/demo/proj /repos/demo/proj api/repos/demo/proj /api/repos/demo/proj
|
|
38
|
+
*/
|
|
39
|
+
function normalizeApiPath(given) {
|
|
40
|
+
let p = given.trim();
|
|
41
|
+
if (/^https?:\/\//i.test(p)) {
|
|
42
|
+
// A full URL is accepted for its path only: the host comes from the login,
|
|
43
|
+
// so that a token is never sent somewhere the caller did not configure.
|
|
44
|
+
try {
|
|
45
|
+
const u = new URL(p);
|
|
46
|
+
p = u.pathname + u.search;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
throw new exit_1.CliError(`not a URL: ${given}`, exit_1.EXIT_USAGE);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
p = p.replace(/^\/+/, '');
|
|
53
|
+
if (!p.startsWith('api/') && p !== 'api')
|
|
54
|
+
p = `api/${p}`;
|
|
55
|
+
return `/${p}`;
|
|
56
|
+
}
|
|
57
|
+
function coerce(value) {
|
|
58
|
+
if (value === 'true')
|
|
59
|
+
return true;
|
|
60
|
+
if (value === 'false')
|
|
61
|
+
return false;
|
|
62
|
+
if (value === 'null')
|
|
63
|
+
return null;
|
|
64
|
+
if (/^-?\d+$/.test(value))
|
|
65
|
+
return Number(value);
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function splitField(pair, flag) {
|
|
69
|
+
const i = pair.indexOf('=');
|
|
70
|
+
if (i <= 0)
|
|
71
|
+
throw new exit_1.CliError(`--${flag} takes key=value, not '${pair}'`, exit_1.EXIT_USAGE);
|
|
72
|
+
return [pair.slice(0, i), pair.slice(i + 1)];
|
|
73
|
+
}
|
|
74
|
+
async function bodyFor(inv) {
|
|
75
|
+
const input = inv.str('input');
|
|
76
|
+
const fields = inv.list('field');
|
|
77
|
+
const rawFields = inv.list('raw-field');
|
|
78
|
+
if (input && (fields.length || rawFields.length)) {
|
|
79
|
+
throw new exit_1.CliError('Pass either --input or --field/--raw-field, not both.', exit_1.EXIT_USAGE);
|
|
80
|
+
}
|
|
81
|
+
if (input)
|
|
82
|
+
return await (0, input_1.readFileArg)(input);
|
|
83
|
+
if (!fields.length && !rawFields.length)
|
|
84
|
+
return undefined;
|
|
85
|
+
const body = {};
|
|
86
|
+
for (const pair of fields) {
|
|
87
|
+
const [k, v] = splitField(pair, 'field');
|
|
88
|
+
body[k] = coerce(v);
|
|
89
|
+
}
|
|
90
|
+
for (const pair of rawFields) {
|
|
91
|
+
const [k, v] = splitField(pair, 'raw-field');
|
|
92
|
+
body[k] = v;
|
|
93
|
+
}
|
|
94
|
+
return JSON.stringify(body);
|
|
95
|
+
}
|
|
96
|
+
exports.apiCommand = {
|
|
97
|
+
path: ['api'],
|
|
98
|
+
summary: 'Call any route of the vault JSON API and print what it answers',
|
|
99
|
+
description: `The path may be written with or without a leading slash and with or without the
|
|
100
|
+
'api/' prefix, so 'repos/demo/proj' and '/api/repos/demo/proj' are the same
|
|
101
|
+
route. The response body is printed verbatim on stdout, whatever its type; a
|
|
102
|
+
non-2xx status prints the body on stderr instead and exits non-zero, with the
|
|
103
|
+
same exit codes every other command uses (4 for 404, 5 for 409).
|
|
104
|
+
|
|
105
|
+
mochi api repos/demo/proj/issues
|
|
106
|
+
mochi api repos/demo/proj/issues -X POST --field title=Bug --field 'body=It broke'
|
|
107
|
+
mochi api repos/demo/proj/issues/1/state -X POST --field state=closed
|
|
108
|
+
mochi api repos/demo/proj/contents/README.md --input patch.json -X PUT`,
|
|
109
|
+
args: [{ name: 'path', required: true }],
|
|
110
|
+
options: [...FIELD_OPTIONS, ...target_1.TARGET_OPTIONS],
|
|
111
|
+
async run(inv) {
|
|
112
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
113
|
+
const pathname = normalizeApiPath(inv.args[0]);
|
|
114
|
+
const body = await bodyFor(inv);
|
|
115
|
+
const method = (inv.str('method') ?? (body === undefined ? 'GET' : 'POST')).toUpperCase();
|
|
116
|
+
const r = await (0, cli_api_1.request)(target, method, pathname, { body });
|
|
117
|
+
if (inv.bool('include')) {
|
|
118
|
+
process.stderr.write(`HTTP ${r.status}\n`);
|
|
119
|
+
if (r.contentType)
|
|
120
|
+
process.stderr.write(`content-type: ${r.contentType}\n\n`);
|
|
121
|
+
}
|
|
122
|
+
if (!r.ok) {
|
|
123
|
+
process.stderr.write(r.body.endsWith('\n') || r.body === '' ? r.body : r.body + '\n');
|
|
124
|
+
process.exit((0, exit_1.exitCodeForStatus)(r.status));
|
|
125
|
+
}
|
|
126
|
+
process.stdout.write(r.body.endsWith('\n') || r.body === '' ? r.body : r.body + '\n');
|
|
127
|
+
},
|
|
128
|
+
};
|