@metabase/rde 0.1.12 → 0.1.13

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 +15 -10
  2. package/dist/cli.mjs +416 -178
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -35,19 +35,22 @@ The wizard shows its checklist first (`✓` done, `○` to do, `–` not applica
35
35
  ○ Start Metabase
36
36
  ○ Create the admin user
37
37
  ○ Connect the Metabase CLI
38
+ ○ Enable transforms
38
39
  ○ Connect a sample database
39
40
  ○ Activate your license
40
- ○ Connect your databases
41
41
  – Connect a git repository
42
+ ○ Connect your databases
42
43
  ○ Summary
43
44
  │
44
45
  ● Running npm install -g @metabase/cli@alpha-release-65-integration-branch
45
46
  ```
46
47
 
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 23000 up that no other container or instance claims, 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
+ 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 23000 up that no other container or instance claims, 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), with a license that includes remote sync where the git repository lives and which remote it uses, and your own databases (Enter skips them).
48
49
 
49
50
  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
51
 
52
+ It runs after the git step because content imported from a repository names its databases, and Metabase only finds them by name. Every database the content names that this instance lacks becomes a stub: a placeholder with that name and no connection, which keeps the content importable and which Metabase leaves out of its own database lists. When there are stubs, the step asks for them instead of the connection string: a checklist of their names, none picked, where you pick the few you work on (a repository can name dozens, most of them someone else's or out of reach from your machine). Each one picked asks for its connection string (Enter goes to the engine list, whose last choice leaves that one unconnected), then review, test, and connect: the stub becomes the real database under the same name and id, so the content on it works and its sync starts. Picking none skips the step like Enter does. The ones left over are named at the end of the step and in the summary, and `rde db add` and `rde db list` show them until they are connected. A database named ahead of time (`--database`, an answers file, `rde db add --url --name`) whose name is a stub's connects that stub too, never a second database of the same name.
53
+
51
54
  The git step reads `–` until a license with remote sync is active; it is checked again when its turn comes, so a run that activates a license connects the repository in the same pass.
52
55
 
53
56
  | Step | What it does |
@@ -59,10 +62,11 @@ The git step reads `–` until a license with remote sync is active; it is check
59
62
  | Start Metabase | Starts the container or the jar and waits for `/api/health`, showing Metabase's own progress |
60
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) |
61
64
  | Connect the Metabase CLI | Creates an API key named `rde` and logs `mb` in with it under the profile `default`, so plain `mb` uses the instance; when `default` already points at another Metabase it asks first, see [The `mb` profile](#the-mb-profile); a key Metabase rejects is replaced |
62
- | Connect a sample database | Runs Metabase's Postgres sample data (`metabase/qa-databases:postgres-sample-15`) in a container named `rde-sampledb-default` on the first free port from 25432 (or one Docker picks, when another program or a concurrent `rde init` takes that port first), connects it as `Sample Database`, and switches transforms on. It replaces the H2 sample database Metabase would otherwise bundle, which cannot be a transform target. Needs Docker, so a jar instance on a machine without Docker keeps the H2 one instead |
65
+ | Enable transforms | Turns transforms on, unless someone already set them either way: turned off in the admin settings, they stay off on every later `rde init`, and the admin settings turn them back on |
66
+ | Connect a sample database | Runs Metabase's Postgres sample data (`metabase/qa-databases:postgres-sample-15`) in a container named `rde-sampledb-default` on the first free port from 25432 (or one Docker picks, when another program or a concurrent `rde init` takes that port first), and connects it as `Sample Database`. It replaces the H2 sample database Metabase would otherwise bundle, which cannot be a transform target. Needs Docker, so a jar instance on a machine without Docker has no sample database |
63
67
  | Activate your license | Optional. Paste a token, or press Enter to skip; remote sync needs one with the `remote-sync` feature |
64
- | Connect your databases | At a terminal, until you connect one of your own or skip it: paste a connection string (Enter skips), review, test, add; repeat. `--database` names them ahead of time; with `--yes` or without a terminal nothing is asked. `rde db add` starts from the engine list, where the fields can also be filled one by one |
65
- | Connect a git repository | Only with a license. Initializes or reuses a repository and points Metabase's remote sync at it |
68
+ | Connect a git repository | Only with a license. Initializes or reuses a repository and points Metabase's remote sync at it, with transforms included in the sync (the Transforms row under Collections to sync), and imports the content it already holds |
69
+ | Connect your databases | At a terminal, until you connect one of your own or skip it: pick the databases the imported content uses from a checklist, or, with none, paste a connection string (Enter skips); review, test, add; repeat. `--database` names them ahead of time; with `--yes` or without a terminal nothing is asked. `rde db add` starts from the same checklist, then the engine list, where the fields can also be filled one by one |
66
70
  | Summary | URL, login email, the `mb` command for the instance, databases, repository, and the command that opens each detected agent |
67
71
 
68
72
  The second run on the same machine detects every step as done and prints the summary only:
@@ -76,10 +80,11 @@ The second run on the same machine detects every step as done and prints the sum
76
80
  ✓ Start Metabase
77
81
  ✓ Create the admin user
78
82
  ✓ Connect the Metabase CLI
83
+ ✓ Enable transforms
79
84
  ✓ Connect a sample database
80
85
  – Activate your license
81
- ✓ Connect your databases
82
86
  – Connect a git repository
87
+ ✓ Connect your databases
83
88
  ○ Summary
84
89
  │
85
90
  ◇ Your environment ─────────────────────────────────────────────────────────────────╮
@@ -167,8 +172,8 @@ Docker is used whenever it is available: the image is self-contained, and the co
167
172
  | `rde credentials` | The Metabase URL, admin email, and password; `--json` for scripts and agents; `--api-key` for the API key alone |
168
173
  | `rde logs` | The container's log, or the jar's `metabase.log`; `-f` keeps streaming |
169
174
  | `rde open` | Opens the instance in the browser |
170
- | `rde db add` | The database wizard: pick the engine, paste a connection string or fill the fields, review, test, add. `rde db add --url <connection string> [--name <display name>]` asks nothing: it reads the engine from the string (`--engine` for one that fits several), tests it and adds it, which is how a script or a coding agent connects a database without a terminal; `--url -` reads the string from stdin |
171
- | `rde db list` | The connected databases; `--json` for scripts; a bare `rde db` does the same |
175
+ | `rde db add` | The database wizard: pick the stubs to connect when the content uses databases not connected yet, otherwise (or after them) pick the engine, paste a connection string or fill the fields, review, test, add. `rde db add --url <connection string> [--name <display name>]` asks nothing: it reads the engine from the string (`--engine` for one that fits several), tests it and adds it, which is how a script or a coding agent connects a database without a terminal; `--url -` reads the string from stdin; a `--name` that is a stub's connects that stub |
176
+ | `rde db list` | The connected databases and the stubs still waiting for a connection (`stub` in `--json`); `--json` for scripts; a bare `rde db` does the same |
172
177
  | `rde doctor` | One table of checks with a fix hint per failure; `--fix` re-runs the init steps behind them; `--json` for scripts and the rde skill (every row's `id`, `status`, `detail`, `hint`, and `fix`: the `rde` command that repairs it, or null when you have to act); exit 1 while anything fails |
173
178
  | `rde upgrade` | Moves the instance to a newer Metabase after backing up its data; see [Upgrading](#upgrading) |
174
179
  | `rde uninstall` | Removes the instance, its data, and the `mb` profile; see [Resetting](#resetting) |
@@ -191,12 +196,12 @@ mb git-sync status
191
196
 
192
197
  ## The git repository
193
198
 
194
- With a license that includes remote sync, the last step connects a git repository. You choose where Metabase pushes, then where the working directory lives (here, another directory, or an existing repository):
199
+ With a license that includes remote sync, the step after the license connects a git repository. You choose where Metabase pushes, then where the working directory lives (here, another directory, or an existing repository):
195
200
 
196
201
  - **Local remote inside `~/.rde`** (default): a bare repository at `instances/default/data/content.git`, mounted into the container and reached as `file:///rde/content.git`. No account, no token. Your working directory gets it as `origin` (or as `rde-remote` when `origin` already points elsewhere).
197
202
  - **Hosted remote**: a GitHub, GitLab, or Bitbucket `https://` URL plus a token with read and write access. `rde` tests the connection through Metabase before saving anything, then clones the repository into the working directory (an existing repository gets the URL as `origin` instead); an empty hosted repository gets the first commit pushed to it. The token is handed to Metabase and, for the clone and that first push, to git in its environment; it never lands in the clone's config, the remote URL, or a credential helper, so a later `git pull` uses your own git credentials.
198
203
 
199
- A fresh repository gets a first commit with a `README.md` and a `.gitignore` that keeps out what a data app built in it must not push: `.env`, `.env.local`, `.env.*.local`, `node_modules/`, `.vite/`, the rde skill's `.scratch/`, and `.DS_Store` (a data app's built `dist/` stays tracked, since that bundle is what Metabase serves); a `.gitignore` already there keeps its lines and gets the missing ones, and a repository that already has commits is left as it is. A repository that already holds Metabase content is imported on the spot. Afterwards:
204
+ A fresh repository gets a first commit with a `README.md` and a `.gitignore` that keeps out what a data app built in it must not push: `.env`, `.env.local`, `.env.*.local`, `node_modules/`, `.vite/`, the rde skill's `.scratch/`, and `.DS_Store` (a data app's built `dist/` stays tracked, since that bundle is what Metabase serves); a `.gitignore` already there keeps its lines and gets the missing ones, and a repository that already has commits is left as it is. A repository that already holds Metabase content is imported on the spot; each database it names that this instance lacks becomes a stub, which the databases step right after offers to connect. Afterwards:
200
205
 
201
206
  ```sh
202
207
  cd <your working directory>
package/dist/cli.mjs CHANGED
@@ -240,10 +240,11 @@ const StepId = z.enum([
240
240
  "start-metabase",
241
241
  "admin-user",
242
242
  "api-key",
243
+ "transforms",
243
244
  "sample-database",
244
245
  "license",
245
- "databases",
246
246
  "git-sync",
247
+ "databases",
247
248
  "summary"
248
249
  ]);
249
250
  const GlobalConfig = z.object({
@@ -1037,8 +1038,12 @@ const Database = z.object({
1037
1038
  id: z.int(),
1038
1039
  name: z.string(),
1039
1040
  engine: z.string(),
1040
- initial_sync_status: InitialSyncStatus
1041
+ initial_sync_status: InitialSyncStatus,
1042
+ is_stub: z.boolean().optional()
1041
1043
  }).loose();
1044
+ function isStub(database) {
1045
+ return database.is_stub === true;
1046
+ }
1042
1047
  const DatabaseList = z.object({
1043
1048
  data: z.array(Database),
1044
1049
  total: z.int()
@@ -1072,6 +1077,17 @@ function databaseResource(http) {
1072
1077
  async function list() {
1073
1078
  return http.requestJson("GET", "/api/database", { schema: DatabaseList });
1074
1079
  }
1080
+ /** GET /api/database?include_stubs=true */
1081
+ async function listWithStubs() {
1082
+ return http.requestJson("GET", "/api/database?include_stubs=true", { schema: DatabaseList });
1083
+ }
1084
+ /** PUT /api/database/:id — on a stub, new connection details make it a real database */
1085
+ async function connect(id, connection) {
1086
+ return http.requestJson("PUT", `/api/database/${id}`, {
1087
+ body: connection,
1088
+ schema: Database
1089
+ });
1090
+ }
1075
1091
  /** GET /api/database/:id */
1076
1092
  async function get(id) {
1077
1093
  return http.requestJson("GET", `/api/database/${id}`, { schema: Database });
@@ -1080,6 +1096,8 @@ function databaseResource(http) {
1080
1096
  validate,
1081
1097
  create,
1082
1098
  list,
1099
+ listWithStubs,
1100
+ connect,
1083
1101
  get
1084
1102
  };
1085
1103
  }
@@ -1132,6 +1150,13 @@ function remoteSyncResource(http) {
1132
1150
  schema: RemoteSyncSettingsResult
1133
1151
  });
1134
1152
  }
1153
+ /** PUT /api/ee/remote-sync/settings with only `remote-sync-transforms`, which skips the repository check. */
1154
+ async function syncTransforms() {
1155
+ return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
1156
+ body: { "remote-sync-transforms": true },
1157
+ schema: RemoteSyncSettingsResult
1158
+ });
1159
+ }
1135
1160
  /** POST /api/ee/remote-sync/test-connection; an empty body tests the saved url and token. */
1136
1161
  async function testConnection(connection = {}) {
1137
1162
  return http.requestJson("POST", "/api/ee/remote-sync/test-connection", {
@@ -1145,6 +1170,7 @@ function remoteSyncResource(http) {
1145
1170
  }
1146
1171
  return {
1147
1172
  updateSettings,
1173
+ syncTransforms,
1148
1174
  testConnection,
1149
1175
  currentTask
1150
1176
  };
@@ -1154,6 +1180,33 @@ function remoteSyncResource(http) {
1154
1180
  const Session = z.object({ id: z.string() }).loose();
1155
1181
  //#endregion
1156
1182
  //#region src/metabase/domain/session-properties.ts
1183
+ const SessionProperties = z.object({
1184
+ "setup-token": z.string().nullable(),
1185
+ "has-user-setup": z.boolean(),
1186
+ version: z.object({ tag: z.string() }).loose(),
1187
+ "token-features": z.record(z.string(), z.boolean())
1188
+ }).loose();
1189
+ //#endregion
1190
+ //#region src/metabase/resources/session.ts
1191
+ function sessionResource(http) {
1192
+ /** GET /api/session/properties */
1193
+ async function properties() {
1194
+ return http.requestJson("GET", "/api/session/properties", { schema: SessionProperties });
1195
+ }
1196
+ /** POST /api/session */
1197
+ async function create(credentials) {
1198
+ return http.requestJson("POST", "/api/session", {
1199
+ body: credentials,
1200
+ schema: Session
1201
+ });
1202
+ }
1203
+ return {
1204
+ properties,
1205
+ create
1206
+ };
1207
+ }
1208
+ //#endregion
1209
+ //#region src/metabase/domain/engine.ts
1157
1210
  const EngineFieldOption = z.object({
1158
1211
  name: z.string(),
1159
1212
  value: z.unknown()
@@ -1179,36 +1232,16 @@ const Engine = z.object({
1179
1232
  "superseded-by": z.string().nullable(),
1180
1233
  "creatable?": z.boolean().optional()
1181
1234
  }).loose();
1182
- const SessionProperties = z.object({
1183
- "setup-token": z.string().nullable(),
1184
- "has-user-setup": z.boolean(),
1185
- version: z.object({ tag: z.string() }).loose(),
1186
- "token-features": z.record(z.string(), z.boolean()),
1187
- engines: z.record(z.string(), Engine)
1188
- }).loose();
1235
+ const Engines = z.record(z.string(), Engine);
1189
1236
  //#endregion
1190
- //#region src/metabase/resources/session.ts
1191
- function sessionResource(http) {
1192
- /** GET /api/session/properties */
1193
- async function properties() {
1194
- return http.requestJson("GET", "/api/session/properties", { schema: SessionProperties });
1195
- }
1196
- /** POST /api/session */
1197
- async function create(credentials) {
1198
- return http.requestJson("POST", "/api/session", {
1199
- body: credentials,
1200
- schema: Session
1201
- });
1202
- }
1203
- return {
1204
- properties,
1205
- create
1206
- };
1207
- }
1237
+ //#region src/metabase/domain/setting.ts
1238
+ const BooleanSetting = z.boolean();
1208
1239
  //#endregion
1209
1240
  //#region src/metabase/resources/setting.ts
1210
1241
  const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
1242
+ const ENGINES_SETTING = "engines";
1211
1243
  const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
1244
+ const TRANSFORMS_SETUP_COMPLETE_SETTING = "transforms-setup-complete";
1212
1245
  function settingResource(http) {
1213
1246
  /** PUT /api/setting/premium-embedding-token */
1214
1247
  async function setPremiumEmbeddingToken(value) {
@@ -1218,9 +1251,19 @@ function settingResource(http) {
1218
1251
  async function enableTransforms() {
1219
1252
  await http.requestNoContent("PUT", `/api/setting/${TRANSFORMS_ENABLED_SETTING}`, { body: { value: true } });
1220
1253
  }
1254
+ /** GET /api/setting/transforms-setup-complete */
1255
+ async function transformsDecided() {
1256
+ return http.requestJson("GET", `/api/setting/${TRANSFORMS_SETUP_COMPLETE_SETTING}`, { schema: BooleanSetting });
1257
+ }
1258
+ /** GET /api/setting/engines; admins only, which the API key's user is. */
1259
+ async function engines() {
1260
+ return http.requestJson("GET", `/api/setting/${ENGINES_SETTING}`, { schema: Engines });
1261
+ }
1221
1262
  return {
1222
1263
  setPremiumEmbeddingToken,
1223
- enableTransforms
1264
+ enableTransforms,
1265
+ transformsDecided,
1266
+ engines
1224
1267
  };
1225
1268
  }
1226
1269
  //#endregion
@@ -2443,13 +2486,6 @@ const PLUGINS_DIR = "plugins";
2443
2486
  const METABASE_STOP_GRACE_MS = 3e4;
2444
2487
  const DB_DIR = "metabase.db";
2445
2488
  const DB_FILE = "metabase.db";
2446
- const STAGING_STORE_ENV = {
2447
- MB_RUN_MODE: "e2e",
2448
- MB_STORE_USE_STAGING: "true"
2449
- };
2450
- const ALLOW_LOCAL_WAREHOUSES_ENV = { MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all" };
2451
- const NO_H2_SAMPLE_ENV = { MB_LOAD_SAMPLE_CONTENT: "false" };
2452
- const NO_AUDIT_DB_ENV = { MB_INSTALL_ANALYTICS_DATABASE: "false" };
2453
2489
  function instanceUrl(port) {
2454
2490
  return `http://localhost:${port}`;
2455
2491
  }
@@ -2458,10 +2494,11 @@ function serverEnv(options) {
2458
2494
  MB_DB_FILE: options.dbFile,
2459
2495
  MB_PLUGINS_DIR: options.pluginsDir,
2460
2496
  MB_SITE_URL: instanceUrl(options.port),
2461
- ...STAGING_STORE_ENV,
2462
- ...ALLOW_LOCAL_WAREHOUSES_ENV,
2463
- ...NO_H2_SAMPLE_ENV,
2464
- ...NO_AUDIT_DB_ENV
2497
+ MB_RUN_MODE: "e2e",
2498
+ MB_STORE_USE_STAGING: "true",
2499
+ MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all",
2500
+ MB_LOAD_SAMPLE_CONTENT: "false",
2501
+ MB_INSTALL_ANALYTICS_DATABASE: "false"
2465
2502
  };
2466
2503
  }
2467
2504
  function jettyEnv(port) {
@@ -3018,12 +3055,12 @@ async function portClaims(state, runtime) {
3018
3055
  }
3019
3056
  //#endregion
3020
3057
  //#region src/steps/admin-user.ts
3021
- const STEP_ID$9 = "admin-user";
3058
+ const STEP_ID$10 = "admin-user";
3022
3059
  const ADMIN_USER_KEYS = {
3023
- email: `${STEP_ID$9}.email`,
3024
- password: `${STEP_ID$9}.password`,
3025
- firstName: `${STEP_ID$9}.first-name`,
3026
- lastName: `${STEP_ID$9}.last-name`
3060
+ email: `${STEP_ID$10}.email`,
3061
+ password: `${STEP_ID$10}.password`,
3062
+ firstName: `${STEP_ID$10}.first-name`,
3063
+ lastName: `${STEP_ID$10}.last-name`
3027
3064
  };
3028
3065
  const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
3029
3066
  const GENERATED_PASSWORD_BYTES = 24;
@@ -3038,7 +3075,7 @@ function generatePassword(random) {
3038
3075
  if (DIGIT.test(candidate)) return candidate;
3039
3076
  }
3040
3077
  }
3041
- function hostDeps$13(ctx) {
3078
+ function hostDeps$14(ctx) {
3042
3079
  return {
3043
3080
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3044
3081
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
@@ -3176,9 +3213,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
3176
3213
  ctx.ui.success(`Admin user ${email} recorded`);
3177
3214
  return "completed";
3178
3215
  }
3179
- function adminUserStep(options, deps = hostDeps$13) {
3216
+ function adminUserStep(options, deps = hostDeps$14) {
3180
3217
  return {
3181
- id: STEP_ID$9,
3218
+ id: STEP_ID$10,
3182
3219
  title: "Create the admin user",
3183
3220
  redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
3184
3221
  async detect(ctx) {
@@ -3559,14 +3596,14 @@ function mbTool(runner) {
3559
3596
  }
3560
3597
  //#endregion
3561
3598
  //#region src/steps/api-key.ts
3562
- const STEP_ID$8 = "api-key";
3599
+ const STEP_ID$9 = "api-key";
3563
3600
  const API_KEY_KEYS = {
3564
- email: `${STEP_ID$8}.email`,
3565
- password: `${STEP_ID$8}.password`,
3566
- profile: `${STEP_ID$8}.profile`
3601
+ email: `${STEP_ID$9}.email`,
3602
+ password: `${STEP_ID$9}.password`,
3603
+ profile: `${STEP_ID$9}.profile`
3567
3604
  };
3568
3605
  const ADMINISTRATORS_GROUP_NAME = "Administrators";
3569
- function hostDeps$12(ctx) {
3606
+ function hostDeps$13(ctx) {
3570
3607
  return {
3571
3608
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3572
3609
  runner: processRunner(ctx.signal)
@@ -3685,9 +3722,9 @@ function usageNote(profile) {
3685
3722
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
3686
3723
  return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
3687
3724
  }
3688
- function apiKeyStep(deps = hostDeps$12) {
3725
+ function apiKeyStep(deps = hostDeps$13) {
3689
3726
  return {
3690
- id: STEP_ID$8,
3727
+ id: STEP_ID$9,
3691
3728
  title: "Connect the Metabase CLI",
3692
3729
  redoHint: "`rde status` shows the mb profile it uses.",
3693
3730
  async detect(ctx) {
@@ -4646,20 +4683,29 @@ function syncWithHint(database) {
4646
4683
  function existingDatabaseLines(databases) {
4647
4684
  return databases.map((db) => `${db.name} (${db.engine}, ${syncWithHint(db)})`);
4648
4685
  }
4686
+ function unconnectedStubsLine(stubs) {
4687
+ return `Not connected yet, used by your content: ${stubs.map((stub) => stub.name).join(", ")}; \`rde db add\` connects one`;
4688
+ }
4689
+ function listingStatus(entry) {
4690
+ if (entry.stub) return "not connected, your content uses it; `rde db add` connects it";
4691
+ return entry.initialSyncStatus === null ? "missing on the server" : syncLabel(entry.initialSyncStatus);
4692
+ }
4649
4693
  function databaseListingLines(listing) {
4650
4694
  if (listing.length === 0) return ["No databases connected yet; run `rde db add`"];
4651
4695
  return listing.map((entry) => {
4652
- const sync = entry.initialSyncStatus === null ? "missing on the server" : syncLabel(entry.initialSyncStatus);
4653
- return `${entry.name} ${entry.engine} id ${entry.id} ${sync}`;
4696
+ const engine = entry.stub ? "?" : entry.engine;
4697
+ return `${entry.name} ${engine} id ${entry.id} ${listingStatus(entry)}`;
4654
4698
  });
4655
4699
  }
4656
4700
  //#endregion
4657
4701
  //#region src/steps/databases/flow.ts
4658
4702
  const DATABASE_KEYS = {
4659
4703
  connection: "databases.connection",
4660
- skip: "databases.skip"
4704
+ skip: "databases.skip",
4705
+ stubs: "databases.stubs"
4661
4706
  };
4662
4707
  const DATABASE_KEY_NAMES = {
4708
+ connection: "connection",
4663
4709
  detectedEngine: "detected-engine",
4664
4710
  engine: "engine",
4665
4711
  mode: "mode",
@@ -4703,11 +4749,13 @@ const RSA_KEY_UNAVAILABLE_ERROR = "RSA public key is not available";
4703
4749
  const KEY_RETRIEVAL_NOTE = "allowPublicKeyRetrieval=true is added: MySQL 8 needs it to log in to a local server without SSL; to leave it out, paste a connection string with allowPublicKeyRetrieval=false instead";
4704
4750
  const KEY_RETRIEVAL_HINT = "MySQL 8 without SSL needs allowPublicKeyRetrieval=true: go back and add ?allowPublicKeyRetrieval=true to the connection string, or turn on SSL";
4705
4751
  const CONNECTION_PLACEHOLDER = "leave empty to skip";
4752
+ const STUB_CONNECTION_PLACEHOLDER = "leave empty to pick the engine";
4706
4753
  const START_SCREEN = {
4707
4754
  engine: "pick-engine",
4708
4755
  "connection-string": "connection-string"
4709
4756
  };
4710
4757
  const FRESH$1 = {
4758
+ target: null,
4711
4759
  engine: null,
4712
4760
  uri: null,
4713
4761
  pasted: null,
@@ -4717,6 +4765,35 @@ const FRESH$1 = {
4717
4765
  name: null,
4718
4766
  fieldErrors: null
4719
4767
  };
4768
+ function targeting(stub) {
4769
+ return {
4770
+ ...FRESH$1,
4771
+ target: stub,
4772
+ name: stub.name
4773
+ };
4774
+ }
4775
+ function restart(model) {
4776
+ return model.target === null ? FRESH$1 : targeting(model.target);
4777
+ }
4778
+ function nextStub(run, model) {
4779
+ const stub = run.queue.shift();
4780
+ if (stub !== void 0) return {
4781
+ type: "next",
4782
+ to: "connection-string",
4783
+ model: targeting(stub)
4784
+ };
4785
+ return run.created.length > 0 ? {
4786
+ type: "next",
4787
+ to: "another",
4788
+ model
4789
+ } : {
4790
+ type: "done",
4791
+ model
4792
+ };
4793
+ }
4794
+ function stubNamed(run, name) {
4795
+ return run.stubs.find((stub) => stub.name === name) ?? null;
4796
+ }
4720
4797
  function ordinalOf(run) {
4721
4798
  return run.created.length + 1;
4722
4799
  }
@@ -4786,20 +4863,60 @@ async function pickDetectedEngine(run, candidates) {
4786
4863
  if (entry === void 0) throw new Error(`engine ${picked} was not offered`);
4787
4864
  return entry;
4788
4865
  }
4866
+ function pickStubsScreen(run) {
4867
+ return async (model) => {
4868
+ const picked = await run.deps.prompts.multiselect({
4869
+ key: DATABASE_KEYS.stubs,
4870
+ message: "Your content uses these databases, not connected yet. Pick the ones to connect now (space picks, Enter goes on; the rest wait for `rde db add`)",
4871
+ choices: run.stubs.map((stub) => ({
4872
+ value: stub.id,
4873
+ label: stub.name
4874
+ })),
4875
+ required: false
4876
+ });
4877
+ if (picked.length > 0) {
4878
+ run.queue = run.stubs.filter((stub) => picked.includes(stub.id));
4879
+ return nextStub(run, model);
4880
+ }
4881
+ if (run.deps.start === "connection-string" && run.created.length === 0) return {
4882
+ type: "done",
4883
+ model
4884
+ };
4885
+ return {
4886
+ type: "next",
4887
+ to: "pick-engine",
4888
+ model: FRESH$1
4889
+ };
4890
+ };
4891
+ }
4892
+ function connectionQuestion(run, target) {
4893
+ if (target === null) return {
4894
+ key: DATABASE_KEYS.connection,
4895
+ message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
4896
+ placeholder: CONNECTION_PLACEHOLDER
4897
+ };
4898
+ return {
4899
+ key: databaseKey(ordinalOf(run), "connection"),
4900
+ message: `Connection string of ${target.name} (Enter to pick its engine instead)`,
4901
+ placeholder: STUB_CONNECTION_PLACEHOLDER
4902
+ };
4903
+ }
4789
4904
  function connectionStringScreen(run) {
4790
4905
  return async (model) => {
4791
4906
  let draft = model.uri;
4792
4907
  for (;;) {
4793
4908
  const uri = (await run.deps.prompts.text({
4794
- key: DATABASE_KEYS.connection,
4795
- message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
4796
- placeholder: CONNECTION_PLACEHOLDER,
4909
+ ...connectionQuestion(run, model.target),
4797
4910
  ...draft !== null && { initialValue: draft }
4798
4911
  })).trim();
4799
4912
  draft = uri;
4800
- if (uri.length === 0) return {
4913
+ if (uri.length === 0) return model.target === null ? {
4801
4914
  type: "done",
4802
4915
  model
4916
+ } : {
4917
+ type: "next",
4918
+ to: "pick-engine",
4919
+ model: restart(model)
4803
4920
  };
4804
4921
  const candidates = detectEngine(uri).map((key) => run.deps.catalog.entry(key)).filter(isCreatableEntry);
4805
4922
  const [first, ...others] = candidates;
@@ -4808,7 +4925,7 @@ function connectionStringScreen(run) {
4808
4925
  return {
4809
4926
  type: "next",
4810
4927
  to: "pick-engine",
4811
- model: FRESH$1
4928
+ model: restart(model)
4812
4929
  };
4813
4930
  }
4814
4931
  const engine = others.length === 0 ? first : await pickDetectedEngine(run, candidates);
@@ -4822,7 +4939,7 @@ function connectionStringScreen(run) {
4822
4939
  return {
4823
4940
  type: "next",
4824
4941
  to: "pick-engine",
4825
- model: FRESH$1
4942
+ model: restart(model)
4826
4943
  };
4827
4944
  }
4828
4945
  return toReview(run, model, {
@@ -4832,12 +4949,16 @@ function connectionStringScreen(run) {
4832
4949
  }
4833
4950
  };
4834
4951
  }
4952
+ function finishLabel(run, model) {
4953
+ if (model.target !== null) return `Leave ${model.target.name} unconnected`;
4954
+ return run.deps.existingCount + run.created.length > 0 ? "Finish" : "Skip for now";
4955
+ }
4835
4956
  function pickEngineScreen(run) {
4836
4957
  return async (model) => {
4837
4958
  const total = run.deps.existingCount + run.created.length;
4838
4959
  const finishChoice = {
4839
4960
  value: FINISH,
4840
- label: total > 0 ? "Finish" : "Skip for now"
4961
+ label: finishLabel(run, model)
4841
4962
  };
4842
4963
  for (;;) {
4843
4964
  const picked = await run.deps.prompts.select({
@@ -4846,6 +4967,7 @@ function pickEngineScreen(run) {
4846
4967
  choices: [...run.deps.catalog.list().map(engineChoice), finishChoice],
4847
4968
  ...model.engine !== null && { initialValue: model.engine.key }
4848
4969
  });
4970
+ if (picked === "finish" && model.target !== null) return nextStub(run, model);
4849
4971
  if (picked === "finish") {
4850
4972
  if (total > 0 || await confirmSkip(run)) return {
4851
4973
  type: "done",
@@ -4863,7 +4985,7 @@ function pickEngineScreen(run) {
4863
4985
  type: "next",
4864
4986
  to: "paste-uri",
4865
4987
  model: model.engine?.key === entry.key ? model : {
4866
- ...FRESH$1,
4988
+ ...restart(model),
4867
4989
  engine: entry
4868
4990
  }
4869
4991
  };
@@ -4894,6 +5016,10 @@ async function chooseAfterParseError(run, engine, uri, error) {
4894
5016
  if (entry === void 0) throw new Error(`engine ${picked} was not offered`);
4895
5017
  return entry;
4896
5018
  }
5019
+ function reviewName(model, sameString) {
5020
+ if (model.target !== null) return model.target.name;
5021
+ return sameString ? model.name : null;
5022
+ }
4897
5023
  function toReview(run, model, connection) {
4898
5024
  const mapped = mapDatabaseValues(connection.parsed);
4899
5025
  const sameString = model.uri === connection.parsed.uri;
@@ -4901,13 +5027,14 @@ function toReview(run, model, connection) {
4901
5027
  type: "next",
4902
5028
  to: "review",
4903
5029
  model: {
5030
+ target: model.target,
4904
5031
  engine: connection.engine,
4905
5032
  uri: connection.parsed.uri,
4906
5033
  pasted: mapped.details,
4907
5034
  details: mapped.details,
4908
5035
  failure: null,
4909
5036
  suggestedName: defaultDisplayName(mapped, connection.parsed, connection.engine),
4910
- name: sameString ? model.name : null,
5037
+ name: reviewName(model, sameString),
4911
5038
  fieldErrors: null
4912
5039
  }
4913
5040
  };
@@ -5050,6 +5177,33 @@ async function fixReportedFields(run, fields, details, errors) {
5050
5177
  }
5051
5178
  return current;
5052
5179
  }
5180
+ function reviewQuestion(target, name) {
5181
+ const edit = {
5182
+ value: "edit",
5183
+ label: "Edit a field"
5184
+ };
5185
+ if (target !== null) return {
5186
+ message: `Connect ${name}?`,
5187
+ choices: [{
5188
+ value: "test",
5189
+ label: "Test and connect"
5190
+ }, edit]
5191
+ };
5192
+ return {
5193
+ message: `Add ${name}?`,
5194
+ choices: [
5195
+ {
5196
+ value: "test",
5197
+ label: "Test and add"
5198
+ },
5199
+ edit,
5200
+ {
5201
+ value: "rename",
5202
+ label: "Change the display name"
5203
+ }
5204
+ ]
5205
+ };
5206
+ }
5053
5207
  function reviewScreen(run) {
5054
5208
  const review = reviewAttempt(run);
5055
5209
  return (model) => model.failure === null ? review(model) : orFailure(model.failure, () => review({
@@ -5075,21 +5229,7 @@ function reviewAttempt(run) {
5075
5229
  if (name === null) name = await promptName(run, current, chosen.engine);
5076
5230
  const action = await run.deps.prompts.selectOrBack({
5077
5231
  key: databaseKey(ordinalOf(run), "action"),
5078
- message: `Add ${name}?`,
5079
- choices: [
5080
- {
5081
- value: "test",
5082
- label: "Test and add"
5083
- },
5084
- {
5085
- value: "edit",
5086
- label: "Edit a field"
5087
- },
5088
- {
5089
- value: "rename",
5090
- label: "Change the display name"
5091
- }
5092
- ]
5232
+ ...reviewQuestion(model.target, name)
5093
5233
  });
5094
5234
  if (action === BACK) return { type: "back" };
5095
5235
  if (action === "edit") {
@@ -5168,26 +5308,44 @@ function testScreen(run) {
5168
5308
  function isRejectedDetails(error) {
5169
5309
  return error instanceof MetabaseApiError && error.status === BAD_REQUEST_STATUS$1;
5170
5310
  }
5311
+ function commitFor(run, stub) {
5312
+ if (stub === null) return {
5313
+ verb: "add",
5314
+ progress: "Adding",
5315
+ past: "Added",
5316
+ send: (request) => run.deps.database.create(request)
5317
+ };
5318
+ return {
5319
+ verb: "connect",
5320
+ progress: "Connecting",
5321
+ past: "Connected",
5322
+ send: ({ engine, details }) => run.deps.database.connect(stub.id, {
5323
+ engine,
5324
+ details
5325
+ })
5326
+ };
5327
+ }
5171
5328
  function createScreen(run) {
5172
5329
  return async (model) => {
5173
5330
  const chosen = requireEngine(run, model);
5174
5331
  if (model.name === null) throw new Error("no display name before create");
5175
- const spinner = run.deps.ui.spinner(`Adding ${model.name}…`);
5332
+ const commit = commitFor(run, model.target ?? stubNamed(run, model.name));
5333
+ const spinner = run.deps.ui.spinner(`${commit.progress} ${model.name}…`);
5176
5334
  let database;
5177
5335
  try {
5178
- database = await run.deps.database.create({
5336
+ database = await commit.send({
5179
5337
  name: model.name,
5180
5338
  engine: chosen.engine.key,
5181
5339
  details: sentDetails(run, chosen, model.details).details
5182
5340
  });
5183
5341
  } catch (error) {
5184
5342
  if (!isRejectedDetails(error)) {
5185
- spinner.fail(`Could not add ${model.name}`);
5343
+ spinner.fail(`Could not ${commit.verb} ${model.name}`);
5186
5344
  throw error;
5187
5345
  }
5188
5346
  spinner.fail(error.message);
5189
5347
  keyRetrievalHint(run, chosen, error.message);
5190
- const failure = `Metabase could not add ${model.name}: ${error.message}`;
5348
+ const failure = `Metabase could not ${commit.verb} ${model.name}: ${error.message}`;
5191
5349
  return {
5192
5350
  type: "next",
5193
5351
  to: "review",
@@ -5199,14 +5357,15 @@ function createScreen(run) {
5199
5357
  };
5200
5358
  }
5201
5359
  run.created.push(database);
5202
- spinner.stop(`Added ${database.name}. Initial sync is running in the background; \`rde status\` shows progress.`);
5360
+ run.stubs = run.stubs.filter((candidate) => candidate.id !== database.id);
5361
+ spinner.stop(`${commit.past} ${database.name}. Initial sync is running in the background; \`rde status\` shows progress.`);
5203
5362
  run.added.push(addedDatabase(chosen, model, model.name));
5204
5363
  await run.deps.onCreated(database);
5205
- return {
5364
+ return model.target === null ? {
5206
5365
  type: "next",
5207
5366
  to: "another",
5208
5367
  model
5209
- };
5368
+ } : nextStub(run, model);
5210
5369
  };
5211
5370
  }
5212
5371
  function anotherScreen(run) {
@@ -5227,18 +5386,24 @@ function anotherScreen(run) {
5227
5386
  };
5228
5387
  return {
5229
5388
  type: "next",
5230
- to: "pick-engine",
5389
+ to: offersStubs(run) ? "pick-stubs" : "pick-engine",
5231
5390
  model: FRESH$1
5232
5391
  };
5233
5392
  };
5234
5393
  }
5394
+ function offersStubs(run) {
5395
+ return run.deps.offerStubs && run.stubs.length > 0;
5396
+ }
5235
5397
  async function runDatabaseFlow(deps) {
5236
5398
  const run = {
5237
5399
  deps,
5238
5400
  created: [],
5239
- added: []
5401
+ added: [],
5402
+ stubs: [...deps.stubs],
5403
+ queue: []
5240
5404
  };
5241
5405
  await runFlow({
5406
+ "pick-stubs": pickStubsScreen(run),
5242
5407
  "connection-string": connectionStringScreen(run),
5243
5408
  "pick-engine": pickEngineScreen(run),
5244
5409
  "paste-uri": pasteUriScreen(run),
@@ -5246,10 +5411,11 @@ async function runDatabaseFlow(deps) {
5246
5411
  test: testScreen(run),
5247
5412
  create: createScreen(run),
5248
5413
  another: anotherScreen(run)
5249
- }, START_SCREEN[deps.start], FRESH$1);
5414
+ }, offersStubs(run) ? "pick-stubs" : START_SCREEN[deps.start], FRESH$1);
5250
5415
  return {
5251
5416
  created: run.created,
5252
- added: run.added
5417
+ added: run.added,
5418
+ stubsLeft: run.stubs
5253
5419
  };
5254
5420
  }
5255
5421
  //#endregion
@@ -5305,8 +5471,9 @@ function gitTool(runner) {
5305
5471
  }
5306
5472
  async function isRepo(dir) {
5307
5473
  if (!await directoryExists(dir)) return false;
5308
- const result = await runner.run(GIT, ["rev-parse", "--is-inside-work-tree"], { cwd: dir });
5309
- return result.exitCode === 0 && result.stdout.trim() === "true";
5474
+ const result = await runner.run(GIT, ["rev-parse", "--show-toplevel"], { cwd: dir });
5475
+ if (result.exitCode !== 0) return false;
5476
+ return result.stdout.trim() === await promises.realpath(dir);
5310
5477
  }
5311
5478
  async function initBare(path, options) {
5312
5479
  await git([
@@ -5419,13 +5586,13 @@ function gitTool(runner) {
5419
5586
  }
5420
5587
  //#endregion
5421
5588
  //#region src/steps/git-sync.ts
5422
- const STEP_ID$7 = "git-sync";
5589
+ const STEP_ID$8 = "git-sync";
5423
5590
  const GIT_SYNC_KEYS = {
5424
- location: `${STEP_ID$7}.location`,
5425
- path: `${STEP_ID$7}.path`,
5426
- remote: `${STEP_ID$7}.remote`,
5427
- url: `${STEP_ID$7}.url`,
5428
- token: `${STEP_ID$7}.token`
5591
+ location: `${STEP_ID$8}.location`,
5592
+ path: `${STEP_ID$8}.path`,
5593
+ remote: `${STEP_ID$8}.remote`,
5594
+ url: `${STEP_ID$8}.url`,
5595
+ token: `${STEP_ID$8}.token`
5429
5596
  };
5430
5597
  const BARE_REPO_NAME = "content.git";
5431
5598
  const DEFAULT_BRANCH = "main";
@@ -5455,7 +5622,11 @@ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
5455
5622
  "timed-out",
5456
5623
  "conflict"
5457
5624
  ]);
5458
- function hostDeps$11(ctx) {
5625
+ const SYNC_MODE_SETTINGS = {
5626
+ "remote-sync-type": "read-write",
5627
+ "remote-sync-transforms": true
5628
+ };
5629
+ function hostDeps$12(ctx) {
5459
5630
  return {
5460
5631
  runner: processRunner(ctx.signal),
5461
5632
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
@@ -5746,7 +5917,7 @@ async function configureLocalBare(run, runtime, workdir) {
5746
5917
  },
5747
5918
  settings: {
5748
5919
  "remote-sync-url": bareUrlFor(runtime, barePath),
5749
- "remote-sync-type": "read-write",
5920
+ ...SYNC_MODE_SETTINGS,
5750
5921
  "remote-sync-branch": prepared.branch
5751
5922
  }
5752
5923
  };
@@ -5789,7 +5960,7 @@ async function configureHosted(run, workdir, url) {
5789
5960
  settings: {
5790
5961
  "remote-sync-url": url,
5791
5962
  "remote-sync-token": token,
5792
- "remote-sync-type": "read-write",
5963
+ ...SYNC_MODE_SETTINGS,
5793
5964
  "remote-sync-branch": prepared.branch
5794
5965
  }
5795
5966
  };
@@ -5862,6 +6033,7 @@ async function importExistingContent(run, gitRemote) {
5862
6033
  }
5863
6034
  spinner.stop("Import finished");
5864
6035
  reportImport(run, outcome);
6036
+ await run.client.remoteSync.syncTransforms();
5865
6037
  }
5866
6038
  const SHELL_SAFE = /^[\w./-]+$/;
5867
6039
  function shellWord(value) {
@@ -5922,15 +6094,15 @@ async function runGitSync(ctx, deps) {
5922
6094
  ctx.ui.note(usageLines$1(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
5923
6095
  return "completed";
5924
6096
  }
5925
- function gitSyncStep(deps = hostDeps$11) {
6097
+ function gitSyncStep(deps = hostDeps$12) {
5926
6098
  return {
5927
- id: STEP_ID$7,
6099
+ id: STEP_ID$8,
5928
6100
  title: "Connect a git repository",
5929
6101
  redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
5930
6102
  async detect(ctx) {
5931
6103
  if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
5932
- if (ctx.state.steps[STEP_ID$7]?.status === "skipped") return "not-applicable";
5933
- if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
6104
+ if (ctx.state.steps[STEP_ID$8]?.status === "skipped") return "not-applicable";
6105
+ if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
5934
6106
  try {
5935
6107
  return (await mbTool(deps(ctx).runner).gitSyncStatus(ctx.state.mbProfile)).branch === null ? "todo" : "done";
5936
6108
  } catch (error) {
@@ -6071,12 +6243,12 @@ function skillsCli(runner) {
6071
6243
  }
6072
6244
  //#endregion
6073
6245
  //#region src/steps/agents-skills.ts
6074
- const STEP_ID$6 = "agents-skills";
6075
- const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$6}.agents` };
6246
+ const STEP_ID$7 = "agents-skills";
6247
+ const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
6076
6248
  const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
6077
6249
  const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
6078
6250
  const DETECT_AGENTS = { kind: "detected" };
6079
- function hostDeps$10(ctx) {
6251
+ function hostDeps$11(ctx) {
6080
6252
  return {
6081
6253
  runner: processRunner(ctx.signal),
6082
6254
  probe: hostAgentProbe(),
@@ -6165,19 +6337,19 @@ async function selectAgents(ctx, all, options) {
6165
6337
  function skipLine(all, source) {
6166
6338
  switch (source) {
6167
6339
  case "override":
6168
- case "chosen": return `Skipping the skills; run \`rde init --only ${STEP_ID$6}\` to install it later`;
6169
- case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$6} --agents <name>\` once one is installed`;
6340
+ case "chosen": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
6341
+ case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
6170
6342
  }
6171
6343
  }
6172
- function agentsSkillsStep(options, deps = hostDeps$10) {
6344
+ function agentsSkillsStep(options, deps = hostDeps$11) {
6173
6345
  return {
6174
- id: STEP_ID$6,
6346
+ id: STEP_ID$7,
6175
6347
  title: "Install the rde and data-app skills for your coding agents",
6176
6348
  redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
6177
6349
  async detect(ctx) {
6178
6350
  const resolved = deps(ctx);
6179
6351
  const detected = detectedOnly(await detectAgents(resolved.probe));
6180
- if (detected.length === 0) return ctx.state.steps[STEP_ID$6]?.status === "skipped" ? "not-applicable" : "todo";
6352
+ if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
6181
6353
  return missingSkills(await skillsCli(resolved.runner).list(), detected).length === 0 ? "done" : "todo";
6182
6354
  },
6183
6355
  async run(ctx) {
@@ -6217,7 +6389,7 @@ function agentsSkillsStep(options, deps = hostDeps$10) {
6217
6389
  }
6218
6390
  //#endregion
6219
6391
  //#region src/steps/databases.ts
6220
- const STEP_ID$5 = "databases";
6392
+ const STEP_ID$6 = "databases";
6221
6393
  const SAMPLE_STANDS_IN = { request: "sample-stands-in" };
6222
6394
  function hostDatabasesDeps(signal) {
6223
6395
  return {
@@ -6226,7 +6398,7 @@ function hostDatabasesDeps(signal) {
6226
6398
  now: () => /* @__PURE__ */ new Date()
6227
6399
  };
6228
6400
  }
6229
- function isNotServingYet$1(error) {
6401
+ function isNotServingYet$2(error) {
6230
6402
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6231
6403
  }
6232
6404
  function requireApiKey$2(state, apiKey) {
@@ -6255,14 +6427,15 @@ function authenticatedClient(state, apiKey, deps) {
6255
6427
  });
6256
6428
  }
6257
6429
  async function listDatabases(state, apiKey, deps) {
6258
- const listed = await authenticatedClient(state, apiKey, deps).database.list();
6430
+ const listed = await authenticatedClient(state, apiKey, deps).database.listWithStubs();
6259
6431
  const records = new Map(state.databases.map((record) => [record.id, record]));
6260
6432
  const onServer = listed.data.map((database) => ({
6261
6433
  id: database.id,
6262
6434
  name: database.name,
6263
6435
  engine: database.engine,
6264
6436
  initialSyncStatus: database.initial_sync_status,
6265
- createdAt: records.get(database.id)?.createdAt ?? null
6437
+ createdAt: records.get(database.id)?.createdAt ?? null,
6438
+ stub: isStub(database)
6266
6439
  }));
6267
6440
  const serverIds = new Set(listed.data.map((database) => database.id));
6268
6441
  const missing = state.databases.filter((record) => !serverIds.has(record.id)).map((record) => ({
@@ -6270,24 +6443,27 @@ async function listDatabases(state, apiKey, deps) {
6270
6443
  name: record.name,
6271
6444
  engine: record.engine,
6272
6445
  initialSyncStatus: null,
6273
- createdAt: record.createdAt
6446
+ createdAt: record.createdAt,
6447
+ stub: false
6274
6448
  }));
6275
6449
  return [...onServer, ...missing];
6276
6450
  }
6277
- async function connectDatabases(ctx, deps, start) {
6451
+ async function connectDatabases(ctx, deps, opening) {
6278
6452
  const configured = requireConfiguredRuntime(ctx.state);
6279
6453
  const client = authenticatedClient(ctx.state, ctx.secrets.apiKey, deps);
6280
- const properties = await client.session.properties();
6281
- const existing = await client.database.list();
6282
- if (existing.data.length > 0) ctx.ui.note(existingDatabaseLines(existing.data).join("\n"), "Connected databases");
6454
+ const engines = await client.setting.engines();
6455
+ const listed = await client.database.listWithStubs();
6456
+ const existing = listed.data.filter((database) => !isStub(database));
6457
+ if (existing.length > 0) ctx.ui.note(existingDatabaseLines(existing).join("\n"), "Connected databases");
6283
6458
  const result = await runDatabaseFlow({
6284
- start,
6459
+ ...opening,
6460
+ stubs: listed.data.filter(isStub),
6285
6461
  prompts: ctx.prompts,
6286
6462
  ui: ctx.ui,
6287
- catalog: engineCatalog(properties.engines),
6463
+ catalog: engineCatalog(engines),
6288
6464
  hostAlias: hostAliasOf(configured.runtime),
6289
6465
  database: client.database,
6290
- existingCount: existing.data.length,
6466
+ existingCount: existing.length,
6291
6467
  readTextFile: deps.readTextFile,
6292
6468
  onCreated: async (database) => {
6293
6469
  const record = toRecord$1(database, deps.now().toISOString());
@@ -6297,27 +6473,34 @@ async function connectDatabases(ctx, deps, start) {
6297
6473
  });
6298
6474
  }
6299
6475
  });
6476
+ if (result.stubsLeft.length > 0) ctx.ui.info(unconnectedStubsLine(result.stubsLeft));
6300
6477
  return {
6301
- existing: existing.data.length,
6478
+ existing: existing.length,
6302
6479
  created: result.created.length,
6303
6480
  added: result.added
6304
6481
  };
6305
6482
  }
6306
- async function addDatabases(ctx, deps) {
6307
- const connected = await connectDatabases(ctx, deps, "engine");
6483
+ async function addDatabases(ctx, deps, options) {
6484
+ const connected = await connectDatabases(ctx, deps, {
6485
+ start: "engine",
6486
+ ...options
6487
+ });
6308
6488
  return {
6309
6489
  outcome: connected.existing + connected.created > 0 ? "completed" : "skipped",
6310
6490
  result: { addedDatabases: connected.added }
6311
6491
  };
6312
6492
  }
6313
6493
  async function askForOwnDatabase(ctx, deps) {
6314
- const connected = await connectDatabases(ctx, deps, "connection-string");
6494
+ const connected = await connectDatabases(ctx, deps, {
6495
+ start: "connection-string",
6496
+ offerStubs: true
6497
+ });
6315
6498
  return {
6316
6499
  outcome: connected.created > 0 ? "completed" : "skipped",
6317
6500
  result: { addedDatabases: connected.added }
6318
6501
  };
6319
6502
  }
6320
- function hostDeps$9(ctx) {
6503
+ function hostDeps$10(ctx) {
6321
6504
  return {
6322
6505
  ...hostDatabasesDeps(ctx.signal),
6323
6506
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
@@ -6327,15 +6510,15 @@ function sampleDatabaseId(state) {
6327
6510
  return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
6328
6511
  }
6329
6512
  function skippedBefore(state) {
6330
- return state.steps[STEP_ID$5]?.status === "skipped";
6513
+ return state.steps[STEP_ID$6]?.status === "skipped";
6331
6514
  }
6332
6515
  function ownDatabaseIds(state) {
6333
6516
  const sampleId = sampleDatabaseId(state);
6334
6517
  return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
6335
6518
  }
6336
- function databasesStep(options, deps = hostDeps$9) {
6519
+ function databasesStep(options, deps = hostDeps$10) {
6337
6520
  return {
6338
- id: STEP_ID$5,
6521
+ id: STEP_ID$6,
6339
6522
  title: "Connect your databases",
6340
6523
  redoHint: "Connect another with `rde db add --url <connection string>`.",
6341
6524
  async detect(ctx) {
@@ -6358,14 +6541,14 @@ function databasesStep(options, deps = hostDeps$9) {
6358
6541
  try {
6359
6542
  return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
6360
6543
  } catch (error) {
6361
- if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
6544
+ if (isNotServingYet$2(error) || isRejectedCredential(error)) return "todo";
6362
6545
  throw error;
6363
6546
  }
6364
6547
  },
6365
6548
  async run(ctx) {
6366
6549
  switch (options.request) {
6367
6550
  case "ask-once": return askForOwnDatabase(ctx, deps(ctx));
6368
- case "named": return addDatabases(ctx, deps(ctx));
6551
+ case "named": return addDatabases(ctx, deps(ctx), { offerStubs: false });
6369
6552
  case "sample-stands-in": return { outcome: "skipped" };
6370
6553
  }
6371
6554
  }
@@ -6373,7 +6556,7 @@ function databasesStep(options, deps = hostDeps$9) {
6373
6556
  }
6374
6557
  //#endregion
6375
6558
  //#region src/steps/install-mb.ts
6376
- const STEP_ID$4 = "install-mb";
6559
+ const STEP_ID$5 = "install-mb";
6377
6560
  const MIN_MB_VERSION = "0.3.0";
6378
6561
  const MB_RANGE = `>=${MIN_MB_VERSION}`;
6379
6562
  function installedWith(scriptPath) {
@@ -6382,7 +6565,7 @@ function installedWith(scriptPath) {
6382
6565
  function rdeInstalledWith() {
6383
6566
  return installedWith(process.argv[1]);
6384
6567
  }
6385
- function hostDeps$8(ctx) {
6568
+ function hostDeps$9(ctx) {
6386
6569
  return {
6387
6570
  runner: processRunner(ctx.signal),
6388
6571
  installedWith: rdeInstalledWith(),
@@ -6498,9 +6681,9 @@ async function stillWrong(install, after, deps) {
6498
6681
  `${stillWrongAdvice(diagnosis)}.`
6499
6682
  ].join(" "));
6500
6683
  }
6501
- function installMbStep(deps = hostDeps$8) {
6684
+ function installMbStep(deps = hostDeps$9) {
6502
6685
  return {
6503
- id: STEP_ID$4,
6686
+ id: STEP_ID$5,
6504
6687
  title: "Install the Metabase CLI",
6505
6688
  async detect(ctx) {
6506
6689
  const found = await inspectMb(deps(ctx).runner);
@@ -6526,11 +6709,11 @@ function installMbStep(deps = hostDeps$8) {
6526
6709
  }
6527
6710
  //#endregion
6528
6711
  //#region src/steps/license.ts
6529
- const STEP_ID$3 = "license";
6530
- const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
6712
+ const STEP_ID$4 = "license";
6713
+ const LICENSE_KEYS = { token: `${STEP_ID$4}.token` };
6531
6714
  const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
6532
6715
  const NOT_FOUND_STATUS$2 = 404;
6533
- function hostDeps$7(ctx) {
6716
+ function hostDeps$8(ctx) {
6534
6717
  return {
6535
6718
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
6536
6719
  runner: processRunner(ctx.signal),
@@ -6553,13 +6736,13 @@ function activeLabel(status) {
6553
6736
  const plan = status["plan-alias"];
6554
6737
  return plan === void 0 ? "License active" : `License active (plan ${plan})`;
6555
6738
  }
6556
- function licenseStep(deps = hostDeps$7) {
6739
+ function licenseStep(deps = hostDeps$8) {
6557
6740
  return {
6558
- id: STEP_ID$3,
6741
+ id: STEP_ID$4,
6559
6742
  title: "Activate your license",
6560
6743
  redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
6561
6744
  async detect(ctx) {
6562
- if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
6745
+ if (ctx.state.steps[STEP_ID$4]?.status === "skipped") return "not-applicable";
6563
6746
  const configured = configuredRuntime(ctx.state);
6564
6747
  if (configured === null || ctx.secrets.apiKey === null) return "todo";
6565
6748
  const resolved = deps(ctx);
@@ -6650,7 +6833,7 @@ const PLATFORMS = {
6650
6833
  javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
6651
6834
  }
6652
6835
  };
6653
- function hostDeps$6(ctx) {
6836
+ function hostDeps$7(ctx) {
6654
6837
  const runner = processRunner(ctx.signal);
6655
6838
  return {
6656
6839
  platform: process.platform,
@@ -6772,7 +6955,7 @@ function checkLine(check) {
6772
6955
  function checkLines(checks) {
6773
6956
  return checks.map(checkLine);
6774
6957
  }
6775
- function preflightStep(deps = hostDeps$6) {
6958
+ function preflightStep(deps = hostDeps$7) {
6776
6959
  return {
6777
6960
  id: "preflight",
6778
6961
  title: "Check this machine",
@@ -6791,7 +6974,7 @@ function preflightStep(deps = hostDeps$6) {
6791
6974
  }
6792
6975
  //#endregion
6793
6976
  //#region src/steps/runtime.ts
6794
- const STEP_ID$2 = "runtime";
6977
+ const STEP_ID$3 = "runtime";
6795
6978
  const MIN_PORT = 1;
6796
6979
  const MAX_PORT = 65535;
6797
6980
  const PORT_PATTERN = /^\d+$/;
@@ -6805,7 +6988,7 @@ const NO_RUNTIME_FLAGS = {
6805
6988
  jar: null,
6806
6989
  port: null
6807
6990
  };
6808
- function hostDeps$5(ctx) {
6991
+ function hostDeps$6(ctx) {
6809
6992
  return {
6810
6993
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
6811
6994
  releases: releaseLookup(ctx.signal)
@@ -6934,9 +7117,9 @@ async function persistedRuntime(ctx, options, deps) {
6934
7117
  });
6935
7118
  return chosen;
6936
7119
  }
6937
- function runtimeStep(options, deps = hostDeps$5) {
7120
+ function runtimeStep(options, deps = hostDeps$6) {
6938
7121
  return {
6939
- id: STEP_ID$2,
7122
+ id: STEP_ID$3,
6940
7123
  title: "Install Metabase",
6941
7124
  redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
6942
7125
  async detect(ctx) {
@@ -6962,11 +7145,11 @@ function runtimeStep(options, deps = hostDeps$5) {
6962
7145
  }
6963
7146
  //#endregion
6964
7147
  //#region src/steps/sample-database.ts
6965
- const STEP_ID$1 = "sample-database";
7148
+ const STEP_ID$2 = "sample-database";
6966
7149
  const SAMPLE_DATABASE_DISPLAY_NAME = "Sample Database";
6967
7150
  const POSTGRES_ENGINE = "postgres";
6968
7151
  const NOT_FOUND_STATUS$1 = 404;
6969
- function hostDeps$4(ctx) {
7152
+ function hostDeps$5(ctx) {
6970
7153
  return {
6971
7154
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
6972
7155
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
@@ -6974,7 +7157,7 @@ function hostDeps$4(ctx) {
6974
7157
  now: () => /* @__PURE__ */ new Date()
6975
7158
  };
6976
7159
  }
6977
- function isNotServingYet(error) {
7160
+ function isNotServingYet$1(error) {
6978
7161
  return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
6979
7162
  }
6980
7163
  function isNotFound$2(error) {
@@ -7117,11 +7300,11 @@ async function connectDatabase(ctx, choice, configured, client, deps) {
7117
7300
  return database;
7118
7301
  }
7119
7302
  function usageLines(record) {
7120
- return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms are enabled; this database is where they can write."].join("\n");
7303
+ return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms can write to this database."].join("\n");
7121
7304
  }
7122
- function sampleDatabaseStep(deps = hostDeps$4) {
7305
+ function sampleDatabaseStep(deps = hostDeps$5) {
7123
7306
  return {
7124
- id: STEP_ID$1,
7307
+ id: STEP_ID$2,
7125
7308
  title: "Connect a sample database",
7126
7309
  async detect(ctx) {
7127
7310
  const resolved = deps(ctx);
@@ -7137,7 +7320,7 @@ function sampleDatabaseStep(deps = hostDeps$4) {
7137
7320
  try {
7138
7321
  return await recordedDatabase(record, client) === null ? "todo" : "done";
7139
7322
  } catch (error) {
7140
- if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
7323
+ if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
7141
7324
  throw error;
7142
7325
  }
7143
7326
  },
@@ -7149,16 +7332,13 @@ function sampleDatabaseStep(deps = hostDeps$4) {
7149
7332
  if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
7150
7333
  const persisted = await persistedRecord(ctx, resolved);
7151
7334
  const record = await installAndStart(ctx, sampleDatabaseContainer(persisted.record, resolved.runtime.docker, resolved.runtime.rdeHome), persisted.record, resolved);
7152
- const choice = {
7335
+ await connectDatabase(ctx, {
7153
7336
  ...persisted,
7154
7337
  record
7155
- };
7156
- const client = resolved.client(configured.url, {
7338
+ }, configured, resolved.client(configured.url, {
7157
7339
  kind: "api-key",
7158
7340
  key: apiKey
7159
- });
7160
- await connectDatabase(ctx, choice, configured, client, resolved);
7161
- await client.setting.enableTransforms();
7341
+ }), resolved);
7162
7342
  ctx.ui.note(usageLines(record), "Sample database");
7163
7343
  return { outcome: "completed" };
7164
7344
  }
@@ -7169,7 +7349,7 @@ const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
7169
7349
  //#region src/steps/start-metabase.ts
7170
7350
  const LOG_TAIL_ON_TIMEOUT = 30;
7171
7351
  const PERCENT = 100;
7172
- function hostDeps$3(ctx) {
7352
+ function hostDeps$4(ctx) {
7173
7353
  return {
7174
7354
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
7175
7355
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
@@ -7213,7 +7393,7 @@ async function startRuntime(ctx, runtime, deps) {
7213
7393
  throw error;
7214
7394
  }
7215
7395
  }
7216
- function startMetabaseStep(deps = hostDeps$3) {
7396
+ function startMetabaseStep(deps = hostDeps$4) {
7217
7397
  return {
7218
7398
  id: "start-metabase",
7219
7399
  title: "Start Metabase",
@@ -7273,9 +7453,9 @@ function startMetabaseStep(deps = hostDeps$3) {
7273
7453
  }
7274
7454
  //#endregion
7275
7455
  //#region src/steps/summary.ts
7276
- const STEP_ID = "summary";
7456
+ const STEP_ID$1 = "summary";
7277
7457
  const GIT_SYNC_STEP_ID = "git-sync";
7278
- function hostDeps$2(ctx) {
7458
+ function hostDeps$3(ctx) {
7279
7459
  return {
7280
7460
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
7281
7461
  runner: processRunner(ctx.signal),
@@ -7299,14 +7479,21 @@ async function databaseLines$1(state, apiKey, deps) {
7299
7479
  databases = (await deps.client(state.url, {
7300
7480
  kind: "api-key",
7301
7481
  key: apiKey
7302
- }).database.list()).data;
7482
+ }).database.listWithStubs()).data;
7303
7483
  } catch (error) {
7304
7484
  if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
7305
7485
  if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
7306
7486
  throw error;
7307
7487
  }
7308
- if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
7309
- return ["Databases:", ...existingDatabaseLines(databases).map((line) => ` ${line}`)];
7488
+ const connected = databases.filter((database) => !isStub(database));
7489
+ const stubs = databases.filter(isStub);
7490
+ const stubLines = stubs.length === 0 ? [] : [unconnectedStubsLine(stubs)];
7491
+ if (connected.length === 0) return ["Databases: none connected yet; `rde db add`", ...stubLines];
7492
+ return [
7493
+ "Databases:",
7494
+ ...existingDatabaseLines(connected).map((line) => ` ${line}`),
7495
+ ...stubLines
7496
+ ];
7310
7497
  }
7311
7498
  function remoteLine(remote) {
7312
7499
  switch (remote.kind) {
@@ -7353,9 +7540,9 @@ async function summaryLines(state, apiKey, deps) {
7353
7540
  lines.push("Lifecycle: rde status (--json for scripts), rde stop, rde start");
7354
7541
  return lines;
7355
7542
  }
7356
- function summaryStep(deps = hostDeps$2) {
7543
+ function summaryStep(deps = hostDeps$3) {
7357
7544
  return {
7358
- id: STEP_ID,
7545
+ id: STEP_ID$1,
7359
7546
  title: "Summary",
7360
7547
  async detect() {
7361
7548
  return "todo";
@@ -7368,6 +7555,55 @@ function summaryStep(deps = hostDeps$2) {
7368
7555
  };
7369
7556
  }
7370
7557
  //#endregion
7558
+ //#region src/steps/transforms.ts
7559
+ const STEP_ID = "transforms";
7560
+ function hostDeps$2(ctx) {
7561
+ return {
7562
+ client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
7563
+ instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
7564
+ };
7565
+ }
7566
+ function isNotServingYet(error) {
7567
+ return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
7568
+ }
7569
+ function transformsStep(deps = hostDeps$2) {
7570
+ return {
7571
+ id: STEP_ID,
7572
+ title: "Enable transforms",
7573
+ async detect(ctx) {
7574
+ const configured = configuredRuntime(ctx.state);
7575
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
7576
+ const resolved = deps(ctx);
7577
+ if (!await resolved.instanceRunning()) return "todo";
7578
+ const client = resolved.client(configured.url, {
7579
+ kind: "api-key",
7580
+ key: ctx.secrets.apiKey
7581
+ });
7582
+ try {
7583
+ return await client.setting.transformsDecided() ? "done" : "todo";
7584
+ } catch (error) {
7585
+ if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
7586
+ throw error;
7587
+ }
7588
+ },
7589
+ async run(ctx) {
7590
+ const configured = requireConfiguredRuntime(ctx.state);
7591
+ if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
7592
+ const client = deps(ctx).client(configured.url, {
7593
+ kind: "api-key",
7594
+ key: ctx.secrets.apiKey
7595
+ });
7596
+ if (await client.setting.transformsDecided()) {
7597
+ ctx.ui.info("Transforms are already set in Metabase; leaving them as they are");
7598
+ return { outcome: "completed" };
7599
+ }
7600
+ await client.setting.enableTransforms();
7601
+ ctx.ui.success("Transforms enabled");
7602
+ return { outcome: "completed" };
7603
+ }
7604
+ };
7605
+ }
7606
+ //#endregion
7371
7607
  //#region src/steps/index.ts
7372
7608
  const DEFAULT_STEP_OPTIONS = {
7373
7609
  runtime: NO_RUNTIME_FLAGS,
@@ -7383,6 +7619,7 @@ const STEP_BY_ID = {
7383
7619
  "start-metabase": () => startMetabaseStep(),
7384
7620
  "admin-user": (options) => adminUserStep(options.admin),
7385
7621
  "api-key": () => apiKeyStep(),
7622
+ transforms: () => transformsStep(),
7386
7623
  "sample-database": () => sampleDatabaseStep(),
7387
7624
  license: () => licenseStep(),
7388
7625
  databases: (options) => databasesStep(options.databases),
@@ -8318,7 +8555,7 @@ const dbAdd = defineRdeCommand({
8318
8555
  await addDatabases(await createWizardContext(name, {
8319
8556
  signal: interruptSignal,
8320
8557
  prompts
8321
- }), hostDatabasesDeps(interruptSignal));
8558
+ }), hostDatabasesDeps(interruptSignal), { offerStubs: args.url === void 0 });
8322
8559
  }
8323
8560
  });
8324
8561
  //#endregion
@@ -9360,7 +9597,8 @@ function applyDatabaseRecord(draft, record) {
9360
9597
  const value = stringOf(record.answer);
9361
9598
  const skippedConnection = record.key === DATABASE_KEYS.connection && value?.trim() === "";
9362
9599
  const finished = parseDatabaseKey(record.key)?.name === "engine" && value === "finish";
9363
- if (skippedConnection || finished) draft.databasesFinished = true;
9600
+ const pickedNoStub = record.key === DATABASE_KEYS.stubs && record.answer.kind === "multiselect" && record.answer.values.length === 0;
9601
+ if (skippedConnection || finished || pickedNoStub) draft.databasesFinished = true;
9364
9602
  }
9365
9603
  function applyAdminRecord(draft, key, value) {
9366
9604
  if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
@@ -10737,7 +10975,7 @@ const upgradeCommand = defineRdeCommand({
10737
10975
  const main = defineCommand({
10738
10976
  meta: {
10739
10977
  name: "rde",
10740
- version: "0.1.12",
10978
+ version: "0.1.13",
10741
10979
  description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
10742
10980
  },
10743
10981
  subCommands: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@metabase/rde",
3
- "version": "0.1.12",
3
+ "version": "0.1.13",
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": {