@ultime/codecraft 1.2.1 → 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 +7 -0
- package/README.md +20 -1
- package/VERSION +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +42 -4
- package/src/install.mjs +96 -26
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 1.2.1 - 2026-10-27
|
|
4
11
|
|
|
5
12
|
- 修复 pack 自动判定:识别 Nuxt 2 的 `pages/`、Nuxt 3 的 `app/`、uni-app 的 `manifest.json + pages.json`、Taro / uni-app 依赖,以及 workspace 内的嵌套应用。
|
package/README.md
CHANGED
|
@@ -69,7 +69,25 @@ npx @ultime/codecraft install --dry-run # 只看计划
|
|
|
69
69
|
npx @ultime/codecraft install --force # 覆盖已有目录
|
|
70
70
|
```
|
|
71
71
|
|
|
72
|
-
|
|
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`,不必先写配置。
|
|
73
91
|
|
|
74
92
|
### 方式 C:手动指针
|
|
75
93
|
|
|
@@ -270,6 +288,7 @@ ROADMAP.md 后续版本计划与交接说明
|
|
|
270
288
|
- **静态检查误报怎么办?** 在 `codecraft.config.json` 的 `ignore` 里登记 `rule` + `file` + `reason`,必要时加 `expires`;不要直接删规则。
|
|
271
289
|
- **`Files: 0` 是什么意思?** 没有扫到源文件,命令会输出警告并以 2 退出;用 `--pack app|library` 指定正确类型,或检查 `--root`。
|
|
272
290
|
- **Trae CN 装到哪?** 用 `--agent trae-cn`(`~/.trae-cn/skills`);未知目录用 `--dest`。
|
|
291
|
+
- **怎么支持新的 agent?** 在 `codecraft.agents.json` 里加 `project` / `global` / `markers`,再跑 `--list-agents` 确认;不需要等 Codecraft 发版。
|
|
273
292
|
- **`check` / `audit` / `plan` 有什么区别?** `check` 只跑内置规则;`audit` 额外跑 config 里的 gates;`plan` 只读,把 findings 转成计划,默认不跑 gates。
|
|
274
293
|
- **为什么不自动跑 typecheck / test?** 门禁由项目提供,Codecraft 只编排,避免第二套实现。
|
|
275
294
|
- **monorepo 为什么被判成 app / library?** 有 `pages/`、`app/`、`manifest.json + pages.json`、Taro / uni-app 依赖或嵌套应用时按 `app`;有 `packages/` 或 `src/` 时按 `library`;仍不确定就显式传 `--pack`。
|
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,7 +24,8 @@ 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
30
|
--dest <path> Explicit destination (relative to root / home), e.g. .trae-cn/skills
|
|
30
31
|
--force Replace an existing codecraft skill directory
|
|
@@ -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,
|
|
@@ -248,6 +266,7 @@ function parseArgs(argv) {
|
|
|
248
266
|
dest: '',
|
|
249
267
|
force: false,
|
|
250
268
|
dryRun: false,
|
|
269
|
+
listAgents: false,
|
|
251
270
|
format: 'md',
|
|
252
271
|
severity: 'warn',
|
|
253
272
|
ci: false,
|
|
@@ -267,6 +286,7 @@ function parseArgs(argv) {
|
|
|
267
286
|
else if (arg === '--global') args.global = true
|
|
268
287
|
else if (arg === '--force') args.force = true
|
|
269
288
|
else if (arg === '--dry-run') args.dryRun = true
|
|
289
|
+
else if (arg === '--list-agents') args.listAgents = true
|
|
270
290
|
else if (arg === '--name') args.name = argv[++i] ?? ''
|
|
271
291
|
else if (arg === '--agent') args.agent = argv[++i] ?? 'auto'
|
|
272
292
|
else if (arg === '--dest') args.dest = argv[++i] ?? ''
|
|
@@ -501,6 +521,24 @@ function formatInit(meta, plan, applied) {
|
|
|
501
521
|
return lines.filter((line, index) => !(line === '' && lines[index - 1] === '')).join('\n')
|
|
502
522
|
}
|
|
503
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
|
+
|
|
504
542
|
function formatInstall(meta, plan, applied) {
|
|
505
543
|
const lines = [
|
|
506
544
|
'# Codecraft Install',
|
package/src/install.mjs
CHANGED
|
@@ -7,53 +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
|
-
|
|
15
|
-
'
|
|
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'] },
|
|
16
19
|
}
|
|
17
20
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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
|
|
24
30
|
}
|
|
25
31
|
|
|
26
|
-
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
|
+
}
|
|
27
45
|
|
|
28
|
-
export function parseAgents(value) {
|
|
46
|
+
export function parseAgents(value, names = AGENTS) {
|
|
29
47
|
if (!value || value === 'auto') return []
|
|
30
48
|
const agents = [...new Set(value.split(',').map((item) => item.trim()).filter(Boolean))]
|
|
31
|
-
const invalid = agents.filter((agent) => !
|
|
49
|
+
const invalid = agents.filter((agent) => !names.includes(agent))
|
|
32
50
|
if (invalid.length) {
|
|
33
|
-
throw new Error(`unknown agent: ${invalid.join(', ')}
|
|
51
|
+
throw new Error(`unknown agent: ${invalid.join(', ')}(可选:${names.join(', ')};用 --list-agents 查看)`)
|
|
34
52
|
}
|
|
35
53
|
return agents
|
|
36
54
|
}
|
|
37
55
|
|
|
38
56
|
export function buildInstallPlan(root, options = {}) {
|
|
57
|
+
const registry = options.registry ?? loadAgents(root, options)
|
|
39
58
|
const base = options.global ? os.homedir() : path.resolve(root)
|
|
59
|
+
const names = [...registry.keys()]
|
|
40
60
|
const explicit = options.agents ?? []
|
|
41
|
-
const invalid = explicit.filter((agent) => !
|
|
61
|
+
const invalid = explicit.filter((agent) => !registry.has(agent))
|
|
42
62
|
if (invalid.length) {
|
|
43
|
-
throw new Error(`unknown agent: ${invalid.join(', ')}(可选:${
|
|
63
|
+
throw new Error(`unknown agent: ${invalid.join(', ')}(可选:${names.join(', ')};用 --list-agents 查看)`)
|
|
44
64
|
}
|
|
45
65
|
|
|
46
|
-
const agents = explicit.length ? explicit : detectAgents(base, options.global)
|
|
66
|
+
const agents = explicit.length ? explicit : detectAgents(base, registry, options.global)
|
|
47
67
|
const destinations = options.dest
|
|
48
68
|
? [{ agent: 'custom', dir: path.resolve(base, options.dest) }]
|
|
49
|
-
: 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
|
+
})
|
|
50
73
|
|
|
51
74
|
assertSource()
|
|
52
75
|
|
|
53
|
-
|
|
76
|
+
const byTarget = new Map()
|
|
77
|
+
for (const { agent, dir } of destinations) {
|
|
54
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 }) => {
|
|
55
84
|
const exists = fs.existsSync(target)
|
|
56
|
-
return {
|
|
85
|
+
return {
|
|
86
|
+
agent: targetAgents.join(','),
|
|
87
|
+
agents: targetAgents,
|
|
88
|
+
target,
|
|
89
|
+
exists,
|
|
90
|
+
action: exists ? 'exists' : 'create',
|
|
91
|
+
}
|
|
57
92
|
})
|
|
58
93
|
}
|
|
59
94
|
|
|
@@ -85,14 +120,49 @@ export function applyInstallPlan(plan, options = {}) {
|
|
|
85
120
|
return { created, skipped, replaced }
|
|
86
121
|
}
|
|
87
122
|
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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)
|
|
92
158
|
if (global) return found.length ? found : ['generic']
|
|
93
159
|
return [...new Set([...found, 'generic'])]
|
|
94
160
|
}
|
|
95
161
|
|
|
162
|
+
function isDetected(base, markers) {
|
|
163
|
+
return markers.some((marker) => fs.existsSync(path.join(base, marker)))
|
|
164
|
+
}
|
|
165
|
+
|
|
96
166
|
function assertSource() {
|
|
97
167
|
for (const file of SKILL_FILES) {
|
|
98
168
|
if (!fs.existsSync(path.join(PACKAGE_ROOT, file))) {
|