@metabase/rde 0.1.4 → 0.1.6

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 +265 -205
  3. package/package.json +2 -2
package/README.md CHANGED
@@ -44,7 +44,7 @@ The wizard shows its checklist first (`✓` done, `○` to do, `–` not applica
44
44
  ● Running npm install -g @metabase/cli@alpha-fix-transform-test-ee-path
45
45
  ```
46
46
 
47
- The steps up to the license run without a question: `mb` is installed with the package manager that installed `rde`, the skill goes to every coding agent found on the machine, Metabase runs in Docker when Docker is there (the jar with your Java otherwise), on the first free port from 3100 up, with an admin user whose password is generated and kept in `~/.rde`, `mb` is logged in under its `default` profile (the one question here comes when `default` already points at another Metabase), and a Postgres sample database comes up in a second container, connected to Metabase as a place transforms can write. Each of those choices is printed as it is made and can be overridden with a flag. The prompts that remain are the license token (Enter skips it), a connection string for your own database (Enter skips it), and, with a license that includes remote sync, where the git repository lives and which remote it uses.
47
+ The steps up to the license run without a question, save one: `mb` is installed with the package manager that installed `rde`, the skill goes to the coding agents you pick from the ones found on the machine (at a terminal; without one, to every agent found), Metabase runs in Docker when Docker is there (the jar with your Java otherwise), on the first free port from 3100 up, with an admin user whose password is generated and kept in `~/.rde`, `mb` is logged in under its `default` profile (the one question here comes when `default` already points at another Metabase), and a Postgres sample database comes up in a second container, connected to Metabase as a place transforms can write. Each of those choices is printed as it is made and can be overridden with a flag. The prompts that remain are the license token (Enter skips it), a connection string for your own database (Enter skips it), and, with a license that includes remote sync, where the git repository lives and which remote it uses.
48
48
 
49
49
  The databases step asks once. At a terminal, without `--yes` or `--skip-databases`, and while none of your own databases is connected, it lists what is connected (the sample database) and asks for a connection string: paste one and `rde` reads the engine from it (or asks which, when the string fits several, or shows the engine list when it fits none), then shows the connection for review, tests it, adds it, and offers another. Enter skips it, and the skip is remembered, so the next `rde init` does not ask again; `rde init --only databases` and `rde db add` still do. With `--yes` or without a terminal it asks nothing and the sample database stands in; `--database` connects yours ahead of time.
50
50
 
@@ -54,7 +54,7 @@ The git step reads `–` until a license with remote sync is active; it is check
54
54
  | --- | --- |
55
55
  | Check this machine | Platform, Node version, git, and a reachable Docker daemon or a Java 21+ runtime |
56
56
  | Install the Metabase CLI | `@metabase/cli` (`mb`) at the `alpha-fix-transform-test-ee-path` dist-tag, with the package manager that installed `rde` |
57
- | Install the rde skill | The `rde` skill from the `rde-skill` branch of `metabase/agent-skills` for every coding agent found on this machine (Claude Code, Codex, Cursor, Gemini CLI, OpenCode, Windsurf); `--agents` picks a different set, `--skip-agents` none; `RDE_SKILL_SOURCE` installs it from another URL or a local directory |
57
+ | Install the rde skill | The `rde` skill from the `rde-skill` branch of `metabase/agent-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) plus OpenCode, which is always offered: picked while missing, it is installed first (`opencode-ai` with the package manager that installed `rde`). Without a terminal, or with `--yes`, every agent found gets the skill; `--agents` picks a set ahead of time (`opencode` in it is installed when missing), `--skip-agents` none; `RDE_SKILL_SOURCE` installs it from another URL or a local directory |
58
58
  | 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 3100, or the first free port above it, or `--port` |
59
59
  | Start Metabase | Starts the container or the jar and waits for `/api/health`, showing Metabase's own progress |
60
60
  | Create the admin user | `admin@metabase.local` with a generated password that `rde credentials` shows, or your own with `--admin prompted`, see [Credentials](#credentials) |
@@ -219,7 +219,7 @@ rde init --skip-license \
219
219
 
220
220
  | Flag | Answers |
221
221
  | --- | --- |
222
- | `--agents claude-code,codex` / `--skip-agents` | Which agents get the skill, instead of the ones found on the machine |
222
+ | `--agents claude-code,codex` / `--skip-agents` | Which agents get the skill, instead of asking at a terminal or taking the ones found on the machine; a missing `opencode` is installed |
223
223
  | `--runtime docker\|jar`, `--image`, `--jar`, `--port` | How Metabase runs, the Docker image or the jar (`latest`, a release tag, or a `.jar` path), and the host port, instead of Docker when available, `metabase/metabase-enterprise-head:latest`, the latest release, and the first free port from 3100; `--image` implies `docker` and `--jar` implies `jar` |
224
224
  | `--admin generated\|prompted`, `--admin-email`, `--admin-password` | The admin user, `generated` unless given; an email or password implies `prompted` |
225
225
  | `--license-token` / `--skip-license` | The license |
package/dist/cli.mjs CHANGED
@@ -2229,7 +2229,6 @@ async function chooseAfterParseError(run, engine, uri, error) {
2229
2229
  }
2230
2230
  function toReview(run, model, connection) {
2231
2231
  const mapped = mapDatabaseValues(connection.parsed);
2232
- const aliased = applyHostAlias(mapped.details, connection.engine.key, run.deps.hostAlias);
2233
2232
  const sameString = model.uri === connection.parsed.uri;
2234
2233
  return {
2235
2234
  type: "next",
@@ -2237,8 +2236,8 @@ function toReview(run, model, connection) {
2237
2236
  model: {
2238
2237
  engine: connection.engine,
2239
2238
  uri: connection.parsed.uri,
2240
- details: aliased.details,
2241
- hostRewritten: aliased.rewritten,
2239
+ details: mapped.details,
2240
+ hostRewritten: false,
2242
2241
  suggestedName: defaultDisplayName(mapped, connection.parsed, connection.engine),
2243
2242
  name: sameString ? model.name : null,
2244
2243
  fieldErrors: null
@@ -2359,11 +2358,15 @@ function reviewScreen(run) {
2359
2358
  let details = model.details;
2360
2359
  if (model.fieldErrors !== null) details = await fixReportedFields(run, chosen.fields, details, model.fieldErrors);
2361
2360
  let name = model.name;
2361
+ let hostRewritten = model.hostRewritten;
2362
2362
  for (;;) {
2363
- details = await fillRequiredFields(run.deps, chosen.fields, details);
2363
+ const aliased = applyHostAlias(await fillRequiredFields(run.deps, chosen.fields, details), chosen.engine.key, run.deps.hostAlias);
2364
+ details = aliased.details;
2365
+ hostRewritten ||= aliased.rewritten;
2364
2366
  const current = {
2365
2367
  ...model,
2366
2368
  details,
2369
+ hostRewritten,
2367
2370
  name,
2368
2371
  fieldErrors: null
2369
2372
  };
@@ -3847,134 +3850,36 @@ function sampleDatabaseContainer(state, docker) {
3847
3850
  };
3848
3851
  }
3849
3852
  //#endregion
3850
- //#region src/tools/agents.ts
3851
- const AgentId = z.enum([
3852
- "claude-code",
3853
- "codex",
3854
- "cursor",
3855
- "gemini-cli",
3856
- "opencode",
3857
- "windsurf"
3858
- ]);
3859
- const AGENTS = [
3860
- {
3861
- id: "claude-code",
3862
- displayName: "Claude Code",
3863
- skillsCliName: "claude-code",
3864
- command: "claude",
3865
- launch: "claude",
3866
- configDir: (home) => join(home, ".claude")
3867
- },
3868
- {
3869
- id: "codex",
3870
- displayName: "Codex",
3871
- skillsCliName: "codex",
3872
- command: "codex",
3873
- launch: "codex",
3874
- configDir: (home) => join(home, ".codex")
3875
- },
3876
- {
3877
- id: "cursor",
3878
- displayName: "Cursor",
3879
- skillsCliName: "cursor",
3880
- command: "cursor",
3881
- launch: "cursor .",
3882
- configDir: (home) => join(home, ".cursor")
3883
- },
3884
- {
3885
- id: "gemini-cli",
3886
- displayName: "Gemini CLI",
3887
- skillsCliName: "gemini-cli",
3888
- command: "gemini",
3889
- launch: "gemini",
3890
- configDir: (home) => join(home, ".gemini")
3891
- },
3892
- {
3893
- id: "opencode",
3894
- displayName: "OpenCode",
3895
- skillsCliName: "opencode",
3896
- command: "opencode",
3897
- launch: "opencode",
3898
- configDir: (_home, configHome) => join(configHome, "opencode")
3899
- },
3900
- {
3901
- id: "windsurf",
3902
- displayName: "Windsurf",
3903
- skillsCliName: "windsurf",
3904
- command: null,
3905
- launch: "windsurf .",
3906
- configDir: (home) => join(home, ".codeium", "windsurf")
3907
- }
3908
- ];
3909
- async function directoryExists$1(path) {
3910
- try {
3911
- return (await promises.stat(path)).isDirectory();
3912
- } catch {
3913
- return false;
3914
- }
3915
- }
3916
- function hostAgentProbe() {
3917
- const home = homedir();
3918
- return {
3919
- commandExists: async (command) => await whichCommand(command) !== null,
3920
- directoryExists: directoryExists$1,
3921
- home,
3922
- configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
3923
- };
3924
- }
3925
- async function isDetected(agent, probe) {
3926
- if (agent.command !== null && await probe.commandExists(agent.command)) return true;
3927
- return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
3928
- }
3929
- async function detectAgents(probe) {
3930
- return Promise.all(AGENTS.map(async (agent) => ({
3931
- id: agent.id,
3932
- displayName: agent.displayName,
3933
- skillsCliName: agent.skillsCliName,
3934
- launch: agent.launch,
3935
- detected: await isDetected(agent, probe)
3936
- })));
3937
- }
3938
- //#endregion
3939
3853
  //#region src/tools/mb.ts
3940
3854
  const MB = "mb";
3941
3855
  const MB_INSTALL_SPEC = `@metabase/cli@alpha-fix-transform-test-ee-path`;
3942
3856
  const GLOBAL_INSTALL = {
3943
3857
  npm: {
3944
3858
  command: "npm",
3945
- args: [
3946
- "install",
3947
- "-g",
3948
- MB_INSTALL_SPEC
3949
- ]
3859
+ args: ["install", "-g"]
3950
3860
  },
3951
3861
  pnpm: {
3952
3862
  command: "pnpm",
3953
- args: [
3954
- "add",
3955
- "-g",
3956
- MB_INSTALL_SPEC
3957
- ]
3863
+ args: ["add", "-g"]
3958
3864
  },
3959
3865
  yarn: {
3960
3866
  command: "yarn",
3961
- args: [
3962
- "global",
3963
- "add",
3964
- MB_INSTALL_SPEC
3965
- ]
3867
+ args: ["global", "add"]
3966
3868
  },
3967
3869
  bun: {
3968
3870
  command: "bun",
3969
- args: [
3970
- "add",
3971
- "-g",
3972
- MB_INSTALL_SPEC
3973
- ]
3871
+ args: ["add", "-g"]
3974
3872
  }
3975
3873
  };
3874
+ function globalInstallCommand(packageManager, spec) {
3875
+ const install = GLOBAL_INSTALL[packageManager];
3876
+ return {
3877
+ command: install.command,
3878
+ args: [...install.args, spec]
3879
+ };
3880
+ }
3976
3881
  function mbInstallCommand(packageManager) {
3977
- return GLOBAL_INSTALL[packageManager];
3882
+ return globalInstallCommand(packageManager, MB_INSTALL_SPEC);
3978
3883
  }
3979
3884
  function ownerByMarker(path, markers) {
3980
3885
  const normalized = path.replaceAll("\\", "/");
@@ -4258,6 +4163,99 @@ function mbTool(runner) {
4258
4163
  };
4259
4164
  }
4260
4165
  //#endregion
4166
+ //#region src/tools/agents.ts
4167
+ const AgentId = z.enum([
4168
+ "claude-code",
4169
+ "codex",
4170
+ "cursor",
4171
+ "gemini-cli",
4172
+ "opencode",
4173
+ "windsurf"
4174
+ ]);
4175
+ const OPENCODE_PACKAGE = "opencode-ai@latest";
4176
+ function opencodeInstallCommand(packageManager) {
4177
+ return globalInstallCommand(packageManager, OPENCODE_PACKAGE);
4178
+ }
4179
+ const AGENTS = [
4180
+ {
4181
+ id: "claude-code",
4182
+ displayName: "Claude Code",
4183
+ skillsCliName: "claude-code",
4184
+ command: "claude",
4185
+ launch: "claude",
4186
+ configDir: (home) => join(home, ".claude")
4187
+ },
4188
+ {
4189
+ id: "codex",
4190
+ displayName: "Codex",
4191
+ skillsCliName: "codex",
4192
+ command: "codex",
4193
+ launch: "codex",
4194
+ configDir: (home) => join(home, ".codex")
4195
+ },
4196
+ {
4197
+ id: "cursor",
4198
+ displayName: "Cursor",
4199
+ skillsCliName: "cursor",
4200
+ command: "cursor",
4201
+ launch: "cursor .",
4202
+ configDir: (home) => join(home, ".cursor")
4203
+ },
4204
+ {
4205
+ id: "gemini-cli",
4206
+ displayName: "Gemini CLI",
4207
+ skillsCliName: "gemini-cli",
4208
+ command: "gemini",
4209
+ launch: "gemini",
4210
+ configDir: (home) => join(home, ".gemini")
4211
+ },
4212
+ {
4213
+ id: "opencode",
4214
+ displayName: "OpenCode",
4215
+ skillsCliName: "opencode",
4216
+ command: "opencode",
4217
+ launch: "opencode",
4218
+ configDir: (_home, configHome) => join(configHome, "opencode")
4219
+ },
4220
+ {
4221
+ id: "windsurf",
4222
+ displayName: "Windsurf",
4223
+ skillsCliName: "windsurf",
4224
+ command: null,
4225
+ launch: "windsurf .",
4226
+ configDir: (home) => join(home, ".codeium", "windsurf")
4227
+ }
4228
+ ];
4229
+ async function directoryExists$1(path) {
4230
+ try {
4231
+ return (await promises.stat(path)).isDirectory();
4232
+ } catch {
4233
+ return false;
4234
+ }
4235
+ }
4236
+ function hostAgentProbe() {
4237
+ const home = homedir();
4238
+ return {
4239
+ commandExists: async (command) => await whichCommand(command) !== null,
4240
+ directoryExists: directoryExists$1,
4241
+ home,
4242
+ configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
4243
+ };
4244
+ }
4245
+ async function isDetected(agent, probe) {
4246
+ if (agent.command !== null && await probe.commandExists(agent.command)) return true;
4247
+ return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
4248
+ }
4249
+ async function detectAgents(probe) {
4250
+ return Promise.all(AGENTS.map(async (agent) => ({
4251
+ id: agent.id,
4252
+ displayName: agent.displayName,
4253
+ skillsCliName: agent.skillsCliName,
4254
+ launch: agent.launch,
4255
+ detected: await isDetected(agent, probe)
4256
+ })));
4257
+ }
4258
+ //#endregion
4261
4259
  //#region src/tools/skills-cli.ts
4262
4260
  const NPX = "npx";
4263
4261
  const SKILLS_PACKAGE = "skills@latest";
@@ -4445,92 +4443,8 @@ async function runWizard(steps, ctx, options = {}) {
4445
4443
  }
4446
4444
  }
4447
4445
  //#endregion
4448
- //#region src/steps/agents-skills.ts
4449
- const STEP_ID$8 = "agents-skills";
4450
- const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
4451
- const DETECT_AGENTS = { agents: null };
4452
- function hostDeps$11(ctx) {
4453
- return {
4454
- runner: processRunner(ctx.signal),
4455
- probe: hostAgentProbe(),
4456
- skillSource: rdeSkillSource()
4457
- };
4458
- }
4459
- function detectedOnly(agents) {
4460
- return agents.filter((agent) => agent.detected);
4461
- }
4462
- function displayNames(agents) {
4463
- return agents.map((agent) => agent.displayName).join(", ");
4464
- }
4465
- function skillCoversAgent(entry, agent) {
4466
- return entry.agents.includes(agent.displayName);
4467
- }
4468
- function coversAny(entry, agents) {
4469
- return agents.some((agent) => skillCoversAgent(entry, agent));
4470
- }
4471
- async function rdeSkillEntries(runner) {
4472
- return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
4473
- }
4474
- function installedLines(entries) {
4475
- return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
4476
- }
4477
- const SOURCE_NOTES = {
4478
- override: "from --agents",
4479
- detected: "detected on this machine"
4480
- };
4481
- function selectAgents(all, options) {
4482
- if (options.agents === null) return {
4483
- agents: detectedOnly(all),
4484
- source: "detected"
4485
- };
4486
- const chosen = options.agents;
4487
- return {
4488
- agents: all.filter((agent) => chosen.includes(agent.id)),
4489
- source: "override"
4490
- };
4491
- }
4492
- function skipLine(all, source) {
4493
- switch (source) {
4494
- case "override": return `Skipping the rde skill; run \`rde init --only ${STEP_ID$8}\` to install it later`;
4495
- case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$8} --agents <name>\` once one is installed`;
4496
- }
4497
- }
4498
- function agentsSkillsStep(options, deps = hostDeps$11) {
4499
- return {
4500
- id: STEP_ID$8,
4501
- title: "Install the rde skill for your coding agents",
4502
- async detect(ctx) {
4503
- const resolved = deps(ctx);
4504
- const detected = detectedOnly(await detectAgents(resolved.probe));
4505
- if (detected.length === 0) return ctx.state.steps[STEP_ID$8]?.status === "skipped" ? "not-applicable" : "todo";
4506
- return (await rdeSkillEntries(resolved.runner)).some((entry) => coversAny(entry, detected)) ? "done" : "todo";
4507
- },
4508
- async run(ctx) {
4509
- const resolved = deps(ctx);
4510
- const all = await detectAgents(resolved.probe);
4511
- const selection = selectAgents(all, options);
4512
- if (selection.agents.length === 0) {
4513
- ctx.ui.info(skipLine(all, selection.source));
4514
- return "skipped";
4515
- }
4516
- const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-skill" ? "" : ` from ${resolved.skillSource}`;
4517
- ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
4518
- await skillsCli(resolved.runner).add({
4519
- source: resolved.skillSource,
4520
- skill: "rde",
4521
- agents: selection.agents.map((agent) => agent.skillsCliName)
4522
- });
4523
- const entries = await rdeSkillEntries(resolved.runner);
4524
- if (entries.length === 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show the rde skill`);
4525
- ctx.ui.note(installedLines(entries), "rde skill installed");
4526
- ctx.ui.info(MECHANICS_NOTE);
4527
- return "completed";
4528
- }
4529
- };
4530
- }
4531
- //#endregion
4532
4446
  //#region src/steps/install-mb.ts
4533
- const STEP_ID$7 = "install-mb";
4447
+ const STEP_ID$8 = "install-mb";
4534
4448
  const MIN_MB_VERSION = "0.3.0";
4535
4449
  const MB_RANGE = `>=${MIN_MB_VERSION}`;
4536
4450
  function installedWith(scriptPath) {
@@ -4539,7 +4453,7 @@ function installedWith(scriptPath) {
4539
4453
  function rdeInstalledWith() {
4540
4454
  return installedWith(process.argv[1]);
4541
4455
  }
4542
- function hostDeps$10(ctx) {
4456
+ function hostDeps$11(ctx) {
4543
4457
  return {
4544
4458
  runner: processRunner(ctx.signal),
4545
4459
  installedWith: rdeInstalledWith(),
@@ -4655,9 +4569,9 @@ async function stillWrong(install, after, deps) {
4655
4569
  `${stillWrongAdvice(diagnosis)}.`
4656
4570
  ].join(" "));
4657
4571
  }
4658
- function installMbStep(deps = hostDeps$10) {
4572
+ function installMbStep(deps = hostDeps$11) {
4659
4573
  return {
4660
- id: STEP_ID$7,
4574
+ id: STEP_ID$8,
4661
4575
  title: "Install the Metabase CLI",
4662
4576
  async detect(ctx) {
4663
4577
  const found = await inspectMb(deps(ctx).runner);
@@ -4688,6 +4602,138 @@ function installMbStep(deps = hostDeps$10) {
4688
4602
  };
4689
4603
  }
4690
4604
  //#endregion
4605
+ //#region src/steps/agents-skills.ts
4606
+ const STEP_ID$7 = "agents-skills";
4607
+ const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
4608
+ const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
4609
+ const DETECT_AGENTS = { kind: "detected" };
4610
+ function hostDeps$10(ctx) {
4611
+ return {
4612
+ runner: processRunner(ctx.signal),
4613
+ probe: hostAgentProbe(),
4614
+ skillSource: rdeSkillSource(),
4615
+ installedWith: rdeInstalledWith()
4616
+ };
4617
+ }
4618
+ function detectedOnly(agents) {
4619
+ return agents.filter((agent) => agent.detected);
4620
+ }
4621
+ function displayNames(agents) {
4622
+ return agents.map((agent) => agent.displayName).join(", ");
4623
+ }
4624
+ function skillCoversAgent(entry, agent) {
4625
+ return entry.agents.includes(agent.displayName);
4626
+ }
4627
+ function coversAny(entry, agents) {
4628
+ return agents.some((agent) => skillCoversAgent(entry, agent));
4629
+ }
4630
+ async function rdeSkillEntries(runner) {
4631
+ return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
4632
+ }
4633
+ function installedLines(entries) {
4634
+ return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
4635
+ }
4636
+ const SOURCE_NOTES = {
4637
+ override: "from --agents",
4638
+ detected: "detected on this machine",
4639
+ chosen: "as chosen"
4640
+ };
4641
+ const MISSING_OPENCODE_HINT = "not installed; rde installs it";
4642
+ function isMissingOpencode(agent) {
4643
+ return agent.id === "opencode" && !agent.detected;
4644
+ }
4645
+ function offeredChoice(agent) {
4646
+ return {
4647
+ value: agent.id,
4648
+ label: agent.displayName,
4649
+ ...isMissingOpencode(agent) && { hint: MISSING_OPENCODE_HINT }
4650
+ };
4651
+ }
4652
+ async function askAgents(ctx, all) {
4653
+ const offered = all.filter((agent) => agent.detected || agent.id === "opencode");
4654
+ const picked = await ctx.prompts.multiselect({
4655
+ key: AGENTS_SKILLS_KEYS.agents,
4656
+ message: "Which coding agents should get the rde skill?",
4657
+ choices: offered.map(offeredChoice),
4658
+ initialValues: detectedOnly(all).map((agent) => agent.id),
4659
+ required: false
4660
+ });
4661
+ return offered.filter((agent) => picked.includes(agent.id));
4662
+ }
4663
+ async function selectAgents(ctx, all, options) {
4664
+ switch (options.kind) {
4665
+ case "detected": return {
4666
+ agents: detectedOnly(all),
4667
+ source: "detected"
4668
+ };
4669
+ case "listed": {
4670
+ const listed = options.agents;
4671
+ return {
4672
+ agents: all.filter((agent) => listed.includes(agent.id)),
4673
+ source: "override"
4674
+ };
4675
+ }
4676
+ case "ask": return {
4677
+ agents: await askAgents(ctx, all),
4678
+ source: "chosen"
4679
+ };
4680
+ }
4681
+ }
4682
+ async function installOpencode(ctx, deps) {
4683
+ const install = opencodeInstallCommand(deps.installedWith);
4684
+ const commandLine = [install.command, ...install.args].join(" ");
4685
+ ctx.ui.info(`Running ${commandLine}`);
4686
+ const exitCode = await deps.runner.stream(install.command, install.args);
4687
+ if (exitCode !== 0) throw new ConfigError(`\`${commandLine}\` exited with ${exitCode}`);
4688
+ if (!(await detectAgents(deps.probe)).some((agent) => agent.id === "opencode" && agent.detected)) throw new ConfigError("OpenCode was installed but `opencode` is not on PATH; open a new terminal and run rde init again");
4689
+ }
4690
+ function listsMissingOpencode(all, options) {
4691
+ return options.kind === "listed" && options.agents.includes("opencode") && all.some(isMissingOpencode);
4692
+ }
4693
+ function skipLine(all, source) {
4694
+ switch (source) {
4695
+ case "override":
4696
+ case "chosen": return `Skipping the rde skill; run \`rde init --only ${STEP_ID$7}\` to install it later`;
4697
+ case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
4698
+ }
4699
+ }
4700
+ function agentsSkillsStep(options, deps = hostDeps$10) {
4701
+ return {
4702
+ id: STEP_ID$7,
4703
+ title: "Install the rde skill for your coding agents",
4704
+ async detect(ctx) {
4705
+ const resolved = deps(ctx);
4706
+ const all = await detectAgents(resolved.probe);
4707
+ if (listsMissingOpencode(all, options)) return "todo";
4708
+ const detected = detectedOnly(all);
4709
+ if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
4710
+ return (await rdeSkillEntries(resolved.runner)).some((entry) => coversAny(entry, detected)) ? "done" : "todo";
4711
+ },
4712
+ async run(ctx) {
4713
+ const resolved = deps(ctx);
4714
+ const all = await detectAgents(resolved.probe);
4715
+ const selection = await selectAgents(ctx, all, options);
4716
+ if (selection.agents.length === 0) {
4717
+ ctx.ui.info(skipLine(all, selection.source));
4718
+ return "skipped";
4719
+ }
4720
+ if (selection.agents.some(isMissingOpencode)) await installOpencode(ctx, resolved);
4721
+ const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-skill" ? "" : ` from ${resolved.skillSource}`;
4722
+ ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
4723
+ await skillsCli(resolved.runner).add({
4724
+ source: resolved.skillSource,
4725
+ skill: "rde",
4726
+ agents: selection.agents.map((agent) => agent.skillsCliName)
4727
+ });
4728
+ const entries = await rdeSkillEntries(resolved.runner);
4729
+ if (entries.length === 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show the rde skill`);
4730
+ ctx.ui.note(installedLines(entries), "rde skill installed");
4731
+ ctx.ui.info(MECHANICS_NOTE);
4732
+ return "completed";
4733
+ }
4734
+ };
4735
+ }
4736
+ //#endregion
4691
4737
  //#region src/tools/git.ts
4692
4738
  const GIT = "git";
4693
4739
  const FALLBACK_IDENTITY_NAME = "rde";
@@ -7446,6 +7492,13 @@ function databasesRequest(databases, run) {
7446
7492
  if (databases !== void 0) return databases.length > 0 ? "named" : "sample-stands-in";
7447
7493
  return run.interactive ? "ask-once" : "sample-stands-in";
7448
7494
  }
7495
+ function agentsOption(agents, run) {
7496
+ if (agents !== void 0) return {
7497
+ kind: "listed",
7498
+ agents: agents.agents
7499
+ };
7500
+ return run.interactive ? { kind: "ask" } : DEFAULT_STEP_OPTIONS.agents;
7501
+ }
7449
7502
  function stepOptionsFrom(answers, run) {
7450
7503
  const runtime = answers.runtime;
7451
7504
  const agents = answers["agents-skills"];
@@ -7458,7 +7511,7 @@ function stepOptionsFrom(answers, run) {
7458
7511
  jar: runtime.jar ?? null,
7459
7512
  port: runtime.port ?? null
7460
7513
  },
7461
- agents: agents === void 0 ? DEFAULT_STEP_OPTIONS.agents : { agents: agents.agents },
7514
+ agents: agentsOption(agents, run),
7462
7515
  admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
7463
7516
  databases: { request: databasesRequest(databases, run) }
7464
7517
  };
@@ -7807,12 +7860,19 @@ function applyGitRecord(draft, key, value) {
7807
7860
  else if (key === GIT_SYNC_KEYS.url && value !== null) gitSync(draft).url = value;
7808
7861
  else if (key === GIT_SYNC_KEYS.token) gitSync(draft).token = envReference(ENV_GIT_TOKEN);
7809
7862
  }
7863
+ function agentIds(values) {
7864
+ return values.flatMap((value) => {
7865
+ const parsed = AgentId.safeParse(value);
7866
+ return parsed.success ? [parsed.data] : [];
7867
+ });
7868
+ }
7810
7869
  function applyRecord(draft, record) {
7811
7870
  const { key } = record;
7812
7871
  const value = stringOf(record.answer);
7813
7872
  if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
7814
7873
  else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
7815
7874
  else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
7875
+ else if (key === AGENTS_SKILLS_KEYS.agents && record.answer.kind === "multiselect") draft.answers["agents-skills"] = { agents: agentIds(record.answer.values) };
7816
7876
  else if (key === API_KEY_KEYS.profile && value !== null) {
7817
7877
  const profile = MbProfileChoice.safeParse(value);
7818
7878
  if (profile.success) draft.answers["api-key"] = { profile: profile.data };
@@ -8031,7 +8091,7 @@ const init = defineRdeCommand({
8031
8091
  },
8032
8092
  agents: {
8033
8093
  type: "string",
8034
- description: "Agents to install the skill for, comma-separated; the detected ones by default"
8094
+ description: "Agents to install the skill for, comma-separated (a missing opencode is installed); asked at a terminal, the detected ones otherwise"
8035
8095
  },
8036
8096
  "skip-agents": {
8037
8097
  type: "boolean",
@@ -9022,7 +9082,7 @@ const upgradeCommand = defineRdeCommand({
9022
9082
  const main = defineCommand({
9023
9083
  meta: {
9024
9084
  name: "rde",
9025
- version: "0.1.4",
9085
+ version: "0.1.6",
9026
9086
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
9027
9087
  },
9028
9088
  subCommands: {
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@metabase/rde",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
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": {
7
- "rde": "./dist/cli.mjs"
7
+ "rde": "dist/cli.mjs"
8
8
  },
9
9
  "files": [
10
10
  "dist"