jinzd-ai-cli 0.1.49 → 0.1.50

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/CLAUDE.md CHANGED
@@ -350,6 +350,55 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
350
350
  - [x] **web_fetch DNS 解析时 SSRF 防护**(v0.1.25):新增 `resolveAndCheck()` 函数,用 `dns.promises.lookup()` 预解析域名,检查结果 IP 是否为私有地址。初始 URL 和 redirect 目标均校验。
351
351
  - [ ] **`persistentCwd` 全局状态**:bash 工具的当前工作目录是模块级全局变量,多 session 并发时可能串扰。现阶段单 session REPL 无影响,GUI 多会话扩展时需重构为 per-session 状态。
352
352
 
353
+ ## 本轮开发完成记录(2026-03-08,v0.1.49 → v0.1.50)
354
+
355
+ ### 代码质量:L1 低危修复 — run-tests.ts package.json 细粒度错误处理
356
+
357
+ **问题**:`detectProject()` 中 `JSON.parse(readFileSync('package.json'))` 错误处理粗糙——文件读取和 JSON 解析混在同一个 catch 中;解析失败时静默 fallthrough 可能误判为 Python/Go 项目;无 test script 时不尝试检测已安装的测试框架。
358
+
359
+ **修复**(`src/tools/builtin/run-tests.ts`):
360
+
361
+ 新增 `safeReadPackageJson(cwd)` 辅助函数——细粒度四层处理:
362
+ 1. **文件读取错误**:区分 `EACCES`/`EPERM`(权限问题)和其他读取错误,分别给出不同提示
363
+ 2. **UTF-8 BOM 处理**:自动剥离 BOM(Windows Notepad 等编辑器常见问题),防止 `JSON.parse` 失败
364
+ 3. **空文件检测**:`raw.trim() === ''` 时给出明确 "package.json is empty" 提示
365
+ 4. **JSON 解析错误细分**:区分语法错误(`Unexpected token`)、截断文件(`Unexpected end`)、根不是对象(数组或其他类型)
366
+
367
+ 新增 `detectNodeTestFramework(cwd, pkg)` 辅助函数——当 package.json 有效但无 `scripts.test` 时:
368
+ - 从 `devDependencies` + `dependencies` + 配置文件检测 5 种测试框架:
369
+ - **Vitest**:`vitest` 依赖 或 `vitest.config.{ts,js,mts}`
370
+ - **Jest**:`jest` 依赖 或 `jest.config.{js,ts,mjs}`
371
+ - **Mocha**:`mocha` 依赖 或 `.mocharc.{yml,yaml,json,js}`
372
+ - **Ava**:`ava` 依赖
373
+ - **Playwright**:`@playwright/test` 依赖
374
+ - 检测到框架时直接使用 `npx <framework>` 命令,无需 `scripts.test`
375
+
376
+ 更新 filter 参数处理——不同 Node 测试框架使用正确的 filter 语法:
377
+ - vitest/jest → `-t "filter"`
378
+ - mocha/playwright → `--grep "filter"`
379
+ - npm test → `-- --grep "filter"`(passthrough)
380
+
381
+ **改善效果**:
382
+ - package.json 格式错误时不再静默 fallthrough,返回 `npm (package.json error)` 仍识别为 Node 项目
383
+ - 安装了 vitest/jest/mocha 但未配置 `scripts.test` 的项目现在能自动检测并运行测试
384
+ - 错误信息更具体、更有指导性
385
+
386
+ ### 版本与收尾
387
+ - `src/core/constants.ts`:VERSION `0.1.49` → `0.1.50`
388
+ - `package.json`:version 同步
389
+ - 构建验证:`npm run build` 零错误(ESM + CJS 双产物)
390
+ - 代码审查报告 L1 条目标记为 ✅ 已修复
391
+
392
+ ### 本轮变更文件汇总
393
+
394
+ | 文件 | 变更类型 | 说明 |
395
+ |------|---------|------|
396
+ | `src/tools/builtin/run-tests.ts` | 修改 | safeReadPackageJson + detectNodeTestFramework + filter 语法更新 |
397
+ | `src/core/constants.ts` | 修改 | VERSION 0.1.49 → 0.1.50 |
398
+ | `package.json` | 修改 | version 0.1.49 → 0.1.50 |
399
+
400
+ ---
401
+
353
402
  ## 本轮开发完成记录(2026-03-08,v0.1.47 → v0.1.48)
354
403
 
355
404
  ### Tier 2 体验增强:/undo 增强 + /fork 对话分支
@@ -415,7 +464,7 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
415
464
  ### 下一步建议
416
465
 
417
466
  #### Tier 2 — 体验增强(剩余)
418
- 1. **L1 低危**:`run-tests.ts` `JSON.parse(package.json)` 细粒度错误处理
467
+ 1. ~~**L1 低危**~~:✅ 已在 v0.1.50 修复(safeReadPackageJson + detectNodeTestFramework)
419
468
  2. **IDE 集成**:VS Code 扩展(架构已准备就绪,core/providers/tools 无终端依赖)
420
469
  3. **OAuth/浏览器登录**:无需手动填 API Key,打开浏览器完成 OAuth 流程自动保存 token
421
470
 
@@ -1437,7 +1486,7 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
1437
1486
 
1438
1487
  | # | 文件 | 问题描述 |
1439
1488
  |---|------|---------|
1440
- | L1 | `src/tools/builtin/run-tests.ts:35` | `JSON.parse(package.json)` 无细粒度错误处理 |
1489
+ | L1 | `src/tools/builtin/run-tests.ts` | ✅ v0.1.50 已修复:`safeReadPackageJson()` 细粒度错误处理 + BOM + 框架自动检测 |
1441
1490
  | L2 | `src/tools/builtin/web-fetch.ts` | 恶意 HTML 大量 script 标签时正则性能下降 |
1442
1491
  | L3 | `src/session/session.ts` | `new Date(d.created)` 非法字符串返回 Invalid Date,比较失败 |
1443
1492
  | L4 | `src/tools/builtin/ask-user.ts` / `google-search.ts` | 模块级全局上下文,多会话架构下会串扰 |
@@ -8,7 +8,7 @@ import { platform } from "os";
8
8
  import chalk from "chalk";
9
9
 
10
10
  // src/core/constants.ts
11
- var VERSION = "0.1.49";
11
+ var VERSION = "0.1.50";
12
12
  var APP_NAME = "ai-cli";
13
13
  var CONFIG_DIR_NAME = ".aicli";
14
14
  var CONFIG_FILE_NAME = "config.json";
@@ -80,6 +80,73 @@ var REPO_URL = "https://gitee.com/jinzhengdong/ai-courses";
80
80
 
81
81
  // src/tools/builtin/run-tests.ts
82
82
  var IS_WINDOWS = platform() === "win32";
83
+ function detectNodeTestFramework(cwd, pkg) {
84
+ const devDeps = pkg.devDependencies ?? {};
85
+ const deps = pkg.dependencies ?? {};
86
+ const allDeps = { ...deps, ...devDeps };
87
+ if ("vitest" in allDeps || existsSync(join(cwd, "vitest.config.ts")) || existsSync(join(cwd, "vitest.config.js")) || existsSync(join(cwd, "vitest.config.mts"))) {
88
+ return { type: "node", framework: "vitest", command: "npx vitest run" };
89
+ }
90
+ if ("jest" in allDeps || existsSync(join(cwd, "jest.config.js")) || existsSync(join(cwd, "jest.config.ts")) || existsSync(join(cwd, "jest.config.mjs"))) {
91
+ return { type: "node", framework: "jest", command: "npx jest" };
92
+ }
93
+ if ("mocha" in allDeps || existsSync(join(cwd, ".mocharc.yml")) || existsSync(join(cwd, ".mocharc.yaml")) || existsSync(join(cwd, ".mocharc.json")) || existsSync(join(cwd, ".mocharc.js"))) {
94
+ return { type: "node", framework: "mocha", command: "npx mocha" };
95
+ }
96
+ if ("ava" in allDeps) {
97
+ return { type: "node", framework: "ava", command: "npx ava" };
98
+ }
99
+ if ("@playwright/test" in allDeps) {
100
+ return { type: "node", framework: "playwright", command: "npx playwright test" };
101
+ }
102
+ return null;
103
+ }
104
+ function safeReadPackageJson(cwd) {
105
+ const filePath = join(cwd, "package.json");
106
+ let raw;
107
+ try {
108
+ raw = readFileSync(filePath, "utf-8");
109
+ } catch (err) {
110
+ const code = err.code;
111
+ if (code === "EACCES" || code === "EPERM") {
112
+ process.stderr.write(`[Warning] Permission denied reading package.json (${code})
113
+ `);
114
+ } else {
115
+ process.stderr.write(
116
+ `[Warning] Cannot read package.json: ${err instanceof Error ? err.message : String(err)}
117
+ `
118
+ );
119
+ }
120
+ return null;
121
+ }
122
+ if (raw.charCodeAt(0) === 65279) {
123
+ raw = raw.slice(1);
124
+ }
125
+ if (!raw.trim()) {
126
+ process.stderr.write("[Warning] package.json is empty\n");
127
+ return null;
128
+ }
129
+ try {
130
+ const parsed = JSON.parse(raw);
131
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
132
+ process.stderr.write("[Warning] package.json root is not a JSON object\n");
133
+ return null;
134
+ }
135
+ return parsed;
136
+ } catch (err) {
137
+ const msg = err instanceof Error ? err.message : String(err);
138
+ if (msg.includes("Unexpected token")) {
139
+ process.stderr.write(`[Warning] package.json has syntax error: ${msg}
140
+ `);
141
+ } else if (msg.includes("Unexpected end")) {
142
+ process.stderr.write("[Warning] package.json is truncated or incomplete\n");
143
+ } else {
144
+ process.stderr.write(`[Warning] package.json parse failed: ${msg}
145
+ `);
146
+ }
147
+ return null;
148
+ }
149
+ }
83
150
  function detectProject(cwd) {
84
151
  if (existsSync(join(cwd, "pom.xml"))) {
85
152
  return { type: "java", framework: "Maven (JUnit)", command: IS_WINDOWS ? "mvn.cmd test" : "mvn test" };
@@ -90,17 +157,16 @@ function detectProject(cwd) {
90
157
  return { type: "java", framework: "Gradle (JUnit)", command: cmd };
91
158
  }
92
159
  if (existsSync(join(cwd, "package.json"))) {
93
- try {
94
- const pkg = JSON.parse(readFileSync(join(cwd, "package.json"), "utf-8"));
95
- const testScript = pkg?.scripts?.test;
160
+ const pkg = safeReadPackageJson(cwd);
161
+ if (pkg) {
162
+ const testScript = pkg.scripts?.test;
96
163
  if (testScript && testScript !== 'echo "Error: no test specified" && exit 1') {
97
164
  return { type: "node", framework: "npm", command: "npm test" };
98
165
  }
99
- } catch (err) {
100
- process.stderr.write(
101
- `[Warning] Failed to parse package.json: ${err instanceof Error ? err.message : String(err)}
102
- `
103
- );
166
+ const frameworkDetected = detectNodeTestFramework(cwd, pkg);
167
+ if (frameworkDetected) return frameworkDetected;
168
+ } else {
169
+ return { type: "node", framework: "npm (package.json error)", command: "npm test" };
104
170
  }
105
171
  }
106
172
  if (existsSync(join(cwd, "pyproject.toml")) || existsSync(join(cwd, "setup.py")) || existsSync(join(cwd, "pytest.ini"))) {
@@ -268,7 +334,17 @@ async function executeTests(args) {
268
334
  } else if (detected.type === "go") {
269
335
  command = `go test ./... -run "${filter}"`;
270
336
  } else if (detected.type === "node") {
271
- command += ` -- --grep "${filter}"`;
337
+ if (detected.framework === "vitest") {
338
+ command += ` -t "${filter}"`;
339
+ } else if (detected.framework === "jest") {
340
+ command += ` -t "${filter}"`;
341
+ } else if (detected.framework === "mocha") {
342
+ command += ` --grep "${filter}"`;
343
+ } else if (detected.framework === "playwright") {
344
+ command += ` --grep "${filter}"`;
345
+ } else {
346
+ command += ` -- --grep "${filter}"`;
347
+ }
272
348
  }
273
349
  }
274
350
  }
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  SUBAGENT_MAX_ROUNDS_LIMIT,
30
30
  VERSION,
31
31
  runTestsTool
32
- } from "./chunk-P4XKHPXU.js";
32
+ } from "./chunk-64YCGWC5.js";
33
33
 
34
34
  // src/index.ts
35
35
  import { program } from "commander";
@@ -4288,7 +4288,7 @@ ${hint}` : "")
4288
4288
  description: "Run project tests and show structured report",
4289
4289
  usage: "/test [command|filter]",
4290
4290
  async execute(args, _ctx) {
4291
- const { executeTests } = await import("./run-tests-JJVZWQKI.js");
4291
+ const { executeTests } = await import("./run-tests-6DPROQJ6.js");
4292
4292
  const argStr = args.join(" ").trim();
4293
4293
  let testArgs = {};
4294
4294
  if (argStr) {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-P4XKHPXU.js";
5
+ } from "./chunk-64YCGWC5.js";
6
6
  export {
7
7
  executeTests,
8
8
  runTestsTool
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinzd-ai-cli",
3
- "version": "0.1.49",
3
+ "version": "0.1.50",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",