jinzd-ai-cli 0.1.49 → 0.1.51

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
 
@@ -608,7 +657,7 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
608
657
  3. **`/diff` 命令**:显示当前 session 内所有文件修改的汇总 diff,便于 AI 操作后快速审查变更
609
658
 
610
659
  #### Tier 2 — 体验增强
611
- 4. **L1 低危**:`run-tests.ts` `JSON.parse(package.json)` 细粒度错误处理
660
+ 4. ~~**L1 低危**~~:✅ 已在 v0.1.50 修复(safeReadPackageJson + detectNodeTestFramework)
612
661
  5. **IDE 集成**:VS Code 扩展(架构已准备就绪,core/providers/tools 无终端依赖)
613
662
  6. **OAuth/浏览器登录**:无需手动填 API Key,打开浏览器完成 OAuth 流程自动保存 token
614
663
  7. ~~**`/undo` 增强**~~:✅ 已在 v0.1.48 实现(bash 文件追踪 + /undo list + /undo <n>)
@@ -687,7 +736,7 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
687
736
  ### 下一步建议
688
737
  1. ~~**Extended Thinking**~~:✅ 已在 v0.1.38 实现
689
738
  2. ~~**theme 迁移扩展**~~:✅ 已在 v0.1.38 全覆盖迁移
690
- 3. **L1 低危**:`run-tests.ts` `JSON.parse(package.json)` 细粒度错误处理
739
+ 3. ~~**L1 低危**~~:✅ 已在 v0.1.50 修复
691
740
  4. **IDE 集成**:VS Code 扩展(架构已准备就绪)
692
741
  5. **OAuth/浏览器登录**:无需手动填 API Key
693
742
 
@@ -777,7 +826,7 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
777
826
  2. ~~**`--resume <id>` 启动参数**~~:✅ 已在 v0.1.37 实现
778
827
  3. ~~**Word wrap 配置**~~:✅ 已在 v0.1.37 实现
779
828
  4. ~~**主题/颜色自定义**~~:✅ 已在 v0.1.37 实现
780
- 5. **L1 低危**:`run-tests.ts` `JSON.parse(package.json)` 细粒度错误处理
829
+ 5. ~~**L1 低危**~~:✅ 已在 v0.1.50 修复
781
830
 
782
831
  ---
783
832
 
@@ -851,7 +900,7 @@ const stdinLines = Array.isArray(rawStdin) ? rawStdin.map(String)
851
900
  ### 下一步建议
852
901
  1. **P0 功能缺口**:并行工具调用、`/add-dir` 命令
853
902
  2. **P1 功能缺口**:`/memory` 命令、`/doctor` 健康检查、`/bug` 反馈
854
- 3. **L1 低危**:`run-tests.ts` `JSON.parse(package.json)` 细粒度错误处理
903
+ 3. ~~**L1 低危**~~:✅ 已在 v0.1.50 修复
855
904
 
856
905
  ---
857
906
 
@@ -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.51";
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-U4BEPHO5.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-AWR6YAYZ.js");
4292
4292
  const argStr = args.join(" ").trim();
4293
4293
  let testArgs = {};
4294
4294
  if (argStr) {
@@ -5234,21 +5234,19 @@ ${pdfText}`;
5234
5234
  const dir = dirname3(normalizedPath);
5235
5235
  const nameNoExt = basename2(normalizedPath, ext);
5236
5236
  const textAlts = [".md", ".txt", ".html"].map((e) => resolve3(dir, nameNoExt + e)).filter(existsSync8);
5237
- return `[Binary file: ${filePath}]
5238
- PDF \u6587\u672C\u63D0\u53D6\u5931\u8D25\uFF08pdftotext \u548C pdfminer \u5747\u4E0D\u53EF\u7528\uFF09\u3002
5239
- ` + (textAlts.length > 0 ? `\u627E\u5230\u53EF\u66FF\u4EE3\u7684\u6587\u672C\u7248\u672C\uFF1A
5240
- ${textAlts.map((p) => ` \u2192 ${basename2(p)}`).join("\n")}
5241
- \u8BF7\u4F7F\u7528 read_file \u8BFB\u53D6\u4E0A\u8FF0\u6587\u4EF6\u3002` : `\u5982\u9700\u4F7F\u7528\u5176\u5185\u5BB9\uFF0C\u8BF7\u4F7F\u7528 bash \u5DE5\u5177\u5B89\u88C5\u5E76\u8C03\u7528 pdftotext\uFF1A
5242
- pip install pdfminer.six # \u5B89\u88C5 Python PDF \u5E93
5243
- \u6216\u5C06 PDF \u624B\u52A8\u8F6C\u6362\u4E3A .md / .txt \u6587\u4EF6\u540E\u518D\u8BFB\u53D6\u3002`);
5237
+ if (textAlts.length > 0) {
5238
+ return `[PDF file: ${filePath}]
5239
+ \u6B64 PDF \u6587\u4EF6\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u81EA\u52A8\u63D0\u53D6\u6587\u672C\uFF0C\u4F46\u627E\u5230\u53EF\u66FF\u4EE3\u7684\u6587\u672C\u7248\u672C\uFF1A
5240
+ ` + textAlts.map((p) => ` \u2192 ${basename2(p)}`).join("\n") + `
5241
+ \u8BF7\u4F7F\u7528 read_file \u8BFB\u53D6\u4E0A\u8FF0\u6587\u4EF6\u3002`;
5242
+ }
5243
+ return `[PDF file: ${filePath}]
5244
+ \u6B64 PDF \u6587\u4EF6\u5F53\u524D\u73AF\u5883\u65E0\u6CD5\u81EA\u52A8\u63D0\u53D6\u6587\u672C\uFF08\u9700\u5B89\u88C5 pdftotext \u6216 pdfminer.six\uFF09\u3002
5245
+ \u5EFA\u8BAE\uFF1A\u76F4\u63A5\u53C2\u8003\u9879\u76EE\u4E2D\u5DF2\u6709\u7684\u6587\u672C\u7248\u672C\uFF08.md / .txt\uFF09\uFF0C\u6216\u7528 bash \u5B89\u88C5\u63D0\u53D6\u5DE5\u5177\u540E\u91CD\u8BD5\u3002`;
5244
5246
  }
5245
5247
  if (BINARY_EXTENSIONS.has(ext)) {
5246
- return `[Binary file: ${filePath}]
5247
- \u6B64\u6587\u4EF6\u4E3A\u4E8C\u8FDB\u5236\u683C\u5F0F\uFF08${ext}\uFF09\uFF0C\u65E0\u6CD5\u4F5C\u4E3A\u6587\u672C\u8BFB\u53D6\u3002
5248
- \u5982\u9700\u4F7F\u7528\u5176\u5185\u5BB9\uFF0C\u8BF7\u8003\u8651\uFF1A
5249
- 1. \u5C06\u6587\u4EF6\u8F6C\u6362\u4E3A\u6587\u672C\u683C\u5F0F\u540E\u518D\u8BFB\u53D6
5250
- 2. \u4F7F\u7528 bash \u5DE5\u5177\u8C03\u7528\u5916\u90E8\u8F6C\u6362\u7A0B\u5E8F\uFF08\u5982 pandoc \u7B49\uFF09
5251
- 3. \u82E5\u6709\u5BF9\u5E94\u7684\u7EAF\u6587\u672C\u7248\u672C\uFF08.md / .txt\uFF09\uFF0C\u8BF7\u76F4\u63A5\u8BFB\u53D6\u90A3\u4E2A\u6587\u4EF6`;
5248
+ return `[Binary file: ${filePath} (${ext})]
5249
+ \u6B64\u6587\u4EF6\u4E3A\u4E8C\u8FDB\u5236\u683C\u5F0F\uFF0C\u4E0D\u652F\u6301\u76F4\u63A5\u6587\u672C\u8BFB\u53D6\u3002\u5982\u9879\u76EE\u4E2D\u6709\u5BF9\u5E94\u7684\u6587\u672C\u7248\u672C\uFF08.md / .txt\uFF09\uFF0C\u8BF7\u76F4\u63A5\u8BFB\u53D6\u3002`;
5252
5250
  }
5253
5251
  const buf = readFileSync5(normalizedPath);
5254
5252
  if (encoding === "base64") {
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  executeTests,
4
4
  runTestsTool
5
- } from "./chunk-P4XKHPXU.js";
5
+ } from "./chunk-U4BEPHO5.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.51",
4
4
  "description": "Cross-platform REPL-style AI CLI with multi-provider support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",