symfonia-ai-tools 1.7.1 → 1.9.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/README.md +44 -9
- package/lib/i18n.mjs +30 -0
- package/lib/installer.mjs +100 -0
- package/lib/questions.mjs +108 -3
- package/package.json +1 -1
- package/templates/packs/python-fastapi/_ai/instructions/api-schema.instructions.md +177 -0
- package/templates/packs/python-fastapi/_ai/instructions/module.instructions.md +211 -0
- package/templates/packs/python-fastapi/_ai/instructions/service-repository.instructions.md +363 -0
- package/templates/packs/python-fastapi/_ai/instructions/testing.instructions.md +295 -0
- package/templates/packs/python-fastapi/_ai/skills/smf-background-task/SKILL.md +244 -0
- package/templates/packs/python-fastapi/_ai/skills/smf-migration/SKILL.md +215 -0
- package/templates/packs/python-fastapi/_ai/skills/smf-new-endpoint/SKILL.md +188 -0
- package/templates/packs/python-fastapi/_ai/skills/smf-new-module/SKILL.md +303 -0
- package/templates/packs/python-fastapi/_ai/skills/smf-testing-integration/SKILL.md +218 -0
- package/templates/packs/python-fastapi/_ai/skills/smf-testing-manual/SKILL.md +254 -0
- package/templates/packs/python-fastapi/_ai/skills/smf-testing-unit/SKILL.md +215 -0
- package/templates/packs/python-fastapi/_guidelines.md +45 -0
- package/templates/packs/python-fastapi/pack.json +8 -0
package/README.md
CHANGED
|
@@ -19,21 +19,33 @@ symfonia-ai-tools
|
|
|
19
19
|
|
|
20
20
|
The configurator guides you through:
|
|
21
21
|
1. Language selection (Polish / English)
|
|
22
|
-
2.
|
|
23
|
-
3.
|
|
24
|
-
4.
|
|
25
|
-
5.
|
|
26
|
-
6.
|
|
27
|
-
7.
|
|
28
|
-
8.
|
|
22
|
+
2. **Install scope** — choose what to install
|
|
23
|
+
3. Project type, description, tech stack *(full setup only)*
|
|
24
|
+
4. Instruction & skill packs (checkboxes)
|
|
25
|
+
5. AI tools to configure (checkboxes)
|
|
26
|
+
6. Skills to install (checkboxes — base + from selected packs)
|
|
27
|
+
7. Paths, commands, CI pipeline, JIRA prefix *(full setup only)*
|
|
28
|
+
8. MCP servers (checkboxes + tokens)
|
|
29
|
+
9. CLI installation (checkboxes) *(full setup only)*
|
|
29
30
|
|
|
30
31
|
The entire configurator uses arrow-key navigation — no typing numbers.
|
|
31
32
|
|
|
32
|
-
Finally, it automatically runs GSD:
|
|
33
|
+
Finally (full setup only), it automatically runs GSD:
|
|
33
34
|
- **New project** → `/gsd-new-project`
|
|
34
35
|
- **Existing project** → `/gsd-map-codebase` → `/gsd-new-project`
|
|
35
36
|
|
|
36
|
-
###
|
|
37
|
+
### Install scopes
|
|
38
|
+
|
|
39
|
+
| Scope | What it installs | Use case |
|
|
40
|
+
|-------|-----------------|----------|
|
|
41
|
+
| **Full setup** | Everything — files, skills, MCP, CLI, GSD bootstrap | First-time setup or full reinstall |
|
|
42
|
+
| **Skills only** | Selected skills → `.ai/skills/` + mirrors | Add new skills to existing project |
|
|
43
|
+
| **MCP only** | MCP server configuration | Add/update MCP servers |
|
|
44
|
+
| **Skills + MCP** | Skills and MCP servers | Add skills and MCP in one go |
|
|
45
|
+
|
|
46
|
+
Slim scopes (skills only, MCP only, skills + MCP) skip project metadata, commands, CI, JIRA, and CLI installation — they only ask for what's needed.
|
|
47
|
+
|
|
48
|
+
### Installation modes (existing project, full setup)
|
|
37
49
|
|
|
38
50
|
| Mode | Description |
|
|
39
51
|
|------|-------------|
|
|
@@ -73,6 +85,7 @@ Instead of choosing a single stack, you pick any combination of instruction & sk
|
|
|
73
85
|
| **Vitest** | 1 instruction | Yes |
|
|
74
86
|
| **Storybook** | 1 instruction | No |
|
|
75
87
|
| **Laravel + PHP** | 4 instructions, 7 skills | No |
|
|
88
|
+
| **Python FastAPI** | 4 instructions, 7 skills | No |
|
|
76
89
|
| **Playwright E2E** | 1 instruction, 3 skills | No |
|
|
77
90
|
| **Docker** | 1 instruction | No |
|
|
78
91
|
|
|
@@ -155,6 +168,15 @@ When AI opens/edits a file matching `applyTo`, it automatically loads those inst
|
|
|
155
168
|
| `api-resource.instructions.md` | `**/*Resource*.php, **/*Request*.php` | Resources with whenLoaded(), Form Requests, Policies |
|
|
156
169
|
| `testing.instructions.md` | `**/*Test*.php, **/tests/**` | PHPUnit, data providers, factories, Laravel fakes |
|
|
157
170
|
|
|
171
|
+
### Python FastAPI (4 instructions)
|
|
172
|
+
|
|
173
|
+
| Instruction | applyTo | What it teaches AI |
|
|
174
|
+
|-------------|---------|-------------------|
|
|
175
|
+
| `api-schema.instructions.md` | `**/schemas.py, **/*schema*.py, **/*request*.py, **/*response*.py` | Pydantic v2 schemas, StrEnum status fields, `PaginatedResponse[T]`, frozen request models, `ErrorResponse` |
|
|
176
|
+
| `module.instructions.md` | `**/modules/**/router.py, **/modules/**/dependencies.py, **/modules/**/domain/**, **/modules/**/infrastructure/**` | Module structure, FastAPI router pattern, SQLAlchemy 2.0 ORM models, `lifespan=` setup, DI wiring |
|
|
177
|
+
| `service-repository.instructions.md` | `**/domain/services.py, **/infrastructure/repositories/**, **/domain/entities.py, **/domain/value_objects.py` | CQRS repos, domain entities, EventPublisher pattern, layer isolation rules |
|
|
178
|
+
| `testing.instructions.md` | `**/tests/**, **/*test*.py, **/conftest.py` | pytest-asyncio conftest, `async_sessionmaker`, rollback pattern, `AsyncClient` |
|
|
179
|
+
|
|
158
180
|
### Playwright (1 instruction)
|
|
159
181
|
|
|
160
182
|
| Instruction | applyTo | What it teaches AI |
|
|
@@ -267,6 +289,18 @@ All skills have the `smf-` prefix (Symfonia). The configurator lets you choose w
|
|
|
267
289
|
| `smf-testing-unit` | Unit tests | Isolated tests without DB, PHPUnit mocks, data providers |
|
|
268
290
|
| `smf-testing-manual` | Manual HTTP tests | JetBrains HTTP Client — `.http` files for interactive API testing |
|
|
269
291
|
|
|
292
|
+
### Python FastAPI skills (7 skills)
|
|
293
|
+
|
|
294
|
+
| Skill | Purpose | When to use |
|
|
295
|
+
|-------|---------|-------------|
|
|
296
|
+
| `smf-new-module` | New module from scratch | router → schemas → ORM model → CQRS repos → service → migration → tests |
|
|
297
|
+
| `smf-new-endpoint` | New endpoint in existing module | request schema → route → service method → response schema → tests |
|
|
298
|
+
| `smf-migration` | Alembic migration | autogenerate → review → upgrade → verify; async env.py setup |
|
|
299
|
+
| `smf-background-task` | Celery background task | task def (sync) → `SyncSessionLocal` → dispatch from router → beat schedule → tests |
|
|
300
|
+
| `smf-testing-integration` | Integration tests (HTTP) | `AsyncClient` → real DB via rollback conftest → router assertions |
|
|
301
|
+
| `smf-testing-unit` | Unit tests (service) | mock repos → service logic → no DB needed |
|
|
302
|
+
| `smf-testing-manual` | Manual HTTP tests | JetBrains HTTP Client `.http` files for interactive API testing |
|
|
303
|
+
|
|
270
304
|
### Playwright skills (3 skills)
|
|
271
305
|
|
|
272
306
|
| Skill | Purpose | When to use |
|
|
@@ -524,6 +558,7 @@ templates/
|
|
|
524
558
|
├── vitest/ # 1 instr.
|
|
525
559
|
├── storybook/ # 1 instr.
|
|
526
560
|
├── laravel/ # 4 instr. · 7 skills
|
|
561
|
+
├── python-fastapi/ # 4 instr. · 7 skills
|
|
527
562
|
├── playwright/ # 1 instr. · 3 skills
|
|
528
563
|
└── docker/ # 1 instr.
|
|
529
564
|
```
|
package/lib/i18n.mjs
CHANGED
|
@@ -32,6 +32,15 @@ const translations = {
|
|
|
32
32
|
'q.quit_hint': 'Wpisz',
|
|
33
33
|
'q.quit_hint2': 'aby przerwac w dowolnym momencie.',
|
|
34
34
|
|
|
35
|
+
// questions.mjs — install scope
|
|
36
|
+
'q.section.scope': 'Zakres instalacji',
|
|
37
|
+
'q.scope.select': 'Co chcesz zainstalowac?',
|
|
38
|
+
'q.scope.full': 'Pelny setup',
|
|
39
|
+
'q.scope.full_hint': '(pliki, skille, MCP, CLI)',
|
|
40
|
+
'q.scope.skills_only': 'Tylko skille',
|
|
41
|
+
'q.scope.mcp_only': 'Tylko serwery MCP',
|
|
42
|
+
'q.scope.skills_mcp': 'Skille + MCP',
|
|
43
|
+
|
|
35
44
|
// questions.mjs — project
|
|
36
45
|
'q.section.project': 'Projekt',
|
|
37
46
|
'q.project_type': 'Typ projektu:',
|
|
@@ -143,12 +152,16 @@ const translations = {
|
|
|
143
152
|
'i.mode.overwrite': 'nadpisz wszystko',
|
|
144
153
|
'i.mode.skip': 'tylko nowe pliki',
|
|
145
154
|
'i.mode.mcp': 'tylko MCP',
|
|
155
|
+
'i.mode.skills': 'tylko skille',
|
|
156
|
+
'i.mode.skills_mcp': 'skille + MCP',
|
|
146
157
|
'i.dir': 'Katalog:',
|
|
147
158
|
'i.mode_label': 'Tryb:',
|
|
148
159
|
'i.step.base': 'Szablony bazowe',
|
|
149
160
|
'i.step.pack': 'Pakiet',
|
|
150
161
|
'i.step.guidelines': 'Skladanie',
|
|
151
162
|
'i.step.mirror': 'Mirror',
|
|
163
|
+
'i.step.skills': 'Kopiowanie skilli',
|
|
164
|
+
'i.step.skills_mirror': 'Mirror skilli',
|
|
152
165
|
'i.step.mcp': 'Konfiguracja MCP',
|
|
153
166
|
'i.step.cli': 'Instalacja CLI',
|
|
154
167
|
|
|
@@ -170,6 +183,8 @@ const translations = {
|
|
|
170
183
|
|
|
171
184
|
// installer.mjs — next steps
|
|
172
185
|
'i.done.mcp': 'Konfiguracja MCP zakonczona!',
|
|
186
|
+
'i.done.skills': 'Instalacja skilli zakonczona!',
|
|
187
|
+
'i.done.skills_mcp': 'Instalacja skilli i MCP zakonczona!',
|
|
173
188
|
'i.done.install': 'Instalacja zakonczona!',
|
|
174
189
|
'i.done.check_mcp': 'Sprawdz MCP:',
|
|
175
190
|
'i.done.install_gsd': 'Zainstaluj GSD',
|
|
@@ -213,6 +228,15 @@ const translations = {
|
|
|
213
228
|
'q.quit_hint': 'Type',
|
|
214
229
|
'q.quit_hint2': 'to abort at any time.',
|
|
215
230
|
|
|
231
|
+
// questions.mjs — install scope
|
|
232
|
+
'q.section.scope': 'Installation scope',
|
|
233
|
+
'q.scope.select': 'What do you want to install?',
|
|
234
|
+
'q.scope.full': 'Full setup',
|
|
235
|
+
'q.scope.full_hint': '(files, skills, MCP, CLI)',
|
|
236
|
+
'q.scope.skills_only': 'Skills only',
|
|
237
|
+
'q.scope.mcp_only': 'MCP servers only',
|
|
238
|
+
'q.scope.skills_mcp': 'Skills + MCP',
|
|
239
|
+
|
|
216
240
|
// questions.mjs — project
|
|
217
241
|
'q.section.project': 'Project',
|
|
218
242
|
'q.project_type': 'Project type:',
|
|
@@ -324,12 +348,16 @@ const translations = {
|
|
|
324
348
|
'i.mode.overwrite': 'overwrite all',
|
|
325
349
|
'i.mode.skip': 'new files only',
|
|
326
350
|
'i.mode.mcp': 'MCP only',
|
|
351
|
+
'i.mode.skills': 'skills only',
|
|
352
|
+
'i.mode.skills_mcp': 'skills + MCP',
|
|
327
353
|
'i.dir': 'Directory:',
|
|
328
354
|
'i.mode_label': 'Mode:',
|
|
329
355
|
'i.step.base': 'Base templates',
|
|
330
356
|
'i.step.pack': 'Pack',
|
|
331
357
|
'i.step.guidelines': 'Assembling',
|
|
332
358
|
'i.step.mirror': 'Mirror',
|
|
359
|
+
'i.step.skills': 'Copying skills',
|
|
360
|
+
'i.step.skills_mirror': 'Mirroring skills',
|
|
333
361
|
'i.step.mcp': 'MCP configuration',
|
|
334
362
|
'i.step.cli': 'CLI installation',
|
|
335
363
|
|
|
@@ -351,6 +379,8 @@ const translations = {
|
|
|
351
379
|
|
|
352
380
|
// installer.mjs — next steps
|
|
353
381
|
'i.done.mcp': 'MCP configuration complete!',
|
|
382
|
+
'i.done.skills': 'Skills installation complete!',
|
|
383
|
+
'i.done.skills_mcp': 'Skills & MCP installation complete!',
|
|
354
384
|
'i.done.install': 'Installation complete!',
|
|
355
385
|
'i.done.check_mcp': 'Check MCP:',
|
|
356
386
|
'i.done.install_gsd': 'Install GSD',
|
package/lib/installer.mjs
CHANGED
|
@@ -116,6 +116,12 @@ export async function install(packageRoot, answers) {
|
|
|
116
116
|
const templatesDir = join(packageRoot, 'templates');
|
|
117
117
|
const mode = answers.installMode || 'fresh';
|
|
118
118
|
|
|
119
|
+
// Handle slim install scopes (skills-only, mcp-only, skills+mcp)
|
|
120
|
+
const installScope = answers.installScope || 'full';
|
|
121
|
+
if (installScope !== 'full') {
|
|
122
|
+
return await installSlim(targetDir, templatesDir, answers, installScope);
|
|
123
|
+
}
|
|
124
|
+
|
|
119
125
|
const modeLabel = mode === 'fresh' ? boldGreen(t('i.mode.fresh'))
|
|
120
126
|
: mode === 'overwrite' ? boldYellow(t('i.mode.overwrite'))
|
|
121
127
|
: mode === 'skip-existing' ? boldCyan(t('i.mode.skip'))
|
|
@@ -183,6 +189,100 @@ export async function install(packageRoot, answers) {
|
|
|
183
189
|
}
|
|
184
190
|
}
|
|
185
191
|
|
|
192
|
+
// ─── Slim install (skills-only, mcp-only, skills+mcp) ───
|
|
193
|
+
|
|
194
|
+
async function installSlim(targetDir, templatesDir, answers, scope) {
|
|
195
|
+
const needsSkills = scope !== 'mcp-only';
|
|
196
|
+
const needsMcp = scope !== 'skills-only';
|
|
197
|
+
const needsMirror = needsSkills && (answers.toolCopilot || answers.toolClaude);
|
|
198
|
+
|
|
199
|
+
const totalSteps = (needsSkills ? 1 : 0) + (needsMirror ? 1 : 0) + (needsMcp ? 1 : 0);
|
|
200
|
+
let currentStep = 0;
|
|
201
|
+
|
|
202
|
+
const modeLabel = scope === 'skills-only' ? boldCyan(t('i.mode.skills'))
|
|
203
|
+
: scope === 'mcp-only' ? boldCyan(t('i.mode.mcp'))
|
|
204
|
+
: boldCyan(t('i.mode.skills_mcp'));
|
|
205
|
+
|
|
206
|
+
section(t('i.section.install'));
|
|
207
|
+
console.log(info(`${t('i.dir')} ${bold(targetDir)}`));
|
|
208
|
+
console.log(info(`${t('i.mode_label')} ${modeLabel}`));
|
|
209
|
+
console.log('');
|
|
210
|
+
|
|
211
|
+
if (needsSkills) {
|
|
212
|
+
step(++currentStep, totalSteps, t('i.step.skills'));
|
|
213
|
+
await installSkills(templatesDir, targetDir, answers);
|
|
214
|
+
|
|
215
|
+
if (needsMirror) {
|
|
216
|
+
step(++currentStep, totalSteps, t('i.step.skills_mirror'));
|
|
217
|
+
const aiSkillsDir = join(targetDir, '.ai', 'skills');
|
|
218
|
+
if (answers.toolCopilot) {
|
|
219
|
+
await mirrorDir(aiSkillsDir, join(targetDir, '.github', 'skills'), targetDir, 'overwrite');
|
|
220
|
+
}
|
|
221
|
+
if (answers.toolClaude) {
|
|
222
|
+
await mirrorDir(aiSkillsDir, join(targetDir, '.claude', 'skills'), targetDir, 'overwrite');
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (needsMcp) {
|
|
228
|
+
step(++currentStep, totalSteps, t('i.step.mcp'));
|
|
229
|
+
await generateMcpConfig(targetDir, answers);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
console.log('');
|
|
233
|
+
const doneKey = scope === 'skills-only' ? 'i.done.skills'
|
|
234
|
+
: scope === 'mcp-only' ? 'i.done.mcp'
|
|
235
|
+
: 'i.done.skills_mcp';
|
|
236
|
+
box([boldGreen(` ${t(doneKey)}`)], boldGreen);
|
|
237
|
+
console.log('');
|
|
238
|
+
|
|
239
|
+
if (needsMcp && answers.toolClaude) {
|
|
240
|
+
console.log(info(`${t('i.done.check_mcp')} ${cyan('claude mcp list')}`));
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function installSkills(templatesDir, targetDir, answers) {
|
|
245
|
+
const selectedSkills = answers.selectedSkills || [];
|
|
246
|
+
if (selectedSkills.length === 0) {
|
|
247
|
+
console.log(` ${dim(t('q.skills.none'))}`);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const aiSkillsDir = join(targetDir, '.ai', 'skills');
|
|
252
|
+
await mkdir(aiSkillsDir, { recursive: true });
|
|
253
|
+
|
|
254
|
+
// Base skills
|
|
255
|
+
const baseSkillsDir = join(templatesDir, 'base', '_ai', 'skills');
|
|
256
|
+
try {
|
|
257
|
+
const entries = await readdir(baseSkillsDir, { withFileTypes: true });
|
|
258
|
+
for (const e of entries) {
|
|
259
|
+
if (e.isDirectory() && selectedSkills.includes(e.name)) {
|
|
260
|
+
const dest = join(aiSkillsDir, e.name);
|
|
261
|
+
await mkdir(dest, { recursive: true });
|
|
262
|
+
await copyTemplateDir(join(baseSkillsDir, e.name), dest, answers, 'overwrite');
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
} catch { /* no base skills */ }
|
|
266
|
+
|
|
267
|
+
// Pack skills
|
|
268
|
+
const allPacks = await loadPacks(join(templatesDir, 'packs'));
|
|
269
|
+
for (const packId of (answers.packs || [])) {
|
|
270
|
+
const pack = allPacks[packId];
|
|
271
|
+
if (!pack) continue;
|
|
272
|
+
const packSkillsDir = join(pack._dir, '_ai', 'skills');
|
|
273
|
+
try {
|
|
274
|
+
const entries = await readdir(packSkillsDir, { withFileTypes: true });
|
|
275
|
+
for (const e of entries) {
|
|
276
|
+
if (e.isDirectory() && selectedSkills.includes(e.name)) {
|
|
277
|
+
const dest = join(aiSkillsDir, e.name);
|
|
278
|
+
await mkdir(dest, { recursive: true });
|
|
279
|
+
await copyTemplateDir(join(packSkillsDir, e.name), dest, answers, 'overwrite');
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
} catch { /* no pack skills */ }
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
186
286
|
// ─── Guidelines assembly ───
|
|
187
287
|
|
|
188
288
|
async function assembleGuidelines(templatesDir, targetDir, answers, selectedPacks, allPacks, mode) {
|
package/lib/questions.mjs
CHANGED
|
@@ -80,6 +80,25 @@ export async function askQuestions(packsDir) {
|
|
|
80
80
|
console.log('');
|
|
81
81
|
console.log(` ${dim(t('q.quit_hint'))} ${yellow('"q"')} ${dim(t('q.quit_hint2'))}`);
|
|
82
82
|
|
|
83
|
+
// --- Install scope ---
|
|
84
|
+
section(t('q.section.scope'));
|
|
85
|
+
answers.installScope = await askChoice(t('q.scope.select'), [
|
|
86
|
+
'full',
|
|
87
|
+
'skills-only',
|
|
88
|
+
'mcp-only',
|
|
89
|
+
'skills+mcp',
|
|
90
|
+
], [
|
|
91
|
+
`${boldGreen(t('q.scope.full'))} ${dim(t('q.scope.full_hint'))}`,
|
|
92
|
+
`${boldCyan(t('q.scope.skills_only'))}`,
|
|
93
|
+
`${boldCyan(t('q.scope.mcp_only'))}`,
|
|
94
|
+
`${boldCyan(t('q.scope.skills_mcp'))}`,
|
|
95
|
+
]);
|
|
96
|
+
|
|
97
|
+
// Slim flows — early return
|
|
98
|
+
if (answers.installScope !== 'full') {
|
|
99
|
+
return await askSlimFlow(answers, packsDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
83
102
|
// --- Project type ---
|
|
84
103
|
section(t('q.section.project'));
|
|
85
104
|
|
|
@@ -156,9 +175,10 @@ export async function askQuestions(packsDir) {
|
|
|
156
175
|
|
|
157
176
|
const hasVue = answers.packs.includes('vue3');
|
|
158
177
|
const hasLaravel = answers.packs.includes('laravel');
|
|
159
|
-
|
|
160
|
-
answers.
|
|
161
|
-
answers.
|
|
178
|
+
const hasPythonFastapi = answers.packs.includes('python-fastapi');
|
|
179
|
+
answers.testCommand = await ask(t('q.cmd.test'), hasVue ? 'npm run test' : hasLaravel ? 'php artisan test' : hasPythonFastapi ? 'pytest' : 'npm test');
|
|
180
|
+
answers.buildCommand = await ask(t('q.cmd.build'), hasVue ? 'npm run build' : hasLaravel ? 'composer build' : hasPythonFastapi ? 'docker build .' : 'npm run build');
|
|
181
|
+
answers.lintCommand = await ask(t('q.cmd.lint'), hasVue ? 'npm run lint' : hasLaravel ? 'php artisan pint' : hasPythonFastapi ? 'ruff check . && mypy .' : 'npm run lint');
|
|
162
182
|
|
|
163
183
|
// --- CI ---
|
|
164
184
|
section(t('q.section.ci'));
|
|
@@ -493,3 +513,88 @@ function printSummary(answers, allPacks) {
|
|
|
493
513
|
tableRow(t('q.summary.gsd'), cyan(gsdSteps.join(dim(' → '))));
|
|
494
514
|
}
|
|
495
515
|
}
|
|
516
|
+
|
|
517
|
+
// ─── Slim flow (skills-only, mcp-only, skills+mcp) ───
|
|
518
|
+
|
|
519
|
+
async function askSlimFlow(answers, packsDir) {
|
|
520
|
+
const scope = answers.installScope;
|
|
521
|
+
const needsSkills = scope !== 'mcp-only';
|
|
522
|
+
const needsMcp = scope !== 'skills-only';
|
|
523
|
+
|
|
524
|
+
// --- Target directory ---
|
|
525
|
+
answers.targetDir = await ask(t('q.target_dir'), '.');
|
|
526
|
+
answers.installMode = 'overwrite';
|
|
527
|
+
|
|
528
|
+
// --- Tool selection ---
|
|
529
|
+
await askToolSelection(answers);
|
|
530
|
+
|
|
531
|
+
// --- Skills flow ---
|
|
532
|
+
let allPacks = {};
|
|
533
|
+
if (needsSkills) {
|
|
534
|
+
allPacks = await loadPacks(packsDir);
|
|
535
|
+
await askPackSelection(answers, allPacks);
|
|
536
|
+
const baseSkillsDir = join(packsDir, '..', 'base', '_ai', 'skills');
|
|
537
|
+
await askSkillSelection(answers, allPacks, baseSkillsDir);
|
|
538
|
+
await askPackPlaceholders(answers, allPacks);
|
|
539
|
+
checkCliDeps(answers.selectedSkills || []);
|
|
540
|
+
} else {
|
|
541
|
+
answers.packs = [];
|
|
542
|
+
answers.selectedSkills = [];
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// --- MCP flow ---
|
|
546
|
+
if (needsMcp) {
|
|
547
|
+
const requiredMcps = collectRequiredMcps(answers.selectedSkills || []);
|
|
548
|
+
await askMcpServers(answers, requiredMcps);
|
|
549
|
+
} else {
|
|
550
|
+
answers.mcpJira = false;
|
|
551
|
+
answers.mcpBitbucket = false;
|
|
552
|
+
answers.mcpFigma = false;
|
|
553
|
+
answers.mcpGrafana = false;
|
|
554
|
+
answers.mcpContext7 = false;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
// --- Defaults for unused fields ---
|
|
558
|
+
answers.installClaudeCli = false;
|
|
559
|
+
answers.installGsd = false;
|
|
560
|
+
|
|
561
|
+
// --- Summary ---
|
|
562
|
+
printSlimSummary(answers, allPacks);
|
|
563
|
+
|
|
564
|
+
const proceed = await askYN(`\n ${t('q.summary.continue')}`, true);
|
|
565
|
+
if (!proceed) throw new Error('USER_ABORT');
|
|
566
|
+
|
|
567
|
+
return answers;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
function printSlimSummary(answers, allPacks) {
|
|
571
|
+
section(t('q.section.summary'));
|
|
572
|
+
|
|
573
|
+
tableRow(t('q.summary.dir'), answers.targetDir);
|
|
574
|
+
|
|
575
|
+
const tools = [];
|
|
576
|
+
if (answers.toolClaude) tools.push('Claude');
|
|
577
|
+
if (answers.toolCopilot) tools.push('Copilot');
|
|
578
|
+
if (answers.toolCursor) tools.push('Cursor');
|
|
579
|
+
if (answers.toolGemini) tools.push('Gemini');
|
|
580
|
+
if (answers.toolJunie) tools.push('Junie');
|
|
581
|
+
tableRow(t('q.summary.tools'), tools.length > 0 ? cyan(tools.join(dim(' · '))) : dim(t('q.summary.none')));
|
|
582
|
+
|
|
583
|
+
if (answers.installScope !== 'mcp-only') {
|
|
584
|
+
const packNames = (answers.packs || []).map(id => allPacks[id]?.name || id);
|
|
585
|
+
tableRow(t('q.summary.packs'), packNames.length > 0 ? cyan(packNames.join(dim(' · '))) : dim(t('q.summary.none')));
|
|
586
|
+
|
|
587
|
+
const skills = answers.selectedSkills || [];
|
|
588
|
+
tableRow(t('q.summary.skills'), skills.length > 0 ? cyan(skills.join(dim(' · '))) : dim(t('q.summary.none')));
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
if (answers.installScope !== 'skills-only') {
|
|
592
|
+
const mcps = [];
|
|
593
|
+
if (answers.mcpJira) mcps.push('Jira');
|
|
594
|
+
if (answers.mcpBitbucket) mcps.push('Bitbucket');
|
|
595
|
+
if (answers.mcpFigma) mcps.push('Figma');
|
|
596
|
+
if (answers.mcpGrafana) mcps.push('Grafana');
|
|
597
|
+
if (answers.mcpContext7) mcps.push('Context7');
|
|
598
|
+
tableRow(t('q.summary.mcp'), mcps.length > 0 ? cyan(mcps.join(dim(' · '))) : dim(t('q.summary.none')));
|
|
599
|
+
}
|
|
600
|
+
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
---
|
|
2
|
+
applyTo: "**/schemas.py,**/*schema*.py,**/*request*.py,**/*response*.py"
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
# API Schemas, Request & Response Models — Instructions
|
|
6
|
+
|
|
7
|
+
## Pydantic v2 Schemas
|
|
8
|
+
|
|
9
|
+
Never return ORM models directly from endpoints. All input/output passes through Pydantic schemas.
|
|
10
|
+
|
|
11
|
+
```python
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
from uuid import UUID
|
|
16
|
+
|
|
17
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class FeatureResponse(BaseModel):
|
|
21
|
+
model_config = ConfigDict(from_attributes=True)
|
|
22
|
+
|
|
23
|
+
id: UUID
|
|
24
|
+
name: str
|
|
25
|
+
description: str | None
|
|
26
|
+
status: FeatureStatus # use the StrEnum — not bare `str`
|
|
27
|
+
created_at: datetime
|
|
28
|
+
|
|
29
|
+
# Nested relations — use None default (loaded on demand)
|
|
30
|
+
author: UserResponse | None = None
|
|
31
|
+
category: CategoryResponse | None = None
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Request Schemas (Input Validation)
|
|
35
|
+
|
|
36
|
+
Request schemas are **read-only once validated** — add `frozen=True` to catch accidental mutation:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
42
|
+
from uuid import UUID
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StoreFeatureRequest(BaseModel):
|
|
46
|
+
model_config = ConfigDict(frozen=True)
|
|
47
|
+
|
|
48
|
+
name: str = Field(min_length=2, max_length=255)
|
|
49
|
+
description: str | None = Field(default=None, max_length=5000)
|
|
50
|
+
category_id: UUID | None = None
|
|
51
|
+
status: FeatureStatus
|
|
52
|
+
tags: list[str] = Field(default_factory=list)
|
|
53
|
+
|
|
54
|
+
@field_validator("tags")
|
|
55
|
+
@classmethod
|
|
56
|
+
def validate_tags(cls, v: list[str]) -> list[str]:
|
|
57
|
+
if any(len(tag) > 50 for tag in v):
|
|
58
|
+
raise ValueError("Each tag must be at most 50 characters")
|
|
59
|
+
return v
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class UpdateFeatureRequest(BaseModel):
|
|
63
|
+
model_config = ConfigDict(frozen=True)
|
|
64
|
+
|
|
65
|
+
name: str | None = Field(default=None, min_length=2, max_length=255)
|
|
66
|
+
description: str | None = None
|
|
67
|
+
status: FeatureStatus | None = None
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### List / Index Request with Filtering and Sorting
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
from __future__ import annotations
|
|
74
|
+
|
|
75
|
+
from typing import Literal
|
|
76
|
+
from pydantic import BaseModel, Field
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class IndexFeatureRequest(BaseModel):
|
|
80
|
+
page: int = Field(default=1, ge=1)
|
|
81
|
+
limit: int = Field(default=20, ge=1, le=100)
|
|
82
|
+
status: FeatureStatus | None = None
|
|
83
|
+
sort_by: Literal["created_at", "name", "status"] = "created_at"
|
|
84
|
+
sort_dir: Literal["asc", "desc"] = "desc"
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Paginated Collection Response
|
|
88
|
+
|
|
89
|
+
`PaginatedResponse` and `PaginationMeta` are **shared types** — define them once in `app/core/schemas.py`, never per-module:
|
|
90
|
+
|
|
91
|
+
```python
|
|
92
|
+
# app/core/schemas.py
|
|
93
|
+
from __future__ import annotations
|
|
94
|
+
|
|
95
|
+
from typing import Generic, TypeVar
|
|
96
|
+
from pydantic import BaseModel
|
|
97
|
+
|
|
98
|
+
T = TypeVar("T")
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class PaginationMeta(BaseModel):
|
|
102
|
+
total: int
|
|
103
|
+
per_page: int
|
|
104
|
+
current_page: int
|
|
105
|
+
last_page: int
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class PaginatedResponse(BaseModel, Generic[T]):
|
|
109
|
+
data: list[T]
|
|
110
|
+
meta: PaginationMeta
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Import from `app.core.schemas` in every module that needs it.
|
|
114
|
+
|
|
115
|
+
## Error Responses
|
|
116
|
+
|
|
117
|
+
Standardize the error body shape. Define `ErrorResponse` in `app/core/schemas.py`:
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
# app/core/schemas.py
|
|
121
|
+
class ErrorResponse(BaseModel):
|
|
122
|
+
code: str # machine-readable code: "FEATURE_NOT_FOUND"
|
|
123
|
+
message: str # human-readable description
|
|
124
|
+
field: str | None = None # which field caused the error (optional)
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
Raise via `HTTPException` — the exception handler converts it:
|
|
128
|
+
|
|
129
|
+
```python
|
|
130
|
+
from fastapi import HTTPException, status
|
|
131
|
+
|
|
132
|
+
# In service (expected domain errors):
|
|
133
|
+
raise HTTPException(
|
|
134
|
+
status_code=status.HTTP_404_NOT_FOUND,
|
|
135
|
+
detail={"code": "FEATURE_NOT_FOUND", "message": "Feature not found"},
|
|
136
|
+
)
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Register a handler in `app/main.py` to enforce consistent shape:
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from fastapi import Request
|
|
143
|
+
from fastapi.responses import JSONResponse
|
|
144
|
+
|
|
145
|
+
@app.exception_handler(HTTPException)
|
|
146
|
+
async def http_exception_handler(request: Request, exc: HTTPException) -> JSONResponse:
|
|
147
|
+
if isinstance(exc.detail, dict):
|
|
148
|
+
body = exc.detail
|
|
149
|
+
else:
|
|
150
|
+
body = {"code": "HTTP_ERROR", "message": str(exc.detail)}
|
|
151
|
+
return JSONResponse(status_code=exc.status_code, content=body)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Enums
|
|
155
|
+
|
|
156
|
+
```python
|
|
157
|
+
from enum import StrEnum
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class FeatureStatus(StrEnum):
|
|
161
|
+
DRAFT = "draft"
|
|
162
|
+
ACTIVE = "active"
|
|
163
|
+
ARCHIVED = "archived"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
Use `StrEnum` (Python 3.11+) — auto-serializes to string in JSON.
|
|
167
|
+
|
|
168
|
+
## Rules
|
|
169
|
+
|
|
170
|
+
- `from_attributes=True` on every response schema that reads from ORM
|
|
171
|
+
- Never expose internal IDs as integers — use UUID
|
|
172
|
+
- Use `Field(...)` for constraints, not just annotations
|
|
173
|
+
- `StrEnum` for all enums — clean JSON serialization, use in both request and response schemas
|
|
174
|
+
- Nested relations are always `| None = None` — populate only when loaded
|
|
175
|
+
- `frozen=True` on all request/input schemas — prevents accidental mutation after validation
|
|
176
|
+
- `PaginatedResponse[T]` and `PaginationMeta` live in `app/core/schemas.py` — import, never redefine
|
|
177
|
+
- `ErrorResponse` body has `code` (machine) and `message` (human) — register a global exception handler
|