@metabase/rde 0.1.17 → 0.1.18
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 +50 -20
- package/dist/cli.mjs +741 -314
- package/package.json +1 -1
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",
|
|
@@ -537,10 +544,14 @@ function migrateInstanceState(raw) {
|
|
|
537
544
|
//#endregion
|
|
538
545
|
//#region src/env.ts
|
|
539
546
|
const ENV_RDE_HOME = "RDE_HOME";
|
|
540
|
-
const
|
|
547
|
+
const ENV_RDE_DEV = "RDE_DEV";
|
|
541
548
|
function readEnv(name) {
|
|
542
549
|
return process.env[name];
|
|
543
550
|
}
|
|
551
|
+
function readSetEnv(name) {
|
|
552
|
+
const value = readEnv(name);
|
|
553
|
+
return value === void 0 || value.length === 0 ? null : value;
|
|
554
|
+
}
|
|
544
555
|
const ENV_PATH = "PATH";
|
|
545
556
|
const ENV_NO_COLOR = "NO_COLOR";
|
|
546
557
|
const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
|
|
@@ -548,6 +559,9 @@ const ENV_JAVA_HOME = "JAVA_HOME";
|
|
|
548
559
|
const ENV_MB_PROFILE = "MB_PROFILE";
|
|
549
560
|
const ENV_MB_URL = "MB_URL";
|
|
550
561
|
const ENV_MB_API_KEY = "MB_API_KEY";
|
|
562
|
+
function developmentMode() {
|
|
563
|
+
return readSetEnv(ENV_RDE_DEV) === "1";
|
|
564
|
+
}
|
|
551
565
|
function hostEnvironment() {
|
|
552
566
|
return { ...process.env };
|
|
553
567
|
}
|
|
@@ -566,8 +580,8 @@ const INSTANCE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
|
|
|
566
580
|
const INSTANCE_NAME_RULE = "letters, digits, - and _, starting with a letter or digit";
|
|
567
581
|
const HOME_TAG_LENGTH = 8;
|
|
568
582
|
function rdeHome() {
|
|
569
|
-
const override =
|
|
570
|
-
if (override !==
|
|
583
|
+
const override = readSetEnv(ENV_RDE_HOME);
|
|
584
|
+
if (override !== null) return resolve(override);
|
|
571
585
|
return join(homedir(), RDE_DIR_NAME);
|
|
572
586
|
}
|
|
573
587
|
function homeTag(home) {
|
|
@@ -958,6 +972,7 @@ function lookupFailure(error) {
|
|
|
958
972
|
}
|
|
959
973
|
function createHttp(options) {
|
|
960
974
|
const fetchImpl = options.fetchImpl ?? fetch;
|
|
975
|
+
const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
|
|
961
976
|
const baseUrl = options.baseUrl.replace(/\/+$/, "");
|
|
962
977
|
async function send(method, path, body) {
|
|
963
978
|
const url = `${baseUrl}${path}`;
|
|
@@ -968,7 +983,7 @@ function createHttp(options) {
|
|
|
968
983
|
const init = {
|
|
969
984
|
method,
|
|
970
985
|
headers,
|
|
971
|
-
signal: AbortSignal.any([options.signal, AbortSignal.timeout(
|
|
986
|
+
signal: AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)])
|
|
972
987
|
};
|
|
973
988
|
if (body !== void 0) {
|
|
974
989
|
headers["content-type"] = JSON_CONTENT_TYPE;
|
|
@@ -982,7 +997,7 @@ function createHttp(options) {
|
|
|
982
997
|
};
|
|
983
998
|
} catch (error) {
|
|
984
999
|
if (options.signal.aborted) throw new AbortError();
|
|
985
|
-
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${
|
|
1000
|
+
if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${timeoutMs}ms`);
|
|
986
1001
|
throw new MetabaseUnreachableError(url, describeCause(error));
|
|
987
1002
|
}
|
|
988
1003
|
}
|
|
@@ -2200,18 +2215,74 @@ async function detectAgents(probe) {
|
|
|
2200
2215
|
}
|
|
2201
2216
|
//#endregion
|
|
2202
2217
|
//#region src/metabase/domain/version-info.ts
|
|
2203
|
-
const
|
|
2218
|
+
const VersionEntry = z.object({ version: z.string() }).loose();
|
|
2219
|
+
const VersionInfo = z.object({ latest: VersionEntry }).loose();
|
|
2220
|
+
const VersionHistory = z.object({
|
|
2221
|
+
latest: VersionEntry,
|
|
2222
|
+
older: z.array(VersionEntry)
|
|
2223
|
+
}).loose();
|
|
2204
2224
|
//#endregion
|
|
2205
2225
|
//#region src/metabase/releases.ts
|
|
2206
2226
|
const DOWNLOADS_HOST = "https://downloads.metabase.com";
|
|
2207
2227
|
const VERSION_INFO_HOST = "https://static.metabase.com";
|
|
2208
2228
|
const VERSION_INFO_PATH = "/version-info-ee.json";
|
|
2209
2229
|
const JAR_FILE = "metabase.jar";
|
|
2210
|
-
const RELEASE_TAG$1 = /^v(?<edition>[01])\.\d
|
|
2230
|
+
const RELEASE_TAG$1 = /^v(?<edition>[01])\.(?<major>\d+)\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
|
|
2231
|
+
const MAJOR_LINE_TAG = /^v(?<edition>[01])\.(?<major>\d+)\.x$/;
|
|
2232
|
+
const FINAL_ENTERPRISE_RELEASE = /^v1\.\d+\.\d+(?:\.\d+)?$/;
|
|
2211
2233
|
const ENTERPRISE_EDITION = "1";
|
|
2234
|
+
const OPEN_SOURCE_EDITION = "0";
|
|
2235
|
+
const SNAPSHOT_SUFFIX = "-SNAPSHOT";
|
|
2212
2236
|
function isReleaseTag(value) {
|
|
2213
2237
|
return RELEASE_TAG$1.test(value);
|
|
2214
2238
|
}
|
|
2239
|
+
function isMajorLineTag(value) {
|
|
2240
|
+
return MAJOR_LINE_TAG.test(value);
|
|
2241
|
+
}
|
|
2242
|
+
function majorLineTag(major) {
|
|
2243
|
+
return `v${ENTERPRISE_EDITION}.${major}.x`;
|
|
2244
|
+
}
|
|
2245
|
+
function tagGroups(tag) {
|
|
2246
|
+
return (RELEASE_TAG$1.exec(tag) ?? MAJOR_LINE_TAG.exec(tag))?.groups;
|
|
2247
|
+
}
|
|
2248
|
+
function isOpenSourceRelease(version) {
|
|
2249
|
+
return tagGroups(version)?.["edition"] === OPEN_SOURCE_EDITION;
|
|
2250
|
+
}
|
|
2251
|
+
function isMasterBuild(version) {
|
|
2252
|
+
return !(isReleaseTag(version) || isMajorLineTag(version)) || version.endsWith(SNAPSHOT_SUFFIX);
|
|
2253
|
+
}
|
|
2254
|
+
function releaseMajor(version) {
|
|
2255
|
+
const major = tagGroups(version)?.["major"];
|
|
2256
|
+
return major === void 0 ? null : Number.parseInt(major, 10);
|
|
2257
|
+
}
|
|
2258
|
+
function releaseNumbers(version) {
|
|
2259
|
+
return version.slice(1).split(".").map((part) => Number.parseInt(part, 10));
|
|
2260
|
+
}
|
|
2261
|
+
function compareReleases(left, right) {
|
|
2262
|
+
const a = releaseNumbers(left);
|
|
2263
|
+
const b = releaseNumbers(right);
|
|
2264
|
+
for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
|
|
2265
|
+
const difference = (a[index] ?? 0) - (b[index] ?? 0);
|
|
2266
|
+
if (difference !== 0) return difference;
|
|
2267
|
+
}
|
|
2268
|
+
return 0;
|
|
2269
|
+
}
|
|
2270
|
+
function supportedRelease(version) {
|
|
2271
|
+
if (!FINAL_ENTERPRISE_RELEASE.test(version)) return null;
|
|
2272
|
+
const major = releaseMajor(version);
|
|
2273
|
+
return major !== null && major >= 65 ? {
|
|
2274
|
+
version,
|
|
2275
|
+
major
|
|
2276
|
+
} : null;
|
|
2277
|
+
}
|
|
2278
|
+
function newestSupportedRelease(versions) {
|
|
2279
|
+
let newest = null;
|
|
2280
|
+
for (const version of versions) {
|
|
2281
|
+
const release = supportedRelease(version);
|
|
2282
|
+
if (release !== null && (newest === null || compareReleases(version, newest.version) > 0)) newest = release;
|
|
2283
|
+
}
|
|
2284
|
+
return newest;
|
|
2285
|
+
}
|
|
2215
2286
|
function jarDownloadUrl(version) {
|
|
2216
2287
|
const edition = RELEASE_TAG$1.exec(version)?.groups?.["edition"];
|
|
2217
2288
|
if (edition === void 0) throw new Error(`${version} is not a Metabase release tag`);
|
|
@@ -2224,9 +2295,15 @@ function releaseLookup(signal, options = {}) {
|
|
|
2224
2295
|
signal,
|
|
2225
2296
|
...options
|
|
2226
2297
|
});
|
|
2227
|
-
return {
|
|
2228
|
-
|
|
2229
|
-
|
|
2298
|
+
return {
|
|
2299
|
+
async latestVersion() {
|
|
2300
|
+
return (await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionInfo })).latest.version;
|
|
2301
|
+
},
|
|
2302
|
+
async newestSupportedRelease() {
|
|
2303
|
+
const info = await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionHistory });
|
|
2304
|
+
return newestSupportedRelease([info.latest, ...info.older].map((entry) => entry.version));
|
|
2305
|
+
}
|
|
2306
|
+
};
|
|
2230
2307
|
}
|
|
2231
2308
|
function fileDownloader(signal, options = {}) {
|
|
2232
2309
|
return (url, destination, onProgress) => downloadFile(url, destination, {
|
|
@@ -2294,11 +2371,13 @@ const PortBinding = z.array(z.object({
|
|
|
2294
2371
|
HostIp: z.string(),
|
|
2295
2372
|
HostPort: z.string()
|
|
2296
2373
|
}).loose()).nullable();
|
|
2374
|
+
const EnvEntry = z.string().regex(/^[^=]+=/, "expected NAME=value");
|
|
2297
2375
|
const InspectedContainer = z.object({
|
|
2298
2376
|
State: z.object({ Running: z.boolean() }).loose(),
|
|
2299
2377
|
Config: z.object({
|
|
2300
2378
|
Image: z.string(),
|
|
2301
|
-
Labels: z.record(z.string(), z.string()).nullish()
|
|
2379
|
+
Labels: z.record(z.string(), z.string()).nullish(),
|
|
2380
|
+
Env: z.array(EnvEntry).nullish()
|
|
2302
2381
|
}).loose(),
|
|
2303
2382
|
HostConfig: z.object({ PortBindings: z.record(z.string(), PortBinding).nullable() }).loose(),
|
|
2304
2383
|
NetworkSettings: z.object({ Ports: z.record(z.string(), PortBinding).nullish() }).loose()
|
|
@@ -2329,6 +2408,14 @@ function portMappings(bindings) {
|
|
|
2329
2408
|
}
|
|
2330
2409
|
return mappings;
|
|
2331
2410
|
}
|
|
2411
|
+
function containerEnv(entries) {
|
|
2412
|
+
const env = {};
|
|
2413
|
+
for (const entry of entries) {
|
|
2414
|
+
const separator = entry.indexOf("=");
|
|
2415
|
+
env[entry.slice(0, separator)] = entry.slice(separator + 1);
|
|
2416
|
+
}
|
|
2417
|
+
return env;
|
|
2418
|
+
}
|
|
2332
2419
|
function createArgs(options) {
|
|
2333
2420
|
const args = [
|
|
2334
2421
|
"create",
|
|
@@ -2420,7 +2507,8 @@ function dockerTool(runner) {
|
|
|
2420
2507
|
image: labels?.[IMAGE_LABEL] ?? container.Config.Image,
|
|
2421
2508
|
ports: portMappings(container.HostConfig.PortBindings),
|
|
2422
2509
|
forwarded: portMappings(container.NetworkSettings.Ports ?? null),
|
|
2423
|
-
home: labels?.[HOME_LABEL] ?? null
|
|
2510
|
+
home: labels?.[HOME_LABEL] ?? null,
|
|
2511
|
+
env: containerEnv(container.Config.Env ?? [])
|
|
2424
2512
|
};
|
|
2425
2513
|
}
|
|
2426
2514
|
async function portClaims() {
|
|
@@ -2617,6 +2705,10 @@ const PLUGINS_DIR = "plugins";
|
|
|
2617
2705
|
const METABASE_STOP_GRACE_MS = 3e4;
|
|
2618
2706
|
const DB_DIR = "metabase.db";
|
|
2619
2707
|
const DB_FILE = "metabase.db";
|
|
2708
|
+
const STAGING_STORE_ENV = {
|
|
2709
|
+
MB_RUN_MODE: "e2e",
|
|
2710
|
+
MB_STORE_USE_STAGING: "true"
|
|
2711
|
+
};
|
|
2620
2712
|
function instanceUrl(port) {
|
|
2621
2713
|
return `http://localhost:${port}`;
|
|
2622
2714
|
}
|
|
@@ -2625,13 +2717,15 @@ function serverEnv(options) {
|
|
|
2625
2717
|
MB_DB_FILE: options.dbFile,
|
|
2626
2718
|
MB_PLUGINS_DIR: options.pluginsDir,
|
|
2627
2719
|
MB_SITE_URL: instanceUrl(options.port),
|
|
2628
|
-
|
|
2629
|
-
MB_STORE_USE_STAGING: "true",
|
|
2720
|
+
...developmentMode() ? STAGING_STORE_ENV : {},
|
|
2630
2721
|
MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all",
|
|
2631
2722
|
MB_LOAD_SAMPLE_CONTENT: "false",
|
|
2632
2723
|
MB_INSTALL_ANALYTICS_DATABASE: "false"
|
|
2633
2724
|
};
|
|
2634
2725
|
}
|
|
2726
|
+
function envDrift(actual, expected) {
|
|
2727
|
+
return [.../* @__PURE__ */ new Set([...Object.keys(expected), ...Object.keys(STAGING_STORE_ENV)])].filter((name) => actual[name] !== expected[name]);
|
|
2728
|
+
}
|
|
2635
2729
|
function jettyEnv(port) {
|
|
2636
2730
|
return {
|
|
2637
2731
|
MB_JETTY_HOST: LOOPBACK_HOST,
|
|
@@ -2640,7 +2734,34 @@ function jettyEnv(port) {
|
|
|
2640
2734
|
}
|
|
2641
2735
|
//#endregion
|
|
2642
2736
|
//#region src/runtime/docker.ts
|
|
2643
|
-
const
|
|
2737
|
+
const RELEASE_REPOSITORY = "metabase/metabase-enterprise";
|
|
2738
|
+
const DEVELOPMENT_IMAGE = "metabase/metabase-enterprise-head:latest";
|
|
2739
|
+
const HEAD_SUFFIX = "-head";
|
|
2740
|
+
const METABASE_REPOSITORIES = /* @__PURE__ */ new Set([
|
|
2741
|
+
RELEASE_REPOSITORY,
|
|
2742
|
+
`${RELEASE_REPOSITORY}${HEAD_SUFFIX}`,
|
|
2743
|
+
"metabase/metabase"
|
|
2744
|
+
]);
|
|
2745
|
+
function majorLineImage(major) {
|
|
2746
|
+
return `${RELEASE_REPOSITORY}:${majorLineTag(major)}`;
|
|
2747
|
+
}
|
|
2748
|
+
function imageTag(ref) {
|
|
2749
|
+
const colon = ref.lastIndexOf(":");
|
|
2750
|
+
if (colon === -1 || colon < ref.lastIndexOf("/")) return null;
|
|
2751
|
+
return ref.slice(colon + 1);
|
|
2752
|
+
}
|
|
2753
|
+
function imageRepository(ref) {
|
|
2754
|
+
const digest = ref.indexOf("@");
|
|
2755
|
+
const named = digest === -1 ? ref : ref.slice(0, digest);
|
|
2756
|
+
const tag = imageTag(named);
|
|
2757
|
+
return tag === null ? named : named.slice(0, named.length - tag.length - 1);
|
|
2758
|
+
}
|
|
2759
|
+
function isMasterBuildImage(ref) {
|
|
2760
|
+
return imageRepository(ref).endsWith(HEAD_SUFFIX);
|
|
2761
|
+
}
|
|
2762
|
+
function isMetabaseImage(ref) {
|
|
2763
|
+
return METABASE_REPOSITORIES.has(imageRepository(ref));
|
|
2764
|
+
}
|
|
2644
2765
|
const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
|
|
2645
2766
|
const CONTAINER_PORT = 3e3;
|
|
2646
2767
|
const STOP = { graceMs: METABASE_STOP_GRACE_MS };
|
|
@@ -2655,6 +2776,16 @@ function onLoopback(mappings, port) {
|
|
|
2655
2776
|
function hostUserFor(platform, hostUser) {
|
|
2656
2777
|
return platform === "linux" ? hostUser : null;
|
|
2657
2778
|
}
|
|
2779
|
+
function containerServerEnv(port) {
|
|
2780
|
+
return serverEnv({
|
|
2781
|
+
dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
|
|
2782
|
+
pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
|
|
2783
|
+
port
|
|
2784
|
+
});
|
|
2785
|
+
}
|
|
2786
|
+
function containerEnvDrift(container, state) {
|
|
2787
|
+
return envDrift(container.env, containerServerEnv(state.port));
|
|
2788
|
+
}
|
|
2658
2789
|
function dockerRuntime(state, deps) {
|
|
2659
2790
|
const { docker } = deps;
|
|
2660
2791
|
const url = instanceUrl(state.port);
|
|
@@ -2681,11 +2812,7 @@ function dockerRuntime(state, deps) {
|
|
|
2681
2812
|
source: deps.dataDir,
|
|
2682
2813
|
target: CONTAINER_DATA_DIR
|
|
2683
2814
|
},
|
|
2684
|
-
env:
|
|
2685
|
-
dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
|
|
2686
|
-
pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
|
|
2687
|
-
port: state.port
|
|
2688
|
-
}),
|
|
2815
|
+
env: containerServerEnv(state.port),
|
|
2689
2816
|
addHostGateway: deps.platform === "linux",
|
|
2690
2817
|
hostUser: hostUserFor(deps.platform, deps.hostUser)
|
|
2691
2818
|
};
|
|
@@ -2791,6 +2918,7 @@ function parseJarSpec(value) {
|
|
|
2791
2918
|
path: resolve(trimmed)
|
|
2792
2919
|
};
|
|
2793
2920
|
const tagged = trimmed.startsWith(TAG_PREFIX) ? trimmed : `${TAG_PREFIX}${trimmed}`;
|
|
2921
|
+
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
2922
|
return isReleaseTag(tagged) ? {
|
|
2795
2923
|
kind: "release",
|
|
2796
2924
|
version: tagged
|
|
@@ -3277,7 +3405,7 @@ function adminUserStep(options, deps = hostDeps$14) {
|
|
|
3277
3405
|
//#region src/tools/mb.ts
|
|
3278
3406
|
const MB = "mb";
|
|
3279
3407
|
const MB_PACKAGE = "@metabase/cli";
|
|
3280
|
-
const MB_DIST_TAG = "
|
|
3408
|
+
const MB_DIST_TAG = "latest";
|
|
3281
3409
|
const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
|
|
3282
3410
|
const GLOBAL_INSTALL = {
|
|
3283
3411
|
npm: {
|
|
@@ -3408,15 +3536,11 @@ function mbRemoveCommand(binary) {
|
|
|
3408
3536
|
const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
|
|
3409
3537
|
return name === null ? null : REMOVE_COMMANDS[owner](name);
|
|
3410
3538
|
}
|
|
3411
|
-
function setValue(name) {
|
|
3412
|
-
const value = readEnv(name);
|
|
3413
|
-
return value === void 0 || value.length === 0 ? null : value;
|
|
3414
|
-
}
|
|
3415
3539
|
function mbEnvironment() {
|
|
3416
3540
|
return {
|
|
3417
|
-
profile:
|
|
3418
|
-
url:
|
|
3419
|
-
apiKeySet:
|
|
3541
|
+
profile: readSetEnv(ENV_MB_PROFILE),
|
|
3542
|
+
url: readSetEnv(ENV_MB_URL),
|
|
3543
|
+
apiKeySet: readSetEnv(ENV_MB_API_KEY) !== null
|
|
3420
3544
|
};
|
|
3421
3545
|
}
|
|
3422
3546
|
const ProbedUser = z.object({
|
|
@@ -5728,14 +5852,201 @@ function gitTool(runner) {
|
|
|
5728
5852
|
};
|
|
5729
5853
|
}
|
|
5730
5854
|
//#endregion
|
|
5855
|
+
//#region src/steps/license.ts
|
|
5856
|
+
const STEP_ID$7 = "license";
|
|
5857
|
+
const LICENSE_KEYS = { token: `${STEP_ID$7}.token` };
|
|
5858
|
+
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
5859
|
+
const NOT_FOUND_STATUS$1 = 404;
|
|
5860
|
+
const TOKEN_REFUSED_STATUS = 400;
|
|
5861
|
+
const STORE_UNREACHABLE_STATUS = 503;
|
|
5862
|
+
const TOKEN_CHECK_HOST = "token-check.metabase.com";
|
|
5863
|
+
const ACTIVATION_FAILED = "License activation failed";
|
|
5864
|
+
const LICENSED_FEATURES = {
|
|
5865
|
+
[REMOTE_SYNC_FEATURE]: "remote sync",
|
|
5866
|
+
"data-apps": "data apps",
|
|
5867
|
+
"transforms-testing": "transform tests"
|
|
5868
|
+
};
|
|
5869
|
+
const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
|
|
5870
|
+
function licensedFeatureCoverage(features) {
|
|
5871
|
+
const entries = Object.entries(LICENSED_FEATURES);
|
|
5872
|
+
return {
|
|
5873
|
+
included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
|
|
5874
|
+
missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
|
|
5875
|
+
};
|
|
5876
|
+
}
|
|
5877
|
+
function listOf(names, type) {
|
|
5878
|
+
return new Intl.ListFormat("en", { type }).format(names);
|
|
5879
|
+
}
|
|
5880
|
+
function hostDeps$12(ctx) {
|
|
5881
|
+
return {
|
|
5882
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5883
|
+
runner: processRunner(ctx.signal),
|
|
5884
|
+
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
|
|
5885
|
+
terminal: stdinIsTty()
|
|
5886
|
+
};
|
|
5887
|
+
}
|
|
5888
|
+
function isNoTokenYet(error) {
|
|
5889
|
+
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
5890
|
+
return isMetabaseNotServing(error);
|
|
5891
|
+
}
|
|
5892
|
+
function validateToken(value) {
|
|
5893
|
+
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
5894
|
+
return "A license token is 64 hexadecimal characters";
|
|
5895
|
+
}
|
|
5896
|
+
function requireApiKey$1(ctx) {
|
|
5897
|
+
if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
5898
|
+
return ctx.secrets.apiKey;
|
|
5899
|
+
}
|
|
5900
|
+
function licenseOf(status) {
|
|
5901
|
+
if (!status.valid) return { activated: false };
|
|
5902
|
+
return {
|
|
5903
|
+
activated: true,
|
|
5904
|
+
features: status.features,
|
|
5905
|
+
validThru: status["valid-thru"] ?? null
|
|
5906
|
+
};
|
|
5907
|
+
}
|
|
5908
|
+
async function liveLicense(client) {
|
|
5909
|
+
try {
|
|
5910
|
+
return licenseOf(await client.tokenStatus.get());
|
|
5911
|
+
} catch (error) {
|
|
5912
|
+
if (error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1) return { activated: false };
|
|
5913
|
+
throw error;
|
|
5914
|
+
}
|
|
5915
|
+
}
|
|
5916
|
+
async function recordLiveLicense(ctx, client) {
|
|
5917
|
+
const live = await liveLicense(client);
|
|
5918
|
+
if (!isDeepStrictEqual(live, ctx.state.license)) await ctx.saveState({
|
|
5919
|
+
...ctx.state,
|
|
5920
|
+
license: live
|
|
5921
|
+
});
|
|
5922
|
+
return live;
|
|
5923
|
+
}
|
|
5924
|
+
function activeLabel(status) {
|
|
5925
|
+
const plan = status["plan-alias"];
|
|
5926
|
+
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
5927
|
+
}
|
|
5928
|
+
function refusalOf(error) {
|
|
5929
|
+
if (!(error instanceof MetabaseApiError)) return null;
|
|
5930
|
+
if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
|
|
5931
|
+
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`);
|
|
5932
|
+
return null;
|
|
5933
|
+
}
|
|
5934
|
+
async function activate(client, token) {
|
|
5935
|
+
try {
|
|
5936
|
+
await client.setting.setPremiumEmbeddingToken(token);
|
|
5937
|
+
} catch (error) {
|
|
5938
|
+
const refusal = refusalOf(error);
|
|
5939
|
+
if (refusal === null) throw error;
|
|
5940
|
+
return {
|
|
5941
|
+
kind: "refused",
|
|
5942
|
+
error: refusal
|
|
5943
|
+
};
|
|
5944
|
+
}
|
|
5945
|
+
const status = await client.tokenStatus.get();
|
|
5946
|
+
if (!status.valid) return {
|
|
5947
|
+
kind: "refused",
|
|
5948
|
+
error: new ConfigError(`Metabase reports the token as ${status.status}`)
|
|
5949
|
+
};
|
|
5950
|
+
return {
|
|
5951
|
+
kind: "activated",
|
|
5952
|
+
status
|
|
5953
|
+
};
|
|
5954
|
+
}
|
|
5955
|
+
async function activateAskedToken(ctx, client, terminal) {
|
|
5956
|
+
const token = await ctx.prompts.password({
|
|
5957
|
+
key: LICENSE_KEYS.token,
|
|
5958
|
+
message: "Paste your Metabase license token (Enter to skip)",
|
|
5959
|
+
validate: validateToken
|
|
5960
|
+
});
|
|
5961
|
+
if (token.length === 0) return null;
|
|
5962
|
+
const spinner = ctx.ui.spinner("Activating the license…");
|
|
5963
|
+
let activation;
|
|
5964
|
+
try {
|
|
5965
|
+
activation = await activate(client, token);
|
|
5966
|
+
} catch (error) {
|
|
5967
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
5968
|
+
throw error;
|
|
5969
|
+
}
|
|
5970
|
+
if (activation.kind === "activated") {
|
|
5971
|
+
spinner.stop(activeLabel(activation.status));
|
|
5972
|
+
ctx.session.licenseToken.set(token);
|
|
5973
|
+
return activation.status;
|
|
5974
|
+
}
|
|
5975
|
+
spinner.fail(ACTIVATION_FAILED);
|
|
5976
|
+
if (!terminal) throw activation.error;
|
|
5977
|
+
ctx.ui.warn(activation.error.message);
|
|
5978
|
+
return activateAskedToken(ctx, client, terminal);
|
|
5979
|
+
}
|
|
5980
|
+
function reportFeatures(ctx, features) {
|
|
5981
|
+
const { missing } = licensedFeatureCoverage(features);
|
|
5982
|
+
const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
|
|
5983
|
+
if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
|
|
5984
|
+
if (missing.length > 0) {
|
|
5985
|
+
const gitNote = remoteSync ? "" : "; the git step will be skipped";
|
|
5986
|
+
ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
|
|
5987
|
+
}
|
|
5988
|
+
}
|
|
5989
|
+
function licenseStep(deps = hostDeps$12) {
|
|
5990
|
+
return {
|
|
5991
|
+
id: STEP_ID$7,
|
|
5992
|
+
title: "Activate your license",
|
|
5993
|
+
redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
|
|
5994
|
+
async detect(ctx) {
|
|
5995
|
+
if (ctx.state.steps[STEP_ID$7]?.status === "skipped") return "not-applicable";
|
|
5996
|
+
const configured = configuredRuntime(ctx.state);
|
|
5997
|
+
if (configured === null || ctx.secrets.apiKey === null) return "todo";
|
|
5998
|
+
const resolved = deps(ctx);
|
|
5999
|
+
if (!await resolved.instanceRunning()) return "todo";
|
|
6000
|
+
const client = resolved.client(configured.url, {
|
|
6001
|
+
kind: "api-key",
|
|
6002
|
+
key: ctx.secrets.apiKey
|
|
6003
|
+
});
|
|
6004
|
+
try {
|
|
6005
|
+
return (await client.tokenStatus.get()).valid ? "done" : "todo";
|
|
6006
|
+
} catch (error) {
|
|
6007
|
+
if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
|
|
6008
|
+
throw error;
|
|
6009
|
+
}
|
|
6010
|
+
},
|
|
6011
|
+
async run(ctx) {
|
|
6012
|
+
const configured = requireConfiguredRuntime(ctx.state);
|
|
6013
|
+
const apiKey = requireApiKey$1(ctx);
|
|
6014
|
+
const resolved = deps(ctx);
|
|
6015
|
+
const client = resolved.client(configured.url, {
|
|
6016
|
+
kind: "api-key",
|
|
6017
|
+
key: apiKey
|
|
6018
|
+
});
|
|
6019
|
+
const status = await activateAskedToken(ctx, client, resolved.terminal);
|
|
6020
|
+
if (status === null) {
|
|
6021
|
+
await recordLiveLicense(ctx, client);
|
|
6022
|
+
const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
|
|
6023
|
+
ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
|
|
6024
|
+
return { outcome: "skipped" };
|
|
6025
|
+
}
|
|
6026
|
+
await ctx.saveState({
|
|
6027
|
+
...ctx.state,
|
|
6028
|
+
license: licenseOf(status)
|
|
6029
|
+
});
|
|
6030
|
+
const mb = mbTool(resolved.runner);
|
|
6031
|
+
if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
|
|
6032
|
+
url: configured.url,
|
|
6033
|
+
profile: ctx.state.mbProfile,
|
|
6034
|
+
apiKey
|
|
6035
|
+
});
|
|
6036
|
+
reportFeatures(ctx, status.features);
|
|
6037
|
+
return { outcome: "completed" };
|
|
6038
|
+
}
|
|
6039
|
+
};
|
|
6040
|
+
}
|
|
6041
|
+
//#endregion
|
|
5731
6042
|
//#region src/steps/git-sync.ts
|
|
5732
|
-
const STEP_ID$
|
|
6043
|
+
const STEP_ID$6 = "git-sync";
|
|
5733
6044
|
const GIT_SYNC_KEYS = {
|
|
5734
|
-
location: `${STEP_ID$
|
|
5735
|
-
path: `${STEP_ID$
|
|
5736
|
-
remote: `${STEP_ID$
|
|
5737
|
-
url: `${STEP_ID$
|
|
5738
|
-
token: `${STEP_ID$
|
|
6045
|
+
location: `${STEP_ID$6}.location`,
|
|
6046
|
+
path: `${STEP_ID$6}.path`,
|
|
6047
|
+
remote: `${STEP_ID$6}.remote`,
|
|
6048
|
+
url: `${STEP_ID$6}.url`,
|
|
6049
|
+
token: `${STEP_ID$6}.token`
|
|
5739
6050
|
};
|
|
5740
6051
|
const BARE_REPO_NAME = "content.git";
|
|
5741
6052
|
const DEFAULT_BRANCH = "main";
|
|
@@ -5769,7 +6080,7 @@ const SYNC_MODE_SETTINGS = {
|
|
|
5769
6080
|
"remote-sync-type": "read-write",
|
|
5770
6081
|
"remote-sync-transforms": true
|
|
5771
6082
|
};
|
|
5772
|
-
function hostDeps$
|
|
6083
|
+
function hostDeps$11(ctx) {
|
|
5773
6084
|
return {
|
|
5774
6085
|
runner: processRunner(ctx.signal),
|
|
5775
6086
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -6206,6 +6517,9 @@ function configureRemote(run, runtime, model) {
|
|
|
6206
6517
|
function isMbUnavailable(error) {
|
|
6207
6518
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
6208
6519
|
}
|
|
6520
|
+
function remoteSyncGap(license) {
|
|
6521
|
+
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";
|
|
6522
|
+
}
|
|
6209
6523
|
async function runGitSync(ctx, deps) {
|
|
6210
6524
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
6211
6525
|
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
@@ -6220,6 +6534,8 @@ async function runGitSync(ctx, deps) {
|
|
|
6220
6534
|
}),
|
|
6221
6535
|
instance
|
|
6222
6536
|
};
|
|
6537
|
+
const license = await recordLiveLicense(ctx, run.client);
|
|
6538
|
+
if (!hasRemoteSync(license)) throw new ConfigError(`${remoteSyncGap(license)}; \`rde init --from license\` activates one`);
|
|
6223
6539
|
const flow = await runFlow({
|
|
6224
6540
|
remote: remoteScreen(run),
|
|
6225
6541
|
hosted: hostedScreen(run),
|
|
@@ -6239,15 +6555,15 @@ async function runGitSync(ctx, deps) {
|
|
|
6239
6555
|
ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
6240
6556
|
return "completed";
|
|
6241
6557
|
}
|
|
6242
|
-
function gitSyncStep(deps = hostDeps$
|
|
6558
|
+
function gitSyncStep(deps = hostDeps$11) {
|
|
6243
6559
|
return {
|
|
6244
|
-
id: STEP_ID$
|
|
6560
|
+
id: STEP_ID$6,
|
|
6245
6561
|
title: "Connect a git repository",
|
|
6246
6562
|
redoHint: "`rde init --only git-sync` with it points Metabase's remote sync at that repository instead.",
|
|
6247
6563
|
async detect(ctx) {
|
|
6248
6564
|
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$
|
|
6565
|
+
if (ctx.state.steps[STEP_ID$6]?.status === "skipped") return "not-applicable";
|
|
6566
|
+
if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$6]?.status !== "done") return "todo";
|
|
6251
6567
|
const instance = mbInstanceOf(ctx.state, ctx.secrets);
|
|
6252
6568
|
if (instance === null) return "todo";
|
|
6253
6569
|
try {
|
|
@@ -6291,11 +6607,9 @@ function githubContents(signal, options = {}) {
|
|
|
6291
6607
|
//#region src/tools/skills-cli.ts
|
|
6292
6608
|
const NPX = "npx";
|
|
6293
6609
|
const SKILLS_PACKAGE = "skills@latest";
|
|
6294
|
-
const
|
|
6295
|
-
const RDE_SOURCE_SKILL_NAMES = ["rde",
|
|
6610
|
+
const RDE_DATA_APPS_SKILL_NAME = "rde-data-apps";
|
|
6611
|
+
const RDE_SOURCE_SKILL_NAMES = ["rde", RDE_DATA_APPS_SKILL_NAME];
|
|
6296
6612
|
const DATA_APP_REPOSITORY = "metabase/metabase";
|
|
6297
|
-
const DATA_APP_BRANCH = "master";
|
|
6298
|
-
const DATA_APP_SKILLS_SOURCE = `${DATA_APP_REPOSITORY}/skills#${DATA_APP_BRANCH}`;
|
|
6299
6613
|
const DATA_APP_SKILL_NAMES = [
|
|
6300
6614
|
"metabase-data-app-setup",
|
|
6301
6615
|
"metabase-data-app-routing",
|
|
@@ -6304,8 +6618,7 @@ const DATA_APP_SKILL_NAMES = [
|
|
|
6304
6618
|
"metabase-data-app-migrate"
|
|
6305
6619
|
];
|
|
6306
6620
|
function rdeSkillSource() {
|
|
6307
|
-
|
|
6308
|
-
return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
|
|
6621
|
+
return readSetEnv("RDE_SKILL_SOURCE") ?? "https://github.com/metabase/agent-skills/tree/main";
|
|
6309
6622
|
}
|
|
6310
6623
|
const InstalledSkill = z.object({
|
|
6311
6624
|
name: z.string(),
|
|
@@ -6331,8 +6644,7 @@ function projectSkillLockPath(project) {
|
|
|
6331
6644
|
return join(project, PROJECT_LOCK_FILE);
|
|
6332
6645
|
}
|
|
6333
6646
|
function xdgStateHome() {
|
|
6334
|
-
|
|
6335
|
-
return value === void 0 || value.length === 0 ? null : value;
|
|
6647
|
+
return readSetEnv(ENV_XDG_STATE_HOME);
|
|
6336
6648
|
}
|
|
6337
6649
|
function lockedSkill(path, raw, skill) {
|
|
6338
6650
|
if (raw === null) return null;
|
|
@@ -6344,11 +6656,16 @@ const hostSkillFiles = {
|
|
|
6344
6656
|
readJson: readJsonFile,
|
|
6345
6657
|
readText: readTextFile
|
|
6346
6658
|
};
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6659
|
+
function dataAppSkillsSource(ref) {
|
|
6660
|
+
return `${DATA_APP_REPOSITORY}/skills#${ref}`;
|
|
6661
|
+
}
|
|
6662
|
+
function dataAppSkillsOrigin(ref) {
|
|
6663
|
+
return {
|
|
6664
|
+
kind: "github",
|
|
6665
|
+
repository: DATA_APP_REPOSITORY,
|
|
6666
|
+
ref
|
|
6667
|
+
};
|
|
6668
|
+
}
|
|
6352
6669
|
const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
|
|
6353
6670
|
function skillOrigin(source, cwd) {
|
|
6354
6671
|
const github = GITHUB_URL.exec(source);
|
|
@@ -6429,12 +6746,13 @@ function skillsCli(runner) {
|
|
|
6429
6746
|
}
|
|
6430
6747
|
//#endregion
|
|
6431
6748
|
//#region src/steps/agents-skills.ts
|
|
6432
|
-
const STEP_ID$
|
|
6433
|
-
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$
|
|
6749
|
+
const STEP_ID$5 = "agents-skills";
|
|
6750
|
+
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
|
|
6434
6751
|
const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
6435
6752
|
const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
|
|
6436
6753
|
const DETECT_AGENTS = { kind: "detected" };
|
|
6437
|
-
|
|
6754
|
+
const ASK_AGENTS = { kind: "ask" };
|
|
6755
|
+
function hostDeps$10(ctx) {
|
|
6438
6756
|
return {
|
|
6439
6757
|
runner: processRunner(ctx.signal),
|
|
6440
6758
|
probe: hostAgentProbe(),
|
|
@@ -6458,9 +6776,6 @@ function skillCoversAgent(entry, agent) {
|
|
|
6458
6776
|
function coversAny(entry, agents) {
|
|
6459
6777
|
return agents.some((agent) => skillCoversAgent(entry, agent));
|
|
6460
6778
|
}
|
|
6461
|
-
async function rdeSkillEntries(runner) {
|
|
6462
|
-
return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
|
|
6463
|
-
}
|
|
6464
6779
|
function requiredEntries(installed) {
|
|
6465
6780
|
return installed.filter((entry) => REQUIRED_SKILL_NAMES.includes(entry.name));
|
|
6466
6781
|
}
|
|
@@ -6477,12 +6792,26 @@ function coveredAgentNames(entry, agents) {
|
|
|
6477
6792
|
function installedLines(entries, agents) {
|
|
6478
6793
|
return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
|
|
6479
6794
|
}
|
|
6480
|
-
|
|
6795
|
+
const DEVELOPMENT_BRANCH = "master";
|
|
6796
|
+
function releaseBranch(major) {
|
|
6797
|
+
return `release-x.${major}.x`;
|
|
6798
|
+
}
|
|
6799
|
+
function dataAppSkillsRef(metabaseVersion, development) {
|
|
6800
|
+
if (metabaseVersion === null) return releaseBranch(65);
|
|
6801
|
+
if (development && isMasterBuild(metabaseVersion)) return DEVELOPMENT_BRANCH;
|
|
6802
|
+
const major = releaseMajor(metabaseVersion) ?? 65;
|
|
6803
|
+
return releaseBranch(Math.max(major, 65));
|
|
6804
|
+
}
|
|
6805
|
+
function wantedDataAppRef(metabaseVersion) {
|
|
6806
|
+
return dataAppSkillsRef(metabaseVersion, developmentMode());
|
|
6807
|
+
}
|
|
6808
|
+
function skillSets(deps, metabaseVersion) {
|
|
6809
|
+
const dataAppRef = wantedDataAppRef(metabaseVersion);
|
|
6481
6810
|
return [{
|
|
6482
6811
|
label: "data-app",
|
|
6483
|
-
source:
|
|
6484
|
-
sourceNote:
|
|
6485
|
-
origin:
|
|
6812
|
+
source: dataAppSkillsSource(dataAppRef),
|
|
6813
|
+
sourceNote: ` from ${dataAppRef}`,
|
|
6814
|
+
origin: dataAppSkillsOrigin(dataAppRef),
|
|
6486
6815
|
skills: DATA_APP_SKILL_NAMES
|
|
6487
6816
|
}, {
|
|
6488
6817
|
label: "rde",
|
|
@@ -6613,11 +6942,11 @@ function skipLine(all, source) {
|
|
|
6613
6942
|
switch (source) {
|
|
6614
6943
|
case "override":
|
|
6615
6944
|
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$
|
|
6945
|
+
case "installed": return `Skipping the skills; run \`rde init --only ${STEP_ID$5}\` to install it later`;
|
|
6946
|
+
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$5} --agents <name>\` once one is installed`;
|
|
6618
6947
|
}
|
|
6619
6948
|
}
|
|
6620
|
-
function agentsSkillsStep(options, updates, deps = hostDeps$
|
|
6949
|
+
function agentsSkillsStep(options, updates, deps = hostDeps$10) {
|
|
6621
6950
|
let hashes = null;
|
|
6622
6951
|
async function setCheck(resolved) {
|
|
6623
6952
|
hashes ??= hashLookup(resolved.github);
|
|
@@ -6628,16 +6957,16 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6628
6957
|
};
|
|
6629
6958
|
}
|
|
6630
6959
|
return {
|
|
6631
|
-
id: STEP_ID$
|
|
6960
|
+
id: STEP_ID$5,
|
|
6632
6961
|
title: "Install the rde and data-app skills for your coding agents",
|
|
6633
6962
|
redoHint: "`rde init --only agents-skills` with it installs the skills for those agents.",
|
|
6634
6963
|
async detect(ctx) {
|
|
6635
6964
|
const resolved = deps(ctx);
|
|
6636
6965
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
6637
|
-
if (detected.length === 0) return ctx.state.steps[STEP_ID$
|
|
6966
|
+
if (detected.length === 0) return ctx.state.steps[STEP_ID$5]?.status === "skipped" ? "not-applicable" : "todo";
|
|
6638
6967
|
if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
|
|
6639
6968
|
const check = await setCheck(resolved);
|
|
6640
|
-
for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
|
|
6969
|
+
for (const set of skillSets(resolved, ctx.state.version)) if (await setBehind(set, check)) return "outdated";
|
|
6641
6970
|
return "done";
|
|
6642
6971
|
},
|
|
6643
6972
|
async run(ctx) {
|
|
@@ -6654,7 +6983,7 @@ function agentsSkillsStep(options, updates, deps = hostDeps$11) {
|
|
|
6654
6983
|
const before = await installedNow();
|
|
6655
6984
|
const check = await setCheck(resolved);
|
|
6656
6985
|
const pending = [];
|
|
6657
|
-
for (const set of skillSets(resolved)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
|
|
6986
|
+
for (const set of skillSets(resolved, ctx.state.version)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
|
|
6658
6987
|
const names = displayNames(selection.agents);
|
|
6659
6988
|
if (pending.length === 0) {
|
|
6660
6989
|
ctx.ui.success(`The rde and data-app skills are current for ${names}`);
|
|
@@ -6693,7 +7022,7 @@ function hostDatabasesDeps(signal) {
|
|
|
6693
7022
|
now: () => /* @__PURE__ */ new Date()
|
|
6694
7023
|
};
|
|
6695
7024
|
}
|
|
6696
|
-
function requireApiKey
|
|
7025
|
+
function requireApiKey(state, apiKey) {
|
|
6697
7026
|
if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
6698
7027
|
return apiKey;
|
|
6699
7028
|
}
|
|
@@ -6715,7 +7044,7 @@ function authenticatedClient(state, apiKey, deps) {
|
|
|
6715
7044
|
const configured = requireConfiguredRuntime(state);
|
|
6716
7045
|
return deps.client(configured.url, {
|
|
6717
7046
|
kind: "api-key",
|
|
6718
|
-
key: requireApiKey
|
|
7047
|
+
key: requireApiKey(state, apiKey)
|
|
6719
7048
|
});
|
|
6720
7049
|
}
|
|
6721
7050
|
async function listDatabases(state, apiKey, deps) {
|
|
@@ -6791,13 +7120,13 @@ async function connectRequired(ctx, deps, opening) {
|
|
|
6791
7120
|
result: { addedDatabases: connected.added }
|
|
6792
7121
|
};
|
|
6793
7122
|
}
|
|
6794
|
-
function hostDeps$
|
|
7123
|
+
function hostDeps$9(ctx) {
|
|
6795
7124
|
return {
|
|
6796
7125
|
...hostDatabasesDeps(ctx.signal),
|
|
6797
7126
|
instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
|
|
6798
7127
|
};
|
|
6799
7128
|
}
|
|
6800
|
-
function databasesStep(options, deps = hostDeps$
|
|
7129
|
+
function databasesStep(options, deps = hostDeps$9) {
|
|
6801
7130
|
return {
|
|
6802
7131
|
id: "databases",
|
|
6803
7132
|
title: "Connect your databases",
|
|
@@ -6860,7 +7189,7 @@ function npmRegistry(signal, options = {}) {
|
|
|
6860
7189
|
}
|
|
6861
7190
|
//#endregion
|
|
6862
7191
|
//#region src/steps/install-mb.ts
|
|
6863
|
-
const STEP_ID$
|
|
7192
|
+
const STEP_ID$4 = "install-mb";
|
|
6864
7193
|
const MIN_MB_VERSION = "0.3.0";
|
|
6865
7194
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
6866
7195
|
function installedWith(scriptPath) {
|
|
@@ -6869,7 +7198,7 @@ function installedWith(scriptPath) {
|
|
|
6869
7198
|
function rdeInstalledWith() {
|
|
6870
7199
|
return installedWith(process.argv[1]);
|
|
6871
7200
|
}
|
|
6872
|
-
function hostDeps$
|
|
7201
|
+
function hostDeps$8(ctx) {
|
|
6873
7202
|
return {
|
|
6874
7203
|
runner: processRunner(ctx.signal),
|
|
6875
7204
|
installedWith: rdeInstalledWith(),
|
|
@@ -6923,6 +7252,33 @@ function updateTo(release, installed) {
|
|
|
6923
7252
|
function displayCommand(install) {
|
|
6924
7253
|
return [install.command, ...install.args].join(" ");
|
|
6925
7254
|
}
|
|
7255
|
+
const NPM_EACCES_EXIT = 243;
|
|
7256
|
+
const NPM_PERMISSION_TEXT = [
|
|
7257
|
+
"EACCES",
|
|
7258
|
+
"EPERM",
|
|
7259
|
+
"might be a permissions issue"
|
|
7260
|
+
];
|
|
7261
|
+
function deniedGlobalWrite(install, error) {
|
|
7262
|
+
if (install.command !== "npm" || !(error instanceof ExternalToolError)) return false;
|
|
7263
|
+
return NPM_PERMISSION_TEXT.some((text) => error.stderrTail.includes(text)) || error.exitCode === NPM_EACCES_EXIT;
|
|
7264
|
+
}
|
|
7265
|
+
function globalWriteDenied(install) {
|
|
7266
|
+
return new ConfigError([
|
|
7267
|
+
`\`${displayCommand(install)}\` failed because npm cannot write its global directory.`,
|
|
7268
|
+
"Give npm a prefix you own with `npm config set prefix ~/.npm-global`,",
|
|
7269
|
+
"add ~/.npm-global/bin to PATH and open a new terminal,",
|
|
7270
|
+
"or install Node with a version manager such as nvm, fnm or mise;",
|
|
7271
|
+
"then run rde init again."
|
|
7272
|
+
].join(" "));
|
|
7273
|
+
}
|
|
7274
|
+
async function installMb(install, ctx, deps) {
|
|
7275
|
+
try {
|
|
7276
|
+
await runShown(deps.runner, ctx.ui.childOutput, install.command, install.args);
|
|
7277
|
+
} catch (error) {
|
|
7278
|
+
if (deniedGlobalWrite(install, error)) throw globalWriteDenied(install);
|
|
7279
|
+
throw error;
|
|
7280
|
+
}
|
|
7281
|
+
}
|
|
6926
7282
|
async function versionAt(runner, path) {
|
|
6927
7283
|
try {
|
|
6928
7284
|
const raw = await mbTool(runner).version(path);
|
|
@@ -7036,7 +7392,7 @@ async function updateFor(release, installed, deps) {
|
|
|
7036
7392
|
version
|
|
7037
7393
|
};
|
|
7038
7394
|
}
|
|
7039
|
-
function installMbStep(updates, deps = hostDeps$
|
|
7395
|
+
function installMbStep(updates, deps = hostDeps$8) {
|
|
7040
7396
|
let published = null;
|
|
7041
7397
|
function release(resolved) {
|
|
7042
7398
|
if (updates === "installed") return Promise.resolve(UNCHECKED);
|
|
@@ -7044,7 +7400,7 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
7044
7400
|
return published;
|
|
7045
7401
|
}
|
|
7046
7402
|
return {
|
|
7047
|
-
id: STEP_ID$
|
|
7403
|
+
id: STEP_ID$4,
|
|
7048
7404
|
title: "Install the Metabase CLI",
|
|
7049
7405
|
async detect(ctx) {
|
|
7050
7406
|
const resolved = deps(ctx);
|
|
@@ -7071,7 +7427,7 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
7071
7427
|
} else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
7072
7428
|
const install = mbInstallCommand(resolved.installedWith);
|
|
7073
7429
|
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
7074
|
-
await
|
|
7430
|
+
await installMb(install, ctx, resolved);
|
|
7075
7431
|
const after = await inspectMb(resolved.runner);
|
|
7076
7432
|
if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
|
|
7077
7433
|
const stillNewer = updateTo(target, after.version);
|
|
@@ -7085,171 +7441,6 @@ function installMbStep(updates, deps = hostDeps$9) {
|
|
|
7085
7441
|
};
|
|
7086
7442
|
}
|
|
7087
7443
|
//#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
7444
|
//#region src/steps/preflight.ts
|
|
7254
7445
|
const MIN_NODE_VERSION = "20.19.0";
|
|
7255
7446
|
const NODE_RANGE = `>=${MIN_NODE_VERSION}`;
|
|
@@ -7480,22 +7671,56 @@ async function chooseKind(ctx, options, runtime) {
|
|
|
7480
7671
|
ctx.ui.info(`Docker is not available (${available.docker.reason}); using the jar runtime with ${javaLabel(available.java)}`);
|
|
7481
7672
|
return "jar";
|
|
7482
7673
|
}
|
|
7483
|
-
|
|
7674
|
+
const NAME_ONE = "`--image <image>` or `--jar <release tag>` names a Metabase to run";
|
|
7675
|
+
const INIT_WAYS_OUT = {
|
|
7676
|
+
named: NAME_ONE,
|
|
7677
|
+
unreleased: `${NAME_ONE}, and \`RDE_DEV=1\` runs the development build`
|
|
7678
|
+
};
|
|
7679
|
+
function isFeedFailure(error) {
|
|
7680
|
+
return error instanceof MetabaseApiError || error instanceof MetabaseUnreachableError;
|
|
7681
|
+
}
|
|
7682
|
+
async function requireNewestRelease(releases, waysOut) {
|
|
7683
|
+
let newest;
|
|
7684
|
+
try {
|
|
7685
|
+
newest = await releases.newestSupportedRelease();
|
|
7686
|
+
} catch (error) {
|
|
7687
|
+
if (!isFeedFailure(error)) throw error;
|
|
7688
|
+
throw new ConfigError(`the Metabase release feed could not be read (${error.reason}); ${waysOut.named}`);
|
|
7689
|
+
}
|
|
7690
|
+
if (newest === null) throw new ConfigError(`no release of Metabase Enterprise 65 or newer is out yet; ${waysOut.unreleased}`);
|
|
7691
|
+
return newest;
|
|
7692
|
+
}
|
|
7693
|
+
async function releaseImage(ctx, releases, waysOut) {
|
|
7694
|
+
const newest = await requireNewestRelease(releases, waysOut);
|
|
7695
|
+
const image = majorLineImage(newest.major);
|
|
7696
|
+
ctx.ui.info(`The newest Metabase release is ${newest.version}; ${image} carries the newest build of ${newest.major}`);
|
|
7697
|
+
return image;
|
|
7698
|
+
}
|
|
7699
|
+
async function latestJarRelease(ctx, releases, waysOut) {
|
|
7700
|
+
if (developmentMode()) {
|
|
7701
|
+
const version = await releases.latestVersion();
|
|
7702
|
+
ctx.ui.info(`The latest Metabase release is ${version}`);
|
|
7703
|
+
return version;
|
|
7704
|
+
}
|
|
7705
|
+
const newest = await requireNewestRelease(releases, waysOut);
|
|
7706
|
+
ctx.ui.info(`The newest Metabase release is ${newest.version}`);
|
|
7707
|
+
return newest.version;
|
|
7708
|
+
}
|
|
7709
|
+
async function chooseImage(ctx, options, releases) {
|
|
7484
7710
|
if (options.image !== null) {
|
|
7485
7711
|
ctx.ui.info(`Using image ${options.image} from --image`);
|
|
7486
7712
|
return options.image;
|
|
7487
7713
|
}
|
|
7488
|
-
return
|
|
7714
|
+
if (developmentMode()) return DEVELOPMENT_IMAGE;
|
|
7715
|
+
return releaseImage(ctx, releases, INIT_WAYS_OUT);
|
|
7489
7716
|
}
|
|
7490
7717
|
async function resolveJar(ctx, spec, releases) {
|
|
7491
7718
|
const parsed = parseJarSpec(spec);
|
|
7492
|
-
if (parsed === null) throw new ConfigError(`${spec} is not latest,
|
|
7719
|
+
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
7720
|
if (parsed.kind !== "latest") return parsed;
|
|
7494
|
-
const version = await releases.latestVersion();
|
|
7495
|
-
ctx.ui.info(`The latest Metabase release is ${version}`);
|
|
7496
7721
|
return {
|
|
7497
7722
|
kind: "release",
|
|
7498
|
-
version
|
|
7723
|
+
version: await latestJarRelease(ctx, releases, INIT_WAYS_OUT)
|
|
7499
7724
|
};
|
|
7500
7725
|
}
|
|
7501
7726
|
async function chooseJar(ctx, options, releases) {
|
|
@@ -7520,7 +7745,7 @@ async function chooseRuntime(ctx, options, deps) {
|
|
|
7520
7745
|
const kind = await chooseKind(ctx, options, deps.runtime);
|
|
7521
7746
|
switch (kind) {
|
|
7522
7747
|
case "docker": {
|
|
7523
|
-
const image = chooseImage(ctx, options);
|
|
7748
|
+
const image = await chooseImage(ctx, options, deps.releases);
|
|
7524
7749
|
const port = await choosePort(ctx, options, deps.runtime);
|
|
7525
7750
|
return {
|
|
7526
7751
|
kind,
|
|
@@ -7601,6 +7826,16 @@ function isNotHealthy(error) {
|
|
|
7601
7826
|
async function runningVersion(client) {
|
|
7602
7827
|
return (await client.session.properties()).version.tag;
|
|
7603
7828
|
}
|
|
7829
|
+
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";
|
|
7830
|
+
function unsupportedVersionReason(version, development) {
|
|
7831
|
+
if (development) return null;
|
|
7832
|
+
const needs = `rde needs Metabase Enterprise 65 or newer: ${PICK_ANOTHER}`;
|
|
7833
|
+
if (isOpenSourceRelease(version)) return `Metabase ${version} is the open-source edition; ${needs}`;
|
|
7834
|
+
if (isMasterBuild(version)) return `Metabase ${version} is a development build of master, which only development mode (RDE_DEV=1) runs; ${needs}`;
|
|
7835
|
+
const major = releaseMajor(version);
|
|
7836
|
+
if (major !== null && major < 65) return `Metabase ${version} is older than 65; ${needs}`;
|
|
7837
|
+
return null;
|
|
7838
|
+
}
|
|
7604
7839
|
function progressLabel(progress) {
|
|
7605
7840
|
return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
|
|
7606
7841
|
}
|
|
@@ -7617,7 +7852,7 @@ async function startRuntime(ctx, runtime, deps) {
|
|
|
7617
7852
|
throw error;
|
|
7618
7853
|
}
|
|
7619
7854
|
}
|
|
7620
|
-
function startMetabaseStep(deps = hostDeps$5) {
|
|
7855
|
+
function startMetabaseStep(deps = hostDeps$5, accepted = "supported") {
|
|
7621
7856
|
return {
|
|
7622
7857
|
id: "start-metabase",
|
|
7623
7858
|
title: "Start Metabase",
|
|
@@ -7669,6 +7904,11 @@ function startMetabaseStep(deps = hostDeps$5) {
|
|
|
7669
7904
|
...ctx.state,
|
|
7670
7905
|
version
|
|
7671
7906
|
});
|
|
7907
|
+
const refusal = accepted === "supported" ? unsupportedVersionReason(version, developmentMode()) : null;
|
|
7908
|
+
if (refusal !== null) {
|
|
7909
|
+
spinner.fail(`Metabase ${version} is running at ${configured.url}, but rde does not run it`);
|
|
7910
|
+
throw new UnsupportedMetabaseError(refusal);
|
|
7911
|
+
}
|
|
7672
7912
|
spinner.stop(`Metabase ${version} is running at ${configured.url}`);
|
|
7673
7913
|
return { outcome: "completed" };
|
|
7674
7914
|
}
|
|
@@ -7976,16 +8216,23 @@ function remoteLine(remote) {
|
|
|
7976
8216
|
case "hosted": return `Remote: ${remote.url}`;
|
|
7977
8217
|
}
|
|
7978
8218
|
}
|
|
7979
|
-
function
|
|
7980
|
-
const license = state.license;
|
|
8219
|
+
function licenseGap(license) {
|
|
7981
8220
|
if (!license.activated) return "no license";
|
|
7982
8221
|
if (!hasRemoteSync(license)) return "the license has no remote-sync feature";
|
|
8222
|
+
return null;
|
|
8223
|
+
}
|
|
8224
|
+
function notConfiguredReason(state) {
|
|
8225
|
+
const gap = licenseGap(state.license);
|
|
8226
|
+
if (gap !== null) return gap;
|
|
7983
8227
|
if (state.steps[GIT_SYNC_STEP_ID]?.status === "skipped") return "skipped; `rde init --only git-sync` sets it up";
|
|
7984
8228
|
return "not set up yet; `rde init --only git-sync`";
|
|
7985
8229
|
}
|
|
7986
8230
|
function gitLines(state) {
|
|
7987
8231
|
if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
|
|
7988
|
-
|
|
8232
|
+
const repository = [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
|
|
8233
|
+
const gap = licenseGap(state.license);
|
|
8234
|
+
if (gap === null) return repository;
|
|
8235
|
+
return [...repository, `Remote sync: unavailable (${gap}; \`rde init --from license\` activates one)`];
|
|
7989
8236
|
}
|
|
7990
8237
|
async function pendingExportLines(state, secrets, runner) {
|
|
7991
8238
|
const instance = mbInstanceOf(state, secrets);
|
|
@@ -8956,8 +9203,7 @@ function plainUi(output) {
|
|
|
8956
9203
|
};
|
|
8957
9204
|
}
|
|
8958
9205
|
function noColorRequested() {
|
|
8959
|
-
|
|
8960
|
-
return value !== void 0 && value !== "";
|
|
9206
|
+
return readSetEnv(ENV_NO_COLOR) !== null;
|
|
8961
9207
|
}
|
|
8962
9208
|
function drawsOnTerminal() {
|
|
8963
9209
|
return process.stdout.isTTY === true && !noColorRequested();
|
|
@@ -9088,6 +9334,20 @@ const db = defineCommand({
|
|
|
9088
9334
|
default: "list"
|
|
9089
9335
|
});
|
|
9090
9336
|
//#endregion
|
|
9337
|
+
//#region package.json
|
|
9338
|
+
var name = "@metabase/rde";
|
|
9339
|
+
var version = "0.1.18";
|
|
9340
|
+
var description = "Prepares a local machine for data-engineering work with Metabase and coding agents";
|
|
9341
|
+
//#endregion
|
|
9342
|
+
//#region src/version.ts
|
|
9343
|
+
const PACKAGE_NAME = name;
|
|
9344
|
+
const VERSION = version;
|
|
9345
|
+
const ROOT_META = {
|
|
9346
|
+
name: "rde",
|
|
9347
|
+
version: VERSION,
|
|
9348
|
+
description
|
|
9349
|
+
};
|
|
9350
|
+
//#endregion
|
|
9091
9351
|
//#region src/wizard/engine.ts
|
|
9092
9352
|
function requireKnownStep(steps, id, flag) {
|
|
9093
9353
|
if (id === void 0) return;
|
|
@@ -9184,6 +9444,8 @@ async function runWizard(steps, ctx, options = {}) {
|
|
|
9184
9444
|
}
|
|
9185
9445
|
//#endregion
|
|
9186
9446
|
//#region src/steps/doctor.ts
|
|
9447
|
+
const RDE_DIST_TAG = "latest";
|
|
9448
|
+
const RDE_RELEASE_TIMEOUT_MS = 3e3;
|
|
9187
9449
|
function hostDoctorDeps(instanceName, signal) {
|
|
9188
9450
|
const runner = processRunner(signal);
|
|
9189
9451
|
const runtime = hostRuntimeDeps(instanceName, signal);
|
|
@@ -9203,7 +9465,8 @@ function hostDoctorDeps(instanceName, signal) {
|
|
|
9203
9465
|
cwd: process.cwd(),
|
|
9204
9466
|
stateHome: xdgStateHome(),
|
|
9205
9467
|
files: hostSkillFiles
|
|
9206
|
-
}
|
|
9468
|
+
},
|
|
9469
|
+
publishedRdeVersion: () => npmRegistry(signal, { timeoutMs: RDE_RELEASE_TIMEOUT_MS }).distTagVersion(PACKAGE_NAME, RDE_DIST_TAG)
|
|
9207
9470
|
};
|
|
9208
9471
|
}
|
|
9209
9472
|
const NOT_FOUND_STATUS = 404;
|
|
@@ -9297,6 +9560,26 @@ function mbRow(diagnosis, lines, deps) {
|
|
|
9297
9560
|
case "current": return ok("mb", MB_LABEL, lines.join("\n"));
|
|
9298
9561
|
}
|
|
9299
9562
|
}
|
|
9563
|
+
const RDE_LABEL = "rde";
|
|
9564
|
+
const RDE_UPDATE_HINT = `\`npm i -g ${PACKAGE_NAME}@${RDE_DIST_TAG}\``;
|
|
9565
|
+
function rdeUncheckedRow(reason) {
|
|
9566
|
+
return skipped("rde", RDE_LABEL, `${VERSION}; could not check for a newer release: ${reason}`);
|
|
9567
|
+
}
|
|
9568
|
+
async function rdeRow(deps) {
|
|
9569
|
+
let raw;
|
|
9570
|
+
try {
|
|
9571
|
+
raw = await deps.publishedRdeVersion();
|
|
9572
|
+
} catch (error) {
|
|
9573
|
+
const reason = lookupFailure(error);
|
|
9574
|
+
if (reason === null) throw error;
|
|
9575
|
+
return rdeUncheckedRow(reason);
|
|
9576
|
+
}
|
|
9577
|
+
const published = semver.valid(raw);
|
|
9578
|
+
if (published === null) return rdeUncheckedRow(`the registry names "${raw}" for ${RDE_DIST_TAG}, which is not a version`);
|
|
9579
|
+
if (semver.lt(VERSION, published)) return note$1("rde", RDE_LABEL, `${VERSION}; ${published} is released`, RDE_UPDATE_HINT);
|
|
9580
|
+
if (semver.gt(VERSION, published)) return ok("rde", RDE_LABEL, `${VERSION}, ahead of the latest release ${published}`);
|
|
9581
|
+
return ok("rde", RDE_LABEL, `${VERSION}, the latest release`);
|
|
9582
|
+
}
|
|
9300
9583
|
async function mbFinding(deps) {
|
|
9301
9584
|
const found = await inspectMbOnPath(deps.runner, deps.locateMb);
|
|
9302
9585
|
const diagnosis = diagnoseMb(found, deps.installedWith);
|
|
@@ -9317,6 +9600,20 @@ function publishesOnLoopback(container, port) {
|
|
|
9317
9600
|
}
|
|
9318
9601
|
const CONTAINER = "the container";
|
|
9319
9602
|
const CONTAINER_SILENT_PORT_HINT = "Docker's forward of the port is broken; `rde start` repairs it";
|
|
9603
|
+
function containerSettingsFailure(runtime, inspection) {
|
|
9604
|
+
const onMaster = !developmentMode() && isMasterBuildImage(runtime.image);
|
|
9605
|
+
const drift = containerEnvDrift(inspection, runtime);
|
|
9606
|
+
const problems = [];
|
|
9607
|
+
if (onMaster) problems.push(`${runtime.containerName} runs ${runtime.image}, a development build of Metabase`);
|
|
9608
|
+
if (drift.length > 0) problems.push(`${runtime.containerName} was created with ${drift.join(", ")} set otherwise than rde sets them now`);
|
|
9609
|
+
if (problems.length === 0) return null;
|
|
9610
|
+
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";
|
|
9611
|
+
return {
|
|
9612
|
+
detail: problems.join("\n"),
|
|
9613
|
+
hint,
|
|
9614
|
+
step: null
|
|
9615
|
+
};
|
|
9616
|
+
}
|
|
9320
9617
|
async function containerRow(runtime, deps, dockerOk) {
|
|
9321
9618
|
const label = "container";
|
|
9322
9619
|
if (!dockerOk) return {
|
|
@@ -9357,6 +9654,11 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
9357
9654
|
}),
|
|
9358
9655
|
...found
|
|
9359
9656
|
};
|
|
9657
|
+
const settings = containerSettingsFailure(runtime, inspection);
|
|
9658
|
+
if (settings !== null) return {
|
|
9659
|
+
row: failed("container", label, settings),
|
|
9660
|
+
...found
|
|
9661
|
+
};
|
|
9360
9662
|
const state = inspection.running ? "running" : "stopped";
|
|
9361
9663
|
return {
|
|
9362
9664
|
row: ok("container", label, `${runtime.containerName} ${state} (${runtime.image})`),
|
|
@@ -9835,25 +10137,71 @@ async function skillCopiesRow(globalEntries, deps) {
|
|
|
9835
10137
|
const stale = judged.filter((copy) => !copy.current);
|
|
9836
10138
|
return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
|
|
9837
10139
|
}
|
|
10140
|
+
function dataAppBranchLine(version, origin) {
|
|
10141
|
+
const wanted = withRef(origin.repository, origin.ref);
|
|
10142
|
+
return version === null ? `Metabase has not reported its version yet; rde installs them from ${wanted}` : `Metabase ${version} takes them from ${wanted}`;
|
|
10143
|
+
}
|
|
10144
|
+
async function dataAppSkillsRow(state, installed, deps) {
|
|
10145
|
+
const id = "data-app-skills";
|
|
10146
|
+
const label = "data-app skills";
|
|
10147
|
+
const branch = dataAppSkillsOrigin(wantedDataAppRef(state.version));
|
|
10148
|
+
const wanted = [...DATA_APP_SKILL_NAMES.map((name) => ({
|
|
10149
|
+
name,
|
|
10150
|
+
origin: branch
|
|
10151
|
+
})), {
|
|
10152
|
+
name: RDE_DATA_APPS_SKILL_NAME,
|
|
10153
|
+
origin: skillOrigin(deps.skills.source, deps.skills.cwd)
|
|
10154
|
+
}];
|
|
10155
|
+
const lines = [dataAppBranchLine(state.version, branch)];
|
|
10156
|
+
let current = true;
|
|
10157
|
+
for (const skill of wanted) {
|
|
10158
|
+
const entry = installed.find((candidate) => candidate.name === skill.name);
|
|
10159
|
+
if (entry === void 0) {
|
|
10160
|
+
lines.push(`${skill.name}: not installed`);
|
|
10161
|
+
current = false;
|
|
10162
|
+
continue;
|
|
10163
|
+
}
|
|
10164
|
+
const judged = await judgeCopy(entry, skill.origin, deps);
|
|
10165
|
+
lines.push(judged.line);
|
|
10166
|
+
current &&= judged.current;
|
|
10167
|
+
}
|
|
10168
|
+
const detail = lines.join("\n");
|
|
10169
|
+
if (current) return ok(id, label, detail);
|
|
10170
|
+
return failed(id, label, {
|
|
10171
|
+
detail,
|
|
10172
|
+
hint: `\`rde init --only ${SKILL_STEP}\` installs them; then restart the agent session`,
|
|
10173
|
+
step: SKILL_STEP
|
|
10174
|
+
});
|
|
10175
|
+
}
|
|
9838
10176
|
async function skillRows(state, deps) {
|
|
9839
10177
|
const label = "rde skill";
|
|
9840
10178
|
if (state === null) return [skipped("skill", label, NO_INSTANCE_REASON)];
|
|
9841
10179
|
if (state.steps[SKILL_STEP]?.status === "skipped") return [skipped("skill", label, `skipped during init; \`rde init --only ${SKILL_STEP}\``)];
|
|
9842
10180
|
const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
|
|
9843
10181
|
if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
|
|
9844
|
-
const
|
|
10182
|
+
const installed = await skillsCli(deps.runner).list();
|
|
10183
|
+
const entries = installed.filter((entry) => entry.name === "rde");
|
|
9845
10184
|
const copies = await skillCopiesRow(entries, deps);
|
|
9846
|
-
|
|
9847
|
-
|
|
9848
|
-
|
|
9849
|
-
|
|
9850
|
-
|
|
9851
|
-
|
|
9852
|
-
|
|
9853
|
-
|
|
9854
|
-
|
|
9855
|
-
|
|
9856
|
-
|
|
10185
|
+
const dataApps = await dataAppSkillsRow(state, installed, deps);
|
|
10186
|
+
if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [
|
|
10187
|
+
failed("skill", label, {
|
|
10188
|
+
detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
|
|
10189
|
+
hint: `\`rde init --only ${SKILL_STEP}\``,
|
|
10190
|
+
step: SKILL_STEP
|
|
10191
|
+
}),
|
|
10192
|
+
copies,
|
|
10193
|
+
dataApps
|
|
10194
|
+
];
|
|
10195
|
+
return [
|
|
10196
|
+
...detected.map((agent) => {
|
|
10197
|
+
const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
|
|
10198
|
+
const agentLabel = `${label} (${agent.displayName})`;
|
|
10199
|
+
if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
|
|
10200
|
+
return ok(skillRowId(agent), agentLabel, entry.path);
|
|
10201
|
+
}),
|
|
10202
|
+
copies,
|
|
10203
|
+
dataApps
|
|
10204
|
+
];
|
|
9857
10205
|
}
|
|
9858
10206
|
function noRuntimeRows() {
|
|
9859
10207
|
return [
|
|
@@ -9898,7 +10246,11 @@ async function collectDoctor(instance, state, secrets, deps) {
|
|
|
9898
10246
|
const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
|
|
9899
10247
|
const mb = await mbFinding(deps);
|
|
9900
10248
|
const profiles = sharedProfileList(deps);
|
|
9901
|
-
const checks = [
|
|
10249
|
+
const checks = [
|
|
10250
|
+
...machine,
|
|
10251
|
+
mb.row,
|
|
10252
|
+
await rdeRow(deps)
|
|
10253
|
+
];
|
|
9902
10254
|
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
10255
|
else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
|
|
9904
10256
|
const rdeProfile = state === null ? null : state.mbProfile;
|
|
@@ -11024,7 +11376,7 @@ function keptOnDisk(options, state, dirPresent) {
|
|
|
11024
11376
|
const kept = [];
|
|
11025
11377
|
if (options.keepData && dirPresent) kept.push(`Instance directory ${instanceDir(options.instance)} (--keep-data); \`rde init\` reinstalls from it`);
|
|
11026
11378
|
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
|
|
11379
|
+
if (!options.removeSkill) kept.push(`The ${RDE_SOURCE_SKILL_NAMES.join(" and ")} skills for your agents; pass --remove-skill to remove them`);
|
|
11028
11380
|
return kept;
|
|
11029
11381
|
}
|
|
11030
11382
|
async function planUninstall(options, deps) {
|
|
@@ -11131,6 +11483,9 @@ function reportLines(report) {
|
|
|
11131
11483
|
function leftBehind(report) {
|
|
11132
11484
|
return [...report.results.flatMap((result) => result.outcome === "failed" ? [result.label] : []), ...report.problems.map((problem) => problem.subject)];
|
|
11133
11485
|
}
|
|
11486
|
+
function uninstallCommandLine(options) {
|
|
11487
|
+
return options.instance === options.defaultInstance ? "rde uninstall" : `rde uninstall --instance ${options.instance}`;
|
|
11488
|
+
}
|
|
11134
11489
|
async function uninstall(options, deps, io) {
|
|
11135
11490
|
const plan = await planUninstall(options, deps);
|
|
11136
11491
|
for (const problem of plan.problems) io.ui.warn(`${problem.subject} ${problem.reason}`);
|
|
@@ -11141,7 +11496,7 @@ async function uninstall(options, deps, io) {
|
|
|
11141
11496
|
problems: plan.problems
|
|
11142
11497
|
};
|
|
11143
11498
|
}
|
|
11144
|
-
io.ui.note(planLines$1(plan).join("\n"),
|
|
11499
|
+
io.ui.note(planLines$1(plan).join("\n"), uninstallCommandLine(options));
|
|
11145
11500
|
if (plan.unpulled !== null) {
|
|
11146
11501
|
io.ui.warn(unpulledWarning(plan.unpulled));
|
|
11147
11502
|
if ((options.yes || !options.terminal) && !options.force) throw new ConfigError(unpulledRefusal(plan.unpulled));
|
|
@@ -11180,7 +11535,7 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11180
11535
|
},
|
|
11181
11536
|
"remove-skill": {
|
|
11182
11537
|
type: "boolean",
|
|
11183
|
-
description: "Also remove the rde
|
|
11538
|
+
description: "Also remove the rde and rde-data-apps skills from your coding agents",
|
|
11184
11539
|
default: false
|
|
11185
11540
|
},
|
|
11186
11541
|
"remove-image": {
|
|
@@ -11201,11 +11556,13 @@ const uninstallCommand = defineRdeCommand({
|
|
|
11201
11556
|
},
|
|
11202
11557
|
interrupted: INTERRUPTED,
|
|
11203
11558
|
async run(args) {
|
|
11204
|
-
const
|
|
11559
|
+
const defaultInstance = await instanceNameOrDefault();
|
|
11560
|
+
const instance = args.instance ?? defaultInstance;
|
|
11205
11561
|
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
11562
|
const ui = terminalUi();
|
|
11207
11563
|
const report = await uninstall({
|
|
11208
11564
|
instance,
|
|
11565
|
+
defaultInstance,
|
|
11209
11566
|
keepData: args["keep-data"],
|
|
11210
11567
|
removeSkill: args["remove-skill"],
|
|
11211
11568
|
removeImage: args["remove-image"],
|
|
@@ -11231,35 +11588,59 @@ function hostDeps(ctx) {
|
|
|
11231
11588
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
11232
11589
|
timing: DEFAULT_HEALTH_TIMING,
|
|
11233
11590
|
releases: releaseLookup(ctx.signal),
|
|
11234
|
-
now: () => /* @__PURE__ */ new Date()
|
|
11591
|
+
now: () => /* @__PURE__ */ new Date(),
|
|
11592
|
+
skills: (agents) => agentsSkillsStep(agents, "published")
|
|
11235
11593
|
};
|
|
11236
11594
|
}
|
|
11237
|
-
function
|
|
11238
|
-
const
|
|
11239
|
-
|
|
11240
|
-
|
|
11595
|
+
function majorMove(from, to) {
|
|
11596
|
+
const fromMajor = releaseMajor(from);
|
|
11597
|
+
const toMajor = to === null ? null : releaseMajor(to);
|
|
11598
|
+
if (fromMajor === null || toMajor === null || fromMajor === toMajor) return null;
|
|
11599
|
+
return {
|
|
11600
|
+
from: fromMajor,
|
|
11601
|
+
to: toMajor
|
|
11602
|
+
};
|
|
11241
11603
|
}
|
|
11242
11604
|
function releaseOf(tag) {
|
|
11243
11605
|
const match = RELEASE_TAG.exec(tag);
|
|
11244
11606
|
if (match === null || match[1] === void 0) return null;
|
|
11245
11607
|
return semver.coerce(match[1]);
|
|
11246
11608
|
}
|
|
11247
|
-
function
|
|
11248
|
-
if (targetTag
|
|
11609
|
+
function isDowngrade(current, targetTag) {
|
|
11610
|
+
if (isMajorLineTag(targetTag)) {
|
|
11611
|
+
const running = releaseMajor(current);
|
|
11612
|
+
const target = releaseMajor(targetTag);
|
|
11613
|
+
return running !== null && target !== null && target < running;
|
|
11614
|
+
}
|
|
11249
11615
|
const running = releaseOf(current);
|
|
11250
11616
|
const target = releaseOf(targetTag);
|
|
11251
|
-
|
|
11617
|
+
return running !== null && target !== null && semver.lt(target, running);
|
|
11618
|
+
}
|
|
11619
|
+
function downgradeReason(current, targetTag) {
|
|
11620
|
+
if (targetTag === null || !isDowngrade(current, targetTag)) return null;
|
|
11252
11621
|
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
11622
|
}
|
|
11254
11623
|
function backupStamp(now) {
|
|
11255
11624
|
return now.toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
|
|
11256
11625
|
}
|
|
11257
|
-
async function ensureRunning(ctx, deps, client) {
|
|
11258
|
-
await startMetabaseStep(() => deps).run(ctx);
|
|
11626
|
+
async function ensureRunning(ctx, deps, client, accepted) {
|
|
11627
|
+
await startMetabaseStep(() => deps, accepted).run(ctx);
|
|
11259
11628
|
return runningVersion(client);
|
|
11260
11629
|
}
|
|
11261
|
-
function
|
|
11262
|
-
const
|
|
11630
|
+
function requireSupported(version) {
|
|
11631
|
+
const refusal = unsupportedVersionReason(version, developmentMode());
|
|
11632
|
+
if (refusal !== null) throw new UnsupportedMetabaseError(refusal);
|
|
11633
|
+
}
|
|
11634
|
+
function knownTargetVersion(target) {
|
|
11635
|
+
const tag = target.releaseTag;
|
|
11636
|
+
return tag !== null && (isReleaseTag(tag) || isMajorLineTag(tag)) ? tag : null;
|
|
11637
|
+
}
|
|
11638
|
+
const TO_ONE = "`--to <image or release tag>` names a Metabase to move to";
|
|
11639
|
+
const UPGRADE_WAYS_OUT = {
|
|
11640
|
+
named: TO_ONE,
|
|
11641
|
+
unreleased: TO_ONE
|
|
11642
|
+
};
|
|
11643
|
+
function dockerTargetOf(runtime, image, ownImage) {
|
|
11263
11644
|
return {
|
|
11264
11645
|
kind: "docker",
|
|
11265
11646
|
next: {
|
|
@@ -11267,22 +11648,24 @@ function dockerTarget(runtime, to) {
|
|
|
11267
11648
|
image
|
|
11268
11649
|
},
|
|
11269
11650
|
label: image,
|
|
11270
|
-
releaseTag: imageTag(image)
|
|
11651
|
+
releaseTag: imageTag(image),
|
|
11652
|
+
ownImage
|
|
11271
11653
|
};
|
|
11272
11654
|
}
|
|
11655
|
+
async function dockerTarget(ctx, runtime, to, releases) {
|
|
11656
|
+
if (to !== null) return dockerTargetOf(runtime, to, false);
|
|
11657
|
+
if (developmentMode()) return dockerTargetOf(runtime, runtime.image, false);
|
|
11658
|
+
if (!isMetabaseImage(runtime.image)) return dockerTargetOf(runtime, runtime.image, true);
|
|
11659
|
+
return dockerTargetOf(runtime, await releaseImage(ctx, releases, UPGRADE_WAYS_OUT), false);
|
|
11660
|
+
}
|
|
11273
11661
|
async function jarTarget(ctx, runtime, to, releases) {
|
|
11274
11662
|
const spec = to ?? "latest";
|
|
11275
11663
|
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;
|
|
11664
|
+
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`);
|
|
11665
|
+
const source = parsed.kind === "latest" ? {
|
|
11666
|
+
kind: "release",
|
|
11667
|
+
version: await latestJarRelease(ctx, releases, UPGRADE_WAYS_OUT)
|
|
11668
|
+
} : parsed;
|
|
11286
11669
|
return {
|
|
11287
11670
|
kind: "jar",
|
|
11288
11671
|
previous: runtime,
|
|
@@ -11296,7 +11679,7 @@ async function jarTarget(ctx, runtime, to, releases) {
|
|
|
11296
11679
|
}
|
|
11297
11680
|
function resolveTarget(ctx, configured, to, releases) {
|
|
11298
11681
|
switch (configured.runtime.kind) {
|
|
11299
|
-
case "docker": return
|
|
11682
|
+
case "docker": return dockerTarget(ctx, configured.runtime, to, releases);
|
|
11300
11683
|
case "jar": return jarTarget(ctx, configured.runtime, to, releases);
|
|
11301
11684
|
}
|
|
11302
11685
|
}
|
|
@@ -11346,9 +11729,13 @@ const SWITCH_TEXT = {
|
|
|
11346
11729
|
jar: "Metabase stops and starts again from the target jar."
|
|
11347
11730
|
};
|
|
11348
11731
|
function planLines(configured, current, target, backup) {
|
|
11732
|
+
const notes = [];
|
|
11733
|
+
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");
|
|
11734
|
+
if (target.releaseTag === null || !isReleaseTag(target.releaseTag)) notes.push("its version shows once it runs");
|
|
11735
|
+
const annotation = notes.length === 0 ? "" : ` (${notes.join("; ")})`;
|
|
11349
11736
|
return [
|
|
11350
11737
|
`Running: ${current} (${describeRuntime(configured.runtime)})`,
|
|
11351
|
-
`Target: ${target.label}`,
|
|
11738
|
+
`Target: ${target.label}${annotation}`,
|
|
11352
11739
|
`Backup: ${backup}`,
|
|
11353
11740
|
"",
|
|
11354
11741
|
SWITCH_TEXT[configured.runtime.kind],
|
|
@@ -11407,31 +11794,70 @@ async function switchTo(ctx, deps, target) {
|
|
|
11407
11794
|
}
|
|
11408
11795
|
spinner.stop(`Installed ${target.label}`);
|
|
11409
11796
|
}
|
|
11797
|
+
function failedStartTitle(error) {
|
|
11798
|
+
if (error instanceof MetabaseUnreachableError) return "Metabase did not come up on the new version";
|
|
11799
|
+
if (error instanceof UnsupportedMetabaseError) return "The new version is not one rde runs";
|
|
11800
|
+
return null;
|
|
11801
|
+
}
|
|
11410
11802
|
async function startUpgraded(ctx, deps, client, previous, backup) {
|
|
11411
11803
|
try {
|
|
11412
|
-
return await ensureRunning(ctx, deps, client);
|
|
11804
|
+
return await ensureRunning(ctx, deps, client, "supported");
|
|
11413
11805
|
} catch (error) {
|
|
11414
|
-
|
|
11806
|
+
const title = failedStartTitle(error);
|
|
11807
|
+
if (title !== null) ctx.ui.note(restoreLines(ctx, previous, deps.runtime.dataDir, backup).join("\n"), title);
|
|
11415
11808
|
throw error;
|
|
11416
11809
|
}
|
|
11417
11810
|
}
|
|
11811
|
+
async function recreateStale(ctx, deps, previous, target) {
|
|
11812
|
+
const next = target.next;
|
|
11813
|
+
const container = await deps.runtime.docker.containerInspect(next.containerName);
|
|
11814
|
+
const drift = container.exists ? containerEnvDrift(container, next) : [];
|
|
11815
|
+
const renamed = previous.kind === "docker" && previous.image !== next.image;
|
|
11816
|
+
if (drift.length === 0 && !renamed) return false;
|
|
11817
|
+
if (renamed) {
|
|
11818
|
+
ctx.ui.info(`${previous.image} is the image ${next.image} names; creating ${next.containerName} again under ${next.image}`);
|
|
11819
|
+
await ctx.saveState({
|
|
11820
|
+
...ctx.state,
|
|
11821
|
+
runtime: next
|
|
11822
|
+
});
|
|
11823
|
+
}
|
|
11824
|
+
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`);
|
|
11825
|
+
await runtimeFromState(next, deps.runtime).reconfigure();
|
|
11826
|
+
return true;
|
|
11827
|
+
}
|
|
11828
|
+
async function alreadyOn(ctx, deps, client, configured, current, target) {
|
|
11829
|
+
if (target.kind === "docker" && await recreateStale(ctx, deps, configured.runtime, target)) {
|
|
11830
|
+
await ensureRunning(ctx, deps, client, "supported");
|
|
11831
|
+
ctx.ui.success(`Already on ${current}; ${target.next.containerName} runs ${target.label} with rde's settings now`);
|
|
11832
|
+
return {
|
|
11833
|
+
outcome: "recreated",
|
|
11834
|
+
version: current
|
|
11835
|
+
};
|
|
11836
|
+
}
|
|
11837
|
+
ctx.ui.success(`Already on ${current}: ${target.label} is what the instance runs`);
|
|
11838
|
+
return {
|
|
11839
|
+
outcome: "unchanged",
|
|
11840
|
+
version: current
|
|
11841
|
+
};
|
|
11842
|
+
}
|
|
11418
11843
|
async function upgrade(ctx, options, deps = hostDeps) {
|
|
11419
11844
|
const resolved = deps(ctx);
|
|
11420
11845
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
11421
11846
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
11422
|
-
const current = await ensureRunning(ctx, resolved, client);
|
|
11423
11847
|
const target = await resolveTarget(ctx, configured, options.to, resolved.releases);
|
|
11848
|
+
const current = await ensureRunning(ctx, resolved, client, "any");
|
|
11424
11849
|
const refusal = downgradeReason(current, target.releaseTag);
|
|
11425
11850
|
if (refusal !== null) throw new ConfigError(refusal);
|
|
11851
|
+
const known = knownTargetVersion(target);
|
|
11852
|
+
if (known !== null) requireSupported(known);
|
|
11426
11853
|
if (await fetchTarget(ctx, resolved, target, current)) {
|
|
11427
|
-
|
|
11428
|
-
return
|
|
11429
|
-
outcome: "unchanged",
|
|
11430
|
-
version: current
|
|
11431
|
-
};
|
|
11854
|
+
requireSupported(current);
|
|
11855
|
+
return alreadyOn(ctx, resolved, client, configured, current, target);
|
|
11432
11856
|
}
|
|
11433
11857
|
const backup = join(instanceBackupsDir(ctx.state.name), backupStamp(resolved.now()));
|
|
11434
11858
|
ctx.ui.note(planLines(configured, current, target, backup).join("\n"), "rde upgrade");
|
|
11859
|
+
const announced = majorMove(current, target.releaseTag);
|
|
11860
|
+
if (announced !== null) ctx.ui.warn(`This moves Metabase from ${announced.from} to ${announced.to}, a new major version; the data-app skills follow it after the upgrade`);
|
|
11435
11861
|
if (!options.yes) {
|
|
11436
11862
|
if (!await ctx.prompts.confirm({
|
|
11437
11863
|
message: "Upgrade now?",
|
|
@@ -11452,6 +11878,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
|
|
|
11452
11878
|
options.onPhase("starting", backup);
|
|
11453
11879
|
const to = await startUpgraded(ctx, resolved, client, configured, backup);
|
|
11454
11880
|
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
11881
|
+
const moved = majorMove(current, to);
|
|
11882
|
+
if (moved !== null) {
|
|
11883
|
+
if (announced === null) ctx.ui.warn(`This moved Metabase from ${moved.from} to ${moved.to}, a new major version; the data-app skills follow it`);
|
|
11884
|
+
await runWizard([resolved.skills(options.yes ? DETECT_AGENTS : ASK_AGENTS)], ctx);
|
|
11885
|
+
}
|
|
11455
11886
|
return {
|
|
11456
11887
|
outcome: "upgraded",
|
|
11457
11888
|
from: current,
|
|
@@ -11477,7 +11908,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11477
11908
|
args: {
|
|
11478
11909
|
to: {
|
|
11479
11910
|
type: "string",
|
|
11480
|
-
description: "Docker: image to upgrade to (default: the instance's image
|
|
11911
|
+
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
11912
|
},
|
|
11482
11913
|
yes: {
|
|
11483
11914
|
type: "boolean",
|
|
@@ -11505,11 +11936,7 @@ const upgradeCommand = defineRdeCommand({
|
|
|
11505
11936
|
//#endregion
|
|
11506
11937
|
//#region src/main.ts
|
|
11507
11938
|
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
|
-
},
|
|
11939
|
+
meta: ROOT_META,
|
|
11513
11940
|
subCommands: {
|
|
11514
11941
|
init,
|
|
11515
11942
|
start,
|