@bonnard/cli 0.2.0 → 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.
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-C31hmPk8.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/.`));
@@ -2314,62 +1818,6 @@ async function validateCommand(options = {}) {
2314
1818
  console.log(pc.dim(" This can cause issues when multiple warehouses are configured."));
2315
1819
  console.log(pc.dim(` ${result.cubesMissingDataSource.join(", ")}`));
2316
1820
  }
2317
- if (options.testConnection) {
2318
- console.log();
2319
- await testReferencedConnections(cwd);
2320
- }
2321
- }
2322
- /**
2323
- * Test connections for datasources referenced by cubes and views
2324
- * Lenient: warns but doesn't fail validation
2325
- */
2326
- async function testReferencedConnections(cwd) {
2327
- const { extractDatasourcesFromCubes } = await import("./cubes-De1_2_YJ.mjs");
2328
- const { loadLocalDatasources, resolveEnvVarsInCredentials } = await Promise.resolve().then(() => local_exports);
2329
- const { testConnection } = await Promise.resolve().then(() => connection_exports);
2330
- const references = extractDatasourcesFromCubes(cwd);
2331
- if (references.length === 0) {
2332
- console.log(pc.dim("No datasource references found in cubes."));
2333
- return;
2334
- }
2335
- console.log(pc.bold("Testing connections..."));
2336
- console.log();
2337
- const localDatasources = loadLocalDatasources(cwd);
2338
- let warnings = 0;
2339
- for (const ref of references) {
2340
- const ds = localDatasources.find((d) => d.name === ref.name);
2341
- if (!ds) {
2342
- console.log(pc.yellow(`⚠ ${ref.name}: not found in .bon/datasources.yaml`));
2343
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2344
- warnings++;
2345
- continue;
2346
- }
2347
- const { resolved, missing } = resolveEnvVarsInCredentials(ds.credentials);
2348
- if (missing.length > 0) {
2349
- console.log(pc.yellow(`⚠ ${ref.name}: missing env vars: ${missing.join(", ")}`));
2350
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2351
- warnings++;
2352
- continue;
2353
- }
2354
- const result = await testConnection({
2355
- type: ds.type,
2356
- config: ds.config,
2357
- credentials: resolved
2358
- });
2359
- if (result.success) {
2360
- const latency = result.latencyMs ? pc.dim(` (${result.latencyMs}ms)`) : "";
2361
- console.log(pc.green(`✓ ${ref.name}${latency}`));
2362
- } else {
2363
- console.log(pc.yellow(`⚠ ${ref.name}: ${result.error || result.message}`));
2364
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2365
- warnings++;
2366
- }
2367
- }
2368
- if (warnings > 0) {
2369
- console.log();
2370
- console.log(pc.yellow(`${warnings} connection warning(s)`));
2371
- console.log(pc.dim("Connection issues won't block file validation, but will fail at deploy."));
2372
- }
2373
1821
  }
2374
1822
 
2375
1823
  //#endregion
@@ -2401,7 +1849,7 @@ async function deployCommand(options = {}) {
2401
1849
  process.exit(1);
2402
1850
  }
2403
1851
  console.log(pc.dim("Validating cubes and views..."));
2404
- const { validate } = await import("./validate-C31hmPk8.mjs");
1852
+ const { validate } = await import("./validate-DEh1XQnH.mjs");
2405
1853
  const result = await validate(cwd);
2406
1854
  if (!result.valid) {
2407
1855
  console.log(pc.red("Validation failed:\n"));
@@ -2470,51 +1918,29 @@ async function deployCommand(options = {}) {
2470
1918
  * Returns true if any connection failed (strict mode)
2471
1919
  */
2472
1920
  async function testAndSyncDatasources(cwd, options = {}) {
2473
- const { extractDatasourcesFromCubes } = await import("./cubes-De1_2_YJ.mjs");
2474
- const { loadLocalDatasources, resolveEnvVarsInCredentials } = await Promise.resolve().then(() => local_exports);
2475
- 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);
2476
1923
  const { pushDatasource } = await Promise.resolve().then(() => push_exports);
2477
1924
  const references = extractDatasourcesFromCubes(cwd);
2478
1925
  if (references.length === 0) return false;
2479
1926
  console.log();
2480
- console.log(pc.dim("Testing datasource connections..."));
1927
+ console.log(pc.dim("Checking datasources..."));
2481
1928
  const localDatasources = loadLocalDatasources(cwd);
2482
1929
  let failed = false;
2483
- const validatedDatasources = [];
1930
+ const foundDatasources = [];
2484
1931
  for (const ref of references) {
2485
- const ds = localDatasources.find((d) => d.name === ref.name);
2486
- if (!ds) {
1932
+ if (!localDatasources.find((d) => d.name === ref.name)) {
2487
1933
  console.log(pc.red(`✗ ${ref.name}: not found in .bon/datasources.yaml`));
2488
1934
  console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2489
1935
  console.log(pc.dim(` Run: bon datasource add --from-dbt`));
2490
1936
  failed = true;
2491
1937
  continue;
2492
1938
  }
2493
- const { resolved, missing } = resolveEnvVarsInCredentials(ds.credentials);
2494
- if (missing.length > 0) {
2495
- console.log(pc.red(`✗ ${ref.name}: missing env vars: ${missing.join(", ")}`));
2496
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2497
- failed = true;
2498
- continue;
2499
- }
2500
- const result = await testConnection({
2501
- type: ds.type,
2502
- config: ds.config,
2503
- credentials: resolved
2504
- });
2505
- if (result.success) {
2506
- const latency = result.latencyMs ? pc.dim(` (${result.latencyMs}ms)`) : "";
2507
- console.log(pc.green(`✓ ${ref.name}${latency}`));
2508
- validatedDatasources.push(ref.name);
2509
- } else {
2510
- console.log(pc.red(`✗ ${ref.name}: ${result.error || result.message}`));
2511
- console.log(pc.dim(` Used by: ${ref.cubes.join(", ")}`));
2512
- failed = true;
2513
- }
1939
+ foundDatasources.push(ref.name);
2514
1940
  }
2515
- console.log();
2516
1941
  if (failed) {
2517
- 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."));
2518
1944
  return true;
2519
1945
  }
2520
1946
  console.log(pc.dim("Checking remote datasources..."));
@@ -2526,22 +1952,17 @@ async function testAndSyncDatasources(cwd, options = {}) {
2526
1952
  return true;
2527
1953
  }
2528
1954
  const remoteNames = new Set(remoteDatasources.map((ds) => ds.name));
2529
- const missingRemote = validatedDatasources.filter((name) => !remoteNames.has(name));
2530
- if (missingRemote.length === 0) {
2531
- console.log(pc.green("✓ All datasources exist on remote"));
1955
+ const missingRemote = foundDatasources.filter((name) => !remoteNames.has(name));
1956
+ if (missingRemote.length > 0) {
2532
1957
  console.log();
2533
- return false;
2534
- }
2535
- console.log();
2536
- console.log(pc.yellow(`⚠ Missing remote datasource${missingRemote.length > 1 ? "s" : ""}: ${missingRemote.join(", ")}`));
2537
- console.log();
2538
- if (options.ci) {
2539
- console.log(pc.red("Deploy aborted (--ci mode)."));
2540
- console.log(pc.dim(`Run: bon datasource push <name>`));
2541
- return true;
2542
- }
2543
- if (options.pushDatasources) {
2544
- 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) {
2545
1966
  console.log(pc.dim(`Pushing "${name}"...`));
2546
1967
  if (await pushDatasource(name, { silent: true })) console.log(pc.green(`✓ Pushed "${name}"`));
2547
1968
  else {
@@ -2549,25 +1970,46 @@ async function testAndSyncDatasources(cwd, options = {}) {
2549
1970
  return true;
2550
1971
  }
2551
1972
  }
2552
- console.log();
2553
- return false;
2554
- }
2555
- if (!await confirm({
2556
- message: `Push ${missingRemote.length > 1 ? "these datasources" : `"${missingRemote[0]}"`} to Bonnard? (credentials will be encrypted)`,
2557
- default: true
2558
- })) {
2559
- console.log(pc.dim("Deploy aborted."));
2560
- return true;
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
+ }
2561
1991
  }
2562
1992
  console.log();
2563
- for (const name of missingRemote) {
2564
- console.log(pc.dim(`Pushing "${name}"...`));
2565
- if (await pushDatasource(name, { silent: true })) console.log(pc.green(`✓ Pushed "${name}"`));
2566
- else {
2567
- console.log(pc.red(`✗ Failed to push "${name}"`));
2568
- return true;
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;
2569
2002
  }
2003
+ } catch (err) {
2004
+ console.log(pc.red(`✗ ${name}: ${err.message}`));
2005
+ failed = true;
2006
+ }
2007
+ console.log();
2008
+ if (failed) {
2009
+ console.log(pc.red("Connection tests failed. Fix datasource issues before deploying."));
2010
+ return true;
2570
2011
  }
2012
+ console.log(pc.green("✓ All datasources connected"));
2571
2013
  console.log();
2572
2014
  return false;
2573
2015
  }
@@ -3093,12 +2535,11 @@ program.command("whoami").description("Show current login status").option("--ver
3093
2535
  const datasource = program.command("datasource").description("Manage warehouse data source connections");
3094
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);
3095
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);
3096
- 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);
3097
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);
3098
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);
3099
- 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);
3100
- 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);
3101
- 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").requiredOption("-m, --message <text>", "Deploy message describing your changes").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);
3102
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);
3103
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);
3104
2545
  program.command("annotate").description("Annotate deployment changes with reasoning").argument("<id>", "Deployment ID").option("--data <json>", "Annotations JSON").action(annotateCommand);