@metabase/cli 0.1.0-alpha.workspaces-commands.818a8f1
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/LICENSE +661 -0
- package/README.md +762 -0
- package/dist/api-key-D9XxErQn.mjs +13 -0
- package/dist/archive-BPG5c88Y.mjs +38 -0
- package/dist/auth--Hpjwlaf.mjs +18 -0
- package/dist/body-DwU2s6Pg.mjs +19 -0
- package/dist/body-flags-7oqLhu5j.mjs +14 -0
- package/dist/branches-BbcoJXfp.mjs +41 -0
- package/dist/cancel-task-BDas45YO.mjs +29 -0
- package/dist/card-C31pGtBZ.mjs +113 -0
- package/dist/card-D4zZSPUb.mjs +19 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +61 -0
- package/dist/command-augment-D9pI9Vbh.mjs +11 -0
- package/dist/create-Bd_U1zWU.mjs +124 -0
- package/dist/create-CCzsCZMm.mjs +47 -0
- package/dist/create-CwVcoq0O.mjs +43 -0
- package/dist/create-DpnjQvPw.mjs +43 -0
- package/dist/create-_UOeEXAj.mjs +39 -0
- package/dist/create-branch-sDttBORB.mjs +54 -0
- package/dist/credentials-C0xKke5D.mjs +84 -0
- package/dist/current-task-BGt1mqaX.mjs +35 -0
- package/dist/database-4V1iiPEx.mjs +17 -0
- package/dist/database-BTX5qbSv.mjs +33 -0
- package/dist/db-Dm2u2ISJ.mjs +17 -0
- package/dist/delete-DRBTgyus.mjs +47 -0
- package/dist/delete-DUC_stoL.mjs +47 -0
- package/dist/delete-runtime-inOVw3IX.mjs +58 -0
- package/dist/delete-table-9Is631O_.mjs +47 -0
- package/dist/deprovision-BAMzZc6f.mjs +60 -0
- package/dist/dirty-CLjHbz6J.mjs +32 -0
- package/dist/docker-QWVMG2gl.mjs +605 -0
- package/dist/eid-BNhutC1U.mjs +13 -0
- package/dist/export-D2Anfu3p.mjs +97 -0
- package/dist/field-Dhs2AND3.mjs +13 -0
- package/dist/field-QwBMAWsq.mjs +76 -0
- package/dist/flag-pair-CWvvzDJ_.mjs +17 -0
- package/dist/get-2po1uv9i.mjs +35 -0
- package/dist/get-BHJA78zg.mjs +35 -0
- package/dist/get-CAPLfawI.mjs +35 -0
- package/dist/get-CAVVmdMX.mjs +49 -0
- package/dist/get-DDWpubE8.mjs +36 -0
- package/dist/get-DhIoNeOp.mjs +35 -0
- package/dist/get-qPOsuTPw.mjs +35 -0
- package/dist/has-remote-changes-DAL5jetW.mjs +63 -0
- package/dist/import-CUMxUfSF.mjs +92 -0
- package/dist/input-BNqSFl38.mjs +33 -0
- package/dist/is-dirty-B10S6MG0.mjs +35 -0
- package/dist/is-dirty-CUuq-aB6.mjs +9 -0
- package/dist/key-CyhOpgWt.mjs +12 -0
- package/dist/license-DtsGJi3l.mjs +17 -0
- package/dist/list-B8s7Qnzk.mjs +31 -0
- package/dist/list-C5MGydGU.mjs +31 -0
- package/dist/list-DeFGwhhJ.mjs +60 -0
- package/dist/list-OBx5B3gd.mjs +39 -0
- package/dist/list-Y7iGsOfE.mjs +31 -0
- package/dist/list-evtQS7jl.mjs +39 -0
- package/dist/list-qetY9OIN.mjs +31 -0
- package/dist/login-Dqw9ZtCx.mjs +172 -0
- package/dist/logout-DwYJ5OUi.mjs +74 -0
- package/dist/logs-B_lrY7Js.mjs +57 -0
- package/dist/manifest-wzEFG0JB.mjs +124 -0
- package/dist/package-t8dKf4m_.mjs +73 -0
- package/dist/parse-id-C1prc9US.mjs +12 -0
- package/dist/poll-D2sXM5rc.mjs +49 -0
- package/dist/poll-task-Byiunmaj.mjs +194 -0
- package/dist/prompt-fXeNtj0M.mjs +40 -0
- package/dist/provision-DC4_HWZD.mjs +80 -0
- package/dist/ps-1bZKIwWh.mjs +9 -0
- package/dist/ps-BiOrecEe.mjs +78 -0
- package/dist/query-BnGVGeM3.mjs +100 -0
- package/dist/remove-Bx48o-0S.mjs +62 -0
- package/dist/remove-DecoZzNd.mjs +97 -0
- package/dist/render-DlBijc5i.mjs +179 -0
- package/dist/run-D4NgvaRh.mjs +87 -0
- package/dist/runtime-DUgFfYkN.mjs +950 -0
- package/dist/search-4wKx5ug2.mjs +171 -0
- package/dist/set-BZnCRL4c.mjs +66 -0
- package/dist/set-DCjrmTFm.mjs +66 -0
- package/dist/setting-C4vQSqer.mjs +18 -0
- package/dist/setting-DM7pm7yh.mjs +55 -0
- package/dist/setup-Dqh9hN6l.mjs +70 -0
- package/dist/start-xXQypG5L.mjs +324 -0
- package/dist/stash-ZZkmW_V7.mjs +106 -0
- package/dist/status-9KAPIpX8.mjs +31 -0
- package/dist/status-DezF-PIM.mjs +63 -0
- package/dist/status-JH6BZppo.mjs +55 -0
- package/dist/stop-br-ZOnve.mjs +80 -0
- package/dist/sync-C7VOWD00.mjs +26 -0
- package/dist/table-BvAr2ixC.mjs +75 -0
- package/dist/table-D-Mb5Nvw.mjs +16 -0
- package/dist/transform-CqxZwhGs.mjs +21 -0
- package/dist/transform-DfVkUttP.mjs +137 -0
- package/dist/transform-job-DuB_OjhO.mjs +91 -0
- package/dist/transform-job-HjbqjEoP.mjs +19 -0
- package/dist/translate-DJxDVAE4.mjs +110 -0
- package/dist/update-CDtm71m2.mjs +50 -0
- package/dist/update-DYVeVjk2.mjs +76 -0
- package/dist/update-DxKlQ0hP.mjs +50 -0
- package/dist/url-DP88YHNo.mjs +53 -0
- package/dist/wait-Cj_8wu4y.mjs +52 -0
- package/dist/wait-DwZN3ZwR.mjs +19 -0
- package/dist/wait-flags-CjW4ogUJ.mjs +35 -0
- package/dist/workspace-CbwR0vX_.mjs +24 -0
- package/dist/workspace-Dr9lWU3D.mjs +72 -0
- package/dist/workspace-credentials-q5RRFMT8.mjs +139 -0
- package/package.json +62 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
import "./package-t8dKf4m_.mjs";
|
|
2
|
+
import "./command-augment-D9pI9Vbh.mjs";
|
|
3
|
+
import { renderItem, warn } from "./render-DlBijc5i.mjs";
|
|
4
|
+
import { ConfigError, connectionFlags, defineMetabaseCommand, errorMessage, localUrl, outputFlags, parseInteger, parseOptionalInteger, profileFlag, resolveLicenseToken } from "./runtime-DUgFfYkN.mjs";
|
|
5
|
+
import { parseId } from "./parse-id-C1prc9US.mjs";
|
|
6
|
+
import { pollUntil } from "./poll-D2sXM5rc.mjs";
|
|
7
|
+
import { Workspace } from "./workspace-Dr9lWU3D.mjs";
|
|
8
|
+
import { CONTAINER_REPO_DIR, checkDockerReady, containerLifecycleStatus, containerNameFor, pullImage, removeContainer, runProcess, runWorkspaceContainer, scrubContainerConfig, waitForConfigConsumed } from "./docker-QWVMG2gl.mjs";
|
|
9
|
+
import { REPO_SYNC_MODES, RepoSyncMode, buildCredentialsJson, generateWorkspaceCredentials, injectCredentialsIntoConfig, injectRepoSettingsIntoConfig } from "./workspace-credentials-q5RRFMT8.mjs";
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
import { stat } from "node:fs/promises";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { createServer } from "node:net";
|
|
14
|
+
|
|
15
|
+
//#region src/core/http/probe.ts
|
|
16
|
+
async function probeHealth(url, timeoutMs) {
|
|
17
|
+
try {
|
|
18
|
+
const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) });
|
|
19
|
+
return {
|
|
20
|
+
ready: response.ok,
|
|
21
|
+
status: response.status
|
|
22
|
+
};
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error instanceof Error) return {
|
|
25
|
+
ready: false,
|
|
26
|
+
status: null
|
|
27
|
+
};
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
//#endregion
|
|
33
|
+
//#region src/runtime/port.ts
|
|
34
|
+
const PORT_SCAN_LIMIT = 100;
|
|
35
|
+
function isPortFree(port) {
|
|
36
|
+
return new Promise((resolve$1) => {
|
|
37
|
+
const server = createServer();
|
|
38
|
+
server.unref();
|
|
39
|
+
server.once("error", () => resolve$1(false));
|
|
40
|
+
server.once("listening", () => {
|
|
41
|
+
server.close(() => resolve$1(true));
|
|
42
|
+
});
|
|
43
|
+
server.listen(port, "127.0.0.1");
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async function findFreePort(start) {
|
|
47
|
+
for (let port = start; port < start + PORT_SCAN_LIMIT; port++) if (await isPortFree(port)) return port;
|
|
48
|
+
throw new ConfigError(`no free port in range ${start}..${start + PORT_SCAN_LIMIT - 1}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
//#endregion
|
|
52
|
+
//#region src/commands/workspace/start.ts
|
|
53
|
+
const DEFAULT_IMAGE = "metabase/metabase-dev:feature-workspaces-v2";
|
|
54
|
+
const DEFAULT_HOST_PORT = 3e3;
|
|
55
|
+
const DEFAULT_HEALTH_TIMEOUT_MS = 18e4;
|
|
56
|
+
const DEFAULT_CONFIG_CONSUMED_TIMEOUT_MS = 6e4;
|
|
57
|
+
const HEALTH_INTERVAL_MS = 2e3;
|
|
58
|
+
const HEALTH_MAX_INTERVAL_MS = 1e4;
|
|
59
|
+
const HEALTH_PROBE_TIMEOUT_MS = 4e3;
|
|
60
|
+
const DEFAULT_REPO_MODE = "read-write";
|
|
61
|
+
const REPO_FILE_URL = `file://${CONTAINER_REPO_DIR}`;
|
|
62
|
+
const StartResult = z.object({
|
|
63
|
+
workspace_id: z.number().int().positive(),
|
|
64
|
+
workspace_name: z.string(),
|
|
65
|
+
container_name: z.string(),
|
|
66
|
+
state: z.enum(["running", "starting"]),
|
|
67
|
+
host_port: z.number().int().positive(),
|
|
68
|
+
url: z.string(),
|
|
69
|
+
image: z.string()
|
|
70
|
+
});
|
|
71
|
+
const startResultView = {
|
|
72
|
+
compactPick: StartResult.pick({
|
|
73
|
+
workspace_id: true,
|
|
74
|
+
workspace_name: true,
|
|
75
|
+
state: true,
|
|
76
|
+
url: true
|
|
77
|
+
}).strip(),
|
|
78
|
+
tableColumns: [
|
|
79
|
+
{
|
|
80
|
+
key: "workspace_id",
|
|
81
|
+
label: "ID"
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
key: "workspace_name",
|
|
85
|
+
label: "Name"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
key: "state",
|
|
89
|
+
label: "State"
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
key: "url",
|
|
93
|
+
label: "URL"
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
};
|
|
97
|
+
var start_default = defineMetabaseCommand({
|
|
98
|
+
meta: {
|
|
99
|
+
name: "start",
|
|
100
|
+
description: "Start a local Docker container that serves as the workspace's dev instance"
|
|
101
|
+
},
|
|
102
|
+
args: {
|
|
103
|
+
...outputFlags,
|
|
104
|
+
...profileFlag,
|
|
105
|
+
...connectionFlags,
|
|
106
|
+
id: {
|
|
107
|
+
type: "positional",
|
|
108
|
+
description: "Workspace id",
|
|
109
|
+
required: true
|
|
110
|
+
},
|
|
111
|
+
port: {
|
|
112
|
+
type: "string",
|
|
113
|
+
description: `Host port to bind (default: ${DEFAULT_HOST_PORT}; auto-shifts up if taken)`
|
|
114
|
+
},
|
|
115
|
+
image: {
|
|
116
|
+
type: "string",
|
|
117
|
+
description: `Docker image to run (default: ${DEFAULT_IMAGE})`,
|
|
118
|
+
default: DEFAULT_IMAGE
|
|
119
|
+
},
|
|
120
|
+
wait: {
|
|
121
|
+
type: "boolean",
|
|
122
|
+
description: "Block until /api/health is ready before returning. Default: return as soon as the container has consumed config.yml.",
|
|
123
|
+
default: false
|
|
124
|
+
},
|
|
125
|
+
timeout: {
|
|
126
|
+
type: "string",
|
|
127
|
+
description: `Health check deadline in ms (used with --wait; default: ${DEFAULT_HEALTH_TIMEOUT_MS})`,
|
|
128
|
+
default: String(DEFAULT_HEALTH_TIMEOUT_MS)
|
|
129
|
+
},
|
|
130
|
+
pull: {
|
|
131
|
+
type: "boolean",
|
|
132
|
+
description: "Pull the image before starting",
|
|
133
|
+
default: true
|
|
134
|
+
},
|
|
135
|
+
metadata: {
|
|
136
|
+
type: "boolean",
|
|
137
|
+
description: "Fetch the workspace's warehouse metadata and stage it inside the container",
|
|
138
|
+
default: true
|
|
139
|
+
},
|
|
140
|
+
force: {
|
|
141
|
+
type: "boolean",
|
|
142
|
+
description: "If a container for this workspace already exists, remove it first",
|
|
143
|
+
default: false
|
|
144
|
+
},
|
|
145
|
+
repo: {
|
|
146
|
+
type: "string",
|
|
147
|
+
description: `Bind-mount a host directory (typically a remote-sync git repo) into the container at ${CONTAINER_REPO_DIR}. Sets remote-sync-url=${REPO_FILE_URL} in the workspace config.yml so the child boots already wired to the repo.`
|
|
148
|
+
},
|
|
149
|
+
"repo-branch": {
|
|
150
|
+
type: "string",
|
|
151
|
+
description: "Branch to set as remote-sync-branch (default: the current branch of the host repo, read from HEAD)"
|
|
152
|
+
},
|
|
153
|
+
"repo-mode": {
|
|
154
|
+
type: "string",
|
|
155
|
+
description: "remote-sync-type: 'read-write' (default) or 'read-only'",
|
|
156
|
+
default: DEFAULT_REPO_MODE
|
|
157
|
+
}
|
|
158
|
+
},
|
|
159
|
+
outputSchema: StartResult,
|
|
160
|
+
examples: [
|
|
161
|
+
"metabase workspace start 1",
|
|
162
|
+
"metabase workspace start 1 --wait",
|
|
163
|
+
"metabase workspace start 1 --port 3100",
|
|
164
|
+
"metabase workspace start 1 --image metabase/metabase-dev:feature-workspaces-v2 --no-pull",
|
|
165
|
+
"metabase workspace start 1 --force",
|
|
166
|
+
"metabase workspace start 1 --repo /path/to/sync-repo --wait",
|
|
167
|
+
"metabase workspace start 1 --repo /path/to/sync-repo --repo-branch dev --repo-mode read-only"
|
|
168
|
+
],
|
|
169
|
+
async run({ args, ctx, getClient, getResolvedConfig }) {
|
|
170
|
+
const workspaceId = parseId(args.id);
|
|
171
|
+
const containerName = containerNameFor(workspaceId);
|
|
172
|
+
const requestedPort = parseOptionalInteger(args.port, {
|
|
173
|
+
name: "--port",
|
|
174
|
+
min: 1
|
|
175
|
+
});
|
|
176
|
+
const healthTimeoutMs = parseInteger(args.timeout ?? String(DEFAULT_HEALTH_TIMEOUT_MS), {
|
|
177
|
+
name: "--timeout",
|
|
178
|
+
min: 1e3
|
|
179
|
+
});
|
|
180
|
+
const client = await getClient();
|
|
181
|
+
const resolved = await getResolvedConfig();
|
|
182
|
+
const licenseToken = await resolveLicenseToken({});
|
|
183
|
+
await checkDockerReady();
|
|
184
|
+
await ensureNoExistingContainer(containerName, args.force);
|
|
185
|
+
const pullPromise = args.pull ? pullImage(args.image) : Promise.resolve();
|
|
186
|
+
const workspace = await client.requestParsed(Workspace, `/api/ee/workspace-manager/${workspaceId}`);
|
|
187
|
+
assertAllDatabasesProvisioned(workspace);
|
|
188
|
+
const hostPort = await resolveHostPort(requestedPort);
|
|
189
|
+
const [parentConfigYaml, metadataJson, repoOptions] = await Promise.all([
|
|
190
|
+
fetchConfigYaml(client, workspaceId),
|
|
191
|
+
args.metadata ? fetchMetadataJson(client, workspaceId) : Promise.resolve(null),
|
|
192
|
+
resolveRepoOptions({
|
|
193
|
+
hostPath: args.repo,
|
|
194
|
+
branch: args["repo-branch"],
|
|
195
|
+
mode: args["repo-mode"]
|
|
196
|
+
})
|
|
197
|
+
]);
|
|
198
|
+
const credentials = generateWorkspaceCredentials(workspaceId);
|
|
199
|
+
const configWithCredentials = injectCredentialsIntoConfig(parentConfigYaml, credentials);
|
|
200
|
+
const configYaml = repoOptions !== null ? injectRepoSettingsIntoConfig(configWithCredentials, repoOptions.repo) : configWithCredentials;
|
|
201
|
+
const credentialsJson = buildCredentialsJson(credentials);
|
|
202
|
+
await pullPromise;
|
|
203
|
+
await runWorkspaceContainer({
|
|
204
|
+
workspaceId,
|
|
205
|
+
workspaceName: workspace.name,
|
|
206
|
+
profile: resolved.profile,
|
|
207
|
+
parentUrl: resolved.url,
|
|
208
|
+
image: args.image,
|
|
209
|
+
hostPort,
|
|
210
|
+
configYaml,
|
|
211
|
+
credentialsJson,
|
|
212
|
+
metadataJson,
|
|
213
|
+
licenseToken,
|
|
214
|
+
bindMounts: repoOptions === null ? [] : [repoOptions.bindMount]
|
|
215
|
+
});
|
|
216
|
+
await waitForConfigConsumed(workspaceId, DEFAULT_CONFIG_CONSUMED_TIMEOUT_MS);
|
|
217
|
+
try {
|
|
218
|
+
await scrubContainerConfig(workspaceId);
|
|
219
|
+
} catch (error) {
|
|
220
|
+
warn(`could not scrub in-container config.yml: ${errorMessage(error)}`);
|
|
221
|
+
}
|
|
222
|
+
if (args.wait) await waitForHealth(hostPort, healthTimeoutMs);
|
|
223
|
+
const result = {
|
|
224
|
+
workspace_id: workspaceId,
|
|
225
|
+
workspace_name: workspace.name,
|
|
226
|
+
container_name: containerName,
|
|
227
|
+
state: args.wait ? "running" : "starting",
|
|
228
|
+
host_port: hostPort,
|
|
229
|
+
url: localUrl(hostPort),
|
|
230
|
+
image: args.image
|
|
231
|
+
};
|
|
232
|
+
renderItem(result, startResultView, ctx);
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
function assertAllDatabasesProvisioned(workspace) {
|
|
236
|
+
const databases = workspace.databases ?? [];
|
|
237
|
+
if (databases.length === 0) throw new ConfigError(`workspace ${workspace.id} has no databases — provision at least one before starting`);
|
|
238
|
+
const unready = databases.filter((entry) => entry.status !== "provisioned");
|
|
239
|
+
if (unready.length > 0) {
|
|
240
|
+
const summary = unready.map((entry) => `database ${entry.database_id}=${entry.status}`).join(", ");
|
|
241
|
+
throw new ConfigError(`workspace ${workspace.id} is not ready: ${summary}. Wait for provisioning to finish.`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
async function ensureNoExistingContainer(containerName, force) {
|
|
245
|
+
if (!force) {
|
|
246
|
+
const status = await containerLifecycleStatus(containerName);
|
|
247
|
+
if (status !== "missing") throw new ConfigError(`container ${containerName} already exists (state=${status}). Use --force to recreate, or stop/remove it first.`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
await removeContainer(containerName);
|
|
251
|
+
}
|
|
252
|
+
async function resolveHostPort(requested) {
|
|
253
|
+
if (requested !== null) {
|
|
254
|
+
if (!await isPortFree(requested)) throw new ConfigError(`port ${requested} is already in use`);
|
|
255
|
+
return requested;
|
|
256
|
+
}
|
|
257
|
+
if (await isPortFree(DEFAULT_HOST_PORT)) return DEFAULT_HOST_PORT;
|
|
258
|
+
return findFreePort(DEFAULT_HOST_PORT + 1);
|
|
259
|
+
}
|
|
260
|
+
async function fetchConfigYaml(client, workspaceId) {
|
|
261
|
+
const response = await client.requestRaw(`/api/ee/workspace-manager/${workspaceId}/config`, { expectContentType: "binary" });
|
|
262
|
+
return response.text();
|
|
263
|
+
}
|
|
264
|
+
async function fetchMetadataJson(client, workspaceId) {
|
|
265
|
+
const response = await client.requestRaw(`/api/ee/workspace-manager/${workspaceId}/metadata/export`, { expectContentType: "binary" });
|
|
266
|
+
return new Uint8Array(await response.arrayBuffer());
|
|
267
|
+
}
|
|
268
|
+
async function waitForHealth(hostPort, timeoutMs) {
|
|
269
|
+
const url = `${localUrl(hostPort)}/api/health`;
|
|
270
|
+
await pollUntil(() => probeHealth(url, HEALTH_PROBE_TIMEOUT_MS), (probe) => probe.ready, {
|
|
271
|
+
intervalMs: HEALTH_INTERVAL_MS,
|
|
272
|
+
maxIntervalMs: HEALTH_MAX_INTERVAL_MS,
|
|
273
|
+
backoff: "exponential",
|
|
274
|
+
timeoutMs
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
async function resolveRepoOptions(input) {
|
|
278
|
+
if (input.hostPath === void 0 || input.hostPath === "") {
|
|
279
|
+
const explicitBranch = input.branch !== void 0;
|
|
280
|
+
const explicitNonDefaultMode = input.mode !== void 0 && input.mode !== DEFAULT_REPO_MODE;
|
|
281
|
+
if (explicitBranch || explicitNonDefaultMode) throw new ConfigError("--repo-branch and --repo-mode require --repo to point at a host repo path");
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
const hostPath = resolve(input.hostPath);
|
|
285
|
+
const stats = await stat(hostPath).catch(() => null);
|
|
286
|
+
if (stats === null || !stats.isDirectory()) throw new ConfigError(`--repo path does not exist or is not a directory: ${hostPath}`);
|
|
287
|
+
const mode = parseRepoMode(input.mode);
|
|
288
|
+
const branch = input.branch ?? await detectBranch(hostPath);
|
|
289
|
+
return {
|
|
290
|
+
bindMount: {
|
|
291
|
+
hostPath,
|
|
292
|
+
containerPath: CONTAINER_REPO_DIR,
|
|
293
|
+
readOnly: mode === "read-only"
|
|
294
|
+
},
|
|
295
|
+
repo: {
|
|
296
|
+
url: REPO_FILE_URL,
|
|
297
|
+
branch,
|
|
298
|
+
mode
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
function parseRepoMode(raw) {
|
|
303
|
+
const result = RepoSyncMode.safeParse(raw ?? DEFAULT_REPO_MODE);
|
|
304
|
+
if (!result.success) throw new ConfigError(`invalid --repo-mode: "${raw}" (expected one of: ${REPO_SYNC_MODES.join(", ")})`);
|
|
305
|
+
return result.data;
|
|
306
|
+
}
|
|
307
|
+
async function detectBranch(hostPath) {
|
|
308
|
+
const result = await runProcess("git", [
|
|
309
|
+
"-C",
|
|
310
|
+
hostPath,
|
|
311
|
+
"symbolic-ref",
|
|
312
|
+
"--short",
|
|
313
|
+
"HEAD"
|
|
314
|
+
]).catch((error) => {
|
|
315
|
+
throw new ConfigError(`--repo-branch not provided and could not detect a branch at ${hostPath}: ${errorMessage(error)}`);
|
|
316
|
+
});
|
|
317
|
+
if (result.exitCode !== 0) throw new ConfigError(`--repo-branch not provided and \`git symbolic-ref\` at ${hostPath} failed: ${result.stderr.trim() || "no output"}`);
|
|
318
|
+
const branch = result.stdout.trim();
|
|
319
|
+
if (branch === "") throw new ConfigError(`--repo-branch not provided and HEAD at ${hostPath} resolved to an empty branch name`);
|
|
320
|
+
return branch;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
//#endregion
|
|
324
|
+
export { start_default as default };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import "./package-t8dKf4m_.mjs";
|
|
2
|
+
import "./command-augment-D9pI9Vbh.mjs";
|
|
3
|
+
import { renderItem } from "./render-DlBijc5i.mjs";
|
|
4
|
+
import { ConfigError, connectionFlags, defineMetabaseCommand, outputFlags, profileFlag } from "./runtime-DUgFfYkN.mjs";
|
|
5
|
+
import { parseId } from "./parse-id-C1prc9US.mjs";
|
|
6
|
+
import { REMOTE_SYNC_PATHS, SyncTask, pollFlags, pollSyncTask, throwIfFailedTask } from "./poll-task-Byiunmaj.mjs";
|
|
7
|
+
import "./poll-D2sXM5rc.mjs";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
//#region src/commands/sync/stash.ts
|
|
11
|
+
const SyncStashKickoff = z.object({
|
|
12
|
+
status: z.literal("success"),
|
|
13
|
+
message: z.string(),
|
|
14
|
+
task_id: z.number().int().positive()
|
|
15
|
+
});
|
|
16
|
+
const SyncStashResult = z.object({
|
|
17
|
+
status: z.literal("success"),
|
|
18
|
+
message: z.string(),
|
|
19
|
+
task_id: z.number().int().positive(),
|
|
20
|
+
final: SyncTask.nullable().optional()
|
|
21
|
+
});
|
|
22
|
+
const syncStashView = {
|
|
23
|
+
compactPick: SyncStashResult,
|
|
24
|
+
tableColumns: [
|
|
25
|
+
{
|
|
26
|
+
key: "task_id",
|
|
27
|
+
label: "Task ID"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "status",
|
|
31
|
+
label: "Status"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "message",
|
|
35
|
+
label: "Message"
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
};
|
|
39
|
+
const DEFAULT_STASH_MESSAGE = "Stashed from metabase CLI";
|
|
40
|
+
var stash_default = defineMetabaseCommand({
|
|
41
|
+
meta: {
|
|
42
|
+
name: "stash",
|
|
43
|
+
description: "Export current Metabase state to a new branch on the git remote"
|
|
44
|
+
},
|
|
45
|
+
args: {
|
|
46
|
+
...outputFlags,
|
|
47
|
+
...profileFlag,
|
|
48
|
+
...connectionFlags,
|
|
49
|
+
newBranch: {
|
|
50
|
+
type: "string",
|
|
51
|
+
description: "Branch to create and export to",
|
|
52
|
+
alias: "new-branch",
|
|
53
|
+
required: true
|
|
54
|
+
},
|
|
55
|
+
message: {
|
|
56
|
+
type: "string",
|
|
57
|
+
description: "Commit message",
|
|
58
|
+
alias: "m",
|
|
59
|
+
default: DEFAULT_STASH_MESSAGE
|
|
60
|
+
},
|
|
61
|
+
...pollFlags
|
|
62
|
+
},
|
|
63
|
+
outputSchema: SyncStashResult,
|
|
64
|
+
examples: ["metabase sync stash --new-branch wip", "metabase sync stash --new-branch wip -m \"work in progress\" --json"],
|
|
65
|
+
async run({ args, ctx, getClient }) {
|
|
66
|
+
const newBranch = args.newBranch.trim();
|
|
67
|
+
if (newBranch === "") throw new ConfigError("invalid new-branch: must not be blank");
|
|
68
|
+
const message = args.message.trim();
|
|
69
|
+
if (message === "") throw new ConfigError("invalid message: must not be blank");
|
|
70
|
+
const timeoutMs = parseId(args.timeout, "timeout");
|
|
71
|
+
const intervalMs = parseId(args.interval, "interval");
|
|
72
|
+
const body = {
|
|
73
|
+
new_branch: newBranch,
|
|
74
|
+
message
|
|
75
|
+
};
|
|
76
|
+
const client = await getClient();
|
|
77
|
+
const kickoff = await client.requestParsed(SyncStashKickoff, REMOTE_SYNC_PATHS.stash, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
body
|
|
80
|
+
});
|
|
81
|
+
if (!args.wait) {
|
|
82
|
+
const result$1 = {
|
|
83
|
+
status: kickoff.status,
|
|
84
|
+
message: kickoff.message,
|
|
85
|
+
task_id: kickoff.task_id
|
|
86
|
+
};
|
|
87
|
+
renderItem(result$1, syncStashView, ctx);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const final = await pollSyncTask(client, {
|
|
91
|
+
timeoutMs,
|
|
92
|
+
intervalMs
|
|
93
|
+
});
|
|
94
|
+
const result = {
|
|
95
|
+
status: kickoff.status,
|
|
96
|
+
message: kickoff.message,
|
|
97
|
+
task_id: kickoff.task_id,
|
|
98
|
+
final
|
|
99
|
+
};
|
|
100
|
+
renderItem(result, syncStashView, ctx);
|
|
101
|
+
throwIfFailedTask(final, "stash");
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
//#endregion
|
|
106
|
+
export { stash_default as default };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import "./package-t8dKf4m_.mjs";
|
|
2
|
+
import "./command-augment-D9pI9Vbh.mjs";
|
|
3
|
+
import { renderItem } from "./render-DlBijc5i.mjs";
|
|
4
|
+
import { account, credentials, defineMetabaseCommand, outputFlags } from "./runtime-DUgFfYkN.mjs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/commands/license/status.ts
|
|
8
|
+
const LicenseStatus = z.object({ present: z.boolean() });
|
|
9
|
+
const licenseStatusView = {
|
|
10
|
+
compactPick: LicenseStatus,
|
|
11
|
+
tableColumns: [{
|
|
12
|
+
key: "present",
|
|
13
|
+
label: "Present"
|
|
14
|
+
}]
|
|
15
|
+
};
|
|
16
|
+
var status_default = defineMetabaseCommand({
|
|
17
|
+
meta: {
|
|
18
|
+
name: "status",
|
|
19
|
+
description: "Show whether a license token is stored (does not reveal value)"
|
|
20
|
+
},
|
|
21
|
+
args: { ...outputFlags },
|
|
22
|
+
outputSchema: LicenseStatus,
|
|
23
|
+
examples: ["metabase license status", "metabase license status --json"],
|
|
24
|
+
async run({ ctx }) {
|
|
25
|
+
const present = await credentials.has(account.license);
|
|
26
|
+
renderItem({ present }, licenseStatusView, ctx);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
//#endregion
|
|
31
|
+
export { status_default as default };
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import "./package-t8dKf4m_.mjs";
|
|
2
|
+
import "./command-augment-D9pI9Vbh.mjs";
|
|
3
|
+
import { renderItem } from "./render-DlBijc5i.mjs";
|
|
4
|
+
import { connectionFlags, defineMetabaseCommand, outputFlags, profileFlag } from "./runtime-DUgFfYkN.mjs";
|
|
5
|
+
import { REMOTE_SYNC_PATHS, SyncTask, fetchCurrentTask, fetchOptionalParsed } from "./poll-task-Byiunmaj.mjs";
|
|
6
|
+
import "./poll-D2sXM5rc.mjs";
|
|
7
|
+
import { IsDirtyResult } from "./is-dirty-B10S6MG0.mjs";
|
|
8
|
+
import { z } from "zod";
|
|
9
|
+
|
|
10
|
+
//#region src/commands/sync/status.ts
|
|
11
|
+
const RemoteSyncBranch = z.string().nullable();
|
|
12
|
+
const SyncStatus = z.object({
|
|
13
|
+
branch: z.string().nullable(),
|
|
14
|
+
is_dirty: z.boolean(),
|
|
15
|
+
current_task: SyncTask.nullable()
|
|
16
|
+
});
|
|
17
|
+
const syncStatusView = {
|
|
18
|
+
compactPick: SyncStatus,
|
|
19
|
+
tableColumns: [
|
|
20
|
+
{
|
|
21
|
+
key: "branch",
|
|
22
|
+
label: "Branch"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "is_dirty",
|
|
26
|
+
label: "Dirty"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
key: "current_task",
|
|
30
|
+
label: "Current task"
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
};
|
|
34
|
+
var status_default = defineMetabaseCommand({
|
|
35
|
+
meta: {
|
|
36
|
+
name: "status",
|
|
37
|
+
description: "Show current sync state (branch, dirty, current task)"
|
|
38
|
+
},
|
|
39
|
+
args: {
|
|
40
|
+
...outputFlags,
|
|
41
|
+
...profileFlag,
|
|
42
|
+
...connectionFlags
|
|
43
|
+
},
|
|
44
|
+
outputSchema: SyncStatus,
|
|
45
|
+
examples: ["metabase sync status", "metabase sync status --json"],
|
|
46
|
+
async run({ ctx, getClient }) {
|
|
47
|
+
const client = await getClient();
|
|
48
|
+
const [branch, isDirty, currentTask] = await Promise.all([
|
|
49
|
+
fetchOptionalParsed(client, "/api/setting/remote-sync-branch", RemoteSyncBranch),
|
|
50
|
+
client.requestParsed(IsDirtyResult, REMOTE_SYNC_PATHS.isDirty),
|
|
51
|
+
fetchCurrentTask(client)
|
|
52
|
+
]);
|
|
53
|
+
const summary = {
|
|
54
|
+
branch,
|
|
55
|
+
is_dirty: isDirty.is_dirty,
|
|
56
|
+
current_task: currentTask
|
|
57
|
+
};
|
|
58
|
+
renderItem(summary, syncStatusView, ctx);
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
//#endregion
|
|
63
|
+
export { status_default as default };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import "./package-t8dKf4m_.mjs";
|
|
2
|
+
import "./command-augment-D9pI9Vbh.mjs";
|
|
3
|
+
import { renderItem } from "./render-DlBijc5i.mjs";
|
|
4
|
+
import { account, credentials, defineMetabaseCommand, originOnly, outputFlags, profileFlag, resolveProfileName } from "./runtime-DUgFfYkN.mjs";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
|
|
7
|
+
//#region src/commands/auth/status.ts
|
|
8
|
+
const AuthStatus = z.object({
|
|
9
|
+
profile: z.string(),
|
|
10
|
+
present: z.boolean(),
|
|
11
|
+
url: z.string().nullable()
|
|
12
|
+
});
|
|
13
|
+
const authStatusView = {
|
|
14
|
+
compactPick: AuthStatus,
|
|
15
|
+
tableColumns: [
|
|
16
|
+
{
|
|
17
|
+
key: "profile",
|
|
18
|
+
label: "Profile"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
key: "present",
|
|
22
|
+
label: "Authenticated"
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
key: "url",
|
|
26
|
+
label: "URL"
|
|
27
|
+
}
|
|
28
|
+
]
|
|
29
|
+
};
|
|
30
|
+
var status_default = defineMetabaseCommand({
|
|
31
|
+
meta: {
|
|
32
|
+
name: "status",
|
|
33
|
+
description: "Show authentication status for a profile"
|
|
34
|
+
},
|
|
35
|
+
args: {
|
|
36
|
+
...outputFlags,
|
|
37
|
+
...profileFlag
|
|
38
|
+
},
|
|
39
|
+
outputSchema: AuthStatus,
|
|
40
|
+
examples: ["metabase auth status --json", "metabase auth status --profile staging"],
|
|
41
|
+
async run({ args, ctx }) {
|
|
42
|
+
const profileName = resolveProfileName(args.profile);
|
|
43
|
+
const [url, apiKey] = await Promise.all([credentials.read(account.profileUrl(profileName)), credentials.read(account.profileApiKey(profileName))]);
|
|
44
|
+
const present = url !== null && apiKey !== null;
|
|
45
|
+
const payload = {
|
|
46
|
+
profile: profileName,
|
|
47
|
+
present,
|
|
48
|
+
url: url === null ? null : originOnly(url)
|
|
49
|
+
};
|
|
50
|
+
renderItem(payload, authStatusView, ctx);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
export { status_default as default };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import "./package-t8dKf4m_.mjs";
|
|
2
|
+
import "./command-augment-D9pI9Vbh.mjs";
|
|
3
|
+
import { renderItem } from "./render-DlBijc5i.mjs";
|
|
4
|
+
import { defineMetabaseCommand, outputFlags } from "./runtime-DUgFfYkN.mjs";
|
|
5
|
+
import { parseId } from "./parse-id-C1prc9US.mjs";
|
|
6
|
+
import "./poll-D2sXM5rc.mjs";
|
|
7
|
+
import { checkDockerReady, containerLifecycleStatus, containerNameFor, stopContainer } from "./docker-QWVMG2gl.mjs";
|
|
8
|
+
import { LocalWorkspaceState } from "./ps-BiOrecEe.mjs";
|
|
9
|
+
import { z } from "zod";
|
|
10
|
+
|
|
11
|
+
//#region src/commands/workspace/stop.ts
|
|
12
|
+
const StopResult = z.object({
|
|
13
|
+
workspace_id: z.number().int().positive(),
|
|
14
|
+
container_name: z.string(),
|
|
15
|
+
stopped: z.boolean(),
|
|
16
|
+
prior_state: LocalWorkspaceState.nullable()
|
|
17
|
+
});
|
|
18
|
+
const stopResultView = {
|
|
19
|
+
compactPick: StopResult.pick({
|
|
20
|
+
workspace_id: true,
|
|
21
|
+
stopped: true,
|
|
22
|
+
prior_state: true
|
|
23
|
+
}).strip(),
|
|
24
|
+
tableColumns: [
|
|
25
|
+
{
|
|
26
|
+
key: "workspace_id",
|
|
27
|
+
label: "ID"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
key: "container_name",
|
|
31
|
+
label: "Container"
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
key: "stopped",
|
|
35
|
+
label: "Stopped"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
key: "prior_state",
|
|
39
|
+
label: "Prior State"
|
|
40
|
+
}
|
|
41
|
+
]
|
|
42
|
+
};
|
|
43
|
+
var stop_default = defineMetabaseCommand({
|
|
44
|
+
meta: {
|
|
45
|
+
name: "stop",
|
|
46
|
+
description: "Stop the local Docker container for a workspace (does not remove it)"
|
|
47
|
+
},
|
|
48
|
+
args: {
|
|
49
|
+
...outputFlags,
|
|
50
|
+
id: {
|
|
51
|
+
type: "positional",
|
|
52
|
+
description: "Workspace id",
|
|
53
|
+
required: true
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
outputSchema: StopResult,
|
|
57
|
+
examples: ["metabase workspace stop 1", "metabase workspace stop 1 --json"],
|
|
58
|
+
async run({ args, ctx }) {
|
|
59
|
+
const workspaceId = parseId(args.id);
|
|
60
|
+
const containerName = containerNameFor(workspaceId);
|
|
61
|
+
await checkDockerReady();
|
|
62
|
+
const status = await containerLifecycleStatus(containerName);
|
|
63
|
+
const priorState = status === "missing" ? null : status;
|
|
64
|
+
let stopped = false;
|
|
65
|
+
if (status === "running") {
|
|
66
|
+
await stopContainer(containerName);
|
|
67
|
+
stopped = true;
|
|
68
|
+
}
|
|
69
|
+
const result = {
|
|
70
|
+
workspace_id: workspaceId,
|
|
71
|
+
container_name: containerName,
|
|
72
|
+
stopped,
|
|
73
|
+
prior_state: priorState
|
|
74
|
+
};
|
|
75
|
+
renderItem(result, stopResultView, ctx);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
//#endregion
|
|
80
|
+
export { stop_default as default };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { defineCommand } from "citty";
|
|
2
|
+
|
|
3
|
+
//#region src/commands/sync/index.ts
|
|
4
|
+
var sync_default = defineCommand({
|
|
5
|
+
meta: {
|
|
6
|
+
name: "sync",
|
|
7
|
+
description: "Remote-sync operations (import, export, status, branches)"
|
|
8
|
+
},
|
|
9
|
+
subCommands: {
|
|
10
|
+
status: () => import("./status-DezF-PIM.mjs").then((mod) => mod.default),
|
|
11
|
+
"is-dirty": () => import("./is-dirty-CUuq-aB6.mjs").then((mod) => mod.default),
|
|
12
|
+
"has-remote-changes": () => import("./has-remote-changes-DAL5jetW.mjs").then((mod) => mod.default),
|
|
13
|
+
dirty: () => import("./dirty-CLjHbz6J.mjs").then((mod) => mod.default),
|
|
14
|
+
"current-task": () => import("./current-task-BGt1mqaX.mjs").then((mod) => mod.default),
|
|
15
|
+
"cancel-task": () => import("./cancel-task-BDas45YO.mjs").then((mod) => mod.default),
|
|
16
|
+
wait: () => import("./wait-Cj_8wu4y.mjs").then((mod) => mod.default),
|
|
17
|
+
import: () => import("./import-CUMxUfSF.mjs").then((mod) => mod.default),
|
|
18
|
+
export: () => import("./export-D2Anfu3p.mjs").then((mod) => mod.default),
|
|
19
|
+
stash: () => import("./stash-ZZkmW_V7.mjs").then((mod) => mod.default),
|
|
20
|
+
branches: () => import("./branches-BbcoJXfp.mjs").then((mod) => mod.default),
|
|
21
|
+
"create-branch": () => import("./create-branch-sDttBORB.mjs").then((mod) => mod.default)
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
//#endregion
|
|
26
|
+
export { sync_default as default };
|