@metabase/rde 0.1.15 → 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 +489 -722
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -123,12 +123,10 @@ var DownloadError = class extends Error {
123
123
  };
124
124
  //#endregion
125
125
  //#region src/output/error.ts
126
- const INTERRUPTED_MESSAGE = "Interrupted. Run `rde init` again to resume.";
127
126
  const FAILURE_EXIT_CODE = 1;
128
127
  const USAGE_EXIT_CODE = 2;
129
128
  function exitCodeFor(error) {
130
129
  if (error instanceof StepFailedError) return exitCodeFor(error.cause);
131
- if (error instanceof AbortError) return 130;
132
130
  if (error instanceof ConfigError) return USAGE_EXIT_CODE;
133
131
  return FAILURE_EXIT_CODE;
134
132
  }
@@ -140,7 +138,6 @@ function stepFailureMessage(step, cause) {
140
138
  }
141
139
  function messageFor$1(error) {
142
140
  if (error instanceof StepFailedError) return stepFailureMessage(error.step, messageFor$1(error.cause));
143
- if (error instanceof AbortError) return INTERRUPTED_MESSAGE;
144
141
  if (error instanceof Error) return error.message;
145
142
  return String(error);
146
143
  }
@@ -148,10 +145,15 @@ function reportError(error) {
148
145
  process.stderr.write(messageFor$1(error) + "\n");
149
146
  process.exitCode = exitCodeFor(error);
150
147
  }
148
+ function reportInterrupted(message) {
149
+ if (message !== null) process.stderr.write(message + "\n");
150
+ process.exitCode = 130;
151
+ }
151
152
  //#endregion
152
153
  //#region src/commands/define.ts
153
154
  const SHORT_PREFIX = "-";
154
155
  const END_OF_OPTIONS = "--";
156
+ const INTERRUPTED = "Interrupted.";
155
157
  function repeatedValues(rawArgs, flag) {
156
158
  const values = [];
157
159
  const inline = `${flag}=`;
@@ -174,9 +176,14 @@ function defineRdeCommand(def) {
174
176
  meta: def.meta,
175
177
  ...def.args !== void 0 && { args: def.args },
176
178
  async run({ args, rawArgs }) {
179
+ const interruption = { message: def.interrupted };
177
180
  try {
178
- await def.run(args, rawArgs);
181
+ await def.run(args, rawArgs, interruption);
179
182
  } catch (error) {
183
+ if (error instanceof AbortError) {
184
+ reportInterrupted(interruption.message);
185
+ return;
186
+ }
180
187
  reportError(error);
181
188
  }
182
189
  }
@@ -255,7 +262,6 @@ const StepId = z.enum([
255
262
  "admin-user",
256
263
  "api-key",
257
264
  "transforms",
258
- "sample-database",
259
265
  "license",
260
266
  "git-sync",
261
267
  "databases",
@@ -332,12 +338,6 @@ const DatabaseRecord = z.object({
332
338
  engine: z.string(),
333
339
  createdAt: z.string()
334
340
  });
335
- const SampleDatabaseState = z.object({
336
- containerName: z.string(),
337
- image: z.string(),
338
- port: z.int().positive(),
339
- databaseId: z.int().nullable()
340
- });
341
341
  const GitRemoteState = z.discriminatedUnion("kind", [z.object({
342
342
  kind: z.literal("local-bare"),
343
343
  barePath: z.string(),
@@ -354,7 +354,7 @@ const StepRecord = z.object({
354
354
  completedAt: z.string()
355
355
  });
356
356
  const InstanceState = z.object({
357
- schemaVersion: z.literal(5),
357
+ schemaVersion: z.literal(6),
358
358
  name: z.string(),
359
359
  runtime: RuntimeState.nullable(),
360
360
  url: z.string().nullable(),
@@ -363,7 +363,6 @@ const InstanceState = z.object({
363
363
  mbProfile: z.string(),
364
364
  license: LicenseState,
365
365
  databases: z.array(DatabaseRecord),
366
- sampleDatabase: SampleDatabaseState.nullable(),
367
366
  gitRemote: GitRemoteState.nullable(),
368
367
  movedFrom: z.string().nullable(),
369
368
  steps: z.partialRecord(StepId, StepRecord)
@@ -395,7 +394,7 @@ function mbCommand(profile) {
395
394
  }
396
395
  function emptyInstanceState(name) {
397
396
  return {
398
- schemaVersion: 5,
397
+ schemaVersion: 6,
399
398
  name,
400
399
  runtime: null,
401
400
  url: null,
@@ -404,7 +403,6 @@ function emptyInstanceState(name) {
404
403
  mbProfile: mbProfileFor(name),
405
404
  license: { activated: false },
406
405
  databases: [],
407
- sampleDatabase: null,
408
406
  gitRemote: null,
409
407
  movedFrom: null,
410
408
  steps: {}
@@ -417,6 +415,7 @@ const V1 = 1;
417
415
  const V2 = 2;
418
416
  const V3 = 3;
419
417
  const V4 = 4;
418
+ const V5 = 5;
420
419
  const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
421
420
  const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
422
421
  const AdminV3 = z.discriminatedUnion("kind", [z.object({
@@ -428,6 +427,7 @@ const AdminV3 = z.discriminatedUnion("kind", [z.object({
428
427
  password: z.string()
429
428
  })]);
430
429
  const StateV4 = z.object({ schemaVersion: z.literal(V4) }).loose();
430
+ const StateV5 = z.object({ schemaVersion: z.literal(V5) }).loose();
431
431
  const StateV3 = z.object({
432
432
  schemaVersion: z.literal(V3),
433
433
  admin: AdminV3.nullable(),
@@ -443,17 +443,23 @@ function liftV1(raw) {
443
443
  function liftV2(raw) {
444
444
  return {
445
445
  ...StateV2.parse(raw),
446
- schemaVersion: V3,
447
- sampleDatabase: null
446
+ schemaVersion: V3
448
447
  };
449
448
  }
450
449
  function liftV4(raw) {
451
450
  return {
452
451
  ...StateV4.parse(raw),
453
- schemaVersion: 5,
452
+ schemaVersion: V5,
454
453
  movedFrom: null
455
454
  };
456
455
  }
456
+ function liftV5(raw) {
457
+ const { sampleDatabase: _sampleDatabase, ...state } = StateV5.parse(raw);
458
+ return {
459
+ ...state,
460
+ schemaVersion: 6
461
+ };
462
+ }
457
463
  function adminV4(admin) {
458
464
  switch (admin.kind) {
459
465
  case "default": return {
@@ -473,11 +479,11 @@ function liftV3(raw) {
473
479
  password: admin.password
474
480
  } : null;
475
481
  return {
476
- state: InstanceState.parse(liftV4({
482
+ state: migrateInstanceState(liftV4({
477
483
  ...rest,
478
484
  schemaVersion: V4,
479
485
  admin: admin === null ? null : adminV4(admin)
480
- })),
486
+ })).state,
481
487
  movedSecrets: {
482
488
  schemaVersion: 1,
483
489
  admin: login,
@@ -491,15 +497,16 @@ function migrateInstanceState(raw) {
491
497
  case V1: return migrateInstanceState(liftV1(raw));
492
498
  case V2: return migrateInstanceState(liftV2(raw));
493
499
  case V3: return liftV3(raw);
494
- case V4: return {
495
- state: InstanceState.parse(liftV4(raw)),
500
+ case V4: return migrateInstanceState(liftV4(raw));
501
+ case V5: return {
502
+ state: InstanceState.parse(liftV5(raw)),
496
503
  movedSecrets: null
497
504
  };
498
- case 5: return {
505
+ case 6: return {
499
506
  state: InstanceState.parse(raw),
500
507
  movedSecrets: null
501
508
  };
502
- 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)`);
503
510
  }
504
511
  }
505
512
  //#endregion
@@ -1479,6 +1486,7 @@ const credentials = defineRdeCommand({
1479
1486
  default: false
1480
1487
  }
1481
1488
  },
1489
+ interrupted: INTERRUPTED,
1482
1490
  async run(args) {
1483
1491
  const name = await defaultInstanceName();
1484
1492
  const state = await requireInstanceState(name);
@@ -2188,10 +2196,10 @@ function createArgs(options) {
2188
2196
  "--label",
2189
2197
  `${IMAGE_LABEL}=${options.image}`,
2190
2198
  "-p",
2191
- `${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}`
2192
2202
  ];
2193
- if (options.port.alsoOn !== null) args.push("-p", `${options.port.alsoOn}:${options.port.host ?? ""}:${options.port.container}`);
2194
- if (options.mount !== null) args.push("-v", `${options.mount.source}:${options.mount.target}`);
2195
2203
  for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
2196
2204
  if (options.hostUser !== null) {
2197
2205
  args.push("-e", `${ENV_HOST_UID}=${options.hostUser.uid}`);
@@ -2250,17 +2258,6 @@ function dockerTool(runner) {
2250
2258
  if (NO_SUCH_IMAGE.test(result.stderr)) return null;
2251
2259
  throw toolFailure(DOCKER, args, result);
2252
2260
  }
2253
- async function bridgeGateway() {
2254
- const gateway = (await docker([
2255
- "network",
2256
- "inspect",
2257
- "bridge",
2258
- "--format",
2259
- "{{range .IPAM.Config}}{{.Gateway}}{{end}}"
2260
- ])).stdout.trim();
2261
- if (gateway.length === 0) throw new ConfigError("`docker network inspect bridge` names no gateway address");
2262
- return gateway;
2263
- }
2264
2261
  async function imageRemove(image) {
2265
2262
  await docker(["rmi", image]);
2266
2263
  }
@@ -2337,34 +2334,6 @@ function dockerTool(runner) {
2337
2334
  name
2338
2335
  ]);
2339
2336
  }
2340
- async function containerLoopbackPort(name, containerPort) {
2341
- const args = [
2342
- "port",
2343
- name,
2344
- `${containerPort}/tcp`
2345
- ];
2346
- const result = await docker(args);
2347
- const prefix = `${LOOPBACK_HOST}:`;
2348
- const line = result.stdout.split("\n").find((entry) => entry.startsWith(prefix));
2349
- if (line !== void 0) {
2350
- const port = Number.parseInt(line.slice(prefix.length), 10);
2351
- if (Number.isInteger(port)) return port;
2352
- }
2353
- throw new ExternalToolError({
2354
- tool: DOCKER,
2355
- argv: args,
2356
- envNames: [],
2357
- exitCode: 0,
2358
- stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
2359
- });
2360
- }
2361
- async function containerExecSucceeds(name, command) {
2362
- return (await runner.run(DOCKER, [
2363
- "exec",
2364
- name,
2365
- ...command
2366
- ])).exitCode === 0;
2367
- }
2368
2337
  async function containerLogs(name, options) {
2369
2338
  const args = ["logs"];
2370
2339
  if (options.follow) args.push("--follow");
@@ -2377,7 +2346,6 @@ function dockerTool(runner) {
2377
2346
  imagePull,
2378
2347
  imageId,
2379
2348
  imageRemove,
2380
- bridgeGateway,
2381
2349
  containerInspect,
2382
2350
  containerImageId,
2383
2351
  portClaims,
@@ -2387,8 +2355,6 @@ function dockerTool(runner) {
2387
2355
  containerRestart,
2388
2356
  containerRemove,
2389
2357
  containerForceRemove,
2390
- containerLoopbackPort,
2391
- containerExecSucceeds,
2392
2358
  containerLogs
2393
2359
  };
2394
2360
  }
@@ -2566,8 +2532,7 @@ function dockerRuntime(state, deps) {
2566
2532
  imageId,
2567
2533
  port: {
2568
2534
  host: state.port,
2569
- container: CONTAINER_PORT,
2570
- alsoOn: null
2535
+ container: CONTAINER_PORT
2571
2536
  },
2572
2537
  mount: {
2573
2538
  source: deps.dataDir,
@@ -2881,148 +2846,21 @@ function runtimeFromState(state, deps) {
2881
2846
  }
2882
2847
  }
2883
2848
  //#endregion
2884
- //#region src/runtime/sample-database.ts
2885
- const SAMPLE_DATABASE_IMAGE = "metabase/qa-databases:postgres-sample-15";
2886
- const SAMPLE_DATABASE_USER = "metabase";
2887
- const SAMPLE_DATABASE_PASSWORD = "metasample123";
2888
- const SAMPLE_DATABASE_NAME = "sample";
2889
- const SAMPLE_DATABASE_PORT = 25432;
2890
- const SAMPLE_DATABASE_READY_TIMEOUT_MS = 12e4;
2891
- const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
2892
- const POSTGRES_PORT = 5432;
2849
+ //#region src/runtime/leftover-sample-database.ts
2893
2850
  const CONTAINER_NAME_PREFIX = "rde-sampledb";
2894
- const READY_POLL_MS = 1e3;
2895
- const READY_PROBE = [
2896
- "pg_isready",
2897
- "-h",
2898
- LOOPBACK_HOST,
2899
- "-U",
2900
- SAMPLE_DATABASE_USER,
2901
- "-d",
2902
- SAMPLE_DATABASE_NAME
2903
- ];
2904
- function sampleContainerName(instanceName, home) {
2851
+ const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
2852
+ function sampleDatabaseContainerName(instanceName, home) {
2905
2853
  const tag = homeTag(home);
2906
2854
  const name = `${CONTAINER_NAME_PREFIX}-${instanceName}`;
2907
2855
  return tag === null ? name : `${name}-${tag}`;
2908
2856
  }
2909
- function sampleDatabaseUrl(port) {
2910
- return `postgres://${SAMPLE_DATABASE_USER}:${SAMPLE_DATABASE_PASSWORD}@localhost:${port}/${SAMPLE_DATABASE_NAME}`;
2911
- }
2912
- function publishesOnLoopback$1(inspection, port) {
2913
- return inspection.ports.some((mapping) => mapping.container === POSTGRES_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
2914
- }
2915
- function createOptions(state, home, hostPort, alsoOn) {
2916
- return {
2917
- name: state.containerName,
2918
- home,
2919
- image: state.image,
2920
- imageId: null,
2921
- port: {
2922
- host: hostPort,
2923
- container: POSTGRES_PORT,
2924
- alsoOn
2925
- },
2926
- mount: null,
2927
- env: {},
2928
- addHostGateway: false,
2929
- hostUser: null
2930
- };
2931
- }
2932
- function requireOwnContainer(name, existing, home) {
2933
- if (existing.home !== null && existing.home !== home) throw new ConfigError(`container ${name} belongs to the rde home ${existing.home} and is left alone`);
2934
- }
2935
- function sampleDatabaseContainer(state, docker, home) {
2936
- const name = state.containerName;
2937
- function conflict(reason) {
2938
- return new ConfigError(`container ${name} already exists ${reason}; remove it with \`docker rm -f ${name}\` and run \`rde init\` again`);
2939
- }
2940
- async function install(options) {
2941
- const existing = await docker.containerInspect(name);
2942
- if (existing.exists) {
2943
- if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
2944
- if (publishesOnLoopback$1(existing, state.port)) return;
2945
- requireOwnContainer(name, existing, home);
2946
- await docker.containerForceRemove(name);
2947
- }
2948
- await docker.imagePull(state.image, { onLine: options.onProgress });
2949
- await docker.containerCreate(createOptions(state, home, state.port, options.alsoOn));
2950
- }
2951
- async function start() {
2952
- const existing = await docker.containerInspect(name);
2953
- if (!existing.exists) return "missing";
2954
- if (existing.running) return "running";
2955
- await docker.containerStart(name);
2956
- return "started";
2957
- }
2958
- async function stop() {
2959
- const existing = await docker.containerInspect(name);
2960
- if (existing.exists && existing.running) await docker.containerStop(name, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
2961
- }
2962
- async function status() {
2963
- const existing = await docker.containerInspect(name);
2964
- return {
2965
- installed: existing.exists,
2966
- running: existing.exists && existing.running
2967
- };
2968
- }
2969
- async function waitReady(options) {
2970
- const deadline = Date.now() + options.timeoutMs;
2971
- const interval = options.pollMs ?? READY_POLL_MS;
2972
- for (;;) {
2973
- if (await docker.containerExecSucceeds(name, READY_PROBE)) return;
2974
- const existing = await docker.containerInspect(name);
2975
- if (!existing.exists || !existing.running) throw new ConfigError(`container ${name} stopped before Postgres was ready; \`docker logs ${name}\` shows why`);
2976
- 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`);
2977
- try {
2978
- await setTimeout$1(interval, void 0, { signal: options.signal });
2979
- } catch {
2980
- throw new AbortError();
2981
- }
2982
- }
2983
- }
2984
- return {
2985
- install,
2986
- start,
2987
- stop,
2988
- status,
2989
- waitReady
2990
- };
2991
- }
2992
- function extraAddress(inspection) {
2993
- const extra = inspection.ports.find((mapping) => mapping.container === POSTGRES_PORT && mapping.hostIp !== "127.0.0.1");
2994
- return extra === void 0 ? null : extra.hostIp;
2995
- }
2996
- async function startOnDockerPort(state, docker, home, alsoOn) {
2997
- const name = state.containerName;
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;
2998
2862
  await docker.containerForceRemove(name);
2999
- await docker.containerCreate(createOptions(state, home, null, null));
3000
- await docker.containerStart(name);
3001
- const port = await docker.containerLoopbackPort(name, POSTGRES_PORT);
3002
- await docker.containerForceRemove(name);
3003
- const pinned = {
3004
- ...state,
3005
- port
3006
- };
3007
- await docker.containerCreate(createOptions(pinned, home, port, alsoOn));
3008
- await docker.containerStart(name);
3009
- return pinned;
3010
- }
3011
- async function startOnFreePort(state, docker, home) {
3012
- try {
3013
- return {
3014
- outcome: await sampleDatabaseContainer(state, docker, home).start(),
3015
- state
3016
- };
3017
- } catch (error) {
3018
- if (!isPortTakenError(error)) throw error;
3019
- }
3020
- const existing = await docker.containerInspect(state.containerName);
3021
- if (existing.exists) requireOwnContainer(state.containerName, existing, home);
3022
- return {
3023
- outcome: "started",
3024
- state: await startOnDockerPort(state, docker, home, existing.exists ? extraAddress(existing) : null)
3025
- };
2863
+ return name;
3026
2864
  }
3027
2865
  //#endregion
3028
2866
  //#region src/steps/lifecycle.ts
@@ -3033,10 +2871,14 @@ async function loadInstance(name, signal) {
3033
2871
  return {
3034
2872
  state,
3035
2873
  url: configured.url,
3036
- runtime: runtimeFromState(configured.runtime, deps),
3037
- sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker, deps.rdeHome)
2874
+ runtime: runtimeFromState(configured.runtime, deps)
3038
2875
  };
3039
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
+ }
3040
2882
  async function isInstanceRunning(state, runtime) {
3041
2883
  const configured = configuredRuntime(state);
3042
2884
  if (configured === null) return false;
@@ -3045,44 +2887,29 @@ async function isInstanceRunning(state, runtime) {
3045
2887
  function openInstance(url) {
3046
2888
  return openBrowser(url);
3047
2889
  }
3048
- function ownContainers(state) {
3049
- const names = /* @__PURE__ */ new Set();
3050
- if (state.runtime?.kind === "docker") names.add(state.runtime.containerName);
3051
- if (state.sampleDatabase !== null) names.add(state.sampleDatabase.containerName);
3052
- return names;
3053
- }
3054
- function instanceClaims(other) {
3055
- const claims = [];
3056
- if (other.runtime !== null) claims.push({
3057
- port: other.runtime.port,
3058
- holder: `rde instance ${other.name}`
3059
- });
3060
- if (other.sampleDatabase !== null) claims.push({
3061
- port: other.sampleDatabase.port,
3062
- holder: `the sample database of rde instance ${other.name}`
3063
- });
3064
- return claims;
3065
- }
3066
2890
  async function portClaims(state, runtime) {
3067
2891
  const claims = [];
3068
2892
  if ((await runtime.docker.available()).available) {
3069
- const own = ownContainers(state);
3070
- 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({
3071
2895
  port: claim.port,
3072
2896
  holder: `container ${claim.container}`
3073
2897
  });
3074
2898
  }
3075
- 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
+ });
3076
2903
  return claims;
3077
2904
  }
3078
2905
  //#endregion
3079
2906
  //#region src/steps/admin-user.ts
3080
- const STEP_ID$10 = "admin-user";
2907
+ const STEP_ID$8 = "admin-user";
3081
2908
  const ADMIN_USER_KEYS = {
3082
- email: `${STEP_ID$10}.email`,
3083
- password: `${STEP_ID$10}.password`,
3084
- firstName: `${STEP_ID$10}.first-name`,
3085
- 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`
3086
2913
  };
3087
2914
  const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
3088
2915
  const GENERATED_PASSWORD_BYTES = 24;
@@ -3097,7 +2924,7 @@ function generatePassword(random) {
3097
2924
  if (DIGIT.test(candidate)) return candidate;
3098
2925
  }
3099
2926
  }
3100
- function hostDeps$14(ctx) {
2927
+ function hostDeps$13(ctx) {
3101
2928
  return {
3102
2929
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3103
2930
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
@@ -3235,9 +3062,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
3235
3062
  ctx.ui.success(`Admin user ${email} recorded`);
3236
3063
  return "completed";
3237
3064
  }
3238
- function adminUserStep(options, deps = hostDeps$14) {
3065
+ function adminUserStep(options, deps = hostDeps$13) {
3239
3066
  return {
3240
- id: STEP_ID$10,
3067
+ id: STEP_ID$8,
3241
3068
  title: "Create the admin user",
3242
3069
  redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
3243
3070
  async detect(ctx) {
@@ -3652,14 +3479,14 @@ function mbTool(runner) {
3652
3479
  }
3653
3480
  //#endregion
3654
3481
  //#region src/steps/api-key.ts
3655
- const STEP_ID$9 = "api-key";
3482
+ const STEP_ID$7 = "api-key";
3656
3483
  const API_KEY_KEYS = {
3657
- email: `${STEP_ID$9}.email`,
3658
- password: `${STEP_ID$9}.password`,
3659
- profile: `${STEP_ID$9}.profile`
3484
+ email: `${STEP_ID$7}.email`,
3485
+ password: `${STEP_ID$7}.password`,
3486
+ profile: `${STEP_ID$7}.profile`
3660
3487
  };
3661
3488
  const ADMINISTRATORS_GROUP_NAME = "Administrators";
3662
- function hostDeps$13(ctx) {
3489
+ function hostDeps$12(ctx) {
3663
3490
  return {
3664
3491
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3665
3492
  runner: processRunner(ctx.signal),
@@ -3720,7 +3547,7 @@ async function authenticates(client) {
3720
3547
  }
3721
3548
  }
3722
3549
  async function forgetCompletion(ctx) {
3723
- const { [STEP_ID$9]: record, ...steps } = ctx.state.steps;
3550
+ const { [STEP_ID$7]: record, ...steps } = ctx.state.steps;
3724
3551
  if (record !== void 0) await ctx.saveState({
3725
3552
  ...ctx.state,
3726
3553
  steps
@@ -3797,15 +3624,15 @@ function usageNote(profile) {
3797
3624
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
3798
3625
  return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
3799
3626
  }
3800
- function apiKeyStep(deps = hostDeps$13) {
3627
+ function apiKeyStep(deps = hostDeps$12) {
3801
3628
  return {
3802
- id: STEP_ID$9,
3629
+ id: STEP_ID$7,
3803
3630
  title: "Connect the Metabase CLI",
3804
3631
  redoHint: "`rde status` shows the mb profile it uses.",
3805
3632
  async detect(ctx) {
3806
3633
  const configured = configuredRuntime(ctx.state);
3807
3634
  const apiKey = ctx.secrets.apiKey;
3808
- if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$9]?.status !== "done") return "todo";
3635
+ if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
3809
3636
  const resolved = deps(ctx);
3810
3637
  if (!await resolved.instanceRunning()) return "todo";
3811
3638
  if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
@@ -4827,7 +4654,7 @@ const RSA_KEY_UNAVAILABLE_ERROR = "RSA public key is not available";
4827
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";
4828
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";
4829
4656
  const CONNECTION_PLACEHOLDER = "leave empty to skip";
4830
- const STUB_CONNECTION_PLACEHOLDER = "leave empty to pick the engine";
4657
+ const ENGINE_PLACEHOLDER = "leave empty to pick the engine";
4831
4658
  const START_SCREEN = {
4832
4659
  engine: "pick-engine",
4833
4660
  "connection-string": "connection-string"
@@ -4853,6 +4680,9 @@ function targeting(stub) {
4853
4680
  function restart(model) {
4854
4681
  return model.target === null ? FRESH$1 : targeting(model.target);
4855
4682
  }
4683
+ function mayFinish(run) {
4684
+ return !run.deps.required || run.deps.existingCount + run.created.length > 0;
4685
+ }
4856
4686
  function nextStub(run, model) {
4857
4687
  const stub = run.queue.shift();
4858
4688
  if (stub !== void 0) return {
@@ -4860,13 +4690,18 @@ function nextStub(run, model) {
4860
4690
  to: "connection-string",
4861
4691
  model: targeting(stub)
4862
4692
  };
4863
- return run.created.length > 0 ? {
4693
+ if (run.created.length > 0) return {
4864
4694
  type: "next",
4865
4695
  to: "another",
4866
4696
  model
4867
- } : {
4697
+ };
4698
+ return mayFinish(run) ? {
4868
4699
  type: "done",
4869
4700
  model
4701
+ } : {
4702
+ type: "next",
4703
+ to: "connection-string",
4704
+ model: FRESH$1
4870
4705
  };
4871
4706
  }
4872
4707
  function stubNamed(run, name) {
@@ -4956,9 +4791,13 @@ function pickStubsScreen(run) {
4956
4791
  run.queue = run.stubs.filter((stub) => picked.includes(stub.id));
4957
4792
  return nextStub(run, model);
4958
4793
  }
4959
- 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) ? {
4960
4795
  type: "done",
4961
4796
  model
4797
+ } : {
4798
+ type: "next",
4799
+ to: "connection-string",
4800
+ model: FRESH$1
4962
4801
  };
4963
4802
  return {
4964
4803
  type: "next",
@@ -4968,6 +4807,11 @@ function pickStubsScreen(run) {
4968
4807
  };
4969
4808
  }
4970
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
+ };
4971
4815
  if (target === null) return {
4972
4816
  key: DATABASE_KEYS.connection,
4973
4817
  message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
@@ -4976,7 +4820,7 @@ function connectionQuestion(run, target) {
4976
4820
  return {
4977
4821
  key: databaseKey(ordinalOf(run), "connection"),
4978
4822
  message: `Connection string of ${target.name} (Enter to pick its engine instead)`,
4979
- placeholder: STUB_CONNECTION_PLACEHOLDER
4823
+ placeholder: ENGINE_PLACEHOLDER
4980
4824
  };
4981
4825
  }
4982
4826
  function connectionStringScreen(run) {
@@ -4988,7 +4832,7 @@ function connectionStringScreen(run) {
4988
4832
  ...draft !== null && { initialValue: draft }
4989
4833
  })).trim();
4990
4834
  draft = uri;
4991
- if (uri.length === 0) return model.target === null ? {
4835
+ if (uri.length === 0) return model.target === null && mayFinish(run) ? {
4992
4836
  type: "done",
4993
4837
  model
4994
4838
  } : {
@@ -5027,22 +4871,28 @@ function connectionStringScreen(run) {
5027
4871
  }
5028
4872
  };
5029
4873
  }
5030
- function finishLabel(run, model) {
5031
- if (model.target !== null) return `Leave ${model.target.name} unconnected`;
5032
- 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
+ }] : [];
5033
4887
  }
5034
4888
  function pickEngineScreen(run) {
5035
4889
  return async (model) => {
5036
4890
  const total = run.deps.existingCount + run.created.length;
5037
- const finishChoice = {
5038
- value: FINISH,
5039
- label: finishLabel(run, model)
5040
- };
5041
4891
  for (;;) {
5042
4892
  const picked = await run.deps.prompts.select({
5043
4893
  key: databaseKey(ordinalOf(run), "engine"),
5044
4894
  message: "Which database engine?",
5045
- choices: [...run.deps.catalog.list().map(engineChoice), finishChoice],
4895
+ choices: [...run.deps.catalog.list().map(engineChoice), ...finishChoices(run, model)],
5046
4896
  ...model.engine !== null && { initialValue: model.engine.key }
5047
4897
  });
5048
4898
  if (picked === "finish" && model.target !== null) return nextStub(run, model);
@@ -5518,6 +5368,13 @@ const METABASE_CONTENT_DIRS = [
5518
5368
  ];
5519
5369
  const TOKEN_USERNAMES = { "bitbucket.org": "x-token-auth" };
5520
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");
5521
5378
  function credentialEnv(credentials) {
5522
5379
  const { origin, hostname } = new URL(credentials.url);
5523
5380
  const username = TOKEN_USERNAMES[hostname] ?? DEFAULT_TOKEN_USERNAME;
@@ -5531,6 +5388,18 @@ function credentialEnv(credentials) {
5531
5388
  GIT_TERMINAL_PROMPT: "0"
5532
5389
  };
5533
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
+ }
5534
5403
  async function directoryExists(path) {
5535
5404
  try {
5536
5405
  return (await promises.stat(path)).isDirectory();
@@ -5586,8 +5455,12 @@ function gitTool(runner) {
5586
5455
  ], { cwd: dir });
5587
5456
  return result.exitCode === 0 && result.stdout.trim().length > 0;
5588
5457
  }
5589
- async function commitAll(dir, message) {
5590
- await git(["add", "--all"], { cwd: dir });
5458
+ async function commitPaths(dir, paths, message) {
5459
+ await git([
5460
+ "add",
5461
+ "--",
5462
+ ...paths
5463
+ ], { cwd: dir });
5591
5464
  await git([
5592
5465
  ...await hasIdentity(dir) ? [] : [
5593
5466
  "-c",
@@ -5597,7 +5470,9 @@ function gitTool(runner) {
5597
5470
  ],
5598
5471
  "commit",
5599
5472
  "--message",
5600
- message
5473
+ message,
5474
+ "--",
5475
+ ...paths
5601
5476
  ], { cwd: dir });
5602
5477
  }
5603
5478
  async function addRemote(dir, name, url) {
@@ -5643,6 +5518,42 @@ function gitTool(runner) {
5643
5518
  if (result.exitCode !== 0) return null;
5644
5519
  return result.stdout.trim();
5645
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
+ }
5646
5557
  async function hasMetabaseContent(dir) {
5647
5558
  for (const name of METABASE_CONTENT_DIRS) if (await directoryExists(join(dir, name))) return true;
5648
5559
  return false;
@@ -5653,24 +5564,27 @@ function gitTool(runner) {
5653
5564
  initBare,
5654
5565
  init,
5655
5566
  hasCommits,
5656
- commitAll,
5567
+ commitPaths,
5657
5568
  addRemote,
5658
5569
  clone,
5659
5570
  push,
5660
5571
  currentBranch,
5661
5572
  remoteUrl,
5573
+ isBareRepo,
5574
+ bareBranchCommits,
5575
+ reachableCommits,
5662
5576
  hasMetabaseContent
5663
5577
  };
5664
5578
  }
5665
5579
  //#endregion
5666
5580
  //#region src/steps/git-sync.ts
5667
- const STEP_ID$8 = "git-sync";
5581
+ const STEP_ID$6 = "git-sync";
5668
5582
  const GIT_SYNC_KEYS = {
5669
- location: `${STEP_ID$8}.location`,
5670
- path: `${STEP_ID$8}.path`,
5671
- remote: `${STEP_ID$8}.remote`,
5672
- url: `${STEP_ID$8}.url`,
5673
- 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`
5674
5588
  };
5675
5589
  const BARE_REPO_NAME = "content.git";
5676
5590
  const DEFAULT_BRANCH = "main";
@@ -5704,7 +5618,7 @@ const SYNC_MODE_SETTINGS = {
5704
5618
  "remote-sync-type": "read-write",
5705
5619
  "remote-sync-transforms": true
5706
5620
  };
5707
- function hostDeps$12(ctx) {
5621
+ function hostDeps$11(ctx) {
5708
5622
  return {
5709
5623
  runner: processRunner(ctx.signal),
5710
5624
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
@@ -5956,7 +5870,7 @@ async function prepareWorkdir(run, workdir) {
5956
5870
  if (!await run.git.hasCommits(workdir)) {
5957
5871
  await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
5958
5872
  await ignoreDataAppFiles(workdir);
5959
- await run.git.commitAll(workdir, INITIAL_COMMIT_MESSAGE);
5873
+ await run.git.commitPaths(workdir, [README_NAME, GITIGNORE_NAME], INITIAL_COMMIT_MESSAGE);
5960
5874
  }
5961
5875
  return {
5962
5876
  workdir,
@@ -6120,7 +6034,7 @@ function shellWord(value) {
6120
6034
  function exportCommand(profile, branch) {
6121
6035
  return `${mbCommand(profile)} git-sync export -b ${shellWord(branch)}`;
6122
6036
  }
6123
- function usageLines$1(state, gitRemote) {
6037
+ function usageLines(state, gitRemote) {
6124
6038
  const profile = state.mbProfile;
6125
6039
  return [
6126
6040
  `cd ${gitRemote.workdir}`,
@@ -6171,18 +6085,18 @@ async function runGitSync(ctx, deps) {
6171
6085
  ...ctx.state,
6172
6086
  gitRemote: remote.gitRemote
6173
6087
  });
6174
- 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");
6175
6089
  return "completed";
6176
6090
  }
6177
- function gitSyncStep(deps = hostDeps$12) {
6091
+ function gitSyncStep(deps = hostDeps$11) {
6178
6092
  return {
6179
- id: STEP_ID$8,
6093
+ id: STEP_ID$6,
6180
6094
  title: "Connect a git repository",
6181
6095
  redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
6182
6096
  async detect(ctx) {
6183
6097
  if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
6184
- if (ctx.state.steps[STEP_ID$8]?.status === "skipped") return "not-applicable";
6185
- 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";
6186
6100
  const instance = mbInstanceOf(ctx.state, ctx.secrets);
6187
6101
  if (instance === null) return "todo";
6188
6102
  try {
@@ -6364,12 +6278,12 @@ function skillsCli(runner) {
6364
6278
  }
6365
6279
  //#endregion
6366
6280
  //#region src/steps/agents-skills.ts
6367
- const STEP_ID$7 = "agents-skills";
6368
- 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` };
6369
6283
  const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
6370
6284
  const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
6371
6285
  const DETECT_AGENTS = { kind: "detected" };
6372
- function hostDeps$11(ctx) {
6286
+ function hostDeps$10(ctx) {
6373
6287
  return {
6374
6288
  runner: processRunner(ctx.signal),
6375
6289
  probe: hostAgentProbe(),
@@ -6548,11 +6462,11 @@ function skipLine(all, source) {
6548
6462
  switch (source) {
6549
6463
  case "override":
6550
6464
  case "chosen":
6551
- case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
6552
- 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`;
6553
6467
  }
6554
6468
  }
6555
- function agentsSkillsStep(options, updates, deps = hostDeps$11) {
6469
+ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
6556
6470
  let hashes = null;
6557
6471
  async function setCheck(resolved) {
6558
6472
  hashes ??= hashLookup(resolved.github);
@@ -6563,13 +6477,13 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
6563
6477
  };
6564
6478
  }
6565
6479
  return {
6566
- id: STEP_ID$7,
6480
+ id: STEP_ID$5,
6567
6481
  title: "Install the rde and data-app skills for your coding agents",
6568
6482
  redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
6569
6483
  async detect(ctx) {
6570
6484
  const resolved = deps(ctx);
6571
6485
  const detected = detectedOnly(await detectAgents(resolved.probe));
6572
- 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";
6573
6487
  if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
6574
6488
  const check = await setCheck(resolved);
6575
6489
  for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
@@ -6617,8 +6531,10 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
6617
6531
  }
6618
6532
  //#endregion
6619
6533
  //#region src/steps/databases.ts
6620
- const STEP_ID$6 = "databases";
6621
- 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}`;
6622
6538
  function hostDatabasesDeps(signal) {
6623
6539
  return {
6624
6540
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
@@ -6626,7 +6542,7 @@ function hostDatabasesDeps(signal) {
6626
6542
  now: () => /* @__PURE__ */ new Date()
6627
6543
  };
6628
6544
  }
6629
- function requireApiKey$2(state, apiKey) {
6545
+ function requireApiKey$1(state, apiKey) {
6630
6546
  if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
6631
6547
  return apiKey;
6632
6548
  }
@@ -6636,7 +6552,7 @@ function hostAliasOf(runtime) {
6636
6552
  case "jar": return null;
6637
6553
  }
6638
6554
  }
6639
- function toRecord$1(database, createdAt) {
6555
+ function toRecord(database, createdAt) {
6640
6556
  return {
6641
6557
  id: database.id,
6642
6558
  name: database.name,
@@ -6648,7 +6564,7 @@ function authenticatedClient(state, apiKey, deps) {
6648
6564
  const configured = requireConfiguredRuntime(state);
6649
6565
  return deps.client(configured.url, {
6650
6566
  kind: "api-key",
6651
- key: requireApiKey$2(state, apiKey)
6567
+ key: requireApiKey$1(state, apiKey)
6652
6568
  });
6653
6569
  }
6654
6570
  async function listDatabases(state, apiKey, deps) {
@@ -6691,7 +6607,7 @@ async function connectDatabases(ctx, deps, opening) {
6691
6607
  existingCount: existing.length,
6692
6608
  readTextFile: deps.readTextFile,
6693
6609
  onCreated: async (database) => {
6694
- const record = toRecord$1(database, deps.now().toISOString());
6610
+ const record = toRecord(database, deps.now().toISOString());
6695
6611
  await ctx.saveState({
6696
6612
  ...ctx.state,
6697
6613
  databases: [...ctx.state.databases, record]
@@ -6708,6 +6624,7 @@ async function connectDatabases(ctx, deps, opening) {
6708
6624
  async function addDatabases(ctx, deps, options) {
6709
6625
  const connected = await connectDatabases(ctx, deps, {
6710
6626
  start: "engine",
6627
+ required: false,
6711
6628
  ...options
6712
6629
  });
6713
6630
  return {
@@ -6715,50 +6632,33 @@ async function addDatabases(ctx, deps, options) {
6715
6632
  result: { addedDatabases: connected.added }
6716
6633
  };
6717
6634
  }
6718
- async function askForOwnDatabase(ctx, deps) {
6719
- const connected = await connectDatabases(ctx, deps, {
6720
- start: "connection-string",
6721
- offerStubs: true
6722
- });
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);
6723
6638
  return {
6724
- outcome: connected.created > 0 ? "completed" : "skipped",
6639
+ outcome: "completed",
6725
6640
  result: { addedDatabases: connected.added }
6726
6641
  };
6727
6642
  }
6728
- function hostDeps$10(ctx) {
6643
+ function hostDeps$9(ctx) {
6729
6644
  return {
6730
6645
  ...hostDatabasesDeps(ctx.signal),
6731
6646
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
6732
6647
  };
6733
6648
  }
6734
- function sampleDatabaseId(state) {
6735
- return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
6736
- }
6737
- function skippedBefore(state) {
6738
- return state.steps[STEP_ID$6]?.status === "skipped";
6739
- }
6740
- function ownDatabaseIds(state) {
6741
- const sampleId = sampleDatabaseId(state);
6742
- return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
6743
- }
6744
- function databasesStep(options, deps = hostDeps$10) {
6649
+ function databasesStep(options, deps = hostDeps$9) {
6745
6650
  return {
6746
- id: STEP_ID$6,
6651
+ id: "databases",
6747
6652
  title: "Connect your databases",
6748
6653
  redoHint: "Connect another with `rde db add --url <connection string>`.",
6749
6654
  async detect(ctx) {
6750
6655
  const configured = configuredRuntime(ctx.state);
6751
6656
  if (configured === null || ctx.secrets.apiKey === null) return "todo";
6752
- const own = ownDatabaseIds(ctx.state);
6753
- const sampleId = sampleDatabaseId(ctx.state);
6754
- const asking = options.request === "ask-once" && !skippedBefore(ctx.state);
6755
- if (own.size === 0 && !asking) {
6756
- if (options.request === "named") return "todo";
6757
- return sampleId !== null || skippedBefore(ctx.state) ? "done" : "todo";
6758
- }
6657
+ const recorded = new Set(ctx.state.databases.map((record) => record.id));
6658
+ if (recorded.size === 0 && options.request === "named") return "todo";
6759
6659
  const resolved = deps(ctx);
6760
6660
  if (!await resolved.instanceRunning()) return "todo";
6761
- const isOwn = (id) => own.size > 0 ? own.has(id) : id !== sampleId;
6661
+ const isOwn = (id) => recorded.size === 0 || recorded.has(id);
6762
6662
  const client = resolved.client(configured.url, {
6763
6663
  kind: "api-key",
6764
6664
  key: ctx.secrets.apiKey
@@ -6772,9 +6672,19 @@ function databasesStep(options, deps = hostDeps$10) {
6772
6672
  },
6773
6673
  async run(ctx) {
6774
6674
  switch (options.request) {
6775
- case "ask-once": return askForOwnDatabase(ctx, deps(ctx));
6776
- case "named": return addDatabases(ctx, deps(ctx), { offerStubs: false });
6777
- 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);
6778
6688
  }
6779
6689
  }
6780
6690
  };
@@ -6799,7 +6709,7 @@ function npmRegistry(signal, options = {}) {
6799
6709
  }
6800
6710
  //#endregion
6801
6711
  //#region src/steps/install-mb.ts
6802
- const STEP_ID$5 = "install-mb";
6712
+ const STEP_ID$4 = "install-mb";
6803
6713
  const MIN_MB_VERSION = "0.3.0";
6804
6714
  const MB_RANGE = `>=${MIN_MB_VERSION}`;
6805
6715
  function installedWith(scriptPath) {
@@ -6808,7 +6718,7 @@ function installedWith(scriptPath) {
6808
6718
  function rdeInstalledWith() {
6809
6719
  return installedWith(process.argv[1]);
6810
6720
  }
6811
- function hostDeps$9(ctx) {
6721
+ function hostDeps$8(ctx) {
6812
6722
  return {
6813
6723
  runner: processRunner(ctx.signal),
6814
6724
  installedWith: rdeInstalledWith(),
@@ -6975,7 +6885,7 @@ async function updateFor(release, installed, deps) {
6975
6885
  version
6976
6886
  };
6977
6887
  }
6978
- function installMbStep(updates, deps = hostDeps$9) {
6888
+ function installMbStep(updates, deps = hostDeps$8) {
6979
6889
  let published = null;
6980
6890
  function release(resolved) {
6981
6891
  if (updates === "installed") return Promise.resolve(UNCHECKED);
@@ -6983,7 +6893,7 @@ function installMbStep(updates, deps = hostDeps$9) {
6983
6893
  return published;
6984
6894
  }
6985
6895
  return {
6986
- id: STEP_ID$5,
6896
+ id: STEP_ID$4,
6987
6897
  title: "Install the Metabase CLI",
6988
6898
  async detect(ctx) {
6989
6899
  const resolved = deps(ctx);
@@ -7025,26 +6935,47 @@ function installMbStep(updates, deps = hostDeps$9) {
7025
6935
  }
7026
6936
  //#endregion
7027
6937
  //#region src/steps/license.ts
7028
- const STEP_ID$4 = "license";
7029
- const LICENSE_KEYS = { token: `${STEP_ID$4}.token` };
6938
+ const STEP_ID$3 = "license";
6939
+ const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
7030
6940
  const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
7031
- const NOT_FOUND_STATUS$2 = 404;
7032
- 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) {
7033
6963
  return {
7034
6964
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
7035
6965
  runner: processRunner(ctx.signal),
7036
- instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
6966
+ instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
6967
+ terminal: stdinIsTty()
7037
6968
  };
7038
6969
  }
7039
6970
  function isNoTokenYet(error) {
7040
- if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$2;
6971
+ if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
7041
6972
  return isMetabaseNotServing(error);
7042
6973
  }
7043
6974
  function validateToken(value) {
7044
6975
  if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
7045
6976
  return "A license token is 64 hexadecimal characters";
7046
6977
  }
7047
- function requireApiKey$1(ctx) {
6978
+ function requireApiKey(ctx) {
7048
6979
  if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
7049
6980
  return ctx.secrets.apiKey;
7050
6981
  }
@@ -7052,13 +6983,74 @@ function activeLabel(status) {
7052
6983
  const plan = status["plan-alias"];
7053
6984
  return plan === void 0 ? "License active" : `License active (plan ${plan})`;
7054
6985
  }
7055
- 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
+ };
7056
7008
  return {
7057
- 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,
7058
7050
  title: "Activate your license",
7059
7051
  redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
7060
7052
  async detect(ctx) {
7061
- 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";
7062
7054
  const configured = configuredRuntime(ctx.state);
7063
7055
  if (configured === null || ctx.secrets.apiKey === null) return "todo";
7064
7056
  const resolved = deps(ctx);
@@ -7076,36 +7068,17 @@ function licenseStep(deps = hostDeps$8) {
7076
7068
  },
7077
7069
  async run(ctx) {
7078
7070
  const configured = requireConfiguredRuntime(ctx.state);
7079
- const apiKey = requireApiKey$1(ctx);
7080
- const token = await ctx.prompts.password({
7081
- key: LICENSE_KEYS.token,
7082
- message: "Paste your Metabase license token (Enter to skip)",
7083
- validate: validateToken
7084
- });
7085
- if (token.length === 0) {
7086
- ctx.ui.info("Skipping the license; remote sync needs one. Run `rde init --from license` to add it later");
7087
- return { outcome: "skipped" };
7088
- }
7089
- ctx.session.licenseToken.set(token);
7071
+ const apiKey = requireApiKey(ctx);
7090
7072
  const resolved = deps(ctx);
7091
- const client = resolved.client(configured.url, {
7073
+ const status = await activateAskedToken(ctx, resolved.client(configured.url, {
7092
7074
  kind: "api-key",
7093
7075
  key: apiKey
7094
- });
7095
- const spinner = ctx.ui.spinner("Activating the license…");
7096
- let status;
7097
- try {
7098
- await client.setting.setPremiumEmbeddingToken(token);
7099
- status = await client.tokenStatus.get();
7100
- } catch (error) {
7101
- spinner.fail("License activation failed");
7102
- throw error;
7103
- }
7104
- if (!status.valid) {
7105
- spinner.fail("License activation failed");
7106
- 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" };
7107
7081
  }
7108
- spinner.stop(activeLabel(status));
7109
7082
  await ctx.saveState({
7110
7083
  ...ctx.state,
7111
7084
  license: {
@@ -7120,8 +7093,7 @@ function licenseStep(deps = hostDeps$8) {
7120
7093
  profile: ctx.state.mbProfile,
7121
7094
  apiKey
7122
7095
  });
7123
- if (status.features.includes("remote-sync")) ctx.ui.info("Remote sync is included; the git step will set it up");
7124
- else ctx.ui.warn("This license does not include remote sync; the git step will be skipped");
7096
+ reportFeatures(ctx, status.features);
7125
7097
  return { outcome: "completed" };
7126
7098
  }
7127
7099
  };
@@ -7134,7 +7106,7 @@ const NODE_HINT = `Install Node ${MIN_NODE_VERSION} or newer from https://nodejs
7134
7106
  const MARK_OK = "✓";
7135
7107
  const MARK_MISSING = "✗";
7136
7108
  const MARK_OPTIONAL = "–";
7137
- 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`;
7138
7110
  const JAVA_OPTIONAL_NOTE = `Metabase can run in Docker instead`;
7139
7111
  const PLATFORMS = {
7140
7112
  darwin: {
@@ -7150,7 +7122,7 @@ const PLATFORMS = {
7150
7122
  javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
7151
7123
  }
7152
7124
  };
7153
- function hostDeps$7(ctx) {
7125
+ function hostDeps$6(ctx) {
7154
7126
  const runner = processRunner(ctx.signal);
7155
7127
  return {
7156
7128
  platform: process.platform,
@@ -7272,7 +7244,7 @@ function checkLine(check) {
7272
7244
  function checkLines(checks) {
7273
7245
  return checks.map(checkLine);
7274
7246
  }
7275
- function preflightStep(deps = hostDeps$7) {
7247
+ function preflightStep(deps = hostDeps$6) {
7276
7248
  return {
7277
7249
  id: "preflight",
7278
7250
  title: "Check this machine",
@@ -7291,7 +7263,7 @@ function preflightStep(deps = hostDeps$7) {
7291
7263
  }
7292
7264
  //#endregion
7293
7265
  //#region src/steps/runtime.ts
7294
- const STEP_ID$3 = "runtime";
7266
+ const STEP_ID$2 = "runtime";
7295
7267
  const MIN_PORT = 1;
7296
7268
  const MAX_PORT = 65535;
7297
7269
  const PORT_PATTERN = /^\d+$/;
@@ -7305,7 +7277,7 @@ const NO_RUNTIME_FLAGS = {
7305
7277
  jar: null,
7306
7278
  port: null
7307
7279
  };
7308
- function hostDeps$6(ctx) {
7280
+ function hostDeps$5(ctx) {
7309
7281
  return {
7310
7282
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
7311
7283
  releases: releaseLookup(ctx.signal)
@@ -7434,9 +7406,9 @@ async function persistedRuntime(ctx, options, deps) {
7434
7406
  });
7435
7407
  return chosen;
7436
7408
  }
7437
- function runtimeStep(options, deps = hostDeps$6) {
7409
+ function runtimeStep(options, deps = hostDeps$5) {
7438
7410
  return {
7439
- id: STEP_ID$3,
7411
+ id: STEP_ID$2,
7440
7412
  title: "Install Metabase",
7441
7413
  redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
7442
7414
  async detect(ctx) {
@@ -7460,204 +7432,6 @@ function runtimeStep(options, deps = hostDeps$6) {
7460
7432
  }
7461
7433
  };
7462
7434
  }
7463
- //#endregion
7464
- //#region src/steps/sample-database.ts
7465
- const STEP_ID$2 = "sample-database";
7466
- const SAMPLE_DATABASE_DISPLAY_NAME = "Sample Database";
7467
- const POSTGRES_ENGINE = "postgres";
7468
- const NOT_FOUND_STATUS$1 = 404;
7469
- function hostDeps$5(ctx) {
7470
- return {
7471
- runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
7472
- client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
7473
- readyTimeoutMs: SAMPLE_DATABASE_READY_TIMEOUT_MS,
7474
- now: () => /* @__PURE__ */ new Date()
7475
- };
7476
- }
7477
- function isNotFound$2(error) {
7478
- return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1;
7479
- }
7480
- function requireApiKey(ctx) {
7481
- if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
7482
- return ctx.secrets.apiKey;
7483
- }
7484
- async function recordedDatabase(record, client) {
7485
- if (record.databaseId === null) return null;
7486
- try {
7487
- return await client.database.get(record.databaseId);
7488
- } catch (error) {
7489
- if (isNotFound$2(error)) return null;
7490
- throw error;
7491
- }
7492
- }
7493
- async function persistedRecord(ctx, deps) {
7494
- const existing = ctx.state.sampleDatabase;
7495
- if (existing !== null) {
7496
- if ((await sampleDatabaseContainer(existing, deps.runtime.docker, deps.runtime.rdeHome).status()).installed || await deps.runtime.ports.isFree(existing.port)) return {
7497
- record: existing,
7498
- staleDatabaseId: null
7499
- };
7500
- const pointer = existing.databaseId === null ? "" : `; Metabase database ${existing.databaseId} still points at the old one`;
7501
- ctx.ui.warn(`Port ${existing.port} is in use by another program; the sample database moves to a new port${pointer}`);
7502
- }
7503
- const claims = await portClaims(ctx.state, deps.runtime);
7504
- const port = await deps.runtime.ports.findFree(SAMPLE_DATABASE_PORT, claims);
7505
- if (port !== 25432) ctx.ui.warn(`Port ${SAMPLE_DATABASE_PORT} is in use or claimed; using ${port} for the sample database`);
7506
- const record = {
7507
- containerName: sampleContainerName(ctx.state.name, deps.runtime.rdeHome),
7508
- image: SAMPLE_DATABASE_IMAGE,
7509
- port,
7510
- databaseId: null
7511
- };
7512
- await ctx.saveState({
7513
- ...ctx.state,
7514
- sampleDatabase: record
7515
- });
7516
- return {
7517
- record,
7518
- staleDatabaseId: existing?.databaseId ?? null
7519
- };
7520
- }
7521
- const LINUX = "linux";
7522
- async function gatewayAddress(ctx, deps) {
7523
- if (!(ctx.state.runtime?.kind === "docker") || deps.runtime.platform !== LINUX) return null;
7524
- return deps.runtime.docker.bridgeGateway();
7525
- }
7526
- async function recordSampleDatabaseMove(ctx, from, to) {
7527
- ctx.ui.warn(`Port ${from.port} is taken; the sample database moved to ${to.port}`);
7528
- const record = {
7529
- ...to,
7530
- databaseId: null
7531
- };
7532
- await ctx.saveState({
7533
- ...ctx.state,
7534
- sampleDatabase: record,
7535
- databases: ctx.state.databases.filter((entry) => entry.id !== from.databaseId)
7536
- });
7537
- return record;
7538
- }
7539
- async function installAndStart(ctx, sidecar, installed, deps) {
7540
- const installing = ctx.ui.spinner(`Installing the sample database from ${installed.image}`);
7541
- try {
7542
- await sidecar.install({
7543
- onProgress: (line) => installing.update(line),
7544
- alsoOn: await gatewayAddress(ctx, deps)
7545
- });
7546
- } catch (error) {
7547
- installing.fail("Sample database install failed");
7548
- throw error;
7549
- }
7550
- installing.stop(`Sample database container ${installed.containerName} on port ${installed.port}`);
7551
- const started = await startOnFreePort(installed, deps.runtime.docker, deps.runtime.rdeHome);
7552
- const record = started.state.port === installed.port ? installed : await recordSampleDatabaseMove(ctx, installed, started.state);
7553
- const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
7554
- try {
7555
- await sidecar.waitReady({
7556
- timeoutMs: deps.readyTimeoutMs,
7557
- signal: ctx.signal
7558
- });
7559
- } catch (error) {
7560
- waiting.fail("Postgres did not become ready");
7561
- throw error;
7562
- }
7563
- waiting.stop(`Postgres is ready on ${LOOPBACK_HOST}:${record.port}`);
7564
- return record;
7565
- }
7566
- function connectionDetails(record, configured, deps) {
7567
- const alias = runtimeFromState(configured.runtime, deps.runtime).hostAlias();
7568
- return applyHostAlias({
7569
- host: "localhost",
7570
- port: record.port,
7571
- dbname: SAMPLE_DATABASE_NAME,
7572
- user: SAMPLE_DATABASE_USER,
7573
- password: SAMPLE_DATABASE_PASSWORD
7574
- }, alias).details;
7575
- }
7576
- function toRecord(database, createdAt) {
7577
- return {
7578
- id: database.id,
7579
- name: database.name,
7580
- engine: database.engine,
7581
- createdAt
7582
- };
7583
- }
7584
- async function connectDatabase(ctx, choice, configured, client, deps) {
7585
- const { record } = choice;
7586
- const known = await recordedDatabase(record, client);
7587
- if (known !== null) {
7588
- ctx.ui.info(`${known.name} is already connected as database ${known.id}`);
7589
- return known;
7590
- }
7591
- const spinner = ctx.ui.spinner(`Connecting ${SAMPLE_DATABASE_DISPLAY_NAME}…`);
7592
- let database;
7593
- try {
7594
- database = await client.database.create({
7595
- name: SAMPLE_DATABASE_DISPLAY_NAME,
7596
- engine: POSTGRES_ENGINE,
7597
- details: connectionDetails(record, configured, deps)
7598
- });
7599
- } catch (error) {
7600
- spinner.fail(`Could not connect ${SAMPLE_DATABASE_DISPLAY_NAME}`);
7601
- throw error;
7602
- }
7603
- const stale = /* @__PURE__ */ new Set([record.databaseId, choice.staleDatabaseId]);
7604
- const others = ctx.state.databases.filter((entry) => !stale.has(entry.id));
7605
- await ctx.saveState({
7606
- ...ctx.state,
7607
- sampleDatabase: {
7608
- ...record,
7609
- databaseId: database.id
7610
- },
7611
- databases: [...others, toRecord(database, deps.now().toISOString())]
7612
- });
7613
- spinner.stop(`Connected ${database.name} as database ${database.id}`);
7614
- return database;
7615
- }
7616
- function usageLines(record) {
7617
- return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms can write to this database."].join("\n");
7618
- }
7619
- function sampleDatabaseStep(deps = hostDeps$5) {
7620
- return {
7621
- id: STEP_ID$2,
7622
- title: "Connect a sample database",
7623
- async detect(ctx) {
7624
- const resolved = deps(ctx);
7625
- if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
7626
- const record = ctx.state.sampleDatabase;
7627
- const configured = configuredRuntime(ctx.state);
7628
- if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
7629
- if (!(await sampleDatabaseContainer(record, resolved.runtime.docker, resolved.runtime.rdeHome).status()).installed || !await isInstanceRunning(ctx.state, resolved.runtime)) return "todo";
7630
- const client = resolved.client(configured.url, {
7631
- kind: "api-key",
7632
- key: ctx.secrets.apiKey
7633
- });
7634
- try {
7635
- return await recordedDatabase(record, client) === null ? "todo" : "done";
7636
- } catch (error) {
7637
- if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
7638
- throw error;
7639
- }
7640
- },
7641
- async run(ctx) {
7642
- const resolved = deps(ctx);
7643
- const configured = requireConfiguredRuntime(ctx.state);
7644
- const apiKey = requireApiKey(ctx);
7645
- const docker = await resolved.runtime.docker.available();
7646
- if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
7647
- const persisted = await persistedRecord(ctx, resolved);
7648
- const record = await installAndStart(ctx, sampleDatabaseContainer(persisted.record, resolved.runtime.docker, resolved.runtime.rdeHome), persisted.record, resolved);
7649
- await connectDatabase(ctx, {
7650
- ...persisted,
7651
- record
7652
- }, configured, resolved.client(configured.url, {
7653
- kind: "api-key",
7654
- key: apiKey
7655
- }), resolved);
7656
- ctx.ui.note(usageLines(record), "Sample database");
7657
- return { outcome: "completed" };
7658
- }
7659
- };
7660
- }
7661
7435
  const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
7662
7436
  //#endregion
7663
7437
  //#region src/steps/start-metabase.ts
@@ -7679,21 +7453,6 @@ async function runningVersion(client) {
7679
7453
  function progressLabel(progress) {
7680
7454
  return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
7681
7455
  }
7682
- async function startSampleDatabase(ctx, deps) {
7683
- const record = ctx.state.sampleDatabase;
7684
- if (record === null) return;
7685
- const started = await startOnFreePort(record, deps.runtime.docker, deps.runtime.rdeHome);
7686
- if (started.state.port !== record.port) await recordSampleDatabaseMove(ctx, record, started.state);
7687
- switch (started.outcome) {
7688
- case "missing":
7689
- ctx.ui.warn(`Sample database container ${record.containerName} is missing; \`rde init\` recreates it`);
7690
- return;
7691
- case "started":
7692
- ctx.ui.info(`Sample database container ${record.containerName} started`);
7693
- return;
7694
- case "running": return;
7695
- }
7696
- }
7697
7456
  async function withMoveHint(ctx, deps, taken) {
7698
7457
  const claims = await portClaims(ctx.state, deps.runtime);
7699
7458
  const free = await deps.runtime.ports.findFree(taken.port + 1, claims);
@@ -7730,7 +7489,6 @@ function startMetabaseStep(deps = hostDeps$4) {
7730
7489
  const runtime = runtimeFromState(configured.runtime, resolved.runtime);
7731
7490
  const client = resolved.client(configured.url, NO_AUTH);
7732
7491
  await startRuntime(ctx, runtime, resolved);
7733
- await startSampleDatabase(ctx, resolved);
7734
7492
  const spinner = ctx.ui.spinner("Starting Metabase…");
7735
7493
  let shown = 0;
7736
7494
  try {
@@ -7841,7 +7599,6 @@ async function summaryLines(state, apiKey, deps) {
7841
7599
  if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
7842
7600
  lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
7843
7601
  lines.push(...await databaseLines$1(state, apiKey, deps));
7844
- if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
7845
7602
  lines.push(...gitLines(state));
7846
7603
  const installed = await skillsCli(deps.runner).list();
7847
7604
  const agents = await detectAgents(deps.probe);
@@ -7917,7 +7674,7 @@ const DEFAULT_STEP_OPTIONS = {
7917
7674
  runtime: NO_RUNTIME_FLAGS,
7918
7675
  agents: DETECT_AGENTS,
7919
7676
  admin: DEFAULT_ADMIN,
7920
- databases: SAMPLE_STANDS_IN,
7677
+ databases: DATABASE_REQUIRED,
7921
7678
  updates: "published"
7922
7679
  };
7923
7680
  const LOCAL_STEP_OPTIONS = {
@@ -7933,7 +7690,6 @@ const STEP_BY_ID = {
7933
7690
  "admin-user": (options) => adminUserStep(options.admin),
7934
7691
  "api-key": () => apiKeyStep(),
7935
7692
  transforms: () => transformsStep(),
7936
- "sample-database": () => sampleDatabaseStep(),
7937
7693
  license: () => licenseStep(),
7938
7694
  databases: (options) => databasesStep(options.databases),
7939
7695
  "git-sync": () => gitSyncStep(),
@@ -8009,7 +7765,7 @@ const SECTIONS = {
8009
7765
  }).optional(),
8010
7766
  "api-key": z.strictObject({ profile: MbProfileChoice }).optional(),
8011
7767
  license: z.strictObject({ token: EnvString.nullable() }).optional(),
8012
- 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(),
8013
7769
  "git-sync": z.strictObject({
8014
7770
  location: z.enum([
8015
7771
  "here",
@@ -8081,8 +7837,8 @@ function runtimeKindOf(runtime) {
8081
7837
  return null;
8082
7838
  }
8083
7839
  function databasesRequest(databases, run) {
8084
- if (databases !== void 0) return databases.length > 0 ? "named" : "sample-stands-in";
8085
- return run.interactive ? "ask-once" : "sample-stands-in";
7840
+ if (databases !== void 0) return "named";
7841
+ return run.interactive ? "ask" : "required";
8086
7842
  }
8087
7843
  function agentsOption(agents, run) {
8088
7844
  if (agents !== void 0) return {
@@ -8144,7 +7900,6 @@ async function loadInitAnswers(path) {
8144
7900
  }
8145
7901
  function stepsAskingForWork(answers) {
8146
7902
  const license = answers.license;
8147
- const databases = answers.databases;
8148
7903
  const git = answers["git-sync"];
8149
7904
  const agents = answers["agents-skills"];
8150
7905
  return [
@@ -8166,7 +7921,7 @@ function stepsAskingForWork(answers) {
8166
7921
  },
8167
7922
  {
8168
7923
  step: "databases",
8169
- asks: databases !== void 0 && databases.length > 0
7924
+ asks: answers.databases !== void 0
8170
7925
  },
8171
7926
  {
8172
7927
  step: "git-sync",
@@ -8203,7 +7958,6 @@ const FLAG_FOR_KEY$1 = {
8203
7958
  [API_KEY_KEYS.password]: "--admin-password",
8204
7959
  [API_KEY_KEYS.profile]: "--mb-profile",
8205
7960
  [LICENSE_KEYS.token]: "--license-token or --skip-license",
8206
- [DATABASE_KEYS.skip]: "--skip-databases",
8207
7961
  [GIT_SYNC_KEYS.location]: "--git-dir",
8208
7962
  [GIT_SYNC_KEYS.path]: "--git-dir",
8209
7963
  [GIT_SYNC_KEYS.remote]: "--git-remote or --skip-git",
@@ -8211,7 +7965,7 @@ const FLAG_FOR_KEY$1 = {
8211
7965
  [GIT_SYNC_KEYS.token]: "--git-token"
8212
7966
  };
8213
7967
  const DATABASE_FLAG_FOR_NAME = {
8214
- engine: "--database or --skip-databases",
7968
+ engine: "--database",
8215
7969
  uri: "--database"
8216
7970
  };
8217
7971
  function howToAnswer(key) {
@@ -8302,13 +8056,6 @@ function licenseSection(flags, secrets) {
8302
8056
  }
8303
8057
  return secrets.has("--license-token") ? { token: secrets.get("--license-token") } : void 0;
8304
8058
  }
8305
- function databasesSection(flags) {
8306
- if (flags.skipDatabases) {
8307
- if (flags.database.length > 0) throw conflict("--skip-databases", "--database");
8308
- return [];
8309
- }
8310
- return flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0;
8311
- }
8312
8059
  function gitRemoteKind(flag, url) {
8313
8060
  if (flag === void 0) return url === void 0 ? void 0 : "hosted";
8314
8061
  const kind = GIT_REMOTE_NAMES[flag];
@@ -8429,7 +8176,7 @@ async function answersFromFlags(flags, readStdin) {
8429
8176
  runtime: runtimeSection(flags),
8430
8177
  "admin-user": adminSection(flags, secrets),
8431
8178
  license: licenseSection(flags, secrets),
8432
- databases: databasesSection(flags),
8179
+ databases: flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0,
8433
8180
  "git-sync": gitSection(flags, secrets),
8434
8181
  "agents-skills": agentsSection(flags),
8435
8182
  "api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
@@ -8854,6 +8601,7 @@ const dbAdd = defineRdeCommand({
8854
8601
  description: "Engine of a --url that fits several engines; the error names them"
8855
8602
  }
8856
8603
  },
8604
+ interrupted: INTERRUPTED,
8857
8605
  async run(args) {
8858
8606
  if (args.url === void 0 && !stdinIsTty()) throw new ConfigError(NEEDS_URL_ERROR);
8859
8607
  const name = await defaultInstanceName();
@@ -8885,6 +8633,7 @@ const dbList = defineRdeCommand({
8885
8633
  description: "Print the list as JSON",
8886
8634
  default: false
8887
8635
  } },
8636
+ interrupted: INTERRUPTED,
8888
8637
  async run(args) {
8889
8638
  const name = await defaultInstanceName();
8890
8639
  const state = await requireInstanceState(name);
@@ -9508,6 +9257,11 @@ async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
9508
9257
  function isNotFound$1(error) {
9509
9258
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
9510
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
+ }
9511
9265
  async function licenseRow(client) {
9512
9266
  const label = "license";
9513
9267
  try {
@@ -9517,9 +9271,9 @@ async function licenseRow(client) {
9517
9271
  hint: "`rde init --from license` with a valid token",
9518
9272
  step: "license"
9519
9273
  });
9520
- 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)));
9521
9275
  } catch (error) {
9522
- 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`);
9523
9277
  throw error;
9524
9278
  }
9525
9279
  }
@@ -9537,46 +9291,6 @@ async function databasesRow(state, client) {
9537
9291
  const names = state.databases.map((record) => record.name).join(", ");
9538
9292
  return ok("databases", label, `${state.databases.length} connected: ${names}`);
9539
9293
  }
9540
- const SAMPLE_DATABASE_STEP = "sample-database";
9541
- const SAMPLE_LABEL = "sample database";
9542
- async function sampleDatabaseRow(record, deps, dockerOk, access) {
9543
- const id = "sample-database";
9544
- if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
9545
- const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
9546
- if (!status.installed) return failed(id, SAMPLE_LABEL, {
9547
- detail: `${record.containerName} does not exist`,
9548
- hint: "`rde init` recreates it",
9549
- step: SAMPLE_DATABASE_STEP
9550
- });
9551
- if (!status.running) return failed(id, SAMPLE_LABEL, {
9552
- detail: `${record.containerName} is stopped`,
9553
- hint: "`rde start`",
9554
- step: "start-metabase"
9555
- });
9556
- if (await deps.runtime.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
9557
- detail: `${record.containerName} runs but port ${record.port} is not answering`,
9558
- hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
9559
- step: null
9560
- });
9561
- const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
9562
- if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
9563
- detail: `${where}; not connected to Metabase yet`,
9564
- hint: "`rde init` connects it",
9565
- step: SAMPLE_DATABASE_STEP
9566
- });
9567
- if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
9568
- try {
9569
- const database = await access.client.database.get(record.databaseId);
9570
- return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} ${syncLabel(database.initial_sync_status)}`);
9571
- } catch (error) {
9572
- if (isNotFound$1(error)) return failed(id, SAMPLE_LABEL, {
9573
- detail: `${where}; database ${record.databaseId} is gone from Metabase`,
9574
- hint: "`rde init` connects it again",
9575
- step: SAMPLE_DATABASE_STEP
9576
- });
9577
- throw error;
9578
- }
9579
- }
9580
9294
  function isMbFailure$1(error) {
9581
9295
  return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
9582
9296
  }
@@ -9730,7 +9444,6 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
9730
9444
  skipped("mb-profile", "mb profile", NO_INSTANCE_REASON),
9731
9445
  skipped("license", "license", NO_INSTANCE_REASON),
9732
9446
  skipped("databases", "databases", NO_INSTANCE_REASON),
9733
- skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE_REASON),
9734
9447
  skipped("git-sync", "remote sync", NO_INSTANCE_REASON)
9735
9448
  ];
9736
9449
  const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
@@ -9749,7 +9462,6 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
9749
9462
  const access = apiKey.access;
9750
9463
  if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
9751
9464
  else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
9752
- rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
9753
9465
  rows.push(await gitSyncRow(state, mbInstanceOf(state, secrets), deps));
9754
9466
  return rows;
9755
9467
  }
@@ -9759,7 +9471,7 @@ async function collectDoctor(instance, state, secrets, deps) {
9759
9471
  const mb = await mbFinding(deps);
9760
9472
  const profiles = sharedProfileList(deps);
9761
9473
  const checks = [...machine, mb.row];
9762
- 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));
9763
9475
  else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
9764
9476
  const rdeProfile = state === null ? null : state.mbProfile;
9765
9477
  checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
@@ -9866,6 +9578,7 @@ const doctor = defineRdeCommand({
9866
9578
  default: false
9867
9579
  }
9868
9580
  },
9581
+ interrupted: INTERRUPTED,
9869
9582
  async run(args) {
9870
9583
  const name = await instanceNameOrDefault();
9871
9584
  const state = await readInstanceState(name);
@@ -9924,13 +9637,6 @@ function isLocation(value) {
9924
9637
  function isRemote(value) {
9925
9638
  return value === "local-bare" || value === "hosted";
9926
9639
  }
9927
- function applyDatabaseRecord(draft, record) {
9928
- const value = stringOf(record.answer);
9929
- const skippedConnection = record.key === DATABASE_KEYS.connection && value?.trim() === "";
9930
- const finished = parseDatabaseKey(record.key)?.name === "engine" && value === "finish";
9931
- const pickedNoStub = record.key === DATABASE_KEYS.stubs && record.answer.kind === "multiselect" && record.answer.values.length === 0;
9932
- if (skippedConnection || finished || pickedNoStub) draft.databasesFinished = true;
9933
- }
9934
9640
  function applyAdminRecord(draft, key, value) {
9935
9641
  if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
9936
9642
  else if (key === ADMIN_USER_KEYS.password) admin(draft).password = envReference(ENV_ADMIN_PASSWORD);
@@ -9954,8 +9660,7 @@ function agentIds(values) {
9954
9660
  function applyRecord(draft, record) {
9955
9661
  const { key } = record;
9956
9662
  const value = stringOf(record.answer);
9957
- if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
9958
- else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
9663
+ if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
9959
9664
  else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
9960
9665
  else if (key === AGENTS_SKILLS_KEYS.agents && record.answer.kind === "multiselect") draft.answers["agents-skills"] = { agents: agentIds(record.answer.values) };
9961
9666
  else if (key === API_KEY_KEYS.profile && value !== null) {
@@ -9981,13 +9686,9 @@ function databaseEntry(added, index) {
9981
9686
  };
9982
9687
  }
9983
9688
  function answersFromRecords(records, overrides, added) {
9984
- const draft = {
9985
- answers: {},
9986
- databasesFinished: false
9987
- };
9689
+ const draft = { answers: {} };
9988
9690
  for (const record of records) applyRecord(draft, record);
9989
- const databases = added.map(databaseEntry);
9990
- if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
9691
+ if (added.length > 0) draft.answers.databases = added.map(databaseEntry);
9991
9692
  return {
9992
9693
  ...draft.answers,
9993
9694
  ...overrides.runtime !== void 0 && { runtime: overrides.runtime },
@@ -10113,7 +9814,7 @@ const init = defineRdeCommand({
10113
9814
  },
10114
9815
  runtime: {
10115
9816
  type: "string",
10116
- 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`
10117
9818
  },
10118
9819
  image: {
10119
9820
  type: "string",
@@ -10158,12 +9859,7 @@ const init = defineRdeCommand({
10158
9859
  },
10159
9860
  database: {
10160
9861
  type: "string",
10161
- description: "Database to connect, a connection string or <engine>=<connection string> when it fits several engines; repeatable"
10162
- },
10163
- "skip-databases": {
10164
- type: "boolean",
10165
- description: "Connect none of your own databases (the sample database is still connected)",
10166
- 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"
10167
9863
  },
10168
9864
  "git-dir": {
10169
9865
  type: "string",
@@ -10201,10 +9897,11 @@ const init = defineRdeCommand({
10201
9897
  },
10202
9898
  yes: {
10203
9899
  type: "boolean",
10204
- 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",
10205
9901
  default: false
10206
9902
  }
10207
9903
  },
9904
+ interrupted: "Interrupted. Run `rde init` again to resume.",
10208
9905
  async run(args, rawArgs) {
10209
9906
  const options = stepSelection(args.from, args.only);
10210
9907
  const runOptions = {
@@ -10221,7 +9918,6 @@ const init = defineRdeCommand({
10221
9918
  licenseToken: args["license-token"],
10222
9919
  skipLicense: args["skip-license"],
10223
9920
  database: repeatedValues(rawArgs, "--database"),
10224
- skipDatabases: args["skip-databases"],
10225
9921
  gitDir: args["git-dir"],
10226
9922
  gitRemote: args["git-remote"],
10227
9923
  gitUrl: args["git-url"],
@@ -10268,6 +9964,7 @@ const logs = defineRdeCommand({
10268
9964
  description: "Keep streaming new lines",
10269
9965
  default: false
10270
9966
  } },
9967
+ interrupted: null,
10271
9968
  async run(args) {
10272
9969
  await (await loadInstance(await defaultInstanceName(), interruptSignal)).runtime.logs({
10273
9970
  follow: args.follow,
@@ -10282,6 +9979,7 @@ const open = defineRdeCommand({
10282
9979
  name: "open",
10283
9980
  description: "Open the Metabase instance in the browser"
10284
9981
  },
9982
+ interrupted: INTERRUPTED,
10285
9983
  async run() {
10286
9984
  const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
10287
9985
  printLines([await openInstance(instance.url) ? `Opened ${instance.url}` : `Open ${instance.url} in your browser`]);
@@ -10361,6 +10059,7 @@ const start = defineRdeCommand({
10361
10059
  type: "string",
10362
10060
  description: "Move the instance to this host port first; the mb profile follows, the data stays"
10363
10061
  } },
10062
+ interrupted: INTERRUPTED,
10364
10063
  async run(args) {
10365
10064
  const port = args.port === void 0 ? null : requirePortFlag(args.port);
10366
10065
  const name = await defaultInstanceName();
@@ -10422,19 +10121,6 @@ async function databasesOf(ctx, deps, health) {
10422
10121
  throw error;
10423
10122
  }
10424
10123
  }
10425
- async function sampleDatabaseOf(ctx, deps, databases) {
10426
- const record = ctx.state.sampleDatabase;
10427
- if (record === null) return null;
10428
- const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
10429
- const listed = databases?.find((database) => database.id === record.databaseId) ?? null;
10430
- return {
10431
- container: status.running ? "running" : status.installed ? "stopped" : "missing",
10432
- port: record.port,
10433
- url: sampleDatabaseUrl(record.port),
10434
- databaseId: record.databaseId,
10435
- initialSyncStatus: listed === null ? null : listed.initialSyncStatus
10436
- };
10437
- }
10438
10124
  function taskLabel(status) {
10439
10125
  const task = status.current_task;
10440
10126
  return task === null ? null : `${task.sync_task_type} ${task.status}`;
@@ -10489,7 +10175,6 @@ async function collectStatus(ctx, deps) {
10489
10175
  steps,
10490
10176
  setup: setupOf(ctx, steps),
10491
10177
  databases,
10492
- sampleDatabase: await sampleDatabaseOf(ctx, deps, databases),
10493
10178
  gitSync: await gitSyncOf(ctx, deps),
10494
10179
  contentRepository: ctx.state.gitRemote === null ? null : ctx.state.gitRemote.workdir
10495
10180
  };
@@ -10533,22 +10218,6 @@ function syncStateLabel(profile, gitSync) {
10533
10218
  if (gitSync.dirty) return `unsynced changes, ${remote}`;
10534
10219
  return `${remote}; \`${mbCommand(profile)} git-sync import\``;
10535
10220
  }
10536
- const CONTAINER_STATE_LABEL = {
10537
- running: "container running",
10538
- stopped: "container stopped; `rde start`",
10539
- missing: "container missing; `rde init` recreates it"
10540
- };
10541
- function sampleConnectionLabel(report, sample) {
10542
- if (sample.databaseId === null) return "not connected to Metabase yet; `rde init`";
10543
- if (report.databases === null) return "sync unknown while Metabase is not running";
10544
- if (sample.initialSyncStatus === null) return `database ${sample.databaseId} is gone from Metabase; \`rde init\` connects it again`;
10545
- return syncLabel(sample.initialSyncStatus);
10546
- }
10547
- function sampleDatabaseLine(report) {
10548
- const sample = report.sampleDatabase;
10549
- if (sample === null) return "Sample database: not connected; `rde init` adds one";
10550
- return `Sample database: ${sample.url}, ${CONTAINER_STATE_LABEL[sample.container]}, ${sampleConnectionLabel(report, sample)}`;
10551
- }
10552
10221
  function databaseLines(report) {
10553
10222
  if (report.databases === null) return ["Databases: unavailable while Metabase is not running"];
10554
10223
  return ["Databases:", ...databaseListingLines(report.databases).map((line) => ` ${line}`)];
@@ -10565,7 +10234,6 @@ function statusLines(report) {
10565
10234
  "Steps:",
10566
10235
  ...checklistLines(report.steps).map((line) => ` ${line}`),
10567
10236
  ...databaseLines(report),
10568
- sampleDatabaseLine(report),
10569
10237
  gitSyncLine(report),
10570
10238
  ...report.contentRepository === null ? [] : [`Content repository: ${report.contentRepository}`]
10571
10239
  ];
@@ -10582,6 +10250,7 @@ const status = defineRdeCommand({
10582
10250
  description: "Print the report as JSON",
10583
10251
  default: false
10584
10252
  } },
10253
+ interrupted: INTERRUPTED,
10585
10254
  async run(args) {
10586
10255
  const name = await defaultInstanceName();
10587
10256
  await requireInstanceState(name);
@@ -10598,18 +10267,17 @@ const status = defineRdeCommand({
10598
10267
  const stop = defineRdeCommand({
10599
10268
  meta: {
10600
10269
  name: "stop",
10601
- description: "Stop the Metabase instance and its sample database"
10270
+ description: "Stop the Metabase instance"
10602
10271
  },
10272
+ interrupted: INTERRUPTED,
10603
10273
  async run() {
10604
- const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
10274
+ const name = await defaultInstanceName();
10275
+ const instance = await loadInstance(name, interruptSignal);
10605
10276
  const before = await instance.runtime.status();
10606
10277
  await instance.runtime.stop();
10607
10278
  const lines = [before.running ? "Metabase stopped" : "Metabase is not running"];
10608
- if (instance.sampleDatabase !== null) {
10609
- const sidecar = await instance.sampleDatabase.status();
10610
- await instance.sampleDatabase.stop();
10611
- lines.push(sidecar.running ? "Sample database stopped" : "Sample database is not running");
10612
- }
10279
+ const removed = await removeLeftovers(name, interruptSignal);
10280
+ if (removed !== null) lines.push(removed);
10613
10281
  printLines(lines);
10614
10282
  }
10615
10283
  });
@@ -10620,11 +10288,13 @@ function hostUninstallDeps(signal, childOutput) {
10620
10288
  return {
10621
10289
  docker: dockerTool(runner),
10622
10290
  daemon: daemonTool(runner),
10291
+ git: gitTool(runner),
10623
10292
  runner,
10624
10293
  childOutput
10625
10294
  };
10626
10295
  }
10627
10296
  const NOT_TOUCHED_LINE = "The mb CLI, Docker, and Java themselves";
10297
+ const UNPULLED_COMMITS_SHOWN = 10;
10628
10298
  const ANY_JAR = "-jar";
10629
10299
  function isFileSystemError(error) {
10630
10300
  return error instanceof Error && "code" in error;
@@ -10830,6 +10500,71 @@ async function findSkill(runner, childOutput) {
10830
10500
  function instanceDirLabel(path, state) {
10831
10501
  return `Instance directory ${path} (${state?.gitRemote?.kind === "local-bare" ? "Metabase data and the remote-sync repository content.git" : "Metabase data"})`;
10832
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
+ }
10833
10568
  async function findConfigEntry(instance) {
10834
10569
  const path = globalConfigPath();
10835
10570
  try {
@@ -10872,14 +10607,13 @@ async function planUninstall(options, deps) {
10872
10607
  const container = await findContainer(runtime?.containerName ?? containerName(options.instance, home), deps.docker, runtime?.image ?? null, home, { graceMs: METABASE_STOP_GRACE_MS });
10873
10608
  if (container.removal !== null) removals.push(container.removal);
10874
10609
  if (container.problem !== null) problems.push(container.problem);
10875
- const sample = state?.sampleDatabase ?? null;
10876
- const sidecar = await findContainer(sample?.containerName ?? sampleContainerName(options.instance, home), deps.docker, sample?.image ?? null, home, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
10877
- if (sidecar.removal !== null) removals.push(sidecar.removal);
10878
- 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);
10879
10613
  const jarSource = state?.runtime?.kind === "jar" ? state.runtime.source : null;
10880
10614
  const jarProcess = await findJarProcess(options.instance, jarSource, deps.daemon);
10881
10615
  if (jarProcess !== null) removals.push(jarProcess);
10882
- for (const image of [container.image, sidecar.image]) {
10616
+ for (const image of [container.image, sampleDatabase.image]) {
10883
10617
  if (image === null) continue;
10884
10618
  const finding = await findImage(image, deps.docker, options.removeImage);
10885
10619
  if (finding.removal !== null) removals.push(finding.removal);
@@ -10899,6 +10633,8 @@ async function planUninstall(options, deps) {
10899
10633
  }
10900
10634
  const dir = instanceDir(options.instance);
10901
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);
10902
10638
  if (!options.keepData) {
10903
10639
  const secrets = instanceSecretsPath(options.instance);
10904
10640
  if (await pathExists(secrets)) removals.push({
@@ -10919,7 +10655,8 @@ async function planUninstall(options, deps) {
10919
10655
  return {
10920
10656
  removals,
10921
10657
  kept,
10922
- problems
10658
+ problems,
10659
+ unpulled
10923
10660
  };
10924
10661
  }
10925
10662
  function planLines$1(plan) {
@@ -10974,6 +10711,10 @@ async function uninstall(options, deps, io) {
10974
10711
  };
10975
10712
  }
10976
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
+ }
10977
10718
  if (!options.yes) {
10978
10719
  if (!await io.prompts.confirm({
10979
10720
  message: "Remove everything listed?",
@@ -11020,8 +10761,14 @@ const uninstallCommand = defineRdeCommand({
11020
10761
  type: "boolean",
11021
10762
  description: "Do not ask for confirmation",
11022
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
11023
10769
  }
11024
10770
  },
10771
+ interrupted: INTERRUPTED,
11025
10772
  async run(args) {
11026
10773
  const instance = args.instance ?? await instanceNameOrDefault();
11027
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})`);
@@ -11031,7 +10778,9 @@ const uninstallCommand = defineRdeCommand({
11031
10778
  keepData: args["keep-data"],
11032
10779
  removeSkill: args["remove-skill"],
11033
10780
  removeImage: args["remove-image"],
11034
- yes: args.yes
10781
+ yes: args.yes,
10782
+ force: args.force,
10783
+ terminal: stdinIsTty()
11035
10784
  }, hostUninstallDeps(interruptSignal, ui.childOutput), {
11036
10785
  prompts: clackPrompts({ confirmsWithYes: true }),
11037
10786
  ui
@@ -11265,8 +11014,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
11265
11014
  const stopping = ctx.ui.spinner("Stopping Metabase");
11266
11015
  await runtime.stop();
11267
11016
  stopping.stop("Metabase stopped");
11017
+ options.onPhase("backing-up", backup);
11268
11018
  await backUp(ctx, resolved.runtime.dataDir, backup);
11019
+ options.onPhase("switching", backup);
11269
11020
  await switchTo(ctx, resolved, target);
11021
+ options.onPhase("starting", backup);
11270
11022
  const to = await startUpgraded(ctx, resolved, client, configured, backup);
11271
11023
  ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
11272
11024
  return {
@@ -11278,6 +11030,14 @@ async function upgrade(ctx, options, deps = hostDeps) {
11278
11030
  }
11279
11031
  //#endregion
11280
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
+ }
11281
11041
  const upgradeCommand = defineRdeCommand({
11282
11042
  meta: {
11283
11043
  name: "upgrade",
@@ -11294,14 +11054,21 @@ const upgradeCommand = defineRdeCommand({
11294
11054
  default: false
11295
11055
  }
11296
11056
  },
11297
- 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) {
11298
11059
  if (!args.yes && !stdinIsTty()) throw new ConfigError("`rde upgrade` asks for confirmation; pass --yes to upgrade without a terminal");
11299
11060
  const name = await defaultInstanceName();
11300
11061
  await requireInstanceState(name);
11301
- await upgrade(await createWizardContext(name, { signal: interruptSignal }), {
11062
+ const ctx = await createWizardContext(name, { signal: interruptSignal });
11063
+ await upgrade(ctx, {
11302
11064
  to: args.to ?? null,
11303
- yes: args.yes
11065
+ yes: args.yes,
11066
+ onPhase: (phase, backup) => {
11067
+ interruption.message = interruptedUpgrade(phase, backup);
11068
+ }
11304
11069
  });
11070
+ const removed = await removeLeftovers(name, interruptSignal);
11071
+ if (removed !== null) ctx.ui.info(removed);
11305
11072
  }
11306
11073
  });
11307
11074
  //#endregion
@@ -11309,7 +11076,7 @@ const upgradeCommand = defineRdeCommand({
11309
11076
  const main = defineCommand({
11310
11077
  meta: {
11311
11078
  name: "rde",
11312
- version: "0.1.15",
11079
+ version: "0.1.16",
11313
11080
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
11314
11081
  },
11315
11082
  subCommands: {