@metabase/rde 0.1.15 → 0.1.16
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 +41 -53
- package/dist/cli.mjs +489 -722
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -123,12 +123,10 @@ var DownloadError = class extends Error {
|
|
|
123
123
|
};
|
|
124
124
|
//#endregion
|
|
125
125
|
//#region src/output/error.ts
|
|
126
|
-
const INTERRUPTED_MESSAGE = "Interrupted. Run `rde init` again to resume.";
|
|
127
126
|
const FAILURE_EXIT_CODE = 1;
|
|
128
127
|
const USAGE_EXIT_CODE = 2;
|
|
129
128
|
function exitCodeFor(error) {
|
|
130
129
|
if (error instanceof StepFailedError) return exitCodeFor(error.cause);
|
|
131
|
-
if (error instanceof AbortError) return 130;
|
|
132
130
|
if (error instanceof ConfigError) return USAGE_EXIT_CODE;
|
|
133
131
|
return FAILURE_EXIT_CODE;
|
|
134
132
|
}
|
|
@@ -140,7 +138,6 @@ function stepFailureMessage(step, cause) {
|
|
|
140
138
|
}
|
|
141
139
|
function messageFor$1(error) {
|
|
142
140
|
if (error instanceof StepFailedError) return stepFailureMessage(error.step, messageFor$1(error.cause));
|
|
143
|
-
if (error instanceof AbortError) return INTERRUPTED_MESSAGE;
|
|
144
141
|
if (error instanceof Error) return error.message;
|
|
145
142
|
return String(error);
|
|
146
143
|
}
|
|
@@ -148,10 +145,15 @@ function reportError(error) {
|
|
|
148
145
|
process.stderr.write(messageFor$1(error) + "\n");
|
|
149
146
|
process.exitCode = exitCodeFor(error);
|
|
150
147
|
}
|
|
148
|
+
function reportInterrupted(message) {
|
|
149
|
+
if (message !== null) process.stderr.write(message + "\n");
|
|
150
|
+
process.exitCode = 130;
|
|
151
|
+
}
|
|
151
152
|
//#endregion
|
|
152
153
|
//#region src/commands/define.ts
|
|
153
154
|
const SHORT_PREFIX = "-";
|
|
154
155
|
const END_OF_OPTIONS = "--";
|
|
156
|
+
const INTERRUPTED = "Interrupted.";
|
|
155
157
|
function repeatedValues(rawArgs, flag) {
|
|
156
158
|
const values = [];
|
|
157
159
|
const inline = `${flag}=`;
|
|
@@ -174,9 +176,14 @@ function defineRdeCommand(def) {
|
|
|
174
176
|
meta: def.meta,
|
|
175
177
|
...def.args !== void 0 && { args: def.args },
|
|
176
178
|
async run({ args, rawArgs }) {
|
|
179
|
+
const interruption = { message: def.interrupted };
|
|
177
180
|
try {
|
|
178
|
-
await def.run(args, rawArgs);
|
|
181
|
+
await def.run(args, rawArgs, interruption);
|
|
179
182
|
} catch (error) {
|
|
183
|
+
if (error instanceof AbortError) {
|
|
184
|
+
reportInterrupted(interruption.message);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
180
187
|
reportError(error);
|
|
181
188
|
}
|
|
182
189
|
}
|
|
@@ -255,7 +262,6 @@ const StepId = z.enum([
|
|
|
255
262
|
"admin-user",
|
|
256
263
|
"api-key",
|
|
257
264
|
"transforms",
|
|
258
|
-
"sample-database",
|
|
259
265
|
"license",
|
|
260
266
|
"git-sync",
|
|
261
267
|
"databases",
|
|
@@ -332,12 +338,6 @@ const DatabaseRecord = z.object({
|
|
|
332
338
|
engine: z.string(),
|
|
333
339
|
createdAt: z.string()
|
|
334
340
|
});
|
|
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
341
|
const GitRemoteState = z.discriminatedUnion("kind", [z.object({
|
|
342
342
|
kind: z.literal("local-bare"),
|
|
343
343
|
barePath: z.string(),
|
|
@@ -354,7 +354,7 @@ const StepRecord = z.object({
|
|
|
354
354
|
completedAt: z.string()
|
|
355
355
|
});
|
|
356
356
|
const InstanceState = z.object({
|
|
357
|
-
schemaVersion: z.literal(
|
|
357
|
+
schemaVersion: z.literal(6),
|
|
358
358
|
name: z.string(),
|
|
359
359
|
runtime: RuntimeState.nullable(),
|
|
360
360
|
url: z.string().nullable(),
|
|
@@ -363,7 +363,6 @@ const InstanceState = z.object({
|
|
|
363
363
|
mbProfile: z.string(),
|
|
364
364
|
license: LicenseState,
|
|
365
365
|
databases: z.array(DatabaseRecord),
|
|
366
|
-
sampleDatabase: SampleDatabaseState.nullable(),
|
|
367
366
|
gitRemote: GitRemoteState.nullable(),
|
|
368
367
|
movedFrom: z.string().nullable(),
|
|
369
368
|
steps: z.partialRecord(StepId, StepRecord)
|
|
@@ -395,7 +394,7 @@ function mbCommand(profile) {
|
|
|
395
394
|
}
|
|
396
395
|
function emptyInstanceState(name) {
|
|
397
396
|
return {
|
|
398
|
-
schemaVersion:
|
|
397
|
+
schemaVersion: 6,
|
|
399
398
|
name,
|
|
400
399
|
runtime: null,
|
|
401
400
|
url: null,
|
|
@@ -404,7 +403,6 @@ function emptyInstanceState(name) {
|
|
|
404
403
|
mbProfile: mbProfileFor(name),
|
|
405
404
|
license: { activated: false },
|
|
406
405
|
databases: [],
|
|
407
|
-
sampleDatabase: null,
|
|
408
406
|
gitRemote: null,
|
|
409
407
|
movedFrom: null,
|
|
410
408
|
steps: {}
|
|
@@ -417,6 +415,7 @@ const V1 = 1;
|
|
|
417
415
|
const V2 = 2;
|
|
418
416
|
const V3 = 3;
|
|
419
417
|
const V4 = 4;
|
|
418
|
+
const V5 = 5;
|
|
420
419
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
421
420
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
422
421
|
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
@@ -428,6 +427,7 @@ const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
|
428
427
|
password: z.string()
|
|
429
428
|
})]);
|
|
430
429
|
const StateV4 = z.object({ schemaVersion: z.literal(V4) }).loose();
|
|
430
|
+
const StateV5 = z.object({ schemaVersion: z.literal(V5) }).loose();
|
|
431
431
|
const StateV3 = z.object({
|
|
432
432
|
schemaVersion: z.literal(V3),
|
|
433
433
|
admin: AdminV3.nullable(),
|
|
@@ -443,17 +443,23 @@ function liftV1(raw) {
|
|
|
443
443
|
function liftV2(raw) {
|
|
444
444
|
return {
|
|
445
445
|
...StateV2.parse(raw),
|
|
446
|
-
schemaVersion: V3
|
|
447
|
-
sampleDatabase: null
|
|
446
|
+
schemaVersion: V3
|
|
448
447
|
};
|
|
449
448
|
}
|
|
450
449
|
function liftV4(raw) {
|
|
451
450
|
return {
|
|
452
451
|
...StateV4.parse(raw),
|
|
453
|
-
schemaVersion:
|
|
452
|
+
schemaVersion: V5,
|
|
454
453
|
movedFrom: null
|
|
455
454
|
};
|
|
456
455
|
}
|
|
456
|
+
function liftV5(raw) {
|
|
457
|
+
const { sampleDatabase: _sampleDatabase, ...state } = StateV5.parse(raw);
|
|
458
|
+
return {
|
|
459
|
+
...state,
|
|
460
|
+
schemaVersion: 6
|
|
461
|
+
};
|
|
462
|
+
}
|
|
457
463
|
function adminV4(admin) {
|
|
458
464
|
switch (admin.kind) {
|
|
459
465
|
case "default": return {
|
|
@@ -473,11 +479,11 @@ function liftV3(raw) {
|
|
|
473
479
|
password: admin.password
|
|
474
480
|
} : null;
|
|
475
481
|
return {
|
|
476
|
-
state:
|
|
482
|
+
state: migrateInstanceState(liftV4({
|
|
477
483
|
...rest,
|
|
478
484
|
schemaVersion: V4,
|
|
479
485
|
admin: admin === null ? null : adminV4(admin)
|
|
480
|
-
})),
|
|
486
|
+
})).state,
|
|
481
487
|
movedSecrets: {
|
|
482
488
|
schemaVersion: 1,
|
|
483
489
|
admin: login,
|
|
@@ -491,15 +497,16 @@ function migrateInstanceState(raw) {
|
|
|
491
497
|
case V1: return migrateInstanceState(liftV1(raw));
|
|
492
498
|
case V2: return migrateInstanceState(liftV2(raw));
|
|
493
499
|
case V3: return liftV3(raw);
|
|
494
|
-
case V4: return
|
|
495
|
-
|
|
500
|
+
case V4: return migrateInstanceState(liftV4(raw));
|
|
501
|
+
case V5: return {
|
|
502
|
+
state: InstanceState.parse(liftV5(raw)),
|
|
496
503
|
movedSecrets: null
|
|
497
504
|
};
|
|
498
|
-
case
|
|
505
|
+
case 6: return {
|
|
499
506
|
state: InstanceState.parse(raw),
|
|
500
507
|
movedSecrets: null
|
|
501
508
|
};
|
|
502
|
-
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected
|
|
509
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 6)`);
|
|
503
510
|
}
|
|
504
511
|
}
|
|
505
512
|
//#endregion
|
|
@@ -1479,6 +1486,7 @@ const credentials = defineRdeCommand({
|
|
|
1479
1486
|
default: false
|
|
1480
1487
|
}
|
|
1481
1488
|
},
|
|
1489
|
+
interrupted: INTERRUPTED,
|
|
1482
1490
|
async run(args) {
|
|
1483
1491
|
const name = await defaultInstanceName();
|
|
1484
1492
|
const state = await requireInstanceState(name);
|
|
@@ -2188,10 +2196,10 @@ function createArgs(options) {
|
|
|
2188
2196
|
"--label",
|
|
2189
2197
|
`${IMAGE_LABEL}=${options.image}`,
|
|
2190
2198
|
"-p",
|
|
2191
|
-
`${LOOPBACK_HOST}:${options.port.host
|
|
2199
|
+
`${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`,
|
|
2200
|
+
"-v",
|
|
2201
|
+
`${options.mount.source}:${options.mount.target}`
|
|
2192
2202
|
];
|
|
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
2203
|
for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
|
|
2196
2204
|
if (options.hostUser !== null) {
|
|
2197
2205
|
args.push("-e", `${ENV_HOST_UID}=${options.hostUser.uid}`);
|
|
@@ -2250,17 +2258,6 @@ function dockerTool(runner) {
|
|
|
2250
2258
|
if (NO_SUCH_IMAGE.test(result.stderr)) return null;
|
|
2251
2259
|
throw toolFailure(DOCKER, args, result);
|
|
2252
2260
|
}
|
|
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
2261
|
async function imageRemove(image) {
|
|
2265
2262
|
await docker(["rmi", image]);
|
|
2266
2263
|
}
|
|
@@ -2337,34 +2334,6 @@ function dockerTool(runner) {
|
|
|
2337
2334
|
name
|
|
2338
2335
|
]);
|
|
2339
2336
|
}
|
|
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
2337
|
async function containerLogs(name, options) {
|
|
2369
2338
|
const args = ["logs"];
|
|
2370
2339
|
if (options.follow) args.push("--follow");
|
|
@@ -2377,7 +2346,6 @@ function dockerTool(runner) {
|
|
|
2377
2346
|
imagePull,
|
|
2378
2347
|
imageId,
|
|
2379
2348
|
imageRemove,
|
|
2380
|
-
bridgeGateway,
|
|
2381
2349
|
containerInspect,
|
|
2382
2350
|
containerImageId,
|
|
2383
2351
|
portClaims,
|
|
@@ -2387,8 +2355,6 @@ function dockerTool(runner) {
|
|
|
2387
2355
|
containerRestart,
|
|
2388
2356
|
containerRemove,
|
|
2389
2357
|
containerForceRemove,
|
|
2390
|
-
containerLoopbackPort,
|
|
2391
|
-
containerExecSucceeds,
|
|
2392
2358
|
containerLogs
|
|
2393
2359
|
};
|
|
2394
2360
|
}
|
|
@@ -2566,8 +2532,7 @@ function dockerRuntime(state, deps) {
|
|
|
2566
2532
|
imageId,
|
|
2567
2533
|
port: {
|
|
2568
2534
|
host: state.port,
|
|
2569
|
-
container: CONTAINER_PORT
|
|
2570
|
-
alsoOn: null
|
|
2535
|
+
container: CONTAINER_PORT
|
|
2571
2536
|
},
|
|
2572
2537
|
mount: {
|
|
2573
2538
|
source: deps.dataDir,
|
|
@@ -2881,148 +2846,21 @@ function runtimeFromState(state, deps) {
|
|
|
2881
2846
|
}
|
|
2882
2847
|
}
|
|
2883
2848
|
//#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;
|
|
2849
|
+
//#region src/runtime/leftover-sample-database.ts
|
|
2893
2850
|
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) {
|
|
2851
|
+
const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
|
|
2852
|
+
function sampleDatabaseContainerName(instanceName, home) {
|
|
2905
2853
|
const tag = homeTag(home);
|
|
2906
2854
|
const name = `${CONTAINER_NAME_PREFIX}-${instanceName}`;
|
|
2907
2855
|
return tag === null ? name : `${name}-${tag}`;
|
|
2908
2856
|
}
|
|
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;
|
|
2857
|
+
async function removeLeftoverSampleDatabase(docker, instanceName, home) {
|
|
2858
|
+
if (!(await docker.available()).available) return null;
|
|
2859
|
+
const name = sampleDatabaseContainerName(instanceName, home);
|
|
2860
|
+
const existing = await docker.containerInspect(name);
|
|
2861
|
+
if (!existing.exists || existing.home !== null && existing.home !== home) return null;
|
|
2998
2862
|
await docker.containerForceRemove(name);
|
|
2999
|
-
|
|
3000
|
-
await docker.containerStart(name);
|
|
3001
|
-
const port = await docker.containerLoopbackPort(name, POSTGRES_PORT);
|
|
3002
|
-
await docker.containerForceRemove(name);
|
|
3003
|
-
const pinned = {
|
|
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
|
-
};
|
|
2863
|
+
return name;
|
|
3026
2864
|
}
|
|
3027
2865
|
//#endregion
|
|
3028
2866
|
//#region src/steps/lifecycle.ts
|
|
@@ -3033,10 +2871,14 @@ async function loadInstance(name, signal) {
|
|
|
3033
2871
|
return {
|
|
3034
2872
|
state,
|
|
3035
2873
|
url: configured.url,
|
|
3036
|
-
runtime: runtimeFromState(configured.runtime, deps)
|
|
3037
|
-
sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker, deps.rdeHome)
|
|
2874
|
+
runtime: runtimeFromState(configured.runtime, deps)
|
|
3038
2875
|
};
|
|
3039
2876
|
}
|
|
2877
|
+
async function removeLeftovers(name, signal) {
|
|
2878
|
+
const deps = hostRuntimeDeps(name, signal);
|
|
2879
|
+
const removed = await removeLeftoverSampleDatabase(deps.docker, name, deps.rdeHome);
|
|
2880
|
+
return removed === null ? null : `Removed container ${removed}, the sample database of an older rde`;
|
|
2881
|
+
}
|
|
3040
2882
|
async function isInstanceRunning(state, runtime) {
|
|
3041
2883
|
const configured = configuredRuntime(state);
|
|
3042
2884
|
if (configured === null) return false;
|
|
@@ -3045,44 +2887,29 @@ async function isInstanceRunning(state, runtime) {
|
|
|
3045
2887
|
function openInstance(url) {
|
|
3046
2888
|
return openBrowser(url);
|
|
3047
2889
|
}
|
|
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
2890
|
async function portClaims(state, runtime) {
|
|
3067
2891
|
const claims = [];
|
|
3068
2892
|
if ((await runtime.docker.available()).available) {
|
|
3069
|
-
const own =
|
|
3070
|
-
for (const claim of await runtime.docker.portClaims()) if (
|
|
2893
|
+
const own = state.runtime?.kind === "docker" ? state.runtime.containerName : null;
|
|
2894
|
+
for (const claim of await runtime.docker.portClaims()) if (claim.container !== own) claims.push({
|
|
3071
2895
|
port: claim.port,
|
|
3072
2896
|
holder: `container ${claim.container}`
|
|
3073
2897
|
});
|
|
3074
2898
|
}
|
|
3075
|
-
for (const other of await runtime.instances()) if (other.name !== state.name) claims.push(
|
|
2899
|
+
for (const other of await runtime.instances()) if (other.name !== state.name && other.runtime !== null) claims.push({
|
|
2900
|
+
port: other.runtime.port,
|
|
2901
|
+
holder: `rde instance ${other.name}`
|
|
2902
|
+
});
|
|
3076
2903
|
return claims;
|
|
3077
2904
|
}
|
|
3078
2905
|
//#endregion
|
|
3079
2906
|
//#region src/steps/admin-user.ts
|
|
3080
|
-
const STEP_ID$
|
|
2907
|
+
const STEP_ID$8 = "admin-user";
|
|
3081
2908
|
const ADMIN_USER_KEYS = {
|
|
3082
|
-
email: `${STEP_ID$
|
|
3083
|
-
password: `${STEP_ID$
|
|
3084
|
-
firstName: `${STEP_ID$
|
|
3085
|
-
lastName: `${STEP_ID$
|
|
2909
|
+
email: `${STEP_ID$8}.email`,
|
|
2910
|
+
password: `${STEP_ID$8}.password`,
|
|
2911
|
+
firstName: `${STEP_ID$8}.first-name`,
|
|
2912
|
+
lastName: `${STEP_ID$8}.last-name`
|
|
3086
2913
|
};
|
|
3087
2914
|
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
3088
2915
|
const GENERATED_PASSWORD_BYTES = 24;
|
|
@@ -3097,7 +2924,7 @@ function generatePassword(random) {
|
|
|
3097
2924
|
if (DIGIT.test(candidate)) return candidate;
|
|
3098
2925
|
}
|
|
3099
2926
|
}
|
|
3100
|
-
function hostDeps$
|
|
2927
|
+
function hostDeps$13(ctx) {
|
|
3101
2928
|
return {
|
|
3102
2929
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3103
2930
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
@@ -3235,9 +3062,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
|
|
|
3235
3062
|
ctx.ui.success(`Admin user ${email} recorded`);
|
|
3236
3063
|
return "completed";
|
|
3237
3064
|
}
|
|
3238
|
-
function adminUserStep(options, deps = hostDeps$
|
|
3065
|
+
function adminUserStep(options, deps = hostDeps$13) {
|
|
3239
3066
|
return {
|
|
3240
|
-
id: STEP_ID$
|
|
3067
|
+
id: STEP_ID$8,
|
|
3241
3068
|
title: "Create the admin user",
|
|
3242
3069
|
redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
|
|
3243
3070
|
async detect(ctx) {
|
|
@@ -3652,14 +3479,14 @@ function mbTool(runner) {
|
|
|
3652
3479
|
}
|
|
3653
3480
|
//#endregion
|
|
3654
3481
|
//#region src/steps/api-key.ts
|
|
3655
|
-
const STEP_ID$
|
|
3482
|
+
const STEP_ID$7 = "api-key";
|
|
3656
3483
|
const API_KEY_KEYS = {
|
|
3657
|
-
email: `${STEP_ID$
|
|
3658
|
-
password: `${STEP_ID$
|
|
3659
|
-
profile: `${STEP_ID$
|
|
3484
|
+
email: `${STEP_ID$7}.email`,
|
|
3485
|
+
password: `${STEP_ID$7}.password`,
|
|
3486
|
+
profile: `${STEP_ID$7}.profile`
|
|
3660
3487
|
};
|
|
3661
3488
|
const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
3662
|
-
function hostDeps$
|
|
3489
|
+
function hostDeps$12(ctx) {
|
|
3663
3490
|
return {
|
|
3664
3491
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3665
3492
|
runner: processRunner(ctx.signal),
|
|
@@ -3720,7 +3547,7 @@ async function authenticates(client) {
|
|
|
3720
3547
|
}
|
|
3721
3548
|
}
|
|
3722
3549
|
async function forgetCompletion(ctx) {
|
|
3723
|
-
const { [STEP_ID$
|
|
3550
|
+
const { [STEP_ID$7]: record, ...steps } = ctx.state.steps;
|
|
3724
3551
|
if (record !== void 0) await ctx.saveState({
|
|
3725
3552
|
...ctx.state,
|
|
3726
3553
|
steps
|
|
@@ -3797,15 +3624,15 @@ function usageNote(profile) {
|
|
|
3797
3624
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
3798
3625
|
return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
|
|
3799
3626
|
}
|
|
3800
|
-
function apiKeyStep(deps = hostDeps$
|
|
3627
|
+
function apiKeyStep(deps = hostDeps$12) {
|
|
3801
3628
|
return {
|
|
3802
|
-
id: STEP_ID$
|
|
3629
|
+
id: STEP_ID$7,
|
|
3803
3630
|
title: "Connect the Metabase CLI",
|
|
3804
3631
|
redoHint: "`rde status` shows the mb profile it uses.",
|
|
3805
3632
|
async detect(ctx) {
|
|
3806
3633
|
const configured = configuredRuntime(ctx.state);
|
|
3807
3634
|
const apiKey = ctx.secrets.apiKey;
|
|
3808
|
-
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$
|
|
3635
|
+
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
|
|
3809
3636
|
const resolved = deps(ctx);
|
|
3810
3637
|
if (!await resolved.instanceRunning()) return "todo";
|
|
3811
3638
|
if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
|
|
@@ -4827,7 +4654,7 @@ const RSA_KEY_UNAVAILABLE_ERROR = "RSA public key is not available";
|
|
|
4827
4654
|
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
4655
|
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
4656
|
const CONNECTION_PLACEHOLDER = "leave empty to skip";
|
|
4830
|
-
const
|
|
4657
|
+
const ENGINE_PLACEHOLDER = "leave empty to pick the engine";
|
|
4831
4658
|
const START_SCREEN = {
|
|
4832
4659
|
engine: "pick-engine",
|
|
4833
4660
|
"connection-string": "connection-string"
|
|
@@ -4853,6 +4680,9 @@ function targeting(stub) {
|
|
|
4853
4680
|
function restart(model) {
|
|
4854
4681
|
return model.target === null ? FRESH$1 : targeting(model.target);
|
|
4855
4682
|
}
|
|
4683
|
+
function mayFinish(run) {
|
|
4684
|
+
return !run.deps.required || run.deps.existingCount + run.created.length > 0;
|
|
4685
|
+
}
|
|
4856
4686
|
function nextStub(run, model) {
|
|
4857
4687
|
const stub = run.queue.shift();
|
|
4858
4688
|
if (stub !== void 0) return {
|
|
@@ -4860,13 +4690,18 @@ function nextStub(run, model) {
|
|
|
4860
4690
|
to: "connection-string",
|
|
4861
4691
|
model: targeting(stub)
|
|
4862
4692
|
};
|
|
4863
|
-
|
|
4693
|
+
if (run.created.length > 0) return {
|
|
4864
4694
|
type: "next",
|
|
4865
4695
|
to: "another",
|
|
4866
4696
|
model
|
|
4867
|
-
}
|
|
4697
|
+
};
|
|
4698
|
+
return mayFinish(run) ? {
|
|
4868
4699
|
type: "done",
|
|
4869
4700
|
model
|
|
4701
|
+
} : {
|
|
4702
|
+
type: "next",
|
|
4703
|
+
to: "connection-string",
|
|
4704
|
+
model: FRESH$1
|
|
4870
4705
|
};
|
|
4871
4706
|
}
|
|
4872
4707
|
function stubNamed(run, name) {
|
|
@@ -4956,9 +4791,13 @@ function pickStubsScreen(run) {
|
|
|
4956
4791
|
run.queue = run.stubs.filter((stub) => picked.includes(stub.id));
|
|
4957
4792
|
return nextStub(run, model);
|
|
4958
4793
|
}
|
|
4959
|
-
if (run.deps.start === "connection-string" && run.created.length === 0) return {
|
|
4794
|
+
if (run.deps.start === "connection-string" && run.created.length === 0) return mayFinish(run) ? {
|
|
4960
4795
|
type: "done",
|
|
4961
4796
|
model
|
|
4797
|
+
} : {
|
|
4798
|
+
type: "next",
|
|
4799
|
+
to: "connection-string",
|
|
4800
|
+
model: FRESH$1
|
|
4962
4801
|
};
|
|
4963
4802
|
return {
|
|
4964
4803
|
type: "next",
|
|
@@ -4968,6 +4807,11 @@ function pickStubsScreen(run) {
|
|
|
4968
4807
|
};
|
|
4969
4808
|
}
|
|
4970
4809
|
function connectionQuestion(run, target) {
|
|
4810
|
+
if (target === null && !mayFinish(run)) return {
|
|
4811
|
+
key: DATABASE_KEYS.connection,
|
|
4812
|
+
message: "Connection string of your database (Enter to pick its engine instead)",
|
|
4813
|
+
placeholder: ENGINE_PLACEHOLDER
|
|
4814
|
+
};
|
|
4971
4815
|
if (target === null) return {
|
|
4972
4816
|
key: DATABASE_KEYS.connection,
|
|
4973
4817
|
message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
|
|
@@ -4976,7 +4820,7 @@ function connectionQuestion(run, target) {
|
|
|
4976
4820
|
return {
|
|
4977
4821
|
key: databaseKey(ordinalOf(run), "connection"),
|
|
4978
4822
|
message: `Connection string of ${target.name} (Enter to pick its engine instead)`,
|
|
4979
|
-
placeholder:
|
|
4823
|
+
placeholder: ENGINE_PLACEHOLDER
|
|
4980
4824
|
};
|
|
4981
4825
|
}
|
|
4982
4826
|
function connectionStringScreen(run) {
|
|
@@ -4988,7 +4832,7 @@ function connectionStringScreen(run) {
|
|
|
4988
4832
|
...draft !== null && { initialValue: draft }
|
|
4989
4833
|
})).trim();
|
|
4990
4834
|
draft = uri;
|
|
4991
|
-
if (uri.length === 0) return model.target === null ? {
|
|
4835
|
+
if (uri.length === 0) return model.target === null && mayFinish(run) ? {
|
|
4992
4836
|
type: "done",
|
|
4993
4837
|
model
|
|
4994
4838
|
} : {
|
|
@@ -5027,22 +4871,28 @@ function connectionStringScreen(run) {
|
|
|
5027
4871
|
}
|
|
5028
4872
|
};
|
|
5029
4873
|
}
|
|
5030
|
-
function
|
|
5031
|
-
if (model.target !== null) return
|
|
5032
|
-
|
|
4874
|
+
function finishChoices(run, model) {
|
|
4875
|
+
if (model.target !== null) return [{
|
|
4876
|
+
value: FINISH,
|
|
4877
|
+
label: `Leave ${model.target.name} unconnected`
|
|
4878
|
+
}];
|
|
4879
|
+
if (run.deps.existingCount + run.created.length > 0) return [{
|
|
4880
|
+
value: FINISH,
|
|
4881
|
+
label: "Finish"
|
|
4882
|
+
}];
|
|
4883
|
+
return mayFinish(run) ? [{
|
|
4884
|
+
value: FINISH,
|
|
4885
|
+
label: "Skip for now"
|
|
4886
|
+
}] : [];
|
|
5033
4887
|
}
|
|
5034
4888
|
function pickEngineScreen(run) {
|
|
5035
4889
|
return async (model) => {
|
|
5036
4890
|
const total = run.deps.existingCount + run.created.length;
|
|
5037
|
-
const finishChoice = {
|
|
5038
|
-
value: FINISH,
|
|
5039
|
-
label: finishLabel(run, model)
|
|
5040
|
-
};
|
|
5041
4891
|
for (;;) {
|
|
5042
4892
|
const picked = await run.deps.prompts.select({
|
|
5043
4893
|
key: databaseKey(ordinalOf(run), "engine"),
|
|
5044
4894
|
message: "Which database engine?",
|
|
5045
|
-
choices: [...run.deps.catalog.list().map(engineChoice),
|
|
4895
|
+
choices: [...run.deps.catalog.list().map(engineChoice), ...finishChoices(run, model)],
|
|
5046
4896
|
...model.engine !== null && { initialValue: model.engine.key }
|
|
5047
4897
|
});
|
|
5048
4898
|
if (picked === "finish" && model.target !== null) return nextStub(run, model);
|
|
@@ -5518,6 +5368,13 @@ const METABASE_CONTENT_DIRS = [
|
|
|
5518
5368
|
];
|
|
5519
5369
|
const TOKEN_USERNAMES = { "bitbucket.org": "x-token-auth" };
|
|
5520
5370
|
const DEFAULT_TOKEN_USERNAME = "x-access-token";
|
|
5371
|
+
const MISSING_REF_EXIT_CODE = 1;
|
|
5372
|
+
const FIELD_SEPARATOR = "";
|
|
5373
|
+
const COMMIT_FORMAT = [
|
|
5374
|
+
"%H",
|
|
5375
|
+
"%h",
|
|
5376
|
+
"%s"
|
|
5377
|
+
].join("%x1f");
|
|
5521
5378
|
function credentialEnv(credentials) {
|
|
5522
5379
|
const { origin, hostname } = new URL(credentials.url);
|
|
5523
5380
|
const username = TOKEN_USERNAMES[hostname] ?? DEFAULT_TOKEN_USERNAME;
|
|
@@ -5531,6 +5388,18 @@ function credentialEnv(credentials) {
|
|
|
5531
5388
|
GIT_TERMINAL_PROMPT: "0"
|
|
5532
5389
|
};
|
|
5533
5390
|
}
|
|
5391
|
+
function parseCommitLine(line) {
|
|
5392
|
+
const [sha, shortSha, subject] = line.split(FIELD_SEPARATOR);
|
|
5393
|
+
if (sha === void 0 || shortSha === void 0 || subject === void 0) throw new Error(`unexpected \`git log\` line: ${line}`);
|
|
5394
|
+
return {
|
|
5395
|
+
sha,
|
|
5396
|
+
shortSha,
|
|
5397
|
+
subject
|
|
5398
|
+
};
|
|
5399
|
+
}
|
|
5400
|
+
function outputLines(stdout) {
|
|
5401
|
+
return stdout.split("\n").filter((line) => line.length > 0);
|
|
5402
|
+
}
|
|
5534
5403
|
async function directoryExists(path) {
|
|
5535
5404
|
try {
|
|
5536
5405
|
return (await promises.stat(path)).isDirectory();
|
|
@@ -5586,8 +5455,12 @@ function gitTool(runner) {
|
|
|
5586
5455
|
], { cwd: dir });
|
|
5587
5456
|
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
5588
5457
|
}
|
|
5589
|
-
async function
|
|
5590
|
-
await git([
|
|
5458
|
+
async function commitPaths(dir, paths, message) {
|
|
5459
|
+
await git([
|
|
5460
|
+
"add",
|
|
5461
|
+
"--",
|
|
5462
|
+
...paths
|
|
5463
|
+
], { cwd: dir });
|
|
5591
5464
|
await git([
|
|
5592
5465
|
...await hasIdentity(dir) ? [] : [
|
|
5593
5466
|
"-c",
|
|
@@ -5597,7 +5470,9 @@ function gitTool(runner) {
|
|
|
5597
5470
|
],
|
|
5598
5471
|
"commit",
|
|
5599
5472
|
"--message",
|
|
5600
|
-
message
|
|
5473
|
+
message,
|
|
5474
|
+
"--",
|
|
5475
|
+
...paths
|
|
5601
5476
|
], { cwd: dir });
|
|
5602
5477
|
}
|
|
5603
5478
|
async function addRemote(dir, name, url) {
|
|
@@ -5643,6 +5518,42 @@ function gitTool(runner) {
|
|
|
5643
5518
|
if (result.exitCode !== 0) return null;
|
|
5644
5519
|
return result.stdout.trim();
|
|
5645
5520
|
}
|
|
5521
|
+
async function isBareRepo(path) {
|
|
5522
|
+
const args = [
|
|
5523
|
+
`--git-dir=${path}`,
|
|
5524
|
+
"rev-parse",
|
|
5525
|
+
"--is-bare-repository"
|
|
5526
|
+
];
|
|
5527
|
+
const result = await runner.run(GIT, args);
|
|
5528
|
+
return result.exitCode === 0 && result.stdout.trim() === "true";
|
|
5529
|
+
}
|
|
5530
|
+
async function bareBranchCommits(path, branch) {
|
|
5531
|
+
const gitDir = `--git-dir=${path}`;
|
|
5532
|
+
const ref = `refs/heads/${branch}`;
|
|
5533
|
+
const verifyArgs = [
|
|
5534
|
+
gitDir,
|
|
5535
|
+
"rev-parse",
|
|
5536
|
+
"--verify",
|
|
5537
|
+
"--quiet",
|
|
5538
|
+
`${ref}^{commit}`
|
|
5539
|
+
];
|
|
5540
|
+
const verified = await runner.run(GIT, verifyArgs);
|
|
5541
|
+
if (verified.exitCode === MISSING_REF_EXIT_CODE) return null;
|
|
5542
|
+
if (verified.exitCode !== 0) throw toolFailure(GIT, verifyArgs, verified);
|
|
5543
|
+
return outputLines((await git([
|
|
5544
|
+
gitDir,
|
|
5545
|
+
"-c",
|
|
5546
|
+
"log.showSignature=false",
|
|
5547
|
+
"log",
|
|
5548
|
+
`--format=${COMMIT_FORMAT}`,
|
|
5549
|
+
ref,
|
|
5550
|
+
"--"
|
|
5551
|
+
])).stdout).map(parseCommitLine);
|
|
5552
|
+
}
|
|
5553
|
+
async function reachableCommits(dir) {
|
|
5554
|
+
const result = await git(["rev-list", "--all"], { cwd: dir });
|
|
5555
|
+
return new Set(outputLines(result.stdout));
|
|
5556
|
+
}
|
|
5646
5557
|
async function hasMetabaseContent(dir) {
|
|
5647
5558
|
for (const name of METABASE_CONTENT_DIRS) if (await directoryExists(join(dir, name))) return true;
|
|
5648
5559
|
return false;
|
|
@@ -5653,24 +5564,27 @@ function gitTool(runner) {
|
|
|
5653
5564
|
initBare,
|
|
5654
5565
|
init,
|
|
5655
5566
|
hasCommits,
|
|
5656
|
-
|
|
5567
|
+
commitPaths,
|
|
5657
5568
|
addRemote,
|
|
5658
5569
|
clone,
|
|
5659
5570
|
push,
|
|
5660
5571
|
currentBranch,
|
|
5661
5572
|
remoteUrl,
|
|
5573
|
+
isBareRepo,
|
|
5574
|
+
bareBranchCommits,
|
|
5575
|
+
reachableCommits,
|
|
5662
5576
|
hasMetabaseContent
|
|
5663
5577
|
};
|
|
5664
5578
|
}
|
|
5665
5579
|
//#endregion
|
|
5666
5580
|
//#region src/steps/git-sync.ts
|
|
5667
|
-
const STEP_ID$
|
|
5581
|
+
const STEP_ID$6 = "git-sync";
|
|
5668
5582
|
const GIT_SYNC_KEYS = {
|
|
5669
|
-
location: `${STEP_ID$
|
|
5670
|
-
path: `${STEP_ID$
|
|
5671
|
-
remote: `${STEP_ID$
|
|
5672
|
-
url: `${STEP_ID$
|
|
5673
|
-
token: `${STEP_ID$
|
|
5583
|
+
location: `${STEP_ID$6}.location`,
|
|
5584
|
+
path: `${STEP_ID$6}.path`,
|
|
5585
|
+
remote: `${STEP_ID$6}.remote`,
|
|
5586
|
+
url: `${STEP_ID$6}.url`,
|
|
5587
|
+
token: `${STEP_ID$6}.token`
|
|
5674
5588
|
};
|
|
5675
5589
|
const BARE_REPO_NAME = "content.git";
|
|
5676
5590
|
const DEFAULT_BRANCH = "main";
|
|
@@ -5704,7 +5618,7 @@ const SYNC_MODE_SETTINGS = {
|
|
|
5704
5618
|
"remote-sync-type": "read-write",
|
|
5705
5619
|
"remote-sync-transforms": true
|
|
5706
5620
|
};
|
|
5707
|
-
function hostDeps$
|
|
5621
|
+
function hostDeps$11(ctx) {
|
|
5708
5622
|
return {
|
|
5709
5623
|
runner: processRunner(ctx.signal),
|
|
5710
5624
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -5956,7 +5870,7 @@ async function prepareWorkdir(run, workdir) {
|
|
|
5956
5870
|
if (!await run.git.hasCommits(workdir)) {
|
|
5957
5871
|
await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
|
|
5958
5872
|
await ignoreDataAppFiles(workdir);
|
|
5959
|
-
await run.git.
|
|
5873
|
+
await run.git.commitPaths(workdir, [README_NAME, GITIGNORE_NAME], INITIAL_COMMIT_MESSAGE);
|
|
5960
5874
|
}
|
|
5961
5875
|
return {
|
|
5962
5876
|
workdir,
|
|
@@ -6120,7 +6034,7 @@ function shellWord(value) {
|
|
|
6120
6034
|
function exportCommand(profile, branch) {
|
|
6121
6035
|
return `${mbCommand(profile)} git-sync export -b ${shellWord(branch)}`;
|
|
6122
6036
|
}
|
|
6123
|
-
function usageLines
|
|
6037
|
+
function usageLines(state, gitRemote) {
|
|
6124
6038
|
const profile = state.mbProfile;
|
|
6125
6039
|
return [
|
|
6126
6040
|
`cd ${gitRemote.workdir}`,
|
|
@@ -6171,18 +6085,18 @@ async function runGitSync(ctx, deps) {
|
|
|
6171
6085
|
...ctx.state,
|
|
6172
6086
|
gitRemote: remote.gitRemote
|
|
6173
6087
|
});
|
|
6174
|
-
ctx.ui.note(usageLines
|
|
6088
|
+
ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
6175
6089
|
return "completed";
|
|
6176
6090
|
}
|
|
6177
|
-
function gitSyncStep(deps = hostDeps$
|
|
6091
|
+
function gitSyncStep(deps = hostDeps$11) {
|
|
6178
6092
|
return {
|
|
6179
|
-
id: STEP_ID$
|
|
6093
|
+
id: STEP_ID$6,
|
|
6180
6094
|
title: "Connect a git repository",
|
|
6181
6095
|
redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
|
|
6182
6096
|
async detect(ctx) {
|
|
6183
6097
|
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$
|
|
6098
|
+
if (ctx.state.steps[STEP_ID$6]?.status === "skipped") return "not-applicable";
|
|
6099
|
+
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$6]?.status !== "done") return "todo";
|
|
6186
6100
|
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6187
6101
|
if (instance === null) return "todo";
|
|
6188
6102
|
try {
|
|
@@ -6364,12 +6278,12 @@ function skillsCli(runner) {
|
|
|
6364
6278
|
}
|
|
6365
6279
|
//#endregion
|
|
6366
6280
|
//#region src/steps/agents-skills.ts
|
|
6367
|
-
const STEP_ID$
|
|
6368
|
-
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$
|
|
6281
|
+
const STEP_ID$5 = "agents-skills";
|
|
6282
|
+
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
|
|
6369
6283
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6370
6284
|
const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
|
|
6371
6285
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6372
|
-
function hostDeps$
|
|
6286
|
+
function hostDeps$10(ctx) {
|
|
6373
6287
|
return {
|
|
6374
6288
|
runner: processRunner(ctx.signal),
|
|
6375
6289
|
probe: hostAgentProbe(),
|
|
@@ -6548,11 +6462,11 @@ function skipLine(all, source) {
|
|
|
6548
6462
|
switch (source) {
|
|
6549
6463
|
case "override":
|
|
6550
6464
|
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$
|
|
6465
|
+
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$5}\` to install it later`;
|
|
6466
|
+
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$5} --agents <name>\` once one is installed`;
|
|
6553
6467
|
}
|
|
6554
6468
|
}
|
|
6555
|
-
function agentsSkillsStep(options, updates, deps = hostDeps$
|
|
6469
|
+
function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
6556
6470
|
let hashes = null;
|
|
6557
6471
|
async function setCheck(resolved) {
|
|
6558
6472
|
hashes ??= hashLookup(resolved.github);
|
|
@@ -6563,13 +6477,13 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6563
6477
|
};
|
|
6564
6478
|
}
|
|
6565
6479
|
return {
|
|
6566
|
-
id: STEP_ID$
|
|
6480
|
+
id: STEP_ID$5,
|
|
6567
6481
|
title: "Install the rde and data-app skills for your coding agents",
|
|
6568
6482
|
redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
|
|
6569
6483
|
async detect(ctx) {
|
|
6570
6484
|
const resolved = deps(ctx);
|
|
6571
6485
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6572
|
-
if (detected.length === 0) return ctx.state.steps[STEP_ID$
|
|
6486
|
+
if (detected.length === 0) return ctx.state.steps[STEP_ID$5]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6573
6487
|
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6574
6488
|
const check = await setCheck(resolved);
|
|
6575
6489
|
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
@@ -6617,8 +6531,10 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6617
6531
|
}
|
|
6618
6532
|
//#endregion
|
|
6619
6533
|
//#region src/steps/databases.ts
|
|
6620
|
-
const
|
|
6621
|
-
const
|
|
6534
|
+
const DATABASE_REQUIRED = { request: "required" };
|
|
6535
|
+
const WRITE_ACCESS = "Transforms write their tables to it, so connect it as a user that can create tables.";
|
|
6536
|
+
const NONE_CONNECTED = `rde init needs a database of your own to go on. ${WRITE_ACCESS}`;
|
|
6537
|
+
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
6538
|
function hostDatabasesDeps(signal) {
|
|
6623
6539
|
return {
|
|
6624
6540
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
@@ -6626,7 +6542,7 @@ function hostDatabasesDeps(signal) {
|
|
|
6626
6542
|
now: () => /* @__PURE__ */ new Date()
|
|
6627
6543
|
};
|
|
6628
6544
|
}
|
|
6629
|
-
function requireApiKey$
|
|
6545
|
+
function requireApiKey$1(state, apiKey) {
|
|
6630
6546
|
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
6631
6547
|
return apiKey;
|
|
6632
6548
|
}
|
|
@@ -6636,7 +6552,7 @@ function hostAliasOf(runtime) {
|
|
|
6636
6552
|
case "jar": return null;
|
|
6637
6553
|
}
|
|
6638
6554
|
}
|
|
6639
|
-
function toRecord
|
|
6555
|
+
function toRecord(database, createdAt) {
|
|
6640
6556
|
return {
|
|
6641
6557
|
id: database.id,
|
|
6642
6558
|
name: database.name,
|
|
@@ -6648,7 +6564,7 @@ function authenticatedClient(state, apiKey, deps) {
|
|
|
6648
6564
|
const configured = requireConfiguredRuntime(state);
|
|
6649
6565
|
return deps.client(configured.url, {
|
|
6650
6566
|
kind: "api-key",
|
|
6651
|
-
key: requireApiKey$
|
|
6567
|
+
key: requireApiKey$1(state, apiKey)
|
|
6652
6568
|
});
|
|
6653
6569
|
}
|
|
6654
6570
|
async function listDatabases(state, apiKey, deps) {
|
|
@@ -6691,7 +6607,7 @@ async function connectDatabases(ctx, deps, opening) {
|
|
|
6691
6607
|
existingCount: existing.length,
|
|
6692
6608
|
readTextFile: deps.readTextFile,
|
|
6693
6609
|
onCreated: async (database) => {
|
|
6694
|
-
const record = toRecord
|
|
6610
|
+
const record = toRecord(database, deps.now().toISOString());
|
|
6695
6611
|
await ctx.saveState({
|
|
6696
6612
|
...ctx.state,
|
|
6697
6613
|
databases: [...ctx.state.databases, record]
|
|
@@ -6708,6 +6624,7 @@ async function connectDatabases(ctx, deps, opening) {
|
|
|
6708
6624
|
async function addDatabases(ctx, deps, options) {
|
|
6709
6625
|
const connected = await connectDatabases(ctx, deps, {
|
|
6710
6626
|
start: "engine",
|
|
6627
|
+
required: false,
|
|
6711
6628
|
...options
|
|
6712
6629
|
});
|
|
6713
6630
|
return {
|
|
@@ -6715,50 +6632,33 @@ async function addDatabases(ctx, deps, options) {
|
|
|
6715
6632
|
result: { addedDatabases: connected.added }
|
|
6716
6633
|
};
|
|
6717
6634
|
}
|
|
6718
|
-
async function
|
|
6719
|
-
const connected = await connectDatabases(ctx, deps,
|
|
6720
|
-
|
|
6721
|
-
offerStubs: true
|
|
6722
|
-
});
|
|
6635
|
+
async function connectRequired(ctx, deps, opening) {
|
|
6636
|
+
const connected = await connectDatabases(ctx, deps, opening);
|
|
6637
|
+
if (connected.existing + connected.created === 0) throw new ConfigError(NONE_CONNECTED);
|
|
6723
6638
|
return {
|
|
6724
|
-
outcome:
|
|
6639
|
+
outcome: "completed",
|
|
6725
6640
|
result: { addedDatabases: connected.added }
|
|
6726
6641
|
};
|
|
6727
6642
|
}
|
|
6728
|
-
function hostDeps$
|
|
6643
|
+
function hostDeps$9(ctx) {
|
|
6729
6644
|
return {
|
|
6730
6645
|
...hostDatabasesDeps(ctx.signal),
|
|
6731
6646
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
6732
6647
|
};
|
|
6733
6648
|
}
|
|
6734
|
-
function
|
|
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
|
-
function databasesStep(options, deps = hostDeps$10) {
|
|
6649
|
+
function databasesStep(options, deps = hostDeps$9) {
|
|
6745
6650
|
return {
|
|
6746
|
-
id:
|
|
6651
|
+
id: "databases",
|
|
6747
6652
|
title: "Connect your databases",
|
|
6748
6653
|
redoHint: "Connect another with `rde db add --url <connection string>`.",
|
|
6749
6654
|
async detect(ctx) {
|
|
6750
6655
|
const configured = configuredRuntime(ctx.state);
|
|
6751
6656
|
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
|
-
}
|
|
6657
|
+
const recorded = new Set(ctx.state.databases.map((record) => record.id));
|
|
6658
|
+
if (recorded.size === 0 && options.request === "named") return "todo";
|
|
6759
6659
|
const resolved = deps(ctx);
|
|
6760
6660
|
if (!await resolved.instanceRunning()) return "todo";
|
|
6761
|
-
const isOwn = (id) =>
|
|
6661
|
+
const isOwn = (id) => recorded.size === 0 || recorded.has(id);
|
|
6762
6662
|
const client = resolved.client(configured.url, {
|
|
6763
6663
|
kind: "api-key",
|
|
6764
6664
|
key: ctx.secrets.apiKey
|
|
@@ -6772,9 +6672,19 @@ function databasesStep(options, deps = hostDeps$10) {
|
|
|
6772
6672
|
},
|
|
6773
6673
|
async run(ctx) {
|
|
6774
6674
|
switch (options.request) {
|
|
6775
|
-
case "ask
|
|
6776
|
-
|
|
6777
|
-
|
|
6675
|
+
case "ask":
|
|
6676
|
+
ctx.ui.info(NONE_CONNECTED);
|
|
6677
|
+
return connectRequired(ctx, deps(ctx), {
|
|
6678
|
+
start: "connection-string",
|
|
6679
|
+
offerStubs: true,
|
|
6680
|
+
required: true
|
|
6681
|
+
});
|
|
6682
|
+
case "named": return connectRequired(ctx, deps(ctx), {
|
|
6683
|
+
start: "engine",
|
|
6684
|
+
offerStubs: false,
|
|
6685
|
+
required: true
|
|
6686
|
+
});
|
|
6687
|
+
case "required": throw new ConfigError(NONE_NAMED);
|
|
6778
6688
|
}
|
|
6779
6689
|
}
|
|
6780
6690
|
};
|
|
@@ -6799,7 +6709,7 @@ function npmRegistry(signal, options = {}) {
|
|
|
6799
6709
|
}
|
|
6800
6710
|
//#endregion
|
|
6801
6711
|
//#region src/steps/install-mb.ts
|
|
6802
|
-
const STEP_ID$
|
|
6712
|
+
const STEP_ID$4 = "install-mb";
|
|
6803
6713
|
const MIN_MB_VERSION = "0.3.0";
|
|
6804
6714
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
6805
6715
|
function installedWith(scriptPath) {
|
|
@@ -6808,7 +6718,7 @@ function installedWith(scriptPath) {
|
|
|
6808
6718
|
function rdeInstalledWith() {
|
|
6809
6719
|
return installedWith(process.argv[1]);
|
|
6810
6720
|
}
|
|
6811
|
-
function hostDeps$
|
|
6721
|
+
function hostDeps$8(ctx) {
|
|
6812
6722
|
return {
|
|
6813
6723
|
runner: processRunner(ctx.signal),
|
|
6814
6724
|
installedWith: rdeInstalledWith(),
|
|
@@ -6975,7 +6885,7 @@ async function updateFor(release, installed, deps) {
|
|
|
6975
6885
|
version
|
|
6976
6886
|
};
|
|
6977
6887
|
}
|
|
6978
|
-
function installMbStep(updates, deps = hostDeps$
|
|
6888
|
+
function installMbStep(updates, deps = hostDeps$8) {
|
|
6979
6889
|
let published = null;
|
|
6980
6890
|
function release(resolved) {
|
|
6981
6891
|
if (updates === "installed") return Promise.resolve(UNCHECKED);
|
|
@@ -6983,7 +6893,7 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
6983
6893
|
return published;
|
|
6984
6894
|
}
|
|
6985
6895
|
return {
|
|
6986
|
-
id: STEP_ID$
|
|
6896
|
+
id: STEP_ID$4,
|
|
6987
6897
|
title: "Install the Metabase CLI",
|
|
6988
6898
|
async detect(ctx) {
|
|
6989
6899
|
const resolved = deps(ctx);
|
|
@@ -7025,26 +6935,47 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
7025
6935
|
}
|
|
7026
6936
|
//#endregion
|
|
7027
6937
|
//#region src/steps/license.ts
|
|
7028
|
-
const STEP_ID$
|
|
7029
|
-
const LICENSE_KEYS = { token: `${STEP_ID$
|
|
6938
|
+
const STEP_ID$3 = "license";
|
|
6939
|
+
const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
|
|
7030
6940
|
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
7031
|
-
const NOT_FOUND_STATUS$
|
|
7032
|
-
|
|
6941
|
+
const NOT_FOUND_STATUS$1 = 404;
|
|
6942
|
+
const TOKEN_REFUSED_STATUS = 400;
|
|
6943
|
+
const STORE_UNREACHABLE_STATUS = 503;
|
|
6944
|
+
const TOKEN_CHECK_HOST = "token-check.metabase.com";
|
|
6945
|
+
const ACTIVATION_FAILED = "License activation failed";
|
|
6946
|
+
const LICENSED_FEATURES = {
|
|
6947
|
+
[REMOTE_SYNC_FEATURE]: "remote sync",
|
|
6948
|
+
"data-apps": "data apps",
|
|
6949
|
+
"transforms-testing": "transform tests"
|
|
6950
|
+
};
|
|
6951
|
+
const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
|
|
6952
|
+
function licensedFeatureCoverage(features) {
|
|
6953
|
+
const entries = Object.entries(LICENSED_FEATURES);
|
|
6954
|
+
return {
|
|
6955
|
+
included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
|
|
6956
|
+
missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
|
|
6957
|
+
};
|
|
6958
|
+
}
|
|
6959
|
+
function listOf(names, type) {
|
|
6960
|
+
return new Intl.ListFormat("en", { type }).format(names);
|
|
6961
|
+
}
|
|
6962
|
+
function hostDeps$7(ctx) {
|
|
7033
6963
|
return {
|
|
7034
6964
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7035
6965
|
runner: processRunner(ctx.signal),
|
|
7036
|
-
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
6966
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
6967
|
+
terminal: stdinIsTty()
|
|
7037
6968
|
};
|
|
7038
6969
|
}
|
|
7039
6970
|
function isNoTokenYet(error) {
|
|
7040
|
-
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$
|
|
6971
|
+
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
7041
6972
|
return isMetabaseNotServing(error);
|
|
7042
6973
|
}
|
|
7043
6974
|
function validateToken(value) {
|
|
7044
6975
|
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
7045
6976
|
return "A license token is 64 hexadecimal characters";
|
|
7046
6977
|
}
|
|
7047
|
-
function requireApiKey
|
|
6978
|
+
function requireApiKey(ctx) {
|
|
7048
6979
|
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
7049
6980
|
return ctx.secrets.apiKey;
|
|
7050
6981
|
}
|
|
@@ -7052,13 +6983,74 @@ function activeLabel(status) {
|
|
|
7052
6983
|
const plan = status["plan-alias"];
|
|
7053
6984
|
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
7054
6985
|
}
|
|
7055
|
-
function
|
|
6986
|
+
function refusalOf(error) {
|
|
6987
|
+
if (!(error instanceof MetabaseApiError)) return null;
|
|
6988
|
+
if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
|
|
6989
|
+
if (error.status === STORE_UNREACHABLE_STATUS) return new ConfigError(`The instance could not reach ${TOKEN_CHECK_HOST} to check the license token; it needs outbound network access`);
|
|
6990
|
+
return null;
|
|
6991
|
+
}
|
|
6992
|
+
async function activate(client, token) {
|
|
6993
|
+
try {
|
|
6994
|
+
await client.setting.setPremiumEmbeddingToken(token);
|
|
6995
|
+
} catch (error) {
|
|
6996
|
+
const refusal = refusalOf(error);
|
|
6997
|
+
if (refusal === null) throw error;
|
|
6998
|
+
return {
|
|
6999
|
+
kind: "refused",
|
|
7000
|
+
error: refusal
|
|
7001
|
+
};
|
|
7002
|
+
}
|
|
7003
|
+
const status = await client.tokenStatus.get();
|
|
7004
|
+
if (!status.valid) return {
|
|
7005
|
+
kind: "refused",
|
|
7006
|
+
error: new ConfigError(`Metabase reports the token as ${status.status}`)
|
|
7007
|
+
};
|
|
7056
7008
|
return {
|
|
7057
|
-
|
|
7009
|
+
kind: "activated",
|
|
7010
|
+
status
|
|
7011
|
+
};
|
|
7012
|
+
}
|
|
7013
|
+
async function activateAskedToken(ctx, client, terminal) {
|
|
7014
|
+
const token = await ctx.prompts.password({
|
|
7015
|
+
key: LICENSE_KEYS.token,
|
|
7016
|
+
message: "Paste your Metabase license token (Enter to skip)",
|
|
7017
|
+
validate: validateToken
|
|
7018
|
+
});
|
|
7019
|
+
if (token.length === 0) return null;
|
|
7020
|
+
const spinner = ctx.ui.spinner("Activating the license…");
|
|
7021
|
+
let activation;
|
|
7022
|
+
try {
|
|
7023
|
+
activation = await activate(client, token);
|
|
7024
|
+
} catch (error) {
|
|
7025
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
7026
|
+
throw error;
|
|
7027
|
+
}
|
|
7028
|
+
if (activation.kind === "activated") {
|
|
7029
|
+
spinner.stop(activeLabel(activation.status));
|
|
7030
|
+
ctx.session.licenseToken.set(token);
|
|
7031
|
+
return activation.status;
|
|
7032
|
+
}
|
|
7033
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
7034
|
+
if (!terminal) throw activation.error;
|
|
7035
|
+
ctx.ui.warn(activation.error.message);
|
|
7036
|
+
return activateAskedToken(ctx, client, terminal);
|
|
7037
|
+
}
|
|
7038
|
+
function reportFeatures(ctx, features) {
|
|
7039
|
+
const { missing } = licensedFeatureCoverage(features);
|
|
7040
|
+
const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
|
|
7041
|
+
if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
|
|
7042
|
+
if (missing.length > 0) {
|
|
7043
|
+
const gitNote = remoteSync ? "" : "; the git step will be skipped";
|
|
7044
|
+
ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
|
|
7045
|
+
}
|
|
7046
|
+
}
|
|
7047
|
+
function licenseStep(deps = hostDeps$7) {
|
|
7048
|
+
return {
|
|
7049
|
+
id: STEP_ID$3,
|
|
7058
7050
|
title: "Activate your license",
|
|
7059
7051
|
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
7060
7052
|
async detect(ctx) {
|
|
7061
|
-
if (ctx.state.steps[STEP_ID$
|
|
7053
|
+
if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
|
|
7062
7054
|
const configured = configuredRuntime(ctx.state);
|
|
7063
7055
|
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7064
7056
|
const resolved = deps(ctx);
|
|
@@ -7076,36 +7068,17 @@ function licenseStep(deps = hostDeps$8) {
|
|
|
7076
7068
|
},
|
|
7077
7069
|
async run(ctx) {
|
|
7078
7070
|
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);
|
|
7071
|
+
const apiKey = requireApiKey(ctx);
|
|
7090
7072
|
const resolved = deps(ctx);
|
|
7091
|
-
const
|
|
7073
|
+
const status = await activateAskedToken(ctx, resolved.client(configured.url, {
|
|
7092
7074
|
kind: "api-key",
|
|
7093
7075
|
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}`);
|
|
7076
|
+
}), resolved.terminal);
|
|
7077
|
+
if (status === null) {
|
|
7078
|
+
const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
|
|
7079
|
+
ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
|
|
7080
|
+
return { outcome: "skipped" };
|
|
7107
7081
|
}
|
|
7108
|
-
spinner.stop(activeLabel(status));
|
|
7109
7082
|
await ctx.saveState({
|
|
7110
7083
|
...ctx.state,
|
|
7111
7084
|
license: {
|
|
@@ -7120,8 +7093,7 @@ function licenseStep(deps = hostDeps$8) {
|
|
|
7120
7093
|
profile: ctx.state.mbProfile,
|
|
7121
7094
|
apiKey
|
|
7122
7095
|
});
|
|
7123
|
-
|
|
7124
|
-
else ctx.ui.warn("This license does not include remote sync; the git step will be skipped");
|
|
7096
|
+
reportFeatures(ctx, status.features);
|
|
7125
7097
|
return { outcome: "completed" };
|
|
7126
7098
|
}
|
|
7127
7099
|
};
|
|
@@ -7134,7 +7106,7 @@ const NODE_HINT = `Install Node ${MIN_NODE_VERSION} or newer from https://nodejs
|
|
|
7134
7106
|
const MARK_OK = "✓";
|
|
7135
7107
|
const MARK_MISSING = "✗";
|
|
7136
7108
|
const MARK_OPTIONAL = "–";
|
|
7137
|
-
const DOCKER_OPTIONAL_NOTE = `Metabase can run as a jar with Java instead
|
|
7109
|
+
const DOCKER_OPTIONAL_NOTE = `Metabase can run as a jar with Java instead`;
|
|
7138
7110
|
const JAVA_OPTIONAL_NOTE = `Metabase can run in Docker instead`;
|
|
7139
7111
|
const PLATFORMS = {
|
|
7140
7112
|
darwin: {
|
|
@@ -7150,7 +7122,7 @@ const PLATFORMS = {
|
|
|
7150
7122
|
javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
|
|
7151
7123
|
}
|
|
7152
7124
|
};
|
|
7153
|
-
function hostDeps$
|
|
7125
|
+
function hostDeps$6(ctx) {
|
|
7154
7126
|
const runner = processRunner(ctx.signal);
|
|
7155
7127
|
return {
|
|
7156
7128
|
platform: process.platform,
|
|
@@ -7272,7 +7244,7 @@ function checkLine(check) {
|
|
|
7272
7244
|
function checkLines(checks) {
|
|
7273
7245
|
return checks.map(checkLine);
|
|
7274
7246
|
}
|
|
7275
|
-
function preflightStep(deps = hostDeps$
|
|
7247
|
+
function preflightStep(deps = hostDeps$6) {
|
|
7276
7248
|
return {
|
|
7277
7249
|
id: "preflight",
|
|
7278
7250
|
title: "Check this machine",
|
|
@@ -7291,7 +7263,7 @@ function preflightStep(deps = hostDeps$7) {
|
|
|
7291
7263
|
}
|
|
7292
7264
|
//#endregion
|
|
7293
7265
|
//#region src/steps/runtime.ts
|
|
7294
|
-
const STEP_ID$
|
|
7266
|
+
const STEP_ID$2 = "runtime";
|
|
7295
7267
|
const MIN_PORT = 1;
|
|
7296
7268
|
const MAX_PORT = 65535;
|
|
7297
7269
|
const PORT_PATTERN = /^\d+$/;
|
|
@@ -7305,7 +7277,7 @@ const NO_RUNTIME_FLAGS = {
|
|
|
7305
7277
|
jar: null,
|
|
7306
7278
|
port: null
|
|
7307
7279
|
};
|
|
7308
|
-
function hostDeps$
|
|
7280
|
+
function hostDeps$5(ctx) {
|
|
7309
7281
|
return {
|
|
7310
7282
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7311
7283
|
releases: releaseLookup(ctx.signal)
|
|
@@ -7434,9 +7406,9 @@ async function persistedRuntime(ctx, options, deps) {
|
|
|
7434
7406
|
});
|
|
7435
7407
|
return chosen;
|
|
7436
7408
|
}
|
|
7437
|
-
function runtimeStep(options, deps = hostDeps$
|
|
7409
|
+
function runtimeStep(options, deps = hostDeps$5) {
|
|
7438
7410
|
return {
|
|
7439
|
-
id: STEP_ID$
|
|
7411
|
+
id: STEP_ID$2,
|
|
7440
7412
|
title: "Install Metabase",
|
|
7441
7413
|
redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
|
|
7442
7414
|
async detect(ctx) {
|
|
@@ -7460,204 +7432,6 @@ function runtimeStep(options, deps = hostDeps$6) {
|
|
|
7460
7432
|
}
|
|
7461
7433
|
};
|
|
7462
7434
|
}
|
|
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
7435
|
const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
|
|
7662
7436
|
//#endregion
|
|
7663
7437
|
//#region src/steps/start-metabase.ts
|
|
@@ -7679,21 +7453,6 @@ async function runningVersion(client) {
|
|
|
7679
7453
|
function progressLabel(progress) {
|
|
7680
7454
|
return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
|
|
7681
7455
|
}
|
|
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
7456
|
async function withMoveHint(ctx, deps, taken) {
|
|
7698
7457
|
const claims = await portClaims(ctx.state, deps.runtime);
|
|
7699
7458
|
const free = await deps.runtime.ports.findFree(taken.port + 1, claims);
|
|
@@ -7730,7 +7489,6 @@ function startMetabaseStep(deps = hostDeps$4) {
|
|
|
7730
7489
|
const runtime = runtimeFromState(configured.runtime, resolved.runtime);
|
|
7731
7490
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
7732
7491
|
await startRuntime(ctx, runtime, resolved);
|
|
7733
|
-
await startSampleDatabase(ctx, resolved);
|
|
7734
7492
|
const spinner = ctx.ui.spinner("Starting Metabase…");
|
|
7735
7493
|
let shown = 0;
|
|
7736
7494
|
try {
|
|
@@ -7841,7 +7599,6 @@ async function summaryLines(state, apiKey, deps) {
|
|
|
7841
7599
|
if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
|
|
7842
7600
|
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
7843
7601
|
lines.push(...await databaseLines$1(state, apiKey, deps));
|
|
7844
|
-
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
7845
7602
|
lines.push(...gitLines(state));
|
|
7846
7603
|
const installed = await skillsCli(deps.runner).list();
|
|
7847
7604
|
const agents = await detectAgents(deps.probe);
|
|
@@ -7917,7 +7674,7 @@ const DEFAULT_STEP_OPTIONS = {
|
|
|
7917
7674
|
runtime: NO_RUNTIME_FLAGS,
|
|
7918
7675
|
agents: DETECT_AGENTS,
|
|
7919
7676
|
admin: DEFAULT_ADMIN,
|
|
7920
|
-
databases:
|
|
7677
|
+
databases: DATABASE_REQUIRED,
|
|
7921
7678
|
updates: "published"
|
|
7922
7679
|
};
|
|
7923
7680
|
const LOCAL_STEP_OPTIONS = {
|
|
@@ -7933,7 +7690,6 @@ const STEP_BY_ID = {
|
|
|
7933
7690
|
"admin-user": (options) => adminUserStep(options.admin),
|
|
7934
7691
|
"api-key": () => apiKeyStep(),
|
|
7935
7692
|
transforms: () => transformsStep(),
|
|
7936
|
-
"sample-database": () => sampleDatabaseStep(),
|
|
7937
7693
|
license: () => licenseStep(),
|
|
7938
7694
|
databases: (options) => databasesStep(options.databases),
|
|
7939
7695
|
"git-sync": () => gitSyncStep(),
|
|
@@ -8009,7 +7765,7 @@ const SECTIONS = {
|
|
|
8009
7765
|
}).optional(),
|
|
8010
7766
|
"api-key": z.strictObject({ profile: MbProfileChoice }).optional(),
|
|
8011
7767
|
license: z.strictObject({ token: EnvString.nullable() }).optional(),
|
|
8012
|
-
databases: z.array(DatabaseEntry).optional(),
|
|
7768
|
+
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
7769
|
"git-sync": z.strictObject({
|
|
8014
7770
|
location: z.enum([
|
|
8015
7771
|
"here",
|
|
@@ -8081,8 +7837,8 @@ function runtimeKindOf(runtime) {
|
|
|
8081
7837
|
return null;
|
|
8082
7838
|
}
|
|
8083
7839
|
function databasesRequest(databases, run) {
|
|
8084
|
-
if (databases !== void 0) return
|
|
8085
|
-
return run.interactive ? "ask
|
|
7840
|
+
if (databases !== void 0) return "named";
|
|
7841
|
+
return run.interactive ? "ask" : "required";
|
|
8086
7842
|
}
|
|
8087
7843
|
function agentsOption(agents, run) {
|
|
8088
7844
|
if (agents !== void 0) return {
|
|
@@ -8144,7 +7900,6 @@ async function loadInitAnswers(path) {
|
|
|
8144
7900
|
}
|
|
8145
7901
|
function stepsAskingForWork(answers) {
|
|
8146
7902
|
const license = answers.license;
|
|
8147
|
-
const databases = answers.databases;
|
|
8148
7903
|
const git = answers["git-sync"];
|
|
8149
7904
|
const agents = answers["agents-skills"];
|
|
8150
7905
|
return [
|
|
@@ -8166,7 +7921,7 @@ function stepsAskingForWork(answers) {
|
|
|
8166
7921
|
},
|
|
8167
7922
|
{
|
|
8168
7923
|
step: "databases",
|
|
8169
|
-
asks: databases !== void 0
|
|
7924
|
+
asks: answers.databases !== void 0
|
|
8170
7925
|
},
|
|
8171
7926
|
{
|
|
8172
7927
|
step: "git-sync",
|
|
@@ -8203,7 +7958,6 @@ const FLAG_FOR_KEY$1 = {
|
|
|
8203
7958
|
[API_KEY_KEYS.password]: "--admin-password",
|
|
8204
7959
|
[API_KEY_KEYS.profile]: "--mb-profile",
|
|
8205
7960
|
[LICENSE_KEYS.token]: "--license-token or --skip-license",
|
|
8206
|
-
[DATABASE_KEYS.skip]: "--skip-databases",
|
|
8207
7961
|
[GIT_SYNC_KEYS.location]: "--git-dir",
|
|
8208
7962
|
[GIT_SYNC_KEYS.path]: "--git-dir",
|
|
8209
7963
|
[GIT_SYNC_KEYS.remote]: "--git-remote or --skip-git",
|
|
@@ -8211,7 +7965,7 @@ const FLAG_FOR_KEY$1 = {
|
|
|
8211
7965
|
[GIT_SYNC_KEYS.token]: "--git-token"
|
|
8212
7966
|
};
|
|
8213
7967
|
const DATABASE_FLAG_FOR_NAME = {
|
|
8214
|
-
engine: "--database
|
|
7968
|
+
engine: "--database",
|
|
8215
7969
|
uri: "--database"
|
|
8216
7970
|
};
|
|
8217
7971
|
function howToAnswer(key) {
|
|
@@ -8302,13 +8056,6 @@ function licenseSection(flags, secrets) {
|
|
|
8302
8056
|
}
|
|
8303
8057
|
return secrets.has("--license-token") ? { token: secrets.get("--license-token") } : void 0;
|
|
8304
8058
|
}
|
|
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
8059
|
function gitRemoteKind(flag, url) {
|
|
8313
8060
|
if (flag === void 0) return url === void 0 ? void 0 : "hosted";
|
|
8314
8061
|
const kind = GIT_REMOTE_NAMES[flag];
|
|
@@ -8429,7 +8176,7 @@ async function answersFromFlags(flags, readStdin) {
|
|
|
8429
8176
|
runtime: runtimeSection(flags),
|
|
8430
8177
|
"admin-user": adminSection(flags, secrets),
|
|
8431
8178
|
license: licenseSection(flags, secrets),
|
|
8432
|
-
databases:
|
|
8179
|
+
databases: flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0,
|
|
8433
8180
|
"git-sync": gitSection(flags, secrets),
|
|
8434
8181
|
"agents-skills": agentsSection(flags),
|
|
8435
8182
|
"api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
|
|
@@ -8854,6 +8601,7 @@ const dbAdd = defineRdeCommand({
|
|
|
8854
8601
|
description: "Engine of a --url that fits several engines; the error names them"
|
|
8855
8602
|
}
|
|
8856
8603
|
},
|
|
8604
|
+
interrupted: INTERRUPTED,
|
|
8857
8605
|
async run(args) {
|
|
8858
8606
|
if (args.url === void 0 && !stdinIsTty()) throw new ConfigError(NEEDS_URL_ERROR);
|
|
8859
8607
|
const name = await defaultInstanceName();
|
|
@@ -8885,6 +8633,7 @@ const dbList = defineRdeCommand({
|
|
|
8885
8633
|
description: "Print the list as JSON",
|
|
8886
8634
|
default: false
|
|
8887
8635
|
} },
|
|
8636
|
+
interrupted: INTERRUPTED,
|
|
8888
8637
|
async run(args) {
|
|
8889
8638
|
const name = await defaultInstanceName();
|
|
8890
8639
|
const state = await requireInstanceState(name);
|
|
@@ -9508,6 +9257,11 @@ async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
|
|
|
9508
9257
|
function isNotFound$1(error) {
|
|
9509
9258
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
|
|
9510
9259
|
}
|
|
9260
|
+
function coverageDetail(coverage) {
|
|
9261
|
+
const valid = coverage.included.length === 0 ? "valid" : `valid, with ${listOf(coverage.included, "conjunction")}`;
|
|
9262
|
+
if (coverage.missing.length === 0) return valid;
|
|
9263
|
+
return `${valid}; without ${listOf(coverage.missing, "disjunction")}`;
|
|
9264
|
+
}
|
|
9511
9265
|
async function licenseRow(client) {
|
|
9512
9266
|
const label = "license";
|
|
9513
9267
|
try {
|
|
@@ -9517,9 +9271,9 @@ async function licenseRow(client) {
|
|
|
9517
9271
|
hint: "`rde init --from license` with a valid token",
|
|
9518
9272
|
step: "license"
|
|
9519
9273
|
});
|
|
9520
|
-
return ok("license", label,
|
|
9274
|
+
return ok("license", label, coverageDetail(licensedFeatureCoverage(status.features)));
|
|
9521
9275
|
} catch (error) {
|
|
9522
|
-
if (isNotFound$1(error)) return skipped("license", label,
|
|
9276
|
+
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
9277
|
throw error;
|
|
9524
9278
|
}
|
|
9525
9279
|
}
|
|
@@ -9537,46 +9291,6 @@ async function databasesRow(state, client) {
|
|
|
9537
9291
|
const names = state.databases.map((record) => record.name).join(", ");
|
|
9538
9292
|
return ok("databases", label, `${state.databases.length} connected: ${names}`);
|
|
9539
9293
|
}
|
|
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
9294
|
function isMbFailure$1(error) {
|
|
9581
9295
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
9582
9296
|
}
|
|
@@ -9730,7 +9444,6 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
|
|
|
9730
9444
|
skipped("mb-profile", "mb profile", NO_INSTANCE_REASON),
|
|
9731
9445
|
skipped("license", "license", NO_INSTANCE_REASON),
|
|
9732
9446
|
skipped("databases", "databases", NO_INSTANCE_REASON),
|
|
9733
|
-
skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE_REASON),
|
|
9734
9447
|
skipped("git-sync", "remote sync", NO_INSTANCE_REASON)
|
|
9735
9448
|
];
|
|
9736
9449
|
const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
|
|
@@ -9749,7 +9462,6 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
|
|
|
9749
9462
|
const access = apiKey.access;
|
|
9750
9463
|
if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
|
|
9751
9464
|
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
9465
|
rows.push(await gitSyncRow(state, mbInstanceOf(state, secrets), deps));
|
|
9754
9466
|
return rows;
|
|
9755
9467
|
}
|
|
@@ -9759,7 +9471,7 @@ async function collectDoctor(instance, state, secrets, deps) {
|
|
|
9759
9471
|
const mb = await mbFinding(deps);
|
|
9760
9472
|
const profiles = sharedProfileList(deps);
|
|
9761
9473
|
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("
|
|
9474
|
+
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
9475
|
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
9764
9476
|
const rdeProfile = state === null ? null : state.mbProfile;
|
|
9765
9477
|
checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
|
|
@@ -9866,6 +9578,7 @@ const doctor = defineRdeCommand({
|
|
|
9866
9578
|
default: false
|
|
9867
9579
|
}
|
|
9868
9580
|
},
|
|
9581
|
+
interrupted: INTERRUPTED,
|
|
9869
9582
|
async run(args) {
|
|
9870
9583
|
const name = await instanceNameOrDefault();
|
|
9871
9584
|
const state = await readInstanceState(name);
|
|
@@ -9924,13 +9637,6 @@ function isLocation(value) {
|
|
|
9924
9637
|
function isRemote(value) {
|
|
9925
9638
|
return value === "local-bare" || value === "hosted";
|
|
9926
9639
|
}
|
|
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
9640
|
function applyAdminRecord(draft, key, value) {
|
|
9935
9641
|
if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
|
|
9936
9642
|
else if (key === ADMIN_USER_KEYS.password) admin(draft).password = envReference(ENV_ADMIN_PASSWORD);
|
|
@@ -9954,8 +9660,7 @@ function agentIds(values) {
|
|
|
9954
9660
|
function applyRecord(draft, record) {
|
|
9955
9661
|
const { key } = record;
|
|
9956
9662
|
const value = stringOf(record.answer);
|
|
9957
|
-
if (key.startsWith("
|
|
9958
|
-
else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
9663
|
+
if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
9959
9664
|
else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
|
|
9960
9665
|
else if (key === AGENTS_SKILLS_KEYS.agents && record.answer.kind === "multiselect") draft.answers["agents-skills"] = { agents: agentIds(record.answer.values) };
|
|
9961
9666
|
else if (key === API_KEY_KEYS.profile && value !== null) {
|
|
@@ -9981,13 +9686,9 @@ function databaseEntry(added, index) {
|
|
|
9981
9686
|
};
|
|
9982
9687
|
}
|
|
9983
9688
|
function answersFromRecords(records, overrides, added) {
|
|
9984
|
-
const draft = {
|
|
9985
|
-
answers: {},
|
|
9986
|
-
databasesFinished: false
|
|
9987
|
-
};
|
|
9689
|
+
const draft = { answers: {} };
|
|
9988
9690
|
for (const record of records) applyRecord(draft, record);
|
|
9989
|
-
|
|
9990
|
-
if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
|
|
9691
|
+
if (added.length > 0) draft.answers.databases = added.map(databaseEntry);
|
|
9991
9692
|
return {
|
|
9992
9693
|
...draft.answers,
|
|
9993
9694
|
...overrides.runtime !== void 0 && { runtime: overrides.runtime },
|
|
@@ -10113,7 +9814,7 @@ const init = defineRdeCommand({
|
|
|
10113
9814
|
},
|
|
10114
9815
|
runtime: {
|
|
10115
9816
|
type: "string",
|
|
10116
|
-
description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available
|
|
9817
|
+
description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available`
|
|
10117
9818
|
},
|
|
10118
9819
|
image: {
|
|
10119
9820
|
type: "string",
|
|
@@ -10158,12 +9859,7 @@ const init = defineRdeCommand({
|
|
|
10158
9859
|
},
|
|
10159
9860
|
database: {
|
|
10160
9861
|
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
|
|
9862
|
+
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
9863
|
},
|
|
10168
9864
|
"git-dir": {
|
|
10169
9865
|
type: "string",
|
|
@@ -10201,10 +9897,11 @@ const init = defineRdeCommand({
|
|
|
10201
9897
|
},
|
|
10202
9898
|
yes: {
|
|
10203
9899
|
type: "boolean",
|
|
10204
|
-
description: "Answer yes to every confirmation and take the default of every setting;
|
|
9900
|
+
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
9901
|
default: false
|
|
10206
9902
|
}
|
|
10207
9903
|
},
|
|
9904
|
+
interrupted: "Interrupted. Run `rde init` again to resume.",
|
|
10208
9905
|
async run(args, rawArgs) {
|
|
10209
9906
|
const options = stepSelection(args.from, args.only);
|
|
10210
9907
|
const runOptions = {
|
|
@@ -10221,7 +9918,6 @@ const init = defineRdeCommand({
|
|
|
10221
9918
|
licenseToken: args["license-token"],
|
|
10222
9919
|
skipLicense: args["skip-license"],
|
|
10223
9920
|
database: repeatedValues(rawArgs, "--database"),
|
|
10224
|
-
skipDatabases: args["skip-databases"],
|
|
10225
9921
|
gitDir: args["git-dir"],
|
|
10226
9922
|
gitRemote: args["git-remote"],
|
|
10227
9923
|
gitUrl: args["git-url"],
|
|
@@ -10268,6 +9964,7 @@ const logs = defineRdeCommand({
|
|
|
10268
9964
|
description: "Keep streaming new lines",
|
|
10269
9965
|
default: false
|
|
10270
9966
|
} },
|
|
9967
|
+
interrupted: null,
|
|
10271
9968
|
async run(args) {
|
|
10272
9969
|
await (await loadInstance(await defaultInstanceName(), interruptSignal)).runtime.logs({
|
|
10273
9970
|
follow: args.follow,
|
|
@@ -10282,6 +9979,7 @@ const open = defineRdeCommand({
|
|
|
10282
9979
|
name: "open",
|
|
10283
9980
|
description: "Open the Metabase instance in the browser"
|
|
10284
9981
|
},
|
|
9982
|
+
interrupted: INTERRUPTED,
|
|
10285
9983
|
async run() {
|
|
10286
9984
|
const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
|
|
10287
9985
|
printLines([await openInstance(instance.url) ? `Opened ${instance.url}` : `Open ${instance.url} in your browser`]);
|
|
@@ -10361,6 +10059,7 @@ const start = defineRdeCommand({
|
|
|
10361
10059
|
type: "string",
|
|
10362
10060
|
description: "Move the instance to this host port first; the mb profile follows, the data stays"
|
|
10363
10061
|
} },
|
|
10062
|
+
interrupted: INTERRUPTED,
|
|
10364
10063
|
async run(args) {
|
|
10365
10064
|
const port = args.port === void 0 ? null : requirePortFlag(args.port);
|
|
10366
10065
|
const name = await defaultInstanceName();
|
|
@@ -10422,19 +10121,6 @@ async function databasesOf(ctx, deps, health) {
|
|
|
10422
10121
|
throw error;
|
|
10423
10122
|
}
|
|
10424
10123
|
}
|
|
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
10124
|
function taskLabel(status) {
|
|
10439
10125
|
const task = status.current_task;
|
|
10440
10126
|
return task === null ? null : `${task.sync_task_type} ${task.status}`;
|
|
@@ -10489,7 +10175,6 @@ async function collectStatus(ctx, deps) {
|
|
|
10489
10175
|
steps,
|
|
10490
10176
|
setup: setupOf(ctx, steps),
|
|
10491
10177
|
databases,
|
|
10492
|
-
sampleDatabase: await sampleDatabaseOf(ctx, deps, databases),
|
|
10493
10178
|
gitSync: await gitSyncOf(ctx, deps),
|
|
10494
10179
|
contentRepository: ctx.state.gitRemote === null ? null : ctx.state.gitRemote.workdir
|
|
10495
10180
|
};
|
|
@@ -10533,22 +10218,6 @@ function syncStateLabel(profile, gitSync) {
|
|
|
10533
10218
|
if (gitSync.dirty) return `unsynced changes, ${remote}`;
|
|
10534
10219
|
return `${remote}; \`${mbCommand(profile)} git-sync import\``;
|
|
10535
10220
|
}
|
|
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
10221
|
function databaseLines(report) {
|
|
10553
10222
|
if (report.databases === null) return ["Databases: unavailable while Metabase is not running"];
|
|
10554
10223
|
return ["Databases:", ...databaseListingLines(report.databases).map((line) => ` ${line}`)];
|
|
@@ -10565,7 +10234,6 @@ function statusLines(report) {
|
|
|
10565
10234
|
"Steps:",
|
|
10566
10235
|
...checklistLines(report.steps).map((line) => ` ${line}`),
|
|
10567
10236
|
...databaseLines(report),
|
|
10568
|
-
sampleDatabaseLine(report),
|
|
10569
10237
|
gitSyncLine(report),
|
|
10570
10238
|
...report.contentRepository === null ? [] : [`Content repository: ${report.contentRepository}`]
|
|
10571
10239
|
];
|
|
@@ -10582,6 +10250,7 @@ const status = defineRdeCommand({
|
|
|
10582
10250
|
description: "Print the report as JSON",
|
|
10583
10251
|
default: false
|
|
10584
10252
|
} },
|
|
10253
|
+
interrupted: INTERRUPTED,
|
|
10585
10254
|
async run(args) {
|
|
10586
10255
|
const name = await defaultInstanceName();
|
|
10587
10256
|
await requireInstanceState(name);
|
|
@@ -10598,18 +10267,17 @@ const status = defineRdeCommand({
|
|
|
10598
10267
|
const stop = defineRdeCommand({
|
|
10599
10268
|
meta: {
|
|
10600
10269
|
name: "stop",
|
|
10601
|
-
description: "Stop the Metabase instance
|
|
10270
|
+
description: "Stop the Metabase instance"
|
|
10602
10271
|
},
|
|
10272
|
+
interrupted: INTERRUPTED,
|
|
10603
10273
|
async run() {
|
|
10604
|
-
const
|
|
10274
|
+
const name = await defaultInstanceName();
|
|
10275
|
+
const instance = await loadInstance(name, interruptSignal);
|
|
10605
10276
|
const before = await instance.runtime.status();
|
|
10606
10277
|
await instance.runtime.stop();
|
|
10607
10278
|
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
|
-
}
|
|
10279
|
+
const removed = await removeLeftovers(name, interruptSignal);
|
|
10280
|
+
if (removed !== null) lines.push(removed);
|
|
10613
10281
|
printLines(lines);
|
|
10614
10282
|
}
|
|
10615
10283
|
});
|
|
@@ -10620,11 +10288,13 @@ function hostUninstallDeps(signal, childOutput) {
|
|
|
10620
10288
|
return {
|
|
10621
10289
|
docker: dockerTool(runner),
|
|
10622
10290
|
daemon: daemonTool(runner),
|
|
10291
|
+
git: gitTool(runner),
|
|
10623
10292
|
runner,
|
|
10624
10293
|
childOutput
|
|
10625
10294
|
};
|
|
10626
10295
|
}
|
|
10627
10296
|
const NOT_TOUCHED_LINE = "The mb CLI, Docker, and Java themselves";
|
|
10297
|
+
const UNPULLED_COMMITS_SHOWN = 10;
|
|
10628
10298
|
const ANY_JAR = "-jar";
|
|
10629
10299
|
function isFileSystemError(error) {
|
|
10630
10300
|
return error instanceof Error && "code" in error;
|
|
@@ -10830,6 +10500,71 @@ async function findSkill(runner, childOutput) {
|
|
|
10830
10500
|
function instanceDirLabel(path, state) {
|
|
10831
10501
|
return `Instance directory ${path} (${state?.gitRemote?.kind === "local-bare" ? "Metabase data and the remote-sync repository content.git" : "Metabase data"})`;
|
|
10832
10502
|
}
|
|
10503
|
+
async function findUnpulled(remote, git) {
|
|
10504
|
+
if (!await pathExists(remote.barePath)) return null;
|
|
10505
|
+
try {
|
|
10506
|
+
if (!await git.isBareRepo(remote.barePath)) return null;
|
|
10507
|
+
const commits = await git.bareBranchCommits(remote.barePath, remote.branch);
|
|
10508
|
+
if (commits === null || commits.length === 0) return null;
|
|
10509
|
+
if (!await git.isRepo(remote.workdir)) return {
|
|
10510
|
+
kind: "orphaned",
|
|
10511
|
+
remote,
|
|
10512
|
+
commits
|
|
10513
|
+
};
|
|
10514
|
+
const reachable = await git.reachableCommits(remote.workdir);
|
|
10515
|
+
const missing = commits.filter((commit) => !reachable.has(commit.sha));
|
|
10516
|
+
return missing.length === 0 ? null : {
|
|
10517
|
+
kind: "behind",
|
|
10518
|
+
remote,
|
|
10519
|
+
commits: missing
|
|
10520
|
+
};
|
|
10521
|
+
} catch (error) {
|
|
10522
|
+
if (isToolFailure(error)) return {
|
|
10523
|
+
kind: "unchecked",
|
|
10524
|
+
remote,
|
|
10525
|
+
reason: error.message
|
|
10526
|
+
};
|
|
10527
|
+
throw error;
|
|
10528
|
+
}
|
|
10529
|
+
}
|
|
10530
|
+
function commitCount(commits) {
|
|
10531
|
+
return commits.length === 1 ? "1 commit" : `${commits.length} commits`;
|
|
10532
|
+
}
|
|
10533
|
+
function unpulledSummary(unpulled) {
|
|
10534
|
+
const { barePath, workdir, branch } = unpulled.remote;
|
|
10535
|
+
switch (unpulled.kind) {
|
|
10536
|
+
case "behind": return `${barePath} has ${commitCount(unpulled.commits)} on ${branch} that ${workdir} lacks; uninstalling deletes them`;
|
|
10537
|
+
case "orphaned": return `${barePath} has ${commitCount(unpulled.commits)} on ${branch}, and ${workdir} is gone or no longer a git repository; uninstalling deletes them`;
|
|
10538
|
+
case "unchecked": return `${barePath} cannot be compared with ${workdir} (${unpulled.reason}); uninstalling deletes it with any commits ${workdir} lacks`;
|
|
10539
|
+
}
|
|
10540
|
+
}
|
|
10541
|
+
function unpulledRemedy(unpulled) {
|
|
10542
|
+
const { barePath, workdir, branch } = unpulled.remote;
|
|
10543
|
+
switch (unpulled.kind) {
|
|
10544
|
+
case "behind": return `\`git -C ${workdir} pull\` brings them in first`;
|
|
10545
|
+
case "orphaned": return `\`git clone --branch ${branch} ${barePath} <directory>\` copies them first`;
|
|
10546
|
+
case "unchecked": return null;
|
|
10547
|
+
}
|
|
10548
|
+
}
|
|
10549
|
+
function commitLines(unpulled) {
|
|
10550
|
+
if (unpulled.kind === "unchecked") return [];
|
|
10551
|
+
const shown = unpulled.commits.slice(0, UNPULLED_COMMITS_SHOWN).map((commit) => ` ${commit.shortSha} ${commit.subject}`);
|
|
10552
|
+
const hidden = unpulled.commits.length - shown.length;
|
|
10553
|
+
return hidden > 0 ? [...shown, ` and ${hidden} more`] : shown;
|
|
10554
|
+
}
|
|
10555
|
+
function unpulledWarning(unpulled) {
|
|
10556
|
+
const remedy = unpulledRemedy(unpulled);
|
|
10557
|
+
return [
|
|
10558
|
+
unpulledSummary(unpulled),
|
|
10559
|
+
...commitLines(unpulled),
|
|
10560
|
+
...remedy === null ? [] : [remedy]
|
|
10561
|
+
].join("\n");
|
|
10562
|
+
}
|
|
10563
|
+
function unpulledRefusal(unpulled) {
|
|
10564
|
+
const remedy = unpulledRemedy(unpulled);
|
|
10565
|
+
const force = "pass --force to uninstall anyway";
|
|
10566
|
+
return remedy === null ? `${unpulledSummary(unpulled)}; ${force}` : `${unpulledSummary(unpulled)}; ${remedy}, or ${force}`;
|
|
10567
|
+
}
|
|
10833
10568
|
async function findConfigEntry(instance) {
|
|
10834
10569
|
const path = globalConfigPath();
|
|
10835
10570
|
try {
|
|
@@ -10872,14 +10607,13 @@ async function planUninstall(options, deps) {
|
|
|
10872
10607
|
const container = await findContainer(runtime?.containerName ?? containerName(options.instance, home), deps.docker, runtime?.image ?? null, home, { graceMs: METABASE_STOP_GRACE_MS });
|
|
10873
10608
|
if (container.removal !== null) removals.push(container.removal);
|
|
10874
10609
|
if (container.problem !== null) problems.push(container.problem);
|
|
10875
|
-
const
|
|
10876
|
-
|
|
10877
|
-
if (
|
|
10878
|
-
if (sidecar.problem !== null) problems.push(sidecar.problem);
|
|
10610
|
+
const sampleDatabase = await findContainer(sampleDatabaseContainerName(options.instance, home), deps.docker, null, home, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
|
|
10611
|
+
if (sampleDatabase.removal !== null) removals.push(sampleDatabase.removal);
|
|
10612
|
+
if (sampleDatabase.problem !== null) problems.push(sampleDatabase.problem);
|
|
10879
10613
|
const jarSource = state?.runtime?.kind === "jar" ? state.runtime.source : null;
|
|
10880
10614
|
const jarProcess = await findJarProcess(options.instance, jarSource, deps.daemon);
|
|
10881
10615
|
if (jarProcess !== null) removals.push(jarProcess);
|
|
10882
|
-
for (const image of [container.image,
|
|
10616
|
+
for (const image of [container.image, sampleDatabase.image]) {
|
|
10883
10617
|
if (image === null) continue;
|
|
10884
10618
|
const finding = await findImage(image, deps.docker, options.removeImage);
|
|
10885
10619
|
if (finding.removal !== null) removals.push(finding.removal);
|
|
@@ -10899,6 +10633,8 @@ async function planUninstall(options, deps) {
|
|
|
10899
10633
|
}
|
|
10900
10634
|
const dir = instanceDir(options.instance);
|
|
10901
10635
|
const dirPresent = await pathExists(dir);
|
|
10636
|
+
const bareRemote = state?.gitRemote?.kind === "local-bare" ? state.gitRemote : null;
|
|
10637
|
+
const unpulled = options.keepData || !dirPresent || bareRemote === null ? null : await findUnpulled(bareRemote, deps.git);
|
|
10902
10638
|
if (!options.keepData) {
|
|
10903
10639
|
const secrets = instanceSecretsPath(options.instance);
|
|
10904
10640
|
if (await pathExists(secrets)) removals.push({
|
|
@@ -10919,7 +10655,8 @@ async function planUninstall(options, deps) {
|
|
|
10919
10655
|
return {
|
|
10920
10656
|
removals,
|
|
10921
10657
|
kept,
|
|
10922
|
-
problems
|
|
10658
|
+
problems,
|
|
10659
|
+
unpulled
|
|
10923
10660
|
};
|
|
10924
10661
|
}
|
|
10925
10662
|
function planLines$1(plan) {
|
|
@@ -10974,6 +10711,10 @@ async function uninstall(options, deps, io) {
|
|
|
10974
10711
|
};
|
|
10975
10712
|
}
|
|
10976
10713
|
io.ui.note(planLines$1(plan).join("\n"), `rde uninstall ${options.instance}`);
|
|
10714
|
+
if (plan.unpulled !== null) {
|
|
10715
|
+
io.ui.warn(unpulledWarning(plan.unpulled));
|
|
10716
|
+
if ((options.yes || !options.terminal) && !options.force) throw new ConfigError(unpulledRefusal(plan.unpulled));
|
|
10717
|
+
}
|
|
10977
10718
|
if (!options.yes) {
|
|
10978
10719
|
if (!await io.prompts.confirm({
|
|
10979
10720
|
message: "Remove everything listed?",
|
|
@@ -11020,8 +10761,14 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11020
10761
|
type: "boolean",
|
|
11021
10762
|
description: "Do not ask for confirmation",
|
|
11022
10763
|
default: false
|
|
10764
|
+
},
|
|
10765
|
+
force: {
|
|
10766
|
+
type: "boolean",
|
|
10767
|
+
description: "With --yes or without a terminal, also delete commits your working directory has not pulled",
|
|
10768
|
+
default: false
|
|
11023
10769
|
}
|
|
11024
10770
|
},
|
|
10771
|
+
interrupted: INTERRUPTED,
|
|
11025
10772
|
async run(args) {
|
|
11026
10773
|
const instance = args.instance ?? await instanceNameOrDefault();
|
|
11027
10774
|
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 +10778,9 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11031
10778
|
keepData: args["keep-data"],
|
|
11032
10779
|
removeSkill: args["remove-skill"],
|
|
11033
10780
|
removeImage: args["remove-image"],
|
|
11034
|
-
yes: args.yes
|
|
10781
|
+
yes: args.yes,
|
|
10782
|
+
force: args.force,
|
|
10783
|
+
terminal: stdinIsTty()
|
|
11035
10784
|
}, hostUninstallDeps(interruptSignal, ui.childOutput), {
|
|
11036
10785
|
prompts: clackPrompts({ confirmsWithYes: true }),
|
|
11037
10786
|
ui
|
|
@@ -11265,8 +11014,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11265
11014
|
const stopping = ctx.ui.spinner("Stopping Metabase");
|
|
11266
11015
|
await runtime.stop();
|
|
11267
11016
|
stopping.stop("Metabase stopped");
|
|
11017
|
+
options.onPhase("backing-up", backup);
|
|
11268
11018
|
await backUp(ctx, resolved.runtime.dataDir, backup);
|
|
11019
|
+
options.onPhase("switching", backup);
|
|
11269
11020
|
await switchTo(ctx, resolved, target);
|
|
11021
|
+
options.onPhase("starting", backup);
|
|
11270
11022
|
const to = await startUpgraded(ctx, resolved, client, configured, backup);
|
|
11271
11023
|
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
11272
11024
|
return {
|
|
@@ -11278,6 +11030,14 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11278
11030
|
}
|
|
11279
11031
|
//#endregion
|
|
11280
11032
|
//#region src/commands/upgrade.ts
|
|
11033
|
+
const INTERRUPTED_IN = {
|
|
11034
|
+
"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.`,
|
|
11035
|
+
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.`,
|
|
11036
|
+
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.`
|
|
11037
|
+
};
|
|
11038
|
+
function interruptedUpgrade(phase, backup) {
|
|
11039
|
+
return INTERRUPTED_IN[phase](backup);
|
|
11040
|
+
}
|
|
11281
11041
|
const upgradeCommand = defineRdeCommand({
|
|
11282
11042
|
meta: {
|
|
11283
11043
|
name: "upgrade",
|
|
@@ -11294,14 +11054,21 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11294
11054
|
default: false
|
|
11295
11055
|
}
|
|
11296
11056
|
},
|
|
11297
|
-
|
|
11057
|
+
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.",
|
|
11058
|
+
async run(args, _rawArgs, interruption) {
|
|
11298
11059
|
if (!args.yes && !stdinIsTty()) throw new ConfigError("`rde upgrade` asks for confirmation; pass --yes to upgrade without a terminal");
|
|
11299
11060
|
const name = await defaultInstanceName();
|
|
11300
11061
|
await requireInstanceState(name);
|
|
11301
|
-
|
|
11062
|
+
const ctx = await createWizardContext(name, { signal: interruptSignal });
|
|
11063
|
+
await upgrade(ctx, {
|
|
11302
11064
|
to: args.to ?? null,
|
|
11303
|
-
yes: args.yes
|
|
11065
|
+
yes: args.yes,
|
|
11066
|
+
onPhase: (phase, backup) => {
|
|
11067
|
+
interruption.message = interruptedUpgrade(phase, backup);
|
|
11068
|
+
}
|
|
11304
11069
|
});
|
|
11070
|
+
const removed = await removeLeftovers(name, interruptSignal);
|
|
11071
|
+
if (removed !== null) ctx.ui.info(removed);
|
|
11305
11072
|
}
|
|
11306
11073
|
});
|
|
11307
11074
|
//#endregion
|
|
@@ -11309,7 +11076,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11309
11076
|
const main = defineCommand({
|
|
11310
11077
|
meta: {
|
|
11311
11078
|
name: "rde",
|
|
11312
|
-
version: "0.1.
|
|
11079
|
+
version: "0.1.16",
|
|
11313
11080
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
11314
11081
|
},
|
|
11315
11082
|
subCommands: {
|