@metabase/rde 0.1.3 → 0.1.5

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 +84 -43
  2. package/dist/cli.mjs +1791 -766
  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;
@@ -1893,7 +2229,6 @@ async function chooseAfterParseError(run, engine, uri, error) {
1893
2229
  }
1894
2230
  function toReview(run, model, connection) {
1895
2231
  const mapped = mapDatabaseValues(connection.parsed);
1896
- const aliased = applyHostAlias(mapped.details, connection.engine.key, run.deps.hostAlias);
1897
2232
  const sameString = model.uri === connection.parsed.uri;
1898
2233
  return {
1899
2234
  type: "next",
@@ -1901,8 +2236,8 @@ function toReview(run, model, connection) {
1901
2236
  model: {
1902
2237
  engine: connection.engine,
1903
2238
  uri: connection.parsed.uri,
1904
- details: aliased.details,
1905
- hostRewritten: aliased.rewritten,
2239
+ details: mapped.details,
2240
+ hostRewritten: false,
1906
2241
  suggestedName: defaultDisplayName(mapped, connection.parsed, connection.engine),
1907
2242
  name: sameString ? model.name : null,
1908
2243
  fieldErrors: null
@@ -2023,11 +2358,15 @@ function reviewScreen(run) {
2023
2358
  let details = model.details;
2024
2359
  if (model.fieldErrors !== null) details = await fixReportedFields(run, chosen.fields, details, model.fieldErrors);
2025
2360
  let name = model.name;
2361
+ let hostRewritten = model.hostRewritten;
2026
2362
  for (;;) {
2027
- details = await fillRequiredFields(run.deps, chosen.fields, details);
2363
+ const aliased = applyHostAlias(await fillRequiredFields(run.deps, chosen.fields, details), chosen.engine.key, run.deps.hostAlias);
2364
+ details = aliased.details;
2365
+ hostRewritten ||= aliased.rewritten;
2028
2366
  const current = {
2029
2367
  ...model,
2030
2368
  details,
2369
+ hostRewritten,
2031
2370
  name,
2032
2371
  fieldErrors: null
2033
2372
  };
@@ -2191,19 +2530,20 @@ async function runDatabaseFlow(deps) {
2191
2530
  created: []
2192
2531
  };
2193
2532
  await runFlow({
2533
+ "connection-string": connectionStringScreen(run),
2194
2534
  "pick-engine": pickEngineScreen(run),
2195
2535
  "paste-uri": pasteUriScreen(run),
2196
2536
  review: reviewScreen(run),
2197
2537
  test: testScreen(run),
2198
2538
  create: createScreen(run),
2199
2539
  another: anotherScreen(run)
2200
- }, "pick-engine", FRESH$1);
2540
+ }, START_SCREEN[deps.start], FRESH$1);
2201
2541
  return { created: run.created };
2202
2542
  }
2203
2543
  //#endregion
2204
2544
  //#region src/steps/databases.ts
2205
2545
  const STEP_ID$9 = "databases";
2206
- const ASK_DATABASES = { requested: false };
2546
+ const SAMPLE_STANDS_IN = { request: "sample-stands-in" };
2207
2547
  function hostDatabasesDeps(signal) {
2208
2548
  return {
2209
2549
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
@@ -2214,9 +2554,9 @@ function hostDatabasesDeps(signal) {
2214
2554
  function isNotServingYet$1(error) {
2215
2555
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
2216
2556
  }
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;
2557
+ function requireApiKey$2(state, apiKey) {
2558
+ if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
2559
+ return apiKey;
2220
2560
  }
2221
2561
  function hostAliasOf(runtime) {
2222
2562
  switch (runtime.kind) {
@@ -2232,15 +2572,15 @@ function toRecord$1(database, createdAt) {
2232
2572
  createdAt
2233
2573
  };
2234
2574
  }
2235
- function authenticatedClient(state, deps) {
2575
+ function authenticatedClient(state, apiKey, deps) {
2236
2576
  const configured = requireConfiguredRuntime(state);
2237
2577
  return deps.client(configured.url, {
2238
2578
  kind: "api-key",
2239
- key: requireApiKey$2(state)
2579
+ key: requireApiKey$2(state, apiKey)
2240
2580
  });
2241
2581
  }
2242
- async function listDatabases(state, deps) {
2243
- const listed = await authenticatedClient(state, deps).database.list();
2582
+ async function listDatabases(state, apiKey, deps) {
2583
+ const listed = await authenticatedClient(state, apiKey, deps).database.list();
2244
2584
  const records = new Map(state.databases.map((record) => [record.id, record]));
2245
2585
  const onServer = listed.data.map((database) => ({
2246
2586
  id: database.id,
@@ -2259,13 +2599,14 @@ async function listDatabases(state, deps) {
2259
2599
  }));
2260
2600
  return [...onServer, ...missing];
2261
2601
  }
2262
- async function addDatabases(ctx, deps) {
2602
+ async function connectDatabases(ctx, deps, start) {
2263
2603
  const configured = requireConfiguredRuntime(ctx.state);
2264
- const client = authenticatedClient(ctx.state, deps);
2604
+ const client = authenticatedClient(ctx.state, ctx.secrets.apiKey, deps);
2265
2605
  const properties = await client.session.properties();
2266
2606
  const existing = await client.database.list();
2267
2607
  if (existing.data.length > 0) ctx.ui.note(existingDatabaseLines(existing.data).join("\n"), "Connected databases");
2268
2608
  const result = await runDatabaseFlow({
2609
+ start,
2269
2610
  prompts: ctx.prompts,
2270
2611
  ui: ctx.ui,
2271
2612
  catalog: engineCatalog(properties.engines),
@@ -2281,7 +2622,17 @@ async function addDatabases(ctx, deps) {
2281
2622
  });
2282
2623
  }
2283
2624
  });
2284
- return existing.data.length + result.created.length > 0 ? "completed" : "skipped";
2625
+ return {
2626
+ existing: existing.data.length,
2627
+ created: result.created.length
2628
+ };
2629
+ }
2630
+ async function addDatabases(ctx, deps) {
2631
+ const connected = await connectDatabases(ctx, deps, "engine");
2632
+ return connected.existing + connected.created > 0 ? "completed" : "skipped";
2633
+ }
2634
+ async function askForOwnDatabase(ctx, deps) {
2635
+ return (await connectDatabases(ctx, deps, "connection-string")).created > 0 ? "completed" : "skipped";
2285
2636
  }
2286
2637
  function hostDeps$12(ctx) {
2287
2638
  return hostDatabasesDeps(ctx.signal);
@@ -2289,6 +2640,9 @@ function hostDeps$12(ctx) {
2289
2640
  function sampleDatabaseId(state) {
2290
2641
  return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
2291
2642
  }
2643
+ function skippedBefore(state) {
2644
+ return state.steps[STEP_ID$9]?.status === "skipped";
2645
+ }
2292
2646
  function ownDatabaseIds(state) {
2293
2647
  const sampleId = sampleDatabaseId(state);
2294
2648
  return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
@@ -2299,21 +2653,28 @@ function databasesStep(options, deps = hostDeps$12) {
2299
2653
  title: "Connect your databases",
2300
2654
  async detect(ctx) {
2301
2655
  const configured = configuredRuntime(ctx.state);
2302
- if (configured === null || ctx.state.apiKey === null) return "todo";
2656
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
2303
2657
  const own = ownDatabaseIds(ctx.state);
2304
- if (own.size === 0) return sampleDatabaseId(ctx.state) !== null && !options.requested ? "done" : "todo";
2658
+ const sampleId = sampleDatabaseId(ctx.state);
2659
+ const asking = options.request === "ask-once" && !skippedBefore(ctx.state);
2660
+ if (own.size === 0 && !asking) {
2661
+ if (options.request === "named") return "todo";
2662
+ return sampleId !== null || skippedBefore(ctx.state) ? "done" : "todo";
2663
+ }
2664
+ const isOwn = (id) => own.size > 0 ? own.has(id) : id !== sampleId;
2305
2665
  const client = deps(ctx).client(configured.url, {
2306
2666
  kind: "api-key",
2307
- key: ctx.state.apiKey
2667
+ key: ctx.secrets.apiKey
2308
2668
  });
2309
2669
  try {
2310
- return (await client.database.list()).data.some((database) => own.has(database.id)) ? "done" : "todo";
2670
+ return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
2311
2671
  } catch (error) {
2312
- if (isNotServingYet$1(error)) return "todo";
2672
+ if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
2313
2673
  throw error;
2314
2674
  }
2315
2675
  },
2316
2676
  run(ctx) {
2677
+ if (options.request === "ask-once") return askForOwnDatabase(ctx, deps(ctx));
2317
2678
  return addDatabases(ctx, deps(ctx));
2318
2679
  }
2319
2680
  };
@@ -2391,6 +2752,11 @@ async function createWizardContext(instanceName, options) {
2391
2752
  await writeInstanceState(next);
2392
2753
  ctx.state = next;
2393
2754
  },
2755
+ secrets: await readInstanceSecrets(instanceName) ?? emptyInstanceSecrets(),
2756
+ saveSecrets: async (next) => {
2757
+ await writeInstanceSecrets(instanceName, next);
2758
+ ctx.secrets = next;
2759
+ },
2394
2760
  prompts: options.prompts ?? clackPrompts(),
2395
2761
  ui: clackUi(),
2396
2762
  signal: options.signal,
@@ -2399,53 +2765,6 @@ async function createWizardContext(instanceName, options) {
2399
2765
  return ctx;
2400
2766
  }
2401
2767
  //#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
2768
  //#region src/commands/db/add.ts
2450
2769
  const dbAdd = defineRdeCommand({
2451
2770
  meta: {
@@ -2455,19 +2774,10 @@ const dbAdd = defineRdeCommand({
2455
2774
  async run() {
2456
2775
  const name = await defaultInstanceName();
2457
2776
  await requireInstanceState(name);
2458
- const ctx = await createWizardContext(name, { signal: interruptSignal });
2459
- await databasesStep(ASK_DATABASES).run(ctx);
2777
+ await addDatabases(await createWizardContext(name, { signal: interruptSignal }), hostDatabasesDeps(interruptSignal));
2460
2778
  }
2461
2779
  });
2462
2780
  //#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
2781
  //#region src/commands/db/list.ts
2472
2782
  const dbList = defineRdeCommand({
2473
2783
  meta: {
@@ -2480,7 +2790,10 @@ const dbList = defineRdeCommand({
2480
2790
  default: false
2481
2791
  } },
2482
2792
  async run(args) {
2483
- const listing = await listDatabases(await requireInstanceState(await defaultInstanceName()), hostDatabasesDeps(interruptSignal));
2793
+ const name = await defaultInstanceName();
2794
+ const state = await requireInstanceState(name);
2795
+ const secrets = await readInstanceSecrets(name);
2796
+ const listing = await listDatabases(state, secrets === null ? null : secrets.apiKey, hostDatabasesDeps(interruptSignal));
2484
2797
  if (args.json) {
2485
2798
  printJson(listing);
2486
2799
  return;
@@ -2809,15 +3122,20 @@ async function isExecutable(path) {
2809
3122
  return false;
2810
3123
  }
2811
3124
  }
2812
- async function whichCommand(name) {
3125
+ async function whichAll(name) {
2813
3126
  const searchPath = readEnv(ENV_PATH);
2814
- if (searchPath === void 0) return null;
2815
- for (const dir of searchPath.split(delimiter)) {
3127
+ if (searchPath === void 0) return [];
3128
+ const found = [];
3129
+ for (const dir of new Set(searchPath.split(delimiter))) {
2816
3130
  if (dir.length === 0) continue;
2817
3131
  const candidate = join(dir, name);
2818
- if (await isExecutable(candidate)) return candidate;
3132
+ if (await isExecutable(candidate)) found.push(candidate);
2819
3133
  }
2820
- return null;
3134
+ return found;
3135
+ }
3136
+ async function whichCommand(name) {
3137
+ const [first] = await whichAll(name);
3138
+ return first ?? null;
2821
3139
  }
2822
3140
  //#endregion
2823
3141
  //#region src/tools/java.ts
@@ -3532,173 +3850,148 @@ function sampleDatabaseContainer(state, docker) {
3532
3850
  };
3533
3851
  }
3534
3852
  //#endregion
3535
- //#region src/tools/agents.ts
3536
- const AgentId = z.enum([
3537
- "claude-code",
3538
- "codex",
3539
- "cursor",
3540
- "gemini-cli",
3541
- "opencode",
3542
- "windsurf"
3543
- ]);
3544
- const AGENTS = [
3545
- {
3546
- id: "claude-code",
3547
- displayName: "Claude Code",
3548
- skillsCliName: "claude-code",
3549
- command: "claude",
3550
- launch: "claude",
3551
- configDir: (home) => join(home, ".claude")
3552
- },
3553
- {
3554
- id: "codex",
3555
- displayName: "Codex",
3556
- skillsCliName: "codex",
3557
- command: "codex",
3558
- launch: "codex",
3559
- configDir: (home) => join(home, ".codex")
3560
- },
3561
- {
3562
- id: "cursor",
3563
- displayName: "Cursor",
3564
- skillsCliName: "cursor",
3565
- command: "cursor",
3566
- launch: "cursor .",
3567
- configDir: (home) => join(home, ".cursor")
3568
- },
3569
- {
3570
- id: "gemini-cli",
3571
- displayName: "Gemini CLI",
3572
- skillsCliName: "gemini-cli",
3573
- command: "gemini",
3574
- launch: "gemini",
3575
- configDir: (home) => join(home, ".gemini")
3576
- },
3577
- {
3578
- id: "opencode",
3579
- displayName: "OpenCode",
3580
- skillsCliName: "opencode",
3581
- command: "opencode",
3582
- launch: "opencode",
3583
- configDir: (_home, configHome) => join(configHome, "opencode")
3584
- },
3585
- {
3586
- id: "windsurf",
3587
- displayName: "Windsurf",
3588
- skillsCliName: "windsurf",
3589
- command: null,
3590
- launch: "windsurf .",
3591
- configDir: (home) => join(home, ".codeium", "windsurf")
3592
- }
3593
- ];
3594
- async function directoryExists$1(path) {
3595
- try {
3596
- return (await promises.stat(path)).isDirectory();
3597
- } catch {
3598
- return false;
3599
- }
3600
- }
3601
- function hostAgentProbe() {
3602
- const home = homedir();
3603
- return {
3604
- commandExists: async (command) => await whichCommand(command) !== null,
3605
- directoryExists: directoryExists$1,
3606
- home,
3607
- configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
3608
- };
3609
- }
3610
- async function isDetected(agent, probe) {
3611
- if (agent.command !== null && await probe.commandExists(agent.command)) return true;
3612
- return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
3613
- }
3614
- async function detectAgents(probe) {
3615
- return Promise.all(AGENTS.map(async (agent) => ({
3616
- id: agent.id,
3617
- displayName: agent.displayName,
3618
- skillsCliName: agent.skillsCliName,
3619
- launch: agent.launch,
3620
- detected: await isDetected(agent, probe)
3621
- })));
3622
- }
3623
- //#endregion
3624
3853
  //#region src/tools/mb.ts
3625
3854
  const MB = "mb";
3626
- const MB_INSTALL_SPEC = `@metabase/cli@alpha-transform-tests`;
3855
+ const MB_INSTALL_SPEC = `@metabase/cli@alpha-fix-transform-test-ee-path`;
3627
3856
  const GLOBAL_INSTALL = {
3628
3857
  npm: {
3629
3858
  command: "npm",
3630
- args: [
3631
- "install",
3632
- "-g",
3633
- MB_INSTALL_SPEC
3634
- ]
3859
+ args: ["install", "-g"]
3635
3860
  },
3636
3861
  pnpm: {
3637
3862
  command: "pnpm",
3638
- args: [
3639
- "add",
3640
- "-g",
3641
- MB_INSTALL_SPEC
3642
- ]
3863
+ args: ["add", "-g"]
3643
3864
  },
3644
3865
  yarn: {
3645
3866
  command: "yarn",
3646
- args: [
3647
- "global",
3648
- "add",
3649
- MB_INSTALL_SPEC
3650
- ]
3867
+ args: ["global", "add"]
3651
3868
  },
3652
3869
  bun: {
3653
3870
  command: "bun",
3654
- args: [
3655
- "add",
3656
- "-g",
3657
- MB_INSTALL_SPEC
3658
- ]
3871
+ args: ["add", "-g"]
3659
3872
  }
3660
3873
  };
3874
+ function globalInstallCommand(packageManager, spec) {
3875
+ const install = GLOBAL_INSTALL[packageManager];
3876
+ return {
3877
+ command: install.command,
3878
+ args: [...install.args, spec]
3879
+ };
3880
+ }
3661
3881
  function mbInstallCommand(packageManager) {
3662
- return GLOBAL_INSTALL[packageManager];
3882
+ return globalInstallCommand(packageManager, MB_INSTALL_SPEC);
3883
+ }
3884
+ function ownerByMarker(path, markers) {
3885
+ const normalized = path.replaceAll("\\", "/");
3886
+ const found = markers.find(({ marker }) => normalized.includes(marker));
3887
+ return found === void 0 ? null : found.owner;
3663
3888
  }
3664
3889
  const GLOBAL_MARKERS = [
3665
3890
  {
3666
3891
  marker: "/.bun/install/global/node_modules/",
3667
- packageManager: "bun"
3892
+ owner: "bun"
3668
3893
  },
3669
3894
  {
3670
3895
  marker: "/Library/pnpm/global/",
3671
- packageManager: "pnpm"
3896
+ owner: "pnpm"
3672
3897
  },
3673
3898
  {
3674
3899
  marker: "/.local/share/pnpm/global/",
3675
- packageManager: "pnpm"
3900
+ owner: "pnpm"
3676
3901
  },
3677
3902
  {
3678
3903
  marker: "/share/pnpm/global/",
3679
- packageManager: "pnpm"
3904
+ owner: "pnpm"
3680
3905
  },
3681
3906
  {
3682
3907
  marker: "/pnpm-global/node_modules/",
3683
- packageManager: "pnpm"
3908
+ owner: "pnpm"
3684
3909
  },
3685
3910
  {
3686
3911
  marker: "/.config/yarn/global/node_modules/",
3687
- packageManager: "yarn"
3912
+ owner: "yarn"
3688
3913
  },
3689
3914
  {
3690
3915
  marker: "/lib/node_modules/",
3691
- packageManager: "npm"
3916
+ owner: "npm"
3692
3917
  },
3693
3918
  {
3694
3919
  marker: "/.npm-global/node_modules/",
3695
- packageManager: "npm"
3920
+ owner: "npm"
3696
3921
  }
3697
3922
  ];
3698
3923
  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";
3924
+ return ownerByMarker(scriptPath, GLOBAL_MARKERS) ?? "npm";
3925
+ }
3926
+ const MB_OWNER_NAMES = {
3927
+ npm: "npm",
3928
+ pnpm: "pnpm",
3929
+ yarn: "Yarn",
3930
+ bun: "Bun",
3931
+ homebrew: "Homebrew"
3932
+ };
3933
+ const CELLAR = "/Cellar/";
3934
+ const NODE_MODULES = "/node_modules/";
3935
+ const OWNER_MARKERS = [
3936
+ ...GLOBAL_MARKERS,
3937
+ {
3938
+ marker: CELLAR,
3939
+ owner: "homebrew"
3940
+ },
3941
+ {
3942
+ marker: "/Library/pnpm/",
3943
+ owner: "pnpm"
3944
+ },
3945
+ {
3946
+ marker: "/.local/share/pnpm/",
3947
+ owner: "pnpm"
3948
+ }
3949
+ ];
3950
+ async function locateMbOnPath() {
3951
+ const paths = await whichAll(MB);
3952
+ return Promise.all(paths.map(async (path) => ({
3953
+ path,
3954
+ resolved: await promises.realpath(path)
3955
+ })));
3956
+ }
3957
+ function mbOwner(binary) {
3958
+ return ownerByMarker(binary.resolved, OWNER_MARKERS) ?? ownerByMarker(binary.path, OWNER_MARKERS);
3959
+ }
3960
+ function firstSegmentAfter(path, marker) {
3961
+ const at = path.lastIndexOf(marker);
3962
+ if (at === -1) return null;
3963
+ const [first] = path.slice(at + marker.length).split("/");
3964
+ return first === void 0 || first.length === 0 ? null : first;
3965
+ }
3966
+ function packageName(resolved) {
3967
+ const scopeOrName = firstSegmentAfter(resolved, NODE_MODULES);
3968
+ if (scopeOrName === null || !scopeOrName.startsWith("@")) return scopeOrName;
3969
+ const name = firstSegmentAfter(resolved, `${NODE_MODULES}${scopeOrName}/`);
3970
+ return name === null ? null : `${scopeOrName}/${name}`;
3971
+ }
3972
+ const REMOVE_COMMANDS = {
3973
+ npm: (name) => `npm uninstall -g ${name}`,
3974
+ pnpm: (name) => `pnpm remove -g ${name}`,
3975
+ yarn: (name) => `yarn global remove ${name}`,
3976
+ bun: (name) => `bun remove -g ${name}`,
3977
+ homebrew: (name) => `brew uninstall ${name}`
3978
+ };
3979
+ function mbRemoveCommand(binary) {
3980
+ const owner = mbOwner(binary);
3981
+ if (owner === null) return null;
3982
+ const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
3983
+ return name === null ? null : REMOVE_COMMANDS[owner](name);
3984
+ }
3985
+ function setValue(name) {
3986
+ const value = readEnv(name);
3987
+ return value === void 0 || value.length === 0 ? null : value;
3988
+ }
3989
+ function mbEnvironment() {
3990
+ return {
3991
+ profile: setValue(ENV_MB_PROFILE),
3992
+ url: setValue(ENV_MB_URL),
3993
+ apiKeySet: setValue(ENV_MB_API_KEY) !== null
3994
+ };
3702
3995
  }
3703
3996
  const ProbedUser = z.object({
3704
3997
  id: z.int(),
@@ -3775,16 +4068,16 @@ function mbTool(runner) {
3775
4068
  if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
3776
4069
  return parseJsonOutput(MB, argv, result.stdout, schema);
3777
4070
  }
3778
- async function version() {
4071
+ async function version(command = MB) {
3779
4072
  const args = ["--version"];
3780
4073
  let result;
3781
4074
  try {
3782
- result = await runner.run(MB, args);
4075
+ result = await runner.run(command, args);
3783
4076
  } catch (error) {
3784
4077
  if (error instanceof ProcessNotFoundError) return null;
3785
4078
  throw error;
3786
4079
  }
3787
- if (result.exitCode !== 0) throw toolFailure(MB, args, result);
4080
+ if (result.exitCode !== 0) throw toolFailure(command, args, result);
3788
4081
  return result.stdout.trim();
3789
4082
  }
3790
4083
  async function authLogin(options) {
@@ -3870,65 +4163,107 @@ function mbTool(runner) {
3870
4163
  };
3871
4164
  }
3872
4165
  //#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(", ")}`);
4166
+ //#region src/tools/agents.ts
4167
+ const AgentId = z.enum([
4168
+ "claude-code",
4169
+ "codex",
4170
+ "cursor",
4171
+ "gemini-cli",
4172
+ "opencode",
4173
+ "windsurf"
4174
+ ]);
4175
+ const OPENCODE_PACKAGE = "opencode-ai@latest";
4176
+ function opencodeInstallCommand(packageManager) {
4177
+ return globalInstallCommand(packageManager, OPENCODE_PACKAGE);
3877
4178
  }
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
- });
4179
+ const AGENTS = [
4180
+ {
4181
+ id: "claude-code",
4182
+ displayName: "Claude Code",
4183
+ skillsCliName: "claude-code",
4184
+ command: "claude",
4185
+ launch: "claude",
4186
+ configDir: (home) => join(home, ".claude")
4187
+ },
4188
+ {
4189
+ id: "codex",
4190
+ displayName: "Codex",
4191
+ skillsCliName: "codex",
4192
+ command: "codex",
4193
+ launch: "codex",
4194
+ configDir: (home) => join(home, ".codex")
4195
+ },
4196
+ {
4197
+ id: "cursor",
4198
+ displayName: "Cursor",
4199
+ skillsCliName: "cursor",
4200
+ command: "cursor",
4201
+ launch: "cursor .",
4202
+ configDir: (home) => join(home, ".cursor")
4203
+ },
4204
+ {
4205
+ id: "gemini-cli",
4206
+ displayName: "Gemini CLI",
4207
+ skillsCliName: "gemini-cli",
4208
+ command: "gemini",
4209
+ launch: "gemini",
4210
+ configDir: (home) => join(home, ".gemini")
4211
+ },
4212
+ {
4213
+ id: "opencode",
4214
+ displayName: "OpenCode",
4215
+ skillsCliName: "opencode",
4216
+ command: "opencode",
4217
+ launch: "opencode",
4218
+ configDir: (_home, configHome) => join(configHome, "opencode")
4219
+ },
4220
+ {
4221
+ id: "windsurf",
4222
+ displayName: "Windsurf",
4223
+ skillsCliName: "windsurf",
4224
+ command: null,
4225
+ launch: "windsurf .",
4226
+ configDir: (home) => join(home, ".codeium", "windsurf")
4227
+ }
4228
+ ];
4229
+ async function directoryExists$1(path) {
4230
+ try {
4231
+ return (await promises.stat(path)).isDirectory();
4232
+ } catch {
4233
+ return false;
3889
4234
  }
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
4235
  }
3897
- function recordFor(outcome, at) {
4236
+ function hostAgentProbe() {
4237
+ const home = homedir();
3898
4238
  return {
3899
- status: outcome === "completed" ? "done" : "skipped",
3900
- completedAt: at.toISOString()
4239
+ commandExists: async (command) => await whichCommand(command) !== null,
4240
+ directoryExists: directoryExists$1,
4241
+ home,
4242
+ configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
3901
4243
  };
3902
4244
  }
3903
- function withStepRecord(state, id, record) {
3904
- return {
3905
- ...state,
3906
- steps: {
3907
- ...state.steps,
3908
- [id]: record
3909
- }
3910
- };
4245
+ async function isDetected(agent, probe) {
4246
+ if (agent.command !== null && await probe.commandExists(agent.command)) return true;
4247
+ return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
3911
4248
  }
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
4249
+ async function detectAgents(probe) {
4250
+ return Promise.all(AGENTS.map(async (agent) => ({
4251
+ id: agent.id,
4252
+ displayName: agent.displayName,
4253
+ skillsCliName: agent.skillsCliName,
4254
+ launch: agent.launch,
4255
+ detected: await isDetected(agent, probe)
3920
4256
  })));
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
4257
  }
3927
4258
  //#endregion
3928
4259
  //#region src/tools/skills-cli.ts
3929
4260
  const NPX = "npx";
3930
4261
  const SKILLS_PACKAGE = "skills@latest";
3931
- const RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-generic-skills";
4262
+ const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-skill";
4263
+ function rdeSkillSource() {
4264
+ const override = readEnv(ENV_RDE_SKILL_SOURCE);
4265
+ return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
4266
+ }
3932
4267
  const InstalledSkill = z.object({
3933
4268
  name: z.string(),
3934
4269
  path: z.string(),
@@ -3937,15 +4272,58 @@ const InstalledSkill = z.object({
3937
4272
  source: z.string().nullable()
3938
4273
  }).loose();
3939
4274
  const InstalledSkillList = z.array(InstalledSkill);
3940
- function skillsCli(runner) {
3941
- async function add(options) {
3942
- const argv = [
3943
- "-y",
3944
- SKILLS_PACKAGE,
3945
- "add",
3946
- options.source,
3947
- "--skill",
3948
- options.skill,
4275
+ const SkillLock = z.object({ skills: z.record(z.string(), z.object({ ref: z.string().nullish() }).loose()) }).loose();
4276
+ const GLOBAL_LOCK_FILE = ".skill-lock.json";
4277
+ const PROJECT_LOCK_FILE = "skills-lock.json";
4278
+ function globalSkillLockPath(home, stateHome) {
4279
+ return stateHome === null ? join(home, ".agents", GLOBAL_LOCK_FILE) : join(stateHome, "skills", GLOBAL_LOCK_FILE);
4280
+ }
4281
+ function projectSkillLockPath(project) {
4282
+ return join(project, PROJECT_LOCK_FILE);
4283
+ }
4284
+ function xdgStateHome() {
4285
+ const value = readEnv(ENV_XDG_STATE_HOME);
4286
+ return value === void 0 || value.length === 0 ? null : value;
4287
+ }
4288
+ function lockedRef(path, raw, skill) {
4289
+ if (raw === null) return null;
4290
+ const parsed = SkillLock.safeParse(raw);
4291
+ if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
4292
+ return parsed.data.skills[skill]?.ref ?? null;
4293
+ }
4294
+ const hostSkillFiles = {
4295
+ readJson: readJsonFile,
4296
+ readText: readTextFile
4297
+ };
4298
+ const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
4299
+ function skillOrigin(source, cwd) {
4300
+ const github = GITHUB_URL.exec(source);
4301
+ if (github !== null) {
4302
+ const [, owner, repo, ref] = github;
4303
+ return {
4304
+ kind: "github",
4305
+ repository: `${owner}/${repo}`,
4306
+ ref: ref ?? null
4307
+ };
4308
+ }
4309
+ if (isAbsolute(source) || source.startsWith(".")) return {
4310
+ kind: "local",
4311
+ path: resolve(cwd, source)
4312
+ };
4313
+ return {
4314
+ kind: "other",
4315
+ source
4316
+ };
4317
+ }
4318
+ function skillsCli(runner) {
4319
+ async function add(options) {
4320
+ const argv = [
4321
+ "-y",
4322
+ SKILLS_PACKAGE,
4323
+ "add",
4324
+ options.source,
4325
+ "--skill",
4326
+ options.skill,
3949
4327
  "-a",
3950
4328
  ...options.agents,
3951
4329
  "-g",
@@ -3988,97 +4366,85 @@ function skillsCli(runner) {
3988
4366
  if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
3989
4367
  return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
3990
4368
  }
4369
+ async function listProject(cwd) {
4370
+ const argv = [
4371
+ "-y",
4372
+ SKILLS_PACKAGE,
4373
+ "list",
4374
+ "--json"
4375
+ ];
4376
+ const result = await runner.run(NPX, argv, {
4377
+ cwd,
4378
+ stdoutToFile: true
4379
+ });
4380
+ if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
4381
+ return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
4382
+ }
3991
4383
  return {
3992
4384
  add,
3993
4385
  remove,
3994
- list
4386
+ list,
4387
+ listProject
3995
4388
  };
3996
4389
  }
3997
4390
  //#endregion
3998
- //#region src/steps/agents-skills.ts
3999
- const STEP_ID$8 = "agents-skills";
4000
- const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
4001
- const DETECT_AGENTS = { agents: null };
4002
- function hostDeps$11(ctx) {
4003
- return {
4004
- runner: processRunner(ctx.signal),
4005
- probe: hostAgentProbe()
4006
- };
4007
- }
4008
- function detectedOnly(agents) {
4009
- return agents.filter((agent) => agent.detected);
4010
- }
4011
- function displayNames(agents) {
4012
- return agents.map((agent) => agent.displayName).join(", ");
4013
- }
4014
- function skillCoversAgent(entry, agent) {
4015
- return entry.agents.includes(agent.displayName);
4016
- }
4017
- function coversAny(entry, agents) {
4018
- return agents.some((agent) => skillCoversAgent(entry, agent));
4391
+ //#region src/wizard/engine.ts
4392
+ function requireKnownStep(steps, id, flag) {
4393
+ if (id === void 0) return;
4394
+ if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
4019
4395
  }
4020
- async function rdeSkillEntries(runner) {
4021
- return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
4396
+ async function detectAll(steps, ctx, options) {
4397
+ const detected = [];
4398
+ let fromHere = false;
4399
+ for (const step of steps) {
4400
+ if (step.id === options.from) fromHere = true;
4401
+ const forced = options.only !== void 0 ? step.id === options.only : fromHere;
4402
+ detected.push({
4403
+ step,
4404
+ status: forced ? "todo" : await step.detect(ctx),
4405
+ forced
4406
+ });
4407
+ }
4408
+ return detected;
4022
4409
  }
4023
- function installedLines(entries) {
4024
- return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
4410
+ async function shouldRun(entry, ctx, options) {
4411
+ if (entry.forced) return true;
4412
+ if (options.only !== void 0 || entry.status === "done") return false;
4413
+ return await entry.step.detect(ctx) === "todo";
4025
4414
  }
4026
- const SOURCE_NOTES = {
4027
- override: "from --agents",
4028
- detected: "detected on this machine"
4029
- };
4030
- function selectAgents(all, options) {
4031
- if (options.agents === null) return {
4032
- agents: detectedOnly(all),
4033
- source: "detected"
4034
- };
4035
- const chosen = options.agents;
4415
+ function recordFor(outcome, at) {
4036
4416
  return {
4037
- agents: all.filter((agent) => chosen.includes(agent.id)),
4038
- source: "override"
4417
+ status: outcome === "completed" ? "done" : "skipped",
4418
+ completedAt: at.toISOString()
4039
4419
  };
4040
4420
  }
4041
- function skipLine(all, source) {
4042
- switch (source) {
4043
- case "override": return `Skipping the rde skill; run \`rde init --only ${STEP_ID$8}\` to install it later`;
4044
- case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$8} --agents <name>\` once one is installed`;
4045
- }
4046
- }
4047
- function agentsSkillsStep(options, deps = hostDeps$11) {
4421
+ function withStepRecord(state, id, record) {
4048
4422
  return {
4049
- id: STEP_ID$8,
4050
- title: "Install the rde skill for your coding agents",
4051
- async detect(ctx) {
4052
- const resolved = deps(ctx);
4053
- const detected = detectedOnly(await detectAgents(resolved.probe));
4054
- if (detected.length === 0) return ctx.state.steps[STEP_ID$8]?.status === "skipped" ? "not-applicable" : "todo";
4055
- return (await rdeSkillEntries(resolved.runner)).some((entry) => coversAny(entry, detected)) ? "done" : "todo";
4056
- },
4057
- async run(ctx) {
4058
- const resolved = deps(ctx);
4059
- const all = await detectAgents(resolved.probe);
4060
- const selection = selectAgents(all, options);
4061
- if (selection.agents.length === 0) {
4062
- ctx.ui.info(skipLine(all, selection.source));
4063
- return "skipped";
4064
- }
4065
- ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`);
4066
- await skillsCli(resolved.runner).add({
4067
- source: RDE_SKILL_SOURCE,
4068
- skill: "rde",
4069
- agents: selection.agents.map((agent) => agent.skillsCliName)
4070
- });
4071
- const entries = await rdeSkillEntries(resolved.runner);
4072
- if (entries.length === 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show the rde skill`);
4073
- ctx.ui.note(installedLines(entries), "rde skill installed");
4074
- ctx.ui.info(MECHANICS_NOTE);
4075
- return "completed";
4423
+ ...state,
4424
+ steps: {
4425
+ ...state.steps,
4426
+ [id]: record
4076
4427
  }
4077
4428
  };
4078
4429
  }
4430
+ async function runWizard(steps, ctx, options = {}) {
4431
+ requireKnownStep(steps, options.from, "--from");
4432
+ requireKnownStep(steps, options.only, "--only");
4433
+ const now = options.now ?? (() => /* @__PURE__ */ new Date());
4434
+ const detected = await detectAll(steps, ctx, options);
4435
+ ctx.ui.checklist(detected.map(({ step, status }) => ({
4436
+ title: step.title,
4437
+ status
4438
+ })));
4439
+ for (const entry of detected) {
4440
+ if (!await shouldRun(entry, ctx, options)) continue;
4441
+ const outcome = await entry.step.run(ctx);
4442
+ await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
4443
+ }
4444
+ }
4079
4445
  //#endregion
4080
4446
  //#region src/steps/install-mb.ts
4081
- const STEP_ID$7 = "install-mb";
4447
+ const STEP_ID$8 = "install-mb";
4082
4448
  const MIN_MB_VERSION = "0.3.0";
4083
4449
  const MB_RANGE = `>=${MIN_MB_VERSION}`;
4084
4450
  function installedWith(scriptPath) {
@@ -4087,10 +4453,11 @@ function installedWith(scriptPath) {
4087
4453
  function rdeInstalledWith() {
4088
4454
  return installedWith(process.argv[1]);
4089
4455
  }
4090
- function hostDeps$10(ctx) {
4456
+ function hostDeps$11(ctx) {
4091
4457
  return {
4092
4458
  runner: processRunner(ctx.signal),
4093
- installedWith: rdeInstalledWith()
4459
+ installedWith: rdeInstalledWith(),
4460
+ locate: locateMbOnPath
4094
4461
  };
4095
4462
  }
4096
4463
  async function inspectMb(runner) {
@@ -4107,18 +4474,104 @@ async function inspectMb(runner) {
4107
4474
  function displayCommand(install) {
4108
4475
  return [install.command, ...install.args].join(" ");
4109
4476
  }
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({
4477
+ async function versionAt(runner, path) {
4478
+ try {
4479
+ const raw = await mbTool(runner).version(path);
4480
+ return raw === null ? null : semver.valid(raw);
4481
+ } catch (error) {
4482
+ if (error instanceof ExternalToolError) return null;
4483
+ throw error;
4484
+ }
4485
+ }
4486
+ async function inspectMbOnPath(runner, locate) {
4487
+ const found = [];
4488
+ for (const binary of await locate()) found.push({
4489
+ path: binary.path,
4490
+ version: await versionAt(runner, binary.path),
4491
+ owner: mbOwner(binary),
4492
+ removeCommand: mbRemoveCommand(binary)
4493
+ });
4494
+ return found;
4495
+ }
4496
+ function satisfiesMinimum(version) {
4497
+ return version !== null && semver.satisfies(version, MB_RANGE, { includePrerelease: true });
4498
+ }
4499
+ function isNewer(candidate, than) {
4500
+ if (candidate === null) return false;
4501
+ return than === null || semver.gt(candidate, than);
4502
+ }
4503
+ function diagnoseMb(found, installer) {
4504
+ const [running, ...later] = found;
4505
+ if (running === void 0) return { kind: "missing" };
4506
+ const sufficient = satisfiesMinimum(running.version);
4507
+ const newer = later.find((candidate) => satisfiesMinimum(candidate.version) && isNewer(candidate.version, running.version));
4508
+ if (newer !== void 0 && (!sufficient || running.owner !== installer)) return {
4509
+ kind: "shadowed",
4510
+ running,
4511
+ newer
4512
+ };
4513
+ return sufficient ? {
4514
+ kind: "current",
4515
+ running
4516
+ } : {
4517
+ kind: "old",
4518
+ running
4519
+ };
4520
+ }
4521
+ function runningMbSuffices(diagnosis) {
4522
+ return diagnosis.kind !== "missing" && satisfiesMinimum(diagnosis.running.version);
4523
+ }
4524
+ function versionText(entry) {
4525
+ return entry.version ?? "an unknown version";
4526
+ }
4527
+ function describeEntry(entry) {
4528
+ const owner = entry.owner === null ? "" : ` (${MB_OWNER_NAMES[entry.owner]})`;
4529
+ return `${versionText(entry)} at ${entry.path}${owner}`;
4530
+ }
4531
+ function mbPathLines(found) {
4532
+ return found.map((entry, index) => `${index === 0 ? "" : "then "}${describeEntry(entry)}`);
4533
+ }
4534
+ function removeClause(entry) {
4535
+ if (entry.removeCommand !== null) return `remove the old one with \`${entry.removeCommand}\``;
4536
+ if (entry.owner !== null) return `remove the old one with ${MB_OWNER_NAMES[entry.owner]}`;
4537
+ return `remove ${entry.path}`;
4538
+ }
4539
+ function shadowedHint(diagnosis) {
4540
+ const { running, newer } = diagnosis;
4541
+ return [
4542
+ `${running.path} (${versionText(running)}) runs before ${newer.path} (${versionText(newer)});`,
4543
+ `${removeClause(running)}, or put ${dirname(newer.path)} before ${dirname(running.path)} on PATH,`,
4544
+ "then open a new terminal"
4545
+ ].join(" ");
4546
+ }
4547
+ const NEW_TERMINAL = "open a new terminal and run rde init again";
4548
+ function stillWrongAdvice(diagnosis) {
4549
+ switch (diagnosis.kind) {
4550
+ case "shadowed": return `${shadowedHint(diagnosis)} and run rde init again`;
4551
+ case "old": return `The install put no newer mb on PATH; ${removeClause(diagnosis.running)}, or ${NEW_TERMINAL}`;
4552
+ case "missing":
4553
+ case "current": return "Open a new terminal and run rde init again";
4554
+ }
4555
+ }
4556
+ async function stillWrong(install, after, deps) {
4557
+ const fail = (stderrTail) => new ExternalToolError({
4113
4558
  tool: install.command,
4114
4559
  argv: install.args,
4115
4560
  exitCode: 0,
4116
- stderrTail: `${found}; open a new terminal and run rde init again`
4561
+ stderrTail
4117
4562
  });
4118
- }
4119
- function installMbStep(deps = hostDeps$10) {
4563
+ if (!after.installed) return fail(`mb is not on PATH after the install; ${NEW_TERMINAL}`);
4564
+ const found = await inspectMbOnPath(deps.runner, deps.locate);
4565
+ const diagnosis = diagnoseMb(found, deps.installedWith);
4566
+ return fail([
4567
+ `mb ${after.version} still runs first (${MIN_MB_VERSION} or newer is required).`,
4568
+ `Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.`,
4569
+ `${stillWrongAdvice(diagnosis)}.`
4570
+ ].join(" "));
4571
+ }
4572
+ function installMbStep(deps = hostDeps$11) {
4120
4573
  return {
4121
- id: STEP_ID$7,
4574
+ id: STEP_ID$8,
4122
4575
  title: "Install the Metabase CLI",
4123
4576
  async detect(ctx) {
4124
4577
  const found = await inspectMb(deps(ctx).runner);
@@ -4142,13 +4595,145 @@ function installMbStep(deps = hostDeps$10) {
4142
4595
  stderrTail: ""
4143
4596
  });
4144
4597
  const after = await inspectMb(resolved.runner);
4145
- if (!after.installed || !after.satisfies) throw stillWrong(install, after);
4598
+ if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
4146
4599
  ctx.ui.success(`mb ${after.version} is installed`);
4147
4600
  return "completed";
4148
4601
  }
4149
4602
  };
4150
4603
  }
4151
4604
  //#endregion
4605
+ //#region src/steps/agents-skills.ts
4606
+ const STEP_ID$7 = "agents-skills";
4607
+ const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
4608
+ const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
4609
+ const DETECT_AGENTS = { kind: "detected" };
4610
+ function hostDeps$10(ctx) {
4611
+ return {
4612
+ runner: processRunner(ctx.signal),
4613
+ probe: hostAgentProbe(),
4614
+ skillSource: rdeSkillSource(),
4615
+ installedWith: rdeInstalledWith()
4616
+ };
4617
+ }
4618
+ function detectedOnly(agents) {
4619
+ return agents.filter((agent) => agent.detected);
4620
+ }
4621
+ function displayNames(agents) {
4622
+ return agents.map((agent) => agent.displayName).join(", ");
4623
+ }
4624
+ function skillCoversAgent(entry, agent) {
4625
+ return entry.agents.includes(agent.displayName);
4626
+ }
4627
+ function coversAny(entry, agents) {
4628
+ return agents.some((agent) => skillCoversAgent(entry, agent));
4629
+ }
4630
+ async function rdeSkillEntries(runner) {
4631
+ return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
4632
+ }
4633
+ function installedLines(entries) {
4634
+ return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
4635
+ }
4636
+ const SOURCE_NOTES = {
4637
+ override: "from --agents",
4638
+ detected: "detected on this machine",
4639
+ chosen: "as chosen"
4640
+ };
4641
+ const MISSING_OPENCODE_HINT = "not installed; rde installs it";
4642
+ function isMissingOpencode(agent) {
4643
+ return agent.id === "opencode" && !agent.detected;
4644
+ }
4645
+ function offeredChoice(agent) {
4646
+ return {
4647
+ value: agent.id,
4648
+ label: agent.displayName,
4649
+ ...isMissingOpencode(agent) && { hint: MISSING_OPENCODE_HINT }
4650
+ };
4651
+ }
4652
+ async function askAgents(ctx, all) {
4653
+ const offered = all.filter((agent) => agent.detected || agent.id === "opencode");
4654
+ const picked = await ctx.prompts.multiselect({
4655
+ key: AGENTS_SKILLS_KEYS.agents,
4656
+ message: "Which coding agents should get the rde skill?",
4657
+ choices: offered.map(offeredChoice),
4658
+ initialValues: detectedOnly(all).map((agent) => agent.id),
4659
+ required: false
4660
+ });
4661
+ return offered.filter((agent) => picked.includes(agent.id));
4662
+ }
4663
+ async function selectAgents(ctx, all, options) {
4664
+ switch (options.kind) {
4665
+ case "detected": return {
4666
+ agents: detectedOnly(all),
4667
+ source: "detected"
4668
+ };
4669
+ case "listed": {
4670
+ const listed = options.agents;
4671
+ return {
4672
+ agents: all.filter((agent) => listed.includes(agent.id)),
4673
+ source: "override"
4674
+ };
4675
+ }
4676
+ case "ask": return {
4677
+ agents: await askAgents(ctx, all),
4678
+ source: "chosen"
4679
+ };
4680
+ }
4681
+ }
4682
+ async function installOpencode(ctx, deps) {
4683
+ const install = opencodeInstallCommand(deps.installedWith);
4684
+ const commandLine = [install.command, ...install.args].join(" ");
4685
+ ctx.ui.info(`Running ${commandLine}`);
4686
+ const exitCode = await deps.runner.stream(install.command, install.args);
4687
+ if (exitCode !== 0) throw new ConfigError(`\`${commandLine}\` exited with ${exitCode}`);
4688
+ if (!(await detectAgents(deps.probe)).some((agent) => agent.id === "opencode" && agent.detected)) throw new ConfigError("OpenCode was installed but `opencode` is not on PATH; open a new terminal and run rde init again");
4689
+ }
4690
+ function listsMissingOpencode(all, options) {
4691
+ return options.kind === "listed" && options.agents.includes("opencode") && all.some(isMissingOpencode);
4692
+ }
4693
+ function skipLine(all, source) {
4694
+ switch (source) {
4695
+ case "override":
4696
+ case "chosen": return `Skipping the rde skill; run \`rde init --only ${STEP_ID$7}\` to install it later`;
4697
+ case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
4698
+ }
4699
+ }
4700
+ function agentsSkillsStep(options, deps = hostDeps$10) {
4701
+ return {
4702
+ id: STEP_ID$7,
4703
+ title: "Install the rde skill for your coding agents",
4704
+ async detect(ctx) {
4705
+ const resolved = deps(ctx);
4706
+ const all = await detectAgents(resolved.probe);
4707
+ if (listsMissingOpencode(all, options)) return "todo";
4708
+ const detected = detectedOnly(all);
4709
+ if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
4710
+ return (await rdeSkillEntries(resolved.runner)).some((entry) => coversAny(entry, detected)) ? "done" : "todo";
4711
+ },
4712
+ async run(ctx) {
4713
+ const resolved = deps(ctx);
4714
+ const all = await detectAgents(resolved.probe);
4715
+ const selection = await selectAgents(ctx, all, options);
4716
+ if (selection.agents.length === 0) {
4717
+ ctx.ui.info(skipLine(all, selection.source));
4718
+ return "skipped";
4719
+ }
4720
+ if (selection.agents.some(isMissingOpencode)) await installOpencode(ctx, resolved);
4721
+ const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-skill" ? "" : ` from ${resolved.skillSource}`;
4722
+ ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
4723
+ await skillsCli(resolved.runner).add({
4724
+ source: resolved.skillSource,
4725
+ skill: "rde",
4726
+ agents: selection.agents.map((agent) => agent.skillsCliName)
4727
+ });
4728
+ const entries = await rdeSkillEntries(resolved.runner);
4729
+ if (entries.length === 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show the rde skill`);
4730
+ ctx.ui.note(installedLines(entries), "rde skill installed");
4731
+ ctx.ui.info(MECHANICS_NOTE);
4732
+ return "completed";
4733
+ }
4734
+ };
4735
+ }
4736
+ //#endregion
4152
4737
  //#region src/tools/git.ts
4153
4738
  const GIT = "git";
4154
4739
  const FALLBACK_IDENTITY_NAME = "rde";
@@ -4463,7 +5048,15 @@ function hostDoctorDeps(instanceName, signal) {
4463
5048
  runtime,
4464
5049
  ports: hostPortProbe,
4465
5050
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
4466
- probe: hostAgentProbe()
5051
+ probe: hostAgentProbe(),
5052
+ locateMb: locateMbOnPath,
5053
+ environment: mbEnvironment(),
5054
+ skills: {
5055
+ source: rdeSkillSource(),
5056
+ cwd: process.cwd(),
5057
+ stateHome: xdgStateHome(),
5058
+ files: hostSkillFiles
5059
+ }
4467
5060
  };
4468
5061
  }
4469
5062
  const NOT_FOUND_STATUS$2 = 404;
@@ -4476,7 +5069,17 @@ function ok(id, label, detail) {
4476
5069
  status: "ok",
4477
5070
  detail,
4478
5071
  hint: null,
4479
- fix: null
5072
+ step: null
5073
+ };
5074
+ }
5075
+ function note$1(id, label, detail, hint) {
5076
+ return {
5077
+ id,
5078
+ label,
5079
+ status: "ok",
5080
+ detail,
5081
+ hint,
5082
+ step: null
4480
5083
  };
4481
5084
  }
4482
5085
  function skipped(id, label, detail) {
@@ -4486,7 +5089,7 @@ function skipped(id, label, detail) {
4486
5089
  status: "skipped",
4487
5090
  detail,
4488
5091
  hint: null,
4489
- fix: null
5092
+ step: null
4490
5093
  };
4491
5094
  }
4492
5095
  function failed(id, label, failure) {
@@ -4507,7 +5110,7 @@ function machineRow(check, needed) {
4507
5110
  return failed(check.id, check.id, {
4508
5111
  detail: check.label,
4509
5112
  hint: check.hint,
4510
- fix: "preflight"
5113
+ step: "preflight"
4511
5114
  });
4512
5115
  }
4513
5116
  async function machineRows(deps, runtime) {
@@ -4519,19 +5122,48 @@ function installHint(installedWith) {
4519
5122
  const install = mbInstallCommand(installedWith);
4520
5123
  return `\`${[install.command, ...install.args].join(" ")}\``;
4521
5124
  }
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);
5125
+ const MB_LABEL = "mb";
5126
+ function mbUnusable(diagnosis) {
5127
+ if (diagnosis.kind === "missing") return "mb is not installed";
5128
+ return runningMbSuffices(diagnosis) ? null : "the mb that runs is too old";
5129
+ }
5130
+ function mbRow(diagnosis, lines, deps) {
5131
+ switch (diagnosis.kind) {
5132
+ case "missing": return failed("mb", MB_LABEL, {
5133
+ detail: "not installed",
5134
+ hint: installHint(deps.installedWith),
5135
+ step: "install-mb"
5136
+ });
5137
+ case "old": {
5138
+ const required = lines.map((line, index) => index === 0 ? `${line}; ${MIN_MB_VERSION} or newer is required` : line);
5139
+ return failed("mb", MB_LABEL, {
5140
+ detail: required.join("\n"),
5141
+ hint: installHint(deps.installedWith),
5142
+ step: "install-mb"
5143
+ });
5144
+ }
5145
+ case "shadowed": return failed("mb", MB_LABEL, {
5146
+ detail: lines.join("\n"),
5147
+ hint: shadowedHint(diagnosis),
5148
+ step: null
5149
+ });
5150
+ case "current": return ok("mb", MB_LABEL, lines.join("\n"));
5151
+ }
5152
+ }
5153
+ async function mbFinding(deps) {
5154
+ const found = await inspectMbOnPath(deps.runner, deps.locateMb);
5155
+ const diagnosis = diagnoseMb(found, deps.installedWith);
5156
+ return {
5157
+ row: mbRow(diagnosis, mbPathLines(found), deps),
5158
+ unusable: mbUnusable(diagnosis)
5159
+ };
5160
+ }
5161
+ function sharedProfileList(deps) {
5162
+ let listed = null;
5163
+ return () => {
5164
+ listed ??= mbTool(deps.runner).authList();
5165
+ return listed;
5166
+ };
4535
5167
  }
4536
5168
  function publishesOnLoopback(container, port) {
4537
5169
  return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
@@ -4549,7 +5181,7 @@ async function containerRow(runtime, deps, dockerOk) {
4549
5181
  row: failed("container", label, {
4550
5182
  detail: `${runtime.containerName} does not exist`,
4551
5183
  hint: "`rde init` recreates it from the saved choices",
4552
- fix: "runtime"
5184
+ step: "runtime"
4553
5185
  }),
4554
5186
  running: false,
4555
5187
  holder: CONTAINER
@@ -4562,7 +5194,7 @@ async function containerRow(runtime, deps, dockerOk) {
4562
5194
  row: failed("container", label, {
4563
5195
  detail: `${runtime.containerName} runs ${inspection.image}, the state says ${runtime.image}`,
4564
5196
  hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
4565
- fix: null
5197
+ step: null
4566
5198
  }),
4567
5199
  ...found
4568
5200
  };
@@ -4570,7 +5202,7 @@ async function containerRow(runtime, deps, dockerOk) {
4570
5202
  row: failed("container", label, {
4571
5203
  detail: `${runtime.containerName} does not publish port ${runtime.port} on ${LOOPBACK_HOST}`,
4572
5204
  hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
4573
- fix: null
5205
+ step: null
4574
5206
  }),
4575
5207
  ...found
4576
5208
  };
@@ -4591,7 +5223,7 @@ async function processRow(runtime, deps) {
4591
5223
  row: failed("process", label, {
4592
5224
  detail: `${inspection.jarPath} does not exist`,
4593
5225
  hint,
4594
- fix: "runtime"
5226
+ step: "runtime"
4595
5227
  }),
4596
5228
  running: false,
4597
5229
  holder: JAR_PROCESS
@@ -4618,13 +5250,13 @@ async function portRow(port, finding, ports) {
4618
5250
  if (finding.running) return failed("port", "port", {
4619
5251
  detail: `${port} is not answering although ${finding.holder} runs`,
4620
5252
  hint: "`rde logs` shows what Metabase is doing",
4621
- fix: null
5253
+ step: null
4622
5254
  });
4623
5255
  if (free) return ok("port", "port", `${port} is free`);
4624
5256
  return failed("port", "port", {
4625
5257
  detail: `${port} is in use by another program`,
4626
5258
  hint: "stop that program before `rde start`",
4627
- fix: null
5259
+ step: null
4628
5260
  });
4629
5261
  }
4630
5262
  async function healthRow(url, deps) {
@@ -4639,7 +5271,7 @@ async function healthRow(url, deps) {
4639
5271
  row: failed("health", "health", {
4640
5272
  detail: `initializing (${Math.round(error.progress * 100)}%)`,
4641
5273
  hint: "wait for it; `rde logs -f` shows progress",
4642
- fix: "start-metabase"
5274
+ step: "start-metabase"
4643
5275
  }),
4644
5276
  healthy: false
4645
5277
  };
@@ -4647,7 +5279,7 @@ async function healthRow(url, deps) {
4647
5279
  row: failed("health", "health", {
4648
5280
  detail: "unreachable",
4649
5281
  hint: "`rde start`",
4650
- fix: "start-metabase"
5282
+ step: "start-metabase"
4651
5283
  }),
4652
5284
  healthy: false
4653
5285
  };
@@ -4655,59 +5287,227 @@ async function healthRow(url, deps) {
4655
5287
  row: failed("health", "health", {
4656
5288
  detail: `something other than Metabase answers at ${url} (HTTP ${error.status})`,
4657
5289
  hint: "another program holds the port; stop it before `rde start`",
4658
- fix: null
5290
+ step: null
5291
+ }),
5292
+ healthy: false
5293
+ };
5294
+ throw error;
5295
+ }
5296
+ }
5297
+ function adminRow(state, properties) {
5298
+ if (!properties["has-user-setup"]) return failed("admin", "admin user", {
5299
+ detail: "Metabase has no admin user yet",
5300
+ hint: "`rde init --only admin-user`",
5301
+ step: "admin-user"
5302
+ });
5303
+ return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
5304
+ }
5305
+ const VERSION_STEP = "start-metabase";
5306
+ function versionRow(state, properties) {
5307
+ const label = "Metabase version";
5308
+ const running = properties.version.tag;
5309
+ if (state.version === null) return ok("version", label, `${running}; \`rde start\` records it in the state file`);
5310
+ if (state.version !== running) return failed("version", label, {
5311
+ detail: `running ${running}, the state file says ${state.version}`,
5312
+ hint: "`rde start` records the running version",
5313
+ step: VERSION_STEP
5314
+ });
5315
+ return ok("version", label, running);
5316
+ }
5317
+ const API_KEY_STEP = "api-key";
5318
+ const RECREATE_HINT = "when the instance holds nothing you need, `rde uninstall` then `rde init`";
5319
+ function adminPasswordRow(state, secrets) {
5320
+ const id = "admin-password";
5321
+ const label = "admin password";
5322
+ const admin = state.admin;
5323
+ if (admin === null) return skipped(id, label, "no admin user created by rde yet");
5324
+ switch (admin.kind) {
5325
+ case "prompted": return ok(id, label, "yours, entered during `rde init` and never stored");
5326
+ case "generated":
5327
+ if (secrets === null || secrets.admin === null) return failed(id, label, {
5328
+ detail: "generated, but missing from the secrets file",
5329
+ hint: `it cannot be recovered; ${RECREATE_HINT}`,
5330
+ step: null
5331
+ });
5332
+ return ok(id, label, "generated and stored; `rde credentials` shows it");
5333
+ case "legacy-default": return failed(id, label, {
5334
+ detail: "the public default password from an earlier rde (legacy-default)",
5335
+ hint: `anyone who knows the default can log in; ${RECREATE_HINT} with a generated one`,
5336
+ step: null
5337
+ });
5338
+ }
5339
+ }
5340
+ async function apiKeyFinding(secrets, url, healthy, deps) {
5341
+ const id = "api-key";
5342
+ const label = "API key";
5343
+ const apiKey = secrets === null ? null : secrets.apiKey;
5344
+ if (apiKey === null) return {
5345
+ row: failed(id, label, {
5346
+ detail: "none in the secrets file",
5347
+ hint: "`rde init --only api-key`",
5348
+ step: API_KEY_STEP
5349
+ }),
5350
+ access: {
5351
+ kind: "unusable",
5352
+ reason: "no API key"
5353
+ }
5354
+ };
5355
+ if (url === null || !healthy) return {
5356
+ row: ok(id, label, "in the secrets file"),
5357
+ access: {
5358
+ kind: "unusable",
5359
+ reason: NOT_HEALTHY
5360
+ }
5361
+ };
5362
+ const client = deps.client(url, {
5363
+ kind: "api-key",
5364
+ key: apiKey
5365
+ });
5366
+ try {
5367
+ await client.user.current();
5368
+ } catch (error) {
5369
+ if (!isRejectedCredential(error)) throw error;
5370
+ return {
5371
+ row: failed(id, label, {
5372
+ detail: "in the secrets file, but Metabase rejects it",
5373
+ hint: "`rde init --only api-key` replaces it",
5374
+ step: API_KEY_STEP
4659
5375
  }),
4660
- healthy: false
5376
+ access: {
5377
+ kind: "unusable",
5378
+ reason: "Metabase rejects the API key"
5379
+ }
4661
5380
  };
4662
- throw error;
4663
5381
  }
5382
+ return {
5383
+ row: ok(id, label, "in the secrets file, accepted by Metabase"),
5384
+ access: {
5385
+ kind: "accepted",
5386
+ client
5387
+ }
5388
+ };
4664
5389
  }
4665
- function adminRow(state, properties) {
4666
- if (!properties["has-user-setup"]) return failed("admin", "admin user", {
4667
- detail: "Metabase has no admin user yet",
4668
- hint: "`rde init --only admin-user`",
4669
- fix: "admin-user"
4670
- });
4671
- return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
5390
+ function octal(mode) {
5391
+ return `0${mode.toString(8)}`;
4672
5392
  }
4673
- const VERSION_STEP = "start-metabase";
4674
- function versionRow(state, properties) {
4675
- const label = "Metabase version";
4676
- const running = properties.version.tag;
4677
- if (state.version === null) return ok("version", label, `${running}; \`rde start\` records it in the state file`);
4678
- if (state.version !== running) return failed("version", label, {
4679
- detail: `running ${running}, the state file says ${state.version}`,
4680
- hint: "`rde start` records the running version",
4681
- fix: VERSION_STEP
4682
- });
4683
- return ok("version", label, running);
5393
+ function expectsSecrets(state) {
5394
+ return state.admin !== null && state.admin.kind !== "prompted" || state.steps[API_KEY_STEP]?.status === "done";
4684
5395
  }
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"
5396
+ function secretsFileRow(state, files, dir) {
5397
+ const id = "secrets-file";
5398
+ const label = "secrets file";
5399
+ const path = secretsFileIn(dir);
5400
+ if (files.secretsMode === null) {
5401
+ if (!expectsSecrets(state)) return skipped(id, label, "nothing stored yet");
5402
+ return failed(id, label, {
5403
+ detail: `${path} is missing`,
5404
+ hint: "`rde init --only api-key` stores a new API key",
5405
+ step: API_KEY_STEP
5406
+ });
5407
+ }
5408
+ if (files.secretsMode !== 384 || files.dirMode !== 448) {
5409
+ const dirMode = files.dirMode === null ? "missing" : octal(files.dirMode);
5410
+ return failed(id, label, {
5411
+ detail: `${path} is ${octal(files.secretsMode)} in a ${dirMode} directory`,
5412
+ hint: `\`chmod ${octal(384)} ${path} && chmod ${octal(448)} ${dir}\``,
5413
+ step: null
5414
+ });
5415
+ }
5416
+ return ok(id, label, `${path}, ${octal(384)} in a ${octal(448)} directory`);
5417
+ }
5418
+ function stateFileRow(files) {
5419
+ const id = "state-file";
5420
+ const label = "state file";
5421
+ if (files.secretsInState.length > 0) return failed(id, label, {
5422
+ detail: `carries ${files.secretsInState.join(", ")}`,
5423
+ hint: "`rde init --only api-key` rewrites it without them",
5424
+ step: API_KEY_STEP
4690
5425
  });
4691
- return ok("api-key", "API key", "in the state file");
5426
+ return ok(id, label, "no secrets");
5427
+ }
5428
+ async function credentialRows(state, secrets, apiKey, deps) {
5429
+ const dir = deps.runtime.instanceDir;
5430
+ const files = await inspectCredentialFiles(dir);
5431
+ return [
5432
+ adminPasswordRow(state, secrets),
5433
+ apiKey.row,
5434
+ secretsFileRow(state, files, dir),
5435
+ stateFileRow(files)
5436
+ ];
4692
5437
  }
4693
- async function mbProfileRow(state, deps, mbInstalled, healthy) {
5438
+ function withoutTrailingSlash(url) {
5439
+ return url.replace(/\/+$/, "");
5440
+ }
5441
+ async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
4694
5442
  const label = "mb profile";
4695
- if (!mbInstalled) return skipped("mb-profile", label, "mb is not installed");
5443
+ if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
4696
5444
  if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY);
4697
- const entry = (await mbTool(deps.runner).authList()).find((candidate) => candidate.profile === state.mbProfile);
5445
+ const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
4698
5446
  if (entry === void 0) return failed("mb-profile", label, {
4699
5447
  detail: `${state.mbProfile} is not configured`,
4700
5448
  hint: "`rde init --only api-key`",
4701
- fix: "api-key"
5449
+ step: "api-key"
5450
+ });
5451
+ if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
5452
+ detail: `${state.mbProfile} points at ${entry.url}, not at this instance (${url})`,
5453
+ hint: "`rde init --only api-key` logs it back in against the instance",
5454
+ step: "api-key"
4702
5455
  });
4703
5456
  if (entry.status !== "ok") return failed("mb-profile", label, {
4704
5457
  detail: `${state.mbProfile}: ${entry.status}`,
4705
5458
  hint: "`rde init --only api-key` logs it in again",
4706
- fix: "api-key"
5459
+ step: "api-key"
4707
5460
  });
4708
5461
  const user = entry.user === null ? "" : ` as ${entry.user.name}`;
4709
5462
  return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
4710
5463
  }
5464
+ function hostOf(url) {
5465
+ return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
5466
+ }
5467
+ function unsetHint(name) {
5468
+ return `\`unset ${name}\`, remove it from your shell profile if it is exported there, then restart the agent session`;
5469
+ }
5470
+ async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
5471
+ const id = `env.${ENV_MB_PROFILE}`;
5472
+ if (name === rdeProfile) return ok(id, ENV_MB_PROFILE, `${name}: rde's own profile, so plain \`mb\` uses this instance`);
5473
+ const rde = rdeProfile === null ? "rde's" : `rde's (${rdeProfile})`;
5474
+ const detail = (where) => `${name}: \`mb\` and the rde skill will use profile ${name} (${where}) instead of ${rde}`;
5475
+ if (mbUnusableReason !== null) return note$1(id, ENV_MB_PROFILE, detail(mbUnusableReason), unsetHint(ENV_MB_PROFILE));
5476
+ const entry = (await profiles()).find((candidate) => candidate.profile === name);
5477
+ const where = entry === void 0 ? "not configured" : hostOf(entry.url);
5478
+ return note$1(id, ENV_MB_PROFILE, detail(where), unsetHint(ENV_MB_PROFILE));
5479
+ }
5480
+ async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
5481
+ const rows = [];
5482
+ if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
5483
+ 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)));
5484
+ 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)));
5485
+ return rows;
5486
+ }
5487
+ const PROFILE_STATUS_WORDS = {
5488
+ ok: "authenticates",
5489
+ "auth-failed": "login rejected",
5490
+ "network-error": "not answering",
5491
+ "server-error": "answers with a server error",
5492
+ "not-probed": "not checked"
5493
+ };
5494
+ function plainMbLine(env, rdeProfile, profiles) {
5495
+ if (env.url !== null) return `plain \`mb\` goes to ${hostOf(env.url)} (${ENV_MB_URL})`;
5496
+ const name = env.profile ?? "default";
5497
+ const via = env.profile === null ? "" : ` (${ENV_MB_PROFILE})`;
5498
+ const entry = profiles.find((candidate) => candidate.profile === name);
5499
+ if (entry === void 0) return `plain \`mb\` uses ${name}${via}, which is not configured`;
5500
+ const instance = name === rdeProfile ? ", this instance" : "";
5501
+ return `plain \`mb\` uses ${name}${via}: ${hostOf(entry.url)}${instance}`;
5502
+ }
5503
+ async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
5504
+ const id = "other-metabases";
5505
+ const label = "other Metabases";
5506
+ if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
5507
+ const all = await profiles();
5508
+ const others = all.filter((entry) => entry.profile !== rdeProfile);
5509
+ 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"));
5510
+ }
4711
5511
  function isNotFound$2(error) {
4712
5512
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
4713
5513
  }
@@ -4718,7 +5518,7 @@ async function licenseRow(client) {
4718
5518
  if (!status.valid) return failed("license", label, {
4719
5519
  detail: `invalid: ${status.status}`,
4720
5520
  hint: "`rde init --from license` with a valid token",
4721
- fix: "license"
5521
+ step: "license"
4722
5522
  });
4723
5523
  return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
4724
5524
  } catch (error) {
@@ -4735,47 +5535,47 @@ async function databasesRow(state, client) {
4735
5535
  if (missing.length > 0) return failed("databases", label, {
4736
5536
  detail: `not on the server any more: ${missing.map((record) => record.name).join(", ")}`,
4737
5537
  hint: "`rde db add` connects them again",
4738
- fix: "databases"
5538
+ step: "databases"
4739
5539
  });
4740
5540
  const names = state.databases.map((record) => record.name).join(", ");
4741
5541
  return ok("databases", label, `${state.databases.length} connected: ${names}`);
4742
5542
  }
4743
5543
  const SAMPLE_DATABASE_STEP = "sample-database";
4744
5544
  const SAMPLE_LABEL = "sample database";
4745
- async function sampleDatabaseRow(record, deps, dockerOk, client) {
5545
+ async function sampleDatabaseRow(record, deps, dockerOk, access) {
4746
5546
  const id = "sample-database";
4747
5547
  if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
4748
5548
  const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
4749
5549
  if (!status.installed) return failed(id, SAMPLE_LABEL, {
4750
5550
  detail: `${record.containerName} does not exist`,
4751
5551
  hint: "`rde init` recreates it",
4752
- fix: SAMPLE_DATABASE_STEP
5552
+ step: SAMPLE_DATABASE_STEP
4753
5553
  });
4754
5554
  if (!status.running) return failed(id, SAMPLE_LABEL, {
4755
5555
  detail: `${record.containerName} is stopped`,
4756
5556
  hint: "`rde start`",
4757
- fix: "start-metabase"
5557
+ step: "start-metabase"
4758
5558
  });
4759
5559
  if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
4760
5560
  detail: `${record.containerName} runs but port ${record.port} is not answering`,
4761
5561
  hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
4762
- fix: null
5562
+ step: null
4763
5563
  });
4764
5564
  const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
4765
5565
  if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
4766
5566
  detail: `${where}; not connected to Metabase yet`,
4767
5567
  hint: "`rde init` connects it",
4768
- fix: SAMPLE_DATABASE_STEP
5568
+ step: SAMPLE_DATABASE_STEP
4769
5569
  });
4770
- if (client === null) return skipped(id, SAMPLE_LABEL, `${where}; ${NOT_HEALTHY}, so the connection is unchecked`);
5570
+ if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
4771
5571
  try {
4772
- const database = await client.database.get(record.databaseId);
5572
+ const database = await access.client.database.get(record.databaseId);
4773
5573
  return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
4774
5574
  } catch (error) {
4775
5575
  if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
4776
5576
  detail: `${where}; database ${record.databaseId} is gone from Metabase`,
4777
5577
  hint: "`rde init` connects it again",
4778
- fix: SAMPLE_DATABASE_STEP
5578
+ step: SAMPLE_DATABASE_STEP
4779
5579
  });
4780
5580
  throw error;
4781
5581
  }
@@ -4792,7 +5592,7 @@ async function gitSyncRow(state, deps) {
4792
5592
  if (status.branch === null) return failed("git-sync", label, {
4793
5593
  detail: "Metabase reports no branch",
4794
5594
  hint: "`rde init --only git-sync`",
4795
- fix: "git-sync"
5595
+ step: "git-sync"
4796
5596
  });
4797
5597
  const dirty = status.is_dirty ? "unsynced changes" : "in sync";
4798
5598
  return ok("git-sync", label, `branch ${status.branch}, ${dirty}, ${state.gitRemote.workdir}`);
@@ -4800,15 +5600,100 @@ async function gitSyncRow(state, deps) {
4800
5600
  if (isMbFailure$1(error)) return failed("git-sync", label, {
4801
5601
  detail: error.message,
4802
5602
  hint: `\`${mbCommand(state.mbProfile)} git-sync status\` shows the full answer`,
4803
- fix: null
5603
+ step: null
4804
5604
  });
4805
5605
  throw error;
4806
5606
  }
4807
5607
  }
4808
5608
  const SKILL_STEP = "agents-skills";
5609
+ const GLOBAL_SCOPE = "global";
5610
+ const SKILL_FILE = "SKILL.md";
5611
+ const CLAUDE_CODE = "Claude Code";
5612
+ const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
4809
5613
  function skillRowId(agent) {
4810
5614
  return `skill.${agent.id}`;
4811
5615
  }
5616
+ function withRef(source, ref) {
5617
+ return ref === null ? source : `${source}#${ref}`;
5618
+ }
5619
+ async function copyRef(entry, deps) {
5620
+ const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
5621
+ return lockedRef(path, await deps.skills.files.readJson(path), entry.name);
5622
+ }
5623
+ async function judgeCopy(entry, origin, deps) {
5624
+ const where = `${entry.scope} ${entry.path}`;
5625
+ switch (origin.kind) {
5626
+ case "local": {
5627
+ const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
5628
+ const wanted = await deps.skills.files.readText(join(origin.path, SKILL_FILE));
5629
+ const current = installed !== null && installed === wanted;
5630
+ return {
5631
+ entry,
5632
+ current,
5633
+ line: `${where}: ${current ? "same as" : "differs from"} ${origin.path}`
5634
+ };
5635
+ }
5636
+ case "github": {
5637
+ const wanted = withRef(origin.repository, origin.ref);
5638
+ if (entry.source === null) return {
5639
+ entry,
5640
+ current: false,
5641
+ line: `${where}: no recorded source, not ${wanted}`
5642
+ };
5643
+ const found = withRef(entry.source, await copyRef(entry, deps));
5644
+ const current = found === wanted;
5645
+ return {
5646
+ entry,
5647
+ current,
5648
+ line: `${where}: from ${found}${current ? "" : `, not ${wanted}`}`
5649
+ };
5650
+ }
5651
+ case "other": {
5652
+ const current = entry.source === origin.source;
5653
+ return {
5654
+ entry,
5655
+ current,
5656
+ line: `${where}: from ${entry.source ?? "no recorded source"}${current ? "" : `, not ${origin.source}`}`
5657
+ };
5658
+ }
5659
+ }
5660
+ }
5661
+ function loadedCopyLine(entries, cwd) {
5662
+ if (entries.length < 2) return null;
5663
+ const count = `${entries.length} copies are installed`;
5664
+ const forClaude = entries.filter((entry) => entry.agents.includes(CLAUDE_CODE));
5665
+ const loaded = forClaude.find((entry) => entry.scope === GLOBAL_SCOPE) ?? forClaude[0];
5666
+ if (loaded === void 0) return count;
5667
+ return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
5668
+ }
5669
+ function staleCopiesRow(stale, detail, deps) {
5670
+ const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
5671
+ const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
5672
+ const repairs = [];
5673
+ if (globalStale) repairs.push(`\`rde init --only ${SKILL_STEP}\` reinstalls the global copy from ${deps.skills.source}`);
5674
+ if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove rde -y\` in ${deps.skills.cwd}`);
5675
+ return failed("skill-copies", "rde skill copies", {
5676
+ detail,
5677
+ hint: `${repairs.join("; ")}; then restart the agent session`,
5678
+ step: globalStale ? SKILL_STEP : null
5679
+ });
5680
+ }
5681
+ async function skillCopiesRow(globalEntries, deps) {
5682
+ const id = "skill-copies";
5683
+ const label = "rde skill copies";
5684
+ const cwd = deps.skills.cwd;
5685
+ const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === "rde");
5686
+ const entries = [...globalEntries, ...projectEntries];
5687
+ if (entries.length === 0) return skipped(id, label, "no copy installed");
5688
+ const origin = skillOrigin(deps.skills.source, cwd);
5689
+ const judged = [];
5690
+ for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
5691
+ const loaded = loadedCopyLine(entries, cwd);
5692
+ const lines = judged.map((copy) => copy.line);
5693
+ const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
5694
+ const stale = judged.filter((copy) => !copy.current);
5695
+ return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
5696
+ }
4812
5697
  async function skillRows(state, deps) {
4813
5698
  const label = "rde skill";
4814
5699
  if (state === null) return [skipped("skill", label, NO_INSTANCE)];
@@ -4816,17 +5701,18 @@ async function skillRows(state, deps) {
4816
5701
  const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
4817
5702
  if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
4818
5703
  const entries = await rdeSkillEntries(deps.runner);
5704
+ const copies = await skillCopiesRow(entries, deps);
4819
5705
  if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
4820
5706
  detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
4821
5707
  hint: `\`rde init --only ${SKILL_STEP}\``,
4822
- fix: SKILL_STEP
4823
- })];
4824
- return detected.map((agent) => {
5708
+ step: SKILL_STEP
5709
+ }), copies];
5710
+ return [...detected.map((agent) => {
4825
5711
  const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
4826
5712
  const agentLabel = `${label} (${agent.displayName})`;
4827
5713
  if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
4828
5714
  return ok(skillRowId(agent), agentLabel, entry.path);
4829
- });
5715
+ }), copies];
4830
5716
  }
4831
5717
  function noRuntimeRows() {
4832
5718
  return [
@@ -4835,13 +5721,13 @@ function noRuntimeRows() {
4835
5721
  "health"
4836
5722
  ].map((id) => skipped(id, id, NO_INSTANCE));
4837
5723
  }
4838
- async function instanceRows(state, deps, dockerOk, mbInstalled) {
5724
+ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusableReason) {
4839
5725
  const configured = configuredRuntime(state);
4840
5726
  if (configured === null) return [
4841
5727
  ...noRuntimeRows(),
4842
5728
  skipped("version", "Metabase version", NO_INSTANCE),
4843
5729
  skipped("admin", "admin user", NO_INSTANCE),
4844
- apiKeyRow(state),
5730
+ ...await credentialRows(state, secrets, await apiKeyFinding(secrets, null, false, deps), deps),
4845
5731
  skipped("mb-profile", "mb profile", NO_INSTANCE),
4846
5732
  skipped("license", "license", NO_INSTANCE),
4847
5733
  skipped("databases", "databases", NO_INSTANCE),
@@ -4851,35 +5737,33 @@ async function instanceRows(state, deps, dockerOk, mbInstalled) {
4851
5737
  const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
4852
5738
  const { row: health, healthy } = await healthRow(configured.url, deps);
4853
5739
  const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
5740
+ const apiKey = await apiKeyFinding(secrets, configured.url, healthy, deps);
4854
5741
  const rows = [
4855
5742
  runtime.row,
4856
5743
  await portRow(configured.runtime.port, runtime, deps.ports),
4857
5744
  health,
4858
5745
  properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
4859
5746
  properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
4860
- apiKeyRow(state),
4861
- await mbProfileRow(state, deps, mbInstalled, healthy)
5747
+ ...await credentialRows(state, secrets, apiKey, deps),
5748
+ await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
4862
5749
  ];
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));
5750
+ const access = apiKey.access;
5751
+ if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
5752
+ else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
5753
+ rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
4872
5754
  rows.push(await gitSyncRow(state, deps));
4873
5755
  return rows;
4874
5756
  }
4875
- async function collectDoctor(instance, state, deps) {
5757
+ async function collectDoctor(instance, state, secrets, deps) {
4876
5758
  const machine = await machineRows(deps, state?.runtime ?? null);
4877
5759
  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));
5760
+ const mb = await mbFinding(deps);
5761
+ const profiles = sharedProfileList(deps);
5762
+ const checks = [...machine, mb.row];
5763
+ 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));
5764
+ else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
5765
+ const rdeProfile = state === null ? null : state.mbProfile;
5766
+ checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
4883
5767
  return {
4884
5768
  instance,
4885
5769
  checks,
@@ -4891,18 +5775,38 @@ const MARK = {
4891
5775
  failed: "✗",
4892
5776
  skipped: "–"
4893
5777
  };
5778
+ const NOTE_MARK = "!";
4894
5779
  const HINT_ARROW = "→";
5780
+ function markFor(row) {
5781
+ return row.status === "ok" && row.hint !== null ? NOTE_MARK : MARK[row.status];
5782
+ }
4895
5783
  function doctorLines(report) {
4896
5784
  const width = Math.max(...report.checks.map((row) => row.label.length));
5785
+ const indent = ` ${" ".repeat(width)} `;
4897
5786
  const lines = [];
4898
5787
  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}`);
5788
+ const [first, ...rest] = row.detail.split("\n");
5789
+ lines.push(`${markFor(row)} ${row.label.padEnd(width)} ${first}`);
5790
+ lines.push(...rest.map((line) => `${indent}${line}`));
5791
+ if (row.hint !== null) lines.push(`${indent}${HINT_ARROW} ${row.hint}`);
4901
5792
  }
4902
5793
  return lines;
4903
5794
  }
5795
+ function fixCommand(step) {
5796
+ return step === null ? null : `rde init --only ${step}`;
5797
+ }
5798
+ function doctorJson(report) {
5799
+ return {
5800
+ instance: report.instance,
5801
+ checks: report.checks.map(({ step, ...row }) => ({
5802
+ ...row,
5803
+ fix: fixCommand(step)
5804
+ })),
5805
+ failed: report.failed
5806
+ };
5807
+ }
4904
5808
  function fixableSteps(report) {
4905
- const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.fix !== null ? [row.fix] : []));
5809
+ const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.step !== null ? [row.step] : []));
4906
5810
  return StepId.options.filter((id) => wanted.has(id));
4907
5811
  }
4908
5812
  function doctorSummary(report) {
@@ -4941,15 +5845,24 @@ const ADMIN_USER_KEYS = {
4941
5845
  firstName: `${STEP_ID$6}.first-name`,
4942
5846
  lastName: `${STEP_ID$6}.last-name`
4943
5847
  };
4944
- const DEFAULT_ADMIN_EMAIL = "admin@metabase.com";
4945
- const DEFAULT_ADMIN_PASSWORD = "metabase123";
5848
+ const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
5849
+ const GENERATED_PASSWORD_BYTES = 24;
4946
5850
  const DEFAULT_SITE_NAME = "rde";
4947
5851
  const MIN_PASSWORD_LENGTH = 6;
4948
5852
  const DIGIT = /\d/;
4949
5853
  const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
4950
- const DEFAULT_ADMIN = { variant: "default" };
5854
+ const DEFAULT_ADMIN = { variant: "generated" };
5855
+ function generatePassword(random) {
5856
+ for (;;) {
5857
+ const candidate = random(GENERATED_PASSWORD_BYTES).toString("base64url");
5858
+ if (DIGIT.test(candidate)) return candidate;
5859
+ }
5860
+ }
4951
5861
  function hostDeps$8(ctx) {
4952
- return { client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal) };
5862
+ return {
5863
+ client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
5864
+ password: () => generatePassword(randomBytes)
5865
+ };
4953
5866
  }
4954
5867
  function isNotReady(error) {
4955
5868
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
@@ -4988,26 +5901,22 @@ async function promptedUser(ctx) {
4988
5901
  ...lastName.length > 0 && { last_name: lastName }
4989
5902
  };
4990
5903
  }
4991
- async function chooseUser(ctx, kind) {
4992
- switch (kind) {
4993
- case "default": return {
4994
- email: DEFAULT_ADMIN_EMAIL,
4995
- password: DEFAULT_ADMIN_PASSWORD
5904
+ async function chooseUser(ctx, variant, deps) {
5905
+ switch (variant) {
5906
+ case "generated": return {
5907
+ email: GENERATED_ADMIN_EMAIL,
5908
+ password: deps.password()
4996
5909
  };
4997
5910
  case "prompted": return promptedUser(ctx);
4998
5911
  }
4999
5912
  }
5000
- function credentialsRecord(kind, user) {
5001
- switch (kind) {
5002
- case "default": return {
5003
- kind,
5913
+ function storedLogin(variant, user) {
5914
+ switch (variant) {
5915
+ case "generated": return {
5004
5916
  email: user.email,
5005
5917
  password: user.password
5006
5918
  };
5007
- case "prompted": return {
5008
- kind,
5009
- email: user.email
5010
- };
5919
+ case "prompted": return null;
5011
5920
  }
5012
5921
  }
5013
5922
  function adminUserStep(options, deps = hostDeps$8) {
@@ -5026,7 +5935,8 @@ function adminUserStep(options, deps = hostDeps$8) {
5026
5935
  },
5027
5936
  async run(ctx) {
5028
5937
  const configured = requireConfiguredRuntime(ctx.state);
5029
- const client = deps(ctx).client(configured.url, NO_AUTH);
5938
+ const resolved = deps(ctx);
5939
+ const client = resolved.client(configured.url, NO_AUTH);
5030
5940
  const properties = await client.session.properties();
5031
5941
  if (properties["has-user-setup"]) {
5032
5942
  ctx.ui.info("Metabase already has an admin user");
@@ -5034,7 +5944,11 @@ function adminUserStep(options, deps = hostDeps$8) {
5034
5944
  }
5035
5945
  const token = properties["setup-token"];
5036
5946
  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);
5947
+ const user = await chooseUser(ctx, options.variant, resolved);
5948
+ await ctx.saveSecrets({
5949
+ ...ctx.secrets,
5950
+ admin: storedLogin(options.variant, user)
5951
+ });
5038
5952
  const session = await client.setup.create({
5039
5953
  token,
5040
5954
  user,
@@ -5043,7 +5957,10 @@ function adminUserStep(options, deps = hostDeps$8) {
5043
5957
  ctx.session.metabaseSession.set(session.id);
5044
5958
  await ctx.saveState({
5045
5959
  ...ctx.state,
5046
- admin: credentialsRecord(options.variant, user)
5960
+ admin: {
5961
+ kind: options.variant,
5962
+ email: user.email
5963
+ }
5047
5964
  });
5048
5965
  ctx.ui.success(`Admin user ${user.email} created`);
5049
5966
  return "completed";
@@ -5066,11 +5983,12 @@ function hostDeps$7(ctx) {
5066
5983
  };
5067
5984
  }
5068
5985
  async function adminCredentials(ctx) {
5069
- const admin = ctx.state.admin;
5070
- if (admin?.kind === "default") return {
5071
- username: admin.email,
5072
- password: admin.password
5986
+ const stored = ctx.secrets.admin;
5987
+ if (stored !== null) return {
5988
+ username: stored.email,
5989
+ password: stored.password
5073
5990
  };
5991
+ const admin = ctx.state.admin;
5074
5992
  ctx.ui.info("An admin login is needed once to create the API key; it is not saved");
5075
5993
  return {
5076
5994
  username: await ctx.prompts.text({
@@ -5108,46 +6026,70 @@ async function freshApiKey(ctx, client) {
5108
6026
  group_id: groupId
5109
6027
  })).unmasked_key;
5110
6028
  }
5111
- async function persistedApiKey(ctx, deps, url) {
5112
- if (ctx.state.apiKey !== null) return ctx.state.apiKey;
6029
+ async function authenticates(client) {
6030
+ try {
6031
+ await client.user.current();
6032
+ return true;
6033
+ } catch (error) {
6034
+ if (isRejectedCredential(error)) return false;
6035
+ throw error;
6036
+ }
6037
+ }
6038
+ async function workingApiKey(ctx, deps, url) {
6039
+ const stored = ctx.secrets.apiKey;
6040
+ if (stored !== null) {
6041
+ if (await authenticates(deps.client(url, {
6042
+ kind: "api-key",
6043
+ key: stored
6044
+ }))) return stored;
6045
+ ctx.ui.warn("Metabase rejects the stored API key; replacing it");
6046
+ }
5113
6047
  const sessionId = await superuserSession(ctx, deps, url);
5114
6048
  const apiKey = await freshApiKey(ctx, deps.client(url, {
5115
6049
  kind: "session",
5116
6050
  id: sessionId
5117
6051
  }));
5118
- await ctx.saveState({
5119
- ...ctx.state,
6052
+ await ctx.saveSecrets({
6053
+ ...ctx.secrets,
5120
6054
  apiKey
5121
6055
  });
5122
6056
  return apiKey;
5123
6057
  }
5124
- async function chooseProfile(ctx, mb, url) {
5125
- const profile = ctx.state.mbProfile;
5126
- if (profile !== "default") return profile;
5127
- let existing;
6058
+ async function defaultProfileStatus(mb) {
5128
6059
  try {
5129
- existing = await mb.authStatus(DEFAULT_MB_PROFILE);
6060
+ return await mb.authStatus(DEFAULT_MB_PROFILE);
5130
6061
  } catch (error) {
5131
- if (error instanceof ProcessNotFoundError) return profile;
6062
+ if (error instanceof ProcessNotFoundError) return null;
5132
6063
  throw error;
5133
6064
  }
5134
- if (!existing.present || existing.url === url) return profile;
5135
- const chosen = await ctx.prompts.select({
6065
+ }
6066
+ async function chooseProfile(ctx, mb, url) {
6067
+ const profile = ctx.state.mbProfile;
6068
+ if (profile !== "default") return profile;
6069
+ const existing = await defaultProfileStatus(mb);
6070
+ if (existing === null || !existing.present || existing.url === null || existing.url === url) return profile;
6071
+ const previous = existing.url;
6072
+ const named = namedMbProfile(ctx.state.name);
6073
+ if (await ctx.prompts.select({
5136
6074
  key: API_KEY_KEYS.profile,
5137
- message: `mb profile ${DEFAULT_MB_PROFILE} already points at ${existing.url}; which profile should this instance use?`,
6075
+ message: `mb profile default already points at ${previous}; which profile should this instance use?`,
5138
6076
  choices: [{
5139
- value: "rde",
5140
- label: `rde, and leave ${DEFAULT_MB_PROFILE} alone`
6077
+ value: "default",
6078
+ label: `default, replacing it (recommended; it points at ${previous} today)`
5141
6079
  }, {
5142
- value: DEFAULT_MB_PROFILE,
5143
- label: `${DEFAULT_MB_PROFILE}, replacing it`
5144
- }]
5145
- });
5146
- if (chosen !== profile) await ctx.saveState({
6080
+ value: MbProfileChoice.enum.rde,
6081
+ label: `${named}, and leave default alone`
6082
+ }],
6083
+ recommended: "default"
6084
+ }) === "default") {
6085
+ ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
6086
+ return profile;
6087
+ }
6088
+ await ctx.saveState({
5147
6089
  ...ctx.state,
5148
- mbProfile: chosen
6090
+ mbProfile: named
5149
6091
  });
5150
- return chosen;
6092
+ return named;
5151
6093
  }
5152
6094
  function usageNote(profile) {
5153
6095
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
@@ -5158,7 +6100,7 @@ function apiKeyStep(deps = hostDeps$7) {
5158
6100
  id: STEP_ID$5,
5159
6101
  title: "Connect the Metabase CLI",
5160
6102
  async detect(ctx) {
5161
- if (ctx.state.apiKey === null) return "todo";
6103
+ if (ctx.secrets.apiKey === null) return "todo";
5162
6104
  const mb = mbTool(deps(ctx).runner);
5163
6105
  const profile = ctx.state.mbProfile;
5164
6106
  try {
@@ -5172,7 +6114,7 @@ function apiKeyStep(deps = hostDeps$7) {
5172
6114
  async run(ctx) {
5173
6115
  const resolved = deps(ctx);
5174
6116
  const configured = requireConfiguredRuntime(ctx.state);
5175
- const apiKey = await persistedApiKey(ctx, resolved, configured.url);
6117
+ const apiKey = await workingApiKey(ctx, resolved, configured.url);
5176
6118
  const mb = mbTool(resolved.runner);
5177
6119
  const profile = await chooseProfile(ctx, mb, configured.url);
5178
6120
  await mb.authLogin({
@@ -5574,14 +6516,14 @@ function isMbUnavailable(error) {
5574
6516
  }
5575
6517
  async function runGitSync(ctx, deps) {
5576
6518
  const configured = requireConfiguredRuntime(ctx.state);
5577
- if (ctx.state.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6519
+ if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
5578
6520
  const run = {
5579
6521
  ctx,
5580
6522
  deps,
5581
6523
  git: gitTool(deps.runner),
5582
6524
  client: deps.client(configured.url, {
5583
6525
  kind: "api-key",
5584
- key: ctx.state.apiKey
6526
+ key: ctx.secrets.apiKey
5585
6527
  })
5586
6528
  };
5587
6529
  const flow = await runFlow({
@@ -5637,8 +6579,8 @@ function validateToken(value) {
5637
6579
  return "A license token is 64 hexadecimal characters";
5638
6580
  }
5639
6581
  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;
6582
+ if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6583
+ return ctx.secrets.apiKey;
5642
6584
  }
5643
6585
  function activeLabel(status) {
5644
6586
  const plan = status["plan-alias"];
@@ -5651,15 +6593,15 @@ function licenseStep(deps = hostDeps$5) {
5651
6593
  async detect(ctx) {
5652
6594
  if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
5653
6595
  const configured = configuredRuntime(ctx.state);
5654
- if (configured === null || ctx.state.apiKey === null) return "todo";
6596
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
5655
6597
  const client = deps(ctx).client(configured.url, {
5656
6598
  kind: "api-key",
5657
- key: ctx.state.apiKey
6599
+ key: ctx.secrets.apiKey
5658
6600
  });
5659
6601
  try {
5660
6602
  return (await client.tokenStatus.get()).valid ? "done" : "todo";
5661
6603
  } catch (error) {
5662
- if (isNoTokenYet(error)) return "todo";
6604
+ if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
5663
6605
  throw error;
5664
6606
  }
5665
6607
  },
@@ -5894,9 +6836,9 @@ function isNotServingYet(error) {
5894
6836
  function isNotFound$1(error) {
5895
6837
  return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
5896
6838
  }
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;
6839
+ function requireApiKey(ctx) {
6840
+ if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
6841
+ return ctx.secrets.apiKey;
5900
6842
  }
5901
6843
  async function recordedDatabase(record, client) {
5902
6844
  if (record.databaseId === null) return null;
@@ -6018,23 +6960,23 @@ function sampleDatabaseStep(deps = hostDeps$3) {
6018
6960
  if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
6019
6961
  const record = ctx.state.sampleDatabase;
6020
6962
  const configured = configuredRuntime(ctx.state);
6021
- if (record === null || configured === null || ctx.state.apiKey === null) return "todo";
6963
+ if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
6022
6964
  if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
6023
6965
  const client = resolved.client(configured.url, {
6024
6966
  kind: "api-key",
6025
- key: ctx.state.apiKey
6967
+ key: ctx.secrets.apiKey
6026
6968
  });
6027
6969
  try {
6028
6970
  return await recordedDatabase(record, client) === null ? "todo" : "done";
6029
6971
  } catch (error) {
6030
- if (isNotServingYet(error)) return "todo";
6972
+ if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
6031
6973
  throw error;
6032
6974
  }
6033
6975
  },
6034
6976
  async run(ctx) {
6035
6977
  const resolved = deps(ctx);
6036
6978
  const configured = requireConfiguredRuntime(ctx.state);
6037
- const apiKey = requireApiKey(ctx.state);
6979
+ const apiKey = requireApiKey(ctx);
6038
6980
  const docker = await resolved.runtime.docker.available();
6039
6981
  if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
6040
6982
  const choice = await persistedRecord(ctx, resolved);
@@ -6149,115 +7091,235 @@ function hostDeps$1(ctx) {
6149
7091
  function isNotServing$1(error) {
6150
7092
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6151
7093
  }
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`;
7094
+ function loginLine(kind, email) {
7095
+ switch (kind) {
7096
+ case "generated":
7097
+ case "legacy-default": return `Login: ${email} (rde credentials shows the password)`;
7098
+ case "prompted": return `Login: ${email} with the password you entered`;
6157
7099
  }
6158
7100
  }
6159
- async function databaseLines$1(state, deps) {
6160
- if (state.url === null || state.apiKey === null) return ["Databases: none connected yet"];
7101
+ async function databaseLines$1(state, apiKey, deps) {
7102
+ if (state.url === null || apiKey === null) return ["Databases: none connected yet"];
6161
7103
  let databases;
6162
7104
  try {
6163
7105
  databases = (await deps.client(state.url, {
6164
7106
  kind: "api-key",
6165
- key: state.apiKey
7107
+ key: apiKey
6166
7108
  }).database.list()).data;
6167
7109
  } catch (error) {
6168
7110
  if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
7111
+ if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
6169
7112
  throw error;
6170
7113
  }
6171
- if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
6172
- return ["Databases:", ...existingDatabaseLines(databases).map((line) => ` ${line}`)];
6173
- }
6174
- function remoteLine(remote) {
6175
- switch (remote.kind) {
6176
- case "local-bare": return `Remote: local bare repository ${remote.barePath}`;
6177
- case "hosted": return `Remote: ${remote.url}`;
7114
+ if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
7115
+ return ["Databases:", ...existingDatabaseLines(databases).map((line) => ` ${line}`)];
7116
+ }
7117
+ function remoteLine(remote) {
7118
+ switch (remote.kind) {
7119
+ case "local-bare": return `Remote: local bare repository ${remote.barePath}`;
7120
+ case "hosted": return `Remote: ${remote.url}`;
7121
+ }
7122
+ }
7123
+ function notConfiguredReason(state) {
7124
+ const license = state.license;
7125
+ if (!license.activated) return "no license";
7126
+ if (!hasRemoteSync(license)) return "the license has no remote-sync feature";
7127
+ if (state.steps[GIT_SYNC_STEP_ID]?.status === "skipped") return "skipped; `rde init --only git-sync` sets it up";
7128
+ return "not set up yet; `rde init --only git-sync`";
7129
+ }
7130
+ function gitLines(state) {
7131
+ if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
7132
+ return [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
7133
+ }
7134
+ function skillLine(entries) {
7135
+ const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
7136
+ if (agents.length === 0) return `rde skill: not installed; \`rde init --only agents-skills\``;
7137
+ return `rde skill: installed for ${agents.join(", ")}`;
7138
+ }
7139
+ function launchLines(agents, workdir) {
7140
+ const detected = agents.filter((agent) => agent.detected);
7141
+ if (detected.length === 0) return [];
7142
+ const prefix = workdir === null ? "" : `cd ${workdir} && `;
7143
+ return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
7144
+ }
7145
+ async function summaryLines(state, apiKey, deps) {
7146
+ const profile = state.mbProfile;
7147
+ const lines = [];
7148
+ if (state.url !== null) lines.push(`Metabase: ${state.url}`);
7149
+ if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
7150
+ lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
7151
+ lines.push(...await databaseLines$1(state, apiKey, deps));
7152
+ if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
7153
+ lines.push(...gitLines(state));
7154
+ const installed = await skillsCli(deps.runner).list();
7155
+ lines.push(skillLine(installed.filter((entry) => entry.name === "rde")));
7156
+ lines.push(...launchLines(await detectAgents(deps.probe), state.gitRemote?.workdir ?? null));
7157
+ lines.push("Lifecycle: rde status, rde stop, rde start");
7158
+ return lines;
7159
+ }
7160
+ function summaryStep(deps = hostDeps$1) {
7161
+ return {
7162
+ id: STEP_ID,
7163
+ title: "Summary",
7164
+ async detect() {
7165
+ return "todo";
7166
+ },
7167
+ async run(ctx) {
7168
+ const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
7169
+ ctx.ui.note(lines.join("\n"), "Your environment");
7170
+ return "completed";
7171
+ }
7172
+ };
7173
+ }
7174
+ //#endregion
7175
+ //#region src/steps/index.ts
7176
+ const DEFAULT_STEP_OPTIONS = {
7177
+ runtime: NO_RUNTIME_FLAGS,
7178
+ agents: DETECT_AGENTS,
7179
+ admin: DEFAULT_ADMIN,
7180
+ databases: SAMPLE_STANDS_IN
7181
+ };
7182
+ const STEP_BY_ID = {
7183
+ preflight: () => preflightStep(),
7184
+ "install-mb": () => installMbStep(),
7185
+ "agents-skills": (options) => agentsSkillsStep(options.agents),
7186
+ runtime: (options) => runtimeStep(options.runtime),
7187
+ "start-metabase": () => startMetabaseStep(),
7188
+ "admin-user": (options) => adminUserStep(options.admin),
7189
+ "api-key": () => apiKeyStep(),
7190
+ "sample-database": () => sampleDatabaseStep(),
7191
+ license: () => licenseStep(),
7192
+ databases: (options) => databasesStep(options.databases),
7193
+ "git-sync": () => gitSyncStep(),
7194
+ summary: () => summaryStep()
7195
+ };
7196
+ function initSteps(options) {
7197
+ return StepId.options.map((id) => STEP_BY_ID[id](options));
7198
+ }
7199
+ //#endregion
7200
+ //#region src/wizard/answers.ts
7201
+ function isOneOf(answer, kinds) {
7202
+ return kinds.some((kind) => kind === answer.kind);
7203
+ }
7204
+ function rejected(key, prompt, reason) {
7205
+ return new ConfigError(`answers file: "${key}" for "${prompt.message}" ${reason}`);
7206
+ }
7207
+ function expectKind(taken, prompt, kinds) {
7208
+ if (taken.answer.kind === "cancel") throw new AbortError();
7209
+ if (isOneOf(taken.answer, kinds)) return taken.answer;
7210
+ throw rejected(taken.key, prompt, `is a ${taken.answer.kind} answer; expected ${kinds.join(" or ")}`);
7211
+ }
7212
+ function choiceLabels(choices) {
7213
+ return choices.map((choice) => String(choice.value)).join(", ");
7214
+ }
7215
+ function matchChoice(key, prompt, value) {
7216
+ const match = prompt.choices.find((choice) => choice.value === value);
7217
+ if (match === void 0) throw rejected(key, prompt, `is ${String(value)}, not one of the choices ${choiceLabels(prompt.choices)}`);
7218
+ return match.value;
7219
+ }
7220
+ function textValue(taken, prompt) {
7221
+ const answer = expectKind(taken, prompt, ["text"]);
7222
+ const value = answer.value.length === 0 && prompt.defaultValue !== void 0 ? prompt.defaultValue : answer.value;
7223
+ const problem = prompt.validate?.(value);
7224
+ if (problem !== void 0) throw rejected(taken.key, prompt, `is "${value}", which is rejected: ${problem}`);
7225
+ return value;
7226
+ }
7227
+ function passwordValue(taken, prompt) {
7228
+ const answer = expectKind(taken, prompt, ["password", "text"]);
7229
+ const problem = prompt.validate?.(answer.value);
7230
+ if (problem !== void 0) throw rejected(taken.key, prompt, `is rejected: ${problem}`);
7231
+ return answer.value;
7232
+ }
7233
+ function isBackAnswer(answer) {
7234
+ return answer.kind === "back";
7235
+ }
7236
+ function answeringPrompts(base, answers, options = {}) {
7237
+ function lookup(prompt) {
7238
+ if (prompt.key === void 0) return null;
7239
+ const answer = answers.get(prompt.key)?.shift();
7240
+ return answer === void 0 ? null : {
7241
+ key: prompt.key,
7242
+ answer
7243
+ };
7244
+ }
7245
+ function refuseUnanswered(prompt) {
7246
+ const refusal = options.unanswered?.(prompt) ?? null;
7247
+ if (refusal !== null) throw refusal;
7248
+ }
7249
+ function take(prompt) {
7250
+ const taken = lookup(prompt);
7251
+ if (taken === null) refuseUnanswered(prompt);
7252
+ return taken;
7253
+ }
7254
+ function takeText(prompt) {
7255
+ const taken = lookup(prompt);
7256
+ if (taken !== null) return taken;
7257
+ if (prompt.defaultValue !== void 0 && options.unanswered !== void 0) return {
7258
+ key: prompt.key ?? prompt.message,
7259
+ answer: {
7260
+ kind: "text",
7261
+ value: ""
7262
+ }
7263
+ };
7264
+ refuseUnanswered(prompt);
7265
+ return null;
7266
+ }
7267
+ function selected(taken, prompt) {
7268
+ const answer = expectKind(taken, prompt, ["select", "text"]);
7269
+ return matchChoice(taken.key, prompt, answer.value);
7270
+ }
7271
+ function multiselected(taken, prompt) {
7272
+ return expectKind(taken, prompt, ["multiselect"]).values.map((value) => matchChoice(taken.key, prompt, value));
6178
7273
  }
6179
- }
6180
- function notConfiguredReason(state) {
6181
- const license = state.license;
6182
- if (!license.activated) return "no license";
6183
- if (!hasRemoteSync(license)) return "the license has no remote-sync feature";
6184
- if (state.steps[GIT_SYNC_STEP_ID]?.status === "skipped") return "skipped; `rde init --only git-sync` sets it up";
6185
- return "not set up yet; `rde init --only git-sync`";
6186
- }
6187
- function gitLines(state) {
6188
- if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
6189
- return [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
6190
- }
6191
- function skillLine(entries) {
6192
- const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
6193
- if (agents.length === 0) return `rde skill: not installed; \`rde init --only agents-skills\``;
6194
- return `rde skill: installed for ${agents.join(", ")}`;
6195
- }
6196
- function launchLines(agents, workdir) {
6197
- const detected = agents.filter((agent) => agent.detected);
6198
- if (detected.length === 0) return [];
6199
- const prefix = workdir === null ? "" : `cd ${workdir} && `;
6200
- return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
6201
- }
6202
- async function summaryLines(state, deps) {
6203
- const profile = state.mbProfile;
6204
- const lines = [];
6205
- if (state.url !== null) lines.push(`Metabase: ${state.url}`);
6206
- const login = loginLine(state.admin);
6207
- if (login !== null) lines.push(login);
6208
- 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));
6210
- if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
6211
- lines.push(...gitLines(state));
6212
- const installed = await skillsCli(deps.runner).list();
6213
- lines.push(skillLine(installed.filter((entry) => entry.name === "rde")));
6214
- lines.push(...launchLines(await detectAgents(deps.probe), state.gitRemote?.workdir ?? null));
6215
- lines.push("Lifecycle: rde status, rde stop, rde start");
6216
- return lines;
6217
- }
6218
- function summaryStep(deps = hostDeps$1) {
6219
7274
  return {
6220
- id: STEP_ID,
6221
- title: "Summary",
6222
- async detect() {
6223
- return "todo";
7275
+ async text(opts) {
7276
+ const taken = takeText(opts);
7277
+ return taken === null ? base.text(opts) : textValue(taken, opts);
6224
7278
  },
6225
- async run(ctx) {
6226
- ctx.ui.note((await summaryLines(ctx.state, deps(ctx))).join("\n"), "Your environment");
6227
- return "completed";
7279
+ async textOrBack(opts) {
7280
+ const taken = takeText(opts);
7281
+ if (taken === null) return base.textOrBack(opts);
7282
+ return isBackAnswer(taken.answer) ? BACK : textValue(taken, opts);
7283
+ },
7284
+ async password(opts) {
7285
+ const taken = take(opts);
7286
+ return taken === null ? base.password(opts) : passwordValue(taken, opts);
7287
+ },
7288
+ async confirm(opts) {
7289
+ const taken = lookup(opts);
7290
+ if (taken !== null) return expectKind(taken, opts, ["confirm"]).value;
7291
+ if (options.confirmAll === true) return true;
7292
+ refuseUnanswered(opts);
7293
+ return base.confirm(opts);
7294
+ },
7295
+ async select(opts) {
7296
+ const taken = lookup(opts);
7297
+ if (taken !== null) return selected(taken, opts);
7298
+ if (options.takeRecommended === true && opts.recommended !== void 0) return opts.recommended;
7299
+ refuseUnanswered(opts);
7300
+ return base.select(opts);
7301
+ },
7302
+ async selectOrBack(opts) {
7303
+ const taken = take(opts);
7304
+ if (taken === null) return base.selectOrBack(opts);
7305
+ return isBackAnswer(taken.answer) ? BACK : selected(taken, opts);
7306
+ },
7307
+ async multiselect(opts) {
7308
+ const taken = take(opts);
7309
+ return taken === null ? base.multiselect(opts) : multiselected(taken, opts);
7310
+ },
7311
+ async multiselectOrBack(opts) {
7312
+ const taken = take(opts);
7313
+ if (taken === null) return base.multiselectOrBack(opts);
7314
+ return isBackAnswer(taken.answer) ? BACK : multiselected(taken, opts);
6228
7315
  }
6229
7316
  };
6230
7317
  }
6231
7318
  //#endregion
6232
- //#region src/steps/index.ts
6233
- const DEFAULT_STEP_OPTIONS = {
6234
- runtime: NO_RUNTIME_FLAGS,
6235
- agents: DETECT_AGENTS,
6236
- admin: DEFAULT_ADMIN,
6237
- databases: ASK_DATABASES
6238
- };
6239
- const STEP_BY_ID = {
6240
- preflight: () => preflightStep(),
6241
- "install-mb": () => installMbStep(),
6242
- "agents-skills": (options) => agentsSkillsStep(options.agents),
6243
- runtime: (options) => runtimeStep(options.runtime),
6244
- "start-metabase": () => startMetabaseStep(),
6245
- "admin-user": (options) => adminUserStep(options.admin),
6246
- "api-key": () => apiKeyStep(),
6247
- "sample-database": () => sampleDatabaseStep(),
6248
- license: () => licenseStep(),
6249
- databases: (options) => databasesStep(options.databases),
6250
- "git-sync": () => gitSyncStep(),
6251
- summary: () => summaryStep()
6252
- };
6253
- function initSteps(options) {
6254
- return StepId.options.map((id) => STEP_BY_ID[id](options));
6255
- }
6256
- //#endregion
6257
7319
  //#region src/commands/doctor.ts
6258
7320
  function print(report, json) {
6259
7321
  if (json) {
6260
- printJson(report);
7322
+ printJson(doctorJson(report));
6261
7323
  return;
6262
7324
  }
6263
7325
  printLines(doctorLines(report));
@@ -6288,12 +7350,16 @@ const doctor = defineRdeCommand({
6288
7350
  const name = await instanceNameOrDefault();
6289
7351
  const state = await readInstanceState(name);
6290
7352
  const deps = hostDoctorDeps(name, interruptSignal);
6291
- let report = await collectDoctor(name, state, deps);
7353
+ let report = await collectDoctor(name, state, state === null ? null : await readInstanceSecrets(name), deps);
6292
7354
  if (args.fix && report.failed > 0) {
6293
7355
  if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
6294
7356
  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);
7357
+ const prompts = answeringPrompts(clackPrompts(), /* @__PURE__ */ new Map(), { takeRecommended: args.yes || !stdinIsTty() });
7358
+ const ctx = await createWizardContext(name, {
7359
+ signal: interruptSignal,
7360
+ prompts
7361
+ });
7362
+ if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
6297
7363
  }
6298
7364
  print(report, args.json);
6299
7365
  if (report.failed > 0) throw new Error(doctorSummary(report));
@@ -6318,7 +7384,7 @@ const EnvString = z.string().transform((value, ctx) => {
6318
7384
  }
6319
7385
  return resolved;
6320
7386
  });
6321
- const AdminVariant = z.enum(["default", "prompted"]);
7387
+ const AdminVariant = z.enum(["generated", "prompted"]);
6322
7388
  const RemoteKind = z.enum(["local-bare", "hosted"]);
6323
7389
  const FieldValue = z.union([
6324
7390
  EnvString,
@@ -6422,7 +7488,18 @@ function runtimeKindOf(runtime) {
6422
7488
  if (runtime.jar !== void 0) return "jar";
6423
7489
  return null;
6424
7490
  }
6425
- function stepOptionsFrom(answers) {
7491
+ function databasesRequest(databases, run) {
7492
+ if (databases !== void 0) return databases.length > 0 ? "named" : "sample-stands-in";
7493
+ return run.interactive ? "ask-once" : "sample-stands-in";
7494
+ }
7495
+ function agentsOption(agents, run) {
7496
+ if (agents !== void 0) return {
7497
+ kind: "listed",
7498
+ agents: agents.agents
7499
+ };
7500
+ return run.interactive ? { kind: "ask" } : DEFAULT_STEP_OPTIONS.agents;
7501
+ }
7502
+ function stepOptionsFrom(answers, run) {
6426
7503
  const runtime = answers.runtime;
6427
7504
  const agents = answers["agents-skills"];
6428
7505
  const admin = answers["admin-user"];
@@ -6434,9 +7511,9 @@ function stepOptionsFrom(answers) {
6434
7511
  jar: runtime.jar ?? null,
6435
7512
  port: runtime.port ?? null
6436
7513
  },
6437
- agents: agents === void 0 ? DEFAULT_STEP_OPTIONS.agents : { agents: agents.agents },
7514
+ agents: agentsOption(agents, run),
6438
7515
  admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
6439
- databases: { requested: databases !== void 0 && databases.length > 0 }
7516
+ databases: { request: databasesRequest(databases, run) }
6440
7517
  };
6441
7518
  }
6442
7519
  function keyedAnswers(answers) {
@@ -6554,7 +7631,7 @@ function parseDatabase(spec) {
6554
7631
  function adminSection(flags, secrets) {
6555
7632
  const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
6556
7633
  if (flags.admin === void 0 && !prompted) return;
6557
- if (flags.admin === "default" && prompted) throw conflict("--admin default", "--admin-email/--admin-password");
7634
+ if (flags.admin === "generated" && prompted) throw conflict("--admin generated", "--admin-email/--admin-password");
6558
7635
  return {
6559
7636
  variant: flags.admin ?? "prompted",
6560
7637
  ...flags.adminEmail !== void 0 && { email: flags.adminEmail },
@@ -6705,20 +7782,57 @@ function isLocation(value) {
6705
7782
  function isRemote(value) {
6706
7783
  return value === "local-bare" || value === "hosted";
6707
7784
  }
7785
+ function isUnreviewedPaste(database) {
7786
+ return database !== void 0 && database.pasted && !database.reviewed;
7787
+ }
7788
+ function applyConnectionRecord(draft, value) {
7789
+ draft.databases = draft.databases.filter((database) => !database.pasted);
7790
+ if (value === null) return;
7791
+ if (value.trim().length === 0) {
7792
+ draft.databasesFinished = true;
7793
+ return;
7794
+ }
7795
+ draft.databases.push({
7796
+ engine: null,
7797
+ uri: value.trim(),
7798
+ fields: {},
7799
+ name: null,
7800
+ pasted: true,
7801
+ reviewed: false
7802
+ });
7803
+ }
7804
+ function applyEngineRecord(draft, value) {
7805
+ if (isUnreviewedPaste(draft.databases.at(-1))) draft.databases.pop();
7806
+ if (value === "finish") draft.databasesFinished = true;
7807
+ else if (value !== null) draft.databases.push({
7808
+ engine: value,
7809
+ uri: null,
7810
+ fields: {},
7811
+ name: null,
7812
+ pasted: false,
7813
+ reviewed: false
7814
+ });
7815
+ }
6708
7816
  function applyDatabaseRecord(draft, record) {
6709
7817
  const value = stringOf(record.answer);
7818
+ if (record.key === DATABASE_KEYS.connection) {
7819
+ applyConnectionRecord(draft, value);
7820
+ return;
7821
+ }
6710
7822
  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
- });
7823
+ applyEngineRecord(draft, value);
6718
7824
  return;
6719
7825
  }
6720
7826
  const database = draft.databases.at(-1);
6721
7827
  if (database === void 0) return;
7828
+ if (record.key === DATABASE_KEYS.detectedEngine && value !== null) {
7829
+ database.engine = value;
7830
+ return;
7831
+ }
7832
+ if (record.key === DATABASE_KEYS.action) {
7833
+ database.reviewed = true;
7834
+ return;
7835
+ }
6722
7836
  if (record.key === DATABASE_KEYS.uri && value !== null) {
6723
7837
  database.uri = value;
6724
7838
  return;
@@ -6746,12 +7860,19 @@ function applyGitRecord(draft, key, value) {
6746
7860
  else if (key === GIT_SYNC_KEYS.url && value !== null) gitSync(draft).url = value;
6747
7861
  else if (key === GIT_SYNC_KEYS.token) gitSync(draft).token = envReference(ENV_GIT_TOKEN);
6748
7862
  }
7863
+ function agentIds(values) {
7864
+ return values.flatMap((value) => {
7865
+ const parsed = AgentId.safeParse(value);
7866
+ return parsed.success ? [parsed.data] : [];
7867
+ });
7868
+ }
6749
7869
  function applyRecord(draft, record) {
6750
7870
  const { key } = record;
6751
7871
  const value = stringOf(record.answer);
6752
7872
  if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
6753
7873
  else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
6754
7874
  else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
7875
+ else if (key === AGENTS_SKILLS_KEYS.agents && record.answer.kind === "multiselect") draft.answers["agents-skills"] = { agents: agentIds(record.answer.values) };
6755
7876
  else if (key === API_KEY_KEYS.profile && value !== null) {
6756
7877
  const profile = MbProfileChoice.safeParse(value);
6757
7878
  if (profile.success) draft.answers["api-key"] = { profile: profile.data };
@@ -6760,11 +7881,17 @@ function applyRecord(draft, record) {
6760
7881
  function writableUri(uri, index) {
6761
7882
  return URI_WITH_PASSWORD.test(uri) ? envReference(databaseEnv(index, "uri")) : uri;
6762
7883
  }
7884
+ function engineOf(database) {
7885
+ if (database.engine !== null) return database.engine;
7886
+ const [only, ...others] = database.uri === null ? [] : detectEngine(database.uri);
7887
+ return only !== void 0 && others.length === 0 ? only : null;
7888
+ }
6763
7889
  function databaseEntry(database, index) {
6764
7890
  const hasFields = Object.keys(database.fields).length > 0;
6765
- if (database.uri === null && !hasFields) return null;
7891
+ const engine = engineOf(database);
7892
+ if (database.uri === null && !hasFields || engine === null) return null;
6766
7893
  return {
6767
- engine: database.engine,
7894
+ engine,
6768
7895
  ...database.uri !== null && { uri: writableUri(database.uri, index) },
6769
7896
  ...database.uri === null && { fields: database.fields },
6770
7897
  ...database.name !== null && { name: database.name }
@@ -6789,122 +7916,6 @@ async function writeAnswersFile(path, answers) {
6789
7916
  await promises.writeFile(path, JSON.stringify(answers, null, 2) + "\n", { mode: FILE_MODE });
6790
7917
  }
6791
7918
  //#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
7919
  //#region src/wizard/record.ts
6909
7920
  function recordingPrompts(base) {
6910
7921
  const records = [];
@@ -6981,9 +7992,11 @@ async function combinedAnswers(options) {
6981
7992
  return mergeInitAnswers(await loadInitAnswers(options.answersPath), fromFlags);
6982
7993
  }
6983
7994
  function promptsFor(answers, yes) {
7995
+ const terminal = stdinIsTty();
6984
7996
  const answering = {
6985
- ...!stdinIsTty() && { unanswered: refusalWithoutTerminal },
6986
- ...yes && { confirmAll: true }
7997
+ ...!terminal && { unanswered: refusalWithoutTerminal },
7998
+ ...yes && { confirmAll: true },
7999
+ ...(yes || !terminal) && { takeRecommended: true }
6987
8000
  };
6988
8001
  return answeringPrompts(clackPrompts(), keyedAnswers(answers), answering);
6989
8002
  }
@@ -7027,7 +8040,7 @@ const init = defineRdeCommand({
7027
8040
  },
7028
8041
  admin: {
7029
8042
  type: "string",
7030
- description: "Admin credentials: default (admin@metabase.com / metabase123) or prompted"
8043
+ description: "Admin credentials: generated (a random password; `rde credentials` shows it) or prompted"
7031
8044
  },
7032
8045
  "admin-email": {
7033
8046
  type: "string",
@@ -7078,7 +8091,7 @@ const init = defineRdeCommand({
7078
8091
  },
7079
8092
  agents: {
7080
8093
  type: "string",
7081
- description: "Agents to install the skill for, comma-separated; the detected ones by default"
8094
+ description: "Agents to install the skill for, comma-separated (a missing opencode is installed); asked at a terminal, the detected ones otherwise"
7082
8095
  },
7083
8096
  "skip-agents": {
7084
8097
  type: "boolean",
@@ -7087,7 +8100,7 @@ const init = defineRdeCommand({
7087
8100
  },
7088
8101
  "mb-profile": {
7089
8102
  type: "string",
7090
- description: "mb profile for this instance when `default` already points elsewhere: default or rde"
8103
+ description: "mb profile for the default instance when `default` already points at another Metabase: default (replace it) or rde"
7091
8104
  },
7092
8105
  yes: {
7093
8106
  type: "boolean",
@@ -7136,7 +8149,7 @@ const init = defineRdeCommand({
7136
8149
  prompts: recording?.prompts ?? answering
7137
8150
  });
7138
8151
  ctx.ui.intro("rde init");
7139
- await runWizard(initSteps(stepOptionsFrom(answers)), ctx, options);
8152
+ await runWizard(initSteps(stepOptionsFrom(answers, { interactive: stdinIsTty() && !runOptions.yes })), ctx, options);
7140
8153
  if (recording !== null && printTo !== void 0) {
7141
8154
  await writeAnswersFile(printTo, answersFromRecords(recording.records, answers));
7142
8155
  ctx.ui.info(`Answers written to ${printTo}`);
@@ -7243,11 +8256,11 @@ function isNotServing(error) {
7243
8256
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7244
8257
  }
7245
8258
  async function databasesOf(ctx, deps, health) {
7246
- if (health !== "healthy" || ctx.state.apiKey === null) return null;
8259
+ if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
7247
8260
  try {
7248
- return await listDatabases(ctx.state, { client: deps.client });
8261
+ return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
7249
8262
  } catch (error) {
7250
- if (isNotServing(error)) return null;
8263
+ if (isNotServing(error) || isRejectedCredential(error)) return null;
7251
8264
  throw error;
7252
8265
  }
7253
8266
  }
@@ -7290,9 +8303,13 @@ async function collectStatus(ctx, deps) {
7290
8303
  const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
7291
8304
  const health = configured === null ? null : await healthOf(configured.url, deps);
7292
8305
  const databases = health === null ? null : await databasesOf(ctx, deps, health);
8306
+ const admin = ctx.state.admin;
7293
8307
  return {
7294
8308
  instance: ctx.state.name,
7295
8309
  url: configured?.url ?? null,
8310
+ mbProfile: ctx.state.mbProfile,
8311
+ adminEmail: admin === null ? null : admin.email,
8312
+ adminVariant: admin === null ? null : admin.kind,
7296
8313
  runtime: configured === null || runtime === null ? null : {
7297
8314
  kind: configured.runtime.kind,
7298
8315
  installed: runtime.installed,
@@ -7346,6 +8363,8 @@ function databaseLines(report) {
7346
8363
  function statusLines(report) {
7347
8364
  return [
7348
8365
  report.url === null ? `Instance ${report.instance}` : `Instance ${report.instance} — ${report.url}`,
8366
+ ...report.adminVariant === null || report.adminEmail === null ? [] : [loginLine(report.adminVariant, report.adminEmail)],
8367
+ `Metabase CLI: ${mbCommand(report.mbProfile)} <command>`,
7349
8368
  runtimeLine(report),
7350
8369
  versionLine(report),
7351
8370
  `Health: ${report.health ?? "unknown"}`,
@@ -7648,7 +8667,7 @@ async function planUninstall(options, deps) {
7648
8667
  if (jar.removal !== null) removals.push(jar.removal);
7649
8668
  if (jar.kept !== null) kept.push(jar.kept);
7650
8669
  }
7651
- const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, "rde"], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
8670
+ const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, namedMbProfile(options.instance)], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
7652
8671
  removals.push(...profiles);
7653
8672
  if (options.removeSkill) {
7654
8673
  const skill = await findSkill(deps.runner);
@@ -7657,6 +8676,11 @@ async function planUninstall(options, deps) {
7657
8676
  const dir = instanceDir(options.instance);
7658
8677
  const dirPresent = await pathExists(dir);
7659
8678
  if (!options.keepData) {
8679
+ const secrets = instanceSecretsPath(options.instance);
8680
+ if (await pathExists(secrets)) removals.push({
8681
+ label: `Stored credentials ${secrets} (the admin password and the API key)`,
8682
+ remove: () => promises.rm(secrets, { force: true })
8683
+ });
7660
8684
  if (dirPresent) removals.push({
7661
8685
  label: instanceDirLabel(dir, state),
7662
8686
  remove: () => promises.rm(dir, {
@@ -8058,7 +9082,7 @@ const upgradeCommand = defineRdeCommand({
8058
9082
  const main = defineCommand({
8059
9083
  meta: {
8060
9084
  name: "rde",
8061
- version: "0.1.3",
9085
+ version: "0.1.5",
8062
9086
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
8063
9087
  },
8064
9088
  subCommands: {
@@ -8066,6 +9090,7 @@ const main = defineCommand({
8066
9090
  start,
8067
9091
  stop,
8068
9092
  status,
9093
+ credentials,
8069
9094
  logs,
8070
9095
  open,
8071
9096
  db,