@metabase/cli 0.1.0 → 0.1.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.
- package/README.md +972 -57
- package/dist/add-collection-DwxQDXzL.mjs +54 -0
- package/dist/add-collection-SL08iMub.mjs +11 -0
- package/dist/api-key-BktzvPb7.mjs +13 -0
- package/dist/{archive-CsWeHXle.mjs → archive-C1mF-9Kj.mjs} +7 -4
- package/dist/archive-CLWtbvvH.mjs +44 -0
- package/dist/archive-Cq4WKmJt.mjs +44 -0
- package/dist/archive-kYoy5LK5.mjs +39 -0
- package/dist/auth-DfYkakP3.mjs +19 -0
- package/dist/{body-Dv9hQ0Qk.mjs → body-rDrR-C1c.mjs} +3 -2
- package/dist/{branches-BujtceGr.mjs → branches-CH2UcCpX.mjs} +8 -6
- package/dist/cancel-CgLZcItQ.mjs +56 -0
- package/dist/{cancel-task-CT2xUMRg.mjs → cancel-task-DcYrFsM6.mjs} +9 -7
- package/dist/{card-CsXk8T6A.mjs → card-CQxvHeyP.mjs} +34 -15
- package/dist/card-ZCGU2JEh.mjs +20 -0
- package/dist/cards-C4NIaERo.mjs +37 -0
- package/dist/cli.mjs +33 -14
- package/dist/collection-B3sPXRLs.mjs +163 -0
- package/dist/collection-D_uFLIAS.mjs +19 -0
- package/dist/create-BUCLNqiN.mjs +48 -0
- package/dist/create-CB0Yp__0.mjs +66 -0
- package/dist/create-CNvd5T8h.mjs +48 -0
- package/dist/create-Cbh1cGj9.mjs +48 -0
- package/dist/create-CzfNOhOF.mjs +48 -0
- package/dist/create-DU0ZhnZu.mjs +44 -0
- package/dist/create-Dh0p-c2Y.mjs +44 -0
- package/dist/create-DvrVZ2hS.mjs +125 -0
- package/dist/create-QgN369N5.mjs +50 -0
- package/dist/{create-B8ektf-R.mjs → create-bqc_rmix.mjs} +8 -6
- package/dist/{create-branch-goZBTNnr.mjs → create-branch-BJFH9Hda.mjs} +9 -7
- package/dist/credentials-DTP1xuKz.mjs +85 -0
- package/dist/{current-task-DBjRNCFq.mjs → current-task-z_TiJ0kt.mjs} +9 -7
- package/dist/dashboard-CnMD04PQ.mjs +163 -0
- package/dist/dashboard-G1-dGLUR.mjs +20 -0
- package/dist/database-DQkUxTLd.mjs +17 -0
- package/dist/database-vvig8k4x.mjs +51 -0
- package/dist/db-CBaEfumR.mjs +22 -0
- package/dist/{delete-8vGU35r3.mjs → delete-CVYII8mq.mjs} +7 -5
- package/dist/{delete-B27KLF5X.mjs → delete-DeZQ1r9w.mjs} +7 -5
- package/dist/{delete-runtime-Byr60cR3.mjs → delete-runtime-BMzvfj_B.mjs} +4 -4
- package/dist/{delete-table-BNaJ_gA4.mjs → delete-table-ZiR9-ndv.mjs} +7 -5
- package/dist/deprovision-BhD3J-Am.mjs +61 -0
- package/dist/{dirty-aNUuph4I.mjs → dirty-D9agt7Os.mjs} +8 -6
- package/dist/docker-CHpV8PRz.mjs +612 -0
- package/dist/eid-B5wawMmO.mjs +13 -0
- package/dist/{export-QDkuuzSE.mjs → export-Bfk7JAlR.mjs} +30 -23
- package/dist/field-B3gvaqpK.mjs +278 -0
- package/dist/field-BDJ1pEgr.mjs +18 -0
- package/dist/fields-7ByLsxLg.mjs +38 -0
- package/dist/flag-pair-DtR1AiBQ.mjs +17 -0
- package/dist/get-BE6Izpus.mjs +36 -0
- package/dist/get-C3CcAJGg.mjs +49 -0
- package/dist/{get-DI_IJvgk.mjs → get-CQGeF-eP.mjs} +6 -4
- package/dist/get-D2m4jhwT.mjs +53 -0
- package/dist/{get-BGBIzMKY.mjs → get-DKy3DAJX.mjs} +6 -4
- package/dist/{get-COXHplHP.mjs → get-DUSR5i99.mjs} +7 -5
- package/dist/get-DikegGzi.mjs +36 -0
- package/dist/get-StkjKuh0.mjs +40 -0
- package/dist/get-bYc7eGYe.mjs +36 -0
- package/dist/{get-Cl8-IauC.mjs → get-cuHp9-6U.mjs} +7 -4
- package/dist/{get-i6LWOByV.mjs → get-gOT_RarI.mjs} +6 -4
- package/dist/get-run-D59Yqaoh.mjs +36 -0
- package/dist/get-tISo-cmg.mjs +41 -0
- package/dist/git-sync-BiTWfLgY.mjs +28 -0
- package/dist/{has-remote-changes-hjKoQuRy.mjs → has-remote-changes-B1TciDVD.mjs} +8 -6
- package/dist/{import-HJsSKRYx.mjs → import-DnnmmJbp.mjs} +11 -9
- package/dist/{input-Dojr-RTw.mjs → input-ikCiip6x.mjs} +2 -1
- package/dist/is-dirty-DClGFOGV.mjs +10 -0
- package/dist/{is-dirty-1Qy7hiHB.mjs → is-dirty-DlfX7e39.mjs} +5 -4
- package/dist/items-DQFQSpjF.mjs +77 -0
- package/dist/{key-DBxPSFwi.mjs → key-NDEARu2L.mjs} +1 -1
- package/dist/{license-MoWse3ZI.mjs → license-DBh13sc8.mjs} +3 -3
- package/dist/list-4kYCGv01.mjs +32 -0
- package/dist/list-9AOWhxqp.mjs +61 -0
- package/dist/{list-Bk6RsbJl.mjs → list-BwjqQ6pp.mjs} +5 -3
- package/dist/{list-C_PRdL5e.mjs → list-CP5RNjO6.mjs} +7 -5
- package/dist/{list-C8tdLOH5.mjs → list-Cy0VhXQs.mjs} +5 -3
- package/dist/list-D067ZSE5.mjs +47 -0
- package/dist/list-DAZP-IM5.mjs +32 -0
- package/dist/list-DJN-OvTZ.mjs +52 -0
- package/dist/list-DQj-QJAs.mjs +40 -0
- package/dist/list-Di529OJD.mjs +55 -0
- package/dist/{list-C4Ajrw8f.mjs → list-DlKzgnqo.mjs} +6 -3
- package/dist/list-GFfR9SuT.mjs +32 -0
- package/dist/{list-CWt3fqrZ.mjs → list-iFVEdi2J.mjs} +5 -3
- package/dist/{login-C9WTwNn6.mjs → login-DxgkosGx.mjs} +30 -9
- package/dist/{logout-oLszGCOg.mjs → logout-BlVwqBog.mjs} +7 -6
- package/dist/logs-CudNEkT4.mjs +58 -0
- package/dist/{manifest-CAdjQYH8.mjs → manifest-Dv5B9Blc.mjs} +3 -7
- package/dist/measure-BEQfnLdN.mjs +67 -0
- package/dist/measure-C7SbdYQk.mjs +19 -0
- package/dist/metadata-B2Td415K.mjs +38 -0
- package/dist/metadata-BTJAFVvZ.mjs +37 -0
- package/dist/{package-BGfw4ZWJ.mjs → package-DV6Asqim.mjs} +7 -1
- package/dist/paginate-CTSfuYiF.mjs +49 -0
- package/dist/parse-id-B38zTlYs.mjs +12 -0
- package/dist/parse-ref-DGvh4aDn.mjs +17 -0
- package/dist/parse-schemas-Ds-cVE-O.mjs +12 -0
- package/dist/{poll-ILanYysl.mjs → poll-Bh6oAifO.mjs} +2 -1
- package/dist/{poll-task-DbpsiQhl.mjs → poll-task-vPwV31Fs.mjs} +8 -7
- package/dist/predicates-DiIiS3k7.mjs +153 -0
- package/dist/preflight-DxJb-hUV.mjs +91 -0
- package/dist/{prompt-DpT8yAVy.mjs → prompt-Bf3DQ-qE.mjs} +1 -1
- package/dist/provision-B-I0zuDe.mjs +77 -0
- package/dist/ps-BmYQYC7t.mjs +10 -0
- package/dist/ps-CaiOFCv2.mjs +78 -0
- package/dist/query-BtF1yWZZ.mjs +90 -0
- package/dist/{query-PihYi-UZ.mjs → query-jmfqaXRP.mjs} +38 -13
- package/dist/remove-C2iv0g03.mjs +98 -0
- package/dist/remove-collection-DhZghaZy.mjs +38 -0
- package/dist/{remove-B2hVYn1v.mjs → remove-xskleeru.mjs} +6 -5
- package/dist/render-DXv-D6fU.mjs +182 -0
- package/dist/rescan-values-DW6u90ep.mjs +43 -0
- package/dist/revision-message-flag-CWQbKhdl.mjs +11 -0
- package/dist/{run-C2so6Qp6.mjs → run-DxVzhcF3.mjs} +27 -36
- package/dist/runs-BOHk1XnM.mjs +54 -0
- package/dist/{runtime-C9CEZhcn.mjs → runtime-cwBS8wwK.mjs} +428 -442
- package/dist/schema-tables-CcFbY_jN.mjs +45 -0
- package/dist/schemas-DZmv_V62.mjs +47 -0
- package/dist/{search-CopOytXY.mjs → search-CYMuc7Fg.mjs} +6 -19
- package/dist/segment-BMrUBz94.mjs +70 -0
- package/dist/segment-Df4pfjco.mjs +19 -0
- package/dist/{set-BcF7M1GQ.mjs → set-B_rrVwU4.mjs} +6 -4
- package/dist/{set-CbibegpA.mjs → set-CbGfQ7Ye.mjs} +8 -6
- package/dist/{setting-U3NtBMFo.mjs → setting-DqZY9NXP.mjs} +3 -3
- package/dist/setup-DxmcAorA.mjs +71 -0
- package/dist/snippet-CwSHjQyn.mjs +19 -0
- package/dist/snippet-Dw0Sjzkr.mjs +64 -0
- package/dist/start-Cn0epTks.mjs +380 -0
- package/dist/{stash-DOBbYozC.mjs → stash-BFZIl9F4.mjs} +9 -7
- package/dist/{status-Buf1ZbNR.mjs → status-BjCeJNLp.mjs} +10 -8
- package/dist/{status-CUcs8XBH.mjs → status-FDIDmqvM.mjs} +4 -2
- package/dist/{status-D1F5XHae.mjs → status-UALK3OJl.mjs} +4 -2
- package/dist/stop-DUwrDWw8.mjs +81 -0
- package/dist/summary-CS4UGiFJ.mjs +41 -0
- package/dist/sync-schema-IrHdJxmX.mjs +43 -0
- package/dist/{table-Cfk7oSvw.mjs → table-B-PYcgGb.mjs} +22 -9
- package/dist/table-Cdr5bKp1.mjs +19 -0
- package/dist/transform-CeZusR_w.mjs +24 -0
- package/dist/{transform-B5uRpg1G.mjs → transform-IEX4Mx3X.mjs} +56 -2
- package/dist/transform-job-BOn9-CGa.mjs +19 -0
- package/dist/{transform-job-C7QXWTVE.mjs → transform-job-Csr86muI.mjs} +7 -0
- package/dist/translate-B__zbDKm.mjs +111 -0
- package/dist/tree-Mh0uQ_Wy.mjs +32 -0
- package/dist/update-1Di9hbPo.mjs +56 -0
- package/dist/update-B5_pp6Jj.mjs +56 -0
- package/dist/update-B9DBMo30.mjs +52 -0
- package/dist/update-BfBsM_y1.mjs +56 -0
- package/dist/update-Bw0WZix_.mjs +73 -0
- package/dist/update-Cp1789qq.mjs +52 -0
- package/dist/update-D2VI_5cy.mjs +57 -0
- package/dist/update-D8GwQTcL.mjs +59 -0
- package/dist/{update-CL8tRbxr.mjs → update-Masp5WeT.mjs} +9 -7
- package/dist/update-dashcard-CNiQw1MD.mjs +71 -0
- package/dist/update-j9vgemKR.mjs +51 -0
- package/dist/url-GFM76VIK.mjs +54 -0
- package/dist/uuid-Uif0lNk8.mjs +47 -0
- package/dist/validate-DCYx6jdL.mjs +1496 -0
- package/dist/validate-query-B07oGG4K.mjs +37 -0
- package/dist/values-DrwNHUAI.mjs +36 -0
- package/dist/{wait-Bugr9eXD.mjs → wait-BoKk8CJy.mjs} +10 -8
- package/dist/wait-DO7tS7NI.mjs +19 -0
- package/dist/wait-flags-CjX2sEGm.mjs +35 -0
- package/dist/workspace-CyEX40D-.mjs +24 -0
- package/dist/workspace-DVuqKJGG.mjs +72 -0
- package/dist/workspace-credentials-B6BL-X0d.mjs +139 -0
- package/package.json +7 -1
- package/dist/auth-BF7IjZIH.mjs +0 -18
- package/dist/card-_Ta7zdYe.mjs +0 -19
- package/dist/create-CI2Cunq5.mjs +0 -38
- package/dist/create-DdbU3TLX.mjs +0 -42
- package/dist/database-PA9Goi25.mjs +0 -33
- package/dist/db-DMghzgb6.mjs +0 -17
- package/dist/field-C8IVs6rp.mjs +0 -76
- package/dist/field-DaYo_90x.mjs +0 -13
- package/dist/get-Cwpj7lDe.mjs +0 -35
- package/dist/get-Dh_acl8q.mjs +0 -34
- package/dist/is-dirty-DpKn9HJp.mjs +0 -8
- package/dist/list-CBSBHtK-.mjs +0 -38
- package/dist/parse-id-BhmmfyCP.mjs +0 -14
- package/dist/sync-BPyGXfUk.mjs +0 -26
- package/dist/table-D7nJt7JO.mjs +0 -16
- package/dist/transform-UbyewMxY.mjs +0 -21
- package/dist/transform-job-CrYkr-Ma.mjs +0 -19
- package/dist/update-DU2oU2j-.mjs +0 -49
- /package/dist/{body-flags-BUA9XV1u.mjs → body-flags-BK7J6Daz.mjs} +0 -0
- /package/dist/{setting-26ckqHAP.mjs → setting-CTaAeMci.mjs} +0 -0
|
@@ -1,113 +1,16 @@
|
|
|
1
|
-
import { package_default } from "./package-
|
|
1
|
+
import { package_default } from "./package-DV6Asqim.mjs";
|
|
2
2
|
import { setMetabaseAugment } from "./command-augment-D9pI9Vbh.mjs";
|
|
3
|
+
import { AbortError, ConfigError, MetabaseError, NetworkError, TimeoutError, VERBOSE_ENV, ValidationError, errorMessage, isNotFoundError, isPlainObject, toMetabaseError } from "./predicates-DiIiS3k7.mjs";
|
|
3
4
|
import { defineCommand } from "citty";
|
|
4
|
-
import {
|
|
5
|
+
import { z } from "zod";
|
|
5
6
|
import { promises } from "node:fs";
|
|
6
|
-
import { homedir } from "node:os";
|
|
7
7
|
import { dirname, join } from "node:path";
|
|
8
|
+
import { homedir } from "node:os";
|
|
8
9
|
import { Entry } from "@napi-rs/keyring";
|
|
9
|
-
import { isCancel } from "@clack/prompts";
|
|
10
10
|
import { setTimeout } from "node:timers/promises";
|
|
11
|
-
import Table from "cli-table3";
|
|
12
11
|
|
|
13
|
-
//#region src/core/errors.ts
|
|
14
|
-
var MetabaseError = class extends Error {
|
|
15
|
-
get userMessage() {
|
|
16
|
-
return this.message;
|
|
17
|
-
}
|
|
18
|
-
};
|
|
19
|
-
var NetworkError = class extends MetabaseError {
|
|
20
|
-
category = "network";
|
|
21
|
-
isRetryable = true;
|
|
22
|
-
exitCode = 1;
|
|
23
|
-
developerDetail;
|
|
24
|
-
constructor(message, developerDetail) {
|
|
25
|
-
super(message);
|
|
26
|
-
this.name = "NetworkError";
|
|
27
|
-
this.developerDetail = developerDetail;
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
var TimeoutError = class extends MetabaseError {
|
|
31
|
-
category = "timeout";
|
|
32
|
-
isRetryable = true;
|
|
33
|
-
exitCode = 1;
|
|
34
|
-
developerDetail;
|
|
35
|
-
constructor(message, developerDetail) {
|
|
36
|
-
super(message);
|
|
37
|
-
this.name = "TimeoutError";
|
|
38
|
-
this.developerDetail = developerDetail;
|
|
39
|
-
}
|
|
40
|
-
};
|
|
41
|
-
var ValidationError = class extends MetabaseError {
|
|
42
|
-
category = "validation";
|
|
43
|
-
isRetryable = false;
|
|
44
|
-
exitCode = 1;
|
|
45
|
-
developerDetail;
|
|
46
|
-
constructor(message, developerDetail) {
|
|
47
|
-
super(message);
|
|
48
|
-
this.name = "ValidationError";
|
|
49
|
-
this.developerDetail = developerDetail;
|
|
50
|
-
}
|
|
51
|
-
};
|
|
52
|
-
var ConfigError = class extends MetabaseError {
|
|
53
|
-
category = "config";
|
|
54
|
-
isRetryable = false;
|
|
55
|
-
exitCode = 2;
|
|
56
|
-
developerDetail = null;
|
|
57
|
-
constructor(message) {
|
|
58
|
-
super(message);
|
|
59
|
-
this.name = "ConfigError";
|
|
60
|
-
}
|
|
61
|
-
};
|
|
62
|
-
var AbortError = class extends MetabaseError {
|
|
63
|
-
category = "abort";
|
|
64
|
-
isRetryable = false;
|
|
65
|
-
exitCode = 130;
|
|
66
|
-
developerDetail = null;
|
|
67
|
-
constructor(message = "aborted") {
|
|
68
|
-
super(message);
|
|
69
|
-
this.name = "AbortError";
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
var UnknownError = class extends MetabaseError {
|
|
73
|
-
category = "unknown";
|
|
74
|
-
isRetryable = false;
|
|
75
|
-
exitCode = 1;
|
|
76
|
-
developerDetail;
|
|
77
|
-
constructor(input) {
|
|
78
|
-
super(input.originalMessage);
|
|
79
|
-
this.name = "UnknownError";
|
|
80
|
-
this.developerDetail = input;
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
function toMetabaseError(error) {
|
|
84
|
-
if (error instanceof MetabaseError) return error;
|
|
85
|
-
if (isCancel(error)) return new AbortError();
|
|
86
|
-
if (error instanceof ZodError) return new ConfigError(formatZodError(error));
|
|
87
|
-
if (error instanceof Error) return new UnknownError({
|
|
88
|
-
originalMessage: error.message,
|
|
89
|
-
stack: error.stack ?? null
|
|
90
|
-
});
|
|
91
|
-
return new UnknownError({
|
|
92
|
-
originalMessage: String(error),
|
|
93
|
-
stack: null
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
function formatZodError(error) {
|
|
97
|
-
return error.issues.map((issue) => {
|
|
98
|
-
const path = issue.path.join(".");
|
|
99
|
-
return path ? `${path}: ${issue.message}` : issue.message;
|
|
100
|
-
}).join("; ");
|
|
101
|
-
}
|
|
102
|
-
function isNotFoundError(value) {
|
|
103
|
-
return value instanceof Error && "code" in value && value.code === "ENOENT";
|
|
104
|
-
}
|
|
105
|
-
function errorMessage(value) {
|
|
106
|
-
return value instanceof Error ? value.message : String(value);
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
//#endregion
|
|
110
12
|
//#region src/runtime/json.ts
|
|
13
|
+
const JSON_CONTENT_TYPE$1 = "application/json";
|
|
111
14
|
function parseJson(input, schema, opts = {}) {
|
|
112
15
|
const result = parseJsonResult(input, schema, opts);
|
|
113
16
|
if (!result.ok) throw result.error;
|
|
@@ -137,12 +40,175 @@ function parseJsonResult(input, schema, opts = {}) {
|
|
|
137
40
|
value: parsed.data
|
|
138
41
|
};
|
|
139
42
|
}
|
|
43
|
+
function parseJsonOrPlain(text, contentType, schema, opts = {}) {
|
|
44
|
+
if (!isJsonContentType(contentType)) return parseJson(JSON.stringify(text), schema, opts);
|
|
45
|
+
const attempt = parseJsonResult(text, schema, opts);
|
|
46
|
+
if (attempt.ok) return attempt.value;
|
|
47
|
+
if (attempt.error instanceof ValidationError) throw attempt.error;
|
|
48
|
+
return parseJson(JSON.stringify(text), schema, opts);
|
|
49
|
+
}
|
|
50
|
+
function isJsonContentType(contentType) {
|
|
51
|
+
return contentType !== null && contentType.includes(JSON_CONTENT_TYPE$1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
//#endregion
|
|
55
|
+
//#region src/output/types.ts
|
|
56
|
+
const DEFAULT_MAX_BYTES = 65536;
|
|
57
|
+
function listEnvelopeSchema(item) {
|
|
58
|
+
return z.object({
|
|
59
|
+
data: z.array(item),
|
|
60
|
+
returned: z.number().int().nonnegative(),
|
|
61
|
+
total: z.number().int().nonnegative().nullable().optional(),
|
|
62
|
+
limit: z.number().int().nonnegative().optional(),
|
|
63
|
+
truncated: z.object({
|
|
64
|
+
reason: z.literal("max_bytes"),
|
|
65
|
+
bytes: z.number().int().nonnegative()
|
|
66
|
+
}).optional()
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
function wrapList(items) {
|
|
70
|
+
return {
|
|
71
|
+
data: items,
|
|
72
|
+
returned: items.length,
|
|
73
|
+
total: items.length
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
//#endregion
|
|
78
|
+
//#region src/commands/flags.ts
|
|
79
|
+
const outputFlags = {
|
|
80
|
+
format: {
|
|
81
|
+
type: "string",
|
|
82
|
+
description: "auto | json | text",
|
|
83
|
+
default: "auto"
|
|
84
|
+
},
|
|
85
|
+
json: {
|
|
86
|
+
type: "boolean",
|
|
87
|
+
description: "Shorthand for --format json"
|
|
88
|
+
},
|
|
89
|
+
full: {
|
|
90
|
+
type: "boolean",
|
|
91
|
+
description: "Return the full object (default: compact)"
|
|
92
|
+
},
|
|
93
|
+
fields: {
|
|
94
|
+
type: "string",
|
|
95
|
+
description: "Dot-paths, comma separated (mutually exclusive with --full)"
|
|
96
|
+
},
|
|
97
|
+
maxBytes: {
|
|
98
|
+
type: "string",
|
|
99
|
+
description: "Output size cap; 0 disables",
|
|
100
|
+
default: String(DEFAULT_MAX_BYTES),
|
|
101
|
+
alias: "max-bytes"
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
const profileFlag = { profile: {
|
|
105
|
+
type: "string",
|
|
106
|
+
description: "Named profile (default: 'default')"
|
|
107
|
+
} };
|
|
108
|
+
const connectionFlags = {
|
|
109
|
+
url: {
|
|
110
|
+
type: "string",
|
|
111
|
+
description: "Metabase URL"
|
|
112
|
+
},
|
|
113
|
+
apiKey: {
|
|
114
|
+
type: "string",
|
|
115
|
+
description: "API key",
|
|
116
|
+
alias: "api-key"
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region src/commands/parse-integer.ts
|
|
122
|
+
const INTEGER_PATTERN = /^-?\d+$/;
|
|
123
|
+
function parseInteger(value, options) {
|
|
124
|
+
const trimmed = value.trim();
|
|
125
|
+
if (!INTEGER_PATTERN.test(trimmed)) throw new ConfigError(`invalid ${options.name}: "${value}" (expected integer)`);
|
|
126
|
+
const parsed = Number.parseInt(trimmed, 10);
|
|
127
|
+
if (parsed < options.min) throw new ConfigError(`invalid ${options.name}: ${parsed} (must be ≥ ${options.min})`);
|
|
128
|
+
return parsed;
|
|
129
|
+
}
|
|
130
|
+
function parseOptionalInteger(value, options) {
|
|
131
|
+
if (value === void 0 || value === "") return null;
|
|
132
|
+
return parseInteger(value, options);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
//#endregion
|
|
136
|
+
//#region src/core/paths.ts
|
|
137
|
+
const APP_DIR_NAME = "metabase-cli";
|
|
138
|
+
function configDir() {
|
|
139
|
+
if (process.platform === "win32") {
|
|
140
|
+
const appData = process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming");
|
|
141
|
+
return join(appData, APP_DIR_NAME);
|
|
142
|
+
}
|
|
143
|
+
const xdg = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
|
|
144
|
+
return join(xdg, APP_DIR_NAME);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
//#endregion
|
|
148
|
+
//#region src/core/auth/rejection.ts
|
|
149
|
+
const REJECTIONS_FILE = "rejections.json";
|
|
150
|
+
const REJECTIONS_FILE_MODE = 384;
|
|
151
|
+
const REJECTIONS_DIR_MODE = 448;
|
|
152
|
+
const RejectionRecord = z.object({
|
|
153
|
+
reason: z.string(),
|
|
154
|
+
url: z.string(),
|
|
155
|
+
rejectedAt: z.string()
|
|
156
|
+
});
|
|
157
|
+
const RejectionsFileSchema = z.record(z.string(), RejectionRecord);
|
|
158
|
+
function rejectionsFilePath() {
|
|
159
|
+
return join(configDir(), REJECTIONS_FILE);
|
|
160
|
+
}
|
|
161
|
+
async function readRejectionsFile() {
|
|
162
|
+
const path = rejectionsFilePath();
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = await promises.readFile(path, "utf8");
|
|
166
|
+
} catch (error) {
|
|
167
|
+
if (isNotFoundError(error)) return {};
|
|
168
|
+
throw error;
|
|
169
|
+
}
|
|
170
|
+
return parseJson(raw, RejectionsFileSchema, { source: path });
|
|
171
|
+
}
|
|
172
|
+
async function writeRejectionsFile(store) {
|
|
173
|
+
const path = rejectionsFilePath();
|
|
174
|
+
if (Object.keys(store).length === 0) {
|
|
175
|
+
await promises.unlink(path).catch(() => void 0);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
await promises.mkdir(dirname(path), {
|
|
179
|
+
recursive: true,
|
|
180
|
+
mode: REJECTIONS_DIR_MODE
|
|
181
|
+
});
|
|
182
|
+
await promises.writeFile(path, JSON.stringify(store, null, 2) + "\n", { mode: REJECTIONS_FILE_MODE });
|
|
183
|
+
if (process.platform !== "win32") await promises.chmod(path, REJECTIONS_FILE_MODE);
|
|
184
|
+
}
|
|
185
|
+
async function recordRejection(profile, input) {
|
|
186
|
+
const store = await readRejectionsFile();
|
|
187
|
+
store[profile] = {
|
|
188
|
+
reason: input.reason,
|
|
189
|
+
url: input.url,
|
|
190
|
+
rejectedAt: new Date().toISOString()
|
|
191
|
+
};
|
|
192
|
+
await writeRejectionsFile(store);
|
|
193
|
+
}
|
|
194
|
+
async function clearRejection(profile) {
|
|
195
|
+
const store = await readRejectionsFile();
|
|
196
|
+
if (!(profile in store)) return false;
|
|
197
|
+
delete store[profile];
|
|
198
|
+
await writeRejectionsFile(store);
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
201
|
+
async function readRejection(profile) {
|
|
202
|
+
const store = await readRejectionsFile();
|
|
203
|
+
return store[profile] ?? null;
|
|
204
|
+
}
|
|
140
205
|
|
|
141
206
|
//#endregion
|
|
142
207
|
//#region src/core/auth/storage.ts
|
|
143
208
|
const CredentialsFileSchema = z.record(z.string(), z.string());
|
|
144
209
|
const KEYRING_SERVICE = "metabase-cli";
|
|
145
210
|
const CREDENTIALS_FILE = "credentials.json";
|
|
211
|
+
const PROFILE_INDEX_FILE = "profiles.json";
|
|
146
212
|
const DEFAULT_PROFILE = "default";
|
|
147
213
|
const CREDENTIALS_FILE_MODE = 384;
|
|
148
214
|
const CREDENTIALS_DIR_MODE = 448;
|
|
@@ -151,17 +217,14 @@ const account = {
|
|
|
151
217
|
profileApiKey: (profile) => `profile:${profile}:apiKey`,
|
|
152
218
|
license: "license"
|
|
153
219
|
};
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
const appData = process.env["APPDATA"] ?? join(homedir(), "AppData", "Roaming");
|
|
157
|
-
return join(appData, "metabase-cli");
|
|
158
|
-
}
|
|
159
|
-
const xdg = process.env["XDG_CONFIG_HOME"] ?? join(homedir(), ".config");
|
|
160
|
-
return join(xdg, "metabase-cli");
|
|
161
|
-
}
|
|
220
|
+
const ProfileIndexSchema = z.array(z.string());
|
|
221
|
+
const FILE_STORE_PROFILE_URL_PATTERN = /^profile:(.+):url$/;
|
|
162
222
|
function fallbackFilePath() {
|
|
163
223
|
return join(configDir(), CREDENTIALS_FILE);
|
|
164
224
|
}
|
|
225
|
+
function profileIndexPath() {
|
|
226
|
+
return join(configDir(), PROFILE_INDEX_FILE);
|
|
227
|
+
}
|
|
165
228
|
function keyringEnabled() {
|
|
166
229
|
return process.env["METABASE_CLI_DISABLE_KEYRING"] !== "1";
|
|
167
230
|
}
|
|
@@ -281,13 +344,74 @@ async function readProfile(name = DEFAULT_PROFILE) {
|
|
|
281
344
|
}
|
|
282
345
|
async function writeProfile(profile, name = DEFAULT_PROFILE) {
|
|
283
346
|
await credentials.set(account.profileUrl(name), profile.url);
|
|
284
|
-
|
|
347
|
+
const location = await credentials.set(account.profileApiKey(name), profile.apiKey);
|
|
348
|
+
await addToProfileIndex(name);
|
|
349
|
+
return location;
|
|
285
350
|
}
|
|
286
351
|
async function clearProfile(name = DEFAULT_PROFILE) {
|
|
287
352
|
const removedUrl = await credentials.remove(account.profileUrl(name));
|
|
288
353
|
const removedKey = await credentials.remove(account.profileApiKey(name));
|
|
354
|
+
await removeFromProfileIndex(name);
|
|
289
355
|
return removedUrl || removedKey;
|
|
290
356
|
}
|
|
357
|
+
async function listProfileNames() {
|
|
358
|
+
const stored = await readProfileIndex();
|
|
359
|
+
if (stored !== null) return stored;
|
|
360
|
+
const backfilled = await backfillProfileIndexFromFile();
|
|
361
|
+
if (backfilled.length > 0) await writeProfileIndex(backfilled);
|
|
362
|
+
return backfilled;
|
|
363
|
+
}
|
|
364
|
+
async function readProfileIndex() {
|
|
365
|
+
const path = profileIndexPath();
|
|
366
|
+
let raw;
|
|
367
|
+
try {
|
|
368
|
+
raw = await promises.readFile(path, "utf8");
|
|
369
|
+
} catch (error) {
|
|
370
|
+
if (isNotFoundError(error)) return null;
|
|
371
|
+
throw error;
|
|
372
|
+
}
|
|
373
|
+
return parseJson(raw, ProfileIndexSchema, { source: path });
|
|
374
|
+
}
|
|
375
|
+
async function writeProfileIndex(names) {
|
|
376
|
+
const path = profileIndexPath();
|
|
377
|
+
const unique = [...new Set(names)].toSorted();
|
|
378
|
+
await promises.mkdir(dirname(path), {
|
|
379
|
+
recursive: true,
|
|
380
|
+
mode: CREDENTIALS_DIR_MODE
|
|
381
|
+
});
|
|
382
|
+
await promises.writeFile(path, JSON.stringify(unique, null, 2) + "\n", { mode: CREDENTIALS_FILE_MODE });
|
|
383
|
+
if (process.platform !== "win32") await promises.chmod(path, CREDENTIALS_FILE_MODE);
|
|
384
|
+
}
|
|
385
|
+
async function deleteProfileIndex() {
|
|
386
|
+
await promises.unlink(profileIndexPath()).catch(() => void 0);
|
|
387
|
+
}
|
|
388
|
+
async function addToProfileIndex(name) {
|
|
389
|
+
const current = await listProfileNames();
|
|
390
|
+
if (current.includes(name)) return;
|
|
391
|
+
await writeProfileIndex([...current, name]);
|
|
392
|
+
}
|
|
393
|
+
async function removeFromProfileIndex(name) {
|
|
394
|
+
const current = await listProfileNames();
|
|
395
|
+
const next = current.filter((entry) => entry !== name);
|
|
396
|
+
if (next.length === current.length) return;
|
|
397
|
+
if (next.length === 0) {
|
|
398
|
+
await deleteProfileIndex();
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
await writeProfileIndex(next);
|
|
402
|
+
}
|
|
403
|
+
async function backfillProfileIndexFromFile() {
|
|
404
|
+
const store = await readFileStore();
|
|
405
|
+
const names = new Set();
|
|
406
|
+
for (const key of Object.keys(store)) {
|
|
407
|
+
const name = FILE_STORE_PROFILE_URL_PATTERN.exec(key)?.[1];
|
|
408
|
+
if (name !== void 0) names.add(name);
|
|
409
|
+
}
|
|
410
|
+
return [...names];
|
|
411
|
+
}
|
|
412
|
+
async function readLicense() {
|
|
413
|
+
return credentials.read(account.license);
|
|
414
|
+
}
|
|
291
415
|
async function writeLicense(token) {
|
|
292
416
|
return credentials.set(account.license, token);
|
|
293
417
|
}
|
|
@@ -295,6 +419,89 @@ async function clearLicense() {
|
|
|
295
419
|
return credentials.remove(account.license);
|
|
296
420
|
}
|
|
297
421
|
|
|
422
|
+
//#endregion
|
|
423
|
+
//#region src/core/url.ts
|
|
424
|
+
function normalizeUrl(input) {
|
|
425
|
+
const trimmed = input.trim().replace(/\/+$/, "");
|
|
426
|
+
if (!/^https?:\/\//i.test(trimmed)) throw new Error("URL must start with http:// or https://");
|
|
427
|
+
return trimmed;
|
|
428
|
+
}
|
|
429
|
+
function originOnly(input) {
|
|
430
|
+
const parsed = new URL(input);
|
|
431
|
+
parsed.username = "";
|
|
432
|
+
parsed.password = "";
|
|
433
|
+
return parsed.origin;
|
|
434
|
+
}
|
|
435
|
+
function localUrl(port) {
|
|
436
|
+
return `http://localhost:${port}`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/core/config.ts
|
|
441
|
+
const ENV_URL = "METABASE_URL";
|
|
442
|
+
const ENV_API_KEY = "METABASE_API_KEY";
|
|
443
|
+
const ENV_PROFILE = "METABASE_PROFILE";
|
|
444
|
+
const ENV_LICENSE_TOKEN = "METABASE_LICENSE_TOKEN";
|
|
445
|
+
function resolveProfileName(profileFlag$1) {
|
|
446
|
+
return explicitProfileName(profileFlag$1) ?? DEFAULT_PROFILE;
|
|
447
|
+
}
|
|
448
|
+
function explicitProfileName(profileFlag$1) {
|
|
449
|
+
return profileFlag$1 || process.env[ENV_PROFILE] || null;
|
|
450
|
+
}
|
|
451
|
+
function readEnvCredentials() {
|
|
452
|
+
return {
|
|
453
|
+
url: process.env[ENV_URL] ?? null,
|
|
454
|
+
apiKey: process.env[ENV_API_KEY] ?? null
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function readEnvLicenseToken() {
|
|
458
|
+
return process.env[ENV_LICENSE_TOKEN] ?? null;
|
|
459
|
+
}
|
|
460
|
+
async function resolveConfig(flags) {
|
|
461
|
+
const profile = resolveProfileName(flags.profile);
|
|
462
|
+
const env = readEnvCredentials();
|
|
463
|
+
const flagUrl = flags.url;
|
|
464
|
+
const flagKey = flags.apiKey;
|
|
465
|
+
const needsStored = !flagUrl && !env.url || !flagKey && !env.apiKey;
|
|
466
|
+
const stored = needsStored ? await readProfile(profile) : null;
|
|
467
|
+
const urlField = pickField(flagUrl, env.url, stored?.url);
|
|
468
|
+
const keyField = pickField(flagKey, env.apiKey, stored?.apiKey);
|
|
469
|
+
if (urlField === null || keyField === null) {
|
|
470
|
+
const rejection = await readRejection(profile);
|
|
471
|
+
if (rejection !== null) throw new ConfigError(`Last login for profile "${profile}" was rejected by ${originOnly(rejection.url)}: ${rejection.reason}. Re-run \`metabase auth login --profile ${profile}\` with valid credentials.`);
|
|
472
|
+
throw new ConfigError(`Not authenticated for profile "${profile}". Run \`metabase auth login\`, set ${ENV_URL}/${ENV_API_KEY}, or pass --url/--api-key.`);
|
|
473
|
+
}
|
|
474
|
+
return {
|
|
475
|
+
url: normalizeUrl(urlField.value),
|
|
476
|
+
apiKey: keyField.value,
|
|
477
|
+
profile,
|
|
478
|
+
source: urlField.source === keyField.source ? urlField.source : "mixed"
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
async function resolveLicenseToken(flags) {
|
|
482
|
+
const flag = flags.token;
|
|
483
|
+
const env = readEnvLicenseToken();
|
|
484
|
+
const stored = !flag && !env ? await readLicense() : null;
|
|
485
|
+
const value = flag ?? env ?? stored;
|
|
486
|
+
if (!value) throw new ConfigError(`No license token. Pass --token, set ${ENV_LICENSE_TOKEN}, or store one with \`metabase license set\`.`);
|
|
487
|
+
return value;
|
|
488
|
+
}
|
|
489
|
+
function pickField(flag, env, stored) {
|
|
490
|
+
if (flag) return {
|
|
491
|
+
value: flag,
|
|
492
|
+
source: "flag"
|
|
493
|
+
};
|
|
494
|
+
if (env) return {
|
|
495
|
+
value: env,
|
|
496
|
+
source: "env"
|
|
497
|
+
};
|
|
498
|
+
if (stored) return {
|
|
499
|
+
value: stored,
|
|
500
|
+
source: "stored"
|
|
501
|
+
};
|
|
502
|
+
return null;
|
|
503
|
+
}
|
|
504
|
+
|
|
298
505
|
//#endregion
|
|
299
506
|
//#region src/runtime/signal.ts
|
|
300
507
|
function createProcessAbortHandler() {
|
|
@@ -394,8 +601,13 @@ const STATUS_CLASSIFICATIONS = {
|
|
|
394
601
|
const ErrorEnvelope = z.object({
|
|
395
602
|
message: z.string().optional(),
|
|
396
603
|
error: z.string().optional(),
|
|
397
|
-
"error-message": z.string().optional()
|
|
398
|
-
})
|
|
604
|
+
"error-message": z.string().optional(),
|
|
605
|
+
via: z.array(z.object({ message: z.string().optional() }).loose()).optional(),
|
|
606
|
+
"specific-errors": z.unknown().optional(),
|
|
607
|
+
errors: z.unknown().optional()
|
|
608
|
+
}).loose();
|
|
609
|
+
const MAX_EXTRACTED_MESSAGE_LEN = 500;
|
|
610
|
+
const ELLIPSIS = "…";
|
|
399
611
|
var HttpError = class extends MetabaseError {
|
|
400
612
|
category = "http";
|
|
401
613
|
exitCode = 1;
|
|
@@ -437,7 +649,46 @@ function parseEnvelopeMessage(sanitizedBody) {
|
|
|
437
649
|
const result = parseJsonResult(sanitizedBody, ErrorEnvelope);
|
|
438
650
|
if (!result.ok) return null;
|
|
439
651
|
const envelope = result.value;
|
|
440
|
-
|
|
652
|
+
const topLevel = envelope.message ?? envelope.error ?? envelope["error-message"];
|
|
653
|
+
if (topLevel) return capLength(topLevel);
|
|
654
|
+
const viaMessage = envelope.via?.find((entry) => entry.message)?.message;
|
|
655
|
+
if (viaMessage) return capLength(viaMessage);
|
|
656
|
+
const specific = formatErrorTree(envelope["specific-errors"]);
|
|
657
|
+
if (specific) return capLength(specific);
|
|
658
|
+
const generic = formatErrorTree(envelope.errors);
|
|
659
|
+
if (generic) return capLength(generic);
|
|
660
|
+
return null;
|
|
661
|
+
}
|
|
662
|
+
function formatErrorTree(value) {
|
|
663
|
+
const entries = collectLeafEntries(value, []);
|
|
664
|
+
if (entries.length === 0) return null;
|
|
665
|
+
return entries.map(formatLeafEntry).join("; ");
|
|
666
|
+
}
|
|
667
|
+
function formatLeafEntry(entry) {
|
|
668
|
+
return entry.path === "" ? entry.message : `${entry.path}: ${entry.message}`;
|
|
669
|
+
}
|
|
670
|
+
function collectLeafEntries(value, path) {
|
|
671
|
+
if (typeof value === "string") {
|
|
672
|
+
const trimmed = value.trim();
|
|
673
|
+
return trimmed === "" ? [] : [{
|
|
674
|
+
path: path.join("."),
|
|
675
|
+
message: trimmed
|
|
676
|
+
}];
|
|
677
|
+
}
|
|
678
|
+
if (Array.isArray(value)) {
|
|
679
|
+
const messages = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
680
|
+
if (messages.length === 0) return [];
|
|
681
|
+
return [{
|
|
682
|
+
path: path.join("."),
|
|
683
|
+
message: messages.join("; ")
|
|
684
|
+
}];
|
|
685
|
+
}
|
|
686
|
+
if (isPlainObject(value)) return Object.entries(value).flatMap(([key, child]) => collectLeafEntries(child, [...path, key]));
|
|
687
|
+
return [];
|
|
688
|
+
}
|
|
689
|
+
function capLength(message) {
|
|
690
|
+
if (message.length <= MAX_EXTRACTED_MESSAGE_LEN) return message;
|
|
691
|
+
return message.slice(0, MAX_EXTRACTED_MESSAGE_LEN - ELLIPSIS.length) + ELLIPSIS;
|
|
441
692
|
}
|
|
442
693
|
function defaultMessageForStatus(status) {
|
|
443
694
|
return STATUS_CLASSIFICATIONS[status]?.message ?? `Metabase returned ${status}`;
|
|
@@ -470,6 +721,7 @@ function sleep(ms, signal) {
|
|
|
470
721
|
//#region src/core/http/client.ts
|
|
471
722
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
472
723
|
const JSON_CONTENT_TYPE = "application/json";
|
|
724
|
+
const OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
|
|
473
725
|
const TEXT_CONTENT_TYPE_PREFIX = "text/";
|
|
474
726
|
const ERROR_BODY_BYTE_CAP = 64 * 1024;
|
|
475
727
|
const USER_AGENT = `metabase-cli/${package_default.version}`;
|
|
@@ -562,7 +814,10 @@ function createClient(config, overrides = {}) {
|
|
|
562
814
|
let body = null;
|
|
563
815
|
if (opts.body !== void 0 && opts.body !== null) if (typeof opts.body === "string" || opts.body instanceof URLSearchParams) body = opts.body;
|
|
564
816
|
else if (opts.body instanceof FormData || opts.body instanceof ReadableStream) body = opts.body;
|
|
565
|
-
else {
|
|
817
|
+
else if (opts.body instanceof Uint8Array) {
|
|
818
|
+
body = opts.body;
|
|
819
|
+
headers.set("content-type", OCTET_STREAM_CONTENT_TYPE);
|
|
820
|
+
} else {
|
|
566
821
|
body = JSON.stringify(opts.body);
|
|
567
822
|
headers.set("content-type", JSON_CONTENT_TYPE);
|
|
568
823
|
}
|
|
@@ -588,9 +843,9 @@ function createClient(config, overrides = {}) {
|
|
|
588
843
|
expectContentType: "json"
|
|
589
844
|
});
|
|
590
845
|
const response = await executeRaw(prepared);
|
|
591
|
-
const text
|
|
846
|
+
const text = await response.text();
|
|
592
847
|
try {
|
|
593
|
-
return parseJson(text
|
|
848
|
+
return parseJson(text, schema, { source: prepared.url });
|
|
594
849
|
} catch (error) {
|
|
595
850
|
if (error instanceof ConfigError) throw new HttpError({
|
|
596
851
|
status: response.status,
|
|
@@ -598,7 +853,7 @@ function createClient(config, overrides = {}) {
|
|
|
598
853
|
method: prepared.method,
|
|
599
854
|
url: prepared.url,
|
|
600
855
|
responseHeaders: response.headers,
|
|
601
|
-
rawBody: text
|
|
856
|
+
rawBody: text,
|
|
602
857
|
redactionContext
|
|
603
858
|
});
|
|
604
859
|
throw error;
|
|
@@ -662,318 +917,12 @@ async function readBodyForError(response) {
|
|
|
662
917
|
}
|
|
663
918
|
}
|
|
664
919
|
|
|
665
|
-
//#endregion
|
|
666
|
-
//#region src/core/url.ts
|
|
667
|
-
function normalizeUrl(input) {
|
|
668
|
-
const trimmed = input.trim().replace(/\/+$/, "");
|
|
669
|
-
if (!/^https?:\/\//i.test(trimmed)) throw new Error("URL must start with http:// or https://");
|
|
670
|
-
return trimmed;
|
|
671
|
-
}
|
|
672
|
-
function originOnly(input) {
|
|
673
|
-
const parsed = new URL(input);
|
|
674
|
-
parsed.username = "";
|
|
675
|
-
parsed.password = "";
|
|
676
|
-
return parsed.origin;
|
|
677
|
-
}
|
|
678
|
-
|
|
679
|
-
//#endregion
|
|
680
|
-
//#region src/core/config.ts
|
|
681
|
-
const ENV_URL = "METABASE_URL";
|
|
682
|
-
const ENV_API_KEY = "METABASE_API_KEY";
|
|
683
|
-
const ENV_PROFILE = "METABASE_PROFILE";
|
|
684
|
-
const ENV_LICENSE_TOKEN = "METABASE_LICENSE_TOKEN";
|
|
685
|
-
function resolveProfileName(profileFlag$1) {
|
|
686
|
-
return profileFlag$1 || process.env[ENV_PROFILE] || DEFAULT_PROFILE;
|
|
687
|
-
}
|
|
688
|
-
function readEnvCredentials() {
|
|
689
|
-
return {
|
|
690
|
-
url: process.env[ENV_URL] ?? null,
|
|
691
|
-
apiKey: process.env[ENV_API_KEY] ?? null
|
|
692
|
-
};
|
|
693
|
-
}
|
|
694
|
-
function readEnvLicenseToken() {
|
|
695
|
-
return process.env[ENV_LICENSE_TOKEN] ?? null;
|
|
696
|
-
}
|
|
697
|
-
async function resolveConfig(flags) {
|
|
698
|
-
const profile = resolveProfileName(flags.profile);
|
|
699
|
-
const env = readEnvCredentials();
|
|
700
|
-
const flagUrl = flags.url;
|
|
701
|
-
const flagKey = flags.apiKey;
|
|
702
|
-
const needsStored = !flagUrl && !env.url || !flagKey && !env.apiKey;
|
|
703
|
-
const stored = needsStored ? await readProfile(profile) : null;
|
|
704
|
-
const urlField = pickField(flagUrl, env.url, stored?.url);
|
|
705
|
-
const keyField = pickField(flagKey, env.apiKey, stored?.apiKey);
|
|
706
|
-
if (urlField === null || keyField === null) throw new ConfigError(`Not authenticated for profile "${profile}". Run \`metabase auth login\`, set ${ENV_URL}/${ENV_API_KEY}, or pass --url/--api-key.`);
|
|
707
|
-
return {
|
|
708
|
-
url: normalizeUrl(urlField.value),
|
|
709
|
-
apiKey: keyField.value,
|
|
710
|
-
profile,
|
|
711
|
-
source: urlField.source === keyField.source ? urlField.source : "mixed"
|
|
712
|
-
};
|
|
713
|
-
}
|
|
714
|
-
function pickField(flag, env, stored) {
|
|
715
|
-
if (flag) return {
|
|
716
|
-
value: flag,
|
|
717
|
-
source: "flag"
|
|
718
|
-
};
|
|
719
|
-
if (env) return {
|
|
720
|
-
value: env,
|
|
721
|
-
source: "env"
|
|
722
|
-
};
|
|
723
|
-
if (stored) return {
|
|
724
|
-
value: stored,
|
|
725
|
-
source: "stored"
|
|
726
|
-
};
|
|
727
|
-
return null;
|
|
728
|
-
}
|
|
729
|
-
|
|
730
|
-
//#endregion
|
|
731
|
-
//#region src/output/notice.ts
|
|
732
|
-
function warn(message) {
|
|
733
|
-
process.stderr.write(message + "\n");
|
|
734
|
-
}
|
|
735
|
-
function listTruncationNotice(bytes) {
|
|
736
|
-
return `… cut at ${bytes} bytes; rerun with --max-bytes 0`;
|
|
737
|
-
}
|
|
738
|
-
function itemOversizeNotice(bytes) {
|
|
739
|
-
return `… item is ${bytes} bytes (exceeds --max-bytes); narrow with --fields, or pass --max-bytes 0`;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
//#endregion
|
|
743
|
-
//#region src/output/cap.ts
|
|
744
|
-
function capListEnvelope(envelope, maxBytes) {
|
|
745
|
-
if (maxBytes <= 0) return envelope;
|
|
746
|
-
const fullBytes = jsonByteLength(envelope);
|
|
747
|
-
if (fullBytes <= maxBytes) return envelope;
|
|
748
|
-
let lo = 0;
|
|
749
|
-
let hi = envelope.data.length;
|
|
750
|
-
while (lo < hi) {
|
|
751
|
-
const mid = Math.ceil((lo + hi) / 2);
|
|
752
|
-
if (jsonByteLength(truncate(envelope, mid, fullBytes)) <= maxBytes) lo = mid;
|
|
753
|
-
else hi = mid - 1;
|
|
754
|
-
}
|
|
755
|
-
return truncate(envelope, lo, fullBytes);
|
|
756
|
-
}
|
|
757
|
-
function truncate(envelope, count, originalBytes) {
|
|
758
|
-
return {
|
|
759
|
-
...envelope,
|
|
760
|
-
data: envelope.data.slice(0, count),
|
|
761
|
-
returned: count,
|
|
762
|
-
truncated: {
|
|
763
|
-
reason: "max_bytes",
|
|
764
|
-
bytes: originalBytes
|
|
765
|
-
}
|
|
766
|
-
};
|
|
767
|
-
}
|
|
768
|
-
function jsonByteLength(value) {
|
|
769
|
-
return Buffer.byteLength(JSON.stringify(value), "utf8");
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
//#endregion
|
|
773
|
-
//#region src/output/projection.ts
|
|
774
|
-
function applyProjection(value, view, full, fields) {
|
|
775
|
-
if (fields !== void 0) {
|
|
776
|
-
if (fields.length === 0) throw new ConfigError("--fields requires at least one path");
|
|
777
|
-
return projectFields(value, fields);
|
|
778
|
-
}
|
|
779
|
-
if (full) return value;
|
|
780
|
-
const parsed = view.compactPick.safeParse(value);
|
|
781
|
-
if (parsed.success) return parsed.data;
|
|
782
|
-
throw new ConfigError(`compact projection failed: ${parsed.error.message}`);
|
|
783
|
-
}
|
|
784
|
-
function projectFields(value, fields) {
|
|
785
|
-
const out = {};
|
|
786
|
-
for (const path of fields) {
|
|
787
|
-
if (path.length === 0) throw new ConfigError(`empty field path`);
|
|
788
|
-
const parts = path.split(".");
|
|
789
|
-
if (parts.some((part) => part.length === 0)) throw new ConfigError(`invalid field path: "${path}"`);
|
|
790
|
-
setPath(out, parts, pickPath(value, parts));
|
|
791
|
-
}
|
|
792
|
-
return out;
|
|
793
|
-
}
|
|
794
|
-
function pickPath(value, parts) {
|
|
795
|
-
let cursor = value;
|
|
796
|
-
for (const part of parts) {
|
|
797
|
-
if (!isPlainObject(cursor) || !Object.hasOwn(cursor, part)) throw new ConfigError(`unknown field path: "${parts.join(".")}"`);
|
|
798
|
-
cursor = Reflect.get(cursor, part);
|
|
799
|
-
}
|
|
800
|
-
return cursor;
|
|
801
|
-
}
|
|
802
|
-
function setPath(target, parts, value) {
|
|
803
|
-
let cursor = target;
|
|
804
|
-
const lastIndex = parts.length - 1;
|
|
805
|
-
for (const [index, part] of parts.entries()) {
|
|
806
|
-
if (index === lastIndex) {
|
|
807
|
-
cursor[part] = value;
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
const existing = cursor[part];
|
|
811
|
-
if (isPlainObject(existing)) cursor = existing;
|
|
812
|
-
else {
|
|
813
|
-
const next = {};
|
|
814
|
-
cursor[part] = next;
|
|
815
|
-
cursor = next;
|
|
816
|
-
}
|
|
817
|
-
}
|
|
818
|
-
}
|
|
819
|
-
function isPlainObject(value) {
|
|
820
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
821
|
-
}
|
|
822
|
-
|
|
823
|
-
//#endregion
|
|
824
|
-
//#region src/output/table.ts
|
|
825
|
-
function renderTable(rows, columns) {
|
|
826
|
-
const head = columns.map((column) => column.label ?? column.key);
|
|
827
|
-
const widths = columns.map((column) => column.width ?? null);
|
|
828
|
-
const hasWidth = widths.some((width) => width !== null);
|
|
829
|
-
const table = new Table(hasWidth ? {
|
|
830
|
-
head,
|
|
831
|
-
colWidths: widths
|
|
832
|
-
} : { head });
|
|
833
|
-
for (const row of rows) table.push(columns.map((column) => formatCell(row, column)));
|
|
834
|
-
return table.toString();
|
|
835
|
-
}
|
|
836
|
-
function formatCell(row, column) {
|
|
837
|
-
const value = row[column.key];
|
|
838
|
-
if (column.format !== void 0) return column.format(value);
|
|
839
|
-
return formatScalar(value);
|
|
840
|
-
}
|
|
841
|
-
function formatScalar(value) {
|
|
842
|
-
if (value === null || value === void 0) return "";
|
|
843
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return String(value);
|
|
844
|
-
return JSON.stringify(value);
|
|
845
|
-
}
|
|
846
|
-
|
|
847
|
-
//#endregion
|
|
848
|
-
//#region src/output/render.ts
|
|
849
|
-
function renderItem(item, view, opts) {
|
|
850
|
-
const projected = applyProjection(item, view, opts.full, opts.fields);
|
|
851
|
-
const body = renderItemBody(item, view, projected, opts) + "\n";
|
|
852
|
-
process.stdout.write(body);
|
|
853
|
-
emitItemOversizeNotice(body, opts.maxBytes);
|
|
854
|
-
}
|
|
855
|
-
function renderList(envelope, view, opts) {
|
|
856
|
-
if (opts.format === "json" || opts.fields !== void 0) {
|
|
857
|
-
renderJsonEnvelope(envelope, view, opts);
|
|
858
|
-
return;
|
|
859
|
-
}
|
|
860
|
-
if (envelope.data.length === 0) {
|
|
861
|
-
process.stdout.write("(no results)\n");
|
|
862
|
-
return;
|
|
863
|
-
}
|
|
864
|
-
const capped = capListEnvelope(envelope, opts.maxBytes);
|
|
865
|
-
process.stdout.write(renderTable(capped.data, view.tableColumns) + "\n");
|
|
866
|
-
if (capped.truncated !== void 0) warn(listTruncationNotice(capped.truncated.bytes));
|
|
867
|
-
}
|
|
868
|
-
function renderJsonEnvelope(envelope, view, opts) {
|
|
869
|
-
const projectedItems = envelope.data.map((item) => applyProjection(item, view, opts.full, opts.fields));
|
|
870
|
-
const projectedEnvelope = {
|
|
871
|
-
...envelope,
|
|
872
|
-
data: projectedItems
|
|
873
|
-
};
|
|
874
|
-
const capped = capListEnvelope(projectedEnvelope, opts.maxBytes);
|
|
875
|
-
process.stdout.write(JSON.stringify(capped, null, 2) + "\n");
|
|
876
|
-
if (capped.truncated !== void 0) warn(listTruncationNotice(capped.truncated.bytes));
|
|
877
|
-
}
|
|
878
|
-
function renderItemBody(item, view, projected, opts) {
|
|
879
|
-
if (opts.format === "json" || opts.fields !== void 0) return JSON.stringify(projected, null, 2);
|
|
880
|
-
if (!opts.full) return renderKeyValueLines(columnPairs(item, view.tableColumns));
|
|
881
|
-
return renderKeyValueLines(objectPairs(projected));
|
|
882
|
-
}
|
|
883
|
-
function columnPairs(item, columns) {
|
|
884
|
-
return columns.map((column) => [column.label ?? column.key, formatCell(item, column)]);
|
|
885
|
-
}
|
|
886
|
-
function objectPairs(value) {
|
|
887
|
-
if (!isPlainObject(value)) {
|
|
888
|
-
const scalar = formatScalar(value);
|
|
889
|
-
return scalar === "" ? [] : [["", scalar]];
|
|
890
|
-
}
|
|
891
|
-
return Object.entries(value).map(([key, raw]) => [key, formatScalar(raw)]);
|
|
892
|
-
}
|
|
893
|
-
function renderKeyValueLines(pairs) {
|
|
894
|
-
if (pairs.length === 0) return "";
|
|
895
|
-
const padding = Math.max(...pairs.map(([label]) => label.length));
|
|
896
|
-
return pairs.map(([label, value]) => `${label.padEnd(padding)} ${value}`).join("\n");
|
|
897
|
-
}
|
|
898
|
-
function emitItemOversizeNotice(body, maxBytes) {
|
|
899
|
-
if (maxBytes <= 0) return;
|
|
900
|
-
const bytes = Buffer.byteLength(body, "utf8");
|
|
901
|
-
if (bytes <= maxBytes) return;
|
|
902
|
-
warn(itemOversizeNotice(bytes));
|
|
903
|
-
}
|
|
904
|
-
|
|
905
|
-
//#endregion
|
|
906
|
-
//#region src/output/types.ts
|
|
907
|
-
const DEFAULT_MAX_BYTES = 65536;
|
|
908
|
-
function listEnvelopeSchema(item) {
|
|
909
|
-
return z.object({
|
|
910
|
-
data: z.array(item),
|
|
911
|
-
returned: z.number().int().nonnegative(),
|
|
912
|
-
total: z.number().int().nonnegative().optional(),
|
|
913
|
-
limit: z.number().int().nonnegative().optional(),
|
|
914
|
-
truncated: z.object({
|
|
915
|
-
reason: z.literal("max_bytes"),
|
|
916
|
-
bytes: z.number().int().nonnegative()
|
|
917
|
-
}).optional()
|
|
918
|
-
});
|
|
919
|
-
}
|
|
920
|
-
function wrapList(items) {
|
|
921
|
-
return {
|
|
922
|
-
data: items,
|
|
923
|
-
returned: items.length,
|
|
924
|
-
total: items.length
|
|
925
|
-
};
|
|
926
|
-
}
|
|
927
|
-
|
|
928
|
-
//#endregion
|
|
929
|
-
//#region src/commands/flags.ts
|
|
930
|
-
const outputFlags = {
|
|
931
|
-
format: {
|
|
932
|
-
type: "string",
|
|
933
|
-
description: "auto | json | text",
|
|
934
|
-
default: "auto"
|
|
935
|
-
},
|
|
936
|
-
json: {
|
|
937
|
-
type: "boolean",
|
|
938
|
-
description: "Shorthand for --format json"
|
|
939
|
-
},
|
|
940
|
-
full: {
|
|
941
|
-
type: "boolean",
|
|
942
|
-
description: "Return the full object (default: compact)"
|
|
943
|
-
},
|
|
944
|
-
fields: {
|
|
945
|
-
type: "string",
|
|
946
|
-
description: "Dot-paths, comma separated (mutually exclusive with --full)"
|
|
947
|
-
},
|
|
948
|
-
maxBytes: {
|
|
949
|
-
type: "string",
|
|
950
|
-
description: "Output size cap; 0 disables",
|
|
951
|
-
default: String(DEFAULT_MAX_BYTES),
|
|
952
|
-
alias: "max-bytes"
|
|
953
|
-
}
|
|
954
|
-
};
|
|
955
|
-
const profileFlag = { profile: {
|
|
956
|
-
type: "string",
|
|
957
|
-
description: "Named profile (default: 'default')"
|
|
958
|
-
} };
|
|
959
|
-
const connectionFlags = {
|
|
960
|
-
url: {
|
|
961
|
-
type: "string",
|
|
962
|
-
description: "Metabase URL"
|
|
963
|
-
},
|
|
964
|
-
apiKey: {
|
|
965
|
-
type: "string",
|
|
966
|
-
description: "API key",
|
|
967
|
-
alias: "api-key"
|
|
968
|
-
}
|
|
969
|
-
};
|
|
970
|
-
|
|
971
920
|
//#endregion
|
|
972
921
|
//#region src/output/error.ts
|
|
973
922
|
function reportError(error) {
|
|
974
923
|
const handled = toMetabaseError(error);
|
|
975
924
|
process.stderr.write(handled.userMessage + "\n");
|
|
976
|
-
if (process.env[
|
|
925
|
+
if (process.env[VERBOSE_ENV] === "1" && handled.developerDetail !== null) process.stderr.write(JSON.stringify(handled.developerDetail, null, 2) + "\n");
|
|
977
926
|
process.exitCode = handled.exitCode;
|
|
978
927
|
}
|
|
979
928
|
|
|
@@ -988,9 +937,40 @@ function resolveFormat({ json, format, isTty }) {
|
|
|
988
937
|
return isTty ? "text" : "json";
|
|
989
938
|
}
|
|
990
939
|
|
|
940
|
+
//#endregion
|
|
941
|
+
//#region src/runtime/csv.ts
|
|
942
|
+
function parseCsv(raw) {
|
|
943
|
+
return raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
944
|
+
}
|
|
945
|
+
function parseEnumCsv(raw, schema, flagName) {
|
|
946
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
947
|
+
const parts = parseCsv(raw);
|
|
948
|
+
if (parts.length === 0) return void 0;
|
|
949
|
+
const accepted = [];
|
|
950
|
+
const rejected = [];
|
|
951
|
+
for (const part of parts) {
|
|
952
|
+
const result = schema.safeParse(part);
|
|
953
|
+
if (result.success) accepted.push(result.data);
|
|
954
|
+
else rejected.push(part);
|
|
955
|
+
}
|
|
956
|
+
if (rejected.length > 0) {
|
|
957
|
+
const allowed = Object.values(schema.enum).join(", ");
|
|
958
|
+
throw new ConfigError(`invalid ${flagName} value: ${rejected.join(", ")} (expected one of: ${allowed})`);
|
|
959
|
+
}
|
|
960
|
+
return accepted;
|
|
961
|
+
}
|
|
962
|
+
function parseEnum(raw, schema, flagName) {
|
|
963
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
964
|
+
const result = schema.safeParse(raw);
|
|
965
|
+
if (!result.success) {
|
|
966
|
+
const allowed = Object.values(schema.enum).join(", ");
|
|
967
|
+
throw new ConfigError(`invalid ${flagName} value: "${raw}" (expected one of: ${allowed})`);
|
|
968
|
+
}
|
|
969
|
+
return result.data;
|
|
970
|
+
}
|
|
971
|
+
|
|
991
972
|
//#endregion
|
|
992
973
|
//#region src/commands/context.ts
|
|
993
|
-
const INTEGER_PATTERN = /^-?\d+$/;
|
|
994
974
|
function resolveCommonFlags(args, options = {}) {
|
|
995
975
|
const isTty = options.isTty ?? Boolean(process.stdout.isTTY);
|
|
996
976
|
const fields = parseFields(args.fields);
|
|
@@ -1012,15 +992,14 @@ function resolveCommonFlags(args, options = {}) {
|
|
|
1012
992
|
}
|
|
1013
993
|
function parseFields(value) {
|
|
1014
994
|
if (value === void 0 || value === "") return void 0;
|
|
1015
|
-
const parts = value
|
|
995
|
+
const parts = parseCsv(value);
|
|
1016
996
|
return parts.length > 0 ? parts : void 0;
|
|
1017
997
|
}
|
|
1018
998
|
function parseMaxBytes(value) {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
return parsed;
|
|
999
|
+
return parseInteger(value ?? String(DEFAULT_MAX_BYTES), {
|
|
1000
|
+
name: "--max-bytes",
|
|
1001
|
+
min: 0
|
|
1002
|
+
});
|
|
1024
1003
|
}
|
|
1025
1004
|
|
|
1026
1005
|
//#endregion
|
|
@@ -1032,20 +1011,27 @@ function defineMetabaseCommand(def) {
|
|
|
1032
1011
|
async run({ args }) {
|
|
1033
1012
|
try {
|
|
1034
1013
|
const ctx = resolveCommonFlags(pickCommonArgs(args));
|
|
1035
|
-
let
|
|
1014
|
+
let cachedConfig = null;
|
|
1015
|
+
let cachedClient = null;
|
|
1016
|
+
const getResolvedConfig = async () => {
|
|
1017
|
+
if (cachedConfig === null) cachedConfig = await resolveConfig(buildConfigFlags(ctx));
|
|
1018
|
+
return cachedConfig;
|
|
1019
|
+
};
|
|
1036
1020
|
const getClient = async () => {
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1021
|
+
if (cachedClient === null) {
|
|
1022
|
+
const resolved = await getResolvedConfig();
|
|
1023
|
+
cachedClient = createClient({
|
|
1024
|
+
url: resolved.url,
|
|
1025
|
+
apiKey: resolved.apiKey
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return cachedClient;
|
|
1044
1029
|
};
|
|
1045
1030
|
await def.run({
|
|
1046
1031
|
args,
|
|
1047
1032
|
ctx,
|
|
1048
|
-
getClient
|
|
1033
|
+
getClient,
|
|
1034
|
+
getResolvedConfig
|
|
1049
1035
|
});
|
|
1050
1036
|
} catch (error) {
|
|
1051
1037
|
reportError(error);
|
|
@@ -1079,4 +1065,4 @@ function buildConfigFlags(ctx) {
|
|
|
1079
1065
|
}
|
|
1080
1066
|
|
|
1081
1067
|
//#endregion
|
|
1082
|
-
export {
|
|
1068
|
+
export { DEFAULT_PROFILE, HttpError, account, clearLicense, clearProfile, clearRejection, combineAborts, connectionFlags, createClient, credentials, defineMetabaseCommand, explicitProfileName, listEnvelopeSchema, listProfileNames, localUrl, normalizeUrl, originOnly, outputFlags, parseCsv, parseEnum, parseEnumCsv, parseInteger, parseJson, parseJsonOrPlain, parseOptionalInteger, profileFlag, readEnvCredentials, readEnvLicenseToken, readProfile, recordRejection, resolveLicenseToken, resolveProfileName, throwIfAborted, wrapList, writeLicense, writeProfile };
|