@metabase/rde 0.1.3 → 0.1.4

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 +83 -42
  2. package/dist/cli.mjs +1452 -487
  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,14 @@ 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 printJson(value) {
131
+ process.stdout.write(JSON.stringify(value, null, 2) + "\n");
132
+ }
120
133
  const StepId = z.enum([
121
134
  "preflight",
122
135
  "install-mb",
@@ -157,14 +170,36 @@ const JarRuntimeState = z.object({
157
170
  });
158
171
  const RuntimeState = z.discriminatedUnion("kind", [DockerRuntimeState, JarRuntimeState]);
159
172
  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"),
173
+ const AdminCredentials = z.discriminatedUnion("kind", [
174
+ z.object({
175
+ kind: z.literal("generated"),
176
+ email: z.string()
177
+ }),
178
+ z.object({
179
+ kind: z.literal("prompted"),
180
+ email: z.string()
181
+ }),
182
+ z.object({
183
+ kind: z.literal("legacy-default"),
184
+ email: z.string()
185
+ })
186
+ ]);
187
+ const AdminLogin = z.object({
165
188
  email: z.string(),
166
189
  password: z.string()
167
- })]);
190
+ });
191
+ const InstanceSecrets = z.object({
192
+ schemaVersion: z.literal(1),
193
+ admin: AdminLogin.nullable(),
194
+ apiKey: z.string().nullable()
195
+ });
196
+ function emptyInstanceSecrets() {
197
+ return {
198
+ schemaVersion: 1,
199
+ admin: null,
200
+ apiKey: null
201
+ };
202
+ }
168
203
  const LicenseState = z.discriminatedUnion("activated", [z.object({ activated: z.literal(false) }), z.object({
169
204
  activated: z.literal(true),
170
205
  features: z.array(z.string()),
@@ -202,13 +237,12 @@ const StepRecord = z.object({
202
237
  completedAt: z.string()
203
238
  });
204
239
  const InstanceState = z.object({
205
- schemaVersion: z.literal(3),
240
+ schemaVersion: z.literal(4),
206
241
  name: z.string(),
207
242
  runtime: RuntimeState.nullable(),
208
243
  url: z.string().nullable(),
209
244
  version: z.string().nullable(),
210
245
  admin: AdminCredentials.nullable(),
211
- apiKey: z.string().nullable(),
212
246
  mbProfile: z.string(),
213
247
  license: LicenseState,
214
248
  databases: z.array(DatabaseRecord),
@@ -230,20 +264,26 @@ function requireConfiguredRuntime(state) {
230
264
  }
231
265
  const DEFAULT_INSTANCE_NAME = "default";
232
266
  const DEFAULT_MB_PROFILE = "default";
233
- const MbProfileChoice = z.enum([DEFAULT_MB_PROFILE, "rde"]);
267
+ const RDE_MB_PROFILE = "rde";
268
+ const MbProfileChoice = z.enum([DEFAULT_MB_PROFILE, RDE_MB_PROFILE]);
269
+ function namedMbProfile(instance) {
270
+ return instance === "default" ? RDE_MB_PROFILE : `${RDE_MB_PROFILE}-${instance}`;
271
+ }
272
+ function mbProfileFor(instance) {
273
+ return instance === "default" ? DEFAULT_MB_PROFILE : namedMbProfile(instance);
274
+ }
234
275
  function mbCommand(profile) {
235
276
  return profile === "default" ? "mb" : `mb --profile ${profile}`;
236
277
  }
237
278
  function emptyInstanceState(name) {
238
279
  return {
239
- schemaVersion: 3,
280
+ schemaVersion: 4,
240
281
  name,
241
282
  runtime: null,
242
283
  url: null,
243
284
  version: null,
244
285
  admin: null,
245
- apiKey: null,
246
- mbProfile: DEFAULT_MB_PROFILE,
286
+ mbProfile: mbProfileFor(name),
247
287
  license: { activated: false },
248
288
  databases: [],
249
289
  sampleDatabase: null,
@@ -256,8 +296,22 @@ function emptyInstanceState(name) {
256
296
  const Versioned = z.object({ schemaVersion: z.int() });
257
297
  const V1 = 1;
258
298
  const V2 = 2;
299
+ const V3 = 3;
259
300
  const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
260
301
  const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
302
+ const AdminV3 = z.discriminatedUnion("kind", [z.object({
303
+ kind: z.literal("prompted"),
304
+ email: z.string()
305
+ }), z.object({
306
+ kind: z.literal("default"),
307
+ email: z.string(),
308
+ password: z.string()
309
+ })]);
310
+ const StateV3 = z.object({
311
+ schemaVersion: z.literal(V3),
312
+ admin: AdminV3.nullable(),
313
+ apiKey: z.string().nullable()
314
+ }).loose();
261
315
  function liftV1(raw) {
262
316
  return {
263
317
  ...StateV1.parse(raw),
@@ -268,27 +322,67 @@ function liftV1(raw) {
268
322
  function liftV2(raw) {
269
323
  return {
270
324
  ...StateV2.parse(raw),
271
- schemaVersion: 3,
325
+ schemaVersion: V3,
272
326
  sampleDatabase: null
273
327
  };
274
328
  }
329
+ function adminV4(admin) {
330
+ switch (admin.kind) {
331
+ case "default": return {
332
+ kind: "legacy-default",
333
+ email: admin.email
334
+ };
335
+ case "prompted": return {
336
+ kind: "prompted",
337
+ email: admin.email
338
+ };
339
+ }
340
+ }
341
+ function liftV3(raw) {
342
+ const { admin, apiKey, ...rest } = StateV3.parse(raw);
343
+ const login = admin?.kind === "default" ? {
344
+ email: admin.email,
345
+ password: admin.password
346
+ } : null;
347
+ return {
348
+ state: InstanceState.parse({
349
+ ...rest,
350
+ schemaVersion: 4,
351
+ admin: admin === null ? null : adminV4(admin)
352
+ }),
353
+ movedSecrets: {
354
+ schemaVersion: 1,
355
+ admin: login,
356
+ apiKey
357
+ }
358
+ };
359
+ }
275
360
  function migrateInstanceState(raw) {
276
361
  const { schemaVersion } = Versioned.parse(raw);
277
362
  switch (schemaVersion) {
278
363
  case V1: return migrateInstanceState(liftV1(raw));
279
364
  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)`);
365
+ case V3: return liftV3(raw);
366
+ case 4: return {
367
+ state: InstanceState.parse(raw),
368
+ movedSecrets: null
369
+ };
370
+ default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 4)`);
282
371
  }
283
372
  }
284
373
  //#endregion
285
374
  //#region src/env.ts
286
375
  const ENV_RDE_HOME = "RDE_HOME";
376
+ const ENV_RDE_SKILL_SOURCE = "RDE_SKILL_SOURCE";
287
377
  function readEnv(name) {
288
378
  return process.env[name];
289
379
  }
290
380
  const ENV_PATH = "PATH";
381
+ const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
291
382
  const ENV_JAVA_HOME = "JAVA_HOME";
383
+ const ENV_MB_PROFILE = "MB_PROFILE";
384
+ const ENV_MB_URL = "MB_URL";
385
+ const ENV_MB_API_KEY = "MB_API_KEY";
292
386
  function environmentWith(overrides) {
293
387
  return {
294
388
  ...process.env,
@@ -302,6 +396,7 @@ const INSTANCES_DIR = "instances";
302
396
  const DATA_DIR = "data";
303
397
  const BACKUPS_DIR = "backups";
304
398
  const STATE_FILE = "state.json";
399
+ const SECRETS_FILE = "secrets.json";
305
400
  const GLOBAL_CONFIG_FILE = "config.json";
306
401
  const JARS_DIR = "jars";
307
402
  const JAR_FILE$1 = "metabase.jar";
@@ -322,8 +417,17 @@ function instanceDataDir(name) {
322
417
  function instanceBackupsDir(name) {
323
418
  return join(instanceDir(name), BACKUPS_DIR);
324
419
  }
420
+ function stateFileIn(dir) {
421
+ return join(dir, STATE_FILE);
422
+ }
423
+ function secretsFileIn(dir) {
424
+ return join(dir, SECRETS_FILE);
425
+ }
325
426
  function instanceStatePath(name) {
326
- return join(instanceDir(name), STATE_FILE);
427
+ return stateFileIn(instanceDir(name));
428
+ }
429
+ function instanceSecretsPath(name) {
430
+ return secretsFileIn(instanceDir(name));
327
431
  }
328
432
  function globalConfigPath() {
329
433
  return join(rdeHome(), GLOBAL_CONFIG_FILE);
@@ -337,21 +441,21 @@ function jarReleaseDir(version) {
337
441
  function releaseJarIn(jarsDirectory, version) {
338
442
  return join(jarsDirectory, version, JAR_FILE$1);
339
443
  }
340
- //#endregion
341
- //#region src/state/store.ts
342
- const FILE_MODE$1 = 384;
343
- const DIR_MODE = 448;
444
+ const PERMISSION_BITS = 511;
344
445
  function isFileNotFound$1(error) {
345
446
  return error instanceof Error && "code" in error && error.code === "ENOENT";
346
447
  }
347
- async function readJsonFile(path) {
348
- let raw;
448
+ async function readTextFile(path) {
349
449
  try {
350
- raw = await promises.readFile(path, "utf8");
450
+ return await promises.readFile(path, "utf8");
351
451
  } catch (error) {
352
452
  if (isFileNotFound$1(error)) return null;
353
453
  throw error;
354
454
  }
455
+ }
456
+ async function readJsonFile(path) {
457
+ const raw = await readTextFile(path);
458
+ if (raw === null) return null;
355
459
  try {
356
460
  return JSON.parse(raw);
357
461
  } catch (error) {
@@ -370,11 +474,11 @@ function parseAt(path, parse) {
370
474
  async function writeJsonFile(path, value) {
371
475
  await promises.mkdir(dirname(path), {
372
476
  recursive: true,
373
- mode: DIR_MODE
477
+ mode: 448
374
478
  });
375
479
  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);
480
+ await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
481
+ await promises.chmod(tmpPath, 384);
378
482
  try {
379
483
  await promises.rename(tmpPath, path);
380
484
  } catch (error) {
@@ -386,7 +490,12 @@ async function readInstanceState(name) {
386
490
  const path = instanceStatePath(name);
387
491
  const raw = await readJsonFile(path);
388
492
  if (raw === null) return null;
389
- return parseAt(path, () => migrateInstanceState(raw));
493
+ const migrated = parseAt(path, () => migrateInstanceState(raw));
494
+ if (migrated.movedSecrets !== null) {
495
+ await writeInstanceSecrets(name, migrated.movedSecrets);
496
+ await writeInstanceState(migrated.state);
497
+ }
498
+ return migrated.state;
390
499
  }
391
500
  async function writeInstanceState(state) {
392
501
  await writeJsonFile(instanceStatePath(state.name), state);
@@ -396,6 +505,35 @@ async function requireInstanceState(name) {
396
505
  if (state === null) throw new ConfigError(`no instance named ${name}; run \`rde init\` first`);
397
506
  return state;
398
507
  }
508
+ async function readInstanceSecrets(name) {
509
+ const path = instanceSecretsPath(name);
510
+ const raw = await readJsonFile(path);
511
+ if (raw === null) return null;
512
+ return parseAt(path, () => InstanceSecrets.parse(raw));
513
+ }
514
+ async function writeInstanceSecrets(name, secrets) {
515
+ await writeJsonFile(instanceSecretsPath(name), secrets);
516
+ await promises.chmod(instanceDir(name), 448);
517
+ }
518
+ async function modeOf(path) {
519
+ try {
520
+ return (await promises.stat(path)).mode & PERMISSION_BITS;
521
+ } catch (error) {
522
+ if (isFileNotFound$1(error)) return null;
523
+ throw error;
524
+ }
525
+ }
526
+ const StateWithApiKey = z.object({ apiKey: z.string() }).loose();
527
+ const StateWithPassword = z.object({ admin: z.object({ password: z.string() }).loose() }).loose();
528
+ async function inspectCredentialFiles(dir) {
529
+ const raw = await readJsonFile(stateFileIn(dir));
530
+ const secretsInState = [...StateWithApiKey.safeParse(raw).success ? ["apiKey"] : [], ...StateWithPassword.safeParse(raw).success ? ["admin.password"] : []];
531
+ return {
532
+ dirMode: await modeOf(dir),
533
+ secretsMode: await modeOf(secretsFileIn(dir)),
534
+ secretsInState
535
+ };
536
+ }
399
537
  async function readGlobalConfig() {
400
538
  const path = globalConfigPath();
401
539
  const raw = await readJsonFile(path);
@@ -406,6 +544,120 @@ async function writeGlobalConfig(config) {
406
544
  await writeJsonFile(globalConfigPath(), config);
407
545
  }
408
546
  //#endregion
547
+ //#region src/steps/credentials.ts
548
+ const LEGACY_NOTE = "This is the public default password; when the instance holds nothing you need, `rde uninstall` then `rde init` recreates it with a generated one";
549
+ const PROMPTED_NOTE = "The password is the one entered during `rde init`; rde never stored it";
550
+ function storedPassword(state, secrets) {
551
+ 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`);
552
+ return secrets.admin.password;
553
+ }
554
+ function credentialsReport(state, secrets) {
555
+ const admin = state.admin;
556
+ if (admin === null) throw new ConfigError(`instance ${state.name} has no admin user yet; run \`rde init\``);
557
+ const login = {
558
+ instance: state.name,
559
+ url: state.url,
560
+ email: admin.email
561
+ };
562
+ switch (admin.kind) {
563
+ case "generated": return {
564
+ ...login,
565
+ password: storedPassword(state, secrets),
566
+ note: null
567
+ };
568
+ case "legacy-default": return {
569
+ ...login,
570
+ password: storedPassword(state, secrets),
571
+ note: LEGACY_NOTE
572
+ };
573
+ case "prompted": return {
574
+ ...login,
575
+ password: null,
576
+ note: PROMPTED_NOTE
577
+ };
578
+ }
579
+ }
580
+ function credentialsLines(report) {
581
+ const url = report.url === null ? [] : [`Metabase: ${report.url}`];
582
+ const password = report.password === null ? [] : [`Password: ${report.password}`];
583
+ const note = report.note === null ? [] : [report.note];
584
+ return [
585
+ ...url,
586
+ `Email: ${report.email}`,
587
+ ...password,
588
+ ...note
589
+ ];
590
+ }
591
+ //#endregion
592
+ //#region src/commands/define.ts
593
+ function repeatedValues(rawArgs, flag) {
594
+ const values = [];
595
+ const inline = `${flag}=`;
596
+ for (let index = 0; index < rawArgs.length; index += 1) {
597
+ const arg = rawArgs[index];
598
+ if (arg === void 0) continue;
599
+ if (arg.startsWith(inline)) values.push(arg.slice(inline.length));
600
+ else if (arg === flag) {
601
+ const next = rawArgs[index + 1];
602
+ if (next !== void 0) {
603
+ values.push(next);
604
+ index += 1;
605
+ }
606
+ }
607
+ }
608
+ return values;
609
+ }
610
+ function defineRdeCommand(def) {
611
+ return defineCommand({
612
+ meta: def.meta,
613
+ ...def.args !== void 0 && { args: def.args },
614
+ async run({ args, rawArgs }) {
615
+ try {
616
+ await def.run(args, rawArgs);
617
+ } catch (error) {
618
+ reportError(error);
619
+ }
620
+ }
621
+ });
622
+ }
623
+ //#endregion
624
+ //#region src/commands/instance.ts
625
+ async function defaultInstanceName() {
626
+ const config = await readGlobalConfig();
627
+ if (config === null) throw new ConfigError("no Metabase instance yet; run `rde init` first");
628
+ return config.defaultInstance;
629
+ }
630
+ async function instanceNameOrDefault() {
631
+ try {
632
+ return (await readGlobalConfig())?.defaultInstance ?? "default";
633
+ } catch (error) {
634
+ if (error instanceof ConfigError) return DEFAULT_INSTANCE_NAME;
635
+ throw error;
636
+ }
637
+ }
638
+ //#endregion
639
+ //#region src/commands/credentials.ts
640
+ const credentials = defineRdeCommand({
641
+ meta: {
642
+ name: "credentials",
643
+ description: "Print the admin login of the instance: URL, email, and password"
644
+ },
645
+ args: { json: {
646
+ type: "boolean",
647
+ description: "Print the login as JSON",
648
+ default: false
649
+ } },
650
+ async run(args) {
651
+ const name = await defaultInstanceName();
652
+ const report = credentialsReport(await requireInstanceState(name), await readInstanceSecrets(name));
653
+ if (args.json) {
654
+ printJson(report);
655
+ return;
656
+ }
657
+ printLines(credentialsLines(report));
658
+ }
659
+ });
660
+ //#endregion
409
661
  //#region src/connections/parse-uri.ts
410
662
  var UriParseError = class extends Error {
411
663
  engine;
@@ -1072,6 +1324,18 @@ function tokenStatusResource(http) {
1072
1324
  return { get };
1073
1325
  }
1074
1326
  //#endregion
1327
+ //#region src/metabase/domain/user.ts
1328
+ const CurrentUser = z.object({ id: z.int() }).loose();
1329
+ //#endregion
1330
+ //#region src/metabase/resources/user.ts
1331
+ function userResource(http) {
1332
+ /** GET /api/user/current */
1333
+ async function current() {
1334
+ return http.requestJson("GET", "/api/user/current", { schema: CurrentUser });
1335
+ }
1336
+ return { current };
1337
+ }
1338
+ //#endregion
1075
1339
  //#region src/metabase/client.ts
1076
1340
  const HEALTH_POLL_INTERVAL_MS = 2e3;
1077
1341
  function createMetabaseClient(baseUrl, auth, signal, options = {}) {
@@ -1092,7 +1356,8 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
1092
1356
  setting: settingResource(http),
1093
1357
  tokenStatus: tokenStatusResource(http),
1094
1358
  database: databaseResource(http),
1095
- remoteSync: remoteSyncResource(http)
1359
+ remoteSync: remoteSyncResource(http),
1360
+ user: userResource(http)
1096
1361
  };
1097
1362
  }
1098
1363
  function isNotReadyYet(error) {
@@ -1497,6 +1762,9 @@ function settledOrBack(value) {
1497
1762
  if (isBack(value)) return BACK;
1498
1763
  return settled(value);
1499
1764
  }
1765
+ function preselected(opts) {
1766
+ return opts.initialValue ?? opts.recommended;
1767
+ }
1500
1768
  function withBackChoice(choices) {
1501
1769
  return [...choices, {
1502
1770
  value: BACK,
@@ -1550,10 +1818,11 @@ function clackPrompts() {
1550
1818
  },
1551
1819
  async select(opts) {
1552
1820
  requireTty(opts.message);
1821
+ const initialValue = preselected(opts);
1553
1822
  return settled(await select({
1554
1823
  message: opts.message,
1555
1824
  options: opts.choices,
1556
- ...opts.initialValue !== void 0 && { initialValue: opts.initialValue }
1825
+ ...initialValue !== void 0 && { initialValue }
1557
1826
  }));
1558
1827
  },
1559
1828
  async selectOrBack(opts) {
@@ -1776,6 +2045,8 @@ function databaseListingLines(listing) {
1776
2045
  //#endregion
1777
2046
  //#region src/steps/databases/flow.ts
1778
2047
  const DATABASE_KEYS = {
2048
+ connection: "databases.connection",
2049
+ detectedEngine: "databases.detected-engine",
1779
2050
  engine: "databases.engine",
1780
2051
  skip: "databases.skip",
1781
2052
  mode: "databases.mode",
@@ -1790,6 +2061,11 @@ const FINISH = "finish";
1790
2061
  const RETRY = "try-again";
1791
2062
  const BAD_REQUEST_STATUS$1 = 400;
1792
2063
  const OPTION_FIELDS = ["additional-options", "jdbc-flags"];
2064
+ const CONNECTION_PLACEHOLDER = "leave empty to skip";
2065
+ const START_SCREEN = {
2066
+ engine: "pick-engine",
2067
+ "connection-string": "connection-string"
2068
+ };
1793
2069
  const FRESH$1 = {
1794
2070
  engine: null,
1795
2071
  uri: null,
@@ -1830,6 +2106,66 @@ async function confirmSkip(run) {
1830
2106
  initialValue: false
1831
2107
  });
1832
2108
  }
2109
+ function isCreatableEntry(entry) {
2110
+ return entry !== null && entry.creatable;
2111
+ }
2112
+ async function pickDetectedEngine(run, candidates) {
2113
+ const picked = await run.deps.prompts.selectOrBack({
2114
+ key: DATABASE_KEYS.detectedEngine,
2115
+ message: "Which database is it?",
2116
+ choices: candidates.map((entry) => ({
2117
+ value: entry.key,
2118
+ label: entry.displayName
2119
+ }))
2120
+ });
2121
+ if (picked === BACK) return BACK;
2122
+ const entry = candidates.find((candidate) => candidate.key === picked);
2123
+ if (entry === void 0) throw new Error(`engine ${picked} was not offered`);
2124
+ return entry;
2125
+ }
2126
+ function connectionStringScreen(run) {
2127
+ return async (model) => {
2128
+ let draft = model.uri;
2129
+ for (;;) {
2130
+ const uri = (await run.deps.prompts.text({
2131
+ key: DATABASE_KEYS.connection,
2132
+ message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
2133
+ placeholder: CONNECTION_PLACEHOLDER,
2134
+ ...draft !== null && { initialValue: draft }
2135
+ })).trim();
2136
+ draft = uri;
2137
+ if (uri.length === 0) return {
2138
+ type: "done",
2139
+ model
2140
+ };
2141
+ const candidates = detectEngine(uri).map((key) => run.deps.catalog.entry(key)).filter(isCreatableEntry);
2142
+ const [first, ...others] = candidates;
2143
+ if (first === void 0) {
2144
+ run.deps.ui.warn("rde can't tell which database that connection string is for; pick it");
2145
+ return {
2146
+ type: "next",
2147
+ to: "pick-engine",
2148
+ model: FRESH$1
2149
+ };
2150
+ }
2151
+ const engine = others.length === 0 ? first : await pickDetectedEngine(run, candidates);
2152
+ if (engine === BACK) continue;
2153
+ const parsed = parseUri(uri, engine.key);
2154
+ if (parsed instanceof UriParseError) {
2155
+ run.deps.ui.warn(parsed.message);
2156
+ return {
2157
+ type: "next",
2158
+ to: "pick-engine",
2159
+ model: FRESH$1
2160
+ };
2161
+ }
2162
+ return toReview(run, model, {
2163
+ engine,
2164
+ parsed
2165
+ });
2166
+ }
2167
+ };
2168
+ }
1833
2169
  function pickEngineScreen(run) {
1834
2170
  return async (model) => {
1835
2171
  const total = run.deps.existingCount + run.created.length;
@@ -2191,19 +2527,20 @@ async function runDatabaseFlow(deps) {
2191
2527
  created: []
2192
2528
  };
2193
2529
  await runFlow({
2530
+ "connection-string": connectionStringScreen(run),
2194
2531
  "pick-engine": pickEngineScreen(run),
2195
2532
  "paste-uri": pasteUriScreen(run),
2196
2533
  review: reviewScreen(run),
2197
2534
  test: testScreen(run),
2198
2535
  create: createScreen(run),
2199
2536
  another: anotherScreen(run)
2200
- }, "pick-engine", FRESH$1);
2537
+ }, START_SCREEN[deps.start], FRESH$1);
2201
2538
  return { created: run.created };
2202
2539
  }
2203
2540
  //#endregion
2204
2541
  //#region src/steps/databases.ts
2205
2542
  const STEP_ID$9 = "databases";
2206
- const ASK_DATABASES = { requested: false };
2543
+ const SAMPLE_STANDS_IN = { request: "sample-stands-in" };
2207
2544
  function hostDatabasesDeps(signal) {
2208
2545
  return {
2209
2546
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
@@ -2214,9 +2551,9 @@ function hostDatabasesDeps(signal) {
2214
2551
  function isNotServingYet$1(error) {
2215
2552
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
2216
2553
  }
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;
2554
+ function requireApiKey$2(state, apiKey) {
2555
+ if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
2556
+ return apiKey;
2220
2557
  }
2221
2558
  function hostAliasOf(runtime) {
2222
2559
  switch (runtime.kind) {
@@ -2232,15 +2569,15 @@ function toRecord$1(database, createdAt) {
2232
2569
  createdAt
2233
2570
  };
2234
2571
  }
2235
- function authenticatedClient(state, deps) {
2572
+ function authenticatedClient(state, apiKey, deps) {
2236
2573
  const configured = requireConfiguredRuntime(state);
2237
2574
  return deps.client(configured.url, {
2238
2575
  kind: "api-key",
2239
- key: requireApiKey$2(state)
2576
+ key: requireApiKey$2(state, apiKey)
2240
2577
  });
2241
2578
  }
2242
- async function listDatabases(state, deps) {
2243
- const listed = await authenticatedClient(state, deps).database.list();
2579
+ async function listDatabases(state, apiKey, deps) {
2580
+ const listed = await authenticatedClient(state, apiKey, deps).database.list();
2244
2581
  const records = new Map(state.databases.map((record) => [record.id, record]));
2245
2582
  const onServer = listed.data.map((database) => ({
2246
2583
  id: database.id,
@@ -2259,13 +2596,14 @@ async function listDatabases(state, deps) {
2259
2596
  }));
2260
2597
  return [...onServer, ...missing];
2261
2598
  }
2262
- async function addDatabases(ctx, deps) {
2599
+ async function connectDatabases(ctx, deps, start) {
2263
2600
  const configured = requireConfiguredRuntime(ctx.state);
2264
- const client = authenticatedClient(ctx.state, deps);
2601
+ const client = authenticatedClient(ctx.state, ctx.secrets.apiKey, deps);
2265
2602
  const properties = await client.session.properties();
2266
2603
  const existing = await client.database.list();
2267
2604
  if (existing.data.length > 0) ctx.ui.note(existingDatabaseLines(existing.data).join("\n"), "Connected databases");
2268
2605
  const result = await runDatabaseFlow({
2606
+ start,
2269
2607
  prompts: ctx.prompts,
2270
2608
  ui: ctx.ui,
2271
2609
  catalog: engineCatalog(properties.engines),
@@ -2281,7 +2619,17 @@ async function addDatabases(ctx, deps) {
2281
2619
  });
2282
2620
  }
2283
2621
  });
2284
- return existing.data.length + result.created.length > 0 ? "completed" : "skipped";
2622
+ return {
2623
+ existing: existing.data.length,
2624
+ created: result.created.length
2625
+ };
2626
+ }
2627
+ async function addDatabases(ctx, deps) {
2628
+ const connected = await connectDatabases(ctx, deps, "engine");
2629
+ return connected.existing + connected.created > 0 ? "completed" : "skipped";
2630
+ }
2631
+ async function askForOwnDatabase(ctx, deps) {
2632
+ return (await connectDatabases(ctx, deps, "connection-string")).created > 0 ? "completed" : "skipped";
2285
2633
  }
2286
2634
  function hostDeps$12(ctx) {
2287
2635
  return hostDatabasesDeps(ctx.signal);
@@ -2289,6 +2637,9 @@ function hostDeps$12(ctx) {
2289
2637
  function sampleDatabaseId(state) {
2290
2638
  return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
2291
2639
  }
2640
+ function skippedBefore(state) {
2641
+ return state.steps[STEP_ID$9]?.status === "skipped";
2642
+ }
2292
2643
  function ownDatabaseIds(state) {
2293
2644
  const sampleId = sampleDatabaseId(state);
2294
2645
  return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
@@ -2299,21 +2650,28 @@ function databasesStep(options, deps = hostDeps$12) {
2299
2650
  title: "Connect your databases",
2300
2651
  async detect(ctx) {
2301
2652
  const configured = configuredRuntime(ctx.state);
2302
- if (configured === null || ctx.state.apiKey === null) return "todo";
2653
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
2303
2654
  const own = ownDatabaseIds(ctx.state);
2304
- if (own.size === 0) return sampleDatabaseId(ctx.state) !== null && !options.requested ? "done" : "todo";
2655
+ const sampleId = sampleDatabaseId(ctx.state);
2656
+ const asking = options.request === "ask-once" && !skippedBefore(ctx.state);
2657
+ if (own.size === 0 && !asking) {
2658
+ if (options.request === "named") return "todo";
2659
+ return sampleId !== null || skippedBefore(ctx.state) ? "done" : "todo";
2660
+ }
2661
+ const isOwn = (id) => own.size > 0 ? own.has(id) : id !== sampleId;
2305
2662
  const client = deps(ctx).client(configured.url, {
2306
2663
  kind: "api-key",
2307
- key: ctx.state.apiKey
2664
+ key: ctx.secrets.apiKey
2308
2665
  });
2309
2666
  try {
2310
- return (await client.database.list()).data.some((database) => own.has(database.id)) ? "done" : "todo";
2667
+ return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
2311
2668
  } catch (error) {
2312
- if (isNotServingYet$1(error)) return "todo";
2669
+ if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
2313
2670
  throw error;
2314
2671
  }
2315
2672
  },
2316
2673
  run(ctx) {
2674
+ if (options.request === "ask-once") return askForOwnDatabase(ctx, deps(ctx));
2317
2675
  return addDatabases(ctx, deps(ctx));
2318
2676
  }
2319
2677
  };
@@ -2391,6 +2749,11 @@ async function createWizardContext(instanceName, options) {
2391
2749
  await writeInstanceState(next);
2392
2750
  ctx.state = next;
2393
2751
  },
2752
+ secrets: await readInstanceSecrets(instanceName) ?? emptyInstanceSecrets(),
2753
+ saveSecrets: async (next) => {
2754
+ await writeInstanceSecrets(instanceName, next);
2755
+ ctx.secrets = next;
2756
+ },
2394
2757
  prompts: options.prompts ?? clackPrompts(),
2395
2758
  ui: clackUi(),
2396
2759
  signal: options.signal,
@@ -2399,53 +2762,6 @@ async function createWizardContext(instanceName, options) {
2399
2762
  return ctx;
2400
2763
  }
2401
2764
  //#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
2765
  //#region src/commands/db/add.ts
2450
2766
  const dbAdd = defineRdeCommand({
2451
2767
  meta: {
@@ -2455,19 +2771,10 @@ const dbAdd = defineRdeCommand({
2455
2771
  async run() {
2456
2772
  const name = await defaultInstanceName();
2457
2773
  await requireInstanceState(name);
2458
- const ctx = await createWizardContext(name, { signal: interruptSignal });
2459
- await databasesStep(ASK_DATABASES).run(ctx);
2774
+ await addDatabases(await createWizardContext(name, { signal: interruptSignal }), hostDatabasesDeps(interruptSignal));
2460
2775
  }
2461
2776
  });
2462
2777
  //#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
2778
  //#region src/commands/db/list.ts
2472
2779
  const dbList = defineRdeCommand({
2473
2780
  meta: {
@@ -2480,7 +2787,10 @@ const dbList = defineRdeCommand({
2480
2787
  default: false
2481
2788
  } },
2482
2789
  async run(args) {
2483
- const listing = await listDatabases(await requireInstanceState(await defaultInstanceName()), hostDatabasesDeps(interruptSignal));
2790
+ const name = await defaultInstanceName();
2791
+ const state = await requireInstanceState(name);
2792
+ const secrets = await readInstanceSecrets(name);
2793
+ const listing = await listDatabases(state, secrets === null ? null : secrets.apiKey, hostDatabasesDeps(interruptSignal));
2484
2794
  if (args.json) {
2485
2795
  printJson(listing);
2486
2796
  return;
@@ -2809,15 +3119,20 @@ async function isExecutable(path) {
2809
3119
  return false;
2810
3120
  }
2811
3121
  }
2812
- async function whichCommand(name) {
3122
+ async function whichAll(name) {
2813
3123
  const searchPath = readEnv(ENV_PATH);
2814
- if (searchPath === void 0) return null;
2815
- for (const dir of searchPath.split(delimiter)) {
3124
+ if (searchPath === void 0) return [];
3125
+ const found = [];
3126
+ for (const dir of new Set(searchPath.split(delimiter))) {
2816
3127
  if (dir.length === 0) continue;
2817
3128
  const candidate = join(dir, name);
2818
- if (await isExecutable(candidate)) return candidate;
3129
+ if (await isExecutable(candidate)) found.push(candidate);
2819
3130
  }
2820
- return null;
3131
+ return found;
3132
+ }
3133
+ async function whichCommand(name) {
3134
+ const [first] = await whichAll(name);
3135
+ return first ?? null;
2821
3136
  }
2822
3137
  //#endregion
2823
3138
  //#region src/tools/java.ts
@@ -3623,7 +3938,7 @@ async function detectAgents(probe) {
3623
3938
  //#endregion
3624
3939
  //#region src/tools/mb.ts
3625
3940
  const MB = "mb";
3626
- const MB_INSTALL_SPEC = `@metabase/cli@alpha-transform-tests`;
3941
+ const MB_INSTALL_SPEC = `@metabase/cli@alpha-fix-transform-test-ee-path`;
3627
3942
  const GLOBAL_INSTALL = {
3628
3943
  npm: {
3629
3944
  command: "npm",
@@ -3661,44 +3976,117 @@ const GLOBAL_INSTALL = {
3661
3976
  function mbInstallCommand(packageManager) {
3662
3977
  return GLOBAL_INSTALL[packageManager];
3663
3978
  }
3979
+ function ownerByMarker(path, markers) {
3980
+ const normalized = path.replaceAll("\\", "/");
3981
+ const found = markers.find(({ marker }) => normalized.includes(marker));
3982
+ return found === void 0 ? null : found.owner;
3983
+ }
3664
3984
  const GLOBAL_MARKERS = [
3665
3985
  {
3666
3986
  marker: "/.bun/install/global/node_modules/",
3667
- packageManager: "bun"
3987
+ owner: "bun"
3668
3988
  },
3669
3989
  {
3670
3990
  marker: "/Library/pnpm/global/",
3671
- packageManager: "pnpm"
3991
+ owner: "pnpm"
3672
3992
  },
3673
3993
  {
3674
3994
  marker: "/.local/share/pnpm/global/",
3675
- packageManager: "pnpm"
3995
+ owner: "pnpm"
3676
3996
  },
3677
3997
  {
3678
3998
  marker: "/share/pnpm/global/",
3679
- packageManager: "pnpm"
3999
+ owner: "pnpm"
3680
4000
  },
3681
4001
  {
3682
4002
  marker: "/pnpm-global/node_modules/",
3683
- packageManager: "pnpm"
4003
+ owner: "pnpm"
3684
4004
  },
3685
4005
  {
3686
4006
  marker: "/.config/yarn/global/node_modules/",
3687
- packageManager: "yarn"
4007
+ owner: "yarn"
3688
4008
  },
3689
4009
  {
3690
4010
  marker: "/lib/node_modules/",
3691
- packageManager: "npm"
4011
+ owner: "npm"
3692
4012
  },
3693
4013
  {
3694
4014
  marker: "/.npm-global/node_modules/",
3695
- packageManager: "npm"
4015
+ owner: "npm"
3696
4016
  }
3697
4017
  ];
3698
4018
  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";
4019
+ return ownerByMarker(scriptPath, GLOBAL_MARKERS) ?? "npm";
4020
+ }
4021
+ const MB_OWNER_NAMES = {
4022
+ npm: "npm",
4023
+ pnpm: "pnpm",
4024
+ yarn: "Yarn",
4025
+ bun: "Bun",
4026
+ homebrew: "Homebrew"
4027
+ };
4028
+ const CELLAR = "/Cellar/";
4029
+ const NODE_MODULES = "/node_modules/";
4030
+ const OWNER_MARKERS = [
4031
+ ...GLOBAL_MARKERS,
4032
+ {
4033
+ marker: CELLAR,
4034
+ owner: "homebrew"
4035
+ },
4036
+ {
4037
+ marker: "/Library/pnpm/",
4038
+ owner: "pnpm"
4039
+ },
4040
+ {
4041
+ marker: "/.local/share/pnpm/",
4042
+ owner: "pnpm"
4043
+ }
4044
+ ];
4045
+ async function locateMbOnPath() {
4046
+ const paths = await whichAll(MB);
4047
+ return Promise.all(paths.map(async (path) => ({
4048
+ path,
4049
+ resolved: await promises.realpath(path)
4050
+ })));
4051
+ }
4052
+ function mbOwner(binary) {
4053
+ return ownerByMarker(binary.resolved, OWNER_MARKERS) ?? ownerByMarker(binary.path, OWNER_MARKERS);
4054
+ }
4055
+ function firstSegmentAfter(path, marker) {
4056
+ const at = path.lastIndexOf(marker);
4057
+ if (at === -1) return null;
4058
+ const [first] = path.slice(at + marker.length).split("/");
4059
+ return first === void 0 || first.length === 0 ? null : first;
4060
+ }
4061
+ function packageName(resolved) {
4062
+ const scopeOrName = firstSegmentAfter(resolved, NODE_MODULES);
4063
+ if (scopeOrName === null || !scopeOrName.startsWith("@")) return scopeOrName;
4064
+ const name = firstSegmentAfter(resolved, `${NODE_MODULES}${scopeOrName}/`);
4065
+ return name === null ? null : `${scopeOrName}/${name}`;
4066
+ }
4067
+ const REMOVE_COMMANDS = {
4068
+ npm: (name) => `npm uninstall -g ${name}`,
4069
+ pnpm: (name) => `pnpm remove -g ${name}`,
4070
+ yarn: (name) => `yarn global remove ${name}`,
4071
+ bun: (name) => `bun remove -g ${name}`,
4072
+ homebrew: (name) => `brew uninstall ${name}`
4073
+ };
4074
+ function mbRemoveCommand(binary) {
4075
+ const owner = mbOwner(binary);
4076
+ if (owner === null) return null;
4077
+ const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
4078
+ return name === null ? null : REMOVE_COMMANDS[owner](name);
4079
+ }
4080
+ function setValue(name) {
4081
+ const value = readEnv(name);
4082
+ return value === void 0 || value.length === 0 ? null : value;
4083
+ }
4084
+ function mbEnvironment() {
4085
+ return {
4086
+ profile: setValue(ENV_MB_PROFILE),
4087
+ url: setValue(ENV_MB_URL),
4088
+ apiKeySet: setValue(ENV_MB_API_KEY) !== null
4089
+ };
3702
4090
  }
3703
4091
  const ProbedUser = z.object({
3704
4092
  id: z.int(),
@@ -3775,16 +4163,16 @@ function mbTool(runner) {
3775
4163
  if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
3776
4164
  return parseJsonOutput(MB, argv, result.stdout, schema);
3777
4165
  }
3778
- async function version() {
4166
+ async function version(command = MB) {
3779
4167
  const args = ["--version"];
3780
4168
  let result;
3781
4169
  try {
3782
- result = await runner.run(MB, args);
4170
+ result = await runner.run(command, args);
3783
4171
  } catch (error) {
3784
4172
  if (error instanceof ProcessNotFoundError) return null;
3785
4173
  throw error;
3786
4174
  }
3787
- if (result.exitCode !== 0) throw toolFailure(MB, args, result);
4175
+ if (result.exitCode !== 0) throw toolFailure(command, args, result);
3788
4176
  return result.stdout.trim();
3789
4177
  }
3790
4178
  async function authLogin(options) {
@@ -3870,65 +4258,14 @@ function mbTool(runner) {
3870
4258
  };
3871
4259
  }
3872
4260
  //#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(", ")}`);
3877
- }
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;
3891
- }
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";
3896
- }
3897
- function recordFor(outcome, at) {
3898
- return {
3899
- status: outcome === "completed" ? "done" : "skipped",
3900
- completedAt: at.toISOString()
3901
- };
3902
- }
3903
- function withStepRecord(state, id, record) {
3904
- return {
3905
- ...state,
3906
- steps: {
3907
- ...state.steps,
3908
- [id]: record
3909
- }
3910
- };
3911
- }
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())));
3925
- }
3926
- }
3927
- //#endregion
3928
4261
  //#region src/tools/skills-cli.ts
3929
4262
  const NPX = "npx";
3930
4263
  const SKILLS_PACKAGE = "skills@latest";
3931
- const RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-generic-skills";
4264
+ const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-skill";
4265
+ function rdeSkillSource() {
4266
+ const override = readEnv(ENV_RDE_SKILL_SOURCE);
4267
+ return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
4268
+ }
3932
4269
  const InstalledSkill = z.object({
3933
4270
  name: z.string(),
3934
4271
  path: z.string(),
@@ -3937,6 +4274,49 @@ const InstalledSkill = z.object({
3937
4274
  source: z.string().nullable()
3938
4275
  }).loose();
3939
4276
  const InstalledSkillList = z.array(InstalledSkill);
4277
+ const SkillLock = z.object({ skills: z.record(z.string(), z.object({ ref: z.string().nullish() }).loose()) }).loose();
4278
+ const GLOBAL_LOCK_FILE = ".skill-lock.json";
4279
+ const PROJECT_LOCK_FILE = "skills-lock.json";
4280
+ function globalSkillLockPath(home, stateHome) {
4281
+ return stateHome === null ? join(home, ".agents", GLOBAL_LOCK_FILE) : join(stateHome, "skills", GLOBAL_LOCK_FILE);
4282
+ }
4283
+ function projectSkillLockPath(project) {
4284
+ return join(project, PROJECT_LOCK_FILE);
4285
+ }
4286
+ function xdgStateHome() {
4287
+ const value = readEnv(ENV_XDG_STATE_HOME);
4288
+ return value === void 0 || value.length === 0 ? null : value;
4289
+ }
4290
+ function lockedRef(path, raw, skill) {
4291
+ if (raw === null) return null;
4292
+ const parsed = SkillLock.safeParse(raw);
4293
+ if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
4294
+ return parsed.data.skills[skill]?.ref ?? null;
4295
+ }
4296
+ const hostSkillFiles = {
4297
+ readJson: readJsonFile,
4298
+ readText: readTextFile
4299
+ };
4300
+ const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
4301
+ function skillOrigin(source, cwd) {
4302
+ const github = GITHUB_URL.exec(source);
4303
+ if (github !== null) {
4304
+ const [, owner, repo, ref] = github;
4305
+ return {
4306
+ kind: "github",
4307
+ repository: `${owner}/${repo}`,
4308
+ ref: ref ?? null
4309
+ };
4310
+ }
4311
+ if (isAbsolute(source) || source.startsWith(".")) return {
4312
+ kind: "local",
4313
+ path: resolve(cwd, source)
4314
+ };
4315
+ return {
4316
+ kind: "other",
4317
+ source
4318
+ };
4319
+ }
3940
4320
  function skillsCli(runner) {
3941
4321
  async function add(options) {
3942
4322
  const argv = [
@@ -3988,13 +4368,83 @@ function skillsCli(runner) {
3988
4368
  if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
3989
4369
  return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
3990
4370
  }
4371
+ async function listProject(cwd) {
4372
+ const argv = [
4373
+ "-y",
4374
+ SKILLS_PACKAGE,
4375
+ "list",
4376
+ "--json"
4377
+ ];
4378
+ const result = await runner.run(NPX, argv, {
4379
+ cwd,
4380
+ stdoutToFile: true
4381
+ });
4382
+ if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
4383
+ return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
4384
+ }
3991
4385
  return {
3992
4386
  add,
3993
4387
  remove,
3994
- list
4388
+ list,
4389
+ listProject
3995
4390
  };
3996
4391
  }
3997
4392
  //#endregion
4393
+ //#region src/wizard/engine.ts
4394
+ function requireKnownStep(steps, id, flag) {
4395
+ if (id === void 0) return;
4396
+ if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
4397
+ }
4398
+ async function detectAll(steps, ctx, options) {
4399
+ const detected = [];
4400
+ let fromHere = false;
4401
+ for (const step of steps) {
4402
+ if (step.id === options.from) fromHere = true;
4403
+ const forced = options.only !== void 0 ? step.id === options.only : fromHere;
4404
+ detected.push({
4405
+ step,
4406
+ status: forced ? "todo" : await step.detect(ctx),
4407
+ forced
4408
+ });
4409
+ }
4410
+ return detected;
4411
+ }
4412
+ async function shouldRun(entry, ctx, options) {
4413
+ if (entry.forced) return true;
4414
+ if (options.only !== void 0 || entry.status === "done") return false;
4415
+ return await entry.step.detect(ctx) === "todo";
4416
+ }
4417
+ function recordFor(outcome, at) {
4418
+ return {
4419
+ status: outcome === "completed" ? "done" : "skipped",
4420
+ completedAt: at.toISOString()
4421
+ };
4422
+ }
4423
+ function withStepRecord(state, id, record) {
4424
+ return {
4425
+ ...state,
4426
+ steps: {
4427
+ ...state.steps,
4428
+ [id]: record
4429
+ }
4430
+ };
4431
+ }
4432
+ async function runWizard(steps, ctx, options = {}) {
4433
+ requireKnownStep(steps, options.from, "--from");
4434
+ requireKnownStep(steps, options.only, "--only");
4435
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
4436
+ const detected = await detectAll(steps, ctx, options);
4437
+ ctx.ui.checklist(detected.map(({ step, status }) => ({
4438
+ title: step.title,
4439
+ status
4440
+ })));
4441
+ for (const entry of detected) {
4442
+ if (!await shouldRun(entry, ctx, options)) continue;
4443
+ const outcome = await entry.step.run(ctx);
4444
+ await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
4445
+ }
4446
+ }
4447
+ //#endregion
3998
4448
  //#region src/steps/agents-skills.ts
3999
4449
  const STEP_ID$8 = "agents-skills";
4000
4450
  const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
@@ -4002,7 +4452,8 @@ const DETECT_AGENTS = { agents: null };
4002
4452
  function hostDeps$11(ctx) {
4003
4453
  return {
4004
4454
  runner: processRunner(ctx.signal),
4005
- probe: hostAgentProbe()
4455
+ probe: hostAgentProbe(),
4456
+ skillSource: rdeSkillSource()
4006
4457
  };
4007
4458
  }
4008
4459
  function detectedOnly(agents) {
@@ -4062,9 +4513,10 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
4062
4513
  ctx.ui.info(skipLine(all, selection.source));
4063
4514
  return "skipped";
4064
4515
  }
4065
- ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`);
4516
+ const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-skill" ? "" : ` from ${resolved.skillSource}`;
4517
+ ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
4066
4518
  await skillsCli(resolved.runner).add({
4067
- source: RDE_SKILL_SOURCE,
4519
+ source: resolved.skillSource,
4068
4520
  skill: "rde",
4069
4521
  agents: selection.agents.map((agent) => agent.skillsCliName)
4070
4522
  });
@@ -4090,7 +4542,8 @@ function rdeInstalledWith() {
4090
4542
  function hostDeps$10(ctx) {
4091
4543
  return {
4092
4544
  runner: processRunner(ctx.signal),
4093
- installedWith: rdeInstalledWith()
4545
+ installedWith: rdeInstalledWith(),
4546
+ locate: locateMbOnPath
4094
4547
  };
4095
4548
  }
4096
4549
  async function inspectMb(runner) {
@@ -4107,14 +4560,100 @@ async function inspectMb(runner) {
4107
4560
  function displayCommand(install) {
4108
4561
  return [install.command, ...install.args].join(" ");
4109
4562
  }
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({
4563
+ async function versionAt(runner, path) {
4564
+ try {
4565
+ const raw = await mbTool(runner).version(path);
4566
+ return raw === null ? null : semver.valid(raw);
4567
+ } catch (error) {
4568
+ if (error instanceof ExternalToolError) return null;
4569
+ throw error;
4570
+ }
4571
+ }
4572
+ async function inspectMbOnPath(runner, locate) {
4573
+ const found = [];
4574
+ for (const binary of await locate()) found.push({
4575
+ path: binary.path,
4576
+ version: await versionAt(runner, binary.path),
4577
+ owner: mbOwner(binary),
4578
+ removeCommand: mbRemoveCommand(binary)
4579
+ });
4580
+ return found;
4581
+ }
4582
+ function satisfiesMinimum(version) {
4583
+ return version !== null && semver.satisfies(version, MB_RANGE, { includePrerelease: true });
4584
+ }
4585
+ function isNewer(candidate, than) {
4586
+ if (candidate === null) return false;
4587
+ return than === null || semver.gt(candidate, than);
4588
+ }
4589
+ function diagnoseMb(found, installer) {
4590
+ const [running, ...later] = found;
4591
+ if (running === void 0) return { kind: "missing" };
4592
+ const sufficient = satisfiesMinimum(running.version);
4593
+ const newer = later.find((candidate) => satisfiesMinimum(candidate.version) && isNewer(candidate.version, running.version));
4594
+ if (newer !== void 0 && (!sufficient || running.owner !== installer)) return {
4595
+ kind: "shadowed",
4596
+ running,
4597
+ newer
4598
+ };
4599
+ return sufficient ? {
4600
+ kind: "current",
4601
+ running
4602
+ } : {
4603
+ kind: "old",
4604
+ running
4605
+ };
4606
+ }
4607
+ function runningMbSuffices(diagnosis) {
4608
+ return diagnosis.kind !== "missing" && satisfiesMinimum(diagnosis.running.version);
4609
+ }
4610
+ function versionText(entry) {
4611
+ return entry.version ?? "an unknown version";
4612
+ }
4613
+ function describeEntry(entry) {
4614
+ const owner = entry.owner === null ? "" : ` (${MB_OWNER_NAMES[entry.owner]})`;
4615
+ return `${versionText(entry)} at ${entry.path}${owner}`;
4616
+ }
4617
+ function mbPathLines(found) {
4618
+ return found.map((entry, index) => `${index === 0 ? "" : "then "}${describeEntry(entry)}`);
4619
+ }
4620
+ function removeClause(entry) {
4621
+ if (entry.removeCommand !== null) return `remove the old one with \`${entry.removeCommand}\``;
4622
+ if (entry.owner !== null) return `remove the old one with ${MB_OWNER_NAMES[entry.owner]}`;
4623
+ return `remove ${entry.path}`;
4624
+ }
4625
+ function shadowedHint(diagnosis) {
4626
+ const { running, newer } = diagnosis;
4627
+ return [
4628
+ `${running.path} (${versionText(running)}) runs before ${newer.path} (${versionText(newer)});`,
4629
+ `${removeClause(running)}, or put ${dirname(newer.path)} before ${dirname(running.path)} on PATH,`,
4630
+ "then open a new terminal"
4631
+ ].join(" ");
4632
+ }
4633
+ const NEW_TERMINAL = "open a new terminal and run rde init again";
4634
+ function stillWrongAdvice(diagnosis) {
4635
+ switch (diagnosis.kind) {
4636
+ case "shadowed": return `${shadowedHint(diagnosis)} and run rde init again`;
4637
+ case "old": return `The install put no newer mb on PATH; ${removeClause(diagnosis.running)}, or ${NEW_TERMINAL}`;
4638
+ case "missing":
4639
+ case "current": return "Open a new terminal and run rde init again";
4640
+ }
4641
+ }
4642
+ async function stillWrong(install, after, deps) {
4643
+ const fail = (stderrTail) => new ExternalToolError({
4113
4644
  tool: install.command,
4114
4645
  argv: install.args,
4115
4646
  exitCode: 0,
4116
- stderrTail: `${found}; open a new terminal and run rde init again`
4647
+ stderrTail
4117
4648
  });
4649
+ if (!after.installed) return fail(`mb is not on PATH after the install; ${NEW_TERMINAL}`);
4650
+ const found = await inspectMbOnPath(deps.runner, deps.locate);
4651
+ const diagnosis = diagnoseMb(found, deps.installedWith);
4652
+ return fail([
4653
+ `mb ${after.version} still runs first (${MIN_MB_VERSION} or newer is required).`,
4654
+ `Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.`,
4655
+ `${stillWrongAdvice(diagnosis)}.`
4656
+ ].join(" "));
4118
4657
  }
4119
4658
  function installMbStep(deps = hostDeps$10) {
4120
4659
  return {
@@ -4142,7 +4681,7 @@ function installMbStep(deps = hostDeps$10) {
4142
4681
  stderrTail: ""
4143
4682
  });
4144
4683
  const after = await inspectMb(resolved.runner);
4145
- if (!after.installed || !after.satisfies) throw stillWrong(install, after);
4684
+ if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
4146
4685
  ctx.ui.success(`mb ${after.version} is installed`);
4147
4686
  return "completed";
4148
4687
  }
@@ -4463,7 +5002,15 @@ function hostDoctorDeps(instanceName, signal) {
4463
5002
  runtime,
4464
5003
  ports: hostPortProbe,
4465
5004
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
4466
- probe: hostAgentProbe()
5005
+ probe: hostAgentProbe(),
5006
+ locateMb: locateMbOnPath,
5007
+ environment: mbEnvironment(),
5008
+ skills: {
5009
+ source: rdeSkillSource(),
5010
+ cwd: process.cwd(),
5011
+ stateHome: xdgStateHome(),
5012
+ files: hostSkillFiles
5013
+ }
4467
5014
  };
4468
5015
  }
4469
5016
  const NOT_FOUND_STATUS$2 = 404;
@@ -4476,7 +5023,17 @@ function ok(id, label, detail) {
4476
5023
  status: "ok",
4477
5024
  detail,
4478
5025
  hint: null,
4479
- fix: null
5026
+ step: null
5027
+ };
5028
+ }
5029
+ function note$1(id, label, detail, hint) {
5030
+ return {
5031
+ id,
5032
+ label,
5033
+ status: "ok",
5034
+ detail,
5035
+ hint,
5036
+ step: null
4480
5037
  };
4481
5038
  }
4482
5039
  function skipped(id, label, detail) {
@@ -4486,7 +5043,7 @@ function skipped(id, label, detail) {
4486
5043
  status: "skipped",
4487
5044
  detail,
4488
5045
  hint: null,
4489
- fix: null
5046
+ step: null
4490
5047
  };
4491
5048
  }
4492
5049
  function failed(id, label, failure) {
@@ -4507,7 +5064,7 @@ function machineRow(check, needed) {
4507
5064
  return failed(check.id, check.id, {
4508
5065
  detail: check.label,
4509
5066
  hint: check.hint,
4510
- fix: "preflight"
5067
+ step: "preflight"
4511
5068
  });
4512
5069
  }
4513
5070
  async function machineRows(deps, runtime) {
@@ -4519,19 +5076,48 @@ function installHint(installedWith) {
4519
5076
  const install = mbInstallCommand(installedWith);
4520
5077
  return `\`${[install.command, ...install.args].join(" ")}\``;
4521
5078
  }
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);
5079
+ const MB_LABEL = "mb";
5080
+ function mbUnusable(diagnosis) {
5081
+ if (diagnosis.kind === "missing") return "mb is not installed";
5082
+ return runningMbSuffices(diagnosis) ? null : "the mb that runs is too old";
5083
+ }
5084
+ function mbRow(diagnosis, lines, deps) {
5085
+ switch (diagnosis.kind) {
5086
+ case "missing": return failed("mb", MB_LABEL, {
5087
+ detail: "not installed",
5088
+ hint: installHint(deps.installedWith),
5089
+ step: "install-mb"
5090
+ });
5091
+ case "old": {
5092
+ const required = lines.map((line, index) => index === 0 ? `${line}; ${MIN_MB_VERSION} or newer is required` : line);
5093
+ return failed("mb", MB_LABEL, {
5094
+ detail: required.join("\n"),
5095
+ hint: installHint(deps.installedWith),
5096
+ step: "install-mb"
5097
+ });
5098
+ }
5099
+ case "shadowed": return failed("mb", MB_LABEL, {
5100
+ detail: lines.join("\n"),
5101
+ hint: shadowedHint(diagnosis),
5102
+ step: null
5103
+ });
5104
+ case "current": return ok("mb", MB_LABEL, lines.join("\n"));
5105
+ }
5106
+ }
5107
+ async function mbFinding(deps) {
5108
+ const found = await inspectMbOnPath(deps.runner, deps.locateMb);
5109
+ const diagnosis = diagnoseMb(found, deps.installedWith);
5110
+ return {
5111
+ row: mbRow(diagnosis, mbPathLines(found), deps),
5112
+ unusable: mbUnusable(diagnosis)
5113
+ };
5114
+ }
5115
+ function sharedProfileList(deps) {
5116
+ let listed = null;
5117
+ return () => {
5118
+ listed ??= mbTool(deps.runner).authList();
5119
+ return listed;
5120
+ };
4535
5121
  }
4536
5122
  function publishesOnLoopback(container, port) {
4537
5123
  return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
@@ -4549,7 +5135,7 @@ async function containerRow(runtime, deps, dockerOk) {
4549
5135
  row: failed("container", label, {
4550
5136
  detail: `${runtime.containerName} does not exist`,
4551
5137
  hint: "`rde init` recreates it from the saved choices",
4552
- fix: "runtime"
5138
+ step: "runtime"
4553
5139
  }),
4554
5140
  running: false,
4555
5141
  holder: CONTAINER
@@ -4562,7 +5148,7 @@ async function containerRow(runtime, deps, dockerOk) {
4562
5148
  row: failed("container", label, {
4563
5149
  detail: `${runtime.containerName} runs ${inspection.image}, the state says ${runtime.image}`,
4564
5150
  hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
4565
- fix: null
5151
+ step: null
4566
5152
  }),
4567
5153
  ...found
4568
5154
  };
@@ -4570,7 +5156,7 @@ async function containerRow(runtime, deps, dockerOk) {
4570
5156
  row: failed("container", label, {
4571
5157
  detail: `${runtime.containerName} does not publish port ${runtime.port} on ${LOOPBACK_HOST}`,
4572
5158
  hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
4573
- fix: null
5159
+ step: null
4574
5160
  }),
4575
5161
  ...found
4576
5162
  };
@@ -4591,7 +5177,7 @@ async function processRow(runtime, deps) {
4591
5177
  row: failed("process", label, {
4592
5178
  detail: `${inspection.jarPath} does not exist`,
4593
5179
  hint,
4594
- fix: "runtime"
5180
+ step: "runtime"
4595
5181
  }),
4596
5182
  running: false,
4597
5183
  holder: JAR_PROCESS
@@ -4618,13 +5204,13 @@ async function portRow(port, finding, ports) {
4618
5204
  if (finding.running) return failed("port", "port", {
4619
5205
  detail: `${port} is not answering although ${finding.holder} runs`,
4620
5206
  hint: "`rde logs` shows what Metabase is doing",
4621
- fix: null
5207
+ step: null
4622
5208
  });
4623
5209
  if (free) return ok("port", "port", `${port} is free`);
4624
5210
  return failed("port", "port", {
4625
5211
  detail: `${port} is in use by another program`,
4626
5212
  hint: "stop that program before `rde start`",
4627
- fix: null
5213
+ step: null
4628
5214
  });
4629
5215
  }
4630
5216
  async function healthRow(url, deps) {
@@ -4639,7 +5225,7 @@ async function healthRow(url, deps) {
4639
5225
  row: failed("health", "health", {
4640
5226
  detail: `initializing (${Math.round(error.progress * 100)}%)`,
4641
5227
  hint: "wait for it; `rde logs -f` shows progress",
4642
- fix: "start-metabase"
5228
+ step: "start-metabase"
4643
5229
  }),
4644
5230
  healthy: false
4645
5231
  };
@@ -4647,7 +5233,7 @@ async function healthRow(url, deps) {
4647
5233
  row: failed("health", "health", {
4648
5234
  detail: "unreachable",
4649
5235
  hint: "`rde start`",
4650
- fix: "start-metabase"
5236
+ step: "start-metabase"
4651
5237
  }),
4652
5238
  healthy: false
4653
5239
  };
@@ -4655,7 +5241,7 @@ async function healthRow(url, deps) {
4655
5241
  row: failed("health", "health", {
4656
5242
  detail: `something other than Metabase answers at ${url} (HTTP ${error.status})`,
4657
5243
  hint: "another program holds the port; stop it before `rde start`",
4658
- fix: null
5244
+ step: null
4659
5245
  }),
4660
5246
  healthy: false
4661
5247
  };
@@ -4666,7 +5252,7 @@ function adminRow(state, properties) {
4666
5252
  if (!properties["has-user-setup"]) return failed("admin", "admin user", {
4667
5253
  detail: "Metabase has no admin user yet",
4668
5254
  hint: "`rde init --only admin-user`",
4669
- fix: "admin-user"
5255
+ step: "admin-user"
4670
5256
  });
4671
5257
  return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
4672
5258
  }
@@ -4678,36 +5264,204 @@ function versionRow(state, properties) {
4678
5264
  if (state.version !== running) return failed("version", label, {
4679
5265
  detail: `running ${running}, the state file says ${state.version}`,
4680
5266
  hint: "`rde start` records the running version",
4681
- fix: VERSION_STEP
5267
+ step: VERSION_STEP
4682
5268
  });
4683
5269
  return ok("version", label, running);
4684
5270
  }
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"
5271
+ const API_KEY_STEP = "api-key";
5272
+ const RECREATE_HINT = "when the instance holds nothing you need, `rde uninstall` then `rde init`";
5273
+ function adminPasswordRow(state, secrets) {
5274
+ const id = "admin-password";
5275
+ const label = "admin password";
5276
+ const admin = state.admin;
5277
+ if (admin === null) return skipped(id, label, "no admin user created by rde yet");
5278
+ switch (admin.kind) {
5279
+ case "prompted": return ok(id, label, "yours, entered during `rde init` and never stored");
5280
+ case "generated":
5281
+ if (secrets === null || secrets.admin === null) return failed(id, label, {
5282
+ detail: "generated, but missing from the secrets file",
5283
+ hint: `it cannot be recovered; ${RECREATE_HINT}`,
5284
+ step: null
5285
+ });
5286
+ return ok(id, label, "generated and stored; `rde credentials` shows it");
5287
+ case "legacy-default": return failed(id, label, {
5288
+ detail: "the public default password from an earlier rde (legacy-default)",
5289
+ hint: `anyone who knows the default can log in; ${RECREATE_HINT} with a generated one`,
5290
+ step: null
5291
+ });
5292
+ }
5293
+ }
5294
+ async function apiKeyFinding(secrets, url, healthy, deps) {
5295
+ const id = "api-key";
5296
+ const label = "API key";
5297
+ const apiKey = secrets === null ? null : secrets.apiKey;
5298
+ if (apiKey === null) return {
5299
+ row: failed(id, label, {
5300
+ detail: "none in the secrets file",
5301
+ hint: "`rde init --only api-key`",
5302
+ step: API_KEY_STEP
5303
+ }),
5304
+ access: {
5305
+ kind: "unusable",
5306
+ reason: "no API key"
5307
+ }
5308
+ };
5309
+ if (url === null || !healthy) return {
5310
+ row: ok(id, label, "in the secrets file"),
5311
+ access: {
5312
+ kind: "unusable",
5313
+ reason: NOT_HEALTHY
5314
+ }
5315
+ };
5316
+ const client = deps.client(url, {
5317
+ kind: "api-key",
5318
+ key: apiKey
5319
+ });
5320
+ try {
5321
+ await client.user.current();
5322
+ } catch (error) {
5323
+ if (!isRejectedCredential(error)) throw error;
5324
+ return {
5325
+ row: failed(id, label, {
5326
+ detail: "in the secrets file, but Metabase rejects it",
5327
+ hint: "`rde init --only api-key` replaces it",
5328
+ step: API_KEY_STEP
5329
+ }),
5330
+ access: {
5331
+ kind: "unusable",
5332
+ reason: "Metabase rejects the API key"
5333
+ }
5334
+ };
5335
+ }
5336
+ return {
5337
+ row: ok(id, label, "in the secrets file, accepted by Metabase"),
5338
+ access: {
5339
+ kind: "accepted",
5340
+ client
5341
+ }
5342
+ };
5343
+ }
5344
+ function octal(mode) {
5345
+ return `0${mode.toString(8)}`;
5346
+ }
5347
+ function expectsSecrets(state) {
5348
+ return state.admin !== null && state.admin.kind !== "prompted" || state.steps[API_KEY_STEP]?.status === "done";
5349
+ }
5350
+ function secretsFileRow(state, files, dir) {
5351
+ const id = "secrets-file";
5352
+ const label = "secrets file";
5353
+ const path = secretsFileIn(dir);
5354
+ if (files.secretsMode === null) {
5355
+ if (!expectsSecrets(state)) return skipped(id, label, "nothing stored yet");
5356
+ return failed(id, label, {
5357
+ detail: `${path} is missing`,
5358
+ hint: "`rde init --only api-key` stores a new API key",
5359
+ step: API_KEY_STEP
5360
+ });
5361
+ }
5362
+ if (files.secretsMode !== 384 || files.dirMode !== 448) {
5363
+ const dirMode = files.dirMode === null ? "missing" : octal(files.dirMode);
5364
+ return failed(id, label, {
5365
+ detail: `${path} is ${octal(files.secretsMode)} in a ${dirMode} directory`,
5366
+ hint: `\`chmod ${octal(384)} ${path} && chmod ${octal(448)} ${dir}\``,
5367
+ step: null
5368
+ });
5369
+ }
5370
+ return ok(id, label, `${path}, ${octal(384)} in a ${octal(448)} directory`);
5371
+ }
5372
+ function stateFileRow(files) {
5373
+ const id = "state-file";
5374
+ const label = "state file";
5375
+ if (files.secretsInState.length > 0) return failed(id, label, {
5376
+ detail: `carries ${files.secretsInState.join(", ")}`,
5377
+ hint: "`rde init --only api-key` rewrites it without them",
5378
+ step: API_KEY_STEP
4690
5379
  });
4691
- return ok("api-key", "API key", "in the state file");
5380
+ return ok(id, label, "no secrets");
4692
5381
  }
4693
- async function mbProfileRow(state, deps, mbInstalled, healthy) {
5382
+ async function credentialRows(state, secrets, apiKey, deps) {
5383
+ const dir = deps.runtime.instanceDir;
5384
+ const files = await inspectCredentialFiles(dir);
5385
+ return [
5386
+ adminPasswordRow(state, secrets),
5387
+ apiKey.row,
5388
+ secretsFileRow(state, files, dir),
5389
+ stateFileRow(files)
5390
+ ];
5391
+ }
5392
+ function withoutTrailingSlash(url) {
5393
+ return url.replace(/\/+$/, "");
5394
+ }
5395
+ async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
4694
5396
  const label = "mb profile";
4695
- if (!mbInstalled) return skipped("mb-profile", label, "mb is not installed");
5397
+ if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
4696
5398
  if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY);
4697
- const entry = (await mbTool(deps.runner).authList()).find((candidate) => candidate.profile === state.mbProfile);
5399
+ const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
4698
5400
  if (entry === void 0) return failed("mb-profile", label, {
4699
5401
  detail: `${state.mbProfile} is not configured`,
4700
5402
  hint: "`rde init --only api-key`",
4701
- fix: "api-key"
5403
+ step: "api-key"
5404
+ });
5405
+ if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
5406
+ detail: `${state.mbProfile} points at ${entry.url}, not at this instance (${url})`,
5407
+ hint: "`rde init --only api-key` logs it back in against the instance",
5408
+ step: "api-key"
4702
5409
  });
4703
5410
  if (entry.status !== "ok") return failed("mb-profile", label, {
4704
5411
  detail: `${state.mbProfile}: ${entry.status}`,
4705
5412
  hint: "`rde init --only api-key` logs it in again",
4706
- fix: "api-key"
5413
+ step: "api-key"
4707
5414
  });
4708
5415
  const user = entry.user === null ? "" : ` as ${entry.user.name}`;
4709
5416
  return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
4710
5417
  }
5418
+ function hostOf(url) {
5419
+ return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
5420
+ }
5421
+ function unsetHint(name) {
5422
+ return `\`unset ${name}\`, remove it from your shell profile if it is exported there, then restart the agent session`;
5423
+ }
5424
+ async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
5425
+ const id = `env.${ENV_MB_PROFILE}`;
5426
+ if (name === rdeProfile) return ok(id, ENV_MB_PROFILE, `${name}: rde's own profile, so plain \`mb\` uses this instance`);
5427
+ const rde = rdeProfile === null ? "rde's" : `rde's (${rdeProfile})`;
5428
+ const detail = (where) => `${name}: \`mb\` and the rde skill will use profile ${name} (${where}) instead of ${rde}`;
5429
+ if (mbUnusableReason !== null) return note$1(id, ENV_MB_PROFILE, detail(mbUnusableReason), unsetHint(ENV_MB_PROFILE));
5430
+ const entry = (await profiles()).find((candidate) => candidate.profile === name);
5431
+ const where = entry === void 0 ? "not configured" : hostOf(entry.url);
5432
+ return note$1(id, ENV_MB_PROFILE, detail(where), unsetHint(ENV_MB_PROFILE));
5433
+ }
5434
+ async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
5435
+ const rows = [];
5436
+ if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
5437
+ 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)));
5438
+ 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)));
5439
+ return rows;
5440
+ }
5441
+ const PROFILE_STATUS_WORDS = {
5442
+ ok: "authenticates",
5443
+ "auth-failed": "login rejected",
5444
+ "network-error": "not answering",
5445
+ "server-error": "answers with a server error",
5446
+ "not-probed": "not checked"
5447
+ };
5448
+ function plainMbLine(env, rdeProfile, profiles) {
5449
+ if (env.url !== null) return `plain \`mb\` goes to ${hostOf(env.url)} (${ENV_MB_URL})`;
5450
+ const name = env.profile ?? "default";
5451
+ const via = env.profile === null ? "" : ` (${ENV_MB_PROFILE})`;
5452
+ const entry = profiles.find((candidate) => candidate.profile === name);
5453
+ if (entry === void 0) return `plain \`mb\` uses ${name}${via}, which is not configured`;
5454
+ const instance = name === rdeProfile ? ", this instance" : "";
5455
+ return `plain \`mb\` uses ${name}${via}: ${hostOf(entry.url)}${instance}`;
5456
+ }
5457
+ async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
5458
+ const id = "other-metabases";
5459
+ const label = "other Metabases";
5460
+ if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
5461
+ const all = await profiles();
5462
+ const others = all.filter((entry) => entry.profile !== rdeProfile);
5463
+ 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"));
5464
+ }
4711
5465
  function isNotFound$2(error) {
4712
5466
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
4713
5467
  }
@@ -4718,7 +5472,7 @@ async function licenseRow(client) {
4718
5472
  if (!status.valid) return failed("license", label, {
4719
5473
  detail: `invalid: ${status.status}`,
4720
5474
  hint: "`rde init --from license` with a valid token",
4721
- fix: "license"
5475
+ step: "license"
4722
5476
  });
4723
5477
  return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
4724
5478
  } catch (error) {
@@ -4735,47 +5489,47 @@ async function databasesRow(state, client) {
4735
5489
  if (missing.length > 0) return failed("databases", label, {
4736
5490
  detail: `not on the server any more: ${missing.map((record) => record.name).join(", ")}`,
4737
5491
  hint: "`rde db add` connects them again",
4738
- fix: "databases"
5492
+ step: "databases"
4739
5493
  });
4740
5494
  const names = state.databases.map((record) => record.name).join(", ");
4741
5495
  return ok("databases", label, `${state.databases.length} connected: ${names}`);
4742
5496
  }
4743
5497
  const SAMPLE_DATABASE_STEP = "sample-database";
4744
5498
  const SAMPLE_LABEL = "sample database";
4745
- async function sampleDatabaseRow(record, deps, dockerOk, client) {
5499
+ async function sampleDatabaseRow(record, deps, dockerOk, access) {
4746
5500
  const id = "sample-database";
4747
5501
  if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
4748
5502
  const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
4749
5503
  if (!status.installed) return failed(id, SAMPLE_LABEL, {
4750
5504
  detail: `${record.containerName} does not exist`,
4751
5505
  hint: "`rde init` recreates it",
4752
- fix: SAMPLE_DATABASE_STEP
5506
+ step: SAMPLE_DATABASE_STEP
4753
5507
  });
4754
5508
  if (!status.running) return failed(id, SAMPLE_LABEL, {
4755
5509
  detail: `${record.containerName} is stopped`,
4756
5510
  hint: "`rde start`",
4757
- fix: "start-metabase"
5511
+ step: "start-metabase"
4758
5512
  });
4759
5513
  if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
4760
5514
  detail: `${record.containerName} runs but port ${record.port} is not answering`,
4761
5515
  hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
4762
- fix: null
5516
+ step: null
4763
5517
  });
4764
5518
  const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
4765
5519
  if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
4766
5520
  detail: `${where}; not connected to Metabase yet`,
4767
5521
  hint: "`rde init` connects it",
4768
- fix: SAMPLE_DATABASE_STEP
5522
+ step: SAMPLE_DATABASE_STEP
4769
5523
  });
4770
- if (client === null) return skipped(id, SAMPLE_LABEL, `${where}; ${NOT_HEALTHY}, so the connection is unchecked`);
5524
+ if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
4771
5525
  try {
4772
- const database = await client.database.get(record.databaseId);
5526
+ const database = await access.client.database.get(record.databaseId);
4773
5527
  return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
4774
5528
  } catch (error) {
4775
5529
  if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
4776
5530
  detail: `${where}; database ${record.databaseId} is gone from Metabase`,
4777
5531
  hint: "`rde init` connects it again",
4778
- fix: SAMPLE_DATABASE_STEP
5532
+ step: SAMPLE_DATABASE_STEP
4779
5533
  });
4780
5534
  throw error;
4781
5535
  }
@@ -4792,7 +5546,7 @@ async function gitSyncRow(state, deps) {
4792
5546
  if (status.branch === null) return failed("git-sync", label, {
4793
5547
  detail: "Metabase reports no branch",
4794
5548
  hint: "`rde init --only git-sync`",
4795
- fix: "git-sync"
5549
+ step: "git-sync"
4796
5550
  });
4797
5551
  const dirty = status.is_dirty ? "unsynced changes" : "in sync";
4798
5552
  return ok("git-sync", label, `branch ${status.branch}, ${dirty}, ${state.gitRemote.workdir}`);
@@ -4800,15 +5554,100 @@ async function gitSyncRow(state, deps) {
4800
5554
  if (isMbFailure$1(error)) return failed("git-sync", label, {
4801
5555
  detail: error.message,
4802
5556
  hint: `\`${mbCommand(state.mbProfile)} git-sync status\` shows the full answer`,
4803
- fix: null
5557
+ step: null
4804
5558
  });
4805
5559
  throw error;
4806
5560
  }
4807
5561
  }
4808
5562
  const SKILL_STEP = "agents-skills";
5563
+ const GLOBAL_SCOPE = "global";
5564
+ const SKILL_FILE = "SKILL.md";
5565
+ const CLAUDE_CODE = "Claude Code";
5566
+ const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
4809
5567
  function skillRowId(agent) {
4810
5568
  return `skill.${agent.id}`;
4811
5569
  }
5570
+ function withRef(source, ref) {
5571
+ return ref === null ? source : `${source}#${ref}`;
5572
+ }
5573
+ async function copyRef(entry, deps) {
5574
+ const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
5575
+ return lockedRef(path, await deps.skills.files.readJson(path), entry.name);
5576
+ }
5577
+ async function judgeCopy(entry, origin, deps) {
5578
+ const where = `${entry.scope} ${entry.path}`;
5579
+ switch (origin.kind) {
5580
+ case "local": {
5581
+ const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
5582
+ const wanted = await deps.skills.files.readText(join(origin.path, SKILL_FILE));
5583
+ const current = installed !== null && installed === wanted;
5584
+ return {
5585
+ entry,
5586
+ current,
5587
+ line: `${where}: ${current ? "same as" : "differs from"} ${origin.path}`
5588
+ };
5589
+ }
5590
+ case "github": {
5591
+ const wanted = withRef(origin.repository, origin.ref);
5592
+ if (entry.source === null) return {
5593
+ entry,
5594
+ current: false,
5595
+ line: `${where}: no recorded source, not ${wanted}`
5596
+ };
5597
+ const found = withRef(entry.source, await copyRef(entry, deps));
5598
+ const current = found === wanted;
5599
+ return {
5600
+ entry,
5601
+ current,
5602
+ line: `${where}: from ${found}${current ? "" : `, not ${wanted}`}`
5603
+ };
5604
+ }
5605
+ case "other": {
5606
+ const current = entry.source === origin.source;
5607
+ return {
5608
+ entry,
5609
+ current,
5610
+ line: `${where}: from ${entry.source ?? "no recorded source"}${current ? "" : `, not ${origin.source}`}`
5611
+ };
5612
+ }
5613
+ }
5614
+ }
5615
+ function loadedCopyLine(entries, cwd) {
5616
+ if (entries.length < 2) return null;
5617
+ const count = `${entries.length} copies are installed`;
5618
+ const forClaude = entries.filter((entry) => entry.agents.includes(CLAUDE_CODE));
5619
+ const loaded = forClaude.find((entry) => entry.scope === GLOBAL_SCOPE) ?? forClaude[0];
5620
+ if (loaded === void 0) return count;
5621
+ return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
5622
+ }
5623
+ function staleCopiesRow(stale, detail, deps) {
5624
+ const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
5625
+ const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
5626
+ const repairs = [];
5627
+ if (globalStale) repairs.push(`\`rde init --only ${SKILL_STEP}\` reinstalls the global copy from ${deps.skills.source}`);
5628
+ if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove rde -y\` in ${deps.skills.cwd}`);
5629
+ return failed("skill-copies", "rde skill copies", {
5630
+ detail,
5631
+ hint: `${repairs.join("; ")}; then restart the agent session`,
5632
+ step: globalStale ? SKILL_STEP : null
5633
+ });
5634
+ }
5635
+ async function skillCopiesRow(globalEntries, deps) {
5636
+ const id = "skill-copies";
5637
+ const label = "rde skill copies";
5638
+ const cwd = deps.skills.cwd;
5639
+ const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === "rde");
5640
+ const entries = [...globalEntries, ...projectEntries];
5641
+ if (entries.length === 0) return skipped(id, label, "no copy installed");
5642
+ const origin = skillOrigin(deps.skills.source, cwd);
5643
+ const judged = [];
5644
+ for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
5645
+ const loaded = loadedCopyLine(entries, cwd);
5646
+ const lines = judged.map((copy) => copy.line);
5647
+ const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
5648
+ const stale = judged.filter((copy) => !copy.current);
5649
+ return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
5650
+ }
4812
5651
  async function skillRows(state, deps) {
4813
5652
  const label = "rde skill";
4814
5653
  if (state === null) return [skipped("skill", label, NO_INSTANCE)];
@@ -4816,17 +5655,18 @@ async function skillRows(state, deps) {
4816
5655
  const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
4817
5656
  if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
4818
5657
  const entries = await rdeSkillEntries(deps.runner);
5658
+ const copies = await skillCopiesRow(entries, deps);
4819
5659
  if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
4820
5660
  detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
4821
5661
  hint: `\`rde init --only ${SKILL_STEP}\``,
4822
- fix: SKILL_STEP
4823
- })];
4824
- return detected.map((agent) => {
5662
+ step: SKILL_STEP
5663
+ }), copies];
5664
+ return [...detected.map((agent) => {
4825
5665
  const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
4826
5666
  const agentLabel = `${label} (${agent.displayName})`;
4827
5667
  if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
4828
5668
  return ok(skillRowId(agent), agentLabel, entry.path);
4829
- });
5669
+ }), copies];
4830
5670
  }
4831
5671
  function noRuntimeRows() {
4832
5672
  return [
@@ -4835,13 +5675,13 @@ function noRuntimeRows() {
4835
5675
  "health"
4836
5676
  ].map((id) => skipped(id, id, NO_INSTANCE));
4837
5677
  }
4838
- async function instanceRows(state, deps, dockerOk, mbInstalled) {
5678
+ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusableReason) {
4839
5679
  const configured = configuredRuntime(state);
4840
5680
  if (configured === null) return [
4841
5681
  ...noRuntimeRows(),
4842
5682
  skipped("version", "Metabase version", NO_INSTANCE),
4843
5683
  skipped("admin", "admin user", NO_INSTANCE),
4844
- apiKeyRow(state),
5684
+ ...await credentialRows(state, secrets, await apiKeyFinding(secrets, null, false, deps), deps),
4845
5685
  skipped("mb-profile", "mb profile", NO_INSTANCE),
4846
5686
  skipped("license", "license", NO_INSTANCE),
4847
5687
  skipped("databases", "databases", NO_INSTANCE),
@@ -4851,35 +5691,33 @@ async function instanceRows(state, deps, dockerOk, mbInstalled) {
4851
5691
  const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
4852
5692
  const { row: health, healthy } = await healthRow(configured.url, deps);
4853
5693
  const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
5694
+ const apiKey = await apiKeyFinding(secrets, configured.url, healthy, deps);
4854
5695
  const rows = [
4855
5696
  runtime.row,
4856
5697
  await portRow(configured.runtime.port, runtime, deps.ports),
4857
5698
  health,
4858
5699
  properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
4859
5700
  properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
4860
- apiKeyRow(state),
4861
- await mbProfileRow(state, deps, mbInstalled, healthy)
5701
+ ...await credentialRows(state, secrets, apiKey, deps),
5702
+ await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
4862
5703
  ];
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));
5704
+ const access = apiKey.access;
5705
+ if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
5706
+ else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
5707
+ rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
4872
5708
  rows.push(await gitSyncRow(state, deps));
4873
5709
  return rows;
4874
5710
  }
4875
- async function collectDoctor(instance, state, deps) {
5711
+ async function collectDoctor(instance, state, secrets, deps) {
4876
5712
  const machine = await machineRows(deps, state?.runtime ?? null);
4877
5713
  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));
5714
+ const mb = await mbFinding(deps);
5715
+ const profiles = sharedProfileList(deps);
5716
+ const checks = [...machine, mb.row];
5717
+ 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));
5718
+ else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
5719
+ const rdeProfile = state === null ? null : state.mbProfile;
5720
+ checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
4883
5721
  return {
4884
5722
  instance,
4885
5723
  checks,
@@ -4891,18 +5729,38 @@ const MARK = {
4891
5729
  failed: "✗",
4892
5730
  skipped: "–"
4893
5731
  };
5732
+ const NOTE_MARK = "!";
4894
5733
  const HINT_ARROW = "→";
5734
+ function markFor(row) {
5735
+ return row.status === "ok" && row.hint !== null ? NOTE_MARK : MARK[row.status];
5736
+ }
4895
5737
  function doctorLines(report) {
4896
5738
  const width = Math.max(...report.checks.map((row) => row.label.length));
5739
+ const indent = ` ${" ".repeat(width)} `;
4897
5740
  const lines = [];
4898
5741
  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}`);
5742
+ const [first, ...rest] = row.detail.split("\n");
5743
+ lines.push(`${markFor(row)} ${row.label.padEnd(width)} ${first}`);
5744
+ lines.push(...rest.map((line) => `${indent}${line}`));
5745
+ if (row.hint !== null) lines.push(`${indent}${HINT_ARROW} ${row.hint}`);
4901
5746
  }
4902
5747
  return lines;
4903
5748
  }
5749
+ function fixCommand(step) {
5750
+ return step === null ? null : `rde init --only ${step}`;
5751
+ }
5752
+ function doctorJson(report) {
5753
+ return {
5754
+ instance: report.instance,
5755
+ checks: report.checks.map(({ step, ...row }) => ({
5756
+ ...row,
5757
+ fix: fixCommand(step)
5758
+ })),
5759
+ failed: report.failed
5760
+ };
5761
+ }
4904
5762
  function fixableSteps(report) {
4905
- const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.fix !== null ? [row.fix] : []));
5763
+ const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.step !== null ? [row.step] : []));
4906
5764
  return StepId.options.filter((id) => wanted.has(id));
4907
5765
  }
4908
5766
  function doctorSummary(report) {
@@ -4941,15 +5799,24 @@ const ADMIN_USER_KEYS = {
4941
5799
  firstName: `${STEP_ID$6}.first-name`,
4942
5800
  lastName: `${STEP_ID$6}.last-name`
4943
5801
  };
4944
- const DEFAULT_ADMIN_EMAIL = "admin@metabase.com";
4945
- const DEFAULT_ADMIN_PASSWORD = "metabase123";
5802
+ const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
5803
+ const GENERATED_PASSWORD_BYTES = 24;
4946
5804
  const DEFAULT_SITE_NAME = "rde";
4947
5805
  const MIN_PASSWORD_LENGTH = 6;
4948
5806
  const DIGIT = /\d/;
4949
5807
  const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4950
- const DEFAULT_ADMIN = { variant: "default" };
5808
+ const DEFAULT_ADMIN = { variant: "generated" };
5809
+ function generatePassword(random) {
5810
+ for (;;) {
5811
+ const candidate = random(GENERATED_PASSWORD_BYTES).toString("base64url");
5812
+ if (DIGIT.test(candidate)) return candidate;
5813
+ }
5814
+ }
4951
5815
  function hostDeps$8(ctx) {
4952
- return { client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal) };
5816
+ return {
5817
+ client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
5818
+ password: () => generatePassword(randomBytes)
5819
+ };
4953
5820
  }
4954
5821
  function isNotReady(error) {
4955
5822
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
@@ -4988,26 +5855,22 @@ async function promptedUser(ctx) {
4988
5855
  ...lastName.length > 0 && { last_name: lastName }
4989
5856
  };
4990
5857
  }
4991
- async function chooseUser(ctx, kind) {
4992
- switch (kind) {
4993
- case "default": return {
4994
- email: DEFAULT_ADMIN_EMAIL,
4995
- password: DEFAULT_ADMIN_PASSWORD
5858
+ async function chooseUser(ctx, variant, deps) {
5859
+ switch (variant) {
5860
+ case "generated": return {
5861
+ email: GENERATED_ADMIN_EMAIL,
5862
+ password: deps.password()
4996
5863
  };
4997
5864
  case "prompted": return promptedUser(ctx);
4998
5865
  }
4999
5866
  }
5000
- function credentialsRecord(kind, user) {
5001
- switch (kind) {
5002
- case "default": return {
5003
- kind,
5867
+ function storedLogin(variant, user) {
5868
+ switch (variant) {
5869
+ case "generated": return {
5004
5870
  email: user.email,
5005
5871
  password: user.password
5006
5872
  };
5007
- case "prompted": return {
5008
- kind,
5009
- email: user.email
5010
- };
5873
+ case "prompted": return null;
5011
5874
  }
5012
5875
  }
5013
5876
  function adminUserStep(options, deps = hostDeps$8) {
@@ -5026,7 +5889,8 @@ function adminUserStep(options, deps = hostDeps$8) {
5026
5889
  },
5027
5890
  async run(ctx) {
5028
5891
  const configured = requireConfiguredRuntime(ctx.state);
5029
- const client = deps(ctx).client(configured.url, NO_AUTH);
5892
+ const resolved = deps(ctx);
5893
+ const client = resolved.client(configured.url, NO_AUTH);
5030
5894
  const properties = await client.session.properties();
5031
5895
  if (properties["has-user-setup"]) {
5032
5896
  ctx.ui.info("Metabase already has an admin user");
@@ -5034,7 +5898,11 @@ function adminUserStep(options, deps = hostDeps$8) {
5034
5898
  }
5035
5899
  const token = properties["setup-token"];
5036
5900
  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);
5901
+ const user = await chooseUser(ctx, options.variant, resolved);
5902
+ await ctx.saveSecrets({
5903
+ ...ctx.secrets,
5904
+ admin: storedLogin(options.variant, user)
5905
+ });
5038
5906
  const session = await client.setup.create({
5039
5907
  token,
5040
5908
  user,
@@ -5043,7 +5911,10 @@ function adminUserStep(options, deps = hostDeps$8) {
5043
5911
  ctx.session.metabaseSession.set(session.id);
5044
5912
  await ctx.saveState({
5045
5913
  ...ctx.state,
5046
- admin: credentialsRecord(options.variant, user)
5914
+ admin: {
5915
+ kind: options.variant,
5916
+ email: user.email
5917
+ }
5047
5918
  });
5048
5919
  ctx.ui.success(`Admin user ${user.email} created`);
5049
5920
  return "completed";
@@ -5066,11 +5937,12 @@ function hostDeps$7(ctx) {
5066
5937
  };
5067
5938
  }
5068
5939
  async function adminCredentials(ctx) {
5069
- const admin = ctx.state.admin;
5070
- if (admin?.kind === "default") return {
5071
- username: admin.email,
5072
- password: admin.password
5940
+ const stored = ctx.secrets.admin;
5941
+ if (stored !== null) return {
5942
+ username: stored.email,
5943
+ password: stored.password
5073
5944
  };
5945
+ const admin = ctx.state.admin;
5074
5946
  ctx.ui.info("An admin login is needed once to create the API key; it is not saved");
5075
5947
  return {
5076
5948
  username: await ctx.prompts.text({
@@ -5108,46 +5980,70 @@ async function freshApiKey(ctx, client) {
5108
5980
  group_id: groupId
5109
5981
  })).unmasked_key;
5110
5982
  }
5111
- async function persistedApiKey(ctx, deps, url) {
5112
- if (ctx.state.apiKey !== null) return ctx.state.apiKey;
5983
+ async function authenticates(client) {
5984
+ try {
5985
+ await client.user.current();
5986
+ return true;
5987
+ } catch (error) {
5988
+ if (isRejectedCredential(error)) return false;
5989
+ throw error;
5990
+ }
5991
+ }
5992
+ async function workingApiKey(ctx, deps, url) {
5993
+ const stored = ctx.secrets.apiKey;
5994
+ if (stored !== null) {
5995
+ if (await authenticates(deps.client(url, {
5996
+ kind: "api-key",
5997
+ key: stored
5998
+ }))) return stored;
5999
+ ctx.ui.warn("Metabase rejects the stored API key; replacing it");
6000
+ }
5113
6001
  const sessionId = await superuserSession(ctx, deps, url);
5114
6002
  const apiKey = await freshApiKey(ctx, deps.client(url, {
5115
6003
  kind: "session",
5116
6004
  id: sessionId
5117
6005
  }));
5118
- await ctx.saveState({
5119
- ...ctx.state,
6006
+ await ctx.saveSecrets({
6007
+ ...ctx.secrets,
5120
6008
  apiKey
5121
6009
  });
5122
6010
  return apiKey;
5123
6011
  }
5124
- async function chooseProfile(ctx, mb, url) {
5125
- const profile = ctx.state.mbProfile;
5126
- if (profile !== "default") return profile;
5127
- let existing;
6012
+ async function defaultProfileStatus(mb) {
5128
6013
  try {
5129
- existing = await mb.authStatus(DEFAULT_MB_PROFILE);
6014
+ return await mb.authStatus(DEFAULT_MB_PROFILE);
5130
6015
  } catch (error) {
5131
- if (error instanceof ProcessNotFoundError) return profile;
6016
+ if (error instanceof ProcessNotFoundError) return null;
5132
6017
  throw error;
5133
6018
  }
5134
- if (!existing.present || existing.url === url) return profile;
5135
- const chosen = await ctx.prompts.select({
6019
+ }
6020
+ async function chooseProfile(ctx, mb, url) {
6021
+ const profile = ctx.state.mbProfile;
6022
+ if (profile !== "default") return profile;
6023
+ const existing = await defaultProfileStatus(mb);
6024
+ if (existing === null || !existing.present || existing.url === null || existing.url === url) return profile;
6025
+ const previous = existing.url;
6026
+ const named = namedMbProfile(ctx.state.name);
6027
+ if (await ctx.prompts.select({
5136
6028
  key: API_KEY_KEYS.profile,
5137
- message: `mb profile ${DEFAULT_MB_PROFILE} already points at ${existing.url}; which profile should this instance use?`,
6029
+ message: `mb profile default already points at ${previous}; which profile should this instance use?`,
5138
6030
  choices: [{
5139
- value: "rde",
5140
- label: `rde, and leave ${DEFAULT_MB_PROFILE} alone`
6031
+ value: "default",
6032
+ label: `default, replacing it (recommended; it points at ${previous} today)`
5141
6033
  }, {
5142
- value: DEFAULT_MB_PROFILE,
5143
- label: `${DEFAULT_MB_PROFILE}, replacing it`
5144
- }]
5145
- });
5146
- if (chosen !== profile) await ctx.saveState({
6034
+ value: MbProfileChoice.enum.rde,
6035
+ label: `${named}, and leave default alone`
6036
+ }],
6037
+ recommended: "default"
6038
+ }) === "default") {
6039
+ ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
6040
+ return profile;
6041
+ }
6042
+ await ctx.saveState({
5147
6043
  ...ctx.state,
5148
- mbProfile: chosen
6044
+ mbProfile: named
5149
6045
  });
5150
- return chosen;
6046
+ return named;
5151
6047
  }
5152
6048
  function usageNote(profile) {
5153
6049
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
@@ -5158,7 +6054,7 @@ function apiKeyStep(deps = hostDeps$7) {
5158
6054
  id: STEP_ID$5,
5159
6055
  title: "Connect the Metabase CLI",
5160
6056
  async detect(ctx) {
5161
- if (ctx.state.apiKey === null) return "todo";
6057
+ if (ctx.secrets.apiKey === null) return "todo";
5162
6058
  const mb = mbTool(deps(ctx).runner);
5163
6059
  const profile = ctx.state.mbProfile;
5164
6060
  try {
@@ -5172,7 +6068,7 @@ function apiKeyStep(deps = hostDeps$7) {
5172
6068
  async run(ctx) {
5173
6069
  const resolved = deps(ctx);
5174
6070
  const configured = requireConfiguredRuntime(ctx.state);
5175
- const apiKey = await persistedApiKey(ctx, resolved, configured.url);
6071
+ const apiKey = await workingApiKey(ctx, resolved, configured.url);
5176
6072
  const mb = mbTool(resolved.runner);
5177
6073
  const profile = await chooseProfile(ctx, mb, configured.url);
5178
6074
  await mb.authLogin({
@@ -5574,14 +6470,14 @@ function isMbUnavailable(error) {
5574
6470
  }
5575
6471
  async function runGitSync(ctx, deps) {
5576
6472
  const configured = requireConfiguredRuntime(ctx.state);
5577
- if (ctx.state.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6473
+ if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
5578
6474
  const run = {
5579
6475
  ctx,
5580
6476
  deps,
5581
6477
  git: gitTool(deps.runner),
5582
6478
  client: deps.client(configured.url, {
5583
6479
  kind: "api-key",
5584
- key: ctx.state.apiKey
6480
+ key: ctx.secrets.apiKey
5585
6481
  })
5586
6482
  };
5587
6483
  const flow = await runFlow({
@@ -5637,8 +6533,8 @@ function validateToken(value) {
5637
6533
  return "A license token is 64 hexadecimal characters";
5638
6534
  }
5639
6535
  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;
6536
+ if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6537
+ return ctx.secrets.apiKey;
5642
6538
  }
5643
6539
  function activeLabel(status) {
5644
6540
  const plan = status["plan-alias"];
@@ -5651,15 +6547,15 @@ function licenseStep(deps = hostDeps$5) {
5651
6547
  async detect(ctx) {
5652
6548
  if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
5653
6549
  const configured = configuredRuntime(ctx.state);
5654
- if (configured === null || ctx.state.apiKey === null) return "todo";
6550
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
5655
6551
  const client = deps(ctx).client(configured.url, {
5656
6552
  kind: "api-key",
5657
- key: ctx.state.apiKey
6553
+ key: ctx.secrets.apiKey
5658
6554
  });
5659
6555
  try {
5660
6556
  return (await client.tokenStatus.get()).valid ? "done" : "todo";
5661
6557
  } catch (error) {
5662
- if (isNoTokenYet(error)) return "todo";
6558
+ if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
5663
6559
  throw error;
5664
6560
  }
5665
6561
  },
@@ -5894,9 +6790,9 @@ function isNotServingYet(error) {
5894
6790
  function isNotFound$1(error) {
5895
6791
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
5896
6792
  }
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;
6793
+ function requireApiKey(ctx) {
6794
+ if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
6795
+ return ctx.secrets.apiKey;
5900
6796
  }
5901
6797
  async function recordedDatabase(record, client) {
5902
6798
  if (record.databaseId === null) return null;
@@ -6018,23 +6914,23 @@ function sampleDatabaseStep(deps = hostDeps$3) {
6018
6914
  if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
6019
6915
  const record = ctx.state.sampleDatabase;
6020
6916
  const configured = configuredRuntime(ctx.state);
6021
- if (record === null || configured === null || ctx.state.apiKey === null) return "todo";
6917
+ if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
6022
6918
  if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
6023
6919
  const client = resolved.client(configured.url, {
6024
6920
  kind: "api-key",
6025
- key: ctx.state.apiKey
6921
+ key: ctx.secrets.apiKey
6026
6922
  });
6027
6923
  try {
6028
6924
  return await recordedDatabase(record, client) === null ? "todo" : "done";
6029
6925
  } catch (error) {
6030
- if (isNotServingYet(error)) return "todo";
6926
+ if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
6031
6927
  throw error;
6032
6928
  }
6033
6929
  },
6034
6930
  async run(ctx) {
6035
6931
  const resolved = deps(ctx);
6036
6932
  const configured = requireConfiguredRuntime(ctx.state);
6037
- const apiKey = requireApiKey(ctx.state);
6933
+ const apiKey = requireApiKey(ctx);
6038
6934
  const docker = await resolved.runtime.docker.available();
6039
6935
  if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
6040
6936
  const choice = await persistedRecord(ctx, resolved);
@@ -6149,23 +7045,24 @@ function hostDeps$1(ctx) {
6149
7045
  function isNotServing$1(error) {
6150
7046
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6151
7047
  }
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`;
7048
+ function loginLine(kind, email) {
7049
+ switch (kind) {
7050
+ case "generated":
7051
+ case "legacy-default": return `Login: ${email} (rde credentials shows the password)`;
7052
+ case "prompted": return `Login: ${email} with the password you entered`;
6157
7053
  }
6158
7054
  }
6159
- async function databaseLines$1(state, deps) {
6160
- if (state.url === null || state.apiKey === null) return ["Databases: none connected yet"];
7055
+ async function databaseLines$1(state, apiKey, deps) {
7056
+ if (state.url === null || apiKey === null) return ["Databases: none connected yet"];
6161
7057
  let databases;
6162
7058
  try {
6163
7059
  databases = (await deps.client(state.url, {
6164
7060
  kind: "api-key",
6165
- key: state.apiKey
7061
+ key: apiKey
6166
7062
  }).database.list()).data;
6167
7063
  } catch (error) {
6168
7064
  if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
7065
+ if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
6169
7066
  throw error;
6170
7067
  }
6171
7068
  if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
@@ -6199,14 +7096,13 @@ function launchLines(agents, workdir) {
6199
7096
  const prefix = workdir === null ? "" : `cd ${workdir} && `;
6200
7097
  return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
6201
7098
  }
6202
- async function summaryLines(state, deps) {
7099
+ async function summaryLines(state, apiKey, deps) {
6203
7100
  const profile = state.mbProfile;
6204
7101
  const lines = [];
6205
7102
  if (state.url !== null) lines.push(`Metabase: ${state.url}`);
6206
- const login = loginLine(state.admin);
6207
- if (login !== null) lines.push(login);
7103
+ if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
6208
7104
  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));
7105
+ lines.push(...await databaseLines$1(state, apiKey, deps));
6210
7106
  if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
6211
7107
  lines.push(...gitLines(state));
6212
7108
  const installed = await skillsCli(deps.runner).list();
@@ -6223,7 +7119,8 @@ function summaryStep(deps = hostDeps$1) {
6223
7119
  return "todo";
6224
7120
  },
6225
7121
  async run(ctx) {
6226
- ctx.ui.note((await summaryLines(ctx.state, deps(ctx))).join("\n"), "Your environment");
7122
+ const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
7123
+ ctx.ui.note(lines.join("\n"), "Your environment");
6227
7124
  return "completed";
6228
7125
  }
6229
7126
  };
@@ -6234,7 +7131,7 @@ const DEFAULT_STEP_OPTIONS = {
6234
7131
  runtime: NO_RUNTIME_FLAGS,
6235
7132
  agents: DETECT_AGENTS,
6236
7133
  admin: DEFAULT_ADMIN,
6237
- databases: ASK_DATABASES
7134
+ databases: SAMPLE_STANDS_IN
6238
7135
  };
6239
7136
  const STEP_BY_ID = {
6240
7137
  preflight: () => preflightStep(),
@@ -6254,10 +7151,129 @@ function initSteps(options) {
6254
7151
  return StepId.options.map((id) => STEP_BY_ID[id](options));
6255
7152
  }
6256
7153
  //#endregion
7154
+ //#region src/wizard/answers.ts
7155
+ function isOneOf(answer, kinds) {
7156
+ return kinds.some((kind) => kind === answer.kind);
7157
+ }
7158
+ function rejected(key, prompt, reason) {
7159
+ return new ConfigError(`answers file: "${key}" for "${prompt.message}" ${reason}`);
7160
+ }
7161
+ function expectKind(taken, prompt, kinds) {
7162
+ if (taken.answer.kind === "cancel") throw new AbortError();
7163
+ if (isOneOf(taken.answer, kinds)) return taken.answer;
7164
+ throw rejected(taken.key, prompt, `is a ${taken.answer.kind} answer; expected ${kinds.join(" or ")}`);
7165
+ }
7166
+ function choiceLabels(choices) {
7167
+ return choices.map((choice) => String(choice.value)).join(", ");
7168
+ }
7169
+ function matchChoice(key, prompt, value) {
7170
+ const match = prompt.choices.find((choice) => choice.value === value);
7171
+ if (match === void 0) throw rejected(key, prompt, `is ${String(value)}, not one of the choices ${choiceLabels(prompt.choices)}`);
7172
+ return match.value;
7173
+ }
7174
+ function textValue(taken, prompt) {
7175
+ const answer = expectKind(taken, prompt, ["text"]);
7176
+ const value = answer.value.length === 0 && prompt.defaultValue !== void 0 ? prompt.defaultValue : answer.value;
7177
+ const problem = prompt.validate?.(value);
7178
+ if (problem !== void 0) throw rejected(taken.key, prompt, `is "${value}", which is rejected: ${problem}`);
7179
+ return value;
7180
+ }
7181
+ function passwordValue(taken, prompt) {
7182
+ const answer = expectKind(taken, prompt, ["password", "text"]);
7183
+ const problem = prompt.validate?.(answer.value);
7184
+ if (problem !== void 0) throw rejected(taken.key, prompt, `is rejected: ${problem}`);
7185
+ return answer.value;
7186
+ }
7187
+ function isBackAnswer(answer) {
7188
+ return answer.kind === "back";
7189
+ }
7190
+ function answeringPrompts(base, answers, options = {}) {
7191
+ function lookup(prompt) {
7192
+ if (prompt.key === void 0) return null;
7193
+ const answer = answers.get(prompt.key)?.shift();
7194
+ return answer === void 0 ? null : {
7195
+ key: prompt.key,
7196
+ answer
7197
+ };
7198
+ }
7199
+ function refuseUnanswered(prompt) {
7200
+ const refusal = options.unanswered?.(prompt) ?? null;
7201
+ if (refusal !== null) throw refusal;
7202
+ }
7203
+ function take(prompt) {
7204
+ const taken = lookup(prompt);
7205
+ if (taken === null) refuseUnanswered(prompt);
7206
+ return taken;
7207
+ }
7208
+ function takeText(prompt) {
7209
+ const taken = lookup(prompt);
7210
+ if (taken !== null) return taken;
7211
+ if (prompt.defaultValue !== void 0 && options.unanswered !== void 0) return {
7212
+ key: prompt.key ?? prompt.message,
7213
+ answer: {
7214
+ kind: "text",
7215
+ value: ""
7216
+ }
7217
+ };
7218
+ refuseUnanswered(prompt);
7219
+ return null;
7220
+ }
7221
+ function selected(taken, prompt) {
7222
+ const answer = expectKind(taken, prompt, ["select", "text"]);
7223
+ return matchChoice(taken.key, prompt, answer.value);
7224
+ }
7225
+ function multiselected(taken, prompt) {
7226
+ return expectKind(taken, prompt, ["multiselect"]).values.map((value) => matchChoice(taken.key, prompt, value));
7227
+ }
7228
+ return {
7229
+ async text(opts) {
7230
+ const taken = takeText(opts);
7231
+ return taken === null ? base.text(opts) : textValue(taken, opts);
7232
+ },
7233
+ async textOrBack(opts) {
7234
+ const taken = takeText(opts);
7235
+ if (taken === null) return base.textOrBack(opts);
7236
+ return isBackAnswer(taken.answer) ? BACK : textValue(taken, opts);
7237
+ },
7238
+ async password(opts) {
7239
+ const taken = take(opts);
7240
+ return taken === null ? base.password(opts) : passwordValue(taken, opts);
7241
+ },
7242
+ async confirm(opts) {
7243
+ const taken = lookup(opts);
7244
+ if (taken !== null) return expectKind(taken, opts, ["confirm"]).value;
7245
+ if (options.confirmAll === true) return true;
7246
+ refuseUnanswered(opts);
7247
+ return base.confirm(opts);
7248
+ },
7249
+ async select(opts) {
7250
+ const taken = lookup(opts);
7251
+ if (taken !== null) return selected(taken, opts);
7252
+ if (options.takeRecommended === true && opts.recommended !== void 0) return opts.recommended;
7253
+ refuseUnanswered(opts);
7254
+ return base.select(opts);
7255
+ },
7256
+ async selectOrBack(opts) {
7257
+ const taken = take(opts);
7258
+ if (taken === null) return base.selectOrBack(opts);
7259
+ return isBackAnswer(taken.answer) ? BACK : selected(taken, opts);
7260
+ },
7261
+ async multiselect(opts) {
7262
+ const taken = take(opts);
7263
+ return taken === null ? base.multiselect(opts) : multiselected(taken, opts);
7264
+ },
7265
+ async multiselectOrBack(opts) {
7266
+ const taken = take(opts);
7267
+ if (taken === null) return base.multiselectOrBack(opts);
7268
+ return isBackAnswer(taken.answer) ? BACK : multiselected(taken, opts);
7269
+ }
7270
+ };
7271
+ }
7272
+ //#endregion
6257
7273
  //#region src/commands/doctor.ts
6258
7274
  function print(report, json) {
6259
7275
  if (json) {
6260
- printJson(report);
7276
+ printJson(doctorJson(report));
6261
7277
  return;
6262
7278
  }
6263
7279
  printLines(doctorLines(report));
@@ -6288,12 +7304,16 @@ const doctor = defineRdeCommand({
6288
7304
  const name = await instanceNameOrDefault();
6289
7305
  const state = await readInstanceState(name);
6290
7306
  const deps = hostDoctorDeps(name, interruptSignal);
6291
- let report = await collectDoctor(name, state, deps);
7307
+ let report = await collectDoctor(name, state, state === null ? null : await readInstanceSecrets(name), deps);
6292
7308
  if (args.fix && report.failed > 0) {
6293
7309
  if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
6294
7310
  print(report, args.json);
6295
- 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);
7311
+ const prompts = answeringPrompts(clackPrompts(), /* @__PURE__ */ new Map(), { takeRecommended: args.yes || !stdinIsTty() });
7312
+ const ctx = await createWizardContext(name, {
7313
+ signal: interruptSignal,
7314
+ prompts
7315
+ });
7316
+ if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
6297
7317
  }
6298
7318
  print(report, args.json);
6299
7319
  if (report.failed > 0) throw new Error(doctorSummary(report));
@@ -6318,7 +7338,7 @@ const EnvString = z.string().transform((value, ctx) => {
6318
7338
  }
6319
7339
  return resolved;
6320
7340
  });
6321
- const AdminVariant = z.enum(["default", "prompted"]);
7341
+ const AdminVariant = z.enum(["generated", "prompted"]);
6322
7342
  const RemoteKind = z.enum(["local-bare", "hosted"]);
6323
7343
  const FieldValue = z.union([
6324
7344
  EnvString,
@@ -6422,7 +7442,11 @@ function runtimeKindOf(runtime) {
6422
7442
  if (runtime.jar !== void 0) return "jar";
6423
7443
  return null;
6424
7444
  }
6425
- function stepOptionsFrom(answers) {
7445
+ function databasesRequest(databases, run) {
7446
+ if (databases !== void 0) return databases.length > 0 ? "named" : "sample-stands-in";
7447
+ return run.interactive ? "ask-once" : "sample-stands-in";
7448
+ }
7449
+ function stepOptionsFrom(answers, run) {
6426
7450
  const runtime = answers.runtime;
6427
7451
  const agents = answers["agents-skills"];
6428
7452
  const admin = answers["admin-user"];
@@ -6436,7 +7460,7 @@ function stepOptionsFrom(answers) {
6436
7460
  },
6437
7461
  agents: agents === void 0 ? DEFAULT_STEP_OPTIONS.agents : { agents: agents.agents },
6438
7462
  admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
6439
- databases: { requested: databases !== void 0 && databases.length > 0 }
7463
+ databases: { request: databasesRequest(databases, run) }
6440
7464
  };
6441
7465
  }
6442
7466
  function keyedAnswers(answers) {
@@ -6554,7 +7578,7 @@ function parseDatabase(spec) {
6554
7578
  function adminSection(flags, secrets) {
6555
7579
  const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
6556
7580
  if (flags.admin === void 0 && !prompted) return;
6557
- if (flags.admin === "default" && prompted) throw conflict("--admin default", "--admin-email/--admin-password");
7581
+ if (flags.admin === "generated" && prompted) throw conflict("--admin generated", "--admin-email/--admin-password");
6558
7582
  return {
6559
7583
  variant: flags.admin ?? "prompted",
6560
7584
  ...flags.adminEmail !== void 0 && { email: flags.adminEmail },
@@ -6705,20 +7729,57 @@ function isLocation(value) {
6705
7729
  function isRemote(value) {
6706
7730
  return value === "local-bare" || value === "hosted";
6707
7731
  }
7732
+ function isUnreviewedPaste(database) {
7733
+ return database !== void 0 && database.pasted && !database.reviewed;
7734
+ }
7735
+ function applyConnectionRecord(draft, value) {
7736
+ draft.databases = draft.databases.filter((database) => !database.pasted);
7737
+ if (value === null) return;
7738
+ if (value.trim().length === 0) {
7739
+ draft.databasesFinished = true;
7740
+ return;
7741
+ }
7742
+ draft.databases.push({
7743
+ engine: null,
7744
+ uri: value.trim(),
7745
+ fields: {},
7746
+ name: null,
7747
+ pasted: true,
7748
+ reviewed: false
7749
+ });
7750
+ }
7751
+ function applyEngineRecord(draft, value) {
7752
+ if (isUnreviewedPaste(draft.databases.at(-1))) draft.databases.pop();
7753
+ if (value === "finish") draft.databasesFinished = true;
7754
+ else if (value !== null) draft.databases.push({
7755
+ engine: value,
7756
+ uri: null,
7757
+ fields: {},
7758
+ name: null,
7759
+ pasted: false,
7760
+ reviewed: false
7761
+ });
7762
+ }
6708
7763
  function applyDatabaseRecord(draft, record) {
6709
7764
  const value = stringOf(record.answer);
7765
+ if (record.key === DATABASE_KEYS.connection) {
7766
+ applyConnectionRecord(draft, value);
7767
+ return;
7768
+ }
6710
7769
  if (record.key === DATABASE_KEYS.engine) {
6711
- if (value === "finish") draft.databasesFinished = true;
6712
- else if (value !== null) draft.databases.push({
6713
- engine: value,
6714
- uri: null,
6715
- fields: {},
6716
- name: null
6717
- });
7770
+ applyEngineRecord(draft, value);
6718
7771
  return;
6719
7772
  }
6720
7773
  const database = draft.databases.at(-1);
6721
7774
  if (database === void 0) return;
7775
+ if (record.key === DATABASE_KEYS.detectedEngine && value !== null) {
7776
+ database.engine = value;
7777
+ return;
7778
+ }
7779
+ if (record.key === DATABASE_KEYS.action) {
7780
+ database.reviewed = true;
7781
+ return;
7782
+ }
6722
7783
  if (record.key === DATABASE_KEYS.uri && value !== null) {
6723
7784
  database.uri = value;
6724
7785
  return;
@@ -6760,11 +7821,17 @@ function applyRecord(draft, record) {
6760
7821
  function writableUri(uri, index) {
6761
7822
  return URI_WITH_PASSWORD.test(uri) ? envReference(databaseEnv(index, "uri")) : uri;
6762
7823
  }
7824
+ function engineOf(database) {
7825
+ if (database.engine !== null) return database.engine;
7826
+ const [only, ...others] = database.uri === null ? [] : detectEngine(database.uri);
7827
+ return only !== void 0 && others.length === 0 ? only : null;
7828
+ }
6763
7829
  function databaseEntry(database, index) {
6764
7830
  const hasFields = Object.keys(database.fields).length > 0;
6765
- if (database.uri === null && !hasFields) return null;
7831
+ const engine = engineOf(database);
7832
+ if (database.uri === null && !hasFields || engine === null) return null;
6766
7833
  return {
6767
- engine: database.engine,
7834
+ engine,
6768
7835
  ...database.uri !== null && { uri: writableUri(database.uri, index) },
6769
7836
  ...database.uri === null && { fields: database.fields },
6770
7837
  ...database.name !== null && { name: database.name }
@@ -6789,122 +7856,6 @@ async function writeAnswersFile(path, answers) {
6789
7856
  await promises.writeFile(path, JSON.stringify(answers, null, 2) + "\n", { mode: FILE_MODE });
6790
7857
  }
6791
7858
  //#endregion
6792
- //#region src/wizard/answers.ts
6793
- function isOneOf(answer, kinds) {
6794
- return kinds.some((kind) => kind === answer.kind);
6795
- }
6796
- function rejected(key, prompt, reason) {
6797
- return new ConfigError(`answers file: "${key}" for "${prompt.message}" ${reason}`);
6798
- }
6799
- function expectKind(taken, prompt, kinds) {
6800
- if (taken.answer.kind === "cancel") throw new AbortError();
6801
- if (isOneOf(taken.answer, kinds)) return taken.answer;
6802
- throw rejected(taken.key, prompt, `is a ${taken.answer.kind} answer; expected ${kinds.join(" or ")}`);
6803
- }
6804
- function choiceLabels(choices) {
6805
- return choices.map((choice) => String(choice.value)).join(", ");
6806
- }
6807
- function matchChoice(key, prompt, value) {
6808
- const match = prompt.choices.find((choice) => choice.value === value);
6809
- if (match === void 0) throw rejected(key, prompt, `is ${String(value)}, not one of the choices ${choiceLabels(prompt.choices)}`);
6810
- return match.value;
6811
- }
6812
- function textValue(taken, prompt) {
6813
- const answer = expectKind(taken, prompt, ["text"]);
6814
- const value = answer.value.length === 0 && prompt.defaultValue !== void 0 ? prompt.defaultValue : answer.value;
6815
- const problem = prompt.validate?.(value);
6816
- if (problem !== void 0) throw rejected(taken.key, prompt, `is "${value}", which is rejected: ${problem}`);
6817
- return value;
6818
- }
6819
- function passwordValue(taken, prompt) {
6820
- const answer = expectKind(taken, prompt, ["password", "text"]);
6821
- const problem = prompt.validate?.(answer.value);
6822
- if (problem !== void 0) throw rejected(taken.key, prompt, `is rejected: ${problem}`);
6823
- return answer.value;
6824
- }
6825
- function isBackAnswer(answer) {
6826
- return answer.kind === "back";
6827
- }
6828
- function answeringPrompts(base, answers, options = {}) {
6829
- function lookup(prompt) {
6830
- if (prompt.key === void 0) return null;
6831
- const answer = answers.get(prompt.key)?.shift();
6832
- return answer === void 0 ? null : {
6833
- key: prompt.key,
6834
- answer
6835
- };
6836
- }
6837
- function refuseUnanswered(prompt) {
6838
- const refusal = options.unanswered?.(prompt) ?? null;
6839
- if (refusal !== null) throw refusal;
6840
- }
6841
- function take(prompt) {
6842
- const taken = lookup(prompt);
6843
- if (taken === null) refuseUnanswered(prompt);
6844
- return taken;
6845
- }
6846
- function takeText(prompt) {
6847
- const taken = lookup(prompt);
6848
- if (taken !== null) return taken;
6849
- if (prompt.defaultValue !== void 0 && options.unanswered !== void 0) return {
6850
- key: prompt.key ?? prompt.message,
6851
- answer: {
6852
- kind: "text",
6853
- value: ""
6854
- }
6855
- };
6856
- refuseUnanswered(prompt);
6857
- return null;
6858
- }
6859
- function selected(taken, prompt) {
6860
- const answer = expectKind(taken, prompt, ["select", "text"]);
6861
- return matchChoice(taken.key, prompt, answer.value);
6862
- }
6863
- function multiselected(taken, prompt) {
6864
- return expectKind(taken, prompt, ["multiselect"]).values.map((value) => matchChoice(taken.key, prompt, value));
6865
- }
6866
- return {
6867
- async text(opts) {
6868
- const taken = takeText(opts);
6869
- return taken === null ? base.text(opts) : textValue(taken, opts);
6870
- },
6871
- async textOrBack(opts) {
6872
- const taken = takeText(opts);
6873
- if (taken === null) return base.textOrBack(opts);
6874
- return isBackAnswer(taken.answer) ? BACK : textValue(taken, opts);
6875
- },
6876
- async password(opts) {
6877
- const taken = take(opts);
6878
- return taken === null ? base.password(opts) : passwordValue(taken, opts);
6879
- },
6880
- async confirm(opts) {
6881
- const taken = lookup(opts);
6882
- if (taken !== null) return expectKind(taken, opts, ["confirm"]).value;
6883
- if (options.confirmAll === true) return true;
6884
- refuseUnanswered(opts);
6885
- return base.confirm(opts);
6886
- },
6887
- async select(opts) {
6888
- const taken = take(opts);
6889
- return taken === null ? base.select(opts) : selected(taken, opts);
6890
- },
6891
- async selectOrBack(opts) {
6892
- const taken = take(opts);
6893
- if (taken === null) return base.selectOrBack(opts);
6894
- return isBackAnswer(taken.answer) ? BACK : selected(taken, opts);
6895
- },
6896
- async multiselect(opts) {
6897
- const taken = take(opts);
6898
- return taken === null ? base.multiselect(opts) : multiselected(taken, opts);
6899
- },
6900
- async multiselectOrBack(opts) {
6901
- const taken = take(opts);
6902
- if (taken === null) return base.multiselectOrBack(opts);
6903
- return isBackAnswer(taken.answer) ? BACK : multiselected(taken, opts);
6904
- }
6905
- };
6906
- }
6907
- //#endregion
6908
7859
  //#region src/wizard/record.ts
6909
7860
  function recordingPrompts(base) {
6910
7861
  const records = [];
@@ -6981,9 +7932,11 @@ async function combinedAnswers(options) {
6981
7932
  return mergeInitAnswers(await loadInitAnswers(options.answersPath), fromFlags);
6982
7933
  }
6983
7934
  function promptsFor(answers, yes) {
7935
+ const terminal = stdinIsTty();
6984
7936
  const answering = {
6985
- ...!stdinIsTty() && { unanswered: refusalWithoutTerminal },
6986
- ...yes && { confirmAll: true }
7937
+ ...!terminal && { unanswered: refusalWithoutTerminal },
7938
+ ...yes && { confirmAll: true },
7939
+ ...(yes || !terminal) && { takeRecommended: true }
6987
7940
  };
6988
7941
  return answeringPrompts(clackPrompts(), keyedAnswers(answers), answering);
6989
7942
  }
@@ -7027,7 +7980,7 @@ const init = defineRdeCommand({
7027
7980
  },
7028
7981
  admin: {
7029
7982
  type: "string",
7030
- description: "Admin credentials: default (admin@metabase.com / metabase123) or prompted"
7983
+ description: "Admin credentials: generated (a random password; `rde credentials` shows it) or prompted"
7031
7984
  },
7032
7985
  "admin-email": {
7033
7986
  type: "string",
@@ -7087,7 +8040,7 @@ const init = defineRdeCommand({
7087
8040
  },
7088
8041
  "mb-profile": {
7089
8042
  type: "string",
7090
- description: "mb profile for this instance when `default` already points elsewhere: default or rde"
8043
+ description: "mb profile for the default instance when `default` already points at another Metabase: default (replace it) or rde"
7091
8044
  },
7092
8045
  yes: {
7093
8046
  type: "boolean",
@@ -7136,7 +8089,7 @@ const init = defineRdeCommand({
7136
8089
  prompts: recording?.prompts ?? answering
7137
8090
  });
7138
8091
  ctx.ui.intro("rde init");
7139
- await runWizard(initSteps(stepOptionsFrom(answers)), ctx, options);
8092
+ await runWizard(initSteps(stepOptionsFrom(answers, { interactive: stdinIsTty() && !runOptions.yes })), ctx, options);
7140
8093
  if (recording !== null && printTo !== void 0) {
7141
8094
  await writeAnswersFile(printTo, answersFromRecords(recording.records, answers));
7142
8095
  ctx.ui.info(`Answers written to ${printTo}`);
@@ -7243,11 +8196,11 @@ function isNotServing(error) {
7243
8196
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7244
8197
  }
7245
8198
  async function databasesOf(ctx, deps, health) {
7246
- if (health !== "healthy" || ctx.state.apiKey === null) return null;
8199
+ if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
7247
8200
  try {
7248
- return await listDatabases(ctx.state, { client: deps.client });
8201
+ return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
7249
8202
  } catch (error) {
7250
- if (isNotServing(error)) return null;
8203
+ if (isNotServing(error) || isRejectedCredential(error)) return null;
7251
8204
  throw error;
7252
8205
  }
7253
8206
  }
@@ -7290,9 +8243,13 @@ async function collectStatus(ctx, deps) {
7290
8243
  const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
7291
8244
  const health = configured === null ? null : await healthOf(configured.url, deps);
7292
8245
  const databases = health === null ? null : await databasesOf(ctx, deps, health);
8246
+ const admin = ctx.state.admin;
7293
8247
  return {
7294
8248
  instance: ctx.state.name,
7295
8249
  url: configured?.url ?? null,
8250
+ mbProfile: ctx.state.mbProfile,
8251
+ adminEmail: admin === null ? null : admin.email,
8252
+ adminVariant: admin === null ? null : admin.kind,
7296
8253
  runtime: configured === null || runtime === null ? null : {
7297
8254
  kind: configured.runtime.kind,
7298
8255
  installed: runtime.installed,
@@ -7346,6 +8303,8 @@ function databaseLines(report) {
7346
8303
  function statusLines(report) {
7347
8304
  return [
7348
8305
  report.url === null ? `Instance ${report.instance}` : `Instance ${report.instance} — ${report.url}`,
8306
+ ...report.adminVariant === null || report.adminEmail === null ? [] : [loginLine(report.adminVariant, report.adminEmail)],
8307
+ `Metabase CLI: ${mbCommand(report.mbProfile)} <command>`,
7349
8308
  runtimeLine(report),
7350
8309
  versionLine(report),
7351
8310
  `Health: ${report.health ?? "unknown"}`,
@@ -7648,7 +8607,7 @@ async function planUninstall(options, deps) {
7648
8607
  if (jar.removal !== null) removals.push(jar.removal);
7649
8608
  if (jar.kept !== null) kept.push(jar.kept);
7650
8609
  }
7651
- const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, "rde"], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
8610
+ const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, namedMbProfile(options.instance)], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
7652
8611
  removals.push(...profiles);
7653
8612
  if (options.removeSkill) {
7654
8613
  const skill = await findSkill(deps.runner);
@@ -7657,6 +8616,11 @@ async function planUninstall(options, deps) {
7657
8616
  const dir = instanceDir(options.instance);
7658
8617
  const dirPresent = await pathExists(dir);
7659
8618
  if (!options.keepData) {
8619
+ const secrets = instanceSecretsPath(options.instance);
8620
+ if (await pathExists(secrets)) removals.push({
8621
+ label: `Stored credentials ${secrets} (the admin password and the API key)`,
8622
+ remove: () => promises.rm(secrets, { force: true })
8623
+ });
7660
8624
  if (dirPresent) removals.push({
7661
8625
  label: instanceDirLabel(dir, state),
7662
8626
  remove: () => promises.rm(dir, {
@@ -8058,7 +9022,7 @@ const upgradeCommand = defineRdeCommand({
8058
9022
  const main = defineCommand({
8059
9023
  meta: {
8060
9024
  name: "rde",
8061
- version: "0.1.3",
9025
+ version: "0.1.4",
8062
9026
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
8063
9027
  },
8064
9028
  subCommands: {
@@ -8066,6 +9030,7 @@ const main = defineCommand({
8066
9030
  start,
8067
9031
  stop,
8068
9032
  status,
9033
+ credentials,
8069
9034
  logs,
8070
9035
  open,
8071
9036
  db,