@metabase/cli 0.1.4 → 0.1.6

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 (218) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/README.md +147 -101
  3. package/dist/{add-collection-CffaBB-Y.mjs → add-collection-BU8r3r2M.mjs} +9 -4
  4. package/dist/add-collection-C0w6ACQF.mjs +11 -0
  5. package/dist/{archive-uJrslh9r.mjs → archive-BNinrUak.mjs} +9 -8
  6. package/dist/{archive-GdGm7l2e.mjs → archive-C1enZgKV.mjs} +8 -7
  7. package/dist/archive-CDA0KxL8.mjs +40 -0
  8. package/dist/{archive-BAcEXbT9.mjs → archive-CRhiBpPJ.mjs} +9 -8
  9. package/dist/{archive-B_B3MQp0.mjs → archive-DMPS8Kih.mjs} +9 -8
  10. package/dist/archive-lWgqiFAt.mjs +40 -0
  11. package/dist/auth-CzXb_zB2.mjs +19 -0
  12. package/dist/{body-D6dHGjMT.mjs → body-DjdFxjpg.mjs} +4 -4
  13. package/dist/{branches-Bpe40fEd.mjs → branches-B1WRfG7-.mjs} +11 -7
  14. package/dist/{cancel-BWTY6oYI.mjs → cancel-Dl_Ho056.mjs} +7 -6
  15. package/dist/{cancel-task--BfiAXfS.mjs → cancel-task-CdigdCaO.mjs} +11 -7
  16. package/dist/capabilities-7e9MgquN.mjs +29 -0
  17. package/dist/card-DP4rfoOi.mjs +21 -0
  18. package/dist/{card-CQxvHeyP.mjs → card-DlCAaAPq.mjs} +1 -1
  19. package/dist/{cards-CVlFJxYh.mjs → cards-BGiJS675.mjs} +8 -7
  20. package/dist/cli.mjs +267 -44
  21. package/dist/collection-tY18ezvn.mjs +21 -0
  22. package/dist/{predicates-CGO17Q15.mjs → command-augment-BH9qgQ5u.mjs} +66 -14
  23. package/dist/create-BNiva__H.mjs +52 -0
  24. package/dist/{create-izE3EKCt.mjs → create-BTcpaop_.mjs} +9 -8
  25. package/dist/{create-BykvNpSA.mjs → create-BYlIju0b.mjs} +14 -12
  26. package/dist/{create-Bu-YhIDL.mjs → create-Be_0Vier.mjs} +10 -9
  27. package/dist/{create-DYoc9IXW.mjs → create-CHF313Qg.mjs} +13 -9
  28. package/dist/{create-Cz3_Wxdt.mjs → create-CwGtmwqm.mjs} +14 -12
  29. package/dist/{create-DP8RrLDi.mjs → create-CzzrbL0u.mjs} +10 -9
  30. package/dist/{create-BzElku2l.mjs → create-DGth_uOp.mjs} +14 -12
  31. package/dist/{create-branch-B49UQyCK.mjs → create-branch-DKZkoQ64.mjs} +11 -7
  32. package/dist/{create-DQVdMT2Y.mjs → create-dhxPxfF3.mjs} +16 -14
  33. package/dist/{credentials-xKSoP6eh.mjs → credentials-dzeq7ckm.mjs} +12 -10
  34. package/dist/{current-task-DweHmjlk.mjs → current-task-CCRzm0_7.mjs} +11 -7
  35. package/dist/dashboard-ChM_Tu0l.mjs +22 -0
  36. package/dist/{dashboard-CnMD04PQ.mjs → dashboard-FY5UzJ_Z.mjs} +2 -1
  37. package/dist/{database-BNlvldUL.mjs → database-CIXwHKjK.mjs} +3 -3
  38. package/dist/{database-vvig8k4x.mjs → database-lH-B3G1I.mjs} +1 -1
  39. package/dist/db-DrQn_i3W.mjs +22 -0
  40. package/dist/{remove-B3ZEqBF7.mjs → delete-CM3jnAeQ.mjs} +21 -20
  41. package/dist/{delete-DojHmKeM.mjs → delete-Dimc-2y8.mjs} +9 -8
  42. package/dist/{delete-DIz9Tgz5.mjs → delete-ZjnV35OJ.mjs} +9 -8
  43. package/dist/{delete-runtime-BkAdygbs.mjs → delete-runtime-B6RQo_pw.mjs} +5 -3
  44. package/dist/{delete-table-DjN8E3sd.mjs → delete-table-agZJpivt.mjs} +9 -8
  45. package/dist/{deprovision-_HDcBApz.mjs → deprovision-CwxcIT3k.mjs} +16 -12
  46. package/dist/{dirty-Co8V0SZ3.mjs → dirty-D4d0yHqj.mjs} +11 -7
  47. package/dist/{docker-D9sC_37H.mjs → docker-Oq80q3tu.mjs} +4 -4
  48. package/dist/{translate-CG_Ka0dO.mjs → eid-BXzaQh0o.mjs} +37 -22
  49. package/dist/error-C9S6PN3-.mjs +190 -0
  50. package/dist/{export-CVMFxoo1.mjs → export-DTygoXBP.mjs} +17 -16
  51. package/dist/field-Z6Pcxf4n.mjs +19 -0
  52. package/dist/{fields-Coha7vKv.mjs → fields-CoQi99gv.mjs} +9 -8
  53. package/dist/{get-DXv2FkA7.mjs → get-Bzys7vgp.mjs} +8 -7
  54. package/dist/{get-bNtA7vWe.mjs → get-C2p383Qc.mjs} +8 -7
  55. package/dist/{get-Br6WayZv.mjs → get-C3HdQ91a.mjs} +8 -7
  56. package/dist/{get-BOtKerj8.mjs → get-CP3Z3NiH.mjs} +9 -8
  57. package/dist/{get-BSKoL8ek.mjs → get-C_w1kvN3.mjs} +9 -8
  58. package/dist/{get-Be6EFh94.mjs → get-CzuzeKSe.mjs} +10 -9
  59. package/dist/{get-BVTz9B_H.mjs → get-D3SbEQSE.mjs} +10 -9
  60. package/dist/get-DFxZXaKz.mjs +79 -0
  61. package/dist/{get-DZrV7v9d.mjs → get-DQTZG_NP.mjs} +8 -7
  62. package/dist/{get-CJwzbVjc.mjs → get-DSWFjy7O.mjs} +8 -7
  63. package/dist/{get-BxzCKVC6.mjs → get-Ddr0XLh7.mjs} +8 -7
  64. package/dist/{get-AOvWo48B.mjs → get-Hc93A0Yz.mjs} +8 -7
  65. package/dist/{get-C_6K7MSW.mjs → get-lb7q3JYs.mjs} +7 -6
  66. package/dist/get-run-B7sKdaDU.mjs +38 -0
  67. package/dist/git-sync-CiGAad76.mjs +28 -0
  68. package/dist/{has-remote-changes-D6xgsuUr.mjs → has-remote-changes-BY10-nnE.mjs} +11 -7
  69. package/dist/{import-Dv0ORSNw.mjs → import-CiMz4Wz-.mjs} +17 -16
  70. package/dist/{input-BQ-BZA8h.mjs → input-cMSEqISy.mjs} +7 -4
  71. package/dist/{is-dirty-WNi8a6O9.mjs → is-dirty-BZOaryxT.mjs} +9 -4
  72. package/dist/is-dirty-Ume4oV0j.mjs +10 -0
  73. package/dist/{items-CTcAMknV.mjs → items-BWfvkY-J.mjs} +9 -8
  74. package/dist/key-C2XG394c.mjs +17 -0
  75. package/dist/license-Dxarh-gG.mjs +17 -0
  76. package/dist/{list-FXuSCYpa.mjs → list--OYdUTtu.mjs} +7 -6
  77. package/dist/{list-8oVMvlLV.mjs → list-2j7GsXsl.mjs} +7 -6
  78. package/dist/{list-xQmtQPSl.mjs → list-BI4zr8LW.mjs} +10 -8
  79. package/dist/{list-DhWG5jiW.mjs → list-Brgh-Z2v.mjs} +8 -6
  80. package/dist/{list-DSs0Q78i.mjs → list-C3hfovHv.mjs} +7 -6
  81. package/dist/{list-DvUjMQze.mjs → list-CL7eCOQE.mjs} +7 -6
  82. package/dist/list-Clz5igWg.mjs +44 -0
  83. package/dist/list-D4sFiqX8.mjs +173 -0
  84. package/dist/{list-BxdXvGTK.mjs → list-DXH7TlkU.mjs} +9 -7
  85. package/dist/{list-CocYwmnI.mjs → list-DZ8fNUoQ.mjs} +9 -8
  86. package/dist/{list-DjhZU-FY.mjs → list-SOG0whQ-.mjs} +7 -6
  87. package/dist/{list-DI7K3K6k.mjs → list-d58BprgJ.mjs} +7 -6
  88. package/dist/{list-NiwCL_1X.mjs → list-sD5N3fGk.mjs} +9 -8
  89. package/dist/{list-CbJeP0Z6.mjs → list-zSO0DMw-.mjs} +10 -6
  90. package/dist/{login-SXsSH0I1.mjs → login-Bm2AnCez.mjs} +65 -80
  91. package/dist/{logout-bgOXjxbN.mjs → logout-BlyRJODO.mjs} +8 -7
  92. package/dist/{logs-BnwVbFuD.mjs → logs-CywPikkL.mjs} +9 -8
  93. package/dist/{manifest-CGM7XNLC.mjs → manifest-BBR46KFM.mjs} +15 -15
  94. package/dist/measure-C44EK_xt.mjs +20 -0
  95. package/dist/{measure-BEQfnLdN.mjs → measure-ClESGxIb.mjs} +2 -2
  96. package/dist/{metadata-Bu2HOmuX.mjs → metadata-B8ZSF9LA.mjs} +10 -9
  97. package/dist/{metadata-B0WZT3Yb.mjs → metadata-DqiI2q9q.mjs} +9 -8
  98. package/dist/parse-enum-CrEWOhuY.mjs +11 -0
  99. package/dist/{parse-id-B3B-0hUA.mjs → parse-id-lk_K-CEF.mjs} +1 -1
  100. package/dist/{parse-ref-D1yeDOn8.mjs → parse-ref-BiETXmvm.mjs} +1 -1
  101. package/dist/{parse-schemas-DgtVLikM.mjs → parse-schemas-BqUdWUwq.mjs} +2 -2
  102. package/dist/path-AEtZ3mBq.mjs +58 -0
  103. package/dist/{poll-BCnrcUVf.mjs → poll-DHKDpCiq.mjs} +2 -2
  104. package/dist/{poll-task-0b1V6G-8.mjs → poll-task-Cooi0lQV.mjs} +3 -20
  105. package/dist/{preflight-5ACaYnDp.mjs → preflight-aXV5LyDs.mjs} +4 -4
  106. package/dist/{process-FjsqDwKo.mjs → process-C7V8LJ-j.mjs} +1 -1
  107. package/dist/{prompt-DgDNy_Pc.mjs → prompt-CFKoys7k.mjs} +3 -1
  108. package/dist/{provision-29Zt62Ft.mjs → provision-UWcNDoDe.mjs} +29 -24
  109. package/dist/{ps-BMFiRCi4.mjs → ps-CJU0EbrC.mjs} +5 -3
  110. package/dist/ps-DEroLgbI.mjs +11 -0
  111. package/dist/{query-DxA353Hy.mjs → query-AaKzYnTY.mjs} +9 -8
  112. package/dist/{query-aba8MEe_.mjs → query-BlsVNZpD.mjs} +15 -13
  113. package/dist/{remove-BfgU_CQi.mjs → remove-BFWun0e8.mjs} +9 -8
  114. package/dist/{remove-collection-Brv72xUe.mjs → remove-collection-CoCmrrQs.mjs} +13 -9
  115. package/dist/{render-DuoDUTVL.mjs → render-CfznwleY.mjs} +15 -17
  116. package/dist/render-OQn3iRsI.mjs +32 -0
  117. package/dist/{rescan-values-DIAdjoq7.mjs → rescan-values-C0FDsjT7.mjs} +10 -9
  118. package/dist/{run-CgXRo0hD.mjs → run-B4Wn43zm.mjs} +10 -9
  119. package/dist/{runs-DtLRw6xg.mjs → runs-Bbaszr18.mjs} +9 -8
  120. package/dist/{runtime-Br8L4NPm.mjs → runtime-Dmv5VtUK.mjs} +657 -428
  121. package/dist/{schema-tables-DiKMY6lx.mjs → schema-tables-CaWinbuK.mjs} +9 -8
  122. package/dist/{schemas-Bvr8cOzo.mjs → schemas-DUgGpAyB.mjs} +7 -6
  123. package/dist/{search-BT_TCcTd.mjs → search-BLrBXLUk.mjs} +12 -16
  124. package/dist/segment-B3Uwwcsm.mjs +20 -0
  125. package/dist/{set-DtG0KH6P.mjs → set-B8cUbRLD.mjs} +13 -12
  126. package/dist/{set-CAIkXlPy.mjs → set-DfGsta5O.mjs} +11 -10
  127. package/dist/{setting-BDOi5fk_.mjs → setting-D2p2MA7f.mjs} +3 -3
  128. package/dist/{setup-LjTvvlJy.mjs → setup-C9ikBRw_.mjs} +9 -8
  129. package/dist/skills-CUHIcQS6.mjs +18 -0
  130. package/dist/skills-CiN1OQ8W.mjs +191 -0
  131. package/dist/snippet-B7D0uWlz.mjs +20 -0
  132. package/dist/{start-CXKt0Q7A.mjs → start-3PX3ahjT.mjs} +68 -36
  133. package/dist/{stash-dRw1UEwg.mjs → stash-EIDcSvpF.mjs} +17 -16
  134. package/dist/{status-C2niMfrQ.mjs → status-95ElRAu9.mjs} +12 -8
  135. package/dist/status-B0_MiZEf.mjs +100 -0
  136. package/dist/status-CEplmC44.mjs +34 -0
  137. package/dist/{stop-BdedYfwU.mjs → stop-CQ0XGrN8.mjs} +11 -10
  138. package/dist/{summary-BPDA4K99.mjs → summary-C12LiEuJ.mjs} +8 -7
  139. package/dist/{sync-schema-D95LLRpf.mjs → sync-schema-Ba8M3DiX.mjs} +10 -9
  140. package/dist/{table-B-PYcgGb.mjs → table-C7a5V6Zn.mjs} +1 -1
  141. package/dist/table-e6h8SLVX.mjs +20 -0
  142. package/dist/transform-BMYh1lsC.mjs +25 -0
  143. package/dist/transform-job-Cm7z5TfH.mjs +20 -0
  144. package/dist/{transform-job-Csr86muI.mjs → transform-job-DeTDPMxt.mjs} +1 -1
  145. package/dist/{tree-DazZT7dR.mjs → tree-Des2ZG9d.mjs} +6 -5
  146. package/dist/{update-DE6kjV-f.mjs → update-Bx54nWEI.mjs} +17 -15
  147. package/dist/{update-bW-i6gjZ.mjs → update-CyIZdbIQ.mjs} +11 -10
  148. package/dist/{update-djgvzO3K.mjs → update-DBi5U8zb.mjs} +16 -14
  149. package/dist/{update-CJSDB6S8.mjs → update-DHZubok3.mjs} +18 -14
  150. package/dist/{update-BBfvArCx.mjs → update-DSgceARZ.mjs} +11 -10
  151. package/dist/{update-DSWZSfpw.mjs → update-DzAN4SPj.mjs} +15 -13
  152. package/dist/{update-WyRKlQPh.mjs → update-F6DmZncY.mjs} +11 -10
  153. package/dist/{update-DTIWJxob.mjs → update-_QfgNa53.mjs} +12 -11
  154. package/dist/{update-dashcard-BhD5x__K.mjs → update-dashcard-wpSjv4M7.mjs} +11 -10
  155. package/dist/{update-9kVyE3BJ.mjs → update-mYVnoYNV.mjs} +15 -13
  156. package/dist/{update-659eQR1L.mjs → update-njHe3j-s.mjs} +15 -13
  157. package/dist/{upgrade-D58rvXHM.mjs → upgrade-iAuvhX-W.mjs} +9 -8
  158. package/dist/{url-DKkSu2D8.mjs → url-DWaT6WIZ.mjs} +11 -10
  159. package/dist/{uuid-BF20B59s.mjs → uuid-CMKnS8-z.mjs} +8 -6
  160. package/dist/{validate-CB0bu50i.mjs → validate-dPEOnOf8.mjs} +2 -1
  161. package/dist/{validate-query-CavIA0Q2.mjs → validate-query-Cw6WE5Y8.mjs} +3 -3
  162. package/dist/{values-DyjmpcbT.mjs → values-BfSTAbzc.mjs} +8 -7
  163. package/dist/verify-D5YtTqqp.mjs +79 -0
  164. package/dist/{wait-CeUPCgdc.mjs → wait-8yV9_WIo.mjs} +2 -2
  165. package/dist/{wait-DhkTaV6E.mjs → wait-Bv3Tsnv4.mjs} +12 -8
  166. package/dist/{wait-flags-BR-yqe7y.mjs → wait-flags-Dzq9BGQY.mjs} +20 -9
  167. package/dist/workspace-CKLZrR7l.mjs +26 -0
  168. package/dist/{workspace-credentials-Cctumbru.mjs → workspace-credentials-BXpABsNZ.mjs} +2 -41
  169. package/dist/yaml-YTQiYJ9s.mjs +43 -0
  170. package/package.json +6 -2
  171. package/skill-data/core/SKILL.md +177 -0
  172. package/skill-data/git-sync/SKILL.md +196 -0
  173. package/skill-data/mbql/SKILL.md +156 -0
  174. package/skill-data/mbql/references/operators.md +253 -0
  175. package/skill-data/transform/SKILL.md +197 -0
  176. package/skill-data/viz/SKILL.md +137 -0
  177. package/skill-data/viz/references/settings.md +312 -0
  178. package/skill-data/workspace/SKILL.md +390 -0
  179. package/skills/metabase-cli/SKILL.md +21 -0
  180. package/dist/add-collection-CPL1njYZ.mjs +0 -11
  181. package/dist/api-key-9p1UPnXn.mjs +0 -13
  182. package/dist/auth-N4w5xtwW.mjs +0 -19
  183. package/dist/card-4rZRb5bc.mjs +0 -20
  184. package/dist/collection-Cp_B02I4.mjs +0 -19
  185. package/dist/command-augment-D9pI9Vbh.mjs +0 -11
  186. package/dist/create-doyv3SxU.mjs +0 -50
  187. package/dist/create-ov-De5dO.mjs +0 -125
  188. package/dist/dashboard-BYBiA-IG.mjs +0 -20
  189. package/dist/db-CObVU22j.mjs +0 -22
  190. package/dist/eid-Cr5r-t9B.mjs +0 -13
  191. package/dist/field-CbljasCH.mjs +0 -18
  192. package/dist/flag-pair-Fmcdkrfx.mjs +0 -17
  193. package/dist/get-run-CSrXHDGS.mjs +0 -36
  194. package/dist/git-sync-BGkS8o5b.mjs +0 -28
  195. package/dist/is-dirty-BOZ4xz92.mjs +0 -10
  196. package/dist/key-CCJdVWKc.mjs +0 -12
  197. package/dist/license-DLLTpFvP.mjs +0 -17
  198. package/dist/list-BNzdnE1c.mjs +0 -55
  199. package/dist/measure-B54VtKym.mjs +0 -19
  200. package/dist/package-D-aVYFKM.mjs +0 -80
  201. package/dist/ps-C5FOLwL2.mjs +0 -11
  202. package/dist/segment-C2ui5dSd.mjs +0 -19
  203. package/dist/snippet-BcgVYsoR.mjs +0 -19
  204. package/dist/status-BEONmJWv.mjs +0 -32
  205. package/dist/status-BWep0PFe.mjs +0 -56
  206. package/dist/table-lCNGbvej.mjs +0 -19
  207. package/dist/transform-BGAm1s4f.mjs +0 -24
  208. package/dist/transform-job-cNTJ30pm.mjs +0 -19
  209. package/dist/workspace-DtcBldk0.mjs +0 -24
  210. /package/dist/{body-flags-BK7J6Daz.mjs → body-flags-D7q87Btw.mjs} +0 -0
  211. /package/dist/{field-B3gvaqpK.mjs → field-yomXlkvl.mjs} +0 -0
  212. /package/dist/{paginate-CTSfuYiF.mjs → paginate-Dfm9eO9A.mjs} +0 -0
  213. /package/dist/{revision-message-flag-oyq2xrDU.mjs → revision-message-flag-WmsIzUOM.mjs} +0 -0
  214. /package/dist/{segment-BMrUBz94.mjs → segment-Be2v4ilr.mjs} +0 -0
  215. /package/dist/{setting-CTaAeMci.mjs → setting-oL97SNeO.mjs} +0 -0
  216. /package/dist/{snippet-Dw0Sjzkr.mjs → snippet-COggaWxx.mjs} +0 -0
  217. /package/dist/{transform-IEX4Mx3X.mjs → transform-GTW3G-01.mjs} +0 -0
  218. /package/dist/{workspace-C5q4nbpY.mjs → workspace-BBXJczJK.mjs} +0 -0
@@ -1,16 +1,17 @@
1
- import { package_default } from "./package-D-aVYFKM.mjs";
2
- import { setMetabaseAugment } from "./command-augment-D9pI9Vbh.mjs";
3
- import { AbortError, ConfigError, MetabaseError, NetworkError, TimeoutError, VERBOSE_ENV, ValidationError, errorMessage, isNotFoundError, isPlainObject, toMetabaseError } from "./predicates-CGO17Q15.mjs";
1
+ import { AbortError, ConfigError, MetabaseError, NetworkError, ResponseShapeError, TimeoutError, ValidationError, errorMessage, flagConsumesValue, isNotFoundError, normalizeFlag, setMetabaseAugment, toAliasArray } from "./command-augment-BH9qgQ5u.mjs";
2
+ import { DEFAULT_MAX_BYTES, package_default, reportError } from "./error-C9S6PN3-.mjs";
3
+ import { BASELINE_CAPABILITIES, isPlainObject, warn } from "./capabilities-7e9MgquN.mjs";
4
4
  import { defineCommand } from "citty";
5
5
  import { z } from "zod";
6
6
  import { promises } from "node:fs";
7
7
  import { dirname, join } from "node:path";
8
- import { homedir } from "node:os";
9
8
  import { Entry } from "@napi-rs/keyring";
9
+ import { homedir } from "node:os";
10
+ import { parse } from "semver";
10
11
  import { setTimeout } from "node:timers/promises";
11
12
 
12
13
  //#region src/runtime/json.ts
13
- const JSON_CONTENT_TYPE$1 = "application/json";
14
+ const JSON_CONTENT_TYPE = "application/json";
14
15
  function parseJson(input, schema, opts = {}) {
15
16
  const result = parseJsonResult(input, schema, opts);
16
17
  if (!result.ok) throw result.error;
@@ -48,88 +49,7 @@ function parseJsonOrPlain(text, contentType, schema, opts = {}) {
48
49
  return parseJson(JSON.stringify(text), schema, opts);
49
50
  }
50
51
  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);
52
+ return contentType !== null && contentType.includes(JSON_CONTENT_TYPE);
133
53
  }
134
54
 
135
55
  //#endregion
@@ -145,85 +65,92 @@ function configDir() {
145
65
  }
146
66
 
147
67
  //#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()
68
+ //#region src/domain/session-properties.ts
69
+ const ServerVersion = z.object({
70
+ tag: z.string(),
71
+ date: z.string().optional(),
72
+ hash: z.string().optional()
73
+ }).loose();
74
+ const TokenFeatures = z.record(z.string(), z.boolean());
75
+ const SessionProperties = z.object({
76
+ version: ServerVersion,
77
+ "token-features": TokenFeatures.optional()
78
+ }).loose();
79
+
80
+ //#endregion
81
+ //#region src/core/version/tag.ts
82
+ const ParsedVersionSchema = z.object({
83
+ tag: z.string(),
84
+ major: z.number().int().nonnegative(),
85
+ patch: z.number().int().nonnegative()
156
86
  });
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()
87
+ function tryParseTag(tag) {
88
+ const parsed = parse(tag);
89
+ if (parsed === null || parsed.major !== 0 && parsed.major !== 1) return null;
90
+ return {
91
+ tag,
92
+ major: parsed.minor,
93
+ patch: parsed.patch
191
94
  };
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
95
  }
205
96
 
97
+ //#endregion
98
+ //#region src/core/auth/profile-record.ts
99
+ const ProbedUser = z.object({
100
+ id: z.number().int(),
101
+ name: z.string(),
102
+ isAdmin: z.boolean()
103
+ });
104
+ const ProfileLastProbe = z.object({
105
+ at: z.iso.datetime(),
106
+ version: ParsedVersionSchema.nullable(),
107
+ tokenFeatures: TokenFeatures.nullable(),
108
+ user: ProbedUser
109
+ });
110
+ const ProfileFailureKind = z.enum([
111
+ "auth",
112
+ "network",
113
+ "server"
114
+ ]);
115
+ const ProfileLastFailure = z.object({
116
+ at: z.iso.datetime(),
117
+ kind: ProfileFailureKind,
118
+ reason: z.string()
119
+ });
120
+ const ProfileRecord = z.object({
121
+ name: z.string(),
122
+ url: z.string(),
123
+ apiKey: z.string().nullable(),
124
+ lastProbe: ProfileLastProbe.nullable(),
125
+ lastFailure: ProfileLastFailure.nullable()
126
+ });
127
+ const ProfilesFile = z.object({
128
+ profiles: z.array(ProfileRecord),
129
+ license: z.string().nullable()
130
+ });
131
+
206
132
  //#endregion
207
133
  //#region src/core/auth/storage.ts
208
- const CredentialsFileSchema = z.record(z.string(), z.string());
209
134
  const KEYRING_SERVICE = "metabase-cli";
210
- const CREDENTIALS_FILE = "credentials.json";
211
- const PROFILE_INDEX_FILE = "profiles.json";
135
+ const PROFILES_FILE = "profiles.json";
136
+ const LEGACY_CREDENTIALS_FILE = "credentials.json";
137
+ const LEGACY_REJECTIONS_FILE = "rejections.json";
138
+ const PROFILES_FILE_MODE = 384;
139
+ const PROFILES_DIR_MODE = 448;
212
140
  const DEFAULT_PROFILE = "default";
213
- const CREDENTIALS_FILE_MODE = 384;
214
- const CREDENTIALS_DIR_MODE = 448;
141
+ const LEGACY_STORAGE_NOTICE = "Old profile storage detected and ignored; re-run `mb auth login` for each profile.";
215
142
  const account = {
216
- profileUrl: (profile) => `profile:${profile}:url`,
217
143
  profileApiKey: (profile) => `profile:${profile}:apiKey`,
218
144
  license: "license"
219
145
  };
220
- const ProfileIndexSchema = z.array(z.string());
221
- const FILE_STORE_PROFILE_URL_PATTERN = /^profile:(.+):url$/;
222
- function fallbackFilePath() {
223
- return join(configDir(), CREDENTIALS_FILE);
146
+ let legacyWarningPending = false;
147
+ function profilesFilePath() {
148
+ return join(configDir(), PROFILES_FILE);
224
149
  }
225
- function profileIndexPath() {
226
- return join(configDir(), PROFILE_INDEX_FILE);
150
+ function consumeLegacyStorageWarning() {
151
+ if (!legacyWarningPending) return null;
152
+ legacyWarningPending = false;
153
+ return LEGACY_STORAGE_NOTICE;
227
154
  }
228
155
  function keyringEnabled() {
229
156
  return process.env["METABASE_CLI_DISABLE_KEYRING"] !== "1";
@@ -253,253 +180,221 @@ function tryRemoveKeyring(key) {
253
180
  return void 0;
254
181
  }
255
182
  }
256
- async function readFileStore() {
257
- const path = fallbackFilePath();
183
+ async function readProfilesFile() {
184
+ const path = profilesFilePath();
258
185
  let raw;
259
186
  try {
260
187
  raw = await promises.readFile(path, "utf8");
261
188
  } catch (error) {
262
- if (isNotFoundError(error)) return {};
263
- throw error;
264
- }
265
- return parseJson(raw, CredentialsFileSchema, { source: path });
266
- }
267
- async function writeFileStore(store) {
268
- const path = fallbackFilePath();
269
- await promises.mkdir(dirname(path), {
270
- recursive: true,
271
- mode: CREDENTIALS_DIR_MODE
272
- });
273
- await promises.writeFile(path, JSON.stringify(store, null, 2) + "\n", { mode: CREDENTIALS_FILE_MODE });
274
- if (process.platform !== "win32") await promises.chmod(path, CREDENTIALS_FILE_MODE);
275
- }
276
- async function setFile(key, value) {
277
- const store = await readFileStore();
278
- store[key] = value;
279
- await writeFileStore(store);
280
- }
281
- async function readFromFile(key) {
282
- const store = await readFileStore();
283
- return store[key] ?? null;
284
- }
285
- async function removeFromFile(key) {
286
- const store = await readFileStore();
287
- if (!(key in store)) return false;
288
- delete store[key];
289
- if (Object.keys(store).length === 0) await promises.unlink(fallbackFilePath()).catch(() => void 0);
290
- else await writeFileStore(store);
291
- return true;
292
- }
293
- const credentials = {
294
- async set(key, value) {
295
- if (trySetKeyring(key, value)) {
296
- await removeFromFile(key).catch(() => void 0);
189
+ if (isNotFoundError(error)) {
190
+ await detectLegacyArtifacts();
297
191
  return {
298
- backend: "keyring",
299
- service: KEYRING_SERVICE,
300
- account: key
192
+ profiles: [],
193
+ license: null
301
194
  };
302
195
  }
303
- await setFile(key, value);
304
- return {
305
- backend: "file",
306
- path: fallbackFilePath(),
307
- account: key
308
- };
309
- },
310
- async read(key) {
311
- const fromKeyring = tryReadKeyring(key);
312
- if (fromKeyring !== void 0) return fromKeyring;
313
- return readFromFile(key);
314
- },
315
- async has(key) {
316
- return await credentials.read(key) !== null;
317
- },
318
- async remove(key) {
319
- const fromKeyring = tryRemoveKeyring(key);
320
- const fromFile = await removeFromFile(key).catch(() => false);
321
- if (fromKeyring === void 0) return fromFile;
322
- return fromKeyring || fromFile;
323
- },
324
- async location(key) {
325
- if (tryReadKeyring(key) !== void 0) return {
326
- backend: "keyring",
327
- service: KEYRING_SERVICE,
328
- account: key
329
- };
196
+ throw error;
197
+ }
198
+ const parsed = parseJsonResult(raw, ProfilesFile, { source: path });
199
+ if (parsed.ok) return parsed.value;
200
+ if (parsed.error instanceof ValidationError) {
201
+ legacyWarningPending = true;
330
202
  return {
331
- backend: "file",
332
- path: fallbackFilePath(),
333
- account: key
203
+ profiles: [],
204
+ license: null
334
205
  };
335
206
  }
336
- };
337
- async function readProfile(name = DEFAULT_PROFILE) {
338
- const [url, apiKey] = await Promise.all([credentials.read(account.profileUrl(name)), credentials.read(account.profileApiKey(name))]);
339
- if (!url || !apiKey) return null;
340
- return {
341
- url,
342
- apiKey
343
- };
207
+ throw parsed.error;
344
208
  }
345
- async function writeProfile(profile, name = DEFAULT_PROFILE) {
346
- await credentials.set(account.profileUrl(name), profile.url);
347
- const location = await credentials.set(account.profileApiKey(name), profile.apiKey);
348
- await addToProfileIndex(name);
349
- return location;
209
+ async function detectLegacyArtifacts() {
210
+ const legacyCredentials = join(configDir(), LEGACY_CREDENTIALS_FILE);
211
+ const legacyRejections = join(configDir(), LEGACY_REJECTIONS_FILE);
212
+ const [credentialsExists, rejectionsExists] = await Promise.all([fileExists(legacyCredentials), fileExists(legacyRejections)]);
213
+ if (credentialsExists || rejectionsExists) legacyWarningPending = true;
350
214
  }
351
- async function clearProfile(name = DEFAULT_PROFILE) {
352
- const removedUrl = await credentials.remove(account.profileUrl(name));
353
- const removedKey = await credentials.remove(account.profileApiKey(name));
354
- await removeFromProfileIndex(name);
355
- return removedUrl || removedKey;
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;
215
+ async function fileExists(path) {
367
216
  try {
368
- raw = await promises.readFile(path, "utf8");
369
- } catch (error) {
370
- if (isNotFoundError(error)) return null;
371
- throw error;
217
+ await promises.access(path);
218
+ return true;
219
+ } catch {
220
+ return false;
372
221
  }
373
- return parseJson(raw, ProfileIndexSchema, { source: path });
374
222
  }
375
- async function writeProfileIndex(names) {
376
- const path = profileIndexPath();
377
- const unique = [...new Set(names)].toSorted();
223
+ async function writeProfilesFile(file) {
224
+ const path = profilesFilePath();
225
+ if (file.profiles.length === 0 && file.license === null) {
226
+ await promises.unlink(path).catch(() => void 0);
227
+ await cleanupLegacyFiles();
228
+ return;
229
+ }
378
230
  await promises.mkdir(dirname(path), {
379
231
  recursive: true,
380
- mode: CREDENTIALS_DIR_MODE
232
+ mode: PROFILES_DIR_MODE
381
233
  });
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];
234
+ await promises.writeFile(path, JSON.stringify(file, null, 2) + "\n", { mode: PROFILES_FILE_MODE });
235
+ if (process.platform !== "win32") await promises.chmod(path, PROFILES_FILE_MODE);
236
+ await cleanupLegacyFiles();
411
237
  }
412
- async function readLicense() {
413
- return credentials.read(account.license);
238
+ async function cleanupLegacyFiles() {
239
+ await Promise.all([promises.unlink(join(configDir(), LEGACY_CREDENTIALS_FILE)).catch(() => void 0), promises.unlink(join(configDir(), LEGACY_REJECTIONS_FILE)).catch(() => void 0)]);
414
240
  }
415
- async function writeLicense(token) {
416
- return credentials.set(account.license, token);
241
+ function findRecord(file, name) {
242
+ return file.profiles.find((entry) => entry.name === name) ?? null;
417
243
  }
418
- async function clearLicense() {
419
- return credentials.remove(account.license);
244
+ function fileLocation(key) {
245
+ return {
246
+ backend: "file",
247
+ path: profilesFilePath(),
248
+ account: key,
249
+ reason: keyringEnabled() ? "unavailable" : "disabled"
250
+ };
420
251
  }
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;
252
+ function keyringFallbackWarning(location, subject) {
253
+ const cause = location.reason === "disabled" ? "OS keychain disabled via METABASE_CLI_DISABLE_KEYRING" : "OS keychain unavailable";
254
+ return `warning: ${cause}; ${subject} stored as plaintext at ${location.path}`;
428
255
  }
429
- function originOnly(input) {
430
- const parsed = new URL(input);
431
- parsed.username = "";
432
- parsed.password = "";
433
- return parsed.origin;
256
+ async function persistApiKey(name, apiKey) {
257
+ const key = account.profileApiKey(name);
258
+ if (trySetKeyring(key, apiKey)) return {
259
+ backend: "keyring",
260
+ service: KEYRING_SERVICE,
261
+ account: key
262
+ };
263
+ return fileLocation(key);
434
264
  }
435
- function localUrl(port) {
436
- return `http://localhost:${port}`;
265
+ async function readProfile(name = DEFAULT_PROFILE) {
266
+ const file = await readProfilesFile();
267
+ const record = findRecord(file, name);
268
+ if (record === null) return null;
269
+ const apiKey = await resolveApiKey(record);
270
+ if (apiKey === null) return null;
271
+ return {
272
+ url: record.url,
273
+ apiKey
274
+ };
437
275
  }
438
-
439
- //#endregion
440
- //#region src/core/config.ts
441
- const ENV_URL = "METABASE_URL";
442
- const ENV_API_KEY = "METABASE_API_KEY";
443
- const ENV_PROFILE = "METABASE_PROFILE";
444
- const ENV_LICENSE_TOKEN = "METABASE_LICENSE_TOKEN";
445
- function resolveProfileName(profileFlag$1) {
446
- return explicitProfileName(profileFlag$1) ?? DEFAULT_PROFILE;
276
+ async function resolveApiKey(record) {
277
+ const fromKeyring = tryReadKeyring(account.profileApiKey(record.name));
278
+ if (typeof fromKeyring === "string") return fromKeyring;
279
+ return record.apiKey;
447
280
  }
448
- function explicitProfileName(profileFlag$1) {
449
- return profileFlag$1 || process.env[ENV_PROFILE] || null;
281
+ async function readProfileRecord(name = DEFAULT_PROFILE) {
282
+ const file = await readProfilesFile();
283
+ return findRecord(file, name);
450
284
  }
451
- function readEnvCredentials() {
452
- return {
453
- url: process.env[ENV_URL] ?? null,
454
- apiKey: process.env[ENV_API_KEY] ?? null
285
+ async function listProfileRecords() {
286
+ const file = await readProfilesFile();
287
+ return file.profiles;
288
+ }
289
+ async function writeProfile(profile, name = DEFAULT_PROFILE) {
290
+ const location = await persistApiKey(name, profile.apiKey);
291
+ const inlineApiKey = location.backend === "file" ? profile.apiKey : null;
292
+ const file = await readProfilesFile();
293
+ const existing = findRecord(file, name);
294
+ const updated = existing === null ? {
295
+ name,
296
+ url: profile.url,
297
+ apiKey: inlineApiKey,
298
+ lastProbe: null,
299
+ lastFailure: null
300
+ } : {
301
+ ...existing,
302
+ url: profile.url,
303
+ apiKey: inlineApiKey
455
304
  };
305
+ const profiles = existing === null ? [...file.profiles, updated] : file.profiles.map((entry) => entry.name === name ? updated : entry);
306
+ await writeProfilesFile({
307
+ ...file,
308
+ profiles
309
+ });
310
+ return location;
456
311
  }
457
- function readEnvLicenseToken() {
458
- return process.env[ENV_LICENSE_TOKEN] ?? null;
312
+ async function writeProbeResult(name, input) {
313
+ const probe = ProfileLastProbe.parse({
314
+ at: new Date().toISOString(),
315
+ version: input.server.version,
316
+ tokenFeatures: input.server.tokenFeatures,
317
+ user: input.user
318
+ });
319
+ const file = await readProfilesFile();
320
+ const existing = findRecord(file, name);
321
+ if (existing === null) return null;
322
+ const profiles = file.profiles.map((entry) => entry.name === name ? {
323
+ ...entry,
324
+ lastProbe: probe,
325
+ lastFailure: null
326
+ } : entry);
327
+ await writeProfilesFile({
328
+ ...file,
329
+ profiles
330
+ });
331
+ return probe;
459
332
  }
460
- async function resolveConfig(flags) {
461
- const profile = resolveProfileName(flags.profile);
462
- const env = readEnvCredentials();
463
- const flagUrl = flags.url;
464
- const flagKey = flags.apiKey;
465
- const needsStored = !flagUrl && !env.url || !flagKey && !env.apiKey;
466
- const stored = needsStored ? await readProfile(profile) : null;
467
- const urlField = pickField(flagUrl, env.url, stored?.url);
468
- const keyField = pickField(flagKey, env.apiKey, stored?.apiKey);
469
- if (urlField === null || keyField === null) {
470
- const rejection = await readRejection(profile);
471
- if (rejection !== null) throw new ConfigError(`Last login for profile "${profile}" was rejected by ${originOnly(rejection.url)}: ${rejection.reason}. Re-run \`mb auth login --profile ${profile}\` with valid credentials.`);
472
- throw new ConfigError(`Not authenticated for profile "${profile}". Run \`mb auth login\`, set ${ENV_URL}/${ENV_API_KEY}, or pass --url/--api-key.`);
473
- }
474
- return {
475
- url: normalizeUrl(urlField.value),
476
- apiKey: keyField.value,
477
- profile,
478
- source: urlField.source === keyField.source ? urlField.source : "mixed"
479
- };
333
+ async function writeProbeFailure(name, input) {
334
+ const failure = ProfileLastFailure.parse({
335
+ at: new Date().toISOString(),
336
+ kind: input.kind,
337
+ reason: input.reason
338
+ });
339
+ const file = await readProfilesFile();
340
+ const existing = findRecord(file, name);
341
+ if (existing === null) return null;
342
+ const profiles = file.profiles.map((entry) => entry.name === name ? {
343
+ ...entry,
344
+ lastFailure: failure
345
+ } : entry);
346
+ await writeProfilesFile({
347
+ ...file,
348
+ profiles
349
+ });
350
+ return failure;
480
351
  }
481
- async function resolveLicenseToken(flags) {
482
- const flag = flags.token;
483
- const env = readEnvLicenseToken();
484
- const stored = !flag && !env ? await readLicense() : null;
485
- const value = flag ?? env ?? stored;
486
- if (!value) throw new ConfigError(`No license token. Pass --token, set ${ENV_LICENSE_TOKEN}, or store one with \`mb license set\`.`);
487
- return value;
352
+ async function clearProfile(name = DEFAULT_PROFILE) {
353
+ tryRemoveKeyring(account.profileApiKey(name));
354
+ const file = await readProfilesFile();
355
+ const existing = findRecord(file, name);
356
+ if (existing === null) return false;
357
+ await writeProfilesFile({
358
+ ...file,
359
+ profiles: file.profiles.filter((entry) => entry.name !== name)
360
+ });
361
+ return true;
488
362
  }
489
- function pickField(flag, env, stored) {
490
- if (flag) return {
491
- value: flag,
492
- source: "flag"
493
- };
494
- if (env) return {
495
- value: env,
496
- source: "env"
497
- };
498
- if (stored) return {
499
- value: stored,
500
- source: "stored"
501
- };
502
- return null;
363
+ async function readLicense() {
364
+ const fromKeyring = tryReadKeyring(account.license);
365
+ if (typeof fromKeyring === "string") return fromKeyring;
366
+ const file = await readProfilesFile();
367
+ return file.license;
368
+ }
369
+ async function writeLicense(token) {
370
+ const key = account.license;
371
+ const file = await readProfilesFile();
372
+ if (trySetKeyring(key, token)) {
373
+ if (file.license !== null) await writeProfilesFile({
374
+ ...file,
375
+ license: null
376
+ });
377
+ return {
378
+ backend: "keyring",
379
+ service: KEYRING_SERVICE,
380
+ account: key
381
+ };
382
+ }
383
+ await writeProfilesFile({
384
+ ...file,
385
+ license: token
386
+ });
387
+ return fileLocation(key);
388
+ }
389
+ async function clearLicense() {
390
+ const removedFromKeyring = tryRemoveKeyring(account.license);
391
+ const file = await readProfilesFile();
392
+ const hadInline = file.license !== null;
393
+ if (hadInline) await writeProfilesFile({
394
+ ...file,
395
+ license: null
396
+ });
397
+ return removedFromKeyring === true || hadInline;
503
398
  }
504
399
 
505
400
  //#endregion
@@ -568,34 +463,27 @@ function redactBody(body, ctx) {
568
463
 
569
464
  //#endregion
570
465
  //#region src/core/http/errors.ts
466
+ const ROUTE_MISSING_LITERAL = "API endpoint does not exist.";
467
+ const RESOURCE_MISSING_LITERAL = "Not found.";
571
468
  const STATUS_CLASSIFICATIONS = {
572
- 401: {
573
- retryable: false,
574
- message: "Invalid or unauthorized API key"
575
- },
576
- 403: {
577
- retryable: false,
578
- message: "Invalid or unauthorized API key"
579
- },
580
- 404: {
581
- retryable: false,
582
- message: "Endpoint not found — is this a Metabase instance?"
583
- },
469
+ 401: { retryable: false },
470
+ 403: { retryable: false },
471
+ 404: { retryable: false },
584
472
  408: {
585
473
  retryable: true,
586
- message: "Metabase timed out responding"
474
+ message: "Metabase timed out responding."
587
475
  },
588
476
  425: { retryable: true },
589
477
  429: {
590
478
  retryable: true,
591
- message: "Metabase rate-limited the request"
479
+ message: "Metabase rate-limited the request."
592
480
  },
593
481
  500: { retryable: true },
594
482
  502: { retryable: true },
595
483
  503: { retryable: true },
596
484
  504: {
597
485
  retryable: true,
598
- message: "Metabase timed out responding"
486
+ message: "Metabase timed out responding."
599
487
  }
600
488
  };
601
489
  const ErrorEnvelope = z.object({
@@ -612,18 +500,22 @@ var HttpError = class extends MetabaseError {
612
500
  category = "http";
613
501
  exitCode = 1;
614
502
  status;
503
+ kind;
615
504
  developerDetail;
616
505
  constructor(input) {
617
506
  const sanitizedBody = sanitizeBody(input.rawBody, input.redactionContext);
618
- super(input.overrideUserMessage ?? extractUserMessage(input.status, sanitizedBody));
507
+ const redactedHeaders = redactHeaders(input.responseHeaders);
508
+ const kind = classifyKind(input.status, sanitizedBody, redactedHeaders);
509
+ super(input.overrideUserMessage ?? buildUserMessage(kind, input, sanitizedBody));
619
510
  this.name = "HttpError";
620
511
  this.status = input.status;
512
+ this.kind = kind;
621
513
  this.developerDetail = {
622
514
  status: input.status,
623
515
  statusText: input.statusText,
624
516
  method: input.method,
625
517
  url: input.url,
626
- responseHeaders: redactHeaders(input.responseHeaders),
518
+ responseHeaders: redactedHeaders,
627
519
  body: sanitizedBody
628
520
  };
629
521
  }
@@ -639,10 +531,39 @@ function sanitizeBody(rawBody, ctx) {
639
531
  if (ctx === void 0) return rawBody;
640
532
  return redactBody(rawBody, ctx);
641
533
  }
642
- function extractUserMessage(status, sanitizedBody) {
534
+ function classifyKind(status, sanitizedBody, redactedHeaders) {
535
+ if (status === 401 || status === 403) return "auth";
536
+ if (status === 404) return isRouteMissingResponse(sanitizedBody, redactedHeaders) ? "route-missing" : "resource-missing";
537
+ if (status === 429) return "rate-limit";
538
+ if (status >= 500 && status < 600) return "server-error";
539
+ return "generic";
540
+ }
541
+ function isRouteMissingResponse(sanitizedBody, redactedHeaders) {
542
+ if (sanitizedBody?.includes(ROUTE_MISSING_LITERAL)) return true;
543
+ if (sanitizedBody?.includes(RESOURCE_MISSING_LITERAL)) return false;
544
+ if (redactedHeaders["content-type"]?.includes(JSON_CONTENT_TYPE)) return false;
545
+ if (sanitizedBody === null || sanitizedBody.trim() === "") return true;
546
+ return !parseJsonResult(sanitizedBody, ErrorEnvelope).ok;
547
+ }
548
+ function buildUserMessage(kind, input, sanitizedBody) {
549
+ if (kind === "route-missing") return buildRouteMissingMessage(input);
550
+ if (kind === "resource-missing") return `Not found: ${input.method} ${pathFromUrl(input.url)}.`;
643
551
  const fromBody = parseEnvelopeMessage(sanitizedBody);
644
- if (fromBody) return fromBody;
645
- return defaultMessageForStatus(status);
552
+ if (fromBody !== null) return fromBody;
553
+ if (kind === "auth") return `Invalid or unauthorized API key (host: ${hostFromUrl(input.url)}).`;
554
+ return defaultMessageForStatus(input.status);
555
+ }
556
+ function buildRouteMissingMessage(input) {
557
+ const path = pathFromUrl(input.url);
558
+ if (!input.serverTag) return `This endpoint is not available on the connected Metabase: ${input.method} ${path}.`;
559
+ return `This endpoint is not available on Metabase ${input.serverTag}: ${input.method} ${path}. The command may require a newer Metabase major version. Run 'mb auth list' to see this server's version, or 'mb __manifest' for per-command requirements.`;
560
+ }
561
+ function pathFromUrl(url) {
562
+ const parsed = new URL(url);
563
+ return parsed.pathname + parsed.search;
564
+ }
565
+ function hostFromUrl(url) {
566
+ return new URL(url).host;
646
567
  }
647
568
  function parseEnvelopeMessage(sanitizedBody) {
648
569
  if (!sanitizedBody) return null;
@@ -691,7 +612,7 @@ function capLength(message) {
691
612
  return message.slice(0, MAX_EXTRACTED_MESSAGE_LEN - ELLIPSIS.length) + ELLIPSIS;
692
613
  }
693
614
  function defaultMessageForStatus(status) {
694
- return STATUS_CLASSIFICATIONS[status]?.message ?? `Metabase returned ${status}`;
615
+ return STATUS_CLASSIFICATIONS[status]?.message ?? `Metabase returned ${status}.`;
695
616
  }
696
617
 
697
618
  //#endregion
@@ -716,11 +637,19 @@ function parseRetryAfter(header) {
716
637
  function sleep(ms, signal) {
717
638
  return setTimeout(ms, void 0, { signal });
718
639
  }
640
+ async function runWithRetries(attempt, signal) {
641
+ let attemptIndex = 0;
642
+ while (true) {
643
+ const outcome = await attempt(attemptIndex);
644
+ if (outcome.kind === "success") return outcome.response;
645
+ await sleep(outcome.delayMs, signal);
646
+ attemptIndex += 1;
647
+ }
648
+ }
719
649
 
720
650
  //#endregion
721
651
  //#region src/core/http/client.ts
722
652
  const DEFAULT_TIMEOUT_MS = 3e4;
723
- const JSON_CONTENT_TYPE = "application/json";
724
653
  const OCTET_STREAM_CONTENT_TYPE = "application/octet-stream";
725
654
  const TEXT_CONTENT_TYPE_PREFIX = "text/";
726
655
  const ERROR_BODY_BYTE_CAP = 64 * 1024;
@@ -730,8 +659,10 @@ const IDEMPOTENT_METHODS = new Set([
730
659
  "HEAD",
731
660
  "OPTIONS"
732
661
  ]);
662
+ const NO_SERVER_TAG = async () => null;
733
663
  function createClient(config, overrides = {}) {
734
664
  const fetchImpl = overrides.fetchImpl ?? globalThis.fetch.bind(globalThis);
665
+ const getServerTag = overrides.getServerTag ?? NO_SERVER_TAG;
735
666
  const redactionContext = { knownSecrets: new Set([config.apiKey]) };
736
667
  async function attemptOnce(prepared, attempt) {
737
668
  const hasRetriesLeft = attempt < prepared.retries;
@@ -757,12 +688,7 @@ function createClient(config, overrides = {}) {
757
688
  url: prepared.url,
758
689
  timeoutMs: prepared.timeoutMs
759
690
  });
760
- const message = errorMessage(error);
761
- throw new NetworkError(`Could not reach Metabase: ${message}`, {
762
- method: prepared.method,
763
- url: prepared.url,
764
- cause: message
765
- });
691
+ throw buildNetworkError(error, prepared.method, prepared.url);
766
692
  }
767
693
  const canRetryStatus = hasRetriesLeft && prepared.idempotent;
768
694
  if (!response.ok && isRetryableStatus(response.status) && canRetryStatus) {
@@ -778,6 +704,7 @@ function createClient(config, overrides = {}) {
778
704
  }
779
705
  if (!response.ok) {
780
706
  const rawBody = await readBodyForError(response);
707
+ const serverTag = await getServerTag();
781
708
  throw new HttpError({
782
709
  status: response.status,
783
710
  statusText: response.statusText,
@@ -785,6 +712,7 @@ function createClient(config, overrides = {}) {
785
712
  url: prepared.url,
786
713
  responseHeaders: response.headers,
787
714
  rawBody,
715
+ serverTag,
788
716
  redactionContext
789
717
  });
790
718
  }
@@ -795,13 +723,7 @@ function createClient(config, overrides = {}) {
795
723
  };
796
724
  }
797
725
  async function executeRaw(prepared) {
798
- let attempt = 0;
799
- while (true) {
800
- const result = await attemptOnce(prepared, attempt);
801
- if (result.kind === "success") return result.response;
802
- await sleep(result.delayMs, prepared.callerSignal);
803
- attempt += 1;
804
- }
726
+ return runWithRetries((attempt) => attemptOnce(prepared, attempt), prepared.callerSignal);
805
727
  }
806
728
  function prepare(path, opts = {}) {
807
729
  const method = opts.method ?? "GET";
@@ -847,14 +769,12 @@ function createClient(config, overrides = {}) {
847
769
  try {
848
770
  return parseJson(text, schema, { source: prepared.url });
849
771
  } catch (error) {
850
- if (error instanceof ConfigError) throw new HttpError({
851
- status: response.status,
852
- statusText: response.statusText,
772
+ if (error instanceof ValidationError) throw new ResponseShapeError({
853
773
  method: prepared.method,
854
774
  url: prepared.url,
855
- responseHeaders: response.headers,
856
- rawBody: text,
857
- redactionContext
775
+ status: response.status,
776
+ zodIssues: error.developerDetail.zodIssues,
777
+ serverTag: await getServerTag()
858
778
  });
859
779
  throw error;
860
780
  }
@@ -908,6 +828,50 @@ function throwContentTypeMismatch(response, prepared, expected) {
908
828
  overrideUserMessage: `Expected ${expected} response but got ${actual}`
909
829
  });
910
830
  }
831
+ const NETWORK_HINTS = {
832
+ ECONNREFUSED: (t) => `Connection refused by ${t.host} — is Metabase running and is the port correct?`,
833
+ ENOTFOUND: (t) => `Host not found: ${t.hostname} — check the URL.`,
834
+ EAI_AGAIN: (t) => `Could not resolve ${t.hostname} — check your network connection and the URL.`,
835
+ ECONNRESET: (t) => `Connection to ${t.host} was reset — the server may have closed it, or http/https may be mismatched.`,
836
+ ETIMEDOUT: (t) => `Connection to ${t.host} timed out — check the host, port, and your network.`
837
+ };
838
+ const TLS_ERROR_CODES = new Set([
839
+ "EPROTO",
840
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
841
+ "SELF_SIGNED_CERT_IN_CHAIN",
842
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
843
+ "CERT_HAS_EXPIRED",
844
+ "ERR_TLS_CERT_ALTNAME_INVALID"
845
+ ]);
846
+ function buildNetworkError(error, method, url) {
847
+ const fallback = errorMessage(error);
848
+ const code = causeCode(error);
849
+ const target = networkTarget(url);
850
+ return new NetworkError(networkMessage(code, target, fallback), {
851
+ method,
852
+ url,
853
+ cause: code ?? fallback
854
+ });
855
+ }
856
+ function networkMessage(code, target, fallback) {
857
+ if (code === null) return `Could not reach Metabase: ${fallback}`;
858
+ if (TLS_ERROR_CODES.has(code)) return `Could not reach Metabase: TLS error contacting ${target.host} (${code}) — the certificate could not be verified, or https:// was used against a plain-HTTP server.`;
859
+ const hint = NETWORK_HINTS[code];
860
+ if (hint === void 0) return `Could not reach Metabase: ${fallback} (${code})`;
861
+ return `Could not reach Metabase: ${hint(target)}`;
862
+ }
863
+ function causeCode(error) {
864
+ const cause = error instanceof Error ? error.cause : void 0;
865
+ if (cause instanceof Error && "code" in cause && typeof cause.code === "string") return cause.code;
866
+ return null;
867
+ }
868
+ function networkTarget(url) {
869
+ const parsed = new URL(url);
870
+ return {
871
+ host: parsed.host,
872
+ hostname: parsed.hostname
873
+ };
874
+ }
911
875
  async function readBodyForError(response) {
912
876
  try {
913
877
  const buffer = Buffer.from(await response.arrayBuffer());
@@ -918,14 +882,159 @@ async function readBodyForError(response) {
918
882
  }
919
883
 
920
884
  //#endregion
921
- //#region src/output/error.ts
922
- function reportError(error) {
923
- const handled = toMetabaseError(error);
924
- process.stderr.write(handled.userMessage + "\n");
925
- if (process.env[VERBOSE_ENV] === "1" && handled.developerDetail !== null) process.stderr.write(JSON.stringify(handled.developerDetail, null, 2) + "\n");
926
- process.exitCode = handled.exitCode;
885
+ //#region src/core/version/probe.ts
886
+ const PROBE_PATH = "/api/session/properties";
887
+ const PROBE_TIMEOUT_MS = 1e4;
888
+ const EMPTY_SERVER_INFO = Object.freeze({
889
+ version: null,
890
+ tokenFeatures: null
891
+ });
892
+ async function probeServer(client, opts = {}) {
893
+ const properties = await client.requestParsed(SessionProperties, PROBE_PATH, {
894
+ timeoutMs: PROBE_TIMEOUT_MS,
895
+ retries: opts.retries ?? 0
896
+ });
897
+ const version = tryParseTag(properties.version.tag);
898
+ return {
899
+ version,
900
+ tokenFeatures: properties["token-features"] ?? null
901
+ };
902
+ }
903
+
904
+ //#endregion
905
+ //#region src/core/url.ts
906
+ function normalizeUrl(input) {
907
+ const trimmed = input.trim().replace(/\/+$/, "");
908
+ if (!/^https?:\/\//i.test(trimmed)) throw new ConfigError("URL must start with http:// or https://");
909
+ return trimmed;
910
+ }
911
+ function originOnly(input) {
912
+ const parsed = new URL(input);
913
+ parsed.username = "";
914
+ parsed.password = "";
915
+ return parsed.origin;
916
+ }
917
+ function localUrl(port) {
918
+ return `http://localhost:${port}`;
919
+ }
920
+
921
+ //#endregion
922
+ //#region src/core/config.ts
923
+ const ENV_URL = "METABASE_URL";
924
+ const ENV_API_KEY = "METABASE_API_KEY";
925
+ const ENV_PROFILE = "METABASE_PROFILE";
926
+ const ENV_LICENSE_TOKEN = "METABASE_LICENSE_TOKEN";
927
+ const ENV_SKIP_PREFLIGHT = "METABASE_CLI_SKIP_PREFLIGHT";
928
+ function isPreflightSkipped() {
929
+ return process.env[ENV_SKIP_PREFLIGHT] === "1";
930
+ }
931
+ function resolveProfileName(profileFlag) {
932
+ return explicitProfileName(profileFlag) ?? DEFAULT_PROFILE;
933
+ }
934
+ function explicitProfileName(profileFlag) {
935
+ return profileFlag || process.env[ENV_PROFILE] || null;
936
+ }
937
+ function readEnvCredentials() {
938
+ return {
939
+ url: process.env[ENV_URL] ?? null,
940
+ apiKey: process.env[ENV_API_KEY] ?? null
941
+ };
942
+ }
943
+ function readEnvLicenseToken() {
944
+ return process.env[ENV_LICENSE_TOKEN] ?? null;
945
+ }
946
+ async function resolveConfig(flags) {
947
+ const profile = resolveProfileName(flags.profile);
948
+ const env = readEnvCredentials();
949
+ const flagUrl = flags.url;
950
+ const flagKey = flags.apiKey;
951
+ const needsStored = !flagUrl && !env.url || !flagKey && !env.apiKey;
952
+ const stored = needsStored ? await readProfile(profile) : null;
953
+ const urlField = pickField(flagUrl, env.url, stored?.url);
954
+ const keyField = pickField(flagKey, env.apiKey, stored?.apiKey);
955
+ if (urlField === null || keyField === null) {
956
+ const hint = await failureHintForProfile(profile);
957
+ throw new ConfigError(`Not authenticated for profile "${profile}". Run \`mb auth login\`, set ${ENV_URL}/${ENV_API_KEY}, or pass --url/--api-key.${hint}`);
958
+ }
959
+ return {
960
+ url: normalizeUrl(urlField.value),
961
+ apiKey: keyField.value,
962
+ profile,
963
+ source: urlField.source === keyField.source ? urlField.source : "mixed"
964
+ };
965
+ }
966
+ async function resolveLicenseToken(flags) {
967
+ const flag = flags.token;
968
+ const env = readEnvLicenseToken();
969
+ const stored = !flag && !env ? await readLicense() : null;
970
+ const value = flag ?? env ?? stored;
971
+ if (!value) throw new ConfigError(`No license token. Pass --token, set ${ENV_LICENSE_TOKEN}, or store one with \`mb workspace license set\`.`);
972
+ return value;
973
+ }
974
+ async function failureHintForProfile(profile) {
975
+ const record = await readProfileRecord(profile);
976
+ if (record === null || record.lastFailure === null) return "";
977
+ if (record.lastProbe !== null && record.lastProbe.at >= record.lastFailure.at) return "";
978
+ return ` profile "${profile}" last verify failed: ${record.lastFailure.reason}. Run \`mb auth login --profile ${profile}\` to update the token.`;
979
+ }
980
+ function pickField(flag, env, stored) {
981
+ if (flag) return {
982
+ value: flag,
983
+ source: "flag"
984
+ };
985
+ if (env) return {
986
+ value: env,
987
+ source: "env"
988
+ };
989
+ if (stored) return {
990
+ value: stored,
991
+ source: "stored"
992
+ };
993
+ return null;
994
+ }
995
+
996
+ //#endregion
997
+ //#region src/core/version/capabilities.ts
998
+ function mergeCapabilities(overrides) {
999
+ if (overrides === void 0) return BASELINE_CAPABILITIES;
1000
+ return {
1001
+ minVersion: overrides.minVersion ?? BASELINE_CAPABILITIES.minVersion,
1002
+ ...overrides.tokenFeature === void 0 ? {} : { tokenFeature: overrides.tokenFeature }
1003
+ };
1004
+ }
1005
+ function checkCapabilities(info, required) {
1006
+ if (info.version === null) return {
1007
+ reason: "unknown-version",
1008
+ detail: "Could not detect Metabase server version. Proceeding without preflight check; failures may produce confusing errors."
1009
+ };
1010
+ if (info.version.major < required.minVersion) return {
1011
+ reason: "version-too-old",
1012
+ detail: `This command requires Metabase v${required.minVersion}+ (this server is ${info.version.tag}). Upgrade Metabase or pin mb-cli to an older release.`
1013
+ };
1014
+ if (required.tokenFeature !== void 0) {
1015
+ const enabled = info.tokenFeatures?.[required.tokenFeature] === true;
1016
+ if (!enabled) return {
1017
+ reason: "missing-token-feature",
1018
+ detail: `This command requires the '${required.tokenFeature}' premium feature (not enabled on this server).`
1019
+ };
1020
+ }
1021
+ return null;
927
1022
  }
928
1023
 
1024
+ //#endregion
1025
+ //#region src/core/version/preflight-error.ts
1026
+ var CapabilityError = class extends MetabaseError {
1027
+ category = "capability";
1028
+ isRetryable = false;
1029
+ exitCode = 2;
1030
+ developerDetail;
1031
+ constructor(failure) {
1032
+ super(failure.detail);
1033
+ this.name = "CapabilityError";
1034
+ this.developerDetail = failure;
1035
+ }
1036
+ };
1037
+
929
1038
  //#endregion
930
1039
  //#region src/output/format.ts
931
1040
  function resolveFormat({ json, format, isTty }) {
@@ -969,6 +1078,21 @@ function parseEnum(raw, schema, flagName) {
969
1078
  return result.data;
970
1079
  }
971
1080
 
1081
+ //#endregion
1082
+ //#region src/commands/parse-integer.ts
1083
+ const INTEGER_PATTERN = /^-?\d+$/;
1084
+ function parseInteger(value, options) {
1085
+ const trimmed = value.trim();
1086
+ if (!INTEGER_PATTERN.test(trimmed)) throw new ConfigError(`invalid ${options.name}: "${value}" (expected integer)`);
1087
+ const parsed = Number.parseInt(trimmed, 10);
1088
+ if (parsed < options.min) throw new ConfigError(`invalid ${options.name}: ${parsed} (must be ≥ ${options.min})`);
1089
+ return parsed;
1090
+ }
1091
+ function parseOptionalInteger(value, options) {
1092
+ if (value === void 0 || value === "") return null;
1093
+ return parseInteger(value, options);
1094
+ }
1095
+
972
1096
  //#endregion
973
1097
  //#region src/commands/context.ts
974
1098
  function resolveCommonFlags(args, options = {}) {
@@ -987,7 +1111,8 @@ function resolveCommonFlags(args, options = {}) {
987
1111
  maxBytes: parseMaxBytes(args.maxBytes),
988
1112
  url: args.url,
989
1113
  apiKey: args.apiKey,
990
- profile: args.profile
1114
+ profile: args.profile,
1115
+ skipPreflight: args.skipPreflight === true
991
1116
  };
992
1117
  }
993
1118
  function parseFields(value) {
@@ -1002,48 +1127,151 @@ function parseMaxBytes(value) {
1002
1127
  });
1003
1128
  }
1004
1129
 
1130
+ //#endregion
1131
+ //#region src/commands/known-flags.ts
1132
+ const ARGUMENT_SEPARATOR = "--";
1133
+ const NEGATION_PREFIX = "no-";
1134
+ const BUILTIN_FLAGS = [
1135
+ "help",
1136
+ "h",
1137
+ "version",
1138
+ "v"
1139
+ ];
1140
+ function assertKnownFlags(rawArgs, argsDef) {
1141
+ const allowed = allowedFlagKeys(argsDef);
1142
+ let index = 0;
1143
+ while (index < rawArgs.length) {
1144
+ const token = rawArgs[index];
1145
+ if (token === void 0 || token === ARGUMENT_SEPARATOR) return;
1146
+ if (!isFlagToken(token)) {
1147
+ index += 1;
1148
+ continue;
1149
+ }
1150
+ const matched = flagCandidates(token).some((candidate) => allowed.has(candidate));
1151
+ if (!matched) throw new ConfigError(`unknown flag: ${displayFlag(token)}`);
1152
+ index += flagConsumesValue(token, argsDef) ? 2 : 1;
1153
+ }
1154
+ }
1155
+ function allowedFlagKeys(argsDef) {
1156
+ const keys = new Set(BUILTIN_FLAGS.map(normalizeFlag));
1157
+ for (const [name, def] of Object.entries(argsDef)) {
1158
+ keys.add(normalizeFlag(name));
1159
+ if ("alias" in def) for (const alias of toAliasArray(def.alias)) keys.add(normalizeFlag(alias));
1160
+ }
1161
+ return keys;
1162
+ }
1163
+ function isFlagToken(token) {
1164
+ return token.startsWith("-") && token !== "-";
1165
+ }
1166
+ function displayFlag(token) {
1167
+ const equals = token.indexOf("=");
1168
+ return equals === -1 ? token : token.slice(0, equals);
1169
+ }
1170
+ function flagCandidates(token) {
1171
+ const name = displayFlag(token).replace(/^-+/, "");
1172
+ const candidates = [normalizeFlag(name)];
1173
+ if (name.startsWith(NEGATION_PREFIX)) candidates.push(normalizeFlag(name.slice(NEGATION_PREFIX.length)));
1174
+ return candidates;
1175
+ }
1176
+
1005
1177
  //#endregion
1006
1178
  //#region src/commands/runtime.ts
1007
1179
  function defineMetabaseCommand(def) {
1180
+ const required = def.capabilities === null ? null : mergeCapabilities(def.capabilities);
1008
1181
  const cmd = defineCommand({
1009
1182
  meta: def.meta,
1010
1183
  args: def.args,
1011
- async run({ args }) {
1184
+ async run({ args, rawArgs }) {
1185
+ let reportFormat;
1012
1186
  try {
1013
1187
  const ctx = resolveCommonFlags(pickCommonArgs(args));
1188
+ reportFormat = ctx.format;
1189
+ assertKnownFlags(rawArgs, def.args);
1014
1190
  let cachedConfig = null;
1015
1191
  let cachedClient = null;
1192
+ let cachedServerInfo = null;
1016
1193
  const getResolvedConfig = async () => {
1017
1194
  if (cachedConfig === null) cachedConfig = await resolveConfig(buildConfigFlags(ctx));
1018
1195
  return cachedConfig;
1019
1196
  };
1020
- const getClient = async () => {
1197
+ const getServerInfo = () => {
1198
+ if (cachedServerInfo === null) cachedServerInfo = loadServerInfo(getResolvedConfig);
1199
+ return cachedServerInfo;
1200
+ };
1201
+ const rawGetClient = async () => {
1021
1202
  if (cachedClient === null) {
1022
1203
  const resolved = await getResolvedConfig();
1023
1204
  cachedClient = createClient({
1024
1205
  url: resolved.url,
1025
1206
  apiKey: resolved.apiKey
1026
- });
1207
+ }, { getServerTag: async () => (await getServerInfo())?.version?.tag ?? null });
1027
1208
  }
1028
1209
  return cachedClient;
1029
1210
  };
1030
- await def.run({
1031
- args,
1032
- ctx,
1033
- getClient,
1034
- getResolvedConfig
1035
- });
1211
+ const enforcePreflight = createPreflightEnforcer(required, getServerInfo, ctx.skipPreflight);
1212
+ const getClient = async () => {
1213
+ const client = await rawGetClient();
1214
+ await enforcePreflight();
1215
+ return client;
1216
+ };
1217
+ try {
1218
+ await def.run({
1219
+ args,
1220
+ ctx,
1221
+ getClient,
1222
+ getResolvedConfig,
1223
+ getServerInfo
1224
+ });
1225
+ } finally {
1226
+ emitLegacyStorageWarningIfPending();
1227
+ }
1036
1228
  } catch (error) {
1037
- reportError(error);
1229
+ reportError(error, reportFormat);
1038
1230
  }
1039
1231
  }
1040
1232
  });
1041
1233
  setMetabaseAugment(cmd, {
1042
1234
  examples: def.examples ?? [],
1043
- outputSchema: def.outputSchema ?? null
1235
+ details: def.details ? def.details : null,
1236
+ outputSchema: def.outputSchema ?? null,
1237
+ capabilities: required
1044
1238
  });
1045
1239
  return cmd;
1046
1240
  }
1241
+ function emitLegacyStorageWarningIfPending() {
1242
+ const message = consumeLegacyStorageWarning();
1243
+ if (message !== null) warn(message);
1244
+ }
1245
+ async function loadServerInfo(getResolvedConfig) {
1246
+ const resolved = await getResolvedConfig();
1247
+ const record = await readProfileRecord(resolved.profile);
1248
+ if (record === null || record.lastProbe === null) return null;
1249
+ return {
1250
+ version: record.lastProbe.version,
1251
+ tokenFeatures: record.lastProbe.tokenFeatures
1252
+ };
1253
+ }
1254
+ const NO_OP_ENFORCER = async () => {};
1255
+ const PROBE_HINT = " Run `mb auth list` (or `mb auth login`) to populate the version cache.";
1256
+ function createPreflightEnforcer(required, getServerInfo, skip) {
1257
+ if (required === null || skip || isPreflightSkipped() || isBaseline(required)) return NO_OP_ENFORCER;
1258
+ let done = false;
1259
+ return async () => {
1260
+ if (done) return;
1261
+ done = true;
1262
+ const info = await getServerInfo() ?? EMPTY_SERVER_INFO;
1263
+ const failure = checkCapabilities(info, required);
1264
+ if (failure === null) return;
1265
+ if (failure.reason === "unknown-version") {
1266
+ warn(failure.detail + PROBE_HINT);
1267
+ return;
1268
+ }
1269
+ throw new CapabilityError(failure);
1270
+ };
1271
+ }
1272
+ function isBaseline(caps) {
1273
+ return caps.minVersion === BASELINE_CAPABILITIES.minVersion && caps.tokenFeature === void 0;
1274
+ }
1047
1275
  function pickCommonArgs(args) {
1048
1276
  const out = {};
1049
1277
  if (typeof args["format"] === "string") out.format = args["format"];
@@ -1054,6 +1282,7 @@ function pickCommonArgs(args) {
1054
1282
  if (typeof args["profile"] === "string") out.profile = args["profile"];
1055
1283
  if (typeof args["url"] === "string") out.url = args["url"];
1056
1284
  if (typeof args["apiKey"] === "string") out.apiKey = args["apiKey"];
1285
+ if (typeof args["skipPreflight"] === "boolean") out.skipPreflight = args["skipPreflight"];
1057
1286
  return out;
1058
1287
  }
1059
1288
  function buildConfigFlags(ctx) {
@@ -1065,4 +1294,4 @@ function buildConfigFlags(ctx) {
1065
1294
  }
1066
1295
 
1067
1296
  //#endregion
1068
- export { DEFAULT_PROFILE, HttpError, USER_AGENT, account, clearLicense, clearProfile, clearRejection, combineAborts, connectionFlags, createClient, credentials, defineMetabaseCommand, explicitProfileName, listEnvelopeSchema, listProfileNames, localUrl, normalizeUrl, originOnly, outputFlags, parseCsv, parseEnum, parseEnumCsv, parseInteger, parseJson, parseJsonOrPlain, parseOptionalInteger, profileFlag, readEnvCredentials, readEnvLicenseToken, readProfile, recordRejection, resolveLicenseToken, resolveProfileName, throwIfAborted, wrapList, writeLicense, writeProfile };
1297
+ export { DEFAULT_PROFILE, HttpError, ParsedVersionSchema, ProbedUser, ProfileLastFailure, TokenFeatures, USER_AGENT, clearLicense, clearProfile, combineAborts, createClient, defineMetabaseCommand, explicitProfileName, keyringFallbackWarning, listProfileRecords, localUrl, normalizeUrl, originOnly, parseCsv, parseEnum, parseEnumCsv, parseInteger, parseJson, parseJsonOrPlain, parseOptionalInteger, probeServer, readEnvCredentials, readEnvLicenseToken, readLicense, readProfile, readProfileRecord, resolveLicenseToken, resolveProfileName, throwIfAborted, writeLicense, writeProbeFailure, writeProbeResult, writeProfile };