@graphenedata/cli 0.0.4 → 0.0.5

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 (69) hide show
  1. package/cli.ts +7 -43
  2. package/dist/cli/cli.js +529 -293
  3. package/dist/docs/graphene.md +924 -63
  4. package/dist/ui/component-utilities/echarts.js +3 -1
  5. package/dist/ui/component-utilities/themeStores.ts +35 -7
  6. package/dist/ui/components/AreaChart.svelte +2 -1
  7. package/dist/ui/components/BarChart.svelte +2 -1
  8. package/dist/ui/components/BigValue.svelte +1 -1
  9. package/dist/ui/components/Chart.svelte +10 -1
  10. package/dist/ui/components/ECharts.svelte +2 -0
  11. package/dist/ui/components/LineChart.svelte +2 -1
  12. package/dist/ui/components/PieChart.svelte +1 -1
  13. package/dist/ui/components/QueryLoad.svelte +5 -6
  14. package/dist/ui/components/TableRow.svelte +1 -1
  15. package/dist/ui/components/_Table.svelte +2 -0
  16. package/dist/ui/internal/queryEngine.ts +16 -13
  17. package/dist/ui/internal/telemetry.ts +5 -3
  18. package/dist/ui/web.js +26 -11
  19. package/package.json +2 -1
  20. package/dist/docs/data_apps/components/charts/annotations.md +0 -673
  21. package/dist/docs/data_apps/components/charts/area-chart.md +0 -202
  22. package/dist/docs/data_apps/components/charts/bar-chart.md +0 -317
  23. package/dist/docs/data_apps/components/charts/box-plot.md +0 -190
  24. package/dist/docs/data_apps/components/charts/bubble-chart.md +0 -151
  25. package/dist/docs/data_apps/components/charts/calendar-heatmap.md +0 -112
  26. package/dist/docs/data_apps/components/charts/custom-echarts.md +0 -308
  27. package/dist/docs/data_apps/components/charts/echarts-options.md +0 -217
  28. package/dist/docs/data_apps/components/charts/funnel-chart.md +0 -106
  29. package/dist/docs/data_apps/components/charts/heatmap.md +0 -180
  30. package/dist/docs/data_apps/components/charts/histogram.md +0 -107
  31. package/dist/docs/data_apps/components/charts/line-chart.md +0 -265
  32. package/dist/docs/data_apps/components/charts/mixed-type-charts.md +0 -240
  33. package/dist/docs/data_apps/components/charts/sankey-diagram.md +0 -301
  34. package/dist/docs/data_apps/components/charts/scatter-plot.md +0 -134
  35. package/dist/docs/data_apps/components/charts/sparkline.md +0 -68
  36. package/dist/docs/data_apps/components/data/big-value.md +0 -153
  37. package/dist/docs/data_apps/components/data/delta.md +0 -89
  38. package/dist/docs/data_apps/components/data/table.md +0 -470
  39. package/dist/docs/data_apps/components/data/value.md +0 -97
  40. package/dist/docs/data_apps/components/inputs/button-group.md +0 -154
  41. package/dist/docs/data_apps/components/inputs/checkbox.md +0 -52
  42. package/dist/docs/data_apps/components/inputs/date-input.md +0 -131
  43. package/dist/docs/data_apps/components/inputs/date-range.md +0 -124
  44. package/dist/docs/data_apps/components/inputs/dimension-grid.md +0 -67
  45. package/dist/docs/data_apps/components/inputs/dropdown.md +0 -199
  46. package/dist/docs/data_apps/components/inputs/index.md +0 -3
  47. package/dist/docs/data_apps/components/inputs/slider.md +0 -126
  48. package/dist/docs/data_apps/components/inputs/text-input.md +0 -86
  49. package/dist/docs/data_apps/components/maps/area-map.md +0 -397
  50. package/dist/docs/data_apps/components/maps/base-map.md +0 -269
  51. package/dist/docs/data_apps/components/maps/bubble-map.md +0 -361
  52. package/dist/docs/data_apps/components/maps/point-map.md +0 -326
  53. package/dist/docs/data_apps/components/maps/us-map.md +0 -167
  54. package/dist/docs/data_apps/components/ui/accordion.md +0 -116
  55. package/dist/docs/data_apps/components/ui/alert.md +0 -37
  56. package/dist/docs/data_apps/components/ui/big-link.md +0 -19
  57. package/dist/docs/data_apps/components/ui/details.md +0 -58
  58. package/dist/docs/data_apps/components/ui/download-data.md +0 -41
  59. package/dist/docs/data_apps/components/ui/embed.md +0 -47
  60. package/dist/docs/data_apps/components/ui/grid.md +0 -45
  61. package/dist/docs/data_apps/components/ui/image.md +0 -61
  62. package/dist/docs/data_apps/components/ui/info.md +0 -47
  63. package/dist/docs/data_apps/components/ui/last-refreshed.md +0 -28
  64. package/dist/docs/data_apps/components/ui/link-button.md +0 -20
  65. package/dist/docs/data_apps/components/ui/link.md +0 -40
  66. package/dist/docs/data_apps/components/ui/modal.md +0 -57
  67. package/dist/docs/data_apps/components/ui/note.md +0 -32
  68. package/dist/docs/data_apps/components/ui/print-format-components.md +0 -85
  69. package/dist/docs/data_apps/components/ui/tabs.md +0 -122
package/dist/cli/cli.js CHANGED
@@ -58,6 +58,15 @@ function walkExpression(root, fn, parent = null) {
58
58
  });
59
59
  }
60
60
  }
61
+ async function pollFor(fn, timeoutMs, interval) {
62
+ let end = Date.now() + timeoutMs;
63
+ while (Date.now() < end) {
64
+ let res = fn();
65
+ if (res) return res;
66
+ await new Promise((r) => setTimeout(r, interval));
67
+ }
68
+ return null;
69
+ }
61
70
  var init_util = __esm({
62
71
  "../lang/util.ts"() {
63
72
  }
@@ -147,17 +156,15 @@ function loadConfig(dir) {
147
156
  } catch {
148
157
  console.warn("No package.json found in current directory");
149
158
  }
150
- setConfig({
151
- ...packageJsonObject,
152
- dialect: packageJsonObject.dialect || "duckdb",
153
- port: process.env.GRAPHENE_PORT || packageJsonObject.port || 4e3,
154
- root: packageJsonObject.root || process.cwd()
155
- });
159
+ let dialect = "duckdb";
160
+ if (packageJsonObject.bigquery) dialect = "bigquery";
161
+ else if (packageJsonObject.snowflake) dialect = "snowflake";
162
+ setConfig({ ...packageJsonObject, dialect, root: packageJsonObject.root || process.cwd() });
156
163
  }
157
164
  var config;
158
165
  var init_config = __esm({
159
166
  "../lang/config.ts"() {
160
- config = { dialect: "duckdb" };
167
+ config = { dialect: "duckdb", root: "" };
161
168
  }
162
169
  });
163
170
 
@@ -521,10 +528,10 @@ function analyzeQueryTable(table2) {
521
528
  if (table2.query) return;
522
529
  let node = TABLE_NODE_MAP.get(table2);
523
530
  let query = analyzeQuery(node.getChild("QueryStatement"));
524
- if (!query) throw new Error("Couldnt find query in QueryStatement");
531
+ if (!query) return;
525
532
  table2.fields = query.fields.map((f) => ({ type: f.type, name: f.name, metadata: f.metadata }));
526
533
  table2.query = query.malloyQuery;
527
- if (typeof table2.query.structRef == "string") {
534
+ if (table2.query && typeof table2.query.structRef == "string") {
528
535
  table2.query.structRef = lookupTable(table2.query.structRef, node);
529
536
  }
530
537
  }
@@ -534,9 +541,10 @@ function analyzeQuery(queryNode) {
534
541
  let isAgg = false;
535
542
  let subQuerySources = [];
536
543
  if (!txt(queryNode)) return;
544
+ if (txt(queryNode).trim().toLowerCase() == "select 1") return { fields: [{ name: "col_0", type: "number", metadata: {}, e: { node: "numberLiteral", literal: "1", type: "number" } }], subQuerySources, rawSql: "select 1" };
537
545
  let froms = queryNode.getChild("FromClause")?.getChildren("TablePrimary") || [];
538
546
  if (froms.find((f) => f.name == "JoinClause")) diag(froms[0], "Query joins not yet supported");
539
- if (froms.length == 0) diag(queryNode, "No tables in FROM clause");
547
+ if (froms.length == 0) return diag(queryNode, "No tables in FROM clause");
540
548
  if (froms.length > 1) diag(froms[0], "Multiple tables/joins in FROM clause not yet supported");
541
549
  if (froms[0].name == "Subquery") {
542
550
  structRef = txt(froms[0].getChild("Alias")) || "subquery";
@@ -555,9 +563,9 @@ function analyzeQuery(queryNode) {
555
563
  isAgg ||= !!isSelectDistinct;
556
564
  selects.forEach((s) => {
557
565
  if (s.getChild("Wildcard")) {
558
- let path8 = s.getChild("Wildcard").getChildren("Identifier");
559
- let pathStrings = path8.map((p) => txt(p));
560
- let target = followJoins(path8, scope.table);
566
+ let path9 = s.getChild("Wildcard").getChildren("Identifier");
567
+ let pathStrings = path9.map((p) => txt(p));
568
+ let target = followJoins(path9, scope.table);
561
569
  if (!target) return;
562
570
  target.fields.forEach((f) => {
563
571
  if (isJoin(f) || f.isAgg) return;
@@ -655,8 +663,8 @@ function analyzeExpression(expr, scope) {
655
663
  if (scope.outputFields.includes(field) && field.isAgg) {
656
664
  return { node: "outputField", name: field.name, ...typeInfo, isAgg: field.isAgg };
657
665
  }
658
- let path8 = expr.getChildren("Identifier").map((i) => txt(i));
659
- return { node: "field", path: path8, ...typeInfo, isAgg: field.isAgg };
666
+ let path9 = expr.getChildren("Identifier").map((i) => txt(i));
667
+ return { node: "field", path: path9, ...typeInfo, isAgg: field.isAgg };
660
668
  }
661
669
  case "ExtractExpression": {
662
670
  let e = analyzeExpression(expr.getChild("Expression"), scope);
@@ -862,6 +870,10 @@ function convertDataType(dataType) {
862
870
  return "number";
863
871
  case "INT64":
864
872
  return "number";
873
+ case "NUMBER":
874
+ return "number";
875
+ case "VARIANT":
876
+ return "string";
865
877
  case "TEXT":
866
878
  return "string";
867
879
  case "STRING":
@@ -884,6 +896,8 @@ function convertDataType(dataType) {
884
896
  return "timestamp";
885
897
  case "TIMESTAMP":
886
898
  return "timestamp";
899
+ case "TIMESTAMP_NTZ":
900
+ return "timestamp";
887
901
  case "DECIMAL":
888
902
  return "number";
889
903
  case "DOUBLE":
@@ -1215,11 +1229,11 @@ async function loadWorkspace(dir, includeMd) {
1215
1229
  updateFile(contents, file);
1216
1230
  }
1217
1231
  }
1218
- function updateFile(contents, path8) {
1219
- FILE_MAP[path8] ||= { path: path8, contents, tree: null, tables: [], queries: [] };
1220
- FILE_MAP[path8].contents = contents;
1221
- FILE_MAP[path8].tree = null;
1222
- return FILE_MAP[path8];
1232
+ function updateFile(contents, path9) {
1233
+ FILE_MAP[path9] ||= { path: path9, contents, tree: null, tables: [], queries: [] };
1234
+ FILE_MAP[path9].contents = contents;
1235
+ FILE_MAP[path9].tree = null;
1236
+ return FILE_MAP[path9];
1223
1237
  }
1224
1238
  function analyze(contents, type) {
1225
1239
  clearDiagnostics();
@@ -1243,11 +1257,13 @@ function analyze(contents, type) {
1243
1257
  }
1244
1258
  }
1245
1259
  function toSql(query, params = {}) {
1260
+ if (query.rawSql) return query.rawSql;
1246
1261
  let contents = {};
1247
1262
  let gsqlTables = Object.values(FILE_MAP).filter((f) => f.path !== "input").flatMap((f) => f.tables);
1248
1263
  gsqlTables.forEach((t) => contents[t.name] = t);
1249
1264
  let inputTables = [...FILE_MAP["input"]?.tables || [], ...query.subQuerySources];
1250
1265
  inputTables.forEach((t) => contents[t.name] = { ...t, query: t.query && fillInParams(t.query, params) });
1266
+ if (!query.malloyQuery) throw new Error("Cannot compile query without Malloy query");
1251
1267
  let malloyQuery = fillInParams(query.malloyQuery, params);
1252
1268
  let qm = new QueryModel({
1253
1269
  name: "generated_model",
@@ -1281,6 +1297,172 @@ var init_core = __esm({
1281
1297
  }
1282
1298
  });
1283
1299
 
1300
+ // printer.ts
1301
+ import { styleText as nodeStyleText } from "node:util";
1302
+ import Table from "cli-table3";
1303
+ import chalk from "chalk";
1304
+ function offsetToLineCol(src, offset2) {
1305
+ let lines = src.split(/\r?\n/);
1306
+ let acc = 0;
1307
+ for (let i = 0; i < lines.length; i++) {
1308
+ let lineText = lines[i];
1309
+ let nextAcc = acc + lineText.length + 1;
1310
+ if (offset2 < nextAcc || i === lines.length - 1) {
1311
+ let col = Math.max(0, offset2 - acc);
1312
+ return { line: i + 1, col, lineStart: acc, lineText };
1313
+ }
1314
+ acc = nextAcc;
1315
+ }
1316
+ return { line: 1, col: 0, lineStart: 0, lineText: lines[0] || "" };
1317
+ }
1318
+ function printDiagnostics(diags, log) {
1319
+ log ||= console.log;
1320
+ let parts = [];
1321
+ for (let d of diags) {
1322
+ let src = getFile2(d.file)?.contents || "";
1323
+ let { line, col, lineStart, lineText } = offsetToLineCol(src, d.from.offset);
1324
+ let endCol = Math.max(col + 1, Math.min(lineText.length, d.to.offset - lineStart));
1325
+ let caretLen = Math.max(1, endCol - col);
1326
+ let sev = d.severity === "error" ? "red" : "yellow";
1327
+ let header = `${styleText(sev, d.severity.toUpperCase())}: ${d.file} line ${line}: ${d.message}`;
1328
+ let gutter = " | ";
1329
+ let caretLine = `${" ".repeat(col)}${styleText(sev, "^".repeat(caretLen))}`;
1330
+ parts.push([header, `${gutter}${lineText}`, `${gutter}${caretLine}`].join("\n"));
1331
+ }
1332
+ if (parts.length) log(parts.join("\n"));
1333
+ }
1334
+ function printTable(rows) {
1335
+ if (!rows || rows.length === 0) {
1336
+ console.log(chalk.yellow("No results returned"));
1337
+ return;
1338
+ }
1339
+ let headers = Object.keys(rows[0]);
1340
+ let table2 = new Table({ head: headers.map((h) => chalk.blue(h)) });
1341
+ let MAX_DISPLAY_ROWS = 200;
1342
+ let displayRows = rows.slice(0, MAX_DISPLAY_ROWS);
1343
+ displayRows.forEach((row) => table2.push(headers.map((h) => row[h]?.toString() || "")));
1344
+ console.log(table2.toString());
1345
+ if (rows.length > MAX_DISPLAY_ROWS) {
1346
+ console.log(chalk.yellow(`Displayed first ${MAX_DISPLAY_ROWS} rows (of ${rows.length} total).`));
1347
+ }
1348
+ }
1349
+ var styleText;
1350
+ var init_printer = __esm({
1351
+ "printer.ts"() {
1352
+ init_core();
1353
+ styleText = (style, text) => {
1354
+ try {
1355
+ return nodeStyleText ? nodeStyleText(style, text) : text;
1356
+ } catch {
1357
+ return text;
1358
+ }
1359
+ };
1360
+ }
1361
+ });
1362
+
1363
+ // background.ts
1364
+ import { spawn } from "child_process";
1365
+ import { fileURLToPath } from "url";
1366
+ import fs2 from "fs-extra";
1367
+ import path3 from "path";
1368
+ async function runServeInBackground() {
1369
+ let root = process.cwd();
1370
+ let grapheneCache = getGrapheneCache(root);
1371
+ let logFile = path3.join(grapheneCache, "serve.log");
1372
+ await fs2.ensureDir(grapheneCache);
1373
+ await stopGrapheneIfRunning(root);
1374
+ let log = fs2.openSync(logFile, "w");
1375
+ let entryPoint = process.argv[1] || fileURLToPath(import.meta.url);
1376
+ let childArgs = [...process.execArgv, entryPoint, "serve", "--fg", ...process.argv.slice(3)];
1377
+ let child = spawn(process.execPath, childArgs, {
1378
+ cwd: root,
1379
+ detached: true,
1380
+ env: { ...process.env },
1381
+ stdio: ["ignore", log, log]
1382
+ });
1383
+ if (!child.pid) throw new Error("Failed to start server process");
1384
+ await new Promise((resolve, reject) => {
1385
+ let buffer = "";
1386
+ fs2.watchFile(logFile, { interval: 200 }, (curr, prev) => {
1387
+ if (curr.size > prev.size) {
1388
+ let stream = fs2.createReadStream(logFile, { start: 0, end: curr.size - 1 });
1389
+ stream.on("data", (d) => {
1390
+ process.stdout.write(d);
1391
+ buffer = (buffer + d.toString()).slice(-200);
1392
+ if (buffer.includes("Server running at http://localhost:")) resolve();
1393
+ });
1394
+ }
1395
+ });
1396
+ child.once("exit", () => {
1397
+ process.stdout.write(fs2.readFileSync(logFile));
1398
+ reject(new Error("Exited before server started"));
1399
+ });
1400
+ child.once("error", (e) => reject(e));
1401
+ });
1402
+ }
1403
+ function getGrapheneCache(root) {
1404
+ return path3.join(root, "node_modules", ".graphene");
1405
+ }
1406
+ function getPidFilePath(root) {
1407
+ return path3.join(getGrapheneCache(root), process.env.NODE_ENV == "test" ? "test.pid" : "serve.pid");
1408
+ }
1409
+ function targetPids(pid) {
1410
+ if (process.platform === "win32") return [pid];
1411
+ return [pid, -pid];
1412
+ }
1413
+ function sendSignal(pid, signal) {
1414
+ for (let target of targetPids(pid)) {
1415
+ try {
1416
+ process.kill(target, signal);
1417
+ } catch (err) {
1418
+ let code = err.code;
1419
+ if (code === "ESRCH" || code === "EINVAL") continue;
1420
+ return false;
1421
+ }
1422
+ }
1423
+ return true;
1424
+ }
1425
+ async function stopGrapheneIfRunning(root) {
1426
+ if (!await isServerRunning()) return;
1427
+ let pidFile = getPidFilePath(root);
1428
+ let pid = await readPid(pidFile);
1429
+ if (!pid) return;
1430
+ console.log(`Stopping server (${pid})`);
1431
+ sendSignal(pid, "SIGTERM");
1432
+ let end = Date.now() + 5e3;
1433
+ while (Date.now() < end && isServerRunning()) {
1434
+ await new Promise((resolve) => setTimeout(resolve, 100));
1435
+ }
1436
+ if (!isServerRunning()) return;
1437
+ sendSignal(pid, "SIGKILL");
1438
+ await fs2.remove(pidFile);
1439
+ }
1440
+ async function readPid(pidFile) {
1441
+ if (!await fs2.pathExists(pidFile)) return void 0;
1442
+ let contents = (await fs2.readFile(pidFile, "utf8")).trim();
1443
+ if (!contents) return void 0;
1444
+ let pid = Number.parseInt(contents, 10);
1445
+ if (Number.isNaN(pid)) return void 0;
1446
+ return pid;
1447
+ }
1448
+ async function isServerRunning() {
1449
+ let pidFile = getPidFilePath(config.root);
1450
+ let pid = await readPid(pidFile);
1451
+ if (!pid) return false;
1452
+ try {
1453
+ process.kill(pid, 0);
1454
+ return true;
1455
+ } catch {
1456
+ fs2.removeSync(pidFile);
1457
+ return false;
1458
+ }
1459
+ }
1460
+ var init_background = __esm({
1461
+ "background.ts"() {
1462
+ init_config();
1463
+ }
1464
+ });
1465
+
1284
1466
  // connections/bigQuery.ts
1285
1467
  var bigQuery_exports = {};
1286
1468
  __export(bigQuery_exports, {
@@ -1294,16 +1476,14 @@ var init_bigQuery = __esm({
1294
1476
  BigQueryConnection = class {
1295
1477
  client;
1296
1478
  constructor(options = {}) {
1479
+ options.projectId ||= config.bigquery?.projectId;
1297
1480
  if (process.env.GOOGLE_CREDENTIALS_CONTENT) {
1298
1481
  let parsed = JSON.parse(process.env.GOOGLE_CREDENTIALS_CONTENT);
1299
1482
  options.projectId = parsed.project_id;
1300
1483
  options.credentials = parsed;
1301
1484
  }
1302
- options.projectId ||= config.googleProjectId;
1303
- options.maxRetries ||= 3;
1304
- options.userAgent ||= "Graphene";
1305
- if (!options.projectId) throw new Error("googleProjectId must be set in config or provided in service account credentials");
1306
- this.client = new BigQuery(options);
1485
+ if (!options.projectId) throw new Error("projectId must be set in config or provided in service account credentials");
1486
+ this.client = new BigQuery({ ...options, userAgent: "Graphene" });
1307
1487
  }
1308
1488
  async runQuery(sql) {
1309
1489
  let [job] = await this.client.createQueryJob({ query: sql, useLegacySql: false });
@@ -1335,19 +1515,24 @@ var init_duckdb = __esm({
1335
1515
  "connections/duckdb.ts"() {
1336
1516
  init_config();
1337
1517
  DuckDBConnection = class {
1518
+ options;
1338
1519
  ready;
1339
1520
  connection = null;
1340
- constructor() {
1521
+ constructor(options) {
1522
+ this.options = options || {};
1341
1523
  this.ready = this.initialize();
1342
1524
  }
1343
1525
  async initialize() {
1344
- let files = await fs3.readdir(config.root);
1345
- let databasePath = files.find((f) => f.endsWith(".duckdb"));
1346
- if (!databasePath) throw new Error("No .duckdb file found in current directory");
1347
- databasePath = path4.resolve(config.root, databasePath);
1526
+ let dbPath = this.options.path;
1527
+ if (!dbPath) {
1528
+ let files = await fs3.readdir(config.root);
1529
+ dbPath = files.find((f) => f.endsWith(".duckdb"));
1530
+ if (!dbPath) throw new Error("No .duckdb file found in current directory");
1531
+ dbPath = path4.resolve(config.root, dbPath);
1532
+ }
1348
1533
  let db = await DuckDBInstance.create(":memory:");
1349
1534
  this.connection = await db.connect();
1350
- let escapedPath = databasePath.replace(/'/g, "''");
1535
+ let escapedPath = dbPath.replace(/'/g, "''");
1351
1536
  await this.connection.run(`attach '${escapedPath}' as graphene_cli (READ_ONLY);`);
1352
1537
  await this.connection.run("use graphene_cli;");
1353
1538
  }
@@ -1372,6 +1557,76 @@ var init_duckdb = __esm({
1372
1557
  }
1373
1558
  });
1374
1559
 
1560
+ // connections/snowflake.ts
1561
+ var snowflake_exports = {};
1562
+ __export(snowflake_exports, {
1563
+ SnowflakeConnection: () => SnowflakeConnection
1564
+ });
1565
+ import { createPrivateKey } from "node:crypto";
1566
+ import snowflake from "snowflake-sdk";
1567
+ var SnowflakeConnection;
1568
+ var init_snowflake = __esm({
1569
+ "connections/snowflake.ts"() {
1570
+ init_config();
1571
+ SnowflakeConnection = class {
1572
+ ready;
1573
+ connection;
1574
+ constructor(opts) {
1575
+ this.ready = this.initialize(opts || {});
1576
+ }
1577
+ async initialize(opts) {
1578
+ let privateKeyPath = process.env.SNOWFLAKE_PRI_KEY_PATH || config.snowflake?.privateKeyPath;
1579
+ let privateKeyPass = process.env.SNOWFLAKE_PRI_PASSPHRASE;
1580
+ let authOptions = {};
1581
+ if (privateKeyPath) {
1582
+ authOptions = { privateKeyPath, privateKeyPass };
1583
+ } else if (opts.privateKey) {
1584
+ let privateKey = createPrivateKey({ key: opts.privateKey, format: "pem", passphrase: privateKeyPass });
1585
+ authOptions = { privateKey: privateKey.export({ format: "pem", type: "pkcs8" }) };
1586
+ }
1587
+ snowflake.configure({ logLevel: process.env.SNOWFLAKE_LOG_LEVEL || "WARN", logFilePath: "/dev/null" });
1588
+ this.connection = snowflake.createConnection({
1589
+ ...opts,
1590
+ ...config.snowflake || {},
1591
+ ...authOptions,
1592
+ authenticator: "SNOWFLAKE_JWT",
1593
+ application: "Graphene"
1594
+ });
1595
+ await new Promise((resolve, reject) => {
1596
+ this.connection.connect((err, conn) => err ? reject(err) : resolve(conn));
1597
+ });
1598
+ }
1599
+ async runQuery(sql) {
1600
+ await this.ready;
1601
+ return await new Promise((resolve, reject) => {
1602
+ let rows = [];
1603
+ this.connection.execute({
1604
+ sqlText: sql,
1605
+ streamResult: true,
1606
+ complete: (error, statement) => {
1607
+ if (error) {
1608
+ reject(new Error(`Snowflake query failed: ${error.message || error}`));
1609
+ return;
1610
+ }
1611
+ let stream = statement.streamRows();
1612
+ stream.on("error", (err) => reject(err));
1613
+ stream.on("readable", function(row) {
1614
+ while ((row = this.read()) !== null) {
1615
+ rows.push(row);
1616
+ }
1617
+ });
1618
+ stream.on("end", () => {
1619
+ let totalRows = Number(statement.getNumRows());
1620
+ resolve({ rows, totalRows });
1621
+ });
1622
+ }
1623
+ });
1624
+ });
1625
+ }
1626
+ };
1627
+ }
1628
+ });
1629
+
1375
1630
  // connections/index.ts
1376
1631
  async function getConnection() {
1377
1632
  if (config.dialect === "bigquery") {
@@ -1379,7 +1634,10 @@ async function getConnection() {
1379
1634
  return new mod.BigQueryConnection();
1380
1635
  } else if (config.dialect === "duckdb") {
1381
1636
  let mod = await Promise.resolve().then(() => (init_duckdb(), duckdb_exports));
1382
- return new mod.DuckDBConnection();
1637
+ return new mod.DuckDBConnection({});
1638
+ } else if (config.dialect === "snowflake") {
1639
+ let mod = await Promise.resolve().then(() => (init_snowflake(), snowflake_exports));
1640
+ return new mod.SnowflakeConnection({});
1383
1641
  } else {
1384
1642
  throw new Error(`Unsupported dialect: ${config.dialect}`);
1385
1643
  }
@@ -1390,9 +1648,204 @@ var init_connections = __esm({
1390
1648
  }
1391
1649
  });
1392
1650
 
1393
- // mdCompile.ts
1394
- import fs4 from "fs";
1651
+ // mockFiles.ts
1652
+ var mockFileMap;
1653
+ var init_mockFiles = __esm({
1654
+ "mockFiles.ts"() {
1655
+ mockFileMap = {};
1656
+ }
1657
+ });
1658
+
1659
+ // check.ts
1660
+ import fs4 from "fs-extra";
1661
+ import os from "os";
1395
1662
  import path5 from "path";
1663
+ import { spawn as spawn2 } from "child_process";
1664
+ import { WebSocketServer } from "ws";
1665
+ import { readFileSync as readFileSync2 } from "node:fs";
1666
+ import { styleText as styleText2 } from "node:util";
1667
+ async function check(options) {
1668
+ let log = options.log || console.log;
1669
+ let mdFile = options.mdArg && normalizeMdFile(options.mdArg);
1670
+ if (options.mdArg && !mdFile) {
1671
+ log(`Couldn't find ${options.mdArg}`);
1672
+ return false;
1673
+ }
1674
+ await loadWorkspace(config.root, !mdFile);
1675
+ if (mdFile) {
1676
+ if (process.env.NODE_ENV == "test" && mockFileMap[mdFile]) {
1677
+ updateFile(mockFileMap[mdFile], mdFile);
1678
+ } else {
1679
+ let content = readFileSync2(path5.resolve(config.root, mdFile), "utf-8");
1680
+ updateFile(content, mdFile);
1681
+ }
1682
+ }
1683
+ analyze();
1684
+ if (getDiagnostics().length > 0) {
1685
+ printDiagnostics(getDiagnostics(), log);
1686
+ return false;
1687
+ }
1688
+ if (!mdFile) {
1689
+ log("No errors found \u{1F48E}");
1690
+ return true;
1691
+ }
1692
+ let host = `http://localhost:${config.port || Number(process.env.GRAPHENE_PORT) || 4e3}`;
1693
+ let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "").replace(/\\/g, "/");
1694
+ if (pageUrl === "/index") pageUrl = "/";
1695
+ if (process.env.NODE_ENV !== "test" && !await isServerRunning()) {
1696
+ log("Starting Graphene server...");
1697
+ await runServeInBackground();
1698
+ }
1699
+ let resp = await sendCheckRequest({ host, pageUrl, chart: options.chart });
1700
+ if (resp.checkError == "no_server") {
1701
+ log("Failed to start Graphene server");
1702
+ return false;
1703
+ }
1704
+ if (resp.checkError == "no_tab" && process.env.NODE_ENV !== "test") {
1705
+ log(`Opening page ${host}${pageUrl}`);
1706
+ spawn2("open", [host + pageUrl]);
1707
+ await new Promise((resolve) => setTimeout(resolve, 500));
1708
+ resp = await sendCheckRequest({ host, pageUrl, chart: options.chart });
1709
+ }
1710
+ if (resp.checkError == "no_tab") {
1711
+ log("Failed to open a new tab");
1712
+ return false;
1713
+ }
1714
+ if (resp.checkError) {
1715
+ log("Failed to run check: " + resp.checkError);
1716
+ return false;
1717
+ }
1718
+ let errors = Array.from(resp.errors || []);
1719
+ if (errors.length) {
1720
+ log(styleText2("red", "Runtime errors") + ` in ${mdFile}:`);
1721
+ } else {
1722
+ log("No errors found \u{1F48E}");
1723
+ }
1724
+ errors.forEach((e) => {
1725
+ if (e.file && e.line) printDiagnostics([e], log);
1726
+ else if (e.id) log(`${e.id}: ${e.message}`);
1727
+ else log(e.message);
1728
+ });
1729
+ if (resp?.stillLoading) {
1730
+ log("Warning: Queries were still loading when the screenshot was taken");
1731
+ }
1732
+ if (resp?.screenshot) {
1733
+ let filename = `graphene-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`;
1734
+ let screenshotPath = path5.join(os.tmpdir(), filename);
1735
+ let base64Data = resp.screenshot.replace(/^data:image\/png;base64,/, "");
1736
+ await fs4.writeFile(screenshotPath, base64Data, "base64");
1737
+ log("Screenshot saved to", screenshotPath);
1738
+ }
1739
+ return !!errors.length;
1740
+ }
1741
+ async function sendCheckRequest({ host, pageUrl, chart }) {
1742
+ let abort = new AbortController();
1743
+ let timeout = setTimeout(() => abort.abort(), 3e4);
1744
+ try {
1745
+ let response = await fetch(`${host}/_api/check`, {
1746
+ method: "POST",
1747
+ headers: { "Content-Type": "application/json" },
1748
+ body: JSON.stringify({ pageUrl: host + pageUrl, chart }),
1749
+ signal: abort.signal
1750
+ });
1751
+ clearTimeout(timeout);
1752
+ let body = response.headers.get("content-type") == "application/json" ? await response.json() : { error: await response.text() };
1753
+ if (!response.ok) {
1754
+ if (body.error) return { checkError: body.error };
1755
+ console.error(`Unexpected response: ${JSON.stringify(body)}`);
1756
+ return { checkError: "Unexpected response from Graphene server" };
1757
+ }
1758
+ return body;
1759
+ } catch (err) {
1760
+ clearTimeout(timeout);
1761
+ if (err.name === "AbortError") return { checkError: "timeout" };
1762
+ return { checkError: "no_server" };
1763
+ }
1764
+ }
1765
+ function normalizeMdFile(mdFile) {
1766
+ let clean = mdFile.trim();
1767
+ if (!clean) return null;
1768
+ if (!clean.endsWith(".md")) clean = clean + ".md";
1769
+ if (process.env.NODE_ENV == "test" && mockFileMap[clean]) {
1770
+ return clean;
1771
+ }
1772
+ let absolute = [
1773
+ path5.resolve(process.cwd(), clean),
1774
+ path5.resolve(config.root, clean)
1775
+ ].find((p) => fs4.existsSync(p)) || null;
1776
+ if (!absolute) return null;
1777
+ let relative = path5.relative(config.root, absolute);
1778
+ return relative;
1779
+ }
1780
+ async function proxyCheckRequest(req, res) {
1781
+ let chunks = [];
1782
+ for await (let chunk of req) chunks.push(chunk);
1783
+ let { pageUrl, chart } = JSON.parse(Buffer.concat(chunks).toString());
1784
+ let id = Math.random().toString(36).slice(2);
1785
+ res.setHeader("Content-Type", "application/json");
1786
+ let normalizedPageUrl = pageUrl.replace(/\/$/, "");
1787
+ let conn = await pollFor(() => browserConnections.find((conn2) => conn2.url === normalizedPageUrl), 5e3, 100);
1788
+ if (!conn) {
1789
+ res.statusCode = 400;
1790
+ res.end(JSON.stringify({ error: "no_tab" }));
1791
+ return;
1792
+ } else {
1793
+ conn.socket.send(JSON.stringify({ type: "check", chart, requestId: id }));
1794
+ pendingRequests[id] = { response: res };
1795
+ }
1796
+ }
1797
+ function checkVitePlugin() {
1798
+ return {
1799
+ name: "graphene-check-plugin",
1800
+ configureServer(server) {
1801
+ let wss = new WebSocketServer({ noServer: true });
1802
+ server.httpServer?.on("upgrade", (req, socket, head) => {
1803
+ if (!req.url || !req.url.includes("/_api/ws") && !req.url.includes("graphene-ws")) return;
1804
+ wss.handleUpgrade(req, socket, head, (ws) => {
1805
+ wss.emit("connection", ws, req);
1806
+ });
1807
+ });
1808
+ wss.on("connection", (socket) => {
1809
+ socket.on("message", (data) => {
1810
+ let message = JSON.parse(data.toString());
1811
+ if (message.type === "register") {
1812
+ let normalizedUrl = message.url.replace(/\/$/, "");
1813
+ browserConnections.push({ url: normalizedUrl, socket });
1814
+ }
1815
+ if (message.type === "checkResponse") {
1816
+ pendingRequests[message.requestId].response.end(JSON.stringify(message));
1817
+ delete pendingRequests[message.requestId];
1818
+ }
1819
+ });
1820
+ socket.on("close", () => {
1821
+ browserConnections = browserConnections.filter((conn) => conn.socket !== socket);
1822
+ });
1823
+ });
1824
+ server.httpServer?.on("close", () => wss.close());
1825
+ server.middlewares.use(async (req, res, next) => {
1826
+ let [pathName] = (req.url || "").split("?");
1827
+ if (pathName === "/_api/check") await proxyCheckRequest(req, res);
1828
+ else next();
1829
+ });
1830
+ }
1831
+ };
1832
+ }
1833
+ var browserConnections, pendingRequests;
1834
+ var init_check = __esm({
1835
+ "check.ts"() {
1836
+ init_core();
1837
+ init_printer();
1838
+ init_mockFiles();
1839
+ init_background();
1840
+ init_util();
1841
+ browserConnections = [];
1842
+ pendingRequests = {};
1843
+ }
1844
+ });
1845
+
1846
+ // mdCompile.ts
1847
+ import fs5 from "fs";
1848
+ import path6 from "path";
1396
1849
  import { visit } from "unist-util-visit";
1397
1850
  import sanitizeHtml from "sanitize-html";
1398
1851
  function extractQueries() {
@@ -1469,8 +1922,8 @@ ${content}`;
1469
1922
  }
1470
1923
  function componentNames() {
1471
1924
  if (cachedComponentNames) return cachedComponentNames;
1472
- let files = fs4.readdirSync(path5.join(import.meta.dirname, "../ui/components"));
1473
- cachedComponentNames = files.map((f) => path5.basename(f, ".svelte")).filter((f) => !f.startsWith("_"));
1925
+ let files = fs5.readdirSync(path6.join(import.meta.dirname, "../ui/components"));
1926
+ cachedComponentNames = files.map((f) => path6.basename(f, ".svelte")).filter((f) => !f.startsWith("_"));
1474
1927
  return cachedComponentNames || [];
1475
1928
  }
1476
1929
  var cachedComponentNames;
@@ -1483,23 +1936,20 @@ var init_mdCompile = __esm({
1483
1936
  // serve2.ts
1484
1937
  var serve2_exports = {};
1485
1938
  __export(serve2_exports, {
1486
- mockFileMap: () => mockFileMap,
1487
1939
  serve2: () => serve2
1488
1940
  });
1489
1941
  import { createServer, optimizeDeps } from "vite";
1490
1942
  import { svelte, vitePreprocess } from "@sveltejs/vite-plugin-svelte";
1491
- import fs5 from "fs-extra";
1943
+ import fs6 from "fs-extra";
1492
1944
  import crypto from "crypto";
1493
1945
  import { mdsvex } from "mdsvex";
1494
- import path6 from "path";
1946
+ import path7 from "path";
1495
1947
  import { fileURLToPath as fileURLToPath2 } from "url";
1496
- import { WebSocketServer } from "ws";
1497
- import { spawn as spawn2 } from "child_process";
1498
1948
  async function serve2() {
1499
- grapheneRoot = config.root;
1500
- uiRoot = path6.join(fileURLToPath2(import.meta.url), "../../ui");
1501
- await fs5.ensureDir(path6.resolve(grapheneRoot, "node_modules/.graphene"));
1502
- await fs5.writeFile(path6.resolve(grapheneRoot, `node_modules/.graphene/${process.env.NODE_ENV == "test" ? "test" : "serve"}.pid`), String(process.pid));
1949
+ uiRoot = path7.join(fileURLToPath2(import.meta.url), "../../ui");
1950
+ let port = Number(process.env.GRAPHENE_PORT) || 4e3;
1951
+ await fs6.ensureDir(path7.resolve(config.root, "node_modules/.graphene"));
1952
+ await fs6.writeFile(path7.resolve(config.root, `node_modules/.graphene/${process.env.NODE_ENV == "test" ? "test" : "serve"}.pid`), String(process.pid));
1503
1953
  let server = await createServer({
1504
1954
  root: config.root,
1505
1955
  plugins: [
@@ -1515,24 +1965,32 @@ async function serve2() {
1515
1965
  injectComponentImports()
1516
1966
  ]
1517
1967
  }),
1968
+ checkVitePlugin(),
1518
1969
  handleRequestPlugin,
1519
1970
  updateWorkspacePlugin,
1520
1971
  mockFilesForTests()
1521
1972
  ],
1973
+ publicDir: path7.resolve(uiRoot),
1522
1974
  server: {
1523
- port: config.port,
1975
+ port,
1524
1976
  fs: { strict: false },
1525
1977
  strictPort: true
1526
1978
  },
1527
1979
  resolve: {
1528
1980
  alias: {
1529
- graphene: path6.resolve(uiRoot, "web.js")
1981
+ graphene: path7.resolve(uiRoot, "web.js")
1530
1982
  }
1531
1983
  }
1984
+ // optimizeDeps: { // this seems prudent in tests, but currently breaks because ssf needs to be optimized, even in tests
1985
+ // noDiscovery: process.env.NODE_ENV == 'test',
1986
+ // include: process.env.NODE_ENV == 'test' ? [] : undefined,
1987
+ // },
1532
1988
  });
1533
1989
  await optimizeDeps(server.config);
1534
1990
  await server.listen();
1535
- console.log(`Server running at http://localhost:${config.port}`);
1991
+ if (process.env.NODE_ENV !== "test") {
1992
+ console.log(`Server running at http://localhost:${port}`);
1993
+ }
1536
1994
  return server;
1537
1995
  }
1538
1996
  async function handleQuery(req, res) {
@@ -1562,31 +2020,6 @@ async function handleQuery(req, res) {
1562
2020
  let fields = queries[0].fields.map((f) => ({ name: f.name, type: f.type }));
1563
2021
  res.end(JSON.stringify({ rows: queryResults.rows, hash, fields, sql }));
1564
2022
  }
1565
- async function handleView(req, res) {
1566
- let chunks = [];
1567
- for await (let chunk of req) chunks.push(chunk);
1568
- let { mdFile, chart } = JSON.parse(Buffer.concat(chunks).toString());
1569
- let id = Math.random().toString(36).slice(2);
1570
- res.setHeader("Content-Type", "application/json");
1571
- viewRequests[id] = { response: res };
1572
- let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "");
1573
- if (pageUrl === "/index") pageUrl = "/";
1574
- pageUrl = `http://localhost:${config.port || 4e3}${pageUrl}`;
1575
- let conn = browserConnections.find((conn2) => conn2.url === pageUrl);
1576
- if (!conn) {
1577
- spawn2("open", [pageUrl]);
1578
- let end = Date.now() + 5e3;
1579
- while (Date.now() < end && !conn) {
1580
- conn = browserConnections.find((conn2) => conn2.url === pageUrl);
1581
- await new Promise((resolve) => setTimeout(resolve, 100));
1582
- }
1583
- if (!conn) {
1584
- res.statusCode = 500;
1585
- return res.end(JSON.stringify({ error: "No browser tab available and failed to open one" }));
1586
- }
1587
- }
1588
- conn.socket.send(JSON.stringify({ type: "view", chart, requestId: id }));
1589
- }
1590
2023
  async function handlePage(server, res, filePath, mount) {
1591
2024
  res.setHeader("Content-Type", "text/html");
1592
2025
  let mdMount = mount ? `
@@ -1599,7 +2032,7 @@ async function handlePage(server, res, filePath, mount) {
1599
2032
  <meta charset="UTF-8" />
1600
2033
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1601
2034
  <title>Graphene</title>
1602
- <link rel="icon" href="@graphenedata/ui/assets/favicon.ico" />
2035
+ <link rel="icon" href="/assets/favicon.ico" />
1603
2036
  <link rel="preconnect" href="https://fonts.googleapis.com">
1604
2037
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1605
2038
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
@@ -1609,10 +2042,7 @@ async function handlePage(server, res, filePath, mount) {
1609
2042
  <div id="content"></div>
1610
2043
  </main>
1611
2044
  <script type="module">
1612
- // do this first so we can track errors caused by importing the md file
1613
- import 'graphene'
1614
- </script>
1615
- <script type="module">
2045
+ import 'graphene' // do this first so we can track errors caused by importing the md file
1616
2046
  ${mdMount}
1617
2047
  </script>
1618
2048
  </body>
@@ -1625,63 +2055,44 @@ function mockFilesForTests() {
1625
2055
  name: "mock-files-for-tests",
1626
2056
  enforce: "pre",
1627
2057
  resolveId(id) {
1628
- if (mockFileMap[id.replace(grapheneRoot, "")]) return id + "?mock";
2058
+ if (mockFileMap[id.replace(config.root + "/", "")]) return id + "?mock";
1629
2059
  },
1630
2060
  load(id) {
1631
2061
  if (!id.endsWith("?mock")) return null;
1632
- return mockFileMap[id.replace(grapheneRoot, "").replace(/\?mock$/, "")];
2062
+ return mockFileMap[id.replace(config.root + "/", "").replace(/\?mock$/, "")];
1633
2063
  }
1634
2064
  };
1635
2065
  }
1636
- var grapheneRoot, uiRoot, workspaceLoadPromise, updateWorkspacePlugin, handleRequestPlugin, browserConnections, viewRequests, mockFileMap;
2066
+ var uiRoot, workspaceLoadPromise, updateWorkspacePlugin, handleRequestPlugin;
1637
2067
  var init_serve2 = __esm({
1638
2068
  "serve2.ts"() {
1639
2069
  init_core();
1640
2070
  init_connections();
1641
2071
  init_mdCompile();
2072
+ init_check();
2073
+ init_mockFiles();
1642
2074
  updateWorkspacePlugin = {
1643
2075
  name: "updateWorkspace",
1644
2076
  configureServer: (s) => {
1645
2077
  s.watcher.add("**/*.gsql");
1646
2078
  s.watcher.on("change", () => {
1647
2079
  clearWorkspace();
1648
- workspaceLoadPromise = loadWorkspace(grapheneRoot, false);
2080
+ workspaceLoadPromise = loadWorkspace(config.root, false);
1649
2081
  });
1650
- workspaceLoadPromise = loadWorkspace(grapheneRoot, false);
2082
+ workspaceLoadPromise = loadWorkspace(config.root, false);
1651
2083
  }
1652
2084
  };
1653
2085
  handleRequestPlugin = {
1654
2086
  name: "handleRequest",
1655
2087
  configureServer: (s) => {
1656
- let wss = new WebSocketServer({ noServer: true });
1657
- s.httpServer.on("upgrade", (req, socket, head) => {
1658
- if (!req.url?.endsWith("/graphene-ws")) return;
1659
- wss.handleUpgrade(req, socket, head, (ws) => {
1660
- wss.emit("connection", ws, req);
1661
- });
1662
- });
1663
- wss.on("connection", (socket) => {
1664
- socket.on("message", (data) => {
1665
- let message = JSON.parse(data.toString());
1666
- if (message.type === "register") {
1667
- browserConnections.push({ url: message.url, socket });
1668
- }
1669
- if (message.type === "viewResponse") {
1670
- viewRequests[message.requestId].response.end(JSON.stringify(message));
1671
- delete viewRequests[message.requestId];
1672
- }
1673
- });
1674
- socket.on("close", () => browserConnections = browserConnections.filter((conn) => conn.socket !== socket));
1675
- });
1676
2088
  s.middlewares.use(async function handleRequest(req, res, next) {
1677
2089
  try {
1678
2090
  let [pathName] = (req.url || "").split("?");
1679
2091
  if (pathName == "/_api/query") return await handleQuery(req, res);
1680
- if (pathName == "/graphene/view") return await handleView(req, res);
1681
2092
  if (pathName == "/__ct") return await handlePage(s, res, "__ct", false);
1682
2093
  if (!pathName || pathName == "/") pathName = "index";
1683
- let mdPath = path6.join(grapheneRoot, pathName + ".md");
1684
- if (await fs5.exists(mdPath)) {
2094
+ let mdPath = path7.join(config.root, pathName + ".md");
2095
+ if (await fs6.exists(mdPath)) {
1685
2096
  await handlePage(s, res, mdPath, true);
1686
2097
  } else {
1687
2098
  next();
@@ -1694,166 +2105,19 @@ var init_serve2 = __esm({
1694
2105
  });
1695
2106
  }
1696
2107
  };
1697
- browserConnections = [];
1698
- viewRequests = {};
1699
- mockFileMap = {};
1700
2108
  }
1701
2109
  });
1702
2110
 
1703
2111
  // cli.ts
1704
- import { Command } from "commander";
1705
-
1706
- // printer.ts
1707
- init_core();
1708
- import { styleText as nodeStyleText } from "node:util";
1709
- import Table from "cli-table3";
1710
- import chalk from "chalk";
1711
- var styleText = (style, text) => {
1712
- try {
1713
- return nodeStyleText ? nodeStyleText(style, text) : text;
1714
- } catch {
1715
- return text;
1716
- }
1717
- };
1718
- function offsetToLineCol(src, offset2) {
1719
- let lines = src.split(/\r?\n/);
1720
- let acc = 0;
1721
- for (let i = 0; i < lines.length; i++) {
1722
- let lineText = lines[i];
1723
- let nextAcc = acc + lineText.length + 1;
1724
- if (offset2 < nextAcc || i === lines.length - 1) {
1725
- let col = Math.max(0, offset2 - acc);
1726
- return { line: i + 1, col, lineStart: acc, lineText };
1727
- }
1728
- acc = nextAcc;
1729
- }
1730
- return { line: 1, col: 0, lineStart: 0, lineText: lines[0] || "" };
1731
- }
1732
- function printDiagnostics(diags) {
1733
- let parts = [];
1734
- for (let d of diags) {
1735
- let src = getFile2(d.file)?.contents || "";
1736
- let { line, col, lineStart, lineText } = offsetToLineCol(src, d.from.offset);
1737
- let endCol = Math.max(col + 1, Math.min(lineText.length, d.to.offset - lineStart));
1738
- let caretLen = Math.max(1, endCol - col);
1739
- let sev = d.severity === "error" ? "red" : "yellow";
1740
- let header = `${styleText(sev, d.severity.toUpperCase())}: ${d.file} line ${line}: ${d.message}`;
1741
- let gutter = " | ";
1742
- let caretLine = `${" ".repeat(col)}${styleText(sev, "^".repeat(caretLen))}`;
1743
- parts.push([header, `${gutter}${lineText}`, `${gutter}${caretLine}`].join("\n"));
1744
- }
1745
- if (parts.length) console.error(parts.join("\n"));
1746
- }
1747
- function printTable(rows) {
1748
- if (!rows || rows.length === 0) {
1749
- console.log(chalk.yellow("No results returned"));
1750
- return;
1751
- }
1752
- let headers = Object.keys(rows[0]);
1753
- let table2 = new Table({ head: headers.map((h) => chalk.blue(h)) });
1754
- let MAX_DISPLAY_ROWS = 200;
1755
- let displayRows = rows.slice(0, MAX_DISPLAY_ROWS);
1756
- displayRows.forEach((row) => table2.push(headers.map((h) => row[h]?.toString() || "")));
1757
- console.log(table2.toString());
1758
- if (rows.length > MAX_DISPLAY_ROWS) {
1759
- console.log(chalk.yellow(`Displayed first ${MAX_DISPLAY_ROWS} rows (of ${rows.length} total).`));
1760
- }
1761
- }
1762
-
1763
- // cli.ts
2112
+ init_printer();
1764
2113
  init_core();
1765
2114
  init_config();
1766
- import fs6 from "fs-extra";
1767
- import path7 from "path";
1768
- import os from "os";
1769
-
1770
- // background.ts
1771
- import { spawn } from "child_process";
1772
- import { fileURLToPath } from "url";
1773
- import fs2 from "fs-extra";
1774
- import path3 from "path";
1775
- async function runServeInBackground() {
1776
- let root = process.cwd();
1777
- let grapheneCache = getGrapheneCache(root);
1778
- let logFile = path3.join(grapheneCache, "serve.log");
1779
- await fs2.ensureDir(grapheneCache);
1780
- await stopGrapheneIfRunning(root);
1781
- let log = fs2.openSync(logFile, "w");
1782
- let entryPoint = process.argv[1] || fileURLToPath(import.meta.url);
1783
- let childArgs = [...process.execArgv, entryPoint, "serve", "--fg", ...process.argv.slice(3)];
1784
- let child = spawn(process.execPath, childArgs, {
1785
- cwd: root,
1786
- detached: true,
1787
- env: { ...process.env },
1788
- stdio: ["ignore", log, log]
1789
- });
1790
- if (!child.pid) throw new Error("Failed to start server process");
1791
- await new Promise((resolve, reject) => {
1792
- let buffer = "";
1793
- fs2.watchFile(logFile, { interval: 200 }, (curr, prev) => {
1794
- if (curr.size > prev.size) {
1795
- let stream = fs2.createReadStream(logFile, { start: 0, end: curr.size - 1 });
1796
- stream.on("data", (d) => {
1797
- process.stdout.write(d);
1798
- buffer = (buffer + d.toString()).slice(-200);
1799
- if (buffer.includes("Server running at http://localhost:")) resolve();
1800
- });
1801
- }
1802
- });
1803
- child.once("exit", () => {
1804
- process.stdout.write(fs2.readFileSync(logFile));
1805
- reject(new Error("Exited before server started"));
1806
- });
1807
- child.once("error", (e) => reject(e));
1808
- });
1809
- }
1810
- function getGrapheneCache(root) {
1811
- return path3.join(root, "node_modules", ".graphene");
1812
- }
1813
- function getPidFilePath(root) {
1814
- return path3.join(getGrapheneCache(root), process.env.NODE_ENV == "test" ? "test.pid" : "serve.pid");
1815
- }
1816
- async function stopGrapheneIfRunning(root) {
1817
- let pidFile = getPidFilePath(root);
1818
- let pid = await readPid(pidFile);
1819
- if (!pid) return true;
1820
- if (!isProcessRunning(pid)) {
1821
- await fs2.remove(pidFile);
1822
- return true;
1823
- }
1824
- try {
1825
- console.log(`Stopping server (${pid})`);
1826
- process.kill(pid, "SIGTERM");
1827
- } catch (err) {
1828
- if (err.code === "ESRCH") return true;
1829
- return false;
1830
- }
1831
- let end = Date.now() + 5e3;
1832
- while (Date.now() < end && isProcessRunning(pid)) {
1833
- await new Promise((resolve) => setTimeout(resolve, 100));
1834
- }
1835
- await fs2.remove(pidFile);
1836
- return !isProcessRunning(pid);
1837
- }
1838
- async function readPid(pidFile) {
1839
- if (!await fs2.pathExists(pidFile)) return void 0;
1840
- let contents = (await fs2.readFile(pidFile, "utf8")).trim();
1841
- if (!contents) return void 0;
1842
- let pid = Number.parseInt(contents, 10);
1843
- if (Number.isNaN(pid)) return void 0;
1844
- return pid;
1845
- }
1846
- function isProcessRunning(pid) {
1847
- try {
1848
- process.kill(pid, 0);
1849
- return true;
1850
- } catch {
1851
- return false;
1852
- }
1853
- }
1854
-
1855
- // cli.ts
2115
+ init_background();
1856
2116
  init_connections();
2117
+ init_check();
2118
+ import { Command } from "commander";
2119
+ import fs7 from "fs-extra";
2120
+ import path8 from "path";
1857
2121
  var program = new Command();
1858
2122
  program.name("graphene").description("Graphene CLI").version("1.0.0");
1859
2123
  program.hook("preAction", async () => {
@@ -1891,37 +2155,9 @@ program.command("serve").description("Run the local server").option("--fg", "Run
1891
2155
  program.command("stop").description("Stop the local server").action(async () => {
1892
2156
  await stopGrapheneIfRunning(process.cwd());
1893
2157
  });
1894
- program.command("check").description("Check the project for errors").action(async () => {
1895
- await loadWorkspace(process.cwd(), true);
1896
- analyze();
1897
- if (getDiagnostics().length) {
1898
- printDiagnostics(getDiagnostics());
1899
- process.exit(1);
1900
- }
1901
- console.log("No errors found \u{1F48E}");
1902
- });
1903
- program.command("view").description("Capture a screenshot of a rendered markdown file").argument("<mdFile>", "Markdown file to view (e.g., index.md)").option("-c, --chart <chartName>", "Name of specific chart to capture").action(async (mdFile, options) => {
1904
- let response = await fetch("http://localhost:4000/graphene/view", {
1905
- method: "POST",
1906
- headers: { "Content-Type": "application/json" },
1907
- body: JSON.stringify({ mdFile, chart: options.chart })
1908
- });
1909
- if (!response.ok) throw new Error(`View request failed: ${await response.text()}`);
1910
- let result = await response.json();
1911
- if (result.errors && result.errors.length > 0) {
1912
- console.error("Errors found:");
1913
- result.errors.forEach((error) => console.error(JSON.stringify(error)));
1914
- }
1915
- if (result.stillLoading) {
1916
- console.error("Warning: Queries were still loading when the screenshot was taken");
1917
- }
1918
- if (result.screenshot) {
1919
- let filename = `graphene-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`;
1920
- let screenshotPath = path7.join(os.tmpdir(), filename);
1921
- let base64Data = result.screenshot.replace(/^data:image\/png;base64,/, "");
1922
- await fs6.writeFile(screenshotPath, base64Data, "base64");
1923
- console.log("Screenshot saved to", screenshotPath);
1924
- }
2158
+ program.command("check").description("Check the project for errors, optionally capturing a page screenshot").argument("[mdFile]", "Markdown file to check (e.g., index.md)").option("-c, --chart <chartTitle>", "Title of a specific chart to capture").action(async (mdArg, options) => {
2159
+ let res = await check({ mdArg, chart: options.chart });
2160
+ process.exit(res ? 0 : 1);
1925
2161
  });
1926
2162
  program.parse(process.argv);
1927
2163
  async function readInput(arg) {
@@ -1934,9 +2170,9 @@ async function readInput(arg) {
1934
2170
  process.stdin.resume();
1935
2171
  });
1936
2172
  }
1937
- let absolutePath = path7.resolve(arg);
1938
- if (fs6.existsSync(absolutePath)) {
1939
- return await fs6.promises.readFile(absolutePath, "utf-8");
2173
+ let absolutePath = path8.resolve(arg);
2174
+ if (fs7.existsSync(absolutePath)) {
2175
+ return await fs7.promises.readFile(absolutePath, "utf-8");
1940
2176
  }
1941
2177
  return arg;
1942
2178
  }