@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.
Files changed (187) hide show
  1. package/README.md +972 -57
  2. package/dist/add-collection--zwkmE1S.mjs +11 -0
  3. package/dist/add-collection-B1qe0D1U.mjs +54 -0
  4. package/dist/api-key-gzCbKDjL.mjs +13 -0
  5. package/dist/archive-CitmlD1e.mjs +39 -0
  6. package/dist/{archive-CsWeHXle.mjs → archive-CnhWegtR.mjs} +7 -4
  7. package/dist/archive-DQjBOXnx.mjs +44 -0
  8. package/dist/archive-Ni8-lQ1Y.mjs +44 -0
  9. package/dist/auth-BPjsrFxM.mjs +19 -0
  10. package/dist/{body-Dv9hQ0Qk.mjs → body-DRBgxS6-.mjs} +3 -2
  11. package/dist/{branches-BujtceGr.mjs → branches-C5Jcw8wu.mjs} +8 -6
  12. package/dist/cancel-Ca3r7Y6v.mjs +56 -0
  13. package/dist/{cancel-task-CT2xUMRg.mjs → cancel-task-C1-8vDKS.mjs} +9 -7
  14. package/dist/card-BGAy3eIb.mjs +20 -0
  15. package/dist/{card-CsXk8T6A.mjs → card-CAEZWixN.mjs} +34 -15
  16. package/dist/cards-CILfMPUP.mjs +37 -0
  17. package/dist/cli.mjs +33 -14
  18. package/dist/collection-B3sPXRLs.mjs +163 -0
  19. package/dist/collection-D8cnCB98.mjs +19 -0
  20. package/dist/create-3Z6rm-4O.mjs +44 -0
  21. package/dist/create-BsY5RrVY.mjs +44 -0
  22. package/dist/create-C4OCclBD.mjs +48 -0
  23. package/dist/create-COsD7Vzm.mjs +48 -0
  24. package/dist/create-CP8ou91U.mjs +125 -0
  25. package/dist/create-CeIi_QLj.mjs +66 -0
  26. package/dist/create-CqNw6PmR.mjs +50 -0
  27. package/dist/create-DE_5NrFy.mjs +48 -0
  28. package/dist/{create-B8ektf-R.mjs → create-MEhhhgMC.mjs} +8 -6
  29. package/dist/create-QxDmleKJ.mjs +48 -0
  30. package/dist/{create-branch-goZBTNnr.mjs → create-branch-CKMYaAHk.mjs} +9 -7
  31. package/dist/credentials-CwRKvdP2.mjs +85 -0
  32. package/dist/{current-task-DBjRNCFq.mjs → current-task-Dutjys16.mjs} +9 -7
  33. package/dist/dashboard-B4fVp392.mjs +20 -0
  34. package/dist/dashboard-CnMD04PQ.mjs +163 -0
  35. package/dist/database-BMTb0CzV.mjs +17 -0
  36. package/dist/database-Dvkfy3JM.mjs +51 -0
  37. package/dist/db-ACuuaEok.mjs +22 -0
  38. package/dist/{delete-8vGU35r3.mjs → delete-BMQZuVXZ.mjs} +7 -5
  39. package/dist/{delete-B27KLF5X.mjs → delete-BvcA4jPj.mjs} +7 -5
  40. package/dist/{delete-runtime-Byr60cR3.mjs → delete-runtime-BMzvfj_B.mjs} +4 -4
  41. package/dist/{delete-table-BNaJ_gA4.mjs → delete-table-DUPjHKk4.mjs} +7 -5
  42. package/dist/deprovision-Bsc1S15j.mjs +61 -0
  43. package/dist/{dirty-aNUuph4I.mjs → dirty-CXcdoUhY.mjs} +8 -6
  44. package/dist/docker-D-ieBsP7.mjs +612 -0
  45. package/dist/eid-pvOsEMPZ.mjs +13 -0
  46. package/dist/{export-QDkuuzSE.mjs → export-BjGhLEOi.mjs} +30 -23
  47. package/dist/field-BI2bt8e9.mjs +18 -0
  48. package/dist/field-DciLbuv-.mjs +276 -0
  49. package/dist/fields-Do8HHm_T.mjs +38 -0
  50. package/dist/flag-pair-DtR1AiBQ.mjs +17 -0
  51. package/dist/{get-BGBIzMKY.mjs → get-BGFGWkH0.mjs} +6 -4
  52. package/dist/get-BmE_VHdl.mjs +36 -0
  53. package/dist/{get-DI_IJvgk.mjs → get-C7sshmqF.mjs} +6 -4
  54. package/dist/get-CObKBj2J.mjs +36 -0
  55. package/dist/get-Cq5U_Eep.mjs +40 -0
  56. package/dist/get-D4GUJBiX.mjs +41 -0
  57. package/dist/{get-COXHplHP.mjs → get-DFrsi77F.mjs} +7 -5
  58. package/dist/get-DczxeETg.mjs +53 -0
  59. package/dist/{get-Cl8-IauC.mjs → get-DeQa3ThJ.mjs} +7 -4
  60. package/dist/get-DhZ_dGUb.mjs +36 -0
  61. package/dist/{get-i6LWOByV.mjs → get-DzCVafyO.mjs} +6 -4
  62. package/dist/get-YCnVqq-z.mjs +49 -0
  63. package/dist/get-run-CTyW29s3.mjs +36 -0
  64. package/dist/git-sync-BOmT8HEU.mjs +28 -0
  65. package/dist/{has-remote-changes-hjKoQuRy.mjs → has-remote-changes-xX8vMVsX.mjs} +8 -6
  66. package/dist/{import-HJsSKRYx.mjs → import-CaAUNtXz.mjs} +11 -9
  67. package/dist/{input-Dojr-RTw.mjs → input-ikCiip6x.mjs} +2 -1
  68. package/dist/is-dirty-CPu-xqkW.mjs +10 -0
  69. package/dist/{is-dirty-1Qy7hiHB.mjs → is-dirty-mgxEwEk4.mjs} +5 -4
  70. package/dist/items-Cg67tdto.mjs +77 -0
  71. package/dist/{key-DBxPSFwi.mjs → key-NDEARu2L.mjs} +1 -1
  72. package/dist/{license-MoWse3ZI.mjs → license-CwKzVMD0.mjs} +3 -3
  73. package/dist/list-BqdNQ1nU.mjs +47 -0
  74. package/dist/list-BwGdD45N.mjs +32 -0
  75. package/dist/list-CfOVsAZz.mjs +55 -0
  76. package/dist/list-CpyNn1Zn.mjs +32 -0
  77. package/dist/list-CwwOoGLK.mjs +40 -0
  78. package/dist/{list-C_PRdL5e.mjs → list-DD8CQx8l.mjs} +7 -5
  79. package/dist/{list-Bk6RsbJl.mjs → list-DL-RWpIE.mjs} +5 -3
  80. package/dist/list-DLlq3FyS.mjs +61 -0
  81. package/dist/list-DdQ4jmUQ.mjs +52 -0
  82. package/dist/{list-C4Ajrw8f.mjs → list-DshbLoqR.mjs} +6 -3
  83. package/dist/{list-C8tdLOH5.mjs → list-DzTMpoBs.mjs} +5 -3
  84. package/dist/list-JgRtCzz3.mjs +32 -0
  85. package/dist/{list-CWt3fqrZ.mjs → list-WzgJcwB5.mjs} +5 -3
  86. package/dist/{login-C9WTwNn6.mjs → login-DJnmR2wX.mjs} +14 -5
  87. package/dist/{logout-oLszGCOg.mjs → logout-BMe_1Zp8.mjs} +7 -6
  88. package/dist/logs-CQxKJ3HG.mjs +58 -0
  89. package/dist/{manifest-CAdjQYH8.mjs → manifest-Dv5B9Blc.mjs} +3 -7
  90. package/dist/measure-BEQfnLdN.mjs +67 -0
  91. package/dist/measure-BGyYbtqO.mjs +19 -0
  92. package/dist/metadata-CLIALntn.mjs +37 -0
  93. package/dist/metadata-T-fNUWg_.mjs +38 -0
  94. package/dist/{package-BGfw4ZWJ.mjs → package-DBsS7a5x.mjs} +7 -1
  95. package/dist/paginate-CTSfuYiF.mjs +49 -0
  96. package/dist/parse-id-BUOZQqjp.mjs +12 -0
  97. package/dist/parse-ref-DGvh4aDn.mjs +17 -0
  98. package/dist/parse-schemas-BnW4T1_I.mjs +12 -0
  99. package/dist/{poll-ILanYysl.mjs → poll-DMmmZWvi.mjs} +2 -1
  100. package/dist/{poll-task-DbpsiQhl.mjs → poll-task-2Ckiwp8U.mjs} +8 -7
  101. package/dist/predicates-DiIiS3k7.mjs +153 -0
  102. package/dist/preflight-CC_g6EWU.mjs +91 -0
  103. package/dist/{prompt-DpT8yAVy.mjs → prompt-Bf3DQ-qE.mjs} +1 -1
  104. package/dist/provision-BUgWJWAV.mjs +77 -0
  105. package/dist/ps-BUNHygf-.mjs +10 -0
  106. package/dist/ps-Yv0JjLVN.mjs +78 -0
  107. package/dist/{query-PihYi-UZ.mjs → query-CzfbuG8a.mjs} +38 -13
  108. package/dist/query-UIebHmbT.mjs +90 -0
  109. package/dist/remove-BAUbcwuF.mjs +98 -0
  110. package/dist/{remove-B2hVYn1v.mjs → remove-CN2PNGTR.mjs} +6 -5
  111. package/dist/remove-collection-C6NxEh53.mjs +38 -0
  112. package/dist/render-DXv-D6fU.mjs +182 -0
  113. package/dist/rescan-values-CcB4F9qa.mjs +43 -0
  114. package/dist/revision-message-flag-CWQbKhdl.mjs +11 -0
  115. package/dist/{run-C2so6Qp6.mjs → run-BjXZtu_6.mjs} +27 -36
  116. package/dist/runs-CXx7l1NY.mjs +54 -0
  117. package/dist/{runtime-C9CEZhcn.mjs → runtime-D7jihh81.mjs} +425 -442
  118. package/dist/schema-tables-BCJT2DM_.mjs +45 -0
  119. package/dist/schemas-DlNpbn4H.mjs +47 -0
  120. package/dist/{search-CopOytXY.mjs → search-Dt-6mdHZ.mjs} +6 -19
  121. package/dist/segment-BMrUBz94.mjs +70 -0
  122. package/dist/segment-C52QNnSs.mjs +19 -0
  123. package/dist/{set-BcF7M1GQ.mjs → set-DCESWpi3.mjs} +6 -4
  124. package/dist/{set-CbibegpA.mjs → set-L7cuHjVZ.mjs} +8 -6
  125. package/dist/{setting-U3NtBMFo.mjs → setting-DysGAuYS.mjs} +3 -3
  126. package/dist/setup-_ypJDPAY.mjs +71 -0
  127. package/dist/snippet-Dw0Sjzkr.mjs +64 -0
  128. package/dist/snippet-vb3G9R8a.mjs +19 -0
  129. package/dist/start-BokXnb0V.mjs +350 -0
  130. package/dist/{stash-DOBbYozC.mjs → stash-CaGX6PfX.mjs} +9 -7
  131. package/dist/{status-Buf1ZbNR.mjs → status-BaX9vedb.mjs} +10 -8
  132. package/dist/{status-CUcs8XBH.mjs → status-CyecXzN4.mjs} +4 -2
  133. package/dist/{status-D1F5XHae.mjs → status-RpVyPEty.mjs} +4 -2
  134. package/dist/stop-BRuF_Cg1.mjs +81 -0
  135. package/dist/summary-CpEOiOlZ.mjs +41 -0
  136. package/dist/sync-schema-4Cl4h8Jn.mjs +43 -0
  137. package/dist/table-BeMWuvzO.mjs +19 -0
  138. package/dist/{table-Cfk7oSvw.mjs → table-jljEqZ0R.mjs} +22 -9
  139. package/dist/transform-DwRc-w6y.mjs +24 -0
  140. package/dist/{transform-B5uRpg1G.mjs → transform-IEX4Mx3X.mjs} +56 -2
  141. package/dist/transform-job-BigWrctt.mjs +19 -0
  142. package/dist/{transform-job-C7QXWTVE.mjs → transform-job-Csr86muI.mjs} +7 -0
  143. package/dist/translate-DqLlXXUx.mjs +111 -0
  144. package/dist/tree-BT24nkLM.mjs +32 -0
  145. package/dist/update-BCXKQi2n.mjs +52 -0
  146. package/dist/{update-CL8tRbxr.mjs → update-BXbLmC2b.mjs} +9 -7
  147. package/dist/update-C1Frz9GR.mjs +52 -0
  148. package/dist/update-C5goGhNr.mjs +56 -0
  149. package/dist/update-CCOyB0iT.mjs +73 -0
  150. package/dist/update-D04NMueX.mjs +59 -0
  151. package/dist/update-D6WVtNV1.mjs +57 -0
  152. package/dist/update-DFR46LsB.mjs +56 -0
  153. package/dist/update-DyLItrpV.mjs +56 -0
  154. package/dist/update-dashcard-av0_PYeg.mjs +71 -0
  155. package/dist/update-mrgvQF4i.mjs +51 -0
  156. package/dist/url-x4wn_l3k.mjs +54 -0
  157. package/dist/uuid-BZHbti8B.mjs +47 -0
  158. package/dist/validate-DCYx6jdL.mjs +1496 -0
  159. package/dist/validate-query-B07oGG4K.mjs +37 -0
  160. package/dist/values-Be6i0Fs9.mjs +36 -0
  161. package/dist/{wait-Bugr9eXD.mjs → wait-BMqQD8k_.mjs} +10 -8
  162. package/dist/wait-CWizX_sR.mjs +19 -0
  163. package/dist/wait-flags-DO3ar2tf.mjs +35 -0
  164. package/dist/workspace-CG1xyJ86.mjs +24 -0
  165. package/dist/workspace-DVuqKJGG.mjs +72 -0
  166. package/dist/workspace-credentials-B6BL-X0d.mjs +139 -0
  167. package/package.json +7 -1
  168. package/dist/auth-BF7IjZIH.mjs +0 -18
  169. package/dist/card-_Ta7zdYe.mjs +0 -19
  170. package/dist/create-CI2Cunq5.mjs +0 -38
  171. package/dist/create-DdbU3TLX.mjs +0 -42
  172. package/dist/database-PA9Goi25.mjs +0 -33
  173. package/dist/db-DMghzgb6.mjs +0 -17
  174. package/dist/field-C8IVs6rp.mjs +0 -76
  175. package/dist/field-DaYo_90x.mjs +0 -13
  176. package/dist/get-Cwpj7lDe.mjs +0 -35
  177. package/dist/get-Dh_acl8q.mjs +0 -34
  178. package/dist/is-dirty-DpKn9HJp.mjs +0 -8
  179. package/dist/list-CBSBHtK-.mjs +0 -38
  180. package/dist/parse-id-BhmmfyCP.mjs +0 -14
  181. package/dist/sync-BPyGXfUk.mjs +0 -26
  182. package/dist/table-D7nJt7JO.mjs +0 -16
  183. package/dist/transform-UbyewMxY.mjs +0 -21
  184. package/dist/transform-job-CrYkr-Ma.mjs +0 -19
  185. package/dist/update-DU2oU2j-.mjs +0 -49
  186. /package/dist/{body-flags-BUA9XV1u.mjs → body-flags-BK7J6Daz.mjs} +0 -0
  187. /package/dist/{setting-26ckqHAP.mjs → setting-CTaAeMci.mjs} +0 -0
@@ -1,113 +1,16 @@
1
- import { package_default } from "./package-BGfw4ZWJ.mjs";
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 { ZodError, z } from "zod";
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
- function configDir() {
155
- if (process.platform === "win32") {
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
- return credentials.set(account.profileApiKey(name), profile.apiKey);
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
- return envelope.message ?? envelope.error ?? envelope["error-message"] ?? null;
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$1 = await response.text();
843
+ const text = await response.text();
592
844
  try {
593
- return parseJson(text$1, schema, { source: prepared.url });
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$1,
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["METABASE_VERBOSE"] === "1" && handled.developerDetail !== null) process.stderr.write(JSON.stringify(handled.developerDetail, null, 2) + "\n");
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.split(",").map((part) => part.trim()).filter((part) => part.length > 0);
992
+ const parts = parseCsv(value);
1016
993
  return parts.length > 0 ? parts : void 0;
1017
994
  }
1018
995
  function parseMaxBytes(value) {
1019
- const raw = value ?? String(DEFAULT_MAX_BYTES);
1020
- if (!INTEGER_PATTERN.test(raw)) throw new ConfigError(`invalid --max-bytes value: "${raw}" (expected non-negative integer)`);
1021
- const parsed = Number.parseInt(raw, 10);
1022
- if (parsed < 0) throw new ConfigError(`invalid --max-bytes value: ${parsed} (must be non-negative)`);
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 cached = null;
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 (cached) return cached;
1038
- const resolved = await resolveConfig(buildConfigFlags(ctx));
1039
- cached = createClient({
1040
- url: resolved.url,
1041
- apiKey: resolved.apiKey
1042
- });
1043
- return cached;
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 { AbortError, ConfigError, HttpError, MetabaseError, TimeoutError, account, clearLicense, clearProfile, combineAborts, connectionFlags, createClient, credentials, defineMetabaseCommand, errorMessage, isNotFoundError, listEnvelopeSchema, normalizeUrl, originOnly, outputFlags, parseJson, profileFlag, readEnvCredentials, readEnvLicenseToken, renderItem, renderList, resolveProfileName, throwIfAborted, warn, wrapList, writeLicense, writeProfile };
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 };