@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
package/dist/index.js
ADDED
|
@@ -0,0 +1,752 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
4
|
+
if (k2 === undefined) k2 = k;
|
|
5
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
6
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
7
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
8
|
+
}
|
|
9
|
+
Object.defineProperty(o, k2, desc);
|
|
10
|
+
}) : (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
o[k2] = m[k];
|
|
13
|
+
}));
|
|
14
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
15
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
16
|
+
}) : function(o, v) {
|
|
17
|
+
o["default"] = v;
|
|
18
|
+
});
|
|
19
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
20
|
+
var ownKeys = function(o) {
|
|
21
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
22
|
+
var ar = [];
|
|
23
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
24
|
+
return ar;
|
|
25
|
+
};
|
|
26
|
+
return ownKeys(o);
|
|
27
|
+
};
|
|
28
|
+
return function (mod) {
|
|
29
|
+
if (mod && mod.__esModule) return mod;
|
|
30
|
+
var result = {};
|
|
31
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
32
|
+
__setModuleDefault(result, mod);
|
|
33
|
+
return result;
|
|
34
|
+
};
|
|
35
|
+
})();
|
|
36
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
37
|
+
const fs = __importStar(require("fs"));
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const credentials_1 = require("./credentials");
|
|
40
|
+
const cli_api_1 = require("./cli-api");
|
|
41
|
+
const api_cmd_1 = require("./cli/api-cmd");
|
|
42
|
+
const issue_cmd_1 = require("./cli/issue-cmd");
|
|
43
|
+
const pr_cmd_1 = require("./cli/pr-cmd");
|
|
44
|
+
const admin_cmd_1 = require("./cli/admin-cmd");
|
|
45
|
+
const backup_cmd_1 = require("./cli/backup-cmd");
|
|
46
|
+
const release_cmd_1 = require("./cli/release-cmd");
|
|
47
|
+
const repo_cmd_1 = require("./cli/repo-cmd");
|
|
48
|
+
const run_cmd_1 = require("./cli/run-cmd");
|
|
49
|
+
const exit_1 = require("./cli/exit");
|
|
50
|
+
const input_1 = require("./cli/input");
|
|
51
|
+
const output_1 = require("./cli/output");
|
|
52
|
+
const parse_1 = require("./cli/parse");
|
|
53
|
+
const target_1 = require("./cli/target");
|
|
54
|
+
const import_cli_1 = require("./import-cli");
|
|
55
|
+
const deploy_cli_1 = require("./deploy-cli");
|
|
56
|
+
const deploy_runner_cli_1 = require("./deploy-runner-cli");
|
|
57
|
+
const job_cli_1 = require("./job-cli");
|
|
58
|
+
const runner_cli_1 = require("./runner-cli");
|
|
59
|
+
const config_1 = require("./config");
|
|
60
|
+
const scan_1 = require("./scan");
|
|
61
|
+
const themes_1 = require("./themes");
|
|
62
|
+
const vault_1 = require("./vault");
|
|
63
|
+
// The CLI's commands, as a registry rather than a chain of string comparisons
|
|
64
|
+
// with one help text covering all of them. See src/cli/parse.ts for why.
|
|
65
|
+
const FOOTER = `Configuration:
|
|
66
|
+
mochi login https://vault.example.com once, then the rest need no arguments
|
|
67
|
+
|
|
68
|
+
The vault URL is kept in ~/.config/mochi/login.json and the token in git's
|
|
69
|
+
own credential store. --host and --token override either for a single command,
|
|
70
|
+
and MOCHI_HOST and MOCHI_TOKEN sit between the two, for a caller with
|
|
71
|
+
no keyring and possibly no writable home directory.
|
|
72
|
+
|
|
73
|
+
Vault layout, where <repos> is <vault>/collections/<collection>/repos:
|
|
74
|
+
<repos>/<repo>.git bare repositories (the .git suffix is optional)
|
|
75
|
+
<repos>/<repo>.site optional static site for a repo
|
|
76
|
+
<repos>/<repo>.lfs Git LFS objects, when no bucket is configured
|
|
77
|
+
<repos>/<repo>.runs workflow run history and logs
|
|
78
|
+
<vault>/vault.json users and hashed tokens (server-managed)
|
|
79
|
+
<vault>/runners.json registered runners (server-managed)
|
|
80
|
+
<vault>/config.json vault settings: theme, sites host, CI retention, limits
|
|
81
|
+
<vault>/.secret session-cookie signing key (server-managed)
|
|
82
|
+
|
|
83
|
+
A vault laid out the older way, with collections directly in <vault>, is moved
|
|
84
|
+
to this one on the first start of a server that knows it.
|
|
85
|
+
|
|
86
|
+
Backing up a hosted vault:
|
|
87
|
+
mochi backup ~/backups/myvault --snapshot incremental, over HTTP; see docs/backup.md
|
|
88
|
+
|
|
89
|
+
Themes: ${(0, themes_1.themeNames)().join(', ')} (default ${themes_1.DEFAULT_THEME}). Pick one under
|
|
90
|
+
Admin > Appearance in the web interface, or write config.json by hand.`;
|
|
91
|
+
// ---- serve ----
|
|
92
|
+
async function serveCmd(args, usage) {
|
|
93
|
+
let dir = null;
|
|
94
|
+
let port = 3000;
|
|
95
|
+
let host = '127.0.0.1';
|
|
96
|
+
for (let i = 0; i < args.length; i++) {
|
|
97
|
+
const a = args[i];
|
|
98
|
+
if (a === '-h' || a === '--help')
|
|
99
|
+
usage();
|
|
100
|
+
else if (a === '-p' || a === '--port')
|
|
101
|
+
port = parseInt(args[++i], 10);
|
|
102
|
+
else if (a === '--host')
|
|
103
|
+
host = args[++i];
|
|
104
|
+
else if (a.startsWith('-'))
|
|
105
|
+
throw new exit_1.CliError(`Unknown option: ${a}`, exit_1.EXIT_USAGE);
|
|
106
|
+
else
|
|
107
|
+
dir = a;
|
|
108
|
+
}
|
|
109
|
+
if (!Number.isInteger(port) || port <= 0 || port > 65535)
|
|
110
|
+
throw new exit_1.CliError('Invalid port', exit_1.EXIT_USAGE);
|
|
111
|
+
const vault = path.resolve(dir ?? process.env.MOCHI_VAULT ?? '.');
|
|
112
|
+
if (!fs.existsSync(vault) || !fs.statSync(vault).isDirectory()) {
|
|
113
|
+
throw new exit_1.CliError(`Vault directory does not exist: ${vault}`);
|
|
114
|
+
}
|
|
115
|
+
// A vault with no vault.json is initialized on first start. The owner token
|
|
116
|
+
// is normally minted here and printed once; MOCHI_OWNER_TOKEN lets the
|
|
117
|
+
// operator supply it instead, which is how `mochi deploy` hands a remote
|
|
118
|
+
// vault a token it already holds. A supplied token is not printed: it is
|
|
119
|
+
// already where it needs to be, and a hosted server's log is not a good
|
|
120
|
+
// place to leave a copy.
|
|
121
|
+
const boot = (0, vault_1.bootstrapVault)(vault, process.env.MOCHI_OWNER_TOKEN ?? null);
|
|
122
|
+
// Set by `mochi deploy fly`, which knows there is a TLS proxy in front but
|
|
123
|
+
// cannot write to the volume before the vault exists. It only seeds the
|
|
124
|
+
// setting; config.json remains the place it lives and can be edited by hand.
|
|
125
|
+
const seeded = process.env.MOCHI_TRUST_PROXY === '1' ? (0, config_1.seedTrustProxy)(vault) : false;
|
|
126
|
+
// Imported here rather than at the top of the file: the server pulls in express
|
|
127
|
+
// and the whole rendering stack, which is most of what starting this process
|
|
128
|
+
// costs, and no other command needs any of it. A CLI a person or an agent runs
|
|
129
|
+
// in a loop should not pay for the server it is not starting.
|
|
130
|
+
const { createApp } = await Promise.resolve().then(() => __importStar(require('./server')));
|
|
131
|
+
const app = createApp(vault);
|
|
132
|
+
app.listen(port, host, () => {
|
|
133
|
+
const url = `http://${host === '0.0.0.0' ? 'localhost' : host}:${port}`;
|
|
134
|
+
if (boot && boot.preset) {
|
|
135
|
+
console.log('');
|
|
136
|
+
console.log('Initialized a new vault (no vault.json found).');
|
|
137
|
+
console.log(`Owner '${boot.username}' was given the token from MOCHI_OWNER_TOKEN, so it is`);
|
|
138
|
+
console.log('not repeated here; only its hash is stored.');
|
|
139
|
+
console.log('');
|
|
140
|
+
}
|
|
141
|
+
else if (boot) {
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log('Initialized a new vault (no vault.json found).');
|
|
144
|
+
console.log(`Owner token for user '${boot.username}' (shown once; only its hash is stored):`);
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(` ${boot.token}`);
|
|
147
|
+
console.log('');
|
|
148
|
+
console.log('Sign in on the web with it, or manage users from anywhere:');
|
|
149
|
+
console.log(` mochi login ${url}`);
|
|
150
|
+
console.log('');
|
|
151
|
+
}
|
|
152
|
+
if (seeded)
|
|
153
|
+
console.log('Recorded network.trustProxy: true in config.json (MOCHI_TRUST_PROXY is set).');
|
|
154
|
+
console.log(`Mochi Forge serving vault ${vault}`);
|
|
155
|
+
console.log(` ${url}`);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// ---- users ----
|
|
159
|
+
// Removed rather than renamed, and kept only to say so: glob scopes on users
|
|
160
|
+
// became roles held where they apply, so a --scope that silently became an
|
|
161
|
+
// unknown option would look like a typo rather than like a change of design.
|
|
162
|
+
const REMOVED_SCOPE_OPTIONS = [
|
|
163
|
+
{ name: 'scope', type: 'string[]', hidden: true, summary: 'Removed: access is granted where it applies' },
|
|
164
|
+
{ name: 'admin', type: 'string[]', hidden: true, summary: 'Removed: see --site-admin and collection owners' },
|
|
165
|
+
];
|
|
166
|
+
function refuseScopeOptions(inv) {
|
|
167
|
+
if (inv.list('scope').length || inv.list('admin').length) {
|
|
168
|
+
throw new exit_1.CliError('--scope and --admin are gone: a user owns the collection named after them, and anything more is granted ' +
|
|
169
|
+
"where it applies. Use 'mochi collab add' for a repository, 'mochi collection owner add' for a " +
|
|
170
|
+
'collection, or --site-admin for everything.', exit_1.EXIT_USAGE);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Removed rather than renamed, and kept only to say so: a `--vault` that
|
|
174
|
+
// silently became an unknown option would look like a typo rather than like a
|
|
175
|
+
// change of design.
|
|
176
|
+
const VAULT_OPTION = {
|
|
177
|
+
name: 'vault',
|
|
178
|
+
type: 'string',
|
|
179
|
+
hidden: true,
|
|
180
|
+
summary: 'Removed: user commands talk to a running server',
|
|
181
|
+
};
|
|
182
|
+
function refuseVaultOption(inv) {
|
|
183
|
+
if (inv.str('vault') !== null) {
|
|
184
|
+
throw new exit_1.CliError('--vault is gone: user commands talk to a running server. Run `mochi login <url>` first.', exit_1.EXIT_USAGE);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
function formatStanding(user) {
|
|
188
|
+
const name = user.username ?? user.name ?? '';
|
|
189
|
+
return user.siteAdmin ? 'site admin' : `owns collection '${name}' by name`;
|
|
190
|
+
}
|
|
191
|
+
async function userAddCmd(inv) {
|
|
192
|
+
refuseVaultOption(inv);
|
|
193
|
+
refuseScopeOptions(inv);
|
|
194
|
+
const username = inv.args[0];
|
|
195
|
+
if (!(0, scan_1.isValidUserName)(username)) {
|
|
196
|
+
throw new exit_1.CliError('A valid username is required (letters, digits, dot, underscore, dash, not starting with a dot)', exit_1.EXIT_USAGE);
|
|
197
|
+
}
|
|
198
|
+
const tokenScope = inv.list('token-scope');
|
|
199
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
200
|
+
const data = await (0, cli_api_1.api)(target, 'POST', '/api/users', {
|
|
201
|
+
username,
|
|
202
|
+
siteAdmin: inv.bool('site-admin') || undefined,
|
|
203
|
+
tokenScope: tokenScope.length ? tokenScope : undefined,
|
|
204
|
+
});
|
|
205
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
206
|
+
if (json.enabled) {
|
|
207
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
console.log(data.created
|
|
211
|
+
? `Created user '${data.username}' on ${target.host}`
|
|
212
|
+
: `Minted a new token for existing user '${data.username}'`);
|
|
213
|
+
console.log(` ${formatStanding(data)}`);
|
|
214
|
+
if (tokenScope.length)
|
|
215
|
+
console.log(` this token is restricted to: ${tokenScope.join(', ')}`);
|
|
216
|
+
console.log('');
|
|
217
|
+
console.log('Token (copy it now; only its hash is stored):');
|
|
218
|
+
console.log(` ${data.token}`);
|
|
219
|
+
console.log('');
|
|
220
|
+
console.log(`Use it as the password with username '${data.username}' when git asks for credentials.`);
|
|
221
|
+
}
|
|
222
|
+
async function userGrantCmd(inv) {
|
|
223
|
+
refuseVaultOption(inv);
|
|
224
|
+
refuseScopeOptions(inv);
|
|
225
|
+
const username = inv.args[0];
|
|
226
|
+
const grant = inv.bool('site-admin');
|
|
227
|
+
const revoke = inv.bool('revoke-site-admin');
|
|
228
|
+
if (grant === revoke) {
|
|
229
|
+
throw new exit_1.CliError(`Pass exactly one of --site-admin or --revoke-site-admin. Repository and collection access is granted with ` +
|
|
230
|
+
`'mochi collab add' and 'mochi collection owner add'.`, exit_1.EXIT_USAGE);
|
|
231
|
+
}
|
|
232
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
233
|
+
const data = await (0, cli_api_1.api)(target, 'POST', `/api/users/${encodeURIComponent(username)}/grant`, {
|
|
234
|
+
siteAdmin: grant,
|
|
235
|
+
});
|
|
236
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
237
|
+
if (json.enabled) {
|
|
238
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
console.log(`${data.username}: ${data.siteAdmin ? 'now a site admin' : 'no longer a site admin'}`);
|
|
242
|
+
}
|
|
243
|
+
async function userListCmd(inv) {
|
|
244
|
+
refuseVaultOption(inv);
|
|
245
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
246
|
+
const data = await (0, cli_api_1.api)(target, 'GET', '/api/users');
|
|
247
|
+
const users = (data.users ?? []);
|
|
248
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
249
|
+
if (json.enabled) {
|
|
250
|
+
(0, output_1.printJson)({ users: (0, output_1.pickFields)(users, json.fields) });
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
if (users.length === 0) {
|
|
254
|
+
console.log(`No users on ${target.host}`);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const width = Math.max(...users.map((u) => u.name.length));
|
|
258
|
+
for (const u of users) {
|
|
259
|
+
const tokens = `${u.tokens} token${u.tokens === 1 ? '' : 's'}`;
|
|
260
|
+
console.log(`${u.name.padEnd(width)} ${tokens.padEnd(9)} ${u.siteAdmin ? 'site admin' : ''}`.trimEnd());
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async function whoamiCmd(inv) {
|
|
264
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
265
|
+
const data = await (0, cli_api_1.api)(target, 'GET', '/api/whoami');
|
|
266
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
267
|
+
if (json.enabled) {
|
|
268
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
269
|
+
return;
|
|
270
|
+
}
|
|
271
|
+
console.log(`${data.username} @ ${target.host}`);
|
|
272
|
+
console.log(` ${formatStanding(data)}`);
|
|
273
|
+
const owned = (data.ownedCollections ?? []);
|
|
274
|
+
if (owned.length)
|
|
275
|
+
console.log(` collections: ${owned.join(', ')}`);
|
|
276
|
+
if (data.tokenScope)
|
|
277
|
+
console.log(` this token is restricted to: ${data.tokenScope.join(', ')}`);
|
|
278
|
+
}
|
|
279
|
+
// ---- login and logout ----
|
|
280
|
+
// The vault being logged in to or out of: the URL given, the environment, or
|
|
281
|
+
// the one logged in to last, which is what makes `mochi logout` need no
|
|
282
|
+
// arguments.
|
|
283
|
+
function loginTarget(host) {
|
|
284
|
+
const resolved = (host ?? process.env.MOCHI_HOST ?? (0, credentials_1.loadLogin)()?.host ?? '').replace(/\/+$/, '');
|
|
285
|
+
if (!resolved)
|
|
286
|
+
throw new exit_1.CliError('Which vault? Give its URL, e.g. https://vault.example.com', exit_1.EXIT_USAGE);
|
|
287
|
+
try {
|
|
288
|
+
return { host: resolved, target: (0, credentials_1.credentialTarget)(resolved) };
|
|
289
|
+
}
|
|
290
|
+
catch (e) {
|
|
291
|
+
throw new exit_1.CliError(e instanceof Error ? e.message : String(e), exit_1.EXIT_USAGE);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
// A token is a credential and a terminal keeps scrollback, so it is read
|
|
295
|
+
// without echo. Passing --token instead would leave it in shell history, and
|
|
296
|
+
// --token-stdin hands one over with no terminal at all.
|
|
297
|
+
// Raw mode rather than readline: readline redraws its line through cursor
|
|
298
|
+
// control that bypasses any echo suppression, which erases the prompt.
|
|
299
|
+
function promptToken(prompt) {
|
|
300
|
+
return new Promise((resolve, reject) => {
|
|
301
|
+
const input = process.stdin;
|
|
302
|
+
if (!input.isTTY) {
|
|
303
|
+
reject(new Error('No token given and no terminal to ask on. Pass --token <t> or --token-stdin.'));
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
process.stdout.write(prompt);
|
|
307
|
+
input.setRawMode(true);
|
|
308
|
+
input.resume();
|
|
309
|
+
input.setEncoding('utf8');
|
|
310
|
+
let value = '';
|
|
311
|
+
const finish = (err) => {
|
|
312
|
+
input.removeListener('data', onData);
|
|
313
|
+
input.setRawMode(false);
|
|
314
|
+
input.pause();
|
|
315
|
+
process.stdout.write('\n');
|
|
316
|
+
if (err)
|
|
317
|
+
reject(err);
|
|
318
|
+
else
|
|
319
|
+
resolve(value.trim());
|
|
320
|
+
};
|
|
321
|
+
// Raw mode delivers ^C as a byte rather than as SIGINT, so cancelling has
|
|
322
|
+
// to be handled here or it would be pasted into the token.
|
|
323
|
+
const onData = (chunk) => {
|
|
324
|
+
for (const ch of chunk) {
|
|
325
|
+
if (ch === '\r' || ch === '\n' || ch === '\u0004')
|
|
326
|
+
return finish(null);
|
|
327
|
+
if (ch === '\u0003')
|
|
328
|
+
return finish(new Error('Cancelled.'));
|
|
329
|
+
if (ch === '\u007f' || ch === '\b')
|
|
330
|
+
value = value.slice(0, -1);
|
|
331
|
+
else if (ch >= ' ')
|
|
332
|
+
value += ch;
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
input.on('data', onData);
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
// login is the one command that reads a token without contacting a vault
|
|
339
|
+
// first, so it resolves --token and --token-stdin itself rather than through
|
|
340
|
+
// targetFrom.
|
|
341
|
+
async function tokenFor(inv) {
|
|
342
|
+
const flag = inv.str('token');
|
|
343
|
+
if (inv.bool('token-stdin')) {
|
|
344
|
+
if (flag)
|
|
345
|
+
throw new exit_1.CliError('Pass either --token or --token-stdin, not both.', exit_1.EXIT_USAGE);
|
|
346
|
+
const value = (await (0, input_1.readStdin)()).trim();
|
|
347
|
+
// Empty stdin is 3 and not 2: the invocation was well formed, and what is
|
|
348
|
+
// missing is the token, which is the case exit code 3 is documented to
|
|
349
|
+
// cover. A pipeline whose token source came up empty gets the same code it
|
|
350
|
+
// would get for having supplied no token at all.
|
|
351
|
+
if (!value)
|
|
352
|
+
throw new exit_1.CliError('--token-stdin was given but stdin was empty.', exit_1.EXIT_AUTH);
|
|
353
|
+
return value;
|
|
354
|
+
}
|
|
355
|
+
return flag ?? process.env.MOCHI_TOKEN?.trim() ?? null;
|
|
356
|
+
}
|
|
357
|
+
async function loginCmd(inv) {
|
|
358
|
+
const { host, target } = loginTarget(inv.args[0] ?? inv.str('host'));
|
|
359
|
+
// Settle where the token would go before asking for one: being prompted for
|
|
360
|
+
// a token and only then told there is nowhere to put it is the wrong order.
|
|
361
|
+
const chosen = inv.str('helper');
|
|
362
|
+
if (chosen)
|
|
363
|
+
await (0, credentials_1.setHelper)(target.url, chosen);
|
|
364
|
+
const helper = await (0, credentials_1.configuredHelper)(target.url);
|
|
365
|
+
if (!helper) {
|
|
366
|
+
console.error(`No credential helper is configured for ${target.url}, so git has nowhere to keep a token.`);
|
|
367
|
+
console.error('Storing one would silently do nothing, so this is refused rather than reported as success.');
|
|
368
|
+
console.error('');
|
|
369
|
+
console.error('Choose where the token should live and run login again:');
|
|
370
|
+
console.error(' mochi login --helper store a file at ~/.git-credentials, mode 0600, in plain text');
|
|
371
|
+
console.error(' mochi login --helper cache memory only, forgotten after 15 minutes');
|
|
372
|
+
console.error(' mochi login --helper libsecret the desktop keyring, on Linux');
|
|
373
|
+
console.error(' mochi login --helper osxkeychain the login keychain, on macOS');
|
|
374
|
+
console.error('');
|
|
375
|
+
console.error(`The choice is recorded for ${target.url} alone; other remotes keep whatever they use now.`);
|
|
376
|
+
process.exit(exit_1.EXIT_FAIL);
|
|
377
|
+
}
|
|
378
|
+
const given = await tokenFor(inv);
|
|
379
|
+
const token = given ?? (await promptToken(`Token for ${target.url}: `));
|
|
380
|
+
if (!token)
|
|
381
|
+
throw new exit_1.CliError('No token given.', exit_1.EXIT_USAGE);
|
|
382
|
+
// Verified before it is stored. A token that does not work is worse stored
|
|
383
|
+
// than absent: git would then fail with it instead of asking for a better one.
|
|
384
|
+
const who = await (0, cli_api_1.api)({ host, token }, 'GET', '/api/whoami');
|
|
385
|
+
const username = String(who.username ?? '');
|
|
386
|
+
if (!username)
|
|
387
|
+
throw new exit_1.CliError(`${host} did not say who this token belongs to.`);
|
|
388
|
+
await (0, credentials_1.approveCredential)(target, username, token);
|
|
389
|
+
// Read back rather than trusting the exit code: approve succeeds whether or
|
|
390
|
+
// not the helper kept anything, and a helper that is configured but not
|
|
391
|
+
// installed fails only here.
|
|
392
|
+
const stored = await (0, credentials_1.readCredential)(target);
|
|
393
|
+
if (!stored || stored.username !== username || stored.password !== token) {
|
|
394
|
+
console.error(`The credential helper '${helper}' did not keep the token for ${target.url}.`);
|
|
395
|
+
console.error(`Check that git credential-${helper} is installed and working.`);
|
|
396
|
+
process.exit(exit_1.EXIT_FAIL);
|
|
397
|
+
}
|
|
398
|
+
// Recorded only now: a login that could not keep its token is not a login,
|
|
399
|
+
// and pointing later commands at a vault they cannot reach would be worse
|
|
400
|
+
// than pointing them nowhere.
|
|
401
|
+
(0, credentials_1.saveLogin)(host);
|
|
402
|
+
console.log(`Stored the token for '${username}' at ${target.url} (helper: ${helper}).`);
|
|
403
|
+
console.log(` ${formatStanding(who)}`);
|
|
404
|
+
if (who.tokenScope)
|
|
405
|
+
console.log(` this token is restricted to: ${who.tokenScope.join(', ')}`);
|
|
406
|
+
console.log('');
|
|
407
|
+
console.log('git clone, fetch, push, and git lfs against this vault will no longer ask for a password,');
|
|
408
|
+
console.log(`and mochi commands talk to it by default (${(0, credentials_1.loginPath)()}).`);
|
|
409
|
+
console.log('Run `mochi logout` to remove it again.');
|
|
410
|
+
}
|
|
411
|
+
async function logoutCmd(inv) {
|
|
412
|
+
if (inv.str('token') || inv.str('helper')) {
|
|
413
|
+
throw new exit_1.CliError('logout takes only --host: it removes a stored credential rather than making one.', exit_1.EXIT_USAGE);
|
|
414
|
+
}
|
|
415
|
+
const { host, target } = loginTarget(inv.args[0] ?? inv.str('host'));
|
|
416
|
+
const stored = await (0, credentials_1.readCredential)(target);
|
|
417
|
+
if (!stored) {
|
|
418
|
+
(0, credentials_1.clearLogin)(host);
|
|
419
|
+
console.log(`No stored credential for ${target.url}.`);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
await (0, credentials_1.rejectCredential)(target, stored.username);
|
|
423
|
+
const after = await (0, credentials_1.readCredential)(target);
|
|
424
|
+
if (after) {
|
|
425
|
+
throw new exit_1.CliError(`The credential for '${after.username}' at ${target.url} is still there: the helper did not erase it.`);
|
|
426
|
+
}
|
|
427
|
+
(0, credentials_1.clearLogin)(host);
|
|
428
|
+
console.log(`Removed the stored credential for '${stored.username}' at ${target.url}.`);
|
|
429
|
+
}
|
|
430
|
+
// ---- the registry ----
|
|
431
|
+
/** A command whose own argument handling is left alone; it is dispatched and documented here all the same. */
|
|
432
|
+
function raw(path, summary, description, run) {
|
|
433
|
+
return {
|
|
434
|
+
path,
|
|
435
|
+
summary,
|
|
436
|
+
description: description || undefined,
|
|
437
|
+
raw: true,
|
|
438
|
+
run: (inv) => run(inv.argv, () => inv.help()),
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
const commands = [
|
|
442
|
+
raw(['serve'], 'Serve a vault over HTTP', `Serve a vault: a directory of collections containing bare git repositories.
|
|
443
|
+
The vault defaults to $MOCHI_VAULT, then the current directory. On the
|
|
444
|
+
first start with no vault.json, the server initializes one and prints an owner
|
|
445
|
+
token once.
|
|
446
|
+
|
|
447
|
+
Options:
|
|
448
|
+
-p, --port <n> port to listen on (default 3000)
|
|
449
|
+
--host <h> address to bind (default 127.0.0.1)`, serveCmd),
|
|
450
|
+
raw(['import'], 'Bring an existing repository into the vault', `Usage: mochi import <source> <collection>[/<name>] [--lfs]
|
|
451
|
+
|
|
452
|
+
Clone the source into a temporary directory, push it here, which creates it,
|
|
453
|
+
and remove the clone again. The source is an https or ssh git URL, owner/repo
|
|
454
|
+
for GitHub, or a directory on this machine; the name defaults to its last
|
|
455
|
+
segment. Nothing happens on the server, so the source is read with whatever git
|
|
456
|
+
credentials this machine already has. Branches and tags come across; --lfs
|
|
457
|
+
carries Git LFS objects too, and needs git-lfs installed.
|
|
458
|
+
|
|
459
|
+
A description is not part of a repository's git data. For a public GitHub
|
|
460
|
+
source it is read from GitHub's API afterwards and set here.
|
|
461
|
+
|
|
462
|
+
Options:
|
|
463
|
+
--lfs carry Git LFS objects too
|
|
464
|
+
--description <text> set this description instead of the source's
|
|
465
|
+
--no-description leave the description empty`, import_cli_1.importCmd),
|
|
466
|
+
raw(['collection', 'add'], 'Create an empty collection', `Pushing to a new path creates its collection on the way, so this is for the
|
|
467
|
+
other order: making the collection first and filling it afterwards.`, import_cli_1.collectionAddCmd),
|
|
468
|
+
raw(['collection', 'list'], "Show the vault's collections and how many repositories each holds", '', import_cli_1.collectionListCmd),
|
|
469
|
+
{
|
|
470
|
+
path: ['collection', 'owner', 'add'],
|
|
471
|
+
summary: 'Make a user an owner of a collection',
|
|
472
|
+
description: `Owners hold the admin role on every repository in the collection, may create
|
|
473
|
+
repositories in it, and manage the collection itself. The user the collection
|
|
474
|
+
is named after owns it by name and needs no entry.`,
|
|
475
|
+
args: [
|
|
476
|
+
{ name: 'collection', required: true },
|
|
477
|
+
{ name: 'username', required: true },
|
|
478
|
+
],
|
|
479
|
+
options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
480
|
+
async run(inv) {
|
|
481
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
482
|
+
const data = await (0, cli_api_1.api)(target, 'PUT', `/api/collections/${encodeURIComponent(inv.args[0])}/owners/${encodeURIComponent(inv.args[1])}`);
|
|
483
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
484
|
+
if (json.enabled) {
|
|
485
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
console.log(`Owners of ${data.name}: ${(data.owners ?? []).join(', ') || '(none listed)'}`);
|
|
489
|
+
},
|
|
490
|
+
},
|
|
491
|
+
{
|
|
492
|
+
path: ['collection', 'owner', 'remove'],
|
|
493
|
+
summary: 'Remove a user from the owners of a collection',
|
|
494
|
+
args: [
|
|
495
|
+
{ name: 'collection', required: true },
|
|
496
|
+
{ name: 'username', required: true },
|
|
497
|
+
],
|
|
498
|
+
options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
499
|
+
async run(inv) {
|
|
500
|
+
const target = await (0, target_1.targetFrom)(inv);
|
|
501
|
+
const data = await (0, cli_api_1.api)(target, 'DELETE', `/api/collections/${encodeURIComponent(inv.args[0])}/owners/${encodeURIComponent(inv.args[1])}`);
|
|
502
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
503
|
+
if (json.enabled) {
|
|
504
|
+
(0, output_1.printJson)((0, output_1.pickObject)(data, json.fields));
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
console.log(`Owners of ${data.name}: ${(data.owners ?? []).join(', ') || '(none listed)'}`);
|
|
508
|
+
},
|
|
509
|
+
},
|
|
510
|
+
{
|
|
511
|
+
path: ['user', 'add'],
|
|
512
|
+
summary: 'Create a user and print its token once',
|
|
513
|
+
description: `A user owns the collection named after them, the way a GitHub account owns its
|
|
514
|
+
namespace: they create repositories there and administer them. Anything more is
|
|
515
|
+
granted where it applies ('mochi collab add' on a repository, 'mochi
|
|
516
|
+
collection owner add' on a collection) or with --site-admin. Run again on an
|
|
517
|
+
existing user to mint an additional token. Only a SHA-256 hash of a token is
|
|
518
|
+
ever stored, so the token is shown once and cannot be recovered afterwards.`,
|
|
519
|
+
args: [{ name: 'username', required: true }],
|
|
520
|
+
options: [
|
|
521
|
+
{ name: 'site-admin', type: 'boolean', summary: 'Admin role everywhere, plus users, runners, and settings' },
|
|
522
|
+
{ name: 'token-scope', type: 'string[]', value: '<glob>', summary: 'Restrict this token alone to these globs' },
|
|
523
|
+
...REMOVED_SCOPE_OPTIONS,
|
|
524
|
+
VAULT_OPTION,
|
|
525
|
+
output_1.JSON_OPTION,
|
|
526
|
+
...target_1.TARGET_OPTIONS,
|
|
527
|
+
],
|
|
528
|
+
run: userAddCmd,
|
|
529
|
+
},
|
|
530
|
+
{
|
|
531
|
+
path: ['user', 'grant'],
|
|
532
|
+
summary: 'Grant or withdraw the site-admin bit',
|
|
533
|
+
description: `Per-repository access is granted on the repository ('mochi collab add') and
|
|
534
|
+
per-collection access on the collection ('mochi collection owner add');
|
|
535
|
+
this command carries only the one bit that is the vault's own.`,
|
|
536
|
+
args: [{ name: 'username', required: true }],
|
|
537
|
+
options: [
|
|
538
|
+
{ name: 'site-admin', type: 'boolean', summary: 'Make this user a site admin' },
|
|
539
|
+
{ name: 'revoke-site-admin', type: 'boolean', summary: 'Withdraw the site-admin bit' },
|
|
540
|
+
...REMOVED_SCOPE_OPTIONS,
|
|
541
|
+
VAULT_OPTION,
|
|
542
|
+
output_1.JSON_OPTION,
|
|
543
|
+
...target_1.TARGET_OPTIONS,
|
|
544
|
+
],
|
|
545
|
+
run: userGrantCmd,
|
|
546
|
+
},
|
|
547
|
+
{
|
|
548
|
+
path: ['user', 'list'],
|
|
549
|
+
summary: 'Show users, who is a site admin, and how many tokens each has',
|
|
550
|
+
options: [VAULT_OPTION, output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
551
|
+
run: userListCmd,
|
|
552
|
+
},
|
|
553
|
+
{
|
|
554
|
+
path: ['whoami'],
|
|
555
|
+
summary: 'Show the user, their standing, and the token restriction for the current token',
|
|
556
|
+
options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
|
|
557
|
+
run: whoamiCmd,
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
path: ['login'],
|
|
561
|
+
summary: 'Log in to a vault and hand the token to git',
|
|
562
|
+
description: `Ask for a token, check it, and hand it to git's credential store, so that clone,
|
|
563
|
+
fetch, push, git lfs, and every other mochi command stop asking for it. The
|
|
564
|
+
vault URL is remembered, so later commands need no arguments. The token is read
|
|
565
|
+
back after storing to confirm it was really kept.
|
|
566
|
+
|
|
567
|
+
--helper picks where it lives (store, cache, libsecret, osxkeychain) and is
|
|
568
|
+
recorded for this vault's host alone; without it, whatever git is already
|
|
569
|
+
configured to use for that host is used, and login refuses rather than storing
|
|
570
|
+
nothing when that is nothing.`,
|
|
571
|
+
args: [{ name: 'vault-url' }],
|
|
572
|
+
options: [
|
|
573
|
+
{
|
|
574
|
+
name: 'helper',
|
|
575
|
+
type: 'string',
|
|
576
|
+
value: '<name>',
|
|
577
|
+
summary: 'Where the token lives: store, cache, libsecret, osxkeychain',
|
|
578
|
+
},
|
|
579
|
+
...target_1.TARGET_OPTIONS,
|
|
580
|
+
],
|
|
581
|
+
run: loginCmd,
|
|
582
|
+
},
|
|
583
|
+
{
|
|
584
|
+
path: ['logout'],
|
|
585
|
+
summary: "Remove this vault's stored credential and forget the vault",
|
|
586
|
+
args: [{ name: 'vault-url' }],
|
|
587
|
+
options: [
|
|
588
|
+
{ name: 'helper', type: 'string', value: '<name>', hidden: true, summary: 'Not accepted by logout' },
|
|
589
|
+
...target_1.TARGET_OPTIONS,
|
|
590
|
+
],
|
|
591
|
+
run: logoutCmd,
|
|
592
|
+
},
|
|
593
|
+
raw(['deploy', 'fly'], 'Put a vault on Fly.io, or deploy an update to one', `Usage: mochi deploy fly <app> [--region <r>] [--volume <gb>] [--vm-size <s>]
|
|
594
|
+
[--vm-memory <m>] [--lfs-bucket] [--org <o>]
|
|
595
|
+
[--image <ref> | --from-source [--local-build]]
|
|
596
|
+
|
|
597
|
+
Needs flyctl installed, and fly auth login done. The app name is globally
|
|
598
|
+
unique on Fly and becomes the URL, https://<app>.fly.dev. Creating one mints
|
|
599
|
+
the owner token here and hands it to the server as a secret, then prints it once
|
|
600
|
+
the vault answers, with how to sign in on the web and how to store it for the
|
|
601
|
+
CLI and git. Nothing is kept on this machine: mochi login with that token is
|
|
602
|
+
what does that. Run it again to deploy a new version; settings not named by a
|
|
603
|
+
flag keep whatever the live app has, so a single flag changes a single thing. A
|
|
604
|
+
vault is a directory on one volume, so the app runs as exactly one machine: a
|
|
605
|
+
busier vault wants a bigger one, not more.
|
|
606
|
+
|
|
607
|
+
By default the image deployed is the published one for this CLI's own version.
|
|
608
|
+
--from-source builds it from the checkout you are running instead, which is how
|
|
609
|
+
to deploy a change before it has been released; --local-build uses this machine's
|
|
610
|
+
Docker rather than Fly's builder. --image <ref> deploys some other published tag.
|
|
611
|
+
|
|
612
|
+
See also: mochi deploy fly show <app>, mochi deploy fly destroy <app>.
|
|
613
|
+
`, deploy_cli_1.deployFlyCmd),
|
|
614
|
+
raw(['deploy', 'fly', 'runner'], 'Put a workflow runner on Fly.io, which stops when idle', `Usage: mochi deploy fly runner <app> [--allow <glob>...] [--labels <l,...>]
|
|
615
|
+
[--idle <5m>] [--region <r>] [--volume <gb>]
|
|
616
|
+
[--vm-size <s>] [--vm-memory <m>] [--org <o>]
|
|
617
|
+
[--image <ref> | --from-source [--local-build]]
|
|
618
|
+
|
|
619
|
+
Needs flyctl, and a login to the vault this runner will serve. Registers the
|
|
620
|
+
runner (named after the app unless --name says otherwise), creates the app and a
|
|
621
|
+
volume for the images jobs run in, hands the machine the vault URL and its token
|
|
622
|
+
as Fly secrets, and tells the vault where to send a wake request.
|
|
623
|
+
|
|
624
|
+
The machine stops when no job has arrived for --idle, and the vault starts it
|
|
625
|
+
again when one is queued, so a stopped machine is the resting state rather than
|
|
626
|
+
a fault. The first job after a stop waits about half a minute for the boot. What
|
|
627
|
+
it costs while stopped is the volume alone.
|
|
628
|
+
|
|
629
|
+
--allow is required the first time and says which repositories this runner may
|
|
630
|
+
take jobs for; it executes whatever their workflows contain, on this machine.
|
|
631
|
+
Run the same command again to deploy a new version.
|
|
632
|
+
|
|
633
|
+
See also: mochi deploy fly runner show <app>, destroy <app>, mochi runner list.
|
|
634
|
+
`, deploy_runner_cli_1.deployFlyRunnerCmd),
|
|
635
|
+
raw(['deploy', 'fly', 'runner', 'show'], 'What Fly has for this runner app, and which runner it serves', '', deploy_runner_cli_1.deployFlyRunnerShowCmd),
|
|
636
|
+
raw(['deploy', 'fly', 'runner', 'destroy'], 'Destroy the runner app, and offer to remove its registration', 'No undo, though a runner keeps nothing that matters. Pass --yes to skip the confirmation.', deploy_runner_cli_1.deployFlyRunnerDestroyCmd),
|
|
637
|
+
raw(['deploy', 'fly', 'show'], 'What Fly has for this app, and whether the vault answers', '', deploy_cli_1.deployShowCmd),
|
|
638
|
+
raw(['deploy', 'fly', 'destroy'], 'Destroy the app and its volume, and with them the vault', 'No undo. Pass --yes to skip the confirmation.', deploy_cli_1.deployDestroyCmd),
|
|
639
|
+
raw(['runner', 'add'], 'Register a machine that will execute workflow jobs', `Usage: mochi runner add <name> --allow <glob>... [--labels <l,...>] [--save]
|
|
640
|
+
|
|
641
|
+
Prints its token once. --allow says which repositories it may take jobs for, as
|
|
642
|
+
globs over collection/repo; you must own every collection they name (a site admin may name any). Jobs never run on
|
|
643
|
+
the vault's machine, so a vault with no runner queues its runs and waits.`, runner_cli_1.runnerAddCmd),
|
|
644
|
+
raw(['runner', 'run'], 'Take jobs and run them, one at a time, each in a Docker container', `Usage: mochi runner run [--host <url>] [--runner-token <t>] [--labels <l,...>]
|
|
645
|
+
|
|
646
|
+
Reads ~/.config/mochi/runner.json when given no arguments. Needs a working
|
|
647
|
+
docker or podman command; --engine picks one when both are present (asked
|
|
648
|
+
interactively otherwise), and --image <label>=<image> overrides which image a
|
|
649
|
+
runs-on label maps to. Actions named by uses: are fetched from github.com (--actions-url
|
|
650
|
+
changes that) and cached under ~/.cache/mochi (--cache-dir changes that),
|
|
651
|
+
keyed by the commit the ref resolves to, so a moved branch or tag is picked up
|
|
652
|
+
on the next run; --no-action-cache downloads every time. --work-dir sets where
|
|
653
|
+
job workspaces are made, --network which Docker network the container joins,
|
|
654
|
+
and MOCHI_RUNNER_TOKEN supplies the token instead of --runner-token.
|
|
655
|
+
|
|
656
|
+
--idle <5m> stops the runner when no job has arrived for that long, which is
|
|
657
|
+
what makes a runner that costs money while it is up affordable: it exits, and
|
|
658
|
+
whatever hosts it stops. Something then has to start it again, so --wake-port
|
|
659
|
+
listens for the vault's wake request and --wake-secret (or MOCHI_WAKE_SECRET)
|
|
660
|
+
is what that request must present. See mochi runner wake.`, runner_cli_1.runnerRunCmd),
|
|
661
|
+
runner_cli_1.runnerListCommand,
|
|
662
|
+
raw(['runner', 'wake'], 'Start a runner that stops when idle, or say where to reach it', `Usage: mochi runner wake <name> [--url <url> [--wake-secret <s>]] [--clear]
|
|
663
|
+
|
|
664
|
+
With no flags this sends the wake request now and reports how long the runner
|
|
665
|
+
took to answer, which is the way to test one without queuing a job. --url says
|
|
666
|
+
where to send it, generating the secret unless --wake-secret gives one; --clear
|
|
667
|
+
removes the address, after which nothing starts this runner.
|
|
668
|
+
|
|
669
|
+
The vault sends this by itself whenever a job is queued that a runner could take
|
|
670
|
+
and that runner has not been heard from, at most once a minute per runner
|
|
671
|
+
however many jobs are waiting.`, runner_cli_1.runnerWakeCmd),
|
|
672
|
+
raw(['runner', 'remove'], 'Remove a registered runner', '', runner_cli_1.runnerRemoveCmd),
|
|
673
|
+
raw(['job', 'run'], "Run a workflow run's manual jobs here, from a command minted on its run page", `Usage: mochi job run <vault-url> <token> [--job <pattern>] [--yes]
|
|
674
|
+
[--engine docker|podman] [--image <label>=<image>]
|
|
675
|
+
|
|
676
|
+
The run page of a run with 'runs-on: manual' jobs mints this command, token and
|
|
677
|
+
all (also: mochi run exec-command <n>). The token must be pasted within fifteen
|
|
678
|
+
minutes and works once: redeeming it starts a session that lives until the run
|
|
679
|
+
finishes, and a copy left in scrollback buys nothing afterwards.
|
|
680
|
+
|
|
681
|
+
Each job is shown step by step and nothing executes until you agree; --yes skips
|
|
682
|
+
the asking, and is required when there is no terminal to ask on. --job limits the
|
|
683
|
+
session to jobs matching a glob over the job key. Jobs execute in containers
|
|
684
|
+
exactly as on a registered runner: --engine picks docker or podman when both are
|
|
685
|
+
present (asked interactively otherwise), and --image, --work-dir, --network,
|
|
686
|
+
--cache-dir, --actions-url, --no-action-cache mean what they mean for
|
|
687
|
+
mochi runner run.
|
|
688
|
+
|
|
689
|
+
Exits 0 when everything it ran succeeded, 1 when something failed. Ctrl-C
|
|
690
|
+
finishes the job in hand and stops; a second Ctrl-C quits now, and the vault
|
|
691
|
+
fails the abandoned job when its lease expires.`, job_cli_1.jobRunCmd),
|
|
692
|
+
...repo_cmd_1.repoCommands,
|
|
693
|
+
...issue_cmd_1.issueCommands,
|
|
694
|
+
...pr_cmd_1.prCommands,
|
|
695
|
+
...run_cmd_1.runCommands,
|
|
696
|
+
...release_cmd_1.releaseCommands,
|
|
697
|
+
...admin_cmd_1.adminCommands,
|
|
698
|
+
...backup_cmd_1.backupCommands,
|
|
699
|
+
api_cmd_1.apiCommand,
|
|
700
|
+
{
|
|
701
|
+
path: ['commands'],
|
|
702
|
+
summary: 'List every command, its arguments, and its options',
|
|
703
|
+
description: `With --json this is the whole registry as data, which is enough to discover the
|
|
704
|
+
command set without reading any documentation.`,
|
|
705
|
+
options: [output_1.JSON_OPTION],
|
|
706
|
+
run: (inv) => {
|
|
707
|
+
const json = (0, output_1.jsonMode)(inv);
|
|
708
|
+
if (json.enabled) {
|
|
709
|
+
(0, output_1.printJson)((0, parse_1.registryJson)(cli));
|
|
710
|
+
return;
|
|
711
|
+
}
|
|
712
|
+
for (const c of cli.commands)
|
|
713
|
+
console.log(`${c.path.join(' ').padEnd(24)} ${c.summary}`);
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
];
|
|
717
|
+
const cli = {
|
|
718
|
+
name: 'mochi',
|
|
719
|
+
groups: [
|
|
720
|
+
{ name: 'repo', summary: 'Repositories: what the vault holds' },
|
|
721
|
+
{ name: 'branch', summary: 'Branches' },
|
|
722
|
+
{ name: 'tag', summary: 'Tags' },
|
|
723
|
+
{ name: 'file', summary: 'Files in a repository, at a ref' },
|
|
724
|
+
{ name: 'commit', summary: 'Commits and their patches' },
|
|
725
|
+
{ name: 'issue', summary: 'Issues' },
|
|
726
|
+
{ name: 'pr', summary: 'Pull requests' },
|
|
727
|
+
{ name: 'workflow', summary: 'Workflow files, and dispatching them by hand' },
|
|
728
|
+
{ name: 'run', summary: 'Workflow runs, their logs, and their artifacts' },
|
|
729
|
+
{ name: 'release', summary: 'Release notes attached to a tag' },
|
|
730
|
+
{ name: 'config', summary: "The vault's own settings" },
|
|
731
|
+
{ name: 'collection', summary: 'Collections: the directories a vault holds repositories in' },
|
|
732
|
+
{ name: 'user', summary: 'Users, their scopes, and their tokens' },
|
|
733
|
+
{ name: 'deploy', summary: 'Put a vault on Fly.io and manage it there' },
|
|
734
|
+
{ name: 'runner', summary: 'Machines that execute workflow jobs' },
|
|
735
|
+
{ name: 'job', summary: 'Run manual workflow jobs from a pasted command' },
|
|
736
|
+
],
|
|
737
|
+
commands,
|
|
738
|
+
footer: FOOTER,
|
|
739
|
+
};
|
|
740
|
+
async function main() {
|
|
741
|
+
await (0, parse_1.dispatch)(cli, process.argv.slice(2));
|
|
742
|
+
}
|
|
743
|
+
main().catch((e) => {
|
|
744
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
745
|
+
// A caller that asked for JSON gets JSON on failure too, so that parsing
|
|
746
|
+
// stderr is possible rather than nearly possible.
|
|
747
|
+
if ((0, exit_1.jsonErrorsWanted)())
|
|
748
|
+
process.stderr.write(JSON.stringify({ error: message }) + '\n');
|
|
749
|
+
else
|
|
750
|
+
console.error(message);
|
|
751
|
+
process.exit(e instanceof exit_1.CliError ? e.code : exit_1.EXIT_FAIL);
|
|
752
|
+
});
|