aiex-cli 0.0.1-beta.27 → 0.0.1-beta.28

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/cli.mjs CHANGED
@@ -1,24 +1,26 @@
1
- import { C as formatDoctorDiagnosticsJson, S as doctorDiagnosticsTableRows, _ as JsonSchemaDefinitionSchema, a as DEFAULT_PROMPT_CONFIG, b as generateDrizzleSchema, c as AIConfigSchema, d as description, f as name, h as createMigrationConfig, i as writeAIConfig, l as createConfig, m as version, n as getDefaultAIConfig, o as PLACEHOLDER_SCHEMA, p as package_default, r as readAIConfig, s as PLACEHOLDER_TEXT, t as collectDoctorDiagnostics, u as seedConfig, v as parseJsonSchema, y as toSnakeCase } from "./doctor-IvHYLDX6.mjs";
1
+ import { C as formatDoctorDiagnosticsJson, S as doctorDiagnosticsTableRows, _ as description, a as parseJsonSchema, b as version, c as getDefaultAIConfig, d as DEFAULT_PROMPT_CONFIG, f as PLACEHOLDER_SCHEMA, g as seedConfig, h as createConfig, i as JsonSchemaDefinitionSchema, l as readAIConfig, m as AIConfigSchema, n as createMigrationConfig, o as toSnakeCase, p as PLACEHOLDER_TEXT, s as generateDrizzleSchema, t as collectDoctorDiagnostics, u as writeAIConfig, v as name, y as package_default } from "./doctor-collector-D2q6iD_e.mjs";
2
2
  import { createRequire } from "node:module";
3
+ import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import process from "node:process";
5
- import { fileURLToPath } from "node:url";
6
6
  import { ZodError } from "zod";
7
- import fs from "node:fs/promises";
7
+ import { fileURLToPath } from "node:url";
8
8
  import { defineCommand, runMain } from "citty";
9
9
  import { consola } from "consola";
10
10
  import updateNotifier from "update-notifier";
11
11
  import CliTable3 from "cli-table3";
12
- import { intro, outro, spinner } from "@clack/prompts";
13
- import Database from "better-sqlite3";
12
+ import { intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
14
13
  import pc from "picocolors";
15
14
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
16
15
  import { LangfuseSpanProcessor } from "@langfuse/otel";
17
16
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
18
17
  import { APICallError, Output, generateText, jsonSchema } from "ai";
18
+ import fs$1 from "node:fs";
19
+ import Database from "better-sqlite3";
20
+ import picomatch from "picomatch";
19
21
  import { Buffer } from "node:buffer";
20
22
  import { extractText, getMeta } from "unpdf";
21
- import { exec, execFile } from "node:child_process";
23
+ import { execFile } from "node:child_process";
22
24
  import { promisify } from "node:util";
23
25
  import { serve } from "@hono/node-server";
24
26
  import { serveStatic } from "@hono/node-server/serve-static";
@@ -100,7 +102,7 @@ function parseAllSchemas(entries) {
100
102
  }
101
103
 
102
104
  //#endregion
103
- //#region src/commands/completion.ts
105
+ //#region src/core/completion-scripts.ts
104
106
  function bashScript(name$1) {
105
107
  return `# ${name$1} bash completion
106
108
  _${name$1}() {
@@ -127,7 +129,7 @@ function fishScript(name$1) {
127
129
  complete -c ${name$1} -f -a '(${name$1} _complete (commandline -cp) 2>/dev/null)'
128
130
  `;
129
131
  }
130
- function generateScript(name$1, shell) {
132
+ function generateCompletionScript(name$1, shell) {
131
133
  switch (shell) {
132
134
  case "bash": return bashScript(name$1);
133
135
  case "zsh": return zshScript(name$1);
@@ -135,6 +137,9 @@ function generateScript(name$1, shell) {
135
137
  default: throw new Error(`Unsupported shell: ${shell}. Use bash, zsh, or fish.`);
136
138
  }
137
139
  }
140
+
141
+ //#endregion
142
+ //#region src/commands/completion.ts
138
143
  const completionCommand = defineCommand({
139
144
  meta: {
140
145
  name: "completion",
@@ -149,7 +154,7 @@ const completionCommand = defineCommand({
149
154
  const name$1 = "aiex";
150
155
  const shell = args.shell;
151
156
  try {
152
- process.stdout.write(generateScript(name$1, shell));
157
+ process.stdout.write(generateCompletionScript(name$1, shell));
153
158
  } catch (error) {
154
159
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
155
160
  process.exit(1);
@@ -189,6 +194,14 @@ const doctorCommand = defineCommand({
189
194
  }
190
195
  });
191
196
 
197
+ //#endregion
198
+ //#region src/commands/utils.ts
199
+ function failCommand(message) {
200
+ if (message) consola.error(message);
201
+ outro("Failed!");
202
+ process.exitCode = 1;
203
+ }
204
+
192
205
  //#endregion
193
206
  //#region src/core/ai-extraction/model-capabilities.json
194
207
  var model_capabilities_default = {
@@ -12788,8 +12801,8 @@ async function withRetry(fn, onRetry, maxRetries = 5) {
12788
12801
 
12789
12802
  //#endregion
12790
12803
  //#region src/core/ai-extraction/json-utils.ts
12791
- function stripFences(text) {
12792
- const trimmed = text.trim();
12804
+ function stripFences(text$1) {
12805
+ const trimmed = text$1.trim();
12793
12806
  if (!trimmed.startsWith("```")) return null;
12794
12807
  const endIndex = trimmed.lastIndexOf("```");
12795
12808
  if (endIndex <= 3) return null;
@@ -12798,8 +12811,8 @@ function stripFences(text) {
12798
12811
  if (firstNewline === -1) return null;
12799
12812
  return inside.slice(firstNewline + 1).trim();
12800
12813
  }
12801
- function extractFirstJSON(text) {
12802
- const trimmed = text.trim();
12814
+ function extractFirstJSON(text$1) {
12815
+ const trimmed = text$1.trim();
12803
12816
  const firstBrace = trimmed.indexOf("{");
12804
12817
  const firstBracket = trimmed.indexOf("[");
12805
12818
  let start = -1;
@@ -12810,8 +12823,8 @@ function extractFirstJSON(text) {
12810
12823
  if (end <= start) return null;
12811
12824
  return trimmed.slice(start, end);
12812
12825
  }
12813
- function safeParseJSON(text) {
12814
- const cleaned = text.trim();
12826
+ function safeParseJSON(text$1) {
12827
+ const cleaned = text$1.trim();
12815
12828
  try {
12816
12829
  return JSON.parse(cleaned);
12817
12830
  } catch {}
@@ -12823,7 +12836,7 @@ function safeParseJSON(text) {
12823
12836
  if (extracted) try {
12824
12837
  return JSON.parse(extracted);
12825
12838
  } catch {}
12826
- const truncated = text.length > 200 ? `${text.slice(0, 200)}...` : text;
12839
+ const truncated = text$1.length > 200 ? `${text$1.slice(0, 200)}...` : text$1;
12827
12840
  throw new Error(`Failed to parse JSON from model output. Expected a valid JSON object or array but received unparseable text. Raw output: ${truncated}`);
12828
12841
  }
12829
12842
 
@@ -12911,11 +12924,11 @@ function schemaToDescription(schema) {
12911
12924
  }
12912
12925
  return lines.join("\n");
12913
12926
  }
12914
- function generateExtractionPrompt(schema, text, promptConfig = DEFAULT_PROMPT_CONFIG) {
12927
+ function generateExtractionPrompt(schema, text$1, promptConfig = DEFAULT_PROMPT_CONFIG) {
12915
12928
  const schemaDescription = schemaToDescription(schema);
12916
12929
  return {
12917
12930
  system: promptConfig.systemTemplate.replaceAll(PLACEHOLDER_SCHEMA, schemaDescription),
12918
- user: promptConfig.userTemplate.replaceAll(PLACEHOLDER_TEXT, text)
12931
+ user: promptConfig.userTemplate.replaceAll(PLACEHOLDER_TEXT, text$1)
12919
12932
  };
12920
12933
  }
12921
12934
  function generatePromptSnapshot(schema, promptConfig = DEFAULT_PROMPT_CONFIG) {
@@ -13098,14 +13111,14 @@ async function loadPromptSnapshot(aiexDir, tableName) {
13098
13111
  return null;
13099
13112
  }
13100
13113
  async function extractStructuredData(input) {
13101
- const { config, schema, text, aiexDir, file, modelOverride } = input;
13114
+ const { config, schema, text: text$1, aiexDir, file, modelOverride } = input;
13102
13115
  if (!config.provider.apiKey) return {
13103
13116
  success: false,
13104
13117
  error: "API Key not configured. Please configure AI settings in the web UI."
13105
13118
  };
13106
13119
  const useFileContent = !!file;
13107
13120
  const isImageFile = useFileContent && detectMimeType(file).startsWith("image/");
13108
- const inputTokens = text ? Math.ceil(text.length / 2) : void 0;
13121
+ const inputTokens = text$1 ? Math.ceil(text$1.length / 2) : void 0;
13109
13122
  const fieldCount = schema.properties ? Object.keys(schema.properties).length : 0;
13110
13123
  const outputTokens = fieldCount > 0 ? fieldCount * 80 : void 0;
13111
13124
  let selected;
@@ -13135,7 +13148,7 @@ async function extractStructuredData(input) {
13135
13148
  let system;
13136
13149
  let user;
13137
13150
  const snapshot = await loadPromptSnapshot(aiexDir, schema.table.name);
13138
- const promptText = file ? PLACEHOLDER_TEXT : text;
13151
+ const promptText = file ? PLACEHOLDER_TEXT : text$1;
13139
13152
  if (snapshot) {
13140
13153
  system = snapshot.system;
13141
13154
  user = snapshot.user.replaceAll(PLACEHOLDER_TEXT, promptText);
@@ -13152,7 +13165,7 @@ async function extractStructuredData(input) {
13152
13165
  const fileName = filePart.type === "file" ? filePart.filename : path.basename(file);
13153
13166
  const contentParts = [{
13154
13167
  type: "text",
13155
- text: user.includes(PLACEHOLDER_TEXT) ? user.replaceAll(PLACEHOLDER_TEXT, text || `Data is contained in the attached file: ${fileName}`) : user
13168
+ text: user.includes(PLACEHOLDER_TEXT) ? user.replaceAll(PLACEHOLDER_TEXT, text$1 || `Data is contained in the attached file: ${fileName}`) : user
13156
13169
  }, filePart];
13157
13170
  const fileOpts = {
13158
13171
  model: provider.chatModel(selected.name),
@@ -13389,7 +13402,7 @@ function createPdfConverter(type) {
13389
13402
  }
13390
13403
 
13391
13404
  //#endregion
13392
- //#region src/commands/extract.ts
13405
+ //#region src/core/extract-runner.ts
13393
13406
  const FILE_PART_EXTENSIONS = new Set([
13394
13407
  "png",
13395
13408
  "jpg",
@@ -13399,12 +13412,20 @@ const FILE_PART_EXTENSIONS = new Set([
13399
13412
  "bmp",
13400
13413
  "svg"
13401
13414
  ]);
13415
+ const SUPPORTED_EXTENSIONS = new Set([
13416
+ ...FILE_PART_EXTENSIONS,
13417
+ "pdf",
13418
+ "txt",
13419
+ "md",
13420
+ "csv",
13421
+ "json",
13422
+ "html",
13423
+ "xml",
13424
+ "yaml",
13425
+ "yml"
13426
+ ]);
13427
+ const JSON_EXT_RE = /\.json$/;
13402
13428
  const PDF_CONVERTER = createPdfConverter();
13403
- function fail$1(message) {
13404
- if (message) consola.error(message);
13405
- outro("Failed!");
13406
- process.exitCode = 1;
13407
- }
13408
13429
  async function ensureDatabaseReady(dbPath, schema) {
13409
13430
  try {
13410
13431
  await fs.access(dbPath);
@@ -13424,6 +13445,194 @@ async function ensureDatabaseReady(dbPath, schema) {
13424
13445
  }
13425
13446
  return null;
13426
13447
  }
13448
+ function listSupportedFiles(dir, pattern) {
13449
+ const entries = fs$1.readdirSync(dir, { withFileTypes: true });
13450
+ const files = [];
13451
+ for (const entry of entries) {
13452
+ if (entry.isDirectory()) continue;
13453
+ const ext = path.extname(entry.name).toLowerCase().replace(".", "");
13454
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
13455
+ if (pattern && !picomatch.isMatch(entry.name, pattern)) continue;
13456
+ files.push(path.join(dir, entry.name));
13457
+ }
13458
+ return files.sort();
13459
+ }
13460
+ async function loadSchema(config, schemaName) {
13461
+ const schemaPath = path.join(config.schemaPath, `${schemaName}.json`);
13462
+ try {
13463
+ const content = await fs.readFile(schemaPath, "utf-8");
13464
+ const parsed = JSON.parse(content);
13465
+ return { schema: JsonSchemaDefinitionSchema.parse(parsed) };
13466
+ } catch (e) {
13467
+ if (e instanceof ZodError) return {
13468
+ schema: null,
13469
+ error: `Schema validation failed: ${schemaName}.json\n${e.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}`
13470
+ };
13471
+ if (e.code === "ENOENT") return {
13472
+ schema: null,
13473
+ error: `Cannot read schema file: ${schemaName}.json`
13474
+ };
13475
+ if (e instanceof SyntaxError) return {
13476
+ schema: null,
13477
+ error: `Invalid JSON in schema file: ${schemaName}.json`
13478
+ };
13479
+ return {
13480
+ schema: null,
13481
+ error: String(e)
13482
+ };
13483
+ }
13484
+ }
13485
+ async function listSchemas(aiexDir) {
13486
+ try {
13487
+ const dir = path.join(aiexDir, "schema");
13488
+ return (await fs.readdir(dir)).filter((f) => f.endsWith(".json")).map((f) => f.replace(JSON_EXT_RE, "")).sort();
13489
+ } catch {
13490
+ return [];
13491
+ }
13492
+ }
13493
+ async function readExtractFileInput(filePath) {
13494
+ const ext = path.extname(filePath).toLowerCase().replace(".", "");
13495
+ if (FILE_PART_EXTENSIONS.has(ext)) return {
13496
+ text: "",
13497
+ filePath
13498
+ };
13499
+ if (ext === "pdf") {
13500
+ const buffer = await fs.readFile(filePath);
13501
+ const result = await PDF_CONVERTER.convert(buffer);
13502
+ consola.info(`Extracted ${result.pageCount} page(s) from PDF`);
13503
+ return { text: result.text };
13504
+ }
13505
+ return { text: await fs.readFile(filePath, "utf-8") };
13506
+ }
13507
+ async function extractSingle(aiexDir, config, aiConfig, schemaName, text$1, filePath, modelOverride, options) {
13508
+ const schemaLoad = await loadSchema(config, schemaName);
13509
+ if (!schemaLoad.schema) {
13510
+ if (!options?.quiet) consola.error(schemaLoad.error);
13511
+ return {
13512
+ success: false,
13513
+ error: schemaLoad.error
13514
+ };
13515
+ }
13516
+ const s = spinner();
13517
+ if (!options?.quiet) s.start(filePath ? `Extracting from ${path.basename(filePath)}...` : "Extracting data...");
13518
+ const result = await extractStructuredData({
13519
+ config: aiConfig,
13520
+ schema: schemaLoad.schema,
13521
+ text: text$1 ?? "",
13522
+ aiexDir,
13523
+ file: filePath,
13524
+ modelOverride,
13525
+ onRetry(info) {
13526
+ if (!options?.quiet) s.message(`API responded with ${info.statusCode}, retrying in ${info.delayMs / 1e3}s (${info.attempt}/${info.maxRetries})...`);
13527
+ }
13528
+ });
13529
+ if (!result.success) {
13530
+ if (!options?.quiet) {
13531
+ s.stop("Extraction failed");
13532
+ consola.error(result.error || "Unknown error");
13533
+ }
13534
+ return {
13535
+ success: false,
13536
+ error: result.error || "Unknown error"
13537
+ };
13538
+ }
13539
+ if (!options?.quiet) s.stop("Extraction complete");
13540
+ if (result.outputPath && !options?.quiet) consola.success(`Result saved: ${pc.cyan(result.outputPath)}`);
13541
+ if (result.tokensUsed && !options?.quiet) consola.info(pc.gray(`Token usage: prompt=${result.tokensUsed.prompt}, completion=${result.tokensUsed.completion}, total=${result.tokensUsed.total}`));
13542
+ if (result.data) {
13543
+ const s2 = spinner();
13544
+ if (!options?.quiet) s2.start("Inserting into database...");
13545
+ const dbError = await ensureDatabaseReady(config.databasePath, schemaLoad.schema);
13546
+ if (dbError) {
13547
+ if (!options?.quiet) s2.stop("Database not ready");
13548
+ consola.error(dbError);
13549
+ return {
13550
+ success: false,
13551
+ error: dbError
13552
+ };
13553
+ }
13554
+ try {
13555
+ const db = new Database(config.databasePath);
13556
+ try {
13557
+ const insertResult = insertExtractedData(db, schemaLoad.schema, result.data);
13558
+ if (insertResult.success) {
13559
+ if (!options?.quiet) s2.stop(`Inserted into ${insertResult.tablesInserted.length} table(s)`);
13560
+ } else {
13561
+ if (!options?.quiet) s2.stop("Database insert failed");
13562
+ consola.error(insertResult.error || "Unknown error");
13563
+ return {
13564
+ success: false,
13565
+ error: insertResult.error
13566
+ };
13567
+ }
13568
+ } finally {
13569
+ db.close();
13570
+ }
13571
+ } catch (e) {
13572
+ if (!options?.quiet) s2.stop("Database insert failed");
13573
+ consola.error(e instanceof Error ? e.message : String(e));
13574
+ return {
13575
+ success: false,
13576
+ error: String(e)
13577
+ };
13578
+ }
13579
+ }
13580
+ return { success: true };
13581
+ }
13582
+ async function processOneFile(aiexDir, config, aiConfig, schemaName, filePath, modelOverride) {
13583
+ try {
13584
+ const input = await readExtractFileInput(filePath);
13585
+ const r = await extractSingle(aiexDir, config, aiConfig, schemaName, input.text, input.filePath, modelOverride, { quiet: false });
13586
+ if (r.success) {
13587
+ consola.success(`Processed: ${path.basename(filePath)}`);
13588
+ return true;
13589
+ } else {
13590
+ consola.error(`Failed: ${r.error}`);
13591
+ return false;
13592
+ }
13593
+ } catch (e) {
13594
+ consola.error(`Error processing ${path.basename(filePath)}: ${e instanceof Error ? e.message : String(e)}`);
13595
+ return false;
13596
+ }
13597
+ }
13598
+ async function runBatchExtraction(aiexDir, config, aiConfig, schemaName, dir, globPattern, modelOverride) {
13599
+ consola.info(`Scanning ${pc.cyan(dir)} for supported files...`);
13600
+ let files;
13601
+ try {
13602
+ files = listSupportedFiles(dir, globPattern);
13603
+ } catch {
13604
+ return {
13605
+ ok: false,
13606
+ successCount: 0,
13607
+ failCount: 0,
13608
+ error: `Cannot read directory: ${dir}`
13609
+ };
13610
+ }
13611
+ if (files.length === 0) return {
13612
+ ok: false,
13613
+ successCount: 0,
13614
+ failCount: 0,
13615
+ error: `No supported files found in ${dir}`
13616
+ };
13617
+ consola.info(`Found ${files.length} file(s) to process`);
13618
+ let successCount = 0;
13619
+ let failCount = 0;
13620
+ for (let i = 0; i < files.length; i++) {
13621
+ const file = files[i];
13622
+ consola.info(`\n[${i + 1}/${files.length}] Processing: ${pc.cyan(path.basename(file))}`);
13623
+ if (await processOneFile(aiexDir, config, aiConfig, schemaName, file, modelOverride)) successCount++;
13624
+ else failCount++;
13625
+ }
13626
+ consola.info(`\nBatch complete: ${pc.green(`${successCount} succeeded`)}, ${pc.red(`${failCount} failed`)}, ${files.length} total`);
13627
+ return {
13628
+ ok: true,
13629
+ successCount,
13630
+ failCount
13631
+ };
13632
+ }
13633
+
13634
+ //#endregion
13635
+ //#region src/commands/extract.ts
13427
13636
  const extractCommand = defineCommand({
13428
13637
  meta: {
13429
13638
  name: "extract",
@@ -13433,8 +13642,7 @@ const extractCommand = defineCommand({
13433
13642
  schema: {
13434
13643
  type: "string",
13435
13644
  alias: "s",
13436
- description: "Schema name (without .json extension)",
13437
- required: true
13645
+ description: "Schema name (without .json extension)"
13438
13646
  },
13439
13647
  text: {
13440
13648
  type: "string",
@@ -13450,31 +13658,41 @@ const extractCommand = defineCommand({
13450
13658
  type: "string",
13451
13659
  alias: "m",
13452
13660
  description: "AI model to use for extraction (overrides auto-selection)"
13661
+ },
13662
+ dir: {
13663
+ type: "string",
13664
+ alias: "d",
13665
+ description: "Directory containing files to batch extract"
13666
+ },
13667
+ glob: {
13668
+ type: "string",
13669
+ alias: "g",
13670
+ description: "Glob pattern to filter files in batch mode (e.g. \"*.pdf\")"
13453
13671
  }
13454
13672
  },
13455
13673
  async run({ args }) {
13456
13674
  intro(pc.inverse(" aiex extract "));
13457
13675
  const config = createMigrationConfig(process.cwd());
13458
13676
  const aiexDir = path.dirname(config.schemaPath);
13459
- if (!args.text && !args.file) {
13460
- fail$1("Please provide text (-t) or a file (-f) to extract from");
13677
+ if (args.dir && args.text) {
13678
+ failCommand("Cannot combine -t/--text with -d/--dir");
13461
13679
  return;
13462
13680
  }
13463
- if (args.text && args.file) {
13464
- fail$1("-t and -f cannot be used together");
13681
+ if (args.dir && args.file) {
13682
+ failCommand("Cannot combine -f/--file with -d/--dir");
13465
13683
  return;
13466
13684
  }
13467
13685
  const aiConfig = await readAIConfig(aiexDir);
13468
13686
  if (!aiConfig) {
13469
- fail$1("AI configuration not found. Please configure AI settings in the Web interface first");
13687
+ failCommand("AI configuration not found. Please run \"aiex web\" to configure AI settings first");
13470
13688
  return;
13471
13689
  }
13472
13690
  if (!aiConfig.provider.apiKey) {
13473
- fail$1("API Key not configured. Please configure AI settings in the Web interface first");
13691
+ failCommand("API Key not configured. Please configure AI settings in the Web interface first");
13474
13692
  return;
13475
13693
  }
13476
13694
  if (!aiConfig.provider.models?.length) {
13477
- fail$1("No models configured. Please add at least one model in AI Settings");
13695
+ failCommand("No models configured. Please add at least one model in AI Settings");
13478
13696
  return;
13479
13697
  }
13480
13698
  let modelOverride;
@@ -13482,108 +13700,265 @@ const extractCommand = defineCommand({
13482
13700
  const matched = aiConfig.provider.models.find((m) => m.name === args.model);
13483
13701
  if (!matched) {
13484
13702
  const available = aiConfig.provider.models.map((m) => m.name).join(", ");
13485
- fail$1(`Model "${args.model}" not found in configuration. Available models: ${available}`);
13703
+ failCommand(`Model "${args.model}" not found in configuration. Available models: ${available}`);
13486
13704
  return;
13487
13705
  }
13488
13706
  modelOverride = matched;
13489
13707
  }
13490
- let text = "";
13491
- let filePath;
13492
- if (args.file) {
13493
- const ext = path.extname(args.file).toLowerCase().replace(".", "");
13494
- if (FILE_PART_EXTENSIONS.has(ext)) filePath = args.file;
13495
- else if (ext === "pdf") {
13496
- const buffer = await fs.readFile(args.file);
13497
- const result$1 = await PDF_CONVERTER.convert(buffer);
13498
- text = result$1.text;
13499
- consola.info(`Extracted ${result$1.pageCount} page(s) from PDF`);
13500
- } else text = await fs.readFile(args.file, "utf-8");
13501
- } else if (args.text) text = args.text;
13502
- const schemaName = args.schema;
13503
- const schemaPath = path.join(config.schemaPath, `${schemaName}.json`);
13504
- let schema;
13505
- try {
13506
- const content = await fs.readFile(schemaPath, "utf-8");
13507
- schema = JSON.parse(content);
13508
- } catch {
13509
- fail$1(`Cannot read schema file: ${schemaName}.json`);
13708
+ if (!args.schema && !args.text && !args.file && !args.dir) {
13709
+ if (await runInteractive(aiexDir, config, aiConfig, modelOverride)) outro("Done!");
13510
13710
  return;
13511
13711
  }
13512
- try {
13513
- schema = JsonSchemaDefinitionSchema.parse(schema);
13514
- } catch (e) {
13515
- if (e instanceof ZodError) {
13516
- consola.error(`Schema validation failed: ${schemaName}.json`);
13517
- for (const issue of e.issues) consola.error(` - ${issue.path.join(".")}: ${issue.message}`);
13712
+ if (args.dir) {
13713
+ if (!args.schema) {
13714
+ failCommand("Schema name (-s) is required in batch mode");
13715
+ return;
13518
13716
  }
13519
- fail$1();
13717
+ const result = await runBatchExtraction(aiexDir, config, aiConfig, args.schema, args.dir, args.glob, modelOverride);
13718
+ if (!result.ok) {
13719
+ failCommand(result.error);
13720
+ return;
13721
+ }
13722
+ if (result.failCount > 0) process.exitCode = 1;
13723
+ if (result.failCount > 0) outro(`Completed with failures (${result.failCount} failed)`);
13724
+ else outro("Done!");
13520
13725
  return;
13521
13726
  }
13522
- const s = spinner();
13523
- s.start(filePath ? "Extracting data from image..." : "Extracting data...");
13524
- const result = await extractStructuredData({
13525
- config: aiConfig,
13526
- schema,
13527
- text,
13528
- aiexDir,
13529
- file: filePath,
13530
- modelOverride,
13531
- onRetry(info) {
13532
- s.message(`API responded with ${info.statusCode}, retrying in ${info.delayMs / 1e3}s (${info.attempt}/${info.maxRetries})...`);
13727
+ if (!args.schema) {
13728
+ failCommand("Please provide a schema name (-s) to extract from");
13729
+ return;
13730
+ }
13731
+ if (!args.text && !args.file) {
13732
+ failCommand("Please provide text (-t) or a file (-f) to extract from");
13733
+ return;
13734
+ }
13735
+ if (args.text && args.file) {
13736
+ failCommand("-t and -f cannot be used together");
13737
+ return;
13738
+ }
13739
+ let text$1 = "";
13740
+ let filePath;
13741
+ if (args.file) try {
13742
+ const input = await readExtractFileInput(args.file);
13743
+ text$1 = input.text;
13744
+ filePath = input.filePath;
13745
+ } catch (e) {
13746
+ failCommand(`Cannot read file: ${args.file} — ${e instanceof Error ? e.message : String(e)}`);
13747
+ return;
13748
+ }
13749
+ else if (args.text) text$1 = args.text;
13750
+ if (!(await extractSingle(aiexDir, config, aiConfig, args.schema, text$1, filePath, modelOverride)).success) {
13751
+ failCommand();
13752
+ return;
13753
+ }
13754
+ outro("Done!");
13755
+ }
13756
+ });
13757
+ async function runInteractive(aiexDir, config, aiConfig, modelOverride) {
13758
+ const schemas = await listSchemas(aiexDir);
13759
+ if (schemas.length === 0) {
13760
+ failCommand(`No schema files found in ${pc.cyan(".aiex/schema/")}. Run ${pc.cyan("aiex schema --init")} first, or add JSON Schema files.`);
13761
+ return false;
13762
+ }
13763
+ const schemaName = await select({
13764
+ message: "Select a schema to extract data for:",
13765
+ options: schemas.map((s) => ({
13766
+ label: s,
13767
+ value: s
13768
+ }))
13769
+ });
13770
+ if (isCancel(schemaName)) {
13771
+ cancel("Cancelled");
13772
+ return false;
13773
+ }
13774
+ const inputSource = await select({
13775
+ message: "Choose input source:",
13776
+ options: [
13777
+ {
13778
+ label: "Text content",
13779
+ value: "text",
13780
+ hint: "Paste or type text directly"
13781
+ },
13782
+ {
13783
+ label: "Single file",
13784
+ value: "file",
13785
+ hint: "Extract from a file (txt, pdf, image)"
13786
+ },
13787
+ {
13788
+ label: "Batch directory",
13789
+ value: "dir",
13790
+ hint: "Extract all supported files in a directory"
13791
+ }
13792
+ ]
13793
+ });
13794
+ if (isCancel(inputSource)) {
13795
+ cancel("Cancelled");
13796
+ return false;
13797
+ }
13798
+ if (inputSource === "text") {
13799
+ const textContent = await text({
13800
+ message: "Enter text content to extract:",
13801
+ validate(value) {
13802
+ if (!value || value.trim().length === 0) return "Please enter some text";
13533
13803
  }
13534
13804
  });
13535
- if (!result.success) {
13536
- s.stop("Extraction failed");
13537
- fail$1(result.error || "Unknown error");
13538
- return;
13805
+ if (isCancel(textContent)) {
13806
+ cancel("Cancelled");
13807
+ return false;
13539
13808
  }
13540
- s.stop("Extraction complete");
13541
- if (result.outputPath) consola.success(`Result saved: ${pc.cyan(result.outputPath)}`);
13542
- if (result.tokensUsed) consola.info(pc.gray(`Token usage: prompt=${result.tokensUsed.prompt}, completion=${result.tokensUsed.completion}, total=${result.tokensUsed.total}`));
13543
- if (result.data) {
13544
- const s2 = spinner();
13545
- s2.start("Inserting into database...");
13546
- const dbError = await ensureDatabaseReady(config.databasePath, schema);
13547
- if (dbError) {
13548
- s2.stop("Database not ready");
13549
- fail$1(dbError);
13550
- return;
13809
+ return (await extractSingle(aiexDir, config, aiConfig, schemaName, textContent, void 0, modelOverride)).success;
13810
+ } else if (inputSource === "file") {
13811
+ const filePathStr = await text({
13812
+ message: "Enter file path:",
13813
+ validate(value) {
13814
+ if (!value || value.trim().length === 0) return "Please enter a file path";
13551
13815
  }
13552
- try {
13553
- const db = new Database(config.databasePath);
13554
- try {
13555
- const insertResult = insertExtractedData(db, schema, result.data);
13556
- if (insertResult.success) s2.stop(`Inserted into ${insertResult.tablesInserted.length} table(s)`);
13557
- else {
13558
- s2.stop("Database insert failed");
13559
- fail$1(insertResult.error || "Unknown error");
13560
- return;
13561
- }
13562
- } finally {
13563
- db.close();
13564
- }
13565
- } catch (e) {
13566
- s2.stop("Database insert failed");
13567
- fail$1(e instanceof Error ? e.message : String(e));
13568
- return;
13816
+ });
13817
+ if (isCancel(filePathStr)) {
13818
+ cancel("Cancelled");
13819
+ return false;
13820
+ }
13821
+ const fp = filePathStr;
13822
+ try {
13823
+ const input = await readExtractFileInput(fp);
13824
+ return (await extractSingle(aiexDir, config, aiConfig, schemaName, input.text, input.filePath, modelOverride)).success;
13825
+ } catch (e) {
13826
+ consola.error(`Cannot read file: ${fp} ${e instanceof Error ? e.message : String(e)}`);
13827
+ return false;
13828
+ }
13829
+ } else if (inputSource === "dir") {
13830
+ const dirPath = await text({
13831
+ message: "Enter directory path:",
13832
+ validate(value) {
13833
+ if (!value || value.trim().length === 0) return "Please enter a directory path";
13569
13834
  }
13835
+ });
13836
+ if (isCancel(dirPath)) {
13837
+ cancel("Cancelled");
13838
+ return false;
13570
13839
  }
13571
- outro("Done!");
13840
+ const result = await runBatchExtraction(aiexDir, config, aiConfig, schemaName, dirPath, void 0, modelOverride);
13841
+ if (!result.ok) failCommand(result.error);
13842
+ return result.ok && result.failCount === 0;
13572
13843
  }
13573
- });
13844
+ return false;
13845
+ }
13846
+ function cancel(msg) {
13847
+ consola.info(msg);
13848
+ outro("Cancelled");
13849
+ process.exitCode = 0;
13850
+ }
13574
13851
 
13575
13852
  //#endregion
13576
13853
  //#region schemas/table-schema.json
13577
13854
  var $id = "https://raw.githubusercontent.com/OSpoon/aiex-cli/main/app/cli/schemas/table-schema.json";
13578
13855
 
13579
13856
  //#endregion
13580
- //#region src/commands/schema.ts
13857
+ //#region src/core/schema-runner.ts
13581
13858
  const execFileAsync$1 = promisify(execFile);
13582
- function fail(message) {
13583
- if (message) consola.error(message);
13584
- outro("Failed!");
13585
- process.exitCode = 1;
13586
- }
13859
+ const EXAMPLE_SCORE_REPORT_SCHEMA = {
13860
+ $schema: $id,
13861
+ title: "ScoreReport",
13862
+ type: "object",
13863
+ table: {
13864
+ name: "score_report",
13865
+ timestamps: true
13866
+ },
13867
+ properties: {
13868
+ name: {
13869
+ type: "string",
13870
+ description: "姓名"
13871
+ },
13872
+ reportNumber: {
13873
+ type: "string",
13874
+ description: "报告编号"
13875
+ },
13876
+ gender: {
13877
+ type: "string",
13878
+ description: "性别"
13879
+ },
13880
+ printDate: {
13881
+ type: "string",
13882
+ format: "date-time",
13883
+ description: "打印日期"
13884
+ },
13885
+ examYear: {
13886
+ type: "integer",
13887
+ description: "考试年份"
13888
+ },
13889
+ examType: {
13890
+ type: "string",
13891
+ description: "考试类型,如全国统考"
13892
+ },
13893
+ examCategory: {
13894
+ type: "string",
13895
+ description: "考试类别,如普通高考"
13896
+ },
13897
+ province: {
13898
+ type: "string",
13899
+ description: "考试省份"
13900
+ },
13901
+ subjectCategory: {
13902
+ type: "string",
13903
+ description: "科类,如艺术(文)"
13904
+ },
13905
+ chinese: {
13906
+ type: "integer",
13907
+ description: "语文成绩"
13908
+ },
13909
+ chineseFull: {
13910
+ type: "integer",
13911
+ description: "语文满分"
13912
+ },
13913
+ math: {
13914
+ type: "integer",
13915
+ description: "数学成绩"
13916
+ },
13917
+ mathFull: {
13918
+ type: "integer",
13919
+ description: "数学满分"
13920
+ },
13921
+ foreignLang: {
13922
+ type: "integer",
13923
+ description: "外语成绩"
13924
+ },
13925
+ foreignLangFull: {
13926
+ type: "integer",
13927
+ description: "外语满分"
13928
+ },
13929
+ comprehensive: {
13930
+ type: "integer",
13931
+ description: "综合成绩"
13932
+ },
13933
+ comprehensiveFull: {
13934
+ type: "integer",
13935
+ description: "综合满分"
13936
+ },
13937
+ totalScore: {
13938
+ type: "integer",
13939
+ description: "总分"
13940
+ },
13941
+ totalFullScore: {
13942
+ type: "integer",
13943
+ description: "总分满分"
13944
+ },
13945
+ batchLineFirst: {
13946
+ type: "integer",
13947
+ description: "本科第一批录取分数线"
13948
+ },
13949
+ batchLineSecond: {
13950
+ type: "integer",
13951
+ description: "本科第二批录取分数线"
13952
+ }
13953
+ },
13954
+ required: [
13955
+ "name",
13956
+ "examYear",
13957
+ "examType",
13958
+ "province",
13959
+ "totalScore"
13960
+ ]
13961
+ };
13587
13962
  async function writeJsonIfAbsent(filePath, data) {
13588
13963
  try {
13589
13964
  await fs.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, { flag: "wx" });
@@ -13593,24 +13968,66 @@ async function writeJsonIfAbsent(filePath, data) {
13593
13968
  throw error;
13594
13969
  }
13595
13970
  }
13596
- async function generateFromFiles(schemaFiles, config) {
13971
+ async function initSchemaProject(config) {
13972
+ await fs.mkdir(config.schemaPath, { recursive: true });
13973
+ await fs.mkdir(path.dirname(config.drizzleSchemaPath), { recursive: true });
13974
+ await fs.mkdir(config.migrationsPath, { recursive: true });
13975
+ return { scoreReportStatus: await writeJsonIfAbsent(path.join(config.schemaPath, "score_report.json"), EXAMPLE_SCORE_REPORT_SCHEMA) };
13976
+ }
13977
+ async function listSchemaFiles(schemaDir) {
13978
+ try {
13979
+ return (await fs.readdir(schemaDir)).filter((f) => f.endsWith(".json")).map((f) => path.join(schemaDir, f)).sort();
13980
+ } catch {
13981
+ return [];
13982
+ }
13983
+ }
13984
+ async function generateSchemaFromFiles(schemaFiles, config) {
13597
13985
  const result = parseAllSchemas(await Promise.all(schemaFiles.map(async (filePath) => {
13598
13986
  return {
13599
13987
  filePath,
13600
13988
  content: await fs.readFile(filePath, "utf-8")
13601
13989
  };
13602
13990
  })));
13603
- if (!result.success) {
13604
- consola.error(result.error);
13605
- return false;
13606
- }
13607
- for (const warning of result.data.warnings) consola.warn(warning);
13991
+ if (!result.success) return {
13992
+ success: false,
13993
+ error: result.error,
13994
+ warnings: [],
13995
+ schemaCount: schemaFiles.length,
13996
+ tables: 0,
13997
+ relations: 0
13998
+ };
13999
+ const { tables, relations, reverseRelations, warnings, drizzleCode } = result.data;
13608
14000
  await fs.mkdir(path.dirname(config.drizzleSchemaPath), { recursive: true });
13609
- await fs.writeFile(config.drizzleSchemaPath, result.data.drizzleCode);
13610
- consola.success(`Generated ${pc.cyan(".aiex/drizzle/schema.ts")} from ${schemaFiles.length} schema file(s)`);
13611
- return true;
14001
+ await fs.writeFile(config.drizzleSchemaPath, drizzleCode);
14002
+ return {
14003
+ success: true,
14004
+ warnings,
14005
+ schemaCount: schemaFiles.length,
14006
+ tables: tables.length,
14007
+ relations: relations.length + reverseRelations.length
14008
+ };
13612
14009
  }
13613
- async function migrate(config, migrationName) {
14010
+ function parseMigrationOutput(stdout, stderr) {
14011
+ try {
14012
+ const jsonLine = stdout.trim().split("\n").find((l) => l.startsWith("{") && l.endsWith("}"));
14013
+ if (!jsonLine) return {
14014
+ success: false,
14015
+ error: "Migration helper did not return valid output"
14016
+ };
14017
+ const result = JSON.parse(jsonLine);
14018
+ if (!result.success) return {
14019
+ success: false,
14020
+ error: result.error || "Migration failed"
14021
+ };
14022
+ return result;
14023
+ } catch {
14024
+ return {
14025
+ success: false,
14026
+ error: stderr || stdout || "Migration helper failed"
14027
+ };
14028
+ }
14029
+ }
14030
+ async function runSchemaMigration(config, migrationName) {
13614
14031
  const helperPath = resolveHelperPath();
13615
14032
  const helperArgs = [
13616
14033
  resolveTsxPath(),
@@ -13621,28 +14038,56 @@ async function migrate(config, migrationName) {
13621
14038
  ];
13622
14039
  if (migrationName) helperArgs.push(migrationName);
13623
14040
  try {
13624
- const { stdout } = await execFileAsync$1(process.execPath, helperArgs, { cwd: process.cwd() });
13625
- const result = JSON.parse(stdout.trim());
13626
- if (!result.success) {
13627
- consola.error("Failed to generate migration");
13628
- consola.error(result.error);
13629
- return false;
13630
- }
13631
- if (result.changes === 0) {
13632
- consola.info(pc.gray("No changes detected"));
13633
- return true;
13634
- }
13635
- consola.success(pc.green("Migration files generated"));
13636
- consola.success(pc.green("Database migrated"));
13637
- return true;
14041
+ const { stdout, stderr } = await execFileAsync$1(process.execPath, helperArgs, { cwd: process.cwd() });
14042
+ return parseMigrationOutput(stdout, stderr);
13638
14043
  } catch (error) {
13639
- consola.error("Failed to generate migration");
13640
14044
  const execError = error;
13641
- if (execError.stderr) consola.error(execError.stderr);
13642
- else consola.error(execError.message || String(error));
13643
- return false;
14045
+ return {
14046
+ success: false,
14047
+ error: execError.stderr || execError.stdout || execError.message || String(error)
14048
+ };
13644
14049
  }
13645
14050
  }
14051
+ async function runSchemaSync(config, options = {}) {
14052
+ const schemaFiles = await listSchemaFiles(config.schemaPath);
14053
+ if (schemaFiles.length === 0) return {
14054
+ success: false,
14055
+ error: "No schema files found",
14056
+ warnings: [],
14057
+ schemaCount: 0,
14058
+ tables: 0,
14059
+ relations: 0
14060
+ };
14061
+ const generated = await generateSchemaFromFiles(schemaFiles, config);
14062
+ if (!generated.success) return {
14063
+ success: false,
14064
+ error: generated.error,
14065
+ warnings: generated.warnings,
14066
+ schemaCount: generated.schemaCount,
14067
+ tables: generated.tables,
14068
+ relations: generated.relations
14069
+ };
14070
+ if (options.generateOnly) return {
14071
+ success: true,
14072
+ warnings: generated.warnings,
14073
+ schemaCount: generated.schemaCount,
14074
+ tables: generated.tables,
14075
+ relations: generated.relations
14076
+ };
14077
+ const migration = await runSchemaMigration(config, options.migrationName);
14078
+ return {
14079
+ success: migration.success,
14080
+ error: migration.error,
14081
+ warnings: generated.warnings,
14082
+ schemaCount: generated.schemaCount,
14083
+ tables: generated.tables,
14084
+ relations: generated.relations,
14085
+ migration
14086
+ };
14087
+ }
14088
+
14089
+ //#endregion
14090
+ //#region src/commands/schema.ts
13646
14091
  const schemaCommand = defineCommand({
13647
14092
  meta: {
13648
14093
  name: "schema",
@@ -13670,137 +14115,28 @@ const schemaCommand = defineCommand({
13670
14115
  intro(pc.inverse(" aiex schema "));
13671
14116
  const config = createMigrationConfig(process.cwd());
13672
14117
  if (args.init) {
13673
- await fs.mkdir(config.schemaPath, { recursive: true });
13674
- await fs.mkdir(path.dirname(config.drizzleSchemaPath), { recursive: true });
13675
- await fs.mkdir(config.migrationsPath, { recursive: true });
13676
- const examReportSchema = {
13677
- $schema: $id,
13678
- title: "ScoreReport",
13679
- type: "object",
13680
- table: {
13681
- name: "score_report",
13682
- timestamps: true
13683
- },
13684
- properties: {
13685
- name: {
13686
- type: "string",
13687
- description: "姓名"
13688
- },
13689
- reportNumber: {
13690
- type: "string",
13691
- description: "报告编号"
13692
- },
13693
- gender: {
13694
- type: "string",
13695
- description: "性别"
13696
- },
13697
- printDate: {
13698
- type: "string",
13699
- format: "date-time",
13700
- description: "打印日期"
13701
- },
13702
- examYear: {
13703
- type: "integer",
13704
- description: "考试年份"
13705
- },
13706
- examType: {
13707
- type: "string",
13708
- description: "考试类型,如全国统考"
13709
- },
13710
- examCategory: {
13711
- type: "string",
13712
- description: "考试类别,如普通高考"
13713
- },
13714
- province: {
13715
- type: "string",
13716
- description: "考试省份"
13717
- },
13718
- subjectCategory: {
13719
- type: "string",
13720
- description: "科类,如艺术(文)"
13721
- },
13722
- chinese: {
13723
- type: "integer",
13724
- description: "语文成绩"
13725
- },
13726
- chineseFull: {
13727
- type: "integer",
13728
- description: "语文满分"
13729
- },
13730
- math: {
13731
- type: "integer",
13732
- description: "数学成绩"
13733
- },
13734
- mathFull: {
13735
- type: "integer",
13736
- description: "数学满分"
13737
- },
13738
- foreignLang: {
13739
- type: "integer",
13740
- description: "外语成绩"
13741
- },
13742
- foreignLangFull: {
13743
- type: "integer",
13744
- description: "外语满分"
13745
- },
13746
- comprehensive: {
13747
- type: "integer",
13748
- description: "综合成绩"
13749
- },
13750
- comprehensiveFull: {
13751
- type: "integer",
13752
- description: "综合满分"
13753
- },
13754
- totalScore: {
13755
- type: "integer",
13756
- description: "总分"
13757
- },
13758
- totalFullScore: {
13759
- type: "integer",
13760
- description: "总分满分"
13761
- },
13762
- batchLineFirst: {
13763
- type: "integer",
13764
- description: "本科第一批录取分数线"
13765
- },
13766
- batchLineSecond: {
13767
- type: "integer",
13768
- description: "本科第二批录取分数线"
13769
- }
13770
- },
13771
- required: [
13772
- "name",
13773
- "examYear",
13774
- "examType",
13775
- "province",
13776
- "totalScore"
13777
- ]
13778
- };
13779
- const examStatus = await writeJsonIfAbsent(path.join(config.schemaPath, "score_report.json"), examReportSchema);
14118
+ const initResult = await initSchemaProject(config);
13780
14119
  consola.success(`Initialized ${pc.cyan(".aiex/")} with example schemas`);
13781
- if (examStatus === "skipped") consola.warn(`${pc.cyan(".aiex/schema/score_report.json")} already exists, skipped`);
14120
+ if (initResult.scoreReportStatus === "skipped") consola.warn(`${pc.cyan(".aiex/schema/score_report.json")} already exists, skipped`);
13782
14121
  consola.info("Example includes: ScoreReport (college entrance exam score report)");
13783
14122
  outro("Run: aiex schema");
13784
14123
  return;
13785
14124
  }
13786
- let schemaFiles;
13787
- try {
13788
- schemaFiles = await fs.readdir(config.schemaPath);
13789
- schemaFiles = schemaFiles.filter((f) => f.endsWith(".json")).map((f) => path.join(config.schemaPath, f));
13790
- } catch {
13791
- schemaFiles = [];
13792
- }
14125
+ const schemaFiles = await listSchemaFiles(config.schemaPath);
13793
14126
  if (schemaFiles.length === 0) {
13794
14127
  consola.info("Use --init to initialize with an example schema");
13795
- fail(`No schema files found in ${pc.cyan(".aiex/schema/")}`);
14128
+ failCommand(`No schema files found in ${pc.cyan(".aiex/schema/")}`);
13796
14129
  return;
13797
14130
  }
13798
14131
  const s1 = spinner();
13799
14132
  s1.start("Generating Drizzle schema...");
13800
- const genOk = await generateFromFiles(schemaFiles, config);
13801
- s1.stop(genOk ? "Schema generated" : "Generation failed");
13802
- if (!genOk) {
13803
- fail();
14133
+ const generated = await generateSchemaFromFiles(schemaFiles, config);
14134
+ for (const warning of generated.warnings) consola.warn(warning);
14135
+ if (generated.success) consola.success(`Generated ${pc.cyan(".aiex/drizzle/schema.ts")} from ${generated.schemaCount} schema file(s)`);
14136
+ else if (generated.error) consola.error(generated.error);
14137
+ s1.stop(generated.success ? "Schema generated" : "Generation failed");
14138
+ if (!generated.success) {
14139
+ failCommand();
13804
14140
  return;
13805
14141
  }
13806
14142
  if (args.generate) {
@@ -13809,10 +14145,18 @@ const schemaCommand = defineCommand({
13809
14145
  }
13810
14146
  const s2 = spinner();
13811
14147
  s2.start("Running migrations...");
13812
- const migOk = await migrate(config, args.name);
13813
- s2.stop(migOk ? "Migrations applied" : "Migration failed");
13814
- if (!migOk) {
13815
- fail();
14148
+ const migration = await runSchemaMigration(config, args.name);
14149
+ if (!migration.success) {
14150
+ consola.error("Failed to generate migration");
14151
+ consola.error(migration.error || "Migration failed");
14152
+ } else if (migration.changes === 0) consola.info(pc.gray("No changes detected"));
14153
+ else {
14154
+ consola.success(pc.green("Migration files generated"));
14155
+ consola.success(pc.green("Database migrated"));
14156
+ }
14157
+ s2.stop(migration.success ? "Migrations applied" : "Migration failed");
14158
+ if (!migration.success) {
14159
+ failCommand();
13816
14160
  return;
13817
14161
  }
13818
14162
  outro("Done!");
@@ -14025,7 +14369,6 @@ function dataRoutes(config) {
14025
14369
 
14026
14370
  //#endregion
14027
14371
  //#region src/server/routes/schema.ts
14028
- const execFileAsync = promisify(execFile);
14029
14372
  const SCHEMA_FILE_RE = /^[\w.-]+\.json$/;
14030
14373
  const TABLE_NAME_RE = /^[a-z][a-z0-9_]*$/;
14031
14374
  function resolveSchemaFile(schemaDir, name$1) {
@@ -14113,59 +14456,21 @@ function schemaRoutes(config) {
14113
14456
  app.post("/migrate", async (c) => {
14114
14457
  try {
14115
14458
  await ensureDir();
14116
- await fs.mkdir(path.dirname(config.drizzleSchemaPath), { recursive: true });
14117
- const jsonFiles = (await fs.readdir(schemaDir)).filter((f) => f.endsWith(".json"));
14118
- if (jsonFiles.length === 0) return c.json({
14119
- success: false,
14120
- error: "No schema files found"
14121
- }, 400);
14122
- const parsedResult = parseAllSchemas(await Promise.all(jsonFiles.map(async (fileName) => {
14123
- const filePath = path.join(schemaDir, fileName);
14124
- return {
14125
- filePath,
14126
- content: await fs.readFile(filePath, "utf-8")
14127
- };
14128
- })));
14129
- if (!parsedResult.success) return c.json({
14130
- success: false,
14131
- error: parsedResult.error
14132
- }, 400);
14133
- const { tables, relations, reverseRelations, warnings, drizzleCode } = parsedResult.data;
14134
- await fs.writeFile(config.drizzleSchemaPath, drizzleCode);
14135
- const helperPath = resolveHelperPath();
14136
- const tsxPath = resolveTsxPath();
14137
- const { stdout, stderr } = await execFileAsync(process.execPath, [
14138
- tsxPath,
14139
- helperPath,
14140
- config.drizzleSchemaPath,
14141
- config.migrationsPath,
14142
- config.databasePath
14143
- ], { cwd: process.cwd() });
14144
- let migrationResult;
14145
- try {
14146
- const jsonLine = stdout.trim().split("\n").find((l) => l.startsWith("{") && l.endsWith("}"));
14147
- if (!jsonLine) return c.json({
14148
- success: false,
14149
- error: "Migration helper did not return valid output"
14150
- }, 500);
14151
- migrationResult = JSON.parse(jsonLine);
14152
- } catch {
14459
+ const result = await runSchemaSync(config);
14460
+ if (!result.success) {
14461
+ const status = result.schemaCount === 0 ? 400 : 500;
14153
14462
  return c.json({
14154
14463
  success: false,
14155
- error: stderr || stdout || "Migration helper failed"
14156
- }, 500);
14464
+ error: result.error || "Migration failed"
14465
+ }, status);
14157
14466
  }
14158
- if (!migrationResult.success) return c.json({
14159
- success: false,
14160
- error: migrationResult.error || "Migration failed"
14161
- }, 500);
14162
14467
  return c.json({
14163
14468
  success: true,
14164
- changes: migrationResult.changes ?? 0,
14165
- tag: migrationResult.tag,
14166
- tables: tables.length,
14167
- relations: relations.length + reverseRelations.length,
14168
- warnings
14469
+ changes: result.migration?.changes ?? 0,
14470
+ tag: result.migration?.tag,
14471
+ tables: result.tables,
14472
+ relations: result.relations,
14473
+ warnings: result.warnings
14169
14474
  });
14170
14475
  } catch (error) {
14171
14476
  return c.json({
@@ -14213,9 +14518,50 @@ function createApp(config, staticDir) {
14213
14518
  return app;
14214
14519
  }
14215
14520
 
14521
+ //#endregion
14522
+ //#region src/core/web-runner.ts
14523
+ const execFileAsync = promisify(execFile);
14524
+ function resolveWebStaticDir() {
14525
+ return path.join(resolvePackageRoot(), "dist/web");
14526
+ }
14527
+ async function openBrowser(url) {
14528
+ if (process.platform === "darwin") {
14529
+ await execFileAsync("open", [url]);
14530
+ return;
14531
+ }
14532
+ if (process.platform === "win32") {
14533
+ await execFileAsync("cmd", [
14534
+ "/c",
14535
+ "start",
14536
+ "",
14537
+ url
14538
+ ]);
14539
+ return;
14540
+ }
14541
+ await execFileAsync("xdg-open", [url]);
14542
+ }
14543
+ async function startWebServer(input) {
14544
+ const { config, port } = input;
14545
+ const staticDir = input.staticDir ?? resolveWebStaticDir();
14546
+ const url = `http://localhost:${port}`;
14547
+ serve({
14548
+ fetch: createApp(config, staticDir).fetch,
14549
+ port
14550
+ }, () => {
14551
+ input.onStarted?.({
14552
+ url,
14553
+ schemaPath: config.schemaPath
14554
+ });
14555
+ if (input.open === false) return;
14556
+ openBrowser(url).catch(() => {
14557
+ input.onOpenFailed?.(url);
14558
+ });
14559
+ });
14560
+ await new Promise(() => {});
14561
+ }
14562
+
14216
14563
  //#endregion
14217
14564
  //#region src/commands/web.ts
14218
- const execAsync = promisify(exec);
14219
14565
  const webCommand = defineCommand({
14220
14566
  meta: {
14221
14567
  name: "web",
@@ -14232,23 +14578,20 @@ const webCommand = defineCommand({
14232
14578
  const cwd = process.cwd();
14233
14579
  const port = Number(args.port) || 13e3;
14234
14580
  const config = createMigrationConfig(cwd);
14235
- const packageRoot = resolvePackageRoot();
14236
- const staticDir = path.join(packageRoot, "dist/web");
14237
14581
  const s = spinner();
14238
14582
  s.start("Starting web server...");
14239
- serve({
14240
- fetch: createApp(config, staticDir).fetch,
14241
- port
14242
- }, () => {
14243
- s.stop(`Server running at ${pc.cyan(`http://localhost:${port}`)}`);
14244
- consola.info(`Schema directory: ${pc.dim(config.schemaPath)}`);
14245
- consola.info("Press Ctrl+C to stop");
14246
- const url = `http://localhost:${port}`;
14247
- execAsync(`${process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open"} ${url}`).catch(() => {
14583
+ await startWebServer({
14584
+ config,
14585
+ port,
14586
+ onStarted(info) {
14587
+ s.stop(`Server running at ${pc.cyan(info.url)}`);
14588
+ consola.info(`Schema directory: ${pc.dim(info.schemaPath)}`);
14589
+ consola.info("Press Ctrl+C to stop");
14590
+ },
14591
+ onOpenFailed(url) {
14248
14592
  consola.warn(`Could not open browser. Visit ${url} manually.`);
14249
- });
14593
+ }
14250
14594
  });
14251
- await new Promise(() => {});
14252
14595
  }
14253
14596
  });
14254
14597