@metabase/rde 0.1.2 → 0.1.3-alpha.rde-fixes.59b956e

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 +72 -40
  2. package/dist/cli.mjs +1115 -378
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { defineCommand, runMain, showUsage } from "citty";
3
3
  import { createWriteStream, promises } from "node:fs";
4
- import { delimiter, dirname, join, resolve } from "node:path";
4
+ import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
5
5
  import { ZodError, z } from "zod";
6
6
  import { homedir, tmpdir, userInfo } from "node:os";
7
7
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
@@ -11,6 +11,7 @@ import { confirm, intro, isCancel, log, multiselect, note, outro, password, sele
11
11
  import { spawn } from "node:child_process";
12
12
  import { connect, createServer } from "node:net";
13
13
  import semver from "semver";
14
+ import { randomBytes } from "node:crypto";
14
15
  //#region src/errors.ts
15
16
  var AbortError = class extends Error {
16
17
  constructor(message = "interrupted") {
@@ -56,6 +57,10 @@ var MetabaseApiError = class extends Error {
56
57
  this.errors = failure.errors;
57
58
  }
58
59
  };
60
+ const UNAUTHORIZED_STATUS = 401;
61
+ function isRejectedCredential(error) {
62
+ return error instanceof MetabaseApiError && error.status === UNAUTHORIZED_STATUS;
63
+ }
59
64
  var MetabaseInitializingError = class extends Error {
60
65
  progress;
61
66
  constructor(progress) {
@@ -117,6 +122,17 @@ function installInterruptHandler(exit) {
117
122
  setTimeout(() => exit(130), FORCED_EXIT_GRACE_MS).unref();
118
123
  });
119
124
  }
125
+ //#endregion
126
+ //#region src/output/print.ts
127
+ function printLines(lines) {
128
+ process.stdout.write(lines.join("\n") + "\n");
129
+ }
130
+ function stdoutIsTty() {
131
+ return process.stdout.isTTY === true;
132
+ }
133
+ function printJson(value) {
134
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
135
+ }
120
136
  const StepId = z.enum([
121
137
  "preflight",
122
138
  "install-mb",
@@ -157,14 +173,36 @@ const JarRuntimeState = z.object({
157
173
  });
158
174
  const RuntimeState = z.discriminatedUnion("kind", [DockerRuntimeState, JarRuntimeState]);
159
175
  const RuntimeKind = z.enum(["docker", "jar"]);
160
- const AdminCredentials = z.discriminatedUnion("kind", [z.object({
161
- kind: z.literal("prompted"),
162
- email: z.string()
163
- }), z.object({
164
- kind: z.literal("default"),
176
+ const AdminCredentials = z.discriminatedUnion("kind", [
177
+ z.object({
178
+ kind: z.literal("generated"),
179
+ email: z.string()
180
+ }),
181
+ z.object({
182
+ kind: z.literal("prompted"),
183
+ email: z.string()
184
+ }),
185
+ z.object({
186
+ kind: z.literal("legacy-default"),
187
+ email: z.string()
188
+ })
189
+ ]);
190
+ const AdminLogin = z.object({
165
191
  email: z.string(),
166
192
  password: z.string()
167
- })]);
193
+ });
194
+ const InstanceSecrets = z.object({
195
+ schemaVersion: z.literal(1),
196
+ admin: AdminLogin.nullable(),
197
+ apiKey: z.string().nullable()
198
+ });
199
+ function emptyInstanceSecrets() {
200
+ return {
201
+ schemaVersion: 1,
202
+ admin: null,
203
+ apiKey: null
204
+ };
205
+ }
168
206
  const LicenseState = z.discriminatedUnion("activated", [z.object({ activated: z.literal(false) }), z.object({
169
207
  activated: z.literal(true),
170
208
  features: z.array(z.string()),
@@ -202,13 +240,12 @@ const StepRecord = z.object({
202
240
  completedAt: z.string()
203
241
  });
204
242
  const InstanceState = z.object({
205
- schemaVersion: z.literal(3),
243
+ schemaVersion: z.literal(4),
206
244
  name: z.string(),
207
245
  runtime: RuntimeState.nullable(),
208
246
  url: z.string().nullable(),
209
247
  version: z.string().nullable(),
210
248
  admin: AdminCredentials.nullable(),
211
- apiKey: z.string().nullable(),
212
249
  mbProfile: z.string(),
213
250
  license: LicenseState,
214
251
  databases: z.array(DatabaseRecord),
@@ -230,20 +267,22 @@ function requireConfiguredRuntime(state) {
230
267
  }
231
268
  const DEFAULT_INSTANCE_NAME = "default";
232
269
  const DEFAULT_MB_PROFILE = "default";
233
- const MbProfileChoice = z.enum([DEFAULT_MB_PROFILE, "rde"]);
270
+ const RDE_MB_PROFILE = "rde";
271
+ function mbProfileFor(instance) {
272
+ return instance === "default" ? RDE_MB_PROFILE : `${RDE_MB_PROFILE}-${instance}`;
273
+ }
234
274
  function mbCommand(profile) {
235
275
  return profile === "default" ? "mb" : `mb --profile ${profile}`;
236
276
  }
237
277
  function emptyInstanceState(name) {
238
278
  return {
239
- schemaVersion: 3,
279
+ schemaVersion: 4,
240
280
  name,
241
281
  runtime: null,
242
282
  url: null,
243
283
  version: null,
244
284
  admin: null,
245
- apiKey: null,
246
- mbProfile: DEFAULT_MB_PROFILE,
285
+ mbProfile: mbProfileFor(name),
247
286
  license: { activated: false },
248
287
  databases: [],
249
288
  sampleDatabase: null,
@@ -256,8 +295,22 @@ function emptyInstanceState(name) {
256
295
  const Versioned = z.object({ schemaVersion: z.int() });
257
296
  const V1 = 1;
258
297
  const V2 = 2;
298
+ const V3 = 3;
259
299
  const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
260
300
  const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
301
+ const AdminV3 = z.discriminatedUnion("kind", [z.object({
302
+ kind: z.literal("prompted"),
303
+ email: z.string()
304
+ }), z.object({
305
+ kind: z.literal("default"),
306
+ email: z.string(),
307
+ password: z.string()
308
+ })]);
309
+ const StateV3 = z.object({
310
+ schemaVersion: z.literal(V3),
311
+ admin: AdminV3.nullable(),
312
+ apiKey: z.string().nullable()
313
+ }).loose();
261
314
  function liftV1(raw) {
262
315
  return {
263
316
  ...StateV1.parse(raw),
@@ -268,27 +321,67 @@ function liftV1(raw) {
268
321
  function liftV2(raw) {
269
322
  return {
270
323
  ...StateV2.parse(raw),
271
- schemaVersion: 3,
324
+ schemaVersion: V3,
272
325
  sampleDatabase: null
273
326
  };
274
327
  }
328
+ function adminV4(admin) {
329
+ switch (admin.kind) {
330
+ case "default": return {
331
+ kind: "legacy-default",
332
+ email: admin.email
333
+ };
334
+ case "prompted": return {
335
+ kind: "prompted",
336
+ email: admin.email
337
+ };
338
+ }
339
+ }
340
+ function liftV3(raw) {
341
+ const { admin, apiKey, ...rest } = StateV3.parse(raw);
342
+ const login = admin?.kind === "default" ? {
343
+ email: admin.email,
344
+ password: admin.password
345
+ } : null;
346
+ return {
347
+ state: InstanceState.parse({
348
+ ...rest,
349
+ schemaVersion: 4,
350
+ admin: admin === null ? null : adminV4(admin)
351
+ }),
352
+ movedSecrets: {
353
+ schemaVersion: 1,
354
+ admin: login,
355
+ apiKey
356
+ }
357
+ };
358
+ }
275
359
  function migrateInstanceState(raw) {
276
360
  const { schemaVersion } = Versioned.parse(raw);
277
361
  switch (schemaVersion) {
278
362
  case V1: return migrateInstanceState(liftV1(raw));
279
363
  case V2: return migrateInstanceState(liftV2(raw));
280
- case 3: return InstanceState.parse(raw);
281
- default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 3)`);
364
+ case V3: return liftV3(raw);
365
+ case 4: return {
366
+ state: InstanceState.parse(raw),
367
+ movedSecrets: null
368
+ };
369
+ default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 4)`);
282
370
  }
283
371
  }
284
372
  //#endregion
285
373
  //#region src/env.ts
286
374
  const ENV_RDE_HOME = "RDE_HOME";
375
+ const ENV_RDE_SKILL_SOURCE = "RDE_SKILL_SOURCE";
287
376
  function readEnv(name) {
288
377
  return process.env[name];
289
378
  }
290
379
  const ENV_PATH = "PATH";
380
+ const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
291
381
  const ENV_JAVA_HOME = "JAVA_HOME";
382
+ const ENV_MB_PROFILE = "MB_PROFILE";
383
+ const ENV_MB_URL = "MB_URL";
384
+ const ENV_MB_API_KEY = "MB_API_KEY";
292
385
  function environmentWith(overrides) {
293
386
  return {
294
387
  ...process.env,
@@ -302,6 +395,7 @@ const INSTANCES_DIR = "instances";
302
395
  const DATA_DIR = "data";
303
396
  const BACKUPS_DIR = "backups";
304
397
  const STATE_FILE = "state.json";
398
+ const SECRETS_FILE = "secrets.json";
305
399
  const GLOBAL_CONFIG_FILE = "config.json";
306
400
  const JARS_DIR = "jars";
307
401
  const JAR_FILE$1 = "metabase.jar";
@@ -322,8 +416,17 @@ function instanceDataDir(name) {
322
416
  function instanceBackupsDir(name) {
323
417
  return join(instanceDir(name), BACKUPS_DIR);
324
418
  }
419
+ function stateFileIn(dir) {
420
+ return join(dir, STATE_FILE);
421
+ }
422
+ function secretsFileIn(dir) {
423
+ return join(dir, SECRETS_FILE);
424
+ }
325
425
  function instanceStatePath(name) {
326
- return join(instanceDir(name), STATE_FILE);
426
+ return stateFileIn(instanceDir(name));
427
+ }
428
+ function instanceSecretsPath(name) {
429
+ return secretsFileIn(instanceDir(name));
327
430
  }
328
431
  function globalConfigPath() {
329
432
  return join(rdeHome(), GLOBAL_CONFIG_FILE);
@@ -337,21 +440,21 @@ function jarReleaseDir(version) {
337
440
  function releaseJarIn(jarsDirectory, version) {
338
441
  return join(jarsDirectory, version, JAR_FILE$1);
339
442
  }
340
- //#endregion
341
- //#region src/state/store.ts
342
- const FILE_MODE$1 = 384;
343
- const DIR_MODE = 448;
443
+ const PERMISSION_BITS = 511;
344
444
  function isFileNotFound$1(error) {
345
445
  return error instanceof Error && "code" in error && error.code === "ENOENT";
346
446
  }
347
- async function readJsonFile(path) {
348
- let raw;
447
+ async function readTextFile(path) {
349
448
  try {
350
- raw = await promises.readFile(path, "utf8");
449
+ return await promises.readFile(path, "utf8");
351
450
  } catch (error) {
352
451
  if (isFileNotFound$1(error)) return null;
353
452
  throw error;
354
453
  }
454
+ }
455
+ async function readJsonFile(path) {
456
+ const raw = await readTextFile(path);
457
+ if (raw === null) return null;
355
458
  try {
356
459
  return JSON.parse(raw);
357
460
  } catch (error) {
@@ -370,11 +473,11 @@ function parseAt(path, parse) {
370
473
  async function writeJsonFile(path, value) {
371
474
  await promises.mkdir(dirname(path), {
372
475
  recursive: true,
373
- mode: DIR_MODE
476
+ mode: 448
374
477
  });
375
478
  const tmpPath = `${path}.${process.pid}.tmp`;
376
- await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode: FILE_MODE$1 });
377
- await promises.chmod(tmpPath, FILE_MODE$1);
479
+ await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
480
+ await promises.chmod(tmpPath, 384);
378
481
  try {
379
482
  await promises.rename(tmpPath, path);
380
483
  } catch (error) {
@@ -386,7 +489,12 @@ async function readInstanceState(name) {
386
489
  const path = instanceStatePath(name);
387
490
  const raw = await readJsonFile(path);
388
491
  if (raw === null) return null;
389
- return parseAt(path, () => migrateInstanceState(raw));
492
+ const migrated = parseAt(path, () => migrateInstanceState(raw));
493
+ if (migrated.movedSecrets !== null) {
494
+ await writeInstanceSecrets(name, migrated.movedSecrets);
495
+ await writeInstanceState(migrated.state);
496
+ }
497
+ return migrated.state;
390
498
  }
391
499
  async function writeInstanceState(state) {
392
500
  await writeJsonFile(instanceStatePath(state.name), state);
@@ -396,6 +504,35 @@ async function requireInstanceState(name) {
396
504
  if (state === null) throw new ConfigError(`no instance named ${name}; run \`rde init\` first`);
397
505
  return state;
398
506
  }
507
+ async function readInstanceSecrets(name) {
508
+ const path = instanceSecretsPath(name);
509
+ const raw = await readJsonFile(path);
510
+ if (raw === null) return null;
511
+ return parseAt(path, () => InstanceSecrets.parse(raw));
512
+ }
513
+ async function writeInstanceSecrets(name, secrets) {
514
+ await writeJsonFile(instanceSecretsPath(name), secrets);
515
+ await promises.chmod(instanceDir(name), 448);
516
+ }
517
+ async function modeOf(path) {
518
+ try {
519
+ return (await promises.stat(path)).mode & PERMISSION_BITS;
520
+ } catch (error) {
521
+ if (isFileNotFound$1(error)) return null;
522
+ throw error;
523
+ }
524
+ }
525
+ const StateWithApiKey = z.object({ apiKey: z.string() }).loose();
526
+ const StateWithPassword = z.object({ admin: z.object({ password: z.string() }).loose() }).loose();
527
+ async function inspectCredentialFiles(dir) {
528
+ const raw = await readJsonFile(stateFileIn(dir));
529
+ const secretsInState = [...StateWithApiKey.safeParse(raw).success ? ["apiKey"] : [], ...StateWithPassword.safeParse(raw).success ? ["admin.password"] : []];
530
+ return {
531
+ dirMode: await modeOf(dir),
532
+ secretsMode: await modeOf(secretsFileIn(dir)),
533
+ secretsInState
534
+ };
535
+ }
399
536
  async function readGlobalConfig() {
400
537
  const path = globalConfigPath();
401
538
  const raw = await readJsonFile(path);
@@ -406,6 +543,91 @@ async function writeGlobalConfig(config) {
406
543
  await writeJsonFile(globalConfigPath(), config);
407
544
  }
408
545
  //#endregion
546
+ //#region src/steps/credentials.ts
547
+ const TERMINAL_ONLY = "rde credentials prints only to a terminal; run it in your own terminal";
548
+ function storedPasswordLines(state, secrets, email) {
549
+ if (secrets === null || secrets.admin === null) throw new ConfigError(`the secrets file of instance ${state.name} holds no admin password; \`rde doctor\` says what to do`);
550
+ return [`Email: ${email}`, `Password: ${secrets.admin.password}`];
551
+ }
552
+ function credentialsLines(state, secrets) {
553
+ const admin = state.admin;
554
+ if (admin === null) throw new ConfigError(`instance ${state.name} has no admin user yet; run \`rde init\``);
555
+ const url = state.url === null ? [] : [`Metabase: ${state.url}`];
556
+ switch (admin.kind) {
557
+ case "generated": return [...url, ...storedPasswordLines(state, secrets, admin.email)];
558
+ case "legacy-default": return [
559
+ ...url,
560
+ ...storedPasswordLines(state, secrets, admin.email),
561
+ "This is the public default password; when the instance holds nothing you need, `rde uninstall` then `rde init` recreates it with a generated one"
562
+ ];
563
+ case "prompted": return [
564
+ ...url,
565
+ `Email: ${admin.email}`,
566
+ "Password: the one you entered during `rde init`; rde never stored it"
567
+ ];
568
+ }
569
+ }
570
+ //#endregion
571
+ //#region src/commands/define.ts
572
+ function repeatedValues(rawArgs, flag) {
573
+ const values = [];
574
+ const inline = `${flag}=`;
575
+ for (let index = 0; index < rawArgs.length; index += 1) {
576
+ const arg = rawArgs[index];
577
+ if (arg === void 0) continue;
578
+ if (arg.startsWith(inline)) values.push(arg.slice(inline.length));
579
+ else if (arg === flag) {
580
+ const next = rawArgs[index + 1];
581
+ if (next !== void 0) {
582
+ values.push(next);
583
+ index += 1;
584
+ }
585
+ }
586
+ }
587
+ return values;
588
+ }
589
+ function defineRdeCommand(def) {
590
+ return defineCommand({
591
+ meta: def.meta,
592
+ ...def.args !== void 0 && { args: def.args },
593
+ async run({ args, rawArgs }) {
594
+ try {
595
+ await def.run(args, rawArgs);
596
+ } catch (error) {
597
+ reportError(error);
598
+ }
599
+ }
600
+ });
601
+ }
602
+ //#endregion
603
+ //#region src/commands/instance.ts
604
+ async function defaultInstanceName() {
605
+ const config = await readGlobalConfig();
606
+ if (config === null) throw new ConfigError("no Metabase instance yet; run `rde init` first");
607
+ return config.defaultInstance;
608
+ }
609
+ async function instanceNameOrDefault() {
610
+ try {
611
+ return (await readGlobalConfig())?.defaultInstance ?? "default";
612
+ } catch (error) {
613
+ if (error instanceof ConfigError) return DEFAULT_INSTANCE_NAME;
614
+ throw error;
615
+ }
616
+ }
617
+ //#endregion
618
+ //#region src/commands/credentials.ts
619
+ const credentials = defineRdeCommand({
620
+ meta: {
621
+ name: "credentials",
622
+ description: "Print the admin login of the instance; only to a terminal"
623
+ },
624
+ async run() {
625
+ if (!stdoutIsTty()) throw new ConfigError(TERMINAL_ONLY);
626
+ const name = await defaultInstanceName();
627
+ printLines(credentialsLines(await requireInstanceState(name), await readInstanceSecrets(name)));
628
+ }
629
+ });
630
+ //#endregion
409
631
  //#region src/connections/parse-uri.ts
410
632
  var UriParseError = class extends Error {
411
633
  engine;
@@ -1072,6 +1294,18 @@ function tokenStatusResource(http) {
1072
1294
  return { get };
1073
1295
  }
1074
1296
  //#endregion
1297
+ //#region src/metabase/domain/user.ts
1298
+ const CurrentUser = z.object({ id: z.int() }).loose();
1299
+ //#endregion
1300
+ //#region src/metabase/resources/user.ts
1301
+ function userResource(http) {
1302
+ /** GET /api/user/current */
1303
+ async function current() {
1304
+ return http.requestJson("GET", "/api/user/current", { schema: CurrentUser });
1305
+ }
1306
+ return { current };
1307
+ }
1308
+ //#endregion
1075
1309
  //#region src/metabase/client.ts
1076
1310
  const HEALTH_POLL_INTERVAL_MS = 2e3;
1077
1311
  function createMetabaseClient(baseUrl, auth, signal, options = {}) {
@@ -1092,7 +1326,8 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
1092
1326
  setting: settingResource(http),
1093
1327
  tokenStatus: tokenStatusResource(http),
1094
1328
  database: databaseResource(http),
1095
- remoteSync: remoteSyncResource(http)
1329
+ remoteSync: remoteSyncResource(http),
1330
+ user: userResource(http)
1096
1331
  };
1097
1332
  }
1098
1333
  function isNotReadyYet(error) {
@@ -2214,9 +2449,9 @@ function hostDatabasesDeps(signal) {
2214
2449
  function isNotServingYet$1(error) {
2215
2450
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
2216
2451
  }
2217
- function requireApiKey$2(state) {
2218
- if (state.apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
2219
- return state.apiKey;
2452
+ function requireApiKey$2(state, apiKey) {
2453
+ if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
2454
+ return apiKey;
2220
2455
  }
2221
2456
  function hostAliasOf(runtime) {
2222
2457
  switch (runtime.kind) {
@@ -2232,15 +2467,15 @@ function toRecord$1(database, createdAt) {
2232
2467
  createdAt
2233
2468
  };
2234
2469
  }
2235
- function authenticatedClient(state, deps) {
2470
+ function authenticatedClient(state, apiKey, deps) {
2236
2471
  const configured = requireConfiguredRuntime(state);
2237
2472
  return deps.client(configured.url, {
2238
2473
  kind: "api-key",
2239
- key: requireApiKey$2(state)
2474
+ key: requireApiKey$2(state, apiKey)
2240
2475
  });
2241
2476
  }
2242
- async function listDatabases(state, deps) {
2243
- const listed = await authenticatedClient(state, deps).database.list();
2477
+ async function listDatabases(state, apiKey, deps) {
2478
+ const listed = await authenticatedClient(state, apiKey, deps).database.list();
2244
2479
  const records = new Map(state.databases.map((record) => [record.id, record]));
2245
2480
  const onServer = listed.data.map((database) => ({
2246
2481
  id: database.id,
@@ -2261,7 +2496,7 @@ async function listDatabases(state, deps) {
2261
2496
  }
2262
2497
  async function addDatabases(ctx, deps) {
2263
2498
  const configured = requireConfiguredRuntime(ctx.state);
2264
- const client = authenticatedClient(ctx.state, deps);
2499
+ const client = authenticatedClient(ctx.state, ctx.secrets.apiKey, deps);
2265
2500
  const properties = await client.session.properties();
2266
2501
  const existing = await client.database.list();
2267
2502
  if (existing.data.length > 0) ctx.ui.note(existingDatabaseLines(existing.data).join("\n"), "Connected databases");
@@ -2299,17 +2534,17 @@ function databasesStep(options, deps = hostDeps$12) {
2299
2534
  title: "Connect your databases",
2300
2535
  async detect(ctx) {
2301
2536
  const configured = configuredRuntime(ctx.state);
2302
- if (configured === null || ctx.state.apiKey === null) return "todo";
2537
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
2303
2538
  const own = ownDatabaseIds(ctx.state);
2304
2539
  if (own.size === 0) return sampleDatabaseId(ctx.state) !== null && !options.requested ? "done" : "todo";
2305
2540
  const client = deps(ctx).client(configured.url, {
2306
2541
  kind: "api-key",
2307
- key: ctx.state.apiKey
2542
+ key: ctx.secrets.apiKey
2308
2543
  });
2309
2544
  try {
2310
2545
  return (await client.database.list()).data.some((database) => own.has(database.id)) ? "done" : "todo";
2311
2546
  } catch (error) {
2312
- if (isNotServingYet$1(error)) return "todo";
2547
+ if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
2313
2548
  throw error;
2314
2549
  }
2315
2550
  },
@@ -2391,6 +2626,11 @@ async function createWizardContext(instanceName, options) {
2391
2626
  await writeInstanceState(next);
2392
2627
  ctx.state = next;
2393
2628
  },
2629
+ secrets: await readInstanceSecrets(instanceName) ?? emptyInstanceSecrets(),
2630
+ saveSecrets: async (next) => {
2631
+ await writeInstanceSecrets(instanceName, next);
2632
+ ctx.secrets = next;
2633
+ },
2394
2634
  prompts: options.prompts ?? clackPrompts(),
2395
2635
  ui: clackUi(),
2396
2636
  signal: options.signal,
@@ -2399,53 +2639,6 @@ async function createWizardContext(instanceName, options) {
2399
2639
  return ctx;
2400
2640
  }
2401
2641
  //#endregion
2402
- //#region src/commands/define.ts
2403
- function repeatedValues(rawArgs, flag) {
2404
- const values = [];
2405
- const inline = `${flag}=`;
2406
- for (let index = 0; index < rawArgs.length; index += 1) {
2407
- const arg = rawArgs[index];
2408
- if (arg === void 0) continue;
2409
- if (arg.startsWith(inline)) values.push(arg.slice(inline.length));
2410
- else if (arg === flag) {
2411
- const next = rawArgs[index + 1];
2412
- if (next !== void 0) {
2413
- values.push(next);
2414
- index += 1;
2415
- }
2416
- }
2417
- }
2418
- return values;
2419
- }
2420
- function defineRdeCommand(def) {
2421
- return defineCommand({
2422
- meta: def.meta,
2423
- ...def.args !== void 0 && { args: def.args },
2424
- async run({ args, rawArgs }) {
2425
- try {
2426
- await def.run(args, rawArgs);
2427
- } catch (error) {
2428
- reportError(error);
2429
- }
2430
- }
2431
- });
2432
- }
2433
- //#endregion
2434
- //#region src/commands/instance.ts
2435
- async function defaultInstanceName() {
2436
- const config = await readGlobalConfig();
2437
- if (config === null) throw new ConfigError("no Metabase instance yet; run `rde init` first");
2438
- return config.defaultInstance;
2439
- }
2440
- async function instanceNameOrDefault() {
2441
- try {
2442
- return (await readGlobalConfig())?.defaultInstance ?? "default";
2443
- } catch (error) {
2444
- if (error instanceof ConfigError) return DEFAULT_INSTANCE_NAME;
2445
- throw error;
2446
- }
2447
- }
2448
- //#endregion
2449
2642
  //#region src/commands/db/add.ts
2450
2643
  const dbAdd = defineRdeCommand({
2451
2644
  meta: {
@@ -2460,14 +2653,6 @@ const dbAdd = defineRdeCommand({
2460
2653
  }
2461
2654
  });
2462
2655
  //#endregion
2463
- //#region src/output/print.ts
2464
- function printLines(lines) {
2465
- process.stdout.write(lines.join("\n") + "\n");
2466
- }
2467
- function printJson(value) {
2468
- process.stdout.write(JSON.stringify(value, null, 2) + "\n");
2469
- }
2470
- //#endregion
2471
2656
  //#region src/commands/db/list.ts
2472
2657
  const dbList = defineRdeCommand({
2473
2658
  meta: {
@@ -2480,7 +2665,10 @@ const dbList = defineRdeCommand({
2480
2665
  default: false
2481
2666
  } },
2482
2667
  async run(args) {
2483
- const listing = await listDatabases(await requireInstanceState(await defaultInstanceName()), hostDatabasesDeps(interruptSignal));
2668
+ const name = await defaultInstanceName();
2669
+ const state = await requireInstanceState(name);
2670
+ const secrets = await readInstanceSecrets(name);
2671
+ const listing = await listDatabases(state, secrets === null ? null : secrets.apiKey, hostDatabasesDeps(interruptSignal));
2484
2672
  if (args.json) {
2485
2673
  printJson(listing);
2486
2674
  return;
@@ -2809,15 +2997,20 @@ async function isExecutable(path) {
2809
2997
  return false;
2810
2998
  }
2811
2999
  }
2812
- async function whichCommand(name) {
3000
+ async function whichAll(name) {
2813
3001
  const searchPath = readEnv(ENV_PATH);
2814
- if (searchPath === void 0) return null;
2815
- for (const dir of searchPath.split(delimiter)) {
3002
+ if (searchPath === void 0) return [];
3003
+ const found = [];
3004
+ for (const dir of new Set(searchPath.split(delimiter))) {
2816
3005
  if (dir.length === 0) continue;
2817
3006
  const candidate = join(dir, name);
2818
- if (await isExecutable(candidate)) return candidate;
3007
+ if (await isExecutable(candidate)) found.push(candidate);
2819
3008
  }
2820
- return null;
3009
+ return found;
3010
+ }
3011
+ async function whichCommand(name) {
3012
+ const [first] = await whichAll(name);
3013
+ return first ?? null;
2821
3014
  }
2822
3015
  //#endregion
2823
3016
  //#region src/tools/java.ts
@@ -3325,7 +3518,7 @@ function daemonTool(runner) {
3325
3518
  }
3326
3519
  //#endregion
3327
3520
  //#region src/runtime/docker.ts
3328
- const DEFAULT_IMAGE = "metabase/metabase-dev:transform-tests-ee";
3521
+ const DEFAULT_IMAGE = "metabase/metabase-enterprise-head:latest";
3329
3522
  const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
3330
3523
  const CONTAINER_PORT = 3e3;
3331
3524
  function containerName(instanceName) {
@@ -3661,44 +3854,117 @@ const GLOBAL_INSTALL = {
3661
3854
  function mbInstallCommand(packageManager) {
3662
3855
  return GLOBAL_INSTALL[packageManager];
3663
3856
  }
3857
+ function ownerByMarker(path, markers) {
3858
+ const normalized = path.replaceAll("\\", "/");
3859
+ const found = markers.find(({ marker }) => normalized.includes(marker));
3860
+ return found === void 0 ? null : found.owner;
3861
+ }
3664
3862
  const GLOBAL_MARKERS = [
3665
3863
  {
3666
3864
  marker: "/.bun/install/global/node_modules/",
3667
- packageManager: "bun"
3865
+ owner: "bun"
3668
3866
  },
3669
3867
  {
3670
3868
  marker: "/Library/pnpm/global/",
3671
- packageManager: "pnpm"
3869
+ owner: "pnpm"
3672
3870
  },
3673
3871
  {
3674
3872
  marker: "/.local/share/pnpm/global/",
3675
- packageManager: "pnpm"
3873
+ owner: "pnpm"
3676
3874
  },
3677
3875
  {
3678
3876
  marker: "/share/pnpm/global/",
3679
- packageManager: "pnpm"
3877
+ owner: "pnpm"
3680
3878
  },
3681
3879
  {
3682
3880
  marker: "/pnpm-global/node_modules/",
3683
- packageManager: "pnpm"
3881
+ owner: "pnpm"
3684
3882
  },
3685
3883
  {
3686
3884
  marker: "/.config/yarn/global/node_modules/",
3687
- packageManager: "yarn"
3885
+ owner: "yarn"
3688
3886
  },
3689
3887
  {
3690
3888
  marker: "/lib/node_modules/",
3691
- packageManager: "npm"
3889
+ owner: "npm"
3692
3890
  },
3693
3891
  {
3694
3892
  marker: "/.npm-global/node_modules/",
3695
- packageManager: "npm"
3893
+ owner: "npm"
3696
3894
  }
3697
3895
  ];
3698
3896
  function packageManagerFor(scriptPath) {
3699
- const normalized = scriptPath.replaceAll("\\", "/");
3700
- for (const { marker, packageManager } of GLOBAL_MARKERS) if (normalized.includes(marker)) return packageManager;
3701
- return "npm";
3897
+ return ownerByMarker(scriptPath, GLOBAL_MARKERS) ?? "npm";
3898
+ }
3899
+ const MB_OWNER_NAMES = {
3900
+ npm: "npm",
3901
+ pnpm: "pnpm",
3902
+ yarn: "Yarn",
3903
+ bun: "Bun",
3904
+ homebrew: "Homebrew"
3905
+ };
3906
+ const CELLAR = "/Cellar/";
3907
+ const NODE_MODULES = "/node_modules/";
3908
+ const OWNER_MARKERS = [
3909
+ ...GLOBAL_MARKERS,
3910
+ {
3911
+ marker: CELLAR,
3912
+ owner: "homebrew"
3913
+ },
3914
+ {
3915
+ marker: "/Library/pnpm/",
3916
+ owner: "pnpm"
3917
+ },
3918
+ {
3919
+ marker: "/.local/share/pnpm/",
3920
+ owner: "pnpm"
3921
+ }
3922
+ ];
3923
+ async function locateMbOnPath() {
3924
+ const paths = await whichAll(MB);
3925
+ return Promise.all(paths.map(async (path) => ({
3926
+ path,
3927
+ resolved: await promises.realpath(path)
3928
+ })));
3929
+ }
3930
+ function mbOwner(binary) {
3931
+ return ownerByMarker(binary.resolved, OWNER_MARKERS) ?? ownerByMarker(binary.path, OWNER_MARKERS);
3932
+ }
3933
+ function firstSegmentAfter(path, marker) {
3934
+ const at = path.lastIndexOf(marker);
3935
+ if (at === -1) return null;
3936
+ const [first] = path.slice(at + marker.length).split("/");
3937
+ return first === void 0 || first.length === 0 ? null : first;
3938
+ }
3939
+ function packageName(resolved) {
3940
+ const scopeOrName = firstSegmentAfter(resolved, NODE_MODULES);
3941
+ if (scopeOrName === null || !scopeOrName.startsWith("@")) return scopeOrName;
3942
+ const name = firstSegmentAfter(resolved, `${NODE_MODULES}${scopeOrName}/`);
3943
+ return name === null ? null : `${scopeOrName}/${name}`;
3944
+ }
3945
+ const REMOVE_COMMANDS = {
3946
+ npm: (name) => `npm uninstall -g ${name}`,
3947
+ pnpm: (name) => `pnpm remove -g ${name}`,
3948
+ yarn: (name) => `yarn global remove ${name}`,
3949
+ bun: (name) => `bun remove -g ${name}`,
3950
+ homebrew: (name) => `brew uninstall ${name}`
3951
+ };
3952
+ function mbRemoveCommand(binary) {
3953
+ const owner = mbOwner(binary);
3954
+ if (owner === null) return null;
3955
+ const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
3956
+ return name === null ? null : REMOVE_COMMANDS[owner](name);
3957
+ }
3958
+ function setValue(name) {
3959
+ const value = readEnv(name);
3960
+ return value === void 0 || value.length === 0 ? null : value;
3961
+ }
3962
+ function mbEnvironment() {
3963
+ return {
3964
+ profile: setValue(ENV_MB_PROFILE),
3965
+ url: setValue(ENV_MB_URL),
3966
+ apiKeySet: setValue(ENV_MB_API_KEY) !== null
3967
+ };
3702
3968
  }
3703
3969
  const ProbedUser = z.object({
3704
3970
  id: z.int(),
@@ -3775,16 +4041,16 @@ function mbTool(runner) {
3775
4041
  if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
3776
4042
  return parseJsonOutput(MB, argv, result.stdout, schema);
3777
4043
  }
3778
- async function version() {
4044
+ async function version(command = MB) {
3779
4045
  const args = ["--version"];
3780
4046
  let result;
3781
4047
  try {
3782
- result = await runner.run(MB, args);
4048
+ result = await runner.run(command, args);
3783
4049
  } catch (error) {
3784
4050
  if (error instanceof ProcessNotFoundError) return null;
3785
4051
  throw error;
3786
4052
  }
3787
- if (result.exitCode !== 0) throw toolFailure(MB, args, result);
4053
+ if (result.exitCode !== 0) throw toolFailure(command, args, result);
3788
4054
  return result.stdout.trim();
3789
4055
  }
3790
4056
  async function authLogin(options) {
@@ -3870,73 +4136,65 @@ function mbTool(runner) {
3870
4136
  };
3871
4137
  }
3872
4138
  //#endregion
3873
- //#region src/wizard/engine.ts
3874
- function requireKnownStep(steps, id, flag) {
3875
- if (id === void 0) return;
3876
- if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
4139
+ //#region src/tools/skills-cli.ts
4140
+ const NPX = "npx";
4141
+ const SKILLS_PACKAGE = "skills@latest";
4142
+ const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-skill";
4143
+ function rdeSkillSource() {
4144
+ const override = readEnv(ENV_RDE_SKILL_SOURCE);
4145
+ return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
3877
4146
  }
3878
- async function detectAll(steps, ctx, options) {
3879
- const detected = [];
3880
- let fromHere = false;
3881
- for (const step of steps) {
3882
- if (step.id === options.from) fromHere = true;
3883
- const forced = options.only !== void 0 ? step.id === options.only : fromHere;
3884
- detected.push({
3885
- step,
3886
- status: forced ? "todo" : await step.detect(ctx),
3887
- forced
3888
- });
3889
- }
3890
- return detected;
4147
+ const InstalledSkill = z.object({
4148
+ name: z.string(),
4149
+ path: z.string(),
4150
+ scope: z.string(),
4151
+ agents: z.array(z.string()),
4152
+ source: z.string().nullable()
4153
+ }).loose();
4154
+ const InstalledSkillList = z.array(InstalledSkill);
4155
+ const SkillLock = z.object({ skills: z.record(z.string(), z.object({ ref: z.string().nullish() }).loose()) }).loose();
4156
+ const GLOBAL_LOCK_FILE = ".skill-lock.json";
4157
+ const PROJECT_LOCK_FILE = "skills-lock.json";
4158
+ function globalSkillLockPath(home, stateHome) {
4159
+ return stateHome === null ? join(home, ".agents", GLOBAL_LOCK_FILE) : join(stateHome, "skills", GLOBAL_LOCK_FILE);
3891
4160
  }
3892
- async function shouldRun(entry, ctx, options) {
3893
- if (entry.forced) return true;
3894
- if (options.only !== void 0 || entry.status === "done") return false;
3895
- return await entry.step.detect(ctx) === "todo";
4161
+ function projectSkillLockPath(project) {
4162
+ return join(project, PROJECT_LOCK_FILE);
3896
4163
  }
3897
- function recordFor(outcome, at) {
3898
- return {
3899
- status: outcome === "completed" ? "done" : "skipped",
3900
- completedAt: at.toISOString()
3901
- };
4164
+ function xdgStateHome() {
4165
+ const value = readEnv(ENV_XDG_STATE_HOME);
4166
+ return value === void 0 || value.length === 0 ? null : value;
3902
4167
  }
3903
- function withStepRecord(state, id, record) {
3904
- return {
3905
- ...state,
3906
- steps: {
3907
- ...state.steps,
3908
- [id]: record
3909
- }
3910
- };
4168
+ function lockedRef(path, raw, skill) {
4169
+ if (raw === null) return null;
4170
+ const parsed = SkillLock.safeParse(raw);
4171
+ if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
4172
+ return parsed.data.skills[skill]?.ref ?? null;
3911
4173
  }
3912
- async function runWizard(steps, ctx, options = {}) {
3913
- requireKnownStep(steps, options.from, "--from");
3914
- requireKnownStep(steps, options.only, "--only");
3915
- const now = options.now ?? (() => /* @__PURE__ */ new Date());
3916
- const detected = await detectAll(steps, ctx, options);
3917
- ctx.ui.checklist(detected.map(({ step, status }) => ({
3918
- title: step.title,
3919
- status
3920
- })));
3921
- for (const entry of detected) {
3922
- if (!await shouldRun(entry, ctx, options)) continue;
3923
- const outcome = await entry.step.run(ctx);
3924
- await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
4174
+ const hostSkillFiles = {
4175
+ readJson: readJsonFile,
4176
+ readText: readTextFile
4177
+ };
4178
+ const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
4179
+ function skillOrigin(source, cwd) {
4180
+ const github = GITHUB_URL.exec(source);
4181
+ if (github !== null) {
4182
+ const [, owner, repo, ref] = github;
4183
+ return {
4184
+ kind: "github",
4185
+ repository: `${owner}/${repo}`,
4186
+ ref: ref ?? null
4187
+ };
3925
4188
  }
4189
+ if (isAbsolute(source) || source.startsWith(".")) return {
4190
+ kind: "local",
4191
+ path: resolve(cwd, source)
4192
+ };
4193
+ return {
4194
+ kind: "other",
4195
+ source
4196
+ };
3926
4197
  }
3927
- //#endregion
3928
- //#region src/tools/skills-cli.ts
3929
- const NPX = "npx";
3930
- const SKILLS_PACKAGE = "skills@latest";
3931
- const RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-generic-skills";
3932
- const InstalledSkill = z.object({
3933
- name: z.string(),
3934
- path: z.string(),
3935
- scope: z.string(),
3936
- agents: z.array(z.string()),
3937
- source: z.string().nullable()
3938
- }).loose();
3939
- const InstalledSkillList = z.array(InstalledSkill);
3940
4198
  function skillsCli(runner) {
3941
4199
  async function add(options) {
3942
4200
  const argv = [
@@ -3988,12 +4246,82 @@ function skillsCli(runner) {
3988
4246
  if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
3989
4247
  return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
3990
4248
  }
4249
+ async function listProject(cwd) {
4250
+ const argv = [
4251
+ "-y",
4252
+ SKILLS_PACKAGE,
4253
+ "list",
4254
+ "--json"
4255
+ ];
4256
+ const result = await runner.run(NPX, argv, {
4257
+ cwd,
4258
+ stdoutToFile: true
4259
+ });
4260
+ if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
4261
+ return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
4262
+ }
3991
4263
  return {
3992
4264
  add,
3993
4265
  remove,
3994
- list
4266
+ list,
4267
+ listProject
4268
+ };
4269
+ }
4270
+ //#endregion
4271
+ //#region src/wizard/engine.ts
4272
+ function requireKnownStep(steps, id, flag) {
4273
+ if (id === void 0) return;
4274
+ if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
4275
+ }
4276
+ async function detectAll(steps, ctx, options) {
4277
+ const detected = [];
4278
+ let fromHere = false;
4279
+ for (const step of steps) {
4280
+ if (step.id === options.from) fromHere = true;
4281
+ const forced = options.only !== void 0 ? step.id === options.only : fromHere;
4282
+ detected.push({
4283
+ step,
4284
+ status: forced ? "todo" : await step.detect(ctx),
4285
+ forced
4286
+ });
4287
+ }
4288
+ return detected;
4289
+ }
4290
+ async function shouldRun(entry, ctx, options) {
4291
+ if (entry.forced) return true;
4292
+ if (options.only !== void 0 || entry.status === "done") return false;
4293
+ return await entry.step.detect(ctx) === "todo";
4294
+ }
4295
+ function recordFor(outcome, at) {
4296
+ return {
4297
+ status: outcome === "completed" ? "done" : "skipped",
4298
+ completedAt: at.toISOString()
4299
+ };
4300
+ }
4301
+ function withStepRecord(state, id, record) {
4302
+ return {
4303
+ ...state,
4304
+ steps: {
4305
+ ...state.steps,
4306
+ [id]: record
4307
+ }
3995
4308
  };
3996
4309
  }
4310
+ async function runWizard(steps, ctx, options = {}) {
4311
+ requireKnownStep(steps, options.from, "--from");
4312
+ requireKnownStep(steps, options.only, "--only");
4313
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
4314
+ const detected = await detectAll(steps, ctx, options);
4315
+ ctx.ui.checklist(detected.map(({ step, status }) => ({
4316
+ title: step.title,
4317
+ status
4318
+ })));
4319
+ for (const entry of detected) {
4320
+ if (!await shouldRun(entry, ctx, options)) continue;
4321
+ const outcome = await entry.step.run(ctx);
4322
+ await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
4323
+ }
4324
+ }
3997
4325
  //#endregion
3998
4326
  //#region src/steps/agents-skills.ts
3999
4327
  const STEP_ID$8 = "agents-skills";
@@ -4002,7 +4330,8 @@ const DETECT_AGENTS = { agents: null };
4002
4330
  function hostDeps$11(ctx) {
4003
4331
  return {
4004
4332
  runner: processRunner(ctx.signal),
4005
- probe: hostAgentProbe()
4333
+ probe: hostAgentProbe(),
4334
+ skillSource: rdeSkillSource()
4006
4335
  };
4007
4336
  }
4008
4337
  function detectedOnly(agents) {
@@ -4062,9 +4391,10 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
4062
4391
  ctx.ui.info(skipLine(all, selection.source));
4063
4392
  return "skipped";
4064
4393
  }
4065
- ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`);
4394
+ const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-skill" ? "" : ` from ${resolved.skillSource}`;
4395
+ ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
4066
4396
  await skillsCli(resolved.runner).add({
4067
- source: RDE_SKILL_SOURCE,
4397
+ source: resolved.skillSource,
4068
4398
  skill: "rde",
4069
4399
  agents: selection.agents.map((agent) => agent.skillsCliName)
4070
4400
  });
@@ -4090,7 +4420,8 @@ function rdeInstalledWith() {
4090
4420
  function hostDeps$10(ctx) {
4091
4421
  return {
4092
4422
  runner: processRunner(ctx.signal),
4093
- installedWith: rdeInstalledWith()
4423
+ installedWith: rdeInstalledWith(),
4424
+ locate: locateMbOnPath
4094
4425
  };
4095
4426
  }
4096
4427
  async function inspectMb(runner) {
@@ -4107,14 +4438,100 @@ async function inspectMb(runner) {
4107
4438
  function displayCommand(install) {
4108
4439
  return [install.command, ...install.args].join(" ");
4109
4440
  }
4110
- function stillWrong(install, after) {
4111
- const found = after.installed ? `mb ${after.version} is still what is on PATH (${MIN_MB_VERSION} or newer is required)` : "mb is not on PATH after the install";
4112
- return new ExternalToolError({
4441
+ async function versionAt(runner, path) {
4442
+ try {
4443
+ const raw = await mbTool(runner).version(path);
4444
+ return raw === null ? null : semver.valid(raw);
4445
+ } catch (error) {
4446
+ if (error instanceof ExternalToolError) return null;
4447
+ throw error;
4448
+ }
4449
+ }
4450
+ async function inspectMbOnPath(runner, locate) {
4451
+ const found = [];
4452
+ for (const binary of await locate()) found.push({
4453
+ path: binary.path,
4454
+ version: await versionAt(runner, binary.path),
4455
+ owner: mbOwner(binary),
4456
+ removeCommand: mbRemoveCommand(binary)
4457
+ });
4458
+ return found;
4459
+ }
4460
+ function satisfiesMinimum(version) {
4461
+ return version !== null && semver.satisfies(version, MB_RANGE, { includePrerelease: true });
4462
+ }
4463
+ function isNewer(candidate, than) {
4464
+ if (candidate === null) return false;
4465
+ return than === null || semver.gt(candidate, than);
4466
+ }
4467
+ function diagnoseMb(found, installer) {
4468
+ const [running, ...later] = found;
4469
+ if (running === void 0) return { kind: "missing" };
4470
+ const sufficient = satisfiesMinimum(running.version);
4471
+ const newer = later.find((candidate) => satisfiesMinimum(candidate.version) && isNewer(candidate.version, running.version));
4472
+ if (newer !== void 0 && (!sufficient || running.owner !== installer)) return {
4473
+ kind: "shadowed",
4474
+ running,
4475
+ newer
4476
+ };
4477
+ return sufficient ? {
4478
+ kind: "current",
4479
+ running
4480
+ } : {
4481
+ kind: "old",
4482
+ running
4483
+ };
4484
+ }
4485
+ function runningMbSuffices(diagnosis) {
4486
+ return diagnosis.kind !== "missing" && satisfiesMinimum(diagnosis.running.version);
4487
+ }
4488
+ function versionText(entry) {
4489
+ return entry.version ?? "an unknown version";
4490
+ }
4491
+ function describeEntry(entry) {
4492
+ const owner = entry.owner === null ? "" : ` (${MB_OWNER_NAMES[entry.owner]})`;
4493
+ return `${versionText(entry)} at ${entry.path}${owner}`;
4494
+ }
4495
+ function mbPathLines(found) {
4496
+ return found.map((entry, index) => `${index === 0 ? "" : "then "}${describeEntry(entry)}`);
4497
+ }
4498
+ function removeClause(entry) {
4499
+ if (entry.removeCommand !== null) return `remove the old one with \`${entry.removeCommand}\``;
4500
+ if (entry.owner !== null) return `remove the old one with ${MB_OWNER_NAMES[entry.owner]}`;
4501
+ return `remove ${entry.path}`;
4502
+ }
4503
+ function shadowedHint(diagnosis) {
4504
+ const { running, newer } = diagnosis;
4505
+ return [
4506
+ `${running.path} (${versionText(running)}) runs before ${newer.path} (${versionText(newer)});`,
4507
+ `${removeClause(running)}, or put ${dirname(newer.path)} before ${dirname(running.path)} on PATH,`,
4508
+ "then open a new terminal"
4509
+ ].join(" ");
4510
+ }
4511
+ const NEW_TERMINAL = "open a new terminal and run rde init again";
4512
+ function stillWrongAdvice(diagnosis) {
4513
+ switch (diagnosis.kind) {
4514
+ case "shadowed": return `${shadowedHint(diagnosis)} and run rde init again`;
4515
+ case "old": return `The install put no newer mb on PATH; ${removeClause(diagnosis.running)}, or ${NEW_TERMINAL}`;
4516
+ case "missing":
4517
+ case "current": return "Open a new terminal and run rde init again";
4518
+ }
4519
+ }
4520
+ async function stillWrong(install, after, deps) {
4521
+ const fail = (stderrTail) => new ExternalToolError({
4113
4522
  tool: install.command,
4114
4523
  argv: install.args,
4115
4524
  exitCode: 0,
4116
- stderrTail: `${found}; open a new terminal and run rde init again`
4525
+ stderrTail
4117
4526
  });
4527
+ if (!after.installed) return fail(`mb is not on PATH after the install; ${NEW_TERMINAL}`);
4528
+ const found = await inspectMbOnPath(deps.runner, deps.locate);
4529
+ const diagnosis = diagnoseMb(found, deps.installedWith);
4530
+ return fail([
4531
+ `mb ${after.version} still runs first (${MIN_MB_VERSION} or newer is required).`,
4532
+ `Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.`,
4533
+ `${stillWrongAdvice(diagnosis)}.`
4534
+ ].join(" "));
4118
4535
  }
4119
4536
  function installMbStep(deps = hostDeps$10) {
4120
4537
  return {
@@ -4142,7 +4559,7 @@ function installMbStep(deps = hostDeps$10) {
4142
4559
  stderrTail: ""
4143
4560
  });
4144
4561
  const after = await inspectMb(resolved.runner);
4145
- if (!after.installed || !after.satisfies) throw stillWrong(install, after);
4562
+ if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
4146
4563
  ctx.ui.success(`mb ${after.version} is installed`);
4147
4564
  return "completed";
4148
4565
  }
@@ -4463,7 +4880,15 @@ function hostDoctorDeps(instanceName, signal) {
4463
4880
  runtime,
4464
4881
  ports: hostPortProbe,
4465
4882
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
4466
- probe: hostAgentProbe()
4883
+ probe: hostAgentProbe(),
4884
+ locateMb: locateMbOnPath,
4885
+ environment: mbEnvironment(),
4886
+ skills: {
4887
+ source: rdeSkillSource(),
4888
+ cwd: process.cwd(),
4889
+ stateHome: xdgStateHome(),
4890
+ files: hostSkillFiles
4891
+ }
4467
4892
  };
4468
4893
  }
4469
4894
  const NOT_FOUND_STATUS$2 = 404;
@@ -4476,7 +4901,17 @@ function ok(id, label, detail) {
4476
4901
  status: "ok",
4477
4902
  detail,
4478
4903
  hint: null,
4479
- fix: null
4904
+ step: null
4905
+ };
4906
+ }
4907
+ function note$1(id, label, detail, hint) {
4908
+ return {
4909
+ id,
4910
+ label,
4911
+ status: "ok",
4912
+ detail,
4913
+ hint,
4914
+ step: null
4480
4915
  };
4481
4916
  }
4482
4917
  function skipped(id, label, detail) {
@@ -4486,7 +4921,7 @@ function skipped(id, label, detail) {
4486
4921
  status: "skipped",
4487
4922
  detail,
4488
4923
  hint: null,
4489
- fix: null
4924
+ step: null
4490
4925
  };
4491
4926
  }
4492
4927
  function failed(id, label, failure) {
@@ -4507,7 +4942,7 @@ function machineRow(check, needed) {
4507
4942
  return failed(check.id, check.id, {
4508
4943
  detail: check.label,
4509
4944
  hint: check.hint,
4510
- fix: "preflight"
4945
+ step: "preflight"
4511
4946
  });
4512
4947
  }
4513
4948
  async function machineRows(deps, runtime) {
@@ -4519,19 +4954,48 @@ function installHint(installedWith) {
4519
4954
  const install = mbInstallCommand(installedWith);
4520
4955
  return `\`${[install.command, ...install.args].join(" ")}\``;
4521
4956
  }
4522
- async function mbRow(deps) {
4523
- const found = await inspectMb(deps.runner);
4524
- if (!found.installed) return failed("mb", "mb", {
4525
- detail: "not installed",
4526
- hint: installHint(deps.installedWith),
4527
- fix: "install-mb"
4528
- });
4529
- if (!found.satisfies) return failed("mb", "mb", {
4530
- detail: `${found.version} (${MIN_MB_VERSION} or newer is required)`,
4531
- hint: installHint(deps.installedWith),
4532
- fix: "install-mb"
4533
- });
4534
- return ok("mb", "mb", found.version);
4957
+ const MB_LABEL = "mb";
4958
+ function mbUnusable(diagnosis) {
4959
+ if (diagnosis.kind === "missing") return "mb is not installed";
4960
+ return runningMbSuffices(diagnosis) ? null : "the mb that runs is too old";
4961
+ }
4962
+ function mbRow(diagnosis, lines, deps) {
4963
+ switch (diagnosis.kind) {
4964
+ case "missing": return failed("mb", MB_LABEL, {
4965
+ detail: "not installed",
4966
+ hint: installHint(deps.installedWith),
4967
+ step: "install-mb"
4968
+ });
4969
+ case "old": {
4970
+ const required = lines.map((line, index) => index === 0 ? `${line}; ${MIN_MB_VERSION} or newer is required` : line);
4971
+ return failed("mb", MB_LABEL, {
4972
+ detail: required.join("\n"),
4973
+ hint: installHint(deps.installedWith),
4974
+ step: "install-mb"
4975
+ });
4976
+ }
4977
+ case "shadowed": return failed("mb", MB_LABEL, {
4978
+ detail: lines.join("\n"),
4979
+ hint: shadowedHint(diagnosis),
4980
+ step: null
4981
+ });
4982
+ case "current": return ok("mb", MB_LABEL, lines.join("\n"));
4983
+ }
4984
+ }
4985
+ async function mbFinding(deps) {
4986
+ const found = await inspectMbOnPath(deps.runner, deps.locateMb);
4987
+ const diagnosis = diagnoseMb(found, deps.installedWith);
4988
+ return {
4989
+ row: mbRow(diagnosis, mbPathLines(found), deps),
4990
+ unusable: mbUnusable(diagnosis)
4991
+ };
4992
+ }
4993
+ function sharedProfileList(deps) {
4994
+ let listed = null;
4995
+ return () => {
4996
+ listed ??= mbTool(deps.runner).authList();
4997
+ return listed;
4998
+ };
4535
4999
  }
4536
5000
  function publishesOnLoopback(container, port) {
4537
5001
  return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
@@ -4549,7 +5013,7 @@ async function containerRow(runtime, deps, dockerOk) {
4549
5013
  row: failed("container", label, {
4550
5014
  detail: `${runtime.containerName} does not exist`,
4551
5015
  hint: "`rde init` recreates it from the saved choices",
4552
- fix: "runtime"
5016
+ step: "runtime"
4553
5017
  }),
4554
5018
  running: false,
4555
5019
  holder: CONTAINER
@@ -4562,7 +5026,7 @@ async function containerRow(runtime, deps, dockerOk) {
4562
5026
  row: failed("container", label, {
4563
5027
  detail: `${runtime.containerName} runs ${inspection.image}, the state says ${runtime.image}`,
4564
5028
  hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
4565
- fix: null
5029
+ step: null
4566
5030
  }),
4567
5031
  ...found
4568
5032
  };
@@ -4570,7 +5034,7 @@ async function containerRow(runtime, deps, dockerOk) {
4570
5034
  row: failed("container", label, {
4571
5035
  detail: `${runtime.containerName} does not publish port ${runtime.port} on ${LOOPBACK_HOST}`,
4572
5036
  hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
4573
- fix: null
5037
+ step: null
4574
5038
  }),
4575
5039
  ...found
4576
5040
  };
@@ -4591,7 +5055,7 @@ async function processRow(runtime, deps) {
4591
5055
  row: failed("process", label, {
4592
5056
  detail: `${inspection.jarPath} does not exist`,
4593
5057
  hint,
4594
- fix: "runtime"
5058
+ step: "runtime"
4595
5059
  }),
4596
5060
  running: false,
4597
5061
  holder: JAR_PROCESS
@@ -4618,13 +5082,13 @@ async function portRow(port, finding, ports) {
4618
5082
  if (finding.running) return failed("port", "port", {
4619
5083
  detail: `${port} is not answering although ${finding.holder} runs`,
4620
5084
  hint: "`rde logs` shows what Metabase is doing",
4621
- fix: null
5085
+ step: null
4622
5086
  });
4623
5087
  if (free) return ok("port", "port", `${port} is free`);
4624
5088
  return failed("port", "port", {
4625
5089
  detail: `${port} is in use by another program`,
4626
5090
  hint: "stop that program before `rde start`",
4627
- fix: null
5091
+ step: null
4628
5092
  });
4629
5093
  }
4630
5094
  async function healthRow(url, deps) {
@@ -4639,7 +5103,7 @@ async function healthRow(url, deps) {
4639
5103
  row: failed("health", "health", {
4640
5104
  detail: `initializing (${Math.round(error.progress * 100)}%)`,
4641
5105
  hint: "wait for it; `rde logs -f` shows progress",
4642
- fix: "start-metabase"
5106
+ step: "start-metabase"
4643
5107
  }),
4644
5108
  healthy: false
4645
5109
  };
@@ -4647,7 +5111,7 @@ async function healthRow(url, deps) {
4647
5111
  row: failed("health", "health", {
4648
5112
  detail: "unreachable",
4649
5113
  hint: "`rde start`",
4650
- fix: "start-metabase"
5114
+ step: "start-metabase"
4651
5115
  }),
4652
5116
  healthy: false
4653
5117
  };
@@ -4655,7 +5119,7 @@ async function healthRow(url, deps) {
4655
5119
  row: failed("health", "health", {
4656
5120
  detail: `something other than Metabase answers at ${url} (HTTP ${error.status})`,
4657
5121
  hint: "another program holds the port; stop it before `rde start`",
4658
- fix: null
5122
+ step: null
4659
5123
  }),
4660
5124
  healthy: false
4661
5125
  };
@@ -4666,7 +5130,7 @@ function adminRow(state, properties) {
4666
5130
  if (!properties["has-user-setup"]) return failed("admin", "admin user", {
4667
5131
  detail: "Metabase has no admin user yet",
4668
5132
  hint: "`rde init --only admin-user`",
4669
- fix: "admin-user"
5133
+ step: "admin-user"
4670
5134
  });
4671
5135
  return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
4672
5136
  }
@@ -4678,36 +5142,204 @@ function versionRow(state, properties) {
4678
5142
  if (state.version !== running) return failed("version", label, {
4679
5143
  detail: `running ${running}, the state file says ${state.version}`,
4680
5144
  hint: "`rde start` records the running version",
4681
- fix: VERSION_STEP
5145
+ step: VERSION_STEP
4682
5146
  });
4683
5147
  return ok("version", label, running);
4684
5148
  }
4685
- function apiKeyRow(state) {
4686
- if (state.apiKey === null) return failed("api-key", "API key", {
4687
- detail: "none in the state file",
4688
- hint: "`rde init --only api-key`",
4689
- fix: "api-key"
5149
+ const API_KEY_STEP = "api-key";
5150
+ const RECREATE_HINT = "when the instance holds nothing you need, `rde uninstall` then `rde init`";
5151
+ function adminPasswordRow(state, secrets) {
5152
+ const id = "admin-password";
5153
+ const label = "admin password";
5154
+ const admin = state.admin;
5155
+ if (admin === null) return skipped(id, label, "no admin user created by rde yet");
5156
+ switch (admin.kind) {
5157
+ case "prompted": return ok(id, label, "yours, entered during `rde init` and never stored");
5158
+ case "generated":
5159
+ if (secrets === null || secrets.admin === null) return failed(id, label, {
5160
+ detail: "generated, but missing from the secrets file",
5161
+ hint: `it cannot be recovered; ${RECREATE_HINT}`,
5162
+ step: null
5163
+ });
5164
+ return ok(id, label, "generated and stored; `rde credentials` shows it in your terminal");
5165
+ case "legacy-default": return failed(id, label, {
5166
+ detail: "the public default password from an earlier rde (legacy-default)",
5167
+ hint: `anyone who knows the default can log in; ${RECREATE_HINT} with a generated one`,
5168
+ step: null
5169
+ });
5170
+ }
5171
+ }
5172
+ async function apiKeyFinding(secrets, url, healthy, deps) {
5173
+ const id = "api-key";
5174
+ const label = "API key";
5175
+ const apiKey = secrets === null ? null : secrets.apiKey;
5176
+ if (apiKey === null) return {
5177
+ row: failed(id, label, {
5178
+ detail: "none in the secrets file",
5179
+ hint: "`rde init --only api-key`",
5180
+ step: API_KEY_STEP
5181
+ }),
5182
+ access: {
5183
+ kind: "unusable",
5184
+ reason: "no API key"
5185
+ }
5186
+ };
5187
+ if (url === null || !healthy) return {
5188
+ row: ok(id, label, "in the secrets file"),
5189
+ access: {
5190
+ kind: "unusable",
5191
+ reason: NOT_HEALTHY
5192
+ }
5193
+ };
5194
+ const client = deps.client(url, {
5195
+ kind: "api-key",
5196
+ key: apiKey
4690
5197
  });
4691
- return ok("api-key", "API key", "in the state file");
5198
+ try {
5199
+ await client.user.current();
5200
+ } catch (error) {
5201
+ if (!isRejectedCredential(error)) throw error;
5202
+ return {
5203
+ row: failed(id, label, {
5204
+ detail: "in the secrets file, but Metabase rejects it",
5205
+ hint: "`rde init --only api-key` replaces it",
5206
+ step: API_KEY_STEP
5207
+ }),
5208
+ access: {
5209
+ kind: "unusable",
5210
+ reason: "Metabase rejects the API key"
5211
+ }
5212
+ };
5213
+ }
5214
+ return {
5215
+ row: ok(id, label, "in the secrets file, accepted by Metabase"),
5216
+ access: {
5217
+ kind: "accepted",
5218
+ client
5219
+ }
5220
+ };
5221
+ }
5222
+ function octal(mode) {
5223
+ return `0${mode.toString(8)}`;
5224
+ }
5225
+ function expectsSecrets(state) {
5226
+ return state.admin !== null && state.admin.kind !== "prompted" || state.steps[API_KEY_STEP]?.status === "done";
4692
5227
  }
4693
- async function mbProfileRow(state, deps, mbInstalled, healthy) {
5228
+ function secretsFileRow(state, files, dir) {
5229
+ const id = "secrets-file";
5230
+ const label = "secrets file";
5231
+ const path = secretsFileIn(dir);
5232
+ if (files.secretsMode === null) {
5233
+ if (!expectsSecrets(state)) return skipped(id, label, "nothing stored yet");
5234
+ return failed(id, label, {
5235
+ detail: `${path} is missing`,
5236
+ hint: "`rde init --only api-key` stores a new API key",
5237
+ step: API_KEY_STEP
5238
+ });
5239
+ }
5240
+ if (files.secretsMode !== 384 || files.dirMode !== 448) {
5241
+ const dirMode = files.dirMode === null ? "missing" : octal(files.dirMode);
5242
+ return failed(id, label, {
5243
+ detail: `${path} is ${octal(files.secretsMode)} in a ${dirMode} directory`,
5244
+ hint: `\`chmod ${octal(384)} ${path} && chmod ${octal(448)} ${dir}\``,
5245
+ step: null
5246
+ });
5247
+ }
5248
+ return ok(id, label, `${path}, ${octal(384)} in a ${octal(448)} directory`);
5249
+ }
5250
+ function stateFileRow(files) {
5251
+ const id = "state-file";
5252
+ const label = "state file";
5253
+ if (files.secretsInState.length > 0) return failed(id, label, {
5254
+ detail: `carries ${files.secretsInState.join(", ")}`,
5255
+ hint: "`rde init --only api-key` rewrites it without them",
5256
+ step: API_KEY_STEP
5257
+ });
5258
+ return ok(id, label, "no secrets");
5259
+ }
5260
+ async function credentialRows(state, secrets, apiKey, deps) {
5261
+ const dir = deps.runtime.instanceDir;
5262
+ const files = await inspectCredentialFiles(dir);
5263
+ return [
5264
+ adminPasswordRow(state, secrets),
5265
+ apiKey.row,
5266
+ secretsFileRow(state, files, dir),
5267
+ stateFileRow(files)
5268
+ ];
5269
+ }
5270
+ function withoutTrailingSlash(url) {
5271
+ return url.replace(/\/+$/, "");
5272
+ }
5273
+ async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
4694
5274
  const label = "mb profile";
4695
- if (!mbInstalled) return skipped("mb-profile", label, "mb is not installed");
5275
+ if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
4696
5276
  if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY);
4697
- const entry = (await mbTool(deps.runner).authList()).find((candidate) => candidate.profile === state.mbProfile);
5277
+ const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
4698
5278
  if (entry === void 0) return failed("mb-profile", label, {
4699
5279
  detail: `${state.mbProfile} is not configured`,
4700
5280
  hint: "`rde init --only api-key`",
4701
- fix: "api-key"
5281
+ step: "api-key"
5282
+ });
5283
+ if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
5284
+ detail: `${state.mbProfile} points at ${entry.url}, not at this instance (${url})`,
5285
+ hint: "`rde init --only api-key` logs it back in against the instance",
5286
+ step: "api-key"
4702
5287
  });
4703
5288
  if (entry.status !== "ok") return failed("mb-profile", label, {
4704
5289
  detail: `${state.mbProfile}: ${entry.status}`,
4705
5290
  hint: "`rde init --only api-key` logs it in again",
4706
- fix: "api-key"
5291
+ step: "api-key"
4707
5292
  });
4708
5293
  const user = entry.user === null ? "" : ` as ${entry.user.name}`;
4709
5294
  return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
4710
5295
  }
5296
+ function hostOf(url) {
5297
+ return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
5298
+ }
5299
+ function unsetHint(name) {
5300
+ return `\`unset ${name}\`, remove it from your shell profile if it is exported there, then restart the agent session`;
5301
+ }
5302
+ async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
5303
+ const id = `env.${ENV_MB_PROFILE}`;
5304
+ if (name === rdeProfile) return ok(id, ENV_MB_PROFILE, `${name}: rde's own profile, so plain \`mb\` uses this instance`);
5305
+ const rde = rdeProfile === null ? "rde's" : `rde's (${rdeProfile})`;
5306
+ const detail = (where) => `${name}: \`mb\` and the rde skill will use profile ${name} (${where}) instead of ${rde}`;
5307
+ if (mbUnusableReason !== null) return note$1(id, ENV_MB_PROFILE, detail(mbUnusableReason), unsetHint(ENV_MB_PROFILE));
5308
+ const entry = (await profiles()).find((candidate) => candidate.profile === name);
5309
+ const where = entry === void 0 ? "not configured" : hostOf(entry.url);
5310
+ return note$1(id, ENV_MB_PROFILE, detail(where), unsetHint(ENV_MB_PROFILE));
5311
+ }
5312
+ async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
5313
+ const rows = [];
5314
+ if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
5315
+ 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)));
5316
+ 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)));
5317
+ return rows;
5318
+ }
5319
+ const PROFILE_STATUS_WORDS = {
5320
+ ok: "authenticates",
5321
+ "auth-failed": "login rejected",
5322
+ "network-error": "not answering",
5323
+ "server-error": "answers with a server error",
5324
+ "not-probed": "not checked"
5325
+ };
5326
+ function plainMbLine(env, rdeProfile, profiles) {
5327
+ if (env.url !== null) return `plain \`mb\` goes to ${hostOf(env.url)} (${ENV_MB_URL})`;
5328
+ const name = env.profile ?? "default";
5329
+ const via = env.profile === null ? "" : ` (${ENV_MB_PROFILE})`;
5330
+ const entry = profiles.find((candidate) => candidate.profile === name);
5331
+ if (entry === void 0) return `plain \`mb\` uses ${name}${via}, which is not configured`;
5332
+ const instance = name === rdeProfile ? ", this instance" : "";
5333
+ return `plain \`mb\` uses ${name}${via}: ${hostOf(entry.url)}${instance}`;
5334
+ }
5335
+ async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
5336
+ const id = "other-metabases";
5337
+ const label = "other Metabases";
5338
+ if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
5339
+ const all = await profiles();
5340
+ const others = all.filter((entry) => entry.profile !== rdeProfile);
5341
+ return ok(id, label, [...others.length === 0 ? ["none"] : others.map((entry) => `${entry.profile}: ${hostOf(entry.url)}, ${PROFILE_STATUS_WORDS[entry.status]}`), plainMbLine(env, rdeProfile, all)].join("\n"));
5342
+ }
4711
5343
  function isNotFound$2(error) {
4712
5344
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
4713
5345
  }
@@ -4718,7 +5350,7 @@ async function licenseRow(client) {
4718
5350
  if (!status.valid) return failed("license", label, {
4719
5351
  detail: `invalid: ${status.status}`,
4720
5352
  hint: "`rde init --from license` with a valid token",
4721
- fix: "license"
5353
+ step: "license"
4722
5354
  });
4723
5355
  return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
4724
5356
  } catch (error) {
@@ -4735,47 +5367,47 @@ async function databasesRow(state, client) {
4735
5367
  if (missing.length > 0) return failed("databases", label, {
4736
5368
  detail: `not on the server any more: ${missing.map((record) => record.name).join(", ")}`,
4737
5369
  hint: "`rde db add` connects them again",
4738
- fix: "databases"
5370
+ step: "databases"
4739
5371
  });
4740
5372
  const names = state.databases.map((record) => record.name).join(", ");
4741
5373
  return ok("databases", label, `${state.databases.length} connected: ${names}`);
4742
5374
  }
4743
5375
  const SAMPLE_DATABASE_STEP = "sample-database";
4744
5376
  const SAMPLE_LABEL = "sample database";
4745
- async function sampleDatabaseRow(record, deps, dockerOk, client) {
5377
+ async function sampleDatabaseRow(record, deps, dockerOk, access) {
4746
5378
  const id = "sample-database";
4747
5379
  if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
4748
5380
  const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
4749
5381
  if (!status.installed) return failed(id, SAMPLE_LABEL, {
4750
5382
  detail: `${record.containerName} does not exist`,
4751
5383
  hint: "`rde init` recreates it",
4752
- fix: SAMPLE_DATABASE_STEP
5384
+ step: SAMPLE_DATABASE_STEP
4753
5385
  });
4754
5386
  if (!status.running) return failed(id, SAMPLE_LABEL, {
4755
5387
  detail: `${record.containerName} is stopped`,
4756
5388
  hint: "`rde start`",
4757
- fix: "start-metabase"
5389
+ step: "start-metabase"
4758
5390
  });
4759
5391
  if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
4760
5392
  detail: `${record.containerName} runs but port ${record.port} is not answering`,
4761
5393
  hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
4762
- fix: null
5394
+ step: null
4763
5395
  });
4764
5396
  const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
4765
5397
  if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
4766
5398
  detail: `${where}; not connected to Metabase yet`,
4767
5399
  hint: "`rde init` connects it",
4768
- fix: SAMPLE_DATABASE_STEP
5400
+ step: SAMPLE_DATABASE_STEP
4769
5401
  });
4770
- if (client === null) return skipped(id, SAMPLE_LABEL, `${where}; ${NOT_HEALTHY}, so the connection is unchecked`);
5402
+ if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
4771
5403
  try {
4772
- const database = await client.database.get(record.databaseId);
5404
+ const database = await access.client.database.get(record.databaseId);
4773
5405
  return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
4774
5406
  } catch (error) {
4775
5407
  if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
4776
5408
  detail: `${where}; database ${record.databaseId} is gone from Metabase`,
4777
5409
  hint: "`rde init` connects it again",
4778
- fix: SAMPLE_DATABASE_STEP
5410
+ step: SAMPLE_DATABASE_STEP
4779
5411
  });
4780
5412
  throw error;
4781
5413
  }
@@ -4792,7 +5424,7 @@ async function gitSyncRow(state, deps) {
4792
5424
  if (status.branch === null) return failed("git-sync", label, {
4793
5425
  detail: "Metabase reports no branch",
4794
5426
  hint: "`rde init --only git-sync`",
4795
- fix: "git-sync"
5427
+ step: "git-sync"
4796
5428
  });
4797
5429
  const dirty = status.is_dirty ? "unsynced changes" : "in sync";
4798
5430
  return ok("git-sync", label, `branch ${status.branch}, ${dirty}, ${state.gitRemote.workdir}`);
@@ -4800,15 +5432,100 @@ async function gitSyncRow(state, deps) {
4800
5432
  if (isMbFailure$1(error)) return failed("git-sync", label, {
4801
5433
  detail: error.message,
4802
5434
  hint: `\`${mbCommand(state.mbProfile)} git-sync status\` shows the full answer`,
4803
- fix: null
5435
+ step: null
4804
5436
  });
4805
5437
  throw error;
4806
5438
  }
4807
5439
  }
4808
5440
  const SKILL_STEP = "agents-skills";
5441
+ const GLOBAL_SCOPE = "global";
5442
+ const SKILL_FILE = "SKILL.md";
5443
+ const CLAUDE_CODE = "Claude Code";
5444
+ const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
4809
5445
  function skillRowId(agent) {
4810
5446
  return `skill.${agent.id}`;
4811
5447
  }
5448
+ function withRef(source, ref) {
5449
+ return ref === null ? source : `${source}#${ref}`;
5450
+ }
5451
+ async function copyRef(entry, deps) {
5452
+ const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
5453
+ return lockedRef(path, await deps.skills.files.readJson(path), entry.name);
5454
+ }
5455
+ async function judgeCopy(entry, origin, deps) {
5456
+ const where = `${entry.scope} ${entry.path}`;
5457
+ switch (origin.kind) {
5458
+ case "local": {
5459
+ const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
5460
+ const wanted = await deps.skills.files.readText(join(origin.path, SKILL_FILE));
5461
+ const current = installed !== null && installed === wanted;
5462
+ return {
5463
+ entry,
5464
+ current,
5465
+ line: `${where}: ${current ? "same as" : "differs from"} ${origin.path}`
5466
+ };
5467
+ }
5468
+ case "github": {
5469
+ const wanted = withRef(origin.repository, origin.ref);
5470
+ if (entry.source === null) return {
5471
+ entry,
5472
+ current: false,
5473
+ line: `${where}: no recorded source, not ${wanted}`
5474
+ };
5475
+ const found = withRef(entry.source, await copyRef(entry, deps));
5476
+ const current = found === wanted;
5477
+ return {
5478
+ entry,
5479
+ current,
5480
+ line: `${where}: from ${found}${current ? "" : `, not ${wanted}`}`
5481
+ };
5482
+ }
5483
+ case "other": {
5484
+ const current = entry.source === origin.source;
5485
+ return {
5486
+ entry,
5487
+ current,
5488
+ line: `${where}: from ${entry.source ?? "no recorded source"}${current ? "" : `, not ${origin.source}`}`
5489
+ };
5490
+ }
5491
+ }
5492
+ }
5493
+ function loadedCopyLine(entries, cwd) {
5494
+ if (entries.length < 2) return null;
5495
+ const count = `${entries.length} copies are installed`;
5496
+ const forClaude = entries.filter((entry) => entry.agents.includes(CLAUDE_CODE));
5497
+ const loaded = forClaude.find((entry) => entry.scope === GLOBAL_SCOPE) ?? forClaude[0];
5498
+ if (loaded === void 0) return count;
5499
+ return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
5500
+ }
5501
+ function staleCopiesRow(stale, detail, deps) {
5502
+ const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
5503
+ const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
5504
+ const repairs = [];
5505
+ if (globalStale) repairs.push(`\`rde init --only ${SKILL_STEP}\` reinstalls the global copy from ${deps.skills.source}`);
5506
+ if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove rde -y\` in ${deps.skills.cwd}`);
5507
+ return failed("skill-copies", "rde skill copies", {
5508
+ detail,
5509
+ hint: `${repairs.join("; ")}; then restart the agent session`,
5510
+ step: globalStale ? SKILL_STEP : null
5511
+ });
5512
+ }
5513
+ async function skillCopiesRow(globalEntries, deps) {
5514
+ const id = "skill-copies";
5515
+ const label = "rde skill copies";
5516
+ const cwd = deps.skills.cwd;
5517
+ const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === "rde");
5518
+ const entries = [...globalEntries, ...projectEntries];
5519
+ if (entries.length === 0) return skipped(id, label, "no copy installed");
5520
+ const origin = skillOrigin(deps.skills.source, cwd);
5521
+ const judged = [];
5522
+ for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
5523
+ const loaded = loadedCopyLine(entries, cwd);
5524
+ const lines = judged.map((copy) => copy.line);
5525
+ const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
5526
+ const stale = judged.filter((copy) => !copy.current);
5527
+ return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
5528
+ }
4812
5529
  async function skillRows(state, deps) {
4813
5530
  const label = "rde skill";
4814
5531
  if (state === null) return [skipped("skill", label, NO_INSTANCE)];
@@ -4816,17 +5533,18 @@ async function skillRows(state, deps) {
4816
5533
  const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
4817
5534
  if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
4818
5535
  const entries = await rdeSkillEntries(deps.runner);
5536
+ const copies = await skillCopiesRow(entries, deps);
4819
5537
  if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
4820
5538
  detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
4821
5539
  hint: `\`rde init --only ${SKILL_STEP}\``,
4822
- fix: SKILL_STEP
4823
- })];
4824
- return detected.map((agent) => {
5540
+ step: SKILL_STEP
5541
+ }), copies];
5542
+ return [...detected.map((agent) => {
4825
5543
  const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
4826
5544
  const agentLabel = `${label} (${agent.displayName})`;
4827
5545
  if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
4828
5546
  return ok(skillRowId(agent), agentLabel, entry.path);
4829
- });
5547
+ }), copies];
4830
5548
  }
4831
5549
  function noRuntimeRows() {
4832
5550
  return [
@@ -4835,13 +5553,13 @@ function noRuntimeRows() {
4835
5553
  "health"
4836
5554
  ].map((id) => skipped(id, id, NO_INSTANCE));
4837
5555
  }
4838
- async function instanceRows(state, deps, dockerOk, mbInstalled) {
5556
+ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusableReason) {
4839
5557
  const configured = configuredRuntime(state);
4840
5558
  if (configured === null) return [
4841
5559
  ...noRuntimeRows(),
4842
5560
  skipped("version", "Metabase version", NO_INSTANCE),
4843
5561
  skipped("admin", "admin user", NO_INSTANCE),
4844
- apiKeyRow(state),
5562
+ ...await credentialRows(state, secrets, await apiKeyFinding(secrets, null, false, deps), deps),
4845
5563
  skipped("mb-profile", "mb profile", NO_INSTANCE),
4846
5564
  skipped("license", "license", NO_INSTANCE),
4847
5565
  skipped("databases", "databases", NO_INSTANCE),
@@ -4851,35 +5569,33 @@ async function instanceRows(state, deps, dockerOk, mbInstalled) {
4851
5569
  const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
4852
5570
  const { row: health, healthy } = await healthRow(configured.url, deps);
4853
5571
  const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
5572
+ const apiKey = await apiKeyFinding(secrets, configured.url, healthy, deps);
4854
5573
  const rows = [
4855
5574
  runtime.row,
4856
5575
  await portRow(configured.runtime.port, runtime, deps.ports),
4857
5576
  health,
4858
5577
  properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
4859
5578
  properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
4860
- apiKeyRow(state),
4861
- await mbProfileRow(state, deps, mbInstalled, healthy)
5579
+ ...await credentialRows(state, secrets, apiKey, deps),
5580
+ await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
4862
5581
  ];
4863
- const client = healthy && state.apiKey !== null ? deps.client(configured.url, {
4864
- kind: "api-key",
4865
- key: state.apiKey
4866
- }) : null;
4867
- if (client === null) {
4868
- const reason = healthy ? "no API key" : NOT_HEALTHY;
4869
- rows.push(skipped("license", "license", reason), skipped("databases", "databases", reason));
4870
- } else rows.push(await licenseRow(client), await databasesRow(state, client));
4871
- rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, client));
5582
+ const access = apiKey.access;
5583
+ if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
5584
+ else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
5585
+ rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
4872
5586
  rows.push(await gitSyncRow(state, deps));
4873
5587
  return rows;
4874
5588
  }
4875
- async function collectDoctor(instance, state, deps) {
5589
+ async function collectDoctor(instance, state, secrets, deps) {
4876
5590
  const machine = await machineRows(deps, state?.runtime ?? null);
4877
5591
  const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
4878
- const mb = await mbRow(deps);
4879
- const checks = [...machine, mb];
4880
- if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE), skipped("admin", "admin user", NO_INSTANCE), skipped("api-key", "API key", NO_INSTANCE), skipped("mb-profile", "mb profile", NO_INSTANCE), skipped("license", "license", NO_INSTANCE), skipped("databases", "databases", NO_INSTANCE), skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE), skipped("git-sync", "remote sync", NO_INSTANCE));
4881
- else checks.push(...await instanceRows(state, deps, dockerOk, mb.status === "ok"));
4882
- checks.push(...await skillRows(state, deps));
5592
+ const mb = await mbFinding(deps);
5593
+ const profiles = sharedProfileList(deps);
5594
+ const checks = [...machine, mb.row];
5595
+ if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE), skipped("admin", "admin user", NO_INSTANCE), skipped("admin-password", "admin password", NO_INSTANCE), skipped("api-key", "API key", NO_INSTANCE), skipped("secrets-file", "secrets file", NO_INSTANCE), skipped("state-file", "state file", NO_INSTANCE), skipped("mb-profile", "mb profile", NO_INSTANCE), skipped("license", "license", NO_INSTANCE), skipped("databases", "databases", NO_INSTANCE), skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE), skipped("git-sync", "remote sync", NO_INSTANCE));
5596
+ else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
5597
+ const rdeProfile = state === null ? null : state.mbProfile;
5598
+ checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
4883
5599
  return {
4884
5600
  instance,
4885
5601
  checks,
@@ -4891,18 +5607,38 @@ const MARK = {
4891
5607
  failed: "✗",
4892
5608
  skipped: "–"
4893
5609
  };
5610
+ const NOTE_MARK = "!";
4894
5611
  const HINT_ARROW = "→";
5612
+ function markFor(row) {
5613
+ return row.status === "ok" && row.hint !== null ? NOTE_MARK : MARK[row.status];
5614
+ }
4895
5615
  function doctorLines(report) {
4896
5616
  const width = Math.max(...report.checks.map((row) => row.label.length));
5617
+ const indent = ` ${" ".repeat(width)} `;
4897
5618
  const lines = [];
4898
5619
  for (const row of report.checks) {
4899
- lines.push(`${MARK[row.status]} ${row.label.padEnd(width)} ${row.detail}`);
4900
- if (row.hint !== null) lines.push(` ${" ".repeat(width)} ${HINT_ARROW} ${row.hint}`);
5620
+ const [first, ...rest] = row.detail.split("\n");
5621
+ lines.push(`${markFor(row)} ${row.label.padEnd(width)} ${first}`);
5622
+ lines.push(...rest.map((line) => `${indent}${line}`));
5623
+ if (row.hint !== null) lines.push(`${indent}${HINT_ARROW} ${row.hint}`);
4901
5624
  }
4902
5625
  return lines;
4903
5626
  }
5627
+ function fixCommand(step) {
5628
+ return step === null ? null : `rde init --only ${step}`;
5629
+ }
5630
+ function doctorJson(report) {
5631
+ return {
5632
+ instance: report.instance,
5633
+ checks: report.checks.map(({ step, ...row }) => ({
5634
+ ...row,
5635
+ fix: fixCommand(step)
5636
+ })),
5637
+ failed: report.failed
5638
+ };
5639
+ }
4904
5640
  function fixableSteps(report) {
4905
- const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.fix !== null ? [row.fix] : []));
5641
+ const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.step !== null ? [row.step] : []));
4906
5642
  return StepId.options.filter((id) => wanted.has(id));
4907
5643
  }
4908
5644
  function doctorSummary(report) {
@@ -4941,15 +5677,24 @@ const ADMIN_USER_KEYS = {
4941
5677
  firstName: `${STEP_ID$6}.first-name`,
4942
5678
  lastName: `${STEP_ID$6}.last-name`
4943
5679
  };
4944
- const DEFAULT_ADMIN_EMAIL = "admin@metabase.com";
4945
- const DEFAULT_ADMIN_PASSWORD = "metabase123";
5680
+ const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
5681
+ const GENERATED_PASSWORD_BYTES = 24;
4946
5682
  const DEFAULT_SITE_NAME = "rde";
4947
5683
  const MIN_PASSWORD_LENGTH = 6;
4948
5684
  const DIGIT = /\d/;
4949
5685
  const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4950
- const DEFAULT_ADMIN = { variant: "default" };
5686
+ const DEFAULT_ADMIN = { variant: "generated" };
5687
+ function generatePassword(random) {
5688
+ for (;;) {
5689
+ const candidate = random(GENERATED_PASSWORD_BYTES).toString("base64url");
5690
+ if (DIGIT.test(candidate)) return candidate;
5691
+ }
5692
+ }
4951
5693
  function hostDeps$8(ctx) {
4952
- return { client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal) };
5694
+ return {
5695
+ client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
5696
+ password: () => generatePassword(randomBytes)
5697
+ };
4953
5698
  }
4954
5699
  function isNotReady(error) {
4955
5700
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
@@ -4988,26 +5733,22 @@ async function promptedUser(ctx) {
4988
5733
  ...lastName.length > 0 && { last_name: lastName }
4989
5734
  };
4990
5735
  }
4991
- async function chooseUser(ctx, kind) {
4992
- switch (kind) {
4993
- case "default": return {
4994
- email: DEFAULT_ADMIN_EMAIL,
4995
- password: DEFAULT_ADMIN_PASSWORD
5736
+ async function chooseUser(ctx, variant, deps) {
5737
+ switch (variant) {
5738
+ case "generated": return {
5739
+ email: GENERATED_ADMIN_EMAIL,
5740
+ password: deps.password()
4996
5741
  };
4997
5742
  case "prompted": return promptedUser(ctx);
4998
5743
  }
4999
5744
  }
5000
- function credentialsRecord(kind, user) {
5001
- switch (kind) {
5002
- case "default": return {
5003
- kind,
5745
+ function storedLogin(variant, user) {
5746
+ switch (variant) {
5747
+ case "generated": return {
5004
5748
  email: user.email,
5005
5749
  password: user.password
5006
5750
  };
5007
- case "prompted": return {
5008
- kind,
5009
- email: user.email
5010
- };
5751
+ case "prompted": return null;
5011
5752
  }
5012
5753
  }
5013
5754
  function adminUserStep(options, deps = hostDeps$8) {
@@ -5026,7 +5767,8 @@ function adminUserStep(options, deps = hostDeps$8) {
5026
5767
  },
5027
5768
  async run(ctx) {
5028
5769
  const configured = requireConfiguredRuntime(ctx.state);
5029
- const client = deps(ctx).client(configured.url, NO_AUTH);
5770
+ const resolved = deps(ctx);
5771
+ const client = resolved.client(configured.url, NO_AUTH);
5030
5772
  const properties = await client.session.properties();
5031
5773
  if (properties["has-user-setup"]) {
5032
5774
  ctx.ui.info("Metabase already has an admin user");
@@ -5034,7 +5776,11 @@ function adminUserStep(options, deps = hostDeps$8) {
5034
5776
  }
5035
5777
  const token = properties["setup-token"];
5036
5778
  if (token === null) throw new ConfigError("Metabase has no admin user yet but offers no setup token");
5037
- const user = await chooseUser(ctx, options.variant);
5779
+ const user = await chooseUser(ctx, options.variant, resolved);
5780
+ await ctx.saveSecrets({
5781
+ ...ctx.secrets,
5782
+ admin: storedLogin(options.variant, user)
5783
+ });
5038
5784
  const session = await client.setup.create({
5039
5785
  token,
5040
5786
  user,
@@ -5043,7 +5789,10 @@ function adminUserStep(options, deps = hostDeps$8) {
5043
5789
  ctx.session.metabaseSession.set(session.id);
5044
5790
  await ctx.saveState({
5045
5791
  ...ctx.state,
5046
- admin: credentialsRecord(options.variant, user)
5792
+ admin: {
5793
+ kind: options.variant,
5794
+ email: user.email
5795
+ }
5047
5796
  });
5048
5797
  ctx.ui.success(`Admin user ${user.email} created`);
5049
5798
  return "completed";
@@ -5055,8 +5804,7 @@ function adminUserStep(options, deps = hostDeps$8) {
5055
5804
  const STEP_ID$5 = "api-key";
5056
5805
  const API_KEY_KEYS = {
5057
5806
  email: `${STEP_ID$5}.email`,
5058
- password: `${STEP_ID$5}.password`,
5059
- profile: `${STEP_ID$5}.profile`
5807
+ password: `${STEP_ID$5}.password`
5060
5808
  };
5061
5809
  const ADMINISTRATORS_GROUP_NAME = "Administrators";
5062
5810
  function hostDeps$7(ctx) {
@@ -5066,11 +5814,12 @@ function hostDeps$7(ctx) {
5066
5814
  };
5067
5815
  }
5068
5816
  async function adminCredentials(ctx) {
5069
- const admin = ctx.state.admin;
5070
- if (admin?.kind === "default") return {
5071
- username: admin.email,
5072
- password: admin.password
5817
+ const stored = ctx.secrets.admin;
5818
+ if (stored !== null) return {
5819
+ username: stored.email,
5820
+ password: stored.password
5073
5821
  };
5822
+ const admin = ctx.state.admin;
5074
5823
  ctx.ui.info("An admin login is needed once to create the API key; it is not saved");
5075
5824
  return {
5076
5825
  username: await ctx.prompts.text({
@@ -5108,47 +5857,35 @@ async function freshApiKey(ctx, client) {
5108
5857
  group_id: groupId
5109
5858
  })).unmasked_key;
5110
5859
  }
5111
- async function persistedApiKey(ctx, deps, url) {
5112
- if (ctx.state.apiKey !== null) return ctx.state.apiKey;
5860
+ async function authenticates(client) {
5861
+ try {
5862
+ await client.user.current();
5863
+ return true;
5864
+ } catch (error) {
5865
+ if (isRejectedCredential(error)) return false;
5866
+ throw error;
5867
+ }
5868
+ }
5869
+ async function workingApiKey(ctx, deps, url) {
5870
+ const stored = ctx.secrets.apiKey;
5871
+ if (stored !== null) {
5872
+ if (await authenticates(deps.client(url, {
5873
+ kind: "api-key",
5874
+ key: stored
5875
+ }))) return stored;
5876
+ ctx.ui.warn("Metabase rejects the stored API key; replacing it");
5877
+ }
5113
5878
  const sessionId = await superuserSession(ctx, deps, url);
5114
5879
  const apiKey = await freshApiKey(ctx, deps.client(url, {
5115
5880
  kind: "session",
5116
5881
  id: sessionId
5117
5882
  }));
5118
- await ctx.saveState({
5119
- ...ctx.state,
5883
+ await ctx.saveSecrets({
5884
+ ...ctx.secrets,
5120
5885
  apiKey
5121
5886
  });
5122
5887
  return apiKey;
5123
5888
  }
5124
- async function chooseProfile(ctx, mb, url) {
5125
- const profile = ctx.state.mbProfile;
5126
- if (profile !== "default") return profile;
5127
- let existing;
5128
- try {
5129
- existing = await mb.authStatus(DEFAULT_MB_PROFILE);
5130
- } catch (error) {
5131
- if (error instanceof ProcessNotFoundError) return profile;
5132
- throw error;
5133
- }
5134
- if (!existing.present || existing.url === url) return profile;
5135
- const chosen = await ctx.prompts.select({
5136
- key: API_KEY_KEYS.profile,
5137
- message: `mb profile ${DEFAULT_MB_PROFILE} already points at ${existing.url}; which profile should this instance use?`,
5138
- choices: [{
5139
- value: "rde",
5140
- label: `rde, and leave ${DEFAULT_MB_PROFILE} alone`
5141
- }, {
5142
- value: DEFAULT_MB_PROFILE,
5143
- label: `${DEFAULT_MB_PROFILE}, replacing it`
5144
- }]
5145
- });
5146
- if (chosen !== profile) await ctx.saveState({
5147
- ...ctx.state,
5148
- mbProfile: chosen
5149
- });
5150
- return chosen;
5151
- }
5152
5889
  function usageNote(profile) {
5153
5890
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
5154
5891
  return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
@@ -5158,7 +5895,7 @@ function apiKeyStep(deps = hostDeps$7) {
5158
5895
  id: STEP_ID$5,
5159
5896
  title: "Connect the Metabase CLI",
5160
5897
  async detect(ctx) {
5161
- if (ctx.state.apiKey === null) return "todo";
5898
+ if (ctx.secrets.apiKey === null) return "todo";
5162
5899
  const mb = mbTool(deps(ctx).runner);
5163
5900
  const profile = ctx.state.mbProfile;
5164
5901
  try {
@@ -5172,9 +5909,9 @@ function apiKeyStep(deps = hostDeps$7) {
5172
5909
  async run(ctx) {
5173
5910
  const resolved = deps(ctx);
5174
5911
  const configured = requireConfiguredRuntime(ctx.state);
5175
- const apiKey = await persistedApiKey(ctx, resolved, configured.url);
5912
+ const apiKey = await workingApiKey(ctx, resolved, configured.url);
5176
5913
  const mb = mbTool(resolved.runner);
5177
- const profile = await chooseProfile(ctx, mb, configured.url);
5914
+ const profile = ctx.state.mbProfile;
5178
5915
  await mb.authLogin({
5179
5916
  url: configured.url,
5180
5917
  profile,
@@ -5574,14 +6311,14 @@ function isMbUnavailable(error) {
5574
6311
  }
5575
6312
  async function runGitSync(ctx, deps) {
5576
6313
  const configured = requireConfiguredRuntime(ctx.state);
5577
- if (ctx.state.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6314
+ if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
5578
6315
  const run = {
5579
6316
  ctx,
5580
6317
  deps,
5581
6318
  git: gitTool(deps.runner),
5582
6319
  client: deps.client(configured.url, {
5583
6320
  kind: "api-key",
5584
- key: ctx.state.apiKey
6321
+ key: ctx.secrets.apiKey
5585
6322
  })
5586
6323
  };
5587
6324
  const flow = await runFlow({
@@ -5637,8 +6374,8 @@ function validateToken(value) {
5637
6374
  return "A license token is 64 hexadecimal characters";
5638
6375
  }
5639
6376
  function requireApiKey$1(ctx) {
5640
- if (ctx.state.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
5641
- return ctx.state.apiKey;
6377
+ if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6378
+ return ctx.secrets.apiKey;
5642
6379
  }
5643
6380
  function activeLabel(status) {
5644
6381
  const plan = status["plan-alias"];
@@ -5651,15 +6388,15 @@ function licenseStep(deps = hostDeps$5) {
5651
6388
  async detect(ctx) {
5652
6389
  if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
5653
6390
  const configured = configuredRuntime(ctx.state);
5654
- if (configured === null || ctx.state.apiKey === null) return "todo";
6391
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
5655
6392
  const client = deps(ctx).client(configured.url, {
5656
6393
  kind: "api-key",
5657
- key: ctx.state.apiKey
6394
+ key: ctx.secrets.apiKey
5658
6395
  });
5659
6396
  try {
5660
6397
  return (await client.tokenStatus.get()).valid ? "done" : "todo";
5661
6398
  } catch (error) {
5662
- if (isNoTokenYet(error)) return "todo";
6399
+ if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
5663
6400
  throw error;
5664
6401
  }
5665
6402
  },
@@ -5894,9 +6631,9 @@ function isNotServingYet(error) {
5894
6631
  function isNotFound$1(error) {
5895
6632
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
5896
6633
  }
5897
- function requireApiKey(state) {
5898
- if (state.apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
5899
- return state.apiKey;
6634
+ function requireApiKey(ctx) {
6635
+ if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
6636
+ return ctx.secrets.apiKey;
5900
6637
  }
5901
6638
  async function recordedDatabase(record, client) {
5902
6639
  if (record.databaseId === null) return null;
@@ -6018,23 +6755,23 @@ function sampleDatabaseStep(deps = hostDeps$3) {
6018
6755
  if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
6019
6756
  const record = ctx.state.sampleDatabase;
6020
6757
  const configured = configuredRuntime(ctx.state);
6021
- if (record === null || configured === null || ctx.state.apiKey === null) return "todo";
6758
+ if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
6022
6759
  if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
6023
6760
  const client = resolved.client(configured.url, {
6024
6761
  kind: "api-key",
6025
- key: ctx.state.apiKey
6762
+ key: ctx.secrets.apiKey
6026
6763
  });
6027
6764
  try {
6028
6765
  return await recordedDatabase(record, client) === null ? "todo" : "done";
6029
6766
  } catch (error) {
6030
- if (isNotServingYet(error)) return "todo";
6767
+ if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
6031
6768
  throw error;
6032
6769
  }
6033
6770
  },
6034
6771
  async run(ctx) {
6035
6772
  const resolved = deps(ctx);
6036
6773
  const configured = requireConfiguredRuntime(ctx.state);
6037
- const apiKey = requireApiKey(ctx.state);
6774
+ const apiKey = requireApiKey(ctx);
6038
6775
  const docker = await resolved.runtime.docker.available();
6039
6776
  if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
6040
6777
  const choice = await persistedRecord(ctx, resolved);
@@ -6149,23 +6886,24 @@ function hostDeps$1(ctx) {
6149
6886
  function isNotServing$1(error) {
6150
6887
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6151
6888
  }
6152
- function loginLine(admin) {
6153
- if (admin === null) return null;
6154
- switch (admin.kind) {
6155
- case "default": return `Login: ${admin.email} / ${admin.password}`;
6156
- case "prompted": return `Login: ${admin.email} with the password you entered`;
6889
+ function loginLine(kind, email) {
6890
+ switch (kind) {
6891
+ case "generated":
6892
+ case "legacy-default": return `Login: ${email} (run rde credentials in your terminal for the password)`;
6893
+ case "prompted": return `Login: ${email} with the password you entered`;
6157
6894
  }
6158
6895
  }
6159
- async function databaseLines$1(state, deps) {
6160
- if (state.url === null || state.apiKey === null) return ["Databases: none connected yet"];
6896
+ async function databaseLines$1(state, apiKey, deps) {
6897
+ if (state.url === null || apiKey === null) return ["Databases: none connected yet"];
6161
6898
  let databases;
6162
6899
  try {
6163
6900
  databases = (await deps.client(state.url, {
6164
6901
  kind: "api-key",
6165
- key: state.apiKey
6902
+ key: apiKey
6166
6903
  }).database.list()).data;
6167
6904
  } catch (error) {
6168
6905
  if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
6906
+ if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
6169
6907
  throw error;
6170
6908
  }
6171
6909
  if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
@@ -6199,14 +6937,13 @@ function launchLines(agents, workdir) {
6199
6937
  const prefix = workdir === null ? "" : `cd ${workdir} && `;
6200
6938
  return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
6201
6939
  }
6202
- async function summaryLines(state, deps) {
6940
+ async function summaryLines(state, apiKey, deps) {
6203
6941
  const profile = state.mbProfile;
6204
6942
  const lines = [];
6205
6943
  if (state.url !== null) lines.push(`Metabase: ${state.url}`);
6206
- const login = loginLine(state.admin);
6207
- if (login !== null) lines.push(login);
6944
+ if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
6208
6945
  lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
6209
- lines.push(...await databaseLines$1(state, deps));
6946
+ lines.push(...await databaseLines$1(state, apiKey, deps));
6210
6947
  if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
6211
6948
  lines.push(...gitLines(state));
6212
6949
  const installed = await skillsCli(deps.runner).list();
@@ -6223,7 +6960,8 @@ function summaryStep(deps = hostDeps$1) {
6223
6960
  return "todo";
6224
6961
  },
6225
6962
  async run(ctx) {
6226
- ctx.ui.note((await summaryLines(ctx.state, deps(ctx))).join("\n"), "Your environment");
6963
+ const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
6964
+ ctx.ui.note(lines.join("\n"), "Your environment");
6227
6965
  return "completed";
6228
6966
  }
6229
6967
  };
@@ -6257,7 +6995,7 @@ function initSteps(options) {
6257
6995
  //#region src/commands/doctor.ts
6258
6996
  function print(report, json) {
6259
6997
  if (json) {
6260
- printJson(report);
6998
+ printJson(doctorJson(report));
6261
6999
  return;
6262
7000
  }
6263
7001
  printLines(doctorLines(report));
@@ -6288,12 +7026,12 @@ const doctor = defineRdeCommand({
6288
7026
  const name = await instanceNameOrDefault();
6289
7027
  const state = await readInstanceState(name);
6290
7028
  const deps = hostDoctorDeps(name, interruptSignal);
6291
- let report = await collectDoctor(name, state, deps);
7029
+ let report = await collectDoctor(name, state, state === null ? null : await readInstanceSecrets(name), deps);
6292
7030
  if (args.fix && report.failed > 0) {
6293
7031
  if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
6294
7032
  print(report, args.json);
6295
7033
  const ctx = await createWizardContext(name, { signal: interruptSignal });
6296
- if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, deps);
7034
+ if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
6297
7035
  }
6298
7036
  print(report, args.json);
6299
7037
  if (report.failed > 0) throw new Error(doctorSummary(report));
@@ -6318,7 +7056,7 @@ const EnvString = z.string().transform((value, ctx) => {
6318
7056
  }
6319
7057
  return resolved;
6320
7058
  });
6321
- const AdminVariant = z.enum(["default", "prompted"]);
7059
+ const AdminVariant = z.enum(["generated", "prompted"]);
6322
7060
  const RemoteKind = z.enum(["local-bare", "hosted"]);
6323
7061
  const FieldValue = z.union([
6324
7062
  EnvString,
@@ -6347,7 +7085,6 @@ const SECTIONS = {
6347
7085
  "first-name": z.string().optional(),
6348
7086
  "last-name": z.string().optional()
6349
7087
  }).optional(),
6350
- "api-key": z.strictObject({ profile: MbProfileChoice }).optional(),
6351
7088
  license: z.strictObject({ token: EnvString.nullable() }).optional(),
6352
7089
  databases: z.array(DatabaseEntry).optional(),
6353
7090
  "git-sync": z.strictObject({
@@ -6443,8 +7180,6 @@ function keyedAnswers(answers) {
6443
7180
  const map = /* @__PURE__ */ new Map();
6444
7181
  const admin = answers["admin-user"];
6445
7182
  if (admin !== void 0) addAdmin(map, admin);
6446
- const apiKey = answers["api-key"];
6447
- if (apiKey !== void 0) add(map, API_KEY_KEYS.profile, select$1(apiKey.profile));
6448
7183
  const license = answers.license;
6449
7184
  if (license !== void 0) add(map, LICENSE_KEYS.token, password$1(license.token ?? SKIP_LICENSE));
6450
7185
  const databases = answers.databases;
@@ -6500,7 +7235,6 @@ const FLAG_FOR_KEY = {
6500
7235
  [ADMIN_USER_KEYS.password]: "--admin-password",
6501
7236
  [API_KEY_KEYS.email]: "--admin-email",
6502
7237
  [API_KEY_KEYS.password]: "--admin-password",
6503
- [API_KEY_KEYS.profile]: "--mb-profile",
6504
7238
  [LICENSE_KEYS.token]: "--license-token or --skip-license",
6505
7239
  [DATABASE_KEYS.engine]: "--database or --skip-databases",
6506
7240
  [DATABASE_KEYS.uri]: "--database",
@@ -6554,7 +7288,7 @@ function parseDatabase(spec) {
6554
7288
  function adminSection(flags, secrets) {
6555
7289
  const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
6556
7290
  if (flags.admin === void 0 && !prompted) return;
6557
- if (flags.admin === "default" && prompted) throw conflict("--admin default", "--admin-email/--admin-password");
7291
+ if (flags.admin === "generated" && prompted) throw conflict("--admin generated", "--admin-email/--admin-password");
6558
7292
  return {
6559
7293
  variant: flags.admin ?? "prompted",
6560
7294
  ...flags.adminEmail !== void 0 && { email: flags.adminEmail },
@@ -6657,8 +7391,7 @@ async function answersFromFlags(flags, readStdin) {
6657
7391
  license: licenseSection(flags, secrets),
6658
7392
  databases: databasesSection(flags),
6659
7393
  "git-sync": gitSection(flags, secrets),
6660
- "agents-skills": agentsSection(flags),
6661
- "api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
7394
+ "agents-skills": agentsSection(flags)
6662
7395
  };
6663
7396
  const present = Object.entries(sections).filter(([, section]) => section !== void 0);
6664
7397
  return parseInitAnswers(Object.fromEntries(present), "the init flags");
@@ -6752,10 +7485,7 @@ function applyRecord(draft, record) {
6752
7485
  if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
6753
7486
  else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
6754
7487
  else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
6755
- else if (key === API_KEY_KEYS.profile && value !== null) {
6756
- const profile = MbProfileChoice.safeParse(value);
6757
- if (profile.success) draft.answers["api-key"] = { profile: profile.data };
6758
- } else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
7488
+ else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
6759
7489
  }
6760
7490
  function writableUri(uri, index) {
6761
7491
  return URI_WITH_PASSWORD.test(uri) ? envReference(databaseEnv(index, "uri")) : uri;
@@ -7027,7 +7757,7 @@ const init = defineRdeCommand({
7027
7757
  },
7028
7758
  admin: {
7029
7759
  type: "string",
7030
- description: "Admin credentials: default (admin@metabase.com / metabase123) or prompted"
7760
+ description: "Admin credentials: generated (a random password; `rde credentials` shows it) or prompted"
7031
7761
  },
7032
7762
  "admin-email": {
7033
7763
  type: "string",
@@ -7085,10 +7815,6 @@ const init = defineRdeCommand({
7085
7815
  description: "Install the skill for no agent",
7086
7816
  default: false
7087
7817
  },
7088
- "mb-profile": {
7089
- type: "string",
7090
- description: "mb profile for this instance when `default` already points elsewhere: default or rde"
7091
- },
7092
7818
  yes: {
7093
7819
  type: "boolean",
7094
7820
  description: "Answer yes to every confirmation (skipping databases)",
@@ -7123,8 +7849,7 @@ const init = defineRdeCommand({
7123
7849
  gitToken: args["git-token"],
7124
7850
  skipGit: args["skip-git"],
7125
7851
  agents: args.agents,
7126
- skipAgents: args["skip-agents"],
7127
- mbProfile: args["mb-profile"]
7852
+ skipAgents: args["skip-agents"]
7128
7853
  }
7129
7854
  };
7130
7855
  const printTo = args["print-answers"];
@@ -7243,11 +7968,11 @@ function isNotServing(error) {
7243
7968
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7244
7969
  }
7245
7970
  async function databasesOf(ctx, deps, health) {
7246
- if (health !== "healthy" || ctx.state.apiKey === null) return null;
7971
+ if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
7247
7972
  try {
7248
- return await listDatabases(ctx.state, { client: deps.client });
7973
+ return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
7249
7974
  } catch (error) {
7250
- if (isNotServing(error)) return null;
7975
+ if (isNotServing(error) || isRejectedCredential(error)) return null;
7251
7976
  throw error;
7252
7977
  }
7253
7978
  }
@@ -7290,9 +8015,13 @@ async function collectStatus(ctx, deps) {
7290
8015
  const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
7291
8016
  const health = configured === null ? null : await healthOf(configured.url, deps);
7292
8017
  const databases = health === null ? null : await databasesOf(ctx, deps, health);
8018
+ const admin = ctx.state.admin;
7293
8019
  return {
7294
8020
  instance: ctx.state.name,
7295
8021
  url: configured?.url ?? null,
8022
+ mbProfile: ctx.state.mbProfile,
8023
+ adminEmail: admin === null ? null : admin.email,
8024
+ adminVariant: admin === null ? null : admin.kind,
7296
8025
  runtime: configured === null || runtime === null ? null : {
7297
8026
  kind: configured.runtime.kind,
7298
8027
  installed: runtime.installed,
@@ -7346,6 +8075,8 @@ function databaseLines(report) {
7346
8075
  function statusLines(report) {
7347
8076
  return [
7348
8077
  report.url === null ? `Instance ${report.instance}` : `Instance ${report.instance} — ${report.url}`,
8078
+ ...report.adminVariant === null || report.adminEmail === null ? [] : [loginLine(report.adminVariant, report.adminEmail)],
8079
+ `Metabase CLI: ${mbCommand(report.mbProfile)} <command>`,
7349
8080
  runtimeLine(report),
7350
8081
  versionLine(report),
7351
8082
  `Health: ${report.health ?? "unknown"}`,
@@ -7648,7 +8379,7 @@ async function planUninstall(options, deps) {
7648
8379
  if (jar.removal !== null) removals.push(jar.removal);
7649
8380
  if (jar.kept !== null) kept.push(jar.kept);
7650
8381
  }
7651
- const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, "rde"], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
8382
+ const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, mbProfileFor(options.instance)], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
7652
8383
  removals.push(...profiles);
7653
8384
  if (options.removeSkill) {
7654
8385
  const skill = await findSkill(deps.runner);
@@ -7657,6 +8388,11 @@ async function planUninstall(options, deps) {
7657
8388
  const dir = instanceDir(options.instance);
7658
8389
  const dirPresent = await pathExists(dir);
7659
8390
  if (!options.keepData) {
8391
+ const secrets = instanceSecretsPath(options.instance);
8392
+ if (await pathExists(secrets)) removals.push({
8393
+ label: `Stored credentials ${secrets} (the admin password and the API key)`,
8394
+ remove: () => promises.rm(secrets, { force: true })
8395
+ });
7660
8396
  if (dirPresent) removals.push({
7661
8397
  label: instanceDirLabel(dir, state),
7662
8398
  remove: () => promises.rm(dir, {
@@ -8058,7 +8794,7 @@ const upgradeCommand = defineRdeCommand({
8058
8794
  const main = defineCommand({
8059
8795
  meta: {
8060
8796
  name: "rde",
8061
- version: "0.1.2",
8797
+ version: "0.1.3-alpha.rde-fixes.59b956e",
8062
8798
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
8063
8799
  },
8064
8800
  subCommands: {
@@ -8066,6 +8802,7 @@ const main = defineCommand({
8066
8802
  start,
8067
8803
  stop,
8068
8804
  status,
8805
+ credentials,
8069
8806
  logs,
8070
8807
  open,
8071
8808
  db,