@metabase/rde 0.1.12 → 0.1.14
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 -13
- package/dist/cli.mjs +730 -229
- 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";
|
|
@@ -71,12 +71,14 @@ var MetabaseApiError = class extends Error {
|
|
|
71
71
|
status;
|
|
72
72
|
path;
|
|
73
73
|
errors;
|
|
74
|
+
reason;
|
|
74
75
|
constructor(failure) {
|
|
75
76
|
super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
|
|
76
77
|
this.name = "MetabaseApiError";
|
|
77
78
|
this.status = failure.status;
|
|
78
79
|
this.path = failure.path;
|
|
79
80
|
this.errors = failure.errors;
|
|
81
|
+
this.reason = failure.message;
|
|
80
82
|
}
|
|
81
83
|
};
|
|
82
84
|
const UNAUTHORIZED_STATUS = 401;
|
|
@@ -93,10 +95,12 @@ var MetabaseInitializingError = class extends Error {
|
|
|
93
95
|
};
|
|
94
96
|
var MetabaseUnreachableError = class extends Error {
|
|
95
97
|
url;
|
|
98
|
+
reason;
|
|
96
99
|
constructor(url, reason) {
|
|
97
100
|
super(`Metabase at ${url} is unreachable: ${reason}`);
|
|
98
101
|
this.name = "MetabaseUnreachableError";
|
|
99
102
|
this.url = url;
|
|
103
|
+
this.reason = reason;
|
|
100
104
|
}
|
|
101
105
|
};
|
|
102
106
|
var DownloadError = class extends Error {
|
|
@@ -240,10 +244,11 @@ const StepId = z.enum([
|
|
|
240
244
|
"start-metabase",
|
|
241
245
|
"admin-user",
|
|
242
246
|
"api-key",
|
|
247
|
+
"transforms",
|
|
243
248
|
"sample-database",
|
|
244
249
|
"license",
|
|
245
|
-
"databases",
|
|
246
250
|
"git-sync",
|
|
251
|
+
"databases",
|
|
247
252
|
"summary"
|
|
248
253
|
]);
|
|
249
254
|
const GlobalConfig = z.object({
|
|
@@ -891,6 +896,11 @@ function describeCause(error) {
|
|
|
891
896
|
if (error instanceof Error && error.cause instanceof Error) return error.cause.message;
|
|
892
897
|
return error instanceof Error ? error.message : String(error);
|
|
893
898
|
}
|
|
899
|
+
function lookupFailure(error) {
|
|
900
|
+
if (error instanceof MetabaseUnreachableError) return `${error.url} is unreachable: ${error.reason}`;
|
|
901
|
+
if (error instanceof MetabaseApiError) return `HTTP ${error.status} on ${error.path}: ${error.reason}`;
|
|
902
|
+
return null;
|
|
903
|
+
}
|
|
894
904
|
function createHttp(options) {
|
|
895
905
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
896
906
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
@@ -910,7 +920,11 @@ function createHttp(options) {
|
|
|
910
920
|
init.body = JSON.stringify(body);
|
|
911
921
|
}
|
|
912
922
|
try {
|
|
913
|
-
|
|
923
|
+
const response = await fetchImpl(url, init);
|
|
924
|
+
return {
|
|
925
|
+
ok: response.ok,
|
|
926
|
+
body: parseBody(response.status, await response.text())
|
|
927
|
+
};
|
|
914
928
|
} catch (error) {
|
|
915
929
|
if (options.signal.aborted) throw new AbortError();
|
|
916
930
|
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${REQUEST_TIMEOUT_MS}ms`);
|
|
@@ -918,10 +932,9 @@ function createHttp(options) {
|
|
|
918
932
|
}
|
|
919
933
|
}
|
|
920
934
|
async function requestBody(method, path, body) {
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
return parsed;
|
|
935
|
+
const received = await send(method, path, body);
|
|
936
|
+
if (!received.ok) throw failureFor(path, received.body);
|
|
937
|
+
return received.body;
|
|
925
938
|
}
|
|
926
939
|
return {
|
|
927
940
|
async requestJson(method, path, request) {
|
|
@@ -1037,8 +1050,12 @@ const Database = z.object({
|
|
|
1037
1050
|
id: z.int(),
|
|
1038
1051
|
name: z.string(),
|
|
1039
1052
|
engine: z.string(),
|
|
1040
|
-
initial_sync_status: InitialSyncStatus
|
|
1053
|
+
initial_sync_status: InitialSyncStatus,
|
|
1054
|
+
is_stub: z.boolean().optional()
|
|
1041
1055
|
}).loose();
|
|
1056
|
+
function isStub(database) {
|
|
1057
|
+
return database.is_stub === true;
|
|
1058
|
+
}
|
|
1042
1059
|
const DatabaseList = z.object({
|
|
1043
1060
|
data: z.array(Database),
|
|
1044
1061
|
total: z.int()
|
|
@@ -1072,6 +1089,17 @@ function databaseResource(http) {
|
|
|
1072
1089
|
async function list() {
|
|
1073
1090
|
return http.requestJson("GET", "/api/database", { schema: DatabaseList });
|
|
1074
1091
|
}
|
|
1092
|
+
/** GET /api/database?include_stubs=true */
|
|
1093
|
+
async function listWithStubs() {
|
|
1094
|
+
return http.requestJson("GET", "/api/database?include_stubs=true", { schema: DatabaseList });
|
|
1095
|
+
}
|
|
1096
|
+
/** PUT /api/database/:id — on a stub, new connection details make it a real database */
|
|
1097
|
+
async function connect(id, connection) {
|
|
1098
|
+
return http.requestJson("PUT", `/api/database/${id}`, {
|
|
1099
|
+
body: connection,
|
|
1100
|
+
schema: Database
|
|
1101
|
+
});
|
|
1102
|
+
}
|
|
1075
1103
|
/** GET /api/database/:id */
|
|
1076
1104
|
async function get(id) {
|
|
1077
1105
|
return http.requestJson("GET", `/api/database/${id}`, { schema: Database });
|
|
@@ -1080,6 +1108,8 @@ function databaseResource(http) {
|
|
|
1080
1108
|
validate,
|
|
1081
1109
|
create,
|
|
1082
1110
|
list,
|
|
1111
|
+
listWithStubs,
|
|
1112
|
+
connect,
|
|
1083
1113
|
get
|
|
1084
1114
|
};
|
|
1085
1115
|
}
|
|
@@ -1132,6 +1162,13 @@ function remoteSyncResource(http) {
|
|
|
1132
1162
|
schema: RemoteSyncSettingsResult
|
|
1133
1163
|
});
|
|
1134
1164
|
}
|
|
1165
|
+
/** PUT /api/ee/remote-sync/settings with only `remote-sync-transforms`, which skips the repository check. */
|
|
1166
|
+
async function syncTransforms() {
|
|
1167
|
+
return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
|
|
1168
|
+
body: { "remote-sync-transforms": true },
|
|
1169
|
+
schema: RemoteSyncSettingsResult
|
|
1170
|
+
});
|
|
1171
|
+
}
|
|
1135
1172
|
/** POST /api/ee/remote-sync/test-connection; an empty body tests the saved url and token. */
|
|
1136
1173
|
async function testConnection(connection = {}) {
|
|
1137
1174
|
return http.requestJson("POST", "/api/ee/remote-sync/test-connection", {
|
|
@@ -1145,6 +1182,7 @@ function remoteSyncResource(http) {
|
|
|
1145
1182
|
}
|
|
1146
1183
|
return {
|
|
1147
1184
|
updateSettings,
|
|
1185
|
+
syncTransforms,
|
|
1148
1186
|
testConnection,
|
|
1149
1187
|
currentTask
|
|
1150
1188
|
};
|
|
@@ -1154,6 +1192,33 @@ function remoteSyncResource(http) {
|
|
|
1154
1192
|
const Session = z.object({ id: z.string() }).loose();
|
|
1155
1193
|
//#endregion
|
|
1156
1194
|
//#region src/metabase/domain/session-properties.ts
|
|
1195
|
+
const SessionProperties = z.object({
|
|
1196
|
+
"setup-token": z.string().nullable(),
|
|
1197
|
+
"has-user-setup": z.boolean(),
|
|
1198
|
+
version: z.object({ tag: z.string() }).loose(),
|
|
1199
|
+
"token-features": z.record(z.string(), z.boolean())
|
|
1200
|
+
}).loose();
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region src/metabase/resources/session.ts
|
|
1203
|
+
function sessionResource(http) {
|
|
1204
|
+
/** GET /api/session/properties */
|
|
1205
|
+
async function properties() {
|
|
1206
|
+
return http.requestJson("GET", "/api/session/properties", { schema: SessionProperties });
|
|
1207
|
+
}
|
|
1208
|
+
/** POST /api/session */
|
|
1209
|
+
async function create(credentials) {
|
|
1210
|
+
return http.requestJson("POST", "/api/session", {
|
|
1211
|
+
body: credentials,
|
|
1212
|
+
schema: Session
|
|
1213
|
+
});
|
|
1214
|
+
}
|
|
1215
|
+
return {
|
|
1216
|
+
properties,
|
|
1217
|
+
create
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
//#endregion
|
|
1221
|
+
//#region src/metabase/domain/engine.ts
|
|
1157
1222
|
const EngineFieldOption = z.object({
|
|
1158
1223
|
name: z.string(),
|
|
1159
1224
|
value: z.unknown()
|
|
@@ -1179,36 +1244,16 @@ const Engine = z.object({
|
|
|
1179
1244
|
"superseded-by": z.string().nullable(),
|
|
1180
1245
|
"creatable?": z.boolean().optional()
|
|
1181
1246
|
}).loose();
|
|
1182
|
-
const
|
|
1183
|
-
"setup-token": z.string().nullable(),
|
|
1184
|
-
"has-user-setup": z.boolean(),
|
|
1185
|
-
version: z.object({ tag: z.string() }).loose(),
|
|
1186
|
-
"token-features": z.record(z.string(), z.boolean()),
|
|
1187
|
-
engines: z.record(z.string(), Engine)
|
|
1188
|
-
}).loose();
|
|
1247
|
+
const Engines = z.record(z.string(), Engine);
|
|
1189
1248
|
//#endregion
|
|
1190
|
-
//#region src/metabase/
|
|
1191
|
-
|
|
1192
|
-
/** GET /api/session/properties */
|
|
1193
|
-
async function properties() {
|
|
1194
|
-
return http.requestJson("GET", "/api/session/properties", { schema: SessionProperties });
|
|
1195
|
-
}
|
|
1196
|
-
/** POST /api/session */
|
|
1197
|
-
async function create(credentials) {
|
|
1198
|
-
return http.requestJson("POST", "/api/session", {
|
|
1199
|
-
body: credentials,
|
|
1200
|
-
schema: Session
|
|
1201
|
-
});
|
|
1202
|
-
}
|
|
1203
|
-
return {
|
|
1204
|
-
properties,
|
|
1205
|
-
create
|
|
1206
|
-
};
|
|
1207
|
-
}
|
|
1249
|
+
//#region src/metabase/domain/setting.ts
|
|
1250
|
+
const BooleanSetting = z.boolean();
|
|
1208
1251
|
//#endregion
|
|
1209
1252
|
//#region src/metabase/resources/setting.ts
|
|
1210
1253
|
const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
|
|
1254
|
+
const ENGINES_SETTING = "engines";
|
|
1211
1255
|
const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
|
|
1256
|
+
const TRANSFORMS_SETUP_COMPLETE_SETTING = "transforms-setup-complete";
|
|
1212
1257
|
function settingResource(http) {
|
|
1213
1258
|
/** PUT /api/setting/premium-embedding-token */
|
|
1214
1259
|
async function setPremiumEmbeddingToken(value) {
|
|
@@ -1218,9 +1263,19 @@ function settingResource(http) {
|
|
|
1218
1263
|
async function enableTransforms() {
|
|
1219
1264
|
await http.requestNoContent("PUT", `/api/setting/${TRANSFORMS_ENABLED_SETTING}`, { body: { value: true } });
|
|
1220
1265
|
}
|
|
1266
|
+
/** GET /api/setting/transforms-setup-complete */
|
|
1267
|
+
async function transformsDecided() {
|
|
1268
|
+
return http.requestJson("GET", `/api/setting/${TRANSFORMS_SETUP_COMPLETE_SETTING}`, { schema: BooleanSetting });
|
|
1269
|
+
}
|
|
1270
|
+
/** GET /api/setting/engines; admins only, which the API key's user is. */
|
|
1271
|
+
async function engines() {
|
|
1272
|
+
return http.requestJson("GET", `/api/setting/${ENGINES_SETTING}`, { schema: Engines });
|
|
1273
|
+
}
|
|
1221
1274
|
return {
|
|
1222
1275
|
setPremiumEmbeddingToken,
|
|
1223
|
-
enableTransforms
|
|
1276
|
+
enableTransforms,
|
|
1277
|
+
transformsDecided,
|
|
1278
|
+
engines
|
|
1224
1279
|
};
|
|
1225
1280
|
}
|
|
1226
1281
|
//#endregion
|
|
@@ -2443,13 +2498,6 @@ const PLUGINS_DIR = "plugins";
|
|
|
2443
2498
|
const METABASE_STOP_GRACE_MS = 3e4;
|
|
2444
2499
|
const DB_DIR = "metabase.db";
|
|
2445
2500
|
const DB_FILE = "metabase.db";
|
|
2446
|
-
const STAGING_STORE_ENV = {
|
|
2447
|
-
MB_RUN_MODE: "e2e",
|
|
2448
|
-
MB_STORE_USE_STAGING: "true"
|
|
2449
|
-
};
|
|
2450
|
-
const ALLOW_LOCAL_WAREHOUSES_ENV = { MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all" };
|
|
2451
|
-
const NO_H2_SAMPLE_ENV = { MB_LOAD_SAMPLE_CONTENT: "false" };
|
|
2452
|
-
const NO_AUDIT_DB_ENV = { MB_INSTALL_ANALYTICS_DATABASE: "false" };
|
|
2453
2501
|
function instanceUrl(port) {
|
|
2454
2502
|
return `http://localhost:${port}`;
|
|
2455
2503
|
}
|
|
@@ -2458,10 +2506,11 @@ function serverEnv(options) {
|
|
|
2458
2506
|
MB_DB_FILE: options.dbFile,
|
|
2459
2507
|
MB_PLUGINS_DIR: options.pluginsDir,
|
|
2460
2508
|
MB_SITE_URL: instanceUrl(options.port),
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
|
|
2509
|
+
MB_RUN_MODE: "e2e",
|
|
2510
|
+
MB_STORE_USE_STAGING: "true",
|
|
2511
|
+
MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all",
|
|
2512
|
+
MB_LOAD_SAMPLE_CONTENT: "false",
|
|
2513
|
+
MB_INSTALL_ANALYTICS_DATABASE: "false"
|
|
2465
2514
|
};
|
|
2466
2515
|
}
|
|
2467
2516
|
function jettyEnv(port) {
|
|
@@ -3018,12 +3067,12 @@ async function portClaims(state, runtime) {
|
|
|
3018
3067
|
}
|
|
3019
3068
|
//#endregion
|
|
3020
3069
|
//#region src/steps/admin-user.ts
|
|
3021
|
-
const STEP_ID$
|
|
3070
|
+
const STEP_ID$10 = "admin-user";
|
|
3022
3071
|
const ADMIN_USER_KEYS = {
|
|
3023
|
-
email: `${STEP_ID$
|
|
3024
|
-
password: `${STEP_ID$
|
|
3025
|
-
firstName: `${STEP_ID$
|
|
3026
|
-
lastName: `${STEP_ID$
|
|
3072
|
+
email: `${STEP_ID$10}.email`,
|
|
3073
|
+
password: `${STEP_ID$10}.password`,
|
|
3074
|
+
firstName: `${STEP_ID$10}.first-name`,
|
|
3075
|
+
lastName: `${STEP_ID$10}.last-name`
|
|
3027
3076
|
};
|
|
3028
3077
|
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
3029
3078
|
const GENERATED_PASSWORD_BYTES = 24;
|
|
@@ -3038,7 +3087,7 @@ function generatePassword(random) {
|
|
|
3038
3087
|
if (DIGIT.test(candidate)) return candidate;
|
|
3039
3088
|
}
|
|
3040
3089
|
}
|
|
3041
|
-
function hostDeps$
|
|
3090
|
+
function hostDeps$14(ctx) {
|
|
3042
3091
|
return {
|
|
3043
3092
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3044
3093
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
@@ -3176,9 +3225,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
|
|
|
3176
3225
|
ctx.ui.success(`Admin user ${email} recorded`);
|
|
3177
3226
|
return "completed";
|
|
3178
3227
|
}
|
|
3179
|
-
function adminUserStep(options, deps = hostDeps$
|
|
3228
|
+
function adminUserStep(options, deps = hostDeps$14) {
|
|
3180
3229
|
return {
|
|
3181
|
-
id: STEP_ID$
|
|
3230
|
+
id: STEP_ID$10,
|
|
3182
3231
|
title: "Create the admin user",
|
|
3183
3232
|
redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
|
|
3184
3233
|
async detect(ctx) {
|
|
@@ -3247,7 +3296,9 @@ function adminUserStep(options, deps = hostDeps$13) {
|
|
|
3247
3296
|
//#endregion
|
|
3248
3297
|
//#region src/tools/mb.ts
|
|
3249
3298
|
const MB = "mb";
|
|
3250
|
-
const
|
|
3299
|
+
const MB_PACKAGE = "@metabase/cli";
|
|
3300
|
+
const MB_DIST_TAG = "alpha-release-65-integration-branch";
|
|
3301
|
+
const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
|
|
3251
3302
|
const GLOBAL_INSTALL = {
|
|
3252
3303
|
npm: {
|
|
3253
3304
|
command: "npm",
|
|
@@ -3421,7 +3472,6 @@ const AuthProfileStatus = z.enum([
|
|
|
3421
3472
|
const AuthProfile = z.object({
|
|
3422
3473
|
profile: z.string(),
|
|
3423
3474
|
url: z.string(),
|
|
3424
|
-
authenticated: z.boolean(),
|
|
3425
3475
|
status: AuthProfileStatus,
|
|
3426
3476
|
user: ProbedUser.nullable()
|
|
3427
3477
|
}).loose();
|
|
@@ -3503,7 +3553,14 @@ function mbTool(runner) {
|
|
|
3503
3553
|
], LogoutResult);
|
|
3504
3554
|
}
|
|
3505
3555
|
async function authList() {
|
|
3506
|
-
return (await mbJson([
|
|
3556
|
+
return (await mbJson([
|
|
3557
|
+
"auth",
|
|
3558
|
+
"list",
|
|
3559
|
+
"--fields",
|
|
3560
|
+
"profile,url,status,user",
|
|
3561
|
+
"--max-bytes",
|
|
3562
|
+
"0"
|
|
3563
|
+
], AuthList)).data;
|
|
3507
3564
|
}
|
|
3508
3565
|
async function gitSyncStatus(profile) {
|
|
3509
3566
|
return mbJson([
|
|
@@ -3559,14 +3616,14 @@ function mbTool(runner) {
|
|
|
3559
3616
|
}
|
|
3560
3617
|
//#endregion
|
|
3561
3618
|
//#region src/steps/api-key.ts
|
|
3562
|
-
const STEP_ID$
|
|
3619
|
+
const STEP_ID$9 = "api-key";
|
|
3563
3620
|
const API_KEY_KEYS = {
|
|
3564
|
-
email: `${STEP_ID$
|
|
3565
|
-
password: `${STEP_ID$
|
|
3566
|
-
profile: `${STEP_ID$
|
|
3621
|
+
email: `${STEP_ID$9}.email`,
|
|
3622
|
+
password: `${STEP_ID$9}.password`,
|
|
3623
|
+
profile: `${STEP_ID$9}.profile`
|
|
3567
3624
|
};
|
|
3568
3625
|
const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
3569
|
-
function hostDeps$
|
|
3626
|
+
function hostDeps$13(ctx) {
|
|
3570
3627
|
return {
|
|
3571
3628
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3572
3629
|
runner: processRunner(ctx.signal)
|
|
@@ -3685,9 +3742,9 @@ function usageNote(profile) {
|
|
|
3685
3742
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
3686
3743
|
return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
|
|
3687
3744
|
}
|
|
3688
|
-
function apiKeyStep(deps = hostDeps$
|
|
3745
|
+
function apiKeyStep(deps = hostDeps$13) {
|
|
3689
3746
|
return {
|
|
3690
|
-
id: STEP_ID$
|
|
3747
|
+
id: STEP_ID$9,
|
|
3691
3748
|
title: "Connect the Metabase CLI",
|
|
3692
3749
|
redoHint: "`rde status` shows the mb profile it uses.",
|
|
3693
3750
|
async detect(ctx) {
|
|
@@ -4646,20 +4703,29 @@ function syncWithHint(database) {
|
|
|
4646
4703
|
function existingDatabaseLines(databases) {
|
|
4647
4704
|
return databases.map((db) => `${db.name} (${db.engine}, ${syncWithHint(db)})`);
|
|
4648
4705
|
}
|
|
4706
|
+
function unconnectedStubsLine(stubs) {
|
|
4707
|
+
return `Not connected yet, used by your content: ${stubs.map((stub) => stub.name).join(", ")}; \`rde db add\` connects one`;
|
|
4708
|
+
}
|
|
4709
|
+
function listingStatus(entry) {
|
|
4710
|
+
if (entry.stub) return "not connected, your content uses it; `rde db add` connects it";
|
|
4711
|
+
return entry.initialSyncStatus === null ? "missing on the server" : syncLabel(entry.initialSyncStatus);
|
|
4712
|
+
}
|
|
4649
4713
|
function databaseListingLines(listing) {
|
|
4650
4714
|
if (listing.length === 0) return ["No databases connected yet; run `rde db add`"];
|
|
4651
4715
|
return listing.map((entry) => {
|
|
4652
|
-
const
|
|
4653
|
-
return `${entry.name} ${
|
|
4716
|
+
const engine = entry.stub ? "?" : entry.engine;
|
|
4717
|
+
return `${entry.name} ${engine} id ${entry.id} ${listingStatus(entry)}`;
|
|
4654
4718
|
});
|
|
4655
4719
|
}
|
|
4656
4720
|
//#endregion
|
|
4657
4721
|
//#region src/steps/databases/flow.ts
|
|
4658
4722
|
const DATABASE_KEYS = {
|
|
4659
4723
|
connection: "databases.connection",
|
|
4660
|
-
skip: "databases.skip"
|
|
4724
|
+
skip: "databases.skip",
|
|
4725
|
+
stubs: "databases.stubs"
|
|
4661
4726
|
};
|
|
4662
4727
|
const DATABASE_KEY_NAMES = {
|
|
4728
|
+
connection: "connection",
|
|
4663
4729
|
detectedEngine: "detected-engine",
|
|
4664
4730
|
engine: "engine",
|
|
4665
4731
|
mode: "mode",
|
|
@@ -4703,11 +4769,13 @@ const RSA_KEY_UNAVAILABLE_ERROR = "RSA public key is not available";
|
|
|
4703
4769
|
const KEY_RETRIEVAL_NOTE = "allowPublicKeyRetrieval=true is added: MySQL 8 needs it to log in to a local server without SSL; to leave it out, paste a connection string with allowPublicKeyRetrieval=false instead";
|
|
4704
4770
|
const KEY_RETRIEVAL_HINT = "MySQL 8 without SSL needs allowPublicKeyRetrieval=true: go back and add ?allowPublicKeyRetrieval=true to the connection string, or turn on SSL";
|
|
4705
4771
|
const CONNECTION_PLACEHOLDER = "leave empty to skip";
|
|
4772
|
+
const STUB_CONNECTION_PLACEHOLDER = "leave empty to pick the engine";
|
|
4706
4773
|
const START_SCREEN = {
|
|
4707
4774
|
engine: "pick-engine",
|
|
4708
4775
|
"connection-string": "connection-string"
|
|
4709
4776
|
};
|
|
4710
4777
|
const FRESH$1 = {
|
|
4778
|
+
target: null,
|
|
4711
4779
|
engine: null,
|
|
4712
4780
|
uri: null,
|
|
4713
4781
|
pasted: null,
|
|
@@ -4717,6 +4785,35 @@ const FRESH$1 = {
|
|
|
4717
4785
|
name: null,
|
|
4718
4786
|
fieldErrors: null
|
|
4719
4787
|
};
|
|
4788
|
+
function targeting(stub) {
|
|
4789
|
+
return {
|
|
4790
|
+
...FRESH$1,
|
|
4791
|
+
target: stub,
|
|
4792
|
+
name: stub.name
|
|
4793
|
+
};
|
|
4794
|
+
}
|
|
4795
|
+
function restart(model) {
|
|
4796
|
+
return model.target === null ? FRESH$1 : targeting(model.target);
|
|
4797
|
+
}
|
|
4798
|
+
function nextStub(run, model) {
|
|
4799
|
+
const stub = run.queue.shift();
|
|
4800
|
+
if (stub !== void 0) return {
|
|
4801
|
+
type: "next",
|
|
4802
|
+
to: "connection-string",
|
|
4803
|
+
model: targeting(stub)
|
|
4804
|
+
};
|
|
4805
|
+
return run.created.length > 0 ? {
|
|
4806
|
+
type: "next",
|
|
4807
|
+
to: "another",
|
|
4808
|
+
model
|
|
4809
|
+
} : {
|
|
4810
|
+
type: "done",
|
|
4811
|
+
model
|
|
4812
|
+
};
|
|
4813
|
+
}
|
|
4814
|
+
function stubNamed(run, name) {
|
|
4815
|
+
return run.stubs.find((stub) => stub.name === name) ?? null;
|
|
4816
|
+
}
|
|
4720
4817
|
function ordinalOf(run) {
|
|
4721
4818
|
return run.created.length + 1;
|
|
4722
4819
|
}
|
|
@@ -4786,20 +4883,60 @@ async function pickDetectedEngine(run, candidates) {
|
|
|
4786
4883
|
if (entry === void 0) throw new Error(`engine ${picked} was not offered`);
|
|
4787
4884
|
return entry;
|
|
4788
4885
|
}
|
|
4886
|
+
function pickStubsScreen(run) {
|
|
4887
|
+
return async (model) => {
|
|
4888
|
+
const picked = await run.deps.prompts.multiselect({
|
|
4889
|
+
key: DATABASE_KEYS.stubs,
|
|
4890
|
+
message: "Your content uses these databases, not connected yet. Pick the ones to connect now (space picks, Enter goes on; the rest wait for `rde db add`)",
|
|
4891
|
+
choices: run.stubs.map((stub) => ({
|
|
4892
|
+
value: stub.id,
|
|
4893
|
+
label: stub.name
|
|
4894
|
+
})),
|
|
4895
|
+
required: false
|
|
4896
|
+
});
|
|
4897
|
+
if (picked.length > 0) {
|
|
4898
|
+
run.queue = run.stubs.filter((stub) => picked.includes(stub.id));
|
|
4899
|
+
return nextStub(run, model);
|
|
4900
|
+
}
|
|
4901
|
+
if (run.deps.start === "connection-string" && run.created.length === 0) return {
|
|
4902
|
+
type: "done",
|
|
4903
|
+
model
|
|
4904
|
+
};
|
|
4905
|
+
return {
|
|
4906
|
+
type: "next",
|
|
4907
|
+
to: "pick-engine",
|
|
4908
|
+
model: FRESH$1
|
|
4909
|
+
};
|
|
4910
|
+
};
|
|
4911
|
+
}
|
|
4912
|
+
function connectionQuestion(run, target) {
|
|
4913
|
+
if (target === null) return {
|
|
4914
|
+
key: DATABASE_KEYS.connection,
|
|
4915
|
+
message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
|
|
4916
|
+
placeholder: CONNECTION_PLACEHOLDER
|
|
4917
|
+
};
|
|
4918
|
+
return {
|
|
4919
|
+
key: databaseKey(ordinalOf(run), "connection"),
|
|
4920
|
+
message: `Connection string of ${target.name} (Enter to pick its engine instead)`,
|
|
4921
|
+
placeholder: STUB_CONNECTION_PLACEHOLDER
|
|
4922
|
+
};
|
|
4923
|
+
}
|
|
4789
4924
|
function connectionStringScreen(run) {
|
|
4790
4925
|
return async (model) => {
|
|
4791
4926
|
let draft = model.uri;
|
|
4792
4927
|
for (;;) {
|
|
4793
4928
|
const uri = (await run.deps.prompts.text({
|
|
4794
|
-
|
|
4795
|
-
message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
|
|
4796
|
-
placeholder: CONNECTION_PLACEHOLDER,
|
|
4929
|
+
...connectionQuestion(run, model.target),
|
|
4797
4930
|
...draft !== null && { initialValue: draft }
|
|
4798
4931
|
})).trim();
|
|
4799
4932
|
draft = uri;
|
|
4800
|
-
if (uri.length === 0) return {
|
|
4933
|
+
if (uri.length === 0) return model.target === null ? {
|
|
4801
4934
|
type: "done",
|
|
4802
4935
|
model
|
|
4936
|
+
} : {
|
|
4937
|
+
type: "next",
|
|
4938
|
+
to: "pick-engine",
|
|
4939
|
+
model: restart(model)
|
|
4803
4940
|
};
|
|
4804
4941
|
const candidates = detectEngine(uri).map((key) => run.deps.catalog.entry(key)).filter(isCreatableEntry);
|
|
4805
4942
|
const [first, ...others] = candidates;
|
|
@@ -4808,7 +4945,7 @@ function connectionStringScreen(run) {
|
|
|
4808
4945
|
return {
|
|
4809
4946
|
type: "next",
|
|
4810
4947
|
to: "pick-engine",
|
|
4811
|
-
model:
|
|
4948
|
+
model: restart(model)
|
|
4812
4949
|
};
|
|
4813
4950
|
}
|
|
4814
4951
|
const engine = others.length === 0 ? first : await pickDetectedEngine(run, candidates);
|
|
@@ -4822,7 +4959,7 @@ function connectionStringScreen(run) {
|
|
|
4822
4959
|
return {
|
|
4823
4960
|
type: "next",
|
|
4824
4961
|
to: "pick-engine",
|
|
4825
|
-
model:
|
|
4962
|
+
model: restart(model)
|
|
4826
4963
|
};
|
|
4827
4964
|
}
|
|
4828
4965
|
return toReview(run, model, {
|
|
@@ -4832,12 +4969,16 @@ function connectionStringScreen(run) {
|
|
|
4832
4969
|
}
|
|
4833
4970
|
};
|
|
4834
4971
|
}
|
|
4972
|
+
function finishLabel(run, model) {
|
|
4973
|
+
if (model.target !== null) return `Leave ${model.target.name} unconnected`;
|
|
4974
|
+
return run.deps.existingCount + run.created.length > 0 ? "Finish" : "Skip for now";
|
|
4975
|
+
}
|
|
4835
4976
|
function pickEngineScreen(run) {
|
|
4836
4977
|
return async (model) => {
|
|
4837
4978
|
const total = run.deps.existingCount + run.created.length;
|
|
4838
4979
|
const finishChoice = {
|
|
4839
4980
|
value: FINISH,
|
|
4840
|
-
label:
|
|
4981
|
+
label: finishLabel(run, model)
|
|
4841
4982
|
};
|
|
4842
4983
|
for (;;) {
|
|
4843
4984
|
const picked = await run.deps.prompts.select({
|
|
@@ -4846,6 +4987,7 @@ function pickEngineScreen(run) {
|
|
|
4846
4987
|
choices: [...run.deps.catalog.list().map(engineChoice), finishChoice],
|
|
4847
4988
|
...model.engine !== null && { initialValue: model.engine.key }
|
|
4848
4989
|
});
|
|
4990
|
+
if (picked === "finish" && model.target !== null) return nextStub(run, model);
|
|
4849
4991
|
if (picked === "finish") {
|
|
4850
4992
|
if (total > 0 || await confirmSkip(run)) return {
|
|
4851
4993
|
type: "done",
|
|
@@ -4863,7 +5005,7 @@ function pickEngineScreen(run) {
|
|
|
4863
5005
|
type: "next",
|
|
4864
5006
|
to: "paste-uri",
|
|
4865
5007
|
model: model.engine?.key === entry.key ? model : {
|
|
4866
|
-
...
|
|
5008
|
+
...restart(model),
|
|
4867
5009
|
engine: entry
|
|
4868
5010
|
}
|
|
4869
5011
|
};
|
|
@@ -4894,6 +5036,10 @@ async function chooseAfterParseError(run, engine, uri, error) {
|
|
|
4894
5036
|
if (entry === void 0) throw new Error(`engine ${picked} was not offered`);
|
|
4895
5037
|
return entry;
|
|
4896
5038
|
}
|
|
5039
|
+
function reviewName(model, sameString) {
|
|
5040
|
+
if (model.target !== null) return model.target.name;
|
|
5041
|
+
return sameString ? model.name : null;
|
|
5042
|
+
}
|
|
4897
5043
|
function toReview(run, model, connection) {
|
|
4898
5044
|
const mapped = mapDatabaseValues(connection.parsed);
|
|
4899
5045
|
const sameString = model.uri === connection.parsed.uri;
|
|
@@ -4901,13 +5047,14 @@ function toReview(run, model, connection) {
|
|
|
4901
5047
|
type: "next",
|
|
4902
5048
|
to: "review",
|
|
4903
5049
|
model: {
|
|
5050
|
+
target: model.target,
|
|
4904
5051
|
engine: connection.engine,
|
|
4905
5052
|
uri: connection.parsed.uri,
|
|
4906
5053
|
pasted: mapped.details,
|
|
4907
5054
|
details: mapped.details,
|
|
4908
5055
|
failure: null,
|
|
4909
5056
|
suggestedName: defaultDisplayName(mapped, connection.parsed, connection.engine),
|
|
4910
|
-
name:
|
|
5057
|
+
name: reviewName(model, sameString),
|
|
4911
5058
|
fieldErrors: null
|
|
4912
5059
|
}
|
|
4913
5060
|
};
|
|
@@ -5050,6 +5197,33 @@ async function fixReportedFields(run, fields, details, errors) {
|
|
|
5050
5197
|
}
|
|
5051
5198
|
return current;
|
|
5052
5199
|
}
|
|
5200
|
+
function reviewQuestion(target, name) {
|
|
5201
|
+
const edit = {
|
|
5202
|
+
value: "edit",
|
|
5203
|
+
label: "Edit a field"
|
|
5204
|
+
};
|
|
5205
|
+
if (target !== null) return {
|
|
5206
|
+
message: `Connect ${name}?`,
|
|
5207
|
+
choices: [{
|
|
5208
|
+
value: "test",
|
|
5209
|
+
label: "Test and connect"
|
|
5210
|
+
}, edit]
|
|
5211
|
+
};
|
|
5212
|
+
return {
|
|
5213
|
+
message: `Add ${name}?`,
|
|
5214
|
+
choices: [
|
|
5215
|
+
{
|
|
5216
|
+
value: "test",
|
|
5217
|
+
label: "Test and add"
|
|
5218
|
+
},
|
|
5219
|
+
edit,
|
|
5220
|
+
{
|
|
5221
|
+
value: "rename",
|
|
5222
|
+
label: "Change the display name"
|
|
5223
|
+
}
|
|
5224
|
+
]
|
|
5225
|
+
};
|
|
5226
|
+
}
|
|
5053
5227
|
function reviewScreen(run) {
|
|
5054
5228
|
const review = reviewAttempt(run);
|
|
5055
5229
|
return (model) => model.failure === null ? review(model) : orFailure(model.failure, () => review({
|
|
@@ -5075,21 +5249,7 @@ function reviewAttempt(run) {
|
|
|
5075
5249
|
if (name === null) name = await promptName(run, current, chosen.engine);
|
|
5076
5250
|
const action = await run.deps.prompts.selectOrBack({
|
|
5077
5251
|
key: databaseKey(ordinalOf(run), "action"),
|
|
5078
|
-
|
|
5079
|
-
choices: [
|
|
5080
|
-
{
|
|
5081
|
-
value: "test",
|
|
5082
|
-
label: "Test and add"
|
|
5083
|
-
},
|
|
5084
|
-
{
|
|
5085
|
-
value: "edit",
|
|
5086
|
-
label: "Edit a field"
|
|
5087
|
-
},
|
|
5088
|
-
{
|
|
5089
|
-
value: "rename",
|
|
5090
|
-
label: "Change the display name"
|
|
5091
|
-
}
|
|
5092
|
-
]
|
|
5252
|
+
...reviewQuestion(model.target, name)
|
|
5093
5253
|
});
|
|
5094
5254
|
if (action === BACK) return { type: "back" };
|
|
5095
5255
|
if (action === "edit") {
|
|
@@ -5168,26 +5328,44 @@ function testScreen(run) {
|
|
|
5168
5328
|
function isRejectedDetails(error) {
|
|
5169
5329
|
return error instanceof MetabaseApiError && error.status === BAD_REQUEST_STATUS$1;
|
|
5170
5330
|
}
|
|
5331
|
+
function commitFor(run, stub) {
|
|
5332
|
+
if (stub === null) return {
|
|
5333
|
+
verb: "add",
|
|
5334
|
+
progress: "Adding",
|
|
5335
|
+
past: "Added",
|
|
5336
|
+
send: (request) => run.deps.database.create(request)
|
|
5337
|
+
};
|
|
5338
|
+
return {
|
|
5339
|
+
verb: "connect",
|
|
5340
|
+
progress: "Connecting",
|
|
5341
|
+
past: "Connected",
|
|
5342
|
+
send: ({ engine, details }) => run.deps.database.connect(stub.id, {
|
|
5343
|
+
engine,
|
|
5344
|
+
details
|
|
5345
|
+
})
|
|
5346
|
+
};
|
|
5347
|
+
}
|
|
5171
5348
|
function createScreen(run) {
|
|
5172
5349
|
return async (model) => {
|
|
5173
5350
|
const chosen = requireEngine(run, model);
|
|
5174
5351
|
if (model.name === null) throw new Error("no display name before create");
|
|
5175
|
-
const
|
|
5352
|
+
const commit = commitFor(run, model.target ?? stubNamed(run, model.name));
|
|
5353
|
+
const spinner = run.deps.ui.spinner(`${commit.progress} ${model.name}…`);
|
|
5176
5354
|
let database;
|
|
5177
5355
|
try {
|
|
5178
|
-
database = await
|
|
5356
|
+
database = await commit.send({
|
|
5179
5357
|
name: model.name,
|
|
5180
5358
|
engine: chosen.engine.key,
|
|
5181
5359
|
details: sentDetails(run, chosen, model.details).details
|
|
5182
5360
|
});
|
|
5183
5361
|
} catch (error) {
|
|
5184
5362
|
if (!isRejectedDetails(error)) {
|
|
5185
|
-
spinner.fail(`Could not
|
|
5363
|
+
spinner.fail(`Could not ${commit.verb} ${model.name}`);
|
|
5186
5364
|
throw error;
|
|
5187
5365
|
}
|
|
5188
5366
|
spinner.fail(error.message);
|
|
5189
5367
|
keyRetrievalHint(run, chosen, error.message);
|
|
5190
|
-
const failure = `Metabase could not
|
|
5368
|
+
const failure = `Metabase could not ${commit.verb} ${model.name}: ${error.message}`;
|
|
5191
5369
|
return {
|
|
5192
5370
|
type: "next",
|
|
5193
5371
|
to: "review",
|
|
@@ -5199,14 +5377,15 @@ function createScreen(run) {
|
|
|
5199
5377
|
};
|
|
5200
5378
|
}
|
|
5201
5379
|
run.created.push(database);
|
|
5202
|
-
|
|
5380
|
+
run.stubs = run.stubs.filter((candidate) => candidate.id !== database.id);
|
|
5381
|
+
spinner.stop(`${commit.past} ${database.name}. Initial sync is running in the background; \`rde status\` shows progress.`);
|
|
5203
5382
|
run.added.push(addedDatabase(chosen, model, model.name));
|
|
5204
5383
|
await run.deps.onCreated(database);
|
|
5205
|
-
return {
|
|
5384
|
+
return model.target === null ? {
|
|
5206
5385
|
type: "next",
|
|
5207
5386
|
to: "another",
|
|
5208
5387
|
model
|
|
5209
|
-
};
|
|
5388
|
+
} : nextStub(run, model);
|
|
5210
5389
|
};
|
|
5211
5390
|
}
|
|
5212
5391
|
function anotherScreen(run) {
|
|
@@ -5227,18 +5406,24 @@ function anotherScreen(run) {
|
|
|
5227
5406
|
};
|
|
5228
5407
|
return {
|
|
5229
5408
|
type: "next",
|
|
5230
|
-
to: "pick-engine",
|
|
5409
|
+
to: offersStubs(run) ? "pick-stubs" : "pick-engine",
|
|
5231
5410
|
model: FRESH$1
|
|
5232
5411
|
};
|
|
5233
5412
|
};
|
|
5234
5413
|
}
|
|
5414
|
+
function offersStubs(run) {
|
|
5415
|
+
return run.deps.offerStubs && run.stubs.length > 0;
|
|
5416
|
+
}
|
|
5235
5417
|
async function runDatabaseFlow(deps) {
|
|
5236
5418
|
const run = {
|
|
5237
5419
|
deps,
|
|
5238
5420
|
created: [],
|
|
5239
|
-
added: []
|
|
5421
|
+
added: [],
|
|
5422
|
+
stubs: [...deps.stubs],
|
|
5423
|
+
queue: []
|
|
5240
5424
|
};
|
|
5241
5425
|
await runFlow({
|
|
5426
|
+
"pick-stubs": pickStubsScreen(run),
|
|
5242
5427
|
"connection-string": connectionStringScreen(run),
|
|
5243
5428
|
"pick-engine": pickEngineScreen(run),
|
|
5244
5429
|
"paste-uri": pasteUriScreen(run),
|
|
@@ -5246,10 +5431,11 @@ async function runDatabaseFlow(deps) {
|
|
|
5246
5431
|
test: testScreen(run),
|
|
5247
5432
|
create: createScreen(run),
|
|
5248
5433
|
another: anotherScreen(run)
|
|
5249
|
-
}, START_SCREEN[deps.start], FRESH$1);
|
|
5434
|
+
}, offersStubs(run) ? "pick-stubs" : START_SCREEN[deps.start], FRESH$1);
|
|
5250
5435
|
return {
|
|
5251
5436
|
created: run.created,
|
|
5252
|
-
added: run.added
|
|
5437
|
+
added: run.added,
|
|
5438
|
+
stubsLeft: run.stubs
|
|
5253
5439
|
};
|
|
5254
5440
|
}
|
|
5255
5441
|
//#endregion
|
|
@@ -5305,8 +5491,9 @@ function gitTool(runner) {
|
|
|
5305
5491
|
}
|
|
5306
5492
|
async function isRepo(dir) {
|
|
5307
5493
|
if (!await directoryExists(dir)) return false;
|
|
5308
|
-
const result = await runner.run(GIT, ["rev-parse", "--
|
|
5309
|
-
|
|
5494
|
+
const result = await runner.run(GIT, ["rev-parse", "--show-toplevel"], { cwd: dir });
|
|
5495
|
+
if (result.exitCode !== 0) return false;
|
|
5496
|
+
return result.stdout.trim() === await promises.realpath(dir);
|
|
5310
5497
|
}
|
|
5311
5498
|
async function initBare(path, options) {
|
|
5312
5499
|
await git([
|
|
@@ -5419,13 +5606,13 @@ function gitTool(runner) {
|
|
|
5419
5606
|
}
|
|
5420
5607
|
//#endregion
|
|
5421
5608
|
//#region src/steps/git-sync.ts
|
|
5422
|
-
const STEP_ID$
|
|
5609
|
+
const STEP_ID$8 = "git-sync";
|
|
5423
5610
|
const GIT_SYNC_KEYS = {
|
|
5424
|
-
location: `${STEP_ID$
|
|
5425
|
-
path: `${STEP_ID$
|
|
5426
|
-
remote: `${STEP_ID$
|
|
5427
|
-
url: `${STEP_ID$
|
|
5428
|
-
token: `${STEP_ID$
|
|
5611
|
+
location: `${STEP_ID$8}.location`,
|
|
5612
|
+
path: `${STEP_ID$8}.path`,
|
|
5613
|
+
remote: `${STEP_ID$8}.remote`,
|
|
5614
|
+
url: `${STEP_ID$8}.url`,
|
|
5615
|
+
token: `${STEP_ID$8}.token`
|
|
5429
5616
|
};
|
|
5430
5617
|
const BARE_REPO_NAME = "content.git";
|
|
5431
5618
|
const DEFAULT_BRANCH = "main";
|
|
@@ -5455,7 +5642,11 @@ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
|
5455
5642
|
"timed-out",
|
|
5456
5643
|
"conflict"
|
|
5457
5644
|
]);
|
|
5458
|
-
|
|
5645
|
+
const SYNC_MODE_SETTINGS = {
|
|
5646
|
+
"remote-sync-type": "read-write",
|
|
5647
|
+
"remote-sync-transforms": true
|
|
5648
|
+
};
|
|
5649
|
+
function hostDeps$12(ctx) {
|
|
5459
5650
|
return {
|
|
5460
5651
|
runner: processRunner(ctx.signal),
|
|
5461
5652
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -5746,7 +5937,7 @@ async function configureLocalBare(run, runtime, workdir) {
|
|
|
5746
5937
|
},
|
|
5747
5938
|
settings: {
|
|
5748
5939
|
"remote-sync-url": bareUrlFor(runtime, barePath),
|
|
5749
|
-
|
|
5940
|
+
...SYNC_MODE_SETTINGS,
|
|
5750
5941
|
"remote-sync-branch": prepared.branch
|
|
5751
5942
|
}
|
|
5752
5943
|
};
|
|
@@ -5789,7 +5980,7 @@ async function configureHosted(run, workdir, url) {
|
|
|
5789
5980
|
settings: {
|
|
5790
5981
|
"remote-sync-url": url,
|
|
5791
5982
|
"remote-sync-token": token,
|
|
5792
|
-
|
|
5983
|
+
...SYNC_MODE_SETTINGS,
|
|
5793
5984
|
"remote-sync-branch": prepared.branch
|
|
5794
5985
|
}
|
|
5795
5986
|
};
|
|
@@ -5862,6 +6053,7 @@ async function importExistingContent(run, gitRemote) {
|
|
|
5862
6053
|
}
|
|
5863
6054
|
spinner.stop("Import finished");
|
|
5864
6055
|
reportImport(run, outcome);
|
|
6056
|
+
await run.client.remoteSync.syncTransforms();
|
|
5865
6057
|
}
|
|
5866
6058
|
const SHELL_SAFE = /^[\w./-]+$/;
|
|
5867
6059
|
function shellWord(value) {
|
|
@@ -5922,15 +6114,15 @@ async function runGitSync(ctx, deps) {
|
|
|
5922
6114
|
ctx.ui.note(usageLines$1(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
5923
6115
|
return "completed";
|
|
5924
6116
|
}
|
|
5925
|
-
function gitSyncStep(deps = hostDeps$
|
|
6117
|
+
function gitSyncStep(deps = hostDeps$12) {
|
|
5926
6118
|
return {
|
|
5927
|
-
id: STEP_ID$
|
|
6119
|
+
id: STEP_ID$8,
|
|
5928
6120
|
title: "Connect a git repository",
|
|
5929
6121
|
redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
|
|
5930
6122
|
async detect(ctx) {
|
|
5931
6123
|
if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
|
|
5932
|
-
if (ctx.state.steps[STEP_ID$
|
|
5933
|
-
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$
|
|
6124
|
+
if (ctx.state.steps[STEP_ID$8]?.status === "skipped") return "not-applicable";
|
|
6125
|
+
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
|
|
5934
6126
|
try {
|
|
5935
6127
|
return (await mbTool(deps(ctx).runner).gitSyncStatus(ctx.state.mbProfile)).branch === null ? "todo" : "done";
|
|
5936
6128
|
} catch (error) {
|
|
@@ -5944,17 +6136,45 @@ function gitSyncStep(deps = hostDeps$11) {
|
|
|
5944
6136
|
};
|
|
5945
6137
|
}
|
|
5946
6138
|
//#endregion
|
|
6139
|
+
//#region src/metabase/domain/github-content.ts
|
|
6140
|
+
const GitHubContentEntry = z.object({
|
|
6141
|
+
name: z.string(),
|
|
6142
|
+
type: z.string(),
|
|
6143
|
+
sha: z.string()
|
|
6144
|
+
}).loose();
|
|
6145
|
+
const GitHubDirectory = z.array(GitHubContentEntry);
|
|
6146
|
+
//#endregion
|
|
6147
|
+
//#region src/metabase/github.ts
|
|
6148
|
+
const GITHUB_API = "https://api.github.com";
|
|
6149
|
+
const DIRECTORY_TYPE = "dir";
|
|
6150
|
+
function githubContents(signal, options = {}) {
|
|
6151
|
+
const http = createHttp({
|
|
6152
|
+
baseUrl: GITHUB_API,
|
|
6153
|
+
auth: NO_AUTH,
|
|
6154
|
+
signal,
|
|
6155
|
+
...options
|
|
6156
|
+
});
|
|
6157
|
+
return { async subdirectoryHashes(repository, ref, directory) {
|
|
6158
|
+
const path = `/repos/${repository}/contents/${directory}${ref === null ? "" : `?ref=${encodeURIComponent(ref)}`}`;
|
|
6159
|
+
const entries = await http.requestJson("GET", path, { schema: GitHubDirectory });
|
|
6160
|
+
return new Map(entries.filter((entry) => entry.type === DIRECTORY_TYPE).map((entry) => [entry.name, entry.sha]));
|
|
6161
|
+
} };
|
|
6162
|
+
}
|
|
6163
|
+
//#endregion
|
|
5947
6164
|
//#region src/tools/skills-cli.ts
|
|
5948
6165
|
const NPX = "npx";
|
|
5949
6166
|
const SKILLS_PACKAGE = "skills@latest";
|
|
5950
6167
|
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
|
|
5951
6168
|
const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
|
|
5952
|
-
const
|
|
6169
|
+
const DATA_APP_REPOSITORY = "metabase/metabase";
|
|
6170
|
+
const DATA_APP_BRANCH = "master";
|
|
6171
|
+
const DATA_APP_SKILLS_SOURCE = `${DATA_APP_REPOSITORY}/skills#${DATA_APP_BRANCH}`;
|
|
5953
6172
|
const DATA_APP_SKILL_NAMES = [
|
|
5954
6173
|
"metabase-data-app-setup",
|
|
5955
6174
|
"metabase-data-app-routing",
|
|
5956
6175
|
"metabase-data-app-actions",
|
|
5957
|
-
"metabase-data-app-semantic-layer"
|
|
6176
|
+
"metabase-data-app-semantic-layer",
|
|
6177
|
+
"metabase-data-app-migrate"
|
|
5958
6178
|
];
|
|
5959
6179
|
function rdeSkillSource() {
|
|
5960
6180
|
const override = readEnv(ENV_RDE_SKILL_SOURCE);
|
|
@@ -5968,7 +6188,13 @@ const InstalledSkill = z.object({
|
|
|
5968
6188
|
source: z.string().nullable()
|
|
5969
6189
|
}).loose();
|
|
5970
6190
|
const InstalledSkillList = z.array(InstalledSkill);
|
|
5971
|
-
const
|
|
6191
|
+
const LockedSkill = z.object({
|
|
6192
|
+
source: z.string().nullish(),
|
|
6193
|
+
ref: z.string().nullish(),
|
|
6194
|
+
skillPath: z.string().nullish(),
|
|
6195
|
+
skillFolderHash: z.string().nullish()
|
|
6196
|
+
}).loose();
|
|
6197
|
+
const SkillLock = z.object({ skills: z.record(z.string(), LockedSkill) }).loose();
|
|
5972
6198
|
const GLOBAL_LOCK_FILE = ".skill-lock.json";
|
|
5973
6199
|
const PROJECT_LOCK_FILE = "skills-lock.json";
|
|
5974
6200
|
function globalSkillLockPath(home, stateHome) {
|
|
@@ -5981,16 +6207,21 @@ function xdgStateHome() {
|
|
|
5981
6207
|
const value = readEnv(ENV_XDG_STATE_HOME);
|
|
5982
6208
|
return value === void 0 || value.length === 0 ? null : value;
|
|
5983
6209
|
}
|
|
5984
|
-
function
|
|
6210
|
+
function lockedSkill(path, raw, skill) {
|
|
5985
6211
|
if (raw === null) return null;
|
|
5986
6212
|
const parsed = SkillLock.safeParse(raw);
|
|
5987
6213
|
if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
|
|
5988
|
-
return parsed.data.skills[skill]
|
|
6214
|
+
return parsed.data.skills[skill] ?? null;
|
|
5989
6215
|
}
|
|
5990
6216
|
const hostSkillFiles = {
|
|
5991
6217
|
readJson: readJsonFile,
|
|
5992
6218
|
readText: readTextFile
|
|
5993
6219
|
};
|
|
6220
|
+
const DATA_APP_SKILLS_ORIGIN = {
|
|
6221
|
+
kind: "github",
|
|
6222
|
+
repository: DATA_APP_REPOSITORY,
|
|
6223
|
+
ref: DATA_APP_BRANCH
|
|
6224
|
+
};
|
|
5994
6225
|
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
5995
6226
|
function skillOrigin(source, cwd) {
|
|
5996
6227
|
const github = GITHUB_URL.exec(source);
|
|
@@ -6071,16 +6302,20 @@ function skillsCli(runner) {
|
|
|
6071
6302
|
}
|
|
6072
6303
|
//#endregion
|
|
6073
6304
|
//#region src/steps/agents-skills.ts
|
|
6074
|
-
const STEP_ID$
|
|
6075
|
-
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$
|
|
6305
|
+
const STEP_ID$7 = "agents-skills";
|
|
6306
|
+
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
|
|
6076
6307
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6077
6308
|
const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
|
|
6078
6309
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6079
|
-
function hostDeps$
|
|
6310
|
+
function hostDeps$11(ctx) {
|
|
6080
6311
|
return {
|
|
6081
6312
|
runner: processRunner(ctx.signal),
|
|
6082
6313
|
probe: hostAgentProbe(),
|
|
6083
|
-
skillSource: rdeSkillSource()
|
|
6314
|
+
skillSource: rdeSkillSource(),
|
|
6315
|
+
stateHome: xdgStateHome(),
|
|
6316
|
+
files: hostSkillFiles,
|
|
6317
|
+
github: githubContents(ctx.signal),
|
|
6318
|
+
cwd: process.cwd()
|
|
6084
6319
|
};
|
|
6085
6320
|
}
|
|
6086
6321
|
function detectedOnly(agents) {
|
|
@@ -6115,10 +6350,91 @@ function coveredAgentNames(entry, agents) {
|
|
|
6115
6350
|
function installedLines(entries, agents) {
|
|
6116
6351
|
return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
|
|
6117
6352
|
}
|
|
6353
|
+
function skillSets(deps) {
|
|
6354
|
+
return [{
|
|
6355
|
+
label: "data-app",
|
|
6356
|
+
source: DATA_APP_SKILLS_SOURCE,
|
|
6357
|
+
sourceNote: "",
|
|
6358
|
+
origin: DATA_APP_SKILLS_ORIGIN,
|
|
6359
|
+
skills: DATA_APP_SKILL_NAMES
|
|
6360
|
+
}, {
|
|
6361
|
+
label: "rde",
|
|
6362
|
+
source: deps.skillSource,
|
|
6363
|
+
sourceNote: deps.skillSource === "https://github.com/metabase/agent-skills/tree/main" ? "" : ` from ${deps.skillSource}`,
|
|
6364
|
+
origin: skillOrigin(deps.skillSource, deps.cwd),
|
|
6365
|
+
skills: RDE_SOURCE_SKILL_NAMES
|
|
6366
|
+
}];
|
|
6367
|
+
}
|
|
6368
|
+
function hashLookup(github) {
|
|
6369
|
+
const asked = /* @__PURE__ */ new Map();
|
|
6370
|
+
return (repository, ref, directory) => {
|
|
6371
|
+
const key = JSON.stringify([
|
|
6372
|
+
repository,
|
|
6373
|
+
ref,
|
|
6374
|
+
directory
|
|
6375
|
+
]);
|
|
6376
|
+
let hashes = asked.get(key);
|
|
6377
|
+
if (hashes === void 0) {
|
|
6378
|
+
hashes = github.subdirectoryHashes(repository, ref, directory).catch((error) => {
|
|
6379
|
+
if (lookupFailure(error) === null) throw error;
|
|
6380
|
+
return null;
|
|
6381
|
+
});
|
|
6382
|
+
asked.set(key, hashes);
|
|
6383
|
+
}
|
|
6384
|
+
return hashes;
|
|
6385
|
+
};
|
|
6386
|
+
}
|
|
6387
|
+
async function readLockedSkills(deps) {
|
|
6388
|
+
const lockPath = globalSkillLockPath(deps.probe.home, deps.stateHome);
|
|
6389
|
+
try {
|
|
6390
|
+
const lock = await deps.files.readJson(lockPath);
|
|
6391
|
+
return new Map(REQUIRED_SKILL_NAMES.map((name) => [name, lockedSkill(lockPath, lock, name)]));
|
|
6392
|
+
} catch (error) {
|
|
6393
|
+
if (error instanceof ConfigError) return null;
|
|
6394
|
+
throw error;
|
|
6395
|
+
}
|
|
6396
|
+
}
|
|
6397
|
+
function lockedCopy(locked, origin) {
|
|
6398
|
+
if (locked === null || locked.source !== origin.repository) return null;
|
|
6399
|
+
const ref = locked.ref ?? null;
|
|
6400
|
+
if (origin.ref !== null && ref !== origin.ref) return null;
|
|
6401
|
+
if (locked.skillPath === null || locked.skillPath === void 0) return null;
|
|
6402
|
+
if (locked.skillFolderHash === null || locked.skillFolderHash === void 0) return null;
|
|
6403
|
+
const folder = posix.dirname(locked.skillPath);
|
|
6404
|
+
return {
|
|
6405
|
+
ref,
|
|
6406
|
+
directory: posix.dirname(folder),
|
|
6407
|
+
folder: posix.basename(folder),
|
|
6408
|
+
hash: locked.skillFolderHash
|
|
6409
|
+
};
|
|
6410
|
+
}
|
|
6411
|
+
async function setBehind(set, check) {
|
|
6412
|
+
const { origin } = set;
|
|
6413
|
+
if (origin.kind !== "github" || check.locked === null) return true;
|
|
6414
|
+
const copies = [];
|
|
6415
|
+
for (const skill of set.skills) {
|
|
6416
|
+
const copy = lockedCopy(check.locked.get(skill) ?? null, origin);
|
|
6417
|
+
if (copy === null) return true;
|
|
6418
|
+
copies.push(copy);
|
|
6419
|
+
}
|
|
6420
|
+
if (check.updates === "installed") return false;
|
|
6421
|
+
for (const copy of copies) {
|
|
6422
|
+
const current = await check.hashes(origin.repository, copy.ref, copy.directory);
|
|
6423
|
+
if (current !== null && current.get(copy.folder) !== copy.hash) return true;
|
|
6424
|
+
}
|
|
6425
|
+
return false;
|
|
6426
|
+
}
|
|
6427
|
+
function hasSkill(installed, skill, agent) {
|
|
6428
|
+
return installed.some((entry) => entry.name === skill && skillCoversAgent(entry, agent));
|
|
6429
|
+
}
|
|
6430
|
+
function coversAll(installed, skills, agents) {
|
|
6431
|
+
return agents.every((agent) => skills.every((skill) => hasSkill(installed, skill, agent)));
|
|
6432
|
+
}
|
|
6118
6433
|
const SOURCE_NOTES = {
|
|
6119
6434
|
override: "from --agents",
|
|
6120
6435
|
detected: "detected on this machine",
|
|
6121
|
-
chosen: "as chosen"
|
|
6436
|
+
chosen: "as chosen",
|
|
6437
|
+
installed: "updating the installed copies"
|
|
6122
6438
|
};
|
|
6123
6439
|
function agentChoice(agent) {
|
|
6124
6440
|
return {
|
|
@@ -6136,7 +6452,7 @@ async function askAgents(ctx, detected) {
|
|
|
6136
6452
|
});
|
|
6137
6453
|
return detected.filter((agent) => picked.includes(agent.id));
|
|
6138
6454
|
}
|
|
6139
|
-
async function selectAgents(ctx, all, options) {
|
|
6455
|
+
async function selectAgents(ctx, all, options, installedNow) {
|
|
6140
6456
|
switch (options.kind) {
|
|
6141
6457
|
case "detected": return {
|
|
6142
6458
|
agents: detectedOnly(all),
|
|
@@ -6155,6 +6471,10 @@ async function selectAgents(ctx, all, options) {
|
|
|
6155
6471
|
agents: [],
|
|
6156
6472
|
source: "detected"
|
|
6157
6473
|
};
|
|
6474
|
+
if (coversAll(await installedNow(), REQUIRED_SKILL_NAMES, detected)) return {
|
|
6475
|
+
agents: detected,
|
|
6476
|
+
source: "installed"
|
|
6477
|
+
};
|
|
6158
6478
|
return {
|
|
6159
6479
|
agents: await askAgents(ctx, detected),
|
|
6160
6480
|
source: "chosen"
|
|
@@ -6165,47 +6485,65 @@ async function selectAgents(ctx, all, options) {
|
|
|
6165
6485
|
function skipLine(all, source) {
|
|
6166
6486
|
switch (source) {
|
|
6167
6487
|
case "override":
|
|
6168
|
-
case "chosen":
|
|
6169
|
-
case "
|
|
6488
|
+
case "chosen":
|
|
6489
|
+
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$7}\` to install it later`;
|
|
6490
|
+
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
|
|
6170
6491
|
}
|
|
6171
6492
|
}
|
|
6172
|
-
function agentsSkillsStep(options, deps = hostDeps$
|
|
6493
|
+
function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
6494
|
+
let hashes = null;
|
|
6495
|
+
async function setCheck(resolved) {
|
|
6496
|
+
hashes ??= hashLookup(resolved.github);
|
|
6497
|
+
return {
|
|
6498
|
+
updates,
|
|
6499
|
+
locked: await readLockedSkills(resolved),
|
|
6500
|
+
hashes
|
|
6501
|
+
};
|
|
6502
|
+
}
|
|
6173
6503
|
return {
|
|
6174
|
-
id: STEP_ID$
|
|
6504
|
+
id: STEP_ID$7,
|
|
6175
6505
|
title: "Install the rde and data-app skills for your coding agents",
|
|
6176
6506
|
redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
|
|
6177
6507
|
async detect(ctx) {
|
|
6178
6508
|
const resolved = deps(ctx);
|
|
6179
6509
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6180
|
-
if (detected.length === 0) return ctx.state.steps[STEP_ID$
|
|
6181
|
-
|
|
6510
|
+
if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6511
|
+
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6512
|
+
const check = await setCheck(resolved);
|
|
6513
|
+
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
6514
|
+
return "done";
|
|
6182
6515
|
},
|
|
6183
6516
|
async run(ctx) {
|
|
6184
6517
|
const resolved = deps(ctx);
|
|
6518
|
+
const skills = skillsCli(resolved.runner);
|
|
6185
6519
|
const all = await detectAgents(resolved.probe);
|
|
6186
|
-
|
|
6520
|
+
let listing = null;
|
|
6521
|
+
const installedNow = () => listing ??= skills.list();
|
|
6522
|
+
const selection = await selectAgents(ctx, all, options, installedNow);
|
|
6187
6523
|
if (selection.agents.length === 0) {
|
|
6188
6524
|
ctx.ui.info(skipLine(all, selection.source));
|
|
6189
6525
|
return { outcome: "skipped" };
|
|
6190
6526
|
}
|
|
6191
|
-
const
|
|
6527
|
+
const before = await installedNow();
|
|
6528
|
+
const check = await setCheck(resolved);
|
|
6529
|
+
const pending = [];
|
|
6530
|
+
for (const set of skillSets(resolved)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
|
|
6531
|
+
const names = displayNames(selection.agents);
|
|
6532
|
+
if (pending.length === 0) {
|
|
6533
|
+
ctx.ui.success(`The rde and data-app skills are current for ${names}`);
|
|
6534
|
+
return { outcome: "completed" };
|
|
6535
|
+
}
|
|
6192
6536
|
const agents = selection.agents.map((agent) => agent.skillsCliName);
|
|
6193
|
-
const forWhom = `for ${
|
|
6194
|
-
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
await skills.add({
|
|
6204
|
-
source: resolved.skillSource,
|
|
6205
|
-
skills: RDE_SOURCE_SKILL_NAMES,
|
|
6206
|
-
agents,
|
|
6207
|
-
output: ctx.ui.childOutput
|
|
6208
|
-
});
|
|
6537
|
+
const forWhom = `for ${names} (${SOURCE_NOTES[selection.source]})`;
|
|
6538
|
+
for (const set of pending) {
|
|
6539
|
+
ctx.ui.info(`Installing the ${set.label} skills ${forWhom}${set.sourceNote}`);
|
|
6540
|
+
await skills.add({
|
|
6541
|
+
source: set.source,
|
|
6542
|
+
skills: set.skills,
|
|
6543
|
+
agents,
|
|
6544
|
+
output: ctx.ui.childOutput
|
|
6545
|
+
});
|
|
6546
|
+
}
|
|
6209
6547
|
const installed = await skills.list();
|
|
6210
6548
|
const missing = unlistedSkills(installed);
|
|
6211
6549
|
if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
|
|
@@ -6217,7 +6555,7 @@ function agentsSkillsStep(options, deps = hostDeps$10) {
|
|
|
6217
6555
|
}
|
|
6218
6556
|
//#endregion
|
|
6219
6557
|
//#region src/steps/databases.ts
|
|
6220
|
-
const STEP_ID$
|
|
6558
|
+
const STEP_ID$6 = "databases";
|
|
6221
6559
|
const SAMPLE_STANDS_IN = { request: "sample-stands-in" };
|
|
6222
6560
|
function hostDatabasesDeps(signal) {
|
|
6223
6561
|
return {
|
|
@@ -6226,7 +6564,7 @@ function hostDatabasesDeps(signal) {
|
|
|
6226
6564
|
now: () => /* @__PURE__ */ new Date()
|
|
6227
6565
|
};
|
|
6228
6566
|
}
|
|
6229
|
-
function isNotServingYet$
|
|
6567
|
+
function isNotServingYet$2(error) {
|
|
6230
6568
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
6231
6569
|
}
|
|
6232
6570
|
function requireApiKey$2(state, apiKey) {
|
|
@@ -6255,14 +6593,15 @@ function authenticatedClient(state, apiKey, deps) {
|
|
|
6255
6593
|
});
|
|
6256
6594
|
}
|
|
6257
6595
|
async function listDatabases(state, apiKey, deps) {
|
|
6258
|
-
const listed = await authenticatedClient(state, apiKey, deps).database.
|
|
6596
|
+
const listed = await authenticatedClient(state, apiKey, deps).database.listWithStubs();
|
|
6259
6597
|
const records = new Map(state.databases.map((record) => [record.id, record]));
|
|
6260
6598
|
const onServer = listed.data.map((database) => ({
|
|
6261
6599
|
id: database.id,
|
|
6262
6600
|
name: database.name,
|
|
6263
6601
|
engine: database.engine,
|
|
6264
6602
|
initialSyncStatus: database.initial_sync_status,
|
|
6265
|
-
createdAt: records.get(database.id)?.createdAt ?? null
|
|
6603
|
+
createdAt: records.get(database.id)?.createdAt ?? null,
|
|
6604
|
+
stub: isStub(database)
|
|
6266
6605
|
}));
|
|
6267
6606
|
const serverIds = new Set(listed.data.map((database) => database.id));
|
|
6268
6607
|
const missing = state.databases.filter((record) => !serverIds.has(record.id)).map((record) => ({
|
|
@@ -6270,24 +6609,27 @@ async function listDatabases(state, apiKey, deps) {
|
|
|
6270
6609
|
name: record.name,
|
|
6271
6610
|
engine: record.engine,
|
|
6272
6611
|
initialSyncStatus: null,
|
|
6273
|
-
createdAt: record.createdAt
|
|
6612
|
+
createdAt: record.createdAt,
|
|
6613
|
+
stub: false
|
|
6274
6614
|
}));
|
|
6275
6615
|
return [...onServer, ...missing];
|
|
6276
6616
|
}
|
|
6277
|
-
async function connectDatabases(ctx, deps,
|
|
6617
|
+
async function connectDatabases(ctx, deps, opening) {
|
|
6278
6618
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6279
6619
|
const client = authenticatedClient(ctx.state, ctx.secrets.apiKey, deps);
|
|
6280
|
-
const
|
|
6281
|
-
const
|
|
6282
|
-
|
|
6620
|
+
const engines = await client.setting.engines();
|
|
6621
|
+
const listed = await client.database.listWithStubs();
|
|
6622
|
+
const existing = listed.data.filter((database) => !isStub(database));
|
|
6623
|
+
if (existing.length > 0) ctx.ui.note(existingDatabaseLines(existing).join("\n"), "Connected databases");
|
|
6283
6624
|
const result = await runDatabaseFlow({
|
|
6284
|
-
|
|
6625
|
+
...opening,
|
|
6626
|
+
stubs: listed.data.filter(isStub),
|
|
6285
6627
|
prompts: ctx.prompts,
|
|
6286
6628
|
ui: ctx.ui,
|
|
6287
|
-
catalog: engineCatalog(
|
|
6629
|
+
catalog: engineCatalog(engines),
|
|
6288
6630
|
hostAlias: hostAliasOf(configured.runtime),
|
|
6289
6631
|
database: client.database,
|
|
6290
|
-
existingCount: existing.
|
|
6632
|
+
existingCount: existing.length,
|
|
6291
6633
|
readTextFile: deps.readTextFile,
|
|
6292
6634
|
onCreated: async (database) => {
|
|
6293
6635
|
const record = toRecord$1(database, deps.now().toISOString());
|
|
@@ -6297,27 +6639,34 @@ async function connectDatabases(ctx, deps, start) {
|
|
|
6297
6639
|
});
|
|
6298
6640
|
}
|
|
6299
6641
|
});
|
|
6642
|
+
if (result.stubsLeft.length > 0) ctx.ui.info(unconnectedStubsLine(result.stubsLeft));
|
|
6300
6643
|
return {
|
|
6301
|
-
existing: existing.
|
|
6644
|
+
existing: existing.length,
|
|
6302
6645
|
created: result.created.length,
|
|
6303
6646
|
added: result.added
|
|
6304
6647
|
};
|
|
6305
6648
|
}
|
|
6306
|
-
async function addDatabases(ctx, deps) {
|
|
6307
|
-
const connected = await connectDatabases(ctx, deps,
|
|
6649
|
+
async function addDatabases(ctx, deps, options) {
|
|
6650
|
+
const connected = await connectDatabases(ctx, deps, {
|
|
6651
|
+
start: "engine",
|
|
6652
|
+
...options
|
|
6653
|
+
});
|
|
6308
6654
|
return {
|
|
6309
6655
|
outcome: connected.existing + connected.created > 0 ? "completed" : "skipped",
|
|
6310
6656
|
result: { addedDatabases: connected.added }
|
|
6311
6657
|
};
|
|
6312
6658
|
}
|
|
6313
6659
|
async function askForOwnDatabase(ctx, deps) {
|
|
6314
|
-
const connected = await connectDatabases(ctx, deps,
|
|
6660
|
+
const connected = await connectDatabases(ctx, deps, {
|
|
6661
|
+
start: "connection-string",
|
|
6662
|
+
offerStubs: true
|
|
6663
|
+
});
|
|
6315
6664
|
return {
|
|
6316
6665
|
outcome: connected.created > 0 ? "completed" : "skipped",
|
|
6317
6666
|
result: { addedDatabases: connected.added }
|
|
6318
6667
|
};
|
|
6319
6668
|
}
|
|
6320
|
-
function hostDeps$
|
|
6669
|
+
function hostDeps$10(ctx) {
|
|
6321
6670
|
return {
|
|
6322
6671
|
...hostDatabasesDeps(ctx.signal),
|
|
6323
6672
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
@@ -6327,15 +6676,15 @@ function sampleDatabaseId(state) {
|
|
|
6327
6676
|
return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
|
|
6328
6677
|
}
|
|
6329
6678
|
function skippedBefore(state) {
|
|
6330
|
-
return state.steps[STEP_ID$
|
|
6679
|
+
return state.steps[STEP_ID$6]?.status === "skipped";
|
|
6331
6680
|
}
|
|
6332
6681
|
function ownDatabaseIds(state) {
|
|
6333
6682
|
const sampleId = sampleDatabaseId(state);
|
|
6334
6683
|
return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
|
|
6335
6684
|
}
|
|
6336
|
-
function databasesStep(options, deps = hostDeps$
|
|
6685
|
+
function databasesStep(options, deps = hostDeps$10) {
|
|
6337
6686
|
return {
|
|
6338
|
-
id: STEP_ID$
|
|
6687
|
+
id: STEP_ID$6,
|
|
6339
6688
|
title: "Connect your databases",
|
|
6340
6689
|
redoHint: "Connect another with `rde db add --url <connection string>`.",
|
|
6341
6690
|
async detect(ctx) {
|
|
@@ -6358,22 +6707,40 @@ function databasesStep(options, deps = hostDeps$9) {
|
|
|
6358
6707
|
try {
|
|
6359
6708
|
return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
|
|
6360
6709
|
} catch (error) {
|
|
6361
|
-
if (isNotServingYet$
|
|
6710
|
+
if (isNotServingYet$2(error) || isRejectedCredential(error)) return "todo";
|
|
6362
6711
|
throw error;
|
|
6363
6712
|
}
|
|
6364
6713
|
},
|
|
6365
6714
|
async run(ctx) {
|
|
6366
6715
|
switch (options.request) {
|
|
6367
6716
|
case "ask-once": return askForOwnDatabase(ctx, deps(ctx));
|
|
6368
|
-
case "named": return addDatabases(ctx, deps(ctx));
|
|
6717
|
+
case "named": return addDatabases(ctx, deps(ctx), { offerStubs: false });
|
|
6369
6718
|
case "sample-stands-in": return { outcome: "skipped" };
|
|
6370
6719
|
}
|
|
6371
6720
|
}
|
|
6372
6721
|
};
|
|
6373
6722
|
}
|
|
6374
6723
|
//#endregion
|
|
6724
|
+
//#region src/metabase/domain/npm-package.ts
|
|
6725
|
+
const NpmVersionManifest = z.object({ version: z.string() }).loose();
|
|
6726
|
+
//#endregion
|
|
6727
|
+
//#region src/metabase/npm-registry.ts
|
|
6728
|
+
const NPM_REGISTRY = "https://registry.npmjs.org";
|
|
6729
|
+
function npmRegistry(signal, options = {}) {
|
|
6730
|
+
const http = createHttp({
|
|
6731
|
+
baseUrl: NPM_REGISTRY,
|
|
6732
|
+
auth: NO_AUTH,
|
|
6733
|
+
signal,
|
|
6734
|
+
...options
|
|
6735
|
+
});
|
|
6736
|
+
return { async distTagVersion(packageName, tag) {
|
|
6737
|
+
const path = `/${encodeURIComponent(packageName)}/${encodeURIComponent(tag)}`;
|
|
6738
|
+
return (await http.requestJson("GET", path, { schema: NpmVersionManifest })).version;
|
|
6739
|
+
} };
|
|
6740
|
+
}
|
|
6741
|
+
//#endregion
|
|
6375
6742
|
//#region src/steps/install-mb.ts
|
|
6376
|
-
const STEP_ID$
|
|
6743
|
+
const STEP_ID$5 = "install-mb";
|
|
6377
6744
|
const MIN_MB_VERSION = "0.3.0";
|
|
6378
6745
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
6379
6746
|
function installedWith(scriptPath) {
|
|
@@ -6382,11 +6749,12 @@ function installedWith(scriptPath) {
|
|
|
6382
6749
|
function rdeInstalledWith() {
|
|
6383
6750
|
return installedWith(process.argv[1]);
|
|
6384
6751
|
}
|
|
6385
|
-
function hostDeps$
|
|
6752
|
+
function hostDeps$9(ctx) {
|
|
6386
6753
|
return {
|
|
6387
6754
|
runner: processRunner(ctx.signal),
|
|
6388
6755
|
installedWith: rdeInstalledWith(),
|
|
6389
|
-
locate: locateMbOnPath
|
|
6756
|
+
locate: locateMbOnPath,
|
|
6757
|
+
publishedVersion: () => npmRegistry(ctx.signal).distTagVersion(MB_PACKAGE, MB_DIST_TAG)
|
|
6390
6758
|
};
|
|
6391
6759
|
}
|
|
6392
6760
|
async function inspectMb(runner) {
|
|
@@ -6400,6 +6768,38 @@ async function inspectMb(runner) {
|
|
|
6400
6768
|
satisfies: semver.satisfies(version, MB_RANGE, { includePrerelease: true })
|
|
6401
6769
|
};
|
|
6402
6770
|
}
|
|
6771
|
+
const UNCHECKED = { kind: "unchecked" };
|
|
6772
|
+
async function publishedMb(deps) {
|
|
6773
|
+
let raw;
|
|
6774
|
+
try {
|
|
6775
|
+
raw = await deps.publishedVersion();
|
|
6776
|
+
} catch (error) {
|
|
6777
|
+
const reason = lookupFailure(error);
|
|
6778
|
+
if (reason === null) throw error;
|
|
6779
|
+
return {
|
|
6780
|
+
kind: "unknown",
|
|
6781
|
+
reason
|
|
6782
|
+
};
|
|
6783
|
+
}
|
|
6784
|
+
const version = semver.valid(raw);
|
|
6785
|
+
if (version === null) return {
|
|
6786
|
+
kind: "unknown",
|
|
6787
|
+
reason: `the registry names "${raw}" for ${MB_DIST_TAG}, which is not a version`
|
|
6788
|
+
};
|
|
6789
|
+
return {
|
|
6790
|
+
kind: "published",
|
|
6791
|
+
version
|
|
6792
|
+
};
|
|
6793
|
+
}
|
|
6794
|
+
function coreOf(version) {
|
|
6795
|
+
return `${semver.major(version)}.${semver.minor(version)}.${semver.patch(version)}`;
|
|
6796
|
+
}
|
|
6797
|
+
function updateTo(release, installed) {
|
|
6798
|
+
if (release.kind !== "published" || release.version === installed) return null;
|
|
6799
|
+
const order = semver.compare(coreOf(installed), coreOf(release.version));
|
|
6800
|
+
if (order === 0) return semver.prerelease(installed) === null ? null : release.version;
|
|
6801
|
+
return order < 0 ? release.version : null;
|
|
6802
|
+
}
|
|
6403
6803
|
function displayCommand(install) {
|
|
6404
6804
|
return [install.command, ...install.args].join(" ");
|
|
6405
6805
|
}
|
|
@@ -6498,27 +6898,66 @@ async function stillWrong(install, after, deps) {
|
|
|
6498
6898
|
`${stillWrongAdvice(diagnosis)}.`
|
|
6499
6899
|
].join(" "));
|
|
6500
6900
|
}
|
|
6501
|
-
function
|
|
6901
|
+
async function shadowedWarning(lead, deps) {
|
|
6902
|
+
const found = await inspectMbOnPath(deps.runner, deps.locate);
|
|
6903
|
+
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
6904
|
+
const advice = diagnosis.kind === "shadowed" ? ` ${shadowedHint(diagnosis)}.` : "";
|
|
6905
|
+
return `${lead} Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.${advice}`;
|
|
6906
|
+
}
|
|
6907
|
+
async function updateFor(release, installed, deps) {
|
|
6908
|
+
const version = updateTo(release, installed);
|
|
6909
|
+
if (version === null) return { kind: "current" };
|
|
6910
|
+
return (await inspectMbOnPath(deps.runner, deps.locate)).some((entry) => entry.version === version) ? {
|
|
6911
|
+
kind: "shadowed",
|
|
6912
|
+
version
|
|
6913
|
+
} : {
|
|
6914
|
+
kind: "update",
|
|
6915
|
+
version
|
|
6916
|
+
};
|
|
6917
|
+
}
|
|
6918
|
+
function installMbStep(updates, deps = hostDeps$9) {
|
|
6919
|
+
let published = null;
|
|
6920
|
+
function release(resolved) {
|
|
6921
|
+
if (updates === "installed") return Promise.resolve(UNCHECKED);
|
|
6922
|
+
published ??= publishedMb(resolved);
|
|
6923
|
+
return published;
|
|
6924
|
+
}
|
|
6502
6925
|
return {
|
|
6503
|
-
id: STEP_ID$
|
|
6926
|
+
id: STEP_ID$5,
|
|
6504
6927
|
title: "Install the Metabase CLI",
|
|
6505
6928
|
async detect(ctx) {
|
|
6506
|
-
const
|
|
6507
|
-
|
|
6929
|
+
const resolved = deps(ctx);
|
|
6930
|
+
const found = await inspectMb(resolved.runner);
|
|
6931
|
+
if (!found.installed || !found.satisfies) return "todo";
|
|
6932
|
+
return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
|
|
6508
6933
|
},
|
|
6509
6934
|
async run(ctx) {
|
|
6510
6935
|
const resolved = deps(ctx);
|
|
6511
6936
|
const before = await inspectMb(resolved.runner);
|
|
6937
|
+
const target = await release(resolved);
|
|
6512
6938
|
if (before.installed && before.satisfies) {
|
|
6513
|
-
ctx.ui.
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6939
|
+
if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
|
|
6940
|
+
const update = await updateFor(target, before.version, resolved);
|
|
6941
|
+
if (update.kind === "shadowed") {
|
|
6942
|
+
const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
|
|
6943
|
+
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
6944
|
+
}
|
|
6945
|
+
if (update.kind !== "update") {
|
|
6946
|
+
ctx.ui.success(`mb ${before.version} is installed`);
|
|
6947
|
+
return { outcome: "completed" };
|
|
6948
|
+
}
|
|
6949
|
+
ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
|
|
6950
|
+
} else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
6517
6951
|
const install = mbInstallCommand(resolved.installedWith);
|
|
6518
6952
|
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
6519
6953
|
await runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
|
|
6520
6954
|
const after = await inspectMb(resolved.runner);
|
|
6521
6955
|
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
6956
|
+
const stillNewer = updateTo(target, after.version);
|
|
6957
|
+
if (stillNewer !== null) {
|
|
6958
|
+
const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
|
|
6959
|
+
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
6960
|
+
}
|
|
6522
6961
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
6523
6962
|
return { outcome: "completed" };
|
|
6524
6963
|
}
|
|
@@ -6526,11 +6965,11 @@ function installMbStep(deps = hostDeps$8) {
|
|
|
6526
6965
|
}
|
|
6527
6966
|
//#endregion
|
|
6528
6967
|
//#region src/steps/license.ts
|
|
6529
|
-
const STEP_ID$
|
|
6530
|
-
const LICENSE_KEYS = { token: `${STEP_ID$
|
|
6968
|
+
const STEP_ID$4 = "license";
|
|
6969
|
+
const LICENSE_KEYS = { token: `${STEP_ID$4}.token` };
|
|
6531
6970
|
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
6532
6971
|
const NOT_FOUND_STATUS$2 = 404;
|
|
6533
|
-
function hostDeps$
|
|
6972
|
+
function hostDeps$8(ctx) {
|
|
6534
6973
|
return {
|
|
6535
6974
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
6536
6975
|
runner: processRunner(ctx.signal),
|
|
@@ -6553,13 +6992,13 @@ function activeLabel(status) {
|
|
|
6553
6992
|
const plan = status["plan-alias"];
|
|
6554
6993
|
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
6555
6994
|
}
|
|
6556
|
-
function licenseStep(deps = hostDeps$
|
|
6995
|
+
function licenseStep(deps = hostDeps$8) {
|
|
6557
6996
|
return {
|
|
6558
|
-
id: STEP_ID$
|
|
6997
|
+
id: STEP_ID$4,
|
|
6559
6998
|
title: "Activate your license",
|
|
6560
6999
|
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
6561
7000
|
async detect(ctx) {
|
|
6562
|
-
if (ctx.state.steps[STEP_ID$
|
|
7001
|
+
if (ctx.state.steps[STEP_ID$4]?.status === "skipped") return "not-applicable";
|
|
6563
7002
|
const configured = configuredRuntime(ctx.state);
|
|
6564
7003
|
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
6565
7004
|
const resolved = deps(ctx);
|
|
@@ -6650,7 +7089,7 @@ const PLATFORMS = {
|
|
|
6650
7089
|
javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
|
|
6651
7090
|
}
|
|
6652
7091
|
};
|
|
6653
|
-
function hostDeps$
|
|
7092
|
+
function hostDeps$7(ctx) {
|
|
6654
7093
|
const runner = processRunner(ctx.signal);
|
|
6655
7094
|
return {
|
|
6656
7095
|
platform: process.platform,
|
|
@@ -6772,7 +7211,7 @@ function checkLine(check) {
|
|
|
6772
7211
|
function checkLines(checks) {
|
|
6773
7212
|
return checks.map(checkLine);
|
|
6774
7213
|
}
|
|
6775
|
-
function preflightStep(deps = hostDeps$
|
|
7214
|
+
function preflightStep(deps = hostDeps$7) {
|
|
6776
7215
|
return {
|
|
6777
7216
|
id: "preflight",
|
|
6778
7217
|
title: "Check this machine",
|
|
@@ -6791,7 +7230,7 @@ function preflightStep(deps = hostDeps$6) {
|
|
|
6791
7230
|
}
|
|
6792
7231
|
//#endregion
|
|
6793
7232
|
//#region src/steps/runtime.ts
|
|
6794
|
-
const STEP_ID$
|
|
7233
|
+
const STEP_ID$3 = "runtime";
|
|
6795
7234
|
const MIN_PORT = 1;
|
|
6796
7235
|
const MAX_PORT = 65535;
|
|
6797
7236
|
const PORT_PATTERN = /^\d+$/;
|
|
@@ -6805,7 +7244,7 @@ const NO_RUNTIME_FLAGS = {
|
|
|
6805
7244
|
jar: null,
|
|
6806
7245
|
port: null
|
|
6807
7246
|
};
|
|
6808
|
-
function hostDeps$
|
|
7247
|
+
function hostDeps$6(ctx) {
|
|
6809
7248
|
return {
|
|
6810
7249
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
6811
7250
|
releases: releaseLookup(ctx.signal)
|
|
@@ -6934,9 +7373,9 @@ async function persistedRuntime(ctx, options, deps) {
|
|
|
6934
7373
|
});
|
|
6935
7374
|
return chosen;
|
|
6936
7375
|
}
|
|
6937
|
-
function runtimeStep(options, deps = hostDeps$
|
|
7376
|
+
function runtimeStep(options, deps = hostDeps$6) {
|
|
6938
7377
|
return {
|
|
6939
|
-
id: STEP_ID$
|
|
7378
|
+
id: STEP_ID$3,
|
|
6940
7379
|
title: "Install Metabase",
|
|
6941
7380
|
redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
|
|
6942
7381
|
async detect(ctx) {
|
|
@@ -6962,11 +7401,11 @@ function runtimeStep(options, deps = hostDeps$5) {
|
|
|
6962
7401
|
}
|
|
6963
7402
|
//#endregion
|
|
6964
7403
|
//#region src/steps/sample-database.ts
|
|
6965
|
-
const STEP_ID$
|
|
7404
|
+
const STEP_ID$2 = "sample-database";
|
|
6966
7405
|
const SAMPLE_DATABASE_DISPLAY_NAME = "Sample Database";
|
|
6967
7406
|
const POSTGRES_ENGINE = "postgres";
|
|
6968
7407
|
const NOT_FOUND_STATUS$1 = 404;
|
|
6969
|
-
function hostDeps$
|
|
7408
|
+
function hostDeps$5(ctx) {
|
|
6970
7409
|
return {
|
|
6971
7410
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
6972
7411
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -6974,7 +7413,7 @@ function hostDeps$4(ctx) {
|
|
|
6974
7413
|
now: () => /* @__PURE__ */ new Date()
|
|
6975
7414
|
};
|
|
6976
7415
|
}
|
|
6977
|
-
function isNotServingYet(error) {
|
|
7416
|
+
function isNotServingYet$1(error) {
|
|
6978
7417
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
6979
7418
|
}
|
|
6980
7419
|
function isNotFound$2(error) {
|
|
@@ -7117,11 +7556,11 @@ async function connectDatabase(ctx, choice, configured, client, deps) {
|
|
|
7117
7556
|
return database;
|
|
7118
7557
|
}
|
|
7119
7558
|
function usageLines(record) {
|
|
7120
|
-
return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms
|
|
7559
|
+
return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms can write to this database."].join("\n");
|
|
7121
7560
|
}
|
|
7122
|
-
function sampleDatabaseStep(deps = hostDeps$
|
|
7561
|
+
function sampleDatabaseStep(deps = hostDeps$5) {
|
|
7123
7562
|
return {
|
|
7124
|
-
id: STEP_ID$
|
|
7563
|
+
id: STEP_ID$2,
|
|
7125
7564
|
title: "Connect a sample database",
|
|
7126
7565
|
async detect(ctx) {
|
|
7127
7566
|
const resolved = deps(ctx);
|
|
@@ -7137,7 +7576,7 @@ function sampleDatabaseStep(deps = hostDeps$4) {
|
|
|
7137
7576
|
try {
|
|
7138
7577
|
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
7139
7578
|
} catch (error) {
|
|
7140
|
-
if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
|
|
7579
|
+
if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
|
|
7141
7580
|
throw error;
|
|
7142
7581
|
}
|
|
7143
7582
|
},
|
|
@@ -7149,16 +7588,13 @@ function sampleDatabaseStep(deps = hostDeps$4) {
|
|
|
7149
7588
|
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
7150
7589
|
const persisted = await persistedRecord(ctx, resolved);
|
|
7151
7590
|
const record = await installAndStart(ctx, sampleDatabaseContainer(persisted.record, resolved.runtime.docker, resolved.runtime.rdeHome), persisted.record, resolved);
|
|
7152
|
-
|
|
7591
|
+
await connectDatabase(ctx, {
|
|
7153
7592
|
...persisted,
|
|
7154
7593
|
record
|
|
7155
|
-
}
|
|
7156
|
-
const client = resolved.client(configured.url, {
|
|
7594
|
+
}, configured, resolved.client(configured.url, {
|
|
7157
7595
|
kind: "api-key",
|
|
7158
7596
|
key: apiKey
|
|
7159
|
-
});
|
|
7160
|
-
await connectDatabase(ctx, choice, configured, client, resolved);
|
|
7161
|
-
await client.setting.enableTransforms();
|
|
7597
|
+
}), resolved);
|
|
7162
7598
|
ctx.ui.note(usageLines(record), "Sample database");
|
|
7163
7599
|
return { outcome: "completed" };
|
|
7164
7600
|
}
|
|
@@ -7169,7 +7605,7 @@ const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
|
|
|
7169
7605
|
//#region src/steps/start-metabase.ts
|
|
7170
7606
|
const LOG_TAIL_ON_TIMEOUT = 30;
|
|
7171
7607
|
const PERCENT = 100;
|
|
7172
|
-
function hostDeps$
|
|
7608
|
+
function hostDeps$4(ctx) {
|
|
7173
7609
|
return {
|
|
7174
7610
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7175
7611
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -7213,7 +7649,7 @@ async function startRuntime(ctx, runtime, deps) {
|
|
|
7213
7649
|
throw error;
|
|
7214
7650
|
}
|
|
7215
7651
|
}
|
|
7216
|
-
function startMetabaseStep(deps = hostDeps$
|
|
7652
|
+
function startMetabaseStep(deps = hostDeps$4) {
|
|
7217
7653
|
return {
|
|
7218
7654
|
id: "start-metabase",
|
|
7219
7655
|
title: "Start Metabase",
|
|
@@ -7273,9 +7709,9 @@ function startMetabaseStep(deps = hostDeps$3) {
|
|
|
7273
7709
|
}
|
|
7274
7710
|
//#endregion
|
|
7275
7711
|
//#region src/steps/summary.ts
|
|
7276
|
-
const STEP_ID = "summary";
|
|
7712
|
+
const STEP_ID$1 = "summary";
|
|
7277
7713
|
const GIT_SYNC_STEP_ID = "git-sync";
|
|
7278
|
-
function hostDeps$
|
|
7714
|
+
function hostDeps$3(ctx) {
|
|
7279
7715
|
return {
|
|
7280
7716
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7281
7717
|
runner: processRunner(ctx.signal),
|
|
@@ -7299,14 +7735,21 @@ async function databaseLines$1(state, apiKey, deps) {
|
|
|
7299
7735
|
databases = (await deps.client(state.url, {
|
|
7300
7736
|
kind: "api-key",
|
|
7301
7737
|
key: apiKey
|
|
7302
|
-
}).database.
|
|
7738
|
+
}).database.listWithStubs()).data;
|
|
7303
7739
|
} catch (error) {
|
|
7304
7740
|
if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
|
|
7305
7741
|
if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
|
|
7306
7742
|
throw error;
|
|
7307
7743
|
}
|
|
7308
|
-
|
|
7309
|
-
|
|
7744
|
+
const connected = databases.filter((database) => !isStub(database));
|
|
7745
|
+
const stubs = databases.filter(isStub);
|
|
7746
|
+
const stubLines = stubs.length === 0 ? [] : [unconnectedStubsLine(stubs)];
|
|
7747
|
+
if (connected.length === 0) return ["Databases: none connected yet; `rde db add`", ...stubLines];
|
|
7748
|
+
return [
|
|
7749
|
+
"Databases:",
|
|
7750
|
+
...existingDatabaseLines(connected).map((line) => ` ${line}`),
|
|
7751
|
+
...stubLines
|
|
7752
|
+
];
|
|
7310
7753
|
}
|
|
7311
7754
|
function remoteLine(remote) {
|
|
7312
7755
|
switch (remote.kind) {
|
|
@@ -7353,9 +7796,9 @@ async function summaryLines(state, apiKey, deps) {
|
|
|
7353
7796
|
lines.push("Lifecycle: rde status (--json for scripts), rde stop, rde start");
|
|
7354
7797
|
return lines;
|
|
7355
7798
|
}
|
|
7356
|
-
function summaryStep(deps = hostDeps$
|
|
7799
|
+
function summaryStep(deps = hostDeps$3) {
|
|
7357
7800
|
return {
|
|
7358
|
-
id: STEP_ID,
|
|
7801
|
+
id: STEP_ID$1,
|
|
7359
7802
|
title: "Summary",
|
|
7360
7803
|
async detect() {
|
|
7361
7804
|
return "todo";
|
|
@@ -7368,21 +7811,76 @@ function summaryStep(deps = hostDeps$2) {
|
|
|
7368
7811
|
};
|
|
7369
7812
|
}
|
|
7370
7813
|
//#endregion
|
|
7814
|
+
//#region src/steps/transforms.ts
|
|
7815
|
+
const STEP_ID = "transforms";
|
|
7816
|
+
function hostDeps$2(ctx) {
|
|
7817
|
+
return {
|
|
7818
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7819
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
7820
|
+
};
|
|
7821
|
+
}
|
|
7822
|
+
function isNotServingYet(error) {
|
|
7823
|
+
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7824
|
+
}
|
|
7825
|
+
function transformsStep(deps = hostDeps$2) {
|
|
7826
|
+
return {
|
|
7827
|
+
id: STEP_ID,
|
|
7828
|
+
title: "Enable transforms",
|
|
7829
|
+
async detect(ctx) {
|
|
7830
|
+
const configured = configuredRuntime(ctx.state);
|
|
7831
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7832
|
+
const resolved = deps(ctx);
|
|
7833
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
7834
|
+
const client = resolved.client(configured.url, {
|
|
7835
|
+
kind: "api-key",
|
|
7836
|
+
key: ctx.secrets.apiKey
|
|
7837
|
+
});
|
|
7838
|
+
try {
|
|
7839
|
+
return await client.setting.transformsDecided() ? "done" : "todo";
|
|
7840
|
+
} catch (error) {
|
|
7841
|
+
if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
|
|
7842
|
+
throw error;
|
|
7843
|
+
}
|
|
7844
|
+
},
|
|
7845
|
+
async run(ctx) {
|
|
7846
|
+
const configured = requireConfiguredRuntime(ctx.state);
|
|
7847
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
7848
|
+
const client = deps(ctx).client(configured.url, {
|
|
7849
|
+
kind: "api-key",
|
|
7850
|
+
key: ctx.secrets.apiKey
|
|
7851
|
+
});
|
|
7852
|
+
if (await client.setting.transformsDecided()) {
|
|
7853
|
+
ctx.ui.info("Transforms are already set in Metabase; leaving them as they are");
|
|
7854
|
+
return { outcome: "completed" };
|
|
7855
|
+
}
|
|
7856
|
+
await client.setting.enableTransforms();
|
|
7857
|
+
ctx.ui.success("Transforms enabled");
|
|
7858
|
+
return { outcome: "completed" };
|
|
7859
|
+
}
|
|
7860
|
+
};
|
|
7861
|
+
}
|
|
7862
|
+
//#endregion
|
|
7371
7863
|
//#region src/steps/index.ts
|
|
7372
7864
|
const DEFAULT_STEP_OPTIONS = {
|
|
7373
7865
|
runtime: NO_RUNTIME_FLAGS,
|
|
7374
7866
|
agents: DETECT_AGENTS,
|
|
7375
7867
|
admin: DEFAULT_ADMIN,
|
|
7376
|
-
databases: SAMPLE_STANDS_IN
|
|
7868
|
+
databases: SAMPLE_STANDS_IN,
|
|
7869
|
+
updates: "published"
|
|
7870
|
+
};
|
|
7871
|
+
const LOCAL_STEP_OPTIONS = {
|
|
7872
|
+
...DEFAULT_STEP_OPTIONS,
|
|
7873
|
+
updates: "installed"
|
|
7377
7874
|
};
|
|
7378
7875
|
const STEP_BY_ID = {
|
|
7379
7876
|
preflight: () => preflightStep(),
|
|
7380
|
-
"install-mb": () => installMbStep(),
|
|
7381
|
-
"agents-skills": (options) => agentsSkillsStep(options.agents),
|
|
7877
|
+
"install-mb": (options) => installMbStep(options.updates),
|
|
7878
|
+
"agents-skills": (options) => agentsSkillsStep(options.agents, options.updates),
|
|
7382
7879
|
runtime: (options) => runtimeStep(options.runtime),
|
|
7383
7880
|
"start-metabase": () => startMetabaseStep(),
|
|
7384
7881
|
"admin-user": (options) => adminUserStep(options.admin),
|
|
7385
7882
|
"api-key": () => apiKeyStep(),
|
|
7883
|
+
transforms: () => transformsStep(),
|
|
7386
7884
|
"sample-database": () => sampleDatabaseStep(),
|
|
7387
7885
|
license: () => licenseStep(),
|
|
7388
7886
|
databases: (options) => databasesStep(options.databases),
|
|
@@ -7555,7 +8053,8 @@ function stepOptionsFrom(answers, run) {
|
|
|
7555
8053
|
},
|
|
7556
8054
|
agents: agentsOption(agents, run),
|
|
7557
8055
|
admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
|
|
7558
|
-
databases: { request: databasesRequest(databases, run) }
|
|
8056
|
+
databases: { request: databasesRequest(databases, run) },
|
|
8057
|
+
updates: DEFAULT_STEP_OPTIONS.updates
|
|
7559
8058
|
};
|
|
7560
8059
|
}
|
|
7561
8060
|
function keyedAnswers(answers) {
|
|
@@ -8161,6 +8660,7 @@ var SessionCache = class {
|
|
|
8161
8660
|
const CHECKLIST_MARK = {
|
|
8162
8661
|
done: "✓",
|
|
8163
8662
|
todo: "○",
|
|
8663
|
+
outdated: "↑",
|
|
8164
8664
|
"not-applicable": "–"
|
|
8165
8665
|
};
|
|
8166
8666
|
const SPINNER_FAILURE_CODE = 1;
|
|
@@ -8318,7 +8818,7 @@ const dbAdd = defineRdeCommand({
|
|
|
8318
8818
|
await addDatabases(await createWizardContext(name, {
|
|
8319
8819
|
signal: interruptSignal,
|
|
8320
8820
|
prompts
|
|
8321
|
-
}), hostDatabasesDeps(interruptSignal));
|
|
8821
|
+
}), hostDatabasesDeps(interruptSignal), { offerStubs: args.url === void 0 });
|
|
8322
8822
|
}
|
|
8323
8823
|
});
|
|
8324
8824
|
//#endregion
|
|
@@ -8401,7 +8901,7 @@ async function turnOf(entry, ctx, options) {
|
|
|
8401
8901
|
if (options.only !== void 0) return "not-selected";
|
|
8402
8902
|
if (entry.status === "done") return "done";
|
|
8403
8903
|
const status = await entry.step.detect(ctx);
|
|
8404
|
-
return status === "todo" ? "run" : status;
|
|
8904
|
+
return status === "todo" || status === "outdated" ? "run" : status;
|
|
8405
8905
|
}
|
|
8406
8906
|
function unusedMessage(step, turn, given) {
|
|
8407
8907
|
const hint = step.redoHint === void 0 ? "" : ` ${step.redoHint}`;
|
|
@@ -9049,7 +9549,7 @@ function withRef(source, ref) {
|
|
|
9049
9549
|
}
|
|
9050
9550
|
async function copyRef(entry, deps) {
|
|
9051
9551
|
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
9052
|
-
return
|
|
9552
|
+
return lockedSkill(path, await deps.skills.files.readJson(path), entry.name)?.ref ?? null;
|
|
9053
9553
|
}
|
|
9054
9554
|
async function judgeCopy(entry, origin, deps) {
|
|
9055
9555
|
const where = `${entry.scope} ${entry.path}`;
|
|
@@ -9311,7 +9811,7 @@ const doctor = defineRdeCommand({
|
|
|
9311
9811
|
signal: interruptSignal,
|
|
9312
9812
|
prompts
|
|
9313
9813
|
});
|
|
9314
|
-
if (await fixDoctor(report, initSteps(
|
|
9814
|
+
if (await fixDoctor(report, initSteps(LOCAL_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
|
|
9315
9815
|
}
|
|
9316
9816
|
print(report, args.json);
|
|
9317
9817
|
if (report.failed > 0) throw new Error(doctorSummary(report));
|
|
@@ -9360,7 +9860,8 @@ function applyDatabaseRecord(draft, record) {
|
|
|
9360
9860
|
const value = stringOf(record.answer);
|
|
9361
9861
|
const skippedConnection = record.key === DATABASE_KEYS.connection && value?.trim() === "";
|
|
9362
9862
|
const finished = parseDatabaseKey(record.key)?.name === "engine" && value === "finish";
|
|
9363
|
-
|
|
9863
|
+
const pickedNoStub = record.key === DATABASE_KEYS.stubs && record.answer.kind === "multiselect" && record.answer.values.length === 0;
|
|
9864
|
+
if (skippedConnection || finished || pickedNoStub) draft.databasesFinished = true;
|
|
9364
9865
|
}
|
|
9365
9866
|
function applyAdminRecord(draft, key, value) {
|
|
9366
9867
|
if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
|
|
@@ -9812,7 +10313,7 @@ const start = defineRdeCommand({
|
|
|
9812
10313
|
const SUMMARY_STEP_ID = "summary";
|
|
9813
10314
|
function hostStatusDeps(instanceName, signal) {
|
|
9814
10315
|
return {
|
|
9815
|
-
steps: initSteps(
|
|
10316
|
+
steps: initSteps(LOCAL_STEP_OPTIONS),
|
|
9816
10317
|
runtime: hostRuntimeDeps(instanceName, signal),
|
|
9817
10318
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
9818
10319
|
runner: processRunner(signal)
|
|
@@ -10737,7 +11238,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
10737
11238
|
const main = defineCommand({
|
|
10738
11239
|
meta: {
|
|
10739
11240
|
name: "rde",
|
|
10740
|
-
version: "0.1.
|
|
11241
|
+
version: "0.1.14",
|
|
10741
11242
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
10742
11243
|
},
|
|
10743
11244
|
subCommands: {
|