@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.
Files changed (116) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +108 -0
  3. package/dist/ansi.js +174 -0
  4. package/dist/api/admin.js +416 -0
  5. package/dist/api/auth.js +166 -0
  6. package/dist/api/backup.js +598 -0
  7. package/dist/api/ci.js +336 -0
  8. package/dist/api/contents.js +339 -0
  9. package/dist/api/issues.js +165 -0
  10. package/dist/api/pulls.js +244 -0
  11. package/dist/api/releases.js +83 -0
  12. package/dist/api/repos.js +156 -0
  13. package/dist/api/write.js +518 -0
  14. package/dist/api.js +326 -0
  15. package/dist/assets.js +29 -0
  16. package/dist/atom.js +32 -0
  17. package/dist/atomic.js +171 -0
  18. package/dist/avatar.js +81 -0
  19. package/dist/browse.js +630 -0
  20. package/dist/build-info.json +4 -0
  21. package/dist/ci/actionref.js +86 -0
  22. package/dist/ci/api.js +829 -0
  23. package/dist/ci/artifacts.js +201 -0
  24. package/dist/ci/dispatch.js +30 -0
  25. package/dist/ci/engine.js +1321 -0
  26. package/dist/ci/expr.js +526 -0
  27. package/dist/ci/manual.js +199 -0
  28. package/dist/ci/present.js +82 -0
  29. package/dist/ci/protocol.js +6 -0
  30. package/dist/ci/runners.js +256 -0
  31. package/dist/ci/runs.js +208 -0
  32. package/dist/ci/trigger.js +28 -0
  33. package/dist/ci/views.js +441 -0
  34. package/dist/ci/wake.js +194 -0
  35. package/dist/ci/web.js +617 -0
  36. package/dist/ci/workflow.js +436 -0
  37. package/dist/cli/admin-cmd.js +324 -0
  38. package/dist/cli/api-cmd.js +128 -0
  39. package/dist/cli/backup-cmd.js +1500 -0
  40. package/dist/cli/exit.js +69 -0
  41. package/dist/cli/input.js +64 -0
  42. package/dist/cli/issue-cmd.js +243 -0
  43. package/dist/cli/output.js +93 -0
  44. package/dist/cli/parse.js +317 -0
  45. package/dist/cli/pr-cmd.js +289 -0
  46. package/dist/cli/release-cmd.js +171 -0
  47. package/dist/cli/repo-cmd.js +763 -0
  48. package/dist/cli/repo.js +101 -0
  49. package/dist/cli/run-cmd.js +438 -0
  50. package/dist/cli/target.js +54 -0
  51. package/dist/cli-api.js +84 -0
  52. package/dist/compare.js +111 -0
  53. package/dist/config.js +212 -0
  54. package/dist/credentials.js +235 -0
  55. package/dist/deploy-cli.js +859 -0
  56. package/dist/deploy-runner-cli.js +592 -0
  57. package/dist/diff.js +171 -0
  58. package/dist/discussion.js +253 -0
  59. package/dist/egress.js +559 -0
  60. package/dist/filecache.js +68 -0
  61. package/dist/find.js +162 -0
  62. package/dist/forms.js +737 -0
  63. package/dist/git.js +547 -0
  64. package/dist/githttp.js +428 -0
  65. package/dist/html.js +87 -0
  66. package/dist/icons.js +101 -0
  67. package/dist/import-cli.js +316 -0
  68. package/dist/index.js +752 -0
  69. package/dist/issues.js +308 -0
  70. package/dist/issueweb.js +447 -0
  71. package/dist/job-cli.js +197 -0
  72. package/dist/jobtoken.js +96 -0
  73. package/dist/languages.js +383 -0
  74. package/dist/layout.js +100 -0
  75. package/dist/lfs.js +438 -0
  76. package/dist/lfsstore.js +425 -0
  77. package/dist/limit.js +259 -0
  78. package/dist/logo.js +61 -0
  79. package/dist/markdown.js +382 -0
  80. package/dist/migrate.js +334 -0
  81. package/dist/multipart.js +90 -0
  82. package/dist/ops.js +869 -0
  83. package/dist/pagescript.js +465 -0
  84. package/dist/perms.js +370 -0
  85. package/dist/pointer.js +55 -0
  86. package/dist/profile.js +106 -0
  87. package/dist/pulls.js +320 -0
  88. package/dist/pullweb.js +461 -0
  89. package/dist/redirects.js +455 -0
  90. package/dist/releases.js +435 -0
  91. package/dist/render.js +233 -0
  92. package/dist/runner/actions.js +448 -0
  93. package/dist/runner/client.js +428 -0
  94. package/dist/runner/context.js +247 -0
  95. package/dist/runner/docker.js +197 -0
  96. package/dist/runner/externals.js +175 -0
  97. package/dist/runner/job.js +290 -0
  98. package/dist/runner/manual-run.js +272 -0
  99. package/dist/runner/overrides.js +554 -0
  100. package/dist/runner/steps.js +571 -0
  101. package/dist/runner/wake.js +84 -0
  102. package/dist/runner-cli.js +405 -0
  103. package/dist/scan.js +231 -0
  104. package/dist/server.js +424 -0
  105. package/dist/session.js +267 -0
  106. package/dist/site.js +259 -0
  107. package/dist/siteshost.js +94 -0
  108. package/dist/source.js +90 -0
  109. package/dist/style.js +1295 -0
  110. package/dist/themes.js +369 -0
  111. package/dist/vault.js +442 -0
  112. package/dist/version.js +88 -0
  113. package/dist/views.js +1007 -0
  114. package/dist/web.js +182 -0
  115. package/dist/webops.js +1402 -0
  116. package/package.json +71 -0
@@ -0,0 +1,405 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.runnerListCommand = void 0;
4
+ exports.runnerAddCmd = runnerAddCmd;
5
+ exports.runnerWakeCmd = runnerWakeCmd;
6
+ exports.runnerRemoveCmd = runnerRemoveCmd;
7
+ exports.runnerRunCmd = runnerRunCmd;
8
+ const cli_api_1 = require("./cli-api");
9
+ const output_1 = require("./cli/output");
10
+ const target_1 = require("./cli/target");
11
+ const credentials_1 = require("./credentials");
12
+ const job_cli_1 = require("./job-cli");
13
+ const client_1 = require("./runner/client");
14
+ const docker_1 = require("./runner/docker");
15
+ const wake_1 = require("./ci/wake");
16
+ const vault_1 = require("./vault");
17
+ /**
18
+ * "5m", "30s", "300" -> seconds.
19
+ *
20
+ * Written out because the one place this is used is a timeout an operator
21
+ * chooses in minutes and a program wants in seconds, and "--idle 300" for
22
+ * five minutes is a needless piece of arithmetic to ask of a reader.
23
+ */
24
+ function parseDuration(text) {
25
+ const m = /^(\d+)\s*(s|m|h)?$/i.exec(text.trim());
26
+ if (!m) {
27
+ console.error(`--idle takes a duration like 5m, 90s, or 1h, got: ${text}`);
28
+ process.exit(1);
29
+ }
30
+ const n = parseInt(m[1], 10);
31
+ const unit = (m[2] ?? 's').toLowerCase();
32
+ return unit === 'h' ? n * 3600 : unit === 'm' ? n * 60 : n;
33
+ }
34
+ function parseArgs(args, usage) {
35
+ const out = {
36
+ name: null,
37
+ host: null,
38
+ token: null,
39
+ runnerToken: null,
40
+ labels: [],
41
+ allow: [],
42
+ images: {},
43
+ workDir: null,
44
+ network: null,
45
+ cacheDir: null,
46
+ actionsUrl: null,
47
+ actionCache: true,
48
+ save: false,
49
+ idleSeconds: null,
50
+ wakePort: null,
51
+ wakeSecret: null,
52
+ wakeUrl: null,
53
+ clear: false,
54
+ engine: null,
55
+ };
56
+ const list = (v) => v.split(/[\s,]+/).filter((s) => s.length > 0);
57
+ for (let i = 0; i < args.length; i++) {
58
+ const a = args[i];
59
+ if (a === '-h' || a === '--help')
60
+ usage();
61
+ else if (a === '--host')
62
+ out.host = args[++i];
63
+ else if (a === '--token')
64
+ out.token = args[++i];
65
+ else if (a === '--runner-token')
66
+ out.runnerToken = args[++i];
67
+ else if (a === '--labels' || a === '--label')
68
+ out.labels.push(...list(args[++i] ?? ''));
69
+ else if (a === '--allow')
70
+ out.allow.push(...list(args[++i] ?? ''));
71
+ else if (a === '--work-dir')
72
+ out.workDir = args[++i];
73
+ else if (a === '--cache-dir')
74
+ out.cacheDir = args[++i];
75
+ else if (a === '--actions-url')
76
+ out.actionsUrl = args[++i];
77
+ else if (a === '--no-action-cache')
78
+ out.actionCache = false;
79
+ else if (a === '--network')
80
+ out.network = args[++i];
81
+ else if (a === '--idle')
82
+ out.idleSeconds = parseDuration(args[++i] ?? '');
83
+ else if (a === '--wake-port') {
84
+ const port = parseInt(args[++i] ?? '', 10);
85
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
86
+ console.error('--wake-port takes a port number, e.g. --wake-port 3000');
87
+ process.exit(1);
88
+ }
89
+ out.wakePort = port;
90
+ }
91
+ else if (a === '--wake-secret')
92
+ out.wakeSecret = args[++i];
93
+ else if (a === '--wake-url')
94
+ out.wakeUrl = args[++i];
95
+ else if (a === '--clear')
96
+ out.clear = true;
97
+ else if (a === '--save')
98
+ out.save = true;
99
+ else if (a === '--engine') {
100
+ const e = args[++i] ?? '';
101
+ if (e !== 'docker' && e !== 'podman') {
102
+ console.error(`--engine takes docker or podman, got: ${e}`);
103
+ process.exit(1);
104
+ }
105
+ out.engine = e;
106
+ }
107
+ else if (a === '--image') {
108
+ // --image ubuntu-latest=my/image:tag
109
+ const spec = args[++i] ?? '';
110
+ const eq = spec.indexOf('=');
111
+ if (eq === -1) {
112
+ console.error(`--image needs <label>=<image>, got: ${spec}`);
113
+ process.exit(1);
114
+ }
115
+ out.images[spec.slice(0, eq)] = spec.slice(eq + 1);
116
+ }
117
+ else if (a.startsWith('-')) {
118
+ console.error(`Unknown option: ${a}`);
119
+ process.exit(1);
120
+ }
121
+ else if (!out.name)
122
+ out.name = a;
123
+ else {
124
+ console.error(`Unexpected argument: ${a}`);
125
+ process.exit(1);
126
+ }
127
+ }
128
+ return out;
129
+ }
130
+ // Registration is an ordinary admin operation, so it uses the same login as
131
+ // `mochi user add` rather than any arrangement of its own.
132
+ async function runnerAddCmd(args, usage) {
133
+ const a = parseArgs(args, usage);
134
+ if (!a.name) {
135
+ console.error('A runner name is required: mochi runner add <name> --allow <glob>');
136
+ process.exit(1);
137
+ }
138
+ if (a.allow.length === 0) {
139
+ console.error('Say which repositories this runner may take jobs for: --allow "mycollection/*".\n' +
140
+ 'A runner executes whatever those repositories\' workflows contain, on the machine you start it on.');
141
+ process.exit(1);
142
+ }
143
+ const target = await (0, cli_api_1.remoteTarget)(a);
144
+ const labels = a.labels.length ? a.labels : ['ubuntu-latest'];
145
+ // A wake address is optional, and its secret is generated here when one is
146
+ // not supplied: it is a credential nobody has to remember, since the vault
147
+ // sends it and the runner is given it as an environment variable.
148
+ const wake = a.wakeUrl ? { url: a.wakeUrl, secret: a.wakeSecret ?? (0, wake_1.newWakeSecret)() } : null;
149
+ const data = await (0, cli_api_1.api)(target, 'POST', '/api/runners', {
150
+ name: a.name,
151
+ labels,
152
+ allow: a.allow,
153
+ ...(wake ? { wakeUrl: wake.url, wakeSecret: wake.secret } : {}),
154
+ });
155
+ console.log(`Registered runner ${data.name}`);
156
+ console.log(` labels: ${data.labels.join(', ')}`);
157
+ console.log(` serving: ${data.allow.join(', ')}`);
158
+ if (wake)
159
+ console.log(` wake: ${wake.url}`);
160
+ console.log('');
161
+ console.log('Runner token (shown once; only its hash is stored):');
162
+ console.log('');
163
+ console.log(` ${data.token}`);
164
+ console.log('');
165
+ if (a.save) {
166
+ const config = { host: target.host, token: data.token, labels };
167
+ if (a.workDir)
168
+ config.workDir = a.workDir;
169
+ if (a.network)
170
+ config.network = a.network;
171
+ if (Object.keys(a.images).length)
172
+ config.images = a.images;
173
+ (0, client_1.saveRunnerConfig)(config);
174
+ console.log(`Saved to ${(0, client_1.configPath)()}. Start it with:`);
175
+ console.log('');
176
+ console.log(' mochi runner run');
177
+ }
178
+ else {
179
+ console.log('On the machine that will run jobs (with Docker installed):');
180
+ console.log('');
181
+ console.log(` mochi runner run --host ${target.host} --runner-token ${data.token}`);
182
+ }
183
+ if (wake) {
184
+ console.log('');
185
+ console.log('This runner has a wake address, so the vault will start it when a job is');
186
+ console.log('waiting and nothing is polling. Start it with the matching secret, and with an');
187
+ console.log('idle timeout, or there will be nothing to wake:');
188
+ console.log('');
189
+ console.log(` MOCHI_WAKE_SECRET=${wake.secret} \\`);
190
+ console.log(` mochi runner run --idle 5m --wake-port 3000`);
191
+ }
192
+ console.log('');
193
+ }
194
+ /**
195
+ * `mochi runner wake <name>`: send the wake request, or change where it goes.
196
+ *
197
+ * Sending it is the useful default, because the question an operator has is
198
+ * almost always "does this actually start" rather than "what is stored". The
199
+ * vault does the sending, since the vault is the only party holding the
200
+ * secret; what comes back is how long it took to answer, which on a machine
201
+ * that has to boot is the number worth knowing.
202
+ */
203
+ async function runnerWakeCmd(args, usage) {
204
+ const a = parseArgs(args, usage);
205
+ if (!a.name) {
206
+ console.error('Which runner? Usage: mochi runner wake <name> [--url <url>] [--clear]');
207
+ process.exit(1);
208
+ }
209
+ if (a.clear && a.wakeUrl) {
210
+ console.error('--clear removes the wake address and --url sets one. Pass one or the other.');
211
+ process.exit(1);
212
+ }
213
+ const target = await (0, cli_api_1.remoteTarget)(a);
214
+ const path = `/api/runners/${encodeURIComponent(a.name)}/wake`;
215
+ if (a.clear) {
216
+ await (0, cli_api_1.api)(target, 'PUT', path, {});
217
+ console.log(`Runner ${a.name} has no wake address now, so nothing will start it.`);
218
+ return;
219
+ }
220
+ if (a.wakeUrl) {
221
+ const secret = a.wakeSecret ?? (0, wake_1.newWakeSecret)();
222
+ await (0, cli_api_1.api)(target, 'PUT', path, { wakeUrl: a.wakeUrl, wakeSecret: secret });
223
+ console.log(`Runner ${a.name} will be woken at ${a.wakeUrl}`);
224
+ if (!a.wakeSecret) {
225
+ console.log('');
226
+ console.log('The secret it must present, generated now and shown once here because the');
227
+ console.log('runner has to be started with the same one:');
228
+ console.log('');
229
+ console.log(` ${secret}`);
230
+ console.log('');
231
+ console.log(' MOCHI_WAKE_SECRET=<that> mochi runner run --idle 5m --wake-port 3000');
232
+ }
233
+ return;
234
+ }
235
+ const data = await (0, cli_api_1.api)(target, 'POST', path, {});
236
+ console.log(`Woke ${a.name} at ${String(data.wakeUrl)} in ${String(data.seconds)}s.`);
237
+ }
238
+ /** "3s ago", for the one column where a bare ISO timestamp would be unreadable. */
239
+ function ago(iso) {
240
+ if (!iso)
241
+ return 'never';
242
+ const secs = Math.max(0, Math.round((Date.now() - Date.parse(iso)) / 1000));
243
+ if (secs < 90)
244
+ return `${secs}s ago`;
245
+ if (secs < 5400)
246
+ return `${Math.round(secs / 60)}m ago`;
247
+ return `${Math.round(secs / 3600)}h ago`;
248
+ }
249
+ function jobLabel(j) {
250
+ return `${j.collection}/${j.repo} #${j.run} ${j.job}`;
251
+ }
252
+ /**
253
+ * A queued job that no registered runner can take: its labels match nobody, or
254
+ * nobody's allow list covers its repository. This is the answer to "the run
255
+ * never started" that neither the run nor the registry gives on its own, so it
256
+ * is worth computing here rather than leaving to the reader.
257
+ */
258
+ function unservedJobs(queued, runners) {
259
+ return queued.filter((j) =>
260
+ // A manual job is waiting for a pasted command by design, so no runner
261
+ // failing to match it is not the problem this list exists to surface.
262
+ !j.manual &&
263
+ !runners.some((r) => r.allow.some((g) => (0, vault_1.globMatch)(g, `${j.collection}/${j.repo}`)) &&
264
+ j.runsOn.some((l) => r.labels.includes(l))));
265
+ }
266
+ exports.runnerListCommand = {
267
+ path: ['runner', 'list'],
268
+ summary: 'Show registered runners, whether each is connected, and what is queued',
269
+ description: `Needs an admin token, as the other runner registration commands do.
270
+
271
+ Beside the registry it reports liveness: when each runner last spoke to the vault
272
+ (since the vault started; a restart forgets it and every live runner re-announces
273
+ within one poll), the job it is holding now, and the jobs waiting for a runner. A
274
+ queued job that no runner's labels and allow globs match is called out, since that
275
+ is the usual reason a run sits at queued forever.`,
276
+ options: [output_1.JSON_OPTION, ...target_1.TARGET_OPTIONS],
277
+ async run(inv) {
278
+ const target = await (0, target_1.targetFrom)(inv);
279
+ const data = await (0, cli_api_1.api)(target, 'GET', '/api/runners');
280
+ const runners = (data.runners ?? []);
281
+ const queued = (data.queued ?? []);
282
+ const json = (0, output_1.jsonMode)(inv);
283
+ if (json.enabled) {
284
+ (0, output_1.printJson)({ runners: (0, output_1.pickFields)(runners, json.fields), queued });
285
+ return;
286
+ }
287
+ if (runners.length === 0) {
288
+ console.log('No runners registered. Runs will queue and wait: mochi runner add <name> --allow <glob>');
289
+ }
290
+ else {
291
+ (0, output_1.printTable)(runners.map((r) => [
292
+ r.name,
293
+ `labels: ${r.labels.join(', ') || '(none)'}`,
294
+ `serving: ${r.allow.join(', ') || '(none)'}`,
295
+ // A vault older than liveness reporting leaves both out, and saying
296
+ // "seen never" of a runner that may be perfectly healthy would be
297
+ // worse than saying nothing.
298
+ r.lastSeen === undefined && !r.running
299
+ ? r.createdBy
300
+ ? `by ${r.createdBy}`
301
+ : ''
302
+ : r.running
303
+ ? `running ${jobLabel(r.running)}`
304
+ : // A runner with a wake address is meant to be absent between
305
+ // jobs, so reporting it as idle since an hour ago without that
306
+ // context reads as a fault rather than as the arrangement
307
+ // working.
308
+ `idle, seen ${ago(r.lastSeen ?? null)}${r.wakeUrl ? ', woken on demand' : ''}`,
309
+ ]));
310
+ }
311
+ if (queued.length > 0) {
312
+ const automatic = queued.filter((j) => !j.manual);
313
+ const manual = queued.filter((j) => j.manual);
314
+ console.log('');
315
+ if (automatic.length > 0) {
316
+ console.log(`${automatic.length} job${automatic.length === 1 ? '' : 's'} waiting for a runner:`);
317
+ for (const j of automatic)
318
+ console.log(` ${jobLabel(j)} (runs-on: ${j.runsOn.join(', ')})`);
319
+ }
320
+ if (manual.length > 0) {
321
+ console.log(`${manual.length} manual job${manual.length === 1 ? '' : 's'} waiting for a pasted command (mochi run exec-command <run>):`);
322
+ for (const j of manual)
323
+ console.log(` ${jobLabel(j)} (runs-on: ${j.runsOn.join(', ')})`);
324
+ }
325
+ const unserved = unservedJobs(queued, runners);
326
+ if (unserved.length > 0) {
327
+ console.log('');
328
+ console.log(`No registered runner can take ${unserved.length === automatic.length ? 'them' : `${unserved.length} of them`}: check the runs-on labels against each runner's labels, and the repository against its serving globs.`);
329
+ }
330
+ }
331
+ },
332
+ };
333
+ async function runnerRemoveCmd(args, usage) {
334
+ const a = parseArgs(args, usage);
335
+ if (!a.name) {
336
+ console.error('A runner name is required: mochi runner remove <name>');
337
+ process.exit(1);
338
+ }
339
+ const target = await (0, cli_api_1.remoteTarget)(a);
340
+ await (0, cli_api_1.api)(target, 'DELETE', `/api/runners/${encodeURIComponent(a.name)}`);
341
+ console.log(`Removed runner ${a.name}`);
342
+ }
343
+ async function runnerRunCmd(args, usage) {
344
+ const a = parseArgs(args, usage);
345
+ const saved = (0, client_1.loadRunnerConfig)();
346
+ // A runner's token is its own, not a user's, so it is not something login
347
+ // stored; only the vault URL can be borrowed from a login on this machine.
348
+ const host = (a.host ?? saved?.host ?? (0, credentials_1.loadLogin)()?.host ?? '').replace(/\/+$/, '');
349
+ const token = a.runnerToken ?? process.env.MOCHI_RUNNER_TOKEN ?? saved?.token ?? '';
350
+ if (!host || !token) {
351
+ console.error(`No runner credentials. Register one with:\n\n` +
352
+ ` mochi runner add <name> --allow 'mycollection/*' --save\n\n` +
353
+ `or pass --host and --runner-token, or write ${(0, client_1.configPath)()}.`);
354
+ process.exit(1);
355
+ }
356
+ const config = {
357
+ host,
358
+ token,
359
+ labels: a.labels.length ? a.labels : saved?.labels,
360
+ images: { ...client_1.DEFAULT_IMAGES, ...(saved?.images ?? {}), ...a.images },
361
+ workDir: a.workDir ?? saved?.workDir,
362
+ network: a.network ?? saved?.network,
363
+ cacheDir: a.cacheDir ?? saved?.cacheDir,
364
+ actionsUrl: a.actionsUrl ?? saved?.actionsUrl,
365
+ actionCache: a.actionCache && (saved?.actionCache ?? true),
366
+ idleSeconds: a.idleSeconds ?? saved?.idleSeconds,
367
+ wakePort: a.wakePort ?? saved?.wakePort,
368
+ // From the environment by preference, like the runner token above: this is
369
+ // the credential a wake request must present, and a secret in argv is
370
+ // readable by every other user on the machine.
371
+ wakeSecret: a.wakeSecret ?? process.env.MOCHI_WAKE_SECRET ?? saved?.wakeSecret,
372
+ };
373
+ if (config.wakePort && !config.wakeSecret) {
374
+ console.error('A wake port needs a secret to check against: pass --wake-secret, or set MOCHI_WAKE_SECRET.\n' +
375
+ 'Without one, anything that can reach this port could start this runner.');
376
+ process.exit(1);
377
+ }
378
+ if (config.idleSeconds && !config.wakePort) {
379
+ // Not an error: a runner started by cron, or by hand for one run, is meant
380
+ // to stop and stay stopped. But the usual reason to pass --idle is the
381
+ // other arrangement, and a runner that stops with nothing able to start it
382
+ // looks exactly like a runner that has failed.
383
+ console.log('Note: this runner will stop when idle, and has no wake port, so nothing can start it.');
384
+ console.log(' Add --wake-port to let the vault do it: mochi runner wake --help');
385
+ }
386
+ if (a.save) {
387
+ (0, client_1.saveRunnerConfig)(config);
388
+ console.log(`Saved to ${(0, client_1.configPath)()}`);
389
+ }
390
+ // Which container engine carries the jobs: the flag if given, whichever of
391
+ // docker and podman works if only one does, a question when both do and
392
+ // there is a terminal to ask on, docker otherwise. Chosen before the runner
393
+ // starts, because a runner must use one engine for everything or nothing.
394
+ const engine = await (0, job_cli_1.chooseEngine)(a.engine, process.stdin.isTTY === true && process.stdout.isTTY === true);
395
+ (0, docker_1.setContainerEngine)(engine);
396
+ const runner = new client_1.Runner(config);
397
+ const stop = () => {
398
+ console.log('\nStopping after the current job. Press Ctrl-C again to quit now.');
399
+ runner.stop();
400
+ process.once('SIGINT', () => process.exit(130));
401
+ };
402
+ process.once('SIGINT', stop);
403
+ process.once('SIGTERM', stop);
404
+ await runner.loop();
405
+ }
package/dist/scan.js ADDED
@@ -0,0 +1,231 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.repoSiblingSuffixes = void 0;
37
+ exports.isValidName = isValidName;
38
+ exports.isDotName = isDotName;
39
+ exports.isValidUserName = isValidUserName;
40
+ exports.reservedRepoSuffix = reservedRepoSuffix;
41
+ exports.isBareRepo = isBareRepo;
42
+ exports.displayName = displayName;
43
+ exports.listRepoDirs = listRepoDirs;
44
+ exports.listCollections = listCollections;
45
+ exports.findRepo = findRepo;
46
+ exports.siteDir = siteDir;
47
+ exports.forkParent = forkParent;
48
+ exports.repoDescription = repoDescription;
49
+ const fs = __importStar(require("fs"));
50
+ const path = __importStar(require("path"));
51
+ const git_1 = require("./git");
52
+ const layout_1 = require("./layout");
53
+ // Names the UI owns as top-level path segments. None of these may ever be a
54
+ // collection or repo name, since a collection is reached at /<collection> and
55
+ // a repository at /<collection>/<repo>. The vault's own files are not among
56
+ // them: they sit beside `collections/` rather than in it, so no file of the
57
+ // vault's can collide with a name a user chose.
58
+ const RESERVED_NAMES = new Set([
59
+ 'about',
60
+ 'api',
61
+ 'assets',
62
+ 'favicon.ico',
63
+ 'favicon.svg',
64
+ 'login',
65
+ 'logout',
66
+ 'new',
67
+ 'import',
68
+ 'admin',
69
+ 'settings',
70
+ ]);
71
+ function isValidName(name) {
72
+ if (RESERVED_NAMES.has(name))
73
+ return false;
74
+ return /^\.?[A-Za-z0-9][A-Za-z0-9._-]*$/.test(name) && !name.includes('..');
75
+ }
76
+ /**
77
+ * Whether a name is dot-prefixed. A single leading dot is allowed so that a
78
+ * repository can carry a name the interface reads rather than a name a project
79
+ * chose: `.mochi` holds a collection's profile README (see
80
+ * src/profile.ts), and the shape is left open for whatever else the forge
81
+ * later wants to read out of a collection.
82
+ *
83
+ * Only a repository may carry such a name. A collection or a user named with a
84
+ * leading dot would be a hidden directory in the vault and a hidden address in
85
+ * the interface, with nothing gained, so both are refused where they are
86
+ * created. Reading stays permissive: isValidName above accepts the dot for
87
+ * every kind of name, since a route that is asked for one only has to resolve
88
+ * it, and a vault that somehow holds one should still serve what it holds.
89
+ */
90
+ function isDotName(name) {
91
+ return name.startsWith('.');
92
+ }
93
+ /**
94
+ * A username. A name being looked up is checked with isValidName, which
95
+ * accepts a leading dot; this is the stricter rule applied where a user is
96
+ * created, since only a repository may carry one.
97
+ */
98
+ function isValidUserName(name) {
99
+ return isValidName(name) && !isDotName(name);
100
+ }
101
+ // The directories a repository accumulates beside its bare repository, by the
102
+ // suffix each one carries. Every feature has its own helper for the directory
103
+ // it owns (siteDir, runsDir, issuesDir, pullsDir, releasesDir), but rename and
104
+ // delete are the two places that need the whole set, and a set spelled out in
105
+ // two places is a set one of them falls behind on. The list therefore lives
106
+ // here, beside the rest of what may and may not be a name, so a sibling added
107
+ // later is added once and is refused as a new repository's name for free.
108
+ //
109
+ // LFS objects are not in the list. Without a bucket they do sit in a sibling
110
+ // <repo>.lfs, but with one they do not sit on disk at all, so the store is
111
+ // asked to move or drop them rather than having its path assumed.
112
+ exports.repoSiblingSuffixes = ['.site', '.runs', '.issues', '.pulls', '.releases'];
113
+ // Suffixes a repository may not be created under. A repository named
114
+ // webapp.site would land on exactly the path the repository webapp keeps its
115
+ // site in, so without this a user with the write role could shadow another
116
+ // repository's site, or have their own issues served as that repository's.
117
+ // The list is the siblings plus two the siblings do not name: .lfs, which is
118
+ // where LFS objects sit when no bucket is configured, and .git, the ordinary
119
+ // on-disk spelling of a bare repository, since displayName would resolve a
120
+ // new webapp.git back to webapp. Compared case-insensitively, because a
121
+ // case-insensitive filesystem would collide where a case-sensitive one would
122
+ // not, and the refusal should not depend on which one is underneath.
123
+ const reservedRepoSuffixes = [...exports.repoSiblingSuffixes, '.lfs', '.git'];
124
+ /**
125
+ * The reserved suffix a proposed repository name ends in, or null if it ends
126
+ * in none. Callers report the suffix, since a name is much easier to fix when
127
+ * the refusal says which part of it is the problem. Asked only where a
128
+ * repository comes into being: reading stays more permissive, so a repository
129
+ * created before this check existed keeps working.
130
+ */
131
+ function reservedRepoSuffix(name) {
132
+ const lower = name.toLowerCase();
133
+ return reservedRepoSuffixes.find((s) => lower.endsWith(s)) ?? null;
134
+ }
135
+ function isBareRepo(dir) {
136
+ try {
137
+ return (fs.statSync(path.join(dir, 'HEAD')).isFile() &&
138
+ fs.statSync(path.join(dir, 'objects')).isDirectory() &&
139
+ fs.statSync(path.join(dir, 'refs')).isDirectory());
140
+ }
141
+ catch {
142
+ return false;
143
+ }
144
+ }
145
+ function displayName(dirName) {
146
+ return dirName.replace(/\.git$/, '');
147
+ }
148
+ function listRepoDirs(root, collection) {
149
+ const dir = (0, layout_1.reposDir)(root, collection);
150
+ let entries;
151
+ try {
152
+ entries = fs.readdirSync(dir, { withFileTypes: true });
153
+ }
154
+ catch {
155
+ return [];
156
+ }
157
+ return entries
158
+ .filter((e) => e.isDirectory() && isValidName(e.name) && isBareRepo(path.join(dir, e.name)))
159
+ .map((e) => e.name)
160
+ .sort();
161
+ }
162
+ function listCollections(root) {
163
+ let entries;
164
+ try {
165
+ entries = fs.readdirSync((0, layout_1.collectionsDir)(root), { withFileTypes: true });
166
+ }
167
+ catch {
168
+ return [];
169
+ }
170
+ return entries
171
+ .filter((e) => e.isDirectory() && isValidName(e.name) && !isDotName(e.name))
172
+ .map((e) => ({ name: e.name, repoCount: listRepoDirs(root, e.name).length }))
173
+ .sort((a, b) => a.name.localeCompare(b.name));
174
+ }
175
+ function findRepo(root, collection, repoName) {
176
+ if (!isValidName(collection) || !isValidName(repoName))
177
+ return null;
178
+ const base = displayName(repoName);
179
+ for (const cand of [base, base + '.git']) {
180
+ const dir = (0, layout_1.repoPath)(root, collection, cand);
181
+ if (isBareRepo(dir))
182
+ return new git_1.GitRepo(dir, collection, base);
183
+ }
184
+ return null;
185
+ }
186
+ // A repository's static site lives in a sibling directory, `<repo>.site`.
187
+ function siteDir(root, collection, repoName) {
188
+ if (!isValidName(collection) || !isValidName(repoName))
189
+ return null;
190
+ const dir = (0, layout_1.repoPath)(root, collection, `${displayName(repoName)}.site`);
191
+ try {
192
+ if (fs.statSync(dir).isDirectory())
193
+ return dir;
194
+ }
195
+ catch {
196
+ // no site directory
197
+ }
198
+ return null;
199
+ }
200
+ /**
201
+ * The repository this one was forked from, as `<collection>/<repo>`, or null
202
+ * if it was not. Read out of the bare repository's own config file rather
203
+ * than through git, since this is asked on page renders.
204
+ */
205
+ function forkParent(dir) {
206
+ let text;
207
+ try {
208
+ text = fs.readFileSync(path.join(dir, 'config'), 'utf8');
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ const m = text.match(/^\s*forkedFrom\s*=\s*(\S+)\s*$/m);
214
+ if (!m)
215
+ return null;
216
+ const [collection, repo] = m[1].split('/');
217
+ if (!collection || !repo || !isValidName(collection) || !isValidName(repo))
218
+ return null;
219
+ return { collection, repo };
220
+ }
221
+ function repoDescription(dir) {
222
+ try {
223
+ const t = fs.readFileSync(path.join(dir, 'description'), 'utf8').trim();
224
+ if (!t || t.startsWith('Unnamed repository'))
225
+ return null;
226
+ return t;
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ }