@metabase/rde 0.1.16 → 0.1.17
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 +12 -6
- package/dist/cli.mjs +518 -87
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -64,8 +64,9 @@ The git step reads `–` until a license with remote sync is active; it is check
|
|
|
64
64
|
| 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 |
|
|
65
65
|
| 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 |
|
|
66
66
|
| 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 |
|
|
67
|
+
| Choose what syncs to git | Only once a repository is connected. Creates the Library (when the license has the `library` feature) and an `Analytics` collection, unless the repository already brought synced top-level collections in, and marks them for sync. Before it changes anything it asks whether to push them 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 |
|
|
67
68
|
| 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 |
|
|
68
|
-
| Summary | URL, login email, the `mb` command for the instance, databases, repository, and the command that opens each detected agent |
|
|
69
|
+
| 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 |
|
|
69
70
|
|
|
70
71
|
The second run on the same machine detects every step as done and prints the summary only:
|
|
71
72
|
|
|
@@ -151,7 +152,7 @@ Docker is used whenever it is available: the image is self-contained, and the co
|
|
|
151
152
|
| --- | --- |
|
|
152
153
|
| `rde start` | Starts the instance, and waits until Metabase is healthy; `--port <n>` moves the instance to another port first (see [The instance's port is taken](#troubleshooting)) |
|
|
153
154
|
| `rde stop` | Stops the instance; the data stays |
|
|
154
|
-
| `rde status` | The login email, the `mb` profile, the init checklist, whether setup is complete (or the first step `init` did not finish, with the `rde init --from` command that resumes there), runtime state, health, databases with their sync state, remote sync state and the content repository's working directory (`contentRepository`); `--json` for scripts and agents (`mbProfile`, `adminEmail`, `setup`, never a secret); exit 0 whatever it reports |
|
|
155
|
+
| `rde status` | The login email, the `mb` profile, the init checklist, whether setup is complete (or the first step `init` did not finish, with the `rde init --from` command that resumes there), runtime state, health, databases with their sync state, remote sync state with the collections it syncs, and the content repository's working directory (`contentRepository`); `--json` for scripts and agents (`mbProfile`, `adminEmail`, `setup`, never a secret); exit 0 whatever it reports |
|
|
155
156
|
| `rde credentials` | The Metabase URL, admin email, and password; `--json` for scripts and agents; `--api-key` for the API key alone |
|
|
156
157
|
| `rde logs` | The container's log, or the jar's `metabase.log`; `-f` keeps streaming |
|
|
157
158
|
| `rde open` | Opens the instance in the browser |
|
|
@@ -184,7 +185,11 @@ With a license that includes remote sync, the step after the license connects a
|
|
|
184
185
|
- **Local remote inside `~/.rde`** (default): a bare repository at `instances/default/data/content.git`, mounted into the container and reached as `file:///rde/content.git`. No account, no token. Your working directory gets it as `origin` (or as `rde-remote` when `origin` already points elsewhere).
|
|
185
186
|
- **Hosted remote**: a GitHub, GitLab, or Bitbucket `https://` URL plus a token with read and write access. `rde` tests the connection through Metabase before saving anything, then clones the repository into the working directory (an existing repository gets the URL as `origin` instead); an empty hosted repository gets the first commit pushed to it. The token is handed to Metabase and, for the clone and that first push, to git in its environment; it never lands in the clone's config, the remote URL, or a credential helper, so a later `git pull` uses your own git credentials.
|
|
186
187
|
|
|
187
|
-
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
|
|
188
|
+
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.
|
|
189
|
+
|
|
190
|
+
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), and one top-level collection for dashboards, questions, models, and documents; transforms already sync through the repository step. The collection is `Analytics`, created for the purpose (beside any `Analytics` of the user's, which stays out of git), unless the import brought top-level collections the repository already syncs, in which case those are used and nothing is created; 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. Anything filed outside these 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.
|
|
191
|
+
|
|
192
|
+
Marking the scope 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 the step asks, before it creates or marks anything, whether to push the scope 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:
|
|
188
193
|
|
|
189
194
|
```sh
|
|
190
195
|
cd <your working directory>
|
|
@@ -195,7 +200,7 @@ git pull # after an export, to see the YAML files
|
|
|
195
200
|
|
|
196
201
|
## Running `init` without a terminal
|
|
197
202
|
|
|
198
|
-
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, and the
|
|
203
|
+
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 sync scope. 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.
|
|
199
204
|
|
|
200
205
|
### Flags
|
|
201
206
|
|
|
@@ -237,11 +242,12 @@ rde init --license-token '$RDE_LICENSE_TOKEN' …
|
|
|
237
242
|
{ "engine": "postgres", "uri": "$RDE_DATABASE_1_URI" },
|
|
238
243
|
{ "engine": "bigquery-cloud-sdk", "fields": { "project-id": "acme", "service-account-json": "/keys/acme.json" }, "name": "Warehouse" }
|
|
239
244
|
],
|
|
240
|
-
"git-sync": { "location": "new", "path": "~/analytics", "remote": "local-bare" }
|
|
245
|
+
"git-sync": { "location": "new", "path": "~/analytics", "remote": "local-bare" },
|
|
246
|
+
"sync-scope": { "export": true }
|
|
241
247
|
}
|
|
242
248
|
```
|
|
243
249
|
|
|
244
|
-
`"runtime"` takes `"kind": "docker"` or `"jar"`, which `"image"` and `"jar"` imply on their own (`"runtime": { "jar": "latest" }` runs the latest release with Java). `"license": { "token": null }` skips the license and `"git-sync": { "location": "skip" }` leaves the repository for later. `"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.
|
|
250
|
+
`"runtime"` takes `"kind": "docker"` or `"jar"`, which `"image"` and `"jar"` imply on their own (`"runtime": { "jar": "latest" }` runs the latest 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 sync scope 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.
|
|
245
251
|
|
|
246
252
|
### Recording a run
|
|
247
253
|
|
package/dist/cli.mjs
CHANGED
|
@@ -79,6 +79,8 @@ var MetabaseApiError = class extends Error {
|
|
|
79
79
|
path;
|
|
80
80
|
errors;
|
|
81
81
|
reason;
|
|
82
|
+
code;
|
|
83
|
+
detail;
|
|
82
84
|
constructor(failure) {
|
|
83
85
|
super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
|
|
84
86
|
this.name = "MetabaseApiError";
|
|
@@ -86,6 +88,8 @@ var MetabaseApiError = class extends Error {
|
|
|
86
88
|
this.path = failure.path;
|
|
87
89
|
this.errors = failure.errors;
|
|
88
90
|
this.reason = failure.message;
|
|
91
|
+
this.code = failure.code;
|
|
92
|
+
this.detail = failure.detail;
|
|
89
93
|
}
|
|
90
94
|
};
|
|
91
95
|
const UNAUTHORIZED_STATUS = 401;
|
|
@@ -264,6 +268,7 @@ const StepId = z.enum([
|
|
|
264
268
|
"transforms",
|
|
265
269
|
"license",
|
|
266
270
|
"git-sync",
|
|
271
|
+
"sync-scope",
|
|
267
272
|
"databases",
|
|
268
273
|
"summary"
|
|
269
274
|
]);
|
|
@@ -332,6 +337,9 @@ const REMOTE_SYNC_FEATURE = "remote-sync";
|
|
|
332
337
|
function hasRemoteSync(license) {
|
|
333
338
|
return license.activated && license.features.includes("remote-sync");
|
|
334
339
|
}
|
|
340
|
+
function hasLibrary(license) {
|
|
341
|
+
return license.activated && license.features.includes("library");
|
|
342
|
+
}
|
|
335
343
|
const DatabaseRecord = z.object({
|
|
336
344
|
id: z.int(),
|
|
337
345
|
name: z.string(),
|
|
@@ -354,7 +362,7 @@ const StepRecord = z.object({
|
|
|
354
362
|
completedAt: z.string()
|
|
355
363
|
});
|
|
356
364
|
const InstanceState = z.object({
|
|
357
|
-
schemaVersion: z.literal(
|
|
365
|
+
schemaVersion: z.literal(7),
|
|
358
366
|
name: z.string(),
|
|
359
367
|
runtime: RuntimeState.nullable(),
|
|
360
368
|
url: z.string().nullable(),
|
|
@@ -365,6 +373,7 @@ const InstanceState = z.object({
|
|
|
365
373
|
databases: z.array(DatabaseRecord),
|
|
366
374
|
gitRemote: GitRemoteState.nullable(),
|
|
367
375
|
movedFrom: z.string().nullable(),
|
|
376
|
+
syncRoot: z.int().nullable(),
|
|
368
377
|
steps: z.partialRecord(StepId, StepRecord)
|
|
369
378
|
});
|
|
370
379
|
function configuredRuntime(state) {
|
|
@@ -394,7 +403,7 @@ function mbCommand(profile) {
|
|
|
394
403
|
}
|
|
395
404
|
function emptyInstanceState(name) {
|
|
396
405
|
return {
|
|
397
|
-
schemaVersion:
|
|
406
|
+
schemaVersion: 7,
|
|
398
407
|
name,
|
|
399
408
|
runtime: null,
|
|
400
409
|
url: null,
|
|
@@ -405,6 +414,7 @@ function emptyInstanceState(name) {
|
|
|
405
414
|
databases: [],
|
|
406
415
|
gitRemote: null,
|
|
407
416
|
movedFrom: null,
|
|
417
|
+
syncRoot: null,
|
|
408
418
|
steps: {}
|
|
409
419
|
};
|
|
410
420
|
}
|
|
@@ -416,6 +426,7 @@ const V2 = 2;
|
|
|
416
426
|
const V3 = 3;
|
|
417
427
|
const V4 = 4;
|
|
418
428
|
const V5 = 5;
|
|
429
|
+
const V6 = 6;
|
|
419
430
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
420
431
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
421
432
|
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
@@ -427,7 +438,11 @@ const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
|
427
438
|
password: z.string()
|
|
428
439
|
})]);
|
|
429
440
|
const StateV4 = z.object({ schemaVersion: z.literal(V4) }).loose();
|
|
430
|
-
const StateV5 = z.object({
|
|
441
|
+
const StateV5 = z.object({
|
|
442
|
+
schemaVersion: z.literal(V5),
|
|
443
|
+
steps: z.record(z.string(), z.unknown())
|
|
444
|
+
}).loose();
|
|
445
|
+
const StateV6 = z.object({ schemaVersion: z.literal(V6) }).loose();
|
|
431
446
|
const StateV3 = z.object({
|
|
432
447
|
schemaVersion: z.literal(V3),
|
|
433
448
|
admin: AdminV3.nullable(),
|
|
@@ -454,10 +469,19 @@ function liftV4(raw) {
|
|
|
454
469
|
};
|
|
455
470
|
}
|
|
456
471
|
function liftV5(raw) {
|
|
457
|
-
const { sampleDatabase: _sampleDatabase, ...state } = StateV5.parse(raw);
|
|
472
|
+
const { sampleDatabase: _sampleDatabase, steps, ...state } = StateV5.parse(raw);
|
|
473
|
+
const { "sample-database": _sampleDatabaseStep, ...remainingSteps } = steps;
|
|
458
474
|
return {
|
|
459
475
|
...state,
|
|
460
|
-
schemaVersion:
|
|
476
|
+
schemaVersion: V6,
|
|
477
|
+
steps: remainingSteps
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function liftV6(raw) {
|
|
481
|
+
return {
|
|
482
|
+
...StateV6.parse(raw),
|
|
483
|
+
schemaVersion: 7,
|
|
484
|
+
syncRoot: null
|
|
461
485
|
};
|
|
462
486
|
}
|
|
463
487
|
function adminV4(admin) {
|
|
@@ -498,15 +522,16 @@ function migrateInstanceState(raw) {
|
|
|
498
522
|
case V2: return migrateInstanceState(liftV2(raw));
|
|
499
523
|
case V3: return liftV3(raw);
|
|
500
524
|
case V4: return migrateInstanceState(liftV4(raw));
|
|
501
|
-
case V5: return
|
|
502
|
-
|
|
525
|
+
case V5: return migrateInstanceState(liftV5(raw));
|
|
526
|
+
case V6: return {
|
|
527
|
+
state: InstanceState.parse(liftV6(raw)),
|
|
503
528
|
movedSecrets: null
|
|
504
529
|
};
|
|
505
|
-
case
|
|
530
|
+
case 7: return {
|
|
506
531
|
state: InstanceState.parse(raw),
|
|
507
532
|
movedSecrets: null
|
|
508
533
|
};
|
|
509
|
-
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected
|
|
534
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 7)`);
|
|
510
535
|
}
|
|
511
536
|
}
|
|
512
537
|
//#endregion
|
|
@@ -814,8 +839,11 @@ const SESSION_HEADER = "X-Metabase-Session";
|
|
|
814
839
|
const API_KEY_HEADER = "X-Api-Key";
|
|
815
840
|
const JSON_CONTENT_TYPE = "application/json";
|
|
816
841
|
const SERVICE_UNAVAILABLE_STATUS = 503;
|
|
842
|
+
const NO_CONTENT_STATUS = 204;
|
|
817
843
|
const NO_AUTH = { kind: "none" };
|
|
818
844
|
const ErrorMessage = z.object({ message: z.string() }).loose();
|
|
845
|
+
const ErrorTitle = z.object({ error: z.string() }).loose();
|
|
846
|
+
const ErrorCode = z.object({ error_code: z.string() }).loose();
|
|
819
847
|
const ErrorFields = z.object({ errors: z.record(z.string(), z.unknown()) }).loose();
|
|
820
848
|
const ErrorText = z.string();
|
|
821
849
|
const ErrorTexts = z.array(z.string());
|
|
@@ -878,6 +906,8 @@ function serverMessage(body) {
|
|
|
878
906
|
const value = jsonValue(body);
|
|
879
907
|
const parsed = ErrorMessage.safeParse(value);
|
|
880
908
|
if (parsed.success) return parsed.data.message;
|
|
909
|
+
const title = ErrorTitle.safeParse(value);
|
|
910
|
+
if (title.success) return title.data.error;
|
|
881
911
|
const errors = serverErrors(body);
|
|
882
912
|
if (errors !== null && Object.keys(errors).length > 0) return Object.entries(errors).map(([name, problem]) => `${name}: ${problem}`).join("; ");
|
|
883
913
|
const plain = ErrorText.safeParse(value);
|
|
@@ -886,6 +916,10 @@ function serverMessage(body) {
|
|
|
886
916
|
if (body.json.parsed || text.length === 0) return `HTTP ${body.status}`;
|
|
887
917
|
return text;
|
|
888
918
|
}
|
|
919
|
+
function errorCode(body) {
|
|
920
|
+
const parsed = ErrorCode.safeParse(jsonValue(body));
|
|
921
|
+
return parsed.success ? parsed.data.error_code : null;
|
|
922
|
+
}
|
|
889
923
|
function failureFor(path, body) {
|
|
890
924
|
if (body.status === SERVICE_UNAVAILABLE_STATUS) {
|
|
891
925
|
const initializing = Initializing.safeParse(jsonValue(body));
|
|
@@ -895,7 +929,9 @@ function failureFor(path, body) {
|
|
|
895
929
|
status: body.status,
|
|
896
930
|
path,
|
|
897
931
|
message: serverMessage(body),
|
|
898
|
-
errors: serverErrors(body)
|
|
932
|
+
errors: serverErrors(body),
|
|
933
|
+
code: errorCode(body),
|
|
934
|
+
detail: body.json.parsed ? body.json.value : null
|
|
899
935
|
});
|
|
900
936
|
}
|
|
901
937
|
function shapeError(path, body, reason) {
|
|
@@ -903,7 +939,9 @@ function shapeError(path, body, reason) {
|
|
|
903
939
|
status: body.status,
|
|
904
940
|
path,
|
|
905
941
|
message: reason,
|
|
906
|
-
errors: null
|
|
942
|
+
errors: null,
|
|
943
|
+
code: null,
|
|
944
|
+
detail: null
|
|
907
945
|
});
|
|
908
946
|
}
|
|
909
947
|
function isTimeout(error) {
|
|
@@ -953,19 +991,33 @@ function createHttp(options) {
|
|
|
953
991
|
if (!received.ok) throw failureFor(path, received.body);
|
|
954
992
|
return received.body;
|
|
955
993
|
}
|
|
994
|
+
function parseJson(path, body, schema) {
|
|
995
|
+
if (!body.json.parsed) throw shapeError(path, body, "expected a JSON body");
|
|
996
|
+
return parseShape(path, body, schema, body.json.value);
|
|
997
|
+
}
|
|
998
|
+
function parseShape(path, body, schema, value) {
|
|
999
|
+
try {
|
|
1000
|
+
return schema.parse(value);
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
|
|
1003
|
+
throw error;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
956
1006
|
return {
|
|
957
1007
|
async requestJson(method, path, request) {
|
|
1008
|
+
return parseJson(path, await requestBody(method, path, request.body), request.schema);
|
|
1009
|
+
},
|
|
1010
|
+
async requestOptionalJson(method, path, request) {
|
|
958
1011
|
const body = await requestBody(method, path, request.body);
|
|
959
|
-
if (
|
|
960
|
-
|
|
961
|
-
return request.schema.parse(body.json.value);
|
|
962
|
-
} catch (error) {
|
|
963
|
-
if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
|
|
964
|
-
throw error;
|
|
965
|
-
}
|
|
1012
|
+
if (body.status === NO_CONTENT_STATUS && body.text.length === 0) return null;
|
|
1013
|
+
return parseJson(path, body, request.schema);
|
|
966
1014
|
},
|
|
967
1015
|
async requestNoContent(method, path, request = {}) {
|
|
968
1016
|
await requestBody(method, path, request.body);
|
|
1017
|
+
},
|
|
1018
|
+
async requestText(method, path, request) {
|
|
1019
|
+
const body = await requestBody(method, path, request.body);
|
|
1020
|
+
return parseShape(path, body, request.schema, body.text);
|
|
969
1021
|
}
|
|
970
1022
|
};
|
|
971
1023
|
}
|
|
@@ -1056,6 +1108,48 @@ function apiKeyResource(http) {
|
|
|
1056
1108
|
regenerate
|
|
1057
1109
|
};
|
|
1058
1110
|
}
|
|
1111
|
+
const Collection = z.object({
|
|
1112
|
+
id: z.int(),
|
|
1113
|
+
name: z.string(),
|
|
1114
|
+
location: z.string(),
|
|
1115
|
+
type: z.string().nullable(),
|
|
1116
|
+
namespace: z.string().nullable(),
|
|
1117
|
+
personal_owner_id: z.int().nullable(),
|
|
1118
|
+
is_remote_synced: z.boolean()
|
|
1119
|
+
}).loose();
|
|
1120
|
+
const RootCollection = z.object({ id: z.literal("root") }).loose();
|
|
1121
|
+
const CollectionList = z.array(z.union([Collection, RootCollection]));
|
|
1122
|
+
const CollectionRef = z.object({
|
|
1123
|
+
id: z.int(),
|
|
1124
|
+
name: z.string()
|
|
1125
|
+
}).loose();
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/metabase/resources/collection.ts
|
|
1128
|
+
function collectionResource(http) {
|
|
1129
|
+
/** GET /api/collection */
|
|
1130
|
+
async function list() {
|
|
1131
|
+
return http.requestJson("GET", "/api/collection", { schema: CollectionList });
|
|
1132
|
+
}
|
|
1133
|
+
/** POST /api/collection */
|
|
1134
|
+
async function create(request) {
|
|
1135
|
+
return http.requestJson("POST", "/api/collection", {
|
|
1136
|
+
body: {
|
|
1137
|
+
...request,
|
|
1138
|
+
parent_id: null
|
|
1139
|
+
},
|
|
1140
|
+
schema: CollectionRef
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
/** POST /api/ee/library; 400 when the Library exists */
|
|
1144
|
+
async function createLibrary() {
|
|
1145
|
+
return http.requestJson("POST", "/api/ee/library", { schema: CollectionRef });
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
list,
|
|
1149
|
+
create,
|
|
1150
|
+
createLibrary
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1059
1153
|
//#endregion
|
|
1060
1154
|
//#region src/metabase/domain/database.ts
|
|
1061
1155
|
const InitialSyncStatus = z.enum([
|
|
@@ -1148,7 +1242,9 @@ function permissionsGroupResource(http) {
|
|
|
1148
1242
|
}
|
|
1149
1243
|
return { list };
|
|
1150
1244
|
}
|
|
1151
|
-
|
|
1245
|
+
//#endregion
|
|
1246
|
+
//#region src/metabase/domain/remote-sync.ts
|
|
1247
|
+
const RemoteSyncType = z.enum(["read-only", "read-write"]);
|
|
1152
1248
|
const RemoteSyncSettingsResult = z.object({
|
|
1153
1249
|
success: z.boolean(),
|
|
1154
1250
|
task_id: z.int().optional()
|
|
@@ -1169,6 +1265,38 @@ const SyncTask = z.object({
|
|
|
1169
1265
|
status: SyncTaskStatus$1,
|
|
1170
1266
|
error_message: z.string().nullable().optional()
|
|
1171
1267
|
}).loose();
|
|
1268
|
+
const NamedCollection = z.object({
|
|
1269
|
+
id: z.int(),
|
|
1270
|
+
name: z.string()
|
|
1271
|
+
}).loose();
|
|
1272
|
+
const DependencyLocation = NamedCollection.nullable().optional();
|
|
1273
|
+
const DependencyItem = z.object({
|
|
1274
|
+
model: z.string(),
|
|
1275
|
+
id: z.int(),
|
|
1276
|
+
name: z.string().nullable()
|
|
1277
|
+
}).loose();
|
|
1278
|
+
const UnsyncedDependency = DependencyItem.extend({
|
|
1279
|
+
collection: DependencyLocation,
|
|
1280
|
+
used_by: z.array(DependencyItem)
|
|
1281
|
+
});
|
|
1282
|
+
const SyncRemedy = z.discriminatedUnion("type", [
|
|
1283
|
+
z.object({
|
|
1284
|
+
type: z.literal("collection"),
|
|
1285
|
+
collection: NamedCollection
|
|
1286
|
+
}).loose(),
|
|
1287
|
+
z.object({ type: z.literal("library") }).loose(),
|
|
1288
|
+
z.object({
|
|
1289
|
+
type: z.literal("none"),
|
|
1290
|
+
collection: DependencyLocation
|
|
1291
|
+
}).loose()
|
|
1292
|
+
]);
|
|
1293
|
+
const RequiredSync = z.object({
|
|
1294
|
+
remedy: SyncRemedy,
|
|
1295
|
+
syncable: z.boolean(),
|
|
1296
|
+
blocks: z.array(NamedCollection),
|
|
1297
|
+
dependencies: z.array(UnsyncedDependency)
|
|
1298
|
+
}).loose();
|
|
1299
|
+
const UnsyncedDependencies = z.object({ errors: z.object({ required: z.array(RequiredSync) }).loose() }).loose();
|
|
1172
1300
|
//#endregion
|
|
1173
1301
|
//#region src/metabase/resources/remote-sync.ts
|
|
1174
1302
|
function remoteSyncResource(http) {
|
|
@@ -1179,6 +1307,13 @@ function remoteSyncResource(http) {
|
|
|
1179
1307
|
schema: RemoteSyncSettingsResult
|
|
1180
1308
|
});
|
|
1181
1309
|
}
|
|
1310
|
+
/** PUT /api/ee/remote-sync/settings, without the git settings, which it leaves as they are */
|
|
1311
|
+
async function updateScope(scope) {
|
|
1312
|
+
return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
|
|
1313
|
+
body: scope,
|
|
1314
|
+
schema: RemoteSyncSettingsResult
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1182
1317
|
/** PUT /api/ee/remote-sync/settings with only `remote-sync-transforms`, which skips the repository check. */
|
|
1183
1318
|
async function syncTransforms() {
|
|
1184
1319
|
return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
|
|
@@ -1195,10 +1330,11 @@ function remoteSyncResource(http) {
|
|
|
1195
1330
|
}
|
|
1196
1331
|
/** GET /api/ee/remote-sync/current-task */
|
|
1197
1332
|
async function currentTask() {
|
|
1198
|
-
return http.
|
|
1333
|
+
return http.requestOptionalJson("GET", "/api/ee/remote-sync/current-task", { schema: SyncTask });
|
|
1199
1334
|
}
|
|
1200
1335
|
return {
|
|
1201
1336
|
updateSettings,
|
|
1337
|
+
updateScope,
|
|
1202
1338
|
syncTransforms,
|
|
1203
1339
|
testConnection,
|
|
1204
1340
|
currentTask
|
|
@@ -1271,6 +1407,7 @@ const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
|
|
|
1271
1407
|
const ENGINES_SETTING = "engines";
|
|
1272
1408
|
const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
|
|
1273
1409
|
const TRANSFORMS_SETUP_COMPLETE_SETTING = "transforms-setup-complete";
|
|
1410
|
+
const REMOTE_SYNC_TYPE_SETTING = "remote-sync-type";
|
|
1274
1411
|
function settingResource(http) {
|
|
1275
1412
|
/** PUT /api/setting/premium-embedding-token */
|
|
1276
1413
|
async function setPremiumEmbeddingToken(value) {
|
|
@@ -1288,11 +1425,16 @@ function settingResource(http) {
|
|
|
1288
1425
|
async function engines() {
|
|
1289
1426
|
return http.requestJson("GET", `/api/setting/${ENGINES_SETTING}`, { schema: Engines });
|
|
1290
1427
|
}
|
|
1428
|
+
/** GET /api/setting/remote-sync-type; Metabase answers its default, read-only, while unset. */
|
|
1429
|
+
async function remoteSyncType() {
|
|
1430
|
+
return http.requestText("GET", `/api/setting/${REMOTE_SYNC_TYPE_SETTING}`, { schema: RemoteSyncType });
|
|
1431
|
+
}
|
|
1291
1432
|
return {
|
|
1292
1433
|
setPremiumEmbeddingToken,
|
|
1293
1434
|
enableTransforms,
|
|
1294
1435
|
transformsDecided,
|
|
1295
|
-
engines
|
|
1436
|
+
engines,
|
|
1437
|
+
remoteSyncType
|
|
1296
1438
|
};
|
|
1297
1439
|
}
|
|
1298
1440
|
//#endregion
|
|
@@ -1362,6 +1504,7 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
|
1362
1504
|
tokenStatus: tokenStatusResource(http),
|
|
1363
1505
|
database: databaseResource(http),
|
|
1364
1506
|
remoteSync: remoteSyncResource(http),
|
|
1507
|
+
collection: collectionResource(http),
|
|
1365
1508
|
user: userResource(http)
|
|
1366
1509
|
};
|
|
1367
1510
|
}
|
|
@@ -2904,12 +3047,12 @@ async function portClaims(state, runtime) {
|
|
|
2904
3047
|
}
|
|
2905
3048
|
//#endregion
|
|
2906
3049
|
//#region src/steps/admin-user.ts
|
|
2907
|
-
const STEP_ID$
|
|
3050
|
+
const STEP_ID$9 = "admin-user";
|
|
2908
3051
|
const ADMIN_USER_KEYS = {
|
|
2909
|
-
email: `${STEP_ID$
|
|
2910
|
-
password: `${STEP_ID$
|
|
2911
|
-
firstName: `${STEP_ID$
|
|
2912
|
-
lastName: `${STEP_ID$
|
|
3052
|
+
email: `${STEP_ID$9}.email`,
|
|
3053
|
+
password: `${STEP_ID$9}.password`,
|
|
3054
|
+
firstName: `${STEP_ID$9}.first-name`,
|
|
3055
|
+
lastName: `${STEP_ID$9}.last-name`
|
|
2913
3056
|
};
|
|
2914
3057
|
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
2915
3058
|
const GENERATED_PASSWORD_BYTES = 24;
|
|
@@ -2924,7 +3067,7 @@ function generatePassword(random) {
|
|
|
2924
3067
|
if (DIGIT.test(candidate)) return candidate;
|
|
2925
3068
|
}
|
|
2926
3069
|
}
|
|
2927
|
-
function hostDeps$
|
|
3070
|
+
function hostDeps$14(ctx) {
|
|
2928
3071
|
return {
|
|
2929
3072
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
2930
3073
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
@@ -3062,9 +3205,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
|
|
|
3062
3205
|
ctx.ui.success(`Admin user ${email} recorded`);
|
|
3063
3206
|
return "completed";
|
|
3064
3207
|
}
|
|
3065
|
-
function adminUserStep(options, deps = hostDeps$
|
|
3208
|
+
function adminUserStep(options, deps = hostDeps$14) {
|
|
3066
3209
|
return {
|
|
3067
|
-
id: STEP_ID$
|
|
3210
|
+
id: STEP_ID$9,
|
|
3068
3211
|
title: "Create the admin user",
|
|
3069
3212
|
redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
|
|
3070
3213
|
async detect(ctx) {
|
|
@@ -3329,10 +3472,15 @@ const GitSyncTask = z.object({
|
|
|
3329
3472
|
progress: z.number().nullable(),
|
|
3330
3473
|
error_message: z.string().nullable().optional()
|
|
3331
3474
|
}).loose();
|
|
3475
|
+
const SyncedCollection = z.object({
|
|
3476
|
+
id: z.int(),
|
|
3477
|
+
name: z.string()
|
|
3478
|
+
}).loose();
|
|
3332
3479
|
const GitSyncStatus = z.object({
|
|
3333
3480
|
branch: z.string().nullable(),
|
|
3334
3481
|
is_dirty: z.boolean(),
|
|
3335
|
-
current_task: GitSyncTask.nullable()
|
|
3482
|
+
current_task: GitSyncTask.nullable(),
|
|
3483
|
+
synced_collections: z.array(SyncedCollection)
|
|
3336
3484
|
}).loose();
|
|
3337
3485
|
const GitSyncOutcome = z.object({
|
|
3338
3486
|
message: z.string().nullable(),
|
|
@@ -3344,6 +3492,9 @@ const RemoteChanges = z.object({
|
|
|
3344
3492
|
remote_version: z.string().nullable(),
|
|
3345
3493
|
local_version: z.string().nullable()
|
|
3346
3494
|
}).loose();
|
|
3495
|
+
function remoteMoved(changes) {
|
|
3496
|
+
return changes.has_changes && changes.local_version !== null;
|
|
3497
|
+
}
|
|
3347
3498
|
function comparableUrl(url) {
|
|
3348
3499
|
const parsed = new URL(url);
|
|
3349
3500
|
return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
@@ -3479,14 +3630,14 @@ function mbTool(runner) {
|
|
|
3479
3630
|
}
|
|
3480
3631
|
//#endregion
|
|
3481
3632
|
//#region src/steps/api-key.ts
|
|
3482
|
-
const STEP_ID$
|
|
3633
|
+
const STEP_ID$8 = "api-key";
|
|
3483
3634
|
const API_KEY_KEYS = {
|
|
3484
|
-
email: `${STEP_ID$
|
|
3485
|
-
password: `${STEP_ID$
|
|
3486
|
-
profile: `${STEP_ID$
|
|
3635
|
+
email: `${STEP_ID$8}.email`,
|
|
3636
|
+
password: `${STEP_ID$8}.password`,
|
|
3637
|
+
profile: `${STEP_ID$8}.profile`
|
|
3487
3638
|
};
|
|
3488
3639
|
const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
3489
|
-
function hostDeps$
|
|
3640
|
+
function hostDeps$13(ctx) {
|
|
3490
3641
|
return {
|
|
3491
3642
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3492
3643
|
runner: processRunner(ctx.signal),
|
|
@@ -3547,7 +3698,7 @@ async function authenticates(client) {
|
|
|
3547
3698
|
}
|
|
3548
3699
|
}
|
|
3549
3700
|
async function forgetCompletion(ctx) {
|
|
3550
|
-
const { [STEP_ID$
|
|
3701
|
+
const { [STEP_ID$8]: record, ...steps } = ctx.state.steps;
|
|
3551
3702
|
if (record !== void 0) await ctx.saveState({
|
|
3552
3703
|
...ctx.state,
|
|
3553
3704
|
steps
|
|
@@ -3624,15 +3775,15 @@ function usageNote(profile) {
|
|
|
3624
3775
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
3625
3776
|
return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
|
|
3626
3777
|
}
|
|
3627
|
-
function apiKeyStep(deps = hostDeps$
|
|
3778
|
+
function apiKeyStep(deps = hostDeps$13) {
|
|
3628
3779
|
return {
|
|
3629
|
-
id: STEP_ID$
|
|
3780
|
+
id: STEP_ID$8,
|
|
3630
3781
|
title: "Connect the Metabase CLI",
|
|
3631
3782
|
redoHint: "`rde status` shows the mb profile it uses.",
|
|
3632
3783
|
async detect(ctx) {
|
|
3633
3784
|
const configured = configuredRuntime(ctx.state);
|
|
3634
3785
|
const apiKey = ctx.secrets.apiKey;
|
|
3635
|
-
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$
|
|
3786
|
+
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
|
|
3636
3787
|
const resolved = deps(ctx);
|
|
3637
3788
|
if (!await resolved.instanceRunning()) return "todo";
|
|
3638
3789
|
if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
|
|
@@ -5578,13 +5729,13 @@ function gitTool(runner) {
|
|
|
5578
5729
|
}
|
|
5579
5730
|
//#endregion
|
|
5580
5731
|
//#region src/steps/git-sync.ts
|
|
5581
|
-
const STEP_ID$
|
|
5732
|
+
const STEP_ID$7 = "git-sync";
|
|
5582
5733
|
const GIT_SYNC_KEYS = {
|
|
5583
|
-
location: `${STEP_ID$
|
|
5584
|
-
path: `${STEP_ID$
|
|
5585
|
-
remote: `${STEP_ID$
|
|
5586
|
-
url: `${STEP_ID$
|
|
5587
|
-
token: `${STEP_ID$
|
|
5734
|
+
location: `${STEP_ID$7}.location`,
|
|
5735
|
+
path: `${STEP_ID$7}.path`,
|
|
5736
|
+
remote: `${STEP_ID$7}.remote`,
|
|
5737
|
+
url: `${STEP_ID$7}.url`,
|
|
5738
|
+
token: `${STEP_ID$7}.token`
|
|
5588
5739
|
};
|
|
5589
5740
|
const BARE_REPO_NAME = "content.git";
|
|
5590
5741
|
const DEFAULT_BRANCH = "main";
|
|
@@ -5618,7 +5769,7 @@ const SYNC_MODE_SETTINGS = {
|
|
|
5618
5769
|
"remote-sync-type": "read-write",
|
|
5619
5770
|
"remote-sync-transforms": true
|
|
5620
5771
|
};
|
|
5621
|
-
function hostDeps$
|
|
5772
|
+
function hostDeps$12(ctx) {
|
|
5622
5773
|
return {
|
|
5623
5774
|
runner: processRunner(ctx.signal),
|
|
5624
5775
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -6088,15 +6239,15 @@ async function runGitSync(ctx, deps) {
|
|
|
6088
6239
|
ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
6089
6240
|
return "completed";
|
|
6090
6241
|
}
|
|
6091
|
-
function gitSyncStep(deps = hostDeps$
|
|
6242
|
+
function gitSyncStep(deps = hostDeps$12) {
|
|
6092
6243
|
return {
|
|
6093
|
-
id: STEP_ID$
|
|
6244
|
+
id: STEP_ID$7,
|
|
6094
6245
|
title: "Connect a git repository",
|
|
6095
6246
|
redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
|
|
6096
6247
|
async detect(ctx) {
|
|
6097
6248
|
if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
|
|
6098
|
-
if (ctx.state.steps[STEP_ID$
|
|
6099
|
-
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$
|
|
6249
|
+
if (ctx.state.steps[STEP_ID$7]?.status === "skipped") return "not-applicable";
|
|
6250
|
+
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
|
|
6100
6251
|
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6101
6252
|
if (instance === null) return "todo";
|
|
6102
6253
|
try {
|
|
@@ -6278,12 +6429,12 @@ function skillsCli(runner) {
|
|
|
6278
6429
|
}
|
|
6279
6430
|
//#endregion
|
|
6280
6431
|
//#region src/steps/agents-skills.ts
|
|
6281
|
-
const STEP_ID$
|
|
6282
|
-
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$
|
|
6432
|
+
const STEP_ID$6 = "agents-skills";
|
|
6433
|
+
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$6}.agents` };
|
|
6283
6434
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6284
6435
|
const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
|
|
6285
6436
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6286
|
-
function hostDeps$
|
|
6437
|
+
function hostDeps$11(ctx) {
|
|
6287
6438
|
return {
|
|
6288
6439
|
runner: processRunner(ctx.signal),
|
|
6289
6440
|
probe: hostAgentProbe(),
|
|
@@ -6462,11 +6613,11 @@ function skipLine(all, source) {
|
|
|
6462
6613
|
switch (source) {
|
|
6463
6614
|
case "override":
|
|
6464
6615
|
case "chosen":
|
|
6465
|
-
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$
|
|
6466
|
-
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$
|
|
6616
|
+
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$6}\` to install it later`;
|
|
6617
|
+
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$6} --agents <name>\` once one is installed`;
|
|
6467
6618
|
}
|
|
6468
6619
|
}
|
|
6469
|
-
function agentsSkillsStep(options, updates, deps = hostDeps$
|
|
6620
|
+
function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
6470
6621
|
let hashes = null;
|
|
6471
6622
|
async function setCheck(resolved) {
|
|
6472
6623
|
hashes ??= hashLookup(resolved.github);
|
|
@@ -6477,13 +6628,13 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
|
6477
6628
|
};
|
|
6478
6629
|
}
|
|
6479
6630
|
return {
|
|
6480
|
-
id: STEP_ID$
|
|
6631
|
+
id: STEP_ID$6,
|
|
6481
6632
|
title: "Install the rde and data-app skills for your coding agents",
|
|
6482
6633
|
redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
|
|
6483
6634
|
async detect(ctx) {
|
|
6484
6635
|
const resolved = deps(ctx);
|
|
6485
6636
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6486
|
-
if (detected.length === 0) return ctx.state.steps[STEP_ID$
|
|
6637
|
+
if (detected.length === 0) return ctx.state.steps[STEP_ID$6]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6487
6638
|
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6488
6639
|
const check = await setCheck(resolved);
|
|
6489
6640
|
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
@@ -6640,13 +6791,13 @@ async function connectRequired(ctx, deps, opening) {
|
|
|
6640
6791
|
result: { addedDatabases: connected.added }
|
|
6641
6792
|
};
|
|
6642
6793
|
}
|
|
6643
|
-
function hostDeps$
|
|
6794
|
+
function hostDeps$10(ctx) {
|
|
6644
6795
|
return {
|
|
6645
6796
|
...hostDatabasesDeps(ctx.signal),
|
|
6646
6797
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
6647
6798
|
};
|
|
6648
6799
|
}
|
|
6649
|
-
function databasesStep(options, deps = hostDeps$
|
|
6800
|
+
function databasesStep(options, deps = hostDeps$10) {
|
|
6650
6801
|
return {
|
|
6651
6802
|
id: "databases",
|
|
6652
6803
|
title: "Connect your databases",
|
|
@@ -6709,7 +6860,7 @@ function npmRegistry(signal, options = {}) {
|
|
|
6709
6860
|
}
|
|
6710
6861
|
//#endregion
|
|
6711
6862
|
//#region src/steps/install-mb.ts
|
|
6712
|
-
const STEP_ID$
|
|
6863
|
+
const STEP_ID$5 = "install-mb";
|
|
6713
6864
|
const MIN_MB_VERSION = "0.3.0";
|
|
6714
6865
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
6715
6866
|
function installedWith(scriptPath) {
|
|
@@ -6718,7 +6869,7 @@ function installedWith(scriptPath) {
|
|
|
6718
6869
|
function rdeInstalledWith() {
|
|
6719
6870
|
return installedWith(process.argv[1]);
|
|
6720
6871
|
}
|
|
6721
|
-
function hostDeps$
|
|
6872
|
+
function hostDeps$9(ctx) {
|
|
6722
6873
|
return {
|
|
6723
6874
|
runner: processRunner(ctx.signal),
|
|
6724
6875
|
installedWith: rdeInstalledWith(),
|
|
@@ -6885,7 +7036,7 @@ async function updateFor(release, installed, deps) {
|
|
|
6885
7036
|
version
|
|
6886
7037
|
};
|
|
6887
7038
|
}
|
|
6888
|
-
function installMbStep(updates, deps = hostDeps$
|
|
7039
|
+
function installMbStep(updates, deps = hostDeps$9) {
|
|
6889
7040
|
let published = null;
|
|
6890
7041
|
function release(resolved) {
|
|
6891
7042
|
if (updates === "installed") return Promise.resolve(UNCHECKED);
|
|
@@ -6893,7 +7044,7 @@ function installMbStep(updates, deps = hostDeps$8) {
|
|
|
6893
7044
|
return published;
|
|
6894
7045
|
}
|
|
6895
7046
|
return {
|
|
6896
|
-
id: STEP_ID$
|
|
7047
|
+
id: STEP_ID$5,
|
|
6897
7048
|
title: "Install the Metabase CLI",
|
|
6898
7049
|
async detect(ctx) {
|
|
6899
7050
|
const resolved = deps(ctx);
|
|
@@ -6935,8 +7086,8 @@ function installMbStep(updates, deps = hostDeps$8) {
|
|
|
6935
7086
|
}
|
|
6936
7087
|
//#endregion
|
|
6937
7088
|
//#region src/steps/license.ts
|
|
6938
|
-
const STEP_ID$
|
|
6939
|
-
const LICENSE_KEYS = { token: `${STEP_ID$
|
|
7089
|
+
const STEP_ID$4 = "license";
|
|
7090
|
+
const LICENSE_KEYS = { token: `${STEP_ID$4}.token` };
|
|
6940
7091
|
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
6941
7092
|
const NOT_FOUND_STATUS$1 = 404;
|
|
6942
7093
|
const TOKEN_REFUSED_STATUS = 400;
|
|
@@ -6959,7 +7110,7 @@ function licensedFeatureCoverage(features) {
|
|
|
6959
7110
|
function listOf(names, type) {
|
|
6960
7111
|
return new Intl.ListFormat("en", { type }).format(names);
|
|
6961
7112
|
}
|
|
6962
|
-
function hostDeps$
|
|
7113
|
+
function hostDeps$8(ctx) {
|
|
6963
7114
|
return {
|
|
6964
7115
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
6965
7116
|
runner: processRunner(ctx.signal),
|
|
@@ -7044,13 +7195,13 @@ function reportFeatures(ctx, features) {
|
|
|
7044
7195
|
ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
|
|
7045
7196
|
}
|
|
7046
7197
|
}
|
|
7047
|
-
function licenseStep(deps = hostDeps$
|
|
7198
|
+
function licenseStep(deps = hostDeps$8) {
|
|
7048
7199
|
return {
|
|
7049
|
-
id: STEP_ID$
|
|
7200
|
+
id: STEP_ID$4,
|
|
7050
7201
|
title: "Activate your license",
|
|
7051
7202
|
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
7052
7203
|
async detect(ctx) {
|
|
7053
|
-
if (ctx.state.steps[STEP_ID$
|
|
7204
|
+
if (ctx.state.steps[STEP_ID$4]?.status === "skipped") return "not-applicable";
|
|
7054
7205
|
const configured = configuredRuntime(ctx.state);
|
|
7055
7206
|
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7056
7207
|
const resolved = deps(ctx);
|
|
@@ -7122,7 +7273,7 @@ const PLATFORMS = {
|
|
|
7122
7273
|
javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
|
|
7123
7274
|
}
|
|
7124
7275
|
};
|
|
7125
|
-
function hostDeps$
|
|
7276
|
+
function hostDeps$7(ctx) {
|
|
7126
7277
|
const runner = processRunner(ctx.signal);
|
|
7127
7278
|
return {
|
|
7128
7279
|
platform: process.platform,
|
|
@@ -7244,7 +7395,7 @@ function checkLine(check) {
|
|
|
7244
7395
|
function checkLines(checks) {
|
|
7245
7396
|
return checks.map(checkLine);
|
|
7246
7397
|
}
|
|
7247
|
-
function preflightStep(deps = hostDeps$
|
|
7398
|
+
function preflightStep(deps = hostDeps$7) {
|
|
7248
7399
|
return {
|
|
7249
7400
|
id: "preflight",
|
|
7250
7401
|
title: "Check this machine",
|
|
@@ -7263,7 +7414,7 @@ function preflightStep(deps = hostDeps$6) {
|
|
|
7263
7414
|
}
|
|
7264
7415
|
//#endregion
|
|
7265
7416
|
//#region src/steps/runtime.ts
|
|
7266
|
-
const STEP_ID$
|
|
7417
|
+
const STEP_ID$3 = "runtime";
|
|
7267
7418
|
const MIN_PORT = 1;
|
|
7268
7419
|
const MAX_PORT = 65535;
|
|
7269
7420
|
const PORT_PATTERN = /^\d+$/;
|
|
@@ -7277,7 +7428,7 @@ const NO_RUNTIME_FLAGS = {
|
|
|
7277
7428
|
jar: null,
|
|
7278
7429
|
port: null
|
|
7279
7430
|
};
|
|
7280
|
-
function hostDeps$
|
|
7431
|
+
function hostDeps$6(ctx) {
|
|
7281
7432
|
return {
|
|
7282
7433
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7283
7434
|
releases: releaseLookup(ctx.signal)
|
|
@@ -7406,9 +7557,9 @@ async function persistedRuntime(ctx, options, deps) {
|
|
|
7406
7557
|
});
|
|
7407
7558
|
return chosen;
|
|
7408
7559
|
}
|
|
7409
|
-
function runtimeStep(options, deps = hostDeps$
|
|
7560
|
+
function runtimeStep(options, deps = hostDeps$6) {
|
|
7410
7561
|
return {
|
|
7411
|
-
id: STEP_ID$
|
|
7562
|
+
id: STEP_ID$3,
|
|
7412
7563
|
title: "Install Metabase",
|
|
7413
7564
|
redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
|
|
7414
7565
|
async detect(ctx) {
|
|
@@ -7437,7 +7588,7 @@ const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
|
|
|
7437
7588
|
//#region src/steps/start-metabase.ts
|
|
7438
7589
|
const LOG_TAIL_ON_TIMEOUT = 30;
|
|
7439
7590
|
const PERCENT = 100;
|
|
7440
|
-
function hostDeps$
|
|
7591
|
+
function hostDeps$5(ctx) {
|
|
7441
7592
|
return {
|
|
7442
7593
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7443
7594
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -7466,7 +7617,7 @@ async function startRuntime(ctx, runtime, deps) {
|
|
|
7466
7617
|
throw error;
|
|
7467
7618
|
}
|
|
7468
7619
|
}
|
|
7469
|
-
function startMetabaseStep(deps = hostDeps$
|
|
7620
|
+
function startMetabaseStep(deps = hostDeps$5) {
|
|
7470
7621
|
return {
|
|
7471
7622
|
id: "start-metabase",
|
|
7472
7623
|
title: "Start Metabase",
|
|
@@ -7524,6 +7675,261 @@ function startMetabaseStep(deps = hostDeps$4) {
|
|
|
7524
7675
|
};
|
|
7525
7676
|
}
|
|
7526
7677
|
//#endregion
|
|
7678
|
+
//#region src/steps/sync-scope.ts
|
|
7679
|
+
const STEP_ID$2 = "sync-scope";
|
|
7680
|
+
const SYNC_SCOPE_KEYS = { export: `${STEP_ID$2}.export` };
|
|
7681
|
+
const AGENT_COLLECTION_NAME = "Analytics";
|
|
7682
|
+
const LIBRARY_NAME = "Library";
|
|
7683
|
+
const ROOT_COLLECTION_NAME = "Our analytics";
|
|
7684
|
+
const AGENT_COLLECTION_DESCRIPTION = "Dashboards, questions, models, and documents that reach git on export.";
|
|
7685
|
+
function hostDeps$4(ctx) {
|
|
7686
|
+
return {
|
|
7687
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7688
|
+
runner: processRunner(ctx.signal),
|
|
7689
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
7690
|
+
};
|
|
7691
|
+
}
|
|
7692
|
+
function isCollection(entry) {
|
|
7693
|
+
return typeof entry.id === "number";
|
|
7694
|
+
}
|
|
7695
|
+
function isOrdinary(collection) {
|
|
7696
|
+
return collection.type === null && collection.namespace === null && collection.personal_owner_id === null;
|
|
7697
|
+
}
|
|
7698
|
+
function readScope(list, syncRoot) {
|
|
7699
|
+
const topLevel = list.filter(isCollection).filter((collection) => collection.location === "/");
|
|
7700
|
+
const ordinary = topLevel.filter(isOrdinary);
|
|
7701
|
+
const unsynced = ordinary.filter((collection) => !collection.is_remote_synced);
|
|
7702
|
+
return {
|
|
7703
|
+
library: topLevel.find((collection) => collection.type === "library") ?? null,
|
|
7704
|
+
syncedRoots: ordinary.filter((collection) => collection.is_remote_synced),
|
|
7705
|
+
unsyncedRecordedRoot: unsynced.find((collection) => collection.id === syncRoot) ?? null,
|
|
7706
|
+
foreignNamesake: unsynced.some((collection) => collection.id !== syncRoot && collection.name === "Analytics")
|
|
7707
|
+
};
|
|
7708
|
+
}
|
|
7709
|
+
function isComplete(scope, libraryLicensed) {
|
|
7710
|
+
return (!libraryLicensed || scope.library !== null && scope.library.is_remote_synced) && scope.syncedRoots.length > 0;
|
|
7711
|
+
}
|
|
7712
|
+
function isNotAnsweringYet(error) {
|
|
7713
|
+
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || isRejectedCredential(error);
|
|
7714
|
+
}
|
|
7715
|
+
function requireMbInstance(ctx) {
|
|
7716
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
7717
|
+
if (instance === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
7718
|
+
return instance;
|
|
7719
|
+
}
|
|
7720
|
+
function requireGitRemote(ctx) {
|
|
7721
|
+
if (ctx.state.gitRemote === null) throw new ConfigError("no repository yet; the git-sync step comes first");
|
|
7722
|
+
return ctx.state.gitRemote;
|
|
7723
|
+
}
|
|
7724
|
+
async function requireSettledImport(ctx, client, gitRemote) {
|
|
7725
|
+
const task = await client.remoteSync.currentTask();
|
|
7726
|
+
if (task === null) return;
|
|
7727
|
+
if (task.status === "running") throw new ConfigError(`a remote sync ${task.sync_task_type} is running; run \`rde init --only ${STEP_ID$2}\` once it finishes`);
|
|
7728
|
+
if (task.sync_task_type === "import" && task.status !== "successful") {
|
|
7729
|
+
const importCommand = `${mbCommand(ctx.state.mbProfile)} git-sync import --branch ${shellWord(gitRemote.branch)}`;
|
|
7730
|
+
throw new ConfigError(`the last import from the repository ended ${task.status}; run \`${importCommand}\`, then \`rde init --only ${STEP_ID$2}\``);
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
async function isReadOnly(client) {
|
|
7734
|
+
return await client.setting.remoteSyncType() === "read-only";
|
|
7735
|
+
}
|
|
7736
|
+
async function requireReadWrite(client) {
|
|
7737
|
+
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}\``);
|
|
7738
|
+
}
|
|
7739
|
+
async function ensureLibrary(client, scope, libraryLicensed) {
|
|
7740
|
+
if (!libraryLicensed) return null;
|
|
7741
|
+
return scope.library ?? client.collection.createLibrary();
|
|
7742
|
+
}
|
|
7743
|
+
async function ensureRoots(ctx, client, scope) {
|
|
7744
|
+
if (scope.syncedRoots.length > 0) return {
|
|
7745
|
+
roots: scope.syncedRoots,
|
|
7746
|
+
created: false,
|
|
7747
|
+
besideNamesake: false
|
|
7748
|
+
};
|
|
7749
|
+
if (scope.unsyncedRecordedRoot !== null) return {
|
|
7750
|
+
roots: [scope.unsyncedRecordedRoot],
|
|
7751
|
+
created: false,
|
|
7752
|
+
besideNamesake: false
|
|
7753
|
+
};
|
|
7754
|
+
const created = await client.collection.create({
|
|
7755
|
+
name: AGENT_COLLECTION_NAME,
|
|
7756
|
+
description: AGENT_COLLECTION_DESCRIPTION
|
|
7757
|
+
});
|
|
7758
|
+
await ctx.saveState({
|
|
7759
|
+
...ctx.state,
|
|
7760
|
+
syncRoot: created.id
|
|
7761
|
+
});
|
|
7762
|
+
return {
|
|
7763
|
+
roots: [created],
|
|
7764
|
+
created: true,
|
|
7765
|
+
besideNamesake: scope.foreignNamesake
|
|
7766
|
+
};
|
|
7767
|
+
}
|
|
7768
|
+
function itemLabel(item) {
|
|
7769
|
+
return item.name === null ? `${item.model} ${item.id}` : `${item.model} ${item.id} "${item.name}"`;
|
|
7770
|
+
}
|
|
7771
|
+
function locationLabel(location) {
|
|
7772
|
+
if (location === void 0) return "in a collection Metabase could not find";
|
|
7773
|
+
return `in ${location === null ? ROOT_COLLECTION_NAME : location.name}`;
|
|
7774
|
+
}
|
|
7775
|
+
function dependencyLine(dependency) {
|
|
7776
|
+
const line = ` ${itemLabel(dependency)} ${locationLabel(dependency.collection)}`;
|
|
7777
|
+
if (dependency.used_by.length === 0) return line;
|
|
7778
|
+
return `${line}, used by ${dependency.used_by.map(itemLabel).join(", ")}`;
|
|
7779
|
+
}
|
|
7780
|
+
function remedyLine(required) {
|
|
7781
|
+
const remedy = required.remedy;
|
|
7782
|
+
switch (remedy.type) {
|
|
7783
|
+
case "collection":
|
|
7784
|
+
if (required.syncable) return ` Fix: sync ${remedy.collection.name} too, or move that content into a synced collection.`;
|
|
7785
|
+
return ` Fix: move that content into a synced collection; ${remedy.collection.name} cannot sync.`;
|
|
7786
|
+
case "library": return " Fix: stop using that content; it syncs only through a Library, and this instance has none.";
|
|
7787
|
+
case "none": return " Fix: move that content into a synced collection, or stop using it.";
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
function requiredSyncLines(required) {
|
|
7791
|
+
return [
|
|
7792
|
+
`Metabase will not sync ${required.blocks.map((collection) => collection.name).join(", ")}, because content there uses:`,
|
|
7793
|
+
...required.dependencies.map(dependencyLine),
|
|
7794
|
+
remedyLine(required)
|
|
7795
|
+
];
|
|
7796
|
+
}
|
|
7797
|
+
function explainRefusal(error) {
|
|
7798
|
+
if (!(error instanceof MetabaseApiError) || error.code !== "unsynced-dependencies") return error;
|
|
7799
|
+
const refusal = UnsyncedDependencies.safeParse(error.detail);
|
|
7800
|
+
if (!refusal.success) return error;
|
|
7801
|
+
return new ConfigError([...refusal.data.errors.required.flatMap(requiredSyncLines), `After the fix, run \`rde init --only ${STEP_ID$2}\`.`].join("\n"));
|
|
7802
|
+
}
|
|
7803
|
+
function plannedNames(scope, libraryLicensed) {
|
|
7804
|
+
const library = libraryLicensed ? [scope.library?.name ?? LIBRARY_NAME] : [];
|
|
7805
|
+
if (scope.syncedRoots.length > 0) return [...library, ...scope.syncedRoots.map((collection) => collection.name)];
|
|
7806
|
+
return [...library, scope.unsyncedRecordedRoot?.name ?? "Analytics"];
|
|
7807
|
+
}
|
|
7808
|
+
function nameList(names) {
|
|
7809
|
+
if (names.length <= 2) return names.join(" and ");
|
|
7810
|
+
return `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
|
|
7811
|
+
}
|
|
7812
|
+
function exportAdvice(profile, branch, changes) {
|
|
7813
|
+
const command = exportCommand(profile, branch);
|
|
7814
|
+
if (!remoteMoved(changes)) return `\`${command}\``;
|
|
7815
|
+
return `the branch has commits Metabase has not imported; \`${mbCommand(profile)} git-sync export-preflight\`, then \`${command} --merge\``;
|
|
7816
|
+
}
|
|
7817
|
+
async function planExport(ctx, runner, instance, names) {
|
|
7818
|
+
const changes = await mbTool(runner).gitSyncHasRemoteChanges(instance);
|
|
7819
|
+
if (remoteMoved(changes)) return {
|
|
7820
|
+
changes,
|
|
7821
|
+
push: false
|
|
7822
|
+
};
|
|
7823
|
+
return {
|
|
7824
|
+
changes,
|
|
7825
|
+
push: await ctx.prompts.confirm({
|
|
7826
|
+
key: SYNC_SCOPE_KEYS.export,
|
|
7827
|
+
message: `Push ${nameList(names)} to ${requireGitRemote(ctx).branch} now?`,
|
|
7828
|
+
initialValue: true
|
|
7829
|
+
})
|
|
7830
|
+
};
|
|
7831
|
+
}
|
|
7832
|
+
function describeExport(task) {
|
|
7833
|
+
const ended = `the export ended ${task.status}`;
|
|
7834
|
+
return task.error_message === null || task.error_message === void 0 ? ended : `${ended}: ${task.error_message}`;
|
|
7835
|
+
}
|
|
7836
|
+
function reportPending(run) {
|
|
7837
|
+
const advice = exportAdvice(run.ctx.state.mbProfile, run.gitRemote.branch, run.changes);
|
|
7838
|
+
run.ctx.ui.info(`First export pending: ${advice}`);
|
|
7839
|
+
}
|
|
7840
|
+
async function exportScope(run, runner) {
|
|
7841
|
+
const mb = mbTool(runner);
|
|
7842
|
+
const label = nameList(run.names);
|
|
7843
|
+
const spinner = run.ctx.ui.spinner(`Pushing ${label} to ${run.gitRemote.branch}…`);
|
|
7844
|
+
try {
|
|
7845
|
+
const final = (await mb.gitSyncExport(run.instance, {
|
|
7846
|
+
branch: run.gitRemote.branch,
|
|
7847
|
+
message: `Sync ${label}`
|
|
7848
|
+
})).final ?? null;
|
|
7849
|
+
if (final !== null && final.status !== "successful") {
|
|
7850
|
+
spinner.fail(describeExport(final));
|
|
7851
|
+
reportPending(run);
|
|
7852
|
+
return;
|
|
7853
|
+
}
|
|
7854
|
+
const status = await mb.gitSyncStatus(run.instance);
|
|
7855
|
+
spinner.stop(`Pushed ${label} to ${run.gitRemote.branch}`);
|
|
7856
|
+
if (status.is_dirty) run.ctx.ui.warn(`Metabase still has changes the export did not carry; \`${mbCommand(run.ctx.state.mbProfile)} git-sync dirty\` lists them`);
|
|
7857
|
+
} catch (error) {
|
|
7858
|
+
if (!(error instanceof ExternalToolError)) throw error;
|
|
7859
|
+
spinner.fail(`Could not push ${label}: ${error.message}`);
|
|
7860
|
+
reportPending(run);
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7863
|
+
function scopeLabel(synced) {
|
|
7864
|
+
return `Synced to git: ${synced.map((collection) => collection.name).join(", ")}`;
|
|
7865
|
+
}
|
|
7866
|
+
function syncScopeStep(deps = hostDeps$4) {
|
|
7867
|
+
return {
|
|
7868
|
+
id: STEP_ID$2,
|
|
7869
|
+
title: "Choose what syncs to git",
|
|
7870
|
+
async detect(ctx) {
|
|
7871
|
+
if (!hasRemoteSync(ctx.state.license) || ctx.state.gitRemote === null) return "not-applicable";
|
|
7872
|
+
const configured = configuredRuntime(ctx.state);
|
|
7873
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7874
|
+
const resolved = deps(ctx);
|
|
7875
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
7876
|
+
const client = resolved.client(configured.url, {
|
|
7877
|
+
kind: "api-key",
|
|
7878
|
+
key: ctx.secrets.apiKey
|
|
7879
|
+
});
|
|
7880
|
+
try {
|
|
7881
|
+
if (isComplete(readScope(await client.collection.list(), ctx.state.syncRoot), hasLibrary(ctx.state.license))) return "done";
|
|
7882
|
+
return await isReadOnly(client) ? "not-applicable" : "todo";
|
|
7883
|
+
} catch (error) {
|
|
7884
|
+
if (isNotAnsweringYet(error)) return "todo";
|
|
7885
|
+
throw error;
|
|
7886
|
+
}
|
|
7887
|
+
},
|
|
7888
|
+
async run(ctx) {
|
|
7889
|
+
requireConfiguredRuntime(ctx.state);
|
|
7890
|
+
const gitRemote = requireGitRemote(ctx);
|
|
7891
|
+
const instance = requireMbInstance(ctx);
|
|
7892
|
+
const resolved = deps(ctx);
|
|
7893
|
+
const client = resolved.client(instance.url, {
|
|
7894
|
+
kind: "api-key",
|
|
7895
|
+
key: instance.apiKey
|
|
7896
|
+
});
|
|
7897
|
+
await requireReadWrite(client);
|
|
7898
|
+
await requireSettledImport(ctx, client, gitRemote);
|
|
7899
|
+
const libraryLicensed = hasLibrary(ctx.state.license);
|
|
7900
|
+
const scope = readScope(await client.collection.list(), ctx.state.syncRoot);
|
|
7901
|
+
const names = plannedNames(scope, libraryLicensed);
|
|
7902
|
+
const plan = isComplete(scope, libraryLicensed) ? null : await planExport(ctx, resolved.runner, instance, names);
|
|
7903
|
+
const spinner = ctx.ui.spinner("Choosing what syncs to git…");
|
|
7904
|
+
let synced;
|
|
7905
|
+
let chosen;
|
|
7906
|
+
try {
|
|
7907
|
+
const library = await ensureLibrary(client, scope, libraryLicensed);
|
|
7908
|
+
chosen = await ensureRoots(ctx, client, scope);
|
|
7909
|
+
synced = library === null ? chosen.roots : [library, ...chosen.roots];
|
|
7910
|
+
await client.remoteSync.updateScope({ collections: Object.fromEntries(synced.map((collection) => [collection.id, true])) });
|
|
7911
|
+
} catch (error) {
|
|
7912
|
+
spinner.fail("Could not choose what syncs to git");
|
|
7913
|
+
throw explainRefusal(error);
|
|
7914
|
+
}
|
|
7915
|
+
spinner.stop(scopeLabel(synced));
|
|
7916
|
+
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`);
|
|
7917
|
+
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`);
|
|
7918
|
+
if (plan === null) return { outcome: "completed" };
|
|
7919
|
+
const exportRun = {
|
|
7920
|
+
ctx,
|
|
7921
|
+
instance,
|
|
7922
|
+
gitRemote,
|
|
7923
|
+
names,
|
|
7924
|
+
changes: plan.changes
|
|
7925
|
+
};
|
|
7926
|
+
if (plan.push) await exportScope(exportRun, resolved.runner);
|
|
7927
|
+
else reportPending(exportRun);
|
|
7928
|
+
return { outcome: "completed" };
|
|
7929
|
+
}
|
|
7930
|
+
};
|
|
7931
|
+
}
|
|
7932
|
+
//#endregion
|
|
7527
7933
|
//#region src/steps/summary.ts
|
|
7528
7934
|
const STEP_ID$1 = "summary";
|
|
7529
7935
|
const GIT_SYNC_STEP_ID = "git-sync";
|
|
@@ -7581,6 +7987,19 @@ function gitLines(state) {
|
|
|
7581
7987
|
if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
|
|
7582
7988
|
return [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
|
|
7583
7989
|
}
|
|
7990
|
+
async function pendingExportLines(state, secrets, runner) {
|
|
7991
|
+
const instance = mbInstanceOf(state, secrets);
|
|
7992
|
+
if (state.gitRemote === null || instance === null) return [];
|
|
7993
|
+
try {
|
|
7994
|
+
const mb = mbTool(runner);
|
|
7995
|
+
if (!(await mb.gitSyncStatus(instance)).is_dirty) return [];
|
|
7996
|
+
const changes = await mb.gitSyncHasRemoteChanges(instance);
|
|
7997
|
+
return [`Export pending: ${exportAdvice(state.mbProfile, state.gitRemote.branch, changes)}`];
|
|
7998
|
+
} catch (error) {
|
|
7999
|
+
if (error instanceof ProcessNotFoundError || error instanceof ExternalToolError) return [`Remote sync: ${error.message}`];
|
|
8000
|
+
throw error;
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
7584
8003
|
function skillLine(entries, detected) {
|
|
7585
8004
|
const agents = [...new Set(entries.flatMap((entry) => coveredAgentNames(entry, detected)))];
|
|
7586
8005
|
if (agents.length === 0) return `rde skill: not installed; \`rde init --only agents-skills\``;
|
|
@@ -7592,14 +8011,15 @@ function launchLines(agents, workdir) {
|
|
|
7592
8011
|
const prefix = workdir === null ? "" : `cd ${workdir} && `;
|
|
7593
8012
|
return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
|
|
7594
8013
|
}
|
|
7595
|
-
async function summaryLines(state,
|
|
8014
|
+
async function summaryLines(state, secrets, deps) {
|
|
7596
8015
|
const profile = state.mbProfile;
|
|
7597
8016
|
const lines = [];
|
|
7598
8017
|
if (state.url !== null) lines.push(`Metabase: ${state.url}`);
|
|
7599
8018
|
if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
|
|
7600
8019
|
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
7601
|
-
lines.push(...await databaseLines$1(state, apiKey, deps));
|
|
8020
|
+
lines.push(...await databaseLines$1(state, secrets.apiKey, deps));
|
|
7602
8021
|
lines.push(...gitLines(state));
|
|
8022
|
+
lines.push(...await pendingExportLines(state, secrets, deps.runner));
|
|
7603
8023
|
const installed = await skillsCli(deps.runner).list();
|
|
7604
8024
|
const agents = await detectAgents(deps.probe);
|
|
7605
8025
|
const detected = agents.filter((agent) => agent.detected);
|
|
@@ -7616,7 +8036,7 @@ function summaryStep(deps = hostDeps$3) {
|
|
|
7616
8036
|
return "todo";
|
|
7617
8037
|
},
|
|
7618
8038
|
async run(ctx) {
|
|
7619
|
-
const lines = await summaryLines(ctx.state, ctx.secrets
|
|
8039
|
+
const lines = await summaryLines(ctx.state, ctx.secrets, deps(ctx));
|
|
7620
8040
|
ctx.ui.note(lines.join("\n"), "Your environment");
|
|
7621
8041
|
return { outcome: "completed" };
|
|
7622
8042
|
}
|
|
@@ -7693,6 +8113,7 @@ const STEP_BY_ID = {
|
|
|
7693
8113
|
license: () => licenseStep(),
|
|
7694
8114
|
databases: (options) => databasesStep(options.databases),
|
|
7695
8115
|
"git-sync": () => gitSyncStep(),
|
|
8116
|
+
"sync-scope": () => syncScopeStep(),
|
|
7696
8117
|
summary: () => summaryStep()
|
|
7697
8118
|
};
|
|
7698
8119
|
function initSteps(options) {
|
|
@@ -7777,7 +8198,8 @@ const SECTIONS = {
|
|
|
7777
8198
|
remote: RemoteKind.optional(),
|
|
7778
8199
|
url: EnvString.optional(),
|
|
7779
8200
|
token: EnvString.optional()
|
|
7780
|
-
}).optional()
|
|
8201
|
+
}).optional(),
|
|
8202
|
+
"sync-scope": z.strictObject({ export: z.boolean() }).optional()
|
|
7781
8203
|
};
|
|
7782
8204
|
const InitAnswers = z.strictObject(SECTIONS);
|
|
7783
8205
|
const select$1 = (value) => ({
|
|
@@ -7886,6 +8308,8 @@ function keyedAnswers(answers) {
|
|
|
7886
8308
|
if (git.url !== void 0) add(map, GIT_SYNC_KEYS.url, text$1(git.url));
|
|
7887
8309
|
if (git.token !== void 0) add(map, GIT_SYNC_KEYS.token, password$1(git.token));
|
|
7888
8310
|
}
|
|
8311
|
+
const scope = answers["sync-scope"];
|
|
8312
|
+
if (scope !== void 0) add(map, SYNC_SCOPE_KEYS.export, confirm$1(scope.export));
|
|
7889
8313
|
return map;
|
|
7890
8314
|
}
|
|
7891
8315
|
function parseInitAnswers(raw, source) {
|
|
@@ -7927,6 +8351,10 @@ function stepsAskingForWork(answers) {
|
|
|
7927
8351
|
step: "git-sync",
|
|
7928
8352
|
asks: git !== void 0 && git.location !== "skip"
|
|
7929
8353
|
},
|
|
8354
|
+
{
|
|
8355
|
+
step: "sync-scope",
|
|
8356
|
+
asks: answers["sync-scope"]?.export === true
|
|
8357
|
+
},
|
|
7930
8358
|
{
|
|
7931
8359
|
step: "agents-skills",
|
|
7932
8360
|
asks: agents !== void 0 && agents.agents.length > 0
|
|
@@ -9666,7 +10094,8 @@ function applyRecord(draft, record) {
|
|
|
9666
10094
|
else if (key === API_KEY_KEYS.profile && value !== null) {
|
|
9667
10095
|
const profile = MbProfileChoice.safeParse(value);
|
|
9668
10096
|
if (profile.success) draft.answers["api-key"] = { profile: profile.data };
|
|
9669
|
-
} else if (key ===
|
|
10097
|
+
} else if (key === SYNC_SCOPE_KEYS.export && record.answer.kind === "confirm") draft.answers["sync-scope"] = { export: record.answer.value };
|
|
10098
|
+
else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
|
|
9670
10099
|
}
|
|
9671
10100
|
function writableUri(uri, index) {
|
|
9672
10101
|
const trimmed = uri.trim();
|
|
@@ -10127,7 +10556,7 @@ function taskLabel(status) {
|
|
|
10127
10556
|
}
|
|
10128
10557
|
function remoteContentOf(changes) {
|
|
10129
10558
|
if (!changes.has_changes) return "imported";
|
|
10130
|
-
return changes
|
|
10559
|
+
return remoteMoved(changes) ? "changed" : "never-imported";
|
|
10131
10560
|
}
|
|
10132
10561
|
function isMbFailure(error) {
|
|
10133
10562
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
@@ -10145,7 +10574,8 @@ async function gitSyncOf(ctx, deps) {
|
|
|
10145
10574
|
branch: status.branch,
|
|
10146
10575
|
dirty: status.is_dirty,
|
|
10147
10576
|
remote: remoteContentOf(changes),
|
|
10148
|
-
task: taskLabel(status)
|
|
10577
|
+
task: taskLabel(status),
|
|
10578
|
+
syncedCollections: status.synced_collections.map((collection) => collection.name)
|
|
10149
10579
|
};
|
|
10150
10580
|
} catch (error) {
|
|
10151
10581
|
if (isMbFailure(error)) return { error: error.message };
|
|
@@ -10205,7 +10635,8 @@ function gitSyncLine(report) {
|
|
|
10205
10635
|
if (isGitSyncFailure(report.gitSync)) return `Remote sync: ${report.gitSync.error}`;
|
|
10206
10636
|
const branch = report.gitSync.branch === null ? "no branch" : `branch ${report.gitSync.branch}`;
|
|
10207
10637
|
const task = report.gitSync.task === null ? "no task running" : `task ${report.gitSync.task}`;
|
|
10208
|
-
|
|
10638
|
+
const scope = report.gitSync.syncedCollections.length === 0 ? "nothing synced; `rde init --only sync-scope`" : `synced: ${report.gitSync.syncedCollections.join(", ")}`;
|
|
10639
|
+
return `Remote sync: ${branch}, ${syncStateLabel(report.mbProfile, report.gitSync)}, ${task}, ${scope}`;
|
|
10209
10640
|
}
|
|
10210
10641
|
const REMOTE_CONTENT_LABEL = {
|
|
10211
10642
|
imported: null,
|
|
@@ -11076,7 +11507,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11076
11507
|
const main = defineCommand({
|
|
11077
11508
|
meta: {
|
|
11078
11509
|
name: "rde",
|
|
11079
|
-
version: "0.1.
|
|
11510
|
+
version: "0.1.17",
|
|
11080
11511
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
11081
11512
|
},
|
|
11082
11513
|
subCommands: {
|