@metabase/rde 0.1.15 → 0.1.17
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 +51 -57
- package/dist/cli.mjs +917 -719
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -79,6 +79,8 @@ var MetabaseApiError = class extends Error {
|
|
|
79
79
|
path;
|
|
80
80
|
errors;
|
|
81
81
|
reason;
|
|
82
|
+
code;
|
|
83
|
+
detail;
|
|
82
84
|
constructor(failure) {
|
|
83
85
|
super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
|
|
84
86
|
this.name = "MetabaseApiError";
|
|
@@ -86,6 +88,8 @@ var MetabaseApiError = class extends Error {
|
|
|
86
88
|
this.path = failure.path;
|
|
87
89
|
this.errors = failure.errors;
|
|
88
90
|
this.reason = failure.message;
|
|
91
|
+
this.code = failure.code;
|
|
92
|
+
this.detail = failure.detail;
|
|
89
93
|
}
|
|
90
94
|
};
|
|
91
95
|
const UNAUTHORIZED_STATUS = 401;
|
|
@@ -123,12 +127,10 @@ var DownloadError = class extends Error {
|
|
|
123
127
|
};
|
|
124
128
|
//#endregion
|
|
125
129
|
//#region src/output/error.ts
|
|
126
|
-
const INTERRUPTED_MESSAGE = "Interrupted. Run `rde init` again to resume.";
|
|
127
130
|
const FAILURE_EXIT_CODE = 1;
|
|
128
131
|
const USAGE_EXIT_CODE = 2;
|
|
129
132
|
function exitCodeFor(error) {
|
|
130
133
|
if (error instanceof StepFailedError) return exitCodeFor(error.cause);
|
|
131
|
-
if (error instanceof AbortError) return 130;
|
|
132
134
|
if (error instanceof ConfigError) return USAGE_EXIT_CODE;
|
|
133
135
|
return FAILURE_EXIT_CODE;
|
|
134
136
|
}
|
|
@@ -140,7 +142,6 @@ function stepFailureMessage(step, cause) {
|
|
|
140
142
|
}
|
|
141
143
|
function messageFor$1(error) {
|
|
142
144
|
if (error instanceof StepFailedError) return stepFailureMessage(error.step, messageFor$1(error.cause));
|
|
143
|
-
if (error instanceof AbortError) return INTERRUPTED_MESSAGE;
|
|
144
145
|
if (error instanceof Error) return error.message;
|
|
145
146
|
return String(error);
|
|
146
147
|
}
|
|
@@ -148,10 +149,15 @@ function reportError(error) {
|
|
|
148
149
|
process.stderr.write(messageFor$1(error) + "\n");
|
|
149
150
|
process.exitCode = exitCodeFor(error);
|
|
150
151
|
}
|
|
152
|
+
function reportInterrupted(message) {
|
|
153
|
+
if (message !== null) process.stderr.write(message + "\n");
|
|
154
|
+
process.exitCode = 130;
|
|
155
|
+
}
|
|
151
156
|
//#endregion
|
|
152
157
|
//#region src/commands/define.ts
|
|
153
158
|
const SHORT_PREFIX = "-";
|
|
154
159
|
const END_OF_OPTIONS = "--";
|
|
160
|
+
const INTERRUPTED = "Interrupted.";
|
|
155
161
|
function repeatedValues(rawArgs, flag) {
|
|
156
162
|
const values = [];
|
|
157
163
|
const inline = `${flag}=`;
|
|
@@ -174,9 +180,14 @@ function defineRdeCommand(def) {
|
|
|
174
180
|
meta: def.meta,
|
|
175
181
|
...def.args !== void 0 && { args: def.args },
|
|
176
182
|
async run({ args, rawArgs }) {
|
|
183
|
+
const interruption = { message: def.interrupted };
|
|
177
184
|
try {
|
|
178
|
-
await def.run(args, rawArgs);
|
|
185
|
+
await def.run(args, rawArgs, interruption);
|
|
179
186
|
} catch (error) {
|
|
187
|
+
if (error instanceof AbortError) {
|
|
188
|
+
reportInterrupted(interruption.message);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
180
191
|
reportError(error);
|
|
181
192
|
}
|
|
182
193
|
}
|
|
@@ -255,9 +266,9 @@ const StepId = z.enum([
|
|
|
255
266
|
"admin-user",
|
|
256
267
|
"api-key",
|
|
257
268
|
"transforms",
|
|
258
|
-
"sample-database",
|
|
259
269
|
"license",
|
|
260
270
|
"git-sync",
|
|
271
|
+
"sync-scope",
|
|
261
272
|
"databases",
|
|
262
273
|
"summary"
|
|
263
274
|
]);
|
|
@@ -326,18 +337,15 @@ const REMOTE_SYNC_FEATURE = "remote-sync";
|
|
|
326
337
|
function hasRemoteSync(license) {
|
|
327
338
|
return license.activated && license.features.includes("remote-sync");
|
|
328
339
|
}
|
|
340
|
+
function hasLibrary(license) {
|
|
341
|
+
return license.activated && license.features.includes("library");
|
|
342
|
+
}
|
|
329
343
|
const DatabaseRecord = z.object({
|
|
330
344
|
id: z.int(),
|
|
331
345
|
name: z.string(),
|
|
332
346
|
engine: z.string(),
|
|
333
347
|
createdAt: z.string()
|
|
334
348
|
});
|
|
335
|
-
const SampleDatabaseState = z.object({
|
|
336
|
-
containerName: z.string(),
|
|
337
|
-
image: z.string(),
|
|
338
|
-
port: z.int().positive(),
|
|
339
|
-
databaseId: z.int().nullable()
|
|
340
|
-
});
|
|
341
349
|
const GitRemoteState = z.discriminatedUnion("kind", [z.object({
|
|
342
350
|
kind: z.literal("local-bare"),
|
|
343
351
|
barePath: z.string(),
|
|
@@ -354,7 +362,7 @@ const StepRecord = z.object({
|
|
|
354
362
|
completedAt: z.string()
|
|
355
363
|
});
|
|
356
364
|
const InstanceState = z.object({
|
|
357
|
-
schemaVersion: z.literal(
|
|
365
|
+
schemaVersion: z.literal(7),
|
|
358
366
|
name: z.string(),
|
|
359
367
|
runtime: RuntimeState.nullable(),
|
|
360
368
|
url: z.string().nullable(),
|
|
@@ -363,9 +371,9 @@ const InstanceState = z.object({
|
|
|
363
371
|
mbProfile: z.string(),
|
|
364
372
|
license: LicenseState,
|
|
365
373
|
databases: z.array(DatabaseRecord),
|
|
366
|
-
sampleDatabase: SampleDatabaseState.nullable(),
|
|
367
374
|
gitRemote: GitRemoteState.nullable(),
|
|
368
375
|
movedFrom: z.string().nullable(),
|
|
376
|
+
syncRoot: z.int().nullable(),
|
|
369
377
|
steps: z.partialRecord(StepId, StepRecord)
|
|
370
378
|
});
|
|
371
379
|
function configuredRuntime(state) {
|
|
@@ -395,7 +403,7 @@ function mbCommand(profile) {
|
|
|
395
403
|
}
|
|
396
404
|
function emptyInstanceState(name) {
|
|
397
405
|
return {
|
|
398
|
-
schemaVersion:
|
|
406
|
+
schemaVersion: 7,
|
|
399
407
|
name,
|
|
400
408
|
runtime: null,
|
|
401
409
|
url: null,
|
|
@@ -404,9 +412,9 @@ function emptyInstanceState(name) {
|
|
|
404
412
|
mbProfile: mbProfileFor(name),
|
|
405
413
|
license: { activated: false },
|
|
406
414
|
databases: [],
|
|
407
|
-
sampleDatabase: null,
|
|
408
415
|
gitRemote: null,
|
|
409
416
|
movedFrom: null,
|
|
417
|
+
syncRoot: null,
|
|
410
418
|
steps: {}
|
|
411
419
|
};
|
|
412
420
|
}
|
|
@@ -417,6 +425,8 @@ const V1 = 1;
|
|
|
417
425
|
const V2 = 2;
|
|
418
426
|
const V3 = 3;
|
|
419
427
|
const V4 = 4;
|
|
428
|
+
const V5 = 5;
|
|
429
|
+
const V6 = 6;
|
|
420
430
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
421
431
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
422
432
|
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
@@ -428,6 +438,11 @@ const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
|
428
438
|
password: z.string()
|
|
429
439
|
})]);
|
|
430
440
|
const StateV4 = z.object({ schemaVersion: z.literal(V4) }).loose();
|
|
441
|
+
const StateV5 = z.object({
|
|
442
|
+
schemaVersion: z.literal(V5),
|
|
443
|
+
steps: z.record(z.string(), z.unknown())
|
|
444
|
+
}).loose();
|
|
445
|
+
const StateV6 = z.object({ schemaVersion: z.literal(V6) }).loose();
|
|
431
446
|
const StateV3 = z.object({
|
|
432
447
|
schemaVersion: z.literal(V3),
|
|
433
448
|
admin: AdminV3.nullable(),
|
|
@@ -443,17 +458,32 @@ function liftV1(raw) {
|
|
|
443
458
|
function liftV2(raw) {
|
|
444
459
|
return {
|
|
445
460
|
...StateV2.parse(raw),
|
|
446
|
-
schemaVersion: V3
|
|
447
|
-
sampleDatabase: null
|
|
461
|
+
schemaVersion: V3
|
|
448
462
|
};
|
|
449
463
|
}
|
|
450
464
|
function liftV4(raw) {
|
|
451
465
|
return {
|
|
452
466
|
...StateV4.parse(raw),
|
|
453
|
-
schemaVersion:
|
|
467
|
+
schemaVersion: V5,
|
|
454
468
|
movedFrom: null
|
|
455
469
|
};
|
|
456
470
|
}
|
|
471
|
+
function liftV5(raw) {
|
|
472
|
+
const { sampleDatabase: _sampleDatabase, steps, ...state } = StateV5.parse(raw);
|
|
473
|
+
const { "sample-database": _sampleDatabaseStep, ...remainingSteps } = steps;
|
|
474
|
+
return {
|
|
475
|
+
...state,
|
|
476
|
+
schemaVersion: V6,
|
|
477
|
+
steps: remainingSteps
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
function liftV6(raw) {
|
|
481
|
+
return {
|
|
482
|
+
...StateV6.parse(raw),
|
|
483
|
+
schemaVersion: 7,
|
|
484
|
+
syncRoot: null
|
|
485
|
+
};
|
|
486
|
+
}
|
|
457
487
|
function adminV4(admin) {
|
|
458
488
|
switch (admin.kind) {
|
|
459
489
|
case "default": return {
|
|
@@ -473,11 +503,11 @@ function liftV3(raw) {
|
|
|
473
503
|
password: admin.password
|
|
474
504
|
} : null;
|
|
475
505
|
return {
|
|
476
|
-
state:
|
|
506
|
+
state: migrateInstanceState(liftV4({
|
|
477
507
|
...rest,
|
|
478
508
|
schemaVersion: V4,
|
|
479
509
|
admin: admin === null ? null : adminV4(admin)
|
|
480
|
-
})),
|
|
510
|
+
})).state,
|
|
481
511
|
movedSecrets: {
|
|
482
512
|
schemaVersion: 1,
|
|
483
513
|
admin: login,
|
|
@@ -491,15 +521,17 @@ function migrateInstanceState(raw) {
|
|
|
491
521
|
case V1: return migrateInstanceState(liftV1(raw));
|
|
492
522
|
case V2: return migrateInstanceState(liftV2(raw));
|
|
493
523
|
case V3: return liftV3(raw);
|
|
494
|
-
case V4: return
|
|
495
|
-
|
|
524
|
+
case V4: return migrateInstanceState(liftV4(raw));
|
|
525
|
+
case V5: return migrateInstanceState(liftV5(raw));
|
|
526
|
+
case V6: return {
|
|
527
|
+
state: InstanceState.parse(liftV6(raw)),
|
|
496
528
|
movedSecrets: null
|
|
497
529
|
};
|
|
498
|
-
case
|
|
530
|
+
case 7: return {
|
|
499
531
|
state: InstanceState.parse(raw),
|
|
500
532
|
movedSecrets: null
|
|
501
533
|
};
|
|
502
|
-
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected
|
|
534
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 7)`);
|
|
503
535
|
}
|
|
504
536
|
}
|
|
505
537
|
//#endregion
|
|
@@ -807,8 +839,11 @@ const SESSION_HEADER = "X-Metabase-Session";
|
|
|
807
839
|
const API_KEY_HEADER = "X-Api-Key";
|
|
808
840
|
const JSON_CONTENT_TYPE = "application/json";
|
|
809
841
|
const SERVICE_UNAVAILABLE_STATUS = 503;
|
|
842
|
+
const NO_CONTENT_STATUS = 204;
|
|
810
843
|
const NO_AUTH = { kind: "none" };
|
|
811
844
|
const ErrorMessage = z.object({ message: z.string() }).loose();
|
|
845
|
+
const ErrorTitle = z.object({ error: z.string() }).loose();
|
|
846
|
+
const ErrorCode = z.object({ error_code: z.string() }).loose();
|
|
812
847
|
const ErrorFields = z.object({ errors: z.record(z.string(), z.unknown()) }).loose();
|
|
813
848
|
const ErrorText = z.string();
|
|
814
849
|
const ErrorTexts = z.array(z.string());
|
|
@@ -871,6 +906,8 @@ function serverMessage(body) {
|
|
|
871
906
|
const value = jsonValue(body);
|
|
872
907
|
const parsed = ErrorMessage.safeParse(value);
|
|
873
908
|
if (parsed.success) return parsed.data.message;
|
|
909
|
+
const title = ErrorTitle.safeParse(value);
|
|
910
|
+
if (title.success) return title.data.error;
|
|
874
911
|
const errors = serverErrors(body);
|
|
875
912
|
if (errors !== null && Object.keys(errors).length > 0) return Object.entries(errors).map(([name, problem]) => `${name}: ${problem}`).join("; ");
|
|
876
913
|
const plain = ErrorText.safeParse(value);
|
|
@@ -879,6 +916,10 @@ function serverMessage(body) {
|
|
|
879
916
|
if (body.json.parsed || text.length === 0) return `HTTP ${body.status}`;
|
|
880
917
|
return text;
|
|
881
918
|
}
|
|
919
|
+
function errorCode(body) {
|
|
920
|
+
const parsed = ErrorCode.safeParse(jsonValue(body));
|
|
921
|
+
return parsed.success ? parsed.data.error_code : null;
|
|
922
|
+
}
|
|
882
923
|
function failureFor(path, body) {
|
|
883
924
|
if (body.status === SERVICE_UNAVAILABLE_STATUS) {
|
|
884
925
|
const initializing = Initializing.safeParse(jsonValue(body));
|
|
@@ -888,7 +929,9 @@ function failureFor(path, body) {
|
|
|
888
929
|
status: body.status,
|
|
889
930
|
path,
|
|
890
931
|
message: serverMessage(body),
|
|
891
|
-
errors: serverErrors(body)
|
|
932
|
+
errors: serverErrors(body),
|
|
933
|
+
code: errorCode(body),
|
|
934
|
+
detail: body.json.parsed ? body.json.value : null
|
|
892
935
|
});
|
|
893
936
|
}
|
|
894
937
|
function shapeError(path, body, reason) {
|
|
@@ -896,7 +939,9 @@ function shapeError(path, body, reason) {
|
|
|
896
939
|
status: body.status,
|
|
897
940
|
path,
|
|
898
941
|
message: reason,
|
|
899
|
-
errors: null
|
|
942
|
+
errors: null,
|
|
943
|
+
code: null,
|
|
944
|
+
detail: null
|
|
900
945
|
});
|
|
901
946
|
}
|
|
902
947
|
function isTimeout(error) {
|
|
@@ -946,19 +991,33 @@ function createHttp(options) {
|
|
|
946
991
|
if (!received.ok) throw failureFor(path, received.body);
|
|
947
992
|
return received.body;
|
|
948
993
|
}
|
|
994
|
+
function parseJson(path, body, schema) {
|
|
995
|
+
if (!body.json.parsed) throw shapeError(path, body, "expected a JSON body");
|
|
996
|
+
return parseShape(path, body, schema, body.json.value);
|
|
997
|
+
}
|
|
998
|
+
function parseShape(path, body, schema, value) {
|
|
999
|
+
try {
|
|
1000
|
+
return schema.parse(value);
|
|
1001
|
+
} catch (error) {
|
|
1002
|
+
if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
|
|
1003
|
+
throw error;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
949
1006
|
return {
|
|
950
1007
|
async requestJson(method, path, request) {
|
|
1008
|
+
return parseJson(path, await requestBody(method, path, request.body), request.schema);
|
|
1009
|
+
},
|
|
1010
|
+
async requestOptionalJson(method, path, request) {
|
|
951
1011
|
const body = await requestBody(method, path, request.body);
|
|
952
|
-
if (
|
|
953
|
-
|
|
954
|
-
return request.schema.parse(body.json.value);
|
|
955
|
-
} catch (error) {
|
|
956
|
-
if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
|
|
957
|
-
throw error;
|
|
958
|
-
}
|
|
1012
|
+
if (body.status === NO_CONTENT_STATUS && body.text.length === 0) return null;
|
|
1013
|
+
return parseJson(path, body, request.schema);
|
|
959
1014
|
},
|
|
960
1015
|
async requestNoContent(method, path, request = {}) {
|
|
961
1016
|
await requestBody(method, path, request.body);
|
|
1017
|
+
},
|
|
1018
|
+
async requestText(method, path, request) {
|
|
1019
|
+
const body = await requestBody(method, path, request.body);
|
|
1020
|
+
return parseShape(path, body, request.schema, body.text);
|
|
962
1021
|
}
|
|
963
1022
|
};
|
|
964
1023
|
}
|
|
@@ -1049,6 +1108,48 @@ function apiKeyResource(http) {
|
|
|
1049
1108
|
regenerate
|
|
1050
1109
|
};
|
|
1051
1110
|
}
|
|
1111
|
+
const Collection = z.object({
|
|
1112
|
+
id: z.int(),
|
|
1113
|
+
name: z.string(),
|
|
1114
|
+
location: z.string(),
|
|
1115
|
+
type: z.string().nullable(),
|
|
1116
|
+
namespace: z.string().nullable(),
|
|
1117
|
+
personal_owner_id: z.int().nullable(),
|
|
1118
|
+
is_remote_synced: z.boolean()
|
|
1119
|
+
}).loose();
|
|
1120
|
+
const RootCollection = z.object({ id: z.literal("root") }).loose();
|
|
1121
|
+
const CollectionList = z.array(z.union([Collection, RootCollection]));
|
|
1122
|
+
const CollectionRef = z.object({
|
|
1123
|
+
id: z.int(),
|
|
1124
|
+
name: z.string()
|
|
1125
|
+
}).loose();
|
|
1126
|
+
//#endregion
|
|
1127
|
+
//#region src/metabase/resources/collection.ts
|
|
1128
|
+
function collectionResource(http) {
|
|
1129
|
+
/** GET /api/collection */
|
|
1130
|
+
async function list() {
|
|
1131
|
+
return http.requestJson("GET", "/api/collection", { schema: CollectionList });
|
|
1132
|
+
}
|
|
1133
|
+
/** POST /api/collection */
|
|
1134
|
+
async function create(request) {
|
|
1135
|
+
return http.requestJson("POST", "/api/collection", {
|
|
1136
|
+
body: {
|
|
1137
|
+
...request,
|
|
1138
|
+
parent_id: null
|
|
1139
|
+
},
|
|
1140
|
+
schema: CollectionRef
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
/** POST /api/ee/library; 400 when the Library exists */
|
|
1144
|
+
async function createLibrary() {
|
|
1145
|
+
return http.requestJson("POST", "/api/ee/library", { schema: CollectionRef });
|
|
1146
|
+
}
|
|
1147
|
+
return {
|
|
1148
|
+
list,
|
|
1149
|
+
create,
|
|
1150
|
+
createLibrary
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1052
1153
|
//#endregion
|
|
1053
1154
|
//#region src/metabase/domain/database.ts
|
|
1054
1155
|
const InitialSyncStatus = z.enum([
|
|
@@ -1141,7 +1242,9 @@ function permissionsGroupResource(http) {
|
|
|
1141
1242
|
}
|
|
1142
1243
|
return { list };
|
|
1143
1244
|
}
|
|
1144
|
-
|
|
1245
|
+
//#endregion
|
|
1246
|
+
//#region src/metabase/domain/remote-sync.ts
|
|
1247
|
+
const RemoteSyncType = z.enum(["read-only", "read-write"]);
|
|
1145
1248
|
const RemoteSyncSettingsResult = z.object({
|
|
1146
1249
|
success: z.boolean(),
|
|
1147
1250
|
task_id: z.int().optional()
|
|
@@ -1162,6 +1265,38 @@ const SyncTask = z.object({
|
|
|
1162
1265
|
status: SyncTaskStatus$1,
|
|
1163
1266
|
error_message: z.string().nullable().optional()
|
|
1164
1267
|
}).loose();
|
|
1268
|
+
const NamedCollection = z.object({
|
|
1269
|
+
id: z.int(),
|
|
1270
|
+
name: z.string()
|
|
1271
|
+
}).loose();
|
|
1272
|
+
const DependencyLocation = NamedCollection.nullable().optional();
|
|
1273
|
+
const DependencyItem = z.object({
|
|
1274
|
+
model: z.string(),
|
|
1275
|
+
id: z.int(),
|
|
1276
|
+
name: z.string().nullable()
|
|
1277
|
+
}).loose();
|
|
1278
|
+
const UnsyncedDependency = DependencyItem.extend({
|
|
1279
|
+
collection: DependencyLocation,
|
|
1280
|
+
used_by: z.array(DependencyItem)
|
|
1281
|
+
});
|
|
1282
|
+
const SyncRemedy = z.discriminatedUnion("type", [
|
|
1283
|
+
z.object({
|
|
1284
|
+
type: z.literal("collection"),
|
|
1285
|
+
collection: NamedCollection
|
|
1286
|
+
}).loose(),
|
|
1287
|
+
z.object({ type: z.literal("library") }).loose(),
|
|
1288
|
+
z.object({
|
|
1289
|
+
type: z.literal("none"),
|
|
1290
|
+
collection: DependencyLocation
|
|
1291
|
+
}).loose()
|
|
1292
|
+
]);
|
|
1293
|
+
const RequiredSync = z.object({
|
|
1294
|
+
remedy: SyncRemedy,
|
|
1295
|
+
syncable: z.boolean(),
|
|
1296
|
+
blocks: z.array(NamedCollection),
|
|
1297
|
+
dependencies: z.array(UnsyncedDependency)
|
|
1298
|
+
}).loose();
|
|
1299
|
+
const UnsyncedDependencies = z.object({ errors: z.object({ required: z.array(RequiredSync) }).loose() }).loose();
|
|
1165
1300
|
//#endregion
|
|
1166
1301
|
//#region src/metabase/resources/remote-sync.ts
|
|
1167
1302
|
function remoteSyncResource(http) {
|
|
@@ -1172,6 +1307,13 @@ function remoteSyncResource(http) {
|
|
|
1172
1307
|
schema: RemoteSyncSettingsResult
|
|
1173
1308
|
});
|
|
1174
1309
|
}
|
|
1310
|
+
/** PUT /api/ee/remote-sync/settings, without the git settings, which it leaves as they are */
|
|
1311
|
+
async function updateScope(scope) {
|
|
1312
|
+
return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
|
|
1313
|
+
body: scope,
|
|
1314
|
+
schema: RemoteSyncSettingsResult
|
|
1315
|
+
});
|
|
1316
|
+
}
|
|
1175
1317
|
/** PUT /api/ee/remote-sync/settings with only `remote-sync-transforms`, which skips the repository check. */
|
|
1176
1318
|
async function syncTransforms() {
|
|
1177
1319
|
return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
|
|
@@ -1188,10 +1330,11 @@ function remoteSyncResource(http) {
|
|
|
1188
1330
|
}
|
|
1189
1331
|
/** GET /api/ee/remote-sync/current-task */
|
|
1190
1332
|
async function currentTask() {
|
|
1191
|
-
return http.
|
|
1333
|
+
return http.requestOptionalJson("GET", "/api/ee/remote-sync/current-task", { schema: SyncTask });
|
|
1192
1334
|
}
|
|
1193
1335
|
return {
|
|
1194
1336
|
updateSettings,
|
|
1337
|
+
updateScope,
|
|
1195
1338
|
syncTransforms,
|
|
1196
1339
|
testConnection,
|
|
1197
1340
|
currentTask
|
|
@@ -1264,6 +1407,7 @@ const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
|
|
|
1264
1407
|
const ENGINES_SETTING = "engines";
|
|
1265
1408
|
const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
|
|
1266
1409
|
const TRANSFORMS_SETUP_COMPLETE_SETTING = "transforms-setup-complete";
|
|
1410
|
+
const REMOTE_SYNC_TYPE_SETTING = "remote-sync-type";
|
|
1267
1411
|
function settingResource(http) {
|
|
1268
1412
|
/** PUT /api/setting/premium-embedding-token */
|
|
1269
1413
|
async function setPremiumEmbeddingToken(value) {
|
|
@@ -1281,11 +1425,16 @@ function settingResource(http) {
|
|
|
1281
1425
|
async function engines() {
|
|
1282
1426
|
return http.requestJson("GET", `/api/setting/${ENGINES_SETTING}`, { schema: Engines });
|
|
1283
1427
|
}
|
|
1428
|
+
/** GET /api/setting/remote-sync-type; Metabase answers its default, read-only, while unset. */
|
|
1429
|
+
async function remoteSyncType() {
|
|
1430
|
+
return http.requestText("GET", `/api/setting/${REMOTE_SYNC_TYPE_SETTING}`, { schema: RemoteSyncType });
|
|
1431
|
+
}
|
|
1284
1432
|
return {
|
|
1285
1433
|
setPremiumEmbeddingToken,
|
|
1286
1434
|
enableTransforms,
|
|
1287
1435
|
transformsDecided,
|
|
1288
|
-
engines
|
|
1436
|
+
engines,
|
|
1437
|
+
remoteSyncType
|
|
1289
1438
|
};
|
|
1290
1439
|
}
|
|
1291
1440
|
//#endregion
|
|
@@ -1355,6 +1504,7 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
|
1355
1504
|
tokenStatus: tokenStatusResource(http),
|
|
1356
1505
|
database: databaseResource(http),
|
|
1357
1506
|
remoteSync: remoteSyncResource(http),
|
|
1507
|
+
collection: collectionResource(http),
|
|
1358
1508
|
user: userResource(http)
|
|
1359
1509
|
};
|
|
1360
1510
|
}
|
|
@@ -1479,6 +1629,7 @@ const credentials = defineRdeCommand({
|
|
|
1479
1629
|
default: false
|
|
1480
1630
|
}
|
|
1481
1631
|
},
|
|
1632
|
+
interrupted: INTERRUPTED,
|
|
1482
1633
|
async run(args) {
|
|
1483
1634
|
const name = await defaultInstanceName();
|
|
1484
1635
|
const state = await requireInstanceState(name);
|
|
@@ -2188,10 +2339,10 @@ function createArgs(options) {
|
|
|
2188
2339
|
"--label",
|
|
2189
2340
|
`${IMAGE_LABEL}=${options.image}`,
|
|
2190
2341
|
"-p",
|
|
2191
|
-
`${LOOPBACK_HOST}:${options.port.host
|
|
2342
|
+
`${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`,
|
|
2343
|
+
"-v",
|
|
2344
|
+
`${options.mount.source}:${options.mount.target}`
|
|
2192
2345
|
];
|
|
2193
|
-
if (options.port.alsoOn !== null) args.push("-p", `${options.port.alsoOn}:${options.port.host ?? ""}:${options.port.container}`);
|
|
2194
|
-
if (options.mount !== null) args.push("-v", `${options.mount.source}:${options.mount.target}`);
|
|
2195
2346
|
for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
|
|
2196
2347
|
if (options.hostUser !== null) {
|
|
2197
2348
|
args.push("-e", `${ENV_HOST_UID}=${options.hostUser.uid}`);
|
|
@@ -2250,17 +2401,6 @@ function dockerTool(runner) {
|
|
|
2250
2401
|
if (NO_SUCH_IMAGE.test(result.stderr)) return null;
|
|
2251
2402
|
throw toolFailure(DOCKER, args, result);
|
|
2252
2403
|
}
|
|
2253
|
-
async function bridgeGateway() {
|
|
2254
|
-
const gateway = (await docker([
|
|
2255
|
-
"network",
|
|
2256
|
-
"inspect",
|
|
2257
|
-
"bridge",
|
|
2258
|
-
"--format",
|
|
2259
|
-
"{{range .IPAM.Config}}{{.Gateway}}{{end}}"
|
|
2260
|
-
])).stdout.trim();
|
|
2261
|
-
if (gateway.length === 0) throw new ConfigError("`docker network inspect bridge` names no gateway address");
|
|
2262
|
-
return gateway;
|
|
2263
|
-
}
|
|
2264
2404
|
async function imageRemove(image) {
|
|
2265
2405
|
await docker(["rmi", image]);
|
|
2266
2406
|
}
|
|
@@ -2337,34 +2477,6 @@ function dockerTool(runner) {
|
|
|
2337
2477
|
name
|
|
2338
2478
|
]);
|
|
2339
2479
|
}
|
|
2340
|
-
async function containerLoopbackPort(name, containerPort) {
|
|
2341
|
-
const args = [
|
|
2342
|
-
"port",
|
|
2343
|
-
name,
|
|
2344
|
-
`${containerPort}/tcp`
|
|
2345
|
-
];
|
|
2346
|
-
const result = await docker(args);
|
|
2347
|
-
const prefix = `${LOOPBACK_HOST}:`;
|
|
2348
|
-
const line = result.stdout.split("\n").find((entry) => entry.startsWith(prefix));
|
|
2349
|
-
if (line !== void 0) {
|
|
2350
|
-
const port = Number.parseInt(line.slice(prefix.length), 10);
|
|
2351
|
-
if (Number.isInteger(port)) return port;
|
|
2352
|
-
}
|
|
2353
|
-
throw new ExternalToolError({
|
|
2354
|
-
tool: DOCKER,
|
|
2355
|
-
argv: args,
|
|
2356
|
-
envNames: [],
|
|
2357
|
-
exitCode: 0,
|
|
2358
|
-
stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
|
|
2359
|
-
});
|
|
2360
|
-
}
|
|
2361
|
-
async function containerExecSucceeds(name, command) {
|
|
2362
|
-
return (await runner.run(DOCKER, [
|
|
2363
|
-
"exec",
|
|
2364
|
-
name,
|
|
2365
|
-
...command
|
|
2366
|
-
])).exitCode === 0;
|
|
2367
|
-
}
|
|
2368
2480
|
async function containerLogs(name, options) {
|
|
2369
2481
|
const args = ["logs"];
|
|
2370
2482
|
if (options.follow) args.push("--follow");
|
|
@@ -2377,7 +2489,6 @@ function dockerTool(runner) {
|
|
|
2377
2489
|
imagePull,
|
|
2378
2490
|
imageId,
|
|
2379
2491
|
imageRemove,
|
|
2380
|
-
bridgeGateway,
|
|
2381
2492
|
containerInspect,
|
|
2382
2493
|
containerImageId,
|
|
2383
2494
|
portClaims,
|
|
@@ -2387,8 +2498,6 @@ function dockerTool(runner) {
|
|
|
2387
2498
|
containerRestart,
|
|
2388
2499
|
containerRemove,
|
|
2389
2500
|
containerForceRemove,
|
|
2390
|
-
containerLoopbackPort,
|
|
2391
|
-
containerExecSucceeds,
|
|
2392
2501
|
containerLogs
|
|
2393
2502
|
};
|
|
2394
2503
|
}
|
|
@@ -2566,8 +2675,7 @@ function dockerRuntime(state, deps) {
|
|
|
2566
2675
|
imageId,
|
|
2567
2676
|
port: {
|
|
2568
2677
|
host: state.port,
|
|
2569
|
-
container: CONTAINER_PORT
|
|
2570
|
-
alsoOn: null
|
|
2678
|
+
container: CONTAINER_PORT
|
|
2571
2679
|
},
|
|
2572
2680
|
mount: {
|
|
2573
2681
|
source: deps.dataDir,
|
|
@@ -2881,148 +2989,21 @@ function runtimeFromState(state, deps) {
|
|
|
2881
2989
|
}
|
|
2882
2990
|
}
|
|
2883
2991
|
//#endregion
|
|
2884
|
-
//#region src/runtime/sample-database.ts
|
|
2885
|
-
const SAMPLE_DATABASE_IMAGE = "metabase/qa-databases:postgres-sample-15";
|
|
2886
|
-
const SAMPLE_DATABASE_USER = "metabase";
|
|
2887
|
-
const SAMPLE_DATABASE_PASSWORD = "metasample123";
|
|
2888
|
-
const SAMPLE_DATABASE_NAME = "sample";
|
|
2889
|
-
const SAMPLE_DATABASE_PORT = 25432;
|
|
2890
|
-
const SAMPLE_DATABASE_READY_TIMEOUT_MS = 12e4;
|
|
2891
|
-
const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
|
|
2892
|
-
const POSTGRES_PORT = 5432;
|
|
2992
|
+
//#region src/runtime/leftover-sample-database.ts
|
|
2893
2993
|
const CONTAINER_NAME_PREFIX = "rde-sampledb";
|
|
2894
|
-
const
|
|
2895
|
-
|
|
2896
|
-
"pg_isready",
|
|
2897
|
-
"-h",
|
|
2898
|
-
LOOPBACK_HOST,
|
|
2899
|
-
"-U",
|
|
2900
|
-
SAMPLE_DATABASE_USER,
|
|
2901
|
-
"-d",
|
|
2902
|
-
SAMPLE_DATABASE_NAME
|
|
2903
|
-
];
|
|
2904
|
-
function sampleContainerName(instanceName, home) {
|
|
2994
|
+
const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
|
|
2995
|
+
function sampleDatabaseContainerName(instanceName, home) {
|
|
2905
2996
|
const tag = homeTag(home);
|
|
2906
2997
|
const name = `${CONTAINER_NAME_PREFIX}-${instanceName}`;
|
|
2907
2998
|
return tag === null ? name : `${name}-${tag}`;
|
|
2908
2999
|
}
|
|
2909
|
-
function
|
|
2910
|
-
return
|
|
2911
|
-
|
|
2912
|
-
|
|
2913
|
-
|
|
2914
|
-
}
|
|
2915
|
-
function createOptions(state, home, hostPort, alsoOn) {
|
|
2916
|
-
return {
|
|
2917
|
-
name: state.containerName,
|
|
2918
|
-
home,
|
|
2919
|
-
image: state.image,
|
|
2920
|
-
imageId: null,
|
|
2921
|
-
port: {
|
|
2922
|
-
host: hostPort,
|
|
2923
|
-
container: POSTGRES_PORT,
|
|
2924
|
-
alsoOn
|
|
2925
|
-
},
|
|
2926
|
-
mount: null,
|
|
2927
|
-
env: {},
|
|
2928
|
-
addHostGateway: false,
|
|
2929
|
-
hostUser: null
|
|
2930
|
-
};
|
|
2931
|
-
}
|
|
2932
|
-
function requireOwnContainer(name, existing, home) {
|
|
2933
|
-
if (existing.home !== null && existing.home !== home) throw new ConfigError(`container ${name} belongs to the rde home ${existing.home} and is left alone`);
|
|
2934
|
-
}
|
|
2935
|
-
function sampleDatabaseContainer(state, docker, home) {
|
|
2936
|
-
const name = state.containerName;
|
|
2937
|
-
function conflict(reason) {
|
|
2938
|
-
return new ConfigError(`container ${name} already exists ${reason}; remove it with \`docker rm -f ${name}\` and run \`rde init\` again`);
|
|
2939
|
-
}
|
|
2940
|
-
async function install(options) {
|
|
2941
|
-
const existing = await docker.containerInspect(name);
|
|
2942
|
-
if (existing.exists) {
|
|
2943
|
-
if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
|
|
2944
|
-
if (publishesOnLoopback$1(existing, state.port)) return;
|
|
2945
|
-
requireOwnContainer(name, existing, home);
|
|
2946
|
-
await docker.containerForceRemove(name);
|
|
2947
|
-
}
|
|
2948
|
-
await docker.imagePull(state.image, { onLine: options.onProgress });
|
|
2949
|
-
await docker.containerCreate(createOptions(state, home, state.port, options.alsoOn));
|
|
2950
|
-
}
|
|
2951
|
-
async function start() {
|
|
2952
|
-
const existing = await docker.containerInspect(name);
|
|
2953
|
-
if (!existing.exists) return "missing";
|
|
2954
|
-
if (existing.running) return "running";
|
|
2955
|
-
await docker.containerStart(name);
|
|
2956
|
-
return "started";
|
|
2957
|
-
}
|
|
2958
|
-
async function stop() {
|
|
2959
|
-
const existing = await docker.containerInspect(name);
|
|
2960
|
-
if (existing.exists && existing.running) await docker.containerStop(name, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
|
|
2961
|
-
}
|
|
2962
|
-
async function status() {
|
|
2963
|
-
const existing = await docker.containerInspect(name);
|
|
2964
|
-
return {
|
|
2965
|
-
installed: existing.exists,
|
|
2966
|
-
running: existing.exists && existing.running
|
|
2967
|
-
};
|
|
2968
|
-
}
|
|
2969
|
-
async function waitReady(options) {
|
|
2970
|
-
const deadline = Date.now() + options.timeoutMs;
|
|
2971
|
-
const interval = options.pollMs ?? READY_POLL_MS;
|
|
2972
|
-
for (;;) {
|
|
2973
|
-
if (await docker.containerExecSucceeds(name, READY_PROBE)) return;
|
|
2974
|
-
const existing = await docker.containerInspect(name);
|
|
2975
|
-
if (!existing.exists || !existing.running) throw new ConfigError(`container ${name} stopped before Postgres was ready; \`docker logs ${name}\` shows why`);
|
|
2976
|
-
if (Date.now() >= deadline) throw new ConfigError(`Postgres in ${name} is not ready after ${Math.round(options.timeoutMs / 1e3)}s; \`docker logs ${name}\` shows what it is doing`);
|
|
2977
|
-
try {
|
|
2978
|
-
await setTimeout$1(interval, void 0, { signal: options.signal });
|
|
2979
|
-
} catch {
|
|
2980
|
-
throw new AbortError();
|
|
2981
|
-
}
|
|
2982
|
-
}
|
|
2983
|
-
}
|
|
2984
|
-
return {
|
|
2985
|
-
install,
|
|
2986
|
-
start,
|
|
2987
|
-
stop,
|
|
2988
|
-
status,
|
|
2989
|
-
waitReady
|
|
2990
|
-
};
|
|
2991
|
-
}
|
|
2992
|
-
function extraAddress(inspection) {
|
|
2993
|
-
const extra = inspection.ports.find((mapping) => mapping.container === POSTGRES_PORT && mapping.hostIp !== "127.0.0.1");
|
|
2994
|
-
return extra === void 0 ? null : extra.hostIp;
|
|
2995
|
-
}
|
|
2996
|
-
async function startOnDockerPort(state, docker, home, alsoOn) {
|
|
2997
|
-
const name = state.containerName;
|
|
2998
|
-
await docker.containerForceRemove(name);
|
|
2999
|
-
await docker.containerCreate(createOptions(state, home, null, null));
|
|
3000
|
-
await docker.containerStart(name);
|
|
3001
|
-
const port = await docker.containerLoopbackPort(name, POSTGRES_PORT);
|
|
3000
|
+
async function removeLeftoverSampleDatabase(docker, instanceName, home) {
|
|
3001
|
+
if (!(await docker.available()).available) return null;
|
|
3002
|
+
const name = sampleDatabaseContainerName(instanceName, home);
|
|
3003
|
+
const existing = await docker.containerInspect(name);
|
|
3004
|
+
if (!existing.exists || existing.home !== null && existing.home !== home) return null;
|
|
3002
3005
|
await docker.containerForceRemove(name);
|
|
3003
|
-
|
|
3004
|
-
...state,
|
|
3005
|
-
port
|
|
3006
|
-
};
|
|
3007
|
-
await docker.containerCreate(createOptions(pinned, home, port, alsoOn));
|
|
3008
|
-
await docker.containerStart(name);
|
|
3009
|
-
return pinned;
|
|
3010
|
-
}
|
|
3011
|
-
async function startOnFreePort(state, docker, home) {
|
|
3012
|
-
try {
|
|
3013
|
-
return {
|
|
3014
|
-
outcome: await sampleDatabaseContainer(state, docker, home).start(),
|
|
3015
|
-
state
|
|
3016
|
-
};
|
|
3017
|
-
} catch (error) {
|
|
3018
|
-
if (!isPortTakenError(error)) throw error;
|
|
3019
|
-
}
|
|
3020
|
-
const existing = await docker.containerInspect(state.containerName);
|
|
3021
|
-
if (existing.exists) requireOwnContainer(state.containerName, existing, home);
|
|
3022
|
-
return {
|
|
3023
|
-
outcome: "started",
|
|
3024
|
-
state: await startOnDockerPort(state, docker, home, existing.exists ? extraAddress(existing) : null)
|
|
3025
|
-
};
|
|
3006
|
+
return name;
|
|
3026
3007
|
}
|
|
3027
3008
|
//#endregion
|
|
3028
3009
|
//#region src/steps/lifecycle.ts
|
|
@@ -3033,10 +3014,14 @@ async function loadInstance(name, signal) {
|
|
|
3033
3014
|
return {
|
|
3034
3015
|
state,
|
|
3035
3016
|
url: configured.url,
|
|
3036
|
-
runtime: runtimeFromState(configured.runtime, deps)
|
|
3037
|
-
sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker, deps.rdeHome)
|
|
3017
|
+
runtime: runtimeFromState(configured.runtime, deps)
|
|
3038
3018
|
};
|
|
3039
3019
|
}
|
|
3020
|
+
async function removeLeftovers(name, signal) {
|
|
3021
|
+
const deps = hostRuntimeDeps(name, signal);
|
|
3022
|
+
const removed = await removeLeftoverSampleDatabase(deps.docker, name, deps.rdeHome);
|
|
3023
|
+
return removed === null ? null : `Removed container ${removed}, the sample database of an older rde`;
|
|
3024
|
+
}
|
|
3040
3025
|
async function isInstanceRunning(state, runtime) {
|
|
3041
3026
|
const configured = configuredRuntime(state);
|
|
3042
3027
|
if (configured === null) return false;
|
|
@@ -3045,44 +3030,29 @@ async function isInstanceRunning(state, runtime) {
|
|
|
3045
3030
|
function openInstance(url) {
|
|
3046
3031
|
return openBrowser(url);
|
|
3047
3032
|
}
|
|
3048
|
-
function ownContainers(state) {
|
|
3049
|
-
const names = /* @__PURE__ */ new Set();
|
|
3050
|
-
if (state.runtime?.kind === "docker") names.add(state.runtime.containerName);
|
|
3051
|
-
if (state.sampleDatabase !== null) names.add(state.sampleDatabase.containerName);
|
|
3052
|
-
return names;
|
|
3053
|
-
}
|
|
3054
|
-
function instanceClaims(other) {
|
|
3055
|
-
const claims = [];
|
|
3056
|
-
if (other.runtime !== null) claims.push({
|
|
3057
|
-
port: other.runtime.port,
|
|
3058
|
-
holder: `rde instance ${other.name}`
|
|
3059
|
-
});
|
|
3060
|
-
if (other.sampleDatabase !== null) claims.push({
|
|
3061
|
-
port: other.sampleDatabase.port,
|
|
3062
|
-
holder: `the sample database of rde instance ${other.name}`
|
|
3063
|
-
});
|
|
3064
|
-
return claims;
|
|
3065
|
-
}
|
|
3066
3033
|
async function portClaims(state, runtime) {
|
|
3067
3034
|
const claims = [];
|
|
3068
3035
|
if ((await runtime.docker.available()).available) {
|
|
3069
|
-
const own =
|
|
3070
|
-
for (const claim of await runtime.docker.portClaims()) if (
|
|
3036
|
+
const own = state.runtime?.kind === "docker" ? state.runtime.containerName : null;
|
|
3037
|
+
for (const claim of await runtime.docker.portClaims()) if (claim.container !== own) claims.push({
|
|
3071
3038
|
port: claim.port,
|
|
3072
3039
|
holder: `container ${claim.container}`
|
|
3073
3040
|
});
|
|
3074
3041
|
}
|
|
3075
|
-
for (const other of await runtime.instances()) if (other.name !== state.name) claims.push(
|
|
3042
|
+
for (const other of await runtime.instances()) if (other.name !== state.name && other.runtime !== null) claims.push({
|
|
3043
|
+
port: other.runtime.port,
|
|
3044
|
+
holder: `rde instance ${other.name}`
|
|
3045
|
+
});
|
|
3076
3046
|
return claims;
|
|
3077
3047
|
}
|
|
3078
3048
|
//#endregion
|
|
3079
3049
|
//#region src/steps/admin-user.ts
|
|
3080
|
-
const STEP_ID$
|
|
3050
|
+
const STEP_ID$9 = "admin-user";
|
|
3081
3051
|
const ADMIN_USER_KEYS = {
|
|
3082
|
-
email: `${STEP_ID$
|
|
3083
|
-
password: `${STEP_ID$
|
|
3084
|
-
firstName: `${STEP_ID$
|
|
3085
|
-
lastName: `${STEP_ID$
|
|
3052
|
+
email: `${STEP_ID$9}.email`,
|
|
3053
|
+
password: `${STEP_ID$9}.password`,
|
|
3054
|
+
firstName: `${STEP_ID$9}.first-name`,
|
|
3055
|
+
lastName: `${STEP_ID$9}.last-name`
|
|
3086
3056
|
};
|
|
3087
3057
|
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
3088
3058
|
const GENERATED_PASSWORD_BYTES = 24;
|
|
@@ -3237,7 +3207,7 @@ async function recordExistingAdmin(ctx, variant, client, url) {
|
|
|
3237
3207
|
}
|
|
3238
3208
|
function adminUserStep(options, deps = hostDeps$14) {
|
|
3239
3209
|
return {
|
|
3240
|
-
id: STEP_ID$
|
|
3210
|
+
id: STEP_ID$9,
|
|
3241
3211
|
title: "Create the admin user",
|
|
3242
3212
|
redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
|
|
3243
3213
|
async detect(ctx) {
|
|
@@ -3502,10 +3472,15 @@ const GitSyncTask = z.object({
|
|
|
3502
3472
|
progress: z.number().nullable(),
|
|
3503
3473
|
error_message: z.string().nullable().optional()
|
|
3504
3474
|
}).loose();
|
|
3475
|
+
const SyncedCollection = z.object({
|
|
3476
|
+
id: z.int(),
|
|
3477
|
+
name: z.string()
|
|
3478
|
+
}).loose();
|
|
3505
3479
|
const GitSyncStatus = z.object({
|
|
3506
3480
|
branch: z.string().nullable(),
|
|
3507
3481
|
is_dirty: z.boolean(),
|
|
3508
|
-
current_task: GitSyncTask.nullable()
|
|
3482
|
+
current_task: GitSyncTask.nullable(),
|
|
3483
|
+
synced_collections: z.array(SyncedCollection)
|
|
3509
3484
|
}).loose();
|
|
3510
3485
|
const GitSyncOutcome = z.object({
|
|
3511
3486
|
message: z.string().nullable(),
|
|
@@ -3517,6 +3492,9 @@ const RemoteChanges = z.object({
|
|
|
3517
3492
|
remote_version: z.string().nullable(),
|
|
3518
3493
|
local_version: z.string().nullable()
|
|
3519
3494
|
}).loose();
|
|
3495
|
+
function remoteMoved(changes) {
|
|
3496
|
+
return changes.has_changes && changes.local_version !== null;
|
|
3497
|
+
}
|
|
3520
3498
|
function comparableUrl(url) {
|
|
3521
3499
|
const parsed = new URL(url);
|
|
3522
3500
|
return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
@@ -3652,11 +3630,11 @@ function mbTool(runner) {
|
|
|
3652
3630
|
}
|
|
3653
3631
|
//#endregion
|
|
3654
3632
|
//#region src/steps/api-key.ts
|
|
3655
|
-
const STEP_ID$
|
|
3633
|
+
const STEP_ID$8 = "api-key";
|
|
3656
3634
|
const API_KEY_KEYS = {
|
|
3657
|
-
email: `${STEP_ID$
|
|
3658
|
-
password: `${STEP_ID$
|
|
3659
|
-
profile: `${STEP_ID$
|
|
3635
|
+
email: `${STEP_ID$8}.email`,
|
|
3636
|
+
password: `${STEP_ID$8}.password`,
|
|
3637
|
+
profile: `${STEP_ID$8}.profile`
|
|
3660
3638
|
};
|
|
3661
3639
|
const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
3662
3640
|
function hostDeps$13(ctx) {
|
|
@@ -3720,7 +3698,7 @@ async function authenticates(client) {
|
|
|
3720
3698
|
}
|
|
3721
3699
|
}
|
|
3722
3700
|
async function forgetCompletion(ctx) {
|
|
3723
|
-
const { [STEP_ID$
|
|
3701
|
+
const { [STEP_ID$8]: record, ...steps } = ctx.state.steps;
|
|
3724
3702
|
if (record !== void 0) await ctx.saveState({
|
|
3725
3703
|
...ctx.state,
|
|
3726
3704
|
steps
|
|
@@ -3799,13 +3777,13 @@ function usageNote(profile) {
|
|
|
3799
3777
|
}
|
|
3800
3778
|
function apiKeyStep(deps = hostDeps$13) {
|
|
3801
3779
|
return {
|
|
3802
|
-
id: STEP_ID$
|
|
3780
|
+
id: STEP_ID$8,
|
|
3803
3781
|
title: "Connect the Metabase CLI",
|
|
3804
3782
|
redoHint: "`rde status` shows the mb profile it uses.",
|
|
3805
3783
|
async detect(ctx) {
|
|
3806
3784
|
const configured = configuredRuntime(ctx.state);
|
|
3807
3785
|
const apiKey = ctx.secrets.apiKey;
|
|
3808
|
-
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$
|
|
3786
|
+
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
|
|
3809
3787
|
const resolved = deps(ctx);
|
|
3810
3788
|
if (!await resolved.instanceRunning()) return "todo";
|
|
3811
3789
|
if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
|
|
@@ -4827,7 +4805,7 @@ const RSA_KEY_UNAVAILABLE_ERROR = "RSA public key is not available";
|
|
|
4827
4805
|
const KEY_RETRIEVAL_NOTE = "allowPublicKeyRetrieval=true is added: MySQL 8 needs it to log in to a local server without SSL; to leave it out, paste a connection string with allowPublicKeyRetrieval=false instead";
|
|
4828
4806
|
const KEY_RETRIEVAL_HINT = "MySQL 8 without SSL needs allowPublicKeyRetrieval=true: go back and add ?allowPublicKeyRetrieval=true to the connection string, or turn on SSL";
|
|
4829
4807
|
const CONNECTION_PLACEHOLDER = "leave empty to skip";
|
|
4830
|
-
const
|
|
4808
|
+
const ENGINE_PLACEHOLDER = "leave empty to pick the engine";
|
|
4831
4809
|
const START_SCREEN = {
|
|
4832
4810
|
engine: "pick-engine",
|
|
4833
4811
|
"connection-string": "connection-string"
|
|
@@ -4853,6 +4831,9 @@ function targeting(stub) {
|
|
|
4853
4831
|
function restart(model) {
|
|
4854
4832
|
return model.target === null ? FRESH$1 : targeting(model.target);
|
|
4855
4833
|
}
|
|
4834
|
+
function mayFinish(run) {
|
|
4835
|
+
return !run.deps.required || run.deps.existingCount + run.created.length > 0;
|
|
4836
|
+
}
|
|
4856
4837
|
function nextStub(run, model) {
|
|
4857
4838
|
const stub = run.queue.shift();
|
|
4858
4839
|
if (stub !== void 0) return {
|
|
@@ -4860,13 +4841,18 @@ function nextStub(run, model) {
|
|
|
4860
4841
|
to: "connection-string",
|
|
4861
4842
|
model: targeting(stub)
|
|
4862
4843
|
};
|
|
4863
|
-
|
|
4844
|
+
if (run.created.length > 0) return {
|
|
4864
4845
|
type: "next",
|
|
4865
4846
|
to: "another",
|
|
4866
4847
|
model
|
|
4867
|
-
}
|
|
4848
|
+
};
|
|
4849
|
+
return mayFinish(run) ? {
|
|
4868
4850
|
type: "done",
|
|
4869
4851
|
model
|
|
4852
|
+
} : {
|
|
4853
|
+
type: "next",
|
|
4854
|
+
to: "connection-string",
|
|
4855
|
+
model: FRESH$1
|
|
4870
4856
|
};
|
|
4871
4857
|
}
|
|
4872
4858
|
function stubNamed(run, name) {
|
|
@@ -4956,9 +4942,13 @@ function pickStubsScreen(run) {
|
|
|
4956
4942
|
run.queue = run.stubs.filter((stub) => picked.includes(stub.id));
|
|
4957
4943
|
return nextStub(run, model);
|
|
4958
4944
|
}
|
|
4959
|
-
if (run.deps.start === "connection-string" && run.created.length === 0) return {
|
|
4945
|
+
if (run.deps.start === "connection-string" && run.created.length === 0) return mayFinish(run) ? {
|
|
4960
4946
|
type: "done",
|
|
4961
4947
|
model
|
|
4948
|
+
} : {
|
|
4949
|
+
type: "next",
|
|
4950
|
+
to: "connection-string",
|
|
4951
|
+
model: FRESH$1
|
|
4962
4952
|
};
|
|
4963
4953
|
return {
|
|
4964
4954
|
type: "next",
|
|
@@ -4968,6 +4958,11 @@ function pickStubsScreen(run) {
|
|
|
4968
4958
|
};
|
|
4969
4959
|
}
|
|
4970
4960
|
function connectionQuestion(run, target) {
|
|
4961
|
+
if (target === null && !mayFinish(run)) return {
|
|
4962
|
+
key: DATABASE_KEYS.connection,
|
|
4963
|
+
message: "Connection string of your database (Enter to pick its engine instead)",
|
|
4964
|
+
placeholder: ENGINE_PLACEHOLDER
|
|
4965
|
+
};
|
|
4971
4966
|
if (target === null) return {
|
|
4972
4967
|
key: DATABASE_KEYS.connection,
|
|
4973
4968
|
message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
|
|
@@ -4976,7 +4971,7 @@ function connectionQuestion(run, target) {
|
|
|
4976
4971
|
return {
|
|
4977
4972
|
key: databaseKey(ordinalOf(run), "connection"),
|
|
4978
4973
|
message: `Connection string of ${target.name} (Enter to pick its engine instead)`,
|
|
4979
|
-
placeholder:
|
|
4974
|
+
placeholder: ENGINE_PLACEHOLDER
|
|
4980
4975
|
};
|
|
4981
4976
|
}
|
|
4982
4977
|
function connectionStringScreen(run) {
|
|
@@ -4988,7 +4983,7 @@ function connectionStringScreen(run) {
|
|
|
4988
4983
|
...draft !== null && { initialValue: draft }
|
|
4989
4984
|
})).trim();
|
|
4990
4985
|
draft = uri;
|
|
4991
|
-
if (uri.length === 0) return model.target === null ? {
|
|
4986
|
+
if (uri.length === 0) return model.target === null && mayFinish(run) ? {
|
|
4992
4987
|
type: "done",
|
|
4993
4988
|
model
|
|
4994
4989
|
} : {
|
|
@@ -5027,22 +5022,28 @@ function connectionStringScreen(run) {
|
|
|
5027
5022
|
}
|
|
5028
5023
|
};
|
|
5029
5024
|
}
|
|
5030
|
-
function
|
|
5031
|
-
if (model.target !== null) return
|
|
5032
|
-
|
|
5025
|
+
function finishChoices(run, model) {
|
|
5026
|
+
if (model.target !== null) return [{
|
|
5027
|
+
value: FINISH,
|
|
5028
|
+
label: `Leave ${model.target.name} unconnected`
|
|
5029
|
+
}];
|
|
5030
|
+
if (run.deps.existingCount + run.created.length > 0) return [{
|
|
5031
|
+
value: FINISH,
|
|
5032
|
+
label: "Finish"
|
|
5033
|
+
}];
|
|
5034
|
+
return mayFinish(run) ? [{
|
|
5035
|
+
value: FINISH,
|
|
5036
|
+
label: "Skip for now"
|
|
5037
|
+
}] : [];
|
|
5033
5038
|
}
|
|
5034
5039
|
function pickEngineScreen(run) {
|
|
5035
5040
|
return async (model) => {
|
|
5036
5041
|
const total = run.deps.existingCount + run.created.length;
|
|
5037
|
-
const finishChoice = {
|
|
5038
|
-
value: FINISH,
|
|
5039
|
-
label: finishLabel(run, model)
|
|
5040
|
-
};
|
|
5041
5042
|
for (;;) {
|
|
5042
5043
|
const picked = await run.deps.prompts.select({
|
|
5043
5044
|
key: databaseKey(ordinalOf(run), "engine"),
|
|
5044
5045
|
message: "Which database engine?",
|
|
5045
|
-
choices: [...run.deps.catalog.list().map(engineChoice),
|
|
5046
|
+
choices: [...run.deps.catalog.list().map(engineChoice), ...finishChoices(run, model)],
|
|
5046
5047
|
...model.engine !== null && { initialValue: model.engine.key }
|
|
5047
5048
|
});
|
|
5048
5049
|
if (picked === "finish" && model.target !== null) return nextStub(run, model);
|
|
@@ -5518,6 +5519,13 @@ const METABASE_CONTENT_DIRS = [
|
|
|
5518
5519
|
];
|
|
5519
5520
|
const TOKEN_USERNAMES = { "bitbucket.org": "x-token-auth" };
|
|
5520
5521
|
const DEFAULT_TOKEN_USERNAME = "x-access-token";
|
|
5522
|
+
const MISSING_REF_EXIT_CODE = 1;
|
|
5523
|
+
const FIELD_SEPARATOR = "";
|
|
5524
|
+
const COMMIT_FORMAT = [
|
|
5525
|
+
"%H",
|
|
5526
|
+
"%h",
|
|
5527
|
+
"%s"
|
|
5528
|
+
].join("%x1f");
|
|
5521
5529
|
function credentialEnv(credentials) {
|
|
5522
5530
|
const { origin, hostname } = new URL(credentials.url);
|
|
5523
5531
|
const username = TOKEN_USERNAMES[hostname] ?? DEFAULT_TOKEN_USERNAME;
|
|
@@ -5531,6 +5539,18 @@ function credentialEnv(credentials) {
|
|
|
5531
5539
|
GIT_TERMINAL_PROMPT: "0"
|
|
5532
5540
|
};
|
|
5533
5541
|
}
|
|
5542
|
+
function parseCommitLine(line) {
|
|
5543
|
+
const [sha, shortSha, subject] = line.split(FIELD_SEPARATOR);
|
|
5544
|
+
if (sha === void 0 || shortSha === void 0 || subject === void 0) throw new Error(`unexpected \`git log\` line: ${line}`);
|
|
5545
|
+
return {
|
|
5546
|
+
sha,
|
|
5547
|
+
shortSha,
|
|
5548
|
+
subject
|
|
5549
|
+
};
|
|
5550
|
+
}
|
|
5551
|
+
function outputLines(stdout) {
|
|
5552
|
+
return stdout.split("\n").filter((line) => line.length > 0);
|
|
5553
|
+
}
|
|
5534
5554
|
async function directoryExists(path) {
|
|
5535
5555
|
try {
|
|
5536
5556
|
return (await promises.stat(path)).isDirectory();
|
|
@@ -5586,8 +5606,12 @@ function gitTool(runner) {
|
|
|
5586
5606
|
], { cwd: dir });
|
|
5587
5607
|
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
5588
5608
|
}
|
|
5589
|
-
async function
|
|
5590
|
-
await git([
|
|
5609
|
+
async function commitPaths(dir, paths, message) {
|
|
5610
|
+
await git([
|
|
5611
|
+
"add",
|
|
5612
|
+
"--",
|
|
5613
|
+
...paths
|
|
5614
|
+
], { cwd: dir });
|
|
5591
5615
|
await git([
|
|
5592
5616
|
...await hasIdentity(dir) ? [] : [
|
|
5593
5617
|
"-c",
|
|
@@ -5597,7 +5621,9 @@ function gitTool(runner) {
|
|
|
5597
5621
|
],
|
|
5598
5622
|
"commit",
|
|
5599
5623
|
"--message",
|
|
5600
|
-
message
|
|
5624
|
+
message,
|
|
5625
|
+
"--",
|
|
5626
|
+
...paths
|
|
5601
5627
|
], { cwd: dir });
|
|
5602
5628
|
}
|
|
5603
5629
|
async function addRemote(dir, name, url) {
|
|
@@ -5643,6 +5669,42 @@ function gitTool(runner) {
|
|
|
5643
5669
|
if (result.exitCode !== 0) return null;
|
|
5644
5670
|
return result.stdout.trim();
|
|
5645
5671
|
}
|
|
5672
|
+
async function isBareRepo(path) {
|
|
5673
|
+
const args = [
|
|
5674
|
+
`--git-dir=${path}`,
|
|
5675
|
+
"rev-parse",
|
|
5676
|
+
"--is-bare-repository"
|
|
5677
|
+
];
|
|
5678
|
+
const result = await runner.run(GIT, args);
|
|
5679
|
+
return result.exitCode === 0 && result.stdout.trim() === "true";
|
|
5680
|
+
}
|
|
5681
|
+
async function bareBranchCommits(path, branch) {
|
|
5682
|
+
const gitDir = `--git-dir=${path}`;
|
|
5683
|
+
const ref = `refs/heads/${branch}`;
|
|
5684
|
+
const verifyArgs = [
|
|
5685
|
+
gitDir,
|
|
5686
|
+
"rev-parse",
|
|
5687
|
+
"--verify",
|
|
5688
|
+
"--quiet",
|
|
5689
|
+
`${ref}^{commit}`
|
|
5690
|
+
];
|
|
5691
|
+
const verified = await runner.run(GIT, verifyArgs);
|
|
5692
|
+
if (verified.exitCode === MISSING_REF_EXIT_CODE) return null;
|
|
5693
|
+
if (verified.exitCode !== 0) throw toolFailure(GIT, verifyArgs, verified);
|
|
5694
|
+
return outputLines((await git([
|
|
5695
|
+
gitDir,
|
|
5696
|
+
"-c",
|
|
5697
|
+
"log.showSignature=false",
|
|
5698
|
+
"log",
|
|
5699
|
+
`--format=${COMMIT_FORMAT}`,
|
|
5700
|
+
ref,
|
|
5701
|
+
"--"
|
|
5702
|
+
])).stdout).map(parseCommitLine);
|
|
5703
|
+
}
|
|
5704
|
+
async function reachableCommits(dir) {
|
|
5705
|
+
const result = await git(["rev-list", "--all"], { cwd: dir });
|
|
5706
|
+
return new Set(outputLines(result.stdout));
|
|
5707
|
+
}
|
|
5646
5708
|
async function hasMetabaseContent(dir) {
|
|
5647
5709
|
for (const name of METABASE_CONTENT_DIRS) if (await directoryExists(join(dir, name))) return true;
|
|
5648
5710
|
return false;
|
|
@@ -5653,24 +5715,27 @@ function gitTool(runner) {
|
|
|
5653
5715
|
initBare,
|
|
5654
5716
|
init,
|
|
5655
5717
|
hasCommits,
|
|
5656
|
-
|
|
5718
|
+
commitPaths,
|
|
5657
5719
|
addRemote,
|
|
5658
5720
|
clone,
|
|
5659
5721
|
push,
|
|
5660
5722
|
currentBranch,
|
|
5661
5723
|
remoteUrl,
|
|
5724
|
+
isBareRepo,
|
|
5725
|
+
bareBranchCommits,
|
|
5726
|
+
reachableCommits,
|
|
5662
5727
|
hasMetabaseContent
|
|
5663
5728
|
};
|
|
5664
5729
|
}
|
|
5665
5730
|
//#endregion
|
|
5666
5731
|
//#region src/steps/git-sync.ts
|
|
5667
|
-
const STEP_ID$
|
|
5732
|
+
const STEP_ID$7 = "git-sync";
|
|
5668
5733
|
const GIT_SYNC_KEYS = {
|
|
5669
|
-
location: `${STEP_ID$
|
|
5670
|
-
path: `${STEP_ID$
|
|
5671
|
-
remote: `${STEP_ID$
|
|
5672
|
-
url: `${STEP_ID$
|
|
5673
|
-
token: `${STEP_ID$
|
|
5734
|
+
location: `${STEP_ID$7}.location`,
|
|
5735
|
+
path: `${STEP_ID$7}.path`,
|
|
5736
|
+
remote: `${STEP_ID$7}.remote`,
|
|
5737
|
+
url: `${STEP_ID$7}.url`,
|
|
5738
|
+
token: `${STEP_ID$7}.token`
|
|
5674
5739
|
};
|
|
5675
5740
|
const BARE_REPO_NAME = "content.git";
|
|
5676
5741
|
const DEFAULT_BRANCH = "main";
|
|
@@ -5956,7 +6021,7 @@ async function prepareWorkdir(run, workdir) {
|
|
|
5956
6021
|
if (!await run.git.hasCommits(workdir)) {
|
|
5957
6022
|
await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
|
|
5958
6023
|
await ignoreDataAppFiles(workdir);
|
|
5959
|
-
await run.git.
|
|
6024
|
+
await run.git.commitPaths(workdir, [README_NAME, GITIGNORE_NAME], INITIAL_COMMIT_MESSAGE);
|
|
5960
6025
|
}
|
|
5961
6026
|
return {
|
|
5962
6027
|
workdir,
|
|
@@ -6120,7 +6185,7 @@ function shellWord(value) {
|
|
|
6120
6185
|
function exportCommand(profile, branch) {
|
|
6121
6186
|
return `${mbCommand(profile)} git-sync export -b ${shellWord(branch)}`;
|
|
6122
6187
|
}
|
|
6123
|
-
function usageLines
|
|
6188
|
+
function usageLines(state, gitRemote) {
|
|
6124
6189
|
const profile = state.mbProfile;
|
|
6125
6190
|
return [
|
|
6126
6191
|
`cd ${gitRemote.workdir}`,
|
|
@@ -6171,18 +6236,18 @@ async function runGitSync(ctx, deps) {
|
|
|
6171
6236
|
...ctx.state,
|
|
6172
6237
|
gitRemote: remote.gitRemote
|
|
6173
6238
|
});
|
|
6174
|
-
ctx.ui.note(usageLines
|
|
6239
|
+
ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
6175
6240
|
return "completed";
|
|
6176
6241
|
}
|
|
6177
6242
|
function gitSyncStep(deps = hostDeps$12) {
|
|
6178
6243
|
return {
|
|
6179
|
-
id: STEP_ID$
|
|
6244
|
+
id: STEP_ID$7,
|
|
6180
6245
|
title: "Connect a git repository",
|
|
6181
6246
|
redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
|
|
6182
6247
|
async detect(ctx) {
|
|
6183
6248
|
if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
|
|
6184
|
-
if (ctx.state.steps[STEP_ID$
|
|
6185
|
-
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$
|
|
6249
|
+
if (ctx.state.steps[STEP_ID$7]?.status === "skipped") return "not-applicable";
|
|
6250
|
+
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
|
|
6186
6251
|
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6187
6252
|
if (instance === null) return "todo";
|
|
6188
6253
|
try {
|
|
@@ -6364,8 +6429,8 @@ function skillsCli(runner) {
|
|
|
6364
6429
|
}
|
|
6365
6430
|
//#endregion
|
|
6366
6431
|
//#region src/steps/agents-skills.ts
|
|
6367
|
-
const STEP_ID$
|
|
6368
|
-
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$
|
|
6432
|
+
const STEP_ID$6 = "agents-skills";
|
|
6433
|
+
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$6}.agents` };
|
|
6369
6434
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6370
6435
|
const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
|
|
6371
6436
|
const DETECT_AGENTS = { kind: "detected" };
|
|
@@ -6548,8 +6613,8 @@ function skipLine(all, source) {
|
|
|
6548
6613
|
switch (source) {
|
|
6549
6614
|
case "override":
|
|
6550
6615
|
case "chosen":
|
|
6551
|
-
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$
|
|
6552
|
-
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$
|
|
6616
|
+
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$6}\` to install it later`;
|
|
6617
|
+
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$6} --agents <name>\` once one is installed`;
|
|
6553
6618
|
}
|
|
6554
6619
|
}
|
|
6555
6620
|
function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
@@ -6563,13 +6628,13 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6563
6628
|
};
|
|
6564
6629
|
}
|
|
6565
6630
|
return {
|
|
6566
|
-
id: STEP_ID$
|
|
6631
|
+
id: STEP_ID$6,
|
|
6567
6632
|
title: "Install the rde and data-app skills for your coding agents",
|
|
6568
6633
|
redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
|
|
6569
6634
|
async detect(ctx) {
|
|
6570
6635
|
const resolved = deps(ctx);
|
|
6571
6636
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6572
|
-
if (detected.length === 0) return ctx.state.steps[STEP_ID$
|
|
6637
|
+
if (detected.length === 0) return ctx.state.steps[STEP_ID$6]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6573
6638
|
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6574
6639
|
const check = await setCheck(resolved);
|
|
6575
6640
|
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
@@ -6617,8 +6682,10 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6617
6682
|
}
|
|
6618
6683
|
//#endregion
|
|
6619
6684
|
//#region src/steps/databases.ts
|
|
6620
|
-
const
|
|
6621
|
-
const
|
|
6685
|
+
const DATABASE_REQUIRED = { request: "required" };
|
|
6686
|
+
const WRITE_ACCESS = "Transforms write their tables to it, so connect it as a user that can create tables.";
|
|
6687
|
+
const NONE_CONNECTED = `rde init needs a database of your own to go on. ${WRITE_ACCESS}`;
|
|
6688
|
+
const NONE_NAMED = `rde init needs a database of your own and asks for one only at a terminal without --yes: pass --database <connection string>, or list it under "databases" in the --answers file. ${WRITE_ACCESS}`;
|
|
6622
6689
|
function hostDatabasesDeps(signal) {
|
|
6623
6690
|
return {
|
|
6624
6691
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
@@ -6626,7 +6693,7 @@ function hostDatabasesDeps(signal) {
|
|
|
6626
6693
|
now: () => /* @__PURE__ */ new Date()
|
|
6627
6694
|
};
|
|
6628
6695
|
}
|
|
6629
|
-
function requireApiKey$
|
|
6696
|
+
function requireApiKey$1(state, apiKey) {
|
|
6630
6697
|
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
6631
6698
|
return apiKey;
|
|
6632
6699
|
}
|
|
@@ -6636,7 +6703,7 @@ function hostAliasOf(runtime) {
|
|
|
6636
6703
|
case "jar": return null;
|
|
6637
6704
|
}
|
|
6638
6705
|
}
|
|
6639
|
-
function toRecord
|
|
6706
|
+
function toRecord(database, createdAt) {
|
|
6640
6707
|
return {
|
|
6641
6708
|
id: database.id,
|
|
6642
6709
|
name: database.name,
|
|
@@ -6648,7 +6715,7 @@ function authenticatedClient(state, apiKey, deps) {
|
|
|
6648
6715
|
const configured = requireConfiguredRuntime(state);
|
|
6649
6716
|
return deps.client(configured.url, {
|
|
6650
6717
|
kind: "api-key",
|
|
6651
|
-
key: requireApiKey$
|
|
6718
|
+
key: requireApiKey$1(state, apiKey)
|
|
6652
6719
|
});
|
|
6653
6720
|
}
|
|
6654
6721
|
async function listDatabases(state, apiKey, deps) {
|
|
@@ -6691,7 +6758,7 @@ async function connectDatabases(ctx, deps, opening) {
|
|
|
6691
6758
|
existingCount: existing.length,
|
|
6692
6759
|
readTextFile: deps.readTextFile,
|
|
6693
6760
|
onCreated: async (database) => {
|
|
6694
|
-
const record = toRecord
|
|
6761
|
+
const record = toRecord(database, deps.now().toISOString());
|
|
6695
6762
|
await ctx.saveState({
|
|
6696
6763
|
...ctx.state,
|
|
6697
6764
|
databases: [...ctx.state.databases, record]
|
|
@@ -6708,6 +6775,7 @@ async function connectDatabases(ctx, deps, opening) {
|
|
|
6708
6775
|
async function addDatabases(ctx, deps, options) {
|
|
6709
6776
|
const connected = await connectDatabases(ctx, deps, {
|
|
6710
6777
|
start: "engine",
|
|
6778
|
+
required: false,
|
|
6711
6779
|
...options
|
|
6712
6780
|
});
|
|
6713
6781
|
return {
|
|
@@ -6715,13 +6783,11 @@ async function addDatabases(ctx, deps, options) {
|
|
|
6715
6783
|
result: { addedDatabases: connected.added }
|
|
6716
6784
|
};
|
|
6717
6785
|
}
|
|
6718
|
-
async function
|
|
6719
|
-
const connected = await connectDatabases(ctx, deps,
|
|
6720
|
-
|
|
6721
|
-
offerStubs: true
|
|
6722
|
-
});
|
|
6786
|
+
async function connectRequired(ctx, deps, opening) {
|
|
6787
|
+
const connected = await connectDatabases(ctx, deps, opening);
|
|
6788
|
+
if (connected.existing + connected.created === 0) throw new ConfigError(NONE_CONNECTED);
|
|
6723
6789
|
return {
|
|
6724
|
-
outcome:
|
|
6790
|
+
outcome: "completed",
|
|
6725
6791
|
result: { addedDatabases: connected.added }
|
|
6726
6792
|
};
|
|
6727
6793
|
}
|
|
@@ -6731,34 +6797,19 @@ function hostDeps$10(ctx) {
|
|
|
6731
6797
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
6732
6798
|
};
|
|
6733
6799
|
}
|
|
6734
|
-
function sampleDatabaseId(state) {
|
|
6735
|
-
return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
|
|
6736
|
-
}
|
|
6737
|
-
function skippedBefore(state) {
|
|
6738
|
-
return state.steps[STEP_ID$6]?.status === "skipped";
|
|
6739
|
-
}
|
|
6740
|
-
function ownDatabaseIds(state) {
|
|
6741
|
-
const sampleId = sampleDatabaseId(state);
|
|
6742
|
-
return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
|
|
6743
|
-
}
|
|
6744
6800
|
function databasesStep(options, deps = hostDeps$10) {
|
|
6745
6801
|
return {
|
|
6746
|
-
id:
|
|
6802
|
+
id: "databases",
|
|
6747
6803
|
title: "Connect your databases",
|
|
6748
6804
|
redoHint: "Connect another with `rde db add --url <connection string>`.",
|
|
6749
6805
|
async detect(ctx) {
|
|
6750
6806
|
const configured = configuredRuntime(ctx.state);
|
|
6751
6807
|
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
6752
|
-
const
|
|
6753
|
-
|
|
6754
|
-
const asking = options.request === "ask-once" && !skippedBefore(ctx.state);
|
|
6755
|
-
if (own.size === 0 && !asking) {
|
|
6756
|
-
if (options.request === "named") return "todo";
|
|
6757
|
-
return sampleId !== null || skippedBefore(ctx.state) ? "done" : "todo";
|
|
6758
|
-
}
|
|
6808
|
+
const recorded = new Set(ctx.state.databases.map((record) => record.id));
|
|
6809
|
+
if (recorded.size === 0 && options.request === "named") return "todo";
|
|
6759
6810
|
const resolved = deps(ctx);
|
|
6760
6811
|
if (!await resolved.instanceRunning()) return "todo";
|
|
6761
|
-
const isOwn = (id) =>
|
|
6812
|
+
const isOwn = (id) => recorded.size === 0 || recorded.has(id);
|
|
6762
6813
|
const client = resolved.client(configured.url, {
|
|
6763
6814
|
kind: "api-key",
|
|
6764
6815
|
key: ctx.secrets.apiKey
|
|
@@ -6772,9 +6823,19 @@ function databasesStep(options, deps = hostDeps$10) {
|
|
|
6772
6823
|
},
|
|
6773
6824
|
async run(ctx) {
|
|
6774
6825
|
switch (options.request) {
|
|
6775
|
-
case "ask
|
|
6776
|
-
|
|
6777
|
-
|
|
6826
|
+
case "ask":
|
|
6827
|
+
ctx.ui.info(NONE_CONNECTED);
|
|
6828
|
+
return connectRequired(ctx, deps(ctx), {
|
|
6829
|
+
start: "connection-string",
|
|
6830
|
+
offerStubs: true,
|
|
6831
|
+
required: true
|
|
6832
|
+
});
|
|
6833
|
+
case "named": return connectRequired(ctx, deps(ctx), {
|
|
6834
|
+
start: "engine",
|
|
6835
|
+
offerStubs: false,
|
|
6836
|
+
required: true
|
|
6837
|
+
});
|
|
6838
|
+
case "required": throw new ConfigError(NONE_NAMED);
|
|
6778
6839
|
}
|
|
6779
6840
|
}
|
|
6780
6841
|
};
|
|
@@ -7028,23 +7089,44 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
7028
7089
|
const STEP_ID$4 = "license";
|
|
7029
7090
|
const LICENSE_KEYS = { token: `${STEP_ID$4}.token` };
|
|
7030
7091
|
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
7031
|
-
const NOT_FOUND_STATUS$
|
|
7092
|
+
const NOT_FOUND_STATUS$1 = 404;
|
|
7093
|
+
const TOKEN_REFUSED_STATUS = 400;
|
|
7094
|
+
const STORE_UNREACHABLE_STATUS = 503;
|
|
7095
|
+
const TOKEN_CHECK_HOST = "token-check.metabase.com";
|
|
7096
|
+
const ACTIVATION_FAILED = "License activation failed";
|
|
7097
|
+
const LICENSED_FEATURES = {
|
|
7098
|
+
[REMOTE_SYNC_FEATURE]: "remote sync",
|
|
7099
|
+
"data-apps": "data apps",
|
|
7100
|
+
"transforms-testing": "transform tests"
|
|
7101
|
+
};
|
|
7102
|
+
const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
|
|
7103
|
+
function licensedFeatureCoverage(features) {
|
|
7104
|
+
const entries = Object.entries(LICENSED_FEATURES);
|
|
7105
|
+
return {
|
|
7106
|
+
included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
|
|
7107
|
+
missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
|
|
7108
|
+
};
|
|
7109
|
+
}
|
|
7110
|
+
function listOf(names, type) {
|
|
7111
|
+
return new Intl.ListFormat("en", { type }).format(names);
|
|
7112
|
+
}
|
|
7032
7113
|
function hostDeps$8(ctx) {
|
|
7033
7114
|
return {
|
|
7034
7115
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7035
7116
|
runner: processRunner(ctx.signal),
|
|
7036
|
-
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
7117
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
7118
|
+
terminal: stdinIsTty()
|
|
7037
7119
|
};
|
|
7038
7120
|
}
|
|
7039
7121
|
function isNoTokenYet(error) {
|
|
7040
|
-
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$
|
|
7122
|
+
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
7041
7123
|
return isMetabaseNotServing(error);
|
|
7042
7124
|
}
|
|
7043
7125
|
function validateToken(value) {
|
|
7044
7126
|
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
7045
7127
|
return "A license token is 64 hexadecimal characters";
|
|
7046
7128
|
}
|
|
7047
|
-
function requireApiKey
|
|
7129
|
+
function requireApiKey(ctx) {
|
|
7048
7130
|
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
7049
7131
|
return ctx.secrets.apiKey;
|
|
7050
7132
|
}
|
|
@@ -7052,6 +7134,67 @@ function activeLabel(status) {
|
|
|
7052
7134
|
const plan = status["plan-alias"];
|
|
7053
7135
|
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
7054
7136
|
}
|
|
7137
|
+
function refusalOf(error) {
|
|
7138
|
+
if (!(error instanceof MetabaseApiError)) return null;
|
|
7139
|
+
if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
|
|
7140
|
+
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`);
|
|
7141
|
+
return null;
|
|
7142
|
+
}
|
|
7143
|
+
async function activate(client, token) {
|
|
7144
|
+
try {
|
|
7145
|
+
await client.setting.setPremiumEmbeddingToken(token);
|
|
7146
|
+
} catch (error) {
|
|
7147
|
+
const refusal = refusalOf(error);
|
|
7148
|
+
if (refusal === null) throw error;
|
|
7149
|
+
return {
|
|
7150
|
+
kind: "refused",
|
|
7151
|
+
error: refusal
|
|
7152
|
+
};
|
|
7153
|
+
}
|
|
7154
|
+
const status = await client.tokenStatus.get();
|
|
7155
|
+
if (!status.valid) return {
|
|
7156
|
+
kind: "refused",
|
|
7157
|
+
error: new ConfigError(`Metabase reports the token as ${status.status}`)
|
|
7158
|
+
};
|
|
7159
|
+
return {
|
|
7160
|
+
kind: "activated",
|
|
7161
|
+
status
|
|
7162
|
+
};
|
|
7163
|
+
}
|
|
7164
|
+
async function activateAskedToken(ctx, client, terminal) {
|
|
7165
|
+
const token = await ctx.prompts.password({
|
|
7166
|
+
key: LICENSE_KEYS.token,
|
|
7167
|
+
message: "Paste your Metabase license token (Enter to skip)",
|
|
7168
|
+
validate: validateToken
|
|
7169
|
+
});
|
|
7170
|
+
if (token.length === 0) return null;
|
|
7171
|
+
const spinner = ctx.ui.spinner("Activating the license…");
|
|
7172
|
+
let activation;
|
|
7173
|
+
try {
|
|
7174
|
+
activation = await activate(client, token);
|
|
7175
|
+
} catch (error) {
|
|
7176
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
7177
|
+
throw error;
|
|
7178
|
+
}
|
|
7179
|
+
if (activation.kind === "activated") {
|
|
7180
|
+
spinner.stop(activeLabel(activation.status));
|
|
7181
|
+
ctx.session.licenseToken.set(token);
|
|
7182
|
+
return activation.status;
|
|
7183
|
+
}
|
|
7184
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
7185
|
+
if (!terminal) throw activation.error;
|
|
7186
|
+
ctx.ui.warn(activation.error.message);
|
|
7187
|
+
return activateAskedToken(ctx, client, terminal);
|
|
7188
|
+
}
|
|
7189
|
+
function reportFeatures(ctx, features) {
|
|
7190
|
+
const { missing } = licensedFeatureCoverage(features);
|
|
7191
|
+
const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
|
|
7192
|
+
if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
|
|
7193
|
+
if (missing.length > 0) {
|
|
7194
|
+
const gitNote = remoteSync ? "" : "; the git step will be skipped";
|
|
7195
|
+
ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
|
|
7196
|
+
}
|
|
7197
|
+
}
|
|
7055
7198
|
function licenseStep(deps = hostDeps$8) {
|
|
7056
7199
|
return {
|
|
7057
7200
|
id: STEP_ID$4,
|
|
@@ -7076,36 +7219,17 @@ function licenseStep(deps = hostDeps$8) {
|
|
|
7076
7219
|
},
|
|
7077
7220
|
async run(ctx) {
|
|
7078
7221
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
7079
|
-
const apiKey = requireApiKey
|
|
7080
|
-
const token = await ctx.prompts.password({
|
|
7081
|
-
key: LICENSE_KEYS.token,
|
|
7082
|
-
message: "Paste your Metabase license token (Enter to skip)",
|
|
7083
|
-
validate: validateToken
|
|
7084
|
-
});
|
|
7085
|
-
if (token.length === 0) {
|
|
7086
|
-
ctx.ui.info("Skipping the license; remote sync needs one. Run `rde init --from license` to add it later");
|
|
7087
|
-
return { outcome: "skipped" };
|
|
7088
|
-
}
|
|
7089
|
-
ctx.session.licenseToken.set(token);
|
|
7222
|
+
const apiKey = requireApiKey(ctx);
|
|
7090
7223
|
const resolved = deps(ctx);
|
|
7091
|
-
const
|
|
7224
|
+
const status = await activateAskedToken(ctx, resolved.client(configured.url, {
|
|
7092
7225
|
kind: "api-key",
|
|
7093
7226
|
key: apiKey
|
|
7094
|
-
});
|
|
7095
|
-
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
status = await client.tokenStatus.get();
|
|
7100
|
-
} catch (error) {
|
|
7101
|
-
spinner.fail("License activation failed");
|
|
7102
|
-
throw error;
|
|
7103
|
-
}
|
|
7104
|
-
if (!status.valid) {
|
|
7105
|
-
spinner.fail("License activation failed");
|
|
7106
|
-
throw new ConfigError(`Metabase reports the token as ${status.status}`);
|
|
7227
|
+
}), resolved.terminal);
|
|
7228
|
+
if (status === null) {
|
|
7229
|
+
const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
|
|
7230
|
+
ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
|
|
7231
|
+
return { outcome: "skipped" };
|
|
7107
7232
|
}
|
|
7108
|
-
spinner.stop(activeLabel(status));
|
|
7109
7233
|
await ctx.saveState({
|
|
7110
7234
|
...ctx.state,
|
|
7111
7235
|
license: {
|
|
@@ -7120,8 +7244,7 @@ function licenseStep(deps = hostDeps$8) {
|
|
|
7120
7244
|
profile: ctx.state.mbProfile,
|
|
7121
7245
|
apiKey
|
|
7122
7246
|
});
|
|
7123
|
-
|
|
7124
|
-
else ctx.ui.warn("This license does not include remote sync; the git step will be skipped");
|
|
7247
|
+
reportFeatures(ctx, status.features);
|
|
7125
7248
|
return { outcome: "completed" };
|
|
7126
7249
|
}
|
|
7127
7250
|
};
|
|
@@ -7134,7 +7257,7 @@ const NODE_HINT = `Install Node ${MIN_NODE_VERSION} or newer from https://nodejs
|
|
|
7134
7257
|
const MARK_OK = "✓";
|
|
7135
7258
|
const MARK_MISSING = "✗";
|
|
7136
7259
|
const MARK_OPTIONAL = "–";
|
|
7137
|
-
const DOCKER_OPTIONAL_NOTE = `Metabase can run as a jar with Java instead
|
|
7260
|
+
const DOCKER_OPTIONAL_NOTE = `Metabase can run as a jar with Java instead`;
|
|
7138
7261
|
const JAVA_OPTIONAL_NOTE = `Metabase can run in Docker instead`;
|
|
7139
7262
|
const PLATFORMS = {
|
|
7140
7263
|
darwin: {
|
|
@@ -7460,210 +7583,12 @@ function runtimeStep(options, deps = hostDeps$6) {
|
|
|
7460
7583
|
}
|
|
7461
7584
|
};
|
|
7462
7585
|
}
|
|
7463
|
-
//#endregion
|
|
7464
|
-
//#region src/steps/sample-database.ts
|
|
7465
|
-
const STEP_ID$2 = "sample-database";
|
|
7466
|
-
const SAMPLE_DATABASE_DISPLAY_NAME = "Sample Database";
|
|
7467
|
-
const POSTGRES_ENGINE = "postgres";
|
|
7468
|
-
const NOT_FOUND_STATUS$1 = 404;
|
|
7469
|
-
function hostDeps$5(ctx) {
|
|
7470
|
-
return {
|
|
7471
|
-
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7472
|
-
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7473
|
-
readyTimeoutMs: SAMPLE_DATABASE_READY_TIMEOUT_MS,
|
|
7474
|
-
now: () => /* @__PURE__ */ new Date()
|
|
7475
|
-
};
|
|
7476
|
-
}
|
|
7477
|
-
function isNotFound$2(error) {
|
|
7478
|
-
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1;
|
|
7479
|
-
}
|
|
7480
|
-
function requireApiKey(ctx) {
|
|
7481
|
-
if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
|
|
7482
|
-
return ctx.secrets.apiKey;
|
|
7483
|
-
}
|
|
7484
|
-
async function recordedDatabase(record, client) {
|
|
7485
|
-
if (record.databaseId === null) return null;
|
|
7486
|
-
try {
|
|
7487
|
-
return await client.database.get(record.databaseId);
|
|
7488
|
-
} catch (error) {
|
|
7489
|
-
if (isNotFound$2(error)) return null;
|
|
7490
|
-
throw error;
|
|
7491
|
-
}
|
|
7492
|
-
}
|
|
7493
|
-
async function persistedRecord(ctx, deps) {
|
|
7494
|
-
const existing = ctx.state.sampleDatabase;
|
|
7495
|
-
if (existing !== null) {
|
|
7496
|
-
if ((await sampleDatabaseContainer(existing, deps.runtime.docker, deps.runtime.rdeHome).status()).installed || await deps.runtime.ports.isFree(existing.port)) return {
|
|
7497
|
-
record: existing,
|
|
7498
|
-
staleDatabaseId: null
|
|
7499
|
-
};
|
|
7500
|
-
const pointer = existing.databaseId === null ? "" : `; Metabase database ${existing.databaseId} still points at the old one`;
|
|
7501
|
-
ctx.ui.warn(`Port ${existing.port} is in use by another program; the sample database moves to a new port${pointer}`);
|
|
7502
|
-
}
|
|
7503
|
-
const claims = await portClaims(ctx.state, deps.runtime);
|
|
7504
|
-
const port = await deps.runtime.ports.findFree(SAMPLE_DATABASE_PORT, claims);
|
|
7505
|
-
if (port !== 25432) ctx.ui.warn(`Port ${SAMPLE_DATABASE_PORT} is in use or claimed; using ${port} for the sample database`);
|
|
7506
|
-
const record = {
|
|
7507
|
-
containerName: sampleContainerName(ctx.state.name, deps.runtime.rdeHome),
|
|
7508
|
-
image: SAMPLE_DATABASE_IMAGE,
|
|
7509
|
-
port,
|
|
7510
|
-
databaseId: null
|
|
7511
|
-
};
|
|
7512
|
-
await ctx.saveState({
|
|
7513
|
-
...ctx.state,
|
|
7514
|
-
sampleDatabase: record
|
|
7515
|
-
});
|
|
7516
|
-
return {
|
|
7517
|
-
record,
|
|
7518
|
-
staleDatabaseId: existing?.databaseId ?? null
|
|
7519
|
-
};
|
|
7520
|
-
}
|
|
7521
|
-
const LINUX = "linux";
|
|
7522
|
-
async function gatewayAddress(ctx, deps) {
|
|
7523
|
-
if (!(ctx.state.runtime?.kind === "docker") || deps.runtime.platform !== LINUX) return null;
|
|
7524
|
-
return deps.runtime.docker.bridgeGateway();
|
|
7525
|
-
}
|
|
7526
|
-
async function recordSampleDatabaseMove(ctx, from, to) {
|
|
7527
|
-
ctx.ui.warn(`Port ${from.port} is taken; the sample database moved to ${to.port}`);
|
|
7528
|
-
const record = {
|
|
7529
|
-
...to,
|
|
7530
|
-
databaseId: null
|
|
7531
|
-
};
|
|
7532
|
-
await ctx.saveState({
|
|
7533
|
-
...ctx.state,
|
|
7534
|
-
sampleDatabase: record,
|
|
7535
|
-
databases: ctx.state.databases.filter((entry) => entry.id !== from.databaseId)
|
|
7536
|
-
});
|
|
7537
|
-
return record;
|
|
7538
|
-
}
|
|
7539
|
-
async function installAndStart(ctx, sidecar, installed, deps) {
|
|
7540
|
-
const installing = ctx.ui.spinner(`Installing the sample database from ${installed.image}`);
|
|
7541
|
-
try {
|
|
7542
|
-
await sidecar.install({
|
|
7543
|
-
onProgress: (line) => installing.update(line),
|
|
7544
|
-
alsoOn: await gatewayAddress(ctx, deps)
|
|
7545
|
-
});
|
|
7546
|
-
} catch (error) {
|
|
7547
|
-
installing.fail("Sample database install failed");
|
|
7548
|
-
throw error;
|
|
7549
|
-
}
|
|
7550
|
-
installing.stop(`Sample database container ${installed.containerName} on port ${installed.port}`);
|
|
7551
|
-
const started = await startOnFreePort(installed, deps.runtime.docker, deps.runtime.rdeHome);
|
|
7552
|
-
const record = started.state.port === installed.port ? installed : await recordSampleDatabaseMove(ctx, installed, started.state);
|
|
7553
|
-
const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
|
|
7554
|
-
try {
|
|
7555
|
-
await sidecar.waitReady({
|
|
7556
|
-
timeoutMs: deps.readyTimeoutMs,
|
|
7557
|
-
signal: ctx.signal
|
|
7558
|
-
});
|
|
7559
|
-
} catch (error) {
|
|
7560
|
-
waiting.fail("Postgres did not become ready");
|
|
7561
|
-
throw error;
|
|
7562
|
-
}
|
|
7563
|
-
waiting.stop(`Postgres is ready on ${LOOPBACK_HOST}:${record.port}`);
|
|
7564
|
-
return record;
|
|
7565
|
-
}
|
|
7566
|
-
function connectionDetails(record, configured, deps) {
|
|
7567
|
-
const alias = runtimeFromState(configured.runtime, deps.runtime).hostAlias();
|
|
7568
|
-
return applyHostAlias({
|
|
7569
|
-
host: "localhost",
|
|
7570
|
-
port: record.port,
|
|
7571
|
-
dbname: SAMPLE_DATABASE_NAME,
|
|
7572
|
-
user: SAMPLE_DATABASE_USER,
|
|
7573
|
-
password: SAMPLE_DATABASE_PASSWORD
|
|
7574
|
-
}, alias).details;
|
|
7575
|
-
}
|
|
7576
|
-
function toRecord(database, createdAt) {
|
|
7577
|
-
return {
|
|
7578
|
-
id: database.id,
|
|
7579
|
-
name: database.name,
|
|
7580
|
-
engine: database.engine,
|
|
7581
|
-
createdAt
|
|
7582
|
-
};
|
|
7583
|
-
}
|
|
7584
|
-
async function connectDatabase(ctx, choice, configured, client, deps) {
|
|
7585
|
-
const { record } = choice;
|
|
7586
|
-
const known = await recordedDatabase(record, client);
|
|
7587
|
-
if (known !== null) {
|
|
7588
|
-
ctx.ui.info(`${known.name} is already connected as database ${known.id}`);
|
|
7589
|
-
return known;
|
|
7590
|
-
}
|
|
7591
|
-
const spinner = ctx.ui.spinner(`Connecting ${SAMPLE_DATABASE_DISPLAY_NAME}…`);
|
|
7592
|
-
let database;
|
|
7593
|
-
try {
|
|
7594
|
-
database = await client.database.create({
|
|
7595
|
-
name: SAMPLE_DATABASE_DISPLAY_NAME,
|
|
7596
|
-
engine: POSTGRES_ENGINE,
|
|
7597
|
-
details: connectionDetails(record, configured, deps)
|
|
7598
|
-
});
|
|
7599
|
-
} catch (error) {
|
|
7600
|
-
spinner.fail(`Could not connect ${SAMPLE_DATABASE_DISPLAY_NAME}`);
|
|
7601
|
-
throw error;
|
|
7602
|
-
}
|
|
7603
|
-
const stale = /* @__PURE__ */ new Set([record.databaseId, choice.staleDatabaseId]);
|
|
7604
|
-
const others = ctx.state.databases.filter((entry) => !stale.has(entry.id));
|
|
7605
|
-
await ctx.saveState({
|
|
7606
|
-
...ctx.state,
|
|
7607
|
-
sampleDatabase: {
|
|
7608
|
-
...record,
|
|
7609
|
-
databaseId: database.id
|
|
7610
|
-
},
|
|
7611
|
-
databases: [...others, toRecord(database, deps.now().toISOString())]
|
|
7612
|
-
});
|
|
7613
|
-
spinner.stop(`Connected ${database.name} as database ${database.id}`);
|
|
7614
|
-
return database;
|
|
7615
|
-
}
|
|
7616
|
-
function usageLines(record) {
|
|
7617
|
-
return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms can write to this database."].join("\n");
|
|
7618
|
-
}
|
|
7619
|
-
function sampleDatabaseStep(deps = hostDeps$5) {
|
|
7620
|
-
return {
|
|
7621
|
-
id: STEP_ID$2,
|
|
7622
|
-
title: "Connect a sample database",
|
|
7623
|
-
async detect(ctx) {
|
|
7624
|
-
const resolved = deps(ctx);
|
|
7625
|
-
if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
|
|
7626
|
-
const record = ctx.state.sampleDatabase;
|
|
7627
|
-
const configured = configuredRuntime(ctx.state);
|
|
7628
|
-
if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7629
|
-
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker, resolved.runtime.rdeHome).status()).installed || !await isInstanceRunning(ctx.state, resolved.runtime)) return "todo";
|
|
7630
|
-
const client = resolved.client(configured.url, {
|
|
7631
|
-
kind: "api-key",
|
|
7632
|
-
key: ctx.secrets.apiKey
|
|
7633
|
-
});
|
|
7634
|
-
try {
|
|
7635
|
-
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
7636
|
-
} catch (error) {
|
|
7637
|
-
if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
|
|
7638
|
-
throw error;
|
|
7639
|
-
}
|
|
7640
|
-
},
|
|
7641
|
-
async run(ctx) {
|
|
7642
|
-
const resolved = deps(ctx);
|
|
7643
|
-
const configured = requireConfiguredRuntime(ctx.state);
|
|
7644
|
-
const apiKey = requireApiKey(ctx);
|
|
7645
|
-
const docker = await resolved.runtime.docker.available();
|
|
7646
|
-
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
7647
|
-
const persisted = await persistedRecord(ctx, resolved);
|
|
7648
|
-
const record = await installAndStart(ctx, sampleDatabaseContainer(persisted.record, resolved.runtime.docker, resolved.runtime.rdeHome), persisted.record, resolved);
|
|
7649
|
-
await connectDatabase(ctx, {
|
|
7650
|
-
...persisted,
|
|
7651
|
-
record
|
|
7652
|
-
}, configured, resolved.client(configured.url, {
|
|
7653
|
-
kind: "api-key",
|
|
7654
|
-
key: apiKey
|
|
7655
|
-
}), resolved);
|
|
7656
|
-
ctx.ui.note(usageLines(record), "Sample database");
|
|
7657
|
-
return { outcome: "completed" };
|
|
7658
|
-
}
|
|
7659
|
-
};
|
|
7660
|
-
}
|
|
7661
7586
|
const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
|
|
7662
7587
|
//#endregion
|
|
7663
7588
|
//#region src/steps/start-metabase.ts
|
|
7664
7589
|
const LOG_TAIL_ON_TIMEOUT = 30;
|
|
7665
7590
|
const PERCENT = 100;
|
|
7666
|
-
function hostDeps$
|
|
7591
|
+
function hostDeps$5(ctx) {
|
|
7667
7592
|
return {
|
|
7668
7593
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7669
7594
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -7679,21 +7604,6 @@ async function runningVersion(client) {
|
|
|
7679
7604
|
function progressLabel(progress) {
|
|
7680
7605
|
return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
|
|
7681
7606
|
}
|
|
7682
|
-
async function startSampleDatabase(ctx, deps) {
|
|
7683
|
-
const record = ctx.state.sampleDatabase;
|
|
7684
|
-
if (record === null) return;
|
|
7685
|
-
const started = await startOnFreePort(record, deps.runtime.docker, deps.runtime.rdeHome);
|
|
7686
|
-
if (started.state.port !== record.port) await recordSampleDatabaseMove(ctx, record, started.state);
|
|
7687
|
-
switch (started.outcome) {
|
|
7688
|
-
case "missing":
|
|
7689
|
-
ctx.ui.warn(`Sample database container ${record.containerName} is missing; \`rde init\` recreates it`);
|
|
7690
|
-
return;
|
|
7691
|
-
case "started":
|
|
7692
|
-
ctx.ui.info(`Sample database container ${record.containerName} started`);
|
|
7693
|
-
return;
|
|
7694
|
-
case "running": return;
|
|
7695
|
-
}
|
|
7696
|
-
}
|
|
7697
7607
|
async function withMoveHint(ctx, deps, taken) {
|
|
7698
7608
|
const claims = await portClaims(ctx.state, deps.runtime);
|
|
7699
7609
|
const free = await deps.runtime.ports.findFree(taken.port + 1, claims);
|
|
@@ -7707,7 +7617,7 @@ async function startRuntime(ctx, runtime, deps) {
|
|
|
7707
7617
|
throw error;
|
|
7708
7618
|
}
|
|
7709
7619
|
}
|
|
7710
|
-
function startMetabaseStep(deps = hostDeps$
|
|
7620
|
+
function startMetabaseStep(deps = hostDeps$5) {
|
|
7711
7621
|
return {
|
|
7712
7622
|
id: "start-metabase",
|
|
7713
7623
|
title: "Start Metabase",
|
|
@@ -7730,7 +7640,6 @@ function startMetabaseStep(deps = hostDeps$4) {
|
|
|
7730
7640
|
const runtime = runtimeFromState(configured.runtime, resolved.runtime);
|
|
7731
7641
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
7732
7642
|
await startRuntime(ctx, runtime, resolved);
|
|
7733
|
-
await startSampleDatabase(ctx, resolved);
|
|
7734
7643
|
const spinner = ctx.ui.spinner("Starting Metabase…");
|
|
7735
7644
|
let shown = 0;
|
|
7736
7645
|
try {
|
|
@@ -7766,6 +7675,261 @@ function startMetabaseStep(deps = hostDeps$4) {
|
|
|
7766
7675
|
};
|
|
7767
7676
|
}
|
|
7768
7677
|
//#endregion
|
|
7678
|
+
//#region src/steps/sync-scope.ts
|
|
7679
|
+
const STEP_ID$2 = "sync-scope";
|
|
7680
|
+
const SYNC_SCOPE_KEYS = { export: `${STEP_ID$2}.export` };
|
|
7681
|
+
const AGENT_COLLECTION_NAME = "Analytics";
|
|
7682
|
+
const LIBRARY_NAME = "Library";
|
|
7683
|
+
const ROOT_COLLECTION_NAME = "Our analytics";
|
|
7684
|
+
const AGENT_COLLECTION_DESCRIPTION = "Dashboards, questions, models, and documents that reach git on export.";
|
|
7685
|
+
function hostDeps$4(ctx) {
|
|
7686
|
+
return {
|
|
7687
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7688
|
+
runner: processRunner(ctx.signal),
|
|
7689
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
7690
|
+
};
|
|
7691
|
+
}
|
|
7692
|
+
function isCollection(entry) {
|
|
7693
|
+
return typeof entry.id === "number";
|
|
7694
|
+
}
|
|
7695
|
+
function isOrdinary(collection) {
|
|
7696
|
+
return collection.type === null && collection.namespace === null && collection.personal_owner_id === null;
|
|
7697
|
+
}
|
|
7698
|
+
function readScope(list, syncRoot) {
|
|
7699
|
+
const topLevel = list.filter(isCollection).filter((collection) => collection.location === "/");
|
|
7700
|
+
const ordinary = topLevel.filter(isOrdinary);
|
|
7701
|
+
const unsynced = ordinary.filter((collection) => !collection.is_remote_synced);
|
|
7702
|
+
return {
|
|
7703
|
+
library: topLevel.find((collection) => collection.type === "library") ?? null,
|
|
7704
|
+
syncedRoots: ordinary.filter((collection) => collection.is_remote_synced),
|
|
7705
|
+
unsyncedRecordedRoot: unsynced.find((collection) => collection.id === syncRoot) ?? null,
|
|
7706
|
+
foreignNamesake: unsynced.some((collection) => collection.id !== syncRoot && collection.name === "Analytics")
|
|
7707
|
+
};
|
|
7708
|
+
}
|
|
7709
|
+
function isComplete(scope, libraryLicensed) {
|
|
7710
|
+
return (!libraryLicensed || scope.library !== null && scope.library.is_remote_synced) && scope.syncedRoots.length > 0;
|
|
7711
|
+
}
|
|
7712
|
+
function isNotAnsweringYet(error) {
|
|
7713
|
+
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || isRejectedCredential(error);
|
|
7714
|
+
}
|
|
7715
|
+
function requireMbInstance(ctx) {
|
|
7716
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
7717
|
+
if (instance === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
7718
|
+
return instance;
|
|
7719
|
+
}
|
|
7720
|
+
function requireGitRemote(ctx) {
|
|
7721
|
+
if (ctx.state.gitRemote === null) throw new ConfigError("no repository yet; the git-sync step comes first");
|
|
7722
|
+
return ctx.state.gitRemote;
|
|
7723
|
+
}
|
|
7724
|
+
async function requireSettledImport(ctx, client, gitRemote) {
|
|
7725
|
+
const task = await client.remoteSync.currentTask();
|
|
7726
|
+
if (task === null) return;
|
|
7727
|
+
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`);
|
|
7728
|
+
if (task.sync_task_type === "import" && task.status !== "successful") {
|
|
7729
|
+
const importCommand = `${mbCommand(ctx.state.mbProfile)} git-sync import --branch ${shellWord(gitRemote.branch)}`;
|
|
7730
|
+
throw new ConfigError(`the last import from the repository ended ${task.status}; run \`${importCommand}\`, then \`rde init --only ${STEP_ID$2}\``);
|
|
7731
|
+
}
|
|
7732
|
+
}
|
|
7733
|
+
async function isReadOnly(client) {
|
|
7734
|
+
return await client.setting.remoteSyncType() === "read-only";
|
|
7735
|
+
}
|
|
7736
|
+
async function requireReadWrite(client) {
|
|
7737
|
+
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}\``);
|
|
7738
|
+
}
|
|
7739
|
+
async function ensureLibrary(client, scope, libraryLicensed) {
|
|
7740
|
+
if (!libraryLicensed) return null;
|
|
7741
|
+
return scope.library ?? client.collection.createLibrary();
|
|
7742
|
+
}
|
|
7743
|
+
async function ensureRoots(ctx, client, scope) {
|
|
7744
|
+
if (scope.syncedRoots.length > 0) return {
|
|
7745
|
+
roots: scope.syncedRoots,
|
|
7746
|
+
created: false,
|
|
7747
|
+
besideNamesake: false
|
|
7748
|
+
};
|
|
7749
|
+
if (scope.unsyncedRecordedRoot !== null) return {
|
|
7750
|
+
roots: [scope.unsyncedRecordedRoot],
|
|
7751
|
+
created: false,
|
|
7752
|
+
besideNamesake: false
|
|
7753
|
+
};
|
|
7754
|
+
const created = await client.collection.create({
|
|
7755
|
+
name: AGENT_COLLECTION_NAME,
|
|
7756
|
+
description: AGENT_COLLECTION_DESCRIPTION
|
|
7757
|
+
});
|
|
7758
|
+
await ctx.saveState({
|
|
7759
|
+
...ctx.state,
|
|
7760
|
+
syncRoot: created.id
|
|
7761
|
+
});
|
|
7762
|
+
return {
|
|
7763
|
+
roots: [created],
|
|
7764
|
+
created: true,
|
|
7765
|
+
besideNamesake: scope.foreignNamesake
|
|
7766
|
+
};
|
|
7767
|
+
}
|
|
7768
|
+
function itemLabel(item) {
|
|
7769
|
+
return item.name === null ? `${item.model} ${item.id}` : `${item.model} ${item.id} "${item.name}"`;
|
|
7770
|
+
}
|
|
7771
|
+
function locationLabel(location) {
|
|
7772
|
+
if (location === void 0) return "in a collection Metabase could not find";
|
|
7773
|
+
return `in ${location === null ? ROOT_COLLECTION_NAME : location.name}`;
|
|
7774
|
+
}
|
|
7775
|
+
function dependencyLine(dependency) {
|
|
7776
|
+
const line = ` ${itemLabel(dependency)} ${locationLabel(dependency.collection)}`;
|
|
7777
|
+
if (dependency.used_by.length === 0) return line;
|
|
7778
|
+
return `${line}, used by ${dependency.used_by.map(itemLabel).join(", ")}`;
|
|
7779
|
+
}
|
|
7780
|
+
function remedyLine(required) {
|
|
7781
|
+
const remedy = required.remedy;
|
|
7782
|
+
switch (remedy.type) {
|
|
7783
|
+
case "collection":
|
|
7784
|
+
if (required.syncable) return ` Fix: sync ${remedy.collection.name} too, or move that content into a synced collection.`;
|
|
7785
|
+
return ` Fix: move that content into a synced collection; ${remedy.collection.name} cannot sync.`;
|
|
7786
|
+
case "library": return " Fix: stop using that content; it syncs only through a Library, and this instance has none.";
|
|
7787
|
+
case "none": return " Fix: move that content into a synced collection, or stop using it.";
|
|
7788
|
+
}
|
|
7789
|
+
}
|
|
7790
|
+
function requiredSyncLines(required) {
|
|
7791
|
+
return [
|
|
7792
|
+
`Metabase will not sync ${required.blocks.map((collection) => collection.name).join(", ")}, because content there uses:`,
|
|
7793
|
+
...required.dependencies.map(dependencyLine),
|
|
7794
|
+
remedyLine(required)
|
|
7795
|
+
];
|
|
7796
|
+
}
|
|
7797
|
+
function explainRefusal(error) {
|
|
7798
|
+
if (!(error instanceof MetabaseApiError) || error.code !== "unsynced-dependencies") return error;
|
|
7799
|
+
const refusal = UnsyncedDependencies.safeParse(error.detail);
|
|
7800
|
+
if (!refusal.success) return error;
|
|
7801
|
+
return new ConfigError([...refusal.data.errors.required.flatMap(requiredSyncLines), `After the fix, run \`rde init --only ${STEP_ID$2}\`.`].join("\n"));
|
|
7802
|
+
}
|
|
7803
|
+
function plannedNames(scope, libraryLicensed) {
|
|
7804
|
+
const library = libraryLicensed ? [scope.library?.name ?? LIBRARY_NAME] : [];
|
|
7805
|
+
if (scope.syncedRoots.length > 0) return [...library, ...scope.syncedRoots.map((collection) => collection.name)];
|
|
7806
|
+
return [...library, scope.unsyncedRecordedRoot?.name ?? "Analytics"];
|
|
7807
|
+
}
|
|
7808
|
+
function nameList(names) {
|
|
7809
|
+
if (names.length <= 2) return names.join(" and ");
|
|
7810
|
+
return `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
|
|
7811
|
+
}
|
|
7812
|
+
function exportAdvice(profile, branch, changes) {
|
|
7813
|
+
const command = exportCommand(profile, branch);
|
|
7814
|
+
if (!remoteMoved(changes)) return `\`${command}\``;
|
|
7815
|
+
return `the branch has commits Metabase has not imported; \`${mbCommand(profile)} git-sync export-preflight\`, then \`${command} --merge\``;
|
|
7816
|
+
}
|
|
7817
|
+
async function planExport(ctx, runner, instance, names) {
|
|
7818
|
+
const changes = await mbTool(runner).gitSyncHasRemoteChanges(instance);
|
|
7819
|
+
if (remoteMoved(changes)) return {
|
|
7820
|
+
changes,
|
|
7821
|
+
push: false
|
|
7822
|
+
};
|
|
7823
|
+
return {
|
|
7824
|
+
changes,
|
|
7825
|
+
push: await ctx.prompts.confirm({
|
|
7826
|
+
key: SYNC_SCOPE_KEYS.export,
|
|
7827
|
+
message: `Push ${nameList(names)} to ${requireGitRemote(ctx).branch} now?`,
|
|
7828
|
+
initialValue: true
|
|
7829
|
+
})
|
|
7830
|
+
};
|
|
7831
|
+
}
|
|
7832
|
+
function describeExport(task) {
|
|
7833
|
+
const ended = `the export ended ${task.status}`;
|
|
7834
|
+
return task.error_message === null || task.error_message === void 0 ? ended : `${ended}: ${task.error_message}`;
|
|
7835
|
+
}
|
|
7836
|
+
function reportPending(run) {
|
|
7837
|
+
const advice = exportAdvice(run.ctx.state.mbProfile, run.gitRemote.branch, run.changes);
|
|
7838
|
+
run.ctx.ui.info(`First export pending: ${advice}`);
|
|
7839
|
+
}
|
|
7840
|
+
async function exportScope(run, runner) {
|
|
7841
|
+
const mb = mbTool(runner);
|
|
7842
|
+
const label = nameList(run.names);
|
|
7843
|
+
const spinner = run.ctx.ui.spinner(`Pushing ${label} to ${run.gitRemote.branch}…`);
|
|
7844
|
+
try {
|
|
7845
|
+
const final = (await mb.gitSyncExport(run.instance, {
|
|
7846
|
+
branch: run.gitRemote.branch,
|
|
7847
|
+
message: `Sync ${label}`
|
|
7848
|
+
})).final ?? null;
|
|
7849
|
+
if (final !== null && final.status !== "successful") {
|
|
7850
|
+
spinner.fail(describeExport(final));
|
|
7851
|
+
reportPending(run);
|
|
7852
|
+
return;
|
|
7853
|
+
}
|
|
7854
|
+
const status = await mb.gitSyncStatus(run.instance);
|
|
7855
|
+
spinner.stop(`Pushed ${label} to ${run.gitRemote.branch}`);
|
|
7856
|
+
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`);
|
|
7857
|
+
} catch (error) {
|
|
7858
|
+
if (!(error instanceof ExternalToolError)) throw error;
|
|
7859
|
+
spinner.fail(`Could not push ${label}: ${error.message}`);
|
|
7860
|
+
reportPending(run);
|
|
7861
|
+
}
|
|
7862
|
+
}
|
|
7863
|
+
function scopeLabel(synced) {
|
|
7864
|
+
return `Synced to git: ${synced.map((collection) => collection.name).join(", ")}`;
|
|
7865
|
+
}
|
|
7866
|
+
function syncScopeStep(deps = hostDeps$4) {
|
|
7867
|
+
return {
|
|
7868
|
+
id: STEP_ID$2,
|
|
7869
|
+
title: "Choose what syncs to git",
|
|
7870
|
+
async detect(ctx) {
|
|
7871
|
+
if (!hasRemoteSync(ctx.state.license) || ctx.state.gitRemote === null) return "not-applicable";
|
|
7872
|
+
const configured = configuredRuntime(ctx.state);
|
|
7873
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7874
|
+
const resolved = deps(ctx);
|
|
7875
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
7876
|
+
const client = resolved.client(configured.url, {
|
|
7877
|
+
kind: "api-key",
|
|
7878
|
+
key: ctx.secrets.apiKey
|
|
7879
|
+
});
|
|
7880
|
+
try {
|
|
7881
|
+
if (isComplete(readScope(await client.collection.list(), ctx.state.syncRoot), hasLibrary(ctx.state.license))) return "done";
|
|
7882
|
+
return await isReadOnly(client) ? "not-applicable" : "todo";
|
|
7883
|
+
} catch (error) {
|
|
7884
|
+
if (isNotAnsweringYet(error)) return "todo";
|
|
7885
|
+
throw error;
|
|
7886
|
+
}
|
|
7887
|
+
},
|
|
7888
|
+
async run(ctx) {
|
|
7889
|
+
requireConfiguredRuntime(ctx.state);
|
|
7890
|
+
const gitRemote = requireGitRemote(ctx);
|
|
7891
|
+
const instance = requireMbInstance(ctx);
|
|
7892
|
+
const resolved = deps(ctx);
|
|
7893
|
+
const client = resolved.client(instance.url, {
|
|
7894
|
+
kind: "api-key",
|
|
7895
|
+
key: instance.apiKey
|
|
7896
|
+
});
|
|
7897
|
+
await requireReadWrite(client);
|
|
7898
|
+
await requireSettledImport(ctx, client, gitRemote);
|
|
7899
|
+
const libraryLicensed = hasLibrary(ctx.state.license);
|
|
7900
|
+
const scope = readScope(await client.collection.list(), ctx.state.syncRoot);
|
|
7901
|
+
const names = plannedNames(scope, libraryLicensed);
|
|
7902
|
+
const plan = isComplete(scope, libraryLicensed) ? null : await planExport(ctx, resolved.runner, instance, names);
|
|
7903
|
+
const spinner = ctx.ui.spinner("Choosing what syncs to git…");
|
|
7904
|
+
let synced;
|
|
7905
|
+
let chosen;
|
|
7906
|
+
try {
|
|
7907
|
+
const library = await ensureLibrary(client, scope, libraryLicensed);
|
|
7908
|
+
chosen = await ensureRoots(ctx, client, scope);
|
|
7909
|
+
synced = library === null ? chosen.roots : [library, ...chosen.roots];
|
|
7910
|
+
await client.remoteSync.updateScope({ collections: Object.fromEntries(synced.map((collection) => [collection.id, true])) });
|
|
7911
|
+
} catch (error) {
|
|
7912
|
+
spinner.fail("Could not choose what syncs to git");
|
|
7913
|
+
throw explainRefusal(error);
|
|
7914
|
+
}
|
|
7915
|
+
spinner.stop(scopeLabel(synced));
|
|
7916
|
+
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`);
|
|
7917
|
+
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`);
|
|
7918
|
+
if (plan === null) return { outcome: "completed" };
|
|
7919
|
+
const exportRun = {
|
|
7920
|
+
ctx,
|
|
7921
|
+
instance,
|
|
7922
|
+
gitRemote,
|
|
7923
|
+
names,
|
|
7924
|
+
changes: plan.changes
|
|
7925
|
+
};
|
|
7926
|
+
if (plan.push) await exportScope(exportRun, resolved.runner);
|
|
7927
|
+
else reportPending(exportRun);
|
|
7928
|
+
return { outcome: "completed" };
|
|
7929
|
+
}
|
|
7930
|
+
};
|
|
7931
|
+
}
|
|
7932
|
+
//#endregion
|
|
7769
7933
|
//#region src/steps/summary.ts
|
|
7770
7934
|
const STEP_ID$1 = "summary";
|
|
7771
7935
|
const GIT_SYNC_STEP_ID = "git-sync";
|
|
@@ -7823,6 +7987,19 @@ function gitLines(state) {
|
|
|
7823
7987
|
if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
|
|
7824
7988
|
return [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
|
|
7825
7989
|
}
|
|
7990
|
+
async function pendingExportLines(state, secrets, runner) {
|
|
7991
|
+
const instance = mbInstanceOf(state, secrets);
|
|
7992
|
+
if (state.gitRemote === null || instance === null) return [];
|
|
7993
|
+
try {
|
|
7994
|
+
const mb = mbTool(runner);
|
|
7995
|
+
if (!(await mb.gitSyncStatus(instance)).is_dirty) return [];
|
|
7996
|
+
const changes = await mb.gitSyncHasRemoteChanges(instance);
|
|
7997
|
+
return [`Export pending: ${exportAdvice(state.mbProfile, state.gitRemote.branch, changes)}`];
|
|
7998
|
+
} catch (error) {
|
|
7999
|
+
if (error instanceof ProcessNotFoundError || error instanceof ExternalToolError) return [`Remote sync: ${error.message}`];
|
|
8000
|
+
throw error;
|
|
8001
|
+
}
|
|
8002
|
+
}
|
|
7826
8003
|
function skillLine(entries, detected) {
|
|
7827
8004
|
const agents = [...new Set(entries.flatMap((entry) => coveredAgentNames(entry, detected)))];
|
|
7828
8005
|
if (agents.length === 0) return `rde skill: not installed; \`rde init --only agents-skills\``;
|
|
@@ -7834,15 +8011,15 @@ function launchLines(agents, workdir) {
|
|
|
7834
8011
|
const prefix = workdir === null ? "" : `cd ${workdir} && `;
|
|
7835
8012
|
return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
|
|
7836
8013
|
}
|
|
7837
|
-
async function summaryLines(state,
|
|
8014
|
+
async function summaryLines(state, secrets, deps) {
|
|
7838
8015
|
const profile = state.mbProfile;
|
|
7839
8016
|
const lines = [];
|
|
7840
8017
|
if (state.url !== null) lines.push(`Metabase: ${state.url}`);
|
|
7841
8018
|
if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
|
|
7842
8019
|
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
7843
|
-
lines.push(...await databaseLines$1(state, apiKey, deps));
|
|
7844
|
-
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
8020
|
+
lines.push(...await databaseLines$1(state, secrets.apiKey, deps));
|
|
7845
8021
|
lines.push(...gitLines(state));
|
|
8022
|
+
lines.push(...await pendingExportLines(state, secrets, deps.runner));
|
|
7846
8023
|
const installed = await skillsCli(deps.runner).list();
|
|
7847
8024
|
const agents = await detectAgents(deps.probe);
|
|
7848
8025
|
const detected = agents.filter((agent) => agent.detected);
|
|
@@ -7859,7 +8036,7 @@ function summaryStep(deps = hostDeps$3) {
|
|
|
7859
8036
|
return "todo";
|
|
7860
8037
|
},
|
|
7861
8038
|
async run(ctx) {
|
|
7862
|
-
const lines = await summaryLines(ctx.state, ctx.secrets
|
|
8039
|
+
const lines = await summaryLines(ctx.state, ctx.secrets, deps(ctx));
|
|
7863
8040
|
ctx.ui.note(lines.join("\n"), "Your environment");
|
|
7864
8041
|
return { outcome: "completed" };
|
|
7865
8042
|
}
|
|
@@ -7917,7 +8094,7 @@ const DEFAULT_STEP_OPTIONS = {
|
|
|
7917
8094
|
runtime: NO_RUNTIME_FLAGS,
|
|
7918
8095
|
agents: DETECT_AGENTS,
|
|
7919
8096
|
admin: DEFAULT_ADMIN,
|
|
7920
|
-
databases:
|
|
8097
|
+
databases: DATABASE_REQUIRED,
|
|
7921
8098
|
updates: "published"
|
|
7922
8099
|
};
|
|
7923
8100
|
const LOCAL_STEP_OPTIONS = {
|
|
@@ -7933,10 +8110,10 @@ const STEP_BY_ID = {
|
|
|
7933
8110
|
"admin-user": (options) => adminUserStep(options.admin),
|
|
7934
8111
|
"api-key": () => apiKeyStep(),
|
|
7935
8112
|
transforms: () => transformsStep(),
|
|
7936
|
-
"sample-database": () => sampleDatabaseStep(),
|
|
7937
8113
|
license: () => licenseStep(),
|
|
7938
8114
|
databases: (options) => databasesStep(options.databases),
|
|
7939
8115
|
"git-sync": () => gitSyncStep(),
|
|
8116
|
+
"sync-scope": () => syncScopeStep(),
|
|
7940
8117
|
summary: () => summaryStep()
|
|
7941
8118
|
};
|
|
7942
8119
|
function initSteps(options) {
|
|
@@ -8009,7 +8186,7 @@ const SECTIONS = {
|
|
|
8009
8186
|
}).optional(),
|
|
8010
8187
|
"api-key": z.strictObject({ profile: MbProfileChoice }).optional(),
|
|
8011
8188
|
license: z.strictObject({ token: EnvString.nullable() }).optional(),
|
|
8012
|
-
databases: z.array(DatabaseEntry).optional(),
|
|
8189
|
+
databases: z.array(DatabaseEntry).min(1, "a database is required: list one, or leave the section out to be asked at a terminal").optional(),
|
|
8013
8190
|
"git-sync": z.strictObject({
|
|
8014
8191
|
location: z.enum([
|
|
8015
8192
|
"here",
|
|
@@ -8021,7 +8198,8 @@ const SECTIONS = {
|
|
|
8021
8198
|
remote: RemoteKind.optional(),
|
|
8022
8199
|
url: EnvString.optional(),
|
|
8023
8200
|
token: EnvString.optional()
|
|
8024
|
-
}).optional()
|
|
8201
|
+
}).optional(),
|
|
8202
|
+
"sync-scope": z.strictObject({ export: z.boolean() }).optional()
|
|
8025
8203
|
};
|
|
8026
8204
|
const InitAnswers = z.strictObject(SECTIONS);
|
|
8027
8205
|
const select$1 = (value) => ({
|
|
@@ -8081,8 +8259,8 @@ function runtimeKindOf(runtime) {
|
|
|
8081
8259
|
return null;
|
|
8082
8260
|
}
|
|
8083
8261
|
function databasesRequest(databases, run) {
|
|
8084
|
-
if (databases !== void 0) return
|
|
8085
|
-
return run.interactive ? "ask
|
|
8262
|
+
if (databases !== void 0) return "named";
|
|
8263
|
+
return run.interactive ? "ask" : "required";
|
|
8086
8264
|
}
|
|
8087
8265
|
function agentsOption(agents, run) {
|
|
8088
8266
|
if (agents !== void 0) return {
|
|
@@ -8130,6 +8308,8 @@ function keyedAnswers(answers) {
|
|
|
8130
8308
|
if (git.url !== void 0) add(map, GIT_SYNC_KEYS.url, text$1(git.url));
|
|
8131
8309
|
if (git.token !== void 0) add(map, GIT_SYNC_KEYS.token, password$1(git.token));
|
|
8132
8310
|
}
|
|
8311
|
+
const scope = answers["sync-scope"];
|
|
8312
|
+
if (scope !== void 0) add(map, SYNC_SCOPE_KEYS.export, confirm$1(scope.export));
|
|
8133
8313
|
return map;
|
|
8134
8314
|
}
|
|
8135
8315
|
function parseInitAnswers(raw, source) {
|
|
@@ -8144,7 +8324,6 @@ async function loadInitAnswers(path) {
|
|
|
8144
8324
|
}
|
|
8145
8325
|
function stepsAskingForWork(answers) {
|
|
8146
8326
|
const license = answers.license;
|
|
8147
|
-
const databases = answers.databases;
|
|
8148
8327
|
const git = answers["git-sync"];
|
|
8149
8328
|
const agents = answers["agents-skills"];
|
|
8150
8329
|
return [
|
|
@@ -8166,12 +8345,16 @@ function stepsAskingForWork(answers) {
|
|
|
8166
8345
|
},
|
|
8167
8346
|
{
|
|
8168
8347
|
step: "databases",
|
|
8169
|
-
asks: databases !== void 0
|
|
8348
|
+
asks: answers.databases !== void 0
|
|
8170
8349
|
},
|
|
8171
8350
|
{
|
|
8172
8351
|
step: "git-sync",
|
|
8173
8352
|
asks: git !== void 0 && git.location !== "skip"
|
|
8174
8353
|
},
|
|
8354
|
+
{
|
|
8355
|
+
step: "sync-scope",
|
|
8356
|
+
asks: answers["sync-scope"]?.export === true
|
|
8357
|
+
},
|
|
8175
8358
|
{
|
|
8176
8359
|
step: "agents-skills",
|
|
8177
8360
|
asks: agents !== void 0 && agents.agents.length > 0
|
|
@@ -8203,7 +8386,6 @@ const FLAG_FOR_KEY$1 = {
|
|
|
8203
8386
|
[API_KEY_KEYS.password]: "--admin-password",
|
|
8204
8387
|
[API_KEY_KEYS.profile]: "--mb-profile",
|
|
8205
8388
|
[LICENSE_KEYS.token]: "--license-token or --skip-license",
|
|
8206
|
-
[DATABASE_KEYS.skip]: "--skip-databases",
|
|
8207
8389
|
[GIT_SYNC_KEYS.location]: "--git-dir",
|
|
8208
8390
|
[GIT_SYNC_KEYS.path]: "--git-dir",
|
|
8209
8391
|
[GIT_SYNC_KEYS.remote]: "--git-remote or --skip-git",
|
|
@@ -8211,7 +8393,7 @@ const FLAG_FOR_KEY$1 = {
|
|
|
8211
8393
|
[GIT_SYNC_KEYS.token]: "--git-token"
|
|
8212
8394
|
};
|
|
8213
8395
|
const DATABASE_FLAG_FOR_NAME = {
|
|
8214
|
-
engine: "--database
|
|
8396
|
+
engine: "--database",
|
|
8215
8397
|
uri: "--database"
|
|
8216
8398
|
};
|
|
8217
8399
|
function howToAnswer(key) {
|
|
@@ -8302,13 +8484,6 @@ function licenseSection(flags, secrets) {
|
|
|
8302
8484
|
}
|
|
8303
8485
|
return secrets.has("--license-token") ? { token: secrets.get("--license-token") } : void 0;
|
|
8304
8486
|
}
|
|
8305
|
-
function databasesSection(flags) {
|
|
8306
|
-
if (flags.skipDatabases) {
|
|
8307
|
-
if (flags.database.length > 0) throw conflict("--skip-databases", "--database");
|
|
8308
|
-
return [];
|
|
8309
|
-
}
|
|
8310
|
-
return flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0;
|
|
8311
|
-
}
|
|
8312
8487
|
function gitRemoteKind(flag, url) {
|
|
8313
8488
|
if (flag === void 0) return url === void 0 ? void 0 : "hosted";
|
|
8314
8489
|
const kind = GIT_REMOTE_NAMES[flag];
|
|
@@ -8429,7 +8604,7 @@ async function answersFromFlags(flags, readStdin) {
|
|
|
8429
8604
|
runtime: runtimeSection(flags),
|
|
8430
8605
|
"admin-user": adminSection(flags, secrets),
|
|
8431
8606
|
license: licenseSection(flags, secrets),
|
|
8432
|
-
databases:
|
|
8607
|
+
databases: flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0,
|
|
8433
8608
|
"git-sync": gitSection(flags, secrets),
|
|
8434
8609
|
"agents-skills": agentsSection(flags),
|
|
8435
8610
|
"api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
|
|
@@ -8854,6 +9029,7 @@ const dbAdd = defineRdeCommand({
|
|
|
8854
9029
|
description: "Engine of a --url that fits several engines; the error names them"
|
|
8855
9030
|
}
|
|
8856
9031
|
},
|
|
9032
|
+
interrupted: INTERRUPTED,
|
|
8857
9033
|
async run(args) {
|
|
8858
9034
|
if (args.url === void 0 && !stdinIsTty()) throw new ConfigError(NEEDS_URL_ERROR);
|
|
8859
9035
|
const name = await defaultInstanceName();
|
|
@@ -8885,6 +9061,7 @@ const dbList = defineRdeCommand({
|
|
|
8885
9061
|
description: "Print the list as JSON",
|
|
8886
9062
|
default: false
|
|
8887
9063
|
} },
|
|
9064
|
+
interrupted: INTERRUPTED,
|
|
8888
9065
|
async run(args) {
|
|
8889
9066
|
const name = await defaultInstanceName();
|
|
8890
9067
|
const state = await requireInstanceState(name);
|
|
@@ -9508,6 +9685,11 @@ async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
|
|
|
9508
9685
|
function isNotFound$1(error) {
|
|
9509
9686
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
|
|
9510
9687
|
}
|
|
9688
|
+
function coverageDetail(coverage) {
|
|
9689
|
+
const valid = coverage.included.length === 0 ? "valid" : `valid, with ${listOf(coverage.included, "conjunction")}`;
|
|
9690
|
+
if (coverage.missing.length === 0) return valid;
|
|
9691
|
+
return `${valid}; without ${listOf(coverage.missing, "disjunction")}`;
|
|
9692
|
+
}
|
|
9511
9693
|
async function licenseRow(client) {
|
|
9512
9694
|
const label = "license";
|
|
9513
9695
|
try {
|
|
@@ -9517,9 +9699,9 @@ async function licenseRow(client) {
|
|
|
9517
9699
|
hint: "`rde init --from license` with a valid token",
|
|
9518
9700
|
step: "license"
|
|
9519
9701
|
});
|
|
9520
|
-
return ok("license", label,
|
|
9702
|
+
return ok("license", label, coverageDetail(licensedFeatureCoverage(status.features)));
|
|
9521
9703
|
} catch (error) {
|
|
9522
|
-
if (isNotFound$1(error)) return skipped("license", label,
|
|
9704
|
+
if (isNotFound$1(error)) return skipped("license", label, `not activated, so ${listOf(LICENSED_FEATURE_NAMES, "conjunction")} are unavailable; \`rde init --from license\` adds one`);
|
|
9523
9705
|
throw error;
|
|
9524
9706
|
}
|
|
9525
9707
|
}
|
|
@@ -9537,46 +9719,6 @@ async function databasesRow(state, client) {
|
|
|
9537
9719
|
const names = state.databases.map((record) => record.name).join(", ");
|
|
9538
9720
|
return ok("databases", label, `${state.databases.length} connected: ${names}`);
|
|
9539
9721
|
}
|
|
9540
|
-
const SAMPLE_DATABASE_STEP = "sample-database";
|
|
9541
|
-
const SAMPLE_LABEL = "sample database";
|
|
9542
|
-
async function sampleDatabaseRow(record, deps, dockerOk, access) {
|
|
9543
|
-
const id = "sample-database";
|
|
9544
|
-
if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
|
|
9545
|
-
const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
|
|
9546
|
-
if (!status.installed) return failed(id, SAMPLE_LABEL, {
|
|
9547
|
-
detail: `${record.containerName} does not exist`,
|
|
9548
|
-
hint: "`rde init` recreates it",
|
|
9549
|
-
step: SAMPLE_DATABASE_STEP
|
|
9550
|
-
});
|
|
9551
|
-
if (!status.running) return failed(id, SAMPLE_LABEL, {
|
|
9552
|
-
detail: `${record.containerName} is stopped`,
|
|
9553
|
-
hint: "`rde start`",
|
|
9554
|
-
step: "start-metabase"
|
|
9555
|
-
});
|
|
9556
|
-
if (await deps.runtime.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
|
|
9557
|
-
detail: `${record.containerName} runs but port ${record.port} is not answering`,
|
|
9558
|
-
hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
|
|
9559
|
-
step: null
|
|
9560
|
-
});
|
|
9561
|
-
const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
|
|
9562
|
-
if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
|
|
9563
|
-
detail: `${where}; not connected to Metabase yet`,
|
|
9564
|
-
hint: "`rde init` connects it",
|
|
9565
|
-
step: SAMPLE_DATABASE_STEP
|
|
9566
|
-
});
|
|
9567
|
-
if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
|
|
9568
|
-
try {
|
|
9569
|
-
const database = await access.client.database.get(record.databaseId);
|
|
9570
|
-
return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} ${syncLabel(database.initial_sync_status)}`);
|
|
9571
|
-
} catch (error) {
|
|
9572
|
-
if (isNotFound$1(error)) return failed(id, SAMPLE_LABEL, {
|
|
9573
|
-
detail: `${where}; database ${record.databaseId} is gone from Metabase`,
|
|
9574
|
-
hint: "`rde init` connects it again",
|
|
9575
|
-
step: SAMPLE_DATABASE_STEP
|
|
9576
|
-
});
|
|
9577
|
-
throw error;
|
|
9578
|
-
}
|
|
9579
|
-
}
|
|
9580
9722
|
function isMbFailure$1(error) {
|
|
9581
9723
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
9582
9724
|
}
|
|
@@ -9730,7 +9872,6 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
|
|
|
9730
9872
|
skipped("mb-profile", "mb profile", NO_INSTANCE_REASON),
|
|
9731
9873
|
skipped("license", "license", NO_INSTANCE_REASON),
|
|
9732
9874
|
skipped("databases", "databases", NO_INSTANCE_REASON),
|
|
9733
|
-
skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE_REASON),
|
|
9734
9875
|
skipped("git-sync", "remote sync", NO_INSTANCE_REASON)
|
|
9735
9876
|
];
|
|
9736
9877
|
const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
|
|
@@ -9749,7 +9890,6 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
|
|
|
9749
9890
|
const access = apiKey.access;
|
|
9750
9891
|
if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
|
|
9751
9892
|
else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
|
|
9752
|
-
rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
|
|
9753
9893
|
rows.push(await gitSyncRow(state, mbInstanceOf(state, secrets), deps));
|
|
9754
9894
|
return rows;
|
|
9755
9895
|
}
|
|
@@ -9759,7 +9899,7 @@ async function collectDoctor(instance, state, secrets, deps) {
|
|
|
9759
9899
|
const mb = await mbFinding(deps);
|
|
9760
9900
|
const profiles = sharedProfileList(deps);
|
|
9761
9901
|
const checks = [...machine, mb.row];
|
|
9762
|
-
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("
|
|
9902
|
+
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));
|
|
9763
9903
|
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
9764
9904
|
const rdeProfile = state === null ? null : state.mbProfile;
|
|
9765
9905
|
checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
|
|
@@ -9866,6 +10006,7 @@ const doctor = defineRdeCommand({
|
|
|
9866
10006
|
default: false
|
|
9867
10007
|
}
|
|
9868
10008
|
},
|
|
10009
|
+
interrupted: INTERRUPTED,
|
|
9869
10010
|
async run(args) {
|
|
9870
10011
|
const name = await instanceNameOrDefault();
|
|
9871
10012
|
const state = await readInstanceState(name);
|
|
@@ -9924,13 +10065,6 @@ function isLocation(value) {
|
|
|
9924
10065
|
function isRemote(value) {
|
|
9925
10066
|
return value === "local-bare" || value === "hosted";
|
|
9926
10067
|
}
|
|
9927
|
-
function applyDatabaseRecord(draft, record) {
|
|
9928
|
-
const value = stringOf(record.answer);
|
|
9929
|
-
const skippedConnection = record.key === DATABASE_KEYS.connection && value?.trim() === "";
|
|
9930
|
-
const finished = parseDatabaseKey(record.key)?.name === "engine" && value === "finish";
|
|
9931
|
-
const pickedNoStub = record.key === DATABASE_KEYS.stubs && record.answer.kind === "multiselect" && record.answer.values.length === 0;
|
|
9932
|
-
if (skippedConnection || finished || pickedNoStub) draft.databasesFinished = true;
|
|
9933
|
-
}
|
|
9934
10068
|
function applyAdminRecord(draft, key, value) {
|
|
9935
10069
|
if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
|
|
9936
10070
|
else if (key === ADMIN_USER_KEYS.password) admin(draft).password = envReference(ENV_ADMIN_PASSWORD);
|
|
@@ -9954,14 +10088,14 @@ function agentIds(values) {
|
|
|
9954
10088
|
function applyRecord(draft, record) {
|
|
9955
10089
|
const { key } = record;
|
|
9956
10090
|
const value = stringOf(record.answer);
|
|
9957
|
-
if (key.startsWith("
|
|
9958
|
-
else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
10091
|
+
if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
9959
10092
|
else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
|
|
9960
10093
|
else if (key === AGENTS_SKILLS_KEYS.agents && record.answer.kind === "multiselect") draft.answers["agents-skills"] = { agents: agentIds(record.answer.values) };
|
|
9961
10094
|
else if (key === API_KEY_KEYS.profile && value !== null) {
|
|
9962
10095
|
const profile = MbProfileChoice.safeParse(value);
|
|
9963
10096
|
if (profile.success) draft.answers["api-key"] = { profile: profile.data };
|
|
9964
|
-
} else if (key ===
|
|
10097
|
+
} else if (key === SYNC_SCOPE_KEYS.export && record.answer.kind === "confirm") draft.answers["sync-scope"] = { export: record.answer.value };
|
|
10098
|
+
else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
|
|
9965
10099
|
}
|
|
9966
10100
|
function writableUri(uri, index) {
|
|
9967
10101
|
const trimmed = uri.trim();
|
|
@@ -9981,13 +10115,9 @@ function databaseEntry(added, index) {
|
|
|
9981
10115
|
};
|
|
9982
10116
|
}
|
|
9983
10117
|
function answersFromRecords(records, overrides, added) {
|
|
9984
|
-
const draft = {
|
|
9985
|
-
answers: {},
|
|
9986
|
-
databasesFinished: false
|
|
9987
|
-
};
|
|
10118
|
+
const draft = { answers: {} };
|
|
9988
10119
|
for (const record of records) applyRecord(draft, record);
|
|
9989
|
-
|
|
9990
|
-
if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
|
|
10120
|
+
if (added.length > 0) draft.answers.databases = added.map(databaseEntry);
|
|
9991
10121
|
return {
|
|
9992
10122
|
...draft.answers,
|
|
9993
10123
|
...overrides.runtime !== void 0 && { runtime: overrides.runtime },
|
|
@@ -10113,7 +10243,7 @@ const init = defineRdeCommand({
|
|
|
10113
10243
|
},
|
|
10114
10244
|
runtime: {
|
|
10115
10245
|
type: "string",
|
|
10116
|
-
description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available
|
|
10246
|
+
description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available`
|
|
10117
10247
|
},
|
|
10118
10248
|
image: {
|
|
10119
10249
|
type: "string",
|
|
@@ -10158,12 +10288,7 @@ const init = defineRdeCommand({
|
|
|
10158
10288
|
},
|
|
10159
10289
|
database: {
|
|
10160
10290
|
type: "string",
|
|
10161
|
-
description: "Database to connect, a connection string or <engine>=<connection string> when it fits several engines; repeatable"
|
|
10162
|
-
},
|
|
10163
|
-
"skip-databases": {
|
|
10164
|
-
type: "boolean",
|
|
10165
|
-
description: "Connect none of your own databases (the sample database is still connected)",
|
|
10166
|
-
default: false
|
|
10291
|
+
description: "Database to connect, a connection string or <engine>=<connection string> when it fits several engines; repeatable. init needs one, and asks for it only at a terminal"
|
|
10167
10292
|
},
|
|
10168
10293
|
"git-dir": {
|
|
10169
10294
|
type: "string",
|
|
@@ -10201,10 +10326,11 @@ const init = defineRdeCommand({
|
|
|
10201
10326
|
},
|
|
10202
10327
|
yes: {
|
|
10203
10328
|
type: "boolean",
|
|
10204
|
-
description: "Answer yes to every confirmation and take the default of every setting;
|
|
10329
|
+
description: "Answer yes to every confirmation and take the default of every setting; the database init needs then comes from --database or the answers file",
|
|
10205
10330
|
default: false
|
|
10206
10331
|
}
|
|
10207
10332
|
},
|
|
10333
|
+
interrupted: "Interrupted. Run `rde init` again to resume.",
|
|
10208
10334
|
async run(args, rawArgs) {
|
|
10209
10335
|
const options = stepSelection(args.from, args.only);
|
|
10210
10336
|
const runOptions = {
|
|
@@ -10221,7 +10347,6 @@ const init = defineRdeCommand({
|
|
|
10221
10347
|
licenseToken: args["license-token"],
|
|
10222
10348
|
skipLicense: args["skip-license"],
|
|
10223
10349
|
database: repeatedValues(rawArgs, "--database"),
|
|
10224
|
-
skipDatabases: args["skip-databases"],
|
|
10225
10350
|
gitDir: args["git-dir"],
|
|
10226
10351
|
gitRemote: args["git-remote"],
|
|
10227
10352
|
gitUrl: args["git-url"],
|
|
@@ -10268,6 +10393,7 @@ const logs = defineRdeCommand({
|
|
|
10268
10393
|
description: "Keep streaming new lines",
|
|
10269
10394
|
default: false
|
|
10270
10395
|
} },
|
|
10396
|
+
interrupted: null,
|
|
10271
10397
|
async run(args) {
|
|
10272
10398
|
await (await loadInstance(await defaultInstanceName(), interruptSignal)).runtime.logs({
|
|
10273
10399
|
follow: args.follow,
|
|
@@ -10282,6 +10408,7 @@ const open = defineRdeCommand({
|
|
|
10282
10408
|
name: "open",
|
|
10283
10409
|
description: "Open the Metabase instance in the browser"
|
|
10284
10410
|
},
|
|
10411
|
+
interrupted: INTERRUPTED,
|
|
10285
10412
|
async run() {
|
|
10286
10413
|
const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
|
|
10287
10414
|
printLines([await openInstance(instance.url) ? `Opened ${instance.url}` : `Open ${instance.url} in your browser`]);
|
|
@@ -10361,6 +10488,7 @@ const start = defineRdeCommand({
|
|
|
10361
10488
|
type: "string",
|
|
10362
10489
|
description: "Move the instance to this host port first; the mb profile follows, the data stays"
|
|
10363
10490
|
} },
|
|
10491
|
+
interrupted: INTERRUPTED,
|
|
10364
10492
|
async run(args) {
|
|
10365
10493
|
const port = args.port === void 0 ? null : requirePortFlag(args.port);
|
|
10366
10494
|
const name = await defaultInstanceName();
|
|
@@ -10422,26 +10550,13 @@ async function databasesOf(ctx, deps, health) {
|
|
|
10422
10550
|
throw error;
|
|
10423
10551
|
}
|
|
10424
10552
|
}
|
|
10425
|
-
async function sampleDatabaseOf(ctx, deps, databases) {
|
|
10426
|
-
const record = ctx.state.sampleDatabase;
|
|
10427
|
-
if (record === null) return null;
|
|
10428
|
-
const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
|
|
10429
|
-
const listed = databases?.find((database) => database.id === record.databaseId) ?? null;
|
|
10430
|
-
return {
|
|
10431
|
-
container: status.running ? "running" : status.installed ? "stopped" : "missing",
|
|
10432
|
-
port: record.port,
|
|
10433
|
-
url: sampleDatabaseUrl(record.port),
|
|
10434
|
-
databaseId: record.databaseId,
|
|
10435
|
-
initialSyncStatus: listed === null ? null : listed.initialSyncStatus
|
|
10436
|
-
};
|
|
10437
|
-
}
|
|
10438
10553
|
function taskLabel(status) {
|
|
10439
10554
|
const task = status.current_task;
|
|
10440
10555
|
return task === null ? null : `${task.sync_task_type} ${task.status}`;
|
|
10441
10556
|
}
|
|
10442
10557
|
function remoteContentOf(changes) {
|
|
10443
10558
|
if (!changes.has_changes) return "imported";
|
|
10444
|
-
return changes
|
|
10559
|
+
return remoteMoved(changes) ? "changed" : "never-imported";
|
|
10445
10560
|
}
|
|
10446
10561
|
function isMbFailure(error) {
|
|
10447
10562
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
@@ -10459,7 +10574,8 @@ async function gitSyncOf(ctx, deps) {
|
|
|
10459
10574
|
branch: status.branch,
|
|
10460
10575
|
dirty: status.is_dirty,
|
|
10461
10576
|
remote: remoteContentOf(changes),
|
|
10462
|
-
task: taskLabel(status)
|
|
10577
|
+
task: taskLabel(status),
|
|
10578
|
+
syncedCollections: status.synced_collections.map((collection) => collection.name)
|
|
10463
10579
|
};
|
|
10464
10580
|
} catch (error) {
|
|
10465
10581
|
if (isMbFailure(error)) return { error: error.message };
|
|
@@ -10489,7 +10605,6 @@ async function collectStatus(ctx, deps) {
|
|
|
10489
10605
|
steps,
|
|
10490
10606
|
setup: setupOf(ctx, steps),
|
|
10491
10607
|
databases,
|
|
10492
|
-
sampleDatabase: await sampleDatabaseOf(ctx, deps, databases),
|
|
10493
10608
|
gitSync: await gitSyncOf(ctx, deps),
|
|
10494
10609
|
contentRepository: ctx.state.gitRemote === null ? null : ctx.state.gitRemote.workdir
|
|
10495
10610
|
};
|
|
@@ -10520,7 +10635,8 @@ function gitSyncLine(report) {
|
|
|
10520
10635
|
if (isGitSyncFailure(report.gitSync)) return `Remote sync: ${report.gitSync.error}`;
|
|
10521
10636
|
const branch = report.gitSync.branch === null ? "no branch" : `branch ${report.gitSync.branch}`;
|
|
10522
10637
|
const task = report.gitSync.task === null ? "no task running" : `task ${report.gitSync.task}`;
|
|
10523
|
-
|
|
10638
|
+
const scope = report.gitSync.syncedCollections.length === 0 ? "nothing synced; `rde init --only sync-scope`" : `synced: ${report.gitSync.syncedCollections.join(", ")}`;
|
|
10639
|
+
return `Remote sync: ${branch}, ${syncStateLabel(report.mbProfile, report.gitSync)}, ${task}, ${scope}`;
|
|
10524
10640
|
}
|
|
10525
10641
|
const REMOTE_CONTENT_LABEL = {
|
|
10526
10642
|
imported: null,
|
|
@@ -10533,22 +10649,6 @@ function syncStateLabel(profile, gitSync) {
|
|
|
10533
10649
|
if (gitSync.dirty) return `unsynced changes, ${remote}`;
|
|
10534
10650
|
return `${remote}; \`${mbCommand(profile)} git-sync import\``;
|
|
10535
10651
|
}
|
|
10536
|
-
const CONTAINER_STATE_LABEL = {
|
|
10537
|
-
running: "container running",
|
|
10538
|
-
stopped: "container stopped; `rde start`",
|
|
10539
|
-
missing: "container missing; `rde init` recreates it"
|
|
10540
|
-
};
|
|
10541
|
-
function sampleConnectionLabel(report, sample) {
|
|
10542
|
-
if (sample.databaseId === null) return "not connected to Metabase yet; `rde init`";
|
|
10543
|
-
if (report.databases === null) return "sync unknown while Metabase is not running";
|
|
10544
|
-
if (sample.initialSyncStatus === null) return `database ${sample.databaseId} is gone from Metabase; \`rde init\` connects it again`;
|
|
10545
|
-
return syncLabel(sample.initialSyncStatus);
|
|
10546
|
-
}
|
|
10547
|
-
function sampleDatabaseLine(report) {
|
|
10548
|
-
const sample = report.sampleDatabase;
|
|
10549
|
-
if (sample === null) return "Sample database: not connected; `rde init` adds one";
|
|
10550
|
-
return `Sample database: ${sample.url}, ${CONTAINER_STATE_LABEL[sample.container]}, ${sampleConnectionLabel(report, sample)}`;
|
|
10551
|
-
}
|
|
10552
10652
|
function databaseLines(report) {
|
|
10553
10653
|
if (report.databases === null) return ["Databases: unavailable while Metabase is not running"];
|
|
10554
10654
|
return ["Databases:", ...databaseListingLines(report.databases).map((line) => ` ${line}`)];
|
|
@@ -10565,7 +10665,6 @@ function statusLines(report) {
|
|
|
10565
10665
|
"Steps:",
|
|
10566
10666
|
...checklistLines(report.steps).map((line) => ` ${line}`),
|
|
10567
10667
|
...databaseLines(report),
|
|
10568
|
-
sampleDatabaseLine(report),
|
|
10569
10668
|
gitSyncLine(report),
|
|
10570
10669
|
...report.contentRepository === null ? [] : [`Content repository: ${report.contentRepository}`]
|
|
10571
10670
|
];
|
|
@@ -10582,6 +10681,7 @@ const status = defineRdeCommand({
|
|
|
10582
10681
|
description: "Print the report as JSON",
|
|
10583
10682
|
default: false
|
|
10584
10683
|
} },
|
|
10684
|
+
interrupted: INTERRUPTED,
|
|
10585
10685
|
async run(args) {
|
|
10586
10686
|
const name = await defaultInstanceName();
|
|
10587
10687
|
await requireInstanceState(name);
|
|
@@ -10598,18 +10698,17 @@ const status = defineRdeCommand({
|
|
|
10598
10698
|
const stop = defineRdeCommand({
|
|
10599
10699
|
meta: {
|
|
10600
10700
|
name: "stop",
|
|
10601
|
-
description: "Stop the Metabase instance
|
|
10701
|
+
description: "Stop the Metabase instance"
|
|
10602
10702
|
},
|
|
10703
|
+
interrupted: INTERRUPTED,
|
|
10603
10704
|
async run() {
|
|
10604
|
-
const
|
|
10705
|
+
const name = await defaultInstanceName();
|
|
10706
|
+
const instance = await loadInstance(name, interruptSignal);
|
|
10605
10707
|
const before = await instance.runtime.status();
|
|
10606
10708
|
await instance.runtime.stop();
|
|
10607
10709
|
const lines = [before.running ? "Metabase stopped" : "Metabase is not running"];
|
|
10608
|
-
|
|
10609
|
-
|
|
10610
|
-
await instance.sampleDatabase.stop();
|
|
10611
|
-
lines.push(sidecar.running ? "Sample database stopped" : "Sample database is not running");
|
|
10612
|
-
}
|
|
10710
|
+
const removed = await removeLeftovers(name, interruptSignal);
|
|
10711
|
+
if (removed !== null) lines.push(removed);
|
|
10613
10712
|
printLines(lines);
|
|
10614
10713
|
}
|
|
10615
10714
|
});
|
|
@@ -10620,11 +10719,13 @@ function hostUninstallDeps(signal, childOutput) {
|
|
|
10620
10719
|
return {
|
|
10621
10720
|
docker: dockerTool(runner),
|
|
10622
10721
|
daemon: daemonTool(runner),
|
|
10722
|
+
git: gitTool(runner),
|
|
10623
10723
|
runner,
|
|
10624
10724
|
childOutput
|
|
10625
10725
|
};
|
|
10626
10726
|
}
|
|
10627
10727
|
const NOT_TOUCHED_LINE = "The mb CLI, Docker, and Java themselves";
|
|
10728
|
+
const UNPULLED_COMMITS_SHOWN = 10;
|
|
10628
10729
|
const ANY_JAR = "-jar";
|
|
10629
10730
|
function isFileSystemError(error) {
|
|
10630
10731
|
return error instanceof Error && "code" in error;
|
|
@@ -10830,6 +10931,71 @@ async function findSkill(runner, childOutput) {
|
|
|
10830
10931
|
function instanceDirLabel(path, state) {
|
|
10831
10932
|
return `Instance directory ${path} (${state?.gitRemote?.kind === "local-bare" ? "Metabase data and the remote-sync repository content.git" : "Metabase data"})`;
|
|
10832
10933
|
}
|
|
10934
|
+
async function findUnpulled(remote, git) {
|
|
10935
|
+
if (!await pathExists(remote.barePath)) return null;
|
|
10936
|
+
try {
|
|
10937
|
+
if (!await git.isBareRepo(remote.barePath)) return null;
|
|
10938
|
+
const commits = await git.bareBranchCommits(remote.barePath, remote.branch);
|
|
10939
|
+
if (commits === null || commits.length === 0) return null;
|
|
10940
|
+
if (!await git.isRepo(remote.workdir)) return {
|
|
10941
|
+
kind: "orphaned",
|
|
10942
|
+
remote,
|
|
10943
|
+
commits
|
|
10944
|
+
};
|
|
10945
|
+
const reachable = await git.reachableCommits(remote.workdir);
|
|
10946
|
+
const missing = commits.filter((commit) => !reachable.has(commit.sha));
|
|
10947
|
+
return missing.length === 0 ? null : {
|
|
10948
|
+
kind: "behind",
|
|
10949
|
+
remote,
|
|
10950
|
+
commits: missing
|
|
10951
|
+
};
|
|
10952
|
+
} catch (error) {
|
|
10953
|
+
if (isToolFailure(error)) return {
|
|
10954
|
+
kind: "unchecked",
|
|
10955
|
+
remote,
|
|
10956
|
+
reason: error.message
|
|
10957
|
+
};
|
|
10958
|
+
throw error;
|
|
10959
|
+
}
|
|
10960
|
+
}
|
|
10961
|
+
function commitCount(commits) {
|
|
10962
|
+
return commits.length === 1 ? "1 commit" : `${commits.length} commits`;
|
|
10963
|
+
}
|
|
10964
|
+
function unpulledSummary(unpulled) {
|
|
10965
|
+
const { barePath, workdir, branch } = unpulled.remote;
|
|
10966
|
+
switch (unpulled.kind) {
|
|
10967
|
+
case "behind": return `${barePath} has ${commitCount(unpulled.commits)} on ${branch} that ${workdir} lacks; uninstalling deletes them`;
|
|
10968
|
+
case "orphaned": return `${barePath} has ${commitCount(unpulled.commits)} on ${branch}, and ${workdir} is gone or no longer a git repository; uninstalling deletes them`;
|
|
10969
|
+
case "unchecked": return `${barePath} cannot be compared with ${workdir} (${unpulled.reason}); uninstalling deletes it with any commits ${workdir} lacks`;
|
|
10970
|
+
}
|
|
10971
|
+
}
|
|
10972
|
+
function unpulledRemedy(unpulled) {
|
|
10973
|
+
const { barePath, workdir, branch } = unpulled.remote;
|
|
10974
|
+
switch (unpulled.kind) {
|
|
10975
|
+
case "behind": return `\`git -C ${workdir} pull\` brings them in first`;
|
|
10976
|
+
case "orphaned": return `\`git clone --branch ${branch} ${barePath} <directory>\` copies them first`;
|
|
10977
|
+
case "unchecked": return null;
|
|
10978
|
+
}
|
|
10979
|
+
}
|
|
10980
|
+
function commitLines(unpulled) {
|
|
10981
|
+
if (unpulled.kind === "unchecked") return [];
|
|
10982
|
+
const shown = unpulled.commits.slice(0, UNPULLED_COMMITS_SHOWN).map((commit) => ` ${commit.shortSha} ${commit.subject}`);
|
|
10983
|
+
const hidden = unpulled.commits.length - shown.length;
|
|
10984
|
+
return hidden > 0 ? [...shown, ` and ${hidden} more`] : shown;
|
|
10985
|
+
}
|
|
10986
|
+
function unpulledWarning(unpulled) {
|
|
10987
|
+
const remedy = unpulledRemedy(unpulled);
|
|
10988
|
+
return [
|
|
10989
|
+
unpulledSummary(unpulled),
|
|
10990
|
+
...commitLines(unpulled),
|
|
10991
|
+
...remedy === null ? [] : [remedy]
|
|
10992
|
+
].join("\n");
|
|
10993
|
+
}
|
|
10994
|
+
function unpulledRefusal(unpulled) {
|
|
10995
|
+
const remedy = unpulledRemedy(unpulled);
|
|
10996
|
+
const force = "pass --force to uninstall anyway";
|
|
10997
|
+
return remedy === null ? `${unpulledSummary(unpulled)}; ${force}` : `${unpulledSummary(unpulled)}; ${remedy}, or ${force}`;
|
|
10998
|
+
}
|
|
10833
10999
|
async function findConfigEntry(instance) {
|
|
10834
11000
|
const path = globalConfigPath();
|
|
10835
11001
|
try {
|
|
@@ -10872,14 +11038,13 @@ async function planUninstall(options, deps) {
|
|
|
10872
11038
|
const container = await findContainer(runtime?.containerName ?? containerName(options.instance, home), deps.docker, runtime?.image ?? null, home, { graceMs: METABASE_STOP_GRACE_MS });
|
|
10873
11039
|
if (container.removal !== null) removals.push(container.removal);
|
|
10874
11040
|
if (container.problem !== null) problems.push(container.problem);
|
|
10875
|
-
const
|
|
10876
|
-
|
|
10877
|
-
if (
|
|
10878
|
-
if (sidecar.problem !== null) problems.push(sidecar.problem);
|
|
11041
|
+
const sampleDatabase = await findContainer(sampleDatabaseContainerName(options.instance, home), deps.docker, null, home, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
|
|
11042
|
+
if (sampleDatabase.removal !== null) removals.push(sampleDatabase.removal);
|
|
11043
|
+
if (sampleDatabase.problem !== null) problems.push(sampleDatabase.problem);
|
|
10879
11044
|
const jarSource = state?.runtime?.kind === "jar" ? state.runtime.source : null;
|
|
10880
11045
|
const jarProcess = await findJarProcess(options.instance, jarSource, deps.daemon);
|
|
10881
11046
|
if (jarProcess !== null) removals.push(jarProcess);
|
|
10882
|
-
for (const image of [container.image,
|
|
11047
|
+
for (const image of [container.image, sampleDatabase.image]) {
|
|
10883
11048
|
if (image === null) continue;
|
|
10884
11049
|
const finding = await findImage(image, deps.docker, options.removeImage);
|
|
10885
11050
|
if (finding.removal !== null) removals.push(finding.removal);
|
|
@@ -10899,6 +11064,8 @@ async function planUninstall(options, deps) {
|
|
|
10899
11064
|
}
|
|
10900
11065
|
const dir = instanceDir(options.instance);
|
|
10901
11066
|
const dirPresent = await pathExists(dir);
|
|
11067
|
+
const bareRemote = state?.gitRemote?.kind === "local-bare" ? state.gitRemote : null;
|
|
11068
|
+
const unpulled = options.keepData || !dirPresent || bareRemote === null ? null : await findUnpulled(bareRemote, deps.git);
|
|
10902
11069
|
if (!options.keepData) {
|
|
10903
11070
|
const secrets = instanceSecretsPath(options.instance);
|
|
10904
11071
|
if (await pathExists(secrets)) removals.push({
|
|
@@ -10919,7 +11086,8 @@ async function planUninstall(options, deps) {
|
|
|
10919
11086
|
return {
|
|
10920
11087
|
removals,
|
|
10921
11088
|
kept,
|
|
10922
|
-
problems
|
|
11089
|
+
problems,
|
|
11090
|
+
unpulled
|
|
10923
11091
|
};
|
|
10924
11092
|
}
|
|
10925
11093
|
function planLines$1(plan) {
|
|
@@ -10974,6 +11142,10 @@ async function uninstall(options, deps, io) {
|
|
|
10974
11142
|
};
|
|
10975
11143
|
}
|
|
10976
11144
|
io.ui.note(planLines$1(plan).join("\n"), `rde uninstall ${options.instance}`);
|
|
11145
|
+
if (plan.unpulled !== null) {
|
|
11146
|
+
io.ui.warn(unpulledWarning(plan.unpulled));
|
|
11147
|
+
if ((options.yes || !options.terminal) && !options.force) throw new ConfigError(unpulledRefusal(plan.unpulled));
|
|
11148
|
+
}
|
|
10977
11149
|
if (!options.yes) {
|
|
10978
11150
|
if (!await io.prompts.confirm({
|
|
10979
11151
|
message: "Remove everything listed?",
|
|
@@ -11020,8 +11192,14 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11020
11192
|
type: "boolean",
|
|
11021
11193
|
description: "Do not ask for confirmation",
|
|
11022
11194
|
default: false
|
|
11195
|
+
},
|
|
11196
|
+
force: {
|
|
11197
|
+
type: "boolean",
|
|
11198
|
+
description: "With --yes or without a terminal, also delete commits your working directory has not pulled",
|
|
11199
|
+
default: false
|
|
11023
11200
|
}
|
|
11024
11201
|
},
|
|
11202
|
+
interrupted: INTERRUPTED,
|
|
11025
11203
|
async run(args) {
|
|
11026
11204
|
const instance = args.instance ?? await instanceNameOrDefault();
|
|
11027
11205
|
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})`);
|
|
@@ -11031,7 +11209,9 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11031
11209
|
keepData: args["keep-data"],
|
|
11032
11210
|
removeSkill: args["remove-skill"],
|
|
11033
11211
|
removeImage: args["remove-image"],
|
|
11034
|
-
yes: args.yes
|
|
11212
|
+
yes: args.yes,
|
|
11213
|
+
force: args.force,
|
|
11214
|
+
terminal: stdinIsTty()
|
|
11035
11215
|
}, hostUninstallDeps(interruptSignal, ui.childOutput), {
|
|
11036
11216
|
prompts: clackPrompts({ confirmsWithYes: true }),
|
|
11037
11217
|
ui
|
|
@@ -11265,8 +11445,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11265
11445
|
const stopping = ctx.ui.spinner("Stopping Metabase");
|
|
11266
11446
|
await runtime.stop();
|
|
11267
11447
|
stopping.stop("Metabase stopped");
|
|
11448
|
+
options.onPhase("backing-up", backup);
|
|
11268
11449
|
await backUp(ctx, resolved.runtime.dataDir, backup);
|
|
11450
|
+
options.onPhase("switching", backup);
|
|
11269
11451
|
await switchTo(ctx, resolved, target);
|
|
11452
|
+
options.onPhase("starting", backup);
|
|
11270
11453
|
const to = await startUpgraded(ctx, resolved, client, configured, backup);
|
|
11271
11454
|
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
11272
11455
|
return {
|
|
@@ -11278,6 +11461,14 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11278
11461
|
}
|
|
11279
11462
|
//#endregion
|
|
11280
11463
|
//#region src/commands/upgrade.ts
|
|
11464
|
+
const INTERRUPTED_IN = {
|
|
11465
|
+
"backing-up": (backup) => `Interrupted before the backup finished; the instance is stopped on the version it had. The next \`rde upgrade\` starts it and takes a new backup; the partial copy at ${backup} can go.`,
|
|
11466
|
+
switching: (backup) => `Interrupted after the backup to ${backup} finished, while installing the new version. Run \`rde init\` to reinstall the instance; the next \`rde upgrade\` then finds it upgraded, or upgrades it if \`rde init\` put back the version it had.`,
|
|
11467
|
+
starting: (backup) => `Interrupted while Metabase starts on the new version, after the backup to ${backup} finished. The next \`rde upgrade\` waits for it to come up and finds it upgraded.`
|
|
11468
|
+
};
|
|
11469
|
+
function interruptedUpgrade(phase, backup) {
|
|
11470
|
+
return INTERRUPTED_IN[phase](backup);
|
|
11471
|
+
}
|
|
11281
11472
|
const upgradeCommand = defineRdeCommand({
|
|
11282
11473
|
meta: {
|
|
11283
11474
|
name: "upgrade",
|
|
@@ -11294,14 +11485,21 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11294
11485
|
default: false
|
|
11295
11486
|
}
|
|
11296
11487
|
},
|
|
11297
|
-
|
|
11488
|
+
interrupted: "Interrupted before the backup; the instance is on the version it had. The next `rde upgrade` starts it if it is stopped and begins again.",
|
|
11489
|
+
async run(args, _rawArgs, interruption) {
|
|
11298
11490
|
if (!args.yes && !stdinIsTty()) throw new ConfigError("`rde upgrade` asks for confirmation; pass --yes to upgrade without a terminal");
|
|
11299
11491
|
const name = await defaultInstanceName();
|
|
11300
11492
|
await requireInstanceState(name);
|
|
11301
|
-
|
|
11493
|
+
const ctx = await createWizardContext(name, { signal: interruptSignal });
|
|
11494
|
+
await upgrade(ctx, {
|
|
11302
11495
|
to: args.to ?? null,
|
|
11303
|
-
yes: args.yes
|
|
11496
|
+
yes: args.yes,
|
|
11497
|
+
onPhase: (phase, backup) => {
|
|
11498
|
+
interruption.message = interruptedUpgrade(phase, backup);
|
|
11499
|
+
}
|
|
11304
11500
|
});
|
|
11501
|
+
const removed = await removeLeftovers(name, interruptSignal);
|
|
11502
|
+
if (removed !== null) ctx.ui.info(removed);
|
|
11305
11503
|
}
|
|
11306
11504
|
});
|
|
11307
11505
|
//#endregion
|
|
@@ -11309,7 +11507,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11309
11507
|
const main = defineCommand({
|
|
11310
11508
|
meta: {
|
|
11311
11509
|
name: "rde",
|
|
11312
|
-
version: "0.1.
|
|
11510
|
+
version: "0.1.17",
|
|
11313
11511
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
11314
11512
|
},
|
|
11315
11513
|
subCommands: {
|