code-abyss 1.7.2 → 1.7.3

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
@@ -15,7 +15,7 @@ if (parseInt(process.versions.node) < parseInt(MIN_NODE)) {
15
15
  process.exit(1);
16
16
  }
17
17
  const PKG_ROOT = fs.realpathSync(path.join(__dirname, '..'));
18
- const { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog } =
18
+ const { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, parseFrontmatter } =
19
19
  require(path.join(__dirname, 'lib', 'utils.js'));
20
20
  const { detectCclineBin, installCcline: _installCcline } = require(path.join(__dirname, 'lib', 'ccline.js'));
21
21
 
@@ -193,6 +193,127 @@ function runUninstall(tgt) {
193
193
 
194
194
  // ── 安装核心 ──
195
195
 
196
+ /**
197
+ * 递归扫描 skills 目录,找出所有 user-invocable: true 的 SKILL.md
198
+ * @param {string} skillsDir - skills 源目录绝对路径
199
+ * @returns {Array<{meta: Object, relPath: string, hasScripts: boolean}>}
200
+ */
201
+ function scanInvocableSkills(skillsDir) {
202
+ const results = [];
203
+ function scan(dir) {
204
+ const skillMd = path.join(dir, 'SKILL.md');
205
+ if (fs.existsSync(skillMd)) {
206
+ try {
207
+ const content = fs.readFileSync(skillMd, 'utf8');
208
+ const meta = parseFrontmatter(content);
209
+ if (meta && meta['user-invocable'] === 'true' && meta.name) {
210
+ const relPath = path.relative(skillsDir, dir);
211
+ const scriptsDir = path.join(dir, 'scripts');
212
+ const hasScripts = fs.existsSync(scriptsDir) &&
213
+ fs.readdirSync(scriptsDir).some(f => f.endsWith('.js'));
214
+ results.push({ meta, relPath, hasScripts });
215
+ }
216
+ } catch (e) { /* 解析失败跳过 */ }
217
+ }
218
+ try {
219
+ fs.readdirSync(dir).forEach(sub => {
220
+ const subPath = path.join(dir, sub);
221
+ if (fs.statSync(subPath).isDirectory() && !shouldSkip(sub) && sub !== 'scripts') {
222
+ scan(subPath);
223
+ }
224
+ });
225
+ } catch (e) { /* 读取失败跳过 */ }
226
+ }
227
+ scan(skillsDir);
228
+ return results;
229
+ }
230
+
231
+ /**
232
+ * 根据 SKILL.md 元数据生成 command .md 内容
233
+ *
234
+ * 设计原则:
235
+ * - 读取 SKILL.md + 执行脚本合并为一气呵成的指令流
236
+ * - 禁止「先…然后…」的分步模式,避免 Claude 在步骤间停顿
237
+ * - 无脚本的 skill:仅读取 SKILL.md 作为知识库提供指导
238
+ *
239
+ * @param {Object} meta - parseFrontmatter 返回的元数据
240
+ * @param {string} skillRelPath - 相对于 skills/ 的路径(如 'tools/gen-docs')
241
+ * @param {boolean} hasScripts - 是否有可执行脚本
242
+ * @returns {string} command .md 文件内容
243
+ */
244
+ function generateCommandContent(meta, skillRelPath, hasScripts) {
245
+ const name = meta.name;
246
+ const desc = (meta.description || '').replace(/"/g, '\\"');
247
+ const argHint = meta['argument-hint'];
248
+ const tools = meta['allowed-tools'] || 'Read';
249
+ const skillPath = skillRelPath
250
+ ? `~/.claude/skills/${skillRelPath}/SKILL.md`
251
+ : '~/.claude/skills/SKILL.md';
252
+
253
+ const lines = [
254
+ '---',
255
+ `name: ${name}`,
256
+ `description: "${desc}"`,
257
+ ];
258
+ if (argHint) lines.push(`argument-hint: "${argHint}"`);
259
+ lines.push(`allowed-tools: ${tools}`);
260
+ lines.push('---');
261
+ lines.push('');
262
+
263
+ if (hasScripts) {
264
+ // ── 有脚本的 skill:读取规范 + 执行脚本,一气呵成 ──
265
+ lines.push('以下所有步骤一气呵成,不要在步骤间停顿等待用户输入:');
266
+ lines.push('');
267
+ lines.push(`1. 读取规范:${skillPath}`);
268
+ lines.push(`2. 执行命令:\`node ~/.claude/skills/run_skill.js ${name} $ARGUMENTS\``);
269
+ lines.push('3. 按规范分析输出,完成后续动作');
270
+ lines.push('');
271
+ lines.push('全程不要停顿,不要询问是否继续。');
272
+ } else {
273
+ // ── 无脚本的 skill:知识库模式 ──
274
+ lines.push('读取以下秘典,根据内容为用户提供专业指导:');
275
+ lines.push('');
276
+ lines.push('```');
277
+ lines.push(skillPath);
278
+ lines.push('```');
279
+ }
280
+
281
+ lines.push('');
282
+ return lines.join('\n');
283
+ }
284
+
285
+ /**
286
+ * 扫描 skills 并为 user-invocable 的 skill 生成 command 包装,文件级合并安装
287
+ */
288
+ function installGeneratedCommands(skillsSrcDir, targetDir, backupDir, manifest) {
289
+ const skills = scanInvocableSkills(skillsSrcDir);
290
+ if (skills.length === 0) return 0;
291
+
292
+ const cmdsDir = path.join(targetDir, 'commands');
293
+ fs.mkdirSync(cmdsDir, { recursive: true });
294
+
295
+ skills.forEach(({ meta, relPath, hasScripts }) => {
296
+ const fileName = `${meta.name}.md`;
297
+ const destFile = path.join(cmdsDir, fileName);
298
+ const relFile = path.posix.join('commands', fileName);
299
+
300
+ if (fs.existsSync(destFile)) {
301
+ const cmdsBackupDir = path.join(backupDir, 'commands');
302
+ fs.mkdirSync(cmdsBackupDir, { recursive: true });
303
+ fs.copyFileSync(destFile, path.join(cmdsBackupDir, fileName));
304
+ manifest.backups.push(relFile);
305
+ info(`备份: ${c.d(relFile)}`);
306
+ }
307
+
308
+ const content = generateCommandContent(meta, relPath, hasScripts);
309
+ fs.writeFileSync(destFile, content);
310
+ manifest.installed.push(relFile);
311
+ });
312
+
313
+ ok(`commands/ ${c.d(`(自动生成 ${skills.length} 个斜杠命令)`)}`);
314
+ return skills.length;
315
+ }
316
+
196
317
  function installCore(tgt) {
197
318
  const targetDir = path.join(HOME, `.${tgt}`);
198
319
  const backupDir = path.join(targetDir, '.sage-backup');
@@ -205,7 +326,7 @@ function installCore(tgt) {
205
326
  { src: 'config/CLAUDE.md', dest: tgt === 'claude' ? 'CLAUDE.md' : null },
206
327
  { src: 'config/AGENTS.md', dest: tgt === 'codex' ? 'AGENTS.md' : null },
207
328
  { src: 'output-styles', dest: tgt === 'claude' ? 'output-styles' : null },
208
- { src: 'skills', dest: 'skills' }
329
+ { src: 'skills', dest: 'skills' },
209
330
  ].filter(f => f.dest !== null);
210
331
 
211
332
  const manifest = {
@@ -223,6 +344,7 @@ function installCore(tgt) {
223
344
  }
224
345
  warn(`跳过: ${src}`); return;
225
346
  }
347
+
226
348
  if (fs.existsSync(destPath)) {
227
349
  const bp = path.join(backupDir, dest);
228
350
  rmSafe(bp); copyRecursive(destPath, bp); manifest.backups.push(dest);
@@ -232,6 +354,12 @@ function installCore(tgt) {
232
354
  rmSafe(destPath); copyRecursive(srcPath, destPath); manifest.installed.push(dest);
233
355
  });
234
356
 
357
+ // 为 Claude 目标自动生成 user-invocable 斜杠命令
358
+ if (tgt === 'claude') {
359
+ const skillsSrc = path.join(PKG_ROOT, 'skills');
360
+ installGeneratedCommands(skillsSrc, targetDir, backupDir, manifest);
361
+ }
362
+
235
363
  const settingsPath = path.join(targetDir, 'settings.json');
236
364
  let settings = {};
237
365
  if (fs.existsSync(settingsPath)) {
@@ -426,5 +554,6 @@ if (require.main === module) {
426
554
 
427
555
  module.exports = {
428
556
  deepMergeNew, detectClaudeAuth, detectCodexAuth,
429
- detectCclineBin, copyRecursive, shouldSkip, SETTINGS_TEMPLATE
557
+ detectCclineBin, copyRecursive, shouldSkip, SETTINGS_TEMPLATE,
558
+ scanInvocableSkills, generateCommandContent, installGeneratedCommands
430
559
  };
package/bin/lib/utils.js CHANGED
@@ -7,15 +7,23 @@ const SKIP = ['__pycache__', '.pyc', '.pyo', '.egg-info', '.DS_Store', 'Thumbs.d
7
7
  function shouldSkip(name) { return SKIP.some(p => name.includes(p)); }
8
8
 
9
9
  function copyRecursive(src, dest) {
10
- const stat = fs.statSync(src);
10
+ let stat;
11
+ try { stat = fs.statSync(src); } catch (e) {
12
+ throw new Error(`复制失败: 源路径不存在 ${src} (${e.code})`);
13
+ }
11
14
  if (stat.isDirectory()) {
12
15
  if (shouldSkip(path.basename(src))) return;
13
16
  fs.mkdirSync(dest, { recursive: true });
14
- fs.readdirSync(src).forEach(f => {
15
- if (!shouldSkip(f)) copyRecursive(path.join(src, f), path.join(dest, f));
16
- });
17
+ for (const f of fs.readdirSync(src)) {
18
+ if (!shouldSkip(f)) {
19
+ try { copyRecursive(path.join(src, f), path.join(dest, f)); }
20
+ catch (e) { console.error(` ⚠ 跳过: ${path.join(src, f)} (${e.message})`); }
21
+ }
22
+ }
17
23
  } else {
18
24
  if (shouldSkip(path.basename(src))) return;
25
+ const destDir = path.dirname(dest);
26
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
19
27
  fs.copyFileSync(src, dest);
20
28
  }
21
29
  }
@@ -58,4 +66,22 @@ function printMergeLog(log, c) {
58
66
  });
59
67
  }
60
68
 
61
- module.exports = { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, SKIP };
69
+ /**
70
+ * 解析 Markdown 文件的 YAML frontmatter
71
+ * @param {string} content - 文件内容
72
+ * @returns {Object|null} 解析后的键值对,无 frontmatter 返回 null
73
+ */
74
+ const UNSAFE_KEYS = new Set(['__proto__', 'constructor', 'prototype']);
75
+
76
+ function parseFrontmatter(content) {
77
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
78
+ if (!match) return null;
79
+ const meta = Object.create(null);
80
+ match[1].split('\n').forEach(line => {
81
+ const m = line.match(/^([\w][\w-]*)\s*:\s*(.+)/);
82
+ if (m && !UNSAFE_KEYS.has(m[1])) meta[m[1]] = m[2].trim().replace(/^["']|["']$/g, '');
83
+ });
84
+ return meta;
85
+ }
86
+
87
+ module.exports = { shouldSkip, copyRecursive, rmSafe, deepMergeNew, printMergeLog, parseFrontmatter, SKIP };
package/config/CLAUDE.md CHANGED
@@ -54,7 +54,7 @@
54
54
  | ❄ 玄冰 | 镇魔之盾,护佑安宁 | 蓝队、告警、IOC、应急、取证、SIEM、EDR |
55
55
  | ⚡ 紫霄 | 攻守一体,方为大道 | 紫队、ATT&CK、TTP、检测验证、规则调优 |
56
56
 
57
- 详细攻防技术见 `skills/security/` 各秘典。
57
+ 详细攻防技术见 `skills/domains/security/` 各秘典。
58
58
 
59
59
  ---
60
60
 
@@ -156,15 +156,15 @@
156
156
 
157
157
  | 化身 | 秘典 | 触发场景 |
158
158
  |------|------|----------|
159
- | 🔥 赤焰 | `skills/security/red-team.md` | 渗透、红队、exploit、C2 |
160
- | ❄ 玄冰 | `skills/security/blue-team.md` | 蓝队、告警、IOC、应急 |
161
- | ⚡ 紫霄 | `skills/security/` | ATT&CK、TTP、攻防演练 |
162
- | 📜 符箓 | `skills/development/` | 语言开发任务 |
163
- | 👁 天眼 | `skills/security/threat-intel.md` | OSINT、威胁情报 |
164
- | 🔮 丹鼎 | `skills/ai/` | RAG、Agent、LLM |
165
- | 🕸 天罗 | `skills/multi-agent/` | TeamCreate、多Agent协同 |
166
- | 🏗 阵法 | `skills/architecture/` | 架构、API、云原生、缓存、合规 |
167
- | 🔧 炼器 | `skills/devops/` | Git、测试、数据库、性能、可观测性 |
159
+ | 🔥 赤焰 | `skills/domains/security/red-team.md` | 渗透、红队、exploit、C2 |
160
+ | ❄ 玄冰 | `skills/domains/security/blue-team.md` | 蓝队、告警、IOC、应急 |
161
+ | ⚡ 紫霄 | `skills/domains/security/` | ATT&CK、TTP、攻防演练 |
162
+ | 📜 符箓 | `skills/domains/development/` | 语言开发任务 |
163
+ | 👁 天眼 | `skills/domains/security/threat-intel.md` | OSINT、威胁情报 |
164
+ | 🔮 丹鼎 | `skills/domains/ai/` | RAG、Agent、LLM |
165
+ | 🕸 天罗 | `skills/orchestration/multi-agent/SKILL.md` | TeamCreate、多Agent协同 |
166
+ | 🏗 阵法 | `skills/domains/architecture/` | 架构、API、云原生、缓存、合规 |
167
+ | 🔧 炼器 | `skills/domains/devops/` | Git、测试、数据库、性能、可观测性 |
168
168
 
169
169
  **校验关卡**(自动触发,不可跳过):
170
170
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-abyss",
3
- "version": "1.7.2",
3
+ "version": "1.7.3",
4
4
  "description": "邪修红尘仙·宿命深渊 - 一键为 Claude Code / Codex CLI 注入邪修人格与安全工程知识体系",
5
5
  "keywords": [
6
6
  "claude",
package/skills/SKILL.md CHANGED
@@ -2,7 +2,7 @@
2
2
  name: sage
3
3
  description: 邪修红尘仙·神通秘典总纲。智能路由到专业秘典。当魔尊需要任何开发、安全、架构、DevOps、AI 相关能力时,通过此入口路由到最匹配的专业秘典。
4
4
  license: MIT
5
- user-invocable: true
5
+ user-invocable: false
6
6
  disable-model-invocation: false
7
7
  ---
8
8
 
@@ -55,38 +55,28 @@ function getScriptPath(skillName) {
55
55
  return available[skillName];
56
56
  }
57
57
 
58
+ function sleepMs(ms) {
59
+ const end = Date.now() + ms;
60
+ while (Date.now() < end) { /* busy wait */ }
61
+ }
62
+
58
63
  function acquireTargetLock(args) {
59
64
  const target = args.find(a => !a.startsWith('-')) || process.cwd();
60
65
  const hash = createHash('md5').update(resolve(target)).digest('hex').slice(0, 12);
61
66
  const lockPath = join(tmpdir(), `sage_skill_${hash}.lock`);
62
67
 
63
- try {
64
- const fd = openSync(lockPath, 'wx');
65
- return { fd, lockPath };
66
- } catch (e) {
67
- if (e.code === 'EEXIST') {
68
- console.log(`⏳ 等待锁释放: ${target}`);
69
- // 异步轮询等待,最多 30s
70
- const deadline = Date.now() + 30000;
71
- const poll = () => {
72
- if (Date.now() >= deadline) { console.error(`⏳ 等待锁超时: ${target}`); process.exit(1); }
73
- try {
74
- const fd = openSync(lockPath, 'wx');
75
- return { fd, lockPath };
76
- } catch { /* still locked */ }
77
- return null;
78
- };
79
- const result = poll();
80
- if (result) return result;
81
- return new Promise((resolve) => {
82
- const timer = setInterval(() => {
83
- const r = poll();
84
- if (r) { clearInterval(timer); resolve(r); }
85
- }, 200);
86
- });
68
+ const deadline = Date.now() + 30000;
69
+ let first = true;
70
+ while (true) {
71
+ try {
72
+ const fd = openSync(lockPath, 'wx');
73
+ return { fd, lockPath };
74
+ } catch (e) {
75
+ if (e.code !== 'EEXIST') return { fd: null, lockPath: null };
76
+ if (first) { console.log(`⏳ 等待锁释放: ${target}`); first = false; }
77
+ if (Date.now() >= deadline) { console.error(`⏳ 等待锁超时: ${target}`); process.exit(1); }
78
+ sleepMs(200);
87
79
  }
88
- // 其他错误,忽略锁继续执行
89
- return { fd: null, lockPath: null };
90
80
  }
91
81
  }
92
82
 
@@ -9,12 +9,69 @@ const path = require('path');
9
9
 
10
10
  // --- Utilities ---
11
11
 
12
- function rglob(dir, filter) {
12
+ function parseGitignore(modPath) {
13
+ const patterns = [];
14
+ const hardcoded = ['node_modules', '.git', '__pycache__', '.vscode', '.idea', 'dist', 'build', '.DS_Store'];
15
+
16
+ // 硬编码常见排除
17
+ hardcoded.forEach(p => patterns.push({ pattern: p, negate: false }));
18
+
19
+ // 解析 .gitignore
20
+ try {
21
+ const gitignorePath = path.join(modPath, '.gitignore');
22
+ const content = fs.readFileSync(gitignorePath, 'utf8');
23
+ content.split('\n').forEach(line => {
24
+ line = line.trim();
25
+ if (line && !line.startsWith('#')) {
26
+ const negate = line.startsWith('!');
27
+ if (negate) line = line.slice(1);
28
+ patterns.push({ pattern: line, negate });
29
+ }
30
+ });
31
+ } catch {}
32
+
33
+ return patterns;
34
+ }
35
+
36
+ function shouldIgnore(filePath, basePath, patterns) {
37
+ const relPath = path.relative(basePath, filePath);
38
+ const parts = relPath.split(path.sep);
39
+ const name = path.basename(filePath);
40
+
41
+ let ignored = false;
42
+ for (const {pattern, negate} of patterns) {
43
+ let match = false;
44
+ const cleanPattern = pattern.replace(/\/$/, '');
45
+
46
+ if (cleanPattern.includes('*')) {
47
+ // 通配符 → 正则:先转义特殊字符,再将 \* 还原为 [^/]*
48
+ const escaped = cleanPattern.replace(/[.+^${}()|[\]\\]/g, '\\$&').replace(/\*/g, '[^/]*');
49
+ const regex = new RegExp('^' + escaped + '$');
50
+ match = regex.test(name) || parts.some(p => regex.test(p));
51
+ } else if (cleanPattern.includes('/')) {
52
+ // 路径匹配:必须从头匹配或完整段匹配
53
+ match = relPath === cleanPattern || relPath.startsWith(cleanPattern + '/');
54
+ } else {
55
+ // 目录/文件名精确匹配
56
+ match = name === cleanPattern || parts.includes(cleanPattern);
57
+ }
58
+
59
+ if (match) ignored = !negate;
60
+ }
61
+ return ignored;
62
+ }
63
+
64
+ function rglob(dir, filter, basePath = dir) {
65
+ const patterns = parseGitignore(basePath);
13
66
  const results = [];
67
+
14
68
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
15
69
  const full = path.join(dir, entry.name);
70
+
71
+ if (shouldIgnore(full, basePath, patterns)) continue;
72
+
16
73
  if (entry.isDirectory()) {
17
- results.push(...rglob(full, filter));
74
+ results.push(...rglob(full, filter, basePath));
18
75
  } else if (!filter || filter(entry.name, full)) {
19
76
  results.push(full);
20
77
  }
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  "use strict";
3
3
 
4
- const { execSync } = require("child_process");
4
+ const { execFileSync } = require("child_process");
5
5
  const path = require("path");
6
6
  const fs = require("fs");
7
7
  const { parseCliArgs, buildReport, hasFatal, DASH } = require(path.join(__dirname, '..', '..', 'lib', 'shared.js'));
@@ -61,20 +61,20 @@ function parsePorcelainLine(line) {
61
61
  return c;
62
62
  }
63
63
 
64
- function git(args) {
65
- try { return execSync('git ' + args, { encoding: "utf8", stdio: ["pipe","pipe","pipe"] }); }
64
+ function git(...args) {
65
+ try { return execFileSync('git', args, { encoding: "utf8", stdio: ["pipe","pipe","pipe"] }); }
66
66
  catch { return ""; }
67
67
  }
68
68
 
69
69
  function getGitChanges(base = "HEAD~1", target = "HEAD") {
70
70
  const changes = [];
71
- for (const line of git(`diff --name-status ${base} ${target}`).split("\n")) {
71
+ for (const line of git('diff', '--name-status', base, target).split("\n")) {
72
72
  if (!line) continue;
73
73
  const c = parseNameStatusLine(line);
74
74
  if (c) changes.push(c);
75
75
  }
76
76
  const statMap = {};
77
- for (const line of git(`diff --numstat ${base} ${target}`).split("\n")) {
77
+ for (const line of git('diff', '--numstat', base, target).split("\n")) {
78
78
  if (!line) continue;
79
79
  const parts = line.split("\t");
80
80
  if (parts.length >= 3) {
@@ -92,7 +92,7 @@ function getGitChanges(base = "HEAD~1", target = "HEAD") {
92
92
 
93
93
  function getStagedChanges() {
94
94
  const changes = [];
95
- for (const line of git("diff --cached --name-status").split("\n")) {
95
+ for (const line of git('diff', '--cached', '--name-status').split("\n")) {
96
96
  if (!line) continue;
97
97
  const c = parseNameStatusLine(line);
98
98
  if (c) changes.push(c);
@@ -102,7 +102,7 @@ function getStagedChanges() {
102
102
 
103
103
  function getWorkingChanges() {
104
104
  const changes = [];
105
- for (const line of git("status --porcelain").split("\n")) {
105
+ for (const line of git('status', '--porcelain').split("\n")) {
106
106
  if (!line) continue;
107
107
  const c = parsePorcelainLine(line);
108
108
  if (c) changes.push(c);