@metabase/rde 0.1.13 → 0.1.14

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 +3 -3
  2. package/dist/cli.mjs +318 -55
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -24,7 +24,7 @@ rde init
24
24
 
25
25
  ## What `rde init` does
26
26
 
27
- The wizard shows its checklist first (`✓` done, `○` to do, `–` not applicable), runs the steps still to do in order, and ends with a summary of your environment. A first run on a machine that has Docker but not yet `mb` starts like this:
27
+ The wizard shows its checklist first (`✓` done, `○` to do, `↑` done but behind what is published, `–` not applicable), runs the steps still to do in order, and ends with a summary of your environment. A first run on a machine that has Docker but not yet `mb` starts like this:
28
28
 
29
29
  ```
30
30
  ┌ rde init
@@ -56,8 +56,8 @@ The git step reads `–` until a license with remote sync is active; it is check
56
56
  | Step | What it does |
57
57
  | --- | --- |
58
58
  | Check this machine | Platform, Node version, git, and a reachable Docker daemon or a Java 21+ runtime |
59
- | Install the Metabase CLI | `@metabase/cli` (`mb`) at the `alpha-release-65-integration-branch` dist-tag, with the package manager that installed `rde` |
60
- | Install the rde and data-app skills | The data-app skills (`metabase-data-app-setup`, `-routing`, `-actions`, `-semantic-layer`) from `metabase/metabase/skills#master`, then the `rde` and `rde-data-apps` skills from the `main` branch of `metabase/agent-skills` (the rde skill hands an explicit app request to `rde-data-apps`, which hands the build to the data-app skills), for the coding agents you pick at a terminal, from the ones found on this machine (Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Windsurf). When none is found, the step is skipped and says how to add the skills once an agent is installed; `rde` installs no agent. Without a terminal, or with `--yes`, every agent found gets the skills; `--agents` picks a set ahead of time (the skills are installed for them whether or not they are on the machine yet), `--skip-agents` none; `RDE_SKILL_SOURCE` installs the two rde skills from another URL or a local checkout of `metabase/agent-skills` |
59
+ | Install the Metabase CLI | `@metabase/cli` (`mb`) at the `alpha-release-65-integration-branch` dist-tag, with the package manager that installed `rde`; an installed `mb` older than what the dist-tag names now is updated. `rde status` and `rde doctor` check only what is installed and ask no registry |
60
+ | Install the rde and data-app skills | The data-app skills (`metabase-data-app-setup`, `-routing`, `-actions`, `-semantic-layer`, `-migrate`) from `metabase/metabase/skills#master`, then the `rde` and `rde-data-apps` skills from the `main` branch of `metabase/agent-skills` (the rde skill hands an explicit app request to `rde-data-apps`, which hands the build to the data-app skills), for the coding agents you pick at a terminal, from the ones found on this machine (Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Windsurf). When none is found, the step is skipped and says how to add the skills once an agent is installed; `rde` installs no agent. Without a terminal, or with `--yes`, every agent found gets the skills; `--agents` picks a set ahead of time (the skills are installed for them whether or not they are on the machine yet), `--skip-agents` none; `RDE_SKILL_SOURCE` installs the two rde skills from another URL or a local checkout of `metabase/agent-skills`. Installed skills whose directory changed on its branch since are installed again, without a question when every detected agent already has them all |
61
61
  | Install Metabase | **Docker** when it is available: pulls `metabase/metabase-enterprise-head:latest` (or `--image`) and creates the container. **Jar** otherwise, or with `--runtime jar`: downloads the latest enterprise release (or the tag or `.jar` path given as `--jar`) into `~/.rde/jars/` and runs it with your Java. Either way on port 23000, or the first free port above it that nothing else claims, or `--port` |
62
62
  | Start Metabase | Starts the container or the jar and waits for `/api/health`, showing Metabase's own progress |
63
63
  | Create the admin user | `admin@metabase.local` with a generated password that `rde credentials` shows, or your own with `--admin prompted`, see [Credentials](#credentials) |
package/dist/cli.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { defineCommand, renderUsage, runMain } from "citty";
3
3
  import { createHash, randomBytes, randomUUID } from "node:crypto";
4
4
  import { createWriteStream, promises } from "node:fs";
5
- import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
5
+ import { delimiter, dirname, isAbsolute, join, posix, resolve, sep } from "node:path";
6
6
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
7
7
  import { ZodError, z } from "zod";
8
8
  import { homedir, tmpdir, userInfo } from "node:os";
@@ -71,12 +71,14 @@ var MetabaseApiError = class extends Error {
71
71
  status;
72
72
  path;
73
73
  errors;
74
+ reason;
74
75
  constructor(failure) {
75
76
  super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
76
77
  this.name = "MetabaseApiError";
77
78
  this.status = failure.status;
78
79
  this.path = failure.path;
79
80
  this.errors = failure.errors;
81
+ this.reason = failure.message;
80
82
  }
81
83
  };
82
84
  const UNAUTHORIZED_STATUS = 401;
@@ -93,10 +95,12 @@ var MetabaseInitializingError = class extends Error {
93
95
  };
94
96
  var MetabaseUnreachableError = class extends Error {
95
97
  url;
98
+ reason;
96
99
  constructor(url, reason) {
97
100
  super(`Metabase at ${url} is unreachable: ${reason}`);
98
101
  this.name = "MetabaseUnreachableError";
99
102
  this.url = url;
103
+ this.reason = reason;
100
104
  }
101
105
  };
102
106
  var DownloadError = class extends Error {
@@ -892,6 +896,11 @@ function describeCause(error) {
892
896
  if (error instanceof Error && error.cause instanceof Error) return error.cause.message;
893
897
  return error instanceof Error ? error.message : String(error);
894
898
  }
899
+ function lookupFailure(error) {
900
+ if (error instanceof MetabaseUnreachableError) return `${error.url} is unreachable: ${error.reason}`;
901
+ if (error instanceof MetabaseApiError) return `HTTP ${error.status} on ${error.path}: ${error.reason}`;
902
+ return null;
903
+ }
895
904
  function createHttp(options) {
896
905
  const fetchImpl = options.fetchImpl ?? fetch;
897
906
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
@@ -911,7 +920,11 @@ function createHttp(options) {
911
920
  init.body = JSON.stringify(body);
912
921
  }
913
922
  try {
914
- return await fetchImpl(url, init);
923
+ const response = await fetchImpl(url, init);
924
+ return {
925
+ ok: response.ok,
926
+ body: parseBody(response.status, await response.text())
927
+ };
915
928
  } catch (error) {
916
929
  if (options.signal.aborted) throw new AbortError();
917
930
  if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${REQUEST_TIMEOUT_MS}ms`);
@@ -919,10 +932,9 @@ function createHttp(options) {
919
932
  }
920
933
  }
921
934
  async function requestBody(method, path, body) {
922
- const response = await send(method, path, body);
923
- const parsed = parseBody(response.status, await response.text());
924
- if (!response.ok) throw failureFor(path, parsed);
925
- return parsed;
935
+ const received = await send(method, path, body);
936
+ if (!received.ok) throw failureFor(path, received.body);
937
+ return received.body;
926
938
  }
927
939
  return {
928
940
  async requestJson(method, path, request) {
@@ -3284,7 +3296,9 @@ function adminUserStep(options, deps = hostDeps$14) {
3284
3296
  //#endregion
3285
3297
  //#region src/tools/mb.ts
3286
3298
  const MB = "mb";
3287
- const MB_INSTALL_SPEC = `@metabase/cli@alpha-release-65-integration-branch`;
3299
+ const MB_PACKAGE = "@metabase/cli";
3300
+ const MB_DIST_TAG = "alpha-release-65-integration-branch";
3301
+ const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
3288
3302
  const GLOBAL_INSTALL = {
3289
3303
  npm: {
3290
3304
  command: "npm",
@@ -3458,7 +3472,6 @@ const AuthProfileStatus = z.enum([
3458
3472
  const AuthProfile = z.object({
3459
3473
  profile: z.string(),
3460
3474
  url: z.string(),
3461
- authenticated: z.boolean(),
3462
3475
  status: AuthProfileStatus,
3463
3476
  user: ProbedUser.nullable()
3464
3477
  }).loose();
@@ -3540,7 +3553,14 @@ function mbTool(runner) {
3540
3553
  ], LogoutResult);
3541
3554
  }
3542
3555
  async function authList() {
3543
- return (await mbJson(["auth", "list"], AuthList)).data;
3556
+ return (await mbJson([
3557
+ "auth",
3558
+ "list",
3559
+ "--fields",
3560
+ "profile,url,status,user",
3561
+ "--max-bytes",
3562
+ "0"
3563
+ ], AuthList)).data;
3544
3564
  }
3545
3565
  async function gitSyncStatus(profile) {
3546
3566
  return mbJson([
@@ -6116,17 +6136,45 @@ function gitSyncStep(deps = hostDeps$12) {
6116
6136
  };
6117
6137
  }
6118
6138
  //#endregion
6139
+ //#region src/metabase/domain/github-content.ts
6140
+ const GitHubContentEntry = z.object({
6141
+ name: z.string(),
6142
+ type: z.string(),
6143
+ sha: z.string()
6144
+ }).loose();
6145
+ const GitHubDirectory = z.array(GitHubContentEntry);
6146
+ //#endregion
6147
+ //#region src/metabase/github.ts
6148
+ const GITHUB_API = "https://api.github.com";
6149
+ const DIRECTORY_TYPE = "dir";
6150
+ function githubContents(signal, options = {}) {
6151
+ const http = createHttp({
6152
+ baseUrl: GITHUB_API,
6153
+ auth: NO_AUTH,
6154
+ signal,
6155
+ ...options
6156
+ });
6157
+ return { async subdirectoryHashes(repository, ref, directory) {
6158
+ const path = `/repos/${repository}/contents/${directory}${ref === null ? "" : `?ref=${encodeURIComponent(ref)}`}`;
6159
+ const entries = await http.requestJson("GET", path, { schema: GitHubDirectory });
6160
+ return new Map(entries.filter((entry) => entry.type === DIRECTORY_TYPE).map((entry) => [entry.name, entry.sha]));
6161
+ } };
6162
+ }
6163
+ //#endregion
6119
6164
  //#region src/tools/skills-cli.ts
6120
6165
  const NPX = "npx";
6121
6166
  const SKILLS_PACKAGE = "skills@latest";
6122
6167
  const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
6123
6168
  const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
6124
- const DATA_APP_SKILLS_SOURCE = "metabase/metabase/skills#master";
6169
+ const DATA_APP_REPOSITORY = "metabase/metabase";
6170
+ const DATA_APP_BRANCH = "master";
6171
+ const DATA_APP_SKILLS_SOURCE = `${DATA_APP_REPOSITORY}/skills#${DATA_APP_BRANCH}`;
6125
6172
  const DATA_APP_SKILL_NAMES = [
6126
6173
  "metabase-data-app-setup",
6127
6174
  "metabase-data-app-routing",
6128
6175
  "metabase-data-app-actions",
6129
- "metabase-data-app-semantic-layer"
6176
+ "metabase-data-app-semantic-layer",
6177
+ "metabase-data-app-migrate"
6130
6178
  ];
6131
6179
  function rdeSkillSource() {
6132
6180
  const override = readEnv(ENV_RDE_SKILL_SOURCE);
@@ -6140,7 +6188,13 @@ const InstalledSkill = z.object({
6140
6188
  source: z.string().nullable()
6141
6189
  }).loose();
6142
6190
  const InstalledSkillList = z.array(InstalledSkill);
6143
- const SkillLock = z.object({ skills: z.record(z.string(), z.object({ ref: z.string().nullish() }).loose()) }).loose();
6191
+ const LockedSkill = z.object({
6192
+ source: z.string().nullish(),
6193
+ ref: z.string().nullish(),
6194
+ skillPath: z.string().nullish(),
6195
+ skillFolderHash: z.string().nullish()
6196
+ }).loose();
6197
+ const SkillLock = z.object({ skills: z.record(z.string(), LockedSkill) }).loose();
6144
6198
  const GLOBAL_LOCK_FILE = ".skill-lock.json";
6145
6199
  const PROJECT_LOCK_FILE = "skills-lock.json";
6146
6200
  function globalSkillLockPath(home, stateHome) {
@@ -6153,16 +6207,21 @@ function xdgStateHome() {
6153
6207
  const value = readEnv(ENV_XDG_STATE_HOME);
6154
6208
  return value === void 0 || value.length === 0 ? null : value;
6155
6209
  }
6156
- function lockedRef(path, raw, skill) {
6210
+ function lockedSkill(path, raw, skill) {
6157
6211
  if (raw === null) return null;
6158
6212
  const parsed = SkillLock.safeParse(raw);
6159
6213
  if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
6160
- return parsed.data.skills[skill]?.ref ?? null;
6214
+ return parsed.data.skills[skill] ?? null;
6161
6215
  }
6162
6216
  const hostSkillFiles = {
6163
6217
  readJson: readJsonFile,
6164
6218
  readText: readTextFile
6165
6219
  };
6220
+ const DATA_APP_SKILLS_ORIGIN = {
6221
+ kind: "github",
6222
+ repository: DATA_APP_REPOSITORY,
6223
+ ref: DATA_APP_BRANCH
6224
+ };
6166
6225
  const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
6167
6226
  function skillOrigin(source, cwd) {
6168
6227
  const github = GITHUB_URL.exec(source);
@@ -6252,7 +6311,11 @@ function hostDeps$11(ctx) {
6252
6311
  return {
6253
6312
  runner: processRunner(ctx.signal),
6254
6313
  probe: hostAgentProbe(),
6255
- skillSource: rdeSkillSource()
6314
+ skillSource: rdeSkillSource(),
6315
+ stateHome: xdgStateHome(),
6316
+ files: hostSkillFiles,
6317
+ github: githubContents(ctx.signal),
6318
+ cwd: process.cwd()
6256
6319
  };
6257
6320
  }
6258
6321
  function detectedOnly(agents) {
@@ -6287,10 +6350,91 @@ function coveredAgentNames(entry, agents) {
6287
6350
  function installedLines(entries, agents) {
6288
6351
  return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
6289
6352
  }
6353
+ function skillSets(deps) {
6354
+ return [{
6355
+ label: "data-app",
6356
+ source: DATA_APP_SKILLS_SOURCE,
6357
+ sourceNote: "",
6358
+ origin: DATA_APP_SKILLS_ORIGIN,
6359
+ skills: DATA_APP_SKILL_NAMES
6360
+ }, {
6361
+ label: "rde",
6362
+ source: deps.skillSource,
6363
+ sourceNote: deps.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${deps.skillSource}`,
6364
+ origin: skillOrigin(deps.skillSource, deps.cwd),
6365
+ skills: RDE_SOURCE_SKILL_NAMES
6366
+ }];
6367
+ }
6368
+ function hashLookup(github) {
6369
+ const asked = /* @__PURE__ */ new Map();
6370
+ return (repository, ref, directory) => {
6371
+ const key = JSON.stringify([
6372
+ repository,
6373
+ ref,
6374
+ directory
6375
+ ]);
6376
+ let hashes = asked.get(key);
6377
+ if (hashes === void 0) {
6378
+ hashes = github.subdirectoryHashes(repository, ref, directory).catch((error) => {
6379
+ if (lookupFailure(error) === null) throw error;
6380
+ return null;
6381
+ });
6382
+ asked.set(key, hashes);
6383
+ }
6384
+ return hashes;
6385
+ };
6386
+ }
6387
+ async function readLockedSkills(deps) {
6388
+ const lockPath = globalSkillLockPath(deps.probe.home, deps.stateHome);
6389
+ try {
6390
+ const lock = await deps.files.readJson(lockPath);
6391
+ return new Map(REQUIRED_SKILL_NAMES.map((name) => [name, lockedSkill(lockPath, lock, name)]));
6392
+ } catch (error) {
6393
+ if (error instanceof ConfigError) return null;
6394
+ throw error;
6395
+ }
6396
+ }
6397
+ function lockedCopy(locked, origin) {
6398
+ if (locked === null || locked.source !== origin.repository) return null;
6399
+ const ref = locked.ref ?? null;
6400
+ if (origin.ref !== null && ref !== origin.ref) return null;
6401
+ if (locked.skillPath === null || locked.skillPath === void 0) return null;
6402
+ if (locked.skillFolderHash === null || locked.skillFolderHash === void 0) return null;
6403
+ const folder = posix.dirname(locked.skillPath);
6404
+ return {
6405
+ ref,
6406
+ directory: posix.dirname(folder),
6407
+ folder: posix.basename(folder),
6408
+ hash: locked.skillFolderHash
6409
+ };
6410
+ }
6411
+ async function setBehind(set, check) {
6412
+ const { origin } = set;
6413
+ if (origin.kind !== "github" || check.locked === null) return true;
6414
+ const copies = [];
6415
+ for (const skill of set.skills) {
6416
+ const copy = lockedCopy(check.locked.get(skill) ?? null, origin);
6417
+ if (copy === null) return true;
6418
+ copies.push(copy);
6419
+ }
6420
+ if (check.updates === "installed") return false;
6421
+ for (const copy of copies) {
6422
+ const current = await check.hashes(origin.repository, copy.ref, copy.directory);
6423
+ if (current !== null && current.get(copy.folder) !== copy.hash) return true;
6424
+ }
6425
+ return false;
6426
+ }
6427
+ function hasSkill(installed, skill, agent) {
6428
+ return installed.some((entry) => entry.name === skill && skillCoversAgent(entry, agent));
6429
+ }
6430
+ function coversAll(installed, skills, agents) {
6431
+ return agents.every((agent) => skills.every((skill) => hasSkill(installed, skill, agent)));
6432
+ }
6290
6433
  const SOURCE_NOTES = {
6291
6434
  override: "from --agents",
6292
6435
  detected: "detected on this machine",
6293
- chosen: "as chosen"
6436
+ chosen: "as chosen",
6437
+ installed: "updating the installed copies"
6294
6438
  };
6295
6439
  function agentChoice(agent) {
6296
6440
  return {
@@ -6308,7 +6452,7 @@ async function askAgents(ctx, detected) {
6308
6452
  });
6309
6453
  return detected.filter((agent) => picked.includes(agent.id));
6310
6454
  }
6311
- async function selectAgents(ctx, all, options) {
6455
+ async function selectAgents(ctx, all, options, installedNow) {
6312
6456
  switch (options.kind) {
6313
6457
  case "detected": return {
6314
6458
  agents: detectedOnly(all),
@@ -6327,6 +6471,10 @@ async function selectAgents(ctx, all, options) {
6327
6471
  agents: [],
6328
6472
  source: "detected"
6329
6473
  };
6474
+ if (coversAll(await installedNow(), REQUIRED_SKILL_NAMES, detected)) return {
6475
+ agents: detected,
6476
+ source: "installed"
6477
+ };
6330
6478
  return {
6331
6479
  agents: await askAgents(ctx, detected),
6332
6480
  source: "chosen"
@@ -6337,11 +6485,21 @@ async function selectAgents(ctx, all, options) {
6337
6485
  function skipLine(all, source) {
6338
6486
  switch (source) {
6339
6487
  case "override":
6340
- case "chosen": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
6488
+ case "chosen":
6489
+ case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
6341
6490
  case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
6342
6491
  }
6343
6492
  }
6344
- function agentsSkillsStep(options, deps = hostDeps$11) {
6493
+ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
6494
+ let hashes = null;
6495
+ async function setCheck(resolved) {
6496
+ hashes ??= hashLookup(resolved.github);
6497
+ return {
6498
+ updates,
6499
+ locked: await readLockedSkills(resolved),
6500
+ hashes
6501
+ };
6502
+ }
6345
6503
  return {
6346
6504
  id: STEP_ID$7,
6347
6505
  title: "Install the rde and data-app skills for your coding agents",
@@ -6350,34 +6508,42 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
6350
6508
  const resolved = deps(ctx);
6351
6509
  const detected = detectedOnly(await detectAgents(resolved.probe));
6352
6510
  if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
6353
- return missingSkills(await skillsCli(resolved.runner).list(), detected).length === 0 ? "done" : "todo";
6511
+ if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
6512
+ const check = await setCheck(resolved);
6513
+ for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
6514
+ return "done";
6354
6515
  },
6355
6516
  async run(ctx) {
6356
6517
  const resolved = deps(ctx);
6518
+ const skills = skillsCli(resolved.runner);
6357
6519
  const all = await detectAgents(resolved.probe);
6358
- const selection = await selectAgents(ctx, all, options);
6520
+ let listing = null;
6521
+ const installedNow = () => listing ??= skills.list();
6522
+ const selection = await selectAgents(ctx, all, options, installedNow);
6359
6523
  if (selection.agents.length === 0) {
6360
6524
  ctx.ui.info(skipLine(all, selection.source));
6361
6525
  return { outcome: "skipped" };
6362
6526
  }
6363
- const skills = skillsCli(resolved.runner);
6527
+ const before = await installedNow();
6528
+ const check = await setCheck(resolved);
6529
+ const pending = [];
6530
+ for (const set of skillSets(resolved)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
6531
+ const names = displayNames(selection.agents);
6532
+ if (pending.length === 0) {
6533
+ ctx.ui.success(`The rde and data-app skills are current for ${names}`);
6534
+ return { outcome: "completed" };
6535
+ }
6364
6536
  const agents = selection.agents.map((agent) => agent.skillsCliName);
6365
- const forWhom = `for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`;
6366
- ctx.ui.info(`Installing the data-app skills ${forWhom}`);
6367
- await skills.add({
6368
- source: DATA_APP_SKILLS_SOURCE,
6369
- skills: DATA_APP_SKILL_NAMES,
6370
- agents,
6371
- output: ctx.ui.childOutput
6372
- });
6373
- const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${resolved.skillSource}`;
6374
- ctx.ui.info(`Installing the rde skills ${forWhom}${from}`);
6375
- await skills.add({
6376
- source: resolved.skillSource,
6377
- skills: RDE_SOURCE_SKILL_NAMES,
6378
- agents,
6379
- output: ctx.ui.childOutput
6380
- });
6537
+ const forWhom = `for ${names} (${SOURCE_NOTES[selection.source]})`;
6538
+ for (const set of pending) {
6539
+ ctx.ui.info(`Installing the ${set.label} skills ${forWhom}${set.sourceNote}`);
6540
+ await skills.add({
6541
+ source: set.source,
6542
+ skills: set.skills,
6543
+ agents,
6544
+ output: ctx.ui.childOutput
6545
+ });
6546
+ }
6381
6547
  const installed = await skills.list();
6382
6548
  const missing = unlistedSkills(installed);
6383
6549
  if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
@@ -6555,6 +6721,24 @@ function databasesStep(options, deps = hostDeps$10) {
6555
6721
  };
6556
6722
  }
6557
6723
  //#endregion
6724
+ //#region src/metabase/domain/npm-package.ts
6725
+ const NpmVersionManifest = z.object({ version: z.string() }).loose();
6726
+ //#endregion
6727
+ //#region src/metabase/npm-registry.ts
6728
+ const NPM_REGISTRY = "https://registry.npmjs.org";
6729
+ function npmRegistry(signal, options = {}) {
6730
+ const http = createHttp({
6731
+ baseUrl: NPM_REGISTRY,
6732
+ auth: NO_AUTH,
6733
+ signal,
6734
+ ...options
6735
+ });
6736
+ return { async distTagVersion(packageName, tag) {
6737
+ const path = `/${encodeURIComponent(packageName)}/${encodeURIComponent(tag)}`;
6738
+ return (await http.requestJson("GET", path, { schema: NpmVersionManifest })).version;
6739
+ } };
6740
+ }
6741
+ //#endregion
6558
6742
  //#region src/steps/install-mb.ts
6559
6743
  const STEP_ID$5 = "install-mb";
6560
6744
  const MIN_MB_VERSION = "0.3.0";
@@ -6569,7 +6753,8 @@ function hostDeps$9(ctx) {
6569
6753
  return {
6570
6754
  runner: processRunner(ctx.signal),
6571
6755
  installedWith: rdeInstalledWith(),
6572
- locate: locateMbOnPath
6756
+ locate: locateMbOnPath,
6757
+ publishedVersion: () => npmRegistry(ctx.signal).distTagVersion(MB_PACKAGE, MB_DIST_TAG)
6573
6758
  };
6574
6759
  }
6575
6760
  async function inspectMb(runner) {
@@ -6583,6 +6768,38 @@ async function inspectMb(runner) {
6583
6768
  satisfies: semver.satisfies(version, MB_RANGE, { includePrerelease: true })
6584
6769
  };
6585
6770
  }
6771
+ const UNCHECKED = { kind: "unchecked" };
6772
+ async function publishedMb(deps) {
6773
+ let raw;
6774
+ try {
6775
+ raw = await deps.publishedVersion();
6776
+ } catch (error) {
6777
+ const reason = lookupFailure(error);
6778
+ if (reason === null) throw error;
6779
+ return {
6780
+ kind: "unknown",
6781
+ reason
6782
+ };
6783
+ }
6784
+ const version = semver.valid(raw);
6785
+ if (version === null) return {
6786
+ kind: "unknown",
6787
+ reason: `the registry names "${raw}" for ${MB_DIST_TAG}, which is not a version`
6788
+ };
6789
+ return {
6790
+ kind: "published",
6791
+ version
6792
+ };
6793
+ }
6794
+ function coreOf(version) {
6795
+ return `${semver.major(version)}.${semver.minor(version)}.${semver.patch(version)}`;
6796
+ }
6797
+ function updateTo(release, installed) {
6798
+ if (release.kind !== "published" || release.version === installed) return null;
6799
+ const order = semver.compare(coreOf(installed), coreOf(release.version));
6800
+ if (order === 0) return semver.prerelease(installed) === null ? null : release.version;
6801
+ return order < 0 ? release.version : null;
6802
+ }
6586
6803
  function displayCommand(install) {
6587
6804
  return [install.command, ...install.args].join(" ");
6588
6805
  }
@@ -6681,27 +6898,66 @@ async function stillWrong(install, after, deps) {
6681
6898
  `${stillWrongAdvice(diagnosis)}.`
6682
6899
  ].join(" "));
6683
6900
  }
6684
- function installMbStep(deps = hostDeps$9) {
6901
+ async function shadowedWarning(lead, deps) {
6902
+ const found = await inspectMbOnPath(deps.runner, deps.locate);
6903
+ const diagnosis = diagnoseMb(found, deps.installedWith);
6904
+ const advice = diagnosis.kind === "shadowed" ? ` ${shadowedHint(diagnosis)}.` : "";
6905
+ return `${lead} Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.${advice}`;
6906
+ }
6907
+ async function updateFor(release, installed, deps) {
6908
+ const version = updateTo(release, installed);
6909
+ if (version === null) return { kind: "current" };
6910
+ return (await inspectMbOnPath(deps.runner, deps.locate)).some((entry) => entry.version === version) ? {
6911
+ kind: "shadowed",
6912
+ version
6913
+ } : {
6914
+ kind: "update",
6915
+ version
6916
+ };
6917
+ }
6918
+ function installMbStep(updates, deps = hostDeps$9) {
6919
+ let published = null;
6920
+ function release(resolved) {
6921
+ if (updates === "installed") return Promise.resolve(UNCHECKED);
6922
+ published ??= publishedMb(resolved);
6923
+ return published;
6924
+ }
6685
6925
  return {
6686
6926
  id: STEP_ID$5,
6687
6927
  title: "Install the Metabase CLI",
6688
6928
  async detect(ctx) {
6689
- const found = await inspectMb(deps(ctx).runner);
6690
- return found.installed && found.satisfies ? "done" : "todo";
6929
+ const resolved = deps(ctx);
6930
+ const found = await inspectMb(resolved.runner);
6931
+ if (!found.installed || !found.satisfies) return "todo";
6932
+ return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
6691
6933
  },
6692
6934
  async run(ctx) {
6693
6935
  const resolved = deps(ctx);
6694
6936
  const before = await inspectMb(resolved.runner);
6937
+ const target = await release(resolved);
6695
6938
  if (before.installed && before.satisfies) {
6696
- ctx.ui.success(`mb ${before.version} is installed`);
6697
- return { outcome: "completed" };
6698
- }
6699
- if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
6939
+ if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
6940
+ const update = await updateFor(target, before.version, resolved);
6941
+ if (update.kind === "shadowed") {
6942
+ const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
6943
+ ctx.ui.warn(await shadowedWarning(lead, resolved));
6944
+ }
6945
+ if (update.kind !== "update") {
6946
+ ctx.ui.success(`mb ${before.version} is installed`);
6947
+ return { outcome: "completed" };
6948
+ }
6949
+ ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
6950
+ } else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
6700
6951
  const install = mbInstallCommand(resolved.installedWith);
6701
6952
  ctx.ui.info(`Running ${displayCommand(install)}`);
6702
6953
  await runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
6703
6954
  const after = await inspectMb(resolved.runner);
6704
6955
  if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
6956
+ const stillNewer = updateTo(target, after.version);
6957
+ if (stillNewer !== null) {
6958
+ const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
6959
+ ctx.ui.warn(await shadowedWarning(lead, resolved));
6960
+ }
6705
6961
  ctx.ui.success(`mb ${after.version} is installed`);
6706
6962
  return { outcome: "completed" };
6707
6963
  }
@@ -7609,12 +7865,17 @@ const DEFAULT_STEP_OPTIONS = {
7609
7865
  runtime: NO_RUNTIME_FLAGS,
7610
7866
  agents: DETECT_AGENTS,
7611
7867
  admin: DEFAULT_ADMIN,
7612
- databases: SAMPLE_STANDS_IN
7868
+ databases: SAMPLE_STANDS_IN,
7869
+ updates: "published"
7870
+ };
7871
+ const LOCAL_STEP_OPTIONS = {
7872
+ ...DEFAULT_STEP_OPTIONS,
7873
+ updates: "installed"
7613
7874
  };
7614
7875
  const STEP_BY_ID = {
7615
7876
  preflight: () => preflightStep(),
7616
- "install-mb": () => installMbStep(),
7617
- "agents-skills": (options) => agentsSkillsStep(options.agents),
7877
+ "install-mb": (options) => installMbStep(options.updates),
7878
+ "agents-skills": (options) => agentsSkillsStep(options.agents, options.updates),
7618
7879
  runtime: (options) => runtimeStep(options.runtime),
7619
7880
  "start-metabase": () => startMetabaseStep(),
7620
7881
  "admin-user": (options) => adminUserStep(options.admin),
@@ -7792,7 +8053,8 @@ function stepOptionsFrom(answers, run) {
7792
8053
  },
7793
8054
  agents: agentsOption(agents, run),
7794
8055
  admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
7795
- databases: { request: databasesRequest(databases, run) }
8056
+ databases: { request: databasesRequest(databases, run) },
8057
+ updates: DEFAULT_STEP_OPTIONS.updates
7796
8058
  };
7797
8059
  }
7798
8060
  function keyedAnswers(answers) {
@@ -8398,6 +8660,7 @@ var SessionCache = class {
8398
8660
  const CHECKLIST_MARK = {
8399
8661
  done: "✓",
8400
8662
  todo: "○",
8663
+ outdated: "↑",
8401
8664
  "not-applicable": "–"
8402
8665
  };
8403
8666
  const SPINNER_FAILURE_CODE = 1;
@@ -8638,7 +8901,7 @@ async function turnOf(entry, ctx, options) {
8638
8901
  if (options.only !== void 0) return "not-selected";
8639
8902
  if (entry.status === "done") return "done";
8640
8903
  const status = await entry.step.detect(ctx);
8641
- return status === "todo" ? "run" : status;
8904
+ return status === "todo" || status === "outdated" ? "run" : status;
8642
8905
  }
8643
8906
  function unusedMessage(step, turn, given) {
8644
8907
  const hint = step.redoHint === void 0 ? "" : ` ${step.redoHint}`;
@@ -9286,7 +9549,7 @@ function withRef(source, ref) {
9286
9549
  }
9287
9550
  async function copyRef(entry, deps) {
9288
9551
  const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
9289
- return lockedRef(path, await deps.skills.files.readJson(path), entry.name);
9552
+ return lockedSkill(path, await deps.skills.files.readJson(path), entry.name)?.ref ?? null;
9290
9553
  }
9291
9554
  async function judgeCopy(entry, origin, deps) {
9292
9555
  const where = `${entry.scope} ${entry.path}`;
@@ -9548,7 +9811,7 @@ const doctor = defineRdeCommand({
9548
9811
  signal: interruptSignal,
9549
9812
  prompts
9550
9813
  });
9551
- if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
9814
+ if (await fixDoctor(report, initSteps(LOCAL_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
9552
9815
  }
9553
9816
  print(report, args.json);
9554
9817
  if (report.failed > 0) throw new Error(doctorSummary(report));
@@ -10050,7 +10313,7 @@ const start = defineRdeCommand({
10050
10313
  const SUMMARY_STEP_ID = "summary";
10051
10314
  function hostStatusDeps(instanceName, signal) {
10052
10315
  return {
10053
- steps: initSteps(DEFAULT_STEP_OPTIONS),
10316
+ steps: initSteps(LOCAL_STEP_OPTIONS),
10054
10317
  runtime: hostRuntimeDeps(instanceName, signal),
10055
10318
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
10056
10319
  runner: processRunner(signal)
@@ -10975,7 +11238,7 @@ const upgradeCommand = defineRdeCommand({
10975
11238
  const main = defineCommand({
10976
11239
  meta: {
10977
11240
  name: "rde",
10978
- version: "0.1.13",
11241
+ version: "0.1.14",
10979
11242
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
10980
11243
  },
10981
11244
  subCommands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metabase/rde",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Prepares a local machine for data-engineering work with Metabase and coding agents",
5
5
  "license": "AGPL-3.0",
6
6
  "bin": {
@@ -28,7 +28,7 @@
28
28
  "e2e:down": "docker compose -f tests/e2e/docker-compose.yml down -v",
29
29
  "e2e:databases:up": "docker compose -f tests/e2e/databases.compose.yml up -d --wait --wait-timeout 900",
30
30
  "e2e:databases:down": "docker compose -f tests/e2e/databases.compose.yml down -v",
31
- "test:e2e": "vitest run --project e2e --reporter=verbose tests/e2e/init.e2e.test.ts",
31
+ "test:e2e": "vitest run --project e2e --reporter=verbose tests/e2e/init.e2e.test.ts tests/e2e/mb-profiles.e2e.test.ts",
32
32
  "test:e2e:databases": "vitest run --project e2e --reporter=verbose tests/e2e/databases.e2e.test.ts",
33
33
  "check": "bun run typecheck && bun run lint && bun run format:check && bun run test",
34
34
  "check:fix": "bun run lint:fix && bun run format"