@ultime/codecraft 1.2.0 → 1.3.0
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/CHANGELOG.md +14 -0
- package/README.md +31 -6
- package/VERSION +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +123 -7
- package/src/install.mjs +96 -24
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.3.0 - 2026-10-27
|
|
4
|
+
|
|
5
|
+
- 新增 agent 注册表:内置 generic / claude / cursor / trae / trae-cn,`--list-agents` 查看项目 / 全局路径与检测结果。
|
|
6
|
+
- 支持项目级 `codecraft.agents.json` 与用户级 `~/.codecraft/agents.json` 自定义 agent(project / global / markers),项目级覆盖用户级。
|
|
7
|
+
- `auto` 按注册表检测所有匹配的 agent,不优先任何一家;重复目标的 agent 自动合并,避免重复复制。
|
|
8
|
+
- 仍保留 `--dest` 作为临时目录逃生口。
|
|
9
|
+
|
|
10
|
+
## 1.2.1 - 2026-10-27
|
|
11
|
+
|
|
12
|
+
- 修复 pack 自动判定:识别 Nuxt 2 的 `pages/`、Nuxt 3 的 `app/`、uni-app 的 `manifest.json + pages.json`、Taro / uni-app 依赖,以及 workspace 内的嵌套应用。
|
|
13
|
+
- 新增 `trae-cn` 安装目标(`~/.trae-cn/skills`);`--agent` 顺序改为 generic / claude / cursor / trae / trae-cn,不再默认优先 Trae。
|
|
14
|
+
- 扫描 0 个源文件时输出警告并以 2 退出,避免把空扫描当成通过。
|
|
15
|
+
- README 补充 agent 列表、`--dest`、`Files: 0` 与 Trae CN 的说明。
|
|
16
|
+
|
|
3
17
|
## 1.2.0 - 2026-10-27
|
|
4
18
|
|
|
5
19
|
- 新增 `ignore` 例外配置(A12):`rule` / `file` / `reason` 必填,`expires` 可选;支持 `*` / `**` / `?` 路径匹配;安全规则 `A21` 不可豁免。
|
package/README.md
CHANGED
|
@@ -58,15 +58,37 @@ npx skills-npm setup
|
|
|
58
58
|
npx @ultime/codecraft install
|
|
59
59
|
```
|
|
60
60
|
|
|
61
|
-
自动检测当前项目的 agent,把 `SKILL.md`、`docs/`、`scenarios/`、`prompts/` 复制到 `.agents/skills/codecraft` 和对应 agent 目录(`.claude/skills`、`.trae/skills`、`.
|
|
61
|
+
自动检测当前项目的 agent,把 `SKILL.md`、`docs/`、`scenarios/`、`prompts/` 复制到 `.agents/skills/codecraft` 和对应 agent 目录(`.claude/skills`、`.cursor/skills`、`.trae/skills`、`.trae-cn/skills`)。不传 `--agent` 时只按检测结果安装,不优先任何一家。
|
|
62
62
|
|
|
63
63
|
```bash
|
|
64
|
-
npx @ultime/codecraft install --agent
|
|
65
|
-
npx @ultime/codecraft install --global --agent claude
|
|
66
|
-
npx @ultime/codecraft install --
|
|
67
|
-
npx @ultime/codecraft install --
|
|
64
|
+
npx @ultime/codecraft install --agent generic # 指定 agent,可逗号分隔
|
|
65
|
+
npx @ultime/codecraft install --global --agent claude # 装到用户目录
|
|
66
|
+
npx @ultime/codecraft install --global --agent trae-cn # Trae CN 的全局目录
|
|
67
|
+
npx @ultime/codecraft install --global --dest .trae-cn/skills # 自定义目录
|
|
68
|
+
npx @ultime/codecraft install --dry-run # 只看计划
|
|
69
|
+
npx @ultime/codecraft install --force # 覆盖已有目录
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
可选内置 agent:`generic` / `claude` / `cursor` / `trae` / `trae-cn`;用 `--list-agents` 查看实际路径与检测结果。
|
|
73
|
+
|
|
74
|
+
自定义 agent 放在项目级 `codecraft.agents.json` 或用户级 `~/.codecraft/agents.json`(项目级覆盖用户级):
|
|
75
|
+
|
|
76
|
+
```json
|
|
77
|
+
{
|
|
78
|
+
"agents": {
|
|
79
|
+
"my-agent": {
|
|
80
|
+
"project": ".my/skills",
|
|
81
|
+
"global": ".my/skills",
|
|
82
|
+
"markers": [".my"]
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- `project` / `global`:项目内与全局的目标目录;`global` 省略时复用 `project`。
|
|
89
|
+
- `markers`:`auto` 检测用的目录或文件;不填则只能显式 `--agent my-agent`。
|
|
90
|
+
- 未知或临时目录仍可用 `--dest`,不必先写配置。
|
|
91
|
+
|
|
70
92
|
### 方式 C:手动指针
|
|
71
93
|
|
|
72
94
|
不复制文件,只在项目规则里留一行:
|
|
@@ -264,9 +286,12 @@ ROADMAP.md 后续版本计划与交接说明
|
|
|
264
286
|
## FAQ
|
|
265
287
|
|
|
266
288
|
- **静态检查误报怎么办?** 在 `codecraft.config.json` 的 `ignore` 里登记 `rule` + `file` + `reason`,必要时加 `expires`;不要直接删规则。
|
|
289
|
+
- **`Files: 0` 是什么意思?** 没有扫到源文件,命令会输出警告并以 2 退出;用 `--pack app|library` 指定正确类型,或检查 `--root`。
|
|
290
|
+
- **Trae CN 装到哪?** 用 `--agent trae-cn`(`~/.trae-cn/skills`);未知目录用 `--dest`。
|
|
291
|
+
- **怎么支持新的 agent?** 在 `codecraft.agents.json` 里加 `project` / `global` / `markers`,再跑 `--list-agents` 确认;不需要等 Codecraft 发版。
|
|
267
292
|
- **`check` / `audit` / `plan` 有什么区别?** `check` 只跑内置规则;`audit` 额外跑 config 里的 gates;`plan` 只读,把 findings 转成计划,默认不跑 gates。
|
|
268
293
|
- **为什么不自动跑 typecheck / test?** 门禁由项目提供,Codecraft 只编排,避免第二套实现。
|
|
269
|
-
- **monorepo 为什么被判成 library?** 有 `
|
|
294
|
+
- **monorepo 为什么被判成 app / library?** 有 `pages/`、`app/`、`manifest.json + pages.json`、Taro / uni-app 依赖或嵌套应用时按 `app`;有 `packages/` 或 `src/` 时按 `library`;仍不确定就显式传 `--pack`。
|
|
270
295
|
- **支持哪些文件?** 只扫 JS/TS 相关扩展名与 `package.json`;其它语言不覆盖。
|
|
271
296
|
|
|
272
297
|
## 边界
|
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
1.
|
|
1
|
+
1.3.0
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -5,7 +5,7 @@ import { loadConfig, runGates } from './gates.mjs'
|
|
|
5
5
|
import { buildPlan, summarizePlan } from './plan.mjs'
|
|
6
6
|
import { applyInitPlan, buildInitPlan } from './init.mjs'
|
|
7
7
|
import { applyIgnores } from './ignore.mjs'
|
|
8
|
-
import { applyInstallPlan, buildInstallPlan, parseAgents } from './install.mjs'
|
|
8
|
+
import { applyInstallPlan, buildInstallPlan, listAgents, loadAgents, parseAgents } from './install.mjs'
|
|
9
9
|
|
|
10
10
|
const PACKAGE = JSON.parse(fs.readFileSync(new URL('../package.json', import.meta.url), 'utf8'))
|
|
11
11
|
const VERSION = PACKAGE.version
|
|
@@ -24,9 +24,10 @@ Options:
|
|
|
24
24
|
--root <path> Project root (default: cwd)
|
|
25
25
|
--pack app|library|design-system|auto Rule pack (default: auto)
|
|
26
26
|
--config <path> Config file (default: codecraft.config.json)
|
|
27
|
-
--agent
|
|
27
|
+
--agent <name>[,<name>] Install target (default: auto; see --list-agents)
|
|
28
|
+
--list-agents List registered agents, paths and detection
|
|
28
29
|
--global Install into the home directory
|
|
29
|
-
--dest <path> Explicit destination (relative to root / home)
|
|
30
|
+
--dest <path> Explicit destination (relative to root / home), e.g. .trae-cn/skills
|
|
30
31
|
--force Replace an existing codecraft skill directory
|
|
31
32
|
--dry-run Show the install plan without writing
|
|
32
33
|
--no-gates Skip gates in audit / plan
|
|
@@ -70,16 +71,33 @@ export function main(argv) {
|
|
|
70
71
|
return 2
|
|
71
72
|
}
|
|
72
73
|
|
|
74
|
+
if (args.listAgents) {
|
|
75
|
+
try {
|
|
76
|
+
const agents = listAgents(root, { global: args.global })
|
|
77
|
+
if (args.format === 'json') {
|
|
78
|
+
console.log(JSON.stringify({ version: VERSION, agents }, null, 2))
|
|
79
|
+
} else {
|
|
80
|
+
console.log(formatAgents(agents))
|
|
81
|
+
}
|
|
82
|
+
return 0
|
|
83
|
+
} catch (error) {
|
|
84
|
+
console.error(`codecraft: ${error.message}`)
|
|
85
|
+
return 2
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
73
89
|
if (command === 'install') {
|
|
74
90
|
let agents
|
|
91
|
+
let registry
|
|
75
92
|
try {
|
|
76
|
-
|
|
93
|
+
registry = loadAgents(root, { global: args.global })
|
|
94
|
+
agents = parseAgents(args.agent, [...registry.keys()])
|
|
77
95
|
} catch (error) {
|
|
78
96
|
console.error(`codecraft: ${error.message}`)
|
|
79
97
|
return 2
|
|
80
98
|
}
|
|
81
99
|
try {
|
|
82
|
-
const plan = buildInstallPlan(root, { agents, global: args.global, dest: args.dest })
|
|
100
|
+
const plan = buildInstallPlan(root, { agents, global: args.global, dest: args.dest, registry })
|
|
83
101
|
const applied = args.dryRun ? null : applyInstallPlan(plan, { force: args.force })
|
|
84
102
|
const meta = {
|
|
85
103
|
version: VERSION,
|
|
@@ -154,6 +172,11 @@ export function main(argv) {
|
|
|
154
172
|
const findings = result.findings.map((finding) => ({ ...finding, severity }))
|
|
155
173
|
const filtered = applyIgnores(findings, config.config.ignore ?? [])
|
|
156
174
|
const activeFindings = filtered.findings
|
|
175
|
+
const warnings = []
|
|
176
|
+
if (result.files === 0 && result.findings.length === 0) {
|
|
177
|
+
warnings.push(`0 source files scanned under ${root}; pass --pack app|library or check --root`)
|
|
178
|
+
console.error(`codecraft: warning: ${warnings[0]}`)
|
|
179
|
+
}
|
|
157
180
|
const gates = shouldRunGates(command, args)
|
|
158
181
|
? runGates(root, config.config.gates ?? [], { failFast: args.failFast })
|
|
159
182
|
: []
|
|
@@ -170,6 +193,7 @@ export function main(argv) {
|
|
|
170
193
|
findings: activeFindings.length,
|
|
171
194
|
ignored: filtered.ignored.length,
|
|
172
195
|
expired: filtered.expired.length,
|
|
196
|
+
warnings,
|
|
173
197
|
plan: plan.length,
|
|
174
198
|
gates: gates.length,
|
|
175
199
|
}
|
|
@@ -207,6 +231,7 @@ export function main(argv) {
|
|
|
207
231
|
const failed =
|
|
208
232
|
activeFindings.some((finding) => finding.severity === 'error') ||
|
|
209
233
|
gates.some((gate) => gate.exitCode !== 0)
|
|
234
|
+
if (warnings.length) return 2
|
|
210
235
|
return failed ? 1 : 0
|
|
211
236
|
}
|
|
212
237
|
|
|
@@ -241,6 +266,7 @@ function parseArgs(argv) {
|
|
|
241
266
|
dest: '',
|
|
242
267
|
force: false,
|
|
243
268
|
dryRun: false,
|
|
269
|
+
listAgents: false,
|
|
244
270
|
format: 'md',
|
|
245
271
|
severity: 'warn',
|
|
246
272
|
ci: false,
|
|
@@ -260,6 +286,7 @@ function parseArgs(argv) {
|
|
|
260
286
|
else if (arg === '--global') args.global = true
|
|
261
287
|
else if (arg === '--force') args.force = true
|
|
262
288
|
else if (arg === '--dry-run') args.dryRun = true
|
|
289
|
+
else if (arg === '--list-agents') args.listAgents = true
|
|
263
290
|
else if (arg === '--name') args.name = argv[++i] ?? ''
|
|
264
291
|
else if (arg === '--agent') args.agent = argv[++i] ?? 'auto'
|
|
265
292
|
else if (arg === '--dest') args.dest = argv[++i] ?? ''
|
|
@@ -281,13 +308,83 @@ function parseArgs(argv) {
|
|
|
281
308
|
return args
|
|
282
309
|
}
|
|
283
310
|
|
|
284
|
-
function detectPack(root) {
|
|
285
|
-
if (isDir(path.join(root, 'apps'))
|
|
311
|
+
export function detectPack(root) {
|
|
312
|
+
if (isDir(path.join(root, 'apps'))) return 'app'
|
|
313
|
+
if (hasAppMarkers(root)) return 'app'
|
|
286
314
|
if (isDir(path.join(root, 'packages'))) return 'library'
|
|
315
|
+
if (hasNestedApp(root)) return 'app'
|
|
316
|
+
if (isDir(path.join(root, 'src'))) return 'library'
|
|
287
317
|
if (fs.existsSync(path.join(root, 'package.json'))) return 'library'
|
|
288
318
|
return 'app'
|
|
289
319
|
}
|
|
290
320
|
|
|
321
|
+
const APP_DIR_MARKERS = ['src/pages', 'pages', 'app', 'src/app']
|
|
322
|
+
const APP_FILE_MARKERS = [
|
|
323
|
+
'nuxt.config.js',
|
|
324
|
+
'nuxt.config.ts',
|
|
325
|
+
'nuxt.config.mjs',
|
|
326
|
+
'app.config.js',
|
|
327
|
+
'app.config.ts',
|
|
328
|
+
'src/app.config.js',
|
|
329
|
+
'src/app.config.ts',
|
|
330
|
+
'taro.config.js',
|
|
331
|
+
'taro.config.ts',
|
|
332
|
+
]
|
|
333
|
+
const APP_DEPS = ['nuxt', '@tarojs/taro', '@dcloudio/uni-app', 'uni-app', 'next']
|
|
334
|
+
|
|
335
|
+
function hasAppMarkers(root) {
|
|
336
|
+
if (APP_DIR_MARKERS.some((dir) => isDir(path.join(root, dir)))) return true
|
|
337
|
+
if (APP_FILE_MARKERS.some((file) => isFile(path.join(root, file)))) return true
|
|
338
|
+
if (hasUniAppFiles(root)) return true
|
|
339
|
+
return hasAppDependency(root)
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function hasNestedApp(root) {
|
|
343
|
+
let entries
|
|
344
|
+
try {
|
|
345
|
+
entries = fs.readdirSync(root, { withFileTypes: true })
|
|
346
|
+
} catch {
|
|
347
|
+
return false
|
|
348
|
+
}
|
|
349
|
+
for (const entry of entries) {
|
|
350
|
+
if (!entry.isDirectory()) continue
|
|
351
|
+
if (['node_modules', 'dist', 'build', 'target', '.git', '.nuxt', '.output', 'coverage'].includes(entry.name)) {
|
|
352
|
+
continue
|
|
353
|
+
}
|
|
354
|
+
const child = path.join(root, entry.name)
|
|
355
|
+
if (APP_DIR_MARKERS.some((dir) => isDir(path.join(child, dir)))) return true
|
|
356
|
+
if (APP_FILE_MARKERS.some((file) => isFile(path.join(child, file)))) return true
|
|
357
|
+
if (hasUniAppFiles(child)) return true
|
|
358
|
+
if (hasAppDependency(child)) return true
|
|
359
|
+
}
|
|
360
|
+
return false
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function hasUniAppFiles(root) {
|
|
364
|
+
const atRoot = isFile(path.join(root, 'manifest.json')) && isFile(path.join(root, 'pages.json'))
|
|
365
|
+
const atSrc = isFile(path.join(root, 'src', 'manifest.json')) && isFile(path.join(root, 'src', 'pages.json'))
|
|
366
|
+
return atRoot || atSrc
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function hasAppDependency(root) {
|
|
370
|
+
let manifest
|
|
371
|
+
try {
|
|
372
|
+
manifest = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'))
|
|
373
|
+
} catch {
|
|
374
|
+
return false
|
|
375
|
+
}
|
|
376
|
+
const deps = { ...(manifest.dependencies ?? {}), ...(manifest.devDependencies ?? {}) }
|
|
377
|
+
return APP_DEPS.some((dep) => dep in deps)
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function isFile(target) {
|
|
381
|
+
try {
|
|
382
|
+
return fs.statSync(target).isFile()
|
|
383
|
+
} catch {
|
|
384
|
+
return false
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
|
|
291
388
|
function fail(message) {
|
|
292
389
|
console.error(`codecraft: ${message}\n\n${HELP}`)
|
|
293
390
|
process.exit(2)
|
|
@@ -424,6 +521,24 @@ function formatInit(meta, plan, applied) {
|
|
|
424
521
|
return lines.filter((line, index) => !(line === '' && lines[index - 1] === '')).join('\n')
|
|
425
522
|
}
|
|
426
523
|
|
|
524
|
+
function formatAgents(agents) {
|
|
525
|
+
const lines = [
|
|
526
|
+
'# Codecraft Agents',
|
|
527
|
+
'',
|
|
528
|
+
`- Version: ${VERSION}`,
|
|
529
|
+
'',
|
|
530
|
+
'| Name | Source | Project | Global | Detected |',
|
|
531
|
+
'|---|---|---|---|---|',
|
|
532
|
+
]
|
|
533
|
+
for (const agent of agents) {
|
|
534
|
+
const detected =
|
|
535
|
+
[agent.detected.project ? 'project' : '', agent.detected.global ? 'global' : ''].filter(Boolean).join(', ') ||
|
|
536
|
+
'—'
|
|
537
|
+
lines.push(`| ${agent.name} | ${agent.source} | ${agent.project} | ${agent.global} | ${detected} |`)
|
|
538
|
+
}
|
|
539
|
+
return lines.join('\n')
|
|
540
|
+
}
|
|
541
|
+
|
|
427
542
|
function formatInstall(meta, plan, applied) {
|
|
428
543
|
const lines = [
|
|
429
544
|
'# Codecraft Install',
|
|
@@ -482,6 +597,7 @@ function header(meta) {
|
|
|
482
597
|
`- Findings: ${meta.findings}`,
|
|
483
598
|
`- Ignored: ${meta.ignored ?? 0}`,
|
|
484
599
|
`- Expired exceptions: ${meta.expired ?? 0}`,
|
|
600
|
+
`- Warnings: ${meta.warnings?.length ?? 0}`,
|
|
485
601
|
`- Gates: ${meta.gates}`,
|
|
486
602
|
'',
|
|
487
603
|
]
|
package/src/install.mjs
CHANGED
|
@@ -7,51 +7,88 @@ const PACKAGE_ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
|
7
7
|
const SKILL_FILES = ['SKILL.md']
|
|
8
8
|
const SKILL_DIRS = ['docs', 'scenarios', 'prompts']
|
|
9
9
|
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
generic: '.agents/skills',
|
|
10
|
+
const USER_AGENTS_FILE = path.join(os.homedir(), '.codecraft', 'agents.json')
|
|
11
|
+
const PROJECT_AGENTS_FILE = 'codecraft.agents.json'
|
|
12
|
+
|
|
13
|
+
const BUILTIN_AGENTS = {
|
|
14
|
+
generic: { project: '.agents/skills', global: '.agents/skills', markers: ['.agents'] },
|
|
15
|
+
claude: { project: '.claude/skills', global: '.claude/skills', markers: ['.claude', 'CLAUDE.md'] },
|
|
16
|
+
cursor: { project: '.cursor/skills', global: '.cursor/skills', markers: ['.cursor'] },
|
|
17
|
+
trae: { project: '.trae/skills', global: '.trae/skills', markers: ['.trae'] },
|
|
18
|
+
'trae-cn': { project: '.trae-cn/skills', global: '.trae-cn/skills', markers: ['.trae-cn'] },
|
|
15
19
|
}
|
|
16
20
|
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
export const AGENTS = Object.keys(BUILTIN_AGENTS)
|
|
22
|
+
|
|
23
|
+
export function loadAgents(root, options = {}) {
|
|
24
|
+
const registry = new Map(
|
|
25
|
+
Object.entries(BUILTIN_AGENTS).map(([name, definition]) => [name, { ...definition, source: 'builtin' }]),
|
|
26
|
+
)
|
|
27
|
+
mergeAgentFile(registry, options.userFile ?? USER_AGENTS_FILE, 'user')
|
|
28
|
+
mergeAgentFile(registry, options.projectFile ?? path.join(root, PROJECT_AGENTS_FILE), 'project')
|
|
29
|
+
return registry
|
|
22
30
|
}
|
|
23
31
|
|
|
24
|
-
export
|
|
32
|
+
export function listAgents(root, options = {}) {
|
|
33
|
+
const registry = loadAgents(root, options)
|
|
34
|
+
return [...registry.entries()].map(([name, definition]) => ({
|
|
35
|
+
name,
|
|
36
|
+
project: definition.project,
|
|
37
|
+
global: definition.global,
|
|
38
|
+
source: definition.source,
|
|
39
|
+
detected: {
|
|
40
|
+
project: isDetected(root, definition.markers),
|
|
41
|
+
global: isDetected(os.homedir(), definition.markers),
|
|
42
|
+
},
|
|
43
|
+
}))
|
|
44
|
+
}
|
|
25
45
|
|
|
26
|
-
export function parseAgents(value) {
|
|
46
|
+
export function parseAgents(value, names = AGENTS) {
|
|
27
47
|
if (!value || value === 'auto') return []
|
|
28
48
|
const agents = [...new Set(value.split(',').map((item) => item.trim()).filter(Boolean))]
|
|
29
|
-
const invalid = agents.filter((agent) => !
|
|
49
|
+
const invalid = agents.filter((agent) => !names.includes(agent))
|
|
30
50
|
if (invalid.length) {
|
|
31
|
-
throw new Error(`unknown agent: ${invalid.join(', ')}
|
|
51
|
+
throw new Error(`unknown agent: ${invalid.join(', ')}(可选:${names.join(', ')};用 --list-agents 查看)`)
|
|
32
52
|
}
|
|
33
53
|
return agents
|
|
34
54
|
}
|
|
35
55
|
|
|
36
56
|
export function buildInstallPlan(root, options = {}) {
|
|
57
|
+
const registry = options.registry ?? loadAgents(root, options)
|
|
37
58
|
const base = options.global ? os.homedir() : path.resolve(root)
|
|
59
|
+
const names = [...registry.keys()]
|
|
38
60
|
const explicit = options.agents ?? []
|
|
39
|
-
const invalid = explicit.filter((agent) => !
|
|
61
|
+
const invalid = explicit.filter((agent) => !registry.has(agent))
|
|
40
62
|
if (invalid.length) {
|
|
41
|
-
throw new Error(`unknown agent: ${invalid.join(', ')}(可选:${
|
|
63
|
+
throw new Error(`unknown agent: ${invalid.join(', ')}(可选:${names.join(', ')};用 --list-agents 查看)`)
|
|
42
64
|
}
|
|
43
65
|
|
|
44
|
-
const agents = explicit.length ? explicit : detectAgents(base, options.global)
|
|
66
|
+
const agents = explicit.length ? explicit : detectAgents(base, registry, options.global)
|
|
45
67
|
const destinations = options.dest
|
|
46
68
|
? [{ agent: 'custom', dir: path.resolve(base, options.dest) }]
|
|
47
|
-
: agents.map((agent) =>
|
|
69
|
+
: agents.map((agent) => {
|
|
70
|
+
const definition = registry.get(agent)
|
|
71
|
+
return { agent, dir: path.join(base, options.global ? definition.global : definition.project) }
|
|
72
|
+
})
|
|
48
73
|
|
|
49
74
|
assertSource()
|
|
50
75
|
|
|
51
|
-
|
|
76
|
+
const byTarget = new Map()
|
|
77
|
+
for (const { agent, dir } of destinations) {
|
|
52
78
|
const target = path.join(dir, 'codecraft')
|
|
79
|
+
if (!byTarget.has(target)) byTarget.set(target, { target, agents: [] })
|
|
80
|
+
byTarget.get(target).agents.push(agent)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return [...byTarget.values()].map(({ target, agents: targetAgents }) => {
|
|
53
84
|
const exists = fs.existsSync(target)
|
|
54
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
agent: targetAgents.join(','),
|
|
87
|
+
agents: targetAgents,
|
|
88
|
+
target,
|
|
89
|
+
exists,
|
|
90
|
+
action: exists ? 'exists' : 'create',
|
|
91
|
+
}
|
|
55
92
|
})
|
|
56
93
|
}
|
|
57
94
|
|
|
@@ -83,14 +120,49 @@ export function applyInstallPlan(plan, options = {}) {
|
|
|
83
120
|
return { created, skipped, replaced }
|
|
84
121
|
}
|
|
85
122
|
|
|
86
|
-
function
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
123
|
+
function mergeAgentFile(registry, file, source) {
|
|
124
|
+
if (!fs.existsSync(file)) return
|
|
125
|
+
|
|
126
|
+
let parsed
|
|
127
|
+
try {
|
|
128
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf8'))
|
|
129
|
+
} catch (error) {
|
|
130
|
+
throw new Error(`invalid agents file ${file}: ${error.message} (fix the JSON syntax or remove the file)`)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const agents = parsed?.agents
|
|
134
|
+
if (!agents || typeof agents !== 'object' || Array.isArray(agents)) {
|
|
135
|
+
throw new Error(`invalid agents file ${file}: expected { "agents": { "<name>": { project, global, markers } } }`)
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
for (const [name, definition] of Object.entries(agents)) {
|
|
139
|
+
if (!/^[a-z0-9][a-z0-9-]*$/.test(name)) {
|
|
140
|
+
throw new Error(`invalid agents file ${file}: agent name "${name}" must be lowercase kebab-case`)
|
|
141
|
+
}
|
|
142
|
+
const project = typeof definition?.project === 'string' ? definition.project.trim() : ''
|
|
143
|
+
if (!project) {
|
|
144
|
+
throw new Error(`invalid agents file ${file}: agent "${name}" needs a non-empty "project" path`)
|
|
145
|
+
}
|
|
146
|
+
const global = typeof definition?.global === 'string' && definition.global.trim() ? definition.global.trim() : project
|
|
147
|
+
const markers = Array.isArray(definition?.markers)
|
|
148
|
+
? definition.markers.filter((marker) => typeof marker === 'string' && marker.trim())
|
|
149
|
+
: []
|
|
150
|
+
registry.set(name, { project, global, markers, source })
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function detectAgents(base, registry, global) {
|
|
155
|
+
const found = [...registry.entries()]
|
|
156
|
+
.filter(([, definition]) => isDetected(base, definition.markers))
|
|
157
|
+
.map(([name]) => name)
|
|
90
158
|
if (global) return found.length ? found : ['generic']
|
|
91
159
|
return [...new Set([...found, 'generic'])]
|
|
92
160
|
}
|
|
93
161
|
|
|
162
|
+
function isDetected(base, markers) {
|
|
163
|
+
return markers.some((marker) => fs.existsSync(path.join(base, marker)))
|
|
164
|
+
}
|
|
165
|
+
|
|
94
166
|
function assertSource() {
|
|
95
167
|
for (const file of SKILL_FILES) {
|
|
96
168
|
if (!fs.existsSync(path.join(PACKAGE_ROOT, file))) {
|