@atlisp/cli 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/.github/workflows/publish.yml +18 -0
  2. package/.github/workflows/test.yml +14 -0
  3. package/README.md +88 -0
  4. package/__tests__/__snapshots__/cli.test.js.snap +12 -0
  5. package/__tests__/build.test.js +48 -0
  6. package/__tests__/cli.test.js +246 -0
  7. package/__tests__/config.test.js +71 -0
  8. package/__tests__/deps.test.js +64 -0
  9. package/__tests__/index.test.js +63 -0
  10. package/__tests__/lint.test.js +58 -0
  11. package/atlisp.js +219 -0
  12. package/commands/batch.js +14 -0
  13. package/commands/build.js +267 -0
  14. package/commands/checksum.js +23 -0
  15. package/commands/config.js +264 -0
  16. package/commands/dcl-migrate.js +62 -0
  17. package/commands/doc.js +45 -0
  18. package/commands/doctor.js +292 -0
  19. package/commands/find.js +14 -0
  20. package/commands/index.js +33 -0
  21. package/commands/info.js +46 -0
  22. package/commands/init.js +202 -0
  23. package/commands/install.js +23 -0
  24. package/commands/lint.js +79 -0
  25. package/commands/list.js +99 -0
  26. package/commands/publish.js +237 -0
  27. package/commands/pull.js +118 -0
  28. package/commands/qrcode.js +33 -0
  29. package/commands/test.js +223 -0
  30. package/commands/watch.js +47 -0
  31. package/completion/atlisp-completion.bash +107 -0
  32. package/completion/atlisp-completion.ps1 +64 -0
  33. package/index.js +13 -0
  34. package/kernel-order.json +28 -0
  35. package/lib/atlisp.js +15 -0
  36. package/lib/batch.js +173 -0
  37. package/lib/cad.js +199 -0
  38. package/lib/common.js +189 -0
  39. package/lib/deps.js +121 -0
  40. package/lib/docgen.js +93 -0
  41. package/lib/find.js +154 -0
  42. package/lib/mcp-client.js +203 -0
  43. package/package.json +49 -0
  44. package/scripts/add-deps.js +81 -0
  45. package/scripts/fix-deps.js +98 -0
package/lib/common.js ADDED
@@ -0,0 +1,189 @@
1
+ const http = require("http");
2
+ const https = require("https");
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const os = require("os");
6
+
7
+ const API_BASE = "http://atlisp.cn";
8
+ const ATLISP_DIR = path.join(os.homedir(), ".atlisp");
9
+ const PKGS_DIR = path.join(os.homedir(), "@lisp", "packages");
10
+
11
+ const KERNEL_DEFAULT = path.join(os.homedir(), "atlisp", "kernel");
12
+ const SRC_DIR = process.env.ATLISP_KERNEL_SRC || path.join(KERNEL_DEFAULT, "src");
13
+ const TEST_DIR = process.env.ATLISP_KERNEL_TEST || path.join(KERNEL_DEFAULT, "test");
14
+ const KERNEL_ROOT = process.env.ATLISP_KERNEL_ROOT || KERNEL_DEFAULT;
15
+
16
+ function getKernelSrc() { return SRC_DIR; }
17
+ function getKernelTest() { return TEST_DIR; }
18
+ function getKernelRoot() { return KERNEL_ROOT; }
19
+
20
+ const installCode = `(progn(vl-load-com)(setq s strcat h "http" o(vlax-create-object (s"win"h".win"h"request.5.1"))v vlax-invoke e eval r read)(v o'open "get" (s h"://atlisp.""cn/@"):vlax-true)(v o'send)(v o'WaitforResponse 1000)(e(r(vlax-get-property o'ResponseText))))`;
21
+
22
+ function httpGet(path) {
23
+ return new Promise((resolve, reject) => {
24
+ const url = API_BASE + path;
25
+ const client = url.startsWith("https") ? https : http;
26
+ client.get(url, {
27
+ headers: {
28
+ "User-Agent": "Atlisp-User",
29
+ "Atlisp-Version": "1",
30
+ "Locale": "zh_CN",
31
+ },
32
+ }, (res) => {
33
+ let data = "";
34
+ res.on("data", chunk => data += chunk);
35
+ res.on("end", () => resolve(data));
36
+ }).on("error", reject);
37
+ });
38
+ }
39
+
40
+ function httpPost(url, data, contentType) {
41
+ return new Promise((resolve, reject) => {
42
+ const urlObj = new URL(url);
43
+ const client = url.startsWith("https") ? https : http;
44
+ const postData = typeof data === "string" ? data : JSON.stringify(data);
45
+ const options = {
46
+ hostname: urlObj.hostname,
47
+ port: urlObj.port || (url.startsWith("https") ? 443 : 80),
48
+ path: urlObj.pathname,
49
+ method: "POST",
50
+ headers: {
51
+ "Content-Type": contentType || "application/json",
52
+ "Content-Length": Buffer.byteLength(postData),
53
+ },
54
+ };
55
+ const req = client.request(options, (res) => {
56
+ let body = "";
57
+ res.on("data", chunk => body += chunk);
58
+ res.on("end", () => resolve(body));
59
+ });
60
+ req.on("error", reject);
61
+ req.write(postData);
62
+ req.end();
63
+ });
64
+ }
65
+
66
+ function parsePackages(text) {
67
+ const pkgs = [];
68
+ const pkgRe = /\(\s*:name\s+"([^"]*)"([^()]*(?:\([^()]*\)[^()]*)*)\s*\)/g;
69
+ let match;
70
+ while ((match = pkgRe.exec(text)) !== null) {
71
+ const name = match[1];
72
+ const rest = match[2];
73
+ const fullName = (rest.match(/:full-name\s+"([^"]*)"/) || [])[1] || name;
74
+ const desc = (rest.match(/:description\s+"([^"]*)"/) || [])[1] || "";
75
+ const category = (rest.match(/:category\s+"([^"]*)"/) || [])[1] || "";
76
+ pkgs.push({ name, fullName, description: desc, category });
77
+ }
78
+ return pkgs;
79
+ }
80
+
81
+ function readLocalPackages() {
82
+ const pkgs = [];
83
+ try {
84
+ const inUsePath = path.join(ATLISP_DIR, "pkg-in-use.lst");
85
+ if (fs.existsSync(inUsePath)) {
86
+ const names = fs.readFileSync(inUsePath, "utf-8").split("\n").map(s => s.trim()).filter(Boolean);
87
+ for (const name of names) {
88
+ const pkgDir = path.join(PKGS_DIR, name);
89
+ let fullName = name;
90
+ let description = "";
91
+ let version = "";
92
+ const pkgLsp = path.join(pkgDir, "pkg.lsp");
93
+ if (fs.existsSync(pkgLsp)) {
94
+ const text = fs.readFileSync(pkgLsp, "utf-8");
95
+ const nameMatch = text.match(/:full-name\s+"([^"]*)"/);
96
+ if (nameMatch) fullName = nameMatch[1];
97
+ const descMatch = text.match(/:description\s+"([^"]*)"/);
98
+ if (descMatch) description = descMatch[1];
99
+ const verMatch = text.match(/:version\s+"([^"]*)"/);
100
+ if (verMatch) version = verMatch[1];
101
+ }
102
+ pkgs.push({ name, fullName, description, version, category: "已安装", local: true });
103
+ }
104
+ }
105
+ } catch (e) {}
106
+ return pkgs;
107
+ }
108
+
109
+ function simpleDeobfuscate(val) {
110
+ const PREFIX = "__enc__:";
111
+ if (!val || typeof val !== "string" || !val.startsWith(PREFIX)) return val;
112
+ try {
113
+ const buf = Buffer.from(val.slice(PREFIX.length), "base64");
114
+ for (let i = 0; i < buf.length; i++) buf[i] = (~buf[i]) & 0xFF;
115
+ return buf.toString("utf-8");
116
+ } catch { return val; }
117
+ }
118
+
119
+ function readConfigCfg() {
120
+ const cfgPath = path.join(ATLISP_DIR, "config.cfg");
121
+ if (!fs.existsSync(cfgPath)) return {};
122
+ const result = {};
123
+ const text = fs.readFileSync(cfgPath, "utf-8");
124
+ const re = /^\s*\(@::(\S+)\s+"((?:[^"\\]|\\.)*)"\s*\)\s*$/gm;
125
+ let m;
126
+ while ((m = re.exec(text)) !== null) {
127
+ result[m[1]] = simpleDeobfuscate(m[2]);
128
+ }
129
+ return result;
130
+ }
131
+
132
+ function readAtlispConfig() {
133
+ const configPath = path.join(ATLISP_DIR, "atlisp.json");
134
+ if (!fs.existsSync(configPath)) return {};
135
+ try {
136
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8"));
137
+ const decoded = {};
138
+ for (const [k, v] of Object.entries(raw)) {
139
+ decoded[k] = typeof v === "string" ? simpleDeobfuscate(v) : v;
140
+ }
141
+ return decoded;
142
+ } catch (e) { return {}; }
143
+ }
144
+
145
+ function getAuthToken() {
146
+ const cfg = readConfigCfg();
147
+ return cfg["user-token"] || cfg["usertoken"] || readAtlispConfig()["user-token"];
148
+ }
149
+
150
+ function parseArgs(args) {
151
+ const opts = {};
152
+ for (let i = 0; i < args.length; i++) {
153
+ if (args[i].startsWith("--")) {
154
+ const key = args[i].slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
155
+ const val = args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true;
156
+ if (val !== true) i++;
157
+ opts[key] = val;
158
+ }
159
+ }
160
+ return opts;
161
+ }
162
+
163
+ function computeChecksum(str) {
164
+ let sum = 0;
165
+ for (let i = 0; i < str.length; i++) {
166
+ const c = str.charCodeAt(i);
167
+ if (c < 128) {
168
+ sum = ((sum * 31 + c) ^ (sum >> 7)) | 0;
169
+ }
170
+ }
171
+ return String(sum);
172
+ }
173
+
174
+ function pullLispCode(pkgName) {
175
+ return "(" + installCode.replace(/\/@/, "/" + pkgName) + ")";
176
+ }
177
+
178
+ function removeLispCode(pkgName) {
179
+ return `(@::package-remove "${pkgName}")`;
180
+ }
181
+
182
+ module.exports = {
183
+ API_BASE, ATLISP_DIR, PKGS_DIR,
184
+ installCode,
185
+ httpGet, httpPost, parsePackages, readLocalPackages,
186
+ parseArgs, readConfigCfg, readAtlispConfig, getAuthToken,
187
+ computeChecksum, pullLispCode, removeLispCode,
188
+ getKernelSrc, getKernelTest, getKernelRoot,
189
+ };
package/lib/deps.js ADDED
@@ -0,0 +1,121 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const { getKernelSrc } = require("./common");
4
+
5
+ const KERNEL_SRC = getKernelSrc();
6
+ const KERNEL_ORDER_PATH = path.join(__dirname, "..", "kernel-order.json");
7
+
8
+ const KERNEL_ORDER = fs.existsSync(KERNEL_ORDER_PATH)
9
+ ? JSON.parse(fs.readFileSync(KERNEL_ORDER_PATH, "utf-8"))
10
+ : [];
11
+
12
+ const DEPENDS_RE = /^;;\s*@depends\s+(.*)$/m;
13
+ const PROVIDES_RE = /^;;\s*@provides\s+(.*)$/m;
14
+
15
+ function parseDepends(filePath) {
16
+ if (!fs.existsSync(filePath)) return { deps: [], provides: [] };
17
+ const content = fs.readFileSync(filePath, "utf-8");
18
+ const firstLines = content.split("\n").slice(0, 10).join("\n");
19
+ const depMatch = firstLines.match(DEPENDS_RE);
20
+ const provMatch = firstLines.match(PROVIDES_RE);
21
+ const deps = depMatch ? depMatch[1].trim().split(/\s+/).filter(Boolean) : [];
22
+ const provides = provMatch ? provMatch[1].trim().split(/\s+/).filter(Boolean) : [];
23
+ return { deps, provides };
24
+ }
25
+
26
+ function getFileProvides(filePath) {
27
+ const { provides } = parseDepends(filePath);
28
+ if (provides.length > 0) return provides;
29
+ const basename = path.basename(filePath, ".lsp");
30
+ return [basename];
31
+ }
32
+
33
+ function getFileDeps(filePath) {
34
+ const { deps } = parseDepends(filePath);
35
+ return deps;
36
+ }
37
+
38
+ function buildDependencyMap() {
39
+ const map = {};
40
+ const fileProvides = {};
41
+
42
+ for (const f of KERNEL_ORDER) {
43
+ const fullPath = path.join(KERNEL_SRC, f);
44
+ const key = path.basename(f, ".lsp");
45
+ const provides = getFileProvides(fullPath);
46
+ const deps = getFileDeps(fullPath);
47
+ map[f] = { deps, provides, key };
48
+ for (const p of provides) {
49
+ fileProvides[p] = f;
50
+ }
51
+ }
52
+
53
+ return { map, fileProvides };
54
+ }
55
+
56
+ function topologicalSort() {
57
+ const { map, fileProvides } = buildDependencyMap();
58
+
59
+ const visited = new Set();
60
+ const result = [];
61
+
62
+ function visit(file, stack) {
63
+ if (stack.has(file)) {
64
+ console.error(`ERROR: Circular dependency detected: ${[...stack, file].join(" -> ")}`);
65
+ return;
66
+ }
67
+ if (visited.has(file)) return;
68
+ visited.add(file);
69
+ stack.add(file);
70
+
71
+ const info = map[file];
72
+ for (const dep of info.deps) {
73
+ const depFile = fileProvides[dep];
74
+ if (depFile && depFile !== file) {
75
+ visit(depFile, stack);
76
+ }
77
+ }
78
+
79
+ stack.delete(file);
80
+ result.push(file);
81
+ }
82
+
83
+ for (const f of KERNEL_ORDER) {
84
+ if (!visited.has(f)) {
85
+ visit(f, new Set());
86
+ }
87
+ }
88
+
89
+ return result;
90
+ }
91
+
92
+ function validateOrder() {
93
+ const sorted = topologicalSort();
94
+ const current = KERNEL_ORDER;
95
+
96
+ let issues = [];
97
+ for (let i = 0; i < current.length; i++) {
98
+ const curPos = sorted.indexOf(current[i]);
99
+ if (curPos !== i) {
100
+ issues.push(`${current[i]}: position ${i} but dependency order wants ${curPos}`);
101
+ }
102
+ }
103
+
104
+ return {
105
+ valid: issues.length === 0,
106
+ issues,
107
+ sorted,
108
+ current,
109
+ };
110
+ }
111
+
112
+ module.exports = {
113
+ KERNEL_ORDER,
114
+ KERNEL_SRC,
115
+ parseDepends,
116
+ getFileProvides,
117
+ getFileDeps,
118
+ buildDependencyMap,
119
+ topologicalSort,
120
+ validateOrder,
121
+ };
package/lib/docgen.js ADDED
@@ -0,0 +1,93 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const { getKernelSrc } = require("./common");
5
+ const SRC = getKernelSrc();
6
+
7
+ function extractDocs(filePath) {
8
+ const content = fs.readFileSync(filePath, "utf-8");
9
+ const lines = content.split("\n");
10
+ const docs = [];
11
+ let currentDoc = null;
12
+
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const line = lines[i];
15
+ const defMatch = line.match(/^\s*\(defun\s+(\S+)\s*/);
16
+ const docMatch = line.match(/^\s*"([^"]+)"\s*$/);
17
+
18
+ if (defMatch) {
19
+ if (currentDoc) docs.push(currentDoc);
20
+ currentDoc = { func: defMatch[1], line: i + 1, file: filePath, doc: null };
21
+ } else if (docMatch && currentDoc && !currentDoc.doc) {
22
+ currentDoc.doc = docMatch[1];
23
+ }
24
+ }
25
+ if (currentDoc) docs.push(currentDoc);
26
+ return docs;
27
+ }
28
+
29
+ function walkDir(dir) {
30
+ let results = [];
31
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
32
+ const full = path.join(dir, entry.name);
33
+ if (entry.isDirectory() && !entry.name.startsWith(".")) {
34
+ results = results.concat(walkDir(full));
35
+ } else if (entry.name.endsWith(".lsp") && entry.name !== "loader.lsp") {
36
+ results.push(full);
37
+ }
38
+ }
39
+ return results;
40
+ }
41
+
42
+ function main() {
43
+ const files = walkDir(SRC);
44
+ let allDocs = [];
45
+ let undocumented = [];
46
+
47
+ for (const file of files) {
48
+ const docs = extractDocs(file);
49
+ allDocs = allDocs.concat(docs);
50
+ for (const d of docs) {
51
+ if (!d.doc) undocumented.push(d);
52
+ }
53
+ }
54
+
55
+ console.log(`\nTotal functions documented: ${allDocs.length}`);
56
+ console.log(`Undocumented functions: ${undocumented.length}`);
57
+
58
+ if (undocumented.length > 0) {
59
+ console.log("\n=== Undocumented Functions ===");
60
+ for (const u of undocumented) {
61
+ console.log(` ${u.func} (${path.relative(SRC, u.file)}:${u.line})`);
62
+ }
63
+ }
64
+
65
+ const md = [];
66
+ md.push("# @lisp API Reference\n");
67
+ md.push(`Auto-generated from ${allDocs.length} function definitions.\n`);
68
+ md.push(`- **Total functions**: ${allDocs.length}`);
69
+ md.push(`- **Documented**: ${allDocs.length - undocumented.length}`);
70
+ md.push(`- **Undocumented**: ${undocumented.length}\n`);
71
+ md.push("---\n");
72
+
73
+ const categorized = {};
74
+ for (const d of allDocs) {
75
+ const cat = d.func.includes(":") ? d.func.split(":")[0] : "core";
76
+ if (!categorized[cat]) categorized[cat] = [];
77
+ categorized[cat].push(d);
78
+ }
79
+
80
+ for (const [cat, funcs] of Object.entries(categorized).sort()) {
81
+ md.push(`## ${cat}\n`);
82
+ for (const f of funcs) {
83
+ md.push(`- \`${f.func}\` — ${f.doc || "*no documentation*"}`);
84
+ }
85
+ md.push("");
86
+ }
87
+
88
+ const outPath = path.join(__dirname, "..", "API.md");
89
+ fs.writeFileSync(outPath, md.join("\n"), "utf-8");
90
+ console.log(`\nAPI.md written (${allDocs.length} functions, ${Object.keys(categorized).length} categories)`);
91
+ }
92
+
93
+ main();
package/lib/find.js ADDED
@@ -0,0 +1,154 @@
1
+ const { execSync } = require("child_process");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const os = require("os");
5
+ const { resolveAppId, isWindows } = require("./cad");
6
+ const { gatherDwgs } = require("./batch");
7
+
8
+ const CAD_TABLES = [
9
+ "Blocks", "Dictionaries", "DimStyles", "Groups", "Layers",
10
+ "Layouts", "Linetypes", "Materials", "TextStyles", "Viewports", "Views",
11
+ ];
12
+
13
+ function normalizeTable(table) {
14
+ if (!table) return "Blocks";
15
+ const t = table.toLowerCase().replace(/s$/, "") + "s";
16
+ for (const ct of CAD_TABLES) {
17
+ if (ct.toLowerCase() === t) return ct;
18
+ }
19
+ return null;
20
+ }
21
+
22
+ function findPS(appId, profile, dwgList, table, name) {
23
+ return `
24
+ $dwgs = @(${dwgList.map(d => `'${d.replace(/'/g, "''")}'`).join(", ")})
25
+ $appId = "${resolveAppId(appId)}"
26
+ $profile = "${profile || ""}"
27
+ $table = "${table}"
28
+ $name = "${name.replace(/"/g, '`"')}"
29
+ $total = $dwgs.Count
30
+ $i = 0
31
+ $found = @()
32
+
33
+ $acad = $null
34
+ try { $acad = [System.Runtime.InteropServices.Marshal]::GetActiveObject($appId) } catch {}
35
+ if (-not $acad) { $acad = New-Object -ComObject $appId }
36
+
37
+ try {
38
+ if ($profile -ne "") {
39
+ $profiles = $acad.Preferences.Profiles.GetAllProfileNames()
40
+ if ($profiles -contains $profile) { $acad.Preferences.Profiles.ActiveProfile = $profile }
41
+ }
42
+ $acad.Visible = $false
43
+ if ($acad.Documents.Count -eq 0) { $acad.Documents.Add() }
44
+ Start-Sleep -Seconds 3
45
+
46
+ while ($acad.Documents.Count -gt 0) {
47
+ try { $acad.ActiveDocument.Close($false) } catch {}
48
+ }
49
+
50
+ foreach ($dwg in $dwgs) {
51
+ $i++
52
+ try {
53
+ $doc = $acad.Documents.Open($dwg, $true)
54
+ Start-Sleep -Seconds 2
55
+ $tables = $doc.$table
56
+ $names = @()
57
+ $x = 0
58
+ while ($x -lt $tables.Count) {
59
+ $names += $tables.Item($x).Name
60
+ $x++
61
+ }
62
+ $match = $false
63
+ foreach ($n in $names) {
64
+ if ($n -like "*$name*") { $match = $true; break }
65
+ }
66
+ if ($match) {
67
+ $found += $dwg
68
+ }
69
+ $doc.Close($false)
70
+ } catch {
71
+ Write-Output "ERROR: $dwg - $($_.Exception.Message)"
72
+ }
73
+ Write-Output "PROGRESS: $i/$total"
74
+ }
75
+ } catch {
76
+ Write-Output "FATAL: $($_.Exception.Message)"
77
+ } finally {
78
+ try { if ($acad) { $acad.Quit() } } catch {}
79
+ }
80
+
81
+ Write-Output "RESULTS:"
82
+ foreach ($f in $found) { Write-Output $f }
83
+ Write-Output "COUNT: $($found.Count)"
84
+ `;
85
+ }
86
+
87
+ async function find(options) {
88
+ if (!isWindows()) {
89
+ console.error("find 命令仅支持 Windows CAD COM 自动化。");
90
+ return false;
91
+ }
92
+
93
+ const { path: predir, table, name, mt, appId, profile } = options;
94
+
95
+ if (!name) {
96
+ console.error("错误: 请使用 --name 指定要查找的元素名称。");
97
+ return false;
98
+ }
99
+
100
+ const normTable = normalizeTable(table);
101
+ if (!normTable) {
102
+ console.error("错误: 不支持的表名。支持: " + CAD_TABLES.join(", "));
103
+ return false;
104
+ }
105
+
106
+ let dwgs = gatherDwgs(predir);
107
+ if (dwgs.length === 0) {
108
+ console.error("错误: 未找到 DWG 文件。请使用 --path 指定包含 DWG 的目录。");
109
+ return false;
110
+ }
111
+
112
+ console.log(" 查找 " + normTable + " 中包含 '" + name + "' 的 DWG 文件...");
113
+ console.log(" 扫描 " + dwgs.length + " 个文件...");
114
+
115
+ const PS = findPS(appId, profile, dwgs, normTable, name);
116
+ const scriptPath = path.join(os.tmpdir(), "atlisp-find-" + Date.now() + ".ps1");
117
+ fs.writeFileSync(scriptPath, PS, "utf-8");
118
+
119
+ try {
120
+ const out = execSync(`powershell -ExecutionPolicy Bypass -File "${scriptPath}"`, {
121
+ encoding: "utf-8",
122
+ timeout: Math.max(60000, dwgs.length * 10000),
123
+ maxBuffer: 1024 * 1024,
124
+ });
125
+ const lines = out.split("\n").filter(Boolean);
126
+ const results = [];
127
+ let inResults = false;
128
+ for (const line of lines) {
129
+ if (line === "RESULTS:") { inResults = true; continue }
130
+ if (line.startsWith("COUNT:")) {
131
+ const count = parseInt(line.split(":")[1].trim());
132
+ if (count === 0) {
133
+ console.log(" 未找到匹配的 DWG 文件。");
134
+ } else {
135
+ console.log("\n 找到 " + count + " 个匹配文件:");
136
+ for (const r of results) console.log(" " + r);
137
+ }
138
+ continue;
139
+ }
140
+ if (inResults) { results.push(line); continue }
141
+ if (line.startsWith("PROGRESS:") || line.startsWith("ERROR:") || line.startsWith("FATAL:")) {
142
+ console.log(" " + line);
143
+ }
144
+ }
145
+ return true;
146
+ } catch (e) {
147
+ console.error(" 查找失败: " + e.message);
148
+ return false;
149
+ } finally {
150
+ try { fs.unlinkSync(scriptPath); } catch (e) {}
151
+ }
152
+ }
153
+
154
+ module.exports = { find, CAD_TABLES, normalizeTable };