@metabase/rde 0.1.8 → 0.1.10

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 +6 -32
  2. package/dist/cli.mjs +433 -263
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -1600,6 +1600,10 @@ async function run$1(signal, command, args, options) {
1600
1600
  "pipe"
1601
1601
  ],
1602
1602
  ...options.cwd !== void 0 && { cwd: options.cwd },
1603
+ ...options.addedEnv !== void 0 && { env: {
1604
+ ...hostEnvironment(),
1605
+ ...options.addedEnv
1606
+ } },
1603
1607
  signal: combined,
1604
1608
  killSignal: "SIGKILL"
1605
1609
  });
@@ -2118,9 +2122,9 @@ function adminUserStep(options, deps = hostDeps$12) {
2118
2122
  const client = resolved.client(configured.url, NO_AUTH);
2119
2123
  const properties = await client.session.properties();
2120
2124
  if (properties["has-user-setup"]) {
2121
- if (ctx.state.admin === null) return recordExistingAdmin(ctx, options.variant, client, configured.url);
2125
+ if (ctx.state.admin === null) return { outcome: await recordExistingAdmin(ctx, options.variant, client, configured.url) };
2122
2126
  ctx.ui.info("Metabase already has an admin user");
2123
- return "completed";
2127
+ return { outcome: "completed" };
2124
2128
  }
2125
2129
  const token = properties["setup-token"];
2126
2130
  if (token === null) throw new ConfigError("Metabase has no admin user yet but offers no setup token");
@@ -2159,7 +2163,7 @@ function adminUserStep(options, deps = hostDeps$12) {
2159
2163
  }
2160
2164
  });
2161
2165
  ctx.ui.success(`Admin user ${user.email} created`);
2162
- return "completed";
2166
+ return { outcome: "completed" };
2163
2167
  }
2164
2168
  };
2165
2169
  }
@@ -2635,7 +2639,7 @@ function apiKeyStep(deps = hostDeps$11) {
2635
2639
  if (entry === void 0) throw new ConfigError(`\`mb auth list\` does not show profile ${profile} after login`);
2636
2640
  if (entry.status !== "ok") throw new ConfigError(`mb profile ${profile} reports ${entry.status} after login`);
2637
2641
  ctx.ui.note(usageNote(profile), "Metabase CLI connected");
2638
- return "completed";
2642
+ return { outcome: "completed" };
2639
2643
  }
2640
2644
  };
2641
2645
  }
@@ -2822,12 +2826,18 @@ const MAPPERS = {
2822
2826
  ...leftoverOptions(parsed, ["db", "warehouse"])
2823
2827
  });
2824
2828
  },
2825
- sparksql: (parsed) => build(parsed.database, {
2826
- host: parsed.host ?? param(parsed, "Server"),
2827
- port: portNumber(parsed.port),
2828
- dbname: parsed.database,
2829
- ...leftoverOptions(parsed, ["Server"], "jdbc-flags")
2830
- }),
2829
+ sparksql: (parsed) => {
2830
+ const user = paramIgnoringCase(parsed, ["user"]);
2831
+ const password = paramIgnoringCase(parsed, ["password"]);
2832
+ return build(parsed.database, {
2833
+ host: parsed.host ?? param(parsed, "Server"),
2834
+ port: portNumber(parsed.port),
2835
+ dbname: parsed.database,
2836
+ user: user?.value ?? null,
2837
+ password: password?.value ?? null,
2838
+ ...leftoverOptions(parsed, ["Server", ...keysOf(user, password)], "jdbc-flags")
2839
+ });
2840
+ },
2831
2841
  sqlite: (parsed) => build(null, { db: parsed.path }),
2832
2842
  sqlserver: (parsed) => {
2833
2843
  const database = parsed.database ?? param(parsed, "databaseName");
@@ -2874,9 +2884,12 @@ const MAPPERS = {
2874
2884
  ...leftoverOptions(parsed, ["user", "password"])
2875
2885
  })
2876
2886
  };
2887
+ const DRUID_SCHEME = /url=(?<scheme>https?:\/\/)/i;
2888
+ const DRUID_DEFAULT_SCHEME = "http://";
2877
2889
  function mapDruid(parsed) {
2890
+ const scheme = DRUID_SCHEME.exec(parsed.uri)?.groups?.["scheme"] ?? DRUID_DEFAULT_SCHEME;
2878
2891
  return build(null, {
2879
- host: parsed.host,
2892
+ host: parsed.host === null ? null : `${scheme}${parsed.host}`,
2880
2893
  port: portNumber(parsed.port)
2881
2894
  });
2882
2895
  }
@@ -3266,6 +3279,43 @@ function withoutUnasked(fields, details) {
3266
3279
  function promptableFields(fields, details) {
3267
3280
  return visibleFields(fields, details).filter(isPromptable);
3268
3281
  }
3282
+ const SECRET_KINDS$1 = /* @__PURE__ */ new Set(["password", "file"]);
3283
+ function answerOf(field, details) {
3284
+ const kind = promptKindFor(field);
3285
+ const current = details[field.name];
3286
+ if (kind === "boolean") {
3287
+ const value = typeof current === "boolean" ? current : Boolean(field.default);
3288
+ return {
3289
+ name: field.name,
3290
+ value,
3291
+ secret: false
3292
+ };
3293
+ }
3294
+ if (kind === "select") {
3295
+ const value = current ?? field.default;
3296
+ return value === void 0 || value === null ? null : {
3297
+ name: field.name,
3298
+ value: String(value),
3299
+ secret: false
3300
+ };
3301
+ }
3302
+ if (current === void 0 || current === "") return {
3303
+ name: field.name,
3304
+ value: "",
3305
+ secret: false
3306
+ };
3307
+ return {
3308
+ name: field.name,
3309
+ value: current,
3310
+ secret: SECRET_KINDS$1.has(kind)
3311
+ };
3312
+ }
3313
+ function answeredFields(fields, details) {
3314
+ return promptableFields(fields, details).flatMap((field) => {
3315
+ const answer = answerOf(field, details);
3316
+ return answer === null ? [] : [answer];
3317
+ });
3318
+ }
3269
3319
  async function fillRequiredFields(deps, fields, details) {
3270
3320
  let current = details;
3271
3321
  const asked = /* @__PURE__ */ new Set();
@@ -3525,18 +3575,45 @@ function databaseListingLines(listing) {
3525
3575
  //#region src/steps/databases/flow.ts
3526
3576
  const DATABASE_KEYS = {
3527
3577
  connection: "databases.connection",
3528
- detectedEngine: "databases.detected-engine",
3529
- engine: "databases.engine",
3530
- skip: "databases.skip",
3531
- mode: "databases.mode",
3532
- uri: "databases.uri",
3533
- name: "databases.name",
3534
- action: "databases.action",
3535
- editField: "databases.edit-field",
3536
- testAction: "databases.test-action",
3537
- switchEngine: "databases.switch-engine",
3538
- another: "databases.another"
3578
+ skip: "databases.skip"
3579
+ };
3580
+ const DATABASE_KEY_NAMES = {
3581
+ detectedEngine: "detected-engine",
3582
+ engine: "engine",
3583
+ mode: "mode",
3584
+ uri: "uri",
3585
+ name: "name",
3586
+ action: "action",
3587
+ editField: "edit-field",
3588
+ testAction: "test-action",
3589
+ switchEngine: "switch-engine",
3590
+ another: "another"
3539
3591
  };
3592
+ function sameDetails(left, right) {
3593
+ return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].every((key) => left[key] === right[key]);
3594
+ }
3595
+ function addedDatabase(chosen, model, name) {
3596
+ const pastedAsIs = model.uri !== null && model.pasted !== null && sameDetails(model.details, model.pasted);
3597
+ return {
3598
+ engine: chosen.engine.key,
3599
+ name,
3600
+ uri: pastedAsIs ? model.uri : null,
3601
+ fields: pastedAsIs ? [] : answeredFields(chosen.fields, model.details)
3602
+ };
3603
+ }
3604
+ function databaseKey(ordinal, name) {
3605
+ return `databases.${ordinal}.${DATABASE_KEY_NAMES[name]}`;
3606
+ }
3607
+ const DATABASE_KEY = /^databases\.(?<ordinal>\d+)\.(?<name>[a-z-]+)$/;
3608
+ function parseDatabaseKey(key) {
3609
+ const groups = DATABASE_KEY.exec(key)?.groups;
3610
+ const ordinal = groups?.["ordinal"];
3611
+ const name = groups?.["name"];
3612
+ return ordinal === void 0 || name === void 0 ? null : {
3613
+ ordinal: Number(ordinal),
3614
+ name
3615
+ };
3616
+ }
3540
3617
  const FINISH = "finish";
3541
3618
  const RETRY = "try-again";
3542
3619
  const BAD_REQUEST_STATUS$1 = 400;
@@ -3558,13 +3635,16 @@ const FRESH$1 = {
3558
3635
  name: null,
3559
3636
  fieldErrors: null
3560
3637
  };
3638
+ function ordinalOf(run) {
3639
+ return run.created.length + 1;
3640
+ }
3561
3641
  function fieldDeps(run) {
3562
3642
  const { prompts, ui, readTextFile } = run.deps;
3563
3643
  return {
3564
3644
  prompts,
3565
3645
  ui,
3566
3646
  readTextFile,
3567
- ordinal: run.created.length + 1
3647
+ ordinal: ordinalOf(run)
3568
3648
  };
3569
3649
  }
3570
3650
  async function orFailure(failure, ask) {
@@ -3612,7 +3692,7 @@ function isCreatableEntry(entry) {
3612
3692
  }
3613
3693
  async function pickDetectedEngine(run, candidates) {
3614
3694
  const picked = await run.deps.prompts.selectOrBack({
3615
- key: DATABASE_KEYS.detectedEngine,
3695
+ key: databaseKey(ordinalOf(run), "detectedEngine"),
3616
3696
  message: "Which database is it?",
3617
3697
  choices: candidates.map((entry) => ({
3618
3698
  value: entry.key,
@@ -3679,7 +3759,7 @@ function pickEngineScreen(run) {
3679
3759
  };
3680
3760
  for (;;) {
3681
3761
  const picked = await run.deps.prompts.select({
3682
- key: DATABASE_KEYS.engine,
3762
+ key: databaseKey(ordinalOf(run), "engine"),
3683
3763
  message: "Which database engine?",
3684
3764
  choices: [...run.deps.catalog.list().map(engineChoice), finishChoice],
3685
3765
  ...model.engine !== null && { initialValue: model.engine.key }
@@ -3720,7 +3800,7 @@ async function chooseAfterParseError(run, engine, uri, error) {
3720
3800
  hint: "this looks like one of its connection strings"
3721
3801
  }));
3722
3802
  const picked = await orFailure(error.message, () => run.deps.prompts.selectOrBack({
3723
- key: DATABASE_KEYS.switchEngine,
3803
+ key: databaseKey(ordinalOf(run), "switchEngine"),
3724
3804
  message: "What now?",
3725
3805
  choices: [...switches, {
3726
3806
  value: RETRY,
@@ -3774,7 +3854,7 @@ function pasteUriScreen(run) {
3774
3854
  for (;;) {
3775
3855
  if (engine.exampleUri === null) return manualEntry(run, model, engine);
3776
3856
  const mode = await run.deps.prompts.selectOrBack({
3777
- key: DATABASE_KEYS.mode,
3857
+ key: databaseKey(ordinalOf(run), "mode"),
3778
3858
  message: `How do you want to connect to ${engine.displayName}?`,
3779
3859
  choices: [{
3780
3860
  value: "paste",
@@ -3787,7 +3867,7 @@ function pasteUriScreen(run) {
3787
3867
  if (mode === BACK) return { type: "back" };
3788
3868
  if (mode === "manual") return manualEntry(run, model, engine);
3789
3869
  const uri = await run.deps.prompts.textOrBack({
3790
- key: DATABASE_KEYS.uri,
3870
+ key: databaseKey(ordinalOf(run), "uri"),
3791
3871
  message: "Connection string",
3792
3872
  placeholder: engine.exampleUri,
3793
3873
  ...draft !== null && { initialValue: draft }
@@ -3852,7 +3932,7 @@ function reviewNote(run, chosen, model) {
3852
3932
  async function promptName(run, model, engine) {
3853
3933
  const suggested = model.name ?? model.suggestedName ?? engine.displayName;
3854
3934
  return run.deps.prompts.text({
3855
- key: DATABASE_KEYS.name,
3935
+ key: databaseKey(ordinalOf(run), "name"),
3856
3936
  message: "Display name",
3857
3937
  defaultValue: suggested,
3858
3938
  placeholder: suggested
@@ -3869,7 +3949,7 @@ async function editOneField(run, fields, details) {
3869
3949
  };
3870
3950
  });
3871
3951
  const picked = await run.deps.prompts.selectOrBack({
3872
- key: DATABASE_KEYS.editField,
3952
+ key: databaseKey(ordinalOf(run), "editField"),
3873
3953
  message: "Which field?",
3874
3954
  choices
3875
3955
  });
@@ -3912,7 +3992,7 @@ function reviewAttempt(run) {
3912
3992
  reviewNote(run, chosen, current);
3913
3993
  if (name === null) name = await promptName(run, current, chosen.engine);
3914
3994
  const action = await run.deps.prompts.selectOrBack({
3915
- key: DATABASE_KEYS.action,
3995
+ key: databaseKey(ordinalOf(run), "action"),
3916
3996
  message: `Add ${name}?`,
3917
3997
  choices: [
3918
3998
  {
@@ -3977,7 +4057,7 @@ function testScreen(run) {
3977
4057
  const lines = fieldErrorLines(chosen.fields, errors);
3978
4058
  if (lines.length > 0) run.deps.ui.warn(lines.join("\n"));
3979
4059
  const action = await orFailure(failure, () => run.deps.prompts.selectOrBack({
3980
- key: DATABASE_KEYS.testAction,
4060
+ key: databaseKey(ordinalOf(run), "testAction"),
3981
4061
  message: "What now?",
3982
4062
  choices: [{
3983
4063
  value: "fix",
@@ -4038,6 +4118,7 @@ function createScreen(run) {
4038
4118
  }
4039
4119
  run.created.push(database);
4040
4120
  spinner.stop(`Added ${database.name}. Initial sync is running in the background; \`rde status\` shows progress.`);
4121
+ run.added.push(addedDatabase(chosen, model, model.name));
4041
4122
  await run.deps.onCreated(database);
4042
4123
  return {
4043
4124
  type: "next",
@@ -4049,7 +4130,7 @@ function createScreen(run) {
4049
4130
  function anotherScreen(run) {
4050
4131
  return async (model) => {
4051
4132
  if (await run.deps.prompts.select({
4052
- key: DATABASE_KEYS.another,
4133
+ key: databaseKey(run.created.length, "another"),
4053
4134
  message: "Anything else?",
4054
4135
  choices: [{
4055
4136
  value: "another",
@@ -4072,7 +4153,8 @@ function anotherScreen(run) {
4072
4153
  async function runDatabaseFlow(deps) {
4073
4154
  const run = {
4074
4155
  deps,
4075
- created: []
4156
+ created: [],
4157
+ added: []
4076
4158
  };
4077
4159
  await runFlow({
4078
4160
  "connection-string": connectionStringScreen(run),
@@ -4083,7 +4165,10 @@ async function runDatabaseFlow(deps) {
4083
4165
  create: createScreen(run),
4084
4166
  another: anotherScreen(run)
4085
4167
  }, START_SCREEN[deps.start], FRESH$1);
4086
- return { created: run.created };
4168
+ return {
4169
+ created: run.created,
4170
+ added: run.added
4171
+ };
4087
4172
  }
4088
4173
  //#endregion
4089
4174
  //#region src/tools/docker.ts
@@ -4131,9 +4216,9 @@ function createArgs(options) {
4131
4216
  "--label",
4132
4217
  `${HOME_LABEL}=${options.home}`,
4133
4218
  "-p",
4134
- `${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`
4219
+ `${LOOPBACK_HOST}:${options.port.host ?? ""}:${options.port.container}`
4135
4220
  ];
4136
- if (options.port.alsoOn !== null) args.push("-p", `${options.port.alsoOn}:${options.port.host}:${options.port.container}`);
4221
+ if (options.port.alsoOn !== null) args.push("-p", `${options.port.alsoOn}:${options.port.host ?? ""}:${options.port.container}`);
4137
4222
  if (options.mount !== null) args.push("-v", `${options.mount.source}:${options.mount.target}`);
4138
4223
  for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
4139
4224
  if (options.hostUser !== null) {
@@ -4244,6 +4329,33 @@ function dockerTool(runner) {
4244
4329
  async function containerRemove(name) {
4245
4330
  await docker(["rm", name]);
4246
4331
  }
4332
+ async function containerForceRemove(name) {
4333
+ await docker([
4334
+ "rm",
4335
+ "-f",
4336
+ name
4337
+ ]);
4338
+ }
4339
+ async function containerLoopbackPort(name, containerPort) {
4340
+ const args = [
4341
+ "port",
4342
+ name,
4343
+ `${containerPort}/tcp`
4344
+ ];
4345
+ const result = await docker(args);
4346
+ const prefix = `${LOOPBACK_HOST}:`;
4347
+ const line = result.stdout.split("\n").find((entry) => entry.startsWith(prefix));
4348
+ if (line !== void 0) {
4349
+ const port = Number.parseInt(line.slice(prefix.length), 10);
4350
+ if (Number.isInteger(port)) return port;
4351
+ }
4352
+ throw new ExternalToolError({
4353
+ tool: DOCKER,
4354
+ argv: args,
4355
+ exitCode: 0,
4356
+ stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
4357
+ });
4358
+ }
4247
4359
  async function containerExecSucceeds(name, command) {
4248
4360
  return (await runner.run(DOCKER, [
4249
4361
  "exec",
@@ -4270,6 +4382,8 @@ function dockerTool(runner) {
4270
4382
  containerStart,
4271
4383
  containerStop,
4272
4384
  containerRemove,
4385
+ containerForceRemove,
4386
+ containerLoopbackPort,
4273
4387
  containerExecSucceeds,
4274
4388
  containerLogs
4275
4389
  };
@@ -4294,6 +4408,21 @@ const METABASE_CONTENT_DIRS = [
4294
4408
  "snippets",
4295
4409
  "transforms"
4296
4410
  ];
4411
+ const TOKEN_USERNAMES = { "bitbucket.org": "x-token-auth" };
4412
+ const DEFAULT_TOKEN_USERNAME = "x-access-token";
4413
+ function credentialEnv(credentials) {
4414
+ const { origin, hostname } = new URL(credentials.url);
4415
+ const username = TOKEN_USERNAMES[hostname] ?? DEFAULT_TOKEN_USERNAME;
4416
+ const basic = Buffer.from(`${username}:${credentials.token}`).toString("base64");
4417
+ return {
4418
+ GIT_CONFIG_COUNT: "2",
4419
+ GIT_CONFIG_KEY_0: `http.${origin}/.extraHeader`,
4420
+ GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`,
4421
+ GIT_CONFIG_KEY_1: "credential.helper",
4422
+ GIT_CONFIG_VALUE_1: "",
4423
+ GIT_TERMINAL_PROMPT: "0"
4424
+ };
4425
+ }
4297
4426
  async function directoryExists(path) {
4298
4427
  try {
4299
4428
  return (await promises.stat(path)).isDirectory();
@@ -4302,8 +4431,8 @@ async function directoryExists(path) {
4302
4431
  }
4303
4432
  }
4304
4433
  function gitTool(runner) {
4305
- async function git(args, cwd) {
4306
- const result = await runner.run(GIT, args, cwd === void 0 ? {} : { cwd });
4434
+ async function git(args, options = {}) {
4435
+ const result = await runner.run(GIT, args, options);
4307
4436
  if (result.exitCode !== 0) throw toolFailure(GIT, args, result);
4308
4437
  return result;
4309
4438
  }
@@ -4349,7 +4478,7 @@ function gitTool(runner) {
4349
4478
  return result.exitCode === 0 && result.stdout.trim().length > 0;
4350
4479
  }
4351
4480
  async function commitAll(dir, message) {
4352
- await git(["add", "--all"], dir);
4481
+ await git(["add", "--all"], { cwd: dir });
4353
4482
  await git([
4354
4483
  ...await hasIdentity(dir) ? [] : [
4355
4484
  "-c",
@@ -4360,7 +4489,7 @@ function gitTool(runner) {
4360
4489
  "commit",
4361
4490
  "--message",
4362
4491
  message
4363
- ], dir);
4492
+ ], { cwd: dir });
4364
4493
  }
4365
4494
  async function addRemote(dir, name, url) {
4366
4495
  await git([
@@ -4368,22 +4497,33 @@ function gitTool(runner) {
4368
4497
  "add",
4369
4498
  name,
4370
4499
  url
4371
- ], dir);
4500
+ ], { cwd: dir });
4501
+ }
4502
+ async function clone(credentials, dir) {
4503
+ await git([
4504
+ "clone",
4505
+ "--quiet",
4506
+ credentials.url,
4507
+ dir
4508
+ ], { addedEnv: credentialEnv(credentials) });
4372
4509
  }
4373
- async function push(dir, remote, branch) {
4510
+ async function push(dir, remote, branch, credentials) {
4374
4511
  await git([
4375
4512
  "push",
4376
4513
  "--set-upstream",
4377
4514
  remote,
4378
4515
  branch
4379
- ], dir);
4516
+ ], {
4517
+ cwd: dir,
4518
+ ...credentials !== void 0 && { addedEnv: credentialEnv(credentials) }
4519
+ });
4380
4520
  }
4381
4521
  async function currentBranch(dir) {
4382
4522
  return (await git([
4383
4523
  "rev-parse",
4384
4524
  "--abbrev-ref",
4385
4525
  "HEAD"
4386
- ], dir)).stdout.trim();
4526
+ ], { cwd: dir })).stdout.trim();
4387
4527
  }
4388
4528
  async function remoteUrl(dir, name) {
4389
4529
  const result = await runner.run(GIT, [
@@ -4406,6 +4546,7 @@ function gitTool(runner) {
4406
4546
  hasCommits,
4407
4547
  commitAll,
4408
4548
  addRemote,
4549
+ clone,
4409
4550
  push,
4410
4551
  currentBranch,
4411
4552
  remoteUrl,
@@ -4459,10 +4600,11 @@ function hostDeps$10(ctx) {
4459
4600
  taskPollIntervalMs: TASK_POLL_INTERVAL_MS
4460
4601
  };
4461
4602
  }
4603
+ const SKIP_GIT_SYNC = "skip";
4462
4604
  const FRESH = {
4463
- workdir: null,
4464
4605
  remoteKind: null,
4465
- hostedUrl: null
4606
+ hostedUrl: null,
4607
+ workdir: null
4466
4608
  };
4467
4609
  function expandHome(path) {
4468
4610
  return path.startsWith(HOME_PREFIX) ? join(homedir(), path.slice(2)) : path;
@@ -4478,6 +4620,10 @@ async function directoryState(path) {
4478
4620
  if (!stat.isDirectory()) return "file";
4479
4621
  return (await promises.readdir(path)).length === 0 ? "empty" : "populated";
4480
4622
  }
4623
+ async function isCloneTarget(path) {
4624
+ const state = await directoryState(path);
4625
+ return state === "missing" || state === "empty";
4626
+ }
4481
4627
  async function initTargetProblem(run, path) {
4482
4628
  switch (await directoryState(path)) {
4483
4629
  case "missing":
@@ -4488,78 +4634,132 @@ async function initTargetProblem(run, path) {
4488
4634
  case "populated": return await run.git.isRepo(path) ? null : `${path} is not empty and not a git repository; pick an empty directory or a repository`;
4489
4635
  }
4490
4636
  }
4637
+ async function cloneTargetProblem(path) {
4638
+ if (await isCloneTarget(path)) return null;
4639
+ return await directoryState(path) === "file" ? `${path} is not a directory` : `${path} is not empty; pick an empty directory, or use an existing repository`;
4640
+ }
4491
4641
  async function existingRepoProblem(run, path) {
4492
4642
  return await run.git.isRepo(path) ? null : `${path} is not a git repository`;
4493
4643
  }
4494
- const PATH_PROMPTS = {
4495
- new: "Directory for the repository (created if missing)",
4496
- existing: "Path of the existing repository"
4644
+ const FRESH_WORKDIRS = {
4645
+ "local-bare": {
4646
+ here: "Initialize a repository here",
4647
+ elsewhere: "Initialize in another directory",
4648
+ pathMessage: "Directory for the repository (created if missing)",
4649
+ problem: initTargetProblem
4650
+ },
4651
+ hosted: {
4652
+ here: "Clone the repository here",
4653
+ elsewhere: "Clone into another directory",
4654
+ pathMessage: "Directory to clone into (created if missing)",
4655
+ problem: (_run, path) => cloneTargetProblem(path)
4656
+ }
4497
4657
  };
4498
- async function askPath(run, model, kind) {
4658
+ async function askPath(run, question) {
4499
4659
  for (;;) {
4500
4660
  const typed = await run.ctx.prompts.textOrBack({
4501
4661
  key: GIT_SYNC_KEYS.path,
4502
- message: PATH_PROMPTS[kind],
4503
- ...model.workdir !== null && { initialValue: model.workdir }
4662
+ message: question.message
4504
4663
  });
4505
4664
  if (typed === BACK) return BACK;
4506
4665
  const path = resolve(run.deps.cwd, expandHome(typed));
4507
- const problem = kind === "new" ? await initTargetProblem(run, path) : await existingRepoProblem(run, path);
4666
+ const problem = await question.problem(path);
4508
4667
  if (problem === null) return path;
4509
4668
  run.ctx.ui.warn(problem);
4510
4669
  }
4511
4670
  }
4671
+ function remoteScreen(run) {
4672
+ return async (model) => {
4673
+ const choice = await run.ctx.prompts.select({
4674
+ key: GIT_SYNC_KEYS.remote,
4675
+ message: "Where should Metabase push the content?",
4676
+ choices: [
4677
+ {
4678
+ value: "local-bare",
4679
+ label: "Local remote inside ~/.rde",
4680
+ hint: "no account needed, recommended"
4681
+ },
4682
+ {
4683
+ value: "hosted",
4684
+ label: "Hosted remote (GitHub, GitLab, Bitbucket…)",
4685
+ hint: "needs a token"
4686
+ },
4687
+ {
4688
+ value: SKIP_GIT_SYNC,
4689
+ label: "Skip for now",
4690
+ hint: "rde init --only git-sync sets it up later"
4691
+ }
4692
+ ],
4693
+ ...model.remoteKind !== null && { initialValue: model.remoteKind }
4694
+ });
4695
+ if (choice === "skip") return {
4696
+ type: "done",
4697
+ model: FRESH
4698
+ };
4699
+ const next = {
4700
+ ...model,
4701
+ remoteKind: choice
4702
+ };
4703
+ return {
4704
+ type: "next",
4705
+ to: choice === "hosted" ? "hosted" : "workdir",
4706
+ model: next
4707
+ };
4708
+ };
4709
+ }
4512
4710
  function workdirScreen(run) {
4513
4711
  return async (model) => {
4712
+ if (model.remoteKind === null) throw new Error("no remote before the workdir screen");
4713
+ const fresh = FRESH_WORKDIRS[model.remoteKind];
4714
+ const questions = {
4715
+ new: {
4716
+ message: fresh.pathMessage,
4717
+ problem: (path) => fresh.problem(run, path)
4718
+ },
4719
+ existing: {
4720
+ message: "Path of the existing repository",
4721
+ problem: (path) => existingRepoProblem(run, path)
4722
+ }
4723
+ };
4514
4724
  for (;;) {
4515
- const choice = await run.ctx.prompts.select({
4725
+ const choice = await run.ctx.prompts.selectOrBack({
4516
4726
  key: GIT_SYNC_KEYS.location,
4517
4727
  message: "Where should the Metabase content repository live?",
4518
4728
  choices: [
4519
4729
  {
4520
4730
  value: "here",
4521
- label: "Initialize a repository here",
4731
+ label: fresh.here,
4522
4732
  hint: run.deps.cwd
4523
4733
  },
4524
4734
  {
4525
4735
  value: "new",
4526
- label: "Initialize in another directory"
4736
+ label: fresh.elsewhere
4527
4737
  },
4528
4738
  {
4529
4739
  value: "existing",
4530
4740
  label: "Use an existing repository"
4531
- },
4532
- {
4533
- value: "skip",
4534
- label: "Skip for now",
4535
- hint: "rde init --only git-sync sets it up later"
4536
4741
  }
4537
4742
  ]
4538
4743
  });
4539
- if (choice === "skip") return {
4540
- type: "done",
4541
- model: FRESH
4542
- };
4744
+ if (choice === BACK) return { type: "back" };
4543
4745
  if (choice === "here") {
4544
- const problem = await initTargetProblem(run, run.deps.cwd);
4746
+ const problem = await fresh.problem(run, run.deps.cwd);
4545
4747
  if (problem !== null) {
4546
4748
  run.ctx.ui.warn(problem);
4547
4749
  continue;
4548
4750
  }
4549
4751
  return {
4550
- type: "next",
4551
- to: "remote",
4752
+ type: "done",
4552
4753
  model: {
4553
4754
  ...model,
4554
4755
  workdir: run.deps.cwd
4555
4756
  }
4556
4757
  };
4557
4758
  }
4558
- const path = await askPath(run, model, choice);
4759
+ const path = await askPath(run, questions[choice]);
4559
4760
  if (path === BACK) continue;
4560
4761
  return {
4561
- type: "next",
4562
- to: "remote",
4762
+ type: "done",
4563
4763
  model: {
4564
4764
  ...model,
4565
4765
  workdir: path
@@ -4568,37 +4768,6 @@ function workdirScreen(run) {
4568
4768
  }
4569
4769
  };
4570
4770
  }
4571
- function remoteScreen(run) {
4572
- return async (model) => {
4573
- const kind = await run.ctx.prompts.selectOrBack({
4574
- key: GIT_SYNC_KEYS.remote,
4575
- message: "Where should Metabase push the content?",
4576
- choices: [{
4577
- value: "local-bare",
4578
- label: "Local remote inside ~/.rde",
4579
- hint: "no account needed, recommended"
4580
- }, {
4581
- value: "hosted",
4582
- label: "Hosted remote (GitHub, GitLab, Bitbucket…)",
4583
- hint: "needs a token"
4584
- }],
4585
- ...model.remoteKind !== null && { initialValue: model.remoteKind }
4586
- });
4587
- if (kind === BACK) return { type: "back" };
4588
- const next = {
4589
- ...model,
4590
- remoteKind: kind
4591
- };
4592
- return kind === "hosted" ? {
4593
- type: "next",
4594
- to: "hosted",
4595
- model: next
4596
- } : {
4597
- type: "done",
4598
- model: next
4599
- };
4600
- };
4601
- }
4602
4771
  function validateHttpsUrl(value) {
4603
4772
  return value.length === 0 || value.startsWith(HTTPS_PREFIX) ? void 0 : `The repository URL must start with ${HTTPS_PREFIX}`;
4604
4773
  }
@@ -4639,7 +4808,8 @@ function hostedScreen(run) {
4639
4808
  spinner.stop("Metabase can reach the repository");
4640
4809
  run.ctx.session.gitToken.set(token);
4641
4810
  return {
4642
- type: "done",
4811
+ type: "next",
4812
+ to: "workdir",
4643
4813
  model: {
4644
4814
  ...model,
4645
4815
  hostedUrl: url
@@ -4706,9 +4876,34 @@ async function configureLocalBare(run, runtime, workdir) {
4706
4876
  }
4707
4877
  };
4708
4878
  }
4709
- async function configureHosted(run, workdir, url) {
4879
+ async function cloneHosted(run, workdir, credentials) {
4880
+ const spinner = run.ctx.ui.spinner(`Cloning ${credentials.url}…`);
4881
+ try {
4882
+ await run.git.clone(credentials, workdir);
4883
+ } catch (error) {
4884
+ spinner.fail("Could not clone the repository");
4885
+ throw error;
4886
+ }
4887
+ spinner.stop(`Cloned into ${workdir}`);
4888
+ if (await run.git.hasCommits(workdir)) return {
4889
+ workdir,
4890
+ branch: await run.git.currentBranch(workdir)
4891
+ };
4892
+ const prepared = await prepareWorkdir(run, workdir);
4893
+ await run.git.push(workdir, ORIGIN, prepared.branch, credentials);
4894
+ return prepared;
4895
+ }
4896
+ async function adoptRepository(run, workdir, url) {
4710
4897
  const prepared = await prepareWorkdir(run, workdir);
4711
4898
  await ensureRemote(run, workdir, url);
4899
+ return prepared;
4900
+ }
4901
+ async function configureHosted(run, workdir, url) {
4902
+ const token = run.ctx.session.gitToken.get();
4903
+ const prepared = await isCloneTarget(workdir) ? await cloneHosted(run, workdir, {
4904
+ url,
4905
+ token
4906
+ }) : await adoptRepository(run, workdir, url);
4712
4907
  return {
4713
4908
  gitRemote: {
4714
4909
  kind: "hosted",
@@ -4718,7 +4913,7 @@ async function configureHosted(run, workdir, url) {
4718
4913
  },
4719
4914
  settings: {
4720
4915
  "remote-sync-url": url,
4721
- "remote-sync-token": run.ctx.session.gitToken.get(),
4916
+ "remote-sync-token": token,
4722
4917
  "remote-sync-type": "read-write",
4723
4918
  "remote-sync-branch": prepared.branch
4724
4919
  }
@@ -4836,10 +5031,10 @@ async function runGitSync(ctx, deps) {
4836
5031
  })
4837
5032
  };
4838
5033
  const flow = await runFlow({
4839
- workdir: workdirScreen(run),
4840
5034
  remote: remoteScreen(run),
4841
- hosted: hostedScreen(run)
4842
- }, "workdir", FRESH);
5035
+ hosted: hostedScreen(run),
5036
+ workdir: workdirScreen(run)
5037
+ }, "remote", FRESH);
4843
5038
  if (flow.type === "exited" || !isChosen(flow.model)) {
4844
5039
  ctx.ui.info("Skipping remote sync; run `rde init --only git-sync` to set it up later");
4845
5040
  return "skipped";
@@ -4865,8 +5060,8 @@ function gitSyncStep(deps = hostDeps$10) {
4865
5060
  throw error;
4866
5061
  }
4867
5062
  },
4868
- run(ctx) {
4869
- return runGitSync(ctx, deps(ctx));
5063
+ async run(ctx) {
5064
+ return { outcome: await runGitSync(ctx, deps(ctx)) };
4870
5065
  }
4871
5066
  };
4872
5067
  }
@@ -4874,7 +5069,7 @@ function gitSyncStep(deps = hostDeps$10) {
4874
5069
  //#region src/tools/skills-cli.ts
4875
5070
  const NPX = "npx";
4876
5071
  const SKILLS_PACKAGE = "skills@latest";
4877
- const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-generic-skills";
5072
+ const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
4878
5073
  const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
4879
5074
  const DATA_APP_SKILLS_SOURCE = "metabase/metabase/skills#master";
4880
5075
  const DATA_APP_SKILL_NAMES = [
@@ -5112,7 +5307,7 @@ function agentsSkillsStep(options, deps = hostDeps$9) {
5112
5307
  const selection = await selectAgents(ctx, all, options);
5113
5308
  if (selection.agents.length === 0) {
5114
5309
  ctx.ui.info(skipLine(all, selection.source));
5115
- return "skipped";
5310
+ return { outcome: "skipped" };
5116
5311
  }
5117
5312
  const skills = skillsCli(resolved.runner);
5118
5313
  const agents = selection.agents.map((agent) => agent.skillsCliName);
@@ -5124,7 +5319,7 @@ function agentsSkillsStep(options, deps = hostDeps$9) {
5124
5319
  agents,
5125
5320
  output: ctx.ui.childOutput
5126
5321
  });
5127
- const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-generic-skills" ? "" : ` from ${resolved.skillSource}`;
5322
+ const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${resolved.skillSource}`;
5128
5323
  ctx.ui.info(`Installing the rde skills ${forWhom}${from}`);
5129
5324
  await skills.add({
5130
5325
  source: resolved.skillSource,
@@ -5137,7 +5332,7 @@ function agentsSkillsStep(options, deps = hostDeps$9) {
5137
5332
  if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
5138
5333
  ctx.ui.note(installedLines(requiredEntries(installed), selection.agents), "Skills installed");
5139
5334
  ctx.ui.info(MECHANICS_NOTE);
5140
- return "completed";
5335
+ return { outcome: "completed" };
5141
5336
  }
5142
5337
  };
5143
5338
  }
@@ -5225,15 +5420,23 @@ async function connectDatabases(ctx, deps, start) {
5225
5420
  });
5226
5421
  return {
5227
5422
  existing: existing.data.length,
5228
- created: result.created.length
5423
+ created: result.created.length,
5424
+ added: result.added
5229
5425
  };
5230
5426
  }
5231
5427
  async function addDatabases(ctx, deps) {
5232
5428
  const connected = await connectDatabases(ctx, deps, "engine");
5233
- return connected.existing + connected.created > 0 ? "completed" : "skipped";
5429
+ return {
5430
+ outcome: connected.existing + connected.created > 0 ? "completed" : "skipped",
5431
+ result: { addedDatabases: connected.added }
5432
+ };
5234
5433
  }
5235
5434
  async function askForOwnDatabase(ctx, deps) {
5236
- return (await connectDatabases(ctx, deps, "connection-string")).created > 0 ? "completed" : "skipped";
5435
+ const connected = await connectDatabases(ctx, deps, "connection-string");
5436
+ return {
5437
+ outcome: connected.created > 0 ? "completed" : "skipped",
5438
+ result: { addedDatabases: connected.added }
5439
+ };
5237
5440
  }
5238
5441
  function hostDeps$8(ctx) {
5239
5442
  return hostDatabasesDeps(ctx.signal);
@@ -5274,9 +5477,12 @@ function databasesStep(options, deps = hostDeps$8) {
5274
5477
  throw error;
5275
5478
  }
5276
5479
  },
5277
- run(ctx) {
5278
- if (options.request === "ask-once") return askForOwnDatabase(ctx, deps(ctx));
5279
- return addDatabases(ctx, deps(ctx));
5480
+ async run(ctx) {
5481
+ switch (options.request) {
5482
+ case "ask-once": return askForOwnDatabase(ctx, deps(ctx));
5483
+ case "named": return addDatabases(ctx, deps(ctx));
5484
+ case "sample-stands-in": return { outcome: "skipped" };
5485
+ }
5280
5486
  }
5281
5487
  };
5282
5488
  }
@@ -5420,7 +5626,7 @@ function installMbStep(deps = hostDeps$7) {
5420
5626
  const before = await inspectMb(resolved.runner);
5421
5627
  if (before.installed && before.satisfies) {
5422
5628
  ctx.ui.success(`mb ${before.version} is installed`);
5423
- return "completed";
5629
+ return { outcome: "completed" };
5424
5630
  }
5425
5631
  if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
5426
5632
  const install = mbInstallCommand(resolved.installedWith);
@@ -5429,7 +5635,7 @@ function installMbStep(deps = hostDeps$7) {
5429
5635
  const after = await inspectMb(resolved.runner);
5430
5636
  if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
5431
5637
  ctx.ui.success(`mb ${after.version} is installed`);
5432
- return "completed";
5638
+ return { outcome: "completed" };
5433
5639
  }
5434
5640
  };
5435
5641
  }
@@ -5487,7 +5693,7 @@ function licenseStep(deps = hostDeps$6) {
5487
5693
  });
5488
5694
  if (token.length === 0) {
5489
5695
  ctx.ui.info("Skipping the license; remote sync needs one. Run `rde init --from license` to add it later");
5490
- return "skipped";
5696
+ return { outcome: "skipped" };
5491
5697
  }
5492
5698
  ctx.session.licenseToken.set(token);
5493
5699
  const client = deps(ctx).client(configured.url, {
@@ -5518,7 +5724,7 @@ function licenseStep(deps = hostDeps$6) {
5518
5724
  });
5519
5725
  if (status.features.includes("remote-sync")) ctx.ui.info("Remote sync is included; the git step will set it up");
5520
5726
  else ctx.ui.warn("This license does not include remote sync; the git step will be skipped");
5521
- return "completed";
5727
+ return { outcome: "completed" };
5522
5728
  }
5523
5729
  };
5524
5730
  }
@@ -5742,7 +5948,7 @@ function preflightStep(deps = hostDeps$5) {
5742
5948
  const checks = await runChecks(resolved, resolved.platform);
5743
5949
  ctx.ui.note(checkLines(checks).join("\n"), "This machine");
5744
5950
  if (checks.some(blocks)) throw new ConfigError("Fix the items above and run rde init again");
5745
- return "completed";
5951
+ return { outcome: "completed" };
5746
5952
  }
5747
5953
  };
5748
5954
  }
@@ -6355,7 +6561,7 @@ function runtimeStep(options, deps = hostDeps$4) {
6355
6561
  throw error;
6356
6562
  }
6357
6563
  spinner.stop(`Metabase installed on port ${chosen.port}`);
6358
- return "completed";
6564
+ return { outcome: "completed" };
6359
6565
  }
6360
6566
  };
6361
6567
  }
@@ -6391,13 +6597,13 @@ function sampleDatabaseUrl(port) {
6391
6597
  function publishesOnLoopback$1(inspection, port) {
6392
6598
  return inspection.ports.some((mapping) => mapping.container === POSTGRES_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
6393
6599
  }
6394
- function createOptions(state, home, alsoOn) {
6600
+ function createOptions(state, home, hostPort, alsoOn) {
6395
6601
  return {
6396
6602
  name: state.containerName,
6397
6603
  home,
6398
6604
  image: state.image,
6399
6605
  port: {
6400
- host: state.port,
6606
+ host: hostPort,
6401
6607
  container: POSTGRES_PORT,
6402
6608
  alsoOn
6403
6609
  },
@@ -6407,6 +6613,9 @@ function createOptions(state, home, alsoOn) {
6407
6613
  hostUser: null
6408
6614
  };
6409
6615
  }
6616
+ function requireOwnContainer(name, existing, home) {
6617
+ if (existing.home !== null && existing.home !== home) throw new ConfigError(`container ${name} belongs to the rde home ${existing.home} and is left alone`);
6618
+ }
6410
6619
  function sampleDatabaseContainer(state, docker, home) {
6411
6620
  const name = state.containerName;
6412
6621
  function conflict(reason) {
@@ -6416,11 +6625,12 @@ function sampleDatabaseContainer(state, docker, home) {
6416
6625
  const existing = await docker.containerInspect(name);
6417
6626
  if (existing.exists) {
6418
6627
  if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
6419
- if (!publishesOnLoopback$1(existing, state.port)) throw conflict(`without port ${state.port} published on ${LOOPBACK_HOST}`);
6420
- return;
6628
+ if (publishesOnLoopback$1(existing, state.port)) return;
6629
+ requireOwnContainer(name, existing, home);
6630
+ await docker.containerForceRemove(name);
6421
6631
  }
6422
6632
  await docker.imagePull(state.image, { onLine: options.onProgress });
6423
- await docker.containerCreate(createOptions(state, home, options.alsoOn));
6633
+ await docker.containerCreate(createOptions(state, home, state.port, options.alsoOn));
6424
6634
  }
6425
6635
  async function start() {
6426
6636
  const existing = await docker.containerInspect(name);
@@ -6470,7 +6680,22 @@ function extraAddress(inspection) {
6470
6680
  const extra = inspection.ports.find((mapping) => mapping.container === POSTGRES_PORT && mapping.hostIp !== "127.0.0.1");
6471
6681
  return extra === void 0 ? null : extra.hostIp;
6472
6682
  }
6473
- async function startOnFreePort(state, docker, ports, home) {
6683
+ async function startOnDockerPort(state, docker, home, alsoOn) {
6684
+ const name = state.containerName;
6685
+ await docker.containerForceRemove(name);
6686
+ await docker.containerCreate(createOptions(state, home, null, null));
6687
+ await docker.containerStart(name);
6688
+ const port = await docker.containerLoopbackPort(name, POSTGRES_PORT);
6689
+ await docker.containerForceRemove(name);
6690
+ const pinned = {
6691
+ ...state,
6692
+ port
6693
+ };
6694
+ await docker.containerCreate(createOptions(pinned, home, port, alsoOn));
6695
+ await docker.containerStart(name);
6696
+ return pinned;
6697
+ }
6698
+ async function startOnFreePort(state, docker, home) {
6474
6699
  try {
6475
6700
  return {
6476
6701
  outcome: await sampleDatabaseContainer(state, docker, home).start(),
@@ -6480,18 +6705,10 @@ async function startOnFreePort(state, docker, ports, home) {
6480
6705
  if (!isPortTaken(error)) throw error;
6481
6706
  }
6482
6707
  const existing = await docker.containerInspect(state.containerName);
6483
- if (existing.exists && existing.home !== null && existing.home !== home) throw new ConfigError(`container ${state.containerName} belongs to the rde home ${existing.home} and is left alone`);
6484
- const moved = {
6485
- ...state,
6486
- port: await ports.findFree(SAMPLE_DATABASE_PORT)
6487
- };
6488
- const alsoOn = existing.exists ? extraAddress(existing) : null;
6489
- await docker.containerRemove(state.containerName);
6490
- await docker.containerCreate(createOptions(moved, home, alsoOn));
6491
- await docker.containerStart(moved.containerName);
6708
+ if (existing.exists) requireOwnContainer(state.containerName, existing, home);
6492
6709
  return {
6493
6710
  outcome: "started",
6494
- state: moved
6711
+ state: await startOnDockerPort(state, docker, home, existing.exists ? extraAddress(existing) : null)
6495
6712
  };
6496
6713
  }
6497
6714
  //#endregion
@@ -6585,7 +6802,7 @@ async function installAndStart(ctx, sidecar, installed, deps) {
6585
6802
  throw error;
6586
6803
  }
6587
6804
  installing.stop(`Sample database container ${installed.containerName} on port ${installed.port}`);
6588
- const started = await startOnFreePort(installed, deps.runtime.docker, deps.ports, deps.runtime.rdeHome);
6805
+ const started = await startOnFreePort(installed, deps.runtime.docker, deps.runtime.rdeHome);
6589
6806
  const record = started.state.port === installed.port ? installed : await recordSampleDatabaseMove(ctx, installed, started.state);
6590
6807
  const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
6591
6808
  try {
@@ -6694,7 +6911,7 @@ function sampleDatabaseStep(deps = hostDeps$3) {
6694
6911
  await connectDatabase(ctx, choice, configured, client, resolved);
6695
6912
  await client.setting.enableTransforms();
6696
6913
  ctx.ui.note(usageLines(record), "Sample database");
6697
- return "completed";
6914
+ return { outcome: "completed" };
6698
6915
  }
6699
6916
  };
6700
6917
  }
@@ -6706,7 +6923,6 @@ const PERCENT = 100;
6706
6923
  function hostDeps$2(ctx) {
6707
6924
  return {
6708
6925
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
6709
- ports: hostPortProbe,
6710
6926
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
6711
6927
  timing: DEFAULT_HEALTH_TIMING
6712
6928
  };
@@ -6723,7 +6939,7 @@ function progressLabel(progress) {
6723
6939
  async function startSampleDatabase(ctx, deps) {
6724
6940
  const record = ctx.state.sampleDatabase;
6725
6941
  if (record === null) return;
6726
- const started = await startOnFreePort(record, deps.runtime.docker, deps.ports, deps.runtime.rdeHome);
6942
+ const started = await startOnFreePort(record, deps.runtime.docker, deps.runtime.rdeHome);
6727
6943
  if (started.state.port !== record.port) await recordSampleDatabaseMove(ctx, record, started.state);
6728
6944
  switch (started.outcome) {
6729
6945
  case "missing":
@@ -6783,7 +6999,7 @@ function startMetabaseStep(deps = hostDeps$2) {
6783
6999
  version
6784
7000
  });
6785
7001
  spinner.stop(`Metabase ${version} is running at ${configured.url}`);
6786
- return "completed";
7002
+ return { outcome: "completed" };
6787
7003
  }
6788
7004
  };
6789
7005
  }
@@ -6879,7 +7095,7 @@ function summaryStep(deps = hostDeps$1) {
6879
7095
  async run(ctx) {
6880
7096
  const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
6881
7097
  ctx.ui.note(lines.join("\n"), "Your environment");
6882
- return "completed";
7098
+ return { outcome: "completed" };
6883
7099
  }
6884
7100
  };
6885
7101
  }
@@ -7016,16 +7232,16 @@ function fieldAnswer(value) {
7016
7232
  return text$1(String(value));
7017
7233
  }
7018
7234
  function addDatabase(map, database, ordinal, last) {
7019
- add(map, DATABASE_KEYS.engine, select$1(database.engine));
7235
+ add(map, databaseKey(ordinal, "engine"), select$1(database.engine));
7020
7236
  if (database.uri !== void 0) {
7021
- add(map, DATABASE_KEYS.mode, select$1("paste"));
7022
- add(map, DATABASE_KEYS.uri, text$1(database.uri));
7237
+ add(map, databaseKey(ordinal, "mode"), select$1("paste"));
7238
+ add(map, databaseKey(ordinal, "uri"), text$1(database.uri));
7023
7239
  }
7024
- if (database.uri === void 0 && EXAMPLE_URIS[database.engine] !== void 0) add(map, DATABASE_KEYS.mode, select$1("manual"));
7240
+ if (database.uri === void 0 && EXAMPLE_URIS[database.engine] !== void 0) add(map, databaseKey(ordinal, "mode"), select$1("manual"));
7025
7241
  if (database.fields !== void 0) for (const [name, value] of Object.entries(database.fields)) add(map, fieldKey(ordinal, name), fieldAnswer(value));
7026
- add(map, DATABASE_KEYS.name, text$1(database.name ?? ACCEPT_DEFAULT));
7027
- add(map, DATABASE_KEYS.action, select$1("test"));
7028
- add(map, DATABASE_KEYS.another, select$1(last ? "finish" : "another"));
7242
+ add(map, databaseKey(ordinal, "name"), text$1(database.name ?? ACCEPT_DEFAULT));
7243
+ add(map, databaseKey(ordinal, "action"), select$1("test"));
7244
+ add(map, databaseKey(ordinal, "another"), select$1(last ? "finish" : "another"));
7029
7245
  }
7030
7246
  function addAdmin(map, admin) {
7031
7247
  if (admin.variant !== "prompted") return;
@@ -7083,17 +7299,12 @@ function keyedAnswers(answers) {
7083
7299
  const license = answers.license;
7084
7300
  if (license !== void 0) add(map, LICENSE_KEYS.token, password$1(license.token ?? SKIP_LICENSE));
7085
7301
  const databases = answers.databases;
7086
- if (databases !== void 0) {
7087
- if (databases.length === 0) {
7088
- add(map, DATABASE_KEYS.engine, select$1(FINISH));
7089
- add(map, DATABASE_KEYS.skip, confirm$1(true));
7090
- }
7091
- databases.forEach((database, index) => {
7092
- addDatabase(map, database, index + 1, index === databases.length - 1);
7093
- });
7094
- }
7302
+ if (databases !== void 0) databases.forEach((database, index) => {
7303
+ addDatabase(map, database, index + 1, index === databases.length - 1);
7304
+ });
7095
7305
  const git = answers["git-sync"];
7096
- if (git !== void 0) {
7306
+ if (git?.location === "skip") add(map, GIT_SYNC_KEYS.remote, select$1(SKIP_GIT_SYNC));
7307
+ else if (git !== void 0) {
7097
7308
  add(map, GIT_SYNC_KEYS.location, select$1(git.location));
7098
7309
  if (git.path !== void 0) add(map, GIT_SYNC_KEYS.path, text$1(git.path));
7099
7310
  if (git.remote !== void 0) add(map, GIT_SYNC_KEYS.remote, select$1(git.remote));
@@ -7137,18 +7348,26 @@ const FLAG_FOR_KEY$1 = {
7137
7348
  [API_KEY_KEYS.password]: "--admin-password",
7138
7349
  [API_KEY_KEYS.profile]: "--mb-profile",
7139
7350
  [LICENSE_KEYS.token]: "--license-token or --skip-license",
7140
- [DATABASE_KEYS.engine]: "--database or --skip-databases",
7141
- [DATABASE_KEYS.uri]: "--database",
7142
7351
  [DATABASE_KEYS.skip]: "--skip-databases",
7143
- [GIT_SYNC_KEYS.location]: "--git-dir or --skip-git",
7352
+ [GIT_SYNC_KEYS.location]: "--git-dir",
7144
7353
  [GIT_SYNC_KEYS.path]: "--git-dir",
7145
- [GIT_SYNC_KEYS.remote]: "--git-remote",
7354
+ [GIT_SYNC_KEYS.remote]: "--git-remote or --skip-git",
7146
7355
  [GIT_SYNC_KEYS.url]: "--git-url",
7147
7356
  [GIT_SYNC_KEYS.token]: "--git-token"
7148
7357
  };
7358
+ const DATABASE_FLAG_FOR_NAME = {
7359
+ engine: "--database or --skip-databases",
7360
+ uri: "--database"
7361
+ };
7149
7362
  function howToAnswer(key) {
7150
7363
  const field = parseFieldKey(key);
7151
7364
  if (field !== null) return `set "${field.name}" under "fields" of database ${field.ordinal} in the --answers file (for a pasted database, in its connection string)`;
7365
+ const database = parseDatabaseKey(key);
7366
+ if (database !== null) {
7367
+ const flag = DATABASE_FLAG_FOR_NAME[database.name];
7368
+ const inFile = `set database ${database.ordinal} in the --answers file`;
7369
+ return flag === void 0 ? inFile : `pass ${flag}, or ${inFile}`;
7370
+ }
7152
7371
  const flag = FLAG_FOR_KEY$1[key];
7153
7372
  const inFile = `set "${key}" in the --answers file`;
7154
7373
  return flag === void 0 ? inFile : `pass ${flag}, or ${inFile}`;
@@ -7741,17 +7960,30 @@ function requireKnownStep(steps, id, flag) {
7741
7960
  if (id === void 0) return;
7742
7961
  if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
7743
7962
  }
7963
+ async function detectOne(step, ctx, options, fromHere) {
7964
+ if (step.id === (options.only ?? options.from)) return {
7965
+ step,
7966
+ status: "todo",
7967
+ forced: true
7968
+ };
7969
+ const status = await step.detect(ctx);
7970
+ if (options.only === void 0 && fromHere && status !== "not-applicable") return {
7971
+ step,
7972
+ status: "todo",
7973
+ forced: true
7974
+ };
7975
+ return {
7976
+ step,
7977
+ status,
7978
+ forced: false
7979
+ };
7980
+ }
7744
7981
  async function detectAll(steps, ctx, options) {
7745
7982
  const detected = [];
7746
7983
  let fromHere = false;
7747
7984
  for (const step of steps) {
7748
7985
  if (step.id === options.from) fromHere = true;
7749
- const forced = options.only !== void 0 ? step.id === options.only : fromHere;
7750
- detected.push({
7751
- step,
7752
- status: forced ? "todo" : await step.detect(ctx),
7753
- forced
7754
- });
7986
+ detected.push(await detectOne(step, ctx, options, fromHere));
7755
7987
  }
7756
7988
  return detected;
7757
7989
  }
@@ -7790,13 +8022,16 @@ async function runWizard(steps, ctx, options = {}) {
7790
8022
  title: step.title,
7791
8023
  status
7792
8024
  })));
8025
+ const results = [];
7793
8026
  for (const entry of detected) try {
7794
8027
  if (!await shouldRun(entry, ctx, options)) continue;
7795
- const outcome = await entry.step.run(ctx);
8028
+ const { outcome, result } = await entry.step.run(ctx);
8029
+ if (result !== void 0) results.push(result);
7796
8030
  await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
7797
8031
  } catch (error) {
7798
8032
  throw failureAt(entry.step, error);
7799
8033
  }
8034
+ return results;
7800
8035
  }
7801
8036
  //#endregion
7802
8037
  //#region src/steps/doctor.ts
@@ -8690,73 +8925,11 @@ function isLocation(value) {
8690
8925
  function isRemote(value) {
8691
8926
  return value === "local-bare" || value === "hosted";
8692
8927
  }
8693
- function applyConnectionRecord(draft, value) {
8694
- draft.databases = draft.databases.filter((database) => !database.pasted);
8695
- if (value === null) return;
8696
- if (value.trim().length === 0) {
8697
- draft.databasesFinished = true;
8698
- return;
8699
- }
8700
- draft.databases.push({
8701
- engine: null,
8702
- uri: value.trim(),
8703
- fields: {},
8704
- name: null,
8705
- pasted: true,
8706
- added: false
8707
- });
8708
- }
8709
- function applyEngineRecord(draft, value) {
8710
- if (draft.databases.at(-1)?.added === false) draft.databases.pop();
8711
- if (value === "finish") draft.databasesFinished = true;
8712
- else if (value !== null) draft.databases.push({
8713
- engine: value,
8714
- uri: null,
8715
- fields: {},
8716
- name: null,
8717
- pasted: false,
8718
- added: false
8719
- });
8720
- }
8721
8928
  function applyDatabaseRecord(draft, record) {
8722
8929
  const value = stringOf(record.answer);
8723
- if (record.key === DATABASE_KEYS.connection) {
8724
- applyConnectionRecord(draft, value);
8725
- return;
8726
- }
8727
- if (record.key === DATABASE_KEYS.engine) {
8728
- applyEngineRecord(draft, value);
8729
- return;
8730
- }
8731
- const database = draft.databases.at(-1);
8732
- if (database === void 0) return;
8733
- const switched = record.key === DATABASE_KEYS.switchEngine && value !== "try-again";
8734
- if ((record.key === DATABASE_KEYS.detectedEngine || switched) && value !== null) {
8735
- database.engine = value;
8736
- return;
8737
- }
8738
- if (record.key === DATABASE_KEYS.another) {
8739
- database.added = true;
8740
- return;
8741
- }
8742
- if (record.key === DATABASE_KEYS.mode) {
8743
- database.fields = {};
8744
- if (value === "manual") database.uri = null;
8745
- return;
8746
- }
8747
- if (record.key === DATABASE_KEYS.uri && value !== null) {
8748
- database.uri = value;
8749
- return;
8750
- }
8751
- if (record.key === DATABASE_KEYS.name && value !== null && value.length > 0) {
8752
- database.name = value;
8753
- return;
8754
- }
8755
- const fieldName = parseFieldKey(record.key)?.name;
8756
- if (fieldName === void 0) return;
8757
- if (record.answer.kind === "confirm") database.fields[fieldName] = record.answer.value;
8758
- else if (record.answer.kind === "password") database.fields[fieldName] = envReference(databaseEnv(draft.databases.length - 1, fieldName));
8759
- else if (value !== null) database.fields[fieldName] = value;
8930
+ const skippedConnection = record.key === DATABASE_KEYS.connection && value?.trim() === "";
8931
+ const finished = parseDatabaseKey(record.key)?.name === "engine" && value === "finish";
8932
+ if (skippedConnection || finished) draft.databasesFinished = true;
8760
8933
  }
8761
8934
  function applyAdminRecord(draft, key, value) {
8762
8935
  if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
@@ -8765,7 +8938,8 @@ function applyAdminRecord(draft, key, value) {
8765
8938
  else if (key === ADMIN_USER_KEYS.lastName && value !== null && value.length > 0) admin(draft)["last-name"] = value;
8766
8939
  }
8767
8940
  function applyGitRecord(draft, key, value) {
8768
- if (key === GIT_SYNC_KEYS.location && value !== null && isLocation(value)) gitSync(draft).location = value;
8941
+ if (key === GIT_SYNC_KEYS.remote && value === "skip") draft.answers["git-sync"] = { location: SKIP_GIT_SYNC };
8942
+ else if (key === GIT_SYNC_KEYS.location && value !== null && isLocation(value)) gitSync(draft).location = value;
8769
8943
  else if (key === GIT_SYNC_KEYS.path && value !== null) gitSync(draft).path = value;
8770
8944
  else if (key === GIT_SYNC_KEYS.remote && value !== null && isRemote(value)) gitSync(draft).remote = value;
8771
8945
  else if (key === GIT_SYNC_KEYS.url && value !== null) gitSync(draft).url = value;
@@ -8793,30 +8967,26 @@ function writableUri(uri, index) {
8793
8967
  const trimmed = uri.trim();
8794
8968
  return URI_PASSWORDS.some((pattern) => pattern.test(trimmed)) ? envReference(databaseEnv(index, "uri")) : trimmed;
8795
8969
  }
8796
- function engineOf(database) {
8797
- if (database.engine !== null) return database.engine;
8798
- const [only, ...others] = database.uri === null ? [] : detectEngine(database.uri);
8799
- return only !== void 0 && others.length === 0 ? only : null;
8800
- }
8801
- function databaseEntry(database, index) {
8802
- const hasFields = Object.keys(database.fields).length > 0;
8803
- const engine = engineOf(database);
8804
- if (!database.added || database.uri === null && !hasFields || engine === null) return null;
8970
+ function databaseEntry(added, index) {
8971
+ if (added.uri !== null) return {
8972
+ engine: added.engine,
8973
+ uri: writableUri(added.uri, index),
8974
+ name: added.name
8975
+ };
8976
+ const fields = added.fields.map(({ name, value, secret }) => [name, secret ? envReference(databaseEnv(index, name)) : value]);
8805
8977
  return {
8806
- engine,
8807
- ...database.uri !== null && { uri: writableUri(database.uri, index) },
8808
- ...database.uri === null && { fields: database.fields },
8809
- ...database.name !== null && { name: database.name }
8978
+ engine: added.engine,
8979
+ fields: Object.fromEntries(fields),
8980
+ name: added.name
8810
8981
  };
8811
8982
  }
8812
- function answersFromRecords(records, overrides) {
8983
+ function answersFromRecords(records, overrides, added) {
8813
8984
  const draft = {
8814
8985
  answers: {},
8815
- databases: [],
8816
8986
  databasesFinished: false
8817
8987
  };
8818
8988
  for (const record of records) applyRecord(draft, record);
8819
- const databases = draft.databases.map(databaseEntry).filter((entry) => entry !== null);
8989
+ const databases = added.map(databaseEntry);
8820
8990
  if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
8821
8991
  return {
8822
8992
  ...draft.answers,
@@ -9063,9 +9233,10 @@ const init = defineRdeCommand({
9063
9233
  prompts: recording?.prompts ?? answering
9064
9234
  });
9065
9235
  ctx.ui.intro("rde init");
9066
- await runWizard(initSteps(stepOptionsFrom(answers, { interactive: stdinIsTty() && !runOptions.yes })), ctx, options);
9236
+ const results = await runWizard(initSteps(stepOptionsFrom(answers, { interactive: stdinIsTty() && !runOptions.yes })), ctx, options);
9067
9237
  if (recording !== null && printTo !== void 0) {
9068
- await writeAnswersFile(printTo, answersFromRecords(recording.records, answers));
9238
+ const added = results.flatMap((result) => result.addedDatabases);
9239
+ await writeAnswersFile(printTo, answersFromRecords(recording.records, answers, added));
9069
9240
  ctx.ui.info(`Answers written to ${printTo}`);
9070
9241
  }
9071
9242
  ctx.ui.outro("rde init finished");
@@ -9771,7 +9942,6 @@ const RELEASE_TAG = /^v?[01]\.(\d+\.\d+(?:\.\d+)?)/;
9771
9942
  function hostDeps(ctx) {
9772
9943
  return {
9773
9944
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
9774
- ports: hostPortProbe,
9775
9945
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
9776
9946
  timing: DEFAULT_HEALTH_TIMING,
9777
9947
  releases: releaseLookup(ctx.signal),
@@ -10033,7 +10203,7 @@ const upgradeCommand = defineRdeCommand({
10033
10203
  const main = defineCommand({
10034
10204
  meta: {
10035
10205
  name: "rde",
10036
- version: "0.1.8",
10206
+ version: "0.1.10",
10037
10207
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
10038
10208
  },
10039
10209
  subCommands: {