@meistrari/tela-build 1.30.3 → 1.30.4

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.
@@ -26,7 +26,7 @@ describe('generateDocsToDirectory', () => {
26
26
 
27
27
  expect(parsed.data).toMatchObject({
28
28
  name: 'tela-build',
29
- description: expect.stringContaining('repository: ask for component props'),
29
+ description: expect.stringContaining('Ask for component props'),
30
30
  })
31
31
  expect(skillMd).toContain('description: "')
32
32
  })
@@ -7,6 +7,7 @@ import { getTypeResolver } from './type-resolver'
7
7
  import type { TypeResolver } from './type-resolver'
8
8
  import { createVolarExtractor } from './extractors/volar-extract'
9
9
  import type { VolarExtractor } from './extractors/volar-extract'
10
+ import { telaBuildSkill } from './tela-build-skill'
10
11
 
11
12
  const colors = {
12
13
  gray: '\x1B[90m',
@@ -516,7 +517,7 @@ export function generateDocsToDirectory(componentDocs: ComponentDoc[], typeResol
516
517
  }
517
518
 
518
519
  // Single Skill: tela-build
519
- const skillDir = join(outDir, 'tela-build')
520
+ const skillDir = join(outDir, telaBuildSkill.name)
520
521
  const supportingDir = join(skillDir, 'components')
521
522
  if (!existsSync(supportingDir)) {
522
523
  mkdirSync(supportingDir, { recursive: true })
@@ -562,27 +563,21 @@ export function generateDocsToDirectory(componentDocs: ComponentDoc[], typeResol
562
563
  }
563
564
 
564
565
  // Create SKILL.md describing Tela Build with links to supporting md
565
- const skillDescription = buildTelaBuildSkillDescription()
566
566
  const body = dedent`
567
567
  # Tela Build
568
568
 
569
569
  This Skill provides structured documentation for the Tela Build component library (Vue 3 + Nuxt).
570
570
  Use it when building, refactoring, or using Tela components — props, events, slots, and examples are included where available.
571
571
 
572
- ## Instructions
573
-
574
- - IMPORTANT: Before making ANY UI change or introducing NEW UI, ALWAYS consult this Tela Build components index first.
575
- - Prefer reusing existing Tela components and patterns. Only create new components when a clear gap exists.
576
- - When a new component is needed, align with existing naming, props, events, and patterns documented here.
577
- - Keep this documentation current when components are added or changed (update supporting files under \
578
- \`components/\`).
579
-
580
572
  ## Components Index
581
573
 
582
574
  ${componentLinks.join('\n')}${docsSection}
583
575
  `
584
576
 
585
- const skillMd = wrapWithSkillFrontmatter({ name: 'tela-build', description: skillDescription }, body)
577
+ const skillMd = wrapWithSkillFrontmatter({
578
+ name: telaBuildSkill.name,
579
+ description: telaBuildSkill.description,
580
+ }, body)
586
581
  writeFileSync(join(skillDir, 'SKILL.md'), skillMd, 'utf-8')
587
582
  }
588
583
 
@@ -691,7 +686,7 @@ function wrapWithSkillFrontmatter(meta: { name: string, description: string, all
691
686
  const lines: string[] = []
692
687
  lines.push('---')
693
688
  lines.push(`name: ${sanitizeSkillName(name)}`)
694
- lines.push(`description: ${serializeYamlString(sanitizeDescription(description))}`)
689
+ lines.push(`description: ${quoteFrontmatterString(sanitizeDescription(description))}`)
695
690
  if (allowedTools && allowedTools.length > 0) {
696
691
  lines.push(`allowed-tools: ${allowedTools.join(', ')}`)
697
692
  }
@@ -703,26 +698,18 @@ function wrapWithSkillFrontmatter(meta: { name: string, description: string, all
703
698
  return lines.join('\n')
704
699
  }
705
700
 
706
- function serializeYamlString(value: string): string {
707
- return JSON.stringify(value)
708
- }
709
-
710
701
  function sanitizeSkillName(name: string): string {
711
702
  // Lowercase + hyphens + digits only, max 64 chars
712
703
  return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').slice(0, 64).replace(/-{2,}/g, '-')
713
704
  }
714
705
 
715
706
  function sanitizeDescription(desc: string): string {
716
- const trimmed = desc.replace(/\s+/g, ' ').trim()
707
+ const trimmed = desc.replace(/\r\n?/g, '\n').trim()
717
708
  return trimmed.slice(0, 1024)
718
709
  }
719
710
 
720
- function buildTelaBuildSkillDescription(): string {
721
- const base = 'Documentation and usage references for the Tela Build component library (Vue 3 + Nuxt). '
722
- + 'Always consult this Tela Build index BEFORE making any UI changes or creating new UI. '
723
- + 'Use when working on UI in this repository: ask for component props, events, slots, or examples. '
724
- + 'Supporting files under components/ contain per-component details.'
725
- return sanitizeDescription(base)
711
+ function quoteFrontmatterString(value: string): string {
712
+ return JSON.stringify(value)
726
713
  }
727
714
 
728
715
  function toGroupSlugFromKebab(kebab: string): string {
@@ -0,0 +1,8 @@
1
+ export const telaBuildSkill = {
2
+ name: 'tela-build',
3
+ description: [
4
+ 'Use when working on any UI in this repository. Ask for component props, slots, variants, and usage examples.',
5
+ 'Always consult this Tela Build index BEFORE making any UI changes or creating new UI.',
6
+ 'Use it for components, layouts, styling, interactions, and design-token decisions.',
7
+ ].join('\n'),
8
+ } as const
@@ -0,0 +1,122 @@
1
+ import {
2
+ existsSync,
3
+ lstatSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readlinkSync,
7
+ rmSync,
8
+ symlinkSync,
9
+ writeFileSync,
10
+ readFileSync,
11
+ } from 'node:fs'
12
+ import { tmpdir } from 'node:os'
13
+ import { join } from 'pathe'
14
+ import { afterEach, describe, expect, it, vi } from 'vitest'
15
+
16
+ import { ensureClaudeSymlink } from '../index'
17
+
18
+ const tempDirs: string[] = []
19
+ const logger = { log: vi.fn() }
20
+ const colors = { gray: '', orange: '', green: '', cyan: '', yellow: '', purple: '', reset: '' }
21
+
22
+ function makeTempRoot() {
23
+ const dir = mkdtempSync(join(tmpdir(), 'claude-symlink-'))
24
+ tempDirs.push(dir)
25
+
26
+ return dir
27
+ }
28
+
29
+ afterEach(() => {
30
+ for (const dir of tempDirs.splice(0)) {
31
+ rmSync(dir, { recursive: true, force: true })
32
+ }
33
+
34
+ logger.log.mockClear()
35
+ })
36
+
37
+ describe('ensureClaudeSymlink', () => {
38
+ it('creates .agents and symlinks .claude → .agents when neither exists', () => {
39
+ const root = makeTempRoot()
40
+
41
+ ensureClaudeSymlink(root, logger, colors)
42
+
43
+ expect(existsSync(join(root, '.agents'))).toBe(true)
44
+ expect(lstatSync(join(root, '.claude')).isSymbolicLink()).toBe(true)
45
+ expect(readlinkSync(join(root, '.claude'))).toBe('.agents')
46
+ })
47
+
48
+ it('creates .claude symlink when .agents already exists', () => {
49
+ const root = makeTempRoot()
50
+
51
+ mkdirSync(join(root, '.agents'))
52
+
53
+ ensureClaudeSymlink(root, logger, colors)
54
+
55
+ expect(lstatSync(join(root, '.claude')).isSymbolicLink()).toBe(true)
56
+ expect(readlinkSync(join(root, '.claude'))).toBe('.agents')
57
+ })
58
+
59
+ it('does nothing when .claude is already a symlink', () => {
60
+ const root = makeTempRoot()
61
+
62
+ mkdirSync(join(root, '.agents'))
63
+ symlinkSync('.agents', join(root, '.claude'))
64
+
65
+ ensureClaudeSymlink(root, logger, colors)
66
+
67
+ expect(lstatSync(join(root, '.claude')).isSymbolicLink()).toBe(true)
68
+ expect(logger.log).not.toHaveBeenCalled()
69
+ })
70
+
71
+ it('symlinks .claude/skills → .agents/skills when .claude is a real directory', () => {
72
+ const root = makeTempRoot()
73
+
74
+ mkdirSync(join(root, '.agents/skills'), { recursive: true })
75
+ mkdirSync(join(root, '.claude'), { recursive: true })
76
+
77
+ ensureClaudeSymlink(root, logger, colors)
78
+
79
+ const skillsLink = join(root, '.claude/skills')
80
+
81
+ expect(lstatSync(skillsLink).isSymbolicLink()).toBe(true)
82
+ expect(existsSync(skillsLink)).toBe(true)
83
+ })
84
+
85
+ it('skips skill symlink when .claude/skills already exists', () => {
86
+ const root = makeTempRoot()
87
+
88
+ mkdirSync(join(root, '.agents/skills'), { recursive: true })
89
+ mkdirSync(join(root, '.claude/skills'), { recursive: true })
90
+
91
+ ensureClaudeSymlink(root, logger, colors)
92
+
93
+ expect(lstatSync(join(root, '.claude/skills')).isDirectory()).toBe(true)
94
+ expect(lstatSync(join(root, '.claude/skills')).isSymbolicLink()).toBe(false)
95
+ })
96
+
97
+ it('creates .agents/skills when .claude is a directory but .agents/skills does not exist', () => {
98
+ const root = makeTempRoot()
99
+
100
+ mkdirSync(join(root, '.agents'), { recursive: true })
101
+ mkdirSync(join(root, '.claude'), { recursive: true })
102
+
103
+ ensureClaudeSymlink(root, logger, colors)
104
+
105
+ expect(existsSync(join(root, '.agents/skills'))).toBe(true)
106
+ expect(lstatSync(join(root, '.claude/skills')).isSymbolicLink()).toBe(true)
107
+ })
108
+
109
+ it('skill files in .agents/skills are accessible through .claude/skills symlink', () => {
110
+ const root = makeTempRoot()
111
+
112
+ mkdirSync(join(root, '.agents/skills/tela-build'), { recursive: true })
113
+ writeFileSync(join(root, '.agents/skills/tela-build/SKILL.md'), '# Test')
114
+ mkdirSync(join(root, '.claude'), { recursive: true })
115
+
116
+ ensureClaudeSymlink(root, logger, colors)
117
+
118
+ const content = readFileSync(join(root, '.claude/skills/tela-build/SKILL.md'), 'utf-8')
119
+
120
+ expect(content).toBe('# Test')
121
+ })
122
+ })
@@ -1,9 +1,9 @@
1
1
  /* eslint-disable no-console */
2
- import { execSync } from 'node:child_process'
3
- import { existsSync, writeFileSync, mkdirSync } from 'node:fs'
2
+ import { existsSync, writeFileSync, mkdirSync, lstatSync, readFileSync, symlinkSync } from 'node:fs'
4
3
  import { defineNuxtModule, createResolver } from '@nuxt/kit'
5
- import { resolve } from 'pathe'
4
+ import { dirname, join, relative, resolve } from 'pathe'
6
5
  import { collectComponentDocs, generateMarkdown, generateDocsToDirectory } from '../../lib/doc-generator'
6
+ import { telaBuildSkill } from '../../lib/tela-build-skill'
7
7
  import { ensureOverlayTsconfig } from '../../lib/extractors/volar-extract'
8
8
 
9
9
  export interface TelaBuildDocsOptions {
@@ -13,6 +13,11 @@ export interface TelaBuildDocsOptions {
13
13
  enabled?: boolean
14
14
  }
15
15
 
16
+ interface IgnoreTarget {
17
+ path: string
18
+ kind: 'dir' | 'file'
19
+ }
20
+
16
21
  export default defineNuxtModule<TelaBuildDocsOptions>({
17
22
  meta: {
18
23
  name: 'tela-build-docs',
@@ -20,8 +25,8 @@ export default defineNuxtModule<TelaBuildDocsOptions>({
20
25
  },
21
26
  defaults: {
22
27
  layer: undefined,
23
- // Prefer directory output suitable for Claude Skills-style indexing
24
- outDir: '.claude/skills',
28
+ // When omitted, docs are written to `.agents/skills` and `.claude` is linked to `.agents`
29
+ outDir: undefined,
25
30
  // Keep outFile for backward compatibility (used only if explicitly set)
26
31
  outFile: undefined,
27
32
  enabled: true,
@@ -67,6 +72,16 @@ export default defineNuxtModule<TelaBuildDocsOptions>({
67
72
  // Run in background without blocking
68
73
  setImmediate(async () => {
69
74
  try {
75
+ const repositoryRoot = resolveRepositoryRoot(nuxt.options.rootDir)
76
+ try {
77
+ if (!options.outDir) {
78
+ ensureClaudeSymlink(repositoryRoot, logger, colors)
79
+ }
80
+ }
81
+ catch (symlinkError: any) {
82
+ logger.log(`${colors.gray}[tela/build] ${colors.orange}✗${colors.gray} Could not create .claude symlink: ${symlinkError.message}${colors.reset}`)
83
+ }
84
+
70
85
  // Prepare tsconfig overlay (safe even if Volar is disabled; creation is idempotent)
71
86
  try {
72
87
  if (nuxt.options.rootDir) {
@@ -83,24 +98,31 @@ export default defineNuxtModule<TelaBuildDocsOptions>({
83
98
  // Generate documentation (directory-first unless outFile explicitly set)
84
99
  logger.log(`${colors.gray}[tela/build] ${colors.orange}◐${colors.gray} Generating documentation for ${componentDocs.length} components${colors.reset}`)
85
100
 
86
- const baseDir = findGitRoot(nuxt.options.rootDir) ?? nuxt.options.rootDir
101
+ const baseDir = repositoryRoot
87
102
 
88
103
  if (options.outFile) {
89
104
  // Single-file mode (legacy)
90
- const outPath = resolve(baseDir, options.outFile)
105
+ const outPath = resolve(nuxt.options.rootDir, options.outFile)
91
106
  const outDir = resolve(outPath, '..')
92
107
  if (!existsSync(outDir)) {
93
108
  mkdirSync(outDir, { recursive: true })
94
109
  }
95
110
  const markdown = generateMarkdown(componentDocs, typeResolver)
96
111
  writeFileSync(outPath, markdown, 'utf-8')
112
+ // Skip auto-gitignore for legacy outFile — the path is user-managed and may be intentionally tracked
97
113
  logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Documentation complete → ${outPath}${colors.reset}`)
98
114
  }
99
115
  else {
100
116
  // Directory output: single Skill (tela-build) + supporting component pages
101
- const outDir = resolve(baseDir, options.outDir!)
102
- generateDocsToDirectory(componentDocs, typeResolver, outDir, layerPath)
103
- logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Documentation complete → ${outDir}${colors.reset}`)
117
+ const outputDirs = resolveOutputDirectories(baseDir, options.outDir)
118
+ for (const dir of outputDirs) {
119
+ generateDocsToDirectory(componentDocs, typeResolver, dir, layerPath)
120
+ }
121
+ ensurePathsIgnored(baseDir, outputDirs.map(dir => ({
122
+ path: join(dir, telaBuildSkill.name),
123
+ kind: 'dir' as const,
124
+ })))
125
+ logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Documentation complete → ${outputDirs.join(', ')}${colors.reset}`)
104
126
  }
105
127
  }
106
128
  catch (error: any) {
@@ -127,15 +149,6 @@ export default defineNuxtModule<TelaBuildDocsOptions>({
127
149
  },
128
150
  })
129
151
 
130
- function findGitRoot(cwd: string): string | null {
131
- try {
132
- return execSync('git rev-parse --show-toplevel', { cwd, encoding: 'utf-8' }).trim()
133
- }
134
- catch {
135
- return null
136
- }
137
- }
138
-
139
152
  function resolveLayerPath(): string | null {
140
153
  // Use createResolver to resolve paths relative to this module
141
154
  // This works correctly even when the module is installed from npm/github
@@ -153,3 +166,129 @@ function resolveLayerPath(): string | null {
153
166
 
154
167
  return null
155
168
  }
169
+
170
+ function resolveOutputDirectories(repositoryRoot: string, outDir?: string): string[] {
171
+ if (outDir) {
172
+ return [resolve(repositoryRoot, outDir)]
173
+ }
174
+
175
+ return [resolve(repositoryRoot, '.agents/skills')]
176
+ }
177
+
178
+ function resolveRepositoryRoot(startDir: string): string {
179
+ let currentDir = resolve(startDir)
180
+
181
+ while (true) {
182
+ if (existsSync(resolve(currentDir, '.git')) || existsSync(resolve(currentDir, 'pnpm-workspace.yaml'))) {
183
+ return currentDir
184
+ }
185
+
186
+ const parentDir = dirname(currentDir)
187
+ if (parentDir === currentDir) {
188
+ return resolve(startDir)
189
+ }
190
+
191
+ currentDir = parentDir
192
+ }
193
+ }
194
+
195
+ function ensurePathsIgnored(repositoryRoot: string, targets: IgnoreTarget[]): void {
196
+ const gitignorePath = resolve(repositoryRoot, '.gitignore')
197
+ const existingLines = existsSync(gitignorePath)
198
+ ? readFileSync(gitignorePath, 'utf-8').split('\n')
199
+ : []
200
+ const existingEntries = new Set(existingLines.map(line => line.trim()).filter(Boolean))
201
+ const entriesToAdd = targets
202
+ .map(target => toGitignoreEntry(repositoryRoot, target))
203
+ .filter(entry => !existingEntries.has(entry))
204
+
205
+ if (entriesToAdd.length === 0) {
206
+ return
207
+ }
208
+
209
+ const nextLines = [...existingLines]
210
+ if (nextLines.length > 0 && nextLines[nextLines.length - 1] !== '') {
211
+ nextLines.push('')
212
+ }
213
+ nextLines.push(...entriesToAdd)
214
+
215
+ writeFileSync(gitignorePath, `${nextLines.join('\n')}\n`, 'utf-8')
216
+ }
217
+
218
+ function toGitignoreEntry(repositoryRoot: string, target: IgnoreTarget): string {
219
+ const relativePath = relative(repositoryRoot, target.path).replace(/\\/g, '/')
220
+ const normalizedPath = relativePath.startsWith('.') ? relativePath : `/${relativePath}`
221
+ return target.kind === 'dir' ? `${normalizedPath}/` : normalizedPath
222
+ }
223
+
224
+ export function ensureClaudeSymlink(
225
+ repositoryRoot: string,
226
+ logger: Pick<Console, 'log'>,
227
+ colors: Record<'gray' | 'orange' | 'green' | 'reset', string>,
228
+ ): void {
229
+ const agentsPath = resolve(repositoryRoot, '.agents')
230
+ const claudePath = resolve(repositoryRoot, '.claude')
231
+
232
+ // Use lstatSync (not existsSync) to detect broken symlinks —
233
+ // existsSync follows symlinks and returns false for broken ones,
234
+ // which would cause mkdirSync/symlinkSync to misbehave.
235
+ let agentsExists = false
236
+ try {
237
+ lstatSync(agentsPath)
238
+ agentsExists = true
239
+ }
240
+ catch {}
241
+
242
+ if (!agentsExists) {
243
+ mkdirSync(agentsPath, { recursive: true })
244
+ }
245
+
246
+ let claudeStats: import('node:fs').Stats | null = null
247
+ try {
248
+ claudeStats = lstatSync(claudePath)
249
+ }
250
+ catch {}
251
+
252
+ if (!claudeStats) {
253
+ symlinkSync('.agents', claudePath)
254
+ logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Created .claude symlink → .agents${colors.reset}`)
255
+ return
256
+ }
257
+
258
+ if (claudeStats.isSymbolicLink()) {
259
+ return
260
+ }
261
+
262
+ if (claudeStats.isDirectory()) {
263
+ ensureSkillSymlinks(repositoryRoot, logger, colors)
264
+ return
265
+ }
266
+
267
+ logger.log(`${colors.gray}[tela/build] ${colors.orange}✗${colors.gray} .claude exists and is not a symlink or directory; keeping .agents as canonical output${colors.reset}`)
268
+ }
269
+
270
+ function ensureSkillSymlinks(
271
+ repositoryRoot: string,
272
+ logger: Pick<Console, 'log'>,
273
+ colors: Record<'gray' | 'orange' | 'green' | 'reset', string>,
274
+ ): void {
275
+ const agentsSkillsPath = resolve(repositoryRoot, '.agents/skills')
276
+ const claudeSkillsPath = resolve(repositoryRoot, '.claude/skills')
277
+
278
+ if (!existsSync(agentsSkillsPath)) {
279
+ mkdirSync(agentsSkillsPath, { recursive: true })
280
+ }
281
+
282
+ let claudeSkillsStats: import('node:fs').Stats | null = null
283
+ try {
284
+ claudeSkillsStats = lstatSync(claudeSkillsPath)
285
+ }
286
+ catch {}
287
+
288
+ if (claudeSkillsStats) {
289
+ return
290
+ }
291
+
292
+ symlinkSync('../.agents/skills', claudeSkillsPath)
293
+ logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Linked .claude/skills → .agents/skills${colors.reset}`)
294
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.30.3",
3
+ "version": "1.30.4",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",