@metabase/rde 0.1.17 → 0.5.0

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