@metabase/rde 0.1.3-alpha.rde-fixes.c2021e7 → 0.1.3

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