@magland/mochi 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/README.md +7 -6
  2. package/dist/agefile.js +47 -0
  3. package/dist/agescript.js +343 -0
  4. package/dist/api/admin.js +66 -28
  5. package/dist/api/auth.js +3 -2
  6. package/dist/api/backup.js +227 -192
  7. package/dist/api/contents.js +46 -16
  8. package/dist/api/repos.js +60 -4
  9. package/dist/api/write.js +138 -8
  10. package/dist/api.js +34 -1
  11. package/dist/browse.js +171 -33
  12. package/dist/build-info.json +2 -2
  13. package/dist/ci/api.js +138 -4
  14. package/dist/ci/artifacts.js +82 -2
  15. package/dist/ci/engine.js +30 -17
  16. package/dist/ci/expr.js +14 -0
  17. package/dist/ci/runners.js +60 -1
  18. package/dist/ci/runs.js +21 -3
  19. package/dist/ci/trigger.js +6 -4
  20. package/dist/ci/views.js +24 -3
  21. package/dist/ci/web.js +48 -0
  22. package/dist/cli/admin-cmd.js +40 -3
  23. package/dist/cli/backup-cmd.js +138 -120
  24. package/dist/cli/gitrun.js +50 -0
  25. package/dist/cli/pr-cmd.js +155 -0
  26. package/dist/cli/repo-cmd.js +134 -6
  27. package/dist/cli/sync-cmd.js +128 -0
  28. package/dist/cli/target.js +7 -3
  29. package/dist/config.js +10 -0
  30. package/dist/credentials.js +9 -8
  31. package/dist/deploy-cli.js +214 -109
  32. package/dist/deploy-runner-cli.js +125 -49
  33. package/dist/domains.js +259 -0
  34. package/dist/egress.js +23 -6
  35. package/dist/forms.js +703 -32
  36. package/dist/git.js +124 -13
  37. package/dist/githttp.js +80 -23
  38. package/dist/githubauth.js +185 -0
  39. package/dist/icons.js +14 -0
  40. package/dist/import-cli.js +40 -25
  41. package/dist/index.js +110 -4
  42. package/dist/issueweb.js +10 -6
  43. package/dist/lfs.js +9 -2
  44. package/dist/limit.js +2 -2
  45. package/dist/logincodes.js +31 -0
  46. package/dist/maintenance.js +297 -0
  47. package/dist/markdown.js +192 -3
  48. package/dist/mdedit.js +29 -0
  49. package/dist/migrate.js +48 -0
  50. package/dist/naming.js +25 -0
  51. package/dist/onetime.js +112 -0
  52. package/dist/ops.js +293 -16
  53. package/dist/pagescript.js +355 -24
  54. package/dist/perms.js +1 -0
  55. package/dist/profile.js +2 -0
  56. package/dist/pulls.js +21 -1
  57. package/dist/pullweb.js +25 -6
  58. package/dist/releases.js +10 -1
  59. package/dist/runner/actions.js +12 -1
  60. package/dist/runner/client.js +40 -2
  61. package/dist/runner/docker.js +40 -28
  62. package/dist/runner/download.js +77 -0
  63. package/dist/runner/externals.js +12 -1
  64. package/dist/runner/job.js +26 -11
  65. package/dist/runner/steps.js +42 -7
  66. package/dist/runner-cli.js +89 -0
  67. package/dist/scan.js +58 -2
  68. package/dist/server.js +107 -26
  69. package/dist/session.js +82 -40
  70. package/dist/site.js +110 -27
  71. package/dist/sitepublish.js +177 -0
  72. package/dist/sitesettings.js +420 -0
  73. package/dist/siteshost.js +116 -28
  74. package/dist/source.js +20 -0
  75. package/dist/style.js +162 -3
  76. package/dist/topics.js +165 -0
  77. package/dist/vault.js +399 -6
  78. package/dist/vendor-age.js +15 -0
  79. package/dist/views.js +242 -57
  80. package/dist/web.js +11 -1
  81. package/dist/webauthn.js +347 -0
  82. package/dist/webops.js +1263 -33
  83. package/package.json +4 -4
@@ -33,12 +33,14 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
- exports.IMAGE_REPO = void 0;
36
+ exports.MOCHI_DEPLOY = exports.IMAGE_REPO = void 0;
37
37
  exports.fly = fly;
38
38
  exports.flyStream = flyStream;
39
39
  exports.die = die;
40
40
  exports.parseVmSize = parseVmSize;
41
41
  exports.normalizeMemory = normalizeMemory;
42
+ exports.imagePublished = imagePublished;
43
+ exports.requirePublishedImage = requirePublishedImage;
42
44
  exports.ownVersion = ownVersion;
43
45
  exports.sourceRoot = sourceRoot;
44
46
  exports.requireFly = requireFly;
@@ -57,6 +59,7 @@ const os = __importStar(require("os"));
57
59
  const path = __importStar(require("path"));
58
60
  const credentials_1 = require("./credentials");
59
61
  const backup_cmd_1 = require("./cli/backup-cmd");
62
+ const naming_1 = require("./naming");
60
63
  const vault_1 = require("./vault");
61
64
  // `mochi deploy fly`: put a vault on Fly.io from one command, and deploy
62
65
  // updates to it with the same one. This is a thin driver of the fly command
@@ -70,8 +73,6 @@ const vault_1 = require("./vault");
70
73
  // fly.toml goes to a temporary directory for the length of the deploy, which is
71
74
  // why there is no fly.toml in this repository to keep in sync or to explain.
72
75
  exports.IMAGE_REPO = 'ghcr.io/magland/mochi';
73
- const VOLUME_NAME = 'vault';
74
- const OWNER_TOKEN_SECRET = 'MOCHI_OWNER_TOKEN';
75
76
  const INTERNAL_PORT = 3000;
76
77
  const DEFAULTS = { region: 'ewr', volumeGb: 10, cpuKind: 'shared', cpus: 1, memory: '512mb' };
77
78
  const FLY_NOT_FOUND = 'Neither fly nor flyctl is on PATH. Install it from https://fly.io/docs/flyctl/install/';
@@ -259,6 +260,65 @@ function normalizeMemory(memory) {
259
260
  die(`Memory of ${memory} is below Fly's 256mb minimum.`);
260
261
  return `${mb}mb`;
261
262
  }
263
+ /**
264
+ * Whether a tag exists in the registry, or null when the registry could not
265
+ * be asked. Anonymous, over the registry protocol every pull uses: a token
266
+ * for the repository, then the manifest by tag.
267
+ *
268
+ * The question arises because of the order a release happens in. The npm
269
+ * package is published first and the image is built from the tag that
270
+ * follows it, so for some minutes after a release the newest CLI names an
271
+ * image that is not there yet. Fly would report that as a failed pull after
272
+ * creating the app; asking first turns it into one sentence before anything
273
+ * is touched. Not being able to ask is not a refusal: the deploy goes ahead
274
+ * and Fly says whatever it finds.
275
+ */
276
+ async function imagePublished(ref) {
277
+ const m = /^ghcr\.io\/([a-z0-9._/-]+):([A-Za-z0-9._-]+)$/.exec(ref);
278
+ if (!m)
279
+ return null;
280
+ try {
281
+ const tokenRes = await fetch(`https://ghcr.io/token?scope=repository:${m[1]}:pull`);
282
+ if (!tokenRes.ok)
283
+ return null;
284
+ const { token } = (await tokenRes.json());
285
+ if (!token)
286
+ return null;
287
+ const res = await fetch(`https://ghcr.io/v2/${m[1]}/manifests/${m[2]}`, {
288
+ method: 'HEAD',
289
+ headers: {
290
+ authorization: `Bearer ${token}`,
291
+ accept: [
292
+ 'application/vnd.oci.image.index.v1+json',
293
+ 'application/vnd.docker.distribution.manifest.list.v2+json',
294
+ 'application/vnd.oci.image.manifest.v1+json',
295
+ 'application/vnd.docker.distribution.manifest.v2+json',
296
+ ].join(', '),
297
+ },
298
+ });
299
+ if (res.status === 404)
300
+ return false;
301
+ return res.ok ? true : null;
302
+ }
303
+ catch {
304
+ return null;
305
+ }
306
+ }
307
+ /**
308
+ * Refuse, with the reason, when the image this CLI's version names has not
309
+ * been published yet. Only for the image chosen by default: a ref the
310
+ * operator passed by hand is theirs to have got right, and Fly says so.
311
+ */
312
+ async function requirePublishedImage(image, runner) {
313
+ if ((await imagePublished(image)) !== false)
314
+ return;
315
+ die(`No published image for this CLI's version: ${image} is not in the registry.\n` +
316
+ "A version's image is built after its npm release and is usually there within\n" +
317
+ 'ten minutes of it. Try again shortly, or pass --image <ref> to deploy another\n' +
318
+ `tag: ${image.replace(/:[^:]+$/, ':main')} is the newest build of main that passed\n` +
319
+ 'its tests' +
320
+ (runner ? '.' : ', and --from-source builds this checkout instead.'));
321
+ }
262
322
  /** The version of this CLI, which is the image tag deployed unless --image says otherwise. */
263
323
  function ownVersion() {
264
324
  try {
@@ -287,7 +347,7 @@ function sourceRoot() {
287
347
  ` ${root}\n\n` +
288
348
  'The published package contains only the compiled output, so there is nothing to\n' +
289
349
  'build. Clone the repository and run the deploy from there:\n\n' +
290
- ' git clone https://github.com/magland/mochi && cd mochi && npm install\n' +
350
+ ' git clone https://github.com/magland/mochiforge && cd mochiforge && npm install\n' +
291
351
  ' npm run build && node dist/index.js deploy fly <app> --from-source\n');
292
352
  }
293
353
  return root;
@@ -306,9 +366,6 @@ async function namedVolume(app, name) {
306
366
  const vols = (await flyJson(['volumes', 'list', '-a', app])) ?? [];
307
367
  return vols.find((v) => v.name === name) ?? null;
308
368
  }
309
- async function vaultVolume(app) {
310
- return namedVolume(app, VOLUME_NAME);
311
- }
312
369
  async function machines(app) {
313
370
  return (await flyJson(['machines', 'list', '-a', app])) ?? [];
314
371
  }
@@ -327,10 +384,45 @@ async function secretNames(app) {
327
384
  const secrets = (await flyJson(['secrets', 'list', '-a', app])) ?? [];
328
385
  return secrets.map((s) => s.Name ?? s.name ?? '').filter(Boolean);
329
386
  }
387
+ exports.MOCHI_DEPLOY = {
388
+ imageRepo: exports.IMAGE_REPO,
389
+ version: ownVersion,
390
+ buildContext: () => ({ dir: sourceRoot(), cleanup: () => undefined }),
391
+ volumeName: 'vault',
392
+ mountPath: '/vault',
393
+ lfsBucket: true,
394
+ concurrency: { type: 'requests', soft: 200, hard: 250 },
395
+ contents: 'repositories, issues, pull requests, users',
396
+ firstSteps: (url, username) => [
397
+ `To administer the vault in a browser, open its sign-in page and give that token as`,
398
+ `'${username}':`,
399
+ '',
400
+ ` ${url}/login`,
401
+ '',
402
+ 'The form asks for a username and a token, since a vault has no passwords. From',
403
+ 'there the Admin page creates the users and the repositories, which is the usual',
404
+ 'way to bootstrap a fresh vault.',
405
+ '',
406
+ "To use the CLI and git instead, hand the same token to git's credential store,",
407
+ 'which is what login is for:',
408
+ '',
409
+ ` mochi login ${url}`,
410
+ '',
411
+ 'It asks for the token without echoing it, checks it, and remembers this vault, so',
412
+ 'these need no arguments afterwards and git stops asking on a push:',
413
+ '',
414
+ ' mochi whoami',
415
+ ' mochi user add alice',
416
+ ` mochi import https://github.com/someone/something.git mine`,
417
+ ],
418
+ };
419
+ function volumeOf(app, profile) {
420
+ return namedVolume(app, profile.volumeName);
421
+ }
330
422
  /** What Fly currently has, so that a flag-less redeploy changes nothing and one flag changes one thing. */
331
- async function liveSettings(app) {
423
+ async function liveSettings(app, profile) {
332
424
  const out = {};
333
- const vol = await vaultVolume(app);
425
+ const vol = await volumeOf(app, profile);
334
426
  if (vol) {
335
427
  out.region = vol.region;
336
428
  out.volumeGb = vol.size_gb;
@@ -362,21 +454,22 @@ function resolveSettings(a, live) {
362
454
  // auto-start here. A second machine would mean a second volume and a second
363
455
  // vault, diverging silently from the first. For the same reason, a busier vault
364
456
  // wants a bigger machine rather than more of them.
365
- function flyToml(app, s) {
366
- return `# Generated by mochi deploy for '${app}'. Written to a temporary
457
+ function flyToml(app, s, profile) {
458
+ const c = profile.concurrency;
459
+ return `# Generated by ${naming_1.naming.product} deploy for '${app}'. Written to a temporary
367
460
  # directory for the length of one deploy; edit the deploy command, not this.
368
461
  app = "${app}"
369
462
  primary_region = "${s.region}"
370
463
 
371
464
  [mounts]
372
- source = "${VOLUME_NAME}"
373
- destination = "/vault"
465
+ source = "${profile.volumeName}"
466
+ destination = "${profile.mountPath}"
374
467
 
375
468
  # Fly always terminates TLS in front, so the forwarded headers are the only place
376
- # the real scheme and address appear. The server records this in the vault's
469
+ # the real scheme and address appear. The server records this in the ${naming_1.naming.rootNoun}'s
377
470
  # config.json on the next start, where it can be changed by hand afterwards.
378
471
  [env]
379
- MOCHI_TRUST_PROXY = "1"
472
+ ${naming_1.naming.envPrefix}_TRUST_PROXY = "1"
380
473
 
381
474
  [http_service]
382
475
  internal_port = ${INTERNAL_PORT}
@@ -385,9 +478,9 @@ primary_region = "${s.region}"
385
478
  auto_start_machines = true
386
479
  min_machines_running = 0
387
480
  [http_service.concurrency]
388
- type = "requests"
389
- hard_limit = 250
390
- soft_limit = 200
481
+ type = "${c.type}"
482
+ hard_limit = ${c.hard}
483
+ soft_limit = ${c.soft}
391
484
 
392
485
  [[vm]]
393
486
  cpu_kind = "${s.cpuKind}"
@@ -395,10 +488,10 @@ primary_region = "${s.region}"
395
488
  memory = "${s.memory}"
396
489
  `;
397
490
  }
398
- function writeTempConfig(app, s) {
399
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'mochi-deploy-'));
491
+ function writeTempConfig(app, s, profile) {
492
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), `${naming_1.naming.product}-deploy-`));
400
493
  const file = path.join(dir, 'fly.toml');
401
- fs.writeFileSync(file, flyToml(app, s));
494
+ fs.writeFileSync(file, flyToml(app, s, profile));
402
495
  return file;
403
496
  }
404
497
  function appUrl(app) {
@@ -411,6 +504,7 @@ function appUrl(app) {
411
504
  * that was already initialized answers 401.
412
505
  */
413
506
  async function waitForVault(url, token, seconds = 120) {
507
+ const noun = naming_1.naming.rootNoun;
414
508
  const deadline = Date.now() + seconds * 1000;
415
509
  let last = 'no answer yet';
416
510
  while (Date.now() < deadline) {
@@ -420,12 +514,12 @@ async function waitForVault(url, token, seconds = 120) {
420
514
  const data = (await resp.json());
421
515
  if (typeof data.username === 'string' && data.username)
422
516
  return { ok: true, username: data.username };
423
- last = 'the vault answered without saying who the token belongs to';
517
+ last = `the ${noun} answered without saying who the token belongs to`;
424
518
  }
425
519
  else if (resp.status === 401 || resp.status === 403) {
426
520
  // Conclusive rather than worth retrying: the server is up and has
427
521
  // rejected this token, which means the vault was initialized before.
428
- return { ok: false, reason: `the vault did not accept the new owner token (HTTP ${resp.status})` };
522
+ return { ok: false, reason: `the ${noun} did not accept the new owner token (HTTP ${resp.status})` };
429
523
  }
430
524
  else {
431
525
  last = `HTTP ${resp.status} from ${url}`;
@@ -438,16 +532,22 @@ async function waitForVault(url, token, seconds = 120) {
438
532
  }
439
533
  return { ok: false, reason: `timed out after ${seconds}s: ${last}` };
440
534
  }
441
- async function deployFlyCmd(args, usage) {
535
+ async function deployFlyCmd(args, usage, profile = exports.MOCHI_DEPLOY) {
536
+ const product = naming_1.naming.product;
537
+ const noun = naming_1.naming.rootNoun;
538
+ const ownerTokenSecret = `${naming_1.naming.envPrefix}_OWNER_TOKEN`;
442
539
  const a = parseDeployArgs(args, usage);
443
540
  if (!a.app) {
444
541
  die('Which app? Fly app names are globally unique, and the name becomes the URL:\n\n' +
445
- ' mochi deploy fly my-vault-name -> https://my-vault-name.fly.dev\n');
542
+ ` ${product} deploy fly my-${noun}-name -> https://my-${noun}-name.fly.dev\n`);
446
543
  }
447
544
  const app = a.app;
448
545
  if (!/^[a-z0-9][a-z0-9-]{0,62}$/.test(app)) {
449
546
  die(`Not a valid Fly app name: ${app}\nUse lowercase letters, digits, and dashes.`);
450
547
  }
548
+ if (a.lfsBucket && !profile.lfsBucket) {
549
+ die(`--lfs-bucket provisions storage for Git LFS objects, and a ${noun} keeps none.`);
550
+ }
451
551
  // What gets deployed: a published image to pull, or this checkout to build.
452
552
  // Settled from the flags before anything reaches the network, so a
453
553
  // contradiction between them costs no round trip and creates no app.
@@ -457,18 +557,28 @@ async function deployFlyCmd(args, usage) {
457
557
  if (a.localBuild && !a.fromSource) {
458
558
  die('--local-build says how to build, and without --from-source there is nothing to build.');
459
559
  }
460
- const buildRoot = a.fromSource ? sourceRoot() : null;
461
- const image = buildRoot === null ? a.image ?? `${exports.IMAGE_REPO}:${ownVersion()}` : null;
560
+ // The build context is settled here, before anything reaches the network,
561
+ // so a machine that cannot build (an installed package rather than a
562
+ // checkout) is told so before an app exists. An application that assembles
563
+ // a context in a temporary directory gets it removed however this command
564
+ // ends, since die() exits without unwinding.
565
+ const build = a.fromSource ? profile.buildContext() : null;
566
+ if (build)
567
+ process.on('exit', () => build.cleanup());
568
+ const buildRoot = build?.dir ?? null;
569
+ const image = buildRoot === null ? a.image ?? `${profile.imageRepo}:${profile.version()}` : null;
570
+ if (image !== null && a.image === null)
571
+ await requirePublishedImage(image, false);
462
572
  await requireFly();
463
573
  const existed = await appExists(app);
464
- const live = existed ? await liveSettings(app) : {};
574
+ const live = existed ? await liveSettings(app, profile) : {};
465
575
  const settings = resolveSettings(a, live);
466
576
  // A volume cannot move, so a region flag that disagrees with the volume that
467
577
  // exists is a request this cannot carry out. Saying so beats deploying a
468
578
  // machine in one region that can never attach the disk in another.
469
579
  if (existed && live.region && a.region && a.region !== live.region) {
470
- die(`This vault's volume is in ${live.region}, and a volume cannot be moved to ${a.region}.\n` +
471
- 'Deploying to another region means a new vault and copying the data across.');
580
+ die(`This ${noun}'s volume is in ${live.region}, and a volume cannot be moved to ${a.region}.\n` +
581
+ `Deploying to another region means a new ${noun} and copying the data across.`);
472
582
  }
473
583
  if (existed) {
474
584
  console.log(`==> Updating '${app}' (${appUrl(app)})`);
@@ -481,13 +591,13 @@ async function deployFlyCmd(args, usage) {
481
591
  'App names are globally unique, so a name in use by anyone stops this. Try another.');
482
592
  }
483
593
  }
484
- const vol = await vaultVolume(app);
594
+ const vol = await volumeOf(app, profile);
485
595
  if (!vol) {
486
- console.log(`==> Creating a ${settings.volumeGb}GB volume '${VOLUME_NAME}' in ${settings.region}`);
596
+ console.log(`==> Creating a ${settings.volumeGb}GB volume '${profile.volumeName}' in ${settings.region}`);
487
597
  const code = await flyStream([
488
598
  'volumes',
489
599
  'create',
490
- VOLUME_NAME,
600
+ profile.volumeName,
491
601
  '-a',
492
602
  app,
493
603
  '--region',
@@ -497,10 +607,10 @@ async function deployFlyCmd(args, usage) {
497
607
  '--yes',
498
608
  ]);
499
609
  if (code !== 0)
500
- die('\nCould not create the volume, so there is nowhere to keep the vault.');
610
+ die(`\nCould not create the volume, so there is nowhere to keep the ${noun}.`);
501
611
  }
502
612
  else if (settings.volumeGb > vol.size_gb) {
503
- console.log(`==> Extending volume '${VOLUME_NAME}' from ${vol.size_gb}GB to ${settings.volumeGb}GB`);
613
+ console.log(`==> Extending volume '${profile.volumeName}' from ${vol.size_gb}GB to ${settings.volumeGb}GB`);
504
614
  const code = await flyStream(['volumes', 'extend', vol.id, '-a', app, '--size', String(settings.volumeGb)]);
505
615
  if (code !== 0)
506
616
  die('\nCould not extend the volume.');
@@ -558,7 +668,7 @@ async function deployFlyCmd(args, usage) {
558
668
  console.log('==> Setting the one-time owner token as a Fly secret');
559
669
  // `secrets import` reads KEY=VALUE lines from stdin, which keeps the token
560
670
  // off the child's argv and so out of `ps`.
561
- const r = await fly(['secrets', 'import', '-a', app, '--stage'], `${OWNER_TOKEN_SECRET}=${ownerToken}\n`);
671
+ const r = await fly(['secrets', 'import', '-a', app, '--stage'], `${ownerTokenSecret}=${ownerToken}\n`);
562
672
  if (r.code !== 0)
563
673
  die(`Could not set the owner token secret:\n${r.stderr.trim() || r.stdout.trim()}`);
564
674
  // From here the token exists in two places: this process, and a Fly secret
@@ -576,16 +686,18 @@ async function deployFlyCmd(args, usage) {
576
686
  // deploy. The one message that must not be lost cannot go out that way.
577
687
  fs.writeSync(2, '\nThe owner token this run staged as a Fly secret, shown here because nothing\n' +
578
688
  'else has a copy of it. A Fly secret can be written but not read back, and this\n' +
579
- 'token is the owner of the vault if this app initialized one:\n' +
689
+ `token is the owner of the ${noun} if this app initialized one:\n` +
580
690
  `\n ${ownerToken}\n` +
581
- `\nKeep it, then: mochi login ${appUrl(app)} --token ${ownerToken}\n`);
691
+ `\nKeep it, then sign the CLI in with it: ${product} login ${appUrl(app)}\n` +
692
+ '(paste the token when asked; --token-stdin takes it from a pipe, and --token\n' +
693
+ 'on the command line would leave it in shell history).\n');
582
694
  });
583
695
  }
584
- const config = writeTempConfig(app, settings);
585
- // A source build runs fly in the checkout, so the build context is the checkout
586
- // and fly finds its Dockerfile without being told where it is. --config still
587
- // points at the generated fly.toml in a temporary directory, which is why there
588
- // is no fly.toml in the repository for a build to pick up by accident.
696
+ const config = writeTempConfig(app, settings, profile);
697
+ // A source build runs fly in the build context, so fly finds its Dockerfile
698
+ // without being told where it is. --config still points at the generated
699
+ // fly.toml in a temporary directory, which is why there is no fly.toml in the
700
+ // repository for a build to pick up by accident.
589
701
  //
590
702
  // Without --local-only, flyctl builds on a Fly builder machine, which needs no
591
703
  // Docker here and provisions a builder app on first use. --local-build asks for
@@ -597,17 +709,22 @@ async function deployFlyCmd(args, usage) {
597
709
  else {
598
710
  console.log(`==> Deploying ${image}`);
599
711
  }
600
- const code = await flyStream([
601
- 'deploy',
602
- '--app',
603
- app,
604
- '--config',
605
- config,
606
- ...(source ? (a.localBuild ? ['--local-only'] : []) : ['--image', image]),
607
- '--ha=false',
608
- '--yes',
609
- ], buildRoot ?? undefined);
610
- fs.rmSync(path.dirname(config), { recursive: true, force: true });
712
+ let code;
713
+ try {
714
+ code = await flyStream([
715
+ 'deploy',
716
+ '--app',
717
+ app,
718
+ '--config',
719
+ config,
720
+ ...(source ? (a.localBuild ? ['--local-only'] : []) : ['--image', image]),
721
+ '--ha=false',
722
+ '--yes',
723
+ ], buildRoot ?? undefined);
724
+ }
725
+ finally {
726
+ fs.rmSync(path.dirname(config), { recursive: true, force: true });
727
+ }
611
728
  if (code !== 0) {
612
729
  console.error('');
613
730
  console.error('The deploy failed. The app and the volume survive, so fix the cause and run the');
@@ -619,7 +736,7 @@ async function deployFlyCmd(args, usage) {
619
736
  // the exit hook is about to print it.
620
737
  console.error('');
621
738
  console.error('The retry may not mint a token of its own, because the one shown at the end of');
622
- console.error('this output is already the token the vault will be initialized with. Keep it,');
739
+ console.error(`this output is already the token the ${noun} will be initialized with. Keep it,`);
623
740
  console.error('and log in with it once a deploy succeeds.');
624
741
  }
625
742
  if (source) {
@@ -641,7 +758,7 @@ async function deployFlyCmd(args, usage) {
641
758
  if (!ownerToken) {
642
759
  console.log(`==> Deployed: ${url}`);
643
760
  console.log('');
644
- console.log('The vault it serves is whichever vault was already on the volume, users and all.');
761
+ console.log(`The ${noun} it serves is whichever ${noun} was already on the volume, users and all.`);
645
762
  // No token was minted because a machine had run before, which usually means
646
763
  // a vault that has been in use and an operator who is already logged in.
647
764
  // With nothing stored here, the other reading is possible: an earlier
@@ -650,18 +767,18 @@ async function deployFlyCmd(args, usage) {
650
767
  // case where it might be the answer.
651
768
  if (!(await (0, credentials_1.readCredential)((0, credentials_1.credentialTarget)(url)))) {
652
769
  console.log('');
653
- console.log('No token for it is stored on this machine, so log in with one the vault knows:');
770
+ console.log(`No token for it is stored on this machine, so log in with one the ${noun} knows:`);
654
771
  console.log('');
655
- console.log(` mochi login ${url}`);
772
+ console.log(` ${product} login ${url}`);
656
773
  console.log('');
657
- console.log('If an earlier deploy of this app failed, the vault was initialized just now with');
774
+ console.log(`If an earlier deploy of this app failed, the ${noun} was initialized just now with`);
658
775
  console.log('the owner token that run printed, and that is the token to use.');
659
776
  console.log('');
660
777
  }
661
778
  console.log(` fly logs -a ${app}`);
662
779
  return;
663
780
  }
664
- console.log('==> Waiting for the vault to answer');
781
+ console.log(`==> Waiting for the ${noun} to answer`);
665
782
  const ready = await waitForVault(url, ownerToken);
666
783
  if (!ready.ok) {
667
784
  console.error('');
@@ -672,10 +789,10 @@ async function deployFlyCmd(args, usage) {
672
789
  // a vault that was initialized by an earlier machine. Its own tokens are
673
790
  // still the way in, and no token minted here will ever work on it.
674
791
  console.error('');
675
- console.error('If this app has served a vault before, that vault keeps the users and tokens it');
792
+ console.error(`If this app has served a ${noun} before, that ${noun} keeps the users and tokens it`);
676
793
  console.error('already had, and a token minted now is not one of them. Log in with one of those:');
677
794
  console.error('');
678
- console.error(` mochi login ${url}`);
795
+ console.error(` ${product} login ${url}`);
679
796
  }
680
797
  // The token this run minted is printed on the way out by the exit hook,
681
798
  // since a vault that has not answered yet may still adopt it.
@@ -685,12 +802,12 @@ async function deployFlyCmd(args, usage) {
685
802
  // left the operator holding a vault whose token they had never seen, which is
686
803
  // no way to sign in to the web UI and nothing to keep anywhere; and the token
687
804
  // cannot be recovered later, since the server keeps only its hash and a Fly
688
- // secret cannot be read back. So it is printed once, with the two ways to use
689
- // it, and `mochi login` stays the one thing that stores a credential.
805
+ // secret cannot be read back. So it is printed once, with the ways to use
806
+ // it, and `login` stays the one thing that stores a credential.
690
807
  console.log('');
691
808
  console.log(`==> Ready: ${url}`);
692
809
  console.log('');
693
- console.log(`The vault is initialized, and '${ready.username}' owns it. This is its token, shown`);
810
+ console.log(`The ${noun} is initialized, and '${ready.username}' owns it. This is its token, shown`);
694
811
  console.log('here once and nowhere else: the server keeps only its hash, and the Fly secret it');
695
812
  console.log('was staged in cannot be read back. Keep it somewhere safe now.');
696
813
  console.log('');
@@ -699,56 +816,40 @@ async function deployFlyCmd(args, usage) {
699
816
  // operator, and stdout can fail (a closed pipe) between here and there.
700
817
  tokenDelivered = true;
701
818
  console.log('');
702
- console.log(`To administer the vault in a browser, open its sign-in page and give that token as`);
703
- console.log(`'${ready.username}':`);
704
- console.log('');
705
- console.log(` ${url}/login`);
706
- console.log('');
707
- console.log('The form asks for a username and a token, since a vault has no passwords. From');
708
- console.log('there the Admin page creates the users and the repositories, which is the usual');
709
- console.log('way to bootstrap a fresh vault.');
710
- console.log('');
711
- console.log('To use the CLI and git instead, hand the same token to git\'s credential store,');
712
- console.log('which is what login is for:');
713
- console.log('');
714
- console.log(` mochi login ${url}`);
715
- console.log('');
716
- console.log('It asks for the token without echoing it, checks it, and remembers this vault, so');
717
- console.log('these need no arguments afterwards and git stops asking on a push:');
718
- console.log('');
719
- console.log(' mochi whoami');
720
- console.log(" mochi user add alice --scope 'alice/*'");
721
- console.log(` mochi import https://github.com/someone/something.git mine`);
819
+ for (const line of profile.firstSteps(url, ready.username))
820
+ console.log(line);
722
821
  console.log('');
723
822
  console.log('Deploy an update, or change a setting, with the same command:');
724
823
  console.log('');
725
- console.log(` mochi deploy fly ${app}`);
726
- console.log(` mochi deploy fly ${app} --volume 50 --vm-memory 1gb`);
824
+ console.log(` ${product} deploy fly ${app}`);
825
+ console.log(` ${product} deploy fly ${app} --volume 50 --vm-memory 1gb`);
727
826
  }
728
- async function deployShowCmd(args, usage) {
827
+ async function deployShowCmd(args, usage, profile = exports.MOCHI_DEPLOY) {
828
+ const product = naming_1.naming.product;
829
+ const noun = naming_1.naming.rootNoun;
729
830
  const a = parseDeployArgs(args, usage);
730
831
  if (!a.app)
731
- die('Which app? Usage: mochi deploy fly show <app>');
732
- rejectFlyFlags(a, 'mochi deploy fly show <app>', false);
832
+ die(`Which app? Usage: ${product} deploy fly show <app>`);
833
+ rejectFlyFlags(a, `${product} deploy fly show <app>`, false);
733
834
  const app = a.app;
734
835
  await requireFly();
735
836
  if (!(await appExists(app))) {
736
837
  die(`No Fly app named '${app}' that you can see. Check the name, or: fly apps list`);
737
838
  }
738
839
  const url = appUrl(app);
739
- const vol = await vaultVolume(app);
840
+ const vol = await volumeOf(app, profile);
740
841
  const ms = await machines(app);
741
842
  const secrets = await secretNames(app);
742
843
  console.log(`${app} ${url}`);
743
844
  console.log('');
744
845
  if (ms.length === 0) {
745
- console.log(' machines none, so nothing is serving this vault');
846
+ console.log(` machines none, so nothing is serving this ${noun}`);
746
847
  }
747
848
  else {
748
849
  // More than one machine is worth naming rather than summarizing: it means
749
850
  // two volumes and two vaults, which is the failure --ha=false prevents.
750
851
  if (ms.length > 1)
751
- console.log(` machines ${ms.length}, which is one too many for a single-volume vault`);
852
+ console.log(` machines ${ms.length}, which is one too many for a single-volume ${noun}`);
752
853
  for (const m of ms) {
753
854
  const g = m.config?.guest;
754
855
  const shape = g ? `${g.cpu_kind}-cpu-${g.cpus}x, ${g.memory_mb}mb` : 'unknown shape';
@@ -757,34 +858,37 @@ async function deployShowCmd(args, usage) {
757
858
  }
758
859
  }
759
860
  console.log(vol ? ` volume ${vol.size_gb}GB in ${vol.region} (${vol.state ?? 'created'})` : ' volume none');
760
- console.log(` lfs ${secrets.includes('BUCKET_NAME') ? 'objects in a bucket (BUCKET_NAME is set)' : 'objects on the volume'}`);
861
+ if (profile.lfsBucket) {
862
+ console.log(` lfs ${secrets.includes('BUCKET_NAME') ? 'objects in a bucket (BUCKET_NAME is set)' : 'objects on the volume'}`);
863
+ }
761
864
  // Whether it works, which is the question `fly status` cannot answer. A
762
865
  // stored credential turns this into a report of who you are on it.
763
866
  const target = (0, credentials_1.credentialTarget)(url);
764
867
  const stored = await (0, credentials_1.readCredential)(target);
765
- let vault = 'not reachable';
868
+ let status = 'not reachable';
766
869
  try {
767
870
  const resp = await fetch(`${url}/api/whoami`, {
768
871
  headers: stored ? { authorization: `Bearer ${stored.password}` } : {},
769
872
  });
770
873
  if (resp.ok) {
771
874
  const data = (await resp.json());
772
- vault = `answering, and you are '${String(data.username)}' on it`;
875
+ status = `answering, and you are '${String(data.username)}' on it`;
773
876
  }
774
877
  else if (resp.status === 401) {
775
- vault = stored ? 'answering, but your stored token is not valid on it' : 'answering (no token stored here)';
878
+ status = stored ? 'answering, but your stored token is not valid on it' : 'answering (no token stored here)';
776
879
  }
777
880
  else {
778
- vault = `answering with HTTP ${resp.status}`;
881
+ status = `answering with HTTP ${resp.status}`;
779
882
  }
780
883
  }
781
884
  catch (e) {
782
- vault = `not reachable: ${e instanceof Error ? e.message : e}`;
885
+ status = `not reachable: ${e instanceof Error ? e.message : e}`;
783
886
  }
784
- console.log(` vault ${vault}`);
887
+ console.log(` ${noun.padEnd(9)} ${status}`);
785
888
  const saved = (0, credentials_1.loadLogin)();
786
- if (saved && saved.host.replace(/\/+$/, '') === url)
787
- console.log(' login this is the vault mochi commands use');
889
+ if (saved && saved.host.replace(/\/+$/, '') === url) {
890
+ console.log(` login this is the ${noun} ${product} commands use`);
891
+ }
788
892
  // Fly's own volume snapshots live at the same provider as the volume, so they
789
893
  // are a complement to a backup on a disk of your own rather than a substitute
790
894
  // for one. Whether this machine keeps such a copy is worth one line.
@@ -793,7 +897,7 @@ async function deployShowCmd(args, usage) {
793
897
  // certainly backed up by that name rather than by <app>.fly.dev, and a report
794
898
  // that said "none" in that case would be worse than no report at all.
795
899
  const backup = (0, backup_cmd_1.backupLineFor)([url, ...(await certHostnames(app)).map((h) => `https://${h}`)]);
796
- console.log(backup ? ` backup ${backup}` : ' backup none on this machine (mochi backup <dir>)');
900
+ console.log(backup ? ` backup ${backup}` : ` backup none on this machine (${product} backup <dir>)`);
797
901
  console.log('');
798
902
  console.log(` fly logs -a ${app}`);
799
903
  }
@@ -814,21 +918,22 @@ function promptLine(prompt) {
814
918
  process.stdin.on('data', onData);
815
919
  });
816
920
  }
817
- async function deployDestroyCmd(args, usage) {
921
+ async function deployDestroyCmd(args, usage, profile = exports.MOCHI_DEPLOY) {
922
+ const product = naming_1.naming.product;
818
923
  const a = parseDeployArgs(args, usage);
819
924
  if (!a.app)
820
- die('Which app? Usage: mochi deploy fly destroy <app> [--yes]');
821
- rejectFlyFlags(a, 'mochi deploy fly destroy <app> [--yes]', true);
925
+ die(`Which app? Usage: ${product} deploy fly destroy <app> [--yes]`);
926
+ rejectFlyFlags(a, `${product} deploy fly destroy <app> [--yes]`, true);
822
927
  const app = a.app;
823
928
  await requireFly();
824
929
  if (!(await appExists(app))) {
825
930
  die(`No Fly app named '${app}' that you can see. Check the name, or: fly apps list`);
826
931
  }
827
- const vol = await vaultVolume(app);
828
- const hadBucket = (await secretNames(app)).includes('BUCKET_NAME');
932
+ const vol = await volumeOf(app, profile);
933
+ const hadBucket = profile.lfsBucket && (await secretNames(app)).includes('BUCKET_NAME');
829
934
  if (!a.yes) {
830
935
  console.log(`This destroys the Fly app '${app}' and its ${vol ? `${vol.size_gb}GB ` : ''}volume.`);
831
- console.log('Everything in the vault goes with it: repositories, issues, pull requests, users.');
936
+ console.log(`Everything in the ${naming_1.naming.rootNoun} goes with it: ${profile.contents}.`);
832
937
  console.log('There is no undo, and Fly keeps no backup of a destroyed volume.');
833
938
  console.log('');
834
939
  const answer = await promptLine(`Type the app name to confirm: `);