@vibecuting/component-project-helper 0.1.17 → 0.1.19

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 CHANGED
@@ -9,11 +9,10 @@ NPM 地址:
9
9
  职责:
10
10
 
11
11
  - 作为可发布的 npm 包承载组件工程辅助逻辑。
12
- - 提供组件注解、schema、发现器、Markdown 生成和安装期生命周期逻辑。
12
+ - 提供组件注解、schema、发现器、元数据生成、技能说明生成和安装期生命周期逻辑。
13
13
  - 安装时会优先把当前工程识别为包含 `src/components` 或 `src/video/chapters` 的独立项目,再扫描 `src/components/**/*.tsx`、`src/video/chapters/**/*.tsx` 以及 `node_modules/@vibecuting/*/src/**/*.tsx`,把组件元数据生成到 `.agents/skills/project-allow-component/components/`。
14
- - 通过 `postinstall` / `preuninstall` 维护生成文件和清理清单。
15
- - 安装时会给宿主工程的 `package.json` 补上 `update-component-skills`,它会调用同一套 `postinstall` 逻辑。
16
- - 宿主工程也可以直接运行 `pnpm update-component-skills` 手动刷新同一套生成结果。
14
+ - 宿主工程的 `update-component-skills` 会基于 `.vibecuting/video-resource-meta.generated.ts` 生成技能说明,不再执行模板源码文件本身。
15
+ - 宿主工程也可以直接运行 `pnpm run update-component-skills` 手动刷新同一套生成结果。
17
16
  - 作为主工程之外的独立 helper 包,避免把辅助逻辑重新塞回主站代码里。
18
17
 
19
18
  目录结构:
@@ -21,7 +20,7 @@ NPM 地址:
21
20
  - `src/decorators`:注解定义和元数据读写
22
21
  - `src/schemas`:zod schema
23
22
  - `src/discovery`:组件发现与解析
24
- - `src/markdown`:Markdown 渲染
23
+ - `src/meta`:元数据和技能说明生成
25
24
  - `src/runtime`:安装根与输出路径解析
26
25
  - `src/lifecycle`:postinstall / preuninstall 生命周期逻辑
27
26
 
@@ -29,7 +28,6 @@ NPM 地址:
29
28
 
30
29
  - `pnpm typecheck`
31
30
  - `pnpm test`
32
- - `node ./bin/component-project-helper.mjs postinstall`
33
31
  - `pnpm update-component-skills`
34
32
 
35
33
  发布与安装:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vibecuting/component-project-helper",
3
- "version": "0.1.17",
3
+ "version": "0.1.19",
4
4
  "type": "module",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "main": "./src/index.ts",
@@ -21,10 +21,10 @@
21
21
  },
22
22
  "scripts": {
23
23
  "update-component-meta": "node ./scripts/update-component-meta.mjs",
24
- "update-component-skills": "pnpm run update-component-meta",
24
+ "update-component-skills": "node ./scripts/update-component-skills.mjs",
25
25
  "lint": "node ../../../scripts/run-eslint.mjs .",
26
- "typecheck": "node ../../../node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/bin/tsc -p tsconfig.json --noEmit",
27
- "test": "node ../../../node_modules/vitest/vitest.mjs run"
26
+ "typecheck": "pnpm exec tsc -p tsconfig.json --noEmit",
27
+ "test": "pnpm exec vitest run"
28
28
  },
29
29
  "dependencies": {
30
30
  "reflect-metadata": "^0.2.2",
@@ -33,6 +33,8 @@
33
33
  "zod": "4.1.12"
34
34
  },
35
35
  "devDependencies": {
36
- "vitest": "^3.2.4"
36
+ "@testing-library/jest-dom": "^6.8.0",
37
+ "@types/node": "20.12.2",
38
+ "vitest": "^4.1.0"
37
39
  }
38
40
  }
@@ -3,7 +3,7 @@ import path from 'node:path'
3
3
 
4
4
  export const componentProjectSkillsScriptName = 'update-component-skills'
5
5
  export const componentProjectSkillsScriptCommand =
6
- 'node ./node_modules/@vibecuting/component-project-helper/scripts/update-component-meta.mjs'
6
+ 'node ./node_modules/@vibecuting/component-project-helper/scripts/update-component-skills.mjs'
7
7
 
8
8
  async function readPackageJson(projectRoot) {
9
9
  const packageJsonPath = path.join(projectRoot, 'package.json')
@@ -0,0 +1,63 @@
1
+ import { existsSync } from 'node:fs'
2
+ import path from 'node:path'
3
+ import { spawnSync } from 'node:child_process'
4
+ import { createRequire } from 'node:module'
5
+ import { fileURLToPath, pathToFileURL } from 'node:url'
6
+
7
+ const require = createRequire(import.meta.url)
8
+
9
+ function resolveLoaderPath() {
10
+ const candidates = [
11
+ path.resolve(process.cwd(), 'node_modules/tsx/dist/loader.mjs'),
12
+ path.resolve(process.cwd(), '../../../project-templates/video-project/node_modules/tsx/dist/loader.mjs'),
13
+ path.resolve(process.cwd(), '../../../project-templates/component-project/node_modules/tsx/dist/loader.mjs'),
14
+ ]
15
+
16
+ for (const candidate of candidates) {
17
+ if (existsSync(candidate)) {
18
+ return candidate
19
+ }
20
+ }
21
+
22
+ return require.resolve('tsx')
23
+ }
24
+
25
+ const scriptPath = path.relative(process.cwd(), fileURLToPath(import.meta.url))
26
+ const helperScriptPath = path.resolve(
27
+ path.dirname(fileURLToPath(import.meta.url)),
28
+ '..',
29
+ 'src',
30
+ 'meta',
31
+ 'update-component-skills.ts',
32
+ )
33
+
34
+ if (!process.env.COMPONENT_PROJECT_HELPER_UPDATE_COMPONENT_SKILLS_LOADER) {
35
+ const loaderPath = resolveLoaderPath()
36
+ const result = spawnSync(
37
+ process.execPath,
38
+ ['--import', loaderPath, scriptPath, ...process.argv.slice(2)],
39
+ {
40
+ cwd: process.cwd(),
41
+ env: {
42
+ ...process.env,
43
+ COMPONENT_PROJECT_HELPER_UPDATE_COMPONENT_SKILLS_LOADER: '1',
44
+ },
45
+ stdio: 'inherit',
46
+ },
47
+ )
48
+
49
+ if (result.error) {
50
+ throw result.error
51
+ }
52
+
53
+ process.exit(result.status ?? 1)
54
+ }
55
+
56
+ const helperModule = await import(
57
+ pathToFileURL(helperScriptPath).href
58
+ )
59
+
60
+ await helperModule.updateComponentSkills(
61
+ process.cwd(),
62
+ process.argv.includes('--check'),
63
+ )
@@ -56,6 +56,13 @@ const metadataFactoryMap: DescriptorMap = {
56
56
  themeResourceDescriptor as VideoResourceDescriptor<VideoResourceMetadata>,
57
57
  }
58
58
 
59
+ const staticMetadataFactoryNames = new Set([
60
+ sceneResourceDescriptor.metadataFactoryName,
61
+ transitionResourceDescriptor.metadataFactoryName,
62
+ themeResourceDescriptor.metadataFactoryName,
63
+ 'defineComponentProjectComponentMetadata',
64
+ ])
65
+
59
66
  function toScriptKind(filePath: string): ts.ScriptKind {
60
67
  if (filePath.endsWith('.tsx')) {
61
68
  return ts.ScriptKind.TSX
@@ -111,7 +118,7 @@ function unwrapExpression(node: ts.Expression): ts.Expression {
111
118
  }
112
119
  }
113
120
 
114
- function evaluateStaticExpression(node: ts.Expression, filePath: string): unknown {
121
+ export function evaluateStaticExpression(node: ts.Expression, filePath: string): unknown {
115
122
  const expression = unwrapExpression(node)
116
123
 
117
124
  if (ts.isStringLiteralLike(expression)) {
@@ -193,6 +200,17 @@ function evaluateStaticExpression(node: ts.Expression, filePath: string): unknow
193
200
  return result
194
201
  }
195
202
 
203
+ if (ts.isCallExpression(expression)) {
204
+ const factoryName = getIdentifierName(expression.expression)
205
+ if (factoryName && staticMetadataFactoryNames.has(factoryName)) {
206
+ const firstArgument = expression.arguments[0]
207
+ if (!firstArgument || !ts.isExpression(firstArgument)) {
208
+ throw new Error(`${filePath}: static metadata factory ${factoryName} requires a single object literal argument`)
209
+ }
210
+ return evaluateStaticExpression(firstArgument, filePath)
211
+ }
212
+ }
213
+
196
214
  if (ts.isIdentifier(expression) && expression.text === 'undefined') {
197
215
  return undefined
198
216
  }
package/src/index.ts CHANGED
@@ -38,6 +38,9 @@ export {
38
38
  export {
39
39
  updateComponentMeta,
40
40
  } from './meta/update-component-meta'
41
+ export {
42
+ updateComponentSkills,
43
+ } from './meta/update-component-skills'
41
44
  export {
42
45
  VideoComponent,
43
46
  defineComponentProjectComponentMetadata,
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { tmpdir } from 'node:os'
4
+
5
+ import { expect, test } from 'vitest'
6
+
7
+ import { updateComponentSkills } from './update-component-skills'
8
+
9
+ test('writes and checks the skill docs from generated metadata', async () => {
10
+ const projectRoot = await fs.mkdtemp(path.join(tmpdir(), 'component-project-helper-skills-'))
11
+ const generatedDir = path.join(projectRoot, '.vibecuting')
12
+ const generatedFilePath = path.join(generatedDir, 'video-resource-meta.generated.ts')
13
+
14
+ await fs.mkdir(generatedDir, { recursive: true })
15
+ await fs.writeFile(
16
+ generatedFilePath,
17
+ `
18
+ export const videoResourceMeta = {
19
+ packageFingerprint: 'sha256:demo',
20
+ packages: [{ packageName: '@vibecuting/demo-pack', packageVersion: '1.0.0' }],
21
+ resources: [
22
+ {
23
+ packageName: '@vibecuting/demo-pack',
24
+ exportName: 'DemoScene',
25
+ metadata: {
26
+ resourceKind: 'scene',
27
+ name: 'DemoScene',
28
+ description: 'Demo scene resource',
29
+ sourceFile: 'src/index.ts',
30
+ pluginKey: 'demo.scene',
31
+ tags: ['demo'],
32
+ aspectRatio: '16:9',
33
+ sceneType: 'scene',
34
+ sceneFamily: 'custom',
35
+ rootLayout: 'absolute-fill',
36
+ propsTypeName: 'DemoSceneProps',
37
+ slots: ['title'],
38
+ },
39
+ },
40
+ ],
41
+ } as const
42
+ `,
43
+ 'utf8',
44
+ )
45
+
46
+ await updateComponentSkills(projectRoot)
47
+
48
+ const docsDir = path.join(projectRoot, '.agents', 'skills', 'project-allow-component', 'components')
49
+ const generatedDocPath = path.join(docsDir, 'DemoScene.md')
50
+ const generatedDocContents = await fs.readFile(generatedDocPath, 'utf8')
51
+ const generatedManifestPath = path.join(
52
+ projectRoot,
53
+ '.agents',
54
+ 'skills',
55
+ 'project-allow-component',
56
+ 'component-project-helper.manifest.json',
57
+ )
58
+ const generatedManifestContents = await fs.readFile(generatedManifestPath, 'utf8')
59
+
60
+ expect(generatedDocContents).toContain('Demo scene resource')
61
+ expect(generatedDocContents).toContain('resourceKind: scene')
62
+ expect(generatedManifestContents).toContain('video-resource-meta.generated.ts')
63
+ expect(generatedManifestContents).toContain('DemoScene.md')
64
+
65
+ await expect(updateComponentSkills(projectRoot, true)).resolves.toBeUndefined()
66
+ })
@@ -0,0 +1,310 @@
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import ts from 'typescript'
4
+
5
+ import { evaluateStaticExpression } from '../discovery/index.ts'
6
+ import {
7
+ resolveComponentProjectGeneratedDocsDir,
8
+ resolveComponentProjectGeneratedManifestPath,
9
+ } from '../runtime/index.ts'
10
+
11
+ type VideoResourceMetaRecord = {
12
+ packageName: string
13
+ exportName: string
14
+ metadata: {
15
+ resourceKind: 'scene' | 'transition' | 'theme'
16
+ name: string
17
+ description: string
18
+ sourceFile: string
19
+ pluginKey: string
20
+ tags: string[]
21
+ aspectRatio?: '9:16' | '16:9'
22
+ sceneType?: 'intro' | 'scene' | 'outro'
23
+ sceneFamily?: string
24
+ themePreset?: string
25
+ slots?: Array<string | {
26
+ name: string
27
+ description?: string
28
+ required?: boolean
29
+ accepts?: 'text' | 'image' | 'video' | 'component' | 'any'
30
+ }>
31
+ propsTypeName?: string
32
+ rootLayout?: 'absolute-fill'
33
+ transitionKind?: 'transition' | 'overlay'
34
+ supportedSceneFamilies?: string[]
35
+ cssVariables?: string[]
36
+ }
37
+ }
38
+
39
+ type VideoResourceMetaFile = {
40
+ videoResourceMeta?: {
41
+ packageFingerprint?: string
42
+ packages?: Array<{
43
+ packageName: string
44
+ packageVersion: string
45
+ }>
46
+ resources: VideoResourceMetaRecord[]
47
+ }
48
+ }
49
+
50
+ type GeneratedSkillDoc = {
51
+ filePath: string
52
+ contents: string
53
+ }
54
+
55
+ function stringifyFrontmatterList(items: readonly string[]): string {
56
+ return items.map((item) => ` - ${item}`).join('\n')
57
+ }
58
+
59
+ function stringifySlots(
60
+ slots: NonNullable<VideoResourceMetaRecord['metadata']['slots']>,
61
+ ): string {
62
+ if (slots.length === 0) {
63
+ return '[]'
64
+ }
65
+
66
+ return slots
67
+ .map((slot) => {
68
+ if (typeof slot === 'string') {
69
+ return ` - ${slot}`
70
+ }
71
+
72
+ const lines = [
73
+ ` - name: ${slot.name}`,
74
+ slot.description ? ` description: ${slot.description}` : undefined,
75
+ slot.required !== undefined ? ` required: ${slot.required}` : undefined,
76
+ slot.accepts ? ` accepts: ${slot.accepts}` : undefined,
77
+ ].filter(Boolean)
78
+
79
+ return lines.join('\n')
80
+ })
81
+ .join('\n')
82
+ }
83
+
84
+ function sanitizeFileName(name: string): string {
85
+ return name.replace(/[^a-zA-Z0-9._-]+/g, '-')
86
+ }
87
+
88
+ function toRelativePath(projectRoot: string, absolutePath: string): string {
89
+ return path.relative(projectRoot, absolutePath) || '.'
90
+ }
91
+
92
+ function getMetaFilePath(projectRoot: string): string {
93
+ return path.join(projectRoot, '.vibecuting', 'video-resource-meta.generated.ts')
94
+ }
95
+
96
+ async function readGeneratedVideoResourceMeta(projectRoot: string): Promise<NonNullable<VideoResourceMetaFile['videoResourceMeta']>> {
97
+ const metaFilePath = getMetaFilePath(projectRoot)
98
+ const sourceText = await fs.readFile(metaFilePath, 'utf8')
99
+ const sourceFile = ts.createSourceFile(metaFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
100
+
101
+ for (const statement of sourceFile.statements) {
102
+ if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
103
+ continue
104
+ }
105
+
106
+ for (const declaration of statement.declarationList.declarations) {
107
+ if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'videoResourceMeta') {
108
+ continue
109
+ }
110
+
111
+ if (!declaration.initializer) {
112
+ throw new Error(`${metaFilePath}: missing videoResourceMeta initializer`)
113
+ }
114
+
115
+ const videoResourceMeta = evaluateStaticExpression(declaration.initializer, metaFilePath) as VideoResourceMetaFile['videoResourceMeta']
116
+ if (!videoResourceMeta) {
117
+ throw new Error(`${metaFilePath}: missing videoResourceMeta export`)
118
+ }
119
+
120
+ return videoResourceMeta
121
+ }
122
+ }
123
+
124
+ throw new Error(`${metaFilePath}: missing videoResourceMeta export`)
125
+ }
126
+
127
+ function renderSceneMetadata(record: VideoResourceMetaRecord): string {
128
+ const { metadata } = record
129
+ const slots = metadata.slots ? stringifySlots(metadata.slots) : '[]'
130
+
131
+ return [
132
+ `- resourceKind: ${metadata.resourceKind}`,
133
+ ` aspectRatio: ${metadata.aspectRatio ?? ''}`,
134
+ ` sceneType: ${metadata.sceneType ?? ''}`,
135
+ ` sceneFamily: ${metadata.sceneFamily ?? ''}`,
136
+ ` themePreset: ${metadata.themePreset ?? ''}`,
137
+ ` rootLayout: ${metadata.rootLayout ?? ''}`,
138
+ ` propsTypeName: ${metadata.propsTypeName ?? ''}`,
139
+ ` slots:`,
140
+ slots,
141
+ ].join('\n')
142
+ }
143
+
144
+ function renderTransitionMetadata(record: VideoResourceMetaRecord): string {
145
+ const { metadata } = record
146
+
147
+ return [
148
+ `- resourceKind: ${metadata.resourceKind}`,
149
+ ` transitionKind: ${metadata.transitionKind ?? ''}`,
150
+ ].join('\n')
151
+ }
152
+
153
+ function renderThemeMetadata(record: VideoResourceMetaRecord): string {
154
+ const { metadata } = record
155
+ const supportedSceneFamilies = metadata.supportedSceneFamilies ?? []
156
+ const cssVariables = metadata.cssVariables ?? []
157
+
158
+ return [
159
+ `- resourceKind: ${metadata.resourceKind}`,
160
+ ` supportedSceneFamilies:`,
161
+ supportedSceneFamilies.length > 0 ? stringifyFrontmatterList(supportedSceneFamilies) : ' []',
162
+ ` cssVariables:`,
163
+ cssVariables.length > 0 ? stringifyFrontmatterList(cssVariables) : ' []',
164
+ ].join('\n')
165
+ }
166
+
167
+ function buildDocContents(record: VideoResourceMetaRecord): string {
168
+ const { metadata, exportName } = record
169
+ const tags = metadata.tags ?? []
170
+ const bodyMetadata =
171
+ metadata.resourceKind === 'scene'
172
+ ? renderSceneMetadata(record)
173
+ : metadata.resourceKind === 'transition'
174
+ ? renderTransitionMetadata(record)
175
+ : renderThemeMetadata(record)
176
+
177
+ return [
178
+ '---',
179
+ `name: ${metadata.name}`,
180
+ `resourceKind: ${metadata.resourceKind}`,
181
+ `description: ${metadata.description}`,
182
+ `sourceFile: ${metadata.sourceFile}`,
183
+ `pluginKey: ${metadata.pluginKey}`,
184
+ `tags:`,
185
+ tags.length > 0 ? stringifyFrontmatterList(tags) : ' []',
186
+ '---',
187
+ '',
188
+ `# ${exportName}`,
189
+ '',
190
+ metadata.description,
191
+ '',
192
+ '## 元数据',
193
+ '',
194
+ `- 资源类型: \`${metadata.resourceKind}\``,
195
+ `- 源文件: \`${metadata.sourceFile}\``,
196
+ `- 插件键: \`${metadata.pluginKey}\``,
197
+ `- 标签: ${tags.length > 0 ? tags.map((tag) => `\`${tag}\``).join(', ') : '[]'}`,
198
+ '',
199
+ bodyMetadata,
200
+ '',
201
+ ].join('\n')
202
+ }
203
+
204
+ async function writeTextFile(filePath: string, contents: string): Promise<void> {
205
+ await fs.mkdir(path.dirname(filePath), { recursive: true })
206
+ const tempPath = `${filePath}.tmp`
207
+ await fs.writeFile(tempPath, `${contents}\n`, 'utf8')
208
+ await fs.rename(tempPath, filePath)
209
+ }
210
+
211
+ async function readOptionalFile(filePath: string): Promise<string | undefined> {
212
+ try {
213
+ return await fs.readFile(filePath, 'utf8')
214
+ } catch (error) {
215
+ if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
216
+ return undefined
217
+ }
218
+
219
+ throw error
220
+ }
221
+ }
222
+
223
+ export async function updateComponentSkills(
224
+ projectRoot: string,
225
+ checkOnly = false,
226
+ ): Promise<void> {
227
+ const videoResourceMeta = await readGeneratedVideoResourceMeta(projectRoot)
228
+ const docsDir = resolveComponentProjectGeneratedDocsDir(projectRoot)
229
+ const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
230
+ const resources = [...videoResourceMeta.resources].sort((left, right) => {
231
+ return (
232
+ left.metadata.resourceKind.localeCompare(right.metadata.resourceKind) ||
233
+ left.metadata.pluginKey.localeCompare(right.metadata.pluginKey) ||
234
+ left.exportName.localeCompare(right.exportName)
235
+ )
236
+ })
237
+
238
+ const generatedDocs: GeneratedSkillDoc[] = resources.map((record) => {
239
+ const filePath = path.join(docsDir, `${sanitizeFileName(record.exportName)}.md`)
240
+ return { filePath, contents: buildDocContents(record) }
241
+ })
242
+
243
+ const manifestContents = JSON.stringify(
244
+ {
245
+ projectRoot: '.',
246
+ docsDir: toRelativePath(projectRoot, docsDir),
247
+ sourceMetaFile: toRelativePath(projectRoot, getMetaFilePath(projectRoot)),
248
+ packageFingerprint: videoResourceMeta.packageFingerprint ?? null,
249
+ files: generatedDocs.map((doc) => toRelativePath(projectRoot, doc.filePath)),
250
+ resources: resources.map((resource) => ({
251
+ packageName: resource.packageName,
252
+ exportName: resource.exportName,
253
+ resourceKind: resource.metadata.resourceKind,
254
+ pluginKey: resource.metadata.pluginKey,
255
+ })),
256
+ },
257
+ null,
258
+ 2,
259
+ )
260
+
261
+ if (checkOnly) {
262
+ const currentManifest = await readOptionalFile(manifestPath)
263
+ if (currentManifest?.trimEnd() !== `${manifestContents}\n`.trimEnd()) {
264
+ throw new Error('Generated component skill manifest is stale. Run pnpm run update-component-skills')
265
+ }
266
+
267
+ for (const doc of generatedDocs) {
268
+ const currentDoc = await readOptionalFile(doc.filePath)
269
+ if (currentDoc?.trimEnd() !== doc.contents.trimEnd()) {
270
+ throw new Error(`Generated component skill doc is stale: ${doc.filePath}`)
271
+ }
272
+ }
273
+
274
+ return
275
+ }
276
+
277
+ await writeTextFile(manifestPath, manifestContents)
278
+ await fs.mkdir(docsDir, { recursive: true })
279
+
280
+ const desiredFiles = new Set(generatedDocs.map((doc) => doc.filePath))
281
+
282
+ await Promise.all(
283
+ generatedDocs.map((doc) => writeTextFile(doc.filePath, doc.contents)),
284
+ )
285
+
286
+ const existingEntries = await fs.readdir(docsDir, { withFileTypes: true })
287
+ await Promise.all(
288
+ existingEntries
289
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
290
+ .map(async (entry) => {
291
+ const filePath = path.join(docsDir, entry.name)
292
+ if (desiredFiles.has(filePath)) {
293
+ return
294
+ }
295
+
296
+ await fs.unlink(filePath)
297
+ }),
298
+ )
299
+ }
300
+
301
+ if (import.meta.url === `file://${process.argv[1]}`) {
302
+ const checkOnly = process.argv.includes('--check')
303
+ const cwd = process.cwd()
304
+
305
+ updateComponentSkills(cwd, checkOnly).catch((error: unknown) => {
306
+ const message = error instanceof Error ? error.message : String(error)
307
+ console.error(message)
308
+ process.exitCode = 1
309
+ })
310
+ }