Skip to content

chore: CLI typegen for Metric Views#459

Open
atilafassina wants to merge 5 commits into
mainfrom
mv-cli
Open

chore: CLI typegen for Metric Views#459
atilafassina wants to merge 5 commits into
mainfrom
mv-cli

Conversation

@atilafassina

@atilafassina atilafassina commented Jun 22, 2026

Copy link
Copy Markdown
Contributor

appkit mv sync CLI that runs the metric-view schema-fetch + type-emission outside the Vite dev loop — for CI, non-Vite builds, and manual refresh after pulling a teammate's metric-views.json.

Note

Dormant feature, still not user facing.

Design

The CLI lives in shared and reaches appkit's metric-sync core via dynamic import("@databricks/appkit/type-generator") — the same pattern generate-types uses — backed by an ambient declaration and a graceful appkit-absent fallback, so shared keeps no static dependency on appkit.

A new appkit syncMetricTypes export runs readMetricConfig → resolveMetricConfig → syncMetrics → write metric.d.ts + metrics.metadata.json, reusing the existing metric writers, the adaptive DESCRIBE fetcher, and the post-#433 persistent cache helpers (loadCache / saveCache / metricCacheHash). Because it is the same code path, the emitted bundle is structurally identical to the Vite plugin output. It deliberately does not generate query types or route through generateFromEntryPoint.

Behaviour

  • Validation against metricSourceSchema (post-chore: Metric Views typegen #433 UC FQN grammar) before any sync, with issues formatted as path: message.
  • Exit-code taxonomy — an absent default config/queries/metric-views.json exits 0 (dormancy invariant); explicit-missing-path / JSON-parse / schema-validation / missing-FQN / unreachable-warehouse / auth each exit non-zero with a distinct message.
  • Interactive vs non-interactive — no flags → @clack/prompts flow (mirrors plugin create); any flag → non-interactive (detected via getOptionValueSource() === "cli").
  • Flags--warehouse-id (+ DATABRICKS_WAREHOUSE_ID fallback), --metric-views-json-path, --output-dir, --no-cache (→ cache: false: ignore + overwrite the metric cache).

@github-actions

Copy link
Copy Markdown
Contributor

🔬  Run evals on this PR  ·  Go to Evals Monitor →

@atilafassina atilafassina force-pushed the mv-cli branch 4 times, most recently from b0ca822 to bef7429 Compare June 23, 2026 14:05
@atilafassina atilafassina changed the title feat(shared): appkit metric sync CLI chore: CLI typegen for Metric Views Jun 25, 2026
Adds an appkit mv sync command that fetches Unity Catalog metric-view schemas
and emits metric.d.ts plus metrics.metadata.json outside the Vite dev loop (CI,
non-Vite builds, manual refresh). The command lives in shared and reaches
appkit's sync core via dynamic import of the type-generator entry with an
ambient declaration and a graceful appkit-absent fallback, so shared keeps no
static appkit dependency. A new appkit syncMetricViewsTypes export reuses the
existing metric writers, adaptive describe fetcher and persistent cache helpers,
so the emitted bundle matches the Vite plugin output. Config is validated
against metricSourceSchema before sync, an absent default file exits zero for
dormancy while error modes exit non-zero with distinct messages, and
interactive and non-interactive flows mirror plugin create. Flags are
--warehouse-id, --metric-views-json-path, --output-dir and --no-cache. Fourth
change in the metric-views decomposition after #427, #429 and #433.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
…Path

"format" describes what the helper does — render a Zod issue path as a CLI
string — without the "humanize" framing it borrowed from the plugin-manifest
validator's humanizePath. Update its caller, tests, and doc comments to match.

Also trim redundant comments in the type-generator .d.ts shim and align the
syncMetricViewsTypes doc with the appkit source.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
`appkit mv sync` runs in describe-now mode, where a not-ready (cold or
starting) warehouse can return no schema for a metric view WITHOUT a hard
DESCRIBE failure: the appkit export writes permissive (degraded) types and
reports them via `schemas[].degraded` with an empty `failures` list. The CLI
only inspected `failures`, so this path fell through to the success message and
exited 0 with no signal, silently shipping permissive `unknown` types.

Add a degraded-schema check after the failures gate: name the affected views,
tell the user to rerun once the warehouse is available, and exit 0 — a not-ready
warehouse is transient, not a hard failure (genuine DESCRIBE failures still exit
non-zero, unchanged).

Also align the ambient `@databricks/appkit/type-generator` shim's
`SyncMetricViewsTypesResult` with the real export by adding `fatalErrors`
(always empty for the CLI's describe-now mode; declared to keep the
cross-package contract honest).

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
Rename the generated metric-view artifacts for naming consistency with the
`metric-views.json` source config (and to fix the metric/metrics singular-vs-
plural split):
  - METRIC_TYPES_FILE:    metric.d.ts           -> metric-views.d.ts
  - METRIC_METADATA_FILE: metrics.metadata.json -> metric-views.metadata.json

Touches the constants, the dev/Vite generate path, the ambient type-generator
shim, the shared `mv sync` CLI, and all test assertions across appkit + shared.
The Metric Views feature is pre-release (not shipped), so no migration is needed.

Co-authored-by: Isaac
Signed-off-by: Atila Fassina <atila@fassina.eu>
@atilafassina atilafassina marked this pull request as ready for review June 25, 2026 11:55
@atilafassina atilafassina requested a review from a team as a code owner June 25, 2026 11:55
@atilafassina atilafassina requested review from Copilot and pkosiec June 25, 2026 11:55

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new appkit mv sync CLI surface (in packages/shared) that can generate Metric View type artifacts outside the Vite dev loop, backed by a new/expanded syncMetricViewsTypes export in @databricks/appkit/type-generator and a refactor to reuse the same unified metric sync pipeline from both the CLI and generateFromEntryPoint.

Changes:

  • Add appkit mv sync command with interactive/non-interactive flows, schema validation, dormancy behavior, and cache controls.
  • Introduce/extend syncMetricViewsTypes in the appkit type-generator and refactor generateFromEntryPoint to delegate Metric View emission to it.
  • Standardize Metric View artifact filenames to metric-views.d.ts and metric-views.metadata.json, updating tests accordingly.

Reviewed changes

Copilot reviewed 13 out of 14 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/shared/src/cli/index.ts Wires the new mv parent command into the shared CLI.
packages/shared/src/cli/commands/type-generator.d.ts Extends the ambient optional @databricks/appkit/type-generator declaration with syncMetricViewsTypes + metric artifact constants.
packages/shared/src/cli/commands/metric-views/index.ts Adds the mv parent command and attaches the sync subcommand.
packages/shared/src/cli/commands/metric-views/validate-metric-views-source.ts Adds Zod-based validation and CLI-friendly formatting for metric-views.json.
packages/shared/src/cli/commands/metric-views/validate-metric-views-source.test.ts Unit tests for metric source validation + formatting.
packages/shared/src/cli/commands/metric-views/sync/sync.ts Implements appkit mv sync (path resolution, dormancy, dynamic import, error taxonomy, interactive prompts).
packages/shared/src/cli/commands/metric-views/sync/sync.test.ts Tests CLI behavior (flag vs interactive, error taxonomy, cache flag propagation).
packages/appkit/tsdown.config.ts Ensures ./type-generator is built as its own entry so dynamic-import consumers see required exports.
packages/appkit/src/type-generator/index.ts Refactors metric emission to a unified syncMetricViewsTypes pipeline and updates exported metric artifact constants.
packages/appkit/src/type-generator/mv-registry/render-types.ts Updates comments to reflect the new artifact filename.
packages/appkit/src/type-generator/tests/vite-plugin.test.ts Updates expected default/custom Metric View artifact filenames.
packages/appkit/src/type-generator/tests/index.test.ts Updates expected Metric View artifact filenames in type-generator tests.
packages/appkit/src/type-generator/tests/mv-registry.test.ts Updates metadata bundle naming references in tests.
packages/appkit/src/type-generator/tests/sync-metric-views-types.test.ts Adds focused unit tests for the new syncMetricViewsTypes export, including cache behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +230 to +232
onProgress?.succeed(`Generated metric types: ${metricOutFile}`);
console.log(`Generated metric types: ${metricOutFile}`);
console.log(`Generated metric metadata: ${metricMetadataOutFile}`);
Comment on lines +271 to +274
const cancelled = (): never => {
cancel("Cancelled.");
process.exit(1);
};
Comment on lines 762 to +772
@@ -710,4 +769,4 @@ export const METRIC_TYPES_FILE = "metric.d.ts";
* `registerMetricsMetadata()`, so the React hook returns per-metric `metadata`
* without a second network round-trip.
*/
export const METRIC_METADATA_FILE = "metrics.metadata.json";
export const METRIC_METADATA_FILE = "metric-views.metadata.json";
* Future subcommands (`list` / `validate` / `describe`) plug in here so users have one top-level surface for everything related to Metric Views.
* Sibling of `plugin`, `setup`, `generate-types`, `lint`, `docs`, `codemod`.
*/
export const metricViewsCommand = new Command("mv")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about mv; as an additional alias - maybe (however I'd still think at first that it's something functionally close to unix mv).

The metric-view command would be consistent with what we have (like plugin). What do you think?
(Another thing is that we have docs (plural) and plugin (singular) - so we're already quite inconsistent 😞 not worth to change it now though)

What do you think?

* - `--output-dir` (artifact output directory; replaces Phase 1's `--out-dir`)
* - `--no-cache` (commander negation → `cache === false` disables the metric type-generation cache)
*/
export interface MetricViewsSyncOptions {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a fan of the approach you suggested here (explicit flags) but I think we should consider being consistent across all commands. And the generate-types command uses positional args:

appkit generate-types [rootDir] [outFile] [warehouseId]  --no-cache  --wait

Personally, I'd rather prefer updating the generate-types command to use explicit flags but that would be a breaking change for users 🤔 maybe it's worth to do it sooner than later?

It's not a blocker but at least we should plan a follow-up to unify the commands.

@pkosiec pkosiec left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed the mv-cli typegen changes — solid refactor, and behavior parity for the dev/Vite (non-blocking/blocking) paths reads as preserved 1:1 (the extracted syncMetricViewsTypes keeps serveDegraded = mode !== "describe-now", the preflight, the #406 gate, the sticky-degraded notice, cache save/prune, and the fatalErrors→throw ordering). Findings inline below — mostly P3 polish; nothing blocks correctness of the extraction. Verified the --no-cache / getOptionValueSource handling and the @clack/prompts usage against their docs — both correct.

Two nits not worth their own threads:

  • Stale metric.d.ts / metrics.metadata.json aren't cleaned up on the rename to metric-views.* (removeOldGeneratedTypes only removes appKitTypes.d.ts) — low impact for a pre-release feature, but an upgraded app could keep an orphaned metric.d.ts.
  • The CLI's Zod pre-validation doesn't mirror resolveMetricConfig's 200-entry / 255-char-segment caps, so an over-cap config passes the "fail fast before the dynamic import" stage and throws only after it (still exit 1, just later).

Verdict: ready with minor fixes.

// some keys): the permissive types ARE written, so unlike `result.failures`
// above this is a warning — not a hard failure — and the command still exits
// 0. The degraded entries refresh on a rerun once the warehouse is available.
const degradedKeys = result.schemas

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2 · parity, optional] No --wait equivalent to generate-types. On a cold warehouse mv sync degrades and exits 0 — which is consistent with generate-types' default (its --wait is documented as "wait for warehouse readiness instead of degrading"), so this is not a bug.

The only gap is the missing CI opt-in: consider adding --waitmode: "blocking" (default unchanged) and exiting non-zero on a still-degraded result under --wait.

For context, the mechanics: describe-now has no preflight; a cold DESCRIBE times out at ~30s, and describeOne returns a degraded schema with no failure (mv-registry/sync.ts:83-85), so result.failures is empty and this branch warns + exits 0.

} catch (error) {
if (
error instanceof Error &&
error.message.includes("Cannot find module")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This catch wraps both the dynamic import("@databricks/appkit/type-generator") and the entire syncMetricViewsTypes call, so any nested error whose message contains Cannot find module (a broken transitive dep, a bad subpath export inside appkit) is misreported as "appkit not installed" — sending the user to reinstall a package that's present.

Narrow the match, e.g. (err as NodeJS.ErrnoException).code === "ERR_MODULE_NOT_FOUND" && err.message.includes("@databricks/appkit"), or scope the heuristic to just the import call.

* The cache is enabled by default. Pass `cache: false` to force fresh
* DESCRIBE results, matching the CLI's `--no-cache` flag.
*/
export function syncMetricViewsTypes(options: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] This ambient module hand-mirrors appkit's real export across the package boundary, with no compile-time link to it: measures/dimensions are unknown[] here vs typed arrays in appkit, and mode/metricFetcher are omitted. Nothing fails to compile if the real SyncMetricViewsTypesResult/MetricSchema contract drifts.

At minimum add a comment that this intentionally narrows the signature; ideally a type-assignability check. (Adding mode?: here is also the prerequisite for the --wait parity suggestion in sync.ts.)

}

onProgress?.succeed(`Generated metric types: ${metricOutFile}`);
console.log(`Generated metric types: ${metricOutFile}`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] In interactive mode this prints twice — onProgress.succeed(...) on the line above already stops the spinner with the same "Generated metric types: …" text, then this console.log repeats it. Gate the console.log on onProgress === undefined (only the flag-driven path needs the plain logs).

fail: () => s.stop("Failed."),
});

outro("Metric types synced.");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3] When metric-views.json is absent, runMetricViewsSync logs "Nothing to sync" and returns normally (~line 124), but this path still prints outro("Metric types synced.") — a contradictory success message. Have runMetricViewsSync signal the dormant/no-op outcome so the outro can match (or be skipped).

// prompt and eventually run the sync.
const cancelled = (): never => {
cancel("Cancelled.");
process.exit(1);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3 · consistency] Cancel exits 1 here, but every cancel in plugin create / add-resource exits 0, and clack's docs use process.exit(0) after cancel(...). A Ctrl-C out of mv sync returns a failure code while the same gesture elsewhere is graceful, and wrapper scripts treating non-zero as error will misread it. Suggest process.exit(0) (and flip the two cancel assertions in sync.test.ts).

* Future subcommands (`list` / `validate` / `describe`) plug in here so users have one top-level surface for everything related to Metric Views.
* Sibling of `plugin`, `setup`, `generate-types`, `lint`, `docs`, `codemod`.
*/
export const metricViewsCommand = new Command("mv")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3 · naming, your call] mv is the only abbreviated subcommand — plugin / setup / generate-types / lint / docs / codemod are all full words — and it collides with the Unix mv (move) command. metrics or metric-views would match the family; the short mv is more ergonomic. Flagging for a deliberate decision rather than drift.

.description(
"Sync UC Metric View schemas: DESCRIBE every entry in metric-views.json, then emit metric-views.d.ts + metric-views.metadata.json.",
)
.option(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P3 · UX consistency, your call] The two type-gen commands now diverge: generate-types [rootDir] [outFile] [warehouseId] --no-cache --wait (positional) vs mv sync --warehouse-id --output-dir … --no-cache (named flags); and on a missing warehouse generate-types skips with a message while mv sync hard-fails (exit 1). The flag-based UX here is arguably the better design — flagging the divergence for a conscious call (is the added strictness intended?).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants