@bonnard/cli 0.1.13 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/bin/bon.mjs +305 -620
  2. package/dist/bin/validate-DEh1XQnH.mjs +365 -0
  3. package/dist/docs/_index.md +1 -1
  4. package/dist/docs/topics/cubes.data-source.md +2 -2
  5. package/dist/docs/topics/cubes.dimensions.format.md +2 -2
  6. package/dist/docs/topics/cubes.dimensions.md +2 -2
  7. package/dist/docs/topics/cubes.dimensions.primary-key.md +2 -2
  8. package/dist/docs/topics/cubes.dimensions.sub-query.md +2 -2
  9. package/dist/docs/topics/cubes.dimensions.time.md +2 -2
  10. package/dist/docs/topics/cubes.dimensions.types.md +2 -2
  11. package/dist/docs/topics/cubes.extends.md +2 -2
  12. package/dist/docs/topics/cubes.hierarchies.md +2 -2
  13. package/dist/docs/topics/cubes.joins.md +2 -2
  14. package/dist/docs/topics/cubes.md +2 -2
  15. package/dist/docs/topics/cubes.measures.calculated.md +2 -2
  16. package/dist/docs/topics/cubes.measures.drill-members.md +2 -2
  17. package/dist/docs/topics/cubes.measures.filters.md +2 -2
  18. package/dist/docs/topics/cubes.measures.format.md +21 -2
  19. package/dist/docs/topics/cubes.measures.md +2 -2
  20. package/dist/docs/topics/cubes.measures.rolling.md +2 -2
  21. package/dist/docs/topics/cubes.measures.types.md +2 -2
  22. package/dist/docs/topics/cubes.public.md +2 -2
  23. package/dist/docs/topics/cubes.refresh-key.md +2 -2
  24. package/dist/docs/topics/cubes.segments.md +2 -2
  25. package/dist/docs/topics/cubes.sql.md +2 -2
  26. package/dist/docs/topics/features.catalog.md +31 -0
  27. package/dist/docs/topics/features.cli.md +59 -0
  28. package/dist/docs/topics/features.context-graph.md +18 -0
  29. package/dist/docs/topics/features.governance.md +84 -0
  30. package/dist/docs/topics/features.mcp.md +48 -0
  31. package/dist/docs/topics/features.md +15 -0
  32. package/dist/docs/topics/features.sdk.md +53 -0
  33. package/dist/docs/topics/features.semantic-layer.md +50 -0
  34. package/dist/docs/topics/features.slack-teams.md +18 -0
  35. package/dist/docs/topics/getting-started.md +2 -143
  36. package/dist/docs/topics/pre-aggregations.md +2 -2
  37. package/dist/docs/topics/pre-aggregations.rollup.md +2 -2
  38. package/dist/docs/topics/syntax.context-variables.md +2 -2
  39. package/dist/docs/topics/syntax.md +2 -2
  40. package/dist/docs/topics/syntax.references.md +2 -2
  41. package/dist/docs/topics/views.cubes.md +2 -2
  42. package/dist/docs/topics/views.folders.md +2 -2
  43. package/dist/docs/topics/views.includes.md +2 -2
  44. package/dist/docs/topics/views.md +2 -2
  45. package/dist/docs/topics/workflow.deploy.md +79 -14
  46. package/dist/docs/topics/workflow.mcp.md +19 -13
  47. package/dist/docs/topics/workflow.md +25 -8
  48. package/dist/docs/topics/workflow.query.md +2 -2
  49. package/dist/docs/topics/workflow.validate.md +4 -31
  50. package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +16 -26
  51. package/dist/templates/cursor/rules/bonnard-get-started.mdc +16 -26
  52. package/dist/templates/shared/bonnard.md +31 -6
  53. package/package.json +4 -8
  54. package/dist/bin/validate-DiN3DaTl.mjs +0 -110
  55. /package/dist/bin/{cubes-De1_2_YJ.mjs → cubes-Bf0IPYd7.mjs} +0 -0
package/dist/bin/bon.mjs CHANGED
@@ -1519,7 +1519,6 @@ async function addDemo(options) {
1519
1519
  console.log(pc.dim(" fact_sales, dim_product, dim_store, dim_customer"));
1520
1520
  console.log();
1521
1521
  console.log(pc.dim(`Test connection: bon datasource test ${name}`));
1522
- console.log(pc.dim(`Explore tables: bon preview ${name} "SELECT table_name FROM information_schema.tables WHERE table_schema = 'contoso'"`));
1523
1522
  }
1524
1523
  /**
1525
1524
  * Main datasource add command
@@ -1626,464 +1625,11 @@ async function datasourceListCommand(options = {}) {
1626
1625
  if (showRemote) await listRemoteDatasources();
1627
1626
  }
1628
1627
 
1629
- //#endregion
1630
- //#region src/lib/connection/snowflake.ts
1631
- /**
1632
- * Snowflake connection testing and querying
1633
- */
1634
- const require$4 = createRequire(import.meta.url);
1635
- function loadSnowflake() {
1636
- try {
1637
- const snowflake = require$4("snowflake-sdk");
1638
- snowflake.configure({ logLevel: "ERROR" });
1639
- return snowflake;
1640
- } catch {
1641
- return null;
1642
- }
1643
- }
1644
- async function testSnowflakeConnection(config, credentials) {
1645
- const snowflake = loadSnowflake();
1646
- if (!snowflake) return {
1647
- success: false,
1648
- message: "Snowflake driver not installed",
1649
- error: "Run: pnpm add snowflake-sdk"
1650
- };
1651
- const startTime = Date.now();
1652
- return new Promise((resolve) => {
1653
- const connection = snowflake.createConnection({
1654
- account: config.account,
1655
- username: credentials.username,
1656
- password: credentials.password,
1657
- database: config.database,
1658
- warehouse: config.warehouse,
1659
- schema: config.schema,
1660
- role: config.role
1661
- });
1662
- connection.connect((err) => {
1663
- if (err) {
1664
- resolve({
1665
- success: false,
1666
- message: "Connection failed",
1667
- error: err.message
1668
- });
1669
- return;
1670
- }
1671
- connection.execute({
1672
- sqlText: "SELECT 1",
1673
- complete: (queryErr) => {
1674
- const latencyMs = Date.now() - startTime;
1675
- connection.destroy(() => {});
1676
- if (queryErr) resolve({
1677
- success: false,
1678
- message: "Query failed",
1679
- error: queryErr.message,
1680
- latencyMs
1681
- });
1682
- else resolve({
1683
- success: true,
1684
- message: "Connection successful",
1685
- latencyMs
1686
- });
1687
- }
1688
- });
1689
- });
1690
- });
1691
- }
1692
- async function querySnowflake(config, credentials, sql, options = {}) {
1693
- const snowflake = loadSnowflake();
1694
- if (!snowflake) return {
1695
- columns: [],
1696
- rows: [],
1697
- rowCount: 0,
1698
- truncated: false,
1699
- error: "Snowflake driver not installed. Run: pnpm add snowflake-sdk"
1700
- };
1701
- const limit = options.limit ?? 1e3;
1702
- return new Promise((resolve) => {
1703
- const connection = snowflake.createConnection({
1704
- account: config.account,
1705
- username: credentials.username,
1706
- password: credentials.password,
1707
- database: options.database || config.database,
1708
- warehouse: config.warehouse,
1709
- schema: options.schema || config.schema,
1710
- role: config.role
1711
- });
1712
- connection.connect((err) => {
1713
- if (err) {
1714
- resolve({
1715
- columns: [],
1716
- rows: [],
1717
- rowCount: 0,
1718
- truncated: false,
1719
- error: err.message
1720
- });
1721
- return;
1722
- }
1723
- connection.execute({
1724
- sqlText: sql,
1725
- complete: (queryErr, _stmt, rows) => {
1726
- connection.destroy(() => {});
1727
- if (queryErr) {
1728
- resolve({
1729
- columns: [],
1730
- rows: [],
1731
- rowCount: 0,
1732
- truncated: false,
1733
- error: queryErr.message
1734
- });
1735
- return;
1736
- }
1737
- const allRows = rows || [];
1738
- const truncated = allRows.length > limit;
1739
- const resultRows = truncated ? allRows.slice(0, limit) : allRows;
1740
- resolve({
1741
- columns: resultRows.length > 0 ? Object.keys(resultRows[0]) : [],
1742
- rows: resultRows,
1743
- rowCount: resultRows.length,
1744
- truncated
1745
- });
1746
- }
1747
- });
1748
- });
1749
- });
1750
- }
1751
-
1752
- //#endregion
1753
- //#region src/lib/connection/postgres.ts
1754
- /**
1755
- * Postgres connection testing and querying
1756
- */
1757
- const require$3 = createRequire(import.meta.url);
1758
- function loadPg() {
1759
- try {
1760
- return require$3("pg");
1761
- } catch {
1762
- return null;
1763
- }
1764
- }
1765
- function createClient(config, credentials, pg) {
1766
- return new pg.Client({
1767
- host: config.host,
1768
- port: config.port ? parseInt(config.port, 10) : 5432,
1769
- database: config.database,
1770
- user: credentials.username,
1771
- password: credentials.password,
1772
- ssl: config.sslmode === "require" ? { rejectUnauthorized: false } : void 0
1773
- });
1774
- }
1775
- async function testPostgresConnection(config, credentials) {
1776
- const pg = loadPg();
1777
- if (!pg) return {
1778
- success: false,
1779
- message: "Postgres driver not installed",
1780
- error: "Run: pnpm add pg"
1781
- };
1782
- const startTime = Date.now();
1783
- const client = createClient(config, credentials, pg);
1784
- try {
1785
- await client.connect();
1786
- await client.query("SELECT 1");
1787
- const latencyMs = Date.now() - startTime;
1788
- await client.end();
1789
- return {
1790
- success: true,
1791
- message: "Connection successful",
1792
- latencyMs
1793
- };
1794
- } catch (err) {
1795
- try {
1796
- await client.end();
1797
- } catch {}
1798
- return {
1799
- success: false,
1800
- message: "Connection failed",
1801
- error: err.message
1802
- };
1803
- }
1804
- }
1805
- async function queryPostgres(config, credentials, sql, options = {}) {
1806
- const pg = loadPg();
1807
- if (!pg) return {
1808
- columns: [],
1809
- rows: [],
1810
- rowCount: 0,
1811
- truncated: false,
1812
- error: "Postgres driver not installed. Run: pnpm add pg"
1813
- };
1814
- const limit = options.limit ?? 1e3;
1815
- const client = createClient(config, credentials, pg);
1816
- try {
1817
- await client.connect();
1818
- const schema = options.schema || config.schema;
1819
- if (schema) {
1820
- if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(schema)) throw new Error("Invalid schema name");
1821
- await client.query(`SET search_path TO "${schema}"`);
1822
- }
1823
- const result = await client.query(sql);
1824
- await client.end();
1825
- const columns = result.fields?.map((f) => f.name) || [];
1826
- const allRows = result.rows || [];
1827
- const truncated = allRows.length > limit;
1828
- const rows = truncated ? allRows.slice(0, limit) : allRows;
1829
- return {
1830
- columns,
1831
- rows,
1832
- rowCount: rows.length,
1833
- truncated
1834
- };
1835
- } catch (err) {
1836
- try {
1837
- await client.end();
1838
- } catch {}
1839
- return {
1840
- columns: [],
1841
- rows: [],
1842
- rowCount: 0,
1843
- truncated: false,
1844
- error: err.message
1845
- };
1846
- }
1847
- }
1848
-
1849
- //#endregion
1850
- //#region src/lib/connection/bigquery.ts
1851
- /**
1852
- * BigQuery connection testing
1853
- */
1854
- const require$2 = createRequire(import.meta.url);
1855
- async function testBigQueryConnection(config, credentials) {
1856
- let BigQuery;
1857
- try {
1858
- BigQuery = require$2("@google-cloud/bigquery").BigQuery;
1859
- } catch {
1860
- return {
1861
- success: false,
1862
- message: "BigQuery driver not installed",
1863
- error: "Run: pnpm add @google-cloud/bigquery"
1864
- };
1865
- }
1866
- const startTime = Date.now();
1867
- try {
1868
- const options = { projectId: config.project_id };
1869
- if (config.location) options.location = config.location;
1870
- if (credentials.service_account_json) options.credentials = JSON.parse(credentials.service_account_json);
1871
- else if (credentials.keyfile_path) options.keyFilename = credentials.keyfile_path;
1872
- await new BigQuery(options).query("SELECT 1");
1873
- return {
1874
- success: true,
1875
- message: "Connection successful",
1876
- latencyMs: Date.now() - startTime
1877
- };
1878
- } catch (err) {
1879
- return {
1880
- success: false,
1881
- message: "Connection failed",
1882
- error: err.message
1883
- };
1884
- }
1885
- }
1886
-
1887
- //#endregion
1888
- //#region src/lib/connection/databricks.ts
1889
- /**
1890
- * Databricks connection testing
1891
- */
1892
- const require$1 = createRequire(import.meta.url);
1893
- async function testDatabricksConnection(config, credentials) {
1894
- let DBSQLClient;
1895
- try {
1896
- const module = require$1("@databricks/sql");
1897
- DBSQLClient = module.default || module;
1898
- } catch {
1899
- return {
1900
- success: false,
1901
- message: "Databricks driver not installed",
1902
- error: "Run: pnpm add @databricks/sql"
1903
- };
1904
- }
1905
- const startTime = Date.now();
1906
- const client = new DBSQLClient();
1907
- try {
1908
- const connection = await client.connect({
1909
- host: config.hostname,
1910
- path: config.http_path,
1911
- token: credentials.token
1912
- });
1913
- const session = await connection.openSession({
1914
- initialCatalog: config.catalog,
1915
- initialSchema: config.schema
1916
- });
1917
- const operation = await session.executeStatement("SELECT 1");
1918
- await operation.fetchAll();
1919
- await operation.close();
1920
- const latencyMs = Date.now() - startTime;
1921
- await session.close();
1922
- await connection.close();
1923
- return {
1924
- success: true,
1925
- message: "Connection successful",
1926
- latencyMs
1927
- };
1928
- } catch (err) {
1929
- try {
1930
- await client.close();
1931
- } catch {}
1932
- return {
1933
- success: false,
1934
- message: "Connection failed",
1935
- error: err.message
1936
- };
1937
- }
1938
- }
1939
-
1940
- //#endregion
1941
- //#region src/lib/connection/index.ts
1942
- var connection_exports = /* @__PURE__ */ __exportAll({
1943
- executeQuery: () => executeQuery,
1944
- testConnection: () => testConnection
1945
- });
1946
- /**
1947
- * Test connection to a datasource
1948
- */
1949
- async function testConnection(datasource) {
1950
- const { type, config, credentials } = datasource;
1951
- switch (type) {
1952
- case "snowflake": return testSnowflakeConnection({
1953
- account: config.account,
1954
- database: config.database,
1955
- warehouse: config.warehouse,
1956
- schema: config.schema,
1957
- role: config.role
1958
- }, {
1959
- username: credentials.username,
1960
- password: credentials.password
1961
- });
1962
- case "postgres": return testPostgresConnection({
1963
- host: config.host,
1964
- port: config.port,
1965
- database: config.database,
1966
- schema: config.schema,
1967
- sslmode: config.sslmode
1968
- }, {
1969
- username: credentials.username,
1970
- password: credentials.password
1971
- });
1972
- case "bigquery": return testBigQueryConnection({
1973
- project_id: config.project_id,
1974
- dataset: config.dataset,
1975
- location: config.location
1976
- }, {
1977
- service_account_json: credentials.service_account_json,
1978
- keyfile_path: credentials.keyfile_path
1979
- });
1980
- case "databricks": return testDatabricksConnection({
1981
- hostname: config.hostname,
1982
- http_path: config.http_path,
1983
- catalog: config.catalog,
1984
- schema: config.schema
1985
- }, { token: credentials.token });
1986
- default: return {
1987
- success: false,
1988
- message: `Unsupported warehouse type: ${type}`
1989
- };
1990
- }
1991
- }
1992
- /**
1993
- * Execute a query against a datasource
1994
- */
1995
- async function executeQuery(datasource, sql, options = {}) {
1996
- const { type, config, credentials } = datasource;
1997
- switch (type) {
1998
- case "snowflake": return querySnowflake({
1999
- account: config.account,
2000
- database: config.database,
2001
- warehouse: config.warehouse,
2002
- schema: config.schema,
2003
- role: config.role
2004
- }, {
2005
- username: credentials.username,
2006
- password: credentials.password
2007
- }, sql, options);
2008
- case "postgres": return queryPostgres({
2009
- host: config.host,
2010
- port: config.port,
2011
- database: config.database,
2012
- schema: config.schema,
2013
- sslmode: config.sslmode
2014
- }, {
2015
- username: credentials.username,
2016
- password: credentials.password
2017
- }, sql, options);
2018
- case "bigquery": return {
2019
- columns: [],
2020
- rows: [],
2021
- rowCount: 0,
2022
- truncated: false,
2023
- error: "BigQuery local querying not yet implemented"
2024
- };
2025
- case "databricks": return {
2026
- columns: [],
2027
- rows: [],
2028
- rowCount: 0,
2029
- truncated: false,
2030
- error: "Databricks local querying not yet implemented"
2031
- };
2032
- default: return {
2033
- columns: [],
2034
- rows: [],
2035
- rowCount: 0,
2036
- truncated: false,
2037
- error: `Unsupported warehouse type: ${type}`
2038
- };
2039
- }
2040
- }
2041
-
2042
1628
  //#endregion
2043
1629
  //#region src/commands/datasource/test.ts
2044
- async function datasourceTestCommand(name, options = {}) {
2045
- const localDs = getLocalDatasource(name);
2046
- if (options.remote || !localDs) {
2047
- await testRemote(name, !localDs);
2048
- return;
2049
- }
2050
- await testLocal(name, localDs);
2051
- }
2052
- /**
2053
- * Test datasource locally using direct connection
2054
- */
2055
- async function testLocal(name, ds) {
2056
- console.log(pc.dim(`Testing ${name} locally...`));
2057
- console.log();
2058
- const { resolved, missing } = resolveEnvVarsInCredentials(ds.credentials);
2059
- if (missing.length > 0) {
2060
- console.log(pc.red(`Missing environment variables: ${missing.join(", ")}`));
2061
- console.log(pc.dim("Set these env vars or update .bon/datasources.yaml with actual values."));
2062
- process.exit(1);
2063
- }
2064
- const result = await testConnection({
2065
- type: ds.type,
2066
- config: ds.config,
2067
- credentials: resolved
2068
- });
2069
- if (result.success) {
2070
- console.log(pc.green(`✓ ${result.message}`));
2071
- if (result.latencyMs) console.log(pc.dim(` Latency: ${result.latencyMs}ms`));
2072
- } else {
2073
- console.log(pc.red(`✗ ${result.message}`));
2074
- if (result.error) console.log(pc.dim(` ${result.error}`));
2075
- process.exit(1);
2076
- }
2077
- }
2078
- /**
2079
- * Test datasource via remote API (requires login)
2080
- */
2081
- async function testRemote(name, notFoundLocally) {
1630
+ async function datasourceTestCommand(name) {
2082
1631
  if (!loadCredentials()) {
2083
- if (notFoundLocally) {
2084
- console.log(pc.red(`Datasource "${name}" not found locally.`));
2085
- console.log(pc.dim("Run `bon datasource add` to create it, or `bon login` to test remote datasources."));
2086
- } else console.log(pc.red("Not logged in. Run `bon login` to test remote datasources."));
1632
+ console.log(pc.red("Not logged in. Run `bon login` to test datasources."));
2087
1633
  process.exit(1);
2088
1634
  }
2089
1635
  console.log(pc.dim(`Testing ${name} via remote API...`));
@@ -2228,58 +1774,16 @@ async function pushDatasource(name, options = {}) {
2228
1774
  }
2229
1775
  }
2230
1776
 
2231
- //#endregion
2232
- //#region src/commands/preview.ts
2233
- async function previewCommand(datasourceName, sql, options) {
2234
- const limit = options.limit ? parseInt(options.limit, 10) : 1e3;
2235
- const format = options.format ?? "toon";
2236
- const ds = getLocalDatasource(datasourceName);
2237
- if (!ds) {
2238
- console.error(pc.red(`Datasource "${datasourceName}" not found in .bon/datasources.yaml`));
2239
- console.log(pc.dim("Run `bon datasource add` to create it."));
2240
- process.exit(1);
2241
- }
2242
- const { resolved, missing } = resolveEnvVarsInCredentials(ds.credentials);
2243
- if (missing.length > 0) {
2244
- console.error(pc.red(`Missing environment variables: ${missing.join(", ")}`));
2245
- console.log(pc.dim("Set these env vars or update .bon/datasources.yaml with actual values."));
2246
- process.exit(1);
2247
- }
2248
- const result = await executeQuery({
2249
- type: ds.type,
2250
- config: ds.config,
2251
- credentials: resolved
2252
- }, sql, {
2253
- limit,
2254
- schema: options.schema,
2255
- database: options.database
2256
- });
2257
- if (result.error) {
2258
- console.error(pc.red(result.error));
2259
- process.exit(1);
2260
- }
2261
- if (result.rowCount === 0) {
2262
- console.log("No rows returned.");
2263
- return;
2264
- }
2265
- if (format === "json") console.log(JSON.stringify(result, null, 2));
2266
- else {
2267
- const toon = encode({ results: result.rows });
2268
- console.log(toon);
2269
- }
2270
- if (result.truncated) console.log(pc.dim(`(truncated to ${result.rowCount} rows)`));
2271
- }
2272
-
2273
1777
  //#endregion
2274
1778
  //#region src/commands/validate.ts
2275
- async function validateCommand(options = {}) {
1779
+ async function validateCommand() {
2276
1780
  const cwd = process.cwd();
2277
1781
  const paths = getProjectPaths(cwd);
2278
1782
  if (!fs.existsSync(paths.config)) {
2279
1783
  console.log(pc.red("No bon.yaml found. Are you in a Bonnard project?"));
2280
1784
  process.exit(1);
2281
1785
  }
2282
- const { validate } = await import("./validate-DiN3DaTl.mjs");
1786
+ const { validate } = await import("./validate-DEh1XQnH.mjs");
2283
1787
  const result = await validate(cwd);
2284
1788
  if (result.cubes.length === 0 && result.views.length === 0 && result.valid) {
2285
1789
  console.log(pc.yellow(`No cube or view files found in ${BONNARD_DIR}/cubes/ or ${BONNARD_DIR}/views/.`));
@@ -2307,61 +1811,12 @@ async function validateCommand(options = {}) {
2307
1811
  }
2308
1812
  for (const [parent, items] of byParent) console.log(pc.dim(` ${parent}: ${items.join(", ")}`));
2309
1813
  }
2310
- if (options.testConnection) {
2311
- console.log();
2312
- await testReferencedConnections(cwd);
2313
- }
2314
- }
2315
- /**
2316
- * Test connections for datasources referenced by cubes and views
2317
- * Lenient: warns but doesn't fail validation
2318
- */
2319
- async function testReferencedConnections(cwd) {
2320
- const { extractDatasourcesFromCubes } = await import("./cubes-De1_2_YJ.mjs");
2321
- const { loadLocalDatasources, resolveEnvVarsInCredentials } = await Promise.resolve().then(() => local_exports);
2322
- const { testConnection } = await Promise.resolve().then(() => connection_exports);
2323
- const references = extractDatasourcesFromCubes(cwd);
2324
- if (references.length === 0) {
2325
- console.log(pc.dim("No datasource references found in cubes."));
2326
- return;
2327
- }
2328
- console.log(pc.bold("Testing connections..."));
2329
- console.log();
2330
- const localDatasources = loadLocalDatasources(cwd);
2331
- let warnings = 0;
2332
- for (const ref of references) {
2333
- const ds = localDatasources.find((d) => d.name === ref.name);
2334
- if (!ds) {
2335
- console.log(pc.yellow(`⚠ ${ref.name}: not found in .bon/datasources.yaml`));
2336
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2337
- warnings++;
2338
- continue;
2339
- }
2340
- const { resolved, missing } = resolveEnvVarsInCredentials(ds.credentials);
2341
- if (missing.length > 0) {
2342
- console.log(pc.yellow(`⚠ ${ref.name}: missing env vars: ${missing.join(", ")}`));
2343
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2344
- warnings++;
2345
- continue;
2346
- }
2347
- const result = await testConnection({
2348
- type: ds.type,
2349
- config: ds.config,
2350
- credentials: resolved
2351
- });
2352
- if (result.success) {
2353
- const latency = result.latencyMs ? pc.dim(` (${result.latencyMs}ms)`) : "";
2354
- console.log(pc.green(`✓ ${ref.name}${latency}`));
2355
- } else {
2356
- console.log(pc.yellow(`⚠ ${ref.name}: ${result.error || result.message}`));
2357
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2358
- warnings++;
2359
- }
2360
- }
2361
- if (warnings > 0) {
1814
+ if (result.cubesMissingDataSource.length > 0) {
2362
1815
  console.log();
2363
- console.log(pc.yellow(`${warnings} connection warning(s)`));
2364
- console.log(pc.dim("Connection issues won't block file validation, but will fail at deploy."));
1816
+ console.log(pc.yellow(`⚠ ${result.cubesMissingDataSource.length} cube(s) missing data_source`));
1817
+ console.log(pc.dim(" Without an explicit data_source, cubes use the default warehouse."));
1818
+ console.log(pc.dim(" This can cause issues when multiple warehouses are configured."));
1819
+ console.log(pc.dim(` ${result.cubesMissingDataSource.join(", ")}`));
2365
1820
  }
2366
1821
  }
2367
1822
 
@@ -2383,6 +1838,9 @@ function collectFiles(dir, rootDir) {
2383
1838
  walk(dir);
2384
1839
  return files;
2385
1840
  }
1841
+ function capitalize$1(s) {
1842
+ return s.charAt(0).toUpperCase() + s.slice(1);
1843
+ }
2386
1844
  async function deployCommand(options = {}) {
2387
1845
  const cwd = process.cwd();
2388
1846
  const paths = getProjectPaths(cwd);
@@ -2391,7 +1849,7 @@ async function deployCommand(options = {}) {
2391
1849
  process.exit(1);
2392
1850
  }
2393
1851
  console.log(pc.dim("Validating cubes and views..."));
2394
- const { validate } = await import("./validate-DiN3DaTl.mjs");
1852
+ const { validate } = await import("./validate-DEh1XQnH.mjs");
2395
1853
  const result = await validate(cwd);
2396
1854
  if (!result.valid) {
2397
1855
  console.log(pc.red("Validation failed:\n"));
@@ -2412,10 +1870,40 @@ async function deployCommand(options = {}) {
2412
1870
  console.log(pc.dim(`Deploying ${fileCount} file(s)...`));
2413
1871
  console.log();
2414
1872
  try {
2415
- const response = await post("/api/deploy", { files });
1873
+ const response = await post("/api/deploy", {
1874
+ files,
1875
+ ...options.message && { message: options.message }
1876
+ });
2416
1877
  console.log(pc.green("Deploy successful!"));
2417
1878
  console.log(`Deployment ID: ${pc.cyan(response.deployment.id)}`);
2418
- console.log(`API: ${pc.cyan(response.deployment.cubeApiUrl)}`);
1879
+ console.log();
1880
+ if (response.deployment.isFirstDeploy) console.log(pc.dim(` First deployment — ${response.deployment.fileCount} files uploaded`));
1881
+ else if (response.deployment.changes && response.deployment.changes.details.length > 0) {
1882
+ const { changes } = response.deployment;
1883
+ console.log(pc.dim("Changes from previous deploy:"));
1884
+ console.log();
1885
+ for (const c of changes.details) {
1886
+ const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
1887
+ const label = `${capitalize$1(c.changeType)} ${c.objectType}: ${c.objectName}`;
1888
+ const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
1889
+ const summaryTag = c.summary ? pc.dim(` — ${c.summary}`) : "";
1890
+ console.log(` ${prefix} ${label}${summaryTag}${breakingTag}`);
1891
+ }
1892
+ if (changes.breaking > 0) {
1893
+ console.log();
1894
+ console.log(pc.red(`${changes.breaking} breaking change${changes.breaking > 1 ? "s" : ""} detected`));
1895
+ }
1896
+ console.log();
1897
+ console.log(pc.bold("Annotate these changes with reasoning:"));
1898
+ console.log();
1899
+ const annotationTemplate = { annotations: changes.details.map((c) => ({
1900
+ objectName: c.objectName,
1901
+ annotation: `Why ${c.objectName} was ${c.changeType}`
1902
+ })) };
1903
+ console.log(` bon annotate ${response.deployment.id} --data '${JSON.stringify(annotationTemplate)}'`);
1904
+ console.log();
1905
+ console.log(pc.dim("Replace each annotation value with the reasoning behind the change."));
1906
+ } else console.log(pc.dim(" No changes detected from previous deployment."));
2419
1907
  console.log();
2420
1908
  console.log(pc.bold("Connect AI agents via MCP:"));
2421
1909
  console.log(` MCP URL: ${pc.cyan("https://mcp.bonnard.dev/mcp")}`);
@@ -2430,51 +1918,29 @@ async function deployCommand(options = {}) {
2430
1918
  * Returns true if any connection failed (strict mode)
2431
1919
  */
2432
1920
  async function testAndSyncDatasources(cwd, options = {}) {
2433
- const { extractDatasourcesFromCubes } = await import("./cubes-De1_2_YJ.mjs");
2434
- const { loadLocalDatasources, resolveEnvVarsInCredentials } = await Promise.resolve().then(() => local_exports);
2435
- const { testConnection } = await Promise.resolve().then(() => connection_exports);
1921
+ const { extractDatasourcesFromCubes } = await import("./cubes-Bf0IPYd7.mjs");
1922
+ const { loadLocalDatasources } = await Promise.resolve().then(() => local_exports);
2436
1923
  const { pushDatasource } = await Promise.resolve().then(() => push_exports);
2437
1924
  const references = extractDatasourcesFromCubes(cwd);
2438
1925
  if (references.length === 0) return false;
2439
1926
  console.log();
2440
- console.log(pc.dim("Testing datasource connections..."));
1927
+ console.log(pc.dim("Checking datasources..."));
2441
1928
  const localDatasources = loadLocalDatasources(cwd);
2442
1929
  let failed = false;
2443
- const validatedDatasources = [];
1930
+ const foundDatasources = [];
2444
1931
  for (const ref of references) {
2445
- const ds = localDatasources.find((d) => d.name === ref.name);
2446
- if (!ds) {
1932
+ if (!localDatasources.find((d) => d.name === ref.name)) {
2447
1933
  console.log(pc.red(`✗ ${ref.name}: not found in .bon/datasources.yaml`));
2448
1934
  console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2449
1935
  console.log(pc.dim(` Run: bon datasource add --from-dbt`));
2450
1936
  failed = true;
2451
1937
  continue;
2452
1938
  }
2453
- const { resolved, missing } = resolveEnvVarsInCredentials(ds.credentials);
2454
- if (missing.length > 0) {
2455
- console.log(pc.red(`✗ ${ref.name}: missing env vars: ${missing.join(", ")}`));
2456
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2457
- failed = true;
2458
- continue;
2459
- }
2460
- const result = await testConnection({
2461
- type: ds.type,
2462
- config: ds.config,
2463
- credentials: resolved
2464
- });
2465
- if (result.success) {
2466
- const latency = result.latencyMs ? pc.dim(` (${result.latencyMs}ms)`) : "";
2467
- console.log(pc.green(`✓ ${ref.name}${latency}`));
2468
- validatedDatasources.push(ref.name);
2469
- } else {
2470
- console.log(pc.red(`✗ ${ref.name}: ${result.error || result.message}`));
2471
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2472
- failed = true;
2473
- }
1939
+ foundDatasources.push(ref.name);
2474
1940
  }
2475
- console.log();
2476
1941
  if (failed) {
2477
- console.log(pc.red("Connection tests failed. Fix datasource issues before deploying."));
1942
+ console.log();
1943
+ console.log(pc.red("Missing datasources. Fix issues before deploying."));
2478
1944
  return true;
2479
1945
  }
2480
1946
  console.log(pc.dim("Checking remote datasources..."));
@@ -2486,22 +1952,17 @@ async function testAndSyncDatasources(cwd, options = {}) {
2486
1952
  return true;
2487
1953
  }
2488
1954
  const remoteNames = new Set(remoteDatasources.map((ds) => ds.name));
2489
- const missingRemote = validatedDatasources.filter((name) => !remoteNames.has(name));
2490
- if (missingRemote.length === 0) {
2491
- console.log(pc.green("✓ All datasources exist on remote"));
1955
+ const missingRemote = foundDatasources.filter((name) => !remoteNames.has(name));
1956
+ if (missingRemote.length > 0) {
2492
1957
  console.log();
2493
- return false;
2494
- }
2495
- console.log();
2496
- console.log(pc.yellow(`⚠ Missing remote datasource${missingRemote.length > 1 ? "s" : ""}: ${missingRemote.join(", ")}`));
2497
- console.log();
2498
- if (options.ci) {
2499
- console.log(pc.red("Deploy aborted (--ci mode)."));
2500
- console.log(pc.dim(`Run: bon datasource push <name>`));
2501
- return true;
2502
- }
2503
- if (options.pushDatasources) {
2504
- for (const name of missingRemote) {
1958
+ console.log(pc.yellow(`⚠ Missing remote datasource${missingRemote.length > 1 ? "s" : ""}: ${missingRemote.join(", ")}`));
1959
+ console.log();
1960
+ if (options.ci) {
1961
+ console.log(pc.red("Deploy aborted (--ci mode)."));
1962
+ console.log(pc.dim(`Run: bon datasource push <name>`));
1963
+ return true;
1964
+ }
1965
+ if (options.pushDatasources) for (const name of missingRemote) {
2505
1966
  console.log(pc.dim(`Pushing "${name}"...`));
2506
1967
  if (await pushDatasource(name, { silent: true })) console.log(pc.green(`✓ Pushed "${name}"`));
2507
1968
  else {
@@ -2509,27 +1970,249 @@ async function testAndSyncDatasources(cwd, options = {}) {
2509
1970
  return true;
2510
1971
  }
2511
1972
  }
2512
- console.log();
2513
- return false;
1973
+ else {
1974
+ if (!await confirm({
1975
+ message: `Push ${missingRemote.length > 1 ? "these datasources" : `"${missingRemote[0]}"`} to Bonnard? (credentials will be encrypted)`,
1976
+ default: true
1977
+ })) {
1978
+ console.log(pc.dim("Deploy aborted."));
1979
+ return true;
1980
+ }
1981
+ console.log();
1982
+ for (const name of missingRemote) {
1983
+ console.log(pc.dim(`Pushing "${name}"...`));
1984
+ if (await pushDatasource(name, { silent: true })) console.log(pc.green(`✓ Pushed "${name}"`));
1985
+ else {
1986
+ console.log(pc.red(`✗ Failed to push "${name}"`));
1987
+ return true;
1988
+ }
1989
+ }
1990
+ }
1991
+ }
1992
+ console.log();
1993
+ console.log(pc.dim("Testing datasource connections..."));
1994
+ for (const name of foundDatasources) try {
1995
+ const result = await post("/api/datasources/test", { name });
1996
+ if (result.success) {
1997
+ const latency = result.details?.latencyMs ? pc.dim(` (${result.details.latencyMs}ms)`) : "";
1998
+ console.log(pc.green(`✓ ${name}${latency}`));
1999
+ } else {
2000
+ console.log(pc.red(`✗ ${name}: ${result.message}`));
2001
+ failed = true;
2002
+ }
2003
+ } catch (err) {
2004
+ console.log(pc.red(`✗ ${name}: ${err.message}`));
2005
+ failed = true;
2514
2006
  }
2515
- if (!await confirm({
2516
- message: `Push ${missingRemote.length > 1 ? "these datasources" : `"${missingRemote[0]}"`} to Bonnard? (credentials will be encrypted)`,
2517
- default: true
2518
- })) {
2519
- console.log(pc.dim("Deploy aborted."));
2007
+ console.log();
2008
+ if (failed) {
2009
+ console.log(pc.red("Connection tests failed. Fix datasource issues before deploying."));
2520
2010
  return true;
2521
2011
  }
2012
+ console.log(pc.green("✓ All datasources connected"));
2522
2013
  console.log();
2523
- for (const name of missingRemote) {
2524
- console.log(pc.dim(`Pushing "${name}"...`));
2525
- if (await pushDatasource(name, { silent: true })) console.log(pc.green(`✓ Pushed "${name}"`));
2526
- else {
2527
- console.log(pc.red(`✗ Failed to push "${name}"`));
2528
- return true;
2014
+ return false;
2015
+ }
2016
+
2017
+ //#endregion
2018
+ //#region src/commands/deployments.ts
2019
+ function relativeTime(dateStr) {
2020
+ const now = Date.now();
2021
+ const then = new Date(dateStr).getTime();
2022
+ const seconds = Math.floor((now - then) / 1e3);
2023
+ if (seconds < 60) return "just now";
2024
+ const minutes = Math.floor(seconds / 60);
2025
+ if (minutes < 60) return `${minutes} min ago`;
2026
+ const hours = Math.floor(minutes / 60);
2027
+ if (hours < 24) return `${hours} hour${hours > 1 ? "s" : ""} ago`;
2028
+ const days = Math.floor(hours / 24);
2029
+ return `${days} day${days > 1 ? "s" : ""} ago`;
2030
+ }
2031
+ function truncate(str, max) {
2032
+ if (str.length <= max) return str;
2033
+ return str.slice(0, max - 1) + "…";
2034
+ }
2035
+ function statusColor(status) {
2036
+ switch (status) {
2037
+ case "success": return pc.green(status);
2038
+ case "failed": return pc.red(status);
2039
+ case "processing": return pc.yellow(status);
2040
+ default: return pc.dim(status);
2041
+ }
2042
+ }
2043
+ async function deploymentsCommand(options = {}) {
2044
+ const limit = options.all ? 100 : 10;
2045
+ let response;
2046
+ try {
2047
+ response = await get(`/api/deploy/history?limit=${limit}`);
2048
+ } catch (err) {
2049
+ console.log(pc.red(`Failed to fetch deployments: ${err instanceof Error ? err.message : err}`));
2050
+ process.exit(1);
2051
+ }
2052
+ const { deployments } = response;
2053
+ if (options.format === "json") {
2054
+ console.log(JSON.stringify(deployments, null, 2));
2055
+ return;
2056
+ }
2057
+ console.log();
2058
+ console.log(pc.bold("Deployments for Bonnard"));
2059
+ console.log();
2060
+ if (deployments.length === 0) {
2061
+ console.log(pc.dim(" No deployments found."));
2062
+ console.log();
2063
+ return;
2064
+ }
2065
+ const colId = 10;
2066
+ const colStatus = 12;
2067
+ const colFiles = 7;
2068
+ const colMessage = 32;
2069
+ console.log(pc.dim(" " + "ID".padEnd(colId) + "Status".padEnd(colStatus) + "Files".padEnd(colFiles) + "Message".padEnd(colMessage) + "Deployed"));
2070
+ for (const d of deployments) {
2071
+ const id = d.id.slice(0, 8);
2072
+ const status = statusColor(d.status);
2073
+ const statusPad = " ".repeat(Math.max(0, colStatus - d.status.length));
2074
+ const files = String(d.fileCount);
2075
+ const message = d.message ? truncate(d.message, 30) : "—";
2076
+ const time = relativeTime(d.createdAt);
2077
+ console.log(" " + id.padEnd(colId) + status + statusPad + files.padEnd(colFiles) + message.padEnd(colMessage) + pc.dim(time));
2078
+ }
2079
+ console.log();
2080
+ console.log(pc.dim(`Showing ${deployments.length} deployment${deployments.length !== 1 ? "s" : ""}.`));
2081
+ console.log();
2082
+ }
2083
+
2084
+ //#endregion
2085
+ //#region src/commands/annotate.ts
2086
+ function readStdin() {
2087
+ return new Promise((resolve) => {
2088
+ if (process.stdin.isTTY) {
2089
+ resolve(null);
2090
+ return;
2529
2091
  }
2092
+ let data = "";
2093
+ process.stdin.setEncoding("utf8");
2094
+ process.stdin.on("data", (chunk) => data += chunk);
2095
+ process.stdin.on("end", () => resolve(data.trim() || null));
2096
+ setTimeout(() => resolve(data.trim() || null), 100);
2097
+ });
2098
+ }
2099
+ function parseAnnotations(raw) {
2100
+ let parsed;
2101
+ try {
2102
+ parsed = JSON.parse(raw);
2103
+ } catch {
2104
+ throw new Error("Invalid JSON. Expected: {\"annotations\": [{\"objectName\": \"...\", \"annotation\": \"...\"}]}");
2105
+ }
2106
+ const obj = parsed;
2107
+ if (!obj.annotations || !Array.isArray(obj.annotations) || obj.annotations.length === 0) throw new Error("JSON must contain a non-empty \"annotations\" array");
2108
+ for (const entry of obj.annotations) {
2109
+ const e = entry;
2110
+ if (!e.objectName || typeof e.objectName !== "string") throw new Error("Each annotation must have a string \"objectName\"");
2111
+ if (!e.annotation || typeof e.annotation !== "string") throw new Error(`Missing \"annotation\" text for \"${e.objectName}\"`);
2112
+ if (e.annotation.length > 1e3) throw new Error(`Annotation for \"${e.objectName}\" exceeds 1000 chars`);
2113
+ }
2114
+ return { annotations: obj.annotations };
2115
+ }
2116
+ async function annotateCommand(id, options = {}) {
2117
+ const raw = options.data || await readStdin();
2118
+ if (!raw) {
2119
+ try {
2120
+ const { changes } = await get(`/api/deploy/changes/${id}`);
2121
+ if (changes.length === 0) {
2122
+ console.log(pc.dim("No changes recorded for this deployment."));
2123
+ return;
2124
+ }
2125
+ console.log();
2126
+ console.log(pc.bold(`Changes in deployment ${id.slice(0, 8)}`));
2127
+ console.log();
2128
+ for (const c of changes) {
2129
+ const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
2130
+ const label = `${c.changeType} ${c.objectType}: ${c.objectName}`;
2131
+ const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
2132
+ console.log(` ${prefix} ${label}${breakingTag}`);
2133
+ if (c.annotation) console.log(pc.dim(` "${c.annotation}"`));
2134
+ }
2135
+ console.log();
2136
+ console.log(pc.dim("To annotate, provide JSON via --data or stdin:"));
2137
+ console.log(pc.dim(` bon annotate ${id.slice(0, 8)} --data '{"annotations":[{"objectName":"...","annotation":"..."}]}'`));
2138
+ console.log();
2139
+ } catch (err) {
2140
+ console.log(pc.red(`Failed to fetch changes: ${err instanceof Error ? err.message : err}`));
2141
+ process.exit(1);
2142
+ }
2143
+ return;
2144
+ }
2145
+ let payload;
2146
+ try {
2147
+ payload = parseAnnotations(raw);
2148
+ } catch (err) {
2149
+ console.log(pc.red(err instanceof Error ? err.message : String(err)));
2150
+ process.exit(1);
2151
+ }
2152
+ try {
2153
+ const response = await post(`/api/deploy/annotate/${id}`, payload);
2154
+ console.log(pc.green(`Annotated ${response.updated}/${response.total} changes.`));
2155
+ } catch (err) {
2156
+ console.log(pc.red(`Failed to submit annotations: ${err instanceof Error ? err.message : err}`));
2157
+ process.exit(1);
2158
+ }
2159
+ }
2160
+
2161
+ //#endregion
2162
+ //#region src/commands/diff.ts
2163
+ async function diffCommand(id, options = {}) {
2164
+ let response;
2165
+ try {
2166
+ response = await get(`/api/deploy/changes/${id}`);
2167
+ } catch (err) {
2168
+ console.log(pc.red(`Failed to fetch changes: ${err instanceof Error ? err.message : err}`));
2169
+ process.exit(1);
2170
+ }
2171
+ let { changes } = response;
2172
+ if (options.breaking) changes = changes.filter((c) => c.breaking);
2173
+ if (options.format === "json") {
2174
+ console.log(JSON.stringify(changes, null, 2));
2175
+ return;
2530
2176
  }
2531
2177
  console.log();
2532
- return false;
2178
+ console.log(pc.bold(`Changes in deployment ${id.slice(0, 8)}`));
2179
+ console.log();
2180
+ if (changes.length === 0) {
2181
+ console.log(pc.dim(" No changes recorded."));
2182
+ console.log();
2183
+ return;
2184
+ }
2185
+ for (const c of changes) {
2186
+ const prefix = c.changeType === "added" ? pc.green("+") : c.changeType === "removed" ? pc.red("-") : pc.yellow("~");
2187
+ const label = `${capitalize(c.changeType)} ${c.objectType}: ${c.objectName}`;
2188
+ const breakingTag = c.breaking ? pc.red(" BREAKING") : "";
2189
+ console.log(` ${prefix} ${label}${breakingTag}`);
2190
+ const details = [];
2191
+ if (c.summary) details.push(c.summary);
2192
+ if (details.length > 0) console.log(pc.dim(` ${details.join(" | ")}`));
2193
+ if (c.changeType === "modified" && c.previousDefinition && c.newDefinition) for (const key of Object.keys(c.newDefinition)) {
2194
+ const oldVal = c.previousDefinition[key];
2195
+ const newVal = c.newDefinition[key];
2196
+ if (oldVal !== newVal && oldVal !== void 0 && newVal !== void 0) console.log(pc.dim(` ${key}: ${JSON.stringify(oldVal)} -> ${JSON.stringify(newVal)}`));
2197
+ }
2198
+ if (c.annotation) console.log(` ${pc.cyan("\"" + c.annotation + "\"")}`);
2199
+ console.log();
2200
+ }
2201
+ const added = changes.filter((c) => c.changeType === "added").length;
2202
+ const modified = changes.filter((c) => c.changeType === "modified").length;
2203
+ const removed = changes.filter((c) => c.changeType === "removed").length;
2204
+ const breaking = changes.filter((c) => c.breaking).length;
2205
+ const parts = [
2206
+ `${added} added`,
2207
+ `${modified} modified`,
2208
+ `${removed} removed`
2209
+ ];
2210
+ if (breaking > 0) parts.push(pc.red(`${breaking} breaking`));
2211
+ console.log(pc.dim(`Summary: ${parts.join(", ")}`));
2212
+ console.log();
2213
+ }
2214
+ function capitalize(s) {
2215
+ return s.charAt(0).toUpperCase() + s.slice(1);
2533
2216
  }
2534
2217
 
2535
2218
  //#endregion
@@ -2852,12 +2535,14 @@ program.command("whoami").description("Show current login status").option("--ver
2852
2535
  const datasource = program.command("datasource").description("Manage warehouse data source connections");
2853
2536
  datasource.command("add").description("Add a data source to .bon/datasources.yaml. Use --name and --type together for non-interactive mode").option("--demo", "Add a read-only demo datasource (Contoso retail dataset) for testing").option("--from-dbt [profile]", "Import from dbt profiles.yml (optionally specify profile/target)").option("--target <target>", "Target name when using --from-dbt").option("--all", "Import all connections from dbt profiles").option("--default-targets", "Import only default targets from dbt profiles (non-interactive)").option("--name <name>", "Datasource name (required for non-interactive mode)").option("--type <type>", "Warehouse type: snowflake, postgres, bigquery, databricks (required for non-interactive mode)").option("--account <account>", "Snowflake account identifier").option("--database <database>", "Database name").option("--schema <schema>", "Schema name").option("--warehouse <warehouse>", "Warehouse name (Snowflake)").option("--role <role>", "Role (Snowflake)").option("--host <host>", "Host (Postgres)").option("--port <port>", "Port (Postgres, default: 5432)").option("--project-id <projectId>", "GCP Project ID (BigQuery)").option("--dataset <dataset>", "Dataset name (BigQuery)").option("--location <location>", "Location (BigQuery)").option("--hostname <hostname>", "Server hostname (Databricks)").option("--http-path <httpPath>", "HTTP path (Databricks)").option("--catalog <catalog>", "Catalog name (Databricks)").option("--user <user>", "Username").option("--password <password>", "Password (use --password-env for env var reference)").option("--token <token>", "Access token (use --token-env for env var reference)").option("--service-account-json <json>", "Service account JSON (BigQuery)").option("--keyfile <path>", "Path to service account key file (BigQuery)").option("--password-env <varName>", "Env var name for password, stores as {{ env_var('NAME') }}").option("--token-env <varName>", "Env var name for token, stores as {{ env_var('NAME') }}").option("--force", "Overwrite existing datasource without prompting").action(datasourceAddCommand);
2854
2537
  datasource.command("list").description("List data sources (shows both local and remote by default)").option("--local", "Show only local data sources from .bon/datasources.yaml").option("--remote", "Show only remote data sources from Bonnard server (requires login)").action(datasourceListCommand);
2855
- datasource.command("test").description("Test data source connectivity by connecting directly to the warehouse").argument("<name>", "Data source name from .bon/datasources.yaml").option("--remote", "Test via Bonnard API instead of direct connection (requires login)").action(datasourceTestCommand);
2538
+ datasource.command("test").description("Test data source connectivity via Bonnard API (requires login)").argument("<name>", "Data source name from .bon/datasources.yaml").action(datasourceTestCommand);
2856
2539
  datasource.command("remove").description("Remove a data source from .bon/datasources.yaml (local by default)").argument("<name>", "Data source name").option("--remote", "Remove from Bonnard server instead of local (requires login)").action(datasourceRemoveCommand);
2857
2540
  datasource.command("push").description("Push a local data source to Bonnard server (requires login)").argument("<name>", "Data source name from .bon/datasources.yaml").option("--force", "Overwrite if already exists on remote").action(datasourcePushCommand);
2858
- program.command("preview").description("Preview data from a local warehouse using raw SQL (for development/exploration)").argument("<datasource>", "Data source name from .bon/datasources.yaml").argument("<sql>", "SQL query to execute").option("--schema <schema>", "Override schema").option("--database <database>", "Override database").option("--limit <limit>", "Max rows to return", "1000").option("--format <format>", "Output format: toon or json", "toon").action(previewCommand);
2859
- program.command("validate").description("Validate YAML syntax in bonnard/cubes/ and bonnard/views/").option("--test-connection", "Also test datasource connections (warns on failure, doesn't block)").action(validateCommand);
2860
- program.command("deploy").description("Deploy cubes and views to Bonnard. Requires login, validates, tests connections (fails on error)").option("--ci", "Non-interactive mode (fail if missing datasources)").option("--push-datasources", "Auto-push missing datasources without prompting").action(deployCommand);
2541
+ program.command("validate").description("Validate YAML syntax in bonnard/cubes/ and bonnard/views/").action(validateCommand);
2542
+ program.command("deploy").description("Deploy cubes and views to Bonnard. Requires login, validates, syncs datasources").option("--ci", "Non-interactive mode (fail if missing datasources)").option("--push-datasources", "Auto-push missing datasources without prompting").requiredOption("-m, --message <text>", "Deploy message describing your changes").action(deployCommand);
2543
+ program.command("deployments").description("List deployment history").option("--all", "Show all deployments (default: last 10)").option("--format <format>", "Output format: table or json", "table").action(deploymentsCommand);
2544
+ program.command("diff").description("Show changes in a deployment").argument("<id>", "Deployment ID").option("--format <format>", "Output format: table or json", "table").option("--breaking", "Show only breaking changes").action(diffCommand);
2545
+ program.command("annotate").description("Annotate deployment changes with reasoning").argument("<id>", "Deployment ID").option("--data <json>", "Annotations JSON").action(annotateCommand);
2861
2546
  program.command("mcp").description("MCP connection info and setup instructions").action(mcpCommand).command("test").description("Test MCP server connectivity").action(mcpTestCommand);
2862
2547
  program.command("query").description("Execute a query against the deployed semantic layer").argument("<query>", "JSON query or SQL (with --sql flag)").option("--sql", "Use SQL API instead of JSON format").option("--limit <limit>", "Max rows to return").option("--format <format>", "Output format: toon or json", "toon").action(cubeQueryCommand);
2863
2548
  program.command("docs").description("Browse documentation for building cubes and views").argument("[topic]", "Topic to display (e.g., cubes, cubes.measures)").option("-r, --recursive", "Show topic and all child topics").option("-s, --search <query>", "Search topics for a keyword").option("-f, --format <format>", "Output format: markdown or json", "markdown").action(docsCommand).command("schema").description("Show JSON schema for a type (cube, view, measure, etc.)").argument("<type>", "Schema type to display").action(docsSchemaCommand);