@metabase/rde 0.1.13 → 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 (3) hide show
  1. package/README.md +3 -3
  2. package/dist/cli.mjs +527 -193
  3. package/package.json +2 -2
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { defineCommand, renderUsage, runMain } from "citty";
3
3
  import { createHash, randomBytes, randomUUID } from "node:crypto";
4
4
  import { createWriteStream, promises } from "node:fs";
5
- import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
5
+ import { delimiter, dirname, isAbsolute, join, posix, resolve, sep } from "node:path";
6
6
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
7
  import { ZodError, z } from "zod";
8
8
  import { homedir, tmpdir, userInfo } from "node:os";
@@ -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,26 +58,34 @@ 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;
72
79
  path;
73
80
  errors;
81
+ reason;
74
82
  constructor(failure) {
75
83
  super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
76
84
  this.name = "MetabaseApiError";
77
85
  this.status = failure.status;
78
86
  this.path = failure.path;
79
87
  this.errors = failure.errors;
88
+ this.reason = failure.message;
80
89
  }
81
90
  };
82
91
  const UNAUTHORIZED_STATUS = 401;
@@ -93,12 +102,17 @@ var MetabaseInitializingError = class extends Error {
93
102
  };
94
103
  var MetabaseUnreachableError = class extends Error {
95
104
  url;
105
+ reason;
96
106
  constructor(url, reason) {
97
107
  super(`Metabase at ${url} is unreachable: ${reason}`);
98
108
  this.name = "MetabaseUnreachableError";
99
109
  this.url = url;
110
+ this.reason = reason;
100
111
  }
101
112
  };
113
+ function isMetabaseNotServing(error) {
114
+ return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
115
+ }
102
116
  var DownloadError = class extends Error {
103
117
  url;
104
118
  constructor(url, reason) {
@@ -892,6 +906,11 @@ function describeCause(error) {
892
906
  if (error instanceof Error && error.cause instanceof Error) return error.cause.message;
893
907
  return error instanceof Error ? error.message : String(error);
894
908
  }
909
+ function lookupFailure(error) {
910
+ if (error instanceof MetabaseUnreachableError) return `${error.url} is unreachable: ${error.reason}`;
911
+ if (error instanceof MetabaseApiError) return `HTTP ${error.status} on ${error.path}: ${error.reason}`;
912
+ return null;
913
+ }
895
914
  function createHttp(options) {
896
915
  const fetchImpl = options.fetchImpl ?? fetch;
897
916
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
@@ -911,7 +930,11 @@ function createHttp(options) {
911
930
  init.body = JSON.stringify(body);
912
931
  }
913
932
  try {
914
- return await fetchImpl(url, init);
933
+ const response = await fetchImpl(url, init);
934
+ return {
935
+ ok: response.ok,
936
+ body: parseBody(response.status, await response.text())
937
+ };
915
938
  } catch (error) {
916
939
  if (options.signal.aborted) throw new AbortError();
917
940
  if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${REQUEST_TIMEOUT_MS}ms`);
@@ -919,10 +942,9 @@ function createHttp(options) {
919
942
  }
920
943
  }
921
944
  async function requestBody(method, path, body) {
922
- const response = await send(method, path, body);
923
- const parsed = parseBody(response.status, await response.text());
924
- if (!response.ok) throw failureFor(path, parsed);
925
- return parsed;
945
+ const received = await send(method, path, body);
946
+ if (!received.ok) throw failureFor(path, received.body);
947
+ return received.body;
926
948
  }
927
949
  return {
928
950
  async requestJson(method, path, request) {
@@ -1336,9 +1358,6 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
1336
1358
  user: userResource(http)
1337
1359
  };
1338
1360
  }
1339
- function isNotReadyYet(error) {
1340
- return error instanceof MetabaseInitializingError || error instanceof MetabaseUnreachableError;
1341
- }
1342
1361
  async function waitForHealth(client, options) {
1343
1362
  const deadline = Date.now() + options.timeoutMs;
1344
1363
  const interval = options.pollIntervalMs ?? HEALTH_POLL_INTERVAL_MS;
@@ -1347,7 +1366,7 @@ async function waitForHealth(client, options) {
1347
1366
  await client.health.get();
1348
1367
  return;
1349
1368
  } catch (error) {
1350
- if (!isNotReadyYet(error)) throw error;
1369
+ if (!isMetabaseNotServing(error)) throw error;
1351
1370
  if (error instanceof MetabaseInitializingError) options.onProgress(error.progress);
1352
1371
  }
1353
1372
  if (!await options.isRunning()) throw new MetabaseUnreachableError(client.baseUrl, "it exited before it became healthy");
@@ -1374,15 +1393,12 @@ function storedApiKey(state, secrets) {
1374
1393
  function hostCredentialsClient(signal) {
1375
1394
  return (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal);
1376
1395
  }
1377
- function isNotReady(error) {
1378
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
1379
- }
1380
1396
  async function hasAdminOutsideRde(state, client) {
1381
1397
  if (state.url === null) return false;
1382
1398
  try {
1383
1399
  return (await client(state.url, NO_AUTH).session.properties())["has-user-setup"];
1384
1400
  } catch (error) {
1385
- if (isNotReady(error)) return false;
1401
+ if (isMetabaseNotServing(error)) return false;
1386
1402
  throw error;
1387
1403
  }
1388
1404
  }
@@ -1818,10 +1834,11 @@ function signalProcess(pid, signal) {
1818
1834
  function stderrTail(stderr) {
1819
1835
  return stderr.trim().split("\n").slice(-5).join("\n");
1820
1836
  }
1821
- function toolFailure(tool, argv, result) {
1837
+ function toolFailure(tool, argv, result, options = {}) {
1822
1838
  return new ExternalToolError({
1823
1839
  tool,
1824
1840
  argv,
1841
+ envNames: options.addedEnv === void 0 ? [] : Object.keys(options.addedEnv),
1825
1842
  exitCode: result.exitCode,
1826
1843
  stderrTail: stderrTail(result.stderr)
1827
1844
  });
@@ -1833,6 +1850,7 @@ async function runShown(runner, output, command, args) {
1833
1850
  if (exitCode !== 0) throw new ExternalToolError({
1834
1851
  tool: command,
1835
1852
  argv: args,
1853
+ envNames: [],
1836
1854
  exitCode,
1837
1855
  stderrTail: ""
1838
1856
  });
@@ -1844,6 +1862,7 @@ async function runShown(runner, output, command, args) {
1844
1862
  throw new ExternalToolError({
1845
1863
  tool: command,
1846
1864
  argv: args,
1865
+ envNames: [],
1847
1866
  exitCode: result.exitCode,
1848
1867
  stderrTail: stderrTail(printed.filter((line) => HAS_TEXT.test(line)).join("\n"))
1849
1868
  });
@@ -1857,6 +1876,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
1857
1876
  throw new ExternalToolError({
1858
1877
  tool,
1859
1878
  argv,
1879
+ envNames: [],
1860
1880
  exitCode: 0,
1861
1881
  stderrTail: `output is not JSON: ${error instanceof Error ? error.message : String(error)}`
1862
1882
  });
@@ -1867,6 +1887,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
1867
1887
  if (error instanceof ZodError) throw new ExternalToolError({
1868
1888
  tool,
1869
1889
  argv,
1890
+ envNames: [],
1870
1891
  exitCode: 0,
1871
1892
  stderrTail: `unexpected output shape:\n${z.prettifyError(error)}`
1872
1893
  });
@@ -2332,6 +2353,7 @@ function dockerTool(runner) {
2332
2353
  throw new ExternalToolError({
2333
2354
  tool: DOCKER,
2334
2355
  argv: args,
2356
+ envNames: [],
2335
2357
  exitCode: 0,
2336
2358
  stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
2337
2359
  });
@@ -3083,7 +3105,7 @@ function hostDeps$14(ctx) {
3083
3105
  };
3084
3106
  }
3085
3107
  function isNotMetabaseYet(error) {
3086
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || error instanceof MetabaseApiError;
3108
+ return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
3087
3109
  }
3088
3110
  function validateEmail(value) {
3089
3111
  return EMAIL.test(value) ? void 0 : "Enter an email address";
@@ -3284,7 +3306,9 @@ function adminUserStep(options, deps = hostDeps$14) {
3284
3306
  //#endregion
3285
3307
  //#region src/tools/mb.ts
3286
3308
  const MB = "mb";
3287
- const MB_INSTALL_SPEC = `@metabase/cli@alpha-release-65-integration-branch`;
3309
+ const MB_PACKAGE = "@metabase/cli";
3310
+ const MB_DIST_TAG = "alpha-release-65-integration-branch";
3311
+ const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
3288
3312
  const GLOBAL_INSTALL = {
3289
3313
  npm: {
3290
3314
  command: "npm",
@@ -3441,11 +3465,12 @@ const LogoutResult = z.object({
3441
3465
  cleared: z.boolean(),
3442
3466
  aborted: z.boolean()
3443
3467
  }).loose();
3468
+ const MbAuthMethod = z.enum(["oauth", "apiKey"]);
3444
3469
  const AuthStatus = z.object({
3445
3470
  profile: z.string(),
3446
3471
  present: z.boolean(),
3447
3472
  url: z.string().nullable(),
3448
- method: z.string().nullable(),
3473
+ method: MbAuthMethod.nullable(),
3449
3474
  user: ProbedUser.nullable()
3450
3475
  }).loose();
3451
3476
  const AuthProfileStatus = z.enum([
@@ -3458,7 +3483,6 @@ const AuthProfileStatus = z.enum([
3458
3483
  const AuthProfile = z.object({
3459
3484
  profile: z.string(),
3460
3485
  url: z.string(),
3461
- authenticated: z.boolean(),
3462
3486
  status: AuthProfileStatus,
3463
3487
  user: ProbedUser.nullable()
3464
3488
  }).loose();
@@ -3493,11 +3517,41 @@ const RemoteChanges = z.object({
3493
3517
  remote_version: z.string().nullable(),
3494
3518
  local_version: z.string().nullable()
3495
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
+ }
3496
3550
  function mbTool(runner) {
3497
- async function mbJson(args, schema, stdin) {
3551
+ async function mbJson(args, schema, options = {}) {
3498
3552
  const argv = [...args, "--json"];
3499
- const result = await runner.run(MB, argv, stdin === void 0 ? {} : { stdin });
3500
- 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);
3501
3555
  return parseJsonOutput(MB, argv, result.stdout, schema);
3502
3556
  }
3503
3557
  async function version(command = MB) {
@@ -3520,7 +3574,7 @@ function mbTool(runner) {
3520
3574
  options.url,
3521
3575
  "--profile",
3522
3576
  options.profile
3523
- ], LoginResult, `${options.apiKey}\n`);
3577
+ ], LoginResult, { stdin: `${options.apiKey}\n` });
3524
3578
  }
3525
3579
  async function authStatus(profile) {
3526
3580
  return mbJson([
@@ -3530,6 +3584,14 @@ function mbTool(runner) {
3530
3584
  profile
3531
3585
  ], AuthStatus);
3532
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
+ }
3533
3595
  async function authLogout(profile) {
3534
3596
  return mbJson([
3535
3597
  "auth",
@@ -3540,52 +3602,46 @@ function mbTool(runner) {
3540
3602
  ], LogoutResult);
3541
3603
  }
3542
3604
  async function authList() {
3543
- return (await mbJson(["auth", "list"], AuthList)).data;
3605
+ return (await mbJson([
3606
+ "auth",
3607
+ "list",
3608
+ "--fields",
3609
+ "profile,url,status,user",
3610
+ "--max-bytes",
3611
+ "0"
3612
+ ], AuthList)).data;
3544
3613
  }
3545
- async function gitSyncStatus(profile) {
3546
- return mbJson([
3547
- "git-sync",
3548
- "status",
3549
- "--profile",
3550
- profile
3551
- ], GitSyncStatus);
3614
+ async function gitSyncStatus(instance) {
3615
+ return mbJson(["git-sync", "status"], GitSyncStatus, instanceEnv(instance));
3552
3616
  }
3553
- async function gitSyncHasRemoteChanges(profile) {
3554
- return mbJson([
3555
- "git-sync",
3556
- "has-remote-changes",
3557
- "--profile",
3558
- profile
3559
- ], RemoteChanges);
3617
+ async function gitSyncHasRemoteChanges(instance) {
3618
+ return mbJson(["git-sync", "has-remote-changes"], RemoteChanges, instanceEnv(instance));
3560
3619
  }
3561
- async function gitSyncImport(profile, options) {
3620
+ async function gitSyncImport(instance, options) {
3562
3621
  return mbJson([
3563
3622
  "git-sync",
3564
3623
  "import",
3565
- "--profile",
3566
- profile,
3567
3624
  "--branch",
3568
3625
  options.branch,
3569
3626
  "--wait"
3570
- ], GitSyncOutcome);
3627
+ ], GitSyncOutcome, instanceEnv(instance));
3571
3628
  }
3572
- async function gitSyncExport(profile, options) {
3629
+ async function gitSyncExport(instance, options) {
3573
3630
  return mbJson([
3574
3631
  "git-sync",
3575
3632
  "export",
3576
- "--profile",
3577
- profile,
3578
3633
  "--branch",
3579
3634
  options.branch,
3580
3635
  "--message",
3581
3636
  options.message,
3582
3637
  "--wait"
3583
- ], GitSyncOutcome);
3638
+ ], GitSyncOutcome, instanceEnv(instance));
3584
3639
  }
3585
3640
  return {
3586
3641
  version,
3587
3642
  authLogin,
3588
3643
  authStatus,
3644
+ authStatusIfInstalled,
3589
3645
  authLogout,
3590
3646
  authList,
3591
3647
  gitSyncStatus,
@@ -3606,7 +3662,8 @@ const ADMINISTRATORS_GROUP_NAME = "Administrators";
3606
3662
  function hostDeps$13(ctx) {
3607
3663
  return {
3608
3664
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3609
- runner: processRunner(ctx.signal)
3665
+ runner: processRunner(ctx.signal),
3666
+ instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
3610
3667
  };
3611
3668
  }
3612
3669
  async function adminCredentials(ctx) {
@@ -3662,6 +3719,13 @@ async function authenticates(client) {
3662
3719
  throw error;
3663
3720
  }
3664
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
+ }
3665
3729
  async function workingApiKey(ctx, deps, url) {
3666
3730
  const stored = ctx.secrets.apiKey;
3667
3731
  if (stored !== null) {
@@ -3671,6 +3735,7 @@ async function workingApiKey(ctx, deps, url) {
3671
3735
  }))) return stored;
3672
3736
  ctx.ui.warn("Metabase rejects the stored API key; replacing it");
3673
3737
  }
3738
+ await forgetCompletion(ctx);
3674
3739
  const sessionId = await superuserSession(ctx, deps, url);
3675
3740
  const apiKey = await freshApiKey(ctx, deps.client(url, {
3676
3741
  kind: "session",
@@ -3682,20 +3747,24 @@ async function workingApiKey(ctx, deps, url) {
3682
3747
  });
3683
3748
  return apiKey;
3684
3749
  }
3685
- async function defaultProfileStatus(mb) {
3686
- try {
3687
- return await mb.authStatus(DEFAULT_MB_PROFILE);
3688
- } catch (error) {
3689
- if (error instanceof ProcessNotFoundError) return null;
3690
- throw error;
3691
- }
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;
3692
3755
  }
3693
3756
  async function chooseProfile(ctx, mb, url) {
3694
3757
  const profile = ctx.state.mbProfile;
3695
- if (profile !== "default") return profile;
3696
- const existing = await defaultProfileStatus(mb);
3697
- if (existing === null || !existing.present || existing.url === null || existing.url === url || existing.url === ctx.state.movedFrom) return profile;
3698
- 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
+ };
3699
3768
  const named = namedMbProfile(ctx.state.name);
3700
3769
  if (await ctx.prompts.select({
3701
3770
  key: API_KEY_KEYS.profile,
@@ -3710,13 +3779,19 @@ async function chooseProfile(ctx, mb, url) {
3710
3779
  recommended: "default"
3711
3780
  }) === "default") {
3712
3781
  ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
3713
- return profile;
3782
+ return {
3783
+ profile,
3784
+ status
3785
+ };
3714
3786
  }
3715
3787
  await ctx.saveState({
3716
3788
  ...ctx.state,
3717
3789
  mbProfile: named
3718
3790
  });
3719
- return named;
3791
+ return {
3792
+ profile: named,
3793
+ status: await mb.authStatusIfInstalled(named)
3794
+ };
3720
3795
  }
3721
3796
  function usageNote(profile) {
3722
3797
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
@@ -3728,31 +3803,34 @@ function apiKeyStep(deps = hostDeps$13) {
3728
3803
  title: "Connect the Metabase CLI",
3729
3804
  redoHint: "`rde status` shows the mb profile it uses.",
3730
3805
  async detect(ctx) {
3731
- if (ctx.secrets.apiKey === null) return "todo";
3732
- const mb = mbTool(deps(ctx).runner);
3733
- 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";
3734
3812
  try {
3735
- 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";
3736
3817
  } catch (error) {
3737
- if (error instanceof ProcessNotFoundError) return "todo";
3818
+ if (isMetabaseNotServing(error) || error instanceof MetabaseApiError) return "todo";
3738
3819
  throw error;
3739
3820
  }
3740
- return (await mb.authList()).find((candidate) => candidate.profile === profile)?.status === "ok" ? "done" : "todo";
3741
3821
  },
3742
3822
  async run(ctx) {
3743
3823
  const resolved = deps(ctx);
3744
3824
  const configured = requireConfiguredRuntime(ctx.state);
3745
3825
  const apiKey = await workingApiKey(ctx, resolved, configured.url);
3746
3826
  const mb = mbTool(resolved.runner);
3747
- const profile = await chooseProfile(ctx, mb, configured.url);
3748
- 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({
3749
3830
  url: configured.url,
3750
3831
  profile,
3751
3832
  apiKey
3752
3833
  });
3753
- const entry = (await mb.authList()).find((candidate) => candidate.profile === profile);
3754
- if (entry === void 0) throw new ConfigError(`\`mb auth list\` does not show profile ${profile} after login`);
3755
- if (entry.status !== "ok") throw new ConfigError(`mb profile ${profile} reports ${entry.status} after login`);
3756
3834
  if (ctx.state.movedFrom !== null) await ctx.saveState({
3757
3835
  ...ctx.state,
3758
3836
  movedFrom: null
@@ -5999,7 +6077,7 @@ async function applySettings(run, configured) {
5999
6077
  async function remoteHasContent(run, gitRemote) {
6000
6078
  switch (gitRemote.kind) {
6001
6079
  case "local-bare": return run.git.hasMetabaseContent(gitRemote.workdir);
6002
- 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;
6003
6081
  }
6004
6082
  }
6005
6083
  function reportImport(run, outcome) {
@@ -6021,7 +6099,7 @@ async function importExistingContent(run, gitRemote) {
6021
6099
  const spinner = run.ctx.ui.spinner("Importing the repository's content into Metabase…");
6022
6100
  let outcome;
6023
6101
  try {
6024
- 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 });
6025
6103
  } catch (error) {
6026
6104
  spinner.fail("Import failed");
6027
6105
  throw error;
@@ -6065,15 +6143,17 @@ function isMbUnavailable(error) {
6065
6143
  }
6066
6144
  async function runGitSync(ctx, deps) {
6067
6145
  const configured = requireConfiguredRuntime(ctx.state);
6068
- 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");
6069
6148
  const run = {
6070
6149
  ctx,
6071
6150
  deps,
6072
6151
  git: gitTool(deps.runner),
6073
- client: deps.client(configured.url, {
6152
+ client: deps.client(instance.url, {
6074
6153
  kind: "api-key",
6075
- key: ctx.secrets.apiKey
6076
- })
6154
+ key: instance.apiKey
6155
+ }),
6156
+ instance
6077
6157
  };
6078
6158
  const flow = await runFlow({
6079
6159
  remote: remoteScreen(run),
@@ -6103,8 +6183,10 @@ function gitSyncStep(deps = hostDeps$12) {
6103
6183
  if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
6104
6184
  if (ctx.state.steps[STEP_ID$8]?.status === "skipped") return "not-applicable";
6105
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";
6106
6188
  try {
6107
- 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";
6108
6190
  } catch (error) {
6109
6191
  if (isMbUnavailable(error)) return "todo";
6110
6192
  throw error;
@@ -6116,17 +6198,45 @@ function gitSyncStep(deps = hostDeps$12) {
6116
6198
  };
6117
6199
  }
6118
6200
  //#endregion
6201
+ //#region src/metabase/domain/github-content.ts
6202
+ const GitHubContentEntry = z.object({
6203
+ name: z.string(),
6204
+ type: z.string(),
6205
+ sha: z.string()
6206
+ }).loose();
6207
+ const GitHubDirectory = z.array(GitHubContentEntry);
6208
+ //#endregion
6209
+ //#region src/metabase/github.ts
6210
+ const GITHUB_API = "https://api.github.com";
6211
+ const DIRECTORY_TYPE = "dir";
6212
+ function githubContents(signal, options = {}) {
6213
+ const http = createHttp({
6214
+ baseUrl: GITHUB_API,
6215
+ auth: NO_AUTH,
6216
+ signal,
6217
+ ...options
6218
+ });
6219
+ return { async subdirectoryHashes(repository, ref, directory) {
6220
+ const path = `/repos/${repository}/contents/${directory}${ref === null ? "" : `?ref=${encodeURIComponent(ref)}`}`;
6221
+ const entries = await http.requestJson("GET", path, { schema: GitHubDirectory });
6222
+ return new Map(entries.filter((entry) => entry.type === DIRECTORY_TYPE).map((entry) => [entry.name, entry.sha]));
6223
+ } };
6224
+ }
6225
+ //#endregion
6119
6226
  //#region src/tools/skills-cli.ts
6120
6227
  const NPX = "npx";
6121
6228
  const SKILLS_PACKAGE = "skills@latest";
6122
6229
  const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
6123
6230
  const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
6124
- const DATA_APP_SKILLS_SOURCE = "metabase/metabase/skills#master";
6231
+ const DATA_APP_REPOSITORY = "metabase/metabase";
6232
+ const DATA_APP_BRANCH = "master";
6233
+ const DATA_APP_SKILLS_SOURCE = `${DATA_APP_REPOSITORY}/skills#${DATA_APP_BRANCH}`;
6125
6234
  const DATA_APP_SKILL_NAMES = [
6126
6235
  "metabase-data-app-setup",
6127
6236
  "metabase-data-app-routing",
6128
6237
  "metabase-data-app-actions",
6129
- "metabase-data-app-semantic-layer"
6238
+ "metabase-data-app-semantic-layer",
6239
+ "metabase-data-app-migrate"
6130
6240
  ];
6131
6241
  function rdeSkillSource() {
6132
6242
  const override = readEnv(ENV_RDE_SKILL_SOURCE);
@@ -6140,7 +6250,13 @@ const InstalledSkill = z.object({
6140
6250
  source: z.string().nullable()
6141
6251
  }).loose();
6142
6252
  const InstalledSkillList = z.array(InstalledSkill);
6143
- const SkillLock = z.object({ skills: z.record(z.string(), z.object({ ref: z.string().nullish() }).loose()) }).loose();
6253
+ const LockedSkill = z.object({
6254
+ source: z.string().nullish(),
6255
+ ref: z.string().nullish(),
6256
+ skillPath: z.string().nullish(),
6257
+ skillFolderHash: z.string().nullish()
6258
+ }).loose();
6259
+ const SkillLock = z.object({ skills: z.record(z.string(), LockedSkill) }).loose();
6144
6260
  const GLOBAL_LOCK_FILE = ".skill-lock.json";
6145
6261
  const PROJECT_LOCK_FILE = "skills-lock.json";
6146
6262
  function globalSkillLockPath(home, stateHome) {
@@ -6153,16 +6269,21 @@ function xdgStateHome() {
6153
6269
  const value = readEnv(ENV_XDG_STATE_HOME);
6154
6270
  return value === void 0 || value.length === 0 ? null : value;
6155
6271
  }
6156
- function lockedRef(path, raw, skill) {
6272
+ function lockedSkill(path, raw, skill) {
6157
6273
  if (raw === null) return null;
6158
6274
  const parsed = SkillLock.safeParse(raw);
6159
6275
  if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
6160
- return parsed.data.skills[skill]?.ref ?? null;
6276
+ return parsed.data.skills[skill] ?? null;
6161
6277
  }
6162
6278
  const hostSkillFiles = {
6163
6279
  readJson: readJsonFile,
6164
6280
  readText: readTextFile
6165
6281
  };
6282
+ const DATA_APP_SKILLS_ORIGIN = {
6283
+ kind: "github",
6284
+ repository: DATA_APP_REPOSITORY,
6285
+ ref: DATA_APP_BRANCH
6286
+ };
6166
6287
  const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
6167
6288
  function skillOrigin(source, cwd) {
6168
6289
  const github = GITHUB_URL.exec(source);
@@ -6252,7 +6373,11 @@ function hostDeps$11(ctx) {
6252
6373
  return {
6253
6374
  runner: processRunner(ctx.signal),
6254
6375
  probe: hostAgentProbe(),
6255
- skillSource: rdeSkillSource()
6376
+ skillSource: rdeSkillSource(),
6377
+ stateHome: xdgStateHome(),
6378
+ files: hostSkillFiles,
6379
+ github: githubContents(ctx.signal),
6380
+ cwd: process.cwd()
6256
6381
  };
6257
6382
  }
6258
6383
  function detectedOnly(agents) {
@@ -6287,10 +6412,91 @@ function coveredAgentNames(entry, agents) {
6287
6412
  function installedLines(entries, agents) {
6288
6413
  return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
6289
6414
  }
6415
+ function skillSets(deps) {
6416
+ return [{
6417
+ label: "data-app",
6418
+ source: DATA_APP_SKILLS_SOURCE,
6419
+ sourceNote: "",
6420
+ origin: DATA_APP_SKILLS_ORIGIN,
6421
+ skills: DATA_APP_SKILL_NAMES
6422
+ }, {
6423
+ label: "rde",
6424
+ source: deps.skillSource,
6425
+ sourceNote: deps.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${deps.skillSource}`,
6426
+ origin: skillOrigin(deps.skillSource, deps.cwd),
6427
+ skills: RDE_SOURCE_SKILL_NAMES
6428
+ }];
6429
+ }
6430
+ function hashLookup(github) {
6431
+ const asked = /* @__PURE__ */ new Map();
6432
+ return (repository, ref, directory) => {
6433
+ const key = JSON.stringify([
6434
+ repository,
6435
+ ref,
6436
+ directory
6437
+ ]);
6438
+ let hashes = asked.get(key);
6439
+ if (hashes === void 0) {
6440
+ hashes = github.subdirectoryHashes(repository, ref, directory).catch((error) => {
6441
+ if (lookupFailure(error) === null) throw error;
6442
+ return null;
6443
+ });
6444
+ asked.set(key, hashes);
6445
+ }
6446
+ return hashes;
6447
+ };
6448
+ }
6449
+ async function readLockedSkills(deps) {
6450
+ const lockPath = globalSkillLockPath(deps.probe.home, deps.stateHome);
6451
+ try {
6452
+ const lock = await deps.files.readJson(lockPath);
6453
+ return new Map(REQUIRED_SKILL_NAMES.map((name) => [name, lockedSkill(lockPath, lock, name)]));
6454
+ } catch (error) {
6455
+ if (error instanceof ConfigError) return null;
6456
+ throw error;
6457
+ }
6458
+ }
6459
+ function lockedCopy(locked, origin) {
6460
+ if (locked === null || locked.source !== origin.repository) return null;
6461
+ const ref = locked.ref ?? null;
6462
+ if (origin.ref !== null && ref !== origin.ref) return null;
6463
+ if (locked.skillPath === null || locked.skillPath === void 0) return null;
6464
+ if (locked.skillFolderHash === null || locked.skillFolderHash === void 0) return null;
6465
+ const folder = posix.dirname(locked.skillPath);
6466
+ return {
6467
+ ref,
6468
+ directory: posix.dirname(folder),
6469
+ folder: posix.basename(folder),
6470
+ hash: locked.skillFolderHash
6471
+ };
6472
+ }
6473
+ async function setBehind(set, check) {
6474
+ const { origin } = set;
6475
+ if (origin.kind !== "github" || check.locked === null) return true;
6476
+ const copies = [];
6477
+ for (const skill of set.skills) {
6478
+ const copy = lockedCopy(check.locked.get(skill) ?? null, origin);
6479
+ if (copy === null) return true;
6480
+ copies.push(copy);
6481
+ }
6482
+ if (check.updates === "installed") return false;
6483
+ for (const copy of copies) {
6484
+ const current = await check.hashes(origin.repository, copy.ref, copy.directory);
6485
+ if (current !== null && current.get(copy.folder) !== copy.hash) return true;
6486
+ }
6487
+ return false;
6488
+ }
6489
+ function hasSkill(installed, skill, agent) {
6490
+ return installed.some((entry) => entry.name === skill && skillCoversAgent(entry, agent));
6491
+ }
6492
+ function coversAll(installed, skills, agents) {
6493
+ return agents.every((agent) => skills.every((skill) => hasSkill(installed, skill, agent)));
6494
+ }
6290
6495
  const SOURCE_NOTES = {
6291
6496
  override: "from --agents",
6292
6497
  detected: "detected on this machine",
6293
- chosen: "as chosen"
6498
+ chosen: "as chosen",
6499
+ installed: "updating the installed copies"
6294
6500
  };
6295
6501
  function agentChoice(agent) {
6296
6502
  return {
@@ -6308,7 +6514,7 @@ async function askAgents(ctx, detected) {
6308
6514
  });
6309
6515
  return detected.filter((agent) => picked.includes(agent.id));
6310
6516
  }
6311
- async function selectAgents(ctx, all, options) {
6517
+ async function selectAgents(ctx, all, options, installedNow) {
6312
6518
  switch (options.kind) {
6313
6519
  case "detected": return {
6314
6520
  agents: detectedOnly(all),
@@ -6327,6 +6533,10 @@ async function selectAgents(ctx, all, options) {
6327
6533
  agents: [],
6328
6534
  source: "detected"
6329
6535
  };
6536
+ if (coversAll(await installedNow(), REQUIRED_SKILL_NAMES, detected)) return {
6537
+ agents: detected,
6538
+ source: "installed"
6539
+ };
6330
6540
  return {
6331
6541
  agents: await askAgents(ctx, detected),
6332
6542
  source: "chosen"
@@ -6337,11 +6547,21 @@ async function selectAgents(ctx, all, options) {
6337
6547
  function skipLine(all, source) {
6338
6548
  switch (source) {
6339
6549
  case "override":
6340
- case "chosen": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
6550
+ case "chosen":
6551
+ case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
6341
6552
  case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
6342
6553
  }
6343
6554
  }
6344
- function agentsSkillsStep(options, deps = hostDeps$11) {
6555
+ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
6556
+ let hashes = null;
6557
+ async function setCheck(resolved) {
6558
+ hashes ??= hashLookup(resolved.github);
6559
+ return {
6560
+ updates,
6561
+ locked: await readLockedSkills(resolved),
6562
+ hashes
6563
+ };
6564
+ }
6345
6565
  return {
6346
6566
  id: STEP_ID$7,
6347
6567
  title: "Install the rde and data-app skills for your coding agents",
@@ -6350,34 +6570,42 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
6350
6570
  const resolved = deps(ctx);
6351
6571
  const detected = detectedOnly(await detectAgents(resolved.probe));
6352
6572
  if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
6353
- return missingSkills(await skillsCli(resolved.runner).list(), detected).length === 0 ? "done" : "todo";
6573
+ if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
6574
+ const check = await setCheck(resolved);
6575
+ for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
6576
+ return "done";
6354
6577
  },
6355
6578
  async run(ctx) {
6356
6579
  const resolved = deps(ctx);
6580
+ const skills = skillsCli(resolved.runner);
6357
6581
  const all = await detectAgents(resolved.probe);
6358
- const selection = await selectAgents(ctx, all, options);
6582
+ let listing = null;
6583
+ const installedNow = () => listing ??= skills.list();
6584
+ const selection = await selectAgents(ctx, all, options, installedNow);
6359
6585
  if (selection.agents.length === 0) {
6360
6586
  ctx.ui.info(skipLine(all, selection.source));
6361
6587
  return { outcome: "skipped" };
6362
6588
  }
6363
- const skills = skillsCli(resolved.runner);
6589
+ const before = await installedNow();
6590
+ const check = await setCheck(resolved);
6591
+ const pending = [];
6592
+ for (const set of skillSets(resolved)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
6593
+ const names = displayNames(selection.agents);
6594
+ if (pending.length === 0) {
6595
+ ctx.ui.success(`The rde and data-app skills are current for ${names}`);
6596
+ return { outcome: "completed" };
6597
+ }
6364
6598
  const agents = selection.agents.map((agent) => agent.skillsCliName);
6365
- const forWhom = `for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`;
6366
- ctx.ui.info(`Installing the data-app skills ${forWhom}`);
6367
- await skills.add({
6368
- source: DATA_APP_SKILLS_SOURCE,
6369
- skills: DATA_APP_SKILL_NAMES,
6370
- agents,
6371
- output: ctx.ui.childOutput
6372
- });
6373
- const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${resolved.skillSource}`;
6374
- ctx.ui.info(`Installing the rde skills ${forWhom}${from}`);
6375
- await skills.add({
6376
- source: resolved.skillSource,
6377
- skills: RDE_SOURCE_SKILL_NAMES,
6378
- agents,
6379
- output: ctx.ui.childOutput
6380
- });
6599
+ const forWhom = `for ${names} (${SOURCE_NOTES[selection.source]})`;
6600
+ for (const set of pending) {
6601
+ ctx.ui.info(`Installing the ${set.label} skills ${forWhom}${set.sourceNote}`);
6602
+ await skills.add({
6603
+ source: set.source,
6604
+ skills: set.skills,
6605
+ agents,
6606
+ output: ctx.ui.childOutput
6607
+ });
6608
+ }
6381
6609
  const installed = await skills.list();
6382
6610
  const missing = unlistedSkills(installed);
6383
6611
  if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
@@ -6398,9 +6626,6 @@ function hostDatabasesDeps(signal) {
6398
6626
  now: () => /* @__PURE__ */ new Date()
6399
6627
  };
6400
6628
  }
6401
- function isNotServingYet$2(error) {
6402
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6403
- }
6404
6629
  function requireApiKey$2(state, apiKey) {
6405
6630
  if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
6406
6631
  return apiKey;
@@ -6541,7 +6766,7 @@ function databasesStep(options, deps = hostDeps$10) {
6541
6766
  try {
6542
6767
  return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
6543
6768
  } catch (error) {
6544
- if (isNotServingYet$2(error) || isRejectedCredential(error)) return "todo";
6769
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
6545
6770
  throw error;
6546
6771
  }
6547
6772
  },
@@ -6555,6 +6780,24 @@ function databasesStep(options, deps = hostDeps$10) {
6555
6780
  };
6556
6781
  }
6557
6782
  //#endregion
6783
+ //#region src/metabase/domain/npm-package.ts
6784
+ const NpmVersionManifest = z.object({ version: z.string() }).loose();
6785
+ //#endregion
6786
+ //#region src/metabase/npm-registry.ts
6787
+ const NPM_REGISTRY = "https://registry.npmjs.org";
6788
+ function npmRegistry(signal, options = {}) {
6789
+ const http = createHttp({
6790
+ baseUrl: NPM_REGISTRY,
6791
+ auth: NO_AUTH,
6792
+ signal,
6793
+ ...options
6794
+ });
6795
+ return { async distTagVersion(packageName, tag) {
6796
+ const path = `/${encodeURIComponent(packageName)}/${encodeURIComponent(tag)}`;
6797
+ return (await http.requestJson("GET", path, { schema: NpmVersionManifest })).version;
6798
+ } };
6799
+ }
6800
+ //#endregion
6558
6801
  //#region src/steps/install-mb.ts
6559
6802
  const STEP_ID$5 = "install-mb";
6560
6803
  const MIN_MB_VERSION = "0.3.0";
@@ -6569,7 +6812,8 @@ function hostDeps$9(ctx) {
6569
6812
  return {
6570
6813
  runner: processRunner(ctx.signal),
6571
6814
  installedWith: rdeInstalledWith(),
6572
- locate: locateMbOnPath
6815
+ locate: locateMbOnPath,
6816
+ publishedVersion: () => npmRegistry(ctx.signal).distTagVersion(MB_PACKAGE, MB_DIST_TAG)
6573
6817
  };
6574
6818
  }
6575
6819
  async function inspectMb(runner) {
@@ -6583,6 +6827,38 @@ async function inspectMb(runner) {
6583
6827
  satisfies: semver.satisfies(version, MB_RANGE, { includePrerelease: true })
6584
6828
  };
6585
6829
  }
6830
+ const UNCHECKED = { kind: "unchecked" };
6831
+ async function publishedMb(deps) {
6832
+ let raw;
6833
+ try {
6834
+ raw = await deps.publishedVersion();
6835
+ } catch (error) {
6836
+ const reason = lookupFailure(error);
6837
+ if (reason === null) throw error;
6838
+ return {
6839
+ kind: "unknown",
6840
+ reason
6841
+ };
6842
+ }
6843
+ const version = semver.valid(raw);
6844
+ if (version === null) return {
6845
+ kind: "unknown",
6846
+ reason: `the registry names "${raw}" for ${MB_DIST_TAG}, which is not a version`
6847
+ };
6848
+ return {
6849
+ kind: "published",
6850
+ version
6851
+ };
6852
+ }
6853
+ function coreOf(version) {
6854
+ return `${semver.major(version)}.${semver.minor(version)}.${semver.patch(version)}`;
6855
+ }
6856
+ function updateTo(release, installed) {
6857
+ if (release.kind !== "published" || release.version === installed) return null;
6858
+ const order = semver.compare(coreOf(installed), coreOf(release.version));
6859
+ if (order === 0) return semver.prerelease(installed) === null ? null : release.version;
6860
+ return order < 0 ? release.version : null;
6861
+ }
6586
6862
  function displayCommand(install) {
6587
6863
  return [install.command, ...install.args].join(" ");
6588
6864
  }
@@ -6669,6 +6945,7 @@ async function stillWrong(install, after, deps) {
6669
6945
  const fail = (stderrTail) => new ExternalToolError({
6670
6946
  tool: install.command,
6671
6947
  argv: install.args,
6948
+ envNames: [],
6672
6949
  exitCode: 0,
6673
6950
  stderrTail
6674
6951
  });
@@ -6681,27 +6958,66 @@ async function stillWrong(install, after, deps) {
6681
6958
  `${stillWrongAdvice(diagnosis)}.`
6682
6959
  ].join(" "));
6683
6960
  }
6684
- function installMbStep(deps = hostDeps$9) {
6961
+ async function shadowedWarning(lead, deps) {
6962
+ const found = await inspectMbOnPath(deps.runner, deps.locate);
6963
+ const diagnosis = diagnoseMb(found, deps.installedWith);
6964
+ const advice = diagnosis.kind === "shadowed" ? ` ${shadowedHint(diagnosis)}.` : "";
6965
+ return `${lead} Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.${advice}`;
6966
+ }
6967
+ async function updateFor(release, installed, deps) {
6968
+ const version = updateTo(release, installed);
6969
+ if (version === null) return { kind: "current" };
6970
+ return (await inspectMbOnPath(deps.runner, deps.locate)).some((entry) => entry.version === version) ? {
6971
+ kind: "shadowed",
6972
+ version
6973
+ } : {
6974
+ kind: "update",
6975
+ version
6976
+ };
6977
+ }
6978
+ function installMbStep(updates, deps = hostDeps$9) {
6979
+ let published = null;
6980
+ function release(resolved) {
6981
+ if (updates === "installed") return Promise.resolve(UNCHECKED);
6982
+ published ??= publishedMb(resolved);
6983
+ return published;
6984
+ }
6685
6985
  return {
6686
6986
  id: STEP_ID$5,
6687
6987
  title: "Install the Metabase CLI",
6688
6988
  async detect(ctx) {
6689
- const found = await inspectMb(deps(ctx).runner);
6690
- return found.installed && found.satisfies ? "done" : "todo";
6989
+ const resolved = deps(ctx);
6990
+ const found = await inspectMb(resolved.runner);
6991
+ if (!found.installed || !found.satisfies) return "todo";
6992
+ return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
6691
6993
  },
6692
6994
  async run(ctx) {
6693
6995
  const resolved = deps(ctx);
6694
6996
  const before = await inspectMb(resolved.runner);
6997
+ const target = await release(resolved);
6695
6998
  if (before.installed && before.satisfies) {
6696
- ctx.ui.success(`mb ${before.version} is installed`);
6697
- return { outcome: "completed" };
6698
- }
6699
- if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
6999
+ if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
7000
+ const update = await updateFor(target, before.version, resolved);
7001
+ if (update.kind === "shadowed") {
7002
+ const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
7003
+ ctx.ui.warn(await shadowedWarning(lead, resolved));
7004
+ }
7005
+ if (update.kind !== "update") {
7006
+ ctx.ui.success(`mb ${before.version} is installed`);
7007
+ return { outcome: "completed" };
7008
+ }
7009
+ ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
7010
+ } else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
6700
7011
  const install = mbInstallCommand(resolved.installedWith);
6701
7012
  ctx.ui.info(`Running ${displayCommand(install)}`);
6702
7013
  await runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
6703
7014
  const after = await inspectMb(resolved.runner);
6704
7015
  if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
7016
+ const stillNewer = updateTo(target, after.version);
7017
+ if (stillNewer !== null) {
7018
+ const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
7019
+ ctx.ui.warn(await shadowedWarning(lead, resolved));
7020
+ }
6705
7021
  ctx.ui.success(`mb ${after.version} is installed`);
6706
7022
  return { outcome: "completed" };
6707
7023
  }
@@ -6722,7 +7038,7 @@ function hostDeps$8(ctx) {
6722
7038
  }
6723
7039
  function isNoTokenYet(error) {
6724
7040
  if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$2;
6725
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7041
+ return isMetabaseNotServing(error);
6726
7042
  }
6727
7043
  function validateToken(value) {
6728
7044
  if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
@@ -6798,7 +7114,8 @@ function licenseStep(deps = hostDeps$8) {
6798
7114
  validThru: status["valid-thru"] ?? null
6799
7115
  }
6800
7116
  });
6801
- 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({
6802
7119
  url: configured.url,
6803
7120
  profile: ctx.state.mbProfile,
6804
7121
  apiKey
@@ -7157,9 +7474,6 @@ function hostDeps$5(ctx) {
7157
7474
  now: () => /* @__PURE__ */ new Date()
7158
7475
  };
7159
7476
  }
7160
- function isNotServingYet$1(error) {
7161
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7162
- }
7163
7477
  function isNotFound$2(error) {
7164
7478
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1;
7165
7479
  }
@@ -7320,7 +7634,7 @@ function sampleDatabaseStep(deps = hostDeps$5) {
7320
7634
  try {
7321
7635
  return await recordedDatabase(record, client) === null ? "todo" : "done";
7322
7636
  } catch (error) {
7323
- if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
7637
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
7324
7638
  throw error;
7325
7639
  }
7326
7640
  },
@@ -7357,7 +7671,7 @@ function hostDeps$4(ctx) {
7357
7671
  };
7358
7672
  }
7359
7673
  function isNotHealthy(error) {
7360
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || error instanceof MetabaseApiError;
7674
+ return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
7361
7675
  }
7362
7676
  async function runningVersion(client) {
7363
7677
  return (await client.session.properties()).version.tag;
@@ -7462,9 +7776,6 @@ function hostDeps$3(ctx) {
7462
7776
  probe: hostAgentProbe()
7463
7777
  };
7464
7778
  }
7465
- function isNotServing$1(error) {
7466
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7467
- }
7468
7779
  function loginLine(kind, email) {
7469
7780
  switch (kind) {
7470
7781
  case "generated":
@@ -7481,7 +7792,7 @@ async function databaseLines$1(state, apiKey, deps) {
7481
7792
  key: apiKey
7482
7793
  }).database.listWithStubs()).data;
7483
7794
  } catch (error) {
7484
- if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
7795
+ if (isMetabaseNotServing(error)) return ["Databases: Metabase is not running; `rde start`"];
7485
7796
  if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
7486
7797
  throw error;
7487
7798
  }
@@ -7563,9 +7874,6 @@ function hostDeps$2(ctx) {
7563
7874
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
7564
7875
  };
7565
7876
  }
7566
- function isNotServingYet(error) {
7567
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7568
- }
7569
7877
  function transformsStep(deps = hostDeps$2) {
7570
7878
  return {
7571
7879
  id: STEP_ID,
@@ -7582,7 +7890,7 @@ function transformsStep(deps = hostDeps$2) {
7582
7890
  try {
7583
7891
  return await client.setting.transformsDecided() ? "done" : "todo";
7584
7892
  } catch (error) {
7585
- if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
7893
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
7586
7894
  throw error;
7587
7895
  }
7588
7896
  },
@@ -7609,12 +7917,17 @@ const DEFAULT_STEP_OPTIONS = {
7609
7917
  runtime: NO_RUNTIME_FLAGS,
7610
7918
  agents: DETECT_AGENTS,
7611
7919
  admin: DEFAULT_ADMIN,
7612
- databases: SAMPLE_STANDS_IN
7920
+ databases: SAMPLE_STANDS_IN,
7921
+ updates: "published"
7922
+ };
7923
+ const LOCAL_STEP_OPTIONS = {
7924
+ ...DEFAULT_STEP_OPTIONS,
7925
+ updates: "installed"
7613
7926
  };
7614
7927
  const STEP_BY_ID = {
7615
7928
  preflight: () => preflightStep(),
7616
- "install-mb": () => installMbStep(),
7617
- "agents-skills": (options) => agentsSkillsStep(options.agents),
7929
+ "install-mb": (options) => installMbStep(options.updates),
7930
+ "agents-skills": (options) => agentsSkillsStep(options.agents, options.updates),
7618
7931
  runtime: (options) => runtimeStep(options.runtime),
7619
7932
  "start-metabase": () => startMetabaseStep(),
7620
7933
  "admin-user": (options) => adminUserStep(options.admin),
@@ -7792,7 +8105,8 @@ function stepOptionsFrom(answers, run) {
7792
8105
  },
7793
8106
  agents: agentsOption(agents, run),
7794
8107
  admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
7795
- databases: { request: databasesRequest(databases, run) }
8108
+ databases: { request: databasesRequest(databases, run) },
8109
+ updates: DEFAULT_STEP_OPTIONS.updates
7796
8110
  };
7797
8111
  }
7798
8112
  function keyedAnswers(answers) {
@@ -8398,6 +8712,7 @@ var SessionCache = class {
8398
8712
  const CHECKLIST_MARK = {
8399
8713
  done: "✓",
8400
8714
  todo: "○",
8715
+ outdated: "↑",
8401
8716
  "not-applicable": "–"
8402
8717
  };
8403
8718
  const SPINNER_FAILURE_CODE = 1;
@@ -8638,7 +8953,7 @@ async function turnOf(entry, ctx, options) {
8638
8953
  if (options.only !== void 0) return "not-selected";
8639
8954
  if (entry.status === "done") return "done";
8640
8955
  const status = await entry.step.detect(ctx);
8641
- return status === "todo" ? "run" : status;
8956
+ return status === "todo" || status === "outdated" ? "run" : status;
8642
8957
  }
8643
8958
  function unusedMessage(step, turn, given) {
8644
8959
  const hint = step.redoHint === void 0 ? "" : ` ${step.redoHint}`;
@@ -9102,31 +9417,46 @@ async function credentialRows(state, secrets, apiKey, deps) {
9102
9417
  stateFileRow(files)
9103
9418
  ];
9104
9419
  }
9105
- function withoutTrailingSlash(url) {
9106
- 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
+ };
9429
+ }
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;
9107
9434
  }
9108
- async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
9435
+ async function mbProfileRow(state, url, deps, profiles, mbUnusableReason, healthy, access) {
9436
+ const id = "mb-profile";
9109
9437
  const label = "mb profile";
9110
- if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
9111
- if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY_REASON);
9112
- const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
9113
- if (entry === void 0) return failed("mb-profile", label, {
9114
- 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`,
9115
9443
  hint: "`rde init --only api-key`",
9116
- step: "api-key"
9444
+ step: API_KEY_STEP
9117
9445
  });
9118
- if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
9119
- 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})`,
9120
9448
  hint: "`rde init --only api-key` logs it back in against the instance",
9121
- step: "api-key"
9449
+ step: API_KEY_STEP
9122
9450
  });
9123
- if (entry.status !== "ok") return failed("mb-profile", label, {
9124
- detail: `${state.mbProfile}: ${entry.status}`,
9125
- hint: "`rde init --only api-key` logs it in again",
9126
- 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)
9127
9458
  });
9128
- const user = entry.user === null ? "" : ` as ${entry.user.name}`;
9129
- return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
9459
+ return ok(id, label, `${profile}${how}: ok${entry.user === null ? "" : ` as ${entry.user.name}`}`);
9130
9460
  }
9131
9461
  function hostOf(url) {
9132
9462
  return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
@@ -9147,8 +9477,8 @@ async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
9147
9477
  async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
9148
9478
  const rows = [];
9149
9479
  if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
9150
- 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)));
9151
- 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)));
9152
9482
  return rows;
9153
9483
  }
9154
9484
  const PROFILE_STATUS_WORDS = {
@@ -9250,12 +9580,13 @@ async function sampleDatabaseRow(record, deps, dockerOk, access) {
9250
9580
  function isMbFailure$1(error) {
9251
9581
  return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
9252
9582
  }
9253
- async function gitSyncRow(state, deps) {
9583
+ async function gitSyncRow(state, instance, deps) {
9254
9584
  const label = "remote sync";
9255
9585
  if (!hasRemoteSync(state.license)) return skipped("git-sync", label, `needs a license with ${REMOTE_SYNC_FEATURE}`);
9256
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");
9257
9588
  try {
9258
- const status = await mbTool(deps.runner).gitSyncStatus(state.mbProfile);
9589
+ const status = await mbTool(deps.runner).gitSyncStatus(instance);
9259
9590
  if (status.branch === null) return failed("git-sync", label, {
9260
9591
  detail: "Metabase reports no branch",
9261
9592
  hint: "`rde init --only git-sync`",
@@ -9286,7 +9617,7 @@ function withRef(source, ref) {
9286
9617
  }
9287
9618
  async function copyRef(entry, deps) {
9288
9619
  const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
9289
- return lockedRef(path, await deps.skills.files.readJson(path), entry.name);
9620
+ return lockedSkill(path, await deps.skills.files.readJson(path), entry.name)?.ref ?? null;
9290
9621
  }
9291
9622
  async function judgeCopy(entry, origin, deps) {
9292
9623
  const where = `${entry.scope} ${entry.path}`;
@@ -9413,13 +9744,13 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
9413
9744
  properties === null ? skipped("version", "Metabase version", NOT_HEALTHY_REASON) : versionRow(state, properties),
9414
9745
  properties === null ? skipped("admin", "admin user", NOT_HEALTHY_REASON) : adminRow(state, properties),
9415
9746
  ...await credentialRows(state, secrets, apiKey, deps),
9416
- await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
9747
+ await mbProfileRow(state, configured.url, deps, profiles, mbUnusableReason, healthy, apiKey.access)
9417
9748
  ];
9418
9749
  const access = apiKey.access;
9419
9750
  if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
9420
9751
  else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
9421
9752
  rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
9422
- rows.push(await gitSyncRow(state, deps));
9753
+ rows.push(await gitSyncRow(state, mbInstanceOf(state, secrets), deps));
9423
9754
  return rows;
9424
9755
  }
9425
9756
  async function collectDoctor(instance, state, secrets, deps) {
@@ -9548,7 +9879,7 @@ const doctor = defineRdeCommand({
9548
9879
  signal: interruptSignal,
9549
9880
  prompts
9550
9881
  });
9551
- if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
9882
+ if (await fixDoctor(report, initSteps(LOCAL_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
9552
9883
  }
9553
9884
  print(report, args.json);
9554
9885
  if (report.failed > 0) throw new Error(doctorSummary(report));
@@ -9994,20 +10325,12 @@ async function moveToPort(ctx, port, deps) {
9994
10325
  await runtimeFromState(next, deps.runtime).reconfigure();
9995
10326
  ctx.ui.info(`Moved instance ${ctx.state.name} from port ${previous} to ${port}`);
9996
10327
  }
9997
- async function profileStatus(runner, profile) {
9998
- try {
9999
- return await mbTool(runner).authStatus(profile);
10000
- } catch (error) {
10001
- if (error instanceof ProcessNotFoundError) return null;
10002
- throw error;
10003
- }
10004
- }
10005
10328
  async function followMove(ctx, from, runner) {
10006
10329
  const configured = requireConfiguredRuntime(ctx.state);
10007
10330
  const apiKey = ctx.secrets.apiKey;
10008
10331
  const profile = ctx.state.mbProfile;
10009
- const status = apiKey === null ? null : await profileStatus(runner, profile);
10010
- 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)) {
10011
10334
  await mbTool(runner).authLogin({
10012
10335
  url: configured.url,
10013
10336
  profile,
@@ -10050,7 +10373,7 @@ const start = defineRdeCommand({
10050
10373
  const SUMMARY_STEP_ID = "summary";
10051
10374
  function hostStatusDeps(instanceName, signal) {
10052
10375
  return {
10053
- steps: initSteps(DEFAULT_STEP_OPTIONS),
10376
+ steps: initSteps(LOCAL_STEP_OPTIONS),
10054
10377
  runtime: hostRuntimeDeps(instanceName, signal),
10055
10378
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
10056
10379
  runner: processRunner(signal)
@@ -10090,15 +10413,12 @@ function setupOf(ctx, steps) {
10090
10413
  resume: `rde init --from ${pending.id}`
10091
10414
  };
10092
10415
  }
10093
- function isNotServing(error) {
10094
- return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
10095
- }
10096
10416
  async function databasesOf(ctx, deps, health) {
10097
10417
  if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
10098
10418
  try {
10099
10419
  return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
10100
10420
  } catch (error) {
10101
- if (isNotServing(error) || isRejectedCredential(error)) return null;
10421
+ if (isMetabaseNotServing(error) || isRejectedCredential(error)) return null;
10102
10422
  throw error;
10103
10423
  }
10104
10424
  }
@@ -10128,10 +10448,13 @@ function isMbFailure(error) {
10128
10448
  }
10129
10449
  async function gitSyncOf(ctx, deps) {
10130
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`" };
10131
10454
  try {
10132
10455
  const mb = mbTool(deps.runner);
10133
- const status = await mb.gitSyncStatus(ctx.state.mbProfile);
10134
- const changes = await mb.gitSyncHasRemoteChanges(ctx.state.mbProfile);
10456
+ const status = await mb.gitSyncStatus(instance);
10457
+ const changes = await mb.gitSyncHasRemoteChanges(instance);
10135
10458
  return {
10136
10459
  branch: status.branch,
10137
10460
  dirty: status.is_dirty,
@@ -10292,7 +10615,6 @@ const stop = defineRdeCommand({
10292
10615
  });
10293
10616
  //#endregion
10294
10617
  //#region src/steps/uninstall.ts
10295
- const API_KEY_METHOD = "apiKey";
10296
10618
  function hostUninstallDeps(signal, childOutput) {
10297
10619
  const runner = processRunner(signal);
10298
10620
  return {
@@ -10451,8 +10773,8 @@ async function findJar(source, removeImage) {
10451
10773
  }
10452
10774
  function isInstanceProfile(status, urls) {
10453
10775
  if (!status.present) return false;
10454
- if (urls !== null) return status.url !== null && urls.includes(status.url);
10455
- 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";
10456
10778
  }
10457
10779
  function instanceUrls(state) {
10458
10780
  if (state.url === null) return null;
@@ -10460,23 +10782,34 @@ function instanceUrls(state) {
10460
10782
  }
10461
10783
  async function findMbProfiles(candidates, urls, runner) {
10462
10784
  const mb = mbTool(runner);
10463
- const removals = [];
10785
+ const found = {
10786
+ removals: [],
10787
+ kept: []
10788
+ };
10464
10789
  for (const profile of candidates) {
10465
10790
  let status;
10466
10791
  try {
10467
10792
  status = await mb.authStatus(profile);
10468
10793
  } catch (error) {
10469
- if (error instanceof ProcessNotFoundError) return [];
10794
+ if (error instanceof ProcessNotFoundError) return {
10795
+ removals: [],
10796
+ kept: []
10797
+ };
10470
10798
  throw error;
10471
10799
  }
10472
- 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({
10473
10806
  label: `mb profile ${profile}`,
10474
10807
  remove: async () => {
10475
10808
  await mb.authLogout(profile);
10476
10809
  }
10477
10810
  });
10478
10811
  }
10479
- return removals;
10812
+ return found;
10480
10813
  }
10481
10814
  function skillLabel(names, entries) {
10482
10815
  const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
@@ -10558,7 +10891,8 @@ async function planUninstall(options, deps) {
10558
10891
  if (jar.kept !== null) kept.push(jar.kept);
10559
10892
  }
10560
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);
10561
- removals.push(...profiles);
10894
+ removals.push(...profiles.removals);
10895
+ kept.push(...profiles.kept);
10562
10896
  if (options.removeSkill) {
10563
10897
  const skill = await findSkill(deps.runner, deps.childOutput);
10564
10898
  if (skill !== null) removals.push(skill);
@@ -10975,7 +11309,7 @@ const upgradeCommand = defineRdeCommand({
10975
11309
  const main = defineCommand({
10976
11310
  meta: {
10977
11311
  name: "rde",
10978
- version: "0.1.13",
11312
+ version: "0.1.15",
10979
11313
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
10980
11314
  },
10981
11315
  subCommands: {