@metabase/rde 0.1.17 → 0.5.0
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 +59 -28
- package/dist/cli.mjs +971 -497
- package/package.json +12 -2
package/dist/cli.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { pipeline } from "node:stream/promises";
|
|
|
11
11
|
import { spawn } from "node:child_process";
|
|
12
12
|
import { connect, createServer } from "node:net";
|
|
13
13
|
import { confirm, intro, isCancel, log, multiselect, note, outro, password, select, spinner, text } from "@clack/prompts";
|
|
14
|
+
import { isDeepStrictEqual } from "node:util";
|
|
14
15
|
import semver from "semver";
|
|
15
16
|
//#region src/errors.ts
|
|
16
17
|
var AbortError = class extends Error {
|
|
@@ -33,6 +34,12 @@ var PortTakenError = class extends ConfigError {
|
|
|
33
34
|
this.port = port;
|
|
34
35
|
}
|
|
35
36
|
};
|
|
37
|
+
var UnsupportedMetabaseError = class extends ConfigError {
|
|
38
|
+
constructor(message) {
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = "UnsupportedMetabaseError";
|
|
41
|
+
}
|
|
42
|
+
};
|
|
36
43
|
var PromptUnavailableError = class extends ConfigError {
|
|
37
44
|
constructor(message) {
|
|
38
45
|
super(message);
|
|
@@ -260,9 +267,9 @@ function withoutEscapes(text) {
|
|
|
260
267
|
const StepId = z.enum([
|
|
261
268
|
"preflight",
|
|
262
269
|
"install-mb",
|
|
263
|
-
"agents-skills",
|
|
264
270
|
"runtime",
|
|
265
271
|
"start-metabase",
|
|
272
|
+
"agents-skills",
|
|
266
273
|
"admin-user",
|
|
267
274
|
"api-key",
|
|
268
275
|
"transforms",
|
|
@@ -362,7 +369,7 @@ const StepRecord = z.object({
|
|
|
362
369
|
completedAt: z.string()
|
|
363
370
|
});
|
|
364
371
|
const InstanceState = z.object({
|
|
365
|
-
schemaVersion: z.literal(
|
|
372
|
+
schemaVersion: z.literal(8),
|
|
366
373
|
name: z.string(),
|
|
367
374
|
runtime: RuntimeState.nullable(),
|
|
368
375
|
url: z.string().nullable(),
|
|
@@ -373,7 +380,6 @@ const InstanceState = z.object({
|
|
|
373
380
|
databases: z.array(DatabaseRecord),
|
|
374
381
|
gitRemote: GitRemoteState.nullable(),
|
|
375
382
|
movedFrom: z.string().nullable(),
|
|
376
|
-
syncRoot: z.int().nullable(),
|
|
377
383
|
steps: z.partialRecord(StepId, StepRecord)
|
|
378
384
|
});
|
|
379
385
|
function configuredRuntime(state) {
|
|
@@ -403,7 +409,7 @@ function mbCommand(profile) {
|
|
|
403
409
|
}
|
|
404
410
|
function emptyInstanceState(name) {
|
|
405
411
|
return {
|
|
406
|
-
schemaVersion:
|
|
412
|
+
schemaVersion: 8,
|
|
407
413
|
name,
|
|
408
414
|
runtime: null,
|
|
409
415
|
url: null,
|
|
@@ -414,7 +420,6 @@ function emptyInstanceState(name) {
|
|
|
414
420
|
databases: [],
|
|
415
421
|
gitRemote: null,
|
|
416
422
|
movedFrom: null,
|
|
417
|
-
syncRoot: null,
|
|
418
423
|
steps: {}
|
|
419
424
|
};
|
|
420
425
|
}
|
|
@@ -427,6 +432,7 @@ const V3 = 3;
|
|
|
427
432
|
const V4 = 4;
|
|
428
433
|
const V5 = 5;
|
|
429
434
|
const V6 = 6;
|
|
435
|
+
const V7 = 7;
|
|
430
436
|
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
431
437
|
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
432
438
|
const AdminV3 = z.discriminatedUnion("kind", [z.object({
|
|
@@ -443,6 +449,7 @@ const StateV5 = z.object({
|
|
|
443
449
|
steps: z.record(z.string(), z.unknown())
|
|
444
450
|
}).loose();
|
|
445
451
|
const StateV6 = z.object({ schemaVersion: z.literal(V6) }).loose();
|
|
452
|
+
const StateV7 = z.object({ schemaVersion: z.literal(V7) }).loose();
|
|
446
453
|
const StateV3 = z.object({
|
|
447
454
|
schemaVersion: z.literal(V3),
|
|
448
455
|
admin: AdminV3.nullable(),
|
|
@@ -480,8 +487,14 @@ function liftV5(raw) {
|
|
|
480
487
|
function liftV6(raw) {
|
|
481
488
|
return {
|
|
482
489
|
...StateV6.parse(raw),
|
|
483
|
-
schemaVersion:
|
|
484
|
-
|
|
490
|
+
schemaVersion: V7
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
function liftV7(raw) {
|
|
494
|
+
const { syncRoot: _syncRoot, ...state } = StateV7.parse(raw);
|
|
495
|
+
return {
|
|
496
|
+
...state,
|
|
497
|
+
schemaVersion: 8
|
|
485
498
|
};
|
|
486
499
|
}
|
|
487
500
|
function adminV4(admin) {
|
|
@@ -523,24 +536,29 @@ function migrateInstanceState(raw) {
|
|
|
523
536
|
case V3: return liftV3(raw);
|
|
524
537
|
case V4: return migrateInstanceState(liftV4(raw));
|
|
525
538
|
case V5: return migrateInstanceState(liftV5(raw));
|
|
526
|
-
case V6: return
|
|
527
|
-
|
|
539
|
+
case V6: return migrateInstanceState(liftV6(raw));
|
|
540
|
+
case V7: return {
|
|
541
|
+
state: InstanceState.parse(liftV7(raw)),
|
|
528
542
|
movedSecrets: null
|
|
529
543
|
};
|
|
530
|
-
case
|
|
544
|
+
case 8: return {
|
|
531
545
|
state: InstanceState.parse(raw),
|
|
532
546
|
movedSecrets: null
|
|
533
547
|
};
|
|
534
|
-
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected
|
|
548
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 8)`);
|
|
535
549
|
}
|
|
536
550
|
}
|
|
537
551
|
//#endregion
|
|
538
552
|
//#region src/env.ts
|
|
539
553
|
const ENV_RDE_HOME = "RDE_HOME";
|
|
540
|
-
const
|
|
554
|
+
const ENV_RDE_DEV = "RDE_DEV";
|
|
541
555
|
function readEnv(name) {
|
|
542
556
|
return process.env[name];
|
|
543
557
|
}
|
|
558
|
+
function readSetEnv(name) {
|
|
559
|
+
const value = readEnv(name);
|
|
560
|
+
return value === void 0 || value.length === 0 ? null : value;
|
|
561
|
+
}
|
|
544
562
|
const ENV_PATH = "PATH";
|
|
545
563
|
const ENV_NO_COLOR = "NO_COLOR";
|
|
546
564
|
const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
|
|
@@ -548,6 +566,9 @@ const ENV_JAVA_HOME = "JAVA_HOME";
|
|
|
548
566
|
const ENV_MB_PROFILE = "MB_PROFILE";
|
|
549
567
|
const ENV_MB_URL = "MB_URL";
|
|
550
568
|
const ENV_MB_API_KEY = "MB_API_KEY";
|
|
569
|
+
function developmentMode() {
|
|
570
|
+
return readSetEnv(ENV_RDE_DEV) === "1";
|
|
571
|
+
}
|
|
551
572
|
function hostEnvironment() {
|
|
552
573
|
return { ...process.env };
|
|
553
574
|
}
|
|
@@ -566,8 +587,8 @@ const INSTANCE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
|
566
587
|
const INSTANCE_NAME_RULE = "letters, digits, - and _, starting with a letter or digit";
|
|
567
588
|
const HOME_TAG_LENGTH = 8;
|
|
568
589
|
function rdeHome() {
|
|
569
|
-
const override =
|
|
570
|
-
if (override !==
|
|
590
|
+
const override = readSetEnv(ENV_RDE_HOME);
|
|
591
|
+
if (override !== null) return resolve(override);
|
|
571
592
|
return join(homedir(), RDE_DIR_NAME);
|
|
572
593
|
}
|
|
573
594
|
function homeTag(home) {
|
|
@@ -700,6 +721,7 @@ async function requireInstanceState(name) {
|
|
|
700
721
|
}
|
|
701
722
|
const LOCK_SUFFIX = ".lock";
|
|
702
723
|
const STALE_SUFFIX = ".stale";
|
|
724
|
+
const PENDING_SUFFIX = ".pending";
|
|
703
725
|
const LOCK_RETRY_MS = 25;
|
|
704
726
|
const LOCK_WAIT_MS = 3e4;
|
|
705
727
|
function lockToken() {
|
|
@@ -717,6 +739,15 @@ function isRunning(pid) {
|
|
|
717
739
|
return !hasCode(error, "ESRCH");
|
|
718
740
|
}
|
|
719
741
|
}
|
|
742
|
+
async function createLock(path, token) {
|
|
743
|
+
const pending = `${path}${PENDING_SUFFIX}.${token.replaceAll(" ", "-")}`;
|
|
744
|
+
await promises.writeFile(pending, token, { mode: 384 });
|
|
745
|
+
try {
|
|
746
|
+
await promises.link(pending, path);
|
|
747
|
+
} finally {
|
|
748
|
+
await promises.rm(pending, { force: true });
|
|
749
|
+
}
|
|
750
|
+
}
|
|
720
751
|
async function takeOverDeadLock(path, held) {
|
|
721
752
|
const aside = `${path}${STALE_SUFFIX}.${held.replaceAll(" ", "-")}`;
|
|
722
753
|
try {
|
|
@@ -741,10 +772,7 @@ async function acquireLock(name, signal) {
|
|
|
741
772
|
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
742
773
|
for (;;) {
|
|
743
774
|
try {
|
|
744
|
-
await
|
|
745
|
-
flag: "wx",
|
|
746
|
-
mode: 384
|
|
747
|
-
});
|
|
775
|
+
await createLock(path, token);
|
|
748
776
|
return token;
|
|
749
777
|
} catch (error) {
|
|
750
778
|
if (isFileNotFound$1(error)) await requireInstanceState(name);
|
|
@@ -958,6 +986,7 @@ function lookupFailure(error) {
|
|
|
958
986
|
}
|
|
959
987
|
function createHttp(options) {
|
|
960
988
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
989
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
961
990
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
962
991
|
async function send(method, path, body) {
|
|
963
992
|
const url = `${baseUrl}${path}`;
|
|
@@ -968,7 +997,7 @@ function createHttp(options) {
|
|
|
968
997
|
const init = {
|
|
969
998
|
method,
|
|
970
999
|
headers,
|
|
971
|
-
signal: AbortSignal.any([options.signal, AbortSignal.timeout(
|
|
1000
|
+
signal: AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)])
|
|
972
1001
|
};
|
|
973
1002
|
if (body !== void 0) {
|
|
974
1003
|
headers["content-type"] = JSON_CONTENT_TYPE;
|
|
@@ -982,7 +1011,7 @@ function createHttp(options) {
|
|
|
982
1011
|
};
|
|
983
1012
|
} catch (error) {
|
|
984
1013
|
if (options.signal.aborted) throw new AbortError();
|
|
985
|
-
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${
|
|
1014
|
+
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${timeoutMs}ms`);
|
|
986
1015
|
throw new MetabaseUnreachableError(url, describeCause(error));
|
|
987
1016
|
}
|
|
988
1017
|
}
|
|
@@ -2101,14 +2130,23 @@ async function whichCommand(name) {
|
|
|
2101
2130
|
//#endregion
|
|
2102
2131
|
//#region src/tools/agents.ts
|
|
2103
2132
|
const AgentId = z.enum([
|
|
2133
|
+
"opencode",
|
|
2104
2134
|
"claude-code",
|
|
2105
2135
|
"codex",
|
|
2106
2136
|
"cursor",
|
|
2107
2137
|
"gemini-cli",
|
|
2108
|
-
"opencode",
|
|
2109
2138
|
"windsurf"
|
|
2110
2139
|
]);
|
|
2111
2140
|
const AGENTS = [
|
|
2141
|
+
{
|
|
2142
|
+
id: "opencode",
|
|
2143
|
+
displayName: "OpenCode",
|
|
2144
|
+
skillsCliName: "opencode",
|
|
2145
|
+
command: "opencode",
|
|
2146
|
+
launch: "opencode",
|
|
2147
|
+
readsSharedSkills: true,
|
|
2148
|
+
configDir: (_home, configHome) => join(configHome, "opencode")
|
|
2149
|
+
},
|
|
2112
2150
|
{
|
|
2113
2151
|
id: "claude-code",
|
|
2114
2152
|
displayName: "Claude Code",
|
|
@@ -2145,15 +2183,6 @@ const AGENTS = [
|
|
|
2145
2183
|
readsSharedSkills: true,
|
|
2146
2184
|
configDir: (home) => join(home, ".gemini")
|
|
2147
2185
|
},
|
|
2148
|
-
{
|
|
2149
|
-
id: "opencode",
|
|
2150
|
-
displayName: "OpenCode",
|
|
2151
|
-
skillsCliName: "opencode",
|
|
2152
|
-
command: "opencode",
|
|
2153
|
-
launch: "opencode",
|
|
2154
|
-
readsSharedSkills: true,
|
|
2155
|
-
configDir: (_home, configHome) => join(configHome, "opencode")
|
|
2156
|
-
},
|
|
2157
2186
|
{
|
|
2158
2187
|
id: "windsurf",
|
|
2159
2188
|
displayName: "Windsurf",
|
|
@@ -2200,18 +2229,74 @@ async function detectAgents(probe) {
|
|
|
2200
2229
|
}
|
|
2201
2230
|
//#endregion
|
|
2202
2231
|
//#region src/metabase/domain/version-info.ts
|
|
2203
|
-
const
|
|
2232
|
+
const VersionEntry = z.object({ version: z.string() }).loose();
|
|
2233
|
+
const VersionInfo = z.object({ latest: VersionEntry }).loose();
|
|
2234
|
+
const VersionHistory = z.object({
|
|
2235
|
+
latest: VersionEntry,
|
|
2236
|
+
older: z.array(VersionEntry)
|
|
2237
|
+
}).loose();
|
|
2204
2238
|
//#endregion
|
|
2205
2239
|
//#region src/metabase/releases.ts
|
|
2206
2240
|
const DOWNLOADS_HOST = "https://downloads.metabase.com";
|
|
2207
2241
|
const VERSION_INFO_HOST = "https://static.metabase.com";
|
|
2208
2242
|
const VERSION_INFO_PATH = "/version-info-ee.json";
|
|
2209
2243
|
const JAR_FILE = "metabase.jar";
|
|
2210
|
-
const RELEASE_TAG$1 = /^v(?<edition>[01])\.\d
|
|
2244
|
+
const RELEASE_TAG$1 = /^v(?<edition>[01])\.(?<major>\d+)\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
|
|
2245
|
+
const MAJOR_LINE_TAG = /^v(?<edition>[01])\.(?<major>\d+)\.x$/;
|
|
2246
|
+
const FINAL_ENTERPRISE_RELEASE = /^v1\.\d+\.\d+(?:\.\d+)?$/;
|
|
2211
2247
|
const ENTERPRISE_EDITION = "1";
|
|
2248
|
+
const OPEN_SOURCE_EDITION = "0";
|
|
2249
|
+
const SNAPSHOT_SUFFIX = "-SNAPSHOT";
|
|
2212
2250
|
function isReleaseTag(value) {
|
|
2213
2251
|
return RELEASE_TAG$1.test(value);
|
|
2214
2252
|
}
|
|
2253
|
+
function isMajorLineTag(value) {
|
|
2254
|
+
return MAJOR_LINE_TAG.test(value);
|
|
2255
|
+
}
|
|
2256
|
+
function majorLineTag(major) {
|
|
2257
|
+
return `v${ENTERPRISE_EDITION}.${major}.x`;
|
|
2258
|
+
}
|
|
2259
|
+
function tagGroups(tag) {
|
|
2260
|
+
return (RELEASE_TAG$1.exec(tag) ?? MAJOR_LINE_TAG.exec(tag))?.groups;
|
|
2261
|
+
}
|
|
2262
|
+
function isOpenSourceRelease(version) {
|
|
2263
|
+
return tagGroups(version)?.["edition"] === OPEN_SOURCE_EDITION;
|
|
2264
|
+
}
|
|
2265
|
+
function isMasterBuild(version) {
|
|
2266
|
+
return !(isReleaseTag(version) || isMajorLineTag(version)) || version.endsWith(SNAPSHOT_SUFFIX);
|
|
2267
|
+
}
|
|
2268
|
+
function releaseMajor(version) {
|
|
2269
|
+
const major = tagGroups(version)?.["major"];
|
|
2270
|
+
return major === void 0 ? null : Number.parseInt(major, 10);
|
|
2271
|
+
}
|
|
2272
|
+
function releaseNumbers(version) {
|
|
2273
|
+
return version.slice(1).split(".").map((part) => Number.parseInt(part, 10));
|
|
2274
|
+
}
|
|
2275
|
+
function compareReleases(left, right) {
|
|
2276
|
+
const a = releaseNumbers(left);
|
|
2277
|
+
const b = releaseNumbers(right);
|
|
2278
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
2279
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
2280
|
+
if (difference !== 0) return difference;
|
|
2281
|
+
}
|
|
2282
|
+
return 0;
|
|
2283
|
+
}
|
|
2284
|
+
function supportedRelease(version) {
|
|
2285
|
+
if (!FINAL_ENTERPRISE_RELEASE.test(version)) return null;
|
|
2286
|
+
const major = releaseMajor(version);
|
|
2287
|
+
return major !== null && major >= 65 ? {
|
|
2288
|
+
version,
|
|
2289
|
+
major
|
|
2290
|
+
} : null;
|
|
2291
|
+
}
|
|
2292
|
+
function newestSupportedRelease(versions) {
|
|
2293
|
+
let newest = null;
|
|
2294
|
+
for (const version of versions) {
|
|
2295
|
+
const release = supportedRelease(version);
|
|
2296
|
+
if (release !== null && (newest === null || compareReleases(version, newest.version) > 0)) newest = release;
|
|
2297
|
+
}
|
|
2298
|
+
return newest;
|
|
2299
|
+
}
|
|
2215
2300
|
function jarDownloadUrl(version) {
|
|
2216
2301
|
const edition = RELEASE_TAG$1.exec(version)?.groups?.["edition"];
|
|
2217
2302
|
if (edition === void 0) throw new Error(`${version} is not a Metabase release tag`);
|
|
@@ -2224,9 +2309,15 @@ function releaseLookup(signal, options = {}) {
|
|
|
2224
2309
|
signal,
|
|
2225
2310
|
...options
|
|
2226
2311
|
});
|
|
2227
|
-
return {
|
|
2228
|
-
|
|
2229
|
-
|
|
2312
|
+
return {
|
|
2313
|
+
async latestVersion() {
|
|
2314
|
+
return (await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionInfo })).latest.version;
|
|
2315
|
+
},
|
|
2316
|
+
async newestSupportedRelease() {
|
|
2317
|
+
const info = await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionHistory });
|
|
2318
|
+
return newestSupportedRelease([info.latest, ...info.older].map((entry) => entry.version));
|
|
2319
|
+
}
|
|
2320
|
+
};
|
|
2230
2321
|
}
|
|
2231
2322
|
function fileDownloader(signal, options = {}) {
|
|
2232
2323
|
return (url, destination, onProgress) => downloadFile(url, destination, {
|
|
@@ -2294,11 +2385,13 @@ const PortBinding = z.array(z.object({
|
|
|
2294
2385
|
HostIp: z.string(),
|
|
2295
2386
|
HostPort: z.string()
|
|
2296
2387
|
}).loose()).nullable();
|
|
2388
|
+
const EnvEntry = z.string().regex(/^[^=]+=/, "expected NAME=value");
|
|
2297
2389
|
const InspectedContainer = z.object({
|
|
2298
2390
|
State: z.object({ Running: z.boolean() }).loose(),
|
|
2299
2391
|
Config: z.object({
|
|
2300
2392
|
Image: z.string(),
|
|
2301
|
-
Labels: z.record(z.string(), z.string()).nullish()
|
|
2393
|
+
Labels: z.record(z.string(), z.string()).nullish(),
|
|
2394
|
+
Env: z.array(EnvEntry).nullish()
|
|
2302
2395
|
}).loose(),
|
|
2303
2396
|
HostConfig: z.object({ PortBindings: z.record(z.string(), PortBinding).nullable() }).loose(),
|
|
2304
2397
|
NetworkSettings: z.object({ Ports: z.record(z.string(), PortBinding).nullish() }).loose()
|
|
@@ -2329,6 +2422,14 @@ function portMappings(bindings) {
|
|
|
2329
2422
|
}
|
|
2330
2423
|
return mappings;
|
|
2331
2424
|
}
|
|
2425
|
+
function containerEnv(entries) {
|
|
2426
|
+
const env = {};
|
|
2427
|
+
for (const entry of entries) {
|
|
2428
|
+
const separator = entry.indexOf("=");
|
|
2429
|
+
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
2430
|
+
}
|
|
2431
|
+
return env;
|
|
2432
|
+
}
|
|
2332
2433
|
function createArgs(options) {
|
|
2333
2434
|
const args = [
|
|
2334
2435
|
"create",
|
|
@@ -2420,7 +2521,8 @@ function dockerTool(runner) {
|
|
|
2420
2521
|
image: labels?.[IMAGE_LABEL] ?? container.Config.Image,
|
|
2421
2522
|
ports: portMappings(container.HostConfig.PortBindings),
|
|
2422
2523
|
forwarded: portMappings(container.NetworkSettings.Ports ?? null),
|
|
2423
|
-
home: labels?.[HOME_LABEL] ?? null
|
|
2524
|
+
home: labels?.[HOME_LABEL] ?? null,
|
|
2525
|
+
env: containerEnv(container.Config.Env ?? [])
|
|
2424
2526
|
};
|
|
2425
2527
|
}
|
|
2426
2528
|
async function portClaims() {
|
|
@@ -2617,6 +2719,10 @@ const PLUGINS_DIR = "plugins";
|
|
|
2617
2719
|
const METABASE_STOP_GRACE_MS = 3e4;
|
|
2618
2720
|
const DB_DIR = "metabase.db";
|
|
2619
2721
|
const DB_FILE = "metabase.db";
|
|
2722
|
+
const STAGING_STORE_ENV = {
|
|
2723
|
+
MB_RUN_MODE: "e2e",
|
|
2724
|
+
MB_STORE_USE_STAGING: "true"
|
|
2725
|
+
};
|
|
2620
2726
|
function instanceUrl(port) {
|
|
2621
2727
|
return `http://localhost:${port}`;
|
|
2622
2728
|
}
|
|
@@ -2625,13 +2731,15 @@ function serverEnv(options) {
|
|
|
2625
2731
|
MB_DB_FILE: options.dbFile,
|
|
2626
2732
|
MB_PLUGINS_DIR: options.pluginsDir,
|
|
2627
2733
|
MB_SITE_URL: instanceUrl(options.port),
|
|
2628
|
-
|
|
2629
|
-
MB_STORE_USE_STAGING: "true",
|
|
2734
|
+
...developmentMode() ? STAGING_STORE_ENV : {},
|
|
2630
2735
|
MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all",
|
|
2631
2736
|
MB_LOAD_SAMPLE_CONTENT: "false",
|
|
2632
2737
|
MB_INSTALL_ANALYTICS_DATABASE: "false"
|
|
2633
2738
|
};
|
|
2634
2739
|
}
|
|
2740
|
+
function envDrift(actual, expected) {
|
|
2741
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(expected), ...Object.keys(STAGING_STORE_ENV)])].filter((name) => actual[name] !== expected[name]);
|
|
2742
|
+
}
|
|
2635
2743
|
function jettyEnv(port) {
|
|
2636
2744
|
return {
|
|
2637
2745
|
MB_JETTY_HOST: LOOPBACK_HOST,
|
|
@@ -2640,7 +2748,34 @@ function jettyEnv(port) {
|
|
|
2640
2748
|
}
|
|
2641
2749
|
//#endregion
|
|
2642
2750
|
//#region src/runtime/docker.ts
|
|
2643
|
-
const
|
|
2751
|
+
const RELEASE_REPOSITORY = "metabase/metabase-enterprise";
|
|
2752
|
+
const DEVELOPMENT_IMAGE = "metabase/metabase-enterprise-head:latest";
|
|
2753
|
+
const HEAD_SUFFIX = "-head";
|
|
2754
|
+
const METABASE_REPOSITORIES = /* @__PURE__ */ new Set([
|
|
2755
|
+
RELEASE_REPOSITORY,
|
|
2756
|
+
`${RELEASE_REPOSITORY}${HEAD_SUFFIX}`,
|
|
2757
|
+
"metabase/metabase"
|
|
2758
|
+
]);
|
|
2759
|
+
function majorLineImage(major) {
|
|
2760
|
+
return `${RELEASE_REPOSITORY}:${majorLineTag(major)}`;
|
|
2761
|
+
}
|
|
2762
|
+
function imageTag(ref) {
|
|
2763
|
+
const colon = ref.lastIndexOf(":");
|
|
2764
|
+
if (colon === -1 || colon < ref.lastIndexOf("/")) return null;
|
|
2765
|
+
return ref.slice(colon + 1);
|
|
2766
|
+
}
|
|
2767
|
+
function imageRepository(ref) {
|
|
2768
|
+
const digest = ref.indexOf("@");
|
|
2769
|
+
const named = digest === -1 ? ref : ref.slice(0, digest);
|
|
2770
|
+
const tag = imageTag(named);
|
|
2771
|
+
return tag === null ? named : named.slice(0, named.length - tag.length - 1);
|
|
2772
|
+
}
|
|
2773
|
+
function isMasterBuildImage(ref) {
|
|
2774
|
+
return imageRepository(ref).endsWith(HEAD_SUFFIX);
|
|
2775
|
+
}
|
|
2776
|
+
function isMetabaseImage(ref) {
|
|
2777
|
+
return METABASE_REPOSITORIES.has(imageRepository(ref));
|
|
2778
|
+
}
|
|
2644
2779
|
const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
|
|
2645
2780
|
const CONTAINER_PORT = 3e3;
|
|
2646
2781
|
const STOP = { graceMs: METABASE_STOP_GRACE_MS };
|
|
@@ -2655,6 +2790,16 @@ function onLoopback(mappings, port) {
|
|
|
2655
2790
|
function hostUserFor(platform, hostUser) {
|
|
2656
2791
|
return platform === "linux" ? hostUser : null;
|
|
2657
2792
|
}
|
|
2793
|
+
function containerServerEnv(port) {
|
|
2794
|
+
return serverEnv({
|
|
2795
|
+
dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
|
|
2796
|
+
pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
|
|
2797
|
+
port
|
|
2798
|
+
});
|
|
2799
|
+
}
|
|
2800
|
+
function containerEnvDrift(container, state) {
|
|
2801
|
+
return envDrift(container.env, containerServerEnv(state.port));
|
|
2802
|
+
}
|
|
2658
2803
|
function dockerRuntime(state, deps) {
|
|
2659
2804
|
const { docker } = deps;
|
|
2660
2805
|
const url = instanceUrl(state.port);
|
|
@@ -2681,11 +2826,7 @@ function dockerRuntime(state, deps) {
|
|
|
2681
2826
|
source: deps.dataDir,
|
|
2682
2827
|
target: CONTAINER_DATA_DIR
|
|
2683
2828
|
},
|
|
2684
|
-
env:
|
|
2685
|
-
dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
|
|
2686
|
-
pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
|
|
2687
|
-
port: state.port
|
|
2688
|
-
}),
|
|
2829
|
+
env: containerServerEnv(state.port),
|
|
2689
2830
|
addHostGateway: deps.platform === "linux",
|
|
2690
2831
|
hostUser: hostUserFor(deps.platform, deps.hostUser)
|
|
2691
2832
|
};
|
|
@@ -2791,6 +2932,7 @@ function parseJarSpec(value) {
|
|
|
2791
2932
|
path: resolve(trimmed)
|
|
2792
2933
|
};
|
|
2793
2934
|
const tagged = trimmed.startsWith(TAG_PREFIX) ? trimmed : `${TAG_PREFIX}${trimmed}`;
|
|
2935
|
+
if (isOpenSourceRelease(tagged)) throw new ConfigError(`${trimmed} is a release of the open-source edition, which rde does not support; enterprise release tags start with v1`);
|
|
2794
2936
|
return isReleaseTag(tagged) ? {
|
|
2795
2937
|
kind: "release",
|
|
2796
2938
|
version: tagged
|
|
@@ -3277,7 +3419,7 @@ function adminUserStep(options, deps = hostDeps$14) {
|
|
|
3277
3419
|
//#region src/tools/mb.ts
|
|
3278
3420
|
const MB = "mb";
|
|
3279
3421
|
const MB_PACKAGE = "@metabase/cli";
|
|
3280
|
-
const MB_DIST_TAG = "
|
|
3422
|
+
const MB_DIST_TAG = "latest";
|
|
3281
3423
|
const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
|
|
3282
3424
|
const GLOBAL_INSTALL = {
|
|
3283
3425
|
npm: {
|
|
@@ -3408,15 +3550,11 @@ function mbRemoveCommand(binary) {
|
|
|
3408
3550
|
const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
|
|
3409
3551
|
return name === null ? null : REMOVE_COMMANDS[owner](name);
|
|
3410
3552
|
}
|
|
3411
|
-
function setValue(name) {
|
|
3412
|
-
const value = readEnv(name);
|
|
3413
|
-
return value === void 0 || value.length === 0 ? null : value;
|
|
3414
|
-
}
|
|
3415
3553
|
function mbEnvironment() {
|
|
3416
3554
|
return {
|
|
3417
|
-
profile:
|
|
3418
|
-
url:
|
|
3419
|
-
apiKeySet:
|
|
3555
|
+
profile: readSetEnv(ENV_MB_PROFILE),
|
|
3556
|
+
url: readSetEnv(ENV_MB_URL),
|
|
3557
|
+
apiKeySet: readSetEnv(ENV_MB_API_KEY) !== null
|
|
3420
3558
|
};
|
|
3421
3559
|
}
|
|
3422
3560
|
const ProbedUser = z.object({
|
|
@@ -5598,6 +5736,14 @@ function gitTool(runner) {
|
|
|
5598
5736
|
"HEAD"
|
|
5599
5737
|
], { cwd: dir })).exitCode === 0;
|
|
5600
5738
|
}
|
|
5739
|
+
async function gitPath(dir, name) {
|
|
5740
|
+
const result = await git([
|
|
5741
|
+
"rev-parse",
|
|
5742
|
+
"--git-path",
|
|
5743
|
+
name
|
|
5744
|
+
], { cwd: dir });
|
|
5745
|
+
return resolve(dir, result.stdout.trim());
|
|
5746
|
+
}
|
|
5601
5747
|
async function hasIdentity(dir) {
|
|
5602
5748
|
const result = await runner.run(GIT, [
|
|
5603
5749
|
"config",
|
|
@@ -5715,6 +5861,7 @@ function gitTool(runner) {
|
|
|
5715
5861
|
initBare,
|
|
5716
5862
|
init,
|
|
5717
5863
|
hasCommits,
|
|
5864
|
+
gitPath,
|
|
5718
5865
|
commitPaths,
|
|
5719
5866
|
addRemote,
|
|
5720
5867
|
clone,
|
|
@@ -5728,14 +5875,201 @@ function gitTool(runner) {
|
|
|
5728
5875
|
};
|
|
5729
5876
|
}
|
|
5730
5877
|
//#endregion
|
|
5878
|
+
//#region src/steps/license.ts
|
|
5879
|
+
const STEP_ID$7 = "license";
|
|
5880
|
+
const LICENSE_KEYS = { token: `${STEP_ID$7}.token` };
|
|
5881
|
+
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
5882
|
+
const NOT_FOUND_STATUS$1 = 404;
|
|
5883
|
+
const TOKEN_REFUSED_STATUS = 400;
|
|
5884
|
+
const STORE_UNREACHABLE_STATUS = 503;
|
|
5885
|
+
const TOKEN_CHECK_HOST = "token-check.metabase.com";
|
|
5886
|
+
const ACTIVATION_FAILED = "License activation failed";
|
|
5887
|
+
const LICENSED_FEATURES = {
|
|
5888
|
+
[REMOTE_SYNC_FEATURE]: "remote sync",
|
|
5889
|
+
"data-apps": "data apps",
|
|
5890
|
+
"transforms-testing": "transform tests"
|
|
5891
|
+
};
|
|
5892
|
+
const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
|
|
5893
|
+
function licensedFeatureCoverage(features) {
|
|
5894
|
+
const entries = Object.entries(LICENSED_FEATURES);
|
|
5895
|
+
return {
|
|
5896
|
+
included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
|
|
5897
|
+
missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
|
|
5898
|
+
};
|
|
5899
|
+
}
|
|
5900
|
+
function listOf(names, type) {
|
|
5901
|
+
return new Intl.ListFormat("en", { type }).format(names);
|
|
5902
|
+
}
|
|
5903
|
+
function hostDeps$12(ctx) {
|
|
5904
|
+
return {
|
|
5905
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5906
|
+
runner: processRunner(ctx.signal),
|
|
5907
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
5908
|
+
terminal: stdinIsTty()
|
|
5909
|
+
};
|
|
5910
|
+
}
|
|
5911
|
+
function isNoTokenYet(error) {
|
|
5912
|
+
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
5913
|
+
return isMetabaseNotServing(error);
|
|
5914
|
+
}
|
|
5915
|
+
function validateToken(value) {
|
|
5916
|
+
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
5917
|
+
return "A license token is 64 hexadecimal characters";
|
|
5918
|
+
}
|
|
5919
|
+
function requireApiKey$1(ctx) {
|
|
5920
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
5921
|
+
return ctx.secrets.apiKey;
|
|
5922
|
+
}
|
|
5923
|
+
function licenseOf(status) {
|
|
5924
|
+
if (!status.valid) return { activated: false };
|
|
5925
|
+
return {
|
|
5926
|
+
activated: true,
|
|
5927
|
+
features: status.features,
|
|
5928
|
+
validThru: status["valid-thru"] ?? null
|
|
5929
|
+
};
|
|
5930
|
+
}
|
|
5931
|
+
async function liveLicense(client) {
|
|
5932
|
+
try {
|
|
5933
|
+
return licenseOf(await client.tokenStatus.get());
|
|
5934
|
+
} catch (error) {
|
|
5935
|
+
if (error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1) return { activated: false };
|
|
5936
|
+
throw error;
|
|
5937
|
+
}
|
|
5938
|
+
}
|
|
5939
|
+
async function recordLiveLicense(ctx, client) {
|
|
5940
|
+
const live = await liveLicense(client);
|
|
5941
|
+
if (!isDeepStrictEqual(live, ctx.state.license)) await ctx.saveState({
|
|
5942
|
+
...ctx.state,
|
|
5943
|
+
license: live
|
|
5944
|
+
});
|
|
5945
|
+
return live;
|
|
5946
|
+
}
|
|
5947
|
+
function activeLabel(status) {
|
|
5948
|
+
const plan = status["plan-alias"];
|
|
5949
|
+
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
5950
|
+
}
|
|
5951
|
+
function refusalOf(error) {
|
|
5952
|
+
if (!(error instanceof MetabaseApiError)) return null;
|
|
5953
|
+
if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
|
|
5954
|
+
if (error.status === STORE_UNREACHABLE_STATUS) return new ConfigError(`The instance could not reach ${TOKEN_CHECK_HOST} to check the license token; it needs outbound network access`);
|
|
5955
|
+
return null;
|
|
5956
|
+
}
|
|
5957
|
+
async function activate(client, token) {
|
|
5958
|
+
try {
|
|
5959
|
+
await client.setting.setPremiumEmbeddingToken(token);
|
|
5960
|
+
} catch (error) {
|
|
5961
|
+
const refusal = refusalOf(error);
|
|
5962
|
+
if (refusal === null) throw error;
|
|
5963
|
+
return {
|
|
5964
|
+
kind: "refused",
|
|
5965
|
+
error: refusal
|
|
5966
|
+
};
|
|
5967
|
+
}
|
|
5968
|
+
const status = await client.tokenStatus.get();
|
|
5969
|
+
if (!status.valid) return {
|
|
5970
|
+
kind: "refused",
|
|
5971
|
+
error: new ConfigError(`Metabase reports the token as ${status.status}`)
|
|
5972
|
+
};
|
|
5973
|
+
return {
|
|
5974
|
+
kind: "activated",
|
|
5975
|
+
status
|
|
5976
|
+
};
|
|
5977
|
+
}
|
|
5978
|
+
async function activateAskedToken(ctx, client, terminal) {
|
|
5979
|
+
const token = await ctx.prompts.password({
|
|
5980
|
+
key: LICENSE_KEYS.token,
|
|
5981
|
+
message: "Paste your Metabase license token (Enter to skip)",
|
|
5982
|
+
validate: validateToken
|
|
5983
|
+
});
|
|
5984
|
+
if (token.length === 0) return null;
|
|
5985
|
+
const spinner = ctx.ui.spinner("Activating the license…");
|
|
5986
|
+
let activation;
|
|
5987
|
+
try {
|
|
5988
|
+
activation = await activate(client, token);
|
|
5989
|
+
} catch (error) {
|
|
5990
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
5991
|
+
throw error;
|
|
5992
|
+
}
|
|
5993
|
+
if (activation.kind === "activated") {
|
|
5994
|
+
spinner.stop(activeLabel(activation.status));
|
|
5995
|
+
ctx.session.licenseToken.set(token);
|
|
5996
|
+
return activation.status;
|
|
5997
|
+
}
|
|
5998
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
5999
|
+
if (!terminal) throw activation.error;
|
|
6000
|
+
ctx.ui.warn(activation.error.message);
|
|
6001
|
+
return activateAskedToken(ctx, client, terminal);
|
|
6002
|
+
}
|
|
6003
|
+
function reportFeatures(ctx, features) {
|
|
6004
|
+
const { missing } = licensedFeatureCoverage(features);
|
|
6005
|
+
const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
|
|
6006
|
+
if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
|
|
6007
|
+
if (missing.length > 0) {
|
|
6008
|
+
const gitNote = remoteSync ? "" : "; the git step will be skipped";
|
|
6009
|
+
ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
|
|
6010
|
+
}
|
|
6011
|
+
}
|
|
6012
|
+
function licenseStep(deps = hostDeps$12) {
|
|
6013
|
+
return {
|
|
6014
|
+
id: STEP_ID$7,
|
|
6015
|
+
title: "Activate your license",
|
|
6016
|
+
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
6017
|
+
async detect(ctx) {
|
|
6018
|
+
if (ctx.state.steps[STEP_ID$7]?.status === "skipped") return "not-applicable";
|
|
6019
|
+
const configured = configuredRuntime(ctx.state);
|
|
6020
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
6021
|
+
const resolved = deps(ctx);
|
|
6022
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
6023
|
+
const client = resolved.client(configured.url, {
|
|
6024
|
+
kind: "api-key",
|
|
6025
|
+
key: ctx.secrets.apiKey
|
|
6026
|
+
});
|
|
6027
|
+
try {
|
|
6028
|
+
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
6029
|
+
} catch (error) {
|
|
6030
|
+
if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
|
|
6031
|
+
throw error;
|
|
6032
|
+
}
|
|
6033
|
+
},
|
|
6034
|
+
async run(ctx) {
|
|
6035
|
+
const configured = requireConfiguredRuntime(ctx.state);
|
|
6036
|
+
const apiKey = requireApiKey$1(ctx);
|
|
6037
|
+
const resolved = deps(ctx);
|
|
6038
|
+
const client = resolved.client(configured.url, {
|
|
6039
|
+
kind: "api-key",
|
|
6040
|
+
key: apiKey
|
|
6041
|
+
});
|
|
6042
|
+
const status = await activateAskedToken(ctx, client, resolved.terminal);
|
|
6043
|
+
if (status === null) {
|
|
6044
|
+
await recordLiveLicense(ctx, client);
|
|
6045
|
+
const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
|
|
6046
|
+
ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
|
|
6047
|
+
return { outcome: "skipped" };
|
|
6048
|
+
}
|
|
6049
|
+
await ctx.saveState({
|
|
6050
|
+
...ctx.state,
|
|
6051
|
+
license: licenseOf(status)
|
|
6052
|
+
});
|
|
6053
|
+
const mb = mbTool(resolved.runner);
|
|
6054
|
+
if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
|
|
6055
|
+
url: configured.url,
|
|
6056
|
+
profile: ctx.state.mbProfile,
|
|
6057
|
+
apiKey
|
|
6058
|
+
});
|
|
6059
|
+
reportFeatures(ctx, status.features);
|
|
6060
|
+
return { outcome: "completed" };
|
|
6061
|
+
}
|
|
6062
|
+
};
|
|
6063
|
+
}
|
|
6064
|
+
//#endregion
|
|
5731
6065
|
//#region src/steps/git-sync.ts
|
|
5732
|
-
const STEP_ID$
|
|
6066
|
+
const STEP_ID$6 = "git-sync";
|
|
5733
6067
|
const GIT_SYNC_KEYS = {
|
|
5734
|
-
location: `${STEP_ID$
|
|
5735
|
-
path: `${STEP_ID$
|
|
5736
|
-
remote: `${STEP_ID$
|
|
5737
|
-
url: `${STEP_ID$
|
|
5738
|
-
token: `${STEP_ID$
|
|
6068
|
+
location: `${STEP_ID$6}.location`,
|
|
6069
|
+
path: `${STEP_ID$6}.path`,
|
|
6070
|
+
remote: `${STEP_ID$6}.remote`,
|
|
6071
|
+
url: `${STEP_ID$6}.url`,
|
|
6072
|
+
token: `${STEP_ID$6}.token`
|
|
5739
6073
|
};
|
|
5740
6074
|
const BARE_REPO_NAME = "content.git";
|
|
5741
6075
|
const DEFAULT_BRANCH = "main";
|
|
@@ -5745,6 +6079,7 @@ const README_NAME = "README.md";
|
|
|
5745
6079
|
const README_CONTENT = "# Metabase content\n\nMetabase content synced by rde.\n";
|
|
5746
6080
|
const INITIAL_COMMIT_MESSAGE = "Initial commit";
|
|
5747
6081
|
const GITIGNORE_NAME = ".gitignore";
|
|
6082
|
+
const EXCLUDE_GIT_PATH = "info/exclude";
|
|
5748
6083
|
const IGNORED_PATHS = [
|
|
5749
6084
|
".env",
|
|
5750
6085
|
".env.local",
|
|
@@ -5765,11 +6100,8 @@ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
|
5765
6100
|
"timed-out",
|
|
5766
6101
|
"conflict"
|
|
5767
6102
|
]);
|
|
5768
|
-
const SYNC_MODE_SETTINGS = {
|
|
5769
|
-
|
|
5770
|
-
"remote-sync-transforms": true
|
|
5771
|
-
};
|
|
5772
|
-
function hostDeps$12(ctx) {
|
|
6103
|
+
const SYNC_MODE_SETTINGS = { "remote-sync-type": "read-write" };
|
|
6104
|
+
function hostDeps$11(ctx) {
|
|
5773
6105
|
return {
|
|
5774
6106
|
runner: processRunner(ctx.signal),
|
|
5775
6107
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -6007,20 +6339,21 @@ function hostedScreen(run) {
|
|
|
6007
6339
|
}
|
|
6008
6340
|
};
|
|
6009
6341
|
}
|
|
6010
|
-
async function ignoreDataAppFiles(
|
|
6011
|
-
const path = join(workdir, GITIGNORE_NAME);
|
|
6342
|
+
async function ignoreDataAppFiles(path) {
|
|
6012
6343
|
const existing = await readTextFile(path);
|
|
6013
6344
|
const present = new Set(existing === null ? [] : existing.split("\n").map((line) => line.trim()));
|
|
6014
6345
|
const missing = IGNORED_PATHS.filter((entry) => !present.has(entry));
|
|
6015
6346
|
if (missing.length === 0) return;
|
|
6016
6347
|
const separator = existing === null || existing.endsWith("\n") ? "" : "\n";
|
|
6348
|
+
await promises.mkdir(dirname(path), { recursive: true });
|
|
6017
6349
|
await promises.appendFile(path, `${separator}${missing.join("\n")}\n`);
|
|
6018
6350
|
}
|
|
6019
6351
|
async function prepareWorkdir(run, workdir) {
|
|
6020
6352
|
if (!await run.git.isRepo(workdir)) await run.git.init(workdir, { branch: DEFAULT_BRANCH });
|
|
6021
|
-
if (
|
|
6353
|
+
if (await run.git.hasCommits(workdir)) await ignoreDataAppFiles(await run.git.gitPath(workdir, EXCLUDE_GIT_PATH));
|
|
6354
|
+
else {
|
|
6022
6355
|
await promises.writeFile(join(workdir, README_NAME), README_CONTENT);
|
|
6023
|
-
await ignoreDataAppFiles(workdir);
|
|
6356
|
+
await ignoreDataAppFiles(join(workdir, GITIGNORE_NAME));
|
|
6024
6357
|
await run.git.commitPaths(workdir, [README_NAME, GITIGNORE_NAME], INITIAL_COMMIT_MESSAGE);
|
|
6025
6358
|
}
|
|
6026
6359
|
return {
|
|
@@ -6074,12 +6407,9 @@ async function cloneHosted(run, workdir, credentials) {
|
|
|
6074
6407
|
throw error;
|
|
6075
6408
|
}
|
|
6076
6409
|
spinner.stop(`Cloned into ${workdir}`);
|
|
6077
|
-
|
|
6078
|
-
workdir,
|
|
6079
|
-
branch: await run.git.currentBranch(workdir)
|
|
6080
|
-
};
|
|
6410
|
+
const hadCommits = await run.git.hasCommits(workdir);
|
|
6081
6411
|
const prepared = await prepareWorkdir(run, workdir);
|
|
6082
|
-
await run.git.push(workdir, ORIGIN, prepared.branch, credentials);
|
|
6412
|
+
if (!hadCommits) await run.git.push(workdir, ORIGIN, prepared.branch, credentials);
|
|
6083
6413
|
return prepared;
|
|
6084
6414
|
}
|
|
6085
6415
|
async function adoptRepository(run, workdir, url) {
|
|
@@ -6158,7 +6488,8 @@ function unfinishedImport(outcome) {
|
|
|
6158
6488
|
}
|
|
6159
6489
|
async function importExistingContent(run, gitRemote) {
|
|
6160
6490
|
if (!await remoteHasContent(run, gitRemote)) {
|
|
6161
|
-
run.
|
|
6491
|
+
await run.client.remoteSync.syncTransforms();
|
|
6492
|
+
run.ctx.ui.info(`Nothing to import from the repository; \`${exportCommand(run.ctx.state.mbProfile, gitRemote.branch)}\` publishes this Metabase's content to it`);
|
|
6162
6493
|
return;
|
|
6163
6494
|
}
|
|
6164
6495
|
const spinner = run.ctx.ui.spinner("Importing the repository's content into Metabase…");
|
|
@@ -6206,6 +6537,9 @@ function configureRemote(run, runtime, model) {
|
|
|
6206
6537
|
function isMbUnavailable(error) {
|
|
6207
6538
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
6208
6539
|
}
|
|
6540
|
+
function remoteSyncGap(license) {
|
|
6541
|
+
return license.activated ? "Remote sync needs a license with the remote-sync feature, and this instance's license lacks it" : "Remote sync needs a license, and this instance has no valid one";
|
|
6542
|
+
}
|
|
6209
6543
|
async function runGitSync(ctx, deps) {
|
|
6210
6544
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6211
6545
|
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
@@ -6220,6 +6554,8 @@ async function runGitSync(ctx, deps) {
|
|
|
6220
6554
|
}),
|
|
6221
6555
|
instance
|
|
6222
6556
|
};
|
|
6557
|
+
const license = await recordLiveLicense(ctx, run.client);
|
|
6558
|
+
if (!hasRemoteSync(license)) throw new ConfigError(`${remoteSyncGap(license)}; \`rde init --from license\` activates one`);
|
|
6223
6559
|
const flow = await runFlow({
|
|
6224
6560
|
remote: remoteScreen(run),
|
|
6225
6561
|
hosted: hostedScreen(run),
|
|
@@ -6239,15 +6575,15 @@ async function runGitSync(ctx, deps) {
|
|
|
6239
6575
|
ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
6240
6576
|
return "completed";
|
|
6241
6577
|
}
|
|
6242
|
-
function gitSyncStep(deps = hostDeps$
|
|
6578
|
+
function gitSyncStep(deps = hostDeps$11) {
|
|
6243
6579
|
return {
|
|
6244
|
-
id: STEP_ID$
|
|
6580
|
+
id: STEP_ID$6,
|
|
6245
6581
|
title: "Connect a git repository",
|
|
6246
6582
|
redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
|
|
6247
6583
|
async detect(ctx) {
|
|
6248
6584
|
if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
|
|
6249
|
-
if (ctx.state.steps[STEP_ID$
|
|
6250
|
-
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$
|
|
6585
|
+
if (ctx.state.steps[STEP_ID$6]?.status === "skipped") return "not-applicable";
|
|
6586
|
+
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$6]?.status !== "done") return "todo";
|
|
6251
6587
|
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6252
6588
|
if (instance === null) return "todo";
|
|
6253
6589
|
try {
|
|
@@ -6291,21 +6627,20 @@ function githubContents(signal, options = {}) {
|
|
|
6291
6627
|
//#region src/tools/skills-cli.ts
|
|
6292
6628
|
const NPX = "npx";
|
|
6293
6629
|
const SKILLS_PACKAGE = "skills@latest";
|
|
6630
|
+
const LIST_INTERNAL_SKILLS = { INSTALL_INTERNAL_SKILLS: "1" };
|
|
6294
6631
|
const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
|
|
6295
|
-
const
|
|
6296
|
-
const
|
|
6297
|
-
const
|
|
6298
|
-
const
|
|
6299
|
-
const
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
];
|
|
6632
|
+
const RDE_SKILL_NAME = "metabase-rde";
|
|
6633
|
+
const DATA_APPS_SKILL_NAME = "metabase-data-apps";
|
|
6634
|
+
const RDE_INSTALLED_SKILL_NAMES = [DATA_APPS_SKILL_NAME, RDE_SKILL_NAME];
|
|
6635
|
+
const REPLACED_RDE_SKILL_NAMES = ["rde", "rde-data-apps"];
|
|
6636
|
+
const RDE_OWNED_SKILL_NAMES = [...RDE_INSTALLED_SKILL_NAMES, ...REPLACED_RDE_SKILL_NAMES];
|
|
6637
|
+
const SKILL_VERSIONS = {
|
|
6638
|
+
[DATA_APPS_SKILL_NAME]: 1,
|
|
6639
|
+
[RDE_SKILL_NAME]: 1
|
|
6640
|
+
};
|
|
6641
|
+
const SOURCE_SKILLS_DIR = "skills";
|
|
6306
6642
|
function rdeSkillSource() {
|
|
6307
|
-
|
|
6308
|
-
return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
|
|
6643
|
+
return readSetEnv("RDE_SKILL_SOURCE") ?? "https://github.com/metabase/agent-skills/tree/main";
|
|
6309
6644
|
}
|
|
6310
6645
|
const InstalledSkill = z.object({
|
|
6311
6646
|
name: z.string(),
|
|
@@ -6331,8 +6666,7 @@ function projectSkillLockPath(project) {
|
|
|
6331
6666
|
return join(project, PROJECT_LOCK_FILE);
|
|
6332
6667
|
}
|
|
6333
6668
|
function xdgStateHome() {
|
|
6334
|
-
|
|
6335
|
-
return value === void 0 || value.length === 0 ? null : value;
|
|
6669
|
+
return readSetEnv(ENV_XDG_STATE_HOME);
|
|
6336
6670
|
}
|
|
6337
6671
|
function lockedSkill(path, raw, skill) {
|
|
6338
6672
|
if (raw === null) return null;
|
|
@@ -6344,11 +6678,6 @@ const hostSkillFiles = {
|
|
|
6344
6678
|
readJson: readJsonFile,
|
|
6345
6679
|
readText: readTextFile
|
|
6346
6680
|
};
|
|
6347
|
-
const DATA_APP_SKILLS_ORIGIN = {
|
|
6348
|
-
kind: "github",
|
|
6349
|
-
repository: DATA_APP_REPOSITORY,
|
|
6350
|
-
ref: DATA_APP_BRANCH
|
|
6351
|
-
};
|
|
6352
6681
|
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
6353
6682
|
function skillOrigin(source, cwd) {
|
|
6354
6683
|
const github = GITHUB_URL.exec(source);
|
|
@@ -6369,6 +6698,23 @@ function skillOrigin(source, cwd) {
|
|
|
6369
6698
|
source
|
|
6370
6699
|
};
|
|
6371
6700
|
}
|
|
6701
|
+
function sourceSkillDirectory(skill) {
|
|
6702
|
+
return posix.join(SOURCE_SKILLS_DIR, skill, `v${SKILL_VERSIONS[skill]}`);
|
|
6703
|
+
}
|
|
6704
|
+
function skillSource(rdeSource, skill, cwd) {
|
|
6705
|
+
const directory = sourceSkillDirectory(skill);
|
|
6706
|
+
const origin = skillOrigin(rdeSource, cwd);
|
|
6707
|
+
switch (origin.kind) {
|
|
6708
|
+
case "github":
|
|
6709
|
+
if (origin.ref === null) throw new ConfigError(`RDE_SKILL_SOURCE must name a branch: https://github.com/${origin.repository}/tree/<branch>`);
|
|
6710
|
+
return `https://github.com/${origin.repository}/tree/${origin.ref}/${directory}`;
|
|
6711
|
+
case "local": return join(origin.path, directory);
|
|
6712
|
+
case "other": {
|
|
6713
|
+
const [path, ref] = origin.source.split("#", 2);
|
|
6714
|
+
return ref === void 0 ? `${path}/${directory}` : `${path}/${directory}#${ref}`;
|
|
6715
|
+
}
|
|
6716
|
+
}
|
|
6717
|
+
}
|
|
6372
6718
|
function skillsCli(runner) {
|
|
6373
6719
|
async function add(options) {
|
|
6374
6720
|
const argv = [
|
|
@@ -6402,7 +6748,10 @@ function skillsCli(runner) {
|
|
|
6402
6748
|
"-g",
|
|
6403
6749
|
"--json"
|
|
6404
6750
|
];
|
|
6405
|
-
const result = await runner.run(NPX, argv, {
|
|
6751
|
+
const result = await runner.run(NPX, argv, {
|
|
6752
|
+
stdoutToFile: true,
|
|
6753
|
+
addedEnv: LIST_INTERNAL_SKILLS
|
|
6754
|
+
});
|
|
6406
6755
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
6407
6756
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
6408
6757
|
}
|
|
@@ -6415,7 +6764,8 @@ function skillsCli(runner) {
|
|
|
6415
6764
|
];
|
|
6416
6765
|
const result = await runner.run(NPX, argv, {
|
|
6417
6766
|
cwd,
|
|
6418
|
-
stdoutToFile: true
|
|
6767
|
+
stdoutToFile: true,
|
|
6768
|
+
addedEnv: LIST_INTERNAL_SKILLS
|
|
6419
6769
|
});
|
|
6420
6770
|
if (result.exitCode !== 0) throw toolFailure(NPX, argv, result);
|
|
6421
6771
|
return parseJsonOutput(NPX, argv, result.stdout, InstalledSkillList);
|
|
@@ -6429,12 +6779,29 @@ function skillsCli(runner) {
|
|
|
6429
6779
|
}
|
|
6430
6780
|
//#endregion
|
|
6431
6781
|
//#region src/steps/agents-skills.ts
|
|
6432
|
-
const STEP_ID$
|
|
6433
|
-
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$
|
|
6782
|
+
const STEP_ID$5 = "agents-skills";
|
|
6783
|
+
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
|
|
6434
6784
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6435
|
-
const REQUIRED_SKILL_NAMES =
|
|
6785
|
+
const REQUIRED_SKILL_NAMES = RDE_INSTALLED_SKILL_NAMES;
|
|
6786
|
+
const LEGACY_DATA_APP_REPOSITORY = "metabase/metabase";
|
|
6787
|
+
const LEGACY_DATA_APP_SKILL_NAMES = [
|
|
6788
|
+
"metabase-data-app-setup",
|
|
6789
|
+
"metabase-data-app-routing",
|
|
6790
|
+
"metabase-data-app-actions",
|
|
6791
|
+
"metabase-data-app-semantic-layer",
|
|
6792
|
+
"metabase-data-app-migrate"
|
|
6793
|
+
];
|
|
6794
|
+
const REPLACED_RDE_NAMES = REPLACED_RDE_SKILL_NAMES;
|
|
6795
|
+
const REPLACEMENTS = [{
|
|
6796
|
+
by: DATA_APPS_SKILL_NAME,
|
|
6797
|
+
names: LEGACY_DATA_APP_SKILL_NAMES
|
|
6798
|
+
}, {
|
|
6799
|
+
by: RDE_SKILL_NAME,
|
|
6800
|
+
names: REPLACED_RDE_NAMES
|
|
6801
|
+
}];
|
|
6436
6802
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6437
|
-
|
|
6803
|
+
const ASK_AGENTS = { kind: "ask" };
|
|
6804
|
+
function hostDeps$10(ctx) {
|
|
6438
6805
|
return {
|
|
6439
6806
|
runner: processRunner(ctx.signal),
|
|
6440
6807
|
probe: hostAgentProbe(),
|
|
@@ -6458,9 +6825,6 @@ function skillCoversAgent(entry, agent) {
|
|
|
6458
6825
|
function coversAny(entry, agents) {
|
|
6459
6826
|
return agents.some((agent) => skillCoversAgent(entry, agent));
|
|
6460
6827
|
}
|
|
6461
|
-
async function rdeSkillEntries(runner) {
|
|
6462
|
-
return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
|
|
6463
|
-
}
|
|
6464
6828
|
function requiredEntries(installed) {
|
|
6465
6829
|
return installed.filter((entry) => REQUIRED_SKILL_NAMES.includes(entry.name));
|
|
6466
6830
|
}
|
|
@@ -6477,20 +6841,17 @@ function coveredAgentNames(entry, agents) {
|
|
|
6477
6841
|
function installedLines(entries, agents) {
|
|
6478
6842
|
return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
|
|
6479
6843
|
}
|
|
6480
|
-
function
|
|
6481
|
-
|
|
6482
|
-
|
|
6483
|
-
source
|
|
6484
|
-
|
|
6485
|
-
|
|
6486
|
-
|
|
6487
|
-
|
|
6488
|
-
|
|
6489
|
-
|
|
6490
|
-
|
|
6491
|
-
origin: skillOrigin(deps.skillSource, deps.cwd),
|
|
6492
|
-
skills: RDE_SOURCE_SKILL_NAMES
|
|
6493
|
-
}];
|
|
6844
|
+
function skillInstalls(deps) {
|
|
6845
|
+
const defaultSource = deps.skillSource === DEFAULT_RDE_SKILL_SOURCE;
|
|
6846
|
+
return RDE_INSTALLED_SKILL_NAMES.map((skill) => {
|
|
6847
|
+
const source = skillSource(deps.skillSource, skill, deps.cwd);
|
|
6848
|
+
return {
|
|
6849
|
+
skill,
|
|
6850
|
+
source,
|
|
6851
|
+
sourceNote: ` from ${defaultSource ? sourceSkillDirectory(skill) : source}`,
|
|
6852
|
+
origin: skillOrigin(source, deps.cwd)
|
|
6853
|
+
};
|
|
6854
|
+
});
|
|
6494
6855
|
}
|
|
6495
6856
|
function hashLookup(github) {
|
|
6496
6857
|
const asked = /* @__PURE__ */ new Map();
|
|
@@ -6515,7 +6876,8 @@ async function readLockedSkills(deps) {
|
|
|
6515
6876
|
const lockPath = globalSkillLockPath(deps.probe.home, deps.stateHome);
|
|
6516
6877
|
try {
|
|
6517
6878
|
const lock = await deps.files.readJson(lockPath);
|
|
6518
|
-
|
|
6879
|
+
const names = [...REQUIRED_SKILL_NAMES, ...LEGACY_DATA_APP_SKILL_NAMES];
|
|
6880
|
+
return new Map(names.map((name) => [name, lockedSkill(lockPath, lock, name)]));
|
|
6519
6881
|
} catch (error) {
|
|
6520
6882
|
if (error instanceof ConfigError) return null;
|
|
6521
6883
|
throw error;
|
|
@@ -6535,25 +6897,54 @@ function lockedCopy(locked, origin) {
|
|
|
6535
6897
|
hash: locked.skillFolderHash
|
|
6536
6898
|
};
|
|
6537
6899
|
}
|
|
6538
|
-
async function
|
|
6539
|
-
const { origin } =
|
|
6900
|
+
async function installBehind(install, check) {
|
|
6901
|
+
const { origin } = install;
|
|
6540
6902
|
if (origin.kind !== "github" || check.locked === null) return true;
|
|
6541
|
-
const
|
|
6542
|
-
|
|
6543
|
-
const copy = lockedCopy(check.locked.get(skill) ?? null, origin);
|
|
6544
|
-
if (copy === null) return true;
|
|
6545
|
-
copies.push(copy);
|
|
6546
|
-
}
|
|
6903
|
+
const copy = lockedCopy(check.locked.get(install.skill) ?? null, origin);
|
|
6904
|
+
if (copy === null) return true;
|
|
6547
6905
|
if (check.updates === "installed") return false;
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6906
|
+
const current = await check.hashes(origin.repository, copy.ref, copy.directory);
|
|
6907
|
+
return current !== null && current.get(copy.folder) !== copy.hash;
|
|
6908
|
+
}
|
|
6909
|
+
function isLegacyDataAppCopy(name, source) {
|
|
6910
|
+
return LEGACY_DATA_APP_SKILL_NAMES.includes(name) && source === LEGACY_DATA_APP_REPOSITORY;
|
|
6911
|
+
}
|
|
6912
|
+
function isReplacedRdeCopy(name) {
|
|
6913
|
+
return REPLACED_RDE_NAMES.includes(name);
|
|
6914
|
+
}
|
|
6915
|
+
function replacedCopies(installed, locked) {
|
|
6916
|
+
const found = /* @__PURE__ */ new Map();
|
|
6917
|
+
for (const { by, names } of REPLACEMENTS) {
|
|
6918
|
+
const present = names.filter((name) => by === "metabase-data-apps" ? isLegacyDataAppCopy(name, locked?.get(name)?.source) : installed.some((entry) => entry.name === name));
|
|
6919
|
+
if (present.length > 0) found.set(by, present);
|
|
6551
6920
|
}
|
|
6552
|
-
return
|
|
6921
|
+
return found;
|
|
6553
6922
|
}
|
|
6554
6923
|
function hasSkill(installed, skill, agent) {
|
|
6555
6924
|
return installed.some((entry) => entry.name === skill && skillCoversAgent(entry, agent));
|
|
6556
6925
|
}
|
|
6926
|
+
function splitReplaced(by, replaced, installed, all) {
|
|
6927
|
+
const split = {
|
|
6928
|
+
removable: [],
|
|
6929
|
+
kept: [],
|
|
6930
|
+
uncovered: []
|
|
6931
|
+
};
|
|
6932
|
+
for (const name of replaced) {
|
|
6933
|
+
const copies = installed.filter((entry) => entry.name === name);
|
|
6934
|
+
const uncovered = all.filter((agent) => copies.some((entry) => skillCoversAgent(entry, agent)) && !hasSkill(installed, by, agent));
|
|
6935
|
+
if (uncovered.length === 0) {
|
|
6936
|
+
split.removable.push(name);
|
|
6937
|
+
continue;
|
|
6938
|
+
}
|
|
6939
|
+
split.kept.push(name);
|
|
6940
|
+
for (const agent of uncovered) if (!split.uncovered.includes(agent)) split.uncovered.push(agent);
|
|
6941
|
+
}
|
|
6942
|
+
return split;
|
|
6943
|
+
}
|
|
6944
|
+
function keptReplacedLine(by, split) {
|
|
6945
|
+
const ids = split.uncovered.map((agent) => agent.id).join(",");
|
|
6946
|
+
return `${by} is not installed for ${displayNames(split.uncovered)}, so rde keeps ${split.kept.join(", ")}; \`rde init --only ${STEP_ID$5} --agents ${ids}\` installs it there and removes what it replaces`;
|
|
6947
|
+
}
|
|
6557
6948
|
function coversAll(installed, skills, agents) {
|
|
6558
6949
|
return agents.every((agent) => skills.every((skill) => hasSkill(installed, skill, agent)));
|
|
6559
6950
|
}
|
|
@@ -6613,13 +7004,13 @@ function skipLine(all, source) {
|
|
|
6613
7004
|
switch (source) {
|
|
6614
7005
|
case "override":
|
|
6615
7006
|
case "chosen":
|
|
6616
|
-
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$
|
|
6617
|
-
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$
|
|
7007
|
+
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$5}\` to install it later`;
|
|
7008
|
+
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$5} --agents <name>\` once one is installed`;
|
|
6618
7009
|
}
|
|
6619
7010
|
}
|
|
6620
|
-
function agentsSkillsStep(options, updates, deps = hostDeps$
|
|
7011
|
+
function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
6621
7012
|
let hashes = null;
|
|
6622
|
-
async function
|
|
7013
|
+
async function installCheck(resolved) {
|
|
6623
7014
|
hashes ??= hashLookup(resolved.github);
|
|
6624
7015
|
return {
|
|
6625
7016
|
updates,
|
|
@@ -6628,16 +7019,18 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6628
7019
|
};
|
|
6629
7020
|
}
|
|
6630
7021
|
return {
|
|
6631
|
-
id: STEP_ID$
|
|
7022
|
+
id: STEP_ID$5,
|
|
6632
7023
|
title: "Install the rde and data-app skills for your coding agents",
|
|
6633
7024
|
redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
|
|
6634
7025
|
async detect(ctx) {
|
|
6635
7026
|
const resolved = deps(ctx);
|
|
6636
7027
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6637
|
-
if (detected.length === 0) return ctx.state.steps[STEP_ID$
|
|
6638
|
-
|
|
6639
|
-
|
|
6640
|
-
|
|
7028
|
+
if (detected.length === 0) return ctx.state.steps[STEP_ID$5]?.status === "skipped" ? "not-applicable" : "todo";
|
|
7029
|
+
const installed = await skillsCli(resolved.runner).list();
|
|
7030
|
+
if (missingSkills(installed, detected).length > 0) return "todo";
|
|
7031
|
+
const check = await installCheck(resolved);
|
|
7032
|
+
if (replacedCopies(installed, check.locked).size > 0) return "outdated";
|
|
7033
|
+
for (const install of skillInstalls(resolved)) if (await installBehind(install, check)) return "outdated";
|
|
6641
7034
|
return "done";
|
|
6642
7035
|
},
|
|
6643
7036
|
async run(ctx) {
|
|
@@ -6652,28 +7045,37 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6652
7045
|
return { outcome: "skipped" };
|
|
6653
7046
|
}
|
|
6654
7047
|
const before = await installedNow();
|
|
6655
|
-
const check = await
|
|
7048
|
+
const check = await installCheck(resolved);
|
|
6656
7049
|
const pending = [];
|
|
6657
|
-
for (const
|
|
7050
|
+
for (const install of skillInstalls(resolved)) if (!coversAll(before, [install.skill], selection.agents) || await installBehind(install, check)) pending.push(install);
|
|
7051
|
+
const replaced = replacedCopies(before, check.locked);
|
|
6658
7052
|
const names = displayNames(selection.agents);
|
|
6659
|
-
if (pending.length === 0) {
|
|
7053
|
+
if (pending.length === 0 && replaced.size === 0) {
|
|
6660
7054
|
ctx.ui.success(`The rde and data-app skills are current for ${names}`);
|
|
6661
7055
|
return { outcome: "completed" };
|
|
6662
7056
|
}
|
|
6663
7057
|
const agents = selection.agents.map((agent) => agent.skillsCliName);
|
|
6664
7058
|
const forWhom = `for ${names} (${SOURCE_NOTES[selection.source]})`;
|
|
6665
|
-
for (const
|
|
6666
|
-
ctx.ui.info(`Installing
|
|
7059
|
+
for (const install of pending) {
|
|
7060
|
+
ctx.ui.info(`Installing ${install.skill} ${forWhom}${install.sourceNote}`);
|
|
6667
7061
|
await skills.add({
|
|
6668
|
-
source:
|
|
6669
|
-
skills:
|
|
7062
|
+
source: install.source,
|
|
7063
|
+
skills: [install.skill],
|
|
6670
7064
|
agents,
|
|
6671
7065
|
output: ctx.ui.childOutput
|
|
6672
7066
|
});
|
|
6673
7067
|
}
|
|
6674
|
-
const installed = await skills.list();
|
|
7068
|
+
const installed = pending.length === 0 ? before : await skills.list();
|
|
6675
7069
|
const missing = unlistedSkills(installed);
|
|
6676
7070
|
if (missing.length > 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show ${missing.join(", ")}`);
|
|
7071
|
+
for (const [by, old] of replaced) {
|
|
7072
|
+
const split = splitReplaced(by, old, installed, all);
|
|
7073
|
+
if (split.removable.length > 0) {
|
|
7074
|
+
ctx.ui.info(`Removing ${split.removable.join(", ")}, which ${by} replaces`);
|
|
7075
|
+
await skills.remove(split.removable, ctx.ui.childOutput);
|
|
7076
|
+
}
|
|
7077
|
+
if (split.kept.length > 0) ctx.ui.warn(keptReplacedLine(by, split));
|
|
7078
|
+
}
|
|
6677
7079
|
ctx.ui.note(installedLines(requiredEntries(installed), selection.agents), "Skills installed");
|
|
6678
7080
|
ctx.ui.info(MECHANICS_NOTE);
|
|
6679
7081
|
return { outcome: "completed" };
|
|
@@ -6693,7 +7095,7 @@ function hostDatabasesDeps(signal) {
|
|
|
6693
7095
|
now: () => /* @__PURE__ */ new Date()
|
|
6694
7096
|
};
|
|
6695
7097
|
}
|
|
6696
|
-
function requireApiKey
|
|
7098
|
+
function requireApiKey(state, apiKey) {
|
|
6697
7099
|
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
6698
7100
|
return apiKey;
|
|
6699
7101
|
}
|
|
@@ -6715,7 +7117,7 @@ function authenticatedClient(state, apiKey, deps) {
|
|
|
6715
7117
|
const configured = requireConfiguredRuntime(state);
|
|
6716
7118
|
return deps.client(configured.url, {
|
|
6717
7119
|
kind: "api-key",
|
|
6718
|
-
key: requireApiKey
|
|
7120
|
+
key: requireApiKey(state, apiKey)
|
|
6719
7121
|
});
|
|
6720
7122
|
}
|
|
6721
7123
|
async function listDatabases(state, apiKey, deps) {
|
|
@@ -6791,13 +7193,13 @@ async function connectRequired(ctx, deps, opening) {
|
|
|
6791
7193
|
result: { addedDatabases: connected.added }
|
|
6792
7194
|
};
|
|
6793
7195
|
}
|
|
6794
|
-
function hostDeps$
|
|
7196
|
+
function hostDeps$9(ctx) {
|
|
6795
7197
|
return {
|
|
6796
7198
|
...hostDatabasesDeps(ctx.signal),
|
|
6797
7199
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
6798
7200
|
};
|
|
6799
7201
|
}
|
|
6800
|
-
function databasesStep(options, deps = hostDeps$
|
|
7202
|
+
function databasesStep(options, deps = hostDeps$9) {
|
|
6801
7203
|
return {
|
|
6802
7204
|
id: "databases",
|
|
6803
7205
|
title: "Connect your databases",
|
|
@@ -6860,8 +7262,8 @@ function npmRegistry(signal, options = {}) {
|
|
|
6860
7262
|
}
|
|
6861
7263
|
//#endregion
|
|
6862
7264
|
//#region src/steps/install-mb.ts
|
|
6863
|
-
const STEP_ID$
|
|
6864
|
-
const MIN_MB_VERSION = "0.
|
|
7265
|
+
const STEP_ID$4 = "install-mb";
|
|
7266
|
+
const MIN_MB_VERSION = "0.5.1";
|
|
6865
7267
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
6866
7268
|
function installedWith(scriptPath) {
|
|
6867
7269
|
return scriptPath === void 0 ? "npm" : packageManagerFor(scriptPath);
|
|
@@ -6869,7 +7271,7 @@ function installedWith(scriptPath) {
|
|
|
6869
7271
|
function rdeInstalledWith() {
|
|
6870
7272
|
return installedWith(process.argv[1]);
|
|
6871
7273
|
}
|
|
6872
|
-
function hostDeps$
|
|
7274
|
+
function hostDeps$8(ctx) {
|
|
6873
7275
|
return {
|
|
6874
7276
|
runner: processRunner(ctx.signal),
|
|
6875
7277
|
installedWith: rdeInstalledWith(),
|
|
@@ -6923,6 +7325,33 @@ function updateTo(release, installed) {
|
|
|
6923
7325
|
function displayCommand(install) {
|
|
6924
7326
|
return [install.command, ...install.args].join(" ");
|
|
6925
7327
|
}
|
|
7328
|
+
const NPM_EACCES_EXIT = 243;
|
|
7329
|
+
const NPM_PERMISSION_TEXT = [
|
|
7330
|
+
"EACCES",
|
|
7331
|
+
"EPERM",
|
|
7332
|
+
"might be a permissions issue"
|
|
7333
|
+
];
|
|
7334
|
+
function deniedGlobalWrite(install, error) {
|
|
7335
|
+
if (install.command !== "npm" || !(error instanceof ExternalToolError)) return false;
|
|
7336
|
+
return NPM_PERMISSION_TEXT.some((text) => error.stderrTail.includes(text)) || error.exitCode === NPM_EACCES_EXIT;
|
|
7337
|
+
}
|
|
7338
|
+
function globalWriteDenied(install) {
|
|
7339
|
+
return new ConfigError([
|
|
7340
|
+
`\`${displayCommand(install)}\` failed because npm cannot write its global directory.`,
|
|
7341
|
+
"Give npm a prefix you own with `npm config set prefix ~/.npm-global`,",
|
|
7342
|
+
"add ~/.npm-global/bin to PATH and open a new terminal,",
|
|
7343
|
+
"or install Node with a version manager such as nvm, fnm or mise;",
|
|
7344
|
+
"then run rde init again."
|
|
7345
|
+
].join(" "));
|
|
7346
|
+
}
|
|
7347
|
+
async function installMb(install, ctx, deps) {
|
|
7348
|
+
try {
|
|
7349
|
+
await runShown(deps.runner, ctx.ui.childOutput, install.command, install.args);
|
|
7350
|
+
} catch (error) {
|
|
7351
|
+
if (deniedGlobalWrite(install, error)) throw globalWriteDenied(install);
|
|
7352
|
+
throw error;
|
|
7353
|
+
}
|
|
7354
|
+
}
|
|
6926
7355
|
async function versionAt(runner, path) {
|
|
6927
7356
|
try {
|
|
6928
7357
|
const raw = await mbTool(runner).version(path);
|
|
@@ -7036,7 +7465,7 @@ async function updateFor(release, installed, deps) {
|
|
|
7036
7465
|
version
|
|
7037
7466
|
};
|
|
7038
7467
|
}
|
|
7039
|
-
function installMbStep(updates, deps = hostDeps$
|
|
7468
|
+
function installMbStep(updates, deps = hostDeps$8) {
|
|
7040
7469
|
let published = null;
|
|
7041
7470
|
function release(resolved) {
|
|
7042
7471
|
if (updates === "installed") return Promise.resolve(UNCHECKED);
|
|
@@ -7044,7 +7473,7 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
7044
7473
|
return published;
|
|
7045
7474
|
}
|
|
7046
7475
|
return {
|
|
7047
|
-
id: STEP_ID$
|
|
7476
|
+
id: STEP_ID$4,
|
|
7048
7477
|
title: "Install the Metabase CLI",
|
|
7049
7478
|
async detect(ctx) {
|
|
7050
7479
|
const resolved = deps(ctx);
|
|
@@ -7071,7 +7500,7 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
7071
7500
|
} else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
7072
7501
|
const install = mbInstallCommand(resolved.installedWith);
|
|
7073
7502
|
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
7074
|
-
await
|
|
7503
|
+
await installMb(install, ctx, resolved);
|
|
7075
7504
|
const after = await inspectMb(resolved.runner);
|
|
7076
7505
|
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
7077
7506
|
const stillNewer = updateTo(target, after.version);
|
|
@@ -7085,171 +7514,6 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
7085
7514
|
};
|
|
7086
7515
|
}
|
|
7087
7516
|
//#endregion
|
|
7088
|
-
//#region src/steps/license.ts
|
|
7089
|
-
const STEP_ID$4 = "license";
|
|
7090
|
-
const LICENSE_KEYS = { token: `${STEP_ID$4}.token` };
|
|
7091
|
-
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
7092
|
-
const NOT_FOUND_STATUS$1 = 404;
|
|
7093
|
-
const TOKEN_REFUSED_STATUS = 400;
|
|
7094
|
-
const STORE_UNREACHABLE_STATUS = 503;
|
|
7095
|
-
const TOKEN_CHECK_HOST = "token-check.metabase.com";
|
|
7096
|
-
const ACTIVATION_FAILED = "License activation failed";
|
|
7097
|
-
const LICENSED_FEATURES = {
|
|
7098
|
-
[REMOTE_SYNC_FEATURE]: "remote sync",
|
|
7099
|
-
"data-apps": "data apps",
|
|
7100
|
-
"transforms-testing": "transform tests"
|
|
7101
|
-
};
|
|
7102
|
-
const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
|
|
7103
|
-
function licensedFeatureCoverage(features) {
|
|
7104
|
-
const entries = Object.entries(LICENSED_FEATURES);
|
|
7105
|
-
return {
|
|
7106
|
-
included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
|
|
7107
|
-
missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
|
|
7108
|
-
};
|
|
7109
|
-
}
|
|
7110
|
-
function listOf(names, type) {
|
|
7111
|
-
return new Intl.ListFormat("en", { type }).format(names);
|
|
7112
|
-
}
|
|
7113
|
-
function hostDeps$8(ctx) {
|
|
7114
|
-
return {
|
|
7115
|
-
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7116
|
-
runner: processRunner(ctx.signal),
|
|
7117
|
-
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
7118
|
-
terminal: stdinIsTty()
|
|
7119
|
-
};
|
|
7120
|
-
}
|
|
7121
|
-
function isNoTokenYet(error) {
|
|
7122
|
-
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
7123
|
-
return isMetabaseNotServing(error);
|
|
7124
|
-
}
|
|
7125
|
-
function validateToken(value) {
|
|
7126
|
-
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
7127
|
-
return "A license token is 64 hexadecimal characters";
|
|
7128
|
-
}
|
|
7129
|
-
function requireApiKey(ctx) {
|
|
7130
|
-
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
7131
|
-
return ctx.secrets.apiKey;
|
|
7132
|
-
}
|
|
7133
|
-
function activeLabel(status) {
|
|
7134
|
-
const plan = status["plan-alias"];
|
|
7135
|
-
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
7136
|
-
}
|
|
7137
|
-
function refusalOf(error) {
|
|
7138
|
-
if (!(error instanceof MetabaseApiError)) return null;
|
|
7139
|
-
if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
|
|
7140
|
-
if (error.status === STORE_UNREACHABLE_STATUS) return new ConfigError(`The instance could not reach ${TOKEN_CHECK_HOST} to check the license token; it needs outbound network access`);
|
|
7141
|
-
return null;
|
|
7142
|
-
}
|
|
7143
|
-
async function activate(client, token) {
|
|
7144
|
-
try {
|
|
7145
|
-
await client.setting.setPremiumEmbeddingToken(token);
|
|
7146
|
-
} catch (error) {
|
|
7147
|
-
const refusal = refusalOf(error);
|
|
7148
|
-
if (refusal === null) throw error;
|
|
7149
|
-
return {
|
|
7150
|
-
kind: "refused",
|
|
7151
|
-
error: refusal
|
|
7152
|
-
};
|
|
7153
|
-
}
|
|
7154
|
-
const status = await client.tokenStatus.get();
|
|
7155
|
-
if (!status.valid) return {
|
|
7156
|
-
kind: "refused",
|
|
7157
|
-
error: new ConfigError(`Metabase reports the token as ${status.status}`)
|
|
7158
|
-
};
|
|
7159
|
-
return {
|
|
7160
|
-
kind: "activated",
|
|
7161
|
-
status
|
|
7162
|
-
};
|
|
7163
|
-
}
|
|
7164
|
-
async function activateAskedToken(ctx, client, terminal) {
|
|
7165
|
-
const token = await ctx.prompts.password({
|
|
7166
|
-
key: LICENSE_KEYS.token,
|
|
7167
|
-
message: "Paste your Metabase license token (Enter to skip)",
|
|
7168
|
-
validate: validateToken
|
|
7169
|
-
});
|
|
7170
|
-
if (token.length === 0) return null;
|
|
7171
|
-
const spinner = ctx.ui.spinner("Activating the license…");
|
|
7172
|
-
let activation;
|
|
7173
|
-
try {
|
|
7174
|
-
activation = await activate(client, token);
|
|
7175
|
-
} catch (error) {
|
|
7176
|
-
spinner.fail(ACTIVATION_FAILED);
|
|
7177
|
-
throw error;
|
|
7178
|
-
}
|
|
7179
|
-
if (activation.kind === "activated") {
|
|
7180
|
-
spinner.stop(activeLabel(activation.status));
|
|
7181
|
-
ctx.session.licenseToken.set(token);
|
|
7182
|
-
return activation.status;
|
|
7183
|
-
}
|
|
7184
|
-
spinner.fail(ACTIVATION_FAILED);
|
|
7185
|
-
if (!terminal) throw activation.error;
|
|
7186
|
-
ctx.ui.warn(activation.error.message);
|
|
7187
|
-
return activateAskedToken(ctx, client, terminal);
|
|
7188
|
-
}
|
|
7189
|
-
function reportFeatures(ctx, features) {
|
|
7190
|
-
const { missing } = licensedFeatureCoverage(features);
|
|
7191
|
-
const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
|
|
7192
|
-
if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
|
|
7193
|
-
if (missing.length > 0) {
|
|
7194
|
-
const gitNote = remoteSync ? "" : "; the git step will be skipped";
|
|
7195
|
-
ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
|
|
7196
|
-
}
|
|
7197
|
-
}
|
|
7198
|
-
function licenseStep(deps = hostDeps$8) {
|
|
7199
|
-
return {
|
|
7200
|
-
id: STEP_ID$4,
|
|
7201
|
-
title: "Activate your license",
|
|
7202
|
-
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
7203
|
-
async detect(ctx) {
|
|
7204
|
-
if (ctx.state.steps[STEP_ID$4]?.status === "skipped") return "not-applicable";
|
|
7205
|
-
const configured = configuredRuntime(ctx.state);
|
|
7206
|
-
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
7207
|
-
const resolved = deps(ctx);
|
|
7208
|
-
if (!await resolved.instanceRunning()) return "todo";
|
|
7209
|
-
const client = resolved.client(configured.url, {
|
|
7210
|
-
kind: "api-key",
|
|
7211
|
-
key: ctx.secrets.apiKey
|
|
7212
|
-
});
|
|
7213
|
-
try {
|
|
7214
|
-
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
7215
|
-
} catch (error) {
|
|
7216
|
-
if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
|
|
7217
|
-
throw error;
|
|
7218
|
-
}
|
|
7219
|
-
},
|
|
7220
|
-
async run(ctx) {
|
|
7221
|
-
const configured = requireConfiguredRuntime(ctx.state);
|
|
7222
|
-
const apiKey = requireApiKey(ctx);
|
|
7223
|
-
const resolved = deps(ctx);
|
|
7224
|
-
const status = await activateAskedToken(ctx, resolved.client(configured.url, {
|
|
7225
|
-
kind: "api-key",
|
|
7226
|
-
key: apiKey
|
|
7227
|
-
}), resolved.terminal);
|
|
7228
|
-
if (status === null) {
|
|
7229
|
-
const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
|
|
7230
|
-
ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
|
|
7231
|
-
return { outcome: "skipped" };
|
|
7232
|
-
}
|
|
7233
|
-
await ctx.saveState({
|
|
7234
|
-
...ctx.state,
|
|
7235
|
-
license: {
|
|
7236
|
-
activated: true,
|
|
7237
|
-
features: status.features,
|
|
7238
|
-
validThru: status["valid-thru"] ?? null
|
|
7239
|
-
}
|
|
7240
|
-
});
|
|
7241
|
-
const mb = mbTool(resolved.runner);
|
|
7242
|
-
if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
|
|
7243
|
-
url: configured.url,
|
|
7244
|
-
profile: ctx.state.mbProfile,
|
|
7245
|
-
apiKey
|
|
7246
|
-
});
|
|
7247
|
-
reportFeatures(ctx, status.features);
|
|
7248
|
-
return { outcome: "completed" };
|
|
7249
|
-
}
|
|
7250
|
-
};
|
|
7251
|
-
}
|
|
7252
|
-
//#endregion
|
|
7253
7517
|
//#region src/steps/preflight.ts
|
|
7254
7518
|
const MIN_NODE_VERSION = "20.19.0";
|
|
7255
7519
|
const NODE_RANGE = `>=${MIN_NODE_VERSION}`;
|
|
@@ -7480,22 +7744,56 @@ async function chooseKind(ctx, options, runtime) {
|
|
|
7480
7744
|
ctx.ui.info(`Docker is not available (${available.docker.reason}); using the jar runtime with ${javaLabel(available.java)}`);
|
|
7481
7745
|
return "jar";
|
|
7482
7746
|
}
|
|
7483
|
-
|
|
7747
|
+
const NAME_ONE = "`--image <image>` or `--jar <release tag>` names a Metabase to run";
|
|
7748
|
+
const INIT_WAYS_OUT = {
|
|
7749
|
+
named: NAME_ONE,
|
|
7750
|
+
unreleased: `${NAME_ONE}, and \`RDE_DEV=1\` runs the development build`
|
|
7751
|
+
};
|
|
7752
|
+
function isFeedFailure(error) {
|
|
7753
|
+
return error instanceof MetabaseApiError || error instanceof MetabaseUnreachableError;
|
|
7754
|
+
}
|
|
7755
|
+
async function requireNewestRelease(releases, waysOut) {
|
|
7756
|
+
let newest;
|
|
7757
|
+
try {
|
|
7758
|
+
newest = await releases.newestSupportedRelease();
|
|
7759
|
+
} catch (error) {
|
|
7760
|
+
if (!isFeedFailure(error)) throw error;
|
|
7761
|
+
throw new ConfigError(`the Metabase release feed could not be read (${error.reason}); ${waysOut.named}`);
|
|
7762
|
+
}
|
|
7763
|
+
if (newest === null) throw new ConfigError(`no release of Metabase Enterprise 65 or newer is out yet; ${waysOut.unreleased}`);
|
|
7764
|
+
return newest;
|
|
7765
|
+
}
|
|
7766
|
+
async function releaseImage(ctx, releases, waysOut) {
|
|
7767
|
+
const newest = await requireNewestRelease(releases, waysOut);
|
|
7768
|
+
const image = majorLineImage(newest.major);
|
|
7769
|
+
ctx.ui.info(`The newest Metabase release is ${newest.version}; ${image} carries the newest build of ${newest.major}`);
|
|
7770
|
+
return image;
|
|
7771
|
+
}
|
|
7772
|
+
async function latestJarRelease(ctx, releases, waysOut) {
|
|
7773
|
+
if (developmentMode()) {
|
|
7774
|
+
const version = await releases.latestVersion();
|
|
7775
|
+
ctx.ui.info(`The latest Metabase release is ${version}`);
|
|
7776
|
+
return version;
|
|
7777
|
+
}
|
|
7778
|
+
const newest = await requireNewestRelease(releases, waysOut);
|
|
7779
|
+
ctx.ui.info(`The newest Metabase release is ${newest.version}`);
|
|
7780
|
+
return newest.version;
|
|
7781
|
+
}
|
|
7782
|
+
async function chooseImage(ctx, options, releases) {
|
|
7484
7783
|
if (options.image !== null) {
|
|
7485
7784
|
ctx.ui.info(`Using image ${options.image} from --image`);
|
|
7486
7785
|
return options.image;
|
|
7487
7786
|
}
|
|
7488
|
-
return
|
|
7787
|
+
if (developmentMode()) return DEVELOPMENT_IMAGE;
|
|
7788
|
+
return releaseImage(ctx, releases, INIT_WAYS_OUT);
|
|
7489
7789
|
}
|
|
7490
7790
|
async function resolveJar(ctx, spec, releases) {
|
|
7491
7791
|
const parsed = parseJarSpec(spec);
|
|
7492
|
-
if (parsed === null) throw new ConfigError(`${spec} is not latest,
|
|
7792
|
+
if (parsed === null) throw new ConfigError(`${spec} is not latest, an enterprise release tag such as v1.65.0, or the path of a .jar file`);
|
|
7493
7793
|
if (parsed.kind !== "latest") return parsed;
|
|
7494
|
-
const version = await releases.latestVersion();
|
|
7495
|
-
ctx.ui.info(`The latest Metabase release is ${version}`);
|
|
7496
7794
|
return {
|
|
7497
7795
|
kind: "release",
|
|
7498
|
-
version
|
|
7796
|
+
version: await latestJarRelease(ctx, releases, INIT_WAYS_OUT)
|
|
7499
7797
|
};
|
|
7500
7798
|
}
|
|
7501
7799
|
async function chooseJar(ctx, options, releases) {
|
|
@@ -7520,7 +7818,7 @@ async function chooseRuntime(ctx, options, deps) {
|
|
|
7520
7818
|
const kind = await chooseKind(ctx, options, deps.runtime);
|
|
7521
7819
|
switch (kind) {
|
|
7522
7820
|
case "docker": {
|
|
7523
|
-
const image = chooseImage(ctx, options);
|
|
7821
|
+
const image = await chooseImage(ctx, options, deps.releases);
|
|
7524
7822
|
const port = await choosePort(ctx, options, deps.runtime);
|
|
7525
7823
|
return {
|
|
7526
7824
|
kind,
|
|
@@ -7601,6 +7899,16 @@ function isNotHealthy(error) {
|
|
|
7601
7899
|
async function runningVersion(client) {
|
|
7602
7900
|
return (await client.session.properties()).version.tag;
|
|
7603
7901
|
}
|
|
7902
|
+
const PICK_ANOTHER = "`rde upgrade --to <image or release tag>` moves this instance to one, or `rde uninstall`, then `rde init --image <image>` or `rde init --jar <release tag>`, sets up a new instance on one";
|
|
7903
|
+
function unsupportedVersionReason(version, development) {
|
|
7904
|
+
if (development) return null;
|
|
7905
|
+
const needs = `rde needs Metabase Enterprise 65 or newer: ${PICK_ANOTHER}`;
|
|
7906
|
+
if (isOpenSourceRelease(version)) return `Metabase ${version} is the open-source edition; ${needs}`;
|
|
7907
|
+
if (isMasterBuild(version)) return `Metabase ${version} is a development build of master, which only development mode (RDE_DEV=1) runs; ${needs}`;
|
|
7908
|
+
const major = releaseMajor(version);
|
|
7909
|
+
if (major !== null && major < 65) return `Metabase ${version} is older than 65; ${needs}`;
|
|
7910
|
+
return null;
|
|
7911
|
+
}
|
|
7604
7912
|
function progressLabel(progress) {
|
|
7605
7913
|
return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
|
|
7606
7914
|
}
|
|
@@ -7617,7 +7925,7 @@ async function startRuntime(ctx, runtime, deps) {
|
|
|
7617
7925
|
throw error;
|
|
7618
7926
|
}
|
|
7619
7927
|
}
|
|
7620
|
-
function startMetabaseStep(deps = hostDeps$5) {
|
|
7928
|
+
function startMetabaseStep(deps = hostDeps$5, accepted = "supported") {
|
|
7621
7929
|
return {
|
|
7622
7930
|
id: "start-metabase",
|
|
7623
7931
|
title: "Start Metabase",
|
|
@@ -7669,6 +7977,11 @@ function startMetabaseStep(deps = hostDeps$5) {
|
|
|
7669
7977
|
...ctx.state,
|
|
7670
7978
|
version
|
|
7671
7979
|
});
|
|
7980
|
+
const refusal = accepted === "supported" ? unsupportedVersionReason(version, developmentMode()) : null;
|
|
7981
|
+
if (refusal !== null) {
|
|
7982
|
+
spinner.fail(`Metabase ${version} is running at ${configured.url}, but rde does not run it`);
|
|
7983
|
+
throw new UnsupportedMetabaseError(refusal);
|
|
7984
|
+
}
|
|
7672
7985
|
spinner.stop(`Metabase ${version} is running at ${configured.url}`);
|
|
7673
7986
|
return { outcome: "completed" };
|
|
7674
7987
|
}
|
|
@@ -7678,10 +7991,8 @@ function startMetabaseStep(deps = hostDeps$5) {
|
|
|
7678
7991
|
//#region src/steps/sync-scope.ts
|
|
7679
7992
|
const STEP_ID$2 = "sync-scope";
|
|
7680
7993
|
const SYNC_SCOPE_KEYS = { export: `${STEP_ID$2}.export` };
|
|
7681
|
-
const AGENT_COLLECTION_NAME = "Analytics";
|
|
7682
7994
|
const LIBRARY_NAME = "Library";
|
|
7683
7995
|
const ROOT_COLLECTION_NAME = "Our analytics";
|
|
7684
|
-
const AGENT_COLLECTION_DESCRIPTION = "Dashboards, questions, models, and documents that reach git on export.";
|
|
7685
7996
|
function hostDeps$4(ctx) {
|
|
7686
7997
|
return {
|
|
7687
7998
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -7692,22 +8003,11 @@ function hostDeps$4(ctx) {
|
|
|
7692
8003
|
function isCollection(entry) {
|
|
7693
8004
|
return typeof entry.id === "number";
|
|
7694
8005
|
}
|
|
7695
|
-
function
|
|
7696
|
-
return collection
|
|
8006
|
+
function findLibrary(list) {
|
|
8007
|
+
return list.filter(isCollection).find((collection) => collection.location === "/" && collection.type === "library") ?? null;
|
|
7697
8008
|
}
|
|
7698
|
-
function
|
|
7699
|
-
|
|
7700
|
-
const ordinary = topLevel.filter(isOrdinary);
|
|
7701
|
-
const unsynced = ordinary.filter((collection) => !collection.is_remote_synced);
|
|
7702
|
-
return {
|
|
7703
|
-
library: topLevel.find((collection) => collection.type === "library") ?? null,
|
|
7704
|
-
syncedRoots: ordinary.filter((collection) => collection.is_remote_synced),
|
|
7705
|
-
unsyncedRecordedRoot: unsynced.find((collection) => collection.id === syncRoot) ?? null,
|
|
7706
|
-
foreignNamesake: unsynced.some((collection) => collection.id !== syncRoot && collection.name === "Analytics")
|
|
7707
|
-
};
|
|
7708
|
-
}
|
|
7709
|
-
function isComplete(scope, libraryLicensed) {
|
|
7710
|
-
return (!libraryLicensed || scope.library !== null && scope.library.is_remote_synced) && scope.syncedRoots.length > 0;
|
|
8009
|
+
function isComplete(library, libraryLicensed) {
|
|
8010
|
+
return !libraryLicensed || library !== null && library.is_remote_synced;
|
|
7711
8011
|
}
|
|
7712
8012
|
function isNotAnsweringYet(error) {
|
|
7713
8013
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || isRejectedCredential(error);
|
|
@@ -7736,34 +8036,8 @@ async function isReadOnly(client) {
|
|
|
7736
8036
|
async function requireReadWrite(client) {
|
|
7737
8037
|
if (await isReadOnly(client)) throw new ConfigError(`remote sync is read-only; the repository decides what syncs. Switch it to read-write in Admin, then \`rde init --only ${STEP_ID$2}\``);
|
|
7738
8038
|
}
|
|
7739
|
-
async function ensureLibrary(client,
|
|
7740
|
-
|
|
7741
|
-
return scope.library ?? client.collection.createLibrary();
|
|
7742
|
-
}
|
|
7743
|
-
async function ensureRoots(ctx, client, scope) {
|
|
7744
|
-
if (scope.syncedRoots.length > 0) return {
|
|
7745
|
-
roots: scope.syncedRoots,
|
|
7746
|
-
created: false,
|
|
7747
|
-
besideNamesake: false
|
|
7748
|
-
};
|
|
7749
|
-
if (scope.unsyncedRecordedRoot !== null) return {
|
|
7750
|
-
roots: [scope.unsyncedRecordedRoot],
|
|
7751
|
-
created: false,
|
|
7752
|
-
besideNamesake: false
|
|
7753
|
-
};
|
|
7754
|
-
const created = await client.collection.create({
|
|
7755
|
-
name: AGENT_COLLECTION_NAME,
|
|
7756
|
-
description: AGENT_COLLECTION_DESCRIPTION
|
|
7757
|
-
});
|
|
7758
|
-
await ctx.saveState({
|
|
7759
|
-
...ctx.state,
|
|
7760
|
-
syncRoot: created.id
|
|
7761
|
-
});
|
|
7762
|
-
return {
|
|
7763
|
-
roots: [created],
|
|
7764
|
-
created: true,
|
|
7765
|
-
besideNamesake: scope.foreignNamesake
|
|
7766
|
-
};
|
|
8039
|
+
async function ensureLibrary(client, library) {
|
|
8040
|
+
return library ?? client.collection.createLibrary();
|
|
7767
8041
|
}
|
|
7768
8042
|
function itemLabel(item) {
|
|
7769
8043
|
return item.name === null ? `${item.model} ${item.id}` : `${item.model} ${item.id} "${item.name}"`;
|
|
@@ -7800,21 +8074,12 @@ function explainRefusal(error) {
|
|
|
7800
8074
|
if (!refusal.success) return error;
|
|
7801
8075
|
return new ConfigError([...refusal.data.errors.required.flatMap(requiredSyncLines), `After the fix, run \`rde init --only ${STEP_ID$2}\`.`].join("\n"));
|
|
7802
8076
|
}
|
|
7803
|
-
function plannedNames(scope, libraryLicensed) {
|
|
7804
|
-
const library = libraryLicensed ? [scope.library?.name ?? LIBRARY_NAME] : [];
|
|
7805
|
-
if (scope.syncedRoots.length > 0) return [...library, ...scope.syncedRoots.map((collection) => collection.name)];
|
|
7806
|
-
return [...library, scope.unsyncedRecordedRoot?.name ?? "Analytics"];
|
|
7807
|
-
}
|
|
7808
|
-
function nameList(names) {
|
|
7809
|
-
if (names.length <= 2) return names.join(" and ");
|
|
7810
|
-
return `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
|
|
7811
|
-
}
|
|
7812
8077
|
function exportAdvice(profile, branch, changes) {
|
|
7813
8078
|
const command = exportCommand(profile, branch);
|
|
7814
8079
|
if (!remoteMoved(changes)) return `\`${command}\``;
|
|
7815
8080
|
return `the branch has commits Metabase has not imported; \`${mbCommand(profile)} git-sync export-preflight\`, then \`${command} --merge\``;
|
|
7816
8081
|
}
|
|
7817
|
-
async function planExport(ctx, runner, instance,
|
|
8082
|
+
async function planExport(ctx, runner, instance, name) {
|
|
7818
8083
|
const changes = await mbTool(runner).gitSyncHasRemoteChanges(instance);
|
|
7819
8084
|
if (remoteMoved(changes)) return {
|
|
7820
8085
|
changes,
|
|
@@ -7824,7 +8089,7 @@ async function planExport(ctx, runner, instance, names) {
|
|
|
7824
8089
|
changes,
|
|
7825
8090
|
push: await ctx.prompts.confirm({
|
|
7826
8091
|
key: SYNC_SCOPE_KEYS.export,
|
|
7827
|
-
message: `Push ${
|
|
8092
|
+
message: `Push ${name} to ${requireGitRemote(ctx).branch} now?`,
|
|
7828
8093
|
initialValue: true
|
|
7829
8094
|
})
|
|
7830
8095
|
};
|
|
@@ -7839,7 +8104,7 @@ function reportPending(run) {
|
|
|
7839
8104
|
}
|
|
7840
8105
|
async function exportScope(run, runner) {
|
|
7841
8106
|
const mb = mbTool(runner);
|
|
7842
|
-
const label =
|
|
8107
|
+
const label = run.name;
|
|
7843
8108
|
const spinner = run.ctx.ui.spinner(`Pushing ${label} to ${run.gitRemote.branch}…`);
|
|
7844
8109
|
try {
|
|
7845
8110
|
const final = (await mb.gitSyncExport(run.instance, {
|
|
@@ -7860,9 +8125,6 @@ async function exportScope(run, runner) {
|
|
|
7860
8125
|
reportPending(run);
|
|
7861
8126
|
}
|
|
7862
8127
|
}
|
|
7863
|
-
function scopeLabel(synced) {
|
|
7864
|
-
return `Synced to git: ${synced.map((collection) => collection.name).join(", ")}`;
|
|
7865
|
-
}
|
|
7866
8128
|
function syncScopeStep(deps = hostDeps$4) {
|
|
7867
8129
|
return {
|
|
7868
8130
|
id: STEP_ID$2,
|
|
@@ -7878,7 +8140,7 @@ function syncScopeStep(deps = hostDeps$4) {
|
|
|
7878
8140
|
key: ctx.secrets.apiKey
|
|
7879
8141
|
});
|
|
7880
8142
|
try {
|
|
7881
|
-
if (isComplete(
|
|
8143
|
+
if (isComplete(findLibrary(await client.collection.list()), hasLibrary(ctx.state.license))) return "done";
|
|
7882
8144
|
return await isReadOnly(client) ? "not-applicable" : "todo";
|
|
7883
8145
|
} catch (error) {
|
|
7884
8146
|
if (isNotAnsweringYet(error)) return "todo";
|
|
@@ -7897,30 +8159,27 @@ function syncScopeStep(deps = hostDeps$4) {
|
|
|
7897
8159
|
await requireReadWrite(client);
|
|
7898
8160
|
await requireSettledImport(ctx, client, gitRemote);
|
|
7899
8161
|
const libraryLicensed = hasLibrary(ctx.state.license);
|
|
7900
|
-
const
|
|
7901
|
-
|
|
7902
|
-
|
|
8162
|
+
const found = findLibrary(await client.collection.list());
|
|
8163
|
+
if (isComplete(found, libraryLicensed)) {
|
|
8164
|
+
ctx.ui.info(libraryLicensed ? `Synced to git: ${found?.name ?? LIBRARY_NAME}` : "The license has no Library, so there is nothing to mark for sync");
|
|
8165
|
+
return { outcome: "completed" };
|
|
8166
|
+
}
|
|
8167
|
+
const name = found?.name ?? LIBRARY_NAME;
|
|
8168
|
+
const plan = await planExport(ctx, resolved.runner, instance, name);
|
|
7903
8169
|
const spinner = ctx.ui.spinner("Choosing what syncs to git…");
|
|
7904
|
-
let synced;
|
|
7905
|
-
let chosen;
|
|
7906
8170
|
try {
|
|
7907
|
-
const library = await ensureLibrary(client,
|
|
7908
|
-
|
|
7909
|
-
synced = library === null ? chosen.roots : [library, ...chosen.roots];
|
|
7910
|
-
await client.remoteSync.updateScope({ collections: Object.fromEntries(synced.map((collection) => [collection.id, true])) });
|
|
8171
|
+
const library = await ensureLibrary(client, found);
|
|
8172
|
+
await client.remoteSync.updateScope({ collections: { [library.id]: true } });
|
|
7911
8173
|
} catch (error) {
|
|
7912
8174
|
spinner.fail("Could not choose what syncs to git");
|
|
7913
8175
|
throw explainRefusal(error);
|
|
7914
8176
|
}
|
|
7915
|
-
spinner.stop(
|
|
7916
|
-
if (chosen.created) ctx.ui.info(`Created the ${AGENT_COLLECTION_NAME} collection: dashboards, questions, models, and documents filed under it reach git on the next export`);
|
|
7917
|
-
if (chosen.besideNamesake) ctx.ui.info(`Another collection is also named ${AGENT_COLLECTION_NAME}; rde did not create it, so it stays out of git`);
|
|
7918
|
-
if (plan === null) return { outcome: "completed" };
|
|
8177
|
+
spinner.stop(`Synced to git: ${name}`);
|
|
7919
8178
|
const exportRun = {
|
|
7920
8179
|
ctx,
|
|
7921
8180
|
instance,
|
|
7922
8181
|
gitRemote,
|
|
7923
|
-
|
|
8182
|
+
name,
|
|
7924
8183
|
changes: plan.changes
|
|
7925
8184
|
};
|
|
7926
8185
|
if (plan.push) await exportScope(exportRun, resolved.runner);
|
|
@@ -7976,16 +8235,23 @@ function remoteLine(remote) {
|
|
|
7976
8235
|
case "hosted": return `Remote: ${remote.url}`;
|
|
7977
8236
|
}
|
|
7978
8237
|
}
|
|
7979
|
-
function
|
|
7980
|
-
const license = state.license;
|
|
8238
|
+
function licenseGap(license) {
|
|
7981
8239
|
if (!license.activated) return "no license";
|
|
7982
8240
|
if (!hasRemoteSync(license)) return "the license has no remote-sync feature";
|
|
8241
|
+
return null;
|
|
8242
|
+
}
|
|
8243
|
+
function notConfiguredReason(state) {
|
|
8244
|
+
const gap = licenseGap(state.license);
|
|
8245
|
+
if (gap !== null) return gap;
|
|
7983
8246
|
if (state.steps[GIT_SYNC_STEP_ID]?.status === "skipped") return "skipped; `rde init --only git-sync` sets it up";
|
|
7984
8247
|
return "not set up yet; `rde init --only git-sync`";
|
|
7985
8248
|
}
|
|
7986
8249
|
function gitLines(state) {
|
|
7987
8250
|
if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
|
|
7988
|
-
|
|
8251
|
+
const repository = [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
|
|
8252
|
+
const gap = licenseGap(state.license);
|
|
8253
|
+
if (gap === null) return repository;
|
|
8254
|
+
return [...repository, `Remote sync: unavailable (${gap}; \`rde init --from license\` activates one)`];
|
|
7989
8255
|
}
|
|
7990
8256
|
async function pendingExportLines(state, secrets, runner) {
|
|
7991
8257
|
const instance = mbInstanceOf(state, secrets);
|
|
@@ -8000,10 +8266,11 @@ async function pendingExportLines(state, secrets, runner) {
|
|
|
8000
8266
|
throw error;
|
|
8001
8267
|
}
|
|
8002
8268
|
}
|
|
8003
|
-
function skillLine(entries, detected) {
|
|
8269
|
+
function skillLine(entries, detected, skipped) {
|
|
8004
8270
|
const agents = [...new Set(entries.flatMap((entry) => coveredAgentNames(entry, detected)))];
|
|
8005
|
-
if (agents.length === 0) return
|
|
8006
|
-
return
|
|
8271
|
+
if (agents.length === 0) return `${RDE_SKILL_NAME} skill: not installed; \`rde init --only agents-skills\``;
|
|
8272
|
+
if (skipped) return `${RDE_SKILL_NAME} skill: skipped by this run; an earlier install covers ${agents.join(", ")}`;
|
|
8273
|
+
return `${RDE_SKILL_NAME} skill: installed for ${agents.join(", ")}`;
|
|
8007
8274
|
}
|
|
8008
8275
|
function launchLines(agents, workdir) {
|
|
8009
8276
|
const detected = agents.filter((agent) => agent.detected);
|
|
@@ -8023,7 +8290,7 @@ async function summaryLines(state, secrets, deps) {
|
|
|
8023
8290
|
const installed = await skillsCli(deps.runner).list();
|
|
8024
8291
|
const agents = await detectAgents(deps.probe);
|
|
8025
8292
|
const detected = agents.filter((agent) => agent.detected);
|
|
8026
|
-
lines.push(skillLine(installed.filter((entry) => entry.name ===
|
|
8293
|
+
lines.push(skillLine(installed.filter((entry) => entry.name === RDE_SKILL_NAME), detected, state.steps["agents-skills"]?.status === "skipped"));
|
|
8027
8294
|
lines.push(...launchLines(agents, state.gitRemote?.workdir ?? null));
|
|
8028
8295
|
lines.push("Lifecycle: rde status (--json for scripts), rde stop, rde start");
|
|
8029
8296
|
return lines;
|
|
@@ -8467,6 +8734,18 @@ function parseDatabase(spec) {
|
|
|
8467
8734
|
uri
|
|
8468
8735
|
};
|
|
8469
8736
|
}
|
|
8737
|
+
function databasesSection(flags) {
|
|
8738
|
+
if (flags.databaseName.length > flags.database.length) throw new ConfigError("--database-name names a --database; give one --database for each name");
|
|
8739
|
+
if (flags.database.length === 0) return;
|
|
8740
|
+
return flags.database.map((spec, index) => {
|
|
8741
|
+
const name = flags.databaseName[index];
|
|
8742
|
+
if (name !== void 0 && name.trim().length === 0) throw new ConfigError("--database-name needs a display name");
|
|
8743
|
+
return {
|
|
8744
|
+
...parseDatabase(spec),
|
|
8745
|
+
...name !== void 0 && { name }
|
|
8746
|
+
};
|
|
8747
|
+
});
|
|
8748
|
+
}
|
|
8470
8749
|
function adminSection(flags, secrets) {
|
|
8471
8750
|
const prompted = flags.adminEmail !== void 0 || secrets.has("--admin-password");
|
|
8472
8751
|
if (flags.admin === void 0 && !prompted) return;
|
|
@@ -8604,7 +8883,7 @@ async function answersFromFlags(flags, readStdin) {
|
|
|
8604
8883
|
runtime: runtimeSection(flags),
|
|
8605
8884
|
"admin-user": adminSection(flags, secrets),
|
|
8606
8885
|
license: licenseSection(flags, secrets),
|
|
8607
|
-
databases: flags
|
|
8886
|
+
databases: databasesSection(flags),
|
|
8608
8887
|
"git-sync": gitSection(flags, secrets),
|
|
8609
8888
|
"agents-skills": agentsSection(flags),
|
|
8610
8889
|
"api-key": flags.mbProfile === void 0 ? void 0 : { profile: flags.mbProfile }
|
|
@@ -8661,6 +8940,11 @@ function givenFlags(flags) {
|
|
|
8661
8940
|
flag: "--database",
|
|
8662
8941
|
given: flags.database.length > 0
|
|
8663
8942
|
},
|
|
8943
|
+
{
|
|
8944
|
+
step: "databases",
|
|
8945
|
+
flag: "--database-name",
|
|
8946
|
+
given: flags.databaseName.length > 0
|
|
8947
|
+
},
|
|
8664
8948
|
{
|
|
8665
8949
|
step: "git-sync",
|
|
8666
8950
|
flag: "--git-dir",
|
|
@@ -8956,8 +9240,7 @@ function plainUi(output) {
|
|
|
8956
9240
|
};
|
|
8957
9241
|
}
|
|
8958
9242
|
function noColorRequested() {
|
|
8959
|
-
|
|
8960
|
-
return value !== void 0 && value !== "";
|
|
9243
|
+
return readSetEnv(ENV_NO_COLOR) !== null;
|
|
8961
9244
|
}
|
|
8962
9245
|
function drawsOnTerminal() {
|
|
8963
9246
|
return process.stdout.isTTY === true && !noColorRequested();
|
|
@@ -9088,6 +9371,20 @@ const db = defineCommand({
|
|
|
9088
9371
|
default: "list"
|
|
9089
9372
|
});
|
|
9090
9373
|
//#endregion
|
|
9374
|
+
//#region package.json
|
|
9375
|
+
var name = "@metabase/rde";
|
|
9376
|
+
var version = "0.5.0";
|
|
9377
|
+
var description = "Prepares a local machine for data-engineering work with Metabase and coding agents";
|
|
9378
|
+
//#endregion
|
|
9379
|
+
//#region src/version.ts
|
|
9380
|
+
const PACKAGE_NAME = name;
|
|
9381
|
+
const VERSION = version;
|
|
9382
|
+
const ROOT_META = {
|
|
9383
|
+
name: "rde",
|
|
9384
|
+
version: VERSION,
|
|
9385
|
+
description
|
|
9386
|
+
};
|
|
9387
|
+
//#endregion
|
|
9091
9388
|
//#region src/wizard/engine.ts
|
|
9092
9389
|
function requireKnownStep(steps, id, flag) {
|
|
9093
9390
|
if (id === void 0) return;
|
|
@@ -9184,6 +9481,8 @@ async function runWizard(steps, ctx, options = {}) {
|
|
|
9184
9481
|
}
|
|
9185
9482
|
//#endregion
|
|
9186
9483
|
//#region src/steps/doctor.ts
|
|
9484
|
+
const RDE_DIST_TAG = "latest";
|
|
9485
|
+
const RDE_RELEASE_TIMEOUT_MS = 3e3;
|
|
9187
9486
|
function hostDoctorDeps(instanceName, signal) {
|
|
9188
9487
|
const runner = processRunner(signal);
|
|
9189
9488
|
const runtime = hostRuntimeDeps(instanceName, signal);
|
|
@@ -9203,7 +9502,8 @@ function hostDoctorDeps(instanceName, signal) {
|
|
|
9203
9502
|
cwd: process.cwd(),
|
|
9204
9503
|
stateHome: xdgStateHome(),
|
|
9205
9504
|
files: hostSkillFiles
|
|
9206
|
-
}
|
|
9505
|
+
},
|
|
9506
|
+
publishedRdeVersion: () => npmRegistry(signal, { timeoutMs: RDE_RELEASE_TIMEOUT_MS }).distTagVersion(PACKAGE_NAME, RDE_DIST_TAG)
|
|
9207
9507
|
};
|
|
9208
9508
|
}
|
|
9209
9509
|
const NOT_FOUND_STATUS = 404;
|
|
@@ -9297,6 +9597,26 @@ function mbRow(diagnosis, lines, deps) {
|
|
|
9297
9597
|
case "current": return ok("mb", MB_LABEL, lines.join("\n"));
|
|
9298
9598
|
}
|
|
9299
9599
|
}
|
|
9600
|
+
const RDE_LABEL = "rde";
|
|
9601
|
+
const RDE_UPDATE_HINT = `\`npm i -g ${PACKAGE_NAME}@${RDE_DIST_TAG}\``;
|
|
9602
|
+
function rdeUncheckedRow(reason) {
|
|
9603
|
+
return skipped("rde", RDE_LABEL, `${VERSION}; could not check for a newer release: ${reason}`);
|
|
9604
|
+
}
|
|
9605
|
+
async function rdeRow(deps) {
|
|
9606
|
+
let raw;
|
|
9607
|
+
try {
|
|
9608
|
+
raw = await deps.publishedRdeVersion();
|
|
9609
|
+
} catch (error) {
|
|
9610
|
+
const reason = lookupFailure(error);
|
|
9611
|
+
if (reason === null) throw error;
|
|
9612
|
+
return rdeUncheckedRow(reason);
|
|
9613
|
+
}
|
|
9614
|
+
const published = semver.valid(raw);
|
|
9615
|
+
if (published === null) return rdeUncheckedRow(`the registry names "${raw}" for ${RDE_DIST_TAG}, which is not a version`);
|
|
9616
|
+
if (semver.lt(VERSION, published)) return note$1("rde", RDE_LABEL, `${VERSION}; ${published} is released`, RDE_UPDATE_HINT);
|
|
9617
|
+
if (semver.gt(VERSION, published)) return ok("rde", RDE_LABEL, `${VERSION}, ahead of the latest release ${published}`);
|
|
9618
|
+
return ok("rde", RDE_LABEL, `${VERSION}, the latest release`);
|
|
9619
|
+
}
|
|
9300
9620
|
async function mbFinding(deps) {
|
|
9301
9621
|
const found = await inspectMbOnPath(deps.runner, deps.locateMb);
|
|
9302
9622
|
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
@@ -9317,6 +9637,20 @@ function publishesOnLoopback(container, port) {
|
|
|
9317
9637
|
}
|
|
9318
9638
|
const CONTAINER = "the container";
|
|
9319
9639
|
const CONTAINER_SILENT_PORT_HINT = "Docker's forward of the port is broken; `rde start` repairs it";
|
|
9640
|
+
function containerSettingsFailure(runtime, inspection) {
|
|
9641
|
+
const onMaster = !developmentMode() && isMasterBuildImage(runtime.image);
|
|
9642
|
+
const drift = containerEnvDrift(inspection, runtime);
|
|
9643
|
+
const problems = [];
|
|
9644
|
+
if (onMaster) problems.push(`${runtime.containerName} runs ${runtime.image}, a development build of Metabase`);
|
|
9645
|
+
if (drift.length > 0) problems.push(`${runtime.containerName} was created with ${drift.join(", ")} set otherwise than rde sets them now`);
|
|
9646
|
+
if (problems.length === 0) return null;
|
|
9647
|
+
const hint = onMaster ? "a build of master is ahead of every release, so `rde uninstall`, then `rde init`, sets up an instance on a release; `RDE_DEV=1` keeps running this one" : "`rde upgrade` creates the container again with rde's settings";
|
|
9648
|
+
return {
|
|
9649
|
+
detail: problems.join("\n"),
|
|
9650
|
+
hint,
|
|
9651
|
+
step: null
|
|
9652
|
+
};
|
|
9653
|
+
}
|
|
9320
9654
|
async function containerRow(runtime, deps, dockerOk) {
|
|
9321
9655
|
const label = "container";
|
|
9322
9656
|
if (!dockerOk) return {
|
|
@@ -9357,6 +9691,11 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
9357
9691
|
}),
|
|
9358
9692
|
...found
|
|
9359
9693
|
};
|
|
9694
|
+
const settings = containerSettingsFailure(runtime, inspection);
|
|
9695
|
+
if (settings !== null) return {
|
|
9696
|
+
row: failed("container", label, settings),
|
|
9697
|
+
...found
|
|
9698
|
+
};
|
|
9360
9699
|
const state = inspection.running ? "running" : "stopped";
|
|
9361
9700
|
return {
|
|
9362
9701
|
row: ok("container", label, `${runtime.containerName} ${state} (${runtime.image})`),
|
|
@@ -9748,8 +10087,8 @@ async function gitSyncRow(state, instance, deps) {
|
|
|
9748
10087
|
const SKILL_STEP = "agents-skills";
|
|
9749
10088
|
const GLOBAL_SCOPE = "global";
|
|
9750
10089
|
const SKILL_FILE = "SKILL.md";
|
|
9751
|
-
const SOURCE_SKILLS_DIR = "skills";
|
|
9752
10090
|
const CLAUDE_CODE = "Claude Code";
|
|
10091
|
+
const COPIES_LABEL = `${RDE_SKILL_NAME} skill copies`;
|
|
9753
10092
|
const CLAUDE_CODE_SKILL_RULE = "a personal skill wins over a project one: https://code.claude.com/docs/en/skills";
|
|
9754
10093
|
function skillRowId(agent) {
|
|
9755
10094
|
return `skill.${agent.id}`;
|
|
@@ -9761,12 +10100,12 @@ async function copyRef(entry, deps) {
|
|
|
9761
10100
|
const path = entry.scope === GLOBAL_SCOPE ? globalSkillLockPath(deps.probe.home, deps.skills.stateHome) : projectSkillLockPath(deps.skills.cwd);
|
|
9762
10101
|
return lockedSkill(path, await deps.skills.files.readJson(path), entry.name)?.ref ?? null;
|
|
9763
10102
|
}
|
|
9764
|
-
async function judgeCopy(entry, origin, deps) {
|
|
10103
|
+
async function judgeCopy(skill, entry, origin, deps) {
|
|
9765
10104
|
const where = `${entry.scope} ${entry.path}`;
|
|
9766
10105
|
switch (origin.kind) {
|
|
9767
10106
|
case "local": {
|
|
9768
10107
|
const installed = await deps.skills.files.readText(join(entry.path, SKILL_FILE));
|
|
9769
|
-
const wanted = await deps.skills.files.readText(join(origin.path,
|
|
10108
|
+
const wanted = await deps.skills.files.readText(join(origin.path, sourceSkillDirectory(skill), SKILL_FILE));
|
|
9770
10109
|
const current = installed !== null && installed === wanted;
|
|
9771
10110
|
return {
|
|
9772
10111
|
entry,
|
|
@@ -9807,53 +10146,99 @@ function loadedCopyLine(entries, cwd) {
|
|
|
9807
10146
|
if (loaded === void 0) return count;
|
|
9808
10147
|
return `${count}; ${CLAUDE_CODE} started in ${cwd} loads ${loaded.path} (${CLAUDE_CODE_SKILL_RULE})`;
|
|
9809
10148
|
}
|
|
9810
|
-
function staleCopiesRow(stale, detail, deps) {
|
|
10149
|
+
function staleCopiesRow(stale, replaced, detail, deps) {
|
|
9811
10150
|
const globalStale = stale.some((copy) => copy.entry.scope === GLOBAL_SCOPE);
|
|
9812
10151
|
const projectStale = stale.some((copy) => copy.entry.scope !== GLOBAL_SCOPE);
|
|
9813
10152
|
const repairs = [];
|
|
9814
|
-
|
|
9815
|
-
if (
|
|
9816
|
-
|
|
10153
|
+
const stepFixes = [];
|
|
10154
|
+
if (globalStale) stepFixes.push(`reinstalls the global copy from ${deps.skills.source}`);
|
|
10155
|
+
if (replaced) stepFixes.push(`removes the copies ${RDE_SKILL_NAME} replaces`);
|
|
10156
|
+
if (stepFixes.length > 0) repairs.push(`\`rde init --only ${SKILL_STEP}\` ${stepFixes.join(" and ")}`);
|
|
10157
|
+
if (projectStale) repairs.push(`remove the project copy with \`npx -y skills@latest remove ${RDE_SKILL_NAME} -y\` in ${deps.skills.cwd}`);
|
|
10158
|
+
return failed("skill-copies", COPIES_LABEL, {
|
|
9817
10159
|
detail,
|
|
9818
10160
|
hint: `${repairs.join("; ")}; then restart the agent session`,
|
|
9819
|
-
step:
|
|
10161
|
+
step: stepFixes.length > 0 ? SKILL_STEP : null
|
|
9820
10162
|
});
|
|
9821
10163
|
}
|
|
9822
|
-
async function skillCopiesRow(globalEntries, deps) {
|
|
10164
|
+
async function skillCopiesRow(globalEntries, replaced, deps) {
|
|
9823
10165
|
const id = "skill-copies";
|
|
9824
|
-
const label =
|
|
10166
|
+
const label = COPIES_LABEL;
|
|
9825
10167
|
const cwd = deps.skills.cwd;
|
|
9826
|
-
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name ===
|
|
10168
|
+
const projectEntries = cwd === deps.probe.home ? [] : (await skillsCli(deps.runner).listProject(cwd)).filter((entry) => entry.name === RDE_SKILL_NAME);
|
|
9827
10169
|
const entries = [...globalEntries, ...projectEntries];
|
|
9828
|
-
if (entries.length === 0) return skipped(id, label, "no copy installed");
|
|
10170
|
+
if (entries.length === 0 && replaced.length === 0) return skipped(id, label, "no copy installed");
|
|
9829
10171
|
const origin = skillOrigin(deps.skills.source, cwd);
|
|
9830
10172
|
const judged = [];
|
|
9831
|
-
for (const entry of entries) judged.push(await judgeCopy(entry, origin, deps));
|
|
10173
|
+
for (const entry of entries) judged.push(await judgeCopy(RDE_SKILL_NAME, entry, origin, deps));
|
|
9832
10174
|
const loaded = loadedCopyLine(entries, cwd);
|
|
9833
|
-
const lines = judged.map((copy) => copy.line);
|
|
10175
|
+
const lines = [...judged.map((copy) => copy.line), ...replaced.map((entry) => `${entry.scope} ${entry.path}: replaced by ${RDE_SKILL_NAME}`)];
|
|
9834
10176
|
const detail = (loaded === null ? lines : [...lines, loaded]).join("\n");
|
|
9835
10177
|
const stale = judged.filter((copy) => !copy.current);
|
|
9836
|
-
|
|
10178
|
+
if (stale.length === 0 && replaced.length === 0) return ok(id, label, detail);
|
|
10179
|
+
return staleCopiesRow(stale, replaced.length > 0, detail, deps);
|
|
10180
|
+
}
|
|
10181
|
+
function dataAppSkillsFix({ current, legacy }) {
|
|
10182
|
+
const command = `\`rde init --only ${SKILL_STEP}\``;
|
|
10183
|
+
const removal = `the copies ${DATA_APPS_SKILL_NAME} replaces`;
|
|
10184
|
+
if (!legacy) return `${command} installs it`;
|
|
10185
|
+
return current ? `${command} removes ${removal}` : `${command} installs it and removes ${removal}`;
|
|
10186
|
+
}
|
|
10187
|
+
async function dataAppSkillsRow(installed, deps) {
|
|
10188
|
+
const id = "data-app-skills";
|
|
10189
|
+
const label = "data-app skills";
|
|
10190
|
+
const origin = skillOrigin(deps.skills.source, deps.skills.cwd);
|
|
10191
|
+
const lines = [];
|
|
10192
|
+
const copy = installed.find((candidate) => candidate.name === DATA_APPS_SKILL_NAME);
|
|
10193
|
+
let current = false;
|
|
10194
|
+
if (copy === void 0) lines.push(`${DATA_APPS_SKILL_NAME}: not installed`);
|
|
10195
|
+
else {
|
|
10196
|
+
const judged = await judgeCopy(DATA_APPS_SKILL_NAME, copy, origin, deps);
|
|
10197
|
+
lines.push(judged.line);
|
|
10198
|
+
current = judged.current;
|
|
10199
|
+
}
|
|
10200
|
+
const legacy = installed.filter((entry) => isLegacyDataAppCopy(entry.name, entry.source));
|
|
10201
|
+
for (const entry of legacy) lines.push(`${entry.scope} ${entry.path}: replaced by ${DATA_APPS_SKILL_NAME}`);
|
|
10202
|
+
const detail = lines.join("\n");
|
|
10203
|
+
if (current && legacy.length === 0) return ok(id, label, detail);
|
|
10204
|
+
return failed(id, label, {
|
|
10205
|
+
detail,
|
|
10206
|
+
hint: `${dataAppSkillsFix({
|
|
10207
|
+
current,
|
|
10208
|
+
legacy: legacy.length > 0
|
|
10209
|
+
})}; then restart the agent session`,
|
|
10210
|
+
step: SKILL_STEP
|
|
10211
|
+
});
|
|
9837
10212
|
}
|
|
9838
10213
|
async function skillRows(state, deps) {
|
|
9839
|
-
const label =
|
|
10214
|
+
const label = `${RDE_SKILL_NAME} skill`;
|
|
9840
10215
|
if (state === null) return [skipped("skill", label, NO_INSTANCE_REASON)];
|
|
9841
10216
|
if (state.steps[SKILL_STEP]?.status === "skipped") return [skipped("skill", label, `skipped during init; \`rde init --only ${SKILL_STEP}\``)];
|
|
9842
10217
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
9843
10218
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
9844
|
-
const
|
|
9845
|
-
const
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
10219
|
+
const installed = await skillsCli(deps.runner).list();
|
|
10220
|
+
const entries = installed.filter((entry) => entry.name === RDE_SKILL_NAME);
|
|
10221
|
+
const copies = await skillCopiesRow(entries, installed.filter((entry) => isReplacedRdeCopy(entry.name)), deps);
|
|
10222
|
+
const dataApps = await dataAppSkillsRow(installed, deps);
|
|
10223
|
+
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [
|
|
10224
|
+
failed("skill", label, {
|
|
10225
|
+
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
10226
|
+
hint: `\`rde init --only ${SKILL_STEP}\``,
|
|
10227
|
+
step: SKILL_STEP
|
|
10228
|
+
}),
|
|
10229
|
+
copies,
|
|
10230
|
+
dataApps
|
|
10231
|
+
];
|
|
10232
|
+
return [
|
|
10233
|
+
...detected.map((agent) => {
|
|
10234
|
+
const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
|
|
10235
|
+
const agentLabel = `${label} (${agent.displayName})`;
|
|
10236
|
+
if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
|
|
10237
|
+
return ok(skillRowId(agent), agentLabel, entry.path);
|
|
10238
|
+
}),
|
|
10239
|
+
copies,
|
|
10240
|
+
dataApps
|
|
10241
|
+
];
|
|
9857
10242
|
}
|
|
9858
10243
|
function noRuntimeRows() {
|
|
9859
10244
|
return [
|
|
@@ -9898,7 +10283,11 @@ async function collectDoctor(instance, state, secrets, deps) {
|
|
|
9898
10283
|
const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
|
|
9899
10284
|
const mb = await mbFinding(deps);
|
|
9900
10285
|
const profiles = sharedProfileList(deps);
|
|
9901
|
-
const checks = [
|
|
10286
|
+
const checks = [
|
|
10287
|
+
...machine,
|
|
10288
|
+
mb.row,
|
|
10289
|
+
await rdeRow(deps)
|
|
10290
|
+
];
|
|
9902
10291
|
if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE_REASON), skipped("admin", "admin user", NO_INSTANCE_REASON), skipped("admin-password", "admin password", NO_INSTANCE_REASON), skipped("api-key", "API key", NO_INSTANCE_REASON), skipped("secrets-file", "secrets file", NO_INSTANCE_REASON), skipped("state-file", "state file", NO_INSTANCE_REASON), skipped("mb-profile", "mb profile", NO_INSTANCE_REASON), skipped("license", "license", NO_INSTANCE_REASON), skipped("databases", "databases", NO_INSTANCE_REASON), skipped("git-sync", "remote sync", NO_INSTANCE_REASON));
|
|
9903
10292
|
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
9904
10293
|
const rdeProfile = state === null ? null : state.mbProfile;
|
|
@@ -10290,6 +10679,10 @@ const init = defineRdeCommand({
|
|
|
10290
10679
|
type: "string",
|
|
10291
10680
|
description: "Database to connect, a connection string or <engine>=<connection string> when it fits several engines; repeatable. init needs one, and asks for it only at a terminal"
|
|
10292
10681
|
},
|
|
10682
|
+
"database-name": {
|
|
10683
|
+
type: "string",
|
|
10684
|
+
description: "Display name of the --database in the same position, such as the name the production Metabase uses; repeatable"
|
|
10685
|
+
},
|
|
10293
10686
|
"git-dir": {
|
|
10294
10687
|
type: "string",
|
|
10295
10688
|
description: "Directory for the content repository"
|
|
@@ -10347,6 +10740,7 @@ const init = defineRdeCommand({
|
|
|
10347
10740
|
licenseToken: args["license-token"],
|
|
10348
10741
|
skipLicense: args["skip-license"],
|
|
10349
10742
|
database: repeatedValues(rawArgs, "--database"),
|
|
10743
|
+
databaseName: repeatedValues(rawArgs, "--database-name"),
|
|
10350
10744
|
gitDir: args["git-dir"],
|
|
10351
10745
|
gitRemote: args["git-remote"],
|
|
10352
10746
|
gitUrl: args["git-url"],
|
|
@@ -10912,17 +11306,22 @@ async function findMbProfiles(candidates, urls, runner) {
|
|
|
10912
11306
|
}
|
|
10913
11307
|
return found;
|
|
10914
11308
|
}
|
|
11309
|
+
function spokenList(items) {
|
|
11310
|
+
const last = items.at(-1);
|
|
11311
|
+
if (last === void 0 || items.length <= 2) return items.join(" and ");
|
|
11312
|
+
return `${items.slice(0, -1).join(", ")}, and ${last}`;
|
|
11313
|
+
}
|
|
10915
11314
|
function skillLabel(names, entries) {
|
|
10916
11315
|
const agents = [...new Set(entries.flatMap((entry) => entry.agents))];
|
|
10917
11316
|
const noun = names.length === 1 ? "skill" : "skills";
|
|
10918
|
-
return `${names
|
|
11317
|
+
return `${spokenList(names)} ${noun} for ${agents.join(", ")}`;
|
|
10919
11318
|
}
|
|
11319
|
+
const OWNED_SKILL_NAMES = RDE_OWNED_SKILL_NAMES;
|
|
10920
11320
|
async function findSkill(runner, childOutput) {
|
|
10921
11321
|
const skills = skillsCli(runner);
|
|
10922
|
-
const
|
|
10923
|
-
const entries = (await skills.list()).filter((entry) => owned.includes(entry.name));
|
|
11322
|
+
const entries = (await skills.list()).filter((entry) => OWNED_SKILL_NAMES.includes(entry.name));
|
|
10924
11323
|
if (entries.length === 0) return null;
|
|
10925
|
-
const names =
|
|
11324
|
+
const names = OWNED_SKILL_NAMES.filter((name) => entries.some((entry) => entry.name === name));
|
|
10926
11325
|
return {
|
|
10927
11326
|
label: skillLabel(names, entries),
|
|
10928
11327
|
remove: () => skills.remove(names, childOutput)
|
|
@@ -11024,7 +11423,7 @@ function keptOnDisk(options, state, dirPresent) {
|
|
|
11024
11423
|
const kept = [];
|
|
11025
11424
|
if (options.keepData && dirPresent) kept.push(`Instance directory ${instanceDir(options.instance)} (--keep-data); \`rde init\` reinstalls from it`);
|
|
11026
11425
|
if (state?.gitRemote !== null && state?.gitRemote !== void 0) kept.push(`Working directory ${state.gitRemote.workdir} and its git history`);
|
|
11027
|
-
if (!options.removeSkill) kept.push(`The
|
|
11426
|
+
if (!options.removeSkill) kept.push(`The ${spokenList(RDE_INSTALLED_SKILL_NAMES)} skills for your agents; pass --remove-skill to remove them`);
|
|
11028
11427
|
return kept;
|
|
11029
11428
|
}
|
|
11030
11429
|
async function planUninstall(options, deps) {
|
|
@@ -11131,6 +11530,9 @@ function reportLines(report) {
|
|
|
11131
11530
|
function leftBehind(report) {
|
|
11132
11531
|
return [...report.results.flatMap((result) => result.outcome === "failed" ? [result.label] : []), ...report.problems.map((problem) => problem.subject)];
|
|
11133
11532
|
}
|
|
11533
|
+
function uninstallCommandLine(options) {
|
|
11534
|
+
return options.instance === options.defaultInstance ? "rde uninstall" : `rde uninstall --instance ${options.instance}`;
|
|
11535
|
+
}
|
|
11134
11536
|
async function uninstall(options, deps, io) {
|
|
11135
11537
|
const plan = await planUninstall(options, deps);
|
|
11136
11538
|
for (const problem of plan.problems) io.ui.warn(`${problem.subject} ${problem.reason}`);
|
|
@@ -11141,7 +11543,7 @@ async function uninstall(options, deps, io) {
|
|
|
11141
11543
|
problems: plan.problems
|
|
11142
11544
|
};
|
|
11143
11545
|
}
|
|
11144
|
-
io.ui.note(planLines$1(plan).join("\n"),
|
|
11546
|
+
io.ui.note(planLines$1(plan).join("\n"), uninstallCommandLine(options));
|
|
11145
11547
|
if (plan.unpulled !== null) {
|
|
11146
11548
|
io.ui.warn(unpulledWarning(plan.unpulled));
|
|
11147
11549
|
if ((options.yes || !options.terminal) && !options.force) throw new ConfigError(unpulledRefusal(plan.unpulled));
|
|
@@ -11180,7 +11582,7 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11180
11582
|
},
|
|
11181
11583
|
"remove-skill": {
|
|
11182
11584
|
type: "boolean",
|
|
11183
|
-
description: "Also remove the rde
|
|
11585
|
+
description: "Also remove the metabase-data-apps and metabase-rde skills, and the rde and rde-data-apps skills they replace, from your coding agents",
|
|
11184
11586
|
default: false
|
|
11185
11587
|
},
|
|
11186
11588
|
"remove-image": {
|
|
@@ -11201,11 +11603,13 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11201
11603
|
},
|
|
11202
11604
|
interrupted: INTERRUPTED,
|
|
11203
11605
|
async run(args) {
|
|
11204
|
-
const
|
|
11606
|
+
const defaultInstance = await instanceNameOrDefault();
|
|
11607
|
+
const instance = args.instance ?? defaultInstance;
|
|
11205
11608
|
if (!isInstanceName(instance)) throw new ConfigError(`${args.instance === void 0 ? `the default instance in ${globalConfigPath()},` : "--instance"} ${instance} is not an instance name (${INSTANCE_NAME_RULE})`);
|
|
11206
11609
|
const ui = terminalUi();
|
|
11207
11610
|
const report = await uninstall({
|
|
11208
11611
|
instance,
|
|
11612
|
+
defaultInstance,
|
|
11209
11613
|
keepData: args["keep-data"],
|
|
11210
11614
|
removeSkill: args["remove-skill"],
|
|
11211
11615
|
removeImage: args["remove-image"],
|
|
@@ -11231,35 +11635,59 @@ function hostDeps(ctx) {
|
|
|
11231
11635
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
11232
11636
|
timing: DEFAULT_HEALTH_TIMING,
|
|
11233
11637
|
releases: releaseLookup(ctx.signal),
|
|
11234
|
-
now: () => /* @__PURE__ */ new Date()
|
|
11638
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
11639
|
+
skills: (agents) => agentsSkillsStep(agents, "published")
|
|
11235
11640
|
};
|
|
11236
11641
|
}
|
|
11237
|
-
function
|
|
11238
|
-
const
|
|
11239
|
-
|
|
11240
|
-
|
|
11642
|
+
function majorMove(from, to) {
|
|
11643
|
+
const fromMajor = releaseMajor(from);
|
|
11644
|
+
const toMajor = to === null ? null : releaseMajor(to);
|
|
11645
|
+
if (fromMajor === null || toMajor === null || fromMajor === toMajor) return null;
|
|
11646
|
+
return {
|
|
11647
|
+
from: fromMajor,
|
|
11648
|
+
to: toMajor
|
|
11649
|
+
};
|
|
11241
11650
|
}
|
|
11242
11651
|
function releaseOf(tag) {
|
|
11243
11652
|
const match = RELEASE_TAG.exec(tag);
|
|
11244
11653
|
if (match === null || match[1] === void 0) return null;
|
|
11245
11654
|
return semver.coerce(match[1]);
|
|
11246
11655
|
}
|
|
11247
|
-
function
|
|
11248
|
-
if (targetTag
|
|
11656
|
+
function isDowngrade(current, targetTag) {
|
|
11657
|
+
if (isMajorLineTag(targetTag)) {
|
|
11658
|
+
const running = releaseMajor(current);
|
|
11659
|
+
const target = releaseMajor(targetTag);
|
|
11660
|
+
return running !== null && target !== null && target < running;
|
|
11661
|
+
}
|
|
11249
11662
|
const running = releaseOf(current);
|
|
11250
11663
|
const target = releaseOf(targetTag);
|
|
11251
|
-
|
|
11664
|
+
return running !== null && target !== null && semver.lt(target, running);
|
|
11665
|
+
}
|
|
11666
|
+
function downgradeReason(current, targetTag) {
|
|
11667
|
+
if (targetTag === null || !isDowngrade(current, targetTag)) return null;
|
|
11252
11668
|
return `refusing to downgrade from ${current} to ${targetTag}: Metabase migrates its application database forward only; a fresh instance (\`rde uninstall\`, then \`rde init\`) is the way to run an older release`;
|
|
11253
11669
|
}
|
|
11254
11670
|
function backupStamp(now) {
|
|
11255
11671
|
return now.toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
|
|
11256
11672
|
}
|
|
11257
|
-
async function ensureRunning(ctx, deps, client) {
|
|
11258
|
-
await startMetabaseStep(() => deps).run(ctx);
|
|
11673
|
+
async function ensureRunning(ctx, deps, client, accepted) {
|
|
11674
|
+
await startMetabaseStep(() => deps, accepted).run(ctx);
|
|
11259
11675
|
return runningVersion(client);
|
|
11260
11676
|
}
|
|
11261
|
-
function
|
|
11262
|
-
const
|
|
11677
|
+
function requireSupported(version) {
|
|
11678
|
+
const refusal = unsupportedVersionReason(version, developmentMode());
|
|
11679
|
+
if (refusal !== null) throw new UnsupportedMetabaseError(refusal);
|
|
11680
|
+
}
|
|
11681
|
+
function knownTargetVersion(target) {
|
|
11682
|
+
const tag = target.releaseTag;
|
|
11683
|
+
return tag !== null && (isReleaseTag(tag) || isMajorLineTag(tag)) ? tag : null;
|
|
11684
|
+
}
|
|
11685
|
+
const TO_ONE = "`--to <image or release tag>` names a Metabase to move to";
|
|
11686
|
+
const UPGRADE_WAYS_OUT = {
|
|
11687
|
+
named: TO_ONE,
|
|
11688
|
+
unreleased: TO_ONE
|
|
11689
|
+
};
|
|
11690
|
+
function dockerTargetOf(runtime, image, ownImage) {
|
|
11263
11691
|
return {
|
|
11264
11692
|
kind: "docker",
|
|
11265
11693
|
next: {
|
|
@@ -11267,22 +11695,24 @@ function dockerTarget(runtime, to) {
|
|
|
11267
11695
|
image
|
|
11268
11696
|
},
|
|
11269
11697
|
label: image,
|
|
11270
|
-
releaseTag: imageTag(image)
|
|
11698
|
+
releaseTag: imageTag(image),
|
|
11699
|
+
ownImage
|
|
11271
11700
|
};
|
|
11272
11701
|
}
|
|
11702
|
+
async function dockerTarget(ctx, runtime, to, releases) {
|
|
11703
|
+
if (to !== null) return dockerTargetOf(runtime, to, false);
|
|
11704
|
+
if (developmentMode()) return dockerTargetOf(runtime, runtime.image, false);
|
|
11705
|
+
if (!isMetabaseImage(runtime.image)) return dockerTargetOf(runtime, runtime.image, true);
|
|
11706
|
+
return dockerTargetOf(runtime, await releaseImage(ctx, releases, UPGRADE_WAYS_OUT), false);
|
|
11707
|
+
}
|
|
11273
11708
|
async function jarTarget(ctx, runtime, to, releases) {
|
|
11274
11709
|
const spec = to ?? "latest";
|
|
11275
11710
|
const parsed = parseJarSpec(spec);
|
|
11276
|
-
if (parsed === null) throw new ConfigError(`--to ${spec} is not latest,
|
|
11277
|
-
|
|
11278
|
-
|
|
11279
|
-
|
|
11280
|
-
|
|
11281
|
-
source = {
|
|
11282
|
-
kind: "release",
|
|
11283
|
-
version
|
|
11284
|
-
};
|
|
11285
|
-
} else source = parsed;
|
|
11711
|
+
if (parsed === null) throw new ConfigError(`--to ${spec} is not latest, an enterprise release tag such as v1.65.0, or the path of a .jar file`);
|
|
11712
|
+
const source = parsed.kind === "latest" ? {
|
|
11713
|
+
kind: "release",
|
|
11714
|
+
version: await latestJarRelease(ctx, releases, UPGRADE_WAYS_OUT)
|
|
11715
|
+
} : parsed;
|
|
11286
11716
|
return {
|
|
11287
11717
|
kind: "jar",
|
|
11288
11718
|
previous: runtime,
|
|
@@ -11296,7 +11726,7 @@ async function jarTarget(ctx, runtime, to, releases) {
|
|
|
11296
11726
|
}
|
|
11297
11727
|
function resolveTarget(ctx, configured, to, releases) {
|
|
11298
11728
|
switch (configured.runtime.kind) {
|
|
11299
|
-
case "docker": return
|
|
11729
|
+
case "docker": return dockerTarget(ctx, configured.runtime, to, releases);
|
|
11300
11730
|
case "jar": return jarTarget(ctx, configured.runtime, to, releases);
|
|
11301
11731
|
}
|
|
11302
11732
|
}
|
|
@@ -11346,9 +11776,13 @@ const SWITCH_TEXT = {
|
|
|
11346
11776
|
jar: "Metabase stops and starts again from the target jar."
|
|
11347
11777
|
};
|
|
11348
11778
|
function planLines(configured, current, target, backup) {
|
|
11779
|
+
const notes = [];
|
|
11780
|
+
if (target.kind === "docker" && target.ownImage) notes.push("the instance's own image pulled again, since only Metabase's images move to the newest release");
|
|
11781
|
+
if (target.releaseTag === null || !isReleaseTag(target.releaseTag)) notes.push("its version shows once it runs");
|
|
11782
|
+
const annotation = notes.length === 0 ? "" : ` (${notes.join("; ")})`;
|
|
11349
11783
|
return [
|
|
11350
11784
|
`Running: ${current} (${describeRuntime(configured.runtime)})`,
|
|
11351
|
-
`Target: ${target.label}`,
|
|
11785
|
+
`Target: ${target.label}${annotation}`,
|
|
11352
11786
|
`Backup: ${backup}`,
|
|
11353
11787
|
"",
|
|
11354
11788
|
SWITCH_TEXT[configured.runtime.kind],
|
|
@@ -11407,31 +11841,70 @@ async function switchTo(ctx, deps, target) {
|
|
|
11407
11841
|
}
|
|
11408
11842
|
spinner.stop(`Installed ${target.label}`);
|
|
11409
11843
|
}
|
|
11844
|
+
function failedStartTitle(error) {
|
|
11845
|
+
if (error instanceof MetabaseUnreachableError) return "Metabase did not come up on the new version";
|
|
11846
|
+
if (error instanceof UnsupportedMetabaseError) return "The new version is not one rde runs";
|
|
11847
|
+
return null;
|
|
11848
|
+
}
|
|
11410
11849
|
async function startUpgraded(ctx, deps, client, previous, backup) {
|
|
11411
11850
|
try {
|
|
11412
|
-
return await ensureRunning(ctx, deps, client);
|
|
11851
|
+
return await ensureRunning(ctx, deps, client, "supported");
|
|
11413
11852
|
} catch (error) {
|
|
11414
|
-
|
|
11853
|
+
const title = failedStartTitle(error);
|
|
11854
|
+
if (title !== null) ctx.ui.note(restoreLines(ctx, previous, deps.runtime.dataDir, backup).join("\n"), title);
|
|
11415
11855
|
throw error;
|
|
11416
11856
|
}
|
|
11417
11857
|
}
|
|
11858
|
+
async function recreateStale(ctx, deps, previous, target) {
|
|
11859
|
+
const next = target.next;
|
|
11860
|
+
const container = await deps.runtime.docker.containerInspect(next.containerName);
|
|
11861
|
+
const drift = container.exists ? containerEnvDrift(container, next) : [];
|
|
11862
|
+
const renamed = previous.kind === "docker" && previous.image !== next.image;
|
|
11863
|
+
if (drift.length === 0 && !renamed) return false;
|
|
11864
|
+
if (renamed) {
|
|
11865
|
+
ctx.ui.info(`${previous.image} is the image ${next.image} names; creating ${next.containerName} again under ${next.image}`);
|
|
11866
|
+
await ctx.saveState({
|
|
11867
|
+
...ctx.state,
|
|
11868
|
+
runtime: next
|
|
11869
|
+
});
|
|
11870
|
+
}
|
|
11871
|
+
if (drift.length > 0) ctx.ui.info(`${next.containerName} was created with ${drift.join(", ")} set otherwise than rde sets them now; creating it again from the image it runs`);
|
|
11872
|
+
await runtimeFromState(next, deps.runtime).reconfigure();
|
|
11873
|
+
return true;
|
|
11874
|
+
}
|
|
11875
|
+
async function alreadyOn(ctx, deps, client, configured, current, target) {
|
|
11876
|
+
if (target.kind === "docker" && await recreateStale(ctx, deps, configured.runtime, target)) {
|
|
11877
|
+
await ensureRunning(ctx, deps, client, "supported");
|
|
11878
|
+
ctx.ui.success(`Already on ${current}; ${target.next.containerName} runs ${target.label} with rde's settings now`);
|
|
11879
|
+
return {
|
|
11880
|
+
outcome: "recreated",
|
|
11881
|
+
version: current
|
|
11882
|
+
};
|
|
11883
|
+
}
|
|
11884
|
+
ctx.ui.success(`Already on ${current}: ${target.label} is what the instance runs`);
|
|
11885
|
+
return {
|
|
11886
|
+
outcome: "unchanged",
|
|
11887
|
+
version: current
|
|
11888
|
+
};
|
|
11889
|
+
}
|
|
11418
11890
|
async function upgrade(ctx, options, deps = hostDeps) {
|
|
11419
11891
|
const resolved = deps(ctx);
|
|
11420
11892
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
11421
11893
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
11422
|
-
const current = await ensureRunning(ctx, resolved, client);
|
|
11423
11894
|
const target = await resolveTarget(ctx, configured, options.to, resolved.releases);
|
|
11895
|
+
const current = await ensureRunning(ctx, resolved, client, "any");
|
|
11424
11896
|
const refusal = downgradeReason(current, target.releaseTag);
|
|
11425
11897
|
if (refusal !== null) throw new ConfigError(refusal);
|
|
11898
|
+
const known = knownTargetVersion(target);
|
|
11899
|
+
if (known !== null) requireSupported(known);
|
|
11426
11900
|
if (await fetchTarget(ctx, resolved, target, current)) {
|
|
11427
|
-
|
|
11428
|
-
return
|
|
11429
|
-
outcome: "unchanged",
|
|
11430
|
-
version: current
|
|
11431
|
-
};
|
|
11901
|
+
requireSupported(current);
|
|
11902
|
+
return alreadyOn(ctx, resolved, client, configured, current, target);
|
|
11432
11903
|
}
|
|
11433
11904
|
const backup = join(instanceBackupsDir(ctx.state.name), backupStamp(resolved.now()));
|
|
11434
11905
|
ctx.ui.note(planLines(configured, current, target, backup).join("\n"), "rde upgrade");
|
|
11906
|
+
const announced = majorMove(current, target.releaseTag);
|
|
11907
|
+
if (announced !== null) ctx.ui.warn(`This moves Metabase from ${announced.from} to ${announced.to}, a new major version; rde updates your coding agents' skills after the upgrade`);
|
|
11435
11908
|
if (!options.yes) {
|
|
11436
11909
|
if (!await ctx.prompts.confirm({
|
|
11437
11910
|
message: "Upgrade now?",
|
|
@@ -11452,6 +11925,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11452
11925
|
options.onPhase("starting", backup);
|
|
11453
11926
|
const to = await startUpgraded(ctx, resolved, client, configured, backup);
|
|
11454
11927
|
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
11928
|
+
const moved = majorMove(current, to);
|
|
11929
|
+
if (moved !== null) {
|
|
11930
|
+
if (announced === null) ctx.ui.warn(`This moved Metabase from ${moved.from} to ${moved.to}, a new major version`);
|
|
11931
|
+
await runWizard([resolved.skills(options.yes ? DETECT_AGENTS : ASK_AGENTS)], ctx);
|
|
11932
|
+
}
|
|
11455
11933
|
return {
|
|
11456
11934
|
outcome: "upgraded",
|
|
11457
11935
|
from: current,
|
|
@@ -11477,7 +11955,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11477
11955
|
args: {
|
|
11478
11956
|
to: {
|
|
11479
11957
|
type: "string",
|
|
11480
|
-
description: "Docker: image to upgrade to (default: the instance's image
|
|
11958
|
+
description: "Docker: image to upgrade to (default: the major line of the newest Metabase Enterprise release, such as metabase/metabase-enterprise:v1.65.x; the instance's own image pulled again with RDE_DEV=1 or for an image from outside Metabase's repositories); jar: latest (the newest release), a release tag, or a .jar path (default: latest)"
|
|
11481
11959
|
},
|
|
11482
11960
|
yes: {
|
|
11483
11961
|
type: "boolean",
|
|
@@ -11505,11 +11983,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11505
11983
|
//#endregion
|
|
11506
11984
|
//#region src/main.ts
|
|
11507
11985
|
const main = defineCommand({
|
|
11508
|
-
meta:
|
|
11509
|
-
name: "rde",
|
|
11510
|
-
version: "0.1.17",
|
|
11511
|
-
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
11512
|
-
},
|
|
11986
|
+
meta: ROOT_META,
|
|
11513
11987
|
subCommands: {
|
|
11514
11988
|
init,
|
|
11515
11989
|
start,
|