@patricksardinha/agentkit-cli 0.1.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/.gitattributes +2 -0
- package/.github/workflows/release.yml +31 -0
- package/AGENT_WORKFLOW.md +55 -0
- package/CLAUDE.md +45 -0
- package/README.md +327 -0
- package/dist/cli.cjs +1079 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +1056 -0
- package/dist/cli.js.map +1 -0
- package/package.json +45 -0
- package/src/cli.ts +18 -0
- package/src/commands/add.ts +166 -0
- package/src/commands/init.ts +130 -0
- package/src/commands/status.ts +57 -0
- package/src/detectors/gitDetector.ts +11 -0
- package/src/detectors/stackDetector.ts +88 -0
- package/src/generators/claudeMdGenerator.ts +42 -0
- package/src/generators/playbookGenerator.ts +76 -0
- package/src/generators/skillsGenerator.ts +56 -0
- package/src/generators/workflowGenerator.ts +62 -0
- package/src/templates/express.ts +64 -0
- package/src/templates/fastapi.ts +63 -0
- package/src/templates/nextjs.ts +63 -0
- package/src/templates/node.ts +54 -0
- package/src/templates/react.ts +61 -0
- package/src/templates/tauri.ts +65 -0
- package/src/templates/unknown.ts +45 -0
- package/src/types/agent.ts +9 -0
- package/src/types/inquirer.d.ts +36 -0
- package/src/utils/agentParser.ts +67 -0
- package/src/utils/blueprintParser.ts +28 -0
- package/src/utils/logger.ts +16 -0
- package/tests/commands/add.test.ts +130 -0
- package/tests/detectors/fixtures/express-app/package.json +9 -0
- package/tests/detectors/fixtures/fastapi-app/requirements.txt +3 -0
- package/tests/detectors/fixtures/nextjs-app/package.json +13 -0
- package/tests/detectors/fixtures/no-git/README.md +2 -0
- package/tests/detectors/fixtures/react-app/package.json +10 -0
- package/tests/detectors/fixtures/tauri-app/package.json +10 -0
- package/tests/detectors/fixtures/tauri-app/src-tauri/tauri.conf.json +12 -0
- package/tests/detectors/gitDetector.test.ts +29 -0
- package/tests/detectors/stackDetector.test.ts +50 -0
- package/tests/generators/blueprintSupport.test.ts +130 -0
- package/tests/generators/claudeMdGenerator.test.ts +86 -0
- package/tests/generators/playbookGenerator.test.ts +152 -0
- package/tests/generators/skillsGenerator.test.ts +94 -0
- package/tests/generators/workflowGenerator.test.ts +84 -0
- package/tsconfig.json +19 -0
- package/tsup.config.ts +11 -0
- package/vitest.config.ts +9 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
declare module 'inquirer' {
|
|
2
|
+
export interface QuestionBase {
|
|
3
|
+
type: string
|
|
4
|
+
name: string
|
|
5
|
+
message: string
|
|
6
|
+
default?: unknown
|
|
7
|
+
choices?: unknown[]
|
|
8
|
+
validate?: (input: unknown) => boolean | string | Promise<boolean | string>
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ConfirmQuestion extends QuestionBase {
|
|
12
|
+
type: 'confirm'
|
|
13
|
+
default?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface InputQuestion extends QuestionBase {
|
|
17
|
+
type: 'input'
|
|
18
|
+
default?: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ListQuestion extends QuestionBase {
|
|
22
|
+
type: 'list'
|
|
23
|
+
choices: string[]
|
|
24
|
+
default?: string
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export type Question = ConfirmQuestion | InputQuestion | ListQuestion
|
|
28
|
+
|
|
29
|
+
export interface Inquirer {
|
|
30
|
+
prompt<T extends Record<string, unknown>>(questions: Question[]): Promise<T>
|
|
31
|
+
createPromptModule(): (questions: Question[]) => Promise<Record<string, unknown>>
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const inquirer: Inquirer
|
|
35
|
+
export default inquirer
|
|
36
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { Agent } from '../types/agent.js'
|
|
2
|
+
|
|
3
|
+
export function toSlug(name: string): string {
|
|
4
|
+
return name
|
|
5
|
+
.toLowerCase()
|
|
6
|
+
.replace(/[·•&]/g, ' ')
|
|
7
|
+
.replace(/[^\w\s-]/g, '')
|
|
8
|
+
.trim()
|
|
9
|
+
.replace(/\s+/g, '-')
|
|
10
|
+
.replace(/-+/g, '-')
|
|
11
|
+
.replace(/^-|-$/g, '')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function getFieldValue(lines: string[], pattern: RegExp): string {
|
|
15
|
+
for (const line of lines) {
|
|
16
|
+
const m = line.match(pattern)
|
|
17
|
+
if (m) return (m[1] ?? '').trim()
|
|
18
|
+
}
|
|
19
|
+
return ''
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function extractAgentsFromWorkflow(content: string): Agent[] {
|
|
23
|
+
const agents: Agent[] = []
|
|
24
|
+
|
|
25
|
+
// Split into blocks starting with "### Agent N"
|
|
26
|
+
const blocks = content
|
|
27
|
+
.split(/(?=^### Agent \d)/m)
|
|
28
|
+
.filter((b) => /^### Agent \d/.test(b.trimStart()))
|
|
29
|
+
|
|
30
|
+
for (const block of blocks) {
|
|
31
|
+
const lines = block.split('\n')
|
|
32
|
+
|
|
33
|
+
const headerMatch = lines[0].match(/^### Agent (\d+)\s*[·•]\s*(.+)$/)
|
|
34
|
+
if (!headerMatch) continue
|
|
35
|
+
|
|
36
|
+
const number = parseInt(headerMatch[1], 10)
|
|
37
|
+
const name = headerMatch[2].trim()
|
|
38
|
+
const fullName = `Agent ${number} · ${name}`
|
|
39
|
+
const slug = toSlug(name)
|
|
40
|
+
|
|
41
|
+
const scope = getFieldValue(lines, /Périmètre\s*:\s*(.+)/)
|
|
42
|
+
const criterion = getFieldValue(lines, /Critère[s]?\s*:\s*(.+)/)
|
|
43
|
+
|
|
44
|
+
// Outputs: may be inline or multi-line (indented "- item")
|
|
45
|
+
const outputs: string[] = []
|
|
46
|
+
const produitIdx = lines.findIndex((l) => /Produit\s*:/.test(l))
|
|
47
|
+
if (produitIdx !== -1) {
|
|
48
|
+
const inlineVal = (lines[produitIdx].match(/Produit\s*:\s*(.+)/)?.[1] ?? '').trim()
|
|
49
|
+
if (inlineVal) {
|
|
50
|
+
outputs.push(inlineVal)
|
|
51
|
+
} else {
|
|
52
|
+
for (let i = produitIdx + 1; i < lines.length; i++) {
|
|
53
|
+
const line = lines[i]
|
|
54
|
+
if (/^\s+[-]/.test(line)) {
|
|
55
|
+
outputs.push(line.trim().replace(/^-\s*/, ''))
|
|
56
|
+
} else if (line.trim() !== '' && !/^\s/.test(line)) {
|
|
57
|
+
break
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
agents.push({ number, name, fullName, slug, scope, outputs, criterion })
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return agents
|
|
67
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export interface BlueprintFeature {
|
|
2
|
+
name: string
|
|
3
|
+
items: string[]
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function parseBlueprint(content: string): BlueprintFeature[] {
|
|
7
|
+
const features: BlueprintFeature[] = []
|
|
8
|
+
|
|
9
|
+
// Locate all ## headings (not # or ###)
|
|
10
|
+
const sectionRegex = /^## (.+)$/gm
|
|
11
|
+
const sections: Array<{ name: string; start: number }> = []
|
|
12
|
+
let m: RegExpExecArray | null
|
|
13
|
+
while ((m = sectionRegex.exec(content)) !== null) {
|
|
14
|
+
sections.push({ name: m[1].trim(), start: m.index })
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
for (let i = 0; i < sections.length; i++) {
|
|
18
|
+
const section = sections[i]
|
|
19
|
+
const end = i + 1 < sections.length ? sections[i + 1].start : content.length
|
|
20
|
+
const body = content.slice(section.start, end)
|
|
21
|
+
|
|
22
|
+
const items = [...body.matchAll(/^[-*]\s+(.+)$/gm)].map((r) => r[1].trim())
|
|
23
|
+
|
|
24
|
+
features.push({ name: section.name, items })
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return features
|
|
28
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import chalk from 'chalk'
|
|
2
|
+
|
|
3
|
+
export const logger = {
|
|
4
|
+
info: (message: string): void => {
|
|
5
|
+
process.stdout.write(chalk.blue('ℹ') + ' ' + message + '\n')
|
|
6
|
+
},
|
|
7
|
+
success: (message: string): void => {
|
|
8
|
+
process.stdout.write(chalk.green('✔') + ' ' + message + '\n')
|
|
9
|
+
},
|
|
10
|
+
warn: (message: string): void => {
|
|
11
|
+
process.stdout.write(chalk.yellow('⚠') + ' ' + message + '\n')
|
|
12
|
+
},
|
|
13
|
+
error: (message: string): void => {
|
|
14
|
+
process.stderr.write(chalk.red('✖') + ' ' + message + '\n')
|
|
15
|
+
},
|
|
16
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach } from 'vitest'
|
|
2
|
+
import { addFeatureToProject } from '../../src/commands/add.js'
|
|
3
|
+
import { readFile, rm, mkdtemp, writeFile, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { join } from 'node:path'
|
|
5
|
+
import { tmpdir } from 'node:os'
|
|
6
|
+
|
|
7
|
+
const INITIAL_WORKFLOW = `# Agent Workflow — my-project
|
|
8
|
+
|
|
9
|
+
## Stack détectée
|
|
10
|
+
Framework: react | Language: typescript
|
|
11
|
+
|
|
12
|
+
## Agents
|
|
13
|
+
|
|
14
|
+
### Agent 1 · Components
|
|
15
|
+
Périmètre : composants UI réutilisables
|
|
16
|
+
Produit : src/components/
|
|
17
|
+
Critère : npm test
|
|
18
|
+
|
|
19
|
+
### Agent 2 · State & Hooks
|
|
20
|
+
Périmètre : state management
|
|
21
|
+
Produit : src/hooks/
|
|
22
|
+
Critère : npm test
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
let tempDir = ''
|
|
26
|
+
|
|
27
|
+
afterEach(async () => {
|
|
28
|
+
if (tempDir) {
|
|
29
|
+
await rm(tempDir, { recursive: true, force: true })
|
|
30
|
+
tempDir = ''
|
|
31
|
+
}
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
async function setupProject(workflowContent = INITIAL_WORKFLOW): Promise<string> {
|
|
35
|
+
tempDir = await mkdtemp(join(tmpdir(), 'agentkit-add-test-'))
|
|
36
|
+
await writeFile(join(tempDir, 'AGENT_WORKFLOW.md'), workflowContent, 'utf-8')
|
|
37
|
+
await writeFile(join(tempDir, 'PLAYBOOK.md'), '# PLAYBOOK placeholder\n', 'utf-8')
|
|
38
|
+
return tempDir
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
describe('addFeatureToProject', () => {
|
|
42
|
+
it('appends a new agent block to AGENT_WORKFLOW.md', async () => {
|
|
43
|
+
const dir = await setupProject()
|
|
44
|
+
await addFeatureToProject('Add user authentication', dir)
|
|
45
|
+
|
|
46
|
+
const workflow = await readFile(join(dir, 'AGENT_WORKFLOW.md'), 'utf-8')
|
|
47
|
+
expect(workflow).toContain('Agent 3 · User Authentication')
|
|
48
|
+
expect(workflow).toContain('Add user authentication')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('assigns the correct next agent number', async () => {
|
|
52
|
+
const dir = await setupProject()
|
|
53
|
+
const result = await addFeatureToProject('Add dark mode', dir)
|
|
54
|
+
|
|
55
|
+
expect(result.agent.number).toBe(3)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('derives a title-cased name from the description', async () => {
|
|
59
|
+
const dir = await setupProject()
|
|
60
|
+
const result = await addFeatureToProject('Add payment integration with Stripe', dir)
|
|
61
|
+
|
|
62
|
+
expect(result.agent.name).toBe('Payment Integration With Stripe')
|
|
63
|
+
expect(result.agent.slug).toBe('payment-integration-with-stripe')
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('strips common action prefixes from the agent name', async () => {
|
|
67
|
+
const dir = await setupProject()
|
|
68
|
+
const r1 = await addFeatureToProject('implement search functionality', dir)
|
|
69
|
+
expect(r1.agent.name).toBe('Search Functionality')
|
|
70
|
+
|
|
71
|
+
const dir2 = await setupProject()
|
|
72
|
+
const r2 = await addFeatureToProject('create admin dashboard', dir2)
|
|
73
|
+
expect(r2.agent.name).toBe('Admin Dashboard')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('creates the agent skills folder with skills.md and context.md', async () => {
|
|
77
|
+
const dir = await setupProject()
|
|
78
|
+
const result = await addFeatureToProject('Add notifications', dir)
|
|
79
|
+
|
|
80
|
+
const skillsContent = await readFile(
|
|
81
|
+
join(result.agentDirPath, 'skills.md'),
|
|
82
|
+
'utf-8',
|
|
83
|
+
)
|
|
84
|
+
const contextContent = await readFile(
|
|
85
|
+
join(result.agentDirPath, 'context.md'),
|
|
86
|
+
'utf-8',
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
expect(skillsContent).toContain(result.agent.fullName)
|
|
90
|
+
expect(contextContent).toContain('Add notifications')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
it('regenerates PLAYBOOK.md with all agents including the new one', async () => {
|
|
94
|
+
const dir = await setupProject()
|
|
95
|
+
await addFeatureToProject('Add analytics dashboard', dir)
|
|
96
|
+
|
|
97
|
+
const playbook = await readFile(join(dir, 'PLAYBOOK.md'), 'utf-8')
|
|
98
|
+
expect(playbook).toContain('Agent 1 · Components')
|
|
99
|
+
expect(playbook).toContain('Agent 2 · State & Hooks')
|
|
100
|
+
expect(playbook).toContain('Agent 3 · Analytics Dashboard')
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('reads project name from package.json when available', async () => {
|
|
104
|
+
const dir = await setupProject()
|
|
105
|
+
await writeFile(
|
|
106
|
+
join(dir, 'package.json'),
|
|
107
|
+
JSON.stringify({ name: 'my-custom-app' }),
|
|
108
|
+
'utf-8',
|
|
109
|
+
)
|
|
110
|
+
await addFeatureToProject('Add search', dir)
|
|
111
|
+
|
|
112
|
+
const playbook = await readFile(join(dir, 'PLAYBOOK.md'), 'utf-8')
|
|
113
|
+
expect(playbook).toContain('my-custom-app')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('throws an error when AGENT_WORKFLOW.md is missing', async () => {
|
|
117
|
+
tempDir = await mkdtemp(join(tmpdir(), 'agentkit-add-test-'))
|
|
118
|
+
await expect(addFeatureToProject('Add feature', tempDir)).rejects.toThrow(
|
|
119
|
+
'AGENT_WORKFLOW.md introuvable',
|
|
120
|
+
)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('works on a project with no existing agents', async () => {
|
|
124
|
+
const dir = await setupProject('# Agent Workflow\n\n## Agents\n')
|
|
125
|
+
const result = await addFeatureToProject('Add first feature', dir)
|
|
126
|
+
|
|
127
|
+
expect(result.agent.number).toBe(1)
|
|
128
|
+
expect(result.agent.name).toBe('First Feature')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname } from 'node:path'
|
|
5
|
+
import { isGitRepo } from '../../src/detectors/gitDetector.js'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = dirname(__filename)
|
|
9
|
+
|
|
10
|
+
const projectRoot = join(__dirname, '..', '..')
|
|
11
|
+
const noGitDir = join(__dirname, 'fixtures', 'no-git')
|
|
12
|
+
const nonExistentDir = join(__dirname, 'fixtures', 'nonexistent-xyz')
|
|
13
|
+
|
|
14
|
+
describe('isGitRepo', () => {
|
|
15
|
+
it('returns true for the project root (which is a git repo)', async () => {
|
|
16
|
+
const result = await isGitRepo(projectRoot)
|
|
17
|
+
expect(result).toBe(true)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
it('returns false for a directory without .git', async () => {
|
|
21
|
+
const result = await isGitRepo(noGitDir)
|
|
22
|
+
expect(result).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('returns false for a non-existent path', async () => {
|
|
26
|
+
const result = await isGitRepo(nonExistentDir)
|
|
27
|
+
expect(result).toBe(false)
|
|
28
|
+
})
|
|
29
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { dirname } from 'node:path'
|
|
5
|
+
import { detectStack } from '../../src/detectors/stackDetector.js'
|
|
6
|
+
|
|
7
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
8
|
+
const __dirname = dirname(__filename)
|
|
9
|
+
const fixturesDir = join(__dirname, 'fixtures')
|
|
10
|
+
|
|
11
|
+
describe('detectStack', () => {
|
|
12
|
+
it('detects React', async () => {
|
|
13
|
+
const result = await detectStack(join(fixturesDir, 'react-app'))
|
|
14
|
+
expect(result.framework).toBe('react')
|
|
15
|
+
expect(result.language).toBe('javascript')
|
|
16
|
+
expect(result.hasTypeScript).toBe(false)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('detects Next.js (priority over React)', async () => {
|
|
20
|
+
const result = await detectStack(join(fixturesDir, 'nextjs-app'))
|
|
21
|
+
expect(result.framework).toBe('nextjs')
|
|
22
|
+
expect(result.hasTypeScript).toBe(true)
|
|
23
|
+
expect(result.language).toBe('typescript')
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
it('detects Tauri via @tauri-apps/api and src-tauri/', async () => {
|
|
27
|
+
const result = await detectStack(join(fixturesDir, 'tauri-app'))
|
|
28
|
+
expect(result.framework).toBe('tauri')
|
|
29
|
+
expect(result.hasTypeScript).toBe(true)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
it('detects FastAPI (Python)', async () => {
|
|
33
|
+
const result = await detectStack(join(fixturesDir, 'fastapi-app'))
|
|
34
|
+
expect(result.framework).toBe('fastapi')
|
|
35
|
+
expect(result.language).toBe('python')
|
|
36
|
+
expect(result.hasTypeScript).toBe(false)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('detects Express', async () => {
|
|
40
|
+
const result = await detectStack(join(fixturesDir, 'express-app'))
|
|
41
|
+
expect(result.framework).toBe('express')
|
|
42
|
+
expect(result.language).toBe('javascript')
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('returns unknown for a directory with no recognizable files', async () => {
|
|
46
|
+
const result = await detectStack(join(fixturesDir, 'no-git'))
|
|
47
|
+
expect(result.framework).toBe('unknown')
|
|
48
|
+
expect(result.language).toBe('unknown')
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { parseBlueprint } from '../../src/utils/blueprintParser.js'
|
|
3
|
+
import { generateClaudeMd } from '../../src/generators/claudeMdGenerator.js'
|
|
4
|
+
import { generateWorkflow } from '../../src/generators/workflowGenerator.js'
|
|
5
|
+
import { extractAgentsFromWorkflow } from '../../src/utils/agentParser.js'
|
|
6
|
+
import type { StackInfo } from '../../src/detectors/stackDetector.js'
|
|
7
|
+
|
|
8
|
+
const BLUEPRINT = `# My App Blueprint
|
|
9
|
+
|
|
10
|
+
## Authentication
|
|
11
|
+
- JWT tokens
|
|
12
|
+
- OAuth2 with Google
|
|
13
|
+
- Password reset flow
|
|
14
|
+
|
|
15
|
+
## Dashboard
|
|
16
|
+
- User statistics
|
|
17
|
+
- Charts and graphs
|
|
18
|
+
- CSV export
|
|
19
|
+
|
|
20
|
+
## API
|
|
21
|
+
- REST endpoints
|
|
22
|
+
- Rate limiting
|
|
23
|
+
- API key management
|
|
24
|
+
`
|
|
25
|
+
|
|
26
|
+
const REACT_STACK: StackInfo = {
|
|
27
|
+
framework: 'react',
|
|
28
|
+
language: 'typescript',
|
|
29
|
+
hasTypeScript: true,
|
|
30
|
+
extras: [],
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('parseBlueprint', () => {
|
|
34
|
+
it('extracts features from ## headings', () => {
|
|
35
|
+
const features = parseBlueprint(BLUEPRINT)
|
|
36
|
+
expect(features).toHaveLength(3)
|
|
37
|
+
expect(features[0].name).toBe('Authentication')
|
|
38
|
+
expect(features[1].name).toBe('Dashboard')
|
|
39
|
+
expect(features[2].name).toBe('API')
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
it('extracts list items under each feature', () => {
|
|
43
|
+
const features = parseBlueprint(BLUEPRINT)
|
|
44
|
+
expect(features[0].items).toContain('JWT tokens')
|
|
45
|
+
expect(features[0].items).toContain('OAuth2 with Google')
|
|
46
|
+
expect(features[1].items).toContain('User statistics')
|
|
47
|
+
expect(features[2].items).toContain('REST endpoints')
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
it('returns empty array for content with no ## headings', () => {
|
|
51
|
+
expect(parseBlueprint('# Title\nSome text without sections.')).toEqual([])
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('handles features with no list items', () => {
|
|
55
|
+
const features = parseBlueprint('## Feature A\nJust prose.\n## Feature B\n- item1')
|
|
56
|
+
expect(features[0].items).toEqual([])
|
|
57
|
+
expect(features[1].items).toEqual(['item1'])
|
|
58
|
+
})
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
describe('generateClaudeMd with blueprint', () => {
|
|
62
|
+
it('returns same output as without blueprint when blueprintContent is undefined', () => {
|
|
63
|
+
expect(generateClaudeMd(REACT_STACK)).toBe(generateClaudeMd(REACT_STACK, undefined))
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
it('includes a Features section when blueprint is provided', () => {
|
|
67
|
+
const result = generateClaudeMd(REACT_STACK, BLUEPRINT)
|
|
68
|
+
expect(result).toContain('## Features (Blueprint)')
|
|
69
|
+
expect(result).toContain('Authentication')
|
|
70
|
+
expect(result).toContain('Dashboard')
|
|
71
|
+
expect(result).toContain('API')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
it('includes blueprint sub-items', () => {
|
|
75
|
+
const result = generateClaudeMd(REACT_STACK, BLUEPRINT)
|
|
76
|
+
expect(result).toContain('JWT tokens')
|
|
77
|
+
expect(result).toContain('User statistics')
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('places Features section before Conventions', () => {
|
|
81
|
+
const result = generateClaudeMd(REACT_STACK, BLUEPRINT)
|
|
82
|
+
const featIdx = result.indexOf('## Features (Blueprint)')
|
|
83
|
+
const convIdx = result.indexOf('## Conventions')
|
|
84
|
+
expect(featIdx).toBeGreaterThan(-1)
|
|
85
|
+
expect(convIdx).toBeGreaterThan(-1)
|
|
86
|
+
expect(featIdx).toBeLessThan(convIdx)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('still contains stack-specific content', () => {
|
|
90
|
+
const result = generateClaudeMd(REACT_STACK, BLUEPRINT)
|
|
91
|
+
expect(result).toContain('React')
|
|
92
|
+
expect(result).toContain('## Stack')
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('generateWorkflow with blueprint', () => {
|
|
97
|
+
it('returns same output as without blueprint when blueprintContent is undefined', () => {
|
|
98
|
+
expect(generateWorkflow(REACT_STACK)).toBe(generateWorkflow(REACT_STACK, undefined))
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
it('generates one agent per blueprint feature plus a CI agent', () => {
|
|
102
|
+
const result = generateWorkflow(REACT_STACK, BLUEPRINT)
|
|
103
|
+
expect(result).toContain('Agent 1 · Authentication')
|
|
104
|
+
expect(result).toContain('Agent 2 · Dashboard')
|
|
105
|
+
expect(result).toContain('Agent 3 · API')
|
|
106
|
+
expect(result).toContain('Agent 4 · Tests & CI')
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('each agent block is parseable by extractAgentsFromWorkflow', () => {
|
|
110
|
+
const result = generateWorkflow(REACT_STACK, BLUEPRINT)
|
|
111
|
+
const agents = extractAgentsFromWorkflow(result)
|
|
112
|
+
expect(agents).toHaveLength(4)
|
|
113
|
+
expect(agents[0].name).toBe('Authentication')
|
|
114
|
+
expect(agents[0].slug).toBe('authentication')
|
|
115
|
+
expect(agents[3].name).toBe('Tests & CI')
|
|
116
|
+
expect(agents[3].slug).toBe('tests-ci')
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('includes blueprint feature items as outputs', () => {
|
|
120
|
+
const result = generateWorkflow(REACT_STACK, BLUEPRINT)
|
|
121
|
+
expect(result).toContain('JWT tokens')
|
|
122
|
+
expect(result).toContain('User statistics')
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('includes stack information in the header', () => {
|
|
126
|
+
const result = generateWorkflow(REACT_STACK, BLUEPRINT)
|
|
127
|
+
expect(result).toContain('react')
|
|
128
|
+
expect(result).toContain('typescript')
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { generateClaudeMd } from '../../src/generators/claudeMdGenerator.js'
|
|
3
|
+
import type { StackInfo } from '../../src/detectors/stackDetector.js'
|
|
4
|
+
|
|
5
|
+
function makeStack(
|
|
6
|
+
framework: StackInfo['framework'],
|
|
7
|
+
opts: Partial<Omit<StackInfo, 'framework'>> = {},
|
|
8
|
+
): StackInfo {
|
|
9
|
+
return {
|
|
10
|
+
framework,
|
|
11
|
+
language: framework === 'fastapi' ? 'python' : opts.hasTypeScript ? 'typescript' : 'javascript',
|
|
12
|
+
hasTypeScript: opts.hasTypeScript ?? false,
|
|
13
|
+
extras: opts.extras ?? [],
|
|
14
|
+
...opts,
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('generateClaudeMd', () => {
|
|
19
|
+
it('returns a non-empty string for every supported framework', () => {
|
|
20
|
+
const frameworks: StackInfo['framework'][] = [
|
|
21
|
+
'react', 'nextjs', 'tauri', 'fastapi', 'express', 'node', 'unknown',
|
|
22
|
+
]
|
|
23
|
+
for (const framework of frameworks) {
|
|
24
|
+
const result = generateClaudeMd(makeStack(framework))
|
|
25
|
+
expect(typeof result).toBe('string')
|
|
26
|
+
expect(result.length).toBeGreaterThan(0)
|
|
27
|
+
}
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
it('React — mentions React and expected sections', () => {
|
|
31
|
+
const result = generateClaudeMd(makeStack('react'))
|
|
32
|
+
expect(result).toContain('React')
|
|
33
|
+
expect(result).toContain('## Stack')
|
|
34
|
+
expect(result).toContain('## Commands')
|
|
35
|
+
expect(result).toContain('## Conventions')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('React — reflects TypeScript when hasTypeScript is true', () => {
|
|
39
|
+
const result = generateClaudeMd(makeStack('react', { hasTypeScript: true }))
|
|
40
|
+
expect(result).toContain('TypeScript')
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
it('React — reflects JavaScript when hasTypeScript is false', () => {
|
|
44
|
+
const result = generateClaudeMd(makeStack('react', { hasTypeScript: false }))
|
|
45
|
+
expect(result).toContain('JavaScript')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('Next.js — mentions Next.js and TypeScript', () => {
|
|
49
|
+
const result = generateClaudeMd(makeStack('nextjs', { hasTypeScript: true }))
|
|
50
|
+
expect(result).toContain('Next.js')
|
|
51
|
+
expect(result).toContain('TypeScript')
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
it('Next.js — includes Prisma section when extra is present', () => {
|
|
55
|
+
const result = generateClaudeMd(makeStack('nextjs', { extras: ['prisma'] }))
|
|
56
|
+
expect(result).toContain('Prisma')
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
it('Tauri — mentions Tauri and Rust', () => {
|
|
60
|
+
const result = generateClaudeMd(makeStack('tauri', { hasTypeScript: true }))
|
|
61
|
+
expect(result).toContain('Tauri')
|
|
62
|
+
expect(result).toContain('Rust')
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('FastAPI — mentions FastAPI and Python', () => {
|
|
66
|
+
const result = generateClaudeMd(makeStack('fastapi'))
|
|
67
|
+
expect(result).toContain('FastAPI')
|
|
68
|
+
expect(result).toContain('Python')
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
it('Express — mentions Express', () => {
|
|
72
|
+
const result = generateClaudeMd(makeStack('express'))
|
|
73
|
+
expect(result).toContain('Express')
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
it('Node — mentions Node.js', () => {
|
|
77
|
+
const result = generateClaudeMd(makeStack('node'))
|
|
78
|
+
expect(result).toContain('Node.js')
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('unknown — returns a fallback string', () => {
|
|
82
|
+
const result = generateClaudeMd(makeStack('unknown'))
|
|
83
|
+
expect(typeof result).toBe('string')
|
|
84
|
+
expect(result.length).toBeGreaterThan(0)
|
|
85
|
+
})
|
|
86
|
+
})
|