code-abyss 1.6.15 → 1.6.16

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/bin/install.js CHANGED
@@ -4,8 +4,16 @@ const fs = require('fs');
4
4
  const path = require('path');
5
5
  const os = require('os');
6
6
 
7
- const VERSION = require(path.join(__dirname, '..', 'package.json')).version;
7
+ const pkg = require(path.join(__dirname, '..', 'package.json'));
8
+ const VERSION = pkg.version;
8
9
  const HOME = os.homedir();
10
+
11
+ // ── Node.js 版本检查 ──
12
+ const MIN_NODE = pkg.engines?.node?.match(/(\d+)/)?.[1] || '18';
13
+ if (parseInt(process.versions.node) < parseInt(MIN_NODE)) {
14
+ console.error(`\x1b[31m✘ 需要 Node.js >= ${MIN_NODE},当前: ${process.versions.node}\x1b[0m`);
15
+ process.exit(1);
16
+ }
9
17
  const SKIP = ['__pycache__', '.pyc', '.pyo', '.egg-info', '.DS_Store', 'Thumbs.db', '.git'];
10
18
  const PKG_ROOT = fs.realpathSync(path.join(__dirname, '..'));
11
19
 
@@ -209,6 +217,10 @@ function runUninstall(tgt) {
209
217
  if (!fs.existsSync(manifestPath)) { fail(`未找到安装记录: ${manifestPath}`); process.exit(1); }
210
218
 
211
219
  const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
220
+ if (manifest.manifest_version && manifest.manifest_version > 1) {
221
+ fail(`manifest 版本 ${manifest.manifest_version} 不兼容,请升级 code-abyss 后再卸载`);
222
+ process.exit(1);
223
+ }
212
224
  divider(`卸载 Code Abyss v${manifest.version}`);
213
225
 
214
226
  (manifest.installed || []).forEach(f => {
@@ -245,7 +257,7 @@ function installCore(tgt) {
245
257
  { src: 'skills', dest: 'skills' }
246
258
  ].filter(f => f.dest !== null);
247
259
 
248
- const manifest = { version: VERSION, target: tgt, timestamp: new Date().toISOString(), installed: [], backups: [] };
260
+ const manifest = { manifest_version: 1, version: VERSION, target: tgt, timestamp: new Date().toISOString(), installed: [], backups: [] };
249
261
 
250
262
  filesToInstall.forEach(({ src, dest }) => {
251
263
  const srcPath = path.join(PKG_ROOT, src);
@@ -349,6 +361,7 @@ async function installCcline(ctx) {
349
361
  const { execSync } = require('child_process');
350
362
  const cclineDir = path.join(HOME, '.claude', 'ccline');
351
363
  const cclineBin = path.join(cclineDir, process.platform === 'win32' ? 'ccline.exe' : 'ccline');
364
+ const errors = [];
352
365
 
353
366
  // 1. 检测是否已有二进制
354
367
  let hasBin = fs.existsSync(cclineBin);
@@ -366,8 +379,9 @@ async function installCcline(ctx) {
366
379
  else {
367
380
  try { execSync('ccline --version', { stdio: 'pipe' }); hasBin = true; ok('ccline 安装成功 (全局)'); } catch (e) {}
368
381
  }
382
+ if (!hasBin) errors.push('ccline 二进制安装后仍未检测到');
369
383
  } catch (e) {
370
- warn('npm install -g @cometix/ccline 失败');
384
+ errors.push(`npm install -g @cometix/ccline 失败: ${e.message}`);
371
385
  info(`手动安装: ${c.cyn('npm install -g @cometix/ccline')}`);
372
386
  info(`或下载: ${c.cyn('https://github.com/Haleclipse/CCometixLine/releases')}`);
373
387
  }
@@ -392,7 +406,7 @@ async function installCcline(ctx) {
392
406
  // 无打包配置,回退到 ccline --init
393
407
  if (hasBin && !fs.existsSync(targetConfig)) {
394
408
  try { execSync('ccline --init', { stdio: 'inherit' }); ok('ccline 默认配置已生成'); }
395
- catch (e) { warn('ccline --init 失败,可手动运行'); }
409
+ catch (e) { errors.push(`ccline --init 失败: ${e.message}`); }
396
410
  }
397
411
  }
398
412
 
@@ -401,6 +415,13 @@ async function installCcline(ctx) {
401
415
  ok(`statusLine → ${c.cyn(CCLINE_STATUS_LINE.statusLine.command)}`);
402
416
  fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
403
417
 
418
+ // 5. 汇总报告
419
+ if (errors.length > 0) {
420
+ console.log('');
421
+ warn(c.b(`ccline 安装有 ${errors.length} 个问题:`));
422
+ errors.forEach(e => fail(` ${e}`));
423
+ }
424
+
404
425
  console.log('');
405
426
  warn(`需要 ${c.b('Nerd Font')} 字体才能正确显示图标`);
406
427
  info(`推荐: FiraCode Nerd Font / JetBrainsMono Nerd Font`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-abyss",
3
- "version": "1.6.15",
3
+ "version": "1.6.16",
4
4
  "description": "邪修红尘仙·宿命深渊 - 一键为 Claude Code / Codex CLI 注入邪修人格与安全工程知识体系",
5
5
  "keywords": [
6
6
  "claude",
@@ -16,9 +16,17 @@ Skills 运行入口
16
16
 
17
17
  import sys
18
18
  import subprocess
19
+ import hashlib
20
+ import tempfile
19
21
  from pathlib import Path
20
22
  import os
21
23
 
24
+ IS_WIN = sys.platform == 'win32'
25
+ if IS_WIN:
26
+ import msvcrt
27
+ else:
28
+ import fcntl
29
+
22
30
 
23
31
  def get_skills_dir() -> Path:
24
32
  """获取 skills 目录路径(跨平台)"""
@@ -28,32 +36,84 @@ def get_skills_dir() -> Path:
28
36
  return Path(__file__).resolve().parent
29
37
 
30
38
 
39
+ def discover_skills(skills_dir: Path) -> dict:
40
+ """自动发现 tools/ 下所有 skill(按目录结构扫描)"""
41
+ found = {}
42
+ tools_dir = skills_dir / "tools"
43
+ if not tools_dir.is_dir():
44
+ return found
45
+ for skill_dir in sorted(tools_dir.iterdir()):
46
+ if not skill_dir.is_dir():
47
+ continue
48
+ scripts_dir = skill_dir / "scripts"
49
+ if not scripts_dir.is_dir():
50
+ continue
51
+ py_files = list(scripts_dir.glob("*.py"))
52
+ if py_files:
53
+ found[skill_dir.name] = py_files[0]
54
+ return found
55
+
56
+
31
57
  def get_script_path(skill_name: str) -> Path:
32
58
  """获取 skill 脚本路径"""
33
59
  skills_dir = get_skills_dir()
60
+ available = discover_skills(skills_dir)
34
61
 
35
- script_map = {
36
- "verify-module": ("tools", "module_scanner.py"),
37
- "verify-security": ("tools", "security_scanner.py"),
38
- "verify-change": ("tools", "change_analyzer.py"),
39
- "verify-quality": ("tools", "quality_checker.py"),
40
- "gen-docs": ("tools", "doc_generator.py"),
41
- }
42
-
43
- if skill_name not in script_map:
44
- available = ", ".join(script_map.keys())
62
+ if skill_name not in available:
63
+ names = ", ".join(available.keys()) if available else "(无)"
45
64
  print(f"错误: 未知的 skill '{skill_name}'")
46
- print(f"可用的 skills: {available}")
65
+ print(f"可用的 skills: {names}")
47
66
  sys.exit(1)
48
67
 
49
- category, script_name = script_map[skill_name]
50
- script_path = skills_dir / category / skill_name / "scripts" / script_name
51
-
52
- if not script_path.exists():
53
- print(f"错误: 脚本不存在 {script_path}")
54
- sys.exit(1)
55
-
56
- return script_path
68
+ return available[skill_name]
69
+
70
+
71
+ def acquire_target_lock(args: list):
72
+ """按目标路径获取文件锁,同路径串行,不同路径并行。返回 (lock_fd, lock_path) 或 (None, None)"""
73
+ # 从参数中提取目标路径(第一个非 flag 参数)
74
+ target = None
75
+ for a in args:
76
+ if not a.startswith('-'):
77
+ target = a
78
+ break
79
+ if not target:
80
+ target = os.getcwd()
81
+
82
+ target = str(Path(target).resolve())
83
+ lock_name = "sage_skill_" + hashlib.md5(target.encode()).hexdigest()[:12] + ".lock"
84
+ lock_path = Path(tempfile.gettempdir()) / lock_name
85
+
86
+ fd = open(lock_path, 'w')
87
+ if IS_WIN:
88
+ import time
89
+ for _ in range(300): # 最多等 30s
90
+ try:
91
+ msvcrt.locking(fd.fileno(), msvcrt.LK_NBLCK, 1)
92
+ return fd, lock_path
93
+ except OSError:
94
+ time.sleep(0.1)
95
+ print(f"⏳ 等待锁超时: {target}")
96
+ msvcrt.locking(fd.fileno(), msvcrt.LK_LOCK, 1)
97
+ else:
98
+ try:
99
+ fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
100
+ except OSError:
101
+ print(f"⏳ 等待锁释放: {target}")
102
+ fcntl.flock(fd, fcntl.LOCK_EX) # 阻塞等待
103
+ return fd, lock_path
104
+
105
+
106
+ def release_lock(fd):
107
+ """释放文件锁"""
108
+ if not fd:
109
+ return
110
+ try:
111
+ if IS_WIN:
112
+ msvcrt.locking(fd.fileno(), msvcrt.LK_UNLCK, 1)
113
+ else:
114
+ fcntl.flock(fd, fcntl.LOCK_UN)
115
+ finally:
116
+ fd.close()
57
117
 
58
118
 
59
119
  def main():
@@ -70,6 +130,9 @@ def main():
70
130
  script_path = get_script_path(skill_name)
71
131
  args = sys.argv[2:]
72
132
 
133
+ # 按目标路径加锁,防止多 Agent 同时操作同一目录
134
+ lock_fd, lock_path = acquire_target_lock(args)
135
+
73
136
  # 使用 sys.executable 确保使用当前 Python 解释器
74
137
  cmd = [sys.executable, str(script_path)] + args
75
138
 
@@ -82,6 +145,8 @@ def main():
82
145
  except Exception as e:
83
146
  print(f"执行错误: {e}")
84
147
  sys.exit(1)
148
+ finally:
149
+ release_lock(lock_fd)
85
150
 
86
151
 
87
152
  if __name__ == "__main__":
@@ -5,6 +5,7 @@
5
5
  """
6
6
 
7
7
  import os
8
+ import re
8
9
  import sys
9
10
  import json
10
11
  import ast
@@ -125,13 +126,41 @@ def analyze_module(path: Path) -> ModuleInfo:
125
126
  if language == 'Python':
126
127
  return analyze_python_module(path)
127
128
 
128
- # 通用分析
129
+ # 通用分析(regex fallback 提取函数/类)
129
130
  info = ModuleInfo(name=path.name, path=str(path), language=language)
130
131
 
132
+ lang_patterns = {
133
+ 'Go': (r'^\s*func\s+(\w+)', r'^\s*type\s+(\w+)\s+struct\b'),
134
+ 'Rust': (r'^\s*(?:pub\s+)?fn\s+(\w+)', r'^\s*(?:pub\s+)?struct\s+(\w+)'),
135
+ 'TypeScript': (r'^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)', r'^\s*(?:export\s+)?class\s+(\w+)'),
136
+ 'JavaScript': (r'^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)', r'^\s*(?:export\s+)?class\s+(\w+)'),
137
+ 'Java': (r'^\s*(?:public|private|protected)?\s*(?:static\s+)?\w+\s+(\w+)\s*\(', r'^\s*(?:public\s+)?class\s+(\w+)'),
138
+ 'C++': (r'^\s*(?:\w+\s+)+(\w+)\s*\([^;]*$', r'^\s*class\s+(\w+)'),
139
+ 'C': (r'^\s*(?:\w+\s+)+(\w+)\s*\([^;]*$', None),
140
+ }
141
+
131
142
  code_extensions = {'.py', '.go', '.rs', '.ts', '.js', '.java', '.c', '.cpp'}
143
+ func_pat, cls_pat = lang_patterns.get(language, (None, None))
144
+
132
145
  for f in path.rglob('*'):
133
146
  if f.is_file() and f.suffix.lower() in code_extensions:
134
- info.files.append(str(f.relative_to(path)))
147
+ rel = str(f.relative_to(path))
148
+ info.files.append(rel)
149
+
150
+ if func_pat or cls_pat:
151
+ try:
152
+ content = f.read_text(encoding='utf-8', errors='ignore')
153
+ for line in content.split('\n'):
154
+ if func_pat:
155
+ m = re.match(func_pat, line)
156
+ if m and not m.group(1).startswith('_'):
157
+ info.functions.append({"name": m.group(1), "file": rel, "doc": ""})
158
+ if cls_pat:
159
+ m = re.match(cls_pat, line)
160
+ if m and not m.group(1).startswith('_'):
161
+ info.classes.append({"name": m.group(1), "file": rel, "doc": ""})
162
+ except Exception:
163
+ continue
135
164
 
136
165
  return info
137
166
 
@@ -95,7 +95,8 @@ SECURITY_RULES = [
95
95
  "id": "HARDCODED_SECRET",
96
96
  "category": "敏感信息",
97
97
  "severity": Severity.HIGH,
98
- "pattern": r'(password|passwd|pwd|secret|api_key|apikey|token|auth)\s*=\s*["\'][^"\']{8,}["\']',
98
+ "pattern": r'(?<!\w)(password|passwd|pwd|secret|api_key|apikey|token|auth_token)\s*=\s*["\'][^"\']{8,}["\']',
99
+ "exclude_pattern": r'(example|placeholder|changeme|xxx|your[_-]|TODO|FIXME|<.*>|\*{3,})',
99
100
  "extensions": [".py", ".js", ".ts", ".go", ".java", ".php", ".rb", ".yaml", ".yml", ".json", ".env"],
100
101
  "message": "可能存在硬编码密钥/密码",
101
102
  "recommendation": "使用环境变量或密钥管理服务"
@@ -245,6 +246,11 @@ def scan_file(file_path: Path, rules: List[Dict]) -> List[Finding]:
245
246
  continue
246
247
 
247
248
  if pattern.search(line):
249
+ # 排除已知安全模式(如 placeholder、example)
250
+ exclude = rule.get("exclude_pattern")
251
+ if exclude and re.search(exclude, line, re.IGNORECASE):
252
+ continue
253
+
248
254
  findings.append(Finding(
249
255
  severity=rule["severity"],
250
256
  category=rule["category"],