@metabase/cli 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +972 -57
- package/dist/add-collection--zwkmE1S.mjs +11 -0
- package/dist/add-collection-B1qe0D1U.mjs +54 -0
- package/dist/api-key-gzCbKDjL.mjs +13 -0
- package/dist/archive-CitmlD1e.mjs +39 -0
- package/dist/{archive-CsWeHXle.mjs → archive-CnhWegtR.mjs} +7 -4
- package/dist/archive-DQjBOXnx.mjs +44 -0
- package/dist/archive-Ni8-lQ1Y.mjs +44 -0
- package/dist/auth-BPjsrFxM.mjs +19 -0
- package/dist/{body-Dv9hQ0Qk.mjs → body-DRBgxS6-.mjs} +3 -2
- package/dist/{branches-BujtceGr.mjs → branches-C5Jcw8wu.mjs} +8 -6
- package/dist/cancel-Ca3r7Y6v.mjs +56 -0
- package/dist/{cancel-task-CT2xUMRg.mjs → cancel-task-C1-8vDKS.mjs} +9 -7
- package/dist/card-BGAy3eIb.mjs +20 -0
- package/dist/{card-CsXk8T6A.mjs → card-CAEZWixN.mjs} +34 -15
- package/dist/cards-CILfMPUP.mjs +37 -0
- package/dist/cli.mjs +33 -14
- package/dist/collection-B3sPXRLs.mjs +163 -0
- package/dist/collection-D8cnCB98.mjs +19 -0
- package/dist/create-3Z6rm-4O.mjs +44 -0
- package/dist/create-BsY5RrVY.mjs +44 -0
- package/dist/create-C4OCclBD.mjs +48 -0
- package/dist/create-COsD7Vzm.mjs +48 -0
- package/dist/create-CP8ou91U.mjs +125 -0
- package/dist/create-CeIi_QLj.mjs +66 -0
- package/dist/create-CqNw6PmR.mjs +50 -0
- package/dist/create-DE_5NrFy.mjs +48 -0
- package/dist/{create-B8ektf-R.mjs → create-MEhhhgMC.mjs} +8 -6
- package/dist/create-QxDmleKJ.mjs +48 -0
- package/dist/{create-branch-goZBTNnr.mjs → create-branch-CKMYaAHk.mjs} +9 -7
- package/dist/credentials-CwRKvdP2.mjs +85 -0
- package/dist/{current-task-DBjRNCFq.mjs → current-task-Dutjys16.mjs} +9 -7
- package/dist/dashboard-B4fVp392.mjs +20 -0
- package/dist/dashboard-CnMD04PQ.mjs +163 -0
- package/dist/database-BMTb0CzV.mjs +17 -0
- package/dist/database-Dvkfy3JM.mjs +51 -0
- package/dist/db-ACuuaEok.mjs +22 -0
- package/dist/{delete-8vGU35r3.mjs → delete-BMQZuVXZ.mjs} +7 -5
- package/dist/{delete-B27KLF5X.mjs → delete-BvcA4jPj.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-DUPjHKk4.mjs} +7 -5
- package/dist/deprovision-Bsc1S15j.mjs +61 -0
- package/dist/{dirty-aNUuph4I.mjs → dirty-CXcdoUhY.mjs} +8 -6
- package/dist/docker-D-ieBsP7.mjs +612 -0
- package/dist/eid-pvOsEMPZ.mjs +13 -0
- package/dist/{export-QDkuuzSE.mjs → export-BjGhLEOi.mjs} +30 -23
- package/dist/field-BI2bt8e9.mjs +18 -0
- package/dist/field-DciLbuv-.mjs +276 -0
- package/dist/fields-Do8HHm_T.mjs +38 -0
- package/dist/flag-pair-DtR1AiBQ.mjs +17 -0
- package/dist/{get-BGBIzMKY.mjs → get-BGFGWkH0.mjs} +6 -4
- package/dist/get-BmE_VHdl.mjs +36 -0
- package/dist/{get-DI_IJvgk.mjs → get-C7sshmqF.mjs} +6 -4
- package/dist/get-CObKBj2J.mjs +36 -0
- package/dist/get-Cq5U_Eep.mjs +40 -0
- package/dist/get-D4GUJBiX.mjs +41 -0
- package/dist/{get-COXHplHP.mjs → get-DFrsi77F.mjs} +7 -5
- package/dist/get-DczxeETg.mjs +53 -0
- package/dist/{get-Cl8-IauC.mjs → get-DeQa3ThJ.mjs} +7 -4
- package/dist/get-DhZ_dGUb.mjs +36 -0
- package/dist/{get-i6LWOByV.mjs → get-DzCVafyO.mjs} +6 -4
- package/dist/get-YCnVqq-z.mjs +49 -0
- package/dist/get-run-CTyW29s3.mjs +36 -0
- package/dist/git-sync-BOmT8HEU.mjs +28 -0
- package/dist/{has-remote-changes-hjKoQuRy.mjs → has-remote-changes-xX8vMVsX.mjs} +8 -6
- package/dist/{import-HJsSKRYx.mjs → import-CaAUNtXz.mjs} +11 -9
- package/dist/{input-Dojr-RTw.mjs → input-ikCiip6x.mjs} +2 -1
- package/dist/is-dirty-CPu-xqkW.mjs +10 -0
- package/dist/{is-dirty-1Qy7hiHB.mjs → is-dirty-mgxEwEk4.mjs} +5 -4
- package/dist/items-Cg67tdto.mjs +77 -0
- package/dist/{key-DBxPSFwi.mjs → key-NDEARu2L.mjs} +1 -1
- package/dist/{license-MoWse3ZI.mjs → license-CwKzVMD0.mjs} +3 -3
- package/dist/list-BqdNQ1nU.mjs +47 -0
- package/dist/list-BwGdD45N.mjs +32 -0
- package/dist/list-CfOVsAZz.mjs +55 -0
- package/dist/list-CpyNn1Zn.mjs +32 -0
- package/dist/list-CwwOoGLK.mjs +40 -0
- package/dist/{list-C_PRdL5e.mjs → list-DD8CQx8l.mjs} +7 -5
- package/dist/{list-Bk6RsbJl.mjs → list-DL-RWpIE.mjs} +5 -3
- package/dist/list-DLlq3FyS.mjs +61 -0
- package/dist/list-DdQ4jmUQ.mjs +52 -0
- package/dist/{list-C4Ajrw8f.mjs → list-DshbLoqR.mjs} +6 -3
- package/dist/{list-C8tdLOH5.mjs → list-DzTMpoBs.mjs} +5 -3
- package/dist/list-JgRtCzz3.mjs +32 -0
- package/dist/{list-CWt3fqrZ.mjs → list-WzgJcwB5.mjs} +5 -3
- package/dist/{login-C9WTwNn6.mjs → login-DJnmR2wX.mjs} +14 -5
- package/dist/{logout-oLszGCOg.mjs → logout-BMe_1Zp8.mjs} +7 -6
- package/dist/logs-CQxKJ3HG.mjs +58 -0
- package/dist/{manifest-CAdjQYH8.mjs → manifest-Dv5B9Blc.mjs} +3 -7
- package/dist/measure-BEQfnLdN.mjs +67 -0
- package/dist/measure-BGyYbtqO.mjs +19 -0
- package/dist/metadata-CLIALntn.mjs +37 -0
- package/dist/metadata-T-fNUWg_.mjs +38 -0
- package/dist/{package-BGfw4ZWJ.mjs → package-DBsS7a5x.mjs} +7 -1
- package/dist/paginate-CTSfuYiF.mjs +49 -0
- package/dist/parse-id-BUOZQqjp.mjs +12 -0
- package/dist/parse-ref-DGvh4aDn.mjs +17 -0
- package/dist/parse-schemas-BnW4T1_I.mjs +12 -0
- package/dist/{poll-ILanYysl.mjs → poll-DMmmZWvi.mjs} +2 -1
- package/dist/{poll-task-DbpsiQhl.mjs → poll-task-2Ckiwp8U.mjs} +8 -7
- package/dist/predicates-DiIiS3k7.mjs +153 -0
- package/dist/preflight-CC_g6EWU.mjs +91 -0
- package/dist/{prompt-DpT8yAVy.mjs → prompt-Bf3DQ-qE.mjs} +1 -1
- package/dist/provision-BUgWJWAV.mjs +77 -0
- package/dist/ps-BUNHygf-.mjs +10 -0
- package/dist/ps-Yv0JjLVN.mjs +78 -0
- package/dist/{query-PihYi-UZ.mjs → query-CzfbuG8a.mjs} +38 -13
- package/dist/query-UIebHmbT.mjs +90 -0
- package/dist/remove-BAUbcwuF.mjs +98 -0
- package/dist/{remove-B2hVYn1v.mjs → remove-CN2PNGTR.mjs} +6 -5
- package/dist/remove-collection-C6NxEh53.mjs +38 -0
- package/dist/render-DXv-D6fU.mjs +182 -0
- package/dist/rescan-values-CcB4F9qa.mjs +43 -0
- package/dist/revision-message-flag-CWQbKhdl.mjs +11 -0
- package/dist/{run-C2so6Qp6.mjs → run-BjXZtu_6.mjs} +27 -36
- package/dist/runs-CXx7l1NY.mjs +54 -0
- package/dist/{runtime-C9CEZhcn.mjs → runtime-D7jihh81.mjs} +425 -442
- package/dist/schema-tables-BCJT2DM_.mjs +45 -0
- package/dist/schemas-DlNpbn4H.mjs +47 -0
- package/dist/{search-CopOytXY.mjs → search-Dt-6mdHZ.mjs} +6 -19
- package/dist/segment-BMrUBz94.mjs +70 -0
- package/dist/segment-C52QNnSs.mjs +19 -0
- package/dist/{set-BcF7M1GQ.mjs → set-DCESWpi3.mjs} +6 -4
- package/dist/{set-CbibegpA.mjs → set-L7cuHjVZ.mjs} +8 -6
- package/dist/{setting-U3NtBMFo.mjs → setting-DysGAuYS.mjs} +3 -3
- package/dist/setup-_ypJDPAY.mjs +71 -0
- package/dist/snippet-Dw0Sjzkr.mjs +64 -0
- package/dist/snippet-vb3G9R8a.mjs +19 -0
- package/dist/start-BokXnb0V.mjs +350 -0
- package/dist/{stash-DOBbYozC.mjs → stash-CaGX6PfX.mjs} +9 -7
- package/dist/{status-Buf1ZbNR.mjs → status-BaX9vedb.mjs} +10 -8
- package/dist/{status-CUcs8XBH.mjs → status-CyecXzN4.mjs} +4 -2
- package/dist/{status-D1F5XHae.mjs → status-RpVyPEty.mjs} +4 -2
- package/dist/stop-BRuF_Cg1.mjs +81 -0
- package/dist/summary-CpEOiOlZ.mjs +41 -0
- package/dist/sync-schema-4Cl4h8Jn.mjs +43 -0
- package/dist/table-BeMWuvzO.mjs +19 -0
- package/dist/{table-Cfk7oSvw.mjs → table-jljEqZ0R.mjs} +22 -9
- package/dist/transform-DwRc-w6y.mjs +24 -0
- package/dist/{transform-B5uRpg1G.mjs → transform-IEX4Mx3X.mjs} +56 -2
- package/dist/transform-job-BigWrctt.mjs +19 -0
- package/dist/{transform-job-C7QXWTVE.mjs → transform-job-Csr86muI.mjs} +7 -0
- package/dist/translate-DqLlXXUx.mjs +111 -0
- package/dist/tree-BT24nkLM.mjs +32 -0
- package/dist/update-BCXKQi2n.mjs +52 -0
- package/dist/{update-CL8tRbxr.mjs → update-BXbLmC2b.mjs} +9 -7
- package/dist/update-C1Frz9GR.mjs +52 -0
- package/dist/update-C5goGhNr.mjs +56 -0
- package/dist/update-CCOyB0iT.mjs +73 -0
- package/dist/update-D04NMueX.mjs +59 -0
- package/dist/update-D6WVtNV1.mjs +57 -0
- package/dist/update-DFR46LsB.mjs +56 -0
- package/dist/update-DyLItrpV.mjs +56 -0
- package/dist/update-dashcard-av0_PYeg.mjs +71 -0
- package/dist/update-mrgvQF4i.mjs +51 -0
- package/dist/url-x4wn_l3k.mjs +54 -0
- package/dist/uuid-BZHbti8B.mjs +47 -0
- package/dist/validate-DCYx6jdL.mjs +1496 -0
- package/dist/validate-query-B07oGG4K.mjs +37 -0
- package/dist/values-Be6i0Fs9.mjs +36 -0
- package/dist/{wait-Bugr9eXD.mjs → wait-BMqQD8k_.mjs} +10 -8
- package/dist/wait-CWizX_sR.mjs +19 -0
- package/dist/wait-flags-DO3ar2tf.mjs +35 -0
- package/dist/workspace-CG1xyJ86.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-DBsS7a5x.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,86 @@ 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 profileFlag$1 || process.env[ENV_PROFILE] || DEFAULT_PROFILE;
|
|
447
|
+
}
|
|
448
|
+
function readEnvCredentials() {
|
|
449
|
+
return {
|
|
450
|
+
url: process.env[ENV_URL] ?? null,
|
|
451
|
+
apiKey: process.env[ENV_API_KEY] ?? null
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
function readEnvLicenseToken() {
|
|
455
|
+
return process.env[ENV_LICENSE_TOKEN] ?? null;
|
|
456
|
+
}
|
|
457
|
+
async function resolveConfig(flags) {
|
|
458
|
+
const profile = resolveProfileName(flags.profile);
|
|
459
|
+
const env = readEnvCredentials();
|
|
460
|
+
const flagUrl = flags.url;
|
|
461
|
+
const flagKey = flags.apiKey;
|
|
462
|
+
const needsStored = !flagUrl && !env.url || !flagKey && !env.apiKey;
|
|
463
|
+
const stored = needsStored ? await readProfile(profile) : null;
|
|
464
|
+
const urlField = pickField(flagUrl, env.url, stored?.url);
|
|
465
|
+
const keyField = pickField(flagKey, env.apiKey, stored?.apiKey);
|
|
466
|
+
if (urlField === null || keyField === null) {
|
|
467
|
+
const rejection = await readRejection(profile);
|
|
468
|
+
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.`);
|
|
469
|
+
throw new ConfigError(`Not authenticated for profile "${profile}". Run \`metabase auth login\`, set ${ENV_URL}/${ENV_API_KEY}, or pass --url/--api-key.`);
|
|
470
|
+
}
|
|
471
|
+
return {
|
|
472
|
+
url: normalizeUrl(urlField.value),
|
|
473
|
+
apiKey: keyField.value,
|
|
474
|
+
profile,
|
|
475
|
+
source: urlField.source === keyField.source ? urlField.source : "mixed"
|
|
476
|
+
};
|
|
477
|
+
}
|
|
478
|
+
async function resolveLicenseToken(flags) {
|
|
479
|
+
const flag = flags.token;
|
|
480
|
+
const env = readEnvLicenseToken();
|
|
481
|
+
const stored = !flag && !env ? await readLicense() : null;
|
|
482
|
+
const value = flag ?? env ?? stored;
|
|
483
|
+
if (!value) throw new ConfigError(`No license token. Pass --token, set ${ENV_LICENSE_TOKEN}, or store one with \`metabase license set\`.`);
|
|
484
|
+
return value;
|
|
485
|
+
}
|
|
486
|
+
function pickField(flag, env, stored) {
|
|
487
|
+
if (flag) return {
|
|
488
|
+
value: flag,
|
|
489
|
+
source: "flag"
|
|
490
|
+
};
|
|
491
|
+
if (env) return {
|
|
492
|
+
value: env,
|
|
493
|
+
source: "env"
|
|
494
|
+
};
|
|
495
|
+
if (stored) return {
|
|
496
|
+
value: stored,
|
|
497
|
+
source: "stored"
|
|
498
|
+
};
|
|
499
|
+
return null;
|
|
500
|
+
}
|
|
501
|
+
|
|
298
502
|
//#endregion
|
|
299
503
|
//#region src/runtime/signal.ts
|
|
300
504
|
function createProcessAbortHandler() {
|
|
@@ -394,8 +598,13 @@ const STATUS_CLASSIFICATIONS = {
|
|
|
394
598
|
const ErrorEnvelope = z.object({
|
|
395
599
|
message: z.string().optional(),
|
|
396
600
|
error: z.string().optional(),
|
|
397
|
-
"error-message": z.string().optional()
|
|
398
|
-
})
|
|
601
|
+
"error-message": z.string().optional(),
|
|
602
|
+
via: z.array(z.object({ message: z.string().optional() }).loose()).optional(),
|
|
603
|
+
"specific-errors": z.unknown().optional(),
|
|
604
|
+
errors: z.unknown().optional()
|
|
605
|
+
}).loose();
|
|
606
|
+
const MAX_EXTRACTED_MESSAGE_LEN = 500;
|
|
607
|
+
const ELLIPSIS = "…";
|
|
399
608
|
var HttpError = class extends MetabaseError {
|
|
400
609
|
category = "http";
|
|
401
610
|
exitCode = 1;
|
|
@@ -437,7 +646,46 @@ function parseEnvelopeMessage(sanitizedBody) {
|
|
|
437
646
|
const result = parseJsonResult(sanitizedBody, ErrorEnvelope);
|
|
438
647
|
if (!result.ok) return null;
|
|
439
648
|
const envelope = result.value;
|
|
440
|
-
|
|
649
|
+
const topLevel = envelope.message ?? envelope.error ?? envelope["error-message"];
|
|
650
|
+
if (topLevel) return capLength(topLevel);
|
|
651
|
+
const viaMessage = envelope.via?.find((entry) => entry.message)?.message;
|
|
652
|
+
if (viaMessage) return capLength(viaMessage);
|
|
653
|
+
const specific = formatErrorTree(envelope["specific-errors"]);
|
|
654
|
+
if (specific) return capLength(specific);
|
|
655
|
+
const generic = formatErrorTree(envelope.errors);
|
|
656
|
+
if (generic) return capLength(generic);
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
function formatErrorTree(value) {
|
|
660
|
+
const entries = collectLeafEntries(value, []);
|
|
661
|
+
if (entries.length === 0) return null;
|
|
662
|
+
return entries.map(formatLeafEntry).join("; ");
|
|
663
|
+
}
|
|
664
|
+
function formatLeafEntry(entry) {
|
|
665
|
+
return entry.path === "" ? entry.message : `${entry.path}: ${entry.message}`;
|
|
666
|
+
}
|
|
667
|
+
function collectLeafEntries(value, path) {
|
|
668
|
+
if (typeof value === "string") {
|
|
669
|
+
const trimmed = value.trim();
|
|
670
|
+
return trimmed === "" ? [] : [{
|
|
671
|
+
path: path.join("."),
|
|
672
|
+
message: trimmed
|
|
673
|
+
}];
|
|
674
|
+
}
|
|
675
|
+
if (Array.isArray(value)) {
|
|
676
|
+
const messages = value.filter((entry) => typeof entry === "string" && entry.trim() !== "");
|
|
677
|
+
if (messages.length === 0) return [];
|
|
678
|
+
return [{
|
|
679
|
+
path: path.join("."),
|
|
680
|
+
message: messages.join("; ")
|
|
681
|
+
}];
|
|
682
|
+
}
|
|
683
|
+
if (isPlainObject(value)) return Object.entries(value).flatMap(([key, child]) => collectLeafEntries(child, [...path, key]));
|
|
684
|
+
return [];
|
|
685
|
+
}
|
|
686
|
+
function capLength(message) {
|
|
687
|
+
if (message.length <= MAX_EXTRACTED_MESSAGE_LEN) return message;
|
|
688
|
+
return message.slice(0, MAX_EXTRACTED_MESSAGE_LEN - ELLIPSIS.length) + ELLIPSIS;
|
|
441
689
|
}
|
|
442
690
|
function defaultMessageForStatus(status) {
|
|
443
691
|
return STATUS_CLASSIFICATIONS[status]?.message ?? `Metabase returned ${status}`;
|
|
@@ -470,6 +718,7 @@ function sleep(ms, signal) {
|
|
|
470
718
|
//#region src/core/http/client.ts
|
|
471
719
|
const DEFAULT_TIMEOUT_MS = 3e4;
|
|
472
720
|
const JSON_CONTENT_TYPE = "application/json";
|
|
721
|
+
const OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
|
|
473
722
|
const TEXT_CONTENT_TYPE_PREFIX = "text/";
|
|
474
723
|
const ERROR_BODY_BYTE_CAP = 64 * 1024;
|
|
475
724
|
const USER_AGENT = `metabase-cli/${package_default.version}`;
|
|
@@ -562,7 +811,10 @@ function createClient(config, overrides = {}) {
|
|
|
562
811
|
let body = null;
|
|
563
812
|
if (opts.body !== void 0 && opts.body !== null) if (typeof opts.body === "string" || opts.body instanceof URLSearchParams) body = opts.body;
|
|
564
813
|
else if (opts.body instanceof FormData || opts.body instanceof ReadableStream) body = opts.body;
|
|
565
|
-
else {
|
|
814
|
+
else if (opts.body instanceof Uint8Array) {
|
|
815
|
+
body = opts.body;
|
|
816
|
+
headers.set("content-type", OCTET_STREAM_CONTENT_TYPE);
|
|
817
|
+
} else {
|
|
566
818
|
body = JSON.stringify(opts.body);
|
|
567
819
|
headers.set("content-type", JSON_CONTENT_TYPE);
|
|
568
820
|
}
|
|
@@ -588,9 +840,9 @@ function createClient(config, overrides = {}) {
|
|
|
588
840
|
expectContentType: "json"
|
|
589
841
|
});
|
|
590
842
|
const response = await executeRaw(prepared);
|
|
591
|
-
const text
|
|
843
|
+
const text = await response.text();
|
|
592
844
|
try {
|
|
593
|
-
return parseJson(text
|
|
845
|
+
return parseJson(text, schema, { source: prepared.url });
|
|
594
846
|
} catch (error) {
|
|
595
847
|
if (error instanceof ConfigError) throw new HttpError({
|
|
596
848
|
status: response.status,
|
|
@@ -598,7 +850,7 @@ function createClient(config, overrides = {}) {
|
|
|
598
850
|
method: prepared.method,
|
|
599
851
|
url: prepared.url,
|
|
600
852
|
responseHeaders: response.headers,
|
|
601
|
-
rawBody: text
|
|
853
|
+
rawBody: text,
|
|
602
854
|
redactionContext
|
|
603
855
|
});
|
|
604
856
|
throw error;
|
|
@@ -662,318 +914,12 @@ async function readBodyForError(response) {
|
|
|
662
914
|
}
|
|
663
915
|
}
|
|
664
916
|
|
|
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
917
|
//#endregion
|
|
972
918
|
//#region src/output/error.ts
|
|
973
919
|
function reportError(error) {
|
|
974
920
|
const handled = toMetabaseError(error);
|
|
975
921
|
process.stderr.write(handled.userMessage + "\n");
|
|
976
|
-
if (process.env[
|
|
922
|
+
if (process.env[VERBOSE_ENV] === "1" && handled.developerDetail !== null) process.stderr.write(JSON.stringify(handled.developerDetail, null, 2) + "\n");
|
|
977
923
|
process.exitCode = handled.exitCode;
|
|
978
924
|
}
|
|
979
925
|
|
|
@@ -988,9 +934,40 @@ function resolveFormat({ json, format, isTty }) {
|
|
|
988
934
|
return isTty ? "text" : "json";
|
|
989
935
|
}
|
|
990
936
|
|
|
937
|
+
//#endregion
|
|
938
|
+
//#region src/runtime/csv.ts
|
|
939
|
+
function parseCsv(raw) {
|
|
940
|
+
return raw.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
|
|
941
|
+
}
|
|
942
|
+
function parseEnumCsv(raw, schema, flagName) {
|
|
943
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
944
|
+
const parts = parseCsv(raw);
|
|
945
|
+
if (parts.length === 0) return void 0;
|
|
946
|
+
const accepted = [];
|
|
947
|
+
const rejected = [];
|
|
948
|
+
for (const part of parts) {
|
|
949
|
+
const result = schema.safeParse(part);
|
|
950
|
+
if (result.success) accepted.push(result.data);
|
|
951
|
+
else rejected.push(part);
|
|
952
|
+
}
|
|
953
|
+
if (rejected.length > 0) {
|
|
954
|
+
const allowed = Object.values(schema.enum).join(", ");
|
|
955
|
+
throw new ConfigError(`invalid ${flagName} value: ${rejected.join(", ")} (expected one of: ${allowed})`);
|
|
956
|
+
}
|
|
957
|
+
return accepted;
|
|
958
|
+
}
|
|
959
|
+
function parseEnum(raw, schema, flagName) {
|
|
960
|
+
if (raw === void 0 || raw === "") return void 0;
|
|
961
|
+
const result = schema.safeParse(raw);
|
|
962
|
+
if (!result.success) {
|
|
963
|
+
const allowed = Object.values(schema.enum).join(", ");
|
|
964
|
+
throw new ConfigError(`invalid ${flagName} value: "${raw}" (expected one of: ${allowed})`);
|
|
965
|
+
}
|
|
966
|
+
return result.data;
|
|
967
|
+
}
|
|
968
|
+
|
|
991
969
|
//#endregion
|
|
992
970
|
//#region src/commands/context.ts
|
|
993
|
-
const INTEGER_PATTERN = /^-?\d+$/;
|
|
994
971
|
function resolveCommonFlags(args, options = {}) {
|
|
995
972
|
const isTty = options.isTty ?? Boolean(process.stdout.isTTY);
|
|
996
973
|
const fields = parseFields(args.fields);
|
|
@@ -1012,15 +989,14 @@ function resolveCommonFlags(args, options = {}) {
|
|
|
1012
989
|
}
|
|
1013
990
|
function parseFields(value) {
|
|
1014
991
|
if (value === void 0 || value === "") return void 0;
|
|
1015
|
-
const parts = value
|
|
992
|
+
const parts = parseCsv(value);
|
|
1016
993
|
return parts.length > 0 ? parts : void 0;
|
|
1017
994
|
}
|
|
1018
995
|
function parseMaxBytes(value) {
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
return parsed;
|
|
996
|
+
return parseInteger(value ?? String(DEFAULT_MAX_BYTES), {
|
|
997
|
+
name: "--max-bytes",
|
|
998
|
+
min: 0
|
|
999
|
+
});
|
|
1024
1000
|
}
|
|
1025
1001
|
|
|
1026
1002
|
//#endregion
|
|
@@ -1032,20 +1008,27 @@ function defineMetabaseCommand(def) {
|
|
|
1032
1008
|
async run({ args }) {
|
|
1033
1009
|
try {
|
|
1034
1010
|
const ctx = resolveCommonFlags(pickCommonArgs(args));
|
|
1035
|
-
let
|
|
1011
|
+
let cachedConfig = null;
|
|
1012
|
+
let cachedClient = null;
|
|
1013
|
+
const getResolvedConfig = async () => {
|
|
1014
|
+
if (cachedConfig === null) cachedConfig = await resolveConfig(buildConfigFlags(ctx));
|
|
1015
|
+
return cachedConfig;
|
|
1016
|
+
};
|
|
1036
1017
|
const getClient = async () => {
|
|
1037
|
-
if (
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1018
|
+
if (cachedClient === null) {
|
|
1019
|
+
const resolved = await getResolvedConfig();
|
|
1020
|
+
cachedClient = createClient({
|
|
1021
|
+
url: resolved.url,
|
|
1022
|
+
apiKey: resolved.apiKey
|
|
1023
|
+
});
|
|
1024
|
+
}
|
|
1025
|
+
return cachedClient;
|
|
1044
1026
|
};
|
|
1045
1027
|
await def.run({
|
|
1046
1028
|
args,
|
|
1047
1029
|
ctx,
|
|
1048
|
-
getClient
|
|
1030
|
+
getClient,
|
|
1031
|
+
getResolvedConfig
|
|
1049
1032
|
});
|
|
1050
1033
|
} catch (error) {
|
|
1051
1034
|
reportError(error);
|
|
@@ -1079,4 +1062,4 @@ function buildConfigFlags(ctx) {
|
|
|
1079
1062
|
}
|
|
1080
1063
|
|
|
1081
1064
|
//#endregion
|
|
1082
|
-
export {
|
|
1065
|
+
export { HttpError, account, clearLicense, clearProfile, clearRejection, combineAborts, connectionFlags, createClient, credentials, defineMetabaseCommand, listEnvelopeSchema, listProfileNames, localUrl, normalizeUrl, originOnly, outputFlags, parseCsv, parseEnum, parseEnumCsv, parseInteger, parseJson, parseJsonOrPlain, parseOptionalInteger, profileFlag, readEnvCredentials, readEnvLicenseToken, readProfile, recordRejection, resolveLicenseToken, resolveProfileName, throwIfAborted, wrapList, writeLicense, writeProfile };
|