@metabase/rde 0.1.14 → 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 +694 -856
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -50,6 +50,7 @@ var StepFailedError = class extends Error {
|
|
|
50
50
|
var ExternalToolError = class extends Error {
|
|
51
51
|
tool;
|
|
52
52
|
argv;
|
|
53
|
+
envNames;
|
|
53
54
|
exitCode;
|
|
54
55
|
stderrTail;
|
|
55
56
|
constructor(failure) {
|
|
@@ -57,15 +58,21 @@ var ExternalToolError = class extends Error {
|
|
|
57
58
|
this.name = "ExternalToolError";
|
|
58
59
|
this.tool = failure.tool;
|
|
59
60
|
this.argv = failure.argv;
|
|
61
|
+
this.envNames = failure.envNames;
|
|
60
62
|
this.exitCode = failure.exitCode;
|
|
61
63
|
this.stderrTail = failure.stderrTail;
|
|
62
64
|
}
|
|
63
65
|
};
|
|
66
|
+
function commandLabel(failure) {
|
|
67
|
+
const command = `\`${[failure.tool, ...failure.argv].join(" ")}\``;
|
|
68
|
+
if (failure.envNames.length === 0) return command;
|
|
69
|
+
return `${command}, run by rde with ${failure.envNames.join(" and ")} set,`;
|
|
70
|
+
}
|
|
64
71
|
function describeToolFailure(failure) {
|
|
65
|
-
const command =
|
|
72
|
+
const command = commandLabel(failure);
|
|
66
73
|
const exit = failure.exitCode === null ? "was killed" : `exited with ${failure.exitCode}`;
|
|
67
74
|
const tail = failure.stderrTail.trim();
|
|
68
|
-
return tail.length === 0 ?
|
|
75
|
+
return tail.length === 0 ? `${command} ${exit}` : `${command} ${exit}: ${tail}`;
|
|
69
76
|
}
|
|
70
77
|
var MetabaseApiError = class extends Error {
|
|
71
78
|
status;
|
|
@@ -103,6 +110,9 @@ var MetabaseUnreachableError = class extends Error {
|
|
|
103
110
|
this.reason = reason;
|
|
104
111
|
}
|
|
105
112
|
};
|
|
113
|
+
function isMetabaseNotServing(error) {
|
|
114
|
+
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
115
|
+
}
|
|
106
116
|
var DownloadError = class extends Error {
|
|
107
117
|
url;
|
|
108
118
|
constructor(url, reason) {
|
|
@@ -113,12 +123,10 @@ var DownloadError = class extends Error {
|
|
|
113
123
|
};
|
|
114
124
|
//#endregion
|
|
115
125
|
//#region src/output/error.ts
|
|
116
|
-
const INTERRUPTED_MESSAGE = "Interrupted. Run `rde init` again to resume.";
|
|
117
126
|
const FAILURE_EXIT_CODE = 1;
|
|
118
127
|
const USAGE_EXIT_CODE = 2;
|
|
119
128
|
function exitCodeFor(error) {
|
|
120
129
|
if (error instanceof StepFailedError) return exitCodeFor(error.cause);
|
|
121
|
-
if (error instanceof AbortError) return 130;
|
|
122
130
|
if (error instanceof ConfigError) return USAGE_EXIT_CODE;
|
|
123
131
|
return FAILURE_EXIT_CODE;
|
|
124
132
|
}
|
|
@@ -130,7 +138,6 @@ function stepFailureMessage(step, cause) {
|
|
|
130
138
|
}
|
|
131
139
|
function messageFor$1(error) {
|
|
132
140
|
if (error instanceof StepFailedError) return stepFailureMessage(error.step, messageFor$1(error.cause));
|
|
133
|
-
if (error instanceof AbortError) return INTERRUPTED_MESSAGE;
|
|
134
141
|
if (error instanceof Error) return error.message;
|
|
135
142
|
return String(error);
|
|
136
143
|
}
|
|
@@ -138,10 +145,15 @@ function reportError(error) {
|
|
|
138
145
|
process.stderr.write(messageFor$1(error) + "\n");
|
|
139
146
|
process.exitCode = exitCodeFor(error);
|
|
140
147
|
}
|
|
148
|
+
function reportInterrupted(message) {
|
|
149
|
+
if (message !== null) process.stderr.write(message + "\n");
|
|
150
|
+
process.exitCode = 130;
|
|
151
|
+
}
|
|
141
152
|
//#endregion
|
|
142
153
|
//#region src/commands/define.ts
|
|
143
154
|
const SHORT_PREFIX = "-";
|
|
144
155
|
const END_OF_OPTIONS = "--";
|
|
156
|
+
const INTERRUPTED = "Interrupted.";
|
|
145
157
|
function repeatedValues(rawArgs, flag) {
|
|
146
158
|
const values = [];
|
|
147
159
|
const inline = `${flag}=`;
|
|
@@ -164,9 +176,14 @@ function defineRdeCommand(def) {
|
|
|
164
176
|
meta: def.meta,
|
|
165
177
|
...def.args !== void 0 && { args: def.args },
|
|
166
178
|
async run({ args, rawArgs }) {
|
|
179
|
+
const interruption = { message: def.interrupted };
|
|
167
180
|
try {
|
|
168
|
-
await def.run(args, rawArgs);
|
|
181
|
+
await def.run(args, rawArgs, interruption);
|
|
169
182
|
} catch (error) {
|
|
183
|
+
if (error instanceof AbortError) {
|
|
184
|
+
reportInterrupted(interruption.message);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
170
187
|
reportError(error);
|
|
171
188
|
}
|
|
172
189
|
}
|
|
@@ -245,7 +262,6 @@ const StepId = z.enum([
|
|
|
245
262
|
"admin-user",
|
|
246
263
|
"api-key",
|
|
247
264
|
"transforms",
|
|
248
|
-
"sample-database",
|
|
249
265
|
"license",
|
|
250
266
|
"git-sync",
|
|
251
267
|
"databases",
|
|
@@ -322,12 +338,6 @@ const DatabaseRecord = z.object({
|
|
|
322
338
|
engine: z.string(),
|
|
323
339
|
createdAt: z.string()
|
|
324
340
|
});
|
|
325
|
-
const SampleDatabaseState = z.object({
|
|
326
|
-
containerName: z.string(),
|
|
327
|
-
image: z.string(),
|
|
328
|
-
port: z.int().positive(),
|
|
329
|
-
databaseId: z.int().nullable()
|
|
330
|
-
});
|
|
331
341
|
const GitRemoteState = z.discriminatedUnion("kind", [z.object({
|
|
332
342
|
kind: z.literal("local-bare"),
|
|
333
343
|
barePath: z.string(),
|
|
@@ -344,7 +354,7 @@ const StepRecord = z.object({
|
|
|
344
354
|
completedAt: z.string()
|
|
345
355
|
});
|
|
346
356
|
const InstanceState = z.object({
|
|
347
|
-
schemaVersion: z.literal(
|
|
357
|
+
schemaVersion: z.literal(6),
|
|
348
358
|
name: z.string(),
|
|
349
359
|
runtime: RuntimeState.nullable(),
|
|
350
360
|
url: z.string().nullable(),
|
|
@@ -353,7 +363,6 @@ const InstanceState = z.object({
|
|
|
353
363
|
mbProfile: z.string(),
|
|
354
364
|
license: LicenseState,
|
|
355
365
|
databases: z.array(DatabaseRecord),
|
|
356
|
-
sampleDatabase: SampleDatabaseState.nullable(),
|
|
357
366
|
gitRemote: GitRemoteState.nullable(),
|
|
358
367
|
movedFrom: z.string().nullable(),
|
|
359
368
|
steps: z.partialRecord(StepId, StepRecord)
|
|
@@ -385,7 +394,7 @@ function mbCommand(profile) {
|
|
|
385
394
|
}
|
|
386
395
|
function emptyInstanceState(name) {
|
|
387
396
|
return {
|
|
388
|
-
schemaVersion:
|
|
397
|
+
schemaVersion: 6,
|
|
389
398
|
name,
|
|
390
399
|
runtime: null,
|
|
391
400
|
url: null,
|
|
@@ -394,7 +403,6 @@ function emptyInstanceState(name) {
|
|
|
394
403
|
mbProfile: mbProfileFor(name),
|
|
395
404
|
license: { activated: false },
|
|
396
405
|
databases: [],
|
|
397
|
-
sampleDatabase: null,
|
|
398
406
|
gitRemote: null,
|
|
399
407
|
movedFrom: null,
|
|
400
408
|
steps: {}
|
|
@@ -407,6 +415,7 @@ const V1 = 1;
|
|
|
407
415
|
const V2 = 2;
|
|
408
416
|
const V3 = 3;
|
|
409
417
|
const V4 = 4;
|
|
418
|
+
const V5 = 5;
|
|
410
419
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
411
420
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
412
421
|
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
@@ -418,6 +427,7 @@ const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
|
418
427
|
password: z.string()
|
|
419
428
|
})]);
|
|
420
429
|
const StateV4 = z.object({ schemaVersion: z.literal(V4) }).loose();
|
|
430
|
+
const StateV5 = z.object({ schemaVersion: z.literal(V5) }).loose();
|
|
421
431
|
const StateV3 = z.object({
|
|
422
432
|
schemaVersion: z.literal(V3),
|
|
423
433
|
admin: AdminV3.nullable(),
|
|
@@ -433,17 +443,23 @@ function liftV1(raw) {
|
|
|
433
443
|
function liftV2(raw) {
|
|
434
444
|
return {
|
|
435
445
|
...StateV2.parse(raw),
|
|
436
|
-
schemaVersion: V3
|
|
437
|
-
sampleDatabase: null
|
|
446
|
+
schemaVersion: V3
|
|
438
447
|
};
|
|
439
448
|
}
|
|
440
449
|
function liftV4(raw) {
|
|
441
450
|
return {
|
|
442
451
|
...StateV4.parse(raw),
|
|
443
|
-
schemaVersion:
|
|
452
|
+
schemaVersion: V5,
|
|
444
453
|
movedFrom: null
|
|
445
454
|
};
|
|
446
455
|
}
|
|
456
|
+
function liftV5(raw) {
|
|
457
|
+
const { sampleDatabase: _sampleDatabase, ...state } = StateV5.parse(raw);
|
|
458
|
+
return {
|
|
459
|
+
...state,
|
|
460
|
+
schemaVersion: 6
|
|
461
|
+
};
|
|
462
|
+
}
|
|
447
463
|
function adminV4(admin) {
|
|
448
464
|
switch (admin.kind) {
|
|
449
465
|
case "default": return {
|
|
@@ -463,11 +479,11 @@ function liftV3(raw) {
|
|
|
463
479
|
password: admin.password
|
|
464
480
|
} : null;
|
|
465
481
|
return {
|
|
466
|
-
state:
|
|
482
|
+
state: migrateInstanceState(liftV4({
|
|
467
483
|
...rest,
|
|
468
484
|
schemaVersion: V4,
|
|
469
485
|
admin: admin === null ? null : adminV4(admin)
|
|
470
|
-
})),
|
|
486
|
+
})).state,
|
|
471
487
|
movedSecrets: {
|
|
472
488
|
schemaVersion: 1,
|
|
473
489
|
admin: login,
|
|
@@ -481,15 +497,16 @@ function migrateInstanceState(raw) {
|
|
|
481
497
|
case V1: return migrateInstanceState(liftV1(raw));
|
|
482
498
|
case V2: return migrateInstanceState(liftV2(raw));
|
|
483
499
|
case V3: return liftV3(raw);
|
|
484
|
-
case V4: return
|
|
485
|
-
|
|
500
|
+
case V4: return migrateInstanceState(liftV4(raw));
|
|
501
|
+
case V5: return {
|
|
502
|
+
state: InstanceState.parse(liftV5(raw)),
|
|
486
503
|
movedSecrets: null
|
|
487
504
|
};
|
|
488
|
-
case
|
|
505
|
+
case 6: return {
|
|
489
506
|
state: InstanceState.parse(raw),
|
|
490
507
|
movedSecrets: null
|
|
491
508
|
};
|
|
492
|
-
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)`);
|
|
493
510
|
}
|
|
494
511
|
}
|
|
495
512
|
//#endregion
|
|
@@ -1348,9 +1365,6 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
|
1348
1365
|
user: userResource(http)
|
|
1349
1366
|
};
|
|
1350
1367
|
}
|
|
1351
|
-
function isNotReadyYet(error) {
|
|
1352
|
-
return error instanceof MetabaseInitializingError || error instanceof MetabaseUnreachableError;
|
|
1353
|
-
}
|
|
1354
1368
|
async function waitForHealth(client, options) {
|
|
1355
1369
|
const deadline = Date.now() + options.timeoutMs;
|
|
1356
1370
|
const interval = options.pollIntervalMs ?? HEALTH_POLL_INTERVAL_MS;
|
|
@@ -1359,7 +1373,7 @@ async function waitForHealth(client, options) {
|
|
|
1359
1373
|
await client.health.get();
|
|
1360
1374
|
return;
|
|
1361
1375
|
} catch (error) {
|
|
1362
|
-
if (!
|
|
1376
|
+
if (!isMetabaseNotServing(error)) throw error;
|
|
1363
1377
|
if (error instanceof MetabaseInitializingError) options.onProgress(error.progress);
|
|
1364
1378
|
}
|
|
1365
1379
|
if (!await options.isRunning()) throw new MetabaseUnreachableError(client.baseUrl, "it exited before it became healthy");
|
|
@@ -1386,15 +1400,12 @@ function storedApiKey(state, secrets) {
|
|
|
1386
1400
|
function hostCredentialsClient(signal) {
|
|
1387
1401
|
return (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal);
|
|
1388
1402
|
}
|
|
1389
|
-
function isNotReady(error) {
|
|
1390
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
1391
|
-
}
|
|
1392
1403
|
async function hasAdminOutsideRde(state, client) {
|
|
1393
1404
|
if (state.url === null) return false;
|
|
1394
1405
|
try {
|
|
1395
1406
|
return (await client(state.url, NO_AUTH).session.properties())["has-user-setup"];
|
|
1396
1407
|
} catch (error) {
|
|
1397
|
-
if (
|
|
1408
|
+
if (isMetabaseNotServing(error)) return false;
|
|
1398
1409
|
throw error;
|
|
1399
1410
|
}
|
|
1400
1411
|
}
|
|
@@ -1475,6 +1486,7 @@ const credentials = defineRdeCommand({
|
|
|
1475
1486
|
default: false
|
|
1476
1487
|
}
|
|
1477
1488
|
},
|
|
1489
|
+
interrupted: INTERRUPTED,
|
|
1478
1490
|
async run(args) {
|
|
1479
1491
|
const name = await defaultInstanceName();
|
|
1480
1492
|
const state = await requireInstanceState(name);
|
|
@@ -1830,10 +1842,11 @@ function signalProcess(pid, signal) {
|
|
|
1830
1842
|
function stderrTail(stderr) {
|
|
1831
1843
|
return stderr.trim().split("\n").slice(-5).join("\n");
|
|
1832
1844
|
}
|
|
1833
|
-
function toolFailure(tool, argv, result) {
|
|
1845
|
+
function toolFailure(tool, argv, result, options = {}) {
|
|
1834
1846
|
return new ExternalToolError({
|
|
1835
1847
|
tool,
|
|
1836
1848
|
argv,
|
|
1849
|
+
envNames: options.addedEnv === void 0 ? [] : Object.keys(options.addedEnv),
|
|
1837
1850
|
exitCode: result.exitCode,
|
|
1838
1851
|
stderrTail: stderrTail(result.stderr)
|
|
1839
1852
|
});
|
|
@@ -1845,6 +1858,7 @@ async function runShown(runner, output, command, args) {
|
|
|
1845
1858
|
if (exitCode !== 0) throw new ExternalToolError({
|
|
1846
1859
|
tool: command,
|
|
1847
1860
|
argv: args,
|
|
1861
|
+
envNames: [],
|
|
1848
1862
|
exitCode,
|
|
1849
1863
|
stderrTail: ""
|
|
1850
1864
|
});
|
|
@@ -1856,6 +1870,7 @@ async function runShown(runner, output, command, args) {
|
|
|
1856
1870
|
throw new ExternalToolError({
|
|
1857
1871
|
tool: command,
|
|
1858
1872
|
argv: args,
|
|
1873
|
+
envNames: [],
|
|
1859
1874
|
exitCode: result.exitCode,
|
|
1860
1875
|
stderrTail: stderrTail(printed.filter((line) => HAS_TEXT.test(line)).join("\n"))
|
|
1861
1876
|
});
|
|
@@ -1869,6 +1884,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
|
|
|
1869
1884
|
throw new ExternalToolError({
|
|
1870
1885
|
tool,
|
|
1871
1886
|
argv,
|
|
1887
|
+
envNames: [],
|
|
1872
1888
|
exitCode: 0,
|
|
1873
1889
|
stderrTail: `output is not JSON: ${error instanceof Error ? error.message : String(error)}`
|
|
1874
1890
|
});
|
|
@@ -1879,6 +1895,7 @@ function parseJsonOutput(tool, argv, stdout, schema) {
|
|
|
1879
1895
|
if (error instanceof ZodError) throw new ExternalToolError({
|
|
1880
1896
|
tool,
|
|
1881
1897
|
argv,
|
|
1898
|
+
envNames: [],
|
|
1882
1899
|
exitCode: 0,
|
|
1883
1900
|
stderrTail: `unexpected output shape:\n${z.prettifyError(error)}`
|
|
1884
1901
|
});
|
|
@@ -2179,10 +2196,10 @@ function createArgs(options) {
|
|
|
2179
2196
|
"--label",
|
|
2180
2197
|
`${IMAGE_LABEL}=${options.image}`,
|
|
2181
2198
|
"-p",
|
|
2182
|
-
`${LOOPBACK_HOST}:${options.port.host
|
|
2199
|
+
`${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`,
|
|
2200
|
+
"-v",
|
|
2201
|
+
`${options.mount.source}:${options.mount.target}`
|
|
2183
2202
|
];
|
|
2184
|
-
if (options.port.alsoOn !== null) args.push("-p", `${options.port.alsoOn}:${options.port.host ?? ""}:${options.port.container}`);
|
|
2185
|
-
if (options.mount !== null) args.push("-v", `${options.mount.source}:${options.mount.target}`);
|
|
2186
2203
|
for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
|
|
2187
2204
|
if (options.hostUser !== null) {
|
|
2188
2205
|
args.push("-e", `${ENV_HOST_UID}=${options.hostUser.uid}`);
|
|
@@ -2241,17 +2258,6 @@ function dockerTool(runner) {
|
|
|
2241
2258
|
if (NO_SUCH_IMAGE.test(result.stderr)) return null;
|
|
2242
2259
|
throw toolFailure(DOCKER, args, result);
|
|
2243
2260
|
}
|
|
2244
|
-
async function bridgeGateway() {
|
|
2245
|
-
const gateway = (await docker([
|
|
2246
|
-
"network",
|
|
2247
|
-
"inspect",
|
|
2248
|
-
"bridge",
|
|
2249
|
-
"--format",
|
|
2250
|
-
"{{range .IPAM.Config}}{{.Gateway}}{{end}}"
|
|
2251
|
-
])).stdout.trim();
|
|
2252
|
-
if (gateway.length === 0) throw new ConfigError("`docker network inspect bridge` names no gateway address");
|
|
2253
|
-
return gateway;
|
|
2254
|
-
}
|
|
2255
2261
|
async function imageRemove(image) {
|
|
2256
2262
|
await docker(["rmi", image]);
|
|
2257
2263
|
}
|
|
@@ -2328,33 +2334,6 @@ function dockerTool(runner) {
|
|
|
2328
2334
|
name
|
|
2329
2335
|
]);
|
|
2330
2336
|
}
|
|
2331
|
-
async function containerLoopbackPort(name, containerPort) {
|
|
2332
|
-
const args = [
|
|
2333
|
-
"port",
|
|
2334
|
-
name,
|
|
2335
|
-
`${containerPort}/tcp`
|
|
2336
|
-
];
|
|
2337
|
-
const result = await docker(args);
|
|
2338
|
-
const prefix = `${LOOPBACK_HOST}:`;
|
|
2339
|
-
const line = result.stdout.split("\n").find((entry) => entry.startsWith(prefix));
|
|
2340
|
-
if (line !== void 0) {
|
|
2341
|
-
const port = Number.parseInt(line.slice(prefix.length), 10);
|
|
2342
|
-
if (Number.isInteger(port)) return port;
|
|
2343
|
-
}
|
|
2344
|
-
throw new ExternalToolError({
|
|
2345
|
-
tool: DOCKER,
|
|
2346
|
-
argv: args,
|
|
2347
|
-
exitCode: 0,
|
|
2348
|
-
stderrTail: `no port published on ${LOOPBACK_HOST} in: ${result.stdout.trim()}`
|
|
2349
|
-
});
|
|
2350
|
-
}
|
|
2351
|
-
async function containerExecSucceeds(name, command) {
|
|
2352
|
-
return (await runner.run(DOCKER, [
|
|
2353
|
-
"exec",
|
|
2354
|
-
name,
|
|
2355
|
-
...command
|
|
2356
|
-
])).exitCode === 0;
|
|
2357
|
-
}
|
|
2358
2337
|
async function containerLogs(name, options) {
|
|
2359
2338
|
const args = ["logs"];
|
|
2360
2339
|
if (options.follow) args.push("--follow");
|
|
@@ -2367,7 +2346,6 @@ function dockerTool(runner) {
|
|
|
2367
2346
|
imagePull,
|
|
2368
2347
|
imageId,
|
|
2369
2348
|
imageRemove,
|
|
2370
|
-
bridgeGateway,
|
|
2371
2349
|
containerInspect,
|
|
2372
2350
|
containerImageId,
|
|
2373
2351
|
portClaims,
|
|
@@ -2377,8 +2355,6 @@ function dockerTool(runner) {
|
|
|
2377
2355
|
containerRestart,
|
|
2378
2356
|
containerRemove,
|
|
2379
2357
|
containerForceRemove,
|
|
2380
|
-
containerLoopbackPort,
|
|
2381
|
-
containerExecSucceeds,
|
|
2382
2358
|
containerLogs
|
|
2383
2359
|
};
|
|
2384
2360
|
}
|
|
@@ -2556,8 +2532,7 @@ function dockerRuntime(state, deps) {
|
|
|
2556
2532
|
imageId,
|
|
2557
2533
|
port: {
|
|
2558
2534
|
host: state.port,
|
|
2559
|
-
container: CONTAINER_PORT
|
|
2560
|
-
alsoOn: null
|
|
2535
|
+
container: CONTAINER_PORT
|
|
2561
2536
|
},
|
|
2562
2537
|
mount: {
|
|
2563
2538
|
source: deps.dataDir,
|
|
@@ -2871,148 +2846,21 @@ function runtimeFromState(state, deps) {
|
|
|
2871
2846
|
}
|
|
2872
2847
|
}
|
|
2873
2848
|
//#endregion
|
|
2874
|
-
//#region src/runtime/sample-database.ts
|
|
2875
|
-
const SAMPLE_DATABASE_IMAGE = "metabase/qa-databases:postgres-sample-15";
|
|
2876
|
-
const SAMPLE_DATABASE_USER = "metabase";
|
|
2877
|
-
const SAMPLE_DATABASE_PASSWORD = "metasample123";
|
|
2878
|
-
const SAMPLE_DATABASE_NAME = "sample";
|
|
2879
|
-
const SAMPLE_DATABASE_PORT = 25432;
|
|
2880
|
-
const SAMPLE_DATABASE_READY_TIMEOUT_MS = 12e4;
|
|
2881
|
-
const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
|
|
2882
|
-
const POSTGRES_PORT = 5432;
|
|
2849
|
+
//#region src/runtime/leftover-sample-database.ts
|
|
2883
2850
|
const CONTAINER_NAME_PREFIX = "rde-sampledb";
|
|
2884
|
-
const
|
|
2885
|
-
|
|
2886
|
-
"pg_isready",
|
|
2887
|
-
"-h",
|
|
2888
|
-
LOOPBACK_HOST,
|
|
2889
|
-
"-U",
|
|
2890
|
-
SAMPLE_DATABASE_USER,
|
|
2891
|
-
"-d",
|
|
2892
|
-
SAMPLE_DATABASE_NAME
|
|
2893
|
-
];
|
|
2894
|
-
function sampleContainerName(instanceName, home) {
|
|
2851
|
+
const SAMPLE_DATABASE_STOP_GRACE_MS = 1e4;
|
|
2852
|
+
function sampleDatabaseContainerName(instanceName, home) {
|
|
2895
2853
|
const tag = homeTag(home);
|
|
2896
2854
|
const name = `${CONTAINER_NAME_PREFIX}-${instanceName}`;
|
|
2897
2855
|
return tag === null ? name : `${name}-${tag}`;
|
|
2898
2856
|
}
|
|
2899
|
-
function
|
|
2900
|
-
return
|
|
2901
|
-
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
}
|
|
2905
|
-
function createOptions(state, home, hostPort, alsoOn) {
|
|
2906
|
-
return {
|
|
2907
|
-
name: state.containerName,
|
|
2908
|
-
home,
|
|
2909
|
-
image: state.image,
|
|
2910
|
-
imageId: null,
|
|
2911
|
-
port: {
|
|
2912
|
-
host: hostPort,
|
|
2913
|
-
container: POSTGRES_PORT,
|
|
2914
|
-
alsoOn
|
|
2915
|
-
},
|
|
2916
|
-
mount: null,
|
|
2917
|
-
env: {},
|
|
2918
|
-
addHostGateway: false,
|
|
2919
|
-
hostUser: null
|
|
2920
|
-
};
|
|
2921
|
-
}
|
|
2922
|
-
function requireOwnContainer(name, existing, home) {
|
|
2923
|
-
if (existing.home !== null && existing.home !== home) throw new ConfigError(`container ${name} belongs to the rde home ${existing.home} and is left alone`);
|
|
2924
|
-
}
|
|
2925
|
-
function sampleDatabaseContainer(state, docker, home) {
|
|
2926
|
-
const name = state.containerName;
|
|
2927
|
-
function conflict(reason) {
|
|
2928
|
-
return new ConfigError(`container ${name} already exists ${reason}; remove it with \`docker rm -f ${name}\` and run \`rde init\` again`);
|
|
2929
|
-
}
|
|
2930
|
-
async function install(options) {
|
|
2931
|
-
const existing = await docker.containerInspect(name);
|
|
2932
|
-
if (existing.exists) {
|
|
2933
|
-
if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
|
|
2934
|
-
if (publishesOnLoopback$1(existing, state.port)) return;
|
|
2935
|
-
requireOwnContainer(name, existing, home);
|
|
2936
|
-
await docker.containerForceRemove(name);
|
|
2937
|
-
}
|
|
2938
|
-
await docker.imagePull(state.image, { onLine: options.onProgress });
|
|
2939
|
-
await docker.containerCreate(createOptions(state, home, state.port, options.alsoOn));
|
|
2940
|
-
}
|
|
2941
|
-
async function start() {
|
|
2942
|
-
const existing = await docker.containerInspect(name);
|
|
2943
|
-
if (!existing.exists) return "missing";
|
|
2944
|
-
if (existing.running) return "running";
|
|
2945
|
-
await docker.containerStart(name);
|
|
2946
|
-
return "started";
|
|
2947
|
-
}
|
|
2948
|
-
async function stop() {
|
|
2949
|
-
const existing = await docker.containerInspect(name);
|
|
2950
|
-
if (existing.exists && existing.running) await docker.containerStop(name, { graceMs: SAMPLE_DATABASE_STOP_GRACE_MS });
|
|
2951
|
-
}
|
|
2952
|
-
async function status() {
|
|
2953
|
-
const existing = await docker.containerInspect(name);
|
|
2954
|
-
return {
|
|
2955
|
-
installed: existing.exists,
|
|
2956
|
-
running: existing.exists && existing.running
|
|
2957
|
-
};
|
|
2958
|
-
}
|
|
2959
|
-
async function waitReady(options) {
|
|
2960
|
-
const deadline = Date.now() + options.timeoutMs;
|
|
2961
|
-
const interval = options.pollMs ?? READY_POLL_MS;
|
|
2962
|
-
for (;;) {
|
|
2963
|
-
if (await docker.containerExecSucceeds(name, READY_PROBE)) return;
|
|
2964
|
-
const existing = await docker.containerInspect(name);
|
|
2965
|
-
if (!existing.exists || !existing.running) throw new ConfigError(`container ${name} stopped before Postgres was ready; \`docker logs ${name}\` shows why`);
|
|
2966
|
-
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`);
|
|
2967
|
-
try {
|
|
2968
|
-
await setTimeout$1(interval, void 0, { signal: options.signal });
|
|
2969
|
-
} catch {
|
|
2970
|
-
throw new AbortError();
|
|
2971
|
-
}
|
|
2972
|
-
}
|
|
2973
|
-
}
|
|
2974
|
-
return {
|
|
2975
|
-
install,
|
|
2976
|
-
start,
|
|
2977
|
-
stop,
|
|
2978
|
-
status,
|
|
2979
|
-
waitReady
|
|
2980
|
-
};
|
|
2981
|
-
}
|
|
2982
|
-
function extraAddress(inspection) {
|
|
2983
|
-
const extra = inspection.ports.find((mapping) => mapping.container === POSTGRES_PORT && mapping.hostIp !== "127.0.0.1");
|
|
2984
|
-
return extra === void 0 ? null : extra.hostIp;
|
|
2985
|
-
}
|
|
2986
|
-
async function startOnDockerPort(state, docker, home, alsoOn) {
|
|
2987
|
-
const name = state.containerName;
|
|
2988
|
-
await docker.containerForceRemove(name);
|
|
2989
|
-
await docker.containerCreate(createOptions(state, home, null, null));
|
|
2990
|
-
await docker.containerStart(name);
|
|
2991
|
-
const port = await docker.containerLoopbackPort(name, POSTGRES_PORT);
|
|
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;
|
|
2992
2862
|
await docker.containerForceRemove(name);
|
|
2993
|
-
|
|
2994
|
-
...state,
|
|
2995
|
-
port
|
|
2996
|
-
};
|
|
2997
|
-
await docker.containerCreate(createOptions(pinned, home, port, alsoOn));
|
|
2998
|
-
await docker.containerStart(name);
|
|
2999
|
-
return pinned;
|
|
3000
|
-
}
|
|
3001
|
-
async function startOnFreePort(state, docker, home) {
|
|
3002
|
-
try {
|
|
3003
|
-
return {
|
|
3004
|
-
outcome: await sampleDatabaseContainer(state, docker, home).start(),
|
|
3005
|
-
state
|
|
3006
|
-
};
|
|
3007
|
-
} catch (error) {
|
|
3008
|
-
if (!isPortTakenError(error)) throw error;
|
|
3009
|
-
}
|
|
3010
|
-
const existing = await docker.containerInspect(state.containerName);
|
|
3011
|
-
if (existing.exists) requireOwnContainer(state.containerName, existing, home);
|
|
3012
|
-
return {
|
|
3013
|
-
outcome: "started",
|
|
3014
|
-
state: await startOnDockerPort(state, docker, home, existing.exists ? extraAddress(existing) : null)
|
|
3015
|
-
};
|
|
2863
|
+
return name;
|
|
3016
2864
|
}
|
|
3017
2865
|
//#endregion
|
|
3018
2866
|
//#region src/steps/lifecycle.ts
|
|
@@ -3023,10 +2871,14 @@ async function loadInstance(name, signal) {
|
|
|
3023
2871
|
return {
|
|
3024
2872
|
state,
|
|
3025
2873
|
url: configured.url,
|
|
3026
|
-
runtime: runtimeFromState(configured.runtime, deps)
|
|
3027
|
-
sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker, deps.rdeHome)
|
|
2874
|
+
runtime: runtimeFromState(configured.runtime, deps)
|
|
3028
2875
|
};
|
|
3029
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
|
+
}
|
|
3030
2882
|
async function isInstanceRunning(state, runtime) {
|
|
3031
2883
|
const configured = configuredRuntime(state);
|
|
3032
2884
|
if (configured === null) return false;
|
|
@@ -3035,44 +2887,29 @@ async function isInstanceRunning(state, runtime) {
|
|
|
3035
2887
|
function openInstance(url) {
|
|
3036
2888
|
return openBrowser(url);
|
|
3037
2889
|
}
|
|
3038
|
-
function ownContainers(state) {
|
|
3039
|
-
const names = /* @__PURE__ */ new Set();
|
|
3040
|
-
if (state.runtime?.kind === "docker") names.add(state.runtime.containerName);
|
|
3041
|
-
if (state.sampleDatabase !== null) names.add(state.sampleDatabase.containerName);
|
|
3042
|
-
return names;
|
|
3043
|
-
}
|
|
3044
|
-
function instanceClaims(other) {
|
|
3045
|
-
const claims = [];
|
|
3046
|
-
if (other.runtime !== null) claims.push({
|
|
3047
|
-
port: other.runtime.port,
|
|
3048
|
-
holder: `rde instance ${other.name}`
|
|
3049
|
-
});
|
|
3050
|
-
if (other.sampleDatabase !== null) claims.push({
|
|
3051
|
-
port: other.sampleDatabase.port,
|
|
3052
|
-
holder: `the sample database of rde instance ${other.name}`
|
|
3053
|
-
});
|
|
3054
|
-
return claims;
|
|
3055
|
-
}
|
|
3056
2890
|
async function portClaims(state, runtime) {
|
|
3057
2891
|
const claims = [];
|
|
3058
2892
|
if ((await runtime.docker.available()).available) {
|
|
3059
|
-
const own =
|
|
3060
|
-
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({
|
|
3061
2895
|
port: claim.port,
|
|
3062
2896
|
holder: `container ${claim.container}`
|
|
3063
2897
|
});
|
|
3064
2898
|
}
|
|
3065
|
-
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
|
+
});
|
|
3066
2903
|
return claims;
|
|
3067
2904
|
}
|
|
3068
2905
|
//#endregion
|
|
3069
2906
|
//#region src/steps/admin-user.ts
|
|
3070
|
-
const STEP_ID$
|
|
2907
|
+
const STEP_ID$8 = "admin-user";
|
|
3071
2908
|
const ADMIN_USER_KEYS = {
|
|
3072
|
-
email: `${STEP_ID$
|
|
3073
|
-
password: `${STEP_ID$
|
|
3074
|
-
firstName: `${STEP_ID$
|
|
3075
|
-
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`
|
|
3076
2913
|
};
|
|
3077
2914
|
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
3078
2915
|
const GENERATED_PASSWORD_BYTES = 24;
|
|
@@ -3087,7 +2924,7 @@ function generatePassword(random) {
|
|
|
3087
2924
|
if (DIGIT.test(candidate)) return candidate;
|
|
3088
2925
|
}
|
|
3089
2926
|
}
|
|
3090
|
-
function hostDeps$
|
|
2927
|
+
function hostDeps$13(ctx) {
|
|
3091
2928
|
return {
|
|
3092
2929
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3093
2930
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
@@ -3095,7 +2932,7 @@ function hostDeps$14(ctx) {
|
|
|
3095
2932
|
};
|
|
3096
2933
|
}
|
|
3097
2934
|
function isNotMetabaseYet(error) {
|
|
3098
|
-
return error
|
|
2935
|
+
return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
|
|
3099
2936
|
}
|
|
3100
2937
|
function validateEmail(value) {
|
|
3101
2938
|
return EMAIL.test(value) ? void 0 : "Enter an email address";
|
|
@@ -3225,9 +3062,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
|
|
|
3225
3062
|
ctx.ui.success(`Admin user ${email} recorded`);
|
|
3226
3063
|
return "completed";
|
|
3227
3064
|
}
|
|
3228
|
-
function adminUserStep(options, deps = hostDeps$
|
|
3065
|
+
function adminUserStep(options, deps = hostDeps$13) {
|
|
3229
3066
|
return {
|
|
3230
|
-
id: STEP_ID$
|
|
3067
|
+
id: STEP_ID$8,
|
|
3231
3068
|
title: "Create the admin user",
|
|
3232
3069
|
redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
|
|
3233
3070
|
async detect(ctx) {
|
|
@@ -3455,11 +3292,12 @@ const LogoutResult = z.object({
|
|
|
3455
3292
|
cleared: z.boolean(),
|
|
3456
3293
|
aborted: z.boolean()
|
|
3457
3294
|
}).loose();
|
|
3295
|
+
const MbAuthMethod = z.enum(["oauth", "apiKey"]);
|
|
3458
3296
|
const AuthStatus = z.object({
|
|
3459
3297
|
profile: z.string(),
|
|
3460
3298
|
present: z.boolean(),
|
|
3461
3299
|
url: z.string().nullable(),
|
|
3462
|
-
method:
|
|
3300
|
+
method: MbAuthMethod.nullable(),
|
|
3463
3301
|
user: ProbedUser.nullable()
|
|
3464
3302
|
}).loose();
|
|
3465
3303
|
const AuthProfileStatus = z.enum([
|
|
@@ -3506,11 +3344,41 @@ const RemoteChanges = z.object({
|
|
|
3506
3344
|
remote_version: z.string().nullable(),
|
|
3507
3345
|
local_version: z.string().nullable()
|
|
3508
3346
|
}).loose();
|
|
3347
|
+
function comparableUrl(url) {
|
|
3348
|
+
const parsed = new URL(url);
|
|
3349
|
+
return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
|
|
3350
|
+
}
|
|
3351
|
+
function sameMetabaseUrl(a, b) {
|
|
3352
|
+
return comparableUrl(a) === comparableUrl(b);
|
|
3353
|
+
}
|
|
3354
|
+
function isLoggedInAt(status, url) {
|
|
3355
|
+
return status !== null && status.present && status.url !== null && sameMetabaseUrl(status.url, url);
|
|
3356
|
+
}
|
|
3357
|
+
function isOAuthLoginAt(status, url) {
|
|
3358
|
+
return status !== null && status.method === MbAuthMethod.enum.oauth && isLoggedInAt(status, url);
|
|
3359
|
+
}
|
|
3360
|
+
function isApiKeyLoginAt(status, url) {
|
|
3361
|
+
return status !== null && status.method === MbAuthMethod.enum.apiKey && isLoggedInAt(status, url);
|
|
3362
|
+
}
|
|
3363
|
+
function mbInstanceOf(state, secrets) {
|
|
3364
|
+
const configured = configuredRuntime(state);
|
|
3365
|
+
if (configured === null || secrets === null || secrets.apiKey === null) return null;
|
|
3366
|
+
return {
|
|
3367
|
+
url: configured.url,
|
|
3368
|
+
apiKey: secrets.apiKey
|
|
3369
|
+
};
|
|
3370
|
+
}
|
|
3371
|
+
function instanceEnv(instance) {
|
|
3372
|
+
return { addedEnv: {
|
|
3373
|
+
[ENV_MB_URL]: instance.url,
|
|
3374
|
+
[ENV_MB_API_KEY]: instance.apiKey
|
|
3375
|
+
} };
|
|
3376
|
+
}
|
|
3509
3377
|
function mbTool(runner) {
|
|
3510
|
-
async function mbJson(args, schema,
|
|
3378
|
+
async function mbJson(args, schema, options = {}) {
|
|
3511
3379
|
const argv = [...args, "--json"];
|
|
3512
|
-
const result = await runner.run(MB, argv,
|
|
3513
|
-
if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
|
|
3380
|
+
const result = await runner.run(MB, argv, options);
|
|
3381
|
+
if (result.exitCode !== 0) throw toolFailure(MB, argv, result, options);
|
|
3514
3382
|
return parseJsonOutput(MB, argv, result.stdout, schema);
|
|
3515
3383
|
}
|
|
3516
3384
|
async function version(command = MB) {
|
|
@@ -3533,7 +3401,7 @@ function mbTool(runner) {
|
|
|
3533
3401
|
options.url,
|
|
3534
3402
|
"--profile",
|
|
3535
3403
|
options.profile
|
|
3536
|
-
], LoginResult, `${options.apiKey}\n`);
|
|
3404
|
+
], LoginResult, { stdin: `${options.apiKey}\n` });
|
|
3537
3405
|
}
|
|
3538
3406
|
async function authStatus(profile) {
|
|
3539
3407
|
return mbJson([
|
|
@@ -3543,6 +3411,14 @@ function mbTool(runner) {
|
|
|
3543
3411
|
profile
|
|
3544
3412
|
], AuthStatus);
|
|
3545
3413
|
}
|
|
3414
|
+
async function authStatusIfInstalled(profile) {
|
|
3415
|
+
try {
|
|
3416
|
+
return await authStatus(profile);
|
|
3417
|
+
} catch (error) {
|
|
3418
|
+
if (error instanceof ProcessNotFoundError) return null;
|
|
3419
|
+
throw error;
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3546
3422
|
async function authLogout(profile) {
|
|
3547
3423
|
return mbJson([
|
|
3548
3424
|
"auth",
|
|
@@ -3562,50 +3438,37 @@ function mbTool(runner) {
|
|
|
3562
3438
|
"0"
|
|
3563
3439
|
], AuthList)).data;
|
|
3564
3440
|
}
|
|
3565
|
-
async function gitSyncStatus(
|
|
3566
|
-
return mbJson([
|
|
3567
|
-
"git-sync",
|
|
3568
|
-
"status",
|
|
3569
|
-
"--profile",
|
|
3570
|
-
profile
|
|
3571
|
-
], GitSyncStatus);
|
|
3441
|
+
async function gitSyncStatus(instance) {
|
|
3442
|
+
return mbJson(["git-sync", "status"], GitSyncStatus, instanceEnv(instance));
|
|
3572
3443
|
}
|
|
3573
|
-
async function gitSyncHasRemoteChanges(
|
|
3574
|
-
return mbJson([
|
|
3575
|
-
"git-sync",
|
|
3576
|
-
"has-remote-changes",
|
|
3577
|
-
"--profile",
|
|
3578
|
-
profile
|
|
3579
|
-
], RemoteChanges);
|
|
3444
|
+
async function gitSyncHasRemoteChanges(instance) {
|
|
3445
|
+
return mbJson(["git-sync", "has-remote-changes"], RemoteChanges, instanceEnv(instance));
|
|
3580
3446
|
}
|
|
3581
|
-
async function gitSyncImport(
|
|
3447
|
+
async function gitSyncImport(instance, options) {
|
|
3582
3448
|
return mbJson([
|
|
3583
3449
|
"git-sync",
|
|
3584
3450
|
"import",
|
|
3585
|
-
"--profile",
|
|
3586
|
-
profile,
|
|
3587
3451
|
"--branch",
|
|
3588
3452
|
options.branch,
|
|
3589
3453
|
"--wait"
|
|
3590
|
-
], GitSyncOutcome);
|
|
3454
|
+
], GitSyncOutcome, instanceEnv(instance));
|
|
3591
3455
|
}
|
|
3592
|
-
async function gitSyncExport(
|
|
3456
|
+
async function gitSyncExport(instance, options) {
|
|
3593
3457
|
return mbJson([
|
|
3594
3458
|
"git-sync",
|
|
3595
3459
|
"export",
|
|
3596
|
-
"--profile",
|
|
3597
|
-
profile,
|
|
3598
3460
|
"--branch",
|
|
3599
3461
|
options.branch,
|
|
3600
3462
|
"--message",
|
|
3601
3463
|
options.message,
|
|
3602
3464
|
"--wait"
|
|
3603
|
-
], GitSyncOutcome);
|
|
3465
|
+
], GitSyncOutcome, instanceEnv(instance));
|
|
3604
3466
|
}
|
|
3605
3467
|
return {
|
|
3606
3468
|
version,
|
|
3607
3469
|
authLogin,
|
|
3608
3470
|
authStatus,
|
|
3471
|
+
authStatusIfInstalled,
|
|
3609
3472
|
authLogout,
|
|
3610
3473
|
authList,
|
|
3611
3474
|
gitSyncStatus,
|
|
@@ -3616,17 +3479,18 @@ function mbTool(runner) {
|
|
|
3616
3479
|
}
|
|
3617
3480
|
//#endregion
|
|
3618
3481
|
//#region src/steps/api-key.ts
|
|
3619
|
-
const STEP_ID$
|
|
3482
|
+
const STEP_ID$7 = "api-key";
|
|
3620
3483
|
const API_KEY_KEYS = {
|
|
3621
|
-
email: `${STEP_ID$
|
|
3622
|
-
password: `${STEP_ID$
|
|
3623
|
-
profile: `${STEP_ID$
|
|
3484
|
+
email: `${STEP_ID$7}.email`,
|
|
3485
|
+
password: `${STEP_ID$7}.password`,
|
|
3486
|
+
profile: `${STEP_ID$7}.profile`
|
|
3624
3487
|
};
|
|
3625
3488
|
const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
3626
|
-
function hostDeps$
|
|
3489
|
+
function hostDeps$12(ctx) {
|
|
3627
3490
|
return {
|
|
3628
3491
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
3629
|
-
runner: processRunner(ctx.signal)
|
|
3492
|
+
runner: processRunner(ctx.signal),
|
|
3493
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
3630
3494
|
};
|
|
3631
3495
|
}
|
|
3632
3496
|
async function adminCredentials(ctx) {
|
|
@@ -3682,6 +3546,13 @@ async function authenticates(client) {
|
|
|
3682
3546
|
throw error;
|
|
3683
3547
|
}
|
|
3684
3548
|
}
|
|
3549
|
+
async function forgetCompletion(ctx) {
|
|
3550
|
+
const { [STEP_ID$7]: record, ...steps } = ctx.state.steps;
|
|
3551
|
+
if (record !== void 0) await ctx.saveState({
|
|
3552
|
+
...ctx.state,
|
|
3553
|
+
steps
|
|
3554
|
+
});
|
|
3555
|
+
}
|
|
3685
3556
|
async function workingApiKey(ctx, deps, url) {
|
|
3686
3557
|
const stored = ctx.secrets.apiKey;
|
|
3687
3558
|
if (stored !== null) {
|
|
@@ -3691,6 +3562,7 @@ async function workingApiKey(ctx, deps, url) {
|
|
|
3691
3562
|
}))) return stored;
|
|
3692
3563
|
ctx.ui.warn("Metabase rejects the stored API key; replacing it");
|
|
3693
3564
|
}
|
|
3565
|
+
await forgetCompletion(ctx);
|
|
3694
3566
|
const sessionId = await superuserSession(ctx, deps, url);
|
|
3695
3567
|
const apiKey = await freshApiKey(ctx, deps.client(url, {
|
|
3696
3568
|
kind: "session",
|
|
@@ -3702,20 +3574,24 @@ async function workingApiKey(ctx, deps, url) {
|
|
|
3702
3574
|
});
|
|
3703
3575
|
return apiKey;
|
|
3704
3576
|
}
|
|
3705
|
-
|
|
3706
|
-
|
|
3707
|
-
|
|
3708
|
-
|
|
3709
|
-
|
|
3710
|
-
throw error;
|
|
3711
|
-
}
|
|
3577
|
+
function elsewhereUrl(status, url, movedFrom) {
|
|
3578
|
+
if (status === null || !status.present || status.url === null) return null;
|
|
3579
|
+
if (sameMetabaseUrl(status.url, url)) return null;
|
|
3580
|
+
if (movedFrom !== null && sameMetabaseUrl(status.url, movedFrom)) return null;
|
|
3581
|
+
return status.url;
|
|
3712
3582
|
}
|
|
3713
3583
|
async function chooseProfile(ctx, mb, url) {
|
|
3714
3584
|
const profile = ctx.state.mbProfile;
|
|
3715
|
-
|
|
3716
|
-
|
|
3717
|
-
|
|
3718
|
-
|
|
3585
|
+
const status = await mb.authStatusIfInstalled(profile);
|
|
3586
|
+
if (profile !== "default") return {
|
|
3587
|
+
profile,
|
|
3588
|
+
status
|
|
3589
|
+
};
|
|
3590
|
+
const previous = elsewhereUrl(status, url, ctx.state.movedFrom);
|
|
3591
|
+
if (previous === null) return {
|
|
3592
|
+
profile,
|
|
3593
|
+
status
|
|
3594
|
+
};
|
|
3719
3595
|
const named = namedMbProfile(ctx.state.name);
|
|
3720
3596
|
if (await ctx.prompts.select({
|
|
3721
3597
|
key: API_KEY_KEYS.profile,
|
|
@@ -3730,49 +3606,58 @@ async function chooseProfile(ctx, mb, url) {
|
|
|
3730
3606
|
recommended: "default"
|
|
3731
3607
|
}) === "default") {
|
|
3732
3608
|
ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
|
|
3733
|
-
return
|
|
3609
|
+
return {
|
|
3610
|
+
profile,
|
|
3611
|
+
status
|
|
3612
|
+
};
|
|
3734
3613
|
}
|
|
3735
3614
|
await ctx.saveState({
|
|
3736
3615
|
...ctx.state,
|
|
3737
3616
|
mbProfile: named
|
|
3738
3617
|
});
|
|
3739
|
-
return
|
|
3618
|
+
return {
|
|
3619
|
+
profile: named,
|
|
3620
|
+
status: await mb.authStatusIfInstalled(named)
|
|
3621
|
+
};
|
|
3740
3622
|
}
|
|
3741
3623
|
function usageNote(profile) {
|
|
3742
3624
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
3743
3625
|
return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
|
|
3744
3626
|
}
|
|
3745
|
-
function apiKeyStep(deps = hostDeps$
|
|
3627
|
+
function apiKeyStep(deps = hostDeps$12) {
|
|
3746
3628
|
return {
|
|
3747
|
-
id: STEP_ID$
|
|
3629
|
+
id: STEP_ID$7,
|
|
3748
3630
|
title: "Connect the Metabase CLI",
|
|
3749
3631
|
redoHint: "`rde status` shows the mb profile it uses.",
|
|
3750
3632
|
async detect(ctx) {
|
|
3751
|
-
|
|
3752
|
-
const
|
|
3753
|
-
|
|
3633
|
+
const configured = configuredRuntime(ctx.state);
|
|
3634
|
+
const apiKey = ctx.secrets.apiKey;
|
|
3635
|
+
if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
|
|
3636
|
+
const resolved = deps(ctx);
|
|
3637
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
3638
|
+
if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
|
|
3754
3639
|
try {
|
|
3755
|
-
|
|
3640
|
+
return await authenticates(resolved.client(configured.url, {
|
|
3641
|
+
kind: "api-key",
|
|
3642
|
+
key: apiKey
|
|
3643
|
+
})) ? "done" : "todo";
|
|
3756
3644
|
} catch (error) {
|
|
3757
|
-
if (error instanceof
|
|
3645
|
+
if (isMetabaseNotServing(error) || error instanceof MetabaseApiError) return "todo";
|
|
3758
3646
|
throw error;
|
|
3759
3647
|
}
|
|
3760
|
-
return (await mb.authList()).find((candidate) => candidate.profile === profile)?.status === "ok" ? "done" : "todo";
|
|
3761
3648
|
},
|
|
3762
3649
|
async run(ctx) {
|
|
3763
3650
|
const resolved = deps(ctx);
|
|
3764
3651
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
3765
3652
|
const apiKey = await workingApiKey(ctx, resolved, configured.url);
|
|
3766
3653
|
const mb = mbTool(resolved.runner);
|
|
3767
|
-
const profile = await chooseProfile(ctx, mb, configured.url);
|
|
3768
|
-
|
|
3654
|
+
const { profile, status } = await chooseProfile(ctx, mb, configured.url);
|
|
3655
|
+
if (isOAuthLoginAt(status, configured.url)) ctx.ui.info(`mb profile ${profile} is logged in to this instance with OAuth; leaving it as it is (\`mb auth logout --profile ${profile}\` first to use rde's API key instead)`);
|
|
3656
|
+
else await mb.authLogin({
|
|
3769
3657
|
url: configured.url,
|
|
3770
3658
|
profile,
|
|
3771
3659
|
apiKey
|
|
3772
3660
|
});
|
|
3773
|
-
const entry = (await mb.authList()).find((candidate) => candidate.profile === profile);
|
|
3774
|
-
if (entry === void 0) throw new ConfigError(`\`mb auth list\` does not show profile ${profile} after login`);
|
|
3775
|
-
if (entry.status !== "ok") throw new ConfigError(`mb profile ${profile} reports ${entry.status} after login`);
|
|
3776
3661
|
if (ctx.state.movedFrom !== null) await ctx.saveState({
|
|
3777
3662
|
...ctx.state,
|
|
3778
3663
|
movedFrom: null
|
|
@@ -4769,7 +4654,7 @@ const RSA_KEY_UNAVAILABLE_ERROR = "RSA public key is not available";
|
|
|
4769
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";
|
|
4770
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";
|
|
4771
4656
|
const CONNECTION_PLACEHOLDER = "leave empty to skip";
|
|
4772
|
-
const
|
|
4657
|
+
const ENGINE_PLACEHOLDER = "leave empty to pick the engine";
|
|
4773
4658
|
const START_SCREEN = {
|
|
4774
4659
|
engine: "pick-engine",
|
|
4775
4660
|
"connection-string": "connection-string"
|
|
@@ -4795,6 +4680,9 @@ function targeting(stub) {
|
|
|
4795
4680
|
function restart(model) {
|
|
4796
4681
|
return model.target === null ? FRESH$1 : targeting(model.target);
|
|
4797
4682
|
}
|
|
4683
|
+
function mayFinish(run) {
|
|
4684
|
+
return !run.deps.required || run.deps.existingCount + run.created.length > 0;
|
|
4685
|
+
}
|
|
4798
4686
|
function nextStub(run, model) {
|
|
4799
4687
|
const stub = run.queue.shift();
|
|
4800
4688
|
if (stub !== void 0) return {
|
|
@@ -4802,13 +4690,18 @@ function nextStub(run, model) {
|
|
|
4802
4690
|
to: "connection-string",
|
|
4803
4691
|
model: targeting(stub)
|
|
4804
4692
|
};
|
|
4805
|
-
|
|
4693
|
+
if (run.created.length > 0) return {
|
|
4806
4694
|
type: "next",
|
|
4807
4695
|
to: "another",
|
|
4808
4696
|
model
|
|
4809
|
-
}
|
|
4697
|
+
};
|
|
4698
|
+
return mayFinish(run) ? {
|
|
4810
4699
|
type: "done",
|
|
4811
4700
|
model
|
|
4701
|
+
} : {
|
|
4702
|
+
type: "next",
|
|
4703
|
+
to: "connection-string",
|
|
4704
|
+
model: FRESH$1
|
|
4812
4705
|
};
|
|
4813
4706
|
}
|
|
4814
4707
|
function stubNamed(run, name) {
|
|
@@ -4898,9 +4791,13 @@ function pickStubsScreen(run) {
|
|
|
4898
4791
|
run.queue = run.stubs.filter((stub) => picked.includes(stub.id));
|
|
4899
4792
|
return nextStub(run, model);
|
|
4900
4793
|
}
|
|
4901
|
-
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) ? {
|
|
4902
4795
|
type: "done",
|
|
4903
4796
|
model
|
|
4797
|
+
} : {
|
|
4798
|
+
type: "next",
|
|
4799
|
+
to: "connection-string",
|
|
4800
|
+
model: FRESH$1
|
|
4904
4801
|
};
|
|
4905
4802
|
return {
|
|
4906
4803
|
type: "next",
|
|
@@ -4910,6 +4807,11 @@ function pickStubsScreen(run) {
|
|
|
4910
4807
|
};
|
|
4911
4808
|
}
|
|
4912
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
|
+
};
|
|
4913
4815
|
if (target === null) return {
|
|
4914
4816
|
key: DATABASE_KEYS.connection,
|
|
4915
4817
|
message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
|
|
@@ -4918,7 +4820,7 @@ function connectionQuestion(run, target) {
|
|
|
4918
4820
|
return {
|
|
4919
4821
|
key: databaseKey(ordinalOf(run), "connection"),
|
|
4920
4822
|
message: `Connection string of ${target.name} (Enter to pick its engine instead)`,
|
|
4921
|
-
placeholder:
|
|
4823
|
+
placeholder: ENGINE_PLACEHOLDER
|
|
4922
4824
|
};
|
|
4923
4825
|
}
|
|
4924
4826
|
function connectionStringScreen(run) {
|
|
@@ -4930,7 +4832,7 @@ function connectionStringScreen(run) {
|
|
|
4930
4832
|
...draft !== null && { initialValue: draft }
|
|
4931
4833
|
})).trim();
|
|
4932
4834
|
draft = uri;
|
|
4933
|
-
if (uri.length === 0) return model.target === null ? {
|
|
4835
|
+
if (uri.length === 0) return model.target === null && mayFinish(run) ? {
|
|
4934
4836
|
type: "done",
|
|
4935
4837
|
model
|
|
4936
4838
|
} : {
|
|
@@ -4969,22 +4871,28 @@ function connectionStringScreen(run) {
|
|
|
4969
4871
|
}
|
|
4970
4872
|
};
|
|
4971
4873
|
}
|
|
4972
|
-
function
|
|
4973
|
-
if (model.target !== null) return
|
|
4974
|
-
|
|
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
|
+
}] : [];
|
|
4975
4887
|
}
|
|
4976
4888
|
function pickEngineScreen(run) {
|
|
4977
4889
|
return async (model) => {
|
|
4978
4890
|
const total = run.deps.existingCount + run.created.length;
|
|
4979
|
-
const finishChoice = {
|
|
4980
|
-
value: FINISH,
|
|
4981
|
-
label: finishLabel(run, model)
|
|
4982
|
-
};
|
|
4983
4891
|
for (;;) {
|
|
4984
4892
|
const picked = await run.deps.prompts.select({
|
|
4985
4893
|
key: databaseKey(ordinalOf(run), "engine"),
|
|
4986
4894
|
message: "Which database engine?",
|
|
4987
|
-
choices: [...run.deps.catalog.list().map(engineChoice),
|
|
4895
|
+
choices: [...run.deps.catalog.list().map(engineChoice), ...finishChoices(run, model)],
|
|
4988
4896
|
...model.engine !== null && { initialValue: model.engine.key }
|
|
4989
4897
|
});
|
|
4990
4898
|
if (picked === "finish" && model.target !== null) return nextStub(run, model);
|
|
@@ -5460,6 +5368,13 @@ const METABASE_CONTENT_DIRS = [
|
|
|
5460
5368
|
];
|
|
5461
5369
|
const TOKEN_USERNAMES = { "bitbucket.org": "x-token-auth" };
|
|
5462
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");
|
|
5463
5378
|
function credentialEnv(credentials) {
|
|
5464
5379
|
const { origin, hostname } = new URL(credentials.url);
|
|
5465
5380
|
const username = TOKEN_USERNAMES[hostname] ?? DEFAULT_TOKEN_USERNAME;
|
|
@@ -5473,6 +5388,18 @@ function credentialEnv(credentials) {
|
|
|
5473
5388
|
GIT_TERMINAL_PROMPT: "0"
|
|
5474
5389
|
};
|
|
5475
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
|
+
}
|
|
5476
5403
|
async function directoryExists(path) {
|
|
5477
5404
|
try {
|
|
5478
5405
|
return (await promises.stat(path)).isDirectory();
|
|
@@ -5528,8 +5455,12 @@ function gitTool(runner) {
|
|
|
5528
5455
|
], { cwd: dir });
|
|
5529
5456
|
return result.exitCode === 0 && result.stdout.trim().length > 0;
|
|
5530
5457
|
}
|
|
5531
|
-
async function
|
|
5532
|
-
await git([
|
|
5458
|
+
async function commitPaths(dir, paths, message) {
|
|
5459
|
+
await git([
|
|
5460
|
+
"add",
|
|
5461
|
+
"--",
|
|
5462
|
+
...paths
|
|
5463
|
+
], { cwd: dir });
|
|
5533
5464
|
await git([
|
|
5534
5465
|
...await hasIdentity(dir) ? [] : [
|
|
5535
5466
|
"-c",
|
|
@@ -5539,7 +5470,9 @@ function gitTool(runner) {
|
|
|
5539
5470
|
],
|
|
5540
5471
|
"commit",
|
|
5541
5472
|
"--message",
|
|
5542
|
-
message
|
|
5473
|
+
message,
|
|
5474
|
+
"--",
|
|
5475
|
+
...paths
|
|
5543
5476
|
], { cwd: dir });
|
|
5544
5477
|
}
|
|
5545
5478
|
async function addRemote(dir, name, url) {
|
|
@@ -5585,6 +5518,42 @@ function gitTool(runner) {
|
|
|
5585
5518
|
if (result.exitCode !== 0) return null;
|
|
5586
5519
|
return result.stdout.trim();
|
|
5587
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
|
+
}
|
|
5588
5557
|
async function hasMetabaseContent(dir) {
|
|
5589
5558
|
for (const name of METABASE_CONTENT_DIRS) if (await directoryExists(join(dir, name))) return true;
|
|
5590
5559
|
return false;
|
|
@@ -5595,24 +5564,27 @@ function gitTool(runner) {
|
|
|
5595
5564
|
initBare,
|
|
5596
5565
|
init,
|
|
5597
5566
|
hasCommits,
|
|
5598
|
-
|
|
5567
|
+
commitPaths,
|
|
5599
5568
|
addRemote,
|
|
5600
5569
|
clone,
|
|
5601
5570
|
push,
|
|
5602
5571
|
currentBranch,
|
|
5603
5572
|
remoteUrl,
|
|
5573
|
+
isBareRepo,
|
|
5574
|
+
bareBranchCommits,
|
|
5575
|
+
reachableCommits,
|
|
5604
5576
|
hasMetabaseContent
|
|
5605
5577
|
};
|
|
5606
5578
|
}
|
|
5607
5579
|
//#endregion
|
|
5608
5580
|
//#region src/steps/git-sync.ts
|
|
5609
|
-
const STEP_ID$
|
|
5581
|
+
const STEP_ID$6 = "git-sync";
|
|
5610
5582
|
const GIT_SYNC_KEYS = {
|
|
5611
|
-
location: `${STEP_ID$
|
|
5612
|
-
path: `${STEP_ID$
|
|
5613
|
-
remote: `${STEP_ID$
|
|
5614
|
-
url: `${STEP_ID$
|
|
5615
|
-
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`
|
|
5616
5588
|
};
|
|
5617
5589
|
const BARE_REPO_NAME = "content.git";
|
|
5618
5590
|
const DEFAULT_BRANCH = "main";
|
|
@@ -5646,7 +5618,7 @@ const SYNC_MODE_SETTINGS = {
|
|
|
5646
5618
|
"remote-sync-type": "read-write",
|
|
5647
5619
|
"remote-sync-transforms": true
|
|
5648
5620
|
};
|
|
5649
|
-
function hostDeps$
|
|
5621
|
+
function hostDeps$11(ctx) {
|
|
5650
5622
|
return {
|
|
5651
5623
|
runner: processRunner(ctx.signal),
|
|
5652
5624
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -5898,7 +5870,7 @@ async function prepareWorkdir(run, workdir) {
|
|
|
5898
5870
|
if (!await run.git.hasCommits(workdir)) {
|
|
5899
5871
|
await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
|
|
5900
5872
|
await ignoreDataAppFiles(workdir);
|
|
5901
|
-
await run.git.
|
|
5873
|
+
await run.git.commitPaths(workdir, [README_NAME, GITIGNORE_NAME], INITIAL_COMMIT_MESSAGE);
|
|
5902
5874
|
}
|
|
5903
5875
|
return {
|
|
5904
5876
|
workdir,
|
|
@@ -6019,7 +5991,7 @@ async function applySettings(run, configured) {
|
|
|
6019
5991
|
async function remoteHasContent(run, gitRemote) {
|
|
6020
5992
|
switch (gitRemote.kind) {
|
|
6021
5993
|
case "local-bare": return run.git.hasMetabaseContent(gitRemote.workdir);
|
|
6022
|
-
case "hosted": return (await mbTool(run.deps.runner).gitSyncHasRemoteChanges(run.
|
|
5994
|
+
case "hosted": return (await mbTool(run.deps.runner).gitSyncHasRemoteChanges(run.instance)).has_changes;
|
|
6023
5995
|
}
|
|
6024
5996
|
}
|
|
6025
5997
|
function reportImport(run, outcome) {
|
|
@@ -6041,7 +6013,7 @@ async function importExistingContent(run, gitRemote) {
|
|
|
6041
6013
|
const spinner = run.ctx.ui.spinner("Importing the repository's content into Metabase…");
|
|
6042
6014
|
let outcome;
|
|
6043
6015
|
try {
|
|
6044
|
-
outcome = await mbTool(run.deps.runner).gitSyncImport(run.
|
|
6016
|
+
outcome = await mbTool(run.deps.runner).gitSyncImport(run.instance, { branch: gitRemote.branch });
|
|
6045
6017
|
} catch (error) {
|
|
6046
6018
|
spinner.fail("Import failed");
|
|
6047
6019
|
throw error;
|
|
@@ -6062,7 +6034,7 @@ function shellWord(value) {
|
|
|
6062
6034
|
function exportCommand(profile, branch) {
|
|
6063
6035
|
return `${mbCommand(profile)} git-sync export -b ${shellWord(branch)}`;
|
|
6064
6036
|
}
|
|
6065
|
-
function usageLines
|
|
6037
|
+
function usageLines(state, gitRemote) {
|
|
6066
6038
|
const profile = state.mbProfile;
|
|
6067
6039
|
return [
|
|
6068
6040
|
`cd ${gitRemote.workdir}`,
|
|
@@ -6085,15 +6057,17 @@ function isMbUnavailable(error) {
|
|
|
6085
6057
|
}
|
|
6086
6058
|
async function runGitSync(ctx, deps) {
|
|
6087
6059
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6088
|
-
|
|
6060
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6061
|
+
if (instance === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
6089
6062
|
const run = {
|
|
6090
6063
|
ctx,
|
|
6091
6064
|
deps,
|
|
6092
6065
|
git: gitTool(deps.runner),
|
|
6093
|
-
client: deps.client(
|
|
6066
|
+
client: deps.client(instance.url, {
|
|
6094
6067
|
kind: "api-key",
|
|
6095
|
-
key:
|
|
6096
|
-
})
|
|
6068
|
+
key: instance.apiKey
|
|
6069
|
+
}),
|
|
6070
|
+
instance
|
|
6097
6071
|
};
|
|
6098
6072
|
const flow = await runFlow({
|
|
6099
6073
|
remote: remoteScreen(run),
|
|
@@ -6111,20 +6085,22 @@ async function runGitSync(ctx, deps) {
|
|
|
6111
6085
|
...ctx.state,
|
|
6112
6086
|
gitRemote: remote.gitRemote
|
|
6113
6087
|
});
|
|
6114
|
-
ctx.ui.note(usageLines
|
|
6088
|
+
ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
6115
6089
|
return "completed";
|
|
6116
6090
|
}
|
|
6117
|
-
function gitSyncStep(deps = hostDeps$
|
|
6091
|
+
function gitSyncStep(deps = hostDeps$11) {
|
|
6118
6092
|
return {
|
|
6119
|
-
id: STEP_ID$
|
|
6093
|
+
id: STEP_ID$6,
|
|
6120
6094
|
title: "Connect a git repository",
|
|
6121
6095
|
redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
|
|
6122
6096
|
async detect(ctx) {
|
|
6123
6097
|
if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
|
|
6124
|
-
if (ctx.state.steps[STEP_ID$
|
|
6125
|
-
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";
|
|
6100
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6101
|
+
if (instance === null) return "todo";
|
|
6126
6102
|
try {
|
|
6127
|
-
return (await mbTool(deps(ctx).runner).gitSyncStatus(
|
|
6103
|
+
return (await mbTool(deps(ctx).runner).gitSyncStatus(instance)).branch === null ? "todo" : "done";
|
|
6128
6104
|
} catch (error) {
|
|
6129
6105
|
if (isMbUnavailable(error)) return "todo";
|
|
6130
6106
|
throw error;
|
|
@@ -6302,12 +6278,12 @@ function skillsCli(runner) {
|
|
|
6302
6278
|
}
|
|
6303
6279
|
//#endregion
|
|
6304
6280
|
//#region src/steps/agents-skills.ts
|
|
6305
|
-
const STEP_ID$
|
|
6306
|
-
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$
|
|
6281
|
+
const STEP_ID$5 = "agents-skills";
|
|
6282
|
+
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
|
|
6307
6283
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6308
6284
|
const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
|
|
6309
6285
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6310
|
-
function hostDeps$
|
|
6286
|
+
function hostDeps$10(ctx) {
|
|
6311
6287
|
return {
|
|
6312
6288
|
runner: processRunner(ctx.signal),
|
|
6313
6289
|
probe: hostAgentProbe(),
|
|
@@ -6486,11 +6462,11 @@ function skipLine(all, source) {
|
|
|
6486
6462
|
switch (source) {
|
|
6487
6463
|
case "override":
|
|
6488
6464
|
case "chosen":
|
|
6489
|
-
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$
|
|
6490
|
-
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`;
|
|
6491
6467
|
}
|
|
6492
6468
|
}
|
|
6493
|
-
function agentsSkillsStep(options, updates, deps = hostDeps$
|
|
6469
|
+
function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
6494
6470
|
let hashes = null;
|
|
6495
6471
|
async function setCheck(resolved) {
|
|
6496
6472
|
hashes ??= hashLookup(resolved.github);
|
|
@@ -6501,13 +6477,13 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6501
6477
|
};
|
|
6502
6478
|
}
|
|
6503
6479
|
return {
|
|
6504
|
-
id: STEP_ID$
|
|
6480
|
+
id: STEP_ID$5,
|
|
6505
6481
|
title: "Install the rde and data-app skills for your coding agents",
|
|
6506
6482
|
redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
|
|
6507
6483
|
async detect(ctx) {
|
|
6508
6484
|
const resolved = deps(ctx);
|
|
6509
6485
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6510
|
-
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";
|
|
6511
6487
|
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6512
6488
|
const check = await setCheck(resolved);
|
|
6513
6489
|
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
@@ -6555,8 +6531,10 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6555
6531
|
}
|
|
6556
6532
|
//#endregion
|
|
6557
6533
|
//#region src/steps/databases.ts
|
|
6558
|
-
const
|
|
6559
|
-
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}`;
|
|
6560
6538
|
function hostDatabasesDeps(signal) {
|
|
6561
6539
|
return {
|
|
6562
6540
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
@@ -6564,10 +6542,7 @@ function hostDatabasesDeps(signal) {
|
|
|
6564
6542
|
now: () => /* @__PURE__ */ new Date()
|
|
6565
6543
|
};
|
|
6566
6544
|
}
|
|
6567
|
-
function
|
|
6568
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
6569
|
-
}
|
|
6570
|
-
function requireApiKey$2(state, apiKey) {
|
|
6545
|
+
function requireApiKey$1(state, apiKey) {
|
|
6571
6546
|
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
6572
6547
|
return apiKey;
|
|
6573
6548
|
}
|
|
@@ -6577,7 +6552,7 @@ function hostAliasOf(runtime) {
|
|
|
6577
6552
|
case "jar": return null;
|
|
6578
6553
|
}
|
|
6579
6554
|
}
|
|
6580
|
-
function toRecord
|
|
6555
|
+
function toRecord(database, createdAt) {
|
|
6581
6556
|
return {
|
|
6582
6557
|
id: database.id,
|
|
6583
6558
|
name: database.name,
|
|
@@ -6589,7 +6564,7 @@ function authenticatedClient(state, apiKey, deps) {
|
|
|
6589
6564
|
const configured = requireConfiguredRuntime(state);
|
|
6590
6565
|
return deps.client(configured.url, {
|
|
6591
6566
|
kind: "api-key",
|
|
6592
|
-
key: requireApiKey$
|
|
6567
|
+
key: requireApiKey$1(state, apiKey)
|
|
6593
6568
|
});
|
|
6594
6569
|
}
|
|
6595
6570
|
async function listDatabases(state, apiKey, deps) {
|
|
@@ -6632,7 +6607,7 @@ async function connectDatabases(ctx, deps, opening) {
|
|
|
6632
6607
|
existingCount: existing.length,
|
|
6633
6608
|
readTextFile: deps.readTextFile,
|
|
6634
6609
|
onCreated: async (database) => {
|
|
6635
|
-
const record = toRecord
|
|
6610
|
+
const record = toRecord(database, deps.now().toISOString());
|
|
6636
6611
|
await ctx.saveState({
|
|
6637
6612
|
...ctx.state,
|
|
6638
6613
|
databases: [...ctx.state.databases, record]
|
|
@@ -6649,6 +6624,7 @@ async function connectDatabases(ctx, deps, opening) {
|
|
|
6649
6624
|
async function addDatabases(ctx, deps, options) {
|
|
6650
6625
|
const connected = await connectDatabases(ctx, deps, {
|
|
6651
6626
|
start: "engine",
|
|
6627
|
+
required: false,
|
|
6652
6628
|
...options
|
|
6653
6629
|
});
|
|
6654
6630
|
return {
|
|
@@ -6656,50 +6632,33 @@ async function addDatabases(ctx, deps, options) {
|
|
|
6656
6632
|
result: { addedDatabases: connected.added }
|
|
6657
6633
|
};
|
|
6658
6634
|
}
|
|
6659
|
-
async function
|
|
6660
|
-
const connected = await connectDatabases(ctx, deps,
|
|
6661
|
-
|
|
6662
|
-
offerStubs: true
|
|
6663
|
-
});
|
|
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);
|
|
6664
6638
|
return {
|
|
6665
|
-
outcome:
|
|
6639
|
+
outcome: "completed",
|
|
6666
6640
|
result: { addedDatabases: connected.added }
|
|
6667
6641
|
};
|
|
6668
6642
|
}
|
|
6669
|
-
function hostDeps$
|
|
6643
|
+
function hostDeps$9(ctx) {
|
|
6670
6644
|
return {
|
|
6671
6645
|
...hostDatabasesDeps(ctx.signal),
|
|
6672
6646
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
6673
6647
|
};
|
|
6674
6648
|
}
|
|
6675
|
-
function
|
|
6676
|
-
return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
|
|
6677
|
-
}
|
|
6678
|
-
function skippedBefore(state) {
|
|
6679
|
-
return state.steps[STEP_ID$6]?.status === "skipped";
|
|
6680
|
-
}
|
|
6681
|
-
function ownDatabaseIds(state) {
|
|
6682
|
-
const sampleId = sampleDatabaseId(state);
|
|
6683
|
-
return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
|
|
6684
|
-
}
|
|
6685
|
-
function databasesStep(options, deps = hostDeps$10) {
|
|
6649
|
+
function databasesStep(options, deps = hostDeps$9) {
|
|
6686
6650
|
return {
|
|
6687
|
-
id:
|
|
6651
|
+
id: "databases",
|
|
6688
6652
|
title: "Connect your databases",
|
|
6689
6653
|
redoHint: "Connect another with `rde db add --url <connection string>`.",
|
|
6690
6654
|
async detect(ctx) {
|
|
6691
6655
|
const configured = configuredRuntime(ctx.state);
|
|
6692
6656
|
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
6693
|
-
const
|
|
6694
|
-
|
|
6695
|
-
const asking = options.request === "ask-once" && !skippedBefore(ctx.state);
|
|
6696
|
-
if (own.size === 0 && !asking) {
|
|
6697
|
-
if (options.request === "named") return "todo";
|
|
6698
|
-
return sampleId !== null || skippedBefore(ctx.state) ? "done" : "todo";
|
|
6699
|
-
}
|
|
6657
|
+
const recorded = new Set(ctx.state.databases.map((record) => record.id));
|
|
6658
|
+
if (recorded.size === 0 && options.request === "named") return "todo";
|
|
6700
6659
|
const resolved = deps(ctx);
|
|
6701
6660
|
if (!await resolved.instanceRunning()) return "todo";
|
|
6702
|
-
const isOwn = (id) =>
|
|
6661
|
+
const isOwn = (id) => recorded.size === 0 || recorded.has(id);
|
|
6703
6662
|
const client = resolved.client(configured.url, {
|
|
6704
6663
|
kind: "api-key",
|
|
6705
6664
|
key: ctx.secrets.apiKey
|
|
@@ -6707,15 +6666,25 @@ function databasesStep(options, deps = hostDeps$10) {
|
|
|
6707
6666
|
try {
|
|
6708
6667
|
return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
|
|
6709
6668
|
} catch (error) {
|
|
6710
|
-
if (
|
|
6669
|
+
if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
|
|
6711
6670
|
throw error;
|
|
6712
6671
|
}
|
|
6713
6672
|
},
|
|
6714
6673
|
async run(ctx) {
|
|
6715
6674
|
switch (options.request) {
|
|
6716
|
-
case "ask
|
|
6717
|
-
|
|
6718
|
-
|
|
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);
|
|
6719
6688
|
}
|
|
6720
6689
|
}
|
|
6721
6690
|
};
|
|
@@ -6740,7 +6709,7 @@ function npmRegistry(signal, options = {}) {
|
|
|
6740
6709
|
}
|
|
6741
6710
|
//#endregion
|
|
6742
6711
|
//#region src/steps/install-mb.ts
|
|
6743
|
-
const STEP_ID$
|
|
6712
|
+
const STEP_ID$4 = "install-mb";
|
|
6744
6713
|
const MIN_MB_VERSION = "0.3.0";
|
|
6745
6714
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
6746
6715
|
function installedWith(scriptPath) {
|
|
@@ -6749,7 +6718,7 @@ function installedWith(scriptPath) {
|
|
|
6749
6718
|
function rdeInstalledWith() {
|
|
6750
6719
|
return installedWith(process.argv[1]);
|
|
6751
6720
|
}
|
|
6752
|
-
function hostDeps$
|
|
6721
|
+
function hostDeps$8(ctx) {
|
|
6753
6722
|
return {
|
|
6754
6723
|
runner: processRunner(ctx.signal),
|
|
6755
6724
|
installedWith: rdeInstalledWith(),
|
|
@@ -6886,6 +6855,7 @@ async function stillWrong(install, after, deps) {
|
|
|
6886
6855
|
const fail = (stderrTail) => new ExternalToolError({
|
|
6887
6856
|
tool: install.command,
|
|
6888
6857
|
argv: install.args,
|
|
6858
|
+
envNames: [],
|
|
6889
6859
|
exitCode: 0,
|
|
6890
6860
|
stderrTail
|
|
6891
6861
|
});
|
|
@@ -6915,7 +6885,7 @@ async function updateFor(release, installed, deps) {
|
|
|
6915
6885
|
version
|
|
6916
6886
|
};
|
|
6917
6887
|
}
|
|
6918
|
-
function installMbStep(updates, deps = hostDeps$
|
|
6888
|
+
function installMbStep(updates, deps = hostDeps$8) {
|
|
6919
6889
|
let published = null;
|
|
6920
6890
|
function release(resolved) {
|
|
6921
6891
|
if (updates === "installed") return Promise.resolve(UNCHECKED);
|
|
@@ -6923,7 +6893,7 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
6923
6893
|
return published;
|
|
6924
6894
|
}
|
|
6925
6895
|
return {
|
|
6926
|
-
id: STEP_ID$
|
|
6896
|
+
id: STEP_ID$4,
|
|
6927
6897
|
title: "Install the Metabase CLI",
|
|
6928
6898
|
async detect(ctx) {
|
|
6929
6899
|
const resolved = deps(ctx);
|
|
@@ -6965,26 +6935,47 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
6965
6935
|
}
|
|
6966
6936
|
//#endregion
|
|
6967
6937
|
//#region src/steps/license.ts
|
|
6968
|
-
const STEP_ID$
|
|
6969
|
-
const LICENSE_KEYS = { token: `${STEP_ID$
|
|
6938
|
+
const STEP_ID$3 = "license";
|
|
6939
|
+
const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
|
|
6970
6940
|
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
6971
|
-
const NOT_FOUND_STATUS$
|
|
6972
|
-
|
|
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) {
|
|
6973
6963
|
return {
|
|
6974
6964
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
6975
6965
|
runner: processRunner(ctx.signal),
|
|
6976
|
-
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
6966
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
6967
|
+
terminal: stdinIsTty()
|
|
6977
6968
|
};
|
|
6978
6969
|
}
|
|
6979
6970
|
function isNoTokenYet(error) {
|
|
6980
|
-
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$
|
|
6981
|
-
return error
|
|
6971
|
+
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
6972
|
+
return isMetabaseNotServing(error);
|
|
6982
6973
|
}
|
|
6983
6974
|
function validateToken(value) {
|
|
6984
6975
|
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
6985
6976
|
return "A license token is 64 hexadecimal characters";
|
|
6986
6977
|
}
|
|
6987
|
-
function requireApiKey
|
|
6978
|
+
function requireApiKey(ctx) {
|
|
6988
6979
|
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
6989
6980
|
return ctx.secrets.apiKey;
|
|
6990
6981
|
}
|
|
@@ -6992,13 +6983,74 @@ function activeLabel(status) {
|
|
|
6992
6983
|
const plan = status["plan-alias"];
|
|
6993
6984
|
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
6994
6985
|
}
|
|
6995
|
-
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
|
+
};
|
|
6996
7008
|
return {
|
|
6997
|
-
|
|
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,
|
|
6998
7050
|
title: "Activate your license",
|
|
6999
7051
|
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
7000
7052
|
async detect(ctx) {
|
|
7001
|
-
if (ctx.state.steps[STEP_ID$
|
|
7053
|
+
if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
|
|
7002
7054
|
const configured = configuredRuntime(ctx.state);
|
|
7003
7055
|
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7004
7056
|
const resolved = deps(ctx);
|
|
@@ -7016,36 +7068,17 @@ function licenseStep(deps = hostDeps$8) {
|
|
|
7016
7068
|
},
|
|
7017
7069
|
async run(ctx) {
|
|
7018
7070
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
7019
|
-
const apiKey = requireApiKey
|
|
7020
|
-
const token = await ctx.prompts.password({
|
|
7021
|
-
key: LICENSE_KEYS.token,
|
|
7022
|
-
message: "Paste your Metabase license token (Enter to skip)",
|
|
7023
|
-
validate: validateToken
|
|
7024
|
-
});
|
|
7025
|
-
if (token.length === 0) {
|
|
7026
|
-
ctx.ui.info("Skipping the license; remote sync needs one. Run `rde init --from license` to add it later");
|
|
7027
|
-
return { outcome: "skipped" };
|
|
7028
|
-
}
|
|
7029
|
-
ctx.session.licenseToken.set(token);
|
|
7071
|
+
const apiKey = requireApiKey(ctx);
|
|
7030
7072
|
const resolved = deps(ctx);
|
|
7031
|
-
const
|
|
7073
|
+
const status = await activateAskedToken(ctx, resolved.client(configured.url, {
|
|
7032
7074
|
kind: "api-key",
|
|
7033
7075
|
key: apiKey
|
|
7034
|
-
});
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
status = await client.tokenStatus.get();
|
|
7040
|
-
} catch (error) {
|
|
7041
|
-
spinner.fail("License activation failed");
|
|
7042
|
-
throw error;
|
|
7043
|
-
}
|
|
7044
|
-
if (!status.valid) {
|
|
7045
|
-
spinner.fail("License activation failed");
|
|
7046
|
-
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" };
|
|
7047
7081
|
}
|
|
7048
|
-
spinner.stop(activeLabel(status));
|
|
7049
7082
|
await ctx.saveState({
|
|
7050
7083
|
...ctx.state,
|
|
7051
7084
|
license: {
|
|
@@ -7054,13 +7087,13 @@ function licenseStep(deps = hostDeps$8) {
|
|
|
7054
7087
|
validThru: status["valid-thru"] ?? null
|
|
7055
7088
|
}
|
|
7056
7089
|
});
|
|
7057
|
-
|
|
7090
|
+
const mb = mbTool(resolved.runner);
|
|
7091
|
+
if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
|
|
7058
7092
|
url: configured.url,
|
|
7059
7093
|
profile: ctx.state.mbProfile,
|
|
7060
7094
|
apiKey
|
|
7061
7095
|
});
|
|
7062
|
-
|
|
7063
|
-
else ctx.ui.warn("This license does not include remote sync; the git step will be skipped");
|
|
7096
|
+
reportFeatures(ctx, status.features);
|
|
7064
7097
|
return { outcome: "completed" };
|
|
7065
7098
|
}
|
|
7066
7099
|
};
|
|
@@ -7073,7 +7106,7 @@ const NODE_HINT = `Install Node ${MIN_NODE_VERSION} or newer from https://nodejs
|
|
|
7073
7106
|
const MARK_OK = "✓";
|
|
7074
7107
|
const MARK_MISSING = "✗";
|
|
7075
7108
|
const MARK_OPTIONAL = "–";
|
|
7076
|
-
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`;
|
|
7077
7110
|
const JAVA_OPTIONAL_NOTE = `Metabase can run in Docker instead`;
|
|
7078
7111
|
const PLATFORMS = {
|
|
7079
7112
|
darwin: {
|
|
@@ -7089,7 +7122,7 @@ const PLATFORMS = {
|
|
|
7089
7122
|
javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
|
|
7090
7123
|
}
|
|
7091
7124
|
};
|
|
7092
|
-
function hostDeps$
|
|
7125
|
+
function hostDeps$6(ctx) {
|
|
7093
7126
|
const runner = processRunner(ctx.signal);
|
|
7094
7127
|
return {
|
|
7095
7128
|
platform: process.platform,
|
|
@@ -7211,7 +7244,7 @@ function checkLine(check) {
|
|
|
7211
7244
|
function checkLines(checks) {
|
|
7212
7245
|
return checks.map(checkLine);
|
|
7213
7246
|
}
|
|
7214
|
-
function preflightStep(deps = hostDeps$
|
|
7247
|
+
function preflightStep(deps = hostDeps$6) {
|
|
7215
7248
|
return {
|
|
7216
7249
|
id: "preflight",
|
|
7217
7250
|
title: "Check this machine",
|
|
@@ -7230,7 +7263,7 @@ function preflightStep(deps = hostDeps$7) {
|
|
|
7230
7263
|
}
|
|
7231
7264
|
//#endregion
|
|
7232
7265
|
//#region src/steps/runtime.ts
|
|
7233
|
-
const STEP_ID$
|
|
7266
|
+
const STEP_ID$2 = "runtime";
|
|
7234
7267
|
const MIN_PORT = 1;
|
|
7235
7268
|
const MAX_PORT = 65535;
|
|
7236
7269
|
const PORT_PATTERN = /^\d+$/;
|
|
@@ -7244,7 +7277,7 @@ const NO_RUNTIME_FLAGS = {
|
|
|
7244
7277
|
jar: null,
|
|
7245
7278
|
port: null
|
|
7246
7279
|
};
|
|
7247
|
-
function hostDeps$
|
|
7280
|
+
function hostDeps$5(ctx) {
|
|
7248
7281
|
return {
|
|
7249
7282
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7250
7283
|
releases: releaseLookup(ctx.signal)
|
|
@@ -7373,9 +7406,9 @@ async function persistedRuntime(ctx, options, deps) {
|
|
|
7373
7406
|
});
|
|
7374
7407
|
return chosen;
|
|
7375
7408
|
}
|
|
7376
|
-
function runtimeStep(options, deps = hostDeps$
|
|
7409
|
+
function runtimeStep(options, deps = hostDeps$5) {
|
|
7377
7410
|
return {
|
|
7378
|
-
id: STEP_ID$
|
|
7411
|
+
id: STEP_ID$2,
|
|
7379
7412
|
title: "Install Metabase",
|
|
7380
7413
|
redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
|
|
7381
7414
|
async detect(ctx) {
|
|
@@ -7399,207 +7432,6 @@ function runtimeStep(options, deps = hostDeps$6) {
|
|
|
7399
7432
|
}
|
|
7400
7433
|
};
|
|
7401
7434
|
}
|
|
7402
|
-
//#endregion
|
|
7403
|
-
//#region src/steps/sample-database.ts
|
|
7404
|
-
const STEP_ID$2 = "sample-database";
|
|
7405
|
-
const SAMPLE_DATABASE_DISPLAY_NAME = "Sample Database";
|
|
7406
|
-
const POSTGRES_ENGINE = "postgres";
|
|
7407
|
-
const NOT_FOUND_STATUS$1 = 404;
|
|
7408
|
-
function hostDeps$5(ctx) {
|
|
7409
|
-
return {
|
|
7410
|
-
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7411
|
-
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7412
|
-
readyTimeoutMs: SAMPLE_DATABASE_READY_TIMEOUT_MS,
|
|
7413
|
-
now: () => /* @__PURE__ */ new Date()
|
|
7414
|
-
};
|
|
7415
|
-
}
|
|
7416
|
-
function isNotServingYet$1(error) {
|
|
7417
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7418
|
-
}
|
|
7419
|
-
function isNotFound$2(error) {
|
|
7420
|
-
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1;
|
|
7421
|
-
}
|
|
7422
|
-
function requireApiKey(ctx) {
|
|
7423
|
-
if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
|
|
7424
|
-
return ctx.secrets.apiKey;
|
|
7425
|
-
}
|
|
7426
|
-
async function recordedDatabase(record, client) {
|
|
7427
|
-
if (record.databaseId === null) return null;
|
|
7428
|
-
try {
|
|
7429
|
-
return await client.database.get(record.databaseId);
|
|
7430
|
-
} catch (error) {
|
|
7431
|
-
if (isNotFound$2(error)) return null;
|
|
7432
|
-
throw error;
|
|
7433
|
-
}
|
|
7434
|
-
}
|
|
7435
|
-
async function persistedRecord(ctx, deps) {
|
|
7436
|
-
const existing = ctx.state.sampleDatabase;
|
|
7437
|
-
if (existing !== null) {
|
|
7438
|
-
if ((await sampleDatabaseContainer(existing, deps.runtime.docker, deps.runtime.rdeHome).status()).installed || await deps.runtime.ports.isFree(existing.port)) return {
|
|
7439
|
-
record: existing,
|
|
7440
|
-
staleDatabaseId: null
|
|
7441
|
-
};
|
|
7442
|
-
const pointer = existing.databaseId === null ? "" : `; Metabase database ${existing.databaseId} still points at the old one`;
|
|
7443
|
-
ctx.ui.warn(`Port ${existing.port} is in use by another program; the sample database moves to a new port${pointer}`);
|
|
7444
|
-
}
|
|
7445
|
-
const claims = await portClaims(ctx.state, deps.runtime);
|
|
7446
|
-
const port = await deps.runtime.ports.findFree(SAMPLE_DATABASE_PORT, claims);
|
|
7447
|
-
if (port !== 25432) ctx.ui.warn(`Port ${SAMPLE_DATABASE_PORT} is in use or claimed; using ${port} for the sample database`);
|
|
7448
|
-
const record = {
|
|
7449
|
-
containerName: sampleContainerName(ctx.state.name, deps.runtime.rdeHome),
|
|
7450
|
-
image: SAMPLE_DATABASE_IMAGE,
|
|
7451
|
-
port,
|
|
7452
|
-
databaseId: null
|
|
7453
|
-
};
|
|
7454
|
-
await ctx.saveState({
|
|
7455
|
-
...ctx.state,
|
|
7456
|
-
sampleDatabase: record
|
|
7457
|
-
});
|
|
7458
|
-
return {
|
|
7459
|
-
record,
|
|
7460
|
-
staleDatabaseId: existing?.databaseId ?? null
|
|
7461
|
-
};
|
|
7462
|
-
}
|
|
7463
|
-
const LINUX = "linux";
|
|
7464
|
-
async function gatewayAddress(ctx, deps) {
|
|
7465
|
-
if (!(ctx.state.runtime?.kind === "docker") || deps.runtime.platform !== LINUX) return null;
|
|
7466
|
-
return deps.runtime.docker.bridgeGateway();
|
|
7467
|
-
}
|
|
7468
|
-
async function recordSampleDatabaseMove(ctx, from, to) {
|
|
7469
|
-
ctx.ui.warn(`Port ${from.port} is taken; the sample database moved to ${to.port}`);
|
|
7470
|
-
const record = {
|
|
7471
|
-
...to,
|
|
7472
|
-
databaseId: null
|
|
7473
|
-
};
|
|
7474
|
-
await ctx.saveState({
|
|
7475
|
-
...ctx.state,
|
|
7476
|
-
sampleDatabase: record,
|
|
7477
|
-
databases: ctx.state.databases.filter((entry) => entry.id !== from.databaseId)
|
|
7478
|
-
});
|
|
7479
|
-
return record;
|
|
7480
|
-
}
|
|
7481
|
-
async function installAndStart(ctx, sidecar, installed, deps) {
|
|
7482
|
-
const installing = ctx.ui.spinner(`Installing the sample database from ${installed.image}`);
|
|
7483
|
-
try {
|
|
7484
|
-
await sidecar.install({
|
|
7485
|
-
onProgress: (line) => installing.update(line),
|
|
7486
|
-
alsoOn: await gatewayAddress(ctx, deps)
|
|
7487
|
-
});
|
|
7488
|
-
} catch (error) {
|
|
7489
|
-
installing.fail("Sample database install failed");
|
|
7490
|
-
throw error;
|
|
7491
|
-
}
|
|
7492
|
-
installing.stop(`Sample database container ${installed.containerName} on port ${installed.port}`);
|
|
7493
|
-
const started = await startOnFreePort(installed, deps.runtime.docker, deps.runtime.rdeHome);
|
|
7494
|
-
const record = started.state.port === installed.port ? installed : await recordSampleDatabaseMove(ctx, installed, started.state);
|
|
7495
|
-
const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
|
|
7496
|
-
try {
|
|
7497
|
-
await sidecar.waitReady({
|
|
7498
|
-
timeoutMs: deps.readyTimeoutMs,
|
|
7499
|
-
signal: ctx.signal
|
|
7500
|
-
});
|
|
7501
|
-
} catch (error) {
|
|
7502
|
-
waiting.fail("Postgres did not become ready");
|
|
7503
|
-
throw error;
|
|
7504
|
-
}
|
|
7505
|
-
waiting.stop(`Postgres is ready on ${LOOPBACK_HOST}:${record.port}`);
|
|
7506
|
-
return record;
|
|
7507
|
-
}
|
|
7508
|
-
function connectionDetails(record, configured, deps) {
|
|
7509
|
-
const alias = runtimeFromState(configured.runtime, deps.runtime).hostAlias();
|
|
7510
|
-
return applyHostAlias({
|
|
7511
|
-
host: "localhost",
|
|
7512
|
-
port: record.port,
|
|
7513
|
-
dbname: SAMPLE_DATABASE_NAME,
|
|
7514
|
-
user: SAMPLE_DATABASE_USER,
|
|
7515
|
-
password: SAMPLE_DATABASE_PASSWORD
|
|
7516
|
-
}, alias).details;
|
|
7517
|
-
}
|
|
7518
|
-
function toRecord(database, createdAt) {
|
|
7519
|
-
return {
|
|
7520
|
-
id: database.id,
|
|
7521
|
-
name: database.name,
|
|
7522
|
-
engine: database.engine,
|
|
7523
|
-
createdAt
|
|
7524
|
-
};
|
|
7525
|
-
}
|
|
7526
|
-
async function connectDatabase(ctx, choice, configured, client, deps) {
|
|
7527
|
-
const { record } = choice;
|
|
7528
|
-
const known = await recordedDatabase(record, client);
|
|
7529
|
-
if (known !== null) {
|
|
7530
|
-
ctx.ui.info(`${known.name} is already connected as database ${known.id}`);
|
|
7531
|
-
return known;
|
|
7532
|
-
}
|
|
7533
|
-
const spinner = ctx.ui.spinner(`Connecting ${SAMPLE_DATABASE_DISPLAY_NAME}…`);
|
|
7534
|
-
let database;
|
|
7535
|
-
try {
|
|
7536
|
-
database = await client.database.create({
|
|
7537
|
-
name: SAMPLE_DATABASE_DISPLAY_NAME,
|
|
7538
|
-
engine: POSTGRES_ENGINE,
|
|
7539
|
-
details: connectionDetails(record, configured, deps)
|
|
7540
|
-
});
|
|
7541
|
-
} catch (error) {
|
|
7542
|
-
spinner.fail(`Could not connect ${SAMPLE_DATABASE_DISPLAY_NAME}`);
|
|
7543
|
-
throw error;
|
|
7544
|
-
}
|
|
7545
|
-
const stale = /* @__PURE__ */ new Set([record.databaseId, choice.staleDatabaseId]);
|
|
7546
|
-
const others = ctx.state.databases.filter((entry) => !stale.has(entry.id));
|
|
7547
|
-
await ctx.saveState({
|
|
7548
|
-
...ctx.state,
|
|
7549
|
-
sampleDatabase: {
|
|
7550
|
-
...record,
|
|
7551
|
-
databaseId: database.id
|
|
7552
|
-
},
|
|
7553
|
-
databases: [...others, toRecord(database, deps.now().toISOString())]
|
|
7554
|
-
});
|
|
7555
|
-
spinner.stop(`Connected ${database.name} as database ${database.id}`);
|
|
7556
|
-
return database;
|
|
7557
|
-
}
|
|
7558
|
-
function usageLines(record) {
|
|
7559
|
-
return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms can write to this database."].join("\n");
|
|
7560
|
-
}
|
|
7561
|
-
function sampleDatabaseStep(deps = hostDeps$5) {
|
|
7562
|
-
return {
|
|
7563
|
-
id: STEP_ID$2,
|
|
7564
|
-
title: "Connect a sample database",
|
|
7565
|
-
async detect(ctx) {
|
|
7566
|
-
const resolved = deps(ctx);
|
|
7567
|
-
if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
|
|
7568
|
-
const record = ctx.state.sampleDatabase;
|
|
7569
|
-
const configured = configuredRuntime(ctx.state);
|
|
7570
|
-
if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7571
|
-
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker, resolved.runtime.rdeHome).status()).installed || !await isInstanceRunning(ctx.state, resolved.runtime)) return "todo";
|
|
7572
|
-
const client = resolved.client(configured.url, {
|
|
7573
|
-
kind: "api-key",
|
|
7574
|
-
key: ctx.secrets.apiKey
|
|
7575
|
-
});
|
|
7576
|
-
try {
|
|
7577
|
-
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
7578
|
-
} catch (error) {
|
|
7579
|
-
if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
|
|
7580
|
-
throw error;
|
|
7581
|
-
}
|
|
7582
|
-
},
|
|
7583
|
-
async run(ctx) {
|
|
7584
|
-
const resolved = deps(ctx);
|
|
7585
|
-
const configured = requireConfiguredRuntime(ctx.state);
|
|
7586
|
-
const apiKey = requireApiKey(ctx);
|
|
7587
|
-
const docker = await resolved.runtime.docker.available();
|
|
7588
|
-
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
7589
|
-
const persisted = await persistedRecord(ctx, resolved);
|
|
7590
|
-
const record = await installAndStart(ctx, sampleDatabaseContainer(persisted.record, resolved.runtime.docker, resolved.runtime.rdeHome), persisted.record, resolved);
|
|
7591
|
-
await connectDatabase(ctx, {
|
|
7592
|
-
...persisted,
|
|
7593
|
-
record
|
|
7594
|
-
}, configured, resolved.client(configured.url, {
|
|
7595
|
-
kind: "api-key",
|
|
7596
|
-
key: apiKey
|
|
7597
|
-
}), resolved);
|
|
7598
|
-
ctx.ui.note(usageLines(record), "Sample database");
|
|
7599
|
-
return { outcome: "completed" };
|
|
7600
|
-
}
|
|
7601
|
-
};
|
|
7602
|
-
}
|
|
7603
7435
|
const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
|
|
7604
7436
|
//#endregion
|
|
7605
7437
|
//#region src/steps/start-metabase.ts
|
|
@@ -7613,7 +7445,7 @@ function hostDeps$4(ctx) {
|
|
|
7613
7445
|
};
|
|
7614
7446
|
}
|
|
7615
7447
|
function isNotHealthy(error) {
|
|
7616
|
-
return error
|
|
7448
|
+
return isMetabaseNotServing(error) || error instanceof MetabaseApiError;
|
|
7617
7449
|
}
|
|
7618
7450
|
async function runningVersion(client) {
|
|
7619
7451
|
return (await client.session.properties()).version.tag;
|
|
@@ -7621,21 +7453,6 @@ async function runningVersion(client) {
|
|
|
7621
7453
|
function progressLabel(progress) {
|
|
7622
7454
|
return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
|
|
7623
7455
|
}
|
|
7624
|
-
async function startSampleDatabase(ctx, deps) {
|
|
7625
|
-
const record = ctx.state.sampleDatabase;
|
|
7626
|
-
if (record === null) return;
|
|
7627
|
-
const started = await startOnFreePort(record, deps.runtime.docker, deps.runtime.rdeHome);
|
|
7628
|
-
if (started.state.port !== record.port) await recordSampleDatabaseMove(ctx, record, started.state);
|
|
7629
|
-
switch (started.outcome) {
|
|
7630
|
-
case "missing":
|
|
7631
|
-
ctx.ui.warn(`Sample database container ${record.containerName} is missing; \`rde init\` recreates it`);
|
|
7632
|
-
return;
|
|
7633
|
-
case "started":
|
|
7634
|
-
ctx.ui.info(`Sample database container ${record.containerName} started`);
|
|
7635
|
-
return;
|
|
7636
|
-
case "running": return;
|
|
7637
|
-
}
|
|
7638
|
-
}
|
|
7639
7456
|
async function withMoveHint(ctx, deps, taken) {
|
|
7640
7457
|
const claims = await portClaims(ctx.state, deps.runtime);
|
|
7641
7458
|
const free = await deps.runtime.ports.findFree(taken.port + 1, claims);
|
|
@@ -7672,7 +7489,6 @@ function startMetabaseStep(deps = hostDeps$4) {
|
|
|
7672
7489
|
const runtime = runtimeFromState(configured.runtime, resolved.runtime);
|
|
7673
7490
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
7674
7491
|
await startRuntime(ctx, runtime, resolved);
|
|
7675
|
-
await startSampleDatabase(ctx, resolved);
|
|
7676
7492
|
const spinner = ctx.ui.spinner("Starting Metabase…");
|
|
7677
7493
|
let shown = 0;
|
|
7678
7494
|
try {
|
|
@@ -7718,9 +7534,6 @@ function hostDeps$3(ctx) {
|
|
|
7718
7534
|
probe: hostAgentProbe()
|
|
7719
7535
|
};
|
|
7720
7536
|
}
|
|
7721
|
-
function isNotServing$1(error) {
|
|
7722
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7723
|
-
}
|
|
7724
7537
|
function loginLine(kind, email) {
|
|
7725
7538
|
switch (kind) {
|
|
7726
7539
|
case "generated":
|
|
@@ -7737,7 +7550,7 @@ async function databaseLines$1(state, apiKey, deps) {
|
|
|
7737
7550
|
key: apiKey
|
|
7738
7551
|
}).database.listWithStubs()).data;
|
|
7739
7552
|
} catch (error) {
|
|
7740
|
-
if (
|
|
7553
|
+
if (isMetabaseNotServing(error)) return ["Databases: Metabase is not running; `rde start`"];
|
|
7741
7554
|
if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
|
|
7742
7555
|
throw error;
|
|
7743
7556
|
}
|
|
@@ -7786,7 +7599,6 @@ async function summaryLines(state, apiKey, deps) {
|
|
|
7786
7599
|
if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
|
|
7787
7600
|
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
7788
7601
|
lines.push(...await databaseLines$1(state, apiKey, deps));
|
|
7789
|
-
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
7790
7602
|
lines.push(...gitLines(state));
|
|
7791
7603
|
const installed = await skillsCli(deps.runner).list();
|
|
7792
7604
|
const agents = await detectAgents(deps.probe);
|
|
@@ -7819,9 +7631,6 @@ function hostDeps$2(ctx) {
|
|
|
7819
7631
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
7820
7632
|
};
|
|
7821
7633
|
}
|
|
7822
|
-
function isNotServingYet(error) {
|
|
7823
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7824
|
-
}
|
|
7825
7634
|
function transformsStep(deps = hostDeps$2) {
|
|
7826
7635
|
return {
|
|
7827
7636
|
id: STEP_ID,
|
|
@@ -7838,7 +7647,7 @@ function transformsStep(deps = hostDeps$2) {
|
|
|
7838
7647
|
try {
|
|
7839
7648
|
return await client.setting.transformsDecided() ? "done" : "todo";
|
|
7840
7649
|
} catch (error) {
|
|
7841
|
-
if (
|
|
7650
|
+
if (isMetabaseNotServing(error) || isRejectedCredential(error)) return "todo";
|
|
7842
7651
|
throw error;
|
|
7843
7652
|
}
|
|
7844
7653
|
},
|
|
@@ -7865,7 +7674,7 @@ const DEFAULT_STEP_OPTIONS = {
|
|
|
7865
7674
|
runtime: NO_RUNTIME_FLAGS,
|
|
7866
7675
|
agents: DETECT_AGENTS,
|
|
7867
7676
|
admin: DEFAULT_ADMIN,
|
|
7868
|
-
databases:
|
|
7677
|
+
databases: DATABASE_REQUIRED,
|
|
7869
7678
|
updates: "published"
|
|
7870
7679
|
};
|
|
7871
7680
|
const LOCAL_STEP_OPTIONS = {
|
|
@@ -7881,7 +7690,6 @@ const STEP_BY_ID = {
|
|
|
7881
7690
|
"admin-user": (options) => adminUserStep(options.admin),
|
|
7882
7691
|
"api-key": () => apiKeyStep(),
|
|
7883
7692
|
transforms: () => transformsStep(),
|
|
7884
|
-
"sample-database": () => sampleDatabaseStep(),
|
|
7885
7693
|
license: () => licenseStep(),
|
|
7886
7694
|
databases: (options) => databasesStep(options.databases),
|
|
7887
7695
|
"git-sync": () => gitSyncStep(),
|
|
@@ -7957,7 +7765,7 @@ const SECTIONS = {
|
|
|
7957
7765
|
}).optional(),
|
|
7958
7766
|
"api-key": z.strictObject({ profile: MbProfileChoice }).optional(),
|
|
7959
7767
|
license: z.strictObject({ token: EnvString.nullable() }).optional(),
|
|
7960
|
-
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(),
|
|
7961
7769
|
"git-sync": z.strictObject({
|
|
7962
7770
|
location: z.enum([
|
|
7963
7771
|
"here",
|
|
@@ -8029,8 +7837,8 @@ function runtimeKindOf(runtime) {
|
|
|
8029
7837
|
return null;
|
|
8030
7838
|
}
|
|
8031
7839
|
function databasesRequest(databases, run) {
|
|
8032
|
-
if (databases !== void 0) return
|
|
8033
|
-
return run.interactive ? "ask
|
|
7840
|
+
if (databases !== void 0) return "named";
|
|
7841
|
+
return run.interactive ? "ask" : "required";
|
|
8034
7842
|
}
|
|
8035
7843
|
function agentsOption(agents, run) {
|
|
8036
7844
|
if (agents !== void 0) return {
|
|
@@ -8092,7 +7900,6 @@ async function loadInitAnswers(path) {
|
|
|
8092
7900
|
}
|
|
8093
7901
|
function stepsAskingForWork(answers) {
|
|
8094
7902
|
const license = answers.license;
|
|
8095
|
-
const databases = answers.databases;
|
|
8096
7903
|
const git = answers["git-sync"];
|
|
8097
7904
|
const agents = answers["agents-skills"];
|
|
8098
7905
|
return [
|
|
@@ -8114,7 +7921,7 @@ function stepsAskingForWork(answers) {
|
|
|
8114
7921
|
},
|
|
8115
7922
|
{
|
|
8116
7923
|
step: "databases",
|
|
8117
|
-
asks: databases !== void 0
|
|
7924
|
+
asks: answers.databases !== void 0
|
|
8118
7925
|
},
|
|
8119
7926
|
{
|
|
8120
7927
|
step: "git-sync",
|
|
@@ -8151,7 +7958,6 @@ const FLAG_FOR_KEY$1 = {
|
|
|
8151
7958
|
[API_KEY_KEYS.password]: "--admin-password",
|
|
8152
7959
|
[API_KEY_KEYS.profile]: "--mb-profile",
|
|
8153
7960
|
[LICENSE_KEYS.token]: "--license-token or --skip-license",
|
|
8154
|
-
[DATABASE_KEYS.skip]: "--skip-databases",
|
|
8155
7961
|
[GIT_SYNC_KEYS.location]: "--git-dir",
|
|
8156
7962
|
[GIT_SYNC_KEYS.path]: "--git-dir",
|
|
8157
7963
|
[GIT_SYNC_KEYS.remote]: "--git-remote or --skip-git",
|
|
@@ -8159,7 +7965,7 @@ const FLAG_FOR_KEY$1 = {
|
|
|
8159
7965
|
[GIT_SYNC_KEYS.token]: "--git-token"
|
|
8160
7966
|
};
|
|
8161
7967
|
const DATABASE_FLAG_FOR_NAME = {
|
|
8162
|
-
engine: "--database
|
|
7968
|
+
engine: "--database",
|
|
8163
7969
|
uri: "--database"
|
|
8164
7970
|
};
|
|
8165
7971
|
function howToAnswer(key) {
|
|
@@ -8250,13 +8056,6 @@ function licenseSection(flags, secrets) {
|
|
|
8250
8056
|
}
|
|
8251
8057
|
return secrets.has("--license-token") ? { token: secrets.get("--license-token") } : void 0;
|
|
8252
8058
|
}
|
|
8253
|
-
function databasesSection(flags) {
|
|
8254
|
-
if (flags.skipDatabases) {
|
|
8255
|
-
if (flags.database.length > 0) throw conflict("--skip-databases", "--database");
|
|
8256
|
-
return [];
|
|
8257
|
-
}
|
|
8258
|
-
return flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0;
|
|
8259
|
-
}
|
|
8260
8059
|
function gitRemoteKind(flag, url) {
|
|
8261
8060
|
if (flag === void 0) return url === void 0 ? void 0 : "hosted";
|
|
8262
8061
|
const kind = GIT_REMOTE_NAMES[flag];
|
|
@@ -8377,7 +8176,7 @@ async function answersFromFlags(flags, readStdin) {
|
|
|
8377
8176
|
runtime: runtimeSection(flags),
|
|
8378
8177
|
"admin-user": adminSection(flags, secrets),
|
|
8379
8178
|
license: licenseSection(flags, secrets),
|
|
8380
|
-
databases:
|
|
8179
|
+
databases: flags.database.length > 0 ? flags.database.map(parseDatabase) : void 0,
|
|
8381
8180
|
"git-sync": gitSection(flags, secrets),
|
|
8382
8181
|
"agents-skills": agentsSection(flags),
|
|
8383
8182
|
"api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
|
|
@@ -8802,6 +8601,7 @@ const dbAdd = defineRdeCommand({
|
|
|
8802
8601
|
description: "Engine of a --url that fits several engines; the error names them"
|
|
8803
8602
|
}
|
|
8804
8603
|
},
|
|
8604
|
+
interrupted: INTERRUPTED,
|
|
8805
8605
|
async run(args) {
|
|
8806
8606
|
if (args.url === void 0 && !stdinIsTty()) throw new ConfigError(NEEDS_URL_ERROR);
|
|
8807
8607
|
const name = await defaultInstanceName();
|
|
@@ -8833,6 +8633,7 @@ const dbList = defineRdeCommand({
|
|
|
8833
8633
|
description: "Print the list as JSON",
|
|
8834
8634
|
default: false
|
|
8835
8635
|
} },
|
|
8636
|
+
interrupted: INTERRUPTED,
|
|
8836
8637
|
async run(args) {
|
|
8837
8638
|
const name = await defaultInstanceName();
|
|
8838
8639
|
const state = await requireInstanceState(name);
|
|
@@ -9365,31 +9166,46 @@ async function credentialRows(state, secrets, apiKey, deps) {
|
|
|
9365
9166
|
stateFileRow(files)
|
|
9366
9167
|
];
|
|
9367
9168
|
}
|
|
9368
|
-
function
|
|
9369
|
-
|
|
9169
|
+
function brokenProfileRepair(profile, method) {
|
|
9170
|
+
if (method === MbAuthMethod.enum.oauth) return {
|
|
9171
|
+
hint: `\`mb auth login --profile ${profile}\` logs it in again; or \`mb auth logout --profile ${profile}\`, then \`rde init --only api-key\` hands the profile to rde's API key`,
|
|
9172
|
+
step: null
|
|
9173
|
+
};
|
|
9174
|
+
return {
|
|
9175
|
+
hint: "`rde init --only api-key` logs it in again",
|
|
9176
|
+
step: API_KEY_STEP
|
|
9177
|
+
};
|
|
9178
|
+
}
|
|
9179
|
+
function unprobedReason(method, healthy, access) {
|
|
9180
|
+
if (access.kind === "accepted") return null;
|
|
9181
|
+
if (method !== MbAuthMethod.enum.oauth) return access.reason;
|
|
9182
|
+
return healthy ? null : NOT_HEALTHY_REASON;
|
|
9370
9183
|
}
|
|
9371
|
-
async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
|
|
9184
|
+
async function mbProfileRow(state, url, deps, profiles, mbUnusableReason, healthy, access) {
|
|
9185
|
+
const id = "mb-profile";
|
|
9372
9186
|
const label = "mb profile";
|
|
9373
|
-
|
|
9374
|
-
if (
|
|
9375
|
-
const
|
|
9376
|
-
if (
|
|
9377
|
-
detail: `${
|
|
9187
|
+
const profile = state.mbProfile;
|
|
9188
|
+
if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
|
|
9189
|
+
const status = await mbTool(deps.runner).authStatus(profile);
|
|
9190
|
+
if (!status.present || status.url === null) return failed(id, label, {
|
|
9191
|
+
detail: `${profile} is not configured`,
|
|
9378
9192
|
hint: "`rde init --only api-key`",
|
|
9379
|
-
step:
|
|
9193
|
+
step: API_KEY_STEP
|
|
9380
9194
|
});
|
|
9381
|
-
if (
|
|
9382
|
-
detail: `${
|
|
9195
|
+
if (!sameMetabaseUrl(status.url, url)) return failed(id, label, {
|
|
9196
|
+
detail: `${profile} points at ${status.url}, not at this instance (${url})`,
|
|
9383
9197
|
hint: "`rde init --only api-key` logs it back in against the instance",
|
|
9384
|
-
step:
|
|
9198
|
+
step: API_KEY_STEP
|
|
9385
9199
|
});
|
|
9386
|
-
|
|
9387
|
-
|
|
9388
|
-
|
|
9389
|
-
|
|
9200
|
+
const unprobed = unprobedReason(status.method, healthy, access);
|
|
9201
|
+
if (unprobed !== null) return skipped(id, label, unprobed);
|
|
9202
|
+
const how = status.method === MbAuthMethod.enum.oauth ? " (OAuth)" : "";
|
|
9203
|
+
const entry = (await profiles()).find((candidate) => candidate.profile === profile);
|
|
9204
|
+
if (entry === void 0 || entry.status !== "ok") return failed(id, label, {
|
|
9205
|
+
detail: `${profile}${how}: ${entry === void 0 ? "not listed by `mb auth list`" : entry.status}`,
|
|
9206
|
+
...brokenProfileRepair(profile, status.method)
|
|
9390
9207
|
});
|
|
9391
|
-
|
|
9392
|
-
return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
|
|
9208
|
+
return ok(id, label, `${profile}${how}: ok${entry.user === null ? "" : ` as ${entry.user.name}`}`);
|
|
9393
9209
|
}
|
|
9394
9210
|
function hostOf(url) {
|
|
9395
9211
|
return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
|
|
@@ -9410,8 +9226,8 @@ async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
|
|
|
9410
9226
|
async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
|
|
9411
9227
|
const rows = [];
|
|
9412
9228
|
if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
|
|
9413
|
-
if (env.url !== null) rows.push(note$1(`env.${ENV_MB_URL}`, ENV_MB_URL, `every \`mb\` call goes to ${hostOf(env.url)}, whatever \`--profile\` says
|
|
9414
|
-
if (env.apiKeySet) rows.push(note$1(`env.${ENV_MB_API_KEY}`, ENV_MB_API_KEY, "set (value not shown): every `mb` call sends it, whatever `--profile` says
|
|
9229
|
+
if (env.url !== null) rows.push(note$1(`env.${ENV_MB_URL}`, ENV_MB_URL, `every \`mb\` call goes to ${hostOf(env.url)}, whatever \`--profile\` says`, unsetHint(ENV_MB_URL)));
|
|
9230
|
+
if (env.apiKeySet) rows.push(note$1(`env.${ENV_MB_API_KEY}`, ENV_MB_API_KEY, "set (value not shown): every `mb` call sends it, whatever `--profile` says", unsetHint(ENV_MB_API_KEY)));
|
|
9415
9231
|
return rows;
|
|
9416
9232
|
}
|
|
9417
9233
|
const PROFILE_STATUS_WORDS = {
|
|
@@ -9441,6 +9257,11 @@ async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
|
|
|
9441
9257
|
function isNotFound$1(error) {
|
|
9442
9258
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
|
|
9443
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
|
+
}
|
|
9444
9265
|
async function licenseRow(client) {
|
|
9445
9266
|
const label = "license";
|
|
9446
9267
|
try {
|
|
@@ -9450,9 +9271,9 @@ async function licenseRow(client) {
|
|
|
9450
9271
|
hint: "`rde init --from license` with a valid token",
|
|
9451
9272
|
step: "license"
|
|
9452
9273
|
});
|
|
9453
|
-
return ok("license", label,
|
|
9274
|
+
return ok("license", label, coverageDetail(licensedFeatureCoverage(status.features)));
|
|
9454
9275
|
} catch (error) {
|
|
9455
|
-
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`);
|
|
9456
9277
|
throw error;
|
|
9457
9278
|
}
|
|
9458
9279
|
}
|
|
@@ -9470,55 +9291,16 @@ async function databasesRow(state, client) {
|
|
|
9470
9291
|
const names = state.databases.map((record) => record.name).join(", ");
|
|
9471
9292
|
return ok("databases", label, `${state.databases.length} connected: ${names}`);
|
|
9472
9293
|
}
|
|
9473
|
-
const SAMPLE_DATABASE_STEP = "sample-database";
|
|
9474
|
-
const SAMPLE_LABEL = "sample database";
|
|
9475
|
-
async function sampleDatabaseRow(record, deps, dockerOk, access) {
|
|
9476
|
-
const id = "sample-database";
|
|
9477
|
-
if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
|
|
9478
|
-
const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
|
|
9479
|
-
if (!status.installed) return failed(id, SAMPLE_LABEL, {
|
|
9480
|
-
detail: `${record.containerName} does not exist`,
|
|
9481
|
-
hint: "`rde init` recreates it",
|
|
9482
|
-
step: SAMPLE_DATABASE_STEP
|
|
9483
|
-
});
|
|
9484
|
-
if (!status.running) return failed(id, SAMPLE_LABEL, {
|
|
9485
|
-
detail: `${record.containerName} is stopped`,
|
|
9486
|
-
hint: "`rde start`",
|
|
9487
|
-
step: "start-metabase"
|
|
9488
|
-
});
|
|
9489
|
-
if (await deps.runtime.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
|
|
9490
|
-
detail: `${record.containerName} runs but port ${record.port} is not answering`,
|
|
9491
|
-
hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
|
|
9492
|
-
step: null
|
|
9493
|
-
});
|
|
9494
|
-
const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
|
|
9495
|
-
if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
|
|
9496
|
-
detail: `${where}; not connected to Metabase yet`,
|
|
9497
|
-
hint: "`rde init` connects it",
|
|
9498
|
-
step: SAMPLE_DATABASE_STEP
|
|
9499
|
-
});
|
|
9500
|
-
if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
|
|
9501
|
-
try {
|
|
9502
|
-
const database = await access.client.database.get(record.databaseId);
|
|
9503
|
-
return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} ${syncLabel(database.initial_sync_status)}`);
|
|
9504
|
-
} catch (error) {
|
|
9505
|
-
if (isNotFound$1(error)) return failed(id, SAMPLE_LABEL, {
|
|
9506
|
-
detail: `${where}; database ${record.databaseId} is gone from Metabase`,
|
|
9507
|
-
hint: "`rde init` connects it again",
|
|
9508
|
-
step: SAMPLE_DATABASE_STEP
|
|
9509
|
-
});
|
|
9510
|
-
throw error;
|
|
9511
|
-
}
|
|
9512
|
-
}
|
|
9513
9294
|
function isMbFailure$1(error) {
|
|
9514
9295
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
9515
9296
|
}
|
|
9516
|
-
async function gitSyncRow(state, deps) {
|
|
9297
|
+
async function gitSyncRow(state, instance, deps) {
|
|
9517
9298
|
const label = "remote sync";
|
|
9518
9299
|
if (!hasRemoteSync(state.license)) return skipped("git-sync", label, `needs a license with ${REMOTE_SYNC_FEATURE}`);
|
|
9519
9300
|
if (state.gitRemote === null) return skipped("git-sync", label, "not configured; `rde init --from git-sync`");
|
|
9301
|
+
if (instance === null) return skipped("git-sync", label, "no API key in the secrets file");
|
|
9520
9302
|
try {
|
|
9521
|
-
const status = await mbTool(deps.runner).gitSyncStatus(
|
|
9303
|
+
const status = await mbTool(deps.runner).gitSyncStatus(instance);
|
|
9522
9304
|
if (status.branch === null) return failed("git-sync", label, {
|
|
9523
9305
|
detail: "Metabase reports no branch",
|
|
9524
9306
|
hint: "`rde init --only git-sync`",
|
|
@@ -9662,7 +9444,6 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
|
|
|
9662
9444
|
skipped("mb-profile", "mb profile", NO_INSTANCE_REASON),
|
|
9663
9445
|
skipped("license", "license", NO_INSTANCE_REASON),
|
|
9664
9446
|
skipped("databases", "databases", NO_INSTANCE_REASON),
|
|
9665
|
-
skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE_REASON),
|
|
9666
9447
|
skipped("git-sync", "remote sync", NO_INSTANCE_REASON)
|
|
9667
9448
|
];
|
|
9668
9449
|
const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
|
|
@@ -9676,13 +9457,12 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
|
|
|
9676
9457
|
properties === null ? skipped("version", "Metabase version", NOT_HEALTHY_REASON) : versionRow(state, properties),
|
|
9677
9458
|
properties === null ? skipped("admin", "admin user", NOT_HEALTHY_REASON) : adminRow(state, properties),
|
|
9678
9459
|
...await credentialRows(state, secrets, apiKey, deps),
|
|
9679
|
-
await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
|
|
9460
|
+
await mbProfileRow(state, configured.url, deps, profiles, mbUnusableReason, healthy, apiKey.access)
|
|
9680
9461
|
];
|
|
9681
9462
|
const access = apiKey.access;
|
|
9682
9463
|
if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
|
|
9683
9464
|
else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
|
|
9684
|
-
rows.push(
|
|
9685
|
-
rows.push(await gitSyncRow(state, deps));
|
|
9465
|
+
rows.push(await gitSyncRow(state, mbInstanceOf(state, secrets), deps));
|
|
9686
9466
|
return rows;
|
|
9687
9467
|
}
|
|
9688
9468
|
async function collectDoctor(instance, state, secrets, deps) {
|
|
@@ -9691,7 +9471,7 @@ async function collectDoctor(instance, state, secrets, deps) {
|
|
|
9691
9471
|
const mb = await mbFinding(deps);
|
|
9692
9472
|
const profiles = sharedProfileList(deps);
|
|
9693
9473
|
const checks = [...machine, mb.row];
|
|
9694
|
-
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));
|
|
9695
9475
|
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
9696
9476
|
const rdeProfile = state === null ? null : state.mbProfile;
|
|
9697
9477
|
checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
|
|
@@ -9798,6 +9578,7 @@ const doctor = defineRdeCommand({
|
|
|
9798
9578
|
default: false
|
|
9799
9579
|
}
|
|
9800
9580
|
},
|
|
9581
|
+
interrupted: INTERRUPTED,
|
|
9801
9582
|
async run(args) {
|
|
9802
9583
|
const name = await instanceNameOrDefault();
|
|
9803
9584
|
const state = await readInstanceState(name);
|
|
@@ -9856,13 +9637,6 @@ function isLocation(value) {
|
|
|
9856
9637
|
function isRemote(value) {
|
|
9857
9638
|
return value === "local-bare" || value === "hosted";
|
|
9858
9639
|
}
|
|
9859
|
-
function applyDatabaseRecord(draft, record) {
|
|
9860
|
-
const value = stringOf(record.answer);
|
|
9861
|
-
const skippedConnection = record.key === DATABASE_KEYS.connection && value?.trim() === "";
|
|
9862
|
-
const finished = parseDatabaseKey(record.key)?.name === "engine" && value === "finish";
|
|
9863
|
-
const pickedNoStub = record.key === DATABASE_KEYS.stubs && record.answer.kind === "multiselect" && record.answer.values.length === 0;
|
|
9864
|
-
if (skippedConnection || finished || pickedNoStub) draft.databasesFinished = true;
|
|
9865
|
-
}
|
|
9866
9640
|
function applyAdminRecord(draft, key, value) {
|
|
9867
9641
|
if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
|
|
9868
9642
|
else if (key === ADMIN_USER_KEYS.password) admin(draft).password = envReference(ENV_ADMIN_PASSWORD);
|
|
@@ -9886,8 +9660,7 @@ function agentIds(values) {
|
|
|
9886
9660
|
function applyRecord(draft, record) {
|
|
9887
9661
|
const { key } = record;
|
|
9888
9662
|
const value = stringOf(record.answer);
|
|
9889
|
-
if (key.startsWith("
|
|
9890
|
-
else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
9663
|
+
if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
9891
9664
|
else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
|
|
9892
9665
|
else if (key === AGENTS_SKILLS_KEYS.agents && record.answer.kind === "multiselect") draft.answers["agents-skills"] = { agents: agentIds(record.answer.values) };
|
|
9893
9666
|
else if (key === API_KEY_KEYS.profile && value !== null) {
|
|
@@ -9913,13 +9686,9 @@ function databaseEntry(added, index) {
|
|
|
9913
9686
|
};
|
|
9914
9687
|
}
|
|
9915
9688
|
function answersFromRecords(records, overrides, added) {
|
|
9916
|
-
const draft = {
|
|
9917
|
-
answers: {},
|
|
9918
|
-
databasesFinished: false
|
|
9919
|
-
};
|
|
9689
|
+
const draft = { answers: {} };
|
|
9920
9690
|
for (const record of records) applyRecord(draft, record);
|
|
9921
|
-
|
|
9922
|
-
if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
|
|
9691
|
+
if (added.length > 0) draft.answers.databases = added.map(databaseEntry);
|
|
9923
9692
|
return {
|
|
9924
9693
|
...draft.answers,
|
|
9925
9694
|
...overrides.runtime !== void 0 && { runtime: overrides.runtime },
|
|
@@ -10045,7 +9814,7 @@ const init = defineRdeCommand({
|
|
|
10045
9814
|
},
|
|
10046
9815
|
runtime: {
|
|
10047
9816
|
type: "string",
|
|
10048
|
-
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`
|
|
10049
9818
|
},
|
|
10050
9819
|
image: {
|
|
10051
9820
|
type: "string",
|
|
@@ -10090,12 +9859,7 @@ const init = defineRdeCommand({
|
|
|
10090
9859
|
},
|
|
10091
9860
|
database: {
|
|
10092
9861
|
type: "string",
|
|
10093
|
-
description: "Database to connect, a connection string or <engine>=<connection string> when it fits several engines; repeatable"
|
|
10094
|
-
},
|
|
10095
|
-
"skip-databases": {
|
|
10096
|
-
type: "boolean",
|
|
10097
|
-
description: "Connect none of your own databases (the sample database is still connected)",
|
|
10098
|
-
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"
|
|
10099
9863
|
},
|
|
10100
9864
|
"git-dir": {
|
|
10101
9865
|
type: "string",
|
|
@@ -10133,10 +9897,11 @@ const init = defineRdeCommand({
|
|
|
10133
9897
|
},
|
|
10134
9898
|
yes: {
|
|
10135
9899
|
type: "boolean",
|
|
10136
|
-
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",
|
|
10137
9901
|
default: false
|
|
10138
9902
|
}
|
|
10139
9903
|
},
|
|
9904
|
+
interrupted: "Interrupted. Run `rde init` again to resume.",
|
|
10140
9905
|
async run(args, rawArgs) {
|
|
10141
9906
|
const options = stepSelection(args.from, args.only);
|
|
10142
9907
|
const runOptions = {
|
|
@@ -10153,7 +9918,6 @@ const init = defineRdeCommand({
|
|
|
10153
9918
|
licenseToken: args["license-token"],
|
|
10154
9919
|
skipLicense: args["skip-license"],
|
|
10155
9920
|
database: repeatedValues(rawArgs, "--database"),
|
|
10156
|
-
skipDatabases: args["skip-databases"],
|
|
10157
9921
|
gitDir: args["git-dir"],
|
|
10158
9922
|
gitRemote: args["git-remote"],
|
|
10159
9923
|
gitUrl: args["git-url"],
|
|
@@ -10200,6 +9964,7 @@ const logs = defineRdeCommand({
|
|
|
10200
9964
|
description: "Keep streaming new lines",
|
|
10201
9965
|
default: false
|
|
10202
9966
|
} },
|
|
9967
|
+
interrupted: null,
|
|
10203
9968
|
async run(args) {
|
|
10204
9969
|
await (await loadInstance(await defaultInstanceName(), interruptSignal)).runtime.logs({
|
|
10205
9970
|
follow: args.follow,
|
|
@@ -10214,6 +9979,7 @@ const open = defineRdeCommand({
|
|
|
10214
9979
|
name: "open",
|
|
10215
9980
|
description: "Open the Metabase instance in the browser"
|
|
10216
9981
|
},
|
|
9982
|
+
interrupted: INTERRUPTED,
|
|
10217
9983
|
async run() {
|
|
10218
9984
|
const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
|
|
10219
9985
|
printLines([await openInstance(instance.url) ? `Opened ${instance.url}` : `Open ${instance.url} in your browser`]);
|
|
@@ -10257,20 +10023,12 @@ async function moveToPort(ctx, port, deps) {
|
|
|
10257
10023
|
await runtimeFromState(next, deps.runtime).reconfigure();
|
|
10258
10024
|
ctx.ui.info(`Moved instance ${ctx.state.name} from port ${previous} to ${port}`);
|
|
10259
10025
|
}
|
|
10260
|
-
async function profileStatus(runner, profile) {
|
|
10261
|
-
try {
|
|
10262
|
-
return await mbTool(runner).authStatus(profile);
|
|
10263
|
-
} catch (error) {
|
|
10264
|
-
if (error instanceof ProcessNotFoundError) return null;
|
|
10265
|
-
throw error;
|
|
10266
|
-
}
|
|
10267
|
-
}
|
|
10268
10026
|
async function followMove(ctx, from, runner) {
|
|
10269
10027
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
10270
10028
|
const apiKey = ctx.secrets.apiKey;
|
|
10271
10029
|
const profile = ctx.state.mbProfile;
|
|
10272
|
-
const status = apiKey === null ? null : await
|
|
10273
|
-
if (apiKey !== null && status
|
|
10030
|
+
const status = apiKey === null ? null : await mbTool(runner).authStatusIfInstalled(profile);
|
|
10031
|
+
if (apiKey !== null && isLoggedInAt(status, from)) {
|
|
10274
10032
|
await mbTool(runner).authLogin({
|
|
10275
10033
|
url: configured.url,
|
|
10276
10034
|
profile,
|
|
@@ -10301,6 +10059,7 @@ const start = defineRdeCommand({
|
|
|
10301
10059
|
type: "string",
|
|
10302
10060
|
description: "Move the instance to this host port first; the mb profile follows, the data stays"
|
|
10303
10061
|
} },
|
|
10062
|
+
interrupted: INTERRUPTED,
|
|
10304
10063
|
async run(args) {
|
|
10305
10064
|
const port = args.port === void 0 ? null : requirePortFlag(args.port);
|
|
10306
10065
|
const name = await defaultInstanceName();
|
|
@@ -10353,31 +10112,15 @@ function setupOf(ctx, steps) {
|
|
|
10353
10112
|
resume: `rde init --from ${pending.id}`
|
|
10354
10113
|
};
|
|
10355
10114
|
}
|
|
10356
|
-
function isNotServing(error) {
|
|
10357
|
-
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
10358
|
-
}
|
|
10359
10115
|
async function databasesOf(ctx, deps, health) {
|
|
10360
10116
|
if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
|
|
10361
10117
|
try {
|
|
10362
10118
|
return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
|
|
10363
10119
|
} catch (error) {
|
|
10364
|
-
if (
|
|
10120
|
+
if (isMetabaseNotServing(error) || isRejectedCredential(error)) return null;
|
|
10365
10121
|
throw error;
|
|
10366
10122
|
}
|
|
10367
10123
|
}
|
|
10368
|
-
async function sampleDatabaseOf(ctx, deps, databases) {
|
|
10369
|
-
const record = ctx.state.sampleDatabase;
|
|
10370
|
-
if (record === null) return null;
|
|
10371
|
-
const status = await sampleDatabaseContainer(record, deps.runtime.docker, deps.runtime.rdeHome).status();
|
|
10372
|
-
const listed = databases?.find((database) => database.id === record.databaseId) ?? null;
|
|
10373
|
-
return {
|
|
10374
|
-
container: status.running ? "running" : status.installed ? "stopped" : "missing",
|
|
10375
|
-
port: record.port,
|
|
10376
|
-
url: sampleDatabaseUrl(record.port),
|
|
10377
|
-
databaseId: record.databaseId,
|
|
10378
|
-
initialSyncStatus: listed === null ? null : listed.initialSyncStatus
|
|
10379
|
-
};
|
|
10380
|
-
}
|
|
10381
10124
|
function taskLabel(status) {
|
|
10382
10125
|
const task = status.current_task;
|
|
10383
10126
|
return task === null ? null : `${task.sync_task_type} ${task.status}`;
|
|
@@ -10391,10 +10134,13 @@ function isMbFailure(error) {
|
|
|
10391
10134
|
}
|
|
10392
10135
|
async function gitSyncOf(ctx, deps) {
|
|
10393
10136
|
if (ctx.state.gitRemote === null) return null;
|
|
10137
|
+
if (configuredRuntime(ctx.state) === null) return { error: "no Metabase runtime configured; `rde init`" };
|
|
10138
|
+
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
10139
|
+
if (instance === null) return { error: "no API key in the secrets file; `rde init --only api-key`" };
|
|
10394
10140
|
try {
|
|
10395
10141
|
const mb = mbTool(deps.runner);
|
|
10396
|
-
const status = await mb.gitSyncStatus(
|
|
10397
|
-
const changes = await mb.gitSyncHasRemoteChanges(
|
|
10142
|
+
const status = await mb.gitSyncStatus(instance);
|
|
10143
|
+
const changes = await mb.gitSyncHasRemoteChanges(instance);
|
|
10398
10144
|
return {
|
|
10399
10145
|
branch: status.branch,
|
|
10400
10146
|
dirty: status.is_dirty,
|
|
@@ -10429,7 +10175,6 @@ async function collectStatus(ctx, deps) {
|
|
|
10429
10175
|
steps,
|
|
10430
10176
|
setup: setupOf(ctx, steps),
|
|
10431
10177
|
databases,
|
|
10432
|
-
sampleDatabase: await sampleDatabaseOf(ctx, deps, databases),
|
|
10433
10178
|
gitSync: await gitSyncOf(ctx, deps),
|
|
10434
10179
|
contentRepository: ctx.state.gitRemote === null ? null : ctx.state.gitRemote.workdir
|
|
10435
10180
|
};
|
|
@@ -10473,22 +10218,6 @@ function syncStateLabel(profile, gitSync) {
|
|
|
10473
10218
|
if (gitSync.dirty) return `unsynced changes, ${remote}`;
|
|
10474
10219
|
return `${remote}; \`${mbCommand(profile)} git-sync import\``;
|
|
10475
10220
|
}
|
|
10476
|
-
const CONTAINER_STATE_LABEL = {
|
|
10477
|
-
running: "container running",
|
|
10478
|
-
stopped: "container stopped; `rde start`",
|
|
10479
|
-
missing: "container missing; `rde init` recreates it"
|
|
10480
|
-
};
|
|
10481
|
-
function sampleConnectionLabel(report, sample) {
|
|
10482
|
-
if (sample.databaseId === null) return "not connected to Metabase yet; `rde init`";
|
|
10483
|
-
if (report.databases === null) return "sync unknown while Metabase is not running";
|
|
10484
|
-
if (sample.initialSyncStatus === null) return `database ${sample.databaseId} is gone from Metabase; \`rde init\` connects it again`;
|
|
10485
|
-
return syncLabel(sample.initialSyncStatus);
|
|
10486
|
-
}
|
|
10487
|
-
function sampleDatabaseLine(report) {
|
|
10488
|
-
const sample = report.sampleDatabase;
|
|
10489
|
-
if (sample === null) return "Sample database: not connected; `rde init` adds one";
|
|
10490
|
-
return `Sample database: ${sample.url}, ${CONTAINER_STATE_LABEL[sample.container]}, ${sampleConnectionLabel(report, sample)}`;
|
|
10491
|
-
}
|
|
10492
10221
|
function databaseLines(report) {
|
|
10493
10222
|
if (report.databases === null) return ["Databases: unavailable while Metabase is not running"];
|
|
10494
10223
|
return ["Databases:", ...databaseListingLines(report.databases).map((line) => ` ${line}`)];
|
|
@@ -10505,7 +10234,6 @@ function statusLines(report) {
|
|
|
10505
10234
|
"Steps:",
|
|
10506
10235
|
...checklistLines(report.steps).map((line) => ` ${line}`),
|
|
10507
10236
|
...databaseLines(report),
|
|
10508
|
-
sampleDatabaseLine(report),
|
|
10509
10237
|
gitSyncLine(report),
|
|
10510
10238
|
...report.contentRepository === null ? [] : [`Content repository: ${report.contentRepository}`]
|
|
10511
10239
|
];
|
|
@@ -10522,6 +10250,7 @@ const status = defineRdeCommand({
|
|
|
10522
10250
|
description: "Print the report as JSON",
|
|
10523
10251
|
default: false
|
|
10524
10252
|
} },
|
|
10253
|
+
interrupted: INTERRUPTED,
|
|
10525
10254
|
async run(args) {
|
|
10526
10255
|
const name = await defaultInstanceName();
|
|
10527
10256
|
await requireInstanceState(name);
|
|
@@ -10538,34 +10267,34 @@ const status = defineRdeCommand({
|
|
|
10538
10267
|
const stop = defineRdeCommand({
|
|
10539
10268
|
meta: {
|
|
10540
10269
|
name: "stop",
|
|
10541
|
-
description: "Stop the Metabase instance
|
|
10270
|
+
description: "Stop the Metabase instance"
|
|
10542
10271
|
},
|
|
10272
|
+
interrupted: INTERRUPTED,
|
|
10543
10273
|
async run() {
|
|
10544
|
-
const
|
|
10274
|
+
const name = await defaultInstanceName();
|
|
10275
|
+
const instance = await loadInstance(name, interruptSignal);
|
|
10545
10276
|
const before = await instance.runtime.status();
|
|
10546
10277
|
await instance.runtime.stop();
|
|
10547
10278
|
const lines = [before.running ? "Metabase stopped" : "Metabase is not running"];
|
|
10548
|
-
|
|
10549
|
-
|
|
10550
|
-
await instance.sampleDatabase.stop();
|
|
10551
|
-
lines.push(sidecar.running ? "Sample database stopped" : "Sample database is not running");
|
|
10552
|
-
}
|
|
10279
|
+
const removed = await removeLeftovers(name, interruptSignal);
|
|
10280
|
+
if (removed !== null) lines.push(removed);
|
|
10553
10281
|
printLines(lines);
|
|
10554
10282
|
}
|
|
10555
10283
|
});
|
|
10556
10284
|
//#endregion
|
|
10557
10285
|
//#region src/steps/uninstall.ts
|
|
10558
|
-
const API_KEY_METHOD = "apiKey";
|
|
10559
10286
|
function hostUninstallDeps(signal, childOutput) {
|
|
10560
10287
|
const runner = processRunner(signal);
|
|
10561
10288
|
return {
|
|
10562
10289
|
docker: dockerTool(runner),
|
|
10563
10290
|
daemon: daemonTool(runner),
|
|
10291
|
+
git: gitTool(runner),
|
|
10564
10292
|
runner,
|
|
10565
10293
|
childOutput
|
|
10566
10294
|
};
|
|
10567
10295
|
}
|
|
10568
10296
|
const NOT_TOUCHED_LINE = "The mb CLI, Docker, and Java themselves";
|
|
10297
|
+
const UNPULLED_COMMITS_SHOWN = 10;
|
|
10569
10298
|
const ANY_JAR = "-jar";
|
|
10570
10299
|
function isFileSystemError(error) {
|
|
10571
10300
|
return error instanceof Error && "code" in error;
|
|
@@ -10714,8 +10443,8 @@ async function findJar(source, removeImage) {
|
|
|
10714
10443
|
}
|
|
10715
10444
|
function isInstanceProfile(status, urls) {
|
|
10716
10445
|
if (!status.present) return false;
|
|
10717
|
-
if (urls !== null) return
|
|
10718
|
-
return status.method ===
|
|
10446
|
+
if (urls !== null) return urls.some((url) => isLoggedInAt(status, url));
|
|
10447
|
+
return status.method === MbAuthMethod.enum.apiKey && status.user?.name === "rde";
|
|
10719
10448
|
}
|
|
10720
10449
|
function instanceUrls(state) {
|
|
10721
10450
|
if (state.url === null) return null;
|
|
@@ -10723,23 +10452,34 @@ function instanceUrls(state) {
|
|
|
10723
10452
|
}
|
|
10724
10453
|
async function findMbProfiles(candidates, urls, runner) {
|
|
10725
10454
|
const mb = mbTool(runner);
|
|
10726
|
-
const
|
|
10455
|
+
const found = {
|
|
10456
|
+
removals: [],
|
|
10457
|
+
kept: []
|
|
10458
|
+
};
|
|
10727
10459
|
for (const profile of candidates) {
|
|
10728
10460
|
let status;
|
|
10729
10461
|
try {
|
|
10730
10462
|
status = await mb.authStatus(profile);
|
|
10731
10463
|
} catch (error) {
|
|
10732
|
-
if (error instanceof ProcessNotFoundError) return
|
|
10464
|
+
if (error instanceof ProcessNotFoundError) return {
|
|
10465
|
+
removals: [],
|
|
10466
|
+
kept: []
|
|
10467
|
+
};
|
|
10733
10468
|
throw error;
|
|
10734
10469
|
}
|
|
10735
|
-
if (isInstanceProfile(status, urls))
|
|
10470
|
+
if (!isInstanceProfile(status, urls)) continue;
|
|
10471
|
+
if (status.method === MbAuthMethod.enum.oauth) {
|
|
10472
|
+
found.kept.push(`mb profile ${profile}, logged in with OAuth; \`mb auth logout --profile ${profile}\` logs it out`);
|
|
10473
|
+
continue;
|
|
10474
|
+
}
|
|
10475
|
+
found.removals.push({
|
|
10736
10476
|
label: `mb profile ${profile}`,
|
|
10737
10477
|
remove: async () => {
|
|
10738
10478
|
await mb.authLogout(profile);
|
|
10739
10479
|
}
|
|
10740
10480
|
});
|
|
10741
10481
|
}
|
|
10742
|
-
return
|
|
10482
|
+
return found;
|
|
10743
10483
|
}
|
|
10744
10484
|
function skillLabel(names, entries) {
|
|
10745
10485
|
const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
|
|
@@ -10760,6 +10500,71 @@ async function findSkill(runner, childOutput) {
|
|
|
10760
10500
|
function instanceDirLabel(path, state) {
|
|
10761
10501
|
return `Instance directory ${path} (${state?.gitRemote?.kind === "local-bare" ? "Metabase data and the remote-sync repository content.git" : "Metabase data"})`;
|
|
10762
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
|
+
}
|
|
10763
10568
|
async function findConfigEntry(instance) {
|
|
10764
10569
|
const path = globalConfigPath();
|
|
10765
10570
|
try {
|
|
@@ -10802,14 +10607,13 @@ async function planUninstall(options, deps) {
|
|
|
10802
10607
|
const container = await findContainer(runtime?.containerName ?? containerName(options.instance, home), deps.docker, runtime?.image ?? null, home, { graceMs: METABASE_STOP_GRACE_MS });
|
|
10803
10608
|
if (container.removal !== null) removals.push(container.removal);
|
|
10804
10609
|
if (container.problem !== null) problems.push(container.problem);
|
|
10805
|
-
const
|
|
10806
|
-
|
|
10807
|
-
if (
|
|
10808
|
-
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);
|
|
10809
10613
|
const jarSource = state?.runtime?.kind === "jar" ? state.runtime.source : null;
|
|
10810
10614
|
const jarProcess = await findJarProcess(options.instance, jarSource, deps.daemon);
|
|
10811
10615
|
if (jarProcess !== null) removals.push(jarProcess);
|
|
10812
|
-
for (const image of [container.image,
|
|
10616
|
+
for (const image of [container.image, sampleDatabase.image]) {
|
|
10813
10617
|
if (image === null) continue;
|
|
10814
10618
|
const finding = await findImage(image, deps.docker, options.removeImage);
|
|
10815
10619
|
if (finding.removal !== null) removals.push(finding.removal);
|
|
@@ -10821,13 +10625,16 @@ async function planUninstall(options, deps) {
|
|
|
10821
10625
|
if (jar.kept !== null) kept.push(jar.kept);
|
|
10822
10626
|
}
|
|
10823
10627
|
const profiles = state === null ? await findMbProfiles([.../* @__PURE__ */ new Set([mbProfileFor(options.instance), namedMbProfile(options.instance)])], null, deps.runner) : await findMbProfiles([state.mbProfile], instanceUrls(state), deps.runner);
|
|
10824
|
-
removals.push(...profiles);
|
|
10628
|
+
removals.push(...profiles.removals);
|
|
10629
|
+
kept.push(...profiles.kept);
|
|
10825
10630
|
if (options.removeSkill) {
|
|
10826
10631
|
const skill = await findSkill(deps.runner, deps.childOutput);
|
|
10827
10632
|
if (skill !== null) removals.push(skill);
|
|
10828
10633
|
}
|
|
10829
10634
|
const dir = instanceDir(options.instance);
|
|
10830
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);
|
|
10831
10638
|
if (!options.keepData) {
|
|
10832
10639
|
const secrets = instanceSecretsPath(options.instance);
|
|
10833
10640
|
if (await pathExists(secrets)) removals.push({
|
|
@@ -10848,7 +10655,8 @@ async function planUninstall(options, deps) {
|
|
|
10848
10655
|
return {
|
|
10849
10656
|
removals,
|
|
10850
10657
|
kept,
|
|
10851
|
-
problems
|
|
10658
|
+
problems,
|
|
10659
|
+
unpulled
|
|
10852
10660
|
};
|
|
10853
10661
|
}
|
|
10854
10662
|
function planLines$1(plan) {
|
|
@@ -10903,6 +10711,10 @@ async function uninstall(options, deps, io) {
|
|
|
10903
10711
|
};
|
|
10904
10712
|
}
|
|
10905
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
|
+
}
|
|
10906
10718
|
if (!options.yes) {
|
|
10907
10719
|
if (!await io.prompts.confirm({
|
|
10908
10720
|
message: "Remove everything listed?",
|
|
@@ -10949,8 +10761,14 @@ const uninstallCommand = defineRdeCommand({
|
|
|
10949
10761
|
type: "boolean",
|
|
10950
10762
|
description: "Do not ask for confirmation",
|
|
10951
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
|
|
10952
10769
|
}
|
|
10953
10770
|
},
|
|
10771
|
+
interrupted: INTERRUPTED,
|
|
10954
10772
|
async run(args) {
|
|
10955
10773
|
const instance = args.instance ?? await instanceNameOrDefault();
|
|
10956
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})`);
|
|
@@ -10960,7 +10778,9 @@ const uninstallCommand = defineRdeCommand({
|
|
|
10960
10778
|
keepData: args["keep-data"],
|
|
10961
10779
|
removeSkill: args["remove-skill"],
|
|
10962
10780
|
removeImage: args["remove-image"],
|
|
10963
|
-
yes: args.yes
|
|
10781
|
+
yes: args.yes,
|
|
10782
|
+
force: args.force,
|
|
10783
|
+
terminal: stdinIsTty()
|
|
10964
10784
|
}, hostUninstallDeps(interruptSignal, ui.childOutput), {
|
|
10965
10785
|
prompts: clackPrompts({ confirmsWithYes: true }),
|
|
10966
10786
|
ui
|
|
@@ -11194,8 +11014,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11194
11014
|
const stopping = ctx.ui.spinner("Stopping Metabase");
|
|
11195
11015
|
await runtime.stop();
|
|
11196
11016
|
stopping.stop("Metabase stopped");
|
|
11017
|
+
options.onPhase("backing-up", backup);
|
|
11197
11018
|
await backUp(ctx, resolved.runtime.dataDir, backup);
|
|
11019
|
+
options.onPhase("switching", backup);
|
|
11198
11020
|
await switchTo(ctx, resolved, target);
|
|
11021
|
+
options.onPhase("starting", backup);
|
|
11199
11022
|
const to = await startUpgraded(ctx, resolved, client, configured, backup);
|
|
11200
11023
|
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
11201
11024
|
return {
|
|
@@ -11207,6 +11030,14 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11207
11030
|
}
|
|
11208
11031
|
//#endregion
|
|
11209
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
|
+
}
|
|
11210
11041
|
const upgradeCommand = defineRdeCommand({
|
|
11211
11042
|
meta: {
|
|
11212
11043
|
name: "upgrade",
|
|
@@ -11223,14 +11054,21 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11223
11054
|
default: false
|
|
11224
11055
|
}
|
|
11225
11056
|
},
|
|
11226
|
-
|
|
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) {
|
|
11227
11059
|
if (!args.yes && !stdinIsTty()) throw new ConfigError("`rde upgrade` asks for confirmation; pass --yes to upgrade without a terminal");
|
|
11228
11060
|
const name = await defaultInstanceName();
|
|
11229
11061
|
await requireInstanceState(name);
|
|
11230
|
-
|
|
11062
|
+
const ctx = await createWizardContext(name, { signal: interruptSignal });
|
|
11063
|
+
await upgrade(ctx, {
|
|
11231
11064
|
to: args.to ?? null,
|
|
11232
|
-
yes: args.yes
|
|
11065
|
+
yes: args.yes,
|
|
11066
|
+
onPhase: (phase, backup) => {
|
|
11067
|
+
interruption.message = interruptedUpgrade(phase, backup);
|
|
11068
|
+
}
|
|
11233
11069
|
});
|
|
11070
|
+
const removed = await removeLeftovers(name, interruptSignal);
|
|
11071
|
+
if (removed !== null) ctx.ui.info(removed);
|
|
11234
11072
|
}
|
|
11235
11073
|
});
|
|
11236
11074
|
//#endregion
|
|
@@ -11238,7 +11076,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11238
11076
|
const main = defineCommand({
|
|
11239
11077
|
meta: {
|
|
11240
11078
|
name: "rde",
|
|
11241
|
-
version: "0.1.
|
|
11079
|
+
version: "0.1.16",
|
|
11242
11080
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
11243
11081
|
},
|
|
11244
11082
|
subCommands: {
|