@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.
- package/.github/workflows/publish.yml +18 -0
- package/.github/workflows/test.yml +14 -0
- package/README.md +88 -0
- package/__tests__/__snapshots__/cli.test.js.snap +12 -0
- package/__tests__/build.test.js +48 -0
- package/__tests__/cli.test.js +246 -0
- package/__tests__/config.test.js +71 -0
- package/__tests__/deps.test.js +64 -0
- package/__tests__/index.test.js +63 -0
- package/__tests__/lint.test.js +58 -0
- package/atlisp.js +219 -0
- package/commands/batch.js +14 -0
- package/commands/build.js +267 -0
- package/commands/checksum.js +23 -0
- package/commands/config.js +264 -0
- package/commands/dcl-migrate.js +62 -0
- package/commands/doc.js +45 -0
- package/commands/doctor.js +292 -0
- package/commands/find.js +14 -0
- package/commands/index.js +33 -0
- package/commands/info.js +46 -0
- package/commands/init.js +202 -0
- package/commands/install.js +23 -0
- package/commands/lint.js +79 -0
- package/commands/list.js +99 -0
- package/commands/publish.js +237 -0
- package/commands/pull.js +118 -0
- package/commands/qrcode.js +33 -0
- package/commands/test.js +223 -0
- package/commands/watch.js +47 -0
- package/completion/atlisp-completion.bash +107 -0
- package/completion/atlisp-completion.ps1 +64 -0
- package/index.js +13 -0
- package/kernel-order.json +28 -0
- package/lib/atlisp.js +15 -0
- package/lib/batch.js +173 -0
- package/lib/cad.js +199 -0
- package/lib/common.js +189 -0
- package/lib/deps.js +121 -0
- package/lib/docgen.js +93 -0
- package/lib/find.js +154 -0
- package/lib/mcp-client.js +203 -0
- package/package.json +49 -0
- package/scripts/add-deps.js +81 -0
- package/scripts/fix-deps.js +98 -0
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const common = require("../lib/common");
|
|
6
|
+
const { buildModule } = require("./build");
|
|
7
|
+
|
|
8
|
+
const { getKernelSrc } = require("../lib/common");
|
|
9
|
+
const KERNEL_SRC = getKernelSrc();
|
|
10
|
+
const MODULES_SRC = path.join(KERNEL_SRC, "modules");
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
async function cmdPublishModule(filePath, opts) {
|
|
14
|
+
const moduleName = opts.name || path.basename(filePath, ".lsp");
|
|
15
|
+
const edition = opts.edition || "stable";
|
|
16
|
+
const uploadUrl = opts.server || "https://atlisp.cn/upload-module";
|
|
17
|
+
|
|
18
|
+
const absPath = path.resolve(filePath);
|
|
19
|
+
if (!fs.existsSync(absPath)) {
|
|
20
|
+
console.error(chalk.red(`错误: 找不到文件 ${absPath}`));
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const versionFile = path.join(KERNEL_SRC, "version.lsp");
|
|
25
|
+
let version = opts.version;
|
|
26
|
+
if (!version && fs.existsSync(versionFile)) {
|
|
27
|
+
const verContent = fs.readFileSync(versionFile, "utf-8");
|
|
28
|
+
const m = verContent.match(/"([^"]+)"/);
|
|
29
|
+
if (m) version = m[1];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const cfg = common.readConfigCfg();
|
|
33
|
+
const json = common.readAtlispConfig();
|
|
34
|
+
const email = opts.email || cfg.userEmail || json["user-email"];
|
|
35
|
+
const token = opts.token || cfg.userToken || json["user-token"];
|
|
36
|
+
if (!email || !token) {
|
|
37
|
+
console.error(chalk.red("错误: 未设置 user-email 和 user-token。"));
|
|
38
|
+
console.error(chalk.gray("请先在 CAD 中用 @login 命令登录,"));
|
|
39
|
+
console.error(chalk.gray("或在 ~/.atlisp/atlisp.json 中配置,或用 --email / --token 参数提供。"));
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
console.log(chalk.cyan("正在构建模块..."));
|
|
44
|
+
buildModule(moduleName);
|
|
45
|
+
|
|
46
|
+
console.log(chalk.cyan(`正在发布模块: ${moduleName} (${edition})`));
|
|
47
|
+
console.log(chalk.gray(` 文件: ${absPath}`));
|
|
48
|
+
|
|
49
|
+
const content = fs.readFileSync(absPath, "utf-8");
|
|
50
|
+
const hash = common.computeChecksum(content.replace(/\n;; @checksum: -?\d+\s*$/, ""));
|
|
51
|
+
console.log(chalk.gray(` checksum: ${hash}`));
|
|
52
|
+
|
|
53
|
+
const boundary = "----AtlispFormBoundary" + Date.now();
|
|
54
|
+
const filename = moduleName + ".lsp";
|
|
55
|
+
const header = [
|
|
56
|
+
`--${boundary}\r\n`,
|
|
57
|
+
`Content-Disposition: form-data; name="email"\r\n\r\n`,
|
|
58
|
+
`${email}\r\n`,
|
|
59
|
+
`--${boundary}\r\n`,
|
|
60
|
+
`Content-Disposition: form-data; name="token"\r\n\r\n`,
|
|
61
|
+
`${token}\r\n`,
|
|
62
|
+
`--${boundary}\r\n`,
|
|
63
|
+
`Content-Disposition: form-data; name="module-name"\r\n\r\n`,
|
|
64
|
+
`${moduleName}\r\n`,
|
|
65
|
+
`--${boundary}\r\n`,
|
|
66
|
+
`Content-Disposition: form-data; name="edition"\r\n\r\n`,
|
|
67
|
+
`${edition}\r\n`,
|
|
68
|
+
`--${boundary}\r\n`,
|
|
69
|
+
`Content-Disposition: form-data; name="version"\r\n\r\n`,
|
|
70
|
+
`${version || ""}\r\n`,
|
|
71
|
+
`--${boundary}\r\n`,
|
|
72
|
+
`Content-Disposition: form-data; name="file"; filename="${filename}"\r\n`,
|
|
73
|
+
`Content-Type: application/octet-stream\r\n\r\n`,
|
|
74
|
+
].join("");
|
|
75
|
+
const footer = `\r\n--${boundary}--\r\n`;
|
|
76
|
+
const bodyBuffer = Buffer.concat([
|
|
77
|
+
Buffer.from(header, "utf-8"),
|
|
78
|
+
Buffer.from(content, "utf-8"),
|
|
79
|
+
Buffer.from(footer, "utf-8"),
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
const urlObj = new URL(uploadUrl);
|
|
83
|
+
const https = require("https");
|
|
84
|
+
const options = {
|
|
85
|
+
hostname: urlObj.hostname,
|
|
86
|
+
port: urlObj.port || 443,
|
|
87
|
+
path: urlObj.pathname,
|
|
88
|
+
method: "POST",
|
|
89
|
+
headers: {
|
|
90
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
91
|
+
"Content-Length": bodyBuffer.length,
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
console.log(chalk.gray(` POST ${uploadUrl}`));
|
|
96
|
+
|
|
97
|
+
return new Promise((resolve, reject) => {
|
|
98
|
+
const req = https.request(options, (res) => {
|
|
99
|
+
let data = "";
|
|
100
|
+
res.on("data", (chunk) => (data += chunk));
|
|
101
|
+
res.on("end", () => {
|
|
102
|
+
if (res.statusCode === 200) {
|
|
103
|
+
try {
|
|
104
|
+
const parsed = data.match(/:result\s*\.\s*([^)\s]+)/);
|
|
105
|
+
const success = parsed && parsed[1] === "T";
|
|
106
|
+
const msgMatch = data.match(/:msg\s*\.\s*"([^"]*)"/);
|
|
107
|
+
const msg = msgMatch ? msgMatch[1] : data.trim();
|
|
108
|
+
if (success) {
|
|
109
|
+
console.log(chalk.green(`✓ 模块 ${moduleName} 已发布到 ${edition}`));
|
|
110
|
+
console.log(chalk.gray(` 服务端: ${msg}`));
|
|
111
|
+
resolve(true);
|
|
112
|
+
} else {
|
|
113
|
+
console.error(chalk.red(`✗ 发布失败: ${msg}`));
|
|
114
|
+
resolve(false);
|
|
115
|
+
}
|
|
116
|
+
} catch (e) {
|
|
117
|
+
console.error(chalk.red(`✗ 响应解析失败: ${data.trim()}`));
|
|
118
|
+
resolve(false);
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
console.error(chalk.red(`✗ HTTP ${res.statusCode}: ${data.trim()}`));
|
|
122
|
+
resolve(false);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
req.on("error", (err) => {
|
|
127
|
+
console.error(chalk.red(`✗ 网络错误: ${err.message}`));
|
|
128
|
+
resolve(false);
|
|
129
|
+
});
|
|
130
|
+
req.write(bodyBuffer);
|
|
131
|
+
req.end();
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function cmdDeploy() {
|
|
136
|
+
const { Client } = require("ssh2");
|
|
137
|
+
const host = "dev.atlisp.cn";
|
|
138
|
+
const user = "vitalgg";
|
|
139
|
+
const remoteDir = "www/@lisp/modules/stable/";
|
|
140
|
+
const keyPath = path.join(os.homedir(), ".ssh", "id_rsa");
|
|
141
|
+
|
|
142
|
+
if (!fs.existsSync(keyPath)) {
|
|
143
|
+
console.error(chalk.red(`SSH 密钥不存在: ${keyPath}`));
|
|
144
|
+
console.log(chalk.gray("请确保 ~/.ssh/id_rsa 存在并可访问 dev.atlisp.cn"));
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const modulesDir = MODULES_SRC;
|
|
149
|
+
const files = fs.readdirSync(modulesDir)
|
|
150
|
+
.filter(f => f.endsWith(".lsp"))
|
|
151
|
+
.sort();
|
|
152
|
+
if (files.length === 0) {
|
|
153
|
+
console.log(chalk.yellow("没有找到模块文件"));
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log(chalk.cyan(`\n部署 ${files.length} 个模块到 ${user}@${host}:${remoteDir}\n`));
|
|
158
|
+
|
|
159
|
+
const conn = new Client();
|
|
160
|
+
await new Promise((resolve, reject) => {
|
|
161
|
+
conn.on("ready", resolve);
|
|
162
|
+
conn.on("error", reject);
|
|
163
|
+
conn.connect({ host, port: 22, username: user, privateKey: fs.readFileSync(keyPath, "utf8") });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return new Promise((resolve) => {
|
|
167
|
+
conn.sftp((err, sftp) => {
|
|
168
|
+
if (err) {
|
|
169
|
+
console.error(chalk.red("SFTP 连接失败: " + err.message));
|
|
170
|
+
conn.end();
|
|
171
|
+
process.exit(1);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let ok = 0, fail = 0;
|
|
176
|
+
let idx = 0;
|
|
177
|
+
|
|
178
|
+
const next = () => {
|
|
179
|
+
if (idx >= files.length) {
|
|
180
|
+
sftp.end();
|
|
181
|
+
conn.end();
|
|
182
|
+
console.log(chalk.cyan(`\n完成: ${ok} 成功, ${fail} 失败\n`));
|
|
183
|
+
resolve();
|
|
184
|
+
if (fail > 0) process.exit(1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const f = files[idx++];
|
|
189
|
+
const src = path.join(modulesDir, f);
|
|
190
|
+
const remote = remoteDir + f;
|
|
191
|
+
process.stdout.write(chalk.gray(` ${f} ... `));
|
|
192
|
+
|
|
193
|
+
sftp.fastPut(src, remote, (err) => {
|
|
194
|
+
if (err) {
|
|
195
|
+
console.log(chalk.red("✗ " + err.message));
|
|
196
|
+
fail++;
|
|
197
|
+
} else {
|
|
198
|
+
console.log(chalk.green("✓"));
|
|
199
|
+
ok++;
|
|
200
|
+
}
|
|
201
|
+
next();
|
|
202
|
+
});
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
next();
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function handler(args) {
|
|
211
|
+
const cmd = args[0];
|
|
212
|
+
const opts = {};
|
|
213
|
+
let val = args[1];
|
|
214
|
+
for (let i = 1; i < args.length; i++) {
|
|
215
|
+
if (args[i].startsWith("--")) {
|
|
216
|
+
const key = args[i].slice(2).replace(/-([a-z])/g, (_, c) => c.toUpperCase());
|
|
217
|
+
const v = args[i + 1] && !args[i + 1].startsWith("--") ? args[i + 1] : true;
|
|
218
|
+
if (v !== true) i++;
|
|
219
|
+
opts[key] = v;
|
|
220
|
+
} else if (val === args[1]) {
|
|
221
|
+
val = args[i];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (cmd === "deploy") {
|
|
225
|
+
await cmdDeploy();
|
|
226
|
+
} else {
|
|
227
|
+
const filePath = val || "";
|
|
228
|
+
await cmdPublishModule(filePath, opts);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function register(reg) {
|
|
233
|
+
reg("publish-module", handler, ["pm"]);
|
|
234
|
+
reg("deploy", handler);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { register };
|
package/commands/pull.js
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const common = require("../lib/common");
|
|
6
|
+
const cad = require("../lib/cad");
|
|
7
|
+
|
|
8
|
+
async function cmdPull(pkgName, opts) {
|
|
9
|
+
if (!pkgName) {
|
|
10
|
+
console.error(chalk.red("错误: 请指定包名。用法: atlisp pull <包名> [--auto] [--download]"));
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
if (opts && opts.download) {
|
|
14
|
+
console.log(chalk.cyan("\n下载包 \"" + pkgName + "\"...\n"));
|
|
15
|
+
try {
|
|
16
|
+
const text = await common.httpGet("/packages-list?edition=stable");
|
|
17
|
+
if (text && text.length > 0) {
|
|
18
|
+
const pkgs = common.parsePackages(text);
|
|
19
|
+
const match = pkgs.find(p => p.name === pkgName.toLowerCase());
|
|
20
|
+
if (match) {
|
|
21
|
+
const apiBase = common.API_BASE;
|
|
22
|
+
const dest = path.join(os.homedir(), "@lisp", "packages", pkgName);
|
|
23
|
+
console.log(chalk.cyan("包名: ") + match.fullName);
|
|
24
|
+
console.log(chalk.cyan("描述: ") + (match.description || "(无)"));
|
|
25
|
+
console.log(chalk.cyan("分类: ") + (match.category || "未分类"));
|
|
26
|
+
console.log(chalk.cyan("下载到: ") + dest);
|
|
27
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
28
|
+
const pkgLspUrl = `${apiBase}/${pkgName}/pkg.lsp`;
|
|
29
|
+
try {
|
|
30
|
+
const resp = await fetch(pkgLspUrl);
|
|
31
|
+
if (resp.ok) {
|
|
32
|
+
const content = await resp.text();
|
|
33
|
+
fs.writeFileSync(path.join(dest, "pkg.lsp"), content, "utf-8");
|
|
34
|
+
console.log(chalk.gray(" ✓ pkg.lsp"));
|
|
35
|
+
const files = (content.match(/:files\s+\(([^)]*)\)/i) || [])[1];
|
|
36
|
+
if (files) {
|
|
37
|
+
const fileList = files.split(/\s+/).filter(Boolean);
|
|
38
|
+
for (const f of fileList) {
|
|
39
|
+
const fUrl = `${apiBase}/${pkgName}/${f}.lsp`;
|
|
40
|
+
const fResp = await fetch(fUrl);
|
|
41
|
+
if (fResp.ok) {
|
|
42
|
+
const fContent = await fResp.text();
|
|
43
|
+
fs.writeFileSync(path.join(dest, f + ".lsp"), fContent, "utf-8");
|
|
44
|
+
console.log(chalk.gray(` ✓ ${f}.lsp`));
|
|
45
|
+
} else {
|
|
46
|
+
const fasResp = await fetch(`${apiBase}/${pkgName}/${f}.fas`);
|
|
47
|
+
if (fasResp.ok) {
|
|
48
|
+
const buf = Buffer.from(await fasResp.arrayBuffer());
|
|
49
|
+
fs.writeFileSync(path.join(dest, f + ".fas"), buf);
|
|
50
|
+
console.log(chalk.gray(` ✓ ${f}.fas`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
console.log(chalk.green("\n✓ 下载完成"));
|
|
56
|
+
} else {
|
|
57
|
+
console.log(chalk.yellow(" 服务器上未找到该包的文件"));
|
|
58
|
+
}
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.error(chalk.red(" ✗ 下载文件失败: " + e.message));
|
|
61
|
+
}
|
|
62
|
+
console.log(chalk.gray("\n使用 --auto 参数可直接安装到 CAD:"));
|
|
63
|
+
console.log(chalk.gray(" atlisp pull " + pkgName + " --auto\n"));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
console.log(chalk.yellow("未找到包: " + pkgName));
|
|
68
|
+
} catch (err) {
|
|
69
|
+
console.error(chalk.red("下载失败: " + err.message));
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
if (!opts || !opts.auto) {
|
|
74
|
+
console.log("\n安装 @lisp 应用包 \"" + pkgName + "\" 到 CAD...");
|
|
75
|
+
console.log("复制以下代码到CAD命令行:\n");
|
|
76
|
+
console.log(common.pullLispCode(pkgName));
|
|
77
|
+
console.log("\n或使用 --auto 参数自动安装(仅 Windows):");
|
|
78
|
+
console.log(" atlisp pull " + pkgName + " --auto");
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
console.log("\n自动安装 \"" + pkgName + "\" 到 CAD...");
|
|
82
|
+
await cad.pull(pkgName, opts.app, opts.profile);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function cmdRemove(pkgName, opts) {
|
|
86
|
+
if (!pkgName) {
|
|
87
|
+
console.error(chalk.red("错误: 请指定包名。用法: atlisp remove <包名> [--auto]"));
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
if (!opts || !opts.auto) {
|
|
91
|
+
console.log("\n卸载 @lisp 应用包 \"" + pkgName + "\"...");
|
|
92
|
+
console.log("复制以下代码到CAD命令行:\n");
|
|
93
|
+
console.log(common.removeLispCode(pkgName));
|
|
94
|
+
console.log("\n或使用 --auto 参数自动卸载(仅 Windows):");
|
|
95
|
+
console.log(" atlisp remove " + pkgName + " --auto");
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
console.log("\n自动卸载 \"" + pkgName + "\"...");
|
|
99
|
+
await cad.remove(pkgName, opts.app, opts.profile);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function handler(args) {
|
|
103
|
+
const cmd = args[0];
|
|
104
|
+
const val = args[1];
|
|
105
|
+
const opts = common.parseArgs(args.slice(1));
|
|
106
|
+
if (cmd === "remove" || cmd === "rm") {
|
|
107
|
+
await cmdRemove(val, opts);
|
|
108
|
+
} else {
|
|
109
|
+
await cmdPull(val, opts);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function register(reg) {
|
|
114
|
+
reg("pull", (args) => cmdPull(args[0], common.parseArgs(args.slice(1))), ["p"]);
|
|
115
|
+
reg("remove", (args) => cmdRemove(args[0], common.parseArgs(args.slice(1))), ["rm"]);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
module.exports = { register };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const chalk = require("chalk");
|
|
2
|
+
const QRCode = require("qrcode");
|
|
3
|
+
|
|
4
|
+
async function cmdQRCode(text, ascii) {
|
|
5
|
+
if (!text) {
|
|
6
|
+
console.error(chalk.red("请指定文本: atlisp qrcode <文本>"));
|
|
7
|
+
process.exit(1);
|
|
8
|
+
}
|
|
9
|
+
try {
|
|
10
|
+
const opts = { type: ascii ? "ascii" : "terminal", small: ascii ? false : true };
|
|
11
|
+
const qr = await QRCode.toString(text, opts);
|
|
12
|
+
console.log("\n" + qr);
|
|
13
|
+
console.log(chalk.cyan("QR 码内容: ") + text + "\n");
|
|
14
|
+
} catch (err) {
|
|
15
|
+
console.error(chalk.red("QR 码生成失败: " + err.message));
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function handler(args) {
|
|
21
|
+
if (args[0] === "qrcode-ascii" || args[0] === "qra") {
|
|
22
|
+
await cmdQRCode(args[1], true);
|
|
23
|
+
} else {
|
|
24
|
+
await cmdQRCode(args[1], false);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function register(reg) {
|
|
29
|
+
reg("qrcode", (args) => cmdQRCode(args[0], false), ["qr"]);
|
|
30
|
+
reg("qrcode-ascii", (args) => cmdQRCode(args[0], true), ["qra"]);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { register };
|
package/commands/test.js
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
const chalk = require("chalk");
|
|
5
|
+
const { execSync } = require("child_process");
|
|
6
|
+
const common = require("../lib/common");
|
|
7
|
+
const KERNEL_SRC = common.getKernelSrc();
|
|
8
|
+
const TEST_DIR = common.getKernelTest();
|
|
9
|
+
const KERNEL_ORDER = (() => {
|
|
10
|
+
try { return require("../kernel-order.json"); }
|
|
11
|
+
catch (e) { return []; }
|
|
12
|
+
})();
|
|
13
|
+
|
|
14
|
+
async function cmdTest(filter, opts) {
|
|
15
|
+
const testDir = path.join(__dirname, "..", "..", "..", "..", "test");
|
|
16
|
+
const srcDir = KERNEL_SRC;
|
|
17
|
+
|
|
18
|
+
if (opts && opts.load) {
|
|
19
|
+
const kernelFiles = KERNEL_ORDER;
|
|
20
|
+
if (opts.list) {
|
|
21
|
+
console.log(chalk.gray("\n内核源文件加载测试列表:\n"));
|
|
22
|
+
kernelFiles.forEach(f => console.log(` ${chalk.cyan(f)}`));
|
|
23
|
+
console.log(chalk.gray(`\n共 ${kernelFiles.length} 个文件`));
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const platform = (opts && opts.platform) || "sbcl";
|
|
28
|
+
|
|
29
|
+
if (platform === "sbcl") {
|
|
30
|
+
console.log(chalk.blue("\n=== @lisp Kernel 加载测试 (SBCL) ==="));
|
|
31
|
+
console.log(chalk.gray(`\n共 ${kernelFiles.length} 个内核源文件需要验证。`));
|
|
32
|
+
console.log(chalk.gray("SBCL 仅做语法检查。使用 --platform cad 在 CAD 中逐个加载验证。\n"));
|
|
33
|
+
let ok = 0, fail = 0;
|
|
34
|
+
for (const f of kernelFiles) {
|
|
35
|
+
const filePath = path.join(srcDir, f);
|
|
36
|
+
if (!fs.existsSync(filePath)) {
|
|
37
|
+
console.log(` ${chalk.red("✗")} ${f} ${chalk.red("(文件不存在)")}`);
|
|
38
|
+
fail++;
|
|
39
|
+
} else {
|
|
40
|
+
console.log(` ${chalk.green("✓")} ${f}`);
|
|
41
|
+
ok++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
console.log(chalk.gray(`\n${ok} 存在, ${fail} 缺失\n`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (platform === "cad") {
|
|
49
|
+
if (opts && opts.manual) {
|
|
50
|
+
console.log(chalk.blue("\n=== @lisp Kernel 加载测试 (CAD) ===\n"));
|
|
51
|
+
console.log(chalk.gray("请在 CAD 中加载测试脚本并执行:\n"));
|
|
52
|
+
console.log(` (load "${testDir.replace(/\\/g, "/")}/test-load.lsp")`);
|
|
53
|
+
console.log(` (@::load-test "${srcDir.replace(/\\/g, "/")}/")\n`);
|
|
54
|
+
console.log(chalk.gray(`共 ${kernelFiles.length} 个源文件,将逐个加载验证。`));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const loadPath = path.join(testDir, "test-load.lsp").replace(/\\/g, "/");
|
|
58
|
+
const srcPath = srcDir.replace(/\\/g, "/") + "/";
|
|
59
|
+
const lispCode = `(load "${loadPath}")(@::load-test "${srcPath}")`;
|
|
60
|
+
console.log(chalk.blue("\n正在通过 MCP 向 CAD 发送加载测试...\n"));
|
|
61
|
+
try {
|
|
62
|
+
const output = await withMcp(async (mcp) => {
|
|
63
|
+
await mcp.ensureDocument();
|
|
64
|
+
return await mcp.evalLisp(lispCode);
|
|
65
|
+
});
|
|
66
|
+
console.log(chalk.green(" ✓ 加载测试完成"));
|
|
67
|
+
if (output) console.log(output);
|
|
68
|
+
} catch (e) {
|
|
69
|
+
console.error(chalk.red(` ✗ ${e.message}`));
|
|
70
|
+
if (e.message.includes("ENOENT") || e.message.includes("spawn")) {
|
|
71
|
+
console.log(chalk.yellow("\natlisp-mcp 未安装。请执行:\n"));
|
|
72
|
+
console.log(chalk.cyan(" npm install -g @atlisp/mcp\n"));
|
|
73
|
+
console.log(chalk.gray("或用 --manual 模式手动在 CAD 中执行测试:\n"));
|
|
74
|
+
console.log(chalk.gray(" node atlisp.js test --load --platform cad --manual\n"));
|
|
75
|
+
}
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (opts && opts.list) {
|
|
83
|
+
console.log(chalk.gray("测试文件列表:"));
|
|
84
|
+
const files = fs.readdirSync(testDir).filter(f => f.startsWith("test-") && f.endsWith(".lsp"));
|
|
85
|
+
files.forEach(f => {
|
|
86
|
+
const content = fs.readFileSync(path.join(testDir, f), "utf-8");
|
|
87
|
+
const desc = (content.match(/;; @lisp\s+(.+)/) || [])[1] || "";
|
|
88
|
+
console.log(` ${chalk.cyan(f)} ${chalk.gray(desc)}`);
|
|
89
|
+
});
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const platform = (opts && opts.platform) || "sbcl";
|
|
94
|
+
const testFiles = fs.readdirSync(testDir)
|
|
95
|
+
.filter(f => f.startsWith("test-") && f.endsWith(".lsp") && f !== "test-runner.lsp" && f !== "test-load.lsp")
|
|
96
|
+
.sort();
|
|
97
|
+
|
|
98
|
+
const filtered = filter
|
|
99
|
+
? testFiles.filter(f => f.includes(filter))
|
|
100
|
+
: testFiles;
|
|
101
|
+
|
|
102
|
+
if (filtered.length === 0) {
|
|
103
|
+
console.log(chalk.yellow("没有匹配的测试文件。"));
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
console.log(chalk.blue(`\n=== @lisp Test Runner (${platform}) ===\n`));
|
|
108
|
+
|
|
109
|
+
if (platform === "sbcl") {
|
|
110
|
+
try {
|
|
111
|
+
require("child_process").execSync("sbcl --version", { encoding: "utf-8" });
|
|
112
|
+
} catch (e) {
|
|
113
|
+
console.log(chalk.yellow("SBCL not found. Run tests in CAD:"));
|
|
114
|
+
console.log(" (load "+ path.join(testDir, "run-all.lsp").replace(/\\/g, "/") + ")");
|
|
115
|
+
console.log(" (@::run-tests)");
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
console.log(chalk.cyan("SBCL mode: syntax validation only. Use --platform cad to run tests."));
|
|
119
|
+
console.log(chalk.gray("AutoLISP tests require a CAD environment with VL functions."));
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (platform === "cad") {
|
|
124
|
+
if (opts && opts.manual) {
|
|
125
|
+
console.log(chalk.gray("请在 CAD 命令行中执行:"));
|
|
126
|
+
console.log(` (load "${testDir.replace(/\\/g, "/")}/test-runner.lsp")`);
|
|
127
|
+
filtered.forEach(f => console.log(` (load "${path.join(testDir, f).replace(/\\/g, "/")}")`));
|
|
128
|
+
console.log(` (@::run-tests nil)`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
console.log(chalk.blue("\n正在通过 MCP 向 CAD 发送测试...\n"));
|
|
132
|
+
try {
|
|
133
|
+
const output = await withMcp(async (mcp) => {
|
|
134
|
+
await mcp.ensureDocument();
|
|
135
|
+
const _modDir = path.join(srcDir, "modules").replace(/\\/g, "/");
|
|
136
|
+
const _runnerPath = path.join(testDir, "test-runner.lsp").replace(/\\/g, "/");
|
|
137
|
+
const _lispDir = path.join(os.homedir(), "@lisp").replace(/\\/g, "/");
|
|
138
|
+
console.log(chalk.gray(" 1/8 Loading @lisp kernel via file content..."));
|
|
139
|
+
const kernelRaw = fs.readFileSync(path.join(os.homedir(), "@lisp", "@lisp_u.lsp"), "utf-8");
|
|
140
|
+
await mcp.evalLisp(kernelRaw);
|
|
141
|
+
console.log(chalk.gray(" 2/8 Setting up environment + modules + runner..."));
|
|
142
|
+
const _stubsPath = path.join(testDir, "test-stubs.lsp").replace(/\\/g, "/");
|
|
143
|
+
const _modNetworkRaw = fs.readFileSync(path.join(srcDir, "modules", "network.lsp"), "utf-8");
|
|
144
|
+
const _modPkgmanRaw = fs.readFileSync(path.join(srcDir, "modules", "pkgman.lsp"), "utf-8");
|
|
145
|
+
const _stubsRaw = fs.readFileSync(path.join(testDir, "test-stubs.lsp"), "utf-8");
|
|
146
|
+
const _runnerRaw = fs.readFileSync(path.join(testDir, "test-runner.lsp"), "utf-8");
|
|
147
|
+
const setupCode = `
|
|
148
|
+
(setq @::*offline-mode-p* T @::*dev-mode* T)
|
|
149
|
+
(defun @::internetp () (princ "nop") nil)
|
|
150
|
+
(if (null @::*prefix*) (setq @::*prefix* (strcat $userhome "/@lisp/")))
|
|
151
|
+
(setq @::*prefix-config* (strcat @::*prefix* ".atlisp/"))
|
|
152
|
+
(setq @::*cache-dir* (strcat @::*prefix* ".cache/"))
|
|
153
|
+
(vl-mkdir @::*cache-dir*)
|
|
154
|
+
(setq @::agent-uri "http://localhost:8211")
|
|
155
|
+
${_modNetworkRaw}
|
|
156
|
+
${_modPkgmanRaw}
|
|
157
|
+
${_stubsRaw}
|
|
158
|
+
${_runnerRaw}
|
|
159
|
+
(if @::*prefix* (strcat "prefix=" @::*prefix*) "prefix=NIL")
|
|
160
|
+
`;
|
|
161
|
+
const setupResult = await mcp.evalLisp(setupCode);
|
|
162
|
+
if (setupResult) {
|
|
163
|
+
if (setupResult.includes("prefix=")) {
|
|
164
|
+
const prefixMatch = setupResult.match(/prefix=([^\s]+)/);
|
|
165
|
+
console.log(chalk.gray(` ${prefixMatch ? 'prefix='+prefixMatch[1] : setupResult.substring(0, 80)}`));
|
|
166
|
+
} else if (setupResult.includes("MISSING") || setupResult.includes("error")) {
|
|
167
|
+
console.log(chalk.yellow(` Setup issue: ${setupResult.substring(0, 100)}`));
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
console.log(chalk.gray(` 6/8 Loading ${filtered.length} test files (batched)...`));
|
|
171
|
+
// Load ALL test files in a SINGLE evalLisp call to survive circuit breaker resets
|
|
172
|
+
const loadExpr = `(setq @::*test-runner-tests* nil)\n(in-package :@)\n` + filtered.map(f =>
|
|
173
|
+
fs.readFileSync(path.join(testDir, f), "utf-8")).join("\n\n");
|
|
174
|
+
const loadResult = await mcp.evalLisp(loadExpr);
|
|
175
|
+
if (loadResult && (loadResult.includes("error") || loadResult.includes("Error")
|
|
176
|
+
|| loadResult.includes("ERROR") || loadResult.includes("no function"))) {
|
|
177
|
+
console.log(chalk.yellow(` Some files had errors (first 200 chars): ${loadResult.substring(0, 200)}`));
|
|
178
|
+
}
|
|
179
|
+
console.log(chalk.gray(` 7/8 Waiting for CAD...`));
|
|
180
|
+
await new Promise(r => setTimeout(r, 1000));
|
|
181
|
+
console.log(chalk.gray(" 8/8 Running tests..."));
|
|
182
|
+
const skipArg = opts && opts.skipPassed
|
|
183
|
+
? `(setq @::*test-skip-file* "${path.join(os.homedir(), "@lisp", ".atlisp", "test-passed.lst").replace(/\\/g, "/")}")`
|
|
184
|
+
: "";
|
|
185
|
+
return await mcp.evalLisp(
|
|
186
|
+
`(if (not (boundp '@::run-tests)) (load "${_runnerPath}"))
|
|
187
|
+
${skipArg}
|
|
188
|
+
(princ (strcat "\\nRegistered " (itoa (length @::*test-runner-tests*)) " tests\\n"))
|
|
189
|
+
(@::run-tests nil)`);
|
|
190
|
+
});
|
|
191
|
+
console.log(chalk.green(" ✓ 测试完成"));
|
|
192
|
+
if (output) console.log(output);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
} catch (e) {
|
|
195
|
+
console.error(chalk.red(` ✗ ${e.message}`));
|
|
196
|
+
if (e.message.includes("Circuit breaker") || e.message.includes("worker")) {
|
|
197
|
+
console.log(chalk.yellow("\nMCP 连接不稳定。请尝试:\n"));
|
|
198
|
+
console.log(chalk.cyan(" 1. 确保 CAD 已打开且无弹窗阻塞\n"));
|
|
199
|
+
console.log(chalk.cyan(" 2. 重启 atlisp-mcp-server\n"));
|
|
200
|
+
console.log(chalk.cyan(" 3. 用 --manual 模式手动测试:\n"));
|
|
201
|
+
console.log(chalk.gray(` node atlisp.js test --platform cad --manual\n`));
|
|
202
|
+
} else if (e.message.includes("ENOENT") || e.message.includes("spawn")) {
|
|
203
|
+
console.log(chalk.yellow("\natlisp-mcp 未安装。请执行:\n"));
|
|
204
|
+
console.log(chalk.cyan(" npm install -g @atlisp/mcp\n"));
|
|
205
|
+
console.log(chalk.gray("或用 --manual 模式手动在 CAD 中执行测试:\n"));
|
|
206
|
+
console.log(chalk.gray(` node atlisp.js test --platform cad --manual\n`));
|
|
207
|
+
}
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function handler(args) {
|
|
214
|
+
const opts = common.parseArgs(args);
|
|
215
|
+
const testFilter = args.find(a => !a.startsWith("--")) || undefined;
|
|
216
|
+
await cmdTest(testFilter, opts);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function register(reg) {
|
|
220
|
+
reg("test", handler, ["t"]);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = { register };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { execSync } = require('child_process');
|
|
4
|
+
|
|
5
|
+
const SRC_DIR = require("../lib/common").getKernelSrc();
|
|
6
|
+
|
|
7
|
+
function register(reg) {
|
|
8
|
+
reg('watch', cmdWatch, ['w']);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
async function cmdWatch() {
|
|
12
|
+
const BUILD_CMD = 'node ../atlisp-cli/atlisp.js build';
|
|
13
|
+
|
|
14
|
+
const debounce = (fn, delay) => {
|
|
15
|
+
let timer;
|
|
16
|
+
return (...args) => {
|
|
17
|
+
clearTimeout(timer);
|
|
18
|
+
timer = setTimeout(() => fn(...args), delay);
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const rebuild = debounce(() => {
|
|
23
|
+
const timestamp = new Date().toLocaleTimeString();
|
|
24
|
+
console.log(`\n[${timestamp}] Change detected. Rebuilding...`);
|
|
25
|
+
try {
|
|
26
|
+
execSync(BUILD_CMD, { cwd: process.cwd(), stdio: 'inherit' });
|
|
27
|
+
console.log(`[${timestamp}] Build complete.`);
|
|
28
|
+
} catch (e) {
|
|
29
|
+
console.error(`[${timestamp}] Build failed.`);
|
|
30
|
+
}
|
|
31
|
+
}, 300);
|
|
32
|
+
|
|
33
|
+
let ready = false;
|
|
34
|
+
fs.watch(SRC_DIR, { recursive: true }, (event, filename) => {
|
|
35
|
+
if (!filename || !filename.endsWith('.lsp')) return;
|
|
36
|
+
if (!ready) { ready = true; return; }
|
|
37
|
+
console.log(` File changed: ${filename}`);
|
|
38
|
+
rebuild();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
console.log(`Watching ${SRC_DIR} for changes...`);
|
|
42
|
+
console.log('Press Ctrl+C to stop.');
|
|
43
|
+
|
|
44
|
+
return new Promise(() => {});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { register };
|