@vibecuting/component-project-helper 0.1.16 → 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.
@@ -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
+ }
@@ -0,0 +1,47 @@
1
+ import { createRequire } from 'node:module'
2
+ import type {
3
+ ScenePluginMetadata,
4
+ ThemePluginMetadata,
5
+ TransitionPluginMetadata,
6
+ VideoResourceDescriptor,
7
+ VideoResourceMetadata,
8
+ } from './video-resource.ts'
9
+ import {
10
+ ScenePluginMetadataSchema,
11
+ ThemePluginMetadataSchema,
12
+ TransitionPluginMetadataSchema,
13
+ } from './video-resource.ts'
14
+
15
+ const require = createRequire(import.meta.url)
16
+ const videoResourceDescriptorDefinitionRows = require('./video-resource-descriptors.json')
17
+
18
+ export const videoResourceDescriptorDefinitions = videoResourceDescriptorDefinitionRows
19
+
20
+ export type VideoResourceDescriptorDefinition = (typeof videoResourceDescriptorDefinitions)[number]
21
+
22
+ export const sceneResourceDescriptor: VideoResourceDescriptor<ScenePluginMetadata> = {
23
+ kind: 'scene',
24
+ annotationName: 'VideoComponent',
25
+ metadataFactoryName: 'defineScenePluginMetadata',
26
+ schema: ScenePluginMetadataSchema,
27
+ }
28
+
29
+ export const transitionResourceDescriptor: VideoResourceDescriptor<TransitionPluginMetadata> = {
30
+ kind: 'transition',
31
+ annotationName: 'TransitionComponent',
32
+ metadataFactoryName: 'defineTransitionPluginMetadata',
33
+ schema: TransitionPluginMetadataSchema,
34
+ }
35
+
36
+ export const themeResourceDescriptor: VideoResourceDescriptor<ThemePluginMetadata> = {
37
+ kind: 'theme',
38
+ annotationName: 'ThemeComponent',
39
+ metadataFactoryName: 'defineThemePluginMetadata',
40
+ schema: ThemePluginMetadataSchema,
41
+ }
42
+
43
+ export const videoResourceDescriptors = [
44
+ sceneResourceDescriptor,
45
+ transitionResourceDescriptor,
46
+ themeResourceDescriptor,
47
+ ] as const satisfies readonly VideoResourceDescriptor<VideoResourceMetadata>[]
@@ -0,0 +1,17 @@
1
+ [
2
+ {
3
+ "kind": "scene",
4
+ "annotationName": "VideoComponent",
5
+ "metadataFactoryName": "defineScenePluginMetadata"
6
+ },
7
+ {
8
+ "kind": "transition",
9
+ "annotationName": "TransitionComponent",
10
+ "metadataFactoryName": "defineTransitionPluginMetadata"
11
+ },
12
+ {
13
+ "kind": "theme",
14
+ "annotationName": "ThemeComponent",
15
+ "metadataFactoryName": "defineThemePluginMetadata"
16
+ }
17
+ ]
@@ -0,0 +1,143 @@
1
+ import 'reflect-metadata'
2
+
3
+ import { z } from 'zod'
4
+
5
+ export const VideoResourceKindSchema = z.enum(['scene', 'transition', 'theme'])
6
+
7
+ export type VideoResourceKind = z.infer<typeof VideoResourceKindSchema>
8
+
9
+ export const BaseVideoResourceMetadataSchema = z.object({
10
+ resourceKind: VideoResourceKindSchema,
11
+ name: z.string().min(1),
12
+ description: z.string().min(1),
13
+ sourceFile: z.string().min(1),
14
+ pluginKey: z.string().min(1),
15
+ tags: z.array(z.string().min(1)).default([]),
16
+ })
17
+
18
+ export const SceneSlotHintSchema = z.object({
19
+ name: z.string().min(1),
20
+ description: z.string().min(1).optional(),
21
+ required: z.boolean().optional(),
22
+ accepts: z.enum(['text', 'image', 'video', 'component', 'any']).optional(),
23
+ })
24
+
25
+ export const SceneSlotsSchema = z.union([
26
+ z.array(z.string().min(1)),
27
+ z.array(SceneSlotHintSchema),
28
+ ])
29
+
30
+ const SceneFamilySchema = z.enum(['slide', 'canvas-2d', 'game-2d', 'three-3d', 'custom'])
31
+
32
+ export const ScenePluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({
33
+ resourceKind: z.literal('scene'),
34
+ aspectRatio: z.enum(['9:16', '16:9']),
35
+ sceneType: z.enum(['intro', 'scene', 'outro']).optional(),
36
+ sceneFamily: SceneFamilySchema.default('custom'),
37
+ themePreset: z.string().min(1).optional(),
38
+ slots: SceneSlotsSchema.optional(),
39
+ propsTypeName: z.string().min(1).optional(),
40
+ rootLayout: z.literal('absolute-fill').default('absolute-fill'),
41
+ })
42
+
43
+ export const TransitionPluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({
44
+ resourceKind: z.literal('transition'),
45
+ transitionKind: z.enum(['transition', 'overlay']),
46
+ })
47
+
48
+ export const ThemePluginMetadataSchema = BaseVideoResourceMetadataSchema.extend({
49
+ resourceKind: z.literal('theme'),
50
+ supportedSceneFamilies: z.array(SceneFamilySchema).default([]),
51
+ cssVariables: z.array(z.string().regex(/^--scene-/)).default([]),
52
+ })
53
+
54
+ export const VideoResourceMetadataSchema = z.discriminatedUnion('resourceKind', [
55
+ ScenePluginMetadataSchema,
56
+ TransitionPluginMetadataSchema,
57
+ ThemePluginMetadataSchema,
58
+ ])
59
+
60
+ export type ScenePluginMetadata = z.infer<typeof ScenePluginMetadataSchema>
61
+ export type SceneSlotHint = z.infer<typeof SceneSlotHintSchema>
62
+ export type TransitionPluginMetadata = z.infer<typeof TransitionPluginMetadataSchema>
63
+ export type ThemePluginMetadata = z.infer<typeof ThemePluginMetadataSchema>
64
+ export type VideoResourceMetadata = z.infer<typeof VideoResourceMetadataSchema>
65
+
66
+ export const VIDEO_RESOURCE_METADATA_KEY = Symbol.for(
67
+ '@vibecuting/component-project-helper/video-resource-metadata',
68
+ )
69
+
70
+ export type VideoResourceDescriptor<TMetadata extends VideoResourceMetadata> = {
71
+ kind: TMetadata['resourceKind']
72
+ annotationName: string
73
+ metadataFactoryName: string
74
+ schema: z.ZodType<TMetadata>
75
+ }
76
+
77
+ export type VideoResourceAnnotation<TMetadata extends VideoResourceMetadata> = {
78
+ defineMetadata(input: TMetadata): TMetadata
79
+ annotate(metadata: TMetadata): <T extends object>(target: T) => T
80
+ getMetadata(target: unknown): TMetadata | undefined
81
+ }
82
+
83
+ export function createVideoResourceAnnotation<TMetadata extends VideoResourceMetadata>(
84
+ descriptor: VideoResourceDescriptor<TMetadata>,
85
+ ): VideoResourceAnnotation<TMetadata> {
86
+ return {
87
+ defineMetadata(input) {
88
+ return descriptor.schema.parse(input)
89
+ },
90
+ annotate(metadata) {
91
+ const normalized = descriptor.schema.parse(metadata)
92
+
93
+ return function annotateTarget<T extends object>(target: T): T {
94
+ Reflect.defineMetadata(VIDEO_RESOURCE_METADATA_KEY, normalized, target)
95
+ return target
96
+ }
97
+ },
98
+ getMetadata(target) {
99
+ if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
100
+ return undefined
101
+ }
102
+
103
+ const metadata = Reflect.getMetadata(VIDEO_RESOURCE_METADATA_KEY, target)
104
+ const parsed = descriptor.schema.safeParse(metadata)
105
+ return parsed.success ? parsed.data : undefined
106
+ },
107
+ }
108
+ }
109
+
110
+ export function getVideoResourceMetadata<TMetadata extends VideoResourceMetadata>(
111
+ target: unknown,
112
+ expectedKind?: TMetadata['resourceKind'],
113
+ ): TMetadata | undefined {
114
+ if (!target || (typeof target !== 'object' && typeof target !== 'function')) {
115
+ return undefined
116
+ }
117
+
118
+ const metadata = Reflect.getMetadata(VIDEO_RESOURCE_METADATA_KEY, target)
119
+ const parsed = VideoResourceMetadataSchema.safeParse(metadata)
120
+ if (!parsed.success) {
121
+ return undefined
122
+ }
123
+
124
+ if (expectedKind && parsed.data.resourceKind !== expectedKind) {
125
+ return undefined
126
+ }
127
+
128
+ return parsed.data as TMetadata
129
+ }
130
+
131
+ export function defineScenePluginMetadata(input: ScenePluginMetadata): ScenePluginMetadata {
132
+ return ScenePluginMetadataSchema.parse(input)
133
+ }
134
+
135
+ export function defineTransitionPluginMetadata(
136
+ input: TransitionPluginMetadata,
137
+ ): TransitionPluginMetadata {
138
+ return TransitionPluginMetadataSchema.parse(input)
139
+ }
140
+
141
+ export function defineThemePluginMetadata(input: ThemePluginMetadata): ThemePluginMetadata {
142
+ return ThemePluginMetadataSchema.parse(input)
143
+ }
@@ -1,6 +1,25 @@
1
1
  import { z } from 'zod'
2
2
 
3
- export const ComponentProjectComponentMetadataSchema = z.object({
3
+ export {
4
+ BaseVideoResourceMetadataSchema,
5
+ ScenePluginMetadataSchema,
6
+ SceneSlotHintSchema,
7
+ SceneSlotsSchema,
8
+ ThemePluginMetadataSchema,
9
+ TransitionPluginMetadataSchema,
10
+ VideoResourceKindSchema,
11
+ VideoResourceMetadataSchema,
12
+ } from '../resources/video-resource.ts'
13
+ export type {
14
+ ScenePluginMetadata,
15
+ SceneSlotHint,
16
+ ThemePluginMetadata,
17
+ TransitionPluginMetadata,
18
+ VideoResourceKind,
19
+ VideoResourceMetadata,
20
+ } from '../resources/video-resource.ts'
21
+
22
+ const ComponentProjectComponentMetadataInputSchema = z.object({
4
23
  name: z.string().min(1),
5
24
  description: z.string().min(1),
6
25
  sourceFile: z.string().min(1),
@@ -10,6 +29,9 @@ export const ComponentProjectComponentMetadataSchema = z.object({
10
29
  propsTypeName: z.string().min(1).optional(),
11
30
  })
12
31
 
32
+ export const ComponentProjectComponentMetadataSchema =
33
+ ComponentProjectComponentMetadataInputSchema
34
+
13
35
  export const ComponentProjectGeneratedManifestSchema = z.object({
14
36
  projectRoot: z.string().min(1),
15
37
  generatedAt: z.string().min(1),
@@ -18,7 +40,7 @@ export const ComponentProjectGeneratedManifestSchema = z.object({
18
40
  })
19
41
 
20
42
  export type ComponentProjectComponentMetadata = z.infer<
21
- typeof ComponentProjectComponentMetadataSchema
43
+ typeof ComponentProjectComponentMetadataInputSchema
22
44
  >
23
45
  export type ComponentProjectGeneratedManifest = z.infer<
24
46
  typeof ComponentProjectGeneratedManifestSchema
@@ -1,112 +0,0 @@
1
- import fs from 'node:fs/promises'
2
- import path from 'node:path'
3
-
4
- import { discoverComponentProjectComponents } from './discover-components.mjs'
5
- import { ensureComponentProjectSkillsScript } from './package-json.mjs'
6
- import {
7
- resolveComponentProjectGeneratedDocsDir,
8
- resolveComponentProjectGeneratedManifestPath,
9
- resolveComponentProjectInstallRoot,
10
- } from './runtime-root.mjs'
11
- import { renderComponentProjectMarkdown } from './render-markdown.mjs'
12
- import { ComponentProjectGeneratedManifestSchema } from './schemas.mjs'
13
-
14
- const supportsColor =
15
- Boolean(process.stdout.isTTY) && process.env.NO_COLOR !== '1' && process.env.CI !== 'true'
16
-
17
- const color = {
18
- cyan: (value) => (supportsColor ? `\u001b[36m${value}\u001b[0m` : value),
19
- green: (value) => (supportsColor ? `\u001b[32m${value}\u001b[0m` : value),
20
- yellow: (value) => (supportsColor ? `\u001b[33m${value}\u001b[0m` : value),
21
- dim: (value) => (supportsColor ? `\u001b[2m${value}\u001b[0m` : value),
22
- }
23
-
24
- async function ensureDirectory(dir) {
25
- await fs.mkdir(dir, { recursive: true })
26
- }
27
-
28
- async function removeStaleGeneratedFiles(manifestPath, nextFiles) {
29
- try {
30
- const previous = ComponentProjectGeneratedManifestSchema.parse(
31
- JSON.parse(await fs.readFile(manifestPath, 'utf8')),
32
- )
33
- const staleFiles = previous.files.filter((filePath) => !nextFiles.includes(filePath))
34
- await Promise.all(staleFiles.map((filePath) => fs.rm(filePath, { force: true })))
35
- } catch (error) {
36
- if (error?.code !== 'ENOENT') {
37
- throw error
38
- }
39
- }
40
- }
41
-
42
- export async function runComponentProjectPostinstall(projectRoot = resolveComponentProjectInstallRoot()) {
43
- const docsDir = resolveComponentProjectGeneratedDocsDir(projectRoot)
44
- const manifestPath = resolveComponentProjectGeneratedManifestPath(projectRoot)
45
- const components = await discoverComponentProjectComponents(projectRoot)
46
-
47
- console.log(color.cyan('[component-project-helper] postinstall start'))
48
- console.log(color.dim(`[component-project-helper] project root: ${projectRoot}`))
49
- console.log(color.dim(`[component-project-helper] docs dir: ${docsDir}`))
50
- console.log(color.dim(`[component-project-helper] manifest path: ${manifestPath}`))
51
- console.log(
52
- color.green(
53
- `[component-project-helper] discovered ${components.length} component(s): ${
54
- components.map((component) => component.name).join(', ') || '(none)'
55
- }`,
56
- ),
57
- )
58
-
59
- await ensureDirectory(docsDir)
60
-
61
- const files = []
62
- for (const component of components) {
63
- const filePath = path.join(docsDir, `${component.name}.md`)
64
- await fs.writeFile(filePath, renderComponentProjectMarkdown(component), 'utf8')
65
- files.push(filePath)
66
- console.log(
67
- color.green(
68
- `[component-project-helper] wrote ${path.relative(projectRoot, filePath)} <- ${component.sourceFile}`,
69
- ),
70
- )
71
- }
72
-
73
- console.log(
74
- color.yellow(
75
- `[component-project-helper] cleaning stale docs relative to manifest ${path.relative(
76
- projectRoot,
77
- manifestPath,
78
- )}`,
79
- ),
80
- )
81
- await removeStaleGeneratedFiles(manifestPath, files)
82
- await ensureComponentProjectSkillsScript(projectRoot)
83
- console.log(color.green('[component-project-helper] ensured update-component-skills script'))
84
- console.log('[component-project-helper] ensured stale docs cleaned')
85
-
86
- const manifest = ComponentProjectGeneratedManifestSchema.parse({
87
- projectRoot,
88
- generatedAt: new Date().toISOString(),
89
- docsDir,
90
- files,
91
- })
92
- await fs.writeFile(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8')
93
- console.log(
94
- color.cyan(
95
- `[component-project-helper] postinstall complete, manifest files: ${files.length}`,
96
- ),
97
- )
98
- }
99
-
100
- async function isDirectRun() {
101
- if (!process.argv[1]) {
102
- return false
103
- }
104
-
105
- const entryPath = await fs.realpath(process.argv[1])
106
- const currentPath = await fs.realpath(new URL(import.meta.url).pathname)
107
- return entryPath === currentPath
108
- }
109
-
110
- if (await isDirectRun()) {
111
- await runComponentProjectPostinstall()
112
- }