@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.
Files changed (3) hide show
  1. package/README.md +50 -20
  2. package/dist/cli.mjs +741 -314
  3. 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 ENV_RDE_SKILL_SOURCE = "RDE_SKILL_SOURCE";
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 = readEnv(ENV_RDE_HOME);
570
- if (override !== void 0) return resolve(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(REQUEST_TIMEOUT_MS)])
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 ${REQUEST_TIMEOUT_MS}ms`);
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 VersionInfo = z.object({ latest: z.object({ version: z.string() }).loose() }).loose();
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+\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
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 { async latestVersion() {
2228
- return (await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionInfo })).latest.version;
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
- MB_RUN_MODE: "e2e",
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 DEFAULT_IMAGE = "metabase/metabase-enterprise-head:latest";
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: serverEnv({
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 = "alpha-release-65-integration-branch";
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: setValue(ENV_MB_PROFILE),
3418
- url: setValue(ENV_MB_URL),
3419
- apiKeySet: setValue(ENV_MB_API_KEY) !== null
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$7 = "git-sync";
6043
+ const STEP_ID$6 = "git-sync";
5733
6044
  const GIT_SYNC_KEYS = {
5734
- location: `${STEP_ID$7}.location`,
5735
- path: `${STEP_ID$7}.path`,
5736
- remote: `${STEP_ID$7}.remote`,
5737
- url: `${STEP_ID$7}.url`,
5738
- token: `${STEP_ID$7}.token`
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$12(ctx) {
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$12) {
6558
+ function gitSyncStep(deps = hostDeps$11) {
6243
6559
  return {
6244
- id: STEP_ID$7,
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$7]?.status === "skipped") return "not-applicable";
6250
- if (ctx.state.gitRemote === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
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 DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
6295
- const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
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
- const override = readEnv(ENV_RDE_SKILL_SOURCE);
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
- const value = readEnv(ENV_XDG_STATE_HOME);
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
- const DATA_APP_SKILLS_ORIGIN = {
6348
- kind: "github",
6349
- repository: DATA_APP_REPOSITORY,
6350
- ref: DATA_APP_BRANCH
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$6 = "agents-skills";
6433
- const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$6}.agents` };
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
- function hostDeps$11(ctx) {
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
- function skillSets(deps) {
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: DATA_APP_SKILLS_SOURCE,
6484
- sourceNote: "",
6485
- origin: DATA_APP_SKILLS_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$6}\` to install it later`;
6617
- case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$6} --agents <name>\` once one is installed`;
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$11) {
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$6,
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$6]?.status === "skipped" ? "not-applicable" : "todo";
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$1(state, apiKey) {
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$1(state, apiKey)
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$10(ctx) {
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$10) {
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$5 = "install-mb";
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$9(ctx) {
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$9) {
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$5,
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 runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
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
- function chooseImage(ctx, options) {
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 DEFAULT_IMAGE;
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, a release tag such as v1.63.18, or the path of a .jar file`);
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 notConfiguredReason(state) {
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
- return [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
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
- const value = readEnv(ENV_NO_COLOR);
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 entries = await rdeSkillEntries(deps.runner);
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
- if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
9847
- detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
9848
- hint: `\`rde init --only ${SKILL_STEP}\``,
9849
- step: SKILL_STEP
9850
- }), copies];
9851
- return [...detected.map((agent) => {
9852
- const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
9853
- const agentLabel = `${label} (${agent.displayName})`;
9854
- if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
9855
- return ok(skillRowId(agent), agentLabel, entry.path);
9856
- }), copies];
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 = [...machine, mb.row];
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 rde skill for your agents; pass --remove-skill to remove it`);
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"), `rde uninstall ${options.instance}`);
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 skill from your coding agents",
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 instance = args.instance ?? await instanceNameOrDefault();
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 imageTag(ref) {
11238
- const colon = ref.lastIndexOf(":");
11239
- if (colon === -1 || colon < ref.lastIndexOf("/")) return null;
11240
- return ref.slice(colon + 1);
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 downgradeReason(current, targetTag) {
11248
- if (targetTag === null) return null;
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
- if (running === null || target === null || !semver.lt(target, running)) return null;
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 dockerTarget(runtime, to) {
11262
- const image = to ?? runtime.image;
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, a release tag such as v1.63.18, or the path of a .jar file`);
11277
- let source;
11278
- if (parsed.kind === "latest") {
11279
- const version = await releases.latestVersion();
11280
- ctx.ui.info(`The latest Metabase release is ${version}`);
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 Promise.resolve(dockerTarget(configured.runtime, to));
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
- if (error instanceof MetabaseUnreachableError) ctx.ui.note(restoreLines(ctx, previous, deps.runtime.dataDir, backup).join("\n"), "Metabase did not come up on the new version");
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
- ctx.ui.success(`Already on ${current}: ${target.label} is what the instance runs`);
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, pulled again); jar: latest, a release tag, or a .jar path (default: latest)"
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,