@metabase/rde 0.1.8 → 0.1.9
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 +5 -5
- package/dist/cli.mjs +415 -254
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -59,7 +59,7 @@ The git step reads `–` until a license with remote sync is active; it is check
|
|
|
59
59
|
| Start Metabase | Starts the container or the jar and waits for `/api/health`, showing Metabase's own progress |
|
|
60
60
|
| Create the admin user | `admin@metabase.local` with a generated password that `rde credentials` shows, or your own with `--admin prompted`, see [Credentials](#credentials) |
|
|
61
61
|
| Connect the Metabase CLI | Creates an API key named `rde` and logs `mb` in with it under the profile `default`, so plain `mb` uses the instance; when `default` already points at another Metabase it asks first, see [The `mb` profile](#the-mb-profile); a key Metabase rejects is replaced |
|
|
62
|
-
| Connect a sample database | Runs Metabase's Postgres sample data (`metabase/qa-databases:postgres-sample-15`) in a container named `rde-sampledb-default` on the first free port from 15432, connects it as `Sample Database`, and switches transforms on. It replaces the H2 sample database Metabase would otherwise bundle, which cannot be a transform target. Needs Docker, so a jar instance on a machine without Docker keeps the H2 one instead |
|
|
62
|
+
| Connect a sample database | Runs Metabase's Postgres sample data (`metabase/qa-databases:postgres-sample-15`) in a container named `rde-sampledb-default` on the first free port from 15432 (or one Docker picks, when another program or a concurrent `rde init` takes that port first), connects it as `Sample Database`, and switches transforms on. It replaces the H2 sample database Metabase would otherwise bundle, which cannot be a transform target. Needs Docker, so a jar instance on a machine without Docker keeps the H2 one instead |
|
|
63
63
|
| Activate your license | Optional. Paste a token, or press Enter to skip; remote sync needs one with the `remote-sync` feature |
|
|
64
64
|
| Connect your databases | At a terminal, until you connect one of your own or skip it: paste a connection string (Enter skips), review, test, add; repeat. `--database` names them ahead of time; with `--yes` or without a terminal nothing is asked. `rde db add` starts from the engine list, where the fields can also be filled one by one |
|
|
65
65
|
| Connect a git repository | Only with a license. Initializes or reuses a repository and points Metabase's remote sync at it |
|
|
@@ -102,7 +102,7 @@ The second run on the same machine detects every step as done and prints the sum
|
|
|
102
102
|
└ rde init finished
|
|
103
103
|
```
|
|
104
104
|
|
|
105
|
-
Ctrl-C at any prompt stops the run; `rde init` again continues from the first step that is not done. A step that fails ends the run with exit code 1 (2 for a missing answer) and a last line naming the step and the command that resumes there: `rde init stopped at "Connect a sample database": <reason>. Resume with rde init --from sample-database.` `rde init --from license` re-runs from a step on, `rde init --only databases` runs one step.
|
|
105
|
+
Ctrl-C at any prompt stops the run; `rde init` again continues from the first step that is not done. A step that fails ends the run with exit code 1 (2 for a missing answer) and a last line naming the step and the command that resumes there: `rde init stopped at "Connect a sample database": <reason>. Resume with rde init --from sample-database.` `rde init --from license` re-runs from a step on (a later step that does not apply, such as the git step without a license that has remote sync, stays skipped), `rde init --only databases` runs one step.
|
|
106
106
|
|
|
107
107
|
## The sample database
|
|
108
108
|
|
|
@@ -191,10 +191,10 @@ mb git-sync status
|
|
|
191
191
|
|
|
192
192
|
## The git repository
|
|
193
193
|
|
|
194
|
-
With a license that includes remote sync, the last step connects a git repository. You choose where the working directory lives (here,
|
|
194
|
+
With a license that includes remote sync, the last step connects a git repository. You choose where Metabase pushes, then where the working directory lives (here, another directory, or an existing repository):
|
|
195
195
|
|
|
196
196
|
- **Local remote inside `~/.rde`** (default): a bare repository at `instances/default/data/content.git`, mounted into the container and reached as `file:///rde/content.git`. No account, no token. Your working directory gets it as `origin` (or as `rde-remote` when `origin` already points elsewhere).
|
|
197
|
-
- **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.
|
|
197
|
+
- **Hosted remote**: a GitHub, GitLab, or Bitbucket `https://` URL plus a token with read and write access. `rde` tests the connection through Metabase before saving anything, then clones the repository into the working directory (an existing repository gets the URL as `origin` instead); an empty hosted repository gets the first commit pushed to it. The token is handed to Metabase and, for the clone and that first push, to git in its environment; it never lands in the clone's config, the remote URL, or a credential helper, so a later `git pull` uses your own git credentials.
|
|
198
198
|
|
|
199
199
|
A fresh repository gets a first commit with a `README.md` and a `.gitignore` that keeps out what a data app built in it must not push: `.env`, `.env.local`, `.env.*.local`, `node_modules/`, `.vite/`, the rde skill's `.scratch/`, and `.DS_Store` (a data app's built `dist/` stays tracked, since that bundle is what Metabase serves); a `.gitignore` already there keeps its lines and gets the missing ones, and a repository that already has commits is left as it is. A repository that already holds Metabase content is imported on the spot. Afterwards:
|
|
200
200
|
|
|
@@ -257,7 +257,7 @@ rde init --license-token '$RDE_LICENSE_TOKEN' …
|
|
|
257
257
|
|
|
258
258
|
### Recording a run
|
|
259
259
|
|
|
260
|
-
`rde init --print-answers <file>` writes, once an interactive run finishes, the answers file that reproduces it: the prompts as they were answered, plus any override given by a flag. Secrets are written as references, never as values: `$RDE_ADMIN_PASSWORD`, `$RDE_LICENSE_TOKEN`, `$RDE_GIT_TOKEN`, `$RDE_DATABASE_<n>_URI` for a connection string carrying a password, and `$RDE_DATABASE_<n>_<FIELD>` for a secret field (a password, or an SSH private key). Set those variables and run `rde init --answers <file>` on the next machine.
|
|
260
|
+
`rde init --print-answers <file>` writes, once an interactive run finishes, the answers file that reproduces it: the prompts as they were answered, each database as it was added (its connection string if used as pasted, otherwise its fields), plus any override given by a flag. Secrets are written as references, never as values: `$RDE_ADMIN_PASSWORD`, `$RDE_LICENSE_TOKEN`, `$RDE_GIT_TOKEN`, `$RDE_DATABASE_<n>_URI` for a connection string carrying a password, and `$RDE_DATABASE_<n>_<FIELD>` for a secret field (a password, or an SSH private key). Set those variables and run `rde init --answers <file>` on the next machine.
|
|
261
261
|
|
|
262
262
|
## Upgrading
|
|
263
263
|
|
package/dist/cli.mjs
CHANGED
|
@@ -1600,6 +1600,10 @@ async function run$1(signal, command, args, options) {
|
|
|
1600
1600
|
"pipe"
|
|
1601
1601
|
],
|
|
1602
1602
|
...options.cwd !== void 0 && { cwd: options.cwd },
|
|
1603
|
+
...options.addedEnv !== void 0 && { env: {
|
|
1604
|
+
...hostEnvironment(),
|
|
1605
|
+
...options.addedEnv
|
|
1606
|
+
} },
|
|
1603
1607
|
signal: combined,
|
|
1604
1608
|
killSignal: "SIGKILL"
|
|
1605
1609
|
});
|
|
@@ -2118,9 +2122,9 @@ function adminUserStep(options, deps = hostDeps$12) {
|
|
|
2118
2122
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
2119
2123
|
const properties = await client.session.properties();
|
|
2120
2124
|
if (properties["has-user-setup"]) {
|
|
2121
|
-
if (ctx.state.admin === null) return recordExistingAdmin(ctx, options.variant, client, configured.url);
|
|
2125
|
+
if (ctx.state.admin === null) return { outcome: await recordExistingAdmin(ctx, options.variant, client, configured.url) };
|
|
2122
2126
|
ctx.ui.info("Metabase already has an admin user");
|
|
2123
|
-
return "completed";
|
|
2127
|
+
return { outcome: "completed" };
|
|
2124
2128
|
}
|
|
2125
2129
|
const token = properties["setup-token"];
|
|
2126
2130
|
if (token === null) throw new ConfigError("Metabase has no admin user yet but offers no setup token");
|
|
@@ -2159,7 +2163,7 @@ function adminUserStep(options, deps = hostDeps$12) {
|
|
|
2159
2163
|
}
|
|
2160
2164
|
});
|
|
2161
2165
|
ctx.ui.success(`Admin user ${user.email} created`);
|
|
2162
|
-
return "completed";
|
|
2166
|
+
return { outcome: "completed" };
|
|
2163
2167
|
}
|
|
2164
2168
|
};
|
|
2165
2169
|
}
|
|
@@ -2635,7 +2639,7 @@ function apiKeyStep(deps = hostDeps$11) {
|
|
|
2635
2639
|
if (entry === void 0) throw new ConfigError(`\`mb auth list\` does not show profile ${profile} after login`);
|
|
2636
2640
|
if (entry.status !== "ok") throw new ConfigError(`mb profile ${profile} reports ${entry.status} after login`);
|
|
2637
2641
|
ctx.ui.note(usageNote(profile), "Metabase CLI connected");
|
|
2638
|
-
return "completed";
|
|
2642
|
+
return { outcome: "completed" };
|
|
2639
2643
|
}
|
|
2640
2644
|
};
|
|
2641
2645
|
}
|
|
@@ -3266,6 +3270,43 @@ function withoutUnasked(fields, details) {
|
|
|
3266
3270
|
function promptableFields(fields, details) {
|
|
3267
3271
|
return visibleFields(fields, details).filter(isPromptable);
|
|
3268
3272
|
}
|
|
3273
|
+
const SECRET_KINDS$1 = /* @__PURE__ */ new Set(["password", "file"]);
|
|
3274
|
+
function answerOf(field, details) {
|
|
3275
|
+
const kind = promptKindFor(field);
|
|
3276
|
+
const current = details[field.name];
|
|
3277
|
+
if (kind === "boolean") {
|
|
3278
|
+
const value = typeof current === "boolean" ? current : Boolean(field.default);
|
|
3279
|
+
return {
|
|
3280
|
+
name: field.name,
|
|
3281
|
+
value,
|
|
3282
|
+
secret: false
|
|
3283
|
+
};
|
|
3284
|
+
}
|
|
3285
|
+
if (kind === "select") {
|
|
3286
|
+
const value = current ?? field.default;
|
|
3287
|
+
return value === void 0 || value === null ? null : {
|
|
3288
|
+
name: field.name,
|
|
3289
|
+
value: String(value),
|
|
3290
|
+
secret: false
|
|
3291
|
+
};
|
|
3292
|
+
}
|
|
3293
|
+
if (current === void 0 || current === "") return {
|
|
3294
|
+
name: field.name,
|
|
3295
|
+
value: "",
|
|
3296
|
+
secret: false
|
|
3297
|
+
};
|
|
3298
|
+
return {
|
|
3299
|
+
name: field.name,
|
|
3300
|
+
value: current,
|
|
3301
|
+
secret: SECRET_KINDS$1.has(kind)
|
|
3302
|
+
};
|
|
3303
|
+
}
|
|
3304
|
+
function answeredFields(fields, details) {
|
|
3305
|
+
return promptableFields(fields, details).flatMap((field) => {
|
|
3306
|
+
const answer = answerOf(field, details);
|
|
3307
|
+
return answer === null ? [] : [answer];
|
|
3308
|
+
});
|
|
3309
|
+
}
|
|
3269
3310
|
async function fillRequiredFields(deps, fields, details) {
|
|
3270
3311
|
let current = details;
|
|
3271
3312
|
const asked = /* @__PURE__ */ new Set();
|
|
@@ -3525,18 +3566,45 @@ function databaseListingLines(listing) {
|
|
|
3525
3566
|
//#region src/steps/databases/flow.ts
|
|
3526
3567
|
const DATABASE_KEYS = {
|
|
3527
3568
|
connection: "databases.connection",
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3535
|
-
|
|
3536
|
-
|
|
3537
|
-
|
|
3538
|
-
|
|
3569
|
+
skip: "databases.skip"
|
|
3570
|
+
};
|
|
3571
|
+
const DATABASE_KEY_NAMES = {
|
|
3572
|
+
detectedEngine: "detected-engine",
|
|
3573
|
+
engine: "engine",
|
|
3574
|
+
mode: "mode",
|
|
3575
|
+
uri: "uri",
|
|
3576
|
+
name: "name",
|
|
3577
|
+
action: "action",
|
|
3578
|
+
editField: "edit-field",
|
|
3579
|
+
testAction: "test-action",
|
|
3580
|
+
switchEngine: "switch-engine",
|
|
3581
|
+
another: "another"
|
|
3539
3582
|
};
|
|
3583
|
+
function sameDetails(left, right) {
|
|
3584
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(left), ...Object.keys(right)])].every((key) => left[key] === right[key]);
|
|
3585
|
+
}
|
|
3586
|
+
function addedDatabase(chosen, model, name) {
|
|
3587
|
+
const pastedAsIs = model.uri !== null && model.pasted !== null && sameDetails(model.details, model.pasted);
|
|
3588
|
+
return {
|
|
3589
|
+
engine: chosen.engine.key,
|
|
3590
|
+
name,
|
|
3591
|
+
uri: pastedAsIs ? model.uri : null,
|
|
3592
|
+
fields: pastedAsIs ? [] : answeredFields(chosen.fields, model.details)
|
|
3593
|
+
};
|
|
3594
|
+
}
|
|
3595
|
+
function databaseKey(ordinal, name) {
|
|
3596
|
+
return `databases.${ordinal}.${DATABASE_KEY_NAMES[name]}`;
|
|
3597
|
+
}
|
|
3598
|
+
const DATABASE_KEY = /^databases\.(?<ordinal>\d+)\.(?<name>[a-z-]+)$/;
|
|
3599
|
+
function parseDatabaseKey(key) {
|
|
3600
|
+
const groups = DATABASE_KEY.exec(key)?.groups;
|
|
3601
|
+
const ordinal = groups?.["ordinal"];
|
|
3602
|
+
const name = groups?.["name"];
|
|
3603
|
+
return ordinal === void 0 || name === void 0 ? null : {
|
|
3604
|
+
ordinal: Number(ordinal),
|
|
3605
|
+
name
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3540
3608
|
const FINISH = "finish";
|
|
3541
3609
|
const RETRY = "try-again";
|
|
3542
3610
|
const BAD_REQUEST_STATUS$1 = 400;
|
|
@@ -3558,13 +3626,16 @@ const FRESH$1 = {
|
|
|
3558
3626
|
name: null,
|
|
3559
3627
|
fieldErrors: null
|
|
3560
3628
|
};
|
|
3629
|
+
function ordinalOf(run) {
|
|
3630
|
+
return run.created.length + 1;
|
|
3631
|
+
}
|
|
3561
3632
|
function fieldDeps(run) {
|
|
3562
3633
|
const { prompts, ui, readTextFile } = run.deps;
|
|
3563
3634
|
return {
|
|
3564
3635
|
prompts,
|
|
3565
3636
|
ui,
|
|
3566
3637
|
readTextFile,
|
|
3567
|
-
ordinal: run
|
|
3638
|
+
ordinal: ordinalOf(run)
|
|
3568
3639
|
};
|
|
3569
3640
|
}
|
|
3570
3641
|
async function orFailure(failure, ask) {
|
|
@@ -3612,7 +3683,7 @@ function isCreatableEntry(entry) {
|
|
|
3612
3683
|
}
|
|
3613
3684
|
async function pickDetectedEngine(run, candidates) {
|
|
3614
3685
|
const picked = await run.deps.prompts.selectOrBack({
|
|
3615
|
-
key:
|
|
3686
|
+
key: databaseKey(ordinalOf(run), "detectedEngine"),
|
|
3616
3687
|
message: "Which database is it?",
|
|
3617
3688
|
choices: candidates.map((entry) => ({
|
|
3618
3689
|
value: entry.key,
|
|
@@ -3679,7 +3750,7 @@ function pickEngineScreen(run) {
|
|
|
3679
3750
|
};
|
|
3680
3751
|
for (;;) {
|
|
3681
3752
|
const picked = await run.deps.prompts.select({
|
|
3682
|
-
key:
|
|
3753
|
+
key: databaseKey(ordinalOf(run), "engine"),
|
|
3683
3754
|
message: "Which database engine?",
|
|
3684
3755
|
choices: [...run.deps.catalog.list().map(engineChoice), finishChoice],
|
|
3685
3756
|
...model.engine !== null && { initialValue: model.engine.key }
|
|
@@ -3720,7 +3791,7 @@ async function chooseAfterParseError(run, engine, uri, error) {
|
|
|
3720
3791
|
hint: "this looks like one of its connection strings"
|
|
3721
3792
|
}));
|
|
3722
3793
|
const picked = await orFailure(error.message, () => run.deps.prompts.selectOrBack({
|
|
3723
|
-
key:
|
|
3794
|
+
key: databaseKey(ordinalOf(run), "switchEngine"),
|
|
3724
3795
|
message: "What now?",
|
|
3725
3796
|
choices: [...switches, {
|
|
3726
3797
|
value: RETRY,
|
|
@@ -3774,7 +3845,7 @@ function pasteUriScreen(run) {
|
|
|
3774
3845
|
for (;;) {
|
|
3775
3846
|
if (engine.exampleUri === null) return manualEntry(run, model, engine);
|
|
3776
3847
|
const mode = await run.deps.prompts.selectOrBack({
|
|
3777
|
-
key:
|
|
3848
|
+
key: databaseKey(ordinalOf(run), "mode"),
|
|
3778
3849
|
message: `How do you want to connect to ${engine.displayName}?`,
|
|
3779
3850
|
choices: [{
|
|
3780
3851
|
value: "paste",
|
|
@@ -3787,7 +3858,7 @@ function pasteUriScreen(run) {
|
|
|
3787
3858
|
if (mode === BACK) return { type: "back" };
|
|
3788
3859
|
if (mode === "manual") return manualEntry(run, model, engine);
|
|
3789
3860
|
const uri = await run.deps.prompts.textOrBack({
|
|
3790
|
-
key:
|
|
3861
|
+
key: databaseKey(ordinalOf(run), "uri"),
|
|
3791
3862
|
message: "Connection string",
|
|
3792
3863
|
placeholder: engine.exampleUri,
|
|
3793
3864
|
...draft !== null && { initialValue: draft }
|
|
@@ -3852,7 +3923,7 @@ function reviewNote(run, chosen, model) {
|
|
|
3852
3923
|
async function promptName(run, model, engine) {
|
|
3853
3924
|
const suggested = model.name ?? model.suggestedName ?? engine.displayName;
|
|
3854
3925
|
return run.deps.prompts.text({
|
|
3855
|
-
key:
|
|
3926
|
+
key: databaseKey(ordinalOf(run), "name"),
|
|
3856
3927
|
message: "Display name",
|
|
3857
3928
|
defaultValue: suggested,
|
|
3858
3929
|
placeholder: suggested
|
|
@@ -3869,7 +3940,7 @@ async function editOneField(run, fields, details) {
|
|
|
3869
3940
|
};
|
|
3870
3941
|
});
|
|
3871
3942
|
const picked = await run.deps.prompts.selectOrBack({
|
|
3872
|
-
key:
|
|
3943
|
+
key: databaseKey(ordinalOf(run), "editField"),
|
|
3873
3944
|
message: "Which field?",
|
|
3874
3945
|
choices
|
|
3875
3946
|
});
|
|
@@ -3912,7 +3983,7 @@ function reviewAttempt(run) {
|
|
|
3912
3983
|
reviewNote(run, chosen, current);
|
|
3913
3984
|
if (name === null) name = await promptName(run, current, chosen.engine);
|
|
3914
3985
|
const action = await run.deps.prompts.selectOrBack({
|
|
3915
|
-
key:
|
|
3986
|
+
key: databaseKey(ordinalOf(run), "action"),
|
|
3916
3987
|
message: `Add ${name}?`,
|
|
3917
3988
|
choices: [
|
|
3918
3989
|
{
|
|
@@ -3977,7 +4048,7 @@ function testScreen(run) {
|
|
|
3977
4048
|
const lines = fieldErrorLines(chosen.fields, errors);
|
|
3978
4049
|
if (lines.length > 0) run.deps.ui.warn(lines.join("\n"));
|
|
3979
4050
|
const action = await orFailure(failure, () => run.deps.prompts.selectOrBack({
|
|
3980
|
-
key:
|
|
4051
|
+
key: databaseKey(ordinalOf(run), "testAction"),
|
|
3981
4052
|
message: "What now?",
|
|
3982
4053
|
choices: [{
|
|
3983
4054
|
value: "fix",
|
|
@@ -4038,6 +4109,7 @@ function createScreen(run) {
|
|
|
4038
4109
|
}
|
|
4039
4110
|
run.created.push(database);
|
|
4040
4111
|
spinner.stop(`Added ${database.name}. Initial sync is running in the background; \`rde status\` shows progress.`);
|
|
4112
|
+
run.added.push(addedDatabase(chosen, model, model.name));
|
|
4041
4113
|
await run.deps.onCreated(database);
|
|
4042
4114
|
return {
|
|
4043
4115
|
type: "next",
|
|
@@ -4049,7 +4121,7 @@ function createScreen(run) {
|
|
|
4049
4121
|
function anotherScreen(run) {
|
|
4050
4122
|
return async (model) => {
|
|
4051
4123
|
if (await run.deps.prompts.select({
|
|
4052
|
-
key:
|
|
4124
|
+
key: databaseKey(run.created.length, "another"),
|
|
4053
4125
|
message: "Anything else?",
|
|
4054
4126
|
choices: [{
|
|
4055
4127
|
value: "another",
|
|
@@ -4072,7 +4144,8 @@ function anotherScreen(run) {
|
|
|
4072
4144
|
async function runDatabaseFlow(deps) {
|
|
4073
4145
|
const run = {
|
|
4074
4146
|
deps,
|
|
4075
|
-
created: []
|
|
4147
|
+
created: [],
|
|
4148
|
+
added: []
|
|
4076
4149
|
};
|
|
4077
4150
|
await runFlow({
|
|
4078
4151
|
"connection-string": connectionStringScreen(run),
|
|
@@ -4083,7 +4156,10 @@ async function runDatabaseFlow(deps) {
|
|
|
4083
4156
|
create: createScreen(run),
|
|
4084
4157
|
another: anotherScreen(run)
|
|
4085
4158
|
}, START_SCREEN[deps.start], FRESH$1);
|
|
4086
|
-
return {
|
|
4159
|
+
return {
|
|
4160
|
+
created: run.created,
|
|
4161
|
+
added: run.added
|
|
4162
|
+
};
|
|
4087
4163
|
}
|
|
4088
4164
|
//#endregion
|
|
4089
4165
|
//#region src/tools/docker.ts
|
|
@@ -4131,9 +4207,9 @@ function createArgs(options) {
|
|
|
4131
4207
|
"--label",
|
|
4132
4208
|
`${HOME_LABEL}=${options.home}`,
|
|
4133
4209
|
"-p",
|
|
4134
|
-
`${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`
|
|
4210
|
+
`${LOOPBACK_HOST}:${options.port.host ?? ""}:${options.port.container}`
|
|
4135
4211
|
];
|
|
4136
|
-
if (options.port.alsoOn !== null) args.push("-p", `${options.port.alsoOn}:${options.port.host}:${options.port.container}`);
|
|
4212
|
+
if (options.port.alsoOn !== null) args.push("-p", `${options.port.alsoOn}:${options.port.host ?? ""}:${options.port.container}`);
|
|
4137
4213
|
if (options.mount !== null) args.push("-v", `${options.mount.source}:${options.mount.target}`);
|
|
4138
4214
|
for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
|
|
4139
4215
|
if (options.hostUser !== null) {
|
|
@@ -4244,6 +4320,33 @@ function dockerTool(runner) {
|
|
|
4244
4320
|
async function containerRemove(name) {
|
|
4245
4321
|
await docker(["rm", name]);
|
|
4246
4322
|
}
|
|
4323
|
+
async function containerForceRemove(name) {
|
|
4324
|
+
await docker([
|
|
4325
|
+
"rm",
|
|
4326
|
+
"-f",
|
|
4327
|
+
name
|
|
4328
|
+
]);
|
|
4329
|
+
}
|
|
4330
|
+
async function containerLoopbackPort(name, containerPort) {
|
|
4331
|
+
const args = [
|
|
4332
|
+
"port",
|
|
4333
|
+
name,
|
|
4334
|
+
`${containerPort}/tcp`
|
|
4335
|
+
];
|
|
4336
|
+
const result = await docker(args);
|
|
4337
|
+
const prefix = `${LOOPBACK_HOST}:`;
|
|
4338
|
+
const line = result.stdout.split("\n").find((entry) => entry.startsWith(prefix));
|
|
4339
|
+
if (line !== void 0) {
|
|
4340
|
+
const port = Number.parseInt(line.slice(prefix.length), 10);
|
|
4341
|
+
if (Number.isInteger(port)) return port;
|
|
4342
|
+
}
|
|
4343
|
+
throw new ExternalToolError({
|
|
4344
|
+
tool: DOCKER,
|
|
4345
|
+
argv: args,
|
|
4346
|
+
exitCode: 0,
|
|
4347
|
+
stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
|
|
4348
|
+
});
|
|
4349
|
+
}
|
|
4247
4350
|
async function containerExecSucceeds(name, command) {
|
|
4248
4351
|
return (await runner.run(DOCKER, [
|
|
4249
4352
|
"exec",
|
|
@@ -4270,6 +4373,8 @@ function dockerTool(runner) {
|
|
|
4270
4373
|
containerStart,
|
|
4271
4374
|
containerStop,
|
|
4272
4375
|
containerRemove,
|
|
4376
|
+
containerForceRemove,
|
|
4377
|
+
containerLoopbackPort,
|
|
4273
4378
|
containerExecSucceeds,
|
|
4274
4379
|
containerLogs
|
|
4275
4380
|
};
|
|
@@ -4294,6 +4399,21 @@ const METABASE_CONTENT_DIRS = [
|
|
|
4294
4399
|
"snippets",
|
|
4295
4400
|
"transforms"
|
|
4296
4401
|
];
|
|
4402
|
+
const TOKEN_USERNAMES = { "bitbucket.org": "x-token-auth" };
|
|
4403
|
+
const DEFAULT_TOKEN_USERNAME = "x-access-token";
|
|
4404
|
+
function credentialEnv(credentials) {
|
|
4405
|
+
const { origin, hostname } = new URL(credentials.url);
|
|
4406
|
+
const username = TOKEN_USERNAMES[hostname] ?? DEFAULT_TOKEN_USERNAME;
|
|
4407
|
+
const basic = Buffer.from(`${username}:${credentials.token}`).toString("base64");
|
|
4408
|
+
return {
|
|
4409
|
+
GIT_CONFIG_COUNT: "2",
|
|
4410
|
+
GIT_CONFIG_KEY_0: `http.${origin}/.extraHeader`,
|
|
4411
|
+
GIT_CONFIG_VALUE_0: `Authorization: Basic ${basic}`,
|
|
4412
|
+
GIT_CONFIG_KEY_1: "credential.helper",
|
|
4413
|
+
GIT_CONFIG_VALUE_1: "",
|
|
4414
|
+
GIT_TERMINAL_PROMPT: "0"
|
|
4415
|
+
};
|
|
4416
|
+
}
|
|
4297
4417
|
async function directoryExists(path) {
|
|
4298
4418
|
try {
|
|
4299
4419
|
return (await promises.stat(path)).isDirectory();
|
|
@@ -4302,8 +4422,8 @@ async function directoryExists(path) {
|
|
|
4302
4422
|
}
|
|
4303
4423
|
}
|
|
4304
4424
|
function gitTool(runner) {
|
|
4305
|
-
async function git(args,
|
|
4306
|
-
const result = await runner.run(GIT, args,
|
|
4425
|
+
async function git(args, options = {}) {
|
|
4426
|
+
const result = await runner.run(GIT, args, options);
|
|
4307
4427
|
if (result.exitCode !== 0) throw toolFailure(GIT, args, result);
|
|
4308
4428
|
return result;
|
|
4309
4429
|
}
|
|
@@ -4349,7 +4469,7 @@ function gitTool(runner) {
|
|
|
4349
4469
|
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
4350
4470
|
}
|
|
4351
4471
|
async function commitAll(dir, message) {
|
|
4352
|
-
await git(["add", "--all"], dir);
|
|
4472
|
+
await git(["add", "--all"], { cwd: dir });
|
|
4353
4473
|
await git([
|
|
4354
4474
|
...await hasIdentity(dir) ? [] : [
|
|
4355
4475
|
"-c",
|
|
@@ -4360,7 +4480,7 @@ function gitTool(runner) {
|
|
|
4360
4480
|
"commit",
|
|
4361
4481
|
"--message",
|
|
4362
4482
|
message
|
|
4363
|
-
], dir);
|
|
4483
|
+
], { cwd: dir });
|
|
4364
4484
|
}
|
|
4365
4485
|
async function addRemote(dir, name, url) {
|
|
4366
4486
|
await git([
|
|
@@ -4368,22 +4488,33 @@ function gitTool(runner) {
|
|
|
4368
4488
|
"add",
|
|
4369
4489
|
name,
|
|
4370
4490
|
url
|
|
4371
|
-
], dir);
|
|
4491
|
+
], { cwd: dir });
|
|
4492
|
+
}
|
|
4493
|
+
async function clone(credentials, dir) {
|
|
4494
|
+
await git([
|
|
4495
|
+
"clone",
|
|
4496
|
+
"--quiet",
|
|
4497
|
+
credentials.url,
|
|
4498
|
+
dir
|
|
4499
|
+
], { addedEnv: credentialEnv(credentials) });
|
|
4372
4500
|
}
|
|
4373
|
-
async function push(dir, remote, branch) {
|
|
4501
|
+
async function push(dir, remote, branch, credentials) {
|
|
4374
4502
|
await git([
|
|
4375
4503
|
"push",
|
|
4376
4504
|
"--set-upstream",
|
|
4377
4505
|
remote,
|
|
4378
4506
|
branch
|
|
4379
|
-
],
|
|
4507
|
+
], {
|
|
4508
|
+
cwd: dir,
|
|
4509
|
+
...credentials !== void 0 && { addedEnv: credentialEnv(credentials) }
|
|
4510
|
+
});
|
|
4380
4511
|
}
|
|
4381
4512
|
async function currentBranch(dir) {
|
|
4382
4513
|
return (await git([
|
|
4383
4514
|
"rev-parse",
|
|
4384
4515
|
"--abbrev-ref",
|
|
4385
4516
|
"HEAD"
|
|
4386
|
-
], dir)).stdout.trim();
|
|
4517
|
+
], { cwd: dir })).stdout.trim();
|
|
4387
4518
|
}
|
|
4388
4519
|
async function remoteUrl(dir, name) {
|
|
4389
4520
|
const result = await runner.run(GIT, [
|
|
@@ -4406,6 +4537,7 @@ function gitTool(runner) {
|
|
|
4406
4537
|
hasCommits,
|
|
4407
4538
|
commitAll,
|
|
4408
4539
|
addRemote,
|
|
4540
|
+
clone,
|
|
4409
4541
|
push,
|
|
4410
4542
|
currentBranch,
|
|
4411
4543
|
remoteUrl,
|
|
@@ -4459,10 +4591,11 @@ function hostDeps$10(ctx) {
|
|
|
4459
4591
|
taskPollIntervalMs: TASK_POLL_INTERVAL_MS
|
|
4460
4592
|
};
|
|
4461
4593
|
}
|
|
4594
|
+
const SKIP_GIT_SYNC = "skip";
|
|
4462
4595
|
const FRESH = {
|
|
4463
|
-
workdir: null,
|
|
4464
4596
|
remoteKind: null,
|
|
4465
|
-
hostedUrl: null
|
|
4597
|
+
hostedUrl: null,
|
|
4598
|
+
workdir: null
|
|
4466
4599
|
};
|
|
4467
4600
|
function expandHome(path) {
|
|
4468
4601
|
return path.startsWith(HOME_PREFIX) ? join(homedir(), path.slice(2)) : path;
|
|
@@ -4478,6 +4611,10 @@ async function directoryState(path) {
|
|
|
4478
4611
|
if (!stat.isDirectory()) return "file";
|
|
4479
4612
|
return (await promises.readdir(path)).length === 0 ? "empty" : "populated";
|
|
4480
4613
|
}
|
|
4614
|
+
async function isCloneTarget(path) {
|
|
4615
|
+
const state = await directoryState(path);
|
|
4616
|
+
return state === "missing" || state === "empty";
|
|
4617
|
+
}
|
|
4481
4618
|
async function initTargetProblem(run, path) {
|
|
4482
4619
|
switch (await directoryState(path)) {
|
|
4483
4620
|
case "missing":
|
|
@@ -4488,78 +4625,132 @@ async function initTargetProblem(run, path) {
|
|
|
4488
4625
|
case "populated": return await run.git.isRepo(path) ? null : `${path} is not empty and not a git repository; pick an empty directory or a repository`;
|
|
4489
4626
|
}
|
|
4490
4627
|
}
|
|
4628
|
+
async function cloneTargetProblem(path) {
|
|
4629
|
+
if (await isCloneTarget(path)) return null;
|
|
4630
|
+
return await directoryState(path) === "file" ? `${path} is not a directory` : `${path} is not empty; pick an empty directory, or use an existing repository`;
|
|
4631
|
+
}
|
|
4491
4632
|
async function existingRepoProblem(run, path) {
|
|
4492
4633
|
return await run.git.isRepo(path) ? null : `${path} is not a git repository`;
|
|
4493
4634
|
}
|
|
4494
|
-
const
|
|
4495
|
-
|
|
4496
|
-
|
|
4635
|
+
const FRESH_WORKDIRS = {
|
|
4636
|
+
"local-bare": {
|
|
4637
|
+
here: "Initialize a repository here",
|
|
4638
|
+
elsewhere: "Initialize in another directory",
|
|
4639
|
+
pathMessage: "Directory for the repository (created if missing)",
|
|
4640
|
+
problem: initTargetProblem
|
|
4641
|
+
},
|
|
4642
|
+
hosted: {
|
|
4643
|
+
here: "Clone the repository here",
|
|
4644
|
+
elsewhere: "Clone into another directory",
|
|
4645
|
+
pathMessage: "Directory to clone into (created if missing)",
|
|
4646
|
+
problem: (_run, path) => cloneTargetProblem(path)
|
|
4647
|
+
}
|
|
4497
4648
|
};
|
|
4498
|
-
async function askPath(run,
|
|
4649
|
+
async function askPath(run, question) {
|
|
4499
4650
|
for (;;) {
|
|
4500
4651
|
const typed = await run.ctx.prompts.textOrBack({
|
|
4501
4652
|
key: GIT_SYNC_KEYS.path,
|
|
4502
|
-
message:
|
|
4503
|
-
...model.workdir !== null && { initialValue: model.workdir }
|
|
4653
|
+
message: question.message
|
|
4504
4654
|
});
|
|
4505
4655
|
if (typed === BACK) return BACK;
|
|
4506
4656
|
const path = resolve(run.deps.cwd, expandHome(typed));
|
|
4507
|
-
const problem =
|
|
4657
|
+
const problem = await question.problem(path);
|
|
4508
4658
|
if (problem === null) return path;
|
|
4509
4659
|
run.ctx.ui.warn(problem);
|
|
4510
4660
|
}
|
|
4511
4661
|
}
|
|
4662
|
+
function remoteScreen(run) {
|
|
4663
|
+
return async (model) => {
|
|
4664
|
+
const choice = await run.ctx.prompts.select({
|
|
4665
|
+
key: GIT_SYNC_KEYS.remote,
|
|
4666
|
+
message: "Where should Metabase push the content?",
|
|
4667
|
+
choices: [
|
|
4668
|
+
{
|
|
4669
|
+
value: "local-bare",
|
|
4670
|
+
label: "Local remote inside ~/.rde",
|
|
4671
|
+
hint: "no account needed, recommended"
|
|
4672
|
+
},
|
|
4673
|
+
{
|
|
4674
|
+
value: "hosted",
|
|
4675
|
+
label: "Hosted remote (GitHub, GitLab, Bitbucket…)",
|
|
4676
|
+
hint: "needs a token"
|
|
4677
|
+
},
|
|
4678
|
+
{
|
|
4679
|
+
value: SKIP_GIT_SYNC,
|
|
4680
|
+
label: "Skip for now",
|
|
4681
|
+
hint: "rde init --only git-sync sets it up later"
|
|
4682
|
+
}
|
|
4683
|
+
],
|
|
4684
|
+
...model.remoteKind !== null && { initialValue: model.remoteKind }
|
|
4685
|
+
});
|
|
4686
|
+
if (choice === "skip") return {
|
|
4687
|
+
type: "done",
|
|
4688
|
+
model: FRESH
|
|
4689
|
+
};
|
|
4690
|
+
const next = {
|
|
4691
|
+
...model,
|
|
4692
|
+
remoteKind: choice
|
|
4693
|
+
};
|
|
4694
|
+
return {
|
|
4695
|
+
type: "next",
|
|
4696
|
+
to: choice === "hosted" ? "hosted" : "workdir",
|
|
4697
|
+
model: next
|
|
4698
|
+
};
|
|
4699
|
+
};
|
|
4700
|
+
}
|
|
4512
4701
|
function workdirScreen(run) {
|
|
4513
4702
|
return async (model) => {
|
|
4703
|
+
if (model.remoteKind === null) throw new Error("no remote before the workdir screen");
|
|
4704
|
+
const fresh = FRESH_WORKDIRS[model.remoteKind];
|
|
4705
|
+
const questions = {
|
|
4706
|
+
new: {
|
|
4707
|
+
message: fresh.pathMessage,
|
|
4708
|
+
problem: (path) => fresh.problem(run, path)
|
|
4709
|
+
},
|
|
4710
|
+
existing: {
|
|
4711
|
+
message: "Path of the existing repository",
|
|
4712
|
+
problem: (path) => existingRepoProblem(run, path)
|
|
4713
|
+
}
|
|
4714
|
+
};
|
|
4514
4715
|
for (;;) {
|
|
4515
|
-
const choice = await run.ctx.prompts.
|
|
4716
|
+
const choice = await run.ctx.prompts.selectOrBack({
|
|
4516
4717
|
key: GIT_SYNC_KEYS.location,
|
|
4517
4718
|
message: "Where should the Metabase content repository live?",
|
|
4518
4719
|
choices: [
|
|
4519
4720
|
{
|
|
4520
4721
|
value: "here",
|
|
4521
|
-
label:
|
|
4722
|
+
label: fresh.here,
|
|
4522
4723
|
hint: run.deps.cwd
|
|
4523
4724
|
},
|
|
4524
4725
|
{
|
|
4525
4726
|
value: "new",
|
|
4526
|
-
label:
|
|
4727
|
+
label: fresh.elsewhere
|
|
4527
4728
|
},
|
|
4528
4729
|
{
|
|
4529
4730
|
value: "existing",
|
|
4530
4731
|
label: "Use an existing repository"
|
|
4531
|
-
},
|
|
4532
|
-
{
|
|
4533
|
-
value: "skip",
|
|
4534
|
-
label: "Skip for now",
|
|
4535
|
-
hint: "rde init --only git-sync sets it up later"
|
|
4536
4732
|
}
|
|
4537
4733
|
]
|
|
4538
4734
|
});
|
|
4539
|
-
if (choice ===
|
|
4540
|
-
type: "done",
|
|
4541
|
-
model: FRESH
|
|
4542
|
-
};
|
|
4735
|
+
if (choice === BACK) return { type: "back" };
|
|
4543
4736
|
if (choice === "here") {
|
|
4544
|
-
const problem = await
|
|
4737
|
+
const problem = await fresh.problem(run, run.deps.cwd);
|
|
4545
4738
|
if (problem !== null) {
|
|
4546
4739
|
run.ctx.ui.warn(problem);
|
|
4547
4740
|
continue;
|
|
4548
4741
|
}
|
|
4549
4742
|
return {
|
|
4550
|
-
type: "
|
|
4551
|
-
to: "remote",
|
|
4743
|
+
type: "done",
|
|
4552
4744
|
model: {
|
|
4553
4745
|
...model,
|
|
4554
4746
|
workdir: run.deps.cwd
|
|
4555
4747
|
}
|
|
4556
4748
|
};
|
|
4557
4749
|
}
|
|
4558
|
-
const path = await askPath(run,
|
|
4750
|
+
const path = await askPath(run, questions[choice]);
|
|
4559
4751
|
if (path === BACK) continue;
|
|
4560
4752
|
return {
|
|
4561
|
-
type: "
|
|
4562
|
-
to: "remote",
|
|
4753
|
+
type: "done",
|
|
4563
4754
|
model: {
|
|
4564
4755
|
...model,
|
|
4565
4756
|
workdir: path
|
|
@@ -4568,37 +4759,6 @@ function workdirScreen(run) {
|
|
|
4568
4759
|
}
|
|
4569
4760
|
};
|
|
4570
4761
|
}
|
|
4571
|
-
function remoteScreen(run) {
|
|
4572
|
-
return async (model) => {
|
|
4573
|
-
const kind = await run.ctx.prompts.selectOrBack({
|
|
4574
|
-
key: GIT_SYNC_KEYS.remote,
|
|
4575
|
-
message: "Where should Metabase push the content?",
|
|
4576
|
-
choices: [{
|
|
4577
|
-
value: "local-bare",
|
|
4578
|
-
label: "Local remote inside ~/.rde",
|
|
4579
|
-
hint: "no account needed, recommended"
|
|
4580
|
-
}, {
|
|
4581
|
-
value: "hosted",
|
|
4582
|
-
label: "Hosted remote (GitHub, GitLab, Bitbucket…)",
|
|
4583
|
-
hint: "needs a token"
|
|
4584
|
-
}],
|
|
4585
|
-
...model.remoteKind !== null && { initialValue: model.remoteKind }
|
|
4586
|
-
});
|
|
4587
|
-
if (kind === BACK) return { type: "back" };
|
|
4588
|
-
const next = {
|
|
4589
|
-
...model,
|
|
4590
|
-
remoteKind: kind
|
|
4591
|
-
};
|
|
4592
|
-
return kind === "hosted" ? {
|
|
4593
|
-
type: "next",
|
|
4594
|
-
to: "hosted",
|
|
4595
|
-
model: next
|
|
4596
|
-
} : {
|
|
4597
|
-
type: "done",
|
|
4598
|
-
model: next
|
|
4599
|
-
};
|
|
4600
|
-
};
|
|
4601
|
-
}
|
|
4602
4762
|
function validateHttpsUrl(value) {
|
|
4603
4763
|
return value.length === 0 || value.startsWith(HTTPS_PREFIX) ? void 0 : `The repository URL must start with ${HTTPS_PREFIX}`;
|
|
4604
4764
|
}
|
|
@@ -4639,7 +4799,8 @@ function hostedScreen(run) {
|
|
|
4639
4799
|
spinner.stop("Metabase can reach the repository");
|
|
4640
4800
|
run.ctx.session.gitToken.set(token);
|
|
4641
4801
|
return {
|
|
4642
|
-
type: "
|
|
4802
|
+
type: "next",
|
|
4803
|
+
to: "workdir",
|
|
4643
4804
|
model: {
|
|
4644
4805
|
...model,
|
|
4645
4806
|
hostedUrl: url
|
|
@@ -4706,9 +4867,34 @@ async function configureLocalBare(run, runtime, workdir) {
|
|
|
4706
4867
|
}
|
|
4707
4868
|
};
|
|
4708
4869
|
}
|
|
4709
|
-
async function
|
|
4870
|
+
async function cloneHosted(run, workdir, credentials) {
|
|
4871
|
+
const spinner = run.ctx.ui.spinner(`Cloning ${credentials.url}…`);
|
|
4872
|
+
try {
|
|
4873
|
+
await run.git.clone(credentials, workdir);
|
|
4874
|
+
} catch (error) {
|
|
4875
|
+
spinner.fail("Could not clone the repository");
|
|
4876
|
+
throw error;
|
|
4877
|
+
}
|
|
4878
|
+
spinner.stop(`Cloned into ${workdir}`);
|
|
4879
|
+
if (await run.git.hasCommits(workdir)) return {
|
|
4880
|
+
workdir,
|
|
4881
|
+
branch: await run.git.currentBranch(workdir)
|
|
4882
|
+
};
|
|
4883
|
+
const prepared = await prepareWorkdir(run, workdir);
|
|
4884
|
+
await run.git.push(workdir, ORIGIN, prepared.branch, credentials);
|
|
4885
|
+
return prepared;
|
|
4886
|
+
}
|
|
4887
|
+
async function adoptRepository(run, workdir, url) {
|
|
4710
4888
|
const prepared = await prepareWorkdir(run, workdir);
|
|
4711
4889
|
await ensureRemote(run, workdir, url);
|
|
4890
|
+
return prepared;
|
|
4891
|
+
}
|
|
4892
|
+
async function configureHosted(run, workdir, url) {
|
|
4893
|
+
const token = run.ctx.session.gitToken.get();
|
|
4894
|
+
const prepared = await isCloneTarget(workdir) ? await cloneHosted(run, workdir, {
|
|
4895
|
+
url,
|
|
4896
|
+
token
|
|
4897
|
+
}) : await adoptRepository(run, workdir, url);
|
|
4712
4898
|
return {
|
|
4713
4899
|
gitRemote: {
|
|
4714
4900
|
kind: "hosted",
|
|
@@ -4718,7 +4904,7 @@ async function configureHosted(run, workdir, url) {
|
|
|
4718
4904
|
},
|
|
4719
4905
|
settings: {
|
|
4720
4906
|
"remote-sync-url": url,
|
|
4721
|
-
"remote-sync-token":
|
|
4907
|
+
"remote-sync-token": token,
|
|
4722
4908
|
"remote-sync-type": "read-write",
|
|
4723
4909
|
"remote-sync-branch": prepared.branch
|
|
4724
4910
|
}
|
|
@@ -4836,10 +5022,10 @@ async function runGitSync(ctx, deps) {
|
|
|
4836
5022
|
})
|
|
4837
5023
|
};
|
|
4838
5024
|
const flow = await runFlow({
|
|
4839
|
-
workdir: workdirScreen(run),
|
|
4840
5025
|
remote: remoteScreen(run),
|
|
4841
|
-
hosted: hostedScreen(run)
|
|
4842
|
-
|
|
5026
|
+
hosted: hostedScreen(run),
|
|
5027
|
+
workdir: workdirScreen(run)
|
|
5028
|
+
}, "remote", FRESH);
|
|
4843
5029
|
if (flow.type === "exited" || !isChosen(flow.model)) {
|
|
4844
5030
|
ctx.ui.info("Skipping remote sync; run `rde init --only git-sync` to set it up later");
|
|
4845
5031
|
return "skipped";
|
|
@@ -4865,8 +5051,8 @@ function gitSyncStep(deps = hostDeps$10) {
|
|
|
4865
5051
|
throw error;
|
|
4866
5052
|
}
|
|
4867
5053
|
},
|
|
4868
|
-
run(ctx) {
|
|
4869
|
-
return runGitSync(ctx, deps(ctx));
|
|
5054
|
+
async run(ctx) {
|
|
5055
|
+
return { outcome: await runGitSync(ctx, deps(ctx)) };
|
|
4870
5056
|
}
|
|
4871
5057
|
};
|
|
4872
5058
|
}
|
|
@@ -5112,7 +5298,7 @@ function agentsSkillsStep(options, deps = hostDeps$9) {
|
|
|
5112
5298
|
const selection = await selectAgents(ctx, all, options);
|
|
5113
5299
|
if (selection.agents.length === 0) {
|
|
5114
5300
|
ctx.ui.info(skipLine(all, selection.source));
|
|
5115
|
-
return "skipped";
|
|
5301
|
+
return { outcome: "skipped" };
|
|
5116
5302
|
}
|
|
5117
5303
|
const skills = skillsCli(resolved.runner);
|
|
5118
5304
|
const agents = selection.agents.map((agent) => agent.skillsCliName);
|
|
@@ -5137,7 +5323,7 @@ function agentsSkillsStep(options, deps = hostDeps$9) {
|
|
|
5137
5323
|
if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
|
|
5138
5324
|
ctx.ui.note(installedLines(requiredEntries(installed), selection.agents), "Skills installed");
|
|
5139
5325
|
ctx.ui.info(MECHANICS_NOTE);
|
|
5140
|
-
return "completed";
|
|
5326
|
+
return { outcome: "completed" };
|
|
5141
5327
|
}
|
|
5142
5328
|
};
|
|
5143
5329
|
}
|
|
@@ -5225,15 +5411,23 @@ async function connectDatabases(ctx, deps, start) {
|
|
|
5225
5411
|
});
|
|
5226
5412
|
return {
|
|
5227
5413
|
existing: existing.data.length,
|
|
5228
|
-
created: result.created.length
|
|
5414
|
+
created: result.created.length,
|
|
5415
|
+
added: result.added
|
|
5229
5416
|
};
|
|
5230
5417
|
}
|
|
5231
5418
|
async function addDatabases(ctx, deps) {
|
|
5232
5419
|
const connected = await connectDatabases(ctx, deps, "engine");
|
|
5233
|
-
return
|
|
5420
|
+
return {
|
|
5421
|
+
outcome: connected.existing + connected.created > 0 ? "completed" : "skipped",
|
|
5422
|
+
result: { addedDatabases: connected.added }
|
|
5423
|
+
};
|
|
5234
5424
|
}
|
|
5235
5425
|
async function askForOwnDatabase(ctx, deps) {
|
|
5236
|
-
|
|
5426
|
+
const connected = await connectDatabases(ctx, deps, "connection-string");
|
|
5427
|
+
return {
|
|
5428
|
+
outcome: connected.created > 0 ? "completed" : "skipped",
|
|
5429
|
+
result: { addedDatabases: connected.added }
|
|
5430
|
+
};
|
|
5237
5431
|
}
|
|
5238
5432
|
function hostDeps$8(ctx) {
|
|
5239
5433
|
return hostDatabasesDeps(ctx.signal);
|
|
@@ -5274,9 +5468,12 @@ function databasesStep(options, deps = hostDeps$8) {
|
|
|
5274
5468
|
throw error;
|
|
5275
5469
|
}
|
|
5276
5470
|
},
|
|
5277
|
-
run(ctx) {
|
|
5278
|
-
|
|
5279
|
-
|
|
5471
|
+
async run(ctx) {
|
|
5472
|
+
switch (options.request) {
|
|
5473
|
+
case "ask-once": return askForOwnDatabase(ctx, deps(ctx));
|
|
5474
|
+
case "named": return addDatabases(ctx, deps(ctx));
|
|
5475
|
+
case "sample-stands-in": return { outcome: "skipped" };
|
|
5476
|
+
}
|
|
5280
5477
|
}
|
|
5281
5478
|
};
|
|
5282
5479
|
}
|
|
@@ -5420,7 +5617,7 @@ function installMbStep(deps = hostDeps$7) {
|
|
|
5420
5617
|
const before = await inspectMb(resolved.runner);
|
|
5421
5618
|
if (before.installed && before.satisfies) {
|
|
5422
5619
|
ctx.ui.success(`mb ${before.version} is installed`);
|
|
5423
|
-
return "completed";
|
|
5620
|
+
return { outcome: "completed" };
|
|
5424
5621
|
}
|
|
5425
5622
|
if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
5426
5623
|
const install = mbInstallCommand(resolved.installedWith);
|
|
@@ -5429,7 +5626,7 @@ function installMbStep(deps = hostDeps$7) {
|
|
|
5429
5626
|
const after = await inspectMb(resolved.runner);
|
|
5430
5627
|
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
5431
5628
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
5432
|
-
return "completed";
|
|
5629
|
+
return { outcome: "completed" };
|
|
5433
5630
|
}
|
|
5434
5631
|
};
|
|
5435
5632
|
}
|
|
@@ -5487,7 +5684,7 @@ function licenseStep(deps = hostDeps$6) {
|
|
|
5487
5684
|
});
|
|
5488
5685
|
if (token.length === 0) {
|
|
5489
5686
|
ctx.ui.info("Skipping the license; remote sync needs one. Run `rde init --from license` to add it later");
|
|
5490
|
-
return "skipped";
|
|
5687
|
+
return { outcome: "skipped" };
|
|
5491
5688
|
}
|
|
5492
5689
|
ctx.session.licenseToken.set(token);
|
|
5493
5690
|
const client = deps(ctx).client(configured.url, {
|
|
@@ -5518,7 +5715,7 @@ function licenseStep(deps = hostDeps$6) {
|
|
|
5518
5715
|
});
|
|
5519
5716
|
if (status.features.includes("remote-sync")) ctx.ui.info("Remote sync is included; the git step will set it up");
|
|
5520
5717
|
else ctx.ui.warn("This license does not include remote sync; the git step will be skipped");
|
|
5521
|
-
return "completed";
|
|
5718
|
+
return { outcome: "completed" };
|
|
5522
5719
|
}
|
|
5523
5720
|
};
|
|
5524
5721
|
}
|
|
@@ -5742,7 +5939,7 @@ function preflightStep(deps = hostDeps$5) {
|
|
|
5742
5939
|
const checks = await runChecks(resolved, resolved.platform);
|
|
5743
5940
|
ctx.ui.note(checkLines(checks).join("\n"), "This machine");
|
|
5744
5941
|
if (checks.some(blocks)) throw new ConfigError("Fix the items above and run rde init again");
|
|
5745
|
-
return "completed";
|
|
5942
|
+
return { outcome: "completed" };
|
|
5746
5943
|
}
|
|
5747
5944
|
};
|
|
5748
5945
|
}
|
|
@@ -6355,7 +6552,7 @@ function runtimeStep(options, deps = hostDeps$4) {
|
|
|
6355
6552
|
throw error;
|
|
6356
6553
|
}
|
|
6357
6554
|
spinner.stop(`Metabase installed on port ${chosen.port}`);
|
|
6358
|
-
return "completed";
|
|
6555
|
+
return { outcome: "completed" };
|
|
6359
6556
|
}
|
|
6360
6557
|
};
|
|
6361
6558
|
}
|
|
@@ -6391,13 +6588,13 @@ function sampleDatabaseUrl(port) {
|
|
|
6391
6588
|
function publishesOnLoopback$1(inspection, port) {
|
|
6392
6589
|
return inspection.ports.some((mapping) => mapping.container === POSTGRES_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
6393
6590
|
}
|
|
6394
|
-
function createOptions(state, home, alsoOn) {
|
|
6591
|
+
function createOptions(state, home, hostPort, alsoOn) {
|
|
6395
6592
|
return {
|
|
6396
6593
|
name: state.containerName,
|
|
6397
6594
|
home,
|
|
6398
6595
|
image: state.image,
|
|
6399
6596
|
port: {
|
|
6400
|
-
host:
|
|
6597
|
+
host: hostPort,
|
|
6401
6598
|
container: POSTGRES_PORT,
|
|
6402
6599
|
alsoOn
|
|
6403
6600
|
},
|
|
@@ -6407,6 +6604,9 @@ function createOptions(state, home, alsoOn) {
|
|
|
6407
6604
|
hostUser: null
|
|
6408
6605
|
};
|
|
6409
6606
|
}
|
|
6607
|
+
function requireOwnContainer(name, existing, home) {
|
|
6608
|
+
if (existing.home !== null && existing.home !== home) throw new ConfigError(`container ${name} belongs to the rde home ${existing.home} and is left alone`);
|
|
6609
|
+
}
|
|
6410
6610
|
function sampleDatabaseContainer(state, docker, home) {
|
|
6411
6611
|
const name = state.containerName;
|
|
6412
6612
|
function conflict(reason) {
|
|
@@ -6416,11 +6616,12 @@ function sampleDatabaseContainer(state, docker, home) {
|
|
|
6416
6616
|
const existing = await docker.containerInspect(name);
|
|
6417
6617
|
if (existing.exists) {
|
|
6418
6618
|
if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
|
|
6419
|
-
if (
|
|
6420
|
-
|
|
6619
|
+
if (publishesOnLoopback$1(existing, state.port)) return;
|
|
6620
|
+
requireOwnContainer(name, existing, home);
|
|
6621
|
+
await docker.containerForceRemove(name);
|
|
6421
6622
|
}
|
|
6422
6623
|
await docker.imagePull(state.image, { onLine: options.onProgress });
|
|
6423
|
-
await docker.containerCreate(createOptions(state, home, options.alsoOn));
|
|
6624
|
+
await docker.containerCreate(createOptions(state, home, state.port, options.alsoOn));
|
|
6424
6625
|
}
|
|
6425
6626
|
async function start() {
|
|
6426
6627
|
const existing = await docker.containerInspect(name);
|
|
@@ -6470,7 +6671,22 @@ function extraAddress(inspection) {
|
|
|
6470
6671
|
const extra = inspection.ports.find((mapping) => mapping.container === POSTGRES_PORT && mapping.hostIp !== "127.0.0.1");
|
|
6471
6672
|
return extra === void 0 ? null : extra.hostIp;
|
|
6472
6673
|
}
|
|
6473
|
-
async function
|
|
6674
|
+
async function startOnDockerPort(state, docker, home, alsoOn) {
|
|
6675
|
+
const name = state.containerName;
|
|
6676
|
+
await docker.containerForceRemove(name);
|
|
6677
|
+
await docker.containerCreate(createOptions(state, home, null, null));
|
|
6678
|
+
await docker.containerStart(name);
|
|
6679
|
+
const port = await docker.containerLoopbackPort(name, POSTGRES_PORT);
|
|
6680
|
+
await docker.containerForceRemove(name);
|
|
6681
|
+
const pinned = {
|
|
6682
|
+
...state,
|
|
6683
|
+
port
|
|
6684
|
+
};
|
|
6685
|
+
await docker.containerCreate(createOptions(pinned, home, port, alsoOn));
|
|
6686
|
+
await docker.containerStart(name);
|
|
6687
|
+
return pinned;
|
|
6688
|
+
}
|
|
6689
|
+
async function startOnFreePort(state, docker, home) {
|
|
6474
6690
|
try {
|
|
6475
6691
|
return {
|
|
6476
6692
|
outcome: await sampleDatabaseContainer(state, docker, home).start(),
|
|
@@ -6480,18 +6696,10 @@ async function startOnFreePort(state, docker, ports, home) {
|
|
|
6480
6696
|
if (!isPortTaken(error)) throw error;
|
|
6481
6697
|
}
|
|
6482
6698
|
const existing = await docker.containerInspect(state.containerName);
|
|
6483
|
-
if (existing.exists
|
|
6484
|
-
const moved = {
|
|
6485
|
-
...state,
|
|
6486
|
-
port: await ports.findFree(SAMPLE_DATABASE_PORT)
|
|
6487
|
-
};
|
|
6488
|
-
const alsoOn = existing.exists ? extraAddress(existing) : null;
|
|
6489
|
-
await docker.containerRemove(state.containerName);
|
|
6490
|
-
await docker.containerCreate(createOptions(moved, home, alsoOn));
|
|
6491
|
-
await docker.containerStart(moved.containerName);
|
|
6699
|
+
if (existing.exists) requireOwnContainer(state.containerName, existing, home);
|
|
6492
6700
|
return {
|
|
6493
6701
|
outcome: "started",
|
|
6494
|
-
state:
|
|
6702
|
+
state: await startOnDockerPort(state, docker, home, existing.exists ? extraAddress(existing) : null)
|
|
6495
6703
|
};
|
|
6496
6704
|
}
|
|
6497
6705
|
//#endregion
|
|
@@ -6585,7 +6793,7 @@ async function installAndStart(ctx, sidecar, installed, deps) {
|
|
|
6585
6793
|
throw error;
|
|
6586
6794
|
}
|
|
6587
6795
|
installing.stop(`Sample database container ${installed.containerName} on port ${installed.port}`);
|
|
6588
|
-
const started = await startOnFreePort(installed, deps.runtime.docker, deps.
|
|
6796
|
+
const started = await startOnFreePort(installed, deps.runtime.docker, deps.runtime.rdeHome);
|
|
6589
6797
|
const record = started.state.port === installed.port ? installed : await recordSampleDatabaseMove(ctx, installed, started.state);
|
|
6590
6798
|
const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
|
|
6591
6799
|
try {
|
|
@@ -6694,7 +6902,7 @@ function sampleDatabaseStep(deps = hostDeps$3) {
|
|
|
6694
6902
|
await connectDatabase(ctx, choice, configured, client, resolved);
|
|
6695
6903
|
await client.setting.enableTransforms();
|
|
6696
6904
|
ctx.ui.note(usageLines(record), "Sample database");
|
|
6697
|
-
return "completed";
|
|
6905
|
+
return { outcome: "completed" };
|
|
6698
6906
|
}
|
|
6699
6907
|
};
|
|
6700
6908
|
}
|
|
@@ -6706,7 +6914,6 @@ const PERCENT = 100;
|
|
|
6706
6914
|
function hostDeps$2(ctx) {
|
|
6707
6915
|
return {
|
|
6708
6916
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
6709
|
-
ports: hostPortProbe,
|
|
6710
6917
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
6711
6918
|
timing: DEFAULT_HEALTH_TIMING
|
|
6712
6919
|
};
|
|
@@ -6723,7 +6930,7 @@ function progressLabel(progress) {
|
|
|
6723
6930
|
async function startSampleDatabase(ctx, deps) {
|
|
6724
6931
|
const record = ctx.state.sampleDatabase;
|
|
6725
6932
|
if (record === null) return;
|
|
6726
|
-
const started = await startOnFreePort(record, deps.runtime.docker, deps.
|
|
6933
|
+
const started = await startOnFreePort(record, deps.runtime.docker, deps.runtime.rdeHome);
|
|
6727
6934
|
if (started.state.port !== record.port) await recordSampleDatabaseMove(ctx, record, started.state);
|
|
6728
6935
|
switch (started.outcome) {
|
|
6729
6936
|
case "missing":
|
|
@@ -6783,7 +6990,7 @@ function startMetabaseStep(deps = hostDeps$2) {
|
|
|
6783
6990
|
version
|
|
6784
6991
|
});
|
|
6785
6992
|
spinner.stop(`Metabase ${version} is running at ${configured.url}`);
|
|
6786
|
-
return "completed";
|
|
6993
|
+
return { outcome: "completed" };
|
|
6787
6994
|
}
|
|
6788
6995
|
};
|
|
6789
6996
|
}
|
|
@@ -6879,7 +7086,7 @@ function summaryStep(deps = hostDeps$1) {
|
|
|
6879
7086
|
async run(ctx) {
|
|
6880
7087
|
const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
|
|
6881
7088
|
ctx.ui.note(lines.join("\n"), "Your environment");
|
|
6882
|
-
return "completed";
|
|
7089
|
+
return { outcome: "completed" };
|
|
6883
7090
|
}
|
|
6884
7091
|
};
|
|
6885
7092
|
}
|
|
@@ -7016,16 +7223,16 @@ function fieldAnswer(value) {
|
|
|
7016
7223
|
return text$1(String(value));
|
|
7017
7224
|
}
|
|
7018
7225
|
function addDatabase(map, database, ordinal, last) {
|
|
7019
|
-
add(map,
|
|
7226
|
+
add(map, databaseKey(ordinal, "engine"), select$1(database.engine));
|
|
7020
7227
|
if (database.uri !== void 0) {
|
|
7021
|
-
add(map,
|
|
7022
|
-
add(map,
|
|
7228
|
+
add(map, databaseKey(ordinal, "mode"), select$1("paste"));
|
|
7229
|
+
add(map, databaseKey(ordinal, "uri"), text$1(database.uri));
|
|
7023
7230
|
}
|
|
7024
|
-
if (database.uri === void 0 && EXAMPLE_URIS[database.engine] !== void 0) add(map,
|
|
7231
|
+
if (database.uri === void 0 && EXAMPLE_URIS[database.engine] !== void 0) add(map, databaseKey(ordinal, "mode"), select$1("manual"));
|
|
7025
7232
|
if (database.fields !== void 0) for (const [name, value] of Object.entries(database.fields)) add(map, fieldKey(ordinal, name), fieldAnswer(value));
|
|
7026
|
-
add(map,
|
|
7027
|
-
add(map,
|
|
7028
|
-
add(map,
|
|
7233
|
+
add(map, databaseKey(ordinal, "name"), text$1(database.name ?? ACCEPT_DEFAULT));
|
|
7234
|
+
add(map, databaseKey(ordinal, "action"), select$1("test"));
|
|
7235
|
+
add(map, databaseKey(ordinal, "another"), select$1(last ? "finish" : "another"));
|
|
7029
7236
|
}
|
|
7030
7237
|
function addAdmin(map, admin) {
|
|
7031
7238
|
if (admin.variant !== "prompted") return;
|
|
@@ -7083,17 +7290,12 @@ function keyedAnswers(answers) {
|
|
|
7083
7290
|
const license = answers.license;
|
|
7084
7291
|
if (license !== void 0) add(map, LICENSE_KEYS.token, password$1(license.token ?? SKIP_LICENSE));
|
|
7085
7292
|
const databases = answers.databases;
|
|
7086
|
-
if (databases !== void 0) {
|
|
7087
|
-
|
|
7088
|
-
|
|
7089
|
-
add(map, DATABASE_KEYS.skip, confirm$1(true));
|
|
7090
|
-
}
|
|
7091
|
-
databases.forEach((database, index) => {
|
|
7092
|
-
addDatabase(map, database, index + 1, index === databases.length - 1);
|
|
7093
|
-
});
|
|
7094
|
-
}
|
|
7293
|
+
if (databases !== void 0) databases.forEach((database, index) => {
|
|
7294
|
+
addDatabase(map, database, index + 1, index === databases.length - 1);
|
|
7295
|
+
});
|
|
7095
7296
|
const git = answers["git-sync"];
|
|
7096
|
-
if (git
|
|
7297
|
+
if (git?.location === "skip") add(map, GIT_SYNC_KEYS.remote, select$1(SKIP_GIT_SYNC));
|
|
7298
|
+
else if (git !== void 0) {
|
|
7097
7299
|
add(map, GIT_SYNC_KEYS.location, select$1(git.location));
|
|
7098
7300
|
if (git.path !== void 0) add(map, GIT_SYNC_KEYS.path, text$1(git.path));
|
|
7099
7301
|
if (git.remote !== void 0) add(map, GIT_SYNC_KEYS.remote, select$1(git.remote));
|
|
@@ -7137,18 +7339,26 @@ const FLAG_FOR_KEY$1 = {
|
|
|
7137
7339
|
[API_KEY_KEYS.password]: "--admin-password",
|
|
7138
7340
|
[API_KEY_KEYS.profile]: "--mb-profile",
|
|
7139
7341
|
[LICENSE_KEYS.token]: "--license-token or --skip-license",
|
|
7140
|
-
[DATABASE_KEYS.engine]: "--database or --skip-databases",
|
|
7141
|
-
[DATABASE_KEYS.uri]: "--database",
|
|
7142
7342
|
[DATABASE_KEYS.skip]: "--skip-databases",
|
|
7143
|
-
[GIT_SYNC_KEYS.location]: "--git-dir
|
|
7343
|
+
[GIT_SYNC_KEYS.location]: "--git-dir",
|
|
7144
7344
|
[GIT_SYNC_KEYS.path]: "--git-dir",
|
|
7145
|
-
[GIT_SYNC_KEYS.remote]: "--git-remote",
|
|
7345
|
+
[GIT_SYNC_KEYS.remote]: "--git-remote or --skip-git",
|
|
7146
7346
|
[GIT_SYNC_KEYS.url]: "--git-url",
|
|
7147
7347
|
[GIT_SYNC_KEYS.token]: "--git-token"
|
|
7148
7348
|
};
|
|
7349
|
+
const DATABASE_FLAG_FOR_NAME = {
|
|
7350
|
+
engine: "--database or --skip-databases",
|
|
7351
|
+
uri: "--database"
|
|
7352
|
+
};
|
|
7149
7353
|
function howToAnswer(key) {
|
|
7150
7354
|
const field = parseFieldKey(key);
|
|
7151
7355
|
if (field !== null) return `set "${field.name}" under "fields" of database ${field.ordinal} in the --answers file (for a pasted database, in its connection string)`;
|
|
7356
|
+
const database = parseDatabaseKey(key);
|
|
7357
|
+
if (database !== null) {
|
|
7358
|
+
const flag = DATABASE_FLAG_FOR_NAME[database.name];
|
|
7359
|
+
const inFile = `set database ${database.ordinal} in the --answers file`;
|
|
7360
|
+
return flag === void 0 ? inFile : `pass ${flag}, or ${inFile}`;
|
|
7361
|
+
}
|
|
7152
7362
|
const flag = FLAG_FOR_KEY$1[key];
|
|
7153
7363
|
const inFile = `set "${key}" in the --answers file`;
|
|
7154
7364
|
return flag === void 0 ? inFile : `pass ${flag}, or ${inFile}`;
|
|
@@ -7741,17 +7951,30 @@ function requireKnownStep(steps, id, flag) {
|
|
|
7741
7951
|
if (id === void 0) return;
|
|
7742
7952
|
if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
|
|
7743
7953
|
}
|
|
7954
|
+
async function detectOne(step, ctx, options, fromHere) {
|
|
7955
|
+
if (step.id === (options.only ?? options.from)) return {
|
|
7956
|
+
step,
|
|
7957
|
+
status: "todo",
|
|
7958
|
+
forced: true
|
|
7959
|
+
};
|
|
7960
|
+
const status = await step.detect(ctx);
|
|
7961
|
+
if (options.only === void 0 && fromHere && status !== "not-applicable") return {
|
|
7962
|
+
step,
|
|
7963
|
+
status: "todo",
|
|
7964
|
+
forced: true
|
|
7965
|
+
};
|
|
7966
|
+
return {
|
|
7967
|
+
step,
|
|
7968
|
+
status,
|
|
7969
|
+
forced: false
|
|
7970
|
+
};
|
|
7971
|
+
}
|
|
7744
7972
|
async function detectAll(steps, ctx, options) {
|
|
7745
7973
|
const detected = [];
|
|
7746
7974
|
let fromHere = false;
|
|
7747
7975
|
for (const step of steps) {
|
|
7748
7976
|
if (step.id === options.from) fromHere = true;
|
|
7749
|
-
|
|
7750
|
-
detected.push({
|
|
7751
|
-
step,
|
|
7752
|
-
status: forced ? "todo" : await step.detect(ctx),
|
|
7753
|
-
forced
|
|
7754
|
-
});
|
|
7977
|
+
detected.push(await detectOne(step, ctx, options, fromHere));
|
|
7755
7978
|
}
|
|
7756
7979
|
return detected;
|
|
7757
7980
|
}
|
|
@@ -7790,13 +8013,16 @@ async function runWizard(steps, ctx, options = {}) {
|
|
|
7790
8013
|
title: step.title,
|
|
7791
8014
|
status
|
|
7792
8015
|
})));
|
|
8016
|
+
const results = [];
|
|
7793
8017
|
for (const entry of detected) try {
|
|
7794
8018
|
if (!await shouldRun(entry, ctx, options)) continue;
|
|
7795
|
-
const outcome = await entry.step.run(ctx);
|
|
8019
|
+
const { outcome, result } = await entry.step.run(ctx);
|
|
8020
|
+
if (result !== void 0) results.push(result);
|
|
7796
8021
|
await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
|
|
7797
8022
|
} catch (error) {
|
|
7798
8023
|
throw failureAt(entry.step, error);
|
|
7799
8024
|
}
|
|
8025
|
+
return results;
|
|
7800
8026
|
}
|
|
7801
8027
|
//#endregion
|
|
7802
8028
|
//#region src/steps/doctor.ts
|
|
@@ -8690,73 +8916,11 @@ function isLocation(value) {
|
|
|
8690
8916
|
function isRemote(value) {
|
|
8691
8917
|
return value === "local-bare" || value === "hosted";
|
|
8692
8918
|
}
|
|
8693
|
-
function applyConnectionRecord(draft, value) {
|
|
8694
|
-
draft.databases = draft.databases.filter((database) => !database.pasted);
|
|
8695
|
-
if (value === null) return;
|
|
8696
|
-
if (value.trim().length === 0) {
|
|
8697
|
-
draft.databasesFinished = true;
|
|
8698
|
-
return;
|
|
8699
|
-
}
|
|
8700
|
-
draft.databases.push({
|
|
8701
|
-
engine: null,
|
|
8702
|
-
uri: value.trim(),
|
|
8703
|
-
fields: {},
|
|
8704
|
-
name: null,
|
|
8705
|
-
pasted: true,
|
|
8706
|
-
added: false
|
|
8707
|
-
});
|
|
8708
|
-
}
|
|
8709
|
-
function applyEngineRecord(draft, value) {
|
|
8710
|
-
if (draft.databases.at(-1)?.added === false) draft.databases.pop();
|
|
8711
|
-
if (value === "finish") draft.databasesFinished = true;
|
|
8712
|
-
else if (value !== null) draft.databases.push({
|
|
8713
|
-
engine: value,
|
|
8714
|
-
uri: null,
|
|
8715
|
-
fields: {},
|
|
8716
|
-
name: null,
|
|
8717
|
-
pasted: false,
|
|
8718
|
-
added: false
|
|
8719
|
-
});
|
|
8720
|
-
}
|
|
8721
8919
|
function applyDatabaseRecord(draft, record) {
|
|
8722
8920
|
const value = stringOf(record.answer);
|
|
8723
|
-
|
|
8724
|
-
|
|
8725
|
-
|
|
8726
|
-
}
|
|
8727
|
-
if (record.key === DATABASE_KEYS.engine) {
|
|
8728
|
-
applyEngineRecord(draft, value);
|
|
8729
|
-
return;
|
|
8730
|
-
}
|
|
8731
|
-
const database = draft.databases.at(-1);
|
|
8732
|
-
if (database === void 0) return;
|
|
8733
|
-
const switched = record.key === DATABASE_KEYS.switchEngine && value !== "try-again";
|
|
8734
|
-
if ((record.key === DATABASE_KEYS.detectedEngine || switched) && value !== null) {
|
|
8735
|
-
database.engine = value;
|
|
8736
|
-
return;
|
|
8737
|
-
}
|
|
8738
|
-
if (record.key === DATABASE_KEYS.another) {
|
|
8739
|
-
database.added = true;
|
|
8740
|
-
return;
|
|
8741
|
-
}
|
|
8742
|
-
if (record.key === DATABASE_KEYS.mode) {
|
|
8743
|
-
database.fields = {};
|
|
8744
|
-
if (value === "manual") database.uri = null;
|
|
8745
|
-
return;
|
|
8746
|
-
}
|
|
8747
|
-
if (record.key === DATABASE_KEYS.uri && value !== null) {
|
|
8748
|
-
database.uri = value;
|
|
8749
|
-
return;
|
|
8750
|
-
}
|
|
8751
|
-
if (record.key === DATABASE_KEYS.name && value !== null && value.length > 0) {
|
|
8752
|
-
database.name = value;
|
|
8753
|
-
return;
|
|
8754
|
-
}
|
|
8755
|
-
const fieldName = parseFieldKey(record.key)?.name;
|
|
8756
|
-
if (fieldName === void 0) return;
|
|
8757
|
-
if (record.answer.kind === "confirm") database.fields[fieldName] = record.answer.value;
|
|
8758
|
-
else if (record.answer.kind === "password") database.fields[fieldName] = envReference(databaseEnv(draft.databases.length - 1, fieldName));
|
|
8759
|
-
else if (value !== null) database.fields[fieldName] = value;
|
|
8921
|
+
const skippedConnection = record.key === DATABASE_KEYS.connection && value?.trim() === "";
|
|
8922
|
+
const finished = parseDatabaseKey(record.key)?.name === "engine" && value === "finish";
|
|
8923
|
+
if (skippedConnection || finished) draft.databasesFinished = true;
|
|
8760
8924
|
}
|
|
8761
8925
|
function applyAdminRecord(draft, key, value) {
|
|
8762
8926
|
if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
|
|
@@ -8765,7 +8929,8 @@ function applyAdminRecord(draft, key, value) {
|
|
|
8765
8929
|
else if (key === ADMIN_USER_KEYS.lastName && value !== null && value.length > 0) admin(draft)["last-name"] = value;
|
|
8766
8930
|
}
|
|
8767
8931
|
function applyGitRecord(draft, key, value) {
|
|
8768
|
-
if (key === GIT_SYNC_KEYS.
|
|
8932
|
+
if (key === GIT_SYNC_KEYS.remote && value === "skip") draft.answers["git-sync"] = { location: SKIP_GIT_SYNC };
|
|
8933
|
+
else if (key === GIT_SYNC_KEYS.location && value !== null && isLocation(value)) gitSync(draft).location = value;
|
|
8769
8934
|
else if (key === GIT_SYNC_KEYS.path && value !== null) gitSync(draft).path = value;
|
|
8770
8935
|
else if (key === GIT_SYNC_KEYS.remote && value !== null && isRemote(value)) gitSync(draft).remote = value;
|
|
8771
8936
|
else if (key === GIT_SYNC_KEYS.url && value !== null) gitSync(draft).url = value;
|
|
@@ -8793,30 +8958,26 @@ function writableUri(uri, index) {
|
|
|
8793
8958
|
const trimmed = uri.trim();
|
|
8794
8959
|
return URI_PASSWORDS.some((pattern) => pattern.test(trimmed)) ? envReference(databaseEnv(index, "uri")) : trimmed;
|
|
8795
8960
|
}
|
|
8796
|
-
function
|
|
8797
|
-
if (
|
|
8798
|
-
|
|
8799
|
-
|
|
8800
|
-
|
|
8801
|
-
|
|
8802
|
-
const
|
|
8803
|
-
const engine = engineOf(database);
|
|
8804
|
-
if (!database.added || database.uri === null && !hasFields || engine === null) return null;
|
|
8961
|
+
function databaseEntry(added, index) {
|
|
8962
|
+
if (added.uri !== null) return {
|
|
8963
|
+
engine: added.engine,
|
|
8964
|
+
uri: writableUri(added.uri, index),
|
|
8965
|
+
name: added.name
|
|
8966
|
+
};
|
|
8967
|
+
const fields = added.fields.map(({ name, value, secret }) => [name, secret ? envReference(databaseEnv(index, name)) : value]);
|
|
8805
8968
|
return {
|
|
8806
|
-
engine,
|
|
8807
|
-
|
|
8808
|
-
|
|
8809
|
-
...database.name !== null && { name: database.name }
|
|
8969
|
+
engine: added.engine,
|
|
8970
|
+
fields: Object.fromEntries(fields),
|
|
8971
|
+
name: added.name
|
|
8810
8972
|
};
|
|
8811
8973
|
}
|
|
8812
|
-
function answersFromRecords(records, overrides) {
|
|
8974
|
+
function answersFromRecords(records, overrides, added) {
|
|
8813
8975
|
const draft = {
|
|
8814
8976
|
answers: {},
|
|
8815
|
-
databases: [],
|
|
8816
8977
|
databasesFinished: false
|
|
8817
8978
|
};
|
|
8818
8979
|
for (const record of records) applyRecord(draft, record);
|
|
8819
|
-
const databases =
|
|
8980
|
+
const databases = added.map(databaseEntry);
|
|
8820
8981
|
if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
|
|
8821
8982
|
return {
|
|
8822
8983
|
...draft.answers,
|
|
@@ -9063,9 +9224,10 @@ const init = defineRdeCommand({
|
|
|
9063
9224
|
prompts: recording?.prompts ?? answering
|
|
9064
9225
|
});
|
|
9065
9226
|
ctx.ui.intro("rde init");
|
|
9066
|
-
await runWizard(initSteps(stepOptionsFrom(answers, { interactive: stdinIsTty() && !runOptions.yes })), ctx, options);
|
|
9227
|
+
const results = await runWizard(initSteps(stepOptionsFrom(answers, { interactive: stdinIsTty() && !runOptions.yes })), ctx, options);
|
|
9067
9228
|
if (recording !== null && printTo !== void 0) {
|
|
9068
|
-
|
|
9229
|
+
const added = results.flatMap((result) => result.addedDatabases);
|
|
9230
|
+
await writeAnswersFile(printTo, answersFromRecords(recording.records, answers, added));
|
|
9069
9231
|
ctx.ui.info(`Answers written to ${printTo}`);
|
|
9070
9232
|
}
|
|
9071
9233
|
ctx.ui.outro("rde init finished");
|
|
@@ -9771,7 +9933,6 @@ const RELEASE_TAG = /^v?[01]\.(\d+\.\d+(?:\.\d+)?)/;
|
|
|
9771
9933
|
function hostDeps(ctx) {
|
|
9772
9934
|
return {
|
|
9773
9935
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
9774
|
-
ports: hostPortProbe,
|
|
9775
9936
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
9776
9937
|
timing: DEFAULT_HEALTH_TIMING,
|
|
9777
9938
|
releases: releaseLookup(ctx.signal),
|
|
@@ -10033,7 +10194,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
10033
10194
|
const main = defineCommand({
|
|
10034
10195
|
meta: {
|
|
10035
10196
|
name: "rde",
|
|
10036
|
-
version: "0.1.
|
|
10197
|
+
version: "0.1.9",
|
|
10037
10198
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
10038
10199
|
},
|
|
10039
10200
|
subCommands: {
|