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

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,29 @@
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 doctorDiagnosticsTableRows, _ as seedConfig, a as parseJsonSchema, b as package_default, c as getDefaultAIConfig, d as DEFAULT_MINERU_CONFIG, f as DEFAULT_PROMPT_CONFIG, g as createConfig, h as AIConfigSchema, i as JsonSchemaDefinitionSchema, l as readAIConfig, m as PLACEHOLDER_TEXT, n as createMigrationConfig, o as toSnakeCase, p as PLACEHOLDER_SCHEMA, s as generateDrizzleSchema, t as collectDoctorDiagnostics, u as writeAIConfig, v as description, w as formatDoctorDiagnosticsJson, x as version, y as name } from "./doctor-collector-j2dG7dG1.mjs";
2
2
  import { createRequire } from "node:module";
3
+ import fs from "node:fs/promises";
4
+ import os from "node:os";
3
5
  import path from "node:path";
4
6
  import process from "node:process";
5
- import { fileURLToPath } from "node:url";
6
7
  import { ZodError } from "zod";
7
- import fs from "node:fs/promises";
8
+ import { fileURLToPath } from "node:url";
8
9
  import { defineCommand, runMain } from "citty";
9
10
  import { consola } from "consola";
10
11
  import updateNotifier from "update-notifier";
11
12
  import CliTable3 from "cli-table3";
12
- import { intro, outro, spinner } from "@clack/prompts";
13
- import Database from "better-sqlite3";
13
+ import { intro, isCancel, outro, select, spinner, text } from "@clack/prompts";
14
14
  import pc from "picocolors";
15
15
  import { createOpenAICompatible } from "@ai-sdk/openai-compatible";
16
16
  import { LangfuseSpanProcessor } from "@langfuse/otel";
17
17
  import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
18
18
  import { APICallError, Output, generateText, jsonSchema } from "ai";
19
+ import pRetry from "p-retry";
20
+ import fs$1 from "node:fs";
21
+ import Database from "better-sqlite3";
22
+ import picomatch from "picomatch";
23
+ import { execa } from "execa";
19
24
  import { Buffer } from "node:buffer";
20
25
  import { extractText, getMeta } from "unpdf";
21
- import { exec, execFile } from "node:child_process";
26
+ import { execFile } from "node:child_process";
22
27
  import { promisify } from "node:util";
23
28
  import { serve } from "@hono/node-server";
24
29
  import { serveStatic } from "@hono/node-server/serve-static";
@@ -100,7 +105,7 @@ function parseAllSchemas(entries) {
100
105
  }
101
106
 
102
107
  //#endregion
103
- //#region src/commands/completion.ts
108
+ //#region src/core/completion-scripts.ts
104
109
  function bashScript(name$1) {
105
110
  return `# ${name$1} bash completion
106
111
  _${name$1}() {
@@ -127,7 +132,7 @@ function fishScript(name$1) {
127
132
  complete -c ${name$1} -f -a '(${name$1} _complete (commandline -cp) 2>/dev/null)'
128
133
  `;
129
134
  }
130
- function generateScript(name$1, shell) {
135
+ function generateCompletionScript(name$1, shell) {
131
136
  switch (shell) {
132
137
  case "bash": return bashScript(name$1);
133
138
  case "zsh": return zshScript(name$1);
@@ -135,6 +140,9 @@ function generateScript(name$1, shell) {
135
140
  default: throw new Error(`Unsupported shell: ${shell}. Use bash, zsh, or fish.`);
136
141
  }
137
142
  }
143
+
144
+ //#endregion
145
+ //#region src/commands/completion.ts
138
146
  const completionCommand = defineCommand({
139
147
  meta: {
140
148
  name: "completion",
@@ -149,7 +157,7 @@ const completionCommand = defineCommand({
149
157
  const name$1 = "aiex";
150
158
  const shell = args.shell;
151
159
  try {
152
- process.stdout.write(generateScript(name$1, shell));
160
+ process.stdout.write(generateCompletionScript(name$1, shell));
153
161
  } catch (error) {
154
162
  process.stderr.write(`Error: ${error instanceof Error ? error.message : String(error)}\n`);
155
163
  process.exit(1);
@@ -189,6 +197,14 @@ const doctorCommand = defineCommand({
189
197
  }
190
198
  });
191
199
 
200
+ //#endregion
201
+ //#region src/commands/utils.ts
202
+ function failCommand(message) {
203
+ if (message) consola.error(message);
204
+ outro("Failed!");
205
+ process.exitCode = 1;
206
+ }
207
+
192
208
  //#endregion
193
209
  //#region src/core/ai-extraction/model-capabilities.json
194
210
  var model_capabilities_default = {
@@ -12767,29 +12783,31 @@ function lookupModelCapabilities(modelName) {
12767
12783
  //#endregion
12768
12784
  //#region src/utils/retry.ts
12769
12785
  async function withRetry(fn, onRetry, maxRetries = 5) {
12770
- let lastError;
12771
- for (let attempt = 0; attempt <= maxRetries; attempt++) try {
12772
- return await fn();
12773
- } catch (error) {
12774
- const err = error instanceof Error ? error : new Error(String(error));
12775
- lastError = err;
12776
- if (!(err instanceof APICallError && err.isRetryable && attempt < maxRetries)) throw err;
12777
- const delayMs = 1e3 * 2 ** attempt + Math.round(Math.random() * 500);
12778
- onRetry?.({
12779
- attempt: attempt + 1,
12780
- maxRetries,
12781
- delayMs,
12782
- statusCode: err.statusCode
12783
- });
12784
- await new Promise((resolve) => setTimeout(resolve, delayMs));
12785
- }
12786
- throw lastError ?? /* @__PURE__ */ new Error("Retry failed after all attempts");
12786
+ return pRetry(async () => fn(), {
12787
+ retries: maxRetries,
12788
+ factor: 2,
12789
+ minTimeout: 1e3,
12790
+ randomize: true,
12791
+ onFailedAttempt({ error, attemptNumber, retriesLeft }) {
12792
+ if (!(error instanceof APICallError) || !error.isRetryable || retriesLeft <= 0) return;
12793
+ const baseDelayMs = 1e3 * 2 ** (attemptNumber - 1);
12794
+ onRetry?.({
12795
+ attempt: attemptNumber,
12796
+ maxRetries,
12797
+ delayMs: baseDelayMs,
12798
+ statusCode: error.statusCode
12799
+ });
12800
+ },
12801
+ shouldRetry({ error }) {
12802
+ return error instanceof APICallError && error.isRetryable;
12803
+ }
12804
+ });
12787
12805
  }
12788
12806
 
12789
12807
  //#endregion
12790
12808
  //#region src/core/ai-extraction/json-utils.ts
12791
- function stripFences(text) {
12792
- const trimmed = text.trim();
12809
+ function stripFences(text$1) {
12810
+ const trimmed = text$1.trim();
12793
12811
  if (!trimmed.startsWith("```")) return null;
12794
12812
  const endIndex = trimmed.lastIndexOf("```");
12795
12813
  if (endIndex <= 3) return null;
@@ -12798,8 +12816,8 @@ function stripFences(text) {
12798
12816
  if (firstNewline === -1) return null;
12799
12817
  return inside.slice(firstNewline + 1).trim();
12800
12818
  }
12801
- function extractFirstJSON(text) {
12802
- const trimmed = text.trim();
12819
+ function extractFirstJSON(text$1) {
12820
+ const trimmed = text$1.trim();
12803
12821
  const firstBrace = trimmed.indexOf("{");
12804
12822
  const firstBracket = trimmed.indexOf("[");
12805
12823
  let start = -1;
@@ -12810,8 +12828,8 @@ function extractFirstJSON(text) {
12810
12828
  if (end <= start) return null;
12811
12829
  return trimmed.slice(start, end);
12812
12830
  }
12813
- function safeParseJSON(text) {
12814
- const cleaned = text.trim();
12831
+ function safeParseJSON(text$1) {
12832
+ const cleaned = text$1.trim();
12815
12833
  try {
12816
12834
  return JSON.parse(cleaned);
12817
12835
  } catch {}
@@ -12823,7 +12841,7 @@ function safeParseJSON(text) {
12823
12841
  if (extracted) try {
12824
12842
  return JSON.parse(extracted);
12825
12843
  } catch {}
12826
- const truncated = text.length > 200 ? `${text.slice(0, 200)}...` : text;
12844
+ const truncated = text$1.length > 200 ? `${text$1.slice(0, 200)}...` : text$1;
12827
12845
  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
12846
  }
12829
12847
 
@@ -12911,11 +12929,11 @@ function schemaToDescription(schema) {
12911
12929
  }
12912
12930
  return lines.join("\n");
12913
12931
  }
12914
- function generateExtractionPrompt(schema, text, promptConfig = DEFAULT_PROMPT_CONFIG) {
12932
+ function generateExtractionPrompt(schema, text$1, promptConfig = DEFAULT_PROMPT_CONFIG) {
12915
12933
  const schemaDescription = schemaToDescription(schema);
12916
12934
  return {
12917
12935
  system: promptConfig.systemTemplate.replaceAll(PLACEHOLDER_SCHEMA, schemaDescription),
12918
- user: promptConfig.userTemplate.replaceAll(PLACEHOLDER_TEXT, text)
12936
+ user: promptConfig.userTemplate.replaceAll(PLACEHOLDER_TEXT, text$1)
12919
12937
  };
12920
12938
  }
12921
12939
  function generatePromptSnapshot(schema, promptConfig = DEFAULT_PROMPT_CONFIG) {
@@ -13098,14 +13116,14 @@ async function loadPromptSnapshot(aiexDir, tableName) {
13098
13116
  return null;
13099
13117
  }
13100
13118
  async function extractStructuredData(input) {
13101
- const { config, schema, text, aiexDir, file, modelOverride } = input;
13119
+ const { config, schema, text: text$1, aiexDir, file, modelOverride } = input;
13102
13120
  if (!config.provider.apiKey) return {
13103
13121
  success: false,
13104
13122
  error: "API Key not configured. Please configure AI settings in the web UI."
13105
13123
  };
13106
13124
  const useFileContent = !!file;
13107
13125
  const isImageFile = useFileContent && detectMimeType(file).startsWith("image/");
13108
- const inputTokens = text ? Math.ceil(text.length / 2) : void 0;
13126
+ const inputTokens = text$1 ? Math.ceil(text$1.length / 2) : void 0;
13109
13127
  const fieldCount = schema.properties ? Object.keys(schema.properties).length : 0;
13110
13128
  const outputTokens = fieldCount > 0 ? fieldCount * 80 : void 0;
13111
13129
  let selected;
@@ -13135,7 +13153,7 @@ async function extractStructuredData(input) {
13135
13153
  let system;
13136
13154
  let user;
13137
13155
  const snapshot = await loadPromptSnapshot(aiexDir, schema.table.name);
13138
- const promptText = file ? PLACEHOLDER_TEXT : text;
13156
+ const promptText = file ? PLACEHOLDER_TEXT : text$1;
13139
13157
  if (snapshot) {
13140
13158
  system = snapshot.system;
13141
13159
  user = snapshot.user.replaceAll(PLACEHOLDER_TEXT, promptText);
@@ -13152,7 +13170,7 @@ async function extractStructuredData(input) {
13152
13170
  const fileName = filePart.type === "file" ? filePart.filename : path.basename(file);
13153
13171
  const contentParts = [{
13154
13172
  type: "text",
13155
- text: user.includes(PLACEHOLDER_TEXT) ? user.replaceAll(PLACEHOLDER_TEXT, text || `Data is contained in the attached file: ${fileName}`) : user
13173
+ text: user.includes(PLACEHOLDER_TEXT) ? user.replaceAll(PLACEHOLDER_TEXT, text$1 || `Data is contained in the attached file: ${fileName}`) : user
13156
13174
  }, filePart];
13157
13175
  const fileOpts = {
13158
13176
  model: provider.chatModel(selected.name),
@@ -13360,6 +13378,97 @@ async function savePromptSnapshot(schema, aiexDir) {
13360
13378
  return outputPath;
13361
13379
  }
13362
13380
 
13381
+ //#endregion
13382
+ //#region src/core/pdf-converter/external.ts
13383
+ function applyTemplate(value, context) {
13384
+ return value.replaceAll("{input}", context.input).replaceAll("{outputDir}", context.outputDir).replaceAll("{basename}", context.basename);
13385
+ }
13386
+ function isError(error) {
13387
+ return error instanceof Error;
13388
+ }
13389
+ async function pathExists(filePath) {
13390
+ try {
13391
+ await fs.access(filePath);
13392
+ return true;
13393
+ } catch {
13394
+ return false;
13395
+ }
13396
+ }
13397
+ async function collectMarkdownFiles(dir) {
13398
+ const entries = await fs.readdir(dir, { withFileTypes: true });
13399
+ const files = [];
13400
+ for (const entry of entries) {
13401
+ const entryPath = path.join(dir, entry.name);
13402
+ if (entry.isDirectory()) {
13403
+ files.push(...await collectMarkdownFiles(entryPath));
13404
+ continue;
13405
+ }
13406
+ if (entry.isFile() && entry.name.toLowerCase().endsWith(".md")) files.push(entryPath);
13407
+ }
13408
+ return files.sort();
13409
+ }
13410
+ async function selectMarkdownFile(outputDir, basename) {
13411
+ const files = await collectMarkdownFiles(outputDir);
13412
+ if (files.length === 0) throw new Error(`External PDF converter did not produce a markdown file in ${outputDir}`);
13413
+ const preferredName = `${basename}.md`.toLowerCase();
13414
+ return files.find((file) => path.basename(file).toLowerCase() === preferredName) ?? files[0];
13415
+ }
13416
+ function formatCommandError(error, command$1) {
13417
+ if (!isError(error)) return new Error(String(error));
13418
+ const details = [`External PDF converter failed: ${command$1}`];
13419
+ if ("exitCode" in error && typeof error.exitCode === "number") details.push(`exitCode=${error.exitCode}`);
13420
+ if ("signal" in error && error.signal) details.push(`signal=${String(error.signal)}`);
13421
+ if ("stderr" in error && typeof error.stderr === "string" && error.stderr.trim()) details.push(error.stderr.trim());
13422
+ else if (error.message) details.push(error.message);
13423
+ return new Error(details.join("\n"));
13424
+ }
13425
+ var ExternalCommandPdfConverter = class {
13426
+ name;
13427
+ constructor(name$1, config) {
13428
+ this.config = config;
13429
+ this.name = name$1;
13430
+ }
13431
+ async convert(input, filePath) {
13432
+ const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "aiex-pdf-"));
13433
+ const outputDir = path.join(tempRoot, "output");
13434
+ await fs.mkdir(outputDir, { recursive: true });
13435
+ const inputPath = filePath ?? path.join(tempRoot, "input.pdf");
13436
+ if (!filePath) await fs.writeFile(inputPath, input);
13437
+ const basename = path.basename(inputPath, path.extname(inputPath));
13438
+ const context = {
13439
+ input: inputPath,
13440
+ outputDir,
13441
+ basename
13442
+ };
13443
+ const args = this.config.args.map((arg) => applyTemplate(arg, context));
13444
+ const timeoutMs = (this.config.timeout ?? 600) * 1e3;
13445
+ try {
13446
+ await execa(this.config.command, args, {
13447
+ shell: false,
13448
+ timeout: timeoutMs,
13449
+ maxBuffer: 1024 * 1024 * 20
13450
+ });
13451
+ const outputPath = this.config.outputFile ? applyTemplate(this.config.outputFile, context) : await selectMarkdownFile(outputDir, basename);
13452
+ if (!await pathExists(outputPath)) throw new Error(`External PDF converter output was not found: ${outputPath}`);
13453
+ return {
13454
+ text: await fs.readFile(outputPath, "utf-8"),
13455
+ pageCount: 0,
13456
+ metadata: {
13457
+ converter: this.name,
13458
+ outputPath
13459
+ }
13460
+ };
13461
+ } catch (error) {
13462
+ throw formatCommandError(error, `${this.config.command} ${args.join(" ")}`);
13463
+ } finally {
13464
+ await fs.rm(tempRoot, {
13465
+ recursive: true,
13466
+ force: true
13467
+ });
13468
+ }
13469
+ }
13470
+ };
13471
+
13363
13472
  //#endregion
13364
13473
  //#region src/core/pdf-converter/unpdf.ts
13365
13474
  var UnpdfConverter = class {
@@ -13378,10 +13487,40 @@ var UnpdfConverter = class {
13378
13487
  //#endregion
13379
13488
  //#region src/core/pdf-converter/factory.ts
13380
13489
  const registry = /* @__PURE__ */ new Map();
13381
- function createPdfConverter(type) {
13382
- const key = type ?? "unpdf";
13490
+ var FallbackPdfConverter = class {
13491
+ name;
13492
+ constructor(primary, fallback) {
13493
+ this.primary = primary;
13494
+ this.fallback = fallback;
13495
+ this.name = primary.name;
13496
+ }
13497
+ async convert(input, filePath) {
13498
+ try {
13499
+ return await this.primary.convert(input, filePath);
13500
+ } catch {
13501
+ return await this.fallback.convert(input, filePath);
13502
+ }
13503
+ }
13504
+ };
13505
+ function withFallback(converter, config) {
13506
+ if (!config.fallbackToUnpdf) return converter;
13507
+ return new FallbackPdfConverter(converter, new UnpdfConverter());
13508
+ }
13509
+ function createPdfConverter(config) {
13510
+ if (typeof config === "object") {
13511
+ if (config.converter === "mineru") {
13512
+ const mineruConfig = config.mineru ?? DEFAULT_MINERU_CONFIG;
13513
+ return withFallback(new ExternalCommandPdfConverter("mineru", mineruConfig), mineruConfig);
13514
+ }
13515
+ if (config.converter === "external") {
13516
+ if (!config.external) throw new Error("External PDF converter is selected but no external command is configured.");
13517
+ return withFallback(new ExternalCommandPdfConverter("external", config.external), config.external);
13518
+ }
13519
+ }
13520
+ const key = typeof config === "string" ? config : "unpdf";
13383
13521
  let instance = registry.get(key);
13384
13522
  if (!instance) {
13523
+ if (key !== "unpdf") throw new Error(`PDF converter "${key}" requires configuration.`);
13385
13524
  instance = new UnpdfConverter();
13386
13525
  registry.set(key, instance);
13387
13526
  }
@@ -13389,7 +13528,7 @@ function createPdfConverter(type) {
13389
13528
  }
13390
13529
 
13391
13530
  //#endregion
13392
- //#region src/commands/extract.ts
13531
+ //#region src/core/extract-runner.ts
13393
13532
  const FILE_PART_EXTENSIONS = new Set([
13394
13533
  "png",
13395
13534
  "jpg",
@@ -13399,12 +13538,19 @@ const FILE_PART_EXTENSIONS = new Set([
13399
13538
  "bmp",
13400
13539
  "svg"
13401
13540
  ]);
13402
- const PDF_CONVERTER = createPdfConverter();
13403
- function fail$1(message) {
13404
- if (message) consola.error(message);
13405
- outro("Failed!");
13406
- process.exitCode = 1;
13407
- }
13541
+ const SUPPORTED_EXTENSIONS = new Set([
13542
+ ...FILE_PART_EXTENSIONS,
13543
+ "pdf",
13544
+ "txt",
13545
+ "md",
13546
+ "csv",
13547
+ "json",
13548
+ "html",
13549
+ "xml",
13550
+ "yaml",
13551
+ "yml"
13552
+ ]);
13553
+ const JSON_EXT_RE = /\.json$/;
13408
13554
  async function ensureDatabaseReady(dbPath, schema) {
13409
13555
  try {
13410
13556
  await fs.access(dbPath);
@@ -13424,6 +13570,194 @@ async function ensureDatabaseReady(dbPath, schema) {
13424
13570
  }
13425
13571
  return null;
13426
13572
  }
13573
+ function listSupportedFiles(dir, pattern) {
13574
+ const entries = fs$1.readdirSync(dir, { withFileTypes: true });
13575
+ const files = [];
13576
+ for (const entry of entries) {
13577
+ if (entry.isDirectory()) continue;
13578
+ const ext = path.extname(entry.name).toLowerCase().replace(".", "");
13579
+ if (!SUPPORTED_EXTENSIONS.has(ext)) continue;
13580
+ if (pattern && !picomatch.isMatch(entry.name, pattern)) continue;
13581
+ files.push(path.join(dir, entry.name));
13582
+ }
13583
+ return files.sort();
13584
+ }
13585
+ async function loadSchema(config, schemaName) {
13586
+ const schemaPath = path.join(config.schemaPath, `${schemaName}.json`);
13587
+ try {
13588
+ const content = await fs.readFile(schemaPath, "utf-8");
13589
+ const parsed = JSON.parse(content);
13590
+ return { schema: JsonSchemaDefinitionSchema.parse(parsed) };
13591
+ } catch (e) {
13592
+ if (e instanceof ZodError) return {
13593
+ schema: null,
13594
+ error: `Schema validation failed: ${schemaName}.json\n${e.issues.map((i) => ` - ${i.path.join(".")}: ${i.message}`).join("\n")}`
13595
+ };
13596
+ if (e.code === "ENOENT") return {
13597
+ schema: null,
13598
+ error: `Cannot read schema file: ${schemaName}.json`
13599
+ };
13600
+ if (e instanceof SyntaxError) return {
13601
+ schema: null,
13602
+ error: `Invalid JSON in schema file: ${schemaName}.json`
13603
+ };
13604
+ return {
13605
+ schema: null,
13606
+ error: String(e)
13607
+ };
13608
+ }
13609
+ }
13610
+ async function listSchemas(aiexDir) {
13611
+ try {
13612
+ const dir = path.join(aiexDir, "schema");
13613
+ return (await fs.readdir(dir)).filter((f) => f.endsWith(".json")).map((f) => f.replace(JSON_EXT_RE, "")).sort();
13614
+ } catch {
13615
+ return [];
13616
+ }
13617
+ }
13618
+ async function readExtractFileInput(filePath, aiConfig) {
13619
+ const ext = path.extname(filePath).toLowerCase().replace(".", "");
13620
+ if (FILE_PART_EXTENSIONS.has(ext)) return {
13621
+ text: "",
13622
+ filePath
13623
+ };
13624
+ if (ext === "pdf") {
13625
+ const buffer = await fs.readFile(filePath);
13626
+ const result = await createPdfConverter(aiConfig?.pdf).convert(buffer, filePath);
13627
+ consola.info(`Extracted ${result.pageCount} page(s) from PDF`);
13628
+ return { text: result.text };
13629
+ }
13630
+ return { text: await fs.readFile(filePath, "utf-8") };
13631
+ }
13632
+ async function extractSingle(aiexDir, config, aiConfig, schemaName, text$1, filePath, modelOverride, options) {
13633
+ const schemaLoad = await loadSchema(config, schemaName);
13634
+ if (!schemaLoad.schema) {
13635
+ if (!options?.quiet) consola.error(schemaLoad.error);
13636
+ return {
13637
+ success: false,
13638
+ error: schemaLoad.error
13639
+ };
13640
+ }
13641
+ const s = spinner();
13642
+ if (!options?.quiet) s.start(filePath ? `Extracting from ${path.basename(filePath)}...` : "Extracting data...");
13643
+ const result = await extractStructuredData({
13644
+ config: aiConfig,
13645
+ schema: schemaLoad.schema,
13646
+ text: text$1 ?? "",
13647
+ aiexDir,
13648
+ file: filePath,
13649
+ modelOverride,
13650
+ onRetry(info) {
13651
+ if (!options?.quiet) s.message(`API responded with ${info.statusCode}, retrying in ${info.delayMs / 1e3}s (${info.attempt}/${info.maxRetries})...`);
13652
+ }
13653
+ });
13654
+ if (!result.success) {
13655
+ if (!options?.quiet) {
13656
+ s.stop("Extraction failed");
13657
+ consola.error(result.error || "Unknown error");
13658
+ }
13659
+ return {
13660
+ success: false,
13661
+ error: result.error || "Unknown error"
13662
+ };
13663
+ }
13664
+ if (!options?.quiet) s.stop("Extraction complete");
13665
+ if (result.outputPath && !options?.quiet) consola.success(`Result saved: ${pc.cyan(result.outputPath)}`);
13666
+ if (result.tokensUsed && !options?.quiet) consola.info(pc.gray(`Token usage: prompt=${result.tokensUsed.prompt}, completion=${result.tokensUsed.completion}, total=${result.tokensUsed.total}`));
13667
+ if (result.data) {
13668
+ const s2 = spinner();
13669
+ if (!options?.quiet) s2.start("Inserting into database...");
13670
+ const dbError = await ensureDatabaseReady(config.databasePath, schemaLoad.schema);
13671
+ if (dbError) {
13672
+ if (!options?.quiet) s2.stop("Database not ready");
13673
+ consola.error(dbError);
13674
+ return {
13675
+ success: false,
13676
+ error: dbError
13677
+ };
13678
+ }
13679
+ try {
13680
+ const db = new Database(config.databasePath);
13681
+ try {
13682
+ const insertResult = insertExtractedData(db, schemaLoad.schema, result.data);
13683
+ if (insertResult.success) {
13684
+ if (!options?.quiet) s2.stop(`Inserted into ${insertResult.tablesInserted.length} table(s)`);
13685
+ } else {
13686
+ if (!options?.quiet) s2.stop("Database insert failed");
13687
+ consola.error(insertResult.error || "Unknown error");
13688
+ return {
13689
+ success: false,
13690
+ error: insertResult.error
13691
+ };
13692
+ }
13693
+ } finally {
13694
+ db.close();
13695
+ }
13696
+ } catch (e) {
13697
+ if (!options?.quiet) s2.stop("Database insert failed");
13698
+ consola.error(e instanceof Error ? e.message : String(e));
13699
+ return {
13700
+ success: false,
13701
+ error: String(e)
13702
+ };
13703
+ }
13704
+ }
13705
+ return { success: true };
13706
+ }
13707
+ async function processOneFile(aiexDir, config, aiConfig, schemaName, filePath, modelOverride) {
13708
+ try {
13709
+ const input = await readExtractFileInput(filePath, aiConfig);
13710
+ const r = await extractSingle(aiexDir, config, aiConfig, schemaName, input.text, input.filePath, modelOverride, { quiet: false });
13711
+ if (r.success) {
13712
+ consola.success(`Processed: ${path.basename(filePath)}`);
13713
+ return true;
13714
+ } else {
13715
+ consola.error(`Failed: ${r.error}`);
13716
+ return false;
13717
+ }
13718
+ } catch (e) {
13719
+ consola.error(`Error processing ${path.basename(filePath)}: ${e instanceof Error ? e.message : String(e)}`);
13720
+ return false;
13721
+ }
13722
+ }
13723
+ async function runBatchExtraction(aiexDir, config, aiConfig, schemaName, dir, globPattern, modelOverride) {
13724
+ consola.info(`Scanning ${pc.cyan(dir)} for supported files...`);
13725
+ let files;
13726
+ try {
13727
+ files = listSupportedFiles(dir, globPattern);
13728
+ } catch {
13729
+ return {
13730
+ ok: false,
13731
+ successCount: 0,
13732
+ failCount: 0,
13733
+ error: `Cannot read directory: ${dir}`
13734
+ };
13735
+ }
13736
+ if (files.length === 0) return {
13737
+ ok: false,
13738
+ successCount: 0,
13739
+ failCount: 0,
13740
+ error: `No supported files found in ${dir}`
13741
+ };
13742
+ consola.info(`Found ${files.length} file(s) to process`);
13743
+ let successCount = 0;
13744
+ let failCount = 0;
13745
+ for (let i = 0; i < files.length; i++) {
13746
+ const file = files[i];
13747
+ consola.info(`\n[${i + 1}/${files.length}] Processing: ${pc.cyan(path.basename(file))}`);
13748
+ if (await processOneFile(aiexDir, config, aiConfig, schemaName, file, modelOverride)) successCount++;
13749
+ else failCount++;
13750
+ }
13751
+ consola.info(`\nBatch complete: ${pc.green(`${successCount} succeeded`)}, ${pc.red(`${failCount} failed`)}, ${files.length} total`);
13752
+ return {
13753
+ ok: true,
13754
+ successCount,
13755
+ failCount
13756
+ };
13757
+ }
13758
+
13759
+ //#endregion
13760
+ //#region src/commands/extract.ts
13427
13761
  const extractCommand = defineCommand({
13428
13762
  meta: {
13429
13763
  name: "extract",
@@ -13433,8 +13767,7 @@ const extractCommand = defineCommand({
13433
13767
  schema: {
13434
13768
  type: "string",
13435
13769
  alias: "s",
13436
- description: "Schema name (without .json extension)",
13437
- required: true
13770
+ description: "Schema name (without .json extension)"
13438
13771
  },
13439
13772
  text: {
13440
13773
  type: "string",
@@ -13450,31 +13783,41 @@ const extractCommand = defineCommand({
13450
13783
  type: "string",
13451
13784
  alias: "m",
13452
13785
  description: "AI model to use for extraction (overrides auto-selection)"
13786
+ },
13787
+ dir: {
13788
+ type: "string",
13789
+ alias: "d",
13790
+ description: "Directory containing files to batch extract"
13791
+ },
13792
+ glob: {
13793
+ type: "string",
13794
+ alias: "g",
13795
+ description: "Glob pattern to filter files in batch mode (e.g. \"*.pdf\")"
13453
13796
  }
13454
13797
  },
13455
13798
  async run({ args }) {
13456
13799
  intro(pc.inverse(" aiex extract "));
13457
13800
  const config = createMigrationConfig(process.cwd());
13458
13801
  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");
13802
+ if (args.dir && args.text) {
13803
+ failCommand("Cannot combine -t/--text with -d/--dir");
13461
13804
  return;
13462
13805
  }
13463
- if (args.text && args.file) {
13464
- fail$1("-t and -f cannot be used together");
13806
+ if (args.dir && args.file) {
13807
+ failCommand("Cannot combine -f/--file with -d/--dir");
13465
13808
  return;
13466
13809
  }
13467
13810
  const aiConfig = await readAIConfig(aiexDir);
13468
13811
  if (!aiConfig) {
13469
- fail$1("AI configuration not found. Please configure AI settings in the Web interface first");
13812
+ failCommand("AI configuration not found. Please run \"aiex web\" to configure AI settings first");
13470
13813
  return;
13471
13814
  }
13472
13815
  if (!aiConfig.provider.apiKey) {
13473
- fail$1("API Key not configured. Please configure AI settings in the Web interface first");
13816
+ failCommand("API Key not configured. Please configure AI settings in the Web interface first");
13474
13817
  return;
13475
13818
  }
13476
13819
  if (!aiConfig.provider.models?.length) {
13477
- fail$1("No models configured. Please add at least one model in AI Settings");
13820
+ failCommand("No models configured. Please add at least one model in AI Settings");
13478
13821
  return;
13479
13822
  }
13480
13823
  let modelOverride;
@@ -13482,108 +13825,265 @@ const extractCommand = defineCommand({
13482
13825
  const matched = aiConfig.provider.models.find((m) => m.name === args.model);
13483
13826
  if (!matched) {
13484
13827
  const available = aiConfig.provider.models.map((m) => m.name).join(", ");
13485
- fail$1(`Model "${args.model}" not found in configuration. Available models: ${available}`);
13828
+ failCommand(`Model "${args.model}" not found in configuration. Available models: ${available}`);
13486
13829
  return;
13487
13830
  }
13488
13831
  modelOverride = matched;
13489
13832
  }
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`);
13833
+ if (!args.schema && !args.text && !args.file && !args.dir) {
13834
+ if (await runInteractive(aiexDir, config, aiConfig, modelOverride)) outro("Done!");
13510
13835
  return;
13511
13836
  }
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}`);
13837
+ if (args.dir) {
13838
+ if (!args.schema) {
13839
+ failCommand("Schema name (-s) is required in batch mode");
13840
+ return;
13841
+ }
13842
+ const result = await runBatchExtraction(aiexDir, config, aiConfig, args.schema, args.dir, args.glob, modelOverride);
13843
+ if (!result.ok) {
13844
+ failCommand(result.error);
13845
+ return;
13518
13846
  }
13519
- fail$1();
13847
+ if (result.failCount > 0) process.exitCode = 1;
13848
+ if (result.failCount > 0) outro(`Completed with failures (${result.failCount} failed)`);
13849
+ else outro("Done!");
13520
13850
  return;
13521
13851
  }
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})...`);
13852
+ if (!args.schema) {
13853
+ failCommand("Please provide a schema name (-s) to extract from");
13854
+ return;
13855
+ }
13856
+ if (!args.text && !args.file) {
13857
+ failCommand("Please provide text (-t) or a file (-f) to extract from");
13858
+ return;
13859
+ }
13860
+ if (args.text && args.file) {
13861
+ failCommand("-t and -f cannot be used together");
13862
+ return;
13863
+ }
13864
+ let text$1 = "";
13865
+ let filePath;
13866
+ if (args.file) try {
13867
+ const input = await readExtractFileInput(args.file, aiConfig);
13868
+ text$1 = input.text;
13869
+ filePath = input.filePath;
13870
+ } catch (e) {
13871
+ failCommand(`Cannot read file: ${args.file} — ${e instanceof Error ? e.message : String(e)}`);
13872
+ return;
13873
+ }
13874
+ else if (args.text) text$1 = args.text;
13875
+ if (!(await extractSingle(aiexDir, config, aiConfig, args.schema, text$1, filePath, modelOverride)).success) {
13876
+ failCommand();
13877
+ return;
13878
+ }
13879
+ outro("Done!");
13880
+ }
13881
+ });
13882
+ async function runInteractive(aiexDir, config, aiConfig, modelOverride) {
13883
+ const schemas = await listSchemas(aiexDir);
13884
+ if (schemas.length === 0) {
13885
+ failCommand(`No schema files found in ${pc.cyan(".aiex/schema/")}. Run ${pc.cyan("aiex schema --init")} first, or add JSON Schema files.`);
13886
+ return false;
13887
+ }
13888
+ const schemaName = await select({
13889
+ message: "Select a schema to extract data for:",
13890
+ options: schemas.map((s) => ({
13891
+ label: s,
13892
+ value: s
13893
+ }))
13894
+ });
13895
+ if (isCancel(schemaName)) {
13896
+ cancel("Cancelled");
13897
+ return false;
13898
+ }
13899
+ const inputSource = await select({
13900
+ message: "Choose input source:",
13901
+ options: [
13902
+ {
13903
+ label: "Text content",
13904
+ value: "text",
13905
+ hint: "Paste or type text directly"
13906
+ },
13907
+ {
13908
+ label: "Single file",
13909
+ value: "file",
13910
+ hint: "Extract from a file (txt, pdf, image)"
13911
+ },
13912
+ {
13913
+ label: "Batch directory",
13914
+ value: "dir",
13915
+ hint: "Extract all supported files in a directory"
13916
+ }
13917
+ ]
13918
+ });
13919
+ if (isCancel(inputSource)) {
13920
+ cancel("Cancelled");
13921
+ return false;
13922
+ }
13923
+ if (inputSource === "text") {
13924
+ const textContent = await text({
13925
+ message: "Enter text content to extract:",
13926
+ validate(value) {
13927
+ if (!value || value.trim().length === 0) return "Please enter some text";
13533
13928
  }
13534
13929
  });
13535
- if (!result.success) {
13536
- s.stop("Extraction failed");
13537
- fail$1(result.error || "Unknown error");
13538
- return;
13930
+ if (isCancel(textContent)) {
13931
+ cancel("Cancelled");
13932
+ return false;
13539
13933
  }
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;
13934
+ return (await extractSingle(aiexDir, config, aiConfig, schemaName, textContent, void 0, modelOverride)).success;
13935
+ } else if (inputSource === "file") {
13936
+ const filePathStr = await text({
13937
+ message: "Enter file path:",
13938
+ validate(value) {
13939
+ if (!value || value.trim().length === 0) return "Please enter a file path";
13551
13940
  }
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;
13941
+ });
13942
+ if (isCancel(filePathStr)) {
13943
+ cancel("Cancelled");
13944
+ return false;
13945
+ }
13946
+ const fp = filePathStr;
13947
+ try {
13948
+ const input = await readExtractFileInput(fp, aiConfig);
13949
+ return (await extractSingle(aiexDir, config, aiConfig, schemaName, input.text, input.filePath, modelOverride)).success;
13950
+ } catch (e) {
13951
+ consola.error(`Cannot read file: ${fp} ${e instanceof Error ? e.message : String(e)}`);
13952
+ return false;
13953
+ }
13954
+ } else if (inputSource === "dir") {
13955
+ const dirPath = await text({
13956
+ message: "Enter directory path:",
13957
+ validate(value) {
13958
+ if (!value || value.trim().length === 0) return "Please enter a directory path";
13569
13959
  }
13960
+ });
13961
+ if (isCancel(dirPath)) {
13962
+ cancel("Cancelled");
13963
+ return false;
13570
13964
  }
13571
- outro("Done!");
13965
+ const result = await runBatchExtraction(aiexDir, config, aiConfig, schemaName, dirPath, void 0, modelOverride);
13966
+ if (!result.ok) failCommand(result.error);
13967
+ return result.ok && result.failCount === 0;
13572
13968
  }
13573
- });
13969
+ return false;
13970
+ }
13971
+ function cancel(msg) {
13972
+ consola.info(msg);
13973
+ outro("Cancelled");
13974
+ process.exitCode = 0;
13975
+ }
13574
13976
 
13575
13977
  //#endregion
13576
13978
  //#region schemas/table-schema.json
13577
13979
  var $id = "https://raw.githubusercontent.com/OSpoon/aiex-cli/main/app/cli/schemas/table-schema.json";
13578
13980
 
13579
13981
  //#endregion
13580
- //#region src/commands/schema.ts
13982
+ //#region src/core/schema-runner.ts
13581
13983
  const execFileAsync$1 = promisify(execFile);
13582
- function fail(message) {
13583
- if (message) consola.error(message);
13584
- outro("Failed!");
13585
- process.exitCode = 1;
13586
- }
13984
+ const EXAMPLE_SCORE_REPORT_SCHEMA = {
13985
+ $schema: $id,
13986
+ title: "ScoreReport",
13987
+ type: "object",
13988
+ table: {
13989
+ name: "score_report",
13990
+ timestamps: true
13991
+ },
13992
+ properties: {
13993
+ name: {
13994
+ type: "string",
13995
+ description: "姓名"
13996
+ },
13997
+ reportNumber: {
13998
+ type: "string",
13999
+ description: "报告编号"
14000
+ },
14001
+ gender: {
14002
+ type: "string",
14003
+ description: "性别"
14004
+ },
14005
+ printDate: {
14006
+ type: "string",
14007
+ format: "date-time",
14008
+ description: "打印日期"
14009
+ },
14010
+ examYear: {
14011
+ type: "integer",
14012
+ description: "考试年份"
14013
+ },
14014
+ examType: {
14015
+ type: "string",
14016
+ description: "考试类型,如全国统考"
14017
+ },
14018
+ examCategory: {
14019
+ type: "string",
14020
+ description: "考试类别,如普通高考"
14021
+ },
14022
+ province: {
14023
+ type: "string",
14024
+ description: "考试省份"
14025
+ },
14026
+ subjectCategory: {
14027
+ type: "string",
14028
+ description: "科类,如艺术(文)"
14029
+ },
14030
+ chinese: {
14031
+ type: "integer",
14032
+ description: "语文成绩"
14033
+ },
14034
+ chineseFull: {
14035
+ type: "integer",
14036
+ description: "语文满分"
14037
+ },
14038
+ math: {
14039
+ type: "integer",
14040
+ description: "数学成绩"
14041
+ },
14042
+ mathFull: {
14043
+ type: "integer",
14044
+ description: "数学满分"
14045
+ },
14046
+ foreignLang: {
14047
+ type: "integer",
14048
+ description: "外语成绩"
14049
+ },
14050
+ foreignLangFull: {
14051
+ type: "integer",
14052
+ description: "外语满分"
14053
+ },
14054
+ comprehensive: {
14055
+ type: "integer",
14056
+ description: "综合成绩"
14057
+ },
14058
+ comprehensiveFull: {
14059
+ type: "integer",
14060
+ description: "综合满分"
14061
+ },
14062
+ totalScore: {
14063
+ type: "integer",
14064
+ description: "总分"
14065
+ },
14066
+ totalFullScore: {
14067
+ type: "integer",
14068
+ description: "总分满分"
14069
+ },
14070
+ batchLineFirst: {
14071
+ type: "integer",
14072
+ description: "本科第一批录取分数线"
14073
+ },
14074
+ batchLineSecond: {
14075
+ type: "integer",
14076
+ description: "本科第二批录取分数线"
14077
+ }
14078
+ },
14079
+ required: [
14080
+ "name",
14081
+ "examYear",
14082
+ "examType",
14083
+ "province",
14084
+ "totalScore"
14085
+ ]
14086
+ };
13587
14087
  async function writeJsonIfAbsent(filePath, data) {
13588
14088
  try {
13589
14089
  await fs.writeFile(filePath, `${JSON.stringify(data, null, 2)}\n`, { flag: "wx" });
@@ -13593,24 +14093,66 @@ async function writeJsonIfAbsent(filePath, data) {
13593
14093
  throw error;
13594
14094
  }
13595
14095
  }
13596
- async function generateFromFiles(schemaFiles, config) {
14096
+ async function initSchemaProject(config) {
14097
+ await fs.mkdir(config.schemaPath, { recursive: true });
14098
+ await fs.mkdir(path.dirname(config.drizzleSchemaPath), { recursive: true });
14099
+ await fs.mkdir(config.migrationsPath, { recursive: true });
14100
+ return { scoreReportStatus: await writeJsonIfAbsent(path.join(config.schemaPath, "score_report.json"), EXAMPLE_SCORE_REPORT_SCHEMA) };
14101
+ }
14102
+ async function listSchemaFiles(schemaDir) {
14103
+ try {
14104
+ return (await fs.readdir(schemaDir)).filter((f) => f.endsWith(".json")).map((f) => path.join(schemaDir, f)).sort();
14105
+ } catch {
14106
+ return [];
14107
+ }
14108
+ }
14109
+ async function generateSchemaFromFiles(schemaFiles, config) {
13597
14110
  const result = parseAllSchemas(await Promise.all(schemaFiles.map(async (filePath) => {
13598
14111
  return {
13599
14112
  filePath,
13600
14113
  content: await fs.readFile(filePath, "utf-8")
13601
14114
  };
13602
14115
  })));
13603
- if (!result.success) {
13604
- consola.error(result.error);
13605
- return false;
13606
- }
13607
- for (const warning of result.data.warnings) consola.warn(warning);
14116
+ if (!result.success) return {
14117
+ success: false,
14118
+ error: result.error,
14119
+ warnings: [],
14120
+ schemaCount: schemaFiles.length,
14121
+ tables: 0,
14122
+ relations: 0
14123
+ };
14124
+ const { tables, relations, reverseRelations, warnings, drizzleCode } = result.data;
13608
14125
  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;
14126
+ await fs.writeFile(config.drizzleSchemaPath, drizzleCode);
14127
+ return {
14128
+ success: true,
14129
+ warnings,
14130
+ schemaCount: schemaFiles.length,
14131
+ tables: tables.length,
14132
+ relations: relations.length + reverseRelations.length
14133
+ };
14134
+ }
14135
+ function parseMigrationOutput(stdout, stderr) {
14136
+ try {
14137
+ const jsonLine = stdout.trim().split("\n").find((l) => l.startsWith("{") && l.endsWith("}"));
14138
+ if (!jsonLine) return {
14139
+ success: false,
14140
+ error: "Migration helper did not return valid output"
14141
+ };
14142
+ const result = JSON.parse(jsonLine);
14143
+ if (!result.success) return {
14144
+ success: false,
14145
+ error: result.error || "Migration failed"
14146
+ };
14147
+ return result;
14148
+ } catch {
14149
+ return {
14150
+ success: false,
14151
+ error: stderr || stdout || "Migration helper failed"
14152
+ };
14153
+ }
13612
14154
  }
13613
- async function migrate(config, migrationName) {
14155
+ async function runSchemaMigration(config, migrationName) {
13614
14156
  const helperPath = resolveHelperPath();
13615
14157
  const helperArgs = [
13616
14158
  resolveTsxPath(),
@@ -13621,28 +14163,56 @@ async function migrate(config, migrationName) {
13621
14163
  ];
13622
14164
  if (migrationName) helperArgs.push(migrationName);
13623
14165
  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;
14166
+ const { stdout, stderr } = await execFileAsync$1(process.execPath, helperArgs, { cwd: process.cwd() });
14167
+ return parseMigrationOutput(stdout, stderr);
13638
14168
  } catch (error) {
13639
- consola.error("Failed to generate migration");
13640
14169
  const execError = error;
13641
- if (execError.stderr) consola.error(execError.stderr);
13642
- else consola.error(execError.message || String(error));
13643
- return false;
14170
+ return {
14171
+ success: false,
14172
+ error: execError.stderr || execError.stdout || execError.message || String(error)
14173
+ };
13644
14174
  }
13645
14175
  }
14176
+ async function runSchemaSync(config, options = {}) {
14177
+ const schemaFiles = await listSchemaFiles(config.schemaPath);
14178
+ if (schemaFiles.length === 0) return {
14179
+ success: false,
14180
+ error: "No schema files found",
14181
+ warnings: [],
14182
+ schemaCount: 0,
14183
+ tables: 0,
14184
+ relations: 0
14185
+ };
14186
+ const generated = await generateSchemaFromFiles(schemaFiles, config);
14187
+ if (!generated.success) return {
14188
+ success: false,
14189
+ error: generated.error,
14190
+ warnings: generated.warnings,
14191
+ schemaCount: generated.schemaCount,
14192
+ tables: generated.tables,
14193
+ relations: generated.relations
14194
+ };
14195
+ if (options.generateOnly) return {
14196
+ success: true,
14197
+ warnings: generated.warnings,
14198
+ schemaCount: generated.schemaCount,
14199
+ tables: generated.tables,
14200
+ relations: generated.relations
14201
+ };
14202
+ const migration = await runSchemaMigration(config, options.migrationName);
14203
+ return {
14204
+ success: migration.success,
14205
+ error: migration.error,
14206
+ warnings: generated.warnings,
14207
+ schemaCount: generated.schemaCount,
14208
+ tables: generated.tables,
14209
+ relations: generated.relations,
14210
+ migration
14211
+ };
14212
+ }
14213
+
14214
+ //#endregion
14215
+ //#region src/commands/schema.ts
13646
14216
  const schemaCommand = defineCommand({
13647
14217
  meta: {
13648
14218
  name: "schema",
@@ -13670,137 +14240,28 @@ const schemaCommand = defineCommand({
13670
14240
  intro(pc.inverse(" aiex schema "));
13671
14241
  const config = createMigrationConfig(process.cwd());
13672
14242
  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);
14243
+ const initResult = await initSchemaProject(config);
13780
14244
  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`);
14245
+ if (initResult.scoreReportStatus === "skipped") consola.warn(`${pc.cyan(".aiex/schema/score_report.json")} already exists, skipped`);
13782
14246
  consola.info("Example includes: ScoreReport (college entrance exam score report)");
13783
14247
  outro("Run: aiex schema");
13784
14248
  return;
13785
14249
  }
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
- }
14250
+ const schemaFiles = await listSchemaFiles(config.schemaPath);
13793
14251
  if (schemaFiles.length === 0) {
13794
14252
  consola.info("Use --init to initialize with an example schema");
13795
- fail(`No schema files found in ${pc.cyan(".aiex/schema/")}`);
14253
+ failCommand(`No schema files found in ${pc.cyan(".aiex/schema/")}`);
13796
14254
  return;
13797
14255
  }
13798
14256
  const s1 = spinner();
13799
14257
  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();
14258
+ const generated = await generateSchemaFromFiles(schemaFiles, config);
14259
+ for (const warning of generated.warnings) consola.warn(warning);
14260
+ if (generated.success) consola.success(`Generated ${pc.cyan(".aiex/drizzle/schema.ts")} from ${generated.schemaCount} schema file(s)`);
14261
+ else if (generated.error) consola.error(generated.error);
14262
+ s1.stop(generated.success ? "Schema generated" : "Generation failed");
14263
+ if (!generated.success) {
14264
+ failCommand();
13804
14265
  return;
13805
14266
  }
13806
14267
  if (args.generate) {
@@ -13809,10 +14270,18 @@ const schemaCommand = defineCommand({
13809
14270
  }
13810
14271
  const s2 = spinner();
13811
14272
  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();
14273
+ const migration = await runSchemaMigration(config, args.name);
14274
+ if (!migration.success) {
14275
+ consola.error("Failed to generate migration");
14276
+ consola.error(migration.error || "Migration failed");
14277
+ } else if (migration.changes === 0) consola.info(pc.gray("No changes detected"));
14278
+ else {
14279
+ consola.success(pc.green("Migration files generated"));
14280
+ consola.success(pc.green("Database migrated"));
14281
+ }
14282
+ s2.stop(migration.success ? "Migrations applied" : "Migration failed");
14283
+ if (!migration.success) {
14284
+ failCommand();
13816
14285
  return;
13817
14286
  }
13818
14287
  outro("Done!");
@@ -14025,7 +14494,6 @@ function dataRoutes(config) {
14025
14494
 
14026
14495
  //#endregion
14027
14496
  //#region src/server/routes/schema.ts
14028
- const execFileAsync = promisify(execFile);
14029
14497
  const SCHEMA_FILE_RE = /^[\w.-]+\.json$/;
14030
14498
  const TABLE_NAME_RE = /^[a-z][a-z0-9_]*$/;
14031
14499
  function resolveSchemaFile(schemaDir, name$1) {
@@ -14113,59 +14581,21 @@ function schemaRoutes(config) {
14113
14581
  app.post("/migrate", async (c) => {
14114
14582
  try {
14115
14583
  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 {
14584
+ const result = await runSchemaSync(config);
14585
+ if (!result.success) {
14586
+ const status = result.schemaCount === 0 ? 400 : 500;
14153
14587
  return c.json({
14154
14588
  success: false,
14155
- error: stderr || stdout || "Migration helper failed"
14156
- }, 500);
14589
+ error: result.error || "Migration failed"
14590
+ }, status);
14157
14591
  }
14158
- if (!migrationResult.success) return c.json({
14159
- success: false,
14160
- error: migrationResult.error || "Migration failed"
14161
- }, 500);
14162
14592
  return c.json({
14163
14593
  success: true,
14164
- changes: migrationResult.changes ?? 0,
14165
- tag: migrationResult.tag,
14166
- tables: tables.length,
14167
- relations: relations.length + reverseRelations.length,
14168
- warnings
14594
+ changes: result.migration?.changes ?? 0,
14595
+ tag: result.migration?.tag,
14596
+ tables: result.tables,
14597
+ relations: result.relations,
14598
+ warnings: result.warnings
14169
14599
  });
14170
14600
  } catch (error) {
14171
14601
  return c.json({
@@ -14213,9 +14643,50 @@ function createApp(config, staticDir) {
14213
14643
  return app;
14214
14644
  }
14215
14645
 
14646
+ //#endregion
14647
+ //#region src/core/web-runner.ts
14648
+ const execFileAsync = promisify(execFile);
14649
+ function resolveWebStaticDir() {
14650
+ return path.join(resolvePackageRoot(), "dist/web");
14651
+ }
14652
+ async function openBrowser(url) {
14653
+ if (process.platform === "darwin") {
14654
+ await execFileAsync("open", [url]);
14655
+ return;
14656
+ }
14657
+ if (process.platform === "win32") {
14658
+ await execFileAsync("cmd", [
14659
+ "/c",
14660
+ "start",
14661
+ "",
14662
+ url
14663
+ ]);
14664
+ return;
14665
+ }
14666
+ await execFileAsync("xdg-open", [url]);
14667
+ }
14668
+ async function startWebServer(input) {
14669
+ const { config, port } = input;
14670
+ const staticDir = input.staticDir ?? resolveWebStaticDir();
14671
+ const url = `http://localhost:${port}`;
14672
+ serve({
14673
+ fetch: createApp(config, staticDir).fetch,
14674
+ port
14675
+ }, () => {
14676
+ input.onStarted?.({
14677
+ url,
14678
+ schemaPath: config.schemaPath
14679
+ });
14680
+ if (input.open === false) return;
14681
+ openBrowser(url).catch(() => {
14682
+ input.onOpenFailed?.(url);
14683
+ });
14684
+ });
14685
+ await new Promise(() => {});
14686
+ }
14687
+
14216
14688
  //#endregion
14217
14689
  //#region src/commands/web.ts
14218
- const execAsync = promisify(exec);
14219
14690
  const webCommand = defineCommand({
14220
14691
  meta: {
14221
14692
  name: "web",
@@ -14232,23 +14703,20 @@ const webCommand = defineCommand({
14232
14703
  const cwd = process.cwd();
14233
14704
  const port = Number(args.port) || 13e3;
14234
14705
  const config = createMigrationConfig(cwd);
14235
- const packageRoot = resolvePackageRoot();
14236
- const staticDir = path.join(packageRoot, "dist/web");
14237
14706
  const s = spinner();
14238
14707
  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(() => {
14708
+ await startWebServer({
14709
+ config,
14710
+ port,
14711
+ onStarted(info) {
14712
+ s.stop(`Server running at ${pc.cyan(info.url)}`);
14713
+ consola.info(`Schema directory: ${pc.dim(info.schemaPath)}`);
14714
+ consola.info("Press Ctrl+C to stop");
14715
+ },
14716
+ onOpenFailed(url) {
14248
14717
  consola.warn(`Could not open browser. Visit ${url} manually.`);
14249
- });
14718
+ }
14250
14719
  });
14251
- await new Promise(() => {});
14252
14720
  }
14253
14721
  });
14254
14722