@metabase/rde 0.1.14 → 0.1.15

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 (2) hide show
  1. package/dist/cli.mjs +210 -139
  2. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -50,6 +50,7 @@ var StepFailedError = class extends Error {
50
50
  var ExternalToolError = class extends Error {
51
51
  tool;
52
52
  argv;
53
+ envNames;
53
54
  exitCode;
54
55
  stderrTail;
55
56
  constructor(failure) {
@@ -57,15 +58,21 @@ var ExternalToolError = class extends Error {
57
58
  this.name = "ExternalToolError";
58
59
  this.tool = failure.tool;
59
60
  this.argv = failure.argv;
61
+ this.envNames = failure.envNames;
60
62
  this.exitCode = failure.exitCode;
61
63
  this.stderrTail = failure.stderrTail;
62
64
  }
63
65
  };
66
+ function commandLabel(failure) {
67
+ const command = `\`${[failure.tool, ...failure.argv].join(" ")}\``;
68
+ if (failure.envNames.length === 0) return command;
69
+ return `${command}, run by rde with ${failure.envNames.join(" and ")} set,`;
70
+ }
64
71
  function describeToolFailure(failure) {
65
- const command = [failure.tool, ...failure.argv].join(" ");
72
+ const command = commandLabel(failure);
66
73
  const exit = failure.exitCode === null ? "was killed" : `exited with ${failure.exitCode}`;
67
74
  const tail = failure.stderrTail.trim();
68
- return tail.length === 0 ? `\`${command}\` ${exit}` : `\`${command}\` ${exit}: ${tail}`;
75
+ return tail.length === 0 ? `${command} ${exit}` : `${command} ${exit}: ${tail}`;
69
76
  }
70
77
  var MetabaseApiError = class extends Error {
71
78
  status;
@@ -103,6 +110,9 @@ var MetabaseUnreachableError = class extends Error {
103
110
  this.reason = reason;
104
111
  }
105
112
  };
113
+ function isMetabaseNotServing(error) {
114
+ return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
115
+ }
106
116
  var DownloadError = class extends Error {
107
117
  url;
108
118
  constructor(url, reason) {
@@ -1348,9 +1358,6 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
1348
1358
  user: userResource(http)
1349
1359
  };
1350
1360
  }
1351
- function isNotReadyYet(error) {
1352
- return error instanceof MetabaseInitializingError || error instanceof MetabaseUnreachableError;
1353
- }
1354
1361
  async function waitForHealth(client, options) {
1355
1362
  const deadline = Date.now() + options.timeoutMs;
1356
1363
  const interval = options.pollIntervalMs ?? HEALTH_POLL_INTERVAL_MS;
@@ -1359,7 +1366,7 @@ async function waitForHealth(client, options) {
1359
1366
  await client.health.get();
1360
1367
  return;
1361
1368
  } catch (error) {
1362
- if (!isNotReadyYet(error)) throw error;
1369
+ if (!isMetabaseNotServing(error)) throw error;
1363
1370
  if (error instanceof MetabaseInitializingError) options.onProgress(error.progress);
1364
1371
  }
1365
1372
  if (!await options.isRunning()) throw new MetabaseUnreachableError(client.baseUrl, "it exited before it became healthy");
@@ -1386,15 +1393,12 @@ function storedApiKey(state, secrets) {
1386
1393
  function hostCredentialsClient(signal) {
1387
1394
  return (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal);
1388
1395
  }
1389
- function isNotReady(error) {
1390
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
1391
- }
1392
1396
  async function hasAdminOutsideRde(state, client) {
1393
1397
  if (state.url === null) return false;
1394
1398
  try {
1395
1399
  return (await client(state.url, NO_AUTH).session.properties())["has-user-setup"];
1396
1400
  } catch (error) {
1397
- if (isNotReady(error)) return false;
1401
+ if (isMetabaseNotServing(error)) return false;
1398
1402
  throw error;
1399
1403
  }
1400
1404
  }
@@ -1830,10 +1834,11 @@ function signalProcess(pid, signal) {
1830
1834
  function stderrTail(stderr) {
1831
1835
  return stderr.trim().split("\n").slice(-5).join("\n");
1832
1836
  }
1833
- function toolFailure(tool, argv, result) {
1837
+ function toolFailure(tool, argv, result, options = {}) {
1834
1838
  return new ExternalToolError({
1835
1839
  tool,
1836
1840
  argv,
1841
+ envNames: options.addedEnv === void 0 ? [] : Object.keys(options.addedEnv),
1837
1842
  exitCode: result.exitCode,
1838
1843
  stderrTail: stderrTail(result.stderr)
1839
1844
  });
@@ -1845,6 +1850,7 @@ async function runShown(runner, output, command, args) {
1845
1850
  if (exitCode !== 0) throw new ExternalToolError({
1846
1851
  tool: command,
1847
1852
  argv: args,
1853
+ envNames: [],
1848
1854
  exitCode,
1849
1855
  stderrTail: ""
1850
1856
  });
@@ -1856,6 +1862,7 @@ async function runShown(runner, output, command, args) {
1856
1862
  throw new ExternalToolError({
1857
1863
  tool: command,
1858
1864
  argv: args,
1865
+ envNames: [],
1859
1866
  exitCode: result.exitCode,
1860
1867
  stderrTail: stderrTail(printed.filter((line) => HAS_TEXT.test(line)).join("\n"))
1861
1868
  });
@@ -1869,6 +1876,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
1869
1876
  throw new ExternalToolError({
1870
1877
  tool,
1871
1878
  argv,
1879
+ envNames: [],
1872
1880
  exitCode: 0,
1873
1881
  stderrTail: `output is not JSON: ${error instanceof Error ? error.message : String(error)}`
1874
1882
  });
@@ -1879,6 +1887,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
1879
1887
  if (error instanceof ZodError) throw new ExternalToolError({
1880
1888
  tool,
1881
1889
  argv,
1890
+ envNames: [],
1882
1891
  exitCode: 0,
1883
1892
  stderrTail: `unexpected output shape:\n${z.prettifyError(error)}`
1884
1893
  });
@@ -2344,6 +2353,7 @@ function dockerTool(runner) {
2344
2353
  throw new ExternalToolError({
2345
2354
  tool: DOCKER,
2346
2355
  argv: args,
2356
+ envNames: [],
2347
2357
  exitCode: 0,
2348
2358
  stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
2349
2359
  });
@@ -3095,7 +3105,7 @@ function hostDeps$14(ctx) {
3095
3105
  };
3096
3106
  }
3097
3107
  function isNotMetabaseYet(error) {
3098
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || error instanceof MetabaseApiError;
3108
+ return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
3099
3109
  }
3100
3110
  function validateEmail(value) {
3101
3111
  return EMAIL.test(value) ? void 0 : "Enter an email address";
@@ -3455,11 +3465,12 @@ const LogoutResult = z.object({
3455
3465
  cleared: z.boolean(),
3456
3466
  aborted: z.boolean()
3457
3467
  }).loose();
3468
+ const MbAuthMethod = z.enum(["oauth", "apiKey"]);
3458
3469
  const AuthStatus = z.object({
3459
3470
  profile: z.string(),
3460
3471
  present: z.boolean(),
3461
3472
  url: z.string().nullable(),
3462
- method: z.string().nullable(),
3473
+ method: MbAuthMethod.nullable(),
3463
3474
  user: ProbedUser.nullable()
3464
3475
  }).loose();
3465
3476
  const AuthProfileStatus = z.enum([
@@ -3506,11 +3517,41 @@ const RemoteChanges = z.object({
3506
3517
  remote_version: z.string().nullable(),
3507
3518
  local_version: z.string().nullable()
3508
3519
  }).loose();
3520
+ function comparableUrl(url) {
3521
+ const parsed = new URL(url);
3522
+ return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
3523
+ }
3524
+ function sameMetabaseUrl(a, b) {
3525
+ return comparableUrl(a) === comparableUrl(b);
3526
+ }
3527
+ function isLoggedInAt(status, url) {
3528
+ return status !== null && status.present && status.url !== null && sameMetabaseUrl(status.url, url);
3529
+ }
3530
+ function isOAuthLoginAt(status, url) {
3531
+ return status !== null && status.method === MbAuthMethod.enum.oauth && isLoggedInAt(status, url);
3532
+ }
3533
+ function isApiKeyLoginAt(status, url) {
3534
+ return status !== null && status.method === MbAuthMethod.enum.apiKey && isLoggedInAt(status, url);
3535
+ }
3536
+ function mbInstanceOf(state, secrets) {
3537
+ const configured = configuredRuntime(state);
3538
+ if (configured === null || secrets === null || secrets.apiKey === null) return null;
3539
+ return {
3540
+ url: configured.url,
3541
+ apiKey: secrets.apiKey
3542
+ };
3543
+ }
3544
+ function instanceEnv(instance) {
3545
+ return { addedEnv: {
3546
+ [ENV_MB_URL]: instance.url,
3547
+ [ENV_MB_API_KEY]: instance.apiKey
3548
+ } };
3549
+ }
3509
3550
  function mbTool(runner) {
3510
- async function mbJson(args, schema, stdin) {
3551
+ async function mbJson(args, schema, options = {}) {
3511
3552
  const argv = [...args, "--json"];
3512
- const result = await runner.run(MB, argv, stdin === void 0 ? {} : { stdin });
3513
- if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
3553
+ const result = await runner.run(MB, argv, options);
3554
+ if (result.exitCode !== 0) throw toolFailure(MB, argv, result, options);
3514
3555
  return parseJsonOutput(MB, argv, result.stdout, schema);
3515
3556
  }
3516
3557
  async function version(command = MB) {
@@ -3533,7 +3574,7 @@ function mbTool(runner) {
3533
3574
  options.url,
3534
3575
  "--profile",
3535
3576
  options.profile
3536
- ], LoginResult, `${options.apiKey}\n`);
3577
+ ], LoginResult, { stdin: `${options.apiKey}\n` });
3537
3578
  }
3538
3579
  async function authStatus(profile) {
3539
3580
  return mbJson([
@@ -3543,6 +3584,14 @@ function mbTool(runner) {
3543
3584
  profile
3544
3585
  ], AuthStatus);
3545
3586
  }
3587
+ async function authStatusIfInstalled(profile) {
3588
+ try {
3589
+ return await authStatus(profile);
3590
+ } catch (error) {
3591
+ if (error instanceof ProcessNotFoundError) return null;
3592
+ throw error;
3593
+ }
3594
+ }
3546
3595
  async function authLogout(profile) {
3547
3596
  return mbJson([
3548
3597
  "auth",
@@ -3562,50 +3611,37 @@ function mbTool(runner) {
3562
3611
  "0"
3563
3612
  ], AuthList)).data;
3564
3613
  }
3565
- async function gitSyncStatus(profile) {
3566
- return mbJson([
3567
- "git-sync",
3568
- "status",
3569
- "--profile",
3570
- profile
3571
- ], GitSyncStatus);
3614
+ async function gitSyncStatus(instance) {
3615
+ return mbJson(["git-sync", "status"], GitSyncStatus, instanceEnv(instance));
3572
3616
  }
3573
- async function gitSyncHasRemoteChanges(profile) {
3574
- return mbJson([
3575
- "git-sync",
3576
- "has-remote-changes",
3577
- "--profile",
3578
- profile
3579
- ], RemoteChanges);
3617
+ async function gitSyncHasRemoteChanges(instance) {
3618
+ return mbJson(["git-sync", "has-remote-changes"], RemoteChanges, instanceEnv(instance));
3580
3619
  }
3581
- async function gitSyncImport(profile, options) {
3620
+ async function gitSyncImport(instance, options) {
3582
3621
  return mbJson([
3583
3622
  "git-sync",
3584
3623
  "import",
3585
- "--profile",
3586
- profile,
3587
3624
  "--branch",
3588
3625
  options.branch,
3589
3626
  "--wait"
3590
- ], GitSyncOutcome);
3627
+ ], GitSyncOutcome, instanceEnv(instance));
3591
3628
  }
3592
- async function gitSyncExport(profile, options) {
3629
+ async function gitSyncExport(instance, options) {
3593
3630
  return mbJson([
3594
3631
  "git-sync",
3595
3632
  "export",
3596
- "--profile",
3597
- profile,
3598
3633
  "--branch",
3599
3634
  options.branch,
3600
3635
  "--message",
3601
3636
  options.message,
3602
3637
  "--wait"
3603
- ], GitSyncOutcome);
3638
+ ], GitSyncOutcome, instanceEnv(instance));
3604
3639
  }
3605
3640
  return {
3606
3641
  version,
3607
3642
  authLogin,
3608
3643
  authStatus,
3644
+ authStatusIfInstalled,
3609
3645
  authLogout,
3610
3646
  authList,
3611
3647
  gitSyncStatus,
@@ -3626,7 +3662,8 @@ const ADMINISTRATORS_GROUP_NAME = "Administrators";
3626
3662
  function hostDeps$13(ctx) {
3627
3663
  return {
3628
3664
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3629
- runner: processRunner(ctx.signal)
3665
+ runner: processRunner(ctx.signal),
3666
+ instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
3630
3667
  };
3631
3668
  }
3632
3669
  async function adminCredentials(ctx) {
@@ -3682,6 +3719,13 @@ async function authenticates(client) {
3682
3719
  throw error;
3683
3720
  }
3684
3721
  }
3722
+ async function forgetCompletion(ctx) {
3723
+ const { [STEP_ID$9]: record, ...steps } = ctx.state.steps;
3724
+ if (record !== void 0) await ctx.saveState({
3725
+ ...ctx.state,
3726
+ steps
3727
+ });
3728
+ }
3685
3729
  async function workingApiKey(ctx, deps, url) {
3686
3730
  const stored = ctx.secrets.apiKey;
3687
3731
  if (stored !== null) {
@@ -3691,6 +3735,7 @@ async function workingApiKey(ctx, deps, url) {
3691
3735
  }))) return stored;
3692
3736
  ctx.ui.warn("Metabase rejects the stored API key; replacing it");
3693
3737
  }
3738
+ await forgetCompletion(ctx);
3694
3739
  const sessionId = await superuserSession(ctx, deps, url);
3695
3740
  const apiKey = await freshApiKey(ctx, deps.client(url, {
3696
3741
  kind: "session",
@@ -3702,20 +3747,24 @@ async function workingApiKey(ctx, deps, url) {
3702
3747
  });
3703
3748
  return apiKey;
3704
3749
  }
3705
- async function defaultProfileStatus(mb) {
3706
- try {
3707
- return await mb.authStatus(DEFAULT_MB_PROFILE);
3708
- } catch (error) {
3709
- if (error instanceof ProcessNotFoundError) return null;
3710
- throw error;
3711
- }
3750
+ function elsewhereUrl(status, url, movedFrom) {
3751
+ if (status === null || !status.present || status.url === null) return null;
3752
+ if (sameMetabaseUrl(status.url, url)) return null;
3753
+ if (movedFrom !== null && sameMetabaseUrl(status.url, movedFrom)) return null;
3754
+ return status.url;
3712
3755
  }
3713
3756
  async function chooseProfile(ctx, mb, url) {
3714
3757
  const profile = ctx.state.mbProfile;
3715
- if (profile !== "default") return profile;
3716
- const existing = await defaultProfileStatus(mb);
3717
- if (existing === null || !existing.present || existing.url === null || existing.url === url || existing.url === ctx.state.movedFrom) return profile;
3718
- const previous = existing.url;
3758
+ const status = await mb.authStatusIfInstalled(profile);
3759
+ if (profile !== "default") return {
3760
+ profile,
3761
+ status
3762
+ };
3763
+ const previous = elsewhereUrl(status, url, ctx.state.movedFrom);
3764
+ if (previous === null) return {
3765
+ profile,
3766
+ status
3767
+ };
3719
3768
  const named = namedMbProfile(ctx.state.name);
3720
3769
  if (await ctx.prompts.select({
3721
3770
  key: API_KEY_KEYS.profile,
@@ -3730,13 +3779,19 @@ async function chooseProfile(ctx, mb, url) {
3730
3779
  recommended: "default"
3731
3780
  }) === "default") {
3732
3781
  ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
3733
- return profile;
3782
+ return {
3783
+ profile,
3784
+ status
3785
+ };
3734
3786
  }
3735
3787
  await ctx.saveState({
3736
3788
  ...ctx.state,
3737
3789
  mbProfile: named
3738
3790
  });
3739
- return named;
3791
+ return {
3792
+ profile: named,
3793
+ status: await mb.authStatusIfInstalled(named)
3794
+ };
3740
3795
  }
3741
3796
  function usageNote(profile) {
3742
3797
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
@@ -3748,31 +3803,34 @@ function apiKeyStep(deps = hostDeps$13) {
3748
3803
  title: "Connect the Metabase CLI",
3749
3804
  redoHint: "`rde status` shows the mb profile it uses.",
3750
3805
  async detect(ctx) {
3751
- if (ctx.secrets.apiKey === null) return "todo";
3752
- const mb = mbTool(deps(ctx).runner);
3753
- const profile = ctx.state.mbProfile;
3806
+ const configured = configuredRuntime(ctx.state);
3807
+ const apiKey = ctx.secrets.apiKey;
3808
+ if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$9]?.status !== "done") return "todo";
3809
+ const resolved = deps(ctx);
3810
+ if (!await resolved.instanceRunning()) return "todo";
3811
+ if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
3754
3812
  try {
3755
- if (!(await mb.authStatus(profile)).present) return "todo";
3813
+ return await authenticates(resolved.client(configured.url, {
3814
+ kind: "api-key",
3815
+ key: apiKey
3816
+ })) ? "done" : "todo";
3756
3817
  } catch (error) {
3757
- if (error instanceof ProcessNotFoundError) return "todo";
3818
+ if (isMetabaseNotServing(error) || error instanceof MetabaseApiError) return "todo";
3758
3819
  throw error;
3759
3820
  }
3760
- return (await mb.authList()).find((candidate) => candidate.profile === profile)?.status === "ok" ? "done" : "todo";
3761
3821
  },
3762
3822
  async run(ctx) {
3763
3823
  const resolved = deps(ctx);
3764
3824
  const configured = requireConfiguredRuntime(ctx.state);
3765
3825
  const apiKey = await workingApiKey(ctx, resolved, configured.url);
3766
3826
  const mb = mbTool(resolved.runner);
3767
- const profile = await chooseProfile(ctx, mb, configured.url);
3768
- await mb.authLogin({
3827
+ const { profile, status } = await chooseProfile(ctx, mb, configured.url);
3828
+ if (isOAuthLoginAt(status, configured.url)) ctx.ui.info(`mb profile ${profile} is logged in to this instance with OAuth; leaving it as it is (\`mb auth logout --profile ${profile}\` first to use rde's API key instead)`);
3829
+ else await mb.authLogin({
3769
3830
  url: configured.url,
3770
3831
  profile,
3771
3832
  apiKey
3772
3833
  });
3773
- const entry = (await mb.authList()).find((candidate) => candidate.profile === profile);
3774
- if (entry === void 0) throw new ConfigError(`\`mb auth list\` does not show profile ${profile} after login`);
3775
- if (entry.status !== "ok") throw new ConfigError(`mb profile ${profile} reports ${entry.status} after login`);
3776
3834
  if (ctx.state.movedFrom !== null) await ctx.saveState({
3777
3835
  ...ctx.state,
3778
3836
  movedFrom: null
@@ -6019,7 +6077,7 @@ async function applySettings(run, configured) {
6019
6077
  async function remoteHasContent(run, gitRemote) {
6020
6078
  switch (gitRemote.kind) {
6021
6079
  case "local-bare": return run.git.hasMetabaseContent(gitRemote.workdir);
6022
- case "hosted": return (await mbTool(run.deps.runner).gitSyncHasRemoteChanges(run.ctx.state.mbProfile)).has_changes;
6080
+ case "hosted": return (await mbTool(run.deps.runner).gitSyncHasRemoteChanges(run.instance)).has_changes;
6023
6081
  }
6024
6082
  }
6025
6083
  function reportImport(run, outcome) {
@@ -6041,7 +6099,7 @@ async function importExistingContent(run, gitRemote) {
6041
6099
  const spinner = run.ctx.ui.spinner("Importing the repository's content into Metabase…");
6042
6100
  let outcome;
6043
6101
  try {
6044
- outcome = await mbTool(run.deps.runner).gitSyncImport(run.ctx.state.mbProfile, { branch: gitRemote.branch });
6102
+ outcome = await mbTool(run.deps.runner).gitSyncImport(run.instance, { branch: gitRemote.branch });
6045
6103
  } catch (error) {
6046
6104
  spinner.fail("Import failed");
6047
6105
  throw error;
@@ -6085,15 +6143,17 @@ function isMbUnavailable(error) {
6085
6143
  }
6086
6144
  async function runGitSync(ctx, deps) {
6087
6145
  const configured = requireConfiguredRuntime(ctx.state);
6088
- if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6146
+ const instance = mbInstanceOf(ctx.state, ctx.secrets);
6147
+ if (instance === null) throw new ConfigError("no API key yet; the api-key step comes first");
6089
6148
  const run = {
6090
6149
  ctx,
6091
6150
  deps,
6092
6151
  git: gitTool(deps.runner),
6093
- client: deps.client(configured.url, {
6152
+ client: deps.client(instance.url, {
6094
6153
  kind: "api-key",
6095
- key: ctx.secrets.apiKey
6096
- })
6154
+ key: instance.apiKey
6155
+ }),
6156
+ instance
6097
6157
  };
6098
6158
  const flow = await runFlow({
6099
6159
  remote: remoteScreen(run),
@@ -6123,8 +6183,10 @@ function gitSyncStep(deps = hostDeps$12) {
6123
6183
  if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
6124
6184
  if (ctx.state.steps[STEP_ID$8]?.status === "skipped") return "not-applicable";
6125
6185
  if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
6186
+ const instance = mbInstanceOf(ctx.state, ctx.secrets);
6187
+ if (instance === null) return "todo";
6126
6188
  try {
6127
- return (await mbTool(deps(ctx).runner).gitSyncStatus(ctx.state.mbProfile)).branch === null ? "todo" : "done";
6189
+ return (await mbTool(deps(ctx).runner).gitSyncStatus(instance)).branch === null ? "todo" : "done";
6128
6190
  } catch (error) {
6129
6191
  if (isMbUnavailable(error)) return "todo";
6130
6192
  throw error;
@@ -6564,9 +6626,6 @@ function hostDatabasesDeps(signal) {
6564
6626
  now: () => /* @__PURE__ */ new Date()
6565
6627
  };
6566
6628
  }
6567
- function isNotServingYet$2(error) {
6568
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6569
- }
6570
6629
  function requireApiKey$2(state, apiKey) {
6571
6630
  if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
6572
6631
  return apiKey;
@@ -6707,7 +6766,7 @@ function databasesStep(options, deps = hostDeps$10) {
6707
6766
  try {
6708
6767
  return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
6709
6768
  } catch (error) {
6710
- if (isNotServingYet$2(error) || isRejectedCredential(error)) return "todo";
6769
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
6711
6770
  throw error;
6712
6771
  }
6713
6772
  },
@@ -6886,6 +6945,7 @@ async function stillWrong(install, after, deps) {
6886
6945
  const fail = (stderrTail) => new ExternalToolError({
6887
6946
  tool: install.command,
6888
6947
  argv: install.args,
6948
+ envNames: [],
6889
6949
  exitCode: 0,
6890
6950
  stderrTail
6891
6951
  });
@@ -6978,7 +7038,7 @@ function hostDeps$8(ctx) {
6978
7038
  }
6979
7039
  function isNoTokenYet(error) {
6980
7040
  if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$2;
6981
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7041
+ return isMetabaseNotServing(error);
6982
7042
  }
6983
7043
  function validateToken(value) {
6984
7044
  if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
@@ -7054,7 +7114,8 @@ function licenseStep(deps = hostDeps$8) {
7054
7114
  validThru: status["valid-thru"] ?? null
7055
7115
  }
7056
7116
  });
7057
- await mbTool(resolved.runner).authLogin({
7117
+ const mb = mbTool(resolved.runner);
7118
+ if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
7058
7119
  url: configured.url,
7059
7120
  profile: ctx.state.mbProfile,
7060
7121
  apiKey
@@ -7413,9 +7474,6 @@ function hostDeps$5(ctx) {
7413
7474
  now: () => /* @__PURE__ */ new Date()
7414
7475
  };
7415
7476
  }
7416
- function isNotServingYet$1(error) {
7417
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7418
- }
7419
7477
  function isNotFound$2(error) {
7420
7478
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1;
7421
7479
  }
@@ -7576,7 +7634,7 @@ function sampleDatabaseStep(deps = hostDeps$5) {
7576
7634
  try {
7577
7635
  return await recordedDatabase(record, client) === null ? "todo" : "done";
7578
7636
  } catch (error) {
7579
- if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
7637
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
7580
7638
  throw error;
7581
7639
  }
7582
7640
  },
@@ -7613,7 +7671,7 @@ function hostDeps$4(ctx) {
7613
7671
  };
7614
7672
  }
7615
7673
  function isNotHealthy(error) {
7616
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || error instanceof MetabaseApiError;
7674
+ return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
7617
7675
  }
7618
7676
  async function runningVersion(client) {
7619
7677
  return (await client.session.properties()).version.tag;
@@ -7718,9 +7776,6 @@ function hostDeps$3(ctx) {
7718
7776
  probe: hostAgentProbe()
7719
7777
  };
7720
7778
  }
7721
- function isNotServing$1(error) {
7722
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7723
- }
7724
7779
  function loginLine(kind, email) {
7725
7780
  switch (kind) {
7726
7781
  case "generated":
@@ -7737,7 +7792,7 @@ async function databaseLines$1(state, apiKey, deps) {
7737
7792
  key: apiKey
7738
7793
  }).database.listWithStubs()).data;
7739
7794
  } catch (error) {
7740
- if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
7795
+ if (isMetabaseNotServing(error)) return ["Databases: Metabase is not running; `rde start`"];
7741
7796
  if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
7742
7797
  throw error;
7743
7798
  }
@@ -7819,9 +7874,6 @@ function hostDeps$2(ctx) {
7819
7874
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
7820
7875
  };
7821
7876
  }
7822
- function isNotServingYet(error) {
7823
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7824
- }
7825
7877
  function transformsStep(deps = hostDeps$2) {
7826
7878
  return {
7827
7879
  id: STEP_ID,
@@ -7838,7 +7890,7 @@ function transformsStep(deps = hostDeps$2) {
7838
7890
  try {
7839
7891
  return await client.setting.transformsDecided() ? "done" : "todo";
7840
7892
  } catch (error) {
7841
- if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
7893
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
7842
7894
  throw error;
7843
7895
  }
7844
7896
  },
@@ -9365,31 +9417,46 @@ async function credentialRows(state, secrets, apiKey, deps) {
9365
9417
  stateFileRow(files)
9366
9418
  ];
9367
9419
  }
9368
- function withoutTrailingSlash(url) {
9369
- return url.replace(/\/+$/, "");
9420
+ function brokenProfileRepair(profile, method) {
9421
+ if (method === MbAuthMethod.enum.oauth) return {
9422
+ hint: `\`mb auth login --profile ${profile}\` logs it in again; or \`mb auth logout --profile ${profile}\`, then \`rde init --only api-key\` hands the profile to rde's API key`,
9423
+ step: null
9424
+ };
9425
+ return {
9426
+ hint: "`rde init --only api-key` logs it in again",
9427
+ step: API_KEY_STEP
9428
+ };
9370
9429
  }
9371
- async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
9430
+ function unprobedReason(method, healthy, access) {
9431
+ if (access.kind === "accepted") return null;
9432
+ if (method !== MbAuthMethod.enum.oauth) return access.reason;
9433
+ return healthy ? null : NOT_HEALTHY_REASON;
9434
+ }
9435
+ async function mbProfileRow(state, url, deps, profiles, mbUnusableReason, healthy, access) {
9436
+ const id = "mb-profile";
9372
9437
  const label = "mb profile";
9373
- if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
9374
- if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY_REASON);
9375
- const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
9376
- if (entry === void 0) return failed("mb-profile", label, {
9377
- detail: `${state.mbProfile} is not configured`,
9438
+ const profile = state.mbProfile;
9439
+ if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
9440
+ const status = await mbTool(deps.runner).authStatus(profile);
9441
+ if (!status.present || status.url === null) return failed(id, label, {
9442
+ detail: `${profile} is not configured`,
9378
9443
  hint: "`rde init --only api-key`",
9379
- step: "api-key"
9444
+ step: API_KEY_STEP
9380
9445
  });
9381
- if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
9382
- detail: `${state.mbProfile} points at ${entry.url}, not at this instance (${url})`,
9446
+ if (!sameMetabaseUrl(status.url, url)) return failed(id, label, {
9447
+ detail: `${profile} points at ${status.url}, not at this instance (${url})`,
9383
9448
  hint: "`rde init --only api-key` logs it back in against the instance",
9384
- step: "api-key"
9449
+ step: API_KEY_STEP
9385
9450
  });
9386
- if (entry.status !== "ok") return failed("mb-profile", label, {
9387
- detail: `${state.mbProfile}: ${entry.status}`,
9388
- hint: "`rde init --only api-key` logs it in again",
9389
- step: "api-key"
9451
+ const unprobed = unprobedReason(status.method, healthy, access);
9452
+ if (unprobed !== null) return skipped(id, label, unprobed);
9453
+ const how = status.method === MbAuthMethod.enum.oauth ? " (OAuth)" : "";
9454
+ const entry = (await profiles()).find((candidate) => candidate.profile === profile);
9455
+ if (entry === void 0 || entry.status !== "ok") return failed(id, label, {
9456
+ detail: `${profile}${how}: ${entry === void 0 ? "not listed by `mb auth list`" : entry.status}`,
9457
+ ...brokenProfileRepair(profile, status.method)
9390
9458
  });
9391
- const user = entry.user === null ? "" : ` as ${entry.user.name}`;
9392
- return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
9459
+ return ok(id, label, `${profile}${how}: ok${entry.user === null ? "" : ` as ${entry.user.name}`}`);
9393
9460
  }
9394
9461
  function hostOf(url) {
9395
9462
  return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
@@ -9410,8 +9477,8 @@ async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
9410
9477
  async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
9411
9478
  const rows = [];
9412
9479
  if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
9413
- if (env.url !== null) rows.push(note$1(`env.${ENV_MB_URL}`, ENV_MB_URL, `every \`mb\` call goes to ${hostOf(env.url)}, whatever \`--profile\` says, rde's own included`, unsetHint(ENV_MB_URL)));
9414
- if (env.apiKeySet) rows.push(note$1(`env.${ENV_MB_API_KEY}`, ENV_MB_API_KEY, "set (value not shown): every `mb` call sends it, whatever `--profile` says, rde's own included", unsetHint(ENV_MB_API_KEY)));
9480
+ if (env.url !== null) rows.push(note$1(`env.${ENV_MB_URL}`, ENV_MB_URL, `every \`mb\` call goes to ${hostOf(env.url)}, whatever \`--profile\` says`, unsetHint(ENV_MB_URL)));
9481
+ if (env.apiKeySet) rows.push(note$1(`env.${ENV_MB_API_KEY}`, ENV_MB_API_KEY, "set (value not shown): every `mb` call sends it, whatever `--profile` says", unsetHint(ENV_MB_API_KEY)));
9415
9482
  return rows;
9416
9483
  }
9417
9484
  const PROFILE_STATUS_WORDS = {
@@ -9513,12 +9580,13 @@ async function sampleDatabaseRow(record, deps, dockerOk, access) {
9513
9580
  function isMbFailure$1(error) {
9514
9581
  return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
9515
9582
  }
9516
- async function gitSyncRow(state, deps) {
9583
+ async function gitSyncRow(state, instance, deps) {
9517
9584
  const label = "remote sync";
9518
9585
  if (!hasRemoteSync(state.license)) return skipped("git-sync", label, `needs a license with ${REMOTE_SYNC_FEATURE}`);
9519
9586
  if (state.gitRemote === null) return skipped("git-sync", label, "not configured; `rde init --from git-sync`");
9587
+ if (instance === null) return skipped("git-sync", label, "no API key in the secrets file");
9520
9588
  try {
9521
- const status = await mbTool(deps.runner).gitSyncStatus(state.mbProfile);
9589
+ const status = await mbTool(deps.runner).gitSyncStatus(instance);
9522
9590
  if (status.branch === null) return failed("git-sync", label, {
9523
9591
  detail: "Metabase reports no branch",
9524
9592
  hint: "`rde init --only git-sync`",
@@ -9676,13 +9744,13 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
9676
9744
  properties === null ? skipped("version", "Metabase version", NOT_HEALTHY_REASON) : versionRow(state, properties),
9677
9745
  properties === null ? skipped("admin", "admin user", NOT_HEALTHY_REASON) : adminRow(state, properties),
9678
9746
  ...await credentialRows(state, secrets, apiKey, deps),
9679
- await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
9747
+ await mbProfileRow(state, configured.url, deps, profiles, mbUnusableReason, healthy, apiKey.access)
9680
9748
  ];
9681
9749
  const access = apiKey.access;
9682
9750
  if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
9683
9751
  else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
9684
9752
  rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
9685
- rows.push(await gitSyncRow(state, deps));
9753
+ rows.push(await gitSyncRow(state, mbInstanceOf(state, secrets), deps));
9686
9754
  return rows;
9687
9755
  }
9688
9756
  async function collectDoctor(instance, state, secrets, deps) {
@@ -10257,20 +10325,12 @@ async function moveToPort(ctx, port, deps) {
10257
10325
  await runtimeFromState(next, deps.runtime).reconfigure();
10258
10326
  ctx.ui.info(`Moved instance ${ctx.state.name} from port ${previous} to ${port}`);
10259
10327
  }
10260
- async function profileStatus(runner, profile) {
10261
- try {
10262
- return await mbTool(runner).authStatus(profile);
10263
- } catch (error) {
10264
- if (error instanceof ProcessNotFoundError) return null;
10265
- throw error;
10266
- }
10267
- }
10268
10328
  async function followMove(ctx, from, runner) {
10269
10329
  const configured = requireConfiguredRuntime(ctx.state);
10270
10330
  const apiKey = ctx.secrets.apiKey;
10271
10331
  const profile = ctx.state.mbProfile;
10272
- const status = apiKey === null ? null : await profileStatus(runner, profile);
10273
- if (apiKey !== null && status !== null && status.present && status.url === from) {
10332
+ const status = apiKey === null ? null : await mbTool(runner).authStatusIfInstalled(profile);
10333
+ if (apiKey !== null && isLoggedInAt(status, from)) {
10274
10334
  await mbTool(runner).authLogin({
10275
10335
  url: configured.url,
10276
10336
  profile,
@@ -10353,15 +10413,12 @@ function setupOf(ctx, steps) {
10353
10413
  resume: `rde init --from ${pending.id}`
10354
10414
  };
10355
10415
  }
10356
- function isNotServing(error) {
10357
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
10358
- }
10359
10416
  async function databasesOf(ctx, deps, health) {
10360
10417
  if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
10361
10418
  try {
10362
10419
  return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
10363
10420
  } catch (error) {
10364
- if (isNotServing(error) || isRejectedCredential(error)) return null;
10421
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return null;
10365
10422
  throw error;
10366
10423
  }
10367
10424
  }
@@ -10391,10 +10448,13 @@ function isMbFailure(error) {
10391
10448
  }
10392
10449
  async function gitSyncOf(ctx, deps) {
10393
10450
  if (ctx.state.gitRemote === null) return null;
10451
+ if (configuredRuntime(ctx.state) === null) return { error: "no Metabase runtime configured; `rde init`" };
10452
+ const instance = mbInstanceOf(ctx.state, ctx.secrets);
10453
+ if (instance === null) return { error: "no API key in the secrets file; `rde init --only api-key`" };
10394
10454
  try {
10395
10455
  const mb = mbTool(deps.runner);
10396
- const status = await mb.gitSyncStatus(ctx.state.mbProfile);
10397
- const changes = await mb.gitSyncHasRemoteChanges(ctx.state.mbProfile);
10456
+ const status = await mb.gitSyncStatus(instance);
10457
+ const changes = await mb.gitSyncHasRemoteChanges(instance);
10398
10458
  return {
10399
10459
  branch: status.branch,
10400
10460
  dirty: status.is_dirty,
@@ -10555,7 +10615,6 @@ const stop = defineRdeCommand({
10555
10615
  });
10556
10616
  //#endregion
10557
10617
  //#region src/steps/uninstall.ts
10558
- const API_KEY_METHOD = "apiKey";
10559
10618
  function hostUninstallDeps(signal, childOutput) {
10560
10619
  const runner = processRunner(signal);
10561
10620
  return {
@@ -10714,8 +10773,8 @@ async function findJar(source, removeImage) {
10714
10773
  }
10715
10774
  function isInstanceProfile(status, urls) {
10716
10775
  if (!status.present) return false;
10717
- if (urls !== null) return status.url !== null && urls.includes(status.url);
10718
- return status.method === API_KEY_METHOD && status.user?.name === "rde";
10776
+ if (urls !== null) return urls.some((url) => isLoggedInAt(status, url));
10777
+ return status.method === MbAuthMethod.enum.apiKey && status.user?.name === "rde";
10719
10778
  }
10720
10779
  function instanceUrls(state) {
10721
10780
  if (state.url === null) return null;
@@ -10723,23 +10782,34 @@ function instanceUrls(state) {
10723
10782
  }
10724
10783
  async function findMbProfiles(candidates, urls, runner) {
10725
10784
  const mb = mbTool(runner);
10726
- const removals = [];
10785
+ const found = {
10786
+ removals: [],
10787
+ kept: []
10788
+ };
10727
10789
  for (const profile of candidates) {
10728
10790
  let status;
10729
10791
  try {
10730
10792
  status = await mb.authStatus(profile);
10731
10793
  } catch (error) {
10732
- if (error instanceof ProcessNotFoundError) return [];
10794
+ if (error instanceof ProcessNotFoundError) return {
10795
+ removals: [],
10796
+ kept: []
10797
+ };
10733
10798
  throw error;
10734
10799
  }
10735
- if (isInstanceProfile(status, urls)) removals.push({
10800
+ if (!isInstanceProfile(status, urls)) continue;
10801
+ if (status.method === MbAuthMethod.enum.oauth) {
10802
+ found.kept.push(`mb profile ${profile}, logged in with OAuth; \`mb auth logout --profile ${profile}\` logs it out`);
10803
+ continue;
10804
+ }
10805
+ found.removals.push({
10736
10806
  label: `mb profile ${profile}`,
10737
10807
  remove: async () => {
10738
10808
  await mb.authLogout(profile);
10739
10809
  }
10740
10810
  });
10741
10811
  }
10742
- return removals;
10812
+ return found;
10743
10813
  }
10744
10814
  function skillLabel(names, entries) {
10745
10815
  const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
@@ -10821,7 +10891,8 @@ async function planUninstall(options, deps) {
10821
10891
  if (jar.kept !== null) kept.push(jar.kept);
10822
10892
  }
10823
10893
  const profiles = state === null ? await findMbProfiles([.../* @__PURE__ */ new Set([mbProfileFor(options.instance), namedMbProfile(options.instance)])], null, deps.runner) : await findMbProfiles([state.mbProfile], instanceUrls(state), deps.runner);
10824
- removals.push(...profiles);
10894
+ removals.push(...profiles.removals);
10895
+ kept.push(...profiles.kept);
10825
10896
  if (options.removeSkill) {
10826
10897
  const skill = await findSkill(deps.runner, deps.childOutput);
10827
10898
  if (skill !== null) removals.push(skill);
@@ -11238,7 +11309,7 @@ const upgradeCommand = defineRdeCommand({
11238
11309
  const main = defineCommand({
11239
11310
  meta: {
11240
11311
  name: "rde",
11241
- version: "0.1.14",
11312
+ version: "0.1.15",
11242
11313
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
11243
11314
  },
11244
11315
  subCommands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metabase/rde",
3
- "version": "0.1.14",
3
+ "version": "0.1.15",
4
4
  "description": "Prepares a local machine for data-engineering work with Metabase and coding agents",
5
5
  "license": "AGPL-3.0",
6
6
  "bin": {