@metabase/rde 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +108 -61
  2. package/dist/cli.mjs +1944 -634
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -1,13 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain, showUsage } from "citty";
3
- import { promises } from "node:fs";
3
+ import { createWriteStream, promises } from "node:fs";
4
4
  import { delimiter, dirname, join, resolve } from "node:path";
5
5
  import { ZodError, z } from "zod";
6
6
  import { homedir, tmpdir, userInfo } from "node:os";
7
7
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
8
+ import { Readable } from "node:stream";
9
+ import { pipeline } from "node:stream/promises";
8
10
  import { confirm, intro, isCancel, log, multiselect, note, outro, password, select, spinner, text } from "@clack/prompts";
9
- import { connect, createServer } from "node:net";
10
11
  import { spawn } from "node:child_process";
12
+ import { connect, createServer } from "node:net";
11
13
  import semver from "semver";
12
14
  //#region src/errors.ts
13
15
  var AbortError = class extends Error {
@@ -70,6 +72,14 @@ var MetabaseUnreachableError = class extends Error {
70
72
  this.url = url;
71
73
  }
72
74
  };
75
+ var DownloadError = class extends Error {
76
+ url;
77
+ constructor(url, reason) {
78
+ super(`downloading ${url} failed: ${reason}`);
79
+ this.name = "DownloadError";
80
+ this.url = url;
81
+ }
82
+ };
73
83
  //#endregion
74
84
  //#region src/output/error.ts
75
85
  const INTERRUPTED_MESSAGE = "Interrupted. Run `rde init` again to resume.";
@@ -115,6 +125,7 @@ const StepId = z.enum([
115
125
  "start-metabase",
116
126
  "admin-user",
117
127
  "api-key",
128
+ "sample-database",
118
129
  "license",
119
130
  "databases",
120
131
  "git-sync",
@@ -132,17 +143,20 @@ const DockerRuntimeState = z.object({
132
143
  port: z.int().positive(),
133
144
  hostAlias: z.literal(DOCKER_HOST_ALIAS)
134
145
  });
135
- const JarEdition = z.enum(["oss", "enterprise"]);
146
+ const JarSource = z.discriminatedUnion("kind", [z.object({
147
+ kind: z.literal("release"),
148
+ version: z.string()
149
+ }), z.object({
150
+ kind: z.literal("file"),
151
+ path: z.string()
152
+ })]);
136
153
  const JarRuntimeState = z.object({
137
154
  kind: z.literal("jar"),
138
- edition: JarEdition,
139
- version: z.string(),
140
- jarPath: z.string(),
141
- javaPath: z.string(),
142
- port: z.int().positive(),
143
- pid: z.int().positive().nullable()
155
+ source: JarSource,
156
+ port: z.int().positive()
144
157
  });
145
158
  const RuntimeState = z.discriminatedUnion("kind", [DockerRuntimeState, JarRuntimeState]);
159
+ const RuntimeKind = z.enum(["docker", "jar"]);
146
160
  const AdminCredentials = z.discriminatedUnion("kind", [z.object({
147
161
  kind: z.literal("prompted"),
148
162
  email: z.string()
@@ -166,6 +180,12 @@ const DatabaseRecord = z.object({
166
180
  engine: z.string(),
167
181
  createdAt: z.string()
168
182
  });
183
+ const SampleDatabaseState = z.object({
184
+ containerName: z.string(),
185
+ image: z.string(),
186
+ port: z.int().positive(),
187
+ databaseId: z.int().nullable()
188
+ });
169
189
  const GitRemoteState = z.discriminatedUnion("kind", [z.object({
170
190
  kind: z.literal("local-bare"),
171
191
  barePath: z.string(),
@@ -182,15 +202,17 @@ const StepRecord = z.object({
182
202
  completedAt: z.string()
183
203
  });
184
204
  const InstanceState = z.object({
185
- schemaVersion: z.literal(1),
205
+ schemaVersion: z.literal(3),
186
206
  name: z.string(),
187
207
  runtime: RuntimeState.nullable(),
188
208
  url: z.string().nullable(),
209
+ version: z.string().nullable(),
189
210
  admin: AdminCredentials.nullable(),
190
211
  apiKey: z.string().nullable(),
191
212
  mbProfile: z.string(),
192
213
  license: LicenseState,
193
214
  databases: z.array(DatabaseRecord),
215
+ sampleDatabase: SampleDatabaseState.nullable(),
194
216
  gitRemote: GitRemoteState.nullable(),
195
217
  steps: z.partialRecord(StepId, StepRecord)
196
218
  });
@@ -214,15 +236,17 @@ function mbCommand(profile) {
214
236
  }
215
237
  function emptyInstanceState(name) {
216
238
  return {
217
- schemaVersion: 1,
239
+ schemaVersion: 3,
218
240
  name,
219
241
  runtime: null,
220
242
  url: null,
243
+ version: null,
221
244
  admin: null,
222
245
  apiKey: null,
223
246
  mbProfile: DEFAULT_MB_PROFILE,
224
247
  license: { activated: false },
225
248
  databases: [],
249
+ sampleDatabase: null,
226
250
  gitRemote: null,
227
251
  steps: {}
228
252
  };
@@ -230,11 +254,31 @@ function emptyInstanceState(name) {
230
254
  //#endregion
231
255
  //#region src/state/migrate.ts
232
256
  const Versioned = z.object({ schemaVersion: z.int() });
257
+ const V1 = 1;
258
+ const V2 = 2;
259
+ const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
260
+ const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
261
+ function liftV1(raw) {
262
+ return {
263
+ ...StateV1.parse(raw),
264
+ schemaVersion: V2,
265
+ version: null
266
+ };
267
+ }
268
+ function liftV2(raw) {
269
+ return {
270
+ ...StateV2.parse(raw),
271
+ schemaVersion: 3,
272
+ sampleDatabase: null
273
+ };
274
+ }
233
275
  function migrateInstanceState(raw) {
234
276
  const { schemaVersion } = Versioned.parse(raw);
235
277
  switch (schemaVersion) {
236
- case 1: return InstanceState.parse(raw);
237
- default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 1)`);
278
+ case V1: return migrateInstanceState(liftV1(raw));
279
+ case V2: return migrateInstanceState(liftV2(raw));
280
+ case 3: return InstanceState.parse(raw);
281
+ default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 3)`);
238
282
  }
239
283
  }
240
284
  //#endregion
@@ -244,13 +288,23 @@ function readEnv(name) {
244
288
  return process.env[name];
245
289
  }
246
290
  const ENV_PATH = "PATH";
291
+ const ENV_JAVA_HOME = "JAVA_HOME";
292
+ function environmentWith(overrides) {
293
+ return {
294
+ ...process.env,
295
+ ...overrides
296
+ };
297
+ }
247
298
  //#endregion
248
299
  //#region src/state/paths.ts
249
300
  const RDE_DIR_NAME = ".rde";
250
301
  const INSTANCES_DIR = "instances";
251
302
  const DATA_DIR = "data";
303
+ const BACKUPS_DIR = "backups";
252
304
  const STATE_FILE = "state.json";
253
305
  const GLOBAL_CONFIG_FILE = "config.json";
306
+ const JARS_DIR = "jars";
307
+ const JAR_FILE$1 = "metabase.jar";
254
308
  function rdeHome() {
255
309
  const override = readEnv(ENV_RDE_HOME);
256
310
  if (override !== void 0) return override;
@@ -265,12 +319,24 @@ function instanceDir(name) {
265
319
  function instanceDataDir(name) {
266
320
  return join(instanceDir(name), DATA_DIR);
267
321
  }
322
+ function instanceBackupsDir(name) {
323
+ return join(instanceDir(name), BACKUPS_DIR);
324
+ }
268
325
  function instanceStatePath(name) {
269
326
  return join(instanceDir(name), STATE_FILE);
270
327
  }
271
328
  function globalConfigPath() {
272
329
  return join(rdeHome(), GLOBAL_CONFIG_FILE);
273
330
  }
331
+ function jarsDir() {
332
+ return join(rdeHome(), JARS_DIR);
333
+ }
334
+ function jarReleaseDir(version) {
335
+ return join(jarsDir(), version);
336
+ }
337
+ function releaseJarIn(jarsDirectory, version) {
338
+ return join(jarsDirectory, version, JAR_FILE$1);
339
+ }
274
340
  //#endregion
275
341
  //#region src/state/store.ts
276
342
  const FILE_MODE$1 = 384;
@@ -561,6 +627,8 @@ const Initializing = z.object({
561
627
  //#endregion
562
628
  //#region src/metabase/http.ts
563
629
  const REQUEST_TIMEOUT_MS = 3e4;
630
+ const PARTIAL_SUFFIX = ".part";
631
+ const CONTENT_LENGTH_HEADER = "content-length";
564
632
  const SESSION_HEADER = "X-Metabase-Session";
565
633
  const API_KEY_HEADER = "X-Api-Key";
566
634
  const JSON_CONTENT_TYPE = "application/json";
@@ -686,6 +754,47 @@ function createHttp(options) {
686
754
  }
687
755
  };
688
756
  }
757
+ function contentLength(response) {
758
+ const header = response.headers.get(CONTENT_LENGTH_HEADER);
759
+ if (header === null) return null;
760
+ const total = Number.parseInt(header, 10);
761
+ return Number.isNaN(total) ? null : total;
762
+ }
763
+ function progressCounter(total, onProgress) {
764
+ let received = 0;
765
+ return (chunk) => {
766
+ received += chunk.byteLength;
767
+ onProgress({
768
+ received,
769
+ total
770
+ });
771
+ return chunk;
772
+ };
773
+ }
774
+ async function downloadFile(url, destination, options) {
775
+ const fetchImpl = options.fetchImpl ?? fetch;
776
+ let response;
777
+ try {
778
+ response = await fetchImpl(url, { signal: options.signal });
779
+ } catch (error) {
780
+ if (options.signal.aborted) throw new AbortError();
781
+ throw new DownloadError(url, describeCause(error));
782
+ }
783
+ if (!response.ok) throw new DownloadError(url, `HTTP ${response.status}`);
784
+ if (response.body === null) throw new DownloadError(url, "empty response");
785
+ const partial = `${destination}${PARTIAL_SUFFIX}`;
786
+ const count = progressCounter(contentLength(response), options.onProgress);
787
+ try {
788
+ await pipeline(Readable.fromWeb(response.body), async function* (chunks) {
789
+ for await (const chunk of chunks) yield count(chunk);
790
+ }, createWriteStream(partial), { signal: options.signal });
791
+ } catch (error) {
792
+ await promises.rm(partial, { force: true });
793
+ if (options.signal.aborted) throw new AbortError();
794
+ throw new DownloadError(url, describeCause(error));
795
+ }
796
+ await promises.rename(partial, destination);
797
+ }
689
798
  //#endregion
690
799
  //#region src/metabase/domain/permissions-group.ts
691
800
  const PermissionsGroup = z.object({
@@ -914,12 +1023,20 @@ function sessionResource(http) {
914
1023
  //#endregion
915
1024
  //#region src/metabase/resources/setting.ts
916
1025
  const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
1026
+ const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
917
1027
  function settingResource(http) {
918
1028
  /** PUT /api/setting/premium-embedding-token */
919
1029
  async function setPremiumEmbeddingToken(value) {
920
1030
  await http.requestNoContent("PUT", `/api/setting/${PREMIUM_EMBEDDING_TOKEN_SETTING}`, { body: { value } });
921
1031
  }
922
- return { setPremiumEmbeddingToken };
1032
+ /** PUT /api/setting/transforms-enabled */
1033
+ async function enableTransforms() {
1034
+ await http.requestNoContent("PUT", `/api/setting/${TRANSFORMS_ENABLED_SETTING}`, { body: { value: true } });
1035
+ }
1036
+ return {
1037
+ setPremiumEmbeddingToken,
1038
+ enableTransforms
1039
+ };
923
1040
  }
924
1041
  //#endregion
925
1042
  //#region src/metabase/domain/setup.ts
@@ -2085,7 +2202,8 @@ async function runDatabaseFlow(deps) {
2085
2202
  }
2086
2203
  //#endregion
2087
2204
  //#region src/steps/databases.ts
2088
- const STEP_ID$8 = "databases";
2205
+ const STEP_ID$9 = "databases";
2206
+ const ASK_DATABASES = { requested: false };
2089
2207
  function hostDatabasesDeps(signal) {
2090
2208
  return {
2091
2209
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
@@ -2093,10 +2211,10 @@ function hostDatabasesDeps(signal) {
2093
2211
  now: () => /* @__PURE__ */ new Date()
2094
2212
  };
2095
2213
  }
2096
- function isNotServingYet(error) {
2214
+ function isNotServingYet$1(error) {
2097
2215
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
2098
2216
  }
2099
- function requireApiKey$1(state) {
2217
+ function requireApiKey$2(state) {
2100
2218
  if (state.apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
2101
2219
  return state.apiKey;
2102
2220
  }
@@ -2106,7 +2224,7 @@ function hostAliasOf(runtime) {
2106
2224
  case "jar": return null;
2107
2225
  }
2108
2226
  }
2109
- function toRecord(database, createdAt) {
2227
+ function toRecord$1(database, createdAt) {
2110
2228
  return {
2111
2229
  id: database.id,
2112
2230
  name: database.name,
@@ -2118,7 +2236,7 @@ function authenticatedClient(state, deps) {
2118
2236
  const configured = requireConfiguredRuntime(state);
2119
2237
  return deps.client(configured.url, {
2120
2238
  kind: "api-key",
2121
- key: requireApiKey$1(state)
2239
+ key: requireApiKey$2(state)
2122
2240
  });
2123
2241
  }
2124
2242
  async function listDatabases(state, deps) {
@@ -2156,7 +2274,7 @@ async function addDatabases(ctx, deps) {
2156
2274
  existingCount: existing.data.length,
2157
2275
  readTextFile: deps.readTextFile,
2158
2276
  onCreated: async (database) => {
2159
- const record = toRecord(database, deps.now().toISOString());
2277
+ const record = toRecord$1(database, deps.now().toISOString());
2160
2278
  await ctx.saveState({
2161
2279
  ...ctx.state,
2162
2280
  databases: [...ctx.state.databases, record]
@@ -2165,26 +2283,33 @@ async function addDatabases(ctx, deps) {
2165
2283
  });
2166
2284
  return existing.data.length + result.created.length > 0 ? "completed" : "skipped";
2167
2285
  }
2168
- function hostDeps$10(ctx) {
2286
+ function hostDeps$12(ctx) {
2169
2287
  return hostDatabasesDeps(ctx.signal);
2170
2288
  }
2171
- function databasesStep(deps = hostDeps$10) {
2289
+ function sampleDatabaseId(state) {
2290
+ return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
2291
+ }
2292
+ function ownDatabaseIds(state) {
2293
+ const sampleId = sampleDatabaseId(state);
2294
+ return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
2295
+ }
2296
+ function databasesStep(options, deps = hostDeps$12) {
2172
2297
  return {
2173
- id: STEP_ID$8,
2298
+ id: STEP_ID$9,
2174
2299
  title: "Connect your databases",
2175
2300
  async detect(ctx) {
2176
2301
  const configured = configuredRuntime(ctx.state);
2177
- if (configured === null || ctx.state.apiKey === null || ctx.state.databases.length === 0) return "todo";
2302
+ if (configured === null || ctx.state.apiKey === null) return "todo";
2303
+ const own = ownDatabaseIds(ctx.state);
2304
+ if (own.size === 0) return sampleDatabaseId(ctx.state) !== null && !options.requested ? "done" : "todo";
2178
2305
  const client = deps(ctx).client(configured.url, {
2179
2306
  kind: "api-key",
2180
2307
  key: ctx.state.apiKey
2181
2308
  });
2182
2309
  try {
2183
- const listed = await client.database.list();
2184
- const known = new Set(ctx.state.databases.map((record) => record.id));
2185
- return listed.data.some((database) => known.has(database.id)) ? "done" : "todo";
2310
+ return (await client.database.list()).data.some((database) => own.has(database.id)) ? "done" : "todo";
2186
2311
  } catch (error) {
2187
- if (isNotServingYet(error)) return "todo";
2312
+ if (isNotServingYet$1(error)) return "todo";
2188
2313
  throw error;
2189
2314
  }
2190
2315
  },
@@ -2331,7 +2456,7 @@ const dbAdd = defineRdeCommand({
2331
2456
  const name = await defaultInstanceName();
2332
2457
  await requireInstanceState(name);
2333
2458
  const ctx = await createWizardContext(name, { signal: interruptSignal });
2334
- await databasesStep().run(ctx);
2459
+ await databasesStep(ASK_DATABASES).run(ctx);
2335
2460
  }
2336
2461
  });
2337
2462
  //#endregion
@@ -2376,6 +2501,43 @@ const db = defineCommand({
2376
2501
  }
2377
2502
  });
2378
2503
  //#endregion
2504
+ //#region src/metabase/domain/version-info.ts
2505
+ const VersionInfo = z.object({ latest: z.object({ version: z.string() }).loose() }).loose();
2506
+ //#endregion
2507
+ //#region src/metabase/releases.ts
2508
+ const DOWNLOADS_HOST = "https://downloads.metabase.com";
2509
+ const VERSION_INFO_HOST = "https://static.metabase.com";
2510
+ const VERSION_INFO_PATH = "/version-info-ee.json";
2511
+ const JAR_FILE = "metabase.jar";
2512
+ const RELEASE_TAG$1 = /^v(?<edition>[01])\.\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
2513
+ const ENTERPRISE_EDITION = "1";
2514
+ function isReleaseTag(value) {
2515
+ return RELEASE_TAG$1.test(value);
2516
+ }
2517
+ function jarDownloadUrl(version) {
2518
+ const edition = RELEASE_TAG$1.exec(version)?.groups?.["edition"];
2519
+ if (edition === void 0) throw new Error(`${version} is not a Metabase release tag`);
2520
+ return `${DOWNLOADS_HOST}${edition === ENTERPRISE_EDITION ? "/enterprise" : ""}/${version}/${JAR_FILE}`;
2521
+ }
2522
+ function releaseLookup(signal, options = {}) {
2523
+ const http = createHttp({
2524
+ baseUrl: VERSION_INFO_HOST,
2525
+ auth: NO_AUTH,
2526
+ signal,
2527
+ ...options
2528
+ });
2529
+ return { async latestVersion() {
2530
+ return (await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionInfo })).latest.version;
2531
+ } };
2532
+ }
2533
+ function fileDownloader(signal, options = {}) {
2534
+ return (url, destination, onProgress) => downloadFile(url, destination, {
2535
+ signal,
2536
+ onProgress,
2537
+ ...options
2538
+ });
2539
+ }
2540
+ //#endregion
2379
2541
  //#region src/tools/process.ts
2380
2542
  var ProcessNotFoundError = class extends Error {
2381
2543
  command;
@@ -2531,6 +2693,49 @@ function stream(signal, command, args, options) {
2531
2693
  });
2532
2694
  });
2533
2695
  }
2696
+ async function spawnDetached(command, args, options) {
2697
+ const log = await promises.open(options.logPath, "a");
2698
+ try {
2699
+ return await new Promise((resolve, reject) => {
2700
+ const child = spawn(command, args, {
2701
+ stdio: [
2702
+ "ignore",
2703
+ log.fd,
2704
+ log.fd
2705
+ ],
2706
+ cwd: options.cwd,
2707
+ env: environmentWith(options.env),
2708
+ detached: true
2709
+ });
2710
+ child.on("error", (error) => {
2711
+ reject(isFileNotFound(error) ? new ProcessNotFoundError(command) : error);
2712
+ });
2713
+ child.on("spawn", () => {
2714
+ child.unref();
2715
+ if (child.pid === void 0) {
2716
+ reject(/* @__PURE__ */ new Error(`${command} spawned without a pid`));
2717
+ return;
2718
+ }
2719
+ resolve(child.pid);
2720
+ });
2721
+ });
2722
+ } finally {
2723
+ await log.close();
2724
+ }
2725
+ }
2726
+ const GONE_PROCESS_CODES = /* @__PURE__ */ new Set(["ESRCH", "EPERM"]);
2727
+ function isGoneProcess(error) {
2728
+ return error instanceof Error && "code" in error && typeof error.code === "string" && GONE_PROCESS_CODES.has(error.code);
2729
+ }
2730
+ function signalProcess(pid, signal) {
2731
+ try {
2732
+ process.kill(pid, signal);
2733
+ return true;
2734
+ } catch (error) {
2735
+ if (isGoneProcess(error)) return false;
2736
+ throw error;
2737
+ }
2738
+ }
2534
2739
  function stderrTail(stderr) {
2535
2740
  return stderr.trim().split("\n").slice(-5).join("\n");
2536
2741
  }
@@ -2615,15 +2820,71 @@ async function whichCommand(name) {
2615
2820
  return null;
2616
2821
  }
2617
2822
  //#endregion
2823
+ //#region src/tools/java.ts
2824
+ const JAVA = "java";
2825
+ const JAVA_INSTALL_HINT = `install a Java 21+ runtime (Temurin from https://adoptium.net, or your package manager) or point JAVA_HOME at one`;
2826
+ const VERSION_LINE = /version "(?<version>[^"]+)"/;
2827
+ const LEGACY_MAJOR = 1;
2828
+ function javaMajor(version) {
2829
+ const [first, second] = version.split(".").map((part) => Number.parseInt(part, 10));
2830
+ if (first === void 0 || Number.isNaN(first)) return null;
2831
+ if (first === LEGACY_MAJOR && second !== void 0 && !Number.isNaN(second)) return second;
2832
+ return first;
2833
+ }
2834
+ async function javaPath() {
2835
+ const home = readEnv(ENV_JAVA_HOME);
2836
+ if (home !== void 0) {
2837
+ const candidate = join(home, "bin", JAVA);
2838
+ if (await isExecutable(candidate)) return candidate;
2839
+ }
2840
+ return whichCommand(JAVA);
2841
+ }
2842
+ function javaTool(runner) {
2843
+ async function find() {
2844
+ const path = await javaPath();
2845
+ if (path === null) return {
2846
+ found: false,
2847
+ reason: "java is not installed"
2848
+ };
2849
+ let output;
2850
+ try {
2851
+ const result = await runner.run(path, ["-version"]);
2852
+ if (result.exitCode !== 0) return {
2853
+ found: false,
2854
+ reason: `${path} -version failed: ${stderrTail(result.stderr)}`
2855
+ };
2856
+ output = result.stderr;
2857
+ } catch (error) {
2858
+ if (error instanceof ProcessNotFoundError) return {
2859
+ found: false,
2860
+ reason: "java is not installed"
2861
+ };
2862
+ throw error;
2863
+ }
2864
+ const version = VERSION_LINE.exec(output)?.groups?.["version"];
2865
+ if (version === void 0) return {
2866
+ found: false,
2867
+ reason: `${path} -version printed no version`
2868
+ };
2869
+ const major = javaMajor(version);
2870
+ if (major === null) return {
2871
+ found: false,
2872
+ reason: `${path} reports an unreadable version ${version}`
2873
+ };
2874
+ return {
2875
+ found: true,
2876
+ path,
2877
+ version,
2878
+ major
2879
+ };
2880
+ }
2881
+ return { find };
2882
+ }
2883
+ //#endregion
2618
2884
  //#region src/tools/docker.ts
2619
2885
  const DOCKER = "docker";
2620
2886
  const CONTAINER_DATA_DIR = "/rde";
2621
2887
  const LOOPBACK_HOST = "127.0.0.1";
2622
- const CONTAINER_PORT$1 = 3e3;
2623
- const ENV_DB_FILE = `MB_DB_FILE=${CONTAINER_DATA_DIR}/metabase.db`;
2624
- const ENV_PLUGINS_DIR = `MB_PLUGINS_DIR=${CONTAINER_DATA_DIR}/plugins`;
2625
- const STAGING_STORE_ENV = ["MB_RUN_MODE=e2e", "MB_STORE_USE_STAGING=true"];
2626
- const ALLOW_LOCAL_WAREHOUSES_ENV = "MB_WAREHOUSE_ALLOWED_NETWORKS=allow-all";
2627
2888
  const ENV_HOST_UID = "MUID";
2628
2889
  const ENV_HOST_GID = "MGID";
2629
2890
  const ADD_HOST_GATEWAY = `--add-host=${DOCKER_HOST_ALIAS}:host-gateway`;
@@ -2659,17 +2920,10 @@ function createArgs(options) {
2659
2920
  "--name",
2660
2921
  options.name,
2661
2922
  "-p",
2662
- `${LOOPBACK_HOST}:${options.port}:${CONTAINER_PORT$1}`,
2663
- "-v",
2664
- `${options.dataDir}:${CONTAINER_DATA_DIR}`,
2665
- "-e",
2666
- ENV_DB_FILE,
2667
- "-e",
2668
- ENV_PLUGINS_DIR,
2669
- "-e",
2670
- `MB_SITE_URL=http://localhost:${options.port}`
2923
+ `${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`
2671
2924
  ];
2672
- for (const env of [...STAGING_STORE_ENV, ALLOW_LOCAL_WAREHOUSES_ENV]) args.push("-e", env);
2925
+ if (options.mount !== null) args.push("-v", `${options.mount.source}:${options.mount.target}`);
2926
+ for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
2673
2927
  if (options.hostUser !== null) {
2674
2928
  args.push("-e", `${ENV_HOST_UID}=${options.hostUser.uid}`);
2675
2929
  args.push("-e", `${ENV_HOST_GID}=${options.hostUser.gid}`);
@@ -2714,7 +2968,7 @@ function dockerTool(runner) {
2714
2968
  const result = await runner.run(DOCKER, args, { onLine: options.onLine });
2715
2969
  if (result.exitCode !== 0) throw toolFailure(DOCKER, args, result);
2716
2970
  }
2717
- async function imageExists(image) {
2971
+ async function imageId(image) {
2718
2972
  const args = [
2719
2973
  "image",
2720
2974
  "inspect",
@@ -2723,8 +2977,8 @@ function dockerTool(runner) {
2723
2977
  "{{.Id}}"
2724
2978
  ];
2725
2979
  const result = await runner.run(DOCKER, args);
2726
- if (result.exitCode === 0) return true;
2727
- if (NO_SUCH_IMAGE.test(result.stderr)) return false;
2980
+ if (result.exitCode === 0) return result.stdout.trim();
2981
+ if (NO_SUCH_IMAGE.test(result.stderr)) return null;
2728
2982
  throw toolFailure(DOCKER, args, result);
2729
2983
  }
2730
2984
  async function imageRemove(image) {
@@ -2746,6 +3000,14 @@ function dockerTool(runner) {
2746
3000
  ports: portMappings(container.HostConfig.PortBindings)
2747
3001
  };
2748
3002
  }
3003
+ async function containerImageId(name) {
3004
+ return (await docker([
3005
+ "inspect",
3006
+ name,
3007
+ "--format",
3008
+ "{{.Image}}"
3009
+ ])).stdout.trim();
3010
+ }
2749
3011
  async function containerCreate(options) {
2750
3012
  await docker(createArgs(options));
2751
3013
  }
@@ -2758,6 +3020,13 @@ function dockerTool(runner) {
2758
3020
  async function containerRemove(name) {
2759
3021
  await docker(["rm", name]);
2760
3022
  }
3023
+ async function containerExecSucceeds(name, command) {
3024
+ return (await runner.run(DOCKER, [
3025
+ "exec",
3026
+ name,
3027
+ ...command
3028
+ ])).exitCode === 0;
3029
+ }
2761
3030
  async function containerLogs(name, options) {
2762
3031
  const args = ["logs"];
2763
3032
  if (options.follow) args.push("--follow");
@@ -2768,17 +3037,213 @@ function dockerTool(runner) {
2768
3037
  return {
2769
3038
  available,
2770
3039
  imagePull,
2771
- imageExists,
3040
+ imageId,
2772
3041
  imageRemove,
2773
3042
  containerInspect,
3043
+ containerImageId,
2774
3044
  containerCreate,
2775
3045
  containerStart,
2776
3046
  containerStop,
2777
3047
  containerRemove,
3048
+ containerExecSucceeds,
2778
3049
  containerLogs
2779
3050
  };
2780
3051
  }
2781
3052
  //#endregion
3053
+ //#region src/runtime/server.ts
3054
+ const DEFAULT_PORT = 3100;
3055
+ const PLUGINS_DIR = "plugins";
3056
+ const DB_DIR = "metabase.db";
3057
+ const DB_FILE = "metabase.db";
3058
+ const STAGING_STORE_ENV = {
3059
+ MB_RUN_MODE: "e2e",
3060
+ MB_STORE_USE_STAGING: "true"
3061
+ };
3062
+ const ALLOW_LOCAL_WAREHOUSES_ENV = { MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all" };
3063
+ const NO_H2_SAMPLE_ENV = { MB_LOAD_SAMPLE_CONTENT: "false" };
3064
+ function instanceUrl(port) {
3065
+ return `http://localhost:${port}`;
3066
+ }
3067
+ function serverEnv(options) {
3068
+ return {
3069
+ MB_DB_FILE: options.dbFile,
3070
+ MB_PLUGINS_DIR: options.pluginsDir,
3071
+ MB_SITE_URL: instanceUrl(options.port),
3072
+ ...STAGING_STORE_ENV,
3073
+ ...ALLOW_LOCAL_WAREHOUSES_ENV,
3074
+ ...NO_H2_SAMPLE_ENV
3075
+ };
3076
+ }
3077
+ function jettyEnv(port) {
3078
+ return {
3079
+ MB_JETTY_HOST: LOOPBACK_HOST,
3080
+ MB_JETTY_PORT: String(port)
3081
+ };
3082
+ }
3083
+ //#endregion
3084
+ //#region src/runtime/jar.ts
3085
+ const LATEST = "latest";
3086
+ const JAR_SUFFIX = ".jar";
3087
+ const LOG_FILE = "metabase.log";
3088
+ const PID_FILE = "metabase.pid";
3089
+ const PID_FILE_MODE = 384;
3090
+ const JAR_STOP_GRACE_MS = 3e4;
3091
+ const BYTES_PER_MB = 1048576;
3092
+ const TAG_PREFIX = "v";
3093
+ function parseJarSpec(value) {
3094
+ const trimmed = value.trim();
3095
+ if (trimmed === "latest") return { kind: "latest" };
3096
+ if (trimmed.endsWith(JAR_SUFFIX)) return {
3097
+ kind: "file",
3098
+ path: resolve(trimmed)
3099
+ };
3100
+ const tagged = trimmed.startsWith(TAG_PREFIX) ? trimmed : `${TAG_PREFIX}${trimmed}`;
3101
+ return isReleaseTag(tagged) ? {
3102
+ kind: "release",
3103
+ version: tagged
3104
+ } : null;
3105
+ }
3106
+ function jarPathOf(source, jarsDir) {
3107
+ switch (source.kind) {
3108
+ case "release": return releaseJarIn(jarsDir, source.version);
3109
+ case "file": return source.path;
3110
+ }
3111
+ }
3112
+ function describeJar(source) {
3113
+ switch (source.kind) {
3114
+ case "release": return `Metabase ${source.version} jar`;
3115
+ case "file": return `jar ${source.path}`;
3116
+ }
3117
+ }
3118
+ function logPathOf(instanceDir) {
3119
+ return join(instanceDir, LOG_FILE);
3120
+ }
3121
+ function pidPathOf(instanceDir) {
3122
+ return join(instanceDir, PID_FILE);
3123
+ }
3124
+ async function pathExists$1(path) {
3125
+ try {
3126
+ await promises.access(path);
3127
+ return true;
3128
+ } catch {
3129
+ return false;
3130
+ }
3131
+ }
3132
+ async function requireJava(java) {
3133
+ const found = await java.find();
3134
+ if (!found.found) throw new ConfigError(`${found.reason}; ${JAVA_INSTALL_HINT}`);
3135
+ if (found.major < 21) throw new ConfigError(`Java ${found.version} at ${found.path} is too old: Metabase needs 21 or newer; ${JAVA_INSTALL_HINT}`);
3136
+ return found;
3137
+ }
3138
+ function megabytes(bytes) {
3139
+ return (bytes / BYTES_PER_MB).toFixed(0);
3140
+ }
3141
+ function progressLine(progress) {
3142
+ if (progress.total === null) return `${megabytes(progress.received)} MB`;
3143
+ return `${megabytes(progress.received)} of ${megabytes(progress.total)} MB`;
3144
+ }
3145
+ async function readPid(pidPath) {
3146
+ let text;
3147
+ try {
3148
+ text = await promises.readFile(pidPath, "utf8");
3149
+ } catch (error) {
3150
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
3151
+ throw error;
3152
+ }
3153
+ const pid = Number.parseInt(text.trim(), 10);
3154
+ if (Number.isNaN(pid) || pid <= 0) throw new ConfigError(`${pidPath} does not hold a pid; delete it and run \`rde start\``);
3155
+ return pid;
3156
+ }
3157
+ async function runningJarPid(instanceDir, jarPath, daemon) {
3158
+ const pid = await readPid(pidPathOf(instanceDir));
3159
+ if (pid === null) return null;
3160
+ const commandLine = await daemon.commandLine(pid);
3161
+ return commandLine !== null && commandLine.includes(jarPath) ? pid : null;
3162
+ }
3163
+ async function inspectJar(state, deps) {
3164
+ const jarPath = jarPathOf(state.source, deps.jarsDir);
3165
+ return {
3166
+ jarPath,
3167
+ jarExists: await pathExists$1(jarPath),
3168
+ pid: await runningJarPid(deps.instanceDir, jarPath, deps.daemon)
3169
+ };
3170
+ }
3171
+ function jarRuntime(state, deps) {
3172
+ const { daemon } = deps;
3173
+ const url = instanceUrl(state.port);
3174
+ const jarPath = jarPathOf(state.source, deps.jarsDir);
3175
+ const logPath = logPathOf(deps.instanceDir);
3176
+ const pidPath = pidPathOf(deps.instanceDir);
3177
+ function runningPid() {
3178
+ return runningJarPid(deps.instanceDir, jarPath, daemon);
3179
+ }
3180
+ async function ensureJar(options) {
3181
+ if (await pathExists$1(jarPath)) return;
3182
+ switch (state.source.kind) {
3183
+ case "file": throw new ConfigError(`${jarPath} does not exist; run \`rde init\` again with another jar`);
3184
+ case "release":
3185
+ await promises.mkdir(dirname(jarPath), { recursive: true });
3186
+ await deps.download(jarDownloadUrl(state.source.version), jarPath, (progress) => options.onProgress(progressLine(progress)));
3187
+ }
3188
+ }
3189
+ async function install(options) {
3190
+ await requireJava(deps.java);
3191
+ await promises.mkdir(join(deps.dataDir, PLUGINS_DIR), { recursive: true });
3192
+ await promises.mkdir(join(deps.dataDir, DB_DIR), { recursive: true });
3193
+ await ensureJar(options);
3194
+ }
3195
+ async function start() {
3196
+ if (await runningPid() !== null) return;
3197
+ if (!await pathExists$1(jarPath)) throw new ConfigError(`${jarPath} does not exist; run \`rde init\``);
3198
+ const java = await requireJava(deps.java);
3199
+ await promises.mkdir(deps.instanceDir, { recursive: true });
3200
+ const pid = await daemon.start({
3201
+ command: java.path,
3202
+ args: ["-jar", jarPath],
3203
+ options: {
3204
+ cwd: deps.dataDir,
3205
+ env: {
3206
+ ...serverEnv({
3207
+ dbFile: join(deps.dataDir, DB_DIR, DB_FILE),
3208
+ pluginsDir: join(deps.dataDir, PLUGINS_DIR),
3209
+ port: state.port
3210
+ }),
3211
+ ...jettyEnv(state.port)
3212
+ },
3213
+ logPath
3214
+ }
3215
+ });
3216
+ await promises.writeFile(pidPath, `${pid}\n`, { mode: PID_FILE_MODE });
3217
+ }
3218
+ async function stop() {
3219
+ const pid = await runningPid();
3220
+ if (pid !== null) await daemon.terminate(pid, { graceMs: JAR_STOP_GRACE_MS });
3221
+ await promises.rm(pidPath, { force: true });
3222
+ }
3223
+ async function status() {
3224
+ const inspection = await inspectJar(state, deps);
3225
+ return {
3226
+ installed: inspection.jarExists,
3227
+ running: inspection.pid !== null,
3228
+ url
3229
+ };
3230
+ }
3231
+ async function logs(options) {
3232
+ if (!await pathExists$1(logPath)) throw new ConfigError(`${logPath} does not exist yet; run \`rde start\``);
3233
+ await daemon.tailLog(logPath, options);
3234
+ }
3235
+ return {
3236
+ kind: "jar",
3237
+ install,
3238
+ start,
3239
+ stop,
3240
+ status,
3241
+ logs,
3242
+ url: () => url,
3243
+ hostAlias: () => null
3244
+ };
3245
+ }
3246
+ //#endregion
2782
3247
  //#region src/runtime/port.ts
2783
3248
  const PORT_SCAN_LIMIT = 100;
2784
3249
  function canBind(port) {
@@ -2818,93 +3283,342 @@ const hostPortProbe = {
2818
3283
  findFree: findFreePort
2819
3284
  };
2820
3285
  //#endregion
2821
- //#region src/tools/agents.ts
2822
- const AgentId = z.enum([
2823
- "claude-code",
2824
- "codex",
2825
- "cursor",
2826
- "gemini-cli",
2827
- "opencode",
2828
- "windsurf"
2829
- ]);
2830
- const AGENTS = [
2831
- {
2832
- id: "claude-code",
2833
- displayName: "Claude Code",
2834
- skillsCliName: "claude-code",
2835
- command: "claude",
2836
- launch: "claude",
2837
- configDir: (home) => join(home, ".claude")
2838
- },
2839
- {
2840
- id: "codex",
2841
- displayName: "Codex",
2842
- skillsCliName: "codex",
2843
- command: "codex",
2844
- launch: "codex",
2845
- configDir: (home) => join(home, ".codex")
2846
- },
2847
- {
2848
- id: "cursor",
2849
- displayName: "Cursor",
2850
- skillsCliName: "cursor",
2851
- command: "cursor",
2852
- launch: "cursor .",
2853
- configDir: (home) => join(home, ".cursor")
2854
- },
2855
- {
2856
- id: "gemini-cli",
2857
- displayName: "Gemini CLI",
2858
- skillsCliName: "gemini-cli",
2859
- command: "gemini",
2860
- launch: "gemini",
2861
- configDir: (home) => join(home, ".gemini")
2862
- },
2863
- {
2864
- id: "opencode",
2865
- displayName: "OpenCode",
2866
- skillsCliName: "opencode",
2867
- command: "opencode",
2868
- launch: "opencode",
2869
- configDir: (_home, configHome) => join(configHome, "opencode")
2870
- },
2871
- {
2872
- id: "windsurf",
2873
- displayName: "Windsurf",
2874
- skillsCliName: "windsurf",
2875
- command: null,
2876
- launch: "windsurf .",
2877
- configDir: (home) => join(home, ".codeium", "windsurf")
2878
- }
2879
- ];
2880
- async function directoryExists$1(path) {
2881
- try {
2882
- return (await promises.stat(path)).isDirectory();
2883
- } catch {
2884
- return false;
2885
- }
3286
+ //#region src/tools/daemon.ts
3287
+ const PS = "ps";
3288
+ const EXIT_POLL_MS = 250;
3289
+ const TAIL = "tail";
3290
+ const WHOLE_FILE = "+1";
3291
+ function tailArgs(path, options) {
3292
+ const args = ["-n", options.tail === null ? WHOLE_FILE : String(options.tail)];
3293
+ if (options.follow) args.push("-f");
3294
+ args.push(path);
3295
+ return args;
2886
3296
  }
2887
- function hostAgentProbe() {
2888
- const home = homedir();
3297
+ function daemonTool(runner) {
3298
+ async function commandLine(pid) {
3299
+ if (!signalProcess(pid, 0)) return null;
3300
+ const result = await runner.run(PS, [
3301
+ "-o",
3302
+ "command=",
3303
+ "-p",
3304
+ String(pid)
3305
+ ]);
3306
+ const line = result.stdout.trim();
3307
+ return result.exitCode === 0 && line.length > 0 ? line : null;
3308
+ }
3309
+ async function terminate(pid, options) {
3310
+ if (!signalProcess(pid, "SIGTERM")) return;
3311
+ const deadline = Date.now() + options.graceMs;
3312
+ while (Date.now() < deadline) {
3313
+ await setTimeout$1(EXIT_POLL_MS);
3314
+ if (!signalProcess(pid, 0)) return;
3315
+ }
3316
+ signalProcess(pid, "SIGKILL");
3317
+ while (signalProcess(pid, 0)) await setTimeout$1(EXIT_POLL_MS);
3318
+ }
2889
3319
  return {
2890
- commandExists: async (command) => await whichCommand(command) !== null,
2891
- directoryExists: directoryExists$1,
2892
- home,
2893
- configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
3320
+ start: (spawn) => spawnDetached(spawn.command, spawn.args, spawn.options),
3321
+ commandLine,
3322
+ terminate,
3323
+ tailLog: (path, options) => runner.stream(TAIL, tailArgs(path, options))
2894
3324
  };
2895
3325
  }
2896
- async function isDetected(agent, probe) {
2897
- if (agent.command !== null && await probe.commandExists(agent.command)) return true;
2898
- return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
3326
+ //#endregion
3327
+ //#region src/runtime/docker.ts
3328
+ const DEFAULT_IMAGE = "metabase/metabase-dev:transform-tests-ee";
3329
+ const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
3330
+ const CONTAINER_PORT = 3e3;
3331
+ function containerName(instanceName) {
3332
+ return `${CONTAINER_NAME_PREFIX$1}-${instanceName}`;
2899
3333
  }
2900
- async function detectAgents(probe) {
2901
- return Promise.all(AGENTS.map(async (agent) => ({
2902
- id: agent.id,
2903
- displayName: agent.displayName,
2904
- skillsCliName: agent.skillsCliName,
2905
- launch: agent.launch,
2906
- detected: await isDetected(agent, probe)
2907
- })));
3334
+ function publishesPortOnLoopback(inspection, port) {
3335
+ return inspection.ports.some((mapping) => mapping.container === CONTAINER_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
3336
+ }
3337
+ function hostUserFor(platform, hostUser) {
3338
+ return platform === "linux" ? hostUser : null;
3339
+ }
3340
+ function dockerRuntime(state, deps) {
3341
+ const { docker } = deps;
3342
+ const url = instanceUrl(state.port);
3343
+ async function inspect() {
3344
+ return docker.containerInspect(state.containerName);
3345
+ }
3346
+ function conflict(reason) {
3347
+ return new ConfigError(`container ${state.containerName} already exists ${reason}; remove it with \`docker rm -f ${state.containerName}\` and run \`rde init\` again`);
3348
+ }
3349
+ async function install(options) {
3350
+ const existing = await inspect();
3351
+ if (existing.exists) {
3352
+ if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
3353
+ if (!publishesPortOnLoopback(existing, state.port)) throw conflict(`without port ${state.port} published on ${LOOPBACK_HOST}`);
3354
+ return;
3355
+ }
3356
+ await promises.mkdir(join(deps.dataDir, PLUGINS_DIR), { recursive: true });
3357
+ await docker.imagePull(state.image, { onLine: options.onProgress });
3358
+ await docker.containerCreate({
3359
+ name: state.containerName,
3360
+ image: state.image,
3361
+ port: {
3362
+ host: state.port,
3363
+ container: CONTAINER_PORT
3364
+ },
3365
+ mount: {
3366
+ source: deps.dataDir,
3367
+ target: CONTAINER_DATA_DIR
3368
+ },
3369
+ env: serverEnv({
3370
+ dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
3371
+ pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
3372
+ port: state.port
3373
+ }),
3374
+ addHostGateway: deps.platform === "linux",
3375
+ hostUser: hostUserFor(deps.platform, deps.hostUser)
3376
+ });
3377
+ }
3378
+ async function start() {
3379
+ const existing = await inspect();
3380
+ if (!existing.exists) throw new ConfigError(`container ${state.containerName} does not exist; run \`rde init\``);
3381
+ if (existing.running) return;
3382
+ await docker.containerStart(state.containerName);
3383
+ }
3384
+ async function stop() {
3385
+ const existing = await inspect();
3386
+ if (!existing.exists || !existing.running) return;
3387
+ await docker.containerStop(state.containerName);
3388
+ }
3389
+ async function status() {
3390
+ const existing = await inspect();
3391
+ return {
3392
+ installed: existing.exists,
3393
+ running: existing.exists && existing.running,
3394
+ url
3395
+ };
3396
+ }
3397
+ async function logs(options) {
3398
+ if (!(await inspect()).exists) throw new ConfigError(`container ${state.containerName} does not exist; run \`rde init\``);
3399
+ await docker.containerLogs(state.containerName, options);
3400
+ }
3401
+ return {
3402
+ kind: "docker",
3403
+ install,
3404
+ start,
3405
+ stop,
3406
+ status,
3407
+ logs,
3408
+ url: () => url,
3409
+ hostAlias: () => state.hostAlias
3410
+ };
3411
+ }
3412
+ //#endregion
3413
+ //#region src/runtime/runtime.ts
3414
+ function hostRuntimeDeps(instanceName, signal) {
3415
+ const user = userInfo();
3416
+ const runner = processRunner(signal);
3417
+ return {
3418
+ docker: dockerTool(runner),
3419
+ java: javaTool(runner),
3420
+ daemon: daemonTool(runner),
3421
+ download: fileDownloader(signal),
3422
+ dataDir: instanceDataDir(instanceName),
3423
+ instanceDir: instanceDir(instanceName),
3424
+ jarsDir: jarsDir(),
3425
+ platform: process.platform,
3426
+ hostUser: {
3427
+ uid: user.uid,
3428
+ gid: user.gid
3429
+ }
3430
+ };
3431
+ }
3432
+ function runtimeFromState(state, deps) {
3433
+ switch (state.kind) {
3434
+ case "docker": return dockerRuntime(state, deps);
3435
+ case "jar": return jarRuntime(state, deps);
3436
+ }
3437
+ }
3438
+ //#endregion
3439
+ //#region src/runtime/sample-database.ts
3440
+ const SAMPLE_DATABASE_IMAGE = "metabase/qa-databases:postgres-sample-15";
3441
+ const SAMPLE_DATABASE_USER = "metabase";
3442
+ const SAMPLE_DATABASE_PASSWORD = "metasample123";
3443
+ const SAMPLE_DATABASE_NAME = "sample";
3444
+ const SAMPLE_DATABASE_PORT = 15432;
3445
+ const SAMPLE_DATABASE_READY_TIMEOUT_MS = 12e4;
3446
+ const POSTGRES_PORT = 5432;
3447
+ const CONTAINER_NAME_PREFIX = "rde-sampledb";
3448
+ const READY_POLL_MS = 1e3;
3449
+ const READY_PROBE = [
3450
+ "pg_isready",
3451
+ "-h",
3452
+ LOOPBACK_HOST,
3453
+ "-U",
3454
+ SAMPLE_DATABASE_USER,
3455
+ "-d",
3456
+ SAMPLE_DATABASE_NAME
3457
+ ];
3458
+ function sampleContainerName(instanceName) {
3459
+ return `${CONTAINER_NAME_PREFIX}-${instanceName}`;
3460
+ }
3461
+ function sampleDatabaseUrl(port) {
3462
+ return `postgres://${SAMPLE_DATABASE_USER}:${SAMPLE_DATABASE_PASSWORD}@localhost:${port}/${SAMPLE_DATABASE_NAME}`;
3463
+ }
3464
+ function publishesOnLoopback$1(inspection, port) {
3465
+ return inspection.ports.some((mapping) => mapping.container === POSTGRES_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
3466
+ }
3467
+ function sampleDatabaseContainer(state, docker) {
3468
+ const name = state.containerName;
3469
+ function conflict(reason) {
3470
+ return new ConfigError(`container ${name} already exists ${reason}; remove it with \`docker rm -f ${name}\` and run \`rde init\` again`);
3471
+ }
3472
+ async function install(options) {
3473
+ const existing = await docker.containerInspect(name);
3474
+ if (existing.exists) {
3475
+ if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
3476
+ if (!publishesOnLoopback$1(existing, state.port)) throw conflict(`without port ${state.port} published on ${LOOPBACK_HOST}`);
3477
+ return;
3478
+ }
3479
+ await docker.imagePull(state.image, { onLine: options.onProgress });
3480
+ await docker.containerCreate({
3481
+ name,
3482
+ image: state.image,
3483
+ port: {
3484
+ host: state.port,
3485
+ container: POSTGRES_PORT
3486
+ },
3487
+ mount: null,
3488
+ env: {},
3489
+ addHostGateway: false,
3490
+ hostUser: null
3491
+ });
3492
+ }
3493
+ async function start() {
3494
+ const existing = await docker.containerInspect(name);
3495
+ if (!existing.exists) return "missing";
3496
+ if (existing.running) return "running";
3497
+ await docker.containerStart(name);
3498
+ return "started";
3499
+ }
3500
+ async function stop() {
3501
+ const existing = await docker.containerInspect(name);
3502
+ if (existing.exists && existing.running) await docker.containerStop(name);
3503
+ }
3504
+ async function status() {
3505
+ const existing = await docker.containerInspect(name);
3506
+ return {
3507
+ installed: existing.exists,
3508
+ running: existing.exists && existing.running
3509
+ };
3510
+ }
3511
+ async function waitReady(options) {
3512
+ const deadline = Date.now() + options.timeoutMs;
3513
+ const interval = options.pollMs ?? READY_POLL_MS;
3514
+ for (;;) {
3515
+ if (await docker.containerExecSucceeds(name, READY_PROBE)) return;
3516
+ const existing = await docker.containerInspect(name);
3517
+ if (!existing.exists || !existing.running) throw new ConfigError(`container ${name} stopped before Postgres was ready; \`docker logs ${name}\` shows why`);
3518
+ 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`);
3519
+ try {
3520
+ await setTimeout$1(interval, void 0, { signal: options.signal });
3521
+ } catch {
3522
+ throw new AbortError();
3523
+ }
3524
+ }
3525
+ }
3526
+ return {
3527
+ install,
3528
+ start,
3529
+ stop,
3530
+ status,
3531
+ waitReady
3532
+ };
3533
+ }
3534
+ //#endregion
3535
+ //#region src/tools/agents.ts
3536
+ const AgentId = z.enum([
3537
+ "claude-code",
3538
+ "codex",
3539
+ "cursor",
3540
+ "gemini-cli",
3541
+ "opencode",
3542
+ "windsurf"
3543
+ ]);
3544
+ const AGENTS = [
3545
+ {
3546
+ id: "claude-code",
3547
+ displayName: "Claude Code",
3548
+ skillsCliName: "claude-code",
3549
+ command: "claude",
3550
+ launch: "claude",
3551
+ configDir: (home) => join(home, ".claude")
3552
+ },
3553
+ {
3554
+ id: "codex",
3555
+ displayName: "Codex",
3556
+ skillsCliName: "codex",
3557
+ command: "codex",
3558
+ launch: "codex",
3559
+ configDir: (home) => join(home, ".codex")
3560
+ },
3561
+ {
3562
+ id: "cursor",
3563
+ displayName: "Cursor",
3564
+ skillsCliName: "cursor",
3565
+ command: "cursor",
3566
+ launch: "cursor .",
3567
+ configDir: (home) => join(home, ".cursor")
3568
+ },
3569
+ {
3570
+ id: "gemini-cli",
3571
+ displayName: "Gemini CLI",
3572
+ skillsCliName: "gemini-cli",
3573
+ command: "gemini",
3574
+ launch: "gemini",
3575
+ configDir: (home) => join(home, ".gemini")
3576
+ },
3577
+ {
3578
+ id: "opencode",
3579
+ displayName: "OpenCode",
3580
+ skillsCliName: "opencode",
3581
+ command: "opencode",
3582
+ launch: "opencode",
3583
+ configDir: (_home, configHome) => join(configHome, "opencode")
3584
+ },
3585
+ {
3586
+ id: "windsurf",
3587
+ displayName: "Windsurf",
3588
+ skillsCliName: "windsurf",
3589
+ command: null,
3590
+ launch: "windsurf .",
3591
+ configDir: (home) => join(home, ".codeium", "windsurf")
3592
+ }
3593
+ ];
3594
+ async function directoryExists$1(path) {
3595
+ try {
3596
+ return (await promises.stat(path)).isDirectory();
3597
+ } catch {
3598
+ return false;
3599
+ }
3600
+ }
3601
+ function hostAgentProbe() {
3602
+ const home = homedir();
3603
+ return {
3604
+ commandExists: async (command) => await whichCommand(command) !== null,
3605
+ directoryExists: directoryExists$1,
3606
+ home,
3607
+ configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
3608
+ };
3609
+ }
3610
+ async function isDetected(agent, probe) {
3611
+ if (agent.command !== null && await probe.commandExists(agent.command)) return true;
3612
+ return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
3613
+ }
3614
+ async function detectAgents(probe) {
3615
+ return Promise.all(AGENTS.map(async (agent) => ({
3616
+ id: agent.id,
3617
+ displayName: agent.displayName,
3618
+ skillsCliName: agent.skillsCliName,
3619
+ launch: agent.launch,
3620
+ detected: await isDetected(agent, probe)
3621
+ })));
2908
3622
  }
2909
3623
  //#endregion
2910
3624
  //#region src/tools/mb.ts
@@ -3282,11 +3996,10 @@ function skillsCli(runner) {
3282
3996
  }
3283
3997
  //#endregion
3284
3998
  //#region src/steps/agents-skills.ts
3285
- const STEP_ID$7 = "agents-skills";
3286
- const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
3287
- const DETECTED_HINT = "detected";
3999
+ const STEP_ID$8 = "agents-skills";
3288
4000
  const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
3289
- function hostDeps$9(ctx) {
4001
+ const DETECT_AGENTS = { agents: null };
4002
+ function hostDeps$11(ctx) {
3290
4003
  return {
3291
4004
  runner: processRunner(ctx.signal),
3292
4005
  probe: hostAgentProbe()
@@ -3295,6 +4008,9 @@ function hostDeps$9(ctx) {
3295
4008
  function detectedOnly(agents) {
3296
4009
  return agents.filter((agent) => agent.detected);
3297
4010
  }
4011
+ function displayNames(agents) {
4012
+ return agents.map((agent) => agent.displayName).join(", ");
4013
+ }
3298
4014
  function skillCoversAgent(entry, agent) {
3299
4015
  return entry.agents.includes(agent.displayName);
3300
4016
  }
@@ -3304,45 +4020,53 @@ function coversAny(entry, agents) {
3304
4020
  async function rdeSkillEntries(runner) {
3305
4021
  return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
3306
4022
  }
3307
- function agentChoice(agent) {
4023
+ function installedLines(entries) {
4024
+ return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
4025
+ }
4026
+ const SOURCE_NOTES = {
4027
+ override: "from --agents",
4028
+ detected: "detected on this machine"
4029
+ };
4030
+ function selectAgents(all, options) {
4031
+ if (options.agents === null) return {
4032
+ agents: detectedOnly(all),
4033
+ source: "detected"
4034
+ };
4035
+ const chosen = options.agents;
3308
4036
  return {
3309
- value: agent.id,
3310
- label: agent.displayName,
3311
- ...agent.detected && { hint: DETECTED_HINT }
4037
+ agents: all.filter((agent) => chosen.includes(agent.id)),
4038
+ source: "override"
3312
4039
  };
3313
4040
  }
3314
- function installedLines(entries) {
3315
- return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
4041
+ function skipLine(all, source) {
4042
+ switch (source) {
4043
+ case "override": return `Skipping the rde skill; run \`rde init --only ${STEP_ID$8}\` to install it later`;
4044
+ case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$8} --agents <name>\` once one is installed`;
4045
+ }
3316
4046
  }
3317
- function agentsSkillsStep(deps = hostDeps$9) {
4047
+ function agentsSkillsStep(options, deps = hostDeps$11) {
3318
4048
  return {
3319
- id: STEP_ID$7,
4049
+ id: STEP_ID$8,
3320
4050
  title: "Install the rde skill for your coding agents",
3321
4051
  async detect(ctx) {
3322
4052
  const resolved = deps(ctx);
3323
4053
  const detected = detectedOnly(await detectAgents(resolved.probe));
3324
- if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
4054
+ if (detected.length === 0) return ctx.state.steps[STEP_ID$8]?.status === "skipped" ? "not-applicable" : "todo";
3325
4055
  return (await rdeSkillEntries(resolved.runner)).some((entry) => coversAny(entry, detected)) ? "done" : "todo";
3326
4056
  },
3327
4057
  async run(ctx) {
3328
4058
  const resolved = deps(ctx);
3329
- const agents = await detectAgents(resolved.probe);
3330
- const chosen = await ctx.prompts.multiselect({
3331
- key: AGENTS_SKILLS_KEYS.agents,
3332
- message: "Install the rde skill for which coding agents?",
3333
- choices: agents.map(agentChoice),
3334
- initialValues: detectedOnly(agents).map((agent) => agent.id),
3335
- required: false
3336
- });
3337
- if (chosen.length === 0) {
3338
- ctx.ui.info(`Skipping the rde skill; run \`rde init --only ${STEP_ID$7}\` to install it later`);
4059
+ const all = await detectAgents(resolved.probe);
4060
+ const selection = selectAgents(all, options);
4061
+ if (selection.agents.length === 0) {
4062
+ ctx.ui.info(skipLine(all, selection.source));
3339
4063
  return "skipped";
3340
4064
  }
3341
- const selected = agents.filter((agent) => chosen.includes(agent.id));
4065
+ ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`);
3342
4066
  await skillsCli(resolved.runner).add({
3343
4067
  source: RDE_SKILL_SOURCE,
3344
4068
  skill: "rde",
3345
- agents: selected.map((agent) => agent.skillsCliName)
4069
+ agents: selection.agents.map((agent) => agent.skillsCliName)
3346
4070
  });
3347
4071
  const entries = await rdeSkillEntries(resolved.runner);
3348
4072
  if (entries.length === 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show the rde skill`);
@@ -3354,30 +4078,19 @@ function agentsSkillsStep(deps = hostDeps$9) {
3354
4078
  }
3355
4079
  //#endregion
3356
4080
  //#region src/steps/install-mb.ts
3357
- const STEP_ID$6 = "install-mb";
3358
- const INSTALL_MB_KEYS = {
3359
- packageManager: `${STEP_ID$6}.package-manager`,
3360
- confirm: `${STEP_ID$6}.confirm`
3361
- };
4081
+ const STEP_ID$7 = "install-mb";
3362
4082
  const MIN_MB_VERSION = "0.3.0";
3363
4083
  const MB_RANGE = `>=${MIN_MB_VERSION}`;
3364
- const PACKAGE_MANAGERS = [
3365
- "npm",
3366
- "pnpm",
3367
- "yarn",
3368
- "bun"
3369
- ];
3370
4084
  function installedWith(scriptPath) {
3371
4085
  return scriptPath === void 0 ? "npm" : packageManagerFor(scriptPath);
3372
4086
  }
3373
4087
  function rdeInstalledWith() {
3374
4088
  return installedWith(process.argv[1]);
3375
4089
  }
3376
- function hostDeps$8(ctx) {
4090
+ function hostDeps$10(ctx) {
3377
4091
  return {
3378
4092
  runner: processRunner(ctx.signal),
3379
- installedWith: rdeInstalledWith(),
3380
- commandExists: async (name) => await whichCommand(name) !== null
4093
+ installedWith: rdeInstalledWith()
3381
4094
  };
3382
4095
  }
3383
4096
  async function inspectMb(runner) {
@@ -3391,35 +4104,9 @@ async function inspectMb(runner) {
3391
4104
  satisfies: semver.satisfies(version, MB_RANGE, { includePrerelease: true })
3392
4105
  };
3393
4106
  }
3394
- async function candidateManagers(deps) {
3395
- const others = [];
3396
- for (const manager of PACKAGE_MANAGERS) if (manager !== deps.installedWith && await deps.commandExists(manager)) others.push(manager);
3397
- return [deps.installedWith, ...others];
3398
- }
3399
- function managerChoice(manager, preferred) {
3400
- return {
3401
- value: manager,
3402
- label: manager,
3403
- ...manager === preferred && { hint: "installed rde" }
3404
- };
3405
- }
3406
4107
  function displayCommand(install) {
3407
4108
  return [install.command, ...install.args].join(" ");
3408
4109
  }
3409
- async function chooseManager(ctx, deps, verb) {
3410
- const managers = await candidateManagers(deps);
3411
- if (managers.length > 1) return ctx.prompts.select({
3412
- key: INSTALL_MB_KEYS.packageManager,
3413
- message: `${verb} ${MB_PACKAGE} globally with which package manager?`,
3414
- choices: managers.map((manager) => managerChoice(manager, deps.installedWith))
3415
- });
3416
- if (!await ctx.prompts.confirm({
3417
- key: INSTALL_MB_KEYS.confirm,
3418
- message: `${verb} @metabase/cli globally with ${deps.installedWith}?`,
3419
- initialValue: true
3420
- })) throw new ConfigError(`mb is required; install it with \`${displayCommand(mbInstallCommand(deps.installedWith))}\` and run rde init again`);
3421
- return deps.installedWith;
3422
- }
3423
4110
  function stillWrong(install, after) {
3424
4111
  const found = after.installed ? `mb ${after.version} is still what is on PATH (${MIN_MB_VERSION} or newer is required)` : "mb is not on PATH after the install";
3425
4112
  return new ExternalToolError({
@@ -3429,9 +4116,9 @@ function stillWrong(install, after) {
3429
4116
  stderrTail: `${found}; open a new terminal and run rde init again`
3430
4117
  });
3431
4118
  }
3432
- function installMbStep(deps = hostDeps$8) {
4119
+ function installMbStep(deps = hostDeps$10) {
3433
4120
  return {
3434
- id: STEP_ID$6,
4121
+ id: STEP_ID$7,
3435
4122
  title: "Install the Metabase CLI",
3436
4123
  async detect(ctx) {
3437
4124
  const found = await inspectMb(deps(ctx).runner);
@@ -3445,7 +4132,7 @@ function installMbStep(deps = hostDeps$8) {
3445
4132
  return "completed";
3446
4133
  }
3447
4134
  if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
3448
- const install = mbInstallCommand(await chooseManager(ctx, resolved, before.installed ? "Upgrade" : "Install"));
4135
+ const install = mbInstallCommand(resolved.installedWith);
3449
4136
  ctx.ui.info(`Running ${displayCommand(install)}`);
3450
4137
  const exitCode = await resolved.runner.stream(install.command, install.args);
3451
4138
  if (exitCode !== 0) throw new ExternalToolError({
@@ -3606,25 +4293,35 @@ const NODE_RANGE = `>=${MIN_NODE_VERSION}`;
3606
4293
  const NODE_HINT = `Install Node ${MIN_NODE_VERSION} or newer from https://nodejs.org or with your version manager`;
3607
4294
  const MARK_OK = "✓";
3608
4295
  const MARK_MISSING = "✗";
4296
+ const MARK_OPTIONAL = "–";
4297
+ const DOCKER_OPTIONAL = `Metabase can run as a jar with Java instead, without the sample database`;
4298
+ const JAVA_OPTIONAL = `Metabase can run in Docker instead`;
3609
4299
  const PLATFORMS = {
3610
4300
  darwin: {
3611
4301
  name: "macOS",
3612
4302
  gitHint: "Install the Xcode Command Line Tools: xcode-select --install",
3613
- dockerHint: "Install Docker Desktop (https://www.docker.com/products/docker-desktop/), OrbStack, or colima, and start it"
4303
+ dockerHint: "Install Docker Desktop (https://www.docker.com/products/docker-desktop/), OrbStack, or colima, and start it",
4304
+ javaHint: `Install Java 21 or newer: brew install --cask temurin@21, or from https://adoptium.net`
3614
4305
  },
3615
4306
  linux: {
3616
4307
  name: "Linux",
3617
4308
  gitHint: "Install the git package with your distribution's package manager",
3618
- dockerHint: "Install the docker package, start the daemon, and add yourself to the docker group: sudo usermod -aG docker $USER (then log in again)"
4309
+ dockerHint: "Install the docker package, start the daemon, and add yourself to the docker group: sudo usermod -aG docker $USER (then log in again)",
4310
+ javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
3619
4311
  }
3620
4312
  };
3621
- function hostDeps$7(ctx) {
4313
+ function hostDeps$9(ctx) {
4314
+ const runner = processRunner(ctx.signal);
3622
4315
  return {
3623
4316
  platform: process.platform,
3624
4317
  nodeVersion: process.version,
3625
- runner: processRunner(ctx.signal)
4318
+ runner,
4319
+ java: javaTool(runner)
3626
4320
  };
3627
4321
  }
4322
+ function blocks(check) {
4323
+ return !check.ok && check.optional === null;
4324
+ }
3628
4325
  function isSupported(platform) {
3629
4326
  return platform === "darwin" || platform === "linux";
3630
4327
  }
@@ -3641,7 +4338,8 @@ function nodeCheck(nodeVersion) {
3641
4338
  id: "node",
3642
4339
  ok: false,
3643
4340
  label: `Node ${nodeVersion} (${MIN_NODE_VERSION} or newer is required)`,
3644
- hint: NODE_HINT
4341
+ hint: NODE_HINT,
4342
+ optional: null
3645
4343
  };
3646
4344
  }
3647
4345
  async function gitCheck(runner, facts) {
@@ -3656,7 +4354,8 @@ async function gitCheck(runner, facts) {
3656
4354
  id: "git",
3657
4355
  ok: false,
3658
4356
  label: "git is not installed",
3659
- hint: facts.gitHint
4357
+ hint: facts.gitHint,
4358
+ optional: null
3660
4359
  };
3661
4360
  throw error;
3662
4361
  }
@@ -3672,8 +4371,35 @@ async function dockerCheck(runner, facts) {
3672
4371
  id: "docker",
3673
4372
  ok: false,
3674
4373
  label: `Docker: ${docker.reason}`,
3675
- hint: facts.dockerHint
4374
+ hint: facts.dockerHint,
4375
+ optional: null
4376
+ };
4377
+ }
4378
+ async function javaCheck(tool, facts) {
4379
+ const java = await tool.find();
4380
+ if (java.found && java.major >= 21) return {
4381
+ id: "java",
4382
+ ok: true,
4383
+ label: `Java ${java.version}`
3676
4384
  };
4385
+ return {
4386
+ id: "java",
4387
+ ok: false,
4388
+ label: java.found ? `Java ${java.version} (21 or newer is required)` : `Java: ${java.reason}`,
4389
+ hint: facts.javaHint,
4390
+ optional: null
4391
+ };
4392
+ }
4393
+ function coveredBy(check, other, optional) {
4394
+ return !check.ok && other.ok ? {
4395
+ ...check,
4396
+ optional
4397
+ } : check;
4398
+ }
4399
+ async function runtimeChecks(deps, facts) {
4400
+ const docker = await dockerCheck(deps.runner, facts);
4401
+ const java = await javaCheck(deps.java, facts);
4402
+ return [coveredBy(docker, java, DOCKER_OPTIONAL), coveredBy(java, docker, JAVA_OPTIONAL)];
3677
4403
  }
3678
4404
  async function runChecks(deps, platform) {
3679
4405
  const facts = PLATFORMS[platform];
@@ -3685,7 +4411,7 @@ async function runChecks(deps, platform) {
3685
4411
  },
3686
4412
  nodeCheck(deps.nodeVersion),
3687
4413
  await gitCheck(deps.runner, facts),
3688
- await dockerCheck(deps.runner, facts)
4414
+ ...await runtimeChecks(deps, facts)
3689
4415
  ];
3690
4416
  }
3691
4417
  async function machineChecks(deps) {
@@ -3693,46 +4419,54 @@ async function machineChecks(deps) {
3693
4419
  id: "platform",
3694
4420
  ok: false,
3695
4421
  label: `${deps.platform} is not supported`,
3696
- hint: "rde runs on macOS and Linux"
4422
+ hint: "rde runs on macOS and Linux",
4423
+ optional: null
3697
4424
  }];
3698
4425
  return runChecks(deps, deps.platform);
3699
4426
  }
4427
+ function checkLine(check) {
4428
+ if (check.ok) return `${MARK_OK} ${check.label}`;
4429
+ if (check.optional !== null) return `${MARK_OPTIONAL} ${check.label}; ${check.optional}\n ${check.hint}`;
4430
+ return `${MARK_MISSING} ${check.label}\n ${check.hint}`;
4431
+ }
3700
4432
  function checkLines(checks) {
3701
- return checks.map((check) => check.ok ? `${MARK_OK} ${check.label}` : `${MARK_MISSING} ${check.label}\n ${check.hint}`);
4433
+ return checks.map(checkLine);
3702
4434
  }
3703
- function preflightStep(deps = hostDeps$7) {
4435
+ function preflightStep(deps = hostDeps$9) {
3704
4436
  return {
3705
4437
  id: "preflight",
3706
4438
  title: "Check this machine",
3707
4439
  async detect(ctx) {
3708
- return (await machineChecks(deps(ctx))).every((check) => check.ok) ? "done" : "todo";
4440
+ return (await machineChecks(deps(ctx))).some(blocks) ? "todo" : "done";
3709
4441
  },
3710
4442
  async run(ctx) {
3711
4443
  const resolved = deps(ctx);
3712
4444
  if (!isSupported(resolved.platform)) throw unsupportedPlatform(resolved.platform);
3713
4445
  const checks = await runChecks(resolved, resolved.platform);
3714
4446
  ctx.ui.note(checkLines(checks).join("\n"), "This machine");
3715
- if (checks.some((check) => !check.ok)) throw new ConfigError("Fix the items above and run rde init again");
4447
+ if (checks.some(blocks)) throw new ConfigError("Fix the items above and run rde init again");
3716
4448
  return "completed";
3717
4449
  }
3718
4450
  };
3719
4451
  }
3720
4452
  //#endregion
3721
4453
  //#region src/steps/doctor.ts
3722
- function hostDoctorDeps(signal) {
4454
+ function hostDoctorDeps(instanceName, signal) {
3723
4455
  const runner = processRunner(signal);
4456
+ const runtime = hostRuntimeDeps(instanceName, signal);
3724
4457
  return {
3725
4458
  platform: process.platform,
3726
4459
  nodeVersion: process.version,
3727
4460
  runner,
4461
+ java: runtime.java,
3728
4462
  installedWith: rdeInstalledWith(),
3729
- docker: dockerTool(runner),
4463
+ runtime,
3730
4464
  ports: hostPortProbe,
3731
4465
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
3732
4466
  probe: hostAgentProbe()
3733
4467
  };
3734
4468
  }
3735
- const NOT_FOUND_STATUS$1 = 404;
4469
+ const NOT_FOUND_STATUS$2 = 404;
3736
4470
  const NO_INSTANCE = "no instance yet; run `rde init`";
3737
4471
  const NOT_HEALTHY = "Metabase is not healthy";
3738
4472
  function ok(id, label, detail) {
@@ -3763,12 +4497,23 @@ function failed(id, label, failure) {
3763
4497
  ...failure
3764
4498
  };
3765
4499
  }
3766
- async function machineRows(deps) {
3767
- return (await machineChecks(deps)).map((check) => check.ok ? ok(check.id, check.id, check.label) : failed(check.id, check.id, {
4500
+ const PROGRAM_FOR_RUNTIME = {
4501
+ docker: "docker",
4502
+ jar: "java"
4503
+ };
4504
+ function machineRow(check, needed) {
4505
+ if (check.ok) return ok(check.id, check.id, check.label);
4506
+ if (!blocks(check) && check.id !== needed) return skipped(check.id, check.id, `${check.label}; ${check.optional}`);
4507
+ return failed(check.id, check.id, {
3768
4508
  detail: check.label,
3769
4509
  hint: check.hint,
3770
4510
  fix: "preflight"
3771
- }));
4511
+ });
4512
+ }
4513
+ async function machineRows(deps, runtime) {
4514
+ const checks = await machineChecks(deps);
4515
+ const needed = runtime === null ? null : PROGRAM_FOR_RUNTIME[runtime.kind];
4516
+ return checks.map((check) => machineRow(check, needed));
3772
4517
  }
3773
4518
  function installHint(installedWith) {
3774
4519
  const install = mbInstallCommand(installedWith);
@@ -3791,20 +4536,27 @@ async function mbRow(deps) {
3791
4536
  function publishesOnLoopback(container, port) {
3792
4537
  return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
3793
4538
  }
4539
+ const CONTAINER = "the container";
3794
4540
  async function containerRow(runtime, deps, dockerOk) {
3795
4541
  const label = "container";
3796
4542
  if (!dockerOk) return {
3797
4543
  row: skipped("container", label, "Docker is not available"),
3798
- present: null
4544
+ running: false,
4545
+ holder: CONTAINER
3799
4546
  };
3800
- const inspection = await deps.docker.containerInspect(runtime.containerName);
4547
+ const inspection = await deps.runtime.docker.containerInspect(runtime.containerName);
3801
4548
  if (!inspection.exists) return {
3802
4549
  row: failed("container", label, {
3803
4550
  detail: `${runtime.containerName} does not exist`,
3804
4551
  hint: "`rde init` recreates it from the saved choices",
3805
4552
  fix: "runtime"
3806
4553
  }),
3807
- present: null
4554
+ running: false,
4555
+ holder: CONTAINER
4556
+ };
4557
+ const found = {
4558
+ running: inspection.running,
4559
+ holder: CONTAINER
3808
4560
  };
3809
4561
  if (inspection.image !== runtime.image) return {
3810
4562
  row: failed("container", label, {
@@ -3812,7 +4564,7 @@ async function containerRow(runtime, deps, dockerOk) {
3812
4564
  hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
3813
4565
  fix: null
3814
4566
  }),
3815
- present: inspection
4567
+ ...found
3816
4568
  };
3817
4569
  if (!publishesOnLoopback(inspection, runtime.port)) return {
3818
4570
  row: failed("container", label, {
@@ -3820,20 +4572,51 @@ async function containerRow(runtime, deps, dockerOk) {
3820
4572
  hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
3821
4573
  fix: null
3822
4574
  }),
3823
- present: inspection
4575
+ ...found
4576
+ };
4577
+ const state = inspection.running ? "running" : "stopped";
4578
+ return {
4579
+ row: ok("container", label, `${runtime.containerName} ${state} (${runtime.image})`),
4580
+ ...found
4581
+ };
4582
+ }
4583
+ const JAR_PROCESS = "the Metabase process";
4584
+ async function processRow(runtime, deps) {
4585
+ const label = "process";
4586
+ const inspection = await inspectJar(runtime, deps.runtime);
4587
+ const jar = describeJar(runtime.source);
4588
+ if (!inspection.jarExists) {
4589
+ const hint = runtime.source.kind === "release" ? "`rde init` downloads it again" : "put the jar back, or run `rde init` again with another one";
4590
+ return {
4591
+ row: failed("process", label, {
4592
+ detail: `${inspection.jarPath} does not exist`,
4593
+ hint,
4594
+ fix: "runtime"
4595
+ }),
4596
+ running: false,
4597
+ holder: JAR_PROCESS
4598
+ };
4599
+ }
4600
+ const found = {
4601
+ running: inspection.pid !== null,
4602
+ holder: JAR_PROCESS
3824
4603
  };
3825
- const state = inspection.running ? "running" : "stopped";
3826
4604
  return {
3827
- row: ok("container", label, `${runtime.containerName} ${state} (${runtime.image})`),
3828
- present: inspection
4605
+ row: ok("process", label, `${jar} ${inspection.pid === null ? "stopped" : `running as pid ${inspection.pid}`}`),
4606
+ ...found
3829
4607
  };
3830
4608
  }
3831
- async function portRow(port, container, ports) {
4609
+ function runtimeRow(runtime, deps, dockerOk) {
4610
+ switch (runtime.kind) {
4611
+ case "docker": return containerRow(runtime, deps, dockerOk);
4612
+ case "jar": return processRow(runtime, deps);
4613
+ }
4614
+ }
4615
+ async function portRow(port, finding, ports) {
3832
4616
  const free = await ports.isFree(port);
3833
- const running = container?.running === true;
3834
- if (running && !free) return ok("port", "port", `${port}, published by the container`);
3835
- if (running) return failed("port", "port", {
3836
- detail: `${port} is not answering although the container runs`,
4617
+ if (finding.running && !free) return ok("port", "port", `${port}, held by ${finding.holder}`);
4618
+ if (finding.running) return failed("port", "port", {
4619
+ detail: `${port} is not answering although ${finding.holder} runs`,
3837
4620
  hint: "`rde logs` shows what Metabase is doing",
3838
4621
  fix: null
3839
4622
  });
@@ -3879,14 +4662,26 @@ async function healthRow(url, deps) {
3879
4662
  throw error;
3880
4663
  }
3881
4664
  }
3882
- async function adminRow(state, url, deps) {
3883
- if (!(await deps.client(url, NO_AUTH).session.properties())["has-user-setup"]) return failed("admin", "admin user", {
4665
+ function adminRow(state, properties) {
4666
+ if (!properties["has-user-setup"]) return failed("admin", "admin user", {
3884
4667
  detail: "Metabase has no admin user yet",
3885
4668
  hint: "`rde init --only admin-user`",
3886
4669
  fix: "admin-user"
3887
4670
  });
3888
4671
  return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
3889
4672
  }
4673
+ const VERSION_STEP = "start-metabase";
4674
+ function versionRow(state, properties) {
4675
+ const label = "Metabase version";
4676
+ const running = properties.version.tag;
4677
+ if (state.version === null) return ok("version", label, `${running}; \`rde start\` records it in the state file`);
4678
+ if (state.version !== running) return failed("version", label, {
4679
+ detail: `running ${running}, the state file says ${state.version}`,
4680
+ hint: "`rde start` records the running version",
4681
+ fix: VERSION_STEP
4682
+ });
4683
+ return ok("version", label, running);
4684
+ }
3890
4685
  function apiKeyRow(state) {
3891
4686
  if (state.apiKey === null) return failed("api-key", "API key", {
3892
4687
  detail: "none in the state file",
@@ -3913,8 +4708,8 @@ async function mbProfileRow(state, deps, mbInstalled, healthy) {
3913
4708
  const user = entry.user === null ? "" : ` as ${entry.user.name}`;
3914
4709
  return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
3915
4710
  }
3916
- function isNoToken(error) {
3917
- return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1;
4711
+ function isNotFound$2(error) {
4712
+ return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
3918
4713
  }
3919
4714
  async function licenseRow(client) {
3920
4715
  const label = "license";
@@ -3927,7 +4722,7 @@ async function licenseRow(client) {
3927
4722
  });
3928
4723
  return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
3929
4724
  } catch (error) {
3930
- if (isNoToken(error)) return skipped("license", label, "not activated; `rde init --from license` adds one");
4725
+ if (isNotFound$2(error)) return skipped("license", label, "not activated; `rde init --from license` adds one");
3931
4726
  throw error;
3932
4727
  }
3933
4728
  }
@@ -3945,6 +4740,46 @@ async function databasesRow(state, client) {
3945
4740
  const names = state.databases.map((record) => record.name).join(", ");
3946
4741
  return ok("databases", label, `${state.databases.length} connected: ${names}`);
3947
4742
  }
4743
+ const SAMPLE_DATABASE_STEP = "sample-database";
4744
+ const SAMPLE_LABEL = "sample database";
4745
+ async function sampleDatabaseRow(record, deps, dockerOk, client) {
4746
+ const id = "sample-database";
4747
+ if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
4748
+ const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
4749
+ if (!status.installed) return failed(id, SAMPLE_LABEL, {
4750
+ detail: `${record.containerName} does not exist`,
4751
+ hint: "`rde init` recreates it",
4752
+ fix: SAMPLE_DATABASE_STEP
4753
+ });
4754
+ if (!status.running) return failed(id, SAMPLE_LABEL, {
4755
+ detail: `${record.containerName} is stopped`,
4756
+ hint: "`rde start`",
4757
+ fix: "start-metabase"
4758
+ });
4759
+ if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
4760
+ detail: `${record.containerName} runs but port ${record.port} is not answering`,
4761
+ hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
4762
+ fix: null
4763
+ });
4764
+ const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
4765
+ if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
4766
+ detail: `${where}; not connected to Metabase yet`,
4767
+ hint: "`rde init` connects it",
4768
+ fix: SAMPLE_DATABASE_STEP
4769
+ });
4770
+ if (client === null) return skipped(id, SAMPLE_LABEL, `${where}; ${NOT_HEALTHY}, so the connection is unchecked`);
4771
+ try {
4772
+ const database = await client.database.get(record.databaseId);
4773
+ return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
4774
+ } catch (error) {
4775
+ if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
4776
+ detail: `${where}; database ${record.databaseId} is gone from Metabase`,
4777
+ hint: "`rde init` connects it again",
4778
+ fix: SAMPLE_DATABASE_STEP
4779
+ });
4780
+ throw error;
4781
+ }
4782
+ }
3948
4783
  function isMbFailure$1(error) {
3949
4784
  return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
3950
4785
  }
@@ -3993,66 +4828,56 @@ async function skillRows(state, deps) {
3993
4828
  return ok(skillRowId(agent), agentLabel, entry.path);
3994
4829
  });
3995
4830
  }
3996
- function dockerRuntimeOf(state) {
3997
- const configured = configuredRuntime(state);
3998
- if (configured === null || configured.runtime.kind !== "docker") return null;
3999
- return {
4000
- runtime: configured.runtime,
4001
- url: configured.url
4002
- };
4003
- }
4004
- function noRuntimeRows(state) {
4005
- const detail = state.runtime?.kind === "jar" ? "the jar runtime is not available yet" : NO_INSTANCE;
4831
+ function noRuntimeRows() {
4006
4832
  return [
4007
- "container",
4833
+ "runtime",
4008
4834
  "port",
4009
4835
  "health"
4010
- ].map((id) => skipped(id, id, detail));
4836
+ ].map((id) => skipped(id, id, NO_INSTANCE));
4011
4837
  }
4012
4838
  async function instanceRows(state, deps, dockerOk, mbInstalled) {
4013
- const docker = dockerRuntimeOf(state);
4014
- if (docker === null) return [
4015
- ...noRuntimeRows(state),
4839
+ const configured = configuredRuntime(state);
4840
+ if (configured === null) return [
4841
+ ...noRuntimeRows(),
4842
+ skipped("version", "Metabase version", NO_INSTANCE),
4016
4843
  skipped("admin", "admin user", NO_INSTANCE),
4017
4844
  apiKeyRow(state),
4018
4845
  skipped("mb-profile", "mb profile", NO_INSTANCE),
4019
4846
  skipped("license", "license", NO_INSTANCE),
4020
4847
  skipped("databases", "databases", NO_INSTANCE),
4848
+ skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE),
4021
4849
  skipped("git-sync", "remote sync", NO_INSTANCE)
4022
4850
  ];
4023
- const container = await containerRow(docker.runtime, deps, dockerOk);
4024
- const { row: health, healthy } = await healthRow(docker.url, deps);
4851
+ const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
4852
+ const { row: health, healthy } = await healthRow(configured.url, deps);
4853
+ const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
4025
4854
  const rows = [
4026
- container.row,
4027
- await portRow(docker.runtime.port, container.present, deps.ports),
4855
+ runtime.row,
4856
+ await portRow(configured.runtime.port, runtime, deps.ports),
4028
4857
  health,
4029
- healthy ? await adminRow(state, docker.url, deps) : skipped("admin", "admin user", NOT_HEALTHY),
4858
+ properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
4859
+ properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
4030
4860
  apiKeyRow(state),
4031
4861
  await mbProfileRow(state, deps, mbInstalled, healthy)
4032
4862
  ];
4033
- if (!healthy || state.apiKey === null) {
4863
+ const client = healthy && state.apiKey !== null ? deps.client(configured.url, {
4864
+ kind: "api-key",
4865
+ key: state.apiKey
4866
+ }) : null;
4867
+ if (client === null) {
4034
4868
  const reason = healthy ? "no API key" : NOT_HEALTHY;
4035
4869
  rows.push(skipped("license", "license", reason), skipped("databases", "databases", reason));
4036
- } else {
4037
- const client = deps.client(docker.url, {
4038
- kind: "api-key",
4039
- key: state.apiKey
4040
- });
4041
- rows.push(await licenseRow(client), await databasesRow(state, client));
4042
- }
4870
+ } else rows.push(await licenseRow(client), await databasesRow(state, client));
4871
+ rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, client));
4043
4872
  rows.push(await gitSyncRow(state, deps));
4044
4873
  return rows;
4045
4874
  }
4046
4875
  async function collectDoctor(instance, state, deps) {
4047
- const machine = await machineRows(deps);
4876
+ const machine = await machineRows(deps, state?.runtime ?? null);
4048
4877
  const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
4049
4878
  const mb = await mbRow(deps);
4050
4879
  const checks = [...machine, mb];
4051
- if (state === null) checks.push(...[
4052
- "container",
4053
- "port",
4054
- "health"
4055
- ].map((id) => skipped(id, id, NO_INSTANCE)), skipped("admin", "admin user", NO_INSTANCE), skipped("api-key", "API key", NO_INSTANCE), skipped("mb-profile", "mb profile", NO_INSTANCE), skipped("license", "license", NO_INSTANCE), skipped("databases", "databases", NO_INSTANCE), skipped("git-sync", "remote sync", NO_INSTANCE));
4880
+ if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE), skipped("admin", "admin user", NO_INSTANCE), skipped("api-key", "API key", NO_INSTANCE), skipped("mb-profile", "mb profile", NO_INSTANCE), skipped("license", "license", NO_INSTANCE), skipped("databases", "databases", NO_INSTANCE), skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE), skipped("git-sync", "remote sync", NO_INSTANCE));
4056
4881
  else checks.push(...await instanceRows(state, deps, dockerOk, mb.status === "ok"));
4057
4882
  checks.push(...await skillRows(state, deps));
4058
4883
  return {
@@ -4109,13 +4934,12 @@ async function fixDoctor(report, steps, ctx, options) {
4109
4934
  }
4110
4935
  //#endregion
4111
4936
  //#region src/steps/admin-user.ts
4112
- const STEP_ID$5 = "admin-user";
4937
+ const STEP_ID$6 = "admin-user";
4113
4938
  const ADMIN_USER_KEYS = {
4114
- variant: `${STEP_ID$5}.variant`,
4115
- email: `${STEP_ID$5}.email`,
4116
- password: `${STEP_ID$5}.password`,
4117
- firstName: `${STEP_ID$5}.first-name`,
4118
- lastName: `${STEP_ID$5}.last-name`
4939
+ email: `${STEP_ID$6}.email`,
4940
+ password: `${STEP_ID$6}.password`,
4941
+ firstName: `${STEP_ID$6}.first-name`,
4942
+ lastName: `${STEP_ID$6}.last-name`
4119
4943
  };
4120
4944
  const DEFAULT_ADMIN_EMAIL = "admin@metabase.com";
4121
4945
  const DEFAULT_ADMIN_PASSWORD = "metabase123";
@@ -4123,16 +4947,8 @@ const DEFAULT_SITE_NAME = "rde";
4123
4947
  const MIN_PASSWORD_LENGTH = 6;
4124
4948
  const DIGIT = /\d/;
4125
4949
  const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4126
- const CREDENTIALS_CHOICES = [{
4127
- value: "default",
4128
- label: "Use default credentials",
4129
- hint: `${DEFAULT_ADMIN_EMAIL} / ${DEFAULT_ADMIN_PASSWORD}, saved in ~/.rde`
4130
- }, {
4131
- value: "prompted",
4132
- label: "Enter my own",
4133
- hint: "never saved"
4134
- }];
4135
- function hostDeps$6(ctx) {
4950
+ const DEFAULT_ADMIN = { variant: "default" };
4951
+ function hostDeps$8(ctx) {
4136
4952
  return { client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal) };
4137
4953
  }
4138
4954
  function isNotReady(error) {
@@ -4194,9 +5010,9 @@ function credentialsRecord(kind, user) {
4194
5010
  };
4195
5011
  }
4196
5012
  }
4197
- function adminUserStep(deps = hostDeps$6) {
5013
+ function adminUserStep(options, deps = hostDeps$8) {
4198
5014
  return {
4199
- id: STEP_ID$5,
5015
+ id: STEP_ID$6,
4200
5016
  title: "Create the admin user",
4201
5017
  async detect(ctx) {
4202
5018
  const configured = configuredRuntime(ctx.state);
@@ -4218,12 +5034,7 @@ function adminUserStep(deps = hostDeps$6) {
4218
5034
  }
4219
5035
  const token = properties["setup-token"];
4220
5036
  if (token === null) throw new ConfigError("Metabase has no admin user yet but offers no setup token");
4221
- const kind = await ctx.prompts.select({
4222
- key: ADMIN_USER_KEYS.variant,
4223
- message: "Admin credentials",
4224
- choices: CREDENTIALS_CHOICES
4225
- });
4226
- const user = await chooseUser(ctx, kind);
5037
+ const user = await chooseUser(ctx, options.variant);
4227
5038
  const session = await client.setup.create({
4228
5039
  token,
4229
5040
  user,
@@ -4232,7 +5043,7 @@ function adminUserStep(deps = hostDeps$6) {
4232
5043
  ctx.session.metabaseSession.set(session.id);
4233
5044
  await ctx.saveState({
4234
5045
  ...ctx.state,
4235
- admin: credentialsRecord(kind, user)
5046
+ admin: credentialsRecord(options.variant, user)
4236
5047
  });
4237
5048
  ctx.ui.success(`Admin user ${user.email} created`);
4238
5049
  return "completed";
@@ -4241,14 +5052,14 @@ function adminUserStep(deps = hostDeps$6) {
4241
5052
  }
4242
5053
  //#endregion
4243
5054
  //#region src/steps/api-key.ts
4244
- const STEP_ID$4 = "api-key";
5055
+ const STEP_ID$5 = "api-key";
4245
5056
  const API_KEY_KEYS = {
4246
- email: `${STEP_ID$4}.email`,
4247
- password: `${STEP_ID$4}.password`,
4248
- profile: `${STEP_ID$4}.profile`
5057
+ email: `${STEP_ID$5}.email`,
5058
+ password: `${STEP_ID$5}.password`,
5059
+ profile: `${STEP_ID$5}.profile`
4249
5060
  };
4250
5061
  const ADMINISTRATORS_GROUP_NAME = "Administrators";
4251
- function hostDeps$5(ctx) {
5062
+ function hostDeps$7(ctx) {
4252
5063
  return {
4253
5064
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
4254
5065
  runner: processRunner(ctx.signal)
@@ -4342,9 +5153,9 @@ function usageNote(profile) {
4342
5153
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
4343
5154
  return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
4344
5155
  }
4345
- function apiKeyStep(deps = hostDeps$5) {
5156
+ function apiKeyStep(deps = hostDeps$7) {
4346
5157
  return {
4347
- id: STEP_ID$4,
5158
+ id: STEP_ID$5,
4348
5159
  title: "Connect the Metabase CLI",
4349
5160
  async detect(ctx) {
4350
5161
  if (ctx.state.apiKey === null) return "todo";
@@ -4379,13 +5190,13 @@ function apiKeyStep(deps = hostDeps$5) {
4379
5190
  }
4380
5191
  //#endregion
4381
5192
  //#region src/steps/git-sync.ts
4382
- const STEP_ID$3 = "git-sync";
5193
+ const STEP_ID$4 = "git-sync";
4383
5194
  const GIT_SYNC_KEYS = {
4384
- location: `${STEP_ID$3}.location`,
4385
- path: `${STEP_ID$3}.path`,
4386
- remote: `${STEP_ID$3}.remote`,
4387
- url: `${STEP_ID$3}.url`,
4388
- token: `${STEP_ID$3}.token`
5195
+ location: `${STEP_ID$4}.location`,
5196
+ path: `${STEP_ID$4}.path`,
5197
+ remote: `${STEP_ID$4}.remote`,
5198
+ url: `${STEP_ID$4}.url`,
5199
+ token: `${STEP_ID$4}.token`
4389
5200
  };
4390
5201
  const BARE_REPO_NAME = "content.git";
4391
5202
  const DEFAULT_BRANCH = "main";
@@ -4405,7 +5216,7 @@ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
4405
5216
  "timed-out",
4406
5217
  "conflict"
4407
5218
  ]);
4408
- function hostDeps$4(ctx) {
5219
+ function hostDeps$6(ctx) {
4409
5220
  return {
4410
5221
  runner: processRunner(ctx.signal),
4411
5222
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
@@ -4738,7 +5549,7 @@ async function importExistingContent(run, gitRemote) {
4738
5549
  spinner.stop("Import finished");
4739
5550
  reportImport(run, outcome);
4740
5551
  }
4741
- function usageLines(state, gitRemote) {
5552
+ function usageLines$1(state, gitRemote) {
4742
5553
  const profile = state.mbProfile;
4743
5554
  return [
4744
5555
  `cd ${gitRemote.workdir}`,
@@ -4785,16 +5596,16 @@ async function runGitSync(ctx, deps) {
4785
5596
  const remote = await configureRemote(run, configured.runtime, flow.model);
4786
5597
  await applySettings(run, remote);
4787
5598
  await importExistingContent(run, remote.gitRemote);
4788
- ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
5599
+ ctx.ui.note(usageLines$1(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
4789
5600
  return "completed";
4790
5601
  }
4791
- function gitSyncStep(deps = hostDeps$4) {
5602
+ function gitSyncStep(deps = hostDeps$6) {
4792
5603
  return {
4793
- id: STEP_ID$3,
5604
+ id: STEP_ID$4,
4794
5605
  title: "Connect a git repository",
4795
5606
  async detect(ctx) {
4796
5607
  if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
4797
- if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
5608
+ if (ctx.state.steps[STEP_ID$4]?.status === "skipped") return "not-applicable";
4798
5609
  if (ctx.state.gitRemote === null) return "todo";
4799
5610
  try {
4800
5611
  return (await mbTool(deps(ctx).runner).gitSyncStatus(ctx.state.mbProfile)).branch === null ? "todo" : "done";
@@ -4810,22 +5621,22 @@ function gitSyncStep(deps = hostDeps$4) {
4810
5621
  }
4811
5622
  //#endregion
4812
5623
  //#region src/steps/license.ts
4813
- const STEP_ID$2 = "license";
4814
- const LICENSE_KEYS = { token: `${STEP_ID$2}.token` };
5624
+ const STEP_ID$3 = "license";
5625
+ const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
4815
5626
  const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
4816
- const NOT_FOUND_STATUS = 404;
4817
- function hostDeps$3(ctx) {
5627
+ const NOT_FOUND_STATUS$1 = 404;
5628
+ function hostDeps$5(ctx) {
4818
5629
  return { client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal) };
4819
5630
  }
4820
5631
  function isNoTokenYet(error) {
4821
- if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS;
5632
+ if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
4822
5633
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
4823
5634
  }
4824
5635
  function validateToken(value) {
4825
5636
  if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
4826
5637
  return "A license token is 64 hexadecimal characters";
4827
5638
  }
4828
- function requireApiKey(ctx) {
5639
+ function requireApiKey$1(ctx) {
4829
5640
  if (ctx.state.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
4830
5641
  return ctx.state.apiKey;
4831
5642
  }
@@ -4833,12 +5644,12 @@ function activeLabel(status) {
4833
5644
  const plan = status["plan-alias"];
4834
5645
  return plan === void 0 ? "License active" : `License active (plan ${plan})`;
4835
5646
  }
4836
- function licenseStep(deps = hostDeps$3) {
5647
+ function licenseStep(deps = hostDeps$5) {
4837
5648
  return {
4838
- id: STEP_ID$2,
5649
+ id: STEP_ID$3,
4839
5650
  title: "Activate your license",
4840
5651
  async detect(ctx) {
4841
- if (ctx.state.steps[STEP_ID$2]?.status === "skipped") return "not-applicable";
5652
+ if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
4842
5653
  const configured = configuredRuntime(ctx.state);
4843
5654
  if (configured === null || ctx.state.apiKey === null) return "todo";
4844
5655
  const client = deps(ctx).client(configured.url, {
@@ -4854,7 +5665,7 @@ function licenseStep(deps = hostDeps$3) {
4854
5665
  },
4855
5666
  async run(ctx) {
4856
5667
  const configured = requireConfiguredRuntime(ctx.state);
4857
- const apiKey = requireApiKey(ctx);
5668
+ const apiKey = requireApiKey$1(ctx);
4858
5669
  const token = await ctx.prompts.password({
4859
5670
  key: LICENSE_KEYS.token,
4860
5671
  message: "Paste your Metabase license token (Enter to skip)",
@@ -4898,242 +5709,343 @@ function licenseStep(deps = hostDeps$3) {
4898
5709
  };
4899
5710
  }
4900
5711
  //#endregion
4901
- //#region src/runtime/docker.ts
4902
- const DEFAULT_IMAGE = "metabase/metabase-enterprise:latest";
4903
- const DEFAULT_PORT = 13e3;
4904
- const CONTAINER_NAME_PREFIX = "rde-metabase";
4905
- const CONTAINER_PORT = 3e3;
4906
- const PLUGINS_DIR = "plugins";
4907
- function containerName(instanceName) {
4908
- return `${CONTAINER_NAME_PREFIX}-${instanceName}`;
5712
+ //#region src/steps/runtime.ts
5713
+ const STEP_ID$2 = "runtime";
5714
+ const MIN_PORT = 1;
5715
+ const MAX_PORT = 65535;
5716
+ const PORT_PATTERN = /^\d+$/;
5717
+ const RUNTIME_NAMES = {
5718
+ docker: "Docker",
5719
+ jar: "jar"
5720
+ };
5721
+ const NO_RUNTIME_FLAGS = {
5722
+ kind: null,
5723
+ image: null,
5724
+ jar: null,
5725
+ port: null
5726
+ };
5727
+ function hostDeps$4(ctx) {
5728
+ return {
5729
+ runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
5730
+ ports: hostPortProbe,
5731
+ releases: releaseLookup(ctx.signal)
5732
+ };
4909
5733
  }
4910
- function instanceUrl(port) {
4911
- return `http://localhost:${port}`;
5734
+ function parsePort(value) {
5735
+ if (!PORT_PATTERN.test(value)) return null;
5736
+ const port = Number.parseInt(value, 10);
5737
+ return port >= MIN_PORT && port <= MAX_PORT ? port : null;
4912
5738
  }
4913
- function publishesPortOnLoopback(inspection, port) {
4914
- return inspection.ports.some((mapping) => mapping.container === CONTAINER_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
5739
+ function javaReason(java) {
5740
+ if (!java.found) return java.reason;
5741
+ return java.major >= 21 ? null : `Java ${java.version} is older than 21`;
4915
5742
  }
4916
- function hostUserFor(platform, hostUser) {
4917
- return platform === "linux" ? hostUser : null;
5743
+ function unavailableReason(kind, available) {
5744
+ switch (kind) {
5745
+ case "docker": return available.docker.available ? null : available.docker.reason;
5746
+ case "jar": return javaReason(available.java);
5747
+ }
4918
5748
  }
4919
- function dockerRuntime(state, deps) {
4920
- const { docker } = deps;
4921
- const url = instanceUrl(state.port);
4922
- async function inspect() {
4923
- return docker.containerInspect(state.containerName);
5749
+ function javaLabel(java) {
5750
+ return java.found ? `Java ${java.version}` : "Java";
5751
+ }
5752
+ async function availability(runtime) {
5753
+ return {
5754
+ docker: await runtime.docker.available(),
5755
+ java: await runtime.java.find()
5756
+ };
5757
+ }
5758
+ async function chooseKind(ctx, options, runtime) {
5759
+ const available = await availability(runtime);
5760
+ if (options.kind !== null) {
5761
+ const reason = unavailableReason(options.kind, available);
5762
+ if (reason !== null) throw new ConfigError(`--runtime ${options.kind} is not usable: ${reason}`);
5763
+ ctx.ui.info(`Using the ${RUNTIME_NAMES[options.kind]} runtime from --runtime`);
5764
+ return options.kind;
4924
5765
  }
4925
- function conflict(reason) {
4926
- return new ConfigError(`container ${state.containerName} already exists ${reason}; remove it with \`docker rm -f ${state.containerName}\` and run \`rde init\` again`);
5766
+ if (available.docker.available) {
5767
+ ctx.ui.info(`Using Docker ${available.docker.serverVersion}`);
5768
+ return "docker";
4927
5769
  }
4928
- async function install(options) {
4929
- const existing = await inspect();
4930
- if (existing.exists) {
4931
- if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
4932
- if (!publishesPortOnLoopback(existing, state.port)) throw conflict(`without port ${state.port} published on ${LOOPBACK_HOST}`);
4933
- return;
4934
- }
4935
- await promises.mkdir(join(deps.dataDir, PLUGINS_DIR), { recursive: true });
4936
- await docker.imagePull(state.image, { onLine: options.onProgress });
4937
- await docker.containerCreate({
4938
- name: state.containerName,
4939
- image: state.image,
4940
- port: state.port,
4941
- dataDir: deps.dataDir,
4942
- addHostGateway: deps.platform === "linux",
4943
- hostUser: hostUserFor(deps.platform, deps.hostUser)
4944
- });
5770
+ const javaProblem = javaReason(available.java);
5771
+ if (javaProblem !== null) throw new ConfigError(`neither Docker nor Java can run Metabase here: ${available.docker.reason}; ${javaProblem}`);
5772
+ ctx.ui.info(`Docker is not available (${available.docker.reason}); using the jar runtime with ${javaLabel(available.java)}`);
5773
+ return "jar";
5774
+ }
5775
+ function chooseImage(ctx, options) {
5776
+ if (options.image !== null) {
5777
+ ctx.ui.info(`Using image ${options.image} from --image`);
5778
+ return options.image;
4945
5779
  }
4946
- async function start() {
4947
- const existing = await inspect();
4948
- if (!existing.exists) throw new ConfigError(`container ${state.containerName} does not exist; run \`rde init\``);
4949
- if (existing.running) return;
4950
- await docker.containerStart(state.containerName);
5780
+ return DEFAULT_IMAGE;
5781
+ }
5782
+ async function resolveJar(ctx, spec, releases) {
5783
+ const parsed = parseJarSpec(spec);
5784
+ if (parsed === null) throw new ConfigError(`${spec} is not latest, a release tag such as v1.63.18, or the path of a .jar file`);
5785
+ if (parsed.kind !== "latest") return parsed;
5786
+ const version = await releases.latestVersion();
5787
+ ctx.ui.info(`The latest Metabase release is ${version}`);
5788
+ return {
5789
+ kind: "release",
5790
+ version
5791
+ };
5792
+ }
5793
+ async function chooseJar(ctx, options, releases) {
5794
+ if (options.jar !== null) {
5795
+ ctx.ui.info(`Using ${options.jar} from --jar`);
5796
+ return resolveJar(ctx, options.jar, releases);
4951
5797
  }
4952
- async function stop() {
4953
- const existing = await inspect();
4954
- if (!existing.exists || !existing.running) return;
4955
- await docker.containerStop(state.containerName);
5798
+ return resolveJar(ctx, LATEST, releases);
5799
+ }
5800
+ async function choosePort(ctx, options, ports) {
5801
+ if (options.port !== null) {
5802
+ if (!await ports.isFree(options.port)) throw new ConfigError(`port ${options.port} from --port is in use by another program`);
5803
+ ctx.ui.info(`Using port ${options.port} from --port`);
5804
+ return options.port;
4956
5805
  }
4957
- async function status() {
4958
- const existing = await inspect();
4959
- return {
4960
- installed: existing.exists,
4961
- running: existing.exists && existing.running,
4962
- url
5806
+ const free = await ports.findFree(DEFAULT_PORT);
5807
+ if (free !== 3100) ctx.ui.warn(`Port ${DEFAULT_PORT} is in use; using ${free}`);
5808
+ return free;
5809
+ }
5810
+ async function chooseRuntime(ctx, options, deps) {
5811
+ const kind = await chooseKind(ctx, options, deps.runtime);
5812
+ switch (kind) {
5813
+ case "docker": {
5814
+ const image = chooseImage(ctx, options);
5815
+ const port = await choosePort(ctx, options, deps.ports);
5816
+ return {
5817
+ kind,
5818
+ image,
5819
+ containerName: containerName(ctx.state.name),
5820
+ port,
5821
+ hostAlias: DOCKER_HOST_ALIAS
5822
+ };
5823
+ }
5824
+ case "jar": return {
5825
+ kind,
5826
+ source: await chooseJar(ctx, options, deps.releases),
5827
+ port: await choosePort(ctx, options, deps.ports)
4963
5828
  };
4964
5829
  }
4965
- async function logs(options) {
4966
- if (!(await inspect()).exists) throw new ConfigError(`container ${state.containerName} does not exist; run \`rde init\``);
4967
- await docker.containerLogs(state.containerName, options);
5830
+ }
5831
+ function describeRuntime(runtime) {
5832
+ switch (runtime.kind) {
5833
+ case "docker": return runtime.image;
5834
+ case "jar": return describeJar(runtime.source);
4968
5835
  }
4969
- return {
4970
- kind: "docker",
4971
- install,
4972
- start,
4973
- stop,
4974
- status,
4975
- logs,
4976
- url: () => url,
4977
- hostAlias: () => state.hostAlias
4978
- };
4979
5836
  }
4980
- //#endregion
4981
- //#region src/runtime/runtime.ts
4982
- function hostRuntimeDeps(instanceName, signal) {
4983
- const user = userInfo();
5837
+ async function persistedRuntime(ctx, options, deps) {
5838
+ const existing = configuredRuntime(ctx.state);
5839
+ if (existing !== null) {
5840
+ ctx.ui.info(`Reinstalling Metabase from the saved choices: ${describeRuntime(existing.runtime)} on port ${existing.runtime.port}`);
5841
+ return existing.runtime;
5842
+ }
5843
+ const chosen = await chooseRuntime(ctx, options, deps);
5844
+ await ctx.saveState({
5845
+ ...ctx.state,
5846
+ runtime: chosen,
5847
+ url: instanceUrl(chosen.port)
5848
+ });
5849
+ return chosen;
5850
+ }
5851
+ function runtimeStep(options, deps = hostDeps$4) {
4984
5852
  return {
4985
- docker: dockerTool(processRunner(signal)),
4986
- dataDir: instanceDataDir(instanceName),
4987
- platform: process.platform,
4988
- hostUser: {
4989
- uid: user.uid,
4990
- gid: user.gid
5853
+ id: STEP_ID$2,
5854
+ title: "Install Metabase",
5855
+ async detect(ctx) {
5856
+ const existing = configuredRuntime(ctx.state);
5857
+ if (existing === null) return "todo";
5858
+ return (await runtimeFromState(existing.runtime, deps(ctx).runtime).status()).installed ? "done" : "todo";
5859
+ },
5860
+ async run(ctx) {
5861
+ const resolved = deps(ctx);
5862
+ const chosen = await persistedRuntime(ctx, options, resolved);
5863
+ const runtime = runtimeFromState(chosen, resolved.runtime);
5864
+ const spinner = ctx.ui.spinner(`Installing Metabase from ${describeRuntime(chosen)}`);
5865
+ try {
5866
+ await runtime.install({ onProgress: (line) => spinner.update(line) });
5867
+ } catch (error) {
5868
+ spinner.fail("Metabase install failed");
5869
+ throw error;
5870
+ }
5871
+ spinner.stop(`Metabase installed on port ${chosen.port}`);
5872
+ return "completed";
4991
5873
  }
4992
5874
  };
4993
5875
  }
4994
- function runtimeFromState(state, deps) {
4995
- switch (state.kind) {
4996
- case "docker": return dockerRuntime(state, deps);
4997
- case "jar": throw new ConfigError("jar runtime is not available yet");
4998
- }
4999
- }
5000
5876
  //#endregion
5001
- //#region src/steps/runtime.ts
5002
- const STEP_ID$1 = "runtime";
5003
- const RUNTIME_KEYS = {
5004
- kind: `${STEP_ID$1}.kind`,
5005
- image: `${STEP_ID$1}.image`,
5006
- port: `${STEP_ID$1}.port`
5007
- };
5008
- const MIN_PORT = 1;
5009
- const MAX_PORT = 65535;
5010
- const PORT_PATTERN = /^\d+$/;
5011
- const RUNTIME_CHOICES = [{
5012
- value: "docker",
5013
- label: "Docker",
5014
- hint: "recommended"
5015
- }, {
5016
- value: "jar",
5017
- label: "Jar",
5018
- hint: "coming soon"
5019
- }];
5020
- function hostDeps$2(ctx) {
5877
+ //#region src/steps/sample-database.ts
5878
+ const STEP_ID$1 = "sample-database";
5879
+ const SAMPLE_DATABASE_DISPLAY_NAME = "Sample Database";
5880
+ const POSTGRES_ENGINE = "postgres";
5881
+ const NOT_FOUND_STATUS = 404;
5882
+ function hostDeps$3(ctx) {
5021
5883
  return {
5022
5884
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
5023
- ports: hostPortProbe
5885
+ ports: hostPortProbe,
5886
+ client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
5887
+ readyTimeoutMs: SAMPLE_DATABASE_READY_TIMEOUT_MS,
5888
+ now: () => /* @__PURE__ */ new Date()
5024
5889
  };
5025
5890
  }
5026
- function parsePort(value) {
5027
- if (!PORT_PATTERN.test(value)) return null;
5028
- const port = Number.parseInt(value, 10);
5029
- return port >= MIN_PORT && port <= MAX_PORT ? port : null;
5891
+ function isNotServingYet(error) {
5892
+ return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
5030
5893
  }
5031
- function validatePort(value) {
5032
- return parsePort(value) === null ? `Enter a port between ${MIN_PORT} and ${MAX_PORT}` : void 0;
5894
+ function isNotFound$1(error) {
5895
+ return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
5033
5896
  }
5034
- async function chooseKind(ctx) {
5035
- for (;;) {
5036
- const kind = await ctx.prompts.select({
5037
- key: RUNTIME_KEYS.kind,
5038
- message: "How should Metabase run?",
5039
- choices: RUNTIME_CHOICES
5040
- });
5041
- if (kind === "docker") return kind;
5042
- ctx.ui.warn("The jar runtime is not available yet; pick Docker for now");
5043
- }
5897
+ function requireApiKey(state) {
5898
+ if (state.apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
5899
+ return state.apiKey;
5044
5900
  }
5045
- async function chooseImage(ctx, options) {
5046
- if (options.image !== null) {
5047
- ctx.ui.info(`Using image ${options.image} from --image`);
5048
- return options.image;
5901
+ async function recordedDatabase(record, client) {
5902
+ if (record.databaseId === null) return null;
5903
+ try {
5904
+ return await client.database.get(record.databaseId);
5905
+ } catch (error) {
5906
+ if (isNotFound$1(error)) return null;
5907
+ throw error;
5049
5908
  }
5050
- return ctx.prompts.text({
5051
- key: RUNTIME_KEYS.image,
5052
- message: "Docker image",
5053
- defaultValue: DEFAULT_IMAGE,
5054
- placeholder: DEFAULT_IMAGE
5055
- });
5056
5909
  }
5057
- async function suggestPort(ctx, ports) {
5058
- if (await ports.isFree(13e3)) return DEFAULT_PORT;
5059
- const free = await ports.findFree(DEFAULT_PORT);
5060
- ctx.ui.warn(`Port ${DEFAULT_PORT} is in use by another program; ${free} is free`);
5061
- return free;
5910
+ async function persistedRecord(ctx, deps) {
5911
+ const existing = ctx.state.sampleDatabase;
5912
+ if (existing !== null) {
5913
+ if ((await sampleDatabaseContainer(existing, deps.runtime.docker).status()).installed || await deps.ports.isFree(existing.port)) return {
5914
+ record: existing,
5915
+ staleDatabaseId: null
5916
+ };
5917
+ const pointer = existing.databaseId === null ? "" : `; Metabase database ${existing.databaseId} still points at the old one`;
5918
+ ctx.ui.warn(`Port ${existing.port} is in use by another program; the sample database moves to a new port${pointer}`);
5919
+ }
5920
+ const port = await deps.ports.findFree(SAMPLE_DATABASE_PORT);
5921
+ if (port !== 15432) ctx.ui.warn(`Port ${SAMPLE_DATABASE_PORT} is in use; using ${port} for the sample database`);
5922
+ const record = {
5923
+ containerName: sampleContainerName(ctx.state.name),
5924
+ image: SAMPLE_DATABASE_IMAGE,
5925
+ port,
5926
+ databaseId: null
5927
+ };
5928
+ await ctx.saveState({
5929
+ ...ctx.state,
5930
+ sampleDatabase: record
5931
+ });
5932
+ return {
5933
+ record,
5934
+ staleDatabaseId: existing?.databaseId ?? null
5935
+ };
5062
5936
  }
5063
- async function choosePort(ctx, options, ports) {
5064
- if (options.port !== null) {
5065
- if (!await ports.isFree(options.port)) throw new ConfigError(`port ${options.port} from --port is in use by another program`);
5066
- ctx.ui.info(`Using port ${options.port} from --port`);
5067
- return options.port;
5937
+ async function installAndStart(ctx, sidecar, record, deps) {
5938
+ const installing = ctx.ui.spinner(`Installing the sample database from ${record.image}`);
5939
+ try {
5940
+ await sidecar.install({ onProgress: (line) => installing.update(line) });
5941
+ } catch (error) {
5942
+ installing.fail("Sample database install failed");
5943
+ throw error;
5068
5944
  }
5069
- const suggested = await suggestPort(ctx, ports);
5070
- for (;;) {
5071
- const answer = await ctx.prompts.text({
5072
- key: RUNTIME_KEYS.port,
5073
- message: "Host port for Metabase",
5074
- defaultValue: String(suggested),
5075
- placeholder: String(suggested),
5076
- validate: validatePort
5945
+ installing.stop(`Sample database container ${record.containerName} on port ${record.port}`);
5946
+ await sidecar.start();
5947
+ const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
5948
+ try {
5949
+ await sidecar.waitReady({
5950
+ timeoutMs: deps.readyTimeoutMs,
5951
+ signal: ctx.signal
5077
5952
  });
5078
- const port = parsePort(answer);
5079
- if (port === null) throw new ConfigError(`${answer} is not a port between ${MIN_PORT} and ${MAX_PORT}`);
5080
- if (await ports.isFree(port)) return port;
5081
- ctx.ui.warn(`Port ${port} is in use by another program`);
5953
+ } catch (error) {
5954
+ waiting.fail("Postgres did not become ready");
5955
+ throw error;
5082
5956
  }
5957
+ waiting.stop(`Postgres is ready on ${LOOPBACK_HOST}:${record.port}`);
5958
+ }
5959
+ function connectionDetails(record, configured, deps) {
5960
+ const alias = runtimeFromState(configured.runtime, deps.runtime).hostAlias();
5961
+ return applyHostAlias({
5962
+ host: "localhost",
5963
+ port: record.port,
5964
+ dbname: SAMPLE_DATABASE_NAME,
5965
+ user: SAMPLE_DATABASE_USER,
5966
+ password: SAMPLE_DATABASE_PASSWORD
5967
+ }, POSTGRES_ENGINE, alias).details;
5083
5968
  }
5084
- async function chooseRuntime(ctx, options, ports) {
5085
- await chooseKind(ctx);
5086
- const image = await chooseImage(ctx, options);
5087
- const port = await choosePort(ctx, options, ports);
5969
+ function toRecord(database, createdAt) {
5088
5970
  return {
5089
- kind: "docker",
5090
- image,
5091
- containerName: containerName(ctx.state.name),
5092
- port,
5093
- hostAlias: DOCKER_HOST_ALIAS
5971
+ id: database.id,
5972
+ name: database.name,
5973
+ engine: database.engine,
5974
+ createdAt
5094
5975
  };
5095
5976
  }
5096
- function describeRuntime(runtime) {
5097
- switch (runtime.kind) {
5098
- case "docker": return runtime.image;
5099
- case "jar": return `${runtime.edition} jar ${runtime.version}`;
5977
+ async function connectDatabase(ctx, choice, configured, client, deps) {
5978
+ const { record } = choice;
5979
+ const known = await recordedDatabase(record, client);
5980
+ if (known !== null) {
5981
+ ctx.ui.info(`${known.name} is already connected as database ${known.id}`);
5982
+ return known;
5100
5983
  }
5101
- }
5102
- async function persistedRuntime(ctx, options, ports) {
5103
- const existing = configuredRuntime(ctx.state);
5104
- if (existing !== null) {
5105
- ctx.ui.info(`Reinstalling Metabase from the saved choices: ${describeRuntime(existing.runtime)} on port ${existing.runtime.port}`);
5106
- return existing.runtime;
5984
+ const spinner = ctx.ui.spinner(`Connecting ${SAMPLE_DATABASE_DISPLAY_NAME}…`);
5985
+ let database;
5986
+ try {
5987
+ database = await client.database.create({
5988
+ name: SAMPLE_DATABASE_DISPLAY_NAME,
5989
+ engine: POSTGRES_ENGINE,
5990
+ details: connectionDetails(record, configured, deps)
5991
+ });
5992
+ } catch (error) {
5993
+ spinner.fail(`Could not connect ${SAMPLE_DATABASE_DISPLAY_NAME}`);
5994
+ throw error;
5107
5995
  }
5108
- const chosen = await chooseRuntime(ctx, options, ports);
5996
+ const stale = /* @__PURE__ */ new Set([record.databaseId, choice.staleDatabaseId]);
5997
+ const others = ctx.state.databases.filter((entry) => !stale.has(entry.id));
5109
5998
  await ctx.saveState({
5110
5999
  ...ctx.state,
5111
- runtime: chosen,
5112
- url: instanceUrl(chosen.port)
6000
+ sampleDatabase: {
6001
+ ...record,
6002
+ databaseId: database.id
6003
+ },
6004
+ databases: [...others, toRecord(database, deps.now().toISOString())]
5113
6005
  });
5114
- return chosen;
6006
+ spinner.stop(`Connected ${database.name} as database ${database.id}`);
6007
+ return database;
6008
+ }
6009
+ function usageLines(record) {
6010
+ return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms are enabled; this database is where they can write."].join("\n");
5115
6011
  }
5116
- function runtimeStep(options, deps = hostDeps$2) {
6012
+ function sampleDatabaseStep(deps = hostDeps$3) {
5117
6013
  return {
5118
6014
  id: STEP_ID$1,
5119
- title: "Install Metabase",
6015
+ title: "Connect a sample database",
5120
6016
  async detect(ctx) {
5121
- const existing = configuredRuntime(ctx.state);
5122
- if (existing === null) return "todo";
5123
- return (await runtimeFromState(existing.runtime, deps(ctx).runtime).status()).installed ? "done" : "todo";
5124
- },
5125
- async run(ctx) {
5126
6017
  const resolved = deps(ctx);
5127
- const chosen = await persistedRuntime(ctx, options, resolved.ports);
5128
- const runtime = runtimeFromState(chosen, resolved.runtime);
5129
- const spinner = ctx.ui.spinner(`Installing Metabase from ${describeRuntime(chosen)}`);
6018
+ if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
6019
+ const record = ctx.state.sampleDatabase;
6020
+ const configured = configuredRuntime(ctx.state);
6021
+ if (record === null || configured === null || ctx.state.apiKey === null) return "todo";
6022
+ if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
6023
+ const client = resolved.client(configured.url, {
6024
+ kind: "api-key",
6025
+ key: ctx.state.apiKey
6026
+ });
5130
6027
  try {
5131
- await runtime.install({ onProgress: (line) => spinner.update(line) });
6028
+ return await recordedDatabase(record, client) === null ? "todo" : "done";
5132
6029
  } catch (error) {
5133
- spinner.fail("Metabase install failed");
6030
+ if (isNotServingYet(error)) return "todo";
5134
6031
  throw error;
5135
6032
  }
5136
- spinner.stop(`Metabase installed on port ${chosen.port}`);
6033
+ },
6034
+ async run(ctx) {
6035
+ const resolved = deps(ctx);
6036
+ const configured = requireConfiguredRuntime(ctx.state);
6037
+ const apiKey = requireApiKey(ctx.state);
6038
+ const docker = await resolved.runtime.docker.available();
6039
+ if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
6040
+ const choice = await persistedRecord(ctx, resolved);
6041
+ await installAndStart(ctx, sampleDatabaseContainer(choice.record, resolved.runtime.docker), choice.record, resolved);
6042
+ const client = resolved.client(configured.url, {
6043
+ kind: "api-key",
6044
+ key: apiKey
6045
+ });
6046
+ await connectDatabase(ctx, choice, configured, client, resolved);
6047
+ await client.setting.enableTransforms();
6048
+ ctx.ui.note(usageLines(choice.record), "Sample database");
5137
6049
  return "completed";
5138
6050
  }
5139
6051
  };
@@ -5147,7 +6059,7 @@ async function startAndWaitForHealth(runtime, client, options) {
5147
6059
  //#region src/steps/start-metabase.ts
5148
6060
  const LOG_TAIL_ON_TIMEOUT = 30;
5149
6061
  const PERCENT = 100;
5150
- function hostDeps$1(ctx) {
6062
+ function hostDeps$2(ctx) {
5151
6063
  return {
5152
6064
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
5153
6065
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
@@ -5157,10 +6069,26 @@ function hostDeps$1(ctx) {
5157
6069
  function isNotHealthy(error) {
5158
6070
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || error instanceof MetabaseApiError;
5159
6071
  }
6072
+ async function runningVersion(client) {
6073
+ return (await client.session.properties()).version.tag;
6074
+ }
5160
6075
  function progressLabel(progress) {
5161
6076
  return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
5162
6077
  }
5163
- function startMetabaseStep(deps = hostDeps$1) {
6078
+ async function startSampleDatabase(ctx, deps) {
6079
+ const record = ctx.state.sampleDatabase;
6080
+ if (record === null) return;
6081
+ switch (await sampleDatabaseContainer(record, deps.runtime.docker).start()) {
6082
+ case "missing":
6083
+ ctx.ui.warn(`Sample database container ${record.containerName} is missing; \`rde init\` recreates it`);
6084
+ return;
6085
+ case "started":
6086
+ ctx.ui.info(`Sample database container ${record.containerName} started`);
6087
+ return;
6088
+ case "running": return;
6089
+ }
6090
+ }
6091
+ function startMetabaseStep(deps = hostDeps$2) {
5164
6092
  return {
5165
6093
  id: "start-metabase",
5166
6094
  title: "Start Metabase",
@@ -5180,6 +6108,7 @@ function startMetabaseStep(deps = hostDeps$1) {
5180
6108
  const configured = requireConfiguredRuntime(ctx.state);
5181
6109
  const runtime = runtimeFromState(configured.runtime, resolved.runtime);
5182
6110
  const client = resolved.client(configured.url, NO_AUTH);
6111
+ await startSampleDatabase(ctx, resolved);
5183
6112
  const spinner = ctx.ui.spinner("Starting Metabase…");
5184
6113
  try {
5185
6114
  await startAndWaitForHealth(runtime, client, {
@@ -5196,7 +6125,12 @@ function startMetabaseStep(deps = hostDeps$1) {
5196
6125
  } else spinner.fail("Metabase failed to start");
5197
6126
  throw error;
5198
6127
  }
5199
- spinner.stop(`Metabase is running at ${configured.url}`);
6128
+ const version = await runningVersion(client);
6129
+ await ctx.saveState({
6130
+ ...ctx.state,
6131
+ version
6132
+ });
6133
+ spinner.stop(`Metabase ${version} is running at ${configured.url}`);
5200
6134
  return "completed";
5201
6135
  }
5202
6136
  };
@@ -5205,7 +6139,7 @@ function startMetabaseStep(deps = hostDeps$1) {
5205
6139
  //#region src/steps/summary.ts
5206
6140
  const STEP_ID = "summary";
5207
6141
  const GIT_SYNC_STEP_ID = "git-sync";
5208
- function hostDeps(ctx) {
6142
+ function hostDeps$1(ctx) {
5209
6143
  return {
5210
6144
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
5211
6145
  runner: processRunner(ctx.signal),
@@ -5273,6 +6207,7 @@ async function summaryLines(state, deps) {
5273
6207
  if (login !== null) lines.push(login);
5274
6208
  lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
5275
6209
  lines.push(...await databaseLines$1(state, deps));
6210
+ if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
5276
6211
  lines.push(...gitLines(state));
5277
6212
  const installed = await skillsCli(deps.runner).list();
5278
6213
  lines.push(skillLine(installed.filter((entry) => entry.name === "rde")));
@@ -5280,7 +6215,7 @@ async function summaryLines(state, deps) {
5280
6215
  lines.push("Lifecycle: rde status, rde stop, rde start");
5281
6216
  return lines;
5282
6217
  }
5283
- function summaryStep(deps = hostDeps) {
6218
+ function summaryStep(deps = hostDeps$1) {
5284
6219
  return {
5285
6220
  id: STEP_ID,
5286
6221
  title: "Summary",
@@ -5295,16 +6230,23 @@ function summaryStep(deps = hostDeps) {
5295
6230
  }
5296
6231
  //#endregion
5297
6232
  //#region src/steps/index.ts
6233
+ const DEFAULT_STEP_OPTIONS = {
6234
+ runtime: NO_RUNTIME_FLAGS,
6235
+ agents: DETECT_AGENTS,
6236
+ admin: DEFAULT_ADMIN,
6237
+ databases: ASK_DATABASES
6238
+ };
5298
6239
  const STEP_BY_ID = {
5299
6240
  preflight: () => preflightStep(),
5300
6241
  "install-mb": () => installMbStep(),
5301
- "agents-skills": () => agentsSkillsStep(),
5302
- runtime: (options) => runtimeStep(options),
6242
+ "agents-skills": (options) => agentsSkillsStep(options.agents),
6243
+ runtime: (options) => runtimeStep(options.runtime),
5303
6244
  "start-metabase": () => startMetabaseStep(),
5304
- "admin-user": () => adminUserStep(),
6245
+ "admin-user": (options) => adminUserStep(options.admin),
5305
6246
  "api-key": () => apiKeyStep(),
6247
+ "sample-database": () => sampleDatabaseStep(),
5306
6248
  license: () => licenseStep(),
5307
- databases: () => databasesStep(),
6249
+ databases: (options) => databasesStep(options.databases),
5308
6250
  "git-sync": () => gitSyncStep(),
5309
6251
  summary: () => summaryStep()
5310
6252
  };
@@ -5345,16 +6287,13 @@ const doctor = defineRdeCommand({
5345
6287
  async run(args) {
5346
6288
  const name = await instanceNameOrDefault();
5347
6289
  const state = await readInstanceState(name);
5348
- const deps = hostDoctorDeps(interruptSignal);
6290
+ const deps = hostDoctorDeps(name, interruptSignal);
5349
6291
  let report = await collectDoctor(name, state, deps);
5350
6292
  if (args.fix && report.failed > 0) {
5351
6293
  if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
5352
6294
  print(report, args.json);
5353
6295
  const ctx = await createWizardContext(name, { signal: interruptSignal });
5354
- if (await fixDoctor(report, initSteps({
5355
- image: null,
5356
- port: null
5357
- }), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, deps);
6296
+ if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, deps);
5358
6297
  }
5359
6298
  print(report, args.json);
5360
6299
  if (report.failed > 0) throw new Error(doctorSummary(report));
@@ -5381,12 +6320,6 @@ const EnvString = z.string().transform((value, ctx) => {
5381
6320
  });
5382
6321
  const AdminVariant = z.enum(["default", "prompted"]);
5383
6322
  const RemoteKind = z.enum(["local-bare", "hosted"]);
5384
- const PackageManagerName = z.enum([
5385
- "npm",
5386
- "pnpm",
5387
- "yarn",
5388
- "bun"
5389
- ]);
5390
6323
  const FieldValue = z.union([
5391
6324
  EnvString,
5392
6325
  z.number(),
@@ -5398,13 +6331,15 @@ const DatabaseEntry = z.strictObject({
5398
6331
  fields: z.record(z.string(), FieldValue).optional(),
5399
6332
  name: z.string().optional()
5400
6333
  }).refine((entry) => entry.uri === void 0 !== (entry.fields === void 0), { message: "a database needs either uri or fields" });
6334
+ const RuntimeSection = z.strictObject({
6335
+ kind: RuntimeKind.optional(),
6336
+ image: EnvString.optional(),
6337
+ jar: EnvString.optional(),
6338
+ port: z.int().positive().optional()
6339
+ }).refine((section) => section.image === void 0 || section.jar === void 0, { message: "a runtime section has either image or jar" }).refine((section) => section.kind !== "docker" || section.jar === void 0, { message: "jar does not apply to the docker runtime" }).refine((section) => section.kind !== "jar" || section.image === void 0, { message: "image does not apply to the jar runtime" });
5401
6340
  const SECTIONS = {
5402
- "install-mb": z.strictObject({ "package-manager": PackageManagerName }).optional(),
5403
6341
  "agents-skills": z.strictObject({ agents: z.array(AgentId) }).optional(),
5404
- runtime: z.strictObject({
5405
- image: EnvString.optional(),
5406
- port: z.int().positive().optional()
5407
- }).optional(),
6342
+ runtime: RuntimeSection.optional(),
5408
6343
  "admin-user": z.strictObject({
5409
6344
  variant: AdminVariant,
5410
6345
  email: EnvString.optional(),
@@ -5469,7 +6404,6 @@ function addDatabase(map, database, last) {
5469
6404
  add(map, DATABASE_KEYS.another, select$1(last ? "finish" : "another"));
5470
6405
  }
5471
6406
  function addAdmin(map, admin) {
5472
- add(map, ADMIN_USER_KEYS.variant, select$1(admin.variant));
5473
6407
  if (admin.variant !== "prompted") return;
5474
6408
  if (admin.email !== void 0) {
5475
6409
  add(map, ADMIN_USER_KEYS.email, text$1(admin.email));
@@ -5482,24 +6416,31 @@ function addAdmin(map, admin) {
5482
6416
  add(map, ADMIN_USER_KEYS.firstName, text$1(admin["first-name"] ?? NO_NAME));
5483
6417
  add(map, ADMIN_USER_KEYS.lastName, text$1(admin["last-name"] ?? NO_NAME));
5484
6418
  }
6419
+ function runtimeKindOf(runtime) {
6420
+ if (runtime.kind !== void 0) return runtime.kind;
6421
+ if (runtime.image !== void 0) return "docker";
6422
+ if (runtime.jar !== void 0) return "jar";
6423
+ return null;
6424
+ }
6425
+ function stepOptionsFrom(answers) {
6426
+ const runtime = answers.runtime;
6427
+ const agents = answers["agents-skills"];
6428
+ const admin = answers["admin-user"];
6429
+ const databases = answers.databases;
6430
+ return {
6431
+ runtime: runtime === void 0 ? DEFAULT_STEP_OPTIONS.runtime : {
6432
+ kind: runtimeKindOf(runtime),
6433
+ image: runtime.image ?? null,
6434
+ jar: runtime.jar ?? null,
6435
+ port: runtime.port ?? null
6436
+ },
6437
+ agents: agents === void 0 ? DEFAULT_STEP_OPTIONS.agents : { agents: agents.agents },
6438
+ admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
6439
+ databases: { requested: databases !== void 0 && databases.length > 0 }
6440
+ };
6441
+ }
5485
6442
  function keyedAnswers(answers) {
5486
6443
  const map = /* @__PURE__ */ new Map();
5487
- const installMb = answers["install-mb"];
5488
- if (installMb !== void 0) {
5489
- add(map, INSTALL_MB_KEYS.packageManager, select$1(installMb["package-manager"]));
5490
- add(map, INSTALL_MB_KEYS.confirm, confirm$1(true));
5491
- }
5492
- const agents = answers["agents-skills"];
5493
- if (agents !== void 0) add(map, AGENTS_SKILLS_KEYS.agents, {
5494
- kind: "multiselect",
5495
- values: agents.agents
5496
- });
5497
- const runtime = answers.runtime;
5498
- if (runtime !== void 0) {
5499
- add(map, RUNTIME_KEYS.kind, select$1("docker"));
5500
- if (runtime.image !== void 0) add(map, RUNTIME_KEYS.image, text$1(runtime.image));
5501
- if (runtime.port !== void 0) add(map, RUNTIME_KEYS.port, text$1(String(runtime.port)));
5502
- }
5503
6444
  const admin = answers["admin-user"];
5504
6445
  if (admin !== void 0) addAdmin(map, admin);
5505
6446
  const apiKey = answers["api-key"];
@@ -5555,12 +6496,6 @@ function mergeInitAnswers(file, flags) {
5555
6496
  };
5556
6497
  }
5557
6498
  const FLAG_FOR_KEY = {
5558
- [INSTALL_MB_KEYS.packageManager]: "--yes",
5559
- [INSTALL_MB_KEYS.confirm]: "--yes",
5560
- [AGENTS_SKILLS_KEYS.agents]: "--agents or --skip-agents",
5561
- [RUNTIME_KEYS.image]: "--image",
5562
- [RUNTIME_KEYS.port]: "--port",
5563
- [ADMIN_USER_KEYS.variant]: "--admin",
5564
6499
  [ADMIN_USER_KEYS.email]: "--admin-email",
5565
6500
  [ADMIN_USER_KEYS.password]: "--admin-password",
5566
6501
  [API_KEY_KEYS.email]: "--admin-email",
@@ -5590,6 +6525,7 @@ function refusalWithoutTerminal(prompt) {
5590
6525
  }
5591
6526
  const LIST_SEPARATOR = ",";
5592
6527
  const DATABASE_SEPARATOR = "=";
6528
+ const RUNTIME_LIST$1 = RuntimeKind.options.join(", ");
5593
6529
  const GIT_REMOTE_NAMES = {
5594
6530
  local: "local-bare",
5595
6531
  hosted: "hosted"
@@ -5661,6 +6597,37 @@ function gitSection(flags, secrets) {
5661
6597
  ...secrets.has("--git-token") && { token: secrets.get("--git-token") }
5662
6598
  };
5663
6599
  }
6600
+ function impliedKind(flags) {
6601
+ if (flags.image !== void 0 && flags.jar !== void 0) throw conflict("--image", "--jar");
6602
+ if (flags.image !== void 0) return "docker";
6603
+ if (flags.jar !== void 0) return "jar";
6604
+ return null;
6605
+ }
6606
+ function runtimeKind(flags) {
6607
+ const implied = impliedKind(flags);
6608
+ if (flags.runtime === void 0) return;
6609
+ const parsed = RuntimeKind.safeParse(flags.runtime);
6610
+ if (!parsed.success) throw new ConfigError(`--runtime ${flags.runtime} is not one of ${RUNTIME_LIST$1}`);
6611
+ if (implied !== null && implied !== parsed.data) throw conflict(`--runtime ${parsed.data}`, implied === "docker" ? "--image" : "--jar");
6612
+ return parsed.data;
6613
+ }
6614
+ function portFlag(value) {
6615
+ if (value === void 0) return;
6616
+ const port = parsePort(value);
6617
+ if (port === null) throw new ConfigError(`--port ${value} is not a valid port number`);
6618
+ return port;
6619
+ }
6620
+ function runtimeSection(flags) {
6621
+ const kind = runtimeKind(flags);
6622
+ const port = portFlag(flags.port);
6623
+ if (kind === void 0 && flags.image === void 0 && flags.jar === void 0 && port === void 0) return;
6624
+ return {
6625
+ ...kind !== void 0 && { kind },
6626
+ ...flags.image !== void 0 && { image: flags.image },
6627
+ ...flags.jar !== void 0 && { jar: flags.jar },
6628
+ ...port !== void 0 && { port }
6629
+ };
6630
+ }
5664
6631
  function agentsSection(flags) {
5665
6632
  if (flags.skipAgents) {
5666
6633
  if (flags.agents !== void 0) throw conflict("--skip-agents", "--agents");
@@ -5685,7 +6652,7 @@ async function answersFromFlags(flags, readStdin) {
5685
6652
  }
5686
6653
  ], readStdin);
5687
6654
  const sections = {
5688
- runtime: {},
6655
+ runtime: runtimeSection(flags),
5689
6656
  "admin-user": adminSection(flags, secrets),
5690
6657
  license: licenseSection(flags, secrets),
5691
6658
  databases: databasesSection(flags),
@@ -5724,10 +6691,6 @@ function stringOf(answer) {
5724
6691
  if (answer.kind === "select" && typeof answer.value === "string") return answer.value;
5725
6692
  return null;
5726
6693
  }
5727
- function runtime(draft) {
5728
- draft.answers.runtime ??= {};
5729
- return draft.answers.runtime;
5730
- }
5731
6694
  function admin(draft) {
5732
6695
  draft.answers["admin-user"] ??= { variant: "prompted" };
5733
6696
  return draft.answers["admin-user"];
@@ -5736,21 +6699,12 @@ function gitSync(draft) {
5736
6699
  draft.answers["git-sync"] ??= { location: "skip" };
5737
6700
  return draft.answers["git-sync"];
5738
6701
  }
5739
- function isAdminVariant(value) {
5740
- return value === "default" || value === "prompted";
5741
- }
5742
6702
  function isLocation(value) {
5743
6703
  return value === "here" || value === "new" || value === "existing" || value === "skip";
5744
6704
  }
5745
6705
  function isRemote(value) {
5746
6706
  return value === "local-bare" || value === "hosted";
5747
6707
  }
5748
- function isPackageManager(value) {
5749
- return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun";
5750
- }
5751
- function isAgentList(values) {
5752
- return values.every((value) => typeof value === "string");
5753
- }
5754
6708
  function applyDatabaseRecord(draft, record) {
5755
6709
  const value = stringOf(record.answer);
5756
6710
  if (record.key === DATABASE_KEYS.engine) {
@@ -5780,8 +6734,7 @@ function applyDatabaseRecord(draft, record) {
5780
6734
  else if (value !== null) database.fields[fieldName] = value;
5781
6735
  }
5782
6736
  function applyAdminRecord(draft, key, value) {
5783
- if (key === ADMIN_USER_KEYS.variant && value !== null && isAdminVariant(value)) admin(draft).variant = value;
5784
- else if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
6737
+ if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
5785
6738
  else if (key === ADMIN_USER_KEYS.password) admin(draft).password = envReference(ENV_ADMIN_PASSWORD);
5786
6739
  else if (key === ADMIN_USER_KEYS.firstName && value !== null && value.length > 0) admin(draft)["first-name"] = value;
5787
6740
  else if (key === ADMIN_USER_KEYS.lastName && value !== null && value.length > 0) admin(draft)["last-name"] = value;
@@ -5794,16 +6747,11 @@ function applyGitRecord(draft, key, value) {
5794
6747
  else if (key === GIT_SYNC_KEYS.token) gitSync(draft).token = envReference(ENV_GIT_TOKEN);
5795
6748
  }
5796
6749
  function applyRecord(draft, record) {
5797
- const { key, answer } = record;
5798
- const value = stringOf(answer);
6750
+ const { key } = record;
6751
+ const value = stringOf(record.answer);
5799
6752
  if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
5800
6753
  else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
5801
6754
  else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
5802
- else if (key === INSTALL_MB_KEYS.packageManager && value !== null && isPackageManager(value)) draft.answers["install-mb"] = { "package-manager": value };
5803
- else if (key === AGENTS_SKILLS_KEYS.agents && answer.kind === "multiselect") {
5804
- if (isAgentList(answer.values)) draft.answers["agents-skills"] = { agents: answer.values };
5805
- } else if (key === RUNTIME_KEYS.image && value !== null) runtime(draft).image = value;
5806
- else if (key === RUNTIME_KEYS.port && value !== null) runtime(draft).port = Number(value);
5807
6755
  else if (key === API_KEY_KEYS.profile && value !== null) {
5808
6756
  const profile = MbProfileChoice.safeParse(value);
5809
6757
  if (profile.success) draft.answers["api-key"] = { profile: profile.data };
@@ -5822,7 +6770,7 @@ function databaseEntry(database, index) {
5822
6770
  ...database.name !== null && { name: database.name }
5823
6771
  };
5824
6772
  }
5825
- function answersFromRecords(records) {
6773
+ function answersFromRecords(records, overrides) {
5826
6774
  const draft = {
5827
6775
  answers: {},
5828
6776
  databases: [],
@@ -5831,7 +6779,11 @@ function answersFromRecords(records) {
5831
6779
  for (const record of records) applyRecord(draft, record);
5832
6780
  const databases = draft.databases.map(databaseEntry).filter((entry) => entry !== null);
5833
6781
  if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
5834
- return draft.answers;
6782
+ return {
6783
+ ...draft.answers,
6784
+ ...overrides.runtime !== void 0 && { runtime: overrides.runtime },
6785
+ ...overrides["agents-skills"] !== void 0 && { "agents-skills": overrides["agents-skills"] }
6786
+ };
5835
6787
  }
5836
6788
  async function writeAnswersFile(path, answers) {
5837
6789
  await promises.writeFile(path, JSON.stringify(answers, null, 2) + "\n", { mode: FILE_MODE });
@@ -6016,40 +6968,24 @@ function recordingPrompts(base) {
6016
6968
  //#endregion
6017
6969
  //#region src/commands/init.ts
6018
6970
  const STEP_LIST = StepId.options.join(", ");
6971
+ const RUNTIME_LIST = RuntimeKind.options.join(", ");
6019
6972
  function stepFlag(flag, value) {
6020
6973
  if (value === void 0) return;
6021
6974
  const parsed = StepId.safeParse(value);
6022
6975
  if (!parsed.success) throw new ConfigError(`${flag} ${value} is not one of the steps: ${STEP_LIST}`);
6023
6976
  return parsed.data;
6024
6977
  }
6025
- function portFlag(value) {
6026
- if (value === void 0) return null;
6027
- const port = parsePort(value);
6028
- if (port === null) throw new ConfigError(`--port ${value} is not a valid port number`);
6029
- return port;
6030
- }
6031
6978
  async function combinedAnswers(options) {
6032
6979
  const fromFlags = await answersFromFlags(options.flags, readStdinLine);
6033
6980
  if (options.answersPath === void 0) return fromFlags;
6034
6981
  return mergeInitAnswers(await loadInitAnswers(options.answersPath), fromFlags);
6035
6982
  }
6036
- async function promptsFor(options) {
6983
+ function promptsFor(answers, yes) {
6037
6984
  const answering = {
6038
6985
  ...!stdinIsTty() && { unanswered: refusalWithoutTerminal },
6039
- ...options.yes && { confirmAll: true }
6040
- };
6041
- return answeringPrompts(clackPrompts(), keyedAnswers(await combinedAnswers(options)), answering);
6042
- }
6043
- function withRuntimeFlags(answers, image, port) {
6044
- if (image === null && port === null) return answers;
6045
- return {
6046
- ...answers,
6047
- runtime: {
6048
- ...answers.runtime,
6049
- ...image !== null && { image },
6050
- ...port !== null && { port }
6051
- }
6986
+ ...yes && { confirmAll: true }
6052
6987
  };
6988
+ return answeringPrompts(clackPrompts(), keyedAnswers(answers), answering);
6053
6989
  }
6054
6990
  const init = defineRdeCommand({
6055
6991
  meta: {
@@ -6065,13 +7001,21 @@ const init = defineRdeCommand({
6065
7001
  type: "string",
6066
7002
  description: "Run only this step"
6067
7003
  },
7004
+ runtime: {
7005
+ type: "string",
7006
+ description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available`
7007
+ },
6068
7008
  image: {
6069
7009
  type: "string",
6070
- description: "Docker image for a new instance"
7010
+ description: "Docker image for a new instance (implies --runtime docker)"
7011
+ },
7012
+ jar: {
7013
+ type: "string",
7014
+ description: "Metabase jar for a new instance: latest, a release tag, or a .jar path (implies --runtime jar)"
6071
7015
  },
6072
7016
  port: {
6073
7017
  type: "string",
6074
- description: "Host port for a new instance"
7018
+ description: "Host port for a new instance; the first free from 3100"
6075
7019
  },
6076
7020
  answers: {
6077
7021
  type: "string",
@@ -6083,7 +7027,7 @@ const init = defineRdeCommand({
6083
7027
  },
6084
7028
  admin: {
6085
7029
  type: "string",
6086
- description: "Admin credentials: default or prompted"
7030
+ description: "Admin credentials: default (admin@metabase.com / metabase123) or prompted"
6087
7031
  },
6088
7032
  "admin-email": {
6089
7033
  type: "string",
@@ -6134,7 +7078,7 @@ const init = defineRdeCommand({
6134
7078
  },
6135
7079
  agents: {
6136
7080
  type: "string",
6137
- description: "Agents to install the skill for, comma-separated"
7081
+ description: "Agents to install the skill for, comma-separated; the detected ones by default"
6138
7082
  },
6139
7083
  "skip-agents": {
6140
7084
  type: "boolean",
@@ -6147,7 +7091,7 @@ const init = defineRdeCommand({
6147
7091
  },
6148
7092
  yes: {
6149
7093
  type: "boolean",
6150
- description: "Answer yes to every confirmation",
7094
+ description: "Answer yes to every confirmation (skipping databases)",
6151
7095
  default: false
6152
7096
  }
6153
7097
  },
@@ -6158,14 +7102,14 @@ const init = defineRdeCommand({
6158
7102
  ...from !== void 0 && { from },
6159
7103
  ...only !== void 0 && { only }
6160
7104
  };
6161
- const stepOptions = {
6162
- image: args.image ?? null,
6163
- port: portFlag(args.port)
6164
- };
6165
7105
  const runOptions = {
6166
7106
  answersPath: args.answers,
6167
7107
  yes: args.yes,
6168
7108
  flags: {
7109
+ runtime: args.runtime,
7110
+ image: args.image,
7111
+ jar: args.jar,
7112
+ port: args.port,
6169
7113
  admin: args.admin,
6170
7114
  adminEmail: args["admin-email"],
6171
7115
  adminPassword: args["admin-password"],
@@ -6184,16 +7128,17 @@ const init = defineRdeCommand({
6184
7128
  }
6185
7129
  };
6186
7130
  const printTo = args["print-answers"];
6187
- const answering = await promptsFor(runOptions);
7131
+ const answers = await combinedAnswers(runOptions);
7132
+ const answering = promptsFor(answers, runOptions.yes);
6188
7133
  const recording = printTo === void 0 ? null : recordingPrompts(answering);
6189
7134
  const ctx = await createWizardContext(DEFAULT_INSTANCE_NAME, {
6190
7135
  signal: interruptSignal,
6191
7136
  prompts: recording?.prompts ?? answering
6192
7137
  });
6193
7138
  ctx.ui.intro("rde init");
6194
- await runWizard(initSteps(stepOptions), ctx, options);
7139
+ await runWizard(initSteps(stepOptionsFrom(answers)), ctx, options);
6195
7140
  if (recording !== null && printTo !== void 0) {
6196
- await writeAnswersFile(printTo, withRuntimeFlags(answersFromRecords(recording.records), stepOptions.image, stepOptions.port));
7141
+ await writeAnswersFile(printTo, answersFromRecords(recording.records, answers));
6197
7142
  ctx.ui.info(`Answers written to ${printTo}`);
6198
7143
  }
6199
7144
  ctx.ui.outro("rde init finished");
@@ -6204,10 +7149,12 @@ const init = defineRdeCommand({
6204
7149
  async function loadInstance(name, signal) {
6205
7150
  const state = await requireInstanceState(name);
6206
7151
  const configured = requireConfiguredRuntime(state);
7152
+ const deps = hostRuntimeDeps(name, signal);
6207
7153
  return {
6208
7154
  state,
6209
7155
  url: configured.url,
6210
- runtime: runtimeFromState(configured.runtime, hostRuntimeDeps(name, signal))
7156
+ runtime: runtimeFromState(configured.runtime, deps),
7157
+ sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker)
6211
7158
  };
6212
7159
  }
6213
7160
  function openInstance(url) {
@@ -6264,10 +7211,7 @@ const start = defineRdeCommand({
6264
7211
  const SUMMARY_STEP_ID = "summary";
6265
7212
  function hostStatusDeps(instanceName, signal) {
6266
7213
  return {
6267
- steps: initSteps({
6268
- image: null,
6269
- port: null
6270
- }),
7214
+ steps: initSteps(DEFAULT_STEP_OPTIONS),
6271
7215
  runtime: hostRuntimeDeps(instanceName, signal),
6272
7216
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
6273
7217
  runner: processRunner(signal)
@@ -6307,6 +7251,19 @@ async function databasesOf(ctx, deps, health) {
6307
7251
  throw error;
6308
7252
  }
6309
7253
  }
7254
+ async function sampleDatabaseOf(ctx, deps, databases) {
7255
+ const record = ctx.state.sampleDatabase;
7256
+ if (record === null) return null;
7257
+ const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
7258
+ const listed = databases?.find((database) => database.id === record.databaseId) ?? null;
7259
+ return {
7260
+ container: status.running ? "running" : status.installed ? "stopped" : "missing",
7261
+ port: record.port,
7262
+ url: sampleDatabaseUrl(record.port),
7263
+ databaseId: record.databaseId,
7264
+ initialSyncStatus: listed === null ? null : listed.initialSyncStatus
7265
+ };
7266
+ }
6310
7267
  function taskLabel(status) {
6311
7268
  const task = status.current_task;
6312
7269
  return task === null ? null : `${task.sync_task_type} ${task.status}`;
@@ -6332,6 +7289,7 @@ async function collectStatus(ctx, deps) {
6332
7289
  const configured = configuredRuntime(ctx.state);
6333
7290
  const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
6334
7291
  const health = configured === null ? null : await healthOf(configured.url, deps);
7292
+ const databases = health === null ? null : await databasesOf(ctx, deps, health);
6335
7293
  return {
6336
7294
  instance: ctx.state.name,
6337
7295
  url: configured?.url ?? null,
@@ -6340,9 +7298,11 @@ async function collectStatus(ctx, deps) {
6340
7298
  installed: runtime.installed,
6341
7299
  running: runtime.running
6342
7300
  },
7301
+ version: ctx.state.version,
6343
7302
  health,
6344
7303
  steps: await stepReports(ctx, deps),
6345
- databases: health === null ? null : await databasesOf(ctx, deps, health),
7304
+ databases,
7305
+ sampleDatabase: await sampleDatabaseOf(ctx, deps, databases),
6346
7306
  gitSync: await gitSyncOf(ctx, deps)
6347
7307
  };
6348
7308
  }
@@ -6352,6 +7312,9 @@ function runtimeLine(report) {
6352
7312
  if (report.runtime.installed) return `Runtime: ${report.runtime.kind}, stopped; \`rde start\``;
6353
7313
  return `Runtime: ${report.runtime.kind}, not installed; run \`rde init\``;
6354
7314
  }
7315
+ function versionLine(report) {
7316
+ return report.version === null ? "Version: not recorded yet; `rde start` records it" : `Version: ${report.version}`;
7317
+ }
6355
7318
  function isGitSyncFailure(value) {
6356
7319
  return "error" in value;
6357
7320
  }
@@ -6360,6 +7323,22 @@ function gitSyncLine(report) {
6360
7323
  if (isGitSyncFailure(report.gitSync)) return `Remote sync: ${report.gitSync.error}`;
6361
7324
  return `Remote sync: ${report.gitSync.branch === null ? "no branch" : `branch ${report.gitSync.branch}`}, ${report.gitSync.dirty ? "unsynced changes" : "in sync"}, ${report.gitSync.task === null ? "no task running" : `task ${report.gitSync.task}`}`;
6362
7325
  }
7326
+ const CONTAINER_STATE_LABEL = {
7327
+ running: "container running",
7328
+ stopped: "container stopped; `rde start`",
7329
+ missing: "container missing; `rde init` recreates it"
7330
+ };
7331
+ function sampleConnectionLabel(report, sample) {
7332
+ if (sample.databaseId === null) return "not connected to Metabase yet; `rde init`";
7333
+ if (report.databases === null) return "sync unknown while Metabase is not running";
7334
+ if (sample.initialSyncStatus === null) return `database ${sample.databaseId} is gone from Metabase; \`rde init\` connects it again`;
7335
+ return `sync ${sample.initialSyncStatus}`;
7336
+ }
7337
+ function sampleDatabaseLine(report) {
7338
+ const sample = report.sampleDatabase;
7339
+ if (sample === null) return "Sample database: not connected; `rde init` adds one";
7340
+ return `Sample database: ${sample.url}, ${CONTAINER_STATE_LABEL[sample.container]}, ${sampleConnectionLabel(report, sample)}`;
7341
+ }
6363
7342
  function databaseLines(report) {
6364
7343
  if (report.databases === null) return ["Databases: unavailable while Metabase is not running"];
6365
7344
  return ["Databases:", ...databaseListingLines(report.databases).map((line) => ` ${line}`)];
@@ -6368,10 +7347,12 @@ function statusLines(report) {
6368
7347
  return [
6369
7348
  report.url === null ? `Instance ${report.instance}` : `Instance ${report.instance} — ${report.url}`,
6370
7349
  runtimeLine(report),
7350
+ versionLine(report),
6371
7351
  `Health: ${report.health ?? "unknown"}`,
6372
7352
  "Steps:",
6373
7353
  ...checklistLines(report.steps).map((line) => ` ${line}`),
6374
7354
  ...databaseLines(report),
7355
+ sampleDatabaseLine(report),
6375
7356
  gitSyncLine(report)
6376
7357
  ];
6377
7358
  }
@@ -6403,13 +7384,19 @@ const status = defineRdeCommand({
6403
7384
  const stop = defineRdeCommand({
6404
7385
  meta: {
6405
7386
  name: "stop",
6406
- description: "Stop the Metabase instance"
7387
+ description: "Stop the Metabase instance and its sample database"
6407
7388
  },
6408
7389
  async run() {
6409
7390
  const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
6410
7391
  const before = await instance.runtime.status();
6411
7392
  await instance.runtime.stop();
6412
- printLines([before.running ? "Metabase stopped" : "Metabase is not running"]);
7393
+ const lines = [before.running ? "Metabase stopped" : "Metabase is not running"];
7394
+ if (instance.sampleDatabase !== null) {
7395
+ const sidecar = await instance.sampleDatabase.status();
7396
+ await instance.sampleDatabase.stop();
7397
+ lines.push(sidecar.running ? "Sample database stopped" : "Sample database is not running");
7398
+ }
7399
+ printLines(lines);
6413
7400
  }
6414
7401
  });
6415
7402
  //#endregion
@@ -6419,10 +7406,12 @@ function hostUninstallDeps(signal) {
6419
7406
  const runner = processRunner(signal);
6420
7407
  return {
6421
7408
  docker: dockerTool(runner),
7409
+ daemon: daemonTool(runner),
6422
7410
  runner
6423
7411
  };
6424
7412
  }
6425
- const NOT_TOUCHED = "The mb CLI and Docker itself";
7413
+ const NOT_TOUCHED = "The mb CLI, Docker, and Java themselves";
7414
+ const ANY_JAR = "-jar";
6426
7415
  function isFileSystemError(error) {
6427
7416
  return error instanceof Error && "code" in error;
6428
7417
  }
@@ -6495,13 +7484,28 @@ async function findContainer(name, docker, stateImage) {
6495
7484
  throw error;
6496
7485
  }
6497
7486
  }
7487
+ async function findJarProcess(instance, source, daemon) {
7488
+ const dir = instanceDir(instance);
7489
+ const pid = await runningJarPid(dir, source === null ? ANY_JAR : jarPathOf(source, jarsDir()), daemon);
7490
+ if (pid === null) {
7491
+ await promises.rm(pidPathOf(dir), { force: true });
7492
+ return null;
7493
+ }
7494
+ return {
7495
+ label: `Metabase process ${pid} (running; it is stopped first)`,
7496
+ remove: async () => {
7497
+ await daemon.terminate(pid, { graceMs: JAR_STOP_GRACE_MS });
7498
+ await promises.rm(pidPathOf(dir), { force: true });
7499
+ }
7500
+ };
7501
+ }
6498
7502
  async function findImage(image, docker, removeImage) {
6499
7503
  if (!removeImage) return {
6500
7504
  removal: null,
6501
7505
  kept: `Docker image ${image}; pass --remove-image to delete it`
6502
7506
  };
6503
7507
  try {
6504
- if (!await docker.imageExists(image)) return {
7508
+ if (await docker.imageId(image) === null) return {
6505
7509
  removal: null,
6506
7510
  kept: null
6507
7511
  };
@@ -6520,6 +7524,31 @@ async function findImage(image, docker, removeImage) {
6520
7524
  kept: null
6521
7525
  };
6522
7526
  }
7527
+ async function findJar(source, removeImage) {
7528
+ if (source.kind !== "release") return {
7529
+ removal: null,
7530
+ kept: null
7531
+ };
7532
+ const dir = jarReleaseDir(source.version);
7533
+ if (!await pathExists(dir)) return {
7534
+ removal: null,
7535
+ kept: null
7536
+ };
7537
+ if (!removeImage) return {
7538
+ removal: null,
7539
+ kept: `Downloaded jar ${dir}; pass --remove-image to delete it`
7540
+ };
7541
+ return {
7542
+ removal: {
7543
+ label: `Downloaded jar ${dir}`,
7544
+ remove: () => promises.rm(dir, {
7545
+ recursive: true,
7546
+ force: true
7547
+ })
7548
+ },
7549
+ kept: null
7550
+ };
7551
+ }
6523
7552
  function isInstanceProfile(status, url) {
6524
7553
  if (!status.present) return false;
6525
7554
  if (url !== null) return status.url === url;
@@ -6574,7 +7603,11 @@ async function findConfigEntry(instance) {
6574
7603
  };
6575
7604
  }
6576
7605
  async function pruneEmptyDirs() {
6577
- for (const dir of [instancesDir(), rdeHome()]) try {
7606
+ for (const dir of [
7607
+ instancesDir(),
7608
+ jarsDir(),
7609
+ rdeHome()
7610
+ ]) try {
6578
7611
  await promises.rmdir(dir);
6579
7612
  } catch (error) {
6580
7613
  if (!isNotFound(error) && !isNotEmpty(error)) throw error;
@@ -6597,10 +7630,23 @@ async function planUninstall(options, deps) {
6597
7630
  const container = await findContainer(runtime?.containerName ?? containerName(options.instance), deps.docker, runtime?.image ?? null);
6598
7631
  if (container.removal !== null) removals.push(container.removal);
6599
7632
  if (container.problem !== null) problems.push(container.problem);
6600
- if (container.image !== null) {
6601
- const image = await findImage(container.image, deps.docker, options.removeImage);
6602
- if (image.removal !== null) removals.push(image.removal);
6603
- if (image.kept !== null) kept.push(image.kept);
7633
+ const sample = state?.sampleDatabase ?? null;
7634
+ const sidecar = await findContainer(sample?.containerName ?? sampleContainerName(options.instance), deps.docker, sample?.image ?? null);
7635
+ if (sidecar.removal !== null) removals.push(sidecar.removal);
7636
+ if (sidecar.problem !== null) problems.push(sidecar.problem);
7637
+ const jarSource = state?.runtime?.kind === "jar" ? state.runtime.source : null;
7638
+ const jarProcess = await findJarProcess(options.instance, jarSource, deps.daemon);
7639
+ if (jarProcess !== null) removals.push(jarProcess);
7640
+ for (const image of [container.image, sidecar.image]) {
7641
+ if (image === null) continue;
7642
+ const finding = await findImage(image, deps.docker, options.removeImage);
7643
+ if (finding.removal !== null) removals.push(finding.removal);
7644
+ if (finding.kept !== null) kept.push(finding.kept);
7645
+ }
7646
+ if (jarSource !== null) {
7647
+ const jar = await findJar(jarSource, options.removeImage);
7648
+ if (jar.removal !== null) removals.push(jar.removal);
7649
+ if (jar.kept !== null) kept.push(jar.kept);
6604
7650
  }
6605
7651
  const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, "rde"], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
6606
7652
  removals.push(...profiles);
@@ -6628,7 +7674,7 @@ async function planUninstall(options, deps) {
6628
7674
  problems
6629
7675
  };
6630
7676
  }
6631
- function planLines(plan) {
7677
+ function planLines$1(plan) {
6632
7678
  return [
6633
7679
  "Removing:",
6634
7680
  ...plan.removals.map((removal) => ` ${removal.label}`),
@@ -6679,7 +7725,7 @@ async function uninstall(options, deps, io) {
6679
7725
  problems: plan.problems
6680
7726
  };
6681
7727
  }
6682
- io.ui.note(planLines(plan).join("\n"), `rde uninstall ${options.instance}`);
7728
+ io.ui.note(planLines$1(plan).join("\n"), `rde uninstall ${options.instance}`);
6683
7729
  if (!options.yes) {
6684
7730
  if (!await io.prompts.confirm({
6685
7731
  message: "Remove everything listed?",
@@ -6719,7 +7765,7 @@ const uninstallCommand = defineRdeCommand({
6719
7765
  },
6720
7766
  "remove-image": {
6721
7767
  type: "boolean",
6722
- description: "Also delete the Docker image",
7768
+ description: "Also delete the Docker image or the downloaded jar",
6723
7769
  default: false
6724
7770
  },
6725
7771
  yes: {
@@ -6745,11 +7791,274 @@ const uninstallCommand = defineRdeCommand({
6745
7791
  }
6746
7792
  });
6747
7793
  //#endregion
7794
+ //#region src/steps/upgrade.ts
7795
+ const BACKUPS_DIR_MODE = 448;
7796
+ const RELEASE_TAG = /^v?[01]\.(\d+\.\d+(?:\.\d+)?)/;
7797
+ function hostDeps(ctx) {
7798
+ return {
7799
+ runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
7800
+ client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
7801
+ timing: DEFAULT_HEALTH_TIMING,
7802
+ releases: releaseLookup(ctx.signal),
7803
+ now: () => /* @__PURE__ */ new Date()
7804
+ };
7805
+ }
7806
+ function imageTag(ref) {
7807
+ const colon = ref.lastIndexOf(":");
7808
+ if (colon === -1 || colon < ref.lastIndexOf("/")) return null;
7809
+ return ref.slice(colon + 1);
7810
+ }
7811
+ function releaseOf(tag) {
7812
+ const match = RELEASE_TAG.exec(tag);
7813
+ if (match === null || match[1] === void 0) return null;
7814
+ return semver.coerce(match[1]);
7815
+ }
7816
+ function downgradeReason(current, targetTag) {
7817
+ if (targetTag === null) return null;
7818
+ const running = releaseOf(current);
7819
+ const target = releaseOf(targetTag);
7820
+ if (running === null || target === null || !semver.lt(target, running)) return null;
7821
+ return `refusing to downgrade from ${current} to ${targetTag}: Metabase migrates its application database forward only; a fresh instance (\`rde uninstall\`, then \`rde init\`) is the way to run an older release`;
7822
+ }
7823
+ function backupStamp(now) {
7824
+ return now.toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
7825
+ }
7826
+ async function ensureRunning(ctx, deps, client) {
7827
+ await startMetabaseStep(() => deps).run(ctx);
7828
+ return runningVersion(client);
7829
+ }
7830
+ function dockerTarget(runtime, to) {
7831
+ const image = to ?? runtime.image;
7832
+ return {
7833
+ kind: "docker",
7834
+ next: {
7835
+ ...runtime,
7836
+ image
7837
+ },
7838
+ label: image,
7839
+ releaseTag: imageTag(image)
7840
+ };
7841
+ }
7842
+ async function jarTarget(ctx, runtime, to, releases) {
7843
+ const spec = to ?? "latest";
7844
+ const parsed = parseJarSpec(spec);
7845
+ if (parsed === null) throw new ConfigError(`--to ${spec} is not latest, a release tag such as v1.63.18, or the path of a .jar file`);
7846
+ let source;
7847
+ if (parsed.kind === "latest") {
7848
+ const version = await releases.latestVersion();
7849
+ ctx.ui.info(`The latest Metabase release is ${version}`);
7850
+ source = {
7851
+ kind: "release",
7852
+ version
7853
+ };
7854
+ } else source = parsed;
7855
+ return {
7856
+ kind: "jar",
7857
+ previous: runtime,
7858
+ next: {
7859
+ ...runtime,
7860
+ source
7861
+ },
7862
+ label: describeJar(source),
7863
+ releaseTag: source.kind === "release" ? source.version : null
7864
+ };
7865
+ }
7866
+ function resolveTarget(ctx, configured, to, releases) {
7867
+ switch (configured.runtime.kind) {
7868
+ case "docker": return Promise.resolve(dockerTarget(configured.runtime, to));
7869
+ case "jar": return jarTarget(ctx, configured.runtime, to, releases);
7870
+ }
7871
+ }
7872
+ async function pullImage(ctx, docker, target) {
7873
+ const image = target.next.image;
7874
+ const spinner = ctx.ui.spinner(`Pulling ${image}`);
7875
+ try {
7876
+ await docker.imagePull(image, { onLine: (line) => spinner.update(line) });
7877
+ } catch (error) {
7878
+ spinner.fail(`Pulling ${image} failed`);
7879
+ throw error;
7880
+ }
7881
+ const id = await docker.imageId(image);
7882
+ if (id === null) {
7883
+ spinner.fail(`Pulled ${image}, but Docker has no such image`);
7884
+ throw new ConfigError(`docker pull ${image} succeeded, yet \`docker image inspect\` finds nothing`);
7885
+ }
7886
+ const running = await docker.containerImageId(target.next.containerName);
7887
+ spinner.stop(`Pulled ${image}`);
7888
+ return id === running;
7889
+ }
7890
+ function jarUnchanged(target, current, jarsDir) {
7891
+ const source = target.next.source;
7892
+ if (source.kind === "release") return source.version === current;
7893
+ return jarPathOf(source, jarsDir) === jarPathOf(target.previous.source, jarsDir);
7894
+ }
7895
+ async function fetchJar(ctx, deps, target, current) {
7896
+ if (jarUnchanged(target, current, deps.runtime.jarsDir)) return true;
7897
+ const spinner = ctx.ui.spinner(`Fetching ${target.label}`);
7898
+ try {
7899
+ await runtimeFromState(target.next, deps.runtime).install({ onProgress: (line) => spinner.update(line) });
7900
+ } catch (error) {
7901
+ spinner.fail(`Fetching ${target.label} failed`);
7902
+ throw error;
7903
+ }
7904
+ spinner.stop(`Fetched ${target.label}`);
7905
+ return false;
7906
+ }
7907
+ function fetchTarget(ctx, deps, target, current) {
7908
+ switch (target.kind) {
7909
+ case "docker": return pullImage(ctx, deps.runtime.docker, target);
7910
+ case "jar": return fetchJar(ctx, deps, target, current);
7911
+ }
7912
+ }
7913
+ const SWITCH_TEXT = {
7914
+ docker: "Metabase stops while the container is recreated from the target image.",
7915
+ jar: "Metabase stops and starts again from the target jar."
7916
+ };
7917
+ function planLines(configured, current, target, backup) {
7918
+ return [
7919
+ `Running: ${current} (${describeRuntime(configured.runtime)})`,
7920
+ `Target: ${target.label}`,
7921
+ `Backup: ${backup}`,
7922
+ "",
7923
+ SWITCH_TEXT[configured.runtime.kind],
7924
+ "Its application database is migrated forward on the first start and cannot be moved back, so",
7925
+ "the whole data directory is copied to the backup path first."
7926
+ ];
7927
+ }
7928
+ async function backUp(ctx, dataDir, backup) {
7929
+ const spinner = ctx.ui.spinner(`Backing up ${dataDir}`);
7930
+ try {
7931
+ await promises.mkdir(instanceBackupsDir(ctx.state.name), {
7932
+ recursive: true,
7933
+ mode: BACKUPS_DIR_MODE
7934
+ });
7935
+ await promises.cp(dataDir, backup, { recursive: true });
7936
+ } catch (error) {
7937
+ spinner.fail("Backup failed; nothing was changed");
7938
+ throw error;
7939
+ }
7940
+ spinner.stop(`Backed up to ${backup}`);
7941
+ }
7942
+ function restoreSteps(runtime, statePath) {
7943
+ switch (runtime.kind) {
7944
+ case "docker": return [
7945
+ ` set runtime.image to "${runtime.image}" in ${statePath}`,
7946
+ ` docker rm -f ${runtime.containerName}`,
7947
+ " rde init --only runtime"
7948
+ ];
7949
+ case "jar": return [` set runtime.source to ${JSON.stringify(runtime.source)} in ${statePath}`];
7950
+ }
7951
+ }
7952
+ function restoreLines(ctx, configured, dataDir, backup) {
7953
+ return [
7954
+ `The data directory as it was before the upgrade is at ${backup}.`,
7955
+ `To go back to ${describeRuntime(configured.runtime)}:`,
7956
+ " rde stop",
7957
+ ` rm -rf ${dataDir} && cp -R ${backup} ${dataDir}`,
7958
+ ...restoreSteps(configured.runtime, instanceStatePath(ctx.state.name)),
7959
+ " rde start"
7960
+ ];
7961
+ }
7962
+ async function switchTo(ctx, deps, target) {
7963
+ const configured = requireConfiguredRuntime(ctx.state);
7964
+ if (configured.runtime.kind === "docker") await deps.runtime.docker.containerRemove(configured.runtime.containerName);
7965
+ await ctx.saveState({
7966
+ ...ctx.state,
7967
+ runtime: target.next,
7968
+ version: null
7969
+ });
7970
+ const spinner = ctx.ui.spinner(`Installing ${target.label}`);
7971
+ try {
7972
+ await runtimeFromState(target.next, deps.runtime).install({ onProgress: (line) => spinner.update(line) });
7973
+ } catch (error) {
7974
+ spinner.fail(`Installing ${target.label} failed`);
7975
+ throw error;
7976
+ }
7977
+ spinner.stop(`Installed ${target.label}`);
7978
+ }
7979
+ async function startUpgraded(ctx, deps, client, previous, backup) {
7980
+ try {
7981
+ return await ensureRunning(ctx, deps, client);
7982
+ } catch (error) {
7983
+ if (error instanceof MetabaseUnreachableError) ctx.ui.note(restoreLines(ctx, previous, deps.runtime.dataDir, backup).join("\n"), "Metabase did not come up on the new version");
7984
+ throw error;
7985
+ }
7986
+ }
7987
+ async function upgrade(ctx, options, deps = hostDeps) {
7988
+ const resolved = deps(ctx);
7989
+ const configured = requireConfiguredRuntime(ctx.state);
7990
+ const client = resolved.client(configured.url, NO_AUTH);
7991
+ const current = await ensureRunning(ctx, resolved, client);
7992
+ const target = await resolveTarget(ctx, configured, options.to, resolved.releases);
7993
+ const refusal = downgradeReason(current, target.releaseTag);
7994
+ if (refusal !== null) throw new ConfigError(refusal);
7995
+ if (await fetchTarget(ctx, resolved, target, current)) {
7996
+ ctx.ui.success(`Already on ${current}: ${target.label} is what the instance runs`);
7997
+ return {
7998
+ outcome: "unchanged",
7999
+ version: current
8000
+ };
8001
+ }
8002
+ const backup = join(instanceBackupsDir(ctx.state.name), backupStamp(resolved.now()));
8003
+ ctx.ui.note(planLines(configured, current, target, backup).join("\n"), "rde upgrade");
8004
+ if (!options.yes) {
8005
+ if (!await ctx.prompts.confirm({
8006
+ message: "Upgrade now?",
8007
+ initialValue: true
8008
+ })) {
8009
+ ctx.ui.info("Nothing changed");
8010
+ return { outcome: "declined" };
8011
+ }
8012
+ }
8013
+ const runtime = runtimeFromState(configured.runtime, resolved.runtime);
8014
+ const stopping = ctx.ui.spinner("Stopping Metabase");
8015
+ await runtime.stop();
8016
+ stopping.stop("Metabase stopped");
8017
+ await backUp(ctx, resolved.runtime.dataDir, backup);
8018
+ await switchTo(ctx, resolved, target);
8019
+ const to = await startUpgraded(ctx, resolved, client, configured, backup);
8020
+ ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
8021
+ return {
8022
+ outcome: "upgraded",
8023
+ from: current,
8024
+ to,
8025
+ backup
8026
+ };
8027
+ }
8028
+ //#endregion
8029
+ //#region src/commands/upgrade.ts
8030
+ const upgradeCommand = defineRdeCommand({
8031
+ meta: {
8032
+ name: "upgrade",
8033
+ description: "Move the instance to a newer Metabase, backing up its data first"
8034
+ },
8035
+ args: {
8036
+ to: {
8037
+ type: "string",
8038
+ description: "Docker: image to upgrade to (default: the instance's image, pulled again); jar: latest, a release tag, or a .jar path (default: latest)"
8039
+ },
8040
+ yes: {
8041
+ type: "boolean",
8042
+ description: "Do not ask for confirmation",
8043
+ default: false
8044
+ }
8045
+ },
8046
+ async run(args) {
8047
+ if (!args.yes && !stdinIsTty()) throw new ConfigError("`rde upgrade` asks for confirmation; pass --yes to upgrade without a terminal");
8048
+ const name = await defaultInstanceName();
8049
+ await requireInstanceState(name);
8050
+ await upgrade(await createWizardContext(name, { signal: interruptSignal }), {
8051
+ to: args.to ?? null,
8052
+ yes: args.yes
8053
+ });
8054
+ }
8055
+ });
8056
+ //#endregion
6748
8057
  //#region src/main.ts
6749
8058
  const main = defineCommand({
6750
8059
  meta: {
6751
8060
  name: "rde",
6752
- version: "0.1.0",
8061
+ version: "0.1.1",
6753
8062
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
6754
8063
  },
6755
8064
  subCommands: {
@@ -6761,6 +8070,7 @@ const main = defineCommand({
6761
8070
  open,
6762
8071
  db,
6763
8072
  doctor,
8073
+ upgrade: upgradeCommand,
6764
8074
  uninstall: uninstallCommand
6765
8075
  }
6766
8076
  });