@metabase/rde 0.1.14 → 0.1.16

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 (3) hide show
  1. package/README.md +41 -53
  2. package/dist/cli.mjs +694 -856
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -50,6 +50,7 @@ var StepFailedError = class extends Error {
50
50
  var ExternalToolError = class extends Error {
51
51
  tool;
52
52
  argv;
53
+ envNames;
53
54
  exitCode;
54
55
  stderrTail;
55
56
  constructor(failure) {
@@ -57,15 +58,21 @@ var ExternalToolError = class extends Error {
57
58
  this.name = "ExternalToolError";
58
59
  this.tool = failure.tool;
59
60
  this.argv = failure.argv;
61
+ this.envNames = failure.envNames;
60
62
  this.exitCode = failure.exitCode;
61
63
  this.stderrTail = failure.stderrTail;
62
64
  }
63
65
  };
66
+ function commandLabel(failure) {
67
+ const command = `\`${[failure.tool, ...failure.argv].join(" ")}\``;
68
+ if (failure.envNames.length === 0) return command;
69
+ return `${command}, run by rde with ${failure.envNames.join(" and ")} set,`;
70
+ }
64
71
  function describeToolFailure(failure) {
65
- const command = [failure.tool, ...failure.argv].join(" ");
72
+ const command = commandLabel(failure);
66
73
  const exit = failure.exitCode === null ? "was killed" : `exited with ${failure.exitCode}`;
67
74
  const tail = failure.stderrTail.trim();
68
- return tail.length === 0 ? `\`${command}\` ${exit}` : `\`${command}\` ${exit}: ${tail}`;
75
+ return tail.length === 0 ? `${command} ${exit}` : `${command} ${exit}: ${tail}`;
69
76
  }
70
77
  var MetabaseApiError = class extends Error {
71
78
  status;
@@ -103,6 +110,9 @@ var MetabaseUnreachableError = class extends Error {
103
110
  this.reason = reason;
104
111
  }
105
112
  };
113
+ function isMetabaseNotServing(error) {
114
+ return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
115
+ }
106
116
  var DownloadError = class extends Error {
107
117
  url;
108
118
  constructor(url, reason) {
@@ -113,12 +123,10 @@ var DownloadError = class extends Error {
113
123
  };
114
124
  //#endregion
115
125
  //#region src/output/error.ts
116
- const INTERRUPTED_MESSAGE = "Interrupted. Run `rde init` again to resume.";
117
126
  const FAILURE_EXIT_CODE = 1;
118
127
  const USAGE_EXIT_CODE = 2;
119
128
  function exitCodeFor(error) {
120
129
  if (error instanceof StepFailedError) return exitCodeFor(error.cause);
121
- if (error instanceof AbortError) return 130;
122
130
  if (error instanceof ConfigError) return USAGE_EXIT_CODE;
123
131
  return FAILURE_EXIT_CODE;
124
132
  }
@@ -130,7 +138,6 @@ function stepFailureMessage(step, cause) {
130
138
  }
131
139
  function messageFor$1(error) {
132
140
  if (error instanceof StepFailedError) return stepFailureMessage(error.step, messageFor$1(error.cause));
133
- if (error instanceof AbortError) return INTERRUPTED_MESSAGE;
134
141
  if (error instanceof Error) return error.message;
135
142
  return String(error);
136
143
  }
@@ -138,10 +145,15 @@ function reportError(error) {
138
145
  process.stderr.write(messageFor$1(error) + "\n");
139
146
  process.exitCode = exitCodeFor(error);
140
147
  }
148
+ function reportInterrupted(message) {
149
+ if (message !== null) process.stderr.write(message + "\n");
150
+ process.exitCode = 130;
151
+ }
141
152
  //#endregion
142
153
  //#region src/commands/define.ts
143
154
  const SHORT_PREFIX = "-";
144
155
  const END_OF_OPTIONS = "--";
156
+ const INTERRUPTED = "Interrupted.";
145
157
  function repeatedValues(rawArgs, flag) {
146
158
  const values = [];
147
159
  const inline = `${flag}=`;
@@ -164,9 +176,14 @@ function defineRdeCommand(def) {
164
176
  meta: def.meta,
165
177
  ...def.args !== void 0 && { args: def.args },
166
178
  async run({ args, rawArgs }) {
179
+ const interruption = { message: def.interrupted };
167
180
  try {
168
- await def.run(args, rawArgs);
181
+ await def.run(args, rawArgs, interruption);
169
182
  } catch (error) {
183
+ if (error instanceof AbortError) {
184
+ reportInterrupted(interruption.message);
185
+ return;
186
+ }
170
187
  reportError(error);
171
188
  }
172
189
  }
@@ -245,7 +262,6 @@ const StepId = z.enum([
245
262
  "admin-user",
246
263
  "api-key",
247
264
  "transforms",
248
- "sample-database",
249
265
  "license",
250
266
  "git-sync",
251
267
  "databases",
@@ -322,12 +338,6 @@ const DatabaseRecord = z.object({
322
338
  engine: z.string(),
323
339
  createdAt: z.string()
324
340
  });
325
- const SampleDatabaseState = z.object({
326
- containerName: z.string(),
327
- image: z.string(),
328
- port: z.int().positive(),
329
- databaseId: z.int().nullable()
330
- });
331
341
  const GitRemoteState = z.discriminatedUnion("kind", [z.object({
332
342
  kind: z.literal("local-bare"),
333
343
  barePath: z.string(),
@@ -344,7 +354,7 @@ const StepRecord = z.object({
344
354
  completedAt: z.string()
345
355
  });
346
356
  const InstanceState = z.object({
347
- schemaVersion: z.literal(5),
357
+ schemaVersion: z.literal(6),
348
358
  name: z.string(),
349
359
  runtime: RuntimeState.nullable(),
350
360
  url: z.string().nullable(),
@@ -353,7 +363,6 @@ const InstanceState = z.object({
353
363
  mbProfile: z.string(),
354
364
  license: LicenseState,
355
365
  databases: z.array(DatabaseRecord),
356
- sampleDatabase: SampleDatabaseState.nullable(),
357
366
  gitRemote: GitRemoteState.nullable(),
358
367
  movedFrom: z.string().nullable(),
359
368
  steps: z.partialRecord(StepId, StepRecord)
@@ -385,7 +394,7 @@ function mbCommand(profile) {
385
394
  }
386
395
  function emptyInstanceState(name) {
387
396
  return {
388
- schemaVersion: 5,
397
+ schemaVersion: 6,
389
398
  name,
390
399
  runtime: null,
391
400
  url: null,
@@ -394,7 +403,6 @@ function emptyInstanceState(name) {
394
403
  mbProfile: mbProfileFor(name),
395
404
  license: { activated: false },
396
405
  databases: [],
397
- sampleDatabase: null,
398
406
  gitRemote: null,
399
407
  movedFrom: null,
400
408
  steps: {}
@@ -407,6 +415,7 @@ const V1 = 1;
407
415
  const V2 = 2;
408
416
  const V3 = 3;
409
417
  const V4 = 4;
418
+ const V5 = 5;
410
419
  const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
411
420
  const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
412
421
  const AdminV3 = z.discriminatedUnion("kind", [z.object({
@@ -418,6 +427,7 @@ const AdminV3 = z.discriminatedUnion("kind", [z.object({
418
427
  password: z.string()
419
428
  })]);
420
429
  const StateV4 = z.object({ schemaVersion: z.literal(V4) }).loose();
430
+ const StateV5 = z.object({ schemaVersion: z.literal(V5) }).loose();
421
431
  const StateV3 = z.object({
422
432
  schemaVersion: z.literal(V3),
423
433
  admin: AdminV3.nullable(),
@@ -433,17 +443,23 @@ function liftV1(raw) {
433
443
  function liftV2(raw) {
434
444
  return {
435
445
  ...StateV2.parse(raw),
436
- schemaVersion: V3,
437
- sampleDatabase: null
446
+ schemaVersion: V3
438
447
  };
439
448
  }
440
449
  function liftV4(raw) {
441
450
  return {
442
451
  ...StateV4.parse(raw),
443
- schemaVersion: 5,
452
+ schemaVersion: V5,
444
453
  movedFrom: null
445
454
  };
446
455
  }
456
+ function liftV5(raw) {
457
+ const { sampleDatabase: _sampleDatabase, ...state } = StateV5.parse(raw);
458
+ return {
459
+ ...state,
460
+ schemaVersion: 6
461
+ };
462
+ }
447
463
  function adminV4(admin) {
448
464
  switch (admin.kind) {
449
465
  case "default": return {
@@ -463,11 +479,11 @@ function liftV3(raw) {
463
479
  password: admin.password
464
480
  } : null;
465
481
  return {
466
- state: InstanceState.parse(liftV4({
482
+ state: migrateInstanceState(liftV4({
467
483
  ...rest,
468
484
  schemaVersion: V4,
469
485
  admin: admin === null ? null : adminV4(admin)
470
- })),
486
+ })).state,
471
487
  movedSecrets: {
472
488
  schemaVersion: 1,
473
489
  admin: login,
@@ -481,15 +497,16 @@ function migrateInstanceState(raw) {
481
497
  case V1: return migrateInstanceState(liftV1(raw));
482
498
  case V2: return migrateInstanceState(liftV2(raw));
483
499
  case V3: return liftV3(raw);
484
- case V4: return {
485
- state: InstanceState.parse(liftV4(raw)),
500
+ case V4: return migrateInstanceState(liftV4(raw));
501
+ case V5: return {
502
+ state: InstanceState.parse(liftV5(raw)),
486
503
  movedSecrets: null
487
504
  };
488
- case 5: return {
505
+ case 6: return {
489
506
  state: InstanceState.parse(raw),
490
507
  movedSecrets: null
491
508
  };
492
- default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 5)`);
509
+ default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 6)`);
493
510
  }
494
511
  }
495
512
  //#endregion
@@ -1348,9 +1365,6 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
1348
1365
  user: userResource(http)
1349
1366
  };
1350
1367
  }
1351
- function isNotReadyYet(error) {
1352
- return error instanceof MetabaseInitializingError || error instanceof MetabaseUnreachableError;
1353
- }
1354
1368
  async function waitForHealth(client, options) {
1355
1369
  const deadline = Date.now() + options.timeoutMs;
1356
1370
  const interval = options.pollIntervalMs ?? HEALTH_POLL_INTERVAL_MS;
@@ -1359,7 +1373,7 @@ async function waitForHealth(client, options) {
1359
1373
  await client.health.get();
1360
1374
  return;
1361
1375
  } catch (error) {
1362
- if (!isNotReadyYet(error)) throw error;
1376
+ if (!isMetabaseNotServing(error)) throw error;
1363
1377
  if (error instanceof MetabaseInitializingError) options.onProgress(error.progress);
1364
1378
  }
1365
1379
  if (!await options.isRunning()) throw new MetabaseUnreachableError(client.baseUrl, "it exited before it became healthy");
@@ -1386,15 +1400,12 @@ function storedApiKey(state, secrets) {
1386
1400
  function hostCredentialsClient(signal) {
1387
1401
  return (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal);
1388
1402
  }
1389
- function isNotReady(error) {
1390
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
1391
- }
1392
1403
  async function hasAdminOutsideRde(state, client) {
1393
1404
  if (state.url === null) return false;
1394
1405
  try {
1395
1406
  return (await client(state.url, NO_AUTH).session.properties())["has-user-setup"];
1396
1407
  } catch (error) {
1397
- if (isNotReady(error)) return false;
1408
+ if (isMetabaseNotServing(error)) return false;
1398
1409
  throw error;
1399
1410
  }
1400
1411
  }
@@ -1475,6 +1486,7 @@ const credentials = defineRdeCommand({
1475
1486
  default: false
1476
1487
  }
1477
1488
  },
1489
+ interrupted: INTERRUPTED,
1478
1490
  async run(args) {
1479
1491
  const name = await defaultInstanceName();
1480
1492
  const state = await requireInstanceState(name);
@@ -1830,10 +1842,11 @@ function signalProcess(pid, signal) {
1830
1842
  function stderrTail(stderr) {
1831
1843
  return stderr.trim().split("\n").slice(-5).join("\n");
1832
1844
  }
1833
- function toolFailure(tool, argv, result) {
1845
+ function toolFailure(tool, argv, result, options = {}) {
1834
1846
  return new ExternalToolError({
1835
1847
  tool,
1836
1848
  argv,
1849
+ envNames: options.addedEnv === void 0 ? [] : Object.keys(options.addedEnv),
1837
1850
  exitCode: result.exitCode,
1838
1851
  stderrTail: stderrTail(result.stderr)
1839
1852
  });
@@ -1845,6 +1858,7 @@ async function runShown(runner, output, command, args) {
1845
1858
  if (exitCode !== 0) throw new ExternalToolError({
1846
1859
  tool: command,
1847
1860
  argv: args,
1861
+ envNames: [],
1848
1862
  exitCode,
1849
1863
  stderrTail: ""
1850
1864
  });
@@ -1856,6 +1870,7 @@ async function runShown(runner, output, command, args) {
1856
1870
  throw new ExternalToolError({
1857
1871
  tool: command,
1858
1872
  argv: args,
1873
+ envNames: [],
1859
1874
  exitCode: result.exitCode,
1860
1875
  stderrTail: stderrTail(printed.filter((line) => HAS_TEXT.test(line)).join("\n"))
1861
1876
  });
@@ -1869,6 +1884,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
1869
1884
  throw new ExternalToolError({
1870
1885
  tool,
1871
1886
  argv,
1887
+ envNames: [],
1872
1888
  exitCode: 0,
1873
1889
  stderrTail: `output is not JSON: ${error instanceof Error ? error.message : String(error)}`
1874
1890
  });
@@ -1879,6 +1895,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
1879
1895
  if (error instanceof ZodError) throw new ExternalToolError({
1880
1896
  tool,
1881
1897
  argv,
1898
+ envNames: [],
1882
1899
  exitCode: 0,
1883
1900
  stderrTail: `unexpected output shape:\n${z.prettifyError(error)}`
1884
1901
  });
@@ -2179,10 +2196,10 @@ function createArgs(options) {
2179
2196
  "--label",
2180
2197
  `${IMAGE_LABEL}=${options.image}`,
2181
2198
  "-p",
2182
- `${LOOPBACK_HOST}:${options.port.host ?? ""}:${options.port.container}`
2199
+ `${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`,
2200
+ "-v",
2201
+ `${options.mount.source}:${options.mount.target}`
2183
2202
  ];
2184
- if (options.port.alsoOn !== null) args.push("-p", `${options.port.alsoOn}:${options.port.host ?? ""}:${options.port.container}`);
2185
- if (options.mount !== null) args.push("-v", `${options.mount.source}:${options.mount.target}`);
2186
2203
  for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
2187
2204
  if (options.hostUser !== null) {
2188
2205
  args.push("-e", `${ENV_HOST_UID}=${options.hostUser.uid}`);
@@ -2241,17 +2258,6 @@ function dockerTool(runner) {
2241
2258
  if (NO_SUCH_IMAGE.test(result.stderr)) return null;
2242
2259
  throw toolFailure(DOCKER, args, result);
2243
2260
  }
2244
- async function bridgeGateway() {
2245
- const gateway = (await docker([
2246
- "network",
2247
- "inspect",
2248
- "bridge",
2249
- "--format",
2250
- "{{range .IPAM.Config}}{{.Gateway}}{{end}}"
2251
- ])).stdout.trim();
2252
- if (gateway.length === 0) throw new ConfigError("`docker network inspect bridge` names no gateway address");
2253
- return gateway;
2254
- }
2255
2261
  async function imageRemove(image) {
2256
2262
  await docker(["rmi", image]);
2257
2263
  }
@@ -2328,33 +2334,6 @@ function dockerTool(runner) {
2328
2334
  name
2329
2335
  ]);
2330
2336
  }
2331
- async function containerLoopbackPort(name, containerPort) {
2332
- const args = [
2333
- "port",
2334
- name,
2335
- `${containerPort}/tcp`
2336
- ];
2337
- const result = await docker(args);
2338
- const prefix = `${LOOPBACK_HOST}:`;
2339
- const line = result.stdout.split("\n").find((entry) => entry.startsWith(prefix));
2340
- if (line !== void 0) {
2341
- const port = Number.parseInt(line.slice(prefix.length), 10);
2342
- if (Number.isInteger(port)) return port;
2343
- }
2344
- throw new ExternalToolError({
2345
- tool: DOCKER,
2346
- argv: args,
2347
- exitCode: 0,
2348
- stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
2349
- });
2350
- }
2351
- async function containerExecSucceeds(name, command) {
2352
- return (await runner.run(DOCKER, [
2353
- "exec",
2354
- name,
2355
- ...command
2356
- ])).exitCode === 0;
2357
- }
2358
2337
  async function containerLogs(name, options) {
2359
2338
  const args = ["logs"];
2360
2339
  if (options.follow) args.push("--follow");
@@ -2367,7 +2346,6 @@ function dockerTool(runner) {
2367
2346
  imagePull,
2368
2347
  imageId,
2369
2348
  imageRemove,
2370
- bridgeGateway,
2371
2349
  containerInspect,
2372
2350
  containerImageId,
2373
2351
  portClaims,
@@ -2377,8 +2355,6 @@ function dockerTool(runner) {
2377
2355
  containerRestart,
2378
2356
  containerRemove,
2379
2357
  containerForceRemove,
2380
- containerLoopbackPort,
2381
- containerExecSucceeds,
2382
2358
  containerLogs
2383
2359
  };
2384
2360
  }
@@ -2556,8 +2532,7 @@ function dockerRuntime(state, deps) {
2556
2532
  imageId,
2557
2533
  port: {
2558
2534
  host: state.port,
2559
- container: CONTAINER_PORT,
2560
- alsoOn: null
2535
+ container: CONTAINER_PORT
2561
2536
  },
2562
2537
  mount: {
2563
2538
  source: deps.dataDir,
@@ -2871,148 +2846,21 @@ function runtimeFromState(state, deps) {
2871
2846
  }
2872
2847
  }
2873
2848
  //#endregion
2874
- //#region src/runtime/sample-database.ts
2875
- const SAMPLE_DATABASE_IMAGE = "metabase/qa-databases:postgres-sample-15";
2876
- const SAMPLE_DATABASE_USER = "metabase";
2877
- const SAMPLE_DATABASE_PASSWORD = "metasample123";
2878
- const SAMPLE_DATABASE_NAME = "sample";
2879
- const SAMPLE_DATABASE_PORT = 25432;
2880
- const SAMPLE_DATABASE_READY_TIMEOUT_MS = 12e4;
2881
- const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
2882
- const POSTGRES_PORT = 5432;
2849
+ //#region src/runtime/leftover-sample-database.ts
2883
2850
  const CONTAINER_NAME_PREFIX = "rde-sampledb";
2884
- const READY_POLL_MS = 1e3;
2885
- const READY_PROBE = [
2886
- "pg_isready",
2887
- "-h",
2888
- LOOPBACK_HOST,
2889
- "-U",
2890
- SAMPLE_DATABASE_USER,
2891
- "-d",
2892
- SAMPLE_DATABASE_NAME
2893
- ];
2894
- function sampleContainerName(instanceName, home) {
2851
+ const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
2852
+ function sampleDatabaseContainerName(instanceName, home) {
2895
2853
  const tag = homeTag(home);
2896
2854
  const name = `${CONTAINER_NAME_PREFIX}-${instanceName}`;
2897
2855
  return tag === null ? name : `${name}-${tag}`;
2898
2856
  }
2899
- function sampleDatabaseUrl(port) {
2900
- return `postgres://${SAMPLE_DATABASE_USER}:${SAMPLE_DATABASE_PASSWORD}@localhost:${port}/${SAMPLE_DATABASE_NAME}`;
2901
- }
2902
- function publishesOnLoopback$1(inspection, port) {
2903
- return inspection.ports.some((mapping) => mapping.container === POSTGRES_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
2904
- }
2905
- function createOptions(state, home, hostPort, alsoOn) {
2906
- return {
2907
- name: state.containerName,
2908
- home,
2909
- image: state.image,
2910
- imageId: null,
2911
- port: {
2912
- host: hostPort,
2913
- container: POSTGRES_PORT,
2914
- alsoOn
2915
- },
2916
- mount: null,
2917
- env: {},
2918
- addHostGateway: false,
2919
- hostUser: null
2920
- };
2921
- }
2922
- function requireOwnContainer(name, existing, home) {
2923
- if (existing.home !== null && existing.home !== home) throw new ConfigError(`container ${name} belongs to the rde home ${existing.home} and is left alone`);
2924
- }
2925
- function sampleDatabaseContainer(state, docker, home) {
2926
- const name = state.containerName;
2927
- function conflict(reason) {
2928
- return new ConfigError(`container ${name} already exists ${reason}; remove it with \`docker rm -f ${name}\` and run \`rde init\` again`);
2929
- }
2930
- async function install(options) {
2931
- const existing = await docker.containerInspect(name);
2932
- if (existing.exists) {
2933
- if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
2934
- if (publishesOnLoopback$1(existing, state.port)) return;
2935
- requireOwnContainer(name, existing, home);
2936
- await docker.containerForceRemove(name);
2937
- }
2938
- await docker.imagePull(state.image, { onLine: options.onProgress });
2939
- await docker.containerCreate(createOptions(state, home, state.port, options.alsoOn));
2940
- }
2941
- async function start() {
2942
- const existing = await docker.containerInspect(name);
2943
- if (!existing.exists) return "missing";
2944
- if (existing.running) return "running";
2945
- await docker.containerStart(name);
2946
- return "started";
2947
- }
2948
- async function stop() {
2949
- const existing = await docker.containerInspect(name);
2950
- if (existing.exists && existing.running) await docker.containerStop(name, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
2951
- }
2952
- async function status() {
2953
- const existing = await docker.containerInspect(name);
2954
- return {
2955
- installed: existing.exists,
2956
- running: existing.exists && existing.running
2957
- };
2958
- }
2959
- async function waitReady(options) {
2960
- const deadline = Date.now() + options.timeoutMs;
2961
- const interval = options.pollMs ?? READY_POLL_MS;
2962
- for (;;) {
2963
- if (await docker.containerExecSucceeds(name, READY_PROBE)) return;
2964
- const existing = await docker.containerInspect(name);
2965
- if (!existing.exists || !existing.running) throw new ConfigError(`container ${name} stopped before Postgres was ready; \`docker logs ${name}\` shows why`);
2966
- if (Date.now() >= deadline) throw new ConfigError(`Postgres in ${name} is not ready after ${Math.round(options.timeoutMs / 1e3)}s; \`docker logs ${name}\` shows what it is doing`);
2967
- try {
2968
- await setTimeout$1(interval, void 0, { signal: options.signal });
2969
- } catch {
2970
- throw new AbortError();
2971
- }
2972
- }
2973
- }
2974
- return {
2975
- install,
2976
- start,
2977
- stop,
2978
- status,
2979
- waitReady
2980
- };
2981
- }
2982
- function extraAddress(inspection) {
2983
- const extra = inspection.ports.find((mapping) => mapping.container === POSTGRES_PORT && mapping.hostIp !== "127.0.0.1");
2984
- return extra === void 0 ? null : extra.hostIp;
2985
- }
2986
- async function startOnDockerPort(state, docker, home, alsoOn) {
2987
- const name = state.containerName;
2988
- await docker.containerForceRemove(name);
2989
- await docker.containerCreate(createOptions(state, home, null, null));
2990
- await docker.containerStart(name);
2991
- const port = await docker.containerLoopbackPort(name, POSTGRES_PORT);
2857
+ async function removeLeftoverSampleDatabase(docker, instanceName, home) {
2858
+ if (!(await docker.available()).available) return null;
2859
+ const name = sampleDatabaseContainerName(instanceName, home);
2860
+ const existing = await docker.containerInspect(name);
2861
+ if (!existing.exists || existing.home !== null && existing.home !== home) return null;
2992
2862
  await docker.containerForceRemove(name);
2993
- const pinned = {
2994
- ...state,
2995
- port
2996
- };
2997
- await docker.containerCreate(createOptions(pinned, home, port, alsoOn));
2998
- await docker.containerStart(name);
2999
- return pinned;
3000
- }
3001
- async function startOnFreePort(state, docker, home) {
3002
- try {
3003
- return {
3004
- outcome: await sampleDatabaseContainer(state, docker, home).start(),
3005
- state
3006
- };
3007
- } catch (error) {
3008
- if (!isPortTakenError(error)) throw error;
3009
- }
3010
- const existing = await docker.containerInspect(state.containerName);
3011
- if (existing.exists) requireOwnContainer(state.containerName, existing, home);
3012
- return {
3013
- outcome: "started",
3014
- state: await startOnDockerPort(state, docker, home, existing.exists ? extraAddress(existing) : null)
3015
- };
2863
+ return name;
3016
2864
  }
3017
2865
  //#endregion
3018
2866
  //#region src/steps/lifecycle.ts
@@ -3023,10 +2871,14 @@ async function loadInstance(name, signal) {
3023
2871
  return {
3024
2872
  state,
3025
2873
  url: configured.url,
3026
- runtime: runtimeFromState(configured.runtime, deps),
3027
- sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker, deps.rdeHome)
2874
+ runtime: runtimeFromState(configured.runtime, deps)
3028
2875
  };
3029
2876
  }
2877
+ async function removeLeftovers(name, signal) {
2878
+ const deps = hostRuntimeDeps(name, signal);
2879
+ const removed = await removeLeftoverSampleDatabase(deps.docker, name, deps.rdeHome);
2880
+ return removed === null ? null : `Removed container ${removed}, the sample database of an older rde`;
2881
+ }
3030
2882
  async function isInstanceRunning(state, runtime) {
3031
2883
  const configured = configuredRuntime(state);
3032
2884
  if (configured === null) return false;
@@ -3035,44 +2887,29 @@ async function isInstanceRunning(state, runtime) {
3035
2887
  function openInstance(url) {
3036
2888
  return openBrowser(url);
3037
2889
  }
3038
- function ownContainers(state) {
3039
- const names = /* @__PURE__ */ new Set();
3040
- if (state.runtime?.kind === "docker") names.add(state.runtime.containerName);
3041
- if (state.sampleDatabase !== null) names.add(state.sampleDatabase.containerName);
3042
- return names;
3043
- }
3044
- function instanceClaims(other) {
3045
- const claims = [];
3046
- if (other.runtime !== null) claims.push({
3047
- port: other.runtime.port,
3048
- holder: `rde instance ${other.name}`
3049
- });
3050
- if (other.sampleDatabase !== null) claims.push({
3051
- port: other.sampleDatabase.port,
3052
- holder: `the sample database of rde instance ${other.name}`
3053
- });
3054
- return claims;
3055
- }
3056
2890
  async function portClaims(state, runtime) {
3057
2891
  const claims = [];
3058
2892
  if ((await runtime.docker.available()).available) {
3059
- const own = ownContainers(state);
3060
- for (const claim of await runtime.docker.portClaims()) if (!own.has(claim.container)) claims.push({
2893
+ const own = state.runtime?.kind === "docker" ? state.runtime.containerName : null;
2894
+ for (const claim of await runtime.docker.portClaims()) if (claim.container !== own) claims.push({
3061
2895
  port: claim.port,
3062
2896
  holder: `container ${claim.container}`
3063
2897
  });
3064
2898
  }
3065
- for (const other of await runtime.instances()) if (other.name !== state.name) claims.push(...instanceClaims(other));
2899
+ for (const other of await runtime.instances()) if (other.name !== state.name && other.runtime !== null) claims.push({
2900
+ port: other.runtime.port,
2901
+ holder: `rde instance ${other.name}`
2902
+ });
3066
2903
  return claims;
3067
2904
  }
3068
2905
  //#endregion
3069
2906
  //#region src/steps/admin-user.ts
3070
- const STEP_ID$10 = "admin-user";
2907
+ const STEP_ID$8 = "admin-user";
3071
2908
  const ADMIN_USER_KEYS = {
3072
- email: `${STEP_ID$10}.email`,
3073
- password: `${STEP_ID$10}.password`,
3074
- firstName: `${STEP_ID$10}.first-name`,
3075
- lastName: `${STEP_ID$10}.last-name`
2909
+ email: `${STEP_ID$8}.email`,
2910
+ password: `${STEP_ID$8}.password`,
2911
+ firstName: `${STEP_ID$8}.first-name`,
2912
+ lastName: `${STEP_ID$8}.last-name`
3076
2913
  };
3077
2914
  const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
3078
2915
  const GENERATED_PASSWORD_BYTES = 24;
@@ -3087,7 +2924,7 @@ function generatePassword(random) {
3087
2924
  if (DIGIT.test(candidate)) return candidate;
3088
2925
  }
3089
2926
  }
3090
- function hostDeps$14(ctx) {
2927
+ function hostDeps$13(ctx) {
3091
2928
  return {
3092
2929
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3093
2930
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
@@ -3095,7 +2932,7 @@ function hostDeps$14(ctx) {
3095
2932
  };
3096
2933
  }
3097
2934
  function isNotMetabaseYet(error) {
3098
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || error instanceof MetabaseApiError;
2935
+ return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
3099
2936
  }
3100
2937
  function validateEmail(value) {
3101
2938
  return EMAIL.test(value) ? void 0 : "Enter an email address";
@@ -3225,9 +3062,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
3225
3062
  ctx.ui.success(`Admin user ${email} recorded`);
3226
3063
  return "completed";
3227
3064
  }
3228
- function adminUserStep(options, deps = hostDeps$14) {
3065
+ function adminUserStep(options, deps = hostDeps$13) {
3229
3066
  return {
3230
- id: STEP_ID$10,
3067
+ id: STEP_ID$8,
3231
3068
  title: "Create the admin user",
3232
3069
  redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
3233
3070
  async detect(ctx) {
@@ -3455,11 +3292,12 @@ const LogoutResult = z.object({
3455
3292
  cleared: z.boolean(),
3456
3293
  aborted: z.boolean()
3457
3294
  }).loose();
3295
+ const MbAuthMethod = z.enum(["oauth", "apiKey"]);
3458
3296
  const AuthStatus = z.object({
3459
3297
  profile: z.string(),
3460
3298
  present: z.boolean(),
3461
3299
  url: z.string().nullable(),
3462
- method: z.string().nullable(),
3300
+ method: MbAuthMethod.nullable(),
3463
3301
  user: ProbedUser.nullable()
3464
3302
  }).loose();
3465
3303
  const AuthProfileStatus = z.enum([
@@ -3506,11 +3344,41 @@ const RemoteChanges = z.object({
3506
3344
  remote_version: z.string().nullable(),
3507
3345
  local_version: z.string().nullable()
3508
3346
  }).loose();
3347
+ function comparableUrl(url) {
3348
+ const parsed = new URL(url);
3349
+ return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
3350
+ }
3351
+ function sameMetabaseUrl(a, b) {
3352
+ return comparableUrl(a) === comparableUrl(b);
3353
+ }
3354
+ function isLoggedInAt(status, url) {
3355
+ return status !== null && status.present && status.url !== null && sameMetabaseUrl(status.url, url);
3356
+ }
3357
+ function isOAuthLoginAt(status, url) {
3358
+ return status !== null && status.method === MbAuthMethod.enum.oauth && isLoggedInAt(status, url);
3359
+ }
3360
+ function isApiKeyLoginAt(status, url) {
3361
+ return status !== null && status.method === MbAuthMethod.enum.apiKey && isLoggedInAt(status, url);
3362
+ }
3363
+ function mbInstanceOf(state, secrets) {
3364
+ const configured = configuredRuntime(state);
3365
+ if (configured === null || secrets === null || secrets.apiKey === null) return null;
3366
+ return {
3367
+ url: configured.url,
3368
+ apiKey: secrets.apiKey
3369
+ };
3370
+ }
3371
+ function instanceEnv(instance) {
3372
+ return { addedEnv: {
3373
+ [ENV_MB_URL]: instance.url,
3374
+ [ENV_MB_API_KEY]: instance.apiKey
3375
+ } };
3376
+ }
3509
3377
  function mbTool(runner) {
3510
- async function mbJson(args, schema, stdin) {
3378
+ async function mbJson(args, schema, options = {}) {
3511
3379
  const argv = [...args, "--json"];
3512
- const result = await runner.run(MB, argv, stdin === void 0 ? {} : { stdin });
3513
- if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
3380
+ const result = await runner.run(MB, argv, options);
3381
+ if (result.exitCode !== 0) throw toolFailure(MB, argv, result, options);
3514
3382
  return parseJsonOutput(MB, argv, result.stdout, schema);
3515
3383
  }
3516
3384
  async function version(command = MB) {
@@ -3533,7 +3401,7 @@ function mbTool(runner) {
3533
3401
  options.url,
3534
3402
  "--profile",
3535
3403
  options.profile
3536
- ], LoginResult, `${options.apiKey}\n`);
3404
+ ], LoginResult, { stdin: `${options.apiKey}\n` });
3537
3405
  }
3538
3406
  async function authStatus(profile) {
3539
3407
  return mbJson([
@@ -3543,6 +3411,14 @@ function mbTool(runner) {
3543
3411
  profile
3544
3412
  ], AuthStatus);
3545
3413
  }
3414
+ async function authStatusIfInstalled(profile) {
3415
+ try {
3416
+ return await authStatus(profile);
3417
+ } catch (error) {
3418
+ if (error instanceof ProcessNotFoundError) return null;
3419
+ throw error;
3420
+ }
3421
+ }
3546
3422
  async function authLogout(profile) {
3547
3423
  return mbJson([
3548
3424
  "auth",
@@ -3562,50 +3438,37 @@ function mbTool(runner) {
3562
3438
  "0"
3563
3439
  ], AuthList)).data;
3564
3440
  }
3565
- async function gitSyncStatus(profile) {
3566
- return mbJson([
3567
- "git-sync",
3568
- "status",
3569
- "--profile",
3570
- profile
3571
- ], GitSyncStatus);
3441
+ async function gitSyncStatus(instance) {
3442
+ return mbJson(["git-sync", "status"], GitSyncStatus, instanceEnv(instance));
3572
3443
  }
3573
- async function gitSyncHasRemoteChanges(profile) {
3574
- return mbJson([
3575
- "git-sync",
3576
- "has-remote-changes",
3577
- "--profile",
3578
- profile
3579
- ], RemoteChanges);
3444
+ async function gitSyncHasRemoteChanges(instance) {
3445
+ return mbJson(["git-sync", "has-remote-changes"], RemoteChanges, instanceEnv(instance));
3580
3446
  }
3581
- async function gitSyncImport(profile, options) {
3447
+ async function gitSyncImport(instance, options) {
3582
3448
  return mbJson([
3583
3449
  "git-sync",
3584
3450
  "import",
3585
- "--profile",
3586
- profile,
3587
3451
  "--branch",
3588
3452
  options.branch,
3589
3453
  "--wait"
3590
- ], GitSyncOutcome);
3454
+ ], GitSyncOutcome, instanceEnv(instance));
3591
3455
  }
3592
- async function gitSyncExport(profile, options) {
3456
+ async function gitSyncExport(instance, options) {
3593
3457
  return mbJson([
3594
3458
  "git-sync",
3595
3459
  "export",
3596
- "--profile",
3597
- profile,
3598
3460
  "--branch",
3599
3461
  options.branch,
3600
3462
  "--message",
3601
3463
  options.message,
3602
3464
  "--wait"
3603
- ], GitSyncOutcome);
3465
+ ], GitSyncOutcome, instanceEnv(instance));
3604
3466
  }
3605
3467
  return {
3606
3468
  version,
3607
3469
  authLogin,
3608
3470
  authStatus,
3471
+ authStatusIfInstalled,
3609
3472
  authLogout,
3610
3473
  authList,
3611
3474
  gitSyncStatus,
@@ -3616,17 +3479,18 @@ function mbTool(runner) {
3616
3479
  }
3617
3480
  //#endregion
3618
3481
  //#region src/steps/api-key.ts
3619
- const STEP_ID$9 = "api-key";
3482
+ const STEP_ID$7 = "api-key";
3620
3483
  const API_KEY_KEYS = {
3621
- email: `${STEP_ID$9}.email`,
3622
- password: `${STEP_ID$9}.password`,
3623
- profile: `${STEP_ID$9}.profile`
3484
+ email: `${STEP_ID$7}.email`,
3485
+ password: `${STEP_ID$7}.password`,
3486
+ profile: `${STEP_ID$7}.profile`
3624
3487
  };
3625
3488
  const ADMINISTRATORS_GROUP_NAME = "Administrators";
3626
- function hostDeps$13(ctx) {
3489
+ function hostDeps$12(ctx) {
3627
3490
  return {
3628
3491
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3629
- runner: processRunner(ctx.signal)
3492
+ runner: processRunner(ctx.signal),
3493
+ instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
3630
3494
  };
3631
3495
  }
3632
3496
  async function adminCredentials(ctx) {
@@ -3682,6 +3546,13 @@ async function authenticates(client) {
3682
3546
  throw error;
3683
3547
  }
3684
3548
  }
3549
+ async function forgetCompletion(ctx) {
3550
+ const { [STEP_ID$7]: record, ...steps } = ctx.state.steps;
3551
+ if (record !== void 0) await ctx.saveState({
3552
+ ...ctx.state,
3553
+ steps
3554
+ });
3555
+ }
3685
3556
  async function workingApiKey(ctx, deps, url) {
3686
3557
  const stored = ctx.secrets.apiKey;
3687
3558
  if (stored !== null) {
@@ -3691,6 +3562,7 @@ async function workingApiKey(ctx, deps, url) {
3691
3562
  }))) return stored;
3692
3563
  ctx.ui.warn("Metabase rejects the stored API key; replacing it");
3693
3564
  }
3565
+ await forgetCompletion(ctx);
3694
3566
  const sessionId = await superuserSession(ctx, deps, url);
3695
3567
  const apiKey = await freshApiKey(ctx, deps.client(url, {
3696
3568
  kind: "session",
@@ -3702,20 +3574,24 @@ async function workingApiKey(ctx, deps, url) {
3702
3574
  });
3703
3575
  return apiKey;
3704
3576
  }
3705
- async function defaultProfileStatus(mb) {
3706
- try {
3707
- return await mb.authStatus(DEFAULT_MB_PROFILE);
3708
- } catch (error) {
3709
- if (error instanceof ProcessNotFoundError) return null;
3710
- throw error;
3711
- }
3577
+ function elsewhereUrl(status, url, movedFrom) {
3578
+ if (status === null || !status.present || status.url === null) return null;
3579
+ if (sameMetabaseUrl(status.url, url)) return null;
3580
+ if (movedFrom !== null && sameMetabaseUrl(status.url, movedFrom)) return null;
3581
+ return status.url;
3712
3582
  }
3713
3583
  async function chooseProfile(ctx, mb, url) {
3714
3584
  const profile = ctx.state.mbProfile;
3715
- if (profile !== "default") return profile;
3716
- const existing = await defaultProfileStatus(mb);
3717
- if (existing === null || !existing.present || existing.url === null || existing.url === url || existing.url === ctx.state.movedFrom) return profile;
3718
- const previous = existing.url;
3585
+ const status = await mb.authStatusIfInstalled(profile);
3586
+ if (profile !== "default") return {
3587
+ profile,
3588
+ status
3589
+ };
3590
+ const previous = elsewhereUrl(status, url, ctx.state.movedFrom);
3591
+ if (previous === null) return {
3592
+ profile,
3593
+ status
3594
+ };
3719
3595
  const named = namedMbProfile(ctx.state.name);
3720
3596
  if (await ctx.prompts.select({
3721
3597
  key: API_KEY_KEYS.profile,
@@ -3730,49 +3606,58 @@ async function chooseProfile(ctx, mb, url) {
3730
3606
  recommended: "default"
3731
3607
  }) === "default") {
3732
3608
  ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
3733
- return profile;
3609
+ return {
3610
+ profile,
3611
+ status
3612
+ };
3734
3613
  }
3735
3614
  await ctx.saveState({
3736
3615
  ...ctx.state,
3737
3616
  mbProfile: named
3738
3617
  });
3739
- return named;
3618
+ return {
3619
+ profile: named,
3620
+ status: await mb.authStatusIfInstalled(named)
3621
+ };
3740
3622
  }
3741
3623
  function usageNote(profile) {
3742
3624
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
3743
3625
  return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
3744
3626
  }
3745
- function apiKeyStep(deps = hostDeps$13) {
3627
+ function apiKeyStep(deps = hostDeps$12) {
3746
3628
  return {
3747
- id: STEP_ID$9,
3629
+ id: STEP_ID$7,
3748
3630
  title: "Connect the Metabase CLI",
3749
3631
  redoHint: "`rde status` shows the mb profile it uses.",
3750
3632
  async detect(ctx) {
3751
- if (ctx.secrets.apiKey === null) return "todo";
3752
- const mb = mbTool(deps(ctx).runner);
3753
- const profile = ctx.state.mbProfile;
3633
+ const configured = configuredRuntime(ctx.state);
3634
+ const apiKey = ctx.secrets.apiKey;
3635
+ if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
3636
+ const resolved = deps(ctx);
3637
+ if (!await resolved.instanceRunning()) return "todo";
3638
+ if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
3754
3639
  try {
3755
- if (!(await mb.authStatus(profile)).present) return "todo";
3640
+ return await authenticates(resolved.client(configured.url, {
3641
+ kind: "api-key",
3642
+ key: apiKey
3643
+ })) ? "done" : "todo";
3756
3644
  } catch (error) {
3757
- if (error instanceof ProcessNotFoundError) return "todo";
3645
+ if (isMetabaseNotServing(error) || error instanceof MetabaseApiError) return "todo";
3758
3646
  throw error;
3759
3647
  }
3760
- return (await mb.authList()).find((candidate) => candidate.profile === profile)?.status === "ok" ? "done" : "todo";
3761
3648
  },
3762
3649
  async run(ctx) {
3763
3650
  const resolved = deps(ctx);
3764
3651
  const configured = requireConfiguredRuntime(ctx.state);
3765
3652
  const apiKey = await workingApiKey(ctx, resolved, configured.url);
3766
3653
  const mb = mbTool(resolved.runner);
3767
- const profile = await chooseProfile(ctx, mb, configured.url);
3768
- await mb.authLogin({
3654
+ const { profile, status } = await chooseProfile(ctx, mb, configured.url);
3655
+ if (isOAuthLoginAt(status, configured.url)) ctx.ui.info(`mb profile ${profile} is logged in to this instance with OAuth; leaving it as it is (\`mb auth logout --profile ${profile}\` first to use rde's API key instead)`);
3656
+ else await mb.authLogin({
3769
3657
  url: configured.url,
3770
3658
  profile,
3771
3659
  apiKey
3772
3660
  });
3773
- const entry = (await mb.authList()).find((candidate) => candidate.profile === profile);
3774
- if (entry === void 0) throw new ConfigError(`\`mb auth list\` does not show profile ${profile} after login`);
3775
- if (entry.status !== "ok") throw new ConfigError(`mb profile ${profile} reports ${entry.status} after login`);
3776
3661
  if (ctx.state.movedFrom !== null) await ctx.saveState({
3777
3662
  ...ctx.state,
3778
3663
  movedFrom: null
@@ -4769,7 +4654,7 @@ const RSA_KEY_UNAVAILABLE_ERROR = "RSA public key is not available";
4769
4654
  const KEY_RETRIEVAL_NOTE = "allowPublicKeyRetrieval=true is added: MySQL 8 needs it to log in to a local server without SSL; to leave it out, paste a connection string with allowPublicKeyRetrieval=false instead";
4770
4655
  const KEY_RETRIEVAL_HINT = "MySQL 8 without SSL needs allowPublicKeyRetrieval=true: go back and add ?allowPublicKeyRetrieval=true to the connection string, or turn on SSL";
4771
4656
  const CONNECTION_PLACEHOLDER = "leave empty to skip";
4772
- const STUB_CONNECTION_PLACEHOLDER = "leave empty to pick the engine";
4657
+ const ENGINE_PLACEHOLDER = "leave empty to pick the engine";
4773
4658
  const START_SCREEN = {
4774
4659
  engine: "pick-engine",
4775
4660
  "connection-string": "connection-string"
@@ -4795,6 +4680,9 @@ function targeting(stub) {
4795
4680
  function restart(model) {
4796
4681
  return model.target === null ? FRESH$1 : targeting(model.target);
4797
4682
  }
4683
+ function mayFinish(run) {
4684
+ return !run.deps.required || run.deps.existingCount + run.created.length > 0;
4685
+ }
4798
4686
  function nextStub(run, model) {
4799
4687
  const stub = run.queue.shift();
4800
4688
  if (stub !== void 0) return {
@@ -4802,13 +4690,18 @@ function nextStub(run, model) {
4802
4690
  to: "connection-string",
4803
4691
  model: targeting(stub)
4804
4692
  };
4805
- return run.created.length > 0 ? {
4693
+ if (run.created.length > 0) return {
4806
4694
  type: "next",
4807
4695
  to: "another",
4808
4696
  model
4809
- } : {
4697
+ };
4698
+ return mayFinish(run) ? {
4810
4699
  type: "done",
4811
4700
  model
4701
+ } : {
4702
+ type: "next",
4703
+ to: "connection-string",
4704
+ model: FRESH$1
4812
4705
  };
4813
4706
  }
4814
4707
  function stubNamed(run, name) {
@@ -4898,9 +4791,13 @@ function pickStubsScreen(run) {
4898
4791
  run.queue = run.stubs.filter((stub) => picked.includes(stub.id));
4899
4792
  return nextStub(run, model);
4900
4793
  }
4901
- if (run.deps.start === "connection-string" && run.created.length === 0) return {
4794
+ if (run.deps.start === "connection-string" && run.created.length === 0) return mayFinish(run) ? {
4902
4795
  type: "done",
4903
4796
  model
4797
+ } : {
4798
+ type: "next",
4799
+ to: "connection-string",
4800
+ model: FRESH$1
4904
4801
  };
4905
4802
  return {
4906
4803
  type: "next",
@@ -4910,6 +4807,11 @@ function pickStubsScreen(run) {
4910
4807
  };
4911
4808
  }
4912
4809
  function connectionQuestion(run, target) {
4810
+ if (target === null && !mayFinish(run)) return {
4811
+ key: DATABASE_KEYS.connection,
4812
+ message: "Connection string of your database (Enter to pick its engine instead)",
4813
+ placeholder: ENGINE_PLACEHOLDER
4814
+ };
4913
4815
  if (target === null) return {
4914
4816
  key: DATABASE_KEYS.connection,
4915
4817
  message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
@@ -4918,7 +4820,7 @@ function connectionQuestion(run, target) {
4918
4820
  return {
4919
4821
  key: databaseKey(ordinalOf(run), "connection"),
4920
4822
  message: `Connection string of ${target.name} (Enter to pick its engine instead)`,
4921
- placeholder: STUB_CONNECTION_PLACEHOLDER
4823
+ placeholder: ENGINE_PLACEHOLDER
4922
4824
  };
4923
4825
  }
4924
4826
  function connectionStringScreen(run) {
@@ -4930,7 +4832,7 @@ function connectionStringScreen(run) {
4930
4832
  ...draft !== null && { initialValue: draft }
4931
4833
  })).trim();
4932
4834
  draft = uri;
4933
- if (uri.length === 0) return model.target === null ? {
4835
+ if (uri.length === 0) return model.target === null && mayFinish(run) ? {
4934
4836
  type: "done",
4935
4837
  model
4936
4838
  } : {
@@ -4969,22 +4871,28 @@ function connectionStringScreen(run) {
4969
4871
  }
4970
4872
  };
4971
4873
  }
4972
- function finishLabel(run, model) {
4973
- if (model.target !== null) return `Leave ${model.target.name} unconnected`;
4974
- return run.deps.existingCount + run.created.length > 0 ? "Finish" : "Skip for now";
4874
+ function finishChoices(run, model) {
4875
+ if (model.target !== null) return [{
4876
+ value: FINISH,
4877
+ label: `Leave ${model.target.name} unconnected`
4878
+ }];
4879
+ if (run.deps.existingCount + run.created.length > 0) return [{
4880
+ value: FINISH,
4881
+ label: "Finish"
4882
+ }];
4883
+ return mayFinish(run) ? [{
4884
+ value: FINISH,
4885
+ label: "Skip for now"
4886
+ }] : [];
4975
4887
  }
4976
4888
  function pickEngineScreen(run) {
4977
4889
  return async (model) => {
4978
4890
  const total = run.deps.existingCount + run.created.length;
4979
- const finishChoice = {
4980
- value: FINISH,
4981
- label: finishLabel(run, model)
4982
- };
4983
4891
  for (;;) {
4984
4892
  const picked = await run.deps.prompts.select({
4985
4893
  key: databaseKey(ordinalOf(run), "engine"),
4986
4894
  message: "Which database engine?",
4987
- choices: [...run.deps.catalog.list().map(engineChoice), finishChoice],
4895
+ choices: [...run.deps.catalog.list().map(engineChoice), ...finishChoices(run, model)],
4988
4896
  ...model.engine !== null && { initialValue: model.engine.key }
4989
4897
  });
4990
4898
  if (picked === "finish" && model.target !== null) return nextStub(run, model);
@@ -5460,6 +5368,13 @@ const METABASE_CONTENT_DIRS = [
5460
5368
  ];
5461
5369
  const TOKEN_USERNAMES = { "bitbucket.org": "x-token-auth" };
5462
5370
  const DEFAULT_TOKEN_USERNAME = "x-access-token";
5371
+ const MISSING_REF_EXIT_CODE = 1;
5372
+ const FIELD_SEPARATOR = "";
5373
+ const COMMIT_FORMAT = [
5374
+ "%H",
5375
+ "%h",
5376
+ "%s"
5377
+ ].join("%x1f");
5463
5378
  function credentialEnv(credentials) {
5464
5379
  const { origin, hostname } = new URL(credentials.url);
5465
5380
  const username = TOKEN_USERNAMES[hostname] ?? DEFAULT_TOKEN_USERNAME;
@@ -5473,6 +5388,18 @@ function credentialEnv(credentials) {
5473
5388
  GIT_TERMINAL_PROMPT: "0"
5474
5389
  };
5475
5390
  }
5391
+ function parseCommitLine(line) {
5392
+ const [sha, shortSha, subject] = line.split(FIELD_SEPARATOR);
5393
+ if (sha === void 0 || shortSha === void 0 || subject === void 0) throw new Error(`unexpected \`git log\` line: ${line}`);
5394
+ return {
5395
+ sha,
5396
+ shortSha,
5397
+ subject
5398
+ };
5399
+ }
5400
+ function outputLines(stdout) {
5401
+ return stdout.split("\n").filter((line) => line.length > 0);
5402
+ }
5476
5403
  async function directoryExists(path) {
5477
5404
  try {
5478
5405
  return (await promises.stat(path)).isDirectory();
@@ -5528,8 +5455,12 @@ function gitTool(runner) {
5528
5455
  ], { cwd: dir });
5529
5456
  return result.exitCode === 0 && result.stdout.trim().length > 0;
5530
5457
  }
5531
- async function commitAll(dir, message) {
5532
- await git(["add", "--all"], { cwd: dir });
5458
+ async function commitPaths(dir, paths, message) {
5459
+ await git([
5460
+ "add",
5461
+ "--",
5462
+ ...paths
5463
+ ], { cwd: dir });
5533
5464
  await git([
5534
5465
  ...await hasIdentity(dir) ? [] : [
5535
5466
  "-c",
@@ -5539,7 +5470,9 @@ function gitTool(runner) {
5539
5470
  ],
5540
5471
  "commit",
5541
5472
  "--message",
5542
- message
5473
+ message,
5474
+ "--",
5475
+ ...paths
5543
5476
  ], { cwd: dir });
5544
5477
  }
5545
5478
  async function addRemote(dir, name, url) {
@@ -5585,6 +5518,42 @@ function gitTool(runner) {
5585
5518
  if (result.exitCode !== 0) return null;
5586
5519
  return result.stdout.trim();
5587
5520
  }
5521
+ async function isBareRepo(path) {
5522
+ const args = [
5523
+ `--git-dir=${path}`,
5524
+ "rev-parse",
5525
+ "--is-bare-repository"
5526
+ ];
5527
+ const result = await runner.run(GIT, args);
5528
+ return result.exitCode === 0 && result.stdout.trim() === "true";
5529
+ }
5530
+ async function bareBranchCommits(path, branch) {
5531
+ const gitDir = `--git-dir=${path}`;
5532
+ const ref = `refs/heads/${branch}`;
5533
+ const verifyArgs = [
5534
+ gitDir,
5535
+ "rev-parse",
5536
+ "--verify",
5537
+ "--quiet",
5538
+ `${ref}^{commit}`
5539
+ ];
5540
+ const verified = await runner.run(GIT, verifyArgs);
5541
+ if (verified.exitCode === MISSING_REF_EXIT_CODE) return null;
5542
+ if (verified.exitCode !== 0) throw toolFailure(GIT, verifyArgs, verified);
5543
+ return outputLines((await git([
5544
+ gitDir,
5545
+ "-c",
5546
+ "log.showSignature=false",
5547
+ "log",
5548
+ `--format=${COMMIT_FORMAT}`,
5549
+ ref,
5550
+ "--"
5551
+ ])).stdout).map(parseCommitLine);
5552
+ }
5553
+ async function reachableCommits(dir) {
5554
+ const result = await git(["rev-list", "--all"], { cwd: dir });
5555
+ return new Set(outputLines(result.stdout));
5556
+ }
5588
5557
  async function hasMetabaseContent(dir) {
5589
5558
  for (const name of METABASE_CONTENT_DIRS) if (await directoryExists(join(dir, name))) return true;
5590
5559
  return false;
@@ -5595,24 +5564,27 @@ function gitTool(runner) {
5595
5564
  initBare,
5596
5565
  init,
5597
5566
  hasCommits,
5598
- commitAll,
5567
+ commitPaths,
5599
5568
  addRemote,
5600
5569
  clone,
5601
5570
  push,
5602
5571
  currentBranch,
5603
5572
  remoteUrl,
5573
+ isBareRepo,
5574
+ bareBranchCommits,
5575
+ reachableCommits,
5604
5576
  hasMetabaseContent
5605
5577
  };
5606
5578
  }
5607
5579
  //#endregion
5608
5580
  //#region src/steps/git-sync.ts
5609
- const STEP_ID$8 = "git-sync";
5581
+ const STEP_ID$6 = "git-sync";
5610
5582
  const GIT_SYNC_KEYS = {
5611
- location: `${STEP_ID$8}.location`,
5612
- path: `${STEP_ID$8}.path`,
5613
- remote: `${STEP_ID$8}.remote`,
5614
- url: `${STEP_ID$8}.url`,
5615
- token: `${STEP_ID$8}.token`
5583
+ location: `${STEP_ID$6}.location`,
5584
+ path: `${STEP_ID$6}.path`,
5585
+ remote: `${STEP_ID$6}.remote`,
5586
+ url: `${STEP_ID$6}.url`,
5587
+ token: `${STEP_ID$6}.token`
5616
5588
  };
5617
5589
  const BARE_REPO_NAME = "content.git";
5618
5590
  const DEFAULT_BRANCH = "main";
@@ -5646,7 +5618,7 @@ const SYNC_MODE_SETTINGS = {
5646
5618
  "remote-sync-type": "read-write",
5647
5619
  "remote-sync-transforms": true
5648
5620
  };
5649
- function hostDeps$12(ctx) {
5621
+ function hostDeps$11(ctx) {
5650
5622
  return {
5651
5623
  runner: processRunner(ctx.signal),
5652
5624
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
@@ -5898,7 +5870,7 @@ async function prepareWorkdir(run, workdir) {
5898
5870
  if (!await run.git.hasCommits(workdir)) {
5899
5871
  await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
5900
5872
  await ignoreDataAppFiles(workdir);
5901
- await run.git.commitAll(workdir, INITIAL_COMMIT_MESSAGE);
5873
+ await run.git.commitPaths(workdir, [README_NAME, GITIGNORE_NAME], INITIAL_COMMIT_MESSAGE);
5902
5874
  }
5903
5875
  return {
5904
5876
  workdir,
@@ -6019,7 +5991,7 @@ async function applySettings(run, configured) {
6019
5991
  async function remoteHasContent(run, gitRemote) {
6020
5992
  switch (gitRemote.kind) {
6021
5993
  case "local-bare": return run.git.hasMetabaseContent(gitRemote.workdir);
6022
- case "hosted": return (await mbTool(run.deps.runner).gitSyncHasRemoteChanges(run.ctx.state.mbProfile)).has_changes;
5994
+ case "hosted": return (await mbTool(run.deps.runner).gitSyncHasRemoteChanges(run.instance)).has_changes;
6023
5995
  }
6024
5996
  }
6025
5997
  function reportImport(run, outcome) {
@@ -6041,7 +6013,7 @@ async function importExistingContent(run, gitRemote) {
6041
6013
  const spinner = run.ctx.ui.spinner("Importing the repository's content into Metabase…");
6042
6014
  let outcome;
6043
6015
  try {
6044
- outcome = await mbTool(run.deps.runner).gitSyncImport(run.ctx.state.mbProfile, { branch: gitRemote.branch });
6016
+ outcome = await mbTool(run.deps.runner).gitSyncImport(run.instance, { branch: gitRemote.branch });
6045
6017
  } catch (error) {
6046
6018
  spinner.fail("Import failed");
6047
6019
  throw error;
@@ -6062,7 +6034,7 @@ function shellWord(value) {
6062
6034
  function exportCommand(profile, branch) {
6063
6035
  return `${mbCommand(profile)} git-sync export -b ${shellWord(branch)}`;
6064
6036
  }
6065
- function usageLines$1(state, gitRemote) {
6037
+ function usageLines(state, gitRemote) {
6066
6038
  const profile = state.mbProfile;
6067
6039
  return [
6068
6040
  `cd ${gitRemote.workdir}`,
@@ -6085,15 +6057,17 @@ function isMbUnavailable(error) {
6085
6057
  }
6086
6058
  async function runGitSync(ctx, deps) {
6087
6059
  const configured = requireConfiguredRuntime(ctx.state);
6088
- if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6060
+ const instance = mbInstanceOf(ctx.state, ctx.secrets);
6061
+ if (instance === null) throw new ConfigError("no API key yet; the api-key step comes first");
6089
6062
  const run = {
6090
6063
  ctx,
6091
6064
  deps,
6092
6065
  git: gitTool(deps.runner),
6093
- client: deps.client(configured.url, {
6066
+ client: deps.client(instance.url, {
6094
6067
  kind: "api-key",
6095
- key: ctx.secrets.apiKey
6096
- })
6068
+ key: instance.apiKey
6069
+ }),
6070
+ instance
6097
6071
  };
6098
6072
  const flow = await runFlow({
6099
6073
  remote: remoteScreen(run),
@@ -6111,20 +6085,22 @@ async function runGitSync(ctx, deps) {
6111
6085
  ...ctx.state,
6112
6086
  gitRemote: remote.gitRemote
6113
6087
  });
6114
- ctx.ui.note(usageLines$1(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
6088
+ ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
6115
6089
  return "completed";
6116
6090
  }
6117
- function gitSyncStep(deps = hostDeps$12) {
6091
+ function gitSyncStep(deps = hostDeps$11) {
6118
6092
  return {
6119
- id: STEP_ID$8,
6093
+ id: STEP_ID$6,
6120
6094
  title: "Connect a git repository",
6121
6095
  redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
6122
6096
  async detect(ctx) {
6123
6097
  if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
6124
- if (ctx.state.steps[STEP_ID$8]?.status === "skipped") return "not-applicable";
6125
- if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
6098
+ if (ctx.state.steps[STEP_ID$6]?.status === "skipped") return "not-applicable";
6099
+ if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$6]?.status !== "done") return "todo";
6100
+ const instance = mbInstanceOf(ctx.state, ctx.secrets);
6101
+ if (instance === null) return "todo";
6126
6102
  try {
6127
- return (await mbTool(deps(ctx).runner).gitSyncStatus(ctx.state.mbProfile)).branch === null ? "todo" : "done";
6103
+ return (await mbTool(deps(ctx).runner).gitSyncStatus(instance)).branch === null ? "todo" : "done";
6128
6104
  } catch (error) {
6129
6105
  if (isMbUnavailable(error)) return "todo";
6130
6106
  throw error;
@@ -6302,12 +6278,12 @@ function skillsCli(runner) {
6302
6278
  }
6303
6279
  //#endregion
6304
6280
  //#region src/steps/agents-skills.ts
6305
- const STEP_ID$7 = "agents-skills";
6306
- const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
6281
+ const STEP_ID$5 = "agents-skills";
6282
+ const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
6307
6283
  const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
6308
6284
  const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
6309
6285
  const DETECT_AGENTS = { kind: "detected" };
6310
- function hostDeps$11(ctx) {
6286
+ function hostDeps$10(ctx) {
6311
6287
  return {
6312
6288
  runner: processRunner(ctx.signal),
6313
6289
  probe: hostAgentProbe(),
@@ -6486,11 +6462,11 @@ function skipLine(all, source) {
6486
6462
  switch (source) {
6487
6463
  case "override":
6488
6464
  case "chosen":
6489
- case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
6490
- case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
6465
+ case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$5}\` to install it later`;
6466
+ case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$5} --agents <name>\` once one is installed`;
6491
6467
  }
6492
6468
  }
6493
- function agentsSkillsStep(options, updates, deps = hostDeps$11) {
6469
+ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
6494
6470
  let hashes = null;
6495
6471
  async function setCheck(resolved) {
6496
6472
  hashes ??= hashLookup(resolved.github);
@@ -6501,13 +6477,13 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
6501
6477
  };
6502
6478
  }
6503
6479
  return {
6504
- id: STEP_ID$7,
6480
+ id: STEP_ID$5,
6505
6481
  title: "Install the rde and data-app skills for your coding agents",
6506
6482
  redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
6507
6483
  async detect(ctx) {
6508
6484
  const resolved = deps(ctx);
6509
6485
  const detected = detectedOnly(await detectAgents(resolved.probe));
6510
- if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
6486
+ if (detected.length === 0) return ctx.state.steps[STEP_ID$5]?.status === "skipped" ? "not-applicable" : "todo";
6511
6487
  if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
6512
6488
  const check = await setCheck(resolved);
6513
6489
  for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
@@ -6555,8 +6531,10 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
6555
6531
  }
6556
6532
  //#endregion
6557
6533
  //#region src/steps/databases.ts
6558
- const STEP_ID$6 = "databases";
6559
- const SAMPLE_STANDS_IN = { request: "sample-stands-in" };
6534
+ const DATABASE_REQUIRED = { request: "required" };
6535
+ const WRITE_ACCESS = "Transforms write their tables to it, so connect it as a user that can create tables.";
6536
+ const NONE_CONNECTED = `rde init needs a database of your own to go on. ${WRITE_ACCESS}`;
6537
+ const NONE_NAMED = `rde init needs a database of your own and asks for one only at a terminal without --yes: pass --database <connection string>, or list it under "databases" in the --answers file. ${WRITE_ACCESS}`;
6560
6538
  function hostDatabasesDeps(signal) {
6561
6539
  return {
6562
6540
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
@@ -6564,10 +6542,7 @@ function hostDatabasesDeps(signal) {
6564
6542
  now: () => /* @__PURE__ */ new Date()
6565
6543
  };
6566
6544
  }
6567
- function isNotServingYet$2(error) {
6568
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6569
- }
6570
- function requireApiKey$2(state, apiKey) {
6545
+ function requireApiKey$1(state, apiKey) {
6571
6546
  if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
6572
6547
  return apiKey;
6573
6548
  }
@@ -6577,7 +6552,7 @@ function hostAliasOf(runtime) {
6577
6552
  case "jar": return null;
6578
6553
  }
6579
6554
  }
6580
- function toRecord$1(database, createdAt) {
6555
+ function toRecord(database, createdAt) {
6581
6556
  return {
6582
6557
  id: database.id,
6583
6558
  name: database.name,
@@ -6589,7 +6564,7 @@ function authenticatedClient(state, apiKey, deps) {
6589
6564
  const configured = requireConfiguredRuntime(state);
6590
6565
  return deps.client(configured.url, {
6591
6566
  kind: "api-key",
6592
- key: requireApiKey$2(state, apiKey)
6567
+ key: requireApiKey$1(state, apiKey)
6593
6568
  });
6594
6569
  }
6595
6570
  async function listDatabases(state, apiKey, deps) {
@@ -6632,7 +6607,7 @@ async function connectDatabases(ctx, deps, opening) {
6632
6607
  existingCount: existing.length,
6633
6608
  readTextFile: deps.readTextFile,
6634
6609
  onCreated: async (database) => {
6635
- const record = toRecord$1(database, deps.now().toISOString());
6610
+ const record = toRecord(database, deps.now().toISOString());
6636
6611
  await ctx.saveState({
6637
6612
  ...ctx.state,
6638
6613
  databases: [...ctx.state.databases, record]
@@ -6649,6 +6624,7 @@ async function connectDatabases(ctx, deps, opening) {
6649
6624
  async function addDatabases(ctx, deps, options) {
6650
6625
  const connected = await connectDatabases(ctx, deps, {
6651
6626
  start: "engine",
6627
+ required: false,
6652
6628
  ...options
6653
6629
  });
6654
6630
  return {
@@ -6656,50 +6632,33 @@ async function addDatabases(ctx, deps, options) {
6656
6632
  result: { addedDatabases: connected.added }
6657
6633
  };
6658
6634
  }
6659
- async function askForOwnDatabase(ctx, deps) {
6660
- const connected = await connectDatabases(ctx, deps, {
6661
- start: "connection-string",
6662
- offerStubs: true
6663
- });
6635
+ async function connectRequired(ctx, deps, opening) {
6636
+ const connected = await connectDatabases(ctx, deps, opening);
6637
+ if (connected.existing + connected.created === 0) throw new ConfigError(NONE_CONNECTED);
6664
6638
  return {
6665
- outcome: connected.created > 0 ? "completed" : "skipped",
6639
+ outcome: "completed",
6666
6640
  result: { addedDatabases: connected.added }
6667
6641
  };
6668
6642
  }
6669
- function hostDeps$10(ctx) {
6643
+ function hostDeps$9(ctx) {
6670
6644
  return {
6671
6645
  ...hostDatabasesDeps(ctx.signal),
6672
6646
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
6673
6647
  };
6674
6648
  }
6675
- function sampleDatabaseId(state) {
6676
- return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
6677
- }
6678
- function skippedBefore(state) {
6679
- return state.steps[STEP_ID$6]?.status === "skipped";
6680
- }
6681
- function ownDatabaseIds(state) {
6682
- const sampleId = sampleDatabaseId(state);
6683
- return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
6684
- }
6685
- function databasesStep(options, deps = hostDeps$10) {
6649
+ function databasesStep(options, deps = hostDeps$9) {
6686
6650
  return {
6687
- id: STEP_ID$6,
6651
+ id: "databases",
6688
6652
  title: "Connect your databases",
6689
6653
  redoHint: "Connect another with `rde db add --url <connection string>`.",
6690
6654
  async detect(ctx) {
6691
6655
  const configured = configuredRuntime(ctx.state);
6692
6656
  if (configured === null || ctx.secrets.apiKey === null) return "todo";
6693
- const own = ownDatabaseIds(ctx.state);
6694
- const sampleId = sampleDatabaseId(ctx.state);
6695
- const asking = options.request === "ask-once" && !skippedBefore(ctx.state);
6696
- if (own.size === 0 && !asking) {
6697
- if (options.request === "named") return "todo";
6698
- return sampleId !== null || skippedBefore(ctx.state) ? "done" : "todo";
6699
- }
6657
+ const recorded = new Set(ctx.state.databases.map((record) => record.id));
6658
+ if (recorded.size === 0 && options.request === "named") return "todo";
6700
6659
  const resolved = deps(ctx);
6701
6660
  if (!await resolved.instanceRunning()) return "todo";
6702
- const isOwn = (id) => own.size > 0 ? own.has(id) : id !== sampleId;
6661
+ const isOwn = (id) => recorded.size === 0 || recorded.has(id);
6703
6662
  const client = resolved.client(configured.url, {
6704
6663
  kind: "api-key",
6705
6664
  key: ctx.secrets.apiKey
@@ -6707,15 +6666,25 @@ function databasesStep(options, deps = hostDeps$10) {
6707
6666
  try {
6708
6667
  return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
6709
6668
  } catch (error) {
6710
- if (isNotServingYet$2(error) || isRejectedCredential(error)) return "todo";
6669
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
6711
6670
  throw error;
6712
6671
  }
6713
6672
  },
6714
6673
  async run(ctx) {
6715
6674
  switch (options.request) {
6716
- case "ask-once": return askForOwnDatabase(ctx, deps(ctx));
6717
- case "named": return addDatabases(ctx, deps(ctx), { offerStubs: false });
6718
- case "sample-stands-in": return { outcome: "skipped" };
6675
+ case "ask":
6676
+ ctx.ui.info(NONE_CONNECTED);
6677
+ return connectRequired(ctx, deps(ctx), {
6678
+ start: "connection-string",
6679
+ offerStubs: true,
6680
+ required: true
6681
+ });
6682
+ case "named": return connectRequired(ctx, deps(ctx), {
6683
+ start: "engine",
6684
+ offerStubs: false,
6685
+ required: true
6686
+ });
6687
+ case "required": throw new ConfigError(NONE_NAMED);
6719
6688
  }
6720
6689
  }
6721
6690
  };
@@ -6740,7 +6709,7 @@ function npmRegistry(signal, options = {}) {
6740
6709
  }
6741
6710
  //#endregion
6742
6711
  //#region src/steps/install-mb.ts
6743
- const STEP_ID$5 = "install-mb";
6712
+ const STEP_ID$4 = "install-mb";
6744
6713
  const MIN_MB_VERSION = "0.3.0";
6745
6714
  const MB_RANGE = `>=${MIN_MB_VERSION}`;
6746
6715
  function installedWith(scriptPath) {
@@ -6749,7 +6718,7 @@ function installedWith(scriptPath) {
6749
6718
  function rdeInstalledWith() {
6750
6719
  return installedWith(process.argv[1]);
6751
6720
  }
6752
- function hostDeps$9(ctx) {
6721
+ function hostDeps$8(ctx) {
6753
6722
  return {
6754
6723
  runner: processRunner(ctx.signal),
6755
6724
  installedWith: rdeInstalledWith(),
@@ -6886,6 +6855,7 @@ async function stillWrong(install, after, deps) {
6886
6855
  const fail = (stderrTail) => new ExternalToolError({
6887
6856
  tool: install.command,
6888
6857
  argv: install.args,
6858
+ envNames: [],
6889
6859
  exitCode: 0,
6890
6860
  stderrTail
6891
6861
  });
@@ -6915,7 +6885,7 @@ async function updateFor(release, installed, deps) {
6915
6885
  version
6916
6886
  };
6917
6887
  }
6918
- function installMbStep(updates, deps = hostDeps$9) {
6888
+ function installMbStep(updates, deps = hostDeps$8) {
6919
6889
  let published = null;
6920
6890
  function release(resolved) {
6921
6891
  if (updates === "installed") return Promise.resolve(UNCHECKED);
@@ -6923,7 +6893,7 @@ function installMbStep(updates, deps = hostDeps$9) {
6923
6893
  return published;
6924
6894
  }
6925
6895
  return {
6926
- id: STEP_ID$5,
6896
+ id: STEP_ID$4,
6927
6897
  title: "Install the Metabase CLI",
6928
6898
  async detect(ctx) {
6929
6899
  const resolved = deps(ctx);
@@ -6965,26 +6935,47 @@ function installMbStep(updates, deps = hostDeps$9) {
6965
6935
  }
6966
6936
  //#endregion
6967
6937
  //#region src/steps/license.ts
6968
- const STEP_ID$4 = "license";
6969
- const LICENSE_KEYS = { token: `${STEP_ID$4}.token` };
6938
+ const STEP_ID$3 = "license";
6939
+ const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
6970
6940
  const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
6971
- const NOT_FOUND_STATUS$2 = 404;
6972
- function hostDeps$8(ctx) {
6941
+ const NOT_FOUND_STATUS$1 = 404;
6942
+ const TOKEN_REFUSED_STATUS = 400;
6943
+ const STORE_UNREACHABLE_STATUS = 503;
6944
+ const TOKEN_CHECK_HOST = "token-check.metabase.com";
6945
+ const ACTIVATION_FAILED = "License activation failed";
6946
+ const LICENSED_FEATURES = {
6947
+ [REMOTE_SYNC_FEATURE]: "remote sync",
6948
+ "data-apps": "data apps",
6949
+ "transforms-testing": "transform tests"
6950
+ };
6951
+ const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
6952
+ function licensedFeatureCoverage(features) {
6953
+ const entries = Object.entries(LICENSED_FEATURES);
6954
+ return {
6955
+ included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
6956
+ missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
6957
+ };
6958
+ }
6959
+ function listOf(names, type) {
6960
+ return new Intl.ListFormat("en", { type }).format(names);
6961
+ }
6962
+ function hostDeps$7(ctx) {
6973
6963
  return {
6974
6964
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
6975
6965
  runner: processRunner(ctx.signal),
6976
- instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
6966
+ instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
6967
+ terminal: stdinIsTty()
6977
6968
  };
6978
6969
  }
6979
6970
  function isNoTokenYet(error) {
6980
- if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$2;
6981
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6971
+ if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
6972
+ return isMetabaseNotServing(error);
6982
6973
  }
6983
6974
  function validateToken(value) {
6984
6975
  if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
6985
6976
  return "A license token is 64 hexadecimal characters";
6986
6977
  }
6987
- function requireApiKey$1(ctx) {
6978
+ function requireApiKey(ctx) {
6988
6979
  if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6989
6980
  return ctx.secrets.apiKey;
6990
6981
  }
@@ -6992,13 +6983,74 @@ function activeLabel(status) {
6992
6983
  const plan = status["plan-alias"];
6993
6984
  return plan === void 0 ? "License active" : `License active (plan ${plan})`;
6994
6985
  }
6995
- function licenseStep(deps = hostDeps$8) {
6986
+ function refusalOf(error) {
6987
+ if (!(error instanceof MetabaseApiError)) return null;
6988
+ if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
6989
+ if (error.status === STORE_UNREACHABLE_STATUS) return new ConfigError(`The instance could not reach ${TOKEN_CHECK_HOST} to check the license token; it needs outbound network access`);
6990
+ return null;
6991
+ }
6992
+ async function activate(client, token) {
6993
+ try {
6994
+ await client.setting.setPremiumEmbeddingToken(token);
6995
+ } catch (error) {
6996
+ const refusal = refusalOf(error);
6997
+ if (refusal === null) throw error;
6998
+ return {
6999
+ kind: "refused",
7000
+ error: refusal
7001
+ };
7002
+ }
7003
+ const status = await client.tokenStatus.get();
7004
+ if (!status.valid) return {
7005
+ kind: "refused",
7006
+ error: new ConfigError(`Metabase reports the token as ${status.status}`)
7007
+ };
6996
7008
  return {
6997
- id: STEP_ID$4,
7009
+ kind: "activated",
7010
+ status
7011
+ };
7012
+ }
7013
+ async function activateAskedToken(ctx, client, terminal) {
7014
+ const token = await ctx.prompts.password({
7015
+ key: LICENSE_KEYS.token,
7016
+ message: "Paste your Metabase license token (Enter to skip)",
7017
+ validate: validateToken
7018
+ });
7019
+ if (token.length === 0) return null;
7020
+ const spinner = ctx.ui.spinner("Activating the license…");
7021
+ let activation;
7022
+ try {
7023
+ activation = await activate(client, token);
7024
+ } catch (error) {
7025
+ spinner.fail(ACTIVATION_FAILED);
7026
+ throw error;
7027
+ }
7028
+ if (activation.kind === "activated") {
7029
+ spinner.stop(activeLabel(activation.status));
7030
+ ctx.session.licenseToken.set(token);
7031
+ return activation.status;
7032
+ }
7033
+ spinner.fail(ACTIVATION_FAILED);
7034
+ if (!terminal) throw activation.error;
7035
+ ctx.ui.warn(activation.error.message);
7036
+ return activateAskedToken(ctx, client, terminal);
7037
+ }
7038
+ function reportFeatures(ctx, features) {
7039
+ const { missing } = licensedFeatureCoverage(features);
7040
+ const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
7041
+ if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
7042
+ if (missing.length > 0) {
7043
+ const gitNote = remoteSync ? "" : "; the git step will be skipped";
7044
+ ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
7045
+ }
7046
+ }
7047
+ function licenseStep(deps = hostDeps$7) {
7048
+ return {
7049
+ id: STEP_ID$3,
6998
7050
  title: "Activate your license",
6999
7051
  redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
7000
7052
  async detect(ctx) {
7001
- if (ctx.state.steps[STEP_ID$4]?.status === "skipped") return "not-applicable";
7053
+ if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
7002
7054
  const configured = configuredRuntime(ctx.state);
7003
7055
  if (configured === null || ctx.secrets.apiKey === null) return "todo";
7004
7056
  const resolved = deps(ctx);
@@ -7016,36 +7068,17 @@ function licenseStep(deps = hostDeps$8) {
7016
7068
  },
7017
7069
  async run(ctx) {
7018
7070
  const configured = requireConfiguredRuntime(ctx.state);
7019
- const apiKey = requireApiKey$1(ctx);
7020
- const token = await ctx.prompts.password({
7021
- key: LICENSE_KEYS.token,
7022
- message: "Paste your Metabase license token (Enter to skip)",
7023
- validate: validateToken
7024
- });
7025
- if (token.length === 0) {
7026
- ctx.ui.info("Skipping the license; remote sync needs one. Run `rde init --from license` to add it later");
7027
- return { outcome: "skipped" };
7028
- }
7029
- ctx.session.licenseToken.set(token);
7071
+ const apiKey = requireApiKey(ctx);
7030
7072
  const resolved = deps(ctx);
7031
- const client = resolved.client(configured.url, {
7073
+ const status = await activateAskedToken(ctx, resolved.client(configured.url, {
7032
7074
  kind: "api-key",
7033
7075
  key: apiKey
7034
- });
7035
- const spinner = ctx.ui.spinner("Activating the license…");
7036
- let status;
7037
- try {
7038
- await client.setting.setPremiumEmbeddingToken(token);
7039
- status = await client.tokenStatus.get();
7040
- } catch (error) {
7041
- spinner.fail("License activation failed");
7042
- throw error;
7043
- }
7044
- if (!status.valid) {
7045
- spinner.fail("License activation failed");
7046
- throw new ConfigError(`Metabase reports the token as ${status.status}`);
7076
+ }), resolved.terminal);
7077
+ if (status === null) {
7078
+ const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
7079
+ ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
7080
+ return { outcome: "skipped" };
7047
7081
  }
7048
- spinner.stop(activeLabel(status));
7049
7082
  await ctx.saveState({
7050
7083
  ...ctx.state,
7051
7084
  license: {
@@ -7054,13 +7087,13 @@ function licenseStep(deps = hostDeps$8) {
7054
7087
  validThru: status["valid-thru"] ?? null
7055
7088
  }
7056
7089
  });
7057
- await mbTool(resolved.runner).authLogin({
7090
+ const mb = mbTool(resolved.runner);
7091
+ if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
7058
7092
  url: configured.url,
7059
7093
  profile: ctx.state.mbProfile,
7060
7094
  apiKey
7061
7095
  });
7062
- if (status.features.includes("remote-sync")) ctx.ui.info("Remote sync is included; the git step will set it up");
7063
- else ctx.ui.warn("This license does not include remote sync; the git step will be skipped");
7096
+ reportFeatures(ctx, status.features);
7064
7097
  return { outcome: "completed" };
7065
7098
  }
7066
7099
  };
@@ -7073,7 +7106,7 @@ const NODE_HINT = `Install Node ${MIN_NODE_VERSION} or newer from https://nodejs
7073
7106
  const MARK_OK = "✓";
7074
7107
  const MARK_MISSING = "✗";
7075
7108
  const MARK_OPTIONAL = "–";
7076
- const DOCKER_OPTIONAL_NOTE = `Metabase can run as a jar with Java instead, without the sample database`;
7109
+ const DOCKER_OPTIONAL_NOTE = `Metabase can run as a jar with Java instead`;
7077
7110
  const JAVA_OPTIONAL_NOTE = `Metabase can run in Docker instead`;
7078
7111
  const PLATFORMS = {
7079
7112
  darwin: {
@@ -7089,7 +7122,7 @@ const PLATFORMS = {
7089
7122
  javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
7090
7123
  }
7091
7124
  };
7092
- function hostDeps$7(ctx) {
7125
+ function hostDeps$6(ctx) {
7093
7126
  const runner = processRunner(ctx.signal);
7094
7127
  return {
7095
7128
  platform: process.platform,
@@ -7211,7 +7244,7 @@ function checkLine(check) {
7211
7244
  function checkLines(checks) {
7212
7245
  return checks.map(checkLine);
7213
7246
  }
7214
- function preflightStep(deps = hostDeps$7) {
7247
+ function preflightStep(deps = hostDeps$6) {
7215
7248
  return {
7216
7249
  id: "preflight",
7217
7250
  title: "Check this machine",
@@ -7230,7 +7263,7 @@ function preflightStep(deps = hostDeps$7) {
7230
7263
  }
7231
7264
  //#endregion
7232
7265
  //#region src/steps/runtime.ts
7233
- const STEP_ID$3 = "runtime";
7266
+ const STEP_ID$2 = "runtime";
7234
7267
  const MIN_PORT = 1;
7235
7268
  const MAX_PORT = 65535;
7236
7269
  const PORT_PATTERN = /^\d+$/;
@@ -7244,7 +7277,7 @@ const NO_RUNTIME_FLAGS = {
7244
7277
  jar: null,
7245
7278
  port: null
7246
7279
  };
7247
- function hostDeps$6(ctx) {
7280
+ function hostDeps$5(ctx) {
7248
7281
  return {
7249
7282
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
7250
7283
  releases: releaseLookup(ctx.signal)
@@ -7373,9 +7406,9 @@ async function persistedRuntime(ctx, options, deps) {
7373
7406
  });
7374
7407
  return chosen;
7375
7408
  }
7376
- function runtimeStep(options, deps = hostDeps$6) {
7409
+ function runtimeStep(options, deps = hostDeps$5) {
7377
7410
  return {
7378
- id: STEP_ID$3,
7411
+ id: STEP_ID$2,
7379
7412
  title: "Install Metabase",
7380
7413
  redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
7381
7414
  async detect(ctx) {
@@ -7399,207 +7432,6 @@ function runtimeStep(options, deps = hostDeps$6) {
7399
7432
  }
7400
7433
  };
7401
7434
  }
7402
- //#endregion
7403
- //#region src/steps/sample-database.ts
7404
- const STEP_ID$2 = "sample-database";
7405
- const SAMPLE_DATABASE_DISPLAY_NAME = "Sample Database";
7406
- const POSTGRES_ENGINE = "postgres";
7407
- const NOT_FOUND_STATUS$1 = 404;
7408
- function hostDeps$5(ctx) {
7409
- return {
7410
- runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
7411
- client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
7412
- readyTimeoutMs: SAMPLE_DATABASE_READY_TIMEOUT_MS,
7413
- now: () => /* @__PURE__ */ new Date()
7414
- };
7415
- }
7416
- function isNotServingYet$1(error) {
7417
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7418
- }
7419
- function isNotFound$2(error) {
7420
- return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1;
7421
- }
7422
- function requireApiKey(ctx) {
7423
- if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
7424
- return ctx.secrets.apiKey;
7425
- }
7426
- async function recordedDatabase(record, client) {
7427
- if (record.databaseId === null) return null;
7428
- try {
7429
- return await client.database.get(record.databaseId);
7430
- } catch (error) {
7431
- if (isNotFound$2(error)) return null;
7432
- throw error;
7433
- }
7434
- }
7435
- async function persistedRecord(ctx, deps) {
7436
- const existing = ctx.state.sampleDatabase;
7437
- if (existing !== null) {
7438
- if ((await sampleDatabaseContainer(existing, deps.runtime.docker, deps.runtime.rdeHome).status()).installed || await deps.runtime.ports.isFree(existing.port)) return {
7439
- record: existing,
7440
- staleDatabaseId: null
7441
- };
7442
- const pointer = existing.databaseId === null ? "" : `; Metabase database ${existing.databaseId} still points at the old one`;
7443
- ctx.ui.warn(`Port ${existing.port} is in use by another program; the sample database moves to a new port${pointer}`);
7444
- }
7445
- const claims = await portClaims(ctx.state, deps.runtime);
7446
- const port = await deps.runtime.ports.findFree(SAMPLE_DATABASE_PORT, claims);
7447
- if (port !== 25432) ctx.ui.warn(`Port ${SAMPLE_DATABASE_PORT} is in use or claimed; using ${port} for the sample database`);
7448
- const record = {
7449
- containerName: sampleContainerName(ctx.state.name, deps.runtime.rdeHome),
7450
- image: SAMPLE_DATABASE_IMAGE,
7451
- port,
7452
- databaseId: null
7453
- };
7454
- await ctx.saveState({
7455
- ...ctx.state,
7456
- sampleDatabase: record
7457
- });
7458
- return {
7459
- record,
7460
- staleDatabaseId: existing?.databaseId ?? null
7461
- };
7462
- }
7463
- const LINUX = "linux";
7464
- async function gatewayAddress(ctx, deps) {
7465
- if (!(ctx.state.runtime?.kind === "docker") || deps.runtime.platform !== LINUX) return null;
7466
- return deps.runtime.docker.bridgeGateway();
7467
- }
7468
- async function recordSampleDatabaseMove(ctx, from, to) {
7469
- ctx.ui.warn(`Port ${from.port} is taken; the sample database moved to ${to.port}`);
7470
- const record = {
7471
- ...to,
7472
- databaseId: null
7473
- };
7474
- await ctx.saveState({
7475
- ...ctx.state,
7476
- sampleDatabase: record,
7477
- databases: ctx.state.databases.filter((entry) => entry.id !== from.databaseId)
7478
- });
7479
- return record;
7480
- }
7481
- async function installAndStart(ctx, sidecar, installed, deps) {
7482
- const installing = ctx.ui.spinner(`Installing the sample database from ${installed.image}`);
7483
- try {
7484
- await sidecar.install({
7485
- onProgress: (line) => installing.update(line),
7486
- alsoOn: await gatewayAddress(ctx, deps)
7487
- });
7488
- } catch (error) {
7489
- installing.fail("Sample database install failed");
7490
- throw error;
7491
- }
7492
- installing.stop(`Sample database container ${installed.containerName} on port ${installed.port}`);
7493
- const started = await startOnFreePort(installed, deps.runtime.docker, deps.runtime.rdeHome);
7494
- const record = started.state.port === installed.port ? installed : await recordSampleDatabaseMove(ctx, installed, started.state);
7495
- const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
7496
- try {
7497
- await sidecar.waitReady({
7498
- timeoutMs: deps.readyTimeoutMs,
7499
- signal: ctx.signal
7500
- });
7501
- } catch (error) {
7502
- waiting.fail("Postgres did not become ready");
7503
- throw error;
7504
- }
7505
- waiting.stop(`Postgres is ready on ${LOOPBACK_HOST}:${record.port}`);
7506
- return record;
7507
- }
7508
- function connectionDetails(record, configured, deps) {
7509
- const alias = runtimeFromState(configured.runtime, deps.runtime).hostAlias();
7510
- return applyHostAlias({
7511
- host: "localhost",
7512
- port: record.port,
7513
- dbname: SAMPLE_DATABASE_NAME,
7514
- user: SAMPLE_DATABASE_USER,
7515
- password: SAMPLE_DATABASE_PASSWORD
7516
- }, alias).details;
7517
- }
7518
- function toRecord(database, createdAt) {
7519
- return {
7520
- id: database.id,
7521
- name: database.name,
7522
- engine: database.engine,
7523
- createdAt
7524
- };
7525
- }
7526
- async function connectDatabase(ctx, choice, configured, client, deps) {
7527
- const { record } = choice;
7528
- const known = await recordedDatabase(record, client);
7529
- if (known !== null) {
7530
- ctx.ui.info(`${known.name} is already connected as database ${known.id}`);
7531
- return known;
7532
- }
7533
- const spinner = ctx.ui.spinner(`Connecting ${SAMPLE_DATABASE_DISPLAY_NAME}…`);
7534
- let database;
7535
- try {
7536
- database = await client.database.create({
7537
- name: SAMPLE_DATABASE_DISPLAY_NAME,
7538
- engine: POSTGRES_ENGINE,
7539
- details: connectionDetails(record, configured, deps)
7540
- });
7541
- } catch (error) {
7542
- spinner.fail(`Could not connect ${SAMPLE_DATABASE_DISPLAY_NAME}`);
7543
- throw error;
7544
- }
7545
- const stale = /* @__PURE__ */ new Set([record.databaseId, choice.staleDatabaseId]);
7546
- const others = ctx.state.databases.filter((entry) => !stale.has(entry.id));
7547
- await ctx.saveState({
7548
- ...ctx.state,
7549
- sampleDatabase: {
7550
- ...record,
7551
- databaseId: database.id
7552
- },
7553
- databases: [...others, toRecord(database, deps.now().toISOString())]
7554
- });
7555
- spinner.stop(`Connected ${database.name} as database ${database.id}`);
7556
- return database;
7557
- }
7558
- function usageLines(record) {
7559
- return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms can write to this database."].join("\n");
7560
- }
7561
- function sampleDatabaseStep(deps = hostDeps$5) {
7562
- return {
7563
- id: STEP_ID$2,
7564
- title: "Connect a sample database",
7565
- async detect(ctx) {
7566
- const resolved = deps(ctx);
7567
- if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
7568
- const record = ctx.state.sampleDatabase;
7569
- const configured = configuredRuntime(ctx.state);
7570
- if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
7571
- if (!(await sampleDatabaseContainer(record, resolved.runtime.docker, resolved.runtime.rdeHome).status()).installed || !await isInstanceRunning(ctx.state, resolved.runtime)) return "todo";
7572
- const client = resolved.client(configured.url, {
7573
- kind: "api-key",
7574
- key: ctx.secrets.apiKey
7575
- });
7576
- try {
7577
- return await recordedDatabase(record, client) === null ? "todo" : "done";
7578
- } catch (error) {
7579
- if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
7580
- throw error;
7581
- }
7582
- },
7583
- async run(ctx) {
7584
- const resolved = deps(ctx);
7585
- const configured = requireConfiguredRuntime(ctx.state);
7586
- const apiKey = requireApiKey(ctx);
7587
- const docker = await resolved.runtime.docker.available();
7588
- if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
7589
- const persisted = await persistedRecord(ctx, resolved);
7590
- const record = await installAndStart(ctx, sampleDatabaseContainer(persisted.record, resolved.runtime.docker, resolved.runtime.rdeHome), persisted.record, resolved);
7591
- await connectDatabase(ctx, {
7592
- ...persisted,
7593
- record
7594
- }, configured, resolved.client(configured.url, {
7595
- kind: "api-key",
7596
- key: apiKey
7597
- }), resolved);
7598
- ctx.ui.note(usageLines(record), "Sample database");
7599
- return { outcome: "completed" };
7600
- }
7601
- };
7602
- }
7603
7435
  const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
7604
7436
  //#endregion
7605
7437
  //#region src/steps/start-metabase.ts
@@ -7613,7 +7445,7 @@ function hostDeps$4(ctx) {
7613
7445
  };
7614
7446
  }
7615
7447
  function isNotHealthy(error) {
7616
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || error instanceof MetabaseApiError;
7448
+ return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
7617
7449
  }
7618
7450
  async function runningVersion(client) {
7619
7451
  return (await client.session.properties()).version.tag;
@@ -7621,21 +7453,6 @@ async function runningVersion(client) {
7621
7453
  function progressLabel(progress) {
7622
7454
  return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
7623
7455
  }
7624
- async function startSampleDatabase(ctx, deps) {
7625
- const record = ctx.state.sampleDatabase;
7626
- if (record === null) return;
7627
- const started = await startOnFreePort(record, deps.runtime.docker, deps.runtime.rdeHome);
7628
- if (started.state.port !== record.port) await recordSampleDatabaseMove(ctx, record, started.state);
7629
- switch (started.outcome) {
7630
- case "missing":
7631
- ctx.ui.warn(`Sample database container ${record.containerName} is missing; \`rde init\` recreates it`);
7632
- return;
7633
- case "started":
7634
- ctx.ui.info(`Sample database container ${record.containerName} started`);
7635
- return;
7636
- case "running": return;
7637
- }
7638
- }
7639
7456
  async function withMoveHint(ctx, deps, taken) {
7640
7457
  const claims = await portClaims(ctx.state, deps.runtime);
7641
7458
  const free = await deps.runtime.ports.findFree(taken.port + 1, claims);
@@ -7672,7 +7489,6 @@ function startMetabaseStep(deps = hostDeps$4) {
7672
7489
  const runtime = runtimeFromState(configured.runtime, resolved.runtime);
7673
7490
  const client = resolved.client(configured.url, NO_AUTH);
7674
7491
  await startRuntime(ctx, runtime, resolved);
7675
- await startSampleDatabase(ctx, resolved);
7676
7492
  const spinner = ctx.ui.spinner("Starting Metabase…");
7677
7493
  let shown = 0;
7678
7494
  try {
@@ -7718,9 +7534,6 @@ function hostDeps$3(ctx) {
7718
7534
  probe: hostAgentProbe()
7719
7535
  };
7720
7536
  }
7721
- function isNotServing$1(error) {
7722
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7723
- }
7724
7537
  function loginLine(kind, email) {
7725
7538
  switch (kind) {
7726
7539
  case "generated":
@@ -7737,7 +7550,7 @@ async function databaseLines$1(state, apiKey, deps) {
7737
7550
  key: apiKey
7738
7551
  }).database.listWithStubs()).data;
7739
7552
  } catch (error) {
7740
- if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
7553
+ if (isMetabaseNotServing(error)) return ["Databases: Metabase is not running; `rde start`"];
7741
7554
  if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
7742
7555
  throw error;
7743
7556
  }
@@ -7786,7 +7599,6 @@ async function summaryLines(state, apiKey, deps) {
7786
7599
  if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
7787
7600
  lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
7788
7601
  lines.push(...await databaseLines$1(state, apiKey, deps));
7789
- if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
7790
7602
  lines.push(...gitLines(state));
7791
7603
  const installed = await skillsCli(deps.runner).list();
7792
7604
  const agents = await detectAgents(deps.probe);
@@ -7819,9 +7631,6 @@ function hostDeps$2(ctx) {
7819
7631
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
7820
7632
  };
7821
7633
  }
7822
- function isNotServingYet(error) {
7823
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7824
- }
7825
7634
  function transformsStep(deps = hostDeps$2) {
7826
7635
  return {
7827
7636
  id: STEP_ID,
@@ -7838,7 +7647,7 @@ function transformsStep(deps = hostDeps$2) {
7838
7647
  try {
7839
7648
  return await client.setting.transformsDecided() ? "done" : "todo";
7840
7649
  } catch (error) {
7841
- if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
7650
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
7842
7651
  throw error;
7843
7652
  }
7844
7653
  },
@@ -7865,7 +7674,7 @@ const DEFAULT_STEP_OPTIONS = {
7865
7674
  runtime: NO_RUNTIME_FLAGS,
7866
7675
  agents: DETECT_AGENTS,
7867
7676
  admin: DEFAULT_ADMIN,
7868
- databases: SAMPLE_STANDS_IN,
7677
+ databases: DATABASE_REQUIRED,
7869
7678
  updates: "published"
7870
7679
  };
7871
7680
  const LOCAL_STEP_OPTIONS = {
@@ -7881,7 +7690,6 @@ const STEP_BY_ID = {
7881
7690
  "admin-user": (options) => adminUserStep(options.admin),
7882
7691
  "api-key": () => apiKeyStep(),
7883
7692
  transforms: () => transformsStep(),
7884
- "sample-database": () => sampleDatabaseStep(),
7885
7693
  license: () => licenseStep(),
7886
7694
  databases: (options) => databasesStep(options.databases),
7887
7695
  "git-sync": () => gitSyncStep(),
@@ -7957,7 +7765,7 @@ const SECTIONS = {
7957
7765
  }).optional(),
7958
7766
  "api-key": z.strictObject({ profile: MbProfileChoice }).optional(),
7959
7767
  license: z.strictObject({ token: EnvString.nullable() }).optional(),
7960
- databases: z.array(DatabaseEntry).optional(),
7768
+ databases: z.array(DatabaseEntry).min(1, "a database is required: list one, or leave the section out to be asked at a terminal").optional(),
7961
7769
  "git-sync": z.strictObject({
7962
7770
  location: z.enum([
7963
7771
  "here",
@@ -8029,8 +7837,8 @@ function runtimeKindOf(runtime) {
8029
7837
  return null;
8030
7838
  }
8031
7839
  function databasesRequest(databases, run) {
8032
- if (databases !== void 0) return databases.length > 0 ? "named" : "sample-stands-in";
8033
- return run.interactive ? "ask-once" : "sample-stands-in";
7840
+ if (databases !== void 0) return "named";
7841
+ return run.interactive ? "ask" : "required";
8034
7842
  }
8035
7843
  function agentsOption(agents, run) {
8036
7844
  if (agents !== void 0) return {
@@ -8092,7 +7900,6 @@ async function loadInitAnswers(path) {
8092
7900
  }
8093
7901
  function stepsAskingForWork(answers) {
8094
7902
  const license = answers.license;
8095
- const databases = answers.databases;
8096
7903
  const git = answers["git-sync"];
8097
7904
  const agents = answers["agents-skills"];
8098
7905
  return [
@@ -8114,7 +7921,7 @@ function stepsAskingForWork(answers) {
8114
7921
  },
8115
7922
  {
8116
7923
  step: "databases",
8117
- asks: databases !== void 0 && databases.length > 0
7924
+ asks: answers.databases !== void 0
8118
7925
  },
8119
7926
  {
8120
7927
  step: "git-sync",
@@ -8151,7 +7958,6 @@ const FLAG_FOR_KEY$1 = {
8151
7958
  [API_KEY_KEYS.password]: "--admin-password",
8152
7959
  [API_KEY_KEYS.profile]: "--mb-profile",
8153
7960
  [LICENSE_KEYS.token]: "--license-token or --skip-license",
8154
- [DATABASE_KEYS.skip]: "--skip-databases",
8155
7961
  [GIT_SYNC_KEYS.location]: "--git-dir",
8156
7962
  [GIT_SYNC_KEYS.path]: "--git-dir",
8157
7963
  [GIT_SYNC_KEYS.remote]: "--git-remote or --skip-git",
@@ -8159,7 +7965,7 @@ const FLAG_FOR_KEY$1 = {
8159
7965
  [GIT_SYNC_KEYS.token]: "--git-token"
8160
7966
  };
8161
7967
  const DATABASE_FLAG_FOR_NAME = {
8162
- engine: "--database or --skip-databases",
7968
+ engine: "--database",
8163
7969
  uri: "--database"
8164
7970
  };
8165
7971
  function howToAnswer(key) {
@@ -8250,13 +8056,6 @@ function licenseSection(flags, secrets) {
8250
8056
  }
8251
8057
  return secrets.has("--license-token") ? { token: secrets.get("--license-token") } : void 0;
8252
8058
  }
8253
- function databasesSection(flags) {
8254
- if (flags.skipDatabases) {
8255
- if (flags.database.length > 0) throw conflict("--skip-databases", "--database");
8256
- return [];
8257
- }
8258
- return flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0;
8259
- }
8260
8059
  function gitRemoteKind(flag, url) {
8261
8060
  if (flag === void 0) return url === void 0 ? void 0 : "hosted";
8262
8061
  const kind = GIT_REMOTE_NAMES[flag];
@@ -8377,7 +8176,7 @@ async function answersFromFlags(flags, readStdin) {
8377
8176
  runtime: runtimeSection(flags),
8378
8177
  "admin-user": adminSection(flags, secrets),
8379
8178
  license: licenseSection(flags, secrets),
8380
- databases: databasesSection(flags),
8179
+ databases: flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0,
8381
8180
  "git-sync": gitSection(flags, secrets),
8382
8181
  "agents-skills": agentsSection(flags),
8383
8182
  "api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
@@ -8802,6 +8601,7 @@ const dbAdd = defineRdeCommand({
8802
8601
  description: "Engine of a --url that fits several engines; the error names them"
8803
8602
  }
8804
8603
  },
8604
+ interrupted: INTERRUPTED,
8805
8605
  async run(args) {
8806
8606
  if (args.url === void 0 && !stdinIsTty()) throw new ConfigError(NEEDS_URL_ERROR);
8807
8607
  const name = await defaultInstanceName();
@@ -8833,6 +8633,7 @@ const dbList = defineRdeCommand({
8833
8633
  description: "Print the list as JSON",
8834
8634
  default: false
8835
8635
  } },
8636
+ interrupted: INTERRUPTED,
8836
8637
  async run(args) {
8837
8638
  const name = await defaultInstanceName();
8838
8639
  const state = await requireInstanceState(name);
@@ -9365,31 +9166,46 @@ async function credentialRows(state, secrets, apiKey, deps) {
9365
9166
  stateFileRow(files)
9366
9167
  ];
9367
9168
  }
9368
- function withoutTrailingSlash(url) {
9369
- return url.replace(/\/+$/, "");
9169
+ function brokenProfileRepair(profile, method) {
9170
+ if (method === MbAuthMethod.enum.oauth) return {
9171
+ hint: `\`mb auth login --profile ${profile}\` logs it in again; or \`mb auth logout --profile ${profile}\`, then \`rde init --only api-key\` hands the profile to rde's API key`,
9172
+ step: null
9173
+ };
9174
+ return {
9175
+ hint: "`rde init --only api-key` logs it in again",
9176
+ step: API_KEY_STEP
9177
+ };
9178
+ }
9179
+ function unprobedReason(method, healthy, access) {
9180
+ if (access.kind === "accepted") return null;
9181
+ if (method !== MbAuthMethod.enum.oauth) return access.reason;
9182
+ return healthy ? null : NOT_HEALTHY_REASON;
9370
9183
  }
9371
- async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
9184
+ async function mbProfileRow(state, url, deps, profiles, mbUnusableReason, healthy, access) {
9185
+ const id = "mb-profile";
9372
9186
  const label = "mb profile";
9373
- if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
9374
- if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY_REASON);
9375
- const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
9376
- if (entry === void 0) return failed("mb-profile", label, {
9377
- detail: `${state.mbProfile} is not configured`,
9187
+ const profile = state.mbProfile;
9188
+ if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
9189
+ const status = await mbTool(deps.runner).authStatus(profile);
9190
+ if (!status.present || status.url === null) return failed(id, label, {
9191
+ detail: `${profile} is not configured`,
9378
9192
  hint: "`rde init --only api-key`",
9379
- step: "api-key"
9193
+ step: API_KEY_STEP
9380
9194
  });
9381
- if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
9382
- detail: `${state.mbProfile} points at ${entry.url}, not at this instance (${url})`,
9195
+ if (!sameMetabaseUrl(status.url, url)) return failed(id, label, {
9196
+ detail: `${profile} points at ${status.url}, not at this instance (${url})`,
9383
9197
  hint: "`rde init --only api-key` logs it back in against the instance",
9384
- step: "api-key"
9198
+ step: API_KEY_STEP
9385
9199
  });
9386
- if (entry.status !== "ok") return failed("mb-profile", label, {
9387
- detail: `${state.mbProfile}: ${entry.status}`,
9388
- hint: "`rde init --only api-key` logs it in again",
9389
- step: "api-key"
9200
+ const unprobed = unprobedReason(status.method, healthy, access);
9201
+ if (unprobed !== null) return skipped(id, label, unprobed);
9202
+ const how = status.method === MbAuthMethod.enum.oauth ? " (OAuth)" : "";
9203
+ const entry = (await profiles()).find((candidate) => candidate.profile === profile);
9204
+ if (entry === void 0 || entry.status !== "ok") return failed(id, label, {
9205
+ detail: `${profile}${how}: ${entry === void 0 ? "not listed by `mb auth list`" : entry.status}`,
9206
+ ...brokenProfileRepair(profile, status.method)
9390
9207
  });
9391
- const user = entry.user === null ? "" : ` as ${entry.user.name}`;
9392
- return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
9208
+ return ok(id, label, `${profile}${how}: ok${entry.user === null ? "" : ` as ${entry.user.name}`}`);
9393
9209
  }
9394
9210
  function hostOf(url) {
9395
9211
  return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
@@ -9410,8 +9226,8 @@ async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
9410
9226
  async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
9411
9227
  const rows = [];
9412
9228
  if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
9413
- if (env.url !== null) rows.push(note$1(`env.${ENV_MB_URL}`, ENV_MB_URL, `every \`mb\` call goes to ${hostOf(env.url)}, whatever \`--profile\` says, rde's own included`, unsetHint(ENV_MB_URL)));
9414
- if (env.apiKeySet) rows.push(note$1(`env.${ENV_MB_API_KEY}`, ENV_MB_API_KEY, "set (value not shown): every `mb` call sends it, whatever `--profile` says, rde's own included", unsetHint(ENV_MB_API_KEY)));
9229
+ if (env.url !== null) rows.push(note$1(`env.${ENV_MB_URL}`, ENV_MB_URL, `every \`mb\` call goes to ${hostOf(env.url)}, whatever \`--profile\` says`, unsetHint(ENV_MB_URL)));
9230
+ if (env.apiKeySet) rows.push(note$1(`env.${ENV_MB_API_KEY}`, ENV_MB_API_KEY, "set (value not shown): every `mb` call sends it, whatever `--profile` says", unsetHint(ENV_MB_API_KEY)));
9415
9231
  return rows;
9416
9232
  }
9417
9233
  const PROFILE_STATUS_WORDS = {
@@ -9441,6 +9257,11 @@ async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
9441
9257
  function isNotFound$1(error) {
9442
9258
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
9443
9259
  }
9260
+ function coverageDetail(coverage) {
9261
+ const valid = coverage.included.length === 0 ? "valid" : `valid, with ${listOf(coverage.included, "conjunction")}`;
9262
+ if (coverage.missing.length === 0) return valid;
9263
+ return `${valid}; without ${listOf(coverage.missing, "disjunction")}`;
9264
+ }
9444
9265
  async function licenseRow(client) {
9445
9266
  const label = "license";
9446
9267
  try {
@@ -9450,9 +9271,9 @@ async function licenseRow(client) {
9450
9271
  hint: "`rde init --from license` with a valid token",
9451
9272
  step: "license"
9452
9273
  });
9453
- return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
9274
+ return ok("license", label, coverageDetail(licensedFeatureCoverage(status.features)));
9454
9275
  } catch (error) {
9455
- if (isNotFound$1(error)) return skipped("license", label, "not activated; `rde init --from license` adds one");
9276
+ if (isNotFound$1(error)) return skipped("license", label, `not activated, so ${listOf(LICENSED_FEATURE_NAMES, "conjunction")} are unavailable; \`rde init --from license\` adds one`);
9456
9277
  throw error;
9457
9278
  }
9458
9279
  }
@@ -9470,55 +9291,16 @@ async function databasesRow(state, client) {
9470
9291
  const names = state.databases.map((record) => record.name).join(", ");
9471
9292
  return ok("databases", label, `${state.databases.length} connected: ${names}`);
9472
9293
  }
9473
- const SAMPLE_DATABASE_STEP = "sample-database";
9474
- const SAMPLE_LABEL = "sample database";
9475
- async function sampleDatabaseRow(record, deps, dockerOk, access) {
9476
- const id = "sample-database";
9477
- if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
9478
- const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
9479
- if (!status.installed) return failed(id, SAMPLE_LABEL, {
9480
- detail: `${record.containerName} does not exist`,
9481
- hint: "`rde init` recreates it",
9482
- step: SAMPLE_DATABASE_STEP
9483
- });
9484
- if (!status.running) return failed(id, SAMPLE_LABEL, {
9485
- detail: `${record.containerName} is stopped`,
9486
- hint: "`rde start`",
9487
- step: "start-metabase"
9488
- });
9489
- if (await deps.runtime.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
9490
- detail: `${record.containerName} runs but port ${record.port} is not answering`,
9491
- hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
9492
- step: null
9493
- });
9494
- const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
9495
- if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
9496
- detail: `${where}; not connected to Metabase yet`,
9497
- hint: "`rde init` connects it",
9498
- step: SAMPLE_DATABASE_STEP
9499
- });
9500
- if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
9501
- try {
9502
- const database = await access.client.database.get(record.databaseId);
9503
- return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} ${syncLabel(database.initial_sync_status)}`);
9504
- } catch (error) {
9505
- if (isNotFound$1(error)) return failed(id, SAMPLE_LABEL, {
9506
- detail: `${where}; database ${record.databaseId} is gone from Metabase`,
9507
- hint: "`rde init` connects it again",
9508
- step: SAMPLE_DATABASE_STEP
9509
- });
9510
- throw error;
9511
- }
9512
- }
9513
9294
  function isMbFailure$1(error) {
9514
9295
  return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
9515
9296
  }
9516
- async function gitSyncRow(state, deps) {
9297
+ async function gitSyncRow(state, instance, deps) {
9517
9298
  const label = "remote sync";
9518
9299
  if (!hasRemoteSync(state.license)) return skipped("git-sync", label, `needs a license with ${REMOTE_SYNC_FEATURE}`);
9519
9300
  if (state.gitRemote === null) return skipped("git-sync", label, "not configured; `rde init --from git-sync`");
9301
+ if (instance === null) return skipped("git-sync", label, "no API key in the secrets file");
9520
9302
  try {
9521
- const status = await mbTool(deps.runner).gitSyncStatus(state.mbProfile);
9303
+ const status = await mbTool(deps.runner).gitSyncStatus(instance);
9522
9304
  if (status.branch === null) return failed("git-sync", label, {
9523
9305
  detail: "Metabase reports no branch",
9524
9306
  hint: "`rde init --only git-sync`",
@@ -9662,7 +9444,6 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
9662
9444
  skipped("mb-profile", "mb profile", NO_INSTANCE_REASON),
9663
9445
  skipped("license", "license", NO_INSTANCE_REASON),
9664
9446
  skipped("databases", "databases", NO_INSTANCE_REASON),
9665
- skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE_REASON),
9666
9447
  skipped("git-sync", "remote sync", NO_INSTANCE_REASON)
9667
9448
  ];
9668
9449
  const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
@@ -9676,13 +9457,12 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
9676
9457
  properties === null ? skipped("version", "Metabase version", NOT_HEALTHY_REASON) : versionRow(state, properties),
9677
9458
  properties === null ? skipped("admin", "admin user", NOT_HEALTHY_REASON) : adminRow(state, properties),
9678
9459
  ...await credentialRows(state, secrets, apiKey, deps),
9679
- await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
9460
+ await mbProfileRow(state, configured.url, deps, profiles, mbUnusableReason, healthy, apiKey.access)
9680
9461
  ];
9681
9462
  const access = apiKey.access;
9682
9463
  if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
9683
9464
  else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
9684
- rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
9685
- rows.push(await gitSyncRow(state, deps));
9465
+ rows.push(await gitSyncRow(state, mbInstanceOf(state, secrets), deps));
9686
9466
  return rows;
9687
9467
  }
9688
9468
  async function collectDoctor(instance, state, secrets, deps) {
@@ -9691,7 +9471,7 @@ async function collectDoctor(instance, state, secrets, deps) {
9691
9471
  const mb = await mbFinding(deps);
9692
9472
  const profiles = sharedProfileList(deps);
9693
9473
  const checks = [...machine, mb.row];
9694
- if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE_REASON), skipped("admin", "admin user", NO_INSTANCE_REASON), skipped("admin-password", "admin password", NO_INSTANCE_REASON), skipped("api-key", "API key", NO_INSTANCE_REASON), skipped("secrets-file", "secrets file", NO_INSTANCE_REASON), skipped("state-file", "state file", NO_INSTANCE_REASON), skipped("mb-profile", "mb profile", NO_INSTANCE_REASON), skipped("license", "license", NO_INSTANCE_REASON), skipped("databases", "databases", NO_INSTANCE_REASON), skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE_REASON), skipped("git-sync", "remote sync", NO_INSTANCE_REASON));
9474
+ if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE_REASON), skipped("admin", "admin user", NO_INSTANCE_REASON), skipped("admin-password", "admin password", NO_INSTANCE_REASON), skipped("api-key", "API key", NO_INSTANCE_REASON), skipped("secrets-file", "secrets file", NO_INSTANCE_REASON), skipped("state-file", "state file", NO_INSTANCE_REASON), skipped("mb-profile", "mb profile", NO_INSTANCE_REASON), skipped("license", "license", NO_INSTANCE_REASON), skipped("databases", "databases", NO_INSTANCE_REASON), skipped("git-sync", "remote sync", NO_INSTANCE_REASON));
9695
9475
  else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
9696
9476
  const rdeProfile = state === null ? null : state.mbProfile;
9697
9477
  checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
@@ -9798,6 +9578,7 @@ const doctor = defineRdeCommand({
9798
9578
  default: false
9799
9579
  }
9800
9580
  },
9581
+ interrupted: INTERRUPTED,
9801
9582
  async run(args) {
9802
9583
  const name = await instanceNameOrDefault();
9803
9584
  const state = await readInstanceState(name);
@@ -9856,13 +9637,6 @@ function isLocation(value) {
9856
9637
  function isRemote(value) {
9857
9638
  return value === "local-bare" || value === "hosted";
9858
9639
  }
9859
- function applyDatabaseRecord(draft, record) {
9860
- const value = stringOf(record.answer);
9861
- const skippedConnection = record.key === DATABASE_KEYS.connection && value?.trim() === "";
9862
- const finished = parseDatabaseKey(record.key)?.name === "engine" && value === "finish";
9863
- const pickedNoStub = record.key === DATABASE_KEYS.stubs && record.answer.kind === "multiselect" && record.answer.values.length === 0;
9864
- if (skippedConnection || finished || pickedNoStub) draft.databasesFinished = true;
9865
- }
9866
9640
  function applyAdminRecord(draft, key, value) {
9867
9641
  if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
9868
9642
  else if (key === ADMIN_USER_KEYS.password) admin(draft).password = envReference(ENV_ADMIN_PASSWORD);
@@ -9886,8 +9660,7 @@ function agentIds(values) {
9886
9660
  function applyRecord(draft, record) {
9887
9661
  const { key } = record;
9888
9662
  const value = stringOf(record.answer);
9889
- if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
9890
- else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
9663
+ if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
9891
9664
  else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
9892
9665
  else if (key === AGENTS_SKILLS_KEYS.agents && record.answer.kind === "multiselect") draft.answers["agents-skills"] = { agents: agentIds(record.answer.values) };
9893
9666
  else if (key === API_KEY_KEYS.profile && value !== null) {
@@ -9913,13 +9686,9 @@ function databaseEntry(added, index) {
9913
9686
  };
9914
9687
  }
9915
9688
  function answersFromRecords(records, overrides, added) {
9916
- const draft = {
9917
- answers: {},
9918
- databasesFinished: false
9919
- };
9689
+ const draft = { answers: {} };
9920
9690
  for (const record of records) applyRecord(draft, record);
9921
- const databases = added.map(databaseEntry);
9922
- if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
9691
+ if (added.length > 0) draft.answers.databases = added.map(databaseEntry);
9923
9692
  return {
9924
9693
  ...draft.answers,
9925
9694
  ...overrides.runtime !== void 0 && { runtime: overrides.runtime },
@@ -10045,7 +9814,7 @@ const init = defineRdeCommand({
10045
9814
  },
10046
9815
  runtime: {
10047
9816
  type: "string",
10048
- description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available; the sample database always runs in Docker`
9817
+ description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available`
10049
9818
  },
10050
9819
  image: {
10051
9820
  type: "string",
@@ -10090,12 +9859,7 @@ const init = defineRdeCommand({
10090
9859
  },
10091
9860
  database: {
10092
9861
  type: "string",
10093
- description: "Database to connect, a connection string or <engine>=<connection string> when it fits several engines; repeatable"
10094
- },
10095
- "skip-databases": {
10096
- type: "boolean",
10097
- description: "Connect none of your own databases (the sample database is still connected)",
10098
- default: false
9862
+ description: "Database to connect, a connection string or <engine>=<connection string> when it fits several engines; repeatable. init needs one, and asks for it only at a terminal"
10099
9863
  },
10100
9864
  "git-dir": {
10101
9865
  type: "string",
@@ -10133,10 +9897,11 @@ const init = defineRdeCommand({
10133
9897
  },
10134
9898
  yes: {
10135
9899
  type: "boolean",
10136
- description: "Answer yes to every confirmation and take the default of every setting; skips the question about your own databases (the sample database is still connected)",
9900
+ description: "Answer yes to every confirmation and take the default of every setting; the database init needs then comes from --database or the answers file",
10137
9901
  default: false
10138
9902
  }
10139
9903
  },
9904
+ interrupted: "Interrupted. Run `rde init` again to resume.",
10140
9905
  async run(args, rawArgs) {
10141
9906
  const options = stepSelection(args.from, args.only);
10142
9907
  const runOptions = {
@@ -10153,7 +9918,6 @@ const init = defineRdeCommand({
10153
9918
  licenseToken: args["license-token"],
10154
9919
  skipLicense: args["skip-license"],
10155
9920
  database: repeatedValues(rawArgs, "--database"),
10156
- skipDatabases: args["skip-databases"],
10157
9921
  gitDir: args["git-dir"],
10158
9922
  gitRemote: args["git-remote"],
10159
9923
  gitUrl: args["git-url"],
@@ -10200,6 +9964,7 @@ const logs = defineRdeCommand({
10200
9964
  description: "Keep streaming new lines",
10201
9965
  default: false
10202
9966
  } },
9967
+ interrupted: null,
10203
9968
  async run(args) {
10204
9969
  await (await loadInstance(await defaultInstanceName(), interruptSignal)).runtime.logs({
10205
9970
  follow: args.follow,
@@ -10214,6 +9979,7 @@ const open = defineRdeCommand({
10214
9979
  name: "open",
10215
9980
  description: "Open the Metabase instance in the browser"
10216
9981
  },
9982
+ interrupted: INTERRUPTED,
10217
9983
  async run() {
10218
9984
  const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
10219
9985
  printLines([await openInstance(instance.url) ? `Opened ${instance.url}` : `Open ${instance.url} in your browser`]);
@@ -10257,20 +10023,12 @@ async function moveToPort(ctx, port, deps) {
10257
10023
  await runtimeFromState(next, deps.runtime).reconfigure();
10258
10024
  ctx.ui.info(`Moved instance ${ctx.state.name} from port ${previous} to ${port}`);
10259
10025
  }
10260
- async function profileStatus(runner, profile) {
10261
- try {
10262
- return await mbTool(runner).authStatus(profile);
10263
- } catch (error) {
10264
- if (error instanceof ProcessNotFoundError) return null;
10265
- throw error;
10266
- }
10267
- }
10268
10026
  async function followMove(ctx, from, runner) {
10269
10027
  const configured = requireConfiguredRuntime(ctx.state);
10270
10028
  const apiKey = ctx.secrets.apiKey;
10271
10029
  const profile = ctx.state.mbProfile;
10272
- const status = apiKey === null ? null : await profileStatus(runner, profile);
10273
- if (apiKey !== null && status !== null && status.present && status.url === from) {
10030
+ const status = apiKey === null ? null : await mbTool(runner).authStatusIfInstalled(profile);
10031
+ if (apiKey !== null && isLoggedInAt(status, from)) {
10274
10032
  await mbTool(runner).authLogin({
10275
10033
  url: configured.url,
10276
10034
  profile,
@@ -10301,6 +10059,7 @@ const start = defineRdeCommand({
10301
10059
  type: "string",
10302
10060
  description: "Move the instance to this host port first; the mb profile follows, the data stays"
10303
10061
  } },
10062
+ interrupted: INTERRUPTED,
10304
10063
  async run(args) {
10305
10064
  const port = args.port === void 0 ? null : requirePortFlag(args.port);
10306
10065
  const name = await defaultInstanceName();
@@ -10353,31 +10112,15 @@ function setupOf(ctx, steps) {
10353
10112
  resume: `rde init --from ${pending.id}`
10354
10113
  };
10355
10114
  }
10356
- function isNotServing(error) {
10357
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
10358
- }
10359
10115
  async function databasesOf(ctx, deps, health) {
10360
10116
  if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
10361
10117
  try {
10362
10118
  return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
10363
10119
  } catch (error) {
10364
- if (isNotServing(error) || isRejectedCredential(error)) return null;
10120
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return null;
10365
10121
  throw error;
10366
10122
  }
10367
10123
  }
10368
- async function sampleDatabaseOf(ctx, deps, databases) {
10369
- const record = ctx.state.sampleDatabase;
10370
- if (record === null) return null;
10371
- const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
10372
- const listed = databases?.find((database) => database.id === record.databaseId) ?? null;
10373
- return {
10374
- container: status.running ? "running" : status.installed ? "stopped" : "missing",
10375
- port: record.port,
10376
- url: sampleDatabaseUrl(record.port),
10377
- databaseId: record.databaseId,
10378
- initialSyncStatus: listed === null ? null : listed.initialSyncStatus
10379
- };
10380
- }
10381
10124
  function taskLabel(status) {
10382
10125
  const task = status.current_task;
10383
10126
  return task === null ? null : `${task.sync_task_type} ${task.status}`;
@@ -10391,10 +10134,13 @@ function isMbFailure(error) {
10391
10134
  }
10392
10135
  async function gitSyncOf(ctx, deps) {
10393
10136
  if (ctx.state.gitRemote === null) return null;
10137
+ if (configuredRuntime(ctx.state) === null) return { error: "no Metabase runtime configured; `rde init`" };
10138
+ const instance = mbInstanceOf(ctx.state, ctx.secrets);
10139
+ if (instance === null) return { error: "no API key in the secrets file; `rde init --only api-key`" };
10394
10140
  try {
10395
10141
  const mb = mbTool(deps.runner);
10396
- const status = await mb.gitSyncStatus(ctx.state.mbProfile);
10397
- const changes = await mb.gitSyncHasRemoteChanges(ctx.state.mbProfile);
10142
+ const status = await mb.gitSyncStatus(instance);
10143
+ const changes = await mb.gitSyncHasRemoteChanges(instance);
10398
10144
  return {
10399
10145
  branch: status.branch,
10400
10146
  dirty: status.is_dirty,
@@ -10429,7 +10175,6 @@ async function collectStatus(ctx, deps) {
10429
10175
  steps,
10430
10176
  setup: setupOf(ctx, steps),
10431
10177
  databases,
10432
- sampleDatabase: await sampleDatabaseOf(ctx, deps, databases),
10433
10178
  gitSync: await gitSyncOf(ctx, deps),
10434
10179
  contentRepository: ctx.state.gitRemote === null ? null : ctx.state.gitRemote.workdir
10435
10180
  };
@@ -10473,22 +10218,6 @@ function syncStateLabel(profile, gitSync) {
10473
10218
  if (gitSync.dirty) return `unsynced changes, ${remote}`;
10474
10219
  return `${remote}; \`${mbCommand(profile)} git-sync import\``;
10475
10220
  }
10476
- const CONTAINER_STATE_LABEL = {
10477
- running: "container running",
10478
- stopped: "container stopped; `rde start`",
10479
- missing: "container missing; `rde init` recreates it"
10480
- };
10481
- function sampleConnectionLabel(report, sample) {
10482
- if (sample.databaseId === null) return "not connected to Metabase yet; `rde init`";
10483
- if (report.databases === null) return "sync unknown while Metabase is not running";
10484
- if (sample.initialSyncStatus === null) return `database ${sample.databaseId} is gone from Metabase; \`rde init\` connects it again`;
10485
- return syncLabel(sample.initialSyncStatus);
10486
- }
10487
- function sampleDatabaseLine(report) {
10488
- const sample = report.sampleDatabase;
10489
- if (sample === null) return "Sample database: not connected; `rde init` adds one";
10490
- return `Sample database: ${sample.url}, ${CONTAINER_STATE_LABEL[sample.container]}, ${sampleConnectionLabel(report, sample)}`;
10491
- }
10492
10221
  function databaseLines(report) {
10493
10222
  if (report.databases === null) return ["Databases: unavailable while Metabase is not running"];
10494
10223
  return ["Databases:", ...databaseListingLines(report.databases).map((line) => ` ${line}`)];
@@ -10505,7 +10234,6 @@ function statusLines(report) {
10505
10234
  "Steps:",
10506
10235
  ...checklistLines(report.steps).map((line) => ` ${line}`),
10507
10236
  ...databaseLines(report),
10508
- sampleDatabaseLine(report),
10509
10237
  gitSyncLine(report),
10510
10238
  ...report.contentRepository === null ? [] : [`Content repository: ${report.contentRepository}`]
10511
10239
  ];
@@ -10522,6 +10250,7 @@ const status = defineRdeCommand({
10522
10250
  description: "Print the report as JSON",
10523
10251
  default: false
10524
10252
  } },
10253
+ interrupted: INTERRUPTED,
10525
10254
  async run(args) {
10526
10255
  const name = await defaultInstanceName();
10527
10256
  await requireInstanceState(name);
@@ -10538,34 +10267,34 @@ const status = defineRdeCommand({
10538
10267
  const stop = defineRdeCommand({
10539
10268
  meta: {
10540
10269
  name: "stop",
10541
- description: "Stop the Metabase instance and its sample database"
10270
+ description: "Stop the Metabase instance"
10542
10271
  },
10272
+ interrupted: INTERRUPTED,
10543
10273
  async run() {
10544
- const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
10274
+ const name = await defaultInstanceName();
10275
+ const instance = await loadInstance(name, interruptSignal);
10545
10276
  const before = await instance.runtime.status();
10546
10277
  await instance.runtime.stop();
10547
10278
  const lines = [before.running ? "Metabase stopped" : "Metabase is not running"];
10548
- if (instance.sampleDatabase !== null) {
10549
- const sidecar = await instance.sampleDatabase.status();
10550
- await instance.sampleDatabase.stop();
10551
- lines.push(sidecar.running ? "Sample database stopped" : "Sample database is not running");
10552
- }
10279
+ const removed = await removeLeftovers(name, interruptSignal);
10280
+ if (removed !== null) lines.push(removed);
10553
10281
  printLines(lines);
10554
10282
  }
10555
10283
  });
10556
10284
  //#endregion
10557
10285
  //#region src/steps/uninstall.ts
10558
- const API_KEY_METHOD = "apiKey";
10559
10286
  function hostUninstallDeps(signal, childOutput) {
10560
10287
  const runner = processRunner(signal);
10561
10288
  return {
10562
10289
  docker: dockerTool(runner),
10563
10290
  daemon: daemonTool(runner),
10291
+ git: gitTool(runner),
10564
10292
  runner,
10565
10293
  childOutput
10566
10294
  };
10567
10295
  }
10568
10296
  const NOT_TOUCHED_LINE = "The mb CLI, Docker, and Java themselves";
10297
+ const UNPULLED_COMMITS_SHOWN = 10;
10569
10298
  const ANY_JAR = "-jar";
10570
10299
  function isFileSystemError(error) {
10571
10300
  return error instanceof Error && "code" in error;
@@ -10714,8 +10443,8 @@ async function findJar(source, removeImage) {
10714
10443
  }
10715
10444
  function isInstanceProfile(status, urls) {
10716
10445
  if (!status.present) return false;
10717
- if (urls !== null) return status.url !== null && urls.includes(status.url);
10718
- return status.method === API_KEY_METHOD && status.user?.name === "rde";
10446
+ if (urls !== null) return urls.some((url) => isLoggedInAt(status, url));
10447
+ return status.method === MbAuthMethod.enum.apiKey && status.user?.name === "rde";
10719
10448
  }
10720
10449
  function instanceUrls(state) {
10721
10450
  if (state.url === null) return null;
@@ -10723,23 +10452,34 @@ function instanceUrls(state) {
10723
10452
  }
10724
10453
  async function findMbProfiles(candidates, urls, runner) {
10725
10454
  const mb = mbTool(runner);
10726
- const removals = [];
10455
+ const found = {
10456
+ removals: [],
10457
+ kept: []
10458
+ };
10727
10459
  for (const profile of candidates) {
10728
10460
  let status;
10729
10461
  try {
10730
10462
  status = await mb.authStatus(profile);
10731
10463
  } catch (error) {
10732
- if (error instanceof ProcessNotFoundError) return [];
10464
+ if (error instanceof ProcessNotFoundError) return {
10465
+ removals: [],
10466
+ kept: []
10467
+ };
10733
10468
  throw error;
10734
10469
  }
10735
- if (isInstanceProfile(status, urls)) removals.push({
10470
+ if (!isInstanceProfile(status, urls)) continue;
10471
+ if (status.method === MbAuthMethod.enum.oauth) {
10472
+ found.kept.push(`mb profile ${profile}, logged in with OAuth; \`mb auth logout --profile ${profile}\` logs it out`);
10473
+ continue;
10474
+ }
10475
+ found.removals.push({
10736
10476
  label: `mb profile ${profile}`,
10737
10477
  remove: async () => {
10738
10478
  await mb.authLogout(profile);
10739
10479
  }
10740
10480
  });
10741
10481
  }
10742
- return removals;
10482
+ return found;
10743
10483
  }
10744
10484
  function skillLabel(names, entries) {
10745
10485
  const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
@@ -10760,6 +10500,71 @@ async function findSkill(runner, childOutput) {
10760
10500
  function instanceDirLabel(path, state) {
10761
10501
  return `Instance directory ${path} (${state?.gitRemote?.kind === "local-bare" ? "Metabase data and the remote-sync repository content.git" : "Metabase data"})`;
10762
10502
  }
10503
+ async function findUnpulled(remote, git) {
10504
+ if (!await pathExists(remote.barePath)) return null;
10505
+ try {
10506
+ if (!await git.isBareRepo(remote.barePath)) return null;
10507
+ const commits = await git.bareBranchCommits(remote.barePath, remote.branch);
10508
+ if (commits === null || commits.length === 0) return null;
10509
+ if (!await git.isRepo(remote.workdir)) return {
10510
+ kind: "orphaned",
10511
+ remote,
10512
+ commits
10513
+ };
10514
+ const reachable = await git.reachableCommits(remote.workdir);
10515
+ const missing = commits.filter((commit) => !reachable.has(commit.sha));
10516
+ return missing.length === 0 ? null : {
10517
+ kind: "behind",
10518
+ remote,
10519
+ commits: missing
10520
+ };
10521
+ } catch (error) {
10522
+ if (isToolFailure(error)) return {
10523
+ kind: "unchecked",
10524
+ remote,
10525
+ reason: error.message
10526
+ };
10527
+ throw error;
10528
+ }
10529
+ }
10530
+ function commitCount(commits) {
10531
+ return commits.length === 1 ? "1 commit" : `${commits.length} commits`;
10532
+ }
10533
+ function unpulledSummary(unpulled) {
10534
+ const { barePath, workdir, branch } = unpulled.remote;
10535
+ switch (unpulled.kind) {
10536
+ case "behind": return `${barePath} has ${commitCount(unpulled.commits)} on ${branch} that ${workdir} lacks; uninstalling deletes them`;
10537
+ case "orphaned": return `${barePath} has ${commitCount(unpulled.commits)} on ${branch}, and ${workdir} is gone or no longer a git repository; uninstalling deletes them`;
10538
+ case "unchecked": return `${barePath} cannot be compared with ${workdir} (${unpulled.reason}); uninstalling deletes it with any commits ${workdir} lacks`;
10539
+ }
10540
+ }
10541
+ function unpulledRemedy(unpulled) {
10542
+ const { barePath, workdir, branch } = unpulled.remote;
10543
+ switch (unpulled.kind) {
10544
+ case "behind": return `\`git -C ${workdir} pull\` brings them in first`;
10545
+ case "orphaned": return `\`git clone --branch ${branch} ${barePath} <directory>\` copies them first`;
10546
+ case "unchecked": return null;
10547
+ }
10548
+ }
10549
+ function commitLines(unpulled) {
10550
+ if (unpulled.kind === "unchecked") return [];
10551
+ const shown = unpulled.commits.slice(0, UNPULLED_COMMITS_SHOWN).map((commit) => ` ${commit.shortSha} ${commit.subject}`);
10552
+ const hidden = unpulled.commits.length - shown.length;
10553
+ return hidden > 0 ? [...shown, ` and ${hidden} more`] : shown;
10554
+ }
10555
+ function unpulledWarning(unpulled) {
10556
+ const remedy = unpulledRemedy(unpulled);
10557
+ return [
10558
+ unpulledSummary(unpulled),
10559
+ ...commitLines(unpulled),
10560
+ ...remedy === null ? [] : [remedy]
10561
+ ].join("\n");
10562
+ }
10563
+ function unpulledRefusal(unpulled) {
10564
+ const remedy = unpulledRemedy(unpulled);
10565
+ const force = "pass --force to uninstall anyway";
10566
+ return remedy === null ? `${unpulledSummary(unpulled)}; ${force}` : `${unpulledSummary(unpulled)}; ${remedy}, or ${force}`;
10567
+ }
10763
10568
  async function findConfigEntry(instance) {
10764
10569
  const path = globalConfigPath();
10765
10570
  try {
@@ -10802,14 +10607,13 @@ async function planUninstall(options, deps) {
10802
10607
  const container = await findContainer(runtime?.containerName ?? containerName(options.instance, home), deps.docker, runtime?.image ?? null, home, { graceMs: METABASE_STOP_GRACE_MS });
10803
10608
  if (container.removal !== null) removals.push(container.removal);
10804
10609
  if (container.problem !== null) problems.push(container.problem);
10805
- const sample = state?.sampleDatabase ?? null;
10806
- const sidecar = await findContainer(sample?.containerName ?? sampleContainerName(options.instance, home), deps.docker, sample?.image ?? null, home, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
10807
- if (sidecar.removal !== null) removals.push(sidecar.removal);
10808
- if (sidecar.problem !== null) problems.push(sidecar.problem);
10610
+ const sampleDatabase = await findContainer(sampleDatabaseContainerName(options.instance, home), deps.docker, null, home, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
10611
+ if (sampleDatabase.removal !== null) removals.push(sampleDatabase.removal);
10612
+ if (sampleDatabase.problem !== null) problems.push(sampleDatabase.problem);
10809
10613
  const jarSource = state?.runtime?.kind === "jar" ? state.runtime.source : null;
10810
10614
  const jarProcess = await findJarProcess(options.instance, jarSource, deps.daemon);
10811
10615
  if (jarProcess !== null) removals.push(jarProcess);
10812
- for (const image of [container.image, sidecar.image]) {
10616
+ for (const image of [container.image, sampleDatabase.image]) {
10813
10617
  if (image === null) continue;
10814
10618
  const finding = await findImage(image, deps.docker, options.removeImage);
10815
10619
  if (finding.removal !== null) removals.push(finding.removal);
@@ -10821,13 +10625,16 @@ async function planUninstall(options, deps) {
10821
10625
  if (jar.kept !== null) kept.push(jar.kept);
10822
10626
  }
10823
10627
  const profiles = state === null ? await findMbProfiles([.../* @__PURE__ */ new Set([mbProfileFor(options.instance), namedMbProfile(options.instance)])], null, deps.runner) : await findMbProfiles([state.mbProfile], instanceUrls(state), deps.runner);
10824
- removals.push(...profiles);
10628
+ removals.push(...profiles.removals);
10629
+ kept.push(...profiles.kept);
10825
10630
  if (options.removeSkill) {
10826
10631
  const skill = await findSkill(deps.runner, deps.childOutput);
10827
10632
  if (skill !== null) removals.push(skill);
10828
10633
  }
10829
10634
  const dir = instanceDir(options.instance);
10830
10635
  const dirPresent = await pathExists(dir);
10636
+ const bareRemote = state?.gitRemote?.kind === "local-bare" ? state.gitRemote : null;
10637
+ const unpulled = options.keepData || !dirPresent || bareRemote === null ? null : await findUnpulled(bareRemote, deps.git);
10831
10638
  if (!options.keepData) {
10832
10639
  const secrets = instanceSecretsPath(options.instance);
10833
10640
  if (await pathExists(secrets)) removals.push({
@@ -10848,7 +10655,8 @@ async function planUninstall(options, deps) {
10848
10655
  return {
10849
10656
  removals,
10850
10657
  kept,
10851
- problems
10658
+ problems,
10659
+ unpulled
10852
10660
  };
10853
10661
  }
10854
10662
  function planLines$1(plan) {
@@ -10903,6 +10711,10 @@ async function uninstall(options, deps, io) {
10903
10711
  };
10904
10712
  }
10905
10713
  io.ui.note(planLines$1(plan).join("\n"), `rde uninstall ${options.instance}`);
10714
+ if (plan.unpulled !== null) {
10715
+ io.ui.warn(unpulledWarning(plan.unpulled));
10716
+ if ((options.yes || !options.terminal) && !options.force) throw new ConfigError(unpulledRefusal(plan.unpulled));
10717
+ }
10906
10718
  if (!options.yes) {
10907
10719
  if (!await io.prompts.confirm({
10908
10720
  message: "Remove everything listed?",
@@ -10949,8 +10761,14 @@ const uninstallCommand = defineRdeCommand({
10949
10761
  type: "boolean",
10950
10762
  description: "Do not ask for confirmation",
10951
10763
  default: false
10764
+ },
10765
+ force: {
10766
+ type: "boolean",
10767
+ description: "With --yes or without a terminal, also delete commits your working directory has not pulled",
10768
+ default: false
10952
10769
  }
10953
10770
  },
10771
+ interrupted: INTERRUPTED,
10954
10772
  async run(args) {
10955
10773
  const instance = args.instance ?? await instanceNameOrDefault();
10956
10774
  if (!isInstanceName(instance)) throw new ConfigError(`${args.instance === void 0 ? `the default instance in ${globalConfigPath()},` : "--instance"} ${instance} is not an instance name (${INSTANCE_NAME_RULE})`);
@@ -10960,7 +10778,9 @@ const uninstallCommand = defineRdeCommand({
10960
10778
  keepData: args["keep-data"],
10961
10779
  removeSkill: args["remove-skill"],
10962
10780
  removeImage: args["remove-image"],
10963
- yes: args.yes
10781
+ yes: args.yes,
10782
+ force: args.force,
10783
+ terminal: stdinIsTty()
10964
10784
  }, hostUninstallDeps(interruptSignal, ui.childOutput), {
10965
10785
  prompts: clackPrompts({ confirmsWithYes: true }),
10966
10786
  ui
@@ -11194,8 +11014,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
11194
11014
  const stopping = ctx.ui.spinner("Stopping Metabase");
11195
11015
  await runtime.stop();
11196
11016
  stopping.stop("Metabase stopped");
11017
+ options.onPhase("backing-up", backup);
11197
11018
  await backUp(ctx, resolved.runtime.dataDir, backup);
11019
+ options.onPhase("switching", backup);
11198
11020
  await switchTo(ctx, resolved, target);
11021
+ options.onPhase("starting", backup);
11199
11022
  const to = await startUpgraded(ctx, resolved, client, configured, backup);
11200
11023
  ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
11201
11024
  return {
@@ -11207,6 +11030,14 @@ async function upgrade(ctx, options, deps = hostDeps) {
11207
11030
  }
11208
11031
  //#endregion
11209
11032
  //#region src/commands/upgrade.ts
11033
+ const INTERRUPTED_IN = {
11034
+ "backing-up": (backup) => `Interrupted before the backup finished; the instance is stopped on the version it had. The next \`rde upgrade\` starts it and takes a new backup; the partial copy at ${backup} can go.`,
11035
+ switching: (backup) => `Interrupted after the backup to ${backup} finished, while installing the new version. Run \`rde init\` to reinstall the instance; the next \`rde upgrade\` then finds it upgraded, or upgrades it if \`rde init\` put back the version it had.`,
11036
+ starting: (backup) => `Interrupted while Metabase starts on the new version, after the backup to ${backup} finished. The next \`rde upgrade\` waits for it to come up and finds it upgraded.`
11037
+ };
11038
+ function interruptedUpgrade(phase, backup) {
11039
+ return INTERRUPTED_IN[phase](backup);
11040
+ }
11210
11041
  const upgradeCommand = defineRdeCommand({
11211
11042
  meta: {
11212
11043
  name: "upgrade",
@@ -11223,14 +11054,21 @@ const upgradeCommand = defineRdeCommand({
11223
11054
  default: false
11224
11055
  }
11225
11056
  },
11226
- async run(args) {
11057
+ interrupted: "Interrupted before the backup; the instance is on the version it had. The next `rde upgrade` starts it if it is stopped and begins again.",
11058
+ async run(args, _rawArgs, interruption) {
11227
11059
  if (!args.yes && !stdinIsTty()) throw new ConfigError("`rde upgrade` asks for confirmation; pass --yes to upgrade without a terminal");
11228
11060
  const name = await defaultInstanceName();
11229
11061
  await requireInstanceState(name);
11230
- await upgrade(await createWizardContext(name, { signal: interruptSignal }), {
11062
+ const ctx = await createWizardContext(name, { signal: interruptSignal });
11063
+ await upgrade(ctx, {
11231
11064
  to: args.to ?? null,
11232
- yes: args.yes
11065
+ yes: args.yes,
11066
+ onPhase: (phase, backup) => {
11067
+ interruption.message = interruptedUpgrade(phase, backup);
11068
+ }
11233
11069
  });
11070
+ const removed = await removeLeftovers(name, interruptSignal);
11071
+ if (removed !== null) ctx.ui.info(removed);
11234
11072
  }
11235
11073
  });
11236
11074
  //#endregion
@@ -11238,7 +11076,7 @@ const upgradeCommand = defineRdeCommand({
11238
11076
  const main = defineCommand({
11239
11077
  meta: {
11240
11078
  name: "rde",
11241
- version: "0.1.14",
11079
+ version: "0.1.16",
11242
11080
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
11243
11081
  },
11244
11082
  subCommands: {