@meistrari/tela-build 1.52.0 → 1.52.1

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.
@@ -82,16 +82,65 @@ describe('ensureClaudeSymlink', () => {
82
82
  expect(existsSync(skillsLink)).toBe(true)
83
83
  })
84
84
 
85
- it('skips skill symlink when .claude/skills already exists', () => {
85
+ it('links tela-build individually when .claude/skills already exists as a real directory', () => {
86
86
  const root = makeTempRoot()
87
87
 
88
- mkdirSync(join(root, '.agents/skills'), { recursive: true })
89
- mkdirSync(join(root, '.claude/skills'), { recursive: true })
88
+ mkdirSync(join(root, '.agents/skills/tela-build'), { recursive: true })
89
+ writeFileSync(join(root, '.agents/skills/tela-build/SKILL.md'), '# Test')
90
+ // Developer keeps their own real .claude/skills dir with a personal skill.
91
+ mkdirSync(join(root, '.claude/skills/my-skill'), { recursive: true })
90
92
 
91
93
  ensureClaudeSymlink(root, logger, colors)
92
94
 
93
- expect(lstatSync(join(root, '.claude/skills')).isDirectory()).toBe(true)
95
+ // The real directory is preserved, not replaced by a symlink.
94
96
  expect(lstatSync(join(root, '.claude/skills')).isSymbolicLink()).toBe(false)
97
+ // The developer's own skill is untouched.
98
+ expect(existsSync(join(root, '.claude/skills/my-skill'))).toBe(true)
99
+ // The generated skill is linked in and readable through .claude/skills.
100
+ expect(lstatSync(join(root, '.claude/skills/tela-build')).isSymbolicLink()).toBe(true)
101
+ expect(readFileSync(join(root, '.claude/skills/tela-build/SKILL.md'), 'utf-8')).toBe('# Test')
102
+ })
103
+
104
+ it('leaves a real .claude/skills/tela-build directory untouched', () => {
105
+ const root = makeTempRoot()
106
+
107
+ mkdirSync(join(root, '.agents/skills/tela-build'), { recursive: true })
108
+ // A stale real copy already sits at the destination.
109
+ mkdirSync(join(root, '.claude/skills/tela-build'), { recursive: true })
110
+ writeFileSync(join(root, '.claude/skills/tela-build/SKILL.md'), '# Stale')
111
+
112
+ ensureClaudeSymlink(root, logger, colors)
113
+
114
+ expect(lstatSync(join(root, '.claude/skills/tela-build')).isSymbolicLink()).toBe(false)
115
+ expect(readFileSync(join(root, '.claude/skills/tela-build/SKILL.md'), 'utf-8')).toBe('# Stale')
116
+ })
117
+
118
+ it('is idempotent when .claude/skills/tela-build is already a symlink', () => {
119
+ const root = makeTempRoot()
120
+
121
+ mkdirSync(join(root, '.agents/skills/tela-build'), { recursive: true })
122
+ mkdirSync(join(root, '.claude/skills'), { recursive: true })
123
+ symlinkSync('../../.agents/skills/tela-build', join(root, '.claude/skills/tela-build'))
124
+
125
+ ensureClaudeSymlink(root, logger, colors)
126
+
127
+ expect(lstatSync(join(root, '.claude/skills/tela-build')).isSymbolicLink()).toBe(true)
128
+ })
129
+
130
+ it('re-points a stale .claude/skills/tela-build symlink at the generated skill', () => {
131
+ const root = makeTempRoot()
132
+
133
+ mkdirSync(join(root, '.agents/skills/tela-build'), { recursive: true })
134
+ writeFileSync(join(root, '.agents/skills/tela-build/SKILL.md'), '# Test')
135
+ mkdirSync(join(root, '.claude/skills'), { recursive: true })
136
+ // A symlink from an earlier setup that points somewhere wrong/broken.
137
+ symlinkSync('../../.agents/skills/stale-target', join(root, '.claude/skills/tela-build'))
138
+
139
+ ensureClaudeSymlink(root, logger, colors)
140
+
141
+ expect(lstatSync(join(root, '.claude/skills/tela-build')).isSymbolicLink()).toBe(true)
142
+ expect(readlinkSync(join(root, '.claude/skills/tela-build'))).toBe('../../.agents/skills/tela-build')
143
+ expect(readFileSync(join(root, '.claude/skills/tela-build/SKILL.md'), 'utf-8')).toBe('# Test')
95
144
  })
96
145
 
97
146
  it('creates .agents/skills when .claude is a directory but .agents/skills does not exist', () => {
@@ -1,5 +1,5 @@
1
1
  /* eslint-disable no-console */
2
- import { existsSync, writeFileSync, mkdirSync, lstatSync, readFileSync, symlinkSync } from 'node:fs'
2
+ import { existsSync, writeFileSync, mkdirSync, lstatSync, readFileSync, symlinkSync, readlinkSync, unlinkSync } from 'node:fs'
3
3
  import { defineNuxtModule, createResolver } from 'nuxt/kit'
4
4
  import { dirname, resolve } from 'pathe'
5
5
  import { collectComponentDocs, generateMarkdown, generateDocsToDirectory } from '../../lib/doc-generator'
@@ -248,6 +248,10 @@ export function ensureClaudeSymlink(
248
248
  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}`)
249
249
  }
250
250
 
251
+ // The single Skill produced by doc generation. Linked individually so it stays
252
+ // discoverable even when the developer keeps their own real .claude/skills dir.
253
+ const GENERATED_SKILL_NAME = 'tela-build'
254
+
251
255
  function ensureSkillSymlinks(
252
256
  repositoryRoot: string,
253
257
  logger: Pick<Console, 'log'>,
@@ -266,10 +270,58 @@ function ensureSkillSymlinks(
266
270
  }
267
271
  catch {}
268
272
 
269
- if (claudeSkillsStats) {
273
+ // No .claude/skills yet → link the whole directory in one shot.
274
+ if (!claudeSkillsStats) {
275
+ symlinkSync('../.agents/skills', claudeSkillsPath)
276
+ logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Linked .claude/skills → .agents/skills${colors.reset}`)
277
+ return
278
+ }
279
+
280
+ // Already a symlink (e.g. a previous run linked the whole dir) → the generated
281
+ // skill resolves through it; nothing to do.
282
+ if (claudeSkillsStats.isSymbolicLink()) {
283
+ return
284
+ }
285
+
286
+ // .claude/skills is a real directory the developer manages themselves. Don't
287
+ // touch it — just drop a per-skill symlink so the generated skill stays
288
+ // discoverable alongside their own skills.
289
+ ensureGeneratedSkillSymlink(claudeSkillsPath, logger, colors)
290
+ }
291
+
292
+ function ensureGeneratedSkillSymlink(
293
+ claudeSkillsPath: string,
294
+ logger: Pick<Console, 'log'>,
295
+ colors: Record<'gray' | 'orange' | 'green' | 'reset', string>,
296
+ ): void {
297
+ const generatedSkillLink = resolve(claudeSkillsPath, GENERATED_SKILL_NAME)
298
+ const expectedTarget = `../../.agents/skills/${GENERATED_SKILL_NAME}`
299
+
300
+ let linkStats: import('node:fs').Stats | null = null
301
+ try {
302
+ linkStats = lstatSync(generatedSkillLink)
303
+ }
304
+ catch {}
305
+
306
+ // Already a symlink → keep it only if it points where we want. A stale or
307
+ // broken target from an earlier setup gets re-pointed at the generated skill.
308
+ if (linkStats?.isSymbolicLink()) {
309
+ if (readlinkSync(generatedSkillLink) === expectedTarget) {
310
+ return
311
+ }
312
+ unlinkSync(generatedSkillLink)
313
+ linkStats = null
314
+ }
315
+
316
+ // A real directory/file already sits here (e.g. a stale copy). Don't clobber
317
+ // developer-owned content; surface it instead.
318
+ if (linkStats) {
319
+ logger.log(`${colors.gray}[tela/build] ${colors.orange}✗${colors.gray} .claude/skills/${GENERATED_SKILL_NAME} exists and is not a symlink; skipping link${colors.reset}`)
270
320
  return
271
321
  }
272
322
 
273
- symlinkSync('../.agents/skills', claudeSkillsPath)
274
- logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Linked .claude/skills → .agents/skills${colors.reset}`)
323
+ // Target is relative to the link's own directory (.claude/skills):
324
+ // ../../.agents/skills/<skill><root>/.agents/skills/<skill>
325
+ symlinkSync(expectedTarget, generatedSkillLink)
326
+ logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Linked .claude/skills/${GENERATED_SKILL_NAME} → .agents/skills/${GENERATED_SKILL_NAME}${colors.reset}`)
275
327
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@meistrari/tela-build",
3
- "version": "1.52.0",
3
+ "version": "1.52.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "app.config.ts",