@metabase/rde 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +108 -61
- package/dist/cli.mjs +1944 -634
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { defineCommand, runMain, showUsage } from "citty";
|
|
3
|
-
import { promises } from "node:fs";
|
|
3
|
+
import { createWriteStream, promises } from "node:fs";
|
|
4
4
|
import { delimiter, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { ZodError, z } from "zod";
|
|
6
6
|
import { homedir, tmpdir, userInfo } from "node:os";
|
|
7
7
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
8
|
+
import { Readable } from "node:stream";
|
|
9
|
+
import { pipeline } from "node:stream/promises";
|
|
8
10
|
import { confirm, intro, isCancel, log, multiselect, note, outro, password, select, spinner, text } from "@clack/prompts";
|
|
9
|
-
import { connect, createServer } from "node:net";
|
|
10
11
|
import { spawn } from "node:child_process";
|
|
12
|
+
import { connect, createServer } from "node:net";
|
|
11
13
|
import semver from "semver";
|
|
12
14
|
//#region src/errors.ts
|
|
13
15
|
var AbortError = class extends Error {
|
|
@@ -70,6 +72,14 @@ var MetabaseUnreachableError = class extends Error {
|
|
|
70
72
|
this.url = url;
|
|
71
73
|
}
|
|
72
74
|
};
|
|
75
|
+
var DownloadError = class extends Error {
|
|
76
|
+
url;
|
|
77
|
+
constructor(url, reason) {
|
|
78
|
+
super(`downloading ${url} failed: ${reason}`);
|
|
79
|
+
this.name = "DownloadError";
|
|
80
|
+
this.url = url;
|
|
81
|
+
}
|
|
82
|
+
};
|
|
73
83
|
//#endregion
|
|
74
84
|
//#region src/output/error.ts
|
|
75
85
|
const INTERRUPTED_MESSAGE = "Interrupted. Run `rde init` again to resume.";
|
|
@@ -115,6 +125,7 @@ const StepId = z.enum([
|
|
|
115
125
|
"start-metabase",
|
|
116
126
|
"admin-user",
|
|
117
127
|
"api-key",
|
|
128
|
+
"sample-database",
|
|
118
129
|
"license",
|
|
119
130
|
"databases",
|
|
120
131
|
"git-sync",
|
|
@@ -132,17 +143,20 @@ const DockerRuntimeState = z.object({
|
|
|
132
143
|
port: z.int().positive(),
|
|
133
144
|
hostAlias: z.literal(DOCKER_HOST_ALIAS)
|
|
134
145
|
});
|
|
135
|
-
const
|
|
146
|
+
const JarSource = z.discriminatedUnion("kind", [z.object({
|
|
147
|
+
kind: z.literal("release"),
|
|
148
|
+
version: z.string()
|
|
149
|
+
}), z.object({
|
|
150
|
+
kind: z.literal("file"),
|
|
151
|
+
path: z.string()
|
|
152
|
+
})]);
|
|
136
153
|
const JarRuntimeState = z.object({
|
|
137
154
|
kind: z.literal("jar"),
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
jarPath: z.string(),
|
|
141
|
-
javaPath: z.string(),
|
|
142
|
-
port: z.int().positive(),
|
|
143
|
-
pid: z.int().positive().nullable()
|
|
155
|
+
source: JarSource,
|
|
156
|
+
port: z.int().positive()
|
|
144
157
|
});
|
|
145
158
|
const RuntimeState = z.discriminatedUnion("kind", [DockerRuntimeState, JarRuntimeState]);
|
|
159
|
+
const RuntimeKind = z.enum(["docker", "jar"]);
|
|
146
160
|
const AdminCredentials = z.discriminatedUnion("kind", [z.object({
|
|
147
161
|
kind: z.literal("prompted"),
|
|
148
162
|
email: z.string()
|
|
@@ -166,6 +180,12 @@ const DatabaseRecord = z.object({
|
|
|
166
180
|
engine: z.string(),
|
|
167
181
|
createdAt: z.string()
|
|
168
182
|
});
|
|
183
|
+
const SampleDatabaseState = z.object({
|
|
184
|
+
containerName: z.string(),
|
|
185
|
+
image: z.string(),
|
|
186
|
+
port: z.int().positive(),
|
|
187
|
+
databaseId: z.int().nullable()
|
|
188
|
+
});
|
|
169
189
|
const GitRemoteState = z.discriminatedUnion("kind", [z.object({
|
|
170
190
|
kind: z.literal("local-bare"),
|
|
171
191
|
barePath: z.string(),
|
|
@@ -182,15 +202,17 @@ const StepRecord = z.object({
|
|
|
182
202
|
completedAt: z.string()
|
|
183
203
|
});
|
|
184
204
|
const InstanceState = z.object({
|
|
185
|
-
schemaVersion: z.literal(
|
|
205
|
+
schemaVersion: z.literal(3),
|
|
186
206
|
name: z.string(),
|
|
187
207
|
runtime: RuntimeState.nullable(),
|
|
188
208
|
url: z.string().nullable(),
|
|
209
|
+
version: z.string().nullable(),
|
|
189
210
|
admin: AdminCredentials.nullable(),
|
|
190
211
|
apiKey: z.string().nullable(),
|
|
191
212
|
mbProfile: z.string(),
|
|
192
213
|
license: LicenseState,
|
|
193
214
|
databases: z.array(DatabaseRecord),
|
|
215
|
+
sampleDatabase: SampleDatabaseState.nullable(),
|
|
194
216
|
gitRemote: GitRemoteState.nullable(),
|
|
195
217
|
steps: z.partialRecord(StepId, StepRecord)
|
|
196
218
|
});
|
|
@@ -214,15 +236,17 @@ function mbCommand(profile) {
|
|
|
214
236
|
}
|
|
215
237
|
function emptyInstanceState(name) {
|
|
216
238
|
return {
|
|
217
|
-
schemaVersion:
|
|
239
|
+
schemaVersion: 3,
|
|
218
240
|
name,
|
|
219
241
|
runtime: null,
|
|
220
242
|
url: null,
|
|
243
|
+
version: null,
|
|
221
244
|
admin: null,
|
|
222
245
|
apiKey: null,
|
|
223
246
|
mbProfile: DEFAULT_MB_PROFILE,
|
|
224
247
|
license: { activated: false },
|
|
225
248
|
databases: [],
|
|
249
|
+
sampleDatabase: null,
|
|
226
250
|
gitRemote: null,
|
|
227
251
|
steps: {}
|
|
228
252
|
};
|
|
@@ -230,11 +254,31 @@ function emptyInstanceState(name) {
|
|
|
230
254
|
//#endregion
|
|
231
255
|
//#region src/state/migrate.ts
|
|
232
256
|
const Versioned = z.object({ schemaVersion: z.int() });
|
|
257
|
+
const V1 = 1;
|
|
258
|
+
const V2 = 2;
|
|
259
|
+
const StateV1 = z.object({ schemaVersion: z.literal(V1) }).loose();
|
|
260
|
+
const StateV2 = z.object({ schemaVersion: z.literal(V2) }).loose();
|
|
261
|
+
function liftV1(raw) {
|
|
262
|
+
return {
|
|
263
|
+
...StateV1.parse(raw),
|
|
264
|
+
schemaVersion: V2,
|
|
265
|
+
version: null
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function liftV2(raw) {
|
|
269
|
+
return {
|
|
270
|
+
...StateV2.parse(raw),
|
|
271
|
+
schemaVersion: 3,
|
|
272
|
+
sampleDatabase: null
|
|
273
|
+
};
|
|
274
|
+
}
|
|
233
275
|
function migrateInstanceState(raw) {
|
|
234
276
|
const { schemaVersion } = Versioned.parse(raw);
|
|
235
277
|
switch (schemaVersion) {
|
|
236
|
-
case
|
|
237
|
-
|
|
278
|
+
case V1: return migrateInstanceState(liftV1(raw));
|
|
279
|
+
case V2: return migrateInstanceState(liftV2(raw));
|
|
280
|
+
case 3: return InstanceState.parse(raw);
|
|
281
|
+
default: throw new ConfigError(`state schemaVersion ${schemaVersion} is not supported by this build (expected 3)`);
|
|
238
282
|
}
|
|
239
283
|
}
|
|
240
284
|
//#endregion
|
|
@@ -244,13 +288,23 @@ function readEnv(name) {
|
|
|
244
288
|
return process.env[name];
|
|
245
289
|
}
|
|
246
290
|
const ENV_PATH = "PATH";
|
|
291
|
+
const ENV_JAVA_HOME = "JAVA_HOME";
|
|
292
|
+
function environmentWith(overrides) {
|
|
293
|
+
return {
|
|
294
|
+
...process.env,
|
|
295
|
+
...overrides
|
|
296
|
+
};
|
|
297
|
+
}
|
|
247
298
|
//#endregion
|
|
248
299
|
//#region src/state/paths.ts
|
|
249
300
|
const RDE_DIR_NAME = ".rde";
|
|
250
301
|
const INSTANCES_DIR = "instances";
|
|
251
302
|
const DATA_DIR = "data";
|
|
303
|
+
const BACKUPS_DIR = "backups";
|
|
252
304
|
const STATE_FILE = "state.json";
|
|
253
305
|
const GLOBAL_CONFIG_FILE = "config.json";
|
|
306
|
+
const JARS_DIR = "jars";
|
|
307
|
+
const JAR_FILE$1 = "metabase.jar";
|
|
254
308
|
function rdeHome() {
|
|
255
309
|
const override = readEnv(ENV_RDE_HOME);
|
|
256
310
|
if (override !== void 0) return override;
|
|
@@ -265,12 +319,24 @@ function instanceDir(name) {
|
|
|
265
319
|
function instanceDataDir(name) {
|
|
266
320
|
return join(instanceDir(name), DATA_DIR);
|
|
267
321
|
}
|
|
322
|
+
function instanceBackupsDir(name) {
|
|
323
|
+
return join(instanceDir(name), BACKUPS_DIR);
|
|
324
|
+
}
|
|
268
325
|
function instanceStatePath(name) {
|
|
269
326
|
return join(instanceDir(name), STATE_FILE);
|
|
270
327
|
}
|
|
271
328
|
function globalConfigPath() {
|
|
272
329
|
return join(rdeHome(), GLOBAL_CONFIG_FILE);
|
|
273
330
|
}
|
|
331
|
+
function jarsDir() {
|
|
332
|
+
return join(rdeHome(), JARS_DIR);
|
|
333
|
+
}
|
|
334
|
+
function jarReleaseDir(version) {
|
|
335
|
+
return join(jarsDir(), version);
|
|
336
|
+
}
|
|
337
|
+
function releaseJarIn(jarsDirectory, version) {
|
|
338
|
+
return join(jarsDirectory, version, JAR_FILE$1);
|
|
339
|
+
}
|
|
274
340
|
//#endregion
|
|
275
341
|
//#region src/state/store.ts
|
|
276
342
|
const FILE_MODE$1 = 384;
|
|
@@ -561,6 +627,8 @@ const Initializing = z.object({
|
|
|
561
627
|
//#endregion
|
|
562
628
|
//#region src/metabase/http.ts
|
|
563
629
|
const REQUEST_TIMEOUT_MS = 3e4;
|
|
630
|
+
const PARTIAL_SUFFIX = ".part";
|
|
631
|
+
const CONTENT_LENGTH_HEADER = "content-length";
|
|
564
632
|
const SESSION_HEADER = "X-Metabase-Session";
|
|
565
633
|
const API_KEY_HEADER = "X-Api-Key";
|
|
566
634
|
const JSON_CONTENT_TYPE = "application/json";
|
|
@@ -686,6 +754,47 @@ function createHttp(options) {
|
|
|
686
754
|
}
|
|
687
755
|
};
|
|
688
756
|
}
|
|
757
|
+
function contentLength(response) {
|
|
758
|
+
const header = response.headers.get(CONTENT_LENGTH_HEADER);
|
|
759
|
+
if (header === null) return null;
|
|
760
|
+
const total = Number.parseInt(header, 10);
|
|
761
|
+
return Number.isNaN(total) ? null : total;
|
|
762
|
+
}
|
|
763
|
+
function progressCounter(total, onProgress) {
|
|
764
|
+
let received = 0;
|
|
765
|
+
return (chunk) => {
|
|
766
|
+
received += chunk.byteLength;
|
|
767
|
+
onProgress({
|
|
768
|
+
received,
|
|
769
|
+
total
|
|
770
|
+
});
|
|
771
|
+
return chunk;
|
|
772
|
+
};
|
|
773
|
+
}
|
|
774
|
+
async function downloadFile(url, destination, options) {
|
|
775
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
776
|
+
let response;
|
|
777
|
+
try {
|
|
778
|
+
response = await fetchImpl(url, { signal: options.signal });
|
|
779
|
+
} catch (error) {
|
|
780
|
+
if (options.signal.aborted) throw new AbortError();
|
|
781
|
+
throw new DownloadError(url, describeCause(error));
|
|
782
|
+
}
|
|
783
|
+
if (!response.ok) throw new DownloadError(url, `HTTP ${response.status}`);
|
|
784
|
+
if (response.body === null) throw new DownloadError(url, "empty response");
|
|
785
|
+
const partial = `${destination}${PARTIAL_SUFFIX}`;
|
|
786
|
+
const count = progressCounter(contentLength(response), options.onProgress);
|
|
787
|
+
try {
|
|
788
|
+
await pipeline(Readable.fromWeb(response.body), async function* (chunks) {
|
|
789
|
+
for await (const chunk of chunks) yield count(chunk);
|
|
790
|
+
}, createWriteStream(partial), { signal: options.signal });
|
|
791
|
+
} catch (error) {
|
|
792
|
+
await promises.rm(partial, { force: true });
|
|
793
|
+
if (options.signal.aborted) throw new AbortError();
|
|
794
|
+
throw new DownloadError(url, describeCause(error));
|
|
795
|
+
}
|
|
796
|
+
await promises.rename(partial, destination);
|
|
797
|
+
}
|
|
689
798
|
//#endregion
|
|
690
799
|
//#region src/metabase/domain/permissions-group.ts
|
|
691
800
|
const PermissionsGroup = z.object({
|
|
@@ -914,12 +1023,20 @@ function sessionResource(http) {
|
|
|
914
1023
|
//#endregion
|
|
915
1024
|
//#region src/metabase/resources/setting.ts
|
|
916
1025
|
const PREMIUM_EMBEDDING_TOKEN_SETTING = "premium-embedding-token";
|
|
1026
|
+
const TRANSFORMS_ENABLED_SETTING = "transforms-enabled";
|
|
917
1027
|
function settingResource(http) {
|
|
918
1028
|
/** PUT /api/setting/premium-embedding-token */
|
|
919
1029
|
async function setPremiumEmbeddingToken(value) {
|
|
920
1030
|
await http.requestNoContent("PUT", `/api/setting/${PREMIUM_EMBEDDING_TOKEN_SETTING}`, { body: { value } });
|
|
921
1031
|
}
|
|
922
|
-
|
|
1032
|
+
/** PUT /api/setting/transforms-enabled */
|
|
1033
|
+
async function enableTransforms() {
|
|
1034
|
+
await http.requestNoContent("PUT", `/api/setting/${TRANSFORMS_ENABLED_SETTING}`, { body: { value: true } });
|
|
1035
|
+
}
|
|
1036
|
+
return {
|
|
1037
|
+
setPremiumEmbeddingToken,
|
|
1038
|
+
enableTransforms
|
|
1039
|
+
};
|
|
923
1040
|
}
|
|
924
1041
|
//#endregion
|
|
925
1042
|
//#region src/metabase/domain/setup.ts
|
|
@@ -2085,7 +2202,8 @@ async function runDatabaseFlow(deps) {
|
|
|
2085
2202
|
}
|
|
2086
2203
|
//#endregion
|
|
2087
2204
|
//#region src/steps/databases.ts
|
|
2088
|
-
const STEP_ID$
|
|
2205
|
+
const STEP_ID$9 = "databases";
|
|
2206
|
+
const ASK_DATABASES = { requested: false };
|
|
2089
2207
|
function hostDatabasesDeps(signal) {
|
|
2090
2208
|
return {
|
|
2091
2209
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
@@ -2093,10 +2211,10 @@ function hostDatabasesDeps(signal) {
|
|
|
2093
2211
|
now: () => /* @__PURE__ */ new Date()
|
|
2094
2212
|
};
|
|
2095
2213
|
}
|
|
2096
|
-
function isNotServingYet(error) {
|
|
2214
|
+
function isNotServingYet$1(error) {
|
|
2097
2215
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
2098
2216
|
}
|
|
2099
|
-
function requireApiKey$
|
|
2217
|
+
function requireApiKey$2(state) {
|
|
2100
2218
|
if (state.apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
2101
2219
|
return state.apiKey;
|
|
2102
2220
|
}
|
|
@@ -2106,7 +2224,7 @@ function hostAliasOf(runtime) {
|
|
|
2106
2224
|
case "jar": return null;
|
|
2107
2225
|
}
|
|
2108
2226
|
}
|
|
2109
|
-
function toRecord(database, createdAt) {
|
|
2227
|
+
function toRecord$1(database, createdAt) {
|
|
2110
2228
|
return {
|
|
2111
2229
|
id: database.id,
|
|
2112
2230
|
name: database.name,
|
|
@@ -2118,7 +2236,7 @@ function authenticatedClient(state, deps) {
|
|
|
2118
2236
|
const configured = requireConfiguredRuntime(state);
|
|
2119
2237
|
return deps.client(configured.url, {
|
|
2120
2238
|
kind: "api-key",
|
|
2121
|
-
key: requireApiKey$
|
|
2239
|
+
key: requireApiKey$2(state)
|
|
2122
2240
|
});
|
|
2123
2241
|
}
|
|
2124
2242
|
async function listDatabases(state, deps) {
|
|
@@ -2156,7 +2274,7 @@ async function addDatabases(ctx, deps) {
|
|
|
2156
2274
|
existingCount: existing.data.length,
|
|
2157
2275
|
readTextFile: deps.readTextFile,
|
|
2158
2276
|
onCreated: async (database) => {
|
|
2159
|
-
const record = toRecord(database, deps.now().toISOString());
|
|
2277
|
+
const record = toRecord$1(database, deps.now().toISOString());
|
|
2160
2278
|
await ctx.saveState({
|
|
2161
2279
|
...ctx.state,
|
|
2162
2280
|
databases: [...ctx.state.databases, record]
|
|
@@ -2165,26 +2283,33 @@ async function addDatabases(ctx, deps) {
|
|
|
2165
2283
|
});
|
|
2166
2284
|
return existing.data.length + result.created.length > 0 ? "completed" : "skipped";
|
|
2167
2285
|
}
|
|
2168
|
-
function hostDeps$
|
|
2286
|
+
function hostDeps$12(ctx) {
|
|
2169
2287
|
return hostDatabasesDeps(ctx.signal);
|
|
2170
2288
|
}
|
|
2171
|
-
function
|
|
2289
|
+
function sampleDatabaseId(state) {
|
|
2290
|
+
return state.sampleDatabase === null ? null : state.sampleDatabase.databaseId;
|
|
2291
|
+
}
|
|
2292
|
+
function ownDatabaseIds(state) {
|
|
2293
|
+
const sampleId = sampleDatabaseId(state);
|
|
2294
|
+
return new Set(state.databases.filter((record) => record.id !== sampleId).map((record) => record.id));
|
|
2295
|
+
}
|
|
2296
|
+
function databasesStep(options, deps = hostDeps$12) {
|
|
2172
2297
|
return {
|
|
2173
|
-
id: STEP_ID$
|
|
2298
|
+
id: STEP_ID$9,
|
|
2174
2299
|
title: "Connect your databases",
|
|
2175
2300
|
async detect(ctx) {
|
|
2176
2301
|
const configured = configuredRuntime(ctx.state);
|
|
2177
|
-
if (configured === null || ctx.state.apiKey === null
|
|
2302
|
+
if (configured === null || ctx.state.apiKey === null) return "todo";
|
|
2303
|
+
const own = ownDatabaseIds(ctx.state);
|
|
2304
|
+
if (own.size === 0) return sampleDatabaseId(ctx.state) !== null && !options.requested ? "done" : "todo";
|
|
2178
2305
|
const client = deps(ctx).client(configured.url, {
|
|
2179
2306
|
kind: "api-key",
|
|
2180
2307
|
key: ctx.state.apiKey
|
|
2181
2308
|
});
|
|
2182
2309
|
try {
|
|
2183
|
-
|
|
2184
|
-
const known = new Set(ctx.state.databases.map((record) => record.id));
|
|
2185
|
-
return listed.data.some((database) => known.has(database.id)) ? "done" : "todo";
|
|
2310
|
+
return (await client.database.list()).data.some((database) => own.has(database.id)) ? "done" : "todo";
|
|
2186
2311
|
} catch (error) {
|
|
2187
|
-
if (isNotServingYet(error)) return "todo";
|
|
2312
|
+
if (isNotServingYet$1(error)) return "todo";
|
|
2188
2313
|
throw error;
|
|
2189
2314
|
}
|
|
2190
2315
|
},
|
|
@@ -2331,7 +2456,7 @@ const dbAdd = defineRdeCommand({
|
|
|
2331
2456
|
const name = await defaultInstanceName();
|
|
2332
2457
|
await requireInstanceState(name);
|
|
2333
2458
|
const ctx = await createWizardContext(name, { signal: interruptSignal });
|
|
2334
|
-
await databasesStep().run(ctx);
|
|
2459
|
+
await databasesStep(ASK_DATABASES).run(ctx);
|
|
2335
2460
|
}
|
|
2336
2461
|
});
|
|
2337
2462
|
//#endregion
|
|
@@ -2376,6 +2501,43 @@ const db = defineCommand({
|
|
|
2376
2501
|
}
|
|
2377
2502
|
});
|
|
2378
2503
|
//#endregion
|
|
2504
|
+
//#region src/metabase/domain/version-info.ts
|
|
2505
|
+
const VersionInfo = z.object({ latest: z.object({ version: z.string() }).loose() }).loose();
|
|
2506
|
+
//#endregion
|
|
2507
|
+
//#region src/metabase/releases.ts
|
|
2508
|
+
const DOWNLOADS_HOST = "https://downloads.metabase.com";
|
|
2509
|
+
const VERSION_INFO_HOST = "https://static.metabase.com";
|
|
2510
|
+
const VERSION_INFO_PATH = "/version-info-ee.json";
|
|
2511
|
+
const JAR_FILE = "metabase.jar";
|
|
2512
|
+
const RELEASE_TAG$1 = /^v(?<edition>[01])\.\d+\.\d+(?:\.\d+)?(?:-[\w.]+)?$/;
|
|
2513
|
+
const ENTERPRISE_EDITION = "1";
|
|
2514
|
+
function isReleaseTag(value) {
|
|
2515
|
+
return RELEASE_TAG$1.test(value);
|
|
2516
|
+
}
|
|
2517
|
+
function jarDownloadUrl(version) {
|
|
2518
|
+
const edition = RELEASE_TAG$1.exec(version)?.groups?.["edition"];
|
|
2519
|
+
if (edition === void 0) throw new Error(`${version} is not a Metabase release tag`);
|
|
2520
|
+
return `${DOWNLOADS_HOST}${edition === ENTERPRISE_EDITION ? "/enterprise" : ""}/${version}/${JAR_FILE}`;
|
|
2521
|
+
}
|
|
2522
|
+
function releaseLookup(signal, options = {}) {
|
|
2523
|
+
const http = createHttp({
|
|
2524
|
+
baseUrl: VERSION_INFO_HOST,
|
|
2525
|
+
auth: NO_AUTH,
|
|
2526
|
+
signal,
|
|
2527
|
+
...options
|
|
2528
|
+
});
|
|
2529
|
+
return { async latestVersion() {
|
|
2530
|
+
return (await http.requestJson("GET", VERSION_INFO_PATH, { schema: VersionInfo })).latest.version;
|
|
2531
|
+
} };
|
|
2532
|
+
}
|
|
2533
|
+
function fileDownloader(signal, options = {}) {
|
|
2534
|
+
return (url, destination, onProgress) => downloadFile(url, destination, {
|
|
2535
|
+
signal,
|
|
2536
|
+
onProgress,
|
|
2537
|
+
...options
|
|
2538
|
+
});
|
|
2539
|
+
}
|
|
2540
|
+
//#endregion
|
|
2379
2541
|
//#region src/tools/process.ts
|
|
2380
2542
|
var ProcessNotFoundError = class extends Error {
|
|
2381
2543
|
command;
|
|
@@ -2531,6 +2693,49 @@ function stream(signal, command, args, options) {
|
|
|
2531
2693
|
});
|
|
2532
2694
|
});
|
|
2533
2695
|
}
|
|
2696
|
+
async function spawnDetached(command, args, options) {
|
|
2697
|
+
const log = await promises.open(options.logPath, "a");
|
|
2698
|
+
try {
|
|
2699
|
+
return await new Promise((resolve, reject) => {
|
|
2700
|
+
const child = spawn(command, args, {
|
|
2701
|
+
stdio: [
|
|
2702
|
+
"ignore",
|
|
2703
|
+
log.fd,
|
|
2704
|
+
log.fd
|
|
2705
|
+
],
|
|
2706
|
+
cwd: options.cwd,
|
|
2707
|
+
env: environmentWith(options.env),
|
|
2708
|
+
detached: true
|
|
2709
|
+
});
|
|
2710
|
+
child.on("error", (error) => {
|
|
2711
|
+
reject(isFileNotFound(error) ? new ProcessNotFoundError(command) : error);
|
|
2712
|
+
});
|
|
2713
|
+
child.on("spawn", () => {
|
|
2714
|
+
child.unref();
|
|
2715
|
+
if (child.pid === void 0) {
|
|
2716
|
+
reject(/* @__PURE__ */ new Error(`${command} spawned without a pid`));
|
|
2717
|
+
return;
|
|
2718
|
+
}
|
|
2719
|
+
resolve(child.pid);
|
|
2720
|
+
});
|
|
2721
|
+
});
|
|
2722
|
+
} finally {
|
|
2723
|
+
await log.close();
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
const GONE_PROCESS_CODES = /* @__PURE__ */ new Set(["ESRCH", "EPERM"]);
|
|
2727
|
+
function isGoneProcess(error) {
|
|
2728
|
+
return error instanceof Error && "code" in error && typeof error.code === "string" && GONE_PROCESS_CODES.has(error.code);
|
|
2729
|
+
}
|
|
2730
|
+
function signalProcess(pid, signal) {
|
|
2731
|
+
try {
|
|
2732
|
+
process.kill(pid, signal);
|
|
2733
|
+
return true;
|
|
2734
|
+
} catch (error) {
|
|
2735
|
+
if (isGoneProcess(error)) return false;
|
|
2736
|
+
throw error;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2534
2739
|
function stderrTail(stderr) {
|
|
2535
2740
|
return stderr.trim().split("\n").slice(-5).join("\n");
|
|
2536
2741
|
}
|
|
@@ -2615,15 +2820,71 @@ async function whichCommand(name) {
|
|
|
2615
2820
|
return null;
|
|
2616
2821
|
}
|
|
2617
2822
|
//#endregion
|
|
2823
|
+
//#region src/tools/java.ts
|
|
2824
|
+
const JAVA = "java";
|
|
2825
|
+
const JAVA_INSTALL_HINT = `install a Java 21+ runtime (Temurin from https://adoptium.net, or your package manager) or point JAVA_HOME at one`;
|
|
2826
|
+
const VERSION_LINE = /version "(?<version>[^"]+)"/;
|
|
2827
|
+
const LEGACY_MAJOR = 1;
|
|
2828
|
+
function javaMajor(version) {
|
|
2829
|
+
const [first, second] = version.split(".").map((part) => Number.parseInt(part, 10));
|
|
2830
|
+
if (first === void 0 || Number.isNaN(first)) return null;
|
|
2831
|
+
if (first === LEGACY_MAJOR && second !== void 0 && !Number.isNaN(second)) return second;
|
|
2832
|
+
return first;
|
|
2833
|
+
}
|
|
2834
|
+
async function javaPath() {
|
|
2835
|
+
const home = readEnv(ENV_JAVA_HOME);
|
|
2836
|
+
if (home !== void 0) {
|
|
2837
|
+
const candidate = join(home, "bin", JAVA);
|
|
2838
|
+
if (await isExecutable(candidate)) return candidate;
|
|
2839
|
+
}
|
|
2840
|
+
return whichCommand(JAVA);
|
|
2841
|
+
}
|
|
2842
|
+
function javaTool(runner) {
|
|
2843
|
+
async function find() {
|
|
2844
|
+
const path = await javaPath();
|
|
2845
|
+
if (path === null) return {
|
|
2846
|
+
found: false,
|
|
2847
|
+
reason: "java is not installed"
|
|
2848
|
+
};
|
|
2849
|
+
let output;
|
|
2850
|
+
try {
|
|
2851
|
+
const result = await runner.run(path, ["-version"]);
|
|
2852
|
+
if (result.exitCode !== 0) return {
|
|
2853
|
+
found: false,
|
|
2854
|
+
reason: `${path} -version failed: ${stderrTail(result.stderr)}`
|
|
2855
|
+
};
|
|
2856
|
+
output = result.stderr;
|
|
2857
|
+
} catch (error) {
|
|
2858
|
+
if (error instanceof ProcessNotFoundError) return {
|
|
2859
|
+
found: false,
|
|
2860
|
+
reason: "java is not installed"
|
|
2861
|
+
};
|
|
2862
|
+
throw error;
|
|
2863
|
+
}
|
|
2864
|
+
const version = VERSION_LINE.exec(output)?.groups?.["version"];
|
|
2865
|
+
if (version === void 0) return {
|
|
2866
|
+
found: false,
|
|
2867
|
+
reason: `${path} -version printed no version`
|
|
2868
|
+
};
|
|
2869
|
+
const major = javaMajor(version);
|
|
2870
|
+
if (major === null) return {
|
|
2871
|
+
found: false,
|
|
2872
|
+
reason: `${path} reports an unreadable version ${version}`
|
|
2873
|
+
};
|
|
2874
|
+
return {
|
|
2875
|
+
found: true,
|
|
2876
|
+
path,
|
|
2877
|
+
version,
|
|
2878
|
+
major
|
|
2879
|
+
};
|
|
2880
|
+
}
|
|
2881
|
+
return { find };
|
|
2882
|
+
}
|
|
2883
|
+
//#endregion
|
|
2618
2884
|
//#region src/tools/docker.ts
|
|
2619
2885
|
const DOCKER = "docker";
|
|
2620
2886
|
const CONTAINER_DATA_DIR = "/rde";
|
|
2621
2887
|
const LOOPBACK_HOST = "127.0.0.1";
|
|
2622
|
-
const CONTAINER_PORT$1 = 3e3;
|
|
2623
|
-
const ENV_DB_FILE = `MB_DB_FILE=${CONTAINER_DATA_DIR}/metabase.db`;
|
|
2624
|
-
const ENV_PLUGINS_DIR = `MB_PLUGINS_DIR=${CONTAINER_DATA_DIR}/plugins`;
|
|
2625
|
-
const STAGING_STORE_ENV = ["MB_RUN_MODE=e2e", "MB_STORE_USE_STAGING=true"];
|
|
2626
|
-
const ALLOW_LOCAL_WAREHOUSES_ENV = "MB_WAREHOUSE_ALLOWED_NETWORKS=allow-all";
|
|
2627
2888
|
const ENV_HOST_UID = "MUID";
|
|
2628
2889
|
const ENV_HOST_GID = "MGID";
|
|
2629
2890
|
const ADD_HOST_GATEWAY = `--add-host=${DOCKER_HOST_ALIAS}:host-gateway`;
|
|
@@ -2659,17 +2920,10 @@ function createArgs(options) {
|
|
|
2659
2920
|
"--name",
|
|
2660
2921
|
options.name,
|
|
2661
2922
|
"-p",
|
|
2662
|
-
`${LOOPBACK_HOST}:${options.port}:${
|
|
2663
|
-
"-v",
|
|
2664
|
-
`${options.dataDir}:${CONTAINER_DATA_DIR}`,
|
|
2665
|
-
"-e",
|
|
2666
|
-
ENV_DB_FILE,
|
|
2667
|
-
"-e",
|
|
2668
|
-
ENV_PLUGINS_DIR,
|
|
2669
|
-
"-e",
|
|
2670
|
-
`MB_SITE_URL=http://localhost:${options.port}`
|
|
2923
|
+
`${LOOPBACK_HOST}:${options.port.host}:${options.port.container}`
|
|
2671
2924
|
];
|
|
2672
|
-
|
|
2925
|
+
if (options.mount !== null) args.push("-v", `${options.mount.source}:${options.mount.target}`);
|
|
2926
|
+
for (const [name, value] of Object.entries(options.env)) args.push("-e", `${name}=${value}`);
|
|
2673
2927
|
if (options.hostUser !== null) {
|
|
2674
2928
|
args.push("-e", `${ENV_HOST_UID}=${options.hostUser.uid}`);
|
|
2675
2929
|
args.push("-e", `${ENV_HOST_GID}=${options.hostUser.gid}`);
|
|
@@ -2714,7 +2968,7 @@ function dockerTool(runner) {
|
|
|
2714
2968
|
const result = await runner.run(DOCKER, args, { onLine: options.onLine });
|
|
2715
2969
|
if (result.exitCode !== 0) throw toolFailure(DOCKER, args, result);
|
|
2716
2970
|
}
|
|
2717
|
-
async function
|
|
2971
|
+
async function imageId(image) {
|
|
2718
2972
|
const args = [
|
|
2719
2973
|
"image",
|
|
2720
2974
|
"inspect",
|
|
@@ -2723,8 +2977,8 @@ function dockerTool(runner) {
|
|
|
2723
2977
|
"{{.Id}}"
|
|
2724
2978
|
];
|
|
2725
2979
|
const result = await runner.run(DOCKER, args);
|
|
2726
|
-
if (result.exitCode === 0) return
|
|
2727
|
-
if (NO_SUCH_IMAGE.test(result.stderr)) return
|
|
2980
|
+
if (result.exitCode === 0) return result.stdout.trim();
|
|
2981
|
+
if (NO_SUCH_IMAGE.test(result.stderr)) return null;
|
|
2728
2982
|
throw toolFailure(DOCKER, args, result);
|
|
2729
2983
|
}
|
|
2730
2984
|
async function imageRemove(image) {
|
|
@@ -2746,6 +3000,14 @@ function dockerTool(runner) {
|
|
|
2746
3000
|
ports: portMappings(container.HostConfig.PortBindings)
|
|
2747
3001
|
};
|
|
2748
3002
|
}
|
|
3003
|
+
async function containerImageId(name) {
|
|
3004
|
+
return (await docker([
|
|
3005
|
+
"inspect",
|
|
3006
|
+
name,
|
|
3007
|
+
"--format",
|
|
3008
|
+
"{{.Image}}"
|
|
3009
|
+
])).stdout.trim();
|
|
3010
|
+
}
|
|
2749
3011
|
async function containerCreate(options) {
|
|
2750
3012
|
await docker(createArgs(options));
|
|
2751
3013
|
}
|
|
@@ -2758,6 +3020,13 @@ function dockerTool(runner) {
|
|
|
2758
3020
|
async function containerRemove(name) {
|
|
2759
3021
|
await docker(["rm", name]);
|
|
2760
3022
|
}
|
|
3023
|
+
async function containerExecSucceeds(name, command) {
|
|
3024
|
+
return (await runner.run(DOCKER, [
|
|
3025
|
+
"exec",
|
|
3026
|
+
name,
|
|
3027
|
+
...command
|
|
3028
|
+
])).exitCode === 0;
|
|
3029
|
+
}
|
|
2761
3030
|
async function containerLogs(name, options) {
|
|
2762
3031
|
const args = ["logs"];
|
|
2763
3032
|
if (options.follow) args.push("--follow");
|
|
@@ -2768,17 +3037,213 @@ function dockerTool(runner) {
|
|
|
2768
3037
|
return {
|
|
2769
3038
|
available,
|
|
2770
3039
|
imagePull,
|
|
2771
|
-
|
|
3040
|
+
imageId,
|
|
2772
3041
|
imageRemove,
|
|
2773
3042
|
containerInspect,
|
|
3043
|
+
containerImageId,
|
|
2774
3044
|
containerCreate,
|
|
2775
3045
|
containerStart,
|
|
2776
3046
|
containerStop,
|
|
2777
3047
|
containerRemove,
|
|
3048
|
+
containerExecSucceeds,
|
|
2778
3049
|
containerLogs
|
|
2779
3050
|
};
|
|
2780
3051
|
}
|
|
2781
3052
|
//#endregion
|
|
3053
|
+
//#region src/runtime/server.ts
|
|
3054
|
+
const DEFAULT_PORT = 3100;
|
|
3055
|
+
const PLUGINS_DIR = "plugins";
|
|
3056
|
+
const DB_DIR = "metabase.db";
|
|
3057
|
+
const DB_FILE = "metabase.db";
|
|
3058
|
+
const STAGING_STORE_ENV = {
|
|
3059
|
+
MB_RUN_MODE: "e2e",
|
|
3060
|
+
MB_STORE_USE_STAGING: "true"
|
|
3061
|
+
};
|
|
3062
|
+
const ALLOW_LOCAL_WAREHOUSES_ENV = { MB_WAREHOUSE_ALLOWED_NETWORKS: "allow-all" };
|
|
3063
|
+
const NO_H2_SAMPLE_ENV = { MB_LOAD_SAMPLE_CONTENT: "false" };
|
|
3064
|
+
function instanceUrl(port) {
|
|
3065
|
+
return `http://localhost:${port}`;
|
|
3066
|
+
}
|
|
3067
|
+
function serverEnv(options) {
|
|
3068
|
+
return {
|
|
3069
|
+
MB_DB_FILE: options.dbFile,
|
|
3070
|
+
MB_PLUGINS_DIR: options.pluginsDir,
|
|
3071
|
+
MB_SITE_URL: instanceUrl(options.port),
|
|
3072
|
+
...STAGING_STORE_ENV,
|
|
3073
|
+
...ALLOW_LOCAL_WAREHOUSES_ENV,
|
|
3074
|
+
...NO_H2_SAMPLE_ENV
|
|
3075
|
+
};
|
|
3076
|
+
}
|
|
3077
|
+
function jettyEnv(port) {
|
|
3078
|
+
return {
|
|
3079
|
+
MB_JETTY_HOST: LOOPBACK_HOST,
|
|
3080
|
+
MB_JETTY_PORT: String(port)
|
|
3081
|
+
};
|
|
3082
|
+
}
|
|
3083
|
+
//#endregion
|
|
3084
|
+
//#region src/runtime/jar.ts
|
|
3085
|
+
const LATEST = "latest";
|
|
3086
|
+
const JAR_SUFFIX = ".jar";
|
|
3087
|
+
const LOG_FILE = "metabase.log";
|
|
3088
|
+
const PID_FILE = "metabase.pid";
|
|
3089
|
+
const PID_FILE_MODE = 384;
|
|
3090
|
+
const JAR_STOP_GRACE_MS = 3e4;
|
|
3091
|
+
const BYTES_PER_MB = 1048576;
|
|
3092
|
+
const TAG_PREFIX = "v";
|
|
3093
|
+
function parseJarSpec(value) {
|
|
3094
|
+
const trimmed = value.trim();
|
|
3095
|
+
if (trimmed === "latest") return { kind: "latest" };
|
|
3096
|
+
if (trimmed.endsWith(JAR_SUFFIX)) return {
|
|
3097
|
+
kind: "file",
|
|
3098
|
+
path: resolve(trimmed)
|
|
3099
|
+
};
|
|
3100
|
+
const tagged = trimmed.startsWith(TAG_PREFIX) ? trimmed : `${TAG_PREFIX}${trimmed}`;
|
|
3101
|
+
return isReleaseTag(tagged) ? {
|
|
3102
|
+
kind: "release",
|
|
3103
|
+
version: tagged
|
|
3104
|
+
} : null;
|
|
3105
|
+
}
|
|
3106
|
+
function jarPathOf(source, jarsDir) {
|
|
3107
|
+
switch (source.kind) {
|
|
3108
|
+
case "release": return releaseJarIn(jarsDir, source.version);
|
|
3109
|
+
case "file": return source.path;
|
|
3110
|
+
}
|
|
3111
|
+
}
|
|
3112
|
+
function describeJar(source) {
|
|
3113
|
+
switch (source.kind) {
|
|
3114
|
+
case "release": return `Metabase ${source.version} jar`;
|
|
3115
|
+
case "file": return `jar ${source.path}`;
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
function logPathOf(instanceDir) {
|
|
3119
|
+
return join(instanceDir, LOG_FILE);
|
|
3120
|
+
}
|
|
3121
|
+
function pidPathOf(instanceDir) {
|
|
3122
|
+
return join(instanceDir, PID_FILE);
|
|
3123
|
+
}
|
|
3124
|
+
async function pathExists$1(path) {
|
|
3125
|
+
try {
|
|
3126
|
+
await promises.access(path);
|
|
3127
|
+
return true;
|
|
3128
|
+
} catch {
|
|
3129
|
+
return false;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
async function requireJava(java) {
|
|
3133
|
+
const found = await java.find();
|
|
3134
|
+
if (!found.found) throw new ConfigError(`${found.reason}; ${JAVA_INSTALL_HINT}`);
|
|
3135
|
+
if (found.major < 21) throw new ConfigError(`Java ${found.version} at ${found.path} is too old: Metabase needs 21 or newer; ${JAVA_INSTALL_HINT}`);
|
|
3136
|
+
return found;
|
|
3137
|
+
}
|
|
3138
|
+
function megabytes(bytes) {
|
|
3139
|
+
return (bytes / BYTES_PER_MB).toFixed(0);
|
|
3140
|
+
}
|
|
3141
|
+
function progressLine(progress) {
|
|
3142
|
+
if (progress.total === null) return `${megabytes(progress.received)} MB`;
|
|
3143
|
+
return `${megabytes(progress.received)} of ${megabytes(progress.total)} MB`;
|
|
3144
|
+
}
|
|
3145
|
+
async function readPid(pidPath) {
|
|
3146
|
+
let text;
|
|
3147
|
+
try {
|
|
3148
|
+
text = await promises.readFile(pidPath, "utf8");
|
|
3149
|
+
} catch (error) {
|
|
3150
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") return null;
|
|
3151
|
+
throw error;
|
|
3152
|
+
}
|
|
3153
|
+
const pid = Number.parseInt(text.trim(), 10);
|
|
3154
|
+
if (Number.isNaN(pid) || pid <= 0) throw new ConfigError(`${pidPath} does not hold a pid; delete it and run \`rde start\``);
|
|
3155
|
+
return pid;
|
|
3156
|
+
}
|
|
3157
|
+
async function runningJarPid(instanceDir, jarPath, daemon) {
|
|
3158
|
+
const pid = await readPid(pidPathOf(instanceDir));
|
|
3159
|
+
if (pid === null) return null;
|
|
3160
|
+
const commandLine = await daemon.commandLine(pid);
|
|
3161
|
+
return commandLine !== null && commandLine.includes(jarPath) ? pid : null;
|
|
3162
|
+
}
|
|
3163
|
+
async function inspectJar(state, deps) {
|
|
3164
|
+
const jarPath = jarPathOf(state.source, deps.jarsDir);
|
|
3165
|
+
return {
|
|
3166
|
+
jarPath,
|
|
3167
|
+
jarExists: await pathExists$1(jarPath),
|
|
3168
|
+
pid: await runningJarPid(deps.instanceDir, jarPath, deps.daemon)
|
|
3169
|
+
};
|
|
3170
|
+
}
|
|
3171
|
+
function jarRuntime(state, deps) {
|
|
3172
|
+
const { daemon } = deps;
|
|
3173
|
+
const url = instanceUrl(state.port);
|
|
3174
|
+
const jarPath = jarPathOf(state.source, deps.jarsDir);
|
|
3175
|
+
const logPath = logPathOf(deps.instanceDir);
|
|
3176
|
+
const pidPath = pidPathOf(deps.instanceDir);
|
|
3177
|
+
function runningPid() {
|
|
3178
|
+
return runningJarPid(deps.instanceDir, jarPath, daemon);
|
|
3179
|
+
}
|
|
3180
|
+
async function ensureJar(options) {
|
|
3181
|
+
if (await pathExists$1(jarPath)) return;
|
|
3182
|
+
switch (state.source.kind) {
|
|
3183
|
+
case "file": throw new ConfigError(`${jarPath} does not exist; run \`rde init\` again with another jar`);
|
|
3184
|
+
case "release":
|
|
3185
|
+
await promises.mkdir(dirname(jarPath), { recursive: true });
|
|
3186
|
+
await deps.download(jarDownloadUrl(state.source.version), jarPath, (progress) => options.onProgress(progressLine(progress)));
|
|
3187
|
+
}
|
|
3188
|
+
}
|
|
3189
|
+
async function install(options) {
|
|
3190
|
+
await requireJava(deps.java);
|
|
3191
|
+
await promises.mkdir(join(deps.dataDir, PLUGINS_DIR), { recursive: true });
|
|
3192
|
+
await promises.mkdir(join(deps.dataDir, DB_DIR), { recursive: true });
|
|
3193
|
+
await ensureJar(options);
|
|
3194
|
+
}
|
|
3195
|
+
async function start() {
|
|
3196
|
+
if (await runningPid() !== null) return;
|
|
3197
|
+
if (!await pathExists$1(jarPath)) throw new ConfigError(`${jarPath} does not exist; run \`rde init\``);
|
|
3198
|
+
const java = await requireJava(deps.java);
|
|
3199
|
+
await promises.mkdir(deps.instanceDir, { recursive: true });
|
|
3200
|
+
const pid = await daemon.start({
|
|
3201
|
+
command: java.path,
|
|
3202
|
+
args: ["-jar", jarPath],
|
|
3203
|
+
options: {
|
|
3204
|
+
cwd: deps.dataDir,
|
|
3205
|
+
env: {
|
|
3206
|
+
...serverEnv({
|
|
3207
|
+
dbFile: join(deps.dataDir, DB_DIR, DB_FILE),
|
|
3208
|
+
pluginsDir: join(deps.dataDir, PLUGINS_DIR),
|
|
3209
|
+
port: state.port
|
|
3210
|
+
}),
|
|
3211
|
+
...jettyEnv(state.port)
|
|
3212
|
+
},
|
|
3213
|
+
logPath
|
|
3214
|
+
}
|
|
3215
|
+
});
|
|
3216
|
+
await promises.writeFile(pidPath, `${pid}\n`, { mode: PID_FILE_MODE });
|
|
3217
|
+
}
|
|
3218
|
+
async function stop() {
|
|
3219
|
+
const pid = await runningPid();
|
|
3220
|
+
if (pid !== null) await daemon.terminate(pid, { graceMs: JAR_STOP_GRACE_MS });
|
|
3221
|
+
await promises.rm(pidPath, { force: true });
|
|
3222
|
+
}
|
|
3223
|
+
async function status() {
|
|
3224
|
+
const inspection = await inspectJar(state, deps);
|
|
3225
|
+
return {
|
|
3226
|
+
installed: inspection.jarExists,
|
|
3227
|
+
running: inspection.pid !== null,
|
|
3228
|
+
url
|
|
3229
|
+
};
|
|
3230
|
+
}
|
|
3231
|
+
async function logs(options) {
|
|
3232
|
+
if (!await pathExists$1(logPath)) throw new ConfigError(`${logPath} does not exist yet; run \`rde start\``);
|
|
3233
|
+
await daemon.tailLog(logPath, options);
|
|
3234
|
+
}
|
|
3235
|
+
return {
|
|
3236
|
+
kind: "jar",
|
|
3237
|
+
install,
|
|
3238
|
+
start,
|
|
3239
|
+
stop,
|
|
3240
|
+
status,
|
|
3241
|
+
logs,
|
|
3242
|
+
url: () => url,
|
|
3243
|
+
hostAlias: () => null
|
|
3244
|
+
};
|
|
3245
|
+
}
|
|
3246
|
+
//#endregion
|
|
2782
3247
|
//#region src/runtime/port.ts
|
|
2783
3248
|
const PORT_SCAN_LIMIT = 100;
|
|
2784
3249
|
function canBind(port) {
|
|
@@ -2818,93 +3283,342 @@ const hostPortProbe = {
|
|
|
2818
3283
|
findFree: findFreePort
|
|
2819
3284
|
};
|
|
2820
3285
|
//#endregion
|
|
2821
|
-
//#region src/tools/
|
|
2822
|
-
const
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
|
|
2826
|
-
|
|
2827
|
-
"
|
|
2828
|
-
"
|
|
2829
|
-
|
|
2830
|
-
|
|
2831
|
-
{
|
|
2832
|
-
id: "claude-code",
|
|
2833
|
-
displayName: "Claude Code",
|
|
2834
|
-
skillsCliName: "claude-code",
|
|
2835
|
-
command: "claude",
|
|
2836
|
-
launch: "claude",
|
|
2837
|
-
configDir: (home) => join(home, ".claude")
|
|
2838
|
-
},
|
|
2839
|
-
{
|
|
2840
|
-
id: "codex",
|
|
2841
|
-
displayName: "Codex",
|
|
2842
|
-
skillsCliName: "codex",
|
|
2843
|
-
command: "codex",
|
|
2844
|
-
launch: "codex",
|
|
2845
|
-
configDir: (home) => join(home, ".codex")
|
|
2846
|
-
},
|
|
2847
|
-
{
|
|
2848
|
-
id: "cursor",
|
|
2849
|
-
displayName: "Cursor",
|
|
2850
|
-
skillsCliName: "cursor",
|
|
2851
|
-
command: "cursor",
|
|
2852
|
-
launch: "cursor .",
|
|
2853
|
-
configDir: (home) => join(home, ".cursor")
|
|
2854
|
-
},
|
|
2855
|
-
{
|
|
2856
|
-
id: "gemini-cli",
|
|
2857
|
-
displayName: "Gemini CLI",
|
|
2858
|
-
skillsCliName: "gemini-cli",
|
|
2859
|
-
command: "gemini",
|
|
2860
|
-
launch: "gemini",
|
|
2861
|
-
configDir: (home) => join(home, ".gemini")
|
|
2862
|
-
},
|
|
2863
|
-
{
|
|
2864
|
-
id: "opencode",
|
|
2865
|
-
displayName: "OpenCode",
|
|
2866
|
-
skillsCliName: "opencode",
|
|
2867
|
-
command: "opencode",
|
|
2868
|
-
launch: "opencode",
|
|
2869
|
-
configDir: (_home, configHome) => join(configHome, "opencode")
|
|
2870
|
-
},
|
|
2871
|
-
{
|
|
2872
|
-
id: "windsurf",
|
|
2873
|
-
displayName: "Windsurf",
|
|
2874
|
-
skillsCliName: "windsurf",
|
|
2875
|
-
command: null,
|
|
2876
|
-
launch: "windsurf .",
|
|
2877
|
-
configDir: (home) => join(home, ".codeium", "windsurf")
|
|
2878
|
-
}
|
|
2879
|
-
];
|
|
2880
|
-
async function directoryExists$1(path) {
|
|
2881
|
-
try {
|
|
2882
|
-
return (await promises.stat(path)).isDirectory();
|
|
2883
|
-
} catch {
|
|
2884
|
-
return false;
|
|
2885
|
-
}
|
|
3286
|
+
//#region src/tools/daemon.ts
|
|
3287
|
+
const PS = "ps";
|
|
3288
|
+
const EXIT_POLL_MS = 250;
|
|
3289
|
+
const TAIL = "tail";
|
|
3290
|
+
const WHOLE_FILE = "+1";
|
|
3291
|
+
function tailArgs(path, options) {
|
|
3292
|
+
const args = ["-n", options.tail === null ? WHOLE_FILE : String(options.tail)];
|
|
3293
|
+
if (options.follow) args.push("-f");
|
|
3294
|
+
args.push(path);
|
|
3295
|
+
return args;
|
|
2886
3296
|
}
|
|
2887
|
-
function
|
|
2888
|
-
|
|
3297
|
+
function daemonTool(runner) {
|
|
3298
|
+
async function commandLine(pid) {
|
|
3299
|
+
if (!signalProcess(pid, 0)) return null;
|
|
3300
|
+
const result = await runner.run(PS, [
|
|
3301
|
+
"-o",
|
|
3302
|
+
"command=",
|
|
3303
|
+
"-p",
|
|
3304
|
+
String(pid)
|
|
3305
|
+
]);
|
|
3306
|
+
const line = result.stdout.trim();
|
|
3307
|
+
return result.exitCode === 0 && line.length > 0 ? line : null;
|
|
3308
|
+
}
|
|
3309
|
+
async function terminate(pid, options) {
|
|
3310
|
+
if (!signalProcess(pid, "SIGTERM")) return;
|
|
3311
|
+
const deadline = Date.now() + options.graceMs;
|
|
3312
|
+
while (Date.now() < deadline) {
|
|
3313
|
+
await setTimeout$1(EXIT_POLL_MS);
|
|
3314
|
+
if (!signalProcess(pid, 0)) return;
|
|
3315
|
+
}
|
|
3316
|
+
signalProcess(pid, "SIGKILL");
|
|
3317
|
+
while (signalProcess(pid, 0)) await setTimeout$1(EXIT_POLL_MS);
|
|
3318
|
+
}
|
|
2889
3319
|
return {
|
|
2890
|
-
|
|
2891
|
-
|
|
2892
|
-
|
|
2893
|
-
|
|
3320
|
+
start: (spawn) => spawnDetached(spawn.command, spawn.args, spawn.options),
|
|
3321
|
+
commandLine,
|
|
3322
|
+
terminate,
|
|
3323
|
+
tailLog: (path, options) => runner.stream(TAIL, tailArgs(path, options))
|
|
2894
3324
|
};
|
|
2895
3325
|
}
|
|
2896
|
-
|
|
2897
|
-
|
|
2898
|
-
|
|
3326
|
+
//#endregion
|
|
3327
|
+
//#region src/runtime/docker.ts
|
|
3328
|
+
const DEFAULT_IMAGE = "metabase/metabase-dev:transform-tests-ee";
|
|
3329
|
+
const CONTAINER_NAME_PREFIX$1 = "rde-metabase";
|
|
3330
|
+
const CONTAINER_PORT = 3e3;
|
|
3331
|
+
function containerName(instanceName) {
|
|
3332
|
+
return `${CONTAINER_NAME_PREFIX$1}-${instanceName}`;
|
|
2899
3333
|
}
|
|
2900
|
-
|
|
2901
|
-
return
|
|
2902
|
-
|
|
2903
|
-
|
|
2904
|
-
|
|
2905
|
-
|
|
2906
|
-
|
|
2907
|
-
}
|
|
3334
|
+
function publishesPortOnLoopback(inspection, port) {
|
|
3335
|
+
return inspection.ports.some((mapping) => mapping.container === CONTAINER_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
3336
|
+
}
|
|
3337
|
+
function hostUserFor(platform, hostUser) {
|
|
3338
|
+
return platform === "linux" ? hostUser : null;
|
|
3339
|
+
}
|
|
3340
|
+
function dockerRuntime(state, deps) {
|
|
3341
|
+
const { docker } = deps;
|
|
3342
|
+
const url = instanceUrl(state.port);
|
|
3343
|
+
async function inspect() {
|
|
3344
|
+
return docker.containerInspect(state.containerName);
|
|
3345
|
+
}
|
|
3346
|
+
function conflict(reason) {
|
|
3347
|
+
return new ConfigError(`container ${state.containerName} already exists ${reason}; remove it with \`docker rm -f ${state.containerName}\` and run \`rde init\` again`);
|
|
3348
|
+
}
|
|
3349
|
+
async function install(options) {
|
|
3350
|
+
const existing = await inspect();
|
|
3351
|
+
if (existing.exists) {
|
|
3352
|
+
if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
|
|
3353
|
+
if (!publishesPortOnLoopback(existing, state.port)) throw conflict(`without port ${state.port} published on ${LOOPBACK_HOST}`);
|
|
3354
|
+
return;
|
|
3355
|
+
}
|
|
3356
|
+
await promises.mkdir(join(deps.dataDir, PLUGINS_DIR), { recursive: true });
|
|
3357
|
+
await docker.imagePull(state.image, { onLine: options.onProgress });
|
|
3358
|
+
await docker.containerCreate({
|
|
3359
|
+
name: state.containerName,
|
|
3360
|
+
image: state.image,
|
|
3361
|
+
port: {
|
|
3362
|
+
host: state.port,
|
|
3363
|
+
container: CONTAINER_PORT
|
|
3364
|
+
},
|
|
3365
|
+
mount: {
|
|
3366
|
+
source: deps.dataDir,
|
|
3367
|
+
target: CONTAINER_DATA_DIR
|
|
3368
|
+
},
|
|
3369
|
+
env: serverEnv({
|
|
3370
|
+
dbFile: `${CONTAINER_DATA_DIR}/${DB_DIR}`,
|
|
3371
|
+
pluginsDir: `${CONTAINER_DATA_DIR}/${PLUGINS_DIR}`,
|
|
3372
|
+
port: state.port
|
|
3373
|
+
}),
|
|
3374
|
+
addHostGateway: deps.platform === "linux",
|
|
3375
|
+
hostUser: hostUserFor(deps.platform, deps.hostUser)
|
|
3376
|
+
});
|
|
3377
|
+
}
|
|
3378
|
+
async function start() {
|
|
3379
|
+
const existing = await inspect();
|
|
3380
|
+
if (!existing.exists) throw new ConfigError(`container ${state.containerName} does not exist; run \`rde init\``);
|
|
3381
|
+
if (existing.running) return;
|
|
3382
|
+
await docker.containerStart(state.containerName);
|
|
3383
|
+
}
|
|
3384
|
+
async function stop() {
|
|
3385
|
+
const existing = await inspect();
|
|
3386
|
+
if (!existing.exists || !existing.running) return;
|
|
3387
|
+
await docker.containerStop(state.containerName);
|
|
3388
|
+
}
|
|
3389
|
+
async function status() {
|
|
3390
|
+
const existing = await inspect();
|
|
3391
|
+
return {
|
|
3392
|
+
installed: existing.exists,
|
|
3393
|
+
running: existing.exists && existing.running,
|
|
3394
|
+
url
|
|
3395
|
+
};
|
|
3396
|
+
}
|
|
3397
|
+
async function logs(options) {
|
|
3398
|
+
if (!(await inspect()).exists) throw new ConfigError(`container ${state.containerName} does not exist; run \`rde init\``);
|
|
3399
|
+
await docker.containerLogs(state.containerName, options);
|
|
3400
|
+
}
|
|
3401
|
+
return {
|
|
3402
|
+
kind: "docker",
|
|
3403
|
+
install,
|
|
3404
|
+
start,
|
|
3405
|
+
stop,
|
|
3406
|
+
status,
|
|
3407
|
+
logs,
|
|
3408
|
+
url: () => url,
|
|
3409
|
+
hostAlias: () => state.hostAlias
|
|
3410
|
+
};
|
|
3411
|
+
}
|
|
3412
|
+
//#endregion
|
|
3413
|
+
//#region src/runtime/runtime.ts
|
|
3414
|
+
function hostRuntimeDeps(instanceName, signal) {
|
|
3415
|
+
const user = userInfo();
|
|
3416
|
+
const runner = processRunner(signal);
|
|
3417
|
+
return {
|
|
3418
|
+
docker: dockerTool(runner),
|
|
3419
|
+
java: javaTool(runner),
|
|
3420
|
+
daemon: daemonTool(runner),
|
|
3421
|
+
download: fileDownloader(signal),
|
|
3422
|
+
dataDir: instanceDataDir(instanceName),
|
|
3423
|
+
instanceDir: instanceDir(instanceName),
|
|
3424
|
+
jarsDir: jarsDir(),
|
|
3425
|
+
platform: process.platform,
|
|
3426
|
+
hostUser: {
|
|
3427
|
+
uid: user.uid,
|
|
3428
|
+
gid: user.gid
|
|
3429
|
+
}
|
|
3430
|
+
};
|
|
3431
|
+
}
|
|
3432
|
+
function runtimeFromState(state, deps) {
|
|
3433
|
+
switch (state.kind) {
|
|
3434
|
+
case "docker": return dockerRuntime(state, deps);
|
|
3435
|
+
case "jar": return jarRuntime(state, deps);
|
|
3436
|
+
}
|
|
3437
|
+
}
|
|
3438
|
+
//#endregion
|
|
3439
|
+
//#region src/runtime/sample-database.ts
|
|
3440
|
+
const SAMPLE_DATABASE_IMAGE = "metabase/qa-databases:postgres-sample-15";
|
|
3441
|
+
const SAMPLE_DATABASE_USER = "metabase";
|
|
3442
|
+
const SAMPLE_DATABASE_PASSWORD = "metasample123";
|
|
3443
|
+
const SAMPLE_DATABASE_NAME = "sample";
|
|
3444
|
+
const SAMPLE_DATABASE_PORT = 15432;
|
|
3445
|
+
const SAMPLE_DATABASE_READY_TIMEOUT_MS = 12e4;
|
|
3446
|
+
const POSTGRES_PORT = 5432;
|
|
3447
|
+
const CONTAINER_NAME_PREFIX = "rde-sampledb";
|
|
3448
|
+
const READY_POLL_MS = 1e3;
|
|
3449
|
+
const READY_PROBE = [
|
|
3450
|
+
"pg_isready",
|
|
3451
|
+
"-h",
|
|
3452
|
+
LOOPBACK_HOST,
|
|
3453
|
+
"-U",
|
|
3454
|
+
SAMPLE_DATABASE_USER,
|
|
3455
|
+
"-d",
|
|
3456
|
+
SAMPLE_DATABASE_NAME
|
|
3457
|
+
];
|
|
3458
|
+
function sampleContainerName(instanceName) {
|
|
3459
|
+
return `${CONTAINER_NAME_PREFIX}-${instanceName}`;
|
|
3460
|
+
}
|
|
3461
|
+
function sampleDatabaseUrl(port) {
|
|
3462
|
+
return `postgres://${SAMPLE_DATABASE_USER}:${SAMPLE_DATABASE_PASSWORD}@localhost:${port}/${SAMPLE_DATABASE_NAME}`;
|
|
3463
|
+
}
|
|
3464
|
+
function publishesOnLoopback$1(inspection, port) {
|
|
3465
|
+
return inspection.ports.some((mapping) => mapping.container === POSTGRES_PORT && mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
3466
|
+
}
|
|
3467
|
+
function sampleDatabaseContainer(state, docker) {
|
|
3468
|
+
const name = state.containerName;
|
|
3469
|
+
function conflict(reason) {
|
|
3470
|
+
return new ConfigError(`container ${name} already exists ${reason}; remove it with \`docker rm -f ${name}\` and run \`rde init\` again`);
|
|
3471
|
+
}
|
|
3472
|
+
async function install(options) {
|
|
3473
|
+
const existing = await docker.containerInspect(name);
|
|
3474
|
+
if (existing.exists) {
|
|
3475
|
+
if (existing.image !== state.image) throw conflict(`with image ${existing.image}, not ${state.image}`);
|
|
3476
|
+
if (!publishesOnLoopback$1(existing, state.port)) throw conflict(`without port ${state.port} published on ${LOOPBACK_HOST}`);
|
|
3477
|
+
return;
|
|
3478
|
+
}
|
|
3479
|
+
await docker.imagePull(state.image, { onLine: options.onProgress });
|
|
3480
|
+
await docker.containerCreate({
|
|
3481
|
+
name,
|
|
3482
|
+
image: state.image,
|
|
3483
|
+
port: {
|
|
3484
|
+
host: state.port,
|
|
3485
|
+
container: POSTGRES_PORT
|
|
3486
|
+
},
|
|
3487
|
+
mount: null,
|
|
3488
|
+
env: {},
|
|
3489
|
+
addHostGateway: false,
|
|
3490
|
+
hostUser: null
|
|
3491
|
+
});
|
|
3492
|
+
}
|
|
3493
|
+
async function start() {
|
|
3494
|
+
const existing = await docker.containerInspect(name);
|
|
3495
|
+
if (!existing.exists) return "missing";
|
|
3496
|
+
if (existing.running) return "running";
|
|
3497
|
+
await docker.containerStart(name);
|
|
3498
|
+
return "started";
|
|
3499
|
+
}
|
|
3500
|
+
async function stop() {
|
|
3501
|
+
const existing = await docker.containerInspect(name);
|
|
3502
|
+
if (existing.exists && existing.running) await docker.containerStop(name);
|
|
3503
|
+
}
|
|
3504
|
+
async function status() {
|
|
3505
|
+
const existing = await docker.containerInspect(name);
|
|
3506
|
+
return {
|
|
3507
|
+
installed: existing.exists,
|
|
3508
|
+
running: existing.exists && existing.running
|
|
3509
|
+
};
|
|
3510
|
+
}
|
|
3511
|
+
async function waitReady(options) {
|
|
3512
|
+
const deadline = Date.now() + options.timeoutMs;
|
|
3513
|
+
const interval = options.pollMs ?? READY_POLL_MS;
|
|
3514
|
+
for (;;) {
|
|
3515
|
+
if (await docker.containerExecSucceeds(name, READY_PROBE)) return;
|
|
3516
|
+
const existing = await docker.containerInspect(name);
|
|
3517
|
+
if (!existing.exists || !existing.running) throw new ConfigError(`container ${name} stopped before Postgres was ready; \`docker logs ${name}\` shows why`);
|
|
3518
|
+
if (Date.now() >= deadline) throw new ConfigError(`Postgres in ${name} is not ready after ${Math.round(options.timeoutMs / 1e3)}s; \`docker logs ${name}\` shows what it is doing`);
|
|
3519
|
+
try {
|
|
3520
|
+
await setTimeout$1(interval, void 0, { signal: options.signal });
|
|
3521
|
+
} catch {
|
|
3522
|
+
throw new AbortError();
|
|
3523
|
+
}
|
|
3524
|
+
}
|
|
3525
|
+
}
|
|
3526
|
+
return {
|
|
3527
|
+
install,
|
|
3528
|
+
start,
|
|
3529
|
+
stop,
|
|
3530
|
+
status,
|
|
3531
|
+
waitReady
|
|
3532
|
+
};
|
|
3533
|
+
}
|
|
3534
|
+
//#endregion
|
|
3535
|
+
//#region src/tools/agents.ts
|
|
3536
|
+
const AgentId = z.enum([
|
|
3537
|
+
"claude-code",
|
|
3538
|
+
"codex",
|
|
3539
|
+
"cursor",
|
|
3540
|
+
"gemini-cli",
|
|
3541
|
+
"opencode",
|
|
3542
|
+
"windsurf"
|
|
3543
|
+
]);
|
|
3544
|
+
const AGENTS = [
|
|
3545
|
+
{
|
|
3546
|
+
id: "claude-code",
|
|
3547
|
+
displayName: "Claude Code",
|
|
3548
|
+
skillsCliName: "claude-code",
|
|
3549
|
+
command: "claude",
|
|
3550
|
+
launch: "claude",
|
|
3551
|
+
configDir: (home) => join(home, ".claude")
|
|
3552
|
+
},
|
|
3553
|
+
{
|
|
3554
|
+
id: "codex",
|
|
3555
|
+
displayName: "Codex",
|
|
3556
|
+
skillsCliName: "codex",
|
|
3557
|
+
command: "codex",
|
|
3558
|
+
launch: "codex",
|
|
3559
|
+
configDir: (home) => join(home, ".codex")
|
|
3560
|
+
},
|
|
3561
|
+
{
|
|
3562
|
+
id: "cursor",
|
|
3563
|
+
displayName: "Cursor",
|
|
3564
|
+
skillsCliName: "cursor",
|
|
3565
|
+
command: "cursor",
|
|
3566
|
+
launch: "cursor .",
|
|
3567
|
+
configDir: (home) => join(home, ".cursor")
|
|
3568
|
+
},
|
|
3569
|
+
{
|
|
3570
|
+
id: "gemini-cli",
|
|
3571
|
+
displayName: "Gemini CLI",
|
|
3572
|
+
skillsCliName: "gemini-cli",
|
|
3573
|
+
command: "gemini",
|
|
3574
|
+
launch: "gemini",
|
|
3575
|
+
configDir: (home) => join(home, ".gemini")
|
|
3576
|
+
},
|
|
3577
|
+
{
|
|
3578
|
+
id: "opencode",
|
|
3579
|
+
displayName: "OpenCode",
|
|
3580
|
+
skillsCliName: "opencode",
|
|
3581
|
+
command: "opencode",
|
|
3582
|
+
launch: "opencode",
|
|
3583
|
+
configDir: (_home, configHome) => join(configHome, "opencode")
|
|
3584
|
+
},
|
|
3585
|
+
{
|
|
3586
|
+
id: "windsurf",
|
|
3587
|
+
displayName: "Windsurf",
|
|
3588
|
+
skillsCliName: "windsurf",
|
|
3589
|
+
command: null,
|
|
3590
|
+
launch: "windsurf .",
|
|
3591
|
+
configDir: (home) => join(home, ".codeium", "windsurf")
|
|
3592
|
+
}
|
|
3593
|
+
];
|
|
3594
|
+
async function directoryExists$1(path) {
|
|
3595
|
+
try {
|
|
3596
|
+
return (await promises.stat(path)).isDirectory();
|
|
3597
|
+
} catch {
|
|
3598
|
+
return false;
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
function hostAgentProbe() {
|
|
3602
|
+
const home = homedir();
|
|
3603
|
+
return {
|
|
3604
|
+
commandExists: async (command) => await whichCommand(command) !== null,
|
|
3605
|
+
directoryExists: directoryExists$1,
|
|
3606
|
+
home,
|
|
3607
|
+
configHome: readEnv("XDG_CONFIG_HOME") ?? join(home, ".config")
|
|
3608
|
+
};
|
|
3609
|
+
}
|
|
3610
|
+
async function isDetected(agent, probe) {
|
|
3611
|
+
if (agent.command !== null && await probe.commandExists(agent.command)) return true;
|
|
3612
|
+
return probe.directoryExists(agent.configDir(probe.home, probe.configHome));
|
|
3613
|
+
}
|
|
3614
|
+
async function detectAgents(probe) {
|
|
3615
|
+
return Promise.all(AGENTS.map(async (agent) => ({
|
|
3616
|
+
id: agent.id,
|
|
3617
|
+
displayName: agent.displayName,
|
|
3618
|
+
skillsCliName: agent.skillsCliName,
|
|
3619
|
+
launch: agent.launch,
|
|
3620
|
+
detected: await isDetected(agent, probe)
|
|
3621
|
+
})));
|
|
2908
3622
|
}
|
|
2909
3623
|
//#endregion
|
|
2910
3624
|
//#region src/tools/mb.ts
|
|
@@ -3282,11 +3996,10 @@ function skillsCli(runner) {
|
|
|
3282
3996
|
}
|
|
3283
3997
|
//#endregion
|
|
3284
3998
|
//#region src/steps/agents-skills.ts
|
|
3285
|
-
const STEP_ID$
|
|
3286
|
-
const AGENTS_SKILLS_KEYS = { agents: `${STEP_ID$7}.agents` };
|
|
3287
|
-
const DETECTED_HINT = "detected";
|
|
3999
|
+
const STEP_ID$8 = "agents-skills";
|
|
3288
4000
|
const MECHANICS_NOTE = "The skill's mechanics come from `mb skills get <name>` at runtime, so nothing else needs installing.";
|
|
3289
|
-
|
|
4001
|
+
const DETECT_AGENTS = { agents: null };
|
|
4002
|
+
function hostDeps$11(ctx) {
|
|
3290
4003
|
return {
|
|
3291
4004
|
runner: processRunner(ctx.signal),
|
|
3292
4005
|
probe: hostAgentProbe()
|
|
@@ -3295,6 +4008,9 @@ function hostDeps$9(ctx) {
|
|
|
3295
4008
|
function detectedOnly(agents) {
|
|
3296
4009
|
return agents.filter((agent) => agent.detected);
|
|
3297
4010
|
}
|
|
4011
|
+
function displayNames(agents) {
|
|
4012
|
+
return agents.map((agent) => agent.displayName).join(", ");
|
|
4013
|
+
}
|
|
3298
4014
|
function skillCoversAgent(entry, agent) {
|
|
3299
4015
|
return entry.agents.includes(agent.displayName);
|
|
3300
4016
|
}
|
|
@@ -3304,45 +4020,53 @@ function coversAny(entry, agents) {
|
|
|
3304
4020
|
async function rdeSkillEntries(runner) {
|
|
3305
4021
|
return (await skillsCli(runner).list()).filter((entry) => entry.name === "rde");
|
|
3306
4022
|
}
|
|
3307
|
-
function
|
|
4023
|
+
function installedLines(entries) {
|
|
4024
|
+
return entries.map((entry) => `${entry.agents.join(", ")}: ${entry.path}`).join("\n");
|
|
4025
|
+
}
|
|
4026
|
+
const SOURCE_NOTES = {
|
|
4027
|
+
override: "from --agents",
|
|
4028
|
+
detected: "detected on this machine"
|
|
4029
|
+
};
|
|
4030
|
+
function selectAgents(all, options) {
|
|
4031
|
+
if (options.agents === null) return {
|
|
4032
|
+
agents: detectedOnly(all),
|
|
4033
|
+
source: "detected"
|
|
4034
|
+
};
|
|
4035
|
+
const chosen = options.agents;
|
|
3308
4036
|
return {
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
...agent.detected && { hint: DETECTED_HINT }
|
|
4037
|
+
agents: all.filter((agent) => chosen.includes(agent.id)),
|
|
4038
|
+
source: "override"
|
|
3312
4039
|
};
|
|
3313
4040
|
}
|
|
3314
|
-
function
|
|
3315
|
-
|
|
4041
|
+
function skipLine(all, source) {
|
|
4042
|
+
switch (source) {
|
|
4043
|
+
case "override": return `Skipping the rde skill; run \`rde init --only ${STEP_ID$8}\` to install it later`;
|
|
4044
|
+
case "detected": return `No coding agent found (${displayNames(all)}); run \`rde init --only ${STEP_ID$8} --agents <name>\` once one is installed`;
|
|
4045
|
+
}
|
|
3316
4046
|
}
|
|
3317
|
-
function agentsSkillsStep(deps = hostDeps$
|
|
4047
|
+
function agentsSkillsStep(options, deps = hostDeps$11) {
|
|
3318
4048
|
return {
|
|
3319
|
-
id: STEP_ID$
|
|
4049
|
+
id: STEP_ID$8,
|
|
3320
4050
|
title: "Install the rde skill for your coding agents",
|
|
3321
4051
|
async detect(ctx) {
|
|
3322
4052
|
const resolved = deps(ctx);
|
|
3323
4053
|
const detected = detectedOnly(await detectAgents(resolved.probe));
|
|
3324
|
-
if (detected.length === 0) return ctx.state.steps[STEP_ID$
|
|
4054
|
+
if (detected.length === 0) return ctx.state.steps[STEP_ID$8]?.status === "skipped" ? "not-applicable" : "todo";
|
|
3325
4055
|
return (await rdeSkillEntries(resolved.runner)).some((entry) => coversAny(entry, detected)) ? "done" : "todo";
|
|
3326
4056
|
},
|
|
3327
4057
|
async run(ctx) {
|
|
3328
4058
|
const resolved = deps(ctx);
|
|
3329
|
-
const
|
|
3330
|
-
const
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
choices: agents.map(agentChoice),
|
|
3334
|
-
initialValues: detectedOnly(agents).map((agent) => agent.id),
|
|
3335
|
-
required: false
|
|
3336
|
-
});
|
|
3337
|
-
if (chosen.length === 0) {
|
|
3338
|
-
ctx.ui.info(`Skipping the rde skill; run \`rde init --only ${STEP_ID$7}\` to install it later`);
|
|
4059
|
+
const all = await detectAgents(resolved.probe);
|
|
4060
|
+
const selection = selectAgents(all, options);
|
|
4061
|
+
if (selection.agents.length === 0) {
|
|
4062
|
+
ctx.ui.info(skipLine(all, selection.source));
|
|
3339
4063
|
return "skipped";
|
|
3340
4064
|
}
|
|
3341
|
-
|
|
4065
|
+
ctx.ui.info(`Installing the rde skill for ${displayNames(selection.agents)} (${SOURCE_NOTES[selection.source]})`);
|
|
3342
4066
|
await skillsCli(resolved.runner).add({
|
|
3343
4067
|
source: RDE_SKILL_SOURCE,
|
|
3344
4068
|
skill: "rde",
|
|
3345
|
-
agents:
|
|
4069
|
+
agents: selection.agents.map((agent) => agent.skillsCliName)
|
|
3346
4070
|
});
|
|
3347
4071
|
const entries = await rdeSkillEntries(resolved.runner);
|
|
3348
4072
|
if (entries.length === 0) throw new ConfigError(`the skills installer finished but \`skills list\` does not show the rde skill`);
|
|
@@ -3354,30 +4078,19 @@ function agentsSkillsStep(deps = hostDeps$9) {
|
|
|
3354
4078
|
}
|
|
3355
4079
|
//#endregion
|
|
3356
4080
|
//#region src/steps/install-mb.ts
|
|
3357
|
-
const STEP_ID$
|
|
3358
|
-
const INSTALL_MB_KEYS = {
|
|
3359
|
-
packageManager: `${STEP_ID$6}.package-manager`,
|
|
3360
|
-
confirm: `${STEP_ID$6}.confirm`
|
|
3361
|
-
};
|
|
4081
|
+
const STEP_ID$7 = "install-mb";
|
|
3362
4082
|
const MIN_MB_VERSION = "0.3.0";
|
|
3363
4083
|
const MB_RANGE = `>=${MIN_MB_VERSION}`;
|
|
3364
|
-
const PACKAGE_MANAGERS = [
|
|
3365
|
-
"npm",
|
|
3366
|
-
"pnpm",
|
|
3367
|
-
"yarn",
|
|
3368
|
-
"bun"
|
|
3369
|
-
];
|
|
3370
4084
|
function installedWith(scriptPath) {
|
|
3371
4085
|
return scriptPath === void 0 ? "npm" : packageManagerFor(scriptPath);
|
|
3372
4086
|
}
|
|
3373
4087
|
function rdeInstalledWith() {
|
|
3374
4088
|
return installedWith(process.argv[1]);
|
|
3375
4089
|
}
|
|
3376
|
-
function hostDeps$
|
|
4090
|
+
function hostDeps$10(ctx) {
|
|
3377
4091
|
return {
|
|
3378
4092
|
runner: processRunner(ctx.signal),
|
|
3379
|
-
installedWith: rdeInstalledWith()
|
|
3380
|
-
commandExists: async (name) => await whichCommand(name) !== null
|
|
4093
|
+
installedWith: rdeInstalledWith()
|
|
3381
4094
|
};
|
|
3382
4095
|
}
|
|
3383
4096
|
async function inspectMb(runner) {
|
|
@@ -3391,35 +4104,9 @@ async function inspectMb(runner) {
|
|
|
3391
4104
|
satisfies: semver.satisfies(version, MB_RANGE, { includePrerelease: true })
|
|
3392
4105
|
};
|
|
3393
4106
|
}
|
|
3394
|
-
async function candidateManagers(deps) {
|
|
3395
|
-
const others = [];
|
|
3396
|
-
for (const manager of PACKAGE_MANAGERS) if (manager !== deps.installedWith && await deps.commandExists(manager)) others.push(manager);
|
|
3397
|
-
return [deps.installedWith, ...others];
|
|
3398
|
-
}
|
|
3399
|
-
function managerChoice(manager, preferred) {
|
|
3400
|
-
return {
|
|
3401
|
-
value: manager,
|
|
3402
|
-
label: manager,
|
|
3403
|
-
...manager === preferred && { hint: "installed rde" }
|
|
3404
|
-
};
|
|
3405
|
-
}
|
|
3406
4107
|
function displayCommand(install) {
|
|
3407
4108
|
return [install.command, ...install.args].join(" ");
|
|
3408
4109
|
}
|
|
3409
|
-
async function chooseManager(ctx, deps, verb) {
|
|
3410
|
-
const managers = await candidateManagers(deps);
|
|
3411
|
-
if (managers.length > 1) return ctx.prompts.select({
|
|
3412
|
-
key: INSTALL_MB_KEYS.packageManager,
|
|
3413
|
-
message: `${verb} ${MB_PACKAGE} globally with which package manager?`,
|
|
3414
|
-
choices: managers.map((manager) => managerChoice(manager, deps.installedWith))
|
|
3415
|
-
});
|
|
3416
|
-
if (!await ctx.prompts.confirm({
|
|
3417
|
-
key: INSTALL_MB_KEYS.confirm,
|
|
3418
|
-
message: `${verb} @metabase/cli globally with ${deps.installedWith}?`,
|
|
3419
|
-
initialValue: true
|
|
3420
|
-
})) throw new ConfigError(`mb is required; install it with \`${displayCommand(mbInstallCommand(deps.installedWith))}\` and run rde init again`);
|
|
3421
|
-
return deps.installedWith;
|
|
3422
|
-
}
|
|
3423
4110
|
function stillWrong(install, after) {
|
|
3424
4111
|
const found = after.installed ? `mb ${after.version} is still what is on PATH (${MIN_MB_VERSION} or newer is required)` : "mb is not on PATH after the install";
|
|
3425
4112
|
return new ExternalToolError({
|
|
@@ -3429,9 +4116,9 @@ function stillWrong(install, after) {
|
|
|
3429
4116
|
stderrTail: `${found}; open a new terminal and run rde init again`
|
|
3430
4117
|
});
|
|
3431
4118
|
}
|
|
3432
|
-
function installMbStep(deps = hostDeps$
|
|
4119
|
+
function installMbStep(deps = hostDeps$10) {
|
|
3433
4120
|
return {
|
|
3434
|
-
id: STEP_ID$
|
|
4121
|
+
id: STEP_ID$7,
|
|
3435
4122
|
title: "Install the Metabase CLI",
|
|
3436
4123
|
async detect(ctx) {
|
|
3437
4124
|
const found = await inspectMb(deps(ctx).runner);
|
|
@@ -3445,7 +4132,7 @@ function installMbStep(deps = hostDeps$8) {
|
|
|
3445
4132
|
return "completed";
|
|
3446
4133
|
}
|
|
3447
4134
|
if (before.installed) ctx.ui.warn(`mb ${before.version} is installed; ${MIN_MB_VERSION} or newer is required`);
|
|
3448
|
-
const install = mbInstallCommand(
|
|
4135
|
+
const install = mbInstallCommand(resolved.installedWith);
|
|
3449
4136
|
ctx.ui.info(`Running ${displayCommand(install)}`);
|
|
3450
4137
|
const exitCode = await resolved.runner.stream(install.command, install.args);
|
|
3451
4138
|
if (exitCode !== 0) throw new ExternalToolError({
|
|
@@ -3606,25 +4293,35 @@ const NODE_RANGE = `>=${MIN_NODE_VERSION}`;
|
|
|
3606
4293
|
const NODE_HINT = `Install Node ${MIN_NODE_VERSION} or newer from https://nodejs.org or with your version manager`;
|
|
3607
4294
|
const MARK_OK = "✓";
|
|
3608
4295
|
const MARK_MISSING = "✗";
|
|
4296
|
+
const MARK_OPTIONAL = "–";
|
|
4297
|
+
const DOCKER_OPTIONAL = `Metabase can run as a jar with Java instead, without the sample database`;
|
|
4298
|
+
const JAVA_OPTIONAL = `Metabase can run in Docker instead`;
|
|
3609
4299
|
const PLATFORMS = {
|
|
3610
4300
|
darwin: {
|
|
3611
4301
|
name: "macOS",
|
|
3612
4302
|
gitHint: "Install the Xcode Command Line Tools: xcode-select --install",
|
|
3613
|
-
dockerHint: "Install Docker Desktop (https://www.docker.com/products/docker-desktop/), OrbStack, or colima, and start it"
|
|
4303
|
+
dockerHint: "Install Docker Desktop (https://www.docker.com/products/docker-desktop/), OrbStack, or colima, and start it",
|
|
4304
|
+
javaHint: `Install Java 21 or newer: brew install --cask temurin@21, or from https://adoptium.net`
|
|
3614
4305
|
},
|
|
3615
4306
|
linux: {
|
|
3616
4307
|
name: "Linux",
|
|
3617
4308
|
gitHint: "Install the git package with your distribution's package manager",
|
|
3618
|
-
dockerHint: "Install the docker package, start the daemon, and add yourself to the docker group: sudo usermod -aG docker $USER (then log in again)"
|
|
4309
|
+
dockerHint: "Install the docker package, start the daemon, and add yourself to the docker group: sudo usermod -aG docker $USER (then log in again)",
|
|
4310
|
+
javaHint: `Install Java 21 or newer with your distribution's package manager (openjdk-21-jre) or from https://adoptium.net`
|
|
3619
4311
|
}
|
|
3620
4312
|
};
|
|
3621
|
-
function hostDeps$
|
|
4313
|
+
function hostDeps$9(ctx) {
|
|
4314
|
+
const runner = processRunner(ctx.signal);
|
|
3622
4315
|
return {
|
|
3623
4316
|
platform: process.platform,
|
|
3624
4317
|
nodeVersion: process.version,
|
|
3625
|
-
runner
|
|
4318
|
+
runner,
|
|
4319
|
+
java: javaTool(runner)
|
|
3626
4320
|
};
|
|
3627
4321
|
}
|
|
4322
|
+
function blocks(check) {
|
|
4323
|
+
return !check.ok && check.optional === null;
|
|
4324
|
+
}
|
|
3628
4325
|
function isSupported(platform) {
|
|
3629
4326
|
return platform === "darwin" || platform === "linux";
|
|
3630
4327
|
}
|
|
@@ -3641,7 +4338,8 @@ function nodeCheck(nodeVersion) {
|
|
|
3641
4338
|
id: "node",
|
|
3642
4339
|
ok: false,
|
|
3643
4340
|
label: `Node ${nodeVersion} (${MIN_NODE_VERSION} or newer is required)`,
|
|
3644
|
-
hint: NODE_HINT
|
|
4341
|
+
hint: NODE_HINT,
|
|
4342
|
+
optional: null
|
|
3645
4343
|
};
|
|
3646
4344
|
}
|
|
3647
4345
|
async function gitCheck(runner, facts) {
|
|
@@ -3656,7 +4354,8 @@ async function gitCheck(runner, facts) {
|
|
|
3656
4354
|
id: "git",
|
|
3657
4355
|
ok: false,
|
|
3658
4356
|
label: "git is not installed",
|
|
3659
|
-
hint: facts.gitHint
|
|
4357
|
+
hint: facts.gitHint,
|
|
4358
|
+
optional: null
|
|
3660
4359
|
};
|
|
3661
4360
|
throw error;
|
|
3662
4361
|
}
|
|
@@ -3672,8 +4371,35 @@ async function dockerCheck(runner, facts) {
|
|
|
3672
4371
|
id: "docker",
|
|
3673
4372
|
ok: false,
|
|
3674
4373
|
label: `Docker: ${docker.reason}`,
|
|
3675
|
-
hint: facts.dockerHint
|
|
4374
|
+
hint: facts.dockerHint,
|
|
4375
|
+
optional: null
|
|
4376
|
+
};
|
|
4377
|
+
}
|
|
4378
|
+
async function javaCheck(tool, facts) {
|
|
4379
|
+
const java = await tool.find();
|
|
4380
|
+
if (java.found && java.major >= 21) return {
|
|
4381
|
+
id: "java",
|
|
4382
|
+
ok: true,
|
|
4383
|
+
label: `Java ${java.version}`
|
|
3676
4384
|
};
|
|
4385
|
+
return {
|
|
4386
|
+
id: "java",
|
|
4387
|
+
ok: false,
|
|
4388
|
+
label: java.found ? `Java ${java.version} (21 or newer is required)` : `Java: ${java.reason}`,
|
|
4389
|
+
hint: facts.javaHint,
|
|
4390
|
+
optional: null
|
|
4391
|
+
};
|
|
4392
|
+
}
|
|
4393
|
+
function coveredBy(check, other, optional) {
|
|
4394
|
+
return !check.ok && other.ok ? {
|
|
4395
|
+
...check,
|
|
4396
|
+
optional
|
|
4397
|
+
} : check;
|
|
4398
|
+
}
|
|
4399
|
+
async function runtimeChecks(deps, facts) {
|
|
4400
|
+
const docker = await dockerCheck(deps.runner, facts);
|
|
4401
|
+
const java = await javaCheck(deps.java, facts);
|
|
4402
|
+
return [coveredBy(docker, java, DOCKER_OPTIONAL), coveredBy(java, docker, JAVA_OPTIONAL)];
|
|
3677
4403
|
}
|
|
3678
4404
|
async function runChecks(deps, platform) {
|
|
3679
4405
|
const facts = PLATFORMS[platform];
|
|
@@ -3685,7 +4411,7 @@ async function runChecks(deps, platform) {
|
|
|
3685
4411
|
},
|
|
3686
4412
|
nodeCheck(deps.nodeVersion),
|
|
3687
4413
|
await gitCheck(deps.runner, facts),
|
|
3688
|
-
await
|
|
4414
|
+
...await runtimeChecks(deps, facts)
|
|
3689
4415
|
];
|
|
3690
4416
|
}
|
|
3691
4417
|
async function machineChecks(deps) {
|
|
@@ -3693,46 +4419,54 @@ async function machineChecks(deps) {
|
|
|
3693
4419
|
id: "platform",
|
|
3694
4420
|
ok: false,
|
|
3695
4421
|
label: `${deps.platform} is not supported`,
|
|
3696
|
-
hint: "rde runs on macOS and Linux"
|
|
4422
|
+
hint: "rde runs on macOS and Linux",
|
|
4423
|
+
optional: null
|
|
3697
4424
|
}];
|
|
3698
4425
|
return runChecks(deps, deps.platform);
|
|
3699
4426
|
}
|
|
4427
|
+
function checkLine(check) {
|
|
4428
|
+
if (check.ok) return `${MARK_OK} ${check.label}`;
|
|
4429
|
+
if (check.optional !== null) return `${MARK_OPTIONAL} ${check.label}; ${check.optional}\n ${check.hint}`;
|
|
4430
|
+
return `${MARK_MISSING} ${check.label}\n ${check.hint}`;
|
|
4431
|
+
}
|
|
3700
4432
|
function checkLines(checks) {
|
|
3701
|
-
return checks.map(
|
|
4433
|
+
return checks.map(checkLine);
|
|
3702
4434
|
}
|
|
3703
|
-
function preflightStep(deps = hostDeps$
|
|
4435
|
+
function preflightStep(deps = hostDeps$9) {
|
|
3704
4436
|
return {
|
|
3705
4437
|
id: "preflight",
|
|
3706
4438
|
title: "Check this machine",
|
|
3707
4439
|
async detect(ctx) {
|
|
3708
|
-
return (await machineChecks(deps(ctx))).
|
|
4440
|
+
return (await machineChecks(deps(ctx))).some(blocks) ? "todo" : "done";
|
|
3709
4441
|
},
|
|
3710
4442
|
async run(ctx) {
|
|
3711
4443
|
const resolved = deps(ctx);
|
|
3712
4444
|
if (!isSupported(resolved.platform)) throw unsupportedPlatform(resolved.platform);
|
|
3713
4445
|
const checks = await runChecks(resolved, resolved.platform);
|
|
3714
4446
|
ctx.ui.note(checkLines(checks).join("\n"), "This machine");
|
|
3715
|
-
if (checks.some(
|
|
4447
|
+
if (checks.some(blocks)) throw new ConfigError("Fix the items above and run rde init again");
|
|
3716
4448
|
return "completed";
|
|
3717
4449
|
}
|
|
3718
4450
|
};
|
|
3719
4451
|
}
|
|
3720
4452
|
//#endregion
|
|
3721
4453
|
//#region src/steps/doctor.ts
|
|
3722
|
-
function hostDoctorDeps(signal) {
|
|
4454
|
+
function hostDoctorDeps(instanceName, signal) {
|
|
3723
4455
|
const runner = processRunner(signal);
|
|
4456
|
+
const runtime = hostRuntimeDeps(instanceName, signal);
|
|
3724
4457
|
return {
|
|
3725
4458
|
platform: process.platform,
|
|
3726
4459
|
nodeVersion: process.version,
|
|
3727
4460
|
runner,
|
|
4461
|
+
java: runtime.java,
|
|
3728
4462
|
installedWith: rdeInstalledWith(),
|
|
3729
|
-
|
|
4463
|
+
runtime,
|
|
3730
4464
|
ports: hostPortProbe,
|
|
3731
4465
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
3732
4466
|
probe: hostAgentProbe()
|
|
3733
4467
|
};
|
|
3734
4468
|
}
|
|
3735
|
-
const NOT_FOUND_STATUS$
|
|
4469
|
+
const NOT_FOUND_STATUS$2 = 404;
|
|
3736
4470
|
const NO_INSTANCE = "no instance yet; run `rde init`";
|
|
3737
4471
|
const NOT_HEALTHY = "Metabase is not healthy";
|
|
3738
4472
|
function ok(id, label, detail) {
|
|
@@ -3763,12 +4497,23 @@ function failed(id, label, failure) {
|
|
|
3763
4497
|
...failure
|
|
3764
4498
|
};
|
|
3765
4499
|
}
|
|
3766
|
-
|
|
3767
|
-
|
|
4500
|
+
const PROGRAM_FOR_RUNTIME = {
|
|
4501
|
+
docker: "docker",
|
|
4502
|
+
jar: "java"
|
|
4503
|
+
};
|
|
4504
|
+
function machineRow(check, needed) {
|
|
4505
|
+
if (check.ok) return ok(check.id, check.id, check.label);
|
|
4506
|
+
if (!blocks(check) && check.id !== needed) return skipped(check.id, check.id, `${check.label}; ${check.optional}`);
|
|
4507
|
+
return failed(check.id, check.id, {
|
|
3768
4508
|
detail: check.label,
|
|
3769
4509
|
hint: check.hint,
|
|
3770
4510
|
fix: "preflight"
|
|
3771
|
-
})
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
async function machineRows(deps, runtime) {
|
|
4514
|
+
const checks = await machineChecks(deps);
|
|
4515
|
+
const needed = runtime === null ? null : PROGRAM_FOR_RUNTIME[runtime.kind];
|
|
4516
|
+
return checks.map((check) => machineRow(check, needed));
|
|
3772
4517
|
}
|
|
3773
4518
|
function installHint(installedWith) {
|
|
3774
4519
|
const install = mbInstallCommand(installedWith);
|
|
@@ -3791,20 +4536,27 @@ async function mbRow(deps) {
|
|
|
3791
4536
|
function publishesOnLoopback(container, port) {
|
|
3792
4537
|
return container.ports.some((mapping) => mapping.host === port && mapping.hostIp === "127.0.0.1");
|
|
3793
4538
|
}
|
|
4539
|
+
const CONTAINER = "the container";
|
|
3794
4540
|
async function containerRow(runtime, deps, dockerOk) {
|
|
3795
4541
|
const label = "container";
|
|
3796
4542
|
if (!dockerOk) return {
|
|
3797
4543
|
row: skipped("container", label, "Docker is not available"),
|
|
3798
|
-
|
|
4544
|
+
running: false,
|
|
4545
|
+
holder: CONTAINER
|
|
3799
4546
|
};
|
|
3800
|
-
const inspection = await deps.docker.containerInspect(runtime.containerName);
|
|
4547
|
+
const inspection = await deps.runtime.docker.containerInspect(runtime.containerName);
|
|
3801
4548
|
if (!inspection.exists) return {
|
|
3802
4549
|
row: failed("container", label, {
|
|
3803
4550
|
detail: `${runtime.containerName} does not exist`,
|
|
3804
4551
|
hint: "`rde init` recreates it from the saved choices",
|
|
3805
4552
|
fix: "runtime"
|
|
3806
4553
|
}),
|
|
3807
|
-
|
|
4554
|
+
running: false,
|
|
4555
|
+
holder: CONTAINER
|
|
4556
|
+
};
|
|
4557
|
+
const found = {
|
|
4558
|
+
running: inspection.running,
|
|
4559
|
+
holder: CONTAINER
|
|
3808
4560
|
};
|
|
3809
4561
|
if (inspection.image !== runtime.image) return {
|
|
3810
4562
|
row: failed("container", label, {
|
|
@@ -3812,7 +4564,7 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
3812
4564
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
3813
4565
|
fix: null
|
|
3814
4566
|
}),
|
|
3815
|
-
|
|
4567
|
+
...found
|
|
3816
4568
|
};
|
|
3817
4569
|
if (!publishesOnLoopback(inspection, runtime.port)) return {
|
|
3818
4570
|
row: failed("container", label, {
|
|
@@ -3820,20 +4572,51 @@ async function containerRow(runtime, deps, dockerOk) {
|
|
|
3820
4572
|
hint: `remove it with \`docker rm -f ${runtime.containerName}\` and run \`rde init\``,
|
|
3821
4573
|
fix: null
|
|
3822
4574
|
}),
|
|
3823
|
-
|
|
4575
|
+
...found
|
|
4576
|
+
};
|
|
4577
|
+
const state = inspection.running ? "running" : "stopped";
|
|
4578
|
+
return {
|
|
4579
|
+
row: ok("container", label, `${runtime.containerName} ${state} (${runtime.image})`),
|
|
4580
|
+
...found
|
|
4581
|
+
};
|
|
4582
|
+
}
|
|
4583
|
+
const JAR_PROCESS = "the Metabase process";
|
|
4584
|
+
async function processRow(runtime, deps) {
|
|
4585
|
+
const label = "process";
|
|
4586
|
+
const inspection = await inspectJar(runtime, deps.runtime);
|
|
4587
|
+
const jar = describeJar(runtime.source);
|
|
4588
|
+
if (!inspection.jarExists) {
|
|
4589
|
+
const hint = runtime.source.kind === "release" ? "`rde init` downloads it again" : "put the jar back, or run `rde init` again with another one";
|
|
4590
|
+
return {
|
|
4591
|
+
row: failed("process", label, {
|
|
4592
|
+
detail: `${inspection.jarPath} does not exist`,
|
|
4593
|
+
hint,
|
|
4594
|
+
fix: "runtime"
|
|
4595
|
+
}),
|
|
4596
|
+
running: false,
|
|
4597
|
+
holder: JAR_PROCESS
|
|
4598
|
+
};
|
|
4599
|
+
}
|
|
4600
|
+
const found = {
|
|
4601
|
+
running: inspection.pid !== null,
|
|
4602
|
+
holder: JAR_PROCESS
|
|
3824
4603
|
};
|
|
3825
|
-
const state = inspection.running ? "running" : "stopped";
|
|
3826
4604
|
return {
|
|
3827
|
-
row: ok("
|
|
3828
|
-
|
|
4605
|
+
row: ok("process", label, `${jar} ${inspection.pid === null ? "stopped" : `running as pid ${inspection.pid}`}`),
|
|
4606
|
+
...found
|
|
3829
4607
|
};
|
|
3830
4608
|
}
|
|
3831
|
-
|
|
4609
|
+
function runtimeRow(runtime, deps, dockerOk) {
|
|
4610
|
+
switch (runtime.kind) {
|
|
4611
|
+
case "docker": return containerRow(runtime, deps, dockerOk);
|
|
4612
|
+
case "jar": return processRow(runtime, deps);
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
async function portRow(port, finding, ports) {
|
|
3832
4616
|
const free = await ports.isFree(port);
|
|
3833
|
-
|
|
3834
|
-
if (running
|
|
3835
|
-
|
|
3836
|
-
detail: `${port} is not answering although the container runs`,
|
|
4617
|
+
if (finding.running && !free) return ok("port", "port", `${port}, held by ${finding.holder}`);
|
|
4618
|
+
if (finding.running) return failed("port", "port", {
|
|
4619
|
+
detail: `${port} is not answering although ${finding.holder} runs`,
|
|
3837
4620
|
hint: "`rde logs` shows what Metabase is doing",
|
|
3838
4621
|
fix: null
|
|
3839
4622
|
});
|
|
@@ -3879,14 +4662,26 @@ async function healthRow(url, deps) {
|
|
|
3879
4662
|
throw error;
|
|
3880
4663
|
}
|
|
3881
4664
|
}
|
|
3882
|
-
|
|
3883
|
-
if (!
|
|
4665
|
+
function adminRow(state, properties) {
|
|
4666
|
+
if (!properties["has-user-setup"]) return failed("admin", "admin user", {
|
|
3884
4667
|
detail: "Metabase has no admin user yet",
|
|
3885
4668
|
hint: "`rde init --only admin-user`",
|
|
3886
4669
|
fix: "admin-user"
|
|
3887
4670
|
});
|
|
3888
4671
|
return ok("admin", "admin user", state.admin?.email ?? "created outside rde");
|
|
3889
4672
|
}
|
|
4673
|
+
const VERSION_STEP = "start-metabase";
|
|
4674
|
+
function versionRow(state, properties) {
|
|
4675
|
+
const label = "Metabase version";
|
|
4676
|
+
const running = properties.version.tag;
|
|
4677
|
+
if (state.version === null) return ok("version", label, `${running}; \`rde start\` records it in the state file`);
|
|
4678
|
+
if (state.version !== running) return failed("version", label, {
|
|
4679
|
+
detail: `running ${running}, the state file says ${state.version}`,
|
|
4680
|
+
hint: "`rde start` records the running version",
|
|
4681
|
+
fix: VERSION_STEP
|
|
4682
|
+
});
|
|
4683
|
+
return ok("version", label, running);
|
|
4684
|
+
}
|
|
3890
4685
|
function apiKeyRow(state) {
|
|
3891
4686
|
if (state.apiKey === null) return failed("api-key", "API key", {
|
|
3892
4687
|
detail: "none in the state file",
|
|
@@ -3913,8 +4708,8 @@ async function mbProfileRow(state, deps, mbInstalled, healthy) {
|
|
|
3913
4708
|
const user = entry.user === null ? "" : ` as ${entry.user.name}`;
|
|
3914
4709
|
return ok("mb-profile", label, `${state.mbProfile}: ok${user}`);
|
|
3915
4710
|
}
|
|
3916
|
-
function
|
|
3917
|
-
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$
|
|
4711
|
+
function isNotFound$2(error) {
|
|
4712
|
+
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS$2;
|
|
3918
4713
|
}
|
|
3919
4714
|
async function licenseRow(client) {
|
|
3920
4715
|
const label = "license";
|
|
@@ -3927,7 +4722,7 @@ async function licenseRow(client) {
|
|
|
3927
4722
|
});
|
|
3928
4723
|
return ok("license", label, `valid, ${status.features.includes("remote-sync") ? `with ${REMOTE_SYNC_FEATURE}` : `without ${REMOTE_SYNC_FEATURE}, so git sync is unavailable`}`);
|
|
3929
4724
|
} catch (error) {
|
|
3930
|
-
if (
|
|
4725
|
+
if (isNotFound$2(error)) return skipped("license", label, "not activated; `rde init --from license` adds one");
|
|
3931
4726
|
throw error;
|
|
3932
4727
|
}
|
|
3933
4728
|
}
|
|
@@ -3945,6 +4740,46 @@ async function databasesRow(state, client) {
|
|
|
3945
4740
|
const names = state.databases.map((record) => record.name).join(", ");
|
|
3946
4741
|
return ok("databases", label, `${state.databases.length} connected: ${names}`);
|
|
3947
4742
|
}
|
|
4743
|
+
const SAMPLE_DATABASE_STEP = "sample-database";
|
|
4744
|
+
const SAMPLE_LABEL = "sample database";
|
|
4745
|
+
async function sampleDatabaseRow(record, deps, dockerOk, client) {
|
|
4746
|
+
const id = "sample-database";
|
|
4747
|
+
if (!dockerOk) return skipped(id, SAMPLE_LABEL, "Docker is not available");
|
|
4748
|
+
const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
|
|
4749
|
+
if (!status.installed) return failed(id, SAMPLE_LABEL, {
|
|
4750
|
+
detail: `${record.containerName} does not exist`,
|
|
4751
|
+
hint: "`rde init` recreates it",
|
|
4752
|
+
fix: SAMPLE_DATABASE_STEP
|
|
4753
|
+
});
|
|
4754
|
+
if (!status.running) return failed(id, SAMPLE_LABEL, {
|
|
4755
|
+
detail: `${record.containerName} is stopped`,
|
|
4756
|
+
hint: "`rde start`",
|
|
4757
|
+
fix: "start-metabase"
|
|
4758
|
+
});
|
|
4759
|
+
if (await deps.ports.isFree(record.port)) return failed(id, SAMPLE_LABEL, {
|
|
4760
|
+
detail: `${record.containerName} runs but port ${record.port} is not answering`,
|
|
4761
|
+
hint: `\`docker logs ${record.containerName}\` shows what Postgres is doing`,
|
|
4762
|
+
fix: null
|
|
4763
|
+
});
|
|
4764
|
+
const where = `${record.containerName} running on ${LOOPBACK_HOST}:${record.port}`;
|
|
4765
|
+
if (record.databaseId === null) return failed(id, SAMPLE_LABEL, {
|
|
4766
|
+
detail: `${where}; not connected to Metabase yet`,
|
|
4767
|
+
hint: "`rde init` connects it",
|
|
4768
|
+
fix: SAMPLE_DATABASE_STEP
|
|
4769
|
+
});
|
|
4770
|
+
if (client === null) return skipped(id, SAMPLE_LABEL, `${where}; ${NOT_HEALTHY}, so the connection is unchecked`);
|
|
4771
|
+
try {
|
|
4772
|
+
const database = await client.database.get(record.databaseId);
|
|
4773
|
+
return ok(id, SAMPLE_LABEL, `${where}, database ${database.id} sync ${database.initial_sync_status}`);
|
|
4774
|
+
} catch (error) {
|
|
4775
|
+
if (isNotFound$2(error)) return failed(id, SAMPLE_LABEL, {
|
|
4776
|
+
detail: `${where}; database ${record.databaseId} is gone from Metabase`,
|
|
4777
|
+
hint: "`rde init` connects it again",
|
|
4778
|
+
fix: SAMPLE_DATABASE_STEP
|
|
4779
|
+
});
|
|
4780
|
+
throw error;
|
|
4781
|
+
}
|
|
4782
|
+
}
|
|
3948
4783
|
function isMbFailure$1(error) {
|
|
3949
4784
|
return error instanceof ProcessNotFoundError || error instanceof ExternalToolError;
|
|
3950
4785
|
}
|
|
@@ -3993,66 +4828,56 @@ async function skillRows(state, deps) {
|
|
|
3993
4828
|
return ok(skillRowId(agent), agentLabel, entry.path);
|
|
3994
4829
|
});
|
|
3995
4830
|
}
|
|
3996
|
-
function
|
|
3997
|
-
const configured = configuredRuntime(state);
|
|
3998
|
-
if (configured === null || configured.runtime.kind !== "docker") return null;
|
|
3999
|
-
return {
|
|
4000
|
-
runtime: configured.runtime,
|
|
4001
|
-
url: configured.url
|
|
4002
|
-
};
|
|
4003
|
-
}
|
|
4004
|
-
function noRuntimeRows(state) {
|
|
4005
|
-
const detail = state.runtime?.kind === "jar" ? "the jar runtime is not available yet" : NO_INSTANCE;
|
|
4831
|
+
function noRuntimeRows() {
|
|
4006
4832
|
return [
|
|
4007
|
-
"
|
|
4833
|
+
"runtime",
|
|
4008
4834
|
"port",
|
|
4009
4835
|
"health"
|
|
4010
|
-
].map((id) => skipped(id, id,
|
|
4836
|
+
].map((id) => skipped(id, id, NO_INSTANCE));
|
|
4011
4837
|
}
|
|
4012
4838
|
async function instanceRows(state, deps, dockerOk, mbInstalled) {
|
|
4013
|
-
const
|
|
4014
|
-
if (
|
|
4015
|
-
...noRuntimeRows(
|
|
4839
|
+
const configured = configuredRuntime(state);
|
|
4840
|
+
if (configured === null) return [
|
|
4841
|
+
...noRuntimeRows(),
|
|
4842
|
+
skipped("version", "Metabase version", NO_INSTANCE),
|
|
4016
4843
|
skipped("admin", "admin user", NO_INSTANCE),
|
|
4017
4844
|
apiKeyRow(state),
|
|
4018
4845
|
skipped("mb-profile", "mb profile", NO_INSTANCE),
|
|
4019
4846
|
skipped("license", "license", NO_INSTANCE),
|
|
4020
4847
|
skipped("databases", "databases", NO_INSTANCE),
|
|
4848
|
+
skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE),
|
|
4021
4849
|
skipped("git-sync", "remote sync", NO_INSTANCE)
|
|
4022
4850
|
];
|
|
4023
|
-
const
|
|
4024
|
-
const { row: health, healthy } = await healthRow(
|
|
4851
|
+
const runtime = await runtimeRow(configured.runtime, deps, dockerOk);
|
|
4852
|
+
const { row: health, healthy } = await healthRow(configured.url, deps);
|
|
4853
|
+
const properties = healthy ? await deps.client(configured.url, NO_AUTH).session.properties() : null;
|
|
4025
4854
|
const rows = [
|
|
4026
|
-
|
|
4027
|
-
await portRow(
|
|
4855
|
+
runtime.row,
|
|
4856
|
+
await portRow(configured.runtime.port, runtime, deps.ports),
|
|
4028
4857
|
health,
|
|
4029
|
-
|
|
4858
|
+
properties === null ? skipped("version", "Metabase version", NOT_HEALTHY) : versionRow(state, properties),
|
|
4859
|
+
properties === null ? skipped("admin", "admin user", NOT_HEALTHY) : adminRow(state, properties),
|
|
4030
4860
|
apiKeyRow(state),
|
|
4031
4861
|
await mbProfileRow(state, deps, mbInstalled, healthy)
|
|
4032
4862
|
];
|
|
4033
|
-
|
|
4863
|
+
const client = healthy && state.apiKey !== null ? deps.client(configured.url, {
|
|
4864
|
+
kind: "api-key",
|
|
4865
|
+
key: state.apiKey
|
|
4866
|
+
}) : null;
|
|
4867
|
+
if (client === null) {
|
|
4034
4868
|
const reason = healthy ? "no API key" : NOT_HEALTHY;
|
|
4035
4869
|
rows.push(skipped("license", "license", reason), skipped("databases", "databases", reason));
|
|
4036
|
-
} else
|
|
4037
|
-
|
|
4038
|
-
kind: "api-key",
|
|
4039
|
-
key: state.apiKey
|
|
4040
|
-
});
|
|
4041
|
-
rows.push(await licenseRow(client), await databasesRow(state, client));
|
|
4042
|
-
}
|
|
4870
|
+
} else rows.push(await licenseRow(client), await databasesRow(state, client));
|
|
4871
|
+
rows.push(state.sampleDatabase === null ? skipped("sample-database", SAMPLE_LABEL, "not connected; `rde init` adds one") : await sampleDatabaseRow(state.sampleDatabase, deps, dockerOk, client));
|
|
4043
4872
|
rows.push(await gitSyncRow(state, deps));
|
|
4044
4873
|
return rows;
|
|
4045
4874
|
}
|
|
4046
4875
|
async function collectDoctor(instance, state, deps) {
|
|
4047
|
-
const machine = await machineRows(deps);
|
|
4876
|
+
const machine = await machineRows(deps, state?.runtime ?? null);
|
|
4048
4877
|
const dockerOk = machine.some((row) => row.id === "docker" && row.status === "ok");
|
|
4049
4878
|
const mb = await mbRow(deps);
|
|
4050
4879
|
const checks = [...machine, mb];
|
|
4051
|
-
if (state === null) checks.push(...
|
|
4052
|
-
"container",
|
|
4053
|
-
"port",
|
|
4054
|
-
"health"
|
|
4055
|
-
].map((id) => skipped(id, id, NO_INSTANCE)), skipped("admin", "admin user", NO_INSTANCE), skipped("api-key", "API key", NO_INSTANCE), skipped("mb-profile", "mb profile", NO_INSTANCE), skipped("license", "license", NO_INSTANCE), skipped("databases", "databases", NO_INSTANCE), skipped("git-sync", "remote sync", NO_INSTANCE));
|
|
4880
|
+
if (state === null) checks.push(...noRuntimeRows(), skipped("version", "Metabase version", NO_INSTANCE), skipped("admin", "admin user", NO_INSTANCE), skipped("api-key", "API key", NO_INSTANCE), skipped("mb-profile", "mb profile", NO_INSTANCE), skipped("license", "license", NO_INSTANCE), skipped("databases", "databases", NO_INSTANCE), skipped("sample-database", SAMPLE_LABEL, NO_INSTANCE), skipped("git-sync", "remote sync", NO_INSTANCE));
|
|
4056
4881
|
else checks.push(...await instanceRows(state, deps, dockerOk, mb.status === "ok"));
|
|
4057
4882
|
checks.push(...await skillRows(state, deps));
|
|
4058
4883
|
return {
|
|
@@ -4109,13 +4934,12 @@ async function fixDoctor(report, steps, ctx, options) {
|
|
|
4109
4934
|
}
|
|
4110
4935
|
//#endregion
|
|
4111
4936
|
//#region src/steps/admin-user.ts
|
|
4112
|
-
const STEP_ID$
|
|
4937
|
+
const STEP_ID$6 = "admin-user";
|
|
4113
4938
|
const ADMIN_USER_KEYS = {
|
|
4114
|
-
|
|
4115
|
-
|
|
4116
|
-
|
|
4117
|
-
|
|
4118
|
-
lastName: `${STEP_ID$5}.last-name`
|
|
4939
|
+
email: `${STEP_ID$6}.email`,
|
|
4940
|
+
password: `${STEP_ID$6}.password`,
|
|
4941
|
+
firstName: `${STEP_ID$6}.first-name`,
|
|
4942
|
+
lastName: `${STEP_ID$6}.last-name`
|
|
4119
4943
|
};
|
|
4120
4944
|
const DEFAULT_ADMIN_EMAIL = "admin@metabase.com";
|
|
4121
4945
|
const DEFAULT_ADMIN_PASSWORD = "metabase123";
|
|
@@ -4123,16 +4947,8 @@ const DEFAULT_SITE_NAME = "rde";
|
|
|
4123
4947
|
const MIN_PASSWORD_LENGTH = 6;
|
|
4124
4948
|
const DIGIT = /\d/;
|
|
4125
4949
|
const EMAIL = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
4126
|
-
const
|
|
4127
|
-
|
|
4128
|
-
label: "Use default credentials",
|
|
4129
|
-
hint: `${DEFAULT_ADMIN_EMAIL} / ${DEFAULT_ADMIN_PASSWORD}, saved in ~/.rde`
|
|
4130
|
-
}, {
|
|
4131
|
-
value: "prompted",
|
|
4132
|
-
label: "Enter my own",
|
|
4133
|
-
hint: "never saved"
|
|
4134
|
-
}];
|
|
4135
|
-
function hostDeps$6(ctx) {
|
|
4950
|
+
const DEFAULT_ADMIN = { variant: "default" };
|
|
4951
|
+
function hostDeps$8(ctx) {
|
|
4136
4952
|
return { client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal) };
|
|
4137
4953
|
}
|
|
4138
4954
|
function isNotReady(error) {
|
|
@@ -4194,9 +5010,9 @@ function credentialsRecord(kind, user) {
|
|
|
4194
5010
|
};
|
|
4195
5011
|
}
|
|
4196
5012
|
}
|
|
4197
|
-
function adminUserStep(deps = hostDeps$
|
|
5013
|
+
function adminUserStep(options, deps = hostDeps$8) {
|
|
4198
5014
|
return {
|
|
4199
|
-
id: STEP_ID$
|
|
5015
|
+
id: STEP_ID$6,
|
|
4200
5016
|
title: "Create the admin user",
|
|
4201
5017
|
async detect(ctx) {
|
|
4202
5018
|
const configured = configuredRuntime(ctx.state);
|
|
@@ -4218,12 +5034,7 @@ function adminUserStep(deps = hostDeps$6) {
|
|
|
4218
5034
|
}
|
|
4219
5035
|
const token = properties["setup-token"];
|
|
4220
5036
|
if (token === null) throw new ConfigError("Metabase has no admin user yet but offers no setup token");
|
|
4221
|
-
const
|
|
4222
|
-
key: ADMIN_USER_KEYS.variant,
|
|
4223
|
-
message: "Admin credentials",
|
|
4224
|
-
choices: CREDENTIALS_CHOICES
|
|
4225
|
-
});
|
|
4226
|
-
const user = await chooseUser(ctx, kind);
|
|
5037
|
+
const user = await chooseUser(ctx, options.variant);
|
|
4227
5038
|
const session = await client.setup.create({
|
|
4228
5039
|
token,
|
|
4229
5040
|
user,
|
|
@@ -4232,7 +5043,7 @@ function adminUserStep(deps = hostDeps$6) {
|
|
|
4232
5043
|
ctx.session.metabaseSession.set(session.id);
|
|
4233
5044
|
await ctx.saveState({
|
|
4234
5045
|
...ctx.state,
|
|
4235
|
-
admin: credentialsRecord(
|
|
5046
|
+
admin: credentialsRecord(options.variant, user)
|
|
4236
5047
|
});
|
|
4237
5048
|
ctx.ui.success(`Admin user ${user.email} created`);
|
|
4238
5049
|
return "completed";
|
|
@@ -4241,14 +5052,14 @@ function adminUserStep(deps = hostDeps$6) {
|
|
|
4241
5052
|
}
|
|
4242
5053
|
//#endregion
|
|
4243
5054
|
//#region src/steps/api-key.ts
|
|
4244
|
-
const STEP_ID$
|
|
5055
|
+
const STEP_ID$5 = "api-key";
|
|
4245
5056
|
const API_KEY_KEYS = {
|
|
4246
|
-
email: `${STEP_ID$
|
|
4247
|
-
password: `${STEP_ID$
|
|
4248
|
-
profile: `${STEP_ID$
|
|
5057
|
+
email: `${STEP_ID$5}.email`,
|
|
5058
|
+
password: `${STEP_ID$5}.password`,
|
|
5059
|
+
profile: `${STEP_ID$5}.profile`
|
|
4249
5060
|
};
|
|
4250
5061
|
const ADMINISTRATORS_GROUP_NAME = "Administrators";
|
|
4251
|
-
function hostDeps$
|
|
5062
|
+
function hostDeps$7(ctx) {
|
|
4252
5063
|
return {
|
|
4253
5064
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
4254
5065
|
runner: processRunner(ctx.signal)
|
|
@@ -4342,9 +5153,9 @@ function usageNote(profile) {
|
|
|
4342
5153
|
if (profile === "default") return `${mbCommand(profile)} <command> uses this instance`;
|
|
4343
5154
|
return `${mbCommand(profile)} <command>\nMB_PROFILE=${profile} for agents and scripts`;
|
|
4344
5155
|
}
|
|
4345
|
-
function apiKeyStep(deps = hostDeps$
|
|
5156
|
+
function apiKeyStep(deps = hostDeps$7) {
|
|
4346
5157
|
return {
|
|
4347
|
-
id: STEP_ID$
|
|
5158
|
+
id: STEP_ID$5,
|
|
4348
5159
|
title: "Connect the Metabase CLI",
|
|
4349
5160
|
async detect(ctx) {
|
|
4350
5161
|
if (ctx.state.apiKey === null) return "todo";
|
|
@@ -4379,13 +5190,13 @@ function apiKeyStep(deps = hostDeps$5) {
|
|
|
4379
5190
|
}
|
|
4380
5191
|
//#endregion
|
|
4381
5192
|
//#region src/steps/git-sync.ts
|
|
4382
|
-
const STEP_ID$
|
|
5193
|
+
const STEP_ID$4 = "git-sync";
|
|
4383
5194
|
const GIT_SYNC_KEYS = {
|
|
4384
|
-
location: `${STEP_ID$
|
|
4385
|
-
path: `${STEP_ID$
|
|
4386
|
-
remote: `${STEP_ID$
|
|
4387
|
-
url: `${STEP_ID$
|
|
4388
|
-
token: `${STEP_ID$
|
|
5195
|
+
location: `${STEP_ID$4}.location`,
|
|
5196
|
+
path: `${STEP_ID$4}.path`,
|
|
5197
|
+
remote: `${STEP_ID$4}.remote`,
|
|
5198
|
+
url: `${STEP_ID$4}.url`,
|
|
5199
|
+
token: `${STEP_ID$4}.token`
|
|
4389
5200
|
};
|
|
4390
5201
|
const BARE_REPO_NAME = "content.git";
|
|
4391
5202
|
const DEFAULT_BRANCH = "main";
|
|
@@ -4405,7 +5216,7 @@ const TERMINAL_TASK_STATUSES = /* @__PURE__ */ new Set([
|
|
|
4405
5216
|
"timed-out",
|
|
4406
5217
|
"conflict"
|
|
4407
5218
|
]);
|
|
4408
|
-
function hostDeps$
|
|
5219
|
+
function hostDeps$6(ctx) {
|
|
4409
5220
|
return {
|
|
4410
5221
|
runner: processRunner(ctx.signal),
|
|
4411
5222
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -4738,7 +5549,7 @@ async function importExistingContent(run, gitRemote) {
|
|
|
4738
5549
|
spinner.stop("Import finished");
|
|
4739
5550
|
reportImport(run, outcome);
|
|
4740
5551
|
}
|
|
4741
|
-
function usageLines(state, gitRemote) {
|
|
5552
|
+
function usageLines$1(state, gitRemote) {
|
|
4742
5553
|
const profile = state.mbProfile;
|
|
4743
5554
|
return [
|
|
4744
5555
|
`cd ${gitRemote.workdir}`,
|
|
@@ -4785,16 +5596,16 @@ async function runGitSync(ctx, deps) {
|
|
|
4785
5596
|
const remote = await configureRemote(run, configured.runtime, flow.model);
|
|
4786
5597
|
await applySettings(run, remote);
|
|
4787
5598
|
await importExistingContent(run, remote.gitRemote);
|
|
4788
|
-
ctx.ui.note(usageLines(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
5599
|
+
ctx.ui.note(usageLines$1(ctx.state, remote.gitRemote).join("\n"), "Working with the repository");
|
|
4789
5600
|
return "completed";
|
|
4790
5601
|
}
|
|
4791
|
-
function gitSyncStep(deps = hostDeps$
|
|
5602
|
+
function gitSyncStep(deps = hostDeps$6) {
|
|
4792
5603
|
return {
|
|
4793
|
-
id: STEP_ID$
|
|
5604
|
+
id: STEP_ID$4,
|
|
4794
5605
|
title: "Connect a git repository",
|
|
4795
5606
|
async detect(ctx) {
|
|
4796
5607
|
if (!hasRemoteSync(ctx.state.license)) return "not-applicable";
|
|
4797
|
-
if (ctx.state.steps[STEP_ID$
|
|
5608
|
+
if (ctx.state.steps[STEP_ID$4]?.status === "skipped") return "not-applicable";
|
|
4798
5609
|
if (ctx.state.gitRemote === null) return "todo";
|
|
4799
5610
|
try {
|
|
4800
5611
|
return (await mbTool(deps(ctx).runner).gitSyncStatus(ctx.state.mbProfile)).branch === null ? "todo" : "done";
|
|
@@ -4810,22 +5621,22 @@ function gitSyncStep(deps = hostDeps$4) {
|
|
|
4810
5621
|
}
|
|
4811
5622
|
//#endregion
|
|
4812
5623
|
//#region src/steps/license.ts
|
|
4813
|
-
const STEP_ID$
|
|
4814
|
-
const LICENSE_KEYS = { token: `${STEP_ID$
|
|
5624
|
+
const STEP_ID$3 = "license";
|
|
5625
|
+
const LICENSE_KEYS = { token: `${STEP_ID$3}.token` };
|
|
4815
5626
|
const TOKEN_PATTERN = /^[0-9a-f]{64}$/i;
|
|
4816
|
-
const NOT_FOUND_STATUS = 404;
|
|
4817
|
-
function hostDeps$
|
|
5627
|
+
const NOT_FOUND_STATUS$1 = 404;
|
|
5628
|
+
function hostDeps$5(ctx) {
|
|
4818
5629
|
return { client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal) };
|
|
4819
5630
|
}
|
|
4820
5631
|
function isNoTokenYet(error) {
|
|
4821
|
-
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS;
|
|
5632
|
+
if (error instanceof MetabaseApiError) return error.status === NOT_FOUND_STATUS$1;
|
|
4822
5633
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
4823
5634
|
}
|
|
4824
5635
|
function validateToken(value) {
|
|
4825
5636
|
if (value.length === 0 || TOKEN_PATTERN.test(value)) return;
|
|
4826
5637
|
return "A license token is 64 hexadecimal characters";
|
|
4827
5638
|
}
|
|
4828
|
-
function requireApiKey(ctx) {
|
|
5639
|
+
function requireApiKey$1(ctx) {
|
|
4829
5640
|
if (ctx.state.apiKey === null) throw new ConfigError("no API key yet; the api-key step comes first");
|
|
4830
5641
|
return ctx.state.apiKey;
|
|
4831
5642
|
}
|
|
@@ -4833,12 +5644,12 @@ function activeLabel(status) {
|
|
|
4833
5644
|
const plan = status["plan-alias"];
|
|
4834
5645
|
return plan === void 0 ? "License active" : `License active (plan ${plan})`;
|
|
4835
5646
|
}
|
|
4836
|
-
function licenseStep(deps = hostDeps$
|
|
5647
|
+
function licenseStep(deps = hostDeps$5) {
|
|
4837
5648
|
return {
|
|
4838
|
-
id: STEP_ID$
|
|
5649
|
+
id: STEP_ID$3,
|
|
4839
5650
|
title: "Activate your license",
|
|
4840
5651
|
async detect(ctx) {
|
|
4841
|
-
if (ctx.state.steps[STEP_ID$
|
|
5652
|
+
if (ctx.state.steps[STEP_ID$3]?.status === "skipped") return "not-applicable";
|
|
4842
5653
|
const configured = configuredRuntime(ctx.state);
|
|
4843
5654
|
if (configured === null || ctx.state.apiKey === null) return "todo";
|
|
4844
5655
|
const client = deps(ctx).client(configured.url, {
|
|
@@ -4854,7 +5665,7 @@ function licenseStep(deps = hostDeps$3) {
|
|
|
4854
5665
|
},
|
|
4855
5666
|
async run(ctx) {
|
|
4856
5667
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
4857
|
-
const apiKey = requireApiKey(ctx);
|
|
5668
|
+
const apiKey = requireApiKey$1(ctx);
|
|
4858
5669
|
const token = await ctx.prompts.password({
|
|
4859
5670
|
key: LICENSE_KEYS.token,
|
|
4860
5671
|
message: "Paste your Metabase license token (Enter to skip)",
|
|
@@ -4898,242 +5709,343 @@ function licenseStep(deps = hostDeps$3) {
|
|
|
4898
5709
|
};
|
|
4899
5710
|
}
|
|
4900
5711
|
//#endregion
|
|
4901
|
-
//#region src/runtime
|
|
4902
|
-
const
|
|
4903
|
-
const
|
|
4904
|
-
const
|
|
4905
|
-
const
|
|
4906
|
-
const
|
|
4907
|
-
|
|
4908
|
-
|
|
5712
|
+
//#region src/steps/runtime.ts
|
|
5713
|
+
const STEP_ID$2 = "runtime";
|
|
5714
|
+
const MIN_PORT = 1;
|
|
5715
|
+
const MAX_PORT = 65535;
|
|
5716
|
+
const PORT_PATTERN = /^\d+$/;
|
|
5717
|
+
const RUNTIME_NAMES = {
|
|
5718
|
+
docker: "Docker",
|
|
5719
|
+
jar: "jar"
|
|
5720
|
+
};
|
|
5721
|
+
const NO_RUNTIME_FLAGS = {
|
|
5722
|
+
kind: null,
|
|
5723
|
+
image: null,
|
|
5724
|
+
jar: null,
|
|
5725
|
+
port: null
|
|
5726
|
+
};
|
|
5727
|
+
function hostDeps$4(ctx) {
|
|
5728
|
+
return {
|
|
5729
|
+
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
5730
|
+
ports: hostPortProbe,
|
|
5731
|
+
releases: releaseLookup(ctx.signal)
|
|
5732
|
+
};
|
|
4909
5733
|
}
|
|
4910
|
-
function
|
|
4911
|
-
return
|
|
5734
|
+
function parsePort(value) {
|
|
5735
|
+
if (!PORT_PATTERN.test(value)) return null;
|
|
5736
|
+
const port = Number.parseInt(value, 10);
|
|
5737
|
+
return port >= MIN_PORT && port <= MAX_PORT ? port : null;
|
|
4912
5738
|
}
|
|
4913
|
-
function
|
|
4914
|
-
|
|
5739
|
+
function javaReason(java) {
|
|
5740
|
+
if (!java.found) return java.reason;
|
|
5741
|
+
return java.major >= 21 ? null : `Java ${java.version} is older than 21`;
|
|
4915
5742
|
}
|
|
4916
|
-
function
|
|
4917
|
-
|
|
5743
|
+
function unavailableReason(kind, available) {
|
|
5744
|
+
switch (kind) {
|
|
5745
|
+
case "docker": return available.docker.available ? null : available.docker.reason;
|
|
5746
|
+
case "jar": return javaReason(available.java);
|
|
5747
|
+
}
|
|
4918
5748
|
}
|
|
4919
|
-
function
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
5749
|
+
function javaLabel(java) {
|
|
5750
|
+
return java.found ? `Java ${java.version}` : "Java";
|
|
5751
|
+
}
|
|
5752
|
+
async function availability(runtime) {
|
|
5753
|
+
return {
|
|
5754
|
+
docker: await runtime.docker.available(),
|
|
5755
|
+
java: await runtime.java.find()
|
|
5756
|
+
};
|
|
5757
|
+
}
|
|
5758
|
+
async function chooseKind(ctx, options, runtime) {
|
|
5759
|
+
const available = await availability(runtime);
|
|
5760
|
+
if (options.kind !== null) {
|
|
5761
|
+
const reason = unavailableReason(options.kind, available);
|
|
5762
|
+
if (reason !== null) throw new ConfigError(`--runtime ${options.kind} is not usable: ${reason}`);
|
|
5763
|
+
ctx.ui.info(`Using the ${RUNTIME_NAMES[options.kind]} runtime from --runtime`);
|
|
5764
|
+
return options.kind;
|
|
4924
5765
|
}
|
|
4925
|
-
|
|
4926
|
-
|
|
5766
|
+
if (available.docker.available) {
|
|
5767
|
+
ctx.ui.info(`Using Docker ${available.docker.serverVersion}`);
|
|
5768
|
+
return "docker";
|
|
4927
5769
|
}
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
4932
|
-
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
await docker.containerCreate({
|
|
4938
|
-
name: state.containerName,
|
|
4939
|
-
image: state.image,
|
|
4940
|
-
port: state.port,
|
|
4941
|
-
dataDir: deps.dataDir,
|
|
4942
|
-
addHostGateway: deps.platform === "linux",
|
|
4943
|
-
hostUser: hostUserFor(deps.platform, deps.hostUser)
|
|
4944
|
-
});
|
|
5770
|
+
const javaProblem = javaReason(available.java);
|
|
5771
|
+
if (javaProblem !== null) throw new ConfigError(`neither Docker nor Java can run Metabase here: ${available.docker.reason}; ${javaProblem}`);
|
|
5772
|
+
ctx.ui.info(`Docker is not available (${available.docker.reason}); using the jar runtime with ${javaLabel(available.java)}`);
|
|
5773
|
+
return "jar";
|
|
5774
|
+
}
|
|
5775
|
+
function chooseImage(ctx, options) {
|
|
5776
|
+
if (options.image !== null) {
|
|
5777
|
+
ctx.ui.info(`Using image ${options.image} from --image`);
|
|
5778
|
+
return options.image;
|
|
4945
5779
|
}
|
|
4946
|
-
|
|
4947
|
-
|
|
4948
|
-
|
|
4949
|
-
|
|
4950
|
-
|
|
5780
|
+
return DEFAULT_IMAGE;
|
|
5781
|
+
}
|
|
5782
|
+
async function resolveJar(ctx, spec, releases) {
|
|
5783
|
+
const parsed = parseJarSpec(spec);
|
|
5784
|
+
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`);
|
|
5785
|
+
if (parsed.kind !== "latest") return parsed;
|
|
5786
|
+
const version = await releases.latestVersion();
|
|
5787
|
+
ctx.ui.info(`The latest Metabase release is ${version}`);
|
|
5788
|
+
return {
|
|
5789
|
+
kind: "release",
|
|
5790
|
+
version
|
|
5791
|
+
};
|
|
5792
|
+
}
|
|
5793
|
+
async function chooseJar(ctx, options, releases) {
|
|
5794
|
+
if (options.jar !== null) {
|
|
5795
|
+
ctx.ui.info(`Using ${options.jar} from --jar`);
|
|
5796
|
+
return resolveJar(ctx, options.jar, releases);
|
|
4951
5797
|
}
|
|
4952
|
-
|
|
4953
|
-
|
|
4954
|
-
|
|
4955
|
-
|
|
5798
|
+
return resolveJar(ctx, LATEST, releases);
|
|
5799
|
+
}
|
|
5800
|
+
async function choosePort(ctx, options, ports) {
|
|
5801
|
+
if (options.port !== null) {
|
|
5802
|
+
if (!await ports.isFree(options.port)) throw new ConfigError(`port ${options.port} from --port is in use by another program`);
|
|
5803
|
+
ctx.ui.info(`Using port ${options.port} from --port`);
|
|
5804
|
+
return options.port;
|
|
4956
5805
|
}
|
|
4957
|
-
|
|
4958
|
-
|
|
4959
|
-
|
|
4960
|
-
|
|
4961
|
-
|
|
4962
|
-
|
|
5806
|
+
const free = await ports.findFree(DEFAULT_PORT);
|
|
5807
|
+
if (free !== 3100) ctx.ui.warn(`Port ${DEFAULT_PORT} is in use; using ${free}`);
|
|
5808
|
+
return free;
|
|
5809
|
+
}
|
|
5810
|
+
async function chooseRuntime(ctx, options, deps) {
|
|
5811
|
+
const kind = await chooseKind(ctx, options, deps.runtime);
|
|
5812
|
+
switch (kind) {
|
|
5813
|
+
case "docker": {
|
|
5814
|
+
const image = chooseImage(ctx, options);
|
|
5815
|
+
const port = await choosePort(ctx, options, deps.ports);
|
|
5816
|
+
return {
|
|
5817
|
+
kind,
|
|
5818
|
+
image,
|
|
5819
|
+
containerName: containerName(ctx.state.name),
|
|
5820
|
+
port,
|
|
5821
|
+
hostAlias: DOCKER_HOST_ALIAS
|
|
5822
|
+
};
|
|
5823
|
+
}
|
|
5824
|
+
case "jar": return {
|
|
5825
|
+
kind,
|
|
5826
|
+
source: await chooseJar(ctx, options, deps.releases),
|
|
5827
|
+
port: await choosePort(ctx, options, deps.ports)
|
|
4963
5828
|
};
|
|
4964
5829
|
}
|
|
4965
|
-
|
|
4966
|
-
|
|
4967
|
-
|
|
5830
|
+
}
|
|
5831
|
+
function describeRuntime(runtime) {
|
|
5832
|
+
switch (runtime.kind) {
|
|
5833
|
+
case "docker": return runtime.image;
|
|
5834
|
+
case "jar": return describeJar(runtime.source);
|
|
4968
5835
|
}
|
|
4969
|
-
return {
|
|
4970
|
-
kind: "docker",
|
|
4971
|
-
install,
|
|
4972
|
-
start,
|
|
4973
|
-
stop,
|
|
4974
|
-
status,
|
|
4975
|
-
logs,
|
|
4976
|
-
url: () => url,
|
|
4977
|
-
hostAlias: () => state.hostAlias
|
|
4978
|
-
};
|
|
4979
5836
|
}
|
|
4980
|
-
|
|
4981
|
-
|
|
4982
|
-
|
|
4983
|
-
|
|
5837
|
+
async function persistedRuntime(ctx, options, deps) {
|
|
5838
|
+
const existing = configuredRuntime(ctx.state);
|
|
5839
|
+
if (existing !== null) {
|
|
5840
|
+
ctx.ui.info(`Reinstalling Metabase from the saved choices: ${describeRuntime(existing.runtime)} on port ${existing.runtime.port}`);
|
|
5841
|
+
return existing.runtime;
|
|
5842
|
+
}
|
|
5843
|
+
const chosen = await chooseRuntime(ctx, options, deps);
|
|
5844
|
+
await ctx.saveState({
|
|
5845
|
+
...ctx.state,
|
|
5846
|
+
runtime: chosen,
|
|
5847
|
+
url: instanceUrl(chosen.port)
|
|
5848
|
+
});
|
|
5849
|
+
return chosen;
|
|
5850
|
+
}
|
|
5851
|
+
function runtimeStep(options, deps = hostDeps$4) {
|
|
4984
5852
|
return {
|
|
4985
|
-
|
|
4986
|
-
|
|
4987
|
-
|
|
4988
|
-
|
|
4989
|
-
|
|
4990
|
-
|
|
5853
|
+
id: STEP_ID$2,
|
|
5854
|
+
title: "Install Metabase",
|
|
5855
|
+
async detect(ctx) {
|
|
5856
|
+
const existing = configuredRuntime(ctx.state);
|
|
5857
|
+
if (existing === null) return "todo";
|
|
5858
|
+
return (await runtimeFromState(existing.runtime, deps(ctx).runtime).status()).installed ? "done" : "todo";
|
|
5859
|
+
},
|
|
5860
|
+
async run(ctx) {
|
|
5861
|
+
const resolved = deps(ctx);
|
|
5862
|
+
const chosen = await persistedRuntime(ctx, options, resolved);
|
|
5863
|
+
const runtime = runtimeFromState(chosen, resolved.runtime);
|
|
5864
|
+
const spinner = ctx.ui.spinner(`Installing Metabase from ${describeRuntime(chosen)}`);
|
|
5865
|
+
try {
|
|
5866
|
+
await runtime.install({ onProgress: (line) => spinner.update(line) });
|
|
5867
|
+
} catch (error) {
|
|
5868
|
+
spinner.fail("Metabase install failed");
|
|
5869
|
+
throw error;
|
|
5870
|
+
}
|
|
5871
|
+
spinner.stop(`Metabase installed on port ${chosen.port}`);
|
|
5872
|
+
return "completed";
|
|
4991
5873
|
}
|
|
4992
5874
|
};
|
|
4993
5875
|
}
|
|
4994
|
-
function runtimeFromState(state, deps) {
|
|
4995
|
-
switch (state.kind) {
|
|
4996
|
-
case "docker": return dockerRuntime(state, deps);
|
|
4997
|
-
case "jar": throw new ConfigError("jar runtime is not available yet");
|
|
4998
|
-
}
|
|
4999
|
-
}
|
|
5000
5876
|
//#endregion
|
|
5001
|
-
//#region src/steps/
|
|
5002
|
-
const STEP_ID$1 = "
|
|
5003
|
-
const
|
|
5004
|
-
|
|
5005
|
-
|
|
5006
|
-
|
|
5007
|
-
};
|
|
5008
|
-
const MIN_PORT = 1;
|
|
5009
|
-
const MAX_PORT = 65535;
|
|
5010
|
-
const PORT_PATTERN = /^\d+$/;
|
|
5011
|
-
const RUNTIME_CHOICES = [{
|
|
5012
|
-
value: "docker",
|
|
5013
|
-
label: "Docker",
|
|
5014
|
-
hint: "recommended"
|
|
5015
|
-
}, {
|
|
5016
|
-
value: "jar",
|
|
5017
|
-
label: "Jar",
|
|
5018
|
-
hint: "coming soon"
|
|
5019
|
-
}];
|
|
5020
|
-
function hostDeps$2(ctx) {
|
|
5877
|
+
//#region src/steps/sample-database.ts
|
|
5878
|
+
const STEP_ID$1 = "sample-database";
|
|
5879
|
+
const SAMPLE_DATABASE_DISPLAY_NAME = "Sample Database";
|
|
5880
|
+
const POSTGRES_ENGINE = "postgres";
|
|
5881
|
+
const NOT_FOUND_STATUS = 404;
|
|
5882
|
+
function hostDeps$3(ctx) {
|
|
5021
5883
|
return {
|
|
5022
5884
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
5023
|
-
ports: hostPortProbe
|
|
5885
|
+
ports: hostPortProbe,
|
|
5886
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5887
|
+
readyTimeoutMs: SAMPLE_DATABASE_READY_TIMEOUT_MS,
|
|
5888
|
+
now: () => /* @__PURE__ */ new Date()
|
|
5024
5889
|
};
|
|
5025
5890
|
}
|
|
5026
|
-
function
|
|
5027
|
-
|
|
5028
|
-
const port = Number.parseInt(value, 10);
|
|
5029
|
-
return port >= MIN_PORT && port <= MAX_PORT ? port : null;
|
|
5891
|
+
function isNotServingYet(error) {
|
|
5892
|
+
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError;
|
|
5030
5893
|
}
|
|
5031
|
-
function
|
|
5032
|
-
return
|
|
5894
|
+
function isNotFound$1(error) {
|
|
5895
|
+
return error instanceof MetabaseApiError && error.status === NOT_FOUND_STATUS;
|
|
5033
5896
|
}
|
|
5034
|
-
|
|
5035
|
-
|
|
5036
|
-
|
|
5037
|
-
key: RUNTIME_KEYS.kind,
|
|
5038
|
-
message: "How should Metabase run?",
|
|
5039
|
-
choices: RUNTIME_CHOICES
|
|
5040
|
-
});
|
|
5041
|
-
if (kind === "docker") return kind;
|
|
5042
|
-
ctx.ui.warn("The jar runtime is not available yet; pick Docker for now");
|
|
5043
|
-
}
|
|
5897
|
+
function requireApiKey(state) {
|
|
5898
|
+
if (state.apiKey === null) throw new ConfigError(`instance ${state.name} has no API key yet; run \`rde init\``);
|
|
5899
|
+
return state.apiKey;
|
|
5044
5900
|
}
|
|
5045
|
-
async function
|
|
5046
|
-
if (
|
|
5047
|
-
|
|
5048
|
-
return
|
|
5901
|
+
async function recordedDatabase(record, client) {
|
|
5902
|
+
if (record.databaseId === null) return null;
|
|
5903
|
+
try {
|
|
5904
|
+
return await client.database.get(record.databaseId);
|
|
5905
|
+
} catch (error) {
|
|
5906
|
+
if (isNotFound$1(error)) return null;
|
|
5907
|
+
throw error;
|
|
5049
5908
|
}
|
|
5050
|
-
return ctx.prompts.text({
|
|
5051
|
-
key: RUNTIME_KEYS.image,
|
|
5052
|
-
message: "Docker image",
|
|
5053
|
-
defaultValue: DEFAULT_IMAGE,
|
|
5054
|
-
placeholder: DEFAULT_IMAGE
|
|
5055
|
-
});
|
|
5056
5909
|
}
|
|
5057
|
-
async function
|
|
5058
|
-
|
|
5059
|
-
|
|
5060
|
-
|
|
5061
|
-
|
|
5910
|
+
async function persistedRecord(ctx, deps) {
|
|
5911
|
+
const existing = ctx.state.sampleDatabase;
|
|
5912
|
+
if (existing !== null) {
|
|
5913
|
+
if ((await sampleDatabaseContainer(existing, deps.runtime.docker).status()).installed || await deps.ports.isFree(existing.port)) return {
|
|
5914
|
+
record: existing,
|
|
5915
|
+
staleDatabaseId: null
|
|
5916
|
+
};
|
|
5917
|
+
const pointer = existing.databaseId === null ? "" : `; Metabase database ${existing.databaseId} still points at the old one`;
|
|
5918
|
+
ctx.ui.warn(`Port ${existing.port} is in use by another program; the sample database moves to a new port${pointer}`);
|
|
5919
|
+
}
|
|
5920
|
+
const port = await deps.ports.findFree(SAMPLE_DATABASE_PORT);
|
|
5921
|
+
if (port !== 15432) ctx.ui.warn(`Port ${SAMPLE_DATABASE_PORT} is in use; using ${port} for the sample database`);
|
|
5922
|
+
const record = {
|
|
5923
|
+
containerName: sampleContainerName(ctx.state.name),
|
|
5924
|
+
image: SAMPLE_DATABASE_IMAGE,
|
|
5925
|
+
port,
|
|
5926
|
+
databaseId: null
|
|
5927
|
+
};
|
|
5928
|
+
await ctx.saveState({
|
|
5929
|
+
...ctx.state,
|
|
5930
|
+
sampleDatabase: record
|
|
5931
|
+
});
|
|
5932
|
+
return {
|
|
5933
|
+
record,
|
|
5934
|
+
staleDatabaseId: existing?.databaseId ?? null
|
|
5935
|
+
};
|
|
5062
5936
|
}
|
|
5063
|
-
async function
|
|
5064
|
-
|
|
5065
|
-
|
|
5066
|
-
|
|
5067
|
-
|
|
5937
|
+
async function installAndStart(ctx, sidecar, record, deps) {
|
|
5938
|
+
const installing = ctx.ui.spinner(`Installing the sample database from ${record.image}`);
|
|
5939
|
+
try {
|
|
5940
|
+
await sidecar.install({ onProgress: (line) => installing.update(line) });
|
|
5941
|
+
} catch (error) {
|
|
5942
|
+
installing.fail("Sample database install failed");
|
|
5943
|
+
throw error;
|
|
5068
5944
|
}
|
|
5069
|
-
|
|
5070
|
-
|
|
5071
|
-
|
|
5072
|
-
|
|
5073
|
-
|
|
5074
|
-
|
|
5075
|
-
|
|
5076
|
-
validate: validatePort
|
|
5945
|
+
installing.stop(`Sample database container ${record.containerName} on port ${record.port}`);
|
|
5946
|
+
await sidecar.start();
|
|
5947
|
+
const waiting = ctx.ui.spinner("Waiting for Postgres to accept connections…");
|
|
5948
|
+
try {
|
|
5949
|
+
await sidecar.waitReady({
|
|
5950
|
+
timeoutMs: deps.readyTimeoutMs,
|
|
5951
|
+
signal: ctx.signal
|
|
5077
5952
|
});
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
ctx.ui.warn(`Port ${port} is in use by another program`);
|
|
5953
|
+
} catch (error) {
|
|
5954
|
+
waiting.fail("Postgres did not become ready");
|
|
5955
|
+
throw error;
|
|
5082
5956
|
}
|
|
5957
|
+
waiting.stop(`Postgres is ready on ${LOOPBACK_HOST}:${record.port}`);
|
|
5958
|
+
}
|
|
5959
|
+
function connectionDetails(record, configured, deps) {
|
|
5960
|
+
const alias = runtimeFromState(configured.runtime, deps.runtime).hostAlias();
|
|
5961
|
+
return applyHostAlias({
|
|
5962
|
+
host: "localhost",
|
|
5963
|
+
port: record.port,
|
|
5964
|
+
dbname: SAMPLE_DATABASE_NAME,
|
|
5965
|
+
user: SAMPLE_DATABASE_USER,
|
|
5966
|
+
password: SAMPLE_DATABASE_PASSWORD
|
|
5967
|
+
}, POSTGRES_ENGINE, alias).details;
|
|
5083
5968
|
}
|
|
5084
|
-
|
|
5085
|
-
await chooseKind(ctx);
|
|
5086
|
-
const image = await chooseImage(ctx, options);
|
|
5087
|
-
const port = await choosePort(ctx, options, ports);
|
|
5969
|
+
function toRecord(database, createdAt) {
|
|
5088
5970
|
return {
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
hostAlias: DOCKER_HOST_ALIAS
|
|
5971
|
+
id: database.id,
|
|
5972
|
+
name: database.name,
|
|
5973
|
+
engine: database.engine,
|
|
5974
|
+
createdAt
|
|
5094
5975
|
};
|
|
5095
5976
|
}
|
|
5096
|
-
function
|
|
5097
|
-
|
|
5098
|
-
|
|
5099
|
-
|
|
5977
|
+
async function connectDatabase(ctx, choice, configured, client, deps) {
|
|
5978
|
+
const { record } = choice;
|
|
5979
|
+
const known = await recordedDatabase(record, client);
|
|
5980
|
+
if (known !== null) {
|
|
5981
|
+
ctx.ui.info(`${known.name} is already connected as database ${known.id}`);
|
|
5982
|
+
return known;
|
|
5100
5983
|
}
|
|
5101
|
-
}
|
|
5102
|
-
|
|
5103
|
-
|
|
5104
|
-
|
|
5105
|
-
|
|
5106
|
-
|
|
5984
|
+
const spinner = ctx.ui.spinner(`Connecting ${SAMPLE_DATABASE_DISPLAY_NAME}…`);
|
|
5985
|
+
let database;
|
|
5986
|
+
try {
|
|
5987
|
+
database = await client.database.create({
|
|
5988
|
+
name: SAMPLE_DATABASE_DISPLAY_NAME,
|
|
5989
|
+
engine: POSTGRES_ENGINE,
|
|
5990
|
+
details: connectionDetails(record, configured, deps)
|
|
5991
|
+
});
|
|
5992
|
+
} catch (error) {
|
|
5993
|
+
spinner.fail(`Could not connect ${SAMPLE_DATABASE_DISPLAY_NAME}`);
|
|
5994
|
+
throw error;
|
|
5107
5995
|
}
|
|
5108
|
-
const
|
|
5996
|
+
const stale = /* @__PURE__ */ new Set([record.databaseId, choice.staleDatabaseId]);
|
|
5997
|
+
const others = ctx.state.databases.filter((entry) => !stale.has(entry.id));
|
|
5109
5998
|
await ctx.saveState({
|
|
5110
5999
|
...ctx.state,
|
|
5111
|
-
|
|
5112
|
-
|
|
6000
|
+
sampleDatabase: {
|
|
6001
|
+
...record,
|
|
6002
|
+
databaseId: database.id
|
|
6003
|
+
},
|
|
6004
|
+
databases: [...others, toRecord(database, deps.now().toISOString())]
|
|
5113
6005
|
});
|
|
5114
|
-
|
|
6006
|
+
spinner.stop(`Connected ${database.name} as database ${database.id}`);
|
|
6007
|
+
return database;
|
|
6008
|
+
}
|
|
6009
|
+
function usageLines(record) {
|
|
6010
|
+
return [`psql "${sampleDatabaseUrl(record.port)}"`, "Transforms are enabled; this database is where they can write."].join("\n");
|
|
5115
6011
|
}
|
|
5116
|
-
function
|
|
6012
|
+
function sampleDatabaseStep(deps = hostDeps$3) {
|
|
5117
6013
|
return {
|
|
5118
6014
|
id: STEP_ID$1,
|
|
5119
|
-
title: "
|
|
6015
|
+
title: "Connect a sample database",
|
|
5120
6016
|
async detect(ctx) {
|
|
5121
|
-
const existing = configuredRuntime(ctx.state);
|
|
5122
|
-
if (existing === null) return "todo";
|
|
5123
|
-
return (await runtimeFromState(existing.runtime, deps(ctx).runtime).status()).installed ? "done" : "todo";
|
|
5124
|
-
},
|
|
5125
|
-
async run(ctx) {
|
|
5126
6017
|
const resolved = deps(ctx);
|
|
5127
|
-
|
|
5128
|
-
const
|
|
5129
|
-
const
|
|
6018
|
+
if (!(await resolved.runtime.docker.available()).available) return "not-applicable";
|
|
6019
|
+
const record = ctx.state.sampleDatabase;
|
|
6020
|
+
const configured = configuredRuntime(ctx.state);
|
|
6021
|
+
if (record === null || configured === null || ctx.state.apiKey === null) return "todo";
|
|
6022
|
+
if (!(await sampleDatabaseContainer(record, resolved.runtime.docker).status()).installed) return "todo";
|
|
6023
|
+
const client = resolved.client(configured.url, {
|
|
6024
|
+
kind: "api-key",
|
|
6025
|
+
key: ctx.state.apiKey
|
|
6026
|
+
});
|
|
5130
6027
|
try {
|
|
5131
|
-
await
|
|
6028
|
+
return await recordedDatabase(record, client) === null ? "todo" : "done";
|
|
5132
6029
|
} catch (error) {
|
|
5133
|
-
|
|
6030
|
+
if (isNotServingYet(error)) return "todo";
|
|
5134
6031
|
throw error;
|
|
5135
6032
|
}
|
|
5136
|
-
|
|
6033
|
+
},
|
|
6034
|
+
async run(ctx) {
|
|
6035
|
+
const resolved = deps(ctx);
|
|
6036
|
+
const configured = requireConfiguredRuntime(ctx.state);
|
|
6037
|
+
const apiKey = requireApiKey(ctx.state);
|
|
6038
|
+
const docker = await resolved.runtime.docker.available();
|
|
6039
|
+
if (!docker.available) throw new ConfigError(`the sample database runs in Docker, which is not available: ${docker.reason}`);
|
|
6040
|
+
const choice = await persistedRecord(ctx, resolved);
|
|
6041
|
+
await installAndStart(ctx, sampleDatabaseContainer(choice.record, resolved.runtime.docker), choice.record, resolved);
|
|
6042
|
+
const client = resolved.client(configured.url, {
|
|
6043
|
+
kind: "api-key",
|
|
6044
|
+
key: apiKey
|
|
6045
|
+
});
|
|
6046
|
+
await connectDatabase(ctx, choice, configured, client, resolved);
|
|
6047
|
+
await client.setting.enableTransforms();
|
|
6048
|
+
ctx.ui.note(usageLines(choice.record), "Sample database");
|
|
5137
6049
|
return "completed";
|
|
5138
6050
|
}
|
|
5139
6051
|
};
|
|
@@ -5147,7 +6059,7 @@ async function startAndWaitForHealth(runtime, client, options) {
|
|
|
5147
6059
|
//#region src/steps/start-metabase.ts
|
|
5148
6060
|
const LOG_TAIL_ON_TIMEOUT = 30;
|
|
5149
6061
|
const PERCENT = 100;
|
|
5150
|
-
function hostDeps$
|
|
6062
|
+
function hostDeps$2(ctx) {
|
|
5151
6063
|
return {
|
|
5152
6064
|
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
5153
6065
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
@@ -5157,10 +6069,26 @@ function hostDeps$1(ctx) {
|
|
|
5157
6069
|
function isNotHealthy(error) {
|
|
5158
6070
|
return error instanceof MetabaseUnreachableError || error instanceof MetabaseInitializingError || error instanceof MetabaseApiError;
|
|
5159
6071
|
}
|
|
6072
|
+
async function runningVersion(client) {
|
|
6073
|
+
return (await client.session.properties()).version.tag;
|
|
6074
|
+
}
|
|
5160
6075
|
function progressLabel(progress) {
|
|
5161
6076
|
return `Starting Metabase… ${Math.round(progress * PERCENT)}%`;
|
|
5162
6077
|
}
|
|
5163
|
-
function
|
|
6078
|
+
async function startSampleDatabase(ctx, deps) {
|
|
6079
|
+
const record = ctx.state.sampleDatabase;
|
|
6080
|
+
if (record === null) return;
|
|
6081
|
+
switch (await sampleDatabaseContainer(record, deps.runtime.docker).start()) {
|
|
6082
|
+
case "missing":
|
|
6083
|
+
ctx.ui.warn(`Sample database container ${record.containerName} is missing; \`rde init\` recreates it`);
|
|
6084
|
+
return;
|
|
6085
|
+
case "started":
|
|
6086
|
+
ctx.ui.info(`Sample database container ${record.containerName} started`);
|
|
6087
|
+
return;
|
|
6088
|
+
case "running": return;
|
|
6089
|
+
}
|
|
6090
|
+
}
|
|
6091
|
+
function startMetabaseStep(deps = hostDeps$2) {
|
|
5164
6092
|
return {
|
|
5165
6093
|
id: "start-metabase",
|
|
5166
6094
|
title: "Start Metabase",
|
|
@@ -5180,6 +6108,7 @@ function startMetabaseStep(deps = hostDeps$1) {
|
|
|
5180
6108
|
const configured = requireConfiguredRuntime(ctx.state);
|
|
5181
6109
|
const runtime = runtimeFromState(configured.runtime, resolved.runtime);
|
|
5182
6110
|
const client = resolved.client(configured.url, NO_AUTH);
|
|
6111
|
+
await startSampleDatabase(ctx, resolved);
|
|
5183
6112
|
const spinner = ctx.ui.spinner("Starting Metabase…");
|
|
5184
6113
|
try {
|
|
5185
6114
|
await startAndWaitForHealth(runtime, client, {
|
|
@@ -5196,7 +6125,12 @@ function startMetabaseStep(deps = hostDeps$1) {
|
|
|
5196
6125
|
} else spinner.fail("Metabase failed to start");
|
|
5197
6126
|
throw error;
|
|
5198
6127
|
}
|
|
5199
|
-
|
|
6128
|
+
const version = await runningVersion(client);
|
|
6129
|
+
await ctx.saveState({
|
|
6130
|
+
...ctx.state,
|
|
6131
|
+
version
|
|
6132
|
+
});
|
|
6133
|
+
spinner.stop(`Metabase ${version} is running at ${configured.url}`);
|
|
5200
6134
|
return "completed";
|
|
5201
6135
|
}
|
|
5202
6136
|
};
|
|
@@ -5205,7 +6139,7 @@ function startMetabaseStep(deps = hostDeps$1) {
|
|
|
5205
6139
|
//#region src/steps/summary.ts
|
|
5206
6140
|
const STEP_ID = "summary";
|
|
5207
6141
|
const GIT_SYNC_STEP_ID = "git-sync";
|
|
5208
|
-
function hostDeps(ctx) {
|
|
6142
|
+
function hostDeps$1(ctx) {
|
|
5209
6143
|
return {
|
|
5210
6144
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
5211
6145
|
runner: processRunner(ctx.signal),
|
|
@@ -5273,6 +6207,7 @@ async function summaryLines(state, deps) {
|
|
|
5273
6207
|
if (login !== null) lines.push(login);
|
|
5274
6208
|
lines.push(profile === "default" ? `Metabase CLI: ${mbCommand(profile)} <command>` : `Metabase CLI: ${mbCommand(profile)} <command> or export MB_PROFILE=${profile}`);
|
|
5275
6209
|
lines.push(...await databaseLines$1(state, deps));
|
|
6210
|
+
if (state.sampleDatabase !== null) lines.push(`Sample database: psql "${sampleDatabaseUrl(state.sampleDatabase.port)}"`);
|
|
5276
6211
|
lines.push(...gitLines(state));
|
|
5277
6212
|
const installed = await skillsCli(deps.runner).list();
|
|
5278
6213
|
lines.push(skillLine(installed.filter((entry) => entry.name === "rde")));
|
|
@@ -5280,7 +6215,7 @@ async function summaryLines(state, deps) {
|
|
|
5280
6215
|
lines.push("Lifecycle: rde status, rde stop, rde start");
|
|
5281
6216
|
return lines;
|
|
5282
6217
|
}
|
|
5283
|
-
function summaryStep(deps = hostDeps) {
|
|
6218
|
+
function summaryStep(deps = hostDeps$1) {
|
|
5284
6219
|
return {
|
|
5285
6220
|
id: STEP_ID,
|
|
5286
6221
|
title: "Summary",
|
|
@@ -5295,16 +6230,23 @@ function summaryStep(deps = hostDeps) {
|
|
|
5295
6230
|
}
|
|
5296
6231
|
//#endregion
|
|
5297
6232
|
//#region src/steps/index.ts
|
|
6233
|
+
const DEFAULT_STEP_OPTIONS = {
|
|
6234
|
+
runtime: NO_RUNTIME_FLAGS,
|
|
6235
|
+
agents: DETECT_AGENTS,
|
|
6236
|
+
admin: DEFAULT_ADMIN,
|
|
6237
|
+
databases: ASK_DATABASES
|
|
6238
|
+
};
|
|
5298
6239
|
const STEP_BY_ID = {
|
|
5299
6240
|
preflight: () => preflightStep(),
|
|
5300
6241
|
"install-mb": () => installMbStep(),
|
|
5301
|
-
"agents-skills": () => agentsSkillsStep(),
|
|
5302
|
-
runtime: (options) => runtimeStep(options),
|
|
6242
|
+
"agents-skills": (options) => agentsSkillsStep(options.agents),
|
|
6243
|
+
runtime: (options) => runtimeStep(options.runtime),
|
|
5303
6244
|
"start-metabase": () => startMetabaseStep(),
|
|
5304
|
-
"admin-user": () => adminUserStep(),
|
|
6245
|
+
"admin-user": (options) => adminUserStep(options.admin),
|
|
5305
6246
|
"api-key": () => apiKeyStep(),
|
|
6247
|
+
"sample-database": () => sampleDatabaseStep(),
|
|
5306
6248
|
license: () => licenseStep(),
|
|
5307
|
-
databases: () => databasesStep(),
|
|
6249
|
+
databases: (options) => databasesStep(options.databases),
|
|
5308
6250
|
"git-sync": () => gitSyncStep(),
|
|
5309
6251
|
summary: () => summaryStep()
|
|
5310
6252
|
};
|
|
@@ -5345,16 +6287,13 @@ const doctor = defineRdeCommand({
|
|
|
5345
6287
|
async run(args) {
|
|
5346
6288
|
const name = await instanceNameOrDefault();
|
|
5347
6289
|
const state = await readInstanceState(name);
|
|
5348
|
-
const deps = hostDoctorDeps(interruptSignal);
|
|
6290
|
+
const deps = hostDoctorDeps(name, interruptSignal);
|
|
5349
6291
|
let report = await collectDoctor(name, state, deps);
|
|
5350
6292
|
if (args.fix && report.failed > 0) {
|
|
5351
6293
|
if (state === null) throw new ConfigError("no instance to fix yet; run `rde init`");
|
|
5352
6294
|
print(report, args.json);
|
|
5353
6295
|
const ctx = await createWizardContext(name, { signal: interruptSignal });
|
|
5354
|
-
if (await fixDoctor(report, initSteps({
|
|
5355
|
-
image: null,
|
|
5356
|
-
port: null
|
|
5357
|
-
}), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, deps);
|
|
6296
|
+
if (await fixDoctor(report, initSteps(DEFAULT_STEP_OPTIONS), ctx, { yes: args.yes })) report = await collectDoctor(name, ctx.state, deps);
|
|
5358
6297
|
}
|
|
5359
6298
|
print(report, args.json);
|
|
5360
6299
|
if (report.failed > 0) throw new Error(doctorSummary(report));
|
|
@@ -5381,12 +6320,6 @@ const EnvString = z.string().transform((value, ctx) => {
|
|
|
5381
6320
|
});
|
|
5382
6321
|
const AdminVariant = z.enum(["default", "prompted"]);
|
|
5383
6322
|
const RemoteKind = z.enum(["local-bare", "hosted"]);
|
|
5384
|
-
const PackageManagerName = z.enum([
|
|
5385
|
-
"npm",
|
|
5386
|
-
"pnpm",
|
|
5387
|
-
"yarn",
|
|
5388
|
-
"bun"
|
|
5389
|
-
]);
|
|
5390
6323
|
const FieldValue = z.union([
|
|
5391
6324
|
EnvString,
|
|
5392
6325
|
z.number(),
|
|
@@ -5398,13 +6331,15 @@ const DatabaseEntry = z.strictObject({
|
|
|
5398
6331
|
fields: z.record(z.string(), FieldValue).optional(),
|
|
5399
6332
|
name: z.string().optional()
|
|
5400
6333
|
}).refine((entry) => entry.uri === void 0 !== (entry.fields === void 0), { message: "a database needs either uri or fields" });
|
|
6334
|
+
const RuntimeSection = z.strictObject({
|
|
6335
|
+
kind: RuntimeKind.optional(),
|
|
6336
|
+
image: EnvString.optional(),
|
|
6337
|
+
jar: EnvString.optional(),
|
|
6338
|
+
port: z.int().positive().optional()
|
|
6339
|
+
}).refine((section) => section.image === void 0 || section.jar === void 0, { message: "a runtime section has either image or jar" }).refine((section) => section.kind !== "docker" || section.jar === void 0, { message: "jar does not apply to the docker runtime" }).refine((section) => section.kind !== "jar" || section.image === void 0, { message: "image does not apply to the jar runtime" });
|
|
5401
6340
|
const SECTIONS = {
|
|
5402
|
-
"install-mb": z.strictObject({ "package-manager": PackageManagerName }).optional(),
|
|
5403
6341
|
"agents-skills": z.strictObject({ agents: z.array(AgentId) }).optional(),
|
|
5404
|
-
runtime:
|
|
5405
|
-
image: EnvString.optional(),
|
|
5406
|
-
port: z.int().positive().optional()
|
|
5407
|
-
}).optional(),
|
|
6342
|
+
runtime: RuntimeSection.optional(),
|
|
5408
6343
|
"admin-user": z.strictObject({
|
|
5409
6344
|
variant: AdminVariant,
|
|
5410
6345
|
email: EnvString.optional(),
|
|
@@ -5469,7 +6404,6 @@ function addDatabase(map, database, last) {
|
|
|
5469
6404
|
add(map, DATABASE_KEYS.another, select$1(last ? "finish" : "another"));
|
|
5470
6405
|
}
|
|
5471
6406
|
function addAdmin(map, admin) {
|
|
5472
|
-
add(map, ADMIN_USER_KEYS.variant, select$1(admin.variant));
|
|
5473
6407
|
if (admin.variant !== "prompted") return;
|
|
5474
6408
|
if (admin.email !== void 0) {
|
|
5475
6409
|
add(map, ADMIN_USER_KEYS.email, text$1(admin.email));
|
|
@@ -5482,24 +6416,31 @@ function addAdmin(map, admin) {
|
|
|
5482
6416
|
add(map, ADMIN_USER_KEYS.firstName, text$1(admin["first-name"] ?? NO_NAME));
|
|
5483
6417
|
add(map, ADMIN_USER_KEYS.lastName, text$1(admin["last-name"] ?? NO_NAME));
|
|
5484
6418
|
}
|
|
6419
|
+
function runtimeKindOf(runtime) {
|
|
6420
|
+
if (runtime.kind !== void 0) return runtime.kind;
|
|
6421
|
+
if (runtime.image !== void 0) return "docker";
|
|
6422
|
+
if (runtime.jar !== void 0) return "jar";
|
|
6423
|
+
return null;
|
|
6424
|
+
}
|
|
6425
|
+
function stepOptionsFrom(answers) {
|
|
6426
|
+
const runtime = answers.runtime;
|
|
6427
|
+
const agents = answers["agents-skills"];
|
|
6428
|
+
const admin = answers["admin-user"];
|
|
6429
|
+
const databases = answers.databases;
|
|
6430
|
+
return {
|
|
6431
|
+
runtime: runtime === void 0 ? DEFAULT_STEP_OPTIONS.runtime : {
|
|
6432
|
+
kind: runtimeKindOf(runtime),
|
|
6433
|
+
image: runtime.image ?? null,
|
|
6434
|
+
jar: runtime.jar ?? null,
|
|
6435
|
+
port: runtime.port ?? null
|
|
6436
|
+
},
|
|
6437
|
+
agents: agents === void 0 ? DEFAULT_STEP_OPTIONS.agents : { agents: agents.agents },
|
|
6438
|
+
admin: admin === void 0 ? DEFAULT_STEP_OPTIONS.admin : { variant: admin.variant },
|
|
6439
|
+
databases: { requested: databases !== void 0 && databases.length > 0 }
|
|
6440
|
+
};
|
|
6441
|
+
}
|
|
5485
6442
|
function keyedAnswers(answers) {
|
|
5486
6443
|
const map = /* @__PURE__ */ new Map();
|
|
5487
|
-
const installMb = answers["install-mb"];
|
|
5488
|
-
if (installMb !== void 0) {
|
|
5489
|
-
add(map, INSTALL_MB_KEYS.packageManager, select$1(installMb["package-manager"]));
|
|
5490
|
-
add(map, INSTALL_MB_KEYS.confirm, confirm$1(true));
|
|
5491
|
-
}
|
|
5492
|
-
const agents = answers["agents-skills"];
|
|
5493
|
-
if (agents !== void 0) add(map, AGENTS_SKILLS_KEYS.agents, {
|
|
5494
|
-
kind: "multiselect",
|
|
5495
|
-
values: agents.agents
|
|
5496
|
-
});
|
|
5497
|
-
const runtime = answers.runtime;
|
|
5498
|
-
if (runtime !== void 0) {
|
|
5499
|
-
add(map, RUNTIME_KEYS.kind, select$1("docker"));
|
|
5500
|
-
if (runtime.image !== void 0) add(map, RUNTIME_KEYS.image, text$1(runtime.image));
|
|
5501
|
-
if (runtime.port !== void 0) add(map, RUNTIME_KEYS.port, text$1(String(runtime.port)));
|
|
5502
|
-
}
|
|
5503
6444
|
const admin = answers["admin-user"];
|
|
5504
6445
|
if (admin !== void 0) addAdmin(map, admin);
|
|
5505
6446
|
const apiKey = answers["api-key"];
|
|
@@ -5555,12 +6496,6 @@ function mergeInitAnswers(file, flags) {
|
|
|
5555
6496
|
};
|
|
5556
6497
|
}
|
|
5557
6498
|
const FLAG_FOR_KEY = {
|
|
5558
|
-
[INSTALL_MB_KEYS.packageManager]: "--yes",
|
|
5559
|
-
[INSTALL_MB_KEYS.confirm]: "--yes",
|
|
5560
|
-
[AGENTS_SKILLS_KEYS.agents]: "--agents or --skip-agents",
|
|
5561
|
-
[RUNTIME_KEYS.image]: "--image",
|
|
5562
|
-
[RUNTIME_KEYS.port]: "--port",
|
|
5563
|
-
[ADMIN_USER_KEYS.variant]: "--admin",
|
|
5564
6499
|
[ADMIN_USER_KEYS.email]: "--admin-email",
|
|
5565
6500
|
[ADMIN_USER_KEYS.password]: "--admin-password",
|
|
5566
6501
|
[API_KEY_KEYS.email]: "--admin-email",
|
|
@@ -5590,6 +6525,7 @@ function refusalWithoutTerminal(prompt) {
|
|
|
5590
6525
|
}
|
|
5591
6526
|
const LIST_SEPARATOR = ",";
|
|
5592
6527
|
const DATABASE_SEPARATOR = "=";
|
|
6528
|
+
const RUNTIME_LIST$1 = RuntimeKind.options.join(", ");
|
|
5593
6529
|
const GIT_REMOTE_NAMES = {
|
|
5594
6530
|
local: "local-bare",
|
|
5595
6531
|
hosted: "hosted"
|
|
@@ -5661,6 +6597,37 @@ function gitSection(flags, secrets) {
|
|
|
5661
6597
|
...secrets.has("--git-token") && { token: secrets.get("--git-token") }
|
|
5662
6598
|
};
|
|
5663
6599
|
}
|
|
6600
|
+
function impliedKind(flags) {
|
|
6601
|
+
if (flags.image !== void 0 && flags.jar !== void 0) throw conflict("--image", "--jar");
|
|
6602
|
+
if (flags.image !== void 0) return "docker";
|
|
6603
|
+
if (flags.jar !== void 0) return "jar";
|
|
6604
|
+
return null;
|
|
6605
|
+
}
|
|
6606
|
+
function runtimeKind(flags) {
|
|
6607
|
+
const implied = impliedKind(flags);
|
|
6608
|
+
if (flags.runtime === void 0) return;
|
|
6609
|
+
const parsed = RuntimeKind.safeParse(flags.runtime);
|
|
6610
|
+
if (!parsed.success) throw new ConfigError(`--runtime ${flags.runtime} is not one of ${RUNTIME_LIST$1}`);
|
|
6611
|
+
if (implied !== null && implied !== parsed.data) throw conflict(`--runtime ${parsed.data}`, implied === "docker" ? "--image" : "--jar");
|
|
6612
|
+
return parsed.data;
|
|
6613
|
+
}
|
|
6614
|
+
function portFlag(value) {
|
|
6615
|
+
if (value === void 0) return;
|
|
6616
|
+
const port = parsePort(value);
|
|
6617
|
+
if (port === null) throw new ConfigError(`--port ${value} is not a valid port number`);
|
|
6618
|
+
return port;
|
|
6619
|
+
}
|
|
6620
|
+
function runtimeSection(flags) {
|
|
6621
|
+
const kind = runtimeKind(flags);
|
|
6622
|
+
const port = portFlag(flags.port);
|
|
6623
|
+
if (kind === void 0 && flags.image === void 0 && flags.jar === void 0 && port === void 0) return;
|
|
6624
|
+
return {
|
|
6625
|
+
...kind !== void 0 && { kind },
|
|
6626
|
+
...flags.image !== void 0 && { image: flags.image },
|
|
6627
|
+
...flags.jar !== void 0 && { jar: flags.jar },
|
|
6628
|
+
...port !== void 0 && { port }
|
|
6629
|
+
};
|
|
6630
|
+
}
|
|
5664
6631
|
function agentsSection(flags) {
|
|
5665
6632
|
if (flags.skipAgents) {
|
|
5666
6633
|
if (flags.agents !== void 0) throw conflict("--skip-agents", "--agents");
|
|
@@ -5685,7 +6652,7 @@ async function answersFromFlags(flags, readStdin) {
|
|
|
5685
6652
|
}
|
|
5686
6653
|
], readStdin);
|
|
5687
6654
|
const sections = {
|
|
5688
|
-
runtime:
|
|
6655
|
+
runtime: runtimeSection(flags),
|
|
5689
6656
|
"admin-user": adminSection(flags, secrets),
|
|
5690
6657
|
license: licenseSection(flags, secrets),
|
|
5691
6658
|
databases: databasesSection(flags),
|
|
@@ -5724,10 +6691,6 @@ function stringOf(answer) {
|
|
|
5724
6691
|
if (answer.kind === "select" && typeof answer.value === "string") return answer.value;
|
|
5725
6692
|
return null;
|
|
5726
6693
|
}
|
|
5727
|
-
function runtime(draft) {
|
|
5728
|
-
draft.answers.runtime ??= {};
|
|
5729
|
-
return draft.answers.runtime;
|
|
5730
|
-
}
|
|
5731
6694
|
function admin(draft) {
|
|
5732
6695
|
draft.answers["admin-user"] ??= { variant: "prompted" };
|
|
5733
6696
|
return draft.answers["admin-user"];
|
|
@@ -5736,21 +6699,12 @@ function gitSync(draft) {
|
|
|
5736
6699
|
draft.answers["git-sync"] ??= { location: "skip" };
|
|
5737
6700
|
return draft.answers["git-sync"];
|
|
5738
6701
|
}
|
|
5739
|
-
function isAdminVariant(value) {
|
|
5740
|
-
return value === "default" || value === "prompted";
|
|
5741
|
-
}
|
|
5742
6702
|
function isLocation(value) {
|
|
5743
6703
|
return value === "here" || value === "new" || value === "existing" || value === "skip";
|
|
5744
6704
|
}
|
|
5745
6705
|
function isRemote(value) {
|
|
5746
6706
|
return value === "local-bare" || value === "hosted";
|
|
5747
6707
|
}
|
|
5748
|
-
function isPackageManager(value) {
|
|
5749
|
-
return value === "npm" || value === "pnpm" || value === "yarn" || value === "bun";
|
|
5750
|
-
}
|
|
5751
|
-
function isAgentList(values) {
|
|
5752
|
-
return values.every((value) => typeof value === "string");
|
|
5753
|
-
}
|
|
5754
6708
|
function applyDatabaseRecord(draft, record) {
|
|
5755
6709
|
const value = stringOf(record.answer);
|
|
5756
6710
|
if (record.key === DATABASE_KEYS.engine) {
|
|
@@ -5780,8 +6734,7 @@ function applyDatabaseRecord(draft, record) {
|
|
|
5780
6734
|
else if (value !== null) database.fields[fieldName] = value;
|
|
5781
6735
|
}
|
|
5782
6736
|
function applyAdminRecord(draft, key, value) {
|
|
5783
|
-
if (key === ADMIN_USER_KEYS.
|
|
5784
|
-
else if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
|
|
6737
|
+
if (key === ADMIN_USER_KEYS.email && value !== null) admin(draft).email = value;
|
|
5785
6738
|
else if (key === ADMIN_USER_KEYS.password) admin(draft).password = envReference(ENV_ADMIN_PASSWORD);
|
|
5786
6739
|
else if (key === ADMIN_USER_KEYS.firstName && value !== null && value.length > 0) admin(draft)["first-name"] = value;
|
|
5787
6740
|
else if (key === ADMIN_USER_KEYS.lastName && value !== null && value.length > 0) admin(draft)["last-name"] = value;
|
|
@@ -5794,16 +6747,11 @@ function applyGitRecord(draft, key, value) {
|
|
|
5794
6747
|
else if (key === GIT_SYNC_KEYS.token) gitSync(draft).token = envReference(ENV_GIT_TOKEN);
|
|
5795
6748
|
}
|
|
5796
6749
|
function applyRecord(draft, record) {
|
|
5797
|
-
const { key
|
|
5798
|
-
const value = stringOf(answer);
|
|
6750
|
+
const { key } = record;
|
|
6751
|
+
const value = stringOf(record.answer);
|
|
5799
6752
|
if (key.startsWith("databases.")) applyDatabaseRecord(draft, record);
|
|
5800
6753
|
else if (key.startsWith("admin-user.")) applyAdminRecord(draft, key, value);
|
|
5801
6754
|
else if (key.startsWith("git-sync.")) applyGitRecord(draft, key, value);
|
|
5802
|
-
else if (key === INSTALL_MB_KEYS.packageManager && value !== null && isPackageManager(value)) draft.answers["install-mb"] = { "package-manager": value };
|
|
5803
|
-
else if (key === AGENTS_SKILLS_KEYS.agents && answer.kind === "multiselect") {
|
|
5804
|
-
if (isAgentList(answer.values)) draft.answers["agents-skills"] = { agents: answer.values };
|
|
5805
|
-
} else if (key === RUNTIME_KEYS.image && value !== null) runtime(draft).image = value;
|
|
5806
|
-
else if (key === RUNTIME_KEYS.port && value !== null) runtime(draft).port = Number(value);
|
|
5807
6755
|
else if (key === API_KEY_KEYS.profile && value !== null) {
|
|
5808
6756
|
const profile = MbProfileChoice.safeParse(value);
|
|
5809
6757
|
if (profile.success) draft.answers["api-key"] = { profile: profile.data };
|
|
@@ -5822,7 +6770,7 @@ function databaseEntry(database, index) {
|
|
|
5822
6770
|
...database.name !== null && { name: database.name }
|
|
5823
6771
|
};
|
|
5824
6772
|
}
|
|
5825
|
-
function answersFromRecords(records) {
|
|
6773
|
+
function answersFromRecords(records, overrides) {
|
|
5826
6774
|
const draft = {
|
|
5827
6775
|
answers: {},
|
|
5828
6776
|
databases: [],
|
|
@@ -5831,7 +6779,11 @@ function answersFromRecords(records) {
|
|
|
5831
6779
|
for (const record of records) applyRecord(draft, record);
|
|
5832
6780
|
const databases = draft.databases.map(databaseEntry).filter((entry) => entry !== null);
|
|
5833
6781
|
if (databases.length > 0 || draft.databasesFinished) draft.answers.databases = databases;
|
|
5834
|
-
return
|
|
6782
|
+
return {
|
|
6783
|
+
...draft.answers,
|
|
6784
|
+
...overrides.runtime !== void 0 && { runtime: overrides.runtime },
|
|
6785
|
+
...overrides["agents-skills"] !== void 0 && { "agents-skills": overrides["agents-skills"] }
|
|
6786
|
+
};
|
|
5835
6787
|
}
|
|
5836
6788
|
async function writeAnswersFile(path, answers) {
|
|
5837
6789
|
await promises.writeFile(path, JSON.stringify(answers, null, 2) + "\n", { mode: FILE_MODE });
|
|
@@ -6016,40 +6968,24 @@ function recordingPrompts(base) {
|
|
|
6016
6968
|
//#endregion
|
|
6017
6969
|
//#region src/commands/init.ts
|
|
6018
6970
|
const STEP_LIST = StepId.options.join(", ");
|
|
6971
|
+
const RUNTIME_LIST = RuntimeKind.options.join(", ");
|
|
6019
6972
|
function stepFlag(flag, value) {
|
|
6020
6973
|
if (value === void 0) return;
|
|
6021
6974
|
const parsed = StepId.safeParse(value);
|
|
6022
6975
|
if (!parsed.success) throw new ConfigError(`${flag} ${value} is not one of the steps: ${STEP_LIST}`);
|
|
6023
6976
|
return parsed.data;
|
|
6024
6977
|
}
|
|
6025
|
-
function portFlag(value) {
|
|
6026
|
-
if (value === void 0) return null;
|
|
6027
|
-
const port = parsePort(value);
|
|
6028
|
-
if (port === null) throw new ConfigError(`--port ${value} is not a valid port number`);
|
|
6029
|
-
return port;
|
|
6030
|
-
}
|
|
6031
6978
|
async function combinedAnswers(options) {
|
|
6032
6979
|
const fromFlags = await answersFromFlags(options.flags, readStdinLine);
|
|
6033
6980
|
if (options.answersPath === void 0) return fromFlags;
|
|
6034
6981
|
return mergeInitAnswers(await loadInitAnswers(options.answersPath), fromFlags);
|
|
6035
6982
|
}
|
|
6036
|
-
|
|
6983
|
+
function promptsFor(answers, yes) {
|
|
6037
6984
|
const answering = {
|
|
6038
6985
|
...!stdinIsTty() && { unanswered: refusalWithoutTerminal },
|
|
6039
|
-
...
|
|
6040
|
-
};
|
|
6041
|
-
return answeringPrompts(clackPrompts(), keyedAnswers(await combinedAnswers(options)), answering);
|
|
6042
|
-
}
|
|
6043
|
-
function withRuntimeFlags(answers, image, port) {
|
|
6044
|
-
if (image === null && port === null) return answers;
|
|
6045
|
-
return {
|
|
6046
|
-
...answers,
|
|
6047
|
-
runtime: {
|
|
6048
|
-
...answers.runtime,
|
|
6049
|
-
...image !== null && { image },
|
|
6050
|
-
...port !== null && { port }
|
|
6051
|
-
}
|
|
6986
|
+
...yes && { confirmAll: true }
|
|
6052
6987
|
};
|
|
6988
|
+
return answeringPrompts(clackPrompts(), keyedAnswers(answers), answering);
|
|
6053
6989
|
}
|
|
6054
6990
|
const init = defineRdeCommand({
|
|
6055
6991
|
meta: {
|
|
@@ -6065,13 +7001,21 @@ const init = defineRdeCommand({
|
|
|
6065
7001
|
type: "string",
|
|
6066
7002
|
description: "Run only this step"
|
|
6067
7003
|
},
|
|
7004
|
+
runtime: {
|
|
7005
|
+
type: "string",
|
|
7006
|
+
description: `How a new instance runs Metabase (${RUNTIME_LIST}); Docker when it is available`
|
|
7007
|
+
},
|
|
6068
7008
|
image: {
|
|
6069
7009
|
type: "string",
|
|
6070
|
-
description: "Docker image for a new instance"
|
|
7010
|
+
description: "Docker image for a new instance (implies --runtime docker)"
|
|
7011
|
+
},
|
|
7012
|
+
jar: {
|
|
7013
|
+
type: "string",
|
|
7014
|
+
description: "Metabase jar for a new instance: latest, a release tag, or a .jar path (implies --runtime jar)"
|
|
6071
7015
|
},
|
|
6072
7016
|
port: {
|
|
6073
7017
|
type: "string",
|
|
6074
|
-
description: "Host port for a new instance"
|
|
7018
|
+
description: "Host port for a new instance; the first free from 3100"
|
|
6075
7019
|
},
|
|
6076
7020
|
answers: {
|
|
6077
7021
|
type: "string",
|
|
@@ -6083,7 +7027,7 @@ const init = defineRdeCommand({
|
|
|
6083
7027
|
},
|
|
6084
7028
|
admin: {
|
|
6085
7029
|
type: "string",
|
|
6086
|
-
description: "Admin credentials: default or prompted"
|
|
7030
|
+
description: "Admin credentials: default (admin@metabase.com / metabase123) or prompted"
|
|
6087
7031
|
},
|
|
6088
7032
|
"admin-email": {
|
|
6089
7033
|
type: "string",
|
|
@@ -6134,7 +7078,7 @@ const init = defineRdeCommand({
|
|
|
6134
7078
|
},
|
|
6135
7079
|
agents: {
|
|
6136
7080
|
type: "string",
|
|
6137
|
-
description: "Agents to install the skill for, comma-separated"
|
|
7081
|
+
description: "Agents to install the skill for, comma-separated; the detected ones by default"
|
|
6138
7082
|
},
|
|
6139
7083
|
"skip-agents": {
|
|
6140
7084
|
type: "boolean",
|
|
@@ -6147,7 +7091,7 @@ const init = defineRdeCommand({
|
|
|
6147
7091
|
},
|
|
6148
7092
|
yes: {
|
|
6149
7093
|
type: "boolean",
|
|
6150
|
-
description: "Answer yes to every confirmation",
|
|
7094
|
+
description: "Answer yes to every confirmation (skipping databases)",
|
|
6151
7095
|
default: false
|
|
6152
7096
|
}
|
|
6153
7097
|
},
|
|
@@ -6158,14 +7102,14 @@ const init = defineRdeCommand({
|
|
|
6158
7102
|
...from !== void 0 && { from },
|
|
6159
7103
|
...only !== void 0 && { only }
|
|
6160
7104
|
};
|
|
6161
|
-
const stepOptions = {
|
|
6162
|
-
image: args.image ?? null,
|
|
6163
|
-
port: portFlag(args.port)
|
|
6164
|
-
};
|
|
6165
7105
|
const runOptions = {
|
|
6166
7106
|
answersPath: args.answers,
|
|
6167
7107
|
yes: args.yes,
|
|
6168
7108
|
flags: {
|
|
7109
|
+
runtime: args.runtime,
|
|
7110
|
+
image: args.image,
|
|
7111
|
+
jar: args.jar,
|
|
7112
|
+
port: args.port,
|
|
6169
7113
|
admin: args.admin,
|
|
6170
7114
|
adminEmail: args["admin-email"],
|
|
6171
7115
|
adminPassword: args["admin-password"],
|
|
@@ -6184,16 +7128,17 @@ const init = defineRdeCommand({
|
|
|
6184
7128
|
}
|
|
6185
7129
|
};
|
|
6186
7130
|
const printTo = args["print-answers"];
|
|
6187
|
-
const
|
|
7131
|
+
const answers = await combinedAnswers(runOptions);
|
|
7132
|
+
const answering = promptsFor(answers, runOptions.yes);
|
|
6188
7133
|
const recording = printTo === void 0 ? null : recordingPrompts(answering);
|
|
6189
7134
|
const ctx = await createWizardContext(DEFAULT_INSTANCE_NAME, {
|
|
6190
7135
|
signal: interruptSignal,
|
|
6191
7136
|
prompts: recording?.prompts ?? answering
|
|
6192
7137
|
});
|
|
6193
7138
|
ctx.ui.intro("rde init");
|
|
6194
|
-
await runWizard(initSteps(
|
|
7139
|
+
await runWizard(initSteps(stepOptionsFrom(answers)), ctx, options);
|
|
6195
7140
|
if (recording !== null && printTo !== void 0) {
|
|
6196
|
-
await writeAnswersFile(printTo,
|
|
7141
|
+
await writeAnswersFile(printTo, answersFromRecords(recording.records, answers));
|
|
6197
7142
|
ctx.ui.info(`Answers written to ${printTo}`);
|
|
6198
7143
|
}
|
|
6199
7144
|
ctx.ui.outro("rde init finished");
|
|
@@ -6204,10 +7149,12 @@ const init = defineRdeCommand({
|
|
|
6204
7149
|
async function loadInstance(name, signal) {
|
|
6205
7150
|
const state = await requireInstanceState(name);
|
|
6206
7151
|
const configured = requireConfiguredRuntime(state);
|
|
7152
|
+
const deps = hostRuntimeDeps(name, signal);
|
|
6207
7153
|
return {
|
|
6208
7154
|
state,
|
|
6209
7155
|
url: configured.url,
|
|
6210
|
-
runtime: runtimeFromState(configured.runtime,
|
|
7156
|
+
runtime: runtimeFromState(configured.runtime, deps),
|
|
7157
|
+
sampleDatabase: state.sampleDatabase === null ? null : sampleDatabaseContainer(state.sampleDatabase, deps.docker)
|
|
6211
7158
|
};
|
|
6212
7159
|
}
|
|
6213
7160
|
function openInstance(url) {
|
|
@@ -6264,10 +7211,7 @@ const start = defineRdeCommand({
|
|
|
6264
7211
|
const SUMMARY_STEP_ID = "summary";
|
|
6265
7212
|
function hostStatusDeps(instanceName, signal) {
|
|
6266
7213
|
return {
|
|
6267
|
-
steps: initSteps(
|
|
6268
|
-
image: null,
|
|
6269
|
-
port: null
|
|
6270
|
-
}),
|
|
7214
|
+
steps: initSteps(DEFAULT_STEP_OPTIONS),
|
|
6271
7215
|
runtime: hostRuntimeDeps(instanceName, signal),
|
|
6272
7216
|
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, signal),
|
|
6273
7217
|
runner: processRunner(signal)
|
|
@@ -6307,6 +7251,19 @@ async function databasesOf(ctx, deps, health) {
|
|
|
6307
7251
|
throw error;
|
|
6308
7252
|
}
|
|
6309
7253
|
}
|
|
7254
|
+
async function sampleDatabaseOf(ctx, deps, databases) {
|
|
7255
|
+
const record = ctx.state.sampleDatabase;
|
|
7256
|
+
if (record === null) return null;
|
|
7257
|
+
const status = await sampleDatabaseContainer(record, deps.runtime.docker).status();
|
|
7258
|
+
const listed = databases?.find((database) => database.id === record.databaseId) ?? null;
|
|
7259
|
+
return {
|
|
7260
|
+
container: status.running ? "running" : status.installed ? "stopped" : "missing",
|
|
7261
|
+
port: record.port,
|
|
7262
|
+
url: sampleDatabaseUrl(record.port),
|
|
7263
|
+
databaseId: record.databaseId,
|
|
7264
|
+
initialSyncStatus: listed === null ? null : listed.initialSyncStatus
|
|
7265
|
+
};
|
|
7266
|
+
}
|
|
6310
7267
|
function taskLabel(status) {
|
|
6311
7268
|
const task = status.current_task;
|
|
6312
7269
|
return task === null ? null : `${task.sync_task_type} ${task.status}`;
|
|
@@ -6332,6 +7289,7 @@ async function collectStatus(ctx, deps) {
|
|
|
6332
7289
|
const configured = configuredRuntime(ctx.state);
|
|
6333
7290
|
const runtime = configured === null ? null : await runtimeFromState(configured.runtime, deps.runtime).status();
|
|
6334
7291
|
const health = configured === null ? null : await healthOf(configured.url, deps);
|
|
7292
|
+
const databases = health === null ? null : await databasesOf(ctx, deps, health);
|
|
6335
7293
|
return {
|
|
6336
7294
|
instance: ctx.state.name,
|
|
6337
7295
|
url: configured?.url ?? null,
|
|
@@ -6340,9 +7298,11 @@ async function collectStatus(ctx, deps) {
|
|
|
6340
7298
|
installed: runtime.installed,
|
|
6341
7299
|
running: runtime.running
|
|
6342
7300
|
},
|
|
7301
|
+
version: ctx.state.version,
|
|
6343
7302
|
health,
|
|
6344
7303
|
steps: await stepReports(ctx, deps),
|
|
6345
|
-
databases
|
|
7304
|
+
databases,
|
|
7305
|
+
sampleDatabase: await sampleDatabaseOf(ctx, deps, databases),
|
|
6346
7306
|
gitSync: await gitSyncOf(ctx, deps)
|
|
6347
7307
|
};
|
|
6348
7308
|
}
|
|
@@ -6352,6 +7312,9 @@ function runtimeLine(report) {
|
|
|
6352
7312
|
if (report.runtime.installed) return `Runtime: ${report.runtime.kind}, stopped; \`rde start\``;
|
|
6353
7313
|
return `Runtime: ${report.runtime.kind}, not installed; run \`rde init\``;
|
|
6354
7314
|
}
|
|
7315
|
+
function versionLine(report) {
|
|
7316
|
+
return report.version === null ? "Version: not recorded yet; `rde start` records it" : `Version: ${report.version}`;
|
|
7317
|
+
}
|
|
6355
7318
|
function isGitSyncFailure(value) {
|
|
6356
7319
|
return "error" in value;
|
|
6357
7320
|
}
|
|
@@ -6360,6 +7323,22 @@ function gitSyncLine(report) {
|
|
|
6360
7323
|
if (isGitSyncFailure(report.gitSync)) return `Remote sync: ${report.gitSync.error}`;
|
|
6361
7324
|
return `Remote sync: ${report.gitSync.branch === null ? "no branch" : `branch ${report.gitSync.branch}`}, ${report.gitSync.dirty ? "unsynced changes" : "in sync"}, ${report.gitSync.task === null ? "no task running" : `task ${report.gitSync.task}`}`;
|
|
6362
7325
|
}
|
|
7326
|
+
const CONTAINER_STATE_LABEL = {
|
|
7327
|
+
running: "container running",
|
|
7328
|
+
stopped: "container stopped; `rde start`",
|
|
7329
|
+
missing: "container missing; `rde init` recreates it"
|
|
7330
|
+
};
|
|
7331
|
+
function sampleConnectionLabel(report, sample) {
|
|
7332
|
+
if (sample.databaseId === null) return "not connected to Metabase yet; `rde init`";
|
|
7333
|
+
if (report.databases === null) return "sync unknown while Metabase is not running";
|
|
7334
|
+
if (sample.initialSyncStatus === null) return `database ${sample.databaseId} is gone from Metabase; \`rde init\` connects it again`;
|
|
7335
|
+
return `sync ${sample.initialSyncStatus}`;
|
|
7336
|
+
}
|
|
7337
|
+
function sampleDatabaseLine(report) {
|
|
7338
|
+
const sample = report.sampleDatabase;
|
|
7339
|
+
if (sample === null) return "Sample database: not connected; `rde init` adds one";
|
|
7340
|
+
return `Sample database: ${sample.url}, ${CONTAINER_STATE_LABEL[sample.container]}, ${sampleConnectionLabel(report, sample)}`;
|
|
7341
|
+
}
|
|
6363
7342
|
function databaseLines(report) {
|
|
6364
7343
|
if (report.databases === null) return ["Databases: unavailable while Metabase is not running"];
|
|
6365
7344
|
return ["Databases:", ...databaseListingLines(report.databases).map((line) => ` ${line}`)];
|
|
@@ -6368,10 +7347,12 @@ function statusLines(report) {
|
|
|
6368
7347
|
return [
|
|
6369
7348
|
report.url === null ? `Instance ${report.instance}` : `Instance ${report.instance} — ${report.url}`,
|
|
6370
7349
|
runtimeLine(report),
|
|
7350
|
+
versionLine(report),
|
|
6371
7351
|
`Health: ${report.health ?? "unknown"}`,
|
|
6372
7352
|
"Steps:",
|
|
6373
7353
|
...checklistLines(report.steps).map((line) => ` ${line}`),
|
|
6374
7354
|
...databaseLines(report),
|
|
7355
|
+
sampleDatabaseLine(report),
|
|
6375
7356
|
gitSyncLine(report)
|
|
6376
7357
|
];
|
|
6377
7358
|
}
|
|
@@ -6403,13 +7384,19 @@ const status = defineRdeCommand({
|
|
|
6403
7384
|
const stop = defineRdeCommand({
|
|
6404
7385
|
meta: {
|
|
6405
7386
|
name: "stop",
|
|
6406
|
-
description: "Stop the Metabase instance"
|
|
7387
|
+
description: "Stop the Metabase instance and its sample database"
|
|
6407
7388
|
},
|
|
6408
7389
|
async run() {
|
|
6409
7390
|
const instance = await loadInstance(await defaultInstanceName(), interruptSignal);
|
|
6410
7391
|
const before = await instance.runtime.status();
|
|
6411
7392
|
await instance.runtime.stop();
|
|
6412
|
-
|
|
7393
|
+
const lines = [before.running ? "Metabase stopped" : "Metabase is not running"];
|
|
7394
|
+
if (instance.sampleDatabase !== null) {
|
|
7395
|
+
const sidecar = await instance.sampleDatabase.status();
|
|
7396
|
+
await instance.sampleDatabase.stop();
|
|
7397
|
+
lines.push(sidecar.running ? "Sample database stopped" : "Sample database is not running");
|
|
7398
|
+
}
|
|
7399
|
+
printLines(lines);
|
|
6413
7400
|
}
|
|
6414
7401
|
});
|
|
6415
7402
|
//#endregion
|
|
@@ -6419,10 +7406,12 @@ function hostUninstallDeps(signal) {
|
|
|
6419
7406
|
const runner = processRunner(signal);
|
|
6420
7407
|
return {
|
|
6421
7408
|
docker: dockerTool(runner),
|
|
7409
|
+
daemon: daemonTool(runner),
|
|
6422
7410
|
runner
|
|
6423
7411
|
};
|
|
6424
7412
|
}
|
|
6425
|
-
const NOT_TOUCHED = "The mb CLI and
|
|
7413
|
+
const NOT_TOUCHED = "The mb CLI, Docker, and Java themselves";
|
|
7414
|
+
const ANY_JAR = "-jar";
|
|
6426
7415
|
function isFileSystemError(error) {
|
|
6427
7416
|
return error instanceof Error && "code" in error;
|
|
6428
7417
|
}
|
|
@@ -6495,13 +7484,28 @@ async function findContainer(name, docker, stateImage) {
|
|
|
6495
7484
|
throw error;
|
|
6496
7485
|
}
|
|
6497
7486
|
}
|
|
7487
|
+
async function findJarProcess(instance, source, daemon) {
|
|
7488
|
+
const dir = instanceDir(instance);
|
|
7489
|
+
const pid = await runningJarPid(dir, source === null ? ANY_JAR : jarPathOf(source, jarsDir()), daemon);
|
|
7490
|
+
if (pid === null) {
|
|
7491
|
+
await promises.rm(pidPathOf(dir), { force: true });
|
|
7492
|
+
return null;
|
|
7493
|
+
}
|
|
7494
|
+
return {
|
|
7495
|
+
label: `Metabase process ${pid} (running; it is stopped first)`,
|
|
7496
|
+
remove: async () => {
|
|
7497
|
+
await daemon.terminate(pid, { graceMs: JAR_STOP_GRACE_MS });
|
|
7498
|
+
await promises.rm(pidPathOf(dir), { force: true });
|
|
7499
|
+
}
|
|
7500
|
+
};
|
|
7501
|
+
}
|
|
6498
7502
|
async function findImage(image, docker, removeImage) {
|
|
6499
7503
|
if (!removeImage) return {
|
|
6500
7504
|
removal: null,
|
|
6501
7505
|
kept: `Docker image ${image}; pass --remove-image to delete it`
|
|
6502
7506
|
};
|
|
6503
7507
|
try {
|
|
6504
|
-
if (
|
|
7508
|
+
if (await docker.imageId(image) === null) return {
|
|
6505
7509
|
removal: null,
|
|
6506
7510
|
kept: null
|
|
6507
7511
|
};
|
|
@@ -6520,6 +7524,31 @@ async function findImage(image, docker, removeImage) {
|
|
|
6520
7524
|
kept: null
|
|
6521
7525
|
};
|
|
6522
7526
|
}
|
|
7527
|
+
async function findJar(source, removeImage) {
|
|
7528
|
+
if (source.kind !== "release") return {
|
|
7529
|
+
removal: null,
|
|
7530
|
+
kept: null
|
|
7531
|
+
};
|
|
7532
|
+
const dir = jarReleaseDir(source.version);
|
|
7533
|
+
if (!await pathExists(dir)) return {
|
|
7534
|
+
removal: null,
|
|
7535
|
+
kept: null
|
|
7536
|
+
};
|
|
7537
|
+
if (!removeImage) return {
|
|
7538
|
+
removal: null,
|
|
7539
|
+
kept: `Downloaded jar ${dir}; pass --remove-image to delete it`
|
|
7540
|
+
};
|
|
7541
|
+
return {
|
|
7542
|
+
removal: {
|
|
7543
|
+
label: `Downloaded jar ${dir}`,
|
|
7544
|
+
remove: () => promises.rm(dir, {
|
|
7545
|
+
recursive: true,
|
|
7546
|
+
force: true
|
|
7547
|
+
})
|
|
7548
|
+
},
|
|
7549
|
+
kept: null
|
|
7550
|
+
};
|
|
7551
|
+
}
|
|
6523
7552
|
function isInstanceProfile(status, url) {
|
|
6524
7553
|
if (!status.present) return false;
|
|
6525
7554
|
if (url !== null) return status.url === url;
|
|
@@ -6574,7 +7603,11 @@ async function findConfigEntry(instance) {
|
|
|
6574
7603
|
};
|
|
6575
7604
|
}
|
|
6576
7605
|
async function pruneEmptyDirs() {
|
|
6577
|
-
for (const dir of [
|
|
7606
|
+
for (const dir of [
|
|
7607
|
+
instancesDir(),
|
|
7608
|
+
jarsDir(),
|
|
7609
|
+
rdeHome()
|
|
7610
|
+
]) try {
|
|
6578
7611
|
await promises.rmdir(dir);
|
|
6579
7612
|
} catch (error) {
|
|
6580
7613
|
if (!isNotFound(error) && !isNotEmpty(error)) throw error;
|
|
@@ -6597,10 +7630,23 @@ async function planUninstall(options, deps) {
|
|
|
6597
7630
|
const container = await findContainer(runtime?.containerName ?? containerName(options.instance), deps.docker, runtime?.image ?? null);
|
|
6598
7631
|
if (container.removal !== null) removals.push(container.removal);
|
|
6599
7632
|
if (container.problem !== null) problems.push(container.problem);
|
|
6600
|
-
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
7633
|
+
const sample = state?.sampleDatabase ?? null;
|
|
7634
|
+
const sidecar = await findContainer(sample?.containerName ?? sampleContainerName(options.instance), deps.docker, sample?.image ?? null);
|
|
7635
|
+
if (sidecar.removal !== null) removals.push(sidecar.removal);
|
|
7636
|
+
if (sidecar.problem !== null) problems.push(sidecar.problem);
|
|
7637
|
+
const jarSource = state?.runtime?.kind === "jar" ? state.runtime.source : null;
|
|
7638
|
+
const jarProcess = await findJarProcess(options.instance, jarSource, deps.daemon);
|
|
7639
|
+
if (jarProcess !== null) removals.push(jarProcess);
|
|
7640
|
+
for (const image of [container.image, sidecar.image]) {
|
|
7641
|
+
if (image === null) continue;
|
|
7642
|
+
const finding = await findImage(image, deps.docker, options.removeImage);
|
|
7643
|
+
if (finding.removal !== null) removals.push(finding.removal);
|
|
7644
|
+
if (finding.kept !== null) kept.push(finding.kept);
|
|
7645
|
+
}
|
|
7646
|
+
if (jarSource !== null) {
|
|
7647
|
+
const jar = await findJar(jarSource, options.removeImage);
|
|
7648
|
+
if (jar.removal !== null) removals.push(jar.removal);
|
|
7649
|
+
if (jar.kept !== null) kept.push(jar.kept);
|
|
6604
7650
|
}
|
|
6605
7651
|
const profiles = state === null ? await findMbProfiles([DEFAULT_MB_PROFILE, "rde"], null, deps.runner) : await findMbProfiles([state.mbProfile], state.url, deps.runner);
|
|
6606
7652
|
removals.push(...profiles);
|
|
@@ -6628,7 +7674,7 @@ async function planUninstall(options, deps) {
|
|
|
6628
7674
|
problems
|
|
6629
7675
|
};
|
|
6630
7676
|
}
|
|
6631
|
-
function planLines(plan) {
|
|
7677
|
+
function planLines$1(plan) {
|
|
6632
7678
|
return [
|
|
6633
7679
|
"Removing:",
|
|
6634
7680
|
...plan.removals.map((removal) => ` ${removal.label}`),
|
|
@@ -6679,7 +7725,7 @@ async function uninstall(options, deps, io) {
|
|
|
6679
7725
|
problems: plan.problems
|
|
6680
7726
|
};
|
|
6681
7727
|
}
|
|
6682
|
-
io.ui.note(planLines(plan).join("\n"), `rde uninstall ${options.instance}`);
|
|
7728
|
+
io.ui.note(planLines$1(plan).join("\n"), `rde uninstall ${options.instance}`);
|
|
6683
7729
|
if (!options.yes) {
|
|
6684
7730
|
if (!await io.prompts.confirm({
|
|
6685
7731
|
message: "Remove everything listed?",
|
|
@@ -6719,7 +7765,7 @@ const uninstallCommand = defineRdeCommand({
|
|
|
6719
7765
|
},
|
|
6720
7766
|
"remove-image": {
|
|
6721
7767
|
type: "boolean",
|
|
6722
|
-
description: "Also delete the Docker image",
|
|
7768
|
+
description: "Also delete the Docker image or the downloaded jar",
|
|
6723
7769
|
default: false
|
|
6724
7770
|
},
|
|
6725
7771
|
yes: {
|
|
@@ -6745,11 +7791,274 @@ const uninstallCommand = defineRdeCommand({
|
|
|
6745
7791
|
}
|
|
6746
7792
|
});
|
|
6747
7793
|
//#endregion
|
|
7794
|
+
//#region src/steps/upgrade.ts
|
|
7795
|
+
const BACKUPS_DIR_MODE = 448;
|
|
7796
|
+
const RELEASE_TAG = /^v?[01]\.(\d+\.\d+(?:\.\d+)?)/;
|
|
7797
|
+
function hostDeps(ctx) {
|
|
7798
|
+
return {
|
|
7799
|
+
runtime: hostRuntimeDeps(ctx.state.name, ctx.signal),
|
|
7800
|
+
client: (baseUrl, auth) => createMetabaseClient(baseUrl, auth, ctx.signal),
|
|
7801
|
+
timing: DEFAULT_HEALTH_TIMING,
|
|
7802
|
+
releases: releaseLookup(ctx.signal),
|
|
7803
|
+
now: () => /* @__PURE__ */ new Date()
|
|
7804
|
+
};
|
|
7805
|
+
}
|
|
7806
|
+
function imageTag(ref) {
|
|
7807
|
+
const colon = ref.lastIndexOf(":");
|
|
7808
|
+
if (colon === -1 || colon < ref.lastIndexOf("/")) return null;
|
|
7809
|
+
return ref.slice(colon + 1);
|
|
7810
|
+
}
|
|
7811
|
+
function releaseOf(tag) {
|
|
7812
|
+
const match = RELEASE_TAG.exec(tag);
|
|
7813
|
+
if (match === null || match[1] === void 0) return null;
|
|
7814
|
+
return semver.coerce(match[1]);
|
|
7815
|
+
}
|
|
7816
|
+
function downgradeReason(current, targetTag) {
|
|
7817
|
+
if (targetTag === null) return null;
|
|
7818
|
+
const running = releaseOf(current);
|
|
7819
|
+
const target = releaseOf(targetTag);
|
|
7820
|
+
if (running === null || target === null || !semver.lt(target, running)) return null;
|
|
7821
|
+
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`;
|
|
7822
|
+
}
|
|
7823
|
+
function backupStamp(now) {
|
|
7824
|
+
return now.toISOString().replace(/\.\d{3}Z$/, "Z").replaceAll(":", "-");
|
|
7825
|
+
}
|
|
7826
|
+
async function ensureRunning(ctx, deps, client) {
|
|
7827
|
+
await startMetabaseStep(() => deps).run(ctx);
|
|
7828
|
+
return runningVersion(client);
|
|
7829
|
+
}
|
|
7830
|
+
function dockerTarget(runtime, to) {
|
|
7831
|
+
const image = to ?? runtime.image;
|
|
7832
|
+
return {
|
|
7833
|
+
kind: "docker",
|
|
7834
|
+
next: {
|
|
7835
|
+
...runtime,
|
|
7836
|
+
image
|
|
7837
|
+
},
|
|
7838
|
+
label: image,
|
|
7839
|
+
releaseTag: imageTag(image)
|
|
7840
|
+
};
|
|
7841
|
+
}
|
|
7842
|
+
async function jarTarget(ctx, runtime, to, releases) {
|
|
7843
|
+
const spec = to ?? "latest";
|
|
7844
|
+
const parsed = parseJarSpec(spec);
|
|
7845
|
+
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`);
|
|
7846
|
+
let source;
|
|
7847
|
+
if (parsed.kind === "latest") {
|
|
7848
|
+
const version = await releases.latestVersion();
|
|
7849
|
+
ctx.ui.info(`The latest Metabase release is ${version}`);
|
|
7850
|
+
source = {
|
|
7851
|
+
kind: "release",
|
|
7852
|
+
version
|
|
7853
|
+
};
|
|
7854
|
+
} else source = parsed;
|
|
7855
|
+
return {
|
|
7856
|
+
kind: "jar",
|
|
7857
|
+
previous: runtime,
|
|
7858
|
+
next: {
|
|
7859
|
+
...runtime,
|
|
7860
|
+
source
|
|
7861
|
+
},
|
|
7862
|
+
label: describeJar(source),
|
|
7863
|
+
releaseTag: source.kind === "release" ? source.version : null
|
|
7864
|
+
};
|
|
7865
|
+
}
|
|
7866
|
+
function resolveTarget(ctx, configured, to, releases) {
|
|
7867
|
+
switch (configured.runtime.kind) {
|
|
7868
|
+
case "docker": return Promise.resolve(dockerTarget(configured.runtime, to));
|
|
7869
|
+
case "jar": return jarTarget(ctx, configured.runtime, to, releases);
|
|
7870
|
+
}
|
|
7871
|
+
}
|
|
7872
|
+
async function pullImage(ctx, docker, target) {
|
|
7873
|
+
const image = target.next.image;
|
|
7874
|
+
const spinner = ctx.ui.spinner(`Pulling ${image}`);
|
|
7875
|
+
try {
|
|
7876
|
+
await docker.imagePull(image, { onLine: (line) => spinner.update(line) });
|
|
7877
|
+
} catch (error) {
|
|
7878
|
+
spinner.fail(`Pulling ${image} failed`);
|
|
7879
|
+
throw error;
|
|
7880
|
+
}
|
|
7881
|
+
const id = await docker.imageId(image);
|
|
7882
|
+
if (id === null) {
|
|
7883
|
+
spinner.fail(`Pulled ${image}, but Docker has no such image`);
|
|
7884
|
+
throw new ConfigError(`docker pull ${image} succeeded, yet \`docker image inspect\` finds nothing`);
|
|
7885
|
+
}
|
|
7886
|
+
const running = await docker.containerImageId(target.next.containerName);
|
|
7887
|
+
spinner.stop(`Pulled ${image}`);
|
|
7888
|
+
return id === running;
|
|
7889
|
+
}
|
|
7890
|
+
function jarUnchanged(target, current, jarsDir) {
|
|
7891
|
+
const source = target.next.source;
|
|
7892
|
+
if (source.kind === "release") return source.version === current;
|
|
7893
|
+
return jarPathOf(source, jarsDir) === jarPathOf(target.previous.source, jarsDir);
|
|
7894
|
+
}
|
|
7895
|
+
async function fetchJar(ctx, deps, target, current) {
|
|
7896
|
+
if (jarUnchanged(target, current, deps.runtime.jarsDir)) return true;
|
|
7897
|
+
const spinner = ctx.ui.spinner(`Fetching ${target.label}`);
|
|
7898
|
+
try {
|
|
7899
|
+
await runtimeFromState(target.next, deps.runtime).install({ onProgress: (line) => spinner.update(line) });
|
|
7900
|
+
} catch (error) {
|
|
7901
|
+
spinner.fail(`Fetching ${target.label} failed`);
|
|
7902
|
+
throw error;
|
|
7903
|
+
}
|
|
7904
|
+
spinner.stop(`Fetched ${target.label}`);
|
|
7905
|
+
return false;
|
|
7906
|
+
}
|
|
7907
|
+
function fetchTarget(ctx, deps, target, current) {
|
|
7908
|
+
switch (target.kind) {
|
|
7909
|
+
case "docker": return pullImage(ctx, deps.runtime.docker, target);
|
|
7910
|
+
case "jar": return fetchJar(ctx, deps, target, current);
|
|
7911
|
+
}
|
|
7912
|
+
}
|
|
7913
|
+
const SWITCH_TEXT = {
|
|
7914
|
+
docker: "Metabase stops while the container is recreated from the target image.",
|
|
7915
|
+
jar: "Metabase stops and starts again from the target jar."
|
|
7916
|
+
};
|
|
7917
|
+
function planLines(configured, current, target, backup) {
|
|
7918
|
+
return [
|
|
7919
|
+
`Running: ${current} (${describeRuntime(configured.runtime)})`,
|
|
7920
|
+
`Target: ${target.label}`,
|
|
7921
|
+
`Backup: ${backup}`,
|
|
7922
|
+
"",
|
|
7923
|
+
SWITCH_TEXT[configured.runtime.kind],
|
|
7924
|
+
"Its application database is migrated forward on the first start and cannot be moved back, so",
|
|
7925
|
+
"the whole data directory is copied to the backup path first."
|
|
7926
|
+
];
|
|
7927
|
+
}
|
|
7928
|
+
async function backUp(ctx, dataDir, backup) {
|
|
7929
|
+
const spinner = ctx.ui.spinner(`Backing up ${dataDir}`);
|
|
7930
|
+
try {
|
|
7931
|
+
await promises.mkdir(instanceBackupsDir(ctx.state.name), {
|
|
7932
|
+
recursive: true,
|
|
7933
|
+
mode: BACKUPS_DIR_MODE
|
|
7934
|
+
});
|
|
7935
|
+
await promises.cp(dataDir, backup, { recursive: true });
|
|
7936
|
+
} catch (error) {
|
|
7937
|
+
spinner.fail("Backup failed; nothing was changed");
|
|
7938
|
+
throw error;
|
|
7939
|
+
}
|
|
7940
|
+
spinner.stop(`Backed up to ${backup}`);
|
|
7941
|
+
}
|
|
7942
|
+
function restoreSteps(runtime, statePath) {
|
|
7943
|
+
switch (runtime.kind) {
|
|
7944
|
+
case "docker": return [
|
|
7945
|
+
` set runtime.image to "${runtime.image}" in ${statePath}`,
|
|
7946
|
+
` docker rm -f ${runtime.containerName}`,
|
|
7947
|
+
" rde init --only runtime"
|
|
7948
|
+
];
|
|
7949
|
+
case "jar": return [` set runtime.source to ${JSON.stringify(runtime.source)} in ${statePath}`];
|
|
7950
|
+
}
|
|
7951
|
+
}
|
|
7952
|
+
function restoreLines(ctx, configured, dataDir, backup) {
|
|
7953
|
+
return [
|
|
7954
|
+
`The data directory as it was before the upgrade is at ${backup}.`,
|
|
7955
|
+
`To go back to ${describeRuntime(configured.runtime)}:`,
|
|
7956
|
+
" rde stop",
|
|
7957
|
+
` rm -rf ${dataDir} && cp -R ${backup} ${dataDir}`,
|
|
7958
|
+
...restoreSteps(configured.runtime, instanceStatePath(ctx.state.name)),
|
|
7959
|
+
" rde start"
|
|
7960
|
+
];
|
|
7961
|
+
}
|
|
7962
|
+
async function switchTo(ctx, deps, target) {
|
|
7963
|
+
const configured = requireConfiguredRuntime(ctx.state);
|
|
7964
|
+
if (configured.runtime.kind === "docker") await deps.runtime.docker.containerRemove(configured.runtime.containerName);
|
|
7965
|
+
await ctx.saveState({
|
|
7966
|
+
...ctx.state,
|
|
7967
|
+
runtime: target.next,
|
|
7968
|
+
version: null
|
|
7969
|
+
});
|
|
7970
|
+
const spinner = ctx.ui.spinner(`Installing ${target.label}`);
|
|
7971
|
+
try {
|
|
7972
|
+
await runtimeFromState(target.next, deps.runtime).install({ onProgress: (line) => spinner.update(line) });
|
|
7973
|
+
} catch (error) {
|
|
7974
|
+
spinner.fail(`Installing ${target.label} failed`);
|
|
7975
|
+
throw error;
|
|
7976
|
+
}
|
|
7977
|
+
spinner.stop(`Installed ${target.label}`);
|
|
7978
|
+
}
|
|
7979
|
+
async function startUpgraded(ctx, deps, client, previous, backup) {
|
|
7980
|
+
try {
|
|
7981
|
+
return await ensureRunning(ctx, deps, client);
|
|
7982
|
+
} catch (error) {
|
|
7983
|
+
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");
|
|
7984
|
+
throw error;
|
|
7985
|
+
}
|
|
7986
|
+
}
|
|
7987
|
+
async function upgrade(ctx, options, deps = hostDeps) {
|
|
7988
|
+
const resolved = deps(ctx);
|
|
7989
|
+
const configured = requireConfiguredRuntime(ctx.state);
|
|
7990
|
+
const client = resolved.client(configured.url, NO_AUTH);
|
|
7991
|
+
const current = await ensureRunning(ctx, resolved, client);
|
|
7992
|
+
const target = await resolveTarget(ctx, configured, options.to, resolved.releases);
|
|
7993
|
+
const refusal = downgradeReason(current, target.releaseTag);
|
|
7994
|
+
if (refusal !== null) throw new ConfigError(refusal);
|
|
7995
|
+
if (await fetchTarget(ctx, resolved, target, current)) {
|
|
7996
|
+
ctx.ui.success(`Already on ${current}: ${target.label} is what the instance runs`);
|
|
7997
|
+
return {
|
|
7998
|
+
outcome: "unchanged",
|
|
7999
|
+
version: current
|
|
8000
|
+
};
|
|
8001
|
+
}
|
|
8002
|
+
const backup = join(instanceBackupsDir(ctx.state.name), backupStamp(resolved.now()));
|
|
8003
|
+
ctx.ui.note(planLines(configured, current, target, backup).join("\n"), "rde upgrade");
|
|
8004
|
+
if (!options.yes) {
|
|
8005
|
+
if (!await ctx.prompts.confirm({
|
|
8006
|
+
message: "Upgrade now?",
|
|
8007
|
+
initialValue: true
|
|
8008
|
+
})) {
|
|
8009
|
+
ctx.ui.info("Nothing changed");
|
|
8010
|
+
return { outcome: "declined" };
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
const runtime = runtimeFromState(configured.runtime, resolved.runtime);
|
|
8014
|
+
const stopping = ctx.ui.spinner("Stopping Metabase");
|
|
8015
|
+
await runtime.stop();
|
|
8016
|
+
stopping.stop("Metabase stopped");
|
|
8017
|
+
await backUp(ctx, resolved.runtime.dataDir, backup);
|
|
8018
|
+
await switchTo(ctx, resolved, target);
|
|
8019
|
+
const to = await startUpgraded(ctx, resolved, client, configured, backup);
|
|
8020
|
+
ctx.ui.success(`Upgraded Metabase from ${current} to ${to}; the backup at ${backup} can go once you are happy with it`);
|
|
8021
|
+
return {
|
|
8022
|
+
outcome: "upgraded",
|
|
8023
|
+
from: current,
|
|
8024
|
+
to,
|
|
8025
|
+
backup
|
|
8026
|
+
};
|
|
8027
|
+
}
|
|
8028
|
+
//#endregion
|
|
8029
|
+
//#region src/commands/upgrade.ts
|
|
8030
|
+
const upgradeCommand = defineRdeCommand({
|
|
8031
|
+
meta: {
|
|
8032
|
+
name: "upgrade",
|
|
8033
|
+
description: "Move the instance to a newer Metabase, backing up its data first"
|
|
8034
|
+
},
|
|
8035
|
+
args: {
|
|
8036
|
+
to: {
|
|
8037
|
+
type: "string",
|
|
8038
|
+
description: "Docker: image to upgrade to (default: the instance's image, pulled again); jar: latest, a release tag, or a .jar path (default: latest)"
|
|
8039
|
+
},
|
|
8040
|
+
yes: {
|
|
8041
|
+
type: "boolean",
|
|
8042
|
+
description: "Do not ask for confirmation",
|
|
8043
|
+
default: false
|
|
8044
|
+
}
|
|
8045
|
+
},
|
|
8046
|
+
async run(args) {
|
|
8047
|
+
if (!args.yes && !stdinIsTty()) throw new ConfigError("`rde upgrade` asks for confirmation; pass --yes to upgrade without a terminal");
|
|
8048
|
+
const name = await defaultInstanceName();
|
|
8049
|
+
await requireInstanceState(name);
|
|
8050
|
+
await upgrade(await createWizardContext(name, { signal: interruptSignal }), {
|
|
8051
|
+
to: args.to ?? null,
|
|
8052
|
+
yes: args.yes
|
|
8053
|
+
});
|
|
8054
|
+
}
|
|
8055
|
+
});
|
|
8056
|
+
//#endregion
|
|
6748
8057
|
//#region src/main.ts
|
|
6749
8058
|
const main = defineCommand({
|
|
6750
8059
|
meta: {
|
|
6751
8060
|
name: "rde",
|
|
6752
|
-
version: "0.1.
|
|
8061
|
+
version: "0.1.1",
|
|
6753
8062
|
description: "Prepares a local machine for data-engineering work with Metabase and coding agents"
|
|
6754
8063
|
},
|
|
6755
8064
|
subCommands: {
|
|
@@ -6761,6 +8070,7 @@ const main = defineCommand({
|
|
|
6761
8070
|
open,
|
|
6762
8071
|
db,
|
|
6763
8072
|
doctor,
|
|
8073
|
+
upgrade: upgradeCommand,
|
|
6764
8074
|
uninstall: uninstallCommand
|
|
6765
8075
|
}
|
|
6766
8076
|
});
|