@vibecuting/component-project-helper 0.1.17 → 0.1.18
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 +4 -6
- package/package.json +7 -5
- package/scripts/package-json.mjs +1 -1
- package/scripts/update-component-skills.mjs +63 -0
- package/src/discovery/index.ts +19 -1
- package/src/index.ts +3 -0
- package/src/meta/update-component-skills.test.ts +66 -0
- package/src/meta/update-component-skills.ts +306 -0
package/README.md
CHANGED
|
@@ -9,11 +9,10 @@ NPM 地址:
|
|
|
9
9
|
职责:
|
|
10
10
|
|
|
11
11
|
- 作为可发布的 npm 包承载组件工程辅助逻辑。
|
|
12
|
-
- 提供组件注解、schema
|
|
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
|
-
-
|
|
15
|
-
-
|
|
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/
|
|
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.
|
|
3
|
+
"version": "0.1.18",
|
|
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": "
|
|
24
|
+
"update-component-skills": "node ./scripts/update-component-skills.mjs",
|
|
25
25
|
"lint": "node ../../../scripts/run-eslint.mjs .",
|
|
26
|
-
"typecheck": "
|
|
27
|
-
"test": "
|
|
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
|
-
"
|
|
36
|
+
"@testing-library/jest-dom": "^6.8.0",
|
|
37
|
+
"@types/node": "20.12.2",
|
|
38
|
+
"vitest": "^4.1.0"
|
|
37
39
|
}
|
|
38
40
|
}
|
package/scripts/package-json.mjs
CHANGED
|
@@ -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-
|
|
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
|
+
)
|
package/src/discovery/index.ts
CHANGED
|
@@ -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
|
@@ -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,306 @@
|
|
|
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 getMetaFilePath(projectRoot: string): string {
|
|
89
|
+
return path.join(projectRoot, '.vibecuting', 'video-resource-meta.generated.ts')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readGeneratedVideoResourceMeta(projectRoot: string): Promise<NonNullable<VideoResourceMetaFile['videoResourceMeta']>> {
|
|
93
|
+
const metaFilePath = getMetaFilePath(projectRoot)
|
|
94
|
+
const sourceText = await fs.readFile(metaFilePath, 'utf8')
|
|
95
|
+
const sourceFile = ts.createSourceFile(metaFilePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS)
|
|
96
|
+
|
|
97
|
+
for (const statement of sourceFile.statements) {
|
|
98
|
+
if (!ts.isVariableStatement(statement) || !statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword)) {
|
|
99
|
+
continue
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
103
|
+
if (!ts.isIdentifier(declaration.name) || declaration.name.text !== 'videoResourceMeta') {
|
|
104
|
+
continue
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (!declaration.initializer) {
|
|
108
|
+
throw new Error(`${metaFilePath}: missing videoResourceMeta initializer`)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const videoResourceMeta = evaluateStaticExpression(declaration.initializer, metaFilePath) as VideoResourceMetaFile['videoResourceMeta']
|
|
112
|
+
if (!videoResourceMeta) {
|
|
113
|
+
throw new Error(`${metaFilePath}: missing videoResourceMeta export`)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return videoResourceMeta
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
throw new Error(`${metaFilePath}: missing videoResourceMeta export`)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function renderSceneMetadata(record: VideoResourceMetaRecord): string {
|
|
124
|
+
const { metadata } = record
|
|
125
|
+
const slots = metadata.slots ? stringifySlots(metadata.slots) : '[]'
|
|
126
|
+
|
|
127
|
+
return [
|
|
128
|
+
`- resourceKind: ${metadata.resourceKind}`,
|
|
129
|
+
` aspectRatio: ${metadata.aspectRatio ?? ''}`,
|
|
130
|
+
` sceneType: ${metadata.sceneType ?? ''}`,
|
|
131
|
+
` sceneFamily: ${metadata.sceneFamily ?? ''}`,
|
|
132
|
+
` themePreset: ${metadata.themePreset ?? ''}`,
|
|
133
|
+
` rootLayout: ${metadata.rootLayout ?? ''}`,
|
|
134
|
+
` propsTypeName: ${metadata.propsTypeName ?? ''}`,
|
|
135
|
+
` slots:`,
|
|
136
|
+
slots,
|
|
137
|
+
].join('\n')
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function renderTransitionMetadata(record: VideoResourceMetaRecord): string {
|
|
141
|
+
const { metadata } = record
|
|
142
|
+
|
|
143
|
+
return [
|
|
144
|
+
`- resourceKind: ${metadata.resourceKind}`,
|
|
145
|
+
` transitionKind: ${metadata.transitionKind ?? ''}`,
|
|
146
|
+
].join('\n')
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function renderThemeMetadata(record: VideoResourceMetaRecord): string {
|
|
150
|
+
const { metadata } = record
|
|
151
|
+
const supportedSceneFamilies = metadata.supportedSceneFamilies ?? []
|
|
152
|
+
const cssVariables = metadata.cssVariables ?? []
|
|
153
|
+
|
|
154
|
+
return [
|
|
155
|
+
`- resourceKind: ${metadata.resourceKind}`,
|
|
156
|
+
` supportedSceneFamilies:`,
|
|
157
|
+
supportedSceneFamilies.length > 0 ? stringifyFrontmatterList(supportedSceneFamilies) : ' []',
|
|
158
|
+
` cssVariables:`,
|
|
159
|
+
cssVariables.length > 0 ? stringifyFrontmatterList(cssVariables) : ' []',
|
|
160
|
+
].join('\n')
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildDocContents(record: VideoResourceMetaRecord): string {
|
|
164
|
+
const { metadata, exportName } = record
|
|
165
|
+
const tags = metadata.tags ?? []
|
|
166
|
+
const bodyMetadata =
|
|
167
|
+
metadata.resourceKind === 'scene'
|
|
168
|
+
? renderSceneMetadata(record)
|
|
169
|
+
: metadata.resourceKind === 'transition'
|
|
170
|
+
? renderTransitionMetadata(record)
|
|
171
|
+
: renderThemeMetadata(record)
|
|
172
|
+
|
|
173
|
+
return [
|
|
174
|
+
'---',
|
|
175
|
+
`name: ${metadata.name}`,
|
|
176
|
+
`resourceKind: ${metadata.resourceKind}`,
|
|
177
|
+
`description: ${metadata.description}`,
|
|
178
|
+
`sourceFile: ${metadata.sourceFile}`,
|
|
179
|
+
`pluginKey: ${metadata.pluginKey}`,
|
|
180
|
+
`tags:`,
|
|
181
|
+
tags.length > 0 ? stringifyFrontmatterList(tags) : ' []',
|
|
182
|
+
'---',
|
|
183
|
+
'',
|
|
184
|
+
`# ${exportName}`,
|
|
185
|
+
'',
|
|
186
|
+
metadata.description,
|
|
187
|
+
'',
|
|
188
|
+
'## 元数据',
|
|
189
|
+
'',
|
|
190
|
+
`- 资源类型: \`${metadata.resourceKind}\``,
|
|
191
|
+
`- 源文件: \`${metadata.sourceFile}\``,
|
|
192
|
+
`- 插件键: \`${metadata.pluginKey}\``,
|
|
193
|
+
`- 标签: ${tags.length > 0 ? tags.map((tag) => `\`${tag}\``).join(', ') : '[]'}`,
|
|
194
|
+
'',
|
|
195
|
+
bodyMetadata,
|
|
196
|
+
'',
|
|
197
|
+
].join('\n')
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async function writeTextFile(filePath: string, contents: string): Promise<void> {
|
|
201
|
+
await fs.mkdir(path.dirname(filePath), { recursive: true })
|
|
202
|
+
const tempPath = `${filePath}.tmp`
|
|
203
|
+
await fs.writeFile(tempPath, `${contents}\n`, 'utf8')
|
|
204
|
+
await fs.rename(tempPath, filePath)
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function readOptionalFile(filePath: string): Promise<string | undefined> {
|
|
208
|
+
try {
|
|
209
|
+
return await fs.readFile(filePath, 'utf8')
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') {
|
|
212
|
+
return undefined
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
throw error
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
export async function updateComponentSkills(
|
|
220
|
+
projectRoot: string,
|
|
221
|
+
checkOnly = false,
|
|
222
|
+
): Promise<void> {
|
|
223
|
+
const videoResourceMeta = await readGeneratedVideoResourceMeta(projectRoot)
|
|
224
|
+
const docsDir = resolveComponentProjectGeneratedDocsDir(projectRoot)
|
|
225
|
+
const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
|
|
226
|
+
const resources = [...videoResourceMeta.resources].sort((left, right) => {
|
|
227
|
+
return (
|
|
228
|
+
left.metadata.resourceKind.localeCompare(right.metadata.resourceKind) ||
|
|
229
|
+
left.metadata.pluginKey.localeCompare(right.metadata.pluginKey) ||
|
|
230
|
+
left.exportName.localeCompare(right.exportName)
|
|
231
|
+
)
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
const generatedDocs: GeneratedSkillDoc[] = resources.map((record) => {
|
|
235
|
+
const filePath = path.join(docsDir, `${sanitizeFileName(record.exportName)}.md`)
|
|
236
|
+
return { filePath, contents: buildDocContents(record) }
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
const manifestContents = JSON.stringify(
|
|
240
|
+
{
|
|
241
|
+
projectRoot,
|
|
242
|
+
docsDir,
|
|
243
|
+
sourceMetaFile: getMetaFilePath(projectRoot),
|
|
244
|
+
packageFingerprint: videoResourceMeta.packageFingerprint ?? null,
|
|
245
|
+
files: generatedDocs.map((doc) => doc.filePath),
|
|
246
|
+
resources: resources.map((resource) => ({
|
|
247
|
+
packageName: resource.packageName,
|
|
248
|
+
exportName: resource.exportName,
|
|
249
|
+
resourceKind: resource.metadata.resourceKind,
|
|
250
|
+
pluginKey: resource.metadata.pluginKey,
|
|
251
|
+
})),
|
|
252
|
+
},
|
|
253
|
+
null,
|
|
254
|
+
2,
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
if (checkOnly) {
|
|
258
|
+
const currentManifest = await readOptionalFile(manifestPath)
|
|
259
|
+
if (currentManifest?.trimEnd() !== `${manifestContents}\n`.trimEnd()) {
|
|
260
|
+
throw new Error('Generated component skill manifest is stale. Run pnpm run update-component-skills')
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
for (const doc of generatedDocs) {
|
|
264
|
+
const currentDoc = await readOptionalFile(doc.filePath)
|
|
265
|
+
if (currentDoc?.trimEnd() !== doc.contents.trimEnd()) {
|
|
266
|
+
throw new Error(`Generated component skill doc is stale: ${doc.filePath}`)
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await writeTextFile(manifestPath, manifestContents)
|
|
274
|
+
await fs.mkdir(docsDir, { recursive: true })
|
|
275
|
+
|
|
276
|
+
const desiredFiles = new Set(generatedDocs.map((doc) => doc.filePath))
|
|
277
|
+
|
|
278
|
+
await Promise.all(
|
|
279
|
+
generatedDocs.map((doc) => writeTextFile(doc.filePath, doc.contents)),
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
const existingEntries = await fs.readdir(docsDir, { withFileTypes: true })
|
|
283
|
+
await Promise.all(
|
|
284
|
+
existingEntries
|
|
285
|
+
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
286
|
+
.map(async (entry) => {
|
|
287
|
+
const filePath = path.join(docsDir, entry.name)
|
|
288
|
+
if (desiredFiles.has(filePath)) {
|
|
289
|
+
return
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
await fs.unlink(filePath)
|
|
293
|
+
}),
|
|
294
|
+
)
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
298
|
+
const checkOnly = process.argv.includes('--check')
|
|
299
|
+
const cwd = process.cwd()
|
|
300
|
+
|
|
301
|
+
updateComponentSkills(cwd, checkOnly).catch((error: unknown) => {
|
|
302
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
303
|
+
console.error(message)
|
|
304
|
+
process.exitCode = 1
|
|
305
|
+
})
|
|
306
|
+
}
|