metabase-cli 0.2.2

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.
@@ -0,0 +1,1710 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // bin/metabase.ts
27
+ var import_node_fs3 = require("fs");
28
+ var import_node_path3 = require("path");
29
+ var import_commander9 = require("commander");
30
+
31
+ // src/commands/profile.ts
32
+ var import_commander = require("commander");
33
+
34
+ // src/config/store.ts
35
+ var fs = __toESM(require("fs"));
36
+ var path = __toESM(require("path"));
37
+ var os = __toESM(require("os"));
38
+ function getConfigDir() {
39
+ return path.join(os.homedir(), ".metabase-cli");
40
+ }
41
+ function getConfigFile() {
42
+ return path.join(getConfigDir(), "config.json");
43
+ }
44
+ function ensureConfigDir() {
45
+ if (!fs.existsSync(getConfigDir())) {
46
+ fs.mkdirSync(getConfigDir(), { recursive: true, mode: 448 });
47
+ }
48
+ }
49
+ function defaultConfig() {
50
+ return { activeProfile: "", profiles: {} };
51
+ }
52
+ function loadConfig() {
53
+ ensureConfigDir();
54
+ if (!fs.existsSync(getConfigFile())) {
55
+ return defaultConfig();
56
+ }
57
+ const raw = fs.readFileSync(getConfigFile(), "utf-8");
58
+ return JSON.parse(raw);
59
+ }
60
+ function saveConfig(config) {
61
+ ensureConfigDir();
62
+ fs.writeFileSync(getConfigFile(), JSON.stringify(config, null, 2), {
63
+ encoding: "utf-8",
64
+ mode: 384
65
+ });
66
+ }
67
+ function getActiveProfile() {
68
+ const config = loadConfig();
69
+ if (!config.activeProfile || !config.profiles[config.activeProfile]) {
70
+ return null;
71
+ }
72
+ return config.profiles[config.activeProfile];
73
+ }
74
+ function setActiveProfile(name) {
75
+ const config = loadConfig();
76
+ if (!config.profiles[name]) {
77
+ throw new Error(`Profile "${name}" does not exist`);
78
+ }
79
+ config.activeProfile = name;
80
+ saveConfig(config);
81
+ }
82
+ function addProfile(profile) {
83
+ const config = loadConfig();
84
+ config.profiles[profile.name] = profile;
85
+ if (!config.activeProfile) {
86
+ config.activeProfile = profile.name;
87
+ }
88
+ saveConfig(config);
89
+ }
90
+ function removeProfile(name) {
91
+ const config = loadConfig();
92
+ if (!config.profiles[name]) {
93
+ throw new Error(`Profile "${name}" does not exist`);
94
+ }
95
+ delete config.profiles[name];
96
+ if (config.activeProfile === name) {
97
+ const remaining = Object.keys(config.profiles);
98
+ config.activeProfile = remaining.length > 0 ? remaining[0] : "";
99
+ }
100
+ saveConfig(config);
101
+ }
102
+ function updateProfile(name, updates) {
103
+ const config = loadConfig();
104
+ if (!config.profiles[name]) {
105
+ throw new Error(`Profile "${name}" does not exist`);
106
+ }
107
+ config.profiles[name] = { ...config.profiles[name], ...updates };
108
+ saveConfig(config);
109
+ }
110
+ function listProfiles() {
111
+ const config = loadConfig();
112
+ return {
113
+ profiles: Object.values(config.profiles),
114
+ active: config.activeProfile
115
+ };
116
+ }
117
+
118
+ // src/utils/output.ts
119
+ var import_cli_table3 = __toESM(require("cli-table3"));
120
+ function formatDatasetResponse(dataset, format = "table", columns) {
121
+ const cols = dataset.data.cols;
122
+ const rows = dataset.data.rows;
123
+ let colIndices;
124
+ if (columns?.length) {
125
+ colIndices = columns.map((name) => {
126
+ const idx = cols.findIndex(
127
+ (c) => c.name === name || c.display_name === name
128
+ );
129
+ if (idx === -1) throw new Error(`Column "${name}" not found`);
130
+ return idx;
131
+ });
132
+ } else {
133
+ colIndices = cols.map((_, i) => i);
134
+ }
135
+ const filteredCols = colIndices.map((i) => cols[i]);
136
+ const filteredRows = rows.map((row) => colIndices.map((i) => row[i]));
137
+ switch (format) {
138
+ case "json":
139
+ return JSON.stringify(
140
+ filteredRows.map(
141
+ (row) => Object.fromEntries(
142
+ filteredCols.map((col, i) => [col.name, row[i]])
143
+ )
144
+ ),
145
+ null,
146
+ 2
147
+ );
148
+ case "csv":
149
+ return formatDelimited(filteredCols, filteredRows, ",");
150
+ case "tsv":
151
+ return formatDelimited(filteredCols, filteredRows, " ");
152
+ case "table":
153
+ default:
154
+ return formatTable(filteredCols, filteredRows);
155
+ }
156
+ }
157
+ function formatDelimited(cols, rows, delimiter) {
158
+ const header = cols.map((c) => escapeCsvField(String(c.name), delimiter)).join(delimiter);
159
+ const body = rows.map(
160
+ (row) => row.map((cell) => escapeCsvField(formatCell(cell), delimiter)).join(delimiter)
161
+ );
162
+ return [header, ...body].join("\n");
163
+ }
164
+ function escapeCsvField(value, delimiter) {
165
+ if (value.includes(delimiter) || value.includes('"') || value.includes("\n")) {
166
+ return `"${value.replace(/"/g, '""')}"`;
167
+ }
168
+ return value;
169
+ }
170
+ function formatTable(cols, rows) {
171
+ const table = new import_cli_table3.default({
172
+ head: cols.map((c) => c.display_name),
173
+ style: { head: ["cyan"] }
174
+ });
175
+ for (const row of rows) {
176
+ table.push(row.map((cell) => formatCell(cell)));
177
+ }
178
+ return table.toString();
179
+ }
180
+ function formatCell(value) {
181
+ if (value === null || value === void 0) return "";
182
+ if (typeof value === "object") return JSON.stringify(value);
183
+ return String(value);
184
+ }
185
+ function formatJson(data) {
186
+ return JSON.stringify(data, null, 2);
187
+ }
188
+ function formatEntityTable(items, columns) {
189
+ const table = new import_cli_table3.default({
190
+ head: columns.map((c) => c.header),
191
+ style: { head: ["cyan"] }
192
+ });
193
+ for (const item of items) {
194
+ table.push(columns.map((c) => formatCell(item[c.key])));
195
+ }
196
+ return table.toString();
197
+ }
198
+
199
+ // src/commands/profile.ts
200
+ function profileCommand() {
201
+ const cmd = new import_commander.Command("profile").description("Manage Metabase profiles").addHelpText("after", `
202
+ The first profile added becomes the default (active) profile.
203
+ All commands use the active profile unless you switch with 'profile switch'.
204
+
205
+ Examples:
206
+ $ metabase-cli profile add prod --domain https://metabase.example.com --email you@co.com --password secret
207
+ $ metabase-cli profile add staging --domain https://staging.metabase.co --api-key mb_xxxxx
208
+ $ metabase-cli profile list
209
+ $ metabase-cli profile switch staging
210
+ $ metabase-cli profile current
211
+ $ metabase-cli profile remove staging`);
212
+ cmd.command("add <name>").description("Add a new profile (becomes default if first)").requiredOption("--domain <url>", "Metabase instance URL").option("--email <email>", "Login email").option("--password <password>", "Login password").option("--api-key <key>", "API key (alternative to email/password)").option("--default-db <id>", "Default database ID for queries", parseInt).addHelpText("after", `
213
+ Examples:
214
+ $ metabase-cli profile add prod --domain https://metabase.example.com --email you@co.com --password secret
215
+ $ metabase-cli profile add prod --domain https://metabase.example.com --email you@co.com --password secret --default-db 1
216
+ $ metabase-cli profile add staging --domain https://staging.example.com --api-key mb_xxxxx`).action((name, opts) => {
217
+ let auth;
218
+ if (opts.apiKey) {
219
+ auth = { method: "api-key", apiKey: opts.apiKey };
220
+ } else if (opts.email && opts.password) {
221
+ auth = { method: "session", email: opts.email, password: opts.password };
222
+ } else {
223
+ console.error("Error: Provide --email and --password, or --api-key");
224
+ process.exit(1);
225
+ }
226
+ const profile = {
227
+ name,
228
+ domain: opts.domain,
229
+ auth,
230
+ defaultDb: opts.defaultDb
231
+ };
232
+ addProfile(profile);
233
+ console.log(`Profile "${name}" added and set as active.`);
234
+ });
235
+ cmd.command("list").description("List all profiles (* = active/default)").addHelpText("after", `
236
+ Examples:
237
+ $ metabase-cli profile list`).action(() => {
238
+ const { profiles, active } = listProfiles();
239
+ if (profiles.length === 0) {
240
+ console.log("No profiles configured. Run: metabase-cli profile add <name>");
241
+ return;
242
+ }
243
+ const items = profiles.map((p) => ({
244
+ active: p.name === active ? "*" : "",
245
+ name: p.name,
246
+ domain: p.domain,
247
+ auth: p.auth.method,
248
+ user: p.user ? `${p.user.first_name} ${p.user.last_name} (${p.user.email})` : "not logged in"
249
+ }));
250
+ console.log(
251
+ formatEntityTable(items, [
252
+ { key: "active", header: "" },
253
+ { key: "name", header: "Name" },
254
+ { key: "domain", header: "Domain" },
255
+ { key: "auth", header: "Auth" },
256
+ { key: "user", header: "User" }
257
+ ])
258
+ );
259
+ });
260
+ cmd.command("switch <name>").description("Switch active (default) profile").addHelpText("after", `
261
+ Examples:
262
+ $ metabase-cli profile switch staging`).action((name) => {
263
+ setActiveProfile(name);
264
+ console.log(`Switched to profile "${name}".`);
265
+ });
266
+ cmd.command("remove <name>").description("Remove a profile").addHelpText("after", `
267
+ Examples:
268
+ $ metabase-cli profile remove staging`).action((name) => {
269
+ removeProfile(name);
270
+ console.log(`Profile "${name}" removed.`);
271
+ });
272
+ cmd.command("current").description("Show current active (default) profile").addHelpText("after", `
273
+ Examples:
274
+ $ metabase-cli profile current`).action(() => {
275
+ const profile = getActiveProfile();
276
+ if (!profile) {
277
+ console.log("No active profile. Run: metabase-cli profile add <name>");
278
+ return;
279
+ }
280
+ console.log(`Profile: ${profile.name}`);
281
+ console.log(`Domain: ${profile.domain}`);
282
+ console.log(`Auth: ${profile.auth.method}`);
283
+ if (profile.user) {
284
+ console.log(`User: ${profile.user.first_name} ${profile.user.last_name} (${profile.user.email})`);
285
+ }
286
+ if (profile.defaultDb) {
287
+ console.log(`Default DB: ${profile.defaultDb}`);
288
+ }
289
+ });
290
+ cmd.command("set-default-db <id>").description("Set the default database ID for the active profile").addHelpText("after", `
291
+ The default database is used when --db is not specified in query/question commands.
292
+
293
+ Examples:
294
+ $ metabase-cli profile set-default-db 1
295
+ $ metabase-cli profile set-default-db 3`).action((id) => {
296
+ const profile = getActiveProfile();
297
+ if (!profile) {
298
+ console.error("No active profile. Run: metabase-cli profile add <name>");
299
+ process.exit(1);
300
+ }
301
+ updateProfile(profile.name, { defaultDb: parseInt(id) });
302
+ console.log(`Default database set to #${id} for profile "${profile.name}".`);
303
+ });
304
+ return cmd;
305
+ }
306
+
307
+ // src/commands/query.ts
308
+ var import_commander2 = require("commander");
309
+ var import_node_fs = require("fs");
310
+ var import_node_path = require("path");
311
+
312
+ // src/utils/export.ts
313
+ function checkExportError(buf, format) {
314
+ if (format !== "json" && buf.length > 0 && buf[0] === 123) {
315
+ try {
316
+ const parsed = JSON.parse(buf.toString("utf-8"));
317
+ if (parsed.status === "failed" || parsed.error) {
318
+ throw new Error(`Query failed: ${parsed.error || "unknown error"}`);
319
+ }
320
+ } catch (e) {
321
+ if (e.message.startsWith("Query failed:")) throw e;
322
+ }
323
+ }
324
+ }
325
+ var EXT_TO_FORMAT = {
326
+ ".csv": "csv",
327
+ ".tsv": "tsv",
328
+ ".json": "json",
329
+ ".xlsx": "xlsx"
330
+ };
331
+
332
+ // src/api/dataset.ts
333
+ var DatasetApi = class {
334
+ constructor(client) {
335
+ this.client = client;
336
+ }
337
+ async query(datasetQuery) {
338
+ return this.client.post("/api/dataset", datasetQuery);
339
+ }
340
+ async queryNative(database, sql, templateTags) {
341
+ return this.query({
342
+ type: "native",
343
+ database,
344
+ native: {
345
+ query: sql,
346
+ "template-tags": templateTags ?? {}
347
+ }
348
+ });
349
+ }
350
+ async export(datasetQuery, format) {
351
+ const res = await this.client.requestRaw(
352
+ "POST",
353
+ `/api/dataset/${format}`,
354
+ datasetQuery
355
+ );
356
+ if (!res.ok) {
357
+ throw new Error(`Export failed: ${res.status} ${await res.text()}`);
358
+ }
359
+ return res.text();
360
+ }
361
+ async exportBinary(datasetQuery, format) {
362
+ const res = await this.client.requestFormExport(
363
+ `/api/dataset/${format}`,
364
+ { query: JSON.stringify(datasetQuery) }
365
+ );
366
+ if (!res.ok) {
367
+ throw new Error(`Export failed: ${res.status} ${await res.text()}`);
368
+ }
369
+ const buf = Buffer.from(await res.arrayBuffer());
370
+ checkExportError(buf, format);
371
+ return buf;
372
+ }
373
+ };
374
+
375
+ // src/client.ts
376
+ var MetabaseClient = class {
377
+ domain;
378
+ sessionToken;
379
+ apiKey;
380
+ profile;
381
+ constructor(profile) {
382
+ this.profile = profile;
383
+ this.domain = profile.domain.replace(/\/+$/, "");
384
+ if (profile.auth.method === "session") {
385
+ this.sessionToken = profile.auth.sessionToken;
386
+ } else {
387
+ this.apiKey = profile.auth.apiKey;
388
+ }
389
+ }
390
+ getHeaders() {
391
+ const headers = {
392
+ "Content-Type": "application/json"
393
+ };
394
+ if (this.apiKey) {
395
+ headers["X-Api-Key"] = this.apiKey;
396
+ } else if (this.sessionToken) {
397
+ headers["X-Metabase-Session"] = this.sessionToken;
398
+ }
399
+ return headers;
400
+ }
401
+ async request(method, path2, body, params) {
402
+ let url = `${this.domain}${path2}`;
403
+ if (params) {
404
+ const qs = new URLSearchParams(params).toString();
405
+ url += `?${qs}`;
406
+ }
407
+ const res = await fetch(url, {
408
+ method,
409
+ headers: this.getHeaders(),
410
+ body: body ? JSON.stringify(body) : void 0
411
+ });
412
+ if (res.status === 401 && this.profile.auth.method === "session") {
413
+ await this.login();
414
+ const retryRes = await fetch(url, {
415
+ method,
416
+ headers: this.getHeaders(),
417
+ body: body ? JSON.stringify(body) : void 0
418
+ });
419
+ if (!retryRes.ok) {
420
+ const err = await retryRes.text();
421
+ throw new Error(`${retryRes.status} ${retryRes.statusText}: ${err}`);
422
+ }
423
+ const retryText = await retryRes.text();
424
+ if (!retryText) return void 0;
425
+ return JSON.parse(retryText);
426
+ }
427
+ if (!res.ok) {
428
+ const err = await res.text();
429
+ throw new Error(`${res.status} ${res.statusText}: ${err}`);
430
+ }
431
+ const text = await res.text();
432
+ if (!text) return void 0;
433
+ return JSON.parse(text);
434
+ }
435
+ async get(path2, params) {
436
+ return this.request("GET", path2, void 0, params);
437
+ }
438
+ async post(path2, body) {
439
+ return this.request("POST", path2, body);
440
+ }
441
+ async put(path2, body) {
442
+ return this.request("PUT", path2, body);
443
+ }
444
+ async delete(path2) {
445
+ return this.request("DELETE", path2);
446
+ }
447
+ async requestFormExport(path2, fields) {
448
+ const url = `${this.domain}${path2}`;
449
+ const headers = {};
450
+ if (this.apiKey) {
451
+ headers["X-Api-Key"] = this.apiKey;
452
+ } else if (this.sessionToken) {
453
+ headers["X-Metabase-Session"] = this.sessionToken;
454
+ }
455
+ const body = new URLSearchParams(fields);
456
+ const res = await fetch(url, { method: "POST", headers, body });
457
+ if (res.status === 401 && this.profile.auth.method === "session") {
458
+ await this.login();
459
+ if (this.sessionToken) {
460
+ headers["X-Metabase-Session"] = this.sessionToken;
461
+ }
462
+ return fetch(url, { method: "POST", headers, body: new URLSearchParams(fields) });
463
+ }
464
+ return res;
465
+ }
466
+ async requestRaw(method, path2, body) {
467
+ const url = `${this.domain}${path2}`;
468
+ const res = await fetch(url, {
469
+ method,
470
+ headers: this.getHeaders(),
471
+ body: body ? JSON.stringify(body) : void 0
472
+ });
473
+ if (res.status === 401 && this.profile.auth.method === "session") {
474
+ await this.login();
475
+ return fetch(url, {
476
+ method,
477
+ headers: this.getHeaders(),
478
+ body: body ? JSON.stringify(body) : void 0
479
+ });
480
+ }
481
+ return res;
482
+ }
483
+ async login() {
484
+ const auth = this.profile.auth;
485
+ const res = await fetch(`${this.domain}/api/session`, {
486
+ method: "POST",
487
+ headers: { "Content-Type": "application/json" },
488
+ body: JSON.stringify({ username: auth.email, password: auth.password })
489
+ });
490
+ if (!res.ok) {
491
+ const err = await res.text();
492
+ throw new Error(`Login failed: ${res.status} ${err}`);
493
+ }
494
+ const session = await res.json();
495
+ this.sessionToken = session.id;
496
+ updateProfile(this.profile.name, {
497
+ auth: { ...auth, sessionToken: session.id }
498
+ });
499
+ const user = await this.get("/api/user/current");
500
+ const cachedUser = {
501
+ id: user.id,
502
+ email: user.email,
503
+ first_name: user.first_name,
504
+ last_name: user.last_name,
505
+ is_superuser: user.is_superuser
506
+ };
507
+ updateProfile(this.profile.name, { user: cachedUser });
508
+ this.profile.user = cachedUser;
509
+ return session;
510
+ }
511
+ async logout() {
512
+ await this.delete("/api/session");
513
+ this.sessionToken = void 0;
514
+ if (this.profile.auth.method === "session") {
515
+ updateProfile(this.profile.name, {
516
+ auth: { ...this.profile.auth, sessionToken: void 0 }
517
+ });
518
+ }
519
+ }
520
+ async ensureAuthenticated() {
521
+ if (this.apiKey) return;
522
+ if (!this.sessionToken) {
523
+ await this.login();
524
+ }
525
+ }
526
+ getProfile() {
527
+ return this.profile;
528
+ }
529
+ getUserId() {
530
+ return this.profile.user?.id ?? null;
531
+ }
532
+ };
533
+
534
+ // src/commands/helpers.ts
535
+ async function resolveClient() {
536
+ const profile = getActiveProfile();
537
+ if (!profile) {
538
+ console.error("No active profile. Run: metabase-cli profile add <name>");
539
+ process.exit(1);
540
+ }
541
+ const client = new MetabaseClient(profile);
542
+ await client.ensureAuthenticated();
543
+ return client;
544
+ }
545
+ function resolveDb(optDb) {
546
+ if (optDb !== void 0) return optDb;
547
+ const profile = getActiveProfile();
548
+ if (profile?.defaultDb) return profile.defaultDb;
549
+ console.error("Error: --db is required (or set a default with: metabase-cli profile set-default-db <id>)");
550
+ process.exit(1);
551
+ }
552
+ function isUnsafe(cmd, localFlag) {
553
+ if (localFlag) return true;
554
+ if (process.env.METABASE_UNSAFE === "1") return true;
555
+ let current = cmd;
556
+ while (current) {
557
+ if (current.opts().unsafe) return true;
558
+ current = current.parent;
559
+ }
560
+ return false;
561
+ }
562
+
563
+ // src/commands/query.ts
564
+ function queryCommand() {
565
+ const cmd = new import_commander2.Command("query").description("Run queries against a database").addHelpText("after", `
566
+ Examples:
567
+ $ metabase-cli query run --sql "SELECT * FROM users LIMIT 10" --db 1
568
+ $ metabase-cli query run --sql "SELECT count(*) FROM orders" --db 1 --format json
569
+ $ metabase-cli query run --sql "SELECT * FROM products" --db 1 --format csv
570
+ $ metabase-cli query run --sql "SELECT * FROM users" --db 1 --columns "id,email" --limit 5`);
571
+ cmd.command("run").description("Execute a SQL query").requiredOption("--sql <sql>", "SQL query to execute").option("--db <id>", "Database ID (uses profile default if not set)", parseInt).option("--format <format>", "Output format: table, json, csv, tsv, xlsx", "table").option("--output <file>", "Write output to a file (format auto-detected from extension)").option("--columns <cols>", "Comma-separated column names to display").option("--limit <n>", "Limit number of rows", parseInt).addHelpText("after", `
572
+ Examples:
573
+ $ metabase-cli query run --sql "SELECT * FROM users LIMIT 10" --db 1
574
+ $ metabase-cli query run --sql "SELECT count(*) FROM orders" --db 1 --format json
575
+ $ metabase-cli query run --sql "SELECT * FROM products" --db 2 --format csv > products.csv
576
+ $ metabase-cli query run --sql "SELECT * FROM products" --db 2 --output products.xlsx
577
+ $ metabase-cli query run --sql "SELECT * FROM products" --db 2 --output results.csv`).action(async (opts) => {
578
+ const client = await resolveClient();
579
+ const api = new DatasetApi(client);
580
+ let sql = opts.sql;
581
+ if (opts.limit) {
582
+ sql = `SELECT * FROM (${sql}) _q LIMIT ${opts.limit}`;
583
+ }
584
+ const db = resolveDb(opts.db);
585
+ let format = opts.format;
586
+ const outputPath = opts.output ? (0, import_node_path.resolve)(opts.output) : null;
587
+ if (outputPath) {
588
+ const ext = (0, import_node_path.extname)(outputPath).toLowerCase();
589
+ const inferredFormat = EXT_TO_FORMAT[ext];
590
+ if (inferredFormat && format === "table") {
591
+ format = inferredFormat;
592
+ }
593
+ }
594
+ if (outputPath) {
595
+ if (format === "xlsx" || format === "csv" || format === "json") {
596
+ if (opts.columns) {
597
+ console.warn("Warning: --columns is not supported with native export (csv/json/xlsx). All columns will be exported.");
598
+ }
599
+ const datasetQuery = {
600
+ type: "native",
601
+ database: db,
602
+ native: { query: sql, "template-tags": {} }
603
+ };
604
+ const data = await api.exportBinary(datasetQuery, format);
605
+ (0, import_node_fs.writeFileSync)(outputPath, data);
606
+ console.log(`Exported to ${outputPath}`);
607
+ } else {
608
+ const result2 = await api.queryNative(db, sql);
609
+ if (result2.status === "failed") {
610
+ console.error("Query failed:", result2.error);
611
+ process.exit(1);
612
+ }
613
+ const columns2 = opts.columns?.split(",");
614
+ const output = formatDatasetResponse(result2, format, columns2);
615
+ (0, import_node_fs.writeFileSync)(outputPath, output, "utf-8");
616
+ console.log(`Exported ${result2.row_count} row(s) to ${outputPath}`);
617
+ }
618
+ return;
619
+ }
620
+ if (format === "xlsx") {
621
+ console.error("Error: xlsx format requires --output <file>");
622
+ process.exit(1);
623
+ }
624
+ const result = await api.queryNative(db, sql);
625
+ if (result.status === "failed") {
626
+ console.error("Query failed:", result.error);
627
+ process.exit(1);
628
+ }
629
+ const columns = opts.columns?.split(",");
630
+ console.log(
631
+ formatDatasetResponse(result, format, columns)
632
+ );
633
+ console.log(`
634
+ ${result.row_count} row(s) returned.`);
635
+ });
636
+ return cmd;
637
+ }
638
+
639
+ // src/commands/question.ts
640
+ var import_commander3 = require("commander");
641
+ var import_node_fs2 = require("fs");
642
+ var import_node_path2 = require("path");
643
+
644
+ // src/api/card.ts
645
+ var CardApi = class {
646
+ constructor(client) {
647
+ this.client = client;
648
+ }
649
+ async list(params) {
650
+ return this.client.get("/api/card", params);
651
+ }
652
+ async get(id) {
653
+ return this.client.get(`/api/card/${id}`);
654
+ }
655
+ async create(params) {
656
+ return this.client.post("/api/card", params);
657
+ }
658
+ async update(id, params) {
659
+ return this.client.put(`/api/card/${id}`, params);
660
+ }
661
+ async delete(id) {
662
+ await this.client.delete(`/api/card/${id}`);
663
+ }
664
+ async copy(id, overrides) {
665
+ return this.client.post(`/api/card/${id}/copy`, overrides);
666
+ }
667
+ async query(id, parameters) {
668
+ return this.client.post(`/api/card/${id}/query`, {
669
+ parameters: parameters ?? []
670
+ });
671
+ }
672
+ async queryExport(id, format, parameters) {
673
+ const res = await this.client.requestRaw(
674
+ "POST",
675
+ `/api/card/${id}/query/${format}`,
676
+ { parameters: parameters ?? [] }
677
+ );
678
+ if (!res.ok) {
679
+ throw new Error(`Export failed: ${res.status} ${await res.text()}`);
680
+ }
681
+ return res.text();
682
+ }
683
+ async queryExportBinary(id, format, parameters) {
684
+ const fields = {};
685
+ if (parameters && parameters.length > 0) {
686
+ fields.parameters = JSON.stringify(parameters);
687
+ }
688
+ const res = await this.client.requestFormExport(
689
+ `/api/card/${id}/query/${format}`,
690
+ fields
691
+ );
692
+ if (!res.ok) {
693
+ throw new Error(`Export failed: ${res.status} ${await res.text()}`);
694
+ }
695
+ const buf = Buffer.from(await res.arrayBuffer());
696
+ checkExportError(buf, format);
697
+ return buf;
698
+ }
699
+ };
700
+
701
+ // src/safety/guard.ts
702
+ var SafetyGuard = class {
703
+ client;
704
+ unsafe;
705
+ constructor(client, unsafe = false) {
706
+ this.client = client;
707
+ this.unsafe = unsafe;
708
+ }
709
+ async checkOwnership(entityType, entityId) {
710
+ const pathMap = {
711
+ card: `/api/card/${entityId}`,
712
+ dashboard: `/api/dashboard/${entityId}`,
713
+ snippet: `/api/native-query-snippet/${entityId}`,
714
+ collection: `/api/collection/${entityId}`
715
+ };
716
+ const path2 = pathMap[entityType];
717
+ if (!path2) {
718
+ throw new Error(`Unknown entity type: ${entityType}`);
719
+ }
720
+ const entity = await this.client.get(path2);
721
+ const userId = this.client.getUserId();
722
+ if (userId === null) {
723
+ throw new Error("No cached user ID. Run 'metabase-cli login' or 'metabase-cli whoami --refresh' first.");
724
+ }
725
+ return {
726
+ owned: entity.creator_id === userId,
727
+ creatorId: entity.creator_id
728
+ };
729
+ }
730
+ async guard(entityType, entityId, action, fn) {
731
+ if (this.unsafe) {
732
+ return fn();
733
+ }
734
+ const { owned, creatorId } = await this.checkOwnership(entityType, entityId);
735
+ if (!owned) {
736
+ const userId = this.client.getUserId();
737
+ throw new Error(
738
+ `Safe mode: Cannot ${action} ${entityType} #${entityId} \u2014 owned by user #${creatorId}, you are user #${userId}. Use --unsafe to bypass.`
739
+ );
740
+ }
741
+ return fn();
742
+ }
743
+ };
744
+
745
+ // src/commands/question.ts
746
+ var TAG_TYPE_TO_PARAM_TYPE = {
747
+ date: "date/single",
748
+ text: "string/=",
749
+ number: "number/="
750
+ };
751
+ function buildParametersFromTags(tags) {
752
+ if (Object.keys(tags).length === 0) return [];
753
+ return Object.entries(tags).filter(([, tag]) => {
754
+ return tag.type !== "snippet" && tag.type !== "card";
755
+ }).map(([name, tag]) => {
756
+ const isDimension = tag.type === "dimension";
757
+ const paramType = isDimension ? tag["widget-type"] || "string/=" : TAG_TYPE_TO_PARAM_TYPE[tag.type] || "string/=";
758
+ const param = {
759
+ id: tag.id || crypto.randomUUID(),
760
+ type: paramType,
761
+ target: isDimension ? ["dimension", ["template-tag", name]] : ["variable", ["template-tag", name]],
762
+ name: tag["display-name"] || name,
763
+ slug: name
764
+ };
765
+ if (tag.default !== void 0) {
766
+ param.default = tag.default;
767
+ }
768
+ return param;
769
+ });
770
+ }
771
+ function questionCommand() {
772
+ const cmd = new import_commander3.Command("question").description("Manage questions (saved cards)").addHelpText("after", `
773
+ Examples:
774
+ $ metabase-cli question list --filter mine
775
+ $ metabase-cli question show 42
776
+ $ metabase-cli question run 42 --format csv
777
+ $ metabase-cli question create --name "Active Users" --sql "SELECT * FROM users WHERE active" --db 1
778
+ $ metabase-cli question update 42 --name "New Name" --unsafe
779
+ $ metabase-cli question delete 42
780
+ $ metabase-cli question copy 42 --name "Copy" --collection 5`);
781
+ cmd.command("list").description("List questions").option("--filter <f>", "Filter: all, mine, bookmarked, archived").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
782
+ Examples:
783
+ $ metabase-cli question list
784
+ $ metabase-cli question list --filter mine
785
+ $ metabase-cli question list --format json`).action(async (opts) => {
786
+ const client = await resolveClient();
787
+ const api = new CardApi(client);
788
+ const params = {};
789
+ if (opts.filter) params.f = opts.filter;
790
+ const cards = await api.list(params);
791
+ if (opts.format === "json") {
792
+ console.log(formatJson(cards));
793
+ return;
794
+ }
795
+ console.log(
796
+ formatEntityTable(cards, [
797
+ { key: "id", header: "ID" },
798
+ { key: "name", header: "Name" },
799
+ { key: "display", header: "Display" },
800
+ { key: "collection_id", header: "Collection" },
801
+ { key: "creator_id", header: "Creator" }
802
+ ])
803
+ );
804
+ });
805
+ cmd.command("show <id>").description("Show question details").addHelpText("after", `
806
+ Examples:
807
+ $ metabase-cli question show 42`).action(async (id) => {
808
+ const client = await resolveClient();
809
+ const api = new CardApi(client);
810
+ const card = await api.get(parseInt(id));
811
+ console.log(formatJson(card));
812
+ });
813
+ cmd.command("run <id>").description("Execute a saved question").option("--format <format>", "Output format: table, json, csv, tsv, xlsx", "table").option("--output <file>", "Write output to a file (format auto-detected from extension)").option("--columns <cols>", "Comma-separated column names").option("--params <json>", `Parameter values as JSON, e.g. '{"start_date":"2025-01-01"}'`).addHelpText("after", `
814
+ Examples:
815
+ $ metabase-cli question run 42
816
+ $ metabase-cli question run 42 --format csv
817
+ $ metabase-cli question run 42 --columns "id,name,email"
818
+ $ metabase-cli question run 42 --params '{"start_date":"2025-01-01"}'
819
+ $ metabase-cli question run 42 --output results.xlsx
820
+ $ metabase-cli question run 42 --output results.csv`).action(async (id, opts) => {
821
+ const client = await resolveClient();
822
+ const api = new CardApi(client);
823
+ const cardId = parseInt(id);
824
+ let format = opts.format;
825
+ const outputPath = opts.output ? (0, import_node_path2.resolve)(opts.output) : null;
826
+ if (outputPath) {
827
+ const ext = (0, import_node_path2.extname)(outputPath).toLowerCase();
828
+ const inferredFormat = EXT_TO_FORMAT[ext];
829
+ if (inferredFormat && format === "table") {
830
+ format = inferredFormat;
831
+ }
832
+ }
833
+ const card = await api.get(cardId);
834
+ const cardParams = card.parameters || [];
835
+ let paramsInput = {};
836
+ if (opts.params) {
837
+ try {
838
+ paramsInput = JSON.parse(opts.params);
839
+ } catch {
840
+ console.error("Error: --params must be valid JSON");
841
+ process.exit(1);
842
+ }
843
+ }
844
+ const parameterValues = cardParams.map((p) => {
845
+ const value = paramsInput[p.slug] ?? p.default;
846
+ if (value === void 0) return null;
847
+ return {
848
+ id: p.id,
849
+ type: p.type,
850
+ target: p.target,
851
+ value
852
+ };
853
+ }).filter(Boolean);
854
+ if (outputPath) {
855
+ if (format === "xlsx" || format === "csv" || format === "json") {
856
+ if (opts.columns) {
857
+ console.warn("Warning: --columns is not supported with native export (csv/json/xlsx). All columns will be exported.");
858
+ }
859
+ const data = await api.queryExportBinary(cardId, format, parameterValues);
860
+ (0, import_node_fs2.writeFileSync)(outputPath, data);
861
+ console.log(`Exported to ${outputPath}`);
862
+ } else {
863
+ const result2 = await api.query(cardId, parameterValues);
864
+ if (result2.status === "failed") {
865
+ console.error("Query failed:", result2.error);
866
+ process.exit(1);
867
+ }
868
+ const columns2 = opts.columns?.split(",");
869
+ const output = formatDatasetResponse(result2, format, columns2);
870
+ (0, import_node_fs2.writeFileSync)(outputPath, output, "utf-8");
871
+ console.log(`Exported ${result2.row_count} row(s) to ${outputPath}`);
872
+ }
873
+ return;
874
+ }
875
+ if (format === "xlsx") {
876
+ console.error("Error: xlsx format requires --output <file>");
877
+ process.exit(1);
878
+ }
879
+ const result = await api.query(cardId, parameterValues);
880
+ if (result.status === "failed") {
881
+ console.error("Query failed:", result.error);
882
+ process.exit(1);
883
+ }
884
+ const columns = opts.columns?.split(",");
885
+ console.log(
886
+ formatDatasetResponse(result, format, columns)
887
+ );
888
+ console.log(`
889
+ ${result.row_count} row(s) returned.`);
890
+ });
891
+ cmd.command("create").description("Create a new question").requiredOption("--name <name>", "Question name").requiredOption("--sql <sql>", "SQL query").option("--db <id>", "Database ID (uses profile default if not set)", parseInt).option("--description <desc>", "Description").option("--collection <id>", "Collection ID", parseInt).option("--display <type>", "Display type (table, line, bar, pie, scalar, etc.)", "table").option("--viz <json>", "Visualization settings as JSON string").option("--template-tags <json>", "Template tags as JSON string for parameterized queries").addHelpText("after", `
892
+ Display types: table, line, bar, area, pie, scalar, row, scatter, funnel, map, pivot, progress, gauge, waterfall
893
+
894
+ Examples:
895
+ $ metabase-cli question create --name "Active Users" --sql "SELECT * FROM users WHERE active = true" --db 1
896
+ $ metabase-cli question create --name "Revenue" --sql "SELECT sum(amount) FROM orders" --db 1 --collection 5
897
+ $ metabase-cli question create --name "Revenue Trend" --sql "SELECT date, sum(amount) FROM orders GROUP BY date" --display line
898
+ $ metabase-cli question create --name "Revenue Trend" --sql "..." --display line --viz '{"graph.show_values":true}'
899
+ $ metabase-cli question create --name "Users Since" --sql "SELECT * FROM users WHERE created_at >= {{start_date}}" --template-tags '{"start_date":{"type":"date","name":"start_date","display-name":"Start Date","default":"2024-01-01"}}'`).action(async (opts) => {
900
+ const client = await resolveClient();
901
+ const api = new CardApi(client);
902
+ const db = resolveDb(opts.db);
903
+ let vizSettings = {};
904
+ if (opts.viz) {
905
+ try {
906
+ vizSettings = JSON.parse(opts.viz);
907
+ } catch {
908
+ console.error("Error: --viz must be valid JSON");
909
+ process.exit(1);
910
+ }
911
+ }
912
+ let templateTags = {};
913
+ if (opts.templateTags) {
914
+ try {
915
+ templateTags = JSON.parse(opts.templateTags);
916
+ } catch {
917
+ console.error("Error: --template-tags must be valid JSON");
918
+ process.exit(1);
919
+ }
920
+ }
921
+ for (const tag of Object.values(templateTags)) {
922
+ if (!tag.id) {
923
+ tag.id = crypto.randomUUID();
924
+ }
925
+ }
926
+ const parameters = buildParametersFromTags(templateTags);
927
+ const card = await api.create({
928
+ name: opts.name,
929
+ display: opts.display,
930
+ description: opts.description,
931
+ collection_id: opts.collection,
932
+ visualization_settings: vizSettings,
933
+ parameters,
934
+ dataset_query: {
935
+ type: "native",
936
+ database: db,
937
+ native: {
938
+ query: opts.sql,
939
+ "template-tags": Object.keys(templateTags).length > 0 ? templateTags : void 0
940
+ }
941
+ }
942
+ });
943
+ console.log(`Question #${card.id} "${card.name}" created.`);
944
+ });
945
+ cmd.command("update <id>").description("Update a question (safe mode by default)").option("--name <name>", "New name").option("--description <desc>", "New description").option("--collection <id>", "Move to collection", parseInt).option("--sql <sql>", "New SQL query").option("--display <type>", "Change display type (table, line, bar, pie, scalar, etc.)").option("--viz <json>", "Visualization settings as JSON (merged with existing)").option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
946
+ Safe mode blocks updates to questions you didn't create. Use --unsafe to bypass.
947
+
948
+ Examples:
949
+ $ metabase-cli question update 42 --name "New Name"
950
+ $ metabase-cli question update 42 --sql "SELECT * FROM users WHERE active" --unsafe
951
+ $ metabase-cli question update 42 --display line
952
+ $ metabase-cli question update 42 --viz '{"graph.show_values":true,"graph.dimensions":["date"]}'`).action(async function(id, opts) {
953
+ const client = await resolveClient();
954
+ const api = new CardApi(client);
955
+ const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
956
+ const cardId = parseInt(id);
957
+ await guard.guard("card", cardId, "update", async () => {
958
+ const updates = {};
959
+ if (opts.name) updates.name = opts.name;
960
+ if (opts.description) updates.description = opts.description;
961
+ if (opts.collection !== void 0) updates.collection_id = opts.collection;
962
+ if (opts.display) updates.display = opts.display;
963
+ if (opts.viz) {
964
+ let vizSettings;
965
+ try {
966
+ vizSettings = JSON.parse(opts.viz);
967
+ } catch {
968
+ console.error("Error: --viz must be valid JSON");
969
+ process.exit(1);
970
+ }
971
+ const existing = await api.get(cardId);
972
+ updates.visualization_settings = {
973
+ ...existing.visualization_settings,
974
+ ...vizSettings
975
+ };
976
+ }
977
+ if (opts.sql) {
978
+ const existing = await api.get(cardId);
979
+ updates.dataset_query = {
980
+ ...existing.dataset_query,
981
+ native: {
982
+ ...existing.dataset_query.native,
983
+ query: opts.sql
984
+ }
985
+ };
986
+ }
987
+ const card = await api.update(cardId, updates);
988
+ console.log(`Question #${card.id} "${card.name}" updated.`);
989
+ });
990
+ });
991
+ cmd.command("delete <id>").description("Delete a question (safe mode by default)").option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
992
+ Examples:
993
+ $ metabase-cli question delete 42
994
+ $ metabase-cli question delete 42 --unsafe`).action(async function(id, opts) {
995
+ const client = await resolveClient();
996
+ const api = new CardApi(client);
997
+ const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
998
+ const cardId = parseInt(id);
999
+ await guard.guard("card", cardId, "delete", async () => {
1000
+ await api.delete(cardId);
1001
+ console.log(`Question #${cardId} deleted.`);
1002
+ });
1003
+ });
1004
+ cmd.command("copy <id>").description("Copy a question").option("--name <name>", "Name for the copy").option("--collection <id>", "Target collection", parseInt).addHelpText("after", `
1005
+ Examples:
1006
+ $ metabase-cli question copy 42
1007
+ $ metabase-cli question copy 42 --name "Copy of Revenue" --collection 10`).action(async (id, opts) => {
1008
+ const client = await resolveClient();
1009
+ const api = new CardApi(client);
1010
+ const overrides = {};
1011
+ if (opts.name) overrides.name = opts.name;
1012
+ if (opts.collection !== void 0) overrides.collection_id = opts.collection;
1013
+ const card = await api.copy(parseInt(id), overrides);
1014
+ console.log(`Question #${card.id} "${card.name}" created (copy).`);
1015
+ });
1016
+ return cmd;
1017
+ }
1018
+
1019
+ // src/commands/dashboard.ts
1020
+ var import_commander4 = require("commander");
1021
+
1022
+ // src/api/dashboard.ts
1023
+ var DashboardApi = class {
1024
+ constructor(client) {
1025
+ this.client = client;
1026
+ }
1027
+ async list(params) {
1028
+ return this.client.get("/api/dashboard", params);
1029
+ }
1030
+ async get(id) {
1031
+ return this.client.get(`/api/dashboard/${id}`);
1032
+ }
1033
+ async create(params) {
1034
+ return this.client.post("/api/dashboard", params);
1035
+ }
1036
+ async update(id, params) {
1037
+ return this.client.put(`/api/dashboard/${id}`, params);
1038
+ }
1039
+ async delete(id) {
1040
+ await this.client.delete(`/api/dashboard/${id}`);
1041
+ }
1042
+ async copy(id, overrides) {
1043
+ return this.client.post(`/api/dashboard/${id}/copy`, overrides);
1044
+ }
1045
+ };
1046
+
1047
+ // src/commands/dashboard.ts
1048
+ function dashboardCommand() {
1049
+ const cmd = new import_commander4.Command("dashboard").description("Manage dashboards").addHelpText("after", `
1050
+ Examples:
1051
+ $ metabase-cli dashboard list
1052
+ $ metabase-cli dashboard show 7
1053
+ $ metabase-cli dashboard create --name "Sales Overview" --collection 5
1054
+ $ metabase-cli dashboard update 7 --name "Updated" --unsafe
1055
+ $ metabase-cli dashboard delete 7
1056
+ $ metabase-cli dashboard copy 7 --name "Sales Overview (copy)"`);
1057
+ cmd.command("list").description("List dashboards").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
1058
+ Examples:
1059
+ $ metabase-cli dashboard list
1060
+ $ metabase-cli dashboard list --format json`).action(async (opts) => {
1061
+ const client = await resolveClient();
1062
+ const api = new DashboardApi(client);
1063
+ const dashboards = await api.list();
1064
+ if (opts.format === "json") {
1065
+ console.log(formatJson(dashboards));
1066
+ return;
1067
+ }
1068
+ console.log(
1069
+ formatEntityTable(dashboards, [
1070
+ { key: "id", header: "ID" },
1071
+ { key: "name", header: "Name" },
1072
+ { key: "collection_id", header: "Collection" },
1073
+ { key: "creator_id", header: "Creator" }
1074
+ ])
1075
+ );
1076
+ });
1077
+ cmd.command("show <id>").description("Show dashboard details").addHelpText("after", `
1078
+ Examples:
1079
+ $ metabase-cli dashboard show 7`).action(async (id) => {
1080
+ const client = await resolveClient();
1081
+ const api = new DashboardApi(client);
1082
+ const dashboard = await api.get(parseInt(id));
1083
+ console.log(formatJson(dashboard));
1084
+ });
1085
+ cmd.command("create").description("Create a new dashboard").requiredOption("--name <name>", "Dashboard name").option("--description <desc>", "Description").option("--collection <id>", "Collection ID", parseInt).addHelpText("after", `
1086
+ Examples:
1087
+ $ metabase-cli dashboard create --name "Sales Overview"
1088
+ $ metabase-cli dashboard create --name "Q1 Report" --description "Quarterly report" --collection 5`).action(async (opts) => {
1089
+ const client = await resolveClient();
1090
+ const api = new DashboardApi(client);
1091
+ const dashboard = await api.create({
1092
+ name: opts.name,
1093
+ description: opts.description,
1094
+ collection_id: opts.collection
1095
+ });
1096
+ console.log(`Dashboard #${dashboard.id} "${dashboard.name}" created.`);
1097
+ });
1098
+ cmd.command("update <id>").description("Update a dashboard (safe mode by default)").option("--name <name>", "New name").option("--description <desc>", "New description").option("--collection <id>", "Move to collection", parseInt).option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
1099
+ Safe mode blocks updates to dashboards you didn't create. Use --unsafe to bypass.
1100
+
1101
+ Examples:
1102
+ $ metabase-cli dashboard update 7 --name "Updated Name"
1103
+ $ metabase-cli dashboard update 7 --collection 10 --unsafe`).action(async function(id, opts) {
1104
+ const client = await resolveClient();
1105
+ const api = new DashboardApi(client);
1106
+ const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
1107
+ const dashId = parseInt(id);
1108
+ await guard.guard("dashboard", dashId, "update", async () => {
1109
+ const updates = {};
1110
+ if (opts.name) updates.name = opts.name;
1111
+ if (opts.description) updates.description = opts.description;
1112
+ if (opts.collection !== void 0) updates.collection_id = opts.collection;
1113
+ const dashboard = await api.update(dashId, updates);
1114
+ console.log(`Dashboard #${dashboard.id} "${dashboard.name}" updated.`);
1115
+ });
1116
+ });
1117
+ cmd.command("delete <id>").description("Delete a dashboard (safe mode by default)").option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
1118
+ Examples:
1119
+ $ metabase-cli dashboard delete 7
1120
+ $ metabase-cli dashboard delete 7 --unsafe`).action(async function(id, opts) {
1121
+ const client = await resolveClient();
1122
+ const api = new DashboardApi(client);
1123
+ const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
1124
+ const dashId = parseInt(id);
1125
+ await guard.guard("dashboard", dashId, "delete", async () => {
1126
+ await api.delete(dashId);
1127
+ console.log(`Dashboard #${dashId} deleted.`);
1128
+ });
1129
+ });
1130
+ cmd.command("copy <id>").description("Copy a dashboard").option("--name <name>", "Name for the copy").option("--collection <id>", "Target collection", parseInt).addHelpText("after", `
1131
+ Examples:
1132
+ $ metabase-cli dashboard copy 7
1133
+ $ metabase-cli dashboard copy 7 --name "Sales Overview (copy)" --collection 10`).action(async (id, opts) => {
1134
+ const client = await resolveClient();
1135
+ const api = new DashboardApi(client);
1136
+ const overrides = {};
1137
+ if (opts.name) overrides.name = opts.name;
1138
+ if (opts.collection !== void 0) overrides.collection_id = opts.collection;
1139
+ const dashboard = await api.copy(parseInt(id), overrides);
1140
+ console.log(`Dashboard #${dashboard.id} "${dashboard.name}" created (copy).`);
1141
+ });
1142
+ return cmd;
1143
+ }
1144
+
1145
+ // src/commands/collection.ts
1146
+ var import_commander5 = require("commander");
1147
+
1148
+ // src/api/collection.ts
1149
+ var CollectionApi = class {
1150
+ constructor(client) {
1151
+ this.client = client;
1152
+ }
1153
+ async list() {
1154
+ return this.client.get("/api/collection");
1155
+ }
1156
+ async tree() {
1157
+ return this.client.get("/api/collection/tree");
1158
+ }
1159
+ async get(id) {
1160
+ return this.client.get(`/api/collection/${id}`);
1161
+ }
1162
+ async items(id, params) {
1163
+ return this.client.get(`/api/collection/${id}/items`, params);
1164
+ }
1165
+ async create(params) {
1166
+ return this.client.post("/api/collection", params);
1167
+ }
1168
+ async update(id, params) {
1169
+ return this.client.put(`/api/collection/${id}`, params);
1170
+ }
1171
+ };
1172
+
1173
+ // src/commands/collection.ts
1174
+ function collectionCommand() {
1175
+ const cmd = new import_commander5.Command("collection").description("Manage collections").addHelpText("after", `
1176
+ Examples:
1177
+ $ metabase-cli collection list
1178
+ $ metabase-cli collection tree
1179
+ $ metabase-cli collection items 5 --models card
1180
+ $ metabase-cli collection create --name "Analytics" --parent 3`);
1181
+ cmd.command("list").description("List all collections").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
1182
+ Examples:
1183
+ $ metabase-cli collection list
1184
+ $ metabase-cli collection list --format json`).action(async (opts) => {
1185
+ const client = await resolveClient();
1186
+ const api = new CollectionApi(client);
1187
+ const collections = await api.list();
1188
+ if (opts.format === "json") {
1189
+ console.log(formatJson(collections));
1190
+ return;
1191
+ }
1192
+ console.log(
1193
+ formatEntityTable(collections, [
1194
+ { key: "id", header: "ID" },
1195
+ { key: "name", header: "Name" },
1196
+ { key: "parent_id", header: "Parent" }
1197
+ ])
1198
+ );
1199
+ });
1200
+ cmd.command("tree").description("Show collection hierarchy").addHelpText("after", `
1201
+ Examples:
1202
+ $ metabase-cli collection tree`).action(async () => {
1203
+ const client = await resolveClient();
1204
+ const api = new CollectionApi(client);
1205
+ const tree = await api.tree();
1206
+ console.log(formatJson(tree));
1207
+ });
1208
+ cmd.command("show <id>").description("Show collection details (use 'root' for root collection)").addHelpText("after", `
1209
+ Examples:
1210
+ $ metabase-cli collection show 5
1211
+ $ metabase-cli collection show root`).action(async (id) => {
1212
+ const client = await resolveClient();
1213
+ const api = new CollectionApi(client);
1214
+ const coll = await api.get(id === "root" ? "root" : parseInt(id));
1215
+ console.log(formatJson(coll));
1216
+ });
1217
+ cmd.command("items <id>").description("List items in a collection (use 'root' for root)").option("--models <models>", "Filter by type: card, dashboard, collection").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
1218
+ Examples:
1219
+ $ metabase-cli collection items 5
1220
+ $ metabase-cli collection items root --models card,dashboard
1221
+ $ metabase-cli collection items 5 --models card --format json`).action(async (id, opts) => {
1222
+ const client = await resolveClient();
1223
+ const api = new CollectionApi(client);
1224
+ const params = {};
1225
+ if (opts.models) params.models = opts.models;
1226
+ const collId = id === "root" ? "root" : parseInt(id);
1227
+ const result = await api.items(collId, params);
1228
+ if (opts.format === "json") {
1229
+ console.log(formatJson(result));
1230
+ return;
1231
+ }
1232
+ console.log(
1233
+ formatEntityTable(result.data, [
1234
+ { key: "id", header: "ID" },
1235
+ { key: "name", header: "Name" },
1236
+ { key: "model", header: "Type" },
1237
+ { key: "description", header: "Description" }
1238
+ ])
1239
+ );
1240
+ console.log(`
1241
+ ${result.total} item(s).`);
1242
+ });
1243
+ cmd.command("create").description("Create a new collection").requiredOption("--name <name>", "Collection name").option("--description <desc>", "Description").option("--parent <id>", "Parent collection ID", parseInt).addHelpText("after", `
1244
+ Examples:
1245
+ $ metabase-cli collection create --name "Analytics"
1246
+ $ metabase-cli collection create --name "Q1 Reports" --parent 3 --description "First quarter"`).action(async (opts) => {
1247
+ const client = await resolveClient();
1248
+ const api = new CollectionApi(client);
1249
+ const coll = await api.create({
1250
+ name: opts.name,
1251
+ description: opts.description,
1252
+ parent_id: opts.parent
1253
+ });
1254
+ console.log(`Collection #${coll.id} "${coll.name}" created.`);
1255
+ });
1256
+ cmd.command("update <id>").description("Update a collection (safe mode by default)").option("--name <name>", "New name").option("--description <desc>", "New description").option("--parent <id>", "Move to parent collection", parseInt).option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
1257
+ Safe mode blocks updates to collections you didn't create. Use --unsafe to bypass.
1258
+
1259
+ Examples:
1260
+ $ metabase-cli collection update 5 --name "New Name"
1261
+ $ metabase-cli collection update 5 --parent 3 --unsafe`).action(async function(id, opts) {
1262
+ const client = await resolveClient();
1263
+ const api = new CollectionApi(client);
1264
+ const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
1265
+ const collId = parseInt(id);
1266
+ await guard.guard("collection", collId, "update", async () => {
1267
+ const updates = {};
1268
+ if (opts.name) updates.name = opts.name;
1269
+ if (opts.description) updates.description = opts.description;
1270
+ if (opts.parent !== void 0) updates.parent_id = opts.parent;
1271
+ const coll = await api.update(collId, updates);
1272
+ console.log(`Collection #${coll.id} "${coll.name}" updated.`);
1273
+ });
1274
+ });
1275
+ return cmd;
1276
+ }
1277
+
1278
+ // src/commands/database.ts
1279
+ var import_commander6 = require("commander");
1280
+
1281
+ // src/api/database.ts
1282
+ var DatabaseApi = class {
1283
+ constructor(client) {
1284
+ this.client = client;
1285
+ }
1286
+ async list() {
1287
+ return this.client.get("/api/database");
1288
+ }
1289
+ async get(id) {
1290
+ return this.client.get(`/api/database/${id}`);
1291
+ }
1292
+ async metadata(id) {
1293
+ return this.client.get(`/api/database/${id}/metadata`);
1294
+ }
1295
+ async schemas(id) {
1296
+ return this.client.get(`/api/database/${id}/schemas`);
1297
+ }
1298
+ async tablesInSchema(id, schema) {
1299
+ return this.client.get(
1300
+ `/api/database/${id}/schema/${encodeURIComponent(schema)}`
1301
+ );
1302
+ }
1303
+ };
1304
+
1305
+ // src/api/table.ts
1306
+ var TableApi = class {
1307
+ constructor(client) {
1308
+ this.client = client;
1309
+ }
1310
+ async get(id) {
1311
+ return this.client.get(`/api/table/${id}`);
1312
+ }
1313
+ async queryMetadata(id) {
1314
+ return this.client.get(`/api/table/${id}/query_metadata`);
1315
+ }
1316
+ async foreignKeys(id) {
1317
+ return this.client.get(`/api/table/${id}/fks`);
1318
+ }
1319
+ };
1320
+
1321
+ // src/api/field.ts
1322
+ var FieldApi = class {
1323
+ constructor(client) {
1324
+ this.client = client;
1325
+ }
1326
+ async get(id) {
1327
+ return this.client.get(`/api/field/${id}`);
1328
+ }
1329
+ async values(id) {
1330
+ return this.client.get(`/api/field/${id}/values`);
1331
+ }
1332
+ };
1333
+
1334
+ // src/commands/database.ts
1335
+ function databaseCommand() {
1336
+ const cmd = new import_commander6.Command("database").description("Browse databases, tables, and fields").addHelpText("after", `
1337
+ Examples:
1338
+ $ metabase-cli database list
1339
+ $ metabase-cli database show 1
1340
+ $ metabase-cli database schemas 1
1341
+ $ metabase-cli database tables 1 public`);
1342
+ cmd.command("list").description("List all databases").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
1343
+ Examples:
1344
+ $ metabase-cli database list
1345
+ $ metabase-cli database list --format json`).action(async (opts) => {
1346
+ const client = await resolveClient();
1347
+ const api = new DatabaseApi(client);
1348
+ const result = await api.list();
1349
+ if (opts.format === "json") {
1350
+ console.log(formatJson(result.data));
1351
+ return;
1352
+ }
1353
+ console.log(
1354
+ formatEntityTable(result.data, [
1355
+ { key: "id", header: "ID" },
1356
+ { key: "name", header: "Name" },
1357
+ { key: "engine", header: "Engine" }
1358
+ ])
1359
+ );
1360
+ });
1361
+ cmd.command("show <id>").description("Show database details").addHelpText("after", `
1362
+ Examples:
1363
+ $ metabase-cli database show 1`).action(async (id) => {
1364
+ const client = await resolveClient();
1365
+ const api = new DatabaseApi(client);
1366
+ const db = await api.get(parseInt(id));
1367
+ console.log(formatJson(db));
1368
+ });
1369
+ cmd.command("metadata <id>").description("Show database metadata (tables and fields)").addHelpText("after", `
1370
+ Examples:
1371
+ $ metabase-cli database metadata 1`).action(async (id) => {
1372
+ const client = await resolveClient();
1373
+ const api = new DatabaseApi(client);
1374
+ const meta = await api.metadata(parseInt(id));
1375
+ console.log(formatJson(meta));
1376
+ });
1377
+ cmd.command("schemas <id>").description("List schemas in a database").addHelpText("after", `
1378
+ Examples:
1379
+ $ metabase-cli database schemas 1`).action(async (id) => {
1380
+ const client = await resolveClient();
1381
+ const api = new DatabaseApi(client);
1382
+ const schemas = await api.schemas(parseInt(id));
1383
+ for (const s of schemas) console.log(s);
1384
+ });
1385
+ cmd.command("tables <dbId> <schema>").description("List tables in a database schema").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
1386
+ Examples:
1387
+ $ metabase-cli database tables 1 public
1388
+ $ metabase-cli database tables 1 public --format json`).action(async (dbId, schema, opts) => {
1389
+ const client = await resolveClient();
1390
+ const api = new DatabaseApi(client);
1391
+ const tables = await api.tablesInSchema(parseInt(dbId), schema);
1392
+ if (opts.format === "json") {
1393
+ console.log(formatJson(tables));
1394
+ return;
1395
+ }
1396
+ console.log(
1397
+ formatEntityTable(tables, [
1398
+ { key: "id", header: "ID" },
1399
+ { key: "name", header: "Name" },
1400
+ { key: "display_name", header: "Display Name" }
1401
+ ])
1402
+ );
1403
+ });
1404
+ return cmd;
1405
+ }
1406
+ function tableCommand() {
1407
+ const cmd = new import_commander6.Command("table").description("Inspect tables").addHelpText("after", `
1408
+ Examples:
1409
+ $ metabase-cli table show 15
1410
+ $ metabase-cli table metadata 15
1411
+ $ metabase-cli table fks 15`);
1412
+ cmd.command("show <id>").description("Show table details").addHelpText("after", `
1413
+ Examples:
1414
+ $ metabase-cli table show 15`).action(async (id) => {
1415
+ const client = await resolveClient();
1416
+ const api = new TableApi(client);
1417
+ const table = await api.get(parseInt(id));
1418
+ console.log(formatJson(table));
1419
+ });
1420
+ cmd.command("metadata <id>").description("Show table metadata with fields").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
1421
+ Examples:
1422
+ $ metabase-cli table metadata 15
1423
+ $ metabase-cli table metadata 15 --format json`).action(async (id, opts) => {
1424
+ const client = await resolveClient();
1425
+ const api = new TableApi(client);
1426
+ const meta = await api.queryMetadata(parseInt(id));
1427
+ if (opts.format === "json") {
1428
+ console.log(formatJson(meta));
1429
+ return;
1430
+ }
1431
+ console.log(`Table: ${meta.name} (${meta.display_name})`);
1432
+ console.log(
1433
+ formatEntityTable(meta.fields, [
1434
+ { key: "id", header: "ID" },
1435
+ { key: "name", header: "Name" },
1436
+ { key: "display_name", header: "Display Name" },
1437
+ { key: "base_type", header: "Type" },
1438
+ { key: "semantic_type", header: "Semantic" }
1439
+ ])
1440
+ );
1441
+ });
1442
+ cmd.command("fks <id>").description("Show foreign keys for a table").addHelpText("after", `
1443
+ Examples:
1444
+ $ metabase-cli table fks 15`).action(async (id) => {
1445
+ const client = await resolveClient();
1446
+ const api = new TableApi(client);
1447
+ const fks = await api.foreignKeys(parseInt(id));
1448
+ console.log(formatJson(fks));
1449
+ });
1450
+ return cmd;
1451
+ }
1452
+ function fieldCommand() {
1453
+ const cmd = new import_commander6.Command("field").description("Inspect fields").addHelpText("after", `
1454
+ Examples:
1455
+ $ metabase-cli field show 100
1456
+ $ metabase-cli field values 100`);
1457
+ cmd.command("show <id>").description("Show field details").addHelpText("after", `
1458
+ Examples:
1459
+ $ metabase-cli field show 100`).action(async (id) => {
1460
+ const client = await resolveClient();
1461
+ const api = new FieldApi(client);
1462
+ const field = await api.get(parseInt(id));
1463
+ console.log(formatJson(field));
1464
+ });
1465
+ cmd.command("values <id>").description("Show distinct values for a field").addHelpText("after", `
1466
+ Examples:
1467
+ $ metabase-cli field values 100`).action(async (id) => {
1468
+ const client = await resolveClient();
1469
+ const api = new FieldApi(client);
1470
+ const result = await api.values(parseInt(id));
1471
+ console.log(formatJson(result));
1472
+ });
1473
+ return cmd;
1474
+ }
1475
+
1476
+ // src/commands/search.ts
1477
+ var import_commander7 = require("commander");
1478
+
1479
+ // src/api/search.ts
1480
+ var SearchApi = class {
1481
+ constructor(client) {
1482
+ this.client = client;
1483
+ }
1484
+ async search(query, params) {
1485
+ const qs = { q: query };
1486
+ if (params?.models?.length) {
1487
+ qs.models = params.models.join(",");
1488
+ }
1489
+ if (params?.limit !== void 0) qs.limit = String(params.limit);
1490
+ if (params?.offset !== void 0) qs.offset = String(params.offset);
1491
+ return this.client.get("/api/search", qs);
1492
+ }
1493
+ };
1494
+
1495
+ // src/commands/search.ts
1496
+ function searchCommand() {
1497
+ const cmd = new import_commander7.Command("search").description("Search across all entities").argument("<query>", "Search query").option("--models <models>", "Filter by type: card, dashboard, collection, table, database").option("--limit <n>", "Max results", parseInt).option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
1498
+ Examples:
1499
+ $ metabase-cli search "revenue"
1500
+ $ metabase-cli search "users" --models card,dashboard
1501
+ $ metabase-cli search "orders" --limit 5 --format json`).action(async (query, opts) => {
1502
+ const client = await resolveClient();
1503
+ const api = new SearchApi(client);
1504
+ const result = await api.search(query, {
1505
+ models: opts.models?.split(","),
1506
+ limit: opts.limit
1507
+ });
1508
+ if (opts.format === "json") {
1509
+ console.log(formatJson(result));
1510
+ return;
1511
+ }
1512
+ console.log(
1513
+ formatEntityTable(result.data, [
1514
+ { key: "id", header: "ID" },
1515
+ { key: "model", header: "Type" },
1516
+ { key: "name", header: "Name" },
1517
+ { key: "description", header: "Description" },
1518
+ { key: "collection_id", header: "Collection" }
1519
+ ])
1520
+ );
1521
+ console.log(`
1522
+ ${result.total} result(s).`);
1523
+ });
1524
+ return cmd;
1525
+ }
1526
+
1527
+ // src/commands/snippet.ts
1528
+ var import_commander8 = require("commander");
1529
+
1530
+ // src/api/snippet.ts
1531
+ var SnippetApi = class {
1532
+ constructor(client) {
1533
+ this.client = client;
1534
+ }
1535
+ async list(params) {
1536
+ return this.client.get("/api/native-query-snippet", params);
1537
+ }
1538
+ async get(id) {
1539
+ return this.client.get(`/api/native-query-snippet/${id}`);
1540
+ }
1541
+ async create(params) {
1542
+ return this.client.post("/api/native-query-snippet", params);
1543
+ }
1544
+ async update(id, params) {
1545
+ return this.client.put(`/api/native-query-snippet/${id}`, params);
1546
+ }
1547
+ };
1548
+
1549
+ // src/commands/snippet.ts
1550
+ function snippetCommand() {
1551
+ const cmd = new import_commander8.Command("snippet").description("Manage SQL snippets").addHelpText("after", `
1552
+ Examples:
1553
+ $ metabase-cli snippet list
1554
+ $ metabase-cli snippet show 3
1555
+ $ metabase-cli snippet create --name "Active filter" --content "WHERE active = true"
1556
+ $ metabase-cli snippet update 3 --content "WHERE active AND NOT deleted" --unsafe`);
1557
+ cmd.command("list").description("List snippets").option("--archived", "Include archived snippets").option("--format <format>", "Output format: table, json", "table").addHelpText("after", `
1558
+ Examples:
1559
+ $ metabase-cli snippet list
1560
+ $ metabase-cli snippet list --archived --format json`).action(async (opts) => {
1561
+ const client = await resolveClient();
1562
+ const api = new SnippetApi(client);
1563
+ const params = {};
1564
+ if (opts.archived) params.archived = "true";
1565
+ const snippets = await api.list(params);
1566
+ if (opts.format === "json") {
1567
+ console.log(formatJson(snippets));
1568
+ return;
1569
+ }
1570
+ console.log(
1571
+ formatEntityTable(snippets, [
1572
+ { key: "id", header: "ID" },
1573
+ { key: "name", header: "Name" },
1574
+ { key: "description", header: "Description" },
1575
+ { key: "creator_id", header: "Creator" }
1576
+ ])
1577
+ );
1578
+ });
1579
+ cmd.command("show <id>").description("Show snippet details and content").addHelpText("after", `
1580
+ Examples:
1581
+ $ metabase-cli snippet show 3`).action(async (id) => {
1582
+ const client = await resolveClient();
1583
+ const api = new SnippetApi(client);
1584
+ const snippet = await api.get(parseInt(id));
1585
+ console.log(formatJson(snippet));
1586
+ });
1587
+ cmd.command("create").description("Create a new snippet").requiredOption("--name <name>", "Snippet name").requiredOption("--content <sql>", "SQL content").option("--description <desc>", "Description").option("--collection <id>", "Collection ID", parseInt).addHelpText("after", `
1588
+ Examples:
1589
+ $ metabase-cli snippet create --name "Active filter" --content "WHERE active = true"
1590
+ $ metabase-cli snippet create --name "Date range" --content "WHERE created_at > NOW() - INTERVAL '30 days'"`).action(async (opts) => {
1591
+ const client = await resolveClient();
1592
+ const api = new SnippetApi(client);
1593
+ const snippet = await api.create({
1594
+ name: opts.name,
1595
+ content: opts.content,
1596
+ description: opts.description,
1597
+ collection_id: opts.collection
1598
+ });
1599
+ console.log(`Snippet #${snippet.id} "${snippet.name}" created.`);
1600
+ });
1601
+ cmd.command("update <id>").description("Update a snippet (safe mode by default)").option("--name <name>", "New name").option("--content <sql>", "New SQL content").option("--description <desc>", "New description").option("--unsafe", "Bypass safe mode", false).addHelpText("after", `
1602
+ Safe mode blocks updates to snippets you didn't create. Use --unsafe to bypass.
1603
+
1604
+ Examples:
1605
+ $ metabase-cli snippet update 3 --content "WHERE active = true AND deleted_at IS NULL"
1606
+ $ metabase-cli snippet update 3 --name "New Name" --unsafe`).action(async function(id, opts) {
1607
+ const client = await resolveClient();
1608
+ const api = new SnippetApi(client);
1609
+ const guard = new SafetyGuard(client, isUnsafe(this, opts.unsafe));
1610
+ const snippetId = parseInt(id);
1611
+ await guard.guard("snippet", snippetId, "update", async () => {
1612
+ const updates = {};
1613
+ if (opts.name) updates.name = opts.name;
1614
+ if (opts.content) updates.content = opts.content;
1615
+ if (opts.description) updates.description = opts.description;
1616
+ const snippet = await api.update(snippetId, updates);
1617
+ console.log(`Snippet #${snippet.id} "${snippet.name}" updated.`);
1618
+ });
1619
+ });
1620
+ return cmd;
1621
+ }
1622
+
1623
+ // src/utils/errors.ts
1624
+ function handleError(err) {
1625
+ if (err instanceof Error) {
1626
+ console.error(`Error: ${err.message}`);
1627
+ } else {
1628
+ console.error("Unknown error:", err);
1629
+ }
1630
+ process.exit(1);
1631
+ }
1632
+
1633
+ // bin/metabase.ts
1634
+ var pkg = JSON.parse((0, import_node_fs3.readFileSync)((0, import_node_path3.resolve)(__dirname, "..", "package.json"), "utf-8"));
1635
+ var program = new import_commander9.Command();
1636
+ program.name("metabase-cli").description("Headless CLI for Metabase instances").version(pkg.version).option("--unsafe", "Bypass safe mode globally");
1637
+ program.addCommand(profileCommand());
1638
+ program.addCommand(queryCommand());
1639
+ program.addCommand(questionCommand());
1640
+ program.addCommand(dashboardCommand());
1641
+ program.addCommand(collectionCommand());
1642
+ program.addCommand(databaseCommand());
1643
+ program.addCommand(tableCommand());
1644
+ program.addCommand(fieldCommand());
1645
+ program.addCommand(searchCommand());
1646
+ program.addCommand(snippetCommand());
1647
+ program.command("login").description("Login to the active profile").addHelpText("after", `
1648
+ Examples:
1649
+ $ metabase-cli login`).action(async () => {
1650
+ const profile = getActiveProfile();
1651
+ if (!profile) {
1652
+ console.error("No active profile. Run: metabase-cli profile add <name>");
1653
+ process.exit(1);
1654
+ }
1655
+ if (profile.auth.method !== "session") {
1656
+ console.log("Profile uses API key auth \u2014 no login needed.");
1657
+ return;
1658
+ }
1659
+ const client = new MetabaseClient(profile);
1660
+ await client.login();
1661
+ console.log(`Logged in to ${profile.domain} as ${profile.auth.email}.`);
1662
+ if (client.getProfile().user) {
1663
+ console.log(`User: ${client.getProfile().user.first_name} ${client.getProfile().user.last_name} (ID: ${client.getProfile().user.id})`);
1664
+ }
1665
+ });
1666
+ program.command("logout").description("Logout from the active profile").addHelpText("after", `
1667
+ Examples:
1668
+ $ metabase-cli logout`).action(async () => {
1669
+ const profile = getActiveProfile();
1670
+ if (!profile) {
1671
+ console.error("No active profile.");
1672
+ process.exit(1);
1673
+ }
1674
+ const client = new MetabaseClient(profile);
1675
+ await client.logout();
1676
+ console.log("Logged out.");
1677
+ });
1678
+ program.command("whoami").description("Show current user info").option("--refresh", "Refresh cached user info from server").addHelpText("after", `
1679
+ Examples:
1680
+ $ metabase-cli whoami
1681
+ $ metabase-cli whoami --refresh`).action(async (opts) => {
1682
+ const profile = getActiveProfile();
1683
+ if (!profile) {
1684
+ console.error("No active profile.");
1685
+ process.exit(1);
1686
+ }
1687
+ if (!opts.refresh && profile.user) {
1688
+ console.log(`${profile.user.first_name} ${profile.user.last_name}`);
1689
+ console.log(`Email: ${profile.user.email}`);
1690
+ console.log(`User ID: ${profile.user.id}`);
1691
+ console.log(`Superuser: ${profile.user.is_superuser}`);
1692
+ console.log(`Profile: ${profile.name}`);
1693
+ console.log(`Domain: ${profile.domain}`);
1694
+ return;
1695
+ }
1696
+ const client = new MetabaseClient(profile);
1697
+ await client.ensureAuthenticated();
1698
+ const user = await client.get("/api/user/current");
1699
+ updateProfile(profile.name, {
1700
+ user: {
1701
+ id: user.id,
1702
+ email: user.email,
1703
+ first_name: user.first_name,
1704
+ last_name: user.last_name,
1705
+ is_superuser: user.is_superuser
1706
+ }
1707
+ });
1708
+ console.log(formatJson(user));
1709
+ });
1710
+ program.parseAsync(process.argv).catch(handleError);