@metabase/rde 0.1.5 → 0.1.7
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 +18 -18
- package/dist/cli.mjs +1249 -877
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { defineCommand,
|
|
2
|
+
import { defineCommand, renderUsage, runMain } from "citty";
|
|
3
3
|
import { createWriteStream, promises } from "node:fs";
|
|
4
4
|
import { delimiter, dirname, isAbsolute, join, resolve } from "node:path";
|
|
5
5
|
import { ZodError, z } from "zod";
|
|
6
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
6
7
|
import { homedir, tmpdir, userInfo } from "node:os";
|
|
7
8
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
9
|
import { Readable } from "node:stream";
|
|
@@ -11,7 +12,6 @@ import { confirm, intro, isCancel, log, multiselect, note, outro, password, sele
|
|
|
11
12
|
import { spawn } from "node:child_process";
|
|
12
13
|
import { connect, createServer } from "node:net";
|
|
13
14
|
import semver from "semver";
|
|
14
|
-
import { randomBytes } from "node:crypto";
|
|
15
15
|
//#region src/errors.ts
|
|
16
16
|
var AbortError = class extends Error {
|
|
17
17
|
constructor(message = "interrupted") {
|
|
@@ -25,6 +25,14 @@ var ConfigError = class extends Error {
|
|
|
25
25
|
this.name = "ConfigError";
|
|
26
26
|
}
|
|
27
27
|
};
|
|
28
|
+
var StepFailedError = class extends Error {
|
|
29
|
+
step;
|
|
30
|
+
constructor(step, cause) {
|
|
31
|
+
super(`step ${step.id} failed`, { cause });
|
|
32
|
+
this.name = "StepFailedError";
|
|
33
|
+
this.step = step;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
28
36
|
var ExternalToolError = class extends Error {
|
|
29
37
|
tool;
|
|
30
38
|
argv;
|
|
@@ -91,11 +99,19 @@ const INTERRUPTED_MESSAGE = "Interrupted. Run `rde init` again to resume.";
|
|
|
91
99
|
const FAILURE_EXIT_CODE = 1;
|
|
92
100
|
const USAGE_EXIT_CODE = 2;
|
|
93
101
|
function exitCodeFor(error) {
|
|
102
|
+
if (error instanceof StepFailedError) return exitCodeFor(error.cause);
|
|
94
103
|
if (error instanceof AbortError) return 130;
|
|
95
104
|
if (error instanceof ConfigError) return USAGE_EXIT_CODE;
|
|
96
105
|
return FAILURE_EXIT_CODE;
|
|
97
106
|
}
|
|
107
|
+
function stepFailureMessage(step, cause) {
|
|
108
|
+
const stopped = `rde init stopped at "${step.title}"`;
|
|
109
|
+
const resume = `Resume with rde init --from ${step.id}.`;
|
|
110
|
+
if (cause.includes("\n")) return `${cause}\n${stopped}. ${resume}`;
|
|
111
|
+
return `${stopped}: ${cause.replace(/\.$/, "")}. ${resume}`;
|
|
112
|
+
}
|
|
98
113
|
function messageFor$1(error) {
|
|
114
|
+
if (error instanceof StepFailedError) return stepFailureMessage(error.step, messageFor$1(error.cause));
|
|
99
115
|
if (error instanceof AbortError) return INTERRUPTED_MESSAGE;
|
|
100
116
|
if (error instanceof Error) return error.message;
|
|
101
117
|
return String(error);
|
|
@@ -130,6 +146,10 @@ function printLines(lines) {
|
|
|
130
146
|
function printJson(value) {
|
|
131
147
|
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
132
148
|
}
|
|
149
|
+
const ESCAPE_SEQUENCE = new RegExp(`${String.fromCodePoint(27)}(?:\\[[0-?]*[ -/]*[@-~]|[@-Z\\\\-_])`, "g");
|
|
150
|
+
function withoutEscapes(text) {
|
|
151
|
+
return text.replace(ESCAPE_SEQUENCE, "");
|
|
152
|
+
}
|
|
133
153
|
const StepId = z.enum([
|
|
134
154
|
"preflight",
|
|
135
155
|
"install-mb",
|
|
@@ -378,16 +398,14 @@ function readEnv(name) {
|
|
|
378
398
|
return process.env[name];
|
|
379
399
|
}
|
|
380
400
|
const ENV_PATH = "PATH";
|
|
401
|
+
const ENV_NO_COLOR = "NO_COLOR";
|
|
381
402
|
const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
|
|
382
403
|
const ENV_JAVA_HOME = "JAVA_HOME";
|
|
383
404
|
const ENV_MB_PROFILE = "MB_PROFILE";
|
|
384
405
|
const ENV_MB_URL = "MB_URL";
|
|
385
406
|
const ENV_MB_API_KEY = "MB_API_KEY";
|
|
386
|
-
function
|
|
387
|
-
return {
|
|
388
|
-
...process.env,
|
|
389
|
-
...overrides
|
|
390
|
-
};
|
|
407
|
+
function hostEnvironment() {
|
|
408
|
+
return { ...process.env };
|
|
391
409
|
}
|
|
392
410
|
//#endregion
|
|
393
411
|
//#region src/state/paths.ts
|
|
@@ -400,11 +418,16 @@ const SECRETS_FILE = "secrets.json";
|
|
|
400
418
|
const GLOBAL_CONFIG_FILE = "config.json";
|
|
401
419
|
const JARS_DIR = "jars";
|
|
402
420
|
const JAR_FILE$1 = "metabase.jar";
|
|
421
|
+
const HOME_TAG_LENGTH = 8;
|
|
403
422
|
function rdeHome() {
|
|
404
423
|
const override = readEnv(ENV_RDE_HOME);
|
|
405
|
-
if (override !== void 0) return override;
|
|
424
|
+
if (override !== void 0) return resolve(override);
|
|
406
425
|
return join(homedir(), RDE_DIR_NAME);
|
|
407
426
|
}
|
|
427
|
+
function homeTag(home) {
|
|
428
|
+
if (home === join(homedir(), RDE_DIR_NAME)) return null;
|
|
429
|
+
return createHash("sha256").update(home).digest("hex").slice(0, HOME_TAG_LENGTH);
|
|
430
|
+
}
|
|
408
431
|
function instancesDir() {
|
|
409
432
|
return join(rdeHome(), INSTANCES_DIR);
|
|
410
433
|
}
|
|
@@ -544,509 +567,183 @@ async function writeGlobalConfig(config) {
|
|
|
544
567
|
await writeJsonFile(globalConfigPath(), config);
|
|
545
568
|
}
|
|
546
569
|
//#endregion
|
|
547
|
-
//#region src/
|
|
548
|
-
const
|
|
549
|
-
const
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
570
|
+
//#region src/metabase/domain/health.ts
|
|
571
|
+
const Health = z.object({ status: z.literal("ok") }).loose();
|
|
572
|
+
const Initializing = z.object({
|
|
573
|
+
status: z.literal("initializing"),
|
|
574
|
+
progress: z.number()
|
|
575
|
+
}).loose();
|
|
576
|
+
//#endregion
|
|
577
|
+
//#region src/metabase/http.ts
|
|
578
|
+
const REQUEST_TIMEOUT_MS = 3e4;
|
|
579
|
+
const PARTIAL_SUFFIX = ".part";
|
|
580
|
+
const CONTENT_LENGTH_HEADER = "content-length";
|
|
581
|
+
const SESSION_HEADER = "X-Metabase-Session";
|
|
582
|
+
const API_KEY_HEADER = "X-Api-Key";
|
|
583
|
+
const JSON_CONTENT_TYPE = "application/json";
|
|
584
|
+
const SERVICE_UNAVAILABLE_STATUS = 503;
|
|
585
|
+
const NO_AUTH = { kind: "none" };
|
|
586
|
+
const ErrorMessage = z.object({ message: z.string() }).loose();
|
|
587
|
+
const ErrorFields = z.object({ errors: z.record(z.string(), z.string()) }).loose();
|
|
588
|
+
function authHeaders(auth) {
|
|
589
|
+
switch (auth.kind) {
|
|
590
|
+
case "none": return {};
|
|
591
|
+
case "session": return { [SESSION_HEADER]: auth.id };
|
|
592
|
+
case "api-key": return { [API_KEY_HEADER]: auth.key };
|
|
593
|
+
}
|
|
553
594
|
}
|
|
554
|
-
function
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
url: state.url,
|
|
560
|
-
email: admin.email
|
|
595
|
+
function parseBody(status, text) {
|
|
596
|
+
if (text.length === 0) return {
|
|
597
|
+
status,
|
|
598
|
+
text,
|
|
599
|
+
json: { parsed: false }
|
|
561
600
|
};
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
password: storedPassword(state, secrets),
|
|
571
|
-
note: LEGACY_NOTE
|
|
601
|
+
try {
|
|
602
|
+
return {
|
|
603
|
+
status,
|
|
604
|
+
text,
|
|
605
|
+
json: {
|
|
606
|
+
parsed: true,
|
|
607
|
+
value: JSON.parse(text)
|
|
608
|
+
}
|
|
572
609
|
};
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
610
|
+
} catch {
|
|
611
|
+
return {
|
|
612
|
+
status,
|
|
613
|
+
text,
|
|
614
|
+
json: { parsed: false }
|
|
577
615
|
};
|
|
578
616
|
}
|
|
579
617
|
}
|
|
580
|
-
function
|
|
581
|
-
|
|
582
|
-
const password = report.password === null ? [] : [`Password: ${report.password}`];
|
|
583
|
-
const note = report.note === null ? [] : [report.note];
|
|
584
|
-
return [
|
|
585
|
-
...url,
|
|
586
|
-
`Email: ${report.email}`,
|
|
587
|
-
...password,
|
|
588
|
-
...note
|
|
589
|
-
];
|
|
618
|
+
function jsonValue(body) {
|
|
619
|
+
return body.json.parsed ? body.json.value : void 0;
|
|
590
620
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
}
|
|
606
|
-
}
|
|
621
|
+
function serverMessage(body) {
|
|
622
|
+
const parsed = ErrorMessage.safeParse(jsonValue(body));
|
|
623
|
+
if (parsed.success) return parsed.data.message;
|
|
624
|
+
const text = body.text.trim();
|
|
625
|
+
return text.length === 0 ? `HTTP ${body.status}` : text;
|
|
626
|
+
}
|
|
627
|
+
function serverErrors(body) {
|
|
628
|
+
const parsed = ErrorFields.safeParse(jsonValue(body));
|
|
629
|
+
return parsed.success ? parsed.data.errors : null;
|
|
630
|
+
}
|
|
631
|
+
function failureFor(path, body) {
|
|
632
|
+
if (body.status === SERVICE_UNAVAILABLE_STATUS) {
|
|
633
|
+
const initializing = Initializing.safeParse(jsonValue(body));
|
|
634
|
+
if (initializing.success) return new MetabaseInitializingError(initializing.data.progress);
|
|
607
635
|
}
|
|
608
|
-
return
|
|
636
|
+
return new MetabaseApiError({
|
|
637
|
+
status: body.status,
|
|
638
|
+
path,
|
|
639
|
+
message: serverMessage(body),
|
|
640
|
+
errors: serverErrors(body)
|
|
641
|
+
});
|
|
609
642
|
}
|
|
610
|
-
function
|
|
611
|
-
return
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
643
|
+
function shapeError(path, body, reason) {
|
|
644
|
+
return new MetabaseApiError({
|
|
645
|
+
status: body.status,
|
|
646
|
+
path,
|
|
647
|
+
message: reason,
|
|
648
|
+
errors: null
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
function isTimeout(error) {
|
|
652
|
+
return error instanceof Error && error.name === "TimeoutError";
|
|
653
|
+
}
|
|
654
|
+
function describeCause(error) {
|
|
655
|
+
if (error instanceof Error && error.cause instanceof Error) return error.cause.message;
|
|
656
|
+
return error instanceof Error ? error.message : String(error);
|
|
657
|
+
}
|
|
658
|
+
function createHttp(options) {
|
|
659
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
660
|
+
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
661
|
+
async function send(method, path, body) {
|
|
662
|
+
const url = `${baseUrl}${path}`;
|
|
663
|
+
const headers = {
|
|
664
|
+
accept: JSON_CONTENT_TYPE,
|
|
665
|
+
...authHeaders(options.auth)
|
|
666
|
+
};
|
|
667
|
+
const init = {
|
|
668
|
+
method,
|
|
669
|
+
headers,
|
|
670
|
+
signal: AbortSignal.any([options.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)])
|
|
671
|
+
};
|
|
672
|
+
if (body !== void 0) {
|
|
673
|
+
headers["content-type"] = JSON_CONTENT_TYPE;
|
|
674
|
+
init.body = JSON.stringify(body);
|
|
675
|
+
}
|
|
676
|
+
try {
|
|
677
|
+
return await fetchImpl(url, init);
|
|
678
|
+
} catch (error) {
|
|
679
|
+
if (options.signal.aborted) throw new AbortError();
|
|
680
|
+
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${REQUEST_TIMEOUT_MS}ms`);
|
|
681
|
+
throw new MetabaseUnreachableError(url, describeCause(error));
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
async function requestBody(method, path, body) {
|
|
685
|
+
const response = await send(method, path, body);
|
|
686
|
+
const parsed = parseBody(response.status, await response.text());
|
|
687
|
+
if (!response.ok) throw failureFor(path, parsed);
|
|
688
|
+
return parsed;
|
|
689
|
+
}
|
|
690
|
+
return {
|
|
691
|
+
async requestJson(method, path, request) {
|
|
692
|
+
const body = await requestBody(method, path, request.body);
|
|
693
|
+
if (!body.json.parsed) throw shapeError(path, body, "expected a JSON body");
|
|
615
694
|
try {
|
|
616
|
-
|
|
695
|
+
return request.schema.parse(body.json.value);
|
|
617
696
|
} catch (error) {
|
|
618
|
-
|
|
697
|
+
if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
|
|
698
|
+
throw error;
|
|
619
699
|
}
|
|
700
|
+
},
|
|
701
|
+
async requestNoContent(method, path, request = {}) {
|
|
702
|
+
await requestBody(method, path, request.body);
|
|
620
703
|
}
|
|
621
|
-
}
|
|
704
|
+
};
|
|
622
705
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
const
|
|
627
|
-
|
|
628
|
-
return config.defaultInstance;
|
|
706
|
+
function contentLength(response) {
|
|
707
|
+
const header = response.headers.get(CONTENT_LENGTH_HEADER);
|
|
708
|
+
if (header === null) return null;
|
|
709
|
+
const total = Number.parseInt(header, 10);
|
|
710
|
+
return Number.isNaN(total) ? null : total;
|
|
629
711
|
}
|
|
630
|
-
|
|
712
|
+
function progressCounter(total, onProgress) {
|
|
713
|
+
let received = 0;
|
|
714
|
+
return (chunk) => {
|
|
715
|
+
received += chunk.byteLength;
|
|
716
|
+
onProgress({
|
|
717
|
+
received,
|
|
718
|
+
total
|
|
719
|
+
});
|
|
720
|
+
return chunk;
|
|
721
|
+
};
|
|
722
|
+
}
|
|
723
|
+
async function downloadFile(url, destination, options) {
|
|
724
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
725
|
+
let response;
|
|
631
726
|
try {
|
|
632
|
-
|
|
727
|
+
response = await fetchImpl(url, { signal: options.signal });
|
|
633
728
|
} catch (error) {
|
|
634
|
-
if (
|
|
635
|
-
throw error;
|
|
729
|
+
if (options.signal.aborted) throw new AbortError();
|
|
730
|
+
throw new DownloadError(url, describeCause(error));
|
|
636
731
|
}
|
|
637
|
-
}
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
const
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
} },
|
|
650
|
-
async run(args) {
|
|
651
|
-
const name = await defaultInstanceName();
|
|
652
|
-
const report = credentialsReport(await requireInstanceState(name), await readInstanceSecrets(name));
|
|
653
|
-
if (args.json) {
|
|
654
|
-
printJson(report);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
printLines(credentialsLines(report));
|
|
732
|
+
if (!response.ok) throw new DownloadError(url, `HTTP ${response.status}`);
|
|
733
|
+
if (response.body === null) throw new DownloadError(url, "empty response");
|
|
734
|
+
const partial = `${destination}${PARTIAL_SUFFIX}`;
|
|
735
|
+
const count = progressCounter(contentLength(response), options.onProgress);
|
|
736
|
+
try {
|
|
737
|
+
await pipeline(Readable.fromWeb(response.body), async function* (chunks) {
|
|
738
|
+
for await (const chunk of chunks) yield count(chunk);
|
|
739
|
+
}, createWriteStream(partial), { signal: options.signal });
|
|
740
|
+
} catch (error) {
|
|
741
|
+
await promises.rm(partial, { force: true });
|
|
742
|
+
if (options.signal.aborted) throw new AbortError();
|
|
743
|
+
throw new DownloadError(url, describeCause(error));
|
|
658
744
|
}
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
//#region src/connections/parse-uri.ts
|
|
662
|
-
var UriParseError = class extends Error {
|
|
663
|
-
engine;
|
|
664
|
-
reason;
|
|
665
|
-
example;
|
|
666
|
-
constructor(engine, reason) {
|
|
667
|
-
const example = EXAMPLE_URIS[engine] ?? null;
|
|
668
|
-
super(describeFailure(engine, reason, example));
|
|
669
|
-
this.name = "UriParseError";
|
|
670
|
-
this.engine = engine;
|
|
671
|
-
this.reason = reason;
|
|
672
|
-
this.example = example;
|
|
673
|
-
}
|
|
674
|
-
};
|
|
675
|
-
function describeFailure(engine, reason, example) {
|
|
676
|
-
if (reason === "unsupported-engine") return `Connection strings for ${engine} can't be parsed here; enter the fields one by one instead`;
|
|
677
|
-
return `That doesn't look like a ${engine} connection string${example === null ? "" : `, for example ${example}`}`;
|
|
678
|
-
}
|
|
679
|
-
const EXAMPLE_URIS = {
|
|
680
|
-
athena: "jdbc:athena://WorkGroup=primary;Region=us-east-1;",
|
|
681
|
-
"bigquery-cloud-sdk": "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;ProjectId=MyBigQueryProject;OAuthType=1;",
|
|
682
|
-
clickhouse: "jdbc:clickhouse:https://localhost:8443?ssl=true",
|
|
683
|
-
druid: "jdbc:avatica:remote:url=http://localhost:8888/druid/v2/sql/avatica/;transparent_reconnection=true",
|
|
684
|
-
"druid-jdbc": "jdbc:avatica:remote:url=http://localhost:8888/druid/v2/sql/avatica/;transparent_reconnection=true",
|
|
685
|
-
databricks: "jdbc:databricks://127.0.0.1:8123;httpPath=/sql/1.0/endpoints/abc;OAuthSecret=1234567890;OAuth2ClientId=xyz",
|
|
686
|
-
mongo: "mongodb://user:pass@localhost:27017/dbname?authSource=admin",
|
|
687
|
-
mysql: "jdbc:mysql://user:pass@host:3306/dbname?ssl=true",
|
|
688
|
-
oracle: "jdbc:oracle:thin:@mydbhost:1521/mydbservice?ssl_server_cert_dn=ServerDN",
|
|
689
|
-
postgres: "jdbc:postgresql://localhost:5432/mydb",
|
|
690
|
-
"presto-jdbc": "jdbc:presto://host:1234/sample-catalog/sample-schema?SSL=true&SSLTrustStorePassword=1234",
|
|
691
|
-
redshift: "jdbc:redshift://examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com:5439/dev",
|
|
692
|
-
snowflake: "snowflake://example.snowflakecomputing.com/?db=maindb&warehouse=mainwarehouse",
|
|
693
|
-
sparksql: "jdbc:sparksql:Server=127.0.0.1;",
|
|
694
|
-
sqlite: "jdbc:sqlite:///C:/path/to/database.db",
|
|
695
|
-
sqlserver: "jdbc:sqlserver://mydbhost:1433;databaseName=mydb",
|
|
696
|
-
starburst: "jdbc:trino://starburst.example.com:43011/hive/sales?user=test&password=secret&SSL=true&roles=system:myrole",
|
|
697
|
-
vertica: "jdbc:vertica://vertica.example.com:1234/databaseName?user=jane"
|
|
698
|
-
};
|
|
699
|
-
const jdbcPrefix = "(?<hasJdbcPrefix>jdbc:)?";
|
|
700
|
-
const userPass = "(?:(?<username>[^:/?#]+)(?::(?<password>[^@/?#]*))?@)?";
|
|
701
|
-
const hostAndPort = "(?<host>[^:/?#]+)?(?::(?<port>\\d+)?)?";
|
|
702
|
-
const params = "(?:(?<params>.*))?";
|
|
703
|
-
const semicolonParams = ";?(?<semicolonParams>.*)?;?";
|
|
704
|
-
const druidRegex = new RegExp("^" + jdbcPrefix + "(?<protocol>avatica)" + ":remote:url=(https?://)?(?<host>[^:/]+):(?<port>\\d+)(?<path>/[^;]*)" + semicolonParams + "$", "i");
|
|
705
|
-
const mongoRegex = new RegExp("^(?<protocol>mongodb(?:\\+srv)?)://" + userPass + "(?<host>[^/?#]+)?" + "(?:/(?<database>[^/?#]*))?" + "(?:\\?(?<params>.*))?" + "$", "i");
|
|
706
|
-
const CONNECTION_STRING_REGEXES = {
|
|
707
|
-
athena: [/* @__PURE__ */ new RegExp("^(?<hasJdbcPrefix>jdbc:)?(?<protocol>awsathena|athena)://((?<host>athena.*com)(?::(?<port>\\d+)?)?)?;?(?<semicolonParams>.*)?$", "i")],
|
|
708
|
-
redshift: [new RegExp("^" + jdbcPrefix + "(?<protocol>redshift)://" + userPass + hostAndPort + "(?:/(?<database>[^/?#;]*))?" + ";?" + "(?<semicolonParams>.*)?" + "$", "i")],
|
|
709
|
-
"bigquery-cloud-sdk": [new RegExp("^" + jdbcPrefix + "(?<protocol>bigquery)://" + "(https?://)?(?<host>[^:/]+)(?<path>/[^;]*):(?<port>\\d+)" + semicolonParams + "$", "i")],
|
|
710
|
-
clickhouse: [new RegExp("^" + jdbcPrefix + "(?<protocol>clickhouse|ch):(?:https?:)?//" + userPass + hostAndPort + "(?:/(?<database>[^/?#]*))?" + params)],
|
|
711
|
-
databricks: [new RegExp("^" + jdbcPrefix + "(?<protocol>databricks)://" + hostAndPort + "(?:/(?<schema>[^/?#]*))?" + semicolonParams + "$", "i")],
|
|
712
|
-
druid: [druidRegex],
|
|
713
|
-
"druid-jdbc": [druidRegex],
|
|
714
|
-
mongo: [mongoRegex],
|
|
715
|
-
mysql: [new RegExp("^" + jdbcPrefix + "(?<protocol>mysql)://" + userPass + hostAndPort + "(?:/(?<database>[^/?#]*))?" + params + "$", "i")],
|
|
716
|
-
postgres: [new RegExp("^" + jdbcPrefix + "(?<protocol>postgres(?:ql)?)://" + userPass + hostAndPort + "(?:/(?<database>[^/?#]*))?" + params + "$", "i")],
|
|
717
|
-
"presto-jdbc": [new RegExp("^" + jdbcPrefix + "(?<protocol>presto)://" + hostAndPort + "(?:/(?<catalog>[^/?#]*)/(?<schema>[^/?#]*))?" + params + "$", "i")],
|
|
718
|
-
oracle: [new RegExp("^" + jdbcPrefix + "(?<protocol>oracle):(?<subprotocol>thin):" + "(?:(?<username>[^@/]+)/(?<password>[^@/]+))?" + "@" + "(?<host>[^:/?#]+)(?::(?<port>\\d+))?(?:/(?<database>[^/?#]+))?" + params + "$", "i")],
|
|
719
|
-
snowflake: [new RegExp("^" + jdbcPrefix + "(?<protocol>snowflake)://" + userPass + hostAndPort + "(?:/(?<database>[^/?#]*)?)?" + params + "$", "i")],
|
|
720
|
-
sparksql: [new RegExp("^" + jdbcPrefix + "(?<protocol>sparksql):" + semicolonParams + "$", "i"), new RegExp("^" + jdbcPrefix + "(?<protocol>hive2)://" + hostAndPort + "(?:/(?<database>[^/?#;]*))?" + semicolonParams + "$", "i")],
|
|
721
|
-
sqlite: [new RegExp("^" + jdbcPrefix + "(?<protocol>sqlite):///(?<path>.+)$", "i")],
|
|
722
|
-
sqlserver: [new RegExp("^" + jdbcPrefix + "(?<protocol>sqlserver)://" + hostAndPort + semicolonParams + "$", "i")],
|
|
723
|
-
starburst: [new RegExp("^" + jdbcPrefix + "(?<protocol>trino)://" + hostAndPort + "(?:/(?<catalog>[^/?#]*)/(?<schema>[^/?#]*))?" + params + "$", "i")],
|
|
724
|
-
vertica: [new RegExp("^" + jdbcPrefix + "(?<protocol>vertica)://" + hostAndPort + "(?:/(?<database>[^/?#]*))?" + params + "$", "i")]
|
|
725
|
-
};
|
|
726
|
-
function parseUri(uri, engine) {
|
|
727
|
-
const regexes = CONNECTION_STRING_REGEXES[engine];
|
|
728
|
-
if (regexes === void 0) return new UriParseError(engine, "unsupported-engine");
|
|
729
|
-
const trimmed = uri.trim();
|
|
730
|
-
for (const regex of regexes) {
|
|
731
|
-
const match = trimmed.match(regex);
|
|
732
|
-
if (match !== null) return fromMatch(engine, trimmed, match);
|
|
733
|
-
}
|
|
734
|
-
return new UriParseError(engine, "no-match");
|
|
735
|
-
}
|
|
736
|
-
function detectEngine(uri) {
|
|
737
|
-
const trimmed = uri.trim();
|
|
738
|
-
return Object.entries(CONNECTION_STRING_REGEXES).filter(([, regexes]) => regexes.some((regex) => regex.test(trimmed))).map(([engine]) => engine);
|
|
739
|
-
}
|
|
740
|
-
function fromMatch(engine, uri, match) {
|
|
741
|
-
const groups = match.groups;
|
|
742
|
-
if (groups === void 0) throw new Error(`the ${engine} connection string pattern has no named groups`);
|
|
743
|
-
const group = (name) => groups[name] ?? null;
|
|
744
|
-
const protocol = group("protocol");
|
|
745
|
-
if (protocol === null) throw new Error(`the ${engine} connection string pattern has no protocol group`);
|
|
746
|
-
return {
|
|
747
|
-
engine,
|
|
748
|
-
uri,
|
|
749
|
-
protocol,
|
|
750
|
-
hasJdbcPrefix: group("hasJdbcPrefix") !== null,
|
|
751
|
-
username: safeDecode(group("username")),
|
|
752
|
-
password: safeDecode(group("password")),
|
|
753
|
-
host: group("host"),
|
|
754
|
-
port: group("port"),
|
|
755
|
-
database: group("database"),
|
|
756
|
-
catalog: group("catalog"),
|
|
757
|
-
schema: group("schema"),
|
|
758
|
-
path: group("path"),
|
|
759
|
-
...parseParams(group("params"), group("semicolonParams"))
|
|
760
|
-
};
|
|
761
|
-
}
|
|
762
|
-
function parseParams(query, semicolon) {
|
|
763
|
-
if (semicolon !== null) return {
|
|
764
|
-
params: parseSemicolonParams(semicolon),
|
|
765
|
-
paramSeparator: ";"
|
|
766
|
-
};
|
|
767
|
-
return {
|
|
768
|
-
params: query === null ? {} : Object.fromEntries(new URLSearchParams(query)),
|
|
769
|
-
paramSeparator: "&"
|
|
770
|
-
};
|
|
771
|
-
}
|
|
772
|
-
function parseSemicolonParams(text) {
|
|
773
|
-
const result = {};
|
|
774
|
-
for (const param of text.split(";")) {
|
|
775
|
-
const separator = param.indexOf("=");
|
|
776
|
-
const key = separator === -1 ? param : param.slice(0, separator);
|
|
777
|
-
if (key.length === 0) continue;
|
|
778
|
-
result[key] = decodeComponent(separator === -1 ? "" : param.slice(separator + 1));
|
|
779
|
-
}
|
|
780
|
-
return result;
|
|
781
|
-
}
|
|
782
|
-
function safeDecode(text) {
|
|
783
|
-
return text === null ? null : decodeComponent(text);
|
|
784
|
-
}
|
|
785
|
-
function decodeComponent(text) {
|
|
786
|
-
try {
|
|
787
|
-
return decodeURIComponent(text);
|
|
788
|
-
} catch {
|
|
789
|
-
return text;
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
//#endregion
|
|
793
|
-
//#region src/connections/engines.ts
|
|
794
|
-
const ELEVATED_ENGINES = [
|
|
795
|
-
"postgres",
|
|
796
|
-
"mysql",
|
|
797
|
-
"clickhouse",
|
|
798
|
-
"snowflake",
|
|
799
|
-
"bigquery-cloud-sdk",
|
|
800
|
-
"redshift",
|
|
801
|
-
"databricks",
|
|
802
|
-
"sqlserver",
|
|
803
|
-
"athena",
|
|
804
|
-
"starburst",
|
|
805
|
-
"presto-jdbc",
|
|
806
|
-
"mongo",
|
|
807
|
-
"sqlite",
|
|
808
|
-
"druid-jdbc"
|
|
809
|
-
];
|
|
810
|
-
const HIDDEN_ENGINES = /* @__PURE__ */ new Set(["h2"]);
|
|
811
|
-
const PROMPT_KINDS = {
|
|
812
|
-
string: "text",
|
|
813
|
-
password: "password",
|
|
814
|
-
integer: "integer",
|
|
815
|
-
boolean: "boolean",
|
|
816
|
-
select: "select",
|
|
817
|
-
text: "multiline",
|
|
818
|
-
textFile: "file",
|
|
819
|
-
section: "none",
|
|
820
|
-
info: "none",
|
|
821
|
-
hidden: "none"
|
|
822
|
-
};
|
|
823
|
-
function promptKindFor(field) {
|
|
824
|
-
if (field.type === void 0) return "text";
|
|
825
|
-
return PROMPT_KINDS[field.type] ?? "text";
|
|
826
|
-
}
|
|
827
|
-
function fieldLabel(field) {
|
|
828
|
-
return field["display-name"] ?? field.name;
|
|
829
|
-
}
|
|
830
|
-
function flattenFields(fields) {
|
|
831
|
-
return fields.flatMap((field) => isFieldGroup(field) ? field.fields : [field]);
|
|
832
|
-
}
|
|
833
|
-
function isFieldGroup(field) {
|
|
834
|
-
return "fields" in field && field.type === "group";
|
|
835
|
-
}
|
|
836
|
-
function toEntry(key, engine) {
|
|
837
|
-
return {
|
|
838
|
-
key,
|
|
839
|
-
displayName: engine["driver-name"],
|
|
840
|
-
creatable: engine["creatable?"] !== false,
|
|
841
|
-
supersededBy: engine["superseded-by"],
|
|
842
|
-
exampleUri: EXAMPLE_URIS[key] ?? null
|
|
843
|
-
};
|
|
844
|
-
}
|
|
845
|
-
function rank(key) {
|
|
846
|
-
const position = ELEVATED_ENGINES.indexOf(key);
|
|
847
|
-
return position === -1 ? ELEVATED_ENGINES.length : position;
|
|
848
|
-
}
|
|
849
|
-
function compareEntries(a, b) {
|
|
850
|
-
const byRank = rank(a.key) - rank(b.key);
|
|
851
|
-
return byRank !== 0 ? byRank : a.displayName.localeCompare(b.displayName);
|
|
852
|
-
}
|
|
853
|
-
function engineCatalog(engines) {
|
|
854
|
-
const entries = /* @__PURE__ */ new Map();
|
|
855
|
-
const fieldsByEngine = /* @__PURE__ */ new Map();
|
|
856
|
-
for (const [key, engine] of Object.entries(engines)) {
|
|
857
|
-
if (HIDDEN_ENGINES.has(key)) continue;
|
|
858
|
-
entries.set(key, toEntry(key, engine));
|
|
859
|
-
fieldsByEngine.set(key, flattenFields(engine["details-fields"] ?? []));
|
|
860
|
-
}
|
|
861
|
-
const ordered = [...entries.values()].toSorted(compareEntries);
|
|
862
|
-
return {
|
|
863
|
-
list: () => [...ordered],
|
|
864
|
-
entry: (engine) => entries.get(engine) ?? null,
|
|
865
|
-
fields: (engine) => {
|
|
866
|
-
const fields = fieldsByEngine.get(engine);
|
|
867
|
-
if (fields === void 0) throw new Error(`unknown engine ${engine}`);
|
|
868
|
-
return [...fields];
|
|
869
|
-
}
|
|
870
|
-
};
|
|
871
|
-
}
|
|
872
|
-
//#endregion
|
|
873
|
-
//#region src/metabase/domain/health.ts
|
|
874
|
-
const Health = z.object({ status: z.literal("ok") }).loose();
|
|
875
|
-
const Initializing = z.object({
|
|
876
|
-
status: z.literal("initializing"),
|
|
877
|
-
progress: z.number()
|
|
878
|
-
}).loose();
|
|
879
|
-
//#endregion
|
|
880
|
-
//#region src/metabase/http.ts
|
|
881
|
-
const REQUEST_TIMEOUT_MS = 3e4;
|
|
882
|
-
const PARTIAL_SUFFIX = ".part";
|
|
883
|
-
const CONTENT_LENGTH_HEADER = "content-length";
|
|
884
|
-
const SESSION_HEADER = "X-Metabase-Session";
|
|
885
|
-
const API_KEY_HEADER = "X-Api-Key";
|
|
886
|
-
const JSON_CONTENT_TYPE = "application/json";
|
|
887
|
-
const SERVICE_UNAVAILABLE_STATUS = 503;
|
|
888
|
-
const NO_AUTH = { kind: "none" };
|
|
889
|
-
const ErrorMessage = z.object({ message: z.string() }).loose();
|
|
890
|
-
const ErrorFields = z.object({ errors: z.record(z.string(), z.string()) }).loose();
|
|
891
|
-
function authHeaders(auth) {
|
|
892
|
-
switch (auth.kind) {
|
|
893
|
-
case "none": return {};
|
|
894
|
-
case "session": return { [SESSION_HEADER]: auth.id };
|
|
895
|
-
case "api-key": return { [API_KEY_HEADER]: auth.key };
|
|
896
|
-
}
|
|
897
|
-
}
|
|
898
|
-
function parseBody(status, text) {
|
|
899
|
-
if (text.length === 0) return {
|
|
900
|
-
status,
|
|
901
|
-
text,
|
|
902
|
-
json: { parsed: false }
|
|
903
|
-
};
|
|
904
|
-
try {
|
|
905
|
-
return {
|
|
906
|
-
status,
|
|
907
|
-
text,
|
|
908
|
-
json: {
|
|
909
|
-
parsed: true,
|
|
910
|
-
value: JSON.parse(text)
|
|
911
|
-
}
|
|
912
|
-
};
|
|
913
|
-
} catch {
|
|
914
|
-
return {
|
|
915
|
-
status,
|
|
916
|
-
text,
|
|
917
|
-
json: { parsed: false }
|
|
918
|
-
};
|
|
919
|
-
}
|
|
920
|
-
}
|
|
921
|
-
function jsonValue(body) {
|
|
922
|
-
return body.json.parsed ? body.json.value : void 0;
|
|
923
|
-
}
|
|
924
|
-
function serverMessage(body) {
|
|
925
|
-
const parsed = ErrorMessage.safeParse(jsonValue(body));
|
|
926
|
-
if (parsed.success) return parsed.data.message;
|
|
927
|
-
const text = body.text.trim();
|
|
928
|
-
return text.length === 0 ? `HTTP ${body.status}` : text;
|
|
929
|
-
}
|
|
930
|
-
function serverErrors(body) {
|
|
931
|
-
const parsed = ErrorFields.safeParse(jsonValue(body));
|
|
932
|
-
return parsed.success ? parsed.data.errors : null;
|
|
933
|
-
}
|
|
934
|
-
function failureFor(path, body) {
|
|
935
|
-
if (body.status === SERVICE_UNAVAILABLE_STATUS) {
|
|
936
|
-
const initializing = Initializing.safeParse(jsonValue(body));
|
|
937
|
-
if (initializing.success) return new MetabaseInitializingError(initializing.data.progress);
|
|
938
|
-
}
|
|
939
|
-
return new MetabaseApiError({
|
|
940
|
-
status: body.status,
|
|
941
|
-
path,
|
|
942
|
-
message: serverMessage(body),
|
|
943
|
-
errors: serverErrors(body)
|
|
944
|
-
});
|
|
945
|
-
}
|
|
946
|
-
function shapeError(path, body, reason) {
|
|
947
|
-
return new MetabaseApiError({
|
|
948
|
-
status: body.status,
|
|
949
|
-
path,
|
|
950
|
-
message: reason,
|
|
951
|
-
errors: null
|
|
952
|
-
});
|
|
953
|
-
}
|
|
954
|
-
function isTimeout(error) {
|
|
955
|
-
return error instanceof Error && error.name === "TimeoutError";
|
|
956
|
-
}
|
|
957
|
-
function describeCause(error) {
|
|
958
|
-
if (error instanceof Error && error.cause instanceof Error) return error.cause.message;
|
|
959
|
-
return error instanceof Error ? error.message : String(error);
|
|
960
|
-
}
|
|
961
|
-
function createHttp(options) {
|
|
962
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
963
|
-
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
964
|
-
async function send(method, path, body) {
|
|
965
|
-
const url = `${baseUrl}${path}`;
|
|
966
|
-
const headers = {
|
|
967
|
-
accept: JSON_CONTENT_TYPE,
|
|
968
|
-
...authHeaders(options.auth)
|
|
969
|
-
};
|
|
970
|
-
const init = {
|
|
971
|
-
method,
|
|
972
|
-
headers,
|
|
973
|
-
signal: AbortSignal.any([options.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)])
|
|
974
|
-
};
|
|
975
|
-
if (body !== void 0) {
|
|
976
|
-
headers["content-type"] = JSON_CONTENT_TYPE;
|
|
977
|
-
init.body = JSON.stringify(body);
|
|
978
|
-
}
|
|
979
|
-
try {
|
|
980
|
-
return await fetchImpl(url, init);
|
|
981
|
-
} catch (error) {
|
|
982
|
-
if (options.signal.aborted) throw new AbortError();
|
|
983
|
-
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${REQUEST_TIMEOUT_MS}ms`);
|
|
984
|
-
throw new MetabaseUnreachableError(url, describeCause(error));
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
async function requestBody(method, path, body) {
|
|
988
|
-
const response = await send(method, path, body);
|
|
989
|
-
const parsed = parseBody(response.status, await response.text());
|
|
990
|
-
if (!response.ok) throw failureFor(path, parsed);
|
|
991
|
-
return parsed;
|
|
992
|
-
}
|
|
993
|
-
return {
|
|
994
|
-
async requestJson(method, path, request) {
|
|
995
|
-
const body = await requestBody(method, path, request.body);
|
|
996
|
-
if (!body.json.parsed) throw shapeError(path, body, "expected a JSON body");
|
|
997
|
-
try {
|
|
998
|
-
return request.schema.parse(body.json.value);
|
|
999
|
-
} catch (error) {
|
|
1000
|
-
if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
|
|
1001
|
-
throw error;
|
|
1002
|
-
}
|
|
1003
|
-
},
|
|
1004
|
-
async requestNoContent(method, path, request = {}) {
|
|
1005
|
-
await requestBody(method, path, request.body);
|
|
1006
|
-
}
|
|
1007
|
-
};
|
|
1008
|
-
}
|
|
1009
|
-
function contentLength(response) {
|
|
1010
|
-
const header = response.headers.get(CONTENT_LENGTH_HEADER);
|
|
1011
|
-
if (header === null) return null;
|
|
1012
|
-
const total = Number.parseInt(header, 10);
|
|
1013
|
-
return Number.isNaN(total) ? null : total;
|
|
1014
|
-
}
|
|
1015
|
-
function progressCounter(total, onProgress) {
|
|
1016
|
-
let received = 0;
|
|
1017
|
-
return (chunk) => {
|
|
1018
|
-
received += chunk.byteLength;
|
|
1019
|
-
onProgress({
|
|
1020
|
-
received,
|
|
1021
|
-
total
|
|
1022
|
-
});
|
|
1023
|
-
return chunk;
|
|
1024
|
-
};
|
|
1025
|
-
}
|
|
1026
|
-
async function downloadFile(url, destination, options) {
|
|
1027
|
-
const fetchImpl = options.fetchImpl ?? fetch;
|
|
1028
|
-
let response;
|
|
1029
|
-
try {
|
|
1030
|
-
response = await fetchImpl(url, { signal: options.signal });
|
|
1031
|
-
} catch (error) {
|
|
1032
|
-
if (options.signal.aborted) throw new AbortError();
|
|
1033
|
-
throw new DownloadError(url, describeCause(error));
|
|
1034
|
-
}
|
|
1035
|
-
if (!response.ok) throw new DownloadError(url, `HTTP ${response.status}`);
|
|
1036
|
-
if (response.body === null) throw new DownloadError(url, "empty response");
|
|
1037
|
-
const partial = `${destination}${PARTIAL_SUFFIX}`;
|
|
1038
|
-
const count = progressCounter(contentLength(response), options.onProgress);
|
|
1039
|
-
try {
|
|
1040
|
-
await pipeline(Readable.fromWeb(response.body), async function* (chunks) {
|
|
1041
|
-
for await (const chunk of chunks) yield count(chunk);
|
|
1042
|
-
}, createWriteStream(partial), { signal: options.signal });
|
|
1043
|
-
} catch (error) {
|
|
1044
|
-
await promises.rm(partial, { force: true });
|
|
1045
|
-
if (options.signal.aborted) throw new AbortError();
|
|
1046
|
-
throw new DownloadError(url, describeCause(error));
|
|
1047
|
-
}
|
|
1048
|
-
await promises.rename(partial, destination);
|
|
1049
|
-
}
|
|
745
|
+
await promises.rename(partial, destination);
|
|
746
|
+
}
|
|
1050
747
|
//#endregion
|
|
1051
748
|
//#region src/metabase/domain/permissions-group.ts
|
|
1052
749
|
const PermissionsGroup = z.object({
|
|
@@ -1094,293 +791,657 @@ function apiKeyResource(http) {
|
|
|
1094
791
|
};
|
|
1095
792
|
}
|
|
1096
793
|
//#endregion
|
|
1097
|
-
//#region src/metabase/domain/database.ts
|
|
1098
|
-
const InitialSyncStatus = z.enum([
|
|
1099
|
-
"incomplete",
|
|
1100
|
-
"complete",
|
|
1101
|
-
"aborted"
|
|
1102
|
-
]);
|
|
1103
|
-
const Database = z.object({
|
|
1104
|
-
id: z.int(),
|
|
1105
|
-
name: z.string(),
|
|
1106
|
-
engine: z.string(),
|
|
1107
|
-
initial_sync_status: InitialSyncStatus
|
|
1108
|
-
}).loose();
|
|
1109
|
-
const DatabaseList = z.object({
|
|
1110
|
-
data: z.array(Database),
|
|
1111
|
-
total: z.int()
|
|
1112
|
-
}).loose();
|
|
1113
|
-
const DatabaseValidation = z.discriminatedUnion("valid", [z.object({ valid: z.literal(true) }).loose(), z.object({
|
|
1114
|
-
valid: z.literal(false),
|
|
1115
|
-
message: z.string().optional(),
|
|
1116
|
-
errors: z.record(z.string(), z.string()).optional()
|
|
1117
|
-
}).loose()]);
|
|
794
|
+
//#region src/metabase/domain/database.ts
|
|
795
|
+
const InitialSyncStatus = z.enum([
|
|
796
|
+
"incomplete",
|
|
797
|
+
"complete",
|
|
798
|
+
"aborted"
|
|
799
|
+
]);
|
|
800
|
+
const Database = z.object({
|
|
801
|
+
id: z.int(),
|
|
802
|
+
name: z.string(),
|
|
803
|
+
engine: z.string(),
|
|
804
|
+
initial_sync_status: InitialSyncStatus
|
|
805
|
+
}).loose();
|
|
806
|
+
const DatabaseList = z.object({
|
|
807
|
+
data: z.array(Database),
|
|
808
|
+
total: z.int()
|
|
809
|
+
}).loose();
|
|
810
|
+
const DatabaseValidation = z.discriminatedUnion("valid", [z.object({ valid: z.literal(true) }).loose(), z.object({
|
|
811
|
+
valid: z.literal(false),
|
|
812
|
+
message: z.string().optional(),
|
|
813
|
+
errors: z.record(z.string(), z.string()).optional()
|
|
814
|
+
}).loose()]);
|
|
815
|
+
//#endregion
|
|
816
|
+
//#region src/metabase/resources/database.ts
|
|
817
|
+
function databaseResource(http) {
|
|
818
|
+
/** POST /api/database/validate */
|
|
819
|
+
async function validate(connection) {
|
|
820
|
+
return http.requestJson("POST", "/api/database/validate", {
|
|
821
|
+
body: { details: connection },
|
|
822
|
+
schema: DatabaseValidation
|
|
823
|
+
});
|
|
824
|
+
}
|
|
825
|
+
/** POST /api/database */
|
|
826
|
+
async function create(request) {
|
|
827
|
+
return http.requestJson("POST", "/api/database", {
|
|
828
|
+
body: {
|
|
829
|
+
...request,
|
|
830
|
+
is_full_sync: true
|
|
831
|
+
},
|
|
832
|
+
schema: Database
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
/** GET /api/database */
|
|
836
|
+
async function list() {
|
|
837
|
+
return http.requestJson("GET", "/api/database", { schema: DatabaseList });
|
|
838
|
+
}
|
|
839
|
+
/** GET /api/database/:id */
|
|
840
|
+
async function get(id) {
|
|
841
|
+
return http.requestJson("GET", `/api/database/${id}`, { schema: Database });
|
|
842
|
+
}
|
|
843
|
+
return {
|
|
844
|
+
validate,
|
|
845
|
+
create,
|
|
846
|
+
list,
|
|
847
|
+
get
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
//#endregion
|
|
851
|
+
//#region src/metabase/resources/health.ts
|
|
852
|
+
function healthResource(http) {
|
|
853
|
+
/** GET /api/health */
|
|
854
|
+
async function get() {
|
|
855
|
+
return http.requestJson("GET", "/api/health", { schema: Health });
|
|
856
|
+
}
|
|
857
|
+
return { get };
|
|
858
|
+
}
|
|
859
|
+
//#endregion
|
|
860
|
+
//#region src/metabase/resources/permissions-group.ts
|
|
861
|
+
function permissionsGroupResource(http) {
|
|
862
|
+
/** GET /api/permissions/group */
|
|
863
|
+
async function list() {
|
|
864
|
+
return http.requestJson("GET", "/api/permissions/group", { schema: PermissionsGroupList });
|
|
865
|
+
}
|
|
866
|
+
return { list };
|
|
867
|
+
}
|
|
868
|
+
z.enum(["read-only", "read-write"]);
|
|
869
|
+
const RemoteSyncSettingsResult = z.object({
|
|
870
|
+
success: z.boolean(),
|
|
871
|
+
task_id: z.int().optional()
|
|
872
|
+
}).loose();
|
|
873
|
+
const RemoteSyncTestConnection = z.object({ status: z.literal("success") }).loose();
|
|
874
|
+
const SyncTaskStatus$1 = z.enum([
|
|
875
|
+
"running",
|
|
876
|
+
"successful",
|
|
877
|
+
"errored",
|
|
878
|
+
"cancelled",
|
|
879
|
+
"timed-out",
|
|
880
|
+
"conflict"
|
|
881
|
+
]);
|
|
882
|
+
const SyncTask = z.object({
|
|
883
|
+
id: z.int(),
|
|
884
|
+
sync_task_type: z.enum(["import", "export"]),
|
|
885
|
+
progress: z.number().nullable(),
|
|
886
|
+
status: SyncTaskStatus$1,
|
|
887
|
+
error_message: z.string().nullable().optional()
|
|
888
|
+
}).loose();
|
|
889
|
+
//#endregion
|
|
890
|
+
//#region src/metabase/resources/remote-sync.ts
|
|
891
|
+
function remoteSyncResource(http) {
|
|
892
|
+
/** PUT /api/ee/remote-sync/settings */
|
|
893
|
+
async function updateSettings(settings) {
|
|
894
|
+
return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
|
|
895
|
+
body: settings,
|
|
896
|
+
schema: RemoteSyncSettingsResult
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
/** POST /api/ee/remote-sync/test-connection; an empty body tests the saved url and token. */
|
|
900
|
+
async function testConnection(connection = {}) {
|
|
901
|
+
return http.requestJson("POST", "/api/ee/remote-sync/test-connection", {
|
|
902
|
+
body: connection,
|
|
903
|
+
schema: RemoteSyncTestConnection
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
/** GET /api/ee/remote-sync/current-task */
|
|
907
|
+
async function currentTask() {
|
|
908
|
+
return http.requestJson("GET", "/api/ee/remote-sync/current-task", { schema: SyncTask.nullable() });
|
|
909
|
+
}
|
|
910
|
+
return {
|
|
911
|
+
updateSettings,
|
|
912
|
+
testConnection,
|
|
913
|
+
currentTask
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
//#endregion
|
|
917
|
+
//#region src/metabase/domain/session.ts
|
|
918
|
+
const Session = z.object({ id: z.string() }).loose();
|
|
919
|
+
//#endregion
|
|
920
|
+
//#region src/metabase/domain/session-properties.ts
|
|
921
|
+
const EngineFieldOption = z.object({
|
|
922
|
+
name: z.string(),
|
|
923
|
+
value: z.unknown()
|
|
924
|
+
}).loose();
|
|
925
|
+
const EngineField = z.object({
|
|
926
|
+
name: z.string(),
|
|
927
|
+
"display-name": z.string().optional(),
|
|
928
|
+
type: z.string().optional(),
|
|
929
|
+
required: z.boolean().optional(),
|
|
930
|
+
default: z.unknown().optional(),
|
|
931
|
+
placeholder: z.unknown().optional(),
|
|
932
|
+
"visible-if": z.record(z.string(), z.unknown()).optional(),
|
|
933
|
+
options: z.array(EngineFieldOption).optional(),
|
|
934
|
+
"helper-text": z.string().optional()
|
|
935
|
+
}).loose();
|
|
936
|
+
const EngineFieldGroup = z.object({
|
|
937
|
+
type: z.literal("group"),
|
|
938
|
+
fields: z.array(EngineField)
|
|
939
|
+
}).loose();
|
|
940
|
+
const Engine = z.object({
|
|
941
|
+
"driver-name": z.string(),
|
|
942
|
+
"details-fields": z.array(z.union([EngineFieldGroup, EngineField])).optional(),
|
|
943
|
+
"superseded-by": z.string().nullable(),
|
|
944
|
+
"creatable?": z.boolean().optional()
|
|
945
|
+
}).loose();
|
|
946
|
+
const SessionProperties = z.object({
|
|
947
|
+
"setup-token": z.string().nullable(),
|
|
948
|
+
"has-user-setup": z.boolean(),
|
|
949
|
+
version: z.object({ tag: z.string() }).loose(),
|
|
950
|
+
"token-features": z.record(z.string(), z.boolean()),
|
|
951
|
+
engines: z.record(z.string(), Engine)
|
|
952
|
+
}).loose();
|
|
953
|
+
//#endregion
|
|
954
|
+
//#region src/metabase/resources/session.ts
|
|
955
|
+
function sessionResource(http) {
|
|
956
|
+
/** GET /api/session/properties */
|
|
957
|
+
async function properties() {
|
|
958
|
+
return http.requestJson("GET", "/api/session/properties", { schema: SessionProperties });
|
|
959
|
+
}
|
|
960
|
+
/** POST /api/session */
|
|
961
|
+
async function create(credentials) {
|
|
962
|
+
return http.requestJson("POST", "/api/session", {
|
|
963
|
+
body: credentials,
|
|
964
|
+
schema: Session
|
|
965
|
+
});
|
|
966
|
+
}
|
|
967
|
+
return {
|
|
968
|
+
properties,
|
|
969
|
+
create
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
//#endregion
|
|
973
|
+
//#region src/metabase/resources/setting.ts
|
|
974
|
+
const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
|
|
975
|
+
const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
|
|
976
|
+
function settingResource(http) {
|
|
977
|
+
/** PUT /api/setting/premium-embedding-token */
|
|
978
|
+
async function setPremiumEmbeddingToken(value) {
|
|
979
|
+
await http.requestNoContent("PUT", `/api/setting/${PREMIUM_EMBEDDING_TOKEN_SETTING}`, { body: { value } });
|
|
980
|
+
}
|
|
981
|
+
/** PUT /api/setting/transforms-enabled */
|
|
982
|
+
async function enableTransforms() {
|
|
983
|
+
await http.requestNoContent("PUT", `/api/setting/${TRANSFORMS_ENABLED_SETTING}`, { body: { value: true } });
|
|
984
|
+
}
|
|
985
|
+
return {
|
|
986
|
+
setPremiumEmbeddingToken,
|
|
987
|
+
enableTransforms
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
//#endregion
|
|
991
|
+
//#region src/metabase/domain/setup.ts
|
|
992
|
+
const SetupResult = z.object({ id: z.string() }).loose();
|
|
993
|
+
//#endregion
|
|
994
|
+
//#region src/metabase/resources/setup.ts
|
|
995
|
+
function setupResource(http) {
|
|
996
|
+
/** POST /api/setup */
|
|
997
|
+
async function create(request) {
|
|
998
|
+
return http.requestJson("POST", "/api/setup", {
|
|
999
|
+
body: request,
|
|
1000
|
+
schema: SetupResult
|
|
1001
|
+
});
|
|
1002
|
+
}
|
|
1003
|
+
return { create };
|
|
1004
|
+
}
|
|
1005
|
+
//#endregion
|
|
1006
|
+
//#region src/metabase/domain/token-status.ts
|
|
1007
|
+
const TokenStatus = z.object({
|
|
1008
|
+
valid: z.boolean(),
|
|
1009
|
+
status: z.string(),
|
|
1010
|
+
features: z.array(z.string()).default([]),
|
|
1011
|
+
"valid-thru": z.string().optional(),
|
|
1012
|
+
"plan-alias": z.string().optional()
|
|
1013
|
+
}).loose();
|
|
1014
|
+
//#endregion
|
|
1015
|
+
//#region src/metabase/resources/token-status.ts
|
|
1016
|
+
function tokenStatusResource(http) {
|
|
1017
|
+
/** GET /api/premium-features/token/status */
|
|
1018
|
+
async function get() {
|
|
1019
|
+
return http.requestJson("GET", "/api/premium-features/token/status", { schema: TokenStatus });
|
|
1020
|
+
}
|
|
1021
|
+
return { get };
|
|
1022
|
+
}
|
|
1023
|
+
//#endregion
|
|
1024
|
+
//#region src/metabase/domain/user.ts
|
|
1025
|
+
const CurrentUser = z.object({ id: z.int() }).loose();
|
|
1118
1026
|
//#endregion
|
|
1119
|
-
//#region src/metabase/resources/
|
|
1120
|
-
function
|
|
1121
|
-
/**
|
|
1122
|
-
async function
|
|
1123
|
-
return http.requestJson("
|
|
1124
|
-
body: { details: connection },
|
|
1125
|
-
schema: DatabaseValidation
|
|
1126
|
-
});
|
|
1027
|
+
//#region src/metabase/resources/user.ts
|
|
1028
|
+
function userResource(http) {
|
|
1029
|
+
/** GET /api/user/current */
|
|
1030
|
+
async function current() {
|
|
1031
|
+
return http.requestJson("GET", "/api/user/current", { schema: CurrentUser });
|
|
1127
1032
|
}
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1033
|
+
return { current };
|
|
1034
|
+
}
|
|
1035
|
+
//#endregion
|
|
1036
|
+
//#region src/metabase/client.ts
|
|
1037
|
+
const HEALTH_POLL_INTERVAL_MS = 2e3;
|
|
1038
|
+
function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
1039
|
+
const http = createHttp({
|
|
1040
|
+
baseUrl,
|
|
1041
|
+
auth,
|
|
1042
|
+
signal,
|
|
1043
|
+
...options
|
|
1044
|
+
});
|
|
1045
|
+
return {
|
|
1046
|
+
baseUrl,
|
|
1047
|
+
signal,
|
|
1048
|
+
health: healthResource(http),
|
|
1049
|
+
session: sessionResource(http),
|
|
1050
|
+
setup: setupResource(http),
|
|
1051
|
+
permissionsGroup: permissionsGroupResource(http),
|
|
1052
|
+
apiKey: apiKeyResource(http),
|
|
1053
|
+
setting: settingResource(http),
|
|
1054
|
+
tokenStatus: tokenStatusResource(http),
|
|
1055
|
+
database: databaseResource(http),
|
|
1056
|
+
remoteSync: remoteSyncResource(http),
|
|
1057
|
+
user: userResource(http)
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
function isNotReadyYet(error) {
|
|
1061
|
+
return error instanceof MetabaseInitializingError || error instanceof MetabaseUnreachableError;
|
|
1062
|
+
}
|
|
1063
|
+
async function waitForHealth(client, options) {
|
|
1064
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
1065
|
+
const interval = options.pollIntervalMs ?? HEALTH_POLL_INTERVAL_MS;
|
|
1066
|
+
for (;;) {
|
|
1067
|
+
try {
|
|
1068
|
+
await client.health.get();
|
|
1069
|
+
return;
|
|
1070
|
+
} catch (error) {
|
|
1071
|
+
if (!isNotReadyYet(error)) throw error;
|
|
1072
|
+
if (error instanceof MetabaseInitializingError) options.onProgress(error.progress);
|
|
1073
|
+
}
|
|
1074
|
+
if (Date.now() >= deadline) throw new MetabaseUnreachableError(client.baseUrl, `not healthy after ${Math.round(options.timeoutMs / 1e3)}s`);
|
|
1075
|
+
try {
|
|
1076
|
+
await setTimeout$1(interval, void 0, { signal: client.signal });
|
|
1077
|
+
} catch {
|
|
1078
|
+
throw new AbortError();
|
|
1079
|
+
}
|
|
1137
1080
|
}
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1081
|
+
}
|
|
1082
|
+
//#endregion
|
|
1083
|
+
//#region src/steps/credentials.ts
|
|
1084
|
+
const LEGACY_NOTE = "This is the public default password; when the instance holds nothing you need, `rde uninstall` then `rde init` recreates it with a generated one";
|
|
1085
|
+
const PROMPTED_NOTE = "The password is the one entered during `rde init`; rde never stored it";
|
|
1086
|
+
function storedPassword(state, secrets) {
|
|
1087
|
+
if (secrets === null || secrets.admin === null) throw new ConfigError(`the secrets file of instance ${state.name} holds no admin password; \`rde doctor\` says what to do`);
|
|
1088
|
+
return secrets.admin.password;
|
|
1089
|
+
}
|
|
1090
|
+
function storedApiKey(state, secrets) {
|
|
1091
|
+
if (secrets === null || secrets.apiKey === null) throw new ConfigError(`the secrets file of instance ${state.name} holds no API key; run \`rde init --only api-key\``);
|
|
1092
|
+
return secrets.apiKey;
|
|
1093
|
+
}
|
|
1094
|
+
function hostCredentialsClient(signal) {
|
|
1095
|
+
return (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal);
|
|
1096
|
+
}
|
|
1097
|
+
function isNotReady$1(error) {
|
|
1098
|
+
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
1099
|
+
}
|
|
1100
|
+
async function hasAdminOutsideRde(state, client) {
|
|
1101
|
+
if (state.url === null) return false;
|
|
1102
|
+
try {
|
|
1103
|
+
return (await client(state.url, NO_AUTH).session.properties())["has-user-setup"];
|
|
1104
|
+
} catch (error) {
|
|
1105
|
+
if (isNotReady$1(error)) return false;
|
|
1106
|
+
throw error;
|
|
1141
1107
|
}
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1108
|
+
}
|
|
1109
|
+
async function credentialsReport(state, secrets, client) {
|
|
1110
|
+
const admin = state.admin;
|
|
1111
|
+
if (admin === null) {
|
|
1112
|
+
if (await hasAdminOutsideRde(state, client)) {
|
|
1113
|
+
if (secrets !== null && secrets.admin !== null) throw new ConfigError(`instance ${state.name} has an admin user rde has not recorded yet; run \`rde init\` to record it`);
|
|
1114
|
+
throw new ConfigError(`instance ${state.name} has an admin user created outside rde; rde has no password for it`);
|
|
1115
|
+
}
|
|
1116
|
+
throw new ConfigError(`instance ${state.name} has no admin user yet; run \`rde init\``);
|
|
1145
1117
|
}
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
get
|
|
1118
|
+
const login = {
|
|
1119
|
+
instance: state.name,
|
|
1120
|
+
url: state.url,
|
|
1121
|
+
email: admin.email
|
|
1151
1122
|
};
|
|
1123
|
+
switch (admin.kind) {
|
|
1124
|
+
case "generated": return {
|
|
1125
|
+
...login,
|
|
1126
|
+
password: storedPassword(state, secrets),
|
|
1127
|
+
note: null
|
|
1128
|
+
};
|
|
1129
|
+
case "legacy-default": return {
|
|
1130
|
+
...login,
|
|
1131
|
+
password: storedPassword(state, secrets),
|
|
1132
|
+
note: LEGACY_NOTE
|
|
1133
|
+
};
|
|
1134
|
+
case "prompted": return {
|
|
1135
|
+
...login,
|
|
1136
|
+
password: null,
|
|
1137
|
+
note: PROMPTED_NOTE
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
function credentialsLines(report) {
|
|
1142
|
+
const url = report.url === null ? [] : [`Metabase: ${report.url}`];
|
|
1143
|
+
const password = report.password === null ? [] : [`Password: ${report.password}`];
|
|
1144
|
+
const note = report.note === null ? [] : [report.note];
|
|
1145
|
+
return [
|
|
1146
|
+
...url,
|
|
1147
|
+
`Email: ${report.email}`,
|
|
1148
|
+
...password,
|
|
1149
|
+
...note
|
|
1150
|
+
];
|
|
1152
1151
|
}
|
|
1153
1152
|
//#endregion
|
|
1154
|
-
//#region src/
|
|
1155
|
-
function
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1153
|
+
//#region src/commands/define.ts
|
|
1154
|
+
function repeatedValues(rawArgs, flag) {
|
|
1155
|
+
const values = [];
|
|
1156
|
+
const inline = `${flag}=`;
|
|
1157
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
1158
|
+
const arg = rawArgs[index];
|
|
1159
|
+
if (arg === void 0) continue;
|
|
1160
|
+
if (arg.startsWith(inline)) values.push(arg.slice(inline.length));
|
|
1161
|
+
else if (arg === flag) {
|
|
1162
|
+
const next = rawArgs[index + 1];
|
|
1163
|
+
if (next !== void 0) {
|
|
1164
|
+
values.push(next);
|
|
1165
|
+
index += 1;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1159
1168
|
}
|
|
1160
|
-
return
|
|
1169
|
+
return values;
|
|
1170
|
+
}
|
|
1171
|
+
function defineRdeCommand(def) {
|
|
1172
|
+
return defineCommand({
|
|
1173
|
+
meta: def.meta,
|
|
1174
|
+
...def.args !== void 0 && { args: def.args },
|
|
1175
|
+
async run({ args, rawArgs }) {
|
|
1176
|
+
try {
|
|
1177
|
+
await def.run(args, rawArgs);
|
|
1178
|
+
} catch (error) {
|
|
1179
|
+
reportError(error);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
});
|
|
1161
1183
|
}
|
|
1162
1184
|
//#endregion
|
|
1163
|
-
//#region src/
|
|
1164
|
-
function
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1185
|
+
//#region src/commands/instance.ts
|
|
1186
|
+
async function defaultInstanceName() {
|
|
1187
|
+
const config = await readGlobalConfig();
|
|
1188
|
+
if (config === null) throw new ConfigError("no Metabase instance yet; run `rde init` first");
|
|
1189
|
+
return config.defaultInstance;
|
|
1190
|
+
}
|
|
1191
|
+
async function instanceNameOrDefault() {
|
|
1192
|
+
try {
|
|
1193
|
+
return (await readGlobalConfig())?.defaultInstance ?? "default";
|
|
1194
|
+
} catch (error) {
|
|
1195
|
+
if (error instanceof ConfigError) return DEFAULT_INSTANCE_NAME;
|
|
1196
|
+
throw error;
|
|
1168
1197
|
}
|
|
1169
|
-
return { list };
|
|
1170
1198
|
}
|
|
1171
|
-
z.enum(["read-only", "read-write"]);
|
|
1172
|
-
const RemoteSyncSettingsResult = z.object({
|
|
1173
|
-
success: z.boolean(),
|
|
1174
|
-
task_id: z.int().optional()
|
|
1175
|
-
}).loose();
|
|
1176
|
-
const RemoteSyncTestConnection = z.object({ status: z.literal("success") }).loose();
|
|
1177
|
-
const SyncTaskStatus$1 = z.enum([
|
|
1178
|
-
"running",
|
|
1179
|
-
"successful",
|
|
1180
|
-
"errored",
|
|
1181
|
-
"cancelled",
|
|
1182
|
-
"timed-out",
|
|
1183
|
-
"conflict"
|
|
1184
|
-
]);
|
|
1185
|
-
const SyncTask = z.object({
|
|
1186
|
-
id: z.int(),
|
|
1187
|
-
sync_task_type: z.enum(["import", "export"]),
|
|
1188
|
-
progress: z.number().nullable(),
|
|
1189
|
-
status: SyncTaskStatus$1,
|
|
1190
|
-
error_message: z.string().nullable().optional()
|
|
1191
|
-
}).loose();
|
|
1192
1199
|
//#endregion
|
|
1193
|
-
//#region src/
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1200
|
+
//#region src/commands/credentials.ts
|
|
1201
|
+
const credentials = defineRdeCommand({
|
|
1202
|
+
meta: {
|
|
1203
|
+
name: "credentials",
|
|
1204
|
+
description: "Print the admin login of the instance: URL, email, and password"
|
|
1205
|
+
},
|
|
1206
|
+
args: {
|
|
1207
|
+
json: {
|
|
1208
|
+
type: "boolean",
|
|
1209
|
+
description: "Print the login as JSON",
|
|
1210
|
+
default: false
|
|
1211
|
+
},
|
|
1212
|
+
"api-key": {
|
|
1213
|
+
type: "boolean",
|
|
1214
|
+
description: "Print only the API key rde and mb use, for a script to read",
|
|
1215
|
+
default: false
|
|
1216
|
+
}
|
|
1217
|
+
},
|
|
1218
|
+
async run(args) {
|
|
1219
|
+
const name = await defaultInstanceName();
|
|
1220
|
+
const state = await requireInstanceState(name);
|
|
1221
|
+
const secrets = await readInstanceSecrets(name);
|
|
1222
|
+
if (args["api-key"]) {
|
|
1223
|
+
printLines([storedApiKey(state, secrets)]);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
const report = await credentialsReport(state, secrets, hostCredentialsClient(interruptSignal));
|
|
1227
|
+
if (args.json) {
|
|
1228
|
+
printJson(report);
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
printLines(credentialsLines(report));
|
|
1201
1232
|
}
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1233
|
+
});
|
|
1234
|
+
//#endregion
|
|
1235
|
+
//#region src/connections/parse-uri.ts
|
|
1236
|
+
var UriParseError = class extends Error {
|
|
1237
|
+
engine;
|
|
1238
|
+
reason;
|
|
1239
|
+
example;
|
|
1240
|
+
constructor(engine, reason) {
|
|
1241
|
+
const example = EXAMPLE_URIS[engine] ?? null;
|
|
1242
|
+
super(describeFailure(engine, reason, example));
|
|
1243
|
+
this.name = "UriParseError";
|
|
1244
|
+
this.engine = engine;
|
|
1245
|
+
this.reason = reason;
|
|
1246
|
+
this.example = example;
|
|
1208
1247
|
}
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1248
|
+
};
|
|
1249
|
+
function describeFailure(engine, reason, example) {
|
|
1250
|
+
if (reason === "unsupported-engine") return `Connection strings for ${engine} can't be parsed here; enter the fields one by one instead`;
|
|
1251
|
+
return `That doesn't look like a ${engine} connection string${example === null ? "" : `, for example ${example}`}`;
|
|
1252
|
+
}
|
|
1253
|
+
const EXAMPLE_URIS = {
|
|
1254
|
+
athena: "jdbc:athena://WorkGroup=primary;Region=us-east-1;",
|
|
1255
|
+
"bigquery-cloud-sdk": "jdbc:bigquery://https://www.googleapis.com/bigquery/v2:443;ProjectId=MyBigQueryProject;OAuthType=1;",
|
|
1256
|
+
clickhouse: "jdbc:clickhouse:https://localhost:8443?ssl=true",
|
|
1257
|
+
druid: "jdbc:avatica:remote:url=http://localhost:8888/druid/v2/sql/avatica/;transparent_reconnection=true",
|
|
1258
|
+
"druid-jdbc": "jdbc:avatica:remote:url=http://localhost:8888/druid/v2/sql/avatica/;transparent_reconnection=true",
|
|
1259
|
+
databricks: "jdbc:databricks://127.0.0.1:8123;httpPath=/sql/1.0/endpoints/abc;OAuthSecret=1234567890;OAuth2ClientId=xyz",
|
|
1260
|
+
mongo: "mongodb://user:pass@localhost:27017/dbname?authSource=admin",
|
|
1261
|
+
mysql: "jdbc:mysql://user:pass@host:3306/dbname?ssl=true",
|
|
1262
|
+
oracle: "jdbc:oracle:thin:@mydbhost:1521/mydbservice?ssl_server_cert_dn=ServerDN",
|
|
1263
|
+
postgres: "jdbc:postgresql://localhost:5432/mydb",
|
|
1264
|
+
"presto-jdbc": "jdbc:presto://host:1234/sample-catalog/sample-schema?SSL=true&SSLTrustStorePassword=1234",
|
|
1265
|
+
redshift: "jdbc:redshift://examplecluster.abc123xyz789.us-west-2.redshift.amazonaws.com:5439/dev",
|
|
1266
|
+
snowflake: "snowflake://example.snowflakecomputing.com/?db=maindb&warehouse=mainwarehouse",
|
|
1267
|
+
sparksql: "jdbc:sparksql:Server=127.0.0.1;",
|
|
1268
|
+
sqlite: "jdbc:sqlite:///C:/path/to/database.db",
|
|
1269
|
+
sqlserver: "jdbc:sqlserver://mydbhost:1433;databaseName=mydb",
|
|
1270
|
+
starburst: "jdbc:trino://starburst.example.com:43011/hive/sales?user=test&password=secret&SSL=true&roles=system:myrole",
|
|
1271
|
+
vertica: "jdbc:vertica://vertica.example.com:1234/databaseName?user=jane"
|
|
1272
|
+
};
|
|
1273
|
+
const jdbcPrefix = "(?<hasJdbcPrefix>jdbc:)?";
|
|
1274
|
+
const userPass = "(?:(?<username>[^:/?#]+)(?::(?<password>[^@/?#]*))?@)?";
|
|
1275
|
+
const hostAndPort = "(?<host>[^:/?#]+)?(?::(?<port>\\d+)?)?";
|
|
1276
|
+
const params = "(?:(?<params>.*))?";
|
|
1277
|
+
const semicolonParams = ";?(?<semicolonParams>.*)?;?";
|
|
1278
|
+
const druidRegex = new RegExp("^" + jdbcPrefix + "(?<protocol>avatica)" + ":remote:url=(https?://)?(?<host>[^:/]+):(?<port>\\d+)(?<path>/[^;]*)" + semicolonParams + "$", "i");
|
|
1279
|
+
const mongoRegex = new RegExp("^(?<protocol>mongodb(?:\\+srv)?)://" + userPass + "(?<host>[^/?#]+)?" + "(?:/(?<database>[^/?#]*))?" + "(?:\\?(?<params>.*))?" + "$", "i");
|
|
1280
|
+
const CONNECTION_STRING_REGEXES = {
|
|
1281
|
+
athena: [/* @__PURE__ */ new RegExp("^(?<hasJdbcPrefix>jdbc:)?(?<protocol>awsathena|athena)://((?<host>athena.*com)(?::(?<port>\\d+)?)?)?;?(?<semicolonParams>.*)?$", "i")],
|
|
1282
|
+
redshift: [new RegExp("^" + jdbcPrefix + "(?<protocol>redshift)://" + userPass + hostAndPort + "(?:/(?<database>[^/?#;]*))?" + ";?" + "(?<semicolonParams>.*)?" + "$", "i")],
|
|
1283
|
+
"bigquery-cloud-sdk": [new RegExp("^" + jdbcPrefix + "(?<protocol>bigquery)://" + "(https?://)?(?<host>[^:/]+)(?<path>/[^;]*):(?<port>\\d+)" + semicolonParams + "$", "i")],
|
|
1284
|
+
clickhouse: [new RegExp("^" + jdbcPrefix + "(?<protocol>clickhouse|ch):(?:https?:)?//" + userPass + hostAndPort + "(?:/(?<database>[^/?#]*))?" + params)],
|
|
1285
|
+
databricks: [new RegExp("^" + jdbcPrefix + "(?<protocol>databricks)://" + hostAndPort + "(?:/(?<schema>[^/?#]*))?" + semicolonParams + "$", "i")],
|
|
1286
|
+
druid: [druidRegex],
|
|
1287
|
+
"druid-jdbc": [druidRegex],
|
|
1288
|
+
mongo: [mongoRegex],
|
|
1289
|
+
mysql: [new RegExp("^" + jdbcPrefix + "(?<protocol>mysql)://" + userPass + hostAndPort + "(?:/(?<database>[^/?#]*))?" + params + "$", "i")],
|
|
1290
|
+
postgres: [new RegExp("^" + jdbcPrefix + "(?<protocol>postgres(?:ql)?)://" + userPass + hostAndPort + "(?:/(?<database>[^/?#]*))?" + params + "$", "i")],
|
|
1291
|
+
"presto-jdbc": [new RegExp("^" + jdbcPrefix + "(?<protocol>presto)://" + hostAndPort + "(?:/(?<catalog>[^/?#]*)/(?<schema>[^/?#]*))?" + params + "$", "i")],
|
|
1292
|
+
oracle: [new RegExp("^" + jdbcPrefix + "(?<protocol>oracle):(?<subprotocol>thin):" + "(?:(?<username>[^@/]+)/(?<password>[^@/]+))?" + "@" + "(?<host>[^:/?#]+)(?::(?<port>\\d+))?(?:/(?<database>[^/?#]+))?" + params + "$", "i")],
|
|
1293
|
+
snowflake: [new RegExp("^" + jdbcPrefix + "(?<protocol>snowflake)://" + userPass + hostAndPort + "(?:/(?<database>[^/?#]*)?)?" + params + "$", "i")],
|
|
1294
|
+
sparksql: [new RegExp("^" + jdbcPrefix + "(?<protocol>sparksql):" + semicolonParams + "$", "i"), new RegExp("^" + jdbcPrefix + "(?<protocol>hive2)://" + hostAndPort + "(?:/(?<database>[^/?#;]*))?" + semicolonParams + "$", "i")],
|
|
1295
|
+
sqlite: [new RegExp("^" + jdbcPrefix + "(?<protocol>sqlite):///(?<path>.+)$", "i")],
|
|
1296
|
+
sqlserver: [new RegExp("^" + jdbcPrefix + "(?<protocol>sqlserver)://" + hostAndPort + semicolonParams + "$", "i")],
|
|
1297
|
+
starburst: [new RegExp("^" + jdbcPrefix + "(?<protocol>trino)://" + hostAndPort + "(?:/(?<catalog>[^/?#]*)/(?<schema>[^/?#]*))?" + params + "$", "i")],
|
|
1298
|
+
vertica: [new RegExp("^" + jdbcPrefix + "(?<protocol>vertica)://" + hostAndPort + "(?:/(?<database>[^/?#]*))?" + params + "$", "i")]
|
|
1299
|
+
};
|
|
1300
|
+
function parseUri(uri, engine) {
|
|
1301
|
+
const regexes = CONNECTION_STRING_REGEXES[engine];
|
|
1302
|
+
if (regexes === void 0) return new UriParseError(engine, "unsupported-engine");
|
|
1303
|
+
const trimmed = uri.trim();
|
|
1304
|
+
for (const regex of regexes) {
|
|
1305
|
+
const match = trimmed.match(regex);
|
|
1306
|
+
if (match !== null) return fromMatch(engine, trimmed, match);
|
|
1212
1307
|
}
|
|
1308
|
+
return new UriParseError(engine, "no-match");
|
|
1309
|
+
}
|
|
1310
|
+
function detectEngine(uri) {
|
|
1311
|
+
const trimmed = uri.trim();
|
|
1312
|
+
return Object.entries(CONNECTION_STRING_REGEXES).filter(([, regexes]) => regexes.some((regex) => regex.test(trimmed))).map(([engine]) => engine);
|
|
1313
|
+
}
|
|
1314
|
+
function fromMatch(engine, uri, match) {
|
|
1315
|
+
const groups = match.groups;
|
|
1316
|
+
if (groups === void 0) throw new Error(`the ${engine} connection string pattern has no named groups`);
|
|
1317
|
+
const group = (name) => groups[name] ?? null;
|
|
1318
|
+
const protocol = group("protocol");
|
|
1319
|
+
if (protocol === null) throw new Error(`the ${engine} connection string pattern has no protocol group`);
|
|
1213
1320
|
return {
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1321
|
+
engine,
|
|
1322
|
+
uri,
|
|
1323
|
+
protocol,
|
|
1324
|
+
hasJdbcPrefix: group("hasJdbcPrefix") !== null,
|
|
1325
|
+
username: safeDecode(group("username")),
|
|
1326
|
+
password: safeDecode(group("password")),
|
|
1327
|
+
host: group("host"),
|
|
1328
|
+
port: group("port"),
|
|
1329
|
+
database: group("database"),
|
|
1330
|
+
catalog: group("catalog"),
|
|
1331
|
+
schema: group("schema"),
|
|
1332
|
+
path: group("path"),
|
|
1333
|
+
...parseParams(group("params"), group("semicolonParams"))
|
|
1217
1334
|
};
|
|
1218
1335
|
}
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
const EngineFieldOption = z.object({
|
|
1225
|
-
name: z.string(),
|
|
1226
|
-
value: z.unknown()
|
|
1227
|
-
}).loose();
|
|
1228
|
-
const EngineField = z.object({
|
|
1229
|
-
name: z.string(),
|
|
1230
|
-
"display-name": z.string().optional(),
|
|
1231
|
-
type: z.string().optional(),
|
|
1232
|
-
required: z.boolean().optional(),
|
|
1233
|
-
default: z.unknown().optional(),
|
|
1234
|
-
placeholder: z.unknown().optional(),
|
|
1235
|
-
"visible-if": z.record(z.string(), z.unknown()).optional(),
|
|
1236
|
-
options: z.array(EngineFieldOption).optional(),
|
|
1237
|
-
"helper-text": z.string().optional()
|
|
1238
|
-
}).loose();
|
|
1239
|
-
const EngineFieldGroup = z.object({
|
|
1240
|
-
type: z.literal("group"),
|
|
1241
|
-
fields: z.array(EngineField)
|
|
1242
|
-
}).loose();
|
|
1243
|
-
const Engine = z.object({
|
|
1244
|
-
"driver-name": z.string(),
|
|
1245
|
-
"details-fields": z.array(z.union([EngineFieldGroup, EngineField])).optional(),
|
|
1246
|
-
"superseded-by": z.string().nullable(),
|
|
1247
|
-
"creatable?": z.boolean().optional()
|
|
1248
|
-
}).loose();
|
|
1249
|
-
const SessionProperties = z.object({
|
|
1250
|
-
"setup-token": z.string().nullable(),
|
|
1251
|
-
"has-user-setup": z.boolean(),
|
|
1252
|
-
version: z.object({ tag: z.string() }).loose(),
|
|
1253
|
-
"token-features": z.record(z.string(), z.boolean()),
|
|
1254
|
-
engines: z.record(z.string(), Engine)
|
|
1255
|
-
}).loose();
|
|
1256
|
-
//#endregion
|
|
1257
|
-
//#region src/metabase/resources/session.ts
|
|
1258
|
-
function sessionResource(http) {
|
|
1259
|
-
/** GET /api/session/properties */
|
|
1260
|
-
async function properties() {
|
|
1261
|
-
return http.requestJson("GET", "/api/session/properties", { schema: SessionProperties });
|
|
1262
|
-
}
|
|
1263
|
-
/** POST /api/session */
|
|
1264
|
-
async function create(credentials) {
|
|
1265
|
-
return http.requestJson("POST", "/api/session", {
|
|
1266
|
-
body: credentials,
|
|
1267
|
-
schema: Session
|
|
1268
|
-
});
|
|
1269
|
-
}
|
|
1336
|
+
function parseParams(query, semicolon) {
|
|
1337
|
+
if (semicolon !== null) return {
|
|
1338
|
+
params: parseSemicolonParams(semicolon),
|
|
1339
|
+
paramSeparator: ";"
|
|
1340
|
+
};
|
|
1270
1341
|
return {
|
|
1271
|
-
|
|
1272
|
-
|
|
1342
|
+
params: query === null ? {} : Object.fromEntries(new URLSearchParams(query)),
|
|
1343
|
+
paramSeparator: "&"
|
|
1273
1344
|
};
|
|
1274
1345
|
}
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
const
|
|
1278
|
-
const
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
await http.requestNoContent("PUT", `/api/setting/${PREMIUM_EMBEDDING_TOKEN_SETTING}`, { body: { value } });
|
|
1346
|
+
function parseSemicolonParams(text) {
|
|
1347
|
+
const result = {};
|
|
1348
|
+
for (const param of text.split(";")) {
|
|
1349
|
+
const separator = param.indexOf("=");
|
|
1350
|
+
const key = separator === -1 ? param : param.slice(0, separator);
|
|
1351
|
+
if (key.length === 0) continue;
|
|
1352
|
+
result[key] = decodeComponent(separator === -1 ? "" : param.slice(separator + 1));
|
|
1283
1353
|
}
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1354
|
+
return result;
|
|
1355
|
+
}
|
|
1356
|
+
function safeDecode(text) {
|
|
1357
|
+
return text === null ? null : decodeComponent(text);
|
|
1358
|
+
}
|
|
1359
|
+
function decodeComponent(text) {
|
|
1360
|
+
try {
|
|
1361
|
+
return decodeURIComponent(text);
|
|
1362
|
+
} catch {
|
|
1363
|
+
return text;
|
|
1287
1364
|
}
|
|
1288
|
-
return {
|
|
1289
|
-
setPremiumEmbeddingToken,
|
|
1290
|
-
enableTransforms
|
|
1291
|
-
};
|
|
1292
1365
|
}
|
|
1293
1366
|
//#endregion
|
|
1294
|
-
//#region src/
|
|
1295
|
-
const
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1367
|
+
//#region src/connections/engines.ts
|
|
1368
|
+
const ELEVATED_ENGINES = [
|
|
1369
|
+
"postgres",
|
|
1370
|
+
"mysql",
|
|
1371
|
+
"clickhouse",
|
|
1372
|
+
"snowflake",
|
|
1373
|
+
"bigquery-cloud-sdk",
|
|
1374
|
+
"redshift",
|
|
1375
|
+
"databricks",
|
|
1376
|
+
"sqlserver",
|
|
1377
|
+
"athena",
|
|
1378
|
+
"starburst",
|
|
1379
|
+
"presto-jdbc",
|
|
1380
|
+
"mongo",
|
|
1381
|
+
"sqlite",
|
|
1382
|
+
"druid-jdbc"
|
|
1383
|
+
];
|
|
1384
|
+
const HIDDEN_ENGINES = /* @__PURE__ */ new Set(["h2"]);
|
|
1385
|
+
const PROMPT_KINDS = {
|
|
1386
|
+
string: "text",
|
|
1387
|
+
password: "password",
|
|
1388
|
+
integer: "integer",
|
|
1389
|
+
boolean: "boolean",
|
|
1390
|
+
select: "select",
|
|
1391
|
+
text: "multiline",
|
|
1392
|
+
textFile: "file",
|
|
1393
|
+
section: "none",
|
|
1394
|
+
info: "none",
|
|
1395
|
+
hidden: "none"
|
|
1396
|
+
};
|
|
1397
|
+
function promptKindFor(field) {
|
|
1398
|
+
if (field.type === void 0) return "text";
|
|
1399
|
+
return PROMPT_KINDS[field.type] ?? "text";
|
|
1307
1400
|
}
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
const TokenStatus = z.object({
|
|
1311
|
-
valid: z.boolean(),
|
|
1312
|
-
status: z.string(),
|
|
1313
|
-
features: z.array(z.string()).default([]),
|
|
1314
|
-
"valid-thru": z.string().optional(),
|
|
1315
|
-
"plan-alias": z.string().optional()
|
|
1316
|
-
}).loose();
|
|
1317
|
-
//#endregion
|
|
1318
|
-
//#region src/metabase/resources/token-status.ts
|
|
1319
|
-
function tokenStatusResource(http) {
|
|
1320
|
-
/** GET /api/premium-features/token/status */
|
|
1321
|
-
async function get() {
|
|
1322
|
-
return http.requestJson("GET", "/api/premium-features/token/status", { schema: TokenStatus });
|
|
1323
|
-
}
|
|
1324
|
-
return { get };
|
|
1401
|
+
function fieldLabel(field) {
|
|
1402
|
+
return field["display-name"] ?? field.name;
|
|
1325
1403
|
}
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
const CurrentUser = z.object({ id: z.int() }).loose();
|
|
1329
|
-
//#endregion
|
|
1330
|
-
//#region src/metabase/resources/user.ts
|
|
1331
|
-
function userResource(http) {
|
|
1332
|
-
/** GET /api/user/current */
|
|
1333
|
-
async function current() {
|
|
1334
|
-
return http.requestJson("GET", "/api/user/current", { schema: CurrentUser });
|
|
1335
|
-
}
|
|
1336
|
-
return { current };
|
|
1404
|
+
function flattenFields(fields) {
|
|
1405
|
+
return fields.flatMap((field) => isFieldGroup(field) ? field.fields : [field]);
|
|
1337
1406
|
}
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
function
|
|
1342
|
-
const http = createHttp({
|
|
1343
|
-
baseUrl,
|
|
1344
|
-
auth,
|
|
1345
|
-
signal,
|
|
1346
|
-
...options
|
|
1347
|
-
});
|
|
1407
|
+
function isFieldGroup(field) {
|
|
1408
|
+
return "fields" in field && field.type === "group";
|
|
1409
|
+
}
|
|
1410
|
+
function toEntry(key, engine) {
|
|
1348
1411
|
return {
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
permissionsGroup: permissionsGroupResource(http),
|
|
1355
|
-
apiKey: apiKeyResource(http),
|
|
1356
|
-
setting: settingResource(http),
|
|
1357
|
-
tokenStatus: tokenStatusResource(http),
|
|
1358
|
-
database: databaseResource(http),
|
|
1359
|
-
remoteSync: remoteSyncResource(http),
|
|
1360
|
-
user: userResource(http)
|
|
1412
|
+
key,
|
|
1413
|
+
displayName: engine["driver-name"],
|
|
1414
|
+
creatable: engine["creatable?"] !== false,
|
|
1415
|
+
supersededBy: engine["superseded-by"],
|
|
1416
|
+
exampleUri: EXAMPLE_URIS[key] ?? null
|
|
1361
1417
|
};
|
|
1362
1418
|
}
|
|
1363
|
-
function
|
|
1364
|
-
|
|
1419
|
+
function rank(key) {
|
|
1420
|
+
const position = ELEVATED_ENGINES.indexOf(key);
|
|
1421
|
+
return position === -1 ? ELEVATED_ENGINES.length : position;
|
|
1365
1422
|
}
|
|
1366
|
-
|
|
1367
|
-
const
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
if (Date.now() >= deadline) throw new MetabaseUnreachableError(client.baseUrl, `not healthy after ${Math.round(options.timeoutMs / 1e3)}s`);
|
|
1378
|
-
try {
|
|
1379
|
-
await setTimeout$1(interval, void 0, { signal: client.signal });
|
|
1380
|
-
} catch {
|
|
1381
|
-
throw new AbortError();
|
|
1382
|
-
}
|
|
1423
|
+
function compareEntries(a, b) {
|
|
1424
|
+
const byRank = rank(a.key) - rank(b.key);
|
|
1425
|
+
return byRank !== 0 ? byRank : a.displayName.localeCompare(b.displayName);
|
|
1426
|
+
}
|
|
1427
|
+
function engineCatalog(engines) {
|
|
1428
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1429
|
+
const fieldsByEngine = /* @__PURE__ */ new Map();
|
|
1430
|
+
for (const [key, engine] of Object.entries(engines)) {
|
|
1431
|
+
if (HIDDEN_ENGINES.has(key)) continue;
|
|
1432
|
+
entries.set(key, toEntry(key, engine));
|
|
1433
|
+
fieldsByEngine.set(key, flattenFields(engine["details-fields"] ?? []));
|
|
1383
1434
|
}
|
|
1435
|
+
const ordered = [...entries.values()].toSorted(compareEntries);
|
|
1436
|
+
return {
|
|
1437
|
+
list: () => [...ordered],
|
|
1438
|
+
entry: (engine) => entries.get(engine) ?? null,
|
|
1439
|
+
fields: (engine) => {
|
|
1440
|
+
const fields = fieldsByEngine.get(engine);
|
|
1441
|
+
if (fields === void 0) throw new Error(`unknown engine ${engine}`);
|
|
1442
|
+
return [...fields];
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1384
1445
|
}
|
|
1385
1446
|
//#endregion
|
|
1386
1447
|
//#region src/connections/map-fields.ts
|
|
@@ -1747,8 +1808,11 @@ const BACK_PLACEHOLDER = "leave empty to go back";
|
|
|
1747
1808
|
function stdinIsTty() {
|
|
1748
1809
|
return process.stdin.isTTY === true;
|
|
1749
1810
|
}
|
|
1811
|
+
function refusal(prompt) {
|
|
1812
|
+
return `cannot prompt "${prompt}" — stdin is not a TTY`;
|
|
1813
|
+
}
|
|
1750
1814
|
function requireTty(prompt) {
|
|
1751
|
-
if (!stdinIsTty()) throw new ConfigError(
|
|
1815
|
+
if (!stdinIsTty()) throw new ConfigError(refusal(prompt));
|
|
1752
1816
|
}
|
|
1753
1817
|
function settled(value) {
|
|
1754
1818
|
if (isCancel(value)) throw new AbortError();
|
|
@@ -1790,7 +1854,8 @@ async function askText(opts) {
|
|
|
1790
1854
|
...validate !== void 0 && { validate }
|
|
1791
1855
|
}));
|
|
1792
1856
|
}
|
|
1793
|
-
|
|
1857
|
+
const CONFIRM_WITH_YES = "pass --yes to confirm without a terminal";
|
|
1858
|
+
function clackPrompts(options) {
|
|
1794
1859
|
return {
|
|
1795
1860
|
text: askText,
|
|
1796
1861
|
async textOrBack(opts) {
|
|
@@ -1810,7 +1875,10 @@ function clackPrompts() {
|
|
|
1810
1875
|
}));
|
|
1811
1876
|
},
|
|
1812
1877
|
async confirm(opts) {
|
|
1813
|
-
|
|
1878
|
+
if (!stdinIsTty()) {
|
|
1879
|
+
const message = refusal(opts.message);
|
|
1880
|
+
throw new ConfigError(options.confirmsWithYes ? `${message}; ${CONFIRM_WITH_YES}` : message);
|
|
1881
|
+
}
|
|
1814
1882
|
return settled(await confirm({
|
|
1815
1883
|
message: opts.message,
|
|
1816
1884
|
...opts.initialValue !== void 0 && { initialValue: opts.initialValue }
|
|
@@ -2711,11 +2779,15 @@ const CHECKLIST_MARK = {
|
|
|
2711
2779
|
"not-applicable": "–"
|
|
2712
2780
|
};
|
|
2713
2781
|
const SPINNER_FAILURE_CODE = 1;
|
|
2782
|
+
const PLAIN_PROGRESS_INTERVAL_MS = 1e4;
|
|
2783
|
+
const MS_PER_SECOND = 1e3;
|
|
2784
|
+
const PLAIN_INDENT = " ";
|
|
2714
2785
|
function checklistLines(items) {
|
|
2715
2786
|
return items.map((item) => `${CHECKLIST_MARK[item.status]} ${item.title}`);
|
|
2716
2787
|
}
|
|
2717
2788
|
function clackUi() {
|
|
2718
2789
|
return {
|
|
2790
|
+
childOutput: "inherit",
|
|
2719
2791
|
intro,
|
|
2720
2792
|
outro,
|
|
2721
2793
|
note: (message, title) => note(message, title),
|
|
@@ -2736,6 +2808,55 @@ function clackUi() {
|
|
|
2736
2808
|
}
|
|
2737
2809
|
};
|
|
2738
2810
|
}
|
|
2811
|
+
function indented(text) {
|
|
2812
|
+
return text.split("\n").map((line) => PLAIN_INDENT + line).join("\n");
|
|
2813
|
+
}
|
|
2814
|
+
function plainUi(output) {
|
|
2815
|
+
const line = (text) => output.write(text + "\n");
|
|
2816
|
+
return {
|
|
2817
|
+
childOutput: "capture",
|
|
2818
|
+
intro: line,
|
|
2819
|
+
outro: line,
|
|
2820
|
+
note(message, title) {
|
|
2821
|
+
if (title !== void 0) line(title);
|
|
2822
|
+
line(indented(message));
|
|
2823
|
+
},
|
|
2824
|
+
info: line,
|
|
2825
|
+
warn: (message) => line(`Warning: ${message}`),
|
|
2826
|
+
success: line,
|
|
2827
|
+
spinner(label) {
|
|
2828
|
+
const startedAt = output.now();
|
|
2829
|
+
let printedAt = startedAt;
|
|
2830
|
+
const elapsed = () => `${Math.round((output.now() - startedAt) / MS_PER_SECOND)}s`;
|
|
2831
|
+
line(label);
|
|
2832
|
+
return {
|
|
2833
|
+
update(message) {
|
|
2834
|
+
const now = output.now();
|
|
2835
|
+
if (now - printedAt < PLAIN_PROGRESS_INTERVAL_MS) return;
|
|
2836
|
+
printedAt = now;
|
|
2837
|
+
line(indented(message));
|
|
2838
|
+
},
|
|
2839
|
+
stop: (message) => line(`${message} (${elapsed()})`),
|
|
2840
|
+
fail: (message) => line(`Failed: ${message} (${elapsed()})`)
|
|
2841
|
+
};
|
|
2842
|
+
},
|
|
2843
|
+
checklist: (items) => line(checklistLines(items).join("\n"))
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
function noColorRequested() {
|
|
2847
|
+
const value = readEnv(ENV_NO_COLOR);
|
|
2848
|
+
return value !== void 0 && value !== "";
|
|
2849
|
+
}
|
|
2850
|
+
function drawsOnTerminal() {
|
|
2851
|
+
return process.stdout.isTTY === true && !noColorRequested();
|
|
2852
|
+
}
|
|
2853
|
+
function terminalUi() {
|
|
2854
|
+
if (drawsOnTerminal()) return clackUi();
|
|
2855
|
+
return plainUi({
|
|
2856
|
+
write: (text) => process.stdout.write(text),
|
|
2857
|
+
now: Date.now
|
|
2858
|
+
});
|
|
2859
|
+
}
|
|
2739
2860
|
//#endregion
|
|
2740
2861
|
//#region src/wizard/context.ts
|
|
2741
2862
|
async function createWizardContext(instanceName, options) {
|
|
@@ -2757,8 +2878,8 @@ async function createWizardContext(instanceName, options) {
|
|
|
2757
2878
|
await writeInstanceSecrets(instanceName, next);
|
|
2758
2879
|
ctx.secrets = next;
|
|
2759
2880
|
},
|
|
2760
|
-
prompts: options.prompts ?? clackPrompts(),
|
|
2761
|
-
ui:
|
|
2881
|
+
prompts: options.prompts ?? clackPrompts({ confirmsWithYes: false }),
|
|
2882
|
+
ui: terminalUi(),
|
|
2762
2883
|
signal: options.signal,
|
|
2763
2884
|
session: new SessionCache()
|
|
2764
2885
|
};
|
|
@@ -2811,7 +2932,8 @@ const db = defineCommand({
|
|
|
2811
2932
|
subCommands: {
|
|
2812
2933
|
add: dbAdd,
|
|
2813
2934
|
list: dbList
|
|
2814
|
-
}
|
|
2935
|
+
},
|
|
2936
|
+
default: "list"
|
|
2815
2937
|
});
|
|
2816
2938
|
//#endregion
|
|
2817
2939
|
//#region src/metabase/domain/version-info.ts
|
|
@@ -3017,7 +3139,7 @@ async function spawnDetached(command, args, options) {
|
|
|
3017
3139
|
log.fd
|
|
3018
3140
|
],
|
|
3019
3141
|
cwd: options.cwd,
|
|
3020
|
-
env:
|
|
3142
|
+
env: options.env,
|
|
3021
3143
|
detached: true
|
|
3022
3144
|
});
|
|
3023
3145
|
child.on("error", (error) => {
|
|
@@ -3060,6 +3182,29 @@ function toolFailure(tool, argv, result) {
|
|
|
3060
3182
|
stderrTail: stderrTail(result.stderr)
|
|
3061
3183
|
});
|
|
3062
3184
|
}
|
|
3185
|
+
const HAS_TEXT = /[\p{L}\p{N}]/u;
|
|
3186
|
+
async function runShown(runner, output, command, args) {
|
|
3187
|
+
if (output === "inherit") {
|
|
3188
|
+
const exitCode = await runner.stream(command, args);
|
|
3189
|
+
if (exitCode !== 0) throw new ExternalToolError({
|
|
3190
|
+
tool: command,
|
|
3191
|
+
argv: args,
|
|
3192
|
+
exitCode,
|
|
3193
|
+
stderrTail: ""
|
|
3194
|
+
});
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
const result = await runner.run(command, args, { stdoutToFile: true });
|
|
3198
|
+
if (result.exitCode !== 0) {
|
|
3199
|
+
const printed = withoutEscapes(`${result.stdout}\n${result.stderr}`).split(LINE_BREAK);
|
|
3200
|
+
throw new ExternalToolError({
|
|
3201
|
+
tool: command,
|
|
3202
|
+
argv: args,
|
|
3203
|
+
exitCode: result.exitCode,
|
|
3204
|
+
stderrTail: stderrTail(printed.filter((line) => HAS_TEXT.test(line)).join("\n"))
|
|
3205
|
+
});
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3063
3208
|
function parseJsonOutput(tool, argv, stdout, schema) {
|
|
3064
3209
|
let raw;
|
|
3065
3210
|
try {
|
|
@@ -3203,6 +3348,7 @@ function javaTool(runner) {
|
|
|
3203
3348
|
const DOCKER = "docker";
|
|
3204
3349
|
const CONTAINER_DATA_DIR = "/rde";
|
|
3205
3350
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
3351
|
+
const HOME_LABEL = "rde.home";
|
|
3206
3352
|
const ENV_HOST_UID = "MUID";
|
|
3207
3353
|
const ENV_HOST_GID = "MGID";
|
|
3208
3354
|
const ADD_HOST_GATEWAY = `--add-host=${DOCKER_HOST_ALIAS}:host-gateway`;
|
|
@@ -3215,7 +3361,10 @@ const PortBinding = z.array(z.object({
|
|
|
3215
3361
|
}).loose()).nullable();
|
|
3216
3362
|
const InspectedContainer = z.object({
|
|
3217
3363
|
State: z.object({ Running: z.boolean() }).loose(),
|
|
3218
|
-
Config: z.object({
|
|
3364
|
+
Config: z.object({
|
|
3365
|
+
Image: z.string(),
|
|
3366
|
+
Labels: z.record(z.string(), z.string()).nullish()
|
|
3367
|
+
}).loose(),
|
|
3219
3368
|
HostConfig: z.object({ PortBindings: z.record(z.string(), PortBinding).nullable() }).loose()
|
|
3220
3369
|
}).loose();
|
|
3221
3370
|
const InspectOutput = z.array(InspectedContainer);
|
|
@@ -3237,6 +3386,8 @@ function createArgs(options) {
|
|
|
3237
3386
|
"create",
|
|
3238
3387
|
"--name",
|
|
3239
3388
|
options.name,
|
|
3389
|
+
"--label",
|
|
3390
|
+
`${HOME_LABEL}=${options.home}`,
|
|
3240
3391
|
"-p",
|
|
3241
3392
|
`${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`
|
|
3242
3393
|
];
|
|
@@ -3315,7 +3466,8 @@ function dockerTool(runner) {
|
|
|
3315
3466
|
exists: true,
|
|
3316
3467
|
running: container.State.Running,
|
|
3317
3468
|
image: container.Config.Image,
|
|
3318
|
-
ports: portMappings(container.HostConfig.PortBindings)
|
|
3469
|
+
ports: portMappings(container.HostConfig.PortBindings),
|
|
3470
|
+
home: container.Config.Labels?.[HOME_LABEL] ?? null
|
|
3319
3471
|
};
|
|
3320
3472
|
}
|
|
3321
3473
|
async function containerImageId(name) {
|
|
@@ -3439,6 +3591,23 @@ function logPathOf(instanceDir) {
|
|
|
3439
3591
|
function pidPathOf(instanceDir) {
|
|
3440
3592
|
return join(instanceDir, PID_FILE);
|
|
3441
3593
|
}
|
|
3594
|
+
const METABASE_VARIABLE_PREFIX = "MB_";
|
|
3595
|
+
function jarEnvironment(host, server) {
|
|
3596
|
+
const env = {};
|
|
3597
|
+
const ignored = [];
|
|
3598
|
+
for (const [name, value] of Object.entries(host)) if (name.startsWith(METABASE_VARIABLE_PREFIX)) ignored.push(name);
|
|
3599
|
+
else env[name] = value;
|
|
3600
|
+
return {
|
|
3601
|
+
env: {
|
|
3602
|
+
...env,
|
|
3603
|
+
...server
|
|
3604
|
+
},
|
|
3605
|
+
ignored: ignored.toSorted()
|
|
3606
|
+
};
|
|
3607
|
+
}
|
|
3608
|
+
function ignoredVariablesNotice(ignored) {
|
|
3609
|
+
return `Ignoring ${ignored.join(", ")} from your shell; rde configures Metabase itself`;
|
|
3610
|
+
}
|
|
3442
3611
|
async function pathExists$1(path) {
|
|
3443
3612
|
try {
|
|
3444
3613
|
await promises.access(path);
|
|
@@ -3510,24 +3679,26 @@ function jarRuntime(state, deps) {
|
|
|
3510
3679
|
await promises.mkdir(join(deps.dataDir, DB_DIR), { recursive: true });
|
|
3511
3680
|
await ensureJar(options);
|
|
3512
3681
|
}
|
|
3513
|
-
async function start() {
|
|
3682
|
+
async function start(options) {
|
|
3514
3683
|
if (await runningPid() !== null) return;
|
|
3515
3684
|
if (!await pathExists$1(jarPath)) throw new ConfigError(`${jarPath} does not exist; run \`rde init\``);
|
|
3516
3685
|
const java = await requireJava(deps.java);
|
|
3517
3686
|
await promises.mkdir(deps.instanceDir, { recursive: true });
|
|
3687
|
+
const { env, ignored } = jarEnvironment(deps.hostEnv, {
|
|
3688
|
+
...serverEnv({
|
|
3689
|
+
dbFile: join(deps.dataDir, DB_DIR, DB_FILE),
|
|
3690
|
+
pluginsDir: join(deps.dataDir, PLUGINS_DIR),
|
|
3691
|
+
port: state.port
|
|
3692
|
+
}),
|
|
3693
|
+
...jettyEnv(state.port)
|
|
3694
|
+
});
|
|
3695
|
+
if (ignored.length > 0) options.onNotice(ignoredVariablesNotice(ignored));
|
|
3518
3696
|
const pid = await daemon.start({
|
|
3519
3697
|
command: java.path,
|
|
3520
3698
|
args: ["-jar", jarPath],
|
|
3521
3699
|
options: {
|
|
3522
3700
|
cwd: deps.dataDir,
|
|
3523
|
-
env
|
|
3524
|
-
...serverEnv({
|
|
3525
|
-
dbFile: join(deps.dataDir, DB_DIR, DB_FILE),
|
|
3526
|
-
pluginsDir: join(deps.dataDir, PLUGINS_DIR),
|
|
3527
|
-
port: state.port
|
|
3528
|
-
}),
|
|
3529
|
-
...jettyEnv(state.port)
|
|
3530
|
-
},
|
|
3701
|
+
env,
|
|
3531
3702
|
logPath
|
|
3532
3703
|
}
|
|
3533
3704
|
});
|
|
@@ -3646,8 +3817,10 @@ function daemonTool(runner) {
|
|
|
3646
3817
|
const DEFAULT_IMAGE = "metabase/metabase-enterprise-head:latest";
|
|
3647
3818
|
const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
|
|
3648
3819
|
const CONTAINER_PORT = 3e3;
|
|
3649
|
-
function containerName(instanceName) {
|
|
3650
|
-
|
|
3820
|
+
function containerName(instanceName, home) {
|
|
3821
|
+
const tag = homeTag(home);
|
|
3822
|
+
const name = `${CONTAINER_NAME_PREFIX$1}-${instanceName}`;
|
|
3823
|
+
return tag === null ? name : `${name}-${tag}`;
|
|
3651
3824
|
}
|
|
3652
3825
|
function publishesPortOnLoopback(inspection, port) {
|
|
3653
3826
|
return inspection.ports.some((mapping) => mapping.container === CONTAINER_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
@@ -3675,6 +3848,7 @@ function dockerRuntime(state, deps) {
|
|
|
3675
3848
|
await docker.imagePull(state.image, { onLine: options.onProgress });
|
|
3676
3849
|
await docker.containerCreate({
|
|
3677
3850
|
name: state.containerName,
|
|
3851
|
+
home: deps.rdeHome,
|
|
3678
3852
|
image: state.image,
|
|
3679
3853
|
port: {
|
|
3680
3854
|
host: state.port,
|
|
@@ -3740,11 +3914,13 @@ function hostRuntimeDeps(instanceName, signal) {
|
|
|
3740
3914
|
dataDir: instanceDataDir(instanceName),
|
|
3741
3915
|
instanceDir: instanceDir(instanceName),
|
|
3742
3916
|
jarsDir: jarsDir(),
|
|
3917
|
+
rdeHome: rdeHome(),
|
|
3743
3918
|
platform: process.platform,
|
|
3744
3919
|
hostUser: {
|
|
3745
3920
|
uid: user.uid,
|
|
3746
3921
|
gid: user.gid
|
|
3747
|
-
}
|
|
3922
|
+
},
|
|
3923
|
+
hostEnv: hostEnvironment()
|
|
3748
3924
|
};
|
|
3749
3925
|
}
|
|
3750
3926
|
function runtimeFromState(state, deps) {
|
|
@@ -3764,6 +3940,7 @@ const SAMPLE_DATABASE_READY_TIMEOUT_MS = 12e4;
|
|
|
3764
3940
|
const POSTGRES_PORT = 5432;
|
|
3765
3941
|
const CONTAINER_NAME_PREFIX = "rde-sampledb";
|
|
3766
3942
|
const READY_POLL_MS = 1e3;
|
|
3943
|
+
const PORT_TAKEN = /port is already allocated|address already in use/;
|
|
3767
3944
|
const READY_PROBE = [
|
|
3768
3945
|
"pg_isready",
|
|
3769
3946
|
"-h",
|
|
@@ -3773,8 +3950,10 @@ const READY_PROBE = [
|
|
|
3773
3950
|
"-d",
|
|
3774
3951
|
SAMPLE_DATABASE_NAME
|
|
3775
3952
|
];
|
|
3776
|
-
function sampleContainerName(instanceName) {
|
|
3777
|
-
|
|
3953
|
+
function sampleContainerName(instanceName, home) {
|
|
3954
|
+
const tag = homeTag(home);
|
|
3955
|
+
const name = `${CONTAINER_NAME_PREFIX}-${instanceName}`;
|
|
3956
|
+
return tag === null ? name : `${name}-${tag}`;
|
|
3778
3957
|
}
|
|
3779
3958
|
function sampleDatabaseUrl(port) {
|
|
3780
3959
|
return `postgres://${SAMPLE_DATABASE_USER}:${SAMPLE_DATABASE_PASSWORD}@localhost:${port}/${SAMPLE_DATABASE_NAME}`;
|
|
@@ -3782,7 +3961,22 @@ function sampleDatabaseUrl(port) {
|
|
|
3782
3961
|
function publishesOnLoopback$1(inspection, port) {
|
|
3783
3962
|
return inspection.ports.some((mapping) => mapping.container === POSTGRES_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
3784
3963
|
}
|
|
3785
|
-
function
|
|
3964
|
+
function createOptions(state, home) {
|
|
3965
|
+
return {
|
|
3966
|
+
name: state.containerName,
|
|
3967
|
+
home,
|
|
3968
|
+
image: state.image,
|
|
3969
|
+
port: {
|
|
3970
|
+
host: state.port,
|
|
3971
|
+
container: POSTGRES_PORT
|
|
3972
|
+
},
|
|
3973
|
+
mount: null,
|
|
3974
|
+
env: {},
|
|
3975
|
+
addHostGateway: false,
|
|
3976
|
+
hostUser: null
|
|
3977
|
+
};
|
|
3978
|
+
}
|
|
3979
|
+
function sampleDatabaseContainer(state, docker, home) {
|
|
3786
3980
|
const name = state.containerName;
|
|
3787
3981
|
function conflict(reason) {
|
|
3788
3982
|
return new ConfigError(`container ${name} already exists ${reason}; remove it with \`docker rm -f ${name}\` and run \`rde init\` again`);
|
|
@@ -3795,18 +3989,7 @@ function sampleDatabaseContainer(state, docker) {
|
|
|
3795
3989
|
return;
|
|
3796
3990
|
}
|
|
3797
3991
|
await docker.imagePull(state.image, { onLine: options.onProgress });
|
|
3798
|
-
await docker.containerCreate(
|
|
3799
|
-
name,
|
|
3800
|
-
image: state.image,
|
|
3801
|
-
port: {
|
|
3802
|
-
host: state.port,
|
|
3803
|
-
container: POSTGRES_PORT
|
|
3804
|
-
},
|
|
3805
|
-
mount: null,
|
|
3806
|
-
env: {},
|
|
3807
|
-
addHostGateway: false,
|
|
3808
|
-
hostUser: null
|
|
3809
|
-
});
|
|
3992
|
+
await docker.containerCreate(createOptions(state, home));
|
|
3810
3993
|
}
|
|
3811
3994
|
async function start() {
|
|
3812
3995
|
const existing = await docker.containerInspect(name);
|
|
@@ -3849,6 +4032,32 @@ function sampleDatabaseContainer(state, docker) {
|
|
|
3849
4032
|
waitReady
|
|
3850
4033
|
};
|
|
3851
4034
|
}
|
|
4035
|
+
function isPortTaken(error) {
|
|
4036
|
+
return error instanceof ExternalToolError && PORT_TAKEN.test(error.stderrTail);
|
|
4037
|
+
}
|
|
4038
|
+
async function startOnFreePort(state, docker, ports, home) {
|
|
4039
|
+
try {
|
|
4040
|
+
return {
|
|
4041
|
+
outcome: await sampleDatabaseContainer(state, docker, home).start(),
|
|
4042
|
+
state
|
|
4043
|
+
};
|
|
4044
|
+
} catch (error) {
|
|
4045
|
+
if (!isPortTaken(error)) throw error;
|
|
4046
|
+
}
|
|
4047
|
+
const existing = await docker.containerInspect(state.containerName);
|
|
4048
|
+
if (existing.exists && existing.home !== null && existing.home !== home) throw new ConfigError(`container ${state.containerName} belongs to the rde home ${existing.home} and is left alone`);
|
|
4049
|
+
const moved = {
|
|
4050
|
+
...state,
|
|
4051
|
+
port: await ports.findFree(SAMPLE_DATABASE_PORT)
|
|
4052
|
+
};
|
|
4053
|
+
await docker.containerRemove(state.containerName);
|
|
4054
|
+
await docker.containerCreate(createOptions(moved, home));
|
|
4055
|
+
await docker.containerStart(moved.containerName);
|
|
4056
|
+
return {
|
|
4057
|
+
outcome: "started",
|
|
4058
|
+
state: moved
|
|
4059
|
+
};
|
|
4060
|
+
}
|
|
3852
4061
|
//#endregion
|
|
3853
4062
|
//#region src/tools/mb.ts
|
|
3854
4063
|
const MB = "mb";
|
|
@@ -4259,7 +4468,15 @@ async function detectAgents(probe) {
|
|
|
4259
4468
|
//#region src/tools/skills-cli.ts
|
|
4260
4469
|
const NPX = "npx";
|
|
4261
4470
|
const SKILLS_PACKAGE = "skills@latest";
|
|
4262
|
-
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-
|
|
4471
|
+
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-generic-skills";
|
|
4472
|
+
const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
|
|
4473
|
+
const DATA_APP_SKILLS_SOURCE = "metabase/metabase/skills#master";
|
|
4474
|
+
const DATA_APP_SKILL_NAMES = [
|
|
4475
|
+
"metabase-data-app-setup",
|
|
4476
|
+
"metabase-data-app-routing",
|
|
4477
|
+
"metabase-data-app-actions",
|
|
4478
|
+
"metabase-data-app-semantic-layer"
|
|
4479
|
+
];
|
|
4263
4480
|
function rdeSkillSource() {
|
|
4264
4481
|
const override = readEnv(ENV_RDE_SKILL_SOURCE);
|
|
4265
4482
|
return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
|
|
@@ -4322,37 +4539,23 @@ function skillsCli(runner) {
|
|
|
4322
4539
|
SKILLS_PACKAGE,
|
|
4323
4540
|
"add",
|
|
4324
4541
|
options.source,
|
|
4325
|
-
"--skill",
|
|
4326
|
-
options.skill,
|
|
4542
|
+
...options.skills.flatMap((skill) => ["--skill", skill]),
|
|
4327
4543
|
"-a",
|
|
4328
4544
|
...options.agents,
|
|
4329
4545
|
"-g",
|
|
4330
4546
|
"-y"
|
|
4331
4547
|
];
|
|
4332
|
-
|
|
4333
|
-
if (exitCode !== 0) throw new ExternalToolError({
|
|
4334
|
-
tool: NPX,
|
|
4335
|
-
argv,
|
|
4336
|
-
exitCode,
|
|
4337
|
-
stderrTail: ""
|
|
4338
|
-
});
|
|
4548
|
+
await runShown(runner, options.output, NPX, argv);
|
|
4339
4549
|
}
|
|
4340
|
-
async function remove(
|
|
4341
|
-
|
|
4550
|
+
async function remove(skills, output) {
|
|
4551
|
+
await runShown(runner, output, NPX, [
|
|
4342
4552
|
"-y",
|
|
4343
4553
|
SKILLS_PACKAGE,
|
|
4344
4554
|
"remove",
|
|
4345
|
-
|
|
4555
|
+
...skills,
|
|
4346
4556
|
"-g",
|
|
4347
4557
|
"-y"
|
|
4348
|
-
];
|
|
4349
|
-
const exitCode = await runner.stream(NPX, argv);
|
|
4350
|
-
if (exitCode !== 0) throw new ExternalToolError({
|
|
4351
|
-
tool: NPX,
|
|
4352
|
-
argv,
|
|
4353
|
-
exitCode,
|
|
4354
|
-
stderrTail: ""
|
|
4355
|
-
});
|
|
4558
|
+
]);
|
|
4356
4559
|
}
|
|
4357
4560
|
async function list() {
|
|
4358
4561
|
const argv = [
|
|
@@ -4427,6 +4630,12 @@ function withStepRecord(state, id, record) {
|
|
|
4427
4630
|
}
|
|
4428
4631
|
};
|
|
4429
4632
|
}
|
|
4633
|
+
function failureAt(step, error) {
|
|
4634
|
+
return error instanceof AbortError ? error : new StepFailedError({
|
|
4635
|
+
id: step.id,
|
|
4636
|
+
title: step.title
|
|
4637
|
+
}, error);
|
|
4638
|
+
}
|
|
4430
4639
|
async function runWizard(steps, ctx, options = {}) {
|
|
4431
4640
|
requireKnownStep(steps, options.from, "--from");
|
|
4432
4641
|
requireKnownStep(steps, options.only, "--only");
|
|
@@ -4436,10 +4645,12 @@ async function runWizard(steps, ctx, options = {}) {
|
|
|
4436
4645
|
title: step.title,
|
|
4437
4646
|
status
|
|
4438
4647
|
})));
|
|
4439
|
-
for (const entry of detected) {
|
|
4648
|
+
for (const entry of detected) try {
|
|
4440
4649
|
if (!await shouldRun(entry, ctx, options)) continue;
|
|
4441
4650
|
const outcome = await entry.step.run(ctx);
|
|
4442
4651
|
await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
|
|
4652
|
+
} catch (error) {
|
|
4653
|
+
throw failureAt(entry.step, error);
|
|
4443
4654
|
}
|
|
4444
4655
|
}
|
|
4445
4656
|
//#endregion
|
|
@@ -4587,13 +4798,7 @@ function installMbStep(deps = hostDeps$11) {
|
|
|
4587
4798
|
if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
4588
4799
|
const install = mbInstallCommand(resolved.installedWith);
|
|
4589
4800
|
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
4590
|
-
|
|
4591
|
-
if (exitCode !== 0) throw new ExternalToolError({
|
|
4592
|
-
tool: install.command,
|
|
4593
|
-
argv: install.args,
|
|
4594
|
-
exitCode,
|
|
4595
|
-
stderrTail: ""
|
|
4596
|
-
});
|
|
4801
|
+
await runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
|
|
4597
4802
|
const after = await inspectMb(resolved.runner);
|
|
4598
4803
|
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
4599
4804
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
@@ -4605,7 +4810,8 @@ function installMbStep(deps = hostDeps$11) {
|
|
|
4605
4810
|
//#region src/steps/agents-skills.ts
|
|
4606
4811
|
const STEP_ID$7 = "agents-skills";
|
|
4607
4812
|
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
|
|
4608
|
-
const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
4813
|
+
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
4814
|
+
const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
|
|
4609
4815
|
const DETECT_AGENTS = { kind: "detected" };
|
|
4610
4816
|
function hostDeps$10(ctx) {
|
|
4611
4817
|
return {
|
|
@@ -4630,8 +4836,17 @@ function coversAny(entry, agents) {
|
|
|
4630
4836
|
async function rdeSkillEntries(runner) {
|
|
4631
4837
|
return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
|
|
4632
4838
|
}
|
|
4839
|
+
function requiredEntries(installed) {
|
|
4840
|
+
return installed.filter((entry) => REQUIRED_SKILL_NAMES.includes(entry.name));
|
|
4841
|
+
}
|
|
4842
|
+
function missingSkills(installed, agents) {
|
|
4843
|
+
return REQUIRED_SKILL_NAMES.filter((name) => !installed.some((entry) => entry.name === name && coversAny(entry, agents)));
|
|
4844
|
+
}
|
|
4845
|
+
function unlistedSkills(installed) {
|
|
4846
|
+
return REQUIRED_SKILL_NAMES.filter((name) => !installed.some((entry) => entry.name === name));
|
|
4847
|
+
}
|
|
4633
4848
|
function installedLines(entries) {
|
|
4634
|
-
return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
|
|
4849
|
+
return entries.map((entry) => `${entry.name} (${entry.agents.join(", ")}): ${entry.path}`).join("\n");
|
|
4635
4850
|
}
|
|
4636
4851
|
const SOURCE_NOTES = {
|
|
4637
4852
|
override: "from --agents",
|
|
@@ -4653,7 +4868,7 @@ async function askAgents(ctx, all) {
|
|
|
4653
4868
|
const offered = all.filter((agent) => agent.detected || agent.id === "opencode");
|
|
4654
4869
|
const picked = await ctx.prompts.multiselect({
|
|
4655
4870
|
key: AGENTS_SKILLS_KEYS.agents,
|
|
4656
|
-
message: "Which coding agents should get the rde
|
|
4871
|
+
message: "Which coding agents should get the rde and data-app skills?",
|
|
4657
4872
|
choices: offered.map(offeredChoice),
|
|
4658
4873
|
initialValues: detectedOnly(all).map((agent) => agent.id),
|
|
4659
4874
|
required: false
|
|
@@ -4683,8 +4898,7 @@ async function installOpencode(ctx, deps) {
|
|
|
4683
4898
|
const install = opencodeInstallCommand(deps.installedWith);
|
|
4684
4899
|
const commandLine = [install.command, ...install.args].join(" ");
|
|
4685
4900
|
ctx.ui.info(`Running ${commandLine}`);
|
|
4686
|
-
|
|
4687
|
-
if (exitCode !== 0) throw new ConfigError(`\`${commandLine}\` exited with ${exitCode}`);
|
|
4901
|
+
await runShown(deps.runner, ctx.ui.childOutput, install.command, install.args);
|
|
4688
4902
|
if (!(await detectAgents(deps.probe)).some((agent) => agent.id === "opencode" && agent.detected)) throw new ConfigError("OpenCode was installed but `opencode` is not on PATH; open a new terminal and run rde init again");
|
|
4689
4903
|
}
|
|
4690
4904
|
function listsMissingOpencode(all, options) {
|
|
@@ -4693,21 +4907,21 @@ function listsMissingOpencode(all, options) {
|
|
|
4693
4907
|
function skipLine(all, source) {
|
|
4694
4908
|
switch (source) {
|
|
4695
4909
|
case "override":
|
|
4696
|
-
case "chosen": return `Skipping the
|
|
4910
|
+
case "chosen": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
|
|
4697
4911
|
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
|
|
4698
4912
|
}
|
|
4699
4913
|
}
|
|
4700
4914
|
function agentsSkillsStep(options, deps = hostDeps$10) {
|
|
4701
4915
|
return {
|
|
4702
4916
|
id: STEP_ID$7,
|
|
4703
|
-
title: "Install the rde
|
|
4917
|
+
title: "Install the rde and data-app skills for your coding agents",
|
|
4704
4918
|
async detect(ctx) {
|
|
4705
4919
|
const resolved = deps(ctx);
|
|
4706
4920
|
const all = await detectAgents(resolved.probe);
|
|
4707
4921
|
if (listsMissingOpencode(all, options)) return "todo";
|
|
4708
4922
|
const detected = detectedOnly(all);
|
|
4709
4923
|
if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
|
|
4710
|
-
return (await
|
|
4924
|
+
return missingSkills(await skillsCli(resolved.runner).list(), detected).length === 0 ? "done" : "todo";
|
|
4711
4925
|
},
|
|
4712
4926
|
async run(ctx) {
|
|
4713
4927
|
const resolved = deps(ctx);
|
|
@@ -4718,16 +4932,28 @@ function agentsSkillsStep(options, deps = hostDeps$10) {
|
|
|
4718
4932
|
return "skipped";
|
|
4719
4933
|
}
|
|
4720
4934
|
if (selection.agents.some(isMissingOpencode)) await installOpencode(ctx, resolved);
|
|
4721
|
-
const
|
|
4722
|
-
|
|
4723
|
-
|
|
4935
|
+
const skills = skillsCli(resolved.runner);
|
|
4936
|
+
const agents = selection.agents.map((agent) => agent.skillsCliName);
|
|
4937
|
+
const forWhom = `for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`;
|
|
4938
|
+
ctx.ui.info(`Installing the data-app skills ${forWhom}`);
|
|
4939
|
+
await skills.add({
|
|
4940
|
+
source: DATA_APP_SKILLS_SOURCE,
|
|
4941
|
+
skills: DATA_APP_SKILL_NAMES,
|
|
4942
|
+
agents,
|
|
4943
|
+
output: ctx.ui.childOutput
|
|
4944
|
+
});
|
|
4945
|
+
const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-generic-skills" ? "" : ` from ${resolved.skillSource}`;
|
|
4946
|
+
ctx.ui.info(`Installing the rde skills ${forWhom}${from}`);
|
|
4947
|
+
await skills.add({
|
|
4724
4948
|
source: resolved.skillSource,
|
|
4725
|
-
|
|
4726
|
-
agents
|
|
4949
|
+
skills: RDE_SOURCE_SKILL_NAMES,
|
|
4950
|
+
agents,
|
|
4951
|
+
output: ctx.ui.childOutput
|
|
4727
4952
|
});
|
|
4728
|
-
const
|
|
4729
|
-
|
|
4730
|
-
|
|
4953
|
+
const installed = await skills.list();
|
|
4954
|
+
const missing = unlistedSkills(installed);
|
|
4955
|
+
if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
|
|
4956
|
+
ctx.ui.note(installedLines(requiredEntries(installed)), "Skills installed");
|
|
4731
4957
|
ctx.ui.info(MECHANICS_NOTE);
|
|
4732
4958
|
return "completed";
|
|
4733
4959
|
}
|
|
@@ -5545,7 +5771,7 @@ const SAMPLE_LABEL = "sample database";
|
|
|
5545
5771
|
async function sampleDatabaseRow(record, deps, dockerOk, access) {
|
|
5546
5772
|
const id = "sample-database";
|
|
5547
5773
|
if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
|
|
5548
|
-
const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
|
|
5774
|
+
const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
|
|
5549
5775
|
if (!status.installed) return failed(id, SAMPLE_LABEL, {
|
|
5550
5776
|
detail: `${record.containerName} does not exist`,
|
|
5551
5777
|
hint: "`rde init` recreates it",
|
|
@@ -5608,6 +5834,7 @@ async function gitSyncRow(state, deps) {
|
|
|
5608
5834
|
const SKILL_STEP = "agents-skills";
|
|
5609
5835
|
const GLOBAL_SCOPE = "global";
|
|
5610
5836
|
const SKILL_FILE = "SKILL.md";
|
|
5837
|
+
const SOURCE_SKILLS_DIR = "skills";
|
|
5611
5838
|
const CLAUDE_CODE = "Claude Code";
|
|
5612
5839
|
const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
|
|
5613
5840
|
function skillRowId(agent) {
|
|
@@ -5625,7 +5852,7 @@ async function judgeCopy(entry, origin, deps) {
|
|
|
5625
5852
|
switch (origin.kind) {
|
|
5626
5853
|
case "local": {
|
|
5627
5854
|
const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
|
|
5628
|
-
const wanted = await deps.skills.files.readText(join(origin.path, SKILL_FILE));
|
|
5855
|
+
const wanted = await deps.skills.files.readText(join(origin.path, SOURCE_SKILLS_DIR, entry.name, SKILL_FILE));
|
|
5629
5856
|
const current = installed !== null && installed === wanted;
|
|
5630
5857
|
return {
|
|
5631
5858
|
entry,
|
|
@@ -5919,6 +6146,62 @@ function storedLogin(variant, user) {
|
|
|
5919
6146
|
case "prompted": return null;
|
|
5920
6147
|
}
|
|
5921
6148
|
}
|
|
6149
|
+
async function resumeGeneratedAdmin(ctx, client) {
|
|
6150
|
+
const login = ctx.secrets.admin;
|
|
6151
|
+
if (login === null) return false;
|
|
6152
|
+
try {
|
|
6153
|
+
const session = await client.session.create({
|
|
6154
|
+
username: login.email,
|
|
6155
|
+
password: login.password
|
|
6156
|
+
});
|
|
6157
|
+
ctx.session.metabaseSession.set(session.id);
|
|
6158
|
+
} catch (error) {
|
|
6159
|
+
if (isRejectedCredential(error)) return false;
|
|
6160
|
+
throw error;
|
|
6161
|
+
}
|
|
6162
|
+
await ctx.saveState({
|
|
6163
|
+
...ctx.state,
|
|
6164
|
+
admin: {
|
|
6165
|
+
kind: "generated",
|
|
6166
|
+
email: login.email
|
|
6167
|
+
}
|
|
6168
|
+
});
|
|
6169
|
+
ctx.ui.success(`Admin user ${login.email} recorded (created by an earlier rde init that was interrupted)`);
|
|
6170
|
+
return true;
|
|
6171
|
+
}
|
|
6172
|
+
async function recordExistingAdmin(ctx, variant, client, url) {
|
|
6173
|
+
if (variant !== "prompted") {
|
|
6174
|
+
if (await resumeGeneratedAdmin(ctx, client)) return "completed";
|
|
6175
|
+
throw new ConfigError(`Metabase at ${url} already has an admin that rde did not create; pass its login with --admin-email and --admin-password, or run \`rde uninstall\` then \`rde init\` for a fresh instance`);
|
|
6176
|
+
}
|
|
6177
|
+
const email = await ctx.prompts.text({
|
|
6178
|
+
key: ADMIN_USER_KEYS.email,
|
|
6179
|
+
message: "Admin email",
|
|
6180
|
+
validate: validateEmail
|
|
6181
|
+
});
|
|
6182
|
+
const password = await ctx.prompts.password({
|
|
6183
|
+
key: ADMIN_USER_KEYS.password,
|
|
6184
|
+
message: "Admin password"
|
|
6185
|
+
});
|
|
6186
|
+
const session = await client.session.create({
|
|
6187
|
+
username: email,
|
|
6188
|
+
password
|
|
6189
|
+
});
|
|
6190
|
+
ctx.session.metabaseSession.set(session.id);
|
|
6191
|
+
await ctx.saveSecrets({
|
|
6192
|
+
...ctx.secrets,
|
|
6193
|
+
admin: null
|
|
6194
|
+
});
|
|
6195
|
+
await ctx.saveState({
|
|
6196
|
+
...ctx.state,
|
|
6197
|
+
admin: {
|
|
6198
|
+
kind: "prompted",
|
|
6199
|
+
email
|
|
6200
|
+
}
|
|
6201
|
+
});
|
|
6202
|
+
ctx.ui.success(`Admin user ${email} recorded`);
|
|
6203
|
+
return "completed";
|
|
6204
|
+
}
|
|
5922
6205
|
function adminUserStep(options, deps = hostDeps$8) {
|
|
5923
6206
|
return {
|
|
5924
6207
|
id: STEP_ID$6,
|
|
@@ -5927,7 +6210,7 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5927
6210
|
const configured = configuredRuntime(ctx.state);
|
|
5928
6211
|
if (configured === null) return "todo";
|
|
5929
6212
|
try {
|
|
5930
|
-
return (await deps(ctx).client(configured.url, NO_AUTH).session.properties())["has-user-setup"] ? "done" : "todo";
|
|
6213
|
+
return (await deps(ctx).client(configured.url, NO_AUTH).session.properties())["has-user-setup"] && ctx.state.admin !== null ? "done" : "todo";
|
|
5931
6214
|
} catch (error) {
|
|
5932
6215
|
if (isNotReady(error)) return "todo";
|
|
5933
6216
|
throw error;
|
|
@@ -5939,6 +6222,7 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5939
6222
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
5940
6223
|
const properties = await client.session.properties();
|
|
5941
6224
|
if (properties["has-user-setup"]) {
|
|
6225
|
+
if (ctx.state.admin === null) return recordExistingAdmin(ctx, options.variant, client, configured.url);
|
|
5942
6226
|
ctx.ui.info("Metabase already has an admin user");
|
|
5943
6227
|
return "completed";
|
|
5944
6228
|
}
|
|
@@ -6147,6 +6431,16 @@ const FALLBACK_REMOTE_NAME = "rde-remote";
|
|
|
6147
6431
|
const README_NAME = "README.md";
|
|
6148
6432
|
const README_CONTENT = "# Metabase content\n\nMetabase content synced by rde.\n";
|
|
6149
6433
|
const INITIAL_COMMIT_MESSAGE = "Initial commit";
|
|
6434
|
+
const GITIGNORE_NAME = ".gitignore";
|
|
6435
|
+
const IGNORED_PATHS = [
|
|
6436
|
+
".env",
|
|
6437
|
+
".env.local",
|
|
6438
|
+
".env.*.local",
|
|
6439
|
+
"node_modules/",
|
|
6440
|
+
".vite/",
|
|
6441
|
+
".scratch/",
|
|
6442
|
+
".DS_Store"
|
|
6443
|
+
];
|
|
6150
6444
|
const HTTPS_PREFIX = "https://";
|
|
6151
6445
|
const HOME_PREFIX = "~/";
|
|
6152
6446
|
const BAD_REQUEST_STATUS = 400;
|
|
@@ -6356,10 +6650,20 @@ function hostedScreen(run) {
|
|
|
6356
6650
|
}
|
|
6357
6651
|
};
|
|
6358
6652
|
}
|
|
6653
|
+
async function ignoreDataAppFiles(workdir) {
|
|
6654
|
+
const path = join(workdir, GITIGNORE_NAME);
|
|
6655
|
+
const existing = await readTextFile(path);
|
|
6656
|
+
const present = new Set(existing === null ? [] : existing.split("\n").map((line) => line.trim()));
|
|
6657
|
+
const missing = IGNORED_PATHS.filter((entry) => !present.has(entry));
|
|
6658
|
+
if (missing.length === 0) return;
|
|
6659
|
+
const separator = existing === null || existing.endsWith("\n") ? "" : "\n";
|
|
6660
|
+
await promises.appendFile(path, `${separator}${missing.join("\n")}\n`);
|
|
6661
|
+
}
|
|
6359
6662
|
async function prepareWorkdir(run, workdir) {
|
|
6360
6663
|
if (!await run.git.isRepo(workdir)) await run.git.init(workdir, { branch: DEFAULT_BRANCH });
|
|
6361
6664
|
if (!await run.git.hasCommits(workdir)) {
|
|
6362
6665
|
await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
|
|
6666
|
+
await ignoreDataAppFiles(workdir);
|
|
6363
6667
|
await run.git.commitAll(workdir, INITIAL_COMMIT_MESSAGE);
|
|
6364
6668
|
}
|
|
6365
6669
|
return {
|
|
@@ -6758,7 +7062,7 @@ async function chooseRuntime(ctx, options, deps) {
|
|
|
6758
7062
|
return {
|
|
6759
7063
|
kind,
|
|
6760
7064
|
image,
|
|
6761
|
-
containerName: containerName(ctx.state.name),
|
|
7065
|
+
containerName: containerName(ctx.state.name, deps.runtime.rdeHome),
|
|
6762
7066
|
port,
|
|
6763
7067
|
hostAlias: DOCKER_HOST_ALIAS
|
|
6764
7068
|
};
|
|
@@ -6852,7 +7156,7 @@ async function recordedDatabase(record, client) {
|
|
|
6852
7156
|
async function persistedRecord(ctx, deps) {
|
|
6853
7157
|
const existing = ctx.state.sampleDatabase;
|
|
6854
7158
|
if (existing !== null) {
|
|
6855
|
-
if ((await sampleDatabaseContainer(existing, deps.runtime.docker).status()).installed || await deps.ports.isFree(existing.port)) return {
|
|
7159
|
+
if ((await sampleDatabaseContainer(existing, deps.runtime.docker, deps.runtime.rdeHome).status()).installed || await deps.ports.isFree(existing.port)) return {
|
|
6856
7160
|
record: existing,
|
|
6857
7161
|
staleDatabaseId: null
|
|
6858
7162
|
};
|
|
@@ -6862,7 +7166,7 @@ async function persistedRecord(ctx, deps) {
|
|
|
6862
7166
|
const port = await deps.ports.findFree(SAMPLE_DATABASE_PORT);
|
|
6863
7167
|
if (port !== 15432) ctx.ui.warn(`Port ${SAMPLE_DATABASE_PORT} is in use; using ${port} for the sample database`);
|
|
6864
7168
|
const record = {
|
|
6865
|
-
containerName: sampleContainerName(ctx.state.name),
|
|
7169
|
+
containerName: sampleContainerName(ctx.state.name, deps.runtime.rdeHome),
|
|
6866
7170
|
image: SAMPLE_DATABASE_IMAGE,
|
|
6867
7171
|
port,
|
|
6868
7172
|
databaseId: null
|
|
@@ -6876,16 +7180,30 @@ async function persistedRecord(ctx, deps) {
|
|
|
6876
7180
|
staleDatabaseId: existing?.databaseId ?? null
|
|
6877
7181
|
};
|
|
6878
7182
|
}
|
|
6879
|
-
async function
|
|
6880
|
-
|
|
7183
|
+
async function recordSampleDatabaseMove(ctx, from, to) {
|
|
7184
|
+
ctx.ui.warn(`Port ${from.port} is taken; the sample database moved to ${to.port}`);
|
|
7185
|
+
const record = {
|
|
7186
|
+
...to,
|
|
7187
|
+
databaseId: null
|
|
7188
|
+
};
|
|
7189
|
+
await ctx.saveState({
|
|
7190
|
+
...ctx.state,
|
|
7191
|
+
sampleDatabase: record,
|
|
7192
|
+
databases: ctx.state.databases.filter((entry) => entry.id !== from.databaseId)
|
|
7193
|
+
});
|
|
7194
|
+
return record;
|
|
7195
|
+
}
|
|
7196
|
+
async function installAndStart(ctx, sidecar, installed, deps) {
|
|
7197
|
+
const installing = ctx.ui.spinner(`Installing the sample database from ${installed.image}`);
|
|
6881
7198
|
try {
|
|
6882
7199
|
await sidecar.install({ onProgress: (line) => installing.update(line) });
|
|
6883
7200
|
} catch (error) {
|
|
6884
7201
|
installing.fail("Sample database install failed");
|
|
6885
7202
|
throw error;
|
|
6886
7203
|
}
|
|
6887
|
-
installing.stop(`Sample database container ${
|
|
6888
|
-
await
|
|
7204
|
+
installing.stop(`Sample database container ${installed.containerName} on port ${installed.port}`);
|
|
7205
|
+
const started = await startOnFreePort(installed, deps.runtime.docker, deps.ports, deps.runtime.rdeHome);
|
|
7206
|
+
const record = started.state.port === installed.port ? installed : await recordSampleDatabaseMove(ctx, installed, started.state);
|
|
6889
7207
|
const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
|
|
6890
7208
|
try {
|
|
6891
7209
|
await sidecar.waitReady({
|
|
@@ -6897,6 +7215,7 @@ async function installAndStart(ctx, sidecar, record, deps) {
|
|
|
6897
7215
|
throw error;
|
|
6898
7216
|
}
|
|
6899
7217
|
waiting.stop(`Postgres is ready on ${LOOPBACK_HOST}:${record.port}`);
|
|
7218
|
+
return record;
|
|
6900
7219
|
}
|
|
6901
7220
|
function connectionDetails(record, configured, deps) {
|
|
6902
7221
|
const alias = runtimeFromState(configured.runtime, deps.runtime).hostAlias();
|
|
@@ -6961,7 +7280,7 @@ function sampleDatabaseStep(deps = hostDeps$3) {
|
|
|
6961
7280
|
const record = ctx.state.sampleDatabase;
|
|
6962
7281
|
const configured = configuredRuntime(ctx.state);
|
|
6963
7282
|
if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
6964
|
-
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
|
|
7283
|
+
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker, resolved.runtime.rdeHome).status()).installed) return "todo";
|
|
6965
7284
|
const client = resolved.client(configured.url, {
|
|
6966
7285
|
kind: "api-key",
|
|
6967
7286
|
key: ctx.secrets.apiKey
|
|
@@ -6979,24 +7298,24 @@ function sampleDatabaseStep(deps = hostDeps$3) {
|
|
|
6979
7298
|
const apiKey = requireApiKey(ctx);
|
|
6980
7299
|
const docker = await resolved.runtime.docker.available();
|
|
6981
7300
|
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
6982
|
-
const
|
|
6983
|
-
await installAndStart(ctx, sampleDatabaseContainer(
|
|
7301
|
+
const persisted = await persistedRecord(ctx, resolved);
|
|
7302
|
+
const record = await installAndStart(ctx, sampleDatabaseContainer(persisted.record, resolved.runtime.docker, resolved.runtime.rdeHome), persisted.record, resolved);
|
|
7303
|
+
const choice = {
|
|
7304
|
+
...persisted,
|
|
7305
|
+
record
|
|
7306
|
+
};
|
|
6984
7307
|
const client = resolved.client(configured.url, {
|
|
6985
7308
|
kind: "api-key",
|
|
6986
7309
|
key: apiKey
|
|
6987
7310
|
});
|
|
6988
7311
|
await connectDatabase(ctx, choice, configured, client, resolved);
|
|
6989
7312
|
await client.setting.enableTransforms();
|
|
6990
|
-
ctx.ui.note(usageLines(
|
|
7313
|
+
ctx.ui.note(usageLines(record), "Sample database");
|
|
6991
7314
|
return "completed";
|
|
6992
7315
|
}
|
|
6993
7316
|
};
|
|
6994
7317
|
}
|
|
6995
7318
|
const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
|
|
6996
|
-
async function startAndWaitForHealth(runtime, client, options) {
|
|
6997
|
-
await runtime.start();
|
|
6998
|
-
await waitForHealth(client, options);
|
|
6999
|
-
}
|
|
7000
7319
|
//#endregion
|
|
7001
7320
|
//#region src/steps/start-metabase.ts
|
|
7002
7321
|
const LOG_TAIL_ON_TIMEOUT = 30;
|
|
@@ -7004,6 +7323,7 @@ const PERCENT = 100;
|
|
|
7004
7323
|
function hostDeps$2(ctx) {
|
|
7005
7324
|
return {
|
|
7006
7325
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7326
|
+
ports: hostPortProbe,
|
|
7007
7327
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7008
7328
|
timing: DEFAULT_HEALTH_TIMING
|
|
7009
7329
|
};
|
|
@@ -7020,7 +7340,9 @@ function progressLabel(progress) {
|
|
|
7020
7340
|
async function startSampleDatabase(ctx, deps) {
|
|
7021
7341
|
const record = ctx.state.sampleDatabase;
|
|
7022
7342
|
if (record === null) return;
|
|
7023
|
-
|
|
7343
|
+
const started = await startOnFreePort(record, deps.runtime.docker, deps.ports, deps.runtime.rdeHome);
|
|
7344
|
+
if (started.state.port !== record.port) await recordSampleDatabaseMove(ctx, record, started.state);
|
|
7345
|
+
switch (started.outcome) {
|
|
7024
7346
|
case "missing":
|
|
7025
7347
|
ctx.ui.warn(`Sample database container ${record.containerName} is missing; \`rde init\` recreates it`);
|
|
7026
7348
|
return;
|
|
@@ -7051,11 +7373,16 @@ function startMetabaseStep(deps = hostDeps$2) {
|
|
|
7051
7373
|
const runtime = runtimeFromState(configured.runtime, resolved.runtime);
|
|
7052
7374
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
7053
7375
|
await startSampleDatabase(ctx, resolved);
|
|
7376
|
+
await runtime.start({ onNotice: (line) => ctx.ui.info(line) });
|
|
7054
7377
|
const spinner = ctx.ui.spinner("Starting Metabase…");
|
|
7378
|
+
let shown = 0;
|
|
7055
7379
|
try {
|
|
7056
|
-
await
|
|
7380
|
+
await waitForHealth(client, {
|
|
7057
7381
|
...resolved.timing,
|
|
7058
|
-
onProgress: (progress) =>
|
|
7382
|
+
onProgress: (progress) => {
|
|
7383
|
+
shown = Math.max(shown, progress);
|
|
7384
|
+
spinner.update(progressLabel(shown));
|
|
7385
|
+
}
|
|
7059
7386
|
});
|
|
7060
7387
|
} catch (error) {
|
|
7061
7388
|
if (error instanceof MetabaseUnreachableError) {
|
|
@@ -7154,7 +7481,7 @@ async function summaryLines(state, apiKey, deps) {
|
|
|
7154
7481
|
const installed = await skillsCli(deps.runner).list();
|
|
7155
7482
|
lines.push(skillLine(installed.filter((entry) => entry.name === "rde")));
|
|
7156
7483
|
lines.push(...launchLines(await detectAgents(deps.probe), state.gitRemote?.workdir ?? null));
|
|
7157
|
-
lines.push("Lifecycle: rde status, rde stop, rde start");
|
|
7484
|
+
lines.push("Lifecycle: rde status (--json for scripts), rde stop, rde start");
|
|
7158
7485
|
return lines;
|
|
7159
7486
|
}
|
|
7160
7487
|
function summaryStep(deps = hostDeps$1) {
|
|
@@ -7354,7 +7681,7 @@ const doctor = defineRdeCommand({
|
|
|
7354
7681
|
if (args.fix && report.failed > 0) {
|
|
7355
7682
|
if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
|
|
7356
7683
|
print(report, args.json);
|
|
7357
|
-
const prompts = answeringPrompts(clackPrompts(), /* @__PURE__ */ new Map(), { takeRecommended: args.yes || !stdinIsTty() });
|
|
7684
|
+
const prompts = answeringPrompts(clackPrompts({ confirmsWithYes: true }), /* @__PURE__ */ new Map(), { takeRecommended: args.yes || !stdinIsTty() });
|
|
7358
7685
|
const ctx = await createWizardContext(name, {
|
|
7359
7686
|
signal: interruptSignal,
|
|
7360
7687
|
prompts
|
|
@@ -7998,7 +8325,7 @@ function promptsFor(answers, yes) {
|
|
|
7998
8325
|
...yes && { confirmAll: true },
|
|
7999
8326
|
...(yes || !terminal) && { takeRecommended: true }
|
|
8000
8327
|
};
|
|
8001
|
-
return answeringPrompts(clackPrompts(), keyedAnswers(answers), answering);
|
|
8328
|
+
return answeringPrompts(clackPrompts({ confirmsWithYes: true }), keyedAnswers(answers), answering);
|
|
8002
8329
|
}
|
|
8003
8330
|
const init = defineRdeCommand({
|
|
8004
8331
|
meta: {
|
|
@@ -8016,7 +8343,7 @@ const init = defineRdeCommand({
|
|
|
8016
8343
|
},
|
|
8017
8344
|
runtime: {
|
|
8018
8345
|
type: "string",
|
|
8019
|
-
description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available`
|
|
8346
|
+
description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available; the sample database always runs in Docker`
|
|
8020
8347
|
},
|
|
8021
8348
|
image: {
|
|
8022
8349
|
type: "string",
|
|
@@ -8065,7 +8392,7 @@ const init = defineRdeCommand({
|
|
|
8065
8392
|
},
|
|
8066
8393
|
"skip-databases": {
|
|
8067
8394
|
type: "boolean",
|
|
8068
|
-
description: "Connect
|
|
8395
|
+
description: "Connect none of your own databases (the sample database is still connected)",
|
|
8069
8396
|
default: false
|
|
8070
8397
|
},
|
|
8071
8398
|
"git-dir": {
|
|
@@ -8104,7 +8431,7 @@ const init = defineRdeCommand({
|
|
|
8104
8431
|
},
|
|
8105
8432
|
yes: {
|
|
8106
8433
|
type: "boolean",
|
|
8107
|
-
description: "Answer yes to every confirmation (
|
|
8434
|
+
description: "Answer yes to every confirmation; skips the question about your own databases (the sample database is still connected)",
|
|
8108
8435
|
default: false
|
|
8109
8436
|
}
|
|
8110
8437
|
},
|
|
@@ -8144,7 +8471,7 @@ const init = defineRdeCommand({
|
|
|
8144
8471
|
const answers = await combinedAnswers(runOptions);
|
|
8145
8472
|
const answering = promptsFor(answers, runOptions.yes);
|
|
8146
8473
|
const recording = printTo === void 0 ? null : recordingPrompts(answering);
|
|
8147
|
-
const ctx = await createWizardContext(
|
|
8474
|
+
const ctx = await createWizardContext(await instanceNameOrDefault(), {
|
|
8148
8475
|
signal: interruptSignal,
|
|
8149
8476
|
prompts: recording?.prompts ?? answering
|
|
8150
8477
|
});
|
|
@@ -8167,7 +8494,7 @@ async function loadInstance(name, signal) {
|
|
|
8167
8494
|
state,
|
|
8168
8495
|
url: configured.url,
|
|
8169
8496
|
runtime: runtimeFromState(configured.runtime, deps),
|
|
8170
|
-
sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker)
|
|
8497
|
+
sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker, deps.rdeHome)
|
|
8171
8498
|
};
|
|
8172
8499
|
}
|
|
8173
8500
|
function openInstance(url) {
|
|
@@ -8252,6 +8579,16 @@ async function stepReports(ctx, deps) {
|
|
|
8252
8579
|
}
|
|
8253
8580
|
return reports;
|
|
8254
8581
|
}
|
|
8582
|
+
function setupOf(ctx, steps) {
|
|
8583
|
+
const pending = steps.find((step) => step.status === "todo" && ctx.state.steps[step.id] === void 0);
|
|
8584
|
+
if (pending === void 0) return { complete: true };
|
|
8585
|
+
return {
|
|
8586
|
+
complete: false,
|
|
8587
|
+
step: pending.id,
|
|
8588
|
+
title: pending.title,
|
|
8589
|
+
resume: `rde init --from ${pending.id}`
|
|
8590
|
+
};
|
|
8591
|
+
}
|
|
8255
8592
|
function isNotServing(error) {
|
|
8256
8593
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
8257
8594
|
}
|
|
@@ -8267,7 +8604,7 @@ async function databasesOf(ctx, deps, health) {
|
|
|
8267
8604
|
async function sampleDatabaseOf(ctx, deps, databases) {
|
|
8268
8605
|
const record = ctx.state.sampleDatabase;
|
|
8269
8606
|
if (record === null) return null;
|
|
8270
|
-
const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
|
|
8607
|
+
const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
|
|
8271
8608
|
const listed = databases?.find((database) => database.id === record.databaseId) ?? null;
|
|
8272
8609
|
return {
|
|
8273
8610
|
container: status.running ? "running" : status.installed ? "stopped" : "missing",
|
|
@@ -8304,6 +8641,7 @@ async function collectStatus(ctx, deps) {
|
|
|
8304
8641
|
const health = configured === null ? null : await healthOf(configured.url, deps);
|
|
8305
8642
|
const databases = health === null ? null : await databasesOf(ctx, deps, health);
|
|
8306
8643
|
const admin = ctx.state.admin;
|
|
8644
|
+
const steps = await stepReports(ctx, deps);
|
|
8307
8645
|
return {
|
|
8308
8646
|
instance: ctx.state.name,
|
|
8309
8647
|
url: configured?.url ?? null,
|
|
@@ -8317,10 +8655,12 @@ async function collectStatus(ctx, deps) {
|
|
|
8317
8655
|
},
|
|
8318
8656
|
version: ctx.state.version,
|
|
8319
8657
|
health,
|
|
8320
|
-
steps
|
|
8658
|
+
steps,
|
|
8659
|
+
setup: setupOf(ctx, steps),
|
|
8321
8660
|
databases,
|
|
8322
8661
|
sampleDatabase: await sampleDatabaseOf(ctx, deps, databases),
|
|
8323
|
-
gitSync: await gitSyncOf(ctx, deps)
|
|
8662
|
+
gitSync: await gitSyncOf(ctx, deps),
|
|
8663
|
+
contentRepository: ctx.state.gitRemote === null ? null : ctx.state.gitRemote.workdir
|
|
8324
8664
|
};
|
|
8325
8665
|
}
|
|
8326
8666
|
function runtimeLine(report) {
|
|
@@ -8332,6 +8672,10 @@ function runtimeLine(report) {
|
|
|
8332
8672
|
function versionLine(report) {
|
|
8333
8673
|
return report.version === null ? "Version: not recorded yet; `rde start` records it" : `Version: ${report.version}`;
|
|
8334
8674
|
}
|
|
8675
|
+
function setupLine(setup) {
|
|
8676
|
+
if (setup.complete) return "Setup: complete";
|
|
8677
|
+
return `Setup: incomplete; "${setup.title}" is not done; run \`${setup.resume}\``;
|
|
8678
|
+
}
|
|
8335
8679
|
function isGitSyncFailure(value) {
|
|
8336
8680
|
return "error" in value;
|
|
8337
8681
|
}
|
|
@@ -8368,11 +8712,13 @@ function statusLines(report) {
|
|
|
8368
8712
|
runtimeLine(report),
|
|
8369
8713
|
versionLine(report),
|
|
8370
8714
|
`Health: ${report.health ?? "unknown"}`,
|
|
8715
|
+
setupLine(report.setup),
|
|
8371
8716
|
"Steps:",
|
|
8372
8717
|
...checklistLines(report.steps).map((line) => ` ${line}`),
|
|
8373
8718
|
...databaseLines(report),
|
|
8374
8719
|
sampleDatabaseLine(report),
|
|
8375
|
-
gitSyncLine(report)
|
|
8720
|
+
gitSyncLine(report),
|
|
8721
|
+
...report.contentRepository === null ? [] : [`Content repository: ${report.contentRepository}`]
|
|
8376
8722
|
];
|
|
8377
8723
|
}
|
|
8378
8724
|
//#endregion
|
|
@@ -8421,12 +8767,13 @@ const stop = defineRdeCommand({
|
|
|
8421
8767
|
//#endregion
|
|
8422
8768
|
//#region src/steps/uninstall.ts
|
|
8423
8769
|
const API_KEY_METHOD = "apiKey";
|
|
8424
|
-
function hostUninstallDeps(signal) {
|
|
8770
|
+
function hostUninstallDeps(signal, childOutput) {
|
|
8425
8771
|
const runner = processRunner(signal);
|
|
8426
8772
|
return {
|
|
8427
8773
|
docker: dockerTool(runner),
|
|
8428
8774
|
daemon: daemonTool(runner),
|
|
8429
|
-
runner
|
|
8775
|
+
runner,
|
|
8776
|
+
childOutput
|
|
8430
8777
|
};
|
|
8431
8778
|
}
|
|
8432
8779
|
const NOT_TOUCHED = "The mb CLI, Docker, and Java themselves";
|
|
@@ -8472,7 +8819,7 @@ async function pathExists(path) {
|
|
|
8472
8819
|
throw error;
|
|
8473
8820
|
}
|
|
8474
8821
|
}
|
|
8475
|
-
async function findContainer(name, docker, stateImage) {
|
|
8822
|
+
async function findContainer(name, docker, stateImage, home) {
|
|
8476
8823
|
try {
|
|
8477
8824
|
const inspection = await docker.containerInspect(name);
|
|
8478
8825
|
if (!inspection.exists) return {
|
|
@@ -8480,6 +8827,14 @@ async function findContainer(name, docker, stateImage) {
|
|
|
8480
8827
|
image: stateImage,
|
|
8481
8828
|
problem: null
|
|
8482
8829
|
};
|
|
8830
|
+
if (inspection.home !== null && inspection.home !== home) return {
|
|
8831
|
+
removal: null,
|
|
8832
|
+
image: null,
|
|
8833
|
+
problem: {
|
|
8834
|
+
subject: `Container ${name}`,
|
|
8835
|
+
reason: `belongs to the rde home ${inspection.home} and is left alone`
|
|
8836
|
+
}
|
|
8837
|
+
};
|
|
8483
8838
|
return {
|
|
8484
8839
|
removal: {
|
|
8485
8840
|
label: inspection.running ? `Container ${name} (running; it is stopped first)` : `Container ${name} (stopped)`,
|
|
@@ -8593,16 +8948,20 @@ async function findMbProfiles(candidates, url, runner) {
|
|
|
8593
8948
|
}
|
|
8594
8949
|
return removals;
|
|
8595
8950
|
}
|
|
8596
|
-
function skillLabel(entries) {
|
|
8597
|
-
|
|
8951
|
+
function skillLabel(names, entries) {
|
|
8952
|
+
const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
|
|
8953
|
+
const noun = names.length === 1 ? "skill" : "skills";
|
|
8954
|
+
return `${names.join(" and ")} ${noun} for ${agents.join(", ")}`;
|
|
8598
8955
|
}
|
|
8599
|
-
async function findSkill(runner) {
|
|
8956
|
+
async function findSkill(runner, childOutput) {
|
|
8600
8957
|
const skills = skillsCli(runner);
|
|
8601
|
-
const
|
|
8958
|
+
const owned = RDE_SOURCE_SKILL_NAMES;
|
|
8959
|
+
const entries = (await skills.list()).filter((entry) => owned.includes(entry.name));
|
|
8602
8960
|
if (entries.length === 0) return null;
|
|
8961
|
+
const names = [...new Set(entries.map((entry) => entry.name))];
|
|
8603
8962
|
return {
|
|
8604
|
-
label: skillLabel(entries),
|
|
8605
|
-
remove: () => skills.remove(
|
|
8963
|
+
label: skillLabel(names, entries),
|
|
8964
|
+
remove: () => skills.remove(names, childOutput)
|
|
8606
8965
|
};
|
|
8607
8966
|
}
|
|
8608
8967
|
function instanceDirLabel(path, state) {
|
|
@@ -8645,12 +9004,13 @@ async function planUninstall(options, deps) {
|
|
|
8645
9004
|
const kept = [];
|
|
8646
9005
|
const { state, problem } = await readStateLeniently(options.instance);
|
|
8647
9006
|
if (problem !== null) problems.push(problem);
|
|
9007
|
+
const home = rdeHome();
|
|
8648
9008
|
const runtime = state?.runtime?.kind === "docker" ? state.runtime : null;
|
|
8649
|
-
const container = await findContainer(runtime?.containerName ?? containerName(options.instance), deps.docker, runtime?.image ?? null);
|
|
9009
|
+
const container = await findContainer(runtime?.containerName ?? containerName(options.instance, home), deps.docker, runtime?.image ?? null, home);
|
|
8650
9010
|
if (container.removal !== null) removals.push(container.removal);
|
|
8651
9011
|
if (container.problem !== null) problems.push(container.problem);
|
|
8652
9012
|
const sample = state?.sampleDatabase ?? null;
|
|
8653
|
-
const sidecar = await findContainer(sample?.containerName ?? sampleContainerName(options.instance), deps.docker, sample?.image ?? null);
|
|
9013
|
+
const sidecar = await findContainer(sample?.containerName ?? sampleContainerName(options.instance, home), deps.docker, sample?.image ?? null, home);
|
|
8654
9014
|
if (sidecar.removal !== null) removals.push(sidecar.removal);
|
|
8655
9015
|
if (sidecar.problem !== null) problems.push(sidecar.problem);
|
|
8656
9016
|
const jarSource = state?.runtime?.kind === "jar" ? state.runtime.source : null;
|
|
@@ -8670,7 +9030,7 @@ async function planUninstall(options, deps) {
|
|
|
8670
9030
|
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, namedMbProfile(options.instance)], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
|
|
8671
9031
|
removals.push(...profiles);
|
|
8672
9032
|
if (options.removeSkill) {
|
|
8673
|
-
const skill = await findSkill(deps.runner);
|
|
9033
|
+
const skill = await findSkill(deps.runner, deps.childOutput);
|
|
8674
9034
|
if (skill !== null) removals.push(skill);
|
|
8675
9035
|
}
|
|
8676
9036
|
const dir = instanceDir(options.instance);
|
|
@@ -8799,15 +9159,17 @@ const uninstallCommand = defineRdeCommand({
|
|
|
8799
9159
|
}
|
|
8800
9160
|
},
|
|
8801
9161
|
async run(args) {
|
|
9162
|
+
const instance = args.instance ?? await instanceNameOrDefault();
|
|
9163
|
+
const ui = terminalUi();
|
|
8802
9164
|
const report = await uninstall({
|
|
8803
|
-
instance
|
|
9165
|
+
instance,
|
|
8804
9166
|
keepData: args["keep-data"],
|
|
8805
9167
|
removeSkill: args["remove-skill"],
|
|
8806
9168
|
removeImage: args["remove-image"],
|
|
8807
9169
|
yes: args.yes
|
|
8808
|
-
}, hostUninstallDeps(interruptSignal), {
|
|
8809
|
-
prompts: clackPrompts(),
|
|
8810
|
-
ui
|
|
9170
|
+
}, hostUninstallDeps(interruptSignal, ui.childOutput), {
|
|
9171
|
+
prompts: clackPrompts({ confirmsWithYes: true }),
|
|
9172
|
+
ui
|
|
8811
9173
|
});
|
|
8812
9174
|
if (report === null) return;
|
|
8813
9175
|
const remaining = leftBehind(report);
|
|
@@ -8821,6 +9183,7 @@ const RELEASE_TAG = /^v?[01]\.(\d+\.\d+(?:\.\d+)?)/;
|
|
|
8821
9183
|
function hostDeps(ctx) {
|
|
8822
9184
|
return {
|
|
8823
9185
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
9186
|
+
ports: hostPortProbe,
|
|
8824
9187
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
8825
9188
|
timing: DEFAULT_HEALTH_TIMING,
|
|
8826
9189
|
releases: releaseLookup(ctx.signal),
|
|
@@ -9082,7 +9445,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
9082
9445
|
const main = defineCommand({
|
|
9083
9446
|
meta: {
|
|
9084
9447
|
name: "rde",
|
|
9085
|
-
version: "0.1.
|
|
9448
|
+
version: "0.1.7",
|
|
9086
9449
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
9087
9450
|
},
|
|
9088
9451
|
subCommands: {
|
|
@@ -9100,15 +9463,24 @@ const main = defineCommand({
|
|
|
9100
9463
|
}
|
|
9101
9464
|
});
|
|
9102
9465
|
//#endregion
|
|
9466
|
+
//#region src/output/usage.ts
|
|
9467
|
+
async function printUsage(cmd, parent) {
|
|
9468
|
+
const usage = await renderUsage(cmd, parent);
|
|
9469
|
+
process.stdout.write((drawsOnTerminal() ? usage : withoutEscapes(usage)) + "\n\n");
|
|
9470
|
+
}
|
|
9471
|
+
//#endregion
|
|
9103
9472
|
//#region src/cli.ts
|
|
9104
9473
|
async function run() {
|
|
9105
9474
|
installInterruptHandler((code) => process.exit(code));
|
|
9106
9475
|
const rawArgs = process.argv.slice(2);
|
|
9107
9476
|
if (rawArgs.length === 0) {
|
|
9108
|
-
await
|
|
9477
|
+
await printUsage(main);
|
|
9109
9478
|
return;
|
|
9110
9479
|
}
|
|
9111
|
-
await runMain(main, {
|
|
9480
|
+
await runMain(main, {
|
|
9481
|
+
rawArgs,
|
|
9482
|
+
showUsage: printUsage
|
|
9483
|
+
});
|
|
9112
9484
|
}
|
|
9113
9485
|
await run();
|
|
9114
9486
|
//#endregion
|