@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,18 @@
|
|
|
1
|
+
name: Publish to npm
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
|
|
6
|
+
jobs:
|
|
7
|
+
publish:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
- uses: actions/setup-node@v4
|
|
12
|
+
with:
|
|
13
|
+
node-version: 18
|
|
14
|
+
registry-url: "https://registry.npmjs.org"
|
|
15
|
+
- run: npm ci
|
|
16
|
+
- run: npm publish --access public
|
|
17
|
+
env:
|
|
18
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/README.md
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# @atlisp/cli — @lisp 命令行工具
|
|
2
|
+
|
|
3
|
+
在 AutoCAD、ZWCAD、GStarCAD、BricsCAD 中管理 AutoLISP 包、构建内核、运行测试的命令行工具。
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npm install -g @atlisp/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 用法
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
atlisp <命令> [参数]
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
### 包管理
|
|
18
|
+
|
|
19
|
+
| 命令 | 说明 |
|
|
20
|
+
|------|------|
|
|
21
|
+
| `atlisp install` | 安装 @lisp Core 到 CAD |
|
|
22
|
+
| `atlisp pull <包名>` | 安装一个包 |
|
|
23
|
+
| `atlisp remove <包名>` | 卸载一个包 |
|
|
24
|
+
| `atlisp list` | 列出本地包 |
|
|
25
|
+
| `atlisp search <关键词>` | 搜索远程包 |
|
|
26
|
+
|
|
27
|
+
### 内核开发
|
|
28
|
+
|
|
29
|
+
| 命令 | 说明 |
|
|
30
|
+
|------|------|
|
|
31
|
+
| `atlisp build` | 构建 `@lisp_u.lsp`(需要 `ATLISP_KERNEL_SRC`) |
|
|
32
|
+
| `atlisp build-module <名>` | 构建单个模块 |
|
|
33
|
+
| `atlisp watch` | 监听源文件变更自动构建 |
|
|
34
|
+
| `atlisp lint` | AutoLISP 代码检查 |
|
|
35
|
+
| `atlisp test` | 在 SBCL 中运行语法测试 |
|
|
36
|
+
| `atlisp test --platform cad` | 在 CAD 中运行测试(需 CAD + MCP) |
|
|
37
|
+
| `atlisp init --template module <名>` | 创建新模块脚手架 |
|
|
38
|
+
|
|
39
|
+
### 配置与诊断
|
|
40
|
+
|
|
41
|
+
| 命令 | 说明 |
|
|
42
|
+
|------|------|
|
|
43
|
+
| `atlisp config list` | 查看配置 |
|
|
44
|
+
| `atlisp config set <key> <val>` | 设置配置 |
|
|
45
|
+
| `atlisp login` | 登录 atlisp.cn |
|
|
46
|
+
| `atlisp doctor` | 环境诊断 |
|
|
47
|
+
| `atlisp check` | CAD 连接检查 |
|
|
48
|
+
|
|
49
|
+
### 工具
|
|
50
|
+
|
|
51
|
+
| 命令 | 说明 |
|
|
52
|
+
|------|------|
|
|
53
|
+
| `atlisp batch --path <dir> --expr <lisp>` | 批量 DWG 处理 |
|
|
54
|
+
| `atlisp find --name <pattern>` | 在 DWG 中搜索元素 |
|
|
55
|
+
| `atlisp qrcode <text>` | 生成 QR 码 |
|
|
56
|
+
| `atlisp dcl-migrate <dcl文件>` | 转换 DCL 到 DSL 格式 |
|
|
57
|
+
|
|
58
|
+
## 环境变量
|
|
59
|
+
|
|
60
|
+
| 变量 | 说明 | 默认值 |
|
|
61
|
+
|------|------|--------|
|
|
62
|
+
| `ATLISP_KERNEL_SRC` | 内核源码目录 | `~/atlisp/kernel/src` |
|
|
63
|
+
| `ATLISP_KERNEL_TEST` | 内核测试目录 | `~/atlisp/kernel/test` |
|
|
64
|
+
| `ATLISP_KERNEL_ROOT` | 内核根目录 | `~/atlisp/kernel` |
|
|
65
|
+
|
|
66
|
+
## 开发
|
|
67
|
+
|
|
68
|
+
```sh
|
|
69
|
+
git clone https://gitee.com/atlisp/atlisp-cli.git
|
|
70
|
+
cd atlisp-cli
|
|
71
|
+
npm install
|
|
72
|
+
npm test # 运行 Jest 测试
|
|
73
|
+
node atlisp.js # 交互模式
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## 从 kernel 仓库迁移
|
|
77
|
+
|
|
78
|
+
`@atlisp/cli` 原来位于 `atlisp/kernel/tools/cli/atlisp/`。现已独立为单独项目。
|
|
79
|
+
|
|
80
|
+
迁移后,kernel 仓库不再自带 CLI 工具,但可通过以下方式使用:
|
|
81
|
+
|
|
82
|
+
1. **全局安装**:`npm install -g @atlisp/cli`
|
|
83
|
+
2. **npx 直接使用**:`npx @atlisp/cli build`
|
|
84
|
+
3. **原位使用**:kernel 仓库的 Makefile 会自动探测可用方式
|
|
85
|
+
|
|
86
|
+
## 许可证
|
|
87
|
+
|
|
88
|
+
ISC
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const fs = require("fs");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { execSync } = require("child_process");
|
|
4
|
+
|
|
5
|
+
const CLI = path.join(__dirname, "..", "atlisp.js");
|
|
6
|
+
const { getKernelSrc } = require("../lib/common");
|
|
7
|
+
const KERNEL_ORDER = (() => { try { return require("../kernel-order.json"); } catch(e) { return []; } })();
|
|
8
|
+
|
|
9
|
+
describe("Build system", () => {
|
|
10
|
+
test("KERNEL_ORDER contains scheduler", () => {
|
|
11
|
+
expect(KERNEL_ORDER).toContain("sub-system/scheduler.lsp");
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
test("all KERNEL_ORDER files exist", () => {
|
|
15
|
+
const srcDir = getKernelSrc();
|
|
16
|
+
for (const f of KERNEL_ORDER) {
|
|
17
|
+
const fullPath = path.join(srcDir, f);
|
|
18
|
+
expect(fs.existsSync(fullPath)).toBe(true);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("KERNEL_ORDER has no duplicates", () => {
|
|
23
|
+
const seen = new Set();
|
|
24
|
+
for (const f of KERNEL_ORDER) {
|
|
25
|
+
expect(seen.has(f)).toBe(false);
|
|
26
|
+
seen.add(f);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("modules with subdirs have matching subfiles", () => {
|
|
31
|
+
const modulesDir = path.join(path.dirname(getKernelSrc()), "modules");
|
|
32
|
+
const entries = fs.readdirSync(modulesDir, { withFileTypes: true });
|
|
33
|
+
const dirs = entries.filter(e => e.isDirectory());
|
|
34
|
+
expect(dirs.length).toBeGreaterThan(0);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe("CLI help", () => {
|
|
39
|
+
test("--help exits with code 0", () => {
|
|
40
|
+
const result = execSync(`node ${CLI} --help`, { encoding: "utf-8" });
|
|
41
|
+
expect(result).toContain("@atlisp/cli v");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("--version outputs version", () => {
|
|
45
|
+
const result = execSync(`node ${CLI} --version`, { encoding: "utf-8" });
|
|
46
|
+
expect(result).toMatch(/^\d+\.\d+\.\d+/);
|
|
47
|
+
});
|
|
48
|
+
});
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
const nock = require("nock");
|
|
2
|
+
const {
|
|
3
|
+
pullLispCode, removeLispCode, installCode, parsePackages, computeChecksum,
|
|
4
|
+
resolveAppId, isWindows, CAD_TABLES, normalizeTable,
|
|
5
|
+
httpGet, API_BASE,
|
|
6
|
+
} = require("../lib/atlisp");
|
|
7
|
+
|
|
8
|
+
describe("CLI Commands", () => {
|
|
9
|
+
const validCommands = ["install", "pull", "remove", "list", "search", "batch", "find", "check", "test", "build", "build-module", "checksum"];
|
|
10
|
+
|
|
11
|
+
test("should recognize all commands", () => {
|
|
12
|
+
expect(validCommands).toContain("install");
|
|
13
|
+
expect(validCommands).toContain("pull");
|
|
14
|
+
expect(validCommands).toContain("remove");
|
|
15
|
+
expect(validCommands).toContain("list");
|
|
16
|
+
expect(validCommands).toContain("search");
|
|
17
|
+
expect(validCommands).toContain("batch");
|
|
18
|
+
expect(validCommands).toContain("find");
|
|
19
|
+
expect(validCommands).toContain("check");
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("should recognize short aliases", () => {
|
|
23
|
+
const aliases = { i: "install", p: "pull", rm: "remove", l: "list", s: "search", b: "build", t: "test", cs: "checksum" };
|
|
24
|
+
for (const [alias, cmd] of Object.entries(aliases)) {
|
|
25
|
+
expect(validCommands).toContain(cmd);
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("should reject unknown commands", () => {
|
|
30
|
+
const unknownCmds = ["upgrade", "help", "xyz"];
|
|
31
|
+
unknownCmds.forEach(cmd => {
|
|
32
|
+
expect(validCommands.includes(cmd)).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe("Install Code", () => {
|
|
38
|
+
test("should generate valid install code", () => {
|
|
39
|
+
expect(installCode).toContain("vl-load-com");
|
|
40
|
+
expect(installCode).toContain("atlisp");
|
|
41
|
+
expect(installCode).toContain("/@");
|
|
42
|
+
expect(installCode.length).toBeGreaterThan(100);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("should generate correct pull code for package", () => {
|
|
46
|
+
const code = pullLispCode("base");
|
|
47
|
+
expect(code).toContain("vl-load-com");
|
|
48
|
+
expect(code).toContain("/base");
|
|
49
|
+
expect(code).not.toContain("/@");
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("pull code should wrap in outer parens", () => {
|
|
53
|
+
const code = pullLispCode("at-pm");
|
|
54
|
+
expect(code.startsWith("(")).toBe(true);
|
|
55
|
+
expect(code.endsWith("))")).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("should handle package name with hyphens", () => {
|
|
59
|
+
const code = pullLispCode("my-package");
|
|
60
|
+
expect(code).toContain("/my-package");
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("Remove Code", () => {
|
|
65
|
+
test("should generate remove code", () => {
|
|
66
|
+
const code = removeLispCode("base");
|
|
67
|
+
expect(code).toContain("base");
|
|
68
|
+
expect(code).toContain("package-remove");
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
test("should handle package name with hyphens", () => {
|
|
72
|
+
const code = removeLispCode("my-package");
|
|
73
|
+
expect(code).toContain("my-package");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("Package Parsing", () => {
|
|
78
|
+
test("should parse S-expression package list", () => {
|
|
79
|
+
const text = `((:name "base" :full-name "基础工具" :author "VitalGG" :version "1.2" :description "基础工具集" :category "工具" :required nil :url nil :files ("base-whole") :opensource 1 :free T)
|
|
80
|
+
(:name "at-pm" :full-name "图号管理" :author "VitalGG" :description "图号管理工具" :category "管理" :required nil :files ("at-pm/entry") :opensource 1 :free T))`;
|
|
81
|
+
const pkgs = parsePackages(text);
|
|
82
|
+
expect(pkgs).toHaveLength(2);
|
|
83
|
+
expect(pkgs[0].name).toBe("base");
|
|
84
|
+
expect(pkgs[0].fullName).toBe("基础工具");
|
|
85
|
+
expect(pkgs[0].category).toBe("工具");
|
|
86
|
+
expect(pkgs[1].name).toBe("at-pm");
|
|
87
|
+
expect(pkgs[1].fullName).toBe("图号管理");
|
|
88
|
+
expect(pkgs[1].category).toBe("管理");
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("should handle empty input", () => {
|
|
92
|
+
expect(parsePackages("")).toEqual([]);
|
|
93
|
+
expect(parsePackages(" ")).toEqual([]);
|
|
94
|
+
expect(parsePackages("()")).toEqual([]);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("should handle packages with missing fields", () => {
|
|
98
|
+
const text = `((:name "simple" :version "1.0"))`;
|
|
99
|
+
const pkgs = parsePackages(text);
|
|
100
|
+
expect(pkgs).toHaveLength(1);
|
|
101
|
+
expect(pkgs[0].name).toBe("simple");
|
|
102
|
+
expect(pkgs[0].fullName).toBe("simple");
|
|
103
|
+
expect(pkgs[0].description).toBe("");
|
|
104
|
+
expect(pkgs[0].category).toBe("");
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test("should handle category-only packages", () => {
|
|
108
|
+
const text = `((:name "arch" :category "建筑" :full-name "建筑工具"))`;
|
|
109
|
+
const pkgs = parsePackages(text);
|
|
110
|
+
expect(pkgs[0].category).toBe("建筑");
|
|
111
|
+
expect(pkgs[0].fullName).toBe("建筑工具");
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("Checksum", () => {
|
|
116
|
+
test("should compute consistent checksum", () => {
|
|
117
|
+
const h1 = computeChecksum("hello");
|
|
118
|
+
const h2 = computeChecksum("hello");
|
|
119
|
+
expect(h1).toBe(h2);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test("should produce different checksums for different content", () => {
|
|
123
|
+
const h1 = computeChecksum("hello");
|
|
124
|
+
const h2 = computeChecksum("world");
|
|
125
|
+
expect(h1).not.toBe(h2);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("checksum should be numeric string", () => {
|
|
129
|
+
const h = computeChecksum("test content here");
|
|
130
|
+
expect(h).toMatch(/^-?\d+$/);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
describe("CAD Platform Resolution", () => {
|
|
135
|
+
test("should default to AutoCAD", () => {
|
|
136
|
+
expect(resolveAppId()).toBe("AutoCAD.Application");
|
|
137
|
+
expect(resolveAppId("")).toBe("AutoCAD.Application");
|
|
138
|
+
expect(resolveAppId("auto")).toBe("AutoCAD.Application");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("should resolve platform names", () => {
|
|
142
|
+
expect(resolveAppId("autocad")).toBe("AutoCAD.Application");
|
|
143
|
+
expect(resolveAppId("zwcad")).toBe("ZWCAD.Application");
|
|
144
|
+
expect(resolveAppId("gstarcad")).toBe("GStarCAD.Application");
|
|
145
|
+
expect(resolveAppId("bricscad")).toBe("BricscadApp.AcadApplication");
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("should pass through custom app IDs", () => {
|
|
149
|
+
expect(resolveAppId("AutoCAD.Application.24")).toBe("AutoCAD.Application.24");
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe("Platform Detection", () => {
|
|
154
|
+
test("isWindows should return boolean", () => {
|
|
155
|
+
expect(typeof isWindows()).toBe("boolean");
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("CAD Tables", () => {
|
|
160
|
+
test("should have all expected table names", () => {
|
|
161
|
+
expect(CAD_TABLES).toContain("Blocks");
|
|
162
|
+
expect(CAD_TABLES).toContain("Layers");
|
|
163
|
+
expect(CAD_TABLES).toContain("TextStyles");
|
|
164
|
+
expect(CAD_TABLES).toContain("Linetypes");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("normalizeTable should default to Blocks", () => {
|
|
168
|
+
expect(normalizeTable()).toBe("Blocks");
|
|
169
|
+
expect(normalizeTable("")).toBe("Blocks");
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("normalizeTable should normalize names", () => {
|
|
173
|
+
expect(normalizeTable("block")).toBe("Blocks");
|
|
174
|
+
expect(normalizeTable("Blocks")).toBe("Blocks");
|
|
175
|
+
expect(normalizeTable("layer")).toBe("Layers");
|
|
176
|
+
expect(normalizeTable("textstyle")).toBe("TextStyles");
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test("normalizeTable should reject unknown tables", () => {
|
|
180
|
+
expect(normalizeTable("xyz")).toBeNull();
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
describe("HTTP API", () => {
|
|
185
|
+
afterEach(() => nock.cleanAll());
|
|
186
|
+
|
|
187
|
+
test("httpGet should fetch package list", async () => {
|
|
188
|
+
const mockData = `((:name "base" :full-name "基础工具" :category "工具"))`;
|
|
189
|
+
nock(API_BASE)
|
|
190
|
+
.get("/packages-list?edition=stable")
|
|
191
|
+
.reply(200, mockData);
|
|
192
|
+
|
|
193
|
+
const text = await httpGet("/packages-list?edition=stable");
|
|
194
|
+
expect(text).toBe(mockData);
|
|
195
|
+
const pkgs = parsePackages(text);
|
|
196
|
+
expect(pkgs).toHaveLength(1);
|
|
197
|
+
expect(pkgs[0].name).toBe("base");
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test("httpGet should handle network errors", async () => {
|
|
201
|
+
nock(API_BASE)
|
|
202
|
+
.get("/packages-list?edition=stable")
|
|
203
|
+
.replyWithError("Network error");
|
|
204
|
+
|
|
205
|
+
await expect(httpGet("/packages-list?edition=stable")).rejects.toThrow();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("httpGet should handle 404", async () => {
|
|
209
|
+
nock(API_BASE)
|
|
210
|
+
.get("/nonexistent")
|
|
211
|
+
.reply(404, "Not found");
|
|
212
|
+
|
|
213
|
+
const text = await httpGet("/nonexistent");
|
|
214
|
+
expect(text).toBe("Not found");
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
test("httpGet should fetch large package list", async () => {
|
|
218
|
+
const pkgs = Array.from({ length: 20 }, (_, i) =>
|
|
219
|
+
`(:name "pkg${i}" :full-name "Package ${i}" :category "test")`
|
|
220
|
+
).join("\n ");
|
|
221
|
+
const mockData = `(${pkgs})`;
|
|
222
|
+
nock(API_BASE)
|
|
223
|
+
.get("/packages-list?edition=stable")
|
|
224
|
+
.reply(200, mockData);
|
|
225
|
+
|
|
226
|
+
const text = await httpGet("/packages-list?edition=stable");
|
|
227
|
+
const result = parsePackages(text);
|
|
228
|
+
expect(result).toHaveLength(20);
|
|
229
|
+
expect(result[0].name).toBe("pkg0");
|
|
230
|
+
expect(result[19].name).toBe("pkg19");
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe("API_BASE configuration", () => {
|
|
235
|
+
test("API_BASE should point to atlisp.cn", () => {
|
|
236
|
+
expect(API_BASE).toMatch(/atlisp\.cn/);
|
|
237
|
+
});
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
describe("Snapshot: package list output", () => {
|
|
241
|
+
test("parsePackages output snapshot", () => {
|
|
242
|
+
const text = `((:name "base" :full-name "基础工具" :category "工具" :description "基础工具集"))`;
|
|
243
|
+
const pkgs = parsePackages(text);
|
|
244
|
+
expect(pkgs).toMatchSnapshot();
|
|
245
|
+
});
|
|
246
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
const os = require("os");
|
|
4
|
+
|
|
5
|
+
// Mock homedir before requiring config
|
|
6
|
+
const TEST_HOME = path.join(os.tmpdir(), "atlisp-test-config-" + Date.now());
|
|
7
|
+
const realHomedir = os.homedir;
|
|
8
|
+
os.homedir = () => TEST_HOME;
|
|
9
|
+
|
|
10
|
+
const config = require("../commands/config");
|
|
11
|
+
|
|
12
|
+
afterAll(() => {
|
|
13
|
+
os.homedir = realHomedir;
|
|
14
|
+
fs.rmSync(TEST_HOME, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
describe("Config system", () => {
|
|
18
|
+
test("readConfigCfg returns empty when no file", () => {
|
|
19
|
+
const cfg = config.readConfigCfg();
|
|
20
|
+
expect(cfg).toEqual({});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("readAtlispConfig returns empty when no file", () => {
|
|
24
|
+
const cfg = config.readAtlispConfig();
|
|
25
|
+
expect(cfg).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("getAllConfig returns merged config", () => {
|
|
29
|
+
const all = config.getAllConfig();
|
|
30
|
+
expect(all).toBeDefined();
|
|
31
|
+
expect(typeof all).toBe("object");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
test("saveAuthToBoth writes to both config stores", () => {
|
|
35
|
+
config.saveAuthToBoth("test@example.com", "tok_12345");
|
|
36
|
+
|
|
37
|
+
const cfgCfg = config.readConfigCfg();
|
|
38
|
+
expect(cfgCfg["user-email"]).toBe("test@example.com");
|
|
39
|
+
expect(cfgCfg["user-token"]).toBe("tok_12345");
|
|
40
|
+
|
|
41
|
+
const jsonCfg = config.readAtlispConfig();
|
|
42
|
+
expect(jsonCfg["user-email"]).toBe("test@example.com");
|
|
43
|
+
expect(jsonCfg["user-token"]).toBe("tok_12345");
|
|
44
|
+
|
|
45
|
+
// token should be obfuscated in raw config.cfg
|
|
46
|
+
const rawCfg = fs.readFileSync(
|
|
47
|
+
path.join(TEST_HOME, ".atlisp", "config.cfg"),
|
|
48
|
+
"utf-8"
|
|
49
|
+
);
|
|
50
|
+
expect(rawCfg).toContain("__enc__:");
|
|
51
|
+
expect(rawCfg).not.toContain("tok_12345");
|
|
52
|
+
|
|
53
|
+
// token should be obfuscated in raw atlisp.json
|
|
54
|
+
const rawJson = fs.readFileSync(
|
|
55
|
+
path.join(TEST_HOME, ".atlisp", "atlisp.json"),
|
|
56
|
+
"utf-8"
|
|
57
|
+
);
|
|
58
|
+
expect(rawJson).toContain("__enc__:");
|
|
59
|
+
expect(rawJson).not.toContain("tok_12345");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("getAuthToken deobfuscates token", () => {
|
|
63
|
+
const token = config.getAuthToken();
|
|
64
|
+
expect(token).toBe("tok_12345");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("getAuthEmail returns email", () => {
|
|
68
|
+
const email = config.getAuthEmail();
|
|
69
|
+
expect(email).toBe("test@example.com");
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const deps = require("../lib/deps");
|
|
3
|
+
|
|
4
|
+
describe("Dependency analysis", () => {
|
|
5
|
+
test("buildDependencyMap returns map for all kernel files", () => {
|
|
6
|
+
const { map, fileProvides } = deps.buildDependencyMap();
|
|
7
|
+
expect(Object.keys(map).length).toBe(deps.KERNEL_ORDER.length);
|
|
8
|
+
expect(Object.keys(fileProvides).length).toBeGreaterThan(0);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
test("topologicalSort returns all files", () => {
|
|
12
|
+
const sorted = deps.topologicalSort();
|
|
13
|
+
expect(sorted.length).toBe(deps.KERNEL_ORDER.length);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("topologicalSort places roots first", () => {
|
|
17
|
+
const sorted = deps.topologicalSort();
|
|
18
|
+
// platform-api has no deps, should be early
|
|
19
|
+
const idx = sorted.indexOf("platform-api.lsp");
|
|
20
|
+
expect(idx).toBeGreaterThanOrEqual(0);
|
|
21
|
+
expect(idx).toBeLessThan(5);
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test("validateOrder passes", () => {
|
|
25
|
+
const result = deps.validateOrder();
|
|
26
|
+
expect(result.valid).toBe(true);
|
|
27
|
+
expect(result.issues.length).toBe(0);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("no circular dependencies", () => {
|
|
31
|
+
// topologicalSort prints error for cycles, but doesn't throw
|
|
32
|
+
// We verify by checking sorted order is complete and valid
|
|
33
|
+
const sorted = deps.topologicalSort();
|
|
34
|
+
const { map, fileProvides } = deps.buildDependencyMap();
|
|
35
|
+
|
|
36
|
+
// Verify all deps of each file appear before it in sorted order
|
|
37
|
+
const position = {};
|
|
38
|
+
sorted.forEach((f, i) => { position[f] = i; });
|
|
39
|
+
|
|
40
|
+
for (const [f, info] of Object.entries(map)) {
|
|
41
|
+
for (const dep of info.deps) {
|
|
42
|
+
const depFile = fileProvides[dep];
|
|
43
|
+
if (depFile && depFile !== f) {
|
|
44
|
+
expect(position[f]).toBeGreaterThan(position[depFile]);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
test("parseDepends handles files without annotations", () => {
|
|
51
|
+
const tmpFile = path.join(__dirname, "_tmp_test_no_annot.lsp");
|
|
52
|
+
require("fs").writeFileSync(tmpFile, "(princ \"hello\")\n");
|
|
53
|
+
const result = deps.parseDepends(tmpFile);
|
|
54
|
+
expect(result.deps).toEqual([]);
|
|
55
|
+
expect(result.provides).toEqual([]);
|
|
56
|
+
require("fs").unlinkSync(tmpFile);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
test("validateOrder detects missing files", () => {
|
|
60
|
+
// Should not crash for missing files
|
|
61
|
+
const result = deps.validateOrder();
|
|
62
|
+
expect(result).toHaveProperty("valid");
|
|
63
|
+
});
|
|
64
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
const lib = require("../index");
|
|
2
|
+
|
|
3
|
+
describe("atlisp library", () => {
|
|
4
|
+
test("returns expected string", () => {
|
|
5
|
+
expect(lib.atlisp()).toBe("atlisp - CAD package manager!");
|
|
6
|
+
});
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
describe("Library exports", () => {
|
|
10
|
+
test("should export all common functions", () => {
|
|
11
|
+
expect(typeof lib.httpGet).toBe("function");
|
|
12
|
+
expect(typeof lib.parsePackages).toBe("function");
|
|
13
|
+
expect(typeof lib.readLocalPackages).toBe("function");
|
|
14
|
+
expect(typeof lib.computeChecksum).toBe("function");
|
|
15
|
+
expect(typeof lib.pullLispCode).toBe("function");
|
|
16
|
+
expect(typeof lib.removeLispCode).toBe("function");
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test("should export CAD functions", () => {
|
|
20
|
+
expect(typeof lib.checkCAD).toBe("function");
|
|
21
|
+
expect(typeof lib.installAtLisp).toBe("function");
|
|
22
|
+
expect(typeof lib.pull).toBe("function");
|
|
23
|
+
expect(typeof lib.remove).toBe("function");
|
|
24
|
+
expect(typeof lib.resolveAppId).toBe("function");
|
|
25
|
+
expect(typeof lib.isWindows).toBe("function");
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test("should export batch and find", () => {
|
|
29
|
+
expect(typeof lib.batch).toBe("function");
|
|
30
|
+
expect(typeof lib.find).toBe("function");
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test("should export CAD_TABLES and installCode", () => {
|
|
34
|
+
expect(Array.isArray(lib.CAD_TABLES)).toBe(true);
|
|
35
|
+
expect(typeof lib.installCode).toBe("string");
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test("should export constants", () => {
|
|
39
|
+
expect(typeof lib.API_BASE).toBe("string");
|
|
40
|
+
expect(typeof lib.ATLISP_DIR).toBe("string");
|
|
41
|
+
expect(typeof lib.PKGS_DIR).toBe("string");
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe("CLI Commands", () => {
|
|
46
|
+
const validCommands = ["install", "pull", "remove", "list", "search", "batch", "find", "check", "test", "build"];
|
|
47
|
+
|
|
48
|
+
test("should recognize new commands", () => {
|
|
49
|
+
expect(validCommands).toContain("remove");
|
|
50
|
+
expect(validCommands).toContain("batch");
|
|
51
|
+
expect(validCommands).toContain("find");
|
|
52
|
+
expect(validCommands).toContain("check");
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe("Package Validation", () => {
|
|
57
|
+
test("should validate package name format", () => {
|
|
58
|
+
const validNames = ["at-pm", "base", "network", "userman"];
|
|
59
|
+
validNames.forEach(name => {
|
|
60
|
+
expect(name).toMatch(/^[a-z-]+$/);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const path = require("path");
|
|
2
|
+
const fs = require("fs");
|
|
3
|
+
|
|
4
|
+
describe("Lint system", () => {
|
|
5
|
+
test("@atlisp/lint module loads", () => {
|
|
6
|
+
const lintMod = require("@atlisp/lint/dist/index");
|
|
7
|
+
expect(lintMod).toHaveProperty("main");
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
test("lintFiles API works on small files", () => {
|
|
11
|
+
const { lintFiles } = require("@atlisp/lint/dist/runner");
|
|
12
|
+
const { loadConfig } = require("@atlisp/lint/dist/config");
|
|
13
|
+
const config = loadConfig();
|
|
14
|
+
|
|
15
|
+
// Create a temp file with known issues
|
|
16
|
+
const tmpDir = fs.mkdtempSync(path.join(require("os").tmpdir(), "lint-test-"));
|
|
17
|
+
const tmpFile = path.join(tmpDir, "test.lsp");
|
|
18
|
+
fs.writeFileSync(tmpFile, `(defun foo (x)
|
|
19
|
+
"test fn"
|
|
20
|
+
(print x)
|
|
21
|
+
nil)
|
|
22
|
+
`);
|
|
23
|
+
const issues = lintFiles([tmpFile], config, tmpDir);
|
|
24
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
25
|
+
|
|
26
|
+
// Cleanup
|
|
27
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("lintFiles handles empty file", () => {
|
|
31
|
+
const { lintFiles } = require("@atlisp/lint/dist/runner");
|
|
32
|
+
const { loadConfig } = require("@atlisp/lint/dist/config");
|
|
33
|
+
const config = loadConfig();
|
|
34
|
+
|
|
35
|
+
const tmpDir = fs.mkdtempSync(path.join(require("os").tmpdir(), "lint-test-"));
|
|
36
|
+
const tmpFile = path.join(tmpDir, "empty.lsp");
|
|
37
|
+
fs.writeFileSync(tmpFile, "");
|
|
38
|
+
|
|
39
|
+
const issues = lintFiles([tmpFile], config, tmpDir);
|
|
40
|
+
expect(Array.isArray(issues)).toBe(true);
|
|
41
|
+
|
|
42
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test("collectLspFiles returns .lsp files", () => {
|
|
46
|
+
const { collectLspFiles } = require("../commands/lint");
|
|
47
|
+
const tmpDir = fs.mkdtempSync(path.join(require("os").tmpdir(), "lint-collect-"));
|
|
48
|
+
fs.writeFileSync(path.join(tmpDir, "a.lsp"), "");
|
|
49
|
+
fs.writeFileSync(path.join(tmpDir, "b.txt"), "not lisp");
|
|
50
|
+
fs.writeFileSync(path.join(tmpDir, "c.lsp"), "");
|
|
51
|
+
|
|
52
|
+
const files = collectLspFiles(tmpDir);
|
|
53
|
+
expect(files.length).toBe(2);
|
|
54
|
+
expect(files.every(f => f.endsWith(".lsp"))).toBe(true);
|
|
55
|
+
|
|
56
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
57
|
+
});
|
|
58
|
+
});
|