@metabase/rde 0.1.3-alpha.rde-fixes.c2021e7 → 0.1.3
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 +42 -83
- package/dist/cli.mjs +487 -1452
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain, showUsage } from "citty";
|
|
3
3
|
import { createWriteStream, promises } from "node:fs";
|
|
4
|
-
import { delimiter, dirname,
|
|
4
|
+
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { ZodError, z } from "zod";
|
|
6
6
|
import { homedir, tmpdir, userInfo } from "node:os";
|
|
7
7
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
@@ -11,7 +11,6 @@ import { confirm, intro, isCancel, log, multiselect, note, outro, password, sele
|
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { connect, createServer } from "node:net";
|
|
13
13
|
import semver from "semver";
|
|
14
|
-
import { randomBytes } from "node:crypto";
|
|
15
14
|
//#region src/errors.ts
|
|
16
15
|
var AbortError = class extends Error {
|
|
17
16
|
constructor(message = "interrupted") {
|
|
@@ -57,10 +56,6 @@ var MetabaseApiError = class extends Error {
|
|
|
57
56
|
this.errors = failure.errors;
|
|
58
57
|
}
|
|
59
58
|
};
|
|
60
|
-
const UNAUTHORIZED_STATUS = 401;
|
|
61
|
-
function isRejectedCredential(error) {
|
|
62
|
-
return error instanceof MetabaseApiError && error.status === UNAUTHORIZED_STATUS;
|
|
63
|
-
}
|
|
64
59
|
var MetabaseInitializingError = class extends Error {
|
|
65
60
|
progress;
|
|
66
61
|
constructor(progress) {
|
|
@@ -122,14 +117,6 @@ function installInterruptHandler(exit) {
|
|
|
122
117
|
setTimeout(() => exit(130), FORCED_EXIT_GRACE_MS).unref();
|
|
123
118
|
});
|
|
124
119
|
}
|
|
125
|
-
//#endregion
|
|
126
|
-
//#region src/output/print.ts
|
|
127
|
-
function printLines(lines) {
|
|
128
|
-
process.stdout.write(lines.join("\n") + "\n");
|
|
129
|
-
}
|
|
130
|
-
function printJson(value) {
|
|
131
|
-
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
132
|
-
}
|
|
133
120
|
const StepId = z.enum([
|
|
134
121
|
"preflight",
|
|
135
122
|
"install-mb",
|
|
@@ -170,36 +157,14 @@ const JarRuntimeState = z.object({
|
|
|
170
157
|
});
|
|
171
158
|
const RuntimeState = z.discriminatedUnion("kind", [DockerRuntimeState, JarRuntimeState]);
|
|
172
159
|
const RuntimeKind = z.enum(["docker", "jar"]);
|
|
173
|
-
const AdminCredentials = z.discriminatedUnion("kind", [
|
|
174
|
-
z.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
z.object({
|
|
179
|
-
kind: z.literal("prompted"),
|
|
180
|
-
email: z.string()
|
|
181
|
-
}),
|
|
182
|
-
z.object({
|
|
183
|
-
kind: z.literal("legacy-default"),
|
|
184
|
-
email: z.string()
|
|
185
|
-
})
|
|
186
|
-
]);
|
|
187
|
-
const AdminLogin = z.object({
|
|
160
|
+
const AdminCredentials = z.discriminatedUnion("kind", [z.object({
|
|
161
|
+
kind: z.literal("prompted"),
|
|
162
|
+
email: z.string()
|
|
163
|
+
}), z.object({
|
|
164
|
+
kind: z.literal("default"),
|
|
188
165
|
email: z.string(),
|
|
189
166
|
password: z.string()
|
|
190
|
-
});
|
|
191
|
-
const InstanceSecrets = z.object({
|
|
192
|
-
schemaVersion: z.literal(1),
|
|
193
|
-
admin: AdminLogin.nullable(),
|
|
194
|
-
apiKey: z.string().nullable()
|
|
195
|
-
});
|
|
196
|
-
function emptyInstanceSecrets() {
|
|
197
|
-
return {
|
|
198
|
-
schemaVersion: 1,
|
|
199
|
-
admin: null,
|
|
200
|
-
apiKey: null
|
|
201
|
-
};
|
|
202
|
-
}
|
|
167
|
+
})]);
|
|
203
168
|
const LicenseState = z.discriminatedUnion("activated", [z.object({ activated: z.literal(false) }), z.object({
|
|
204
169
|
activated: z.literal(true),
|
|
205
170
|
features: z.array(z.string()),
|
|
@@ -237,12 +202,13 @@ const StepRecord = z.object({
|
|
|
237
202
|
completedAt: z.string()
|
|
238
203
|
});
|
|
239
204
|
const InstanceState = z.object({
|
|
240
|
-
schemaVersion: z.literal(
|
|
205
|
+
schemaVersion: z.literal(3),
|
|
241
206
|
name: z.string(),
|
|
242
207
|
runtime: RuntimeState.nullable(),
|
|
243
208
|
url: z.string().nullable(),
|
|
244
209
|
version: z.string().nullable(),
|
|
245
210
|
admin: AdminCredentials.nullable(),
|
|
211
|
+
apiKey: z.string().nullable(),
|
|
246
212
|
mbProfile: z.string(),
|
|
247
213
|
license: LicenseState,
|
|
248
214
|
databases: z.array(DatabaseRecord),
|
|
@@ -264,26 +230,20 @@ function requireConfiguredRuntime(state) {
|
|
|
264
230
|
}
|
|
265
231
|
const DEFAULT_INSTANCE_NAME = "default";
|
|
266
232
|
const DEFAULT_MB_PROFILE = "default";
|
|
267
|
-
const
|
|
268
|
-
const MbProfileChoice = z.enum([DEFAULT_MB_PROFILE, RDE_MB_PROFILE]);
|
|
269
|
-
function namedMbProfile(instance) {
|
|
270
|
-
return instance === "default" ? RDE_MB_PROFILE : `${RDE_MB_PROFILE}-${instance}`;
|
|
271
|
-
}
|
|
272
|
-
function mbProfileFor(instance) {
|
|
273
|
-
return instance === "default" ? DEFAULT_MB_PROFILE : namedMbProfile(instance);
|
|
274
|
-
}
|
|
233
|
+
const MbProfileChoice = z.enum([DEFAULT_MB_PROFILE, "rde"]);
|
|
275
234
|
function mbCommand(profile) {
|
|
276
235
|
return profile === "default" ? "mb" : `mb --profile ${profile}`;
|
|
277
236
|
}
|
|
278
237
|
function emptyInstanceState(name) {
|
|
279
238
|
return {
|
|
280
|
-
schemaVersion:
|
|
239
|
+
schemaVersion: 3,
|
|
281
240
|
name,
|
|
282
241
|
runtime: null,
|
|
283
242
|
url: null,
|
|
284
243
|
version: null,
|
|
285
244
|
admin: null,
|
|
286
|
-
|
|
245
|
+
apiKey: null,
|
|
246
|
+
mbProfile: DEFAULT_MB_PROFILE,
|
|
287
247
|
license: { activated: false },
|
|
288
248
|
databases: [],
|
|
289
249
|
sampleDatabase: null,
|
|
@@ -296,22 +256,8 @@ function emptyInstanceState(name) {
|
|
|
296
256
|
const Versioned = z.object({ schemaVersion: z.int() });
|
|
297
257
|
const V1 = 1;
|
|
298
258
|
const V2 = 2;
|
|
299
|
-
const V3 = 3;
|
|
300
259
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
301
260
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
302
|
-
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
303
|
-
kind: z.literal("prompted"),
|
|
304
|
-
email: z.string()
|
|
305
|
-
}), z.object({
|
|
306
|
-
kind: z.literal("default"),
|
|
307
|
-
email: z.string(),
|
|
308
|
-
password: z.string()
|
|
309
|
-
})]);
|
|
310
|
-
const StateV3 = z.object({
|
|
311
|
-
schemaVersion: z.literal(V3),
|
|
312
|
-
admin: AdminV3.nullable(),
|
|
313
|
-
apiKey: z.string().nullable()
|
|
314
|
-
}).loose();
|
|
315
261
|
function liftV1(raw) {
|
|
316
262
|
return {
|
|
317
263
|
...StateV1.parse(raw),
|
|
@@ -322,67 +268,27 @@ function liftV1(raw) {
|
|
|
322
268
|
function liftV2(raw) {
|
|
323
269
|
return {
|
|
324
270
|
...StateV2.parse(raw),
|
|
325
|
-
schemaVersion:
|
|
271
|
+
schemaVersion: 3,
|
|
326
272
|
sampleDatabase: null
|
|
327
273
|
};
|
|
328
274
|
}
|
|
329
|
-
function adminV4(admin) {
|
|
330
|
-
switch (admin.kind) {
|
|
331
|
-
case "default": return {
|
|
332
|
-
kind: "legacy-default",
|
|
333
|
-
email: admin.email
|
|
334
|
-
};
|
|
335
|
-
case "prompted": return {
|
|
336
|
-
kind: "prompted",
|
|
337
|
-
email: admin.email
|
|
338
|
-
};
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
function liftV3(raw) {
|
|
342
|
-
const { admin, apiKey, ...rest } = StateV3.parse(raw);
|
|
343
|
-
const login = admin?.kind === "default" ? {
|
|
344
|
-
email: admin.email,
|
|
345
|
-
password: admin.password
|
|
346
|
-
} : null;
|
|
347
|
-
return {
|
|
348
|
-
state: InstanceState.parse({
|
|
349
|
-
...rest,
|
|
350
|
-
schemaVersion: 4,
|
|
351
|
-
admin: admin === null ? null : adminV4(admin)
|
|
352
|
-
}),
|
|
353
|
-
movedSecrets: {
|
|
354
|
-
schemaVersion: 1,
|
|
355
|
-
admin: login,
|
|
356
|
-
apiKey
|
|
357
|
-
}
|
|
358
|
-
};
|
|
359
|
-
}
|
|
360
275
|
function migrateInstanceState(raw) {
|
|
361
276
|
const { schemaVersion } = Versioned.parse(raw);
|
|
362
277
|
switch (schemaVersion) {
|
|
363
278
|
case V1: return migrateInstanceState(liftV1(raw));
|
|
364
279
|
case V2: return migrateInstanceState(liftV2(raw));
|
|
365
|
-
case
|
|
366
|
-
|
|
367
|
-
state: InstanceState.parse(raw),
|
|
368
|
-
movedSecrets: null
|
|
369
|
-
};
|
|
370
|
-
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 4)`);
|
|
280
|
+
case 3: return InstanceState.parse(raw);
|
|
281
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 3)`);
|
|
371
282
|
}
|
|
372
283
|
}
|
|
373
284
|
//#endregion
|
|
374
285
|
//#region src/env.ts
|
|
375
286
|
const ENV_RDE_HOME = "RDE_HOME";
|
|
376
|
-
const ENV_RDE_SKILL_SOURCE = "RDE_SKILL_SOURCE";
|
|
377
287
|
function readEnv(name) {
|
|
378
288
|
return process.env[name];
|
|
379
289
|
}
|
|
380
290
|
const ENV_PATH = "PATH";
|
|
381
|
-
const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
|
|
382
291
|
const ENV_JAVA_HOME = "JAVA_HOME";
|
|
383
|
-
const ENV_MB_PROFILE = "MB_PROFILE";
|
|
384
|
-
const ENV_MB_URL = "MB_URL";
|
|
385
|
-
const ENV_MB_API_KEY = "MB_API_KEY";
|
|
386
292
|
function environmentWith(overrides) {
|
|
387
293
|
return {
|
|
388
294
|
...process.env,
|
|
@@ -396,7 +302,6 @@ const INSTANCES_DIR = "instances";
|
|
|
396
302
|
const DATA_DIR = "data";
|
|
397
303
|
const BACKUPS_DIR = "backups";
|
|
398
304
|
const STATE_FILE = "state.json";
|
|
399
|
-
const SECRETS_FILE = "secrets.json";
|
|
400
305
|
const GLOBAL_CONFIG_FILE = "config.json";
|
|
401
306
|
const JARS_DIR = "jars";
|
|
402
307
|
const JAR_FILE$1 = "metabase.jar";
|
|
@@ -417,17 +322,8 @@ function instanceDataDir(name) {
|
|
|
417
322
|
function instanceBackupsDir(name) {
|
|
418
323
|
return join(instanceDir(name), BACKUPS_DIR);
|
|
419
324
|
}
|
|
420
|
-
function stateFileIn(dir) {
|
|
421
|
-
return join(dir, STATE_FILE);
|
|
422
|
-
}
|
|
423
|
-
function secretsFileIn(dir) {
|
|
424
|
-
return join(dir, SECRETS_FILE);
|
|
425
|
-
}
|
|
426
325
|
function instanceStatePath(name) {
|
|
427
|
-
return
|
|
428
|
-
}
|
|
429
|
-
function instanceSecretsPath(name) {
|
|
430
|
-
return secretsFileIn(instanceDir(name));
|
|
326
|
+
return join(instanceDir(name), STATE_FILE);
|
|
431
327
|
}
|
|
432
328
|
function globalConfigPath() {
|
|
433
329
|
return join(rdeHome(), GLOBAL_CONFIG_FILE);
|
|
@@ -441,21 +337,21 @@ function jarReleaseDir(version) {
|
|
|
441
337
|
function releaseJarIn(jarsDirectory, version) {
|
|
442
338
|
return join(jarsDirectory, version, JAR_FILE$1);
|
|
443
339
|
}
|
|
444
|
-
|
|
340
|
+
//#endregion
|
|
341
|
+
//#region src/state/store.ts
|
|
342
|
+
const FILE_MODE$1 = 384;
|
|
343
|
+
const DIR_MODE = 448;
|
|
445
344
|
function isFileNotFound$1(error) {
|
|
446
345
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
447
346
|
}
|
|
448
|
-
async function
|
|
347
|
+
async function readJsonFile(path) {
|
|
348
|
+
let raw;
|
|
449
349
|
try {
|
|
450
|
-
|
|
350
|
+
raw = await promises.readFile(path, "utf8");
|
|
451
351
|
} catch (error) {
|
|
452
352
|
if (isFileNotFound$1(error)) return null;
|
|
453
353
|
throw error;
|
|
454
354
|
}
|
|
455
|
-
}
|
|
456
|
-
async function readJsonFile(path) {
|
|
457
|
-
const raw = await readTextFile(path);
|
|
458
|
-
if (raw === null) return null;
|
|
459
355
|
try {
|
|
460
356
|
return JSON.parse(raw);
|
|
461
357
|
} catch (error) {
|
|
@@ -474,11 +370,11 @@ function parseAt(path, parse) {
|
|
|
474
370
|
async function writeJsonFile(path, value) {
|
|
475
371
|
await promises.mkdir(dirname(path), {
|
|
476
372
|
recursive: true,
|
|
477
|
-
mode:
|
|
373
|
+
mode: DIR_MODE
|
|
478
374
|
});
|
|
479
375
|
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
480
|
-
await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode:
|
|
481
|
-
await promises.chmod(tmpPath,
|
|
376
|
+
await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode: FILE_MODE$1 });
|
|
377
|
+
await promises.chmod(tmpPath, FILE_MODE$1);
|
|
482
378
|
try {
|
|
483
379
|
await promises.rename(tmpPath, path);
|
|
484
380
|
} catch (error) {
|
|
@@ -490,12 +386,7 @@ async function readInstanceState(name) {
|
|
|
490
386
|
const path = instanceStatePath(name);
|
|
491
387
|
const raw = await readJsonFile(path);
|
|
492
388
|
if (raw === null) return null;
|
|
493
|
-
|
|
494
|
-
if (migrated.movedSecrets !== null) {
|
|
495
|
-
await writeInstanceSecrets(name, migrated.movedSecrets);
|
|
496
|
-
await writeInstanceState(migrated.state);
|
|
497
|
-
}
|
|
498
|
-
return migrated.state;
|
|
389
|
+
return parseAt(path, () => migrateInstanceState(raw));
|
|
499
390
|
}
|
|
500
391
|
async function writeInstanceState(state) {
|
|
501
392
|
await writeJsonFile(instanceStatePath(state.name), state);
|
|
@@ -505,35 +396,6 @@ async function requireInstanceState(name) {
|
|
|
505
396
|
if (state === null) throw new ConfigError(`no instance named ${name}; run \`rde init\` first`);
|
|
506
397
|
return state;
|
|
507
398
|
}
|
|
508
|
-
async function readInstanceSecrets(name) {
|
|
509
|
-
const path = instanceSecretsPath(name);
|
|
510
|
-
const raw = await readJsonFile(path);
|
|
511
|
-
if (raw === null) return null;
|
|
512
|
-
return parseAt(path, () => InstanceSecrets.parse(raw));
|
|
513
|
-
}
|
|
514
|
-
async function writeInstanceSecrets(name, secrets) {
|
|
515
|
-
await writeJsonFile(instanceSecretsPath(name), secrets);
|
|
516
|
-
await promises.chmod(instanceDir(name), 448);
|
|
517
|
-
}
|
|
518
|
-
async function modeOf(path) {
|
|
519
|
-
try {
|
|
520
|
-
return (await promises.stat(path)).mode & PERMISSION_BITS;
|
|
521
|
-
} catch (error) {
|
|
522
|
-
if (isFileNotFound$1(error)) return null;
|
|
523
|
-
throw error;
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
const StateWithApiKey = z.object({ apiKey: z.string() }).loose();
|
|
527
|
-
const StateWithPassword = z.object({ admin: z.object({ password: z.string() }).loose() }).loose();
|
|
528
|
-
async function inspectCredentialFiles(dir) {
|
|
529
|
-
const raw = await readJsonFile(stateFileIn(dir));
|
|
530
|
-
const secretsInState = [...StateWithApiKey.safeParse(raw).success ? ["apiKey"] : [], ...StateWithPassword.safeParse(raw).success ? ["admin.password"] : []];
|
|
531
|
-
return {
|
|
532
|
-
dirMode: await modeOf(dir),
|
|
533
|
-
secretsMode: await modeOf(secretsFileIn(dir)),
|
|
534
|
-
secretsInState
|
|
535
|
-
};
|
|
536
|
-
}
|
|
537
399
|
async function readGlobalConfig() {
|
|
538
400
|
const path = globalConfigPath();
|
|
539
401
|
const raw = await readJsonFile(path);
|
|
@@ -544,120 +406,6 @@ async function writeGlobalConfig(config) {
|
|
|
544
406
|
await writeJsonFile(globalConfigPath(), config);
|
|
545
407
|
}
|
|
546
408
|
//#endregion
|
|
547
|
-
//#region src/steps/credentials.ts
|
|
548
|
-
const LEGACY_NOTE = "This is the public default password; when the instance holds nothing you need, `rde uninstall` then `rde init` recreates it with a generated one";
|
|
549
|
-
const PROMPTED_NOTE = "The password is the one entered during `rde init`; rde never stored it";
|
|
550
|
-
function storedPassword(state, secrets) {
|
|
551
|
-
if (secrets === null || secrets.admin === null) throw new ConfigError(`the secrets file of instance ${state.name} holds no admin password; \`rde doctor\` says what to do`);
|
|
552
|
-
return secrets.admin.password;
|
|
553
|
-
}
|
|
554
|
-
function credentialsReport(state, secrets) {
|
|
555
|
-
const admin = state.admin;
|
|
556
|
-
if (admin === null) throw new ConfigError(`instance ${state.name} has no admin user yet; run \`rde init\``);
|
|
557
|
-
const login = {
|
|
558
|
-
instance: state.name,
|
|
559
|
-
url: state.url,
|
|
560
|
-
email: admin.email
|
|
561
|
-
};
|
|
562
|
-
switch (admin.kind) {
|
|
563
|
-
case "generated": return {
|
|
564
|
-
...login,
|
|
565
|
-
password: storedPassword(state, secrets),
|
|
566
|
-
note: null
|
|
567
|
-
};
|
|
568
|
-
case "legacy-default": return {
|
|
569
|
-
...login,
|
|
570
|
-
password: storedPassword(state, secrets),
|
|
571
|
-
note: LEGACY_NOTE
|
|
572
|
-
};
|
|
573
|
-
case "prompted": return {
|
|
574
|
-
...login,
|
|
575
|
-
password: null,
|
|
576
|
-
note: PROMPTED_NOTE
|
|
577
|
-
};
|
|
578
|
-
}
|
|
579
|
-
}
|
|
580
|
-
function credentialsLines(report) {
|
|
581
|
-
const url = report.url === null ? [] : [`Metabase: ${report.url}`];
|
|
582
|
-
const password = report.password === null ? [] : [`Password: ${report.password}`];
|
|
583
|
-
const note = report.note === null ? [] : [report.note];
|
|
584
|
-
return [
|
|
585
|
-
...url,
|
|
586
|
-
`Email: ${report.email}`,
|
|
587
|
-
...password,
|
|
588
|
-
...note
|
|
589
|
-
];
|
|
590
|
-
}
|
|
591
|
-
//#endregion
|
|
592
|
-
//#region src/commands/define.ts
|
|
593
|
-
function repeatedValues(rawArgs, flag) {
|
|
594
|
-
const values = [];
|
|
595
|
-
const inline = `${flag}=`;
|
|
596
|
-
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
597
|
-
const arg = rawArgs[index];
|
|
598
|
-
if (arg === void 0) continue;
|
|
599
|
-
if (arg.startsWith(inline)) values.push(arg.slice(inline.length));
|
|
600
|
-
else if (arg === flag) {
|
|
601
|
-
const next = rawArgs[index + 1];
|
|
602
|
-
if (next !== void 0) {
|
|
603
|
-
values.push(next);
|
|
604
|
-
index += 1;
|
|
605
|
-
}
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
return values;
|
|
609
|
-
}
|
|
610
|
-
function defineRdeCommand(def) {
|
|
611
|
-
return defineCommand({
|
|
612
|
-
meta: def.meta,
|
|
613
|
-
...def.args !== void 0 && { args: def.args },
|
|
614
|
-
async run({ args, rawArgs }) {
|
|
615
|
-
try {
|
|
616
|
-
await def.run(args, rawArgs);
|
|
617
|
-
} catch (error) {
|
|
618
|
-
reportError(error);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
//#endregion
|
|
624
|
-
//#region src/commands/instance.ts
|
|
625
|
-
async function defaultInstanceName() {
|
|
626
|
-
const config = await readGlobalConfig();
|
|
627
|
-
if (config === null) throw new ConfigError("no Metabase instance yet; run `rde init` first");
|
|
628
|
-
return config.defaultInstance;
|
|
629
|
-
}
|
|
630
|
-
async function instanceNameOrDefault() {
|
|
631
|
-
try {
|
|
632
|
-
return (await readGlobalConfig())?.defaultInstance ?? "default";
|
|
633
|
-
} catch (error) {
|
|
634
|
-
if (error instanceof ConfigError) return DEFAULT_INSTANCE_NAME;
|
|
635
|
-
throw error;
|
|
636
|
-
}
|
|
637
|
-
}
|
|
638
|
-
//#endregion
|
|
639
|
-
//#region src/commands/credentials.ts
|
|
640
|
-
const credentials = defineRdeCommand({
|
|
641
|
-
meta: {
|
|
642
|
-
name: "credentials",
|
|
643
|
-
description: "Print the admin login of the instance: URL, email, and password"
|
|
644
|
-
},
|
|
645
|
-
args: { json: {
|
|
646
|
-
type: "boolean",
|
|
647
|
-
description: "Print the login as JSON",
|
|
648
|
-
default: false
|
|
649
|
-
} },
|
|
650
|
-
async run(args) {
|
|
651
|
-
const name = await defaultInstanceName();
|
|
652
|
-
const report = credentialsReport(await requireInstanceState(name), await readInstanceSecrets(name));
|
|
653
|
-
if (args.json) {
|
|
654
|
-
printJson(report);
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
printLines(credentialsLines(report));
|
|
658
|
-
}
|
|
659
|
-
});
|
|
660
|
-
//#endregion
|
|
661
409
|
//#region src/connections/parse-uri.ts
|
|
662
410
|
var UriParseError = class extends Error {
|
|
663
411
|
engine;
|
|
@@ -1324,18 +1072,6 @@ function tokenStatusResource(http) {
|
|
|
1324
1072
|
return { get };
|
|
1325
1073
|
}
|
|
1326
1074
|
//#endregion
|
|
1327
|
-
//#region src/metabase/domain/user.ts
|
|
1328
|
-
const CurrentUser = z.object({ id: z.int() }).loose();
|
|
1329
|
-
//#endregion
|
|
1330
|
-
//#region src/metabase/resources/user.ts
|
|
1331
|
-
function userResource(http) {
|
|
1332
|
-
/** GET /api/user/current */
|
|
1333
|
-
async function current() {
|
|
1334
|
-
return http.requestJson("GET", "/api/user/current", { schema: CurrentUser });
|
|
1335
|
-
}
|
|
1336
|
-
return { current };
|
|
1337
|
-
}
|
|
1338
|
-
//#endregion
|
|
1339
1075
|
//#region src/metabase/client.ts
|
|
1340
1076
|
const HEALTH_POLL_INTERVAL_MS = 2e3;
|
|
1341
1077
|
function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
@@ -1356,8 +1092,7 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
|
1356
1092
|
setting: settingResource(http),
|
|
1357
1093
|
tokenStatus: tokenStatusResource(http),
|
|
1358
1094
|
database: databaseResource(http),
|
|
1359
|
-
remoteSync: remoteSyncResource(http)
|
|
1360
|
-
user: userResource(http)
|
|
1095
|
+
remoteSync: remoteSyncResource(http)
|
|
1361
1096
|
};
|
|
1362
1097
|
}
|
|
1363
1098
|
function isNotReadyYet(error) {
|
|
@@ -1762,9 +1497,6 @@ function settledOrBack(value) {
|
|
|
1762
1497
|
if (isBack(value)) return BACK;
|
|
1763
1498
|
return settled(value);
|
|
1764
1499
|
}
|
|
1765
|
-
function preselected(opts) {
|
|
1766
|
-
return opts.initialValue ?? opts.recommended;
|
|
1767
|
-
}
|
|
1768
1500
|
function withBackChoice(choices) {
|
|
1769
1501
|
return [...choices, {
|
|
1770
1502
|
value: BACK,
|
|
@@ -1818,11 +1550,10 @@ function clackPrompts() {
|
|
|
1818
1550
|
},
|
|
1819
1551
|
async select(opts) {
|
|
1820
1552
|
requireTty(opts.message);
|
|
1821
|
-
const initialValue = preselected(opts);
|
|
1822
1553
|
return settled(await select({
|
|
1823
1554
|
message: opts.message,
|
|
1824
1555
|
options: opts.choices,
|
|
1825
|
-
...initialValue !== void 0 && { initialValue }
|
|
1556
|
+
...opts.initialValue !== void 0 && { initialValue: opts.initialValue }
|
|
1826
1557
|
}));
|
|
1827
1558
|
},
|
|
1828
1559
|
async selectOrBack(opts) {
|
|
@@ -2045,8 +1776,6 @@ function databaseListingLines(listing) {
|
|
|
2045
1776
|
//#endregion
|
|
2046
1777
|
//#region src/steps/databases/flow.ts
|
|
2047
1778
|
const DATABASE_KEYS = {
|
|
2048
|
-
connection: "databases.connection",
|
|
2049
|
-
detectedEngine: "databases.detected-engine",
|
|
2050
1779
|
engine: "databases.engine",
|
|
2051
1780
|
skip: "databases.skip",
|
|
2052
1781
|
mode: "databases.mode",
|
|
@@ -2061,11 +1790,6 @@ const FINISH = "finish";
|
|
|
2061
1790
|
const RETRY = "try-again";
|
|
2062
1791
|
const BAD_REQUEST_STATUS$1 = 400;
|
|
2063
1792
|
const OPTION_FIELDS = ["additional-options", "jdbc-flags"];
|
|
2064
|
-
const CONNECTION_PLACEHOLDER = "leave empty to skip";
|
|
2065
|
-
const START_SCREEN = {
|
|
2066
|
-
engine: "pick-engine",
|
|
2067
|
-
"connection-string": "connection-string"
|
|
2068
|
-
};
|
|
2069
1793
|
const FRESH$1 = {
|
|
2070
1794
|
engine: null,
|
|
2071
1795
|
uri: null,
|
|
@@ -2106,66 +1830,6 @@ async function confirmSkip(run) {
|
|
|
2106
1830
|
initialValue: false
|
|
2107
1831
|
});
|
|
2108
1832
|
}
|
|
2109
|
-
function isCreatableEntry(entry) {
|
|
2110
|
-
return entry !== null && entry.creatable;
|
|
2111
|
-
}
|
|
2112
|
-
async function pickDetectedEngine(run, candidates) {
|
|
2113
|
-
const picked = await run.deps.prompts.selectOrBack({
|
|
2114
|
-
key: DATABASE_KEYS.detectedEngine,
|
|
2115
|
-
message: "Which database is it?",
|
|
2116
|
-
choices: candidates.map((entry) => ({
|
|
2117
|
-
value: entry.key,
|
|
2118
|
-
label: entry.displayName
|
|
2119
|
-
}))
|
|
2120
|
-
});
|
|
2121
|
-
if (picked === BACK) return BACK;
|
|
2122
|
-
const entry = candidates.find((candidate) => candidate.key === picked);
|
|
2123
|
-
if (entry === void 0) throw new Error(`engine ${picked} was not offered`);
|
|
2124
|
-
return entry;
|
|
2125
|
-
}
|
|
2126
|
-
function connectionStringScreen(run) {
|
|
2127
|
-
return async (model) => {
|
|
2128
|
-
let draft = model.uri;
|
|
2129
|
-
for (;;) {
|
|
2130
|
-
const uri = (await run.deps.prompts.text({
|
|
2131
|
-
key: DATABASE_KEYS.connection,
|
|
2132
|
-
message: "Connection string of your database (Enter to skip; `rde db add` connects one later)",
|
|
2133
|
-
placeholder: CONNECTION_PLACEHOLDER,
|
|
2134
|
-
...draft !== null && { initialValue: draft }
|
|
2135
|
-
})).trim();
|
|
2136
|
-
draft = uri;
|
|
2137
|
-
if (uri.length === 0) return {
|
|
2138
|
-
type: "done",
|
|
2139
|
-
model
|
|
2140
|
-
};
|
|
2141
|
-
const candidates = detectEngine(uri).map((key) => run.deps.catalog.entry(key)).filter(isCreatableEntry);
|
|
2142
|
-
const [first, ...others] = candidates;
|
|
2143
|
-
if (first === void 0) {
|
|
2144
|
-
run.deps.ui.warn("rde can't tell which database that connection string is for; pick it");
|
|
2145
|
-
return {
|
|
2146
|
-
type: "next",
|
|
2147
|
-
to: "pick-engine",
|
|
2148
|
-
model: FRESH$1
|
|
2149
|
-
};
|
|
2150
|
-
}
|
|
2151
|
-
const engine = others.length === 0 ? first : await pickDetectedEngine(run, candidates);
|
|
2152
|
-
if (engine === BACK) continue;
|
|
2153
|
-
const parsed = parseUri(uri, engine.key);
|
|
2154
|
-
if (parsed instanceof UriParseError) {
|
|
2155
|
-
run.deps.ui.warn(parsed.message);
|
|
2156
|
-
return {
|
|
2157
|
-
type: "next",
|
|
2158
|
-
to: "pick-engine",
|
|
2159
|
-
model: FRESH$1
|
|
2160
|
-
};
|
|
2161
|
-
}
|
|
2162
|
-
return toReview(run, model, {
|
|
2163
|
-
engine,
|
|
2164
|
-
parsed
|
|
2165
|
-
});
|
|
2166
|
-
}
|
|
2167
|
-
};
|
|
2168
|
-
}
|
|
2169
1833
|
function pickEngineScreen(run) {
|
|
2170
1834
|
return async (model) => {
|
|
2171
1835
|
const total = run.deps.existingCount + run.created.length;
|
|
@@ -2527,20 +2191,19 @@ async function runDatabaseFlow(deps) {
|
|
|
2527
2191
|
created: []
|
|
2528
2192
|
};
|
|
2529
2193
|
await runFlow({
|
|
2530
|
-
"connection-string": connectionStringScreen(run),
|
|
2531
2194
|
"pick-engine": pickEngineScreen(run),
|
|
2532
2195
|
"paste-uri": pasteUriScreen(run),
|
|
2533
2196
|
review: reviewScreen(run),
|
|
2534
2197
|
test: testScreen(run),
|
|
2535
2198
|
create: createScreen(run),
|
|
2536
2199
|
another: anotherScreen(run)
|
|
2537
|
-
},
|
|
2200
|
+
}, "pick-engine", FRESH$1);
|
|
2538
2201
|
return { created: run.created };
|
|
2539
2202
|
}
|
|
2540
2203
|
//#endregion
|
|
2541
2204
|
//#region src/steps/databases.ts
|
|
2542
2205
|
const STEP_ID$9 = "databases";
|
|
2543
|
-
const
|
|
2206
|
+
const ASK_DATABASES = { requested: false };
|
|
2544
2207
|
function hostDatabasesDeps(signal) {
|
|
2545
2208
|
return {
|
|
2546
2209
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
@@ -2551,9 +2214,9 @@ function hostDatabasesDeps(signal) {
|
|
|
2551
2214
|
function isNotServingYet$1(error) {
|
|
2552
2215
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
2553
2216
|
}
|
|
2554
|
-
function requireApiKey$2(state
|
|
2555
|
-
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
2556
|
-
return apiKey;
|
|
2217
|
+
function requireApiKey$2(state) {
|
|
2218
|
+
if (state.apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
2219
|
+
return state.apiKey;
|
|
2557
2220
|
}
|
|
2558
2221
|
function hostAliasOf(runtime) {
|
|
2559
2222
|
switch (runtime.kind) {
|
|
@@ -2569,15 +2232,15 @@ function toRecord$1(database, createdAt) {
|
|
|
2569
2232
|
createdAt
|
|
2570
2233
|
};
|
|
2571
2234
|
}
|
|
2572
|
-
function authenticatedClient(state,
|
|
2235
|
+
function authenticatedClient(state, deps) {
|
|
2573
2236
|
const configured = requireConfiguredRuntime(state);
|
|
2574
2237
|
return deps.client(configured.url, {
|
|
2575
2238
|
kind: "api-key",
|
|
2576
|
-
key: requireApiKey$2(state
|
|
2239
|
+
key: requireApiKey$2(state)
|
|
2577
2240
|
});
|
|
2578
2241
|
}
|
|
2579
|
-
async function listDatabases(state,
|
|
2580
|
-
const listed = await authenticatedClient(state,
|
|
2242
|
+
async function listDatabases(state, deps) {
|
|
2243
|
+
const listed = await authenticatedClient(state, deps).database.list();
|
|
2581
2244
|
const records = new Map(state.databases.map((record) => [record.id, record]));
|
|
2582
2245
|
const onServer = listed.data.map((database) => ({
|
|
2583
2246
|
id: database.id,
|
|
@@ -2596,14 +2259,13 @@ async function listDatabases(state, apiKey, deps) {
|
|
|
2596
2259
|
}));
|
|
2597
2260
|
return [...onServer, ...missing];
|
|
2598
2261
|
}
|
|
2599
|
-
async function
|
|
2262
|
+
async function addDatabases(ctx, deps) {
|
|
2600
2263
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
2601
|
-
const client = authenticatedClient(ctx.state,
|
|
2264
|
+
const client = authenticatedClient(ctx.state, deps);
|
|
2602
2265
|
const properties = await client.session.properties();
|
|
2603
2266
|
const existing = await client.database.list();
|
|
2604
2267
|
if (existing.data.length > 0) ctx.ui.note(existingDatabaseLines(existing.data).join("\n"), "Connected databases");
|
|
2605
2268
|
const result = await runDatabaseFlow({
|
|
2606
|
-
start,
|
|
2607
2269
|
prompts: ctx.prompts,
|
|
2608
2270
|
ui: ctx.ui,
|
|
2609
2271
|
catalog: engineCatalog(properties.engines),
|
|
@@ -2619,17 +2281,7 @@ async function connectDatabases(ctx, deps, start) {
|
|
|
2619
2281
|
});
|
|
2620
2282
|
}
|
|
2621
2283
|
});
|
|
2622
|
-
return
|
|
2623
|
-
existing: existing.data.length,
|
|
2624
|
-
created: result.created.length
|
|
2625
|
-
};
|
|
2626
|
-
}
|
|
2627
|
-
async function addDatabases(ctx, deps) {
|
|
2628
|
-
const connected = await connectDatabases(ctx, deps, "engine");
|
|
2629
|
-
return connected.existing + connected.created > 0 ? "completed" : "skipped";
|
|
2630
|
-
}
|
|
2631
|
-
async function askForOwnDatabase(ctx, deps) {
|
|
2632
|
-
return (await connectDatabases(ctx, deps, "connection-string")).created > 0 ? "completed" : "skipped";
|
|
2284
|
+
return existing.data.length + result.created.length > 0 ? "completed" : "skipped";
|
|
2633
2285
|
}
|
|
2634
2286
|
function hostDeps$12(ctx) {
|
|
2635
2287
|
return hostDatabasesDeps(ctx.signal);
|
|
@@ -2637,9 +2289,6 @@ function hostDeps$12(ctx) {
|
|
|
2637
2289
|
function sampleDatabaseId(state) {
|
|
2638
2290
|
return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
|
|
2639
2291
|
}
|
|
2640
|
-
function skippedBefore(state) {
|
|
2641
|
-
return state.steps[STEP_ID$9]?.status === "skipped";
|
|
2642
|
-
}
|
|
2643
2292
|
function ownDatabaseIds(state) {
|
|
2644
2293
|
const sampleId = sampleDatabaseId(state);
|
|
2645
2294
|
return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
|
|
@@ -2650,28 +2299,21 @@ function databasesStep(options, deps = hostDeps$12) {
|
|
|
2650
2299
|
title: "Connect your databases",
|
|
2651
2300
|
async detect(ctx) {
|
|
2652
2301
|
const configured = configuredRuntime(ctx.state);
|
|
2653
|
-
if (configured === null || ctx.
|
|
2302
|
+
if (configured === null || ctx.state.apiKey === null) return "todo";
|
|
2654
2303
|
const own = ownDatabaseIds(ctx.state);
|
|
2655
|
-
|
|
2656
|
-
const asking = options.request === "ask-once" && !skippedBefore(ctx.state);
|
|
2657
|
-
if (own.size === 0 && !asking) {
|
|
2658
|
-
if (options.request === "named") return "todo";
|
|
2659
|
-
return sampleId !== null || skippedBefore(ctx.state) ? "done" : "todo";
|
|
2660
|
-
}
|
|
2661
|
-
const isOwn = (id) => own.size > 0 ? own.has(id) : id !== sampleId;
|
|
2304
|
+
if (own.size === 0) return sampleDatabaseId(ctx.state) !== null && !options.requested ? "done" : "todo";
|
|
2662
2305
|
const client = deps(ctx).client(configured.url, {
|
|
2663
2306
|
kind: "api-key",
|
|
2664
|
-
key: ctx.
|
|
2307
|
+
key: ctx.state.apiKey
|
|
2665
2308
|
});
|
|
2666
2309
|
try {
|
|
2667
|
-
return (await client.database.list()).data.some((database) =>
|
|
2310
|
+
return (await client.database.list()).data.some((database) => own.has(database.id)) ? "done" : "todo";
|
|
2668
2311
|
} catch (error) {
|
|
2669
|
-
if (isNotServingYet$1(error)
|
|
2312
|
+
if (isNotServingYet$1(error)) return "todo";
|
|
2670
2313
|
throw error;
|
|
2671
2314
|
}
|
|
2672
2315
|
},
|
|
2673
2316
|
run(ctx) {
|
|
2674
|
-
if (options.request === "ask-once") return askForOwnDatabase(ctx, deps(ctx));
|
|
2675
2317
|
return addDatabases(ctx, deps(ctx));
|
|
2676
2318
|
}
|
|
2677
2319
|
};
|
|
@@ -2749,11 +2391,6 @@ async function createWizardContext(instanceName, options) {
|
|
|
2749
2391
|
await writeInstanceState(next);
|
|
2750
2392
|
ctx.state = next;
|
|
2751
2393
|
},
|
|
2752
|
-
secrets: await readInstanceSecrets(instanceName) ?? emptyInstanceSecrets(),
|
|
2753
|
-
saveSecrets: async (next) => {
|
|
2754
|
-
await writeInstanceSecrets(instanceName, next);
|
|
2755
|
-
ctx.secrets = next;
|
|
2756
|
-
},
|
|
2757
2394
|
prompts: options.prompts ?? clackPrompts(),
|
|
2758
2395
|
ui: clackUi(),
|
|
2759
2396
|
signal: options.signal,
|
|
@@ -2762,6 +2399,53 @@ async function createWizardContext(instanceName, options) {
|
|
|
2762
2399
|
return ctx;
|
|
2763
2400
|
}
|
|
2764
2401
|
//#endregion
|
|
2402
|
+
//#region src/commands/define.ts
|
|
2403
|
+
function repeatedValues(rawArgs, flag) {
|
|
2404
|
+
const values = [];
|
|
2405
|
+
const inline = `${flag}=`;
|
|
2406
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
2407
|
+
const arg = rawArgs[index];
|
|
2408
|
+
if (arg === void 0) continue;
|
|
2409
|
+
if (arg.startsWith(inline)) values.push(arg.slice(inline.length));
|
|
2410
|
+
else if (arg === flag) {
|
|
2411
|
+
const next = rawArgs[index + 1];
|
|
2412
|
+
if (next !== void 0) {
|
|
2413
|
+
values.push(next);
|
|
2414
|
+
index += 1;
|
|
2415
|
+
}
|
|
2416
|
+
}
|
|
2417
|
+
}
|
|
2418
|
+
return values;
|
|
2419
|
+
}
|
|
2420
|
+
function defineRdeCommand(def) {
|
|
2421
|
+
return defineCommand({
|
|
2422
|
+
meta: def.meta,
|
|
2423
|
+
...def.args !== void 0 && { args: def.args },
|
|
2424
|
+
async run({ args, rawArgs }) {
|
|
2425
|
+
try {
|
|
2426
|
+
await def.run(args, rawArgs);
|
|
2427
|
+
} catch (error) {
|
|
2428
|
+
reportError(error);
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
});
|
|
2432
|
+
}
|
|
2433
|
+
//#endregion
|
|
2434
|
+
//#region src/commands/instance.ts
|
|
2435
|
+
async function defaultInstanceName() {
|
|
2436
|
+
const config = await readGlobalConfig();
|
|
2437
|
+
if (config === null) throw new ConfigError("no Metabase instance yet; run `rde init` first");
|
|
2438
|
+
return config.defaultInstance;
|
|
2439
|
+
}
|
|
2440
|
+
async function instanceNameOrDefault() {
|
|
2441
|
+
try {
|
|
2442
|
+
return (await readGlobalConfig())?.defaultInstance ?? "default";
|
|
2443
|
+
} catch (error) {
|
|
2444
|
+
if (error instanceof ConfigError) return DEFAULT_INSTANCE_NAME;
|
|
2445
|
+
throw error;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
//#endregion
|
|
2765
2449
|
//#region src/commands/db/add.ts
|
|
2766
2450
|
const dbAdd = defineRdeCommand({
|
|
2767
2451
|
meta: {
|
|
@@ -2771,10 +2455,19 @@ const dbAdd = defineRdeCommand({
|
|
|
2771
2455
|
async run() {
|
|
2772
2456
|
const name = await defaultInstanceName();
|
|
2773
2457
|
await requireInstanceState(name);
|
|
2774
|
-
|
|
2458
|
+
const ctx = await createWizardContext(name, { signal: interruptSignal });
|
|
2459
|
+
await databasesStep(ASK_DATABASES).run(ctx);
|
|
2775
2460
|
}
|
|
2776
2461
|
});
|
|
2777
2462
|
//#endregion
|
|
2463
|
+
//#region src/output/print.ts
|
|
2464
|
+
function printLines(lines) {
|
|
2465
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
2466
|
+
}
|
|
2467
|
+
function printJson(value) {
|
|
2468
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
2469
|
+
}
|
|
2470
|
+
//#endregion
|
|
2778
2471
|
//#region src/commands/db/list.ts
|
|
2779
2472
|
const dbList = defineRdeCommand({
|
|
2780
2473
|
meta: {
|
|
@@ -2787,10 +2480,7 @@ const dbList = defineRdeCommand({
|
|
|
2787
2480
|
default: false
|
|
2788
2481
|
} },
|
|
2789
2482
|
async run(args) {
|
|
2790
|
-
const
|
|
2791
|
-
const state = await requireInstanceState(name);
|
|
2792
|
-
const secrets = await readInstanceSecrets(name);
|
|
2793
|
-
const listing = await listDatabases(state, secrets === null ? null : secrets.apiKey, hostDatabasesDeps(interruptSignal));
|
|
2483
|
+
const listing = await listDatabases(await requireInstanceState(await defaultInstanceName()), hostDatabasesDeps(interruptSignal));
|
|
2794
2484
|
if (args.json) {
|
|
2795
2485
|
printJson(listing);
|
|
2796
2486
|
return;
|
|
@@ -3119,20 +2809,15 @@ async function isExecutable(path) {
|
|
|
3119
2809
|
return false;
|
|
3120
2810
|
}
|
|
3121
2811
|
}
|
|
3122
|
-
async function
|
|
2812
|
+
async function whichCommand(name) {
|
|
3123
2813
|
const searchPath = readEnv(ENV_PATH);
|
|
3124
|
-
if (searchPath === void 0) return
|
|
3125
|
-
const
|
|
3126
|
-
for (const dir of new Set(searchPath.split(delimiter))) {
|
|
2814
|
+
if (searchPath === void 0) return null;
|
|
2815
|
+
for (const dir of searchPath.split(delimiter)) {
|
|
3127
2816
|
if (dir.length === 0) continue;
|
|
3128
2817
|
const candidate = join(dir, name);
|
|
3129
|
-
if (await isExecutable(candidate))
|
|
2818
|
+
if (await isExecutable(candidate)) return candidate;
|
|
3130
2819
|
}
|
|
3131
|
-
return
|
|
3132
|
-
}
|
|
3133
|
-
async function whichCommand(name) {
|
|
3134
|
-
const [first] = await whichAll(name);
|
|
3135
|
-
return first ?? null;
|
|
2820
|
+
return null;
|
|
3136
2821
|
}
|
|
3137
2822
|
//#endregion
|
|
3138
2823
|
//#region src/tools/java.ts
|
|
@@ -3938,7 +3623,7 @@ async function detectAgents(probe) {
|
|
|
3938
3623
|
//#endregion
|
|
3939
3624
|
//#region src/tools/mb.ts
|
|
3940
3625
|
const MB = "mb";
|
|
3941
|
-
const MB_INSTALL_SPEC = `@metabase/cli@alpha-
|
|
3626
|
+
const MB_INSTALL_SPEC = `@metabase/cli@alpha-transform-tests`;
|
|
3942
3627
|
const GLOBAL_INSTALL = {
|
|
3943
3628
|
npm: {
|
|
3944
3629
|
command: "npm",
|
|
@@ -3976,117 +3661,44 @@ const GLOBAL_INSTALL = {
|
|
|
3976
3661
|
function mbInstallCommand(packageManager) {
|
|
3977
3662
|
return GLOBAL_INSTALL[packageManager];
|
|
3978
3663
|
}
|
|
3979
|
-
function ownerByMarker(path, markers) {
|
|
3980
|
-
const normalized = path.replaceAll("\\", "/");
|
|
3981
|
-
const found = markers.find(({ marker }) => normalized.includes(marker));
|
|
3982
|
-
return found === void 0 ? null : found.owner;
|
|
3983
|
-
}
|
|
3984
3664
|
const GLOBAL_MARKERS = [
|
|
3985
3665
|
{
|
|
3986
3666
|
marker: "/.bun/install/global/node_modules/",
|
|
3987
|
-
|
|
3667
|
+
packageManager: "bun"
|
|
3988
3668
|
},
|
|
3989
3669
|
{
|
|
3990
3670
|
marker: "/Library/pnpm/global/",
|
|
3991
|
-
|
|
3671
|
+
packageManager: "pnpm"
|
|
3992
3672
|
},
|
|
3993
3673
|
{
|
|
3994
3674
|
marker: "/.local/share/pnpm/global/",
|
|
3995
|
-
|
|
3675
|
+
packageManager: "pnpm"
|
|
3996
3676
|
},
|
|
3997
3677
|
{
|
|
3998
3678
|
marker: "/share/pnpm/global/",
|
|
3999
|
-
|
|
3679
|
+
packageManager: "pnpm"
|
|
4000
3680
|
},
|
|
4001
3681
|
{
|
|
4002
3682
|
marker: "/pnpm-global/node_modules/",
|
|
4003
|
-
|
|
3683
|
+
packageManager: "pnpm"
|
|
4004
3684
|
},
|
|
4005
3685
|
{
|
|
4006
3686
|
marker: "/.config/yarn/global/node_modules/",
|
|
4007
|
-
|
|
3687
|
+
packageManager: "yarn"
|
|
4008
3688
|
},
|
|
4009
3689
|
{
|
|
4010
3690
|
marker: "/lib/node_modules/",
|
|
4011
|
-
|
|
3691
|
+
packageManager: "npm"
|
|
4012
3692
|
},
|
|
4013
3693
|
{
|
|
4014
3694
|
marker: "/.npm-global/node_modules/",
|
|
4015
|
-
|
|
3695
|
+
packageManager: "npm"
|
|
4016
3696
|
}
|
|
4017
3697
|
];
|
|
4018
3698
|
function packageManagerFor(scriptPath) {
|
|
4019
|
-
|
|
4020
|
-
}
|
|
4021
|
-
|
|
4022
|
-
npm: "npm",
|
|
4023
|
-
pnpm: "pnpm",
|
|
4024
|
-
yarn: "Yarn",
|
|
4025
|
-
bun: "Bun",
|
|
4026
|
-
homebrew: "Homebrew"
|
|
4027
|
-
};
|
|
4028
|
-
const CELLAR = "/Cellar/";
|
|
4029
|
-
const NODE_MODULES = "/node_modules/";
|
|
4030
|
-
const OWNER_MARKERS = [
|
|
4031
|
-
...GLOBAL_MARKERS,
|
|
4032
|
-
{
|
|
4033
|
-
marker: CELLAR,
|
|
4034
|
-
owner: "homebrew"
|
|
4035
|
-
},
|
|
4036
|
-
{
|
|
4037
|
-
marker: "/Library/pnpm/",
|
|
4038
|
-
owner: "pnpm"
|
|
4039
|
-
},
|
|
4040
|
-
{
|
|
4041
|
-
marker: "/.local/share/pnpm/",
|
|
4042
|
-
owner: "pnpm"
|
|
4043
|
-
}
|
|
4044
|
-
];
|
|
4045
|
-
async function locateMbOnPath() {
|
|
4046
|
-
const paths = await whichAll(MB);
|
|
4047
|
-
return Promise.all(paths.map(async (path) => ({
|
|
4048
|
-
path,
|
|
4049
|
-
resolved: await promises.realpath(path)
|
|
4050
|
-
})));
|
|
4051
|
-
}
|
|
4052
|
-
function mbOwner(binary) {
|
|
4053
|
-
return ownerByMarker(binary.resolved, OWNER_MARKERS) ?? ownerByMarker(binary.path, OWNER_MARKERS);
|
|
4054
|
-
}
|
|
4055
|
-
function firstSegmentAfter(path, marker) {
|
|
4056
|
-
const at = path.lastIndexOf(marker);
|
|
4057
|
-
if (at === -1) return null;
|
|
4058
|
-
const [first] = path.slice(at + marker.length).split("/");
|
|
4059
|
-
return first === void 0 || first.length === 0 ? null : first;
|
|
4060
|
-
}
|
|
4061
|
-
function packageName(resolved) {
|
|
4062
|
-
const scopeOrName = firstSegmentAfter(resolved, NODE_MODULES);
|
|
4063
|
-
if (scopeOrName === null || !scopeOrName.startsWith("@")) return scopeOrName;
|
|
4064
|
-
const name = firstSegmentAfter(resolved, `${NODE_MODULES}${scopeOrName}/`);
|
|
4065
|
-
return name === null ? null : `${scopeOrName}/${name}`;
|
|
4066
|
-
}
|
|
4067
|
-
const REMOVE_COMMANDS = {
|
|
4068
|
-
npm: (name) => `npm uninstall -g ${name}`,
|
|
4069
|
-
pnpm: (name) => `pnpm remove -g ${name}`,
|
|
4070
|
-
yarn: (name) => `yarn global remove ${name}`,
|
|
4071
|
-
bun: (name) => `bun remove -g ${name}`,
|
|
4072
|
-
homebrew: (name) => `brew uninstall ${name}`
|
|
4073
|
-
};
|
|
4074
|
-
function mbRemoveCommand(binary) {
|
|
4075
|
-
const owner = mbOwner(binary);
|
|
4076
|
-
if (owner === null) return null;
|
|
4077
|
-
const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
|
|
4078
|
-
return name === null ? null : REMOVE_COMMANDS[owner](name);
|
|
4079
|
-
}
|
|
4080
|
-
function setValue(name) {
|
|
4081
|
-
const value = readEnv(name);
|
|
4082
|
-
return value === void 0 || value.length === 0 ? null : value;
|
|
4083
|
-
}
|
|
4084
|
-
function mbEnvironment() {
|
|
4085
|
-
return {
|
|
4086
|
-
profile: setValue(ENV_MB_PROFILE),
|
|
4087
|
-
url: setValue(ENV_MB_URL),
|
|
4088
|
-
apiKeySet: setValue(ENV_MB_API_KEY) !== null
|
|
4089
|
-
};
|
|
3699
|
+
const normalized = scriptPath.replaceAll("\\", "/");
|
|
3700
|
+
for (const { marker, packageManager } of GLOBAL_MARKERS) if (normalized.includes(marker)) return packageManager;
|
|
3701
|
+
return "npm";
|
|
4090
3702
|
}
|
|
4091
3703
|
const ProbedUser = z.object({
|
|
4092
3704
|
id: z.int(),
|
|
@@ -4163,16 +3775,16 @@ function mbTool(runner) {
|
|
|
4163
3775
|
if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
|
|
4164
3776
|
return parseJsonOutput(MB, argv, result.stdout, schema);
|
|
4165
3777
|
}
|
|
4166
|
-
async function version(
|
|
3778
|
+
async function version() {
|
|
4167
3779
|
const args = ["--version"];
|
|
4168
3780
|
let result;
|
|
4169
3781
|
try {
|
|
4170
|
-
result = await runner.run(
|
|
3782
|
+
result = await runner.run(MB, args);
|
|
4171
3783
|
} catch (error) {
|
|
4172
3784
|
if (error instanceof ProcessNotFoundError) return null;
|
|
4173
3785
|
throw error;
|
|
4174
3786
|
}
|
|
4175
|
-
if (result.exitCode !== 0) throw toolFailure(
|
|
3787
|
+
if (result.exitCode !== 0) throw toolFailure(MB, args, result);
|
|
4176
3788
|
return result.stdout.trim();
|
|
4177
3789
|
}
|
|
4178
3790
|
async function authLogin(options) {
|
|
@@ -4258,14 +3870,65 @@ function mbTool(runner) {
|
|
|
4258
3870
|
};
|
|
4259
3871
|
}
|
|
4260
3872
|
//#endregion
|
|
3873
|
+
//#region src/wizard/engine.ts
|
|
3874
|
+
function requireKnownStep(steps, id, flag) {
|
|
3875
|
+
if (id === void 0) return;
|
|
3876
|
+
if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
|
|
3877
|
+
}
|
|
3878
|
+
async function detectAll(steps, ctx, options) {
|
|
3879
|
+
const detected = [];
|
|
3880
|
+
let fromHere = false;
|
|
3881
|
+
for (const step of steps) {
|
|
3882
|
+
if (step.id === options.from) fromHere = true;
|
|
3883
|
+
const forced = options.only !== void 0 ? step.id === options.only : fromHere;
|
|
3884
|
+
detected.push({
|
|
3885
|
+
step,
|
|
3886
|
+
status: forced ? "todo" : await step.detect(ctx),
|
|
3887
|
+
forced
|
|
3888
|
+
});
|
|
3889
|
+
}
|
|
3890
|
+
return detected;
|
|
3891
|
+
}
|
|
3892
|
+
async function shouldRun(entry, ctx, options) {
|
|
3893
|
+
if (entry.forced) return true;
|
|
3894
|
+
if (options.only !== void 0 || entry.status === "done") return false;
|
|
3895
|
+
return await entry.step.detect(ctx) === "todo";
|
|
3896
|
+
}
|
|
3897
|
+
function recordFor(outcome, at) {
|
|
3898
|
+
return {
|
|
3899
|
+
status: outcome === "completed" ? "done" : "skipped",
|
|
3900
|
+
completedAt: at.toISOString()
|
|
3901
|
+
};
|
|
3902
|
+
}
|
|
3903
|
+
function withStepRecord(state, id, record) {
|
|
3904
|
+
return {
|
|
3905
|
+
...state,
|
|
3906
|
+
steps: {
|
|
3907
|
+
...state.steps,
|
|
3908
|
+
[id]: record
|
|
3909
|
+
}
|
|
3910
|
+
};
|
|
3911
|
+
}
|
|
3912
|
+
async function runWizard(steps, ctx, options = {}) {
|
|
3913
|
+
requireKnownStep(steps, options.from, "--from");
|
|
3914
|
+
requireKnownStep(steps, options.only, "--only");
|
|
3915
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
3916
|
+
const detected = await detectAll(steps, ctx, options);
|
|
3917
|
+
ctx.ui.checklist(detected.map(({ step, status }) => ({
|
|
3918
|
+
title: step.title,
|
|
3919
|
+
status
|
|
3920
|
+
})));
|
|
3921
|
+
for (const entry of detected) {
|
|
3922
|
+
if (!await shouldRun(entry, ctx, options)) continue;
|
|
3923
|
+
const outcome = await entry.step.run(ctx);
|
|
3924
|
+
await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
|
|
3925
|
+
}
|
|
3926
|
+
}
|
|
3927
|
+
//#endregion
|
|
4261
3928
|
//#region src/tools/skills-cli.ts
|
|
4262
3929
|
const NPX = "npx";
|
|
4263
3930
|
const SKILLS_PACKAGE = "skills@latest";
|
|
4264
|
-
const
|
|
4265
|
-
function rdeSkillSource() {
|
|
4266
|
-
const override = readEnv(ENV_RDE_SKILL_SOURCE);
|
|
4267
|
-
return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
|
|
4268
|
-
}
|
|
3931
|
+
const RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-generic-skills";
|
|
4269
3932
|
const InstalledSkill = z.object({
|
|
4270
3933
|
name: z.string(),
|
|
4271
3934
|
path: z.string(),
|
|
@@ -4274,49 +3937,6 @@ const InstalledSkill = z.object({
|
|
|
4274
3937
|
source: z.string().nullable()
|
|
4275
3938
|
}).loose();
|
|
4276
3939
|
const InstalledSkillList = z.array(InstalledSkill);
|
|
4277
|
-
const SkillLock = z.object({ skills: z.record(z.string(), z.object({ ref: z.string().nullish() }).loose()) }).loose();
|
|
4278
|
-
const GLOBAL_LOCK_FILE = ".skill-lock.json";
|
|
4279
|
-
const PROJECT_LOCK_FILE = "skills-lock.json";
|
|
4280
|
-
function globalSkillLockPath(home, stateHome) {
|
|
4281
|
-
return stateHome === null ? join(home, ".agents", GLOBAL_LOCK_FILE) : join(stateHome, "skills", GLOBAL_LOCK_FILE);
|
|
4282
|
-
}
|
|
4283
|
-
function projectSkillLockPath(project) {
|
|
4284
|
-
return join(project, PROJECT_LOCK_FILE);
|
|
4285
|
-
}
|
|
4286
|
-
function xdgStateHome() {
|
|
4287
|
-
const value = readEnv(ENV_XDG_STATE_HOME);
|
|
4288
|
-
return value === void 0 || value.length === 0 ? null : value;
|
|
4289
|
-
}
|
|
4290
|
-
function lockedRef(path, raw, skill) {
|
|
4291
|
-
if (raw === null) return null;
|
|
4292
|
-
const parsed = SkillLock.safeParse(raw);
|
|
4293
|
-
if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
|
|
4294
|
-
return parsed.data.skills[skill]?.ref ?? null;
|
|
4295
|
-
}
|
|
4296
|
-
const hostSkillFiles = {
|
|
4297
|
-
readJson: readJsonFile,
|
|
4298
|
-
readText: readTextFile
|
|
4299
|
-
};
|
|
4300
|
-
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
4301
|
-
function skillOrigin(source, cwd) {
|
|
4302
|
-
const github = GITHUB_URL.exec(source);
|
|
4303
|
-
if (github !== null) {
|
|
4304
|
-
const [, owner, repo, ref] = github;
|
|
4305
|
-
return {
|
|
4306
|
-
kind: "github",
|
|
4307
|
-
repository: `${owner}/${repo}`,
|
|
4308
|
-
ref: ref ?? null
|
|
4309
|
-
};
|
|
4310
|
-
}
|
|
4311
|
-
if (isAbsolute(source) || source.startsWith(".")) return {
|
|
4312
|
-
kind: "local",
|
|
4313
|
-
path: resolve(cwd, source)
|
|
4314
|
-
};
|
|
4315
|
-
return {
|
|
4316
|
-
kind: "other",
|
|
4317
|
-
source
|
|
4318
|
-
};
|
|
4319
|
-
}
|
|
4320
3940
|
function skillsCli(runner) {
|
|
4321
3941
|
async function add(options) {
|
|
4322
3942
|
const argv = [
|
|
@@ -4368,83 +3988,13 @@ function skillsCli(runner) {
|
|
|
4368
3988
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
4369
3989
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
4370
3990
|
}
|
|
4371
|
-
async function listProject(cwd) {
|
|
4372
|
-
const argv = [
|
|
4373
|
-
"-y",
|
|
4374
|
-
SKILLS_PACKAGE,
|
|
4375
|
-
"list",
|
|
4376
|
-
"--json"
|
|
4377
|
-
];
|
|
4378
|
-
const result = await runner.run(NPX, argv, {
|
|
4379
|
-
cwd,
|
|
4380
|
-
stdoutToFile: true
|
|
4381
|
-
});
|
|
4382
|
-
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
4383
|
-
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
4384
|
-
}
|
|
4385
3991
|
return {
|
|
4386
3992
|
add,
|
|
4387
3993
|
remove,
|
|
4388
|
-
list
|
|
4389
|
-
listProject
|
|
3994
|
+
list
|
|
4390
3995
|
};
|
|
4391
3996
|
}
|
|
4392
3997
|
//#endregion
|
|
4393
|
-
//#region src/wizard/engine.ts
|
|
4394
|
-
function requireKnownStep(steps, id, flag) {
|
|
4395
|
-
if (id === void 0) return;
|
|
4396
|
-
if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
|
|
4397
|
-
}
|
|
4398
|
-
async function detectAll(steps, ctx, options) {
|
|
4399
|
-
const detected = [];
|
|
4400
|
-
let fromHere = false;
|
|
4401
|
-
for (const step of steps) {
|
|
4402
|
-
if (step.id === options.from) fromHere = true;
|
|
4403
|
-
const forced = options.only !== void 0 ? step.id === options.only : fromHere;
|
|
4404
|
-
detected.push({
|
|
4405
|
-
step,
|
|
4406
|
-
status: forced ? "todo" : await step.detect(ctx),
|
|
4407
|
-
forced
|
|
4408
|
-
});
|
|
4409
|
-
}
|
|
4410
|
-
return detected;
|
|
4411
|
-
}
|
|
4412
|
-
async function shouldRun(entry, ctx, options) {
|
|
4413
|
-
if (entry.forced) return true;
|
|
4414
|
-
if (options.only !== void 0 || entry.status === "done") return false;
|
|
4415
|
-
return await entry.step.detect(ctx) === "todo";
|
|
4416
|
-
}
|
|
4417
|
-
function recordFor(outcome, at) {
|
|
4418
|
-
return {
|
|
4419
|
-
status: outcome === "completed" ? "done" : "skipped",
|
|
4420
|
-
completedAt: at.toISOString()
|
|
4421
|
-
};
|
|
4422
|
-
}
|
|
4423
|
-
function withStepRecord(state, id, record) {
|
|
4424
|
-
return {
|
|
4425
|
-
...state,
|
|
4426
|
-
steps: {
|
|
4427
|
-
...state.steps,
|
|
4428
|
-
[id]: record
|
|
4429
|
-
}
|
|
4430
|
-
};
|
|
4431
|
-
}
|
|
4432
|
-
async function runWizard(steps, ctx, options = {}) {
|
|
4433
|
-
requireKnownStep(steps, options.from, "--from");
|
|
4434
|
-
requireKnownStep(steps, options.only, "--only");
|
|
4435
|
-
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4436
|
-
const detected = await detectAll(steps, ctx, options);
|
|
4437
|
-
ctx.ui.checklist(detected.map(({ step, status }) => ({
|
|
4438
|
-
title: step.title,
|
|
4439
|
-
status
|
|
4440
|
-
})));
|
|
4441
|
-
for (const entry of detected) {
|
|
4442
|
-
if (!await shouldRun(entry, ctx, options)) continue;
|
|
4443
|
-
const outcome = await entry.step.run(ctx);
|
|
4444
|
-
await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
|
|
4445
|
-
}
|
|
4446
|
-
}
|
|
4447
|
-
//#endregion
|
|
4448
3998
|
//#region src/steps/agents-skills.ts
|
|
4449
3999
|
const STEP_ID$8 = "agents-skills";
|
|
4450
4000
|
const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
@@ -4452,8 +4002,7 @@ const DETECT_AGENTS = { agents: null };
|
|
|
4452
4002
|
function hostDeps$11(ctx) {
|
|
4453
4003
|
return {
|
|
4454
4004
|
runner: processRunner(ctx.signal),
|
|
4455
|
-
probe: hostAgentProbe()
|
|
4456
|
-
skillSource: rdeSkillSource()
|
|
4005
|
+
probe: hostAgentProbe()
|
|
4457
4006
|
};
|
|
4458
4007
|
}
|
|
4459
4008
|
function detectedOnly(agents) {
|
|
@@ -4513,10 +4062,9 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
|
4513
4062
|
ctx.ui.info(skipLine(all, selection.source));
|
|
4514
4063
|
return "skipped";
|
|
4515
4064
|
}
|
|
4516
|
-
|
|
4517
|
-
ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
|
|
4065
|
+
ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`);
|
|
4518
4066
|
await skillsCli(resolved.runner).add({
|
|
4519
|
-
source:
|
|
4067
|
+
source: RDE_SKILL_SOURCE,
|
|
4520
4068
|
skill: "rde",
|
|
4521
4069
|
agents: selection.agents.map((agent) => agent.skillsCliName)
|
|
4522
4070
|
});
|
|
@@ -4542,8 +4090,7 @@ function rdeInstalledWith() {
|
|
|
4542
4090
|
function hostDeps$10(ctx) {
|
|
4543
4091
|
return {
|
|
4544
4092
|
runner: processRunner(ctx.signal),
|
|
4545
|
-
installedWith: rdeInstalledWith()
|
|
4546
|
-
locate: locateMbOnPath
|
|
4093
|
+
installedWith: rdeInstalledWith()
|
|
4547
4094
|
};
|
|
4548
4095
|
}
|
|
4549
4096
|
async function inspectMb(runner) {
|
|
@@ -4560,100 +4107,14 @@ async function inspectMb(runner) {
|
|
|
4560
4107
|
function displayCommand(install) {
|
|
4561
4108
|
return [install.command, ...install.args].join(" ");
|
|
4562
4109
|
}
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
return raw === null ? null : semver.valid(raw);
|
|
4567
|
-
} catch (error) {
|
|
4568
|
-
if (error instanceof ExternalToolError) return null;
|
|
4569
|
-
throw error;
|
|
4570
|
-
}
|
|
4571
|
-
}
|
|
4572
|
-
async function inspectMbOnPath(runner, locate) {
|
|
4573
|
-
const found = [];
|
|
4574
|
-
for (const binary of await locate()) found.push({
|
|
4575
|
-
path: binary.path,
|
|
4576
|
-
version: await versionAt(runner, binary.path),
|
|
4577
|
-
owner: mbOwner(binary),
|
|
4578
|
-
removeCommand: mbRemoveCommand(binary)
|
|
4579
|
-
});
|
|
4580
|
-
return found;
|
|
4581
|
-
}
|
|
4582
|
-
function satisfiesMinimum(version) {
|
|
4583
|
-
return version !== null && semver.satisfies(version, MB_RANGE, { includePrerelease: true });
|
|
4584
|
-
}
|
|
4585
|
-
function isNewer(candidate, than) {
|
|
4586
|
-
if (candidate === null) return false;
|
|
4587
|
-
return than === null || semver.gt(candidate, than);
|
|
4588
|
-
}
|
|
4589
|
-
function diagnoseMb(found, installer) {
|
|
4590
|
-
const [running, ...later] = found;
|
|
4591
|
-
if (running === void 0) return { kind: "missing" };
|
|
4592
|
-
const sufficient = satisfiesMinimum(running.version);
|
|
4593
|
-
const newer = later.find((candidate) => satisfiesMinimum(candidate.version) && isNewer(candidate.version, running.version));
|
|
4594
|
-
if (newer !== void 0 && (!sufficient || running.owner !== installer)) return {
|
|
4595
|
-
kind: "shadowed",
|
|
4596
|
-
running,
|
|
4597
|
-
newer
|
|
4598
|
-
};
|
|
4599
|
-
return sufficient ? {
|
|
4600
|
-
kind: "current",
|
|
4601
|
-
running
|
|
4602
|
-
} : {
|
|
4603
|
-
kind: "old",
|
|
4604
|
-
running
|
|
4605
|
-
};
|
|
4606
|
-
}
|
|
4607
|
-
function runningMbSuffices(diagnosis) {
|
|
4608
|
-
return diagnosis.kind !== "missing" && satisfiesMinimum(diagnosis.running.version);
|
|
4609
|
-
}
|
|
4610
|
-
function versionText(entry) {
|
|
4611
|
-
return entry.version ?? "an unknown version";
|
|
4612
|
-
}
|
|
4613
|
-
function describeEntry(entry) {
|
|
4614
|
-
const owner = entry.owner === null ? "" : ` (${MB_OWNER_NAMES[entry.owner]})`;
|
|
4615
|
-
return `${versionText(entry)} at ${entry.path}${owner}`;
|
|
4616
|
-
}
|
|
4617
|
-
function mbPathLines(found) {
|
|
4618
|
-
return found.map((entry, index) => `${index === 0 ? "" : "then "}${describeEntry(entry)}`);
|
|
4619
|
-
}
|
|
4620
|
-
function removeClause(entry) {
|
|
4621
|
-
if (entry.removeCommand !== null) return `remove the old one with \`${entry.removeCommand}\``;
|
|
4622
|
-
if (entry.owner !== null) return `remove the old one with ${MB_OWNER_NAMES[entry.owner]}`;
|
|
4623
|
-
return `remove ${entry.path}`;
|
|
4624
|
-
}
|
|
4625
|
-
function shadowedHint(diagnosis) {
|
|
4626
|
-
const { running, newer } = diagnosis;
|
|
4627
|
-
return [
|
|
4628
|
-
`${running.path} (${versionText(running)}) runs before ${newer.path} (${versionText(newer)});`,
|
|
4629
|
-
`${removeClause(running)}, or put ${dirname(newer.path)} before ${dirname(running.path)} on PATH,`,
|
|
4630
|
-
"then open a new terminal"
|
|
4631
|
-
].join(" ");
|
|
4632
|
-
}
|
|
4633
|
-
const NEW_TERMINAL = "open a new terminal and run rde init again";
|
|
4634
|
-
function stillWrongAdvice(diagnosis) {
|
|
4635
|
-
switch (diagnosis.kind) {
|
|
4636
|
-
case "shadowed": return `${shadowedHint(diagnosis)} and run rde init again`;
|
|
4637
|
-
case "old": return `The install put no newer mb on PATH; ${removeClause(diagnosis.running)}, or ${NEW_TERMINAL}`;
|
|
4638
|
-
case "missing":
|
|
4639
|
-
case "current": return "Open a new terminal and run rde init again";
|
|
4640
|
-
}
|
|
4641
|
-
}
|
|
4642
|
-
async function stillWrong(install, after, deps) {
|
|
4643
|
-
const fail = (stderrTail) => new ExternalToolError({
|
|
4110
|
+
function stillWrong(install, after) {
|
|
4111
|
+
const found = after.installed ? `mb ${after.version} is still what is on PATH (${MIN_MB_VERSION} or newer is required)` : "mb is not on PATH after the install";
|
|
4112
|
+
return new ExternalToolError({
|
|
4644
4113
|
tool: install.command,
|
|
4645
4114
|
argv: install.args,
|
|
4646
4115
|
exitCode: 0,
|
|
4647
|
-
stderrTail
|
|
4116
|
+
stderrTail: `${found}; open a new terminal and run rde init again`
|
|
4648
4117
|
});
|
|
4649
|
-
if (!after.installed) return fail(`mb is not on PATH after the install; ${NEW_TERMINAL}`);
|
|
4650
|
-
const found = await inspectMbOnPath(deps.runner, deps.locate);
|
|
4651
|
-
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
4652
|
-
return fail([
|
|
4653
|
-
`mb ${after.version} still runs first (${MIN_MB_VERSION} or newer is required).`,
|
|
4654
|
-
`Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.`,
|
|
4655
|
-
`${stillWrongAdvice(diagnosis)}.`
|
|
4656
|
-
].join(" "));
|
|
4657
4118
|
}
|
|
4658
4119
|
function installMbStep(deps = hostDeps$10) {
|
|
4659
4120
|
return {
|
|
@@ -4681,7 +4142,7 @@ function installMbStep(deps = hostDeps$10) {
|
|
|
4681
4142
|
stderrTail: ""
|
|
4682
4143
|
});
|
|
4683
4144
|
const after = await inspectMb(resolved.runner);
|
|
4684
|
-
if (!after.installed || !after.satisfies) throw
|
|
4145
|
+
if (!after.installed || !after.satisfies) throw stillWrong(install, after);
|
|
4685
4146
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
4686
4147
|
return "completed";
|
|
4687
4148
|
}
|
|
@@ -5002,15 +4463,7 @@ function hostDoctorDeps(instanceName, signal) {
|
|
|
5002
4463
|
runtime,
|
|
5003
4464
|
ports: hostPortProbe,
|
|
5004
4465
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
5005
|
-
probe: hostAgentProbe()
|
|
5006
|
-
locateMb: locateMbOnPath,
|
|
5007
|
-
environment: mbEnvironment(),
|
|
5008
|
-
skills: {
|
|
5009
|
-
source: rdeSkillSource(),
|
|
5010
|
-
cwd: process.cwd(),
|
|
5011
|
-
stateHome: xdgStateHome(),
|
|
5012
|
-
files: hostSkillFiles
|
|
5013
|
-
}
|
|
4466
|
+
probe: hostAgentProbe()
|
|
5014
4467
|
};
|
|
5015
4468
|
}
|
|
5016
4469
|
const NOT_FOUND_STATUS$2 = 404;
|
|
@@ -5023,17 +4476,7 @@ function ok(id, label, detail) {
|
|
|
5023
4476
|
status: "ok",
|
|
5024
4477
|
detail,
|
|
5025
4478
|
hint: null,
|
|
5026
|
-
|
|
5027
|
-
};
|
|
5028
|
-
}
|
|
5029
|
-
function note$1(id, label, detail, hint) {
|
|
5030
|
-
return {
|
|
5031
|
-
id,
|
|
5032
|
-
label,
|
|
5033
|
-
status: "ok",
|
|
5034
|
-
detail,
|
|
5035
|
-
hint,
|
|
5036
|
-
step: null
|
|
4479
|
+
fix: null
|
|
5037
4480
|
};
|
|
5038
4481
|
}
|
|
5039
4482
|
function skipped(id, label, detail) {
|
|
@@ -5043,7 +4486,7 @@ function skipped(id, label, detail) {
|
|
|
5043
4486
|
status: "skipped",
|
|
5044
4487
|
detail,
|
|
5045
4488
|
hint: null,
|
|
5046
|
-
|
|
4489
|
+
fix: null
|
|
5047
4490
|
};
|
|
5048
4491
|
}
|
|
5049
4492
|
function failed(id, label, failure) {
|
|
@@ -5064,7 +4507,7 @@ function machineRow(check, needed) {
|
|
|
5064
4507
|
return failed(check.id, check.id, {
|
|
5065
4508
|
detail: check.label,
|
|
5066
4509
|
hint: check.hint,
|
|
5067
|
-
|
|
4510
|
+
fix: "preflight"
|
|
5068
4511
|
});
|
|
5069
4512
|
}
|
|
5070
4513
|
async function machineRows(deps, runtime) {
|
|
@@ -5076,48 +4519,19 @@ function installHint(installedWith) {
|
|
|
5076
4519
|
const install = mbInstallCommand(installedWith);
|
|
5077
4520
|
return `\`${[install.command, ...install.args].join(" ")}\``;
|
|
5078
4521
|
}
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
if (
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
const required = lines.map((line, index) => index === 0 ? `${line}; ${MIN_MB_VERSION} or newer is required` : line);
|
|
5093
|
-
return failed("mb", MB_LABEL, {
|
|
5094
|
-
detail: required.join("\n"),
|
|
5095
|
-
hint: installHint(deps.installedWith),
|
|
5096
|
-
step: "install-mb"
|
|
5097
|
-
});
|
|
5098
|
-
}
|
|
5099
|
-
case "shadowed": return failed("mb", MB_LABEL, {
|
|
5100
|
-
detail: lines.join("\n"),
|
|
5101
|
-
hint: shadowedHint(diagnosis),
|
|
5102
|
-
step: null
|
|
5103
|
-
});
|
|
5104
|
-
case "current": return ok("mb", MB_LABEL, lines.join("\n"));
|
|
5105
|
-
}
|
|
5106
|
-
}
|
|
5107
|
-
async function mbFinding(deps) {
|
|
5108
|
-
const found = await inspectMbOnPath(deps.runner, deps.locateMb);
|
|
5109
|
-
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
5110
|
-
return {
|
|
5111
|
-
row: mbRow(diagnosis, mbPathLines(found), deps),
|
|
5112
|
-
unusable: mbUnusable(diagnosis)
|
|
5113
|
-
};
|
|
5114
|
-
}
|
|
5115
|
-
function sharedProfileList(deps) {
|
|
5116
|
-
let listed = null;
|
|
5117
|
-
return () => {
|
|
5118
|
-
listed ??= mbTool(deps.runner).authList();
|
|
5119
|
-
return listed;
|
|
5120
|
-
};
|
|
4522
|
+
async function mbRow(deps) {
|
|
4523
|
+
const found = await inspectMb(deps.runner);
|
|
4524
|
+
if (!found.installed) return failed("mb", "mb", {
|
|
4525
|
+
detail: "not installed",
|
|
4526
|
+
hint: installHint(deps.installedWith),
|
|
4527
|
+
fix: "install-mb"
|
|
4528
|
+
});
|
|
4529
|
+
if (!found.satisfies) return failed("mb", "mb", {
|
|
4530
|
+
detail: `${found.version} (${MIN_MB_VERSION} or newer is required)`,
|
|
4531
|
+
hint: installHint(deps.installedWith),
|
|
4532
|
+
fix: "install-mb"
|
|
4533
|
+
});
|
|
4534
|
+
return ok("mb", "mb", found.version);
|
|
5121
4535
|
}
|
|
5122
4536
|
function publishesOnLoopback(container, port) {
|
|
5123
4537
|
return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
@@ -5135,7 +4549,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
5135
4549
|
row: failed("container", label, {
|
|
5136
4550
|
detail: `${runtime.containerName} does not exist`,
|
|
5137
4551
|
hint: "`rde init` recreates it from the saved choices",
|
|
5138
|
-
|
|
4552
|
+
fix: "runtime"
|
|
5139
4553
|
}),
|
|
5140
4554
|
running: false,
|
|
5141
4555
|
holder: CONTAINER
|
|
@@ -5148,7 +4562,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
5148
4562
|
row: failed("container", label, {
|
|
5149
4563
|
detail: `${runtime.containerName} runs ${inspection.image}, the state says ${runtime.image}`,
|
|
5150
4564
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
5151
|
-
|
|
4565
|
+
fix: null
|
|
5152
4566
|
}),
|
|
5153
4567
|
...found
|
|
5154
4568
|
};
|
|
@@ -5156,7 +4570,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
5156
4570
|
row: failed("container", label, {
|
|
5157
4571
|
detail: `${runtime.containerName} does not publish port ${runtime.port} on ${LOOPBACK_HOST}`,
|
|
5158
4572
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
5159
|
-
|
|
4573
|
+
fix: null
|
|
5160
4574
|
}),
|
|
5161
4575
|
...found
|
|
5162
4576
|
};
|
|
@@ -5177,7 +4591,7 @@ async function processRow(runtime, deps) {
|
|
|
5177
4591
|
row: failed("process", label, {
|
|
5178
4592
|
detail: `${inspection.jarPath} does not exist`,
|
|
5179
4593
|
hint,
|
|
5180
|
-
|
|
4594
|
+
fix: "runtime"
|
|
5181
4595
|
}),
|
|
5182
4596
|
running: false,
|
|
5183
4597
|
holder: JAR_PROCESS
|
|
@@ -5204,13 +4618,13 @@ async function portRow(port, finding, ports) {
|
|
|
5204
4618
|
if (finding.running) return failed("port", "port", {
|
|
5205
4619
|
detail: `${port} is not answering although ${finding.holder} runs`,
|
|
5206
4620
|
hint: "`rde logs` shows what Metabase is doing",
|
|
5207
|
-
|
|
4621
|
+
fix: null
|
|
5208
4622
|
});
|
|
5209
4623
|
if (free) return ok("port", "port", `${port} is free`);
|
|
5210
4624
|
return failed("port", "port", {
|
|
5211
4625
|
detail: `${port} is in use by another program`,
|
|
5212
4626
|
hint: "stop that program before `rde start`",
|
|
5213
|
-
|
|
4627
|
+
fix: null
|
|
5214
4628
|
});
|
|
5215
4629
|
}
|
|
5216
4630
|
async function healthRow(url, deps) {
|
|
@@ -5225,7 +4639,7 @@ async function healthRow(url, deps) {
|
|
|
5225
4639
|
row: failed("health", "health", {
|
|
5226
4640
|
detail: `initializing (${Math.round(error.progress * 100)}%)`,
|
|
5227
4641
|
hint: "wait for it; `rde logs -f` shows progress",
|
|
5228
|
-
|
|
4642
|
+
fix: "start-metabase"
|
|
5229
4643
|
}),
|
|
5230
4644
|
healthy: false
|
|
5231
4645
|
};
|
|
@@ -5233,7 +4647,7 @@ async function healthRow(url, deps) {
|
|
|
5233
4647
|
row: failed("health", "health", {
|
|
5234
4648
|
detail: "unreachable",
|
|
5235
4649
|
hint: "`rde start`",
|
|
5236
|
-
|
|
4650
|
+
fix: "start-metabase"
|
|
5237
4651
|
}),
|
|
5238
4652
|
healthy: false
|
|
5239
4653
|
};
|
|
@@ -5241,7 +4655,7 @@ async function healthRow(url, deps) {
|
|
|
5241
4655
|
row: failed("health", "health", {
|
|
5242
4656
|
detail: `something other than Metabase answers at ${url} (HTTP ${error.status})`,
|
|
5243
4657
|
hint: "another program holds the port; stop it before `rde start`",
|
|
5244
|
-
|
|
4658
|
+
fix: null
|
|
5245
4659
|
}),
|
|
5246
4660
|
healthy: false
|
|
5247
4661
|
};
|
|
@@ -5252,7 +4666,7 @@ function adminRow(state, properties) {
|
|
|
5252
4666
|
if (!properties["has-user-setup"]) return failed("admin", "admin user", {
|
|
5253
4667
|
detail: "Metabase has no admin user yet",
|
|
5254
4668
|
hint: "`rde init --only admin-user`",
|
|
5255
|
-
|
|
4669
|
+
fix: "admin-user"
|
|
5256
4670
|
});
|
|
5257
4671
|
return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
|
|
5258
4672
|
}
|
|
@@ -5264,204 +4678,36 @@ function versionRow(state, properties) {
|
|
|
5264
4678
|
if (state.version !== running) return failed("version", label, {
|
|
5265
4679
|
detail: `running ${running}, the state file says ${state.version}`,
|
|
5266
4680
|
hint: "`rde start` records the running version",
|
|
5267
|
-
|
|
4681
|
+
fix: VERSION_STEP
|
|
5268
4682
|
});
|
|
5269
4683
|
return ok("version", label, running);
|
|
5270
4684
|
}
|
|
5271
|
-
|
|
5272
|
-
|
|
5273
|
-
|
|
5274
|
-
|
|
5275
|
-
|
|
5276
|
-
const admin = state.admin;
|
|
5277
|
-
if (admin === null) return skipped(id, label, "no admin user created by rde yet");
|
|
5278
|
-
switch (admin.kind) {
|
|
5279
|
-
case "prompted": return ok(id, label, "yours, entered during `rde init` and never stored");
|
|
5280
|
-
case "generated":
|
|
5281
|
-
if (secrets === null || secrets.admin === null) return failed(id, label, {
|
|
5282
|
-
detail: "generated, but missing from the secrets file",
|
|
5283
|
-
hint: `it cannot be recovered; ${RECREATE_HINT}`,
|
|
5284
|
-
step: null
|
|
5285
|
-
});
|
|
5286
|
-
return ok(id, label, "generated and stored; `rde credentials` shows it");
|
|
5287
|
-
case "legacy-default": return failed(id, label, {
|
|
5288
|
-
detail: "the public default password from an earlier rde (legacy-default)",
|
|
5289
|
-
hint: `anyone who knows the default can log in; ${RECREATE_HINT} with a generated one`,
|
|
5290
|
-
step: null
|
|
5291
|
-
});
|
|
5292
|
-
}
|
|
5293
|
-
}
|
|
5294
|
-
async function apiKeyFinding(secrets, url, healthy, deps) {
|
|
5295
|
-
const id = "api-key";
|
|
5296
|
-
const label = "API key";
|
|
5297
|
-
const apiKey = secrets === null ? null : secrets.apiKey;
|
|
5298
|
-
if (apiKey === null) return {
|
|
5299
|
-
row: failed(id, label, {
|
|
5300
|
-
detail: "none in the secrets file",
|
|
5301
|
-
hint: "`rde init --only api-key`",
|
|
5302
|
-
step: API_KEY_STEP
|
|
5303
|
-
}),
|
|
5304
|
-
access: {
|
|
5305
|
-
kind: "unusable",
|
|
5306
|
-
reason: "no API key"
|
|
5307
|
-
}
|
|
5308
|
-
};
|
|
5309
|
-
if (url === null || !healthy) return {
|
|
5310
|
-
row: ok(id, label, "in the secrets file"),
|
|
5311
|
-
access: {
|
|
5312
|
-
kind: "unusable",
|
|
5313
|
-
reason: NOT_HEALTHY
|
|
5314
|
-
}
|
|
5315
|
-
};
|
|
5316
|
-
const client = deps.client(url, {
|
|
5317
|
-
kind: "api-key",
|
|
5318
|
-
key: apiKey
|
|
5319
|
-
});
|
|
5320
|
-
try {
|
|
5321
|
-
await client.user.current();
|
|
5322
|
-
} catch (error) {
|
|
5323
|
-
if (!isRejectedCredential(error)) throw error;
|
|
5324
|
-
return {
|
|
5325
|
-
row: failed(id, label, {
|
|
5326
|
-
detail: "in the secrets file, but Metabase rejects it",
|
|
5327
|
-
hint: "`rde init --only api-key` replaces it",
|
|
5328
|
-
step: API_KEY_STEP
|
|
5329
|
-
}),
|
|
5330
|
-
access: {
|
|
5331
|
-
kind: "unusable",
|
|
5332
|
-
reason: "Metabase rejects the API key"
|
|
5333
|
-
}
|
|
5334
|
-
};
|
|
5335
|
-
}
|
|
5336
|
-
return {
|
|
5337
|
-
row: ok(id, label, "in the secrets file, accepted by Metabase"),
|
|
5338
|
-
access: {
|
|
5339
|
-
kind: "accepted",
|
|
5340
|
-
client
|
|
5341
|
-
}
|
|
5342
|
-
};
|
|
5343
|
-
}
|
|
5344
|
-
function octal(mode) {
|
|
5345
|
-
return `0${mode.toString(8)}`;
|
|
5346
|
-
}
|
|
5347
|
-
function expectsSecrets(state) {
|
|
5348
|
-
return state.admin !== null && state.admin.kind !== "prompted" || state.steps[API_KEY_STEP]?.status === "done";
|
|
5349
|
-
}
|
|
5350
|
-
function secretsFileRow(state, files, dir) {
|
|
5351
|
-
const id = "secrets-file";
|
|
5352
|
-
const label = "secrets file";
|
|
5353
|
-
const path = secretsFileIn(dir);
|
|
5354
|
-
if (files.secretsMode === null) {
|
|
5355
|
-
if (!expectsSecrets(state)) return skipped(id, label, "nothing stored yet");
|
|
5356
|
-
return failed(id, label, {
|
|
5357
|
-
detail: `${path} is missing`,
|
|
5358
|
-
hint: "`rde init --only api-key` stores a new API key",
|
|
5359
|
-
step: API_KEY_STEP
|
|
5360
|
-
});
|
|
5361
|
-
}
|
|
5362
|
-
if (files.secretsMode !== 384 || files.dirMode !== 448) {
|
|
5363
|
-
const dirMode = files.dirMode === null ? "missing" : octal(files.dirMode);
|
|
5364
|
-
return failed(id, label, {
|
|
5365
|
-
detail: `${path} is ${octal(files.secretsMode)} in a ${dirMode} directory`,
|
|
5366
|
-
hint: `\`chmod ${octal(384)} ${path} && chmod ${octal(448)} ${dir}\``,
|
|
5367
|
-
step: null
|
|
5368
|
-
});
|
|
5369
|
-
}
|
|
5370
|
-
return ok(id, label, `${path}, ${octal(384)} in a ${octal(448)} directory`);
|
|
5371
|
-
}
|
|
5372
|
-
function stateFileRow(files) {
|
|
5373
|
-
const id = "state-file";
|
|
5374
|
-
const label = "state file";
|
|
5375
|
-
if (files.secretsInState.length > 0) return failed(id, label, {
|
|
5376
|
-
detail: `carries ${files.secretsInState.join(", ")}`,
|
|
5377
|
-
hint: "`rde init --only api-key` rewrites it without them",
|
|
5378
|
-
step: API_KEY_STEP
|
|
4685
|
+
function apiKeyRow(state) {
|
|
4686
|
+
if (state.apiKey === null) return failed("api-key", "API key", {
|
|
4687
|
+
detail: "none in the state file",
|
|
4688
|
+
hint: "`rde init --only api-key`",
|
|
4689
|
+
fix: "api-key"
|
|
5379
4690
|
});
|
|
5380
|
-
return ok(
|
|
4691
|
+
return ok("api-key", "API key", "in the state file");
|
|
5381
4692
|
}
|
|
5382
|
-
async function
|
|
5383
|
-
const dir = deps.runtime.instanceDir;
|
|
5384
|
-
const files = await inspectCredentialFiles(dir);
|
|
5385
|
-
return [
|
|
5386
|
-
adminPasswordRow(state, secrets),
|
|
5387
|
-
apiKey.row,
|
|
5388
|
-
secretsFileRow(state, files, dir),
|
|
5389
|
-
stateFileRow(files)
|
|
5390
|
-
];
|
|
5391
|
-
}
|
|
5392
|
-
function withoutTrailingSlash(url) {
|
|
5393
|
-
return url.replace(/\/+$/, "");
|
|
5394
|
-
}
|
|
5395
|
-
async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
|
|
4693
|
+
async function mbProfileRow(state, deps, mbInstalled, healthy) {
|
|
5396
4694
|
const label = "mb profile";
|
|
5397
|
-
if (
|
|
4695
|
+
if (!mbInstalled) return skipped("mb-profile", label, "mb is not installed");
|
|
5398
4696
|
if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY);
|
|
5399
|
-
const entry = (await
|
|
4697
|
+
const entry = (await mbTool(deps.runner).authList()).find((candidate) => candidate.profile === state.mbProfile);
|
|
5400
4698
|
if (entry === void 0) return failed("mb-profile", label, {
|
|
5401
4699
|
detail: `${state.mbProfile} is not configured`,
|
|
5402
4700
|
hint: "`rde init --only api-key`",
|
|
5403
|
-
|
|
5404
|
-
});
|
|
5405
|
-
if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
|
|
5406
|
-
detail: `${state.mbProfile} points at ${entry.url}, not at this instance (${url})`,
|
|
5407
|
-
hint: "`rde init --only api-key` logs it back in against the instance",
|
|
5408
|
-
step: "api-key"
|
|
4701
|
+
fix: "api-key"
|
|
5409
4702
|
});
|
|
5410
4703
|
if (entry.status !== "ok") return failed("mb-profile", label, {
|
|
5411
4704
|
detail: `${state.mbProfile}: ${entry.status}`,
|
|
5412
4705
|
hint: "`rde init --only api-key` logs it in again",
|
|
5413
|
-
|
|
4706
|
+
fix: "api-key"
|
|
5414
4707
|
});
|
|
5415
4708
|
const user = entry.user === null ? "" : ` as ${entry.user.name}`;
|
|
5416
4709
|
return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
|
|
5417
4710
|
}
|
|
5418
|
-
function hostOf(url) {
|
|
5419
|
-
return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
|
|
5420
|
-
}
|
|
5421
|
-
function unsetHint(name) {
|
|
5422
|
-
return `\`unset ${name}\`, remove it from your shell profile if it is exported there, then restart the agent session`;
|
|
5423
|
-
}
|
|
5424
|
-
async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
|
|
5425
|
-
const id = `env.${ENV_MB_PROFILE}`;
|
|
5426
|
-
if (name === rdeProfile) return ok(id, ENV_MB_PROFILE, `${name}: rde's own profile, so plain \`mb\` uses this instance`);
|
|
5427
|
-
const rde = rdeProfile === null ? "rde's" : `rde's (${rdeProfile})`;
|
|
5428
|
-
const detail = (where) => `${name}: \`mb\` and the rde skill will use profile ${name} (${where}) instead of ${rde}`;
|
|
5429
|
-
if (mbUnusableReason !== null) return note$1(id, ENV_MB_PROFILE, detail(mbUnusableReason), unsetHint(ENV_MB_PROFILE));
|
|
5430
|
-
const entry = (await profiles()).find((candidate) => candidate.profile === name);
|
|
5431
|
-
const where = entry === void 0 ? "not configured" : hostOf(entry.url);
|
|
5432
|
-
return note$1(id, ENV_MB_PROFILE, detail(where), unsetHint(ENV_MB_PROFILE));
|
|
5433
|
-
}
|
|
5434
|
-
async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
|
|
5435
|
-
const rows = [];
|
|
5436
|
-
if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
|
|
5437
|
-
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, rde's own included`, unsetHint(ENV_MB_URL)));
|
|
5438
|
-
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, rde's own included", unsetHint(ENV_MB_API_KEY)));
|
|
5439
|
-
return rows;
|
|
5440
|
-
}
|
|
5441
|
-
const PROFILE_STATUS_WORDS = {
|
|
5442
|
-
ok: "authenticates",
|
|
5443
|
-
"auth-failed": "login rejected",
|
|
5444
|
-
"network-error": "not answering",
|
|
5445
|
-
"server-error": "answers with a server error",
|
|
5446
|
-
"not-probed": "not checked"
|
|
5447
|
-
};
|
|
5448
|
-
function plainMbLine(env, rdeProfile, profiles) {
|
|
5449
|
-
if (env.url !== null) return `plain \`mb\` goes to ${hostOf(env.url)} (${ENV_MB_URL})`;
|
|
5450
|
-
const name = env.profile ?? "default";
|
|
5451
|
-
const via = env.profile === null ? "" : ` (${ENV_MB_PROFILE})`;
|
|
5452
|
-
const entry = profiles.find((candidate) => candidate.profile === name);
|
|
5453
|
-
if (entry === void 0) return `plain \`mb\` uses ${name}${via}, which is not configured`;
|
|
5454
|
-
const instance = name === rdeProfile ? ", this instance" : "";
|
|
5455
|
-
return `plain \`mb\` uses ${name}${via}: ${hostOf(entry.url)}${instance}`;
|
|
5456
|
-
}
|
|
5457
|
-
async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
|
|
5458
|
-
const id = "other-metabases";
|
|
5459
|
-
const label = "other Metabases";
|
|
5460
|
-
if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
|
|
5461
|
-
const all = await profiles();
|
|
5462
|
-
const others = all.filter((entry) => entry.profile !== rdeProfile);
|
|
5463
|
-
return ok(id, label, [...others.length === 0 ? ["none"] : others.map((entry) => `${entry.profile}: ${hostOf(entry.url)}, ${PROFILE_STATUS_WORDS[entry.status]}`), plainMbLine(env, rdeProfile, all)].join("\n"));
|
|
5464
|
-
}
|
|
5465
4711
|
function isNotFound$2(error) {
|
|
5466
4712
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
|
|
5467
4713
|
}
|
|
@@ -5472,7 +4718,7 @@ async function licenseRow(client) {
|
|
|
5472
4718
|
if (!status.valid) return failed("license", label, {
|
|
5473
4719
|
detail: `invalid: ${status.status}`,
|
|
5474
4720
|
hint: "`rde init --from license` with a valid token",
|
|
5475
|
-
|
|
4721
|
+
fix: "license"
|
|
5476
4722
|
});
|
|
5477
4723
|
return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
|
|
5478
4724
|
} catch (error) {
|
|
@@ -5489,47 +4735,47 @@ async function databasesRow(state, client) {
|
|
|
5489
4735
|
if (missing.length > 0) return failed("databases", label, {
|
|
5490
4736
|
detail: `not on the server any more: ${missing.map((record) => record.name).join(", ")}`,
|
|
5491
4737
|
hint: "`rde db add` connects them again",
|
|
5492
|
-
|
|
4738
|
+
fix: "databases"
|
|
5493
4739
|
});
|
|
5494
4740
|
const names = state.databases.map((record) => record.name).join(", ");
|
|
5495
4741
|
return ok("databases", label, `${state.databases.length} connected: ${names}`);
|
|
5496
4742
|
}
|
|
5497
4743
|
const SAMPLE_DATABASE_STEP = "sample-database";
|
|
5498
4744
|
const SAMPLE_LABEL = "sample database";
|
|
5499
|
-
async function sampleDatabaseRow(record, deps, dockerOk,
|
|
4745
|
+
async function sampleDatabaseRow(record, deps, dockerOk, client) {
|
|
5500
4746
|
const id = "sample-database";
|
|
5501
4747
|
if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
|
|
5502
4748
|
const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
|
|
5503
4749
|
if (!status.installed) return failed(id, SAMPLE_LABEL, {
|
|
5504
4750
|
detail: `${record.containerName} does not exist`,
|
|
5505
4751
|
hint: "`rde init` recreates it",
|
|
5506
|
-
|
|
4752
|
+
fix: SAMPLE_DATABASE_STEP
|
|
5507
4753
|
});
|
|
5508
4754
|
if (!status.running) return failed(id, SAMPLE_LABEL, {
|
|
5509
4755
|
detail: `${record.containerName} is stopped`,
|
|
5510
4756
|
hint: "`rde start`",
|
|
5511
|
-
|
|
4757
|
+
fix: "start-metabase"
|
|
5512
4758
|
});
|
|
5513
4759
|
if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
|
|
5514
4760
|
detail: `${record.containerName} runs but port ${record.port} is not answering`,
|
|
5515
4761
|
hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
|
|
5516
|
-
|
|
4762
|
+
fix: null
|
|
5517
4763
|
});
|
|
5518
4764
|
const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
|
|
5519
4765
|
if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
|
|
5520
4766
|
detail: `${where}; not connected to Metabase yet`,
|
|
5521
4767
|
hint: "`rde init` connects it",
|
|
5522
|
-
|
|
4768
|
+
fix: SAMPLE_DATABASE_STEP
|
|
5523
4769
|
});
|
|
5524
|
-
if (
|
|
4770
|
+
if (client === null) return skipped(id, SAMPLE_LABEL, `${where}; ${NOT_HEALTHY}, so the connection is unchecked`);
|
|
5525
4771
|
try {
|
|
5526
|
-
const database = await
|
|
4772
|
+
const database = await client.database.get(record.databaseId);
|
|
5527
4773
|
return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
|
|
5528
4774
|
} catch (error) {
|
|
5529
4775
|
if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
|
|
5530
4776
|
detail: `${where}; database ${record.databaseId} is gone from Metabase`,
|
|
5531
4777
|
hint: "`rde init` connects it again",
|
|
5532
|
-
|
|
4778
|
+
fix: SAMPLE_DATABASE_STEP
|
|
5533
4779
|
});
|
|
5534
4780
|
throw error;
|
|
5535
4781
|
}
|
|
@@ -5546,7 +4792,7 @@ async function gitSyncRow(state, deps) {
|
|
|
5546
4792
|
if (status.branch === null) return failed("git-sync", label, {
|
|
5547
4793
|
detail: "Metabase reports no branch",
|
|
5548
4794
|
hint: "`rde init --only git-sync`",
|
|
5549
|
-
|
|
4795
|
+
fix: "git-sync"
|
|
5550
4796
|
});
|
|
5551
4797
|
const dirty = status.is_dirty ? "unsynced changes" : "in sync";
|
|
5552
4798
|
return ok("git-sync", label, `branch ${status.branch}, ${dirty}, ${state.gitRemote.workdir}`);
|
|
@@ -5554,100 +4800,15 @@ async function gitSyncRow(state, deps) {
|
|
|
5554
4800
|
if (isMbFailure$1(error)) return failed("git-sync", label, {
|
|
5555
4801
|
detail: error.message,
|
|
5556
4802
|
hint: `\`${mbCommand(state.mbProfile)} git-sync status\` shows the full answer`,
|
|
5557
|
-
|
|
4803
|
+
fix: null
|
|
5558
4804
|
});
|
|
5559
4805
|
throw error;
|
|
5560
4806
|
}
|
|
5561
4807
|
}
|
|
5562
4808
|
const SKILL_STEP = "agents-skills";
|
|
5563
|
-
const GLOBAL_SCOPE = "global";
|
|
5564
|
-
const SKILL_FILE = "SKILL.md";
|
|
5565
|
-
const CLAUDE_CODE = "Claude Code";
|
|
5566
|
-
const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
|
|
5567
4809
|
function skillRowId(agent) {
|
|
5568
4810
|
return `skill.${agent.id}`;
|
|
5569
4811
|
}
|
|
5570
|
-
function withRef(source, ref) {
|
|
5571
|
-
return ref === null ? source : `${source}#${ref}`;
|
|
5572
|
-
}
|
|
5573
|
-
async function copyRef(entry, deps) {
|
|
5574
|
-
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
5575
|
-
return lockedRef(path, await deps.skills.files.readJson(path), entry.name);
|
|
5576
|
-
}
|
|
5577
|
-
async function judgeCopy(entry, origin, deps) {
|
|
5578
|
-
const where = `${entry.scope} ${entry.path}`;
|
|
5579
|
-
switch (origin.kind) {
|
|
5580
|
-
case "local": {
|
|
5581
|
-
const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
|
|
5582
|
-
const wanted = await deps.skills.files.readText(join(origin.path, SKILL_FILE));
|
|
5583
|
-
const current = installed !== null && installed === wanted;
|
|
5584
|
-
return {
|
|
5585
|
-
entry,
|
|
5586
|
-
current,
|
|
5587
|
-
line: `${where}: ${current ? "same as" : "differs from"} ${origin.path}`
|
|
5588
|
-
};
|
|
5589
|
-
}
|
|
5590
|
-
case "github": {
|
|
5591
|
-
const wanted = withRef(origin.repository, origin.ref);
|
|
5592
|
-
if (entry.source === null) return {
|
|
5593
|
-
entry,
|
|
5594
|
-
current: false,
|
|
5595
|
-
line: `${where}: no recorded source, not ${wanted}`
|
|
5596
|
-
};
|
|
5597
|
-
const found = withRef(entry.source, await copyRef(entry, deps));
|
|
5598
|
-
const current = found === wanted;
|
|
5599
|
-
return {
|
|
5600
|
-
entry,
|
|
5601
|
-
current,
|
|
5602
|
-
line: `${where}: from ${found}${current ? "" : `, not ${wanted}`}`
|
|
5603
|
-
};
|
|
5604
|
-
}
|
|
5605
|
-
case "other": {
|
|
5606
|
-
const current = entry.source === origin.source;
|
|
5607
|
-
return {
|
|
5608
|
-
entry,
|
|
5609
|
-
current,
|
|
5610
|
-
line: `${where}: from ${entry.source ?? "no recorded source"}${current ? "" : `, not ${origin.source}`}`
|
|
5611
|
-
};
|
|
5612
|
-
}
|
|
5613
|
-
}
|
|
5614
|
-
}
|
|
5615
|
-
function loadedCopyLine(entries, cwd) {
|
|
5616
|
-
if (entries.length < 2) return null;
|
|
5617
|
-
const count = `${entries.length} copies are installed`;
|
|
5618
|
-
const forClaude = entries.filter((entry) => entry.agents.includes(CLAUDE_CODE));
|
|
5619
|
-
const loaded = forClaude.find((entry) => entry.scope === GLOBAL_SCOPE) ?? forClaude[0];
|
|
5620
|
-
if (loaded === void 0) return count;
|
|
5621
|
-
return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
|
|
5622
|
-
}
|
|
5623
|
-
function staleCopiesRow(stale, detail, deps) {
|
|
5624
|
-
const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
|
|
5625
|
-
const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
|
|
5626
|
-
const repairs = [];
|
|
5627
|
-
if (globalStale) repairs.push(`\`rde init --only ${SKILL_STEP}\` reinstalls the global copy from ${deps.skills.source}`);
|
|
5628
|
-
if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove rde -y\` in ${deps.skills.cwd}`);
|
|
5629
|
-
return failed("skill-copies", "rde skill copies", {
|
|
5630
|
-
detail,
|
|
5631
|
-
hint: `${repairs.join("; ")}; then restart the agent session`,
|
|
5632
|
-
step: globalStale ? SKILL_STEP : null
|
|
5633
|
-
});
|
|
5634
|
-
}
|
|
5635
|
-
async function skillCopiesRow(globalEntries, deps) {
|
|
5636
|
-
const id = "skill-copies";
|
|
5637
|
-
const label = "rde skill copies";
|
|
5638
|
-
const cwd = deps.skills.cwd;
|
|
5639
|
-
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === "rde");
|
|
5640
|
-
const entries = [...globalEntries, ...projectEntries];
|
|
5641
|
-
if (entries.length === 0) return skipped(id, label, "no copy installed");
|
|
5642
|
-
const origin = skillOrigin(deps.skills.source, cwd);
|
|
5643
|
-
const judged = [];
|
|
5644
|
-
for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
|
|
5645
|
-
const loaded = loadedCopyLine(entries, cwd);
|
|
5646
|
-
const lines = judged.map((copy) => copy.line);
|
|
5647
|
-
const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
|
|
5648
|
-
const stale = judged.filter((copy) => !copy.current);
|
|
5649
|
-
return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
|
|
5650
|
-
}
|
|
5651
4812
|
async function skillRows(state, deps) {
|
|
5652
4813
|
const label = "rde skill";
|
|
5653
4814
|
if (state === null) return [skipped("skill", label, NO_INSTANCE)];
|
|
@@ -5655,18 +4816,17 @@ async function skillRows(state, deps) {
|
|
|
5655
4816
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
5656
4817
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
5657
4818
|
const entries = await rdeSkillEntries(deps.runner);
|
|
5658
|
-
const copies = await skillCopiesRow(entries, deps);
|
|
5659
4819
|
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
|
|
5660
4820
|
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
5661
4821
|
hint: `\`rde init --only ${SKILL_STEP}\``,
|
|
5662
|
-
|
|
5663
|
-
})
|
|
5664
|
-
return
|
|
4822
|
+
fix: SKILL_STEP
|
|
4823
|
+
})];
|
|
4824
|
+
return detected.map((agent) => {
|
|
5665
4825
|
const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
|
|
5666
4826
|
const agentLabel = `${label} (${agent.displayName})`;
|
|
5667
4827
|
if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
|
|
5668
4828
|
return ok(skillRowId(agent), agentLabel, entry.path);
|
|
5669
|
-
})
|
|
4829
|
+
});
|
|
5670
4830
|
}
|
|
5671
4831
|
function noRuntimeRows() {
|
|
5672
4832
|
return [
|
|
@@ -5675,13 +4835,13 @@ function noRuntimeRows() {
|
|
|
5675
4835
|
"health"
|
|
5676
4836
|
].map((id) => skipped(id, id, NO_INSTANCE));
|
|
5677
4837
|
}
|
|
5678
|
-
async function instanceRows(state,
|
|
4838
|
+
async function instanceRows(state, deps, dockerOk, mbInstalled) {
|
|
5679
4839
|
const configured = configuredRuntime(state);
|
|
5680
4840
|
if (configured === null) return [
|
|
5681
4841
|
...noRuntimeRows(),
|
|
5682
4842
|
skipped("version", "Metabase version", NO_INSTANCE),
|
|
5683
4843
|
skipped("admin", "admin user", NO_INSTANCE),
|
|
5684
|
-
|
|
4844
|
+
apiKeyRow(state),
|
|
5685
4845
|
skipped("mb-profile", "mb profile", NO_INSTANCE),
|
|
5686
4846
|
skipped("license", "license", NO_INSTANCE),
|
|
5687
4847
|
skipped("databases", "databases", NO_INSTANCE),
|
|
@@ -5691,33 +4851,35 @@ async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusable
|
|
|
5691
4851
|
const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
|
|
5692
4852
|
const { row: health, healthy } = await healthRow(configured.url, deps);
|
|
5693
4853
|
const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
|
|
5694
|
-
const apiKey = await apiKeyFinding(secrets, configured.url, healthy, deps);
|
|
5695
4854
|
const rows = [
|
|
5696
4855
|
runtime.row,
|
|
5697
4856
|
await portRow(configured.runtime.port, runtime, deps.ports),
|
|
5698
4857
|
health,
|
|
5699
4858
|
properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
|
|
5700
4859
|
properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
|
|
5701
|
-
|
|
5702
|
-
await mbProfileRow(state,
|
|
4860
|
+
apiKeyRow(state),
|
|
4861
|
+
await mbProfileRow(state, deps, mbInstalled, healthy)
|
|
5703
4862
|
];
|
|
5704
|
-
const
|
|
5705
|
-
|
|
5706
|
-
|
|
5707
|
-
|
|
4863
|
+
const client = healthy && state.apiKey !== null ? deps.client(configured.url, {
|
|
4864
|
+
kind: "api-key",
|
|
4865
|
+
key: state.apiKey
|
|
4866
|
+
}) : null;
|
|
4867
|
+
if (client === null) {
|
|
4868
|
+
const reason = healthy ? "no API key" : NOT_HEALTHY;
|
|
4869
|
+
rows.push(skipped("license", "license", reason), skipped("databases", "databases", reason));
|
|
4870
|
+
} else rows.push(await licenseRow(client), await databasesRow(state, client));
|
|
4871
|
+
rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, client));
|
|
5708
4872
|
rows.push(await gitSyncRow(state, deps));
|
|
5709
4873
|
return rows;
|
|
5710
4874
|
}
|
|
5711
|
-
async function collectDoctor(instance, state,
|
|
4875
|
+
async function collectDoctor(instance, state, deps) {
|
|
5712
4876
|
const machine = await machineRows(deps, state?.runtime ?? null);
|
|
5713
4877
|
const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
|
|
5714
|
-
const mb = await
|
|
5715
|
-
const
|
|
5716
|
-
|
|
5717
|
-
|
|
5718
|
-
|
|
5719
|
-
const rdeProfile = state === null ? null : state.mbProfile;
|
|
5720
|
-
checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
|
|
4878
|
+
const mb = await mbRow(deps);
|
|
4879
|
+
const checks = [...machine, mb];
|
|
4880
|
+
if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE), skipped("admin", "admin user", NO_INSTANCE), skipped("api-key", "API key", NO_INSTANCE), skipped("mb-profile", "mb profile", NO_INSTANCE), skipped("license", "license", NO_INSTANCE), skipped("databases", "databases", NO_INSTANCE), skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE), skipped("git-sync", "remote sync", NO_INSTANCE));
|
|
4881
|
+
else checks.push(...await instanceRows(state, deps, dockerOk, mb.status === "ok"));
|
|
4882
|
+
checks.push(...await skillRows(state, deps));
|
|
5721
4883
|
return {
|
|
5722
4884
|
instance,
|
|
5723
4885
|
checks,
|
|
@@ -5729,38 +4891,18 @@ const MARK = {
|
|
|
5729
4891
|
failed: "✗",
|
|
5730
4892
|
skipped: "–"
|
|
5731
4893
|
};
|
|
5732
|
-
const NOTE_MARK = "!";
|
|
5733
4894
|
const HINT_ARROW = "→";
|
|
5734
|
-
function markFor(row) {
|
|
5735
|
-
return row.status === "ok" && row.hint !== null ? NOTE_MARK : MARK[row.status];
|
|
5736
|
-
}
|
|
5737
4895
|
function doctorLines(report) {
|
|
5738
4896
|
const width = Math.max(...report.checks.map((row) => row.label.length));
|
|
5739
|
-
const indent = ` ${" ".repeat(width)} `;
|
|
5740
4897
|
const lines = [];
|
|
5741
4898
|
for (const row of report.checks) {
|
|
5742
|
-
|
|
5743
|
-
lines.push(
|
|
5744
|
-
lines.push(...rest.map((line) => `${indent}${line}`));
|
|
5745
|
-
if (row.hint !== null) lines.push(`${indent}${HINT_ARROW} ${row.hint}`);
|
|
4899
|
+
lines.push(`${MARK[row.status]} ${row.label.padEnd(width)} ${row.detail}`);
|
|
4900
|
+
if (row.hint !== null) lines.push(` ${" ".repeat(width)} ${HINT_ARROW} ${row.hint}`);
|
|
5746
4901
|
}
|
|
5747
4902
|
return lines;
|
|
5748
4903
|
}
|
|
5749
|
-
function fixCommand(step) {
|
|
5750
|
-
return step === null ? null : `rde init --only ${step}`;
|
|
5751
|
-
}
|
|
5752
|
-
function doctorJson(report) {
|
|
5753
|
-
return {
|
|
5754
|
-
instance: report.instance,
|
|
5755
|
-
checks: report.checks.map(({ step, ...row }) => ({
|
|
5756
|
-
...row,
|
|
5757
|
-
fix: fixCommand(step)
|
|
5758
|
-
})),
|
|
5759
|
-
failed: report.failed
|
|
5760
|
-
};
|
|
5761
|
-
}
|
|
5762
4904
|
function fixableSteps(report) {
|
|
5763
|
-
const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.
|
|
4905
|
+
const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.fix !== null ? [row.fix] : []));
|
|
5764
4906
|
return StepId.options.filter((id) => wanted.has(id));
|
|
5765
4907
|
}
|
|
5766
4908
|
function doctorSummary(report) {
|
|
@@ -5799,24 +4941,15 @@ const ADMIN_USER_KEYS = {
|
|
|
5799
4941
|
firstName: `${STEP_ID$6}.first-name`,
|
|
5800
4942
|
lastName: `${STEP_ID$6}.last-name`
|
|
5801
4943
|
};
|
|
5802
|
-
const
|
|
5803
|
-
const
|
|
4944
|
+
const DEFAULT_ADMIN_EMAIL = "admin@metabase.com";
|
|
4945
|
+
const DEFAULT_ADMIN_PASSWORD = "metabase123";
|
|
5804
4946
|
const DEFAULT_SITE_NAME = "rde";
|
|
5805
4947
|
const MIN_PASSWORD_LENGTH = 6;
|
|
5806
4948
|
const DIGIT = /\d/;
|
|
5807
4949
|
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
5808
|
-
const DEFAULT_ADMIN = { variant: "
|
|
5809
|
-
function generatePassword(random) {
|
|
5810
|
-
for (;;) {
|
|
5811
|
-
const candidate = random(GENERATED_PASSWORD_BYTES).toString("base64url");
|
|
5812
|
-
if (DIGIT.test(candidate)) return candidate;
|
|
5813
|
-
}
|
|
5814
|
-
}
|
|
4950
|
+
const DEFAULT_ADMIN = { variant: "default" };
|
|
5815
4951
|
function hostDeps$8(ctx) {
|
|
5816
|
-
return {
|
|
5817
|
-
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5818
|
-
password: () => generatePassword(randomBytes)
|
|
5819
|
-
};
|
|
4952
|
+
return { client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal) };
|
|
5820
4953
|
}
|
|
5821
4954
|
function isNotReady(error) {
|
|
5822
4955
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
@@ -5855,22 +4988,26 @@ async function promptedUser(ctx) {
|
|
|
5855
4988
|
...lastName.length > 0 && { last_name: lastName }
|
|
5856
4989
|
};
|
|
5857
4990
|
}
|
|
5858
|
-
async function chooseUser(ctx,
|
|
5859
|
-
switch (
|
|
5860
|
-
case "
|
|
5861
|
-
email:
|
|
5862
|
-
password:
|
|
4991
|
+
async function chooseUser(ctx, kind) {
|
|
4992
|
+
switch (kind) {
|
|
4993
|
+
case "default": return {
|
|
4994
|
+
email: DEFAULT_ADMIN_EMAIL,
|
|
4995
|
+
password: DEFAULT_ADMIN_PASSWORD
|
|
5863
4996
|
};
|
|
5864
4997
|
case "prompted": return promptedUser(ctx);
|
|
5865
4998
|
}
|
|
5866
4999
|
}
|
|
5867
|
-
function
|
|
5868
|
-
switch (
|
|
5869
|
-
case "
|
|
5000
|
+
function credentialsRecord(kind, user) {
|
|
5001
|
+
switch (kind) {
|
|
5002
|
+
case "default": return {
|
|
5003
|
+
kind,
|
|
5870
5004
|
email: user.email,
|
|
5871
5005
|
password: user.password
|
|
5872
5006
|
};
|
|
5873
|
-
case "prompted": return
|
|
5007
|
+
case "prompted": return {
|
|
5008
|
+
kind,
|
|
5009
|
+
email: user.email
|
|
5010
|
+
};
|
|
5874
5011
|
}
|
|
5875
5012
|
}
|
|
5876
5013
|
function adminUserStep(options, deps = hostDeps$8) {
|
|
@@ -5889,8 +5026,7 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5889
5026
|
},
|
|
5890
5027
|
async run(ctx) {
|
|
5891
5028
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5892
|
-
const
|
|
5893
|
-
const client = resolved.client(configured.url, NO_AUTH);
|
|
5029
|
+
const client = deps(ctx).client(configured.url, NO_AUTH);
|
|
5894
5030
|
const properties = await client.session.properties();
|
|
5895
5031
|
if (properties["has-user-setup"]) {
|
|
5896
5032
|
ctx.ui.info("Metabase already has an admin user");
|
|
@@ -5898,11 +5034,7 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5898
5034
|
}
|
|
5899
5035
|
const token = properties["setup-token"];
|
|
5900
5036
|
if (token === null) throw new ConfigError("Metabase has no admin user yet but offers no setup token");
|
|
5901
|
-
const user = await chooseUser(ctx, options.variant
|
|
5902
|
-
await ctx.saveSecrets({
|
|
5903
|
-
...ctx.secrets,
|
|
5904
|
-
admin: storedLogin(options.variant, user)
|
|
5905
|
-
});
|
|
5037
|
+
const user = await chooseUser(ctx, options.variant);
|
|
5906
5038
|
const session = await client.setup.create({
|
|
5907
5039
|
token,
|
|
5908
5040
|
user,
|
|
@@ -5911,10 +5043,7 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5911
5043
|
ctx.session.metabaseSession.set(session.id);
|
|
5912
5044
|
await ctx.saveState({
|
|
5913
5045
|
...ctx.state,
|
|
5914
|
-
admin:
|
|
5915
|
-
kind: options.variant,
|
|
5916
|
-
email: user.email
|
|
5917
|
-
}
|
|
5046
|
+
admin: credentialsRecord(options.variant, user)
|
|
5918
5047
|
});
|
|
5919
5048
|
ctx.ui.success(`Admin user ${user.email} created`);
|
|
5920
5049
|
return "completed";
|
|
@@ -5937,12 +5066,11 @@ function hostDeps$7(ctx) {
|
|
|
5937
5066
|
};
|
|
5938
5067
|
}
|
|
5939
5068
|
async function adminCredentials(ctx) {
|
|
5940
|
-
const stored = ctx.secrets.admin;
|
|
5941
|
-
if (stored !== null) return {
|
|
5942
|
-
username: stored.email,
|
|
5943
|
-
password: stored.password
|
|
5944
|
-
};
|
|
5945
5069
|
const admin = ctx.state.admin;
|
|
5070
|
+
if (admin?.kind === "default") return {
|
|
5071
|
+
username: admin.email,
|
|
5072
|
+
password: admin.password
|
|
5073
|
+
};
|
|
5946
5074
|
ctx.ui.info("An admin login is needed once to create the API key; it is not saved");
|
|
5947
5075
|
return {
|
|
5948
5076
|
username: await ctx.prompts.text({
|
|
@@ -5980,70 +5108,46 @@ async function freshApiKey(ctx, client) {
|
|
|
5980
5108
|
group_id: groupId
|
|
5981
5109
|
})).unmasked_key;
|
|
5982
5110
|
}
|
|
5983
|
-
async function
|
|
5984
|
-
|
|
5985
|
-
await client.user.current();
|
|
5986
|
-
return true;
|
|
5987
|
-
} catch (error) {
|
|
5988
|
-
if (isRejectedCredential(error)) return false;
|
|
5989
|
-
throw error;
|
|
5990
|
-
}
|
|
5991
|
-
}
|
|
5992
|
-
async function workingApiKey(ctx, deps, url) {
|
|
5993
|
-
const stored = ctx.secrets.apiKey;
|
|
5994
|
-
if (stored !== null) {
|
|
5995
|
-
if (await authenticates(deps.client(url, {
|
|
5996
|
-
kind: "api-key",
|
|
5997
|
-
key: stored
|
|
5998
|
-
}))) return stored;
|
|
5999
|
-
ctx.ui.warn("Metabase rejects the stored API key; replacing it");
|
|
6000
|
-
}
|
|
5111
|
+
async function persistedApiKey(ctx, deps, url) {
|
|
5112
|
+
if (ctx.state.apiKey !== null) return ctx.state.apiKey;
|
|
6001
5113
|
const sessionId = await superuserSession(ctx, deps, url);
|
|
6002
5114
|
const apiKey = await freshApiKey(ctx, deps.client(url, {
|
|
6003
5115
|
kind: "session",
|
|
6004
5116
|
id: sessionId
|
|
6005
5117
|
}));
|
|
6006
|
-
await ctx.
|
|
6007
|
-
...ctx.
|
|
5118
|
+
await ctx.saveState({
|
|
5119
|
+
...ctx.state,
|
|
6008
5120
|
apiKey
|
|
6009
5121
|
});
|
|
6010
5122
|
return apiKey;
|
|
6011
5123
|
}
|
|
6012
|
-
async function
|
|
5124
|
+
async function chooseProfile(ctx, mb, url) {
|
|
5125
|
+
const profile = ctx.state.mbProfile;
|
|
5126
|
+
if (profile !== "default") return profile;
|
|
5127
|
+
let existing;
|
|
6013
5128
|
try {
|
|
6014
|
-
|
|
5129
|
+
existing = await mb.authStatus(DEFAULT_MB_PROFILE);
|
|
6015
5130
|
} catch (error) {
|
|
6016
|
-
if (error instanceof ProcessNotFoundError) return
|
|
5131
|
+
if (error instanceof ProcessNotFoundError) return profile;
|
|
6017
5132
|
throw error;
|
|
6018
5133
|
}
|
|
6019
|
-
|
|
6020
|
-
|
|
6021
|
-
const profile = ctx.state.mbProfile;
|
|
6022
|
-
if (profile !== "default") return profile;
|
|
6023
|
-
const existing = await defaultProfileStatus(mb);
|
|
6024
|
-
if (existing === null || !existing.present || existing.url === null || existing.url === url) return profile;
|
|
6025
|
-
const previous = existing.url;
|
|
6026
|
-
const named = namedMbProfile(ctx.state.name);
|
|
6027
|
-
if (await ctx.prompts.select({
|
|
5134
|
+
if (!existing.present || existing.url === url) return profile;
|
|
5135
|
+
const chosen = await ctx.prompts.select({
|
|
6028
5136
|
key: API_KEY_KEYS.profile,
|
|
6029
|
-
message: `mb profile
|
|
5137
|
+
message: `mb profile ${DEFAULT_MB_PROFILE} already points at ${existing.url}; which profile should this instance use?`,
|
|
6030
5138
|
choices: [{
|
|
6031
|
-
value: "
|
|
6032
|
-
label: `
|
|
5139
|
+
value: "rde",
|
|
5140
|
+
label: `rde, and leave ${DEFAULT_MB_PROFILE} alone`
|
|
6033
5141
|
}, {
|
|
6034
|
-
value:
|
|
6035
|
-
label: `${
|
|
6036
|
-
}]
|
|
6037
|
-
|
|
6038
|
-
|
|
6039
|
-
ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
|
|
6040
|
-
return profile;
|
|
6041
|
-
}
|
|
6042
|
-
await ctx.saveState({
|
|
5142
|
+
value: DEFAULT_MB_PROFILE,
|
|
5143
|
+
label: `${DEFAULT_MB_PROFILE}, replacing it`
|
|
5144
|
+
}]
|
|
5145
|
+
});
|
|
5146
|
+
if (chosen !== profile) await ctx.saveState({
|
|
6043
5147
|
...ctx.state,
|
|
6044
|
-
mbProfile:
|
|
5148
|
+
mbProfile: chosen
|
|
6045
5149
|
});
|
|
6046
|
-
return
|
|
5150
|
+
return chosen;
|
|
6047
5151
|
}
|
|
6048
5152
|
function usageNote(profile) {
|
|
6049
5153
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
@@ -6054,7 +5158,7 @@ function apiKeyStep(deps = hostDeps$7) {
|
|
|
6054
5158
|
id: STEP_ID$5,
|
|
6055
5159
|
title: "Connect the Metabase CLI",
|
|
6056
5160
|
async detect(ctx) {
|
|
6057
|
-
if (ctx.
|
|
5161
|
+
if (ctx.state.apiKey === null) return "todo";
|
|
6058
5162
|
const mb = mbTool(deps(ctx).runner);
|
|
6059
5163
|
const profile = ctx.state.mbProfile;
|
|
6060
5164
|
try {
|
|
@@ -6068,7 +5172,7 @@ function apiKeyStep(deps = hostDeps$7) {
|
|
|
6068
5172
|
async run(ctx) {
|
|
6069
5173
|
const resolved = deps(ctx);
|
|
6070
5174
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6071
|
-
const apiKey = await
|
|
5175
|
+
const apiKey = await persistedApiKey(ctx, resolved, configured.url);
|
|
6072
5176
|
const mb = mbTool(resolved.runner);
|
|
6073
5177
|
const profile = await chooseProfile(ctx, mb, configured.url);
|
|
6074
5178
|
await mb.authLogin({
|
|
@@ -6470,14 +5574,14 @@ function isMbUnavailable(error) {
|
|
|
6470
5574
|
}
|
|
6471
5575
|
async function runGitSync(ctx, deps) {
|
|
6472
5576
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6473
|
-
if (ctx.
|
|
5577
|
+
if (ctx.state.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
6474
5578
|
const run = {
|
|
6475
5579
|
ctx,
|
|
6476
5580
|
deps,
|
|
6477
5581
|
git: gitTool(deps.runner),
|
|
6478
5582
|
client: deps.client(configured.url, {
|
|
6479
5583
|
kind: "api-key",
|
|
6480
|
-
key: ctx.
|
|
5584
|
+
key: ctx.state.apiKey
|
|
6481
5585
|
})
|
|
6482
5586
|
};
|
|
6483
5587
|
const flow = await runFlow({
|
|
@@ -6533,8 +5637,8 @@ function validateToken(value) {
|
|
|
6533
5637
|
return "A license token is 64 hexadecimal characters";
|
|
6534
5638
|
}
|
|
6535
5639
|
function requireApiKey$1(ctx) {
|
|
6536
|
-
if (ctx.
|
|
6537
|
-
return ctx.
|
|
5640
|
+
if (ctx.state.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
5641
|
+
return ctx.state.apiKey;
|
|
6538
5642
|
}
|
|
6539
5643
|
function activeLabel(status) {
|
|
6540
5644
|
const plan = status["plan-alias"];
|
|
@@ -6547,15 +5651,15 @@ function licenseStep(deps = hostDeps$5) {
|
|
|
6547
5651
|
async detect(ctx) {
|
|
6548
5652
|
if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
|
|
6549
5653
|
const configured = configuredRuntime(ctx.state);
|
|
6550
|
-
if (configured === null || ctx.
|
|
5654
|
+
if (configured === null || ctx.state.apiKey === null) return "todo";
|
|
6551
5655
|
const client = deps(ctx).client(configured.url, {
|
|
6552
5656
|
kind: "api-key",
|
|
6553
|
-
key: ctx.
|
|
5657
|
+
key: ctx.state.apiKey
|
|
6554
5658
|
});
|
|
6555
5659
|
try {
|
|
6556
5660
|
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
6557
5661
|
} catch (error) {
|
|
6558
|
-
if (isNoTokenYet(error)
|
|
5662
|
+
if (isNoTokenYet(error)) return "todo";
|
|
6559
5663
|
throw error;
|
|
6560
5664
|
}
|
|
6561
5665
|
},
|
|
@@ -6790,9 +5894,9 @@ function isNotServingYet(error) {
|
|
|
6790
5894
|
function isNotFound$1(error) {
|
|
6791
5895
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
|
|
6792
5896
|
}
|
|
6793
|
-
function requireApiKey(
|
|
6794
|
-
if (
|
|
6795
|
-
return
|
|
5897
|
+
function requireApiKey(state) {
|
|
5898
|
+
if (state.apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
5899
|
+
return state.apiKey;
|
|
6796
5900
|
}
|
|
6797
5901
|
async function recordedDatabase(record, client) {
|
|
6798
5902
|
if (record.databaseId === null) return null;
|
|
@@ -6914,23 +6018,23 @@ function sampleDatabaseStep(deps = hostDeps$3) {
|
|
|
6914
6018
|
if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
|
|
6915
6019
|
const record = ctx.state.sampleDatabase;
|
|
6916
6020
|
const configured = configuredRuntime(ctx.state);
|
|
6917
|
-
if (record === null || configured === null || ctx.
|
|
6021
|
+
if (record === null || configured === null || ctx.state.apiKey === null) return "todo";
|
|
6918
6022
|
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
|
|
6919
6023
|
const client = resolved.client(configured.url, {
|
|
6920
6024
|
kind: "api-key",
|
|
6921
|
-
key: ctx.
|
|
6025
|
+
key: ctx.state.apiKey
|
|
6922
6026
|
});
|
|
6923
6027
|
try {
|
|
6924
6028
|
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
6925
6029
|
} catch (error) {
|
|
6926
|
-
if (isNotServingYet(error)
|
|
6030
|
+
if (isNotServingYet(error)) return "todo";
|
|
6927
6031
|
throw error;
|
|
6928
6032
|
}
|
|
6929
6033
|
},
|
|
6930
6034
|
async run(ctx) {
|
|
6931
6035
|
const resolved = deps(ctx);
|
|
6932
6036
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6933
|
-
const apiKey = requireApiKey(ctx);
|
|
6037
|
+
const apiKey = requireApiKey(ctx.state);
|
|
6934
6038
|
const docker = await resolved.runtime.docker.available();
|
|
6935
6039
|
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
6936
6040
|
const choice = await persistedRecord(ctx, resolved);
|
|
@@ -7045,24 +6149,23 @@ function hostDeps$1(ctx) {
|
|
|
7045
6149
|
function isNotServing$1(error) {
|
|
7046
6150
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7047
6151
|
}
|
|
7048
|
-
function loginLine(
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
case "
|
|
7052
|
-
case "prompted": return `Login: ${email} with the password you entered`;
|
|
6152
|
+
function loginLine(admin) {
|
|
6153
|
+
if (admin === null) return null;
|
|
6154
|
+
switch (admin.kind) {
|
|
6155
|
+
case "default": return `Login: ${admin.email} / ${admin.password}`;
|
|
6156
|
+
case "prompted": return `Login: ${admin.email} with the password you entered`;
|
|
7053
6157
|
}
|
|
7054
6158
|
}
|
|
7055
|
-
async function databaseLines$1(state,
|
|
7056
|
-
if (state.url === null || apiKey === null) return ["Databases: none connected yet"];
|
|
6159
|
+
async function databaseLines$1(state, deps) {
|
|
6160
|
+
if (state.url === null || state.apiKey === null) return ["Databases: none connected yet"];
|
|
7057
6161
|
let databases;
|
|
7058
6162
|
try {
|
|
7059
6163
|
databases = (await deps.client(state.url, {
|
|
7060
6164
|
kind: "api-key",
|
|
7061
|
-
key: apiKey
|
|
6165
|
+
key: state.apiKey
|
|
7062
6166
|
}).database.list()).data;
|
|
7063
6167
|
} catch (error) {
|
|
7064
6168
|
if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
|
|
7065
|
-
if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
|
|
7066
6169
|
throw error;
|
|
7067
6170
|
}
|
|
7068
6171
|
if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
|
|
@@ -7096,13 +6199,14 @@ function launchLines(agents, workdir) {
|
|
|
7096
6199
|
const prefix = workdir === null ? "" : `cd ${workdir} && `;
|
|
7097
6200
|
return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
|
|
7098
6201
|
}
|
|
7099
|
-
async function summaryLines(state,
|
|
6202
|
+
async function summaryLines(state, deps) {
|
|
7100
6203
|
const profile = state.mbProfile;
|
|
7101
6204
|
const lines = [];
|
|
7102
6205
|
if (state.url !== null) lines.push(`Metabase: ${state.url}`);
|
|
7103
|
-
|
|
6206
|
+
const login = loginLine(state.admin);
|
|
6207
|
+
if (login !== null) lines.push(login);
|
|
7104
6208
|
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
7105
|
-
lines.push(...await databaseLines$1(state,
|
|
6209
|
+
lines.push(...await databaseLines$1(state, deps));
|
|
7106
6210
|
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
7107
6211
|
lines.push(...gitLines(state));
|
|
7108
6212
|
const installed = await skillsCli(deps.runner).list();
|
|
@@ -7119,8 +6223,7 @@ function summaryStep(deps = hostDeps$1) {
|
|
|
7119
6223
|
return "todo";
|
|
7120
6224
|
},
|
|
7121
6225
|
async run(ctx) {
|
|
7122
|
-
|
|
7123
|
-
ctx.ui.note(lines.join("\n"), "Your environment");
|
|
6226
|
+
ctx.ui.note((await summaryLines(ctx.state, deps(ctx))).join("\n"), "Your environment");
|
|
7124
6227
|
return "completed";
|
|
7125
6228
|
}
|
|
7126
6229
|
};
|
|
@@ -7131,7 +6234,7 @@ const DEFAULT_STEP_OPTIONS = {
|
|
|
7131
6234
|
runtime: NO_RUNTIME_FLAGS,
|
|
7132
6235
|
agents: DETECT_AGENTS,
|
|
7133
6236
|
admin: DEFAULT_ADMIN,
|
|
7134
|
-
databases:
|
|
6237
|
+
databases: ASK_DATABASES
|
|
7135
6238
|
};
|
|
7136
6239
|
const STEP_BY_ID = {
|
|
7137
6240
|
preflight: () => preflightStep(),
|
|
@@ -7151,129 +6254,10 @@ function initSteps(options) {
|
|
|
7151
6254
|
return StepId.options.map((id) => STEP_BY_ID[id](options));
|
|
7152
6255
|
}
|
|
7153
6256
|
//#endregion
|
|
7154
|
-
//#region src/wizard/answers.ts
|
|
7155
|
-
function isOneOf(answer, kinds) {
|
|
7156
|
-
return kinds.some((kind) => kind === answer.kind);
|
|
7157
|
-
}
|
|
7158
|
-
function rejected(key, prompt, reason) {
|
|
7159
|
-
return new ConfigError(`answers file: "${key}" for "${prompt.message}" ${reason}`);
|
|
7160
|
-
}
|
|
7161
|
-
function expectKind(taken, prompt, kinds) {
|
|
7162
|
-
if (taken.answer.kind === "cancel") throw new AbortError();
|
|
7163
|
-
if (isOneOf(taken.answer, kinds)) return taken.answer;
|
|
7164
|
-
throw rejected(taken.key, prompt, `is a ${taken.answer.kind} answer; expected ${kinds.join(" or ")}`);
|
|
7165
|
-
}
|
|
7166
|
-
function choiceLabels(choices) {
|
|
7167
|
-
return choices.map((choice) => String(choice.value)).join(", ");
|
|
7168
|
-
}
|
|
7169
|
-
function matchChoice(key, prompt, value) {
|
|
7170
|
-
const match = prompt.choices.find((choice) => choice.value === value);
|
|
7171
|
-
if (match === void 0) throw rejected(key, prompt, `is ${String(value)}, not one of the choices ${choiceLabels(prompt.choices)}`);
|
|
7172
|
-
return match.value;
|
|
7173
|
-
}
|
|
7174
|
-
function textValue(taken, prompt) {
|
|
7175
|
-
const answer = expectKind(taken, prompt, ["text"]);
|
|
7176
|
-
const value = answer.value.length === 0 && prompt.defaultValue !== void 0 ? prompt.defaultValue : answer.value;
|
|
7177
|
-
const problem = prompt.validate?.(value);
|
|
7178
|
-
if (problem !== void 0) throw rejected(taken.key, prompt, `is "${value}", which is rejected: ${problem}`);
|
|
7179
|
-
return value;
|
|
7180
|
-
}
|
|
7181
|
-
function passwordValue(taken, prompt) {
|
|
7182
|
-
const answer = expectKind(taken, prompt, ["password", "text"]);
|
|
7183
|
-
const problem = prompt.validate?.(answer.value);
|
|
7184
|
-
if (problem !== void 0) throw rejected(taken.key, prompt, `is rejected: ${problem}`);
|
|
7185
|
-
return answer.value;
|
|
7186
|
-
}
|
|
7187
|
-
function isBackAnswer(answer) {
|
|
7188
|
-
return answer.kind === "back";
|
|
7189
|
-
}
|
|
7190
|
-
function answeringPrompts(base, answers, options = {}) {
|
|
7191
|
-
function lookup(prompt) {
|
|
7192
|
-
if (prompt.key === void 0) return null;
|
|
7193
|
-
const answer = answers.get(prompt.key)?.shift();
|
|
7194
|
-
return answer === void 0 ? null : {
|
|
7195
|
-
key: prompt.key,
|
|
7196
|
-
answer
|
|
7197
|
-
};
|
|
7198
|
-
}
|
|
7199
|
-
function refuseUnanswered(prompt) {
|
|
7200
|
-
const refusal = options.unanswered?.(prompt) ?? null;
|
|
7201
|
-
if (refusal !== null) throw refusal;
|
|
7202
|
-
}
|
|
7203
|
-
function take(prompt) {
|
|
7204
|
-
const taken = lookup(prompt);
|
|
7205
|
-
if (taken === null) refuseUnanswered(prompt);
|
|
7206
|
-
return taken;
|
|
7207
|
-
}
|
|
7208
|
-
function takeText(prompt) {
|
|
7209
|
-
const taken = lookup(prompt);
|
|
7210
|
-
if (taken !== null) return taken;
|
|
7211
|
-
if (prompt.defaultValue !== void 0 && options.unanswered !== void 0) return {
|
|
7212
|
-
key: prompt.key ?? prompt.message,
|
|
7213
|
-
answer: {
|
|
7214
|
-
kind: "text",
|
|
7215
|
-
value: ""
|
|
7216
|
-
}
|
|
7217
|
-
};
|
|
7218
|
-
refuseUnanswered(prompt);
|
|
7219
|
-
return null;
|
|
7220
|
-
}
|
|
7221
|
-
function selected(taken, prompt) {
|
|
7222
|
-
const answer = expectKind(taken, prompt, ["select", "text"]);
|
|
7223
|
-
return matchChoice(taken.key, prompt, answer.value);
|
|
7224
|
-
}
|
|
7225
|
-
function multiselected(taken, prompt) {
|
|
7226
|
-
return expectKind(taken, prompt, ["multiselect"]).values.map((value) => matchChoice(taken.key, prompt, value));
|
|
7227
|
-
}
|
|
7228
|
-
return {
|
|
7229
|
-
async text(opts) {
|
|
7230
|
-
const taken = takeText(opts);
|
|
7231
|
-
return taken === null ? base.text(opts) : textValue(taken, opts);
|
|
7232
|
-
},
|
|
7233
|
-
async textOrBack(opts) {
|
|
7234
|
-
const taken = takeText(opts);
|
|
7235
|
-
if (taken === null) return base.textOrBack(opts);
|
|
7236
|
-
return isBackAnswer(taken.answer) ? BACK : textValue(taken, opts);
|
|
7237
|
-
},
|
|
7238
|
-
async password(opts) {
|
|
7239
|
-
const taken = take(opts);
|
|
7240
|
-
return taken === null ? base.password(opts) : passwordValue(taken, opts);
|
|
7241
|
-
},
|
|
7242
|
-
async confirm(opts) {
|
|
7243
|
-
const taken = lookup(opts);
|
|
7244
|
-
if (taken !== null) return expectKind(taken, opts, ["confirm"]).value;
|
|
7245
|
-
if (options.confirmAll === true) return true;
|
|
7246
|
-
refuseUnanswered(opts);
|
|
7247
|
-
return base.confirm(opts);
|
|
7248
|
-
},
|
|
7249
|
-
async select(opts) {
|
|
7250
|
-
const taken = lookup(opts);
|
|
7251
|
-
if (taken !== null) return selected(taken, opts);
|
|
7252
|
-
if (options.takeRecommended === true && opts.recommended !== void 0) return opts.recommended;
|
|
7253
|
-
refuseUnanswered(opts);
|
|
7254
|
-
return base.select(opts);
|
|
7255
|
-
},
|
|
7256
|
-
async selectOrBack(opts) {
|
|
7257
|
-
const taken = take(opts);
|
|
7258
|
-
if (taken === null) return base.selectOrBack(opts);
|
|
7259
|
-
return isBackAnswer(taken.answer) ? BACK : selected(taken, opts);
|
|
7260
|
-
},
|
|
7261
|
-
async multiselect(opts) {
|
|
7262
|
-
const taken = take(opts);
|
|
7263
|
-
return taken === null ? base.multiselect(opts) : multiselected(taken, opts);
|
|
7264
|
-
},
|
|
7265
|
-
async multiselectOrBack(opts) {
|
|
7266
|
-
const taken = take(opts);
|
|
7267
|
-
if (taken === null) return base.multiselectOrBack(opts);
|
|
7268
|
-
return isBackAnswer(taken.answer) ? BACK : multiselected(taken, opts);
|
|
7269
|
-
}
|
|
7270
|
-
};
|
|
7271
|
-
}
|
|
7272
|
-
//#endregion
|
|
7273
6257
|
//#region src/commands/doctor.ts
|
|
7274
6258
|
function print(report, json) {
|
|
7275
6259
|
if (json) {
|
|
7276
|
-
printJson(
|
|
6260
|
+
printJson(report);
|
|
7277
6261
|
return;
|
|
7278
6262
|
}
|
|
7279
6263
|
printLines(doctorLines(report));
|
|
@@ -7304,16 +6288,12 @@ const doctor = defineRdeCommand({
|
|
|
7304
6288
|
const name = await instanceNameOrDefault();
|
|
7305
6289
|
const state = await readInstanceState(name);
|
|
7306
6290
|
const deps = hostDoctorDeps(name, interruptSignal);
|
|
7307
|
-
let report = await collectDoctor(name, state,
|
|
6291
|
+
let report = await collectDoctor(name, state, deps);
|
|
7308
6292
|
if (args.fix && report.failed > 0) {
|
|
7309
6293
|
if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
|
|
7310
6294
|
print(report, args.json);
|
|
7311
|
-
const
|
|
7312
|
-
|
|
7313
|
-
signal: interruptSignal,
|
|
7314
|
-
prompts
|
|
7315
|
-
});
|
|
7316
|
-
if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
|
|
6295
|
+
const ctx = await createWizardContext(name, { signal: interruptSignal });
|
|
6296
|
+
if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, deps);
|
|
7317
6297
|
}
|
|
7318
6298
|
print(report, args.json);
|
|
7319
6299
|
if (report.failed > 0) throw new Error(doctorSummary(report));
|
|
@@ -7338,7 +6318,7 @@ const EnvString = z.string().transform((value, ctx) => {
|
|
|
7338
6318
|
}
|
|
7339
6319
|
return resolved;
|
|
7340
6320
|
});
|
|
7341
|
-
const AdminVariant = z.enum(["
|
|
6321
|
+
const AdminVariant = z.enum(["default", "prompted"]);
|
|
7342
6322
|
const RemoteKind = z.enum(["local-bare", "hosted"]);
|
|
7343
6323
|
const FieldValue = z.union([
|
|
7344
6324
|
EnvString,
|
|
@@ -7442,11 +6422,7 @@ function runtimeKindOf(runtime) {
|
|
|
7442
6422
|
if (runtime.jar !== void 0) return "jar";
|
|
7443
6423
|
return null;
|
|
7444
6424
|
}
|
|
7445
|
-
function
|
|
7446
|
-
if (databases !== void 0) return databases.length > 0 ? "named" : "sample-stands-in";
|
|
7447
|
-
return run.interactive ? "ask-once" : "sample-stands-in";
|
|
7448
|
-
}
|
|
7449
|
-
function stepOptionsFrom(answers, run) {
|
|
6425
|
+
function stepOptionsFrom(answers) {
|
|
7450
6426
|
const runtime = answers.runtime;
|
|
7451
6427
|
const agents = answers["agents-skills"];
|
|
7452
6428
|
const admin = answers["admin-user"];
|
|
@@ -7460,7 +6436,7 @@ function stepOptionsFrom(answers, run) {
|
|
|
7460
6436
|
},
|
|
7461
6437
|
agents: agents === void 0 ? DEFAULT_STEP_OPTIONS.agents : { agents: agents.agents },
|
|
7462
6438
|
admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
|
|
7463
|
-
databases: {
|
|
6439
|
+
databases: { requested: databases !== void 0 && databases.length > 0 }
|
|
7464
6440
|
};
|
|
7465
6441
|
}
|
|
7466
6442
|
function keyedAnswers(answers) {
|
|
@@ -7578,7 +6554,7 @@ function parseDatabase(spec) {
|
|
|
7578
6554
|
function adminSection(flags, secrets) {
|
|
7579
6555
|
const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
|
|
7580
6556
|
if (flags.admin === void 0 && !prompted) return;
|
|
7581
|
-
if (flags.admin === "
|
|
6557
|
+
if (flags.admin === "default" && prompted) throw conflict("--admin default", "--admin-email/--admin-password");
|
|
7582
6558
|
return {
|
|
7583
6559
|
variant: flags.admin ?? "prompted",
|
|
7584
6560
|
...flags.adminEmail !== void 0 && { email: flags.adminEmail },
|
|
@@ -7729,57 +6705,20 @@ function isLocation(value) {
|
|
|
7729
6705
|
function isRemote(value) {
|
|
7730
6706
|
return value === "local-bare" || value === "hosted";
|
|
7731
6707
|
}
|
|
7732
|
-
function isUnreviewedPaste(database) {
|
|
7733
|
-
return database !== void 0 && database.pasted && !database.reviewed;
|
|
7734
|
-
}
|
|
7735
|
-
function applyConnectionRecord(draft, value) {
|
|
7736
|
-
draft.databases = draft.databases.filter((database) => !database.pasted);
|
|
7737
|
-
if (value === null) return;
|
|
7738
|
-
if (value.trim().length === 0) {
|
|
7739
|
-
draft.databasesFinished = true;
|
|
7740
|
-
return;
|
|
7741
|
-
}
|
|
7742
|
-
draft.databases.push({
|
|
7743
|
-
engine: null,
|
|
7744
|
-
uri: value.trim(),
|
|
7745
|
-
fields: {},
|
|
7746
|
-
name: null,
|
|
7747
|
-
pasted: true,
|
|
7748
|
-
reviewed: false
|
|
7749
|
-
});
|
|
7750
|
-
}
|
|
7751
|
-
function applyEngineRecord(draft, value) {
|
|
7752
|
-
if (isUnreviewedPaste(draft.databases.at(-1))) draft.databases.pop();
|
|
7753
|
-
if (value === "finish") draft.databasesFinished = true;
|
|
7754
|
-
else if (value !== null) draft.databases.push({
|
|
7755
|
-
engine: value,
|
|
7756
|
-
uri: null,
|
|
7757
|
-
fields: {},
|
|
7758
|
-
name: null,
|
|
7759
|
-
pasted: false,
|
|
7760
|
-
reviewed: false
|
|
7761
|
-
});
|
|
7762
|
-
}
|
|
7763
6708
|
function applyDatabaseRecord(draft, record) {
|
|
7764
6709
|
const value = stringOf(record.answer);
|
|
7765
|
-
if (record.key === DATABASE_KEYS.connection) {
|
|
7766
|
-
applyConnectionRecord(draft, value);
|
|
7767
|
-
return;
|
|
7768
|
-
}
|
|
7769
6710
|
if (record.key === DATABASE_KEYS.engine) {
|
|
7770
|
-
|
|
6711
|
+
if (value === "finish") draft.databasesFinished = true;
|
|
6712
|
+
else if (value !== null) draft.databases.push({
|
|
6713
|
+
engine: value,
|
|
6714
|
+
uri: null,
|
|
6715
|
+
fields: {},
|
|
6716
|
+
name: null
|
|
6717
|
+
});
|
|
7771
6718
|
return;
|
|
7772
6719
|
}
|
|
7773
6720
|
const database = draft.databases.at(-1);
|
|
7774
6721
|
if (database === void 0) return;
|
|
7775
|
-
if (record.key === DATABASE_KEYS.detectedEngine && value !== null) {
|
|
7776
|
-
database.engine = value;
|
|
7777
|
-
return;
|
|
7778
|
-
}
|
|
7779
|
-
if (record.key === DATABASE_KEYS.action) {
|
|
7780
|
-
database.reviewed = true;
|
|
7781
|
-
return;
|
|
7782
|
-
}
|
|
7783
6722
|
if (record.key === DATABASE_KEYS.uri && value !== null) {
|
|
7784
6723
|
database.uri = value;
|
|
7785
6724
|
return;
|
|
@@ -7821,17 +6760,11 @@ function applyRecord(draft, record) {
|
|
|
7821
6760
|
function writableUri(uri, index) {
|
|
7822
6761
|
return URI_WITH_PASSWORD.test(uri) ? envReference(databaseEnv(index, "uri")) : uri;
|
|
7823
6762
|
}
|
|
7824
|
-
function engineOf(database) {
|
|
7825
|
-
if (database.engine !== null) return database.engine;
|
|
7826
|
-
const [only, ...others] = database.uri === null ? [] : detectEngine(database.uri);
|
|
7827
|
-
return only !== void 0 && others.length === 0 ? only : null;
|
|
7828
|
-
}
|
|
7829
6763
|
function databaseEntry(database, index) {
|
|
7830
6764
|
const hasFields = Object.keys(database.fields).length > 0;
|
|
7831
|
-
|
|
7832
|
-
if (database.uri === null && !hasFields || engine === null) return null;
|
|
6765
|
+
if (database.uri === null && !hasFields) return null;
|
|
7833
6766
|
return {
|
|
7834
|
-
engine,
|
|
6767
|
+
engine: database.engine,
|
|
7835
6768
|
...database.uri !== null && { uri: writableUri(database.uri, index) },
|
|
7836
6769
|
...database.uri === null && { fields: database.fields },
|
|
7837
6770
|
...database.name !== null && { name: database.name }
|
|
@@ -7856,6 +6789,122 @@ async function writeAnswersFile(path, answers) {
|
|
|
7856
6789
|
await promises.writeFile(path, JSON.stringify(answers, null, 2) + "\n", { mode: FILE_MODE });
|
|
7857
6790
|
}
|
|
7858
6791
|
//#endregion
|
|
6792
|
+
//#region src/wizard/answers.ts
|
|
6793
|
+
function isOneOf(answer, kinds) {
|
|
6794
|
+
return kinds.some((kind) => kind === answer.kind);
|
|
6795
|
+
}
|
|
6796
|
+
function rejected(key, prompt, reason) {
|
|
6797
|
+
return new ConfigError(`answers file: "${key}" for "${prompt.message}" ${reason}`);
|
|
6798
|
+
}
|
|
6799
|
+
function expectKind(taken, prompt, kinds) {
|
|
6800
|
+
if (taken.answer.kind === "cancel") throw new AbortError();
|
|
6801
|
+
if (isOneOf(taken.answer, kinds)) return taken.answer;
|
|
6802
|
+
throw rejected(taken.key, prompt, `is a ${taken.answer.kind} answer; expected ${kinds.join(" or ")}`);
|
|
6803
|
+
}
|
|
6804
|
+
function choiceLabels(choices) {
|
|
6805
|
+
return choices.map((choice) => String(choice.value)).join(", ");
|
|
6806
|
+
}
|
|
6807
|
+
function matchChoice(key, prompt, value) {
|
|
6808
|
+
const match = prompt.choices.find((choice) => choice.value === value);
|
|
6809
|
+
if (match === void 0) throw rejected(key, prompt, `is ${String(value)}, not one of the choices ${choiceLabels(prompt.choices)}`);
|
|
6810
|
+
return match.value;
|
|
6811
|
+
}
|
|
6812
|
+
function textValue(taken, prompt) {
|
|
6813
|
+
const answer = expectKind(taken, prompt, ["text"]);
|
|
6814
|
+
const value = answer.value.length === 0 && prompt.defaultValue !== void 0 ? prompt.defaultValue : answer.value;
|
|
6815
|
+
const problem = prompt.validate?.(value);
|
|
6816
|
+
if (problem !== void 0) throw rejected(taken.key, prompt, `is "${value}", which is rejected: ${problem}`);
|
|
6817
|
+
return value;
|
|
6818
|
+
}
|
|
6819
|
+
function passwordValue(taken, prompt) {
|
|
6820
|
+
const answer = expectKind(taken, prompt, ["password", "text"]);
|
|
6821
|
+
const problem = prompt.validate?.(answer.value);
|
|
6822
|
+
if (problem !== void 0) throw rejected(taken.key, prompt, `is rejected: ${problem}`);
|
|
6823
|
+
return answer.value;
|
|
6824
|
+
}
|
|
6825
|
+
function isBackAnswer(answer) {
|
|
6826
|
+
return answer.kind === "back";
|
|
6827
|
+
}
|
|
6828
|
+
function answeringPrompts(base, answers, options = {}) {
|
|
6829
|
+
function lookup(prompt) {
|
|
6830
|
+
if (prompt.key === void 0) return null;
|
|
6831
|
+
const answer = answers.get(prompt.key)?.shift();
|
|
6832
|
+
return answer === void 0 ? null : {
|
|
6833
|
+
key: prompt.key,
|
|
6834
|
+
answer
|
|
6835
|
+
};
|
|
6836
|
+
}
|
|
6837
|
+
function refuseUnanswered(prompt) {
|
|
6838
|
+
const refusal = options.unanswered?.(prompt) ?? null;
|
|
6839
|
+
if (refusal !== null) throw refusal;
|
|
6840
|
+
}
|
|
6841
|
+
function take(prompt) {
|
|
6842
|
+
const taken = lookup(prompt);
|
|
6843
|
+
if (taken === null) refuseUnanswered(prompt);
|
|
6844
|
+
return taken;
|
|
6845
|
+
}
|
|
6846
|
+
function takeText(prompt) {
|
|
6847
|
+
const taken = lookup(prompt);
|
|
6848
|
+
if (taken !== null) return taken;
|
|
6849
|
+
if (prompt.defaultValue !== void 0 && options.unanswered !== void 0) return {
|
|
6850
|
+
key: prompt.key ?? prompt.message,
|
|
6851
|
+
answer: {
|
|
6852
|
+
kind: "text",
|
|
6853
|
+
value: ""
|
|
6854
|
+
}
|
|
6855
|
+
};
|
|
6856
|
+
refuseUnanswered(prompt);
|
|
6857
|
+
return null;
|
|
6858
|
+
}
|
|
6859
|
+
function selected(taken, prompt) {
|
|
6860
|
+
const answer = expectKind(taken, prompt, ["select", "text"]);
|
|
6861
|
+
return matchChoice(taken.key, prompt, answer.value);
|
|
6862
|
+
}
|
|
6863
|
+
function multiselected(taken, prompt) {
|
|
6864
|
+
return expectKind(taken, prompt, ["multiselect"]).values.map((value) => matchChoice(taken.key, prompt, value));
|
|
6865
|
+
}
|
|
6866
|
+
return {
|
|
6867
|
+
async text(opts) {
|
|
6868
|
+
const taken = takeText(opts);
|
|
6869
|
+
return taken === null ? base.text(opts) : textValue(taken, opts);
|
|
6870
|
+
},
|
|
6871
|
+
async textOrBack(opts) {
|
|
6872
|
+
const taken = takeText(opts);
|
|
6873
|
+
if (taken === null) return base.textOrBack(opts);
|
|
6874
|
+
return isBackAnswer(taken.answer) ? BACK : textValue(taken, opts);
|
|
6875
|
+
},
|
|
6876
|
+
async password(opts) {
|
|
6877
|
+
const taken = take(opts);
|
|
6878
|
+
return taken === null ? base.password(opts) : passwordValue(taken, opts);
|
|
6879
|
+
},
|
|
6880
|
+
async confirm(opts) {
|
|
6881
|
+
const taken = lookup(opts);
|
|
6882
|
+
if (taken !== null) return expectKind(taken, opts, ["confirm"]).value;
|
|
6883
|
+
if (options.confirmAll === true) return true;
|
|
6884
|
+
refuseUnanswered(opts);
|
|
6885
|
+
return base.confirm(opts);
|
|
6886
|
+
},
|
|
6887
|
+
async select(opts) {
|
|
6888
|
+
const taken = take(opts);
|
|
6889
|
+
return taken === null ? base.select(opts) : selected(taken, opts);
|
|
6890
|
+
},
|
|
6891
|
+
async selectOrBack(opts) {
|
|
6892
|
+
const taken = take(opts);
|
|
6893
|
+
if (taken === null) return base.selectOrBack(opts);
|
|
6894
|
+
return isBackAnswer(taken.answer) ? BACK : selected(taken, opts);
|
|
6895
|
+
},
|
|
6896
|
+
async multiselect(opts) {
|
|
6897
|
+
const taken = take(opts);
|
|
6898
|
+
return taken === null ? base.multiselect(opts) : multiselected(taken, opts);
|
|
6899
|
+
},
|
|
6900
|
+
async multiselectOrBack(opts) {
|
|
6901
|
+
const taken = take(opts);
|
|
6902
|
+
if (taken === null) return base.multiselectOrBack(opts);
|
|
6903
|
+
return isBackAnswer(taken.answer) ? BACK : multiselected(taken, opts);
|
|
6904
|
+
}
|
|
6905
|
+
};
|
|
6906
|
+
}
|
|
6907
|
+
//#endregion
|
|
7859
6908
|
//#region src/wizard/record.ts
|
|
7860
6909
|
function recordingPrompts(base) {
|
|
7861
6910
|
const records = [];
|
|
@@ -7932,11 +6981,9 @@ async function combinedAnswers(options) {
|
|
|
7932
6981
|
return mergeInitAnswers(await loadInitAnswers(options.answersPath), fromFlags);
|
|
7933
6982
|
}
|
|
7934
6983
|
function promptsFor(answers, yes) {
|
|
7935
|
-
const terminal = stdinIsTty();
|
|
7936
6984
|
const answering = {
|
|
7937
|
-
...!
|
|
7938
|
-
...yes && { confirmAll: true }
|
|
7939
|
-
...(yes || !terminal) && { takeRecommended: true }
|
|
6985
|
+
...!stdinIsTty() && { unanswered: refusalWithoutTerminal },
|
|
6986
|
+
...yes && { confirmAll: true }
|
|
7940
6987
|
};
|
|
7941
6988
|
return answeringPrompts(clackPrompts(), keyedAnswers(answers), answering);
|
|
7942
6989
|
}
|
|
@@ -7980,7 +7027,7 @@ const init = defineRdeCommand({
|
|
|
7980
7027
|
},
|
|
7981
7028
|
admin: {
|
|
7982
7029
|
type: "string",
|
|
7983
|
-
description: "Admin credentials:
|
|
7030
|
+
description: "Admin credentials: default (admin@metabase.com / metabase123) or prompted"
|
|
7984
7031
|
},
|
|
7985
7032
|
"admin-email": {
|
|
7986
7033
|
type: "string",
|
|
@@ -8040,7 +7087,7 @@ const init = defineRdeCommand({
|
|
|
8040
7087
|
},
|
|
8041
7088
|
"mb-profile": {
|
|
8042
7089
|
type: "string",
|
|
8043
|
-
description: "mb profile for
|
|
7090
|
+
description: "mb profile for this instance when `default` already points elsewhere: default or rde"
|
|
8044
7091
|
},
|
|
8045
7092
|
yes: {
|
|
8046
7093
|
type: "boolean",
|
|
@@ -8089,7 +7136,7 @@ const init = defineRdeCommand({
|
|
|
8089
7136
|
prompts: recording?.prompts ?? answering
|
|
8090
7137
|
});
|
|
8091
7138
|
ctx.ui.intro("rde init");
|
|
8092
|
-
await runWizard(initSteps(stepOptionsFrom(answers
|
|
7139
|
+
await runWizard(initSteps(stepOptionsFrom(answers)), ctx, options);
|
|
8093
7140
|
if (recording !== null && printTo !== void 0) {
|
|
8094
7141
|
await writeAnswersFile(printTo, answersFromRecords(recording.records, answers));
|
|
8095
7142
|
ctx.ui.info(`Answers written to ${printTo}`);
|
|
@@ -8196,11 +7243,11 @@ function isNotServing(error) {
|
|
|
8196
7243
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
8197
7244
|
}
|
|
8198
7245
|
async function databasesOf(ctx, deps, health) {
|
|
8199
|
-
if (health !== "healthy" || ctx.
|
|
7246
|
+
if (health !== "healthy" || ctx.state.apiKey === null) return null;
|
|
8200
7247
|
try {
|
|
8201
|
-
return await listDatabases(ctx.state,
|
|
7248
|
+
return await listDatabases(ctx.state, { client: deps.client });
|
|
8202
7249
|
} catch (error) {
|
|
8203
|
-
if (isNotServing(error)
|
|
7250
|
+
if (isNotServing(error)) return null;
|
|
8204
7251
|
throw error;
|
|
8205
7252
|
}
|
|
8206
7253
|
}
|
|
@@ -8243,13 +7290,9 @@ async function collectStatus(ctx, deps) {
|
|
|
8243
7290
|
const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
|
|
8244
7291
|
const health = configured === null ? null : await healthOf(configured.url, deps);
|
|
8245
7292
|
const databases = health === null ? null : await databasesOf(ctx, deps, health);
|
|
8246
|
-
const admin = ctx.state.admin;
|
|
8247
7293
|
return {
|
|
8248
7294
|
instance: ctx.state.name,
|
|
8249
7295
|
url: configured?.url ?? null,
|
|
8250
|
-
mbProfile: ctx.state.mbProfile,
|
|
8251
|
-
adminEmail: admin === null ? null : admin.email,
|
|
8252
|
-
adminVariant: admin === null ? null : admin.kind,
|
|
8253
7296
|
runtime: configured === null || runtime === null ? null : {
|
|
8254
7297
|
kind: configured.runtime.kind,
|
|
8255
7298
|
installed: runtime.installed,
|
|
@@ -8303,8 +7346,6 @@ function databaseLines(report) {
|
|
|
8303
7346
|
function statusLines(report) {
|
|
8304
7347
|
return [
|
|
8305
7348
|
report.url === null ? `Instance ${report.instance}` : `Instance ${report.instance} — ${report.url}`,
|
|
8306
|
-
...report.adminVariant === null || report.adminEmail === null ? [] : [loginLine(report.adminVariant, report.adminEmail)],
|
|
8307
|
-
`Metabase CLI: ${mbCommand(report.mbProfile)} <command>`,
|
|
8308
7349
|
runtimeLine(report),
|
|
8309
7350
|
versionLine(report),
|
|
8310
7351
|
`Health: ${report.health ?? "unknown"}`,
|
|
@@ -8607,7 +7648,7 @@ async function planUninstall(options, deps) {
|
|
|
8607
7648
|
if (jar.removal !== null) removals.push(jar.removal);
|
|
8608
7649
|
if (jar.kept !== null) kept.push(jar.kept);
|
|
8609
7650
|
}
|
|
8610
|
-
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE,
|
|
7651
|
+
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, "rde"], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
|
|
8611
7652
|
removals.push(...profiles);
|
|
8612
7653
|
if (options.removeSkill) {
|
|
8613
7654
|
const skill = await findSkill(deps.runner);
|
|
@@ -8616,11 +7657,6 @@ async function planUninstall(options, deps) {
|
|
|
8616
7657
|
const dir = instanceDir(options.instance);
|
|
8617
7658
|
const dirPresent = await pathExists(dir);
|
|
8618
7659
|
if (!options.keepData) {
|
|
8619
|
-
const secrets = instanceSecretsPath(options.instance);
|
|
8620
|
-
if (await pathExists(secrets)) removals.push({
|
|
8621
|
-
label: `Stored credentials ${secrets} (the admin password and the API key)`,
|
|
8622
|
-
remove: () => promises.rm(secrets, { force: true })
|
|
8623
|
-
});
|
|
8624
7660
|
if (dirPresent) removals.push({
|
|
8625
7661
|
label: instanceDirLabel(dir, state),
|
|
8626
7662
|
remove: () => promises.rm(dir, {
|
|
@@ -9022,7 +8058,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
9022
8058
|
const main = defineCommand({
|
|
9023
8059
|
meta: {
|
|
9024
8060
|
name: "rde",
|
|
9025
|
-
version: "0.1.3
|
|
8061
|
+
version: "0.1.3",
|
|
9026
8062
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
9027
8063
|
},
|
|
9028
8064
|
subCommands: {
|
|
@@ -9030,7 +8066,6 @@ const main = defineCommand({
|
|
|
9030
8066
|
start,
|
|
9031
8067
|
stop,
|
|
9032
8068
|
status,
|
|
9033
|
-
credentials,
|
|
9034
8069
|
logs,
|
|
9035
8070
|
open,
|
|
9036
8071
|
db,
|