@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.
- package/dist/bin/bon.mjs +305 -620
- package/dist/bin/validate-DEh1XQnH.mjs +365 -0
- package/dist/docs/_index.md +1 -1
- package/dist/docs/topics/cubes.data-source.md +2 -2
- package/dist/docs/topics/cubes.dimensions.format.md +2 -2
- package/dist/docs/topics/cubes.dimensions.md +2 -2
- package/dist/docs/topics/cubes.dimensions.primary-key.md +2 -2
- package/dist/docs/topics/cubes.dimensions.sub-query.md +2 -2
- package/dist/docs/topics/cubes.dimensions.time.md +2 -2
- package/dist/docs/topics/cubes.dimensions.types.md +2 -2
- package/dist/docs/topics/cubes.extends.md +2 -2
- package/dist/docs/topics/cubes.hierarchies.md +2 -2
- package/dist/docs/topics/cubes.joins.md +2 -2
- package/dist/docs/topics/cubes.md +2 -2
- package/dist/docs/topics/cubes.measures.calculated.md +2 -2
- package/dist/docs/topics/cubes.measures.drill-members.md +2 -2
- package/dist/docs/topics/cubes.measures.filters.md +2 -2
- package/dist/docs/topics/cubes.measures.format.md +21 -2
- package/dist/docs/topics/cubes.measures.md +2 -2
- package/dist/docs/topics/cubes.measures.rolling.md +2 -2
- package/dist/docs/topics/cubes.measures.types.md +2 -2
- package/dist/docs/topics/cubes.public.md +2 -2
- package/dist/docs/topics/cubes.refresh-key.md +2 -2
- package/dist/docs/topics/cubes.segments.md +2 -2
- package/dist/docs/topics/cubes.sql.md +2 -2
- package/dist/docs/topics/features.catalog.md +31 -0
- package/dist/docs/topics/features.cli.md +59 -0
- package/dist/docs/topics/features.context-graph.md +18 -0
- package/dist/docs/topics/features.governance.md +84 -0
- package/dist/docs/topics/features.mcp.md +48 -0
- package/dist/docs/topics/features.md +15 -0
- package/dist/docs/topics/features.sdk.md +53 -0
- package/dist/docs/topics/features.semantic-layer.md +50 -0
- package/dist/docs/topics/features.slack-teams.md +18 -0
- package/dist/docs/topics/getting-started.md +2 -143
- package/dist/docs/topics/pre-aggregations.md +2 -2
- package/dist/docs/topics/pre-aggregations.rollup.md +2 -2
- package/dist/docs/topics/syntax.context-variables.md +2 -2
- package/dist/docs/topics/syntax.md +2 -2
- package/dist/docs/topics/syntax.references.md +2 -2
- package/dist/docs/topics/views.cubes.md +2 -2
- package/dist/docs/topics/views.folders.md +2 -2
- package/dist/docs/topics/views.includes.md +2 -2
- package/dist/docs/topics/views.md +2 -2
- package/dist/docs/topics/workflow.deploy.md +79 -14
- package/dist/docs/topics/workflow.mcp.md +19 -13
- package/dist/docs/topics/workflow.md +25 -8
- package/dist/docs/topics/workflow.query.md +2 -2
- package/dist/docs/topics/workflow.validate.md +4 -31
- package/dist/templates/claude/skills/bonnard-get-started/SKILL.md +16 -26
- package/dist/templates/cursor/rules/bonnard-get-started.mdc +16 -26
- package/dist/templates/shared/bonnard.md +31 -6
- package/package.json +4 -8
- package/dist/bin/validate-DiN3DaTl.mjs +0 -110
- /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
|
|
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
|
-
|
|
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(
|
|
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-
|
|
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 (
|
|
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(
|
|
2364
|
-
console.log(pc.dim("
|
|
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-
|
|
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", {
|
|
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(
|
|
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-
|
|
2434
|
-
const { loadLocalDatasources
|
|
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("
|
|
1927
|
+
console.log(pc.dim("Checking datasources..."));
|
|
2441
1928
|
const localDatasources = loadLocalDatasources(cwd);
|
|
2442
1929
|
let failed = false;
|
|
2443
|
-
const
|
|
1930
|
+
const foundDatasources = [];
|
|
2444
1931
|
for (const ref of references) {
|
|
2445
|
-
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
2490
|
-
if (missingRemote.length
|
|
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
|
-
|
|
2494
|
-
|
|
2495
|
-
|
|
2496
|
-
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
|
|
2500
|
-
|
|
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
|
-
|
|
2513
|
-
|
|
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
|
-
|
|
2516
|
-
|
|
2517
|
-
|
|
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
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
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
|
-
|
|
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
|
|
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("
|
|
2859
|
-
program.command("
|
|
2860
|
-
program.command("
|
|
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);
|