@meistrari/tela-build 1.30.3 → 1.31.0
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/lib/__tests__/doc-generator.test.ts +1 -1
- package/lib/doc-generator.ts +10 -23
- package/lib/tela-build-skill.ts +8 -0
- package/modules/tela-build-docs/__tests__/ensure-claude-symlink.test.ts +122 -0
- package/modules/tela-build-docs/__tests__/resolve-repository-root.test.ts +71 -0
- package/modules/tela-build-docs/index.ts +181 -19
- package/package.json +1 -1
|
@@ -26,7 +26,7 @@ describe('generateDocsToDirectory', () => {
|
|
|
26
26
|
|
|
27
27
|
expect(parsed.data).toMatchObject({
|
|
28
28
|
name: 'tela-build',
|
|
29
|
-
description: expect.stringContaining('
|
|
29
|
+
description: expect.stringContaining('Ask for component props'),
|
|
30
30
|
})
|
|
31
31
|
expect(skillMd).toContain('description: "')
|
|
32
32
|
})
|
package/lib/doc-generator.ts
CHANGED
|
@@ -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,
|
|
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({
|
|
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: ${
|
|
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(/\
|
|
707
|
+
const trimmed = desc.replace(/\r\n?/g, '\n').trim()
|
|
717
708
|
return trimmed.slice(0, 1024)
|
|
718
709
|
}
|
|
719
710
|
|
|
720
|
-
function
|
|
721
|
-
|
|
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
|
+
})
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'
|
|
2
|
+
import { tmpdir } from 'node:os'
|
|
3
|
+
import { join } from 'pathe'
|
|
4
|
+
import { afterEach, describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
import { resolveRepositoryRoot } from '../index'
|
|
7
|
+
|
|
8
|
+
const tempDirs: string[] = []
|
|
9
|
+
|
|
10
|
+
function makeTempRoot() {
|
|
11
|
+
const dir = mkdtempSync(join(tmpdir(), 'resolve-repository-root-'))
|
|
12
|
+
tempDirs.push(dir)
|
|
13
|
+
|
|
14
|
+
return dir
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
for (const dir of tempDirs.splice(0)) {
|
|
19
|
+
rmSync(dir, { recursive: true, force: true })
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
describe('resolveRepositoryRoot', () => {
|
|
24
|
+
it('prefers Nuxt workspaceDir for monorepos running without .git in Docker', () => {
|
|
25
|
+
const workspaceRoot = makeTempRoot()
|
|
26
|
+
const appDir = join(workspaceRoot, 'apps/web')
|
|
27
|
+
|
|
28
|
+
mkdirSync(appDir, { recursive: true })
|
|
29
|
+
|
|
30
|
+
expect(resolveRepositoryRoot(appDir, workspaceRoot)).toBe(workspaceRoot)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('detects workspace roots from .git markers', () => {
|
|
34
|
+
const workspaceRoot = makeTempRoot()
|
|
35
|
+
const appDir = join(workspaceRoot, 'apps/web')
|
|
36
|
+
|
|
37
|
+
mkdirSync(appDir, { recursive: true })
|
|
38
|
+
writeFileSync(join(workspaceRoot, '.git'), 'gitdir: ../.git/worktrees/test\n')
|
|
39
|
+
|
|
40
|
+
expect(resolveRepositoryRoot(appDir)).toBe(workspaceRoot)
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('detects workspace roots from pnpm-workspace.yaml', () => {
|
|
44
|
+
const workspaceRoot = makeTempRoot()
|
|
45
|
+
const appDir = join(workspaceRoot, 'apps/web')
|
|
46
|
+
|
|
47
|
+
mkdirSync(appDir, { recursive: true })
|
|
48
|
+
writeFileSync(join(workspaceRoot, 'pnpm-workspace.yaml'), 'packages:\n - apps/*\n')
|
|
49
|
+
|
|
50
|
+
expect(resolveRepositoryRoot(appDir)).toBe(workspaceRoot)
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it('detects workspace roots from package.json workspaces when workspaceDir is unavailable', () => {
|
|
54
|
+
const workspaceRoot = makeTempRoot()
|
|
55
|
+
const appDir = join(workspaceRoot, 'apps/web')
|
|
56
|
+
|
|
57
|
+
mkdirSync(appDir, { recursive: true })
|
|
58
|
+
writeFileSync(join(workspaceRoot, 'package.json'), JSON.stringify({
|
|
59
|
+
private: true,
|
|
60
|
+
workspaces: ['apps/*', 'packages/*'],
|
|
61
|
+
}))
|
|
62
|
+
|
|
63
|
+
expect(resolveRepositoryRoot(appDir)).toBe(workspaceRoot)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('falls back to the app root when no repository markers exist', () => {
|
|
67
|
+
const appDir = makeTempRoot()
|
|
68
|
+
|
|
69
|
+
expect(resolveRepositoryRoot(appDir)).toBe(appDir)
|
|
70
|
+
})
|
|
71
|
+
})
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
|
-
import {
|
|
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
|
-
//
|
|
24
|
-
outDir:
|
|
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, nuxt.options.workspaceDir)
|
|
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 =
|
|
101
|
+
const baseDir = repositoryRoot
|
|
87
102
|
|
|
88
103
|
if (options.outFile) {
|
|
89
104
|
// Single-file mode (legacy)
|
|
90
|
-
const outPath = resolve(
|
|
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
|
|
102
|
-
|
|
103
|
-
|
|
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,152 @@ 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
|
+
export function resolveRepositoryRoot(startDir: string, workspaceDir?: string): string {
|
|
179
|
+
if (workspaceDir && existsSync(resolve(workspaceDir))) {
|
|
180
|
+
return resolve(workspaceDir)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
let currentDir = resolve(startDir)
|
|
184
|
+
|
|
185
|
+
while (true) {
|
|
186
|
+
if (isRepositoryRoot(currentDir)) {
|
|
187
|
+
return currentDir
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const parentDir = dirname(currentDir)
|
|
191
|
+
if (parentDir === currentDir) {
|
|
192
|
+
return resolve(startDir)
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
currentDir = parentDir
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isRepositoryRoot(directory: string): boolean {
|
|
200
|
+
if (existsSync(resolve(directory, '.git')) || existsSync(resolve(directory, 'pnpm-workspace.yaml'))) {
|
|
201
|
+
return true
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const packageJsonPath = resolve(directory, 'package.json')
|
|
205
|
+
if (!existsSync(packageJsonPath)) {
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8')) as { workspaces?: unknown }
|
|
211
|
+
return packageJson.workspaces !== undefined
|
|
212
|
+
}
|
|
213
|
+
catch {
|
|
214
|
+
return false
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function ensurePathsIgnored(repositoryRoot: string, targets: IgnoreTarget[]): void {
|
|
219
|
+
const gitignorePath = resolve(repositoryRoot, '.gitignore')
|
|
220
|
+
const existingLines = existsSync(gitignorePath)
|
|
221
|
+
? readFileSync(gitignorePath, 'utf-8').split('\n')
|
|
222
|
+
: []
|
|
223
|
+
const existingEntries = new Set(existingLines.map(line => line.trim()).filter(Boolean))
|
|
224
|
+
const entriesToAdd = targets
|
|
225
|
+
.map(target => toGitignoreEntry(repositoryRoot, target))
|
|
226
|
+
.filter(entry => !existingEntries.has(entry))
|
|
227
|
+
|
|
228
|
+
if (entriesToAdd.length === 0) {
|
|
229
|
+
return
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const nextLines = [...existingLines]
|
|
233
|
+
if (nextLines.length > 0 && nextLines[nextLines.length - 1] !== '') {
|
|
234
|
+
nextLines.push('')
|
|
235
|
+
}
|
|
236
|
+
nextLines.push(...entriesToAdd)
|
|
237
|
+
|
|
238
|
+
writeFileSync(gitignorePath, `${nextLines.join('\n')}\n`, 'utf-8')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function toGitignoreEntry(repositoryRoot: string, target: IgnoreTarget): string {
|
|
242
|
+
const relativePath = relative(repositoryRoot, target.path).replace(/\\/g, '/')
|
|
243
|
+
const normalizedPath = relativePath.startsWith('.') ? relativePath : `/${relativePath}`
|
|
244
|
+
return target.kind === 'dir' ? `${normalizedPath}/` : normalizedPath
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function ensureClaudeSymlink(
|
|
248
|
+
repositoryRoot: string,
|
|
249
|
+
logger: Pick<Console, 'log'>,
|
|
250
|
+
colors: Record<'gray' | 'orange' | 'green' | 'reset', string>,
|
|
251
|
+
): void {
|
|
252
|
+
const agentsPath = resolve(repositoryRoot, '.agents')
|
|
253
|
+
const claudePath = resolve(repositoryRoot, '.claude')
|
|
254
|
+
|
|
255
|
+
// Use lstatSync (not existsSync) to detect broken symlinks —
|
|
256
|
+
// existsSync follows symlinks and returns false for broken ones,
|
|
257
|
+
// which would cause mkdirSync/symlinkSync to misbehave.
|
|
258
|
+
let agentsExists = false
|
|
259
|
+
try {
|
|
260
|
+
lstatSync(agentsPath)
|
|
261
|
+
agentsExists = true
|
|
262
|
+
}
|
|
263
|
+
catch {}
|
|
264
|
+
|
|
265
|
+
if (!agentsExists) {
|
|
266
|
+
mkdirSync(agentsPath, { recursive: true })
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
let claudeStats: import('node:fs').Stats | null = null
|
|
270
|
+
try {
|
|
271
|
+
claudeStats = lstatSync(claudePath)
|
|
272
|
+
}
|
|
273
|
+
catch {}
|
|
274
|
+
|
|
275
|
+
if (!claudeStats) {
|
|
276
|
+
symlinkSync('.agents', claudePath)
|
|
277
|
+
logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Created .claude symlink → .agents${colors.reset}`)
|
|
278
|
+
return
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (claudeStats.isSymbolicLink()) {
|
|
282
|
+
return
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (claudeStats.isDirectory()) {
|
|
286
|
+
ensureSkillSymlinks(repositoryRoot, logger, colors)
|
|
287
|
+
return
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
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}`)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
function ensureSkillSymlinks(
|
|
294
|
+
repositoryRoot: string,
|
|
295
|
+
logger: Pick<Console, 'log'>,
|
|
296
|
+
colors: Record<'gray' | 'orange' | 'green' | 'reset', string>,
|
|
297
|
+
): void {
|
|
298
|
+
const agentsSkillsPath = resolve(repositoryRoot, '.agents/skills')
|
|
299
|
+
const claudeSkillsPath = resolve(repositoryRoot, '.claude/skills')
|
|
300
|
+
|
|
301
|
+
if (!existsSync(agentsSkillsPath)) {
|
|
302
|
+
mkdirSync(agentsSkillsPath, { recursive: true })
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
let claudeSkillsStats: import('node:fs').Stats | null = null
|
|
306
|
+
try {
|
|
307
|
+
claudeSkillsStats = lstatSync(claudeSkillsPath)
|
|
308
|
+
}
|
|
309
|
+
catch {}
|
|
310
|
+
|
|
311
|
+
if (claudeSkillsStats) {
|
|
312
|
+
return
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
symlinkSync('../.agents/skills', claudeSkillsPath)
|
|
316
|
+
logger.log(`${colors.gray}[tela/build] ${colors.green}●${colors.gray} Linked .claude/skills → .agents/skills${colors.reset}`)
|
|
317
|
+
}
|