@metabase/cli 0.1.5 → 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.
- package/README.md +115 -102
- package/dist/{add-collection-C_iovi9i.mjs → add-collection-BU8r3r2M.mjs} +9 -4
- package/dist/add-collection-C0w6ACQF.mjs +11 -0
- package/dist/{archive-Dvzrmdbk.mjs → archive-BNinrUak.mjs} +9 -8
- package/dist/{archive-WaEW85NB.mjs → archive-C1enZgKV.mjs} +8 -7
- package/dist/archive-CDA0KxL8.mjs +40 -0
- package/dist/{archive-BKPO8lEO.mjs → archive-CRhiBpPJ.mjs} +9 -8
- package/dist/{archive-DdaP94H3.mjs → archive-DMPS8Kih.mjs} +9 -8
- package/dist/archive-lWgqiFAt.mjs +40 -0
- package/dist/auth-CzXb_zB2.mjs +19 -0
- package/dist/{body-XtR7-uCO.mjs → body-DjdFxjpg.mjs} +4 -4
- package/dist/{branches-XUY4JY-X.mjs → branches-B1WRfG7-.mjs} +11 -7
- package/dist/{cancel-BrUVO_ax.mjs → cancel-Dl_Ho056.mjs} +7 -6
- package/dist/{cancel-task-oXheTOB6.mjs → cancel-task-CdigdCaO.mjs} +11 -7
- package/dist/capabilities-7e9MgquN.mjs +29 -0
- package/dist/card-DP4rfoOi.mjs +21 -0
- package/dist/{card-CQxvHeyP.mjs → card-DlCAaAPq.mjs} +1 -1
- package/dist/{cards-CONTTAG9.mjs → cards-BGiJS675.mjs} +8 -7
- package/dist/cli.mjs +264 -44
- package/dist/collection-tY18ezvn.mjs +21 -0
- package/dist/{predicates-CGO17Q15.mjs → command-augment-BH9qgQ5u.mjs} +66 -14
- package/dist/create-BNiva__H.mjs +52 -0
- package/dist/{create-Ca9lIDwP.mjs → create-BTcpaop_.mjs} +9 -8
- package/dist/{create-V-q2rU0T.mjs → create-BYlIju0b.mjs} +14 -12
- package/dist/{create-DZxUeqdf.mjs → create-Be_0Vier.mjs} +10 -9
- package/dist/{create-kYpjobrq.mjs → create-CHF313Qg.mjs} +13 -9
- package/dist/{create-swbIXdo5.mjs → create-CwGtmwqm.mjs} +14 -12
- package/dist/{create-Dq25vsMu.mjs → create-CzzrbL0u.mjs} +10 -9
- package/dist/{create-Le3Bqn7b.mjs → create-DGth_uOp.mjs} +14 -12
- package/dist/{create-branch-D5u14AxL.mjs → create-branch-DKZkoQ64.mjs} +11 -7
- package/dist/{create-Cs2xntFG.mjs → create-dhxPxfF3.mjs} +16 -14
- package/dist/{credentials-BIQ1cEzM.mjs → credentials-dzeq7ckm.mjs} +12 -11
- package/dist/{current-task-DCq7rk9V.mjs → current-task-CCRzm0_7.mjs} +11 -7
- package/dist/dashboard-ChM_Tu0l.mjs +22 -0
- package/dist/{dashboard-CnMD04PQ.mjs → dashboard-FY5UzJ_Z.mjs} +2 -1
- package/dist/{database-BSvzYlRe.mjs → database-CIXwHKjK.mjs} +3 -3
- package/dist/{database-vvig8k4x.mjs → database-lH-B3G1I.mjs} +1 -1
- package/dist/db-DrQn_i3W.mjs +22 -0
- package/dist/{remove-C6bS0Z6w.mjs → delete-CM3jnAeQ.mjs} +21 -20
- package/dist/{delete-CUx6RT9e.mjs → delete-Dimc-2y8.mjs} +9 -8
- package/dist/{delete-VTAS9EUt.mjs → delete-ZjnV35OJ.mjs} +9 -8
- package/dist/{delete-runtime-DfFMWJJ6.mjs → delete-runtime-B6RQo_pw.mjs} +5 -3
- package/dist/{delete-table-DzUneMKe.mjs → delete-table-agZJpivt.mjs} +9 -8
- package/dist/{deprovision-CpJfGgCt.mjs → deprovision-CwxcIT3k.mjs} +16 -12
- package/dist/{dirty-nkAOXxgC.mjs → dirty-D4d0yHqj.mjs} +11 -7
- package/dist/{docker-D5FTIoD0.mjs → docker-Oq80q3tu.mjs} +4 -4
- package/dist/{translate-Cqsd0Px5.mjs → eid-BXzaQh0o.mjs} +37 -22
- package/dist/error-C9S6PN3-.mjs +190 -0
- package/dist/{export-BWvY7X_G.mjs → export-DTygoXBP.mjs} +17 -16
- package/dist/field-Z6Pcxf4n.mjs +19 -0
- package/dist/{fields-dH16G5UV.mjs → fields-CoQi99gv.mjs} +9 -8
- package/dist/{get-BnBRKHr7.mjs → get-Bzys7vgp.mjs} +8 -7
- package/dist/{get-B7i_nYJB.mjs → get-C2p383Qc.mjs} +8 -7
- package/dist/{get-D96QEU49.mjs → get-C3HdQ91a.mjs} +8 -7
- package/dist/{get-DNN1X2gN.mjs → get-CP3Z3NiH.mjs} +9 -8
- package/dist/{get-CACaBFLt.mjs → get-C_w1kvN3.mjs} +9 -8
- package/dist/{get-D8e_RzZ0.mjs → get-CzuzeKSe.mjs} +10 -9
- package/dist/{get-C6SR3A9t.mjs → get-D3SbEQSE.mjs} +10 -9
- package/dist/{get-7macOPAI.mjs → get-DFxZXaKz.mjs} +7 -7
- package/dist/{get-DAWofnzK.mjs → get-DQTZG_NP.mjs} +8 -7
- package/dist/{get-BcqxMVC1.mjs → get-DSWFjy7O.mjs} +8 -7
- package/dist/{get-R7OaVL_t.mjs → get-Ddr0XLh7.mjs} +8 -7
- package/dist/{get-B08K82JV.mjs → get-Hc93A0Yz.mjs} +8 -7
- package/dist/{get-CKxlhMy1.mjs → get-lb7q3JYs.mjs} +7 -6
- package/dist/get-run-B7sKdaDU.mjs +38 -0
- package/dist/git-sync-CiGAad76.mjs +28 -0
- package/dist/{has-remote-changes-BAnIXQXU.mjs → has-remote-changes-BY10-nnE.mjs} +11 -7
- package/dist/{import-CfdPEMng.mjs → import-CiMz4Wz-.mjs} +17 -16
- package/dist/{input-BQ-BZA8h.mjs → input-cMSEqISy.mjs} +7 -4
- package/dist/{is-dirty-CZWcG0vj.mjs → is-dirty-BZOaryxT.mjs} +9 -4
- package/dist/is-dirty-Ume4oV0j.mjs +10 -0
- package/dist/{items-DqwahOKf.mjs → items-BWfvkY-J.mjs} +9 -8
- package/dist/key-C2XG394c.mjs +17 -0
- package/dist/license-Dxarh-gG.mjs +17 -0
- package/dist/{list-vF4EneaE.mjs → list--OYdUTtu.mjs} +7 -6
- package/dist/{list-yxVAE1S7.mjs → list-2j7GsXsl.mjs} +7 -6
- package/dist/{list-D41gfkKb.mjs → list-BI4zr8LW.mjs} +10 -8
- package/dist/{list-BpNU1neq.mjs → list-Brgh-Z2v.mjs} +8 -6
- package/dist/{list-ViT2KWhv.mjs → list-C3hfovHv.mjs} +7 -6
- package/dist/{list-CQkDqphl.mjs → list-CL7eCOQE.mjs} +7 -6
- package/dist/{list-L63TpX1t.mjs → list-Clz5igWg.mjs} +7 -7
- package/dist/list-D4sFiqX8.mjs +173 -0
- package/dist/{list-oftHLFbE.mjs → list-DXH7TlkU.mjs} +9 -7
- package/dist/{list-BqNMpIXy.mjs → list-DZ8fNUoQ.mjs} +9 -8
- package/dist/{list-Bkd7Nbds.mjs → list-SOG0whQ-.mjs} +7 -6
- package/dist/{list-J277Qtki.mjs → list-d58BprgJ.mjs} +7 -6
- package/dist/{list-DJcGwJ4W.mjs → list-sD5N3fGk.mjs} +9 -8
- package/dist/{list-DBOYoJtA.mjs → list-zSO0DMw-.mjs} +10 -6
- package/dist/{login-D1nZwgKv.mjs → login-Bm2AnCez.mjs} +65 -80
- package/dist/{logout-DD4q5whi.mjs → logout-BlyRJODO.mjs} +8 -7
- package/dist/{logs-Ci3mJE2z.mjs → logs-CywPikkL.mjs} +9 -8
- package/dist/{manifest-CGM7XNLC.mjs → manifest-BBR46KFM.mjs} +15 -15
- package/dist/measure-C44EK_xt.mjs +20 -0
- package/dist/{measure-BEQfnLdN.mjs → measure-ClESGxIb.mjs} +2 -2
- package/dist/{metadata-BDat-jN9.mjs → metadata-B8ZSF9LA.mjs} +10 -9
- package/dist/{metadata-29_qlqbz.mjs → metadata-DqiI2q9q.mjs} +9 -8
- package/dist/parse-enum-CrEWOhuY.mjs +11 -0
- package/dist/{parse-id-CysSaCbf.mjs → parse-id-lk_K-CEF.mjs} +1 -1
- package/dist/{parse-ref-D1yeDOn8.mjs → parse-ref-BiETXmvm.mjs} +1 -1
- package/dist/{parse-schemas-B10n01ez.mjs → parse-schemas-BqUdWUwq.mjs} +2 -2
- package/dist/{path-DLByFMMA.mjs → path-AEtZ3mBq.mjs} +7 -7
- package/dist/{poll-p9Y7-JEQ.mjs → poll-DHKDpCiq.mjs} +2 -2
- package/dist/{poll-task-BQe0NvJZ.mjs → poll-task-Cooi0lQV.mjs} +3 -20
- package/dist/{preflight-CvFu0Cct.mjs → preflight-aXV5LyDs.mjs} +4 -4
- package/dist/{process-zJeVJZTM.mjs → process-C7V8LJ-j.mjs} +1 -1
- package/dist/{prompt-DgDNy_Pc.mjs → prompt-CFKoys7k.mjs} +3 -1
- package/dist/{provision-BP-b4Are.mjs → provision-UWcNDoDe.mjs} +29 -24
- package/dist/{ps-BxQdpkr5.mjs → ps-CJU0EbrC.mjs} +5 -3
- package/dist/ps-DEroLgbI.mjs +11 -0
- package/dist/{query-CFH4nBlK.mjs → query-AaKzYnTY.mjs} +9 -8
- package/dist/{query-C7zTlFJA.mjs → query-BlsVNZpD.mjs} +15 -13
- package/dist/{remove-BuWxx3hY.mjs → remove-BFWun0e8.mjs} +9 -8
- package/dist/{remove-collection-Bc4roCq0.mjs → remove-collection-CoCmrrQs.mjs} +13 -9
- package/dist/{render-DuoDUTVL.mjs → render-CfznwleY.mjs} +15 -17
- package/dist/render-OQn3iRsI.mjs +32 -0
- package/dist/{rescan-values-DabyRYQ_.mjs → rescan-values-C0FDsjT7.mjs} +10 -9
- package/dist/{run-Cl-9RtC4.mjs → run-B4Wn43zm.mjs} +10 -9
- package/dist/{runs-BH6s1Zao.mjs → runs-Bbaszr18.mjs} +9 -8
- package/dist/{runtime-CDu6fykq.mjs → runtime-Dmv5VtUK.mjs} +657 -428
- package/dist/{schema-tables-i58wp_p3.mjs → schema-tables-CaWinbuK.mjs} +9 -8
- package/dist/{schemas-_m8RYRl9.mjs → schemas-DUgGpAyB.mjs} +7 -6
- package/dist/{search-DObOsjbP.mjs → search-BLrBXLUk.mjs} +12 -16
- package/dist/segment-B3Uwwcsm.mjs +20 -0
- package/dist/{set-CJA9dpK6.mjs → set-B8cUbRLD.mjs} +13 -12
- package/dist/{set-CwVWeAsi.mjs → set-DfGsta5O.mjs} +11 -10
- package/dist/{setting-Czy4ws6h.mjs → setting-D2p2MA7f.mjs} +3 -3
- package/dist/{setup-DqBOe3HZ.mjs → setup-C9ikBRw_.mjs} +9 -8
- package/dist/{skills-C2rTVj0n.mjs → skills-CUHIcQS6.mjs} +3 -3
- package/dist/{skills-CHU7uuDU.mjs → skills-CiN1OQ8W.mjs} +2 -2
- package/dist/snippet-B7D0uWlz.mjs +20 -0
- package/dist/{start-CfruN4wF.mjs → start-3PX3ahjT.mjs} +68 -37
- package/dist/{stash-CWuXKSZq.mjs → stash-EIDcSvpF.mjs} +17 -16
- package/dist/{status-D-RYZB9G.mjs → status-95ElRAu9.mjs} +12 -8
- package/dist/status-B0_MiZEf.mjs +100 -0
- package/dist/status-CEplmC44.mjs +34 -0
- package/dist/{stop-D8Hr4cKX.mjs → stop-CQ0XGrN8.mjs} +11 -10
- package/dist/{summary-Lt2XLBK9.mjs → summary-C12LiEuJ.mjs} +8 -7
- package/dist/{sync-schema-BDElSynU.mjs → sync-schema-Ba8M3DiX.mjs} +10 -9
- package/dist/{table-B-PYcgGb.mjs → table-C7a5V6Zn.mjs} +1 -1
- package/dist/table-e6h8SLVX.mjs +20 -0
- package/dist/transform-BMYh1lsC.mjs +25 -0
- package/dist/transform-job-Cm7z5TfH.mjs +20 -0
- package/dist/{transform-job-BrhOLO4M.mjs → transform-job-DeTDPMxt.mjs} +1 -1
- package/dist/{tree-DfvjDjmk.mjs → tree-Des2ZG9d.mjs} +6 -5
- package/dist/{update-CqnDMNtZ.mjs → update-Bx54nWEI.mjs} +17 -15
- package/dist/{update-D9Z8cL7h.mjs → update-CyIZdbIQ.mjs} +11 -10
- package/dist/{update-CVxOxmt6.mjs → update-DBi5U8zb.mjs} +16 -14
- package/dist/{update-BYduslhn.mjs → update-DHZubok3.mjs} +18 -14
- package/dist/{update-BgcroYkF.mjs → update-DSgceARZ.mjs} +11 -10
- package/dist/{update-zp7pCBZH.mjs → update-DzAN4SPj.mjs} +15 -13
- package/dist/{update-qnFY5IuC.mjs → update-F6DmZncY.mjs} +11 -10
- package/dist/{update-B0bjPqKC.mjs → update-_QfgNa53.mjs} +12 -11
- package/dist/{update-dashcard-CQ3kmmss.mjs → update-dashcard-wpSjv4M7.mjs} +11 -10
- package/dist/{update-DzgXF082.mjs → update-mYVnoYNV.mjs} +15 -13
- package/dist/{update-DuA8-cCq.mjs → update-njHe3j-s.mjs} +15 -13
- package/dist/{upgrade-CIgTr2CG.mjs → upgrade-iAuvhX-W.mjs} +9 -8
- package/dist/{url-B5MgZXzg.mjs → url-DWaT6WIZ.mjs} +11 -10
- package/dist/{uuid-CJz9TmHI.mjs → uuid-CMKnS8-z.mjs} +8 -6
- package/dist/{validate-CB0bu50i.mjs → validate-dPEOnOf8.mjs} +2 -1
- package/dist/{validate-query-CavIA0Q2.mjs → validate-query-Cw6WE5Y8.mjs} +3 -3
- package/dist/{values-BXN6tx1i.mjs → values-BfSTAbzc.mjs} +8 -7
- package/dist/verify-D5YtTqqp.mjs +79 -0
- package/dist/{wait-BFqBlg0y.mjs → wait-8yV9_WIo.mjs} +2 -2
- package/dist/{wait-tDp9ZOou.mjs → wait-Bv3Tsnv4.mjs} +12 -8
- package/dist/{wait-flags-CN-e9zNq.mjs → wait-flags-Dzq9BGQY.mjs} +20 -9
- package/dist/workspace-CKLZrR7l.mjs +26 -0
- package/dist/{workspace-credentials-4lIxxz4g.mjs → workspace-credentials-BXpABsNZ.mjs} +2 -2
- package/dist/{yaml-ECiog374.mjs → yaml-YTQiYJ9s.mjs} +1 -1
- package/package.json +2 -1
- package/skill-data/core/SKILL.md +55 -453
- package/skill-data/git-sync/SKILL.md +1 -1
- package/skill-data/mbql/SKILL.md +156 -0
- package/skill-data/mbql/references/operators.md +253 -0
- package/skill-data/transform/SKILL.md +2 -40
- package/skill-data/viz/SKILL.md +137 -0
- package/skill-data/viz/references/settings.md +312 -0
- package/skill-data/workspace/SKILL.md +45 -63
- package/skills/metabase-cli/SKILL.md +5 -26
- package/dist/add-collection-ucsyAMkV.mjs +0 -11
- package/dist/api-key-BENHbTbV.mjs +0 -13
- package/dist/auth-DICRtJDy.mjs +0 -19
- package/dist/card-l-UmrUIo.mjs +0 -20
- package/dist/collection-oV0olVY-.mjs +0 -19
- package/dist/command-augment-D9pI9Vbh.mjs +0 -11
- package/dist/create-CrUq6sib.mjs +0 -125
- package/dist/create-D3Z878yr.mjs +0 -50
- package/dist/dashboard-hbKDd36X.mjs +0 -20
- package/dist/db-qVK6NsdB.mjs +0 -22
- package/dist/eid-CDFXX_6H.mjs +0 -13
- package/dist/field-C0LE7RQI.mjs +0 -18
- package/dist/flag-pair-Fmcdkrfx.mjs +0 -17
- package/dist/get-run-CwFuR4Uw.mjs +0 -36
- package/dist/git-sync-DV7YjniX.mjs +0 -28
- package/dist/is-dirty-LxVbm2C5.mjs +0 -10
- package/dist/key-CCJdVWKc.mjs +0 -12
- package/dist/license-Cb6ewEJO.mjs +0 -17
- package/dist/list-DV6CONhp.mjs +0 -55
- package/dist/measure-XhJuL77y.mjs +0 -19
- package/dist/package-DFUprkSZ.mjs +0 -85
- package/dist/ps-Bk6unzaX.mjs +0 -11
- package/dist/segment-DfxZdJmR.mjs +0 -19
- package/dist/snippet-BCY4KHBU.mjs +0 -19
- package/dist/status-1oUnw803.mjs +0 -56
- package/dist/status-J9HIDcA5.mjs +0 -32
- package/dist/table-BwX3Ib5f.mjs +0 -19
- package/dist/transform-iaAi37V0.mjs +0 -24
- package/dist/transform-job-Bemonf82.mjs +0 -19
- package/dist/workspace-BBsT0H0g.mjs +0 -24
- /package/dist/{body-flags-BK7J6Daz.mjs → body-flags-D7q87Btw.mjs} +0 -0
- /package/dist/{field-B3gvaqpK.mjs → field-yomXlkvl.mjs} +0 -0
- /package/dist/{paginate-CTSfuYiF.mjs → paginate-Dfm9eO9A.mjs} +0 -0
- /package/dist/{revision-message-flag-oyq2xrDU.mjs → revision-message-flag-WmsIzUOM.mjs} +0 -0
- /package/dist/{segment-BMrUBz94.mjs → segment-Be2v4ilr.mjs} +0 -0
- /package/dist/{setting-CTaAeMci.mjs → setting-oL97SNeO.mjs} +0 -0
- /package/dist/{snippet-CSWqkslB.mjs → snippet-COggaWxx.mjs} +0 -0
- /package/dist/{transform-DR4ejuPM.mjs → transform-GTW3G-01.mjs} +0 -0
- /package/dist/{workspace-DUfqhPm5.mjs → workspace-BBXJczJK.mjs} +0 -0
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
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
|
|
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
|
|
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/
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
|
211
|
-
const
|
|
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
|
|
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
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
return join(configDir(), CREDENTIALS_FILE);
|
|
146
|
+
let legacyWarningPending = false;
|
|
147
|
+
function profilesFilePath() {
|
|
148
|
+
return join(configDir(), PROFILES_FILE);
|
|
224
149
|
}
|
|
225
|
-
function
|
|
226
|
-
|
|
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
|
|
257
|
-
const path =
|
|
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))
|
|
263
|
-
|
|
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
|
-
|
|
299
|
-
|
|
300
|
-
account: key
|
|
192
|
+
profiles: [],
|
|
193
|
+
license: null
|
|
301
194
|
};
|
|
302
195
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
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
|
-
|
|
332
|
-
|
|
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
|
|
346
|
-
|
|
347
|
-
const
|
|
348
|
-
await
|
|
349
|
-
|
|
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
|
|
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
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
|
376
|
-
const path =
|
|
377
|
-
|
|
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:
|
|
232
|
+
mode: PROFILES_DIR_MODE
|
|
381
233
|
});
|
|
382
|
-
await promises.writeFile(path, JSON.stringify(
|
|
383
|
-
if (process.platform !== "win32") await promises.chmod(path,
|
|
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
|
|
413
|
-
|
|
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
|
-
|
|
416
|
-
return
|
|
241
|
+
function findRecord(file, name) {
|
|
242
|
+
return file.profiles.find((entry) => entry.name === name) ?? null;
|
|
417
243
|
}
|
|
418
|
-
|
|
419
|
-
return
|
|
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
|
-
|
|
423
|
-
|
|
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
|
|
430
|
-
const
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
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
|
|
436
|
-
|
|
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
|
-
|
|
440
|
-
|
|
441
|
-
|
|
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
|
|
449
|
-
|
|
281
|
+
async function readProfileRecord(name = DEFAULT_PROFILE) {
|
|
282
|
+
const file = await readProfilesFile();
|
|
283
|
+
return findRecord(file, name);
|
|
450
284
|
}
|
|
451
|
-
function
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
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
|
|
458
|
-
|
|
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
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
const
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
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
|
|
482
|
-
|
|
483
|
-
const
|
|
484
|
-
const
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
if (
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
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
|
-
|
|
574
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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/
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
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
|
|
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
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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
|
-
|
|
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,
|
|
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 };
|