@metabase/rde 0.1.16 → 0.1.18
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 +61 -25
- package/dist/cli.mjs +1310 -452
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { pipeline } from "node:stream/promises";
|
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { connect, createServer } from "node:net";
|
|
13
13
|
import { confirm, intro, isCancel, log, multiselect, note, outro, password, select, spinner, text } from "@clack/prompts";
|
|
14
|
+
import { isDeepStrictEqual } from "node:util";
|
|
14
15
|
import semver from "semver";
|
|
15
16
|
//#region src/errors.ts
|
|
16
17
|
var AbortError = class extends Error {
|
|
@@ -33,6 +34,12 @@ var PortTakenError = class extends ConfigError {
|
|
|
33
34
|
this.port = port;
|
|
34
35
|
}
|
|
35
36
|
};
|
|
37
|
+
var UnsupportedMetabaseError = class extends ConfigError {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "UnsupportedMetabaseError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
36
43
|
var PromptUnavailableError = class extends ConfigError {
|
|
37
44
|
constructor(message) {
|
|
38
45
|
super(message);
|
|
@@ -79,6 +86,8 @@ var MetabaseApiError = class extends Error {
|
|
|
79
86
|
path;
|
|
80
87
|
errors;
|
|
81
88
|
reason;
|
|
89
|
+
code;
|
|
90
|
+
detail;
|
|
82
91
|
constructor(failure) {
|
|
83
92
|
super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
|
|
84
93
|
this.name = "MetabaseApiError";
|
|
@@ -86,6 +95,8 @@ var MetabaseApiError = class extends Error {
|
|
|
86
95
|
this.path = failure.path;
|
|
87
96
|
this.errors = failure.errors;
|
|
88
97
|
this.reason = failure.message;
|
|
98
|
+
this.code = failure.code;
|
|
99
|
+
this.detail = failure.detail;
|
|
89
100
|
}
|
|
90
101
|
};
|
|
91
102
|
const UNAUTHORIZED_STATUS = 401;
|
|
@@ -256,14 +267,15 @@ function withoutEscapes(text) {
|
|
|
256
267
|
const StepId = z.enum([
|
|
257
268
|
"preflight",
|
|
258
269
|
"install-mb",
|
|
259
|
-
"agents-skills",
|
|
260
270
|
"runtime",
|
|
261
271
|
"start-metabase",
|
|
272
|
+
"agents-skills",
|
|
262
273
|
"admin-user",
|
|
263
274
|
"api-key",
|
|
264
275
|
"transforms",
|
|
265
276
|
"license",
|
|
266
277
|
"git-sync",
|
|
278
|
+
"sync-scope",
|
|
267
279
|
"databases",
|
|
268
280
|
"summary"
|
|
269
281
|
]);
|
|
@@ -332,6 +344,9 @@ const REMOTE_SYNC_FEATURE = "remote-sync";
|
|
|
332
344
|
function hasRemoteSync(license) {
|
|
333
345
|
return license.activated && license.features.includes("remote-sync");
|
|
334
346
|
}
|
|
347
|
+
function hasLibrary(license) {
|
|
348
|
+
return license.activated && license.features.includes("library");
|
|
349
|
+
}
|
|
335
350
|
const DatabaseRecord = z.object({
|
|
336
351
|
id: z.int(),
|
|
337
352
|
name: z.string(),
|
|
@@ -354,7 +369,7 @@ const StepRecord = z.object({
|
|
|
354
369
|
completedAt: z.string()
|
|
355
370
|
});
|
|
356
371
|
const InstanceState = z.object({
|
|
357
|
-
schemaVersion: z.literal(
|
|
372
|
+
schemaVersion: z.literal(7),
|
|
358
373
|
name: z.string(),
|
|
359
374
|
runtime: RuntimeState.nullable(),
|
|
360
375
|
url: z.string().nullable(),
|
|
@@ -365,6 +380,7 @@ const InstanceState = z.object({
|
|
|
365
380
|
databases: z.array(DatabaseRecord),
|
|
366
381
|
gitRemote: GitRemoteState.nullable(),
|
|
367
382
|
movedFrom: z.string().nullable(),
|
|
383
|
+
syncRoot: z.int().nullable(),
|
|
368
384
|
steps: z.partialRecord(StepId, StepRecord)
|
|
369
385
|
});
|
|
370
386
|
function configuredRuntime(state) {
|
|
@@ -394,7 +410,7 @@ function mbCommand(profile) {
|
|
|
394
410
|
}
|
|
395
411
|
function emptyInstanceState(name) {
|
|
396
412
|
return {
|
|
397
|
-
schemaVersion:
|
|
413
|
+
schemaVersion: 7,
|
|
398
414
|
name,
|
|
399
415
|
runtime: null,
|
|
400
416
|
url: null,
|
|
@@ -405,6 +421,7 @@ function emptyInstanceState(name) {
|
|
|
405
421
|
databases: [],
|
|
406
422
|
gitRemote: null,
|
|
407
423
|
movedFrom: null,
|
|
424
|
+
syncRoot: null,
|
|
408
425
|
steps: {}
|
|
409
426
|
};
|
|
410
427
|
}
|
|
@@ -416,6 +433,7 @@ const V2 = 2;
|
|
|
416
433
|
const V3 = 3;
|
|
417
434
|
const V4 = 4;
|
|
418
435
|
const V5 = 5;
|
|
436
|
+
const V6 = 6;
|
|
419
437
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
420
438
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
421
439
|
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
@@ -427,7 +445,11 @@ const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
|
427
445
|
password: z.string()
|
|
428
446
|
})]);
|
|
429
447
|
const StateV4 = z.object({ schemaVersion: z.literal(V4) }).loose();
|
|
430
|
-
const StateV5 = z.object({
|
|
448
|
+
const StateV5 = z.object({
|
|
449
|
+
schemaVersion: z.literal(V5),
|
|
450
|
+
steps: z.record(z.string(), z.unknown())
|
|
451
|
+
}).loose();
|
|
452
|
+
const StateV6 = z.object({ schemaVersion: z.literal(V6) }).loose();
|
|
431
453
|
const StateV3 = z.object({
|
|
432
454
|
schemaVersion: z.literal(V3),
|
|
433
455
|
admin: AdminV3.nullable(),
|
|
@@ -454,10 +476,19 @@ function liftV4(raw) {
|
|
|
454
476
|
};
|
|
455
477
|
}
|
|
456
478
|
function liftV5(raw) {
|
|
457
|
-
const { sampleDatabase: _sampleDatabase, ...state } = StateV5.parse(raw);
|
|
479
|
+
const { sampleDatabase: _sampleDatabase, steps, ...state } = StateV5.parse(raw);
|
|
480
|
+
const { "sample-database": _sampleDatabaseStep, ...remainingSteps } = steps;
|
|
458
481
|
return {
|
|
459
482
|
...state,
|
|
460
|
-
schemaVersion:
|
|
483
|
+
schemaVersion: V6,
|
|
484
|
+
steps: remainingSteps
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
function liftV6(raw) {
|
|
488
|
+
return {
|
|
489
|
+
...StateV6.parse(raw),
|
|
490
|
+
schemaVersion: 7,
|
|
491
|
+
syncRoot: null
|
|
461
492
|
};
|
|
462
493
|
}
|
|
463
494
|
function adminV4(admin) {
|
|
@@ -498,24 +529,29 @@ function migrateInstanceState(raw) {
|
|
|
498
529
|
case V2: return migrateInstanceState(liftV2(raw));
|
|
499
530
|
case V3: return liftV3(raw);
|
|
500
531
|
case V4: return migrateInstanceState(liftV4(raw));
|
|
501
|
-
case V5: return
|
|
502
|
-
|
|
532
|
+
case V5: return migrateInstanceState(liftV5(raw));
|
|
533
|
+
case V6: return {
|
|
534
|
+
state: InstanceState.parse(liftV6(raw)),
|
|
503
535
|
movedSecrets: null
|
|
504
536
|
};
|
|
505
|
-
case
|
|
537
|
+
case 7: return {
|
|
506
538
|
state: InstanceState.parse(raw),
|
|
507
539
|
movedSecrets: null
|
|
508
540
|
};
|
|
509
|
-
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected
|
|
541
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 7)`);
|
|
510
542
|
}
|
|
511
543
|
}
|
|
512
544
|
//#endregion
|
|
513
545
|
//#region src/env.ts
|
|
514
546
|
const ENV_RDE_HOME = "RDE_HOME";
|
|
515
|
-
const
|
|
547
|
+
const ENV_RDE_DEV = "RDE_DEV";
|
|
516
548
|
function readEnv(name) {
|
|
517
549
|
return process.env[name];
|
|
518
550
|
}
|
|
551
|
+
function readSetEnv(name) {
|
|
552
|
+
const value = readEnv(name);
|
|
553
|
+
return value === void 0 || value.length === 0 ? null : value;
|
|
554
|
+
}
|
|
519
555
|
const ENV_PATH = "PATH";
|
|
520
556
|
const ENV_NO_COLOR = "NO_COLOR";
|
|
521
557
|
const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
|
|
@@ -523,6 +559,9 @@ const ENV_JAVA_HOME = "JAVA_HOME";
|
|
|
523
559
|
const ENV_MB_PROFILE = "MB_PROFILE";
|
|
524
560
|
const ENV_MB_URL = "MB_URL";
|
|
525
561
|
const ENV_MB_API_KEY = "MB_API_KEY";
|
|
562
|
+
function developmentMode() {
|
|
563
|
+
return readSetEnv(ENV_RDE_DEV) === "1";
|
|
564
|
+
}
|
|
526
565
|
function hostEnvironment() {
|
|
527
566
|
return { ...process.env };
|
|
528
567
|
}
|
|
@@ -541,8 +580,8 @@ const INSTANCE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
|
541
580
|
const INSTANCE_NAME_RULE = "letters, digits, - and _, starting with a letter or digit";
|
|
542
581
|
const HOME_TAG_LENGTH = 8;
|
|
543
582
|
function rdeHome() {
|
|
544
|
-
const override =
|
|
545
|
-
if (override !==
|
|
583
|
+
const override = readSetEnv(ENV_RDE_HOME);
|
|
584
|
+
if (override !== null) return resolve(override);
|
|
546
585
|
return join(homedir(), RDE_DIR_NAME);
|
|
547
586
|
}
|
|
548
587
|
function homeTag(home) {
|
|
@@ -814,8 +853,11 @@ const SESSION_HEADER = "X-Metabase-Session";
|
|
|
814
853
|
const API_KEY_HEADER = "X-Api-Key";
|
|
815
854
|
const JSON_CONTENT_TYPE = "application/json";
|
|
816
855
|
const SERVICE_UNAVAILABLE_STATUS = 503;
|
|
856
|
+
const NO_CONTENT_STATUS = 204;
|
|
817
857
|
const NO_AUTH = { kind: "none" };
|
|
818
858
|
const ErrorMessage = z.object({ message: z.string() }).loose();
|
|
859
|
+
const ErrorTitle = z.object({ error: z.string() }).loose();
|
|
860
|
+
const ErrorCode = z.object({ error_code: z.string() }).loose();
|
|
819
861
|
const ErrorFields = z.object({ errors: z.record(z.string(), z.unknown()) }).loose();
|
|
820
862
|
const ErrorText = z.string();
|
|
821
863
|
const ErrorTexts = z.array(z.string());
|
|
@@ -878,6 +920,8 @@ function serverMessage(body) {
|
|
|
878
920
|
const value = jsonValue(body);
|
|
879
921
|
const parsed = ErrorMessage.safeParse(value);
|
|
880
922
|
if (parsed.success) return parsed.data.message;
|
|
923
|
+
const title = ErrorTitle.safeParse(value);
|
|
924
|
+
if (title.success) return title.data.error;
|
|
881
925
|
const errors = serverErrors(body);
|
|
882
926
|
if (errors !== null && Object.keys(errors).length > 0) return Object.entries(errors).map(([name, problem]) => `${name}: ${problem}`).join("; ");
|
|
883
927
|
const plain = ErrorText.safeParse(value);
|
|
@@ -886,6 +930,10 @@ function serverMessage(body) {
|
|
|
886
930
|
if (body.json.parsed || text.length === 0) return `HTTP ${body.status}`;
|
|
887
931
|
return text;
|
|
888
932
|
}
|
|
933
|
+
function errorCode(body) {
|
|
934
|
+
const parsed = ErrorCode.safeParse(jsonValue(body));
|
|
935
|
+
return parsed.success ? parsed.data.error_code : null;
|
|
936
|
+
}
|
|
889
937
|
function failureFor(path, body) {
|
|
890
938
|
if (body.status === SERVICE_UNAVAILABLE_STATUS) {
|
|
891
939
|
const initializing = Initializing.safeParse(jsonValue(body));
|
|
@@ -895,7 +943,9 @@ function failureFor(path, body) {
|
|
|
895
943
|
status: body.status,
|
|
896
944
|
path,
|
|
897
945
|
message: serverMessage(body),
|
|
898
|
-
errors: serverErrors(body)
|
|
946
|
+
errors: serverErrors(body),
|
|
947
|
+
code: errorCode(body),
|
|
948
|
+
detail: body.json.parsed ? body.json.value : null
|
|
899
949
|
});
|
|
900
950
|
}
|
|
901
951
|
function shapeError(path, body, reason) {
|
|
@@ -903,7 +953,9 @@ function shapeError(path, body, reason) {
|
|
|
903
953
|
status: body.status,
|
|
904
954
|
path,
|
|
905
955
|
message: reason,
|
|
906
|
-
errors: null
|
|
956
|
+
errors: null,
|
|
957
|
+
code: null,
|
|
958
|
+
detail: null
|
|
907
959
|
});
|
|
908
960
|
}
|
|
909
961
|
function isTimeout(error) {
|
|
@@ -920,6 +972,7 @@ function lookupFailure(error) {
|
|
|
920
972
|
}
|
|
921
973
|
function createHttp(options) {
|
|
922
974
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
975
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
923
976
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
924
977
|
async function send(method, path, body) {
|
|
925
978
|
const url = `${baseUrl}${path}`;
|
|
@@ -930,7 +983,7 @@ function createHttp(options) {
|
|
|
930
983
|
const init = {
|
|
931
984
|
method,
|
|
932
985
|
headers,
|
|
933
|
-
signal: AbortSignal.any([options.signal, AbortSignal.timeout(
|
|
986
|
+
signal: AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)])
|
|
934
987
|
};
|
|
935
988
|
if (body !== void 0) {
|
|
936
989
|
headers["content-type"] = JSON_CONTENT_TYPE;
|
|
@@ -944,7 +997,7 @@ function createHttp(options) {
|
|
|
944
997
|
};
|
|
945
998
|
} catch (error) {
|
|
946
999
|
if (options.signal.aborted) throw new AbortError();
|
|
947
|
-
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${
|
|
1000
|
+
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${timeoutMs}ms`);
|
|
948
1001
|
throw new MetabaseUnreachableError(url, describeCause(error));
|
|
949
1002
|
}
|
|
950
1003
|
}
|
|
@@ -953,19 +1006,33 @@ function createHttp(options) {
|
|
|
953
1006
|
if (!received.ok) throw failureFor(path, received.body);
|
|
954
1007
|
return received.body;
|
|
955
1008
|
}
|
|
1009
|
+
function parseJson(path, body, schema) {
|
|
1010
|
+
if (!body.json.parsed) throw shapeError(path, body, "expected a JSON body");
|
|
1011
|
+
return parseShape(path, body, schema, body.json.value);
|
|
1012
|
+
}
|
|
1013
|
+
function parseShape(path, body, schema, value) {
|
|
1014
|
+
try {
|
|
1015
|
+
return schema.parse(value);
|
|
1016
|
+
} catch (error) {
|
|
1017
|
+
if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
|
|
1018
|
+
throw error;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
956
1021
|
return {
|
|
957
1022
|
async requestJson(method, path, request) {
|
|
1023
|
+
return parseJson(path, await requestBody(method, path, request.body), request.schema);
|
|
1024
|
+
},
|
|
1025
|
+
async requestOptionalJson(method, path, request) {
|
|
958
1026
|
const body = await requestBody(method, path, request.body);
|
|
959
|
-
if (
|
|
960
|
-
|
|
961
|
-
return request.schema.parse(body.json.value);
|
|
962
|
-
} catch (error) {
|
|
963
|
-
if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
|
|
964
|
-
throw error;
|
|
965
|
-
}
|
|
1027
|
+
if (body.status === NO_CONTENT_STATUS && body.text.length === 0) return null;
|
|
1028
|
+
return parseJson(path, body, request.schema);
|
|
966
1029
|
},
|
|
967
1030
|
async requestNoContent(method, path, request = {}) {
|
|
968
1031
|
await requestBody(method, path, request.body);
|
|
1032
|
+
},
|
|
1033
|
+
async requestText(method, path, request) {
|
|
1034
|
+
const body = await requestBody(method, path, request.body);
|
|
1035
|
+
return parseShape(path, body, request.schema, body.text);
|
|
969
1036
|
}
|
|
970
1037
|
};
|
|
971
1038
|
}
|
|
@@ -1056,6 +1123,48 @@ function apiKeyResource(http) {
|
|
|
1056
1123
|
regenerate
|
|
1057
1124
|
};
|
|
1058
1125
|
}
|
|
1126
|
+
const Collection = z.object({
|
|
1127
|
+
id: z.int(),
|
|
1128
|
+
name: z.string(),
|
|
1129
|
+
location: z.string(),
|
|
1130
|
+
type: z.string().nullable(),
|
|
1131
|
+
namespace: z.string().nullable(),
|
|
1132
|
+
personal_owner_id: z.int().nullable(),
|
|
1133
|
+
is_remote_synced: z.boolean()
|
|
1134
|
+
}).loose();
|
|
1135
|
+
const RootCollection = z.object({ id: z.literal("root") }).loose();
|
|
1136
|
+
const CollectionList = z.array(z.union([Collection, RootCollection]));
|
|
1137
|
+
const CollectionRef = z.object({
|
|
1138
|
+
id: z.int(),
|
|
1139
|
+
name: z.string()
|
|
1140
|
+
}).loose();
|
|
1141
|
+
//#endregion
|
|
1142
|
+
//#region src/metabase/resources/collection.ts
|
|
1143
|
+
function collectionResource(http) {
|
|
1144
|
+
/** GET /api/collection */
|
|
1145
|
+
async function list() {
|
|
1146
|
+
return http.requestJson("GET", "/api/collection", { schema: CollectionList });
|
|
1147
|
+
}
|
|
1148
|
+
/** POST /api/collection */
|
|
1149
|
+
async function create(request) {
|
|
1150
|
+
return http.requestJson("POST", "/api/collection", {
|
|
1151
|
+
body: {
|
|
1152
|
+
...request,
|
|
1153
|
+
parent_id: null
|
|
1154
|
+
},
|
|
1155
|
+
schema: CollectionRef
|
|
1156
|
+
});
|
|
1157
|
+
}
|
|
1158
|
+
/** POST /api/ee/library; 400 when the Library exists */
|
|
1159
|
+
async function createLibrary() {
|
|
1160
|
+
return http.requestJson("POST", "/api/ee/library", { schema: CollectionRef });
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
list,
|
|
1164
|
+
create,
|
|
1165
|
+
createLibrary
|
|
1166
|
+
};
|
|
1167
|
+
}
|
|
1059
1168
|
//#endregion
|
|
1060
1169
|
//#region src/metabase/domain/database.ts
|
|
1061
1170
|
const InitialSyncStatus = z.enum([
|
|
@@ -1148,7 +1257,9 @@ function permissionsGroupResource(http) {
|
|
|
1148
1257
|
}
|
|
1149
1258
|
return { list };
|
|
1150
1259
|
}
|
|
1151
|
-
|
|
1260
|
+
//#endregion
|
|
1261
|
+
//#region src/metabase/domain/remote-sync.ts
|
|
1262
|
+
const RemoteSyncType = z.enum(["read-only", "read-write"]);
|
|
1152
1263
|
const RemoteSyncSettingsResult = z.object({
|
|
1153
1264
|
success: z.boolean(),
|
|
1154
1265
|
task_id: z.int().optional()
|
|
@@ -1169,6 +1280,38 @@ const SyncTask = z.object({
|
|
|
1169
1280
|
status: SyncTaskStatus$1,
|
|
1170
1281
|
error_message: z.string().nullable().optional()
|
|
1171
1282
|
}).loose();
|
|
1283
|
+
const NamedCollection = z.object({
|
|
1284
|
+
id: z.int(),
|
|
1285
|
+
name: z.string()
|
|
1286
|
+
}).loose();
|
|
1287
|
+
const DependencyLocation = NamedCollection.nullable().optional();
|
|
1288
|
+
const DependencyItem = z.object({
|
|
1289
|
+
model: z.string(),
|
|
1290
|
+
id: z.int(),
|
|
1291
|
+
name: z.string().nullable()
|
|
1292
|
+
}).loose();
|
|
1293
|
+
const UnsyncedDependency = DependencyItem.extend({
|
|
1294
|
+
collection: DependencyLocation,
|
|
1295
|
+
used_by: z.array(DependencyItem)
|
|
1296
|
+
});
|
|
1297
|
+
const SyncRemedy = z.discriminatedUnion("type", [
|
|
1298
|
+
z.object({
|
|
1299
|
+
type: z.literal("collection"),
|
|
1300
|
+
collection: NamedCollection
|
|
1301
|
+
}).loose(),
|
|
1302
|
+
z.object({ type: z.literal("library") }).loose(),
|
|
1303
|
+
z.object({
|
|
1304
|
+
type: z.literal("none"),
|
|
1305
|
+
collection: DependencyLocation
|
|
1306
|
+
}).loose()
|
|
1307
|
+
]);
|
|
1308
|
+
const RequiredSync = z.object({
|
|
1309
|
+
remedy: SyncRemedy,
|
|
1310
|
+
syncable: z.boolean(),
|
|
1311
|
+
blocks: z.array(NamedCollection),
|
|
1312
|
+
dependencies: z.array(UnsyncedDependency)
|
|
1313
|
+
}).loose();
|
|
1314
|
+
const UnsyncedDependencies = z.object({ errors: z.object({ required: z.array(RequiredSync) }).loose() }).loose();
|
|
1172
1315
|
//#endregion
|
|
1173
1316
|
//#region src/metabase/resources/remote-sync.ts
|
|
1174
1317
|
function remoteSyncResource(http) {
|
|
@@ -1179,6 +1322,13 @@ function remoteSyncResource(http) {
|
|
|
1179
1322
|
schema: RemoteSyncSettingsResult
|
|
1180
1323
|
});
|
|
1181
1324
|
}
|
|
1325
|
+
/** PUT /api/ee/remote-sync/settings, without the git settings, which it leaves as they are */
|
|
1326
|
+
async function updateScope(scope) {
|
|
1327
|
+
return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
|
|
1328
|
+
body: scope,
|
|
1329
|
+
schema: RemoteSyncSettingsResult
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1182
1332
|
/** PUT /api/ee/remote-sync/settings with only `remote-sync-transforms`, which skips the repository check. */
|
|
1183
1333
|
async function syncTransforms() {
|
|
1184
1334
|
return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
|
|
@@ -1195,10 +1345,11 @@ function remoteSyncResource(http) {
|
|
|
1195
1345
|
}
|
|
1196
1346
|
/** GET /api/ee/remote-sync/current-task */
|
|
1197
1347
|
async function currentTask() {
|
|
1198
|
-
return http.
|
|
1348
|
+
return http.requestOptionalJson("GET", "/api/ee/remote-sync/current-task", { schema: SyncTask });
|
|
1199
1349
|
}
|
|
1200
1350
|
return {
|
|
1201
1351
|
updateSettings,
|
|
1352
|
+
updateScope,
|
|
1202
1353
|
syncTransforms,
|
|
1203
1354
|
testConnection,
|
|
1204
1355
|
currentTask
|
|
@@ -1271,6 +1422,7 @@ const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
|
|
|
1271
1422
|
const ENGINES_SETTING = "engines";
|
|
1272
1423
|
const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
|
|
1273
1424
|
const TRANSFORMS_SETUP_COMPLETE_SETTING = "transforms-setup-complete";
|
|
1425
|
+
const REMOTE_SYNC_TYPE_SETTING = "remote-sync-type";
|
|
1274
1426
|
function settingResource(http) {
|
|
1275
1427
|
/** PUT /api/setting/premium-embedding-token */
|
|
1276
1428
|
async function setPremiumEmbeddingToken(value) {
|
|
@@ -1288,11 +1440,16 @@ function settingResource(http) {
|
|
|
1288
1440
|
async function engines() {
|
|
1289
1441
|
return http.requestJson("GET", `/api/setting/${ENGINES_SETTING}`, { schema: Engines });
|
|
1290
1442
|
}
|
|
1443
|
+
/** GET /api/setting/remote-sync-type; Metabase answers its default, read-only, while unset. */
|
|
1444
|
+
async function remoteSyncType() {
|
|
1445
|
+
return http.requestText("GET", `/api/setting/${REMOTE_SYNC_TYPE_SETTING}`, { schema: RemoteSyncType });
|
|
1446
|
+
}
|
|
1291
1447
|
return {
|
|
1292
1448
|
setPremiumEmbeddingToken,
|
|
1293
1449
|
enableTransforms,
|
|
1294
1450
|
transformsDecided,
|
|
1295
|
-
engines
|
|
1451
|
+
engines,
|
|
1452
|
+
remoteSyncType
|
|
1296
1453
|
};
|
|
1297
1454
|
}
|
|
1298
1455
|
//#endregion
|
|
@@ -1362,6 +1519,7 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
|
1362
1519
|
tokenStatus: tokenStatusResource(http),
|
|
1363
1520
|
database: databaseResource(http),
|
|
1364
1521
|
remoteSync: remoteSyncResource(http),
|
|
1522
|
+
collection: collectionResource(http),
|
|
1365
1523
|
user: userResource(http)
|
|
1366
1524
|
};
|
|
1367
1525
|
}
|
|
@@ -2057,18 +2215,74 @@ async function detectAgents(probe) {
|
|
|
2057
2215
|
}
|
|
2058
2216
|
//#endregion
|
|
2059
2217
|
//#region src/metabase/domain/version-info.ts
|
|
2060
|
-
const
|
|
2218
|
+
const VersionEntry = z.object({ version: z.string() }).loose();
|
|
2219
|
+
const VersionInfo = z.object({ latest: VersionEntry }).loose();
|
|
2220
|
+
const VersionHistory = z.object({
|
|
2221
|
+
latest: VersionEntry,
|
|
2222
|
+
older: z.array(VersionEntry)
|
|
2223
|
+
}).loose();
|
|
2061
2224
|
//#endregion
|
|
2062
2225
|
//#region src/metabase/releases.ts
|
|
2063
2226
|
const DOWNLOADS_HOST = "https://downloads.metabase.com";
|
|
2064
2227
|
const VERSION_INFO_HOST = "https://static.metabase.com";
|
|
2065
2228
|
const VERSION_INFO_PATH = "/version-info-ee.json";
|
|
2066
2229
|
const JAR_FILE = "metabase.jar";
|
|
2067
|
-
const RELEASE_TAG$1 = /^v(?<edition>[01])\.\d
|
|
2230
|
+
const RELEASE_TAG$1 = /^v(?<edition>[01])\.(?<major>\d+)\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
|
|
2231
|
+
const MAJOR_LINE_TAG = /^v(?<edition>[01])\.(?<major>\d+)\.x$/;
|
|
2232
|
+
const FINAL_ENTERPRISE_RELEASE = /^v1\.\d+\.\d+(?:\.\d+)?$/;
|
|
2068
2233
|
const ENTERPRISE_EDITION = "1";
|
|
2234
|
+
const OPEN_SOURCE_EDITION = "0";
|
|
2235
|
+
const SNAPSHOT_SUFFIX = "-SNAPSHOT";
|
|
2069
2236
|
function isReleaseTag(value) {
|
|
2070
2237
|
return RELEASE_TAG$1.test(value);
|
|
2071
2238
|
}
|
|
2239
|
+
function isMajorLineTag(value) {
|
|
2240
|
+
return MAJOR_LINE_TAG.test(value);
|
|
2241
|
+
}
|
|
2242
|
+
function majorLineTag(major) {
|
|
2243
|
+
return `v${ENTERPRISE_EDITION}.${major}.x`;
|
|
2244
|
+
}
|
|
2245
|
+
function tagGroups(tag) {
|
|
2246
|
+
return (RELEASE_TAG$1.exec(tag) ?? MAJOR_LINE_TAG.exec(tag))?.groups;
|
|
2247
|
+
}
|
|
2248
|
+
function isOpenSourceRelease(version) {
|
|
2249
|
+
return tagGroups(version)?.["edition"] === OPEN_SOURCE_EDITION;
|
|
2250
|
+
}
|
|
2251
|
+
function isMasterBuild(version) {
|
|
2252
|
+
return !(isReleaseTag(version) || isMajorLineTag(version)) || version.endsWith(SNAPSHOT_SUFFIX);
|
|
2253
|
+
}
|
|
2254
|
+
function releaseMajor(version) {
|
|
2255
|
+
const major = tagGroups(version)?.["major"];
|
|
2256
|
+
return major === void 0 ? null : Number.parseInt(major, 10);
|
|
2257
|
+
}
|
|
2258
|
+
function releaseNumbers(version) {
|
|
2259
|
+
return version.slice(1).split(".").map((part) => Number.parseInt(part, 10));
|
|
2260
|
+
}
|
|
2261
|
+
function compareReleases(left, right) {
|
|
2262
|
+
const a = releaseNumbers(left);
|
|
2263
|
+
const b = releaseNumbers(right);
|
|
2264
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
2265
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
2266
|
+
if (difference !== 0) return difference;
|
|
2267
|
+
}
|
|
2268
|
+
return 0;
|
|
2269
|
+
}
|
|
2270
|
+
function supportedRelease(version) {
|
|
2271
|
+
if (!FINAL_ENTERPRISE_RELEASE.test(version)) return null;
|
|
2272
|
+
const major = releaseMajor(version);
|
|
2273
|
+
return major !== null && major >= 65 ? {
|
|
2274
|
+
version,
|
|
2275
|
+
major
|
|
2276
|
+
} : null;
|
|
2277
|
+
}
|
|
2278
|
+
function newestSupportedRelease(versions) {
|
|
2279
|
+
let newest = null;
|
|
2280
|
+
for (const version of versions) {
|
|
2281
|
+
const release = supportedRelease(version);
|
|
2282
|
+
if (release !== null && (newest === null || compareReleases(version, newest.version) > 0)) newest = release;
|
|
2283
|
+
}
|
|
2284
|
+
return newest;
|
|
2285
|
+
}
|
|
2072
2286
|
function jarDownloadUrl(version) {
|
|
2073
2287
|
const edition = RELEASE_TAG$1.exec(version)?.groups?.["edition"];
|
|
2074
2288
|
if (edition === void 0) throw new Error(`${version} is not a Metabase release tag`);
|
|
@@ -2081,9 +2295,15 @@ function releaseLookup(signal, options = {}) {
|
|
|
2081
2295
|
signal,
|
|
2082
2296
|
...options
|
|
2083
2297
|
});
|
|
2084
|
-
return {
|
|
2085
|
-
|
|
2086
|
-
|
|
2298
|
+
return {
|
|
2299
|
+
async latestVersion() {
|
|
2300
|
+
return (await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionInfo })).latest.version;
|
|
2301
|
+
},
|
|
2302
|
+
async newestSupportedRelease() {
|
|
2303
|
+
const info = await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionHistory });
|
|
2304
|
+
return newestSupportedRelease([info.latest, ...info.older].map((entry) => entry.version));
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2087
2307
|
}
|
|
2088
2308
|
function fileDownloader(signal, options = {}) {
|
|
2089
2309
|
return (url, destination, onProgress) => downloadFile(url, destination, {
|
|
@@ -2151,11 +2371,13 @@ const PortBinding = z.array(z.object({
|
|
|
2151
2371
|
HostIp: z.string(),
|
|
2152
2372
|
HostPort: z.string()
|
|
2153
2373
|
}).loose()).nullable();
|
|
2374
|
+
const EnvEntry = z.string().regex(/^[^=]+=/, "expected NAME=value");
|
|
2154
2375
|
const InspectedContainer = z.object({
|
|
2155
2376
|
State: z.object({ Running: z.boolean() }).loose(),
|
|
2156
2377
|
Config: z.object({
|
|
2157
2378
|
Image: z.string(),
|
|
2158
|
-
Labels: z.record(z.string(), z.string()).nullish()
|
|
2379
|
+
Labels: z.record(z.string(), z.string()).nullish(),
|
|
2380
|
+
Env: z.array(EnvEntry).nullish()
|
|
2159
2381
|
}).loose(),
|
|
2160
2382
|
HostConfig: z.object({ PortBindings: z.record(z.string(), PortBinding).nullable() }).loose(),
|
|
2161
2383
|
NetworkSettings: z.object({ Ports: z.record(z.string(), PortBinding).nullish() }).loose()
|
|
@@ -2186,6 +2408,14 @@ function portMappings(bindings) {
|
|
|
2186
2408
|
}
|
|
2187
2409
|
return mappings;
|
|
2188
2410
|
}
|
|
2411
|
+
function containerEnv(entries) {
|
|
2412
|
+
const env = {};
|
|
2413
|
+
for (const entry of entries) {
|
|
2414
|
+
const separator = entry.indexOf("=");
|
|
2415
|
+
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
2416
|
+
}
|
|
2417
|
+
return env;
|
|
2418
|
+
}
|
|
2189
2419
|
function createArgs(options) {
|
|
2190
2420
|
const args = [
|
|
2191
2421
|
"create",
|
|
@@ -2277,7 +2507,8 @@ function dockerTool(runner) {
|
|
|
2277
2507
|
image: labels?.[IMAGE_LABEL] ?? container.Config.Image,
|
|
2278
2508
|
ports: portMappings(container.HostConfig.PortBindings),
|
|
2279
2509
|
forwarded: portMappings(container.NetworkSettings.Ports ?? null),
|
|
2280
|
-
home: labels?.[HOME_LABEL] ?? null
|
|
2510
|
+
home: labels?.[HOME_LABEL] ?? null,
|
|
2511
|
+
env: containerEnv(container.Config.Env ?? [])
|
|
2281
2512
|
};
|
|
2282
2513
|
}
|
|
2283
2514
|
async function portClaims() {
|
|
@@ -2474,6 +2705,10 @@ const PLUGINS_DIR = "plugins";
|
|
|
2474
2705
|
const METABASE_STOP_GRACE_MS = 3e4;
|
|
2475
2706
|
const DB_DIR = "metabase.db";
|
|
2476
2707
|
const DB_FILE = "metabase.db";
|
|
2708
|
+
const STAGING_STORE_ENV = {
|
|
2709
|
+
MB_RUN_MODE: "e2e",
|
|
2710
|
+
MB_STORE_USE_STAGING: "true"
|
|
2711
|
+
};
|
|
2477
2712
|
function instanceUrl(port) {
|
|
2478
2713
|
return `http://localhost:${port}`;
|
|
2479
2714
|
}
|
|
@@ -2482,13 +2717,15 @@ function serverEnv(options) {
|
|
|
2482
2717
|
MB_DB_FILE: options.dbFile,
|
|
2483
2718
|
MB_PLUGINS_DIR: options.pluginsDir,
|
|
2484
2719
|
MB_SITE_URL: instanceUrl(options.port),
|
|
2485
|
-
|
|
2486
|
-
MB_STORE_USE_STAGING: "true",
|
|
2720
|
+
...developmentMode() ? STAGING_STORE_ENV : {},
|
|
2487
2721
|
MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all",
|
|
2488
2722
|
MB_LOAD_SAMPLE_CONTENT: "false",
|
|
2489
2723
|
MB_INSTALL_ANALYTICS_DATABASE: "false"
|
|
2490
2724
|
};
|
|
2491
2725
|
}
|
|
2726
|
+
function envDrift(actual, expected) {
|
|
2727
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(expected), ...Object.keys(STAGING_STORE_ENV)])].filter((name) => actual[name] !== expected[name]);
|
|
2728
|
+
}
|
|
2492
2729
|
function jettyEnv(port) {
|
|
2493
2730
|
return {
|
|
2494
2731
|
MB_JETTY_HOST: LOOPBACK_HOST,
|
|
@@ -2497,7 +2734,34 @@ function jettyEnv(port) {
|
|
|
2497
2734
|
}
|
|
2498
2735
|
//#endregion
|
|
2499
2736
|
//#region src/runtime/docker.ts
|
|
2500
|
-
const
|
|
2737
|
+
const RELEASE_REPOSITORY = "metabase/metabase-enterprise";
|
|
2738
|
+
const DEVELOPMENT_IMAGE = "metabase/metabase-enterprise-head:latest";
|
|
2739
|
+
const HEAD_SUFFIX = "-head";
|
|
2740
|
+
const METABASE_REPOSITORIES = /* @__PURE__ */ new Set([
|
|
2741
|
+
RELEASE_REPOSITORY,
|
|
2742
|
+
`${RELEASE_REPOSITORY}${HEAD_SUFFIX}`,
|
|
2743
|
+
"metabase/metabase"
|
|
2744
|
+
]);
|
|
2745
|
+
function majorLineImage(major) {
|
|
2746
|
+
return `${RELEASE_REPOSITORY}:${majorLineTag(major)}`;
|
|
2747
|
+
}
|
|
2748
|
+
function imageTag(ref) {
|
|
2749
|
+
const colon = ref.lastIndexOf(":");
|
|
2750
|
+
if (colon === -1 || colon < ref.lastIndexOf("/")) return null;
|
|
2751
|
+
return ref.slice(colon + 1);
|
|
2752
|
+
}
|
|
2753
|
+
function imageRepository(ref) {
|
|
2754
|
+
const digest = ref.indexOf("@");
|
|
2755
|
+
const named = digest === -1 ? ref : ref.slice(0, digest);
|
|
2756
|
+
const tag = imageTag(named);
|
|
2757
|
+
return tag === null ? named : named.slice(0, named.length - tag.length - 1);
|
|
2758
|
+
}
|
|
2759
|
+
function isMasterBuildImage(ref) {
|
|
2760
|
+
return imageRepository(ref).endsWith(HEAD_SUFFIX);
|
|
2761
|
+
}
|
|
2762
|
+
function isMetabaseImage(ref) {
|
|
2763
|
+
return METABASE_REPOSITORIES.has(imageRepository(ref));
|
|
2764
|
+
}
|
|
2501
2765
|
const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
|
|
2502
2766
|
const CONTAINER_PORT = 3e3;
|
|
2503
2767
|
const STOP = { graceMs: METABASE_STOP_GRACE_MS };
|
|
@@ -2512,6 +2776,16 @@ function onLoopback(mappings, port) {
|
|
|
2512
2776
|
function hostUserFor(platform, hostUser) {
|
|
2513
2777
|
return platform === "linux" ? hostUser : null;
|
|
2514
2778
|
}
|
|
2779
|
+
function containerServerEnv(port) {
|
|
2780
|
+
return serverEnv({
|
|
2781
|
+
dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
|
|
2782
|
+
pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
|
|
2783
|
+
port
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
function containerEnvDrift(container, state) {
|
|
2787
|
+
return envDrift(container.env, containerServerEnv(state.port));
|
|
2788
|
+
}
|
|
2515
2789
|
function dockerRuntime(state, deps) {
|
|
2516
2790
|
const { docker } = deps;
|
|
2517
2791
|
const url = instanceUrl(state.port);
|
|
@@ -2538,11 +2812,7 @@ function dockerRuntime(state, deps) {
|
|
|
2538
2812
|
source: deps.dataDir,
|
|
2539
2813
|
target: CONTAINER_DATA_DIR
|
|
2540
2814
|
},
|
|
2541
|
-
env:
|
|
2542
|
-
dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
|
|
2543
|
-
pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
|
|
2544
|
-
port: state.port
|
|
2545
|
-
}),
|
|
2815
|
+
env: containerServerEnv(state.port),
|
|
2546
2816
|
addHostGateway: deps.platform === "linux",
|
|
2547
2817
|
hostUser: hostUserFor(deps.platform, deps.hostUser)
|
|
2548
2818
|
};
|
|
@@ -2648,6 +2918,7 @@ function parseJarSpec(value) {
|
|
|
2648
2918
|
path: resolve(trimmed)
|
|
2649
2919
|
};
|
|
2650
2920
|
const tagged = trimmed.startsWith(TAG_PREFIX) ? trimmed : `${TAG_PREFIX}${trimmed}`;
|
|
2921
|
+
if (isOpenSourceRelease(tagged)) throw new ConfigError(`${trimmed} is a release of the open-source edition, which rde does not support; enterprise release tags start with v1`);
|
|
2651
2922
|
return isReleaseTag(tagged) ? {
|
|
2652
2923
|
kind: "release",
|
|
2653
2924
|
version: tagged
|
|
@@ -2904,12 +3175,12 @@ async function portClaims(state, runtime) {
|
|
|
2904
3175
|
}
|
|
2905
3176
|
//#endregion
|
|
2906
3177
|
//#region src/steps/admin-user.ts
|
|
2907
|
-
const STEP_ID$
|
|
3178
|
+
const STEP_ID$9 = "admin-user";
|
|
2908
3179
|
const ADMIN_USER_KEYS = {
|
|
2909
|
-
email: `${STEP_ID$
|
|
2910
|
-
password: `${STEP_ID$
|
|
2911
|
-
firstName: `${STEP_ID$
|
|
2912
|
-
lastName: `${STEP_ID$
|
|
3180
|
+
email: `${STEP_ID$9}.email`,
|
|
3181
|
+
password: `${STEP_ID$9}.password`,
|
|
3182
|
+
firstName: `${STEP_ID$9}.first-name`,
|
|
3183
|
+
lastName: `${STEP_ID$9}.last-name`
|
|
2913
3184
|
};
|
|
2914
3185
|
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
2915
3186
|
const GENERATED_PASSWORD_BYTES = 24;
|
|
@@ -2924,7 +3195,7 @@ function generatePassword(random) {
|
|
|
2924
3195
|
if (DIGIT.test(candidate)) return candidate;
|
|
2925
3196
|
}
|
|
2926
3197
|
}
|
|
2927
|
-
function hostDeps$
|
|
3198
|
+
function hostDeps$14(ctx) {
|
|
2928
3199
|
return {
|
|
2929
3200
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
2930
3201
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
@@ -3062,9 +3333,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
|
|
|
3062
3333
|
ctx.ui.success(`Admin user ${email} recorded`);
|
|
3063
3334
|
return "completed";
|
|
3064
3335
|
}
|
|
3065
|
-
function adminUserStep(options, deps = hostDeps$
|
|
3336
|
+
function adminUserStep(options, deps = hostDeps$14) {
|
|
3066
3337
|
return {
|
|
3067
|
-
id: STEP_ID$
|
|
3338
|
+
id: STEP_ID$9,
|
|
3068
3339
|
title: "Create the admin user",
|
|
3069
3340
|
redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
|
|
3070
3341
|
async detect(ctx) {
|
|
@@ -3134,7 +3405,7 @@ function adminUserStep(options, deps = hostDeps$13) {
|
|
|
3134
3405
|
//#region src/tools/mb.ts
|
|
3135
3406
|
const MB = "mb";
|
|
3136
3407
|
const MB_PACKAGE = "@metabase/cli";
|
|
3137
|
-
const MB_DIST_TAG = "
|
|
3408
|
+
const MB_DIST_TAG = "latest";
|
|
3138
3409
|
const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
|
|
3139
3410
|
const GLOBAL_INSTALL = {
|
|
3140
3411
|
npm: {
|
|
@@ -3265,15 +3536,11 @@ function mbRemoveCommand(binary) {
|
|
|
3265
3536
|
const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
|
|
3266
3537
|
return name === null ? null : REMOVE_COMMANDS[owner](name);
|
|
3267
3538
|
}
|
|
3268
|
-
function setValue(name) {
|
|
3269
|
-
const value = readEnv(name);
|
|
3270
|
-
return value === void 0 || value.length === 0 ? null : value;
|
|
3271
|
-
}
|
|
3272
3539
|
function mbEnvironment() {
|
|
3273
3540
|
return {
|
|
3274
|
-
profile:
|
|
3275
|
-
url:
|
|
3276
|
-
apiKeySet:
|
|
3541
|
+
profile: readSetEnv(ENV_MB_PROFILE),
|
|
3542
|
+
url: readSetEnv(ENV_MB_URL),
|
|
3543
|
+
apiKeySet: readSetEnv(ENV_MB_API_KEY) !== null
|
|
3277
3544
|
};
|
|
3278
3545
|
}
|
|
3279
3546
|
const ProbedUser = z.object({
|
|
@@ -3329,10 +3596,15 @@ const GitSyncTask = z.object({
|
|
|
3329
3596
|
progress: z.number().nullable(),
|
|
3330
3597
|
error_message: z.string().nullable().optional()
|
|
3331
3598
|
}).loose();
|
|
3599
|
+
const SyncedCollection = z.object({
|
|
3600
|
+
id: z.int(),
|
|
3601
|
+
name: z.string()
|
|
3602
|
+
}).loose();
|
|
3332
3603
|
const GitSyncStatus = z.object({
|
|
3333
3604
|
branch: z.string().nullable(),
|
|
3334
3605
|
is_dirty: z.boolean(),
|
|
3335
|
-
current_task: GitSyncTask.nullable()
|
|
3606
|
+
current_task: GitSyncTask.nullable(),
|
|
3607
|
+
synced_collections: z.array(SyncedCollection)
|
|
3336
3608
|
}).loose();
|
|
3337
3609
|
const GitSyncOutcome = z.object({
|
|
3338
3610
|
message: z.string().nullable(),
|
|
@@ -3344,6 +3616,9 @@ const RemoteChanges = z.object({
|
|
|
3344
3616
|
remote_version: z.string().nullable(),
|
|
3345
3617
|
local_version: z.string().nullable()
|
|
3346
3618
|
}).loose();
|
|
3619
|
+
function remoteMoved(changes) {
|
|
3620
|
+
return changes.has_changes && changes.local_version !== null;
|
|
3621
|
+
}
|
|
3347
3622
|
function comparableUrl(url) {
|
|
3348
3623
|
const parsed = new URL(url);
|
|
3349
3624
|
return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
@@ -3479,14 +3754,14 @@ function mbTool(runner) {
|
|
|
3479
3754
|
}
|
|
3480
3755
|
//#endregion
|
|
3481
3756
|
//#region src/steps/api-key.ts
|
|
3482
|
-
const STEP_ID$
|
|
3757
|
+
const STEP_ID$8 = "api-key";
|
|
3483
3758
|
const API_KEY_KEYS = {
|
|
3484
|
-
email: `${STEP_ID$
|
|
3485
|
-
password: `${STEP_ID$
|
|
3486
|
-
profile: `${STEP_ID$
|
|
3759
|
+
email: `${STEP_ID$8}.email`,
|
|
3760
|
+
password: `${STEP_ID$8}.password`,
|
|
3761
|
+
profile: `${STEP_ID$8}.profile`
|
|
3487
3762
|
};
|
|
3488
3763
|
const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
3489
|
-
function hostDeps$
|
|
3764
|
+
function hostDeps$13(ctx) {
|
|
3490
3765
|
return {
|
|
3491
3766
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3492
3767
|
runner: processRunner(ctx.signal),
|
|
@@ -3547,7 +3822,7 @@ async function authenticates(client) {
|
|
|
3547
3822
|
}
|
|
3548
3823
|
}
|
|
3549
3824
|
async function forgetCompletion(ctx) {
|
|
3550
|
-
const { [STEP_ID$
|
|
3825
|
+
const { [STEP_ID$8]: record, ...steps } = ctx.state.steps;
|
|
3551
3826
|
if (record !== void 0) await ctx.saveState({
|
|
3552
3827
|
...ctx.state,
|
|
3553
3828
|
steps
|
|
@@ -3624,15 +3899,15 @@ function usageNote(profile) {
|
|
|
3624
3899
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
3625
3900
|
return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
|
|
3626
3901
|
}
|
|
3627
|
-
function apiKeyStep(deps = hostDeps$
|
|
3902
|
+
function apiKeyStep(deps = hostDeps$13) {
|
|
3628
3903
|
return {
|
|
3629
|
-
id: STEP_ID$
|
|
3904
|
+
id: STEP_ID$8,
|
|
3630
3905
|
title: "Connect the Metabase CLI",
|
|
3631
3906
|
redoHint: "`rde status` shows the mb profile it uses.",
|
|
3632
3907
|
async detect(ctx) {
|
|
3633
3908
|
const configured = configuredRuntime(ctx.state);
|
|
3634
3909
|
const apiKey = ctx.secrets.apiKey;
|
|
3635
|
-
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$
|
|
3910
|
+
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
|
|
3636
3911
|
const resolved = deps(ctx);
|
|
3637
3912
|
if (!await resolved.instanceRunning()) return "todo";
|
|
3638
3913
|
if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
|
|
@@ -5577,102 +5852,289 @@ function gitTool(runner) {
|
|
|
5577
5852
|
};
|
|
5578
5853
|
}
|
|
5579
5854
|
//#endregion
|
|
5580
|
-
//#region src/steps/
|
|
5581
|
-
const STEP_ID$
|
|
5582
|
-
const
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
|
|
5586
|
-
|
|
5587
|
-
|
|
5588
|
-
|
|
5589
|
-
const
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
const README_NAME = "README.md";
|
|
5594
|
-
const README_CONTENT = "# Metabase content\n\nMetabase content synced by rde.\n";
|
|
5595
|
-
const INITIAL_COMMIT_MESSAGE = "Initial commit";
|
|
5596
|
-
const GITIGNORE_NAME = ".gitignore";
|
|
5597
|
-
const IGNORED_PATHS = [
|
|
5598
|
-
".env",
|
|
5599
|
-
".env.local",
|
|
5600
|
-
".env.*.local",
|
|
5601
|
-
"node_modules/",
|
|
5602
|
-
".vite/",
|
|
5603
|
-
".scratch/",
|
|
5604
|
-
".DS_Store"
|
|
5605
|
-
];
|
|
5606
|
-
const HTTPS_PREFIX = "https://";
|
|
5607
|
-
const HOME_PREFIX = "~/";
|
|
5608
|
-
const BAD_REQUEST_STATUS = 400;
|
|
5609
|
-
const TASK_POLL_INTERVAL_MS = 1e3;
|
|
5610
|
-
const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
5611
|
-
"successful",
|
|
5612
|
-
"errored",
|
|
5613
|
-
"cancelled",
|
|
5614
|
-
"timed-out",
|
|
5615
|
-
"conflict"
|
|
5616
|
-
]);
|
|
5617
|
-
const SYNC_MODE_SETTINGS = {
|
|
5618
|
-
"remote-sync-type": "read-write",
|
|
5619
|
-
"remote-sync-transforms": true
|
|
5855
|
+
//#region src/steps/license.ts
|
|
5856
|
+
const STEP_ID$7 = "license";
|
|
5857
|
+
const LICENSE_KEYS = { token: `${STEP_ID$7}.token` };
|
|
5858
|
+
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
5859
|
+
const NOT_FOUND_STATUS$1 = 404;
|
|
5860
|
+
const TOKEN_REFUSED_STATUS = 400;
|
|
5861
|
+
const STORE_UNREACHABLE_STATUS = 503;
|
|
5862
|
+
const TOKEN_CHECK_HOST = "token-check.metabase.com";
|
|
5863
|
+
const ACTIVATION_FAILED = "License activation failed";
|
|
5864
|
+
const LICENSED_FEATURES = {
|
|
5865
|
+
[REMOTE_SYNC_FEATURE]: "remote sync",
|
|
5866
|
+
"data-apps": "data apps",
|
|
5867
|
+
"transforms-testing": "transform tests"
|
|
5620
5868
|
};
|
|
5621
|
-
|
|
5869
|
+
const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
|
|
5870
|
+
function licensedFeatureCoverage(features) {
|
|
5871
|
+
const entries = Object.entries(LICENSED_FEATURES);
|
|
5872
|
+
return {
|
|
5873
|
+
included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
|
|
5874
|
+
missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
|
|
5875
|
+
};
|
|
5876
|
+
}
|
|
5877
|
+
function listOf(names, type) {
|
|
5878
|
+
return new Intl.ListFormat("en", { type }).format(names);
|
|
5879
|
+
}
|
|
5880
|
+
function hostDeps$12(ctx) {
|
|
5622
5881
|
return {
|
|
5623
|
-
runner: processRunner(ctx.signal),
|
|
5624
5882
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5625
|
-
|
|
5626
|
-
|
|
5627
|
-
|
|
5883
|
+
runner: processRunner(ctx.signal),
|
|
5884
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
5885
|
+
terminal: stdinIsTty()
|
|
5628
5886
|
};
|
|
5629
5887
|
}
|
|
5630
|
-
|
|
5631
|
-
|
|
5632
|
-
|
|
5633
|
-
hostedUrl: null,
|
|
5634
|
-
workdir: null
|
|
5635
|
-
};
|
|
5636
|
-
function expandHome(path) {
|
|
5637
|
-
return path.startsWith(HOME_PREFIX) ? join(homedir(), path.slice(2)) : path;
|
|
5888
|
+
function isNoTokenYet(error) {
|
|
5889
|
+
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
5890
|
+
return isMetabaseNotServing(error);
|
|
5638
5891
|
}
|
|
5639
|
-
|
|
5640
|
-
|
|
5892
|
+
function validateToken(value) {
|
|
5893
|
+
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
5894
|
+
return "A license token is 64 hexadecimal characters";
|
|
5895
|
+
}
|
|
5896
|
+
function requireApiKey$1(ctx) {
|
|
5897
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
5898
|
+
return ctx.secrets.apiKey;
|
|
5899
|
+
}
|
|
5900
|
+
function licenseOf(status) {
|
|
5901
|
+
if (!status.valid) return { activated: false };
|
|
5902
|
+
return {
|
|
5903
|
+
activated: true,
|
|
5904
|
+
features: status.features,
|
|
5905
|
+
validThru: status["valid-thru"] ?? null
|
|
5906
|
+
};
|
|
5907
|
+
}
|
|
5908
|
+
async function liveLicense(client) {
|
|
5641
5909
|
try {
|
|
5642
|
-
|
|
5910
|
+
return licenseOf(await client.tokenStatus.get());
|
|
5643
5911
|
} catch (error) {
|
|
5644
|
-
if (error instanceof
|
|
5912
|
+
if (error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1) return { activated: false };
|
|
5645
5913
|
throw error;
|
|
5646
5914
|
}
|
|
5647
|
-
if (!stat.isDirectory()) return "file";
|
|
5648
|
-
return (await promises.readdir(path)).length === 0 ? "empty" : "populated";
|
|
5649
|
-
}
|
|
5650
|
-
async function isCloneTarget(path) {
|
|
5651
|
-
const state = await directoryState(path);
|
|
5652
|
-
return state === "missing" || state === "empty";
|
|
5653
5915
|
}
|
|
5654
|
-
async function
|
|
5655
|
-
|
|
5656
|
-
|
|
5657
|
-
|
|
5658
|
-
|
|
5659
|
-
|
|
5660
|
-
|
|
5661
|
-
case "populated": return await run.git.isRepo(path) ? null : `${path} is not empty and not a git repository; pick an empty directory or a repository`;
|
|
5662
|
-
}
|
|
5916
|
+
async function recordLiveLicense(ctx, client) {
|
|
5917
|
+
const live = await liveLicense(client);
|
|
5918
|
+
if (!isDeepStrictEqual(live, ctx.state.license)) await ctx.saveState({
|
|
5919
|
+
...ctx.state,
|
|
5920
|
+
license: live
|
|
5921
|
+
});
|
|
5922
|
+
return live;
|
|
5663
5923
|
}
|
|
5664
|
-
|
|
5665
|
-
|
|
5924
|
+
function activeLabel(status) {
|
|
5925
|
+
const plan = status["plan-alias"];
|
|
5926
|
+
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
5666
5927
|
}
|
|
5667
|
-
|
|
5668
|
-
|
|
5669
|
-
|
|
5670
|
-
|
|
5671
|
-
|
|
5672
|
-
case "populated": return await isCloneOf(run, path, url) ? null : `${path} is not empty; pick an empty directory, or use an existing repository`;
|
|
5673
|
-
}
|
|
5928
|
+
function refusalOf(error) {
|
|
5929
|
+
if (!(error instanceof MetabaseApiError)) return null;
|
|
5930
|
+
if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
|
|
5931
|
+
if (error.status === STORE_UNREACHABLE_STATUS) return new ConfigError(`The instance could not reach ${TOKEN_CHECK_HOST} to check the license token; it needs outbound network access`);
|
|
5932
|
+
return null;
|
|
5674
5933
|
}
|
|
5675
|
-
async function
|
|
5934
|
+
async function activate(client, token) {
|
|
5935
|
+
try {
|
|
5936
|
+
await client.setting.setPremiumEmbeddingToken(token);
|
|
5937
|
+
} catch (error) {
|
|
5938
|
+
const refusal = refusalOf(error);
|
|
5939
|
+
if (refusal === null) throw error;
|
|
5940
|
+
return {
|
|
5941
|
+
kind: "refused",
|
|
5942
|
+
error: refusal
|
|
5943
|
+
};
|
|
5944
|
+
}
|
|
5945
|
+
const status = await client.tokenStatus.get();
|
|
5946
|
+
if (!status.valid) return {
|
|
5947
|
+
kind: "refused",
|
|
5948
|
+
error: new ConfigError(`Metabase reports the token as ${status.status}`)
|
|
5949
|
+
};
|
|
5950
|
+
return {
|
|
5951
|
+
kind: "activated",
|
|
5952
|
+
status
|
|
5953
|
+
};
|
|
5954
|
+
}
|
|
5955
|
+
async function activateAskedToken(ctx, client, terminal) {
|
|
5956
|
+
const token = await ctx.prompts.password({
|
|
5957
|
+
key: LICENSE_KEYS.token,
|
|
5958
|
+
message: "Paste your Metabase license token (Enter to skip)",
|
|
5959
|
+
validate: validateToken
|
|
5960
|
+
});
|
|
5961
|
+
if (token.length === 0) return null;
|
|
5962
|
+
const spinner = ctx.ui.spinner("Activating the license…");
|
|
5963
|
+
let activation;
|
|
5964
|
+
try {
|
|
5965
|
+
activation = await activate(client, token);
|
|
5966
|
+
} catch (error) {
|
|
5967
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
5968
|
+
throw error;
|
|
5969
|
+
}
|
|
5970
|
+
if (activation.kind === "activated") {
|
|
5971
|
+
spinner.stop(activeLabel(activation.status));
|
|
5972
|
+
ctx.session.licenseToken.set(token);
|
|
5973
|
+
return activation.status;
|
|
5974
|
+
}
|
|
5975
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
5976
|
+
if (!terminal) throw activation.error;
|
|
5977
|
+
ctx.ui.warn(activation.error.message);
|
|
5978
|
+
return activateAskedToken(ctx, client, terminal);
|
|
5979
|
+
}
|
|
5980
|
+
function reportFeatures(ctx, features) {
|
|
5981
|
+
const { missing } = licensedFeatureCoverage(features);
|
|
5982
|
+
const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
|
|
5983
|
+
if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
|
|
5984
|
+
if (missing.length > 0) {
|
|
5985
|
+
const gitNote = remoteSync ? "" : "; the git step will be skipped";
|
|
5986
|
+
ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
function licenseStep(deps = hostDeps$12) {
|
|
5990
|
+
return {
|
|
5991
|
+
id: STEP_ID$7,
|
|
5992
|
+
title: "Activate your license",
|
|
5993
|
+
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
5994
|
+
async detect(ctx) {
|
|
5995
|
+
if (ctx.state.steps[STEP_ID$7]?.status === "skipped") return "not-applicable";
|
|
5996
|
+
const configured = configuredRuntime(ctx.state);
|
|
5997
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
5998
|
+
const resolved = deps(ctx);
|
|
5999
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
6000
|
+
const client = resolved.client(configured.url, {
|
|
6001
|
+
kind: "api-key",
|
|
6002
|
+
key: ctx.secrets.apiKey
|
|
6003
|
+
});
|
|
6004
|
+
try {
|
|
6005
|
+
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
6006
|
+
} catch (error) {
|
|
6007
|
+
if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
|
|
6008
|
+
throw error;
|
|
6009
|
+
}
|
|
6010
|
+
},
|
|
6011
|
+
async run(ctx) {
|
|
6012
|
+
const configured = requireConfiguredRuntime(ctx.state);
|
|
6013
|
+
const apiKey = requireApiKey$1(ctx);
|
|
6014
|
+
const resolved = deps(ctx);
|
|
6015
|
+
const client = resolved.client(configured.url, {
|
|
6016
|
+
kind: "api-key",
|
|
6017
|
+
key: apiKey
|
|
6018
|
+
});
|
|
6019
|
+
const status = await activateAskedToken(ctx, client, resolved.terminal);
|
|
6020
|
+
if (status === null) {
|
|
6021
|
+
await recordLiveLicense(ctx, client);
|
|
6022
|
+
const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
|
|
6023
|
+
ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
|
|
6024
|
+
return { outcome: "skipped" };
|
|
6025
|
+
}
|
|
6026
|
+
await ctx.saveState({
|
|
6027
|
+
...ctx.state,
|
|
6028
|
+
license: licenseOf(status)
|
|
6029
|
+
});
|
|
6030
|
+
const mb = mbTool(resolved.runner);
|
|
6031
|
+
if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
|
|
6032
|
+
url: configured.url,
|
|
6033
|
+
profile: ctx.state.mbProfile,
|
|
6034
|
+
apiKey
|
|
6035
|
+
});
|
|
6036
|
+
reportFeatures(ctx, status.features);
|
|
6037
|
+
return { outcome: "completed" };
|
|
6038
|
+
}
|
|
6039
|
+
};
|
|
6040
|
+
}
|
|
6041
|
+
//#endregion
|
|
6042
|
+
//#region src/steps/git-sync.ts
|
|
6043
|
+
const STEP_ID$6 = "git-sync";
|
|
6044
|
+
const GIT_SYNC_KEYS = {
|
|
6045
|
+
location: `${STEP_ID$6}.location`,
|
|
6046
|
+
path: `${STEP_ID$6}.path`,
|
|
6047
|
+
remote: `${STEP_ID$6}.remote`,
|
|
6048
|
+
url: `${STEP_ID$6}.url`,
|
|
6049
|
+
token: `${STEP_ID$6}.token`
|
|
6050
|
+
};
|
|
6051
|
+
const BARE_REPO_NAME = "content.git";
|
|
6052
|
+
const DEFAULT_BRANCH = "main";
|
|
6053
|
+
const ORIGIN = "origin";
|
|
6054
|
+
const FALLBACK_REMOTE_NAME = "rde-remote";
|
|
6055
|
+
const README_NAME = "README.md";
|
|
6056
|
+
const README_CONTENT = "# Metabase content\n\nMetabase content synced by rde.\n";
|
|
6057
|
+
const INITIAL_COMMIT_MESSAGE = "Initial commit";
|
|
6058
|
+
const GITIGNORE_NAME = ".gitignore";
|
|
6059
|
+
const IGNORED_PATHS = [
|
|
6060
|
+
".env",
|
|
6061
|
+
".env.local",
|
|
6062
|
+
".env.*.local",
|
|
6063
|
+
"node_modules/",
|
|
6064
|
+
".vite/",
|
|
6065
|
+
".scratch/",
|
|
6066
|
+
".DS_Store"
|
|
6067
|
+
];
|
|
6068
|
+
const HTTPS_PREFIX = "https://";
|
|
6069
|
+
const HOME_PREFIX = "~/";
|
|
6070
|
+
const BAD_REQUEST_STATUS = 400;
|
|
6071
|
+
const TASK_POLL_INTERVAL_MS = 1e3;
|
|
6072
|
+
const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
6073
|
+
"successful",
|
|
6074
|
+
"errored",
|
|
6075
|
+
"cancelled",
|
|
6076
|
+
"timed-out",
|
|
6077
|
+
"conflict"
|
|
6078
|
+
]);
|
|
6079
|
+
const SYNC_MODE_SETTINGS = {
|
|
6080
|
+
"remote-sync-type": "read-write",
|
|
6081
|
+
"remote-sync-transforms": true
|
|
6082
|
+
};
|
|
6083
|
+
function hostDeps$11(ctx) {
|
|
6084
|
+
return {
|
|
6085
|
+
runner: processRunner(ctx.signal),
|
|
6086
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
6087
|
+
cwd: process.cwd(),
|
|
6088
|
+
dataDir: instanceDataDir(ctx.state.name),
|
|
6089
|
+
taskPollIntervalMs: TASK_POLL_INTERVAL_MS
|
|
6090
|
+
};
|
|
6091
|
+
}
|
|
6092
|
+
const SKIP_GIT_SYNC = "skip";
|
|
6093
|
+
const FRESH = {
|
|
6094
|
+
remoteKind: null,
|
|
6095
|
+
hostedUrl: null,
|
|
6096
|
+
workdir: null
|
|
6097
|
+
};
|
|
6098
|
+
function expandHome(path) {
|
|
6099
|
+
return path.startsWith(HOME_PREFIX) ? join(homedir(), path.slice(2)) : path;
|
|
6100
|
+
}
|
|
6101
|
+
async function directoryState(path) {
|
|
6102
|
+
let stat;
|
|
6103
|
+
try {
|
|
6104
|
+
stat = await promises.stat(path);
|
|
6105
|
+
} catch (error) {
|
|
6106
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return "missing";
|
|
6107
|
+
throw error;
|
|
6108
|
+
}
|
|
6109
|
+
if (!stat.isDirectory()) return "file";
|
|
6110
|
+
return (await promises.readdir(path)).length === 0 ? "empty" : "populated";
|
|
6111
|
+
}
|
|
6112
|
+
async function isCloneTarget(path) {
|
|
6113
|
+
const state = await directoryState(path);
|
|
6114
|
+
return state === "missing" || state === "empty";
|
|
6115
|
+
}
|
|
6116
|
+
async function initTargetProblem(run, path) {
|
|
6117
|
+
switch (await directoryState(path)) {
|
|
6118
|
+
case "missing":
|
|
6119
|
+
await promises.mkdir(path, { recursive: true });
|
|
6120
|
+
return null;
|
|
6121
|
+
case "file": return `${path} is not a directory`;
|
|
6122
|
+
case "empty": return null;
|
|
6123
|
+
case "populated": return await run.git.isRepo(path) ? null : `${path} is not empty and not a git repository; pick an empty directory or a repository`;
|
|
6124
|
+
}
|
|
6125
|
+
}
|
|
6126
|
+
async function isCloneOf(run, path, url) {
|
|
6127
|
+
return await run.git.isRepo(path) && await run.git.remoteUrl(path, ORIGIN) === url;
|
|
6128
|
+
}
|
|
6129
|
+
async function cloneTargetProblem(run, path, url) {
|
|
6130
|
+
switch (await directoryState(path)) {
|
|
6131
|
+
case "missing":
|
|
6132
|
+
case "empty": return null;
|
|
6133
|
+
case "file": return `${path} is not a directory`;
|
|
6134
|
+
case "populated": return await isCloneOf(run, path, url) ? null : `${path} is not empty; pick an empty directory, or use an existing repository`;
|
|
6135
|
+
}
|
|
6136
|
+
}
|
|
6137
|
+
async function existingRepoProblem(run, path) {
|
|
5676
6138
|
return await run.git.isRepo(path) ? null : `${path} is not a git repository`;
|
|
5677
6139
|
}
|
|
5678
6140
|
const FRESH_WORKDIRS = {
|
|
@@ -6055,6 +6517,9 @@ function configureRemote(run, runtime, model) {
|
|
|
6055
6517
|
function isMbUnavailable(error) {
|
|
6056
6518
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
6057
6519
|
}
|
|
6520
|
+
function remoteSyncGap(license) {
|
|
6521
|
+
return license.activated ? "Remote sync needs a license with the remote-sync feature, and this instance's license lacks it" : "Remote sync needs a license, and this instance has no valid one";
|
|
6522
|
+
}
|
|
6058
6523
|
async function runGitSync(ctx, deps) {
|
|
6059
6524
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6060
6525
|
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
@@ -6069,6 +6534,8 @@ async function runGitSync(ctx, deps) {
|
|
|
6069
6534
|
}),
|
|
6070
6535
|
instance
|
|
6071
6536
|
};
|
|
6537
|
+
const license = await recordLiveLicense(ctx, run.client);
|
|
6538
|
+
if (!hasRemoteSync(license)) throw new ConfigError(`${remoteSyncGap(license)}; \`rde init --from license\` activates one`);
|
|
6072
6539
|
const flow = await runFlow({
|
|
6073
6540
|
remote: remoteScreen(run),
|
|
6074
6541
|
hosted: hostedScreen(run),
|
|
@@ -6140,11 +6607,9 @@ function githubContents(signal, options = {}) {
|
|
|
6140
6607
|
//#region src/tools/skills-cli.ts
|
|
6141
6608
|
const NPX = "npx";
|
|
6142
6609
|
const SKILLS_PACKAGE = "skills@latest";
|
|
6143
|
-
const
|
|
6144
|
-
const RDE_SOURCE_SKILL_NAMES = ["rde",
|
|
6610
|
+
const RDE_DATA_APPS_SKILL_NAME = "rde-data-apps";
|
|
6611
|
+
const RDE_SOURCE_SKILL_NAMES = ["rde", RDE_DATA_APPS_SKILL_NAME];
|
|
6145
6612
|
const DATA_APP_REPOSITORY = "metabase/metabase";
|
|
6146
|
-
const DATA_APP_BRANCH = "master";
|
|
6147
|
-
const DATA_APP_SKILLS_SOURCE = `${DATA_APP_REPOSITORY}/skills#${DATA_APP_BRANCH}`;
|
|
6148
6613
|
const DATA_APP_SKILL_NAMES = [
|
|
6149
6614
|
"metabase-data-app-setup",
|
|
6150
6615
|
"metabase-data-app-routing",
|
|
@@ -6153,8 +6618,7 @@ const DATA_APP_SKILL_NAMES = [
|
|
|
6153
6618
|
"metabase-data-app-migrate"
|
|
6154
6619
|
];
|
|
6155
6620
|
function rdeSkillSource() {
|
|
6156
|
-
|
|
6157
|
-
return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
|
|
6621
|
+
return readSetEnv("RDE_SKILL_SOURCE") ?? "https://github.com/metabase/agent-skills/tree/main";
|
|
6158
6622
|
}
|
|
6159
6623
|
const InstalledSkill = z.object({
|
|
6160
6624
|
name: z.string(),
|
|
@@ -6180,8 +6644,7 @@ function projectSkillLockPath(project) {
|
|
|
6180
6644
|
return join(project, PROJECT_LOCK_FILE);
|
|
6181
6645
|
}
|
|
6182
6646
|
function xdgStateHome() {
|
|
6183
|
-
|
|
6184
|
-
return value === void 0 || value.length === 0 ? null : value;
|
|
6647
|
+
return readSetEnv(ENV_XDG_STATE_HOME);
|
|
6185
6648
|
}
|
|
6186
6649
|
function lockedSkill(path, raw, skill) {
|
|
6187
6650
|
if (raw === null) return null;
|
|
@@ -6193,11 +6656,16 @@ const hostSkillFiles = {
|
|
|
6193
6656
|
readJson: readJsonFile,
|
|
6194
6657
|
readText: readTextFile
|
|
6195
6658
|
};
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6659
|
+
function dataAppSkillsSource(ref) {
|
|
6660
|
+
return `${DATA_APP_REPOSITORY}/skills#${ref}`;
|
|
6661
|
+
}
|
|
6662
|
+
function dataAppSkillsOrigin(ref) {
|
|
6663
|
+
return {
|
|
6664
|
+
kind: "github",
|
|
6665
|
+
repository: DATA_APP_REPOSITORY,
|
|
6666
|
+
ref
|
|
6667
|
+
};
|
|
6668
|
+
}
|
|
6201
6669
|
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
6202
6670
|
function skillOrigin(source, cwd) {
|
|
6203
6671
|
const github = GITHUB_URL.exec(source);
|
|
@@ -6283,6 +6751,7 @@ const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
|
|
|
6283
6751
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6284
6752
|
const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
|
|
6285
6753
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6754
|
+
const ASK_AGENTS = { kind: "ask" };
|
|
6286
6755
|
function hostDeps$10(ctx) {
|
|
6287
6756
|
return {
|
|
6288
6757
|
runner: processRunner(ctx.signal),
|
|
@@ -6307,9 +6776,6 @@ function skillCoversAgent(entry, agent) {
|
|
|
6307
6776
|
function coversAny(entry, agents) {
|
|
6308
6777
|
return agents.some((agent) => skillCoversAgent(entry, agent));
|
|
6309
6778
|
}
|
|
6310
|
-
async function rdeSkillEntries(runner) {
|
|
6311
|
-
return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
|
|
6312
|
-
}
|
|
6313
6779
|
function requiredEntries(installed) {
|
|
6314
6780
|
return installed.filter((entry) => REQUIRED_SKILL_NAMES.includes(entry.name));
|
|
6315
6781
|
}
|
|
@@ -6326,12 +6792,26 @@ function coveredAgentNames(entry, agents) {
|
|
|
6326
6792
|
function installedLines(entries, agents) {
|
|
6327
6793
|
return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
|
|
6328
6794
|
}
|
|
6329
|
-
|
|
6795
|
+
const DEVELOPMENT_BRANCH = "master";
|
|
6796
|
+
function releaseBranch(major) {
|
|
6797
|
+
return `release-x.${major}.x`;
|
|
6798
|
+
}
|
|
6799
|
+
function dataAppSkillsRef(metabaseVersion, development) {
|
|
6800
|
+
if (metabaseVersion === null) return releaseBranch(65);
|
|
6801
|
+
if (development && isMasterBuild(metabaseVersion)) return DEVELOPMENT_BRANCH;
|
|
6802
|
+
const major = releaseMajor(metabaseVersion) ?? 65;
|
|
6803
|
+
return releaseBranch(Math.max(major, 65));
|
|
6804
|
+
}
|
|
6805
|
+
function wantedDataAppRef(metabaseVersion) {
|
|
6806
|
+
return dataAppSkillsRef(metabaseVersion, developmentMode());
|
|
6807
|
+
}
|
|
6808
|
+
function skillSets(deps, metabaseVersion) {
|
|
6809
|
+
const dataAppRef = wantedDataAppRef(metabaseVersion);
|
|
6330
6810
|
return [{
|
|
6331
6811
|
label: "data-app",
|
|
6332
|
-
source:
|
|
6333
|
-
sourceNote:
|
|
6334
|
-
origin:
|
|
6812
|
+
source: dataAppSkillsSource(dataAppRef),
|
|
6813
|
+
sourceNote: ` from ${dataAppRef}`,
|
|
6814
|
+
origin: dataAppSkillsOrigin(dataAppRef),
|
|
6335
6815
|
skills: DATA_APP_SKILL_NAMES
|
|
6336
6816
|
}, {
|
|
6337
6817
|
label: "rde",
|
|
@@ -6486,7 +6966,7 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
|
6486
6966
|
if (detected.length === 0) return ctx.state.steps[STEP_ID$5]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6487
6967
|
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6488
6968
|
const check = await setCheck(resolved);
|
|
6489
|
-
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
6969
|
+
for (const set of skillSets(resolved, ctx.state.version)) if (await setBehind(set, check)) return "outdated";
|
|
6490
6970
|
return "done";
|
|
6491
6971
|
},
|
|
6492
6972
|
async run(ctx) {
|
|
@@ -6503,7 +6983,7 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
|
6503
6983
|
const before = await installedNow();
|
|
6504
6984
|
const check = await setCheck(resolved);
|
|
6505
6985
|
const pending = [];
|
|
6506
|
-
for (const set of skillSets(resolved)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
|
|
6986
|
+
for (const set of skillSets(resolved, ctx.state.version)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
|
|
6507
6987
|
const names = displayNames(selection.agents);
|
|
6508
6988
|
if (pending.length === 0) {
|
|
6509
6989
|
ctx.ui.success(`The rde and data-app skills are current for ${names}`);
|
|
@@ -6542,7 +7022,7 @@ function hostDatabasesDeps(signal) {
|
|
|
6542
7022
|
now: () => /* @__PURE__ */ new Date()
|
|
6543
7023
|
};
|
|
6544
7024
|
}
|
|
6545
|
-
function requireApiKey
|
|
7025
|
+
function requireApiKey(state, apiKey) {
|
|
6546
7026
|
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
6547
7027
|
return apiKey;
|
|
6548
7028
|
}
|
|
@@ -6564,7 +7044,7 @@ function authenticatedClient(state, apiKey, deps) {
|
|
|
6564
7044
|
const configured = requireConfiguredRuntime(state);
|
|
6565
7045
|
return deps.client(configured.url, {
|
|
6566
7046
|
kind: "api-key",
|
|
6567
|
-
key: requireApiKey
|
|
7047
|
+
key: requireApiKey(state, apiKey)
|
|
6568
7048
|
});
|
|
6569
7049
|
}
|
|
6570
7050
|
async function listDatabases(state, apiKey, deps) {
|
|
@@ -6772,6 +7252,33 @@ function updateTo(release, installed) {
|
|
|
6772
7252
|
function displayCommand(install) {
|
|
6773
7253
|
return [install.command, ...install.args].join(" ");
|
|
6774
7254
|
}
|
|
7255
|
+
const NPM_EACCES_EXIT = 243;
|
|
7256
|
+
const NPM_PERMISSION_TEXT = [
|
|
7257
|
+
"EACCES",
|
|
7258
|
+
"EPERM",
|
|
7259
|
+
"might be a permissions issue"
|
|
7260
|
+
];
|
|
7261
|
+
function deniedGlobalWrite(install, error) {
|
|
7262
|
+
if (install.command !== "npm" || !(error instanceof ExternalToolError)) return false;
|
|
7263
|
+
return NPM_PERMISSION_TEXT.some((text) => error.stderrTail.includes(text)) || error.exitCode === NPM_EACCES_EXIT;
|
|
7264
|
+
}
|
|
7265
|
+
function globalWriteDenied(install) {
|
|
7266
|
+
return new ConfigError([
|
|
7267
|
+
`\`${displayCommand(install)}\` failed because npm cannot write its global directory.`,
|
|
7268
|
+
"Give npm a prefix you own with `npm config set prefix ~/.npm-global`,",
|
|
7269
|
+
"add ~/.npm-global/bin to PATH and open a new terminal,",
|
|
7270
|
+
"or install Node with a version manager such as nvm, fnm or mise;",
|
|
7271
|
+
"then run rde init again."
|
|
7272
|
+
].join(" "));
|
|
7273
|
+
}
|
|
7274
|
+
async function installMb(install, ctx, deps) {
|
|
7275
|
+
try {
|
|
7276
|
+
await runShown(deps.runner, ctx.ui.childOutput, install.command, install.args);
|
|
7277
|
+
} catch (error) {
|
|
7278
|
+
if (deniedGlobalWrite(install, error)) throw globalWriteDenied(install);
|
|
7279
|
+
throw error;
|
|
7280
|
+
}
|
|
7281
|
+
}
|
|
6775
7282
|
async function versionAt(runner, path) {
|
|
6776
7283
|
try {
|
|
6777
7284
|
const raw = await mbTool(runner).version(path);
|
|
@@ -6897,203 +7404,38 @@ function installMbStep(updates, deps = hostDeps$8) {
|
|
|
6897
7404
|
title: "Install the Metabase CLI",
|
|
6898
7405
|
async detect(ctx) {
|
|
6899
7406
|
const resolved = deps(ctx);
|
|
6900
|
-
const found = await inspectMb(resolved.runner);
|
|
6901
|
-
if (!found.installed || !found.satisfies) return "todo";
|
|
6902
|
-
return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
|
|
6903
|
-
},
|
|
6904
|
-
async run(ctx) {
|
|
6905
|
-
const resolved = deps(ctx);
|
|
6906
|
-
const before = await inspectMb(resolved.runner);
|
|
6907
|
-
const target = await release(resolved);
|
|
6908
|
-
if (before.installed && before.satisfies) {
|
|
6909
|
-
if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
|
|
6910
|
-
const update = await updateFor(target, before.version, resolved);
|
|
6911
|
-
if (update.kind === "shadowed") {
|
|
6912
|
-
const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
|
|
6913
|
-
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
6914
|
-
}
|
|
6915
|
-
if (update.kind !== "update") {
|
|
6916
|
-
ctx.ui.success(`mb ${before.version} is installed`);
|
|
6917
|
-
return { outcome: "completed" };
|
|
6918
|
-
}
|
|
6919
|
-
ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
|
|
6920
|
-
} else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
6921
|
-
const install = mbInstallCommand(resolved.installedWith);
|
|
6922
|
-
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
6923
|
-
await runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
|
|
6924
|
-
const after = await inspectMb(resolved.runner);
|
|
6925
|
-
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
6926
|
-
const stillNewer = updateTo(target, after.version);
|
|
6927
|
-
if (stillNewer !== null) {
|
|
6928
|
-
const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
|
|
6929
|
-
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
6930
|
-
}
|
|
6931
|
-
ctx.ui.success(`mb ${after.version} is installed`);
|
|
6932
|
-
return { outcome: "completed" };
|
|
6933
|
-
}
|
|
6934
|
-
};
|
|
6935
|
-
}
|
|
6936
|
-
//#endregion
|
|
6937
|
-
//#region src/steps/license.ts
|
|
6938
|
-
const STEP_ID$3 = "license";
|
|
6939
|
-
const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
|
|
6940
|
-
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
6941
|
-
const NOT_FOUND_STATUS$1 = 404;
|
|
6942
|
-
const TOKEN_REFUSED_STATUS = 400;
|
|
6943
|
-
const STORE_UNREACHABLE_STATUS = 503;
|
|
6944
|
-
const TOKEN_CHECK_HOST = "token-check.metabase.com";
|
|
6945
|
-
const ACTIVATION_FAILED = "License activation failed";
|
|
6946
|
-
const LICENSED_FEATURES = {
|
|
6947
|
-
[REMOTE_SYNC_FEATURE]: "remote sync",
|
|
6948
|
-
"data-apps": "data apps",
|
|
6949
|
-
"transforms-testing": "transform tests"
|
|
6950
|
-
};
|
|
6951
|
-
const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
|
|
6952
|
-
function licensedFeatureCoverage(features) {
|
|
6953
|
-
const entries = Object.entries(LICENSED_FEATURES);
|
|
6954
|
-
return {
|
|
6955
|
-
included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
|
|
6956
|
-
missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
|
|
6957
|
-
};
|
|
6958
|
-
}
|
|
6959
|
-
function listOf(names, type) {
|
|
6960
|
-
return new Intl.ListFormat("en", { type }).format(names);
|
|
6961
|
-
}
|
|
6962
|
-
function hostDeps$7(ctx) {
|
|
6963
|
-
return {
|
|
6964
|
-
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
6965
|
-
runner: processRunner(ctx.signal),
|
|
6966
|
-
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
6967
|
-
terminal: stdinIsTty()
|
|
6968
|
-
};
|
|
6969
|
-
}
|
|
6970
|
-
function isNoTokenYet(error) {
|
|
6971
|
-
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
6972
|
-
return isMetabaseNotServing(error);
|
|
6973
|
-
}
|
|
6974
|
-
function validateToken(value) {
|
|
6975
|
-
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
6976
|
-
return "A license token is 64 hexadecimal characters";
|
|
6977
|
-
}
|
|
6978
|
-
function requireApiKey(ctx) {
|
|
6979
|
-
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
6980
|
-
return ctx.secrets.apiKey;
|
|
6981
|
-
}
|
|
6982
|
-
function activeLabel(status) {
|
|
6983
|
-
const plan = status["plan-alias"];
|
|
6984
|
-
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
6985
|
-
}
|
|
6986
|
-
function refusalOf(error) {
|
|
6987
|
-
if (!(error instanceof MetabaseApiError)) return null;
|
|
6988
|
-
if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
|
|
6989
|
-
if (error.status === STORE_UNREACHABLE_STATUS) return new ConfigError(`The instance could not reach ${TOKEN_CHECK_HOST} to check the license token; it needs outbound network access`);
|
|
6990
|
-
return null;
|
|
6991
|
-
}
|
|
6992
|
-
async function activate(client, token) {
|
|
6993
|
-
try {
|
|
6994
|
-
await client.setting.setPremiumEmbeddingToken(token);
|
|
6995
|
-
} catch (error) {
|
|
6996
|
-
const refusal = refusalOf(error);
|
|
6997
|
-
if (refusal === null) throw error;
|
|
6998
|
-
return {
|
|
6999
|
-
kind: "refused",
|
|
7000
|
-
error: refusal
|
|
7001
|
-
};
|
|
7002
|
-
}
|
|
7003
|
-
const status = await client.tokenStatus.get();
|
|
7004
|
-
if (!status.valid) return {
|
|
7005
|
-
kind: "refused",
|
|
7006
|
-
error: new ConfigError(`Metabase reports the token as ${status.status}`)
|
|
7007
|
-
};
|
|
7008
|
-
return {
|
|
7009
|
-
kind: "activated",
|
|
7010
|
-
status
|
|
7011
|
-
};
|
|
7012
|
-
}
|
|
7013
|
-
async function activateAskedToken(ctx, client, terminal) {
|
|
7014
|
-
const token = await ctx.prompts.password({
|
|
7015
|
-
key: LICENSE_KEYS.token,
|
|
7016
|
-
message: "Paste your Metabase license token (Enter to skip)",
|
|
7017
|
-
validate: validateToken
|
|
7018
|
-
});
|
|
7019
|
-
if (token.length === 0) return null;
|
|
7020
|
-
const spinner = ctx.ui.spinner("Activating the license…");
|
|
7021
|
-
let activation;
|
|
7022
|
-
try {
|
|
7023
|
-
activation = await activate(client, token);
|
|
7024
|
-
} catch (error) {
|
|
7025
|
-
spinner.fail(ACTIVATION_FAILED);
|
|
7026
|
-
throw error;
|
|
7027
|
-
}
|
|
7028
|
-
if (activation.kind === "activated") {
|
|
7029
|
-
spinner.stop(activeLabel(activation.status));
|
|
7030
|
-
ctx.session.licenseToken.set(token);
|
|
7031
|
-
return activation.status;
|
|
7032
|
-
}
|
|
7033
|
-
spinner.fail(ACTIVATION_FAILED);
|
|
7034
|
-
if (!terminal) throw activation.error;
|
|
7035
|
-
ctx.ui.warn(activation.error.message);
|
|
7036
|
-
return activateAskedToken(ctx, client, terminal);
|
|
7037
|
-
}
|
|
7038
|
-
function reportFeatures(ctx, features) {
|
|
7039
|
-
const { missing } = licensedFeatureCoverage(features);
|
|
7040
|
-
const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
|
|
7041
|
-
if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
|
|
7042
|
-
if (missing.length > 0) {
|
|
7043
|
-
const gitNote = remoteSync ? "" : "; the git step will be skipped";
|
|
7044
|
-
ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
|
|
7045
|
-
}
|
|
7046
|
-
}
|
|
7047
|
-
function licenseStep(deps = hostDeps$7) {
|
|
7048
|
-
return {
|
|
7049
|
-
id: STEP_ID$3,
|
|
7050
|
-
title: "Activate your license",
|
|
7051
|
-
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
7052
|
-
async detect(ctx) {
|
|
7053
|
-
if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
|
|
7054
|
-
const configured = configuredRuntime(ctx.state);
|
|
7055
|
-
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7056
|
-
const resolved = deps(ctx);
|
|
7057
|
-
if (!await resolved.instanceRunning()) return "todo";
|
|
7058
|
-
const client = resolved.client(configured.url, {
|
|
7059
|
-
kind: "api-key",
|
|
7060
|
-
key: ctx.secrets.apiKey
|
|
7061
|
-
});
|
|
7062
|
-
try {
|
|
7063
|
-
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
7064
|
-
} catch (error) {
|
|
7065
|
-
if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
|
|
7066
|
-
throw error;
|
|
7067
|
-
}
|
|
7407
|
+
const found = await inspectMb(resolved.runner);
|
|
7408
|
+
if (!found.installed || !found.satisfies) return "todo";
|
|
7409
|
+
return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
|
|
7068
7410
|
},
|
|
7069
7411
|
async run(ctx) {
|
|
7070
|
-
const configured = requireConfiguredRuntime(ctx.state);
|
|
7071
|
-
const apiKey = requireApiKey(ctx);
|
|
7072
7412
|
const resolved = deps(ctx);
|
|
7073
|
-
const
|
|
7074
|
-
|
|
7075
|
-
|
|
7076
|
-
|
|
7077
|
-
|
|
7078
|
-
|
|
7079
|
-
|
|
7080
|
-
|
|
7081
|
-
}
|
|
7082
|
-
await ctx.saveState({
|
|
7083
|
-
...ctx.state,
|
|
7084
|
-
license: {
|
|
7085
|
-
activated: true,
|
|
7086
|
-
features: status.features,
|
|
7087
|
-
validThru: status["valid-thru"] ?? null
|
|
7413
|
+
const before = await inspectMb(resolved.runner);
|
|
7414
|
+
const target = await release(resolved);
|
|
7415
|
+
if (before.installed && before.satisfies) {
|
|
7416
|
+
if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
|
|
7417
|
+
const update = await updateFor(target, before.version, resolved);
|
|
7418
|
+
if (update.kind === "shadowed") {
|
|
7419
|
+
const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
|
|
7420
|
+
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
7088
7421
|
}
|
|
7089
|
-
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
|
|
7094
|
-
|
|
7095
|
-
|
|
7096
|
-
|
|
7422
|
+
if (update.kind !== "update") {
|
|
7423
|
+
ctx.ui.success(`mb ${before.version} is installed`);
|
|
7424
|
+
return { outcome: "completed" };
|
|
7425
|
+
}
|
|
7426
|
+
ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
|
|
7427
|
+
} else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
7428
|
+
const install = mbInstallCommand(resolved.installedWith);
|
|
7429
|
+
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
7430
|
+
await installMb(install, ctx, resolved);
|
|
7431
|
+
const after = await inspectMb(resolved.runner);
|
|
7432
|
+
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
7433
|
+
const stillNewer = updateTo(target, after.version);
|
|
7434
|
+
if (stillNewer !== null) {
|
|
7435
|
+
const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
|
|
7436
|
+
ctx.ui.warn(await shadowedWarning(lead, resolved));
|
|
7437
|
+
}
|
|
7438
|
+
ctx.ui.success(`mb ${after.version} is installed`);
|
|
7097
7439
|
return { outcome: "completed" };
|
|
7098
7440
|
}
|
|
7099
7441
|
};
|
|
@@ -7122,7 +7464,7 @@ const PLATFORMS = {
|
|
|
7122
7464
|
javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
|
|
7123
7465
|
}
|
|
7124
7466
|
};
|
|
7125
|
-
function hostDeps$
|
|
7467
|
+
function hostDeps$7(ctx) {
|
|
7126
7468
|
const runner = processRunner(ctx.signal);
|
|
7127
7469
|
return {
|
|
7128
7470
|
platform: process.platform,
|
|
@@ -7244,7 +7586,7 @@ function checkLine(check) {
|
|
|
7244
7586
|
function checkLines(checks) {
|
|
7245
7587
|
return checks.map(checkLine);
|
|
7246
7588
|
}
|
|
7247
|
-
function preflightStep(deps = hostDeps$
|
|
7589
|
+
function preflightStep(deps = hostDeps$7) {
|
|
7248
7590
|
return {
|
|
7249
7591
|
id: "preflight",
|
|
7250
7592
|
title: "Check this machine",
|
|
@@ -7263,7 +7605,7 @@ function preflightStep(deps = hostDeps$6) {
|
|
|
7263
7605
|
}
|
|
7264
7606
|
//#endregion
|
|
7265
7607
|
//#region src/steps/runtime.ts
|
|
7266
|
-
const STEP_ID$
|
|
7608
|
+
const STEP_ID$3 = "runtime";
|
|
7267
7609
|
const MIN_PORT = 1;
|
|
7268
7610
|
const MAX_PORT = 65535;
|
|
7269
7611
|
const PORT_PATTERN = /^\d+$/;
|
|
@@ -7277,7 +7619,7 @@ const NO_RUNTIME_FLAGS = {
|
|
|
7277
7619
|
jar: null,
|
|
7278
7620
|
port: null
|
|
7279
7621
|
};
|
|
7280
|
-
function hostDeps$
|
|
7622
|
+
function hostDeps$6(ctx) {
|
|
7281
7623
|
return {
|
|
7282
7624
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7283
7625
|
releases: releaseLookup(ctx.signal)
|
|
@@ -7329,22 +7671,56 @@ async function chooseKind(ctx, options, runtime) {
|
|
|
7329
7671
|
ctx.ui.info(`Docker is not available (${available.docker.reason}); using the jar runtime with ${javaLabel(available.java)}`);
|
|
7330
7672
|
return "jar";
|
|
7331
7673
|
}
|
|
7332
|
-
|
|
7674
|
+
const NAME_ONE = "`--image <image>` or `--jar <release tag>` names a Metabase to run";
|
|
7675
|
+
const INIT_WAYS_OUT = {
|
|
7676
|
+
named: NAME_ONE,
|
|
7677
|
+
unreleased: `${NAME_ONE}, and \`RDE_DEV=1\` runs the development build`
|
|
7678
|
+
};
|
|
7679
|
+
function isFeedFailure(error) {
|
|
7680
|
+
return error instanceof MetabaseApiError || error instanceof MetabaseUnreachableError;
|
|
7681
|
+
}
|
|
7682
|
+
async function requireNewestRelease(releases, waysOut) {
|
|
7683
|
+
let newest;
|
|
7684
|
+
try {
|
|
7685
|
+
newest = await releases.newestSupportedRelease();
|
|
7686
|
+
} catch (error) {
|
|
7687
|
+
if (!isFeedFailure(error)) throw error;
|
|
7688
|
+
throw new ConfigError(`the Metabase release feed could not be read (${error.reason}); ${waysOut.named}`);
|
|
7689
|
+
}
|
|
7690
|
+
if (newest === null) throw new ConfigError(`no release of Metabase Enterprise 65 or newer is out yet; ${waysOut.unreleased}`);
|
|
7691
|
+
return newest;
|
|
7692
|
+
}
|
|
7693
|
+
async function releaseImage(ctx, releases, waysOut) {
|
|
7694
|
+
const newest = await requireNewestRelease(releases, waysOut);
|
|
7695
|
+
const image = majorLineImage(newest.major);
|
|
7696
|
+
ctx.ui.info(`The newest Metabase release is ${newest.version}; ${image} carries the newest build of ${newest.major}`);
|
|
7697
|
+
return image;
|
|
7698
|
+
}
|
|
7699
|
+
async function latestJarRelease(ctx, releases, waysOut) {
|
|
7700
|
+
if (developmentMode()) {
|
|
7701
|
+
const version = await releases.latestVersion();
|
|
7702
|
+
ctx.ui.info(`The latest Metabase release is ${version}`);
|
|
7703
|
+
return version;
|
|
7704
|
+
}
|
|
7705
|
+
const newest = await requireNewestRelease(releases, waysOut);
|
|
7706
|
+
ctx.ui.info(`The newest Metabase release is ${newest.version}`);
|
|
7707
|
+
return newest.version;
|
|
7708
|
+
}
|
|
7709
|
+
async function chooseImage(ctx, options, releases) {
|
|
7333
7710
|
if (options.image !== null) {
|
|
7334
7711
|
ctx.ui.info(`Using image ${options.image} from --image`);
|
|
7335
7712
|
return options.image;
|
|
7336
7713
|
}
|
|
7337
|
-
return
|
|
7714
|
+
if (developmentMode()) return DEVELOPMENT_IMAGE;
|
|
7715
|
+
return releaseImage(ctx, releases, INIT_WAYS_OUT);
|
|
7338
7716
|
}
|
|
7339
7717
|
async function resolveJar(ctx, spec, releases) {
|
|
7340
7718
|
const parsed = parseJarSpec(spec);
|
|
7341
|
-
if (parsed === null) throw new ConfigError(`${spec} is not latest,
|
|
7719
|
+
if (parsed === null) throw new ConfigError(`${spec} is not latest, an enterprise release tag such as v1.65.0, or the path of a .jar file`);
|
|
7342
7720
|
if (parsed.kind !== "latest") return parsed;
|
|
7343
|
-
const version = await releases.latestVersion();
|
|
7344
|
-
ctx.ui.info(`The latest Metabase release is ${version}`);
|
|
7345
7721
|
return {
|
|
7346
7722
|
kind: "release",
|
|
7347
|
-
version
|
|
7723
|
+
version: await latestJarRelease(ctx, releases, INIT_WAYS_OUT)
|
|
7348
7724
|
};
|
|
7349
7725
|
}
|
|
7350
7726
|
async function chooseJar(ctx, options, releases) {
|
|
@@ -7369,7 +7745,7 @@ async function chooseRuntime(ctx, options, deps) {
|
|
|
7369
7745
|
const kind = await chooseKind(ctx, options, deps.runtime);
|
|
7370
7746
|
switch (kind) {
|
|
7371
7747
|
case "docker": {
|
|
7372
|
-
const image = chooseImage(ctx, options);
|
|
7748
|
+
const image = await chooseImage(ctx, options, deps.releases);
|
|
7373
7749
|
const port = await choosePort(ctx, options, deps.runtime);
|
|
7374
7750
|
return {
|
|
7375
7751
|
kind,
|
|
@@ -7406,9 +7782,9 @@ async function persistedRuntime(ctx, options, deps) {
|
|
|
7406
7782
|
});
|
|
7407
7783
|
return chosen;
|
|
7408
7784
|
}
|
|
7409
|
-
function runtimeStep(options, deps = hostDeps$
|
|
7785
|
+
function runtimeStep(options, deps = hostDeps$6) {
|
|
7410
7786
|
return {
|
|
7411
|
-
id: STEP_ID$
|
|
7787
|
+
id: STEP_ID$3,
|
|
7412
7788
|
title: "Install Metabase",
|
|
7413
7789
|
redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
|
|
7414
7790
|
async detect(ctx) {
|
|
@@ -7437,7 +7813,7 @@ const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
|
|
|
7437
7813
|
//#region src/steps/start-metabase.ts
|
|
7438
7814
|
const LOG_TAIL_ON_TIMEOUT = 30;
|
|
7439
7815
|
const PERCENT = 100;
|
|
7440
|
-
function hostDeps$
|
|
7816
|
+
function hostDeps$5(ctx) {
|
|
7441
7817
|
return {
|
|
7442
7818
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7443
7819
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -7450,6 +7826,16 @@ function isNotHealthy(error) {
|
|
|
7450
7826
|
async function runningVersion(client) {
|
|
7451
7827
|
return (await client.session.properties()).version.tag;
|
|
7452
7828
|
}
|
|
7829
|
+
const PICK_ANOTHER = "`rde upgrade --to <image or release tag>` moves this instance to one, or `rde uninstall`, then `rde init --image <image>` or `rde init --jar <release tag>`, sets up a new instance on one";
|
|
7830
|
+
function unsupportedVersionReason(version, development) {
|
|
7831
|
+
if (development) return null;
|
|
7832
|
+
const needs = `rde needs Metabase Enterprise 65 or newer: ${PICK_ANOTHER}`;
|
|
7833
|
+
if (isOpenSourceRelease(version)) return `Metabase ${version} is the open-source edition; ${needs}`;
|
|
7834
|
+
if (isMasterBuild(version)) return `Metabase ${version} is a development build of master, which only development mode (RDE_DEV=1) runs; ${needs}`;
|
|
7835
|
+
const major = releaseMajor(version);
|
|
7836
|
+
if (major !== null && major < 65) return `Metabase ${version} is older than 65; ${needs}`;
|
|
7837
|
+
return null;
|
|
7838
|
+
}
|
|
7453
7839
|
function progressLabel(progress) {
|
|
7454
7840
|
return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
|
|
7455
7841
|
}
|
|
@@ -7466,7 +7852,7 @@ async function startRuntime(ctx, runtime, deps) {
|
|
|
7466
7852
|
throw error;
|
|
7467
7853
|
}
|
|
7468
7854
|
}
|
|
7469
|
-
function startMetabaseStep(deps = hostDeps$
|
|
7855
|
+
function startMetabaseStep(deps = hostDeps$5, accepted = "supported") {
|
|
7470
7856
|
return {
|
|
7471
7857
|
id: "start-metabase",
|
|
7472
7858
|
title: "Start Metabase",
|
|
@@ -7518,12 +7904,272 @@ function startMetabaseStep(deps = hostDeps$4) {
|
|
|
7518
7904
|
...ctx.state,
|
|
7519
7905
|
version
|
|
7520
7906
|
});
|
|
7907
|
+
const refusal = accepted === "supported" ? unsupportedVersionReason(version, developmentMode()) : null;
|
|
7908
|
+
if (refusal !== null) {
|
|
7909
|
+
spinner.fail(`Metabase ${version} is running at ${configured.url}, but rde does not run it`);
|
|
7910
|
+
throw new UnsupportedMetabaseError(refusal);
|
|
7911
|
+
}
|
|
7521
7912
|
spinner.stop(`Metabase ${version} is running at ${configured.url}`);
|
|
7522
7913
|
return { outcome: "completed" };
|
|
7523
7914
|
}
|
|
7524
7915
|
};
|
|
7525
7916
|
}
|
|
7526
7917
|
//#endregion
|
|
7918
|
+
//#region src/steps/sync-scope.ts
|
|
7919
|
+
const STEP_ID$2 = "sync-scope";
|
|
7920
|
+
const SYNC_SCOPE_KEYS = { export: `${STEP_ID$2}.export` };
|
|
7921
|
+
const AGENT_COLLECTION_NAME = "Analytics";
|
|
7922
|
+
const LIBRARY_NAME = "Library";
|
|
7923
|
+
const ROOT_COLLECTION_NAME = "Our analytics";
|
|
7924
|
+
const AGENT_COLLECTION_DESCRIPTION = "Dashboards, questions, models, and documents that reach git on export.";
|
|
7925
|
+
function hostDeps$4(ctx) {
|
|
7926
|
+
return {
|
|
7927
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7928
|
+
runner: processRunner(ctx.signal),
|
|
7929
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
7930
|
+
};
|
|
7931
|
+
}
|
|
7932
|
+
function isCollection(entry) {
|
|
7933
|
+
return typeof entry.id === "number";
|
|
7934
|
+
}
|
|
7935
|
+
function isOrdinary(collection) {
|
|
7936
|
+
return collection.type === null && collection.namespace === null && collection.personal_owner_id === null;
|
|
7937
|
+
}
|
|
7938
|
+
function readScope(list, syncRoot) {
|
|
7939
|
+
const topLevel = list.filter(isCollection).filter((collection) => collection.location === "/");
|
|
7940
|
+
const ordinary = topLevel.filter(isOrdinary);
|
|
7941
|
+
const unsynced = ordinary.filter((collection) => !collection.is_remote_synced);
|
|
7942
|
+
return {
|
|
7943
|
+
library: topLevel.find((collection) => collection.type === "library") ?? null,
|
|
7944
|
+
syncedRoots: ordinary.filter((collection) => collection.is_remote_synced),
|
|
7945
|
+
unsyncedRecordedRoot: unsynced.find((collection) => collection.id === syncRoot) ?? null,
|
|
7946
|
+
foreignNamesake: unsynced.some((collection) => collection.id !== syncRoot && collection.name === "Analytics")
|
|
7947
|
+
};
|
|
7948
|
+
}
|
|
7949
|
+
function isComplete(scope, libraryLicensed) {
|
|
7950
|
+
return (!libraryLicensed || scope.library !== null && scope.library.is_remote_synced) && scope.syncedRoots.length > 0;
|
|
7951
|
+
}
|
|
7952
|
+
function isNotAnsweringYet(error) {
|
|
7953
|
+
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || isRejectedCredential(error);
|
|
7954
|
+
}
|
|
7955
|
+
function requireMbInstance(ctx) {
|
|
7956
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
7957
|
+
if (instance === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
7958
|
+
return instance;
|
|
7959
|
+
}
|
|
7960
|
+
function requireGitRemote(ctx) {
|
|
7961
|
+
if (ctx.state.gitRemote === null) throw new ConfigError("no repository yet; the git-sync step comes first");
|
|
7962
|
+
return ctx.state.gitRemote;
|
|
7963
|
+
}
|
|
7964
|
+
async function requireSettledImport(ctx, client, gitRemote) {
|
|
7965
|
+
const task = await client.remoteSync.currentTask();
|
|
7966
|
+
if (task === null) return;
|
|
7967
|
+
if (task.status === "running") throw new ConfigError(`a remote sync ${task.sync_task_type} is running; run \`rde init --only ${STEP_ID$2}\` once it finishes`);
|
|
7968
|
+
if (task.sync_task_type === "import" && task.status !== "successful") {
|
|
7969
|
+
const importCommand = `${mbCommand(ctx.state.mbProfile)} git-sync import --branch ${shellWord(gitRemote.branch)}`;
|
|
7970
|
+
throw new ConfigError(`the last import from the repository ended ${task.status}; run \`${importCommand}\`, then \`rde init --only ${STEP_ID$2}\``);
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
async function isReadOnly(client) {
|
|
7974
|
+
return await client.setting.remoteSyncType() === "read-only";
|
|
7975
|
+
}
|
|
7976
|
+
async function requireReadWrite(client) {
|
|
7977
|
+
if (await isReadOnly(client)) throw new ConfigError(`remote sync is read-only; the repository decides what syncs. Switch it to read-write in Admin, then \`rde init --only ${STEP_ID$2}\``);
|
|
7978
|
+
}
|
|
7979
|
+
async function ensureLibrary(client, scope, libraryLicensed) {
|
|
7980
|
+
if (!libraryLicensed) return null;
|
|
7981
|
+
return scope.library ?? client.collection.createLibrary();
|
|
7982
|
+
}
|
|
7983
|
+
async function ensureRoots(ctx, client, scope) {
|
|
7984
|
+
if (scope.syncedRoots.length > 0) return {
|
|
7985
|
+
roots: scope.syncedRoots,
|
|
7986
|
+
created: false,
|
|
7987
|
+
besideNamesake: false
|
|
7988
|
+
};
|
|
7989
|
+
if (scope.unsyncedRecordedRoot !== null) return {
|
|
7990
|
+
roots: [scope.unsyncedRecordedRoot],
|
|
7991
|
+
created: false,
|
|
7992
|
+
besideNamesake: false
|
|
7993
|
+
};
|
|
7994
|
+
const created = await client.collection.create({
|
|
7995
|
+
name: AGENT_COLLECTION_NAME,
|
|
7996
|
+
description: AGENT_COLLECTION_DESCRIPTION
|
|
7997
|
+
});
|
|
7998
|
+
await ctx.saveState({
|
|
7999
|
+
...ctx.state,
|
|
8000
|
+
syncRoot: created.id
|
|
8001
|
+
});
|
|
8002
|
+
return {
|
|
8003
|
+
roots: [created],
|
|
8004
|
+
created: true,
|
|
8005
|
+
besideNamesake: scope.foreignNamesake
|
|
8006
|
+
};
|
|
8007
|
+
}
|
|
8008
|
+
function itemLabel(item) {
|
|
8009
|
+
return item.name === null ? `${item.model} ${item.id}` : `${item.model} ${item.id} "${item.name}"`;
|
|
8010
|
+
}
|
|
8011
|
+
function locationLabel(location) {
|
|
8012
|
+
if (location === void 0) return "in a collection Metabase could not find";
|
|
8013
|
+
return `in ${location === null ? ROOT_COLLECTION_NAME : location.name}`;
|
|
8014
|
+
}
|
|
8015
|
+
function dependencyLine(dependency) {
|
|
8016
|
+
const line = ` ${itemLabel(dependency)} ${locationLabel(dependency.collection)}`;
|
|
8017
|
+
if (dependency.used_by.length === 0) return line;
|
|
8018
|
+
return `${line}, used by ${dependency.used_by.map(itemLabel).join(", ")}`;
|
|
8019
|
+
}
|
|
8020
|
+
function remedyLine(required) {
|
|
8021
|
+
const remedy = required.remedy;
|
|
8022
|
+
switch (remedy.type) {
|
|
8023
|
+
case "collection":
|
|
8024
|
+
if (required.syncable) return ` Fix: sync ${remedy.collection.name} too, or move that content into a synced collection.`;
|
|
8025
|
+
return ` Fix: move that content into a synced collection; ${remedy.collection.name} cannot sync.`;
|
|
8026
|
+
case "library": return " Fix: stop using that content; it syncs only through a Library, and this instance has none.";
|
|
8027
|
+
case "none": return " Fix: move that content into a synced collection, or stop using it.";
|
|
8028
|
+
}
|
|
8029
|
+
}
|
|
8030
|
+
function requiredSyncLines(required) {
|
|
8031
|
+
return [
|
|
8032
|
+
`Metabase will not sync ${required.blocks.map((collection) => collection.name).join(", ")}, because content there uses:`,
|
|
8033
|
+
...required.dependencies.map(dependencyLine),
|
|
8034
|
+
remedyLine(required)
|
|
8035
|
+
];
|
|
8036
|
+
}
|
|
8037
|
+
function explainRefusal(error) {
|
|
8038
|
+
if (!(error instanceof MetabaseApiError) || error.code !== "unsynced-dependencies") return error;
|
|
8039
|
+
const refusal = UnsyncedDependencies.safeParse(error.detail);
|
|
8040
|
+
if (!refusal.success) return error;
|
|
8041
|
+
return new ConfigError([...refusal.data.errors.required.flatMap(requiredSyncLines), `After the fix, run \`rde init --only ${STEP_ID$2}\`.`].join("\n"));
|
|
8042
|
+
}
|
|
8043
|
+
function plannedNames(scope, libraryLicensed) {
|
|
8044
|
+
const library = libraryLicensed ? [scope.library?.name ?? LIBRARY_NAME] : [];
|
|
8045
|
+
if (scope.syncedRoots.length > 0) return [...library, ...scope.syncedRoots.map((collection) => collection.name)];
|
|
8046
|
+
return [...library, scope.unsyncedRecordedRoot?.name ?? "Analytics"];
|
|
8047
|
+
}
|
|
8048
|
+
function nameList(names) {
|
|
8049
|
+
if (names.length <= 2) return names.join(" and ");
|
|
8050
|
+
return `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
|
|
8051
|
+
}
|
|
8052
|
+
function exportAdvice(profile, branch, changes) {
|
|
8053
|
+
const command = exportCommand(profile, branch);
|
|
8054
|
+
if (!remoteMoved(changes)) return `\`${command}\``;
|
|
8055
|
+
return `the branch has commits Metabase has not imported; \`${mbCommand(profile)} git-sync export-preflight\`, then \`${command} --merge\``;
|
|
8056
|
+
}
|
|
8057
|
+
async function planExport(ctx, runner, instance, names) {
|
|
8058
|
+
const changes = await mbTool(runner).gitSyncHasRemoteChanges(instance);
|
|
8059
|
+
if (remoteMoved(changes)) return {
|
|
8060
|
+
changes,
|
|
8061
|
+
push: false
|
|
8062
|
+
};
|
|
8063
|
+
return {
|
|
8064
|
+
changes,
|
|
8065
|
+
push: await ctx.prompts.confirm({
|
|
8066
|
+
key: SYNC_SCOPE_KEYS.export,
|
|
8067
|
+
message: `Push ${nameList(names)} to ${requireGitRemote(ctx).branch} now?`,
|
|
8068
|
+
initialValue: true
|
|
8069
|
+
})
|
|
8070
|
+
};
|
|
8071
|
+
}
|
|
8072
|
+
function describeExport(task) {
|
|
8073
|
+
const ended = `the export ended ${task.status}`;
|
|
8074
|
+
return task.error_message === null || task.error_message === void 0 ? ended : `${ended}: ${task.error_message}`;
|
|
8075
|
+
}
|
|
8076
|
+
function reportPending(run) {
|
|
8077
|
+
const advice = exportAdvice(run.ctx.state.mbProfile, run.gitRemote.branch, run.changes);
|
|
8078
|
+
run.ctx.ui.info(`First export pending: ${advice}`);
|
|
8079
|
+
}
|
|
8080
|
+
async function exportScope(run, runner) {
|
|
8081
|
+
const mb = mbTool(runner);
|
|
8082
|
+
const label = nameList(run.names);
|
|
8083
|
+
const spinner = run.ctx.ui.spinner(`Pushing ${label} to ${run.gitRemote.branch}…`);
|
|
8084
|
+
try {
|
|
8085
|
+
const final = (await mb.gitSyncExport(run.instance, {
|
|
8086
|
+
branch: run.gitRemote.branch,
|
|
8087
|
+
message: `Sync ${label}`
|
|
8088
|
+
})).final ?? null;
|
|
8089
|
+
if (final !== null && final.status !== "successful") {
|
|
8090
|
+
spinner.fail(describeExport(final));
|
|
8091
|
+
reportPending(run);
|
|
8092
|
+
return;
|
|
8093
|
+
}
|
|
8094
|
+
const status = await mb.gitSyncStatus(run.instance);
|
|
8095
|
+
spinner.stop(`Pushed ${label} to ${run.gitRemote.branch}`);
|
|
8096
|
+
if (status.is_dirty) run.ctx.ui.warn(`Metabase still has changes the export did not carry; \`${mbCommand(run.ctx.state.mbProfile)} git-sync dirty\` lists them`);
|
|
8097
|
+
} catch (error) {
|
|
8098
|
+
if (!(error instanceof ExternalToolError)) throw error;
|
|
8099
|
+
spinner.fail(`Could not push ${label}: ${error.message}`);
|
|
8100
|
+
reportPending(run);
|
|
8101
|
+
}
|
|
8102
|
+
}
|
|
8103
|
+
function scopeLabel(synced) {
|
|
8104
|
+
return `Synced to git: ${synced.map((collection) => collection.name).join(", ")}`;
|
|
8105
|
+
}
|
|
8106
|
+
function syncScopeStep(deps = hostDeps$4) {
|
|
8107
|
+
return {
|
|
8108
|
+
id: STEP_ID$2,
|
|
8109
|
+
title: "Choose what syncs to git",
|
|
8110
|
+
async detect(ctx) {
|
|
8111
|
+
if (!hasRemoteSync(ctx.state.license) || ctx.state.gitRemote === null) return "not-applicable";
|
|
8112
|
+
const configured = configuredRuntime(ctx.state);
|
|
8113
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
8114
|
+
const resolved = deps(ctx);
|
|
8115
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
8116
|
+
const client = resolved.client(configured.url, {
|
|
8117
|
+
kind: "api-key",
|
|
8118
|
+
key: ctx.secrets.apiKey
|
|
8119
|
+
});
|
|
8120
|
+
try {
|
|
8121
|
+
if (isComplete(readScope(await client.collection.list(), ctx.state.syncRoot), hasLibrary(ctx.state.license))) return "done";
|
|
8122
|
+
return await isReadOnly(client) ? "not-applicable" : "todo";
|
|
8123
|
+
} catch (error) {
|
|
8124
|
+
if (isNotAnsweringYet(error)) return "todo";
|
|
8125
|
+
throw error;
|
|
8126
|
+
}
|
|
8127
|
+
},
|
|
8128
|
+
async run(ctx) {
|
|
8129
|
+
requireConfiguredRuntime(ctx.state);
|
|
8130
|
+
const gitRemote = requireGitRemote(ctx);
|
|
8131
|
+
const instance = requireMbInstance(ctx);
|
|
8132
|
+
const resolved = deps(ctx);
|
|
8133
|
+
const client = resolved.client(instance.url, {
|
|
8134
|
+
kind: "api-key",
|
|
8135
|
+
key: instance.apiKey
|
|
8136
|
+
});
|
|
8137
|
+
await requireReadWrite(client);
|
|
8138
|
+
await requireSettledImport(ctx, client, gitRemote);
|
|
8139
|
+
const libraryLicensed = hasLibrary(ctx.state.license);
|
|
8140
|
+
const scope = readScope(await client.collection.list(), ctx.state.syncRoot);
|
|
8141
|
+
const names = plannedNames(scope, libraryLicensed);
|
|
8142
|
+
const plan = isComplete(scope, libraryLicensed) ? null : await planExport(ctx, resolved.runner, instance, names);
|
|
8143
|
+
const spinner = ctx.ui.spinner("Choosing what syncs to git…");
|
|
8144
|
+
let synced;
|
|
8145
|
+
let chosen;
|
|
8146
|
+
try {
|
|
8147
|
+
const library = await ensureLibrary(client, scope, libraryLicensed);
|
|
8148
|
+
chosen = await ensureRoots(ctx, client, scope);
|
|
8149
|
+
synced = library === null ? chosen.roots : [library, ...chosen.roots];
|
|
8150
|
+
await client.remoteSync.updateScope({ collections: Object.fromEntries(synced.map((collection) => [collection.id, true])) });
|
|
8151
|
+
} catch (error) {
|
|
8152
|
+
spinner.fail("Could not choose what syncs to git");
|
|
8153
|
+
throw explainRefusal(error);
|
|
8154
|
+
}
|
|
8155
|
+
spinner.stop(scopeLabel(synced));
|
|
8156
|
+
if (chosen.created) ctx.ui.info(`Created the ${AGENT_COLLECTION_NAME} collection: dashboards, questions, models, and documents filed under it reach git on the next export`);
|
|
8157
|
+
if (chosen.besideNamesake) ctx.ui.info(`Another collection is also named ${AGENT_COLLECTION_NAME}; rde did not create it, so it stays out of git`);
|
|
8158
|
+
if (plan === null) return { outcome: "completed" };
|
|
8159
|
+
const exportRun = {
|
|
8160
|
+
ctx,
|
|
8161
|
+
instance,
|
|
8162
|
+
gitRemote,
|
|
8163
|
+
names,
|
|
8164
|
+
changes: plan.changes
|
|
8165
|
+
};
|
|
8166
|
+
if (plan.push) await exportScope(exportRun, resolved.runner);
|
|
8167
|
+
else reportPending(exportRun);
|
|
8168
|
+
return { outcome: "completed" };
|
|
8169
|
+
}
|
|
8170
|
+
};
|
|
8171
|
+
}
|
|
8172
|
+
//#endregion
|
|
7527
8173
|
//#region src/steps/summary.ts
|
|
7528
8174
|
const STEP_ID$1 = "summary";
|
|
7529
8175
|
const GIT_SYNC_STEP_ID = "git-sync";
|
|
@@ -7570,16 +8216,36 @@ function remoteLine(remote) {
|
|
|
7570
8216
|
case "hosted": return `Remote: ${remote.url}`;
|
|
7571
8217
|
}
|
|
7572
8218
|
}
|
|
7573
|
-
function
|
|
7574
|
-
const license = state.license;
|
|
8219
|
+
function licenseGap(license) {
|
|
7575
8220
|
if (!license.activated) return "no license";
|
|
7576
8221
|
if (!hasRemoteSync(license)) return "the license has no remote-sync feature";
|
|
8222
|
+
return null;
|
|
8223
|
+
}
|
|
8224
|
+
function notConfiguredReason(state) {
|
|
8225
|
+
const gap = licenseGap(state.license);
|
|
8226
|
+
if (gap !== null) return gap;
|
|
7577
8227
|
if (state.steps[GIT_SYNC_STEP_ID]?.status === "skipped") return "skipped; `rde init --only git-sync` sets it up";
|
|
7578
8228
|
return "not set up yet; `rde init --only git-sync`";
|
|
7579
8229
|
}
|
|
7580
8230
|
function gitLines(state) {
|
|
7581
8231
|
if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
|
|
7582
|
-
|
|
8232
|
+
const repository = [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
|
|
8233
|
+
const gap = licenseGap(state.license);
|
|
8234
|
+
if (gap === null) return repository;
|
|
8235
|
+
return [...repository, `Remote sync: unavailable (${gap}; \`rde init --from license\` activates one)`];
|
|
8236
|
+
}
|
|
8237
|
+
async function pendingExportLines(state, secrets, runner) {
|
|
8238
|
+
const instance = mbInstanceOf(state, secrets);
|
|
8239
|
+
if (state.gitRemote === null || instance === null) return [];
|
|
8240
|
+
try {
|
|
8241
|
+
const mb = mbTool(runner);
|
|
8242
|
+
if (!(await mb.gitSyncStatus(instance)).is_dirty) return [];
|
|
8243
|
+
const changes = await mb.gitSyncHasRemoteChanges(instance);
|
|
8244
|
+
return [`Export pending: ${exportAdvice(state.mbProfile, state.gitRemote.branch, changes)}`];
|
|
8245
|
+
} catch (error) {
|
|
8246
|
+
if (error instanceof ProcessNotFoundError || error instanceof ExternalToolError) return [`Remote sync: ${error.message}`];
|
|
8247
|
+
throw error;
|
|
8248
|
+
}
|
|
7583
8249
|
}
|
|
7584
8250
|
function skillLine(entries, detected) {
|
|
7585
8251
|
const agents = [...new Set(entries.flatMap((entry) => coveredAgentNames(entry, detected)))];
|
|
@@ -7592,14 +8258,15 @@ function launchLines(agents, workdir) {
|
|
|
7592
8258
|
const prefix = workdir === null ? "" : `cd ${workdir} && `;
|
|
7593
8259
|
return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
|
|
7594
8260
|
}
|
|
7595
|
-
async function summaryLines(state,
|
|
8261
|
+
async function summaryLines(state, secrets, deps) {
|
|
7596
8262
|
const profile = state.mbProfile;
|
|
7597
8263
|
const lines = [];
|
|
7598
8264
|
if (state.url !== null) lines.push(`Metabase: ${state.url}`);
|
|
7599
8265
|
if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
|
|
7600
8266
|
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
7601
|
-
lines.push(...await databaseLines$1(state, apiKey, deps));
|
|
8267
|
+
lines.push(...await databaseLines$1(state, secrets.apiKey, deps));
|
|
7602
8268
|
lines.push(...gitLines(state));
|
|
8269
|
+
lines.push(...await pendingExportLines(state, secrets, deps.runner));
|
|
7603
8270
|
const installed = await skillsCli(deps.runner).list();
|
|
7604
8271
|
const agents = await detectAgents(deps.probe);
|
|
7605
8272
|
const detected = agents.filter((agent) => agent.detected);
|
|
@@ -7616,7 +8283,7 @@ function summaryStep(deps = hostDeps$3) {
|
|
|
7616
8283
|
return "todo";
|
|
7617
8284
|
},
|
|
7618
8285
|
async run(ctx) {
|
|
7619
|
-
const lines = await summaryLines(ctx.state, ctx.secrets
|
|
8286
|
+
const lines = await summaryLines(ctx.state, ctx.secrets, deps(ctx));
|
|
7620
8287
|
ctx.ui.note(lines.join("\n"), "Your environment");
|
|
7621
8288
|
return { outcome: "completed" };
|
|
7622
8289
|
}
|
|
@@ -7693,6 +8360,7 @@ const STEP_BY_ID = {
|
|
|
7693
8360
|
license: () => licenseStep(),
|
|
7694
8361
|
databases: (options) => databasesStep(options.databases),
|
|
7695
8362
|
"git-sync": () => gitSyncStep(),
|
|
8363
|
+
"sync-scope": () => syncScopeStep(),
|
|
7696
8364
|
summary: () => summaryStep()
|
|
7697
8365
|
};
|
|
7698
8366
|
function initSteps(options) {
|
|
@@ -7777,7 +8445,8 @@ const SECTIONS = {
|
|
|
7777
8445
|
remote: RemoteKind.optional(),
|
|
7778
8446
|
url: EnvString.optional(),
|
|
7779
8447
|
token: EnvString.optional()
|
|
7780
|
-
}).optional()
|
|
8448
|
+
}).optional(),
|
|
8449
|
+
"sync-scope": z.strictObject({ export: z.boolean() }).optional()
|
|
7781
8450
|
};
|
|
7782
8451
|
const InitAnswers = z.strictObject(SECTIONS);
|
|
7783
8452
|
const select$1 = (value) => ({
|
|
@@ -7886,6 +8555,8 @@ function keyedAnswers(answers) {
|
|
|
7886
8555
|
if (git.url !== void 0) add(map, GIT_SYNC_KEYS.url, text$1(git.url));
|
|
7887
8556
|
if (git.token !== void 0) add(map, GIT_SYNC_KEYS.token, password$1(git.token));
|
|
7888
8557
|
}
|
|
8558
|
+
const scope = answers["sync-scope"];
|
|
8559
|
+
if (scope !== void 0) add(map, SYNC_SCOPE_KEYS.export, confirm$1(scope.export));
|
|
7889
8560
|
return map;
|
|
7890
8561
|
}
|
|
7891
8562
|
function parseInitAnswers(raw, source) {
|
|
@@ -7927,6 +8598,10 @@ function stepsAskingForWork(answers) {
|
|
|
7927
8598
|
step: "git-sync",
|
|
7928
8599
|
asks: git !== void 0 && git.location !== "skip"
|
|
7929
8600
|
},
|
|
8601
|
+
{
|
|
8602
|
+
step: "sync-scope",
|
|
8603
|
+
asks: answers["sync-scope"]?.export === true
|
|
8604
|
+
},
|
|
7930
8605
|
{
|
|
7931
8606
|
step: "agents-skills",
|
|
7932
8607
|
asks: agents !== void 0 && agents.agents.length > 0
|
|
@@ -8528,8 +9203,7 @@ function plainUi(output) {
|
|
|
8528
9203
|
};
|
|
8529
9204
|
}
|
|
8530
9205
|
function noColorRequested() {
|
|
8531
|
-
|
|
8532
|
-
return value !== void 0 && value !== "";
|
|
9206
|
+
return readSetEnv(ENV_NO_COLOR) !== null;
|
|
8533
9207
|
}
|
|
8534
9208
|
function drawsOnTerminal() {
|
|
8535
9209
|
return process.stdout.isTTY === true && !noColorRequested();
|
|
@@ -8660,6 +9334,20 @@ const db = defineCommand({
|
|
|
8660
9334
|
default: "list"
|
|
8661
9335
|
});
|
|
8662
9336
|
//#endregion
|
|
9337
|
+
//#region package.json
|
|
9338
|
+
var name = "@metabase/rde";
|
|
9339
|
+
var version = "0.1.18";
|
|
9340
|
+
var description = "Prepares a local machine for data-engineering work with Metabase and coding agents";
|
|
9341
|
+
//#endregion
|
|
9342
|
+
//#region src/version.ts
|
|
9343
|
+
const PACKAGE_NAME = name;
|
|
9344
|
+
const VERSION = version;
|
|
9345
|
+
const ROOT_META = {
|
|
9346
|
+
name: "rde",
|
|
9347
|
+
version: VERSION,
|
|
9348
|
+
description
|
|
9349
|
+
};
|
|
9350
|
+
//#endregion
|
|
8663
9351
|
//#region src/wizard/engine.ts
|
|
8664
9352
|
function requireKnownStep(steps, id, flag) {
|
|
8665
9353
|
if (id === void 0) return;
|
|
@@ -8756,6 +9444,8 @@ async function runWizard(steps, ctx, options = {}) {
|
|
|
8756
9444
|
}
|
|
8757
9445
|
//#endregion
|
|
8758
9446
|
//#region src/steps/doctor.ts
|
|
9447
|
+
const RDE_DIST_TAG = "latest";
|
|
9448
|
+
const RDE_RELEASE_TIMEOUT_MS = 3e3;
|
|
8759
9449
|
function hostDoctorDeps(instanceName, signal) {
|
|
8760
9450
|
const runner = processRunner(signal);
|
|
8761
9451
|
const runtime = hostRuntimeDeps(instanceName, signal);
|
|
@@ -8775,7 +9465,8 @@ function hostDoctorDeps(instanceName, signal) {
|
|
|
8775
9465
|
cwd: process.cwd(),
|
|
8776
9466
|
stateHome: xdgStateHome(),
|
|
8777
9467
|
files: hostSkillFiles
|
|
8778
|
-
}
|
|
9468
|
+
},
|
|
9469
|
+
publishedRdeVersion: () => npmRegistry(signal, { timeoutMs: RDE_RELEASE_TIMEOUT_MS }).distTagVersion(PACKAGE_NAME, RDE_DIST_TAG)
|
|
8779
9470
|
};
|
|
8780
9471
|
}
|
|
8781
9472
|
const NOT_FOUND_STATUS = 404;
|
|
@@ -8869,6 +9560,26 @@ function mbRow(diagnosis, lines, deps) {
|
|
|
8869
9560
|
case "current": return ok("mb", MB_LABEL, lines.join("\n"));
|
|
8870
9561
|
}
|
|
8871
9562
|
}
|
|
9563
|
+
const RDE_LABEL = "rde";
|
|
9564
|
+
const RDE_UPDATE_HINT = `\`npm i -g ${PACKAGE_NAME}@${RDE_DIST_TAG}\``;
|
|
9565
|
+
function rdeUncheckedRow(reason) {
|
|
9566
|
+
return skipped("rde", RDE_LABEL, `${VERSION}; could not check for a newer release: ${reason}`);
|
|
9567
|
+
}
|
|
9568
|
+
async function rdeRow(deps) {
|
|
9569
|
+
let raw;
|
|
9570
|
+
try {
|
|
9571
|
+
raw = await deps.publishedRdeVersion();
|
|
9572
|
+
} catch (error) {
|
|
9573
|
+
const reason = lookupFailure(error);
|
|
9574
|
+
if (reason === null) throw error;
|
|
9575
|
+
return rdeUncheckedRow(reason);
|
|
9576
|
+
}
|
|
9577
|
+
const published = semver.valid(raw);
|
|
9578
|
+
if (published === null) return rdeUncheckedRow(`the registry names "${raw}" for ${RDE_DIST_TAG}, which is not a version`);
|
|
9579
|
+
if (semver.lt(VERSION, published)) return note$1("rde", RDE_LABEL, `${VERSION}; ${published} is released`, RDE_UPDATE_HINT);
|
|
9580
|
+
if (semver.gt(VERSION, published)) return ok("rde", RDE_LABEL, `${VERSION}, ahead of the latest release ${published}`);
|
|
9581
|
+
return ok("rde", RDE_LABEL, `${VERSION}, the latest release`);
|
|
9582
|
+
}
|
|
8872
9583
|
async function mbFinding(deps) {
|
|
8873
9584
|
const found = await inspectMbOnPath(deps.runner, deps.locateMb);
|
|
8874
9585
|
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
@@ -8889,6 +9600,20 @@ function publishesOnLoopback(container, port) {
|
|
|
8889
9600
|
}
|
|
8890
9601
|
const CONTAINER = "the container";
|
|
8891
9602
|
const CONTAINER_SILENT_PORT_HINT = "Docker's forward of the port is broken; `rde start` repairs it";
|
|
9603
|
+
function containerSettingsFailure(runtime, inspection) {
|
|
9604
|
+
const onMaster = !developmentMode() && isMasterBuildImage(runtime.image);
|
|
9605
|
+
const drift = containerEnvDrift(inspection, runtime);
|
|
9606
|
+
const problems = [];
|
|
9607
|
+
if (onMaster) problems.push(`${runtime.containerName} runs ${runtime.image}, a development build of Metabase`);
|
|
9608
|
+
if (drift.length > 0) problems.push(`${runtime.containerName} was created with ${drift.join(", ")} set otherwise than rde sets them now`);
|
|
9609
|
+
if (problems.length === 0) return null;
|
|
9610
|
+
const hint = onMaster ? "a build of master is ahead of every release, so `rde uninstall`, then `rde init`, sets up an instance on a release; `RDE_DEV=1` keeps running this one" : "`rde upgrade` creates the container again with rde's settings";
|
|
9611
|
+
return {
|
|
9612
|
+
detail: problems.join("\n"),
|
|
9613
|
+
hint,
|
|
9614
|
+
step: null
|
|
9615
|
+
};
|
|
9616
|
+
}
|
|
8892
9617
|
async function containerRow(runtime, deps, dockerOk) {
|
|
8893
9618
|
const label = "container";
|
|
8894
9619
|
if (!dockerOk) return {
|
|
@@ -8929,6 +9654,11 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
8929
9654
|
}),
|
|
8930
9655
|
...found
|
|
8931
9656
|
};
|
|
9657
|
+
const settings = containerSettingsFailure(runtime, inspection);
|
|
9658
|
+
if (settings !== null) return {
|
|
9659
|
+
row: failed("container", label, settings),
|
|
9660
|
+
...found
|
|
9661
|
+
};
|
|
8932
9662
|
const state = inspection.running ? "running" : "stopped";
|
|
8933
9663
|
return {
|
|
8934
9664
|
row: ok("container", label, `${runtime.containerName} ${state} (${runtime.image})`),
|
|
@@ -9407,25 +10137,71 @@ async function skillCopiesRow(globalEntries, deps) {
|
|
|
9407
10137
|
const stale = judged.filter((copy) => !copy.current);
|
|
9408
10138
|
return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
|
|
9409
10139
|
}
|
|
10140
|
+
function dataAppBranchLine(version, origin) {
|
|
10141
|
+
const wanted = withRef(origin.repository, origin.ref);
|
|
10142
|
+
return version === null ? `Metabase has not reported its version yet; rde installs them from ${wanted}` : `Metabase ${version} takes them from ${wanted}`;
|
|
10143
|
+
}
|
|
10144
|
+
async function dataAppSkillsRow(state, installed, deps) {
|
|
10145
|
+
const id = "data-app-skills";
|
|
10146
|
+
const label = "data-app skills";
|
|
10147
|
+
const branch = dataAppSkillsOrigin(wantedDataAppRef(state.version));
|
|
10148
|
+
const wanted = [...DATA_APP_SKILL_NAMES.map((name) => ({
|
|
10149
|
+
name,
|
|
10150
|
+
origin: branch
|
|
10151
|
+
})), {
|
|
10152
|
+
name: RDE_DATA_APPS_SKILL_NAME,
|
|
10153
|
+
origin: skillOrigin(deps.skills.source, deps.skills.cwd)
|
|
10154
|
+
}];
|
|
10155
|
+
const lines = [dataAppBranchLine(state.version, branch)];
|
|
10156
|
+
let current = true;
|
|
10157
|
+
for (const skill of wanted) {
|
|
10158
|
+
const entry = installed.find((candidate) => candidate.name === skill.name);
|
|
10159
|
+
if (entry === void 0) {
|
|
10160
|
+
lines.push(`${skill.name}: not installed`);
|
|
10161
|
+
current = false;
|
|
10162
|
+
continue;
|
|
10163
|
+
}
|
|
10164
|
+
const judged = await judgeCopy(entry, skill.origin, deps);
|
|
10165
|
+
lines.push(judged.line);
|
|
10166
|
+
current &&= judged.current;
|
|
10167
|
+
}
|
|
10168
|
+
const detail = lines.join("\n");
|
|
10169
|
+
if (current) return ok(id, label, detail);
|
|
10170
|
+
return failed(id, label, {
|
|
10171
|
+
detail,
|
|
10172
|
+
hint: `\`rde init --only ${SKILL_STEP}\` installs them; then restart the agent session`,
|
|
10173
|
+
step: SKILL_STEP
|
|
10174
|
+
});
|
|
10175
|
+
}
|
|
9410
10176
|
async function skillRows(state, deps) {
|
|
9411
10177
|
const label = "rde skill";
|
|
9412
10178
|
if (state === null) return [skipped("skill", label, NO_INSTANCE_REASON)];
|
|
9413
10179
|
if (state.steps[SKILL_STEP]?.status === "skipped") return [skipped("skill", label, `skipped during init; \`rde init --only ${SKILL_STEP}\``)];
|
|
9414
10180
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
9415
10181
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
9416
|
-
const
|
|
10182
|
+
const installed = await skillsCli(deps.runner).list();
|
|
10183
|
+
const entries = installed.filter((entry) => entry.name === "rde");
|
|
9417
10184
|
const copies = await skillCopiesRow(entries, deps);
|
|
9418
|
-
|
|
9419
|
-
|
|
9420
|
-
|
|
9421
|
-
|
|
9422
|
-
|
|
9423
|
-
|
|
9424
|
-
|
|
9425
|
-
|
|
9426
|
-
|
|
9427
|
-
|
|
9428
|
-
|
|
10185
|
+
const dataApps = await dataAppSkillsRow(state, installed, deps);
|
|
10186
|
+
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [
|
|
10187
|
+
failed("skill", label, {
|
|
10188
|
+
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
10189
|
+
hint: `\`rde init --only ${SKILL_STEP}\``,
|
|
10190
|
+
step: SKILL_STEP
|
|
10191
|
+
}),
|
|
10192
|
+
copies,
|
|
10193
|
+
dataApps
|
|
10194
|
+
];
|
|
10195
|
+
return [
|
|
10196
|
+
...detected.map((agent) => {
|
|
10197
|
+
const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
|
|
10198
|
+
const agentLabel = `${label} (${agent.displayName})`;
|
|
10199
|
+
if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
|
|
10200
|
+
return ok(skillRowId(agent), agentLabel, entry.path);
|
|
10201
|
+
}),
|
|
10202
|
+
copies,
|
|
10203
|
+
dataApps
|
|
10204
|
+
];
|
|
9429
10205
|
}
|
|
9430
10206
|
function noRuntimeRows() {
|
|
9431
10207
|
return [
|
|
@@ -9470,7 +10246,11 @@ async function collectDoctor(instance, state, secrets, deps) {
|
|
|
9470
10246
|
const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
|
|
9471
10247
|
const mb = await mbFinding(deps);
|
|
9472
10248
|
const profiles = sharedProfileList(deps);
|
|
9473
|
-
const checks = [
|
|
10249
|
+
const checks = [
|
|
10250
|
+
...machine,
|
|
10251
|
+
mb.row,
|
|
10252
|
+
await rdeRow(deps)
|
|
10253
|
+
];
|
|
9474
10254
|
if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE_REASON), skipped("admin", "admin user", NO_INSTANCE_REASON), skipped("admin-password", "admin password", NO_INSTANCE_REASON), skipped("api-key", "API key", NO_INSTANCE_REASON), skipped("secrets-file", "secrets file", NO_INSTANCE_REASON), skipped("state-file", "state file", NO_INSTANCE_REASON), skipped("mb-profile", "mb profile", NO_INSTANCE_REASON), skipped("license", "license", NO_INSTANCE_REASON), skipped("databases", "databases", NO_INSTANCE_REASON), skipped("git-sync", "remote sync", NO_INSTANCE_REASON));
|
|
9475
10255
|
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
9476
10256
|
const rdeProfile = state === null ? null : state.mbProfile;
|
|
@@ -9666,7 +10446,8 @@ function applyRecord(draft, record) {
|
|
|
9666
10446
|
else if (key === API_KEY_KEYS.profile && value !== null) {
|
|
9667
10447
|
const profile = MbProfileChoice.safeParse(value);
|
|
9668
10448
|
if (profile.success) draft.answers["api-key"] = { profile: profile.data };
|
|
9669
|
-
} else if (key ===
|
|
10449
|
+
} else if (key === SYNC_SCOPE_KEYS.export && record.answer.kind === "confirm") draft.answers["sync-scope"] = { export: record.answer.value };
|
|
10450
|
+
else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
|
|
9670
10451
|
}
|
|
9671
10452
|
function writableUri(uri, index) {
|
|
9672
10453
|
const trimmed = uri.trim();
|
|
@@ -10127,7 +10908,7 @@ function taskLabel(status) {
|
|
|
10127
10908
|
}
|
|
10128
10909
|
function remoteContentOf(changes) {
|
|
10129
10910
|
if (!changes.has_changes) return "imported";
|
|
10130
|
-
return changes
|
|
10911
|
+
return remoteMoved(changes) ? "changed" : "never-imported";
|
|
10131
10912
|
}
|
|
10132
10913
|
function isMbFailure(error) {
|
|
10133
10914
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
@@ -10145,7 +10926,8 @@ async function gitSyncOf(ctx, deps) {
|
|
|
10145
10926
|
branch: status.branch,
|
|
10146
10927
|
dirty: status.is_dirty,
|
|
10147
10928
|
remote: remoteContentOf(changes),
|
|
10148
|
-
task: taskLabel(status)
|
|
10929
|
+
task: taskLabel(status),
|
|
10930
|
+
syncedCollections: status.synced_collections.map((collection) => collection.name)
|
|
10149
10931
|
};
|
|
10150
10932
|
} catch (error) {
|
|
10151
10933
|
if (isMbFailure(error)) return { error: error.message };
|
|
@@ -10205,7 +10987,8 @@ function gitSyncLine(report) {
|
|
|
10205
10987
|
if (isGitSyncFailure(report.gitSync)) return `Remote sync: ${report.gitSync.error}`;
|
|
10206
10988
|
const branch = report.gitSync.branch === null ? "no branch" : `branch ${report.gitSync.branch}`;
|
|
10207
10989
|
const task = report.gitSync.task === null ? "no task running" : `task ${report.gitSync.task}`;
|
|
10208
|
-
|
|
10990
|
+
const scope = report.gitSync.syncedCollections.length === 0 ? "nothing synced; `rde init --only sync-scope`" : `synced: ${report.gitSync.syncedCollections.join(", ")}`;
|
|
10991
|
+
return `Remote sync: ${branch}, ${syncStateLabel(report.mbProfile, report.gitSync)}, ${task}, ${scope}`;
|
|
10209
10992
|
}
|
|
10210
10993
|
const REMOTE_CONTENT_LABEL = {
|
|
10211
10994
|
imported: null,
|
|
@@ -10593,7 +11376,7 @@ function keptOnDisk(options, state, dirPresent) {
|
|
|
10593
11376
|
const kept = [];
|
|
10594
11377
|
if (options.keepData && dirPresent) kept.push(`Instance directory ${instanceDir(options.instance)} (--keep-data); \`rde init\` reinstalls from it`);
|
|
10595
11378
|
if (state?.gitRemote !== null && state?.gitRemote !== void 0) kept.push(`Working directory ${state.gitRemote.workdir} and its git history`);
|
|
10596
|
-
if (!options.removeSkill) kept.push(`The
|
|
11379
|
+
if (!options.removeSkill) kept.push(`The ${RDE_SOURCE_SKILL_NAMES.join(" and ")} skills for your agents; pass --remove-skill to remove them`);
|
|
10597
11380
|
return kept;
|
|
10598
11381
|
}
|
|
10599
11382
|
async function planUninstall(options, deps) {
|
|
@@ -10700,6 +11483,9 @@ function reportLines(report) {
|
|
|
10700
11483
|
function leftBehind(report) {
|
|
10701
11484
|
return [...report.results.flatMap((result) => result.outcome === "failed" ? [result.label] : []), ...report.problems.map((problem) => problem.subject)];
|
|
10702
11485
|
}
|
|
11486
|
+
function uninstallCommandLine(options) {
|
|
11487
|
+
return options.instance === options.defaultInstance ? "rde uninstall" : `rde uninstall --instance ${options.instance}`;
|
|
11488
|
+
}
|
|
10703
11489
|
async function uninstall(options, deps, io) {
|
|
10704
11490
|
const plan = await planUninstall(options, deps);
|
|
10705
11491
|
for (const problem of plan.problems) io.ui.warn(`${problem.subject} ${problem.reason}`);
|
|
@@ -10710,7 +11496,7 @@ async function uninstall(options, deps, io) {
|
|
|
10710
11496
|
problems: plan.problems
|
|
10711
11497
|
};
|
|
10712
11498
|
}
|
|
10713
|
-
io.ui.note(planLines$1(plan).join("\n"),
|
|
11499
|
+
io.ui.note(planLines$1(plan).join("\n"), uninstallCommandLine(options));
|
|
10714
11500
|
if (plan.unpulled !== null) {
|
|
10715
11501
|
io.ui.warn(unpulledWarning(plan.unpulled));
|
|
10716
11502
|
if ((options.yes || !options.terminal) && !options.force) throw new ConfigError(unpulledRefusal(plan.unpulled));
|
|
@@ -10749,7 +11535,7 @@ const uninstallCommand = defineRdeCommand({
|
|
|
10749
11535
|
},
|
|
10750
11536
|
"remove-skill": {
|
|
10751
11537
|
type: "boolean",
|
|
10752
|
-
description: "Also remove the rde
|
|
11538
|
+
description: "Also remove the rde and rde-data-apps skills from your coding agents",
|
|
10753
11539
|
default: false
|
|
10754
11540
|
},
|
|
10755
11541
|
"remove-image": {
|
|
@@ -10770,11 +11556,13 @@ const uninstallCommand = defineRdeCommand({
|
|
|
10770
11556
|
},
|
|
10771
11557
|
interrupted: INTERRUPTED,
|
|
10772
11558
|
async run(args) {
|
|
10773
|
-
const
|
|
11559
|
+
const defaultInstance = await instanceNameOrDefault();
|
|
11560
|
+
const instance = args.instance ?? defaultInstance;
|
|
10774
11561
|
if (!isInstanceName(instance)) throw new ConfigError(`${args.instance === void 0 ? `the default instance in ${globalConfigPath()},` : "--instance"} ${instance} is not an instance name (${INSTANCE_NAME_RULE})`);
|
|
10775
11562
|
const ui = terminalUi();
|
|
10776
11563
|
const report = await uninstall({
|
|
10777
11564
|
instance,
|
|
11565
|
+
defaultInstance,
|
|
10778
11566
|
keepData: args["keep-data"],
|
|
10779
11567
|
removeSkill: args["remove-skill"],
|
|
10780
11568
|
removeImage: args["remove-image"],
|
|
@@ -10800,35 +11588,59 @@ function hostDeps(ctx) {
|
|
|
10800
11588
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
10801
11589
|
timing: DEFAULT_HEALTH_TIMING,
|
|
10802
11590
|
releases: releaseLookup(ctx.signal),
|
|
10803
|
-
now: () => /* @__PURE__ */ new Date()
|
|
11591
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
11592
|
+
skills: (agents) => agentsSkillsStep(agents, "published")
|
|
10804
11593
|
};
|
|
10805
11594
|
}
|
|
10806
|
-
function
|
|
10807
|
-
const
|
|
10808
|
-
|
|
10809
|
-
|
|
11595
|
+
function majorMove(from, to) {
|
|
11596
|
+
const fromMajor = releaseMajor(from);
|
|
11597
|
+
const toMajor = to === null ? null : releaseMajor(to);
|
|
11598
|
+
if (fromMajor === null || toMajor === null || fromMajor === toMajor) return null;
|
|
11599
|
+
return {
|
|
11600
|
+
from: fromMajor,
|
|
11601
|
+
to: toMajor
|
|
11602
|
+
};
|
|
10810
11603
|
}
|
|
10811
11604
|
function releaseOf(tag) {
|
|
10812
11605
|
const match = RELEASE_TAG.exec(tag);
|
|
10813
11606
|
if (match === null || match[1] === void 0) return null;
|
|
10814
11607
|
return semver.coerce(match[1]);
|
|
10815
11608
|
}
|
|
10816
|
-
function
|
|
10817
|
-
if (targetTag
|
|
11609
|
+
function isDowngrade(current, targetTag) {
|
|
11610
|
+
if (isMajorLineTag(targetTag)) {
|
|
11611
|
+
const running = releaseMajor(current);
|
|
11612
|
+
const target = releaseMajor(targetTag);
|
|
11613
|
+
return running !== null && target !== null && target < running;
|
|
11614
|
+
}
|
|
10818
11615
|
const running = releaseOf(current);
|
|
10819
11616
|
const target = releaseOf(targetTag);
|
|
10820
|
-
|
|
11617
|
+
return running !== null && target !== null && semver.lt(target, running);
|
|
11618
|
+
}
|
|
11619
|
+
function downgradeReason(current, targetTag) {
|
|
11620
|
+
if (targetTag === null || !isDowngrade(current, targetTag)) return null;
|
|
10821
11621
|
return `refusing to downgrade from ${current} to ${targetTag}: Metabase migrates its application database forward only; a fresh instance (\`rde uninstall\`, then \`rde init\`) is the way to run an older release`;
|
|
10822
11622
|
}
|
|
10823
11623
|
function backupStamp(now) {
|
|
10824
11624
|
return now.toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
|
|
10825
11625
|
}
|
|
10826
|
-
async function ensureRunning(ctx, deps, client) {
|
|
10827
|
-
await startMetabaseStep(() => deps).run(ctx);
|
|
11626
|
+
async function ensureRunning(ctx, deps, client, accepted) {
|
|
11627
|
+
await startMetabaseStep(() => deps, accepted).run(ctx);
|
|
10828
11628
|
return runningVersion(client);
|
|
10829
11629
|
}
|
|
10830
|
-
function
|
|
10831
|
-
const
|
|
11630
|
+
function requireSupported(version) {
|
|
11631
|
+
const refusal = unsupportedVersionReason(version, developmentMode());
|
|
11632
|
+
if (refusal !== null) throw new UnsupportedMetabaseError(refusal);
|
|
11633
|
+
}
|
|
11634
|
+
function knownTargetVersion(target) {
|
|
11635
|
+
const tag = target.releaseTag;
|
|
11636
|
+
return tag !== null && (isReleaseTag(tag) || isMajorLineTag(tag)) ? tag : null;
|
|
11637
|
+
}
|
|
11638
|
+
const TO_ONE = "`--to <image or release tag>` names a Metabase to move to";
|
|
11639
|
+
const UPGRADE_WAYS_OUT = {
|
|
11640
|
+
named: TO_ONE,
|
|
11641
|
+
unreleased: TO_ONE
|
|
11642
|
+
};
|
|
11643
|
+
function dockerTargetOf(runtime, image, ownImage) {
|
|
10832
11644
|
return {
|
|
10833
11645
|
kind: "docker",
|
|
10834
11646
|
next: {
|
|
@@ -10836,22 +11648,24 @@ function dockerTarget(runtime, to) {
|
|
|
10836
11648
|
image
|
|
10837
11649
|
},
|
|
10838
11650
|
label: image,
|
|
10839
|
-
releaseTag: imageTag(image)
|
|
11651
|
+
releaseTag: imageTag(image),
|
|
11652
|
+
ownImage
|
|
10840
11653
|
};
|
|
10841
11654
|
}
|
|
11655
|
+
async function dockerTarget(ctx, runtime, to, releases) {
|
|
11656
|
+
if (to !== null) return dockerTargetOf(runtime, to, false);
|
|
11657
|
+
if (developmentMode()) return dockerTargetOf(runtime, runtime.image, false);
|
|
11658
|
+
if (!isMetabaseImage(runtime.image)) return dockerTargetOf(runtime, runtime.image, true);
|
|
11659
|
+
return dockerTargetOf(runtime, await releaseImage(ctx, releases, UPGRADE_WAYS_OUT), false);
|
|
11660
|
+
}
|
|
10842
11661
|
async function jarTarget(ctx, runtime, to, releases) {
|
|
10843
11662
|
const spec = to ?? "latest";
|
|
10844
11663
|
const parsed = parseJarSpec(spec);
|
|
10845
|
-
if (parsed === null) throw new ConfigError(`--to ${spec} is not latest,
|
|
10846
|
-
|
|
10847
|
-
|
|
10848
|
-
|
|
10849
|
-
|
|
10850
|
-
source = {
|
|
10851
|
-
kind: "release",
|
|
10852
|
-
version
|
|
10853
|
-
};
|
|
10854
|
-
} else source = parsed;
|
|
11664
|
+
if (parsed === null) throw new ConfigError(`--to ${spec} is not latest, an enterprise release tag such as v1.65.0, or the path of a .jar file`);
|
|
11665
|
+
const source = parsed.kind === "latest" ? {
|
|
11666
|
+
kind: "release",
|
|
11667
|
+
version: await latestJarRelease(ctx, releases, UPGRADE_WAYS_OUT)
|
|
11668
|
+
} : parsed;
|
|
10855
11669
|
return {
|
|
10856
11670
|
kind: "jar",
|
|
10857
11671
|
previous: runtime,
|
|
@@ -10865,7 +11679,7 @@ async function jarTarget(ctx, runtime, to, releases) {
|
|
|
10865
11679
|
}
|
|
10866
11680
|
function resolveTarget(ctx, configured, to, releases) {
|
|
10867
11681
|
switch (configured.runtime.kind) {
|
|
10868
|
-
case "docker": return
|
|
11682
|
+
case "docker": return dockerTarget(ctx, configured.runtime, to, releases);
|
|
10869
11683
|
case "jar": return jarTarget(ctx, configured.runtime, to, releases);
|
|
10870
11684
|
}
|
|
10871
11685
|
}
|
|
@@ -10915,9 +11729,13 @@ const SWITCH_TEXT = {
|
|
|
10915
11729
|
jar: "Metabase stops and starts again from the target jar."
|
|
10916
11730
|
};
|
|
10917
11731
|
function planLines(configured, current, target, backup) {
|
|
11732
|
+
const notes = [];
|
|
11733
|
+
if (target.kind === "docker" && target.ownImage) notes.push("the instance's own image pulled again, since only Metabase's images move to the newest release");
|
|
11734
|
+
if (target.releaseTag === null || !isReleaseTag(target.releaseTag)) notes.push("its version shows once it runs");
|
|
11735
|
+
const annotation = notes.length === 0 ? "" : ` (${notes.join("; ")})`;
|
|
10918
11736
|
return [
|
|
10919
11737
|
`Running: ${current} (${describeRuntime(configured.runtime)})`,
|
|
10920
|
-
`Target: ${target.label}`,
|
|
11738
|
+
`Target: ${target.label}${annotation}`,
|
|
10921
11739
|
`Backup: ${backup}`,
|
|
10922
11740
|
"",
|
|
10923
11741
|
SWITCH_TEXT[configured.runtime.kind],
|
|
@@ -10976,31 +11794,70 @@ async function switchTo(ctx, deps, target) {
|
|
|
10976
11794
|
}
|
|
10977
11795
|
spinner.stop(`Installed ${target.label}`);
|
|
10978
11796
|
}
|
|
11797
|
+
function failedStartTitle(error) {
|
|
11798
|
+
if (error instanceof MetabaseUnreachableError) return "Metabase did not come up on the new version";
|
|
11799
|
+
if (error instanceof UnsupportedMetabaseError) return "The new version is not one rde runs";
|
|
11800
|
+
return null;
|
|
11801
|
+
}
|
|
10979
11802
|
async function startUpgraded(ctx, deps, client, previous, backup) {
|
|
10980
11803
|
try {
|
|
10981
|
-
return await ensureRunning(ctx, deps, client);
|
|
11804
|
+
return await ensureRunning(ctx, deps, client, "supported");
|
|
10982
11805
|
} catch (error) {
|
|
10983
|
-
|
|
11806
|
+
const title = failedStartTitle(error);
|
|
11807
|
+
if (title !== null) ctx.ui.note(restoreLines(ctx, previous, deps.runtime.dataDir, backup).join("\n"), title);
|
|
10984
11808
|
throw error;
|
|
10985
11809
|
}
|
|
10986
11810
|
}
|
|
11811
|
+
async function recreateStale(ctx, deps, previous, target) {
|
|
11812
|
+
const next = target.next;
|
|
11813
|
+
const container = await deps.runtime.docker.containerInspect(next.containerName);
|
|
11814
|
+
const drift = container.exists ? containerEnvDrift(container, next) : [];
|
|
11815
|
+
const renamed = previous.kind === "docker" && previous.image !== next.image;
|
|
11816
|
+
if (drift.length === 0 && !renamed) return false;
|
|
11817
|
+
if (renamed) {
|
|
11818
|
+
ctx.ui.info(`${previous.image} is the image ${next.image} names; creating ${next.containerName} again under ${next.image}`);
|
|
11819
|
+
await ctx.saveState({
|
|
11820
|
+
...ctx.state,
|
|
11821
|
+
runtime: next
|
|
11822
|
+
});
|
|
11823
|
+
}
|
|
11824
|
+
if (drift.length > 0) ctx.ui.info(`${next.containerName} was created with ${drift.join(", ")} set otherwise than rde sets them now; creating it again from the image it runs`);
|
|
11825
|
+
await runtimeFromState(next, deps.runtime).reconfigure();
|
|
11826
|
+
return true;
|
|
11827
|
+
}
|
|
11828
|
+
async function alreadyOn(ctx, deps, client, configured, current, target) {
|
|
11829
|
+
if (target.kind === "docker" && await recreateStale(ctx, deps, configured.runtime, target)) {
|
|
11830
|
+
await ensureRunning(ctx, deps, client, "supported");
|
|
11831
|
+
ctx.ui.success(`Already on ${current}; ${target.next.containerName} runs ${target.label} with rde's settings now`);
|
|
11832
|
+
return {
|
|
11833
|
+
outcome: "recreated",
|
|
11834
|
+
version: current
|
|
11835
|
+
};
|
|
11836
|
+
}
|
|
11837
|
+
ctx.ui.success(`Already on ${current}: ${target.label} is what the instance runs`);
|
|
11838
|
+
return {
|
|
11839
|
+
outcome: "unchanged",
|
|
11840
|
+
version: current
|
|
11841
|
+
};
|
|
11842
|
+
}
|
|
10987
11843
|
async function upgrade(ctx, options, deps = hostDeps) {
|
|
10988
11844
|
const resolved = deps(ctx);
|
|
10989
11845
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
10990
11846
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
10991
|
-
const current = await ensureRunning(ctx, resolved, client);
|
|
10992
11847
|
const target = await resolveTarget(ctx, configured, options.to, resolved.releases);
|
|
11848
|
+
const current = await ensureRunning(ctx, resolved, client, "any");
|
|
10993
11849
|
const refusal = downgradeReason(current, target.releaseTag);
|
|
10994
11850
|
if (refusal !== null) throw new ConfigError(refusal);
|
|
11851
|
+
const known = knownTargetVersion(target);
|
|
11852
|
+
if (known !== null) requireSupported(known);
|
|
10995
11853
|
if (await fetchTarget(ctx, resolved, target, current)) {
|
|
10996
|
-
|
|
10997
|
-
return
|
|
10998
|
-
outcome: "unchanged",
|
|
10999
|
-
version: current
|
|
11000
|
-
};
|
|
11854
|
+
requireSupported(current);
|
|
11855
|
+
return alreadyOn(ctx, resolved, client, configured, current, target);
|
|
11001
11856
|
}
|
|
11002
11857
|
const backup = join(instanceBackupsDir(ctx.state.name), backupStamp(resolved.now()));
|
|
11003
11858
|
ctx.ui.note(planLines(configured, current, target, backup).join("\n"), "rde upgrade");
|
|
11859
|
+
const announced = majorMove(current, target.releaseTag);
|
|
11860
|
+
if (announced !== null) ctx.ui.warn(`This moves Metabase from ${announced.from} to ${announced.to}, a new major version; the data-app skills follow it after the upgrade`);
|
|
11004
11861
|
if (!options.yes) {
|
|
11005
11862
|
if (!await ctx.prompts.confirm({
|
|
11006
11863
|
message: "Upgrade now?",
|
|
@@ -11021,6 +11878,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11021
11878
|
options.onPhase("starting", backup);
|
|
11022
11879
|
const to = await startUpgraded(ctx, resolved, client, configured, backup);
|
|
11023
11880
|
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
11881
|
+
const moved = majorMove(current, to);
|
|
11882
|
+
if (moved !== null) {
|
|
11883
|
+
if (announced === null) ctx.ui.warn(`This moved Metabase from ${moved.from} to ${moved.to}, a new major version; the data-app skills follow it`);
|
|
11884
|
+
await runWizard([resolved.skills(options.yes ? DETECT_AGENTS : ASK_AGENTS)], ctx);
|
|
11885
|
+
}
|
|
11024
11886
|
return {
|
|
11025
11887
|
outcome: "upgraded",
|
|
11026
11888
|
from: current,
|
|
@@ -11046,7 +11908,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11046
11908
|
args: {
|
|
11047
11909
|
to: {
|
|
11048
11910
|
type: "string",
|
|
11049
|
-
description: "Docker: image to upgrade to (default: the instance's image
|
|
11911
|
+
description: "Docker: image to upgrade to (default: the major line of the newest Metabase Enterprise release, such as metabase/metabase-enterprise:v1.65.x; the instance's own image pulled again with RDE_DEV=1 or for an image from outside Metabase's repositories); jar: latest (the newest release), a release tag, or a .jar path (default: latest)"
|
|
11050
11912
|
},
|
|
11051
11913
|
yes: {
|
|
11052
11914
|
type: "boolean",
|
|
@@ -11074,11 +11936,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11074
11936
|
//#endregion
|
|
11075
11937
|
//#region src/main.ts
|
|
11076
11938
|
const main = defineCommand({
|
|
11077
|
-
meta:
|
|
11078
|
-
name: "rde",
|
|
11079
|
-
version: "0.1.16",
|
|
11080
|
-
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
11081
|
-
},
|
|
11939
|
+
meta: ROOT_META,
|
|
11082
11940
|
subCommands: {
|
|
11083
11941
|
init,
|
|
11084
11942
|
start,
|