@metabase/rde 0.1.16 → 0.1.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +61 -25
  2. package/dist/cli.mjs +1310 -452
  3. package/package.json +1 -1
package/dist/cli.mjs CHANGED
@@ -11,6 +11,7 @@ import { pipeline } from "node:stream/promises";
11
11
  import { spawn } from "node:child_process";
12
12
  import { connect, createServer } from "node:net";
13
13
  import { confirm, intro, isCancel, log, multiselect, note, outro, password, select, spinner, text } from "@clack/prompts";
14
+ import { isDeepStrictEqual } from "node:util";
14
15
  import semver from "semver";
15
16
  //#region src/errors.ts
16
17
  var AbortError = class extends Error {
@@ -33,6 +34,12 @@ var PortTakenError = class extends ConfigError {
33
34
  this.port = port;
34
35
  }
35
36
  };
37
+ var UnsupportedMetabaseError = class extends ConfigError {
38
+ constructor(message) {
39
+ super(message);
40
+ this.name = "UnsupportedMetabaseError";
41
+ }
42
+ };
36
43
  var PromptUnavailableError = class extends ConfigError {
37
44
  constructor(message) {
38
45
  super(message);
@@ -79,6 +86,8 @@ var MetabaseApiError = class extends Error {
79
86
  path;
80
87
  errors;
81
88
  reason;
89
+ code;
90
+ detail;
82
91
  constructor(failure) {
83
92
  super(`Metabase ${failure.status} on ${failure.path}: ${failure.message}`);
84
93
  this.name = "MetabaseApiError";
@@ -86,6 +95,8 @@ var MetabaseApiError = class extends Error {
86
95
  this.path = failure.path;
87
96
  this.errors = failure.errors;
88
97
  this.reason = failure.message;
98
+ this.code = failure.code;
99
+ this.detail = failure.detail;
89
100
  }
90
101
  };
91
102
  const UNAUTHORIZED_STATUS = 401;
@@ -256,14 +267,15 @@ function withoutEscapes(text) {
256
267
  const StepId = z.enum([
257
268
  "preflight",
258
269
  "install-mb",
259
- "agents-skills",
260
270
  "runtime",
261
271
  "start-metabase",
272
+ "agents-skills",
262
273
  "admin-user",
263
274
  "api-key",
264
275
  "transforms",
265
276
  "license",
266
277
  "git-sync",
278
+ "sync-scope",
267
279
  "databases",
268
280
  "summary"
269
281
  ]);
@@ -332,6 +344,9 @@ const REMOTE_SYNC_FEATURE = "remote-sync";
332
344
  function hasRemoteSync(license) {
333
345
  return license.activated && license.features.includes("remote-sync");
334
346
  }
347
+ function hasLibrary(license) {
348
+ return license.activated && license.features.includes("library");
349
+ }
335
350
  const DatabaseRecord = z.object({
336
351
  id: z.int(),
337
352
  name: z.string(),
@@ -354,7 +369,7 @@ const StepRecord = z.object({
354
369
  completedAt: z.string()
355
370
  });
356
371
  const InstanceState = z.object({
357
- schemaVersion: z.literal(6),
372
+ schemaVersion: z.literal(7),
358
373
  name: z.string(),
359
374
  runtime: RuntimeState.nullable(),
360
375
  url: z.string().nullable(),
@@ -365,6 +380,7 @@ const InstanceState = z.object({
365
380
  databases: z.array(DatabaseRecord),
366
381
  gitRemote: GitRemoteState.nullable(),
367
382
  movedFrom: z.string().nullable(),
383
+ syncRoot: z.int().nullable(),
368
384
  steps: z.partialRecord(StepId, StepRecord)
369
385
  });
370
386
  function configuredRuntime(state) {
@@ -394,7 +410,7 @@ function mbCommand(profile) {
394
410
  }
395
411
  function emptyInstanceState(name) {
396
412
  return {
397
- schemaVersion: 6,
413
+ schemaVersion: 7,
398
414
  name,
399
415
  runtime: null,
400
416
  url: null,
@@ -405,6 +421,7 @@ function emptyInstanceState(name) {
405
421
  databases: [],
406
422
  gitRemote: null,
407
423
  movedFrom: null,
424
+ syncRoot: null,
408
425
  steps: {}
409
426
  };
410
427
  }
@@ -416,6 +433,7 @@ const V2 = 2;
416
433
  const V3 = 3;
417
434
  const V4 = 4;
418
435
  const V5 = 5;
436
+ const V6 = 6;
419
437
  const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
420
438
  const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
421
439
  const AdminV3 = z.discriminatedUnion("kind", [z.object({
@@ -427,7 +445,11 @@ const AdminV3 = z.discriminatedUnion("kind", [z.object({
427
445
  password: z.string()
428
446
  })]);
429
447
  const StateV4 = z.object({ schemaVersion: z.literal(V4) }).loose();
430
- const StateV5 = z.object({ schemaVersion: z.literal(V5) }).loose();
448
+ const StateV5 = z.object({
449
+ schemaVersion: z.literal(V5),
450
+ steps: z.record(z.string(), z.unknown())
451
+ }).loose();
452
+ const StateV6 = z.object({ schemaVersion: z.literal(V6) }).loose();
431
453
  const StateV3 = z.object({
432
454
  schemaVersion: z.literal(V3),
433
455
  admin: AdminV3.nullable(),
@@ -454,10 +476,19 @@ function liftV4(raw) {
454
476
  };
455
477
  }
456
478
  function liftV5(raw) {
457
- const { sampleDatabase: _sampleDatabase, ...state } = StateV5.parse(raw);
479
+ const { sampleDatabase: _sampleDatabase, steps, ...state } = StateV5.parse(raw);
480
+ const { "sample-database": _sampleDatabaseStep, ...remainingSteps } = steps;
458
481
  return {
459
482
  ...state,
460
- schemaVersion: 6
483
+ schemaVersion: V6,
484
+ steps: remainingSteps
485
+ };
486
+ }
487
+ function liftV6(raw) {
488
+ return {
489
+ ...StateV6.parse(raw),
490
+ schemaVersion: 7,
491
+ syncRoot: null
461
492
  };
462
493
  }
463
494
  function adminV4(admin) {
@@ -498,24 +529,29 @@ function migrateInstanceState(raw) {
498
529
  case V2: return migrateInstanceState(liftV2(raw));
499
530
  case V3: return liftV3(raw);
500
531
  case V4: return migrateInstanceState(liftV4(raw));
501
- case V5: return {
502
- state: InstanceState.parse(liftV5(raw)),
532
+ case V5: return migrateInstanceState(liftV5(raw));
533
+ case V6: return {
534
+ state: InstanceState.parse(liftV6(raw)),
503
535
  movedSecrets: null
504
536
  };
505
- case 6: return {
537
+ case 7: return {
506
538
  state: InstanceState.parse(raw),
507
539
  movedSecrets: null
508
540
  };
509
- default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 6)`);
541
+ default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 7)`);
510
542
  }
511
543
  }
512
544
  //#endregion
513
545
  //#region src/env.ts
514
546
  const ENV_RDE_HOME = "RDE_HOME";
515
- const ENV_RDE_SKILL_SOURCE = "RDE_SKILL_SOURCE";
547
+ const ENV_RDE_DEV = "RDE_DEV";
516
548
  function readEnv(name) {
517
549
  return process.env[name];
518
550
  }
551
+ function readSetEnv(name) {
552
+ const value = readEnv(name);
553
+ return value === void 0 || value.length === 0 ? null : value;
554
+ }
519
555
  const ENV_PATH = "PATH";
520
556
  const ENV_NO_COLOR = "NO_COLOR";
521
557
  const ENV_XDG_STATE_HOME = "XDG_STATE_HOME";
@@ -523,6 +559,9 @@ const ENV_JAVA_HOME = "JAVA_HOME";
523
559
  const ENV_MB_PROFILE = "MB_PROFILE";
524
560
  const ENV_MB_URL = "MB_URL";
525
561
  const ENV_MB_API_KEY = "MB_API_KEY";
562
+ function developmentMode() {
563
+ return readSetEnv(ENV_RDE_DEV) === "1";
564
+ }
526
565
  function hostEnvironment() {
527
566
  return { ...process.env };
528
567
  }
@@ -541,8 +580,8 @@ const INSTANCE_NAME = /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
541
580
  const INSTANCE_NAME_RULE = "letters, digits, - and _, starting with a letter or digit";
542
581
  const HOME_TAG_LENGTH = 8;
543
582
  function rdeHome() {
544
- const override = readEnv(ENV_RDE_HOME);
545
- if (override !== void 0) return resolve(override);
583
+ const override = readSetEnv(ENV_RDE_HOME);
584
+ if (override !== null) return resolve(override);
546
585
  return join(homedir(), RDE_DIR_NAME);
547
586
  }
548
587
  function homeTag(home) {
@@ -814,8 +853,11 @@ const SESSION_HEADER = "X-Metabase-Session";
814
853
  const API_KEY_HEADER = "X-Api-Key";
815
854
  const JSON_CONTENT_TYPE = "application/json";
816
855
  const SERVICE_UNAVAILABLE_STATUS = 503;
856
+ const NO_CONTENT_STATUS = 204;
817
857
  const NO_AUTH = { kind: "none" };
818
858
  const ErrorMessage = z.object({ message: z.string() }).loose();
859
+ const ErrorTitle = z.object({ error: z.string() }).loose();
860
+ const ErrorCode = z.object({ error_code: z.string() }).loose();
819
861
  const ErrorFields = z.object({ errors: z.record(z.string(), z.unknown()) }).loose();
820
862
  const ErrorText = z.string();
821
863
  const ErrorTexts = z.array(z.string());
@@ -878,6 +920,8 @@ function serverMessage(body) {
878
920
  const value = jsonValue(body);
879
921
  const parsed = ErrorMessage.safeParse(value);
880
922
  if (parsed.success) return parsed.data.message;
923
+ const title = ErrorTitle.safeParse(value);
924
+ if (title.success) return title.data.error;
881
925
  const errors = serverErrors(body);
882
926
  if (errors !== null && Object.keys(errors).length > 0) return Object.entries(errors).map(([name, problem]) => `${name}: ${problem}`).join("; ");
883
927
  const plain = ErrorText.safeParse(value);
@@ -886,6 +930,10 @@ function serverMessage(body) {
886
930
  if (body.json.parsed || text.length === 0) return `HTTP ${body.status}`;
887
931
  return text;
888
932
  }
933
+ function errorCode(body) {
934
+ const parsed = ErrorCode.safeParse(jsonValue(body));
935
+ return parsed.success ? parsed.data.error_code : null;
936
+ }
889
937
  function failureFor(path, body) {
890
938
  if (body.status === SERVICE_UNAVAILABLE_STATUS) {
891
939
  const initializing = Initializing.safeParse(jsonValue(body));
@@ -895,7 +943,9 @@ function failureFor(path, body) {
895
943
  status: body.status,
896
944
  path,
897
945
  message: serverMessage(body),
898
- errors: serverErrors(body)
946
+ errors: serverErrors(body),
947
+ code: errorCode(body),
948
+ detail: body.json.parsed ? body.json.value : null
899
949
  });
900
950
  }
901
951
  function shapeError(path, body, reason) {
@@ -903,7 +953,9 @@ function shapeError(path, body, reason) {
903
953
  status: body.status,
904
954
  path,
905
955
  message: reason,
906
- errors: null
956
+ errors: null,
957
+ code: null,
958
+ detail: null
907
959
  });
908
960
  }
909
961
  function isTimeout(error) {
@@ -920,6 +972,7 @@ function lookupFailure(error) {
920
972
  }
921
973
  function createHttp(options) {
922
974
  const fetchImpl = options.fetchImpl ?? fetch;
975
+ const timeoutMs = options.timeoutMs ?? REQUEST_TIMEOUT_MS;
923
976
  const baseUrl = options.baseUrl.replace(/\/+$/, "");
924
977
  async function send(method, path, body) {
925
978
  const url = `${baseUrl}${path}`;
@@ -930,7 +983,7 @@ function createHttp(options) {
930
983
  const init = {
931
984
  method,
932
985
  headers,
933
- signal: AbortSignal.any([options.signal, AbortSignal.timeout(REQUEST_TIMEOUT_MS)])
986
+ signal: AbortSignal.any([options.signal, AbortSignal.timeout(timeoutMs)])
934
987
  };
935
988
  if (body !== void 0) {
936
989
  headers["content-type"] = JSON_CONTENT_TYPE;
@@ -944,7 +997,7 @@ function createHttp(options) {
944
997
  };
945
998
  } catch (error) {
946
999
  if (options.signal.aborted) throw new AbortError();
947
- if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${REQUEST_TIMEOUT_MS}ms`);
1000
+ if (isTimeout(error)) throw new MetabaseUnreachableError(url, `no response within ${timeoutMs}ms`);
948
1001
  throw new MetabaseUnreachableError(url, describeCause(error));
949
1002
  }
950
1003
  }
@@ -953,19 +1006,33 @@ function createHttp(options) {
953
1006
  if (!received.ok) throw failureFor(path, received.body);
954
1007
  return received.body;
955
1008
  }
1009
+ function parseJson(path, body, schema) {
1010
+ if (!body.json.parsed) throw shapeError(path, body, "expected a JSON body");
1011
+ return parseShape(path, body, schema, body.json.value);
1012
+ }
1013
+ function parseShape(path, body, schema, value) {
1014
+ try {
1015
+ return schema.parse(value);
1016
+ } catch (error) {
1017
+ if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
1018
+ throw error;
1019
+ }
1020
+ }
956
1021
  return {
957
1022
  async requestJson(method, path, request) {
1023
+ return parseJson(path, await requestBody(method, path, request.body), request.schema);
1024
+ },
1025
+ async requestOptionalJson(method, path, request) {
958
1026
  const body = await requestBody(method, path, request.body);
959
- if (!body.json.parsed) throw shapeError(path, body, "expected a JSON body");
960
- try {
961
- return request.schema.parse(body.json.value);
962
- } catch (error) {
963
- if (error instanceof ZodError) throw shapeError(path, body, `unexpected response shape:\n${z.prettifyError(error)}`);
964
- throw error;
965
- }
1027
+ if (body.status === NO_CONTENT_STATUS && body.text.length === 0) return null;
1028
+ return parseJson(path, body, request.schema);
966
1029
  },
967
1030
  async requestNoContent(method, path, request = {}) {
968
1031
  await requestBody(method, path, request.body);
1032
+ },
1033
+ async requestText(method, path, request) {
1034
+ const body = await requestBody(method, path, request.body);
1035
+ return parseShape(path, body, request.schema, body.text);
969
1036
  }
970
1037
  };
971
1038
  }
@@ -1056,6 +1123,48 @@ function apiKeyResource(http) {
1056
1123
  regenerate
1057
1124
  };
1058
1125
  }
1126
+ const Collection = z.object({
1127
+ id: z.int(),
1128
+ name: z.string(),
1129
+ location: z.string(),
1130
+ type: z.string().nullable(),
1131
+ namespace: z.string().nullable(),
1132
+ personal_owner_id: z.int().nullable(),
1133
+ is_remote_synced: z.boolean()
1134
+ }).loose();
1135
+ const RootCollection = z.object({ id: z.literal("root") }).loose();
1136
+ const CollectionList = z.array(z.union([Collection, RootCollection]));
1137
+ const CollectionRef = z.object({
1138
+ id: z.int(),
1139
+ name: z.string()
1140
+ }).loose();
1141
+ //#endregion
1142
+ //#region src/metabase/resources/collection.ts
1143
+ function collectionResource(http) {
1144
+ /** GET /api/collection */
1145
+ async function list() {
1146
+ return http.requestJson("GET", "/api/collection", { schema: CollectionList });
1147
+ }
1148
+ /** POST /api/collection */
1149
+ async function create(request) {
1150
+ return http.requestJson("POST", "/api/collection", {
1151
+ body: {
1152
+ ...request,
1153
+ parent_id: null
1154
+ },
1155
+ schema: CollectionRef
1156
+ });
1157
+ }
1158
+ /** POST /api/ee/library; 400 when the Library exists */
1159
+ async function createLibrary() {
1160
+ return http.requestJson("POST", "/api/ee/library", { schema: CollectionRef });
1161
+ }
1162
+ return {
1163
+ list,
1164
+ create,
1165
+ createLibrary
1166
+ };
1167
+ }
1059
1168
  //#endregion
1060
1169
  //#region src/metabase/domain/database.ts
1061
1170
  const InitialSyncStatus = z.enum([
@@ -1148,7 +1257,9 @@ function permissionsGroupResource(http) {
1148
1257
  }
1149
1258
  return { list };
1150
1259
  }
1151
- z.enum(["read-only", "read-write"]);
1260
+ //#endregion
1261
+ //#region src/metabase/domain/remote-sync.ts
1262
+ const RemoteSyncType = z.enum(["read-only", "read-write"]);
1152
1263
  const RemoteSyncSettingsResult = z.object({
1153
1264
  success: z.boolean(),
1154
1265
  task_id: z.int().optional()
@@ -1169,6 +1280,38 @@ const SyncTask = z.object({
1169
1280
  status: SyncTaskStatus$1,
1170
1281
  error_message: z.string().nullable().optional()
1171
1282
  }).loose();
1283
+ const NamedCollection = z.object({
1284
+ id: z.int(),
1285
+ name: z.string()
1286
+ }).loose();
1287
+ const DependencyLocation = NamedCollection.nullable().optional();
1288
+ const DependencyItem = z.object({
1289
+ model: z.string(),
1290
+ id: z.int(),
1291
+ name: z.string().nullable()
1292
+ }).loose();
1293
+ const UnsyncedDependency = DependencyItem.extend({
1294
+ collection: DependencyLocation,
1295
+ used_by: z.array(DependencyItem)
1296
+ });
1297
+ const SyncRemedy = z.discriminatedUnion("type", [
1298
+ z.object({
1299
+ type: z.literal("collection"),
1300
+ collection: NamedCollection
1301
+ }).loose(),
1302
+ z.object({ type: z.literal("library") }).loose(),
1303
+ z.object({
1304
+ type: z.literal("none"),
1305
+ collection: DependencyLocation
1306
+ }).loose()
1307
+ ]);
1308
+ const RequiredSync = z.object({
1309
+ remedy: SyncRemedy,
1310
+ syncable: z.boolean(),
1311
+ blocks: z.array(NamedCollection),
1312
+ dependencies: z.array(UnsyncedDependency)
1313
+ }).loose();
1314
+ const UnsyncedDependencies = z.object({ errors: z.object({ required: z.array(RequiredSync) }).loose() }).loose();
1172
1315
  //#endregion
1173
1316
  //#region src/metabase/resources/remote-sync.ts
1174
1317
  function remoteSyncResource(http) {
@@ -1179,6 +1322,13 @@ function remoteSyncResource(http) {
1179
1322
  schema: RemoteSyncSettingsResult
1180
1323
  });
1181
1324
  }
1325
+ /** PUT /api/ee/remote-sync/settings, without the git settings, which it leaves as they are */
1326
+ async function updateScope(scope) {
1327
+ return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
1328
+ body: scope,
1329
+ schema: RemoteSyncSettingsResult
1330
+ });
1331
+ }
1182
1332
  /** PUT /api/ee/remote-sync/settings with only `remote-sync-transforms`, which skips the repository check. */
1183
1333
  async function syncTransforms() {
1184
1334
  return http.requestJson("PUT", "/api/ee/remote-sync/settings", {
@@ -1195,10 +1345,11 @@ function remoteSyncResource(http) {
1195
1345
  }
1196
1346
  /** GET /api/ee/remote-sync/current-task */
1197
1347
  async function currentTask() {
1198
- return http.requestJson("GET", "/api/ee/remote-sync/current-task", { schema: SyncTask.nullable() });
1348
+ return http.requestOptionalJson("GET", "/api/ee/remote-sync/current-task", { schema: SyncTask });
1199
1349
  }
1200
1350
  return {
1201
1351
  updateSettings,
1352
+ updateScope,
1202
1353
  syncTransforms,
1203
1354
  testConnection,
1204
1355
  currentTask
@@ -1271,6 +1422,7 @@ const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
1271
1422
  const ENGINES_SETTING = "engines";
1272
1423
  const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
1273
1424
  const TRANSFORMS_SETUP_COMPLETE_SETTING = "transforms-setup-complete";
1425
+ const REMOTE_SYNC_TYPE_SETTING = "remote-sync-type";
1274
1426
  function settingResource(http) {
1275
1427
  /** PUT /api/setting/premium-embedding-token */
1276
1428
  async function setPremiumEmbeddingToken(value) {
@@ -1288,11 +1440,16 @@ function settingResource(http) {
1288
1440
  async function engines() {
1289
1441
  return http.requestJson("GET", `/api/setting/${ENGINES_SETTING}`, { schema: Engines });
1290
1442
  }
1443
+ /** GET /api/setting/remote-sync-type; Metabase answers its default, read-only, while unset. */
1444
+ async function remoteSyncType() {
1445
+ return http.requestText("GET", `/api/setting/${REMOTE_SYNC_TYPE_SETTING}`, { schema: RemoteSyncType });
1446
+ }
1291
1447
  return {
1292
1448
  setPremiumEmbeddingToken,
1293
1449
  enableTransforms,
1294
1450
  transformsDecided,
1295
- engines
1451
+ engines,
1452
+ remoteSyncType
1296
1453
  };
1297
1454
  }
1298
1455
  //#endregion
@@ -1362,6 +1519,7 @@ function createMetabaseClient(baseUrl, auth, signal, options = {}) {
1362
1519
  tokenStatus: tokenStatusResource(http),
1363
1520
  database: databaseResource(http),
1364
1521
  remoteSync: remoteSyncResource(http),
1522
+ collection: collectionResource(http),
1365
1523
  user: userResource(http)
1366
1524
  };
1367
1525
  }
@@ -2057,18 +2215,74 @@ async function detectAgents(probe) {
2057
2215
  }
2058
2216
  //#endregion
2059
2217
  //#region src/metabase/domain/version-info.ts
2060
- const VersionInfo = z.object({ latest: z.object({ version: z.string() }).loose() }).loose();
2218
+ const VersionEntry = z.object({ version: z.string() }).loose();
2219
+ const VersionInfo = z.object({ latest: VersionEntry }).loose();
2220
+ const VersionHistory = z.object({
2221
+ latest: VersionEntry,
2222
+ older: z.array(VersionEntry)
2223
+ }).loose();
2061
2224
  //#endregion
2062
2225
  //#region src/metabase/releases.ts
2063
2226
  const DOWNLOADS_HOST = "https://downloads.metabase.com";
2064
2227
  const VERSION_INFO_HOST = "https://static.metabase.com";
2065
2228
  const VERSION_INFO_PATH = "/version-info-ee.json";
2066
2229
  const JAR_FILE = "metabase.jar";
2067
- const RELEASE_TAG$1 = /^v(?<edition>[01])\.\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
2230
+ const RELEASE_TAG$1 = /^v(?<edition>[01])\.(?<major>\d+)\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
2231
+ const MAJOR_LINE_TAG = /^v(?<edition>[01])\.(?<major>\d+)\.x$/;
2232
+ const FINAL_ENTERPRISE_RELEASE = /^v1\.\d+\.\d+(?:\.\d+)?$/;
2068
2233
  const ENTERPRISE_EDITION = "1";
2234
+ const OPEN_SOURCE_EDITION = "0";
2235
+ const SNAPSHOT_SUFFIX = "-SNAPSHOT";
2069
2236
  function isReleaseTag(value) {
2070
2237
  return RELEASE_TAG$1.test(value);
2071
2238
  }
2239
+ function isMajorLineTag(value) {
2240
+ return MAJOR_LINE_TAG.test(value);
2241
+ }
2242
+ function majorLineTag(major) {
2243
+ return `v${ENTERPRISE_EDITION}.${major}.x`;
2244
+ }
2245
+ function tagGroups(tag) {
2246
+ return (RELEASE_TAG$1.exec(tag) ?? MAJOR_LINE_TAG.exec(tag))?.groups;
2247
+ }
2248
+ function isOpenSourceRelease(version) {
2249
+ return tagGroups(version)?.["edition"] === OPEN_SOURCE_EDITION;
2250
+ }
2251
+ function isMasterBuild(version) {
2252
+ return !(isReleaseTag(version) || isMajorLineTag(version)) || version.endsWith(SNAPSHOT_SUFFIX);
2253
+ }
2254
+ function releaseMajor(version) {
2255
+ const major = tagGroups(version)?.["major"];
2256
+ return major === void 0 ? null : Number.parseInt(major, 10);
2257
+ }
2258
+ function releaseNumbers(version) {
2259
+ return version.slice(1).split(".").map((part) => Number.parseInt(part, 10));
2260
+ }
2261
+ function compareReleases(left, right) {
2262
+ const a = releaseNumbers(left);
2263
+ const b = releaseNumbers(right);
2264
+ for (let index = 0; index < Math.max(a.length, b.length); index += 1) {
2265
+ const difference = (a[index] ?? 0) - (b[index] ?? 0);
2266
+ if (difference !== 0) return difference;
2267
+ }
2268
+ return 0;
2269
+ }
2270
+ function supportedRelease(version) {
2271
+ if (!FINAL_ENTERPRISE_RELEASE.test(version)) return null;
2272
+ const major = releaseMajor(version);
2273
+ return major !== null && major >= 65 ? {
2274
+ version,
2275
+ major
2276
+ } : null;
2277
+ }
2278
+ function newestSupportedRelease(versions) {
2279
+ let newest = null;
2280
+ for (const version of versions) {
2281
+ const release = supportedRelease(version);
2282
+ if (release !== null && (newest === null || compareReleases(version, newest.version) > 0)) newest = release;
2283
+ }
2284
+ return newest;
2285
+ }
2072
2286
  function jarDownloadUrl(version) {
2073
2287
  const edition = RELEASE_TAG$1.exec(version)?.groups?.["edition"];
2074
2288
  if (edition === void 0) throw new Error(`${version} is not a Metabase release tag`);
@@ -2081,9 +2295,15 @@ function releaseLookup(signal, options = {}) {
2081
2295
  signal,
2082
2296
  ...options
2083
2297
  });
2084
- return { async latestVersion() {
2085
- return (await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionInfo })).latest.version;
2086
- } };
2298
+ return {
2299
+ async latestVersion() {
2300
+ return (await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionInfo })).latest.version;
2301
+ },
2302
+ async newestSupportedRelease() {
2303
+ const info = await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionHistory });
2304
+ return newestSupportedRelease([info.latest, ...info.older].map((entry) => entry.version));
2305
+ }
2306
+ };
2087
2307
  }
2088
2308
  function fileDownloader(signal, options = {}) {
2089
2309
  return (url, destination, onProgress) => downloadFile(url, destination, {
@@ -2151,11 +2371,13 @@ const PortBinding = z.array(z.object({
2151
2371
  HostIp: z.string(),
2152
2372
  HostPort: z.string()
2153
2373
  }).loose()).nullable();
2374
+ const EnvEntry = z.string().regex(/^[^=]+=/, "expected NAME=value");
2154
2375
  const InspectedContainer = z.object({
2155
2376
  State: z.object({ Running: z.boolean() }).loose(),
2156
2377
  Config: z.object({
2157
2378
  Image: z.string(),
2158
- Labels: z.record(z.string(), z.string()).nullish()
2379
+ Labels: z.record(z.string(), z.string()).nullish(),
2380
+ Env: z.array(EnvEntry).nullish()
2159
2381
  }).loose(),
2160
2382
  HostConfig: z.object({ PortBindings: z.record(z.string(), PortBinding).nullable() }).loose(),
2161
2383
  NetworkSettings: z.object({ Ports: z.record(z.string(), PortBinding).nullish() }).loose()
@@ -2186,6 +2408,14 @@ function portMappings(bindings) {
2186
2408
  }
2187
2409
  return mappings;
2188
2410
  }
2411
+ function containerEnv(entries) {
2412
+ const env = {};
2413
+ for (const entry of entries) {
2414
+ const separator = entry.indexOf("=");
2415
+ env[entry.slice(0, separator)] = entry.slice(separator + 1);
2416
+ }
2417
+ return env;
2418
+ }
2189
2419
  function createArgs(options) {
2190
2420
  const args = [
2191
2421
  "create",
@@ -2277,7 +2507,8 @@ function dockerTool(runner) {
2277
2507
  image: labels?.[IMAGE_LABEL] ?? container.Config.Image,
2278
2508
  ports: portMappings(container.HostConfig.PortBindings),
2279
2509
  forwarded: portMappings(container.NetworkSettings.Ports ?? null),
2280
- home: labels?.[HOME_LABEL] ?? null
2510
+ home: labels?.[HOME_LABEL] ?? null,
2511
+ env: containerEnv(container.Config.Env ?? [])
2281
2512
  };
2282
2513
  }
2283
2514
  async function portClaims() {
@@ -2474,6 +2705,10 @@ const PLUGINS_DIR = "plugins";
2474
2705
  const METABASE_STOP_GRACE_MS = 3e4;
2475
2706
  const DB_DIR = "metabase.db";
2476
2707
  const DB_FILE = "metabase.db";
2708
+ const STAGING_STORE_ENV = {
2709
+ MB_RUN_MODE: "e2e",
2710
+ MB_STORE_USE_STAGING: "true"
2711
+ };
2477
2712
  function instanceUrl(port) {
2478
2713
  return `http://localhost:${port}`;
2479
2714
  }
@@ -2482,13 +2717,15 @@ function serverEnv(options) {
2482
2717
  MB_DB_FILE: options.dbFile,
2483
2718
  MB_PLUGINS_DIR: options.pluginsDir,
2484
2719
  MB_SITE_URL: instanceUrl(options.port),
2485
- MB_RUN_MODE: "e2e",
2486
- MB_STORE_USE_STAGING: "true",
2720
+ ...developmentMode() ? STAGING_STORE_ENV : {},
2487
2721
  MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all",
2488
2722
  MB_LOAD_SAMPLE_CONTENT: "false",
2489
2723
  MB_INSTALL_ANALYTICS_DATABASE: "false"
2490
2724
  };
2491
2725
  }
2726
+ function envDrift(actual, expected) {
2727
+ return [.../* @__PURE__ */ new Set([...Object.keys(expected), ...Object.keys(STAGING_STORE_ENV)])].filter((name) => actual[name] !== expected[name]);
2728
+ }
2492
2729
  function jettyEnv(port) {
2493
2730
  return {
2494
2731
  MB_JETTY_HOST: LOOPBACK_HOST,
@@ -2497,7 +2734,34 @@ function jettyEnv(port) {
2497
2734
  }
2498
2735
  //#endregion
2499
2736
  //#region src/runtime/docker.ts
2500
- const DEFAULT_IMAGE = "metabase/metabase-enterprise-head:latest";
2737
+ const RELEASE_REPOSITORY = "metabase/metabase-enterprise";
2738
+ const DEVELOPMENT_IMAGE = "metabase/metabase-enterprise-head:latest";
2739
+ const HEAD_SUFFIX = "-head";
2740
+ const METABASE_REPOSITORIES = /* @__PURE__ */ new Set([
2741
+ RELEASE_REPOSITORY,
2742
+ `${RELEASE_REPOSITORY}${HEAD_SUFFIX}`,
2743
+ "metabase/metabase"
2744
+ ]);
2745
+ function majorLineImage(major) {
2746
+ return `${RELEASE_REPOSITORY}:${majorLineTag(major)}`;
2747
+ }
2748
+ function imageTag(ref) {
2749
+ const colon = ref.lastIndexOf(":");
2750
+ if (colon === -1 || colon < ref.lastIndexOf("/")) return null;
2751
+ return ref.slice(colon + 1);
2752
+ }
2753
+ function imageRepository(ref) {
2754
+ const digest = ref.indexOf("@");
2755
+ const named = digest === -1 ? ref : ref.slice(0, digest);
2756
+ const tag = imageTag(named);
2757
+ return tag === null ? named : named.slice(0, named.length - tag.length - 1);
2758
+ }
2759
+ function isMasterBuildImage(ref) {
2760
+ return imageRepository(ref).endsWith(HEAD_SUFFIX);
2761
+ }
2762
+ function isMetabaseImage(ref) {
2763
+ return METABASE_REPOSITORIES.has(imageRepository(ref));
2764
+ }
2501
2765
  const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
2502
2766
  const CONTAINER_PORT = 3e3;
2503
2767
  const STOP = { graceMs: METABASE_STOP_GRACE_MS };
@@ -2512,6 +2776,16 @@ function onLoopback(mappings, port) {
2512
2776
  function hostUserFor(platform, hostUser) {
2513
2777
  return platform === "linux" ? hostUser : null;
2514
2778
  }
2779
+ function containerServerEnv(port) {
2780
+ return serverEnv({
2781
+ dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
2782
+ pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
2783
+ port
2784
+ });
2785
+ }
2786
+ function containerEnvDrift(container, state) {
2787
+ return envDrift(container.env, containerServerEnv(state.port));
2788
+ }
2515
2789
  function dockerRuntime(state, deps) {
2516
2790
  const { docker } = deps;
2517
2791
  const url = instanceUrl(state.port);
@@ -2538,11 +2812,7 @@ function dockerRuntime(state, deps) {
2538
2812
  source: deps.dataDir,
2539
2813
  target: CONTAINER_DATA_DIR
2540
2814
  },
2541
- env: serverEnv({
2542
- dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
2543
- pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
2544
- port: state.port
2545
- }),
2815
+ env: containerServerEnv(state.port),
2546
2816
  addHostGateway: deps.platform === "linux",
2547
2817
  hostUser: hostUserFor(deps.platform, deps.hostUser)
2548
2818
  };
@@ -2648,6 +2918,7 @@ function parseJarSpec(value) {
2648
2918
  path: resolve(trimmed)
2649
2919
  };
2650
2920
  const tagged = trimmed.startsWith(TAG_PREFIX) ? trimmed : `${TAG_PREFIX}${trimmed}`;
2921
+ if (isOpenSourceRelease(tagged)) throw new ConfigError(`${trimmed} is a release of the open-source edition, which rde does not support; enterprise release tags start with v1`);
2651
2922
  return isReleaseTag(tagged) ? {
2652
2923
  kind: "release",
2653
2924
  version: tagged
@@ -2904,12 +3175,12 @@ async function portClaims(state, runtime) {
2904
3175
  }
2905
3176
  //#endregion
2906
3177
  //#region src/steps/admin-user.ts
2907
- const STEP_ID$8 = "admin-user";
3178
+ const STEP_ID$9 = "admin-user";
2908
3179
  const ADMIN_USER_KEYS = {
2909
- email: `${STEP_ID$8}.email`,
2910
- password: `${STEP_ID$8}.password`,
2911
- firstName: `${STEP_ID$8}.first-name`,
2912
- lastName: `${STEP_ID$8}.last-name`
3180
+ email: `${STEP_ID$9}.email`,
3181
+ password: `${STEP_ID$9}.password`,
3182
+ firstName: `${STEP_ID$9}.first-name`,
3183
+ lastName: `${STEP_ID$9}.last-name`
2913
3184
  };
2914
3185
  const GENERATED_ADMIN_EMAIL = "admin@metabase.local";
2915
3186
  const GENERATED_PASSWORD_BYTES = 24;
@@ -2924,7 +3195,7 @@ function generatePassword(random) {
2924
3195
  if (DIGIT.test(candidate)) return candidate;
2925
3196
  }
2926
3197
  }
2927
- function hostDeps$13(ctx) {
3198
+ function hostDeps$14(ctx) {
2928
3199
  return {
2929
3200
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
2930
3201
  instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
@@ -3062,9 +3333,9 @@ async function recordExistingAdmin(ctx, variant, client, url) {
3062
3333
  ctx.ui.success(`Admin user ${email} recorded`);
3063
3334
  return "completed";
3064
3335
  }
3065
- function adminUserStep(options, deps = hostDeps$13) {
3336
+ function adminUserStep(options, deps = hostDeps$14) {
3066
3337
  return {
3067
- id: STEP_ID$8,
3338
+ id: STEP_ID$9,
3068
3339
  title: "Create the admin user",
3069
3340
  redoHint: "An instance keeps the admin it was set up with; `rde credentials` shows it.",
3070
3341
  async detect(ctx) {
@@ -3134,7 +3405,7 @@ function adminUserStep(options, deps = hostDeps$13) {
3134
3405
  //#region src/tools/mb.ts
3135
3406
  const MB = "mb";
3136
3407
  const MB_PACKAGE = "@metabase/cli";
3137
- const MB_DIST_TAG = "alpha-release-65-integration-branch";
3408
+ const MB_DIST_TAG = "latest";
3138
3409
  const MB_INSTALL_SPEC = `${MB_PACKAGE}@${MB_DIST_TAG}`;
3139
3410
  const GLOBAL_INSTALL = {
3140
3411
  npm: {
@@ -3265,15 +3536,11 @@ function mbRemoveCommand(binary) {
3265
3536
  const name = owner === "homebrew" ? firstSegmentAfter(binary.resolved, CELLAR) : packageName(binary.resolved);
3266
3537
  return name === null ? null : REMOVE_COMMANDS[owner](name);
3267
3538
  }
3268
- function setValue(name) {
3269
- const value = readEnv(name);
3270
- return value === void 0 || value.length === 0 ? null : value;
3271
- }
3272
3539
  function mbEnvironment() {
3273
3540
  return {
3274
- profile: setValue(ENV_MB_PROFILE),
3275
- url: setValue(ENV_MB_URL),
3276
- apiKeySet: setValue(ENV_MB_API_KEY) !== null
3541
+ profile: readSetEnv(ENV_MB_PROFILE),
3542
+ url: readSetEnv(ENV_MB_URL),
3543
+ apiKeySet: readSetEnv(ENV_MB_API_KEY) !== null
3277
3544
  };
3278
3545
  }
3279
3546
  const ProbedUser = z.object({
@@ -3329,10 +3596,15 @@ const GitSyncTask = z.object({
3329
3596
  progress: z.number().nullable(),
3330
3597
  error_message: z.string().nullable().optional()
3331
3598
  }).loose();
3599
+ const SyncedCollection = z.object({
3600
+ id: z.int(),
3601
+ name: z.string()
3602
+ }).loose();
3332
3603
  const GitSyncStatus = z.object({
3333
3604
  branch: z.string().nullable(),
3334
3605
  is_dirty: z.boolean(),
3335
- current_task: GitSyncTask.nullable()
3606
+ current_task: GitSyncTask.nullable(),
3607
+ synced_collections: z.array(SyncedCollection)
3336
3608
  }).loose();
3337
3609
  const GitSyncOutcome = z.object({
3338
3610
  message: z.string().nullable(),
@@ -3344,6 +3616,9 @@ const RemoteChanges = z.object({
3344
3616
  remote_version: z.string().nullable(),
3345
3617
  local_version: z.string().nullable()
3346
3618
  }).loose();
3619
+ function remoteMoved(changes) {
3620
+ return changes.has_changes && changes.local_version !== null;
3621
+ }
3347
3622
  function comparableUrl(url) {
3348
3623
  const parsed = new URL(url);
3349
3624
  return `${parsed.origin}${parsed.pathname.replace(/\/+$/, "")}`;
@@ -3479,14 +3754,14 @@ function mbTool(runner) {
3479
3754
  }
3480
3755
  //#endregion
3481
3756
  //#region src/steps/api-key.ts
3482
- const STEP_ID$7 = "api-key";
3757
+ const STEP_ID$8 = "api-key";
3483
3758
  const API_KEY_KEYS = {
3484
- email: `${STEP_ID$7}.email`,
3485
- password: `${STEP_ID$7}.password`,
3486
- profile: `${STEP_ID$7}.profile`
3759
+ email: `${STEP_ID$8}.email`,
3760
+ password: `${STEP_ID$8}.password`,
3761
+ profile: `${STEP_ID$8}.profile`
3487
3762
  };
3488
3763
  const ADMINISTRATORS_GROUP_NAME = "Administrators";
3489
- function hostDeps$12(ctx) {
3764
+ function hostDeps$13(ctx) {
3490
3765
  return {
3491
3766
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
3492
3767
  runner: processRunner(ctx.signal),
@@ -3547,7 +3822,7 @@ async function authenticates(client) {
3547
3822
  }
3548
3823
  }
3549
3824
  async function forgetCompletion(ctx) {
3550
- const { [STEP_ID$7]: record, ...steps } = ctx.state.steps;
3825
+ const { [STEP_ID$8]: record, ...steps } = ctx.state.steps;
3551
3826
  if (record !== void 0) await ctx.saveState({
3552
3827
  ...ctx.state,
3553
3828
  steps
@@ -3624,15 +3899,15 @@ function usageNote(profile) {
3624
3899
  if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
3625
3900
  return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
3626
3901
  }
3627
- function apiKeyStep(deps = hostDeps$12) {
3902
+ function apiKeyStep(deps = hostDeps$13) {
3628
3903
  return {
3629
- id: STEP_ID$7,
3904
+ id: STEP_ID$8,
3630
3905
  title: "Connect the Metabase CLI",
3631
3906
  redoHint: "`rde status` shows the mb profile it uses.",
3632
3907
  async detect(ctx) {
3633
3908
  const configured = configuredRuntime(ctx.state);
3634
3909
  const apiKey = ctx.secrets.apiKey;
3635
- if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$7]?.status !== "done") return "todo";
3910
+ if (configured === null || apiKey === null || ctx.state.steps[STEP_ID$8]?.status !== "done") return "todo";
3636
3911
  const resolved = deps(ctx);
3637
3912
  if (!await resolved.instanceRunning()) return "todo";
3638
3913
  if (!isLoggedInAt(await mbTool(resolved.runner).authStatusIfInstalled(ctx.state.mbProfile), configured.url)) return "todo";
@@ -5577,102 +5852,289 @@ function gitTool(runner) {
5577
5852
  };
5578
5853
  }
5579
5854
  //#endregion
5580
- //#region src/steps/git-sync.ts
5581
- const STEP_ID$6 = "git-sync";
5582
- const GIT_SYNC_KEYS = {
5583
- location: `${STEP_ID$6}.location`,
5584
- path: `${STEP_ID$6}.path`,
5585
- remote: `${STEP_ID$6}.remote`,
5586
- url: `${STEP_ID$6}.url`,
5587
- token: `${STEP_ID$6}.token`
5588
- };
5589
- const BARE_REPO_NAME = "content.git";
5590
- const DEFAULT_BRANCH = "main";
5591
- const ORIGIN = "origin";
5592
- const FALLBACK_REMOTE_NAME = "rde-remote";
5593
- const README_NAME = "README.md";
5594
- const README_CONTENT = "# Metabase content\n\nMetabase content synced by rde.\n";
5595
- const INITIAL_COMMIT_MESSAGE = "Initial commit";
5596
- const GITIGNORE_NAME = ".gitignore";
5597
- const IGNORED_PATHS = [
5598
- ".env",
5599
- ".env.local",
5600
- ".env.*.local",
5601
- "node_modules/",
5602
- ".vite/",
5603
- ".scratch/",
5604
- ".DS_Store"
5605
- ];
5606
- const HTTPS_PREFIX = "https://";
5607
- const HOME_PREFIX = "~/";
5608
- const BAD_REQUEST_STATUS = 400;
5609
- const TASK_POLL_INTERVAL_MS = 1e3;
5610
- const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
5611
- "successful",
5612
- "errored",
5613
- "cancelled",
5614
- "timed-out",
5615
- "conflict"
5616
- ]);
5617
- const SYNC_MODE_SETTINGS = {
5618
- "remote-sync-type": "read-write",
5619
- "remote-sync-transforms": true
5855
+ //#region src/steps/license.ts
5856
+ const STEP_ID$7 = "license";
5857
+ const LICENSE_KEYS = { token: `${STEP_ID$7}.token` };
5858
+ const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
5859
+ const NOT_FOUND_STATUS$1 = 404;
5860
+ const TOKEN_REFUSED_STATUS = 400;
5861
+ const STORE_UNREACHABLE_STATUS = 503;
5862
+ const TOKEN_CHECK_HOST = "token-check.metabase.com";
5863
+ const ACTIVATION_FAILED = "License activation failed";
5864
+ const LICENSED_FEATURES = {
5865
+ [REMOTE_SYNC_FEATURE]: "remote sync",
5866
+ "data-apps": "data apps",
5867
+ "transforms-testing": "transform tests"
5620
5868
  };
5621
- function hostDeps$11(ctx) {
5869
+ const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
5870
+ function licensedFeatureCoverage(features) {
5871
+ const entries = Object.entries(LICENSED_FEATURES);
5872
+ return {
5873
+ included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
5874
+ missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
5875
+ };
5876
+ }
5877
+ function listOf(names, type) {
5878
+ return new Intl.ListFormat("en", { type }).format(names);
5879
+ }
5880
+ function hostDeps$12(ctx) {
5622
5881
  return {
5623
- runner: processRunner(ctx.signal),
5624
5882
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
5625
- cwd: process.cwd(),
5626
- dataDir: instanceDataDir(ctx.state.name),
5627
- taskPollIntervalMs: TASK_POLL_INTERVAL_MS
5883
+ runner: processRunner(ctx.signal),
5884
+ instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
5885
+ terminal: stdinIsTty()
5628
5886
  };
5629
5887
  }
5630
- const SKIP_GIT_SYNC = "skip";
5631
- const FRESH = {
5632
- remoteKind: null,
5633
- hostedUrl: null,
5634
- workdir: null
5635
- };
5636
- function expandHome(path) {
5637
- return path.startsWith(HOME_PREFIX) ? join(homedir(), path.slice(2)) : path;
5888
+ function isNoTokenYet(error) {
5889
+ if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
5890
+ return isMetabaseNotServing(error);
5638
5891
  }
5639
- async function directoryState(path) {
5640
- let stat;
5892
+ function validateToken(value) {
5893
+ if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
5894
+ return "A license token is 64 hexadecimal characters";
5895
+ }
5896
+ function requireApiKey$1(ctx) {
5897
+ if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
5898
+ return ctx.secrets.apiKey;
5899
+ }
5900
+ function licenseOf(status) {
5901
+ if (!status.valid) return { activated: false };
5902
+ return {
5903
+ activated: true,
5904
+ features: status.features,
5905
+ validThru: status["valid-thru"] ?? null
5906
+ };
5907
+ }
5908
+ async function liveLicense(client) {
5641
5909
  try {
5642
- stat = await promises.stat(path);
5910
+ return licenseOf(await client.tokenStatus.get());
5643
5911
  } catch (error) {
5644
- if (error instanceof Error && "code" in error && error.code === "ENOENT") return "missing";
5912
+ if (error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$1) return { activated: false };
5645
5913
  throw error;
5646
5914
  }
5647
- if (!stat.isDirectory()) return "file";
5648
- return (await promises.readdir(path)).length === 0 ? "empty" : "populated";
5649
- }
5650
- async function isCloneTarget(path) {
5651
- const state = await directoryState(path);
5652
- return state === "missing" || state === "empty";
5653
5915
  }
5654
- async function initTargetProblem(run, path) {
5655
- switch (await directoryState(path)) {
5656
- case "missing":
5657
- await promises.mkdir(path, { recursive: true });
5658
- return null;
5659
- case "file": return `${path} is not a directory`;
5660
- case "empty": return null;
5661
- case "populated": return await run.git.isRepo(path) ? null : `${path} is not empty and not a git repository; pick an empty directory or a repository`;
5662
- }
5916
+ async function recordLiveLicense(ctx, client) {
5917
+ const live = await liveLicense(client);
5918
+ if (!isDeepStrictEqual(live, ctx.state.license)) await ctx.saveState({
5919
+ ...ctx.state,
5920
+ license: live
5921
+ });
5922
+ return live;
5663
5923
  }
5664
- async function isCloneOf(run, path, url) {
5665
- return await run.git.isRepo(path) && await run.git.remoteUrl(path, ORIGIN) === url;
5924
+ function activeLabel(status) {
5925
+ const plan = status["plan-alias"];
5926
+ return plan === void 0 ? "License active" : `License active (plan ${plan})`;
5666
5927
  }
5667
- async function cloneTargetProblem(run, path, url) {
5668
- switch (await directoryState(path)) {
5669
- case "missing":
5670
- case "empty": return null;
5671
- case "file": return `${path} is not a directory`;
5672
- case "populated": return await isCloneOf(run, path, url) ? null : `${path} is not empty; pick an empty directory, or use an existing repository`;
5673
- }
5928
+ function refusalOf(error) {
5929
+ if (!(error instanceof MetabaseApiError)) return null;
5930
+ if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
5931
+ if (error.status === STORE_UNREACHABLE_STATUS) return new ConfigError(`The instance could not reach ${TOKEN_CHECK_HOST} to check the license token; it needs outbound network access`);
5932
+ return null;
5674
5933
  }
5675
- async function existingRepoProblem(run, path) {
5934
+ async function activate(client, token) {
5935
+ try {
5936
+ await client.setting.setPremiumEmbeddingToken(token);
5937
+ } catch (error) {
5938
+ const refusal = refusalOf(error);
5939
+ if (refusal === null) throw error;
5940
+ return {
5941
+ kind: "refused",
5942
+ error: refusal
5943
+ };
5944
+ }
5945
+ const status = await client.tokenStatus.get();
5946
+ if (!status.valid) return {
5947
+ kind: "refused",
5948
+ error: new ConfigError(`Metabase reports the token as ${status.status}`)
5949
+ };
5950
+ return {
5951
+ kind: "activated",
5952
+ status
5953
+ };
5954
+ }
5955
+ async function activateAskedToken(ctx, client, terminal) {
5956
+ const token = await ctx.prompts.password({
5957
+ key: LICENSE_KEYS.token,
5958
+ message: "Paste your Metabase license token (Enter to skip)",
5959
+ validate: validateToken
5960
+ });
5961
+ if (token.length === 0) return null;
5962
+ const spinner = ctx.ui.spinner("Activating the license…");
5963
+ let activation;
5964
+ try {
5965
+ activation = await activate(client, token);
5966
+ } catch (error) {
5967
+ spinner.fail(ACTIVATION_FAILED);
5968
+ throw error;
5969
+ }
5970
+ if (activation.kind === "activated") {
5971
+ spinner.stop(activeLabel(activation.status));
5972
+ ctx.session.licenseToken.set(token);
5973
+ return activation.status;
5974
+ }
5975
+ spinner.fail(ACTIVATION_FAILED);
5976
+ if (!terminal) throw activation.error;
5977
+ ctx.ui.warn(activation.error.message);
5978
+ return activateAskedToken(ctx, client, terminal);
5979
+ }
5980
+ function reportFeatures(ctx, features) {
5981
+ const { missing } = licensedFeatureCoverage(features);
5982
+ const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
5983
+ if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
5984
+ if (missing.length > 0) {
5985
+ const gitNote = remoteSync ? "" : "; the git step will be skipped";
5986
+ ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
5987
+ }
5988
+ }
5989
+ function licenseStep(deps = hostDeps$12) {
5990
+ return {
5991
+ id: STEP_ID$7,
5992
+ title: "Activate your license",
5993
+ redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
5994
+ async detect(ctx) {
5995
+ if (ctx.state.steps[STEP_ID$7]?.status === "skipped") return "not-applicable";
5996
+ const configured = configuredRuntime(ctx.state);
5997
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
5998
+ const resolved = deps(ctx);
5999
+ if (!await resolved.instanceRunning()) return "todo";
6000
+ const client = resolved.client(configured.url, {
6001
+ kind: "api-key",
6002
+ key: ctx.secrets.apiKey
6003
+ });
6004
+ try {
6005
+ return (await client.tokenStatus.get()).valid ? "done" : "todo";
6006
+ } catch (error) {
6007
+ if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
6008
+ throw error;
6009
+ }
6010
+ },
6011
+ async run(ctx) {
6012
+ const configured = requireConfiguredRuntime(ctx.state);
6013
+ const apiKey = requireApiKey$1(ctx);
6014
+ const resolved = deps(ctx);
6015
+ const client = resolved.client(configured.url, {
6016
+ kind: "api-key",
6017
+ key: apiKey
6018
+ });
6019
+ const status = await activateAskedToken(ctx, client, resolved.terminal);
6020
+ if (status === null) {
6021
+ await recordLiveLicense(ctx, client);
6022
+ const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
6023
+ ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
6024
+ return { outcome: "skipped" };
6025
+ }
6026
+ await ctx.saveState({
6027
+ ...ctx.state,
6028
+ license: licenseOf(status)
6029
+ });
6030
+ const mb = mbTool(resolved.runner);
6031
+ if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
6032
+ url: configured.url,
6033
+ profile: ctx.state.mbProfile,
6034
+ apiKey
6035
+ });
6036
+ reportFeatures(ctx, status.features);
6037
+ return { outcome: "completed" };
6038
+ }
6039
+ };
6040
+ }
6041
+ //#endregion
6042
+ //#region src/steps/git-sync.ts
6043
+ const STEP_ID$6 = "git-sync";
6044
+ const GIT_SYNC_KEYS = {
6045
+ location: `${STEP_ID$6}.location`,
6046
+ path: `${STEP_ID$6}.path`,
6047
+ remote: `${STEP_ID$6}.remote`,
6048
+ url: `${STEP_ID$6}.url`,
6049
+ token: `${STEP_ID$6}.token`
6050
+ };
6051
+ const BARE_REPO_NAME = "content.git";
6052
+ const DEFAULT_BRANCH = "main";
6053
+ const ORIGIN = "origin";
6054
+ const FALLBACK_REMOTE_NAME = "rde-remote";
6055
+ const README_NAME = "README.md";
6056
+ const README_CONTENT = "# Metabase content\n\nMetabase content synced by rde.\n";
6057
+ const INITIAL_COMMIT_MESSAGE = "Initial commit";
6058
+ const GITIGNORE_NAME = ".gitignore";
6059
+ const IGNORED_PATHS = [
6060
+ ".env",
6061
+ ".env.local",
6062
+ ".env.*.local",
6063
+ "node_modules/",
6064
+ ".vite/",
6065
+ ".scratch/",
6066
+ ".DS_Store"
6067
+ ];
6068
+ const HTTPS_PREFIX = "https://";
6069
+ const HOME_PREFIX = "~/";
6070
+ const BAD_REQUEST_STATUS = 400;
6071
+ const TASK_POLL_INTERVAL_MS = 1e3;
6072
+ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
6073
+ "successful",
6074
+ "errored",
6075
+ "cancelled",
6076
+ "timed-out",
6077
+ "conflict"
6078
+ ]);
6079
+ const SYNC_MODE_SETTINGS = {
6080
+ "remote-sync-type": "read-write",
6081
+ "remote-sync-transforms": true
6082
+ };
6083
+ function hostDeps$11(ctx) {
6084
+ return {
6085
+ runner: processRunner(ctx.signal),
6086
+ client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
6087
+ cwd: process.cwd(),
6088
+ dataDir: instanceDataDir(ctx.state.name),
6089
+ taskPollIntervalMs: TASK_POLL_INTERVAL_MS
6090
+ };
6091
+ }
6092
+ const SKIP_GIT_SYNC = "skip";
6093
+ const FRESH = {
6094
+ remoteKind: null,
6095
+ hostedUrl: null,
6096
+ workdir: null
6097
+ };
6098
+ function expandHome(path) {
6099
+ return path.startsWith(HOME_PREFIX) ? join(homedir(), path.slice(2)) : path;
6100
+ }
6101
+ async function directoryState(path) {
6102
+ let stat;
6103
+ try {
6104
+ stat = await promises.stat(path);
6105
+ } catch (error) {
6106
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") return "missing";
6107
+ throw error;
6108
+ }
6109
+ if (!stat.isDirectory()) return "file";
6110
+ return (await promises.readdir(path)).length === 0 ? "empty" : "populated";
6111
+ }
6112
+ async function isCloneTarget(path) {
6113
+ const state = await directoryState(path);
6114
+ return state === "missing" || state === "empty";
6115
+ }
6116
+ async function initTargetProblem(run, path) {
6117
+ switch (await directoryState(path)) {
6118
+ case "missing":
6119
+ await promises.mkdir(path, { recursive: true });
6120
+ return null;
6121
+ case "file": return `${path} is not a directory`;
6122
+ case "empty": return null;
6123
+ case "populated": return await run.git.isRepo(path) ? null : `${path} is not empty and not a git repository; pick an empty directory or a repository`;
6124
+ }
6125
+ }
6126
+ async function isCloneOf(run, path, url) {
6127
+ return await run.git.isRepo(path) && await run.git.remoteUrl(path, ORIGIN) === url;
6128
+ }
6129
+ async function cloneTargetProblem(run, path, url) {
6130
+ switch (await directoryState(path)) {
6131
+ case "missing":
6132
+ case "empty": return null;
6133
+ case "file": return `${path} is not a directory`;
6134
+ case "populated": return await isCloneOf(run, path, url) ? null : `${path} is not empty; pick an empty directory, or use an existing repository`;
6135
+ }
6136
+ }
6137
+ async function existingRepoProblem(run, path) {
5676
6138
  return await run.git.isRepo(path) ? null : `${path} is not a git repository`;
5677
6139
  }
5678
6140
  const FRESH_WORKDIRS = {
@@ -6055,6 +6517,9 @@ function configureRemote(run, runtime, model) {
6055
6517
  function isMbUnavailable(error) {
6056
6518
  return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
6057
6519
  }
6520
+ function remoteSyncGap(license) {
6521
+ return license.activated ? "Remote sync needs a license with the remote-sync feature, and this instance's license lacks it" : "Remote sync needs a license, and this instance has no valid one";
6522
+ }
6058
6523
  async function runGitSync(ctx, deps) {
6059
6524
  const configured = requireConfiguredRuntime(ctx.state);
6060
6525
  const instance = mbInstanceOf(ctx.state, ctx.secrets);
@@ -6069,6 +6534,8 @@ async function runGitSync(ctx, deps) {
6069
6534
  }),
6070
6535
  instance
6071
6536
  };
6537
+ const license = await recordLiveLicense(ctx, run.client);
6538
+ if (!hasRemoteSync(license)) throw new ConfigError(`${remoteSyncGap(license)}; \`rde init --from license\` activates one`);
6072
6539
  const flow = await runFlow({
6073
6540
  remote: remoteScreen(run),
6074
6541
  hosted: hostedScreen(run),
@@ -6140,11 +6607,9 @@ function githubContents(signal, options = {}) {
6140
6607
  //#region src/tools/skills-cli.ts
6141
6608
  const NPX = "npx";
6142
6609
  const SKILLS_PACKAGE = "skills@latest";
6143
- const DEFAULT_RDE_SKILL_SOURCE = "https://github.com/metabase/agent-skills/tree/main";
6144
- const RDE_SOURCE_SKILL_NAMES = ["rde", "rde-data-apps"];
6610
+ const RDE_DATA_APPS_SKILL_NAME = "rde-data-apps";
6611
+ const RDE_SOURCE_SKILL_NAMES = ["rde", RDE_DATA_APPS_SKILL_NAME];
6145
6612
  const DATA_APP_REPOSITORY = "metabase/metabase";
6146
- const DATA_APP_BRANCH = "master";
6147
- const DATA_APP_SKILLS_SOURCE = `${DATA_APP_REPOSITORY}/skills#${DATA_APP_BRANCH}`;
6148
6613
  const DATA_APP_SKILL_NAMES = [
6149
6614
  "metabase-data-app-setup",
6150
6615
  "metabase-data-app-routing",
@@ -6153,8 +6618,7 @@ const DATA_APP_SKILL_NAMES = [
6153
6618
  "metabase-data-app-migrate"
6154
6619
  ];
6155
6620
  function rdeSkillSource() {
6156
- const override = readEnv(ENV_RDE_SKILL_SOURCE);
6157
- return override === void 0 || override.length === 0 ? DEFAULT_RDE_SKILL_SOURCE : override;
6621
+ return readSetEnv("RDE_SKILL_SOURCE") ?? "https://github.com/metabase/agent-skills/tree/main";
6158
6622
  }
6159
6623
  const InstalledSkill = z.object({
6160
6624
  name: z.string(),
@@ -6180,8 +6644,7 @@ function projectSkillLockPath(project) {
6180
6644
  return join(project, PROJECT_LOCK_FILE);
6181
6645
  }
6182
6646
  function xdgStateHome() {
6183
- const value = readEnv(ENV_XDG_STATE_HOME);
6184
- return value === void 0 || value.length === 0 ? null : value;
6647
+ return readSetEnv(ENV_XDG_STATE_HOME);
6185
6648
  }
6186
6649
  function lockedSkill(path, raw, skill) {
6187
6650
  if (raw === null) return null;
@@ -6193,11 +6656,16 @@ const hostSkillFiles = {
6193
6656
  readJson: readJsonFile,
6194
6657
  readText: readTextFile
6195
6658
  };
6196
- const DATA_APP_SKILLS_ORIGIN = {
6197
- kind: "github",
6198
- repository: DATA_APP_REPOSITORY,
6199
- ref: DATA_APP_BRANCH
6200
- };
6659
+ function dataAppSkillsSource(ref) {
6660
+ return `${DATA_APP_REPOSITORY}/skills#${ref}`;
6661
+ }
6662
+ function dataAppSkillsOrigin(ref) {
6663
+ return {
6664
+ kind: "github",
6665
+ repository: DATA_APP_REPOSITORY,
6666
+ ref
6667
+ };
6668
+ }
6201
6669
  const GITHUB_URL = /^https:\/\/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?(?:\/tree\/([^/]+)(?:\/.*)?)?\/?$/;
6202
6670
  function skillOrigin(source, cwd) {
6203
6671
  const github = GITHUB_URL.exec(source);
@@ -6283,6 +6751,7 @@ const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$5}.agents` };
6283
6751
  const MECHANICS_NOTE = "The rde skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
6284
6752
  const REQUIRED_SKILL_NAMES = [...DATA_APP_SKILL_NAMES, ...RDE_SOURCE_SKILL_NAMES];
6285
6753
  const DETECT_AGENTS = { kind: "detected" };
6754
+ const ASK_AGENTS = { kind: "ask" };
6286
6755
  function hostDeps$10(ctx) {
6287
6756
  return {
6288
6757
  runner: processRunner(ctx.signal),
@@ -6307,9 +6776,6 @@ function skillCoversAgent(entry, agent) {
6307
6776
  function coversAny(entry, agents) {
6308
6777
  return agents.some((agent) => skillCoversAgent(entry, agent));
6309
6778
  }
6310
- async function rdeSkillEntries(runner) {
6311
- return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
6312
- }
6313
6779
  function requiredEntries(installed) {
6314
6780
  return installed.filter((entry) => REQUIRED_SKILL_NAMES.includes(entry.name));
6315
6781
  }
@@ -6326,12 +6792,26 @@ function coveredAgentNames(entry, agents) {
6326
6792
  function installedLines(entries, agents) {
6327
6793
  return entries.map((entry) => `${entry.name} (${coveredAgentNames(entry, agents).join(", ")}): ${entry.path}`).join("\n");
6328
6794
  }
6329
- function skillSets(deps) {
6795
+ const DEVELOPMENT_BRANCH = "master";
6796
+ function releaseBranch(major) {
6797
+ return `release-x.${major}.x`;
6798
+ }
6799
+ function dataAppSkillsRef(metabaseVersion, development) {
6800
+ if (metabaseVersion === null) return releaseBranch(65);
6801
+ if (development && isMasterBuild(metabaseVersion)) return DEVELOPMENT_BRANCH;
6802
+ const major = releaseMajor(metabaseVersion) ?? 65;
6803
+ return releaseBranch(Math.max(major, 65));
6804
+ }
6805
+ function wantedDataAppRef(metabaseVersion) {
6806
+ return dataAppSkillsRef(metabaseVersion, developmentMode());
6807
+ }
6808
+ function skillSets(deps, metabaseVersion) {
6809
+ const dataAppRef = wantedDataAppRef(metabaseVersion);
6330
6810
  return [{
6331
6811
  label: "data-app",
6332
- source: DATA_APP_SKILLS_SOURCE,
6333
- sourceNote: "",
6334
- origin: DATA_APP_SKILLS_ORIGIN,
6812
+ source: dataAppSkillsSource(dataAppRef),
6813
+ sourceNote: ` from ${dataAppRef}`,
6814
+ origin: dataAppSkillsOrigin(dataAppRef),
6335
6815
  skills: DATA_APP_SKILL_NAMES
6336
6816
  }, {
6337
6817
  label: "rde",
@@ -6486,7 +6966,7 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
6486
6966
  if (detected.length === 0) return ctx.state.steps[STEP_ID$5]?.status === "skipped" ? "not-applicable" : "todo";
6487
6967
  if (missingSkills(await skillsCli(resolved.runner).list(), detected).length > 0) return "todo";
6488
6968
  const check = await setCheck(resolved);
6489
- for (const set of skillSets(resolved)) if (await setBehind(set, check)) return "outdated";
6969
+ for (const set of skillSets(resolved, ctx.state.version)) if (await setBehind(set, check)) return "outdated";
6490
6970
  return "done";
6491
6971
  },
6492
6972
  async run(ctx) {
@@ -6503,7 +6983,7 @@ function agentsSkillsStep(options, updates, deps = hostDeps$10) {
6503
6983
  const before = await installedNow();
6504
6984
  const check = await setCheck(resolved);
6505
6985
  const pending = [];
6506
- for (const set of skillSets(resolved)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
6986
+ for (const set of skillSets(resolved, ctx.state.version)) if (!coversAll(before, set.skills, selection.agents) || await setBehind(set, check)) pending.push(set);
6507
6987
  const names = displayNames(selection.agents);
6508
6988
  if (pending.length === 0) {
6509
6989
  ctx.ui.success(`The rde and data-app skills are current for ${names}`);
@@ -6542,7 +7022,7 @@ function hostDatabasesDeps(signal) {
6542
7022
  now: () => /* @__PURE__ */ new Date()
6543
7023
  };
6544
7024
  }
6545
- function requireApiKey$1(state, apiKey) {
7025
+ function requireApiKey(state, apiKey) {
6546
7026
  if (apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
6547
7027
  return apiKey;
6548
7028
  }
@@ -6564,7 +7044,7 @@ function authenticatedClient(state, apiKey, deps) {
6564
7044
  const configured = requireConfiguredRuntime(state);
6565
7045
  return deps.client(configured.url, {
6566
7046
  kind: "api-key",
6567
- key: requireApiKey$1(state, apiKey)
7047
+ key: requireApiKey(state, apiKey)
6568
7048
  });
6569
7049
  }
6570
7050
  async function listDatabases(state, apiKey, deps) {
@@ -6772,6 +7252,33 @@ function updateTo(release, installed) {
6772
7252
  function displayCommand(install) {
6773
7253
  return [install.command, ...install.args].join(" ");
6774
7254
  }
7255
+ const NPM_EACCES_EXIT = 243;
7256
+ const NPM_PERMISSION_TEXT = [
7257
+ "EACCES",
7258
+ "EPERM",
7259
+ "might be a permissions issue"
7260
+ ];
7261
+ function deniedGlobalWrite(install, error) {
7262
+ if (install.command !== "npm" || !(error instanceof ExternalToolError)) return false;
7263
+ return NPM_PERMISSION_TEXT.some((text) => error.stderrTail.includes(text)) || error.exitCode === NPM_EACCES_EXIT;
7264
+ }
7265
+ function globalWriteDenied(install) {
7266
+ return new ConfigError([
7267
+ `\`${displayCommand(install)}\` failed because npm cannot write its global directory.`,
7268
+ "Give npm a prefix you own with `npm config set prefix ~/.npm-global`,",
7269
+ "add ~/.npm-global/bin to PATH and open a new terminal,",
7270
+ "or install Node with a version manager such as nvm, fnm or mise;",
7271
+ "then run rde init again."
7272
+ ].join(" "));
7273
+ }
7274
+ async function installMb(install, ctx, deps) {
7275
+ try {
7276
+ await runShown(deps.runner, ctx.ui.childOutput, install.command, install.args);
7277
+ } catch (error) {
7278
+ if (deniedGlobalWrite(install, error)) throw globalWriteDenied(install);
7279
+ throw error;
7280
+ }
7281
+ }
6775
7282
  async function versionAt(runner, path) {
6776
7283
  try {
6777
7284
  const raw = await mbTool(runner).version(path);
@@ -6897,203 +7404,38 @@ function installMbStep(updates, deps = hostDeps$8) {
6897
7404
  title: "Install the Metabase CLI",
6898
7405
  async detect(ctx) {
6899
7406
  const resolved = deps(ctx);
6900
- const found = await inspectMb(resolved.runner);
6901
- if (!found.installed || !found.satisfies) return "todo";
6902
- return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
6903
- },
6904
- async run(ctx) {
6905
- const resolved = deps(ctx);
6906
- const before = await inspectMb(resolved.runner);
6907
- const target = await release(resolved);
6908
- if (before.installed && before.satisfies) {
6909
- if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
6910
- const update = await updateFor(target, before.version, resolved);
6911
- if (update.kind === "shadowed") {
6912
- const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
6913
- ctx.ui.warn(await shadowedWarning(lead, resolved));
6914
- }
6915
- if (update.kind !== "update") {
6916
- ctx.ui.success(`mb ${before.version} is installed`);
6917
- return { outcome: "completed" };
6918
- }
6919
- ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
6920
- } else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
6921
- const install = mbInstallCommand(resolved.installedWith);
6922
- ctx.ui.info(`Running ${displayCommand(install)}`);
6923
- await runShown(resolved.runner, ctx.ui.childOutput, install.command, install.args);
6924
- const after = await inspectMb(resolved.runner);
6925
- if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
6926
- const stillNewer = updateTo(target, after.version);
6927
- if (stillNewer !== null) {
6928
- const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
6929
- ctx.ui.warn(await shadowedWarning(lead, resolved));
6930
- }
6931
- ctx.ui.success(`mb ${after.version} is installed`);
6932
- return { outcome: "completed" };
6933
- }
6934
- };
6935
- }
6936
- //#endregion
6937
- //#region src/steps/license.ts
6938
- const STEP_ID$3 = "license";
6939
- const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
6940
- const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
6941
- const NOT_FOUND_STATUS$1 = 404;
6942
- const TOKEN_REFUSED_STATUS = 400;
6943
- const STORE_UNREACHABLE_STATUS = 503;
6944
- const TOKEN_CHECK_HOST = "token-check.metabase.com";
6945
- const ACTIVATION_FAILED = "License activation failed";
6946
- const LICENSED_FEATURES = {
6947
- [REMOTE_SYNC_FEATURE]: "remote sync",
6948
- "data-apps": "data apps",
6949
- "transforms-testing": "transform tests"
6950
- };
6951
- const LICENSED_FEATURE_NAMES = Object.values(LICENSED_FEATURES);
6952
- function licensedFeatureCoverage(features) {
6953
- const entries = Object.entries(LICENSED_FEATURES);
6954
- return {
6955
- included: entries.filter(([key]) => features.includes(key)).map(([, name]) => name),
6956
- missing: entries.filter(([key]) => !features.includes(key)).map(([, name]) => name)
6957
- };
6958
- }
6959
- function listOf(names, type) {
6960
- return new Intl.ListFormat("en", { type }).format(names);
6961
- }
6962
- function hostDeps$7(ctx) {
6963
- return {
6964
- client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
6965
- runner: processRunner(ctx.signal),
6966
- instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal)),
6967
- terminal: stdinIsTty()
6968
- };
6969
- }
6970
- function isNoTokenYet(error) {
6971
- if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
6972
- return isMetabaseNotServing(error);
6973
- }
6974
- function validateToken(value) {
6975
- if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
6976
- return "A license token is 64 hexadecimal characters";
6977
- }
6978
- function requireApiKey(ctx) {
6979
- if (ctx.secrets.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
6980
- return ctx.secrets.apiKey;
6981
- }
6982
- function activeLabel(status) {
6983
- const plan = status["plan-alias"];
6984
- return plan === void 0 ? "License active" : `License active (plan ${plan})`;
6985
- }
6986
- function refusalOf(error) {
6987
- if (!(error instanceof MetabaseApiError)) return null;
6988
- if (error.status === TOKEN_REFUSED_STATUS) return new ConfigError(`The license token was refused: ${error.reason}`);
6989
- 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`);
6990
- return null;
6991
- }
6992
- async function activate(client, token) {
6993
- try {
6994
- await client.setting.setPremiumEmbeddingToken(token);
6995
- } catch (error) {
6996
- const refusal = refusalOf(error);
6997
- if (refusal === null) throw error;
6998
- return {
6999
- kind: "refused",
7000
- error: refusal
7001
- };
7002
- }
7003
- const status = await client.tokenStatus.get();
7004
- if (!status.valid) return {
7005
- kind: "refused",
7006
- error: new ConfigError(`Metabase reports the token as ${status.status}`)
7007
- };
7008
- return {
7009
- kind: "activated",
7010
- status
7011
- };
7012
- }
7013
- async function activateAskedToken(ctx, client, terminal) {
7014
- const token = await ctx.prompts.password({
7015
- key: LICENSE_KEYS.token,
7016
- message: "Paste your Metabase license token (Enter to skip)",
7017
- validate: validateToken
7018
- });
7019
- if (token.length === 0) return null;
7020
- const spinner = ctx.ui.spinner("Activating the license…");
7021
- let activation;
7022
- try {
7023
- activation = await activate(client, token);
7024
- } catch (error) {
7025
- spinner.fail(ACTIVATION_FAILED);
7026
- throw error;
7027
- }
7028
- if (activation.kind === "activated") {
7029
- spinner.stop(activeLabel(activation.status));
7030
- ctx.session.licenseToken.set(token);
7031
- return activation.status;
7032
- }
7033
- spinner.fail(ACTIVATION_FAILED);
7034
- if (!terminal) throw activation.error;
7035
- ctx.ui.warn(activation.error.message);
7036
- return activateAskedToken(ctx, client, terminal);
7037
- }
7038
- function reportFeatures(ctx, features) {
7039
- const { missing } = licensedFeatureCoverage(features);
7040
- const remoteSync = features.includes(REMOTE_SYNC_FEATURE);
7041
- if (remoteSync) ctx.ui.info("Remote sync is included; the git step will set it up");
7042
- if (missing.length > 0) {
7043
- const gitNote = remoteSync ? "" : "; the git step will be skipped";
7044
- ctx.ui.warn(`This license does not include ${listOf(missing, "disjunction")}${gitNote}`);
7045
- }
7046
- }
7047
- function licenseStep(deps = hostDeps$7) {
7048
- return {
7049
- id: STEP_ID$3,
7050
- title: "Activate your license",
7051
- redoHint: "`rde init --from license` with it activates the license and runs the steps after it again.",
7052
- async detect(ctx) {
7053
- if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
7054
- const configured = configuredRuntime(ctx.state);
7055
- if (configured === null || ctx.secrets.apiKey === null) return "todo";
7056
- const resolved = deps(ctx);
7057
- if (!await resolved.instanceRunning()) return "todo";
7058
- const client = resolved.client(configured.url, {
7059
- kind: "api-key",
7060
- key: ctx.secrets.apiKey
7061
- });
7062
- try {
7063
- return (await client.tokenStatus.get()).valid ? "done" : "todo";
7064
- } catch (error) {
7065
- if (isNoTokenYet(error) || isRejectedCredential(error)) return "todo";
7066
- throw error;
7067
- }
7407
+ const found = await inspectMb(resolved.runner);
7408
+ if (!found.installed || !found.satisfies) return "todo";
7409
+ return (await updateFor(await release(resolved), found.version, resolved)).kind === "update" ? "outdated" : "done";
7068
7410
  },
7069
7411
  async run(ctx) {
7070
- const configured = requireConfiguredRuntime(ctx.state);
7071
- const apiKey = requireApiKey(ctx);
7072
7412
  const resolved = deps(ctx);
7073
- const status = await activateAskedToken(ctx, resolved.client(configured.url, {
7074
- kind: "api-key",
7075
- key: apiKey
7076
- }), resolved.terminal);
7077
- if (status === null) {
7078
- const lost = listOf(LICENSED_FEATURE_NAMES, "conjunction");
7079
- ctx.ui.info(`Skipping the license; ${lost} need one. Run \`rde init --from license\` to add it later`);
7080
- return { outcome: "skipped" };
7081
- }
7082
- await ctx.saveState({
7083
- ...ctx.state,
7084
- license: {
7085
- activated: true,
7086
- features: status.features,
7087
- validThru: status["valid-thru"] ?? null
7413
+ const before = await inspectMb(resolved.runner);
7414
+ const target = await release(resolved);
7415
+ if (before.installed && before.satisfies) {
7416
+ if (target.kind === "unknown") ctx.ui.warn(`Could not check for a newer mb: ${target.reason}`);
7417
+ const update = await updateFor(target, before.version, resolved);
7418
+ if (update.kind === "shadowed") {
7419
+ const lead = `mb ${before.version} runs first; ${update.version} is installed later on PATH.`;
7420
+ ctx.ui.warn(await shadowedWarning(lead, resolved));
7088
7421
  }
7089
- });
7090
- const mb = mbTool(resolved.runner);
7091
- if (isApiKeyLoginAt(await mb.authStatus(ctx.state.mbProfile), configured.url)) await mb.authLogin({
7092
- url: configured.url,
7093
- profile: ctx.state.mbProfile,
7094
- apiKey
7095
- });
7096
- reportFeatures(ctx, status.features);
7422
+ if (update.kind !== "update") {
7423
+ ctx.ui.success(`mb ${before.version} is installed`);
7424
+ return { outcome: "completed" };
7425
+ }
7426
+ ctx.ui.info(`mb ${before.version} is installed; updating to ${update.version}`);
7427
+ } else if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
7428
+ const install = mbInstallCommand(resolved.installedWith);
7429
+ ctx.ui.info(`Running ${displayCommand(install)}`);
7430
+ await installMb(install, ctx, resolved);
7431
+ const after = await inspectMb(resolved.runner);
7432
+ if (!after.installed || !after.satisfies) throw await stillWrong(install, after, resolved);
7433
+ const stillNewer = updateTo(target, after.version);
7434
+ if (stillNewer !== null) {
7435
+ const lead = `mb ${after.version} still runs first, not the ${stillNewer} just installed.`;
7436
+ ctx.ui.warn(await shadowedWarning(lead, resolved));
7437
+ }
7438
+ ctx.ui.success(`mb ${after.version} is installed`);
7097
7439
  return { outcome: "completed" };
7098
7440
  }
7099
7441
  };
@@ -7122,7 +7464,7 @@ const PLATFORMS = {
7122
7464
  javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
7123
7465
  }
7124
7466
  };
7125
- function hostDeps$6(ctx) {
7467
+ function hostDeps$7(ctx) {
7126
7468
  const runner = processRunner(ctx.signal);
7127
7469
  return {
7128
7470
  platform: process.platform,
@@ -7244,7 +7586,7 @@ function checkLine(check) {
7244
7586
  function checkLines(checks) {
7245
7587
  return checks.map(checkLine);
7246
7588
  }
7247
- function preflightStep(deps = hostDeps$6) {
7589
+ function preflightStep(deps = hostDeps$7) {
7248
7590
  return {
7249
7591
  id: "preflight",
7250
7592
  title: "Check this machine",
@@ -7263,7 +7605,7 @@ function preflightStep(deps = hostDeps$6) {
7263
7605
  }
7264
7606
  //#endregion
7265
7607
  //#region src/steps/runtime.ts
7266
- const STEP_ID$2 = "runtime";
7608
+ const STEP_ID$3 = "runtime";
7267
7609
  const MIN_PORT = 1;
7268
7610
  const MAX_PORT = 65535;
7269
7611
  const PORT_PATTERN = /^\d+$/;
@@ -7277,7 +7619,7 @@ const NO_RUNTIME_FLAGS = {
7277
7619
  jar: null,
7278
7620
  port: null
7279
7621
  };
7280
- function hostDeps$5(ctx) {
7622
+ function hostDeps$6(ctx) {
7281
7623
  return {
7282
7624
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
7283
7625
  releases: releaseLookup(ctx.signal)
@@ -7329,22 +7671,56 @@ async function chooseKind(ctx, options, runtime) {
7329
7671
  ctx.ui.info(`Docker is not available (${available.docker.reason}); using the jar runtime with ${javaLabel(available.java)}`);
7330
7672
  return "jar";
7331
7673
  }
7332
- function chooseImage(ctx, options) {
7674
+ const NAME_ONE = "`--image <image>` or `--jar <release tag>` names a Metabase to run";
7675
+ const INIT_WAYS_OUT = {
7676
+ named: NAME_ONE,
7677
+ unreleased: `${NAME_ONE}, and \`RDE_DEV=1\` runs the development build`
7678
+ };
7679
+ function isFeedFailure(error) {
7680
+ return error instanceof MetabaseApiError || error instanceof MetabaseUnreachableError;
7681
+ }
7682
+ async function requireNewestRelease(releases, waysOut) {
7683
+ let newest;
7684
+ try {
7685
+ newest = await releases.newestSupportedRelease();
7686
+ } catch (error) {
7687
+ if (!isFeedFailure(error)) throw error;
7688
+ throw new ConfigError(`the Metabase release feed could not be read (${error.reason}); ${waysOut.named}`);
7689
+ }
7690
+ if (newest === null) throw new ConfigError(`no release of Metabase Enterprise 65 or newer is out yet; ${waysOut.unreleased}`);
7691
+ return newest;
7692
+ }
7693
+ async function releaseImage(ctx, releases, waysOut) {
7694
+ const newest = await requireNewestRelease(releases, waysOut);
7695
+ const image = majorLineImage(newest.major);
7696
+ ctx.ui.info(`The newest Metabase release is ${newest.version}; ${image} carries the newest build of ${newest.major}`);
7697
+ return image;
7698
+ }
7699
+ async function latestJarRelease(ctx, releases, waysOut) {
7700
+ if (developmentMode()) {
7701
+ const version = await releases.latestVersion();
7702
+ ctx.ui.info(`The latest Metabase release is ${version}`);
7703
+ return version;
7704
+ }
7705
+ const newest = await requireNewestRelease(releases, waysOut);
7706
+ ctx.ui.info(`The newest Metabase release is ${newest.version}`);
7707
+ return newest.version;
7708
+ }
7709
+ async function chooseImage(ctx, options, releases) {
7333
7710
  if (options.image !== null) {
7334
7711
  ctx.ui.info(`Using image ${options.image} from --image`);
7335
7712
  return options.image;
7336
7713
  }
7337
- return DEFAULT_IMAGE;
7714
+ if (developmentMode()) return DEVELOPMENT_IMAGE;
7715
+ return releaseImage(ctx, releases, INIT_WAYS_OUT);
7338
7716
  }
7339
7717
  async function resolveJar(ctx, spec, releases) {
7340
7718
  const parsed = parseJarSpec(spec);
7341
- if (parsed === null) throw new ConfigError(`${spec} is not latest, a release tag such as v1.63.18, or the path of a .jar file`);
7719
+ if (parsed === null) throw new ConfigError(`${spec} is not latest, an enterprise release tag such as v1.65.0, or the path of a .jar file`);
7342
7720
  if (parsed.kind !== "latest") return parsed;
7343
- const version = await releases.latestVersion();
7344
- ctx.ui.info(`The latest Metabase release is ${version}`);
7345
7721
  return {
7346
7722
  kind: "release",
7347
- version
7723
+ version: await latestJarRelease(ctx, releases, INIT_WAYS_OUT)
7348
7724
  };
7349
7725
  }
7350
7726
  async function chooseJar(ctx, options, releases) {
@@ -7369,7 +7745,7 @@ async function chooseRuntime(ctx, options, deps) {
7369
7745
  const kind = await chooseKind(ctx, options, deps.runtime);
7370
7746
  switch (kind) {
7371
7747
  case "docker": {
7372
- const image = chooseImage(ctx, options);
7748
+ const image = await chooseImage(ctx, options, deps.releases);
7373
7749
  const port = await choosePort(ctx, options, deps.runtime);
7374
7750
  return {
7375
7751
  kind,
@@ -7406,9 +7782,9 @@ async function persistedRuntime(ctx, options, deps) {
7406
7782
  });
7407
7783
  return chosen;
7408
7784
  }
7409
- function runtimeStep(options, deps = hostDeps$5) {
7785
+ function runtimeStep(options, deps = hostDeps$6) {
7410
7786
  return {
7411
- id: STEP_ID$2,
7787
+ id: STEP_ID$3,
7412
7788
  title: "Install Metabase",
7413
7789
  redoHint: "Move it with `rde start --port`, or change its Metabase with `rde upgrade --to`.",
7414
7790
  async detect(ctx) {
@@ -7437,7 +7813,7 @@ const DEFAULT_HEALTH_TIMING = { timeoutMs: 3e5 };
7437
7813
  //#region src/steps/start-metabase.ts
7438
7814
  const LOG_TAIL_ON_TIMEOUT = 30;
7439
7815
  const PERCENT = 100;
7440
- function hostDeps$4(ctx) {
7816
+ function hostDeps$5(ctx) {
7441
7817
  return {
7442
7818
  runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
7443
7819
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
@@ -7450,6 +7826,16 @@ function isNotHealthy(error) {
7450
7826
  async function runningVersion(client) {
7451
7827
  return (await client.session.properties()).version.tag;
7452
7828
  }
7829
+ const PICK_ANOTHER = "`rde upgrade --to <image or release tag>` moves this instance to one, or `rde uninstall`, then `rde init --image <image>` or `rde init --jar <release tag>`, sets up a new instance on one";
7830
+ function unsupportedVersionReason(version, development) {
7831
+ if (development) return null;
7832
+ const needs = `rde needs Metabase Enterprise 65 or newer: ${PICK_ANOTHER}`;
7833
+ if (isOpenSourceRelease(version)) return `Metabase ${version} is the open-source edition; ${needs}`;
7834
+ if (isMasterBuild(version)) return `Metabase ${version} is a development build of master, which only development mode (RDE_DEV=1) runs; ${needs}`;
7835
+ const major = releaseMajor(version);
7836
+ if (major !== null && major < 65) return `Metabase ${version} is older than 65; ${needs}`;
7837
+ return null;
7838
+ }
7453
7839
  function progressLabel(progress) {
7454
7840
  return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
7455
7841
  }
@@ -7466,7 +7852,7 @@ async function startRuntime(ctx, runtime, deps) {
7466
7852
  throw error;
7467
7853
  }
7468
7854
  }
7469
- function startMetabaseStep(deps = hostDeps$4) {
7855
+ function startMetabaseStep(deps = hostDeps$5, accepted = "supported") {
7470
7856
  return {
7471
7857
  id: "start-metabase",
7472
7858
  title: "Start Metabase",
@@ -7518,12 +7904,272 @@ function startMetabaseStep(deps = hostDeps$4) {
7518
7904
  ...ctx.state,
7519
7905
  version
7520
7906
  });
7907
+ const refusal = accepted === "supported" ? unsupportedVersionReason(version, developmentMode()) : null;
7908
+ if (refusal !== null) {
7909
+ spinner.fail(`Metabase ${version} is running at ${configured.url}, but rde does not run it`);
7910
+ throw new UnsupportedMetabaseError(refusal);
7911
+ }
7521
7912
  spinner.stop(`Metabase ${version} is running at ${configured.url}`);
7522
7913
  return { outcome: "completed" };
7523
7914
  }
7524
7915
  };
7525
7916
  }
7526
7917
  //#endregion
7918
+ //#region src/steps/sync-scope.ts
7919
+ const STEP_ID$2 = "sync-scope";
7920
+ const SYNC_SCOPE_KEYS = { export: `${STEP_ID$2}.export` };
7921
+ const AGENT_COLLECTION_NAME = "Analytics";
7922
+ const LIBRARY_NAME = "Library";
7923
+ const ROOT_COLLECTION_NAME = "Our analytics";
7924
+ const AGENT_COLLECTION_DESCRIPTION = "Dashboards, questions, models, and documents that reach git on export.";
7925
+ function hostDeps$4(ctx) {
7926
+ return {
7927
+ client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
7928
+ runner: processRunner(ctx.signal),
7929
+ instanceRunning: () => isInstanceRunning(ctx.state, hostRuntimeDeps(ctx.state.name, ctx.signal))
7930
+ };
7931
+ }
7932
+ function isCollection(entry) {
7933
+ return typeof entry.id === "number";
7934
+ }
7935
+ function isOrdinary(collection) {
7936
+ return collection.type === null && collection.namespace === null && collection.personal_owner_id === null;
7937
+ }
7938
+ function readScope(list, syncRoot) {
7939
+ const topLevel = list.filter(isCollection).filter((collection) => collection.location === "/");
7940
+ const ordinary = topLevel.filter(isOrdinary);
7941
+ const unsynced = ordinary.filter((collection) => !collection.is_remote_synced);
7942
+ return {
7943
+ library: topLevel.find((collection) => collection.type === "library") ?? null,
7944
+ syncedRoots: ordinary.filter((collection) => collection.is_remote_synced),
7945
+ unsyncedRecordedRoot: unsynced.find((collection) => collection.id === syncRoot) ?? null,
7946
+ foreignNamesake: unsynced.some((collection) => collection.id !== syncRoot && collection.name === "Analytics")
7947
+ };
7948
+ }
7949
+ function isComplete(scope, libraryLicensed) {
7950
+ return (!libraryLicensed || scope.library !== null && scope.library.is_remote_synced) && scope.syncedRoots.length > 0;
7951
+ }
7952
+ function isNotAnsweringYet(error) {
7953
+ return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || isRejectedCredential(error);
7954
+ }
7955
+ function requireMbInstance(ctx) {
7956
+ const instance = mbInstanceOf(ctx.state, ctx.secrets);
7957
+ if (instance === null) throw new ConfigError("no API key yet; the api-key step comes first");
7958
+ return instance;
7959
+ }
7960
+ function requireGitRemote(ctx) {
7961
+ if (ctx.state.gitRemote === null) throw new ConfigError("no repository yet; the git-sync step comes first");
7962
+ return ctx.state.gitRemote;
7963
+ }
7964
+ async function requireSettledImport(ctx, client, gitRemote) {
7965
+ const task = await client.remoteSync.currentTask();
7966
+ if (task === null) return;
7967
+ if (task.status === "running") throw new ConfigError(`a remote sync ${task.sync_task_type} is running; run \`rde init --only ${STEP_ID$2}\` once it finishes`);
7968
+ if (task.sync_task_type === "import" && task.status !== "successful") {
7969
+ const importCommand = `${mbCommand(ctx.state.mbProfile)} git-sync import --branch ${shellWord(gitRemote.branch)}`;
7970
+ throw new ConfigError(`the last import from the repository ended ${task.status}; run \`${importCommand}\`, then \`rde init --only ${STEP_ID$2}\``);
7971
+ }
7972
+ }
7973
+ async function isReadOnly(client) {
7974
+ return await client.setting.remoteSyncType() === "read-only";
7975
+ }
7976
+ async function requireReadWrite(client) {
7977
+ 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}\``);
7978
+ }
7979
+ async function ensureLibrary(client, scope, libraryLicensed) {
7980
+ if (!libraryLicensed) return null;
7981
+ return scope.library ?? client.collection.createLibrary();
7982
+ }
7983
+ async function ensureRoots(ctx, client, scope) {
7984
+ if (scope.syncedRoots.length > 0) return {
7985
+ roots: scope.syncedRoots,
7986
+ created: false,
7987
+ besideNamesake: false
7988
+ };
7989
+ if (scope.unsyncedRecordedRoot !== null) return {
7990
+ roots: [scope.unsyncedRecordedRoot],
7991
+ created: false,
7992
+ besideNamesake: false
7993
+ };
7994
+ const created = await client.collection.create({
7995
+ name: AGENT_COLLECTION_NAME,
7996
+ description: AGENT_COLLECTION_DESCRIPTION
7997
+ });
7998
+ await ctx.saveState({
7999
+ ...ctx.state,
8000
+ syncRoot: created.id
8001
+ });
8002
+ return {
8003
+ roots: [created],
8004
+ created: true,
8005
+ besideNamesake: scope.foreignNamesake
8006
+ };
8007
+ }
8008
+ function itemLabel(item) {
8009
+ return item.name === null ? `${item.model} ${item.id}` : `${item.model} ${item.id} "${item.name}"`;
8010
+ }
8011
+ function locationLabel(location) {
8012
+ if (location === void 0) return "in a collection Metabase could not find";
8013
+ return `in ${location === null ? ROOT_COLLECTION_NAME : location.name}`;
8014
+ }
8015
+ function dependencyLine(dependency) {
8016
+ const line = ` ${itemLabel(dependency)} ${locationLabel(dependency.collection)}`;
8017
+ if (dependency.used_by.length === 0) return line;
8018
+ return `${line}, used by ${dependency.used_by.map(itemLabel).join(", ")}`;
8019
+ }
8020
+ function remedyLine(required) {
8021
+ const remedy = required.remedy;
8022
+ switch (remedy.type) {
8023
+ case "collection":
8024
+ if (required.syncable) return ` Fix: sync ${remedy.collection.name} too, or move that content into a synced collection.`;
8025
+ return ` Fix: move that content into a synced collection; ${remedy.collection.name} cannot sync.`;
8026
+ case "library": return " Fix: stop using that content; it syncs only through a Library, and this instance has none.";
8027
+ case "none": return " Fix: move that content into a synced collection, or stop using it.";
8028
+ }
8029
+ }
8030
+ function requiredSyncLines(required) {
8031
+ return [
8032
+ `Metabase will not sync ${required.blocks.map((collection) => collection.name).join(", ")}, because content there uses:`,
8033
+ ...required.dependencies.map(dependencyLine),
8034
+ remedyLine(required)
8035
+ ];
8036
+ }
8037
+ function explainRefusal(error) {
8038
+ if (!(error instanceof MetabaseApiError) || error.code !== "unsynced-dependencies") return error;
8039
+ const refusal = UnsyncedDependencies.safeParse(error.detail);
8040
+ if (!refusal.success) return error;
8041
+ return new ConfigError([...refusal.data.errors.required.flatMap(requiredSyncLines), `After the fix, run \`rde init --only ${STEP_ID$2}\`.`].join("\n"));
8042
+ }
8043
+ function plannedNames(scope, libraryLicensed) {
8044
+ const library = libraryLicensed ? [scope.library?.name ?? LIBRARY_NAME] : [];
8045
+ if (scope.syncedRoots.length > 0) return [...library, ...scope.syncedRoots.map((collection) => collection.name)];
8046
+ return [...library, scope.unsyncedRecordedRoot?.name ?? "Analytics"];
8047
+ }
8048
+ function nameList(names) {
8049
+ if (names.length <= 2) return names.join(" and ");
8050
+ return `${names.slice(0, -1).join(", ")}, and ${names.at(-1)}`;
8051
+ }
8052
+ function exportAdvice(profile, branch, changes) {
8053
+ const command = exportCommand(profile, branch);
8054
+ if (!remoteMoved(changes)) return `\`${command}\``;
8055
+ return `the branch has commits Metabase has not imported; \`${mbCommand(profile)} git-sync export-preflight\`, then \`${command} --merge\``;
8056
+ }
8057
+ async function planExport(ctx, runner, instance, names) {
8058
+ const changes = await mbTool(runner).gitSyncHasRemoteChanges(instance);
8059
+ if (remoteMoved(changes)) return {
8060
+ changes,
8061
+ push: false
8062
+ };
8063
+ return {
8064
+ changes,
8065
+ push: await ctx.prompts.confirm({
8066
+ key: SYNC_SCOPE_KEYS.export,
8067
+ message: `Push ${nameList(names)} to ${requireGitRemote(ctx).branch} now?`,
8068
+ initialValue: true
8069
+ })
8070
+ };
8071
+ }
8072
+ function describeExport(task) {
8073
+ const ended = `the export ended ${task.status}`;
8074
+ return task.error_message === null || task.error_message === void 0 ? ended : `${ended}: ${task.error_message}`;
8075
+ }
8076
+ function reportPending(run) {
8077
+ const advice = exportAdvice(run.ctx.state.mbProfile, run.gitRemote.branch, run.changes);
8078
+ run.ctx.ui.info(`First export pending: ${advice}`);
8079
+ }
8080
+ async function exportScope(run, runner) {
8081
+ const mb = mbTool(runner);
8082
+ const label = nameList(run.names);
8083
+ const spinner = run.ctx.ui.spinner(`Pushing ${label} to ${run.gitRemote.branch}…`);
8084
+ try {
8085
+ const final = (await mb.gitSyncExport(run.instance, {
8086
+ branch: run.gitRemote.branch,
8087
+ message: `Sync ${label}`
8088
+ })).final ?? null;
8089
+ if (final !== null && final.status !== "successful") {
8090
+ spinner.fail(describeExport(final));
8091
+ reportPending(run);
8092
+ return;
8093
+ }
8094
+ const status = await mb.gitSyncStatus(run.instance);
8095
+ spinner.stop(`Pushed ${label} to ${run.gitRemote.branch}`);
8096
+ if (status.is_dirty) run.ctx.ui.warn(`Metabase still has changes the export did not carry; \`${mbCommand(run.ctx.state.mbProfile)} git-sync dirty\` lists them`);
8097
+ } catch (error) {
8098
+ if (!(error instanceof ExternalToolError)) throw error;
8099
+ spinner.fail(`Could not push ${label}: ${error.message}`);
8100
+ reportPending(run);
8101
+ }
8102
+ }
8103
+ function scopeLabel(synced) {
8104
+ return `Synced to git: ${synced.map((collection) => collection.name).join(", ")}`;
8105
+ }
8106
+ function syncScopeStep(deps = hostDeps$4) {
8107
+ return {
8108
+ id: STEP_ID$2,
8109
+ title: "Choose what syncs to git",
8110
+ async detect(ctx) {
8111
+ if (!hasRemoteSync(ctx.state.license) || ctx.state.gitRemote === null) return "not-applicable";
8112
+ const configured = configuredRuntime(ctx.state);
8113
+ if (configured === null || ctx.secrets.apiKey === null) return "todo";
8114
+ const resolved = deps(ctx);
8115
+ if (!await resolved.instanceRunning()) return "todo";
8116
+ const client = resolved.client(configured.url, {
8117
+ kind: "api-key",
8118
+ key: ctx.secrets.apiKey
8119
+ });
8120
+ try {
8121
+ if (isComplete(readScope(await client.collection.list(), ctx.state.syncRoot), hasLibrary(ctx.state.license))) return "done";
8122
+ return await isReadOnly(client) ? "not-applicable" : "todo";
8123
+ } catch (error) {
8124
+ if (isNotAnsweringYet(error)) return "todo";
8125
+ throw error;
8126
+ }
8127
+ },
8128
+ async run(ctx) {
8129
+ requireConfiguredRuntime(ctx.state);
8130
+ const gitRemote = requireGitRemote(ctx);
8131
+ const instance = requireMbInstance(ctx);
8132
+ const resolved = deps(ctx);
8133
+ const client = resolved.client(instance.url, {
8134
+ kind: "api-key",
8135
+ key: instance.apiKey
8136
+ });
8137
+ await requireReadWrite(client);
8138
+ await requireSettledImport(ctx, client, gitRemote);
8139
+ const libraryLicensed = hasLibrary(ctx.state.license);
8140
+ const scope = readScope(await client.collection.list(), ctx.state.syncRoot);
8141
+ const names = plannedNames(scope, libraryLicensed);
8142
+ const plan = isComplete(scope, libraryLicensed) ? null : await planExport(ctx, resolved.runner, instance, names);
8143
+ const spinner = ctx.ui.spinner("Choosing what syncs to git…");
8144
+ let synced;
8145
+ let chosen;
8146
+ try {
8147
+ const library = await ensureLibrary(client, scope, libraryLicensed);
8148
+ chosen = await ensureRoots(ctx, client, scope);
8149
+ synced = library === null ? chosen.roots : [library, ...chosen.roots];
8150
+ await client.remoteSync.updateScope({ collections: Object.fromEntries(synced.map((collection) => [collection.id, true])) });
8151
+ } catch (error) {
8152
+ spinner.fail("Could not choose what syncs to git");
8153
+ throw explainRefusal(error);
8154
+ }
8155
+ spinner.stop(scopeLabel(synced));
8156
+ 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`);
8157
+ 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`);
8158
+ if (plan === null) return { outcome: "completed" };
8159
+ const exportRun = {
8160
+ ctx,
8161
+ instance,
8162
+ gitRemote,
8163
+ names,
8164
+ changes: plan.changes
8165
+ };
8166
+ if (plan.push) await exportScope(exportRun, resolved.runner);
8167
+ else reportPending(exportRun);
8168
+ return { outcome: "completed" };
8169
+ }
8170
+ };
8171
+ }
8172
+ //#endregion
7527
8173
  //#region src/steps/summary.ts
7528
8174
  const STEP_ID$1 = "summary";
7529
8175
  const GIT_SYNC_STEP_ID = "git-sync";
@@ -7570,16 +8216,36 @@ function remoteLine(remote) {
7570
8216
  case "hosted": return `Remote: ${remote.url}`;
7571
8217
  }
7572
8218
  }
7573
- function notConfiguredReason(state) {
7574
- const license = state.license;
8219
+ function licenseGap(license) {
7575
8220
  if (!license.activated) return "no license";
7576
8221
  if (!hasRemoteSync(license)) return "the license has no remote-sync feature";
8222
+ return null;
8223
+ }
8224
+ function notConfiguredReason(state) {
8225
+ const gap = licenseGap(state.license);
8226
+ if (gap !== null) return gap;
7577
8227
  if (state.steps[GIT_SYNC_STEP_ID]?.status === "skipped") return "skipped; `rde init --only git-sync` sets it up";
7578
8228
  return "not set up yet; `rde init --only git-sync`";
7579
8229
  }
7580
8230
  function gitLines(state) {
7581
8231
  if (state.gitRemote === null) return [`Remote sync: not configured (${notConfiguredReason(state)})`];
7582
- return [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
8232
+ const repository = [`Content repository: ${state.gitRemote.workdir} (branch ${state.gitRemote.branch})`, remoteLine(state.gitRemote)];
8233
+ const gap = licenseGap(state.license);
8234
+ if (gap === null) return repository;
8235
+ return [...repository, `Remote sync: unavailable (${gap}; \`rde init --from license\` activates one)`];
8236
+ }
8237
+ async function pendingExportLines(state, secrets, runner) {
8238
+ const instance = mbInstanceOf(state, secrets);
8239
+ if (state.gitRemote === null || instance === null) return [];
8240
+ try {
8241
+ const mb = mbTool(runner);
8242
+ if (!(await mb.gitSyncStatus(instance)).is_dirty) return [];
8243
+ const changes = await mb.gitSyncHasRemoteChanges(instance);
8244
+ return [`Export pending: ${exportAdvice(state.mbProfile, state.gitRemote.branch, changes)}`];
8245
+ } catch (error) {
8246
+ if (error instanceof ProcessNotFoundError || error instanceof ExternalToolError) return [`Remote sync: ${error.message}`];
8247
+ throw error;
8248
+ }
7583
8249
  }
7584
8250
  function skillLine(entries, detected) {
7585
8251
  const agents = [...new Set(entries.flatMap((entry) => coveredAgentNames(entry, detected)))];
@@ -7592,14 +8258,15 @@ function launchLines(agents, workdir) {
7592
8258
  const prefix = workdir === null ? "" : `cd ${workdir} && `;
7593
8259
  return ["Start working:", ...detected.map((agent) => ` ${prefix}${agent.launch} (${agent.displayName})`)];
7594
8260
  }
7595
- async function summaryLines(state, apiKey, deps) {
8261
+ async function summaryLines(state, secrets, deps) {
7596
8262
  const profile = state.mbProfile;
7597
8263
  const lines = [];
7598
8264
  if (state.url !== null) lines.push(`Metabase: ${state.url}`);
7599
8265
  if (state.admin !== null) lines.push(loginLine(state.admin.kind, state.admin.email));
7600
8266
  lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
7601
- lines.push(...await databaseLines$1(state, apiKey, deps));
8267
+ lines.push(...await databaseLines$1(state, secrets.apiKey, deps));
7602
8268
  lines.push(...gitLines(state));
8269
+ lines.push(...await pendingExportLines(state, secrets, deps.runner));
7603
8270
  const installed = await skillsCli(deps.runner).list();
7604
8271
  const agents = await detectAgents(deps.probe);
7605
8272
  const detected = agents.filter((agent) => agent.detected);
@@ -7616,7 +8283,7 @@ function summaryStep(deps = hostDeps$3) {
7616
8283
  return "todo";
7617
8284
  },
7618
8285
  async run(ctx) {
7619
- const lines = await summaryLines(ctx.state, ctx.secrets.apiKey, deps(ctx));
8286
+ const lines = await summaryLines(ctx.state, ctx.secrets, deps(ctx));
7620
8287
  ctx.ui.note(lines.join("\n"), "Your environment");
7621
8288
  return { outcome: "completed" };
7622
8289
  }
@@ -7693,6 +8360,7 @@ const STEP_BY_ID = {
7693
8360
  license: () => licenseStep(),
7694
8361
  databases: (options) => databasesStep(options.databases),
7695
8362
  "git-sync": () => gitSyncStep(),
8363
+ "sync-scope": () => syncScopeStep(),
7696
8364
  summary: () => summaryStep()
7697
8365
  };
7698
8366
  function initSteps(options) {
@@ -7777,7 +8445,8 @@ const SECTIONS = {
7777
8445
  remote: RemoteKind.optional(),
7778
8446
  url: EnvString.optional(),
7779
8447
  token: EnvString.optional()
7780
- }).optional()
8448
+ }).optional(),
8449
+ "sync-scope": z.strictObject({ export: z.boolean() }).optional()
7781
8450
  };
7782
8451
  const InitAnswers = z.strictObject(SECTIONS);
7783
8452
  const select$1 = (value) => ({
@@ -7886,6 +8555,8 @@ function keyedAnswers(answers) {
7886
8555
  if (git.url !== void 0) add(map, GIT_SYNC_KEYS.url, text$1(git.url));
7887
8556
  if (git.token !== void 0) add(map, GIT_SYNC_KEYS.token, password$1(git.token));
7888
8557
  }
8558
+ const scope = answers["sync-scope"];
8559
+ if (scope !== void 0) add(map, SYNC_SCOPE_KEYS.export, confirm$1(scope.export));
7889
8560
  return map;
7890
8561
  }
7891
8562
  function parseInitAnswers(raw, source) {
@@ -7927,6 +8598,10 @@ function stepsAskingForWork(answers) {
7927
8598
  step: "git-sync",
7928
8599
  asks: git !== void 0 && git.location !== "skip"
7929
8600
  },
8601
+ {
8602
+ step: "sync-scope",
8603
+ asks: answers["sync-scope"]?.export === true
8604
+ },
7930
8605
  {
7931
8606
  step: "agents-skills",
7932
8607
  asks: agents !== void 0 && agents.agents.length > 0
@@ -8528,8 +9203,7 @@ function plainUi(output) {
8528
9203
  };
8529
9204
  }
8530
9205
  function noColorRequested() {
8531
- const value = readEnv(ENV_NO_COLOR);
8532
- return value !== void 0 && value !== "";
9206
+ return readSetEnv(ENV_NO_COLOR) !== null;
8533
9207
  }
8534
9208
  function drawsOnTerminal() {
8535
9209
  return process.stdout.isTTY === true && !noColorRequested();
@@ -8660,6 +9334,20 @@ const db = defineCommand({
8660
9334
  default: "list"
8661
9335
  });
8662
9336
  //#endregion
9337
+ //#region package.json
9338
+ var name = "@metabase/rde";
9339
+ var version = "0.1.18";
9340
+ var description = "Prepares a local machine for data-engineering work with Metabase and coding agents";
9341
+ //#endregion
9342
+ //#region src/version.ts
9343
+ const PACKAGE_NAME = name;
9344
+ const VERSION = version;
9345
+ const ROOT_META = {
9346
+ name: "rde",
9347
+ version: VERSION,
9348
+ description
9349
+ };
9350
+ //#endregion
8663
9351
  //#region src/wizard/engine.ts
8664
9352
  function requireKnownStep(steps, id, flag) {
8665
9353
  if (id === void 0) return;
@@ -8756,6 +9444,8 @@ async function runWizard(steps, ctx, options = {}) {
8756
9444
  }
8757
9445
  //#endregion
8758
9446
  //#region src/steps/doctor.ts
9447
+ const RDE_DIST_TAG = "latest";
9448
+ const RDE_RELEASE_TIMEOUT_MS = 3e3;
8759
9449
  function hostDoctorDeps(instanceName, signal) {
8760
9450
  const runner = processRunner(signal);
8761
9451
  const runtime = hostRuntimeDeps(instanceName, signal);
@@ -8775,7 +9465,8 @@ function hostDoctorDeps(instanceName, signal) {
8775
9465
  cwd: process.cwd(),
8776
9466
  stateHome: xdgStateHome(),
8777
9467
  files: hostSkillFiles
8778
- }
9468
+ },
9469
+ publishedRdeVersion: () => npmRegistry(signal, { timeoutMs: RDE_RELEASE_TIMEOUT_MS }).distTagVersion(PACKAGE_NAME, RDE_DIST_TAG)
8779
9470
  };
8780
9471
  }
8781
9472
  const NOT_FOUND_STATUS = 404;
@@ -8869,6 +9560,26 @@ function mbRow(diagnosis, lines, deps) {
8869
9560
  case "current": return ok("mb", MB_LABEL, lines.join("\n"));
8870
9561
  }
8871
9562
  }
9563
+ const RDE_LABEL = "rde";
9564
+ const RDE_UPDATE_HINT = `\`npm i -g ${PACKAGE_NAME}@${RDE_DIST_TAG}\``;
9565
+ function rdeUncheckedRow(reason) {
9566
+ return skipped("rde", RDE_LABEL, `${VERSION}; could not check for a newer release: ${reason}`);
9567
+ }
9568
+ async function rdeRow(deps) {
9569
+ let raw;
9570
+ try {
9571
+ raw = await deps.publishedRdeVersion();
9572
+ } catch (error) {
9573
+ const reason = lookupFailure(error);
9574
+ if (reason === null) throw error;
9575
+ return rdeUncheckedRow(reason);
9576
+ }
9577
+ const published = semver.valid(raw);
9578
+ if (published === null) return rdeUncheckedRow(`the registry names "${raw}" for ${RDE_DIST_TAG}, which is not a version`);
9579
+ if (semver.lt(VERSION, published)) return note$1("rde", RDE_LABEL, `${VERSION}; ${published} is released`, RDE_UPDATE_HINT);
9580
+ if (semver.gt(VERSION, published)) return ok("rde", RDE_LABEL, `${VERSION}, ahead of the latest release ${published}`);
9581
+ return ok("rde", RDE_LABEL, `${VERSION}, the latest release`);
9582
+ }
8872
9583
  async function mbFinding(deps) {
8873
9584
  const found = await inspectMbOnPath(deps.runner, deps.locateMb);
8874
9585
  const diagnosis = diagnoseMb(found, deps.installedWith);
@@ -8889,6 +9600,20 @@ function publishesOnLoopback(container, port) {
8889
9600
  }
8890
9601
  const CONTAINER = "the container";
8891
9602
  const CONTAINER_SILENT_PORT_HINT = "Docker's forward of the port is broken; `rde start` repairs it";
9603
+ function containerSettingsFailure(runtime, inspection) {
9604
+ const onMaster = !developmentMode() && isMasterBuildImage(runtime.image);
9605
+ const drift = containerEnvDrift(inspection, runtime);
9606
+ const problems = [];
9607
+ if (onMaster) problems.push(`${runtime.containerName} runs ${runtime.image}, a development build of Metabase`);
9608
+ if (drift.length > 0) problems.push(`${runtime.containerName} was created with ${drift.join(", ")} set otherwise than rde sets them now`);
9609
+ if (problems.length === 0) return null;
9610
+ const hint = onMaster ? "a build of master is ahead of every release, so `rde uninstall`, then `rde init`, sets up an instance on a release; `RDE_DEV=1` keeps running this one" : "`rde upgrade` creates the container again with rde's settings";
9611
+ return {
9612
+ detail: problems.join("\n"),
9613
+ hint,
9614
+ step: null
9615
+ };
9616
+ }
8892
9617
  async function containerRow(runtime, deps, dockerOk) {
8893
9618
  const label = "container";
8894
9619
  if (!dockerOk) return {
@@ -8929,6 +9654,11 @@ async function containerRow(runtime, deps, dockerOk) {
8929
9654
  }),
8930
9655
  ...found
8931
9656
  };
9657
+ const settings = containerSettingsFailure(runtime, inspection);
9658
+ if (settings !== null) return {
9659
+ row: failed("container", label, settings),
9660
+ ...found
9661
+ };
8932
9662
  const state = inspection.running ? "running" : "stopped";
8933
9663
  return {
8934
9664
  row: ok("container", label, `${runtime.containerName} ${state} (${runtime.image})`),
@@ -9407,25 +10137,71 @@ async function skillCopiesRow(globalEntries, deps) {
9407
10137
  const stale = judged.filter((copy) => !copy.current);
9408
10138
  return stale.length === 0 ? ok(id, label, detail) : staleCopiesRow(stale, detail, deps);
9409
10139
  }
10140
+ function dataAppBranchLine(version, origin) {
10141
+ const wanted = withRef(origin.repository, origin.ref);
10142
+ return version === null ? `Metabase has not reported its version yet; rde installs them from ${wanted}` : `Metabase ${version} takes them from ${wanted}`;
10143
+ }
10144
+ async function dataAppSkillsRow(state, installed, deps) {
10145
+ const id = "data-app-skills";
10146
+ const label = "data-app skills";
10147
+ const branch = dataAppSkillsOrigin(wantedDataAppRef(state.version));
10148
+ const wanted = [...DATA_APP_SKILL_NAMES.map((name) => ({
10149
+ name,
10150
+ origin: branch
10151
+ })), {
10152
+ name: RDE_DATA_APPS_SKILL_NAME,
10153
+ origin: skillOrigin(deps.skills.source, deps.skills.cwd)
10154
+ }];
10155
+ const lines = [dataAppBranchLine(state.version, branch)];
10156
+ let current = true;
10157
+ for (const skill of wanted) {
10158
+ const entry = installed.find((candidate) => candidate.name === skill.name);
10159
+ if (entry === void 0) {
10160
+ lines.push(`${skill.name}: not installed`);
10161
+ current = false;
10162
+ continue;
10163
+ }
10164
+ const judged = await judgeCopy(entry, skill.origin, deps);
10165
+ lines.push(judged.line);
10166
+ current &&= judged.current;
10167
+ }
10168
+ const detail = lines.join("\n");
10169
+ if (current) return ok(id, label, detail);
10170
+ return failed(id, label, {
10171
+ detail,
10172
+ hint: `\`rde init --only ${SKILL_STEP}\` installs them; then restart the agent session`,
10173
+ step: SKILL_STEP
10174
+ });
10175
+ }
9410
10176
  async function skillRows(state, deps) {
9411
10177
  const label = "rde skill";
9412
10178
  if (state === null) return [skipped("skill", label, NO_INSTANCE_REASON)];
9413
10179
  if (state.steps[SKILL_STEP]?.status === "skipped") return [skipped("skill", label, `skipped during init; \`rde init --only ${SKILL_STEP}\``)];
9414
10180
  const detected = (await detectAgents(deps.probe)).filter((agent) => agent.detected);
9415
10181
  if (detected.length === 0) return [skipped("skill", label, "no coding agents detected")];
9416
- const entries = await rdeSkillEntries(deps.runner);
10182
+ const installed = await skillsCli(deps.runner).list();
10183
+ const entries = installed.filter((entry) => entry.name === "rde");
9417
10184
  const copies = await skillCopiesRow(entries, deps);
9418
- if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [failed("skill", label, {
9419
- detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
9420
- hint: `\`rde init --only ${SKILL_STEP}\``,
9421
- step: SKILL_STEP
9422
- }), copies];
9423
- return [...detected.map((agent) => {
9424
- const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
9425
- const agentLabel = `${label} (${agent.displayName})`;
9426
- if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
9427
- return ok(skillRowId(agent), agentLabel, entry.path);
9428
- }), copies];
10185
+ const dataApps = await dataAppSkillsRow(state, installed, deps);
10186
+ if (detected.filter((agent) => entries.some((entry) => skillCoversAgent(entry, agent))).length === 0) return [
10187
+ failed("skill", label, {
10188
+ detail: `not installed for ${detected.map((agent) => agent.displayName).join(", ")}`,
10189
+ hint: `\`rde init --only ${SKILL_STEP}\``,
10190
+ step: SKILL_STEP
10191
+ }),
10192
+ copies,
10193
+ dataApps
10194
+ ];
10195
+ return [
10196
+ ...detected.map((agent) => {
10197
+ const entry = entries.find((candidate) => skillCoversAgent(candidate, agent));
10198
+ const agentLabel = `${label} (${agent.displayName})`;
10199
+ if (entry === void 0) return skipped(skillRowId(agent), agentLabel, `not installed; \`rde init --only ${SKILL_STEP}\` adds it`);
10200
+ return ok(skillRowId(agent), agentLabel, entry.path);
10201
+ }),
10202
+ copies,
10203
+ dataApps
10204
+ ];
9429
10205
  }
9430
10206
  function noRuntimeRows() {
9431
10207
  return [
@@ -9470,7 +10246,11 @@ async function collectDoctor(instance, state, secrets, deps) {
9470
10246
  const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
9471
10247
  const mb = await mbFinding(deps);
9472
10248
  const profiles = sharedProfileList(deps);
9473
- const checks = [...machine, mb.row];
10249
+ const checks = [
10250
+ ...machine,
10251
+ mb.row,
10252
+ await rdeRow(deps)
10253
+ ];
9474
10254
  if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE_REASON), skipped("admin", "admin user", NO_INSTANCE_REASON), skipped("admin-password", "admin password", NO_INSTANCE_REASON), skipped("api-key", "API key", NO_INSTANCE_REASON), skipped("secrets-file", "secrets file", NO_INSTANCE_REASON), skipped("state-file", "state file", NO_INSTANCE_REASON), skipped("mb-profile", "mb profile", NO_INSTANCE_REASON), skipped("license", "license", NO_INSTANCE_REASON), skipped("databases", "databases", NO_INSTANCE_REASON), skipped("git-sync", "remote sync", NO_INSTANCE_REASON));
9475
10255
  else checks.push(...await instanceRows(state, secrets, deps, dockerOk, profiles, mb.unusable));
9476
10256
  const rdeProfile = state === null ? null : state.mbProfile;
@@ -9666,7 +10446,8 @@ function applyRecord(draft, record) {
9666
10446
  else if (key === API_KEY_KEYS.profile && value !== null) {
9667
10447
  const profile = MbProfileChoice.safeParse(value);
9668
10448
  if (profile.success) draft.answers["api-key"] = { profile: profile.data };
9669
- } else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
10449
+ } else if (key === SYNC_SCOPE_KEYS.export && record.answer.kind === "confirm") draft.answers["sync-scope"] = { export: record.answer.value };
10450
+ else if (key === LICENSE_KEYS.token && value !== null) draft.answers.license = { token: value.length === 0 ? null : envReference(ENV_LICENSE_TOKEN) };
9670
10451
  }
9671
10452
  function writableUri(uri, index) {
9672
10453
  const trimmed = uri.trim();
@@ -10127,7 +10908,7 @@ function taskLabel(status) {
10127
10908
  }
10128
10909
  function remoteContentOf(changes) {
10129
10910
  if (!changes.has_changes) return "imported";
10130
- return changes.local_version === null ? "never-imported" : "changed";
10911
+ return remoteMoved(changes) ? "changed" : "never-imported";
10131
10912
  }
10132
10913
  function isMbFailure(error) {
10133
10914
  return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
@@ -10145,7 +10926,8 @@ async function gitSyncOf(ctx, deps) {
10145
10926
  branch: status.branch,
10146
10927
  dirty: status.is_dirty,
10147
10928
  remote: remoteContentOf(changes),
10148
- task: taskLabel(status)
10929
+ task: taskLabel(status),
10930
+ syncedCollections: status.synced_collections.map((collection) => collection.name)
10149
10931
  };
10150
10932
  } catch (error) {
10151
10933
  if (isMbFailure(error)) return { error: error.message };
@@ -10205,7 +10987,8 @@ function gitSyncLine(report) {
10205
10987
  if (isGitSyncFailure(report.gitSync)) return `Remote sync: ${report.gitSync.error}`;
10206
10988
  const branch = report.gitSync.branch === null ? "no branch" : `branch ${report.gitSync.branch}`;
10207
10989
  const task = report.gitSync.task === null ? "no task running" : `task ${report.gitSync.task}`;
10208
- return `Remote sync: ${branch}, ${syncStateLabel(report.mbProfile, report.gitSync)}, ${task}`;
10990
+ const scope = report.gitSync.syncedCollections.length === 0 ? "nothing synced; `rde init --only sync-scope`" : `synced: ${report.gitSync.syncedCollections.join(", ")}`;
10991
+ return `Remote sync: ${branch}, ${syncStateLabel(report.mbProfile, report.gitSync)}, ${task}, ${scope}`;
10209
10992
  }
10210
10993
  const REMOTE_CONTENT_LABEL = {
10211
10994
  imported: null,
@@ -10593,7 +11376,7 @@ function keptOnDisk(options, state, dirPresent) {
10593
11376
  const kept = [];
10594
11377
  if (options.keepData && dirPresent) kept.push(`Instance directory ${instanceDir(options.instance)} (--keep-data); \`rde init\` reinstalls from it`);
10595
11378
  if (state?.gitRemote !== null && state?.gitRemote !== void 0) kept.push(`Working directory ${state.gitRemote.workdir} and its git history`);
10596
- if (!options.removeSkill) kept.push(`The rde skill for your agents; pass --remove-skill to remove it`);
11379
+ if (!options.removeSkill) kept.push(`The ${RDE_SOURCE_SKILL_NAMES.join(" and ")} skills for your agents; pass --remove-skill to remove them`);
10597
11380
  return kept;
10598
11381
  }
10599
11382
  async function planUninstall(options, deps) {
@@ -10700,6 +11483,9 @@ function reportLines(report) {
10700
11483
  function leftBehind(report) {
10701
11484
  return [...report.results.flatMap((result) => result.outcome === "failed" ? [result.label] : []), ...report.problems.map((problem) => problem.subject)];
10702
11485
  }
11486
+ function uninstallCommandLine(options) {
11487
+ return options.instance === options.defaultInstance ? "rde uninstall" : `rde uninstall --instance ${options.instance}`;
11488
+ }
10703
11489
  async function uninstall(options, deps, io) {
10704
11490
  const plan = await planUninstall(options, deps);
10705
11491
  for (const problem of plan.problems) io.ui.warn(`${problem.subject} ${problem.reason}`);
@@ -10710,7 +11496,7 @@ async function uninstall(options, deps, io) {
10710
11496
  problems: plan.problems
10711
11497
  };
10712
11498
  }
10713
- io.ui.note(planLines$1(plan).join("\n"), `rde uninstall ${options.instance}`);
11499
+ io.ui.note(planLines$1(plan).join("\n"), uninstallCommandLine(options));
10714
11500
  if (plan.unpulled !== null) {
10715
11501
  io.ui.warn(unpulledWarning(plan.unpulled));
10716
11502
  if ((options.yes || !options.terminal) && !options.force) throw new ConfigError(unpulledRefusal(plan.unpulled));
@@ -10749,7 +11535,7 @@ const uninstallCommand = defineRdeCommand({
10749
11535
  },
10750
11536
  "remove-skill": {
10751
11537
  type: "boolean",
10752
- description: "Also remove the rde skill from your coding agents",
11538
+ description: "Also remove the rde and rde-data-apps skills from your coding agents",
10753
11539
  default: false
10754
11540
  },
10755
11541
  "remove-image": {
@@ -10770,11 +11556,13 @@ const uninstallCommand = defineRdeCommand({
10770
11556
  },
10771
11557
  interrupted: INTERRUPTED,
10772
11558
  async run(args) {
10773
- const instance = args.instance ?? await instanceNameOrDefault();
11559
+ const defaultInstance = await instanceNameOrDefault();
11560
+ const instance = args.instance ?? defaultInstance;
10774
11561
  if (!isInstanceName(instance)) throw new ConfigError(`${args.instance === void 0 ? `the default instance in ${globalConfigPath()},` : "--instance"} ${instance} is not an instance name (${INSTANCE_NAME_RULE})`);
10775
11562
  const ui = terminalUi();
10776
11563
  const report = await uninstall({
10777
11564
  instance,
11565
+ defaultInstance,
10778
11566
  keepData: args["keep-data"],
10779
11567
  removeSkill: args["remove-skill"],
10780
11568
  removeImage: args["remove-image"],
@@ -10800,35 +11588,59 @@ function hostDeps(ctx) {
10800
11588
  client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
10801
11589
  timing: DEFAULT_HEALTH_TIMING,
10802
11590
  releases: releaseLookup(ctx.signal),
10803
- now: () => /* @__PURE__ */ new Date()
11591
+ now: () => /* @__PURE__ */ new Date(),
11592
+ skills: (agents) => agentsSkillsStep(agents, "published")
10804
11593
  };
10805
11594
  }
10806
- function imageTag(ref) {
10807
- const colon = ref.lastIndexOf(":");
10808
- if (colon === -1 || colon < ref.lastIndexOf("/")) return null;
10809
- return ref.slice(colon + 1);
11595
+ function majorMove(from, to) {
11596
+ const fromMajor = releaseMajor(from);
11597
+ const toMajor = to === null ? null : releaseMajor(to);
11598
+ if (fromMajor === null || toMajor === null || fromMajor === toMajor) return null;
11599
+ return {
11600
+ from: fromMajor,
11601
+ to: toMajor
11602
+ };
10810
11603
  }
10811
11604
  function releaseOf(tag) {
10812
11605
  const match = RELEASE_TAG.exec(tag);
10813
11606
  if (match === null || match[1] === void 0) return null;
10814
11607
  return semver.coerce(match[1]);
10815
11608
  }
10816
- function downgradeReason(current, targetTag) {
10817
- if (targetTag === null) return null;
11609
+ function isDowngrade(current, targetTag) {
11610
+ if (isMajorLineTag(targetTag)) {
11611
+ const running = releaseMajor(current);
11612
+ const target = releaseMajor(targetTag);
11613
+ return running !== null && target !== null && target < running;
11614
+ }
10818
11615
  const running = releaseOf(current);
10819
11616
  const target = releaseOf(targetTag);
10820
- if (running === null || target === null || !semver.lt(target, running)) return null;
11617
+ return running !== null && target !== null && semver.lt(target, running);
11618
+ }
11619
+ function downgradeReason(current, targetTag) {
11620
+ if (targetTag === null || !isDowngrade(current, targetTag)) return null;
10821
11621
  return `refusing to downgrade from ${current} to ${targetTag}: Metabase migrates its application database forward only; a fresh instance (\`rde uninstall\`, then \`rde init\`) is the way to run an older release`;
10822
11622
  }
10823
11623
  function backupStamp(now) {
10824
11624
  return now.toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
10825
11625
  }
10826
- async function ensureRunning(ctx, deps, client) {
10827
- await startMetabaseStep(() => deps).run(ctx);
11626
+ async function ensureRunning(ctx, deps, client, accepted) {
11627
+ await startMetabaseStep(() => deps, accepted).run(ctx);
10828
11628
  return runningVersion(client);
10829
11629
  }
10830
- function dockerTarget(runtime, to) {
10831
- const image = to ?? runtime.image;
11630
+ function requireSupported(version) {
11631
+ const refusal = unsupportedVersionReason(version, developmentMode());
11632
+ if (refusal !== null) throw new UnsupportedMetabaseError(refusal);
11633
+ }
11634
+ function knownTargetVersion(target) {
11635
+ const tag = target.releaseTag;
11636
+ return tag !== null && (isReleaseTag(tag) || isMajorLineTag(tag)) ? tag : null;
11637
+ }
11638
+ const TO_ONE = "`--to <image or release tag>` names a Metabase to move to";
11639
+ const UPGRADE_WAYS_OUT = {
11640
+ named: TO_ONE,
11641
+ unreleased: TO_ONE
11642
+ };
11643
+ function dockerTargetOf(runtime, image, ownImage) {
10832
11644
  return {
10833
11645
  kind: "docker",
10834
11646
  next: {
@@ -10836,22 +11648,24 @@ function dockerTarget(runtime, to) {
10836
11648
  image
10837
11649
  },
10838
11650
  label: image,
10839
- releaseTag: imageTag(image)
11651
+ releaseTag: imageTag(image),
11652
+ ownImage
10840
11653
  };
10841
11654
  }
11655
+ async function dockerTarget(ctx, runtime, to, releases) {
11656
+ if (to !== null) return dockerTargetOf(runtime, to, false);
11657
+ if (developmentMode()) return dockerTargetOf(runtime, runtime.image, false);
11658
+ if (!isMetabaseImage(runtime.image)) return dockerTargetOf(runtime, runtime.image, true);
11659
+ return dockerTargetOf(runtime, await releaseImage(ctx, releases, UPGRADE_WAYS_OUT), false);
11660
+ }
10842
11661
  async function jarTarget(ctx, runtime, to, releases) {
10843
11662
  const spec = to ?? "latest";
10844
11663
  const parsed = parseJarSpec(spec);
10845
- 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`);
10846
- let source;
10847
- if (parsed.kind === "latest") {
10848
- const version = await releases.latestVersion();
10849
- ctx.ui.info(`The latest Metabase release is ${version}`);
10850
- source = {
10851
- kind: "release",
10852
- version
10853
- };
10854
- } else source = parsed;
11664
+ if (parsed === null) throw new ConfigError(`--to ${spec} is not latest, an enterprise release tag such as v1.65.0, or the path of a .jar file`);
11665
+ const source = parsed.kind === "latest" ? {
11666
+ kind: "release",
11667
+ version: await latestJarRelease(ctx, releases, UPGRADE_WAYS_OUT)
11668
+ } : parsed;
10855
11669
  return {
10856
11670
  kind: "jar",
10857
11671
  previous: runtime,
@@ -10865,7 +11679,7 @@ async function jarTarget(ctx, runtime, to, releases) {
10865
11679
  }
10866
11680
  function resolveTarget(ctx, configured, to, releases) {
10867
11681
  switch (configured.runtime.kind) {
10868
- case "docker": return Promise.resolve(dockerTarget(configured.runtime, to));
11682
+ case "docker": return dockerTarget(ctx, configured.runtime, to, releases);
10869
11683
  case "jar": return jarTarget(ctx, configured.runtime, to, releases);
10870
11684
  }
10871
11685
  }
@@ -10915,9 +11729,13 @@ const SWITCH_TEXT = {
10915
11729
  jar: "Metabase stops and starts again from the target jar."
10916
11730
  };
10917
11731
  function planLines(configured, current, target, backup) {
11732
+ const notes = [];
11733
+ if (target.kind === "docker" && target.ownImage) notes.push("the instance's own image pulled again, since only Metabase's images move to the newest release");
11734
+ if (target.releaseTag === null || !isReleaseTag(target.releaseTag)) notes.push("its version shows once it runs");
11735
+ const annotation = notes.length === 0 ? "" : ` (${notes.join("; ")})`;
10918
11736
  return [
10919
11737
  `Running: ${current} (${describeRuntime(configured.runtime)})`,
10920
- `Target: ${target.label}`,
11738
+ `Target: ${target.label}${annotation}`,
10921
11739
  `Backup: ${backup}`,
10922
11740
  "",
10923
11741
  SWITCH_TEXT[configured.runtime.kind],
@@ -10976,31 +11794,70 @@ async function switchTo(ctx, deps, target) {
10976
11794
  }
10977
11795
  spinner.stop(`Installed ${target.label}`);
10978
11796
  }
11797
+ function failedStartTitle(error) {
11798
+ if (error instanceof MetabaseUnreachableError) return "Metabase did not come up on the new version";
11799
+ if (error instanceof UnsupportedMetabaseError) return "The new version is not one rde runs";
11800
+ return null;
11801
+ }
10979
11802
  async function startUpgraded(ctx, deps, client, previous, backup) {
10980
11803
  try {
10981
- return await ensureRunning(ctx, deps, client);
11804
+ return await ensureRunning(ctx, deps, client, "supported");
10982
11805
  } catch (error) {
10983
- if (error instanceof MetabaseUnreachableError) ctx.ui.note(restoreLines(ctx, previous, deps.runtime.dataDir, backup).join("\n"), "Metabase did not come up on the new version");
11806
+ const title = failedStartTitle(error);
11807
+ if (title !== null) ctx.ui.note(restoreLines(ctx, previous, deps.runtime.dataDir, backup).join("\n"), title);
10984
11808
  throw error;
10985
11809
  }
10986
11810
  }
11811
+ async function recreateStale(ctx, deps, previous, target) {
11812
+ const next = target.next;
11813
+ const container = await deps.runtime.docker.containerInspect(next.containerName);
11814
+ const drift = container.exists ? containerEnvDrift(container, next) : [];
11815
+ const renamed = previous.kind === "docker" && previous.image !== next.image;
11816
+ if (drift.length === 0 && !renamed) return false;
11817
+ if (renamed) {
11818
+ ctx.ui.info(`${previous.image} is the image ${next.image} names; creating ${next.containerName} again under ${next.image}`);
11819
+ await ctx.saveState({
11820
+ ...ctx.state,
11821
+ runtime: next
11822
+ });
11823
+ }
11824
+ if (drift.length > 0) ctx.ui.info(`${next.containerName} was created with ${drift.join(", ")} set otherwise than rde sets them now; creating it again from the image it runs`);
11825
+ await runtimeFromState(next, deps.runtime).reconfigure();
11826
+ return true;
11827
+ }
11828
+ async function alreadyOn(ctx, deps, client, configured, current, target) {
11829
+ if (target.kind === "docker" && await recreateStale(ctx, deps, configured.runtime, target)) {
11830
+ await ensureRunning(ctx, deps, client, "supported");
11831
+ ctx.ui.success(`Already on ${current}; ${target.next.containerName} runs ${target.label} with rde's settings now`);
11832
+ return {
11833
+ outcome: "recreated",
11834
+ version: current
11835
+ };
11836
+ }
11837
+ ctx.ui.success(`Already on ${current}: ${target.label} is what the instance runs`);
11838
+ return {
11839
+ outcome: "unchanged",
11840
+ version: current
11841
+ };
11842
+ }
10987
11843
  async function upgrade(ctx, options, deps = hostDeps) {
10988
11844
  const resolved = deps(ctx);
10989
11845
  const configured = requireConfiguredRuntime(ctx.state);
10990
11846
  const client = resolved.client(configured.url, NO_AUTH);
10991
- const current = await ensureRunning(ctx, resolved, client);
10992
11847
  const target = await resolveTarget(ctx, configured, options.to, resolved.releases);
11848
+ const current = await ensureRunning(ctx, resolved, client, "any");
10993
11849
  const refusal = downgradeReason(current, target.releaseTag);
10994
11850
  if (refusal !== null) throw new ConfigError(refusal);
11851
+ const known = knownTargetVersion(target);
11852
+ if (known !== null) requireSupported(known);
10995
11853
  if (await fetchTarget(ctx, resolved, target, current)) {
10996
- ctx.ui.success(`Already on ${current}: ${target.label} is what the instance runs`);
10997
- return {
10998
- outcome: "unchanged",
10999
- version: current
11000
- };
11854
+ requireSupported(current);
11855
+ return alreadyOn(ctx, resolved, client, configured, current, target);
11001
11856
  }
11002
11857
  const backup = join(instanceBackupsDir(ctx.state.name), backupStamp(resolved.now()));
11003
11858
  ctx.ui.note(planLines(configured, current, target, backup).join("\n"), "rde upgrade");
11859
+ const announced = majorMove(current, target.releaseTag);
11860
+ if (announced !== null) ctx.ui.warn(`This moves Metabase from ${announced.from} to ${announced.to}, a new major version; the data-app skills follow it after the upgrade`);
11004
11861
  if (!options.yes) {
11005
11862
  if (!await ctx.prompts.confirm({
11006
11863
  message: "Upgrade now?",
@@ -11021,6 +11878,11 @@ async function upgrade(ctx, options, deps = hostDeps) {
11021
11878
  options.onPhase("starting", backup);
11022
11879
  const to = await startUpgraded(ctx, resolved, client, configured, backup);
11023
11880
  ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
11881
+ const moved = majorMove(current, to);
11882
+ if (moved !== null) {
11883
+ if (announced === null) ctx.ui.warn(`This moved Metabase from ${moved.from} to ${moved.to}, a new major version; the data-app skills follow it`);
11884
+ await runWizard([resolved.skills(options.yes ? DETECT_AGENTS : ASK_AGENTS)], ctx);
11885
+ }
11024
11886
  return {
11025
11887
  outcome: "upgraded",
11026
11888
  from: current,
@@ -11046,7 +11908,7 @@ const upgradeCommand = defineRdeCommand({
11046
11908
  args: {
11047
11909
  to: {
11048
11910
  type: "string",
11049
- description: "Docker: image to upgrade to (default: the instance's image, pulled again); jar: latest, a release tag, or a .jar path (default: latest)"
11911
+ description: "Docker: image to upgrade to (default: the major line of the newest Metabase Enterprise release, such as metabase/metabase-enterprise:v1.65.x; the instance's own image pulled again with RDE_DEV=1 or for an image from outside Metabase's repositories); jar: latest (the newest release), a release tag, or a .jar path (default: latest)"
11050
11912
  },
11051
11913
  yes: {
11052
11914
  type: "boolean",
@@ -11074,11 +11936,7 @@ const upgradeCommand = defineRdeCommand({
11074
11936
  //#endregion
11075
11937
  //#region src/main.ts
11076
11938
  const main = defineCommand({
11077
- meta: {
11078
- name: "rde",
11079
- version: "0.1.16",
11080
- description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
11081
- },
11939
+ meta: ROOT_META,
11082
11940
  subCommands: {
11083
11941
  init,
11084
11942
  start,