askdb 1.0.0-beta.27 → 1.0.0-beta.30

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/init.js CHANGED
@@ -4,86 +4,241 @@ import { createRequire } from "node:module";
4
4
  import { dirname, join, resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
6
  const DEFAULT_CONFIG_PATH = "askdb.config.ts";
7
- /** Single template: only `askdb.config.ts` is written (kept in sync with the repo root `askdb.config.ts`). */
8
- const CONFIG_TEMPLATE = `import "dotenv/config";
9
- import { defineConfig, env, type AskDbConfig } from "@askdb/config";
10
-
11
- // \`dotenv/config\` loads a local \`.env\` when this module runs (missing file is OK).
12
- // CLIs call \`bootstrapAskDbEnv\`, which loads \`.env\` then evaluates this file and installs the AskDB runtime snapshot.
13
- // Use \`env("VAR")\` for every value read from the environment; \`flattenAskDbConfig\` applies defaults
14
- // for optional fields (see \`@askdb/config\` / \`defaults.ts\`).
15
- export default defineConfig({
16
- ai: {
17
- // openai | azure | foundry (foundry uses Azure-compatible env vars) | google
18
- provider: "openai",
7
+ // ---------------------------------------------------------------------------
8
+ // Config rendering
9
+ // ---------------------------------------------------------------------------
10
+ /** AI provider defaults for key/model env vars */
11
+ const AI_DEFAULTS = {
12
+ openai: { keyEnv: "OPENAI_API_KEY", modelEnv: "OPENAI_MODEL", modelField: "model" },
13
+ anthropic: { keyEnv: "ANTHROPIC_API_KEY", modelEnv: "ANTHROPIC_MODEL", modelField: "model" },
14
+ google: { keyEnv: "GOOGLE_GENERATIVE_AI_API_KEY", modelEnv: "GOOGLE_GENERATIVE_AI_MODEL", modelField: "model" },
15
+ azure: { keyEnv: "AZURE_OPENAI_API_KEY", modelEnv: "AZURE_OPENAI_DEPLOYMENT", modelField: "model" },
16
+ foundry: { keyEnv: "AZURE_OPENAI_API_KEY", modelEnv: "AZURE_OPENAI_DEPLOYMENT", modelField: "model" },
17
+ };
18
+ function renderAiSection(answers) {
19
+ const { aiProvider, aiKeyEnv, aiModelEnv } = answers;
20
+ const modelLine = aiModelEnv ? `\n ${AI_DEFAULTS[aiProvider].modelField}: env("${aiModelEnv}"),` : "";
21
+ return ` ai: {
22
+ provider: "${aiProvider}",
19
23
  providerConfig: {
20
- openai: {
21
- // Live NL→SQL: set in \`.env\`, e.g. OPENAI_API_KEY=… (optional OPENAI_BASE_URL=…)
22
- apiKey: env("OPENAI_API_KEY"),
23
- model: env("OPENAI_MODEL"),
24
+ ${aiProvider}: {
25
+ apiKey: env("${aiKeyEnv}"),${modelLine}
24
26
  },
25
27
  },
26
- },
27
-
28
- introspection: {
29
- // postgres | prisma | mysql | sqlite | sqlserver
28
+ },`;
29
+ }
30
+ function renderIntrospectionSection(answers) {
31
+ const { database, connectionEnv, sqliteFile, prismaSchema, schemaOut } = answers;
32
+ const outputDirLine = `\n outputDir: "${schemaOut}",`;
33
+ switch (database) {
34
+ case "postgres":
35
+ return ` introspection: {
30
36
  provider: "postgres",
31
37
  providerConfig: {
32
38
  postgres: {
33
- // Postgres URL for \`askdb introspect\` — maps to ASKDB_INTROSPECT_POSTGRES_URL
34
- databaseUrl: env("DATABASE_URL"),
39
+ databaseUrl: env("${connectionEnv ?? "DATABASE_URL"}"),
35
40
  },
36
- },
37
- // Default Schema v2 output when you omit \`askdb introspect --out\` (maps to ASKDB_INTROSPECT_OUT)
38
- outputDir: env("MY_INTROSPECT_OUTPUT_DIR"),
39
- },
40
-
41
- rag: {
42
- // mock | openai | ai-sdk — optional: MY_RAG_EMBEDDER in \`.env\`
43
- embedder: "openai",
44
- embedderConfig: {
45
- openai: {
46
- model: env("ASKDB_RAG_EMBEDDER_MODEL"),
47
- dimension: env("ASKDB_RAG_EMBEDDER_DIMENSIONS"),
48
- apiKey: env("OPENAI_API_KEY"),
49
- baseUrl: env("ASKDB_RAG_EMBEDDER_BASE_URL"),
41
+ },${outputDirLine}
42
+ },`;
43
+ case "mysql":
44
+ return ` introspection: {
45
+ provider: "mysql",
46
+ providerConfig: {
47
+ mysql: {
48
+ databaseUrl: env("${connectionEnv ?? "DATABASE_URL"}"),
50
49
  },
51
- },
52
- // file | memory | pgvector — optional: ASKDB_PGVECTOR_URL for pgvector (e.g. port 5434 fixture)
50
+ },${outputDirLine}
51
+ },`;
52
+ case "sqlite": {
53
+ const fileExpr = sqliteFile && !sqliteFile.startsWith("./") && !sqliteFile.startsWith("/")
54
+ ? `env("${sqliteFile}")`
55
+ : `env("SQLITE_FILE")`;
56
+ const resolvedFile = sqliteFile && (sqliteFile.startsWith("./") || sqliteFile.startsWith("/"))
57
+ ? `"${sqliteFile}"`
58
+ : fileExpr;
59
+ return ` introspection: {
60
+ provider: "sqlite",
61
+ providerConfig: {
62
+ sqlite: {
63
+ file: ${resolvedFile},
64
+ },
65
+ },${outputDirLine}
66
+ },`;
67
+ }
68
+ case "sqlserver":
69
+ return ` introspection: {
70
+ provider: "sqlserver",
71
+ providerConfig: {
72
+ sqlserver: {
73
+ databaseUrl: env("${connectionEnv ?? "SQLSERVER_URL"}"),
74
+ },
75
+ },${outputDirLine}
76
+ },`;
77
+ case "prisma": {
78
+ const schemaLine = prismaSchema ? `\n schemaPath: "${prismaSchema}",` : "";
79
+ return ` introspection: {
80
+ provider: "prisma",
81
+ providerConfig: {
82
+ prisma: {${schemaLine}
83
+ },
84
+ },${outputDirLine}
85
+ },`;
86
+ }
87
+ }
88
+ }
89
+ function renderRagSection(answers) {
90
+ const { ragStore, pgvectorEnv } = answers;
91
+ switch (ragStore) {
92
+ case "file":
93
+ return ` rag: {
94
+ embedder: "mock",
95
+ embedderConfig: {},
53
96
  store: "file",
54
97
  storeConfig: {
55
98
  file: {},
99
+ },
100
+ },`;
101
+ case "memory":
102
+ return ` rag: {
103
+ embedder: "mock",
104
+ embedderConfig: {},
105
+ store: "memory",
106
+ storeConfig: {
56
107
  memory: {},
108
+ },
109
+ },`;
110
+ case "pgvector":
111
+ return ` rag: {
112
+ embedder: "mock",
113
+ embedderConfig: {},
114
+ store: "pgvector",
115
+ storeConfig: {
57
116
  pgvector: {
58
- databaseUrl: env("ASKDB_PGVECTOR_URL"),
59
- dimensions: env("ASKDB_RAG_EMBEDDER_DIMENSIONS"),
117
+ databaseUrl: env("${pgvectorEnv ?? "ASKDB_PGVECTOR_URL"}"),
60
118
  },
61
119
  },
62
- },
63
- logging: {
64
- correlationId: env("ASKDB_CORRELATION_ID"),
65
- },
66
- dev: {
67
- mockSql: env("ASKDB_MOCK_SQL"),
68
- },
69
- studio: {
70
- listen: {
71
- host: env("ASKDB_STUDIO_HOST"),
72
- ...(env("ASKDB_STUDIO_PORT") ? { port: Number(env("ASKDB_STUDIO_PORT")) } : {}),
73
- },
120
+ },`;
121
+ }
122
+ }
123
+ function renderStudioSection(answers) {
124
+ const { studioExecute } = answers;
125
+ if (!studioExecute.enabled)
126
+ return null;
127
+ const { provider } = studioExecute;
128
+ if (provider === "sqlite") {
129
+ const fileExpr = studioExecute.sqliteFile && (studioExecute.sqliteFile.startsWith("./") || studioExecute.sqliteFile.startsWith("/"))
130
+ ? `"${studioExecute.sqliteFile}"`
131
+ : `env("${studioExecute.sqliteFile ?? "SQLITE_FILE"}")`;
132
+ return ` studio: {
74
133
  execute: {
75
- // Connection URL for the Studio playground query runner (maps to ASKDB_STUDIO_DATABASE_URL)
76
- databaseUrl: env("DATABASE_URL"),
134
+ provider: "sqlite",
135
+ file: ${fileExpr},
77
136
  },
78
- },
79
- httpApi: {
80
- listen: {
81
- host: env("HOST"),
82
- ...(env("PORT") ? { port: Number(env("PORT")) } : {}),
137
+ },`;
138
+ }
139
+ const urlEnv = studioExecute.connectionEnv ?? (provider === "sqlserver" ? "SQLSERVER_URL" : "DATABASE_URL");
140
+ return ` studio: {
141
+ execute: {
142
+ provider: "${provider}",
143
+ databaseUrl: env("${urlEnv}"),
83
144
  },
84
- },
145
+ },`;
146
+ }
147
+ export function renderInitConfig(answers) {
148
+ const sections = [
149
+ renderAiSection(answers),
150
+ renderIntrospectionSection(answers),
151
+ renderRagSection(answers),
152
+ ];
153
+ const studio = renderStudioSection(answers);
154
+ if (studio)
155
+ sections.push(studio);
156
+ return `import dotenv from "dotenv";
157
+ import { defineConfig, env, type AskDbConfig } from "@askdb/config";
158
+
159
+ dotenv.config({ quiet: true });
160
+
161
+ export default defineConfig({
162
+ ${sections.join("\n")}
85
163
  } satisfies AskDbConfig);
86
164
  `;
165
+ }
166
+ export function resolveDefaultInitAnswers(overrides = {}) {
167
+ const database = overrides.database ?? "postgres";
168
+ const aiProvider = overrides.aiProvider ?? "openai";
169
+ const aiDefaults = AI_DEFAULTS[aiProvider];
170
+ let connectionEnv = overrides.connectionEnv;
171
+ if (!connectionEnv) {
172
+ if (database === "sqlserver")
173
+ connectionEnv = "SQLSERVER_URL";
174
+ else if (database === "sqlite")
175
+ connectionEnv = undefined;
176
+ else
177
+ connectionEnv = "DATABASE_URL";
178
+ }
179
+ const studioEnabled = overrides.studioExecute ?? false;
180
+ let studioExecute;
181
+ if (studioEnabled) {
182
+ const execProvider = overrides.studioExecuteProvider ??
183
+ (database !== "prisma" ? database : "postgres");
184
+ studioExecute = {
185
+ enabled: true,
186
+ provider: execProvider,
187
+ connectionEnv: overrides.studioExecuteConnectionEnv ?? (execProvider === "sqlite" ? undefined : (execProvider === "sqlserver" ? "SQLSERVER_URL" : "DATABASE_URL")),
188
+ sqliteFile: overrides.studioExecuteSqliteFile ?? (execProvider === "sqlite" ? overrides.sqliteFile : undefined),
189
+ };
190
+ }
191
+ else {
192
+ studioExecute = { enabled: false };
193
+ }
194
+ return {
195
+ database,
196
+ connectionEnv,
197
+ sqliteFile: overrides.sqliteFile,
198
+ prismaSchema: overrides.prismaSchema,
199
+ schemaOut: overrides.schemaOut ?? "./askdb",
200
+ aiProvider,
201
+ aiKeyEnv: overrides.aiKeyEnv ?? aiDefaults.keyEnv,
202
+ aiModelEnv: overrides.aiModelEnv ?? aiDefaults.modelEnv,
203
+ ragStore: overrides.ragStore ?? "file",
204
+ pgvectorEnv: overrides.pgvectorEnv,
205
+ studioExecute,
206
+ };
207
+ }
208
+ /** Package maps — names come from an allowlist, not from user input */
209
+ const DB_DRIVER_PACKAGES = {
210
+ postgres: "pg",
211
+ mysql: "mysql2",
212
+ sqlite: "better-sqlite3",
213
+ sqlserver: "mssql",
214
+ };
215
+ export function buildInitInstallPlan(answers, specs) {
216
+ const packages = [
217
+ `@askdb/config@${specs.configSpec}`,
218
+ `dotenv@${specs.dotenvSpec}`,
219
+ ];
220
+ const labels = ["@askdb/config", "dotenv"];
221
+ const driversNeeded = new Set();
222
+ // Live DB driver for introspection
223
+ if (answers.database !== "prisma") {
224
+ driversNeeded.add(answers.database);
225
+ }
226
+ // Live DB driver for Studio execute
227
+ if (answers.studioExecute.enabled) {
228
+ driversNeeded.add(answers.studioExecute.provider);
229
+ }
230
+ for (const db of driversNeeded) {
231
+ const pkg = DB_DRIVER_PACKAGES[db];
232
+ if (pkg) {
233
+ packages.push(pkg);
234
+ labels.push(pkg);
235
+ }
236
+ }
237
+ return { packages, labels };
238
+ }
239
+ // ---------------------------------------------------------------------------
240
+ // Package manager detection
241
+ // ---------------------------------------------------------------------------
87
242
  const require = createRequire(import.meta.url);
88
243
  /** Walk upward from `startDir` to find a directory containing `package.json`. */
89
244
  export function findNearestPackageJsonDir(startDir) {
@@ -145,7 +300,7 @@ export function resolveInitDepSpecs() {
145
300
  configSpec = "latest";
146
301
  }
147
302
  }
148
- let dotenvSpec = "^16.6.1";
303
+ let dotenvSpec = "^17.4.2";
149
304
  try {
150
305
  const cfgPkgPath = require.resolve("@askdb/config/package.json");
151
306
  const cfg = JSON.parse(readFileSync(cfgPkgPath, "utf8"));
@@ -157,13 +312,7 @@ export function resolveInitDepSpecs() {
157
312
  }
158
313
  return { configSpec, dotenvSpec };
159
314
  }
160
- function formatDepArgs(specs) {
161
- const configArg = `@askdb/config@${specs.configSpec}`;
162
- const dotenvArg = `dotenv@${specs.dotenvSpec}`;
163
- return [configArg, dotenvArg];
164
- }
165
- function runPackageManagerInstall(pm, packageDir, specs) {
166
- const [c, d] = formatDepArgs(specs);
315
+ function defaultInstaller(pm, packageDir, packages) {
167
316
  const env = { ...process.env, CI: process.env.CI ?? "true" };
168
317
  const win = process.platform === "win32";
169
318
  let cmd;
@@ -171,24 +320,389 @@ function runPackageManagerInstall(pm, packageDir, specs) {
171
320
  switch (pm) {
172
321
  case "pnpm":
173
322
  cmd = win ? "pnpm.CMD" : "pnpm";
174
- args = ["add", c, d];
323
+ args = ["add", ...packages];
175
324
  break;
176
325
  case "yarn":
177
326
  cmd = win ? "yarn.cmd" : "yarn";
178
- args = ["add", c, d];
327
+ args = ["add", ...packages];
179
328
  break;
180
329
  case "bun":
181
330
  cmd = win ? "bun.exe" : "bun";
182
- args = ["add", c, d];
331
+ args = ["add", ...packages];
183
332
  break;
184
333
  default:
185
334
  cmd = win ? "npm.cmd" : "npm";
186
- args = ["install", "--save", c, d];
335
+ args = ["install", "--save", ...packages];
187
336
  }
188
337
  const r = spawnSync(cmd, args, { cwd: packageDir, stdio: "inherit", env, shell: false });
189
338
  return r.status === 0;
190
339
  }
191
- export function runInitCli(argv) {
340
+ function formatManualInstallCommand(pm, packages) {
341
+ const pkgList = packages.join(" ");
342
+ switch (pm) {
343
+ case "pnpm":
344
+ return `pnpm add ${pkgList}`;
345
+ case "yarn":
346
+ return `yarn add ${pkgList}`;
347
+ case "bun":
348
+ return `bun add ${pkgList}`;
349
+ default:
350
+ return `npm install --save ${pkgList}`;
351
+ }
352
+ }
353
+ const VALID_DATABASES = ["postgres", "mysql", "sqlite", "sqlserver", "prisma"];
354
+ const VALID_AI_PROVIDERS = ["openai", "anthropic", "google", "azure", "foundry"];
355
+ const VALID_RAG_STORES = ["file", "memory", "pgvector"];
356
+ function parseOptions(argv) {
357
+ const opts = {
358
+ force: false,
359
+ path: DEFAULT_CONFIG_PATH,
360
+ skipInstall: false,
361
+ yes: false,
362
+ interactive: undefined,
363
+ noInteractive: false,
364
+ };
365
+ for (let i = 0; i < argv.length; i += 1) {
366
+ const arg = argv[i];
367
+ switch (arg) {
368
+ case "--force":
369
+ case "-f":
370
+ opts.force = true;
371
+ break;
372
+ case "--skip-install":
373
+ opts.skipInstall = true;
374
+ break;
375
+ case "--yes":
376
+ case "-y":
377
+ opts.yes = true;
378
+ break;
379
+ case "--interactive":
380
+ opts.interactive = true;
381
+ break;
382
+ case "--no-interactive":
383
+ opts.noInteractive = true;
384
+ break;
385
+ case "--studio-execute":
386
+ opts.studioExecute = true;
387
+ break;
388
+ case "--no-studio-execute":
389
+ opts.studioExecute = false;
390
+ break;
391
+ case "--path": {
392
+ const value = argv[++i];
393
+ if (!value || value.startsWith("--"))
394
+ throw new Error(`${arg} requires a value.`);
395
+ opts.path = value;
396
+ break;
397
+ }
398
+ case "--database": {
399
+ const value = argv[++i];
400
+ if (!value || value.startsWith("--"))
401
+ throw new Error(`${arg} requires a value.`);
402
+ if (!VALID_DATABASES.includes(value)) {
403
+ throw new Error(`${arg}: invalid value "${value}". Allowed: ${VALID_DATABASES.join(", ")}.`);
404
+ }
405
+ opts.database = value;
406
+ break;
407
+ }
408
+ case "--connection-env": {
409
+ const value = argv[++i];
410
+ if (!value || value.startsWith("--"))
411
+ throw new Error(`${arg} requires a value.`);
412
+ opts.connectionEnv = value;
413
+ break;
414
+ }
415
+ case "--sqlite-file": {
416
+ const value = argv[++i];
417
+ if (!value || value.startsWith("--"))
418
+ throw new Error(`${arg} requires a value.`);
419
+ opts.sqliteFile = value;
420
+ break;
421
+ }
422
+ case "--prisma-schema": {
423
+ const value = argv[++i];
424
+ if (!value || value.startsWith("--"))
425
+ throw new Error(`${arg} requires a value.`);
426
+ opts.prismaSchema = value;
427
+ break;
428
+ }
429
+ case "--schema-out": {
430
+ const value = argv[++i];
431
+ if (!value || value.startsWith("--"))
432
+ throw new Error(`${arg} requires a value.`);
433
+ opts.schemaOut = value;
434
+ break;
435
+ }
436
+ case "--ai-provider": {
437
+ const value = argv[++i];
438
+ if (!value || value.startsWith("--"))
439
+ throw new Error(`${arg} requires a value.`);
440
+ if (!VALID_AI_PROVIDERS.includes(value)) {
441
+ throw new Error(`${arg}: invalid value "${value}". Allowed: ${VALID_AI_PROVIDERS.join(", ")}.`);
442
+ }
443
+ opts.aiProvider = value;
444
+ break;
445
+ }
446
+ case "--ai-key-env": {
447
+ const value = argv[++i];
448
+ if (!value || value.startsWith("--"))
449
+ throw new Error(`${arg} requires a value.`);
450
+ opts.aiKeyEnv = value;
451
+ break;
452
+ }
453
+ case "--ai-model-env": {
454
+ const value = argv[++i];
455
+ if (!value || value.startsWith("--"))
456
+ throw new Error(`${arg} requires a value.`);
457
+ opts.aiModelEnv = value;
458
+ break;
459
+ }
460
+ case "--rag-store": {
461
+ const value = argv[++i];
462
+ if (!value || value.startsWith("--"))
463
+ throw new Error(`${arg} requires a value.`);
464
+ if (!VALID_RAG_STORES.includes(value)) {
465
+ throw new Error(`${arg}: invalid value "${value}". Allowed: ${VALID_RAG_STORES.join(", ")}.`);
466
+ }
467
+ opts.ragStore = value;
468
+ break;
469
+ }
470
+ case "--pgvector-env": {
471
+ const value = argv[++i];
472
+ if (!value || value.startsWith("--"))
473
+ throw new Error(`${arg} requires a value.`);
474
+ opts.pgvectorEnv = value;
475
+ break;
476
+ }
477
+ case "--help":
478
+ case "-h":
479
+ opts.path = "--help";
480
+ return opts;
481
+ default:
482
+ throw new Error(`Unknown option: ${arg}`);
483
+ }
484
+ }
485
+ // Conflict checks
486
+ if (opts.yes && opts.interactive) {
487
+ throw new Error("--yes and --interactive cannot be used together.");
488
+ }
489
+ if (opts.interactive && opts.noInteractive) {
490
+ throw new Error("--interactive and --no-interactive cannot be used together.");
491
+ }
492
+ return opts;
493
+ }
494
+ // ---------------------------------------------------------------------------
495
+ // TTY wizard
496
+ // ---------------------------------------------------------------------------
497
+ const ENV_VAR_RE = /^[A-Z_][A-Z0-9_]*$/i;
498
+ function validateEnvVar(value) {
499
+ if (!value.trim())
500
+ return "Cannot be empty.";
501
+ if (!ENV_VAR_RE.test(value.trim()))
502
+ return "Use only letters, digits, and underscores (e.g. DATABASE_URL).";
503
+ return true;
504
+ }
505
+ function validatePath(value) {
506
+ if (!value.trim())
507
+ return "Cannot be empty.";
508
+ return true;
509
+ }
510
+ async function runWizard(prompter) {
511
+ const database = await prompter.select({
512
+ message: "Database source",
513
+ choices: [
514
+ { name: "PostgreSQL", value: "postgres" },
515
+ { name: "MySQL", value: "mysql" },
516
+ { name: "SQLite", value: "sqlite" },
517
+ { name: "SQL Server", value: "sqlserver" },
518
+ { name: "Prisma (schema file, no live DB)", value: "prisma" },
519
+ ],
520
+ default: "postgres",
521
+ });
522
+ let connectionEnv;
523
+ let sqliteFile;
524
+ let prismaSchema;
525
+ if (database === "sqlite") {
526
+ const raw = await prompter.input({
527
+ message: "SQLite file path or env var name (e.g. ./data/app.db or SQLITE_FILE)",
528
+ default: "SQLITE_FILE",
529
+ });
530
+ const trimmed = raw.trim();
531
+ if (trimmed.startsWith("./") || trimmed.startsWith("/")) {
532
+ sqliteFile = trimmed;
533
+ }
534
+ else {
535
+ sqliteFile = trimmed;
536
+ }
537
+ }
538
+ else if (database === "prisma") {
539
+ prismaSchema = await prompter.input({
540
+ message: "Path to schema.prisma",
541
+ default: "./prisma/schema.prisma",
542
+ validate: validatePath,
543
+ });
544
+ }
545
+ else {
546
+ const defaultEnv = database === "sqlserver" ? "SQLSERVER_URL" : "DATABASE_URL";
547
+ connectionEnv = await prompter.input({
548
+ message: `Env var name for connection URL`,
549
+ default: defaultEnv,
550
+ validate: validateEnvVar,
551
+ });
552
+ }
553
+ const schemaOut = await prompter.input({
554
+ message: "Schema output directory",
555
+ default: "./askdb",
556
+ validate: validatePath,
557
+ });
558
+ const aiProvider = await prompter.select({
559
+ message: "AI provider",
560
+ choices: [
561
+ { name: "OpenAI", value: "openai" },
562
+ { name: "Anthropic", value: "anthropic" },
563
+ { name: "Google (Gemini)", value: "google" },
564
+ { name: "Azure OpenAI", value: "azure" },
565
+ { name: "Azure AI Foundry", value: "foundry" },
566
+ ],
567
+ default: "openai",
568
+ });
569
+ const aiKeyEnv = await prompter.input({
570
+ message: `Env var for ${aiProvider} API key`,
571
+ default: AI_DEFAULTS[aiProvider].keyEnv,
572
+ validate: validateEnvVar,
573
+ });
574
+ const aiModelEnv = await prompter.input({
575
+ message: `Env var for ${aiProvider} model (leave blank to use provider default)`,
576
+ default: AI_DEFAULTS[aiProvider].modelEnv,
577
+ });
578
+ const ragStore = await prompter.select({
579
+ message: "RAG store",
580
+ choices: [
581
+ { name: "File (default, no setup required)", value: "file" },
582
+ { name: "Memory (fast, non-persistent)", value: "memory" },
583
+ { name: "pgvector (Postgres vector store)", value: "pgvector" },
584
+ ],
585
+ default: "file",
586
+ });
587
+ let pgvectorEnv;
588
+ if (ragStore === "pgvector") {
589
+ pgvectorEnv = await prompter.input({
590
+ message: "Env var for pgvector connection URL",
591
+ default: "ASKDB_PGVECTOR_URL",
592
+ validate: validateEnvVar,
593
+ });
594
+ }
595
+ const studioExecuteDefault = database !== "prisma";
596
+ const enableStudioExecute = await prompter.confirm({
597
+ message: "Enable Studio execute (run queries from the browser playground)?",
598
+ default: studioExecuteDefault,
599
+ });
600
+ let studioExecute;
601
+ if (!enableStudioExecute) {
602
+ studioExecute = { enabled: false };
603
+ }
604
+ else if (database === "prisma") {
605
+ // Prisma: need to choose a live provider
606
+ const execProvider = await prompter.select({
607
+ message: "Studio execute needs a live database provider. Which one?",
608
+ choices: [
609
+ { name: "PostgreSQL", value: "postgres" },
610
+ { name: "MySQL", value: "mysql" },
611
+ { name: "SQLite", value: "sqlite" },
612
+ { name: "SQL Server", value: "sqlserver" },
613
+ ],
614
+ default: "postgres",
615
+ });
616
+ if (execProvider === "sqlite") {
617
+ const execSqliteFile = await prompter.input({
618
+ message: "SQLite file path or env var name for Studio execute",
619
+ default: "SQLITE_FILE",
620
+ });
621
+ studioExecute = {
622
+ enabled: true,
623
+ provider: "sqlite",
624
+ sqliteFile: execSqliteFile.trim(),
625
+ };
626
+ }
627
+ else {
628
+ const execEnv = execProvider === "sqlserver" ? "SQLSERVER_URL" : "DATABASE_URL";
629
+ const execConnEnv = await prompter.input({
630
+ message: `Env var for Studio execute connection URL`,
631
+ default: execEnv,
632
+ validate: validateEnvVar,
633
+ });
634
+ studioExecute = {
635
+ enabled: true,
636
+ provider: execProvider,
637
+ connectionEnv: execConnEnv,
638
+ };
639
+ }
640
+ }
641
+ else {
642
+ const execProvider = database;
643
+ if (execProvider === "sqlite") {
644
+ studioExecute = {
645
+ enabled: true,
646
+ provider: "sqlite",
647
+ sqliteFile: sqliteFile,
648
+ };
649
+ }
650
+ else {
651
+ studioExecute = {
652
+ enabled: true,
653
+ provider: execProvider,
654
+ connectionEnv: connectionEnv,
655
+ };
656
+ }
657
+ }
658
+ return {
659
+ database,
660
+ connectionEnv,
661
+ sqliteFile,
662
+ prismaSchema,
663
+ schemaOut,
664
+ aiProvider,
665
+ aiKeyEnv,
666
+ aiModelEnv: aiModelEnv.trim() || undefined,
667
+ ragStore,
668
+ pgvectorEnv,
669
+ studioExecute,
670
+ };
671
+ }
672
+ // ---------------------------------------------------------------------------
673
+ // Real Inquirer prompter
674
+ // ---------------------------------------------------------------------------
675
+ async function buildInquirerPrompter() {
676
+ const { select, input, confirm } = await import("@inquirer/prompts");
677
+ return {
678
+ select: (opts) => select({ message: opts.message, choices: opts.choices, default: opts.default }),
679
+ input: (opts) => input({ message: opts.message, default: opts.default, validate: opts.validate }),
680
+ confirm: (opts) => confirm({ message: opts.message, default: opts.default }),
681
+ };
682
+ }
683
+ // ---------------------------------------------------------------------------
684
+ // Next steps printer
685
+ // ---------------------------------------------------------------------------
686
+ function printNextSteps(answers) {
687
+ const lines = ["\nNext steps:"];
688
+ lines.push(" 1. Set env vars to match the `env(\"...\")` calls in askdb.config.ts.");
689
+ if (answers.database !== "prisma") {
690
+ lines.push(" 2. Introspect your database: askdb introspect");
691
+ }
692
+ else {
693
+ lines.push(" 2. Introspect your Prisma schema: askdb introspect");
694
+ }
695
+ if (answers.studioExecute.enabled) {
696
+ lines.push(" 3. Open Studio: askdb studio");
697
+ }
698
+ lines.push(` ${answers.studioExecute.enabled ? "4" : "3"}. Ask a question: askdb ask --question "Which customers signed up last week?"`);
699
+ process.stdout.write(lines.join("\n") + "\n");
700
+ }
701
+ // ---------------------------------------------------------------------------
702
+ // Main entry point
703
+ // ---------------------------------------------------------------------------
704
+ export async function runInitCli(argv, _installer) {
705
+ const installer = _installer ?? defaultInstaller;
192
706
  let opts;
193
707
  try {
194
708
  opts = parseOptions(argv);
@@ -198,7 +712,7 @@ export function runInitCli(argv) {
198
712
  process.stderr.write(`${msg}\n`);
199
713
  return 1;
200
714
  }
201
- if (opts.path === "--help" || opts.path === "-h") {
715
+ if (opts.path === "--help") {
202
716
  printHelp();
203
717
  return 0;
204
718
  }
@@ -209,101 +723,155 @@ export function runInitCli(argv) {
209
723
  `Use \`askdb init --force\` to overwrite.\n`);
210
724
  return 1;
211
725
  }
726
+ // Determine interactive mode
727
+ const isTTY = Boolean(process.stdin.isTTY) && Boolean(process.stdout.isTTY);
728
+ const wantInteractive = opts.interactive === true || (!opts.yes && !opts.noInteractive && isTTY);
729
+ if (opts.interactive && !isTTY) {
730
+ process.stderr.write("--interactive requires stdin and stdout to be TTYs.\n");
731
+ return 1;
732
+ }
733
+ let answers;
734
+ if (wantInteractive) {
735
+ let prompter;
736
+ try {
737
+ prompter = await buildInquirerPrompter();
738
+ }
739
+ catch {
740
+ process.stderr.write("Failed to load @inquirer/prompts. Falling back to non-interactive mode.\n");
741
+ answers = resolveDefaultInitAnswers(optsToOverrides(opts));
742
+ return await finishInit(configTarget, answers, opts, installer);
743
+ }
744
+ let wizardResult;
745
+ try {
746
+ wizardResult = await runWizard(prompter);
747
+ }
748
+ catch {
749
+ process.stderr.write("\nSetup cancelled.\n");
750
+ return 1;
751
+ }
752
+ if (!wizardResult) {
753
+ process.stderr.write("\nSetup cancelled.\n");
754
+ return 1;
755
+ }
756
+ answers = wizardResult;
757
+ // Summary and confirmation
758
+ const specs = resolveInitDepSpecs();
759
+ const plan = buildInitInstallPlan(answers, specs);
760
+ process.stdout.write("\n--- Summary ---\n");
761
+ process.stdout.write(` Config path: ${configTarget}\n`);
762
+ process.stdout.write(` Database: ${answers.database}\n`);
763
+ process.stdout.write(` AI provider: ${answers.aiProvider}\n`);
764
+ process.stdout.write(` RAG store: ${answers.ragStore}\n`);
765
+ process.stdout.write(` Studio execute: ${answers.studioExecute.enabled ? `enabled (${answers.studioExecute.provider})` : "disabled"}\n`);
766
+ if (!opts.skipInstall) {
767
+ process.stdout.write(` Packages: ${plan.labels.join(", ")}\n`);
768
+ }
769
+ process.stdout.write("---------------\n\n");
770
+ let confirmed;
771
+ try {
772
+ confirmed = await prompter.confirm({ message: "Write config and install packages?", default: true });
773
+ }
774
+ catch {
775
+ process.stderr.write("\nSetup cancelled.\n");
776
+ return 1;
777
+ }
778
+ if (!confirmed) {
779
+ process.stdout.write("Cancelled. No files written.\n");
780
+ return 1;
781
+ }
782
+ }
783
+ else {
784
+ answers = resolveDefaultInitAnswers(optsToOverrides(opts));
785
+ }
786
+ return await finishInit(configTarget, answers, opts, installer);
787
+ }
788
+ function optsToOverrides(opts) {
789
+ return {
790
+ database: opts.database,
791
+ connectionEnv: opts.connectionEnv,
792
+ sqliteFile: opts.sqliteFile,
793
+ prismaSchema: opts.prismaSchema,
794
+ schemaOut: opts.schemaOut,
795
+ aiProvider: opts.aiProvider,
796
+ aiKeyEnv: opts.aiKeyEnv,
797
+ aiModelEnv: opts.aiModelEnv,
798
+ ragStore: opts.ragStore,
799
+ pgvectorEnv: opts.pgvectorEnv,
800
+ studioExecute: opts.studioExecute,
801
+ };
802
+ }
803
+ async function finishInit(configTarget, answers, opts, installer) {
804
+ const configContent = renderInitConfig(answers);
212
805
  try {
213
- writeFileSync(configTarget, CONFIG_TEMPLATE, { encoding: "utf8" });
806
+ writeFileSync(configTarget, configContent, { encoding: "utf8" });
214
807
  }
215
808
  catch (e) {
216
809
  const msg = e instanceof Error ? e.message : String(e);
217
- process.stderr.write(`Failed to write init template: ${msg}\n`);
810
+ process.stderr.write(`Failed to write config: ${msg}\n`);
218
811
  return 1;
219
812
  }
220
813
  process.stdout.write(`Wrote:\n - ${configTarget}\n`);
221
814
  if (!opts.skipInstall) {
222
815
  const pkgRoot = findNearestPackageJsonDir(process.cwd());
223
816
  const specs = resolveInitDepSpecs();
224
- const [cArg, dArg] = formatDepArgs(specs);
817
+ const plan = buildInitInstallPlan(answers, specs);
225
818
  if (!pkgRoot) {
226
819
  process.stdout.write("\nNo package.json found in this directory or any parent directory.\n" +
227
820
  "Install the template imports yourself (from a project with a package.json):\n" +
228
- ` npm install --save ${cArg} ${dArg}\n\n`);
821
+ ` npm install --save ${plan.packages.join(" ")}\n\n`);
229
822
  }
230
823
  else if (isLikelyWorkspaceRoot(pkgRoot)) {
231
824
  process.stdout.write("\nSkipped automatic dependency install (workspace / monorepo root).\n" +
232
825
  "Add these to the package that will load this config (or run `askdb init` from that package directory):\n" +
233
- ` npm install --save ${cArg} ${dArg}\n\n`);
826
+ ` npm install --save ${plan.packages.join(" ")}\n\n`);
234
827
  }
235
828
  else {
236
829
  const pm = detectPackageManager(pkgRoot);
237
- process.stdout.write(`\nInstalling template dependencies with ${pm} in ${pkgRoot} …\n`);
238
- const ok = runPackageManagerInstall(pm, pkgRoot, specs);
830
+ process.stdout.write(`\nInstalling dependencies with ${pm} in ${pkgRoot} …\n`);
831
+ const ok = installer(pm, pkgRoot, plan.packages);
239
832
  if (!ok) {
240
- process.stderr.write("Dependency install failed. The config file was written, but @askdb/config and dotenv are not installed yet.\n" +
833
+ process.stderr.write("Dependency install failed. The config file was written, but packages are not installed yet.\n" +
241
834
  ` cd ${pkgRoot}\n` +
242
- ` ${formatManualInstallCommand(pm, specs)}\n`);
835
+ ` ${formatManualInstallCommand(pm, plan.packages)}\n`);
243
836
  return 1;
244
837
  }
245
- process.stdout.write("Installed @askdb/config and dotenv.\n");
838
+ process.stdout.write(`Installed: ${plan.labels.join(", ")}.\n`);
246
839
  }
247
840
  }
248
- process.stdout.write("\nNext steps:\n" +
249
- " 1. Create a `.env` (optional) or export variables to match the `env(\"...\")` calls in this file — see the header comment for examples.\n" +
250
- " 2. For live NL→SQL, set an OpenAI-compatible key (or use ASKDB_MOCK_SQL in tests).\n" +
251
- " 3. For a Schema v2 directory from Postgres/Prisma: `askdb introspect ...` (see --help). With MY_INTROSPECT_OUTPUT_DIR / ASKDB_INTROSPECT_OUT you can omit `--out` when a default output dir is set.\n" +
252
- " 4. With an existing schema artifact: `askdb ask --schema <path> --question \"...\"`.\n");
841
+ printNextSteps(answers);
253
842
  return 0;
254
843
  }
255
- function formatManualInstallCommand(pm, specs) {
256
- const [c, d] = formatDepArgs(specs);
257
- switch (pm) {
258
- case "pnpm":
259
- return `pnpm add ${c} ${d}`;
260
- case "yarn":
261
- return `yarn add ${c} ${d}`;
262
- case "bun":
263
- return `bun add ${c} ${d}`;
264
- default:
265
- return `npm install --save ${c} ${d}`;
266
- }
267
- }
268
- function parseOptions(argv) {
269
- const opts = { force: false, path: DEFAULT_CONFIG_PATH, skipInstall: false };
270
- for (let i = 0; i < argv.length; i += 1) {
271
- const arg = argv[i];
272
- switch (arg) {
273
- case "--force":
274
- case "-f":
275
- opts.force = true;
276
- break;
277
- case "--skip-install":
278
- opts.skipInstall = true;
279
- break;
280
- case "--path": {
281
- const value = argv[++i];
282
- if (!value || value.startsWith("--")) {
283
- throw new Error(`${arg} requires a value.`);
284
- }
285
- opts.path = value;
286
- break;
287
- }
288
- case "--help":
289
- case "-h":
290
- opts.path = "--help";
291
- return opts;
292
- default:
293
- throw new Error(`Unknown option: ${arg}`);
294
- }
295
- }
296
- return opts;
297
- }
298
844
  function printHelp() {
299
845
  process.stdout.write([
300
- "askdb init - Create askdb.config.ts (nested defineConfig template with env() examples and .env guidance in comments)",
846
+ "askdb init - Create askdb.config.ts tailored to your database and AI provider",
301
847
  "",
302
848
  "Usage:",
303
- " askdb init Create ./askdb.config.ts (refuses to overwrite unless --force)",
849
+ " askdb init Start wizard in a TTY, or use defaults in CI",
850
+ " askdb init --yes Accept defaults without prompts",
851
+ " askdb init --no-interactive Same as --yes",
304
852
  " askdb init --force Overwrite an existing askdb.config.ts",
305
- " askdb init --path <file> Write the template to a custom file path",
306
- " askdb init --skip-install Only write askdb.config.ts (do not install @askdb/config / dotenv)",
853
+ " askdb init --path <file> Write config to a custom file path",
854
+ " askdb init --skip-install Only write the file; do not install packages",
855
+ "",
856
+ "Database options:",
857
+ " --database <db> postgres|mysql|sqlite|sqlserver|prisma (default: postgres)",
858
+ " --connection-env <name> Env var name for connection URL",
859
+ " --sqlite-file <path-or-env> SQLite file path or env var name",
860
+ " --prisma-schema <path> Path to schema.prisma",
861
+ " --schema-out <dir> Schema output directory (default: ./askdb)",
862
+ "",
863
+ "AI options:",
864
+ " --ai-provider <name> openai|anthropic|google|azure|foundry (default: openai)",
865
+ " --ai-key-env <name> Env var name for API key",
866
+ " --ai-model-env <name> Env var name for model override",
867
+ "",
868
+ "RAG options:",
869
+ " --rag-store <name> file|memory|pgvector (default: file)",
870
+ " --pgvector-env <name> Env var name for pgvector URL",
871
+ "",
872
+ "Studio options:",
873
+ " --studio-execute Enable Studio execute (default: off)",
874
+ " --no-studio-execute Disable Studio execute",
307
875
  "",
308
876
  ].join("\n"));
309
877
  }