@metabase/rde 0.1.3 → 0.1.4
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 +83 -42
- package/dist/cli.mjs +1452 -487
- 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, join, resolve } from "node:path";
|
|
4
|
+
import { delimiter, dirname, isAbsolute, 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,6 +11,7 @@ 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";
|
|
14
15
|
//#region src/errors.ts
|
|
15
16
|
var AbortError = class extends Error {
|
|
16
17
|
constructor(message = "interrupted") {
|
|
@@ -56,6 +57,10 @@ var MetabaseApiError = class extends Error {
|
|
|
56
57
|
this.errors = failure.errors;
|
|
57
58
|
}
|
|
58
59
|
};
|
|
60
|
+
const UNAUTHORIZED_STATUS = 401;
|
|
61
|
+
function isRejectedCredential(error) {
|
|
62
|
+
return error instanceof MetabaseApiError && error.status === UNAUTHORIZED_STATUS;
|
|
63
|
+
}
|
|
59
64
|
var MetabaseInitializingError = class extends Error {
|
|
60
65
|
progress;
|
|
61
66
|
constructor(progress) {
|
|
@@ -117,6 +122,14 @@ function installInterruptHandler(exit) {
|
|
|
117
122
|
setTimeout(() => exit(130), FORCED_EXIT_GRACE_MS).unref();
|
|
118
123
|
});
|
|
119
124
|
}
|
|
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
|
+
}
|
|
120
133
|
const StepId = z.enum([
|
|
121
134
|
"preflight",
|
|
122
135
|
"install-mb",
|
|
@@ -157,14 +170,36 @@ const JarRuntimeState = z.object({
|
|
|
157
170
|
});
|
|
158
171
|
const RuntimeState = z.discriminatedUnion("kind", [DockerRuntimeState, JarRuntimeState]);
|
|
159
172
|
const RuntimeKind = z.enum(["docker", "jar"]);
|
|
160
|
-
const AdminCredentials = z.discriminatedUnion("kind", [
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
173
|
+
const AdminCredentials = z.discriminatedUnion("kind", [
|
|
174
|
+
z.object({
|
|
175
|
+
kind: z.literal("generated"),
|
|
176
|
+
email: z.string()
|
|
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({
|
|
165
188
|
email: z.string(),
|
|
166
189
|
password: z.string()
|
|
167
|
-
})
|
|
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
|
+
}
|
|
168
203
|
const LicenseState = z.discriminatedUnion("activated", [z.object({ activated: z.literal(false) }), z.object({
|
|
169
204
|
activated: z.literal(true),
|
|
170
205
|
features: z.array(z.string()),
|
|
@@ -202,13 +237,12 @@ const StepRecord = z.object({
|
|
|
202
237
|
completedAt: z.string()
|
|
203
238
|
});
|
|
204
239
|
const InstanceState = z.object({
|
|
205
|
-
schemaVersion: z.literal(
|
|
240
|
+
schemaVersion: z.literal(4),
|
|
206
241
|
name: z.string(),
|
|
207
242
|
runtime: RuntimeState.nullable(),
|
|
208
243
|
url: z.string().nullable(),
|
|
209
244
|
version: z.string().nullable(),
|
|
210
245
|
admin: AdminCredentials.nullable(),
|
|
211
|
-
apiKey: z.string().nullable(),
|
|
212
246
|
mbProfile: z.string(),
|
|
213
247
|
license: LicenseState,
|
|
214
248
|
databases: z.array(DatabaseRecord),
|
|
@@ -230,20 +264,26 @@ function requireConfiguredRuntime(state) {
|
|
|
230
264
|
}
|
|
231
265
|
const DEFAULT_INSTANCE_NAME = "default";
|
|
232
266
|
const DEFAULT_MB_PROFILE = "default";
|
|
233
|
-
const
|
|
267
|
+
const RDE_MB_PROFILE = "rde";
|
|
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
|
+
}
|
|
234
275
|
function mbCommand(profile) {
|
|
235
276
|
return profile === "default" ? "mb" : `mb --profile ${profile}`;
|
|
236
277
|
}
|
|
237
278
|
function emptyInstanceState(name) {
|
|
238
279
|
return {
|
|
239
|
-
schemaVersion:
|
|
280
|
+
schemaVersion: 4,
|
|
240
281
|
name,
|
|
241
282
|
runtime: null,
|
|
242
283
|
url: null,
|
|
243
284
|
version: null,
|
|
244
285
|
admin: null,
|
|
245
|
-
|
|
246
|
-
mbProfile: DEFAULT_MB_PROFILE,
|
|
286
|
+
mbProfile: mbProfileFor(name),
|
|
247
287
|
license: { activated: false },
|
|
248
288
|
databases: [],
|
|
249
289
|
sampleDatabase: null,
|
|
@@ -256,8 +296,22 @@ function emptyInstanceState(name) {
|
|
|
256
296
|
const Versioned = z.object({ schemaVersion: z.int() });
|
|
257
297
|
const V1 = 1;
|
|
258
298
|
const V2 = 2;
|
|
299
|
+
const V3 = 3;
|
|
259
300
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
260
301
|
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();
|
|
261
315
|
function liftV1(raw) {
|
|
262
316
|
return {
|
|
263
317
|
...StateV1.parse(raw),
|
|
@@ -268,27 +322,67 @@ function liftV1(raw) {
|
|
|
268
322
|
function liftV2(raw) {
|
|
269
323
|
return {
|
|
270
324
|
...StateV2.parse(raw),
|
|
271
|
-
schemaVersion:
|
|
325
|
+
schemaVersion: V3,
|
|
272
326
|
sampleDatabase: null
|
|
273
327
|
};
|
|
274
328
|
}
|
|
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
|
+
}
|
|
275
360
|
function migrateInstanceState(raw) {
|
|
276
361
|
const { schemaVersion } = Versioned.parse(raw);
|
|
277
362
|
switch (schemaVersion) {
|
|
278
363
|
case V1: return migrateInstanceState(liftV1(raw));
|
|
279
364
|
case V2: return migrateInstanceState(liftV2(raw));
|
|
280
|
-
case
|
|
281
|
-
|
|
365
|
+
case V3: return liftV3(raw);
|
|
366
|
+
case 4: return {
|
|
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)`);
|
|
282
371
|
}
|
|
283
372
|
}
|
|
284
373
|
//#endregion
|
|
285
374
|
//#region src/env.ts
|
|
286
375
|
const ENV_RDE_HOME = "RDE_HOME";
|
|
376
|
+
const ENV_RDE_SKILL_SOURCE = "RDE_SKILL_SOURCE";
|
|
287
377
|
function readEnv(name) {
|
|
288
378
|
return process.env[name];
|
|
289
379
|
}
|
|
290
380
|
const ENV_PATH = "PATH";
|
|
381
|
+
const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
|
|
291
382
|
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";
|
|
292
386
|
function environmentWith(overrides) {
|
|
293
387
|
return {
|
|
294
388
|
...process.env,
|
|
@@ -302,6 +396,7 @@ const INSTANCES_DIR = "instances";
|
|
|
302
396
|
const DATA_DIR = "data";
|
|
303
397
|
const BACKUPS_DIR = "backups";
|
|
304
398
|
const STATE_FILE = "state.json";
|
|
399
|
+
const SECRETS_FILE = "secrets.json";
|
|
305
400
|
const GLOBAL_CONFIG_FILE = "config.json";
|
|
306
401
|
const JARS_DIR = "jars";
|
|
307
402
|
const JAR_FILE$1 = "metabase.jar";
|
|
@@ -322,8 +417,17 @@ function instanceDataDir(name) {
|
|
|
322
417
|
function instanceBackupsDir(name) {
|
|
323
418
|
return join(instanceDir(name), BACKUPS_DIR);
|
|
324
419
|
}
|
|
420
|
+
function stateFileIn(dir) {
|
|
421
|
+
return join(dir, STATE_FILE);
|
|
422
|
+
}
|
|
423
|
+
function secretsFileIn(dir) {
|
|
424
|
+
return join(dir, SECRETS_FILE);
|
|
425
|
+
}
|
|
325
426
|
function instanceStatePath(name) {
|
|
326
|
-
return
|
|
427
|
+
return stateFileIn(instanceDir(name));
|
|
428
|
+
}
|
|
429
|
+
function instanceSecretsPath(name) {
|
|
430
|
+
return secretsFileIn(instanceDir(name));
|
|
327
431
|
}
|
|
328
432
|
function globalConfigPath() {
|
|
329
433
|
return join(rdeHome(), GLOBAL_CONFIG_FILE);
|
|
@@ -337,21 +441,21 @@ function jarReleaseDir(version) {
|
|
|
337
441
|
function releaseJarIn(jarsDirectory, version) {
|
|
338
442
|
return join(jarsDirectory, version, JAR_FILE$1);
|
|
339
443
|
}
|
|
340
|
-
|
|
341
|
-
//#region src/state/store.ts
|
|
342
|
-
const FILE_MODE$1 = 384;
|
|
343
|
-
const DIR_MODE = 448;
|
|
444
|
+
const PERMISSION_BITS = 511;
|
|
344
445
|
function isFileNotFound$1(error) {
|
|
345
446
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
346
447
|
}
|
|
347
|
-
async function
|
|
348
|
-
let raw;
|
|
448
|
+
async function readTextFile(path) {
|
|
349
449
|
try {
|
|
350
|
-
|
|
450
|
+
return await promises.readFile(path, "utf8");
|
|
351
451
|
} catch (error) {
|
|
352
452
|
if (isFileNotFound$1(error)) return null;
|
|
353
453
|
throw error;
|
|
354
454
|
}
|
|
455
|
+
}
|
|
456
|
+
async function readJsonFile(path) {
|
|
457
|
+
const raw = await readTextFile(path);
|
|
458
|
+
if (raw === null) return null;
|
|
355
459
|
try {
|
|
356
460
|
return JSON.parse(raw);
|
|
357
461
|
} catch (error) {
|
|
@@ -370,11 +474,11 @@ function parseAt(path, parse) {
|
|
|
370
474
|
async function writeJsonFile(path, value) {
|
|
371
475
|
await promises.mkdir(dirname(path), {
|
|
372
476
|
recursive: true,
|
|
373
|
-
mode:
|
|
477
|
+
mode: 448
|
|
374
478
|
});
|
|
375
479
|
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
376
|
-
await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode:
|
|
377
|
-
await promises.chmod(tmpPath,
|
|
480
|
+
await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
481
|
+
await promises.chmod(tmpPath, 384);
|
|
378
482
|
try {
|
|
379
483
|
await promises.rename(tmpPath, path);
|
|
380
484
|
} catch (error) {
|
|
@@ -386,7 +490,12 @@ async function readInstanceState(name) {
|
|
|
386
490
|
const path = instanceStatePath(name);
|
|
387
491
|
const raw = await readJsonFile(path);
|
|
388
492
|
if (raw === null) return null;
|
|
389
|
-
|
|
493
|
+
const migrated = parseAt(path, () => migrateInstanceState(raw));
|
|
494
|
+
if (migrated.movedSecrets !== null) {
|
|
495
|
+
await writeInstanceSecrets(name, migrated.movedSecrets);
|
|
496
|
+
await writeInstanceState(migrated.state);
|
|
497
|
+
}
|
|
498
|
+
return migrated.state;
|
|
390
499
|
}
|
|
391
500
|
async function writeInstanceState(state) {
|
|
392
501
|
await writeJsonFile(instanceStatePath(state.name), state);
|
|
@@ -396,6 +505,35 @@ async function requireInstanceState(name) {
|
|
|
396
505
|
if (state === null) throw new ConfigError(`no instance named ${name}; run \`rde init\` first`);
|
|
397
506
|
return state;
|
|
398
507
|
}
|
|
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
|
+
}
|
|
399
537
|
async function readGlobalConfig() {
|
|
400
538
|
const path = globalConfigPath();
|
|
401
539
|
const raw = await readJsonFile(path);
|
|
@@ -406,6 +544,120 @@ async function writeGlobalConfig(config) {
|
|
|
406
544
|
await writeJsonFile(globalConfigPath(), config);
|
|
407
545
|
}
|
|
408
546
|
//#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
|
|
409
661
|
//#region src/connections/parse-uri.ts
|
|
410
662
|
var UriParseError = class extends Error {
|
|
411
663
|
engine;
|
|
@@ -1072,6 +1324,18 @@ function tokenStatusResource(http) {
|
|
|
1072
1324
|
return { get };
|
|
1073
1325
|
}
|
|
1074
1326
|
//#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
|
|
1075
1339
|
//#region src/metabase/client.ts
|
|
1076
1340
|
const HEALTH_POLL_INTERVAL_MS = 2e3;
|
|
1077
1341
|
function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
@@ -1092,7 +1356,8 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
|
1092
1356
|
setting: settingResource(http),
|
|
1093
1357
|
tokenStatus: tokenStatusResource(http),
|
|
1094
1358
|
database: databaseResource(http),
|
|
1095
|
-
remoteSync: remoteSyncResource(http)
|
|
1359
|
+
remoteSync: remoteSyncResource(http),
|
|
1360
|
+
user: userResource(http)
|
|
1096
1361
|
};
|
|
1097
1362
|
}
|
|
1098
1363
|
function isNotReadyYet(error) {
|
|
@@ -1497,6 +1762,9 @@ function settledOrBack(value) {
|
|
|
1497
1762
|
if (isBack(value)) return BACK;
|
|
1498
1763
|
return settled(value);
|
|
1499
1764
|
}
|
|
1765
|
+
function preselected(opts) {
|
|
1766
|
+
return opts.initialValue ?? opts.recommended;
|
|
1767
|
+
}
|
|
1500
1768
|
function withBackChoice(choices) {
|
|
1501
1769
|
return [...choices, {
|
|
1502
1770
|
value: BACK,
|
|
@@ -1550,10 +1818,11 @@ function clackPrompts() {
|
|
|
1550
1818
|
},
|
|
1551
1819
|
async select(opts) {
|
|
1552
1820
|
requireTty(opts.message);
|
|
1821
|
+
const initialValue = preselected(opts);
|
|
1553
1822
|
return settled(await select({
|
|
1554
1823
|
message: opts.message,
|
|
1555
1824
|
options: opts.choices,
|
|
1556
|
-
...
|
|
1825
|
+
...initialValue !== void 0 && { initialValue }
|
|
1557
1826
|
}));
|
|
1558
1827
|
},
|
|
1559
1828
|
async selectOrBack(opts) {
|
|
@@ -1776,6 +2045,8 @@ function databaseListingLines(listing) {
|
|
|
1776
2045
|
//#endregion
|
|
1777
2046
|
//#region src/steps/databases/flow.ts
|
|
1778
2047
|
const DATABASE_KEYS = {
|
|
2048
|
+
connection: "databases.connection",
|
|
2049
|
+
detectedEngine: "databases.detected-engine",
|
|
1779
2050
|
engine: "databases.engine",
|
|
1780
2051
|
skip: "databases.skip",
|
|
1781
2052
|
mode: "databases.mode",
|
|
@@ -1790,6 +2061,11 @@ const FINISH = "finish";
|
|
|
1790
2061
|
const RETRY = "try-again";
|
|
1791
2062
|
const BAD_REQUEST_STATUS$1 = 400;
|
|
1792
2063
|
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
|
+
};
|
|
1793
2069
|
const FRESH$1 = {
|
|
1794
2070
|
engine: null,
|
|
1795
2071
|
uri: null,
|
|
@@ -1830,6 +2106,66 @@ async function confirmSkip(run) {
|
|
|
1830
2106
|
initialValue: false
|
|
1831
2107
|
});
|
|
1832
2108
|
}
|
|
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
|
+
}
|
|
1833
2169
|
function pickEngineScreen(run) {
|
|
1834
2170
|
return async (model) => {
|
|
1835
2171
|
const total = run.deps.existingCount + run.created.length;
|
|
@@ -2191,19 +2527,20 @@ async function runDatabaseFlow(deps) {
|
|
|
2191
2527
|
created: []
|
|
2192
2528
|
};
|
|
2193
2529
|
await runFlow({
|
|
2530
|
+
"connection-string": connectionStringScreen(run),
|
|
2194
2531
|
"pick-engine": pickEngineScreen(run),
|
|
2195
2532
|
"paste-uri": pasteUriScreen(run),
|
|
2196
2533
|
review: reviewScreen(run),
|
|
2197
2534
|
test: testScreen(run),
|
|
2198
2535
|
create: createScreen(run),
|
|
2199
2536
|
another: anotherScreen(run)
|
|
2200
|
-
},
|
|
2537
|
+
}, START_SCREEN[deps.start], FRESH$1);
|
|
2201
2538
|
return { created: run.created };
|
|
2202
2539
|
}
|
|
2203
2540
|
//#endregion
|
|
2204
2541
|
//#region src/steps/databases.ts
|
|
2205
2542
|
const STEP_ID$9 = "databases";
|
|
2206
|
-
const
|
|
2543
|
+
const SAMPLE_STANDS_IN = { request: "sample-stands-in" };
|
|
2207
2544
|
function hostDatabasesDeps(signal) {
|
|
2208
2545
|
return {
|
|
2209
2546
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
@@ -2214,9 +2551,9 @@ function hostDatabasesDeps(signal) {
|
|
|
2214
2551
|
function isNotServingYet$1(error) {
|
|
2215
2552
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
2216
2553
|
}
|
|
2217
|
-
function requireApiKey$2(state) {
|
|
2218
|
-
if (
|
|
2219
|
-
return
|
|
2554
|
+
function requireApiKey$2(state, apiKey) {
|
|
2555
|
+
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
2556
|
+
return apiKey;
|
|
2220
2557
|
}
|
|
2221
2558
|
function hostAliasOf(runtime) {
|
|
2222
2559
|
switch (runtime.kind) {
|
|
@@ -2232,15 +2569,15 @@ function toRecord$1(database, createdAt) {
|
|
|
2232
2569
|
createdAt
|
|
2233
2570
|
};
|
|
2234
2571
|
}
|
|
2235
|
-
function authenticatedClient(state, deps) {
|
|
2572
|
+
function authenticatedClient(state, apiKey, deps) {
|
|
2236
2573
|
const configured = requireConfiguredRuntime(state);
|
|
2237
2574
|
return deps.client(configured.url, {
|
|
2238
2575
|
kind: "api-key",
|
|
2239
|
-
key: requireApiKey$2(state)
|
|
2576
|
+
key: requireApiKey$2(state, apiKey)
|
|
2240
2577
|
});
|
|
2241
2578
|
}
|
|
2242
|
-
async function listDatabases(state, deps) {
|
|
2243
|
-
const listed = await authenticatedClient(state, deps).database.list();
|
|
2579
|
+
async function listDatabases(state, apiKey, deps) {
|
|
2580
|
+
const listed = await authenticatedClient(state, apiKey, deps).database.list();
|
|
2244
2581
|
const records = new Map(state.databases.map((record) => [record.id, record]));
|
|
2245
2582
|
const onServer = listed.data.map((database) => ({
|
|
2246
2583
|
id: database.id,
|
|
@@ -2259,13 +2596,14 @@ async function listDatabases(state, deps) {
|
|
|
2259
2596
|
}));
|
|
2260
2597
|
return [...onServer, ...missing];
|
|
2261
2598
|
}
|
|
2262
|
-
async function
|
|
2599
|
+
async function connectDatabases(ctx, deps, start) {
|
|
2263
2600
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
2264
|
-
const client = authenticatedClient(ctx.state, deps);
|
|
2601
|
+
const client = authenticatedClient(ctx.state, ctx.secrets.apiKey, deps);
|
|
2265
2602
|
const properties = await client.session.properties();
|
|
2266
2603
|
const existing = await client.database.list();
|
|
2267
2604
|
if (existing.data.length > 0) ctx.ui.note(existingDatabaseLines(existing.data).join("\n"), "Connected databases");
|
|
2268
2605
|
const result = await runDatabaseFlow({
|
|
2606
|
+
start,
|
|
2269
2607
|
prompts: ctx.prompts,
|
|
2270
2608
|
ui: ctx.ui,
|
|
2271
2609
|
catalog: engineCatalog(properties.engines),
|
|
@@ -2281,7 +2619,17 @@ async function addDatabases(ctx, deps) {
|
|
|
2281
2619
|
});
|
|
2282
2620
|
}
|
|
2283
2621
|
});
|
|
2284
|
-
return
|
|
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";
|
|
2285
2633
|
}
|
|
2286
2634
|
function hostDeps$12(ctx) {
|
|
2287
2635
|
return hostDatabasesDeps(ctx.signal);
|
|
@@ -2289,6 +2637,9 @@ function hostDeps$12(ctx) {
|
|
|
2289
2637
|
function sampleDatabaseId(state) {
|
|
2290
2638
|
return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
|
|
2291
2639
|
}
|
|
2640
|
+
function skippedBefore(state) {
|
|
2641
|
+
return state.steps[STEP_ID$9]?.status === "skipped";
|
|
2642
|
+
}
|
|
2292
2643
|
function ownDatabaseIds(state) {
|
|
2293
2644
|
const sampleId = sampleDatabaseId(state);
|
|
2294
2645
|
return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
|
|
@@ -2299,21 +2650,28 @@ function databasesStep(options, deps = hostDeps$12) {
|
|
|
2299
2650
|
title: "Connect your databases",
|
|
2300
2651
|
async detect(ctx) {
|
|
2301
2652
|
const configured = configuredRuntime(ctx.state);
|
|
2302
|
-
if (configured === null || ctx.
|
|
2653
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
2303
2654
|
const own = ownDatabaseIds(ctx.state);
|
|
2304
|
-
|
|
2655
|
+
const sampleId = sampleDatabaseId(ctx.state);
|
|
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;
|
|
2305
2662
|
const client = deps(ctx).client(configured.url, {
|
|
2306
2663
|
kind: "api-key",
|
|
2307
|
-
key: ctx.
|
|
2664
|
+
key: ctx.secrets.apiKey
|
|
2308
2665
|
});
|
|
2309
2666
|
try {
|
|
2310
|
-
return (await client.database.list()).data.some((database) =>
|
|
2667
|
+
return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
|
|
2311
2668
|
} catch (error) {
|
|
2312
|
-
if (isNotServingYet$1(error)) return "todo";
|
|
2669
|
+
if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
|
|
2313
2670
|
throw error;
|
|
2314
2671
|
}
|
|
2315
2672
|
},
|
|
2316
2673
|
run(ctx) {
|
|
2674
|
+
if (options.request === "ask-once") return askForOwnDatabase(ctx, deps(ctx));
|
|
2317
2675
|
return addDatabases(ctx, deps(ctx));
|
|
2318
2676
|
}
|
|
2319
2677
|
};
|
|
@@ -2391,6 +2749,11 @@ async function createWizardContext(instanceName, options) {
|
|
|
2391
2749
|
await writeInstanceState(next);
|
|
2392
2750
|
ctx.state = next;
|
|
2393
2751
|
},
|
|
2752
|
+
secrets: await readInstanceSecrets(instanceName) ?? emptyInstanceSecrets(),
|
|
2753
|
+
saveSecrets: async (next) => {
|
|
2754
|
+
await writeInstanceSecrets(instanceName, next);
|
|
2755
|
+
ctx.secrets = next;
|
|
2756
|
+
},
|
|
2394
2757
|
prompts: options.prompts ?? clackPrompts(),
|
|
2395
2758
|
ui: clackUi(),
|
|
2396
2759
|
signal: options.signal,
|
|
@@ -2399,53 +2762,6 @@ async function createWizardContext(instanceName, options) {
|
|
|
2399
2762
|
return ctx;
|
|
2400
2763
|
}
|
|
2401
2764
|
//#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
|
|
2449
2765
|
//#region src/commands/db/add.ts
|
|
2450
2766
|
const dbAdd = defineRdeCommand({
|
|
2451
2767
|
meta: {
|
|
@@ -2455,19 +2771,10 @@ const dbAdd = defineRdeCommand({
|
|
|
2455
2771
|
async run() {
|
|
2456
2772
|
const name = await defaultInstanceName();
|
|
2457
2773
|
await requireInstanceState(name);
|
|
2458
|
-
|
|
2459
|
-
await databasesStep(ASK_DATABASES).run(ctx);
|
|
2774
|
+
await addDatabases(await createWizardContext(name, { signal: interruptSignal }), hostDatabasesDeps(interruptSignal));
|
|
2460
2775
|
}
|
|
2461
2776
|
});
|
|
2462
2777
|
//#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
|
|
2471
2778
|
//#region src/commands/db/list.ts
|
|
2472
2779
|
const dbList = defineRdeCommand({
|
|
2473
2780
|
meta: {
|
|
@@ -2480,7 +2787,10 @@ const dbList = defineRdeCommand({
|
|
|
2480
2787
|
default: false
|
|
2481
2788
|
} },
|
|
2482
2789
|
async run(args) {
|
|
2483
|
-
const
|
|
2790
|
+
const name = await defaultInstanceName();
|
|
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));
|
|
2484
2794
|
if (args.json) {
|
|
2485
2795
|
printJson(listing);
|
|
2486
2796
|
return;
|
|
@@ -2809,15 +3119,20 @@ async function isExecutable(path) {
|
|
|
2809
3119
|
return false;
|
|
2810
3120
|
}
|
|
2811
3121
|
}
|
|
2812
|
-
async function
|
|
3122
|
+
async function whichAll(name) {
|
|
2813
3123
|
const searchPath = readEnv(ENV_PATH);
|
|
2814
|
-
if (searchPath === void 0) return
|
|
2815
|
-
|
|
3124
|
+
if (searchPath === void 0) return [];
|
|
3125
|
+
const found = [];
|
|
3126
|
+
for (const dir of new Set(searchPath.split(delimiter))) {
|
|
2816
3127
|
if (dir.length === 0) continue;
|
|
2817
3128
|
const candidate = join(dir, name);
|
|
2818
|
-
if (await isExecutable(candidate))
|
|
3129
|
+
if (await isExecutable(candidate)) found.push(candidate);
|
|
2819
3130
|
}
|
|
2820
|
-
return
|
|
3131
|
+
return found;
|
|
3132
|
+
}
|
|
3133
|
+
async function whichCommand(name) {
|
|
3134
|
+
const [first] = await whichAll(name);
|
|
3135
|
+
return first ?? null;
|
|
2821
3136
|
}
|
|
2822
3137
|
//#endregion
|
|
2823
3138
|
//#region src/tools/java.ts
|
|
@@ -3623,7 +3938,7 @@ async function detectAgents(probe) {
|
|
|
3623
3938
|
//#endregion
|
|
3624
3939
|
//#region src/tools/mb.ts
|
|
3625
3940
|
const MB = "mb";
|
|
3626
|
-
const MB_INSTALL_SPEC = `@metabase/cli@alpha-transform-
|
|
3941
|
+
const MB_INSTALL_SPEC = `@metabase/cli@alpha-fix-transform-test-ee-path`;
|
|
3627
3942
|
const GLOBAL_INSTALL = {
|
|
3628
3943
|
npm: {
|
|
3629
3944
|
command: "npm",
|
|
@@ -3661,44 +3976,117 @@ const GLOBAL_INSTALL = {
|
|
|
3661
3976
|
function mbInstallCommand(packageManager) {
|
|
3662
3977
|
return GLOBAL_INSTALL[packageManager];
|
|
3663
3978
|
}
|
|
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
|
+
}
|
|
3664
3984
|
const GLOBAL_MARKERS = [
|
|
3665
3985
|
{
|
|
3666
3986
|
marker: "/.bun/install/global/node_modules/",
|
|
3667
|
-
|
|
3987
|
+
owner: "bun"
|
|
3668
3988
|
},
|
|
3669
3989
|
{
|
|
3670
3990
|
marker: "/Library/pnpm/global/",
|
|
3671
|
-
|
|
3991
|
+
owner: "pnpm"
|
|
3672
3992
|
},
|
|
3673
3993
|
{
|
|
3674
3994
|
marker: "/.local/share/pnpm/global/",
|
|
3675
|
-
|
|
3995
|
+
owner: "pnpm"
|
|
3676
3996
|
},
|
|
3677
3997
|
{
|
|
3678
3998
|
marker: "/share/pnpm/global/",
|
|
3679
|
-
|
|
3999
|
+
owner: "pnpm"
|
|
3680
4000
|
},
|
|
3681
4001
|
{
|
|
3682
4002
|
marker: "/pnpm-global/node_modules/",
|
|
3683
|
-
|
|
4003
|
+
owner: "pnpm"
|
|
3684
4004
|
},
|
|
3685
4005
|
{
|
|
3686
4006
|
marker: "/.config/yarn/global/node_modules/",
|
|
3687
|
-
|
|
4007
|
+
owner: "yarn"
|
|
3688
4008
|
},
|
|
3689
4009
|
{
|
|
3690
4010
|
marker: "/lib/node_modules/",
|
|
3691
|
-
|
|
4011
|
+
owner: "npm"
|
|
3692
4012
|
},
|
|
3693
4013
|
{
|
|
3694
4014
|
marker: "/.npm-global/node_modules/",
|
|
3695
|
-
|
|
4015
|
+
owner: "npm"
|
|
3696
4016
|
}
|
|
3697
4017
|
];
|
|
3698
4018
|
function packageManagerFor(scriptPath) {
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
4019
|
+
return ownerByMarker(scriptPath, GLOBAL_MARKERS) ?? "npm";
|
|
4020
|
+
}
|
|
4021
|
+
const MB_OWNER_NAMES = {
|
|
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
|
+
};
|
|
3702
4090
|
}
|
|
3703
4091
|
const ProbedUser = z.object({
|
|
3704
4092
|
id: z.int(),
|
|
@@ -3775,16 +4163,16 @@ function mbTool(runner) {
|
|
|
3775
4163
|
if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
|
|
3776
4164
|
return parseJsonOutput(MB, argv, result.stdout, schema);
|
|
3777
4165
|
}
|
|
3778
|
-
async function version() {
|
|
4166
|
+
async function version(command = MB) {
|
|
3779
4167
|
const args = ["--version"];
|
|
3780
4168
|
let result;
|
|
3781
4169
|
try {
|
|
3782
|
-
result = await runner.run(
|
|
4170
|
+
result = await runner.run(command, args);
|
|
3783
4171
|
} catch (error) {
|
|
3784
4172
|
if (error instanceof ProcessNotFoundError) return null;
|
|
3785
4173
|
throw error;
|
|
3786
4174
|
}
|
|
3787
|
-
if (result.exitCode !== 0) throw toolFailure(
|
|
4175
|
+
if (result.exitCode !== 0) throw toolFailure(command, args, result);
|
|
3788
4176
|
return result.stdout.trim();
|
|
3789
4177
|
}
|
|
3790
4178
|
async function authLogin(options) {
|
|
@@ -3870,65 +4258,14 @@ function mbTool(runner) {
|
|
|
3870
4258
|
};
|
|
3871
4259
|
}
|
|
3872
4260
|
//#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
|
|
3928
4261
|
//#region src/tools/skills-cli.ts
|
|
3929
4262
|
const NPX = "npx";
|
|
3930
4263
|
const SKILLS_PACKAGE = "skills@latest";
|
|
3931
|
-
const
|
|
4264
|
+
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-skill";
|
|
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
|
+
}
|
|
3932
4269
|
const InstalledSkill = z.object({
|
|
3933
4270
|
name: z.string(),
|
|
3934
4271
|
path: z.string(),
|
|
@@ -3937,6 +4274,49 @@ const InstalledSkill = z.object({
|
|
|
3937
4274
|
source: z.string().nullable()
|
|
3938
4275
|
}).loose();
|
|
3939
4276
|
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
|
+
}
|
|
3940
4320
|
function skillsCli(runner) {
|
|
3941
4321
|
async function add(options) {
|
|
3942
4322
|
const argv = [
|
|
@@ -3988,13 +4368,83 @@ function skillsCli(runner) {
|
|
|
3988
4368
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
3989
4369
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
3990
4370
|
}
|
|
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
|
+
}
|
|
3991
4385
|
return {
|
|
3992
4386
|
add,
|
|
3993
4387
|
remove,
|
|
3994
|
-
list
|
|
4388
|
+
list,
|
|
4389
|
+
listProject
|
|
3995
4390
|
};
|
|
3996
4391
|
}
|
|
3997
4392
|
//#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
|
|
3998
4448
|
//#region src/steps/agents-skills.ts
|
|
3999
4449
|
const STEP_ID$8 = "agents-skills";
|
|
4000
4450
|
const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
@@ -4002,7 +4452,8 @@ const DETECT_AGENTS = { agents: null };
|
|
|
4002
4452
|
function hostDeps$11(ctx) {
|
|
4003
4453
|
return {
|
|
4004
4454
|
runner: processRunner(ctx.signal),
|
|
4005
|
-
probe: hostAgentProbe()
|
|
4455
|
+
probe: hostAgentProbe(),
|
|
4456
|
+
skillSource: rdeSkillSource()
|
|
4006
4457
|
};
|
|
4007
4458
|
}
|
|
4008
4459
|
function detectedOnly(agents) {
|
|
@@ -4062,9 +4513,10 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
|
4062
4513
|
ctx.ui.info(skipLine(all, selection.source));
|
|
4063
4514
|
return "skipped";
|
|
4064
4515
|
}
|
|
4065
|
-
|
|
4516
|
+
const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-skill" ? "" : ` from ${resolved.skillSource}`;
|
|
4517
|
+
ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
|
|
4066
4518
|
await skillsCli(resolved.runner).add({
|
|
4067
|
-
source:
|
|
4519
|
+
source: resolved.skillSource,
|
|
4068
4520
|
skill: "rde",
|
|
4069
4521
|
agents: selection.agents.map((agent) => agent.skillsCliName)
|
|
4070
4522
|
});
|
|
@@ -4090,7 +4542,8 @@ function rdeInstalledWith() {
|
|
|
4090
4542
|
function hostDeps$10(ctx) {
|
|
4091
4543
|
return {
|
|
4092
4544
|
runner: processRunner(ctx.signal),
|
|
4093
|
-
installedWith: rdeInstalledWith()
|
|
4545
|
+
installedWith: rdeInstalledWith(),
|
|
4546
|
+
locate: locateMbOnPath
|
|
4094
4547
|
};
|
|
4095
4548
|
}
|
|
4096
4549
|
async function inspectMb(runner) {
|
|
@@ -4107,14 +4560,100 @@ async function inspectMb(runner) {
|
|
|
4107
4560
|
function displayCommand(install) {
|
|
4108
4561
|
return [install.command, ...install.args].join(" ");
|
|
4109
4562
|
}
|
|
4110
|
-
function
|
|
4111
|
-
|
|
4112
|
-
|
|
4563
|
+
async function versionAt(runner, path) {
|
|
4564
|
+
try {
|
|
4565
|
+
const raw = await mbTool(runner).version(path);
|
|
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({
|
|
4113
4644
|
tool: install.command,
|
|
4114
4645
|
argv: install.args,
|
|
4115
4646
|
exitCode: 0,
|
|
4116
|
-
stderrTail
|
|
4647
|
+
stderrTail
|
|
4117
4648
|
});
|
|
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(" "));
|
|
4118
4657
|
}
|
|
4119
4658
|
function installMbStep(deps = hostDeps$10) {
|
|
4120
4659
|
return {
|
|
@@ -4142,7 +4681,7 @@ function installMbStep(deps = hostDeps$10) {
|
|
|
4142
4681
|
stderrTail: ""
|
|
4143
4682
|
});
|
|
4144
4683
|
const after = await inspectMb(resolved.runner);
|
|
4145
|
-
if (!after.installed || !after.satisfies) throw stillWrong(install, after);
|
|
4684
|
+
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
4146
4685
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
4147
4686
|
return "completed";
|
|
4148
4687
|
}
|
|
@@ -4463,7 +5002,15 @@ function hostDoctorDeps(instanceName, signal) {
|
|
|
4463
5002
|
runtime,
|
|
4464
5003
|
ports: hostPortProbe,
|
|
4465
5004
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
4466
|
-
probe: hostAgentProbe()
|
|
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
|
+
}
|
|
4467
5014
|
};
|
|
4468
5015
|
}
|
|
4469
5016
|
const NOT_FOUND_STATUS$2 = 404;
|
|
@@ -4476,7 +5023,17 @@ function ok(id, label, detail) {
|
|
|
4476
5023
|
status: "ok",
|
|
4477
5024
|
detail,
|
|
4478
5025
|
hint: null,
|
|
4479
|
-
|
|
5026
|
+
step: null
|
|
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
|
|
4480
5037
|
};
|
|
4481
5038
|
}
|
|
4482
5039
|
function skipped(id, label, detail) {
|
|
@@ -4486,7 +5043,7 @@ function skipped(id, label, detail) {
|
|
|
4486
5043
|
status: "skipped",
|
|
4487
5044
|
detail,
|
|
4488
5045
|
hint: null,
|
|
4489
|
-
|
|
5046
|
+
step: null
|
|
4490
5047
|
};
|
|
4491
5048
|
}
|
|
4492
5049
|
function failed(id, label, failure) {
|
|
@@ -4507,7 +5064,7 @@ function machineRow(check, needed) {
|
|
|
4507
5064
|
return failed(check.id, check.id, {
|
|
4508
5065
|
detail: check.label,
|
|
4509
5066
|
hint: check.hint,
|
|
4510
|
-
|
|
5067
|
+
step: "preflight"
|
|
4511
5068
|
});
|
|
4512
5069
|
}
|
|
4513
5070
|
async function machineRows(deps, runtime) {
|
|
@@ -4519,19 +5076,48 @@ function installHint(installedWith) {
|
|
|
4519
5076
|
const install = mbInstallCommand(installedWith);
|
|
4520
5077
|
return `\`${[install.command, ...install.args].join(" ")}\``;
|
|
4521
5078
|
}
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
if (
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
5079
|
+
const MB_LABEL = "mb";
|
|
5080
|
+
function mbUnusable(diagnosis) {
|
|
5081
|
+
if (diagnosis.kind === "missing") return "mb is not installed";
|
|
5082
|
+
return runningMbSuffices(diagnosis) ? null : "the mb that runs is too old";
|
|
5083
|
+
}
|
|
5084
|
+
function mbRow(diagnosis, lines, deps) {
|
|
5085
|
+
switch (diagnosis.kind) {
|
|
5086
|
+
case "missing": return failed("mb", MB_LABEL, {
|
|
5087
|
+
detail: "not installed",
|
|
5088
|
+
hint: installHint(deps.installedWith),
|
|
5089
|
+
step: "install-mb"
|
|
5090
|
+
});
|
|
5091
|
+
case "old": {
|
|
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
|
+
};
|
|
4535
5121
|
}
|
|
4536
5122
|
function publishesOnLoopback(container, port) {
|
|
4537
5123
|
return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
@@ -4549,7 +5135,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4549
5135
|
row: failed("container", label, {
|
|
4550
5136
|
detail: `${runtime.containerName} does not exist`,
|
|
4551
5137
|
hint: "`rde init` recreates it from the saved choices",
|
|
4552
|
-
|
|
5138
|
+
step: "runtime"
|
|
4553
5139
|
}),
|
|
4554
5140
|
running: false,
|
|
4555
5141
|
holder: CONTAINER
|
|
@@ -4562,7 +5148,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4562
5148
|
row: failed("container", label, {
|
|
4563
5149
|
detail: `${runtime.containerName} runs ${inspection.image}, the state says ${runtime.image}`,
|
|
4564
5150
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
4565
|
-
|
|
5151
|
+
step: null
|
|
4566
5152
|
}),
|
|
4567
5153
|
...found
|
|
4568
5154
|
};
|
|
@@ -4570,7 +5156,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4570
5156
|
row: failed("container", label, {
|
|
4571
5157
|
detail: `${runtime.containerName} does not publish port ${runtime.port} on ${LOOPBACK_HOST}`,
|
|
4572
5158
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
4573
|
-
|
|
5159
|
+
step: null
|
|
4574
5160
|
}),
|
|
4575
5161
|
...found
|
|
4576
5162
|
};
|
|
@@ -4591,7 +5177,7 @@ async function processRow(runtime, deps) {
|
|
|
4591
5177
|
row: failed("process", label, {
|
|
4592
5178
|
detail: `${inspection.jarPath} does not exist`,
|
|
4593
5179
|
hint,
|
|
4594
|
-
|
|
5180
|
+
step: "runtime"
|
|
4595
5181
|
}),
|
|
4596
5182
|
running: false,
|
|
4597
5183
|
holder: JAR_PROCESS
|
|
@@ -4618,13 +5204,13 @@ async function portRow(port, finding, ports) {
|
|
|
4618
5204
|
if (finding.running) return failed("port", "port", {
|
|
4619
5205
|
detail: `${port} is not answering although ${finding.holder} runs`,
|
|
4620
5206
|
hint: "`rde logs` shows what Metabase is doing",
|
|
4621
|
-
|
|
5207
|
+
step: null
|
|
4622
5208
|
});
|
|
4623
5209
|
if (free) return ok("port", "port", `${port} is free`);
|
|
4624
5210
|
return failed("port", "port", {
|
|
4625
5211
|
detail: `${port} is in use by another program`,
|
|
4626
5212
|
hint: "stop that program before `rde start`",
|
|
4627
|
-
|
|
5213
|
+
step: null
|
|
4628
5214
|
});
|
|
4629
5215
|
}
|
|
4630
5216
|
async function healthRow(url, deps) {
|
|
@@ -4639,7 +5225,7 @@ async function healthRow(url, deps) {
|
|
|
4639
5225
|
row: failed("health", "health", {
|
|
4640
5226
|
detail: `initializing (${Math.round(error.progress * 100)}%)`,
|
|
4641
5227
|
hint: "wait for it; `rde logs -f` shows progress",
|
|
4642
|
-
|
|
5228
|
+
step: "start-metabase"
|
|
4643
5229
|
}),
|
|
4644
5230
|
healthy: false
|
|
4645
5231
|
};
|
|
@@ -4647,7 +5233,7 @@ async function healthRow(url, deps) {
|
|
|
4647
5233
|
row: failed("health", "health", {
|
|
4648
5234
|
detail: "unreachable",
|
|
4649
5235
|
hint: "`rde start`",
|
|
4650
|
-
|
|
5236
|
+
step: "start-metabase"
|
|
4651
5237
|
}),
|
|
4652
5238
|
healthy: false
|
|
4653
5239
|
};
|
|
@@ -4655,7 +5241,7 @@ async function healthRow(url, deps) {
|
|
|
4655
5241
|
row: failed("health", "health", {
|
|
4656
5242
|
detail: `something other than Metabase answers at ${url} (HTTP ${error.status})`,
|
|
4657
5243
|
hint: "another program holds the port; stop it before `rde start`",
|
|
4658
|
-
|
|
5244
|
+
step: null
|
|
4659
5245
|
}),
|
|
4660
5246
|
healthy: false
|
|
4661
5247
|
};
|
|
@@ -4666,7 +5252,7 @@ function adminRow(state, properties) {
|
|
|
4666
5252
|
if (!properties["has-user-setup"]) return failed("admin", "admin user", {
|
|
4667
5253
|
detail: "Metabase has no admin user yet",
|
|
4668
5254
|
hint: "`rde init --only admin-user`",
|
|
4669
|
-
|
|
5255
|
+
step: "admin-user"
|
|
4670
5256
|
});
|
|
4671
5257
|
return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
|
|
4672
5258
|
}
|
|
@@ -4678,36 +5264,204 @@ function versionRow(state, properties) {
|
|
|
4678
5264
|
if (state.version !== running) return failed("version", label, {
|
|
4679
5265
|
detail: `running ${running}, the state file says ${state.version}`,
|
|
4680
5266
|
hint: "`rde start` records the running version",
|
|
4681
|
-
|
|
5267
|
+
step: VERSION_STEP
|
|
4682
5268
|
});
|
|
4683
5269
|
return ok("version", label, running);
|
|
4684
5270
|
}
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
5271
|
+
const API_KEY_STEP = "api-key";
|
|
5272
|
+
const RECREATE_HINT = "when the instance holds nothing you need, `rde uninstall` then `rde init`";
|
|
5273
|
+
function adminPasswordRow(state, secrets) {
|
|
5274
|
+
const id = "admin-password";
|
|
5275
|
+
const label = "admin password";
|
|
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
|
|
4690
5379
|
});
|
|
4691
|
-
return ok(
|
|
5380
|
+
return ok(id, label, "no secrets");
|
|
4692
5381
|
}
|
|
4693
|
-
async function
|
|
5382
|
+
async function credentialRows(state, secrets, apiKey, deps) {
|
|
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) {
|
|
4694
5396
|
const label = "mb profile";
|
|
4695
|
-
if (
|
|
5397
|
+
if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
|
|
4696
5398
|
if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY);
|
|
4697
|
-
const entry = (await
|
|
5399
|
+
const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
|
|
4698
5400
|
if (entry === void 0) return failed("mb-profile", label, {
|
|
4699
5401
|
detail: `${state.mbProfile} is not configured`,
|
|
4700
5402
|
hint: "`rde init --only api-key`",
|
|
4701
|
-
|
|
5403
|
+
step: "api-key"
|
|
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"
|
|
4702
5409
|
});
|
|
4703
5410
|
if (entry.status !== "ok") return failed("mb-profile", label, {
|
|
4704
5411
|
detail: `${state.mbProfile}: ${entry.status}`,
|
|
4705
5412
|
hint: "`rde init --only api-key` logs it in again",
|
|
4706
|
-
|
|
5413
|
+
step: "api-key"
|
|
4707
5414
|
});
|
|
4708
5415
|
const user = entry.user === null ? "" : ` as ${entry.user.name}`;
|
|
4709
5416
|
return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
|
|
4710
5417
|
}
|
|
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
|
+
}
|
|
4711
5465
|
function isNotFound$2(error) {
|
|
4712
5466
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
|
|
4713
5467
|
}
|
|
@@ -4718,7 +5472,7 @@ async function licenseRow(client) {
|
|
|
4718
5472
|
if (!status.valid) return failed("license", label, {
|
|
4719
5473
|
detail: `invalid: ${status.status}`,
|
|
4720
5474
|
hint: "`rde init --from license` with a valid token",
|
|
4721
|
-
|
|
5475
|
+
step: "license"
|
|
4722
5476
|
});
|
|
4723
5477
|
return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
|
|
4724
5478
|
} catch (error) {
|
|
@@ -4735,47 +5489,47 @@ async function databasesRow(state, client) {
|
|
|
4735
5489
|
if (missing.length > 0) return failed("databases", label, {
|
|
4736
5490
|
detail: `not on the server any more: ${missing.map((record) => record.name).join(", ")}`,
|
|
4737
5491
|
hint: "`rde db add` connects them again",
|
|
4738
|
-
|
|
5492
|
+
step: "databases"
|
|
4739
5493
|
});
|
|
4740
5494
|
const names = state.databases.map((record) => record.name).join(", ");
|
|
4741
5495
|
return ok("databases", label, `${state.databases.length} connected: ${names}`);
|
|
4742
5496
|
}
|
|
4743
5497
|
const SAMPLE_DATABASE_STEP = "sample-database";
|
|
4744
5498
|
const SAMPLE_LABEL = "sample database";
|
|
4745
|
-
async function sampleDatabaseRow(record, deps, dockerOk,
|
|
5499
|
+
async function sampleDatabaseRow(record, deps, dockerOk, access) {
|
|
4746
5500
|
const id = "sample-database";
|
|
4747
5501
|
if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
|
|
4748
5502
|
const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
|
|
4749
5503
|
if (!status.installed) return failed(id, SAMPLE_LABEL, {
|
|
4750
5504
|
detail: `${record.containerName} does not exist`,
|
|
4751
5505
|
hint: "`rde init` recreates it",
|
|
4752
|
-
|
|
5506
|
+
step: SAMPLE_DATABASE_STEP
|
|
4753
5507
|
});
|
|
4754
5508
|
if (!status.running) return failed(id, SAMPLE_LABEL, {
|
|
4755
5509
|
detail: `${record.containerName} is stopped`,
|
|
4756
5510
|
hint: "`rde start`",
|
|
4757
|
-
|
|
5511
|
+
step: "start-metabase"
|
|
4758
5512
|
});
|
|
4759
5513
|
if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
|
|
4760
5514
|
detail: `${record.containerName} runs but port ${record.port} is not answering`,
|
|
4761
5515
|
hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
|
|
4762
|
-
|
|
5516
|
+
step: null
|
|
4763
5517
|
});
|
|
4764
5518
|
const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
|
|
4765
5519
|
if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
|
|
4766
5520
|
detail: `${where}; not connected to Metabase yet`,
|
|
4767
5521
|
hint: "`rde init` connects it",
|
|
4768
|
-
|
|
5522
|
+
step: SAMPLE_DATABASE_STEP
|
|
4769
5523
|
});
|
|
4770
|
-
if (
|
|
5524
|
+
if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
|
|
4771
5525
|
try {
|
|
4772
|
-
const database = await client.database.get(record.databaseId);
|
|
5526
|
+
const database = await access.client.database.get(record.databaseId);
|
|
4773
5527
|
return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
|
|
4774
5528
|
} catch (error) {
|
|
4775
5529
|
if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
|
|
4776
5530
|
detail: `${where}; database ${record.databaseId} is gone from Metabase`,
|
|
4777
5531
|
hint: "`rde init` connects it again",
|
|
4778
|
-
|
|
5532
|
+
step: SAMPLE_DATABASE_STEP
|
|
4779
5533
|
});
|
|
4780
5534
|
throw error;
|
|
4781
5535
|
}
|
|
@@ -4792,7 +5546,7 @@ async function gitSyncRow(state, deps) {
|
|
|
4792
5546
|
if (status.branch === null) return failed("git-sync", label, {
|
|
4793
5547
|
detail: "Metabase reports no branch",
|
|
4794
5548
|
hint: "`rde init --only git-sync`",
|
|
4795
|
-
|
|
5549
|
+
step: "git-sync"
|
|
4796
5550
|
});
|
|
4797
5551
|
const dirty = status.is_dirty ? "unsynced changes" : "in sync";
|
|
4798
5552
|
return ok("git-sync", label, `branch ${status.branch}, ${dirty}, ${state.gitRemote.workdir}`);
|
|
@@ -4800,15 +5554,100 @@ async function gitSyncRow(state, deps) {
|
|
|
4800
5554
|
if (isMbFailure$1(error)) return failed("git-sync", label, {
|
|
4801
5555
|
detail: error.message,
|
|
4802
5556
|
hint: `\`${mbCommand(state.mbProfile)} git-sync status\` shows the full answer`,
|
|
4803
|
-
|
|
5557
|
+
step: null
|
|
4804
5558
|
});
|
|
4805
5559
|
throw error;
|
|
4806
5560
|
}
|
|
4807
5561
|
}
|
|
4808
5562
|
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";
|
|
4809
5567
|
function skillRowId(agent) {
|
|
4810
5568
|
return `skill.${agent.id}`;
|
|
4811
5569
|
}
|
|
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
|
+
}
|
|
4812
5651
|
async function skillRows(state, deps) {
|
|
4813
5652
|
const label = "rde skill";
|
|
4814
5653
|
if (state === null) return [skipped("skill", label, NO_INSTANCE)];
|
|
@@ -4816,17 +5655,18 @@ async function skillRows(state, deps) {
|
|
|
4816
5655
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
4817
5656
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
4818
5657
|
const entries = await rdeSkillEntries(deps.runner);
|
|
5658
|
+
const copies = await skillCopiesRow(entries, deps);
|
|
4819
5659
|
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
|
|
4820
5660
|
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
4821
5661
|
hint: `\`rde init --only ${SKILL_STEP}\``,
|
|
4822
|
-
|
|
4823
|
-
})];
|
|
4824
|
-
return detected.map((agent) => {
|
|
5662
|
+
step: SKILL_STEP
|
|
5663
|
+
}), copies];
|
|
5664
|
+
return [...detected.map((agent) => {
|
|
4825
5665
|
const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
|
|
4826
5666
|
const agentLabel = `${label} (${agent.displayName})`;
|
|
4827
5667
|
if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
|
|
4828
5668
|
return ok(skillRowId(agent), agentLabel, entry.path);
|
|
4829
|
-
});
|
|
5669
|
+
}), copies];
|
|
4830
5670
|
}
|
|
4831
5671
|
function noRuntimeRows() {
|
|
4832
5672
|
return [
|
|
@@ -4835,13 +5675,13 @@ function noRuntimeRows() {
|
|
|
4835
5675
|
"health"
|
|
4836
5676
|
].map((id) => skipped(id, id, NO_INSTANCE));
|
|
4837
5677
|
}
|
|
4838
|
-
async function instanceRows(state, deps, dockerOk,
|
|
5678
|
+
async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusableReason) {
|
|
4839
5679
|
const configured = configuredRuntime(state);
|
|
4840
5680
|
if (configured === null) return [
|
|
4841
5681
|
...noRuntimeRows(),
|
|
4842
5682
|
skipped("version", "Metabase version", NO_INSTANCE),
|
|
4843
5683
|
skipped("admin", "admin user", NO_INSTANCE),
|
|
4844
|
-
|
|
5684
|
+
...await credentialRows(state, secrets, await apiKeyFinding(secrets, null, false, deps), deps),
|
|
4845
5685
|
skipped("mb-profile", "mb profile", NO_INSTANCE),
|
|
4846
5686
|
skipped("license", "license", NO_INSTANCE),
|
|
4847
5687
|
skipped("databases", "databases", NO_INSTANCE),
|
|
@@ -4851,35 +5691,33 @@ async function instanceRows(state, deps, dockerOk, mbInstalled) {
|
|
|
4851
5691
|
const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
|
|
4852
5692
|
const { row: health, healthy } = await healthRow(configured.url, deps);
|
|
4853
5693
|
const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
|
|
5694
|
+
const apiKey = await apiKeyFinding(secrets, configured.url, healthy, deps);
|
|
4854
5695
|
const rows = [
|
|
4855
5696
|
runtime.row,
|
|
4856
5697
|
await portRow(configured.runtime.port, runtime, deps.ports),
|
|
4857
5698
|
health,
|
|
4858
5699
|
properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
|
|
4859
5700
|
properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
|
|
4860
|
-
|
|
4861
|
-
await mbProfileRow(state,
|
|
5701
|
+
...await credentialRows(state, secrets, apiKey, deps),
|
|
5702
|
+
await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
|
|
4862
5703
|
];
|
|
4863
|
-
const
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
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));
|
|
5704
|
+
const access = apiKey.access;
|
|
5705
|
+
if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
|
|
5706
|
+
else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
|
|
5707
|
+
rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
|
|
4872
5708
|
rows.push(await gitSyncRow(state, deps));
|
|
4873
5709
|
return rows;
|
|
4874
5710
|
}
|
|
4875
|
-
async function collectDoctor(instance, state, deps) {
|
|
5711
|
+
async function collectDoctor(instance, state, secrets, deps) {
|
|
4876
5712
|
const machine = await machineRows(deps, state?.runtime ?? null);
|
|
4877
5713
|
const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
|
|
4878
|
-
const mb = await
|
|
4879
|
-
const
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
checks.push(...await
|
|
5714
|
+
const mb = await mbFinding(deps);
|
|
5715
|
+
const profiles = sharedProfileList(deps);
|
|
5716
|
+
const checks = [...machine, mb.row];
|
|
5717
|
+
if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE), skipped("admin", "admin user", NO_INSTANCE), skipped("admin-password", "admin password", NO_INSTANCE), skipped("api-key", "API key", NO_INSTANCE), skipped("secrets-file", "secrets file", NO_INSTANCE), skipped("state-file", "state file", 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));
|
|
5718
|
+
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
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));
|
|
4883
5721
|
return {
|
|
4884
5722
|
instance,
|
|
4885
5723
|
checks,
|
|
@@ -4891,18 +5729,38 @@ const MARK = {
|
|
|
4891
5729
|
failed: "✗",
|
|
4892
5730
|
skipped: "–"
|
|
4893
5731
|
};
|
|
5732
|
+
const NOTE_MARK = "!";
|
|
4894
5733
|
const HINT_ARROW = "→";
|
|
5734
|
+
function markFor(row) {
|
|
5735
|
+
return row.status === "ok" && row.hint !== null ? NOTE_MARK : MARK[row.status];
|
|
5736
|
+
}
|
|
4895
5737
|
function doctorLines(report) {
|
|
4896
5738
|
const width = Math.max(...report.checks.map((row) => row.label.length));
|
|
5739
|
+
const indent = ` ${" ".repeat(width)} `;
|
|
4897
5740
|
const lines = [];
|
|
4898
5741
|
for (const row of report.checks) {
|
|
4899
|
-
|
|
4900
|
-
|
|
5742
|
+
const [first, ...rest] = row.detail.split("\n");
|
|
5743
|
+
lines.push(`${markFor(row)} ${row.label.padEnd(width)} ${first}`);
|
|
5744
|
+
lines.push(...rest.map((line) => `${indent}${line}`));
|
|
5745
|
+
if (row.hint !== null) lines.push(`${indent}${HINT_ARROW} ${row.hint}`);
|
|
4901
5746
|
}
|
|
4902
5747
|
return lines;
|
|
4903
5748
|
}
|
|
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
|
+
}
|
|
4904
5762
|
function fixableSteps(report) {
|
|
4905
|
-
const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.
|
|
5763
|
+
const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.step !== null ? [row.step] : []));
|
|
4906
5764
|
return StepId.options.filter((id) => wanted.has(id));
|
|
4907
5765
|
}
|
|
4908
5766
|
function doctorSummary(report) {
|
|
@@ -4941,15 +5799,24 @@ const ADMIN_USER_KEYS = {
|
|
|
4941
5799
|
firstName: `${STEP_ID$6}.first-name`,
|
|
4942
5800
|
lastName: `${STEP_ID$6}.last-name`
|
|
4943
5801
|
};
|
|
4944
|
-
const
|
|
4945
|
-
const
|
|
5802
|
+
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
5803
|
+
const GENERATED_PASSWORD_BYTES = 24;
|
|
4946
5804
|
const DEFAULT_SITE_NAME = "rde";
|
|
4947
5805
|
const MIN_PASSWORD_LENGTH = 6;
|
|
4948
5806
|
const DIGIT = /\d/;
|
|
4949
5807
|
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4950
|
-
const DEFAULT_ADMIN = { variant: "
|
|
5808
|
+
const DEFAULT_ADMIN = { variant: "generated" };
|
|
5809
|
+
function generatePassword(random) {
|
|
5810
|
+
for (;;) {
|
|
5811
|
+
const candidate = random(GENERATED_PASSWORD_BYTES).toString("base64url");
|
|
5812
|
+
if (DIGIT.test(candidate)) return candidate;
|
|
5813
|
+
}
|
|
5814
|
+
}
|
|
4951
5815
|
function hostDeps$8(ctx) {
|
|
4952
|
-
return {
|
|
5816
|
+
return {
|
|
5817
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5818
|
+
password: () => generatePassword(randomBytes)
|
|
5819
|
+
};
|
|
4953
5820
|
}
|
|
4954
5821
|
function isNotReady(error) {
|
|
4955
5822
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
@@ -4988,26 +5855,22 @@ async function promptedUser(ctx) {
|
|
|
4988
5855
|
...lastName.length > 0 && { last_name: lastName }
|
|
4989
5856
|
};
|
|
4990
5857
|
}
|
|
4991
|
-
async function chooseUser(ctx,
|
|
4992
|
-
switch (
|
|
4993
|
-
case "
|
|
4994
|
-
email:
|
|
4995
|
-
password:
|
|
5858
|
+
async function chooseUser(ctx, variant, deps) {
|
|
5859
|
+
switch (variant) {
|
|
5860
|
+
case "generated": return {
|
|
5861
|
+
email: GENERATED_ADMIN_EMAIL,
|
|
5862
|
+
password: deps.password()
|
|
4996
5863
|
};
|
|
4997
5864
|
case "prompted": return promptedUser(ctx);
|
|
4998
5865
|
}
|
|
4999
5866
|
}
|
|
5000
|
-
function
|
|
5001
|
-
switch (
|
|
5002
|
-
case "
|
|
5003
|
-
kind,
|
|
5867
|
+
function storedLogin(variant, user) {
|
|
5868
|
+
switch (variant) {
|
|
5869
|
+
case "generated": return {
|
|
5004
5870
|
email: user.email,
|
|
5005
5871
|
password: user.password
|
|
5006
5872
|
};
|
|
5007
|
-
case "prompted": return
|
|
5008
|
-
kind,
|
|
5009
|
-
email: user.email
|
|
5010
|
-
};
|
|
5873
|
+
case "prompted": return null;
|
|
5011
5874
|
}
|
|
5012
5875
|
}
|
|
5013
5876
|
function adminUserStep(options, deps = hostDeps$8) {
|
|
@@ -5026,7 +5889,8 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5026
5889
|
},
|
|
5027
5890
|
async run(ctx) {
|
|
5028
5891
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5029
|
-
const
|
|
5892
|
+
const resolved = deps(ctx);
|
|
5893
|
+
const client = resolved.client(configured.url, NO_AUTH);
|
|
5030
5894
|
const properties = await client.session.properties();
|
|
5031
5895
|
if (properties["has-user-setup"]) {
|
|
5032
5896
|
ctx.ui.info("Metabase already has an admin user");
|
|
@@ -5034,7 +5898,11 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5034
5898
|
}
|
|
5035
5899
|
const token = properties["setup-token"];
|
|
5036
5900
|
if (token === null) throw new ConfigError("Metabase has no admin user yet but offers no setup token");
|
|
5037
|
-
const user = await chooseUser(ctx, options.variant);
|
|
5901
|
+
const user = await chooseUser(ctx, options.variant, resolved);
|
|
5902
|
+
await ctx.saveSecrets({
|
|
5903
|
+
...ctx.secrets,
|
|
5904
|
+
admin: storedLogin(options.variant, user)
|
|
5905
|
+
});
|
|
5038
5906
|
const session = await client.setup.create({
|
|
5039
5907
|
token,
|
|
5040
5908
|
user,
|
|
@@ -5043,7 +5911,10 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5043
5911
|
ctx.session.metabaseSession.set(session.id);
|
|
5044
5912
|
await ctx.saveState({
|
|
5045
5913
|
...ctx.state,
|
|
5046
|
-
admin:
|
|
5914
|
+
admin: {
|
|
5915
|
+
kind: options.variant,
|
|
5916
|
+
email: user.email
|
|
5917
|
+
}
|
|
5047
5918
|
});
|
|
5048
5919
|
ctx.ui.success(`Admin user ${user.email} created`);
|
|
5049
5920
|
return "completed";
|
|
@@ -5066,11 +5937,12 @@ function hostDeps$7(ctx) {
|
|
|
5066
5937
|
};
|
|
5067
5938
|
}
|
|
5068
5939
|
async function adminCredentials(ctx) {
|
|
5069
|
-
const
|
|
5070
|
-
if (
|
|
5071
|
-
username:
|
|
5072
|
-
password:
|
|
5940
|
+
const stored = ctx.secrets.admin;
|
|
5941
|
+
if (stored !== null) return {
|
|
5942
|
+
username: stored.email,
|
|
5943
|
+
password: stored.password
|
|
5073
5944
|
};
|
|
5945
|
+
const admin = ctx.state.admin;
|
|
5074
5946
|
ctx.ui.info("An admin login is needed once to create the API key; it is not saved");
|
|
5075
5947
|
return {
|
|
5076
5948
|
username: await ctx.prompts.text({
|
|
@@ -5108,46 +5980,70 @@ async function freshApiKey(ctx, client) {
|
|
|
5108
5980
|
group_id: groupId
|
|
5109
5981
|
})).unmasked_key;
|
|
5110
5982
|
}
|
|
5111
|
-
async function
|
|
5112
|
-
|
|
5983
|
+
async function authenticates(client) {
|
|
5984
|
+
try {
|
|
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
|
+
}
|
|
5113
6001
|
const sessionId = await superuserSession(ctx, deps, url);
|
|
5114
6002
|
const apiKey = await freshApiKey(ctx, deps.client(url, {
|
|
5115
6003
|
kind: "session",
|
|
5116
6004
|
id: sessionId
|
|
5117
6005
|
}));
|
|
5118
|
-
await ctx.
|
|
5119
|
-
...ctx.
|
|
6006
|
+
await ctx.saveSecrets({
|
|
6007
|
+
...ctx.secrets,
|
|
5120
6008
|
apiKey
|
|
5121
6009
|
});
|
|
5122
6010
|
return apiKey;
|
|
5123
6011
|
}
|
|
5124
|
-
async function
|
|
5125
|
-
const profile = ctx.state.mbProfile;
|
|
5126
|
-
if (profile !== "default") return profile;
|
|
5127
|
-
let existing;
|
|
6012
|
+
async function defaultProfileStatus(mb) {
|
|
5128
6013
|
try {
|
|
5129
|
-
|
|
6014
|
+
return await mb.authStatus(DEFAULT_MB_PROFILE);
|
|
5130
6015
|
} catch (error) {
|
|
5131
|
-
if (error instanceof ProcessNotFoundError) return
|
|
6016
|
+
if (error instanceof ProcessNotFoundError) return null;
|
|
5132
6017
|
throw error;
|
|
5133
6018
|
}
|
|
5134
|
-
|
|
5135
|
-
|
|
6019
|
+
}
|
|
6020
|
+
async function chooseProfile(ctx, mb, url) {
|
|
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({
|
|
5136
6028
|
key: API_KEY_KEYS.profile,
|
|
5137
|
-
message: `mb profile
|
|
6029
|
+
message: `mb profile default already points at ${previous}; which profile should this instance use?`,
|
|
5138
6030
|
choices: [{
|
|
5139
|
-
value: "
|
|
5140
|
-
label: `
|
|
6031
|
+
value: "default",
|
|
6032
|
+
label: `default, replacing it (recommended; it points at ${previous} today)`
|
|
5141
6033
|
}, {
|
|
5142
|
-
value:
|
|
5143
|
-
label: `${
|
|
5144
|
-
}]
|
|
5145
|
-
|
|
5146
|
-
|
|
6034
|
+
value: MbProfileChoice.enum.rde,
|
|
6035
|
+
label: `${named}, and leave default alone`
|
|
6036
|
+
}],
|
|
6037
|
+
recommended: "default"
|
|
6038
|
+
}) === "default") {
|
|
6039
|
+
ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
|
|
6040
|
+
return profile;
|
|
6041
|
+
}
|
|
6042
|
+
await ctx.saveState({
|
|
5147
6043
|
...ctx.state,
|
|
5148
|
-
mbProfile:
|
|
6044
|
+
mbProfile: named
|
|
5149
6045
|
});
|
|
5150
|
-
return
|
|
6046
|
+
return named;
|
|
5151
6047
|
}
|
|
5152
6048
|
function usageNote(profile) {
|
|
5153
6049
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
@@ -5158,7 +6054,7 @@ function apiKeyStep(deps = hostDeps$7) {
|
|
|
5158
6054
|
id: STEP_ID$5,
|
|
5159
6055
|
title: "Connect the Metabase CLI",
|
|
5160
6056
|
async detect(ctx) {
|
|
5161
|
-
if (ctx.
|
|
6057
|
+
if (ctx.secrets.apiKey === null) return "todo";
|
|
5162
6058
|
const mb = mbTool(deps(ctx).runner);
|
|
5163
6059
|
const profile = ctx.state.mbProfile;
|
|
5164
6060
|
try {
|
|
@@ -5172,7 +6068,7 @@ function apiKeyStep(deps = hostDeps$7) {
|
|
|
5172
6068
|
async run(ctx) {
|
|
5173
6069
|
const resolved = deps(ctx);
|
|
5174
6070
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5175
|
-
const apiKey = await
|
|
6071
|
+
const apiKey = await workingApiKey(ctx, resolved, configured.url);
|
|
5176
6072
|
const mb = mbTool(resolved.runner);
|
|
5177
6073
|
const profile = await chooseProfile(ctx, mb, configured.url);
|
|
5178
6074
|
await mb.authLogin({
|
|
@@ -5574,14 +6470,14 @@ function isMbUnavailable(error) {
|
|
|
5574
6470
|
}
|
|
5575
6471
|
async function runGitSync(ctx, deps) {
|
|
5576
6472
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5577
|
-
if (ctx.
|
|
6473
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
5578
6474
|
const run = {
|
|
5579
6475
|
ctx,
|
|
5580
6476
|
deps,
|
|
5581
6477
|
git: gitTool(deps.runner),
|
|
5582
6478
|
client: deps.client(configured.url, {
|
|
5583
6479
|
kind: "api-key",
|
|
5584
|
-
key: ctx.
|
|
6480
|
+
key: ctx.secrets.apiKey
|
|
5585
6481
|
})
|
|
5586
6482
|
};
|
|
5587
6483
|
const flow = await runFlow({
|
|
@@ -5637,8 +6533,8 @@ function validateToken(value) {
|
|
|
5637
6533
|
return "A license token is 64 hexadecimal characters";
|
|
5638
6534
|
}
|
|
5639
6535
|
function requireApiKey$1(ctx) {
|
|
5640
|
-
if (ctx.
|
|
5641
|
-
return ctx.
|
|
6536
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
6537
|
+
return ctx.secrets.apiKey;
|
|
5642
6538
|
}
|
|
5643
6539
|
function activeLabel(status) {
|
|
5644
6540
|
const plan = status["plan-alias"];
|
|
@@ -5651,15 +6547,15 @@ function licenseStep(deps = hostDeps$5) {
|
|
|
5651
6547
|
async detect(ctx) {
|
|
5652
6548
|
if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
|
|
5653
6549
|
const configured = configuredRuntime(ctx.state);
|
|
5654
|
-
if (configured === null || ctx.
|
|
6550
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
5655
6551
|
const client = deps(ctx).client(configured.url, {
|
|
5656
6552
|
kind: "api-key",
|
|
5657
|
-
key: ctx.
|
|
6553
|
+
key: ctx.secrets.apiKey
|
|
5658
6554
|
});
|
|
5659
6555
|
try {
|
|
5660
6556
|
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
5661
6557
|
} catch (error) {
|
|
5662
|
-
if (isNoTokenYet(error)) return "todo";
|
|
6558
|
+
if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
|
|
5663
6559
|
throw error;
|
|
5664
6560
|
}
|
|
5665
6561
|
},
|
|
@@ -5894,9 +6790,9 @@ function isNotServingYet(error) {
|
|
|
5894
6790
|
function isNotFound$1(error) {
|
|
5895
6791
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
|
|
5896
6792
|
}
|
|
5897
|
-
function requireApiKey(
|
|
5898
|
-
if (
|
|
5899
|
-
return
|
|
6793
|
+
function requireApiKey(ctx) {
|
|
6794
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
|
|
6795
|
+
return ctx.secrets.apiKey;
|
|
5900
6796
|
}
|
|
5901
6797
|
async function recordedDatabase(record, client) {
|
|
5902
6798
|
if (record.databaseId === null) return null;
|
|
@@ -6018,23 +6914,23 @@ function sampleDatabaseStep(deps = hostDeps$3) {
|
|
|
6018
6914
|
if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
|
|
6019
6915
|
const record = ctx.state.sampleDatabase;
|
|
6020
6916
|
const configured = configuredRuntime(ctx.state);
|
|
6021
|
-
if (record === null || configured === null || ctx.
|
|
6917
|
+
if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
6022
6918
|
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
|
|
6023
6919
|
const client = resolved.client(configured.url, {
|
|
6024
6920
|
kind: "api-key",
|
|
6025
|
-
key: ctx.
|
|
6921
|
+
key: ctx.secrets.apiKey
|
|
6026
6922
|
});
|
|
6027
6923
|
try {
|
|
6028
6924
|
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
6029
6925
|
} catch (error) {
|
|
6030
|
-
if (isNotServingYet(error)) return "todo";
|
|
6926
|
+
if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
|
|
6031
6927
|
throw error;
|
|
6032
6928
|
}
|
|
6033
6929
|
},
|
|
6034
6930
|
async run(ctx) {
|
|
6035
6931
|
const resolved = deps(ctx);
|
|
6036
6932
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6037
|
-
const apiKey = requireApiKey(ctx
|
|
6933
|
+
const apiKey = requireApiKey(ctx);
|
|
6038
6934
|
const docker = await resolved.runtime.docker.available();
|
|
6039
6935
|
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
6040
6936
|
const choice = await persistedRecord(ctx, resolved);
|
|
@@ -6149,23 +7045,24 @@ function hostDeps$1(ctx) {
|
|
|
6149
7045
|
function isNotServing$1(error) {
|
|
6150
7046
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
6151
7047
|
}
|
|
6152
|
-
function loginLine(
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
case "default": return `Login: ${
|
|
6156
|
-
case "prompted": return `Login: ${
|
|
7048
|
+
function loginLine(kind, email) {
|
|
7049
|
+
switch (kind) {
|
|
7050
|
+
case "generated":
|
|
7051
|
+
case "legacy-default": return `Login: ${email} (rde credentials shows the password)`;
|
|
7052
|
+
case "prompted": return `Login: ${email} with the password you entered`;
|
|
6157
7053
|
}
|
|
6158
7054
|
}
|
|
6159
|
-
async function databaseLines$1(state, deps) {
|
|
6160
|
-
if (state.url === null ||
|
|
7055
|
+
async function databaseLines$1(state, apiKey, deps) {
|
|
7056
|
+
if (state.url === null || apiKey === null) return ["Databases: none connected yet"];
|
|
6161
7057
|
let databases;
|
|
6162
7058
|
try {
|
|
6163
7059
|
databases = (await deps.client(state.url, {
|
|
6164
7060
|
kind: "api-key",
|
|
6165
|
-
key:
|
|
7061
|
+
key: apiKey
|
|
6166
7062
|
}).database.list()).data;
|
|
6167
7063
|
} catch (error) {
|
|
6168
7064
|
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`"];
|
|
6169
7066
|
throw error;
|
|
6170
7067
|
}
|
|
6171
7068
|
if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
|
|
@@ -6199,14 +7096,13 @@ function launchLines(agents, workdir) {
|
|
|
6199
7096
|
const prefix = workdir === null ? "" : `cd ${workdir} && `;
|
|
6200
7097
|
return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
|
|
6201
7098
|
}
|
|
6202
|
-
async function summaryLines(state, deps) {
|
|
7099
|
+
async function summaryLines(state, apiKey, deps) {
|
|
6203
7100
|
const profile = state.mbProfile;
|
|
6204
7101
|
const lines = [];
|
|
6205
7102
|
if (state.url !== null) lines.push(`Metabase: ${state.url}`);
|
|
6206
|
-
|
|
6207
|
-
if (login !== null) lines.push(login);
|
|
7103
|
+
if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
|
|
6208
7104
|
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
6209
|
-
lines.push(...await databaseLines$1(state, deps));
|
|
7105
|
+
lines.push(...await databaseLines$1(state, apiKey, deps));
|
|
6210
7106
|
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
6211
7107
|
lines.push(...gitLines(state));
|
|
6212
7108
|
const installed = await skillsCli(deps.runner).list();
|
|
@@ -6223,7 +7119,8 @@ function summaryStep(deps = hostDeps$1) {
|
|
|
6223
7119
|
return "todo";
|
|
6224
7120
|
},
|
|
6225
7121
|
async run(ctx) {
|
|
6226
|
-
|
|
7122
|
+
const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
|
|
7123
|
+
ctx.ui.note(lines.join("\n"), "Your environment");
|
|
6227
7124
|
return "completed";
|
|
6228
7125
|
}
|
|
6229
7126
|
};
|
|
@@ -6234,7 +7131,7 @@ const DEFAULT_STEP_OPTIONS = {
|
|
|
6234
7131
|
runtime: NO_RUNTIME_FLAGS,
|
|
6235
7132
|
agents: DETECT_AGENTS,
|
|
6236
7133
|
admin: DEFAULT_ADMIN,
|
|
6237
|
-
databases:
|
|
7134
|
+
databases: SAMPLE_STANDS_IN
|
|
6238
7135
|
};
|
|
6239
7136
|
const STEP_BY_ID = {
|
|
6240
7137
|
preflight: () => preflightStep(),
|
|
@@ -6254,10 +7151,129 @@ function initSteps(options) {
|
|
|
6254
7151
|
return StepId.options.map((id) => STEP_BY_ID[id](options));
|
|
6255
7152
|
}
|
|
6256
7153
|
//#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
|
|
6257
7273
|
//#region src/commands/doctor.ts
|
|
6258
7274
|
function print(report, json) {
|
|
6259
7275
|
if (json) {
|
|
6260
|
-
printJson(report);
|
|
7276
|
+
printJson(doctorJson(report));
|
|
6261
7277
|
return;
|
|
6262
7278
|
}
|
|
6263
7279
|
printLines(doctorLines(report));
|
|
@@ -6288,12 +7304,16 @@ const doctor = defineRdeCommand({
|
|
|
6288
7304
|
const name = await instanceNameOrDefault();
|
|
6289
7305
|
const state = await readInstanceState(name);
|
|
6290
7306
|
const deps = hostDoctorDeps(name, interruptSignal);
|
|
6291
|
-
let report = await collectDoctor(name, state, deps);
|
|
7307
|
+
let report = await collectDoctor(name, state, state === null ? null : await readInstanceSecrets(name), deps);
|
|
6292
7308
|
if (args.fix && report.failed > 0) {
|
|
6293
7309
|
if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
|
|
6294
7310
|
print(report, args.json);
|
|
6295
|
-
const
|
|
6296
|
-
|
|
7311
|
+
const prompts = answeringPrompts(clackPrompts(), /* @__PURE__ */ new Map(), { takeRecommended: args.yes || !stdinIsTty() });
|
|
7312
|
+
const ctx = await createWizardContext(name, {
|
|
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);
|
|
6297
7317
|
}
|
|
6298
7318
|
print(report, args.json);
|
|
6299
7319
|
if (report.failed > 0) throw new Error(doctorSummary(report));
|
|
@@ -6318,7 +7338,7 @@ const EnvString = z.string().transform((value, ctx) => {
|
|
|
6318
7338
|
}
|
|
6319
7339
|
return resolved;
|
|
6320
7340
|
});
|
|
6321
|
-
const AdminVariant = z.enum(["
|
|
7341
|
+
const AdminVariant = z.enum(["generated", "prompted"]);
|
|
6322
7342
|
const RemoteKind = z.enum(["local-bare", "hosted"]);
|
|
6323
7343
|
const FieldValue = z.union([
|
|
6324
7344
|
EnvString,
|
|
@@ -6422,7 +7442,11 @@ function runtimeKindOf(runtime) {
|
|
|
6422
7442
|
if (runtime.jar !== void 0) return "jar";
|
|
6423
7443
|
return null;
|
|
6424
7444
|
}
|
|
6425
|
-
function
|
|
7445
|
+
function databasesRequest(databases, run) {
|
|
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) {
|
|
6426
7450
|
const runtime = answers.runtime;
|
|
6427
7451
|
const agents = answers["agents-skills"];
|
|
6428
7452
|
const admin = answers["admin-user"];
|
|
@@ -6436,7 +7460,7 @@ function stepOptionsFrom(answers) {
|
|
|
6436
7460
|
},
|
|
6437
7461
|
agents: agents === void 0 ? DEFAULT_STEP_OPTIONS.agents : { agents: agents.agents },
|
|
6438
7462
|
admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
|
|
6439
|
-
databases: {
|
|
7463
|
+
databases: { request: databasesRequest(databases, run) }
|
|
6440
7464
|
};
|
|
6441
7465
|
}
|
|
6442
7466
|
function keyedAnswers(answers) {
|
|
@@ -6554,7 +7578,7 @@ function parseDatabase(spec) {
|
|
|
6554
7578
|
function adminSection(flags, secrets) {
|
|
6555
7579
|
const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
|
|
6556
7580
|
if (flags.admin === void 0 && !prompted) return;
|
|
6557
|
-
if (flags.admin === "
|
|
7581
|
+
if (flags.admin === "generated" && prompted) throw conflict("--admin generated", "--admin-email/--admin-password");
|
|
6558
7582
|
return {
|
|
6559
7583
|
variant: flags.admin ?? "prompted",
|
|
6560
7584
|
...flags.adminEmail !== void 0 && { email: flags.adminEmail },
|
|
@@ -6705,20 +7729,57 @@ function isLocation(value) {
|
|
|
6705
7729
|
function isRemote(value) {
|
|
6706
7730
|
return value === "local-bare" || value === "hosted";
|
|
6707
7731
|
}
|
|
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
|
+
}
|
|
6708
7763
|
function applyDatabaseRecord(draft, record) {
|
|
6709
7764
|
const value = stringOf(record.answer);
|
|
7765
|
+
if (record.key === DATABASE_KEYS.connection) {
|
|
7766
|
+
applyConnectionRecord(draft, value);
|
|
7767
|
+
return;
|
|
7768
|
+
}
|
|
6710
7769
|
if (record.key === DATABASE_KEYS.engine) {
|
|
6711
|
-
|
|
6712
|
-
else if (value !== null) draft.databases.push({
|
|
6713
|
-
engine: value,
|
|
6714
|
-
uri: null,
|
|
6715
|
-
fields: {},
|
|
6716
|
-
name: null
|
|
6717
|
-
});
|
|
7770
|
+
applyEngineRecord(draft, value);
|
|
6718
7771
|
return;
|
|
6719
7772
|
}
|
|
6720
7773
|
const database = draft.databases.at(-1);
|
|
6721
7774
|
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
|
+
}
|
|
6722
7783
|
if (record.key === DATABASE_KEYS.uri && value !== null) {
|
|
6723
7784
|
database.uri = value;
|
|
6724
7785
|
return;
|
|
@@ -6760,11 +7821,17 @@ function applyRecord(draft, record) {
|
|
|
6760
7821
|
function writableUri(uri, index) {
|
|
6761
7822
|
return URI_WITH_PASSWORD.test(uri) ? envReference(databaseEnv(index, "uri")) : uri;
|
|
6762
7823
|
}
|
|
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
|
+
}
|
|
6763
7829
|
function databaseEntry(database, index) {
|
|
6764
7830
|
const hasFields = Object.keys(database.fields).length > 0;
|
|
6765
|
-
|
|
7831
|
+
const engine = engineOf(database);
|
|
7832
|
+
if (database.uri === null && !hasFields || engine === null) return null;
|
|
6766
7833
|
return {
|
|
6767
|
-
engine
|
|
7834
|
+
engine,
|
|
6768
7835
|
...database.uri !== null && { uri: writableUri(database.uri, index) },
|
|
6769
7836
|
...database.uri === null && { fields: database.fields },
|
|
6770
7837
|
...database.name !== null && { name: database.name }
|
|
@@ -6789,122 +7856,6 @@ async function writeAnswersFile(path, answers) {
|
|
|
6789
7856
|
await promises.writeFile(path, JSON.stringify(answers, null, 2) + "\n", { mode: FILE_MODE });
|
|
6790
7857
|
}
|
|
6791
7858
|
//#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
|
|
6908
7859
|
//#region src/wizard/record.ts
|
|
6909
7860
|
function recordingPrompts(base) {
|
|
6910
7861
|
const records = [];
|
|
@@ -6981,9 +7932,11 @@ async function combinedAnswers(options) {
|
|
|
6981
7932
|
return mergeInitAnswers(await loadInitAnswers(options.answersPath), fromFlags);
|
|
6982
7933
|
}
|
|
6983
7934
|
function promptsFor(answers, yes) {
|
|
7935
|
+
const terminal = stdinIsTty();
|
|
6984
7936
|
const answering = {
|
|
6985
|
-
...!
|
|
6986
|
-
...yes && { confirmAll: true }
|
|
7937
|
+
...!terminal && { unanswered: refusalWithoutTerminal },
|
|
7938
|
+
...yes && { confirmAll: true },
|
|
7939
|
+
...(yes || !terminal) && { takeRecommended: true }
|
|
6987
7940
|
};
|
|
6988
7941
|
return answeringPrompts(clackPrompts(), keyedAnswers(answers), answering);
|
|
6989
7942
|
}
|
|
@@ -7027,7 +7980,7 @@ const init = defineRdeCommand({
|
|
|
7027
7980
|
},
|
|
7028
7981
|
admin: {
|
|
7029
7982
|
type: "string",
|
|
7030
|
-
description: "Admin credentials:
|
|
7983
|
+
description: "Admin credentials: generated (a random password; `rde credentials` shows it) or prompted"
|
|
7031
7984
|
},
|
|
7032
7985
|
"admin-email": {
|
|
7033
7986
|
type: "string",
|
|
@@ -7087,7 +8040,7 @@ const init = defineRdeCommand({
|
|
|
7087
8040
|
},
|
|
7088
8041
|
"mb-profile": {
|
|
7089
8042
|
type: "string",
|
|
7090
|
-
description: "mb profile for
|
|
8043
|
+
description: "mb profile for the default instance when `default` already points at another Metabase: default (replace it) or rde"
|
|
7091
8044
|
},
|
|
7092
8045
|
yes: {
|
|
7093
8046
|
type: "boolean",
|
|
@@ -7136,7 +8089,7 @@ const init = defineRdeCommand({
|
|
|
7136
8089
|
prompts: recording?.prompts ?? answering
|
|
7137
8090
|
});
|
|
7138
8091
|
ctx.ui.intro("rde init");
|
|
7139
|
-
await runWizard(initSteps(stepOptionsFrom(answers)), ctx, options);
|
|
8092
|
+
await runWizard(initSteps(stepOptionsFrom(answers, { interactive: stdinIsTty() && !runOptions.yes })), ctx, options);
|
|
7140
8093
|
if (recording !== null && printTo !== void 0) {
|
|
7141
8094
|
await writeAnswersFile(printTo, answersFromRecords(recording.records, answers));
|
|
7142
8095
|
ctx.ui.info(`Answers written to ${printTo}`);
|
|
@@ -7243,11 +8196,11 @@ function isNotServing(error) {
|
|
|
7243
8196
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7244
8197
|
}
|
|
7245
8198
|
async function databasesOf(ctx, deps, health) {
|
|
7246
|
-
if (health !== "healthy" || ctx.
|
|
8199
|
+
if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
|
|
7247
8200
|
try {
|
|
7248
|
-
return await listDatabases(ctx.state, { client: deps.client });
|
|
8201
|
+
return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
|
|
7249
8202
|
} catch (error) {
|
|
7250
|
-
if (isNotServing(error)) return null;
|
|
8203
|
+
if (isNotServing(error) || isRejectedCredential(error)) return null;
|
|
7251
8204
|
throw error;
|
|
7252
8205
|
}
|
|
7253
8206
|
}
|
|
@@ -7290,9 +8243,13 @@ async function collectStatus(ctx, deps) {
|
|
|
7290
8243
|
const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
|
|
7291
8244
|
const health = configured === null ? null : await healthOf(configured.url, deps);
|
|
7292
8245
|
const databases = health === null ? null : await databasesOf(ctx, deps, health);
|
|
8246
|
+
const admin = ctx.state.admin;
|
|
7293
8247
|
return {
|
|
7294
8248
|
instance: ctx.state.name,
|
|
7295
8249
|
url: configured?.url ?? null,
|
|
8250
|
+
mbProfile: ctx.state.mbProfile,
|
|
8251
|
+
adminEmail: admin === null ? null : admin.email,
|
|
8252
|
+
adminVariant: admin === null ? null : admin.kind,
|
|
7296
8253
|
runtime: configured === null || runtime === null ? null : {
|
|
7297
8254
|
kind: configured.runtime.kind,
|
|
7298
8255
|
installed: runtime.installed,
|
|
@@ -7346,6 +8303,8 @@ function databaseLines(report) {
|
|
|
7346
8303
|
function statusLines(report) {
|
|
7347
8304
|
return [
|
|
7348
8305
|
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>`,
|
|
7349
8308
|
runtimeLine(report),
|
|
7350
8309
|
versionLine(report),
|
|
7351
8310
|
`Health: ${report.health ?? "unknown"}`,
|
|
@@ -7648,7 +8607,7 @@ async function planUninstall(options, deps) {
|
|
|
7648
8607
|
if (jar.removal !== null) removals.push(jar.removal);
|
|
7649
8608
|
if (jar.kept !== null) kept.push(jar.kept);
|
|
7650
8609
|
}
|
|
7651
|
-
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE,
|
|
8610
|
+
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, namedMbProfile(options.instance)], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
|
|
7652
8611
|
removals.push(...profiles);
|
|
7653
8612
|
if (options.removeSkill) {
|
|
7654
8613
|
const skill = await findSkill(deps.runner);
|
|
@@ -7657,6 +8616,11 @@ async function planUninstall(options, deps) {
|
|
|
7657
8616
|
const dir = instanceDir(options.instance);
|
|
7658
8617
|
const dirPresent = await pathExists(dir);
|
|
7659
8618
|
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
|
+
});
|
|
7660
8624
|
if (dirPresent) removals.push({
|
|
7661
8625
|
label: instanceDirLabel(dir, state),
|
|
7662
8626
|
remove: () => promises.rm(dir, {
|
|
@@ -8058,7 +9022,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
8058
9022
|
const main = defineCommand({
|
|
8059
9023
|
meta: {
|
|
8060
9024
|
name: "rde",
|
|
8061
|
-
version: "0.1.
|
|
9025
|
+
version: "0.1.4",
|
|
8062
9026
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
8063
9027
|
},
|
|
8064
9028
|
subCommands: {
|
|
@@ -8066,6 +9030,7 @@ const main = defineCommand({
|
|
|
8066
9030
|
start,
|
|
8067
9031
|
stop,
|
|
8068
9032
|
status,
|
|
9033
|
+
credentials,
|
|
8069
9034
|
logs,
|
|
8070
9035
|
open,
|
|
8071
9036
|
db,
|