@metabase/rde 0.1.18 → 0.5.0
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 +15 -14
- package/dist/cli.mjs +295 -248
- 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 │
|
|
@@ -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",
|
|
@@ -5722,6 +5736,14 @@ function gitTool(runner) {
|
|
|
5722
5736
|
"HEAD"
|
|
5723
5737
|
], { cwd: dir })).exitCode === 0;
|
|
5724
5738
|
}
|
|
5739
|
+
async function gitPath(dir, name) {
|
|
5740
|
+
const result = await git([
|
|
5741
|
+
"rev-parse",
|
|
5742
|
+
"--git-path",
|
|
5743
|
+
name
|
|
5744
|
+
], { cwd: dir });
|
|
5745
|
+
return resolve(dir, result.stdout.trim());
|
|
5746
|
+
}
|
|
5725
5747
|
async function hasIdentity(dir) {
|
|
5726
5748
|
const result = await runner.run(GIT, [
|
|
5727
5749
|
"config",
|
|
@@ -5839,6 +5861,7 @@ function gitTool(runner) {
|
|
|
5839
5861
|
initBare,
|
|
5840
5862
|
init,
|
|
5841
5863
|
hasCommits,
|
|
5864
|
+
gitPath,
|
|
5842
5865
|
commitPaths,
|
|
5843
5866
|
addRemote,
|
|
5844
5867
|
clone,
|
|
@@ -6056,6 +6079,7 @@ const README_NAME = "README.md";
|
|
|
6056
6079
|
const README_CONTENT = "# Metabase content\n\nMetabase content synced by rde.\n";
|
|
6057
6080
|
const INITIAL_COMMIT_MESSAGE = "Initial commit";
|
|
6058
6081
|
const GITIGNORE_NAME = ".gitignore";
|
|
6082
|
+
const EXCLUDE_GIT_PATH = "info/exclude";
|
|
6059
6083
|
const IGNORED_PATHS = [
|
|
6060
6084
|
".env",
|
|
6061
6085
|
".env.local",
|
|
@@ -6076,10 +6100,7 @@ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
|
6076
6100
|
"timed-out",
|
|
6077
6101
|
"conflict"
|
|
6078
6102
|
]);
|
|
6079
|
-
const SYNC_MODE_SETTINGS = {
|
|
6080
|
-
"remote-sync-type": "read-write",
|
|
6081
|
-
"remote-sync-transforms": true
|
|
6082
|
-
};
|
|
6103
|
+
const SYNC_MODE_SETTINGS = { "remote-sync-type": "read-write" };
|
|
6083
6104
|
function hostDeps$11(ctx) {
|
|
6084
6105
|
return {
|
|
6085
6106
|
runner: processRunner(ctx.signal),
|
|
@@ -6318,20 +6339,21 @@ function hostedScreen(run) {
|
|
|
6318
6339
|
}
|
|
6319
6340
|
};
|
|
6320
6341
|
}
|
|
6321
|
-
async function ignoreDataAppFiles(
|
|
6322
|
-
const path = join(workdir, GITIGNORE_NAME);
|
|
6342
|
+
async function ignoreDataAppFiles(path) {
|
|
6323
6343
|
const existing = await readTextFile(path);
|
|
6324
6344
|
const present = new Set(existing === null ? [] : existing.split("\n").map((line) => line.trim()));
|
|
6325
6345
|
const missing = IGNORED_PATHS.filter((entry) => !present.has(entry));
|
|
6326
6346
|
if (missing.length === 0) return;
|
|
6327
6347
|
const separator = existing === null || existing.endsWith("\n") ? "" : "\n";
|
|
6348
|
+
await promises.mkdir(dirname(path), { recursive: true });
|
|
6328
6349
|
await promises.appendFile(path, `${separator}${missing.join("\n")}\n`);
|
|
6329
6350
|
}
|
|
6330
6351
|
async function prepareWorkdir(run, workdir) {
|
|
6331
6352
|
if (!await run.git.isRepo(workdir)) await run.git.init(workdir, { branch: DEFAULT_BRANCH });
|
|
6332
|
-
if (
|
|
6353
|
+
if (await run.git.hasCommits(workdir)) await ignoreDataAppFiles(await run.git.gitPath(workdir, EXCLUDE_GIT_PATH));
|
|
6354
|
+
else {
|
|
6333
6355
|
await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
|
|
6334
|
-
await ignoreDataAppFiles(workdir);
|
|
6356
|
+
await ignoreDataAppFiles(join(workdir, GITIGNORE_NAME));
|
|
6335
6357
|
await run.git.commitPaths(workdir, [README_NAME, GITIGNORE_NAME], INITIAL_COMMIT_MESSAGE);
|
|
6336
6358
|
}
|
|
6337
6359
|
return {
|
|
@@ -6385,12 +6407,9 @@ async function cloneHosted(run, workdir, credentials) {
|
|
|
6385
6407
|
throw error;
|
|
6386
6408
|
}
|
|
6387
6409
|
spinner.stop(`Cloned into ${workdir}`);
|
|
6388
|
-
|
|
6389
|
-
workdir,
|
|
6390
|
-
branch: await run.git.currentBranch(workdir)
|
|
6391
|
-
};
|
|
6410
|
+
const hadCommits = await run.git.hasCommits(workdir);
|
|
6392
6411
|
const prepared = await prepareWorkdir(run, workdir);
|
|
6393
|
-
await run.git.push(workdir, ORIGIN, prepared.branch, credentials);
|
|
6412
|
+
if (!hadCommits) await run.git.push(workdir, ORIGIN, prepared.branch, credentials);
|
|
6394
6413
|
return prepared;
|
|
6395
6414
|
}
|
|
6396
6415
|
async function adoptRepository(run, workdir, url) {
|
|
@@ -6469,7 +6488,8 @@ function unfinishedImport(outcome) {
|
|
|
6469
6488
|
}
|
|
6470
6489
|
async function importExistingContent(run, gitRemote) {
|
|
6471
6490
|
if (!await remoteHasContent(run, gitRemote)) {
|
|
6472
|
-
run.
|
|
6491
|
+
await run.client.remoteSync.syncTransforms();
|
|
6492
|
+
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
6493
|
return;
|
|
6474
6494
|
}
|
|
6475
6495
|
const spinner = run.ctx.ui.spinner("Importing the repository's content into Metabase…");
|
|
@@ -6607,16 +6627,18 @@ function githubContents(signal, options = {}) {
|
|
|
6607
6627
|
//#region src/tools/skills-cli.ts
|
|
6608
6628
|
const NPX = "npx";
|
|
6609
6629
|
const SKILLS_PACKAGE = "skills@latest";
|
|
6610
|
-
const
|
|
6611
|
-
const
|
|
6612
|
-
const
|
|
6613
|
-
const
|
|
6614
|
-
|
|
6615
|
-
|
|
6616
|
-
|
|
6617
|
-
|
|
6618
|
-
|
|
6619
|
-
]
|
|
6630
|
+
const LIST_INTERNAL_SKILLS = { INSTALL_INTERNAL_SKILLS: "1" };
|
|
6631
|
+
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
|
|
6632
|
+
const RDE_SKILL_NAME = "metabase-rde";
|
|
6633
|
+
const DATA_APPS_SKILL_NAME = "metabase-data-apps";
|
|
6634
|
+
const RDE_INSTALLED_SKILL_NAMES = [DATA_APPS_SKILL_NAME, RDE_SKILL_NAME];
|
|
6635
|
+
const REPLACED_RDE_SKILL_NAMES = ["rde", "rde-data-apps"];
|
|
6636
|
+
const RDE_OWNED_SKILL_NAMES = [...RDE_INSTALLED_SKILL_NAMES, ...REPLACED_RDE_SKILL_NAMES];
|
|
6637
|
+
const SKILL_VERSIONS = {
|
|
6638
|
+
[DATA_APPS_SKILL_NAME]: 1,
|
|
6639
|
+
[RDE_SKILL_NAME]: 1
|
|
6640
|
+
};
|
|
6641
|
+
const SOURCE_SKILLS_DIR = "skills";
|
|
6620
6642
|
function rdeSkillSource() {
|
|
6621
6643
|
return readSetEnv("RDE_SKILL_SOURCE") ?? "https://github.com/metabase/agent-skills/tree/main";
|
|
6622
6644
|
}
|
|
@@ -6656,16 +6678,6 @@ const hostSkillFiles = {
|
|
|
6656
6678
|
readJson: readJsonFile,
|
|
6657
6679
|
readText: readTextFile
|
|
6658
6680
|
};
|
|
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
6681
|
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
6670
6682
|
function skillOrigin(source, cwd) {
|
|
6671
6683
|
const github = GITHUB_URL.exec(source);
|
|
@@ -6686,6 +6698,23 @@ function skillOrigin(source, cwd) {
|
|
|
6686
6698
|
source
|
|
6687
6699
|
};
|
|
6688
6700
|
}
|
|
6701
|
+
function sourceSkillDirectory(skill) {
|
|
6702
|
+
return posix.join(SOURCE_SKILLS_DIR, skill, `v${SKILL_VERSIONS[skill]}`);
|
|
6703
|
+
}
|
|
6704
|
+
function skillSource(rdeSource, skill, cwd) {
|
|
6705
|
+
const directory = sourceSkillDirectory(skill);
|
|
6706
|
+
const origin = skillOrigin(rdeSource, cwd);
|
|
6707
|
+
switch (origin.kind) {
|
|
6708
|
+
case "github":
|
|
6709
|
+
if (origin.ref === null) throw new ConfigError(`RDE_SKILL_SOURCE must name a branch: https://github.com/${origin.repository}/tree/<branch>`);
|
|
6710
|
+
return `https://github.com/${origin.repository}/tree/${origin.ref}/${directory}`;
|
|
6711
|
+
case "local": return join(origin.path, directory);
|
|
6712
|
+
case "other": {
|
|
6713
|
+
const [path, ref] = origin.source.split("#", 2);
|
|
6714
|
+
return ref === void 0 ? `${path}/${directory}` : `${path}/${directory}#${ref}`;
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6689
6718
|
function skillsCli(runner) {
|
|
6690
6719
|
async function add(options) {
|
|
6691
6720
|
const argv = [
|
|
@@ -6719,7 +6748,10 @@ function skillsCli(runner) {
|
|
|
6719
6748
|
"-g",
|
|
6720
6749
|
"--json"
|
|
6721
6750
|
];
|
|
6722
|
-
const result = await runner.run(NPX, argv, {
|
|
6751
|
+
const result = await runner.run(NPX, argv, {
|
|
6752
|
+
stdoutToFile: true,
|
|
6753
|
+
addedEnv: LIST_INTERNAL_SKILLS
|
|
6754
|
+
});
|
|
6723
6755
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
6724
6756
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
6725
6757
|
}
|
|
@@ -6732,7 +6764,8 @@ function skillsCli(runner) {
|
|
|
6732
6764
|
];
|
|
6733
6765
|
const result = await runner.run(NPX, argv, {
|
|
6734
6766
|
cwd,
|
|
6735
|
-
stdoutToFile: true
|
|
6767
|
+
stdoutToFile: true,
|
|
6768
|
+
addedEnv: LIST_INTERNAL_SKILLS
|
|
6736
6769
|
});
|
|
6737
6770
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
6738
6771
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
@@ -6749,7 +6782,23 @@ function skillsCli(runner) {
|
|
|
6749
6782
|
const STEP_ID$5 = "agents-skills";
|
|
6750
6783
|
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
|
|
6751
6784
|
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 =
|
|
6785
|
+
const REQUIRED_SKILL_NAMES = RDE_INSTALLED_SKILL_NAMES;
|
|
6786
|
+
const LEGACY_DATA_APP_REPOSITORY = "metabase/metabase";
|
|
6787
|
+
const LEGACY_DATA_APP_SKILL_NAMES = [
|
|
6788
|
+
"metabase-data-app-setup",
|
|
6789
|
+
"metabase-data-app-routing",
|
|
6790
|
+
"metabase-data-app-actions",
|
|
6791
|
+
"metabase-data-app-semantic-layer",
|
|
6792
|
+
"metabase-data-app-migrate"
|
|
6793
|
+
];
|
|
6794
|
+
const REPLACED_RDE_NAMES = REPLACED_RDE_SKILL_NAMES;
|
|
6795
|
+
const REPLACEMENTS = [{
|
|
6796
|
+
by: DATA_APPS_SKILL_NAME,
|
|
6797
|
+
names: LEGACY_DATA_APP_SKILL_NAMES
|
|
6798
|
+
}, {
|
|
6799
|
+
by: RDE_SKILL_NAME,
|
|
6800
|
+
names: REPLACED_RDE_NAMES
|
|
6801
|
+
}];
|
|
6753
6802
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6754
6803
|
const ASK_AGENTS = { kind: "ask" };
|
|
6755
6804
|
function hostDeps$10(ctx) {
|
|
@@ -6792,34 +6841,17 @@ function coveredAgentNames(entry, agents) {
|
|
|
6792
6841
|
function installedLines(entries, agents) {
|
|
6793
6842
|
return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
|
|
6794
6843
|
}
|
|
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
|
-
}];
|
|
6844
|
+
function skillInstalls(deps) {
|
|
6845
|
+
const defaultSource = deps.skillSource === DEFAULT_RDE_SKILL_SOURCE;
|
|
6846
|
+
return RDE_INSTALLED_SKILL_NAMES.map((skill) => {
|
|
6847
|
+
const source = skillSource(deps.skillSource, skill, deps.cwd);
|
|
6848
|
+
return {
|
|
6849
|
+
skill,
|
|
6850
|
+
source,
|
|
6851
|
+
sourceNote: ` from ${defaultSource ? sourceSkillDirectory(skill) : source}`,
|
|
6852
|
+
origin: skillOrigin(source, deps.cwd)
|
|
6853
|
+
};
|
|
6854
|
+
});
|
|
6823
6855
|
}
|
|
6824
6856
|
function hashLookup(github) {
|
|
6825
6857
|
const asked = /* @__PURE__ */ new Map();
|
|
@@ -6844,7 +6876,8 @@ async function readLockedSkills(deps) {
|
|
|
6844
6876
|
const lockPath = globalSkillLockPath(deps.probe.home, deps.stateHome);
|
|
6845
6877
|
try {
|
|
6846
6878
|
const lock = await deps.files.readJson(lockPath);
|
|
6847
|
-
|
|
6879
|
+
const names = [...REQUIRED_SKILL_NAMES, ...LEGACY_DATA_APP_SKILL_NAMES];
|
|
6880
|
+
return new Map(names.map((name) => [name, lockedSkill(lockPath, lock, name)]));
|
|
6848
6881
|
} catch (error) {
|
|
6849
6882
|
if (error instanceof ConfigError) return null;
|
|
6850
6883
|
throw error;
|
|
@@ -6864,25 +6897,54 @@ function lockedCopy(locked, origin) {
|
|
|
6864
6897
|
hash: locked.skillFolderHash
|
|
6865
6898
|
};
|
|
6866
6899
|
}
|
|
6867
|
-
async function
|
|
6868
|
-
const { origin } =
|
|
6900
|
+
async function installBehind(install, check) {
|
|
6901
|
+
const { origin } = install;
|
|
6869
6902
|
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
|
-
}
|
|
6903
|
+
const copy = lockedCopy(check.locked.get(install.skill) ?? null, origin);
|
|
6904
|
+
if (copy === null) return true;
|
|
6876
6905
|
if (check.updates === "installed") return false;
|
|
6877
|
-
|
|
6878
|
-
|
|
6879
|
-
|
|
6906
|
+
const current = await check.hashes(origin.repository, copy.ref, copy.directory);
|
|
6907
|
+
return current !== null && current.get(copy.folder) !== copy.hash;
|
|
6908
|
+
}
|
|
6909
|
+
function isLegacyDataAppCopy(name, source) {
|
|
6910
|
+
return LEGACY_DATA_APP_SKILL_NAMES.includes(name) && source === LEGACY_DATA_APP_REPOSITORY;
|
|
6911
|
+
}
|
|
6912
|
+
function isReplacedRdeCopy(name) {
|
|
6913
|
+
return REPLACED_RDE_NAMES.includes(name);
|
|
6914
|
+
}
|
|
6915
|
+
function replacedCopies(installed, locked) {
|
|
6916
|
+
const found = /* @__PURE__ */ new Map();
|
|
6917
|
+
for (const { by, names } of REPLACEMENTS) {
|
|
6918
|
+
const present = names.filter((name) => by === "metabase-data-apps" ? isLegacyDataAppCopy(name, locked?.get(name)?.source) : installed.some((entry) => entry.name === name));
|
|
6919
|
+
if (present.length > 0) found.set(by, present);
|
|
6880
6920
|
}
|
|
6881
|
-
return
|
|
6921
|
+
return found;
|
|
6882
6922
|
}
|
|
6883
6923
|
function hasSkill(installed, skill, agent) {
|
|
6884
6924
|
return installed.some((entry) => entry.name === skill && skillCoversAgent(entry, agent));
|
|
6885
6925
|
}
|
|
6926
|
+
function splitReplaced(by, replaced, installed, all) {
|
|
6927
|
+
const split = {
|
|
6928
|
+
removable: [],
|
|
6929
|
+
kept: [],
|
|
6930
|
+
uncovered: []
|
|
6931
|
+
};
|
|
6932
|
+
for (const name of replaced) {
|
|
6933
|
+
const copies = installed.filter((entry) => entry.name === name);
|
|
6934
|
+
const uncovered = all.filter((agent) => copies.some((entry) => skillCoversAgent(entry, agent)) && !hasSkill(installed, by, agent));
|
|
6935
|
+
if (uncovered.length === 0) {
|
|
6936
|
+
split.removable.push(name);
|
|
6937
|
+
continue;
|
|
6938
|
+
}
|
|
6939
|
+
split.kept.push(name);
|
|
6940
|
+
for (const agent of uncovered) if (!split.uncovered.includes(agent)) split.uncovered.push(agent);
|
|
6941
|
+
}
|
|
6942
|
+
return split;
|
|
6943
|
+
}
|
|
6944
|
+
function keptReplacedLine(by, split) {
|
|
6945
|
+
const ids = split.uncovered.map((agent) => agent.id).join(",");
|
|
6946
|
+
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`;
|
|
6947
|
+
}
|
|
6886
6948
|
function coversAll(installed, skills, agents) {
|
|
6887
6949
|
return agents.every((agent) => skills.every((skill) => hasSkill(installed, skill, agent)));
|
|
6888
6950
|
}
|
|
@@ -6948,7 +7010,7 @@ function skipLine(all, source) {
|
|
|
6948
7010
|
}
|
|
6949
7011
|
function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
6950
7012
|
let hashes = null;
|
|
6951
|
-
async function
|
|
7013
|
+
async function installCheck(resolved) {
|
|
6952
7014
|
hashes ??= hashLookup(resolved.github);
|
|
6953
7015
|
return {
|
|
6954
7016
|
updates,
|
|
@@ -6964,9 +7026,11 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
|
6964
7026
|
const resolved = deps(ctx);
|
|
6965
7027
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6966
7028
|
if (detected.length === 0) return ctx.state.steps[STEP_ID$5]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6967
|
-
|
|
6968
|
-
|
|
6969
|
-
|
|
7029
|
+
const installed = await skillsCli(resolved.runner).list();
|
|
7030
|
+
if (missingSkills(installed, detected).length > 0) return "todo";
|
|
7031
|
+
const check = await installCheck(resolved);
|
|
7032
|
+
if (replacedCopies(installed, check.locked).size > 0) return "outdated";
|
|
7033
|
+
for (const install of skillInstalls(resolved)) if (await installBehind(install, check)) return "outdated";
|
|
6970
7034
|
return "done";
|
|
6971
7035
|
},
|
|
6972
7036
|
async run(ctx) {
|
|
@@ -6981,28 +7045,37 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
|
6981
7045
|
return { outcome: "skipped" };
|
|
6982
7046
|
}
|
|
6983
7047
|
const before = await installedNow();
|
|
6984
|
-
const check = await
|
|
7048
|
+
const check = await installCheck(resolved);
|
|
6985
7049
|
const pending = [];
|
|
6986
|
-
for (const
|
|
7050
|
+
for (const install of skillInstalls(resolved)) if (!coversAll(before, [install.skill], selection.agents) || await installBehind(install, check)) pending.push(install);
|
|
7051
|
+
const replaced = replacedCopies(before, check.locked);
|
|
6987
7052
|
const names = displayNames(selection.agents);
|
|
6988
|
-
if (pending.length === 0) {
|
|
7053
|
+
if (pending.length === 0 && replaced.size === 0) {
|
|
6989
7054
|
ctx.ui.success(`The rde and data-app skills are current for ${names}`);
|
|
6990
7055
|
return { outcome: "completed" };
|
|
6991
7056
|
}
|
|
6992
7057
|
const agents = selection.agents.map((agent) => agent.skillsCliName);
|
|
6993
7058
|
const forWhom = `for ${names} (${SOURCE_NOTES[selection.source]})`;
|
|
6994
|
-
for (const
|
|
6995
|
-
ctx.ui.info(`Installing
|
|
7059
|
+
for (const install of pending) {
|
|
7060
|
+
ctx.ui.info(`Installing ${install.skill} ${forWhom}${install.sourceNote}`);
|
|
6996
7061
|
await skills.add({
|
|
6997
|
-
source:
|
|
6998
|
-
skills:
|
|
7062
|
+
source: install.source,
|
|
7063
|
+
skills: [install.skill],
|
|
6999
7064
|
agents,
|
|
7000
7065
|
output: ctx.ui.childOutput
|
|
7001
7066
|
});
|
|
7002
7067
|
}
|
|
7003
|
-
const installed = await skills.list();
|
|
7068
|
+
const installed = pending.length === 0 ? before : await skills.list();
|
|
7004
7069
|
const missing = unlistedSkills(installed);
|
|
7005
7070
|
if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
|
|
7071
|
+
for (const [by, old] of replaced) {
|
|
7072
|
+
const split = splitReplaced(by, old, installed, all);
|
|
7073
|
+
if (split.removable.length > 0) {
|
|
7074
|
+
ctx.ui.info(`Removing ${split.removable.join(", ")}, which ${by} replaces`);
|
|
7075
|
+
await skills.remove(split.removable, ctx.ui.childOutput);
|
|
7076
|
+
}
|
|
7077
|
+
if (split.kept.length > 0) ctx.ui.warn(keptReplacedLine(by, split));
|
|
7078
|
+
}
|
|
7006
7079
|
ctx.ui.note(installedLines(requiredEntries(installed), selection.agents), "Skills installed");
|
|
7007
7080
|
ctx.ui.info(MECHANICS_NOTE);
|
|
7008
7081
|
return { outcome: "completed" };
|
|
@@ -7190,7 +7263,7 @@ function npmRegistry(signal, options = {}) {
|
|
|
7190
7263
|
//#endregion
|
|
7191
7264
|
//#region src/steps/install-mb.ts
|
|
7192
7265
|
const STEP_ID$4 = "install-mb";
|
|
7193
|
-
const MIN_MB_VERSION = "0.
|
|
7266
|
+
const MIN_MB_VERSION = "0.5.1";
|
|
7194
7267
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
7195
7268
|
function installedWith(scriptPath) {
|
|
7196
7269
|
return scriptPath === void 0 ? "npm" : packageManagerFor(scriptPath);
|
|
@@ -7918,10 +7991,8 @@ function startMetabaseStep(deps = hostDeps$5, accepted = "supported") {
|
|
|
7918
7991
|
//#region src/steps/sync-scope.ts
|
|
7919
7992
|
const STEP_ID$2 = "sync-scope";
|
|
7920
7993
|
const SYNC_SCOPE_KEYS = { export: `${STEP_ID$2}.export` };
|
|
7921
|
-
const AGENT_COLLECTION_NAME = "Analytics";
|
|
7922
7994
|
const LIBRARY_NAME = "Library";
|
|
7923
7995
|
const ROOT_COLLECTION_NAME = "Our analytics";
|
|
7924
|
-
const AGENT_COLLECTION_DESCRIPTION = "Dashboards, questions, models, and documents that reach git on export.";
|
|
7925
7996
|
function hostDeps$4(ctx) {
|
|
7926
7997
|
return {
|
|
7927
7998
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -7932,22 +8003,11 @@ function hostDeps$4(ctx) {
|
|
|
7932
8003
|
function isCollection(entry) {
|
|
7933
8004
|
return typeof entry.id === "number";
|
|
7934
8005
|
}
|
|
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
|
-
};
|
|
8006
|
+
function findLibrary(list) {
|
|
8007
|
+
return list.filter(isCollection).find((collection) => collection.location === "/" && collection.type === "library") ?? null;
|
|
7948
8008
|
}
|
|
7949
|
-
function isComplete(
|
|
7950
|
-
return
|
|
8009
|
+
function isComplete(library, libraryLicensed) {
|
|
8010
|
+
return !libraryLicensed || library !== null && library.is_remote_synced;
|
|
7951
8011
|
}
|
|
7952
8012
|
function isNotAnsweringYet(error) {
|
|
7953
8013
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || isRejectedCredential(error);
|
|
@@ -7976,34 +8036,8 @@ async function isReadOnly(client) {
|
|
|
7976
8036
|
async function requireReadWrite(client) {
|
|
7977
8037
|
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
8038
|
}
|
|
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
|
-
};
|
|
8039
|
+
async function ensureLibrary(client, library) {
|
|
8040
|
+
return library ?? client.collection.createLibrary();
|
|
8007
8041
|
}
|
|
8008
8042
|
function itemLabel(item) {
|
|
8009
8043
|
return item.name === null ? `${item.model} ${item.id}` : `${item.model} ${item.id} "${item.name}"`;
|
|
@@ -8040,21 +8074,12 @@ function explainRefusal(error) {
|
|
|
8040
8074
|
if (!refusal.success) return error;
|
|
8041
8075
|
return new ConfigError([...refusal.data.errors.required.flatMap(requiredSyncLines), `After the fix, run \`rde init --only ${STEP_ID$2}\`.`].join("\n"));
|
|
8042
8076
|
}
|
|
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
8077
|
function exportAdvice(profile, branch, changes) {
|
|
8053
8078
|
const command = exportCommand(profile, branch);
|
|
8054
8079
|
if (!remoteMoved(changes)) return `\`${command}\``;
|
|
8055
8080
|
return `the branch has commits Metabase has not imported; \`${mbCommand(profile)} git-sync export-preflight\`, then \`${command} --merge\``;
|
|
8056
8081
|
}
|
|
8057
|
-
async function planExport(ctx, runner, instance,
|
|
8082
|
+
async function planExport(ctx, runner, instance, name) {
|
|
8058
8083
|
const changes = await mbTool(runner).gitSyncHasRemoteChanges(instance);
|
|
8059
8084
|
if (remoteMoved(changes)) return {
|
|
8060
8085
|
changes,
|
|
@@ -8064,7 +8089,7 @@ async function planExport(ctx, runner, instance, names) {
|
|
|
8064
8089
|
changes,
|
|
8065
8090
|
push: await ctx.prompts.confirm({
|
|
8066
8091
|
key: SYNC_SCOPE_KEYS.export,
|
|
8067
|
-
message: `Push ${
|
|
8092
|
+
message: `Push ${name} to ${requireGitRemote(ctx).branch} now?`,
|
|
8068
8093
|
initialValue: true
|
|
8069
8094
|
})
|
|
8070
8095
|
};
|
|
@@ -8079,7 +8104,7 @@ function reportPending(run) {
|
|
|
8079
8104
|
}
|
|
8080
8105
|
async function exportScope(run, runner) {
|
|
8081
8106
|
const mb = mbTool(runner);
|
|
8082
|
-
const label =
|
|
8107
|
+
const label = run.name;
|
|
8083
8108
|
const spinner = run.ctx.ui.spinner(`Pushing ${label} to ${run.gitRemote.branch}…`);
|
|
8084
8109
|
try {
|
|
8085
8110
|
const final = (await mb.gitSyncExport(run.instance, {
|
|
@@ -8100,9 +8125,6 @@ async function exportScope(run, runner) {
|
|
|
8100
8125
|
reportPending(run);
|
|
8101
8126
|
}
|
|
8102
8127
|
}
|
|
8103
|
-
function scopeLabel(synced) {
|
|
8104
|
-
return `Synced to git: ${synced.map((collection) => collection.name).join(", ")}`;
|
|
8105
|
-
}
|
|
8106
8128
|
function syncScopeStep(deps = hostDeps$4) {
|
|
8107
8129
|
return {
|
|
8108
8130
|
id: STEP_ID$2,
|
|
@@ -8118,7 +8140,7 @@ function syncScopeStep(deps = hostDeps$4) {
|
|
|
8118
8140
|
key: ctx.secrets.apiKey
|
|
8119
8141
|
});
|
|
8120
8142
|
try {
|
|
8121
|
-
if (isComplete(
|
|
8143
|
+
if (isComplete(findLibrary(await client.collection.list()), hasLibrary(ctx.state.license))) return "done";
|
|
8122
8144
|
return await isReadOnly(client) ? "not-applicable" : "todo";
|
|
8123
8145
|
} catch (error) {
|
|
8124
8146
|
if (isNotAnsweringYet(error)) return "todo";
|
|
@@ -8137,30 +8159,27 @@ function syncScopeStep(deps = hostDeps$4) {
|
|
|
8137
8159
|
await requireReadWrite(client);
|
|
8138
8160
|
await requireSettledImport(ctx, client, gitRemote);
|
|
8139
8161
|
const libraryLicensed = hasLibrary(ctx.state.license);
|
|
8140
|
-
const
|
|
8141
|
-
|
|
8142
|
-
|
|
8162
|
+
const found = findLibrary(await client.collection.list());
|
|
8163
|
+
if (isComplete(found, libraryLicensed)) {
|
|
8164
|
+
ctx.ui.info(libraryLicensed ? `Synced to git: ${found?.name ?? LIBRARY_NAME}` : "The license has no Library, so there is nothing to mark for sync");
|
|
8165
|
+
return { outcome: "completed" };
|
|
8166
|
+
}
|
|
8167
|
+
const name = found?.name ?? LIBRARY_NAME;
|
|
8168
|
+
const plan = await planExport(ctx, resolved.runner, instance, name);
|
|
8143
8169
|
const spinner = ctx.ui.spinner("Choosing what syncs to git…");
|
|
8144
|
-
let synced;
|
|
8145
|
-
let chosen;
|
|
8146
8170
|
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])) });
|
|
8171
|
+
const library = await ensureLibrary(client, found);
|
|
8172
|
+
await client.remoteSync.updateScope({ collections: { [library.id]: true } });
|
|
8151
8173
|
} catch (error) {
|
|
8152
8174
|
spinner.fail("Could not choose what syncs to git");
|
|
8153
8175
|
throw explainRefusal(error);
|
|
8154
8176
|
}
|
|
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" };
|
|
8177
|
+
spinner.stop(`Synced to git: ${name}`);
|
|
8159
8178
|
const exportRun = {
|
|
8160
8179
|
ctx,
|
|
8161
8180
|
instance,
|
|
8162
8181
|
gitRemote,
|
|
8163
|
-
|
|
8182
|
+
name,
|
|
8164
8183
|
changes: plan.changes
|
|
8165
8184
|
};
|
|
8166
8185
|
if (plan.push) await exportScope(exportRun, resolved.runner);
|
|
@@ -8247,10 +8266,11 @@ async function pendingExportLines(state, secrets, runner) {
|
|
|
8247
8266
|
throw error;
|
|
8248
8267
|
}
|
|
8249
8268
|
}
|
|
8250
|
-
function skillLine(entries, detected) {
|
|
8269
|
+
function skillLine(entries, detected, skipped) {
|
|
8251
8270
|
const agents = [...new Set(entries.flatMap((entry) => coveredAgentNames(entry, detected)))];
|
|
8252
|
-
if (agents.length === 0) return
|
|
8253
|
-
return
|
|
8271
|
+
if (agents.length === 0) return `${RDE_SKILL_NAME} skill: not installed; \`rde init --only agents-skills\``;
|
|
8272
|
+
if (skipped) return `${RDE_SKILL_NAME} skill: skipped by this run; an earlier install covers ${agents.join(", ")}`;
|
|
8273
|
+
return `${RDE_SKILL_NAME} skill: installed for ${agents.join(", ")}`;
|
|
8254
8274
|
}
|
|
8255
8275
|
function launchLines(agents, workdir) {
|
|
8256
8276
|
const detected = agents.filter((agent) => agent.detected);
|
|
@@ -8270,7 +8290,7 @@ async function summaryLines(state, secrets, deps) {
|
|
|
8270
8290
|
const installed = await skillsCli(deps.runner).list();
|
|
8271
8291
|
const agents = await detectAgents(deps.probe);
|
|
8272
8292
|
const detected = agents.filter((agent) => agent.detected);
|
|
8273
|
-
lines.push(skillLine(installed.filter((entry) => entry.name ===
|
|
8293
|
+
lines.push(skillLine(installed.filter((entry) => entry.name === RDE_SKILL_NAME), detected, state.steps["agents-skills"]?.status === "skipped"));
|
|
8274
8294
|
lines.push(...launchLines(agents, state.gitRemote?.workdir ?? null));
|
|
8275
8295
|
lines.push("Lifecycle: rde status (--json for scripts), rde stop, rde start");
|
|
8276
8296
|
return lines;
|
|
@@ -8714,6 +8734,18 @@ function parseDatabase(spec) {
|
|
|
8714
8734
|
uri
|
|
8715
8735
|
};
|
|
8716
8736
|
}
|
|
8737
|
+
function databasesSection(flags) {
|
|
8738
|
+
if (flags.databaseName.length > flags.database.length) throw new ConfigError("--database-name names a --database; give one --database for each name");
|
|
8739
|
+
if (flags.database.length === 0) return;
|
|
8740
|
+
return flags.database.map((spec, index) => {
|
|
8741
|
+
const name = flags.databaseName[index];
|
|
8742
|
+
if (name !== void 0 && name.trim().length === 0) throw new ConfigError("--database-name needs a display name");
|
|
8743
|
+
return {
|
|
8744
|
+
...parseDatabase(spec),
|
|
8745
|
+
...name !== void 0 && { name }
|
|
8746
|
+
};
|
|
8747
|
+
});
|
|
8748
|
+
}
|
|
8717
8749
|
function adminSection(flags, secrets) {
|
|
8718
8750
|
const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
|
|
8719
8751
|
if (flags.admin === void 0 && !prompted) return;
|
|
@@ -8851,7 +8883,7 @@ async function answersFromFlags(flags, readStdin) {
|
|
|
8851
8883
|
runtime: runtimeSection(flags),
|
|
8852
8884
|
"admin-user": adminSection(flags, secrets),
|
|
8853
8885
|
license: licenseSection(flags, secrets),
|
|
8854
|
-
databases: flags
|
|
8886
|
+
databases: databasesSection(flags),
|
|
8855
8887
|
"git-sync": gitSection(flags, secrets),
|
|
8856
8888
|
"agents-skills": agentsSection(flags),
|
|
8857
8889
|
"api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
|
|
@@ -8908,6 +8940,11 @@ function givenFlags(flags) {
|
|
|
8908
8940
|
flag: "--database",
|
|
8909
8941
|
given: flags.database.length > 0
|
|
8910
8942
|
},
|
|
8943
|
+
{
|
|
8944
|
+
step: "databases",
|
|
8945
|
+
flag: "--database-name",
|
|
8946
|
+
given: flags.databaseName.length > 0
|
|
8947
|
+
},
|
|
8911
8948
|
{
|
|
8912
8949
|
step: "git-sync",
|
|
8913
8950
|
flag: "--git-dir",
|
|
@@ -9336,7 +9373,7 @@ const db = defineCommand({
|
|
|
9336
9373
|
//#endregion
|
|
9337
9374
|
//#region package.json
|
|
9338
9375
|
var name = "@metabase/rde";
|
|
9339
|
-
var version = "0.
|
|
9376
|
+
var version = "0.5.0";
|
|
9340
9377
|
var description = "Prepares a local machine for data-engineering work with Metabase and coding agents";
|
|
9341
9378
|
//#endregion
|
|
9342
9379
|
//#region src/version.ts
|
|
@@ -10050,8 +10087,8 @@ async function gitSyncRow(state, instance, deps) {
|
|
|
10050
10087
|
const SKILL_STEP = "agents-skills";
|
|
10051
10088
|
const GLOBAL_SCOPE = "global";
|
|
10052
10089
|
const SKILL_FILE = "SKILL.md";
|
|
10053
|
-
const SOURCE_SKILLS_DIR = "skills";
|
|
10054
10090
|
const CLAUDE_CODE = "Claude Code";
|
|
10091
|
+
const COPIES_LABEL = `${RDE_SKILL_NAME} skill copies`;
|
|
10055
10092
|
const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
|
|
10056
10093
|
function skillRowId(agent) {
|
|
10057
10094
|
return `skill.${agent.id}`;
|
|
@@ -10063,12 +10100,12 @@ async function copyRef(entry, deps) {
|
|
|
10063
10100
|
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
10064
10101
|
return lockedSkill(path, await deps.skills.files.readJson(path), entry.name)?.ref ?? null;
|
|
10065
10102
|
}
|
|
10066
|
-
async function judgeCopy(entry, origin, deps) {
|
|
10103
|
+
async function judgeCopy(skill, entry, origin, deps) {
|
|
10067
10104
|
const where = `${entry.scope} ${entry.path}`;
|
|
10068
10105
|
switch (origin.kind) {
|
|
10069
10106
|
case "local": {
|
|
10070
10107
|
const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
|
|
10071
|
-
const wanted = await deps.skills.files.readText(join(origin.path,
|
|
10108
|
+
const wanted = await deps.skills.files.readText(join(origin.path, sourceSkillDirectory(skill), SKILL_FILE));
|
|
10072
10109
|
const current = installed !== null && installed === wanted;
|
|
10073
10110
|
return {
|
|
10074
10111
|
entry,
|
|
@@ -10109,80 +10146,80 @@ function loadedCopyLine(entries, cwd) {
|
|
|
10109
10146
|
if (loaded === void 0) return count;
|
|
10110
10147
|
return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
|
|
10111
10148
|
}
|
|
10112
|
-
function staleCopiesRow(stale, detail, deps) {
|
|
10149
|
+
function staleCopiesRow(stale, replaced, detail, deps) {
|
|
10113
10150
|
const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
|
|
10114
10151
|
const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
|
|
10115
10152
|
const repairs = [];
|
|
10116
|
-
|
|
10117
|
-
if (
|
|
10118
|
-
|
|
10153
|
+
const stepFixes = [];
|
|
10154
|
+
if (globalStale) stepFixes.push(`reinstalls the global copy from ${deps.skills.source}`);
|
|
10155
|
+
if (replaced) stepFixes.push(`removes the copies ${RDE_SKILL_NAME} replaces`);
|
|
10156
|
+
if (stepFixes.length > 0) repairs.push(`\`rde init --only ${SKILL_STEP}\` ${stepFixes.join(" and ")}`);
|
|
10157
|
+
if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove ${RDE_SKILL_NAME} -y\` in ${deps.skills.cwd}`);
|
|
10158
|
+
return failed("skill-copies", COPIES_LABEL, {
|
|
10119
10159
|
detail,
|
|
10120
10160
|
hint: `${repairs.join("; ")}; then restart the agent session`,
|
|
10121
|
-
step:
|
|
10161
|
+
step: stepFixes.length > 0 ? SKILL_STEP : null
|
|
10122
10162
|
});
|
|
10123
10163
|
}
|
|
10124
|
-
async function skillCopiesRow(globalEntries, deps) {
|
|
10164
|
+
async function skillCopiesRow(globalEntries, replaced, deps) {
|
|
10125
10165
|
const id = "skill-copies";
|
|
10126
|
-
const label =
|
|
10166
|
+
const label = COPIES_LABEL;
|
|
10127
10167
|
const cwd = deps.skills.cwd;
|
|
10128
|
-
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name ===
|
|
10168
|
+
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === RDE_SKILL_NAME);
|
|
10129
10169
|
const entries = [...globalEntries, ...projectEntries];
|
|
10130
|
-
if (entries.length === 0) return skipped(id, label, "no copy installed");
|
|
10170
|
+
if (entries.length === 0 && replaced.length === 0) return skipped(id, label, "no copy installed");
|
|
10131
10171
|
const origin = skillOrigin(deps.skills.source, cwd);
|
|
10132
10172
|
const judged = [];
|
|
10133
|
-
for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
|
|
10173
|
+
for (const entry of entries) judged.push(await judgeCopy(RDE_SKILL_NAME, entry, origin, deps));
|
|
10134
10174
|
const loaded = loadedCopyLine(entries, cwd);
|
|
10135
|
-
const lines = judged.map((copy) => copy.line);
|
|
10175
|
+
const lines = [...judged.map((copy) => copy.line), ...replaced.map((entry) => `${entry.scope} ${entry.path}: replaced by ${RDE_SKILL_NAME}`)];
|
|
10136
10176
|
const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
|
|
10137
10177
|
const stale = judged.filter((copy) => !copy.current);
|
|
10138
|
-
|
|
10178
|
+
if (stale.length === 0 && replaced.length === 0) return ok(id, label, detail);
|
|
10179
|
+
return staleCopiesRow(stale, replaced.length > 0, detail, deps);
|
|
10139
10180
|
}
|
|
10140
|
-
function
|
|
10141
|
-
const
|
|
10142
|
-
|
|
10181
|
+
function dataAppSkillsFix({ current, legacy }) {
|
|
10182
|
+
const command = `\`rde init --only ${SKILL_STEP}\``;
|
|
10183
|
+
const removal = `the copies ${DATA_APPS_SKILL_NAME} replaces`;
|
|
10184
|
+
if (!legacy) return `${command} installs it`;
|
|
10185
|
+
return current ? `${command} removes ${removal}` : `${command} installs it and removes ${removal}`;
|
|
10143
10186
|
}
|
|
10144
|
-
async function dataAppSkillsRow(
|
|
10187
|
+
async function dataAppSkillsRow(installed, deps) {
|
|
10145
10188
|
const id = "data-app-skills";
|
|
10146
10189
|
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);
|
|
10190
|
+
const origin = skillOrigin(deps.skills.source, deps.skills.cwd);
|
|
10191
|
+
const lines = [];
|
|
10192
|
+
const copy = installed.find((candidate) => candidate.name === DATA_APPS_SKILL_NAME);
|
|
10193
|
+
let current = false;
|
|
10194
|
+
if (copy === void 0) lines.push(`${DATA_APPS_SKILL_NAME}: not installed`);
|
|
10195
|
+
else {
|
|
10196
|
+
const judged = await judgeCopy(DATA_APPS_SKILL_NAME, copy, origin, deps);
|
|
10165
10197
|
lines.push(judged.line);
|
|
10166
|
-
current
|
|
10198
|
+
current = judged.current;
|
|
10167
10199
|
}
|
|
10200
|
+
const legacy = installed.filter((entry) => isLegacyDataAppCopy(entry.name, entry.source));
|
|
10201
|
+
for (const entry of legacy) lines.push(`${entry.scope} ${entry.path}: replaced by ${DATA_APPS_SKILL_NAME}`);
|
|
10168
10202
|
const detail = lines.join("\n");
|
|
10169
|
-
if (current) return ok(id, label, detail);
|
|
10203
|
+
if (current && legacy.length === 0) return ok(id, label, detail);
|
|
10170
10204
|
return failed(id, label, {
|
|
10171
10205
|
detail,
|
|
10172
|
-
hint:
|
|
10206
|
+
hint: `${dataAppSkillsFix({
|
|
10207
|
+
current,
|
|
10208
|
+
legacy: legacy.length > 0
|
|
10209
|
+
})}; then restart the agent session`,
|
|
10173
10210
|
step: SKILL_STEP
|
|
10174
10211
|
});
|
|
10175
10212
|
}
|
|
10176
10213
|
async function skillRows(state, deps) {
|
|
10177
|
-
const label =
|
|
10214
|
+
const label = `${RDE_SKILL_NAME} skill`;
|
|
10178
10215
|
if (state === null) return [skipped("skill", label, NO_INSTANCE_REASON)];
|
|
10179
10216
|
if (state.steps[SKILL_STEP]?.status === "skipped") return [skipped("skill", label, `skipped during init; \`rde init --only ${SKILL_STEP}\``)];
|
|
10180
10217
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
10181
10218
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
10182
10219
|
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(
|
|
10220
|
+
const entries = installed.filter((entry) => entry.name === RDE_SKILL_NAME);
|
|
10221
|
+
const copies = await skillCopiesRow(entries, installed.filter((entry) => isReplacedRdeCopy(entry.name)), deps);
|
|
10222
|
+
const dataApps = await dataAppSkillsRow(installed, deps);
|
|
10186
10223
|
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [
|
|
10187
10224
|
failed("skill", label, {
|
|
10188
10225
|
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
@@ -10642,6 +10679,10 @@ const init = defineRdeCommand({
|
|
|
10642
10679
|
type: "string",
|
|
10643
10680
|
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
10681
|
},
|
|
10682
|
+
"database-name": {
|
|
10683
|
+
type: "string",
|
|
10684
|
+
description: "Display name of the --database in the same position, such as the name the production Metabase uses; repeatable"
|
|
10685
|
+
},
|
|
10645
10686
|
"git-dir": {
|
|
10646
10687
|
type: "string",
|
|
10647
10688
|
description: "Directory for the content repository"
|
|
@@ -10699,6 +10740,7 @@ const init = defineRdeCommand({
|
|
|
10699
10740
|
licenseToken: args["license-token"],
|
|
10700
10741
|
skipLicense: args["skip-license"],
|
|
10701
10742
|
database: repeatedValues(rawArgs, "--database"),
|
|
10743
|
+
databaseName: repeatedValues(rawArgs, "--database-name"),
|
|
10702
10744
|
gitDir: args["git-dir"],
|
|
10703
10745
|
gitRemote: args["git-remote"],
|
|
10704
10746
|
gitUrl: args["git-url"],
|
|
@@ -11264,17 +11306,22 @@ async function findMbProfiles(candidates, urls, runner) {
|
|
|
11264
11306
|
}
|
|
11265
11307
|
return found;
|
|
11266
11308
|
}
|
|
11309
|
+
function spokenList(items) {
|
|
11310
|
+
const last = items.at(-1);
|
|
11311
|
+
if (last === void 0 || items.length <= 2) return items.join(" and ");
|
|
11312
|
+
return `${items.slice(0, -1).join(", ")}, and ${last}`;
|
|
11313
|
+
}
|
|
11267
11314
|
function skillLabel(names, entries) {
|
|
11268
11315
|
const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
|
|
11269
11316
|
const noun = names.length === 1 ? "skill" : "skills";
|
|
11270
|
-
return `${names
|
|
11317
|
+
return `${spokenList(names)} ${noun} for ${agents.join(", ")}`;
|
|
11271
11318
|
}
|
|
11319
|
+
const OWNED_SKILL_NAMES = RDE_OWNED_SKILL_NAMES;
|
|
11272
11320
|
async function findSkill(runner, childOutput) {
|
|
11273
11321
|
const skills = skillsCli(runner);
|
|
11274
|
-
const
|
|
11275
|
-
const entries = (await skills.list()).filter((entry) => owned.includes(entry.name));
|
|
11322
|
+
const entries = (await skills.list()).filter((entry) => OWNED_SKILL_NAMES.includes(entry.name));
|
|
11276
11323
|
if (entries.length === 0) return null;
|
|
11277
|
-
const names =
|
|
11324
|
+
const names = OWNED_SKILL_NAMES.filter((name) => entries.some((entry) => entry.name === name));
|
|
11278
11325
|
return {
|
|
11279
11326
|
label: skillLabel(names, entries),
|
|
11280
11327
|
remove: () => skills.remove(names, childOutput)
|
|
@@ -11376,7 +11423,7 @@ function keptOnDisk(options, state, dirPresent) {
|
|
|
11376
11423
|
const kept = [];
|
|
11377
11424
|
if (options.keepData && dirPresent) kept.push(`Instance directory ${instanceDir(options.instance)} (--keep-data); \`rde init\` reinstalls from it`);
|
|
11378
11425
|
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 ${
|
|
11426
|
+
if (!options.removeSkill) kept.push(`The ${spokenList(RDE_INSTALLED_SKILL_NAMES)} skills for your agents; pass --remove-skill to remove them`);
|
|
11380
11427
|
return kept;
|
|
11381
11428
|
}
|
|
11382
11429
|
async function planUninstall(options, deps) {
|
|
@@ -11535,7 +11582,7 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11535
11582
|
},
|
|
11536
11583
|
"remove-skill": {
|
|
11537
11584
|
type: "boolean",
|
|
11538
|
-
description: "Also remove the rde and rde-data-apps skills from your coding agents",
|
|
11585
|
+
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
11586
|
default: false
|
|
11540
11587
|
},
|
|
11541
11588
|
"remove-image": {
|
|
@@ -11857,7 +11904,7 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11857
11904
|
const backup = join(instanceBackupsDir(ctx.state.name), backupStamp(resolved.now()));
|
|
11858
11905
|
ctx.ui.note(planLines(configured, current, target, backup).join("\n"), "rde upgrade");
|
|
11859
11906
|
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;
|
|
11907
|
+
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
11908
|
if (!options.yes) {
|
|
11862
11909
|
if (!await ctx.prompts.confirm({
|
|
11863
11910
|
message: "Upgrade now?",
|
|
@@ -11880,7 +11927,7 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11880
11927
|
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
11881
11928
|
const moved = majorMove(current, to);
|
|
11882
11929
|
if (moved !== null) {
|
|
11883
|
-
if (announced === null) ctx.ui.warn(`This moved Metabase from ${moved.from} to ${moved.to}, a new major version
|
|
11930
|
+
if (announced === null) ctx.ui.warn(`This moved Metabase from ${moved.from} to ${moved.to}, a new major version`);
|
|
11884
11931
|
await runWizard([resolved.skills(options.yes ? DETECT_AGENTS : ASK_AGENTS)], ctx);
|
|
11885
11932
|
}
|
|
11886
11933
|
return {
|
package/package.json
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@metabase/rde",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
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",
|