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/README.md +13 -3
- package/dist/cli.mjs +819 -351
- package/dist/core/schema-sqlite/migrate-helper.mjs +16 -29
- package/dist/{doctor-IvHYLDX6.mjs → doctor-collector-j2dG7dG1.mjs} +270 -231
- package/dist/index.d.mts +14 -15
- package/dist/index.mjs +1 -1
- package/dist/web/assets/AISettings-Dn58ZHhM.js +339 -0
- package/dist/web/assets/DataBrowser-B6WECCZM.js +6 -0
- package/dist/web/assets/{ExtractionViewer-WMUdXeyU.js → ExtractionViewer-CTS1RXzc.js} +1 -1
- package/dist/web/assets/JsonSchemaEditor-Dpgu6HPz.js +570 -0
- package/dist/web/assets/api-client-CbQEkaKT.js +1 -0
- package/dist/web/assets/dialog-CUkPLPNP.js +109 -0
- package/dist/web/assets/{index-CuOQk7nB.js → index-g2pWXPQZ.js} +38 -38
- package/dist/web/assets/object-utils-DPPzLQjH.js +1 -0
- package/dist/web/assets/select-DyjIzt-v.js +439 -0
- package/dist/web/index.html +5 -5
- package/package.json +7 -1
- package/src/core/schema-sqlite/migrate-helper.ts +15 -40
- package/dist/web/assets/AISettings-DFi-nXIi.js +0 -334
- package/dist/web/assets/DataBrowser-BWSX8O2h.js +0 -5
- package/dist/web/assets/JsonSchemaEditor-B57coz1O.js +0 -929
- package/dist/web/assets/api-client-By2rWtpv.js +0 -1
- package/dist/web/assets/dialog-dMXSeJQQ.js +0 -108
- package/dist/web/assets/overlayeventbus-CRKW6UCj.js +0 -80
- package/dist/web/assets/table-schema-C90NJyfq.js +0 -2
- /package/dist/web/assets/{runtime-dom.esm-bundler-DmdkgxQM.js → runtime-dom.esm-bundler-ei_N7Xjw.js} +0 -0
package/dist/cli.mjs
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
|
-
import { C as
|
|
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
|
|
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 {
|
|
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/
|
|
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
|
|
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(
|
|
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
|
-
|
|
12771
|
-
|
|
12772
|
-
|
|
12773
|
-
|
|
12774
|
-
|
|
12775
|
-
|
|
12776
|
-
|
|
12777
|
-
|
|
12778
|
-
|
|
12779
|
-
|
|
12780
|
-
|
|
12781
|
-
|
|
12782
|
-
|
|
12783
|
-
|
|
12784
|
-
|
|
12785
|
-
|
|
12786
|
-
|
|
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
|
-
|
|
13382
|
-
|
|
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/
|
|
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
|
|
13403
|
-
|
|
13404
|
-
|
|
13405
|
-
|
|
13406
|
-
|
|
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 (
|
|
13460
|
-
|
|
13802
|
+
if (args.dir && args.text) {
|
|
13803
|
+
failCommand("Cannot combine -t/--text with -d/--dir");
|
|
13461
13804
|
return;
|
|
13462
13805
|
}
|
|
13463
|
-
if (args.
|
|
13464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13828
|
+
failCommand(`Model "${args.model}" not found in configuration. Available models: ${available}`);
|
|
13486
13829
|
return;
|
|
13487
13830
|
}
|
|
13488
13831
|
modelOverride = matched;
|
|
13489
13832
|
}
|
|
13490
|
-
|
|
13491
|
-
|
|
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
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
text
|
|
13528
|
-
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
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 (
|
|
13536
|
-
|
|
13537
|
-
|
|
13538
|
-
return;
|
|
13930
|
+
if (isCancel(textContent)) {
|
|
13931
|
+
cancel("Cancelled");
|
|
13932
|
+
return false;
|
|
13539
13933
|
}
|
|
13540
|
-
|
|
13541
|
-
|
|
13542
|
-
|
|
13543
|
-
|
|
13544
|
-
|
|
13545
|
-
|
|
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
|
-
|
|
13553
|
-
|
|
13554
|
-
|
|
13555
|
-
|
|
13556
|
-
|
|
13557
|
-
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13561
|
-
|
|
13562
|
-
|
|
13563
|
-
|
|
13564
|
-
|
|
13565
|
-
|
|
13566
|
-
|
|
13567
|
-
|
|
13568
|
-
|
|
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
|
-
|
|
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/
|
|
13982
|
+
//#region src/core/schema-runner.ts
|
|
13581
13983
|
const execFileAsync$1 = promisify(execFile);
|
|
13582
|
-
|
|
13583
|
-
|
|
13584
|
-
|
|
13585
|
-
|
|
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
|
|
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
|
-
|
|
13605
|
-
|
|
13606
|
-
|
|
13607
|
-
|
|
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,
|
|
13610
|
-
|
|
13611
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
13642
|
-
|
|
13643
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
13801
|
-
|
|
13802
|
-
if (
|
|
13803
|
-
|
|
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
|
|
13813
|
-
|
|
13814
|
-
|
|
13815
|
-
|
|
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
|
|
14117
|
-
|
|
14118
|
-
|
|
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:
|
|
14156
|
-
},
|
|
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:
|
|
14165
|
-
tag:
|
|
14166
|
-
tables: tables
|
|
14167
|
-
relations: relations
|
|
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
|
-
|
|
14240
|
-
|
|
14241
|
-
port
|
|
14242
|
-
|
|
14243
|
-
|
|
14244
|
-
|
|
14245
|
-
|
|
14246
|
-
|
|
14247
|
-
|
|
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
|
|