@metabase/rde 0.1.2 → 0.1.3-alpha.rde-fixes.59b956e
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 +72 -40
- package/dist/cli.mjs +1115 -378
- 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,17 @@ 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 stdoutIsTty() {
|
|
131
|
+
return process.stdout.isTTY === true;
|
|
132
|
+
}
|
|
133
|
+
function printJson(value) {
|
|
134
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
135
|
+
}
|
|
120
136
|
const StepId = z.enum([
|
|
121
137
|
"preflight",
|
|
122
138
|
"install-mb",
|
|
@@ -157,14 +173,36 @@ const JarRuntimeState = z.object({
|
|
|
157
173
|
});
|
|
158
174
|
const RuntimeState = z.discriminatedUnion("kind", [DockerRuntimeState, JarRuntimeState]);
|
|
159
175
|
const RuntimeKind = z.enum(["docker", "jar"]);
|
|
160
|
-
const AdminCredentials = z.discriminatedUnion("kind", [
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
176
|
+
const AdminCredentials = z.discriminatedUnion("kind", [
|
|
177
|
+
z.object({
|
|
178
|
+
kind: z.literal("generated"),
|
|
179
|
+
email: z.string()
|
|
180
|
+
}),
|
|
181
|
+
z.object({
|
|
182
|
+
kind: z.literal("prompted"),
|
|
183
|
+
email: z.string()
|
|
184
|
+
}),
|
|
185
|
+
z.object({
|
|
186
|
+
kind: z.literal("legacy-default"),
|
|
187
|
+
email: z.string()
|
|
188
|
+
})
|
|
189
|
+
]);
|
|
190
|
+
const AdminLogin = z.object({
|
|
165
191
|
email: z.string(),
|
|
166
192
|
password: z.string()
|
|
167
|
-
})
|
|
193
|
+
});
|
|
194
|
+
const InstanceSecrets = z.object({
|
|
195
|
+
schemaVersion: z.literal(1),
|
|
196
|
+
admin: AdminLogin.nullable(),
|
|
197
|
+
apiKey: z.string().nullable()
|
|
198
|
+
});
|
|
199
|
+
function emptyInstanceSecrets() {
|
|
200
|
+
return {
|
|
201
|
+
schemaVersion: 1,
|
|
202
|
+
admin: null,
|
|
203
|
+
apiKey: null
|
|
204
|
+
};
|
|
205
|
+
}
|
|
168
206
|
const LicenseState = z.discriminatedUnion("activated", [z.object({ activated: z.literal(false) }), z.object({
|
|
169
207
|
activated: z.literal(true),
|
|
170
208
|
features: z.array(z.string()),
|
|
@@ -202,13 +240,12 @@ const StepRecord = z.object({
|
|
|
202
240
|
completedAt: z.string()
|
|
203
241
|
});
|
|
204
242
|
const InstanceState = z.object({
|
|
205
|
-
schemaVersion: z.literal(
|
|
243
|
+
schemaVersion: z.literal(4),
|
|
206
244
|
name: z.string(),
|
|
207
245
|
runtime: RuntimeState.nullable(),
|
|
208
246
|
url: z.string().nullable(),
|
|
209
247
|
version: z.string().nullable(),
|
|
210
248
|
admin: AdminCredentials.nullable(),
|
|
211
|
-
apiKey: z.string().nullable(),
|
|
212
249
|
mbProfile: z.string(),
|
|
213
250
|
license: LicenseState,
|
|
214
251
|
databases: z.array(DatabaseRecord),
|
|
@@ -230,20 +267,22 @@ function requireConfiguredRuntime(state) {
|
|
|
230
267
|
}
|
|
231
268
|
const DEFAULT_INSTANCE_NAME = "default";
|
|
232
269
|
const DEFAULT_MB_PROFILE = "default";
|
|
233
|
-
const
|
|
270
|
+
const RDE_MB_PROFILE = "rde";
|
|
271
|
+
function mbProfileFor(instance) {
|
|
272
|
+
return instance === "default" ? RDE_MB_PROFILE : `${RDE_MB_PROFILE}-${instance}`;
|
|
273
|
+
}
|
|
234
274
|
function mbCommand(profile) {
|
|
235
275
|
return profile === "default" ? "mb" : `mb --profile ${profile}`;
|
|
236
276
|
}
|
|
237
277
|
function emptyInstanceState(name) {
|
|
238
278
|
return {
|
|
239
|
-
schemaVersion:
|
|
279
|
+
schemaVersion: 4,
|
|
240
280
|
name,
|
|
241
281
|
runtime: null,
|
|
242
282
|
url: null,
|
|
243
283
|
version: null,
|
|
244
284
|
admin: null,
|
|
245
|
-
|
|
246
|
-
mbProfile: DEFAULT_MB_PROFILE,
|
|
285
|
+
mbProfile: mbProfileFor(name),
|
|
247
286
|
license: { activated: false },
|
|
248
287
|
databases: [],
|
|
249
288
|
sampleDatabase: null,
|
|
@@ -256,8 +295,22 @@ function emptyInstanceState(name) {
|
|
|
256
295
|
const Versioned = z.object({ schemaVersion: z.int() });
|
|
257
296
|
const V1 = 1;
|
|
258
297
|
const V2 = 2;
|
|
298
|
+
const V3 = 3;
|
|
259
299
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
260
300
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
301
|
+
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
302
|
+
kind: z.literal("prompted"),
|
|
303
|
+
email: z.string()
|
|
304
|
+
}), z.object({
|
|
305
|
+
kind: z.literal("default"),
|
|
306
|
+
email: z.string(),
|
|
307
|
+
password: z.string()
|
|
308
|
+
})]);
|
|
309
|
+
const StateV3 = z.object({
|
|
310
|
+
schemaVersion: z.literal(V3),
|
|
311
|
+
admin: AdminV3.nullable(),
|
|
312
|
+
apiKey: z.string().nullable()
|
|
313
|
+
}).loose();
|
|
261
314
|
function liftV1(raw) {
|
|
262
315
|
return {
|
|
263
316
|
...StateV1.parse(raw),
|
|
@@ -268,27 +321,67 @@ function liftV1(raw) {
|
|
|
268
321
|
function liftV2(raw) {
|
|
269
322
|
return {
|
|
270
323
|
...StateV2.parse(raw),
|
|
271
|
-
schemaVersion:
|
|
324
|
+
schemaVersion: V3,
|
|
272
325
|
sampleDatabase: null
|
|
273
326
|
};
|
|
274
327
|
}
|
|
328
|
+
function adminV4(admin) {
|
|
329
|
+
switch (admin.kind) {
|
|
330
|
+
case "default": return {
|
|
331
|
+
kind: "legacy-default",
|
|
332
|
+
email: admin.email
|
|
333
|
+
};
|
|
334
|
+
case "prompted": return {
|
|
335
|
+
kind: "prompted",
|
|
336
|
+
email: admin.email
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
function liftV3(raw) {
|
|
341
|
+
const { admin, apiKey, ...rest } = StateV3.parse(raw);
|
|
342
|
+
const login = admin?.kind === "default" ? {
|
|
343
|
+
email: admin.email,
|
|
344
|
+
password: admin.password
|
|
345
|
+
} : null;
|
|
346
|
+
return {
|
|
347
|
+
state: InstanceState.parse({
|
|
348
|
+
...rest,
|
|
349
|
+
schemaVersion: 4,
|
|
350
|
+
admin: admin === null ? null : adminV4(admin)
|
|
351
|
+
}),
|
|
352
|
+
movedSecrets: {
|
|
353
|
+
schemaVersion: 1,
|
|
354
|
+
admin: login,
|
|
355
|
+
apiKey
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
}
|
|
275
359
|
function migrateInstanceState(raw) {
|
|
276
360
|
const { schemaVersion } = Versioned.parse(raw);
|
|
277
361
|
switch (schemaVersion) {
|
|
278
362
|
case V1: return migrateInstanceState(liftV1(raw));
|
|
279
363
|
case V2: return migrateInstanceState(liftV2(raw));
|
|
280
|
-
case
|
|
281
|
-
|
|
364
|
+
case V3: return liftV3(raw);
|
|
365
|
+
case 4: return {
|
|
366
|
+
state: InstanceState.parse(raw),
|
|
367
|
+
movedSecrets: null
|
|
368
|
+
};
|
|
369
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 4)`);
|
|
282
370
|
}
|
|
283
371
|
}
|
|
284
372
|
//#endregion
|
|
285
373
|
//#region src/env.ts
|
|
286
374
|
const ENV_RDE_HOME = "RDE_HOME";
|
|
375
|
+
const ENV_RDE_SKILL_SOURCE = "RDE_SKILL_SOURCE";
|
|
287
376
|
function readEnv(name) {
|
|
288
377
|
return process.env[name];
|
|
289
378
|
}
|
|
290
379
|
const ENV_PATH = "PATH";
|
|
380
|
+
const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
|
|
291
381
|
const ENV_JAVA_HOME = "JAVA_HOME";
|
|
382
|
+
const ENV_MB_PROFILE = "MB_PROFILE";
|
|
383
|
+
const ENV_MB_URL = "MB_URL";
|
|
384
|
+
const ENV_MB_API_KEY = "MB_API_KEY";
|
|
292
385
|
function environmentWith(overrides) {
|
|
293
386
|
return {
|
|
294
387
|
...process.env,
|
|
@@ -302,6 +395,7 @@ const INSTANCES_DIR = "instances";
|
|
|
302
395
|
const DATA_DIR = "data";
|
|
303
396
|
const BACKUPS_DIR = "backups";
|
|
304
397
|
const STATE_FILE = "state.json";
|
|
398
|
+
const SECRETS_FILE = "secrets.json";
|
|
305
399
|
const GLOBAL_CONFIG_FILE = "config.json";
|
|
306
400
|
const JARS_DIR = "jars";
|
|
307
401
|
const JAR_FILE$1 = "metabase.jar";
|
|
@@ -322,8 +416,17 @@ function instanceDataDir(name) {
|
|
|
322
416
|
function instanceBackupsDir(name) {
|
|
323
417
|
return join(instanceDir(name), BACKUPS_DIR);
|
|
324
418
|
}
|
|
419
|
+
function stateFileIn(dir) {
|
|
420
|
+
return join(dir, STATE_FILE);
|
|
421
|
+
}
|
|
422
|
+
function secretsFileIn(dir) {
|
|
423
|
+
return join(dir, SECRETS_FILE);
|
|
424
|
+
}
|
|
325
425
|
function instanceStatePath(name) {
|
|
326
|
-
return
|
|
426
|
+
return stateFileIn(instanceDir(name));
|
|
427
|
+
}
|
|
428
|
+
function instanceSecretsPath(name) {
|
|
429
|
+
return secretsFileIn(instanceDir(name));
|
|
327
430
|
}
|
|
328
431
|
function globalConfigPath() {
|
|
329
432
|
return join(rdeHome(), GLOBAL_CONFIG_FILE);
|
|
@@ -337,21 +440,21 @@ function jarReleaseDir(version) {
|
|
|
337
440
|
function releaseJarIn(jarsDirectory, version) {
|
|
338
441
|
return join(jarsDirectory, version, JAR_FILE$1);
|
|
339
442
|
}
|
|
340
|
-
|
|
341
|
-
//#region src/state/store.ts
|
|
342
|
-
const FILE_MODE$1 = 384;
|
|
343
|
-
const DIR_MODE = 448;
|
|
443
|
+
const PERMISSION_BITS = 511;
|
|
344
444
|
function isFileNotFound$1(error) {
|
|
345
445
|
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
346
446
|
}
|
|
347
|
-
async function
|
|
348
|
-
let raw;
|
|
447
|
+
async function readTextFile(path) {
|
|
349
448
|
try {
|
|
350
|
-
|
|
449
|
+
return await promises.readFile(path, "utf8");
|
|
351
450
|
} catch (error) {
|
|
352
451
|
if (isFileNotFound$1(error)) return null;
|
|
353
452
|
throw error;
|
|
354
453
|
}
|
|
454
|
+
}
|
|
455
|
+
async function readJsonFile(path) {
|
|
456
|
+
const raw = await readTextFile(path);
|
|
457
|
+
if (raw === null) return null;
|
|
355
458
|
try {
|
|
356
459
|
return JSON.parse(raw);
|
|
357
460
|
} catch (error) {
|
|
@@ -370,11 +473,11 @@ function parseAt(path, parse) {
|
|
|
370
473
|
async function writeJsonFile(path, value) {
|
|
371
474
|
await promises.mkdir(dirname(path), {
|
|
372
475
|
recursive: true,
|
|
373
|
-
mode:
|
|
476
|
+
mode: 448
|
|
374
477
|
});
|
|
375
478
|
const tmpPath = `${path}.${process.pid}.tmp`;
|
|
376
|
-
await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode:
|
|
377
|
-
await promises.chmod(tmpPath,
|
|
479
|
+
await promises.writeFile(tmpPath, JSON.stringify(value, null, 2) + "\n", { mode: 384 });
|
|
480
|
+
await promises.chmod(tmpPath, 384);
|
|
378
481
|
try {
|
|
379
482
|
await promises.rename(tmpPath, path);
|
|
380
483
|
} catch (error) {
|
|
@@ -386,7 +489,12 @@ async function readInstanceState(name) {
|
|
|
386
489
|
const path = instanceStatePath(name);
|
|
387
490
|
const raw = await readJsonFile(path);
|
|
388
491
|
if (raw === null) return null;
|
|
389
|
-
|
|
492
|
+
const migrated = parseAt(path, () => migrateInstanceState(raw));
|
|
493
|
+
if (migrated.movedSecrets !== null) {
|
|
494
|
+
await writeInstanceSecrets(name, migrated.movedSecrets);
|
|
495
|
+
await writeInstanceState(migrated.state);
|
|
496
|
+
}
|
|
497
|
+
return migrated.state;
|
|
390
498
|
}
|
|
391
499
|
async function writeInstanceState(state) {
|
|
392
500
|
await writeJsonFile(instanceStatePath(state.name), state);
|
|
@@ -396,6 +504,35 @@ async function requireInstanceState(name) {
|
|
|
396
504
|
if (state === null) throw new ConfigError(`no instance named ${name}; run \`rde init\` first`);
|
|
397
505
|
return state;
|
|
398
506
|
}
|
|
507
|
+
async function readInstanceSecrets(name) {
|
|
508
|
+
const path = instanceSecretsPath(name);
|
|
509
|
+
const raw = await readJsonFile(path);
|
|
510
|
+
if (raw === null) return null;
|
|
511
|
+
return parseAt(path, () => InstanceSecrets.parse(raw));
|
|
512
|
+
}
|
|
513
|
+
async function writeInstanceSecrets(name, secrets) {
|
|
514
|
+
await writeJsonFile(instanceSecretsPath(name), secrets);
|
|
515
|
+
await promises.chmod(instanceDir(name), 448);
|
|
516
|
+
}
|
|
517
|
+
async function modeOf(path) {
|
|
518
|
+
try {
|
|
519
|
+
return (await promises.stat(path)).mode & PERMISSION_BITS;
|
|
520
|
+
} catch (error) {
|
|
521
|
+
if (isFileNotFound$1(error)) return null;
|
|
522
|
+
throw error;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
const StateWithApiKey = z.object({ apiKey: z.string() }).loose();
|
|
526
|
+
const StateWithPassword = z.object({ admin: z.object({ password: z.string() }).loose() }).loose();
|
|
527
|
+
async function inspectCredentialFiles(dir) {
|
|
528
|
+
const raw = await readJsonFile(stateFileIn(dir));
|
|
529
|
+
const secretsInState = [...StateWithApiKey.safeParse(raw).success ? ["apiKey"] : [], ...StateWithPassword.safeParse(raw).success ? ["admin.password"] : []];
|
|
530
|
+
return {
|
|
531
|
+
dirMode: await modeOf(dir),
|
|
532
|
+
secretsMode: await modeOf(secretsFileIn(dir)),
|
|
533
|
+
secretsInState
|
|
534
|
+
};
|
|
535
|
+
}
|
|
399
536
|
async function readGlobalConfig() {
|
|
400
537
|
const path = globalConfigPath();
|
|
401
538
|
const raw = await readJsonFile(path);
|
|
@@ -406,6 +543,91 @@ async function writeGlobalConfig(config) {
|
|
|
406
543
|
await writeJsonFile(globalConfigPath(), config);
|
|
407
544
|
}
|
|
408
545
|
//#endregion
|
|
546
|
+
//#region src/steps/credentials.ts
|
|
547
|
+
const TERMINAL_ONLY = "rde credentials prints only to a terminal; run it in your own terminal";
|
|
548
|
+
function storedPasswordLines(state, secrets, email) {
|
|
549
|
+
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`);
|
|
550
|
+
return [`Email: ${email}`, `Password: ${secrets.admin.password}`];
|
|
551
|
+
}
|
|
552
|
+
function credentialsLines(state, secrets) {
|
|
553
|
+
const admin = state.admin;
|
|
554
|
+
if (admin === null) throw new ConfigError(`instance ${state.name} has no admin user yet; run \`rde init\``);
|
|
555
|
+
const url = state.url === null ? [] : [`Metabase: ${state.url}`];
|
|
556
|
+
switch (admin.kind) {
|
|
557
|
+
case "generated": return [...url, ...storedPasswordLines(state, secrets, admin.email)];
|
|
558
|
+
case "legacy-default": return [
|
|
559
|
+
...url,
|
|
560
|
+
...storedPasswordLines(state, secrets, admin.email),
|
|
561
|
+
"This is the public default password; when the instance holds nothing you need, `rde uninstall` then `rde init` recreates it with a generated one"
|
|
562
|
+
];
|
|
563
|
+
case "prompted": return [
|
|
564
|
+
...url,
|
|
565
|
+
`Email: ${admin.email}`,
|
|
566
|
+
"Password: the one you entered during `rde init`; rde never stored it"
|
|
567
|
+
];
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
//#endregion
|
|
571
|
+
//#region src/commands/define.ts
|
|
572
|
+
function repeatedValues(rawArgs, flag) {
|
|
573
|
+
const values = [];
|
|
574
|
+
const inline = `${flag}=`;
|
|
575
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
576
|
+
const arg = rawArgs[index];
|
|
577
|
+
if (arg === void 0) continue;
|
|
578
|
+
if (arg.startsWith(inline)) values.push(arg.slice(inline.length));
|
|
579
|
+
else if (arg === flag) {
|
|
580
|
+
const next = rawArgs[index + 1];
|
|
581
|
+
if (next !== void 0) {
|
|
582
|
+
values.push(next);
|
|
583
|
+
index += 1;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return values;
|
|
588
|
+
}
|
|
589
|
+
function defineRdeCommand(def) {
|
|
590
|
+
return defineCommand({
|
|
591
|
+
meta: def.meta,
|
|
592
|
+
...def.args !== void 0 && { args: def.args },
|
|
593
|
+
async run({ args, rawArgs }) {
|
|
594
|
+
try {
|
|
595
|
+
await def.run(args, rawArgs);
|
|
596
|
+
} catch (error) {
|
|
597
|
+
reportError(error);
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
//#endregion
|
|
603
|
+
//#region src/commands/instance.ts
|
|
604
|
+
async function defaultInstanceName() {
|
|
605
|
+
const config = await readGlobalConfig();
|
|
606
|
+
if (config === null) throw new ConfigError("no Metabase instance yet; run `rde init` first");
|
|
607
|
+
return config.defaultInstance;
|
|
608
|
+
}
|
|
609
|
+
async function instanceNameOrDefault() {
|
|
610
|
+
try {
|
|
611
|
+
return (await readGlobalConfig())?.defaultInstance ?? "default";
|
|
612
|
+
} catch (error) {
|
|
613
|
+
if (error instanceof ConfigError) return DEFAULT_INSTANCE_NAME;
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
//#endregion
|
|
618
|
+
//#region src/commands/credentials.ts
|
|
619
|
+
const credentials = defineRdeCommand({
|
|
620
|
+
meta: {
|
|
621
|
+
name: "credentials",
|
|
622
|
+
description: "Print the admin login of the instance; only to a terminal"
|
|
623
|
+
},
|
|
624
|
+
async run() {
|
|
625
|
+
if (!stdoutIsTty()) throw new ConfigError(TERMINAL_ONLY);
|
|
626
|
+
const name = await defaultInstanceName();
|
|
627
|
+
printLines(credentialsLines(await requireInstanceState(name), await readInstanceSecrets(name)));
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
//#endregion
|
|
409
631
|
//#region src/connections/parse-uri.ts
|
|
410
632
|
var UriParseError = class extends Error {
|
|
411
633
|
engine;
|
|
@@ -1072,6 +1294,18 @@ function tokenStatusResource(http) {
|
|
|
1072
1294
|
return { get };
|
|
1073
1295
|
}
|
|
1074
1296
|
//#endregion
|
|
1297
|
+
//#region src/metabase/domain/user.ts
|
|
1298
|
+
const CurrentUser = z.object({ id: z.int() }).loose();
|
|
1299
|
+
//#endregion
|
|
1300
|
+
//#region src/metabase/resources/user.ts
|
|
1301
|
+
function userResource(http) {
|
|
1302
|
+
/** GET /api/user/current */
|
|
1303
|
+
async function current() {
|
|
1304
|
+
return http.requestJson("GET", "/api/user/current", { schema: CurrentUser });
|
|
1305
|
+
}
|
|
1306
|
+
return { current };
|
|
1307
|
+
}
|
|
1308
|
+
//#endregion
|
|
1075
1309
|
//#region src/metabase/client.ts
|
|
1076
1310
|
const HEALTH_POLL_INTERVAL_MS = 2e3;
|
|
1077
1311
|
function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
@@ -1092,7 +1326,8 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
|
|
|
1092
1326
|
setting: settingResource(http),
|
|
1093
1327
|
tokenStatus: tokenStatusResource(http),
|
|
1094
1328
|
database: databaseResource(http),
|
|
1095
|
-
remoteSync: remoteSyncResource(http)
|
|
1329
|
+
remoteSync: remoteSyncResource(http),
|
|
1330
|
+
user: userResource(http)
|
|
1096
1331
|
};
|
|
1097
1332
|
}
|
|
1098
1333
|
function isNotReadyYet(error) {
|
|
@@ -2214,9 +2449,9 @@ function hostDatabasesDeps(signal) {
|
|
|
2214
2449
|
function isNotServingYet$1(error) {
|
|
2215
2450
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
2216
2451
|
}
|
|
2217
|
-
function requireApiKey$2(state) {
|
|
2218
|
-
if (
|
|
2219
|
-
return
|
|
2452
|
+
function requireApiKey$2(state, apiKey) {
|
|
2453
|
+
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
2454
|
+
return apiKey;
|
|
2220
2455
|
}
|
|
2221
2456
|
function hostAliasOf(runtime) {
|
|
2222
2457
|
switch (runtime.kind) {
|
|
@@ -2232,15 +2467,15 @@ function toRecord$1(database, createdAt) {
|
|
|
2232
2467
|
createdAt
|
|
2233
2468
|
};
|
|
2234
2469
|
}
|
|
2235
|
-
function authenticatedClient(state, deps) {
|
|
2470
|
+
function authenticatedClient(state, apiKey, deps) {
|
|
2236
2471
|
const configured = requireConfiguredRuntime(state);
|
|
2237
2472
|
return deps.client(configured.url, {
|
|
2238
2473
|
kind: "api-key",
|
|
2239
|
-
key: requireApiKey$2(state)
|
|
2474
|
+
key: requireApiKey$2(state, apiKey)
|
|
2240
2475
|
});
|
|
2241
2476
|
}
|
|
2242
|
-
async function listDatabases(state, deps) {
|
|
2243
|
-
const listed = await authenticatedClient(state, deps).database.list();
|
|
2477
|
+
async function listDatabases(state, apiKey, deps) {
|
|
2478
|
+
const listed = await authenticatedClient(state, apiKey, deps).database.list();
|
|
2244
2479
|
const records = new Map(state.databases.map((record) => [record.id, record]));
|
|
2245
2480
|
const onServer = listed.data.map((database) => ({
|
|
2246
2481
|
id: database.id,
|
|
@@ -2261,7 +2496,7 @@ async function listDatabases(state, deps) {
|
|
|
2261
2496
|
}
|
|
2262
2497
|
async function addDatabases(ctx, deps) {
|
|
2263
2498
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
2264
|
-
const client = authenticatedClient(ctx.state, deps);
|
|
2499
|
+
const client = authenticatedClient(ctx.state, ctx.secrets.apiKey, deps);
|
|
2265
2500
|
const properties = await client.session.properties();
|
|
2266
2501
|
const existing = await client.database.list();
|
|
2267
2502
|
if (existing.data.length > 0) ctx.ui.note(existingDatabaseLines(existing.data).join("\n"), "Connected databases");
|
|
@@ -2299,17 +2534,17 @@ function databasesStep(options, deps = hostDeps$12) {
|
|
|
2299
2534
|
title: "Connect your databases",
|
|
2300
2535
|
async detect(ctx) {
|
|
2301
2536
|
const configured = configuredRuntime(ctx.state);
|
|
2302
|
-
if (configured === null || ctx.
|
|
2537
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
2303
2538
|
const own = ownDatabaseIds(ctx.state);
|
|
2304
2539
|
if (own.size === 0) return sampleDatabaseId(ctx.state) !== null && !options.requested ? "done" : "todo";
|
|
2305
2540
|
const client = deps(ctx).client(configured.url, {
|
|
2306
2541
|
kind: "api-key",
|
|
2307
|
-
key: ctx.
|
|
2542
|
+
key: ctx.secrets.apiKey
|
|
2308
2543
|
});
|
|
2309
2544
|
try {
|
|
2310
2545
|
return (await client.database.list()).data.some((database) => own.has(database.id)) ? "done" : "todo";
|
|
2311
2546
|
} catch (error) {
|
|
2312
|
-
if (isNotServingYet$1(error)) return "todo";
|
|
2547
|
+
if (isNotServingYet$1(error) || isRejectedCredential(error)) return "todo";
|
|
2313
2548
|
throw error;
|
|
2314
2549
|
}
|
|
2315
2550
|
},
|
|
@@ -2391,6 +2626,11 @@ async function createWizardContext(instanceName, options) {
|
|
|
2391
2626
|
await writeInstanceState(next);
|
|
2392
2627
|
ctx.state = next;
|
|
2393
2628
|
},
|
|
2629
|
+
secrets: await readInstanceSecrets(instanceName) ?? emptyInstanceSecrets(),
|
|
2630
|
+
saveSecrets: async (next) => {
|
|
2631
|
+
await writeInstanceSecrets(instanceName, next);
|
|
2632
|
+
ctx.secrets = next;
|
|
2633
|
+
},
|
|
2394
2634
|
prompts: options.prompts ?? clackPrompts(),
|
|
2395
2635
|
ui: clackUi(),
|
|
2396
2636
|
signal: options.signal,
|
|
@@ -2399,53 +2639,6 @@ async function createWizardContext(instanceName, options) {
|
|
|
2399
2639
|
return ctx;
|
|
2400
2640
|
}
|
|
2401
2641
|
//#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
2642
|
//#region src/commands/db/add.ts
|
|
2450
2643
|
const dbAdd = defineRdeCommand({
|
|
2451
2644
|
meta: {
|
|
@@ -2460,14 +2653,6 @@ const dbAdd = defineRdeCommand({
|
|
|
2460
2653
|
}
|
|
2461
2654
|
});
|
|
2462
2655
|
//#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
2656
|
//#region src/commands/db/list.ts
|
|
2472
2657
|
const dbList = defineRdeCommand({
|
|
2473
2658
|
meta: {
|
|
@@ -2480,7 +2665,10 @@ const dbList = defineRdeCommand({
|
|
|
2480
2665
|
default: false
|
|
2481
2666
|
} },
|
|
2482
2667
|
async run(args) {
|
|
2483
|
-
const
|
|
2668
|
+
const name = await defaultInstanceName();
|
|
2669
|
+
const state = await requireInstanceState(name);
|
|
2670
|
+
const secrets = await readInstanceSecrets(name);
|
|
2671
|
+
const listing = await listDatabases(state, secrets === null ? null : secrets.apiKey, hostDatabasesDeps(interruptSignal));
|
|
2484
2672
|
if (args.json) {
|
|
2485
2673
|
printJson(listing);
|
|
2486
2674
|
return;
|
|
@@ -2809,15 +2997,20 @@ async function isExecutable(path) {
|
|
|
2809
2997
|
return false;
|
|
2810
2998
|
}
|
|
2811
2999
|
}
|
|
2812
|
-
async function
|
|
3000
|
+
async function whichAll(name) {
|
|
2813
3001
|
const searchPath = readEnv(ENV_PATH);
|
|
2814
|
-
if (searchPath === void 0) return
|
|
2815
|
-
|
|
3002
|
+
if (searchPath === void 0) return [];
|
|
3003
|
+
const found = [];
|
|
3004
|
+
for (const dir of new Set(searchPath.split(delimiter))) {
|
|
2816
3005
|
if (dir.length === 0) continue;
|
|
2817
3006
|
const candidate = join(dir, name);
|
|
2818
|
-
if (await isExecutable(candidate))
|
|
3007
|
+
if (await isExecutable(candidate)) found.push(candidate);
|
|
2819
3008
|
}
|
|
2820
|
-
return
|
|
3009
|
+
return found;
|
|
3010
|
+
}
|
|
3011
|
+
async function whichCommand(name) {
|
|
3012
|
+
const [first] = await whichAll(name);
|
|
3013
|
+
return first ?? null;
|
|
2821
3014
|
}
|
|
2822
3015
|
//#endregion
|
|
2823
3016
|
//#region src/tools/java.ts
|
|
@@ -3325,7 +3518,7 @@ function daemonTool(runner) {
|
|
|
3325
3518
|
}
|
|
3326
3519
|
//#endregion
|
|
3327
3520
|
//#region src/runtime/docker.ts
|
|
3328
|
-
const DEFAULT_IMAGE = "metabase/metabase-
|
|
3521
|
+
const DEFAULT_IMAGE = "metabase/metabase-enterprise-head:latest";
|
|
3329
3522
|
const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
|
|
3330
3523
|
const CONTAINER_PORT = 3e3;
|
|
3331
3524
|
function containerName(instanceName) {
|
|
@@ -3661,44 +3854,117 @@ const GLOBAL_INSTALL = {
|
|
|
3661
3854
|
function mbInstallCommand(packageManager) {
|
|
3662
3855
|
return GLOBAL_INSTALL[packageManager];
|
|
3663
3856
|
}
|
|
3857
|
+
function ownerByMarker(path, markers) {
|
|
3858
|
+
const normalized = path.replaceAll("\\", "/");
|
|
3859
|
+
const found = markers.find(({ marker }) => normalized.includes(marker));
|
|
3860
|
+
return found === void 0 ? null : found.owner;
|
|
3861
|
+
}
|
|
3664
3862
|
const GLOBAL_MARKERS = [
|
|
3665
3863
|
{
|
|
3666
3864
|
marker: "/.bun/install/global/node_modules/",
|
|
3667
|
-
|
|
3865
|
+
owner: "bun"
|
|
3668
3866
|
},
|
|
3669
3867
|
{
|
|
3670
3868
|
marker: "/Library/pnpm/global/",
|
|
3671
|
-
|
|
3869
|
+
owner: "pnpm"
|
|
3672
3870
|
},
|
|
3673
3871
|
{
|
|
3674
3872
|
marker: "/.local/share/pnpm/global/",
|
|
3675
|
-
|
|
3873
|
+
owner: "pnpm"
|
|
3676
3874
|
},
|
|
3677
3875
|
{
|
|
3678
3876
|
marker: "/share/pnpm/global/",
|
|
3679
|
-
|
|
3877
|
+
owner: "pnpm"
|
|
3680
3878
|
},
|
|
3681
3879
|
{
|
|
3682
3880
|
marker: "/pnpm-global/node_modules/",
|
|
3683
|
-
|
|
3881
|
+
owner: "pnpm"
|
|
3684
3882
|
},
|
|
3685
3883
|
{
|
|
3686
3884
|
marker: "/.config/yarn/global/node_modules/",
|
|
3687
|
-
|
|
3885
|
+
owner: "yarn"
|
|
3688
3886
|
},
|
|
3689
3887
|
{
|
|
3690
3888
|
marker: "/lib/node_modules/",
|
|
3691
|
-
|
|
3889
|
+
owner: "npm"
|
|
3692
3890
|
},
|
|
3693
3891
|
{
|
|
3694
3892
|
marker: "/.npm-global/node_modules/",
|
|
3695
|
-
|
|
3893
|
+
owner: "npm"
|
|
3696
3894
|
}
|
|
3697
3895
|
];
|
|
3698
3896
|
function packageManagerFor(scriptPath) {
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3897
|
+
return ownerByMarker(scriptPath, GLOBAL_MARKERS) ?? "npm";
|
|
3898
|
+
}
|
|
3899
|
+
const MB_OWNER_NAMES = {
|
|
3900
|
+
npm: "npm",
|
|
3901
|
+
pnpm: "pnpm",
|
|
3902
|
+
yarn: "Yarn",
|
|
3903
|
+
bun: "Bun",
|
|
3904
|
+
homebrew: "Homebrew"
|
|
3905
|
+
};
|
|
3906
|
+
const CELLAR = "/Cellar/";
|
|
3907
|
+
const NODE_MODULES = "/node_modules/";
|
|
3908
|
+
const OWNER_MARKERS = [
|
|
3909
|
+
...GLOBAL_MARKERS,
|
|
3910
|
+
{
|
|
3911
|
+
marker: CELLAR,
|
|
3912
|
+
owner: "homebrew"
|
|
3913
|
+
},
|
|
3914
|
+
{
|
|
3915
|
+
marker: "/Library/pnpm/",
|
|
3916
|
+
owner: "pnpm"
|
|
3917
|
+
},
|
|
3918
|
+
{
|
|
3919
|
+
marker: "/.local/share/pnpm/",
|
|
3920
|
+
owner: "pnpm"
|
|
3921
|
+
}
|
|
3922
|
+
];
|
|
3923
|
+
async function locateMbOnPath() {
|
|
3924
|
+
const paths = await whichAll(MB);
|
|
3925
|
+
return Promise.all(paths.map(async (path) => ({
|
|
3926
|
+
path,
|
|
3927
|
+
resolved: await promises.realpath(path)
|
|
3928
|
+
})));
|
|
3929
|
+
}
|
|
3930
|
+
function mbOwner(binary) {
|
|
3931
|
+
return ownerByMarker(binary.resolved, OWNER_MARKERS) ?? ownerByMarker(binary.path, OWNER_MARKERS);
|
|
3932
|
+
}
|
|
3933
|
+
function firstSegmentAfter(path, marker) {
|
|
3934
|
+
const at = path.lastIndexOf(marker);
|
|
3935
|
+
if (at === -1) return null;
|
|
3936
|
+
const [first] = path.slice(at + marker.length).split("/");
|
|
3937
|
+
return first === void 0 || first.length === 0 ? null : first;
|
|
3938
|
+
}
|
|
3939
|
+
function packageName(resolved) {
|
|
3940
|
+
const scopeOrName = firstSegmentAfter(resolved, NODE_MODULES);
|
|
3941
|
+
if (scopeOrName === null || !scopeOrName.startsWith("@")) return scopeOrName;
|
|
3942
|
+
const name = firstSegmentAfter(resolved, `${NODE_MODULES}${scopeOrName}/`);
|
|
3943
|
+
return name === null ? null : `${scopeOrName}/${name}`;
|
|
3944
|
+
}
|
|
3945
|
+
const REMOVE_COMMANDS = {
|
|
3946
|
+
npm: (name) => `npm uninstall -g ${name}`,
|
|
3947
|
+
pnpm: (name) => `pnpm remove -g ${name}`,
|
|
3948
|
+
yarn: (name) => `yarn global remove ${name}`,
|
|
3949
|
+
bun: (name) => `bun remove -g ${name}`,
|
|
3950
|
+
homebrew: (name) => `brew uninstall ${name}`
|
|
3951
|
+
};
|
|
3952
|
+
function mbRemoveCommand(binary) {
|
|
3953
|
+
const owner = mbOwner(binary);
|
|
3954
|
+
if (owner === null) return null;
|
|
3955
|
+
const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
|
|
3956
|
+
return name === null ? null : REMOVE_COMMANDS[owner](name);
|
|
3957
|
+
}
|
|
3958
|
+
function setValue(name) {
|
|
3959
|
+
const value = readEnv(name);
|
|
3960
|
+
return value === void 0 || value.length === 0 ? null : value;
|
|
3961
|
+
}
|
|
3962
|
+
function mbEnvironment() {
|
|
3963
|
+
return {
|
|
3964
|
+
profile: setValue(ENV_MB_PROFILE),
|
|
3965
|
+
url: setValue(ENV_MB_URL),
|
|
3966
|
+
apiKeySet: setValue(ENV_MB_API_KEY) !== null
|
|
3967
|
+
};
|
|
3702
3968
|
}
|
|
3703
3969
|
const ProbedUser = z.object({
|
|
3704
3970
|
id: z.int(),
|
|
@@ -3775,16 +4041,16 @@ function mbTool(runner) {
|
|
|
3775
4041
|
if (result.exitCode !== 0) throw toolFailure(MB, argv, result);
|
|
3776
4042
|
return parseJsonOutput(MB, argv, result.stdout, schema);
|
|
3777
4043
|
}
|
|
3778
|
-
async function version() {
|
|
4044
|
+
async function version(command = MB) {
|
|
3779
4045
|
const args = ["--version"];
|
|
3780
4046
|
let result;
|
|
3781
4047
|
try {
|
|
3782
|
-
result = await runner.run(
|
|
4048
|
+
result = await runner.run(command, args);
|
|
3783
4049
|
} catch (error) {
|
|
3784
4050
|
if (error instanceof ProcessNotFoundError) return null;
|
|
3785
4051
|
throw error;
|
|
3786
4052
|
}
|
|
3787
|
-
if (result.exitCode !== 0) throw toolFailure(
|
|
4053
|
+
if (result.exitCode !== 0) throw toolFailure(command, args, result);
|
|
3788
4054
|
return result.stdout.trim();
|
|
3789
4055
|
}
|
|
3790
4056
|
async function authLogin(options) {
|
|
@@ -3870,73 +4136,65 @@ function mbTool(runner) {
|
|
|
3870
4136
|
};
|
|
3871
4137
|
}
|
|
3872
4138
|
//#endregion
|
|
3873
|
-
//#region src/
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
4139
|
+
//#region src/tools/skills-cli.ts
|
|
4140
|
+
const NPX = "npx";
|
|
4141
|
+
const SKILLS_PACKAGE = "skills@latest";
|
|
4142
|
+
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-skill";
|
|
4143
|
+
function rdeSkillSource() {
|
|
4144
|
+
const override = readEnv(ENV_RDE_SKILL_SOURCE);
|
|
4145
|
+
return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
|
|
3877
4146
|
}
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
return
|
|
4147
|
+
const InstalledSkill = z.object({
|
|
4148
|
+
name: z.string(),
|
|
4149
|
+
path: z.string(),
|
|
4150
|
+
scope: z.string(),
|
|
4151
|
+
agents: z.array(z.string()),
|
|
4152
|
+
source: z.string().nullable()
|
|
4153
|
+
}).loose();
|
|
4154
|
+
const InstalledSkillList = z.array(InstalledSkill);
|
|
4155
|
+
const SkillLock = z.object({ skills: z.record(z.string(), z.object({ ref: z.string().nullish() }).loose()) }).loose();
|
|
4156
|
+
const GLOBAL_LOCK_FILE = ".skill-lock.json";
|
|
4157
|
+
const PROJECT_LOCK_FILE = "skills-lock.json";
|
|
4158
|
+
function globalSkillLockPath(home, stateHome) {
|
|
4159
|
+
return stateHome === null ? join(home, ".agents", GLOBAL_LOCK_FILE) : join(stateHome, "skills", GLOBAL_LOCK_FILE);
|
|
3891
4160
|
}
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
if (options.only !== void 0 || entry.status === "done") return false;
|
|
3895
|
-
return await entry.step.detect(ctx) === "todo";
|
|
4161
|
+
function projectSkillLockPath(project) {
|
|
4162
|
+
return join(project, PROJECT_LOCK_FILE);
|
|
3896
4163
|
}
|
|
3897
|
-
function
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
completedAt: at.toISOString()
|
|
3901
|
-
};
|
|
4164
|
+
function xdgStateHome() {
|
|
4165
|
+
const value = readEnv(ENV_XDG_STATE_HOME);
|
|
4166
|
+
return value === void 0 || value.length === 0 ? null : value;
|
|
3902
4167
|
}
|
|
3903
|
-
function
|
|
3904
|
-
return
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
[id]: record
|
|
3909
|
-
}
|
|
3910
|
-
};
|
|
4168
|
+
function lockedRef(path, raw, skill) {
|
|
4169
|
+
if (raw === null) return null;
|
|
4170
|
+
const parsed = SkillLock.safeParse(raw);
|
|
4171
|
+
if (!parsed.success) throw new ConfigError(`${path} is not a skills lock file: ${parsed.error.message}`);
|
|
4172
|
+
return parsed.data.skills[skill]?.ref ?? null;
|
|
3911
4173
|
}
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
4174
|
+
const hostSkillFiles = {
|
|
4175
|
+
readJson: readJsonFile,
|
|
4176
|
+
readText: readTextFile
|
|
4177
|
+
};
|
|
4178
|
+
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
4179
|
+
function skillOrigin(source, cwd) {
|
|
4180
|
+
const github = GITHUB_URL.exec(source);
|
|
4181
|
+
if (github !== null) {
|
|
4182
|
+
const [, owner, repo, ref] = github;
|
|
4183
|
+
return {
|
|
4184
|
+
kind: "github",
|
|
4185
|
+
repository: `${owner}/${repo}`,
|
|
4186
|
+
ref: ref ?? null
|
|
4187
|
+
};
|
|
3925
4188
|
}
|
|
4189
|
+
if (isAbsolute(source) || source.startsWith(".")) return {
|
|
4190
|
+
kind: "local",
|
|
4191
|
+
path: resolve(cwd, source)
|
|
4192
|
+
};
|
|
4193
|
+
return {
|
|
4194
|
+
kind: "other",
|
|
4195
|
+
source
|
|
4196
|
+
};
|
|
3926
4197
|
}
|
|
3927
|
-
//#endregion
|
|
3928
|
-
//#region src/tools/skills-cli.ts
|
|
3929
|
-
const NPX = "npx";
|
|
3930
|
-
const SKILLS_PACKAGE = "skills@latest";
|
|
3931
|
-
const RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/rde-generic-skills";
|
|
3932
|
-
const InstalledSkill = z.object({
|
|
3933
|
-
name: z.string(),
|
|
3934
|
-
path: z.string(),
|
|
3935
|
-
scope: z.string(),
|
|
3936
|
-
agents: z.array(z.string()),
|
|
3937
|
-
source: z.string().nullable()
|
|
3938
|
-
}).loose();
|
|
3939
|
-
const InstalledSkillList = z.array(InstalledSkill);
|
|
3940
4198
|
function skillsCli(runner) {
|
|
3941
4199
|
async function add(options) {
|
|
3942
4200
|
const argv = [
|
|
@@ -3988,12 +4246,82 @@ function skillsCli(runner) {
|
|
|
3988
4246
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
3989
4247
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
3990
4248
|
}
|
|
4249
|
+
async function listProject(cwd) {
|
|
4250
|
+
const argv = [
|
|
4251
|
+
"-y",
|
|
4252
|
+
SKILLS_PACKAGE,
|
|
4253
|
+
"list",
|
|
4254
|
+
"--json"
|
|
4255
|
+
];
|
|
4256
|
+
const result = await runner.run(NPX, argv, {
|
|
4257
|
+
cwd,
|
|
4258
|
+
stdoutToFile: true
|
|
4259
|
+
});
|
|
4260
|
+
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
4261
|
+
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
4262
|
+
}
|
|
3991
4263
|
return {
|
|
3992
4264
|
add,
|
|
3993
4265
|
remove,
|
|
3994
|
-
list
|
|
4266
|
+
list,
|
|
4267
|
+
listProject
|
|
4268
|
+
};
|
|
4269
|
+
}
|
|
4270
|
+
//#endregion
|
|
4271
|
+
//#region src/wizard/engine.ts
|
|
4272
|
+
function requireKnownStep(steps, id, flag) {
|
|
4273
|
+
if (id === void 0) return;
|
|
4274
|
+
if (!steps.some((step) => step.id === id)) throw new ConfigError(`${flag} ${id} is not one of the steps: ${steps.map((step) => step.id).join(", ")}`);
|
|
4275
|
+
}
|
|
4276
|
+
async function detectAll(steps, ctx, options) {
|
|
4277
|
+
const detected = [];
|
|
4278
|
+
let fromHere = false;
|
|
4279
|
+
for (const step of steps) {
|
|
4280
|
+
if (step.id === options.from) fromHere = true;
|
|
4281
|
+
const forced = options.only !== void 0 ? step.id === options.only : fromHere;
|
|
4282
|
+
detected.push({
|
|
4283
|
+
step,
|
|
4284
|
+
status: forced ? "todo" : await step.detect(ctx),
|
|
4285
|
+
forced
|
|
4286
|
+
});
|
|
4287
|
+
}
|
|
4288
|
+
return detected;
|
|
4289
|
+
}
|
|
4290
|
+
async function shouldRun(entry, ctx, options) {
|
|
4291
|
+
if (entry.forced) return true;
|
|
4292
|
+
if (options.only !== void 0 || entry.status === "done") return false;
|
|
4293
|
+
return await entry.step.detect(ctx) === "todo";
|
|
4294
|
+
}
|
|
4295
|
+
function recordFor(outcome, at) {
|
|
4296
|
+
return {
|
|
4297
|
+
status: outcome === "completed" ? "done" : "skipped",
|
|
4298
|
+
completedAt: at.toISOString()
|
|
4299
|
+
};
|
|
4300
|
+
}
|
|
4301
|
+
function withStepRecord(state, id, record) {
|
|
4302
|
+
return {
|
|
4303
|
+
...state,
|
|
4304
|
+
steps: {
|
|
4305
|
+
...state.steps,
|
|
4306
|
+
[id]: record
|
|
4307
|
+
}
|
|
3995
4308
|
};
|
|
3996
4309
|
}
|
|
4310
|
+
async function runWizard(steps, ctx, options = {}) {
|
|
4311
|
+
requireKnownStep(steps, options.from, "--from");
|
|
4312
|
+
requireKnownStep(steps, options.only, "--only");
|
|
4313
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
4314
|
+
const detected = await detectAll(steps, ctx, options);
|
|
4315
|
+
ctx.ui.checklist(detected.map(({ step, status }) => ({
|
|
4316
|
+
title: step.title,
|
|
4317
|
+
status
|
|
4318
|
+
})));
|
|
4319
|
+
for (const entry of detected) {
|
|
4320
|
+
if (!await shouldRun(entry, ctx, options)) continue;
|
|
4321
|
+
const outcome = await entry.step.run(ctx);
|
|
4322
|
+
await ctx.saveState(withStepRecord(ctx.state, entry.step.id, recordFor(outcome, now())));
|
|
4323
|
+
}
|
|
4324
|
+
}
|
|
3997
4325
|
//#endregion
|
|
3998
4326
|
//#region src/steps/agents-skills.ts
|
|
3999
4327
|
const STEP_ID$8 = "agents-skills";
|
|
@@ -4002,7 +4330,8 @@ const DETECT_AGENTS = { agents: null };
|
|
|
4002
4330
|
function hostDeps$11(ctx) {
|
|
4003
4331
|
return {
|
|
4004
4332
|
runner: processRunner(ctx.signal),
|
|
4005
|
-
probe: hostAgentProbe()
|
|
4333
|
+
probe: hostAgentProbe(),
|
|
4334
|
+
skillSource: rdeSkillSource()
|
|
4006
4335
|
};
|
|
4007
4336
|
}
|
|
4008
4337
|
function detectedOnly(agents) {
|
|
@@ -4062,9 +4391,10 @@ function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
|
4062
4391
|
ctx.ui.info(skipLine(all, selection.source));
|
|
4063
4392
|
return "skipped";
|
|
4064
4393
|
}
|
|
4065
|
-
|
|
4394
|
+
const from = resolved.skillSource === "https://github.com/metabase/agent-skills/tree/rde-skill" ? "" : ` from ${resolved.skillSource}`;
|
|
4395
|
+
ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})${from}`);
|
|
4066
4396
|
await skillsCli(resolved.runner).add({
|
|
4067
|
-
source:
|
|
4397
|
+
source: resolved.skillSource,
|
|
4068
4398
|
skill: "rde",
|
|
4069
4399
|
agents: selection.agents.map((agent) => agent.skillsCliName)
|
|
4070
4400
|
});
|
|
@@ -4090,7 +4420,8 @@ function rdeInstalledWith() {
|
|
|
4090
4420
|
function hostDeps$10(ctx) {
|
|
4091
4421
|
return {
|
|
4092
4422
|
runner: processRunner(ctx.signal),
|
|
4093
|
-
installedWith: rdeInstalledWith()
|
|
4423
|
+
installedWith: rdeInstalledWith(),
|
|
4424
|
+
locate: locateMbOnPath
|
|
4094
4425
|
};
|
|
4095
4426
|
}
|
|
4096
4427
|
async function inspectMb(runner) {
|
|
@@ -4107,14 +4438,100 @@ async function inspectMb(runner) {
|
|
|
4107
4438
|
function displayCommand(install) {
|
|
4108
4439
|
return [install.command, ...install.args].join(" ");
|
|
4109
4440
|
}
|
|
4110
|
-
function
|
|
4111
|
-
|
|
4112
|
-
|
|
4441
|
+
async function versionAt(runner, path) {
|
|
4442
|
+
try {
|
|
4443
|
+
const raw = await mbTool(runner).version(path);
|
|
4444
|
+
return raw === null ? null : semver.valid(raw);
|
|
4445
|
+
} catch (error) {
|
|
4446
|
+
if (error instanceof ExternalToolError) return null;
|
|
4447
|
+
throw error;
|
|
4448
|
+
}
|
|
4449
|
+
}
|
|
4450
|
+
async function inspectMbOnPath(runner, locate) {
|
|
4451
|
+
const found = [];
|
|
4452
|
+
for (const binary of await locate()) found.push({
|
|
4453
|
+
path: binary.path,
|
|
4454
|
+
version: await versionAt(runner, binary.path),
|
|
4455
|
+
owner: mbOwner(binary),
|
|
4456
|
+
removeCommand: mbRemoveCommand(binary)
|
|
4457
|
+
});
|
|
4458
|
+
return found;
|
|
4459
|
+
}
|
|
4460
|
+
function satisfiesMinimum(version) {
|
|
4461
|
+
return version !== null && semver.satisfies(version, MB_RANGE, { includePrerelease: true });
|
|
4462
|
+
}
|
|
4463
|
+
function isNewer(candidate, than) {
|
|
4464
|
+
if (candidate === null) return false;
|
|
4465
|
+
return than === null || semver.gt(candidate, than);
|
|
4466
|
+
}
|
|
4467
|
+
function diagnoseMb(found, installer) {
|
|
4468
|
+
const [running, ...later] = found;
|
|
4469
|
+
if (running === void 0) return { kind: "missing" };
|
|
4470
|
+
const sufficient = satisfiesMinimum(running.version);
|
|
4471
|
+
const newer = later.find((candidate) => satisfiesMinimum(candidate.version) && isNewer(candidate.version, running.version));
|
|
4472
|
+
if (newer !== void 0 && (!sufficient || running.owner !== installer)) return {
|
|
4473
|
+
kind: "shadowed",
|
|
4474
|
+
running,
|
|
4475
|
+
newer
|
|
4476
|
+
};
|
|
4477
|
+
return sufficient ? {
|
|
4478
|
+
kind: "current",
|
|
4479
|
+
running
|
|
4480
|
+
} : {
|
|
4481
|
+
kind: "old",
|
|
4482
|
+
running
|
|
4483
|
+
};
|
|
4484
|
+
}
|
|
4485
|
+
function runningMbSuffices(diagnosis) {
|
|
4486
|
+
return diagnosis.kind !== "missing" && satisfiesMinimum(diagnosis.running.version);
|
|
4487
|
+
}
|
|
4488
|
+
function versionText(entry) {
|
|
4489
|
+
return entry.version ?? "an unknown version";
|
|
4490
|
+
}
|
|
4491
|
+
function describeEntry(entry) {
|
|
4492
|
+
const owner = entry.owner === null ? "" : ` (${MB_OWNER_NAMES[entry.owner]})`;
|
|
4493
|
+
return `${versionText(entry)} at ${entry.path}${owner}`;
|
|
4494
|
+
}
|
|
4495
|
+
function mbPathLines(found) {
|
|
4496
|
+
return found.map((entry, index) => `${index === 0 ? "" : "then "}${describeEntry(entry)}`);
|
|
4497
|
+
}
|
|
4498
|
+
function removeClause(entry) {
|
|
4499
|
+
if (entry.removeCommand !== null) return `remove the old one with \`${entry.removeCommand}\``;
|
|
4500
|
+
if (entry.owner !== null) return `remove the old one with ${MB_OWNER_NAMES[entry.owner]}`;
|
|
4501
|
+
return `remove ${entry.path}`;
|
|
4502
|
+
}
|
|
4503
|
+
function shadowedHint(diagnosis) {
|
|
4504
|
+
const { running, newer } = diagnosis;
|
|
4505
|
+
return [
|
|
4506
|
+
`${running.path} (${versionText(running)}) runs before ${newer.path} (${versionText(newer)});`,
|
|
4507
|
+
`${removeClause(running)}, or put ${dirname(newer.path)} before ${dirname(running.path)} on PATH,`,
|
|
4508
|
+
"then open a new terminal"
|
|
4509
|
+
].join(" ");
|
|
4510
|
+
}
|
|
4511
|
+
const NEW_TERMINAL = "open a new terminal and run rde init again";
|
|
4512
|
+
function stillWrongAdvice(diagnosis) {
|
|
4513
|
+
switch (diagnosis.kind) {
|
|
4514
|
+
case "shadowed": return `${shadowedHint(diagnosis)} and run rde init again`;
|
|
4515
|
+
case "old": return `The install put no newer mb on PATH; ${removeClause(diagnosis.running)}, or ${NEW_TERMINAL}`;
|
|
4516
|
+
case "missing":
|
|
4517
|
+
case "current": return "Open a new terminal and run rde init again";
|
|
4518
|
+
}
|
|
4519
|
+
}
|
|
4520
|
+
async function stillWrong(install, after, deps) {
|
|
4521
|
+
const fail = (stderrTail) => new ExternalToolError({
|
|
4113
4522
|
tool: install.command,
|
|
4114
4523
|
argv: install.args,
|
|
4115
4524
|
exitCode: 0,
|
|
4116
|
-
stderrTail
|
|
4525
|
+
stderrTail
|
|
4117
4526
|
});
|
|
4527
|
+
if (!after.installed) return fail(`mb is not on PATH after the install; ${NEW_TERMINAL}`);
|
|
4528
|
+
const found = await inspectMbOnPath(deps.runner, deps.locate);
|
|
4529
|
+
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
4530
|
+
return fail([
|
|
4531
|
+
`mb ${after.version} still runs first (${MIN_MB_VERSION} or newer is required).`,
|
|
4532
|
+
`Every mb on PATH, in order: ${mbPathLines(found).join("; ")}.`,
|
|
4533
|
+
`${stillWrongAdvice(diagnosis)}.`
|
|
4534
|
+
].join(" "));
|
|
4118
4535
|
}
|
|
4119
4536
|
function installMbStep(deps = hostDeps$10) {
|
|
4120
4537
|
return {
|
|
@@ -4142,7 +4559,7 @@ function installMbStep(deps = hostDeps$10) {
|
|
|
4142
4559
|
stderrTail: ""
|
|
4143
4560
|
});
|
|
4144
4561
|
const after = await inspectMb(resolved.runner);
|
|
4145
|
-
if (!after.installed || !after.satisfies) throw stillWrong(install, after);
|
|
4562
|
+
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
4146
4563
|
ctx.ui.success(`mb ${after.version} is installed`);
|
|
4147
4564
|
return "completed";
|
|
4148
4565
|
}
|
|
@@ -4463,7 +4880,15 @@ function hostDoctorDeps(instanceName, signal) {
|
|
|
4463
4880
|
runtime,
|
|
4464
4881
|
ports: hostPortProbe,
|
|
4465
4882
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
4466
|
-
probe: hostAgentProbe()
|
|
4883
|
+
probe: hostAgentProbe(),
|
|
4884
|
+
locateMb: locateMbOnPath,
|
|
4885
|
+
environment: mbEnvironment(),
|
|
4886
|
+
skills: {
|
|
4887
|
+
source: rdeSkillSource(),
|
|
4888
|
+
cwd: process.cwd(),
|
|
4889
|
+
stateHome: xdgStateHome(),
|
|
4890
|
+
files: hostSkillFiles
|
|
4891
|
+
}
|
|
4467
4892
|
};
|
|
4468
4893
|
}
|
|
4469
4894
|
const NOT_FOUND_STATUS$2 = 404;
|
|
@@ -4476,7 +4901,17 @@ function ok(id, label, detail) {
|
|
|
4476
4901
|
status: "ok",
|
|
4477
4902
|
detail,
|
|
4478
4903
|
hint: null,
|
|
4479
|
-
|
|
4904
|
+
step: null
|
|
4905
|
+
};
|
|
4906
|
+
}
|
|
4907
|
+
function note$1(id, label, detail, hint) {
|
|
4908
|
+
return {
|
|
4909
|
+
id,
|
|
4910
|
+
label,
|
|
4911
|
+
status: "ok",
|
|
4912
|
+
detail,
|
|
4913
|
+
hint,
|
|
4914
|
+
step: null
|
|
4480
4915
|
};
|
|
4481
4916
|
}
|
|
4482
4917
|
function skipped(id, label, detail) {
|
|
@@ -4486,7 +4921,7 @@ function skipped(id, label, detail) {
|
|
|
4486
4921
|
status: "skipped",
|
|
4487
4922
|
detail,
|
|
4488
4923
|
hint: null,
|
|
4489
|
-
|
|
4924
|
+
step: null
|
|
4490
4925
|
};
|
|
4491
4926
|
}
|
|
4492
4927
|
function failed(id, label, failure) {
|
|
@@ -4507,7 +4942,7 @@ function machineRow(check, needed) {
|
|
|
4507
4942
|
return failed(check.id, check.id, {
|
|
4508
4943
|
detail: check.label,
|
|
4509
4944
|
hint: check.hint,
|
|
4510
|
-
|
|
4945
|
+
step: "preflight"
|
|
4511
4946
|
});
|
|
4512
4947
|
}
|
|
4513
4948
|
async function machineRows(deps, runtime) {
|
|
@@ -4519,19 +4954,48 @@ function installHint(installedWith) {
|
|
|
4519
4954
|
const install = mbInstallCommand(installedWith);
|
|
4520
4955
|
return `\`${[install.command, ...install.args].join(" ")}\``;
|
|
4521
4956
|
}
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
if (
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
|
|
4531
|
-
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4957
|
+
const MB_LABEL = "mb";
|
|
4958
|
+
function mbUnusable(diagnosis) {
|
|
4959
|
+
if (diagnosis.kind === "missing") return "mb is not installed";
|
|
4960
|
+
return runningMbSuffices(diagnosis) ? null : "the mb that runs is too old";
|
|
4961
|
+
}
|
|
4962
|
+
function mbRow(diagnosis, lines, deps) {
|
|
4963
|
+
switch (diagnosis.kind) {
|
|
4964
|
+
case "missing": return failed("mb", MB_LABEL, {
|
|
4965
|
+
detail: "not installed",
|
|
4966
|
+
hint: installHint(deps.installedWith),
|
|
4967
|
+
step: "install-mb"
|
|
4968
|
+
});
|
|
4969
|
+
case "old": {
|
|
4970
|
+
const required = lines.map((line, index) => index === 0 ? `${line}; ${MIN_MB_VERSION} or newer is required` : line);
|
|
4971
|
+
return failed("mb", MB_LABEL, {
|
|
4972
|
+
detail: required.join("\n"),
|
|
4973
|
+
hint: installHint(deps.installedWith),
|
|
4974
|
+
step: "install-mb"
|
|
4975
|
+
});
|
|
4976
|
+
}
|
|
4977
|
+
case "shadowed": return failed("mb", MB_LABEL, {
|
|
4978
|
+
detail: lines.join("\n"),
|
|
4979
|
+
hint: shadowedHint(diagnosis),
|
|
4980
|
+
step: null
|
|
4981
|
+
});
|
|
4982
|
+
case "current": return ok("mb", MB_LABEL, lines.join("\n"));
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
async function mbFinding(deps) {
|
|
4986
|
+
const found = await inspectMbOnPath(deps.runner, deps.locateMb);
|
|
4987
|
+
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
4988
|
+
return {
|
|
4989
|
+
row: mbRow(diagnosis, mbPathLines(found), deps),
|
|
4990
|
+
unusable: mbUnusable(diagnosis)
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
function sharedProfileList(deps) {
|
|
4994
|
+
let listed = null;
|
|
4995
|
+
return () => {
|
|
4996
|
+
listed ??= mbTool(deps.runner).authList();
|
|
4997
|
+
return listed;
|
|
4998
|
+
};
|
|
4535
4999
|
}
|
|
4536
5000
|
function publishesOnLoopback(container, port) {
|
|
4537
5001
|
return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
@@ -4549,7 +5013,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4549
5013
|
row: failed("container", label, {
|
|
4550
5014
|
detail: `${runtime.containerName} does not exist`,
|
|
4551
5015
|
hint: "`rde init` recreates it from the saved choices",
|
|
4552
|
-
|
|
5016
|
+
step: "runtime"
|
|
4553
5017
|
}),
|
|
4554
5018
|
running: false,
|
|
4555
5019
|
holder: CONTAINER
|
|
@@ -4562,7 +5026,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4562
5026
|
row: failed("container", label, {
|
|
4563
5027
|
detail: `${runtime.containerName} runs ${inspection.image}, the state says ${runtime.image}`,
|
|
4564
5028
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
4565
|
-
|
|
5029
|
+
step: null
|
|
4566
5030
|
}),
|
|
4567
5031
|
...found
|
|
4568
5032
|
};
|
|
@@ -4570,7 +5034,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
4570
5034
|
row: failed("container", label, {
|
|
4571
5035
|
detail: `${runtime.containerName} does not publish port ${runtime.port} on ${LOOPBACK_HOST}`,
|
|
4572
5036
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
4573
|
-
|
|
5037
|
+
step: null
|
|
4574
5038
|
}),
|
|
4575
5039
|
...found
|
|
4576
5040
|
};
|
|
@@ -4591,7 +5055,7 @@ async function processRow(runtime, deps) {
|
|
|
4591
5055
|
row: failed("process", label, {
|
|
4592
5056
|
detail: `${inspection.jarPath} does not exist`,
|
|
4593
5057
|
hint,
|
|
4594
|
-
|
|
5058
|
+
step: "runtime"
|
|
4595
5059
|
}),
|
|
4596
5060
|
running: false,
|
|
4597
5061
|
holder: JAR_PROCESS
|
|
@@ -4618,13 +5082,13 @@ async function portRow(port, finding, ports) {
|
|
|
4618
5082
|
if (finding.running) return failed("port", "port", {
|
|
4619
5083
|
detail: `${port} is not answering although ${finding.holder} runs`,
|
|
4620
5084
|
hint: "`rde logs` shows what Metabase is doing",
|
|
4621
|
-
|
|
5085
|
+
step: null
|
|
4622
5086
|
});
|
|
4623
5087
|
if (free) return ok("port", "port", `${port} is free`);
|
|
4624
5088
|
return failed("port", "port", {
|
|
4625
5089
|
detail: `${port} is in use by another program`,
|
|
4626
5090
|
hint: "stop that program before `rde start`",
|
|
4627
|
-
|
|
5091
|
+
step: null
|
|
4628
5092
|
});
|
|
4629
5093
|
}
|
|
4630
5094
|
async function healthRow(url, deps) {
|
|
@@ -4639,7 +5103,7 @@ async function healthRow(url, deps) {
|
|
|
4639
5103
|
row: failed("health", "health", {
|
|
4640
5104
|
detail: `initializing (${Math.round(error.progress * 100)}%)`,
|
|
4641
5105
|
hint: "wait for it; `rde logs -f` shows progress",
|
|
4642
|
-
|
|
5106
|
+
step: "start-metabase"
|
|
4643
5107
|
}),
|
|
4644
5108
|
healthy: false
|
|
4645
5109
|
};
|
|
@@ -4647,7 +5111,7 @@ async function healthRow(url, deps) {
|
|
|
4647
5111
|
row: failed("health", "health", {
|
|
4648
5112
|
detail: "unreachable",
|
|
4649
5113
|
hint: "`rde start`",
|
|
4650
|
-
|
|
5114
|
+
step: "start-metabase"
|
|
4651
5115
|
}),
|
|
4652
5116
|
healthy: false
|
|
4653
5117
|
};
|
|
@@ -4655,7 +5119,7 @@ async function healthRow(url, deps) {
|
|
|
4655
5119
|
row: failed("health", "health", {
|
|
4656
5120
|
detail: `something other than Metabase answers at ${url} (HTTP ${error.status})`,
|
|
4657
5121
|
hint: "another program holds the port; stop it before `rde start`",
|
|
4658
|
-
|
|
5122
|
+
step: null
|
|
4659
5123
|
}),
|
|
4660
5124
|
healthy: false
|
|
4661
5125
|
};
|
|
@@ -4666,7 +5130,7 @@ function adminRow(state, properties) {
|
|
|
4666
5130
|
if (!properties["has-user-setup"]) return failed("admin", "admin user", {
|
|
4667
5131
|
detail: "Metabase has no admin user yet",
|
|
4668
5132
|
hint: "`rde init --only admin-user`",
|
|
4669
|
-
|
|
5133
|
+
step: "admin-user"
|
|
4670
5134
|
});
|
|
4671
5135
|
return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
|
|
4672
5136
|
}
|
|
@@ -4678,36 +5142,204 @@ function versionRow(state, properties) {
|
|
|
4678
5142
|
if (state.version !== running) return failed("version", label, {
|
|
4679
5143
|
detail: `running ${running}, the state file says ${state.version}`,
|
|
4680
5144
|
hint: "`rde start` records the running version",
|
|
4681
|
-
|
|
5145
|
+
step: VERSION_STEP
|
|
4682
5146
|
});
|
|
4683
5147
|
return ok("version", label, running);
|
|
4684
5148
|
}
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
5149
|
+
const API_KEY_STEP = "api-key";
|
|
5150
|
+
const RECREATE_HINT = "when the instance holds nothing you need, `rde uninstall` then `rde init`";
|
|
5151
|
+
function adminPasswordRow(state, secrets) {
|
|
5152
|
+
const id = "admin-password";
|
|
5153
|
+
const label = "admin password";
|
|
5154
|
+
const admin = state.admin;
|
|
5155
|
+
if (admin === null) return skipped(id, label, "no admin user created by rde yet");
|
|
5156
|
+
switch (admin.kind) {
|
|
5157
|
+
case "prompted": return ok(id, label, "yours, entered during `rde init` and never stored");
|
|
5158
|
+
case "generated":
|
|
5159
|
+
if (secrets === null || secrets.admin === null) return failed(id, label, {
|
|
5160
|
+
detail: "generated, but missing from the secrets file",
|
|
5161
|
+
hint: `it cannot be recovered; ${RECREATE_HINT}`,
|
|
5162
|
+
step: null
|
|
5163
|
+
});
|
|
5164
|
+
return ok(id, label, "generated and stored; `rde credentials` shows it in your terminal");
|
|
5165
|
+
case "legacy-default": return failed(id, label, {
|
|
5166
|
+
detail: "the public default password from an earlier rde (legacy-default)",
|
|
5167
|
+
hint: `anyone who knows the default can log in; ${RECREATE_HINT} with a generated one`,
|
|
5168
|
+
step: null
|
|
5169
|
+
});
|
|
5170
|
+
}
|
|
5171
|
+
}
|
|
5172
|
+
async function apiKeyFinding(secrets, url, healthy, deps) {
|
|
5173
|
+
const id = "api-key";
|
|
5174
|
+
const label = "API key";
|
|
5175
|
+
const apiKey = secrets === null ? null : secrets.apiKey;
|
|
5176
|
+
if (apiKey === null) return {
|
|
5177
|
+
row: failed(id, label, {
|
|
5178
|
+
detail: "none in the secrets file",
|
|
5179
|
+
hint: "`rde init --only api-key`",
|
|
5180
|
+
step: API_KEY_STEP
|
|
5181
|
+
}),
|
|
5182
|
+
access: {
|
|
5183
|
+
kind: "unusable",
|
|
5184
|
+
reason: "no API key"
|
|
5185
|
+
}
|
|
5186
|
+
};
|
|
5187
|
+
if (url === null || !healthy) return {
|
|
5188
|
+
row: ok(id, label, "in the secrets file"),
|
|
5189
|
+
access: {
|
|
5190
|
+
kind: "unusable",
|
|
5191
|
+
reason: NOT_HEALTHY
|
|
5192
|
+
}
|
|
5193
|
+
};
|
|
5194
|
+
const client = deps.client(url, {
|
|
5195
|
+
kind: "api-key",
|
|
5196
|
+
key: apiKey
|
|
4690
5197
|
});
|
|
4691
|
-
|
|
5198
|
+
try {
|
|
5199
|
+
await client.user.current();
|
|
5200
|
+
} catch (error) {
|
|
5201
|
+
if (!isRejectedCredential(error)) throw error;
|
|
5202
|
+
return {
|
|
5203
|
+
row: failed(id, label, {
|
|
5204
|
+
detail: "in the secrets file, but Metabase rejects it",
|
|
5205
|
+
hint: "`rde init --only api-key` replaces it",
|
|
5206
|
+
step: API_KEY_STEP
|
|
5207
|
+
}),
|
|
5208
|
+
access: {
|
|
5209
|
+
kind: "unusable",
|
|
5210
|
+
reason: "Metabase rejects the API key"
|
|
5211
|
+
}
|
|
5212
|
+
};
|
|
5213
|
+
}
|
|
5214
|
+
return {
|
|
5215
|
+
row: ok(id, label, "in the secrets file, accepted by Metabase"),
|
|
5216
|
+
access: {
|
|
5217
|
+
kind: "accepted",
|
|
5218
|
+
client
|
|
5219
|
+
}
|
|
5220
|
+
};
|
|
5221
|
+
}
|
|
5222
|
+
function octal(mode) {
|
|
5223
|
+
return `0${mode.toString(8)}`;
|
|
5224
|
+
}
|
|
5225
|
+
function expectsSecrets(state) {
|
|
5226
|
+
return state.admin !== null && state.admin.kind !== "prompted" || state.steps[API_KEY_STEP]?.status === "done";
|
|
4692
5227
|
}
|
|
4693
|
-
|
|
5228
|
+
function secretsFileRow(state, files, dir) {
|
|
5229
|
+
const id = "secrets-file";
|
|
5230
|
+
const label = "secrets file";
|
|
5231
|
+
const path = secretsFileIn(dir);
|
|
5232
|
+
if (files.secretsMode === null) {
|
|
5233
|
+
if (!expectsSecrets(state)) return skipped(id, label, "nothing stored yet");
|
|
5234
|
+
return failed(id, label, {
|
|
5235
|
+
detail: `${path} is missing`,
|
|
5236
|
+
hint: "`rde init --only api-key` stores a new API key",
|
|
5237
|
+
step: API_KEY_STEP
|
|
5238
|
+
});
|
|
5239
|
+
}
|
|
5240
|
+
if (files.secretsMode !== 384 || files.dirMode !== 448) {
|
|
5241
|
+
const dirMode = files.dirMode === null ? "missing" : octal(files.dirMode);
|
|
5242
|
+
return failed(id, label, {
|
|
5243
|
+
detail: `${path} is ${octal(files.secretsMode)} in a ${dirMode} directory`,
|
|
5244
|
+
hint: `\`chmod ${octal(384)} ${path} && chmod ${octal(448)} ${dir}\``,
|
|
5245
|
+
step: null
|
|
5246
|
+
});
|
|
5247
|
+
}
|
|
5248
|
+
return ok(id, label, `${path}, ${octal(384)} in a ${octal(448)} directory`);
|
|
5249
|
+
}
|
|
5250
|
+
function stateFileRow(files) {
|
|
5251
|
+
const id = "state-file";
|
|
5252
|
+
const label = "state file";
|
|
5253
|
+
if (files.secretsInState.length > 0) return failed(id, label, {
|
|
5254
|
+
detail: `carries ${files.secretsInState.join(", ")}`,
|
|
5255
|
+
hint: "`rde init --only api-key` rewrites it without them",
|
|
5256
|
+
step: API_KEY_STEP
|
|
5257
|
+
});
|
|
5258
|
+
return ok(id, label, "no secrets");
|
|
5259
|
+
}
|
|
5260
|
+
async function credentialRows(state, secrets, apiKey, deps) {
|
|
5261
|
+
const dir = deps.runtime.instanceDir;
|
|
5262
|
+
const files = await inspectCredentialFiles(dir);
|
|
5263
|
+
return [
|
|
5264
|
+
adminPasswordRow(state, secrets),
|
|
5265
|
+
apiKey.row,
|
|
5266
|
+
secretsFileRow(state, files, dir),
|
|
5267
|
+
stateFileRow(files)
|
|
5268
|
+
];
|
|
5269
|
+
}
|
|
5270
|
+
function withoutTrailingSlash(url) {
|
|
5271
|
+
return url.replace(/\/+$/, "");
|
|
5272
|
+
}
|
|
5273
|
+
async function mbProfileRow(state, url, profiles, mbUnusableReason, healthy) {
|
|
4694
5274
|
const label = "mb profile";
|
|
4695
|
-
if (
|
|
5275
|
+
if (mbUnusableReason !== null) return skipped("mb-profile", label, mbUnusableReason);
|
|
4696
5276
|
if (!healthy) return skipped("mb-profile", label, NOT_HEALTHY);
|
|
4697
|
-
const entry = (await
|
|
5277
|
+
const entry = (await profiles()).find((candidate) => candidate.profile === state.mbProfile);
|
|
4698
5278
|
if (entry === void 0) return failed("mb-profile", label, {
|
|
4699
5279
|
detail: `${state.mbProfile} is not configured`,
|
|
4700
5280
|
hint: "`rde init --only api-key`",
|
|
4701
|
-
|
|
5281
|
+
step: "api-key"
|
|
5282
|
+
});
|
|
5283
|
+
if (withoutTrailingSlash(entry.url) !== withoutTrailingSlash(url)) return failed("mb-profile", label, {
|
|
5284
|
+
detail: `${state.mbProfile} points at ${entry.url}, not at this instance (${url})`,
|
|
5285
|
+
hint: "`rde init --only api-key` logs it back in against the instance",
|
|
5286
|
+
step: "api-key"
|
|
4702
5287
|
});
|
|
4703
5288
|
if (entry.status !== "ok") return failed("mb-profile", label, {
|
|
4704
5289
|
detail: `${state.mbProfile}: ${entry.status}`,
|
|
4705
5290
|
hint: "`rde init --only api-key` logs it in again",
|
|
4706
|
-
|
|
5291
|
+
step: "api-key"
|
|
4707
5292
|
});
|
|
4708
5293
|
const user = entry.user === null ? "" : ` as ${entry.user.name}`;
|
|
4709
5294
|
return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
|
|
4710
5295
|
}
|
|
5296
|
+
function hostOf(url) {
|
|
5297
|
+
return URL.canParse(url) ? new URL(url).host : "an address that is not a URL";
|
|
5298
|
+
}
|
|
5299
|
+
function unsetHint(name) {
|
|
5300
|
+
return `\`unset ${name}\`, remove it from your shell profile if it is exported there, then restart the agent session`;
|
|
5301
|
+
}
|
|
5302
|
+
async function mbProfileEnvRow(name, rdeProfile, profiles, mbUnusableReason) {
|
|
5303
|
+
const id = `env.${ENV_MB_PROFILE}`;
|
|
5304
|
+
if (name === rdeProfile) return ok(id, ENV_MB_PROFILE, `${name}: rde's own profile, so plain \`mb\` uses this instance`);
|
|
5305
|
+
const rde = rdeProfile === null ? "rde's" : `rde's (${rdeProfile})`;
|
|
5306
|
+
const detail = (where) => `${name}: \`mb\` and the rde skill will use profile ${name} (${where}) instead of ${rde}`;
|
|
5307
|
+
if (mbUnusableReason !== null) return note$1(id, ENV_MB_PROFILE, detail(mbUnusableReason), unsetHint(ENV_MB_PROFILE));
|
|
5308
|
+
const entry = (await profiles()).find((candidate) => candidate.profile === name);
|
|
5309
|
+
const where = entry === void 0 ? "not configured" : hostOf(entry.url);
|
|
5310
|
+
return note$1(id, ENV_MB_PROFILE, detail(where), unsetHint(ENV_MB_PROFILE));
|
|
5311
|
+
}
|
|
5312
|
+
async function environmentRows(env, rdeProfile, profiles, mbUnusableReason) {
|
|
5313
|
+
const rows = [];
|
|
5314
|
+
if (env.profile !== null) rows.push(await mbProfileEnvRow(env.profile, rdeProfile, profiles, mbUnusableReason));
|
|
5315
|
+
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)));
|
|
5316
|
+
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)));
|
|
5317
|
+
return rows;
|
|
5318
|
+
}
|
|
5319
|
+
const PROFILE_STATUS_WORDS = {
|
|
5320
|
+
ok: "authenticates",
|
|
5321
|
+
"auth-failed": "login rejected",
|
|
5322
|
+
"network-error": "not answering",
|
|
5323
|
+
"server-error": "answers with a server error",
|
|
5324
|
+
"not-probed": "not checked"
|
|
5325
|
+
};
|
|
5326
|
+
function plainMbLine(env, rdeProfile, profiles) {
|
|
5327
|
+
if (env.url !== null) return `plain \`mb\` goes to ${hostOf(env.url)} (${ENV_MB_URL})`;
|
|
5328
|
+
const name = env.profile ?? "default";
|
|
5329
|
+
const via = env.profile === null ? "" : ` (${ENV_MB_PROFILE})`;
|
|
5330
|
+
const entry = profiles.find((candidate) => candidate.profile === name);
|
|
5331
|
+
if (entry === void 0) return `plain \`mb\` uses ${name}${via}, which is not configured`;
|
|
5332
|
+
const instance = name === rdeProfile ? ", this instance" : "";
|
|
5333
|
+
return `plain \`mb\` uses ${name}${via}: ${hostOf(entry.url)}${instance}`;
|
|
5334
|
+
}
|
|
5335
|
+
async function otherMetabasesRow(env, rdeProfile, profiles, mbUnusableReason) {
|
|
5336
|
+
const id = "other-metabases";
|
|
5337
|
+
const label = "other Metabases";
|
|
5338
|
+
if (mbUnusableReason !== null) return skipped(id, label, mbUnusableReason);
|
|
5339
|
+
const all = await profiles();
|
|
5340
|
+
const others = all.filter((entry) => entry.profile !== rdeProfile);
|
|
5341
|
+
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"));
|
|
5342
|
+
}
|
|
4711
5343
|
function isNotFound$2(error) {
|
|
4712
5344
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
|
|
4713
5345
|
}
|
|
@@ -4718,7 +5350,7 @@ async function licenseRow(client) {
|
|
|
4718
5350
|
if (!status.valid) return failed("license", label, {
|
|
4719
5351
|
detail: `invalid: ${status.status}`,
|
|
4720
5352
|
hint: "`rde init --from license` with a valid token",
|
|
4721
|
-
|
|
5353
|
+
step: "license"
|
|
4722
5354
|
});
|
|
4723
5355
|
return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
|
|
4724
5356
|
} catch (error) {
|
|
@@ -4735,47 +5367,47 @@ async function databasesRow(state, client) {
|
|
|
4735
5367
|
if (missing.length > 0) return failed("databases", label, {
|
|
4736
5368
|
detail: `not on the server any more: ${missing.map((record) => record.name).join(", ")}`,
|
|
4737
5369
|
hint: "`rde db add` connects them again",
|
|
4738
|
-
|
|
5370
|
+
step: "databases"
|
|
4739
5371
|
});
|
|
4740
5372
|
const names = state.databases.map((record) => record.name).join(", ");
|
|
4741
5373
|
return ok("databases", label, `${state.databases.length} connected: ${names}`);
|
|
4742
5374
|
}
|
|
4743
5375
|
const SAMPLE_DATABASE_STEP = "sample-database";
|
|
4744
5376
|
const SAMPLE_LABEL = "sample database";
|
|
4745
|
-
async function sampleDatabaseRow(record, deps, dockerOk,
|
|
5377
|
+
async function sampleDatabaseRow(record, deps, dockerOk, access) {
|
|
4746
5378
|
const id = "sample-database";
|
|
4747
5379
|
if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
|
|
4748
5380
|
const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
|
|
4749
5381
|
if (!status.installed) return failed(id, SAMPLE_LABEL, {
|
|
4750
5382
|
detail: `${record.containerName} does not exist`,
|
|
4751
5383
|
hint: "`rde init` recreates it",
|
|
4752
|
-
|
|
5384
|
+
step: SAMPLE_DATABASE_STEP
|
|
4753
5385
|
});
|
|
4754
5386
|
if (!status.running) return failed(id, SAMPLE_LABEL, {
|
|
4755
5387
|
detail: `${record.containerName} is stopped`,
|
|
4756
5388
|
hint: "`rde start`",
|
|
4757
|
-
|
|
5389
|
+
step: "start-metabase"
|
|
4758
5390
|
});
|
|
4759
5391
|
if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
|
|
4760
5392
|
detail: `${record.containerName} runs but port ${record.port} is not answering`,
|
|
4761
5393
|
hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
|
|
4762
|
-
|
|
5394
|
+
step: null
|
|
4763
5395
|
});
|
|
4764
5396
|
const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
|
|
4765
5397
|
if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
|
|
4766
5398
|
detail: `${where}; not connected to Metabase yet`,
|
|
4767
5399
|
hint: "`rde init` connects it",
|
|
4768
|
-
|
|
5400
|
+
step: SAMPLE_DATABASE_STEP
|
|
4769
5401
|
});
|
|
4770
|
-
if (
|
|
5402
|
+
if (access.kind === "unusable") return skipped(id, SAMPLE_LABEL, `${where}; ${access.reason}, so the connection is unchecked`);
|
|
4771
5403
|
try {
|
|
4772
|
-
const database = await client.database.get(record.databaseId);
|
|
5404
|
+
const database = await access.client.database.get(record.databaseId);
|
|
4773
5405
|
return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
|
|
4774
5406
|
} catch (error) {
|
|
4775
5407
|
if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
|
|
4776
5408
|
detail: `${where}; database ${record.databaseId} is gone from Metabase`,
|
|
4777
5409
|
hint: "`rde init` connects it again",
|
|
4778
|
-
|
|
5410
|
+
step: SAMPLE_DATABASE_STEP
|
|
4779
5411
|
});
|
|
4780
5412
|
throw error;
|
|
4781
5413
|
}
|
|
@@ -4792,7 +5424,7 @@ async function gitSyncRow(state, deps) {
|
|
|
4792
5424
|
if (status.branch === null) return failed("git-sync", label, {
|
|
4793
5425
|
detail: "Metabase reports no branch",
|
|
4794
5426
|
hint: "`rde init --only git-sync`",
|
|
4795
|
-
|
|
5427
|
+
step: "git-sync"
|
|
4796
5428
|
});
|
|
4797
5429
|
const dirty = status.is_dirty ? "unsynced changes" : "in sync";
|
|
4798
5430
|
return ok("git-sync", label, `branch ${status.branch}, ${dirty}, ${state.gitRemote.workdir}`);
|
|
@@ -4800,15 +5432,100 @@ async function gitSyncRow(state, deps) {
|
|
|
4800
5432
|
if (isMbFailure$1(error)) return failed("git-sync", label, {
|
|
4801
5433
|
detail: error.message,
|
|
4802
5434
|
hint: `\`${mbCommand(state.mbProfile)} git-sync status\` shows the full answer`,
|
|
4803
|
-
|
|
5435
|
+
step: null
|
|
4804
5436
|
});
|
|
4805
5437
|
throw error;
|
|
4806
5438
|
}
|
|
4807
5439
|
}
|
|
4808
5440
|
const SKILL_STEP = "agents-skills";
|
|
5441
|
+
const GLOBAL_SCOPE = "global";
|
|
5442
|
+
const SKILL_FILE = "SKILL.md";
|
|
5443
|
+
const CLAUDE_CODE = "Claude Code";
|
|
5444
|
+
const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
|
|
4809
5445
|
function skillRowId(agent) {
|
|
4810
5446
|
return `skill.${agent.id}`;
|
|
4811
5447
|
}
|
|
5448
|
+
function withRef(source, ref) {
|
|
5449
|
+
return ref === null ? source : `${source}#${ref}`;
|
|
5450
|
+
}
|
|
5451
|
+
async function copyRef(entry, deps) {
|
|
5452
|
+
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
5453
|
+
return lockedRef(path, await deps.skills.files.readJson(path), entry.name);
|
|
5454
|
+
}
|
|
5455
|
+
async function judgeCopy(entry, origin, deps) {
|
|
5456
|
+
const where = `${entry.scope} ${entry.path}`;
|
|
5457
|
+
switch (origin.kind) {
|
|
5458
|
+
case "local": {
|
|
5459
|
+
const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
|
|
5460
|
+
const wanted = await deps.skills.files.readText(join(origin.path, SKILL_FILE));
|
|
5461
|
+
const current = installed !== null && installed === wanted;
|
|
5462
|
+
return {
|
|
5463
|
+
entry,
|
|
5464
|
+
current,
|
|
5465
|
+
line: `${where}: ${current ? "same as" : "differs from"} ${origin.path}`
|
|
5466
|
+
};
|
|
5467
|
+
}
|
|
5468
|
+
case "github": {
|
|
5469
|
+
const wanted = withRef(origin.repository, origin.ref);
|
|
5470
|
+
if (entry.source === null) return {
|
|
5471
|
+
entry,
|
|
5472
|
+
current: false,
|
|
5473
|
+
line: `${where}: no recorded source, not ${wanted}`
|
|
5474
|
+
};
|
|
5475
|
+
const found = withRef(entry.source, await copyRef(entry, deps));
|
|
5476
|
+
const current = found === wanted;
|
|
5477
|
+
return {
|
|
5478
|
+
entry,
|
|
5479
|
+
current,
|
|
5480
|
+
line: `${where}: from ${found}${current ? "" : `, not ${wanted}`}`
|
|
5481
|
+
};
|
|
5482
|
+
}
|
|
5483
|
+
case "other": {
|
|
5484
|
+
const current = entry.source === origin.source;
|
|
5485
|
+
return {
|
|
5486
|
+
entry,
|
|
5487
|
+
current,
|
|
5488
|
+
line: `${where}: from ${entry.source ?? "no recorded source"}${current ? "" : `, not ${origin.source}`}`
|
|
5489
|
+
};
|
|
5490
|
+
}
|
|
5491
|
+
}
|
|
5492
|
+
}
|
|
5493
|
+
function loadedCopyLine(entries, cwd) {
|
|
5494
|
+
if (entries.length < 2) return null;
|
|
5495
|
+
const count = `${entries.length} copies are installed`;
|
|
5496
|
+
const forClaude = entries.filter((entry) => entry.agents.includes(CLAUDE_CODE));
|
|
5497
|
+
const loaded = forClaude.find((entry) => entry.scope === GLOBAL_SCOPE) ?? forClaude[0];
|
|
5498
|
+
if (loaded === void 0) return count;
|
|
5499
|
+
return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
|
|
5500
|
+
}
|
|
5501
|
+
function staleCopiesRow(stale, detail, deps) {
|
|
5502
|
+
const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
|
|
5503
|
+
const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
|
|
5504
|
+
const repairs = [];
|
|
5505
|
+
if (globalStale) repairs.push(`\`rde init --only ${SKILL_STEP}\` reinstalls the global copy from ${deps.skills.source}`);
|
|
5506
|
+
if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove rde -y\` in ${deps.skills.cwd}`);
|
|
5507
|
+
return failed("skill-copies", "rde skill copies", {
|
|
5508
|
+
detail,
|
|
5509
|
+
hint: `${repairs.join("; ")}; then restart the agent session`,
|
|
5510
|
+
step: globalStale ? SKILL_STEP : null
|
|
5511
|
+
});
|
|
5512
|
+
}
|
|
5513
|
+
async function skillCopiesRow(globalEntries, deps) {
|
|
5514
|
+
const id = "skill-copies";
|
|
5515
|
+
const label = "rde skill copies";
|
|
5516
|
+
const cwd = deps.skills.cwd;
|
|
5517
|
+
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === "rde");
|
|
5518
|
+
const entries = [...globalEntries, ...projectEntries];
|
|
5519
|
+
if (entries.length === 0) return skipped(id, label, "no copy installed");
|
|
5520
|
+
const origin = skillOrigin(deps.skills.source, cwd);
|
|
5521
|
+
const judged = [];
|
|
5522
|
+
for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
|
|
5523
|
+
const loaded = loadedCopyLine(entries, cwd);
|
|
5524
|
+
const lines = judged.map((copy) => copy.line);
|
|
5525
|
+
const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
|
|
5526
|
+
const stale = judged.filter((copy) => !copy.current);
|
|
5527
|
+
return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
|
|
5528
|
+
}
|
|
4812
5529
|
async function skillRows(state, deps) {
|
|
4813
5530
|
const label = "rde skill";
|
|
4814
5531
|
if (state === null) return [skipped("skill", label, NO_INSTANCE)];
|
|
@@ -4816,17 +5533,18 @@ async function skillRows(state, deps) {
|
|
|
4816
5533
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
4817
5534
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
4818
5535
|
const entries = await rdeSkillEntries(deps.runner);
|
|
5536
|
+
const copies = await skillCopiesRow(entries, deps);
|
|
4819
5537
|
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
|
|
4820
5538
|
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
4821
5539
|
hint: `\`rde init --only ${SKILL_STEP}\``,
|
|
4822
|
-
|
|
4823
|
-
})];
|
|
4824
|
-
return detected.map((agent) => {
|
|
5540
|
+
step: SKILL_STEP
|
|
5541
|
+
}), copies];
|
|
5542
|
+
return [...detected.map((agent) => {
|
|
4825
5543
|
const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
|
|
4826
5544
|
const agentLabel = `${label} (${agent.displayName})`;
|
|
4827
5545
|
if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
|
|
4828
5546
|
return ok(skillRowId(agent), agentLabel, entry.path);
|
|
4829
|
-
});
|
|
5547
|
+
}), copies];
|
|
4830
5548
|
}
|
|
4831
5549
|
function noRuntimeRows() {
|
|
4832
5550
|
return [
|
|
@@ -4835,13 +5553,13 @@ function noRuntimeRows() {
|
|
|
4835
5553
|
"health"
|
|
4836
5554
|
].map((id) => skipped(id, id, NO_INSTANCE));
|
|
4837
5555
|
}
|
|
4838
|
-
async function instanceRows(state, deps, dockerOk,
|
|
5556
|
+
async function instanceRows(state, secrets, deps, dockerOk, profiles, mbUnusableReason) {
|
|
4839
5557
|
const configured = configuredRuntime(state);
|
|
4840
5558
|
if (configured === null) return [
|
|
4841
5559
|
...noRuntimeRows(),
|
|
4842
5560
|
skipped("version", "Metabase version", NO_INSTANCE),
|
|
4843
5561
|
skipped("admin", "admin user", NO_INSTANCE),
|
|
4844
|
-
|
|
5562
|
+
...await credentialRows(state, secrets, await apiKeyFinding(secrets, null, false, deps), deps),
|
|
4845
5563
|
skipped("mb-profile", "mb profile", NO_INSTANCE),
|
|
4846
5564
|
skipped("license", "license", NO_INSTANCE),
|
|
4847
5565
|
skipped("databases", "databases", NO_INSTANCE),
|
|
@@ -4851,35 +5569,33 @@ async function instanceRows(state, deps, dockerOk, mbInstalled) {
|
|
|
4851
5569
|
const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
|
|
4852
5570
|
const { row: health, healthy } = await healthRow(configured.url, deps);
|
|
4853
5571
|
const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
|
|
5572
|
+
const apiKey = await apiKeyFinding(secrets, configured.url, healthy, deps);
|
|
4854
5573
|
const rows = [
|
|
4855
5574
|
runtime.row,
|
|
4856
5575
|
await portRow(configured.runtime.port, runtime, deps.ports),
|
|
4857
5576
|
health,
|
|
4858
5577
|
properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
|
|
4859
5578
|
properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
|
|
4860
|
-
|
|
4861
|
-
await mbProfileRow(state,
|
|
5579
|
+
...await credentialRows(state, secrets, apiKey, deps),
|
|
5580
|
+
await mbProfileRow(state, configured.url, profiles, mbUnusableReason, healthy)
|
|
4862
5581
|
];
|
|
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));
|
|
5582
|
+
const access = apiKey.access;
|
|
5583
|
+
if (access.kind === "unusable") rows.push(skipped("license", "license", access.reason), skipped("databases", "databases", access.reason));
|
|
5584
|
+
else rows.push(await licenseRow(access.client), await databasesRow(state, access.client));
|
|
5585
|
+
rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, access));
|
|
4872
5586
|
rows.push(await gitSyncRow(state, deps));
|
|
4873
5587
|
return rows;
|
|
4874
5588
|
}
|
|
4875
|
-
async function collectDoctor(instance, state, deps) {
|
|
5589
|
+
async function collectDoctor(instance, state, secrets, deps) {
|
|
4876
5590
|
const machine = await machineRows(deps, state?.runtime ?? null);
|
|
4877
5591
|
const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
|
|
4878
|
-
const mb = await
|
|
4879
|
-
const
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
checks.push(...await
|
|
5592
|
+
const mb = await mbFinding(deps);
|
|
5593
|
+
const profiles = sharedProfileList(deps);
|
|
5594
|
+
const checks = [...machine, mb.row];
|
|
5595
|
+
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));
|
|
5596
|
+
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
5597
|
+
const rdeProfile = state === null ? null : state.mbProfile;
|
|
5598
|
+
checks.push(...await environmentRows(deps.environment, rdeProfile, profiles, mb.unusable), await otherMetabasesRow(deps.environment, rdeProfile, profiles, mb.unusable), ...await skillRows(state, deps));
|
|
4883
5599
|
return {
|
|
4884
5600
|
instance,
|
|
4885
5601
|
checks,
|
|
@@ -4891,18 +5607,38 @@ const MARK = {
|
|
|
4891
5607
|
failed: "✗",
|
|
4892
5608
|
skipped: "–"
|
|
4893
5609
|
};
|
|
5610
|
+
const NOTE_MARK = "!";
|
|
4894
5611
|
const HINT_ARROW = "→";
|
|
5612
|
+
function markFor(row) {
|
|
5613
|
+
return row.status === "ok" && row.hint !== null ? NOTE_MARK : MARK[row.status];
|
|
5614
|
+
}
|
|
4895
5615
|
function doctorLines(report) {
|
|
4896
5616
|
const width = Math.max(...report.checks.map((row) => row.label.length));
|
|
5617
|
+
const indent = ` ${" ".repeat(width)} `;
|
|
4897
5618
|
const lines = [];
|
|
4898
5619
|
for (const row of report.checks) {
|
|
4899
|
-
|
|
4900
|
-
|
|
5620
|
+
const [first, ...rest] = row.detail.split("\n");
|
|
5621
|
+
lines.push(`${markFor(row)} ${row.label.padEnd(width)} ${first}`);
|
|
5622
|
+
lines.push(...rest.map((line) => `${indent}${line}`));
|
|
5623
|
+
if (row.hint !== null) lines.push(`${indent}${HINT_ARROW} ${row.hint}`);
|
|
4901
5624
|
}
|
|
4902
5625
|
return lines;
|
|
4903
5626
|
}
|
|
5627
|
+
function fixCommand(step) {
|
|
5628
|
+
return step === null ? null : `rde init --only ${step}`;
|
|
5629
|
+
}
|
|
5630
|
+
function doctorJson(report) {
|
|
5631
|
+
return {
|
|
5632
|
+
instance: report.instance,
|
|
5633
|
+
checks: report.checks.map(({ step, ...row }) => ({
|
|
5634
|
+
...row,
|
|
5635
|
+
fix: fixCommand(step)
|
|
5636
|
+
})),
|
|
5637
|
+
failed: report.failed
|
|
5638
|
+
};
|
|
5639
|
+
}
|
|
4904
5640
|
function fixableSteps(report) {
|
|
4905
|
-
const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.
|
|
5641
|
+
const wanted = new Set(report.checks.flatMap((row) => row.status === "failed" && row.step !== null ? [row.step] : []));
|
|
4906
5642
|
return StepId.options.filter((id) => wanted.has(id));
|
|
4907
5643
|
}
|
|
4908
5644
|
function doctorSummary(report) {
|
|
@@ -4941,15 +5677,24 @@ const ADMIN_USER_KEYS = {
|
|
|
4941
5677
|
firstName: `${STEP_ID$6}.first-name`,
|
|
4942
5678
|
lastName: `${STEP_ID$6}.last-name`
|
|
4943
5679
|
};
|
|
4944
|
-
const
|
|
4945
|
-
const
|
|
5680
|
+
const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
|
|
5681
|
+
const GENERATED_PASSWORD_BYTES = 24;
|
|
4946
5682
|
const DEFAULT_SITE_NAME = "rde";
|
|
4947
5683
|
const MIN_PASSWORD_LENGTH = 6;
|
|
4948
5684
|
const DIGIT = /\d/;
|
|
4949
5685
|
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4950
|
-
const DEFAULT_ADMIN = { variant: "
|
|
5686
|
+
const DEFAULT_ADMIN = { variant: "generated" };
|
|
5687
|
+
function generatePassword(random) {
|
|
5688
|
+
for (;;) {
|
|
5689
|
+
const candidate = random(GENERATED_PASSWORD_BYTES).toString("base64url");
|
|
5690
|
+
if (DIGIT.test(candidate)) return candidate;
|
|
5691
|
+
}
|
|
5692
|
+
}
|
|
4951
5693
|
function hostDeps$8(ctx) {
|
|
4952
|
-
return {
|
|
5694
|
+
return {
|
|
5695
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5696
|
+
password: () => generatePassword(randomBytes)
|
|
5697
|
+
};
|
|
4953
5698
|
}
|
|
4954
5699
|
function isNotReady(error) {
|
|
4955
5700
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
@@ -4988,26 +5733,22 @@ async function promptedUser(ctx) {
|
|
|
4988
5733
|
...lastName.length > 0 && { last_name: lastName }
|
|
4989
5734
|
};
|
|
4990
5735
|
}
|
|
4991
|
-
async function chooseUser(ctx,
|
|
4992
|
-
switch (
|
|
4993
|
-
case "
|
|
4994
|
-
email:
|
|
4995
|
-
password:
|
|
5736
|
+
async function chooseUser(ctx, variant, deps) {
|
|
5737
|
+
switch (variant) {
|
|
5738
|
+
case "generated": return {
|
|
5739
|
+
email: GENERATED_ADMIN_EMAIL,
|
|
5740
|
+
password: deps.password()
|
|
4996
5741
|
};
|
|
4997
5742
|
case "prompted": return promptedUser(ctx);
|
|
4998
5743
|
}
|
|
4999
5744
|
}
|
|
5000
|
-
function
|
|
5001
|
-
switch (
|
|
5002
|
-
case "
|
|
5003
|
-
kind,
|
|
5745
|
+
function storedLogin(variant, user) {
|
|
5746
|
+
switch (variant) {
|
|
5747
|
+
case "generated": return {
|
|
5004
5748
|
email: user.email,
|
|
5005
5749
|
password: user.password
|
|
5006
5750
|
};
|
|
5007
|
-
case "prompted": return
|
|
5008
|
-
kind,
|
|
5009
|
-
email: user.email
|
|
5010
|
-
};
|
|
5751
|
+
case "prompted": return null;
|
|
5011
5752
|
}
|
|
5012
5753
|
}
|
|
5013
5754
|
function adminUserStep(options, deps = hostDeps$8) {
|
|
@@ -5026,7 +5767,8 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5026
5767
|
},
|
|
5027
5768
|
async run(ctx) {
|
|
5028
5769
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5029
|
-
const
|
|
5770
|
+
const resolved = deps(ctx);
|
|
5771
|
+
const client = resolved.client(configured.url, NO_AUTH);
|
|
5030
5772
|
const properties = await client.session.properties();
|
|
5031
5773
|
if (properties["has-user-setup"]) {
|
|
5032
5774
|
ctx.ui.info("Metabase already has an admin user");
|
|
@@ -5034,7 +5776,11 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5034
5776
|
}
|
|
5035
5777
|
const token = properties["setup-token"];
|
|
5036
5778
|
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);
|
|
5779
|
+
const user = await chooseUser(ctx, options.variant, resolved);
|
|
5780
|
+
await ctx.saveSecrets({
|
|
5781
|
+
...ctx.secrets,
|
|
5782
|
+
admin: storedLogin(options.variant, user)
|
|
5783
|
+
});
|
|
5038
5784
|
const session = await client.setup.create({
|
|
5039
5785
|
token,
|
|
5040
5786
|
user,
|
|
@@ -5043,7 +5789,10 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5043
5789
|
ctx.session.metabaseSession.set(session.id);
|
|
5044
5790
|
await ctx.saveState({
|
|
5045
5791
|
...ctx.state,
|
|
5046
|
-
admin:
|
|
5792
|
+
admin: {
|
|
5793
|
+
kind: options.variant,
|
|
5794
|
+
email: user.email
|
|
5795
|
+
}
|
|
5047
5796
|
});
|
|
5048
5797
|
ctx.ui.success(`Admin user ${user.email} created`);
|
|
5049
5798
|
return "completed";
|
|
@@ -5055,8 +5804,7 @@ function adminUserStep(options, deps = hostDeps$8) {
|
|
|
5055
5804
|
const STEP_ID$5 = "api-key";
|
|
5056
5805
|
const API_KEY_KEYS = {
|
|
5057
5806
|
email: `${STEP_ID$5}.email`,
|
|
5058
|
-
password: `${STEP_ID$5}.password
|
|
5059
|
-
profile: `${STEP_ID$5}.profile`
|
|
5807
|
+
password: `${STEP_ID$5}.password`
|
|
5060
5808
|
};
|
|
5061
5809
|
const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
5062
5810
|
function hostDeps$7(ctx) {
|
|
@@ -5066,11 +5814,12 @@ function hostDeps$7(ctx) {
|
|
|
5066
5814
|
};
|
|
5067
5815
|
}
|
|
5068
5816
|
async function adminCredentials(ctx) {
|
|
5069
|
-
const
|
|
5070
|
-
if (
|
|
5071
|
-
username:
|
|
5072
|
-
password:
|
|
5817
|
+
const stored = ctx.secrets.admin;
|
|
5818
|
+
if (stored !== null) return {
|
|
5819
|
+
username: stored.email,
|
|
5820
|
+
password: stored.password
|
|
5073
5821
|
};
|
|
5822
|
+
const admin = ctx.state.admin;
|
|
5074
5823
|
ctx.ui.info("An admin login is needed once to create the API key; it is not saved");
|
|
5075
5824
|
return {
|
|
5076
5825
|
username: await ctx.prompts.text({
|
|
@@ -5108,47 +5857,35 @@ async function freshApiKey(ctx, client) {
|
|
|
5108
5857
|
group_id: groupId
|
|
5109
5858
|
})).unmasked_key;
|
|
5110
5859
|
}
|
|
5111
|
-
async function
|
|
5112
|
-
|
|
5860
|
+
async function authenticates(client) {
|
|
5861
|
+
try {
|
|
5862
|
+
await client.user.current();
|
|
5863
|
+
return true;
|
|
5864
|
+
} catch (error) {
|
|
5865
|
+
if (isRejectedCredential(error)) return false;
|
|
5866
|
+
throw error;
|
|
5867
|
+
}
|
|
5868
|
+
}
|
|
5869
|
+
async function workingApiKey(ctx, deps, url) {
|
|
5870
|
+
const stored = ctx.secrets.apiKey;
|
|
5871
|
+
if (stored !== null) {
|
|
5872
|
+
if (await authenticates(deps.client(url, {
|
|
5873
|
+
kind: "api-key",
|
|
5874
|
+
key: stored
|
|
5875
|
+
}))) return stored;
|
|
5876
|
+
ctx.ui.warn("Metabase rejects the stored API key; replacing it");
|
|
5877
|
+
}
|
|
5113
5878
|
const sessionId = await superuserSession(ctx, deps, url);
|
|
5114
5879
|
const apiKey = await freshApiKey(ctx, deps.client(url, {
|
|
5115
5880
|
kind: "session",
|
|
5116
5881
|
id: sessionId
|
|
5117
5882
|
}));
|
|
5118
|
-
await ctx.
|
|
5119
|
-
...ctx.
|
|
5883
|
+
await ctx.saveSecrets({
|
|
5884
|
+
...ctx.secrets,
|
|
5120
5885
|
apiKey
|
|
5121
5886
|
});
|
|
5122
5887
|
return apiKey;
|
|
5123
5888
|
}
|
|
5124
|
-
async function chooseProfile(ctx, mb, url) {
|
|
5125
|
-
const profile = ctx.state.mbProfile;
|
|
5126
|
-
if (profile !== "default") return profile;
|
|
5127
|
-
let existing;
|
|
5128
|
-
try {
|
|
5129
|
-
existing = await mb.authStatus(DEFAULT_MB_PROFILE);
|
|
5130
|
-
} catch (error) {
|
|
5131
|
-
if (error instanceof ProcessNotFoundError) return profile;
|
|
5132
|
-
throw error;
|
|
5133
|
-
}
|
|
5134
|
-
if (!existing.present || existing.url === url) return profile;
|
|
5135
|
-
const chosen = await ctx.prompts.select({
|
|
5136
|
-
key: API_KEY_KEYS.profile,
|
|
5137
|
-
message: `mb profile ${DEFAULT_MB_PROFILE} already points at ${existing.url}; which profile should this instance use?`,
|
|
5138
|
-
choices: [{
|
|
5139
|
-
value: "rde",
|
|
5140
|
-
label: `rde, and leave ${DEFAULT_MB_PROFILE} alone`
|
|
5141
|
-
}, {
|
|
5142
|
-
value: DEFAULT_MB_PROFILE,
|
|
5143
|
-
label: `${DEFAULT_MB_PROFILE}, replacing it`
|
|
5144
|
-
}]
|
|
5145
|
-
});
|
|
5146
|
-
if (chosen !== profile) await ctx.saveState({
|
|
5147
|
-
...ctx.state,
|
|
5148
|
-
mbProfile: chosen
|
|
5149
|
-
});
|
|
5150
|
-
return chosen;
|
|
5151
|
-
}
|
|
5152
5889
|
function usageNote(profile) {
|
|
5153
5890
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
5154
5891
|
return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
|
|
@@ -5158,7 +5895,7 @@ function apiKeyStep(deps = hostDeps$7) {
|
|
|
5158
5895
|
id: STEP_ID$5,
|
|
5159
5896
|
title: "Connect the Metabase CLI",
|
|
5160
5897
|
async detect(ctx) {
|
|
5161
|
-
if (ctx.
|
|
5898
|
+
if (ctx.secrets.apiKey === null) return "todo";
|
|
5162
5899
|
const mb = mbTool(deps(ctx).runner);
|
|
5163
5900
|
const profile = ctx.state.mbProfile;
|
|
5164
5901
|
try {
|
|
@@ -5172,9 +5909,9 @@ function apiKeyStep(deps = hostDeps$7) {
|
|
|
5172
5909
|
async run(ctx) {
|
|
5173
5910
|
const resolved = deps(ctx);
|
|
5174
5911
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5175
|
-
const apiKey = await
|
|
5912
|
+
const apiKey = await workingApiKey(ctx, resolved, configured.url);
|
|
5176
5913
|
const mb = mbTool(resolved.runner);
|
|
5177
|
-
const profile =
|
|
5914
|
+
const profile = ctx.state.mbProfile;
|
|
5178
5915
|
await mb.authLogin({
|
|
5179
5916
|
url: configured.url,
|
|
5180
5917
|
profile,
|
|
@@ -5574,14 +6311,14 @@ function isMbUnavailable(error) {
|
|
|
5574
6311
|
}
|
|
5575
6312
|
async function runGitSync(ctx, deps) {
|
|
5576
6313
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5577
|
-
if (ctx.
|
|
6314
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
5578
6315
|
const run = {
|
|
5579
6316
|
ctx,
|
|
5580
6317
|
deps,
|
|
5581
6318
|
git: gitTool(deps.runner),
|
|
5582
6319
|
client: deps.client(configured.url, {
|
|
5583
6320
|
kind: "api-key",
|
|
5584
|
-
key: ctx.
|
|
6321
|
+
key: ctx.secrets.apiKey
|
|
5585
6322
|
})
|
|
5586
6323
|
};
|
|
5587
6324
|
const flow = await runFlow({
|
|
@@ -5637,8 +6374,8 @@ function validateToken(value) {
|
|
|
5637
6374
|
return "A license token is 64 hexadecimal characters";
|
|
5638
6375
|
}
|
|
5639
6376
|
function requireApiKey$1(ctx) {
|
|
5640
|
-
if (ctx.
|
|
5641
|
-
return ctx.
|
|
6377
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
6378
|
+
return ctx.secrets.apiKey;
|
|
5642
6379
|
}
|
|
5643
6380
|
function activeLabel(status) {
|
|
5644
6381
|
const plan = status["plan-alias"];
|
|
@@ -5651,15 +6388,15 @@ function licenseStep(deps = hostDeps$5) {
|
|
|
5651
6388
|
async detect(ctx) {
|
|
5652
6389
|
if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
|
|
5653
6390
|
const configured = configuredRuntime(ctx.state);
|
|
5654
|
-
if (configured === null || ctx.
|
|
6391
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
5655
6392
|
const client = deps(ctx).client(configured.url, {
|
|
5656
6393
|
kind: "api-key",
|
|
5657
|
-
key: ctx.
|
|
6394
|
+
key: ctx.secrets.apiKey
|
|
5658
6395
|
});
|
|
5659
6396
|
try {
|
|
5660
6397
|
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
5661
6398
|
} catch (error) {
|
|
5662
|
-
if (isNoTokenYet(error)) return "todo";
|
|
6399
|
+
if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
|
|
5663
6400
|
throw error;
|
|
5664
6401
|
}
|
|
5665
6402
|
},
|
|
@@ -5894,9 +6631,9 @@ function isNotServingYet(error) {
|
|
|
5894
6631
|
function isNotFound$1(error) {
|
|
5895
6632
|
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
|
|
5896
6633
|
}
|
|
5897
|
-
function requireApiKey(
|
|
5898
|
-
if (
|
|
5899
|
-
return
|
|
6634
|
+
function requireApiKey(ctx) {
|
|
6635
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError(`instance ${ctx.state.name} has no API key yet; run \`rde init\``);
|
|
6636
|
+
return ctx.secrets.apiKey;
|
|
5900
6637
|
}
|
|
5901
6638
|
async function recordedDatabase(record, client) {
|
|
5902
6639
|
if (record.databaseId === null) return null;
|
|
@@ -6018,23 +6755,23 @@ function sampleDatabaseStep(deps = hostDeps$3) {
|
|
|
6018
6755
|
if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
|
|
6019
6756
|
const record = ctx.state.sampleDatabase;
|
|
6020
6757
|
const configured = configuredRuntime(ctx.state);
|
|
6021
|
-
if (record === null || configured === null || ctx.
|
|
6758
|
+
if (record === null || configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
6022
6759
|
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
|
|
6023
6760
|
const client = resolved.client(configured.url, {
|
|
6024
6761
|
kind: "api-key",
|
|
6025
|
-
key: ctx.
|
|
6762
|
+
key: ctx.secrets.apiKey
|
|
6026
6763
|
});
|
|
6027
6764
|
try {
|
|
6028
6765
|
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
6029
6766
|
} catch (error) {
|
|
6030
|
-
if (isNotServingYet(error)) return "todo";
|
|
6767
|
+
if (isNotServingYet(error) || isRejectedCredential(error)) return "todo";
|
|
6031
6768
|
throw error;
|
|
6032
6769
|
}
|
|
6033
6770
|
},
|
|
6034
6771
|
async run(ctx) {
|
|
6035
6772
|
const resolved = deps(ctx);
|
|
6036
6773
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6037
|
-
const apiKey = requireApiKey(ctx
|
|
6774
|
+
const apiKey = requireApiKey(ctx);
|
|
6038
6775
|
const docker = await resolved.runtime.docker.available();
|
|
6039
6776
|
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
6040
6777
|
const choice = await persistedRecord(ctx, resolved);
|
|
@@ -6149,23 +6886,24 @@ function hostDeps$1(ctx) {
|
|
|
6149
6886
|
function isNotServing$1(error) {
|
|
6150
6887
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
6151
6888
|
}
|
|
6152
|
-
function loginLine(
|
|
6153
|
-
|
|
6154
|
-
|
|
6155
|
-
case "default": return `Login: ${
|
|
6156
|
-
case "prompted": return `Login: ${
|
|
6889
|
+
function loginLine(kind, email) {
|
|
6890
|
+
switch (kind) {
|
|
6891
|
+
case "generated":
|
|
6892
|
+
case "legacy-default": return `Login: ${email} (run rde credentials in your terminal for the password)`;
|
|
6893
|
+
case "prompted": return `Login: ${email} with the password you entered`;
|
|
6157
6894
|
}
|
|
6158
6895
|
}
|
|
6159
|
-
async function databaseLines$1(state, deps) {
|
|
6160
|
-
if (state.url === null ||
|
|
6896
|
+
async function databaseLines$1(state, apiKey, deps) {
|
|
6897
|
+
if (state.url === null || apiKey === null) return ["Databases: none connected yet"];
|
|
6161
6898
|
let databases;
|
|
6162
6899
|
try {
|
|
6163
6900
|
databases = (await deps.client(state.url, {
|
|
6164
6901
|
kind: "api-key",
|
|
6165
|
-
key:
|
|
6902
|
+
key: apiKey
|
|
6166
6903
|
}).database.list()).data;
|
|
6167
6904
|
} catch (error) {
|
|
6168
6905
|
if (isNotServing$1(error)) return ["Databases: Metabase is not running; `rde start`"];
|
|
6906
|
+
if (isRejectedCredential(error)) return ["Databases: Metabase rejects the stored API key; `rde init --only api-key`"];
|
|
6169
6907
|
throw error;
|
|
6170
6908
|
}
|
|
6171
6909
|
if (databases.length === 0) return ["Databases: none connected yet; `rde db add`"];
|
|
@@ -6199,14 +6937,13 @@ function launchLines(agents, workdir) {
|
|
|
6199
6937
|
const prefix = workdir === null ? "" : `cd ${workdir} && `;
|
|
6200
6938
|
return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
|
|
6201
6939
|
}
|
|
6202
|
-
async function summaryLines(state, deps) {
|
|
6940
|
+
async function summaryLines(state, apiKey, deps) {
|
|
6203
6941
|
const profile = state.mbProfile;
|
|
6204
6942
|
const lines = [];
|
|
6205
6943
|
if (state.url !== null) lines.push(`Metabase: ${state.url}`);
|
|
6206
|
-
|
|
6207
|
-
if (login !== null) lines.push(login);
|
|
6944
|
+
if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
|
|
6208
6945
|
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));
|
|
6946
|
+
lines.push(...await databaseLines$1(state, apiKey, deps));
|
|
6210
6947
|
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
6211
6948
|
lines.push(...gitLines(state));
|
|
6212
6949
|
const installed = await skillsCli(deps.runner).list();
|
|
@@ -6223,7 +6960,8 @@ function summaryStep(deps = hostDeps$1) {
|
|
|
6223
6960
|
return "todo";
|
|
6224
6961
|
},
|
|
6225
6962
|
async run(ctx) {
|
|
6226
|
-
|
|
6963
|
+
const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
|
|
6964
|
+
ctx.ui.note(lines.join("\n"), "Your environment");
|
|
6227
6965
|
return "completed";
|
|
6228
6966
|
}
|
|
6229
6967
|
};
|
|
@@ -6257,7 +6995,7 @@ function initSteps(options) {
|
|
|
6257
6995
|
//#region src/commands/doctor.ts
|
|
6258
6996
|
function print(report, json) {
|
|
6259
6997
|
if (json) {
|
|
6260
|
-
printJson(report);
|
|
6998
|
+
printJson(doctorJson(report));
|
|
6261
6999
|
return;
|
|
6262
7000
|
}
|
|
6263
7001
|
printLines(doctorLines(report));
|
|
@@ -6288,12 +7026,12 @@ const doctor = defineRdeCommand({
|
|
|
6288
7026
|
const name = await instanceNameOrDefault();
|
|
6289
7027
|
const state = await readInstanceState(name);
|
|
6290
7028
|
const deps = hostDoctorDeps(name, interruptSignal);
|
|
6291
|
-
let report = await collectDoctor(name, state, deps);
|
|
7029
|
+
let report = await collectDoctor(name, state, state === null ? null : await readInstanceSecrets(name), deps);
|
|
6292
7030
|
if (args.fix && report.failed > 0) {
|
|
6293
7031
|
if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
|
|
6294
7032
|
print(report, args.json);
|
|
6295
7033
|
const ctx = await createWizardContext(name, { signal: interruptSignal });
|
|
6296
|
-
if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, deps);
|
|
7034
|
+
if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, ctx.secrets, deps);
|
|
6297
7035
|
}
|
|
6298
7036
|
print(report, args.json);
|
|
6299
7037
|
if (report.failed > 0) throw new Error(doctorSummary(report));
|
|
@@ -6318,7 +7056,7 @@ const EnvString = z.string().transform((value, ctx) => {
|
|
|
6318
7056
|
}
|
|
6319
7057
|
return resolved;
|
|
6320
7058
|
});
|
|
6321
|
-
const AdminVariant = z.enum(["
|
|
7059
|
+
const AdminVariant = z.enum(["generated", "prompted"]);
|
|
6322
7060
|
const RemoteKind = z.enum(["local-bare", "hosted"]);
|
|
6323
7061
|
const FieldValue = z.union([
|
|
6324
7062
|
EnvString,
|
|
@@ -6347,7 +7085,6 @@ const SECTIONS = {
|
|
|
6347
7085
|
"first-name": z.string().optional(),
|
|
6348
7086
|
"last-name": z.string().optional()
|
|
6349
7087
|
}).optional(),
|
|
6350
|
-
"api-key": z.strictObject({ profile: MbProfileChoice }).optional(),
|
|
6351
7088
|
license: z.strictObject({ token: EnvString.nullable() }).optional(),
|
|
6352
7089
|
databases: z.array(DatabaseEntry).optional(),
|
|
6353
7090
|
"git-sync": z.strictObject({
|
|
@@ -6443,8 +7180,6 @@ function keyedAnswers(answers) {
|
|
|
6443
7180
|
const map = /* @__PURE__ */ new Map();
|
|
6444
7181
|
const admin = answers["admin-user"];
|
|
6445
7182
|
if (admin !== void 0) addAdmin(map, admin);
|
|
6446
|
-
const apiKey = answers["api-key"];
|
|
6447
|
-
if (apiKey !== void 0) add(map, API_KEY_KEYS.profile, select$1(apiKey.profile));
|
|
6448
7183
|
const license = answers.license;
|
|
6449
7184
|
if (license !== void 0) add(map, LICENSE_KEYS.token, password$1(license.token ?? SKIP_LICENSE));
|
|
6450
7185
|
const databases = answers.databases;
|
|
@@ -6500,7 +7235,6 @@ const FLAG_FOR_KEY = {
|
|
|
6500
7235
|
[ADMIN_USER_KEYS.password]: "--admin-password",
|
|
6501
7236
|
[API_KEY_KEYS.email]: "--admin-email",
|
|
6502
7237
|
[API_KEY_KEYS.password]: "--admin-password",
|
|
6503
|
-
[API_KEY_KEYS.profile]: "--mb-profile",
|
|
6504
7238
|
[LICENSE_KEYS.token]: "--license-token or --skip-license",
|
|
6505
7239
|
[DATABASE_KEYS.engine]: "--database or --skip-databases",
|
|
6506
7240
|
[DATABASE_KEYS.uri]: "--database",
|
|
@@ -6554,7 +7288,7 @@ function parseDatabase(spec) {
|
|
|
6554
7288
|
function adminSection(flags, secrets) {
|
|
6555
7289
|
const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
|
|
6556
7290
|
if (flags.admin === void 0 && !prompted) return;
|
|
6557
|
-
if (flags.admin === "
|
|
7291
|
+
if (flags.admin === "generated" && prompted) throw conflict("--admin generated", "--admin-email/--admin-password");
|
|
6558
7292
|
return {
|
|
6559
7293
|
variant: flags.admin ?? "prompted",
|
|
6560
7294
|
...flags.adminEmail !== void 0 && { email: flags.adminEmail },
|
|
@@ -6657,8 +7391,7 @@ async function answersFromFlags(flags, readStdin) {
|
|
|
6657
7391
|
license: licenseSection(flags, secrets),
|
|
6658
7392
|
databases: databasesSection(flags),
|
|
6659
7393
|
"git-sync": gitSection(flags, secrets),
|
|
6660
|
-
"agents-skills": agentsSection(flags)
|
|
6661
|
-
"api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
|
|
7394
|
+
"agents-skills": agentsSection(flags)
|
|
6662
7395
|
};
|
|
6663
7396
|
const present = Object.entries(sections).filter(([, section]) => section !== void 0);
|
|
6664
7397
|
return parseInitAnswers(Object.fromEntries(present), "the init flags");
|
|
@@ -6752,10 +7485,7 @@ function applyRecord(draft, record) {
|
|
|
6752
7485
|
if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
|
|
6753
7486
|
else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
6754
7487
|
else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
|
|
6755
|
-
else if (key ===
|
|
6756
|
-
const profile = MbProfileChoice.safeParse(value);
|
|
6757
|
-
if (profile.success) draft.answers["api-key"] = { profile: profile.data };
|
|
6758
|
-
} else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
|
|
7488
|
+
else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
|
|
6759
7489
|
}
|
|
6760
7490
|
function writableUri(uri, index) {
|
|
6761
7491
|
return URI_WITH_PASSWORD.test(uri) ? envReference(databaseEnv(index, "uri")) : uri;
|
|
@@ -7027,7 +7757,7 @@ const init = defineRdeCommand({
|
|
|
7027
7757
|
},
|
|
7028
7758
|
admin: {
|
|
7029
7759
|
type: "string",
|
|
7030
|
-
description: "Admin credentials:
|
|
7760
|
+
description: "Admin credentials: generated (a random password; `rde credentials` shows it) or prompted"
|
|
7031
7761
|
},
|
|
7032
7762
|
"admin-email": {
|
|
7033
7763
|
type: "string",
|
|
@@ -7085,10 +7815,6 @@ const init = defineRdeCommand({
|
|
|
7085
7815
|
description: "Install the skill for no agent",
|
|
7086
7816
|
default: false
|
|
7087
7817
|
},
|
|
7088
|
-
"mb-profile": {
|
|
7089
|
-
type: "string",
|
|
7090
|
-
description: "mb profile for this instance when `default` already points elsewhere: default or rde"
|
|
7091
|
-
},
|
|
7092
7818
|
yes: {
|
|
7093
7819
|
type: "boolean",
|
|
7094
7820
|
description: "Answer yes to every confirmation (skipping databases)",
|
|
@@ -7123,8 +7849,7 @@ const init = defineRdeCommand({
|
|
|
7123
7849
|
gitToken: args["git-token"],
|
|
7124
7850
|
skipGit: args["skip-git"],
|
|
7125
7851
|
agents: args.agents,
|
|
7126
|
-
skipAgents: args["skip-agents"]
|
|
7127
|
-
mbProfile: args["mb-profile"]
|
|
7852
|
+
skipAgents: args["skip-agents"]
|
|
7128
7853
|
}
|
|
7129
7854
|
};
|
|
7130
7855
|
const printTo = args["print-answers"];
|
|
@@ -7243,11 +7968,11 @@ function isNotServing(error) {
|
|
|
7243
7968
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
7244
7969
|
}
|
|
7245
7970
|
async function databasesOf(ctx, deps, health) {
|
|
7246
|
-
if (health !== "healthy" || ctx.
|
|
7971
|
+
if (health !== "healthy" || ctx.secrets.apiKey === null) return null;
|
|
7247
7972
|
try {
|
|
7248
|
-
return await listDatabases(ctx.state, { client: deps.client });
|
|
7973
|
+
return await listDatabases(ctx.state, ctx.secrets.apiKey, { client: deps.client });
|
|
7249
7974
|
} catch (error) {
|
|
7250
|
-
if (isNotServing(error)) return null;
|
|
7975
|
+
if (isNotServing(error) || isRejectedCredential(error)) return null;
|
|
7251
7976
|
throw error;
|
|
7252
7977
|
}
|
|
7253
7978
|
}
|
|
@@ -7290,9 +8015,13 @@ async function collectStatus(ctx, deps) {
|
|
|
7290
8015
|
const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
|
|
7291
8016
|
const health = configured === null ? null : await healthOf(configured.url, deps);
|
|
7292
8017
|
const databases = health === null ? null : await databasesOf(ctx, deps, health);
|
|
8018
|
+
const admin = ctx.state.admin;
|
|
7293
8019
|
return {
|
|
7294
8020
|
instance: ctx.state.name,
|
|
7295
8021
|
url: configured?.url ?? null,
|
|
8022
|
+
mbProfile: ctx.state.mbProfile,
|
|
8023
|
+
adminEmail: admin === null ? null : admin.email,
|
|
8024
|
+
adminVariant: admin === null ? null : admin.kind,
|
|
7296
8025
|
runtime: configured === null || runtime === null ? null : {
|
|
7297
8026
|
kind: configured.runtime.kind,
|
|
7298
8027
|
installed: runtime.installed,
|
|
@@ -7346,6 +8075,8 @@ function databaseLines(report) {
|
|
|
7346
8075
|
function statusLines(report) {
|
|
7347
8076
|
return [
|
|
7348
8077
|
report.url === null ? `Instance ${report.instance}` : `Instance ${report.instance} — ${report.url}`,
|
|
8078
|
+
...report.adminVariant === null || report.adminEmail === null ? [] : [loginLine(report.adminVariant, report.adminEmail)],
|
|
8079
|
+
`Metabase CLI: ${mbCommand(report.mbProfile)} <command>`,
|
|
7349
8080
|
runtimeLine(report),
|
|
7350
8081
|
versionLine(report),
|
|
7351
8082
|
`Health: ${report.health ?? "unknown"}`,
|
|
@@ -7648,7 +8379,7 @@ async function planUninstall(options, deps) {
|
|
|
7648
8379
|
if (jar.removal !== null) removals.push(jar.removal);
|
|
7649
8380
|
if (jar.kept !== null) kept.push(jar.kept);
|
|
7650
8381
|
}
|
|
7651
|
-
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE,
|
|
8382
|
+
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, mbProfileFor(options.instance)], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
|
|
7652
8383
|
removals.push(...profiles);
|
|
7653
8384
|
if (options.removeSkill) {
|
|
7654
8385
|
const skill = await findSkill(deps.runner);
|
|
@@ -7657,6 +8388,11 @@ async function planUninstall(options, deps) {
|
|
|
7657
8388
|
const dir = instanceDir(options.instance);
|
|
7658
8389
|
const dirPresent = await pathExists(dir);
|
|
7659
8390
|
if (!options.keepData) {
|
|
8391
|
+
const secrets = instanceSecretsPath(options.instance);
|
|
8392
|
+
if (await pathExists(secrets)) removals.push({
|
|
8393
|
+
label: `Stored credentials ${secrets} (the admin password and the API key)`,
|
|
8394
|
+
remove: () => promises.rm(secrets, { force: true })
|
|
8395
|
+
});
|
|
7660
8396
|
if (dirPresent) removals.push({
|
|
7661
8397
|
label: instanceDirLabel(dir, state),
|
|
7662
8398
|
remove: () => promises.rm(dir, {
|
|
@@ -8058,7 +8794,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
8058
8794
|
const main = defineCommand({
|
|
8059
8795
|
meta: {
|
|
8060
8796
|
name: "rde",
|
|
8061
|
-
version: "0.1.
|
|
8797
|
+
version: "0.1.3-alpha.rde-fixes.59b956e",
|
|
8062
8798
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
8063
8799
|
},
|
|
8064
8800
|
subCommands: {
|
|
@@ -8066,6 +8802,7 @@ const main = defineCommand({
|
|
|
8066
8802
|
start,
|
|
8067
8803
|
stop,
|
|
8068
8804
|
status,
|
|
8805
|
+
credentials,
|
|
8069
8806
|
logs,
|
|
8070
8807
|
open,
|
|
8071
8808
|
db,
|