code-abyss 1.7.2 → 1.7.4
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/README.md +51 -2
- package/bin/adapters/claude.js +150 -0
- package/bin/adapters/codex.js +97 -0
- package/bin/install.js +170 -158
- package/bin/lib/utils.js +31 -5
- package/config/CLAUDE.md +10 -10
- package/config/codex-config.example.toml +18 -24
- package/package.json +1 -1
- package/skills/SKILL.md +1 -1
- package/skills/run_skill.js +16 -26
- package/skills/tools/gen-docs/scripts/doc_generator.js +77 -5
- package/skills/tools/verify-change/scripts/change_analyzer.js +7 -7
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ npx code-abyss
|
|
|
25
25
|
交互式菜单(方向键选择,回车确认):
|
|
26
26
|
|
|
27
27
|
```
|
|
28
|
-
☠️ Code Abyss v1.7.
|
|
28
|
+
☠️ Code Abyss v1.7.3
|
|
29
29
|
|
|
30
30
|
? 请选择操作 (Use arrow keys)
|
|
31
31
|
❯ 安装到 Claude Code (~/.claude/)
|
|
@@ -101,7 +101,7 @@ Code Abyss 是一套 **Claude Code / Codex CLI 个性化配置包**,一条命
|
|
|
101
101
|
- 🔥 **邪修人格** — 宿命压迫叙事 + 道语标签 + 渡劫协议
|
|
102
102
|
- ⚔️ **安全工程知识体系** — 红队/蓝队/紫队三脉道统,11 领域 56 篇专业秘典
|
|
103
103
|
- ⚖️ **5 个校验关卡** — 安全扫描、模块完整性、变更分析、代码质量、文档生成
|
|
104
|
-
- ✅
|
|
104
|
+
- ✅ **单元测试覆盖** — Jest 框架,GitHub Actions CI (Node 18/20/22)
|
|
105
105
|
- ⚡ **三级授权** — T1/T2/T3 分级,零确认直接执行
|
|
106
106
|
|
|
107
107
|
---
|
|
@@ -154,6 +154,8 @@ Code Abyss 是一套 **Claude Code / Codex CLI 个性化配置包**,一条命
|
|
|
154
154
|
|
|
155
155
|
## ⚙️ 推荐配置
|
|
156
156
|
|
|
157
|
+
### Claude `settings.json` 推荐模板
|
|
158
|
+
|
|
157
159
|
安装时选择「精细合并」会自动写入,也可手动参考 [`config/settings.example.json`](config/settings.example.json):
|
|
158
160
|
|
|
159
161
|
```json
|
|
@@ -183,6 +185,53 @@ Code Abyss 是一套 **Claude Code / Codex CLI 个性化配置包**,一条命
|
|
|
183
185
|
|
|
184
186
|
---
|
|
185
187
|
|
|
188
|
+
### Codex `config.toml` 推荐模板
|
|
189
|
+
|
|
190
|
+
安装 `--target codex`(尤其 `-y`)时会写入以下模板到 `~/.codex/config.toml`:
|
|
191
|
+
|
|
192
|
+
```toml
|
|
193
|
+
model_provider = "custom"
|
|
194
|
+
model = "gpt-5.2-codex"
|
|
195
|
+
model_reasoning_effort = "high"
|
|
196
|
+
approval_policy = "never"
|
|
197
|
+
sandbox_mode = "danger-full-access"
|
|
198
|
+
disable_response_storage = true
|
|
199
|
+
|
|
200
|
+
[model_providers.custom]
|
|
201
|
+
name = "custom"
|
|
202
|
+
base_url = "https://your-api-endpoint.com/v1"
|
|
203
|
+
wire_api = "responses"
|
|
204
|
+
requires_openai_auth = true
|
|
205
|
+
|
|
206
|
+
[tools]
|
|
207
|
+
web_search = true
|
|
208
|
+
|
|
209
|
+
[features]
|
|
210
|
+
multi_agent = true
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
### 兼容性说明
|
|
215
|
+
|
|
216
|
+
- 模板已对齐新版 Codex 配置风格:`[tools].web_search` 与 `[features].multi_agent`
|
|
217
|
+
- 若你本地已有旧配置,安装器不会强制覆盖已有 `~/.codex/config.toml`
|
|
218
|
+
- 建议在升级后执行一次 `codex --help` / 启动自检,确认无 deprecation warning
|
|
219
|
+
|
|
220
|
+
---
|
|
221
|
+
|
|
222
|
+
## 🧩 适配器解耦(Claude / Codex)
|
|
223
|
+
|
|
224
|
+
为避免过度耦合,安装器按目标 CLI 拆分适配层:
|
|
225
|
+
|
|
226
|
+
- `bin/install.js`:保留通用编排(参数解析、安装/卸载流程、备份恢复)
|
|
227
|
+
- `bin/adapters/claude.js`:Claude 侧认证检测、settings merge、可选配置流程
|
|
228
|
+
- `bin/lib/ccline.js`:Claude 侧状态栏与 ccline 集成
|
|
229
|
+
- `bin/adapters/codex.js`:Codex 侧认证检测、核心文件映射、config 模板流程
|
|
230
|
+
|
|
231
|
+
当前 Claude/Codex 安装映射分别由 `getClaudeCoreFiles()` 与 `getCodexCoreFiles()` 提供,避免在主流程硬编码目标细节。
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
186
235
|
## 🎯 授权分级
|
|
187
236
|
|
|
188
237
|
| 级别 | 范围 | 行为 |
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
const SETTINGS_TEMPLATE = {
|
|
7
|
+
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
8
|
+
env: {
|
|
9
|
+
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
|
|
10
|
+
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
|
|
11
|
+
},
|
|
12
|
+
alwaysThinkingEnabled: true,
|
|
13
|
+
model: 'opus',
|
|
14
|
+
outputStyle: 'abyss-cultivator',
|
|
15
|
+
attribution: { commit: '', pr: '' },
|
|
16
|
+
permissions: {
|
|
17
|
+
allow: [
|
|
18
|
+
'Bash', 'LS', 'Read', 'Agent', 'Write', 'Edit', 'MultiEdit',
|
|
19
|
+
'Glob', 'Grep', 'WebFetch', 'WebSearch', 'TodoWrite',
|
|
20
|
+
'NotebookRead', 'NotebookEdit'
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const CCLINE_CMD = process.platform === 'win32' ? 'ccline' : '~/.claude/ccline/ccline';
|
|
26
|
+
const CCLINE_STATUS_LINE = {
|
|
27
|
+
statusLine: {
|
|
28
|
+
type: 'command',
|
|
29
|
+
command: CCLINE_CMD,
|
|
30
|
+
padding: 0
|
|
31
|
+
}
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
function getClaudeCoreFiles() {
|
|
35
|
+
return [
|
|
36
|
+
{ src: 'config/CLAUDE.md', dest: 'CLAUDE.md' },
|
|
37
|
+
{ src: 'output-styles', dest: 'output-styles' },
|
|
38
|
+
{ src: 'skills', dest: 'skills' },
|
|
39
|
+
];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function detectClaudeAuth({
|
|
43
|
+
settings = {},
|
|
44
|
+
HOME,
|
|
45
|
+
env = process.env,
|
|
46
|
+
warn = () => {}
|
|
47
|
+
}) {
|
|
48
|
+
const settingsEnv = settings.env || {};
|
|
49
|
+
if (settingsEnv.ANTHROPIC_BASE_URL && settingsEnv.ANTHROPIC_AUTH_TOKEN) {
|
|
50
|
+
return { type: 'custom', detail: settingsEnv.ANTHROPIC_BASE_URL };
|
|
51
|
+
}
|
|
52
|
+
if (env.ANTHROPIC_API_KEY) return { type: 'env', detail: 'ANTHROPIC_API_KEY' };
|
|
53
|
+
if (env.ANTHROPIC_BASE_URL && env.ANTHROPIC_AUTH_TOKEN) {
|
|
54
|
+
return { type: 'env-custom', detail: env.ANTHROPIC_BASE_URL };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const cred = path.join(HOME, '.claude', '.credentials.json');
|
|
58
|
+
if (fs.existsSync(cred)) {
|
|
59
|
+
try {
|
|
60
|
+
const cc = JSON.parse(fs.readFileSync(cred, 'utf8'));
|
|
61
|
+
if (cc.claudeAiOauth || cc.apiKey) return { type: 'login', detail: 'claude login' };
|
|
62
|
+
} catch (e) {
|
|
63
|
+
warn(`凭证文件损坏: ${cred}`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function configureCustomProvider(ctx, { ok }) {
|
|
71
|
+
const { confirm, input } = await import('@inquirer/prompts');
|
|
72
|
+
const doCfg = await confirm({ message: '配置自定义 provider?', default: false });
|
|
73
|
+
if (!doCfg) return;
|
|
74
|
+
|
|
75
|
+
if (!ctx.settings.env) ctx.settings.env = {};
|
|
76
|
+
const url = await input({ message: 'ANTHROPIC_BASE_URL:' });
|
|
77
|
+
const token = await input({ message: 'ANTHROPIC_AUTH_TOKEN:' });
|
|
78
|
+
if (url) ctx.settings.env.ANTHROPIC_BASE_URL = url;
|
|
79
|
+
if (token) ctx.settings.env.ANTHROPIC_AUTH_TOKEN = token;
|
|
80
|
+
|
|
81
|
+
fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
|
|
82
|
+
ok('provider 已配置');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function mergeSettings(ctx, { deepMergeNew, printMergeLog, c, ok }) {
|
|
86
|
+
const log = [];
|
|
87
|
+
deepMergeNew(ctx.settings, SETTINGS_TEMPLATE, '', log);
|
|
88
|
+
printMergeLog(log, c);
|
|
89
|
+
fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
|
|
90
|
+
ok('settings.json 合并完成');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async function postClaude({
|
|
94
|
+
ctx,
|
|
95
|
+
autoYes,
|
|
96
|
+
HOME,
|
|
97
|
+
PKG_ROOT,
|
|
98
|
+
step,
|
|
99
|
+
ok,
|
|
100
|
+
warn,
|
|
101
|
+
info,
|
|
102
|
+
c,
|
|
103
|
+
deepMergeNew,
|
|
104
|
+
printMergeLog,
|
|
105
|
+
installCcline
|
|
106
|
+
}) {
|
|
107
|
+
step(2, 3, '认证检测');
|
|
108
|
+
const auth = detectClaudeAuth({ settings: ctx.settings, HOME, warn });
|
|
109
|
+
if (auth) {
|
|
110
|
+
ok(`${c.b(auth.type)} → ${auth.detail}`);
|
|
111
|
+
} else {
|
|
112
|
+
warn('未检测到 API 认证');
|
|
113
|
+
info(`支持: ${c.cyn('claude login')} | ${c.cyn('ANTHROPIC_API_KEY')} | ${c.cyn('自定义 provider')}`);
|
|
114
|
+
if (!autoYes) await configureCustomProvider(ctx, { ok });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
step(3, 3, '可选配置');
|
|
118
|
+
if (autoYes) {
|
|
119
|
+
info('自动模式: 合并推荐配置');
|
|
120
|
+
mergeSettings(ctx, { deepMergeNew, printMergeLog, c, ok });
|
|
121
|
+
await installCcline(ctx, { HOME, PKG_ROOT, CCLINE_STATUS_LINE, ok, warn, info, c });
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const { checkbox } = await import('@inquirer/prompts');
|
|
126
|
+
const choices = await checkbox({
|
|
127
|
+
message: '选择要安装的配置 (空格选择, 回车确认)',
|
|
128
|
+
choices: [
|
|
129
|
+
{ name: '精细合并推荐 settings.json (保留现有配置)', value: 'settings', checked: true },
|
|
130
|
+
{ name: '安装 ccline 状态栏 (需要 Nerd Font)', value: 'ccline', checked: true },
|
|
131
|
+
],
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
if (choices.includes('settings')) {
|
|
135
|
+
mergeSettings(ctx, { deepMergeNew, printMergeLog, c, ok });
|
|
136
|
+
}
|
|
137
|
+
if (choices.includes('ccline')) {
|
|
138
|
+
await installCcline(ctx, { HOME, PKG_ROOT, CCLINE_STATUS_LINE, ok, warn, info, c });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
module.exports = {
|
|
143
|
+
SETTINGS_TEMPLATE,
|
|
144
|
+
CCLINE_STATUS_LINE,
|
|
145
|
+
getClaudeCoreFiles,
|
|
146
|
+
detectClaudeAuth,
|
|
147
|
+
configureCustomProvider,
|
|
148
|
+
mergeSettings,
|
|
149
|
+
postClaude,
|
|
150
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
function detectCodexAuth({
|
|
7
|
+
HOME,
|
|
8
|
+
env = process.env,
|
|
9
|
+
warn = () => {}
|
|
10
|
+
}) {
|
|
11
|
+
if (env.OPENAI_API_KEY) return { type: 'env', detail: 'OPENAI_API_KEY' };
|
|
12
|
+
|
|
13
|
+
const auth = path.join(HOME, '.codex', 'auth.json');
|
|
14
|
+
if (fs.existsSync(auth)) {
|
|
15
|
+
try {
|
|
16
|
+
const a = JSON.parse(fs.readFileSync(auth, 'utf8'));
|
|
17
|
+
if (a.token || a.api_key) return { type: 'login', detail: 'codex login' };
|
|
18
|
+
} catch (e) {
|
|
19
|
+
warn(`凭证文件损坏: ${auth}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const cfg = path.join(HOME, '.codex', 'config.toml');
|
|
24
|
+
if (fs.existsSync(cfg) && fs.readFileSync(cfg, 'utf8').includes('base_url')) {
|
|
25
|
+
return { type: 'custom', detail: 'config.toml' };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function getCodexCoreFiles() {
|
|
32
|
+
return [
|
|
33
|
+
{ src: 'config/AGENTS.md', dest: 'AGENTS.md' },
|
|
34
|
+
{ src: 'skills', dest: 'skills' },
|
|
35
|
+
];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function postCodex({
|
|
39
|
+
autoYes,
|
|
40
|
+
HOME,
|
|
41
|
+
PKG_ROOT,
|
|
42
|
+
step,
|
|
43
|
+
ok,
|
|
44
|
+
warn,
|
|
45
|
+
info,
|
|
46
|
+
c
|
|
47
|
+
}) {
|
|
48
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
49
|
+
const cfgPath = path.join(HOME, '.codex', 'config.toml');
|
|
50
|
+
const exists = fs.existsSync(cfgPath);
|
|
51
|
+
|
|
52
|
+
step(2, 3, '认证检测');
|
|
53
|
+
const auth = detectCodexAuth({ HOME, warn });
|
|
54
|
+
if (auth) {
|
|
55
|
+
ok(`${c.b(auth.type)} → ${auth.detail}`);
|
|
56
|
+
} else {
|
|
57
|
+
warn('未检测到 API 认证');
|
|
58
|
+
info(`支持: ${c.cyn('codex login')} | ${c.cyn('OPENAI_API_KEY')} | ${c.cyn('自定义 provider')}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
step(3, 3, '可选配置');
|
|
62
|
+
if (autoYes) {
|
|
63
|
+
if (!exists) {
|
|
64
|
+
const src = path.join(PKG_ROOT, 'config', 'codex-config.example.toml');
|
|
65
|
+
if (fs.existsSync(src)) {
|
|
66
|
+
fs.copyFileSync(src, cfgPath);
|
|
67
|
+
ok('写入: ~/.codex/config.toml (模板)');
|
|
68
|
+
warn('请编辑 base_url 和 model');
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
ok('config.toml 已存在');
|
|
72
|
+
}
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (!exists) {
|
|
77
|
+
warn('未检测到 ~/.codex/config.toml');
|
|
78
|
+
const doWrite = await confirm({ message: '写入推荐 config.toml (含自定义 provider 模板)?', default: true });
|
|
79
|
+
if (doWrite) {
|
|
80
|
+
const src = path.join(PKG_ROOT, 'config', 'codex-config.example.toml');
|
|
81
|
+
if (fs.existsSync(src)) {
|
|
82
|
+
fs.copyFileSync(src, cfgPath);
|
|
83
|
+
ok('写入: ~/.codex/config.toml');
|
|
84
|
+
warn('请编辑 base_url 和 model');
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
} else {
|
|
88
|
+
ok('config.toml 已存在');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
detectCodexAuth,
|
|
94
|
+
getCodexCoreFiles,
|
|
95
|
+
postCodex,
|
|
96
|
+
};
|
|
97
|
+
|
package/bin/install.js
CHANGED
|
@@ -15,9 +15,20 @@ 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
|
+
const {
|
|
22
|
+
detectCodexAuth: detectCodexAuthImpl,
|
|
23
|
+
getCodexCoreFiles,
|
|
24
|
+
postCodex: postCodexFlow,
|
|
25
|
+
} = require(path.join(__dirname, 'adapters', 'codex.js'));
|
|
26
|
+
const {
|
|
27
|
+
SETTINGS_TEMPLATE,
|
|
28
|
+
getClaudeCoreFiles,
|
|
29
|
+
detectClaudeAuth: detectClaudeAuthImpl,
|
|
30
|
+
postClaude: postClaudeFlow,
|
|
31
|
+
} = require(path.join(__dirname, 'adapters', 'claude.js'));
|
|
21
32
|
|
|
22
33
|
// ── ANSI ──
|
|
23
34
|
|
|
@@ -66,68 +77,15 @@ function fail(msg) { console.log(` ${c.red('✘')} ${msg}`); }
|
|
|
66
77
|
// ── 认证 ──
|
|
67
78
|
|
|
68
79
|
function detectClaudeAuth(settings) {
|
|
69
|
-
|
|
70
|
-
if (env.ANTHROPIC_BASE_URL && env.ANTHROPIC_AUTH_TOKEN) return { type: 'custom', detail: env.ANTHROPIC_BASE_URL };
|
|
71
|
-
if (process.env.ANTHROPIC_API_KEY) return { type: 'env', detail: 'ANTHROPIC_API_KEY' };
|
|
72
|
-
if (process.env.ANTHROPIC_BASE_URL && process.env.ANTHROPIC_AUTH_TOKEN) {
|
|
73
|
-
return { type: 'env-custom', detail: process.env.ANTHROPIC_BASE_URL };
|
|
74
|
-
}
|
|
75
|
-
const cred = path.join(HOME, '.claude', '.credentials.json');
|
|
76
|
-
if (fs.existsSync(cred)) {
|
|
77
|
-
try {
|
|
78
|
-
const cc = JSON.parse(fs.readFileSync(cred, 'utf8'));
|
|
79
|
-
if (cc.claudeAiOauth || cc.apiKey) return { type: 'login', detail: 'claude login' };
|
|
80
|
-
} catch (e) { warn(`凭证文件损坏: ${cred}`); }
|
|
81
|
-
}
|
|
82
|
-
return null;
|
|
80
|
+
return detectClaudeAuthImpl({ settings, HOME, warn });
|
|
83
81
|
}
|
|
84
82
|
|
|
85
83
|
function detectCodexAuth() {
|
|
86
|
-
|
|
87
|
-
const auth = path.join(HOME, '.codex', 'auth.json');
|
|
88
|
-
if (fs.existsSync(auth)) {
|
|
89
|
-
try {
|
|
90
|
-
const a = JSON.parse(fs.readFileSync(auth, 'utf8'));
|
|
91
|
-
if (a.token || a.api_key) return { type: 'login', detail: 'codex login' };
|
|
92
|
-
} catch (e) { warn(`凭证文件损坏: ${auth}`); }
|
|
93
|
-
}
|
|
94
|
-
const cfg = path.join(HOME, '.codex', 'config.toml');
|
|
95
|
-
if (fs.existsSync(cfg)) {
|
|
96
|
-
if (fs.readFileSync(cfg, 'utf8').includes('base_url')) return { type: 'custom', detail: 'config.toml' };
|
|
97
|
-
}
|
|
98
|
-
return null;
|
|
84
|
+
return detectCodexAuthImpl({ HOME, warn });
|
|
99
85
|
}
|
|
100
86
|
|
|
101
87
|
// ── 模板 ──
|
|
102
88
|
|
|
103
|
-
const SETTINGS_TEMPLATE = {
|
|
104
|
-
$schema: 'https://json.schemastore.org/claude-code-settings.json',
|
|
105
|
-
env: {
|
|
106
|
-
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1',
|
|
107
|
-
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1'
|
|
108
|
-
},
|
|
109
|
-
alwaysThinkingEnabled: true,
|
|
110
|
-
model: 'opus',
|
|
111
|
-
outputStyle: 'abyss-cultivator',
|
|
112
|
-
attribution: { commit: '', pr: '' },
|
|
113
|
-
permissions: {
|
|
114
|
-
allow: [
|
|
115
|
-
'Bash', 'LS', 'Read', 'Agent', 'Write', 'Edit', 'MultiEdit',
|
|
116
|
-
'Glob', 'Grep', 'WebFetch', 'WebSearch', 'TodoWrite',
|
|
117
|
-
'NotebookRead', 'NotebookEdit'
|
|
118
|
-
]
|
|
119
|
-
}
|
|
120
|
-
};
|
|
121
|
-
|
|
122
|
-
const CCLINE_CMD = process.platform === 'win32' ? 'ccline' : '~/.claude/ccline/ccline';
|
|
123
|
-
const CCLINE_STATUS_LINE = {
|
|
124
|
-
statusLine: {
|
|
125
|
-
type: 'command',
|
|
126
|
-
command: CCLINE_CMD,
|
|
127
|
-
padding: 0
|
|
128
|
-
}
|
|
129
|
-
};
|
|
130
|
-
|
|
131
89
|
// ── CLI 参数 ──
|
|
132
90
|
|
|
133
91
|
const args = process.argv.slice(2);
|
|
@@ -193,6 +151,127 @@ function runUninstall(tgt) {
|
|
|
193
151
|
|
|
194
152
|
// ── 安装核心 ──
|
|
195
153
|
|
|
154
|
+
/**
|
|
155
|
+
* 递归扫描 skills 目录,找出所有 user-invocable: true 的 SKILL.md
|
|
156
|
+
* @param {string} skillsDir - skills 源目录绝对路径
|
|
157
|
+
* @returns {Array<{meta: Object, relPath: string, hasScripts: boolean}>}
|
|
158
|
+
*/
|
|
159
|
+
function scanInvocableSkills(skillsDir) {
|
|
160
|
+
const results = [];
|
|
161
|
+
function scan(dir) {
|
|
162
|
+
const skillMd = path.join(dir, 'SKILL.md');
|
|
163
|
+
if (fs.existsSync(skillMd)) {
|
|
164
|
+
try {
|
|
165
|
+
const content = fs.readFileSync(skillMd, 'utf8');
|
|
166
|
+
const meta = parseFrontmatter(content);
|
|
167
|
+
if (meta && meta['user-invocable'] === 'true' && meta.name) {
|
|
168
|
+
const relPath = path.relative(skillsDir, dir);
|
|
169
|
+
const scriptsDir = path.join(dir, 'scripts');
|
|
170
|
+
const hasScripts = fs.existsSync(scriptsDir) &&
|
|
171
|
+
fs.readdirSync(scriptsDir).some(f => f.endsWith('.js'));
|
|
172
|
+
results.push({ meta, relPath, hasScripts });
|
|
173
|
+
}
|
|
174
|
+
} catch (e) { /* 解析失败跳过 */ }
|
|
175
|
+
}
|
|
176
|
+
try {
|
|
177
|
+
fs.readdirSync(dir).forEach(sub => {
|
|
178
|
+
const subPath = path.join(dir, sub);
|
|
179
|
+
if (fs.statSync(subPath).isDirectory() && !shouldSkip(sub) && sub !== 'scripts') {
|
|
180
|
+
scan(subPath);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
} catch (e) { /* 读取失败跳过 */ }
|
|
184
|
+
}
|
|
185
|
+
scan(skillsDir);
|
|
186
|
+
return results;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* 根据 SKILL.md 元数据生成 command .md 内容
|
|
191
|
+
*
|
|
192
|
+
* 设计原则:
|
|
193
|
+
* - 读取 SKILL.md + 执行脚本合并为一气呵成的指令流
|
|
194
|
+
* - 禁止「先…然后…」的分步模式,避免 Claude 在步骤间停顿
|
|
195
|
+
* - 无脚本的 skill:仅读取 SKILL.md 作为知识库提供指导
|
|
196
|
+
*
|
|
197
|
+
* @param {Object} meta - parseFrontmatter 返回的元数据
|
|
198
|
+
* @param {string} skillRelPath - 相对于 skills/ 的路径(如 'tools/gen-docs')
|
|
199
|
+
* @param {boolean} hasScripts - 是否有可执行脚本
|
|
200
|
+
* @returns {string} command .md 文件内容
|
|
201
|
+
*/
|
|
202
|
+
function generateCommandContent(meta, skillRelPath, hasScripts) {
|
|
203
|
+
const name = meta.name;
|
|
204
|
+
const desc = (meta.description || '').replace(/"/g, '\\"');
|
|
205
|
+
const argHint = meta['argument-hint'];
|
|
206
|
+
const tools = meta['allowed-tools'] || 'Read';
|
|
207
|
+
const skillPath = skillRelPath
|
|
208
|
+
? `~/.claude/skills/${skillRelPath}/SKILL.md`
|
|
209
|
+
: '~/.claude/skills/SKILL.md';
|
|
210
|
+
|
|
211
|
+
const lines = [
|
|
212
|
+
'---',
|
|
213
|
+
`name: ${name}`,
|
|
214
|
+
`description: "${desc}"`,
|
|
215
|
+
];
|
|
216
|
+
if (argHint) lines.push(`argument-hint: "${argHint}"`);
|
|
217
|
+
lines.push(`allowed-tools: ${tools}`);
|
|
218
|
+
lines.push('---');
|
|
219
|
+
lines.push('');
|
|
220
|
+
|
|
221
|
+
if (hasScripts) {
|
|
222
|
+
// ── 有脚本的 skill:读取规范 + 执行脚本,一气呵成 ──
|
|
223
|
+
lines.push('以下所有步骤一气呵成,不要在步骤间停顿等待用户输入:');
|
|
224
|
+
lines.push('');
|
|
225
|
+
lines.push(`1. 读取规范:${skillPath}`);
|
|
226
|
+
lines.push(`2. 执行命令:\`node ~/.claude/skills/run_skill.js ${name} $ARGUMENTS\``);
|
|
227
|
+
lines.push('3. 按规范分析输出,完成后续动作');
|
|
228
|
+
lines.push('');
|
|
229
|
+
lines.push('全程不要停顿,不要询问是否继续。');
|
|
230
|
+
} else {
|
|
231
|
+
// ── 无脚本的 skill:知识库模式 ──
|
|
232
|
+
lines.push('读取以下秘典,根据内容为用户提供专业指导:');
|
|
233
|
+
lines.push('');
|
|
234
|
+
lines.push('```');
|
|
235
|
+
lines.push(skillPath);
|
|
236
|
+
lines.push('```');
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
lines.push('');
|
|
240
|
+
return lines.join('\n');
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* 扫描 skills 并为 user-invocable 的 skill 生成 command 包装,文件级合并安装
|
|
245
|
+
*/
|
|
246
|
+
function installGeneratedCommands(skillsSrcDir, targetDir, backupDir, manifest) {
|
|
247
|
+
const skills = scanInvocableSkills(skillsSrcDir);
|
|
248
|
+
if (skills.length === 0) return 0;
|
|
249
|
+
|
|
250
|
+
const cmdsDir = path.join(targetDir, 'commands');
|
|
251
|
+
fs.mkdirSync(cmdsDir, { recursive: true });
|
|
252
|
+
|
|
253
|
+
skills.forEach(({ meta, relPath, hasScripts }) => {
|
|
254
|
+
const fileName = `${meta.name}.md`;
|
|
255
|
+
const destFile = path.join(cmdsDir, fileName);
|
|
256
|
+
const relFile = path.posix.join('commands', fileName);
|
|
257
|
+
|
|
258
|
+
if (fs.existsSync(destFile)) {
|
|
259
|
+
const cmdsBackupDir = path.join(backupDir, 'commands');
|
|
260
|
+
fs.mkdirSync(cmdsBackupDir, { recursive: true });
|
|
261
|
+
fs.copyFileSync(destFile, path.join(cmdsBackupDir, fileName));
|
|
262
|
+
manifest.backups.push(relFile);
|
|
263
|
+
info(`备份: ${c.d(relFile)}`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const content = generateCommandContent(meta, relPath, hasScripts);
|
|
267
|
+
fs.writeFileSync(destFile, content);
|
|
268
|
+
manifest.installed.push(relFile);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
ok(`commands/ ${c.d(`(自动生成 ${skills.length} 个斜杠命令)`)}`);
|
|
272
|
+
return skills.length;
|
|
273
|
+
}
|
|
274
|
+
|
|
196
275
|
function installCore(tgt) {
|
|
197
276
|
const targetDir = path.join(HOME, `.${tgt}`);
|
|
198
277
|
const backupDir = path.join(targetDir, '.sage-backup');
|
|
@@ -201,12 +280,9 @@ function installCore(tgt) {
|
|
|
201
280
|
step(1, 3, `安装核心文件 → ${c.cyn(targetDir)}`);
|
|
202
281
|
fs.mkdirSync(backupDir, { recursive: true });
|
|
203
282
|
|
|
204
|
-
const filesToInstall =
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
{ src: 'output-styles', dest: tgt === 'claude' ? 'output-styles' : null },
|
|
208
|
-
{ src: 'skills', dest: 'skills' }
|
|
209
|
-
].filter(f => f.dest !== null);
|
|
283
|
+
const filesToInstall = tgt === 'codex'
|
|
284
|
+
? getCodexCoreFiles()
|
|
285
|
+
: getClaudeCoreFiles();
|
|
210
286
|
|
|
211
287
|
const manifest = {
|
|
212
288
|
manifest_version: 1, version: VERSION, target: tgt,
|
|
@@ -223,6 +299,7 @@ function installCore(tgt) {
|
|
|
223
299
|
}
|
|
224
300
|
warn(`跳过: ${src}`); return;
|
|
225
301
|
}
|
|
302
|
+
|
|
226
303
|
if (fs.existsSync(destPath)) {
|
|
227
304
|
const bp = path.join(backupDir, dest);
|
|
228
305
|
rmSafe(bp); copyRecursive(destPath, bp); manifest.backups.push(dest);
|
|
@@ -232,6 +309,12 @@ function installCore(tgt) {
|
|
|
232
309
|
rmSafe(destPath); copyRecursive(srcPath, destPath); manifest.installed.push(dest);
|
|
233
310
|
});
|
|
234
311
|
|
|
312
|
+
// 为 Claude 目标自动生成 user-invocable 斜杠命令
|
|
313
|
+
if (tgt === 'claude') {
|
|
314
|
+
const skillsSrc = path.join(PKG_ROOT, 'skills');
|
|
315
|
+
installGeneratedCommands(skillsSrc, targetDir, backupDir, manifest);
|
|
316
|
+
}
|
|
317
|
+
|
|
235
318
|
const settingsPath = path.join(targetDir, 'settings.json');
|
|
236
319
|
let settings = {};
|
|
237
320
|
if (fs.existsSync(settingsPath)) {
|
|
@@ -261,108 +344,36 @@ function installCore(tgt) {
|
|
|
261
344
|
|
|
262
345
|
// ── Claude 后续 ──
|
|
263
346
|
|
|
264
|
-
async function configureCustomProvider(ctx) {
|
|
265
|
-
const { confirm, input } = await import('@inquirer/prompts');
|
|
266
|
-
const doCfg = await confirm({ message: '配置自定义 provider?', default: false });
|
|
267
|
-
if (!doCfg) return;
|
|
268
|
-
if (!ctx.settings.env) ctx.settings.env = {};
|
|
269
|
-
const url = await input({ message: 'ANTHROPIC_BASE_URL:' });
|
|
270
|
-
const token = await input({ message: 'ANTHROPIC_AUTH_TOKEN:' });
|
|
271
|
-
if (url) ctx.settings.env.ANTHROPIC_BASE_URL = url;
|
|
272
|
-
if (token) ctx.settings.env.ANTHROPIC_AUTH_TOKEN = token;
|
|
273
|
-
fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
|
|
274
|
-
ok('provider 已配置');
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function mergeSettings(ctx) {
|
|
278
|
-
const log = [];
|
|
279
|
-
deepMergeNew(ctx.settings, SETTINGS_TEMPLATE, '', log);
|
|
280
|
-
printMergeLog(log, c);
|
|
281
|
-
fs.writeFileSync(ctx.settingsPath, JSON.stringify(ctx.settings, null, 2) + '\n');
|
|
282
|
-
ok('settings.json 合并完成');
|
|
283
|
-
}
|
|
284
|
-
|
|
285
347
|
async function postClaude(ctx) {
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
mergeSettings(ctx);
|
|
300
|
-
await installCcline(ctx);
|
|
301
|
-
return;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const { checkbox } = await import('@inquirer/prompts');
|
|
305
|
-
const choices = await checkbox({
|
|
306
|
-
message: '选择要安装的配置 (空格选择, 回车确认)',
|
|
307
|
-
choices: [
|
|
308
|
-
{ name: '精细合并推荐 settings.json (保留现有配置)', value: 'settings', checked: true },
|
|
309
|
-
{ name: '安装 ccline 状态栏 (需要 Nerd Font)', value: 'ccline', checked: true },
|
|
310
|
-
],
|
|
348
|
+
await postClaudeFlow({
|
|
349
|
+
ctx,
|
|
350
|
+
autoYes,
|
|
351
|
+
HOME,
|
|
352
|
+
PKG_ROOT,
|
|
353
|
+
step,
|
|
354
|
+
ok,
|
|
355
|
+
warn,
|
|
356
|
+
info,
|
|
357
|
+
c,
|
|
358
|
+
deepMergeNew,
|
|
359
|
+
printMergeLog,
|
|
360
|
+
installCcline: _installCcline,
|
|
311
361
|
});
|
|
312
|
-
|
|
313
|
-
if (choices.includes('settings')) mergeSettings(ctx);
|
|
314
|
-
if (choices.includes('ccline')) await installCcline(ctx);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
async function installCcline(ctx) {
|
|
318
|
-
await _installCcline(ctx, { HOME, PKG_ROOT, CCLINE_STATUS_LINE, ok, warn, info, fail, c });
|
|
319
362
|
}
|
|
320
363
|
|
|
321
364
|
// ── Codex 后续 ──
|
|
322
365
|
|
|
323
366
|
async function postCodex() {
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
info(`支持: ${c.cyn('codex login')} | ${c.cyn('OPENAI_API_KEY')} | ${c.cyn('自定义 provider')}`);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
step(3, 3, '可选配置');
|
|
338
|
-
if (autoYes) {
|
|
339
|
-
if (!exists) {
|
|
340
|
-
const src = path.join(PKG_ROOT, 'config', 'codex-config.example.toml');
|
|
341
|
-
if (fs.existsSync(src)) {
|
|
342
|
-
fs.copyFileSync(src, cfgPath);
|
|
343
|
-
ok('写入: ~/.codex/config.toml (模板)');
|
|
344
|
-
warn('请编辑 base_url 和 model');
|
|
345
|
-
}
|
|
346
|
-
} else {
|
|
347
|
-
ok('config.toml 已存在');
|
|
348
|
-
}
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
if (!exists) {
|
|
353
|
-
warn('未检测到 ~/.codex/config.toml');
|
|
354
|
-
const doWrite = await confirm({ message: '写入推荐 config.toml (含自定义 provider 模板)?', default: true });
|
|
355
|
-
if (doWrite) {
|
|
356
|
-
const src = path.join(PKG_ROOT, 'config', 'codex-config.example.toml');
|
|
357
|
-
if (fs.existsSync(src)) {
|
|
358
|
-
fs.copyFileSync(src, cfgPath);
|
|
359
|
-
ok('写入: ~/.codex/config.toml');
|
|
360
|
-
warn('请编辑 base_url 和 model');
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
} else {
|
|
364
|
-
ok('config.toml 已存在');
|
|
365
|
-
}
|
|
367
|
+
await postCodexFlow({
|
|
368
|
+
autoYes,
|
|
369
|
+
HOME,
|
|
370
|
+
PKG_ROOT,
|
|
371
|
+
step,
|
|
372
|
+
ok,
|
|
373
|
+
warn,
|
|
374
|
+
info,
|
|
375
|
+
c,
|
|
376
|
+
});
|
|
366
377
|
}
|
|
367
378
|
|
|
368
379
|
// ── 主流程 ──
|
|
@@ -426,5 +437,6 @@ if (require.main === module) {
|
|
|
426
437
|
|
|
427
438
|
module.exports = {
|
|
428
439
|
deepMergeNew, detectClaudeAuth, detectCodexAuth,
|
|
429
|
-
detectCclineBin, copyRecursive, shouldSkip, SETTINGS_TEMPLATE
|
|
440
|
+
detectCclineBin, copyRecursive, shouldSkip, SETTINGS_TEMPLATE,
|
|
441
|
+
scanInvocableSkills, generateCommandContent, installGeneratedCommands
|
|
430
442
|
};
|
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
|
-
|
|
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)
|
|
15
|
-
if (!shouldSkip(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
|
-
|
|
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
|
|
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
|
|
|
@@ -1,24 +1,18 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
web_search = "live"
|
|
20
|
-
|
|
21
|
-
[features]
|
|
22
|
-
unified_exec = true
|
|
23
|
-
shell_snapshot = true
|
|
24
|
-
steer = true
|
|
1
|
+
model_provider = "custom"
|
|
2
|
+
model = "gpt-5.2-codex"
|
|
3
|
+
model_reasoning_effort = "high"
|
|
4
|
+
approval_policy = "never"
|
|
5
|
+
sandbox_mode = "danger-full-access"
|
|
6
|
+
disable_response_storage = true
|
|
7
|
+
|
|
8
|
+
[model_providers.custom]
|
|
9
|
+
name = "custom"
|
|
10
|
+
base_url = "https://your-api-endpoint.com/v1"
|
|
11
|
+
wire_api = "responses"
|
|
12
|
+
requires_openai_auth = true
|
|
13
|
+
|
|
14
|
+
[tools]
|
|
15
|
+
web_search = true
|
|
16
|
+
|
|
17
|
+
[features]
|
|
18
|
+
multi_agent = true
|
package/package.json
CHANGED
package/skills/SKILL.md
CHANGED
package/skills/run_skill.js
CHANGED
|
@@ -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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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
|
|
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
|
}
|
|
@@ -346,7 +403,7 @@ function main() {
|
|
|
346
403
|
const result = generateDocs(args.path, args.force);
|
|
347
404
|
|
|
348
405
|
if (args.json) {
|
|
349
|
-
|
|
406
|
+
process.stdout.write(JSON.stringify(result, null, 2) + '\n');
|
|
350
407
|
} else {
|
|
351
408
|
console.log('='.repeat(50));
|
|
352
409
|
console.log('文档生成报告');
|
|
@@ -357,7 +414,22 @@ function main() {
|
|
|
357
414
|
console.log('='.repeat(50));
|
|
358
415
|
}
|
|
359
416
|
|
|
360
|
-
process.
|
|
417
|
+
process.exitCode = result.status === 'success' ? 0 : 1;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
if (require.main === module) {
|
|
421
|
+
main();
|
|
361
422
|
}
|
|
362
423
|
|
|
363
|
-
|
|
424
|
+
module.exports = {
|
|
425
|
+
parseGitignore,
|
|
426
|
+
shouldIgnore,
|
|
427
|
+
rglob,
|
|
428
|
+
detectLanguage,
|
|
429
|
+
analyzeModule,
|
|
430
|
+
generateReadme,
|
|
431
|
+
generateDesign,
|
|
432
|
+
generateDocs,
|
|
433
|
+
parseArgs,
|
|
434
|
+
main,
|
|
435
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
const {
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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);
|