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