@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 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
- 可选 agent:`generic` / `claude` / `cursor` / `trae` / `trae-cn`;未知目录用 `--dest`。
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.2.1
1
+ 1.3.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultime/codecraft",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "面向 JS/TS 的代码规范与架构设计(前端默认,Node 后端可复用结构规则),作为可安装的 skill 包。",
5
5
  "keywords": [
6
6
  "ai",
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 auto|generic|claude|cursor|trae|trae-cn Install target, comma-separated (default: auto)
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
- agents = parseAgents(args.agent)
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 AGENT_DIRS = {
11
- generic: '.agents/skills',
12
- claude: '.claude/skills',
13
- cursor: '.cursor/skills',
14
- trae: '.trae/skills',
15
- 'trae-cn': '.trae-cn/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'] },
16
19
  }
17
20
 
18
- const AGENT_MARKERS = {
19
- generic: ['.agents'],
20
- claude: ['.claude', 'CLAUDE.md'],
21
- cursor: ['.cursor'],
22
- trae: ['.trae'],
23
- 'trae-cn': ['.trae-cn'],
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 const AGENTS = Object.keys(AGENT_DIRS)
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) => !AGENTS.includes(agent))
49
+ const invalid = agents.filter((agent) => !names.includes(agent))
32
50
  if (invalid.length) {
33
- throw new Error(`unknown agent: ${invalid.join(', ')}(可选:auto, ${AGENTS.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) => !AGENTS.includes(agent))
61
+ const invalid = explicit.filter((agent) => !registry.has(agent))
42
62
  if (invalid.length) {
43
- throw new Error(`unknown agent: ${invalid.join(', ')}(可选:${AGENTS.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) => ({ agent, dir: path.join(base, AGENT_DIRS[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
- return destinations.map(({ agent, dir }) => {
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 { agent, target, exists, action: exists ? 'exists' : 'create' }
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 detectAgents(base, global) {
89
- const found = AGENTS.filter((agent) =>
90
- AGENT_MARKERS[agent].some((marker) => fs.existsSync(path.join(base, marker))),
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))) {