create-graphene 0.0.16 → 0.0.18

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