@metabase/rde 0.1.13 → 0.1.15
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 +3 -3
- package/dist/cli.mjs +527 -193
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { defineCommand, renderUsage, runMain } from "citty";
|
|
3
3
|
import { createHash, randomBytes, randomUUID } from "node:crypto";
|
|
4
4
|
import { createWriteStream, promises } from "node:fs";
|
|
5
|
-
import { delimiter, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
5
|
+
import { delimiter, dirname, isAbsolute, join, posix, resolve, sep } from "node:path";
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
7
|
import { ZodError, z } from "zod";
|
|
8
8
|
import { homedir, tmpdir, userInfo } from "node:os";
|
|
@@ -50,6 +50,7 @@ var StepFailedError = class extends Error {
|
|
|
50
50
|
var ExternalToolError = class extends Error {
|
|
51
51
|
tool;
|
|
52
52
|
argv;
|
|
53
|
+
envNames;
|
|
53
54
|
exitCode;
|
|
54
55
|
stderrTail;
|
|
55
56
|
constructor(failure) {
|
|
@@ -57,26 +58,34 @@ var ExternalToolError = class extends Error {
|
|
|
57
58
|
this.name = "ExternalToolError";
|
|
58
59
|
this.tool = failure.tool;
|
|
59
60
|
this.argv = failure.argv;
|
|
61
|
+
this.envNames = failure.envNames;
|
|
60
62
|
this.exitCode = failure.exitCode;
|
|
61
63
|
this.stderrTail = failure.stderrTail;
|
|
62
64
|
}
|
|
63
65
|
};
|
|
66
|
+
function commandLabel(failure) {
|
|
67
|
+
const command = `\`${[failure.tool, ...failure.argv].join(" ")}\``;
|
|
68
|
+
if (failure.envNames.length === 0) return command;
|
|
69
|
+
return `${command}, run by rde with ${failure.envNames.join(" and ")} set,`;
|
|
70
|
+
}
|
|
64
71
|
function describeToolFailure(failure) {
|
|
65
|
-
const command =
|
|
72
|
+
const command = commandLabel(failure);
|
|
66
73
|
const exit = failure.exitCode === null ? "was killed" : `exited with ${failure.exitCode}`;
|
|
67
74
|
const tail = failure.stderrTail.trim();
|
|
68
|
-
return tail.length === 0 ?
|
|
75
|
+
return tail.length === 0 ? `${command} ${exit}` : `${command} ${exit}: ${tail}`;
|
|
69
76
|
}
|
|
70
77
|
var MetabaseApiError = class extends Error {
|
|
71
78
|
status;
|
|
72
79
|
path;
|
|
73
80
|
errors;
|
|
81
|
+
reason;
|
|
74
82
|
constructor(failure) {
|
|
75
83
|
super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
|
|
76
84
|
this.name = "MetabaseApiError";
|
|
77
85
|
this.status = failure.status;
|
|
78
86
|
this.path = failure.path;
|
|
79
87
|
this.errors = failure.errors;
|
|
88
|
+
this.reason = failure.message;
|
|
80
89
|
}
|
|
81
90
|
};
|
|
82
91
|
const UNAUTHORIZED_STATUS = 401;
|
|
@@ -93,12 +102,17 @@ var MetabaseInitializingError = class extends Error {
|
|
|
93
102
|
};
|
|
94
103
|
var MetabaseUnreachableError = class extends Error {
|
|
95
104
|
url;
|
|
105
|
+
reason;
|
|
96
106
|
constructor(url, reason) {
|
|
97
107
|
super(`Metabase at ${url} is unreachable: ${reason}`);
|
|
98
108
|
this.name = "MetabaseUnreachableError";
|
|
99
109
|
this.url = url;
|
|
110
|
+
this.reason = reason;
|
|
100
111
|
}
|
|
101
112
|
};
|
|
113
|
+
function isMetabaseNotServing(error) {
|
|
114
|
+
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
115
|
+
}
|
|
102
116
|
var DownloadError = class extends Error {
|
|
103
117
|
url;
|
|
104
118
|
constructor(url, reason) {
|
|
@@ -892,6 +906,11 @@ function describeCause(error) {
|
|
|
892
906
|
if (error instanceof Error && error.cause instanceof Error) return error.cause.message;
|
|
893
907
|
return error instanceof Error ? error.message : String(error);
|
|
894
908
|
}
|
|
909
|
+
function lookupFailure(error) {
|
|
910
|
+
if (error instanceof MetabaseUnreachableError) return `${error.url} is unreachable: ${error.reason}`;
|
|
911
|
+
if (error instanceof MetabaseApiError) return `HTTP ${error.status} on ${error.path}: ${error.reason}`;
|
|
912
|
+
return null;
|
|
913
|
+
}
|
|
895
914
|
function createHttp(options) {
|
|
896
915
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
897
916
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
@@ -911,7 +930,11 @@ function createHttp(options) {
|
|
|
911
930
|
init.body = JSON.stringify(body);
|
|
912
931
|
}
|
|
913
932
|
try {
|
|
914
|
-
|
|
933
|
+
const response = await fetchImpl(url, init);
|
|
934
|
+
return {
|
|
935
|
+
ok: response.ok,
|
|
936
|
+
body: parseBody(response.status, await response.text())
|
|
937
|
+
};
|
|
915
938
|
} catch (error) {
|
|
916
939
|
if (options.signal.aborted) throw new AbortError();
|
|
917
940
|
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${REQUEST_TIMEOUT_MS}ms`);
|
|
@@ -919,10 +942,9 @@ function createHttp(options) {
|
|
|
919
942
|
}
|
|
920
943
|
}
|
|
921
944
|
async function requestBody(method, path, body) {
|
|
922
|
-
const
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
return parsed;
|
|
945
|
+
const received = await send(method, path, body);
|
|
946
|
+
if (!received.ok) throw failureFor(path, received.body);
|
|
947
|
+
return received.body;
|
|
926
948
|
}
|
|
927
949
|
return {
|
|
928
950
|
async requestJson(method, path, request) {
|
|
@@ -1336,9 +1358,6 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
|
1336
1358
|
user: userResource(http)
|
|
1337
1359
|
};
|
|
1338
1360
|
}
|
|
1339
|
-
function isNotReadyYet(error) {
|
|
1340
|
-
return error instanceof MetabaseInitializingError || error instanceof MetabaseUnreachableError;
|
|
1341
|
-
}
|
|
1342
1361
|
async function waitForHealth(client, options) {
|
|
1343
1362
|
const deadline = Date.now() + options.timeoutMs;
|
|
1344
1363
|
const interval = options.pollIntervalMs ?? HEALTH_POLL_INTERVAL_MS;
|
|
@@ -1347,7 +1366,7 @@ async function waitForHealth(client, options) {
|
|
|
1347
1366
|
await client.health.get();
|
|
1348
1367
|
return;
|
|
1349
1368
|
} catch (error) {
|
|
1350
|
-
if (!
|
|
1369
|
+
if (!isMetabaseNotServing(error)) throw error;
|
|
1351
1370
|
if (error instanceof MetabaseInitializingError) options.onProgress(error.progress);
|
|
1352
1371
|
}
|
|
1353
1372
|
if (!await options.isRunning()) throw new MetabaseUnreachableError(client.baseUrl, "it exited before it became healthy");
|
|
@@ -1374,15 +1393,12 @@ function storedApiKey(state, secrets) {
|
|
|
1374
1393
|
function hostCredentialsClient(signal) {
|
|
1375
1394
|
return (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal);
|
|
1376
1395
|
}
|
|
1377
|
-
function isNotReady(error) {
|
|
1378
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
1379
|
-
}
|
|
1380
1396
|
async function hasAdminOutsideRde(state, client) {
|
|
1381
1397
|
if (state.url === null) return false;
|
|
1382
1398
|
try {
|
|
1383
1399
|
return (await client(state.url, NO_AUTH).session.properties())["has-user-setup"];
|
|
1384
1400
|
} catch (error) {
|
|
1385
|
-
if (
|
|
1401
|
+
if (isMetabaseNotServing(error)) return false;
|
|
1386
1402
|
throw error;
|
|
1387
1403
|
}
|
|
1388
1404
|
}
|
|
@@ -1818,10 +1834,11 @@ function signalProcess(pid, signal) {
|
|
|
1818
1834
|
function stderrTail(stderr) {
|
|
1819
1835
|
return stderr.trim().split("\n").slice(-5).join("\n");
|
|
1820
1836
|
}
|
|
1821
|
-
function toolFailure(tool, argv, result) {
|
|
1837
|
+
function toolFailure(tool, argv, result, options = {}) {
|
|
1822
1838
|
return new ExternalToolError({
|
|
1823
1839
|
tool,
|
|
1824
1840
|
argv,
|
|
1841
|
+
envNames: options.addedEnv === void 0 ? [] : Object.keys(options.addedEnv),
|
|
1825
1842
|
exitCode: result.exitCode,
|
|
1826
1843
|
stderrTail: stderrTail(result.stderr)
|
|
1827
1844
|
});
|
|
@@ -1833,6 +1850,7 @@ async function runShown(runner, output, command, args) {
|
|
|
1833
1850
|
if (exitCode !== 0) throw new ExternalToolError({
|
|
1834
1851
|
tool: command,
|
|
1835
1852
|
argv: args,
|
|
1853
|
+
envNames: [],
|
|
1836
1854
|
exitCode,
|
|
1837
1855
|
stderrTail: ""
|
|
1838
1856
|
});
|
|
@@ -1844,6 +1862,7 @@ async function runShown(runner, output, command, args) {
|
|
|
1844
1862
|
throw new ExternalToolError({
|
|
1845
1863
|
tool: command,
|
|
1846
1864
|
argv: args,
|
|
1865
|
+
envNames: [],
|
|
1847
1866
|
exitCode: result.exitCode,
|
|
1848
1867
|
stderrTail: stderrTail(printed.filter((line) => HAS_TEXT.test(line)).join("\n"))
|
|
1849
1868
|
});
|
|
@@ -1857,6 +1876,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
|
|
|
1857
1876
|
throw new ExternalToolError({
|
|
1858
1877
|
tool,
|
|
1859
1878
|
argv,
|
|
1879
|
+
envNames: [],
|
|
1860
1880
|
exitCode: 0,
|
|
1861
1881
|
stderrTail: `output is not JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
1862
1882
|
});
|
|
@@ -1867,6 +1887,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
|
|
|
1867
1887
|
if (error instanceof ZodError) throw new ExternalToolError({
|
|
1868
1888
|
tool,
|
|
1869
1889
|
argv,
|
|
1890
|
+
envNames: [],
|
|
1870
1891
|
exitCode: 0,
|
|
1871
1892
|
stderrTail: `unexpected output shape:\n${z.prettifyError(error)}`
|
|
1872
1893
|
});
|
|
@@ -2332,6 +2353,7 @@ function dockerTool(runner) {
|
|
|
2332
2353
|
throw new ExternalToolError({
|
|
2333
2354
|
tool: DOCKER,
|
|
2334
2355
|
argv: args,
|
|
2356
|
+
envNames: [],
|
|
2335
2357
|
exitCode: 0,
|
|
2336
2358
|
stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
|
|
2337
2359
|
});
|
|
@@ -3083,7 +3105,7 @@ function hostDeps$14(ctx) {
|
|
|
3083
3105
|
};
|
|
3084
3106
|
}
|
|
3085
3107
|
function isNotMetabaseYet(error) {
|
|
3086
|
-
return error
|
|
3108
|
+
return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
|
|
3087
3109
|
}
|
|
3088
3110
|
function validateEmail(value) {
|
|
3089
3111
|
return EMAIL.test(value) ? void 0 : "Enter an email address";
|
|
@@ -3284,7 +3306,9 @@ function adminUserStep(options, deps = hostDeps$14) {
|
|
|
3284
3306
|
//#endregion
|
|
3285
3307
|
//#region src/tools/mb.ts
|
|
3286
3308
|
const MB = "mb";
|
|
3287
|
-
const
|
|
3309
|
+
const MB_PACKAGE = "@metabase/cli";
|
|
3310
|
+
const MB_DIST_TAG = "alpha-release-65-integration-branch";
|
|
3311
|
+
const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
|
|
3288
3312
|
const GLOBAL_INSTALL = {
|
|
3289
3313
|
npm: {
|
|
3290
3314
|
command: "npm",
|
|
@@ -3441,11 +3465,12 @@ const LogoutResult = z.object({
|
|
|
3441
3465
|
cleared: z.boolean(),
|
|
3442
3466
|
aborted: z.boolean()
|
|
3443
3467
|
}).loose();
|
|
3468
|
+
const MbAuthMethod = z.enum(["oauth", "apiKey"]);
|
|
3444
3469
|
const AuthStatus = z.object({
|
|
3445
3470
|
profile: z.string(),
|
|
3446
3471
|
present: z.boolean(),
|
|
3447
3472
|
url: z.string().nullable(),
|
|
3448
|
-
method:
|
|
3473
|
+
method: MbAuthMethod.nullable(),
|
|
3449
3474
|
user: ProbedUser.nullable()
|
|
3450
3475
|
}).loose();
|
|
3451
3476
|
const AuthProfileStatus = z.enum([
|
|
@@ -3458,7 +3483,6 @@ const AuthProfileStatus = z.enum([
|
|
|
3458
3483
|
const AuthProfile = z.object({
|
|
3459
3484
|
profile: z.string(),
|
|
3460
3485
|
url: z.string(),
|
|
3461
|
-
authenticated: z.boolean(),
|
|
3462
3486
|
status: AuthProfileStatus,
|
|
3463
3487
|
user: ProbedUser.nullable()
|
|
3464
3488
|
}).loose();
|
|
@@ -3493,11 +3517,41 @@ const RemoteChanges = z.object({
|
|
|
3493
3517
|
remote_version: z.string().nullable(),
|
|
3494
3518
|
local_version: z.string().nullable()
|
|
3495
3519
|
}).loose();
|
|
3520
|
+
function comparableUrl(url) {
|
|
3521
|
+
const parsed = new URL(url);
|
|
3522
|
+
return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
3523
|
+
}
|
|
3524
|
+
function sameMetabaseUrl(a, b) {
|
|
3525
|
+
return comparableUrl(a) === comparableUrl(b);
|
|
3526
|
+
}
|
|
3527
|
+
function isLoggedInAt(status, url) {
|
|
3528
|
+
return status !== null && status.present && status.url !== null && sameMetabaseUrl(status.url, url);
|
|
3529
|
+
}
|
|
3530
|
+
function isOAuthLoginAt(status, url) {
|
|
3531
|
+
return status !== null && status.method === MbAuthMethod.enum.oauth && isLoggedInAt(status, url);
|
|
3532
|
+
}
|
|
3533
|
+
function isApiKeyLoginAt(status, url) {
|
|
3534
|
+
return status !== null && status.method === MbAuthMethod.enum.apiKey && isLoggedInAt(status, url);
|
|
3535
|
+
}
|
|
3536
|
+
function mbInstanceOf(state, secrets) {
|
|
3537
|
+
const configured = configuredRuntime(state);
|
|
3538
|
+
if (configured === null || secrets === null || secrets.apiKey === null) return null;
|
|
3539
|
+
return {
|
|
3540
|
+
url: configured.url,
|
|
3541
|
+
apiKey: secrets.apiKey
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
function instanceEnv(instance) {
|
|
3545
|
+
return { addedEnv: {
|
|
3546
|
+
[ENV_MB_URL]: instance.url,
|
|
3547
|
+
[ENV_MB_API_KEY]: instance.apiKey
|
|
3548
|
+
} };
|
|
3549
|
+
}
|
|
3496
3550
|
function mbTool(runner) {
|
|
3497
|
-
async function mbJson(args, schema,
|
|
3551
|
+
async function mbJson(args, schema, options = {}) {
|
|
3498
3552
|
const argv = [...args, "--json"];
|
|
3499
|
-
const result = await runner.run(MB, argv,
|
|
3500
|
-
if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
|
|
3553
|
+
const result = await runner.run(MB, argv, options);
|
|
3554
|
+
if (result.exitCode !== 0) throw toolFailure(MB, argv, result, options);
|
|
3501
3555
|
return parseJsonOutput(MB, argv, result.stdout, schema);
|
|
3502
3556
|
}
|
|
3503
3557
|
async function version(command = MB) {
|
|
@@ -3520,7 +3574,7 @@ function mbTool(runner) {
|
|
|
3520
3574
|
options.url,
|
|
3521
3575
|
"--profile",
|
|
3522
3576
|
options.profile
|
|
3523
|
-
], LoginResult, `${options.apiKey}\n`);
|
|
3577
|
+
], LoginResult, { stdin: `${options.apiKey}\n` });
|
|
3524
3578
|
}
|
|
3525
3579
|
async function authStatus(profile) {
|
|
3526
3580
|
return mbJson([
|
|
@@ -3530,6 +3584,14 @@ function mbTool(runner) {
|
|
|
3530
3584
|
profile
|
|
3531
3585
|
], AuthStatus);
|
|
3532
3586
|
}
|
|
3587
|
+
async function authStatusIfInstalled(profile) {
|
|
3588
|
+
try {
|
|
3589
|
+
return await authStatus(profile);
|
|
3590
|
+
} catch (error) {
|
|
3591
|
+
if (error instanceof ProcessNotFoundError) return null;
|
|
3592
|
+
throw error;
|
|
3593
|
+
}
|
|
3594
|
+
}
|
|
3533
3595
|
async function authLogout(profile) {
|
|
3534
3596
|
return mbJson([
|
|
3535
3597
|
"auth",
|
|
@@ -3540,52 +3602,46 @@ function mbTool(runner) {
|
|
|
3540
3602
|
], LogoutResult);
|
|
3541
3603
|
}
|
|
3542
3604
|
async function authList() {
|
|
3543
|
-
return (await mbJson([
|
|
3605
|
+
return (await mbJson([
|
|
3606
|
+
"auth",
|
|
3607
|
+
"list",
|
|
3608
|
+
"--fields",
|
|
3609
|
+
"profile,url,status,user",
|
|
3610
|
+
"--max-bytes",
|
|
3611
|
+
"0"
|
|
3612
|
+
], AuthList)).data;
|
|
3544
3613
|
}
|
|
3545
|
-
async function gitSyncStatus(
|
|
3546
|
-
return mbJson([
|
|
3547
|
-
"git-sync",
|
|
3548
|
-
"status",
|
|
3549
|
-
"--profile",
|
|
3550
|
-
profile
|
|
3551
|
-
], GitSyncStatus);
|
|
3614
|
+
async function gitSyncStatus(instance) {
|
|
3615
|
+
return mbJson(["git-sync", "status"], GitSyncStatus, instanceEnv(instance));
|
|
3552
3616
|
}
|
|
3553
|
-
async function gitSyncHasRemoteChanges(
|
|
3554
|
-
return mbJson([
|
|
3555
|
-
"git-sync",
|
|
3556
|
-
"has-remote-changes",
|
|
3557
|
-
"--profile",
|
|
3558
|
-
profile
|
|
3559
|
-
], RemoteChanges);
|
|
3617
|
+
async function gitSyncHasRemoteChanges(instance) {
|
|
3618
|
+
return mbJson(["git-sync", "has-remote-changes"], RemoteChanges, instanceEnv(instance));
|
|
3560
3619
|
}
|
|
3561
|
-
async function gitSyncImport(
|
|
3620
|
+
async function gitSyncImport(instance, options) {
|
|
3562
3621
|
return mbJson([
|
|
3563
3622
|
"git-sync",
|
|
3564
3623
|
"import",
|
|
3565
|
-
"--profile",
|
|
3566
|
-
profile,
|
|
3567
3624
|
"--branch",
|
|
3568
3625
|
options.branch,
|
|
3569
3626
|
"--wait"
|
|
3570
|
-
], GitSyncOutcome);
|
|
3627
|
+
], GitSyncOutcome, instanceEnv(instance));
|
|
3571
3628
|
}
|
|
3572
|
-
async function gitSyncExport(
|
|
3629
|
+
async function gitSyncExport(instance, options) {
|
|
3573
3630
|
return mbJson([
|
|
3574
3631
|
"git-sync",
|
|
3575
3632
|
"export",
|
|
3576
|
-
"--profile",
|
|
3577
|
-
profile,
|
|
3578
3633
|
"--branch",
|
|
3579
3634
|
options.branch,
|
|
3580
3635
|
"--message",
|
|
3581
3636
|
options.message,
|
|
3582
3637
|
"--wait"
|
|
3583
|
-
], GitSyncOutcome);
|
|
3638
|
+
], GitSyncOutcome, instanceEnv(instance));
|
|
3584
3639
|
}
|
|
3585
3640
|
return {
|
|
3586
3641
|
version,
|
|
3587
3642
|
authLogin,
|
|
3588
3643
|
authStatus,
|
|
3644
|
+
authStatusIfInstalled,
|
|
3589
3645
|
authLogout,
|
|
3590
3646
|
authList,
|
|
3591
3647
|
gitSyncStatus,
|
|
@@ -3606,7 +3662,8 @@ const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
|
3606
3662
|
function hostDeps$13(ctx) {
|
|
3607
3663
|
return {
|
|
3608
3664
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3609
|
-
runner: processRunner(ctx.signal)
|
|
3665
|
+
runner: processRunner(ctx.signal),
|
|
3666
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
3610
3667
|
};
|
|
3611
3668
|
}
|
|
3612
3669
|
async function adminCredentials(ctx) {
|
|
@@ -3662,6 +3719,13 @@ async function authenticates(client) {
|
|
|
3662
3719
|
throw error;
|
|
3663
3720
|
}
|
|
3664
3721
|
}
|
|
3722
|
+
async function forgetCompletion(ctx) {
|
|
3723
|
+
const { [STEP_ID$9]: record, ...steps } = ctx.state.steps;
|
|
3724
|
+
if (record !== void 0) await ctx.saveState({
|
|
3725
|
+
...ctx.state,
|
|
3726
|
+
steps
|
|
3727
|
+
});
|
|
3728
|
+
}
|
|
3665
3729
|
async function workingApiKey(ctx, deps, url) {
|
|
3666
3730
|
const stored = ctx.secrets.apiKey;
|
|
3667
3731
|
if (stored !== null) {
|
|
@@ -3671,6 +3735,7 @@ async function workingApiKey(ctx, deps, url) {
|
|
|
3671
3735
|
}))) return stored;
|
|
3672
3736
|
ctx.ui.warn("Metabase rejects the stored API key; replacing it");
|
|
3673
3737
|
}
|
|
3738
|
+
await forgetCompletion(ctx);
|
|
3674
3739
|
const sessionId = await superuserSession(ctx, deps, url);
|
|
3675
3740
|
const apiKey = await freshApiKey(ctx, deps.client(url, {
|
|
3676
3741
|
kind: "session",
|
|
@@ -3682,20 +3747,24 @@ async function workingApiKey(ctx, deps, url) {
|
|
|
3682
3747
|
});
|
|
3683
3748
|
return apiKey;
|
|
3684
3749
|
}
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
throw error;
|
|
3691
|
-
}
|
|
3750
|
+
function elsewhereUrl(status, url, movedFrom) {
|
|
3751
|
+
if (status === null || !status.present || status.url === null) return null;
|
|
3752
|
+
if (sameMetabaseUrl(status.url, url)) return null;
|
|
3753
|
+
if (movedFrom !== null && sameMetabaseUrl(status.url, movedFrom)) return null;
|
|
3754
|
+
return status.url;
|
|
3692
3755
|
}
|
|
3693
3756
|
async function chooseProfile(ctx, mb, url) {
|
|
3694
3757
|
const profile = ctx.state.mbProfile;
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3758
|
+
const status = await mb.authStatusIfInstalled(profile);
|
|
3759
|
+
if (profile !== "default") return {
|
|
3760
|
+
profile,
|
|
3761
|
+
status
|
|
3762
|
+
};
|
|
3763
|
+
const previous = elsewhereUrl(status, url, ctx.state.movedFrom);
|
|
3764
|
+
if (previous === null) return {
|
|
3765
|
+
profile,
|
|
3766
|
+
status
|
|
3767
|
+
};
|
|
3699
3768
|
const named = namedMbProfile(ctx.state.name);
|
|
3700
3769
|
if (await ctx.prompts.select({
|
|
3701
3770
|
key: API_KEY_KEYS.profile,
|
|
@@ -3710,13 +3779,19 @@ async function chooseProfile(ctx, mb, url) {
|
|
|
3710
3779
|
recommended: "default"
|
|
3711
3780
|
}) === "default") {
|
|
3712
3781
|
ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
|
|
3713
|
-
return
|
|
3782
|
+
return {
|
|
3783
|
+
profile,
|
|
3784
|
+
status
|
|
3785
|
+
};
|
|
3714
3786
|
}
|
|
3715
3787
|
await ctx.saveState({
|
|
3716
3788
|
...ctx.state,
|
|
3717
3789
|
mbProfile: named
|
|
3718
3790
|
});
|
|
3719
|
-
return
|
|
3791
|
+
return {
|
|
3792
|
+
profile: named,
|
|
3793
|
+
status: await mb.authStatusIfInstalled(named)
|
|
3794
|
+
};
|
|
3720
3795
|
}
|
|
3721
3796
|
function usageNote(profile) {
|
|
3722
3797
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
@@ -3728,31 +3803,34 @@ function apiKeyStep(deps = hostDeps$13) {
|
|
|
3728
3803
|
title: "Connect the Metabase CLI",
|
|
3729
3804
|
redoHint: "`rde status` shows the mb profile it uses.",
|
|
3730
3805
|
async detect(ctx) {
|
|
3731
|
-
|
|
3732
|
-
const
|
|
3733
|
-
|
|
3806
|
+
const configured = configuredRuntime(ctx.state);
|
|
3807
|
+
const apiKey = ctx.secrets.apiKey;
|
|
3808
|
+
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$9]?.status !== "done") return "todo";
|
|
3809
|
+
const resolved = deps(ctx);
|
|
3810
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
3811
|
+
if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
|
|
3734
3812
|
try {
|
|
3735
|
-
|
|
3813
|
+
return await authenticates(resolved.client(configured.url, {
|
|
3814
|
+
kind: "api-key",
|
|
3815
|
+
key: apiKey
|
|
3816
|
+
})) ? "done" : "todo";
|
|
3736
3817
|
} catch (error) {
|
|
3737
|
-
if (error instanceof
|
|
3818
|
+
if (isMetabaseNotServing(error) || error instanceof MetabaseApiError) return "todo";
|
|
3738
3819
|
throw error;
|
|
3739
3820
|
}
|
|
3740
|
-
return (await mb.authList()).find((candidate) => candidate.profile === profile)?.status === "ok" ? "done" : "todo";
|
|
3741
3821
|
},
|
|
3742
3822
|
async run(ctx) {
|
|
3743
3823
|
const resolved = deps(ctx);
|
|
3744
3824
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
3745
3825
|
const apiKey = await workingApiKey(ctx, resolved, configured.url);
|
|
3746
3826
|
const mb = mbTool(resolved.runner);
|
|
3747
|
-
const profile = await chooseProfile(ctx, mb, configured.url);
|
|
3748
|
-
|
|
3827
|
+
const { profile, status } = await chooseProfile(ctx, mb, configured.url);
|
|
3828
|
+
if (isOAuthLoginAt(status, configured.url)) ctx.ui.info(`mb profile ${profile} is logged in to this instance with OAuth; leaving it as it is (\`mb auth logout --profile ${profile}\` first to use rde's API key instead)`);
|
|
3829
|
+
else await mb.authLogin({
|
|
3749
3830
|
url: configured.url,
|
|
3750
3831
|
profile,
|
|
3751
3832
|
apiKey
|
|
3752
3833
|
});
|
|
3753
|
-
const entry = (await mb.authList()).find((candidate) => candidate.profile === profile);
|
|
3754
|
-
if (entry === void 0) throw new ConfigError(`\`mb auth list\` does not show profile ${profile} after login`);
|
|
3755
|
-
if (entry.status !== "ok") throw new ConfigError(`mb profile ${profile} reports ${entry.status} after login`);
|
|
3756
3834
|
if (ctx.state.movedFrom !== null) await ctx.saveState({
|
|
3757
3835
|
...ctx.state,
|
|
3758
3836
|
movedFrom: null
|
|
@@ -5999,7 +6077,7 @@ async function applySettings(run, configured) {
|
|
|
5999
6077
|
async function remoteHasContent(run, gitRemote) {
|
|
6000
6078
|
switch (gitRemote.kind) {
|
|
6001
6079
|
case "local-bare": return run.git.hasMetabaseContent(gitRemote.workdir);
|
|
6002
|
-
case "hosted": return (await mbTool(run.deps.runner).gitSyncHasRemoteChanges(run.
|
|
6080
|
+
case "hosted": return (await mbTool(run.deps.runner).gitSyncHasRemoteChanges(run.instance)).has_changes;
|
|
6003
6081
|
}
|
|
6004
6082
|
}
|
|
6005
6083
|
function reportImport(run, outcome) {
|
|
@@ -6021,7 +6099,7 @@ async function importExistingContent(run, gitRemote) {
|
|
|
6021
6099
|
const spinner = run.ctx.ui.spinner("Importing the repository's content into Metabase…");
|
|
6022
6100
|
let outcome;
|
|
6023
6101
|
try {
|
|
6024
|
-
outcome = await mbTool(run.deps.runner).gitSyncImport(run.
|
|
6102
|
+
outcome = await mbTool(run.deps.runner).gitSyncImport(run.instance, { branch: gitRemote.branch });
|
|
6025
6103
|
} catch (error) {
|
|
6026
6104
|
spinner.fail("Import failed");
|
|
6027
6105
|
throw error;
|
|
@@ -6065,15 +6143,17 @@ function isMbUnavailable(error) {
|
|
|
6065
6143
|
}
|
|
6066
6144
|
async function runGitSync(ctx, deps) {
|
|
6067
6145
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6068
|
-
|
|
6146
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6147
|
+
if (instance === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
6069
6148
|
const run = {
|
|
6070
6149
|
ctx,
|
|
6071
6150
|
deps,
|
|
6072
6151
|
git: gitTool(deps.runner),
|
|
6073
|
-
client: deps.client(
|
|
6152
|
+
client: deps.client(instance.url, {
|
|
6074
6153
|
kind: "api-key",
|
|
6075
|
-
key:
|
|
6076
|
-
})
|
|
6154
|
+
key: instance.apiKey
|
|
6155
|
+
}),
|
|
6156
|
+
instance
|
|
6077
6157
|
};
|
|
6078
6158
|
const flow = await runFlow({
|
|
6079
6159
|
remote: remoteScreen(run),
|
|
@@ -6103,8 +6183,10 @@ function gitSyncStep(deps = hostDeps$12) {
|
|
|
6103
6183
|
if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
|
|
6104
6184
|
if (ctx.state.steps[STEP_ID$8]?.status === "skipped") return "not-applicable";
|
|
6105
6185
|
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
|
|
6186
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6187
|
+
if (instance === null) return "todo";
|
|
6106
6188
|
try {
|
|
6107
|
-
return (await mbTool(deps(ctx).runner).gitSyncStatus(
|
|
6189
|
+
return (await mbTool(deps(ctx).runner).gitSyncStatus(instance)).branch === null ? "todo" : "done";
|
|
6108
6190
|
} catch (error) {
|
|
6109
6191
|
if (isMbUnavailable(error)) return "todo";
|
|
6110
6192
|
throw error;
|
|
@@ -6116,17 +6198,45 @@ function gitSyncStep(deps = hostDeps$12) {
|
|
|
6116
6198
|
};
|
|
6117
6199
|
}
|
|
6118
6200
|
//#endregion
|
|
6201
|
+
//#region src/metabase/domain/github-content.ts
|
|
6202
|
+
const GitHubContentEntry = z.object({
|
|
6203
|
+
name: z.string(),
|
|
6204
|
+
type: z.string(),
|
|
6205
|
+
sha: z.string()
|
|
6206
|
+
}).loose();
|
|
6207
|
+
const GitHubDirectory = z.array(GitHubContentEntry);
|
|
6208
|
+
//#endregion
|
|
6209
|
+
//#region src/metabase/github.ts
|
|
6210
|
+
const GITHUB_API = "https://api.github.com";
|
|
6211
|
+
const DIRECTORY_TYPE = "dir";
|
|
6212
|
+
function githubContents(signal, options = {}) {
|
|
6213
|
+
const http = createHttp({
|
|
6214
|
+
baseUrl: GITHUB_API,
|
|
6215
|
+
auth: NO_AUTH,
|
|
6216
|
+
signal,
|
|
6217
|
+
...options
|
|
6218
|
+
});
|
|
6219
|
+
return { async subdirectoryHashes(repository, ref, directory) {
|
|
6220
|
+
const path = `/repos/${repository}/contents/${directory}${ref === null ? "" : `?ref=${encodeURIComponent(ref)}`}`;
|
|
6221
|
+
const entries = await http.requestJson("GET", path, { schema: GitHubDirectory });
|
|
6222
|
+
return new Map(entries.filter((entry) => entry.type === DIRECTORY_TYPE).map((entry) => [entry.name, entry.sha]));
|
|
6223
|
+
} };
|
|
6224
|
+
}
|
|
6225
|
+
//#endregion
|
|
6119
6226
|
//#region src/tools/skills-cli.ts
|
|
6120
6227
|
const NPX = "npx";
|
|
6121
6228
|
const SKILLS_PACKAGE = "skills@latest";
|
|
6122
6229
|
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
|
|
6123
6230
|
const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
|
|
6124
|
-
const
|
|
6231
|
+
const DATA_APP_REPOSITORY = "metabase/metabase";
|
|
6232
|
+
const DATA_APP_BRANCH = "master";
|
|
6233
|
+
const DATA_APP_SKILLS_SOURCE = `${DATA_APP_REPOSITORY}/skills#${DATA_APP_BRANCH}`;
|
|
6125
6234
|
const DATA_APP_SKILL_NAMES = [
|
|
6126
6235
|
"metabase-data-app-setup",
|
|
6127
6236
|
"metabase-data-app-routing",
|
|
6128
6237
|
"metabase-data-app-actions",
|
|
6129
|
-
"metabase-data-app-semantic-layer"
|
|
6238
|
+
"metabase-data-app-semantic-layer",
|
|
6239
|
+
"metabase-data-app-migrate"
|
|
6130
6240
|
];
|
|
6131
6241
|
function rdeSkillSource() {
|
|
6132
6242
|
const override = readEnv(ENV_RDE_SKILL_SOURCE);
|
|
@@ -6140,7 +6250,13 @@ const InstalledSkill = z.object({
|
|
|
6140
6250
|
source: z.string().nullable()
|
|
6141
6251
|
}).loose();
|
|
6142
6252
|
const InstalledSkillList = z.array(InstalledSkill);
|
|
6143
|
-
const
|
|
6253
|
+
const LockedSkill = z.object({
|
|
6254
|
+
source: z.string().nullish(),
|
|
6255
|
+
ref: z.string().nullish(),
|
|
6256
|
+
skillPath: z.string().nullish(),
|
|
6257
|
+
skillFolderHash: z.string().nullish()
|
|
6258
|
+
}).loose();
|
|
6259
|
+
const SkillLock = z.object({ skills: z.record(z.string(), LockedSkill) }).loose();
|
|
6144
6260
|
const GLOBAL_LOCK_FILE = ".skill-lock.json";
|
|
6145
6261
|
const PROJECT_LOCK_FILE = "skills-lock.json";
|
|
6146
6262
|
function globalSkillLockPath(home, stateHome) {
|
|
@@ -6153,16 +6269,21 @@ function xdgStateHome() {
|
|
|
6153
6269
|
const value = readEnv(ENV_XDG_STATE_HOME);
|
|
6154
6270
|
return value === void 0 || value.length === 0 ? null : value;
|
|
6155
6271
|
}
|
|
6156
|
-
function
|
|
6272
|
+
function lockedSkill(path, raw, skill) {
|
|
6157
6273
|
if (raw === null) return null;
|
|
6158
6274
|
const parsed = SkillLock.safeParse(raw);
|
|
6159
6275
|
if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
|
|
6160
|
-
return parsed.data.skills[skill]
|
|
6276
|
+
return parsed.data.skills[skill] ?? null;
|
|
6161
6277
|
}
|
|
6162
6278
|
const hostSkillFiles = {
|
|
6163
6279
|
readJson: readJsonFile,
|
|
6164
6280
|
readText: readTextFile
|
|
6165
6281
|
};
|
|
6282
|
+
const DATA_APP_SKILLS_ORIGIN = {
|
|
6283
|
+
kind: "github",
|
|
6284
|
+
repository: DATA_APP_REPOSITORY,
|
|
6285
|
+
ref: DATA_APP_BRANCH
|
|
6286
|
+
};
|
|
6166
6287
|
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
6167
6288
|
function skillOrigin(source, cwd) {
|
|
6168
6289
|
const github = GITHUB_URL.exec(source);
|
|
@@ -6252,7 +6373,11 @@ function hostDeps$11(ctx) {
|
|
|
6252
6373
|
return {
|
|
6253
6374
|
runner: processRunner(ctx.signal),
|
|
6254
6375
|
probe: hostAgentProbe(),
|
|
6255
|
-
skillSource: rdeSkillSource()
|
|
6376
|
+
skillSource: rdeSkillSource(),
|
|
6377
|
+
stateHome: xdgStateHome(),
|
|
6378
|
+
files: hostSkillFiles,
|
|
6379
|
+
github: githubContents(ctx.signal),
|
|
6380
|
+
cwd: process.cwd()
|
|
6256
6381
|
};
|
|
6257
6382
|
}
|
|
6258
6383
|
function detectedOnly(agents) {
|
|
@@ -6287,10 +6412,91 @@ function coveredAgentNames(entry, agents) {
|
|
|
6287
6412
|
function installedLines(entries, agents) {
|
|
6288
6413
|
return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
|
|
6289
6414
|
}
|
|
6415
|
+
function skillSets(deps) {
|
|
6416
|
+
return [{
|
|
6417
|
+
label: "data-app",
|
|
6418
|
+
source: DATA_APP_SKILLS_SOURCE,
|
|
6419
|
+
sourceNote: "",
|
|
6420
|
+
origin: DATA_APP_SKILLS_ORIGIN,
|
|
6421
|
+
skills: DATA_APP_SKILL_NAMES
|
|
6422
|
+
}, {
|
|
6423
|
+
label: "rde",
|
|
6424
|
+
source: deps.skillSource,
|
|
6425
|
+
sourceNote: deps.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${deps.skillSource}`,
|
|
6426
|
+
origin: skillOrigin(deps.skillSource, deps.cwd),
|
|
6427
|
+
skills: RDE_SOURCE_SKILL_NAMES
|
|
6428
|
+
}];
|
|
6429
|
+
}
|
|
6430
|
+
function hashLookup(github) {
|
|
6431
|
+
const asked = /* @__PURE__ */ new Map();
|
|
6432
|
+
return (repository, ref, directory) => {
|
|
6433
|
+
const key = JSON.stringify([
|
|
6434
|
+
repository,
|
|
6435
|
+
ref,
|
|
6436
|
+
directory
|
|
6437
|
+
]);
|
|
6438
|
+
let hashes = asked.get(key);
|
|
6439
|
+
if (hashes === void 0) {
|
|
6440
|
+
hashes = github.subdirectoryHashes(repository, ref, directory).catch((error) => {
|
|
6441
|
+
if (lookupFailure(error) === null) throw error;
|
|
6442
|
+
return null;
|
|
6443
|
+
});
|
|
6444
|
+
asked.set(key, hashes);
|
|
6445
|
+
}
|
|
6446
|
+
return hashes;
|
|
6447
|
+
};
|
|
6448
|
+
}
|
|
6449
|
+
async function readLockedSkills(deps) {
|
|
6450
|
+
const lockPath = globalSkillLockPath(deps.probe.home, deps.stateHome);
|
|
6451
|
+
try {
|
|
6452
|
+
const lock = await deps.files.readJson(lockPath);
|
|
6453
|
+
return new Map(REQUIRED_SKILL_NAMES.map((name) => [name, lockedSkill(lockPath, lock, name)]));
|
|
6454
|
+
} catch (error) {
|
|
6455
|
+
if (error instanceof ConfigError) return null;
|
|
6456
|
+
throw error;
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
function lockedCopy(locked, origin) {
|
|
6460
|
+
if (locked === null || locked.source !== origin.repository) return null;
|
|
6461
|
+
const ref = locked.ref ?? null;
|
|
6462
|
+
if (origin.ref !== null && ref !== origin.ref) return null;
|
|
6463
|
+
if (locked.skillPath === null || locked.skillPath === void 0) return null;
|
|
6464
|
+
if (locked.skillFolderHash === null || locked.skillFolderHash === void 0) return null;
|
|
6465
|
+
const folder = posix.dirname(locked.skillPath);
|
|
6466
|
+
return {
|
|
6467
|
+
ref,
|
|
6468
|
+
directory: posix.dirname(folder),
|
|
6469
|
+
folder: posix.basename(folder),
|
|
6470
|
+
hash: locked.skillFolderHash
|
|
6471
|
+
};
|
|
6472
|
+
}
|
|
6473
|
+
async function setBehind(set, check) {
|
|
6474
|
+
const { origin } = set;
|
|
6475
|
+
if (origin.kind !== "github" || check.locked === null) return true;
|
|
6476
|
+
const copies = [];
|
|
6477
|
+
for (const skill of set.skills) {
|
|
6478
|
+
const copy = lockedCopy(check.locked.get(skill) ?? null, origin);
|
|
6479
|
+
if (copy === null) return true;
|
|
6480
|
+
copies.push(copy);
|
|
6481
|
+
}
|
|
6482
|
+
if (check.updates === "installed") return false;
|
|
6483
|
+
for (const copy of copies) {
|
|
6484
|
+
const current = await check.hashes(origin.repository, copy.ref, copy.directory);
|
|
6485
|
+
if (current !== null && current.get(copy.folder) !== copy.hash) return true;
|
|
6486
|
+
}
|
|
6487
|
+
return false;
|
|
6488
|
+
}
|
|
6489
|
+
function hasSkill(installed, skill, agent) {
|
|
6490
|
+
return installed.some((entry) => entry.name === skill && skillCoversAgent(entry, agent));
|
|
6491
|
+
}
|
|
6492
|
+
function coversAll(installed, skills, agents) {
|
|
6493
|
+
return agents.every((agent) => skills.every((skill) => hasSkill(installed, skill, agent)));
|
|
6494
|
+
}
|
|
6290
6495
|
const SOURCE_NOTES = {
|
|
6291
6496
|
override: "from --agents",
|
|
6292
6497
|
detected: "detected on this machine",
|
|
6293
|
-
chosen: "as chosen"
|
|
6498
|
+
chosen: "as chosen",
|
|
6499
|
+
installed: "updating the installed copies"
|
|
6294
6500
|
};
|
|
6295
6501
|
function agentChoice(agent) {
|
|
6296
6502
|
return {
|
|
@@ -6308,7 +6514,7 @@ async function askAgents(ctx, detected) {
|
|
|
6308
6514
|
});
|
|
6309
6515
|
return detected.filter((agent) => picked.includes(agent.id));
|
|
6310
6516
|
}
|
|
6311
|
-
async function selectAgents(ctx, all, options) {
|
|
6517
|
+
async function selectAgents(ctx, all, options, installedNow) {
|
|
6312
6518
|
switch (options.kind) {
|
|
6313
6519
|
case "detected": return {
|
|
6314
6520
|
agents: detectedOnly(all),
|
|
@@ -6327,6 +6533,10 @@ async function selectAgents(ctx, all, options) {
|
|
|
6327
6533
|
agents: [],
|
|
6328
6534
|
source: "detected"
|
|
6329
6535
|
};
|
|
6536
|
+
if (coversAll(await installedNow(), REQUIRED_SKILL_NAMES, detected)) return {
|
|
6537
|
+
agents: detected,
|
|
6538
|
+
source: "installed"
|
|
6539
|
+
};
|
|
6330
6540
|
return {
|
|
6331
6541
|
agents: await askAgents(ctx, detected),
|
|
6332
6542
|
source: "chosen"
|
|
@@ -6337,11 +6547,21 @@ async function selectAgents(ctx, all, options) {
|
|
|
6337
6547
|
function skipLine(all, source) {
|
|
6338
6548
|
switch (source) {
|
|
6339
6549
|
case "override":
|
|
6340
|
-
case "chosen":
|
|
6550
|
+
case "chosen":
|
|
6551
|
+
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
|
|
6341
6552
|
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
|
|
6342
6553
|
}
|
|
6343
6554
|
}
|
|
6344
|
-
function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
6555
|
+
function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
6556
|
+
let hashes = null;
|
|
6557
|
+
async function setCheck(resolved) {
|
|
6558
|
+
hashes ??= hashLookup(resolved.github);
|
|
6559
|
+
return {
|
|
6560
|
+
updates,
|
|
6561
|
+
locked: await readLockedSkills(resolved),
|
|
6562
|
+
hashes
|
|
6563
|
+
};
|
|
6564
|
+
}
|
|
6345
6565
|
return {
|
|
6346
6566
|
id: STEP_ID$7,
|
|
6347
6567
|
title: "Install the rde and data-app skills for your coding agents",
|
|
@@ -6350,34 +6570,42 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
|
6350
6570
|
const resolved = deps(ctx);
|
|
6351
6571
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6352
6572
|
if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6353
|
-
|
|
6573
|
+
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6574
|
+
const check = await setCheck(resolved);
|
|
6575
|
+
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
6576
|
+
return "done";
|
|
6354
6577
|
},
|
|
6355
6578
|
async run(ctx) {
|
|
6356
6579
|
const resolved = deps(ctx);
|
|
6580
|
+
const skills = skillsCli(resolved.runner);
|
|
6357
6581
|
const all = await detectAgents(resolved.probe);
|
|
6358
|
-
|
|
6582
|
+
let listing = null;
|
|
6583
|
+
const installedNow = () => listing ??= skills.list();
|
|
6584
|
+
const selection = await selectAgents(ctx, all, options, installedNow);
|
|
6359
6585
|
if (selection.agents.length === 0) {
|
|
6360
6586
|
ctx.ui.info(skipLine(all, selection.source));
|
|
6361
6587
|
return { outcome: "skipped" };
|
|
6362
6588
|
}
|
|
6363
|
-
const
|
|
6589
|
+
const before = await installedNow();
|
|
6590
|
+
const check = await setCheck(resolved);
|
|
6591
|
+
const pending = [];
|
|
6592
|
+
for (const set of skillSets(resolved)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
|
|
6593
|
+
const names = displayNames(selection.agents);
|
|
6594
|
+
if (pending.length === 0) {
|
|
6595
|
+
ctx.ui.success(`The rde and data-app skills are current for ${names}`);
|
|
6596
|
+
return { outcome: "completed" };
|
|
6597
|
+
}
|
|
6364
6598
|
const agents = selection.agents.map((agent) => agent.skillsCliName);
|
|
6365
|
-
const forWhom = `for ${
|
|
6366
|
-
|
|
6367
|
-
|
|
6368
|
-
|
|
6369
|
-
|
|
6370
|
-
|
|
6371
|
-
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
await skills.add({
|
|
6376
|
-
source: resolved.skillSource,
|
|
6377
|
-
skills: RDE_SOURCE_SKILL_NAMES,
|
|
6378
|
-
agents,
|
|
6379
|
-
output: ctx.ui.childOutput
|
|
6380
|
-
});
|
|
6599
|
+
const forWhom = `for ${names} (${SOURCE_NOTES[selection.source]})`;
|
|
6600
|
+
for (const set of pending) {
|
|
6601
|
+
ctx.ui.info(`Installing the ${set.label} skills ${forWhom}${set.sourceNote}`);
|
|
6602
|
+
await skills.add({
|
|
6603
|
+
source: set.source,
|
|
6604
|
+
skills: set.skills,
|
|
6605
|
+
agents,
|
|
6606
|
+
output: ctx.ui.childOutput
|
|
6607
|
+
});
|
|
6608
|
+
}
|
|
6381
6609
|
const installed = await skills.list();
|
|
6382
6610
|
const missing = unlistedSkills(installed);
|
|
6383
6611
|
if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
|
|
@@ -6398,9 +6626,6 @@ function hostDatabasesDeps(signal) {
|
|
|
6398
6626
|
now: () => /* @__PURE__ */ new Date()
|
|
6399
6627
|
};
|
|
6400
6628
|
}
|
|
6401
|
-
function isNotServingYet$2(error) {
|
|
6402
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
6403
|
-
}
|
|
6404
6629
|
function requireApiKey$2(state, apiKey) {
|
|
6405
6630
|
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
6406
6631
|
return apiKey;
|
|
@@ -6541,7 +6766,7 @@ function databasesStep(options, deps = hostDeps$10) {
|
|
|
6541
6766
|
try {
|
|
6542
6767
|
return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
|
|
6543
6768
|
} catch (error) {
|
|
6544
|
-
if (
|
|
6769
|
+
if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
|
|
6545
6770
|
throw error;
|
|
6546
6771
|
}
|
|
6547
6772
|
},
|
|
@@ -6555,6 +6780,24 @@ function databasesStep(options, deps = hostDeps$10) {
|
|
|
6555
6780
|
};
|
|
6556
6781
|
}
|
|
6557
6782
|
//#endregion
|
|
6783
|
+
//#region src/metabase/domain/npm-package.ts
|
|
6784
|
+
const NpmVersionManifest = z.object({ version: z.string() }).loose();
|
|
6785
|
+
//#endregion
|
|
6786
|
+
//#region src/metabase/npm-registry.ts
|
|
6787
|
+
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
6788
|
+
function npmRegistry(signal, options = {}) {
|
|
6789
|
+
const http = createHttp({
|
|
6790
|
+
baseUrl: NPM_REGISTRY,
|
|
6791
|
+
auth: NO_AUTH,
|
|
6792
|
+
signal,
|
|
6793
|
+
...options
|
|
6794
|
+
});
|
|
6795
|
+
return { async distTagVersion(packageName, tag) {
|
|
6796
|
+
const path = `/${encodeURIComponent(packageName)}/${encodeURIComponent(tag)}`;
|
|
6797
|
+
return (await http.requestJson("GET", path, { schema: NpmVersionManifest })).version;
|
|
6798
|
+
} };
|
|
6799
|
+
}
|
|
6800
|
+
//#endregion
|
|
6558
6801
|
//#region src/steps/install-mb.ts
|
|
6559
6802
|
const STEP_ID$5 = "install-mb";
|
|
6560
6803
|
const MIN_MB_VERSION = "0.3.0";
|
|
@@ -6569,7 +6812,8 @@ function hostDeps$9(ctx) {
|
|
|
6569
6812
|
return {
|
|
6570
6813
|
runner: processRunner(ctx.signal),
|
|
6571
6814
|
installedWith: rdeInstalledWith(),
|
|
6572
|
-
locate: locateMbOnPath
|
|
6815
|
+
locate: locateMbOnPath,
|
|
6816
|
+
publishedVersion: () => npmRegistry(ctx.signal).distTagVersion(MB_PACKAGE, MB_DIST_TAG)
|
|
6573
6817
|
};
|
|
6574
6818
|
}
|
|
6575
6819
|
async function inspectMb(runner) {
|
|
@@ -6583,6 +6827,38 @@ async function inspectMb(runner) {
|
|
|
6583
6827
|
satisfies: semver.satisfies(version, MB_RANGE, { includePrerelease: true })
|
|
6584
6828
|
};
|
|
6585
6829
|
}
|
|
6830
|
+
const UNCHECKED = { kind: "unchecked" };
|
|
6831
|
+
async function publishedMb(deps) {
|
|
6832
|
+
let raw;
|
|
6833
|
+
try {
|
|
6834
|
+
raw = await deps.publishedVersion();
|
|
6835
|
+
} catch (error) {
|
|
6836
|
+
const reason = lookupFailure(error);
|
|
6837
|
+
if (reason === null) throw error;
|
|
6838
|
+
return {
|
|
6839
|
+
kind: "unknown",
|
|
6840
|
+
reason
|
|
6841
|
+
};
|
|
6842
|
+
}
|
|
6843
|
+
const version = semver.valid(raw);
|
|
6844
|
+
if (version === null) return {
|
|
6845
|
+
kind: "unknown",
|
|
6846
|
+
reason: `the registry names "${raw}" for ${MB_DIST_TAG}, which is not a version`
|
|
6847
|
+
};
|
|
6848
|
+
return {
|
|
6849
|
+
kind: "published",
|
|
6850
|
+
version
|
|
6851
|
+
};
|
|
6852
|
+
}
|
|
6853
|
+
function coreOf(version) {
|
|
6854
|
+
return `${semver.major(version)}.${semver.minor(version)}.${semver.patch(version)}`;
|
|
6855
|
+
}
|
|
6856
|
+
function updateTo(release, installed) {
|
|
6857
|
+
if (release.kind !== "published" || release.version === installed) return null;
|
|
6858
|
+
const order = semver.compare(coreOf(installed), coreOf(release.version));
|
|
6859
|
+
if (order === 0) return semver.prerelease(installed) === null ? null : release.version;
|
|
6860
|
+
return order < 0 ? release.version : null;
|
|
6861
|
+
}
|
|
6586
6862
|
function displayCommand(install) {
|
|
6587
6863
|
return [install.command, ...install.args].join(" ");
|
|
6588
6864
|
}
|
|
@@ -6669,6 +6945,7 @@ async function stillWrong(install, after, deps) {
|
|
|
6669
6945
|
const fail = (stderrTail) => new ExternalToolError({
|
|
6670
6946
|
tool: install.command,
|
|
6671
6947
|
argv: install.args,
|
|
6948
|
+
envNames: [],
|
|
6672
6949
|
exitCode: 0,
|
|
6673
6950
|
stderrTail
|
|
6674
6951
|
});
|
|
@@ -6681,27 +6958,66 @@ async function stillWrong(install, after, deps) {
|
|
|
6681
6958
|
`${stillWrongAdvice(diagnosis)}.`
|
|
6682
6959
|
].join(" "));
|
|
6683
6960
|
}
|
|
6684
|
-
function
|
|
6961
|
+
async function shadowedWarning(lead, deps) {
|
|
6962
|
+
const found = await inspectMbOnPath(deps.runner, deps.locate);
|
|
6963
|
+
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
6964
|
+
const advice = diagnosis.kind === "shadowed" ? ` ${shadowedHint(diagnosis)}.` : "";
|
|
6965
|
+
return `${lead} Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.${advice}`;
|
|
6966
|
+
}
|
|
6967
|
+
async function updateFor(release, installed, deps) {
|
|
6968
|
+
const version = updateTo(release, installed);
|
|
6969
|
+
if (version === null) return { kind: "current" };
|
|
6970
|
+
return (await inspectMbOnPath(deps.runner, deps.locate)).some((entry) => entry.version === version) ? {
|
|
6971
|
+
kind: "shadowed",
|
|
6972
|
+
version
|
|
6973
|
+
} : {
|
|
6974
|
+
kind: "update",
|
|
6975
|
+
version
|
|
6976
|
+
};
|
|
6977
|
+
}
|
|
6978
|
+
function installMbStep(updates, deps = hostDeps$9) {
|
|
6979
|
+
let published = null;
|
|
6980
|
+
function release(resolved) {
|
|
6981
|
+
if (updates === "installed") return Promise.resolve(UNCHECKED);
|
|
6982
|
+
published ??= publishedMb(resolved);
|
|
6983
|
+
return published;
|
|
6984
|
+
}
|
|
6685
6985
|
return {
|
|
6686
6986
|
id: STEP_ID$5,
|
|
6687
6987
|
title: "Install the Metabase CLI",
|
|
6688
6988
|
async detect(ctx) {
|
|
6689
|
-
const
|
|
6690
|
-
|
|
6989
|
+
const resolved = deps(ctx);
|
|
6990
|
+
const found = await inspectMb(resolved.runner);
|
|
6991
|
+
if (!found.installed || !found.satisfies) return "todo";
|
|
6992
|
+
return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
|
|
6691
6993
|
},
|
|
6692
6994
|
async run(ctx) {
|
|
6693
6995
|
const resolved = deps(ctx);
|
|
6694
6996
|
const before = await inspectMb(resolved.runner);
|
|
6997
|
+
const target = await release(resolved);
|
|
6695
6998
|
if (before.installed && before.satisfies) {
|
|
6696
|
-
ctx.ui.
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6999
|
+
if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
|
|
7000
|
+
const update = await updateFor(target, before.version, resolved);
|
|
7001
|
+
if (update.kind === "shadowed") {
|
|
7002
|
+
const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
|
|
7003
|
+
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
7004
|
+
}
|
|
7005
|
+
if (update.kind !== "update") {
|
|
7006
|
+
ctx.ui.success(`mb ${before.version} is installed`);
|
|
7007
|
+
return { outcome: "completed" };
|
|
7008
|
+
}
|
|
7009
|
+
ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
|
|
7010
|
+
} else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
6700
7011
|
const install = mbInstallCommand(resolved.installedWith);
|
|
6701
7012
|
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
6702
7013
|
await runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
|
|
6703
7014
|
const after = await inspectMb(resolved.runner);
|
|
6704
7015
|
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
7016
|
+
const stillNewer = updateTo(target, after.version);
|
|
7017
|
+
if (stillNewer !== null) {
|
|
7018
|
+
const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
|
|
7019
|
+
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
7020
|
+
}
|
|
6705
7021
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
6706
7022
|
return { outcome: "completed" };
|
|
6707
7023
|
}
|
|
@@ -6722,7 +7038,7 @@ function hostDeps$8(ctx) {
|
|
|
6722
7038
|
}
|
|
6723
7039
|
function isNoTokenYet(error) {
|
|
6724
7040
|
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$2;
|
|
6725
|
-
return error
|
|
7041
|
+
return isMetabaseNotServing(error);
|
|
6726
7042
|
}
|
|
6727
7043
|
function validateToken(value) {
|
|
6728
7044
|
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
@@ -6798,7 +7114,8 @@ function licenseStep(deps = hostDeps$8) {
|
|
|
6798
7114
|
validThru: status["valid-thru"] ?? null
|
|
6799
7115
|
}
|
|
6800
7116
|
});
|
|
6801
|
-
|
|
7117
|
+
const mb = mbTool(resolved.runner);
|
|
7118
|
+
if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
|
|
6802
7119
|
url: configured.url,
|
|
6803
7120
|
profile: ctx.state.mbProfile,
|
|
6804
7121
|
apiKey
|
|
@@ -7157,9 +7474,6 @@ function hostDeps$5(ctx) {
|
|
|
7157
7474
|
now: () => /* @__PURE__ */ new Date()
|
|
7158
7475
|
};
|
|
7159
7476
|
}
|
|
7160
|
-
function isNotServingYet$1(error) {
|
|
7161
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7162
|
-
}
|
|
7163
7477
|
function isNotFound$2(error) {
|
|
7164
7478
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1;
|
|
7165
7479
|
}
|
|
@@ -7320,7 +7634,7 @@ function sampleDatabaseStep(deps = hostDeps$5) {
|
|
|
7320
7634
|
try {
|
|
7321
7635
|
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
7322
7636
|
} catch (error) {
|
|
7323
|
-
if (
|
|
7637
|
+
if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
|
|
7324
7638
|
throw error;
|
|
7325
7639
|
}
|
|
7326
7640
|
},
|
|
@@ -7357,7 +7671,7 @@ function hostDeps$4(ctx) {
|
|
|
7357
7671
|
};
|
|
7358
7672
|
}
|
|
7359
7673
|
function isNotHealthy(error) {
|
|
7360
|
-
return error
|
|
7674
|
+
return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
|
|
7361
7675
|
}
|
|
7362
7676
|
async function runningVersion(client) {
|
|
7363
7677
|
return (await client.session.properties()).version.tag;
|
|
@@ -7462,9 +7776,6 @@ function hostDeps$3(ctx) {
|
|
|
7462
7776
|
probe: hostAgentProbe()
|
|
7463
7777
|
};
|
|
7464
7778
|
}
|
|
7465
|
-
function isNotServing$1(error) {
|
|
7466
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7467
|
-
}
|
|
7468
7779
|
function loginLine(kind, email) {
|
|
7469
7780
|
switch (kind) {
|
|
7470
7781
|
case "generated":
|
|
@@ -7481,7 +7792,7 @@ async function databaseLines$1(state, apiKey, deps) {
|
|
|
7481
7792
|
key: apiKey
|
|
7482
7793
|
}).database.listWithStubs()).data;
|
|
7483
7794
|
} catch (error) {
|
|
7484
|
-
if (
|
|
7795
|
+
if (isMetabaseNotServing(error)) return ["Databases: Metabase is not running; `rde start`"];
|
|
7485
7796
|
if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
|
|
7486
7797
|
throw error;
|
|
7487
7798
|
}
|
|
@@ -7563,9 +7874,6 @@ function hostDeps$2(ctx) {
|
|
|
7563
7874
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
7564
7875
|
};
|
|
7565
7876
|
}
|
|
7566
|
-
function isNotServingYet(error) {
|
|
7567
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7568
|
-
}
|
|
7569
7877
|
function transformsStep(deps = hostDeps$2) {
|
|
7570
7878
|
return {
|
|
7571
7879
|
id: STEP_ID,
|
|
@@ -7582,7 +7890,7 @@ function transformsStep(deps = hostDeps$2) {
|
|
|
7582
7890
|
try {
|
|
7583
7891
|
return await client.setting.transformsDecided() ? "done" : "todo";
|
|
7584
7892
|
} catch (error) {
|
|
7585
|
-
if (
|
|
7893
|
+
if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
|
|
7586
7894
|
throw error;
|
|
7587
7895
|
}
|
|
7588
7896
|
},
|
|
@@ -7609,12 +7917,17 @@ const DEFAULT_STEP_OPTIONS = {
|
|
|
7609
7917
|
runtime: NO_RUNTIME_FLAGS,
|
|
7610
7918
|
agents: DETECT_AGENTS,
|
|
7611
7919
|
admin: DEFAULT_ADMIN,
|
|
7612
|
-
databases: SAMPLE_STANDS_IN
|
|
7920
|
+
databases: SAMPLE_STANDS_IN,
|
|
7921
|
+
updates: "published"
|
|
7922
|
+
};
|
|
7923
|
+
const LOCAL_STEP_OPTIONS = {
|
|
7924
|
+
...DEFAULT_STEP_OPTIONS,
|
|
7925
|
+
updates: "installed"
|
|
7613
7926
|
};
|
|
7614
7927
|
const STEP_BY_ID = {
|
|
7615
7928
|
preflight: () => preflightStep(),
|
|
7616
|
-
"install-mb": () => installMbStep(),
|
|
7617
|
-
"agents-skills": (options) => agentsSkillsStep(options.agents),
|
|
7929
|
+
"install-mb": (options) => installMbStep(options.updates),
|
|
7930
|
+
"agents-skills": (options) => agentsSkillsStep(options.agents, options.updates),
|
|
7618
7931
|
runtime: (options) => runtimeStep(options.runtime),
|
|
7619
7932
|
"start-metabase": () => startMetabaseStep(),
|
|
7620
7933
|
"admin-user": (options) => adminUserStep(options.admin),
|
|
@@ -7792,7 +8105,8 @@ function stepOptionsFrom(answers, run) {
|
|
|
7792
8105
|
},
|
|
7793
8106
|
agents: agentsOption(agents, run),
|
|
7794
8107
|
admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
|
|
7795
|
-
databases: { request: databasesRequest(databases, run) }
|
|
8108
|
+
databases: { request: databasesRequest(databases, run) },
|
|
8109
|
+
updates: DEFAULT_STEP_OPTIONS.updates
|
|
7796
8110
|
};
|
|
7797
8111
|
}
|
|
7798
8112
|
function keyedAnswers(answers) {
|
|
@@ -8398,6 +8712,7 @@ var SessionCache = class {
|
|
|
8398
8712
|
const CHECKLIST_MARK = {
|
|
8399
8713
|
done: "✓",
|
|
8400
8714
|
todo: "○",
|
|
8715
|
+
outdated: "↑",
|
|
8401
8716
|
"not-applicable": "–"
|
|
8402
8717
|
};
|
|
8403
8718
|
const SPINNER_FAILURE_CODE = 1;
|
|
@@ -8638,7 +8953,7 @@ async function turnOf(entry, ctx, options) {
|
|
|
8638
8953
|
if (options.only !== void 0) return "not-selected";
|
|
8639
8954
|
if (entry.status === "done") return "done";
|
|
8640
8955
|
const status = await entry.step.detect(ctx);
|
|
8641
|
-
return status === "todo" ? "run" : status;
|
|
8956
|
+
return status === "todo" || status === "outdated" ? "run" : status;
|
|
8642
8957
|
}
|
|
8643
8958
|
function unusedMessage(step, turn, given) {
|
|
8644
8959
|
const hint = step.redoHint === void 0 ? "" : ` ${step.redoHint}`;
|
|
@@ -9102,31 +9417,46 @@ async function credentialRows(state, secrets, apiKey, deps) {
|
|
|
9102
9417
|
stateFileRow(files)
|
|
9103
9418
|
];
|
|
9104
9419
|
}
|
|
9105
|
-
function
|
|
9106
|
-
|
|
9420
|
+
function brokenProfileRepair(profile, method) {
|
|
9421
|
+
if (method === MbAuthMethod.enum.oauth) return {
|
|
9422
|
+
hint: `\`mb auth login --profile ${profile}\` logs it in again; or \`mb auth logout --profile ${profile}\`, then \`rde init --only api-key\` hands the profile to rde's API key`,
|
|
9423
|
+
step: null
|
|
9424
|
+
};
|
|
9425
|
+
return {
|
|
9426
|
+
hint: "`rde init --only api-key` logs it in again",
|
|
9427
|
+
step: API_KEY_STEP
|
|
9428
|
+
};
|
|
9429
|
+
}
|
|
9430
|
+
function unprobedReason(method, healthy, access) {
|
|
9431
|
+
if (access.kind === "accepted") return null;
|
|
9432
|
+
if (method !== MbAuthMethod.enum.oauth) return access.reason;
|
|
9433
|
+
return healthy ? null : NOT_HEALTHY_REASON;
|
|
9107
9434
|
}
|
|
9108
|
-
async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
|
|
9435
|
+
async function mbProfileRow(state, url, deps, profiles, mbUnusableReason, healthy, access) {
|
|
9436
|
+
const id = "mb-profile";
|
|
9109
9437
|
const label = "mb profile";
|
|
9110
|
-
|
|
9111
|
-
if (
|
|
9112
|
-
const
|
|
9113
|
-
if (
|
|
9114
|
-
detail: `${
|
|
9438
|
+
const profile = state.mbProfile;
|
|
9439
|
+
if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
|
|
9440
|
+
const status = await mbTool(deps.runner).authStatus(profile);
|
|
9441
|
+
if (!status.present || status.url === null) return failed(id, label, {
|
|
9442
|
+
detail: `${profile} is not configured`,
|
|
9115
9443
|
hint: "`rde init --only api-key`",
|
|
9116
|
-
step:
|
|
9444
|
+
step: API_KEY_STEP
|
|
9117
9445
|
});
|
|
9118
|
-
if (
|
|
9119
|
-
detail: `${
|
|
9446
|
+
if (!sameMetabaseUrl(status.url, url)) return failed(id, label, {
|
|
9447
|
+
detail: `${profile} points at ${status.url}, not at this instance (${url})`,
|
|
9120
9448
|
hint: "`rde init --only api-key` logs it back in against the instance",
|
|
9121
|
-
step:
|
|
9449
|
+
step: API_KEY_STEP
|
|
9122
9450
|
});
|
|
9123
|
-
|
|
9124
|
-
|
|
9125
|
-
|
|
9126
|
-
|
|
9451
|
+
const unprobed = unprobedReason(status.method, healthy, access);
|
|
9452
|
+
if (unprobed !== null) return skipped(id, label, unprobed);
|
|
9453
|
+
const how = status.method === MbAuthMethod.enum.oauth ? " (OAuth)" : "";
|
|
9454
|
+
const entry = (await profiles()).find((candidate) => candidate.profile === profile);
|
|
9455
|
+
if (entry === void 0 || entry.status !== "ok") return failed(id, label, {
|
|
9456
|
+
detail: `${profile}${how}: ${entry === void 0 ? "not listed by `mb auth list`" : entry.status}`,
|
|
9457
|
+
...brokenProfileRepair(profile, status.method)
|
|
9127
9458
|
});
|
|
9128
|
-
|
|
9129
|
-
return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
|
|
9459
|
+
return ok(id, label, `${profile}${how}: ok${entry.user === null ? "" : ` as ${entry.user.name}`}`);
|
|
9130
9460
|
}
|
|
9131
9461
|
function hostOf(url) {
|
|
9132
9462
|
return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
|
|
@@ -9147,8 +9477,8 @@ async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
|
|
|
9147
9477
|
async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
|
|
9148
9478
|
const rows = [];
|
|
9149
9479
|
if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
|
|
9150
|
-
if (env.url !== null) rows.push(note$1(`env.${ENV_MB_URL}`, ENV_MB_URL, `every \`mb\` call goes to ${hostOf(env.url)}, whatever \`--profile\` says
|
|
9151
|
-
if (env.apiKeySet) rows.push(note$1(`env.${ENV_MB_API_KEY}`, ENV_MB_API_KEY, "set (value not shown): every `mb` call sends it, whatever `--profile` says
|
|
9480
|
+
if (env.url !== null) rows.push(note$1(`env.${ENV_MB_URL}`, ENV_MB_URL, `every \`mb\` call goes to ${hostOf(env.url)}, whatever \`--profile\` says`, unsetHint(ENV_MB_URL)));
|
|
9481
|
+
if (env.apiKeySet) rows.push(note$1(`env.${ENV_MB_API_KEY}`, ENV_MB_API_KEY, "set (value not shown): every `mb` call sends it, whatever `--profile` says", unsetHint(ENV_MB_API_KEY)));
|
|
9152
9482
|
return rows;
|
|
9153
9483
|
}
|
|
9154
9484
|
const PROFILE_STATUS_WORDS = {
|
|
@@ -9250,12 +9580,13 @@ async function sampleDatabaseRow(record, deps, dockerOk, access) {
|
|
|
9250
9580
|
function isMbFailure$1(error) {
|
|
9251
9581
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
9252
9582
|
}
|
|
9253
|
-
async function gitSyncRow(state, deps) {
|
|
9583
|
+
async function gitSyncRow(state, instance, deps) {
|
|
9254
9584
|
const label = "remote sync";
|
|
9255
9585
|
if (!hasRemoteSync(state.license)) return skipped("git-sync", label, `needs a license with ${REMOTE_SYNC_FEATURE}`);
|
|
9256
9586
|
if (state.gitRemote === null) return skipped("git-sync", label, "not configured; `rde init --from git-sync`");
|
|
9587
|
+
if (instance === null) return skipped("git-sync", label, "no API key in the secrets file");
|
|
9257
9588
|
try {
|
|
9258
|
-
const status = await mbTool(deps.runner).gitSyncStatus(
|
|
9589
|
+
const status = await mbTool(deps.runner).gitSyncStatus(instance);
|
|
9259
9590
|
if (status.branch === null) return failed("git-sync", label, {
|
|
9260
9591
|
detail: "Metabase reports no branch",
|
|
9261
9592
|
hint: "`rde init --only git-sync`",
|
|
@@ -9286,7 +9617,7 @@ function withRef(source, ref) {
|
|
|
9286
9617
|
}
|
|
9287
9618
|
async function copyRef(entry, deps) {
|
|
9288
9619
|
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
9289
|
-
return
|
|
9620
|
+
return lockedSkill(path, await deps.skills.files.readJson(path), entry.name)?.ref ?? null;
|
|
9290
9621
|
}
|
|
9291
9622
|
async function judgeCopy(entry, origin, deps) {
|
|
9292
9623
|
const where = `${entry.scope} ${entry.path}`;
|
|
@@ -9413,13 +9744,13 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
|
|
|
9413
9744
|
properties === null ? skipped("version", "Metabase version", NOT_HEALTHY_REASON) : versionRow(state, properties),
|
|
9414
9745
|
properties === null ? skipped("admin", "admin user", NOT_HEALTHY_REASON) : adminRow(state, properties),
|
|
9415
9746
|
...await credentialRows(state, secrets, apiKey, deps),
|
|
9416
|
-
await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
|
|
9747
|
+
await mbProfileRow(state, configured.url, deps, profiles, mbUnusableReason, healthy, apiKey.access)
|
|
9417
9748
|
];
|
|
9418
9749
|
const access = apiKey.access;
|
|
9419
9750
|
if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
|
|
9420
9751
|
else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
|
|
9421
9752
|
rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
|
|
9422
|
-
rows.push(await gitSyncRow(state, deps));
|
|
9753
|
+
rows.push(await gitSyncRow(state, mbInstanceOf(state, secrets), deps));
|
|
9423
9754
|
return rows;
|
|
9424
9755
|
}
|
|
9425
9756
|
async function collectDoctor(instance, state, secrets, deps) {
|
|
@@ -9548,7 +9879,7 @@ const doctor = defineRdeCommand({
|
|
|
9548
9879
|
signal: interruptSignal,
|
|
9549
9880
|
prompts
|
|
9550
9881
|
});
|
|
9551
|
-
if (await fixDoctor(report, initSteps(
|
|
9882
|
+
if (await fixDoctor(report, initSteps(LOCAL_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
|
|
9552
9883
|
}
|
|
9553
9884
|
print(report, args.json);
|
|
9554
9885
|
if (report.failed > 0) throw new Error(doctorSummary(report));
|
|
@@ -9994,20 +10325,12 @@ async function moveToPort(ctx, port, deps) {
|
|
|
9994
10325
|
await runtimeFromState(next, deps.runtime).reconfigure();
|
|
9995
10326
|
ctx.ui.info(`Moved instance ${ctx.state.name} from port ${previous} to ${port}`);
|
|
9996
10327
|
}
|
|
9997
|
-
async function profileStatus(runner, profile) {
|
|
9998
|
-
try {
|
|
9999
|
-
return await mbTool(runner).authStatus(profile);
|
|
10000
|
-
} catch (error) {
|
|
10001
|
-
if (error instanceof ProcessNotFoundError) return null;
|
|
10002
|
-
throw error;
|
|
10003
|
-
}
|
|
10004
|
-
}
|
|
10005
10328
|
async function followMove(ctx, from, runner) {
|
|
10006
10329
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
10007
10330
|
const apiKey = ctx.secrets.apiKey;
|
|
10008
10331
|
const profile = ctx.state.mbProfile;
|
|
10009
|
-
const status = apiKey === null ? null : await
|
|
10010
|
-
if (apiKey !== null && status
|
|
10332
|
+
const status = apiKey === null ? null : await mbTool(runner).authStatusIfInstalled(profile);
|
|
10333
|
+
if (apiKey !== null && isLoggedInAt(status, from)) {
|
|
10011
10334
|
await mbTool(runner).authLogin({
|
|
10012
10335
|
url: configured.url,
|
|
10013
10336
|
profile,
|
|
@@ -10050,7 +10373,7 @@ const start = defineRdeCommand({
|
|
|
10050
10373
|
const SUMMARY_STEP_ID = "summary";
|
|
10051
10374
|
function hostStatusDeps(instanceName, signal) {
|
|
10052
10375
|
return {
|
|
10053
|
-
steps: initSteps(
|
|
10376
|
+
steps: initSteps(LOCAL_STEP_OPTIONS),
|
|
10054
10377
|
runtime: hostRuntimeDeps(instanceName, signal),
|
|
10055
10378
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
10056
10379
|
runner: processRunner(signal)
|
|
@@ -10090,15 +10413,12 @@ function setupOf(ctx, steps) {
|
|
|
10090
10413
|
resume: `rde init --from ${pending.id}`
|
|
10091
10414
|
};
|
|
10092
10415
|
}
|
|
10093
|
-
function isNotServing(error) {
|
|
10094
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
10095
|
-
}
|
|
10096
10416
|
async function databasesOf(ctx, deps, health) {
|
|
10097
10417
|
if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
|
|
10098
10418
|
try {
|
|
10099
10419
|
return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
|
|
10100
10420
|
} catch (error) {
|
|
10101
|
-
if (
|
|
10421
|
+
if (isMetabaseNotServing(error) || isRejectedCredential(error)) return null;
|
|
10102
10422
|
throw error;
|
|
10103
10423
|
}
|
|
10104
10424
|
}
|
|
@@ -10128,10 +10448,13 @@ function isMbFailure(error) {
|
|
|
10128
10448
|
}
|
|
10129
10449
|
async function gitSyncOf(ctx, deps) {
|
|
10130
10450
|
if (ctx.state.gitRemote === null) return null;
|
|
10451
|
+
if (configuredRuntime(ctx.state) === null) return { error: "no Metabase runtime configured; `rde init`" };
|
|
10452
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
10453
|
+
if (instance === null) return { error: "no API key in the secrets file; `rde init --only api-key`" };
|
|
10131
10454
|
try {
|
|
10132
10455
|
const mb = mbTool(deps.runner);
|
|
10133
|
-
const status = await mb.gitSyncStatus(
|
|
10134
|
-
const changes = await mb.gitSyncHasRemoteChanges(
|
|
10456
|
+
const status = await mb.gitSyncStatus(instance);
|
|
10457
|
+
const changes = await mb.gitSyncHasRemoteChanges(instance);
|
|
10135
10458
|
return {
|
|
10136
10459
|
branch: status.branch,
|
|
10137
10460
|
dirty: status.is_dirty,
|
|
@@ -10292,7 +10615,6 @@ const stop = defineRdeCommand({
|
|
|
10292
10615
|
});
|
|
10293
10616
|
//#endregion
|
|
10294
10617
|
//#region src/steps/uninstall.ts
|
|
10295
|
-
const API_KEY_METHOD = "apiKey";
|
|
10296
10618
|
function hostUninstallDeps(signal, childOutput) {
|
|
10297
10619
|
const runner = processRunner(signal);
|
|
10298
10620
|
return {
|
|
@@ -10451,8 +10773,8 @@ async function findJar(source, removeImage) {
|
|
|
10451
10773
|
}
|
|
10452
10774
|
function isInstanceProfile(status, urls) {
|
|
10453
10775
|
if (!status.present) return false;
|
|
10454
|
-
if (urls !== null) return
|
|
10455
|
-
return status.method ===
|
|
10776
|
+
if (urls !== null) return urls.some((url) => isLoggedInAt(status, url));
|
|
10777
|
+
return status.method === MbAuthMethod.enum.apiKey && status.user?.name === "rde";
|
|
10456
10778
|
}
|
|
10457
10779
|
function instanceUrls(state) {
|
|
10458
10780
|
if (state.url === null) return null;
|
|
@@ -10460,23 +10782,34 @@ function instanceUrls(state) {
|
|
|
10460
10782
|
}
|
|
10461
10783
|
async function findMbProfiles(candidates, urls, runner) {
|
|
10462
10784
|
const mb = mbTool(runner);
|
|
10463
|
-
const
|
|
10785
|
+
const found = {
|
|
10786
|
+
removals: [],
|
|
10787
|
+
kept: []
|
|
10788
|
+
};
|
|
10464
10789
|
for (const profile of candidates) {
|
|
10465
10790
|
let status;
|
|
10466
10791
|
try {
|
|
10467
10792
|
status = await mb.authStatus(profile);
|
|
10468
10793
|
} catch (error) {
|
|
10469
|
-
if (error instanceof ProcessNotFoundError) return
|
|
10794
|
+
if (error instanceof ProcessNotFoundError) return {
|
|
10795
|
+
removals: [],
|
|
10796
|
+
kept: []
|
|
10797
|
+
};
|
|
10470
10798
|
throw error;
|
|
10471
10799
|
}
|
|
10472
|
-
if (isInstanceProfile(status, urls))
|
|
10800
|
+
if (!isInstanceProfile(status, urls)) continue;
|
|
10801
|
+
if (status.method === MbAuthMethod.enum.oauth) {
|
|
10802
|
+
found.kept.push(`mb profile ${profile}, logged in with OAuth; \`mb auth logout --profile ${profile}\` logs it out`);
|
|
10803
|
+
continue;
|
|
10804
|
+
}
|
|
10805
|
+
found.removals.push({
|
|
10473
10806
|
label: `mb profile ${profile}`,
|
|
10474
10807
|
remove: async () => {
|
|
10475
10808
|
await mb.authLogout(profile);
|
|
10476
10809
|
}
|
|
10477
10810
|
});
|
|
10478
10811
|
}
|
|
10479
|
-
return
|
|
10812
|
+
return found;
|
|
10480
10813
|
}
|
|
10481
10814
|
function skillLabel(names, entries) {
|
|
10482
10815
|
const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
|
|
@@ -10558,7 +10891,8 @@ async function planUninstall(options, deps) {
|
|
|
10558
10891
|
if (jar.kept !== null) kept.push(jar.kept);
|
|
10559
10892
|
}
|
|
10560
10893
|
const profiles = state === null ? await findMbProfiles([.../* @__PURE__ */ new Set([mbProfileFor(options.instance), namedMbProfile(options.instance)])], null, deps.runner) : await findMbProfiles([state.mbProfile], instanceUrls(state), deps.runner);
|
|
10561
|
-
removals.push(...profiles);
|
|
10894
|
+
removals.push(...profiles.removals);
|
|
10895
|
+
kept.push(...profiles.kept);
|
|
10562
10896
|
if (options.removeSkill) {
|
|
10563
10897
|
const skill = await findSkill(deps.runner, deps.childOutput);
|
|
10564
10898
|
if (skill !== null) removals.push(skill);
|
|
@@ -10975,7 +11309,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
10975
11309
|
const main = defineCommand({
|
|
10976
11310
|
meta: {
|
|
10977
11311
|
name: "rde",
|
|
10978
|
-
version: "0.1.
|
|
11312
|
+
version: "0.1.15",
|
|
10979
11313
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
10980
11314
|
},
|
|
10981
11315
|
subCommands: {
|