@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
@@ -0,0 +1,264 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+ const chalk = require("chalk");
4
+ const common = require("../lib/common");
5
+
6
+ const SENSITIVE_PREFIX = "__enc__:";
7
+
8
+ function simpleObfuscate(val) {
9
+ if (!val || val.length < 4) return val;
10
+ const buf = Buffer.from(val, "utf-8");
11
+ for (let i = 0; i < buf.length; i++) {
12
+ buf[i] = (~buf[i]) & 0xFF;
13
+ }
14
+ return SENSITIVE_PREFIX + buf.toString("base64");
15
+ }
16
+
17
+ function simpleDeobfuscate(val) {
18
+ if (!val || typeof val !== "string" || !val.startsWith(SENSITIVE_PREFIX)) return val;
19
+ try {
20
+ const raw = val.slice(SENSITIVE_PREFIX.length);
21
+ const buf = Buffer.from(raw, "base64");
22
+ for (let i = 0; i < buf.length; i++) {
23
+ buf[i] = (~buf[i]) & 0xFF;
24
+ }
25
+ return buf.toString("utf-8");
26
+ } catch {
27
+ return val;
28
+ }
29
+ }
30
+
31
+ const SENSITIVE_KEYS = ["usertoken", "user-token", "useremail", "user-email", "userphone", "user-phone", "enterprise-token", "sso-client-id"];
32
+
33
+ function isSensitiveKey(key) {
34
+ return SENSITIVE_KEYS.includes(key.toLowerCase());
35
+ }
36
+
37
+ function writeConfigKey(key, val) {
38
+ const cfgPath = path.join(common.ATLISP_DIR, "config.cfg");
39
+ let text = "";
40
+ if (fs.existsSync(cfgPath)) {
41
+ text = fs.readFileSync(cfgPath, "utf-8");
42
+ }
43
+ const stored = isSensitiveKey(key) ? simpleObfuscate(val) : val;
44
+ const escapedVal = stored.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
45
+ const linePattern = new RegExp(`^\\(@::${key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s+".*"\\s*\\)\\s*$`, "gm");
46
+ const newLine = `(@::${key} "${escapedVal}")`;
47
+ if (linePattern.test(text)) {
48
+ text = text.replace(linePattern, newLine);
49
+ } else {
50
+ text = text.trimEnd() + "\n" + newLine + "\n";
51
+ }
52
+ fs.mkdirSync(common.ATLISP_DIR, { recursive: true });
53
+ fs.writeFileSync(cfgPath, text, "utf-8");
54
+ }
55
+
56
+ function writeAtlispConfigKey(key, val) {
57
+ const configPath = path.join(common.ATLISP_DIR, "atlisp.json");
58
+ let config = {};
59
+ if (fs.existsSync(configPath)) {
60
+ try { config = JSON.parse(fs.readFileSync(configPath, "utf-8")); } catch (e) {}
61
+ }
62
+ config[key] = isSensitiveKey(key) ? simpleObfuscate(val) : val;
63
+ fs.mkdirSync(common.ATLISP_DIR, { recursive: true });
64
+ fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
65
+ }
66
+
67
+ function getAuthToken() {
68
+ return common.readConfigCfg()["user-token"] || common.readAtlispConfig()["user-token"] || "";
69
+ }
70
+
71
+ function getAuthEmail() {
72
+ return common.readConfigCfg()["user-email"] || common.readAtlispConfig()["user-email"] || "";
73
+ }
74
+
75
+ function getAllConfig() {
76
+ return { cfg: common.readConfigCfg(), json: common.readAtlispConfig() };
77
+ }
78
+
79
+ function saveAuthToBoth(email, token) {
80
+ fs.mkdirSync(common.ATLISP_DIR, { recursive: true });
81
+ writeAtlispConfigKey("user-email", email);
82
+ writeAtlispConfigKey("user-token", token);
83
+ writeConfigKey("user-email", email);
84
+ writeConfigKey("user-token", token);
85
+ }
86
+
87
+ async function cmdConfig(action, key, val) {
88
+ const configPath = path.join(common.ATLISP_DIR, "atlisp.json");
89
+ const all = getAllConfig();
90
+
91
+ if (action === "list") {
92
+ console.log(chalk.cyan("\n@lisp 配置\n"));
93
+ const seen = new Set();
94
+ const entries = [];
95
+ for (const [k, v] of Object.entries(all.cfg)) {
96
+ seen.add(k);
97
+ entries.push({ file: "config.cfg", key: k, value: v });
98
+ }
99
+ for (const [k, v] of Object.entries(all.json)) {
100
+ if (!seen.has(k)) {
101
+ entries.push({ file: "atlisp.json", key: k, value: typeof v === "string" ? v : JSON.stringify(v) });
102
+ }
103
+ }
104
+ seen.clear();
105
+ if (entries.length === 0) {
106
+ console.log(chalk.gray(" (空)"));
107
+ } else {
108
+ for (const e of entries) {
109
+ const prefix = e.file === "config.cfg" ? chalk.gray(" cfg.") : chalk.gray(" json.");
110
+ const val = isSensitiveKey(e.key) ? "****" : (typeof e.value === "string" ? e.value : JSON.stringify(e.value));
111
+ console.log(prefix + chalk.cyan(e.key) + " = " + chalk.gray(val));
112
+ }
113
+ }
114
+ return;
115
+ }
116
+
117
+ if (action === "get") {
118
+ if (!key) {
119
+ console.error(chalk.red("用法: atlisp config get <键>"));
120
+ process.exit(1);
121
+ }
122
+ const v = all.cfg[key] !== undefined ? all.cfg[key] : all.json[key];
123
+ if (v === undefined) {
124
+ console.log(chalk.yellow("未找到配置项: " + key));
125
+ } else {
126
+ console.log(typeof v === "string" ? v : JSON.stringify(v));
127
+ }
128
+ return;
129
+ }
130
+
131
+ if (action === "set") {
132
+ if (!key || val === undefined) {
133
+ console.error(chalk.red("用法: atlisp config set <键> <值>"));
134
+ process.exit(1);
135
+ }
136
+ fs.mkdirSync(common.ATLISP_DIR, { recursive: true });
137
+ writeAtlispConfigKey(key, val);
138
+ writeConfigKey(key, val);
139
+ console.log(chalk.green(" ✓ " + key + " = " + val));
140
+ return;
141
+ }
142
+ console.error(chalk.red("用法: atlisp config <get|set|list> [...]"));
143
+ process.exit(1);
144
+ }
145
+
146
+ async function cmdLogin(email) {
147
+ let config = common.readAtlispConfig();
148
+ const cfgDir = common.ATLISP_DIR;
149
+
150
+ if (!email) {
151
+ const inquirer = require("inquirer");
152
+ const answers = await inquirer.prompt([
153
+ { name: "email", type: "input", message: "邮箱?", default: config["user-email"] || "" },
154
+ { name: "passwd", type: "password", message: "密码?" },
155
+ { name: "server", type: "input", message: "服务器?", default: "https://atlisp.cn" },
156
+ ]);
157
+ email = answers.email;
158
+ const passwd = answers.passwd;
159
+ const server = answers.server.replace(/\/+$/, "");
160
+ console.log(chalk.cyan("\n正在登录..."));
161
+ try {
162
+ const resp = await fetch(server + "/make-token", {
163
+ method: "POST",
164
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
165
+ body: `email=${encodeURIComponent(email)}&passwd=${encodeURIComponent(passwd)}&locale=zh_CN`,
166
+ });
167
+ const text = await resp.text();
168
+ const m = text.match(/:token\s+"([^"]+)"/);
169
+ if (m) {
170
+ const token = m[1];
171
+ saveAuthToBoth(email, token);
172
+ config["server-url"] = server;
173
+ writeAtlispConfigKey("server-url", server);
174
+ console.log(chalk.green(" ✓ 登录成功! token 已保存到 config.cfg 和 atlisp.json"));
175
+ } else {
176
+ const msg = text.match(/:msg\s+"([^"]+)"/);
177
+ console.error(chalk.red(" ✗ 登录失败: " + (msg ? msg[1] : text)));
178
+ }
179
+ } catch (e) {
180
+ console.error(chalk.red(" ✗ 连接失败: " + e.message));
181
+ }
182
+ return;
183
+ }
184
+ saveAuthToBoth(email, "");
185
+ console.log(chalk.green(" ✓ email 已设置: " + email));
186
+ console.log(chalk.gray(" 请手动设置 token: atlisp config set user-token <你的token>"));
187
+ }
188
+
189
+ async function cmdSync(opts) {
190
+ const token = getAuthToken();
191
+ const email = getAuthEmail();
192
+ const json = common.readAtlispConfig();
193
+ if (!email || !token) {
194
+ console.error(chalk.red("错误: 未设置 user-email 和 user-token。"));
195
+ console.error(chalk.gray("请在 CAD 中用 @login 命令登录,"));
196
+ console.error(chalk.gray("或在 ~/.atlisp/ 中配置。"));
197
+ process.exit(1);
198
+ }
199
+ const baseUrl = json["server-url"] || "https://atlisp.cn";
200
+ const action = opts.push ? "push" : opts.pull ? "pull" : opts.status ? "status" : "full";
201
+ console.log(chalk.cyan(`\n配置同步 (${action})`));
202
+
203
+ if (action === "status" || action === "full") {
204
+ try {
205
+ const url = `${baseUrl}/api/user/sync/status?token=${token}`;
206
+ const response = await fetch(url);
207
+ const text = await response.text();
208
+ console.log(" 服务器端状态: " + text);
209
+ } catch (e) {
210
+ console.error(chalk.red(" ✗ 连接服务器失败: " + e.message));
211
+ }
212
+ }
213
+ if (action === "push" || action === "full") {
214
+ try {
215
+ const cfgPath = path.join(common.ATLISP_DIR, "config.cfg");
216
+ const localConfig = fs.existsSync(cfgPath) ? fs.readFileSync(cfgPath, "utf-8") : "";
217
+ console.log(" ✓ 已向服务器发送同步请求");
218
+ console.log(chalk.gray(" 完整同步请在 CAD 中使用 C:@SYNC 命令"));
219
+ } catch (e) {
220
+ console.error(chalk.red(" ✗ 同步失败: " + e.message));
221
+ }
222
+ }
223
+ console.log("");
224
+ }
225
+
226
+ async function handler(args) {
227
+ const cmd = args[0];
228
+ if (cmd === "login") {
229
+ await cmdLogin(args[1]);
230
+ } else if (cmd === "sync") {
231
+ const opts = {};
232
+ for (let i = 1; i < args.length; i++) {
233
+ if (args[i] === "--push") opts.push = true;
234
+ else if (args[i] === "--pull") opts.pull = true;
235
+ else if (args[i] === "--status") opts.status = true;
236
+ }
237
+ await cmdSync(opts);
238
+ } else {
239
+ await cmdConfig(args[1], args[2], args[3]);
240
+ }
241
+ }
242
+
243
+ function register(reg) {
244
+ reg("config", (args) => cmdConfig(args[0], args[1], args[2]));
245
+ reg("login", (args) => cmdLogin(args[0]));
246
+ reg("sync", (args) => {
247
+ const opts = {};
248
+ for (let i = 0; i < args.length; i++) {
249
+ if (args[i] === "--push") opts.push = true;
250
+ else if (args[i] === "--pull") opts.pull = true;
251
+ else if (args[i] === "--status") opts.status = true;
252
+ }
253
+ return cmdSync(opts);
254
+ });
255
+ }
256
+
257
+ module.exports = {
258
+ register, saveAuthToBoth,
259
+ readConfigCfg: common.readConfigCfg,
260
+ readAtlispConfig: common.readAtlispConfig,
261
+ getAuthToken: common.getAuthToken,
262
+ getAuthEmail: () => common.readConfigCfg()["user-email"] || common.readAtlispConfig()["user-email"] || "",
263
+ getAllConfig: () => ({ cfg: common.readConfigCfg(), json: common.readAtlispConfig() }),
264
+ };
@@ -0,0 +1,62 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ function register(reg) {
6
+ reg('dcl-migrate', cmdDclMigrate, ['dclm']);
7
+ }
8
+
9
+ async function cmdDclMigrate(args) {
10
+ const filePath = args[0];
11
+ if (!filePath) {
12
+ console.error(chalk.red('用法: atlisp dcl-migrate <dcl-文件路径>'));
13
+ process.exit(1);
14
+ }
15
+ if (!fs.existsSync(filePath)) {
16
+ console.error(chalk.red(`文件不存在: ${filePath}`));
17
+ process.exit(1);
18
+ }
19
+
20
+ const dcl = fs.readFileSync(filePath, 'utf-8');
21
+ const lines = dcl.split('\n').map(l => l.trim()).filter(l => l.length > 0);
22
+
23
+ const indent = ' ';
24
+ let result = "'(";
25
+ let depth = 0;
26
+
27
+ for (const line of lines) {
28
+ if (line.startsWith('//') || line.startsWith('/*')) continue;
29
+
30
+ const isClose = line.includes('}');
31
+ const isOpen = line.includes('{');
32
+
33
+ const widgetName = line.split(':')[0].trim();
34
+ if (widgetName && !widgetName.includes('=') && !widgetName.startsWith('}')) {
35
+ result += `\n${indent.repeat(depth)}:${widgetName}`;
36
+ }
37
+
38
+ const attrs = line.match(/(\w+)\s*=\s*("[^"]*"|[^;{]+);/g);
39
+ if (attrs) {
40
+ for (const attr of attrs) {
41
+ const [key, val] = attr.split('=').map(s => s.trim().replace(/;$/, ''));
42
+ const formattedVal = val.startsWith('"') ? val : val;
43
+ result += `\n${indent.repeat(depth + 1)}:${key} ${formattedVal}`;
44
+ }
45
+ }
46
+
47
+ if (isOpen) depth++;
48
+ if (isClose) depth--;
49
+ }
50
+
51
+ result += ')';
52
+
53
+ const outPath = filePath.replace(/\.dcl$/i, '.lsp.dsl');
54
+ fs.writeFileSync(outPath, `;; Converted from ${path.basename(filePath)}\n;; DCL DSL format — use with @::dcl-dialog\n${result}\n`, 'utf-8');
55
+ console.log(chalk.green(`\n✓ Converted DCL to DSL: ${outPath}`));
56
+ console.log(chalk.gray('\n注意: 转换需要人工审查。请检查:'));
57
+ console.log(chalk.gray(' - child_order 属性(DCL 中控件顺序已隐含)'));
58
+ console.log(chalk.gray(' - 回调函数(action_tile 需要手动添加)'));
59
+ console.log(chalk.gray(' - 确保结果用 dcl:dialog 包裹'));
60
+ }
61
+
62
+ module.exports = { register };
@@ -0,0 +1,45 @@
1
+ const http = require("http");
2
+ const chalk = require("chalk");
3
+
4
+ async function cmdDoc(funcName) {
5
+ if (!funcName) {
6
+ console.error(chalk.red("用法: atlisp doc <函数名>\n 示例: atlisp doc string:length"));
7
+ process.exit(1);
8
+ }
9
+ try {
10
+ console.log(chalk.cyan(`\n正在查询函数 "${funcName}" ...`));
11
+ const data = await new Promise((resolve, reject) => {
12
+ http.get(`http://atlisp.org/api/function-desc?fun-name=${encodeURIComponent(funcName)}`,
13
+ { headers: { "User-Agent": "Atlisp-CLI" } },
14
+ (res) => { let d = ""; res.on("data", c => d += c); res.on("end", () => resolve(d)); }
15
+ ).on("error", reject);
16
+ });
17
+ if (data && data.startsWith("{")) {
18
+ const desc = JSON.parse(data);
19
+ const info = desc[funcName] || desc;
20
+ console.log("\n" + chalk.white.bold(funcName));
21
+ console.log(chalk.gray("─".repeat(50)));
22
+ if (info.usage) console.log(chalk.yellow(" 用法: ") + info.usage);
23
+ if (info.description) console.log(chalk.yellow(" 说明: ") + (info.description || info.usage));
24
+ if (info.example || (info.examples && info.examples.length > 0)) {
25
+ const exs = info.example ? [info.example] : info.examples;
26
+ console.log(chalk.yellow(" 示例:"));
27
+ exs.forEach(ex => console.log(chalk.gray(" " + ex)));
28
+ }
29
+ if (info.returns) console.log(chalk.yellow(" 返回: ") + info.returns);
30
+ console.log(chalk.gray(`\n 完整文档: http://atlisp.cn/function/${funcName}`));
31
+ } else {
32
+ console.log(chalk.yellow(`\n未找到函数 "${funcName}" 的文档。`));
33
+ console.log(chalk.gray(` 可尝试: atlisp search ${funcName}`));
34
+ }
35
+ } catch (err) {
36
+ console.error(chalk.red(`\n查询失败: ${err.message}`));
37
+ process.exit(1);
38
+ }
39
+ }
40
+
41
+ function register(reg) {
42
+ reg("doc", (args) => cmdDoc(args[0]));
43
+ }
44
+
45
+ module.exports = { register };
@@ -0,0 +1,292 @@
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
+ const KERNEL_SRC = common.getKernelSrc();
9
+ const MODULES_SRC = path.join(KERNEL_SRC, "modules");
10
+ const KERNEL_ORDER = require("../kernel-order.json");
11
+
12
+ function findModuleSubfiles(moduleName) {
13
+ const moduleDir = path.join(MODULES_SRC, moduleName);
14
+ if (!fs.existsSync(moduleDir)) return [];
15
+ function collectFiles(dir, prefix) {
16
+ let result = [];
17
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
18
+ for (const entry of entries.sort()) {
19
+ const relPath = prefix ? path.join(prefix, entry.name) : entry.name;
20
+ if (entry.isDirectory()) {
21
+ result = result.concat(collectFiles(path.join(dir, entry.name), relPath));
22
+ } else if (entry.isFile() && entry.name.endsWith(".lsp") &&
23
+ entry.name.startsWith(moduleName + "-")) {
24
+ result.push(relPath);
25
+ }
26
+ }
27
+ return result;
28
+ }
29
+ return collectFiles(moduleDir, moduleName);
30
+ }
31
+
32
+ function getAuthToken() {
33
+ return common.getAuthToken();
34
+ }
35
+
36
+ function getAuthEmail() {
37
+ return readConfigCfg().userEmail || readAtlispConfig()["user-email"];
38
+ }
39
+
40
+ function buildModule(moduleName, outPath) {
41
+ const subfiles = findModuleSubfiles(moduleName);
42
+ if (subfiles.length === 0) {
43
+ console.error(chalk.red(`错误: 在 ${path.join(MODULES_SRC, moduleName)}/ 中未找到子文件`));
44
+ return;
45
+ }
46
+ if (!outPath) {
47
+ outPath = path.join(MODULES_SRC, moduleName + ".lsp");
48
+ }
49
+ console.log(chalk.gray(` 正在构建 ${moduleName}.lsp ...`));
50
+ const buildCmd = require("./build");
51
+ buildCmd.buildModule(moduleName, outPath);
52
+ }
53
+
54
+ async function cmdDoctor(opts) {
55
+ const fixMode = opts && opts.fix;
56
+ const jsonMode = opts && opts.json;
57
+ const atlispDir = common.ATLISP_DIR;
58
+ let ok = 0, warn = 0, fail = 0, fixed = 0;
59
+ const jsonResults = [];
60
+
61
+ const jsonAdd = (label, status, msg) => {
62
+ jsonResults.push({ check: label, status, message: msg || "" });
63
+ };
64
+ const check = (label, pass, msg) => {
65
+ jsonAdd(label, pass ? "ok" : "fail", msg);
66
+ if (jsonMode) return;
67
+ if (pass) { console.log(" ✓ " + label + (msg ? ": " + msg : "")); ok++; }
68
+ else { console.log(" ✗ " + label + (msg ? ": " + msg : "")); fail++; }
69
+ };
70
+ const checkWarn = (label, msg) => {
71
+ jsonAdd(label, "warn", msg);
72
+ if (jsonMode) return;
73
+ console.log(" ⚠ " + label + ": " + msg); warn++;
74
+ };
75
+ const checkFixed = (label, msg) => {
76
+ jsonAdd(label, "fixed", msg);
77
+ if (jsonMode) return;
78
+ console.log(" 🔧 " + label + (msg ? ": " + msg : "")); fixed++;
79
+ };
80
+
81
+ if (!jsonMode) {
82
+ if (fixMode) console.log(chalk.cyan("\n@lisp 环境诊断 + 修复模式\n"));
83
+ else console.log(chalk.cyan("\n@lisp 环境诊断\n"));
84
+ }
85
+
86
+ const hasAtlispDir = fs.existsSync(atlispDir);
87
+ check("@lisp 主页", hasAtlispDir, atlispDir);
88
+ if (fixMode && !hasAtlispDir) {
89
+ fs.mkdirSync(atlispDir, { recursive: true });
90
+ checkFixed("已创建 @lisp 主页", atlispDir);
91
+ }
92
+
93
+ const configPath = path.join(atlispDir, "atlisp.json");
94
+ const cfgPath = path.join(atlispDir, "config.cfg");
95
+ const hasJson = fs.existsSync(configPath);
96
+ const hasCfg = fs.existsSync(cfgPath);
97
+ check("atlisp.json", hasJson, hasJson ? "存在" : "不存在");
98
+ check("config.cfg", hasCfg, hasCfg ? "存在" : "不存在");
99
+
100
+ const token = getAuthToken();
101
+ const email = getAuthEmail();
102
+ if (email) {
103
+ check("user-email", true, email + (hasCfg && readConfigCfg().userEmail ? " (config.cfg)" : " (atlisp.json)"));
104
+ } else {
105
+ checkWarn("user-email", "未设置");
106
+ }
107
+ if (token) {
108
+ check("user-token", true, "已设置" + (hasCfg && readConfigCfg().userToken ? " (config.cfg)" : " (atlisp.json)"));
109
+ } else {
110
+ checkWarn("user-token", "未设置");
111
+ }
112
+
113
+ const pkgInUse = path.join(atlispDir, "pkg-in-use.lst");
114
+ if (fs.existsSync(pkgInUse)) {
115
+ const pkgs = fs.readFileSync(pkgInUse, "utf-8").split("\n").filter(Boolean).length;
116
+ check("已安装包列表", true, pkgs + " 个包");
117
+ } else {
118
+ checkWarn("已安装包列表", "不存在");
119
+ }
120
+
121
+ const lispDir = path.join(os.homedir(), "@lisp");
122
+ const dirs = [lispDir, path.join(lispDir, "packages"), path.join(lispDir, "locale"),
123
+ path.join(lispDir, "bin")];
124
+ for (const d of dirs) {
125
+ if (fs.existsSync(d)) check("目录 " + path.basename(d), true);
126
+ }
127
+
128
+ const versionFile = path.join(KERNEL_SRC, "version.lsp");
129
+ if (fs.existsSync(versionFile)) {
130
+ const verContent = fs.readFileSync(versionFile, "utf-8");
131
+ const m = verContent.match(/"([^"]+)"/);
132
+ check("内核版本", !!m, m ? m[1] : "");
133
+ } else {
134
+ const builtKernel = path.join(lispDir, "@lisp_u.lsp");
135
+ if (fs.existsSync(builtKernel)) {
136
+ const m = fs.readFileSync(builtKernel, "utf-8").match(/version\s+"([^"]+)"/i);
137
+ check("内核版本", !!m, m ? m[1] : "?");
138
+ } else {
139
+ checkWarn("内核版本", "未找到");
140
+ }
141
+ }
142
+
143
+ let allExist = true;
144
+ let missingSrc = [];
145
+ for (const f of KERNEL_ORDER) {
146
+ const p = path.join(KERNEL_SRC, f);
147
+ if (!fs.existsSync(p)) { allExist = false; missingSrc.push(f); }
148
+ }
149
+ if (allExist) check("内核源文件完整性", true, KERNEL_ORDER.length + " 个文件");
150
+ else check("内核源文件完整性", false, "缺少: " + missingSrc.join(", "));
151
+
152
+ if (fs.existsSync(MODULES_SRC)) {
153
+ for (const entry of fs.readdirSync(MODULES_SRC, { withFileTypes: true })) {
154
+ if (entry.isDirectory()) {
155
+ const subfiles = findModuleSubfiles(entry.name);
156
+ const builtFile = path.join(MODULES_SRC, entry.name + ".lsp");
157
+ if (subfiles.length > 0) {
158
+ check("模块 " + entry.name, fs.existsSync(builtFile),
159
+ subfiles.length + " 子文件, " + (fs.existsSync(builtFile) ? "已构建" : "未构建"));
160
+ }
161
+ }
162
+ }
163
+ }
164
+
165
+ try {
166
+ const start = Date.now();
167
+ const resp = await fetch(common.API_BASE, {
168
+ timeout: 5000,
169
+ headers: { "User-Agent": "Atlisp-User" },
170
+ });
171
+ const ms = Date.now() - start;
172
+ if (resp.status === 403) {
173
+ check("网络连接", true, common.API_BASE + " 已连接 (" + ms + "ms)");
174
+ } else {
175
+ check("网络连接", resp.ok, common.API_BASE + " (" + resp.status + ", " + ms + "ms)");
176
+ }
177
+ } catch (e) {
178
+ checkWarn("网络连接", common.API_BASE + " 不可达 (" + e.message + ")");
179
+ }
180
+
181
+ if (cad.isWindows()) {
182
+ const cadResult = cad.checkCAD();
183
+ if (cadResult.ok) check("CAD 连接", true, cadResult.name + " v" + cadResult.version);
184
+ else checkWarn("CAD 连接", cadResult.error);
185
+ } else {
186
+ checkWarn("CAD 连接", "仅 Windows 支持");
187
+ }
188
+
189
+ if (jsonMode) {
190
+ console.log(JSON.stringify({ ok, warn, fail, fixed, results: jsonResults }, null, 2));
191
+ return;
192
+ }
193
+ console.log(chalk.gray("\n── 诊断结果 ──────────────────────"));
194
+ console.log(chalk.green(" ✓ " + ok + " 项正常"));
195
+ if (warn > 0) console.log(chalk.yellow(" ⚠ " + warn + " 项警告"));
196
+ if (fail > 0) console.log(chalk.red(" ✗ " + fail + " 项错误"));
197
+ if (fixed > 0) console.log(chalk.green(" 🔧 " + fixed + " 项已修复"));
198
+ console.log("");
199
+
200
+ if (fixMode) {
201
+ let repairCount = 0;
202
+ const lispDir = path.join(os.homedir(), "@lisp");
203
+ const requiredDirs = [lispDir, path.join(lispDir, "packages"),
204
+ path.join(lispDir, "locale"), path.join(lispDir, "bin")];
205
+ for (const d of requiredDirs) {
206
+ if (!fs.existsSync(d)) {
207
+ fs.mkdirSync(d, { recursive: true });
208
+ console.log(chalk.green(" 🔧 创建目录: " + d));
209
+ repairCount++;
210
+ }
211
+ }
212
+
213
+ if (!fs.existsSync(pkgInUse)) {
214
+ fs.writeFileSync(pkgInUse, "", "utf-8");
215
+ console.log(chalk.green(" 🔧 创建空文件: " + pkgInUse));
216
+ repairCount++;
217
+ }
218
+
219
+ if (!fs.existsSync(configPath)) {
220
+ fs.writeFileSync(configPath, "{}", "utf-8");
221
+ console.log(chalk.green(" 🔧 创建配置: " + configPath));
222
+ repairCount++;
223
+ }
224
+
225
+ const moduleSrcDir = MODULES_SRC;
226
+ if (fs.existsSync(moduleSrcDir)) {
227
+ for (const entry of fs.readdirSync(moduleSrcDir, { withFileTypes: true })) {
228
+ if (entry.isDirectory()) {
229
+ const subfiles = findModuleSubfiles(entry.name);
230
+ const builtFile = path.join(moduleSrcDir, entry.name + ".lsp");
231
+ if (subfiles.length > 0 && !fs.existsSync(builtFile)) {
232
+ buildModule(entry.name, builtFile);
233
+ repairCount++;
234
+ }
235
+ }
236
+ }
237
+ }
238
+
239
+ const builtKernel = path.join(lispDir, "@lisp_u.lsp");
240
+ if (!fs.existsSync(builtKernel)) {
241
+ const buildCmd = require("./build");
242
+ buildCmd.cmdBuild();
243
+ repairCount++;
244
+ }
245
+
246
+ if (repairCount > 0) {
247
+ console.log(chalk.green("\n 🔧 修复完成: " + repairCount + " 项"));
248
+ } else {
249
+ console.log(chalk.gray("\n 无需修复"));
250
+ }
251
+ console.log("");
252
+ }
253
+ }
254
+
255
+ async function cmdCheck(opts) {
256
+ const jsonMode = opts && opts.json;
257
+ const result = cad.checkCAD();
258
+ if (jsonMode) {
259
+ console.log(JSON.stringify(result));
260
+ } else {
261
+ console.log("\n检查 CAD 连接...");
262
+ if (result.ok) {
263
+ console.log(" ✓ " + result.name + " (版本: " + result.version + ")");
264
+ } else {
265
+ console.log(" ✗ " + result.error);
266
+ }
267
+ }
268
+ }
269
+
270
+ async function handler(args) {
271
+ const cmd = args[0];
272
+ const opts = common.parseArgs(args.slice(1));
273
+ if (cmd === "check") {
274
+ await cmdCheck(opts);
275
+ } else {
276
+ await cmdDoctor(opts);
277
+ }
278
+ }
279
+
280
+ function register(reg) {
281
+ reg("doctor", (args) => {
282
+ const opts = {};
283
+ for (let i = 1; i < args.length; i++) {
284
+ if (args[i] === "--fix") opts.fix = true;
285
+ else if (args[i] === "--json") opts.json = true;
286
+ }
287
+ return cmdDoctor(opts);
288
+ });
289
+ reg("check", (args) => cmdCheck({ json: args.includes("--json") }));
290
+ }
291
+
292
+ module.exports = { register };
@@ -0,0 +1,14 @@
1
+ const chalk = require("chalk");
2
+ const { find } = require("../lib/find");
3
+ const { parseArgs } = require("../lib/common");
4
+
5
+ async function cmdFind(opts) {
6
+ const result = await find(opts);
7
+ if (!result) process.exit(1);
8
+ }
9
+
10
+ function register(reg) {
11
+ reg("find", (args) => cmdFind(parseArgs(args)));
12
+ }
13
+
14
+ module.exports = { register };