@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 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`、`.cursor/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 claude # 指定 agent,可逗号分隔
65
- npx @ultime/codecraft install --global --agent claude # 装到用户目录
66
- npx @ultime/codecraft install --dry-run # 只看计划
67
- npx @ultime/codecraft install --force # 覆盖已有目录
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?** 有 `packages/` 或根 `package.json` 即按 library;可用 `--pack` 显式覆盖。
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.2.0
1
+ 1.3.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultime/codecraft",
3
- "version": "1.2.0",
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,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 auto|claude|trae|cursor|generic 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
- --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
- 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,
@@ -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')) || isDir(path.join(root, 'src', 'pages'))) return 'app'
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 AGENT_DIRS = {
11
- claude: '.claude/skills',
12
- trae: '.trae/skills',
13
- cursor: '.cursor/skills',
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 AGENT_MARKERS = {
18
- claude: ['.claude', 'CLAUDE.md'],
19
- trae: ['.trae'],
20
- cursor: ['.cursor'],
21
- generic: ['.agents'],
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 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
+ }
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) => !AGENTS.includes(agent))
49
+ const invalid = agents.filter((agent) => !names.includes(agent))
30
50
  if (invalid.length) {
31
- throw new Error(`unknown agent: ${invalid.join(', ')}(可选:auto, ${AGENTS.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) => !AGENTS.includes(agent))
61
+ const invalid = explicit.filter((agent) => !registry.has(agent))
40
62
  if (invalid.length) {
41
- throw new Error(`unknown agent: ${invalid.join(', ')}(可选:${AGENTS.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) => ({ 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
+ })
48
73
 
49
74
  assertSource()
50
75
 
51
- return destinations.map(({ agent, dir }) => {
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 { 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
+ }
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 detectAgents(base, global) {
87
- const found = AGENTS.filter((agent) =>
88
- AGENT_MARKERS[agent].some((marker) => fs.existsSync(path.join(base, marker))),
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))) {