@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,203 @@
1
+ const { spawn } = require("child_process");
2
+ const path = require("path");
3
+ const fs = require("fs");
4
+ const readline = require("readline");
5
+
6
+ function resolveMcpCommand() {
7
+ if (process.platform !== "win32") return { cmd: "npx", args: ["@atlisp/mcp"] };
8
+ const nodeDir = path.dirname(process.execPath);
9
+ const npx = path.join(nodeDir, "npx.cmd");
10
+ if (fs.existsSync(npx)) return { cmd: process.env.COMSPEC || "cmd.exe", args: ["/c", npx, "@atlisp/mcp"] };
11
+ return { cmd: process.env.COMSPEC || "cmd.exe", args: ["/c", "npx.cmd", "@atlisp/mcp"] };
12
+ }
13
+
14
+ class McpClient {
15
+ constructor(cmd, args) {
16
+ if (cmd) {
17
+ this.cmd = cmd;
18
+ this.args = args || [];
19
+ } else {
20
+ const resolved = resolveMcpCommand();
21
+ this.cmd = resolved.cmd;
22
+ this.args = resolved.args;
23
+ }
24
+ this.process = null;
25
+ this.rl = null;
26
+ this.pending = new Map();
27
+ this.nextId = 1;
28
+ }
29
+
30
+ async connect(timeout = 120000) {
31
+ this.process = spawn(this.cmd, [...this.args, "--stdio", "--security-level", "relaxed"], {
32
+ stdio: ["pipe", "pipe", "pipe"],
33
+ });
34
+
35
+ this.process.stderr.on("data", (d) => {
36
+ const s = d.toString().trim();
37
+ if (s) process.stderr.write(s + "\n");
38
+ });
39
+
40
+ this.process.on("error", (err) => {
41
+ for (const [, resolver] of this.pending) {
42
+ resolver({ error: { message: err.message } });
43
+ }
44
+ this.pending.clear();
45
+ });
46
+
47
+ this.process.on("exit", (code) => {
48
+ for (const [, resolver] of this.pending) {
49
+ resolver({ error: { message: `MCP process exited with code ${code}` } });
50
+ }
51
+ this.pending.clear();
52
+ });
53
+
54
+ this.rl = readline.createInterface({
55
+ input: this.process.stdout,
56
+ crlfDelay: Infinity,
57
+ });
58
+
59
+ this.rl.on("line", (line) => {
60
+ try {
61
+ const msg = JSON.parse(line);
62
+ const resolver = this.pending.get(msg.id);
63
+ if (resolver) {
64
+ this.pending.delete(msg.id);
65
+ resolver(msg);
66
+ }
67
+ } catch (e) {
68
+ // ignore malformed lines
69
+ }
70
+ });
71
+
72
+ const init = await this.request("initialize", {
73
+ protocolVersion: "2024-11-05",
74
+ capabilities: {},
75
+ clientInfo: { name: "atlisp-cli", version: "1.0.0" },
76
+ }, timeout);
77
+
78
+ return init;
79
+ }
80
+
81
+ request(method, params, timeout = 120000) {
82
+ const id = this.nextId++;
83
+ return new Promise((resolve, reject) => {
84
+ const timer = setTimeout(() => {
85
+ this.pending.delete(id);
86
+ reject(new Error(`MCP request "${method}" timed out after ${timeout}ms`));
87
+ }, timeout);
88
+ this.pending.set(id, (msg) => {
89
+ clearTimeout(timer);
90
+ if (msg.error) reject(new Error(msg.error.message));
91
+ else resolve(msg.result);
92
+ });
93
+ this.process.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
94
+ });
95
+ }
96
+
97
+ async callTool(name, args = {}) {
98
+ return this.request("tools/call", { name, arguments: args });
99
+ }
100
+
101
+ async evalLisp(code, encoding, retries = 3) {
102
+ for (let i = 0; i < retries; i++) {
103
+ try {
104
+ const r = await this._evalRaw(code, encoding);
105
+ if (r !== "No open document") return r;
106
+ // "No open document" - wait and retry
107
+ if (i < retries - 1) {
108
+ try { await this.callTool("new_document"); } catch (e) { /* ignore */ }
109
+ await new Promise(r => setTimeout(r, 3000));
110
+ }
111
+ } catch (e) {
112
+ if (i >= retries - 1) throw e;
113
+ // circuit breaker or other transient error - wait
114
+ await new Promise(r => setTimeout(r, 3000));
115
+ }
116
+ }
117
+ return null;
118
+ }
119
+
120
+ async ensureDocument(timeout = 120000) {
121
+ const deadline = Date.now() + timeout;
122
+ let lastErr = "";
123
+ while (Date.now() < deadline) {
124
+ try {
125
+ const r = await this._evalRaw("(getvar \"dwgname\")");
126
+ if (r && r !== "No open document" && r.length > 0) {
127
+ return;
128
+ }
129
+ if (r) lastErr = r;
130
+ } catch (e) {
131
+ lastErr = e.message;
132
+ }
133
+ if (lastErr.includes("No open document")) {
134
+ try {
135
+ await this.callTool("new_document");
136
+ } catch (e) { /* ignore */ }
137
+ }
138
+ for (let i = 0; i < 20 && Date.now() < deadline; i++) {
139
+ try {
140
+ const r = await this._evalRaw("(getvar \"dwgname\")");
141
+ if (r && r !== "No open document" && r.length > 0) {
142
+ return;
143
+ }
144
+ if (r) lastErr = r;
145
+ } catch (e) {
146
+ lastErr = e.message;
147
+ }
148
+ await new Promise(r => setTimeout(r, 500));
149
+ }
150
+ }
151
+ throw new Error("CAD 文档就绪超时: " + lastErr);
152
+ }
153
+
154
+ async _evalRaw(code, encoding) {
155
+ const args = { code };
156
+ if (encoding) args.encoding = encoding;
157
+ const result = await this.callTool("eval_lisp_with_result", args);
158
+ if (result && result.content && result.content.length > 0) {
159
+ return result.content.map((c) => c.text).join("\n");
160
+ }
161
+ return JSON.stringify(result);
162
+ }
163
+
164
+ async evalLisp(code, encoding, retries = 3) {
165
+ for (let i = 0; i < retries; i++) {
166
+ try {
167
+ const r = await this._evalRaw(code, encoding);
168
+ if (r !== "No open document") return r;
169
+ // "No open document" - wait and retry
170
+ if (i < retries - 1) {
171
+ try { await this.callTool("new_document"); } catch (e) { /* ignore */ }
172
+ await new Promise(r => setTimeout(r, 2000));
173
+ }
174
+ } catch (e) {
175
+ if (i >= retries - 1) throw e;
176
+ // circuit breaker or other transient error - wait
177
+ await new Promise(r => setTimeout(r, 2000));
178
+ }
179
+ }
180
+ return null;
181
+ }
182
+
183
+ async disconnect() {
184
+ if (this.process) {
185
+ try { this.process.stdin.end(); } catch (e) { /* ignore */ }
186
+ try { this.process.kill(); } catch (e) { /* ignore */ }
187
+ this.process = null;
188
+ this.rl = null;
189
+ }
190
+ }
191
+ }
192
+
193
+ async function withMcp(fn) {
194
+ const mcp = new McpClient();
195
+ try {
196
+ await mcp.connect();
197
+ return await fn(mcp);
198
+ } finally {
199
+ await mcp.disconnect();
200
+ }
201
+ }
202
+
203
+ module.exports = { McpClient, withMcp };
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@atlisp/cli",
3
+ "version": "1.2.0",
4
+ "description": "@lisp CLI — 在 AutoCAD/ZWCAD/GStarCAD/BricsCAD 中管理 AutoLISP 包、构建内核、运行测试",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "atlisp": "atlisp.js",
8
+ "atlisp-node": "atlisp.js"
9
+ },
10
+ "scripts": {
11
+ "test": "jest",
12
+ "start": "node atlisp.js"
13
+ },
14
+ "repository": {
15
+ "type": "git",
16
+ "url": "https://gitee.com/atlisp/atlisp-cli"
17
+ },
18
+ "homepage": "https://atlisp.cn",
19
+ "keywords": ["atlisp", "@lisp", "CAD", "autolisp", "autocad", "zwcad", "gstarcad", "bricscad"],
20
+ "author": "VitalGG",
21
+ "license": "ISC",
22
+ "engines": { "node": ">=18" },
23
+ "dependencies": {
24
+ "@atlisp/lint": "^0.2.8",
25
+ "chalk": "^4.1.2",
26
+ "figlet": "^1.8.0",
27
+ "inquirer": "^8.2.6",
28
+ "node-fetch": "^3.3.2",
29
+ "qrcode": "^1.5.4",
30
+ "ssh2": "^1.17.0"
31
+ },
32
+ "devDependencies": {
33
+ "jest": "^29.7.0",
34
+ "nock": "^13.5.6"
35
+ },
36
+ "jest": {
37
+ "collectCoverage": true,
38
+ "coverageThreshold": {
39
+ "global": {
40
+ "branches": 15,
41
+ "functions": 25,
42
+ "lines": 25,
43
+ "statements": 25
44
+ }
45
+ },
46
+ "coverageReporters": ["text", "lcov", "clover"],
47
+ "testMatch": ["**/__tests__/**/*.test.js"]
48
+ }
49
+ }
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Script to add @depends annotations to all kernel source files.
3
+ * Run: node scripts/add-deps.js
4
+ */
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const { getKernelSrc } = require("../lib/common");
9
+ const KERNEL_SRC = getKernelSrc();
10
+ const KERNEL_ORDER = (() => { try { return require("../kernel-order.json"); } catch(e) { return []; } })();
11
+
12
+ const KNOWN_DEPS = {
13
+ "compat-cl.lsp": ["compat-cl"],
14
+ "platform-api.lsp": ["platform-api"],
15
+ "lib/string-utils.lsp": ["string-utils"],
16
+ "lib/version.lsp": ["string-utils", "version"],
17
+ "lib/file-utils.lsp": ["string-utils", "file-utils"],
18
+ "lib/dcl.lsp": ["string-utils", "file-utils", "dcl"],
19
+ "event-bus.lsp": ["event-bus"],
20
+ "base-function.lsp": ["event-bus", "platform-api", "base-function"],
21
+ "platform.lsp": ["platform-api", "base-function"],
22
+ "version.lsp": ["version"],
23
+ "bootstrap.lsp": ["platform", "version", "base-function"],
24
+ "error-handle.lsp": ["base-function", "platform", "error-handle"],
25
+ "log.lsp": ["error-handle", "log"],
26
+ "i18n.lsp": ["log", "i18n"],
27
+ "net.lsp": ["log", "event-bus", "base-function", "net"],
28
+ "functionlib.lsp": ["net", "functionlib"],
29
+ "@lisp-env.lsp": ["functionlib"],
30
+ "setup.lsp": ["@lisp-env", "net"],
31
+ "sub-system/help-system.lsp": ["base-function", "i18n"],
32
+ "sub-system/config-manager.lsp": ["base-function", "i18n"],
33
+ "sub-system/ui.lsp": ["base-function", "config-manager"],
34
+ "sub-system/voice.lsp": ["base-function"],
35
+ "sub-system/package-manager.lsp": ["base-function", "net", "config-manager", "i18n"],
36
+ "sub-system/user-manager.lsp": ["base-function", "net", "config-manager"],
37
+ "sub-system/update.lsp": ["base-function", "net", "config-manager"],
38
+ "sub-system/condition.lsp": ["base-function", "net", "i18n"],
39
+ "sub-system/cache.lsp": ["base-function", "file-utils"],
40
+ "sub-system/scheduler.lsp": ["base-function", "event-bus"],
41
+ "sub-system/config-sync.lsp": ["base-function", "net", "config-manager"],
42
+ "sub-system/config/config.lsp": ["sub-system/config-manager", "i18n"],
43
+ "sub-system/command.lsp": ["config", "base-function"],
44
+ "init.lsp": ["command", "setup", "bootstrap"],
45
+ };
46
+
47
+ const DEPENDS_RE = /;;\s*@depends\s+/;
48
+ const PROVIDES_RE = /;;\s*@provides\s+/;
49
+
50
+ let changed = 0;
51
+
52
+ for (const f of KERNEL_ORDER) {
53
+ const fullPath = path.join(KERNEL_SRC, f);
54
+ if (!fs.existsSync(fullPath)) {
55
+ console.error(`Missing: ${f}`);
56
+ continue;
57
+ }
58
+ const content = fs.readFileSync(fullPath, "utf-8");
59
+ const firstLines = content.split("\n");
60
+ const hasDepends = firstLines.some(l => DEPENDS_RE.test(l));
61
+ const hasProvides = firstLines.some(l => PROVIDES_RE.test(l));
62
+
63
+ if (hasDepends) {
64
+ console.log(` ✓ ${f} (already has @depends)`);
65
+ continue;
66
+ }
67
+
68
+ const deps = KNOWN_DEPS[f];
69
+ if (!deps || deps.length === 0) {
70
+ console.log(` - ${f} (no deps)`);
71
+ continue;
72
+ }
73
+
74
+ const annotation = `;; @depends ${deps.join(" ")}`;
75
+ const newContent = annotation + "\n" + content;
76
+ fs.writeFileSync(fullPath, newContent, "utf-8");
77
+ console.log(` + ${f} → added: ${annotation}`);
78
+ changed++;
79
+ }
80
+
81
+ console.log(`\nUpdated ${changed} files with @depends annotations.`);
@@ -0,0 +1,98 @@
1
+ const fs = require("fs");
2
+ const path = require("path");
3
+
4
+ const { getKernelSrc } = require("../lib/common");
5
+ const KERNEL_SRC = getKernelSrc();
6
+ const KERNEL_ORDER = (() => { try { return require("../kernel-order.json"); } catch(e) { return []; } })();
7
+
8
+ const CORRECT_DEPS = {
9
+ // lib/* files are development sources extracted from base-function.lsp
10
+ // They @provide their own feature, only depending on core CL/compat
11
+ "lib/string-utils.lsp": { provides: ["string-utils"], deps: [] },
12
+ "lib/version.lsp": { provides: ["version-utils"], deps: ["string-utils"] },
13
+ "lib/file-utils.lsp": { provides: ["file-utils"], deps: ["string-utils"] },
14
+ "lib/dcl.lsp": { provides: ["dcl"], deps: ["string-utils", "file-utils"] },
15
+
16
+ // Core files
17
+ "compat-cl.lsp": { provides: ["compat-cl"], deps: [] },
18
+ "platform-api.lsp": { provides: ["platform-api"], deps: [] },
19
+ "event-bus.lsp": { provides: ["event-bus"], deps: [] },
20
+ "base-function.lsp": { provides: ["base-function"], deps: ["event-bus", "platform-api"] },
21
+ "platform.lsp": { provides: ["platform"], deps: ["platform-api", "base-function"] },
22
+ "version.lsp": { provides: ["version"], deps: [] },
23
+ "bootstrap.lsp": { provides: ["bootstrap"], deps: ["platform", "version", "base-function"] },
24
+ "error-handle.lsp": { provides: ["error-handle"], deps: ["base-function", "platform"] },
25
+ "log.lsp": { provides: ["log"], deps: ["error-handle"] },
26
+ "i18n.lsp": { provides: ["i18n"], deps: ["log"] },
27
+ "net.lsp": { provides: ["net"], deps: ["log", "event-bus", "base-function"] },
28
+ "functionlib.lsp": { provides: ["functionlib"], deps: ["net"] },
29
+ "@lisp-env.lsp": { provides: ["@lisp-env"], deps: ["functionlib"] },
30
+ "setup.lsp": { provides: ["setup"], deps: ["@lisp-env", "net"] },
31
+
32
+ // Sub-systems
33
+ "sub-system/help-system.lsp": { provides: ["help-system"], deps: ["base-function", "i18n"] },
34
+ "sub-system/config-manager.lsp": { provides: ["config-manager"], deps: ["base-function", "i18n"] },
35
+ "sub-system/ui.lsp": { provides: ["ui"], deps: ["base-function", "config-manager"] },
36
+ "sub-system/voice.lsp": { provides: ["voice"], deps: ["base-function"] },
37
+ "sub-system/package-manager.lsp": { provides: ["package-manager"], deps: ["base-function", "net", "config-manager", "i18n"] },
38
+ "sub-system/user-manager.lsp": { provides: ["user-manager"], deps: ["base-function", "net", "config-manager"] },
39
+ "sub-system/update.lsp": { provides: ["update"], deps: ["base-function", "net", "config-manager"] },
40
+ "sub-system/condition.lsp": { provides: ["condition"], deps: ["base-function", "net", "i18n"] },
41
+ "sub-system/cache.lsp": { provides: ["cache"], deps: ["base-function", "file-utils"] },
42
+ "sub-system/scheduler.lsp": { provides: ["scheduler"], deps: ["base-function", "event-bus"] },
43
+ "sub-system/config-sync.lsp": { provides: ["config-sync"], deps: ["base-function", "net", "config-manager"] },
44
+ "sub-system/audit.lsp": { provides: ["audit"], deps: ["config-manager", "net", "file-utils", "base-function"] },
45
+ "sub-system/permission.lsp": { provides: ["permission"], deps: ["config-manager", "net", "base-function"] },
46
+
47
+ // Final files
48
+ "sub-system/config/config.lsp": { provides: ["config"], deps: ["config-manager", "i18n"] },
49
+ "sub-system/command.lsp": { provides: ["command"], deps: ["config", "base-function"] },
50
+ "init.lsp": { provides: ["init"], deps: ["command", "setup", "bootstrap"] },
51
+ };
52
+
53
+ const DEPENDS_RE = /;;\s*@depends\s+/;
54
+ const PROVIDES_RE = /;;\s*@provides\s+/;
55
+
56
+ function stripAnnotation(content) {
57
+ const lines = content.split("\n");
58
+ const filtered = lines.filter(l => !DEPENDS_RE.test(l) && !PROVIDES_RE.test(l));
59
+ return filtered.join("\n");
60
+ }
61
+
62
+ let changed = 0;
63
+
64
+ for (const f of KERNEL_ORDER) {
65
+ const fullPath = path.join(KERNEL_SRC, f);
66
+ if (!fs.existsSync(fullPath)) {
67
+ console.error(`Missing: ${f}`);
68
+ continue;
69
+ }
70
+
71
+ const info = CORRECT_DEPS[f];
72
+ if (!info) {
73
+ console.log(` - ${f} (no dep info)`);
74
+ continue;
75
+ }
76
+
77
+ const content = fs.readFileSync(fullPath, "utf-8");
78
+ const clean = stripAnnotation(content);
79
+
80
+ const parts = [];
81
+ if (info.deps.length > 0) {
82
+ parts.push(`;; @depends ${info.deps.join(" ")}`);
83
+ }
84
+ if (info.provides.length > 0) {
85
+ parts.push(`;; @provides ${info.provides.join(" ")}`);
86
+ }
87
+
88
+ if (parts.length > 0) {
89
+ const newContent = parts.join("\n") + "\n" + clean;
90
+ if (newContent !== content) {
91
+ fs.writeFileSync(fullPath, newContent, "utf-8");
92
+ console.log(` + ${f} → ${parts.join("; ")}`);
93
+ changed++;
94
+ }
95
+ }
96
+ }
97
+
98
+ console.log(`\nUpdated ${changed} files.`);