create-graphene 0.0.16 → 0.0.17

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 (2) hide show
  1. package/dist/cli.js +229 -70
  2. package/package.json +5 -2
package/dist/cli.js CHANGED
@@ -6,14 +6,19 @@ import * as clack from "@clack/prompts";
6
6
  import { spawn } from "node:child_process";
7
7
  import { access, mkdir, readFile, readlink, readdir, symlink, writeFile } from "node:fs/promises";
8
8
  import path2 from "node:path";
9
+ import { styleText } from "node:util";
9
10
 
10
11
  // ../cli/package.json
11
12
  var package_default = {
12
13
  name: "@graphenedata/cli",
13
- version: "0.0.16",
14
+ version: "0.0.17",
14
15
  private: true,
15
16
  license: "Elastic-2.0",
16
17
  author: "Graphene Systems Inc",
18
+ repository: {
19
+ type: "git",
20
+ url: "git+https://github.com/graphene-data/graphene.git"
21
+ },
17
22
  bin: {
18
23
  graphene: "./bin.js"
19
24
  },
@@ -113,7 +118,7 @@ var CreateCancelled = class extends Error {
113
118
  }
114
119
  };
115
120
  function parseArgs(argv) {
116
- let options = { yes: false, install: true, name: void 0, targetDir: void 0, help: false };
121
+ let options = { yes: false, install: true, skipCredentialValidation: false, name: void 0, targetDir: void 0, help: false };
117
122
  for (let i = 0; i < argv.length; i++) {
118
123
  let arg = argv[i];
119
124
  if (arg === "--yes" || arg === "-y") {
@@ -124,6 +129,10 @@ function parseArgs(argv) {
124
129
  options.install = false;
125
130
  continue;
126
131
  }
132
+ if (arg === "--skip-credential-validation") {
133
+ options.skipCredentialValidation = true;
134
+ continue;
135
+ }
127
136
  if (arg === "--help" || arg === "-h") {
128
137
  options.help = true;
129
138
  continue;
@@ -167,8 +176,10 @@ function renderTemplate({ answers, cliVersion }) {
167
176
  if (answers.duckdbPath) graphene.duckdb = { path: answers.duckdbPath };
168
177
  } else if (answers.database === "snowflake") {
169
178
  graphene.snowflake = { account: answers.snowflakeAccount || "", username: answers.snowflakeUsername || "" };
170
- } else {
179
+ } else if (answers.database === "bigquery") {
171
180
  graphene.bigquery = { projectId: answers.bigqueryProjectId || "" };
181
+ } else {
182
+ graphene.clickhouse = { url: answers.clickhouseUrl || "", username: answers.clickhouseUsername || "" };
172
183
  }
173
184
  let pkg = {
174
185
  name: answers.projectName,
@@ -191,7 +202,7 @@ function renderTemplate({ answers, cliVersion }) {
191
202
  let files = {
192
203
  "package.json": JSON.stringify(pkg, null, 2) + "\n",
193
204
  ".gitignore": ["node_modules", ".env", "*.duckdb"].join("\n") + "\n",
194
- "AGENTS.md": renderAgents(answers),
205
+ [agentsFileName(answers.skillLinkTarget)]: renderAgents(answers),
195
206
  "index.md": renderIndex(answers)
196
207
  };
197
208
  if (answers.packageManager?.name === "yarn") files[".yarnrc.yml"] = "nodeLinker: node-modules\n";
@@ -205,6 +216,10 @@ function grapheneCommand(packageManager, args) {
205
216
  if (packageManager?.name === "bun") return `bun run graphene ${args}`;
206
217
  return `npx graphene ${args}`;
207
218
  }
219
+ function agentsFileName(skillLinkTarget) {
220
+ if (skillLinkTarget === ".claude") return "CLAUDE.md";
221
+ return "AGENTS.md";
222
+ }
208
223
  function renderAgents(answers) {
209
224
  return [
210
225
  "## Graphene Reference",
@@ -221,6 +236,7 @@ function renderAgents(answers) {
221
236
  function databaseLabel(database) {
222
237
  if (database === "snowflake") return "Snowflake";
223
238
  if (database === "bigquery") return "BigQuery";
239
+ if (database === "clickhouse") return "ClickHouse";
224
240
  return "DuckDB";
225
241
  }
226
242
  function renderIndex(answers) {
@@ -233,6 +249,8 @@ function renderEnvFile(answers) {
233
249
  if (answers.snowflakePassphrase) lines.push(`SNOWFLAKE_PRI_PASSPHRASE=${answers.snowflakePassphrase}`);
234
250
  } else if (answers.database === "bigquery") {
235
251
  lines.push(`GOOGLE_APPLICATION_CREDENTIALS=${answers.bigqueryKeyPath || ""}`);
252
+ } else if (answers.database === "clickhouse") {
253
+ lines.push(`CLICKHOUSE_PASSWORD=${answers.clickhousePassword || ""}`);
236
254
  }
237
255
  return lines.length ? lines.join("\n") + "\n" : null;
238
256
  }
@@ -258,9 +276,10 @@ function printHelp(stdout) {
258
276
  "Usage: npm create graphene [target-dir] [-- --yes] [--name <project-name>] [--no-install]",
259
277
  "",
260
278
  "Options:",
261
- " -y, --yes Skip prompts and accept defaults",
262
- " --name <name> Set the generated package name",
263
- " --no-install Skip dependency installation"
279
+ " -y, --yes Skip prompts and accept defaults",
280
+ " --name <name> Set the generated package name",
281
+ " --no-install Skip dependency installation",
282
+ " --skip-credential-validation Skip warehouse credential validation"
264
283
  ].join("\n") + "\n"
265
284
  );
266
285
  }
@@ -280,11 +299,18 @@ function validateFileExtension(filePath, expectedExtension) {
280
299
  if (typeof filePath !== "string" || !filePath.trim()) return `Path to ${expectedExtension} file is required`;
281
300
  if (!filePath.endsWith(expectedExtension)) return `File must end with ${expectedExtension}`;
282
301
  }
283
- async function promptExistingFilePath({ message, expectedExtension, input, output }) {
302
+ async function promptExistingFilePath({
303
+ message,
304
+ expectedExtension,
305
+ initialValue,
306
+ input,
307
+ output
308
+ }) {
284
309
  while (true) {
285
310
  let filePath = unwrapPrompt(
286
311
  await clack.path({
287
312
  message,
313
+ initialValue,
288
314
  validate: (value) => validateFileExtension(value, expectedExtension),
289
315
  ...promptOptions(input, output)
290
316
  })
@@ -297,14 +323,10 @@ async function promptExistingFilePath({ message, expectedExtension, input, outpu
297
323
  }
298
324
  }
299
325
  }
300
- function namespacePlaceholder(database) {
301
- if (database === "snowflake") return "MY_DB.ANALYTICS";
302
- if (database === "bigquery") return "my-project.analytics";
303
- return "analytics";
304
- }
305
326
  function getWarehouseClient(database) {
306
327
  if (database === "snowflake") return "snowflake-sdk";
307
328
  if (database === "bigquery") return "@google-cloud/bigquery";
329
+ if (database === "clickhouse") return "@clickhouse/client";
308
330
  return "@duckdb/node-api";
309
331
  }
310
332
  function getWarehouseClientVersion(packageName) {
@@ -312,6 +334,175 @@ function getWarehouseClientVersion(packageName) {
312
334
  if (!version) throw new Error(`Missing ${packageName} peerDependency in cli/package.json`);
313
335
  return version;
314
336
  }
337
+ async function promptWarehouseCredentials(answers, input, output) {
338
+ if (answers.database === "snowflake") {
339
+ answers.snowflakeAccount = unwrapPrompt(
340
+ await clack.text({
341
+ message: "Snowflake account ID",
342
+ placeholder: "myorg-myaccount",
343
+ initialValue: answers.snowflakeAccount,
344
+ validate: validateRequired("Snowflake account ID is required"),
345
+ ...promptOptions(input, output)
346
+ })
347
+ );
348
+ answers.snowflakeUsername = unwrapPrompt(
349
+ await clack.text({
350
+ message: "Snowflake username",
351
+ placeholder: "graphene_user",
352
+ initialValue: answers.snowflakeUsername,
353
+ validate: validateRequired("Snowflake username is required"),
354
+ ...promptOptions(input, output)
355
+ })
356
+ );
357
+ answers.snowflakeKeyPath = await promptExistingFilePath({ message: "Path to .p8 key file", expectedExtension: ".p8", initialValue: answers.snowflakeKeyPath, input, output });
358
+ answers.snowflakePassphrase = unwrapPrompt(await clack.password({ message: "Key passphrase", ...promptOptions(input, output) }));
359
+ return;
360
+ }
361
+ if (answers.database === "bigquery") {
362
+ answers.bigqueryProjectId = unwrapPrompt(
363
+ await clack.text({
364
+ message: "GCP project ID",
365
+ placeholder: "my-project-123",
366
+ initialValue: answers.bigqueryProjectId,
367
+ validate: validateRequired("GCP project ID is required"),
368
+ ...promptOptions(input, output)
369
+ })
370
+ );
371
+ answers.bigqueryKeyPath = await promptExistingFilePath({
372
+ message: "Path to service account .json key file",
373
+ expectedExtension: ".json",
374
+ initialValue: answers.bigqueryKeyPath,
375
+ input,
376
+ output
377
+ });
378
+ return;
379
+ }
380
+ if (answers.database === "clickhouse") {
381
+ answers.clickhouseUrl = unwrapPrompt(
382
+ await clack.text({
383
+ message: "ClickHouse URL",
384
+ placeholder: "https://example.clickhouse.cloud:8443",
385
+ initialValue: answers.clickhouseUrl,
386
+ validate: validateRequired("ClickHouse URL is required"),
387
+ ...promptOptions(input, output)
388
+ })
389
+ );
390
+ answers.clickhouseUsername = unwrapPrompt(
391
+ await clack.text({
392
+ message: "ClickHouse username",
393
+ placeholder: "default",
394
+ initialValue: answers.clickhouseUsername,
395
+ validate: validateRequired("ClickHouse username is required"),
396
+ ...promptOptions(input, output)
397
+ })
398
+ );
399
+ answers.clickhousePassword = unwrapPrompt(
400
+ await clack.password({
401
+ message: "ClickHouse password",
402
+ validate: validateRequired("ClickHouse password is required"),
403
+ ...promptOptions(input, output)
404
+ })
405
+ );
406
+ }
407
+ }
408
+ async function collectWarehouseCredentials(answers, options, input, output) {
409
+ while (true) {
410
+ await promptWarehouseCredentials(answers, input, output);
411
+ if (options.skipCredentialValidation) return;
412
+ let result = await testWarehouseCredentials(answers, output);
413
+ if (result.ok) return;
414
+ clack.log.error(`${databaseLabel(answers.database)} credential validation failed: ${result.error}`, { output });
415
+ let action = unwrapPrompt(
416
+ await clack.select({
417
+ message: "How would you like to continue?",
418
+ options: [
419
+ { value: "edit", label: "Edit credentials" },
420
+ { value: "continue", label: "Continue anyway" }
421
+ ],
422
+ ...promptOptions(input, output)
423
+ })
424
+ );
425
+ if (action === "continue") return;
426
+ }
427
+ }
428
+ async function testWarehouseCredentials(answers, output) {
429
+ let task = clack.taskLog({ title: `Testing ${databaseLabel(answers.database)} credentials...`, retainLog: true, output });
430
+ try {
431
+ await withTimeout(runWarehouseCredentialTest(answers), 15e3, `${databaseLabel(answers.database)} credential validation timed out`);
432
+ task.success("Credentials validated");
433
+ return { ok: true };
434
+ } catch (err) {
435
+ task.error("Credential validation failed", { showLog: true });
436
+ return { ok: false, error: formatCredentialError(err) };
437
+ }
438
+ }
439
+ async function withTimeout(promise, ms, message) {
440
+ let timeout;
441
+ try {
442
+ return await Promise.race([
443
+ promise,
444
+ new Promise((_resolve, reject) => {
445
+ timeout = setTimeout(() => reject(new Error(message)), ms);
446
+ })
447
+ ]);
448
+ } finally {
449
+ if (timeout) clearTimeout(timeout);
450
+ }
451
+ }
452
+ function runWarehouseCredentialTest(answers) {
453
+ if (answers.database === "snowflake") return testSnowflakeCredentials(answers);
454
+ if (answers.database === "bigquery") return testBigQueryCredentials(answers);
455
+ if (answers.database === "clickhouse") return testClickHouseCredentials(answers);
456
+ return Promise.resolve();
457
+ }
458
+ async function testSnowflakeCredentials(answers) {
459
+ let snowflake = (await import("snowflake-sdk")).default;
460
+ snowflake.configure({ logLevel: "WARN", logFilePath: "/dev/null" });
461
+ let connection = snowflake.createConnection({
462
+ account: answers.snowflakeAccount,
463
+ username: answers.snowflakeUsername,
464
+ privateKeyPath: answers.snowflakeKeyPath,
465
+ privateKeyPass: answers.snowflakePassphrase,
466
+ authenticator: "SNOWFLAKE_JWT",
467
+ application: "Graphene"
468
+ });
469
+ try {
470
+ await new Promise((resolve, reject) => {
471
+ connection.connect((err) => err ? reject(err) : resolve());
472
+ });
473
+ } finally {
474
+ await new Promise((resolve) => {
475
+ connection.destroy(() => resolve());
476
+ }).catch(() => {
477
+ });
478
+ }
479
+ }
480
+ async function testBigQueryCredentials(answers) {
481
+ let { BigQuery } = await import("@google-cloud/bigquery");
482
+ let credentials = JSON.parse(await readFile(answers.bigqueryKeyPath || "", "utf8"));
483
+ let client = new BigQuery({ projectId: answers.bigqueryProjectId, credentials, userAgent: "Graphene" });
484
+ await client.createQueryJob({ query: "select 1", useLegacySql: false, dryRun: true });
485
+ }
486
+ async function testClickHouseCredentials(answers) {
487
+ let { createClient } = await import("@clickhouse/client");
488
+ let client = createClient({
489
+ url: answers.clickhouseUrl,
490
+ username: answers.clickhouseUsername,
491
+ password: answers.clickhousePassword,
492
+ database: answers.defaultNamespace || "default",
493
+ application: "Graphene"
494
+ });
495
+ try {
496
+ let result = await client.query({ query: "SELECT 1", format: "JSONEachRow" });
497
+ await result.json();
498
+ } finally {
499
+ await client.close();
500
+ }
501
+ }
502
+ function formatCredentialError(err) {
503
+ if (err instanceof Error && err.message) return err.message;
504
+ return String(err || "Unknown error");
505
+ }
315
506
  async function collectAnswers({ options, packageManager, input, output }) {
316
507
  if (options.yes) {
317
508
  let targetDir2 = options.targetDir || "graphene-app";
@@ -340,74 +531,23 @@ async function collectAnswers({ options, packageManager, input, output }) {
340
531
  await clack.select({
341
532
  message: "Database",
342
533
  options: [
343
- { value: "duckdb", label: "DuckDB (local file)" },
344
534
  { value: "snowflake", label: "Snowflake" },
345
- { value: "bigquery", label: "BigQuery" }
535
+ { value: "bigquery", label: "BigQuery" },
536
+ { value: "clickhouse", label: "ClickHouse" },
537
+ { value: "duckdb", label: "DuckDB (local file)" }
346
538
  ],
347
539
  ...promptOptions(input, output)
348
540
  })
349
541
  );
350
- let defaultNamespace = unwrapPrompt(
351
- await clack.text({
352
- message: "Default namespace (optional)",
353
- placeholder: namespacePlaceholder(database),
354
- ...promptOptions(input, output)
355
- })
356
- ).trim();
357
542
  let answers = {
358
543
  targetDir,
359
544
  projectName,
360
545
  packageManager,
361
546
  database,
362
- defaultNamespace: defaultNamespace || void 0,
363
547
  skillLinkTarget: "none"
364
548
  };
365
- if (database === "duckdb") {
366
- answers.duckdbPath = unwrapPrompt(
367
- await clack.text({
368
- message: "Path to .duckdb file",
369
- placeholder: "./data.duckdb",
370
- validate(value) {
371
- if (typeof value !== "string" || !value.trim()) return;
372
- if (!value.endsWith(".duckdb")) return "DuckDB path must end with .duckdb";
373
- },
374
- ...promptOptions(input, output)
375
- })
376
- ).trim() || void 0;
377
- } else if (database === "snowflake") {
378
- answers.snowflakeAccount = unwrapPrompt(
379
- await clack.text({
380
- message: "Snowflake account ID",
381
- placeholder: "myorg-myaccount",
382
- validate: validateRequired("Snowflake account ID is required"),
383
- ...promptOptions(input, output)
384
- })
385
- );
386
- answers.snowflakeUsername = unwrapPrompt(
387
- await clack.text({
388
- message: "Snowflake username",
389
- placeholder: "graphene_user",
390
- validate: validateRequired("Snowflake username is required"),
391
- ...promptOptions(input, output)
392
- })
393
- );
394
- answers.snowflakeKeyPath = await promptExistingFilePath({ message: "Path to .p8 key file", expectedExtension: ".p8", input, output });
395
- answers.snowflakePassphrase = unwrapPrompt(
396
- await clack.password({
397
- message: "Key passphrase",
398
- ...promptOptions(input, output)
399
- })
400
- );
401
- } else {
402
- answers.bigqueryProjectId = unwrapPrompt(
403
- await clack.text({
404
- message: "GCP project ID",
405
- placeholder: "my-project-123",
406
- validate: validateRequired("GCP project ID is required"),
407
- ...promptOptions(input, output)
408
- })
409
- );
410
- answers.bigqueryKeyPath = await promptExistingFilePath({ message: "Path to service account .json key file", expectedExtension: ".json", input, output });
549
+ if (database !== "duckdb") {
550
+ await collectWarehouseCredentials(answers, options, input, output);
411
551
  }
412
552
  if (options.install) {
413
553
  answers.skillLinkTarget = unwrapPrompt(
@@ -505,7 +645,7 @@ async function runCreate({ argv, cwd, env = {}, stdin, stdout }) {
505
645
  if (install.code !== 0) throw new Error(install.stderr.trim() || `${packageManager.name} install failed with code ${install.code}`);
506
646
  await symlinkGrapheneSkill(targetDir, answers.skillLinkTarget);
507
647
  }
508
- clack.outro("Done!", { output: stdout });
648
+ clack.outro(completionMessage(answers), { output: stdout });
509
649
  } catch (err) {
510
650
  if (err instanceof CreateCancelled) {
511
651
  clack.cancel("Operation cancelled.", { output: stdout });
@@ -514,6 +654,25 @@ async function runCreate({ argv, cwd, env = {}, stdin, stdout }) {
514
654
  throw err;
515
655
  }
516
656
  }
657
+ function completionMessage(answers) {
658
+ if (answers.database !== "duckdb") return "Done!";
659
+ return [
660
+ "Done!",
661
+ "",
662
+ `For DuckDB, copy your ${color("cyan", ".duckdb")} file into the ${color("magenta", answers.projectName)} folder so Graphene can discover it.`,
663
+ "",
664
+ `If the duckdb file lives elsewhere, set the path in ${color("magenta", "package.json")}:`,
665
+ "",
666
+ color("gray", '"graphene": {'),
667
+ color("gray", ' "duckdb": {'),
668
+ color("gray", ' "path": "/path/to/example.duckdb"'),
669
+ color("gray", " }"),
670
+ color("gray", "}")
671
+ ].join("\n");
672
+ }
673
+ function color(style, text2) {
674
+ return styleText(style, text2, { validateStream: false });
675
+ }
517
676
 
518
677
  // cli.ts
519
678
  await runCreate({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-graphene",
3
- "version": "0.0.16",
3
+ "version": "0.0.17",
4
4
  "license": "Elastic-2.0",
5
5
  "author": "Graphene Systems Inc",
6
6
  "repository": {
@@ -25,7 +25,10 @@
25
25
  "prepack": "pnpm run build"
26
26
  },
27
27
  "dependencies": {
28
- "@clack/prompts": "^1.2.0"
28
+ "@clack/prompts": "^1.2.0",
29
+ "@clickhouse/client": "^1.18.2",
30
+ "@google-cloud/bigquery": "^8.2.0",
31
+ "snowflake-sdk": "2.4.0"
29
32
  },
30
33
  "devDependencies": {
31
34
  "esbuild": "^0.27.2"