@metabase/rde 0.1.18 → 0.5.1
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.
- package/README.md +16 -15
- package/dist/cli.mjs +301 -253
- package/package.json +12 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @metabase/rde
|
|
2
2
|
|
|
3
|
-
`rde` prepares a Mac or Linux machine for data-engineering work with Metabase and coding agents. One command, `rde init`, sets up everything a working setup needs: the `mb` CLI, the `rde` skill for your coding agents, a running Metabase Enterprise instance (in Docker, or a jar run with the machine's Java), an admin user, an API key wired into `mb`, transforms switched on, your license, a database of your own for transforms to write to, and a git repository connected through Metabase's remote sync. It decides what it can on its own and asks only for what it cannot know: your license token, a connection string for your own database (`init` goes no further without one), and with a license, where the git repository lives. Every step checks whether it is already done, so the wizard can be interrupted at any prompt and picked up again with the same command.
|
|
3
|
+
`rde` prepares a Mac or Linux machine for data-engineering work with Metabase and coding agents. One command, `rde init`, sets up everything a working setup needs: the `mb` CLI, the `metabase-rde` skill for your coding agents, a running Metabase Enterprise instance (in Docker, or a jar run with the machine's Java), an admin user, an API key wired into `mb`, transforms switched on, your license, a database of your own for transforms to write to, and a git repository connected through Metabase's remote sync. It decides what it can on its own and asks only for what it cannot know: your license token, a connection string for your own database (`init` goes no further without one), and with a license, where the git repository lives. Every step checks whether it is already done, so the wizard can be interrupted at any prompt and picked up again with the same command.
|
|
4
4
|
|
|
5
5
|
After `init`, the rest is lifecycle: `rde start`, `rde stop`, `rde status`, `rde logs`, `rde open`, and `rde db add` when another database comes along. The instance lives under `~/.rde`, listens on `127.0.0.1` only, and is used through `mb` and through the agent skill.
|
|
6
6
|
|
|
@@ -60,13 +60,13 @@ The git step reads `–` until a license with remote sync is active; it is check
|
|
|
60
60
|
| Install the Metabase CLI | `@metabase/cli` (`mb`) at the `latest` dist-tag, with the package manager that installed `rde`; an installed `mb` older than what the dist-tag names now is updated. `rde status` and `rde doctor` check only what is installed and ask no registry |
|
|
61
61
|
| Install Metabase | **Docker** when it is available: pulls the major line of the newest Metabase Enterprise release, such as `metabase/metabase-enterprise:v1.65.x` (or `--image`), and creates the container. **Jar** otherwise, or with `--runtime jar`: downloads the newest enterprise release (or the tag or `.jar` path given as `--jar`) into `~/.rde/jars/` and runs it with your Java. [Metabase versions](#metabase-versions) says how the release is found. Either way on port 23000, or the first free port above it that nothing else claims, or `--port` |
|
|
62
62
|
| Start Metabase | Starts the container or the jar and waits for `/api/health`, showing Metabase's own progress |
|
|
63
|
-
| Install the rde and data-app skills | The data-
|
|
63
|
+
| Install the rde and data-app skills | The `metabase-data-apps` skill, one skill with a guide per data-app task (setup, routing, actions, semantic layer, migrate), from the `skills/metabase-data-apps/v1` folder of the `main` branch of `metabase/agent-skills`, then the `metabase-rde` skill from the `skills/metabase-rde/v1` folder of the same branch (its data-app reference settles what an app needs on an rde machine and hands the build to `metabase-data-apps`), for the coding agents you pick at a terminal, from the ones found on this machine (OpenCode, Claude Code, Codex, Cursor, Gemini CLI, Windsurf). When none is found, the step is skipped and says how to add the skills once an agent is installed; `rde` installs no agent. Without a terminal, or with `--yes`, every agent found gets the skills; `--agents` picks a set ahead of time (the skills are installed for them whether or not they are on the machine yet), `--skip-agents` none; `RDE_SKILL_SOURCE` installs both from another branch URL or a local checkout of `metabase/agent-skills`. The `v1` folder of `metabase-data-apps` is the data-app contract version Metabase serves, the same whatever Metabase version the instance runs; a breaking contract change gets a folder of its own (`v2`, …), and the guide for migrating an app Metabase marks Outdated moves it to the newer one. Installed skills whose directory changed on its branch since are installed again, without a question when every detected agent already has them all, and copies of the earlier per-task data-app skills (`metabase-data-app-setup`, `-routing`, `-actions`, `-semantic-layer`, `-migrate`) that the skills lock attributes to `metabase/metabase`, whoever installed them, are removed once `metabase-data-apps` is in, so the two never compete; a copy also installed for an agent that has no `metabase-data-apps` after the run stays, with a warning naming those agents and the `rde init --only agents-skills --agents <…>` command that replaces it. Copies of `rde` and `rde-data-apps`, the two skills an earlier rde installed in place of `metabase-rde`, are removed the same way once `metabase-rde` is in, wherever they came from. The skills are installed globally, not per instance, so every instance on the machine shares one set |
|
|
64
64
|
| Create the admin user | `admin@metabase.local` with a generated password that `rde credentials` shows, or your own with `--admin prompted`, see [Credentials](#credentials) |
|
|
65
65
|
| 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 |
|
|
66
66
|
| 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 |
|
|
67
67
|
| Activate your license | Optional. Paste a token, or press Enter to skip. The token unlocks remote sync (the `remote-sync` feature, which the git step needs), data apps (`data-apps`), and transform tests (`transforms-testing`); skipping the license goes without all three, and an activated token that lacks some is named with the ones it lacks. A token Metabase refuses is asked for again at a terminal |
|
|
68
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
|
-
| Choose what syncs to git | Only once a repository is connected
|
|
69
|
+
| Choose what syncs to git | Only once a repository is connected, and only when the license has the `library` feature. Creates the Library unless the repository already brought one in, and marks it for sync; nothing else is created or marked. When the Library was not synced yet, it asks before it changes anything whether to push it to the branch (default yes); without the push, or when the branch has commits Metabase has not imported, it prints the export to run instead |
|
|
70
70
|
| Connect your databases | Required: `init` goes no further until a database of your own is connected, as a user that can create the tables transforms write. At a terminal: pick the databases the imported content uses from a checklist, or, with none, paste a connection string (Enter goes to the engine list); review, test, add; repeat. `--database` names them ahead of time; with `--yes` or without a terminal and none named, the step fails naming `--database` and the answers file. `rde db add` starts from the same checklist, then the engine list, where the fields can also be filled one by one |
|
|
71
71
|
| Summary | URL, login email, the `mb` command for the instance, databases, repository, the export still to run when Metabase has unexported changes, and the command that opens each detected agent |
|
|
72
72
|
|
|
@@ -95,7 +95,7 @@ The second run on the same machine detects every step as done and prints the sum
|
|
|
95
95
|
│ Databases: │
|
|
96
96
|
│ warehouse (postgres, syncing; `rde status` shows when it finishes) │
|
|
97
97
|
│ Remote sync: not configured (no license) │
|
|
98
|
-
│ rde skill: installed for Claude Code
|
|
98
|
+
│ metabase-rde skill: installed for Claude Code │
|
|
99
99
|
│ Start working: │
|
|
100
100
|
│ claude (Claude Code) │
|
|
101
101
|
│ Lifecycle: rde status (--json for scripts), rde stop, rde start │
|
|
@@ -152,7 +152,7 @@ Docker is used whenever it is available: the image is self-contained, and the co
|
|
|
152
152
|
|
|
153
153
|
rde runs Metabase Enterprise 65 or newer. Once Metabase is up, `rde init`, `rde start`, and `rde upgrade` read the version it reports and refuse the OSS edition (a `v0.…` version, which `metabase/metabase` runs), a release before 65, and a build of master (a `-SNAPSHOT` version, which `metabase/metabase-enterprise-head` runs), with exit code 2 and the way to another: `rde upgrade --to <image or release tag>` for this instance, or `rde uninstall`, then `rde init --image <image>` or `rde init --jar <release tag>`, for a new one. The refused instance is left running as it is. `--jar` refuses a `v0.…` tag before anything is downloaded. With `RDE_DEV=1`, for working on rde and Metabase themselves, every version runs.
|
|
154
154
|
|
|
155
|
-
A new instance runs the newest release that Metabase's release feed (`https://static.metabase.com/version-info-ee.json`) lists of Enterprise 65 or newer, betas and release
|
|
155
|
+
A new instance runs the newest release that Metabase's release feed (`https://static.metabase.com/version-info-ee.json`) lists of Enterprise 65 or newer, a beta included: a major's betas are its `.0` builds and its first final release is `.1`, so a beta runs only until that final is out. The feed's own `latest`, like the Docker `:latest` tag, moves to a new major only weeks after its release, so rde reads neither. Docker gets the major line of that release, `metabase/metabase-enterprise:v1.<major>.x`, a tag Metabase moves to every build of the major: the state file records the line, and each pull of it brings the newest build. The jar gets the release itself. While the feed lists no release of 65 or newer, `init` stops with exit code 2 before it pulls or downloads anything, and names the ways out: `--image <image>`, `--jar <release tag>`, or `RDE_DEV=1` for the development build. When the feed cannot be read, `init` stops the same way, with the cause and `--image <image>` or `--jar <release tag>` as the way past it. In development mode the image is `metabase/metabase-enterprise-head:latest`, and the jar's `latest` is the feed's own `latest`.
|
|
156
156
|
|
|
157
157
|
## Everyday commands
|
|
158
158
|
|
|
@@ -195,9 +195,9 @@ With a license that includes remote sync, the step after the license connects a
|
|
|
195
195
|
|
|
196
196
|
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 offers to connect.
|
|
197
197
|
|
|
198
|
-
Remote sync carries only what is marked for it, so the step after the repository decides what that is: the Library (published tables with their measures and segments in Data, metrics in Metrics
|
|
198
|
+
Remote sync carries only what is marked for it, so the step after the repository decides what that is: the Library, where everything for consumption goes (published tables with their measures and segments in Data, metrics in Metrics, official dashboards in Dashboards). Transforms already sync through the repository step, by setting rather than by collection, and data apps sync their own collections. The step never creates or marks any other collection; top-level collections the repository already syncs come in marked with the import and are left as they are. The Library is created only when the import did not bring one, since a second Library would conflict with the repository's on the next import, and when the import brought it already synced, the normal case for an instance cloned from a production repository, the step changes nothing and asks nothing. Anything filed outside the synced collections stays on this instance. The step refuses to run after an import that did not finish cleanly, and names the import to run first. While remote sync is read-only, the repository decides what syncs: `rde init` skips the step, and `rde init --only sync-scope` stops and asks for read-write in Admin. `rde init --only sync-scope` runs it again; it creates nothing that is already there. An instance set up by an earlier `rde` may have an `Analytics` collection that step created; it stays as it is, synced or not, and `rde` no longer tracks it.
|
|
199
199
|
|
|
200
|
-
Marking the
|
|
200
|
+
Marking the Library is itself a change Metabase has not exported, and until it is exported a plain `mb git-sync import` refuses ("There are unsaved changes in the Remote Sync collection"). So when the Library is not synced yet, as with a fresh repository, the step asks, before it creates or marks anything, whether to push it to the branch, and on yes runs `mb git-sync export` and checks that Metabase is in sync afterwards. It asks because the branch may be one others share. On no, or when the push fails, it prints `mb git-sync export -b <branch>` and the summary repeats it while Metabase has unexported changes. When the branch has commits Metabase has not imported, it asks nothing and pushes nothing: a plain export would end in a conflict, which counts the remote commit as synced, so it names `mb git-sync export-preflight` and `mb git-sync export -b <branch> --merge` instead. Afterwards:
|
|
201
201
|
|
|
202
202
|
```sh
|
|
203
203
|
cd <your working directory>
|
|
@@ -208,7 +208,7 @@ git pull # after an export, to see the YAML files
|
|
|
208
208
|
|
|
209
209
|
## Running `init` without a terminal
|
|
210
210
|
|
|
211
|
-
Every prompt can be answered ahead of time, by a flag or by a JSON answers file, so an agent or a CI job can run `rde init` unattended. The choices `init` makes on its own need no answer, so a run without a terminal only has to cover the license, the databases, the git repository, and whether to push the
|
|
211
|
+
Every prompt can be answered ahead of time, by a flag or by a JSON answers file, so an agent or a CI job can run `rde init` unattended. The choices `init` makes on its own need no answer, so a run without a terminal only has to cover the license, the databases, the git repository, and whether to push the Library. When stdin is not a terminal, the first prompt nothing answers fails with exit code 2 and a message naming the step, the prompt, and the flag or answers key that would have answered it; nothing hangs. A text prompt with a default (a display name) takes its default, as Enter would.
|
|
212
212
|
|
|
213
213
|
### Flags
|
|
214
214
|
|
|
@@ -220,11 +220,12 @@ rde init --skip-license \
|
|
|
220
220
|
|
|
221
221
|
| Flag | Answers |
|
|
222
222
|
| --- | --- |
|
|
223
|
-
| `--agents claude-code
|
|
223
|
+
| `--agents opencode,claude-code` / `--skip-agents` | Which agents get the skill, instead of asking at a terminal or taking the ones found on the machine |
|
|
224
224
|
| `--runtime docker\|jar`, `--image`, `--jar`, `--port` | How Metabase runs, the Docker image or the jar (`latest`, an enterprise release tag, or a `.jar` path), and the host port, instead of Docker when available, the major line of the newest release, the newest release, and the first free port from 23000 that nothing else claims; `--image` implies `docker` and `--jar` implies `jar` |
|
|
225
225
|
| `--admin generated\|prompted`, `--admin-email`, `--admin-password` | The admin user, `generated` unless given; an email or password implies `prompted` |
|
|
226
226
|
| `--license-token` / `--skip-license` | The license |
|
|
227
227
|
| `--database <connection string>` (repeatable) | Your own databases, the engine read from the string; `<engine>=<connection string>` names it when the string fits several (`druid=jdbc:avatica:…`). Without a terminal, one of them or the answers file's `databases` section is required unless the instance already has a database |
|
|
228
|
+
| `--database-name <name>` (repeatable) | Display name of the `--database` in the same position. A name the repository's content already uses (the production Metabase's name) connects the placeholder a remote-sync import left for it instead of adding a second database |
|
|
228
229
|
| `--git-dir`, `--git-remote local\|hosted`, `--git-url`, `--git-token` / `--skip-git` | The content repository; a remote or URL without `--git-dir` means the current directory |
|
|
229
230
|
| `--mb-profile default\|rde` | The `mb` profile when `default` already points at another Metabase; `default` (replace it) unless given |
|
|
230
231
|
| `--yes` | Every confirmation, and the recommended choice of a question (replacing `default`); your own database then comes from `--database` or the answers file, as without a terminal. A driver's toggle (SSL, an SSH tunnel) is a setting, not a confirmation, and keeps the driver's default |
|
|
@@ -255,7 +256,7 @@ rde init --license-token '$RDE_LICENSE_TOKEN' …
|
|
|
255
256
|
}
|
|
256
257
|
```
|
|
257
258
|
|
|
258
|
-
`"runtime"` takes `"kind": "docker"` or `"jar"`, which `"image"` and `"jar"` imply on their own (`"runtime": { "jar": "latest" }` runs the newest release with Java). `"license": { "token": null }` skips the license and `"git-sync": { "location": "skip" }` leaves the repository for later. `"sync-scope": { "export": false }` leaves the first export of the
|
|
259
|
+
`"runtime"` takes `"kind": "docker"` or `"jar"`, which `"image"` and `"jar"` imply on their own (`"runtime": { "jar": "latest" }` runs the newest release with Java). `"license": { "token": null }` skips the license and `"git-sync": { "location": "skip" }` leaves the repository for later. `"sync-scope": { "export": false }` leaves the first export of the Library to you. `"databases"` lists at least one database: an empty list is refused, since `init` needs one; leave the section out to be asked at a terminal. A database is pasted as a `uri` or entered with `fields`, keyed by the driver's field names as the admin form shows them; a file field takes a path. Anything a `fields` entry leaves out is asked in the terminal, or named in the error without one. A field that engine does not ask with the others given, such as one behind the admin form's advanced options, is named in a warning and ignored.
|
|
259
260
|
|
|
260
261
|
### Recording a run
|
|
261
262
|
|
|
@@ -265,7 +266,7 @@ rde init --license-token '$RDE_LICENSE_TOKEN' …
|
|
|
265
266
|
|
|
266
267
|
`rde upgrade` moves the instance to a newer Metabase without losing its application database. It makes sure the instance is running and reads the version it is on, fetches the target, and stops there with `Already on …` when the target is what the instance already runs. Otherwise it shows the running version, the target, and the backup path, asks once (`--yes` skips the question; without a terminal `--yes` is required), and then stops Metabase, copies the whole `data/` directory to `~/.rde/instances/default/backups/<timestamp>/`, switches the instance to the target, starts it, waits for health, and records the new version. Metabase migrates its application database forward on that first start.
|
|
267
268
|
|
|
268
|
-
A move to another major version (65 to 66, say) is named before the backup with `This moves Metabase from 65 to 66`, ahead of the same question. A target that names no release shows its version only once it runs, and the plan says so: a major line (`:v1.66.x`) still names its major, so the move is named before the backup all the same, while `:latest` or a jar path names nothing, so the line comes after the start instead. Either way, once the new major runs, `rde upgrade` runs the skills step,
|
|
269
|
+
A move to another major version (65 to 66, say) is named before the backup with `This moves Metabase from 65 to 66`, ahead of the same question. A target that names no release shows its version only once it runs, and the plan says so: a major line (`:v1.66.x`) still names its major, so the move is named before the backup all the same, while `:latest` or a jar path names nothing, so the line comes after the start instead. Either way, once the new major runs, `rde upgrade` runs the skills step, which installs again every skill whose folder changed on its branch; at a terminal it asks which agents get them like `rde init` does, and with `--yes` every agent found gets them.
|
|
269
270
|
|
|
270
271
|
For a Docker instance the target is an image ref (`--to <image ref>`; by default, for an image of `metabase/metabase-enterprise`, `metabase/metabase-enterprise-head`, or `metabase/metabase`, the major line of the newest release, found the way `init` finds it, whatever tag the instance ran before: `:latest`, an older major line, or the same line), the pulled image is compared by id with the one the container runs, and the container is removed and created again from the target with the same port and mount and the environment rde sets now. An image from any other repository (a registry of your own, an image you built) is pulled again under its own ref, and the plan says so. The default target follows the feed rules of [Metabase versions](#metabase-versions): a feed that lists no release of 65 or newer, or cannot be read, stops the upgrade before it starts Metabase or pulls anything. With `RDE_DEV=1` the default target is the instance's own image. When the target is the image already running, a container created under another ref of it (`:latest` that names the same image as the line, say) or one that keeps variables rde sets otherwise now (the staging store of development mode, say) is still created again from that image under the target's ref, with no backup since nothing migrates; `rde doctor` names a container with such variables, with `rde upgrade` as the fix. A build of master (`metabase/metabase-enterprise-head:latest`, the development image) reports a major ahead of every release, so `rde upgrade` refuses it as a downgrade; `rde doctor` names one outside development mode, and the way onto a release is `rde uninstall`, then `rde init`, while `RDE_DEV=1` keeps running it. For a jar instance the target is `latest` (the default, the newest release), a release tag, or a `.jar` path; a release is downloaded into `~/.rde/jars/` before the question, and the instance's `runtime.source` is switched to it.
|
|
271
272
|
|
|
@@ -294,7 +295,7 @@ With the local `content.git` remote, that repository lives in the instance direc
|
|
|
294
295
|
```sh
|
|
295
296
|
rde uninstall # container or process, mb profile, ~/.rde/instances/default
|
|
296
297
|
rde uninstall --keep-data # container or process and profile only; the data and secrets.json stay, and `rde init` reinstalls from them
|
|
297
|
-
rde uninstall --remove-skill # also the rde and
|
|
298
|
+
rde uninstall --remove-skill # also the metabase-rde and metabase-data-apps skills (and the older rde and rde-data-apps) from your coding agents
|
|
298
299
|
rde uninstall --remove-image # also the Docker images or the downloaded jar (otherwise they stay for the next init)
|
|
299
300
|
rde uninstall --yes --force # also deletes commits in content.git that your working directory has not pulled
|
|
300
301
|
```
|
|
@@ -308,7 +309,7 @@ Running it twice is a no-op. It also works with no `~/.rde` at all, so a leftove
|
|
|
308
309
|
| Variable | What it does |
|
|
309
310
|
| --- | --- |
|
|
310
311
|
| `RDE_HOME` | Where rde keeps its state instead of `~/.rde`; a relative path is resolved against the working directory |
|
|
311
|
-
| `RDE_SKILL_SOURCE` | Development only:
|
|
312
|
+
| `RDE_SKILL_SOURCE` | Development only: the `metabase/agent-skills` branch or checkout the `metabase-rde` and `metabase-data-apps` skills are installed from (from its `skills/metabase-rde/v1` and `skills/metabase-data-apps/v1` folders) instead of the `main` branch: a GitHub URL that names a branch (`…/tree/<branch>`) or the root of a local checkout, so they can be tried before they are pushed |
|
|
312
313
|
| `RDE_DEV` | Development only: `1` runs instances on the enterprise build of master and checks license tokens against the staging store; `bin/rde-dev` sets it |
|
|
313
314
|
|
|
314
315
|
An empty value counts as unset.
|
|
@@ -372,9 +373,9 @@ Start Docker (or `colima start`), or install Java, and run `rde init` again. `rd
|
|
|
372
373
|
plain `mb` uses prod (MB_PROFILE): prod.acme.example
|
|
373
374
|
```
|
|
374
375
|
|
|
375
|
-
**The agent follows an old copy of the skill.** The `rde skill copies` row lists every copy of the skill the `skills` CLI knows, global and in the current directory's project, with where it came from, and fails when one did not come from rde's source (the branch is read from the `skills` lock file; against a local `RDE_SKILL_SOURCE` the copy's `SKILL.md` is compared with the checkout's `skills/rde/SKILL.md`). With several copies it says which one Claude Code started in that directory loads (a personal skill wins over a project one). `rde init --only agents-skills` reinstalls the global copy; restart the agent session afterwards.
|
|
376
|
+
**The agent follows an old copy of the skill.** The `metabase-rde skill copies` row lists every copy of the skill the `skills` CLI knows, global and in the current directory's project, with where it came from, and fails when one did not come from rde's source (the branch is read from the `skills` lock file; against a local `RDE_SKILL_SOURCE` the copy's `SKILL.md` is compared with the checkout's `skills/metabase-rde/v1/SKILL.md`), and also fails while a global copy of `rde` or `rde-data-apps`, which `metabase-rde` replaces, is still there. With several copies it says which one Claude Code started in that directory loads (a personal skill wins over a project one). `rde init --only agents-skills` reinstalls the global copy and removes the replaced ones; restart the agent session afterwards.
|
|
376
377
|
|
|
377
|
-
**The
|
|
378
|
+
**The agent follows the wrong data-app skill.** The `data-app skills` row fails when `metabase-data-apps` is missing or its copy did not come from rde's source (against a local `RDE_SKILL_SOURCE`, the checkout's `skills/metabase-data-apps/v1/SKILL.md`), or when a copy of an earlier per-task data-app skill (`metabase-data-app-setup` and the like) that the skills lock attributes to `metabase/metabase` is still there, whoever installed it. `rde init --only agents-skills` (or `rde doctor --fix`) installs the skills and removes the copies `metabase-data-apps` replaces; a copy also installed for an agent that has no `metabase-data-apps` after the run stays, and the run names the `--agents` that let it go. Restart the agent session afterwards.
|
|
378
379
|
|
|
379
380
|
**The license token is rejected.** Metabase checks the token with `token-check.metabase.com` before it saves it. `The license token was refused: <reason>` means the store turned the token down: check that you pasted the whole token and that its subscription is active. `The instance could not reach token-check.metabase.com to check the license token` means the check never got an answer: the container (or the jar) needs outbound network access to that host. At a terminal the step asks for the token again, and Enter skips the license and carries on without remote sync, data apps, and transform tests; without a terminal the run stops, and `rde init --from license` tries again.
|
|
380
381
|
|
package/dist/cli.mjs
CHANGED
|
@@ -369,7 +369,7 @@ const StepRecord = z.object({
|
|
|
369
369
|
completedAt: z.string()
|
|
370
370
|
});
|
|
371
371
|
const InstanceState = z.object({
|
|
372
|
-
schemaVersion: z.literal(
|
|
372
|
+
schemaVersion: z.literal(8),
|
|
373
373
|
name: z.string(),
|
|
374
374
|
runtime: RuntimeState.nullable(),
|
|
375
375
|
url: z.string().nullable(),
|
|
@@ -380,7 +380,6 @@ const InstanceState = z.object({
|
|
|
380
380
|
databases: z.array(DatabaseRecord),
|
|
381
381
|
gitRemote: GitRemoteState.nullable(),
|
|
382
382
|
movedFrom: z.string().nullable(),
|
|
383
|
-
syncRoot: z.int().nullable(),
|
|
384
383
|
steps: z.partialRecord(StepId, StepRecord)
|
|
385
384
|
});
|
|
386
385
|
function configuredRuntime(state) {
|
|
@@ -410,7 +409,7 @@ function mbCommand(profile) {
|
|
|
410
409
|
}
|
|
411
410
|
function emptyInstanceState(name) {
|
|
412
411
|
return {
|
|
413
|
-
schemaVersion:
|
|
412
|
+
schemaVersion: 8,
|
|
414
413
|
name,
|
|
415
414
|
runtime: null,
|
|
416
415
|
url: null,
|
|
@@ -421,7 +420,6 @@ function emptyInstanceState(name) {
|
|
|
421
420
|
databases: [],
|
|
422
421
|
gitRemote: null,
|
|
423
422
|
movedFrom: null,
|
|
424
|
-
syncRoot: null,
|
|
425
423
|
steps: {}
|
|
426
424
|
};
|
|
427
425
|
}
|
|
@@ -434,6 +432,7 @@ const V3 = 3;
|
|
|
434
432
|
const V4 = 4;
|
|
435
433
|
const V5 = 5;
|
|
436
434
|
const V6 = 6;
|
|
435
|
+
const V7 = 7;
|
|
437
436
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
438
437
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
439
438
|
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
@@ -450,6 +449,7 @@ const StateV5 = z.object({
|
|
|
450
449
|
steps: z.record(z.string(), z.unknown())
|
|
451
450
|
}).loose();
|
|
452
451
|
const StateV6 = z.object({ schemaVersion: z.literal(V6) }).loose();
|
|
452
|
+
const StateV7 = z.object({ schemaVersion: z.literal(V7) }).loose();
|
|
453
453
|
const StateV3 = z.object({
|
|
454
454
|
schemaVersion: z.literal(V3),
|
|
455
455
|
admin: AdminV3.nullable(),
|
|
@@ -487,8 +487,14 @@ function liftV5(raw) {
|
|
|
487
487
|
function liftV6(raw) {
|
|
488
488
|
return {
|
|
489
489
|
...StateV6.parse(raw),
|
|
490
|
-
schemaVersion:
|
|
491
|
-
|
|
490
|
+
schemaVersion: V7
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function liftV7(raw) {
|
|
494
|
+
const { syncRoot: _syncRoot, ...state } = StateV7.parse(raw);
|
|
495
|
+
return {
|
|
496
|
+
...state,
|
|
497
|
+
schemaVersion: 8
|
|
492
498
|
};
|
|
493
499
|
}
|
|
494
500
|
function adminV4(admin) {
|
|
@@ -530,15 +536,16 @@ function migrateInstanceState(raw) {
|
|
|
530
536
|
case V3: return liftV3(raw);
|
|
531
537
|
case V4: return migrateInstanceState(liftV4(raw));
|
|
532
538
|
case V5: return migrateInstanceState(liftV5(raw));
|
|
533
|
-
case V6: return
|
|
534
|
-
|
|
539
|
+
case V6: return migrateInstanceState(liftV6(raw));
|
|
540
|
+
case V7: return {
|
|
541
|
+
state: InstanceState.parse(liftV7(raw)),
|
|
535
542
|
movedSecrets: null
|
|
536
543
|
};
|
|
537
|
-
case
|
|
544
|
+
case 8: return {
|
|
538
545
|
state: InstanceState.parse(raw),
|
|
539
546
|
movedSecrets: null
|
|
540
547
|
};
|
|
541
|
-
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected
|
|
548
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 8)`);
|
|
542
549
|
}
|
|
543
550
|
}
|
|
544
551
|
//#endregion
|
|
@@ -714,6 +721,7 @@ async function requireInstanceState(name) {
|
|
|
714
721
|
}
|
|
715
722
|
const LOCK_SUFFIX = ".lock";
|
|
716
723
|
const STALE_SUFFIX = ".stale";
|
|
724
|
+
const PENDING_SUFFIX = ".pending";
|
|
717
725
|
const LOCK_RETRY_MS = 25;
|
|
718
726
|
const LOCK_WAIT_MS = 3e4;
|
|
719
727
|
function lockToken() {
|
|
@@ -731,6 +739,15 @@ function isRunning(pid) {
|
|
|
731
739
|
return !hasCode(error, "ESRCH");
|
|
732
740
|
}
|
|
733
741
|
}
|
|
742
|
+
async function createLock(path, token) {
|
|
743
|
+
const pending = `${path}${PENDING_SUFFIX}.${token.replaceAll(" ", "-")}`;
|
|
744
|
+
await promises.writeFile(pending, token, { mode: 384 });
|
|
745
|
+
try {
|
|
746
|
+
await promises.link(pending, path);
|
|
747
|
+
} finally {
|
|
748
|
+
await promises.rm(pending, { force: true });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
734
751
|
async function takeOverDeadLock(path, held) {
|
|
735
752
|
const aside = `${path}${STALE_SUFFIX}.${held.replaceAll(" ", "-")}`;
|
|
736
753
|
try {
|
|
@@ -755,10 +772,7 @@ async function acquireLock(name, signal) {
|
|
|
755
772
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
756
773
|
for (;;) {
|
|
757
774
|
try {
|
|
758
|
-
await
|
|
759
|
-
flag: "wx",
|
|
760
|
-
mode: 384
|
|
761
|
-
});
|
|
775
|
+
await createLock(path, token);
|
|
762
776
|
return token;
|
|
763
777
|
} catch (error) {
|
|
764
778
|
if (isFileNotFound$1(error)) await requireInstanceState(name);
|
|
@@ -2116,14 +2130,23 @@ async function whichCommand(name) {
|
|
|
2116
2130
|
//#endregion
|
|
2117
2131
|
//#region src/tools/agents.ts
|
|
2118
2132
|
const AgentId = z.enum([
|
|
2133
|
+
"opencode",
|
|
2119
2134
|
"claude-code",
|
|
2120
2135
|
"codex",
|
|
2121
2136
|
"cursor",
|
|
2122
2137
|
"gemini-cli",
|
|
2123
|
-
"opencode",
|
|
2124
2138
|
"windsurf"
|
|
2125
2139
|
]);
|
|
2126
2140
|
const AGENTS = [
|
|
2141
|
+
{
|
|
2142
|
+
id: "opencode",
|
|
2143
|
+
displayName: "OpenCode",
|
|
2144
|
+
skillsCliName: "opencode",
|
|
2145
|
+
command: "opencode",
|
|
2146
|
+
launch: "opencode",
|
|
2147
|
+
readsSharedSkills: true,
|
|
2148
|
+
configDir: (_home, configHome) => join(configHome, "opencode")
|
|
2149
|
+
},
|
|
2127
2150
|
{
|
|
2128
2151
|
id: "claude-code",
|
|
2129
2152
|
displayName: "Claude Code",
|
|
@@ -2160,15 +2183,6 @@ const AGENTS = [
|
|
|
2160
2183
|
readsSharedSkills: true,
|
|
2161
2184
|
configDir: (home) => join(home, ".gemini")
|
|
2162
2185
|
},
|
|
2163
|
-
{
|
|
2164
|
-
id: "opencode",
|
|
2165
|
-
displayName: "OpenCode",
|
|
2166
|
-
skillsCliName: "opencode",
|
|
2167
|
-
command: "opencode",
|
|
2168
|
-
launch: "opencode",
|
|
2169
|
-
readsSharedSkills: true,
|
|
2170
|
-
configDir: (_home, configHome) => join(configHome, "opencode")
|
|
2171
|
-
},
|
|
2172
2186
|
{
|
|
2173
2187
|
id: "windsurf",
|
|
2174
2188
|
displayName: "Windsurf",
|
|
@@ -2229,7 +2243,6 @@ const VERSION_INFO_PATH = "/version-info-ee.json";
|
|
|
2229
2243
|
const JAR_FILE = "metabase.jar";
|
|
2230
2244
|
const RELEASE_TAG$1 = /^v(?<edition>[01])\.(?<major>\d+)\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
|
|
2231
2245
|
const MAJOR_LINE_TAG = /^v(?<edition>[01])\.(?<major>\d+)\.x$/;
|
|
2232
|
-
const FINAL_ENTERPRISE_RELEASE = /^v1\.\d+\.\d+(?:\.\d+)?$/;
|
|
2233
2246
|
const ENTERPRISE_EDITION = "1";
|
|
2234
2247
|
const OPEN_SOURCE_EDITION = "0";
|
|
2235
2248
|
const SNAPSHOT_SUFFIX = "-SNAPSHOT";
|
|
@@ -2268,11 +2281,13 @@ function compareReleases(left, right) {
|
|
|
2268
2281
|
return 0;
|
|
2269
2282
|
}
|
|
2270
2283
|
function supportedRelease(version) {
|
|
2271
|
-
|
|
2272
|
-
const major =
|
|
2273
|
-
|
|
2284
|
+
const groups = RELEASE_TAG$1.exec(version)?.groups;
|
|
2285
|
+
const major = groups?.["major"];
|
|
2286
|
+
if (groups?.["edition"] !== ENTERPRISE_EDITION || major === void 0) return null;
|
|
2287
|
+
const number = Number.parseInt(major, 10);
|
|
2288
|
+
return number >= 65 ? {
|
|
2274
2289
|
version,
|
|
2275
|
-
major
|
|
2290
|
+
major: number
|
|
2276
2291
|
} : null;
|
|
2277
2292
|
}
|
|
2278
2293
|
function newestSupportedRelease(versions) {
|
|
@@ -5722,6 +5737,14 @@ function gitTool(runner) {
|
|
|
5722
5737
|
"HEAD"
|
|
5723
5738
|
], { cwd: dir })).exitCode === 0;
|
|
5724
5739
|
}
|
|
5740
|
+
async function gitPath(dir, name) {
|
|
5741
|
+
const result = await git([
|
|
5742
|
+
"rev-parse",
|
|
5743
|
+
"--git-path",
|
|
5744
|
+
name
|
|
5745
|
+
], { cwd: dir });
|
|
5746
|
+
return resolve(dir, result.stdout.trim());
|
|
5747
|
+
}
|
|
5725
5748
|
async function hasIdentity(dir) {
|
|
5726
5749
|
const result = await runner.run(GIT, [
|
|
5727
5750
|
"config",
|
|
@@ -5839,6 +5862,7 @@ function gitTool(runner) {
|
|
|
5839
5862
|
initBare,
|
|
5840
5863
|
init,
|
|
5841
5864
|
hasCommits,
|
|
5865
|
+
gitPath,
|
|
5842
5866
|
commitPaths,
|
|
5843
5867
|
addRemote,
|
|
5844
5868
|
clone,
|
|
@@ -6056,6 +6080,7 @@ const README_NAME = "README.md";
|
|
|
6056
6080
|
const README_CONTENT = "# Metabase content\n\nMetabase content synced by rde.\n";
|
|
6057
6081
|
const INITIAL_COMMIT_MESSAGE = "Initial commit";
|
|
6058
6082
|
const GITIGNORE_NAME = ".gitignore";
|
|
6083
|
+
const EXCLUDE_GIT_PATH = "info/exclude";
|
|
6059
6084
|
const IGNORED_PATHS = [
|
|
6060
6085
|
".env",
|
|
6061
6086
|
".env.local",
|
|
@@ -6076,10 +6101,7 @@ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
|
6076
6101
|
"timed-out",
|
|
6077
6102
|
"conflict"
|
|
6078
6103
|
]);
|
|
6079
|
-
const SYNC_MODE_SETTINGS = {
|
|
6080
|
-
"remote-sync-type": "read-write",
|
|
6081
|
-
"remote-sync-transforms": true
|
|
6082
|
-
};
|
|
6104
|
+
const SYNC_MODE_SETTINGS = { "remote-sync-type": "read-write" };
|
|
6083
6105
|
function hostDeps$11(ctx) {
|
|
6084
6106
|
return {
|
|
6085
6107
|
runner: processRunner(ctx.signal),
|
|
@@ -6318,20 +6340,21 @@ function hostedScreen(run) {
|
|
|
6318
6340
|
}
|
|
6319
6341
|
};
|
|
6320
6342
|
}
|
|
6321
|
-
async function ignoreDataAppFiles(
|
|
6322
|
-
const path = join(workdir, GITIGNORE_NAME);
|
|
6343
|
+
async function ignoreDataAppFiles(path) {
|
|
6323
6344
|
const existing = await readTextFile(path);
|
|
6324
6345
|
const present = new Set(existing === null ? [] : existing.split("\n").map((line) => line.trim()));
|
|
6325
6346
|
const missing = IGNORED_PATHS.filter((entry) => !present.has(entry));
|
|
6326
6347
|
if (missing.length === 0) return;
|
|
6327
6348
|
const separator = existing === null || existing.endsWith("\n") ? "" : "\n";
|
|
6349
|
+
await promises.mkdir(dirname(path), { recursive: true });
|
|
6328
6350
|
await promises.appendFile(path, `${separator}${missing.join("\n")}\n`);
|
|
6329
6351
|
}
|
|
6330
6352
|
async function prepareWorkdir(run, workdir) {
|
|
6331
6353
|
if (!await run.git.isRepo(workdir)) await run.git.init(workdir, { branch: DEFAULT_BRANCH });
|
|
6332
|
-
if (
|
|
6354
|
+
if (await run.git.hasCommits(workdir)) await ignoreDataAppFiles(await run.git.gitPath(workdir, EXCLUDE_GIT_PATH));
|
|
6355
|
+
else {
|
|
6333
6356
|
await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
|
|
6334
|
-
await ignoreDataAppFiles(workdir);
|
|
6357
|
+
await ignoreDataAppFiles(join(workdir, GITIGNORE_NAME));
|
|
6335
6358
|
await run.git.commitPaths(workdir, [README_NAME, GITIGNORE_NAME], INITIAL_COMMIT_MESSAGE);
|
|
6336
6359
|
}
|
|
6337
6360
|
return {
|
|
@@ -6385,12 +6408,9 @@ async function cloneHosted(run, workdir, credentials) {
|
|
|
6385
6408
|
throw error;
|
|
6386
6409
|
}
|
|
6387
6410
|
spinner.stop(`Cloned into ${workdir}`);
|
|
6388
|
-
|
|
6389
|
-
workdir,
|
|
6390
|
-
branch: await run.git.currentBranch(workdir)
|
|
6391
|
-
};
|
|
6411
|
+
const hadCommits = await run.git.hasCommits(workdir);
|
|
6392
6412
|
const prepared = await prepareWorkdir(run, workdir);
|
|
6393
|
-
await run.git.push(workdir, ORIGIN, prepared.branch, credentials);
|
|
6413
|
+
if (!hadCommits) await run.git.push(workdir, ORIGIN, prepared.branch, credentials);
|
|
6394
6414
|
return prepared;
|
|
6395
6415
|
}
|
|
6396
6416
|
async function adoptRepository(run, workdir, url) {
|
|
@@ -6469,7 +6489,8 @@ function unfinishedImport(outcome) {
|
|
|
6469
6489
|
}
|
|
6470
6490
|
async function importExistingContent(run, gitRemote) {
|
|
6471
6491
|
if (!await remoteHasContent(run, gitRemote)) {
|
|
6472
|
-
run.
|
|
6492
|
+
await run.client.remoteSync.syncTransforms();
|
|
6493
|
+
run.ctx.ui.info(`Nothing to import from the repository; \`${exportCommand(run.ctx.state.mbProfile, gitRemote.branch)}\` publishes this Metabase's content to it`);
|
|
6473
6494
|
return;
|
|
6474
6495
|
}
|
|
6475
6496
|
const spinner = run.ctx.ui.spinner("Importing the repository's content into Metabase…");
|
|
@@ -6607,16 +6628,18 @@ function githubContents(signal, options = {}) {
|
|
|
6607
6628
|
//#region src/tools/skills-cli.ts
|
|
6608
6629
|
const NPX = "npx";
|
|
6609
6630
|
const SKILLS_PACKAGE = "skills@latest";
|
|
6610
|
-
const
|
|
6611
|
-
const
|
|
6612
|
-
const
|
|
6613
|
-
const
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
]
|
|
6631
|
+
const LIST_INTERNAL_SKILLS = { INSTALL_INTERNAL_SKILLS: "1" };
|
|
6632
|
+
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
|
|
6633
|
+
const RDE_SKILL_NAME = "metabase-rde";
|
|
6634
|
+
const DATA_APPS_SKILL_NAME = "metabase-data-apps";
|
|
6635
|
+
const RDE_INSTALLED_SKILL_NAMES = [DATA_APPS_SKILL_NAME, RDE_SKILL_NAME];
|
|
6636
|
+
const REPLACED_RDE_SKILL_NAMES = ["rde", "rde-data-apps"];
|
|
6637
|
+
const RDE_OWNED_SKILL_NAMES = [...RDE_INSTALLED_SKILL_NAMES, ...REPLACED_RDE_SKILL_NAMES];
|
|
6638
|
+
const SKILL_VERSIONS = {
|
|
6639
|
+
[DATA_APPS_SKILL_NAME]: 1,
|
|
6640
|
+
[RDE_SKILL_NAME]: 1
|
|
6641
|
+
};
|
|
6642
|
+
const SOURCE_SKILLS_DIR = "skills";
|
|
6620
6643
|
function rdeSkillSource() {
|
|
6621
6644
|
return readSetEnv("RDE_SKILL_SOURCE") ?? "https://github.com/metabase/agent-skills/tree/main";
|
|
6622
6645
|
}
|
|
@@ -6656,16 +6679,6 @@ const hostSkillFiles = {
|
|
|
6656
6679
|
readJson: readJsonFile,
|
|
6657
6680
|
readText: readTextFile
|
|
6658
6681
|
};
|
|
6659
|
-
function dataAppSkillsSource(ref) {
|
|
6660
|
-
return `${DATA_APP_REPOSITORY}/skills#${ref}`;
|
|
6661
|
-
}
|
|
6662
|
-
function dataAppSkillsOrigin(ref) {
|
|
6663
|
-
return {
|
|
6664
|
-
kind: "github",
|
|
6665
|
-
repository: DATA_APP_REPOSITORY,
|
|
6666
|
-
ref
|
|
6667
|
-
};
|
|
6668
|
-
}
|
|
6669
6682
|
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
6670
6683
|
function skillOrigin(source, cwd) {
|
|
6671
6684
|
const github = GITHUB_URL.exec(source);
|
|
@@ -6686,6 +6699,23 @@ function skillOrigin(source, cwd) {
|
|
|
6686
6699
|
source
|
|
6687
6700
|
};
|
|
6688
6701
|
}
|
|
6702
|
+
function sourceSkillDirectory(skill) {
|
|
6703
|
+
return posix.join(SOURCE_SKILLS_DIR, skill, `v${SKILL_VERSIONS[skill]}`);
|
|
6704
|
+
}
|
|
6705
|
+
function skillSource(rdeSource, skill, cwd) {
|
|
6706
|
+
const directory = sourceSkillDirectory(skill);
|
|
6707
|
+
const origin = skillOrigin(rdeSource, cwd);
|
|
6708
|
+
switch (origin.kind) {
|
|
6709
|
+
case "github":
|
|
6710
|
+
if (origin.ref === null) throw new ConfigError(`RDE_SKILL_SOURCE must name a branch: https://github.com/${origin.repository}/tree/<branch>`);
|
|
6711
|
+
return `https://github.com/${origin.repository}/tree/${origin.ref}/${directory}`;
|
|
6712
|
+
case "local": return join(origin.path, directory);
|
|
6713
|
+
case "other": {
|
|
6714
|
+
const [path, ref] = origin.source.split("#", 2);
|
|
6715
|
+
return ref === void 0 ? `${path}/${directory}` : `${path}/${directory}#${ref}`;
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6718
|
+
}
|
|
6689
6719
|
function skillsCli(runner) {
|
|
6690
6720
|
async function add(options) {
|
|
6691
6721
|
const argv = [
|
|
@@ -6719,7 +6749,10 @@ function skillsCli(runner) {
|
|
|
6719
6749
|
"-g",
|
|
6720
6750
|
"--json"
|
|
6721
6751
|
];
|
|
6722
|
-
const result = await runner.run(NPX, argv, {
|
|
6752
|
+
const result = await runner.run(NPX, argv, {
|
|
6753
|
+
stdoutToFile: true,
|
|
6754
|
+
addedEnv: LIST_INTERNAL_SKILLS
|
|
6755
|
+
});
|
|
6723
6756
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
6724
6757
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
6725
6758
|
}
|
|
@@ -6732,7 +6765,8 @@ function skillsCli(runner) {
|
|
|
6732
6765
|
];
|
|
6733
6766
|
const result = await runner.run(NPX, argv, {
|
|
6734
6767
|
cwd,
|
|
6735
|
-
stdoutToFile: true
|
|
6768
|
+
stdoutToFile: true,
|
|
6769
|
+
addedEnv: LIST_INTERNAL_SKILLS
|
|
6736
6770
|
});
|
|
6737
6771
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
6738
6772
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
@@ -6749,7 +6783,23 @@ function skillsCli(runner) {
|
|
|
6749
6783
|
const STEP_ID$5 = "agents-skills";
|
|
6750
6784
|
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
|
|
6751
6785
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6752
|
-
const REQUIRED_SKILL_NAMES =
|
|
6786
|
+
const REQUIRED_SKILL_NAMES = RDE_INSTALLED_SKILL_NAMES;
|
|
6787
|
+
const LEGACY_DATA_APP_REPOSITORY = "metabase/metabase";
|
|
6788
|
+
const LEGACY_DATA_APP_SKILL_NAMES = [
|
|
6789
|
+
"metabase-data-app-setup",
|
|
6790
|
+
"metabase-data-app-routing",
|
|
6791
|
+
"metabase-data-app-actions",
|
|
6792
|
+
"metabase-data-app-semantic-layer",
|
|
6793
|
+
"metabase-data-app-migrate"
|
|
6794
|
+
];
|
|
6795
|
+
const REPLACED_RDE_NAMES = REPLACED_RDE_SKILL_NAMES;
|
|
6796
|
+
const REPLACEMENTS = [{
|
|
6797
|
+
by: DATA_APPS_SKILL_NAME,
|
|
6798
|
+
names: LEGACY_DATA_APP_SKILL_NAMES
|
|
6799
|
+
}, {
|
|
6800
|
+
by: RDE_SKILL_NAME,
|
|
6801
|
+
names: REPLACED_RDE_NAMES
|
|
6802
|
+
}];
|
|
6753
6803
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6754
6804
|
const ASK_AGENTS = { kind: "ask" };
|
|
6755
6805
|
function hostDeps$10(ctx) {
|
|
@@ -6792,34 +6842,17 @@ function coveredAgentNames(entry, agents) {
|
|
|
6792
6842
|
function installedLines(entries, agents) {
|
|
6793
6843
|
return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
|
|
6794
6844
|
}
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
return
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
6802
|
-
|
|
6803
|
-
|
|
6804
|
-
}
|
|
6805
|
-
|
|
6806
|
-
return dataAppSkillsRef(metabaseVersion, developmentMode());
|
|
6807
|
-
}
|
|
6808
|
-
function skillSets(deps, metabaseVersion) {
|
|
6809
|
-
const dataAppRef = wantedDataAppRef(metabaseVersion);
|
|
6810
|
-
return [{
|
|
6811
|
-
label: "data-app",
|
|
6812
|
-
source: dataAppSkillsSource(dataAppRef),
|
|
6813
|
-
sourceNote: ` from ${dataAppRef}`,
|
|
6814
|
-
origin: dataAppSkillsOrigin(dataAppRef),
|
|
6815
|
-
skills: DATA_APP_SKILL_NAMES
|
|
6816
|
-
}, {
|
|
6817
|
-
label: "rde",
|
|
6818
|
-
source: deps.skillSource,
|
|
6819
|
-
sourceNote: deps.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${deps.skillSource}`,
|
|
6820
|
-
origin: skillOrigin(deps.skillSource, deps.cwd),
|
|
6821
|
-
skills: RDE_SOURCE_SKILL_NAMES
|
|
6822
|
-
}];
|
|
6845
|
+
function skillInstalls(deps) {
|
|
6846
|
+
const defaultSource = deps.skillSource === DEFAULT_RDE_SKILL_SOURCE;
|
|
6847
|
+
return RDE_INSTALLED_SKILL_NAMES.map((skill) => {
|
|
6848
|
+
const source = skillSource(deps.skillSource, skill, deps.cwd);
|
|
6849
|
+
return {
|
|
6850
|
+
skill,
|
|
6851
|
+
source,
|
|
6852
|
+
sourceNote: ` from ${defaultSource ? sourceSkillDirectory(skill) : source}`,
|
|
6853
|
+
origin: skillOrigin(source, deps.cwd)
|
|
6854
|
+
};
|
|
6855
|
+
});
|
|
6823
6856
|
}
|
|
6824
6857
|
function hashLookup(github) {
|
|
6825
6858
|
const asked = /* @__PURE__ */ new Map();
|
|
@@ -6844,7 +6877,8 @@ async function readLockedSkills(deps) {
|
|
|
6844
6877
|
const lockPath = globalSkillLockPath(deps.probe.home, deps.stateHome);
|
|
6845
6878
|
try {
|
|
6846
6879
|
const lock = await deps.files.readJson(lockPath);
|
|
6847
|
-
|
|
6880
|
+
const names = [...REQUIRED_SKILL_NAMES, ...LEGACY_DATA_APP_SKILL_NAMES];
|
|
6881
|
+
return new Map(names.map((name) => [name, lockedSkill(lockPath, lock, name)]));
|
|
6848
6882
|
} catch (error) {
|
|
6849
6883
|
if (error instanceof ConfigError) return null;
|
|
6850
6884
|
throw error;
|
|
@@ -6864,25 +6898,54 @@ function lockedCopy(locked, origin) {
|
|
|
6864
6898
|
hash: locked.skillFolderHash
|
|
6865
6899
|
};
|
|
6866
6900
|
}
|
|
6867
|
-
async function
|
|
6868
|
-
const { origin } =
|
|
6901
|
+
async function installBehind(install, check) {
|
|
6902
|
+
const { origin } = install;
|
|
6869
6903
|
if (origin.kind !== "github" || check.locked === null) return true;
|
|
6870
|
-
const
|
|
6871
|
-
|
|
6872
|
-
const copy = lockedCopy(check.locked.get(skill) ?? null, origin);
|
|
6873
|
-
if (copy === null) return true;
|
|
6874
|
-
copies.push(copy);
|
|
6875
|
-
}
|
|
6904
|
+
const copy = lockedCopy(check.locked.get(install.skill) ?? null, origin);
|
|
6905
|
+
if (copy === null) return true;
|
|
6876
6906
|
if (check.updates === "installed") return false;
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6907
|
+
const current = await check.hashes(origin.repository, copy.ref, copy.directory);
|
|
6908
|
+
return current !== null && current.get(copy.folder) !== copy.hash;
|
|
6909
|
+
}
|
|
6910
|
+
function isLegacyDataAppCopy(name, source) {
|
|
6911
|
+
return LEGACY_DATA_APP_SKILL_NAMES.includes(name) && source === LEGACY_DATA_APP_REPOSITORY;
|
|
6912
|
+
}
|
|
6913
|
+
function isReplacedRdeCopy(name) {
|
|
6914
|
+
return REPLACED_RDE_NAMES.includes(name);
|
|
6915
|
+
}
|
|
6916
|
+
function replacedCopies(installed, locked) {
|
|
6917
|
+
const found = /* @__PURE__ */ new Map();
|
|
6918
|
+
for (const { by, names } of REPLACEMENTS) {
|
|
6919
|
+
const present = names.filter((name) => by === "metabase-data-apps" ? isLegacyDataAppCopy(name, locked?.get(name)?.source) : installed.some((entry) => entry.name === name));
|
|
6920
|
+
if (present.length > 0) found.set(by, present);
|
|
6880
6921
|
}
|
|
6881
|
-
return
|
|
6922
|
+
return found;
|
|
6882
6923
|
}
|
|
6883
6924
|
function hasSkill(installed, skill, agent) {
|
|
6884
6925
|
return installed.some((entry) => entry.name === skill && skillCoversAgent(entry, agent));
|
|
6885
6926
|
}
|
|
6927
|
+
function splitReplaced(by, replaced, installed, all) {
|
|
6928
|
+
const split = {
|
|
6929
|
+
removable: [],
|
|
6930
|
+
kept: [],
|
|
6931
|
+
uncovered: []
|
|
6932
|
+
};
|
|
6933
|
+
for (const name of replaced) {
|
|
6934
|
+
const copies = installed.filter((entry) => entry.name === name);
|
|
6935
|
+
const uncovered = all.filter((agent) => copies.some((entry) => skillCoversAgent(entry, agent)) && !hasSkill(installed, by, agent));
|
|
6936
|
+
if (uncovered.length === 0) {
|
|
6937
|
+
split.removable.push(name);
|
|
6938
|
+
continue;
|
|
6939
|
+
}
|
|
6940
|
+
split.kept.push(name);
|
|
6941
|
+
for (const agent of uncovered) if (!split.uncovered.includes(agent)) split.uncovered.push(agent);
|
|
6942
|
+
}
|
|
6943
|
+
return split;
|
|
6944
|
+
}
|
|
6945
|
+
function keptReplacedLine(by, split) {
|
|
6946
|
+
const ids = split.uncovered.map((agent) => agent.id).join(",");
|
|
6947
|
+
return `${by} is not installed for ${displayNames(split.uncovered)}, so rde keeps ${split.kept.join(", ")}; \`rde init --only ${STEP_ID$5} --agents ${ids}\` installs it there and removes what it replaces`;
|
|
6948
|
+
}
|
|
6886
6949
|
function coversAll(installed, skills, agents) {
|
|
6887
6950
|
return agents.every((agent) => skills.every((skill) => hasSkill(installed, skill, agent)));
|
|
6888
6951
|
}
|
|
@@ -6948,7 +7011,7 @@ function skipLine(all, source) {
|
|
|
6948
7011
|
}
|
|
6949
7012
|
function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
6950
7013
|
let hashes = null;
|
|
6951
|
-
async function
|
|
7014
|
+
async function installCheck(resolved) {
|
|
6952
7015
|
hashes ??= hashLookup(resolved.github);
|
|
6953
7016
|
return {
|
|
6954
7017
|
updates,
|
|
@@ -6964,9 +7027,11 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
|
6964
7027
|
const resolved = deps(ctx);
|
|
6965
7028
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6966
7029
|
if (detected.length === 0) return ctx.state.steps[STEP_ID$5]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
7030
|
+
const installed = await skillsCli(resolved.runner).list();
|
|
7031
|
+
if (missingSkills(installed, detected).length > 0) return "todo";
|
|
7032
|
+
const check = await installCheck(resolved);
|
|
7033
|
+
if (replacedCopies(installed, check.locked).size > 0) return "outdated";
|
|
7034
|
+
for (const install of skillInstalls(resolved)) if (await installBehind(install, check)) return "outdated";
|
|
6970
7035
|
return "done";
|
|
6971
7036
|
},
|
|
6972
7037
|
async run(ctx) {
|
|
@@ -6981,28 +7046,37 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
|
6981
7046
|
return { outcome: "skipped" };
|
|
6982
7047
|
}
|
|
6983
7048
|
const before = await installedNow();
|
|
6984
|
-
const check = await
|
|
7049
|
+
const check = await installCheck(resolved);
|
|
6985
7050
|
const pending = [];
|
|
6986
|
-
for (const
|
|
7051
|
+
for (const install of skillInstalls(resolved)) if (!coversAll(before, [install.skill], selection.agents) || await installBehind(install, check)) pending.push(install);
|
|
7052
|
+
const replaced = replacedCopies(before, check.locked);
|
|
6987
7053
|
const names = displayNames(selection.agents);
|
|
6988
|
-
if (pending.length === 0) {
|
|
7054
|
+
if (pending.length === 0 && replaced.size === 0) {
|
|
6989
7055
|
ctx.ui.success(`The rde and data-app skills are current for ${names}`);
|
|
6990
7056
|
return { outcome: "completed" };
|
|
6991
7057
|
}
|
|
6992
7058
|
const agents = selection.agents.map((agent) => agent.skillsCliName);
|
|
6993
7059
|
const forWhom = `for ${names} (${SOURCE_NOTES[selection.source]})`;
|
|
6994
|
-
for (const
|
|
6995
|
-
ctx.ui.info(`Installing
|
|
7060
|
+
for (const install of pending) {
|
|
7061
|
+
ctx.ui.info(`Installing ${install.skill} ${forWhom}${install.sourceNote}`);
|
|
6996
7062
|
await skills.add({
|
|
6997
|
-
source:
|
|
6998
|
-
skills:
|
|
7063
|
+
source: install.source,
|
|
7064
|
+
skills: [install.skill],
|
|
6999
7065
|
agents,
|
|
7000
7066
|
output: ctx.ui.childOutput
|
|
7001
7067
|
});
|
|
7002
7068
|
}
|
|
7003
|
-
const installed = await skills.list();
|
|
7069
|
+
const installed = pending.length === 0 ? before : await skills.list();
|
|
7004
7070
|
const missing = unlistedSkills(installed);
|
|
7005
7071
|
if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
|
|
7072
|
+
for (const [by, old] of replaced) {
|
|
7073
|
+
const split = splitReplaced(by, old, installed, all);
|
|
7074
|
+
if (split.removable.length > 0) {
|
|
7075
|
+
ctx.ui.info(`Removing ${split.removable.join(", ")}, which ${by} replaces`);
|
|
7076
|
+
await skills.remove(split.removable, ctx.ui.childOutput);
|
|
7077
|
+
}
|
|
7078
|
+
if (split.kept.length > 0) ctx.ui.warn(keptReplacedLine(by, split));
|
|
7079
|
+
}
|
|
7006
7080
|
ctx.ui.note(installedLines(requiredEntries(installed), selection.agents), "Skills installed");
|
|
7007
7081
|
ctx.ui.info(MECHANICS_NOTE);
|
|
7008
7082
|
return { outcome: "completed" };
|
|
@@ -7190,7 +7264,7 @@ function npmRegistry(signal, options = {}) {
|
|
|
7190
7264
|
//#endregion
|
|
7191
7265
|
//#region src/steps/install-mb.ts
|
|
7192
7266
|
const STEP_ID$4 = "install-mb";
|
|
7193
|
-
const MIN_MB_VERSION = "0.
|
|
7267
|
+
const MIN_MB_VERSION = "0.5.1";
|
|
7194
7268
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
7195
7269
|
function installedWith(scriptPath) {
|
|
7196
7270
|
return scriptPath === void 0 ? "npm" : packageManagerFor(scriptPath);
|
|
@@ -7918,10 +7992,8 @@ function startMetabaseStep(deps = hostDeps$5, accepted = "supported") {
|
|
|
7918
7992
|
//#region src/steps/sync-scope.ts
|
|
7919
7993
|
const STEP_ID$2 = "sync-scope";
|
|
7920
7994
|
const SYNC_SCOPE_KEYS = { export: `${STEP_ID$2}.export` };
|
|
7921
|
-
const AGENT_COLLECTION_NAME = "Analytics";
|
|
7922
7995
|
const LIBRARY_NAME = "Library";
|
|
7923
7996
|
const ROOT_COLLECTION_NAME = "Our analytics";
|
|
7924
|
-
const AGENT_COLLECTION_DESCRIPTION = "Dashboards, questions, models, and documents that reach git on export.";
|
|
7925
7997
|
function hostDeps$4(ctx) {
|
|
7926
7998
|
return {
|
|
7927
7999
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -7932,22 +8004,11 @@ function hostDeps$4(ctx) {
|
|
|
7932
8004
|
function isCollection(entry) {
|
|
7933
8005
|
return typeof entry.id === "number";
|
|
7934
8006
|
}
|
|
7935
|
-
function
|
|
7936
|
-
return collection
|
|
7937
|
-
}
|
|
7938
|
-
function readScope(list, syncRoot) {
|
|
7939
|
-
const topLevel = list.filter(isCollection).filter((collection) => collection.location === "/");
|
|
7940
|
-
const ordinary = topLevel.filter(isOrdinary);
|
|
7941
|
-
const unsynced = ordinary.filter((collection) => !collection.is_remote_synced);
|
|
7942
|
-
return {
|
|
7943
|
-
library: topLevel.find((collection) => collection.type === "library") ?? null,
|
|
7944
|
-
syncedRoots: ordinary.filter((collection) => collection.is_remote_synced),
|
|
7945
|
-
unsyncedRecordedRoot: unsynced.find((collection) => collection.id === syncRoot) ?? null,
|
|
7946
|
-
foreignNamesake: unsynced.some((collection) => collection.id !== syncRoot && collection.name === "Analytics")
|
|
7947
|
-
};
|
|
8007
|
+
function findLibrary(list) {
|
|
8008
|
+
return list.filter(isCollection).find((collection) => collection.location === "/" && collection.type === "library") ?? null;
|
|
7948
8009
|
}
|
|
7949
|
-
function isComplete(
|
|
7950
|
-
return
|
|
8010
|
+
function isComplete(library, libraryLicensed) {
|
|
8011
|
+
return !libraryLicensed || library !== null && library.is_remote_synced;
|
|
7951
8012
|
}
|
|
7952
8013
|
function isNotAnsweringYet(error) {
|
|
7953
8014
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || isRejectedCredential(error);
|
|
@@ -7976,34 +8037,8 @@ async function isReadOnly(client) {
|
|
|
7976
8037
|
async function requireReadWrite(client) {
|
|
7977
8038
|
if (await isReadOnly(client)) throw new ConfigError(`remote sync is read-only; the repository decides what syncs. Switch it to read-write in Admin, then \`rde init --only ${STEP_ID$2}\``);
|
|
7978
8039
|
}
|
|
7979
|
-
async function ensureLibrary(client,
|
|
7980
|
-
|
|
7981
|
-
return scope.library ?? client.collection.createLibrary();
|
|
7982
|
-
}
|
|
7983
|
-
async function ensureRoots(ctx, client, scope) {
|
|
7984
|
-
if (scope.syncedRoots.length > 0) return {
|
|
7985
|
-
roots: scope.syncedRoots,
|
|
7986
|
-
created: false,
|
|
7987
|
-
besideNamesake: false
|
|
7988
|
-
};
|
|
7989
|
-
if (scope.unsyncedRecordedRoot !== null) return {
|
|
7990
|
-
roots: [scope.unsyncedRecordedRoot],
|
|
7991
|
-
created: false,
|
|
7992
|
-
besideNamesake: false
|
|
7993
|
-
};
|
|
7994
|
-
const created = await client.collection.create({
|
|
7995
|
-
name: AGENT_COLLECTION_NAME,
|
|
7996
|
-
description: AGENT_COLLECTION_DESCRIPTION
|
|
7997
|
-
});
|
|
7998
|
-
await ctx.saveState({
|
|
7999
|
-
...ctx.state,
|
|
8000
|
-
syncRoot: created.id
|
|
8001
|
-
});
|
|
8002
|
-
return {
|
|
8003
|
-
roots: [created],
|
|
8004
|
-
created: true,
|
|
8005
|
-
besideNamesake: scope.foreignNamesake
|
|
8006
|
-
};
|
|
8040
|
+
async function ensureLibrary(client, library) {
|
|
8041
|
+
return library ?? client.collection.createLibrary();
|
|
8007
8042
|
}
|
|
8008
8043
|
function itemLabel(item) {
|
|
8009
8044
|
return item.name === null ? `${item.model} ${item.id}` : `${item.model} ${item.id} "${item.name}"`;
|
|
@@ -8040,21 +8075,12 @@ function explainRefusal(error) {
|
|
|
8040
8075
|
if (!refusal.success) return error;
|
|
8041
8076
|
return new ConfigError([...refusal.data.errors.required.flatMap(requiredSyncLines), `After the fix, run \`rde init --only ${STEP_ID$2}\`.`].join("\n"));
|
|
8042
8077
|
}
|
|
8043
|
-
function plannedNames(scope, libraryLicensed) {
|
|
8044
|
-
const library = libraryLicensed ? [scope.library?.name ?? LIBRARY_NAME] : [];
|
|
8045
|
-
if (scope.syncedRoots.length > 0) return [...library, ...scope.syncedRoots.map((collection) => collection.name)];
|
|
8046
|
-
return [...library, scope.unsyncedRecordedRoot?.name ?? "Analytics"];
|
|
8047
|
-
}
|
|
8048
|
-
function nameList(names) {
|
|
8049
|
-
if (names.length <= 2) return names.join(" and ");
|
|
8050
|
-
return `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
|
|
8051
|
-
}
|
|
8052
8078
|
function exportAdvice(profile, branch, changes) {
|
|
8053
8079
|
const command = exportCommand(profile, branch);
|
|
8054
8080
|
if (!remoteMoved(changes)) return `\`${command}\``;
|
|
8055
8081
|
return `the branch has commits Metabase has not imported; \`${mbCommand(profile)} git-sync export-preflight\`, then \`${command} --merge\``;
|
|
8056
8082
|
}
|
|
8057
|
-
async function planExport(ctx, runner, instance,
|
|
8083
|
+
async function planExport(ctx, runner, instance, name) {
|
|
8058
8084
|
const changes = await mbTool(runner).gitSyncHasRemoteChanges(instance);
|
|
8059
8085
|
if (remoteMoved(changes)) return {
|
|
8060
8086
|
changes,
|
|
@@ -8064,7 +8090,7 @@ async function planExport(ctx, runner, instance, names) {
|
|
|
8064
8090
|
changes,
|
|
8065
8091
|
push: await ctx.prompts.confirm({
|
|
8066
8092
|
key: SYNC_SCOPE_KEYS.export,
|
|
8067
|
-
message: `Push ${
|
|
8093
|
+
message: `Push ${name} to ${requireGitRemote(ctx).branch} now?`,
|
|
8068
8094
|
initialValue: true
|
|
8069
8095
|
})
|
|
8070
8096
|
};
|
|
@@ -8079,7 +8105,7 @@ function reportPending(run) {
|
|
|
8079
8105
|
}
|
|
8080
8106
|
async function exportScope(run, runner) {
|
|
8081
8107
|
const mb = mbTool(runner);
|
|
8082
|
-
const label =
|
|
8108
|
+
const label = run.name;
|
|
8083
8109
|
const spinner = run.ctx.ui.spinner(`Pushing ${label} to ${run.gitRemote.branch}…`);
|
|
8084
8110
|
try {
|
|
8085
8111
|
const final = (await mb.gitSyncExport(run.instance, {
|
|
@@ -8100,9 +8126,6 @@ async function exportScope(run, runner) {
|
|
|
8100
8126
|
reportPending(run);
|
|
8101
8127
|
}
|
|
8102
8128
|
}
|
|
8103
|
-
function scopeLabel(synced) {
|
|
8104
|
-
return `Synced to git: ${synced.map((collection) => collection.name).join(", ")}`;
|
|
8105
|
-
}
|
|
8106
8129
|
function syncScopeStep(deps = hostDeps$4) {
|
|
8107
8130
|
return {
|
|
8108
8131
|
id: STEP_ID$2,
|
|
@@ -8118,7 +8141,7 @@ function syncScopeStep(deps = hostDeps$4) {
|
|
|
8118
8141
|
key: ctx.secrets.apiKey
|
|
8119
8142
|
});
|
|
8120
8143
|
try {
|
|
8121
|
-
if (isComplete(
|
|
8144
|
+
if (isComplete(findLibrary(await client.collection.list()), hasLibrary(ctx.state.license))) return "done";
|
|
8122
8145
|
return await isReadOnly(client) ? "not-applicable" : "todo";
|
|
8123
8146
|
} catch (error) {
|
|
8124
8147
|
if (isNotAnsweringYet(error)) return "todo";
|
|
@@ -8137,30 +8160,27 @@ function syncScopeStep(deps = hostDeps$4) {
|
|
|
8137
8160
|
await requireReadWrite(client);
|
|
8138
8161
|
await requireSettledImport(ctx, client, gitRemote);
|
|
8139
8162
|
const libraryLicensed = hasLibrary(ctx.state.license);
|
|
8140
|
-
const
|
|
8141
|
-
|
|
8142
|
-
|
|
8163
|
+
const found = findLibrary(await client.collection.list());
|
|
8164
|
+
if (isComplete(found, libraryLicensed)) {
|
|
8165
|
+
ctx.ui.info(libraryLicensed ? `Synced to git: ${found?.name ?? LIBRARY_NAME}` : "The license has no Library, so there is nothing to mark for sync");
|
|
8166
|
+
return { outcome: "completed" };
|
|
8167
|
+
}
|
|
8168
|
+
const name = found?.name ?? LIBRARY_NAME;
|
|
8169
|
+
const plan = await planExport(ctx, resolved.runner, instance, name);
|
|
8143
8170
|
const spinner = ctx.ui.spinner("Choosing what syncs to git…");
|
|
8144
|
-
let synced;
|
|
8145
|
-
let chosen;
|
|
8146
8171
|
try {
|
|
8147
|
-
const library = await ensureLibrary(client,
|
|
8148
|
-
|
|
8149
|
-
synced = library === null ? chosen.roots : [library, ...chosen.roots];
|
|
8150
|
-
await client.remoteSync.updateScope({ collections: Object.fromEntries(synced.map((collection) => [collection.id, true])) });
|
|
8172
|
+
const library = await ensureLibrary(client, found);
|
|
8173
|
+
await client.remoteSync.updateScope({ collections: { [library.id]: true } });
|
|
8151
8174
|
} catch (error) {
|
|
8152
8175
|
spinner.fail("Could not choose what syncs to git");
|
|
8153
8176
|
throw explainRefusal(error);
|
|
8154
8177
|
}
|
|
8155
|
-
spinner.stop(
|
|
8156
|
-
if (chosen.created) ctx.ui.info(`Created the ${AGENT_COLLECTION_NAME} collection: dashboards, questions, models, and documents filed under it reach git on the next export`);
|
|
8157
|
-
if (chosen.besideNamesake) ctx.ui.info(`Another collection is also named ${AGENT_COLLECTION_NAME}; rde did not create it, so it stays out of git`);
|
|
8158
|
-
if (plan === null) return { outcome: "completed" };
|
|
8178
|
+
spinner.stop(`Synced to git: ${name}`);
|
|
8159
8179
|
const exportRun = {
|
|
8160
8180
|
ctx,
|
|
8161
8181
|
instance,
|
|
8162
8182
|
gitRemote,
|
|
8163
|
-
|
|
8183
|
+
name,
|
|
8164
8184
|
changes: plan.changes
|
|
8165
8185
|
};
|
|
8166
8186
|
if (plan.push) await exportScope(exportRun, resolved.runner);
|
|
@@ -8247,10 +8267,11 @@ async function pendingExportLines(state, secrets, runner) {
|
|
|
8247
8267
|
throw error;
|
|
8248
8268
|
}
|
|
8249
8269
|
}
|
|
8250
|
-
function skillLine(entries, detected) {
|
|
8270
|
+
function skillLine(entries, detected, skipped) {
|
|
8251
8271
|
const agents = [...new Set(entries.flatMap((entry) => coveredAgentNames(entry, detected)))];
|
|
8252
|
-
if (agents.length === 0) return
|
|
8253
|
-
return
|
|
8272
|
+
if (agents.length === 0) return `${RDE_SKILL_NAME} skill: not installed; \`rde init --only agents-skills\``;
|
|
8273
|
+
if (skipped) return `${RDE_SKILL_NAME} skill: skipped by this run; an earlier install covers ${agents.join(", ")}`;
|
|
8274
|
+
return `${RDE_SKILL_NAME} skill: installed for ${agents.join(", ")}`;
|
|
8254
8275
|
}
|
|
8255
8276
|
function launchLines(agents, workdir) {
|
|
8256
8277
|
const detected = agents.filter((agent) => agent.detected);
|
|
@@ -8270,7 +8291,7 @@ async function summaryLines(state, secrets, deps) {
|
|
|
8270
8291
|
const installed = await skillsCli(deps.runner).list();
|
|
8271
8292
|
const agents = await detectAgents(deps.probe);
|
|
8272
8293
|
const detected = agents.filter((agent) => agent.detected);
|
|
8273
|
-
lines.push(skillLine(installed.filter((entry) => entry.name ===
|
|
8294
|
+
lines.push(skillLine(installed.filter((entry) => entry.name === RDE_SKILL_NAME), detected, state.steps["agents-skills"]?.status === "skipped"));
|
|
8274
8295
|
lines.push(...launchLines(agents, state.gitRemote?.workdir ?? null));
|
|
8275
8296
|
lines.push("Lifecycle: rde status (--json for scripts), rde stop, rde start");
|
|
8276
8297
|
return lines;
|
|
@@ -8714,6 +8735,18 @@ function parseDatabase(spec) {
|
|
|
8714
8735
|
uri
|
|
8715
8736
|
};
|
|
8716
8737
|
}
|
|
8738
|
+
function databasesSection(flags) {
|
|
8739
|
+
if (flags.databaseName.length > flags.database.length) throw new ConfigError("--database-name names a --database; give one --database for each name");
|
|
8740
|
+
if (flags.database.length === 0) return;
|
|
8741
|
+
return flags.database.map((spec, index) => {
|
|
8742
|
+
const name = flags.databaseName[index];
|
|
8743
|
+
if (name !== void 0 && name.trim().length === 0) throw new ConfigError("--database-name needs a display name");
|
|
8744
|
+
return {
|
|
8745
|
+
...parseDatabase(spec),
|
|
8746
|
+
...name !== void 0 && { name }
|
|
8747
|
+
};
|
|
8748
|
+
});
|
|
8749
|
+
}
|
|
8717
8750
|
function adminSection(flags, secrets) {
|
|
8718
8751
|
const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
|
|
8719
8752
|
if (flags.admin === void 0 && !prompted) return;
|
|
@@ -8851,7 +8884,7 @@ async function answersFromFlags(flags, readStdin) {
|
|
|
8851
8884
|
runtime: runtimeSection(flags),
|
|
8852
8885
|
"admin-user": adminSection(flags, secrets),
|
|
8853
8886
|
license: licenseSection(flags, secrets),
|
|
8854
|
-
databases: flags
|
|
8887
|
+
databases: databasesSection(flags),
|
|
8855
8888
|
"git-sync": gitSection(flags, secrets),
|
|
8856
8889
|
"agents-skills": agentsSection(flags),
|
|
8857
8890
|
"api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
|
|
@@ -8908,6 +8941,11 @@ function givenFlags(flags) {
|
|
|
8908
8941
|
flag: "--database",
|
|
8909
8942
|
given: flags.database.length > 0
|
|
8910
8943
|
},
|
|
8944
|
+
{
|
|
8945
|
+
step: "databases",
|
|
8946
|
+
flag: "--database-name",
|
|
8947
|
+
given: flags.databaseName.length > 0
|
|
8948
|
+
},
|
|
8911
8949
|
{
|
|
8912
8950
|
step: "git-sync",
|
|
8913
8951
|
flag: "--git-dir",
|
|
@@ -9336,7 +9374,7 @@ const db = defineCommand({
|
|
|
9336
9374
|
//#endregion
|
|
9337
9375
|
//#region package.json
|
|
9338
9376
|
var name = "@metabase/rde";
|
|
9339
|
-
var version = "0.1
|
|
9377
|
+
var version = "0.5.1";
|
|
9340
9378
|
var description = "Prepares a local machine for data-engineering work with Metabase and coding agents";
|
|
9341
9379
|
//#endregion
|
|
9342
9380
|
//#region src/version.ts
|
|
@@ -10050,8 +10088,8 @@ async function gitSyncRow(state, instance, deps) {
|
|
|
10050
10088
|
const SKILL_STEP = "agents-skills";
|
|
10051
10089
|
const GLOBAL_SCOPE = "global";
|
|
10052
10090
|
const SKILL_FILE = "SKILL.md";
|
|
10053
|
-
const SOURCE_SKILLS_DIR = "skills";
|
|
10054
10091
|
const CLAUDE_CODE = "Claude Code";
|
|
10092
|
+
const COPIES_LABEL = `${RDE_SKILL_NAME} skill copies`;
|
|
10055
10093
|
const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
|
|
10056
10094
|
function skillRowId(agent) {
|
|
10057
10095
|
return `skill.${agent.id}`;
|
|
@@ -10063,12 +10101,12 @@ async function copyRef(entry, deps) {
|
|
|
10063
10101
|
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
10064
10102
|
return lockedSkill(path, await deps.skills.files.readJson(path), entry.name)?.ref ?? null;
|
|
10065
10103
|
}
|
|
10066
|
-
async function judgeCopy(entry, origin, deps) {
|
|
10104
|
+
async function judgeCopy(skill, entry, origin, deps) {
|
|
10067
10105
|
const where = `${entry.scope} ${entry.path}`;
|
|
10068
10106
|
switch (origin.kind) {
|
|
10069
10107
|
case "local": {
|
|
10070
10108
|
const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
|
|
10071
|
-
const wanted = await deps.skills.files.readText(join(origin.path,
|
|
10109
|
+
const wanted = await deps.skills.files.readText(join(origin.path, sourceSkillDirectory(skill), SKILL_FILE));
|
|
10072
10110
|
const current = installed !== null && installed === wanted;
|
|
10073
10111
|
return {
|
|
10074
10112
|
entry,
|
|
@@ -10109,80 +10147,80 @@ function loadedCopyLine(entries, cwd) {
|
|
|
10109
10147
|
if (loaded === void 0) return count;
|
|
10110
10148
|
return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
|
|
10111
10149
|
}
|
|
10112
|
-
function staleCopiesRow(stale, detail, deps) {
|
|
10150
|
+
function staleCopiesRow(stale, replaced, detail, deps) {
|
|
10113
10151
|
const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
|
|
10114
10152
|
const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
|
|
10115
10153
|
const repairs = [];
|
|
10116
|
-
|
|
10117
|
-
if (
|
|
10118
|
-
|
|
10154
|
+
const stepFixes = [];
|
|
10155
|
+
if (globalStale) stepFixes.push(`reinstalls the global copy from ${deps.skills.source}`);
|
|
10156
|
+
if (replaced) stepFixes.push(`removes the copies ${RDE_SKILL_NAME} replaces`);
|
|
10157
|
+
if (stepFixes.length > 0) repairs.push(`\`rde init --only ${SKILL_STEP}\` ${stepFixes.join(" and ")}`);
|
|
10158
|
+
if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove ${RDE_SKILL_NAME} -y\` in ${deps.skills.cwd}`);
|
|
10159
|
+
return failed("skill-copies", COPIES_LABEL, {
|
|
10119
10160
|
detail,
|
|
10120
10161
|
hint: `${repairs.join("; ")}; then restart the agent session`,
|
|
10121
|
-
step:
|
|
10162
|
+
step: stepFixes.length > 0 ? SKILL_STEP : null
|
|
10122
10163
|
});
|
|
10123
10164
|
}
|
|
10124
|
-
async function skillCopiesRow(globalEntries, deps) {
|
|
10165
|
+
async function skillCopiesRow(globalEntries, replaced, deps) {
|
|
10125
10166
|
const id = "skill-copies";
|
|
10126
|
-
const label =
|
|
10167
|
+
const label = COPIES_LABEL;
|
|
10127
10168
|
const cwd = deps.skills.cwd;
|
|
10128
|
-
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name ===
|
|
10169
|
+
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === RDE_SKILL_NAME);
|
|
10129
10170
|
const entries = [...globalEntries, ...projectEntries];
|
|
10130
|
-
if (entries.length === 0) return skipped(id, label, "no copy installed");
|
|
10171
|
+
if (entries.length === 0 && replaced.length === 0) return skipped(id, label, "no copy installed");
|
|
10131
10172
|
const origin = skillOrigin(deps.skills.source, cwd);
|
|
10132
10173
|
const judged = [];
|
|
10133
|
-
for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
|
|
10174
|
+
for (const entry of entries) judged.push(await judgeCopy(RDE_SKILL_NAME, entry, origin, deps));
|
|
10134
10175
|
const loaded = loadedCopyLine(entries, cwd);
|
|
10135
|
-
const lines = judged.map((copy) => copy.line);
|
|
10176
|
+
const lines = [...judged.map((copy) => copy.line), ...replaced.map((entry) => `${entry.scope} ${entry.path}: replaced by ${RDE_SKILL_NAME}`)];
|
|
10136
10177
|
const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
|
|
10137
10178
|
const stale = judged.filter((copy) => !copy.current);
|
|
10138
|
-
|
|
10179
|
+
if (stale.length === 0 && replaced.length === 0) return ok(id, label, detail);
|
|
10180
|
+
return staleCopiesRow(stale, replaced.length > 0, detail, deps);
|
|
10139
10181
|
}
|
|
10140
|
-
function
|
|
10141
|
-
const
|
|
10142
|
-
|
|
10182
|
+
function dataAppSkillsFix({ current, legacy }) {
|
|
10183
|
+
const command = `\`rde init --only ${SKILL_STEP}\``;
|
|
10184
|
+
const removal = `the copies ${DATA_APPS_SKILL_NAME} replaces`;
|
|
10185
|
+
if (!legacy) return `${command} installs it`;
|
|
10186
|
+
return current ? `${command} removes ${removal}` : `${command} installs it and removes ${removal}`;
|
|
10143
10187
|
}
|
|
10144
|
-
async function dataAppSkillsRow(
|
|
10188
|
+
async function dataAppSkillsRow(installed, deps) {
|
|
10145
10189
|
const id = "data-app-skills";
|
|
10146
10190
|
const label = "data-app skills";
|
|
10147
|
-
const
|
|
10148
|
-
const
|
|
10149
|
-
|
|
10150
|
-
|
|
10151
|
-
|
|
10152
|
-
|
|
10153
|
-
|
|
10154
|
-
}];
|
|
10155
|
-
const lines = [dataAppBranchLine(state.version, branch)];
|
|
10156
|
-
let current = true;
|
|
10157
|
-
for (const skill of wanted) {
|
|
10158
|
-
const entry = installed.find((candidate) => candidate.name === skill.name);
|
|
10159
|
-
if (entry === void 0) {
|
|
10160
|
-
lines.push(`${skill.name}: not installed`);
|
|
10161
|
-
current = false;
|
|
10162
|
-
continue;
|
|
10163
|
-
}
|
|
10164
|
-
const judged = await judgeCopy(entry, skill.origin, deps);
|
|
10191
|
+
const origin = skillOrigin(deps.skills.source, deps.skills.cwd);
|
|
10192
|
+
const lines = [];
|
|
10193
|
+
const copy = installed.find((candidate) => candidate.name === DATA_APPS_SKILL_NAME);
|
|
10194
|
+
let current = false;
|
|
10195
|
+
if (copy === void 0) lines.push(`${DATA_APPS_SKILL_NAME}: not installed`);
|
|
10196
|
+
else {
|
|
10197
|
+
const judged = await judgeCopy(DATA_APPS_SKILL_NAME, copy, origin, deps);
|
|
10165
10198
|
lines.push(judged.line);
|
|
10166
|
-
current
|
|
10199
|
+
current = judged.current;
|
|
10167
10200
|
}
|
|
10201
|
+
const legacy = installed.filter((entry) => isLegacyDataAppCopy(entry.name, entry.source));
|
|
10202
|
+
for (const entry of legacy) lines.push(`${entry.scope} ${entry.path}: replaced by ${DATA_APPS_SKILL_NAME}`);
|
|
10168
10203
|
const detail = lines.join("\n");
|
|
10169
|
-
if (current) return ok(id, label, detail);
|
|
10204
|
+
if (current && legacy.length === 0) return ok(id, label, detail);
|
|
10170
10205
|
return failed(id, label, {
|
|
10171
10206
|
detail,
|
|
10172
|
-
hint:
|
|
10207
|
+
hint: `${dataAppSkillsFix({
|
|
10208
|
+
current,
|
|
10209
|
+
legacy: legacy.length > 0
|
|
10210
|
+
})}; then restart the agent session`,
|
|
10173
10211
|
step: SKILL_STEP
|
|
10174
10212
|
});
|
|
10175
10213
|
}
|
|
10176
10214
|
async function skillRows(state, deps) {
|
|
10177
|
-
const label =
|
|
10215
|
+
const label = `${RDE_SKILL_NAME} skill`;
|
|
10178
10216
|
if (state === null) return [skipped("skill", label, NO_INSTANCE_REASON)];
|
|
10179
10217
|
if (state.steps[SKILL_STEP]?.status === "skipped") return [skipped("skill", label, `skipped during init; \`rde init --only ${SKILL_STEP}\``)];
|
|
10180
10218
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
10181
10219
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
10182
10220
|
const installed = await skillsCli(deps.runner).list();
|
|
10183
|
-
const entries = installed.filter((entry) => entry.name ===
|
|
10184
|
-
const copies = await skillCopiesRow(entries, deps);
|
|
10185
|
-
const dataApps = await dataAppSkillsRow(
|
|
10221
|
+
const entries = installed.filter((entry) => entry.name === RDE_SKILL_NAME);
|
|
10222
|
+
const copies = await skillCopiesRow(entries, installed.filter((entry) => isReplacedRdeCopy(entry.name)), deps);
|
|
10223
|
+
const dataApps = await dataAppSkillsRow(installed, deps);
|
|
10186
10224
|
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [
|
|
10187
10225
|
failed("skill", label, {
|
|
10188
10226
|
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
@@ -10642,6 +10680,10 @@ const init = defineRdeCommand({
|
|
|
10642
10680
|
type: "string",
|
|
10643
10681
|
description: "Database to connect, a connection string or <engine>=<connection string> when it fits several engines; repeatable. init needs one, and asks for it only at a terminal"
|
|
10644
10682
|
},
|
|
10683
|
+
"database-name": {
|
|
10684
|
+
type: "string",
|
|
10685
|
+
description: "Display name of the --database in the same position, such as the name the production Metabase uses; repeatable"
|
|
10686
|
+
},
|
|
10645
10687
|
"git-dir": {
|
|
10646
10688
|
type: "string",
|
|
10647
10689
|
description: "Directory for the content repository"
|
|
@@ -10699,6 +10741,7 @@ const init = defineRdeCommand({
|
|
|
10699
10741
|
licenseToken: args["license-token"],
|
|
10700
10742
|
skipLicense: args["skip-license"],
|
|
10701
10743
|
database: repeatedValues(rawArgs, "--database"),
|
|
10744
|
+
databaseName: repeatedValues(rawArgs, "--database-name"),
|
|
10702
10745
|
gitDir: args["git-dir"],
|
|
10703
10746
|
gitRemote: args["git-remote"],
|
|
10704
10747
|
gitUrl: args["git-url"],
|
|
@@ -11264,17 +11307,22 @@ async function findMbProfiles(candidates, urls, runner) {
|
|
|
11264
11307
|
}
|
|
11265
11308
|
return found;
|
|
11266
11309
|
}
|
|
11310
|
+
function spokenList(items) {
|
|
11311
|
+
const last = items.at(-1);
|
|
11312
|
+
if (last === void 0 || items.length <= 2) return items.join(" and ");
|
|
11313
|
+
return `${items.slice(0, -1).join(", ")}, and ${last}`;
|
|
11314
|
+
}
|
|
11267
11315
|
function skillLabel(names, entries) {
|
|
11268
11316
|
const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
|
|
11269
11317
|
const noun = names.length === 1 ? "skill" : "skills";
|
|
11270
|
-
return `${names
|
|
11318
|
+
return `${spokenList(names)} ${noun} for ${agents.join(", ")}`;
|
|
11271
11319
|
}
|
|
11320
|
+
const OWNED_SKILL_NAMES = RDE_OWNED_SKILL_NAMES;
|
|
11272
11321
|
async function findSkill(runner, childOutput) {
|
|
11273
11322
|
const skills = skillsCli(runner);
|
|
11274
|
-
const
|
|
11275
|
-
const entries = (await skills.list()).filter((entry) => owned.includes(entry.name));
|
|
11323
|
+
const entries = (await skills.list()).filter((entry) => OWNED_SKILL_NAMES.includes(entry.name));
|
|
11276
11324
|
if (entries.length === 0) return null;
|
|
11277
|
-
const names =
|
|
11325
|
+
const names = OWNED_SKILL_NAMES.filter((name) => entries.some((entry) => entry.name === name));
|
|
11278
11326
|
return {
|
|
11279
11327
|
label: skillLabel(names, entries),
|
|
11280
11328
|
remove: () => skills.remove(names, childOutput)
|
|
@@ -11376,7 +11424,7 @@ function keptOnDisk(options, state, dirPresent) {
|
|
|
11376
11424
|
const kept = [];
|
|
11377
11425
|
if (options.keepData && dirPresent) kept.push(`Instance directory ${instanceDir(options.instance)} (--keep-data); \`rde init\` reinstalls from it`);
|
|
11378
11426
|
if (state?.gitRemote !== null && state?.gitRemote !== void 0) kept.push(`Working directory ${state.gitRemote.workdir} and its git history`);
|
|
11379
|
-
if (!options.removeSkill) kept.push(`The ${
|
|
11427
|
+
if (!options.removeSkill) kept.push(`The ${spokenList(RDE_INSTALLED_SKILL_NAMES)} skills for your agents; pass --remove-skill to remove them`);
|
|
11380
11428
|
return kept;
|
|
11381
11429
|
}
|
|
11382
11430
|
async function planUninstall(options, deps) {
|
|
@@ -11535,7 +11583,7 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11535
11583
|
},
|
|
11536
11584
|
"remove-skill": {
|
|
11537
11585
|
type: "boolean",
|
|
11538
|
-
description: "Also remove the rde and rde-data-apps skills from your coding agents",
|
|
11586
|
+
description: "Also remove the metabase-data-apps and metabase-rde skills, and the rde and rde-data-apps skills they replace, from your coding agents",
|
|
11539
11587
|
default: false
|
|
11540
11588
|
},
|
|
11541
11589
|
"remove-image": {
|
|
@@ -11857,7 +11905,7 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11857
11905
|
const backup = join(instanceBackupsDir(ctx.state.name), backupStamp(resolved.now()));
|
|
11858
11906
|
ctx.ui.note(planLines(configured, current, target, backup).join("\n"), "rde upgrade");
|
|
11859
11907
|
const announced = majorMove(current, target.releaseTag);
|
|
11860
|
-
if (announced !== null) ctx.ui.warn(`This moves Metabase from ${announced.from} to ${announced.to}, a new major version;
|
|
11908
|
+
if (announced !== null) ctx.ui.warn(`This moves Metabase from ${announced.from} to ${announced.to}, a new major version; rde updates your coding agents' skills after the upgrade`);
|
|
11861
11909
|
if (!options.yes) {
|
|
11862
11910
|
if (!await ctx.prompts.confirm({
|
|
11863
11911
|
message: "Upgrade now?",
|
|
@@ -11880,7 +11928,7 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11880
11928
|
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
11881
11929
|
const moved = majorMove(current, to);
|
|
11882
11930
|
if (moved !== null) {
|
|
11883
|
-
if (announced === null) ctx.ui.warn(`This moved Metabase from ${moved.from} to ${moved.to}, a new major version
|
|
11931
|
+
if (announced === null) ctx.ui.warn(`This moved Metabase from ${moved.from} to ${moved.to}, a new major version`);
|
|
11884
11932
|
await runWizard([resolved.skills(options.yes ? DETECT_AGENTS : ASK_AGENTS)], ctx);
|
|
11885
11933
|
}
|
|
11886
11934
|
return {
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metabase/rde",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.5.1",
|
|
4
4
|
"description": "Prepares a local machine for data-engineering work with Metabase and coding agents",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"analytics",
|
|
7
|
+
"claude-code",
|
|
8
|
+
"cli",
|
|
9
|
+
"coding-agents",
|
|
10
|
+
"data-engineering",
|
|
11
|
+
"metabase"
|
|
12
|
+
],
|
|
5
13
|
"license": "AGPL-3.0",
|
|
14
|
+
"author": "Metabase",
|
|
6
15
|
"bin": {
|
|
7
16
|
"rde": "dist/cli.mjs"
|
|
8
17
|
},
|
|
@@ -31,7 +40,8 @@
|
|
|
31
40
|
"test:e2e": "vitest run --project e2e --reporter=verbose tests/e2e/init.e2e.test.ts tests/e2e/mb-profiles.e2e.test.ts",
|
|
32
41
|
"test:e2e:databases": "vitest run --project e2e --reporter=verbose tests/e2e/databases.e2e.test.ts",
|
|
33
42
|
"check": "bun run typecheck && bun run lint && bun run format:check && bun run test",
|
|
34
|
-
"check:fix": "bun run lint:fix && bun run format"
|
|
43
|
+
"check:fix": "bun run lint:fix && bun run format",
|
|
44
|
+
"prepublishOnly": "bun run build"
|
|
35
45
|
},
|
|
36
46
|
"dependencies": {
|
|
37
47
|
"@clack/prompts": "^0.8.2",
|