@metabase/rde 0.1.3 → 0.1.5
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 +84 -43
- package/dist/cli.mjs +1791 -766
- 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;
|
|
@@ -1893,7 +2229,6 @@ async function chooseAfterParseError(run, engine, uri, error) {
|
|
|
1893
2229
|
}
|
|
1894
2230
|
function toReview(run, model, connection) {
|
|
1895
2231
|
const mapped = mapDatabaseValues(connection.parsed);
|
|
1896
|
-
const aliased = applyHostAlias(mapped.details, connection.engine.key, run.deps.hostAlias);
|
|
1897
2232
|
const sameString = model.uri === connection.parsed.uri;
|
|
1898
2233
|
return {
|
|
1899
2234
|
type: "next",
|
|
@@ -1901,8 +2236,8 @@ function toReview(run, model, connection) {
|
|
|
1901
2236
|
model: {
|
|
1902
2237
|
engine: connection.engine,
|
|
1903
2238
|
uri: connection.parsed.uri,
|
|
1904
|
-
details:
|
|
1905
|
-
hostRewritten:
|
|
2239
|
+
details: mapped.details,
|
|
2240
|
+
hostRewritten: false,
|
|
1906
2241
|
suggestedName: defaultDisplayName(mapped, connection.parsed, connection.engine),
|
|
1907
2242
|
name: sameString ? model.name : null,
|
|
1908
2243
|
fieldErrors: null
|
|
@@ -2023,11 +2358,15 @@ function reviewScreen(run) {
|
|
|
2023
2358
|
let details = model.details;
|
|
2024
2359
|
if (model.fieldErrors !== null) details = await fixReportedFields(run, chosen.fields, details, model.fieldErrors);
|
|
2025
2360
|
let name = model.name;
|
|
2361
|
+
let hostRewritten = model.hostRewritten;
|
|
2026
2362
|
for (;;) {
|
|
2027
|
-
|
|
2363
|
+
const aliased = applyHostAlias(await fillRequiredFields(run.deps, chosen.fields, details), chosen.engine.key, run.deps.hostAlias);
|
|
2364
|
+
details = aliased.details;
|
|
2365
|
+
hostRewritten ||= aliased.rewritten;
|
|
2028
2366
|
const current = {
|
|
2029
2367
|
...model,
|
|
2030
2368
|
details,
|
|
2369
|
+
hostRewritten,
|
|
2031
2370
|
name,
|
|
2032
2371
|
fieldErrors: null
|
|
2033
2372
|
};
|
|
@@ -2191,19 +2530,20 @@ async function runDatabaseFlow(deps) {
|
|
|
2191
2530
|
created: []
|
|
2192
2531
|
};
|
|
2193
2532
|
await runFlow({
|
|
2533
|
+
"connection-string": connectionStringScreen(run),
|
|
2194
2534
|
"pick-engine": pickEngineScreen(run),
|
|
2195
2535
|
"paste-uri": pasteUriScreen(run),
|
|
2196
2536
|
review: reviewScreen(run),
|
|
2197
2537
|
test: testScreen(run),
|
|
2198
2538
|
create: createScreen(run),
|
|
2199
2539
|
another: anotherScreen(run)
|
|
2200
|
-
},
|
|
2540
|
+
}, START_SCREEN[deps.start], FRESH$1);
|
|
2201
2541
|
return { created: run.created };
|
|
2202
2542
|
}
|
|
2203
2543
|
//#endregion
|
|
2204
2544
|
//#region src/steps/databases.ts
|
|
2205
2545
|
const STEP_ID$9 = "databases";
|
|
2206
|
-
const
|
|
2546
|
+
const SAMPLE_STANDS_IN = { request: "sample-stands-in" };
|
|
2207
2547
|
function hostDatabasesDeps(signal) {
|
|
2208
2548
|
return {
|
|
2209
2549
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
@@ -2214,9 +2554,9 @@ function hostDatabasesDeps(signal) {
|
|
|
2214
2554
|
function isNotServingYet$1(error) {
|
|
2215
2555
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
2216
2556
|
}
|
|
2217
|
-
function requireApiKey$2(state) {
|
|
2218
|
-
if (
|
|
2219
|
-
return
|
|
2557
|
+
function requireApiKey$2(state, apiKey) {
|
|
2558
|
+
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
2559
|
+
return apiKey;
|
|
2220
2560
|
}
|
|
2221
2561
|
function hostAliasOf(runtime) {
|
|
2222
2562
|
switch (runtime.kind) {
|
|
@@ -2232,15 +2572,15 @@ function toRecord$1(database, createdAt) {
|
|
|
2232
2572
|
createdAt
|
|
2233
2573
|
};
|
|
2234
2574
|
}
|
|
2235
|
-
function authenticatedClient(state, deps) {
|
|
2575
|
+
function authenticatedClient(state, apiKey, deps) {
|
|
2236
2576
|
const configured = requireConfiguredRuntime(state);
|
|
2237
2577
|
return deps.client(configured.url, {
|
|
2238
2578
|
kind: "api-key",
|
|
2239
|
-
key: requireApiKey$2(state)
|
|
2579
|
+
key: requireApiKey$2(state, apiKey)
|
|
2240
2580
|
});
|
|
2241
2581
|
}
|
|
2242
|
-
async function listDatabases(state, deps) {
|
|
2243
|
-
const listed = await authenticatedClient(state, deps).database.list();
|
|
2582
|
+
async function listDatabases(state, apiKey, deps) {
|
|
2583
|
+
const listed = await authenticatedClient(state, apiKey, deps).database.list();
|
|
2244
2584
|
const records = new Map(state.databases.map((record) => [record.id, record]));
|
|
2245
2585
|
const onServer = listed.data.map((database) => ({
|
|
2246
2586
|
id: database.id,
|
|
@@ -2259,13 +2599,14 @@ async function listDatabases(state, deps) {
|
|
|
2259
2599
|
}));
|
|
2260
2600
|
return [...onServer, ...missing];
|
|
2261
2601
|
}
|
|
2262
|
-
async function
|
|
2602
|
+
async function connectDatabases(ctx, deps, start) {
|
|
2263
2603
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
2264
|
-
const client = authenticatedClient(ctx.state, deps);
|
|
2604
|
+
const client = authenticatedClient(ctx.state, ctx.secrets.apiKey, deps);
|
|
2265
2605
|
const properties = await client.session.properties();
|
|
2266
2606
|
const existing = await client.database.list();
|
|
2267
2607
|
if (existing.data.length > 0) ctx.ui.note(existingDatabaseLines(existing.data).join("\n"), "Connected databases");
|
|
2268
2608
|
const result = await runDatabaseFlow({
|
|
2609
|
+
start,
|
|
2269
2610
|
prompts: ctx.prompts,
|
|
2270
2611
|
ui: ctx.ui,
|
|
2271
2612
|
catalog: engineCatalog(properties.engines),
|
|
@@ -2281,7 +2622,17 @@ async function addDatabases(ctx, deps) {
|
|
|
2281
2622
|
});
|
|
2282
2623
|
}
|
|
2283
2624
|
});
|
|
2284
|
-
return
|
|
2625
|
+
return {
|
|
2626
|
+
existing: existing.data.length,
|
|
2627
|
+
created: result.created.length
|
|
2628
|
+
};
|
|
2629
|
+
}
|
|
2630
|
+
async function addDatabases(ctx, deps) {
|
|
2631
|
+
const connected = await connectDatabases(ctx, deps, "engine");
|
|
2632
|
+
return connected.existing + connected.created > 0 ? "completed" : "skipped";
|
|
2633
|
+
}
|
|
2634
|
+
async function askForOwnDatabase(ctx, deps) {
|
|
2635
|
+
return (await connectDatabases(ctx, deps, "connection-string")).created > 0 ? "completed" : "skipped";
|
|
2285
2636
|
}
|
|
2286
2637
|
function hostDeps$12(ctx) {
|
|
2287
2638
|
return hostDatabasesDeps(ctx.signal);
|
|
@@ -2289,6 +2640,9 @@ function hostDeps$12(ctx) {
|
|
|
2289
2640
|
function sampleDatabaseId(state) {
|
|
2290
2641
|
return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
|
|
2291
2642
|
}
|
|
2643
|
+
function skippedBefore(state) {
|
|
2644
|
+
return state.steps[STEP_ID$9]?.status === "skipped";
|
|
2645
|
+
}
|
|
2292
2646
|
function ownDatabaseIds(state) {
|
|
2293
2647
|
const sampleId = sampleDatabaseId(state);
|
|
2294
2648
|
return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
|
|
@@ -2299,21 +2653,28 @@ function databasesStep(options, deps = hostDeps$12) {
|
|
|
2299
2653
|
title: "Connect your databases",
|
|
2300
2654
|
async detect(ctx) {
|
|
2301
2655
|
const configured = configuredRuntime(ctx.state);
|
|
2302
|
-
if (configured === null || ctx.
|
|
2656
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
2303
2657
|
const own = ownDatabaseIds(ctx.state);
|
|
2304
|
-
|
|
2658
|
+
const sampleId = sampleDatabaseId(ctx.state);
|
|
2659
|
+
const asking = options.request === "ask-once" && !skippedBefore(ctx.state);
|
|
2660
|
+
if (own.size === 0 && !asking) {
|
|
2661
|
+
if (options.request === "named") return "todo";
|
|
2662
|
+
return sampleId !== null || skippedBefore(ctx.state) ? "done" : "todo";
|
|
2663
|
+
}
|
|
2664
|
+
const isOwn = (id) => own.size > 0 ? own.has(id) : id !== sampleId;
|
|
2305
2665
|
const client = deps(ctx).client(configured.url, {
|
|
2306
2666
|
kind: "api-key",
|
|
2307
|
-
key: ctx.
|
|
2667
|
+
key: ctx.secrets.apiKey
|
|
2308
2668
|
});
|
|
2309
2669
|
try {
|
|
2310
|
-
return (await client.database.list()).data.some((database) =>
|
|
2670
|
+
return (await client.database.list()).data.some((database) => isOwn(database.id)) ? "done" : "todo";
|
|
2311
2671
|
} catch (error) {
|
|
2312
|
-
if (isNotServingYet$1(error)) return "todo";
|
|
2672
|
+
if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
|
|
2313
2673
|
throw error;
|
|
2314
2674
|
}
|
|
2315
2675
|
},
|
|
2316
2676
|
run(ctx) {
|
|
2677
|
+
if (options.request === "ask-once") return askForOwnDatabase(ctx, deps(ctx));
|
|
2317
2678
|
return addDatabases(ctx, deps(ctx));
|
|
2318
2679
|
}
|
|
2319
2680
|
};
|
|
@@ -2391,6 +2752,11 @@ async function createWizardContext(instanceName, options) {
|
|
|
2391
2752
|
await writeInstanceState(next);
|
|
2392
2753
|
ctx.state = next;
|
|
2393
2754
|
},
|
|
2755
|
+
secrets: await readInstanceSecrets(instanceName) ?? emptyInstanceSecrets(),
|
|
2756
|
+
saveSecrets: async (next) => {
|
|
2757
|
+
await writeInstanceSecrets(instanceName, next);
|
|
2758
|
+
ctx.secrets = next;
|
|
2759
|
+
},
|
|
2394
2760
|
prompts: options.prompts ?? clackPrompts(),
|
|
2395
2761
|
ui: clackUi(),
|
|
2396
2762
|
signal: options.signal,
|
|
@@ -2399,53 +2765,6 @@ async function createWizardContext(instanceName, options) {
|
|
|
2399
2765
|
return ctx;
|
|
2400
2766
|
}
|
|
2401
2767
|
//#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
2768
|
//#region src/commands/db/add.ts
|
|
2450
2769
|
const dbAdd = defineRdeCommand({
|
|
2451
2770
|
meta: {
|
|
@@ -2455,19 +2774,10 @@ const dbAdd = defineRdeCommand({
|
|
|
2455
2774
|
async run() {
|
|
2456
2775
|
const name = await defaultInstanceName();
|
|
2457
2776
|
await requireInstanceState(name);
|
|
2458
|
-
|
|
2459
|
-
await databasesStep(ASK_DATABASES).run(ctx);
|
|
2777
|
+
await addDatabases(await createWizardContext(name, { signal: interruptSignal }), hostDatabasesDeps(interruptSignal));
|
|
2460
2778
|
}
|
|
2461
2779
|
});
|
|
2462
2780
|
//#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
2781
|
//#region src/commands/db/list.ts
|
|
2472
2782
|
const dbList = defineRdeCommand({
|
|
2473
2783
|
meta: {
|
|
@@ -2480,7 +2790,10 @@ const dbList = defineRdeCommand({
|
|
|
2480
2790
|
default: false
|
|
2481
2791
|
} },
|
|
2482
2792
|
async run(args) {
|
|
2483
|
-
const
|
|
2793
|
+
const name = await defaultInstanceName();
|
|
2794
|
+
const state = await requireInstanceState(name);
|
|
2795
|
+
const secrets = await readInstanceSecrets(name);
|
|
2796
|
+
const listing = await listDatabases(state, secrets === null ? null : secrets.apiKey, hostDatabasesDeps(interruptSignal));
|
|
2484
2797
|
if (args.json) {
|
|
2485
2798
|
printJson(listing);
|
|
2486
2799
|
return;
|
|
@@ -2809,15 +3122,20 @@ async function isExecutable(path) {
|
|
|
2809
3122
|
return false;
|
|
2810
3123
|
}
|
|
2811
3124
|
}
|
|
2812
|
-
async function
|
|
3125
|
+
async function whichAll(name) {
|
|
2813
3126
|
const searchPath = readEnv(ENV_PATH);
|
|
2814
|
-
if (searchPath === void 0) return
|
|
2815
|
-
|
|
3127
|
+
if (searchPath === void 0) return [];
|
|
3128
|
+
const found = [];
|
|
3129
|
+
for (const dir of new Set(searchPath.split(delimiter))) {
|
|
2816
3130
|
if (dir.length === 0) continue;
|
|
2817
3131
|
const candidate = join(dir, name);
|
|
2818
|
-
if (await isExecutable(candidate))
|
|
3132
|
+
if (await isExecutable(candidate)) found.push(candidate);
|
|
2819
3133
|
}
|
|
2820
|
-
return
|
|
3134
|
+
return found;
|
|
3135
|
+
}
|
|
3136
|
+
async function whichCommand(name) {
|
|
3137
|
+
const [first] = await whichAll(name);
|
|
3138
|
+
return first ?? null;
|
|
2821
3139
|
}
|
|
2822
3140
|
//#endregion
|
|
2823
3141
|
//#region src/tools/java.ts
|
|
@@ -3532,173 +3850,148 @@ function sampleDatabaseContainer(state, docker) {
|
|
|
3532
3850
|
};
|
|
3533
3851
|
}
|
|
3534
3852
|
//#endregion
|
|
3535
|
-
//#region src/tools/agents.ts
|
|
3536
|
-
const AgentId = z.enum([
|
|
3537
|
-
"claude-code",
|
|
3538
|
-
"codex",
|
|
3539
|
-
"cursor",
|
|
3540
|
-
"gemini-cli",
|
|
3541
|
-
"opencode",
|
|
3542
|
-
"windsurf"
|
|
3543
|
-
]);
|
|
3544
|
-
const AGENTS = [
|
|
3545
|
-
{
|
|
3546
|
-
id: "claude-code",
|
|
3547
|
-
displayName: "Claude Code",
|
|
3548
|
-
skillsCliName: "claude-code",
|
|
3549
|
-
command: "claude",
|
|
3550
|
-
launch: "claude",
|
|
3551
|
-
configDir: (home) => join(home, ".claude")
|
|
3552
|
-
},
|
|
3553
|
-
{
|
|
3554
|
-
id: "codex",
|
|
3555
|
-
displayName: "Codex",
|
|
3556
|
-
skillsCliName: "codex",
|
|
3557
|
-
command: "codex",
|
|
3558
|
-
launch: "codex",
|
|
3559
|
-
configDir: (home) => join(home, ".codex")
|
|
3560
|
-
},
|
|
3561
|
-
{
|
|
3562
|
-
id: "cursor",
|
|
3563
|
-
displayName: "Cursor",
|
|
3564
|
-
skillsCliName: "cursor",
|
|
3565
|
-
command: "cursor",
|
|
3566
|
-
launch: "cursor .",
|
|
3567
|
-
configDir: (home) => join(home, ".cursor")
|
|
3568
|
-
},
|
|
3569
|
-
{
|
|
3570
|
-
id: "gemini-cli",
|
|
3571
|
-
displayName: "Gemini CLI",
|
|
3572
|
-
skillsCliName: "gemini-cli",
|
|
3573
|
-
command: "gemini",
|
|
3574
|
-
launch: "gemini",
|
|
3575
|
-
configDir: (home) => join(home, ".gemini")
|
|
3576
|
-
},
|
|
3577
|
-
{
|
|
3578
|
-
id: "opencode",
|
|
3579
|
-
displayName: "OpenCode",
|
|
3580
|
-
skillsCliName: "opencode",
|
|
3581
|
-
command: "opencode",
|
|
3582
|
-
launch: "opencode",
|
|
3583
|
-
configDir: (_home, configHome) => join(configHome, "opencode")
|
|
3584
|
-
},
|
|
3585
|
-
{
|
|
3586
|
-
id: "windsurf",
|
|
3587
|
-
displayName: "Windsurf",
|
|
3588
|
-
skillsCliName: "windsurf",
|
|
3589
|
-
command: null,
|
|
3590
|
-
launch: "windsurf .",
|
|
3591
|
-
configDir: (home) => join(home, ".codeium", "windsurf")
|
|
3592
|
-
}
|
|
3593
|
-
];
|
|
3594
|
-
async function directoryExists$1(path) {
|
|
3595
|
-
try {
|
|
3596
|
-
return (await promises.stat(path)).isDirectory();
|
|
3597
|
-
} catch {
|
|
3598
|
-
return false;
|
|
3599
|
-
}
|
|
3600
|
-
}
|
|
3601
|
-
function hostAgentProbe() {
|
|
3602
|
-
const home = homedir();
|
|
3603
|
-
return {
|
|
3604
|
-
commandExists: async (command) => await whichCommand(command) !== null,
|
|
3605
|
-
directoryExists: directoryExists$1,
|
|
3606
|
-
home,
|
|
3607
|
-
configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
|
|
3608
|
-
};
|
|
3609
|
-
}
|
|
3610
|
-
async function isDetected(agent, probe) {
|
|
3611
|
-
if (agent.command !== null && await probe.commandExists(agent.command)) return true;
|
|
3612
|
-
return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
|
|
3613
|
-
}
|
|
3614
|
-
async function detectAgents(probe) {
|
|
3615
|
-
return Promise.all(AGENTS.map(async (agent) => ({
|
|
3616
|
-
id: agent.id,
|
|
3617
|
-
displayName: agent.displayName,
|
|
3618
|
-
skillsCliName: agent.skillsCliName,
|
|
3619
|
-
launch: agent.launch,
|
|
3620
|
-
detected: await isDetected(agent, probe)
|
|
3621
|
-
})));
|
|
3622
|
-
}
|
|
3623
|
-
//#endregion
|
|
3624
3853
|
//#region src/tools/mb.ts
|
|
3625
3854
|
const MB = "mb";
|
|
3626
|
-
const MB_INSTALL_SPEC = `@metabase/cli@alpha-transform-
|
|
3855
|
+
const MB_INSTALL_SPEC = `@metabase/cli@alpha-fix-transform-test-ee-path`;
|
|
3627
3856
|
const GLOBAL_INSTALL = {
|
|
3628
3857
|
npm: {
|
|
3629
3858
|
command: "npm",
|
|
3630
|
-
args: [
|
|
3631
|
-
"install",
|
|
3632
|
-
"-g",
|
|
3633
|
-
MB_INSTALL_SPEC
|
|
3634
|
-
]
|
|
3859
|
+
args: ["install", "-g"]
|
|
3635
3860
|
},
|
|
3636
3861
|
pnpm: {
|
|
3637
3862
|
command: "pnpm",
|
|
3638
|
-
args: [
|
|
3639
|
-
"add",
|
|
3640
|
-
"-g",
|
|
3641
|
-
MB_INSTALL_SPEC
|
|
3642
|
-
]
|
|
3863
|
+
args: ["add", "-g"]
|
|
3643
3864
|
},
|
|
3644
3865
|
yarn: {
|
|
3645
3866
|
command: "yarn",
|
|
3646
|
-
args: [
|
|
3647
|
-
"global",
|
|
3648
|
-
"add",
|
|
3649
|
-
MB_INSTALL_SPEC
|
|
3650
|
-
]
|
|
3867
|
+
args: ["global", "add"]
|
|
3651
3868
|
},
|
|
3652
3869
|
bun: {
|
|
3653
3870
|
command: "bun",
|
|
3654
|
-
args: [
|
|
3655
|
-
"add",
|
|
3656
|
-
"-g",
|
|
3657
|
-
MB_INSTALL_SPEC
|
|
3658
|
-
]
|
|
3871
|
+
args: ["add", "-g"]
|
|
3659
3872
|
}
|
|
3660
3873
|
};
|
|
3874
|
+
function globalInstallCommand(packageManager, spec) {
|
|
3875
|
+
const install = GLOBAL_INSTALL[packageManager];
|
|
3876
|
+
return {
|
|
3877
|
+
command: install.command,
|
|
3878
|
+
args: [...install.args, spec]
|
|
3879
|
+
};
|
|
3880
|
+
}
|
|
3661
3881
|
function mbInstallCommand(packageManager) {
|
|
3662
|
-
return
|
|
3882
|
+
return globalInstallCommand(packageManager, MB_INSTALL_SPEC);
|
|
3883
|
+
}
|
|
3884
|
+
function ownerByMarker(path, markers) {
|
|
3885
|
+
const normalized = path.replaceAll("\\", "/");
|
|
3886
|
+
const found = markers.find(({ marker }) => normalized.includes(marker));
|
|
3887
|
+
return found === void 0 ? null : found.owner;
|
|
3663
3888
|
}
|
|
3664
3889
|
const GLOBAL_MARKERS = [
|
|
3665
3890
|
{
|
|
3666
3891
|
marker: "/.bun/install/global/node_modules/",
|
|
3667
|
-
|
|
3892
|
+
owner: "bun"
|
|
3668
3893
|
},
|
|
3669
3894
|
{
|
|
3670
3895
|
marker: "/Library/pnpm/global/",
|
|
3671
|
-
|
|
3896
|
+
owner: "pnpm"
|
|
3672
3897
|
},
|
|
3673
3898
|
{
|
|
3674
3899
|
marker: "/.local/share/pnpm/global/",
|
|
3675
|
-
|
|
3900
|
+
owner: "pnpm"
|
|
3676
3901
|
},
|
|
3677
3902
|
{
|
|
3678
3903
|
marker: "/share/pnpm/global/",
|
|
3679
|
-
|
|
3904
|
+
owner: "pnpm"
|
|
3680
3905
|
},
|
|
3681
3906
|
{
|
|
3682
3907
|
marker: "/pnpm-global/node_modules/",
|
|
3683
|
-
|
|
3908
|
+
owner: "pnpm"
|
|
3684
3909
|
},
|
|
3685
3910
|
{
|
|
3686
3911
|
marker: "/.config/yarn/global/node_modules/",
|
|
3687
|
-
|
|
3912
|
+
owner: "yarn"
|
|
3688
3913
|
},
|
|
3689
3914
|
{
|
|
3690
3915
|
marker: "/lib/node_modules/",
|
|
3691
|
-
|
|
3916
|
+
owner: "npm"
|
|
3692
3917
|
},
|
|
3693
3918
|
{
|
|
3694
3919
|
marker: "/.npm-global/node_modules/",
|
|
3695
|
-
|
|
3920
|
+
owner: "npm"
|
|
3696
3921
|
}
|
|
3697
3922
|
];
|
|
3698
3923
|
function packageManagerFor(scriptPath) {
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3924
|
+
return ownerByMarker(scriptPath, GLOBAL_MARKERS) ?? "npm";
|
|
3925
|
+
}
|
|
3926
|
+
const MB_OWNER_NAMES = {
|
|
3927
|
+
npm: "npm",
|
|
3928
|
+
pnpm: "pnpm",
|
|
3929
|
+
yarn: "Yarn",
|
|
3930
|
+
bun: "Bun",
|
|
3931
|
+
homebrew: "Homebrew"
|
|
3932
|
+
};
|
|
3933
|
+
const CELLAR = "/Cellar/";
|
|
3934
|
+
const NODE_MODULES = "/node_modules/";
|
|
3935
|
+
const OWNER_MARKERS = [
|
|
3936
|
+
...GLOBAL_MARKERS,
|
|
3937
|
+
{
|
|
3938
|
+
marker: CELLAR,
|
|
3939
|
+
owner: "homebrew"
|
|
3940
|
+
},
|
|
3941
|
+
{
|
|
3942
|
+
marker: "/Library/pnpm/",
|
|
3943
|
+
owner: "pnpm"
|
|
3944
|
+
},
|
|
3945
|
+
{
|
|
3946
|
+
marker: "/.local/share/pnpm/",
|
|
3947
|
+
owner: "pnpm"
|
|
3948
|
+
}
|
|
3949
|
+
];
|
|
3950
|
+
async function locateMbOnPath() {
|
|
3951
|
+
const paths = await whichAll(MB);
|
|
3952
|
+
return Promise.all(paths.map(async (path) => ({
|
|
3953
|
+
path,
|
|
3954
|
+
resolved: await promises.realpath(path)
|
|
3955
|
+
})));
|
|
3956
|
+
}
|
|
3957
|
+
function mbOwner(binary) {
|
|
3958
|
+
return ownerByMarker(binary.resolved, OWNER_MARKERS) ?? ownerByMarker(binary.path, OWNER_MARKERS);
|
|
3959
|
+
}
|
|
3960
|
+
function firstSegmentAfter(path, marker) {
|
|
3961
|
+
const at = path.lastIndexOf(marker);
|
|
3962
|
+
if (at === -1) return null;
|
|
3963
|
+
const [first] = path.slice(at + marker.length).split("/");
|
|
3964
|
+
return first === void 0 || first.length === 0 ? null : first;
|
|
3965
|
+
}
|
|
3966
|
+
function packageName(resolved) {
|
|
3967
|
+
const scopeOrName = firstSegmentAfter(resolved, NODE_MODULES);
|
|
3968
|
+
if (scopeOrName === null || !scopeOrName.startsWith("@")) return scopeOrName;
|
|
3969
|
+
const name = firstSegmentAfter(resolved, `${NODE_MODULES}${scopeOrName}/`);
|
|
3970
|
+
return name === null ? null : `${scopeOrName}/${name}`;
|
|
3971
|
+
}
|
|
3972
|
+
const REMOVE_COMMANDS = {
|
|
3973
|
+
npm: (name) => `npm uninstall -g ${name}`,
|
|
3974
|
+
pnpm: (name) => `pnpm remove -g ${name}`,
|
|
3975
|
+
yarn: (name) => `yarn global remove ${name}`,
|
|
3976
|
+
bun: (name) => `bun remove -g ${name}`,
|
|
3977
|
+
homebrew: (name) => `brew uninstall ${name}`
|
|
3978
|
+
};
|
|
3979
|
+
function mbRemoveCommand(binary) {
|
|
3980
|
+
const owner = mbOwner(binary);
|
|
3981
|
+
if (owner === null) return null;
|
|
3982
|
+
const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
|
|
3983
|
+
return name === null ? null : REMOVE_COMMANDS[owner](name);
|
|
3984
|
+
}
|
|
3985
|
+
function setValue(name) {
|
|
3986
|
+
const value = readEnv(name);
|
|
3987
|
+
return value === void 0 || value.length === 0 ? null : value;
|
|
3988
|
+
}
|
|
3989
|
+
function mbEnvironment() {
|
|
3990
|
+
return {
|
|
3991
|
+
profile: setValue(ENV_MB_PROFILE),
|
|
3992
|
+
url: setValue(ENV_MB_URL),
|
|
3993
|
+
apiKeySet: setValue(ENV_MB_API_KEY) !== null
|
|
3994
|
+
};
|
|
3702
3995
|
}
|
|
3703
3996
|
const ProbedUser = z.object({
|
|
3704
3997
|
id: z.int(),
|
|
@@ -3775,16 +4068,16 @@ function mbTool(runner) {
|
|
|
3775
4068
|
if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
|
|
3776
4069
|
return parseJsonOutput(MB, argv, result.stdout, schema);
|
|
3777
4070
|
}
|
|
3778
|
-
async function version() {
|
|
4071
|
+
async function version(command = MB) {
|
|
3779
4072
|
const args = ["--version"];
|
|
3780
4073
|
let result;
|
|
3781
4074
|
try {
|
|
3782
|
-
result = await runner.run(
|
|
4075
|
+
result = await runner.run(command, args);
|
|
3783
4076
|
} catch (error) {
|
|
3784
4077
|
if (error instanceof ProcessNotFoundError) return null;
|
|
3785
4078
|
throw error;
|
|
3786
4079
|
}
|
|
3787
|
-
if (result.exitCode !== 0) throw toolFailure(
|
|
4080
|
+
if (result.exitCode !== 0) throw toolFailure(command, args, result);
|
|
3788
4081
|
return result.stdout.trim();
|
|
3789
4082
|
}
|
|
3790
4083
|
async function authLogin(options) {
|
|
@@ -3870,65 +4163,107 @@ function mbTool(runner) {
|
|
|
3870
4163
|
};
|
|
3871
4164
|
}
|
|
3872
4165
|
//#endregion
|
|
3873
|
-
//#region src/
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
4166
|
+
//#region src/tools/agents.ts
|
|
4167
|
+
const AgentId = z.enum([
|
|
4168
|
+
"claude-code",
|
|
4169
|
+
"codex",
|
|
4170
|
+
"cursor",
|
|
4171
|
+
"gemini-cli",
|
|
4172
|
+
"opencode",
|
|
4173
|
+
"windsurf"
|
|
4174
|
+
]);
|
|
4175
|
+
const OPENCODE_PACKAGE = "opencode-ai@latest";
|
|
4176
|
+
function opencodeInstallCommand(packageManager) {
|
|
4177
|
+
return globalInstallCommand(packageManager, OPENCODE_PACKAGE);
|
|
3877
4178
|
}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
4179
|
+
const AGENTS = [
|
|
4180
|
+
{
|
|
4181
|
+
id: "claude-code",
|
|
4182
|
+
displayName: "Claude Code",
|
|
4183
|
+
skillsCliName: "claude-code",
|
|
4184
|
+
command: "claude",
|
|
4185
|
+
launch: "claude",
|
|
4186
|
+
configDir: (home) => join(home, ".claude")
|
|
4187
|
+
},
|
|
4188
|
+
{
|
|
4189
|
+
id: "codex",
|
|
4190
|
+
displayName: "Codex",
|
|
4191
|
+
skillsCliName: "codex",
|
|
4192
|
+
command: "codex",
|
|
4193
|
+
launch: "codex",
|
|
4194
|
+
configDir: (home) => join(home, ".codex")
|
|
4195
|
+
},
|
|
4196
|
+
{
|
|
4197
|
+
id: "cursor",
|
|
4198
|
+
displayName: "Cursor",
|
|
4199
|
+
skillsCliName: "cursor",
|
|
4200
|
+
command: "cursor",
|
|
4201
|
+
launch: "cursor .",
|
|
4202
|
+
configDir: (home) => join(home, ".cursor")
|
|
4203
|
+
},
|
|
4204
|
+
{
|
|
4205
|
+
id: "gemini-cli",
|
|
4206
|
+
displayName: "Gemini CLI",
|
|
4207
|
+
skillsCliName: "gemini-cli",
|
|
4208
|
+
command: "gemini",
|
|
4209
|
+
launch: "gemini",
|
|
4210
|
+
configDir: (home) => join(home, ".gemini")
|
|
4211
|
+
},
|
|
4212
|
+
{
|
|
4213
|
+
id: "opencode",
|
|
4214
|
+
displayName: "OpenCode",
|
|
4215
|
+
skillsCliName: "opencode",
|
|
4216
|
+
command: "opencode",
|
|
4217
|
+
launch: "opencode",
|
|
4218
|
+
configDir: (_home, configHome) => join(configHome, "opencode")
|
|
4219
|
+
},
|
|
4220
|
+
{
|
|
4221
|
+
id: "windsurf",
|
|
4222
|
+
displayName: "Windsurf",
|
|
4223
|
+
skillsCliName: "windsurf",
|
|
4224
|
+
command: null,
|
|
4225
|
+
launch: "windsurf .",
|
|
4226
|
+
configDir: (home) => join(home, ".codeium", "windsurf")
|
|
4227
|
+
}
|
|
4228
|
+
];
|
|
4229
|
+
async function directoryExists$1(path) {
|
|
4230
|
+
try {
|
|
4231
|
+
return (await promises.stat(path)).isDirectory();
|
|
4232
|
+
} catch {
|
|
4233
|
+
return false;
|
|
3889
4234
|
}
|
|
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
4235
|
}
|
|
3897
|
-
function
|
|
4236
|
+
function hostAgentProbe() {
|
|
4237
|
+
const home = homedir();
|
|
3898
4238
|
return {
|
|
3899
|
-
|
|
3900
|
-
|
|
4239
|
+
commandExists: async (command) => await whichCommand(command) !== null,
|
|
4240
|
+
directoryExists: directoryExists$1,
|
|
4241
|
+
home,
|
|
4242
|
+
configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
|
|
3901
4243
|
};
|
|
3902
4244
|
}
|
|
3903
|
-
function
|
|
3904
|
-
return
|
|
3905
|
-
|
|
3906
|
-
steps: {
|
|
3907
|
-
...state.steps,
|
|
3908
|
-
[id]: record
|
|
3909
|
-
}
|
|
3910
|
-
};
|
|
4245
|
+
async function isDetected(agent, probe) {
|
|
4246
|
+
if (agent.command !== null && await probe.commandExists(agent.command)) return true;
|
|
4247
|
+
return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
|
|
3911
4248
|
}
|
|
3912
|
-
async function
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
status
|
|
4249
|
+
async function detectAgents(probe) {
|
|
4250
|
+
return Promise.all(AGENTS.map(async (agent) => ({
|
|
4251
|
+
id: agent.id,
|
|
4252
|
+
displayName: agent.displayName,
|
|
4253
|
+
skillsCliName: agent.skillsCliName,
|
|
4254
|
+
launch: agent.launch,
|
|
4255
|
+
detected: await isDetected(agent, probe)
|
|
3920
4256
|
})));
|
|
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
4257
|
}
|
|
3927
4258
|
//#endregion
|
|
3928
4259
|
//#region src/tools/skills-cli.ts
|
|
3929
4260
|
const NPX = "npx";
|
|
3930
4261
|
const SKILLS_PACKAGE = "skills@latest";
|
|
3931
|
-
const
|
|
4262
|
+
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-skill";
|
|
4263
|
+
function rdeSkillSource() {
|
|
4264
|
+
const override = readEnv(ENV_RDE_SKILL_SOURCE);
|
|
4265
|
+
return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
|
|
4266
|
+
}
|
|
3932
4267
|
const InstalledSkill = z.object({
|
|
3933
4268
|
name: z.string(),
|
|
3934
4269
|
path: z.string(),
|
|
@@ -3937,15 +4272,58 @@ const InstalledSkill = z.object({
|
|
|
3937
4272
|
source: z.string().nullable()
|
|
3938
4273
|
}).loose();
|
|
3939
4274
|
const InstalledSkillList = z.array(InstalledSkill);
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3943
|
-
|
|
3944
|
-
|
|
3945
|
-
|
|
3946
|
-
|
|
3947
|
-
|
|
3948
|
-
|
|
4275
|
+
const SkillLock = z.object({ skills: z.record(z.string(), z.object({ ref: z.string().nullish() }).loose()) }).loose();
|
|
4276
|
+
const GLOBAL_LOCK_FILE = ".skill-lock.json";
|
|
4277
|
+
const PROJECT_LOCK_FILE = "skills-lock.json";
|
|
4278
|
+
function globalSkillLockPath(home, stateHome) {
|
|
4279
|
+
return stateHome === null ? join(home, ".agents", GLOBAL_LOCK_FILE) : join(stateHome, "skills", GLOBAL_LOCK_FILE);
|
|
4280
|
+
}
|
|
4281
|
+
function projectSkillLockPath(project) {
|
|
4282
|
+
return join(project, PROJECT_LOCK_FILE);
|
|
4283
|
+
}
|
|
4284
|
+
function xdgStateHome() {
|
|
4285
|
+
const value = readEnv(ENV_XDG_STATE_HOME);
|
|
4286
|
+
return value === void 0 || value.length === 0 ? null : value;
|
|
4287
|
+
}
|
|
4288
|
+
function lockedRef(path, raw, skill) {
|
|
4289
|
+
if (raw === null) return null;
|
|
4290
|
+
const parsed = SkillLock.safeParse(raw);
|
|
4291
|
+
if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
|
|
4292
|
+
return parsed.data.skills[skill]?.ref ?? null;
|
|
4293
|
+
}
|
|
4294
|
+
const hostSkillFiles = {
|
|
4295
|
+
readJson: readJsonFile,
|
|
4296
|
+
readText: readTextFile
|
|
4297
|
+
};
|
|
4298
|
+
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
4299
|
+
function skillOrigin(source, cwd) {
|
|
4300
|
+
const github = GITHUB_URL.exec(source);
|
|
4301
|
+
if (github !== null) {
|
|
4302
|
+
const [, owner, repo, ref] = github;
|
|
4303
|
+
return {
|
|
4304
|
+
kind: "github",
|
|
4305
|
+
repository: `${owner}/${repo}`,
|
|
4306
|
+
ref: ref ?? null
|
|
4307
|
+
};
|
|
4308
|
+
}
|
|
4309
|
+
if (isAbsolute(source) || source.startsWith(".")) return {
|
|
4310
|
+
kind: "local",
|
|
4311
|
+
path: resolve(cwd, source)
|
|
4312
|
+
};
|
|
4313
|
+
return {
|
|
4314
|
+
kind: "other",
|
|
4315
|
+
source
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
function skillsCli(runner) {
|
|
4319
|
+
async function add(options) {
|
|
4320
|
+
const argv = [
|
|
4321
|
+
"-y",
|
|
4322
|
+
SKILLS_PACKAGE,
|
|
4323
|
+
"add",
|
|
4324
|
+
options.source,
|
|
4325
|
+
"--skill",
|
|
4326
|
+
options.skill,
|
|
3949
4327
|
"-a",
|
|
3950
4328
|
...options.agents,
|
|
3951
4329
|
"-g",
|
|
@@ -3988,97 +4366,85 @@ function skillsCli(runner) {
|
|
|
3988
4366
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
3989
4367
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
3990
4368
|
}
|
|
4369
|
+
async function listProject(cwd) {
|
|
4370
|
+
const argv = [
|
|
4371
|
+
"-y",
|
|
4372
|
+
SKILLS_PACKAGE,
|
|
4373
|
+
"list",
|
|
4374
|
+
"--json"
|
|
4375
|
+
];
|
|
4376
|
+
const result = await runner.run(NPX, argv, {
|
|
4377
|
+
cwd,
|
|
4378
|
+
stdoutToFile: true
|
|
4379
|
+
});
|
|
4380
|
+
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
4381
|
+
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
4382
|
+
}
|
|
3991
4383
|
return {
|
|
3992
4384
|
add,
|
|
3993
4385
|
remove,
|
|
3994
|
-
list
|
|
4386
|
+
list,
|
|
4387
|
+
listProject
|
|
3995
4388
|
};
|
|
3996
4389
|
}
|
|
3997
4390
|
//#endregion
|
|
3998
|
-
//#region src/
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4002
|
-
function hostDeps$11(ctx) {
|
|
4003
|
-
return {
|
|
4004
|
-
runner: processRunner(ctx.signal),
|
|
4005
|
-
probe: hostAgentProbe()
|
|
4006
|
-
};
|
|
4007
|
-
}
|
|
4008
|
-
function detectedOnly(agents) {
|
|
4009
|
-
return agents.filter((agent) => agent.detected);
|
|
4010
|
-
}
|
|
4011
|
-
function displayNames(agents) {
|
|
4012
|
-
return agents.map((agent) => agent.displayName).join(", ");
|
|
4013
|
-
}
|
|
4014
|
-
function skillCoversAgent(entry, agent) {
|
|
4015
|
-
return entry.agents.includes(agent.displayName);
|
|
4016
|
-
}
|
|
4017
|
-
function coversAny(entry, agents) {
|
|
4018
|
-
return agents.some((agent) => skillCoversAgent(entry, agent));
|
|
4391
|
+
//#region src/wizard/engine.ts
|
|
4392
|
+
function requireKnownStep(steps, id, flag) {
|
|
4393
|
+
if (id === void 0) return;
|
|
4394
|
+
if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
|
|
4019
4395
|
}
|
|
4020
|
-
async function
|
|
4021
|
-
|
|
4396
|
+
async function detectAll(steps, ctx, options) {
|
|
4397
|
+
const detected = [];
|
|
4398
|
+
let fromHere = false;
|
|
4399
|
+
for (const step of steps) {
|
|
4400
|
+
if (step.id === options.from) fromHere = true;
|
|
4401
|
+
const forced = options.only !== void 0 ? step.id === options.only : fromHere;
|
|
4402
|
+
detected.push({
|
|
4403
|
+
step,
|
|
4404
|
+
status: forced ? "todo" : await step.detect(ctx),
|
|
4405
|
+
forced
|
|
4406
|
+
});
|
|
4407
|
+
}
|
|
4408
|
+
return detected;
|
|
4022
4409
|
}
|
|
4023
|
-
function
|
|
4024
|
-
|
|
4410
|
+
async function shouldRun(entry, ctx, options) {
|
|
4411
|
+
if (entry.forced) return true;
|
|
4412
|
+
if (options.only !== void 0 || entry.status === "done") return false;
|
|
4413
|
+
return await entry.step.detect(ctx) === "todo";
|
|
4025
4414
|
}
|
|
4026
|
-
|
|
4027
|
-
override: "from --agents",
|
|
4028
|
-
detected: "detected on this machine"
|
|
4029
|
-
};
|
|
4030
|
-
function selectAgents(all, options) {
|
|
4031
|
-
if (options.agents === null) return {
|
|
4032
|
-
agents: detectedOnly(all),
|
|
4033
|
-
source: "detected"
|
|
4034
|
-
};
|
|
4035
|
-
const chosen = options.agents;
|
|
4415
|
+
function recordFor(outcome, at) {
|
|
4036
4416
|
return {
|
|
4037
|
-
|
|
4038
|
-
|
|
4417
|
+
status: outcome === "completed" ? "done" : "skipped",
|
|
4418
|
+
completedAt: at.toISOString()
|
|
4039
4419
|
};
|
|
4040
4420
|
}
|
|
4041
|
-
function
|
|
4042
|
-
switch (source) {
|
|
4043
|
-
case "override": return `Skipping the rde skill; run \`rde init --only ${STEP_ID$8}\` to install it later`;
|
|
4044
|
-
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$8} --agents <name>\` once one is installed`;
|
|
4045
|
-
}
|
|
4046
|
-
}
|
|
4047
|
-
function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
4421
|
+
function withStepRecord(state, id, record) {
|
|
4048
4422
|
return {
|
|
4049
|
-
|
|
4050
|
-
|
|
4051
|
-
|
|
4052
|
-
|
|
4053
|
-
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
4054
|
-
if (detected.length === 0) return ctx.state.steps[STEP_ID$8]?.status === "skipped" ? "not-applicable" : "todo";
|
|
4055
|
-
return (await rdeSkillEntries(resolved.runner)).some((entry) => coversAny(entry, detected)) ? "done" : "todo";
|
|
4056
|
-
},
|
|
4057
|
-
async run(ctx) {
|
|
4058
|
-
const resolved = deps(ctx);
|
|
4059
|
-
const all = await detectAgents(resolved.probe);
|
|
4060
|
-
const selection = selectAgents(all, options);
|
|
4061
|
-
if (selection.agents.length === 0) {
|
|
4062
|
-
ctx.ui.info(skipLine(all, selection.source));
|
|
4063
|
-
return "skipped";
|
|
4064
|
-
}
|
|
4065
|
-
ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`);
|
|
4066
|
-
await skillsCli(resolved.runner).add({
|
|
4067
|
-
source: RDE_SKILL_SOURCE,
|
|
4068
|
-
skill: "rde",
|
|
4069
|
-
agents: selection.agents.map((agent) => agent.skillsCliName)
|
|
4070
|
-
});
|
|
4071
|
-
const entries = await rdeSkillEntries(resolved.runner);
|
|
4072
|
-
if (entries.length === 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show the rde skill`);
|
|
4073
|
-
ctx.ui.note(installedLines(entries), "rde skill installed");
|
|
4074
|
-
ctx.ui.info(MECHANICS_NOTE);
|
|
4075
|
-
return "completed";
|
|
4423
|
+
...state,
|
|
4424
|
+
steps: {
|
|
4425
|
+
...state.steps,
|
|
4426
|
+
[id]: record
|
|
4076
4427
|
}
|
|
4077
4428
|
};
|
|
4078
4429
|
}
|
|
4430
|
+
async function runWizard(steps, ctx, options = {}) {
|
|
4431
|
+
requireKnownStep(steps, options.from, "--from");
|
|
4432
|
+
requireKnownStep(steps, options.only, "--only");
|
|
4433
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4434
|
+
const detected = await detectAll(steps, ctx, options);
|
|
4435
|
+
ctx.ui.checklist(detected.map(({ step, status }) => ({
|
|
4436
|
+
title: step.title,
|
|
4437
|
+
status
|
|
4438
|
+
})));
|
|
4439
|
+
for (const entry of detected) {
|
|
4440
|
+
if (!await shouldRun(entry, ctx, options)) continue;
|
|
4441
|
+
const outcome = await entry.step.run(ctx);
|
|
4442
|
+
await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
|
|
4443
|
+
}
|
|
4444
|
+
}
|
|
4079
4445
|
//#endregion
|
|
4080
4446
|
//#region src/steps/install-mb.ts
|
|
4081
|
-
const STEP_ID$
|
|
4447
|
+
const STEP_ID$8 = "install-mb";
|
|
4082
4448
|
const MIN_MB_VERSION = "0.3.0";
|
|
4083
4449
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
4084
4450
|
function installedWith(scriptPath) {
|
|
@@ -4087,10 +4453,11 @@ function installedWith(scriptPath) {
|
|
|
4087
4453
|
function rdeInstalledWith() {
|
|
4088
4454
|
return installedWith(process.argv[1]);
|
|
4089
4455
|
}
|
|
4090
|
-
function hostDeps$
|
|
4456
|
+
function hostDeps$11(ctx) {
|
|
4091
4457
|
return {
|
|
4092
4458
|
runner: processRunner(ctx.signal),
|
|
4093
|
-
installedWith: rdeInstalledWith()
|
|
4459
|
+
installedWith: rdeInstalledWith(),
|
|
4460
|
+
locate: locateMbOnPath
|
|
4094
4461
|
};
|
|
4095
4462
|
}
|
|
4096
4463
|
async function inspectMb(runner) {
|
|
@@ -4107,18 +4474,104 @@ async function inspectMb(runner) {
|
|
|
4107
4474
|
function displayCommand(install) {
|
|
4108
4475
|
return [install.command, ...install.args].join(" ");
|
|
4109
4476
|
}
|
|
4110
|
-
function
|
|
4111
|
-
|
|
4112
|
-
|
|
4477
|
+
async function versionAt(runner, path) {
|
|
4478
|
+
try {
|
|
4479
|
+
const raw = await mbTool(runner).version(path);
|
|
4480
|
+
return raw === null ? null : semver.valid(raw);
|
|
4481
|
+
} catch (error) {
|
|
4482
|
+
if (error instanceof ExternalToolError) return null;
|
|
4483
|
+
throw error;
|
|
4484
|
+
}
|
|
4485
|
+
}
|
|
4486
|
+
async function inspectMbOnPath(runner, locate) {
|
|
4487
|
+
const found = [];
|
|
4488
|
+
for (const binary of await locate()) found.push({
|
|
4489
|
+
path: binary.path,
|
|
4490
|
+
version: await versionAt(runner, binary.path),
|
|
4491
|
+
owner: mbOwner(binary),
|
|
4492
|
+
removeCommand: mbRemoveCommand(binary)
|
|
4493
|
+
});
|
|
4494
|
+
return found;
|
|
4495
|
+
}
|
|
4496
|
+
function satisfiesMinimum(version) {
|
|
4497
|
+
return version !== null && semver.satisfies(version, MB_RANGE, { includePrerelease: true });
|
|
4498
|
+
}
|
|
4499
|
+
function isNewer(candidate, than) {
|
|
4500
|
+
if (candidate === null) return false;
|
|
4501
|
+
return than === null || semver.gt(candidate, than);
|
|
4502
|
+
}
|
|
4503
|
+
function diagnoseMb(found, installer) {
|
|
4504
|
+
const [running, ...later] = found;
|
|
4505
|
+
if (running === void 0) return { kind: "missing" };
|
|
4506
|
+
const sufficient = satisfiesMinimum(running.version);
|
|
4507
|
+
const newer = later.find((candidate) => satisfiesMinimum(candidate.version) && isNewer(candidate.version, running.version));
|
|
4508
|
+
if (newer !== void 0 && (!sufficient || running.owner !== installer)) return {
|
|
4509
|
+
kind: "shadowed",
|
|
4510
|
+
running,
|
|
4511
|
+
newer
|
|
4512
|
+
};
|
|
4513
|
+
return sufficient ? {
|
|
4514
|
+
kind: "current",
|
|
4515
|
+
running
|
|
4516
|
+
} : {
|
|
4517
|
+
kind: "old",
|
|
4518
|
+
running
|
|
4519
|
+
};
|
|
4520
|
+
}
|
|
4521
|
+
function runningMbSuffices(diagnosis) {
|
|
4522
|
+
return diagnosis.kind !== "missing" && satisfiesMinimum(diagnosis.running.version);
|
|
4523
|
+
}
|
|
4524
|
+
function versionText(entry) {
|
|
4525
|
+
return entry.version ?? "an unknown version";
|
|
4526
|
+
}
|
|
4527
|
+
function describeEntry(entry) {
|
|
4528
|
+
const owner = entry.owner === null ? "" : ` (${MB_OWNER_NAMES[entry.owner]})`;
|
|
4529
|
+
return `${versionText(entry)} at ${entry.path}${owner}`;
|
|
4530
|
+
}
|
|
4531
|
+
function mbPathLines(found) {
|
|
4532
|
+
return found.map((entry, index) => `${index === 0 ? "" : "then "}${describeEntry(entry)}`);
|
|
4533
|
+
}
|
|
4534
|
+
function removeClause(entry) {
|
|
4535
|
+
if (entry.removeCommand !== null) return `remove the old one with \`${entry.removeCommand}\``;
|
|
4536
|
+
if (entry.owner !== null) return `remove the old one with ${MB_OWNER_NAMES[entry.owner]}`;
|
|
4537
|
+
return `remove ${entry.path}`;
|
|
4538
|
+
}
|
|
4539
|
+
function shadowedHint(diagnosis) {
|
|
4540
|
+
const { running, newer } = diagnosis;
|
|
4541
|
+
return [
|
|
4542
|
+
`${running.path} (${versionText(running)}) runs before ${newer.path} (${versionText(newer)});`,
|
|
4543
|
+
`${removeClause(running)}, or put ${dirname(newer.path)} before ${dirname(running.path)} on PATH,`,
|
|
4544
|
+
"then open a new terminal"
|
|
4545
|
+
].join(" ");
|
|
4546
|
+
}
|
|
4547
|
+
const NEW_TERMINAL = "open a new terminal and run rde init again";
|
|
4548
|
+
function stillWrongAdvice(diagnosis) {
|
|
4549
|
+
switch (diagnosis.kind) {
|
|
4550
|
+
case "shadowed": return `${shadowedHint(diagnosis)} and run rde init again`;
|
|
4551
|
+
case "old": return `The install put no newer mb on PATH; ${removeClause(diagnosis.running)}, or ${NEW_TERMINAL}`;
|
|
4552
|
+
case "missing":
|
|
4553
|
+
case "current": return "Open a new terminal and run rde init again";
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
async function stillWrong(install, after, deps) {
|
|
4557
|
+
const fail = (stderrTail) => new ExternalToolError({
|
|
4113
4558
|
tool: install.command,
|
|
4114
4559
|
argv: install.args,
|
|
4115
4560
|
exitCode: 0,
|
|
4116
|
-
stderrTail
|
|
4561
|
+
stderrTail
|
|
4117
4562
|
});
|
|
4118
|
-
}
|
|
4119
|
-
|
|
4563
|
+
if (!after.installed) return fail(`mb is not on PATH after the install; ${NEW_TERMINAL}`);
|
|
4564
|
+
const found = await inspectMbOnPath(deps.runner, deps.locate);
|
|
4565
|
+
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
4566
|
+
return fail([
|
|
4567
|
+
`mb ${after.version} still runs first (${MIN_MB_VERSION} or newer is required).`,
|
|
4568
|
+
`Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.`,
|
|
4569
|
+
`${stillWrongAdvice(diagnosis)}.`
|
|
4570
|
+
].join(" "));
|
|
4571
|
+
}
|
|
4572
|
+
function installMbStep(deps = hostDeps$11) {
|
|
4120
4573
|
return {
|
|
4121
|
-
id: STEP_ID$
|
|
4574
|
+
id: STEP_ID$8,
|
|
4122
4575
|
title: "Install the Metabase CLI",
|
|
4123
4576
|
async detect(ctx) {
|
|
4124
4577
|
const found = await inspectMb(deps(ctx).runner);
|
|
@@ -4142,13 +4595,145 @@ function installMbStep(deps = hostDeps$10) {
|
|
|
4142
4595
|
stderrTail: ""
|
|
4143
4596
|
});
|
|
4144
4597
|
const after = await inspectMb(resolved.runner);
|
|
4145
|
-
if (!after.installed || !after.satisfies) throw stillWrong(install, after);
|
|
4598
|
+
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
4146
4599
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
4147
4600
|
return "completed";
|
|
4148
4601
|
}
|
|
4149
4602
|
};
|
|
4150
4603
|
}
|
|
4151
4604
|
//#endregion
|
|
4605
|
+
//#region src/steps/agents-skills.ts
|
|
4606
|
+
const STEP_ID$7 = "agents-skills";
|
|
4607
|
+
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
|
|
4608
|
+
const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
4609
|
+
const DETECT_AGENTS = { kind: "detected" };
|
|
4610
|
+
function hostDeps$10(ctx) {
|
|
4611
|
+
return {
|
|
4612
|
+
runner: processRunner(ctx.signal),
|
|
4613
|
+
probe: hostAgentProbe(),
|
|
4614
|
+
skillSource: rdeSkillSource(),
|
|
4615
|
+
installedWith: rdeInstalledWith()
|
|
4616
|
+
};
|
|
4617
|
+
}
|
|
4618
|
+
function detectedOnly(agents) {
|
|
4619
|
+
return agents.filter((agent) => agent.detected);
|
|
4620
|
+
}
|
|
4621
|
+
function displayNames(agents) {
|
|
4622
|
+
return agents.map((agent) => agent.displayName).join(", ");
|
|
4623
|
+
}
|
|
4624
|
+
function skillCoversAgent(entry, agent) {
|
|
4625
|
+
return entry.agents.includes(agent.displayName);
|
|
4626
|
+
}
|
|
4627
|
+
function coversAny(entry, agents) {
|
|
4628
|
+
return agents.some((agent) => skillCoversAgent(entry, agent));
|
|
4629
|
+
}
|
|
4630
|
+
async function rdeSkillEntries(runner) {
|
|
4631
|
+
return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
|
|
4632
|
+
}
|
|
4633
|
+
function installedLines(entries) {
|
|
4634
|
+
return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
|
|
4635
|
+
}
|
|
4636
|
+
const SOURCE_NOTES = {
|
|
4637
|
+
override: "from --agents",
|
|
4638
|
+
detected: "detected on this machine",
|
|
4639
|
+
chosen: "as chosen"
|
|
4640
|
+
};
|
|
4641
|
+
const MISSING_OPENCODE_HINT = "not installed; rde installs it";
|
|
4642
|
+
function isMissingOpencode(agent) {
|
|
4643
|
+
return agent.id === "opencode" && !agent.detected;
|
|
4644
|
+
}
|
|
4645
|
+
function offeredChoice(agent) {
|
|
4646
|
+
return {
|
|
4647
|
+
value: agent.id,
|
|
4648
|
+
label: agent.displayName,
|
|
4649
|
+
...isMissingOpencode(agent) && { hint: MISSING_OPENCODE_HINT }
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4652
|
+
async function askAgents(ctx, all) {
|
|
4653
|
+
const offered = all.filter((agent) => agent.detected || agent.id === "opencode");
|
|
4654
|
+
const picked = await ctx.prompts.multiselect({
|
|
4655
|
+
key: AGENTS_SKILLS_KEYS.agents,
|
|
4656
|
+
message: "Which coding agents should get the rde skill?",
|
|
4657
|
+
choices: offered.map(offeredChoice),
|
|
4658
|
+
initialValues: detectedOnly(all).map((agent) => agent.id),
|
|
4659
|
+
required: false
|
|
4660
|
+
});
|
|
4661
|
+
return offered.filter((agent) => picked.includes(agent.id));
|
|
4662
|
+
}
|
|
4663
|
+
async function selectAgents(ctx, all, options) {
|
|
4664
|
+
switch (options.kind) {
|
|
4665
|
+
case "detected": return {
|
|
4666
|
+
agents: detectedOnly(all),
|
|
4667
|
+
source: "detected"
|
|
4668
|
+
};
|
|
4669
|
+
case "listed": {
|
|
4670
|
+
const listed = options.agents;
|
|
4671
|
+
return {
|
|
4672
|
+
agents: all.filter((agent) => listed.includes(agent.id)),
|
|
4673
|
+
source: "override"
|
|
4674
|
+
};
|
|
4675
|
+
}
|
|
4676
|
+
case "ask": return {
|
|
4677
|
+
agents: await askAgents(ctx, all),
|
|
4678
|
+
source: "chosen"
|
|
4679
|
+
};
|
|
4680
|
+
}
|
|
4681
|
+
}
|
|
4682
|
+
async function installOpencode(ctx, deps) {
|
|
4683
|
+
const install = opencodeInstallCommand(deps.installedWith);
|
|
4684
|
+
const commandLine = [install.command, ...install.args].join(" ");
|
|
4685
|
+
ctx.ui.info(`Running ${commandLine}`);
|
|
4686
|
+
const exitCode = await deps.runner.stream(install.command, install.args);
|
|
4687
|
+
if (exitCode !== 0) throw new ConfigError(`\`${commandLine}\` exited with ${exitCode}`);
|
|
4688
|
+
if (!(await detectAgents(deps.probe)).some((agent) => agent.id === "opencode" && agent.detected)) throw new ConfigError("OpenCode was installed but `opencode` is not on PATH; open a new terminal and run rde init again");
|
|
4689
|
+
}
|
|
4690
|
+
function listsMissingOpencode(all, options) {
|
|
4691
|
+
return options.kind === "listed" && options.agents.includes("opencode") && all.some(isMissingOpencode);
|
|
4692
|
+
}
|
|
4693
|
+
function skipLine(all, source) {
|
|
4694
|
+
switch (source) {
|
|
4695
|
+
case "override":
|
|
4696
|
+
case "chosen": return `Skipping the rde skill; run \`rde init --only ${STEP_ID$7}\` to install it later`;
|
|
4697
|
+
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$7} --agents <name>\` once one is installed`;
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
function agentsSkillsStep(options, deps = hostDeps$10) {
|
|
4701
|
+
return {
|
|
4702
|
+
id: STEP_ID$7,
|
|
4703
|
+
title: "Install the rde skill for your coding agents",
|
|
4704
|
+
async detect(ctx) {
|
|
4705
|
+
const resolved = deps(ctx);
|
|
4706
|
+
const all = await detectAgents(resolved.probe);
|
|
4707
|
+
if (listsMissingOpencode(all, options)) return "todo";
|
|
4708
|
+
const detected = detectedOnly(all);
|
|
4709
|
+
if (detected.length === 0) return ctx.state.steps[STEP_ID$7]?.status === "skipped" ? "not-applicable" : "todo";
|
|
4710
|
+
return (await rdeSkillEntries(resolved.runner)).some((entry) => coversAny(entry, detected)) ? "done" : "todo";
|
|
4711
|
+
},
|
|
4712
|
+
async run(ctx) {
|
|
4713
|
+
const resolved = deps(ctx);
|
|
4714
|
+
const all = await detectAgents(resolved.probe);
|
|
4715
|
+
const selection = await selectAgents(ctx, all, options);
|
|
4716
|
+
if (selection.agents.length === 0) {
|
|
4717
|
+
ctx.ui.info(skipLine(all, selection.source));
|
|
4718
|
+
return "skipped";
|
|
4719
|
+
}
|
|
4720
|
+
if (selection.agents.some(isMissingOpencode)) await installOpencode(ctx, resolved);
|
|
4721
|
+
const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-skill" ? "" : ` from ${resolved.skillSource}`;
|
|
4722
|
+
ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
|
|
4723
|
+
await skillsCli(resolved.runner).add({
|
|
4724
|
+
source: resolved.skillSource,
|
|
4725
|
+
skill: "rde",
|
|
4726
|
+
agents: selection.agents.map((agent) => agent.skillsCliName)
|
|
4727
|
+
});
|
|
4728
|
+
const entries = await rdeSkillEntries(resolved.runner);
|
|
4729
|
+
if (entries.length === 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show the rde skill`);
|
|
4730
|
+
ctx.ui.note(installedLines(entries), "rde skill installed");
|
|
4731
|
+
ctx.ui.info(MECHANICS_NOTE);
|
|
4732
|
+
return "completed";
|
|
4733
|
+
}
|
|
4734
|
+
};
|
|
4735
|
+
}
|
|
4736
|
+
//#endregion
|
|
4152
4737
|
//#region src/tools/git.ts
|
|
4153
4738
|
const GIT = "git";
|
|
4154
4739
|
const FALLBACK_IDENTITY_NAME = "rde";
|
|
@@ -4463,7 +5048,15 @@ function hostDoctorDeps(instanceName, signal) {
|
|
|
4463
5048
|
runtime,
|
|
4464
5049
|
ports: hostPortProbe,
|
|
4465
5050
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
4466
|
-
probe: hostAgentProbe()
|
|
5051
|
+
probe: hostAgentProbe(),
|
|
5052
|
+
locateMb: locateMbOnPath,
|
|
5053
|
+
environment: mbEnvironment(),
|
|
5054
|
+
skills: {
|
|
5055
|
+
source: rdeSkillSource(),
|
|
5056
|
+
cwd: process.cwd(),
|
|
5057
|
+
stateHome: xdgStateHome(),
|
|
5058
|
+
files: hostSkillFiles
|
|
5059
|
+
}
|
|
4467
5060
|
};
|
|
4468
5061
|
}
|
|
4469
5062
|
const NOT_FOUND_STATUS$2 = 404;
|
|
@@ -4476,7 +5069,17 @@ function ok(id, label, detail) {
|
|
|
4476
5069
|
status: "ok",
|
|
4477
5070
|
detail,
|
|
4478
5071
|
hint: null,
|
|
4479
|
-
|
|
5072
|
+
step: null
|
|
5073
|
+
};
|
|
5074
|
+
}
|
|
5075
|
+
function note$1(id, label, detail, hint) {
|
|
5076
|
+
return {
|
|
5077
|
+
id,
|
|
5078
|
+
label,
|
|
5079
|
+
status: "ok",
|
|
5080
|
+
detail,
|
|
5081
|
+
hint,
|
|
5082
|
+
step: null
|
|
4480
5083
|
};
|
|
4481
5084
|
}
|
|
4482
5085
|
function skipped(id, label, detail) {
|
|
@@ -4486,7 +5089,7 @@ function skipped(id, label, detail) {
|
|
|
4486
5089
|
status: "skipped",
|
|
4487
5090
|
detail,
|
|
4488
5091
|
hint: null,
|
|
4489
|
-
|
|
5092
|
+
step: null
|
|
4490
5093
|
};
|
|
4491
5094
|
}
|
|
4492
5095
|
function failed(id, label, failure) {
|
|
@@ -4507,7 +5110,7 @@ function machineRow(check, needed) {
|
|
|
4507
5110
|
return failed(check.id, check.id, {
|
|
4508
5111
|
detail: check.label,
|
|
4509
5112
|
hint: check.hint,
|
|
4510
|
-
|
|
5113
|
+
step: "preflight"
|
|
4511
5114
|
});
|
|
4512
5115
|
}
|
|
4513
5116
|
async function machineRows(deps, runtime) {
|
|
@@ -4519,19 +5122,48 @@ function installHint(installedWith) {
|
|
|
4519
5122
|
const install = mbInstallCommand(installedWith);
|
|
4520
5123
|
return `\`${[install.command, ...install.args].join(" ")}\``;
|
|
4521
5124
|
}
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
if (
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
5125
|
+
const MB_LABEL = "mb";
|
|
5126
|
+
function mbUnusable(diagnosis) {
|
|
5127
|
+
if (diagnosis.kind === "missing") return "mb is not installed";
|
|
5128
|
+
return runningMbSuffices(diagnosis) ? null : "the mb that runs is too old";
|
|
5129
|
+
}
|
|
5130
|
+
function mbRow(diagnosis, lines, deps) {
|
|
5131
|
+
switch (diagnosis.kind) {
|
|
5132
|
+
case "missing": return failed("mb", MB_LABEL, {
|
|
5133
|
+
detail: "not installed",
|
|
5134
|
+
hint: installHint(deps.installedWith),
|
|
5135
|
+
step: "install-mb"
|
|
5136
|
+
});
|
|
5137
|
+
case "old": {
|
|
5138
|
+
const required = lines.map((line, index) => index === 0 ? `${line}; ${MIN_MB_VERSION} or newer is required` : line);
|
|
5139
|
+
return failed("mb", MB_LABEL, {
|
|
5140
|
+
detail: required.join("\n"),
|
|
5141
|
+
hint: installHint(deps.installedWith),
|
|
5142
|
+
step: "install-mb"
|
|
5143
|
+
});
|
|
5144
|
+
}
|
|
5145
|
+
case "shadowed": return failed("mb", MB_LABEL, {
|
|
5146
|
+
detail: lines.join("\n"),
|
|
5147
|
+
hint: shadowedHint(diagnosis),
|
|
5148
|
+
step: null
|
|
5149
|
+
});
|
|
5150
|
+
case "current": return ok("mb", MB_LABEL, lines.join("\n"));
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
async function mbFinding(deps) {
|
|
5154
|
+
const found = await inspectMbOnPath(deps.runner, deps.locateMb);
|
|
5155
|
+
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
5156
|
+
return {
|
|
5157
|
+
row: mbRow(diagnosis, mbPathLines(found), deps),
|
|
5158
|
+
unusable: mbUnusable(diagnosis)
|
|
5159
|
+
};
|
|
5160
|
+
}
|
|
5161
|
+
function sharedProfileList(deps) {
|
|
5162
|
+
let listed = null;
|
|
5163
|
+
return () => {
|
|
5164
|
+
listed ??= mbTool(deps.runner).authList();
|
|
5165
|
+
return listed;
|
|
5166
|
+
};
|
|
4535
5167
|
}
|
|
4536
5168
|
function publishesOnLoopback(container, port) {
|
|
4537
5169
|
return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
@@ -4549,7 +5181,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4549
5181
|
row: failed("container", label, {
|
|
4550
5182
|
detail: `${runtime.containerName} does not exist`,
|
|
4551
5183
|
hint: "`rde init` recreates it from the saved choices",
|
|
4552
|
-
|
|
5184
|
+
step: "runtime"
|
|
4553
5185
|
}),
|
|
4554
5186
|
running: false,
|
|
4555
5187
|
holder: CONTAINER
|
|
@@ -4562,7 +5194,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4562
5194
|
row: failed("container", label, {
|
|
4563
5195
|
detail: `${runtime.containerName} runs ${inspection.image}, the state says ${runtime.image}`,
|
|
4564
5196
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
4565
|
-
|
|
5197
|
+
step: null
|
|
4566
5198
|
}),
|
|
4567
5199
|
...found
|
|
4568
5200
|
};
|
|
@@ -4570,7 +5202,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4570
5202
|
row: failed("container", label, {
|
|
4571
5203
|
detail: `${runtime.containerName} does not publish port ${runtime.port} on ${LOOPBACK_HOST}`,
|
|
4572
5204
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
4573
|
-
|
|
5205
|
+
step: null
|
|
4574
5206
|
}),
|
|
4575
5207
|
...found
|
|
4576
5208
|
};
|
|
@@ -4591,7 +5223,7 @@ async function processRow(runtime, deps) {
|
|
|
4591
5223
|
row: failed("process", label, {
|
|
4592
5224
|
detail: `${inspection.jarPath} does not exist`,
|
|
4593
5225
|
hint,
|
|
4594
|
-
|
|
5226
|
+
step: "runtime"
|
|
4595
5227
|
}),
|
|
4596
5228
|
running: false,
|
|
4597
5229
|
holder: JAR_PROCESS
|
|
@@ -4618,13 +5250,13 @@ async function portRow(port, finding, ports) {
|
|
|
4618
5250
|
if (finding.running) return failed("port", "port", {
|
|
4619
5251
|
detail: `${port} is not answering although ${finding.holder} runs`,
|
|
4620
5252
|
hint: "`rde logs` shows what Metabase is doing",
|
|
4621
|
-
|
|
5253
|
+
step: null
|
|
4622
5254
|
});
|
|
4623
5255
|
if (free) return ok("port", "port", `${port} is free`);
|
|
4624
5256
|
return failed("port", "port", {
|
|
4625
5257
|
detail: `${port} is in use by another program`,
|
|
4626
5258
|
hint: "stop that program before `rde start`",
|
|
4627
|
-
|
|
5259
|
+
step: null
|
|
4628
5260
|
});
|
|
4629
5261
|
}
|
|
4630
5262
|
async function healthRow(url, deps) {
|
|
@@ -4639,7 +5271,7 @@ async function healthRow(url, deps) {
|
|
|
4639
5271
|
row: failed("health", "health", {
|
|
4640
5272
|
detail: `initializing (${Math.round(error.progress * 100)}%)`,
|
|
4641
5273
|
hint: "wait for it; `rde logs -f` shows progress",
|
|
4642
|
-
|
|
5274
|
+
step: "start-metabase"
|
|
4643
5275
|
}),
|
|
4644
5276
|
healthy: false
|
|
4645
5277
|
};
|
|
@@ -4647,7 +5279,7 @@ async function healthRow(url, deps) {
|
|
|
4647
5279
|
row: failed("health", "health", {
|
|
4648
5280
|
detail: "unreachable",
|
|
4649
5281
|
hint: "`rde start`",
|
|
4650
|
-
|
|
5282
|
+
step: "start-metabase"
|
|
4651
5283
|
}),
|
|
4652
5284
|
healthy: false
|
|
4653
5285
|
};
|
|
@@ -4655,59 +5287,227 @@ async function healthRow(url, deps) {
|
|
|
4655
5287
|
row: failed("health", "health", {
|
|
4656
5288
|
detail: `something other than Metabase answers at ${url} (HTTP ${error.status})`,
|
|
4657
5289
|
hint: "another program holds the port; stop it before `rde start`",
|
|
4658
|
-
|
|
5290
|
+
step: null
|
|
5291
|
+
}),
|
|
5292
|
+
healthy: false
|
|
5293
|
+
};
|
|
5294
|
+
throw error;
|
|
5295
|
+
}
|
|
5296
|
+
}
|
|
5297
|
+
function adminRow(state, properties) {
|
|
5298
|
+
if (!properties["has-user-setup"]) return failed("admin", "admin user", {
|
|
5299
|
+
detail: "Metabase has no admin user yet",
|
|
5300
|
+
hint: "`rde init --only admin-user`",
|
|
5301
|
+
step: "admin-user"
|
|
5302
|
+
});
|
|
5303
|
+
return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
|
|
5304
|
+
}
|
|
5305
|
+
const VERSION_STEP = "start-metabase";
|
|
5306
|
+
function versionRow(state, properties) {
|
|
5307
|
+
const label = "Metabase version";
|
|
5308
|
+
const running = properties.version.tag;
|
|
5309
|
+
if (state.version === null) return ok("version", label, `${running}; \`rde start\` records it in the state file`);
|
|
5310
|
+
if (state.version !== running) return failed("version", label, {
|
|
5311
|
+
detail: `running ${running}, the state file says ${state.version}`,
|
|
5312
|
+
hint: "`rde start` records the running version",
|
|
5313
|
+
step: VERSION_STEP
|
|
5314
|
+
});
|
|
5315
|
+
return ok("version", label, running);
|
|
5316
|
+
}
|
|
5317
|
+
const API_KEY_STEP = "api-key";
|
|
5318
|
+
const RECREATE_HINT = "when the instance holds nothing you need, `rde uninstall` then `rde init`";
|
|
5319
|
+
function adminPasswordRow(state, secrets) {
|
|
5320
|
+
const id = "admin-password";
|
|
5321
|
+
const label = "admin password";
|
|
5322
|
+
const admin = state.admin;
|
|
5323
|
+
if (admin === null) return skipped(id, label, "no admin user created by rde yet");
|
|
5324
|
+
switch (admin.kind) {
|
|
5325
|
+
case "prompted": return ok(id, label, "yours, entered during `rde init` and never stored");
|
|
5326
|
+
case "generated":
|
|
5327
|
+
if (secrets === null || secrets.admin === null) return failed(id, label, {
|
|
5328
|
+
detail: "generated, but missing from the secrets file",
|
|
5329
|
+
hint: `it cannot be recovered; ${RECREATE_HINT}`,
|
|
5330
|
+
step: null
|
|
5331
|
+
});
|
|
5332
|
+
return ok(id, label, "generated and stored; `rde credentials` shows it");
|
|
5333
|
+
case "legacy-default": return failed(id, label, {
|
|
5334
|
+
detail: "the public default password from an earlier rde (legacy-default)",
|
|
5335
|
+
hint: `anyone who knows the default can log in; ${RECREATE_HINT} with a generated one`,
|
|
5336
|
+
step: null
|
|
5337
|
+
});
|
|
5338
|
+
}
|
|
5339
|
+
}
|
|
5340
|
+
async function apiKeyFinding(secrets, url, healthy, deps) {
|
|
5341
|
+
const id = "api-key";
|
|
5342
|
+
const label = "API key";
|
|
5343
|
+
const apiKey = secrets === null ? null : secrets.apiKey;
|
|
5344
|
+
if (apiKey === null) return {
|
|
5345
|
+
row: failed(id, label, {
|
|
5346
|
+
detail: "none in the secrets file",
|
|
5347
|
+
hint: "`rde init --only api-key`",
|
|
5348
|
+
step: API_KEY_STEP
|
|
5349
|
+
}),
|
|
5350
|
+
access: {
|
|
5351
|
+
kind: "unusable",
|
|
5352
|
+
reason: "no API key"
|
|
5353
|
+
}
|
|
5354
|
+
};
|
|
5355
|
+
if (url === null || !healthy) return {
|
|
5356
|
+
row: ok(id, label, "in the secrets file"),
|
|
5357
|
+
access: {
|
|
5358
|
+
kind: "unusable",
|
|
5359
|
+
reason: NOT_HEALTHY
|
|
5360
|
+
}
|
|
5361
|
+
};
|
|
5362
|
+
const client = deps.client(url, {
|
|
5363
|
+
kind: "api-key",
|
|
5364
|
+
key: apiKey
|
|
5365
|
+
});
|
|
5366
|
+
try {
|
|
5367
|
+
await client.user.current();
|
|
5368
|
+
} catch (error) {
|
|
5369
|
+
if (!isRejectedCredential(error)) throw error;
|
|
5370
|
+
return {
|
|
5371
|
+
row: failed(id, label, {
|
|
5372
|
+
detail: "in the secrets file, but Metabase rejects it",
|
|
5373
|
+
hint: "`rde init --only api-key` replaces it",
|
|
5374
|
+
step: API_KEY_STEP
|
|
4659
5375
|
}),
|
|
4660
|
-
|
|
5376
|
+
access: {
|
|
5377
|
+
kind: "unusable",
|
|
5378
|
+
reason: "Metabase rejects the API key"
|
|
5379
|
+
}
|
|
4661
5380
|
};
|
|
4662
|
-
throw error;
|
|
4663
5381
|
}
|
|
5382
|
+
return {
|
|
5383
|
+
row: ok(id, label, "in the secrets file, accepted by Metabase"),
|
|
5384
|
+
access: {
|
|
5385
|
+
kind: "accepted",
|
|
5386
|
+
client
|
|
5387
|
+
}
|
|
5388
|
+
};
|
|
4664
5389
|
}
|
|
4665
|
-
function
|
|
4666
|
-
|
|
4667
|
-
detail: "Metabase has no admin user yet",
|
|
4668
|
-
hint: "`rde init --only admin-user`",
|
|
4669
|
-
fix: "admin-user"
|
|
4670
|
-
});
|
|
4671
|
-
return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
|
|
5390
|
+
function octal(mode) {
|
|
5391
|
+
return `0${mode.toString(8)}`;
|
|
4672
5392
|
}
|
|
4673
|
-
|
|
4674
|
-
|
|
4675
|
-
const label = "Metabase version";
|
|
4676
|
-
const running = properties.version.tag;
|
|
4677
|
-
if (state.version === null) return ok("version", label, `${running}; \`rde start\` records it in the state file`);
|
|
4678
|
-
if (state.version !== running) return failed("version", label, {
|
|
4679
|
-
detail: `running ${running}, the state file says ${state.version}`,
|
|
4680
|
-
hint: "`rde start` records the running version",
|
|
4681
|
-
fix: VERSION_STEP
|
|
4682
|
-
});
|
|
4683
|
-
return ok("version", label, running);
|
|
5393
|
+
function expectsSecrets(state) {
|
|
5394
|
+
return state.admin !== null && state.admin.kind !== "prompted" || state.steps[API_KEY_STEP]?.status === "done";
|
|
4684
5395
|
}
|
|
4685
|
-
function
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
5396
|
+
function secretsFileRow(state, files, dir) {
|
|
5397
|
+
const id = "secrets-file";
|
|
5398
|
+
const label = "secrets file";
|
|
5399
|
+
const path = secretsFileIn(dir);
|
|
5400
|
+
if (files.secretsMode === null) {
|
|
5401
|
+
if (!expectsSecrets(state)) return skipped(id, label, "nothing stored yet");
|
|
5402
|
+
return failed(id, label, {
|
|
5403
|
+
detail: `${path} is missing`,
|
|
5404
|
+
hint: "`rde init --only api-key` stores a new API key",
|
|
5405
|
+
step: API_KEY_STEP
|
|
5406
|
+
});
|
|
5407
|
+
}
|
|
5408
|
+
if (files.secretsMode !== 384 || files.dirMode !== 448) {
|
|
5409
|
+
const dirMode = files.dirMode === null ? "missing" : octal(files.dirMode);
|
|
5410
|
+
return failed(id, label, {
|
|
5411
|
+
detail: `${path} is ${octal(files.secretsMode)} in a ${dirMode} directory`,
|
|
5412
|
+
hint: `\`chmod ${octal(384)} ${path} && chmod ${octal(448)} ${dir}\``,
|
|
5413
|
+
step: null
|
|
5414
|
+
});
|
|
5415
|
+
}
|
|
5416
|
+
return ok(id, label, `${path}, ${octal(384)} in a ${octal(448)} directory`);
|
|
5417
|
+
}
|
|
5418
|
+
function stateFileRow(files) {
|
|
5419
|
+
const id = "state-file";
|
|
5420
|
+
const label = "state file";
|
|
5421
|
+
if (files.secretsInState.length > 0) return failed(id, label, {
|
|
5422
|
+
detail: `carries ${files.secretsInState.join(", ")}`,
|
|
5423
|
+
hint: "`rde init --only api-key` rewrites it without them",
|
|
5424
|
+
step: API_KEY_STEP
|
|
4690
5425
|
});
|
|
4691
|
-
return ok(
|
|
5426
|
+
return ok(id, label, "no secrets");
|
|
5427
|
+
}
|
|
5428
|
+
async function credentialRows(state, secrets, apiKey, deps) {
|
|
5429
|
+
const dir = deps.runtime.instanceDir;
|
|
5430
|
+
const files = await inspectCredentialFiles(dir);
|
|
5431
|
+
return [
|
|
5432
|
+
adminPasswordRow(state, secrets),
|
|
5433
|
+
apiKey.row,
|
|
5434
|
+
secretsFileRow(state, files, dir),
|
|
5435
|
+
stateFileRow(files)
|
|
5436
|
+
];
|
|
4692
5437
|
}
|
|
4693
|
-
|
|
5438
|
+
function withoutTrailingSlash(url) {
|
|
5439
|
+
return url.replace(/\/+$/, "");
|
|
5440
|
+
}
|
|
5441
|
+
async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
|
|
4694
5442
|
const label = "mb profile";
|
|
4695
|
-
if (
|
|
5443
|
+
if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
|
|
4696
5444
|
if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY);
|
|
4697
|
-
const entry = (await
|
|
5445
|
+
const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
|
|
4698
5446
|
if (entry === void 0) return failed("mb-profile", label, {
|
|
4699
5447
|
detail: `${state.mbProfile} is not configured`,
|
|
4700
5448
|
hint: "`rde init --only api-key`",
|
|
4701
|
-
|
|
5449
|
+
step: "api-key"
|
|
5450
|
+
});
|
|
5451
|
+
if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
|
|
5452
|
+
detail: `${state.mbProfile} points at ${entry.url}, not at this instance (${url})`,
|
|
5453
|
+
hint: "`rde init --only api-key` logs it back in against the instance",
|
|
5454
|
+
step: "api-key"
|
|
4702
5455
|
});
|
|
4703
5456
|
if (entry.status !== "ok") return failed("mb-profile", label, {
|
|
4704
5457
|
detail: `${state.mbProfile}: ${entry.status}`,
|
|
4705
5458
|
hint: "`rde init --only api-key` logs it in again",
|
|
4706
|
-
|
|
5459
|
+
step: "api-key"
|
|
4707
5460
|
});
|
|
4708
5461
|
const user = entry.user === null ? "" : ` as ${entry.user.name}`;
|
|
4709
5462
|
return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
|
|
4710
5463
|
}
|
|
5464
|
+
function hostOf(url) {
|
|
5465
|
+
return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
|
|
5466
|
+
}
|
|
5467
|
+
function unsetHint(name) {
|
|
5468
|
+
return `\`unset ${name}\`, remove it from your shell profile if it is exported there, then restart the agent session`;
|
|
5469
|
+
}
|
|
5470
|
+
async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
|
|
5471
|
+
const id = `env.${ENV_MB_PROFILE}`;
|
|
5472
|
+
if (name === rdeProfile) return ok(id, ENV_MB_PROFILE, `${name}: rde's own profile, so plain \`mb\` uses this instance`);
|
|
5473
|
+
const rde = rdeProfile === null ? "rde's" : `rde's (${rdeProfile})`;
|
|
5474
|
+
const detail = (where) => `${name}: \`mb\` and the rde skill will use profile ${name} (${where}) instead of ${rde}`;
|
|
5475
|
+
if (mbUnusableReason !== null) return note$1(id, ENV_MB_PROFILE, detail(mbUnusableReason), unsetHint(ENV_MB_PROFILE));
|
|
5476
|
+
const entry = (await profiles()).find((candidate) => candidate.profile === name);
|
|
5477
|
+
const where = entry === void 0 ? "not configured" : hostOf(entry.url);
|
|
5478
|
+
return note$1(id, ENV_MB_PROFILE, detail(where), unsetHint(ENV_MB_PROFILE));
|
|
5479
|
+
}
|
|
5480
|
+
async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
|
|
5481
|
+
const rows = [];
|
|
5482
|
+
if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
|
|
5483
|
+
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)));
|
|
5484
|
+
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)));
|
|
5485
|
+
return rows;
|
|
5486
|
+
}
|
|
5487
|
+
const PROFILE_STATUS_WORDS = {
|
|
5488
|
+
ok: "authenticates",
|
|
5489
|
+
"auth-failed": "login rejected",
|
|
5490
|
+
"network-error": "not answering",
|
|
5491
|
+
"server-error": "answers with a server error",
|
|
5492
|
+
"not-probed": "not checked"
|
|
5493
|
+
};
|
|
5494
|
+
function plainMbLine(env, rdeProfile, profiles) {
|
|
5495
|
+
if (env.url !== null) return `plain \`mb\` goes to ${hostOf(env.url)} (${ENV_MB_URL})`;
|
|
5496
|
+
const name = env.profile ?? "default";
|
|
5497
|
+
const via = env.profile === null ? "" : ` (${ENV_MB_PROFILE})`;
|
|
5498
|
+
const entry = profiles.find((candidate) => candidate.profile === name);
|
|
5499
|
+
if (entry === void 0) return `plain \`mb\` uses ${name}${via}, which is not configured`;
|
|
5500
|
+
const instance = name === rdeProfile ? ", this instance" : "";
|
|
5501
|
+
return `plain \`mb\` uses ${name}${via}: ${hostOf(entry.url)}${instance}`;
|
|
5502
|
+
}
|
|
5503
|
+
async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
|
|
5504
|
+
const id = "other-metabases";
|
|
5505
|
+
const label = "other Metabases";
|
|
5506
|
+
if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
|
|
5507
|
+
const all = await profiles();
|
|
5508
|
+
const others = all.filter((entry) => entry.profile !== rdeProfile);
|
|
5509
|
+
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"));
|
|
5510
|
+
}
|
|
4711
5511
|
function isNotFound$2(error) {
|
|
4712
5512
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
|
|
4713
5513
|
}
|
|
@@ -4718,7 +5518,7 @@ async function licenseRow(client) {
|
|
|
4718
5518
|
if (!status.valid) return failed("license", label, {
|
|
4719
5519
|
detail: `invalid: ${status.status}`,
|
|
4720
5520
|
hint: "`rde init --from license` with a valid token",
|
|
4721
|
-
|
|
5521
|
+
step: "license"
|
|
4722
5522
|
});
|
|
4723
5523
|
return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
|
|
4724
5524
|
} catch (error) {
|
|
@@ -4735,47 +5535,47 @@ async function databasesRow(state, client) {
|
|
|
4735
5535
|
if (missing.length > 0) return failed("databases", label, {
|
|
4736
5536
|
detail: `not on the server any more: ${missing.map((record) => record.name).join(", ")}`,
|
|
4737
5537
|
hint: "`rde db add` connects them again",
|
|
4738
|
-
|
|
5538
|
+
step: "databases"
|
|
4739
5539
|
});
|
|
4740
5540
|
const names = state.databases.map((record) => record.name).join(", ");
|
|
4741
5541
|
return ok("databases", label, `${state.databases.length} connected: ${names}`);
|
|
4742
5542
|
}
|
|
4743
5543
|
const SAMPLE_DATABASE_STEP = "sample-database";
|
|
4744
5544
|
const SAMPLE_LABEL = "sample database";
|
|
4745
|
-
async function sampleDatabaseRow(record, deps, dockerOk,
|
|
5545
|
+
async function sampleDatabaseRow(record, deps, dockerOk, access) {
|
|
4746
5546
|
const id = "sample-database";
|
|
4747
5547
|
if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
|
|
4748
5548
|
const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
|
|
4749
5549
|
if (!status.installed) return failed(id, SAMPLE_LABEL, {
|
|
4750
5550
|
detail: `${record.containerName} does not exist`,
|
|
4751
5551
|
hint: "`rde init` recreates it",
|
|
4752
|
-
|
|
5552
|
+
step: SAMPLE_DATABASE_STEP
|
|
4753
5553
|
});
|
|
4754
5554
|
if (!status.running) return failed(id, SAMPLE_LABEL, {
|
|
4755
5555
|
detail: `${record.containerName} is stopped`,
|
|
4756
5556
|
hint: "`rde start`",
|
|
4757
|
-
|
|
5557
|
+
step: "start-metabase"
|
|
4758
5558
|
});
|
|
4759
5559
|
if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
|
|
4760
5560
|
detail: `${record.containerName} runs but port ${record.port} is not answering`,
|
|
4761
5561
|
hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
|
|
4762
|
-
|
|
5562
|
+
step: null
|
|
4763
5563
|
});
|
|
4764
5564
|
const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
|
|
4765
5565
|
if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
|
|
4766
5566
|
detail: `${where}; not connected to Metabase yet`,
|
|
4767
5567
|
hint: "`rde init` connects it",
|
|
4768
|
-
|
|
5568
|
+
step: SAMPLE_DATABASE_STEP
|
|
4769
5569
|
});
|
|
4770
|
-
if (
|
|
5570
|
+
if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
|
|
4771
5571
|
try {
|
|
4772
|
-
const database = await client.database.get(record.databaseId);
|
|
5572
|
+
const database = await access.client.database.get(record.databaseId);
|
|
4773
5573
|
return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
|
|
4774
5574
|
} catch (error) {
|
|
4775
5575
|
if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
|
|
4776
5576
|
detail: `${where}; database ${record.databaseId} is gone from Metabase`,
|
|
4777
5577
|
hint: "`rde init` connects it again",
|
|
4778
|
-
|
|
5578
|
+
step: SAMPLE_DATABASE_STEP
|
|
4779
5579
|
});
|
|
4780
5580
|
throw error;
|
|
4781
5581
|
}
|
|
@@ -4792,7 +5592,7 @@ async function gitSyncRow(state, deps) {
|
|
|
4792
5592
|
if (status.branch === null) return failed("git-sync", label, {
|
|
4793
5593
|
detail: "Metabase reports no branch",
|
|
4794
5594
|
hint: "`rde init --only git-sync`",
|
|
4795
|
-
|
|
5595
|
+
step: "git-sync"
|
|
4796
5596
|
});
|
|
4797
5597
|
const dirty = status.is_dirty ? "unsynced changes" : "in sync";
|
|
4798
5598
|
return ok("git-sync", label, `branch ${status.branch}, ${dirty}, ${state.gitRemote.workdir}`);
|
|
@@ -4800,15 +5600,100 @@ async function gitSyncRow(state, deps) {
|
|
|
4800
5600
|
if (isMbFailure$1(error)) return failed("git-sync", label, {
|
|
4801
5601
|
detail: error.message,
|
|
4802
5602
|
hint: `\`${mbCommand(state.mbProfile)} git-sync status\` shows the full answer`,
|
|
4803
|
-
|
|
5603
|
+
step: null
|
|
4804
5604
|
});
|
|
4805
5605
|
throw error;
|
|
4806
5606
|
}
|
|
4807
5607
|
}
|
|
4808
5608
|
const SKILL_STEP = "agents-skills";
|
|
5609
|
+
const GLOBAL_SCOPE = "global";
|
|
5610
|
+
const SKILL_FILE = "SKILL.md";
|
|
5611
|
+
const CLAUDE_CODE = "Claude Code";
|
|
5612
|
+
const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
|
|
4809
5613
|
function skillRowId(agent) {
|
|
4810
5614
|
return `skill.${agent.id}`;
|
|
4811
5615
|
}
|
|
5616
|
+
function withRef(source, ref) {
|
|
5617
|
+
return ref === null ? source : `${source}#${ref}`;
|
|
5618
|
+
}
|
|
5619
|
+
async function copyRef(entry, deps) {
|
|
5620
|
+
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
5621
|
+
return lockedRef(path, await deps.skills.files.readJson(path), entry.name);
|
|
5622
|
+
}
|
|
5623
|
+
async function judgeCopy(entry, origin, deps) {
|
|
5624
|
+
const where = `${entry.scope} ${entry.path}`;
|
|
5625
|
+
switch (origin.kind) {
|
|
5626
|
+
case "local": {
|
|
5627
|
+
const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
|
|
5628
|
+
const wanted = await deps.skills.files.readText(join(origin.path, SKILL_FILE));
|
|
5629
|
+
const current = installed !== null && installed === wanted;
|
|
5630
|
+
return {
|
|
5631
|
+
entry,
|
|
5632
|
+
current,
|
|
5633
|
+
line: `${where}: ${current ? "same as" : "differs from"} ${origin.path}`
|
|
5634
|
+
};
|
|
5635
|
+
}
|
|
5636
|
+
case "github": {
|
|
5637
|
+
const wanted = withRef(origin.repository, origin.ref);
|
|
5638
|
+
if (entry.source === null) return {
|
|
5639
|
+
entry,
|
|
5640
|
+
current: false,
|
|
5641
|
+
line: `${where}: no recorded source, not ${wanted}`
|
|
5642
|
+
};
|
|
5643
|
+
const found = withRef(entry.source, await copyRef(entry, deps));
|
|
5644
|
+
const current = found === wanted;
|
|
5645
|
+
return {
|
|
5646
|
+
entry,
|
|
5647
|
+
current,
|
|
5648
|
+
line: `${where}: from ${found}${current ? "" : `, not ${wanted}`}`
|
|
5649
|
+
};
|
|
5650
|
+
}
|
|
5651
|
+
case "other": {
|
|
5652
|
+
const current = entry.source === origin.source;
|
|
5653
|
+
return {
|
|
5654
|
+
entry,
|
|
5655
|
+
current,
|
|
5656
|
+
line: `${where}: from ${entry.source ?? "no recorded source"}${current ? "" : `, not ${origin.source}`}`
|
|
5657
|
+
};
|
|
5658
|
+
}
|
|
5659
|
+
}
|
|
5660
|
+
}
|
|
5661
|
+
function loadedCopyLine(entries, cwd) {
|
|
5662
|
+
if (entries.length < 2) return null;
|
|
5663
|
+
const count = `${entries.length} copies are installed`;
|
|
5664
|
+
const forClaude = entries.filter((entry) => entry.agents.includes(CLAUDE_CODE));
|
|
5665
|
+
const loaded = forClaude.find((entry) => entry.scope === GLOBAL_SCOPE) ?? forClaude[0];
|
|
5666
|
+
if (loaded === void 0) return count;
|
|
5667
|
+
return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
|
|
5668
|
+
}
|
|
5669
|
+
function staleCopiesRow(stale, detail, deps) {
|
|
5670
|
+
const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
|
|
5671
|
+
const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
|
|
5672
|
+
const repairs = [];
|
|
5673
|
+
if (globalStale) repairs.push(`\`rde init --only ${SKILL_STEP}\` reinstalls the global copy from ${deps.skills.source}`);
|
|
5674
|
+
if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove rde -y\` in ${deps.skills.cwd}`);
|
|
5675
|
+
return failed("skill-copies", "rde skill copies", {
|
|
5676
|
+
detail,
|
|
5677
|
+
hint: `${repairs.join("; ")}; then restart the agent session`,
|
|
5678
|
+
step: globalStale ? SKILL_STEP : null
|
|
5679
|
+
});
|
|
5680
|
+
}
|
|
5681
|
+
async function skillCopiesRow(globalEntries, deps) {
|
|
5682
|
+
const id = "skill-copies";
|
|
5683
|
+
const label = "rde skill copies";
|
|
5684
|
+
const cwd = deps.skills.cwd;
|
|
5685
|
+
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === "rde");
|
|
5686
|
+
const entries = [...globalEntries, ...projectEntries];
|
|
5687
|
+
if (entries.length === 0) return skipped(id, label, "no copy installed");
|
|
5688
|
+
const origin = skillOrigin(deps.skills.source, cwd);
|
|
5689
|
+
const judged = [];
|
|
5690
|
+
for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
|
|
5691
|
+
const loaded = loadedCopyLine(entries, cwd);
|
|
5692
|
+
const lines = judged.map((copy) => copy.line);
|
|
5693
|
+
const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
|
|
5694
|
+
const stale = judged.filter((copy) => !copy.current);
|
|
5695
|
+
return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
|
|
5696
|
+
}
|
|
4812
5697
|
async function skillRows(state, deps) {
|
|
4813
5698
|
const label = "rde skill";
|
|
4814
5699
|
if (state === null) return [skipped("skill", label, NO_INSTANCE)];
|
|
@@ -4816,17 +5701,18 @@ async function skillRows(state, deps) {
|
|
|
4816
5701
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
4817
5702
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
4818
5703
|
const entries = await rdeSkillEntries(deps.runner);
|
|
5704
|
+
const copies = await skillCopiesRow(entries, deps);
|
|
4819
5705
|
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
|
|
4820
5706
|
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
4821
5707
|
hint: `\`rde init --only ${SKILL_STEP}\``,
|
|
4822
|
-
|
|
4823
|
-
})];
|
|
4824
|
-
return detected.map((agent) => {
|
|
5708
|
+
step: SKILL_STEP
|
|
5709
|
+
}), copies];
|
|
5710
|
+
return [...detected.map((agent) => {
|
|
4825
5711
|
const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
|
|
4826
5712
|
const agentLabel = `${label} (${agent.displayName})`;
|
|
4827
5713
|
if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
|
|
4828
5714
|
return ok(skillRowId(agent), agentLabel, entry.path);
|
|
4829
|
-
});
|
|
5715
|
+
}), copies];
|
|
4830
5716
|
}
|
|
4831
5717
|
function noRuntimeRows() {
|
|
4832
5718
|
return [
|
|
@@ -4835,13 +5721,13 @@ function noRuntimeRows() {
|
|
|
4835
5721
|
"health"
|
|
4836
5722
|
].map((id) => skipped(id, id, NO_INSTANCE));
|
|
4837
5723
|
}
|
|
4838
|
-
async function instanceRows(state, deps, dockerOk,
|
|
5724
|
+
async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusableReason) {
|
|
4839
5725
|
const configured = configuredRuntime(state);
|
|
4840
5726
|
if (configured === null) return [
|
|
4841
5727
|
...noRuntimeRows(),
|
|
4842
5728
|
skipped("version", "Metabase version", NO_INSTANCE),
|
|
4843
5729
|
skipped("admin", "admin user", NO_INSTANCE),
|
|
4844
|
-
|
|
5730
|
+
...await credentialRows(state, secrets, await apiKeyFinding(secrets, null, false, deps), deps),
|
|
4845
5731
|
skipped("mb-profile", "mb profile", NO_INSTANCE),
|
|
4846
5732
|
skipped("license", "license", NO_INSTANCE),
|
|
4847
5733
|
skipped("databases", "databases", NO_INSTANCE),
|
|
@@ -4851,35 +5737,33 @@ async function instanceRows(state, deps, dockerOk, mbInstalled) {
|
|
|
4851
5737
|
const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
|
|
4852
5738
|
const { row: health, healthy } = await healthRow(configured.url, deps);
|
|
4853
5739
|
const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
|
|
5740
|
+
const apiKey = await apiKeyFinding(secrets, configured.url, healthy, deps);
|
|
4854
5741
|
const rows = [
|
|
4855
5742
|
runtime.row,
|
|
4856
5743
|
await portRow(configured.runtime.port, runtime, deps.ports),
|
|
4857
5744
|
health,
|
|
4858
5745
|
properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
|
|
4859
5746
|
properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
|
|
4860
|
-
|
|
4861
|
-
await mbProfileRow(state,
|
|
5747
|
+
...await credentialRows(state, secrets, apiKey, deps),
|
|
5748
|
+
await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
|
|
4862
5749
|
];
|
|
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));
|
|
5750
|
+
const access = apiKey.access;
|
|
5751
|
+
if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
|
|
5752
|
+
else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
|
|
5753
|
+
rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
|
|
4872
5754
|
rows.push(await gitSyncRow(state, deps));
|
|
4873
5755
|
return rows;
|
|
4874
5756
|
}
|
|
4875
|
-
async function collectDoctor(instance, state, deps) {
|
|
5757
|
+
async function collectDoctor(instance, state, secrets, deps) {
|
|
4876
5758
|
const machine = await machineRows(deps, state?.runtime ?? null);
|
|
4877
5759
|
const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
|
|
4878
|
-
const mb = await
|
|
4879
|
-
const
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
checks.push(...await
|
|
5760
|
+
const mb = await mbFinding(deps);
|
|
5761
|
+
const profiles = sharedProfileList(deps);
|
|
5762
|
+
const checks = [...machine, mb.row];
|
|
5763
|
+
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));
|
|
5764
|
+
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
5765
|
+
const rdeProfile = state === null ? null : state.mbProfile;
|
|
5766
|
+
checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
|
|
4883
5767
|
return {
|
|
4884
5768
|
instance,
|
|
4885
5769
|
checks,
|
|
@@ -4891,18 +5775,38 @@ const MARK = {
|
|
|
4891
5775
|
failed: "✗",
|
|
4892
5776
|
skipped: "–"
|
|
4893
5777
|
};
|
|
5778
|
+
const NOTE_MARK = "!";
|
|
4894
5779
|
const HINT_ARROW = "→";
|
|
5780
|
+
function markFor(row) {
|
|
5781
|
+
return row.status === "ok" && row.hint !== null ? NOTE_MARK : MARK[row.status];
|
|
5782
|
+
}
|
|
4895
5783
|
function doctorLines(report) {
|
|
4896
5784
|
const width = Math.max(...report.checks.map((row) => row.label.length));
|
|
5785
|
+
const indent = ` ${" ".repeat(width)} `;
|
|
4897
5786
|
const lines = [];
|
|
4898
5787
|
for (const row of report.checks) {
|
|
4899
|
-
|
|
4900
|
-
|
|
5788
|
+
const [first, ...rest] = row.detail.split("\n");
|
|
5789
|
+
lines.push(`${markFor(row)} ${row.label.padEnd(width)} ${first}`);
|
|
5790
|
+
lines.push(...rest.map((line) => `${indent}${line}`));
|
|
5791
|
+
if (row.hint !== null) lines.push(`${indent}${HINT_ARROW} ${row.hint}`);
|
|
4901
5792
|
}
|
|
4902
5793
|
return lines;
|
|
4903
5794
|
}
|
|
5795
|
+
function fixCommand(step) {
|
|
5796
|
+
return step === null ? null : `rde init --only ${step}`;
|
|
5797
|
+
}
|
|
5798
|
+
function doctorJson(report) {
|
|
5799
|
+
return {
|
|
5800
|
+
instance: report.instance,
|
|
5801
|
+
checks: report.checks.map(({ step, ...row }) => ({
|
|
5802
|
+
...row,
|
|
5803
|
+
fix: fixCommand(step)
|
|
5804
|
+
})),
|
|
5805
|
+
failed: report.failed
|
|
5806
|
+
};
|
|
5807
|
+
}
|
|
4904
5808
|
function fixableSteps(report) {
|
|
4905
|
-
const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.
|
|
5809
|
+
const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.step !== null ? [row.step] : []));
|
|
4906
5810
|
return StepId.options.filter((id) => wanted.has(id));
|
|
4907
5811
|
}
|
|
4908
5812
|
function doctorSummary(report) {
|
|
@@ -4941,15 +5845,24 @@ const ADMIN_USER_KEYS = {
|
|
|
4941
5845
|
firstName: `${STEP_ID$6}.first-name`,
|
|
4942
5846
|
lastName: `${STEP_ID$6}.last-name`
|
|
4943
5847
|
};
|
|
4944
|
-
const
|
|
4945
|
-
const
|
|
5848
|
+
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
5849
|
+
const GENERATED_PASSWORD_BYTES = 24;
|
|
4946
5850
|
const DEFAULT_SITE_NAME = "rde";
|
|
4947
5851
|
const MIN_PASSWORD_LENGTH = 6;
|
|
4948
5852
|
const DIGIT = /\d/;
|
|
4949
5853
|
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4950
|
-
const DEFAULT_ADMIN = { variant: "
|
|
5854
|
+
const DEFAULT_ADMIN = { variant: "generated" };
|
|
5855
|
+
function generatePassword(random) {
|
|
5856
|
+
for (;;) {
|
|
5857
|
+
const candidate = random(GENERATED_PASSWORD_BYTES).toString("base64url");
|
|
5858
|
+
if (DIGIT.test(candidate)) return candidate;
|
|
5859
|
+
}
|
|
5860
|
+
}
|
|
4951
5861
|
function hostDeps$8(ctx) {
|
|
4952
|
-
return {
|
|
5862
|
+
return {
|
|
5863
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5864
|
+
password: () => generatePassword(randomBytes)
|
|
5865
|
+
};
|
|
4953
5866
|
}
|
|
4954
5867
|
function isNotReady(error) {
|
|
4955
5868
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
@@ -4988,26 +5901,22 @@ async function promptedUser(ctx) {
|
|
|
4988
5901
|
...lastName.length > 0 && { last_name: lastName }
|
|
4989
5902
|
};
|
|
4990
5903
|
}
|
|
4991
|
-
async function chooseUser(ctx,
|
|
4992
|
-
switch (
|
|
4993
|
-
case "
|
|
4994
|
-
email:
|
|
4995
|
-
password:
|
|
5904
|
+
async function chooseUser(ctx, variant, deps) {
|
|
5905
|
+
switch (variant) {
|
|
5906
|
+
case "generated": return {
|
|
5907
|
+
email: GENERATED_ADMIN_EMAIL,
|
|
5908
|
+
password: deps.password()
|
|
4996
5909
|
};
|
|
4997
5910
|
case "prompted": return promptedUser(ctx);
|
|
4998
5911
|
}
|
|
4999
5912
|
}
|
|
5000
|
-
function
|
|
5001
|
-
switch (
|
|
5002
|
-
case "
|
|
5003
|
-
kind,
|
|
5913
|
+
function storedLogin(variant, user) {
|
|
5914
|
+
switch (variant) {
|
|
5915
|
+
case "generated": return {
|
|
5004
5916
|
email: user.email,
|
|
5005
5917
|
password: user.password
|
|
5006
5918
|
};
|
|
5007
|
-
case "prompted": return
|
|
5008
|
-
kind,
|
|
5009
|
-
email: user.email
|
|
5010
|
-
};
|
|
5919
|
+
case "prompted": return null;
|
|
5011
5920
|
}
|
|
5012
5921
|
}
|
|
5013
5922
|
function adminUserStep(options, deps = hostDeps$8) {
|
|
@@ -5026,7 +5935,8 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5026
5935
|
},
|
|
5027
5936
|
async run(ctx) {
|
|
5028
5937
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5029
|
-
const
|
|
5938
|
+
const resolved = deps(ctx);
|
|
5939
|
+
const client = resolved.client(configured.url, NO_AUTH);
|
|
5030
5940
|
const properties = await client.session.properties();
|
|
5031
5941
|
if (properties["has-user-setup"]) {
|
|
5032
5942
|
ctx.ui.info("Metabase already has an admin user");
|
|
@@ -5034,7 +5944,11 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5034
5944
|
}
|
|
5035
5945
|
const token = properties["setup-token"];
|
|
5036
5946
|
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);
|
|
5947
|
+
const user = await chooseUser(ctx, options.variant, resolved);
|
|
5948
|
+
await ctx.saveSecrets({
|
|
5949
|
+
...ctx.secrets,
|
|
5950
|
+
admin: storedLogin(options.variant, user)
|
|
5951
|
+
});
|
|
5038
5952
|
const session = await client.setup.create({
|
|
5039
5953
|
token,
|
|
5040
5954
|
user,
|
|
@@ -5043,7 +5957,10 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5043
5957
|
ctx.session.metabaseSession.set(session.id);
|
|
5044
5958
|
await ctx.saveState({
|
|
5045
5959
|
...ctx.state,
|
|
5046
|
-
admin:
|
|
5960
|
+
admin: {
|
|
5961
|
+
kind: options.variant,
|
|
5962
|
+
email: user.email
|
|
5963
|
+
}
|
|
5047
5964
|
});
|
|
5048
5965
|
ctx.ui.success(`Admin user ${user.email} created`);
|
|
5049
5966
|
return "completed";
|
|
@@ -5066,11 +5983,12 @@ function hostDeps$7(ctx) {
|
|
|
5066
5983
|
};
|
|
5067
5984
|
}
|
|
5068
5985
|
async function adminCredentials(ctx) {
|
|
5069
|
-
const
|
|
5070
|
-
if (
|
|
5071
|
-
username:
|
|
5072
|
-
password:
|
|
5986
|
+
const stored = ctx.secrets.admin;
|
|
5987
|
+
if (stored !== null) return {
|
|
5988
|
+
username: stored.email,
|
|
5989
|
+
password: stored.password
|
|
5073
5990
|
};
|
|
5991
|
+
const admin = ctx.state.admin;
|
|
5074
5992
|
ctx.ui.info("An admin login is needed once to create the API key; it is not saved");
|
|
5075
5993
|
return {
|
|
5076
5994
|
username: await ctx.prompts.text({
|
|
@@ -5108,46 +6026,70 @@ async function freshApiKey(ctx, client) {
|
|
|
5108
6026
|
group_id: groupId
|
|
5109
6027
|
})).unmasked_key;
|
|
5110
6028
|
}
|
|
5111
|
-
async function
|
|
5112
|
-
|
|
6029
|
+
async function authenticates(client) {
|
|
6030
|
+
try {
|
|
6031
|
+
await client.user.current();
|
|
6032
|
+
return true;
|
|
6033
|
+
} catch (error) {
|
|
6034
|
+
if (isRejectedCredential(error)) return false;
|
|
6035
|
+
throw error;
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
6038
|
+
async function workingApiKey(ctx, deps, url) {
|
|
6039
|
+
const stored = ctx.secrets.apiKey;
|
|
6040
|
+
if (stored !== null) {
|
|
6041
|
+
if (await authenticates(deps.client(url, {
|
|
6042
|
+
kind: "api-key",
|
|
6043
|
+
key: stored
|
|
6044
|
+
}))) return stored;
|
|
6045
|
+
ctx.ui.warn("Metabase rejects the stored API key; replacing it");
|
|
6046
|
+
}
|
|
5113
6047
|
const sessionId = await superuserSession(ctx, deps, url);
|
|
5114
6048
|
const apiKey = await freshApiKey(ctx, deps.client(url, {
|
|
5115
6049
|
kind: "session",
|
|
5116
6050
|
id: sessionId
|
|
5117
6051
|
}));
|
|
5118
|
-
await ctx.
|
|
5119
|
-
...ctx.
|
|
6052
|
+
await ctx.saveSecrets({
|
|
6053
|
+
...ctx.secrets,
|
|
5120
6054
|
apiKey
|
|
5121
6055
|
});
|
|
5122
6056
|
return apiKey;
|
|
5123
6057
|
}
|
|
5124
|
-
async function
|
|
5125
|
-
const profile = ctx.state.mbProfile;
|
|
5126
|
-
if (profile !== "default") return profile;
|
|
5127
|
-
let existing;
|
|
6058
|
+
async function defaultProfileStatus(mb) {
|
|
5128
6059
|
try {
|
|
5129
|
-
|
|
6060
|
+
return await mb.authStatus(DEFAULT_MB_PROFILE);
|
|
5130
6061
|
} catch (error) {
|
|
5131
|
-
if (error instanceof ProcessNotFoundError) return
|
|
6062
|
+
if (error instanceof ProcessNotFoundError) return null;
|
|
5132
6063
|
throw error;
|
|
5133
6064
|
}
|
|
5134
|
-
|
|
5135
|
-
|
|
6065
|
+
}
|
|
6066
|
+
async function chooseProfile(ctx, mb, url) {
|
|
6067
|
+
const profile = ctx.state.mbProfile;
|
|
6068
|
+
if (profile !== "default") return profile;
|
|
6069
|
+
const existing = await defaultProfileStatus(mb);
|
|
6070
|
+
if (existing === null || !existing.present || existing.url === null || existing.url === url) return profile;
|
|
6071
|
+
const previous = existing.url;
|
|
6072
|
+
const named = namedMbProfile(ctx.state.name);
|
|
6073
|
+
if (await ctx.prompts.select({
|
|
5136
6074
|
key: API_KEY_KEYS.profile,
|
|
5137
|
-
message: `mb profile
|
|
6075
|
+
message: `mb profile default already points at ${previous}; which profile should this instance use?`,
|
|
5138
6076
|
choices: [{
|
|
5139
|
-
value: "
|
|
5140
|
-
label: `
|
|
6077
|
+
value: "default",
|
|
6078
|
+
label: `default, replacing it (recommended; it points at ${previous} today)`
|
|
5141
6079
|
}, {
|
|
5142
|
-
value:
|
|
5143
|
-
label: `${
|
|
5144
|
-
}]
|
|
5145
|
-
|
|
5146
|
-
|
|
6080
|
+
value: MbProfileChoice.enum.rde,
|
|
6081
|
+
label: `${named}, and leave default alone`
|
|
6082
|
+
}],
|
|
6083
|
+
recommended: "default"
|
|
6084
|
+
}) === "default") {
|
|
6085
|
+
ctx.ui.info(`Replacing mb profile ${DEFAULT_MB_PROFILE}, which pointed at ${previous}`);
|
|
6086
|
+
return profile;
|
|
6087
|
+
}
|
|
6088
|
+
await ctx.saveState({
|
|
5147
6089
|
...ctx.state,
|
|
5148
|
-
mbProfile:
|
|
6090
|
+
mbProfile: named
|
|
5149
6091
|
});
|
|
5150
|
-
return
|
|
6092
|
+
return named;
|
|
5151
6093
|
}
|
|
5152
6094
|
function usageNote(profile) {
|
|
5153
6095
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
@@ -5158,7 +6100,7 @@ function apiKeyStep(deps = hostDeps$7) {
|
|
|
5158
6100
|
id: STEP_ID$5,
|
|
5159
6101
|
title: "Connect the Metabase CLI",
|
|
5160
6102
|
async detect(ctx) {
|
|
5161
|
-
if (ctx.
|
|
6103
|
+
if (ctx.secrets.apiKey === null) return "todo";
|
|
5162
6104
|
const mb = mbTool(deps(ctx).runner);
|
|
5163
6105
|
const profile = ctx.state.mbProfile;
|
|
5164
6106
|
try {
|
|
@@ -5172,7 +6114,7 @@ function apiKeyStep(deps = hostDeps$7) {
|
|
|
5172
6114
|
async run(ctx) {
|
|
5173
6115
|
const resolved = deps(ctx);
|
|
5174
6116
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5175
|
-
const apiKey = await
|
|
6117
|
+
const apiKey = await workingApiKey(ctx, resolved, configured.url);
|
|
5176
6118
|
const mb = mbTool(resolved.runner);
|
|
5177
6119
|
const profile = await chooseProfile(ctx, mb, configured.url);
|
|
5178
6120
|
await mb.authLogin({
|
|
@@ -5574,14 +6516,14 @@ function isMbUnavailable(error) {
|
|
|
5574
6516
|
}
|
|
5575
6517
|
async function runGitSync(ctx, deps) {
|
|
5576
6518
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5577
|
-
if (ctx.
|
|
6519
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
5578
6520
|
const run = {
|
|
5579
6521
|
ctx,
|
|
5580
6522
|
deps,
|
|
5581
6523
|
git: gitTool(deps.runner),
|
|
5582
6524
|
client: deps.client(configured.url, {
|
|
5583
6525
|
kind: "api-key",
|
|
5584
|
-
key: ctx.
|
|
6526
|
+
key: ctx.secrets.apiKey
|
|
5585
6527
|
})
|
|
5586
6528
|
};
|
|
5587
6529
|
const flow = await runFlow({
|
|
@@ -5637,8 +6579,8 @@ function validateToken(value) {
|
|
|
5637
6579
|
return "A license token is 64 hexadecimal characters";
|
|
5638
6580
|
}
|
|
5639
6581
|
function requireApiKey$1(ctx) {
|
|
5640
|
-
if (ctx.
|
|
5641
|
-
return ctx.
|
|
6582
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
6583
|
+
return ctx.secrets.apiKey;
|
|
5642
6584
|
}
|
|
5643
6585
|
function activeLabel(status) {
|
|
5644
6586
|
const plan = status["plan-alias"];
|
|
@@ -5651,15 +6593,15 @@ function licenseStep(deps = hostDeps$5) {
|
|
|
5651
6593
|
async detect(ctx) {
|
|
5652
6594
|
if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
|
|
5653
6595
|
const configured = configuredRuntime(ctx.state);
|
|
5654
|
-
if (configured === null || ctx.
|
|
6596
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
5655
6597
|
const client = deps(ctx).client(configured.url, {
|
|
5656
6598
|
kind: "api-key",
|
|
5657
|
-
key: ctx.
|
|
6599
|
+
key: ctx.secrets.apiKey
|
|
5658
6600
|
});
|
|
5659
6601
|
try {
|
|
5660
6602
|
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
5661
6603
|
} catch (error) {
|
|
5662
|
-
if (isNoTokenYet(error)) return "todo";
|
|
6604
|
+
if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
|
|
5663
6605
|
throw error;
|
|
5664
6606
|
}
|
|
5665
6607
|
},
|
|
@@ -5894,9 +6836,9 @@ function isNotServingYet(error) {
|
|
|
5894
6836
|
function isNotFound$1(error) {
|
|
5895
6837
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
|
|
5896
6838
|
}
|
|
5897
|
-
function requireApiKey(
|
|
5898
|
-
if (
|
|
5899
|
-
return
|
|
6839
|
+
function requireApiKey(ctx) {
|
|
6840
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
|
|
6841
|
+
return ctx.secrets.apiKey;
|
|
5900
6842
|
}
|
|
5901
6843
|
async function recordedDatabase(record, client) {
|
|
5902
6844
|
if (record.databaseId === null) return null;
|
|
@@ -6018,23 +6960,23 @@ function sampleDatabaseStep(deps = hostDeps$3) {
|
|
|
6018
6960
|
if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
|
|
6019
6961
|
const record = ctx.state.sampleDatabase;
|
|
6020
6962
|
const configured = configuredRuntime(ctx.state);
|
|
6021
|
-
if (record === null || configured === null || ctx.
|
|
6963
|
+
if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
6022
6964
|
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
|
|
6023
6965
|
const client = resolved.client(configured.url, {
|
|
6024
6966
|
kind: "api-key",
|
|
6025
|
-
key: ctx.
|
|
6967
|
+
key: ctx.secrets.apiKey
|
|
6026
6968
|
});
|
|
6027
6969
|
try {
|
|
6028
6970
|
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
6029
6971
|
} catch (error) {
|
|
6030
|
-
if (isNotServingYet(error)) return "todo";
|
|
6972
|
+
if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
|
|
6031
6973
|
throw error;
|
|
6032
6974
|
}
|
|
6033
6975
|
},
|
|
6034
6976
|
async run(ctx) {
|
|
6035
6977
|
const resolved = deps(ctx);
|
|
6036
6978
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6037
|
-
const apiKey = requireApiKey(ctx
|
|
6979
|
+
const apiKey = requireApiKey(ctx);
|
|
6038
6980
|
const docker = await resolved.runtime.docker.available();
|
|
6039
6981
|
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
6040
6982
|
const choice = await persistedRecord(ctx, resolved);
|
|
@@ -6149,115 +7091,235 @@ function hostDeps$1(ctx) {
|
|
|
6149
7091
|
function isNotServing$1(error) {
|
|
6150
7092
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
6151
7093
|
}
|
|
6152
|
-
function loginLine(
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
case "default": return `Login: ${
|
|
6156
|
-
case "prompted": return `Login: ${
|
|
7094
|
+
function loginLine(kind, email) {
|
|
7095
|
+
switch (kind) {
|
|
7096
|
+
case "generated":
|
|
7097
|
+
case "legacy-default": return `Login: ${email} (rde credentials shows the password)`;
|
|
7098
|
+
case "prompted": return `Login: ${email} with the password you entered`;
|
|
6157
7099
|
}
|
|
6158
7100
|
}
|
|
6159
|
-
async function databaseLines$1(state, deps) {
|
|
6160
|
-
if (state.url === null ||
|
|
7101
|
+
async function databaseLines$1(state, apiKey, deps) {
|
|
7102
|
+
if (state.url === null || apiKey === null) return ["Databases: none connected yet"];
|
|
6161
7103
|
let databases;
|
|
6162
7104
|
try {
|
|
6163
7105
|
databases = (await deps.client(state.url, {
|
|
6164
7106
|
kind: "api-key",
|
|
6165
|
-
key:
|
|
7107
|
+
key: apiKey
|
|
6166
7108
|
}).database.list()).data;
|
|
6167
7109
|
} catch (error) {
|
|
6168
7110
|
if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
|
|
7111
|
+
if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
|
|
6169
7112
|
throw error;
|
|
6170
7113
|
}
|
|
6171
|
-
if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
|
|
6172
|
-
return ["Databases:", ...existingDatabaseLines(databases).map((line) => ` ${line}`)];
|
|
6173
|
-
}
|
|
6174
|
-
function remoteLine(remote) {
|
|
6175
|
-
switch (remote.kind) {
|
|
6176
|
-
case "local-bare": return `Remote: local bare repository ${remote.barePath}`;
|
|
6177
|
-
case "hosted": return `Remote: ${remote.url}`;
|
|
7114
|
+
if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
|
|
7115
|
+
return ["Databases:", ...existingDatabaseLines(databases).map((line) => ` ${line}`)];
|
|
7116
|
+
}
|
|
7117
|
+
function remoteLine(remote) {
|
|
7118
|
+
switch (remote.kind) {
|
|
7119
|
+
case "local-bare": return `Remote: local bare repository ${remote.barePath}`;
|
|
7120
|
+
case "hosted": return `Remote: ${remote.url}`;
|
|
7121
|
+
}
|
|
7122
|
+
}
|
|
7123
|
+
function notConfiguredReason(state) {
|
|
7124
|
+
const license = state.license;
|
|
7125
|
+
if (!license.activated) return "no license";
|
|
7126
|
+
if (!hasRemoteSync(license)) return "the license has no remote-sync feature";
|
|
7127
|
+
if (state.steps[GIT_SYNC_STEP_ID]?.status === "skipped") return "skipped; `rde init --only git-sync` sets it up";
|
|
7128
|
+
return "not set up yet; `rde init --only git-sync`";
|
|
7129
|
+
}
|
|
7130
|
+
function gitLines(state) {
|
|
7131
|
+
if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
|
|
7132
|
+
return [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
|
|
7133
|
+
}
|
|
7134
|
+
function skillLine(entries) {
|
|
7135
|
+
const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
|
|
7136
|
+
if (agents.length === 0) return `rde skill: not installed; \`rde init --only agents-skills\``;
|
|
7137
|
+
return `rde skill: installed for ${agents.join(", ")}`;
|
|
7138
|
+
}
|
|
7139
|
+
function launchLines(agents, workdir) {
|
|
7140
|
+
const detected = agents.filter((agent) => agent.detected);
|
|
7141
|
+
if (detected.length === 0) return [];
|
|
7142
|
+
const prefix = workdir === null ? "" : `cd ${workdir} && `;
|
|
7143
|
+
return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
|
|
7144
|
+
}
|
|
7145
|
+
async function summaryLines(state, apiKey, deps) {
|
|
7146
|
+
const profile = state.mbProfile;
|
|
7147
|
+
const lines = [];
|
|
7148
|
+
if (state.url !== null) lines.push(`Metabase: ${state.url}`);
|
|
7149
|
+
if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
|
|
7150
|
+
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
7151
|
+
lines.push(...await databaseLines$1(state, apiKey, deps));
|
|
7152
|
+
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
7153
|
+
lines.push(...gitLines(state));
|
|
7154
|
+
const installed = await skillsCli(deps.runner).list();
|
|
7155
|
+
lines.push(skillLine(installed.filter((entry) => entry.name === "rde")));
|
|
7156
|
+
lines.push(...launchLines(await detectAgents(deps.probe), state.gitRemote?.workdir ?? null));
|
|
7157
|
+
lines.push("Lifecycle: rde status, rde stop, rde start");
|
|
7158
|
+
return lines;
|
|
7159
|
+
}
|
|
7160
|
+
function summaryStep(deps = hostDeps$1) {
|
|
7161
|
+
return {
|
|
7162
|
+
id: STEP_ID,
|
|
7163
|
+
title: "Summary",
|
|
7164
|
+
async detect() {
|
|
7165
|
+
return "todo";
|
|
7166
|
+
},
|
|
7167
|
+
async run(ctx) {
|
|
7168
|
+
const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
|
|
7169
|
+
ctx.ui.note(lines.join("\n"), "Your environment");
|
|
7170
|
+
return "completed";
|
|
7171
|
+
}
|
|
7172
|
+
};
|
|
7173
|
+
}
|
|
7174
|
+
//#endregion
|
|
7175
|
+
//#region src/steps/index.ts
|
|
7176
|
+
const DEFAULT_STEP_OPTIONS = {
|
|
7177
|
+
runtime: NO_RUNTIME_FLAGS,
|
|
7178
|
+
agents: DETECT_AGENTS,
|
|
7179
|
+
admin: DEFAULT_ADMIN,
|
|
7180
|
+
databases: SAMPLE_STANDS_IN
|
|
7181
|
+
};
|
|
7182
|
+
const STEP_BY_ID = {
|
|
7183
|
+
preflight: () => preflightStep(),
|
|
7184
|
+
"install-mb": () => installMbStep(),
|
|
7185
|
+
"agents-skills": (options) => agentsSkillsStep(options.agents),
|
|
7186
|
+
runtime: (options) => runtimeStep(options.runtime),
|
|
7187
|
+
"start-metabase": () => startMetabaseStep(),
|
|
7188
|
+
"admin-user": (options) => adminUserStep(options.admin),
|
|
7189
|
+
"api-key": () => apiKeyStep(),
|
|
7190
|
+
"sample-database": () => sampleDatabaseStep(),
|
|
7191
|
+
license: () => licenseStep(),
|
|
7192
|
+
databases: (options) => databasesStep(options.databases),
|
|
7193
|
+
"git-sync": () => gitSyncStep(),
|
|
7194
|
+
summary: () => summaryStep()
|
|
7195
|
+
};
|
|
7196
|
+
function initSteps(options) {
|
|
7197
|
+
return StepId.options.map((id) => STEP_BY_ID[id](options));
|
|
7198
|
+
}
|
|
7199
|
+
//#endregion
|
|
7200
|
+
//#region src/wizard/answers.ts
|
|
7201
|
+
function isOneOf(answer, kinds) {
|
|
7202
|
+
return kinds.some((kind) => kind === answer.kind);
|
|
7203
|
+
}
|
|
7204
|
+
function rejected(key, prompt, reason) {
|
|
7205
|
+
return new ConfigError(`answers file: "${key}" for "${prompt.message}" ${reason}`);
|
|
7206
|
+
}
|
|
7207
|
+
function expectKind(taken, prompt, kinds) {
|
|
7208
|
+
if (taken.answer.kind === "cancel") throw new AbortError();
|
|
7209
|
+
if (isOneOf(taken.answer, kinds)) return taken.answer;
|
|
7210
|
+
throw rejected(taken.key, prompt, `is a ${taken.answer.kind} answer; expected ${kinds.join(" or ")}`);
|
|
7211
|
+
}
|
|
7212
|
+
function choiceLabels(choices) {
|
|
7213
|
+
return choices.map((choice) => String(choice.value)).join(", ");
|
|
7214
|
+
}
|
|
7215
|
+
function matchChoice(key, prompt, value) {
|
|
7216
|
+
const match = prompt.choices.find((choice) => choice.value === value);
|
|
7217
|
+
if (match === void 0) throw rejected(key, prompt, `is ${String(value)}, not one of the choices ${choiceLabels(prompt.choices)}`);
|
|
7218
|
+
return match.value;
|
|
7219
|
+
}
|
|
7220
|
+
function textValue(taken, prompt) {
|
|
7221
|
+
const answer = expectKind(taken, prompt, ["text"]);
|
|
7222
|
+
const value = answer.value.length === 0 && prompt.defaultValue !== void 0 ? prompt.defaultValue : answer.value;
|
|
7223
|
+
const problem = prompt.validate?.(value);
|
|
7224
|
+
if (problem !== void 0) throw rejected(taken.key, prompt, `is "${value}", which is rejected: ${problem}`);
|
|
7225
|
+
return value;
|
|
7226
|
+
}
|
|
7227
|
+
function passwordValue(taken, prompt) {
|
|
7228
|
+
const answer = expectKind(taken, prompt, ["password", "text"]);
|
|
7229
|
+
const problem = prompt.validate?.(answer.value);
|
|
7230
|
+
if (problem !== void 0) throw rejected(taken.key, prompt, `is rejected: ${problem}`);
|
|
7231
|
+
return answer.value;
|
|
7232
|
+
}
|
|
7233
|
+
function isBackAnswer(answer) {
|
|
7234
|
+
return answer.kind === "back";
|
|
7235
|
+
}
|
|
7236
|
+
function answeringPrompts(base, answers, options = {}) {
|
|
7237
|
+
function lookup(prompt) {
|
|
7238
|
+
if (prompt.key === void 0) return null;
|
|
7239
|
+
const answer = answers.get(prompt.key)?.shift();
|
|
7240
|
+
return answer === void 0 ? null : {
|
|
7241
|
+
key: prompt.key,
|
|
7242
|
+
answer
|
|
7243
|
+
};
|
|
7244
|
+
}
|
|
7245
|
+
function refuseUnanswered(prompt) {
|
|
7246
|
+
const refusal = options.unanswered?.(prompt) ?? null;
|
|
7247
|
+
if (refusal !== null) throw refusal;
|
|
7248
|
+
}
|
|
7249
|
+
function take(prompt) {
|
|
7250
|
+
const taken = lookup(prompt);
|
|
7251
|
+
if (taken === null) refuseUnanswered(prompt);
|
|
7252
|
+
return taken;
|
|
7253
|
+
}
|
|
7254
|
+
function takeText(prompt) {
|
|
7255
|
+
const taken = lookup(prompt);
|
|
7256
|
+
if (taken !== null) return taken;
|
|
7257
|
+
if (prompt.defaultValue !== void 0 && options.unanswered !== void 0) return {
|
|
7258
|
+
key: prompt.key ?? prompt.message,
|
|
7259
|
+
answer: {
|
|
7260
|
+
kind: "text",
|
|
7261
|
+
value: ""
|
|
7262
|
+
}
|
|
7263
|
+
};
|
|
7264
|
+
refuseUnanswered(prompt);
|
|
7265
|
+
return null;
|
|
7266
|
+
}
|
|
7267
|
+
function selected(taken, prompt) {
|
|
7268
|
+
const answer = expectKind(taken, prompt, ["select", "text"]);
|
|
7269
|
+
return matchChoice(taken.key, prompt, answer.value);
|
|
7270
|
+
}
|
|
7271
|
+
function multiselected(taken, prompt) {
|
|
7272
|
+
return expectKind(taken, prompt, ["multiselect"]).values.map((value) => matchChoice(taken.key, prompt, value));
|
|
6178
7273
|
}
|
|
6179
|
-
}
|
|
6180
|
-
function notConfiguredReason(state) {
|
|
6181
|
-
const license = state.license;
|
|
6182
|
-
if (!license.activated) return "no license";
|
|
6183
|
-
if (!hasRemoteSync(license)) return "the license has no remote-sync feature";
|
|
6184
|
-
if (state.steps[GIT_SYNC_STEP_ID]?.status === "skipped") return "skipped; `rde init --only git-sync` sets it up";
|
|
6185
|
-
return "not set up yet; `rde init --only git-sync`";
|
|
6186
|
-
}
|
|
6187
|
-
function gitLines(state) {
|
|
6188
|
-
if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
|
|
6189
|
-
return [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
|
|
6190
|
-
}
|
|
6191
|
-
function skillLine(entries) {
|
|
6192
|
-
const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
|
|
6193
|
-
if (agents.length === 0) return `rde skill: not installed; \`rde init --only agents-skills\``;
|
|
6194
|
-
return `rde skill: installed for ${agents.join(", ")}`;
|
|
6195
|
-
}
|
|
6196
|
-
function launchLines(agents, workdir) {
|
|
6197
|
-
const detected = agents.filter((agent) => agent.detected);
|
|
6198
|
-
if (detected.length === 0) return [];
|
|
6199
|
-
const prefix = workdir === null ? "" : `cd ${workdir} && `;
|
|
6200
|
-
return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
|
|
6201
|
-
}
|
|
6202
|
-
async function summaryLines(state, deps) {
|
|
6203
|
-
const profile = state.mbProfile;
|
|
6204
|
-
const lines = [];
|
|
6205
|
-
if (state.url !== null) lines.push(`Metabase: ${state.url}`);
|
|
6206
|
-
const login = loginLine(state.admin);
|
|
6207
|
-
if (login !== null) lines.push(login);
|
|
6208
|
-
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));
|
|
6210
|
-
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
6211
|
-
lines.push(...gitLines(state));
|
|
6212
|
-
const installed = await skillsCli(deps.runner).list();
|
|
6213
|
-
lines.push(skillLine(installed.filter((entry) => entry.name === "rde")));
|
|
6214
|
-
lines.push(...launchLines(await detectAgents(deps.probe), state.gitRemote?.workdir ?? null));
|
|
6215
|
-
lines.push("Lifecycle: rde status, rde stop, rde start");
|
|
6216
|
-
return lines;
|
|
6217
|
-
}
|
|
6218
|
-
function summaryStep(deps = hostDeps$1) {
|
|
6219
7274
|
return {
|
|
6220
|
-
|
|
6221
|
-
|
|
6222
|
-
|
|
6223
|
-
return "todo";
|
|
7275
|
+
async text(opts) {
|
|
7276
|
+
const taken = takeText(opts);
|
|
7277
|
+
return taken === null ? base.text(opts) : textValue(taken, opts);
|
|
6224
7278
|
},
|
|
6225
|
-
async
|
|
6226
|
-
|
|
6227
|
-
return
|
|
7279
|
+
async textOrBack(opts) {
|
|
7280
|
+
const taken = takeText(opts);
|
|
7281
|
+
if (taken === null) return base.textOrBack(opts);
|
|
7282
|
+
return isBackAnswer(taken.answer) ? BACK : textValue(taken, opts);
|
|
7283
|
+
},
|
|
7284
|
+
async password(opts) {
|
|
7285
|
+
const taken = take(opts);
|
|
7286
|
+
return taken === null ? base.password(opts) : passwordValue(taken, opts);
|
|
7287
|
+
},
|
|
7288
|
+
async confirm(opts) {
|
|
7289
|
+
const taken = lookup(opts);
|
|
7290
|
+
if (taken !== null) return expectKind(taken, opts, ["confirm"]).value;
|
|
7291
|
+
if (options.confirmAll === true) return true;
|
|
7292
|
+
refuseUnanswered(opts);
|
|
7293
|
+
return base.confirm(opts);
|
|
7294
|
+
},
|
|
7295
|
+
async select(opts) {
|
|
7296
|
+
const taken = lookup(opts);
|
|
7297
|
+
if (taken !== null) return selected(taken, opts);
|
|
7298
|
+
if (options.takeRecommended === true && opts.recommended !== void 0) return opts.recommended;
|
|
7299
|
+
refuseUnanswered(opts);
|
|
7300
|
+
return base.select(opts);
|
|
7301
|
+
},
|
|
7302
|
+
async selectOrBack(opts) {
|
|
7303
|
+
const taken = take(opts);
|
|
7304
|
+
if (taken === null) return base.selectOrBack(opts);
|
|
7305
|
+
return isBackAnswer(taken.answer) ? BACK : selected(taken, opts);
|
|
7306
|
+
},
|
|
7307
|
+
async multiselect(opts) {
|
|
7308
|
+
const taken = take(opts);
|
|
7309
|
+
return taken === null ? base.multiselect(opts) : multiselected(taken, opts);
|
|
7310
|
+
},
|
|
7311
|
+
async multiselectOrBack(opts) {
|
|
7312
|
+
const taken = take(opts);
|
|
7313
|
+
if (taken === null) return base.multiselectOrBack(opts);
|
|
7314
|
+
return isBackAnswer(taken.answer) ? BACK : multiselected(taken, opts);
|
|
6228
7315
|
}
|
|
6229
7316
|
};
|
|
6230
7317
|
}
|
|
6231
7318
|
//#endregion
|
|
6232
|
-
//#region src/steps/index.ts
|
|
6233
|
-
const DEFAULT_STEP_OPTIONS = {
|
|
6234
|
-
runtime: NO_RUNTIME_FLAGS,
|
|
6235
|
-
agents: DETECT_AGENTS,
|
|
6236
|
-
admin: DEFAULT_ADMIN,
|
|
6237
|
-
databases: ASK_DATABASES
|
|
6238
|
-
};
|
|
6239
|
-
const STEP_BY_ID = {
|
|
6240
|
-
preflight: () => preflightStep(),
|
|
6241
|
-
"install-mb": () => installMbStep(),
|
|
6242
|
-
"agents-skills": (options) => agentsSkillsStep(options.agents),
|
|
6243
|
-
runtime: (options) => runtimeStep(options.runtime),
|
|
6244
|
-
"start-metabase": () => startMetabaseStep(),
|
|
6245
|
-
"admin-user": (options) => adminUserStep(options.admin),
|
|
6246
|
-
"api-key": () => apiKeyStep(),
|
|
6247
|
-
"sample-database": () => sampleDatabaseStep(),
|
|
6248
|
-
license: () => licenseStep(),
|
|
6249
|
-
databases: (options) => databasesStep(options.databases),
|
|
6250
|
-
"git-sync": () => gitSyncStep(),
|
|
6251
|
-
summary: () => summaryStep()
|
|
6252
|
-
};
|
|
6253
|
-
function initSteps(options) {
|
|
6254
|
-
return StepId.options.map((id) => STEP_BY_ID[id](options));
|
|
6255
|
-
}
|
|
6256
|
-
//#endregion
|
|
6257
7319
|
//#region src/commands/doctor.ts
|
|
6258
7320
|
function print(report, json) {
|
|
6259
7321
|
if (json) {
|
|
6260
|
-
printJson(report);
|
|
7322
|
+
printJson(doctorJson(report));
|
|
6261
7323
|
return;
|
|
6262
7324
|
}
|
|
6263
7325
|
printLines(doctorLines(report));
|
|
@@ -6288,12 +7350,16 @@ const doctor = defineRdeCommand({
|
|
|
6288
7350
|
const name = await instanceNameOrDefault();
|
|
6289
7351
|
const state = await readInstanceState(name);
|
|
6290
7352
|
const deps = hostDoctorDeps(name, interruptSignal);
|
|
6291
|
-
let report = await collectDoctor(name, state, deps);
|
|
7353
|
+
let report = await collectDoctor(name, state, state === null ? null : await readInstanceSecrets(name), deps);
|
|
6292
7354
|
if (args.fix && report.failed > 0) {
|
|
6293
7355
|
if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
|
|
6294
7356
|
print(report, args.json);
|
|
6295
|
-
const
|
|
6296
|
-
|
|
7357
|
+
const prompts = answeringPrompts(clackPrompts(), /* @__PURE__ */ new Map(), { takeRecommended: args.yes || !stdinIsTty() });
|
|
7358
|
+
const ctx = await createWizardContext(name, {
|
|
7359
|
+
signal: interruptSignal,
|
|
7360
|
+
prompts
|
|
7361
|
+
});
|
|
7362
|
+
if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
|
|
6297
7363
|
}
|
|
6298
7364
|
print(report, args.json);
|
|
6299
7365
|
if (report.failed > 0) throw new Error(doctorSummary(report));
|
|
@@ -6318,7 +7384,7 @@ const EnvString = z.string().transform((value, ctx) => {
|
|
|
6318
7384
|
}
|
|
6319
7385
|
return resolved;
|
|
6320
7386
|
});
|
|
6321
|
-
const AdminVariant = z.enum(["
|
|
7387
|
+
const AdminVariant = z.enum(["generated", "prompted"]);
|
|
6322
7388
|
const RemoteKind = z.enum(["local-bare", "hosted"]);
|
|
6323
7389
|
const FieldValue = z.union([
|
|
6324
7390
|
EnvString,
|
|
@@ -6422,7 +7488,18 @@ function runtimeKindOf(runtime) {
|
|
|
6422
7488
|
if (runtime.jar !== void 0) return "jar";
|
|
6423
7489
|
return null;
|
|
6424
7490
|
}
|
|
6425
|
-
function
|
|
7491
|
+
function databasesRequest(databases, run) {
|
|
7492
|
+
if (databases !== void 0) return databases.length > 0 ? "named" : "sample-stands-in";
|
|
7493
|
+
return run.interactive ? "ask-once" : "sample-stands-in";
|
|
7494
|
+
}
|
|
7495
|
+
function agentsOption(agents, run) {
|
|
7496
|
+
if (agents !== void 0) return {
|
|
7497
|
+
kind: "listed",
|
|
7498
|
+
agents: agents.agents
|
|
7499
|
+
};
|
|
7500
|
+
return run.interactive ? { kind: "ask" } : DEFAULT_STEP_OPTIONS.agents;
|
|
7501
|
+
}
|
|
7502
|
+
function stepOptionsFrom(answers, run) {
|
|
6426
7503
|
const runtime = answers.runtime;
|
|
6427
7504
|
const agents = answers["agents-skills"];
|
|
6428
7505
|
const admin = answers["admin-user"];
|
|
@@ -6434,9 +7511,9 @@ function stepOptionsFrom(answers) {
|
|
|
6434
7511
|
jar: runtime.jar ?? null,
|
|
6435
7512
|
port: runtime.port ?? null
|
|
6436
7513
|
},
|
|
6437
|
-
agents: agents
|
|
7514
|
+
agents: agentsOption(agents, run),
|
|
6438
7515
|
admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
|
|
6439
|
-
databases: {
|
|
7516
|
+
databases: { request: databasesRequest(databases, run) }
|
|
6440
7517
|
};
|
|
6441
7518
|
}
|
|
6442
7519
|
function keyedAnswers(answers) {
|
|
@@ -6554,7 +7631,7 @@ function parseDatabase(spec) {
|
|
|
6554
7631
|
function adminSection(flags, secrets) {
|
|
6555
7632
|
const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
|
|
6556
7633
|
if (flags.admin === void 0 && !prompted) return;
|
|
6557
|
-
if (flags.admin === "
|
|
7634
|
+
if (flags.admin === "generated" && prompted) throw conflict("--admin generated", "--admin-email/--admin-password");
|
|
6558
7635
|
return {
|
|
6559
7636
|
variant: flags.admin ?? "prompted",
|
|
6560
7637
|
...flags.adminEmail !== void 0 && { email: flags.adminEmail },
|
|
@@ -6705,20 +7782,57 @@ function isLocation(value) {
|
|
|
6705
7782
|
function isRemote(value) {
|
|
6706
7783
|
return value === "local-bare" || value === "hosted";
|
|
6707
7784
|
}
|
|
7785
|
+
function isUnreviewedPaste(database) {
|
|
7786
|
+
return database !== void 0 && database.pasted && !database.reviewed;
|
|
7787
|
+
}
|
|
7788
|
+
function applyConnectionRecord(draft, value) {
|
|
7789
|
+
draft.databases = draft.databases.filter((database) => !database.pasted);
|
|
7790
|
+
if (value === null) return;
|
|
7791
|
+
if (value.trim().length === 0) {
|
|
7792
|
+
draft.databasesFinished = true;
|
|
7793
|
+
return;
|
|
7794
|
+
}
|
|
7795
|
+
draft.databases.push({
|
|
7796
|
+
engine: null,
|
|
7797
|
+
uri: value.trim(),
|
|
7798
|
+
fields: {},
|
|
7799
|
+
name: null,
|
|
7800
|
+
pasted: true,
|
|
7801
|
+
reviewed: false
|
|
7802
|
+
});
|
|
7803
|
+
}
|
|
7804
|
+
function applyEngineRecord(draft, value) {
|
|
7805
|
+
if (isUnreviewedPaste(draft.databases.at(-1))) draft.databases.pop();
|
|
7806
|
+
if (value === "finish") draft.databasesFinished = true;
|
|
7807
|
+
else if (value !== null) draft.databases.push({
|
|
7808
|
+
engine: value,
|
|
7809
|
+
uri: null,
|
|
7810
|
+
fields: {},
|
|
7811
|
+
name: null,
|
|
7812
|
+
pasted: false,
|
|
7813
|
+
reviewed: false
|
|
7814
|
+
});
|
|
7815
|
+
}
|
|
6708
7816
|
function applyDatabaseRecord(draft, record) {
|
|
6709
7817
|
const value = stringOf(record.answer);
|
|
7818
|
+
if (record.key === DATABASE_KEYS.connection) {
|
|
7819
|
+
applyConnectionRecord(draft, value);
|
|
7820
|
+
return;
|
|
7821
|
+
}
|
|
6710
7822
|
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
|
-
});
|
|
7823
|
+
applyEngineRecord(draft, value);
|
|
6718
7824
|
return;
|
|
6719
7825
|
}
|
|
6720
7826
|
const database = draft.databases.at(-1);
|
|
6721
7827
|
if (database === void 0) return;
|
|
7828
|
+
if (record.key === DATABASE_KEYS.detectedEngine && value !== null) {
|
|
7829
|
+
database.engine = value;
|
|
7830
|
+
return;
|
|
7831
|
+
}
|
|
7832
|
+
if (record.key === DATABASE_KEYS.action) {
|
|
7833
|
+
database.reviewed = true;
|
|
7834
|
+
return;
|
|
7835
|
+
}
|
|
6722
7836
|
if (record.key === DATABASE_KEYS.uri && value !== null) {
|
|
6723
7837
|
database.uri = value;
|
|
6724
7838
|
return;
|
|
@@ -6746,12 +7860,19 @@ function applyGitRecord(draft, key, value) {
|
|
|
6746
7860
|
else if (key === GIT_SYNC_KEYS.url && value !== null) gitSync(draft).url = value;
|
|
6747
7861
|
else if (key === GIT_SYNC_KEYS.token) gitSync(draft).token = envReference(ENV_GIT_TOKEN);
|
|
6748
7862
|
}
|
|
7863
|
+
function agentIds(values) {
|
|
7864
|
+
return values.flatMap((value) => {
|
|
7865
|
+
const parsed = AgentId.safeParse(value);
|
|
7866
|
+
return parsed.success ? [parsed.data] : [];
|
|
7867
|
+
});
|
|
7868
|
+
}
|
|
6749
7869
|
function applyRecord(draft, record) {
|
|
6750
7870
|
const { key } = record;
|
|
6751
7871
|
const value = stringOf(record.answer);
|
|
6752
7872
|
if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
|
|
6753
7873
|
else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
6754
7874
|
else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
|
|
7875
|
+
else if (key === AGENTS_SKILLS_KEYS.agents && record.answer.kind === "multiselect") draft.answers["agents-skills"] = { agents: agentIds(record.answer.values) };
|
|
6755
7876
|
else if (key === API_KEY_KEYS.profile && value !== null) {
|
|
6756
7877
|
const profile = MbProfileChoice.safeParse(value);
|
|
6757
7878
|
if (profile.success) draft.answers["api-key"] = { profile: profile.data };
|
|
@@ -6760,11 +7881,17 @@ function applyRecord(draft, record) {
|
|
|
6760
7881
|
function writableUri(uri, index) {
|
|
6761
7882
|
return URI_WITH_PASSWORD.test(uri) ? envReference(databaseEnv(index, "uri")) : uri;
|
|
6762
7883
|
}
|
|
7884
|
+
function engineOf(database) {
|
|
7885
|
+
if (database.engine !== null) return database.engine;
|
|
7886
|
+
const [only, ...others] = database.uri === null ? [] : detectEngine(database.uri);
|
|
7887
|
+
return only !== void 0 && others.length === 0 ? only : null;
|
|
7888
|
+
}
|
|
6763
7889
|
function databaseEntry(database, index) {
|
|
6764
7890
|
const hasFields = Object.keys(database.fields).length > 0;
|
|
6765
|
-
|
|
7891
|
+
const engine = engineOf(database);
|
|
7892
|
+
if (database.uri === null && !hasFields || engine === null) return null;
|
|
6766
7893
|
return {
|
|
6767
|
-
engine
|
|
7894
|
+
engine,
|
|
6768
7895
|
...database.uri !== null && { uri: writableUri(database.uri, index) },
|
|
6769
7896
|
...database.uri === null && { fields: database.fields },
|
|
6770
7897
|
...database.name !== null && { name: database.name }
|
|
@@ -6789,122 +7916,6 @@ async function writeAnswersFile(path, answers) {
|
|
|
6789
7916
|
await promises.writeFile(path, JSON.stringify(answers, null, 2) + "\n", { mode: FILE_MODE });
|
|
6790
7917
|
}
|
|
6791
7918
|
//#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
7919
|
//#region src/wizard/record.ts
|
|
6909
7920
|
function recordingPrompts(base) {
|
|
6910
7921
|
const records = [];
|
|
@@ -6981,9 +7992,11 @@ async function combinedAnswers(options) {
|
|
|
6981
7992
|
return mergeInitAnswers(await loadInitAnswers(options.answersPath), fromFlags);
|
|
6982
7993
|
}
|
|
6983
7994
|
function promptsFor(answers, yes) {
|
|
7995
|
+
const terminal = stdinIsTty();
|
|
6984
7996
|
const answering = {
|
|
6985
|
-
...!
|
|
6986
|
-
...yes && { confirmAll: true }
|
|
7997
|
+
...!terminal && { unanswered: refusalWithoutTerminal },
|
|
7998
|
+
...yes && { confirmAll: true },
|
|
7999
|
+
...(yes || !terminal) && { takeRecommended: true }
|
|
6987
8000
|
};
|
|
6988
8001
|
return answeringPrompts(clackPrompts(), keyedAnswers(answers), answering);
|
|
6989
8002
|
}
|
|
@@ -7027,7 +8040,7 @@ const init = defineRdeCommand({
|
|
|
7027
8040
|
},
|
|
7028
8041
|
admin: {
|
|
7029
8042
|
type: "string",
|
|
7030
|
-
description: "Admin credentials:
|
|
8043
|
+
description: "Admin credentials: generated (a random password; `rde credentials` shows it) or prompted"
|
|
7031
8044
|
},
|
|
7032
8045
|
"admin-email": {
|
|
7033
8046
|
type: "string",
|
|
@@ -7078,7 +8091,7 @@ const init = defineRdeCommand({
|
|
|
7078
8091
|
},
|
|
7079
8092
|
agents: {
|
|
7080
8093
|
type: "string",
|
|
7081
|
-
description: "Agents to install the skill for, comma-separated; the detected ones
|
|
8094
|
+
description: "Agents to install the skill for, comma-separated (a missing opencode is installed); asked at a terminal, the detected ones otherwise"
|
|
7082
8095
|
},
|
|
7083
8096
|
"skip-agents": {
|
|
7084
8097
|
type: "boolean",
|
|
@@ -7087,7 +8100,7 @@ const init = defineRdeCommand({
|
|
|
7087
8100
|
},
|
|
7088
8101
|
"mb-profile": {
|
|
7089
8102
|
type: "string",
|
|
7090
|
-
description: "mb profile for
|
|
8103
|
+
description: "mb profile for the default instance when `default` already points at another Metabase: default (replace it) or rde"
|
|
7091
8104
|
},
|
|
7092
8105
|
yes: {
|
|
7093
8106
|
type: "boolean",
|
|
@@ -7136,7 +8149,7 @@ const init = defineRdeCommand({
|
|
|
7136
8149
|
prompts: recording?.prompts ?? answering
|
|
7137
8150
|
});
|
|
7138
8151
|
ctx.ui.intro("rde init");
|
|
7139
|
-
await runWizard(initSteps(stepOptionsFrom(answers)), ctx, options);
|
|
8152
|
+
await runWizard(initSteps(stepOptionsFrom(answers, { interactive: stdinIsTty() && !runOptions.yes })), ctx, options);
|
|
7140
8153
|
if (recording !== null && printTo !== void 0) {
|
|
7141
8154
|
await writeAnswersFile(printTo, answersFromRecords(recording.records, answers));
|
|
7142
8155
|
ctx.ui.info(`Answers written to ${printTo}`);
|
|
@@ -7243,11 +8256,11 @@ function isNotServing(error) {
|
|
|
7243
8256
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7244
8257
|
}
|
|
7245
8258
|
async function databasesOf(ctx, deps, health) {
|
|
7246
|
-
if (health !== "healthy" || ctx.
|
|
8259
|
+
if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
|
|
7247
8260
|
try {
|
|
7248
|
-
return await listDatabases(ctx.state, { client: deps.client });
|
|
8261
|
+
return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
|
|
7249
8262
|
} catch (error) {
|
|
7250
|
-
if (isNotServing(error)) return null;
|
|
8263
|
+
if (isNotServing(error) || isRejectedCredential(error)) return null;
|
|
7251
8264
|
throw error;
|
|
7252
8265
|
}
|
|
7253
8266
|
}
|
|
@@ -7290,9 +8303,13 @@ async function collectStatus(ctx, deps) {
|
|
|
7290
8303
|
const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
|
|
7291
8304
|
const health = configured === null ? null : await healthOf(configured.url, deps);
|
|
7292
8305
|
const databases = health === null ? null : await databasesOf(ctx, deps, health);
|
|
8306
|
+
const admin = ctx.state.admin;
|
|
7293
8307
|
return {
|
|
7294
8308
|
instance: ctx.state.name,
|
|
7295
8309
|
url: configured?.url ?? null,
|
|
8310
|
+
mbProfile: ctx.state.mbProfile,
|
|
8311
|
+
adminEmail: admin === null ? null : admin.email,
|
|
8312
|
+
adminVariant: admin === null ? null : admin.kind,
|
|
7296
8313
|
runtime: configured === null || runtime === null ? null : {
|
|
7297
8314
|
kind: configured.runtime.kind,
|
|
7298
8315
|
installed: runtime.installed,
|
|
@@ -7346,6 +8363,8 @@ function databaseLines(report) {
|
|
|
7346
8363
|
function statusLines(report) {
|
|
7347
8364
|
return [
|
|
7348
8365
|
report.url === null ? `Instance ${report.instance}` : `Instance ${report.instance} — ${report.url}`,
|
|
8366
|
+
...report.adminVariant === null || report.adminEmail === null ? [] : [loginLine(report.adminVariant, report.adminEmail)],
|
|
8367
|
+
`Metabase CLI: ${mbCommand(report.mbProfile)} <command>`,
|
|
7349
8368
|
runtimeLine(report),
|
|
7350
8369
|
versionLine(report),
|
|
7351
8370
|
`Health: ${report.health ?? "unknown"}`,
|
|
@@ -7648,7 +8667,7 @@ async function planUninstall(options, deps) {
|
|
|
7648
8667
|
if (jar.removal !== null) removals.push(jar.removal);
|
|
7649
8668
|
if (jar.kept !== null) kept.push(jar.kept);
|
|
7650
8669
|
}
|
|
7651
|
-
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE,
|
|
8670
|
+
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, namedMbProfile(options.instance)], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
|
|
7652
8671
|
removals.push(...profiles);
|
|
7653
8672
|
if (options.removeSkill) {
|
|
7654
8673
|
const skill = await findSkill(deps.runner);
|
|
@@ -7657,6 +8676,11 @@ async function planUninstall(options, deps) {
|
|
|
7657
8676
|
const dir = instanceDir(options.instance);
|
|
7658
8677
|
const dirPresent = await pathExists(dir);
|
|
7659
8678
|
if (!options.keepData) {
|
|
8679
|
+
const secrets = instanceSecretsPath(options.instance);
|
|
8680
|
+
if (await pathExists(secrets)) removals.push({
|
|
8681
|
+
label: `Stored credentials ${secrets} (the admin password and the API key)`,
|
|
8682
|
+
remove: () => promises.rm(secrets, { force: true })
|
|
8683
|
+
});
|
|
7660
8684
|
if (dirPresent) removals.push({
|
|
7661
8685
|
label: instanceDirLabel(dir, state),
|
|
7662
8686
|
remove: () => promises.rm(dir, {
|
|
@@ -8058,7 +9082,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
8058
9082
|
const main = defineCommand({
|
|
8059
9083
|
meta: {
|
|
8060
9084
|
name: "rde",
|
|
8061
|
-
version: "0.1.
|
|
9085
|
+
version: "0.1.5",
|
|
8062
9086
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
8063
9087
|
},
|
|
8064
9088
|
subCommands: {
|
|
@@ -8066,6 +9090,7 @@ const main = defineCommand({
|
|
|
8066
9090
|
start,
|
|
8067
9091
|
stop,
|
|
8068
9092
|
status,
|
|
9093
|
+
credentials,
|
|
8069
9094
|
logs,
|
|
8070
9095
|
open,
|
|
8071
9096
|
db,
|