@graphenedata/cli 0.0.3 → 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 +509 -277
  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";
@@ -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":
@@ -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,13 +1297,177 @@ 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, {
1287
1469
  BigQueryConnection: () => BigQueryConnection
1288
1470
  });
1289
- import fs3 from "fs";
1290
- import path4 from "path";
1291
1471
  import { BigQuery, BigQueryDate, BigQueryTimestamp } from "@google-cloud/bigquery";
1292
1472
  var BigQueryConnection;
1293
1473
  var init_bigQuery = __esm({
@@ -1296,18 +1476,14 @@ var init_bigQuery = __esm({
1296
1476
  BigQueryConnection = class {
1297
1477
  client;
1298
1478
  constructor(options = {}) {
1479
+ options.projectId ||= config.bigquery?.projectId;
1299
1480
  if (process.env.GOOGLE_CREDENTIALS_CONTENT) {
1300
1481
  let parsed = JSON.parse(process.env.GOOGLE_CREDENTIALS_CONTENT);
1301
- let credPath = path4.resolve("./bq.json");
1302
- fs3.writeFileSync("./bq.json", process.env.GOOGLE_CREDENTIALS_CONTENT.replace(" ", "\n "));
1303
- process.env.GOOGLE_APPLICATION_CREDENTIALS = credPath;
1304
1482
  options.projectId = parsed.project_id;
1483
+ options.credentials = parsed;
1305
1484
  }
1306
- options.projectId ||= config.googleProjectId;
1307
- options.maxRetries ||= 3;
1308
- options.userAgent ||= "Graphene";
1309
- if (!options.projectId) throw new Error("googleProjectId must be set in config or provided in service account credentials");
1310
- 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" });
1311
1487
  }
1312
1488
  async runQuery(sql) {
1313
1489
  let [job] = await this.client.createQueryJob({ query: sql, useLegacySql: false });
@@ -1331,27 +1507,32 @@ var duckdb_exports = {};
1331
1507
  __export(duckdb_exports, {
1332
1508
  DuckDBConnection: () => DuckDBConnection
1333
1509
  });
1334
- import { promises as fs4 } from "fs";
1335
- import path5 from "path";
1510
+ import { promises as fs3 } from "fs";
1511
+ import path4 from "path";
1336
1512
  import { DuckDBTimestampValue, DuckDBInstance, DuckDBDateValue } from "@duckdb/node-api";
1337
1513
  var DuckDBConnection;
1338
1514
  var init_duckdb = __esm({
1339
1515
  "connections/duckdb.ts"() {
1340
1516
  init_config();
1341
1517
  DuckDBConnection = class {
1518
+ options;
1342
1519
  ready;
1343
1520
  connection = null;
1344
- constructor() {
1521
+ constructor(options) {
1522
+ this.options = options || {};
1345
1523
  this.ready = this.initialize();
1346
1524
  }
1347
1525
  async initialize() {
1348
- let files = await fs4.readdir(config.root);
1349
- let databasePath = files.find((f) => f.endsWith(".duckdb"));
1350
- if (!databasePath) throw new Error("No .duckdb file found in current directory");
1351
- databasePath = path5.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
+ }
1352
1533
  let db = await DuckDBInstance.create(":memory:");
1353
1534
  this.connection = await db.connect();
1354
- let escapedPath = databasePath.replace(/'/g, "''");
1535
+ let escapedPath = dbPath.replace(/'/g, "''");
1355
1536
  await this.connection.run(`attach '${escapedPath}' as graphene_cli (READ_ONLY);`);
1356
1537
  await this.connection.run("use graphene_cli;");
1357
1538
  }
@@ -1376,6 +1557,76 @@ var init_duckdb = __esm({
1376
1557
  }
1377
1558
  });
1378
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
+
1379
1630
  // connections/index.ts
1380
1631
  async function getConnection() {
1381
1632
  if (config.dialect === "bigquery") {
@@ -1383,7 +1634,10 @@ async function getConnection() {
1383
1634
  return new mod.BigQueryConnection();
1384
1635
  } else if (config.dialect === "duckdb") {
1385
1636
  let mod = await Promise.resolve().then(() => (init_duckdb(), duckdb_exports));
1386
- 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({});
1387
1641
  } else {
1388
1642
  throw new Error(`Unsupported dialect: ${config.dialect}`);
1389
1643
  }
@@ -1394,6 +1648,201 @@ var init_connections = __esm({
1394
1648
  }
1395
1649
  });
1396
1650
 
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";
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
+
1397
1846
  // mdCompile.ts
1398
1847
  import fs5 from "fs";
1399
1848
  import path6 from "path";
@@ -1487,7 +1936,6 @@ var init_mdCompile = __esm({
1487
1936
  // serve2.ts
1488
1937
  var serve2_exports = {};
1489
1938
  __export(serve2_exports, {
1490
- mockFileMap: () => mockFileMap,
1491
1939
  serve2: () => serve2
1492
1940
  });
1493
1941
  import { createServer, optimizeDeps } from "vite";
@@ -1497,13 +1945,11 @@ import crypto from "crypto";
1497
1945
  import { mdsvex } from "mdsvex";
1498
1946
  import path7 from "path";
1499
1947
  import { fileURLToPath as fileURLToPath2 } from "url";
1500
- import { WebSocketServer } from "ws";
1501
- import { spawn as spawn2 } from "child_process";
1502
1948
  async function serve2() {
1503
- grapheneRoot = config.root;
1504
1949
  uiRoot = path7.join(fileURLToPath2(import.meta.url), "../../ui");
1505
- await fs6.ensureDir(path7.resolve(grapheneRoot, "node_modules/.graphene"));
1506
- await fs6.writeFile(path7.resolve(grapheneRoot, `node_modules/.graphene/${process.env.NODE_ENV == "test" ? "test" : "serve"}.pid`), String(process.pid));
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));
1507
1953
  let server = await createServer({
1508
1954
  root: config.root,
1509
1955
  plugins: [
@@ -1519,12 +1965,14 @@ async function serve2() {
1519
1965
  injectComponentImports()
1520
1966
  ]
1521
1967
  }),
1968
+ checkVitePlugin(),
1522
1969
  handleRequestPlugin,
1523
1970
  updateWorkspacePlugin,
1524
1971
  mockFilesForTests()
1525
1972
  ],
1973
+ publicDir: path7.resolve(uiRoot),
1526
1974
  server: {
1527
- port: config.port,
1975
+ port,
1528
1976
  fs: { strict: false },
1529
1977
  strictPort: true
1530
1978
  },
@@ -1533,10 +1981,16 @@ async function serve2() {
1533
1981
  graphene: path7.resolve(uiRoot, "web.js")
1534
1982
  }
1535
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
+ // },
1536
1988
  });
1537
1989
  await optimizeDeps(server.config);
1538
1990
  await server.listen();
1539
- 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
+ }
1540
1994
  return server;
1541
1995
  }
1542
1996
  async function handleQuery(req, res) {
@@ -1566,31 +2020,6 @@ async function handleQuery(req, res) {
1566
2020
  let fields = queries[0].fields.map((f) => ({ name: f.name, type: f.type }));
1567
2021
  res.end(JSON.stringify({ rows: queryResults.rows, hash, fields, sql }));
1568
2022
  }
1569
- async function handleView(req, res) {
1570
- let chunks = [];
1571
- for await (let chunk of req) chunks.push(chunk);
1572
- let { mdFile, chart } = JSON.parse(Buffer.concat(chunks).toString());
1573
- let id = Math.random().toString(36).slice(2);
1574
- res.setHeader("Content-Type", "application/json");
1575
- viewRequests[id] = { response: res };
1576
- let pageUrl = "/" + mdFile.replace(/\.md$/, "").replace(/^\//, "");
1577
- if (pageUrl === "/index") pageUrl = "/";
1578
- pageUrl = `http://localhost:${config.port || 4e3}${pageUrl}`;
1579
- let conn = browserConnections.find((conn2) => conn2.url === pageUrl);
1580
- if (!conn) {
1581
- spawn2("open", [pageUrl]);
1582
- let end = Date.now() + 5e3;
1583
- while (Date.now() < end && !conn) {
1584
- conn = browserConnections.find((conn2) => conn2.url === pageUrl);
1585
- await new Promise((resolve) => setTimeout(resolve, 100));
1586
- }
1587
- if (!conn) {
1588
- res.statusCode = 500;
1589
- return res.end(JSON.stringify({ error: "No browser tab available and failed to open one" }));
1590
- }
1591
- }
1592
- conn.socket.send(JSON.stringify({ type: "view", chart, requestId: id }));
1593
- }
1594
2023
  async function handlePage(server, res, filePath, mount) {
1595
2024
  res.setHeader("Content-Type", "text/html");
1596
2025
  let mdMount = mount ? `
@@ -1603,7 +2032,7 @@ async function handlePage(server, res, filePath, mount) {
1603
2032
  <meta charset="UTF-8" />
1604
2033
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1605
2034
  <title>Graphene</title>
1606
- <link rel="icon" href="@graphenedata/ui/assets/favicon.ico" />
2035
+ <link rel="icon" href="/assets/favicon.ico" />
1607
2036
  <link rel="preconnect" href="https://fonts.googleapis.com">
1608
2037
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
1609
2038
  <link href="https://fonts.googleapis.com/css2?family=Inter:wght@100..900&display=swap" rel="stylesheet">
@@ -1613,10 +2042,7 @@ async function handlePage(server, res, filePath, mount) {
1613
2042
  <div id="content"></div>
1614
2043
  </main>
1615
2044
  <script type="module">
1616
- // do this first so we can track errors caused by importing the md file
1617
- import 'graphene'
1618
- </script>
1619
- <script type="module">
2045
+ import 'graphene' // do this first so we can track errors caused by importing the md file
1620
2046
  ${mdMount}
1621
2047
  </script>
1622
2048
  </body>
@@ -1629,62 +2055,43 @@ function mockFilesForTests() {
1629
2055
  name: "mock-files-for-tests",
1630
2056
  enforce: "pre",
1631
2057
  resolveId(id) {
1632
- if (mockFileMap[id.replace(grapheneRoot, "")]) return id + "?mock";
2058
+ if (mockFileMap[id.replace(config.root + "/", "")]) return id + "?mock";
1633
2059
  },
1634
2060
  load(id) {
1635
2061
  if (!id.endsWith("?mock")) return null;
1636
- return mockFileMap[id.replace(grapheneRoot, "").replace(/\?mock$/, "")];
2062
+ return mockFileMap[id.replace(config.root + "/", "").replace(/\?mock$/, "")];
1637
2063
  }
1638
2064
  };
1639
2065
  }
1640
- var grapheneRoot, uiRoot, workspaceLoadPromise, updateWorkspacePlugin, handleRequestPlugin, browserConnections, viewRequests, mockFileMap;
2066
+ var uiRoot, workspaceLoadPromise, updateWorkspacePlugin, handleRequestPlugin;
1641
2067
  var init_serve2 = __esm({
1642
2068
  "serve2.ts"() {
1643
2069
  init_core();
1644
2070
  init_connections();
1645
2071
  init_mdCompile();
2072
+ init_check();
2073
+ init_mockFiles();
1646
2074
  updateWorkspacePlugin = {
1647
2075
  name: "updateWorkspace",
1648
2076
  configureServer: (s) => {
1649
2077
  s.watcher.add("**/*.gsql");
1650
2078
  s.watcher.on("change", () => {
1651
2079
  clearWorkspace();
1652
- workspaceLoadPromise = loadWorkspace(grapheneRoot, false);
2080
+ workspaceLoadPromise = loadWorkspace(config.root, false);
1653
2081
  });
1654
- workspaceLoadPromise = loadWorkspace(grapheneRoot, false);
2082
+ workspaceLoadPromise = loadWorkspace(config.root, false);
1655
2083
  }
1656
2084
  };
1657
2085
  handleRequestPlugin = {
1658
2086
  name: "handleRequest",
1659
2087
  configureServer: (s) => {
1660
- let wss = new WebSocketServer({ noServer: true });
1661
- s.httpServer.on("upgrade", (req, socket, head) => {
1662
- if (!req.url?.endsWith("/graphene-ws")) return;
1663
- wss.handleUpgrade(req, socket, head, (ws) => {
1664
- wss.emit("connection", ws, req);
1665
- });
1666
- });
1667
- wss.on("connection", (socket) => {
1668
- socket.on("message", (data) => {
1669
- let message = JSON.parse(data.toString());
1670
- if (message.type === "register") {
1671
- browserConnections.push({ url: message.url, socket });
1672
- }
1673
- if (message.type === "viewResponse") {
1674
- viewRequests[message.requestId].response.end(JSON.stringify(message));
1675
- delete viewRequests[message.requestId];
1676
- }
1677
- });
1678
- socket.on("close", () => browserConnections = browserConnections.filter((conn) => conn.socket !== socket));
1679
- });
1680
2088
  s.middlewares.use(async function handleRequest(req, res, next) {
1681
2089
  try {
1682
2090
  let [pathName] = (req.url || "").split("?");
1683
2091
  if (pathName == "/_api/query") return await handleQuery(req, res);
1684
- if (pathName == "/graphene/view") return await handleView(req, res);
1685
2092
  if (pathName == "/__ct") return await handlePage(s, res, "__ct", false);
1686
2093
  if (!pathName || pathName == "/") pathName = "index";
1687
- let mdPath = path7.join(grapheneRoot, pathName + ".md");
2094
+ let mdPath = path7.join(config.root, pathName + ".md");
1688
2095
  if (await fs6.exists(mdPath)) {
1689
2096
  await handlePage(s, res, mdPath, true);
1690
2097
  } else {
@@ -1698,166 +2105,19 @@ var init_serve2 = __esm({
1698
2105
  });
1699
2106
  }
1700
2107
  };
1701
- browserConnections = [];
1702
- viewRequests = {};
1703
- mockFileMap = {};
1704
2108
  }
1705
2109
  });
1706
2110
 
1707
2111
  // cli.ts
1708
- import { Command } from "commander";
1709
-
1710
- // printer.ts
1711
- init_core();
1712
- import { styleText as nodeStyleText } from "node:util";
1713
- import Table from "cli-table3";
1714
- import chalk from "chalk";
1715
- var styleText = (style, text) => {
1716
- try {
1717
- return nodeStyleText ? nodeStyleText(style, text) : text;
1718
- } catch {
1719
- return text;
1720
- }
1721
- };
1722
- function offsetToLineCol(src, offset2) {
1723
- let lines = src.split(/\r?\n/);
1724
- let acc = 0;
1725
- for (let i = 0; i < lines.length; i++) {
1726
- let lineText = lines[i];
1727
- let nextAcc = acc + lineText.length + 1;
1728
- if (offset2 < nextAcc || i === lines.length - 1) {
1729
- let col = Math.max(0, offset2 - acc);
1730
- return { line: i + 1, col, lineStart: acc, lineText };
1731
- }
1732
- acc = nextAcc;
1733
- }
1734
- return { line: 1, col: 0, lineStart: 0, lineText: lines[0] || "" };
1735
- }
1736
- function printDiagnostics(diags) {
1737
- let parts = [];
1738
- for (let d of diags) {
1739
- let src = getFile2(d.file)?.contents || "";
1740
- let { line, col, lineStart, lineText } = offsetToLineCol(src, d.from.offset);
1741
- let endCol = Math.max(col + 1, Math.min(lineText.length, d.to.offset - lineStart));
1742
- let caretLen = Math.max(1, endCol - col);
1743
- let sev = d.severity === "error" ? "red" : "yellow";
1744
- let header = `${styleText(sev, d.severity.toUpperCase())}: ${d.file} line ${line}: ${d.message}`;
1745
- let gutter = " | ";
1746
- let caretLine = `${" ".repeat(col)}${styleText(sev, "^".repeat(caretLen))}`;
1747
- parts.push([header, `${gutter}${lineText}`, `${gutter}${caretLine}`].join("\n"));
1748
- }
1749
- if (parts.length) console.error(parts.join("\n"));
1750
- }
1751
- function printTable(rows) {
1752
- if (!rows || rows.length === 0) {
1753
- console.log(chalk.yellow("No results returned"));
1754
- return;
1755
- }
1756
- let headers = Object.keys(rows[0]);
1757
- let table2 = new Table({ head: headers.map((h) => chalk.blue(h)) });
1758
- let MAX_DISPLAY_ROWS = 200;
1759
- let displayRows = rows.slice(0, MAX_DISPLAY_ROWS);
1760
- displayRows.forEach((row) => table2.push(headers.map((h) => row[h]?.toString() || "")));
1761
- console.log(table2.toString());
1762
- if (rows.length > MAX_DISPLAY_ROWS) {
1763
- console.log(chalk.yellow(`Displayed first ${MAX_DISPLAY_ROWS} rows (of ${rows.length} total).`));
1764
- }
1765
- }
1766
-
1767
- // cli.ts
2112
+ init_printer();
1768
2113
  init_core();
1769
2114
  init_config();
2115
+ init_background();
2116
+ init_connections();
2117
+ init_check();
2118
+ import { Command } from "commander";
1770
2119
  import fs7 from "fs-extra";
1771
2120
  import path8 from "path";
1772
- import os from "os";
1773
-
1774
- // background.ts
1775
- import { spawn } from "child_process";
1776
- import { fileURLToPath } from "url";
1777
- import fs2 from "fs-extra";
1778
- import path3 from "path";
1779
- async function runServeInBackground() {
1780
- let root = process.cwd();
1781
- let grapheneCache = getGrapheneCache(root);
1782
- let logFile = path3.join(grapheneCache, "serve.log");
1783
- await fs2.ensureDir(grapheneCache);
1784
- await stopGrapheneIfRunning(root);
1785
- let log = fs2.openSync(logFile, "w");
1786
- let entryPoint = process.argv[1] || fileURLToPath(import.meta.url);
1787
- let childArgs = [...process.execArgv, entryPoint, "serve", "--fg", ...process.argv.slice(3)];
1788
- let child = spawn(process.execPath, childArgs, {
1789
- cwd: root,
1790
- detached: true,
1791
- env: { ...process.env },
1792
- stdio: ["ignore", log, log]
1793
- });
1794
- if (!child.pid) throw new Error("Failed to start server process");
1795
- await new Promise((resolve, reject) => {
1796
- let buffer = "";
1797
- fs2.watchFile(logFile, { interval: 200 }, (curr, prev) => {
1798
- if (curr.size > prev.size) {
1799
- let stream = fs2.createReadStream(logFile, { start: 0, end: curr.size - 1 });
1800
- stream.on("data", (d) => {
1801
- process.stdout.write(d);
1802
- buffer = (buffer + d.toString()).slice(-200);
1803
- if (buffer.includes("Server running at http://localhost:")) resolve();
1804
- });
1805
- }
1806
- });
1807
- child.once("exit", () => {
1808
- process.stdout.write(fs2.readFileSync(logFile));
1809
- reject(new Error("Exited before server started"));
1810
- });
1811
- child.once("error", (e) => reject(e));
1812
- });
1813
- }
1814
- function getGrapheneCache(root) {
1815
- return path3.join(root, "node_modules", ".graphene");
1816
- }
1817
- function getPidFilePath(root) {
1818
- return path3.join(getGrapheneCache(root), process.env.NODE_ENV == "test" ? "test.pid" : "serve.pid");
1819
- }
1820
- async function stopGrapheneIfRunning(root) {
1821
- let pidFile = getPidFilePath(root);
1822
- let pid = await readPid(pidFile);
1823
- if (!pid) return true;
1824
- if (!isProcessRunning(pid)) {
1825
- await fs2.remove(pidFile);
1826
- return true;
1827
- }
1828
- try {
1829
- console.log(`Stopping server (${pid})`);
1830
- process.kill(pid, "SIGTERM");
1831
- } catch (err) {
1832
- if (err.code === "ESRCH") return true;
1833
- return false;
1834
- }
1835
- let end = Date.now() + 5e3;
1836
- while (Date.now() < end && isProcessRunning(pid)) {
1837
- await new Promise((resolve) => setTimeout(resolve, 100));
1838
- }
1839
- await fs2.remove(pidFile);
1840
- return !isProcessRunning(pid);
1841
- }
1842
- async function readPid(pidFile) {
1843
- if (!await fs2.pathExists(pidFile)) return void 0;
1844
- let contents = (await fs2.readFile(pidFile, "utf8")).trim();
1845
- if (!contents) return void 0;
1846
- let pid = Number.parseInt(contents, 10);
1847
- if (Number.isNaN(pid)) return void 0;
1848
- return pid;
1849
- }
1850
- function isProcessRunning(pid) {
1851
- try {
1852
- process.kill(pid, 0);
1853
- return true;
1854
- } catch {
1855
- return false;
1856
- }
1857
- }
1858
-
1859
- // cli.ts
1860
- init_connections();
1861
2121
  var program = new Command();
1862
2122
  program.name("graphene").description("Graphene CLI").version("1.0.0");
1863
2123
  program.hook("preAction", async () => {
@@ -1895,37 +2155,9 @@ program.command("serve").description("Run the local server").option("--fg", "Run
1895
2155
  program.command("stop").description("Stop the local server").action(async () => {
1896
2156
  await stopGrapheneIfRunning(process.cwd());
1897
2157
  });
1898
- program.command("check").description("Check the project for errors").action(async () => {
1899
- await loadWorkspace(process.cwd(), true);
1900
- analyze();
1901
- if (getDiagnostics().length) {
1902
- printDiagnostics(getDiagnostics());
1903
- process.exit(1);
1904
- }
1905
- console.log("No errors found \u{1F48E}");
1906
- });
1907
- 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) => {
1908
- let response = await fetch("http://localhost:4000/graphene/view", {
1909
- method: "POST",
1910
- headers: { "Content-Type": "application/json" },
1911
- body: JSON.stringify({ mdFile, chart: options.chart })
1912
- });
1913
- if (!response.ok) throw new Error(`View request failed: ${await response.text()}`);
1914
- let result = await response.json();
1915
- if (result.errors && result.errors.length > 0) {
1916
- console.error("Errors found:");
1917
- result.errors.forEach((error) => console.error(JSON.stringify(error)));
1918
- }
1919
- if (result.stillLoading) {
1920
- console.error("Warning: Queries were still loading when the screenshot was taken");
1921
- }
1922
- if (result.screenshot) {
1923
- let filename = `graphene-screenshot-${(/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-")}.png`;
1924
- let screenshotPath = path8.join(os.tmpdir(), filename);
1925
- let base64Data = result.screenshot.replace(/^data:image\/png;base64,/, "");
1926
- await fs7.writeFile(screenshotPath, base64Data, "base64");
1927
- console.log("Screenshot saved to", screenshotPath);
1928
- }
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);
1929
2161
  });
1930
2162
  program.parse(process.argv);
1931
2163
  async function readInput(arg) {