@malloydata/malloyyo 0.2.27 → 0.2.29

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/dist/index.js CHANGED
@@ -99,8 +99,8 @@ function resolveTarget(dir, name) {
99
99
  }
100
100
  function resolveInstance(dir, arg) {
101
101
  if (arg && /^https?:\/\//i.test(arg)) {
102
- const url5 = normalizeUrl(arg);
103
- return { name: url5, url: url5 };
102
+ const url6 = normalizeUrl(arg);
103
+ return { name: url6, url: url6 };
104
104
  }
105
105
  const targets = readTargetMap(dir);
106
106
  const entries = Object.entries(targets);
@@ -125,9 +125,9 @@ import { join as join2, relative, sep } from "node:path";
125
125
  import { execFileSync } from "node:child_process";
126
126
 
127
127
  // src/host.ts
128
- import fs from "node:fs";
129
- import path2 from "node:path";
130
- import url2 from "node:url";
128
+ import fs2 from "node:fs";
129
+ import path3 from "node:path";
130
+ import url3 from "node:url";
131
131
  import {
132
132
  MalloyConfig,
133
133
  Runtime,
@@ -152,8 +152,8 @@ var contentFiles = {
152
152
  "dashboards/custom-components.md": '---\ndescription: Custom dashboard UI \u2014 a flat dashboards/<name>.jsx|tsx sibling composing @malloyyo/dashboard widgets/hooks/helpers with your own React\n---\n\n# Custom dashboard components (`dashboards/<name>.jsx`)\n\nThe default UI (auto-rendered controls + panel) covers most dashboards. When it\nisn\'t enough, add ONE file \u2014 a **flat sibling** `dashboards/<name>.jsx` (or\n`.tsx`) next to the dashboard\'s `dashboards/<name>.malloy` (same basename) \u2014 that\ncomposes the runtime\'s widgets/hooks with your own React. You own layout, copy,\nand theming; the `.malloy` file still owns every query and filter. See also\n`yo_help dashboards/authoring` and `dashboards/vega-charts`.\n\n```tsx\nimport React from "react";\nimport { Controls, Given, Search, Select, TimeRange, Panel, filters, useGiven } from "@malloyyo/dashboard";\n\nexport default function Dashboard({ dashboard, givens }) {\n return (\n <div style={{ maxWidth: 860, margin: "0 auto", padding: 24 }}>\n <h1>{dashboard.title}</h1>\n <Controls>\n <Given name="STATE" /> {/* picks the control from the declaration */}\n <Search given="NAME" /> {/* committing input + typeahead + validation */}\n <TimeRange given="PERIOD" presets={[\n { value: "", text: "All time" },\n { value: filters.lastN(1, "day"), text: "Last day" },\n { value: filters.lastN(1, "week"), text: "Last week" },\n { value: filters.lastN(1, "month"), text: "Last month" },\n ]} /> {/* "Custom range\u2026" is always appended */}\n <Select given="MIN_SAMPLE"\n options={[10, 200, 1000].map(n => ({ value: filters.greaterThan(n), text: `> ${n}` }))} />\n </Controls>\n <Panel givens={givens} /> {/* the dashboard itself (its tiles/query) */}\n <Panel query="baby_names -> births_by_decade" givens={givens} /> {/* a specific query */}\n </div>\n );\n}\n```\n\nFrom `@malloyyo/dashboard` (also handed to the component as props):\n- **Widgets** (headless-ish; restyle via className/style or the `--dash-*` CSS\n vars \u2014 see Theming below): `<Controls/>` (all givens, or compose children;\n grows Apply/Reset under `autorun=false`), `<Given name/>`,\n `<Select given [options]/>`, `<Search given/>` (committing input + typeahead +\n inline \u2715 clear), `<MultiSelect given [options]/>` (chip multi-select for a\n `filter<string>` \u2014 commits an exact-match list via `filters.oneOf`),\n `<Range given [min max]/>`, `<TimeRange given [presets]/>` (temporal presets +\n custom range), `<Checkbox given/>` (bound to a boolean given),\n `<VegaChart spec query|malloy|data givens/>` (a Vega-Lite chart over query\n rows \u2014 see `yo_help dashboards/vega-charts`)\n- **Hooks**: `useGiven(name)` \u2192 {value, set, spec};\n `useOptions(name, typed?)` \u2192 {options, loading} (typeahead);\n `useQuery({query|malloy, givens})` \u2192 {rows, loading, error} \u2014 plain rows\n for your own visuals;\n `useUrlState(key, initial)` \u2192 [value, setValue] \u2014 shareable view-state (below)\n- **Helpers**: `filters.oneOf/contains/between/atLeast/\u2026` build\n filter-expression strings with correct escaping; temporal:\n `filters.lastN(7, "day")` \u2192 `\'7 days\'`, `filters.dateRange("2026-01-01",\n "2026-07-01")`, `filters.afterDate/beforeDate`; read back with\n `filters.values/numberRange/threshold/inLast/temporalRange`;\n `filters.isValid(type, src)` checks typed input.\n Never hand-concatenate a filter string.\n **Escaping rule for custom controls:** a filter given\'s value is an\n EXPRESSION, so committing a raw column value is wrong the moment it contains\n a comma/percent/dash (\'Tesla, Inc.\' parses as two alternatives and matches\n nothing). Commit `filters.oneOf(value)` (exact) or\n `filters.contains(term)` (substring), and unwrap for display with\n `filters.values(src)`. The stock `<Select/>` does this automatically;\n `<Search/>` deliberately commits raw text (its input IS a filter\n expression).\n- `<Panel/>` runs against the DASHBOARD\'s own file: a bare `<Panel/>` renders\n the whole dashboard (its tiles); `<Panel query="\u2026"/>` runs a query defined in\n the dashboard file (by name) or a `source -> view`; `<Panel malloy="\u2026"/>` and\n `runData(text, givens)` run arbitrary Malloy as a RESTRICTED query (no import /\n given: / connection.* / raw SQL / ##! flags \u2014 the model\'s governed surface\n only). `lint` checks each hard-coded `query="\u2026"` still resolves.\n\n## Shareable view-state: `useUrlState`\n\n`useState` is invisible to the page that owns the URL, so a component built on\nit has an address bar that never changes \u2014 the result can\'t be shared or\nbookmarked. **`useUrlState(key, initial)` is a `useState` twin whose value lives\nin the URL**, under a `~key` param:\n\n```jsx\nimport { useUrlState } from "@malloyyo/dashboard";\n\nconst [rack, setRack] = useUrlState("rack", ""); // string\nconst [reuse, setReuse] = useUrlState("reuse", false); // boolean\nconst [board, setBoard] = useUrlState("board", "........"); // string\nconst [topN, setTopN] = useUrlState("n", 20); // number\n```\n\n- Same shape as `useState`: `[value, setValue]`, and `setValue` takes a value\n **or** an updater fn (`setBoard(b => \u2026)`).\n- The value comes from the URL on load, else `initial`. Every change is written\n back (debounced, `replaceState` \u2014 no history spam), so the address bar is\n always a shareable link.\n- Typed by `initial`: string / number / boolean / any JSON-serializable value.\n Strings stay readable in the URL (`~rack=retinas`); objects and arrays are\n JSON. A value equal to `initial` is dropped from the URL, so defaults never\n clutter it, and a malformed value falls back to `initial` instead of throwing.\n- Works identically in `malloyyo dashboard dev`, on a bundled static site, and\n on a hosted instance \u2014 including inside the sandboxed iframe, which cannot\n reach the top-level URL on its own. That\'s why this is a hook and not\n something a component can do with `history.replaceState`.\n\n**Use it for view-state, not query parameters.** A `given:` is the governed,\nfilter-typed query contract: it\'s declared in the model, drives the default\ncontrols, and is visible over MCP \u2014 bind those with `useGiven` and they already\nround-trip through the URL as `$NAME`. `useUrlState` is for everything else a\ncustom component needs to make shareable: a letter rack whose real query inputs\n(allowed letters, min/max length) are computed from it in JS, a board layout, a\nmode toggle. The two namespaces (`$NAME` vs `~key`) never collide.\n\n## Theming\n\nEvery widget is styled by the runtime\'s **default Malloyyo theme** (system\nfont, neutral grays, blue accent, auto light/dark following the viewer\'s OS) \u2014\na bare component looks styled with zero effort, so DON\'T hand-hardcode\n`fontFamily`/colors. The theme is CSS custom properties; override any subset by\nsetting them on a wrapper element (more specific than the runtime\'s `:root`):\n\n```tsx\n<div style={{ "--dash-accent": "#e11d48", "--dash-controls-bg": "#faf5ff" }}>\n <Controls /> \u2026\n</div>\n```\n\nVars: `--dash-font`, `--dash-bg`, `--dash-fg`, `--dash-muted`, `--dash-border`,\n`--dash-accent`, `--dash-accent-fg`, `--dash-control-bg`, `--dash-controls-bg`,\n`--dash-chip-bg`, `--dash-chip-fg`, `--dash-panel-bg`, `--dash-radius`,\n`--dash-danger`. `DefaultDashboard` also takes a `theme={{ accent, controlsBg }}`\nprop (camelCase keys \u2192 `--dash-*`). The results `<Panel>` keeps a light surface\nin both light/dark (the Malloy renderer has no dark theme) \u2014 override\n`--dash-panel-bg` if your renderer output is dark-safe.\n\n**Use `--dash-*` and nothing else.** A custom component renders in its own\n**iframe**, so CSS variables defined by the surrounding page \u2014 including the\nbundled site\'s `--line` / `--card` / `--muted` \u2014 are NOT in scope inside it. A\ncomponent styled against those still renders, but every rule referencing them\nresolves to nothing: borders, dividers and panel backgrounds vanish silently\nwhile text and layout survive, so the page looks *almost* right and the cause\nisn\'t obvious. If you\'re porting CSS that has to work both inside the frame and\non a bundled page, resolve each colour once through the chain and use the alias:\n\n```css\n.my-card {\n --edge: var(--dash-border, var(--line, #e4e6eb));\n --surface: var(--dash-panel-bg, var(--card, #fff));\n border: 1px solid var(--edge);\n background: var(--surface);\n}\n```\n\nThis is a class of bug `lint` cannot see and a screenshot can \u2014 look at custom\ncomponents in `malloyyo dashboard dev` before shipping them.\n\nFor charts beyond the Malloy renderer\'s `#` tags, use `<VegaChart>` \u2014\n`yo_help dashboards/vega-charts`.\n',
153
153
  "dashboards/givens-and-controls.md": "---\ndescription: Dashboard filter controls \u2014 declare filter<T> givens with # label / suggest / control tags; faceted (related) suggestions\n---\n\n# Dashboard givens & controls\n\nA dashboard's filters are `filter<T>` **givens** declared in the model; the\n`#` tags on each declaration drive its control. This is part of authoring a\ndashboard \u2014 see also `yo_help dashboards/authoring`.\n\n**Declare the filters as `filter<T>` givens** \u2014 never raw strings/numbers.\nA `filter<string>` value accepts one value ('NY'), alternatives ('NY, CA'),\nwildcards ('Ann%'), negation ('-NY'); a `filter<number>` accepts ranges\n('[1910 to 1930]') and comparisons ('> 200'); a `filter<timestamp>` /\n`filter<date>` accepts relative windows ('7 days' = the last 7 days, 'today',\n'last month') and literal ranges ('2026-01-01 to 2026-07-01' \u2014 NO `@` in\nfilter literals). Apply with `~`; `f''` = empty = no filter (the natural\n\"All\"/\"all time\" \u2014 just `col ~ $X`, no `$X = '' or \u2026` dance):\n\n```malloy\n##! experimental { givens }\ngiven:\n # label=\"State\" control=select suggest { source=baby_names dimension=state }\n STATE :: filter<string> is f'NY'\n # label=\"Brand\" suggest { query=brand_suggest dimension=product_brand }\n BRAND :: filter<string> is f''\n # label=\"Names\" control=multiselect suggest { query=name_suggest dimension=name }\n NAMES :: filter<string> is f''\n # label=\"Years\" range_min=1910 range_max=2025\n YEAR_RANGE :: filter<number> is f'[1910 to 1930]'\n # label=\"Time period\"\n PERIOD :: filter<timestamp> is f''\n # label=\"Include rare names\"\n INCLUDE_RARE :: boolean is false\n```\n\nTags on the declaration drive the control (tag syntax is `key=\"value\"` \u2014\nequals, not colon):\n- `label` \u2014 control caption (defaults to the given's name)\n- `suggest { \u2026 }` \u2014 where the control's options come from. NO Malloy code in\n strings \u2014 just names:\n - `suggest { query=brand_suggest dimension=product_brand }` \u2014 the FIRST\n COLUMN of a named query (declare the query in the model \u2014 governed and\n reviewable). PREFER THIS FORM. The query must be in scope where the dashboard\n runs (bring it in with the dashboard file's bare `import`).\n - `suggest { source=baby_names dimension=state }` \u2014 the DISTINCT VALUES of\n a dimension on a source (the source must be in the dashboard's scope)\n A `dimension` (in either form) is what enables SERVER-SIDE TYPEAHEAD: the\n runtime refines the base query with what the user has typed\n (`\u2026 + { where: lower(field) ~ f'll%'; limit: 50 }`, case-insensitive,\n escaped). Without a dimension the fetched list is filtered client-side.\n Runs as a restricted query; lint checks the declaration compiles.\n\n **RELATED (faceted) filters** \u2014 query-form only: a suggest query may\n reference the OTHER givens, and the runtime runs it with the dashboard's\n current values (the suggested given itself is excluded, so the list never\n collapses to the current pick). Brand suggestions narrow when Category is\n set:\n\n ```malloy\n query: brand_suggest is inventory_items -> product_brand + {\n where:\n product_category ~ $CATEGORY, // NOT product_brand ~ $BRAND\n product_department ~ $DEPARTMENT\n limit: 500\n }\n ```\n\n Declare one `*_suggest` per filter, each referencing the others; `f''`\n defaults mean unset filters don't constrain. `source=` suggests can't do\n this (no place for a `where:`) \u2014 another reason to prefer `query=`.\n- `control=select` \u2014 a fixed dropdown instead of a typeahead search box\n- `control=multiselect` \u2014 a tokenized multi-select for a `filter<string>`:\n each pick is a removable chip, the committed value is an exact-match list\n (`Emma, Olivia, Sophia`). Ideal for \"pick several\" filters (names, brands).\n Suggestions come from the given's `suggest {\u2026}` (server-side typeahead when\n it names a dimension). Empty (start from `f''`) = no filter (all).\n- `range_min` / `range_max` \u2014 bounds; makes a filter<number> given a\n dual-thumb range slider\n- anything else passes through in `spec.tags` for custom components\n\nControl picked from the declaration automatically: numeric range tags \u2192\ndual-thumb slider; `filter<timestamp|timestamptz|date>` \u2192 the TimeRange\nwidget (relative presets: Today / Last 7 days / Last 30 days / \u2026 plus a\n\"Custom range\u2026\" from/to date picker); `control=multiselect` \u2192 chip\nmulti-select; suggest + control=select \u2192 dropdown; boolean \u2192 checkbox;\nanything else \u2192 committing search box with typeahead (an inline \u2715 clears it;\na \"Press \u21B5 to apply\" hint shows while the typed draft differs from what's\nrunning \u2014 free text can't safely re-run per keystroke).\nThe suggest-driven options are DATA VALUES only \u2014 options that aren't column\nvalues (custom time presets, threshold buckets) need a custom component\n(`yo_help dashboards/custom-components`) with explicit `{value, text}` options\nwhere value is a filter expression built with `filters.*`.\n\n## When the query re-runs: live (default) vs. Apply\n\nBy default a dashboard is **live** \u2014 every control change re-runs the query\nimmediately (the committing search box is the exception: free text commits on\nEnter/blur, since a half-typed filter is invalid). To batch changes behind an\n**Apply** button instead, set `autorun=false` on the `# artifact` tag:\n\n```malloy\n# artifact { name=\"births-by-name\" title=\"Births by name\" autorun=false }\n```\n\n`autorun=false` makes `<Controls>` grow an Apply/Reset pair \u2014 controls edit a\ndraft and nothing re-runs until Apply. Reach for it when the query is expensive\nor several filters are usually changed together; leave it off (live) otherwise.\n",
154
154
  "dashboards/grid-layout.md": "---\ndescription: Dashboard grid layout \u2014 # dashboard {columns=N} with # colspan and # break to place KPI tiles and charts\n---\n\n# Dashboard grid layout (`# dashboard {columns=N}`)\n\nBy default a `# dashboard` result flows its KPI tiles and cards and wraps.\nAdd `{columns=N}` to place them on a fixed **N-column grid** instead \u2014 use\n`columns=6`, which divides evenly into 2- and 3-wide cards.\n\n**Key mechanic:** a tag placed ABOVE `aggregate:` or `nest:` applies to EVERY\nitem declared in that block. So you set card widths once per block, not per\nfield.\n\n```malloy\n# artifact { title=\"Customer Insights\" } dashboard {columns=6}\nview: customer_insights is {\n where: created_at ~ $PERIOD\n # colspan=2\n aggregate: total_sales, user_count, order_count, average_order_value\n # colspan=3\n nest:\n # break\n # bar_chart\n users_by_spend_tier\n sales_by_traffic_source\n # shape_map\n sales_by_state\n # colspan=6\n recent_orders // wide detail table \u2192 full width\n}\n```\n\n## The conventions\n\n- **`# colspan=2` above `aggregate:`** \u2014 each KPI / measure tile spans 2 of 6\n columns \u2192 3 tiles per row.\n- **`# colspan=3` above `nest:`** \u2014 each graph or small table spans 3 \u2192 2 per\n row. Per-item render tags (`# line_chart`, `# bar_chart`, `# shape_map`) still\n go on the individual nested items.\n- **`# colspan=6`** \u2014 a single wide / many-column table gets its own full-width\n line. Tag that one item; a per-item `# colspan` overrides the block default.\n- **`# break` on the FIRST nest item** \u2014 starts the graphs on a fresh row, so\n KPI tiles and charts never share one. The renderer splits fields into a new\n grid at each `# break`. Just always add it: it's a no-op when the tiles\n already fill complete rows, and the fix when they don't (e.g. 4 measures\n leave a lone tile a colspan-3 chart would otherwise pack in beside).\n\n`# colspan` only does anything in columns mode \u2014 without `{columns=N}` the\nlayout is free-flow wrap and colspan is ignored. Clamp colspans to `1..N`.\n\nSee also `yo_help dashboards/vega-charts` for custom charts, and the fuller\nauthoring guide surfaced by the local `malloyyo mcp` server.\n",
155
- "dashboards/vega-charts.md": '---\ndescription: Custom dashboard charts with Vega-Lite \u2014 the <VegaChart> component, for charts the # renderer tags can\'t do\n---\n\n# Custom charts with Vega-Lite (`<VegaChart>`)\n\nWhen Malloy\'s renderer tags (`# bar_chart`, `# line_chart`, `# shape_map`, \u2026)\ndon\'t cover the chart you want, a dashboard can draw a **Vega-Lite** spec with\nthe `<VegaChart>` component. The chart engine ships in the dashboard runtime, so\nyou author only a JSON spec + a Malloy query \u2014 no library to load.\n\n**It is a COMPONENT, not a `#` tag.** There is no `# vega_lite` or\n`# scatter_chart` tag. `<VegaChart>` lives in a custom component \u2014 a flat sibling\n`dashboards/<name>.jsx` (or `.tsx`) next to the dashboard\'s\n`dashboards/<name>.malloy` \u2014 a different layer from the `#` renderer tags. (The\ndashboard\'s query is declared in the `.malloy` file; the component only\ncustomizes presentation. Preview with `malloyyo dashboard dev`, validate with\n`malloyyo lint`.)\n\n## The recipe\n\n```tsx\nimport { VegaChart } from "@malloyyo/dashboard";\n\n// Encodings point at the query\'s OUTPUT COLUMN NAMES (here: name, births).\nconst spec = {\n mark: { type: "bar", tooltip: true },\n encoding: {\n y: { field: "name", type: "nominal", sort: "-x" },\n x: { field: "births", type: "quantitative" },\n },\n};\n\nexport default function Dashboard({ givens }) {\n return <VegaChart spec={spec} query="births_by_name" givens={givens} />;\n}\n```\n\nThree ways to feed it data:\n- `<VegaChart spec={spec} query="births_by_name" givens={givens}/>` \u2014 a query\n defined in the dashboard\'s `.malloy` file (by name), or a `source -> view`\n- `<VegaChart spec={spec} malloy="source -> view" givens={givens}/>` \u2014 restricted\n Malloy text (same governance as the explore surface: no import / given: /\n connection.* / raw SQL / ##! flags)\n- `<VegaChart spec={spec} data={rows}/>` \u2014 rows you already have from `useQuery`\n\n## Gotchas (the ones that actually bite)\n\n- **Shape the data in Malloy; return FLAT rows.** Do ranking, share/percent\n (`all(x, dim)`), and label lookups (a `pick` for month names) in the QUERY.\n The spec just encodes columns \u2014 it is not the place to reshape data.\n- **Match column names character-for-character.** Run the query once with\n `query(execute:true)` and read the exact output column names; the spec\'s\n `field` values must match them exactly.\n- **The spec\'s `data` is ignored / any `url` is stripped.** The frame has no\n network \u2014 remote data URLs, transform lookups, and remote `image` marks are\n removed. Adapting a Vega-Lite gallery example = delete its\n `"data": {"url": \u2026}` and repoint the encodings; the query rows are inlined for\n you as the dataset.\n- **Nests come back as arrays.** Flatten to plottable rows in the query, or bind\n a nest to its own chart: `<VegaChart data={row.my_nest}/>`.\n- **Interactivity = setting given values**, never rewriting query text per\n interaction. Client-side chart interactions (tooltip, zoom, brush) work;\n anything that calls a server does not.\n- **Reads well:** for normalized/share data use a diverging color scale with\n `domainMid` (e.g. `1/12` for month-share), and sort a discrete axis by a\n companion numeric field (`month_name` sorted by `month_num`) rather than\n alphabetically.\n\n## Validate\n\n`malloyyo lint` checks the query, the givens, AND the component (it compiles,\nand each `query="\u2026"` it references resolves) \u2014 your only pre-browser check. Then\n`malloyyo dashboard dev` to see it render.\n',
156
- "develop/connection-setup.md": '---\ndescription: Setting up a data connection (malloy-config.json)\n---\n\n# Setting up a data connection\n\nA model reaches its data through a **connection** declared in\n`malloy-config.json` at the **root of the model** (next to `index.malloy`). This\nis Malloy\'s standard connection config \u2014 the full reference, with every\nconnector\'s properties, is at\n<https://docs.malloydata.dev/documentation/setup/config>. The essentials:\n\n## The file\n\n```json\n{\n "connections": {\n "mydb": { "is": "duckdb" }\n }\n}\n```\n\n- A connection has a **name** (the key) and a type (`is`). Sources refer to it by\n that name \u2014 `source: x is mydb.table("orders")` or `mydb.sql("SELECT \u2026")` \u2014 so\n the name in the config must match the name in the model.\n- Supported types (`is`): `duckdb` (incl. MotherDuck), `bigquery`, `postgres`,\n `mysql`, `snowflake`, `databricks`, `trino`, `presto`. Each has its own\n properties \u2014 see the full docs.\n\n## Default connections (and when they apply)\n\nSetting `"includeDefaultConnections": true` makes one connection available for\n**each registered database type, named by the type** \u2014 a `duckdb` connection\nnamed `duckdb`, a `postgres` named `postgres`, and so on. Each uses that\nconnector\'s default settings, which for several backends means picking up\ncredentials from the environment (e.g. BigQuery\'s application-default\ncredentials \u2014 see the per-connector setup docs). Connections you name explicitly\nin `connections` always win; the defaults only fill in types you didn\'t list.\n\n```json\n{\n "includeDefaultConnections": true,\n "connections": { "warehouse": { "is": "postgres", "host": "\u2026" } }\n}\n```\n\nmalloyyo has one rule worth knowing:\n\n- **No `malloy-config.json` at all \u2192 defaults are ON.** `duckdb` just works with\n zero setup.\n- **Write a `malloy-config.json` and they turn OFF** unless you add\n `"includeDefaultConnections": true`. A config that only defines, say, a\n `postgres` connection will report *No connection named "duckdb"* if a source\n still references `duckdb`.\n\nThis is deliberate \u2014 your local connections then resolve exactly the way the\npublished server\'s will, rather than silently leaning on a default that would not\nexist in production. (It differs from `malloy-cli`, which forces the defaults on\nunconditionally.)\n\nGive a connection a **custom name** \u2014 not the bare type default \u2014 whenever you\nhave more than one connection of the same type, or need non-default parameters.\n\n## DuckDB and local files (the common case)\n\nDuckDB can either open a **DuckDB database file** or read **local data files**\n(CSV, Parquet, \u2026) directly.\n\n**A pre-loaded database file** \u2014 point `databasePath` at a `.duckdb` file and\nreference its tables by name (an absolute path is safest):\n\n```json\n{ "connections": { "warehouse": { "is": "duckdb", "databasePath": "/data/warehouse.duckdb" } } }\n```\n\n**Local files, by path** \u2014 read a CSV or Parquet file straight into a source.\nThese paths are **project-relative** \u2014 resolved against the model root, so they\nsurvive publishing:\n\n source: my_csv is duckdb.table(\'data/my_file.csv\')\n source: my_parquet is duckdb.table(\'data/my_file.parquet\')\n\n`.table()` names a single file. When you need something it can\'t express \u2014 a\nglob, a union, any SQL \u2014 wrap it in `.sql()` (its paths are project-relative too):\n\n source: payments is duckdb.sql(\n "SELECT * FROM read_parquet(\'data/payments-*.parquet\')"\n )\n\nThe default `duckdb` connection is **in-memory** \u2014 nothing persists between runs;\nyour data lives in the files (or the `databasePath` database) you read.\n\n**MotherDuck:** a DuckDB connection against an `md:` database; set the\n`MOTHERDUCK_TOKEN` environment variable.\n\n## Secrets \u2014 keep them out of the file\n\nAny property value may be written as `{ "env": "VAR_NAME" }`. It resolves from\n`process.env.VAR_NAME` when the connection opens, so passwords and tokens never\nget committed:\n\n```json\n{\n "connections": {\n "analytics": {\n "is": "postgres",\n "host": "db.internal",\n "databaseName": "analytics",\n "username": "reader",\n "password": { "env": "PG_PASSWORD" }\n }\n }\n}\n```\n\n(The non-secret property names here are illustrative \u2014 each connector\'s exact\nproperties are in the full docs. The `{ "env": \u2026 }` form is the part that\nmatters: it works for any value.)\n\n## malloyyo specifics\n\n- **One file, one place.** Only the `malloy-config.json` at the model root is\n read \u2014 there is no walk-up to parent directories.\n- **Local override:** a `malloy-config-local.json` (do **not** commit it)\n **replaces** `malloy-config.json` entirely when present \u2014 your private variant\n for local credentials or a different database.\n- **The same file ships to production.** Publishing uploads this exact\n `malloy-config.json`, so it must resolve the same way locally and on the\n server \u2014 put anything environment-specific behind `{ "env": \u2026 }` rather than\n hard-coding it. That is what makes the local test window faithful to\n production.\n- Edits are picked up **without a restart** \u2014 the server re-reads the file when\n it changes.\n\n## When a connection will not resolve\n\nFix the connection **first**: a broken connection yields an empty schema and then\na cascade of misleading `field-not-found` errors \u2014 ignore those and fix the\nconnection. A fast check is to compile a probe and see if it alone compiles:\n\n source: _probe is mydb.sql("SELECT 1 AS one")\n',
155
+ "dashboards/vega-charts.md": '---\ndescription: Custom dashboard charts with Vega-Lite \u2014 the <VegaChart> component, for charts the # renderer tags can\'t do\n---\n\n# Custom charts with Vega-Lite (`<VegaChart>`)\n\nWhen Malloy\'s renderer tags (`# bar_chart`, `# line_chart`, `# shape_map`, \u2026)\ndon\'t cover the chart you want, a dashboard can draw a **Vega-Lite** spec with\nthe `<VegaChart>` component. The chart engine ships in the dashboard runtime, so\nyou author only a JSON spec + a Malloy query \u2014 no library to load.\n\n**It is a COMPONENT, not a `#` tag.** There is no `# vega_lite` or\n`# scatter_chart` tag. `<VegaChart>` lives in a custom component \u2014 a flat sibling\n`dashboards/<name>.jsx` (or `.tsx`) next to the dashboard\'s\n`dashboards/<name>.malloy` \u2014 a different layer from the `#` renderer tags. (The\ndashboard\'s query is declared in the `.malloy` file; the component only\ncustomizes presentation. Preview with `malloyyo dashboard dev`, validate with\n`malloyyo lint`.)\n\n## The recipe\n\n```tsx\nimport { VegaChart } from "@malloyyo/dashboard";\n\n// Encodings point at the query\'s OUTPUT COLUMN NAMES (here: name, births).\nconst spec = {\n mark: { type: "bar", tooltip: true },\n encoding: {\n y: { field: "name", type: "nominal", sort: "-x" },\n x: { field: "births", type: "quantitative" },\n },\n};\n\nexport default function Dashboard({ givens }) {\n return <VegaChart spec={spec} query="births_by_name" givens={givens} />;\n}\n```\n\nThree ways to feed it data:\n- `<VegaChart spec={spec} query="births_by_name" givens={givens}/>` \u2014 a query\n defined in the dashboard\'s `.malloy` file (by name), or a `source -> view`\n- `<VegaChart spec={spec} malloy="source -> view" givens={givens}/>` \u2014 restricted\n Malloy text (same governance as the explore surface: no import / given: /\n connection.* / raw SQL / ##! flags)\n- `<VegaChart spec={spec} data={rows}/>` \u2014 rows you already have from `useQuery`\n\n## Gotchas (the ones that actually bite)\n\n- **Shape the data in Malloy; return FLAT rows.** Do ranking, share/percent\n (`all(x, dim)`), and label lookups (a `pick` for month names) in the QUERY.\n The spec just encodes columns \u2014 it is not the place to reshape data.\n- **Match column names character-for-character.** Run the query once with\n `query(execute:true)` and read the exact output column names; the spec\'s\n `field` values must match them exactly.\n- **The spec\'s `data` is ignored / any `url` is stripped.** The frame has no\n network \u2014 remote data URLs, transform lookups, and remote `image` marks are\n removed. Adapting a Vega-Lite gallery example = delete its\n `"data": {"url": \u2026}` and repoint the encodings; the query rows are inlined for\n you as the dataset.\n- **Nests come back as arrays.** Flatten to plottable rows in the query, or bind\n a nest to its own chart: `<VegaChart data={row.my_nest}/>`.\n- **Huge integers arrive as strings, on purpose.** Integers serialize as JSON\n numbers, but a value beyond \xB12^53 (a snowflake ID, a 64-bit hash) keeps its\n full precision by staying a string \u2014 JSON has no int64. Bound to a\n `quantitative` or `temporal` channel, Vega-Lite would sort such a column\n lexicographically (`"1","10","11","2"`). If a column can get that big and you\n need to plot it, narrow it in the QUERY \u2014 bucket it, rank it, or emit the\n value you actually want on the axis.\n- **Interactivity = setting given values**, never rewriting query text per\n interaction. Client-side chart interactions (tooltip, zoom, brush) work;\n anything that calls a server does not.\n- **Reads well:** for normalized/share data use a diverging color scale with\n `domainMid` (e.g. `1/12` for month-share), and sort a discrete axis by a\n companion numeric field (`month_name` sorted by `month_num`) rather than\n alphabetically.\n\n## Validate\n\n`malloyyo lint` checks the query, the givens, AND the component (it compiles,\nand each `query="\u2026"` it references resolves) \u2014 your only pre-browser check. Then\n`malloyyo dashboard dev` to see it render.\n',
156
+ "develop/connection-setup.md": '---\ndescription: Setting up a data connection (malloy-config.json)\n---\n\n# Setting up a data connection\n\nA model reaches its data through a **connection** declared in\n`malloy-config.json` at the **root of the model** (next to `index.malloy`). This\nis Malloy\'s standard connection config \u2014 the full reference, with every\nconnector\'s properties, is at\n<https://docs.malloydata.dev/documentation/setup/config>. The essentials:\n\n## The file\n\n```json\n{\n "connections": {\n "mydb": { "is": "duckdb" }\n }\n}\n```\n\n- A connection has a **name** (the key) and a type (`is`). Sources refer to it by\n that name \u2014 `source: x is mydb.table("orders")` or `mydb.sql("SELECT \u2026")` \u2014 so\n the name in the config must match the name in the model.\n- Supported types (`is`): `duckdb` (incl. MotherDuck), `bigquery`, `postgres`,\n `mysql`, `snowflake`, `databricks`, `trino`, `presto`. Each has its own\n properties \u2014 see the full docs.\n\n## Default connections (and when they apply)\n\nSetting `"includeDefaultConnections": true` makes one connection available for\n**each registered database type, named by the type** \u2014 a `duckdb` connection\nnamed `duckdb`, a `postgres` named `postgres`, and so on. Each uses that\nconnector\'s default settings, which for several backends means picking up\ncredentials from the environment (e.g. BigQuery\'s application-default\ncredentials \u2014 see the per-connector setup docs). Connections you name explicitly\nin `connections` always win; the defaults only fill in types you didn\'t list.\n\n```json\n{\n "includeDefaultConnections": true,\n "connections": { "warehouse": { "is": "postgres", "host": "\u2026" } }\n}\n```\n\nmalloyyo has one rule worth knowing:\n\n- **No `malloy-config.json` at all \u2192 defaults are ON.** `duckdb` just works with\n zero setup.\n- **Write a `malloy-config.json` and they turn OFF** unless you add\n `"includeDefaultConnections": true`. A config that only defines, say, a\n `postgres` connection will report *No connection named "duckdb"* if a source\n still references `duckdb`.\n\nThis is deliberate \u2014 your local connections then resolve exactly the way the\npublished server\'s will, rather than silently leaning on a default that would not\nexist in production. (It differs from `malloy-cli`, which forces the defaults on\nunconditionally.)\n\n*No connection named* has one cause that is **not** about your config: if more\nthan one copy of `@malloydata/malloy` is installed, the connectors register with\none copy and malloyyo reads another, and every connection looks missing. malloyyo\ndetects that and says so instead \u2014 if it doesn\'t mention duplicate copies, the\nproblem really is the config. To check by hand: `npm ls @malloydata/malloy`.\n\nGive a connection a **custom name** \u2014 not the bare type default \u2014 whenever you\nhave more than one connection of the same type, or need non-default parameters.\n\n## DuckDB and local files (the common case)\n\nDuckDB can either open a **DuckDB database file** or read **local data files**\n(CSV, Parquet, \u2026) directly.\n\n**A pre-loaded database file** \u2014 point `databasePath` at a `.duckdb` file and\nreference its tables by name (an absolute path is safest):\n\n```json\n{ "connections": { "warehouse": { "is": "duckdb", "databasePath": "/data/warehouse.duckdb" } } }\n```\n\n**Local files, by path** \u2014 read a CSV or Parquet file straight into a source.\nThese paths are **project-relative** \u2014 resolved against the model root, so they\nsurvive publishing:\n\n source: my_csv is duckdb.table(\'data/my_file.csv\')\n source: my_parquet is duckdb.table(\'data/my_file.parquet\')\n\n`.table()` names a single file. When you need something it can\'t express \u2014 a\nglob, a union, any SQL \u2014 wrap it in `.sql()` (its paths are project-relative too):\n\n source: payments is duckdb.sql(\n "SELECT * FROM read_parquet(\'data/payments-*.parquet\')"\n )\n\nThe default `duckdb` connection is **in-memory** \u2014 nothing persists between runs;\nyour data lives in the files (or the `databasePath` database) you read.\n\n**MotherDuck:** a DuckDB connection against an `md:` database; set the\n`MOTHERDUCK_TOKEN` environment variable.\n\n## Secrets \u2014 keep them out of the file\n\nAny property value may be written as `{ "env": "VAR_NAME" }`. It resolves from\n`process.env.VAR_NAME` when the connection opens, so passwords and tokens never\nget committed:\n\n```json\n{\n "connections": {\n "analytics": {\n "is": "postgres",\n "host": "db.internal",\n "databaseName": "analytics",\n "username": "reader",\n "password": { "env": "PG_PASSWORD" }\n }\n }\n}\n```\n\n(The non-secret property names here are illustrative \u2014 each connector\'s exact\nproperties are in the full docs. The `{ "env": \u2026 }` form is the part that\nmatters: it works for any value.)\n\n## malloyyo specifics\n\n- **One file, one place.** Only the `malloy-config.json` at the model root is\n read \u2014 there is no walk-up to parent directories.\n- **Local override:** a `malloy-config-local.json` (do **not** commit it)\n **replaces** `malloy-config.json` entirely when present \u2014 your private variant\n for local credentials or a different database.\n- **The same file ships to production.** Publishing uploads this exact\n `malloy-config.json`, so it must resolve the same way locally and on the\n server \u2014 put anything environment-specific behind `{ "env": \u2026 }` rather than\n hard-coding it. That is what makes the local test window faithful to\n production.\n- Edits are picked up **without a restart** \u2014 the server re-reads the file when\n it changes.\n\n## When a connection will not resolve\n\nFix the connection **first**: a broken connection yields an empty schema and then\na cascade of misleading `field-not-found` errors \u2014 ignore those and fix the\nconnection. A fast check is to compile a probe and see if it alone compiles:\n\n source: _probe is mydb.sql("SELECT 1 AS one")\n',
157
157
  "develop/getting-started.md": '---\ndescription: Getting started \u2014 build a Malloy model step by step\n---\n\n# Building a Malloy model, step by step\n\nA model is a `malloy-config.json` (the connection to the data) and an\n`index.malloy` (the published query surface), optionally with other `.malloy`\nfiles that `index.malloy` imports. You edit these with your own file tools; the\nMCP tools compile, inspect, and test what you wrote. Never read `.malloy` as\ntext \u2014 compiling a bare source is how you read a table\'s schema.\n\n## 1. Verify the connection first\n\nConfirm the connection named in `malloy-config.json` resolves \u2014 compile a\nthrowaway probe inline with `compile` (no file needed):\n\n source: _probe is CONN.sql("SELECT 1 AS one")\n\nIf it compiles, the connection is good. If not, fix the connection / config\nbefore going further \u2014 a broken connection produces an empty schema and then a\ncascade of misleading `field-not-found` errors; ignore the cascade and fix the\nconnection. Call `yo_help("develop/connection-setup")` for how to set up or repair a connection.\n\n## 2. Identify the tables the model needs\n\nIf you are unsure which tables matter, ask the fox \u2014 they own the data and know\nwhere it lives.\n\n## 3. Get a base source per table\n\nA base is "what\'s in the table and what\'s computable from it" \u2014 no joins.\nDiscover the schema by compiling a bare stub inline with `compile`:\n\n source: users_base is CONN.table("users")\n\n`compile` returns the full column list + types \u2014 that is your schema browser.\nThen write the base into its own file and iterate with `compile_file`, adding\nonly the dimensions and measures intrinsic to that one table:\n\n // users_base.malloy\n source: users_base is CONN.table("users") extend {\n measure: user_count is count()\n }\n\nIf the data lives in files rather than database tables \u2014 common when the\nconnection is DuckDB \u2014 DuckDB lets you name a file path as the table (a\nproject-relative path):\n\n source: users_base is CONN.table(\'data/users.parquet\')\n\nOnly drop to a `.sql()` block when a single file-as-table can\'t express what you\nneed \u2014 e.g. globbing or unioning several files:\n\n source: users_base is CONN.sql("SELECT * FROM read_parquet(\'data/users-*.parquet\')")\n\n(`read_parquet` there is DuckDB SQL, not Malloy \u2014 see `yo_help("develop/connection-setup")`.)\n\n## 4. Assemble index.malloy \u2014 the published surface\n\nImport the bases, join them into the consumer-facing sources, and explicitly\nexport what consumers may query:\n\n import "users_base.malloy"\n import "orders_base.malloy"\n\n source: users is users_base extend {\n join_many: orders is orders_base on id = orders.user_id\n }\n source: orders is orders_base extend { }\n\n export { users, orders }\n\n**Export discipline** \u2014 the model is a published artifact, so be deliberate about\nits public surface:\n\n- Imported names are private. Base sources stay internal scaffolding unless you\n export them.\n- Without an `export` statement, everything you define is public. Add one and the\n surface becomes explicit: only the names you list \u2014 defined or imported \u2014 are\n public.\n- Hide intermediates. A staging source you define only so other sources can build\n on it should not be exported.\n- The export list is the consumer\'s menu \u2014 exactly what the test window and real\n consumers can query, nothing more.\n\n## The loop\n\nEdit a file \u2192 `compile_file` \u2192 fix `problems[]` \u2192 `query` (pass the model file\'s\npath as `ref`; `execute: false` validates first and reports the givens the query\nneeds, supplied via `givens`). Query text is restricted \u2014 `import`, `given:`\ndeclarations, `connection.table`/`connection.sql`, raw SQL, and `##!` flags\nbelong in the model file, not the query. When `compile`/`compile_file` returns\n`formatted: false`, call `prettify` and save its output. Use project-relative\ndata paths, not absolute \u2014 they resolve against the project root and survive\npublishing the model.\n',
158
158
  "develop/working-with-models.md": "---\ndescription: Working with an existing Malloy model\n---\n\n# Working with an existing model\n\nAn existing model is an `index.malloy` (plus any `.malloy` files it imports) and\na `malloy-config.json`.\n\n## Understand the model\n\n- **Read `malloy-config.json` directly** \u2014 it is JSON, so read it as text. It\n lists the connection(s) the model queries against; `yo_help(\"develop/connection-setup\")` explains\n the format (and how to set one up or repair it).\n- **Do NOT read `.malloy` as text \u2014 compile it.** `compile_file` returns the\n structured model: each source with its fields, joins, views, and named queries,\n plus `problems[]`. That is how you describe what is in the model. (Compiling a\n bare source \u2014 no `extend` block \u2014 likewise reads a raw table's schema.)\n\n## The loop\n\nEdit a file \u2192 `compile_file` \u2192 fix `problems[]` \u2192 `query`. Pass the model file's\npath as `ref`; `execute: false` validates first and reports the givens the query\nneeds (supplied via `givens`). Query text is restricted \u2014 `import`, `given:`\ndeclarations, `connection.table`/`connection.sql`, raw SQL, and `##!` flags\nbelong in the model file, not the query. When `compile`/`compile_file` returns\n`formatted: false`, run `prettify` and save its output. Use project-relative data\npaths, not absolute.\n",
159
159
  "explore/how-to.md": '---\ndescription: Extended instructions working with this MCP server\n---\n# General Notes\nMalloy is a combined semantic layer/query language. It describes data and analysis, and it can generate and execute SQL.\n\nYou answer questions from published Malloy semantic models. A model publishes sources and queries.\n\n* Some tools return problems[] to indicate invalid Malloy. problems may have a `help_topic` field \u2014 call `yo_help(help_topic)` for detailed guidance.\n* `yo_help()` with no topic will show an index which include error explanations, examples of Malloy syntax for common patterns, and a language reference manual (Malloy syntax is still evolving).\n* Tools that inspect Malloy code return objects with schemas, among other things. An entry with a name which requires `back-tick-quoting` (reserved word, special characters), will have `must_quote: true`\n* When limiting queries, do ranking, top-N, and member selection in Malloy, not in client code. Results are byte-budgeted: oversized results are truncated (the response says so and may link the full result). Reading aggregated rows is better analysis and the only way to see everything.\n* Compose your answer from what the model publishes. When composing a query, you can make new sources, extending existing sources with measures dimensions and joins. If the model\'s surface genuinely cannot answer a question, that is useful signal about the model.\n\n# Answering A Question\nTo answer a question you need to see what sources are available which pertain to the question.\n\n`list_sources` (when available) \u2014 see the sources you can query, grouped by model, with each model\'s named queries. If you already know the source, go straight to describe_source.\n\n# Build the Query\n* New to a pattern? `yo_help("explore/query-examples")` \u2014 the handful of Malloy query shapes (views, the workhorse group_by/aggregate, filtered aggregates, `all()`, `extend:`, `select:`, `nest:`) that cover almost every question, with the SQL habits that are wrong in Malloy.\n* `describe_source(source, model_ref)` \u2014 always describe a source before querying it (`model_ref` optional when the name is unique). Returns:\n * `described_source` \u2014 the source\'s `dimensions` (columns), `measures`, and `views` (the author\'s saved queries). A dimension\'s `type` is a scalar or a nested record (`origin.city`); an array column has no `type` \u2014 it shows up as a `joins` entry at its `path`.\n * `joins` \u2014 keyed by path, the arrays and source-joins this source reaches. `fans_out` marks a path that fans out. Each entry is one of: `{ source }` (fields in `join_source_map`), `{ source_def }` (an anonymous source\'s fields, inline), or an array `{ is_array, source_def }` \u2014 a record array\'s fields are used directly (`parcels.sku`), a scalar array\'s element is `each` (`tags.each`). To write a reference, use the entry\'s `quoted_path` if it has one, else the key.\n * `join_source_map` \u2014 the named sources those `{ source }` joins resolve to, deduped.\n * In its own content block, the source\'s raw Malloy, for anything the structured output above doesn\'t cover.\n* `query(source: "...", malloy: "run: source -> { ... }", execute: false)` \u2014 validate without running; it returns the SQL. Iterate until clean. (`model_ref` optional, needed only when the source name is ambiguous.)\n* Some queries accept parameters (givens). More info if needed: yo_help("language/givens-model-level-parameters")\n\n# Run the query\n* Pass a plain-English question with EVERY query, describing what that specific query answers. Queries are recorded/shared independently. Don\'t try to group related queries.\n* query(source: "...", malloy: "run: source -> { ... }", question: "...") \u2014 run it; get the rows.\n\n# Displaying Results\n* Lead with a natural-language restatement of the query \u2014 a short heading works well.\n* A successful query comes back with an ltool_link \u2014 {text, url}, already assembled. It opens this exact query so the user can keep exploring, or share the insight. Follow the data with a markdown link, [\u2197 text](url).\n* When it helps the reader add a short note on how you got the answer: the Malloy logic (filters, grouping, aggregation, ordering, pipeline stages), and any post-processing done outside Malloy.',
@@ -330,8 +330,8 @@ function errorProblem(e, uri) {
330
330
  uri
331
331
  };
332
332
  }
333
- function codeProblem(code, message, uri) {
334
- const out = { severity: "error", code, message, uri };
333
+ function codeProblem(code, message2, uri) {
334
+ const out = { severity: "error", code, message: message2, uri };
335
335
  const topic = helpTopicForCode(code);
336
336
  if (topic) out.help_topic = topic;
337
337
  return out;
@@ -959,6 +959,28 @@ function modelCatalogEntry(model_ref, model) {
959
959
  if (sources.length) entry.sources = sources;
960
960
  return entry;
961
961
  }
962
+ var MAX_SAFE = BigInt(Number.MAX_SAFE_INTEGER);
963
+ var MIN_SAFE = -MAX_SAFE;
964
+ function narrowBigint(value) {
965
+ return value <= MAX_SAFE && value >= MIN_SAFE ? Number(value) : value.toString();
966
+ }
967
+ function jsonValue(value) {
968
+ if (typeof value === "bigint") return narrowBigint(value);
969
+ if (value instanceof Date) return value.toISOString();
970
+ if (Array.isArray(value)) return value.map(jsonValue);
971
+ if (value !== null && typeof value === "object" && value.constructor === Object) {
972
+ const out = {};
973
+ for (const [k, v] of Object.entries(value)) out[k] = jsonValue(v);
974
+ return out;
975
+ }
976
+ return value;
977
+ }
978
+ function jsonRows(result) {
979
+ if (!result.hasSchema) {
980
+ return result.toJSON().queryResult.result;
981
+ }
982
+ return result.data.toObject().map((row) => jsonValue(row));
983
+ }
962
984
  var DEFAULT_ROW_LIMIT = 1e4;
963
985
  async function executeMaterialized(query, opts, loadProblems, decorate = (p) => p, uri) {
964
986
  const rowLimit = opts.rowLimit ?? DEFAULT_ROW_LIMIT;
@@ -970,7 +992,7 @@ async function executeMaterialized(query, opts, loadProblems, decorate = (p) =>
970
992
  const t1 = Date.now();
971
993
  const results = await retry(() => query.run({ rowLimit, ...compileOpts }));
972
994
  const t2 = Date.now();
973
- const rows = results.toJSON().queryResult.result;
995
+ const rows = jsonRows(results);
974
996
  const out = {
975
997
  ok: true,
976
998
  sql,
@@ -1967,6 +1989,239 @@ function developSurface(host, opts = {}) {
1967
1989
  };
1968
1990
  }
1969
1991
 
1992
+ // src/connections.ts
1993
+ import fs from "node:fs";
1994
+ import path2 from "node:path";
1995
+ import url2 from "node:url";
1996
+ import { createRequire } from "node:module";
1997
+ import { getRegisteredConnectionTypes } from "@malloydata/malloy";
1998
+ var CONNECTIONS_PKG = "@malloydata/malloy-connections";
1999
+ var CONNECTOR_IMPORTS = [
2000
+ "@malloydata/db-bigquery",
2001
+ "@malloydata/db-duckdb/native",
2002
+ "@malloydata/db-mysql",
2003
+ "@malloydata/db-databricks",
2004
+ "@malloydata/db-postgres",
2005
+ "@malloydata/db-snowflake",
2006
+ "@malloydata/db-trino"
2007
+ ];
2008
+ var REGISTRY_WRITERS = [
2009
+ CONNECTIONS_PKG,
2010
+ "@malloydata/db-bigquery",
2011
+ "@malloydata/db-databricks",
2012
+ "@malloydata/db-duckdb",
2013
+ "@malloydata/db-mysql",
2014
+ "@malloydata/db-postgres",
2015
+ "@malloydata/db-snowflake",
2016
+ "@malloydata/db-trino"
2017
+ ];
2018
+ function message(e) {
2019
+ return e instanceof Error ? e.message : String(e);
2020
+ }
2021
+ function oneLine(text) {
2022
+ return text.split("\n")[0];
2023
+ }
2024
+ function indent(text, by = " ") {
2025
+ return text.split("\n").map((l) => l ? by + l : l).join("\n");
2026
+ }
2027
+ function requireFrom(dir) {
2028
+ return createRequire(path2.join(dir, "__resolve__.cjs"));
2029
+ }
2030
+ function packageRootOf(entry) {
2031
+ let dir = path2.dirname(entry);
2032
+ for (; ; ) {
2033
+ if (fs.existsSync(path2.join(dir, "package.json"))) return dir;
2034
+ const up = path2.dirname(dir);
2035
+ if (up === dir) return null;
2036
+ dir = up;
2037
+ }
2038
+ }
2039
+ function packageDir(spec, from) {
2040
+ const req = requireFrom(from);
2041
+ try {
2042
+ return fs.realpathSync(path2.dirname(req.resolve(`${spec}/package.json`)));
2043
+ } catch {
2044
+ }
2045
+ try {
2046
+ const root = packageRootOf(req.resolve(spec));
2047
+ return root ? fs.realpathSync(root) : null;
2048
+ } catch {
2049
+ return null;
2050
+ }
2051
+ }
2052
+ function readVersion(pkgDir) {
2053
+ try {
2054
+ const raw = fs.readFileSync(path2.join(pkgDir, "package.json"), "utf8");
2055
+ return String(JSON.parse(raw).version ?? "?");
2056
+ } catch {
2057
+ return "?";
2058
+ }
2059
+ }
2060
+ function selfDir() {
2061
+ return path2.dirname(url2.fileURLToPath(import.meta.url));
2062
+ }
2063
+ function malloyCopies(from = selfDir()) {
2064
+ const here = from;
2065
+ const byDir = /* @__PURE__ */ new Map();
2066
+ const note = (from2, importer) => {
2067
+ if (!from2) return;
2068
+ const dir = packageDir("@malloydata/malloy", from2);
2069
+ if (!dir) return;
2070
+ const found = byDir.get(dir);
2071
+ if (found) {
2072
+ if (!found.importers.includes(importer)) found.importers.push(importer);
2073
+ return;
2074
+ }
2075
+ byDir.set(dir, { dir, version: readVersion(dir), importers: [importer] });
2076
+ };
2077
+ note(here, "malloyyo");
2078
+ const connectionsDir = packageDir(CONNECTIONS_PKG, here);
2079
+ for (const spec of REGISTRY_WRITERS) {
2080
+ note(packageDir(spec, connectionsDir ?? here) ?? packageDir(spec, here), spec);
2081
+ }
2082
+ return [...byDir.values()];
2083
+ }
2084
+ function duplicateMalloyReport(from) {
2085
+ const copies = malloyCopies(from);
2086
+ if (copies.length < 2) return null;
2087
+ const listing = copies.map((c) => `${c.version.padEnd(9)} ${c.dir}
2088
+ ${" ".repeat(10)}\u21B3 ${c.importers.join(", ")}`).join("\n");
2089
+ return `${copies.length} copies of @malloydata/malloy are installed. The connection
2090
+ registry is module-level state, so each copy has its own: the connectors
2091
+ register with the copy THEY resolve, malloyyo reads the copy IT resolves,
2092
+ and when those differ every connection looks missing.
2093
+
2094
+ ${indent(listing)}
2095
+
2096
+ This is about paths, not versions \u2014 two copies of the same version are still
2097
+ two registries. Collapse the tree to one copy:
2098
+
2099
+ npm ls @malloydata/malloy # show who pulls in the extra copy
2100
+ npm dedupe # often enough on its own
2101
+
2102
+ Or run malloyyo somewhere that has no @malloydata/* dependencies of its own
2103
+ (a global install, or \`npx @malloydata/malloyyo\` from an empty directory).`;
2104
+ }
2105
+ async function importConnector(spec, from) {
2106
+ let target = spec;
2107
+ try {
2108
+ target = url2.pathToFileURL(requireFrom(from).resolve(spec)).href;
2109
+ } catch {
2110
+ }
2111
+ await import(target);
2112
+ }
2113
+ var pending = null;
2114
+ function loadConnectionTypes() {
2115
+ return pending ??= register();
2116
+ }
2117
+ async function register() {
2118
+ const here = selfDir();
2119
+ const failures = [];
2120
+ try {
2121
+ await import(CONNECTIONS_PKG);
2122
+ } catch (bulk) {
2123
+ failures.push({ pkg: CONNECTIONS_PKG, error: message(bulk) });
2124
+ const from = packageDir(CONNECTIONS_PKG, here) ?? here;
2125
+ for (const spec of CONNECTOR_IMPORTS) {
2126
+ try {
2127
+ await importConnector(spec, from);
2128
+ } catch (e) {
2129
+ failures.push({ pkg: spec, error: message(e) });
2130
+ }
2131
+ }
2132
+ }
2133
+ const types = getRegisteredConnectionTypes();
2134
+ const duplicates = malloyCopies();
2135
+ const split = duplicates.length > 1 ? duplicates : null;
2136
+ if (types.length === 0) {
2137
+ throw new Error(noTypesRegistered(failures));
2138
+ }
2139
+ return { types, failures, duplicates: split };
2140
+ }
2141
+ function noTypesRegistered(failures) {
2142
+ const dup = duplicateMalloyReport();
2143
+ const head = `0 connection types registered after loading ${CONNECTIONS_PKG} \u2014 no database connection can be created.
2144
+ `;
2145
+ const why = dup ? `
2146
+ ${dup}
2147
+ ` : `
2148
+ Only one @malloydata/malloy was found from here, so the connector packages
2149
+ themselves did not load or did not register. Check the install:
2150
+
2151
+ npm ls @malloydata/malloy ${CONNECTIONS_PKG}
2152
+ `;
2153
+ return head + why + failureDetail(failures) + `
2154
+ Nothing is wrong with your malloy-config.json.`;
2155
+ }
2156
+ function failureDetail(failures) {
2157
+ if (failures.length === 0) return "";
2158
+ return `
2159
+ Connector packages that failed to load:
2160
+ ` + failures.map((f) => indent(`${f.pkg}: ${f.error}`)).join("\n") + `
2161
+ `;
2162
+ }
2163
+ var warned2 = false;
2164
+ var warnedAboutDuplicates = false;
2165
+ function warnConnectionIssues(result) {
2166
+ if (warned2) return;
2167
+ warned2 = true;
2168
+ if (result.duplicates) {
2169
+ warnedAboutDuplicates = true;
2170
+ console.error(
2171
+ `warning: ${duplicateMalloyReport()}
2172
+ Registered so far: ${result.types.join(", ")}
2173
+ `
2174
+ );
2175
+ }
2176
+ const backends = result.failures.filter((f) => f.pkg !== CONNECTIONS_PKG);
2177
+ for (const f of backends) {
2178
+ console.error(`warning: connection backend ${f.pkg} unavailable \u2014 ${oneLine(f.error)}`);
2179
+ }
2180
+ const bulk = backends.length === 0 && result.failures.find((f) => f.pkg === CONNECTIONS_PKG);
2181
+ if (bulk) {
2182
+ console.error(
2183
+ `warning: ${CONNECTIONS_PKG} failed to load (${oneLine(bulk.error)}); loaded each backend directly instead \u2014 connections are available.`
2184
+ );
2185
+ }
2186
+ }
2187
+ async function initConnections() {
2188
+ const result = await loadConnectionTypes();
2189
+ warnConnectionIssues(result);
2190
+ return result;
2191
+ }
2192
+ var MISSING_CONNECTION = /No connection named ".*" found in config/;
2193
+ function annotateConnectionError(text) {
2194
+ if (!MISSING_CONNECTION.test(text)) return text;
2195
+ const registered = getRegisteredConnectionTypes();
2196
+ const dup = duplicateMalloyReport();
2197
+ const parts = [text, ""];
2198
+ if (dup) {
2199
+ parts.push(
2200
+ warnedAboutDuplicates ? `This is very likely the split @malloydata/malloy install warned about above.
2201
+ ` : `This is very likely the cause:
2202
+
2203
+ ${indent(dup)}
2204
+ `
2205
+ );
2206
+ }
2207
+ parts.push(
2208
+ `Connection types registered: ${registered.length ? registered.join(", ") : "(none)"}`,
2209
+ `Connections come from malloy-config.json, e.g.`,
2210
+ indent(`{ "connections": { "duckdb": { "is": "duckdb" } } }`)
2211
+ );
2212
+ return parts.join("\n");
2213
+ }
2214
+ async function withConnectionDiagnostics(fn) {
2215
+ try {
2216
+ return await fn();
2217
+ } catch (e) {
2218
+ const text = message(e);
2219
+ const annotated = annotateConnectionError(text);
2220
+ if (annotated === text) throw e;
2221
+ throw new Error(annotated, { cause: e });
2222
+ }
2223
+ }
2224
+
1970
2225
  // src/host.ts
1971
2226
  var ENTRY = "index.malloy";
1972
2227
  var IDLE_SHUTDOWN_MS = 6e4;
@@ -1996,7 +2251,7 @@ function fsReader() {
1996
2251
  if (u.protocol !== "file:") {
1997
2252
  throw new Error(`unsupported URL scheme for import: ${u.href}`);
1998
2253
  }
1999
- return fs.promises.readFile(u, "utf8");
2254
+ return fs2.promises.readFile(u, "utf8");
2000
2255
  }
2001
2256
  };
2002
2257
  }
@@ -2007,9 +2262,9 @@ async function loadConfig(rootUrl, reader) {
2007
2262
  });
2008
2263
  }
2009
2264
  async function makeRunner(root) {
2010
- await import("@malloydata/malloy-connections");
2011
- const abs = path2.resolve(root);
2012
- const rootUrl = url2.pathToFileURL(abs + path2.sep);
2265
+ await initConnections();
2266
+ const abs = path3.resolve(root);
2267
+ const rootUrl = url3.pathToFileURL(abs + path3.sep);
2013
2268
  const reader = fsReader();
2014
2269
  let configPromise = null;
2015
2270
  const getConfig = () => configPromise ??= loadConfig(rootUrl, reader);
@@ -2032,12 +2287,12 @@ async function makeRunner(root) {
2032
2287
  };
2033
2288
  async function leaseIn(entryFile, fn) {
2034
2289
  const config = await getConfig();
2035
- const { reader: prepared, entry } = prepareSource(reader, { url: path2.join(abs, entryFile) });
2290
+ const { reader: prepared, entry } = prepareSource(reader, { url: path3.join(abs, entryFile) });
2036
2291
  const runtime = new Runtime({ config, urlReader: prepared });
2037
2292
  inFlight++;
2038
2293
  clearIdleTimer();
2039
2294
  try {
2040
- return await fn(runtime, entry);
2295
+ return await withConnectionDiagnostics(() => fn(runtime, entry));
2041
2296
  } finally {
2042
2297
  inFlight--;
2043
2298
  if (inFlight === 0) scheduleIdleShutdown();
@@ -2046,7 +2301,7 @@ async function makeRunner(root) {
2046
2301
  const lease = (fn) => leaseIn(ENTRY, fn);
2047
2302
  return {
2048
2303
  root: abs,
2049
- entryExists: () => fs.existsSync(path2.join(abs, ENTRY)),
2304
+ entryExists: () => fs2.existsSync(path3.join(abs, ENTRY)),
2050
2305
  async dispose() {
2051
2306
  clearIdleTimer();
2052
2307
  if (!configPromise) return;
@@ -2375,24 +2630,24 @@ function readAll() {
2375
2630
  return {};
2376
2631
  }
2377
2632
  }
2378
- function loadCreds(url5) {
2379
- return readAll()[url5];
2633
+ function loadCreds(url6) {
2634
+ return readAll()[url6];
2380
2635
  }
2381
- function saveCreds(url5, creds) {
2636
+ function saveCreds(url6, creds) {
2382
2637
  const p = credsPath();
2383
2638
  mkdirSync(dirname(p), { recursive: true });
2384
2639
  const all = readAll();
2385
- all[url5] = creds;
2640
+ all[url6] = creds;
2386
2641
  writeFileSync(p, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
2387
2642
  try {
2388
2643
  chmodSync(p, 384);
2389
2644
  } catch {
2390
2645
  }
2391
2646
  }
2392
- function clearCreds(url5) {
2647
+ function clearCreds(url6) {
2393
2648
  const all = readAll();
2394
- if (!(url5 in all)) return false;
2395
- delete all[url5];
2649
+ if (!(url6 in all)) return false;
2650
+ delete all[url6];
2396
2651
  writeFileSync(credsPath(), JSON.stringify(all, null, 2) + "\n", { mode: 384 });
2397
2652
  return true;
2398
2653
  }
@@ -2425,8 +2680,8 @@ async function registerClient(registrationEndpoint, redirectUri) {
2425
2680
  if (!res.ok) throw new Error(`client registration failed: ${res.status} ${await res.text()}`);
2426
2681
  return (await res.json()).client_id;
2427
2682
  }
2428
- function openBrowser(url5) {
2429
- const [cmd, args] = process.platform === "darwin" ? ["open", [url5]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url5]] : ["xdg-open", [url5]];
2683
+ function openBrowser(url6) {
2684
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url6]] : process.platform === "win32" ? ["cmd", ["/c", "start", "", url6]] : ["xdg-open", [url6]];
2430
2685
  try {
2431
2686
  spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
2432
2687
  } catch {
@@ -2555,9 +2810,9 @@ Run: malloyyo login ${target.name}`);
2555
2810
  }
2556
2811
 
2557
2812
  // src/mcp.ts
2558
- import fs2 from "node:fs";
2559
- import path3 from "node:path";
2560
- import url3 from "node:url";
2813
+ import fs3 from "node:fs";
2814
+ import path4 from "node:path";
2815
+ import url4 from "node:url";
2561
2816
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2562
2817
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2563
2818
  import {
@@ -2645,7 +2900,7 @@ function defaultConfig(rootUrl) {
2645
2900
  });
2646
2901
  }
2647
2902
  async function loadConfig2(root, reader) {
2648
- const rootUrl = url3.pathToFileURL(root + path3.sep);
2903
+ const rootUrl = url4.pathToFileURL(root + path4.sep);
2649
2904
  let discovered;
2650
2905
  try {
2651
2906
  discovered = await discoverConfig2(rootUrl, rootUrl, reader);
@@ -2667,13 +2922,13 @@ function fsReader2() {
2667
2922
  if (u.protocol !== "file:") {
2668
2923
  throw new Error(`unsupported URL scheme for import: ${u.href}`);
2669
2924
  }
2670
- return fs2.promises.readFile(u, "utf8");
2925
+ return fs3.promises.readFile(u, "utf8");
2671
2926
  }
2672
2927
  };
2673
2928
  }
2674
2929
  function resolveUnderRoot(root, p) {
2675
- const abs = p.includes("://") ? path3.resolve(decodeURIComponent(new URL(p).pathname)) : path3.resolve(root, p);
2676
- if (abs !== root && !abs.startsWith(root + path3.sep)) {
2930
+ const abs = p.includes("://") ? path4.resolve(decodeURIComponent(new URL(p).pathname)) : path4.resolve(root, p);
2931
+ if (abs !== root && !abs.startsWith(root + path4.sep)) {
2677
2932
  throw new Error(`path is outside the project root: ${p}`);
2678
2933
  }
2679
2934
  return abs;
@@ -2682,7 +2937,7 @@ function makeConfigSource(root) {
2682
2937
  let cached;
2683
2938
  const signature = () => ["malloy-config.json", "malloy-config-local.json"].map((name) => {
2684
2939
  try {
2685
- const st = fs2.statSync(path3.join(root, name));
2940
+ const st = fs3.statSync(path4.join(root, name));
2686
2941
  return `${name}:${st.mtimeMs}:${st.size}`;
2687
2942
  } catch {
2688
2943
  return `${name}:absent`;
@@ -2702,12 +2957,12 @@ function makeWithRuntime(root, currentConfig) {
2702
2957
  return gateConfigProblems(problems, async () => {
2703
2958
  const resolved = "url" in input ? { url: resolveUnderRoot(root, input.url) } : {
2704
2959
  source: input.source,
2705
- baseUrl: input.baseUrl ? resolveUnderRoot(root, input.baseUrl) : root + path3.sep
2960
+ baseUrl: input.baseUrl ? resolveUnderRoot(root, input.baseUrl) : root + path4.sep
2706
2961
  };
2707
2962
  const { reader, entry, readSource } = prepareSource(fsReader2(), resolved);
2708
2963
  const runtime = new Runtime2({ config, urlReader: reader });
2709
2964
  try {
2710
- return await fn({ runtime, entry, readSource });
2965
+ return await withConnectionDiagnostics(() => fn({ runtime, entry, readSource }));
2711
2966
  } finally {
2712
2967
  await config.shutdown("idle");
2713
2968
  }
@@ -2716,7 +2971,7 @@ function makeWithRuntime(root, currentConfig) {
2716
2971
  }
2717
2972
  function makeExploreHost(root, currentConfig) {
2718
2973
  const withRuntime = makeWithRuntime(root, currentConfig);
2719
- const published = (ref) => ref === ENTRY2 && fs2.existsSync(path3.join(root, ENTRY2));
2974
+ const published = (ref) => ref === ENTRY2 && fs3.existsSync(path4.join(root, ENTRY2));
2720
2975
  return {
2721
2976
  withModel: (ref, fn) => {
2722
2977
  if (!published(ref)) throw new Error(`no published model '${ref}'`);
@@ -2736,8 +2991,8 @@ function makeDevelopHost(root, currentConfig) {
2736
2991
  return { withRuntime: makeWithRuntime(root, currentConfig) };
2737
2992
  }
2738
2993
  async function serveMcp(opts) {
2739
- await import("@malloydata/malloy-connections");
2740
- const root = path3.resolve(opts.root ?? process.cwd());
2994
+ await initConnections();
2995
+ const root = path4.resolve(opts.root ?? process.cwd());
2741
2996
  const mode = opts.mode ?? "explore";
2742
2997
  const currentConfig = makeConfigSource(root);
2743
2998
  const surface = mode === "develop" ? developSurface(makeDevelopHost(root, currentConfig)) : exploreSurface(makeExploreHost(root, currentConfig));
@@ -2761,8 +3016,8 @@ async function serveMcp(opts) {
2761
3016
 
2762
3017
  // src/dashboard.ts
2763
3018
  import http2 from "node:http";
2764
- import fs4 from "node:fs";
2765
- import path5 from "node:path";
3019
+ import fs5 from "node:fs";
3020
+ import path6 from "node:path";
2766
3021
  import * as esbuild2 from "esbuild";
2767
3022
 
2768
3023
  // src/shared/givens-url.ts
@@ -2807,11 +3062,11 @@ function navHtml(active, all, href, homeHref = "./") {
2807
3062
  }
2808
3063
 
2809
3064
  // src/discover.ts
2810
- import fs3 from "node:fs";
2811
- import path4 from "node:path";
3065
+ import fs4 from "node:fs";
3066
+ import path5 from "node:path";
2812
3067
  import { fileURLToPath } from "node:url";
2813
- import { createRequire } from "node:module";
2814
- var require2 = createRequire(import.meta.url);
3068
+ import { createRequire as createRequire2 } from "node:module";
3069
+ var require2 = createRequire2(import.meta.url);
2815
3070
  var HOST_LIBS = [
2816
3071
  "react",
2817
3072
  "react-dom",
@@ -2835,7 +3090,7 @@ function resolveRuntimeDir() {
2835
3090
  new URL("../src/frame-runtime/", import.meta.url)
2836
3091
  // built dist/ next to sibling src/ (checkout)
2837
3092
  ].map((u) => fileURLToPath(u));
2838
- const found = candidates.find((c) => fs3.existsSync(c));
3093
+ const found = candidates.find((c) => fs4.existsSync(c));
2839
3094
  if (!found) {
2840
3095
  throw new Error(
2841
3096
  "frame-runtime/ not found next to the CLI (looked in ./frame-runtime and ../src/frame-runtime). A published install should ship it in dist/; reinstall the CLI, or rebuild with `npm run build`."
@@ -2853,31 +3108,31 @@ var hostAliasPlugin = {
2853
3108
  }
2854
3109
  };
2855
3110
  async function discoverDashboards(root, runner) {
2856
- const dir = path4.join(root, "dashboards");
2857
- if (!fs3.existsSync(dir)) return [];
2858
- const files = fs3.readdirSync(dir).filter((f) => f.endsWith(".malloy")).sort();
3111
+ const dir = path5.join(root, "dashboards");
3112
+ if (!fs4.existsSync(dir)) return [];
3113
+ const files = fs4.readdirSync(dir).filter((f) => f.endsWith(".malloy")).sort();
2859
3114
  const dashboards = [];
2860
3115
  for (const file of files) {
2861
3116
  const base = file.slice(0, -".malloy".length);
2862
- const entryFile = path4.join("dashboards", file);
3117
+ const entryFile = path5.join("dashboards", file);
2863
3118
  const res = await runner.artifactForFile(entryFile, base);
2864
3119
  if (!res.ok) throw new Error(`dashboard ${file}: ${res.error}`);
2865
3120
  if (!res.artifact) continue;
2866
- const component = ["jsx", "tsx"].map((ext) => path4.join(dir, `${base}.${ext}`)).find((p) => fs3.existsSync(p));
3121
+ const component = ["jsx", "tsx"].map((ext) => path5.join(dir, `${base}.${ext}`)).find((p) => fs4.existsSync(p));
2867
3122
  dashboards.push({ ...res.artifact, name: res.artifact.name || base, entryFile, tsxPath: component });
2868
3123
  }
2869
3124
  return dashboards;
2870
3125
  }
2871
3126
  function browserBuildBase() {
2872
- const shims = path4.join(resolveRuntimeDir(), "..", "shims");
3127
+ const shims = path5.join(resolveRuntimeDir(), "..", "shims");
2873
3128
  return {
2874
3129
  platform: "browser",
2875
3130
  jsx: "automatic",
2876
3131
  loader: { ".css": "empty" },
2877
3132
  define: { "process.env.NODE_ENV": '"production"' },
2878
3133
  alias: {
2879
- assert: path4.join(shims, "assert.cjs"),
2880
- util: path4.join(shims, "util.cjs")
3134
+ assert: path5.join(shims, "assert.cjs"),
3135
+ util: path5.join(shims, "util.cjs")
2881
3136
  },
2882
3137
  banner: {
2883
3138
  js: "globalThis.process||={env:{},platform:'browser',versions:{},argv:[],cwd:()=>'/'};"
@@ -2886,17 +3141,17 @@ function browserBuildBase() {
2886
3141
  }
2887
3142
 
2888
3143
  // src/dashboard.ts
2889
- var resolveFrameEntry = () => path5.join(resolveRuntimeDir(), "..", "frame-entry.tsx");
2890
- var resolveInPageEntry = () => path5.join(resolveRuntimeDir(), "..", "frame-inpage-entry.tsx");
3144
+ var resolveFrameEntry = () => path6.join(resolveRuntimeDir(), "..", "frame-entry.tsx");
3145
+ var resolveInPageEntry = () => path6.join(resolveRuntimeDir(), "..", "frame-inpage-entry.tsx");
2891
3146
  var esc2 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
2892
3147
  function makeBundler() {
2893
3148
  const cache = /* @__PURE__ */ new Map();
2894
3149
  const frameEntry = resolveFrameEntry();
2895
3150
  const runtimeDir = resolveRuntimeDir();
2896
- const runtimeIndex = path5.join(runtimeDir, "index.ts");
2897
- const runtimeStamp = () => fs4.statSync(frameEntry).mtimeMs + fs4.readdirSync(runtimeDir).map((f) => fs4.statSync(path5.join(runtimeDir, f)).mtimeMs).reduce((a, b) => a + b, 0);
3151
+ const runtimeIndex = path6.join(runtimeDir, "index.ts");
3152
+ const runtimeStamp = () => fs5.statSync(frameEntry).mtimeMs + fs5.readdirSync(runtimeDir).map((f) => fs5.statSync(path6.join(runtimeDir, f)).mtimeMs).reduce((a, b) => a + b, 0);
2898
3153
  return async function bundle(dash) {
2899
- const stamp = runtimeStamp() + (dash.tsxPath ? fs4.statSync(dash.tsxPath).mtimeMs : 0);
3154
+ const stamp = runtimeStamp() + (dash.tsxPath ? fs5.statSync(dash.tsxPath).mtimeMs : 0);
2900
3155
  const hit = cache.get(dash.name);
2901
3156
  if (hit && hit.stamp === stamp) return hit.js;
2902
3157
  const result = await esbuild2.build({
@@ -2941,7 +3196,7 @@ function makeInPageBundler() {
2941
3196
  let cached;
2942
3197
  const entry = resolveInPageEntry();
2943
3198
  const runtimeDir = resolveRuntimeDir();
2944
- const stampOf = () => fs4.statSync(entry).mtimeMs + fs4.readdirSync(runtimeDir).map((f) => fs4.statSync(path5.join(runtimeDir, f)).mtimeMs).reduce((a, b) => a + b, 0);
3199
+ const stampOf = () => fs5.statSync(entry).mtimeMs + fs5.readdirSync(runtimeDir).map((f) => fs5.statSync(path6.join(runtimeDir, f)).mtimeMs).reduce((a, b) => a + b, 0);
2945
3200
  return async function bundle() {
2946
3201
  const stamp = stampOf();
2947
3202
  if (cached && cached.stamp === stamp) return cached.js;
@@ -3040,11 +3295,11 @@ window.addEventListener('message',async(e)=>{
3040
3295
  dash.title
3041
3296
  );
3042
3297
  }
3043
- function givensFromUrl(url5) {
3044
- return givensFromSearch(url5.search);
3298
+ function givensFromUrl(url6) {
3299
+ return givensFromSearch(url6.search);
3045
3300
  }
3046
- function urlStateFromUrl(url5) {
3047
- return urlStateFromSearch(url5.search);
3301
+ function urlStateFromUrl(url6) {
3302
+ return urlStateFromSearch(url6.search);
3048
3303
  }
3049
3304
  function frameDoc(dash, givenSpecs, initialGivens, initialUrlState, tileSpecs) {
3050
3305
  const info = {
@@ -3071,8 +3326,8 @@ async function readBody(req) {
3071
3326
  return Buffer.concat(chunks).toString("utf8");
3072
3327
  }
3073
3328
  async function serveDashboard(opts) {
3074
- await import("@malloydata/malloy-connections");
3075
- const root = path5.resolve(opts.root ?? process.cwd());
3329
+ await initConnections();
3330
+ const root = path6.resolve(opts.root ?? process.cwd());
3076
3331
  const port = opts.port ?? 4173;
3077
3332
  const framePort = port + 1;
3078
3333
  const frameBase = `http://localhost:${framePort}`;
@@ -3089,7 +3344,7 @@ async function serveDashboard(opts) {
3089
3344
  let byName = new Map(dashboards.map((d) => [d.name, d]));
3090
3345
  const bundle = makeBundler();
3091
3346
  const inPageBundle = makeInPageBundler();
3092
- const pick = (url5) => byName.get(url5.searchParams.get("d") ?? dashboards[0].name) ?? dashboards[0];
3347
+ const pick = (url6) => byName.get(url6.searchParams.get("d") ?? dashboards[0].name) ?? dashboards[0];
3093
3348
  async function resolveGivens(dash) {
3094
3349
  if (dash.tiles && dash.entryFile) {
3095
3350
  const t = await runner.dashboardTiles(dash.entryFile, dash.tiles);
@@ -3105,7 +3360,7 @@ async function serveDashboard(opts) {
3105
3360
  };
3106
3361
  let debounce;
3107
3362
  try {
3108
- fs4.watch(root, { recursive: true }, (_evt, filename) => {
3363
+ fs5.watch(root, { recursive: true }, (_evt, filename) => {
3109
3364
  const f = filename?.toString() ?? "";
3110
3365
  if (!f.endsWith(".malloy") && !f.includes("dashboards")) return;
3111
3366
  clearTimeout(debounce);
@@ -3123,15 +3378,15 @@ async function serveDashboard(opts) {
3123
3378
  }
3124
3379
  const handler = async (req, res) => {
3125
3380
  const onFramePort = (req.socket.localPort ?? port) === framePort;
3126
- const url5 = new URL(req.url ?? "/", `http://localhost:${onFramePort ? framePort : port}`);
3381
+ const url6 = new URL(req.url ?? "/", `http://localhost:${onFramePort ? framePort : port}`);
3127
3382
  const send = (code, type, body, extra = {}) => {
3128
3383
  res.writeHead(code, { "content-type": type, ...extra });
3129
3384
  res.end(body);
3130
3385
  };
3131
3386
  try {
3132
3387
  if (onFramePort) {
3133
- if (url5.pathname === "/frame") {
3134
- const dash = pick(url5);
3388
+ if (url6.pathname === "/frame") {
3389
+ const dash = pick(url6);
3135
3390
  const g = await resolveGivens(dash);
3136
3391
  if (!g.ok) {
3137
3392
  return send(
@@ -3143,23 +3398,23 @@ async function serveDashboard(opts) {
3143
3398
  return send(
3144
3399
  200,
3145
3400
  "text/html; charset=utf-8",
3146
- frameDoc(dash, g.union, givensFromUrl(url5), urlStateFromUrl(url5), g.tiles)
3401
+ frameDoc(dash, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles)
3147
3402
  );
3148
3403
  }
3149
- if (url5.pathname === "/bundle.js") {
3150
- return send(200, "application/javascript; charset=utf-8", await bundle(pick(url5)));
3404
+ if (url6.pathname === "/bundle.js") {
3405
+ return send(200, "application/javascript; charset=utf-8", await bundle(pick(url6)));
3151
3406
  }
3152
3407
  return send(404, "text/plain", "not found");
3153
3408
  }
3154
- if (url5.pathname === "/events") {
3409
+ if (url6.pathname === "/events") {
3155
3410
  res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-cache", connection: "keep-alive" });
3156
3411
  res.write("retry: 1000\n\n");
3157
3412
  sseClients.add(res);
3158
3413
  req.on("close", () => sseClients.delete(res));
3159
3414
  return;
3160
3415
  }
3161
- if (url5.pathname === "/") {
3162
- const dash = pick(url5);
3416
+ if (url6.pathname === "/") {
3417
+ const dash = pick(url6);
3163
3418
  if (!dash.tsxPath) {
3164
3419
  const g = await resolveGivens(dash);
3165
3420
  if (!g.ok) {
@@ -3172,19 +3427,19 @@ async function serveDashboard(opts) {
3172
3427
  return send(
3173
3428
  200,
3174
3429
  "text/html; charset=utf-8",
3175
- inPageShell(dash, dashboards, g.union, givensFromUrl(url5), urlStateFromUrl(url5), g.tiles)
3430
+ inPageShell(dash, dashboards, g.union, givensFromUrl(url6), urlStateFromUrl(url6), g.tiles)
3176
3431
  );
3177
3432
  }
3178
3433
  return send(
3179
3434
  200,
3180
3435
  "text/html; charset=utf-8",
3181
- parentShell(dash, frameBase, dashboards, givensFromUrl(url5), urlStateFromUrl(url5))
3436
+ parentShell(dash, frameBase, dashboards, givensFromUrl(url6), urlStateFromUrl(url6))
3182
3437
  );
3183
3438
  }
3184
- if (url5.pathname === "/inpage.js") {
3439
+ if (url6.pathname === "/inpage.js") {
3185
3440
  return send(200, "application/javascript; charset=utf-8", await inPageBundle());
3186
3441
  }
3187
- if (url5.pathname === "/api/run" && req.method === "POST") {
3442
+ if (url6.pathname === "/api/run" && req.method === "POST") {
3188
3443
  const { d, query, malloy, givens } = JSON.parse(await readBody(req));
3189
3444
  const dash = byName.get(d);
3190
3445
  if (!dash) return send(404, "application/json", JSON.stringify({ ok: false, problems: [{ message: `no dashboard '${d}'` }] }));
@@ -3215,15 +3470,15 @@ async function serveDashboard(opts) {
3215
3470
  }
3216
3471
 
3217
3472
  // src/bundle.ts
3218
- import fs6 from "node:fs";
3219
- import path7 from "node:path";
3220
- import { createRequire as createRequire2 } from "node:module";
3473
+ import fs7 from "node:fs";
3474
+ import path8 from "node:path";
3475
+ import { createRequire as createRequire3 } from "node:module";
3221
3476
  import * as esbuild3 from "esbuild";
3222
3477
 
3223
3478
  // src/static-server.ts
3224
- import fs5 from "node:fs";
3479
+ import fs6 from "node:fs";
3225
3480
  import http3 from "node:http";
3226
- import path6 from "node:path";
3481
+ import path7 from "node:path";
3227
3482
  var MIME = {
3228
3483
  ".html": "text/html; charset=utf-8",
3229
3484
  ".js": "text/javascript; charset=utf-8",
@@ -3238,12 +3493,12 @@ var MIME = {
3238
3493
  function serveStatic(dir, port) {
3239
3494
  const server = http3.createServer((req, res) => {
3240
3495
  const rel = decodeURIComponent((req.url ?? "/").split("?")[0]);
3241
- let file = path6.join(dir, rel === "/" ? "index.html" : rel);
3496
+ let file = path7.join(dir, rel === "/" ? "index.html" : rel);
3242
3497
  if (!file.startsWith(dir)) return void res.writeHead(403).end();
3243
- if (fs5.existsSync(file) && fs5.statSync(file).isDirectory()) file = path6.join(file, "index.html");
3244
- if (!fs5.existsSync(file)) return void res.writeHead(404).end("not found");
3245
- const st = fs5.statSync(file);
3246
- const type = MIME[path6.extname(file)] ?? "application/octet-stream";
3498
+ if (fs6.existsSync(file) && fs6.statSync(file).isDirectory()) file = path7.join(file, "index.html");
3499
+ if (!fs6.existsSync(file)) return void res.writeHead(404).end("not found");
3500
+ const st = fs6.statSync(file);
3501
+ const type = MIME[path7.extname(file)] ?? "application/octet-stream";
3247
3502
  const range = req.headers.range;
3248
3503
  if (range) {
3249
3504
  const m = /bytes=(\d*)-(\d*)/.exec(range);
@@ -3255,10 +3510,10 @@ function serveStatic(dir, port) {
3255
3510
  "Accept-Ranges": "bytes",
3256
3511
  "Content-Length": end - start + 1
3257
3512
  });
3258
- return void fs5.createReadStream(file, { start, end }).pipe(res);
3513
+ return void fs6.createReadStream(file, { start, end }).pipe(res);
3259
3514
  }
3260
3515
  res.writeHead(200, { "Content-Type": type, "Content-Length": st.size, "Accept-Ranges": "bytes" });
3261
- fs5.createReadStream(file).pipe(res);
3516
+ fs6.createReadStream(file).pipe(res);
3262
3517
  });
3263
3518
  return new Promise((resolve3, reject) => {
3264
3519
  let attempt = 0;
@@ -3275,7 +3530,7 @@ function serveStatic(dir, port) {
3275
3530
  if (p !== port) console.log(`
3276
3531
  (port ${port} busy \u2014 using ${p})`);
3277
3532
  console.log(`
3278
- serving ${path6.basename(dir)}/ on http://localhost:${p} (ctrl-c to stop)`);
3533
+ serving ${path7.basename(dir)}/ on http://localhost:${p} (ctrl-c to stop)`);
3279
3534
  resolve3();
3280
3535
  });
3281
3536
  };
@@ -3284,19 +3539,19 @@ serving ${path6.basename(dir)}/ on http://localhost:${p} (ctrl-c to stop)`);
3284
3539
  }
3285
3540
 
3286
3541
  // src/bundle.ts
3287
- var require3 = createRequire2(import.meta.url);
3542
+ var require3 = createRequire3(import.meta.url);
3288
3543
  var esc3 = (s) => s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
3289
3544
  function inlineModelFiles(root) {
3290
3545
  const files = {};
3291
3546
  const skip = /* @__PURE__ */ new Set(["node_modules", ".git", "docs", "dist"]);
3292
3547
  const walk = (dir) => {
3293
- for (const entry of fs6.readdirSync(dir, { withFileTypes: true })) {
3548
+ for (const entry of fs7.readdirSync(dir, { withFileTypes: true })) {
3294
3549
  if (entry.name.startsWith(".") || skip.has(entry.name)) continue;
3295
- const abs = path7.join(dir, entry.name);
3550
+ const abs = path8.join(dir, entry.name);
3296
3551
  if (entry.isDirectory()) walk(abs);
3297
3552
  else if (entry.name.endsWith(".malloy")) {
3298
- const rel = path7.relative(root, abs).split(path7.sep).join("/");
3299
- files[`file:///${rel}`] = fs6.readFileSync(abs, "utf8");
3553
+ const rel = path8.relative(root, abs).split(path8.sep).join("/");
3554
+ files[`file:///${rel}`] = fs7.readFileSync(abs, "utf8");
3300
3555
  }
3301
3556
  }
3302
3557
  };
@@ -3359,12 +3614,12 @@ function copyDuckDBAssets(outDir) {
3359
3614
  "duckdb-browser-mvp.worker.js",
3360
3615
  "duckdb-browser-eh.worker.js"
3361
3616
  ];
3362
- const dir = path7.join(outDir, "duckdb");
3363
- fs6.mkdirSync(dir, { recursive: true });
3617
+ const dir = path8.join(outDir, "duckdb");
3618
+ fs7.mkdirSync(dir, { recursive: true });
3364
3619
  const copied = [];
3365
3620
  for (const n of names) {
3366
3621
  const src = require3.resolve(`@duckdb/duckdb-wasm/dist/${n}`);
3367
- fs6.copyFileSync(src, path7.join(dir, n));
3622
+ fs7.copyFileSync(src, path8.join(dir, n));
3368
3623
  copied.push(n);
3369
3624
  }
3370
3625
  return copied;
@@ -3463,37 +3718,37 @@ body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 -apple-system,B
3463
3718
  .index span{font-size:13px;color:var(--muted)}
3464
3719
  ` + NAV_CSS;
3465
3720
  async function bundleDashboards(opts = {}) {
3466
- const root = path7.resolve(opts.root ?? process.cwd());
3467
- const outDir = path7.resolve(root, opts.out ?? "docs");
3468
- const title = opts.title ?? path7.basename(root);
3721
+ const root = path8.resolve(opts.root ?? process.cwd());
3722
+ const outDir = path8.resolve(root, opts.out ?? "docs");
3723
+ const title = opts.title ?? path8.basename(root);
3469
3724
  const target = opts.target ?? "pages";
3470
3725
  const analytics = opts.analytics ?? readSiteConfig(root).analytics;
3471
3726
  const cleanUrls = target === "vercel";
3472
3727
  const selfHostDuckdb = opts.duckdb === "bundled";
3473
3728
  const runner = await makeRunner(root);
3474
3729
  const dashboards = await discoverDashboards(root, runner);
3475
- if (dashboards.length === 0) throw new Error(`no dashboards found in ${path7.join(root, "dashboards")}`);
3476
- const manifestPath = path7.join(outDir, ".bundle-manifest.json");
3730
+ if (dashboards.length === 0) throw new Error(`no dashboards found in ${path8.join(root, "dashboards")}`);
3731
+ const manifestPath = path8.join(outDir, ".bundle-manifest.json");
3477
3732
  let priorData = [];
3478
3733
  try {
3479
- priorData = JSON.parse(fs6.readFileSync(manifestPath, "utf8")).dataFiles ?? [];
3734
+ priorData = JSON.parse(fs7.readFileSync(manifestPath, "utf8")).dataFiles ?? [];
3480
3735
  } catch {
3481
3736
  }
3482
3737
  for (const sub of ["assets", "duckdb"]) {
3483
- fs6.rmSync(path7.join(outDir, sub), { recursive: true, force: true });
3738
+ fs7.rmSync(path8.join(outDir, sub), { recursive: true, force: true });
3484
3739
  }
3485
- if (fs6.existsSync(outDir)) {
3486
- for (const f of fs6.readdirSync(outDir)) {
3487
- if (f.endsWith(".html")) fs6.rmSync(path7.join(outDir, f), { force: true });
3740
+ if (fs7.existsSync(outDir)) {
3741
+ for (const f of fs7.readdirSync(outDir)) {
3742
+ if (f.endsWith(".html")) fs7.rmSync(path8.join(outDir, f), { force: true });
3488
3743
  }
3489
3744
  }
3490
- fs6.mkdirSync(path7.join(outDir, "assets"), { recursive: true });
3745
+ fs7.mkdirSync(path8.join(outDir, "assets"), { recursive: true });
3491
3746
  if (target === "pages") {
3492
- fs6.writeFileSync(path7.join(outDir, ".nojekyll"), "");
3747
+ fs7.writeFileSync(path8.join(outDir, ".nojekyll"), "");
3493
3748
  } else {
3494
- fs6.rmSync(path7.join(outDir, ".nojekyll"), { force: true });
3495
- fs6.writeFileSync(
3496
- path7.join(outDir, "vercel.json"),
3749
+ fs7.rmSync(path8.join(outDir, ".nojekyll"), { force: true });
3750
+ fs7.writeFileSync(
3751
+ path8.join(outDir, "vercel.json"),
3497
3752
  JSON.stringify(
3498
3753
  {
3499
3754
  $schema: "https://openapi.vercel.sh/vercel.json",
@@ -3515,53 +3770,53 @@ async function bundleDashboards(opts = {}) {
3515
3770
  );
3516
3771
  }
3517
3772
  const modelFiles = inlineModelFiles(root);
3518
- const outRel = path7.relative(root, outDir).split(path7.sep).join("/");
3773
+ const outRel = path8.relative(root, outDir).split(path8.sep).join("/");
3519
3774
  const usedFiles = reachableModelFiles(
3520
3775
  modelFiles,
3521
3776
  dashboards.map((d) => d.entryFile).filter((f) => !!f)
3522
3777
  );
3523
3778
  const { map: tableFiles, copies } = tableFilePlan(usedFiles, outRel);
3524
3779
  for (const rel of copies) {
3525
- const from = path7.join(root, rel);
3526
- if (!fs6.existsSync(from)) {
3780
+ const from = path8.join(root, rel);
3781
+ if (!fs7.existsSync(from)) {
3527
3782
  throw new Error(
3528
3783
  `model reads '${rel}' but ${from} does not exist.
3529
3784
  Data files are referenced by a path relative to the project root.`
3530
3785
  );
3531
3786
  }
3532
- const to = path7.join(outDir, rel);
3533
- fs6.mkdirSync(path7.dirname(to), { recursive: true });
3534
- fs6.copyFileSync(from, to);
3787
+ const to = path8.join(outDir, rel);
3788
+ fs7.mkdirSync(path8.dirname(to), { recursive: true });
3789
+ fs7.copyFileSync(from, to);
3535
3790
  }
3536
3791
  const copiedData = copies;
3537
3792
  for (const stale of priorData) {
3538
3793
  if (copies.includes(stale)) continue;
3539
- fs6.rmSync(path7.join(outDir, stale), { force: true });
3794
+ fs7.rmSync(path8.join(outDir, stale), { force: true });
3540
3795
  try {
3541
- fs6.rmdirSync(path7.dirname(path7.join(outDir, stale)));
3796
+ fs7.rmdirSync(path8.dirname(path8.join(outDir, stale)));
3542
3797
  } catch {
3543
3798
  }
3544
3799
  console.log(` removed stale ${stale}`);
3545
3800
  }
3546
- fs6.writeFileSync(manifestPath, JSON.stringify({ dataFiles: copies }, null, 2) + "\n");
3547
- fs6.writeFileSync(
3548
- path7.join(outDir, "assets", "model-files.js"),
3801
+ fs7.writeFileSync(manifestPath, JSON.stringify({ dataFiles: copies }, null, 2) + "\n");
3802
+ fs7.writeFileSync(
3803
+ path8.join(outDir, "assets", "model-files.js"),
3549
3804
  `window.__MODEL_FILES__ = ${JSON.stringify(modelFiles)};
3550
3805
  window.__TABLE_FILES__ = ${JSON.stringify(tableFiles)};
3551
3806
  ` + (selfHostDuckdb ? `window.__DUCKDB_BASE__ = "./duckdb/";
3552
3807
  ` : "")
3553
3808
  );
3554
- const landing = ["jsx", "tsx"].map((ext) => path7.join(root, "dashboards", `index.${ext}`)).find((f) => fs6.existsSync(f));
3809
+ const landing = ["jsx", "tsx"].map((ext) => path8.join(root, "dashboards", `index.${ext}`)).find((f) => fs7.existsSync(f));
3555
3810
  const runtimeDir = resolveRuntimeDir();
3556
- const runtimeIndex = path7.join(runtimeDir, "index.ts");
3557
- const wasmEntry = path7.join(runtimeDir, "..", "frame-wasm-entry.tsx");
3811
+ const runtimeIndex = path8.join(runtimeDir, "index.ts");
3812
+ const wasmEntry = path8.join(runtimeDir, "..", "frame-wasm-entry.tsx");
3558
3813
  const byEntry = new Map(dashboards.map((d) => [`vdash:${d.name}`, d]));
3559
3814
  await esbuild3.build({
3560
3815
  entryPoints: Object.fromEntries(dashboards.map((d) => [d.name, `vdash:${d.name}`])),
3561
3816
  bundle: true,
3562
3817
  splitting: true,
3563
3818
  format: "esm",
3564
- outdir: path7.join(outDir, "assets"),
3819
+ outdir: path8.join(outDir, "assets"),
3565
3820
  minify: true,
3566
3821
  logLevel: "warning",
3567
3822
  ...browserBuildBase(),
@@ -3582,7 +3837,7 @@ boot(Dashboard);
3582
3837
  loader: "js",
3583
3838
  // Resolve the component's own imports (react, @malloyyo/dashboard)
3584
3839
  // from the model repo's directory, matching `dashboard dev`.
3585
- resolveDir: path7.dirname(dash.tsxPath ?? path7.join(root, "dashboards", "x"))
3840
+ resolveDir: path8.dirname(dash.tsxPath ?? path8.join(root, "dashboards", "x"))
3586
3841
  };
3587
3842
  });
3588
3843
  b.onResolve({ filter: /^@malloyyo\/dashboard$/ }, () => ({ path: runtimeIndex }));
@@ -3591,7 +3846,7 @@ boot(Dashboard);
3591
3846
  hostAliasPlugin
3592
3847
  ]
3593
3848
  });
3594
- fs6.writeFileSync(path7.join(outDir, "assets", "site.css"), SITE_CSS);
3849
+ fs7.writeFileSync(path8.join(outDir, "assets", "site.css"), SITE_CSS);
3595
3850
  for (const d of dashboards) {
3596
3851
  let specs = [];
3597
3852
  let tileSpecs;
@@ -3604,9 +3859,9 @@ boot(Dashboard);
3604
3859
  if (!got.ok) throw new Error(`dashboard ${d.name}: ${got.error}`);
3605
3860
  specs = got.givens;
3606
3861
  }
3607
- fs6.writeFileSync(path7.join(outDir, `${d.name}.html`), page(d, dashboards, title, specs, tileSpecs, cleanUrls, analytics));
3862
+ fs7.writeFileSync(path8.join(outDir, `${d.name}.html`), page(d, dashboards, title, specs, tileSpecs, cleanUrls, analytics));
3608
3863
  }
3609
- fs6.writeFileSync(path7.join(outDir, "index.html"), indexPage(dashboards, title, !!landing, cleanUrls, analytics));
3864
+ fs7.writeFileSync(path8.join(outDir, "index.html"), indexPage(dashboards, title, !!landing, cleanUrls, analytics));
3610
3865
  if (landing) {
3611
3866
  await esbuild3.build({
3612
3867
  stdin: {
@@ -3615,13 +3870,13 @@ import { createRoot } from "react-dom/client";
3615
3870
  import Landing from ${JSON.stringify(landing)};
3616
3871
  createRoot(document.getElementById("root")).render(React.createElement(Landing, { dashboards: window.__DASHBOARDS__ || [] }));
3617
3872
  `,
3618
- resolveDir: path7.dirname(landing),
3873
+ resolveDir: path8.dirname(landing),
3619
3874
  loader: "js"
3620
3875
  },
3621
3876
  bundle: true,
3622
3877
  format: "esm",
3623
3878
  minify: true,
3624
- outfile: path7.join(outDir, "assets", "index.js"),
3879
+ outfile: path8.join(outDir, "assets", "index.js"),
3625
3880
  logLevel: "warning",
3626
3881
  // Same base as the dashboard pass. A landing page needs no Malloy today,
3627
3882
  // but one that imported anything reaching antlr4ts would otherwise die at
@@ -3631,12 +3886,12 @@ createRoot(document.getElementById("root")).render(React.createElement(Landing,
3631
3886
  });
3632
3887
  }
3633
3888
  const duck = selfHostDuckdb ? copyDuckDBAssets(outDir) : [];
3634
- if (!selfHostDuckdb) fs6.rmSync(path7.join(outDir, "duckdb"), { recursive: true, force: true });
3635
- const bytes = (p) => fs6.statSync(p).size;
3636
- const assetDir = path7.join(outDir, "assets");
3637
- const jsTotal = fs6.readdirSync(assetDir).filter((f) => f.endsWith(".js")).reduce((a, f) => a + bytes(path7.join(assetDir, f)), 0);
3889
+ if (!selfHostDuckdb) fs7.rmSync(path8.join(outDir, "duckdb"), { recursive: true, force: true });
3890
+ const bytes = (p) => fs7.statSync(p).size;
3891
+ const assetDir = path8.join(outDir, "assets");
3892
+ const jsTotal = fs7.readdirSync(assetDir).filter((f) => f.endsWith(".js")).reduce((a, f) => a + bytes(path8.join(assetDir, f)), 0);
3638
3893
  console.log(`
3639
- bundled ${dashboards.length} dashboard(s) \u2192 ${path7.relative(process.cwd(), outDir) || "."}/`);
3894
+ bundled ${dashboards.length} dashboard(s) \u2192 ${path8.relative(process.cwd(), outDir) || "."}/`);
3640
3895
  for (const d of dashboards) console.log(` ${d.name}.html ${d.title ?? ""}`);
3641
3896
  console.log(`
3642
3897
  js ${(jsTotal / 1048576).toFixed(2)} MB`);
@@ -3646,13 +3901,13 @@ bundled ${dashboards.length} dashboard(s) \u2192 ${path7.relative(process.cwd(),
3646
3901
  console.log(` model ${Object.keys(modelFiles).length} .malloy files inlined`);
3647
3902
  if (analytics) console.log(` analytics ${analytics}`);
3648
3903
  if (copiedData.length) {
3649
- const mb = copiedData.reduce((a, r) => a + fs6.statSync(path7.join(root, r)).size, 0) / 1048576;
3904
+ const mb = copiedData.reduce((a, r) => a + fs7.statSync(path8.join(root, r)).size, 0) / 1048576;
3650
3905
  console.log(` data ${copiedData.length} file(s) copied, ${mb.toFixed(1)} MB (same-origin)`);
3651
3906
  }
3652
3907
  console.log(
3653
3908
  target === "pages" ? `
3654
- Publish: commit ${path7.basename(outDir)}/ and point GitHub Pages at it.` : `
3655
- Publish: deploy ${path7.basename(outDir)}/ to Vercel (vercel.json written: clean URLs + asset caching).`
3909
+ Publish: commit ${path8.basename(outDir)}/ and point GitHub Pages at it.` : `
3910
+ Publish: deploy ${path8.basename(outDir)}/ to Vercel (vercel.json written: clean URLs + asset caching).`
3656
3911
  );
3657
3912
  if (opts.serve !== false) {
3658
3913
  await serveStatic(outDir, opts.port ?? 4180);
@@ -3662,8 +3917,8 @@ Publish: deploy ${path7.basename(outDir)}/ to Vercel (vercel.json written: clean
3662
3917
  }
3663
3918
 
3664
3919
  // src/init.ts
3665
- import fs7 from "node:fs";
3666
- import path8 from "node:path";
3920
+ import fs8 from "node:fs";
3921
+ import path9 from "node:path";
3667
3922
  import { fileURLToPath as fileURLToPath2 } from "node:url";
3668
3923
  var AUTHOR_SERVER = "malloyyo_author";
3669
3924
  var AUTHOR_MCP = {
@@ -3698,11 +3953,11 @@ function withAuthorPermissions(input) {
3698
3953
  return { settings, added };
3699
3954
  }
3700
3955
  function allowAuthorTools(root) {
3701
- const file = path8.join(root, ".claude", "settings.json");
3956
+ const file = path9.join(root, ".claude", "settings.json");
3702
3957
  let existing;
3703
- if (fs7.existsSync(file)) {
3958
+ if (fs8.existsSync(file)) {
3704
3959
  try {
3705
- existing = JSON.parse(fs7.readFileSync(file, "utf8"));
3960
+ existing = JSON.parse(fs8.readFileSync(file, "utf8"));
3706
3961
  } catch {
3707
3962
  return { added: [], note: ".claude/settings.json isn't valid JSON \u2014 left as-is" };
3708
3963
  }
@@ -3710,8 +3965,8 @@ function allowAuthorTools(root) {
3710
3965
  const merged = withAuthorPermissions(existing);
3711
3966
  if ("error" in merged) return { added: [], note: `${merged.error} \u2014 left as-is` };
3712
3967
  if (merged.added.length === 0) return { added: [] };
3713
- fs7.mkdirSync(path8.dirname(file), { recursive: true });
3714
- fs7.writeFileSync(file, JSON.stringify(merged.settings, null, 2) + "\n");
3968
+ fs8.mkdirSync(path9.dirname(file), { recursive: true });
3969
+ fs8.writeFileSync(file, JSON.stringify(merged.settings, null, 2) + "\n");
3715
3970
  return { added: merged.added };
3716
3971
  }
3717
3972
  function exportableNames(src) {
@@ -3726,11 +3981,11 @@ function exportableNames(src) {
3726
3981
  return [...names];
3727
3982
  }
3728
3983
  function scaffoldIndex(root) {
3729
- const indexPath = path8.join(root, "index.malloy");
3730
- if (fs7.existsSync(indexPath)) {
3984
+ const indexPath = path9.join(root, "index.malloy");
3985
+ if (fs8.existsSync(indexPath)) {
3731
3986
  return { wrote: false, note: "index.malloy already exists \u2014 left as-is" };
3732
3987
  }
3733
- const models = fs7.readdirSync(root).filter((f) => f.endsWith(".malloy") && f !== "index.malloy").sort();
3988
+ const models = fs8.readdirSync(root).filter((f) => f.endsWith(".malloy") && f !== "index.malloy").sort();
3734
3989
  if (models.length === 0) {
3735
3990
  return { wrote: false, note: "no .malloy files found \u2014 skipped index.malloy" };
3736
3991
  }
@@ -3743,7 +3998,7 @@ function scaffoldIndex(root) {
3743
3998
  ];
3744
3999
  let anyNames = false;
3745
4000
  for (const file of models) {
3746
- const names = exportableNames(fs7.readFileSync(path8.join(root, file), "utf8"));
4001
+ const names = exportableNames(fs8.readFileSync(path9.join(root, file), "utf8"));
3747
4002
  if (names.length === 0) {
3748
4003
  blocks.push(`// ${file}: no top-level source/query/given detected \u2014 add exports by hand`);
3749
4004
  continue;
@@ -3754,49 +4009,49 @@ function scaffoldIndex(root) {
3754
4009
  blocks.push(`export { ${list} }`);
3755
4010
  blocks.push("");
3756
4011
  }
3757
- fs7.writeFileSync(indexPath, blocks.join("\n") + "\n");
4012
+ fs8.writeFileSync(indexPath, blocks.join("\n") + "\n");
3758
4013
  return {
3759
4014
  wrote: true,
3760
4015
  note: anyNames ? `wrote index.malloy re-exporting ${models.length} model file(s) \u2014 REVIEW it` : "wrote index.malloy skeleton \u2014 no exports detected, fill them in by hand"
3761
4016
  };
3762
4017
  }
3763
4018
  function installSkills(root) {
3764
- const distDir = path8.dirname(fileURLToPath2(import.meta.url));
4019
+ const distDir = path9.dirname(fileURLToPath2(import.meta.url));
3765
4020
  const candidates = [
3766
- path8.join(distDir, "templates", "skills"),
3767
- path8.join(distDir, "..", "src", "templates", "skills")
4021
+ path9.join(distDir, "templates", "skills"),
4022
+ path9.join(distDir, "..", "src", "templates", "skills")
3768
4023
  ];
3769
- const srcSkills = candidates.find((p) => fs7.existsSync(p));
4024
+ const srcSkills = candidates.find((p) => fs8.existsSync(p));
3770
4025
  if (!srcSkills) return { wrote: [], skipped: [], note: "no skill templates found \u2014 skipped" };
3771
- const destSkills = path8.join(root, ".claude", "skills");
3772
- fs7.mkdirSync(destSkills, { recursive: true });
4026
+ const destSkills = path9.join(root, ".claude", "skills");
4027
+ fs8.mkdirSync(destSkills, { recursive: true });
3773
4028
  const wrote = [];
3774
4029
  const skipped = [];
3775
- for (const name of fs7.readdirSync(srcSkills)) {
3776
- const from = path8.join(srcSkills, name);
3777
- if (!fs7.statSync(from).isDirectory()) continue;
3778
- const to = path8.join(destSkills, name);
3779
- if (fs7.existsSync(to)) {
4030
+ for (const name of fs8.readdirSync(srcSkills)) {
4031
+ const from = path9.join(srcSkills, name);
4032
+ if (!fs8.statSync(from).isDirectory()) continue;
4033
+ const to = path9.join(destSkills, name);
4034
+ if (fs8.existsSync(to)) {
3780
4035
  skipped.push(name);
3781
4036
  continue;
3782
4037
  }
3783
- fs7.cpSync(from, to, { recursive: true });
4038
+ fs8.cpSync(from, to, { recursive: true });
3784
4039
  wrote.push(name);
3785
4040
  }
3786
4041
  return { wrote, skipped };
3787
4042
  }
3788
4043
  async function initCmd(dir) {
3789
- const root = path8.resolve(dir);
3790
- if (!fs7.existsSync(root) || !fs7.statSync(root).isDirectory()) {
4044
+ const root = path9.resolve(dir);
4045
+ if (!fs8.existsSync(root) || !fs8.statSync(root).isDirectory()) {
3791
4046
  throw new Error(`not a directory: ${root}`);
3792
4047
  }
3793
- const mcpPath = path8.join(root, ".mcp.json");
3794
- if (fs7.existsSync(mcpPath)) {
4048
+ const mcpPath = path9.join(root, ".mcp.json");
4049
+ if (fs8.existsSync(mcpPath)) {
3795
4050
  console.log(`\u2022 .mcp.json exists \u2014 leaving it. For author-by-default it should be:`);
3796
4051
  console.log(` ${JSON.stringify(AUTHOR_MCP.mcpServers.malloyyo_author)}`);
3797
4052
  console.log(` (server key "malloyyo_author", command "malloyyo mcp --develop").`);
3798
4053
  } else {
3799
- fs7.writeFileSync(mcpPath, JSON.stringify(AUTHOR_MCP, null, 2) + "\n");
4054
+ fs8.writeFileSync(mcpPath, JSON.stringify(AUTHOR_MCP, null, 2) + "\n");
3800
4055
  console.log(`\u2713 wrote .mcp.json \u2014 \`cd ${dir} && claude\` now opens in AUTHOR mode`);
3801
4056
  }
3802
4057
  const idx = scaffoldIndex(root);
@@ -3830,27 +4085,45 @@ async function initCmd(dir) {
3830
4085
  }
3831
4086
 
3832
4087
  // src/sql.ts
3833
- import fs8 from "node:fs";
3834
- import path9 from "node:path";
3835
- import url4 from "node:url";
3836
- import { MalloyConfig as MalloyConfig3, discoverConfig as discoverConfig3 } from "@malloydata/malloy";
4088
+ import fs9 from "node:fs";
4089
+ import path10 from "node:path";
4090
+ import url5 from "node:url";
4091
+ import { MalloyConfig as MalloyConfig3, Runtime as Runtime3, discoverConfig as discoverConfig3 } from "@malloydata/malloy";
3837
4092
  function fileReader() {
3838
4093
  return {
3839
4094
  readURL: async (u) => {
3840
4095
  if (u.protocol !== "file:") {
3841
4096
  throw new Error(`unsupported URL scheme for import: ${u.href}`);
3842
4097
  }
3843
- return fs8.promises.readFile(u, "utf8");
4098
+ return fs9.promises.readFile(u, "utf8");
3844
4099
  }
3845
4100
  };
3846
4101
  }
3847
4102
  async function loadConfig3(rootDir) {
3848
- const rootUrl = url4.pathToFileURL(rootDir.endsWith(path9.sep) ? rootDir : rootDir + path9.sep);
4103
+ const rootUrl = url5.pathToFileURL(rootDir.endsWith(path10.sep) ? rootDir : rootDir + path10.sep);
3849
4104
  const discovered = await discoverConfig3(rootUrl, rootUrl, fileReader()).catch(() => null);
3850
4105
  return discovered ?? new MalloyConfig3({ includeDefaultConnections: true }, {
3851
4106
  rootDirectory: rootUrl.toString()
3852
4107
  });
3853
4108
  }
4109
+ var TYPED_ROW_CAP = 1e6;
4110
+ function malloyStringLiteral(sql) {
4111
+ const body = sql.trim().replace(/;\s*$/, "") + "\n";
4112
+ const escaped = body.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\r/g, "\\r").replace(/\n/g, "\\n").replace(/\t/g, "\\t");
4113
+ return `"${escaped}"`;
4114
+ }
4115
+ async function runTyped(cfg, name, sql) {
4116
+ if (name.includes("`")) return null;
4117
+ try {
4118
+ const runtime = new Runtime3({ config: cfg, urlReader: fileReader() });
4119
+ const query = `run: \`${name}\`.sql(${malloyStringLiteral(sql)})`;
4120
+ const result = await runtime.loadQuery(query).run({ rowLimit: TYPED_ROW_CAP });
4121
+ const rows = jsonRows(result);
4122
+ return rows.length >= TYPED_ROW_CAP ? null : rows;
4123
+ } catch {
4124
+ return null;
4125
+ }
4126
+ }
3854
4127
  async function readStdin() {
3855
4128
  const chunks = [];
3856
4129
  for await (const chunk of process.stdin) chunks.push(chunk);
@@ -3858,22 +4131,25 @@ async function readStdin() {
3858
4131
  }
3859
4132
  async function resolveSql(opts) {
3860
4133
  if (opts.execute != null) return opts.execute;
3861
- if (opts.file) return fs8.promises.readFile(opts.file, "utf8");
4134
+ if (opts.file) return fs9.promises.readFile(opts.file, "utf8");
3862
4135
  return readStdin();
3863
4136
  }
3864
4137
  async function sqlCmd(connection, opts) {
3865
4138
  const name = connection ?? "duckdb";
3866
- const rootDir = path9.resolve(opts.root ?? ".");
4139
+ const rootDir = path10.resolve(opts.root ?? ".");
3867
4140
  const sql = (await resolveSql(opts)).trim();
3868
4141
  if (!sql) {
3869
4142
  throw new Error("no SQL provided \u2014 pass -e <sql>, -f <file>, or pipe it via stdin");
3870
4143
  }
3871
- await import("@malloydata/malloy-connections");
4144
+ await initConnections();
3872
4145
  const cfg = await loadConfig3(rootDir);
3873
4146
  try {
3874
- const conn = await cfg.connections.lookupConnection(name);
3875
- const result = await conn.runSQL(sql);
3876
- const rows = result?.rows ?? [];
4147
+ let rows = await runTyped(cfg, name, sql);
4148
+ if (rows === null) {
4149
+ const conn = await withConnectionDiagnostics(() => cfg.connections.lookupConnection(name));
4150
+ const result = await conn.runSQL(sql);
4151
+ rows = result?.rows ?? [];
4152
+ }
3877
4153
  if (opts.json) {
3878
4154
  process.stdout.write(JSON.stringify(rows, null, 2) + "\n");
3879
4155
  } else if (rows.length === 0) {
@@ -3888,22 +4164,22 @@ async function sqlCmd(connection, opts) {
3888
4164
 
3889
4165
  // src/launch.ts
3890
4166
  import { spawn as spawn2 } from "node:child_process";
3891
- import fs9 from "node:fs";
4167
+ import fs10 from "node:fs";
3892
4168
  import os from "node:os";
3893
- import path10 from "node:path";
4169
+ import path11 from "node:path";
3894
4170
  var SURFACE_FLAG = { author: "--develop", test: "--explore" };
3895
4171
  var SERVER_KEY = { author: "malloyyo_author", test: "malloyyo_test" };
3896
4172
  async function launchCmd(mode, opts) {
3897
- const root = path10.resolve(opts.root ?? process.cwd());
3898
- const tmpDir = fs9.mkdtempSync(path10.join(os.tmpdir(), "malloyyo-launch-"));
3899
- const cfgPath = path10.join(tmpDir, "mcp.json");
4173
+ const root = path11.resolve(opts.root ?? process.cwd());
4174
+ const tmpDir = fs10.mkdtempSync(path11.join(os.tmpdir(), "malloyyo-launch-"));
4175
+ const cfgPath = path11.join(tmpDir, "mcp.json");
3900
4176
  const cfg = {
3901
4177
  mcpServers: {
3902
4178
  // Absolute -C: an ephemeral config, so pinning the root is robust.
3903
4179
  [SERVER_KEY[mode]]: { command: "malloyyo", args: ["mcp", SURFACE_FLAG[mode], "-C", root] }
3904
4180
  }
3905
4181
  };
3906
- fs9.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
4182
+ fs10.writeFileSync(cfgPath, JSON.stringify(cfg, null, 2));
3907
4183
  const label = mode === "author" ? "AUTHOR (compile/edit)" : "TEST (claude.ai web preview)";
3908
4184
  process.stderr.write(`\u25B6 launching Claude in ${label} mode over ${root}
3909
4185
  `);
@@ -3922,11 +4198,11 @@ async function launchCmd(mode, opts) {
3922
4198
  });
3923
4199
  child.on("exit", () => resolve3());
3924
4200
  });
3925
- fs9.rmSync(tmpDir, { recursive: true, force: true });
4201
+ fs10.rmSync(tmpDir, { recursive: true, force: true });
3926
4202
  }
3927
4203
 
3928
4204
  // package.json
3929
- var version = "0.2.27";
4205
+ var version = "0.2.29";
3930
4206
 
3931
4207
  // src/index.ts
3932
4208
  function shortSha(sha) {