symfonia-ai-tools 1.0.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 +489 -0
- package/bin/cli.mjs +35 -0
- package/lib/installer.mjs +495 -0
- package/lib/questions.mjs +332 -0
- package/lib/ui.mjs +76 -0
- package/lib/utils.mjs +231 -0
- package/package.json +26 -0
- package/templates/base/CLAUDE.md +34 -0
- package/templates/base/_ai/_guidelines_header.md +70 -0
- package/templates/base/_ai/context/README.md +20 -0
- package/templates/base/_ai/prompts/codereview.prompt.md +324 -0
- package/templates/base/_ai/prompts/duplicate-code-analysis.prompt.md +128 -0
- package/templates/base/_ai/prompts/figma-analysis.prompt.md +155 -0
- package/templates/base/_ai/prompts/security-review.prompt.md +46 -0
- package/templates/base/_ai/skills/README.md +80 -0
- package/templates/base/_ai/skills/TEMPLATE.md +106 -0
- package/templates/base/_ai/skills/babysit-prs/SKILL.md +105 -0
- package/templates/base/_ai/skills/debug/SKILL.md +93 -0
- package/templates/base/_ai/skills/fill-worklogs/SKILL.md +158 -0
- package/templates/base/_ai/skills/hotfix/SKILL.md +52 -0
- package/templates/base/_ai/skills/jira-task/SKILL.md +170 -0
- package/templates/base/_ai/skills/my-prs/SKILL.md +78 -0
- package/templates/base/_ai/skills/pr-dashboard/SKILL.md +43 -0
- package/templates/base/_ai/skills/pr-prepare/SKILL.md +106 -0
- package/templates/base/_ai/skills/refactor/SKILL.md +87 -0
- package/templates/base/_ai/skills/write-tests/SKILL.md +109 -0
- package/templates/base/_claude/settings.local.json +37 -0
- package/templates/base/_cursor/rules/global.mdc +7 -0
- package/templates/base/_editorconfig +18 -0
- package/templates/base/_gemini/settings.json +3 -0
- package/templates/base/_github/copilot-instructions.md +1 -0
- package/templates/base/_github/pull_request_template.md +23 -0
- package/templates/base/_gitignore +22 -0
- package/templates/base/_junie/guidelines.md +1 -0
- package/templates/base/commit-instructions.md +92 -0
- package/templates/packs/docker/_ai/instructions/docker.instructions.md +193 -0
- package/templates/packs/docker/_guidelines.md +10 -0
- package/templates/packs/docker/pack.json +8 -0
- package/templates/packs/laravel/_ai/instructions/api-resource.instructions.md +251 -0
- package/templates/packs/laravel/_ai/instructions/module.instructions.md +133 -0
- package/templates/packs/laravel/_ai/instructions/service-repository.instructions.md +215 -0
- package/templates/packs/laravel/_ai/instructions/testing.instructions.md +278 -0
- package/templates/packs/laravel/_ai/skills/migration/SKILL.md +172 -0
- package/templates/packs/laravel/_ai/skills/new-endpoint/SKILL.md +165 -0
- package/templates/packs/laravel/_ai/skills/new-module/SKILL.md +208 -0
- package/templates/packs/laravel/_ai/skills/queued-job/SKILL.md +248 -0
- package/templates/packs/laravel/_ai/skills/testing-feature/SKILL.md +196 -0
- package/templates/packs/laravel/_ai/skills/testing-manual/SKILL.md +186 -0
- package/templates/packs/laravel/_ai/skills/testing-unit/SKILL.md +200 -0
- package/templates/packs/laravel/_guidelines.md +25 -0
- package/templates/packs/laravel/pack.json +6 -0
- package/templates/packs/playwright/_ai/instructions/playwright.instructions.md +219 -0
- package/templates/packs/playwright/_ai/skills/playwright/README.md +194 -0
- package/templates/packs/playwright/_ai/skills/playwright/SKILL.md +1245 -0
- package/templates/packs/playwright/_ai/skills/playwright-codereview/SKILL.md +642 -0
- package/templates/packs/playwright/_ai/skills/playwright-record/README.md +87 -0
- package/templates/packs/playwright/_ai/skills/playwright-record/SKILL.md +564 -0
- package/templates/packs/playwright/_guidelines.md +12 -0
- package/templates/packs/playwright/pack.json +9 -0
- package/templates/packs/storybook/_ai/instructions/storybook.instructions.md +181 -0
- package/templates/packs/storybook/pack.json +6 -0
- package/templates/packs/vitest/_ai/instructions/vitest.instructions.md +688 -0
- package/templates/packs/vitest/pack.json +6 -0
- package/templates/packs/vue3/_ai/instructions/api.instructions.md +163 -0
- package/templates/packs/vue3/_ai/instructions/coding-conventions.instructions.md +160 -0
- package/templates/packs/vue3/_ai/instructions/composables.instructions.md +218 -0
- package/templates/packs/vue3/_ai/instructions/forms.instructions.md +227 -0
- package/templates/packs/vue3/_ai/instructions/store.instructions.md +504 -0
- package/templates/packs/vue3/_ai/instructions/vue.instructions.md +339 -0
- package/templates/packs/vue3/_ai/skills/api-integration/SKILL.md +195 -0
- package/templates/packs/vue3/_ai/skills/new-component/SKILL.md +133 -0
- package/templates/packs/vue3/_ai/skills/new-module/SKILL.md +177 -0
- package/templates/packs/vue3/_guidelines.md +45 -0
- package/templates/packs/vue3/pack.json +11 -0
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
import { join } from 'node:path';
|
|
2
|
+
import { ask, askYN, askChoice, askCheckbox, closeRL } from './utils.mjs';
|
|
3
|
+
import { detectExisting, loadPacks, loadBaseSkills, loadPackSkills } from './installer.mjs';
|
|
4
|
+
import {
|
|
5
|
+
section, tableRow, dim, yellow, green, cyan, bold, boldGreen, boldYellow,
|
|
6
|
+
boldCyan, white, gray, info,
|
|
7
|
+
} from './ui.mjs';
|
|
8
|
+
|
|
9
|
+
export async function askQuestions(packsDir) {
|
|
10
|
+
const answers = {};
|
|
11
|
+
|
|
12
|
+
try {
|
|
13
|
+
console.log(` ${dim('Wpisz')} ${yellow('"q"')} ${dim('aby przerwac w dowolnym momencie.')}`);
|
|
14
|
+
|
|
15
|
+
// --- Project type ---
|
|
16
|
+
section('Projekt');
|
|
17
|
+
|
|
18
|
+
answers.projectType = await askChoice('Typ projektu:', [
|
|
19
|
+
'new',
|
|
20
|
+
'existing',
|
|
21
|
+
], [
|
|
22
|
+
`${boldGreen('Nowy projekt')}`,
|
|
23
|
+
`${boldYellow('Istniejacy projekt')} ${dim('(aktualizacja)')}`,
|
|
24
|
+
]);
|
|
25
|
+
|
|
26
|
+
answers.projectName = await ask('Nazwa projektu', 'my-project');
|
|
27
|
+
answers.projectDescription = await ask('Krotki opis projektu', '');
|
|
28
|
+
answers.techStack = await ask('Tech stack (np. Vue 3 + TS, Laravel 12 + PHP 8.4)', '');
|
|
29
|
+
answers.targetDir = await ask('Katalog docelowy (sciezka)', '.');
|
|
30
|
+
|
|
31
|
+
// --- Install mode for existing projects ---
|
|
32
|
+
if (answers.projectType === 'existing') {
|
|
33
|
+
const existingFiles = await detectExisting(answers.targetDir);
|
|
34
|
+
if (existingFiles.length > 0) {
|
|
35
|
+
console.log(`\n ${yellow('Wykryte pliki konfiguracji:')}`);
|
|
36
|
+
existingFiles.forEach(f => console.log(` ${dim('•')} ${gray(f)}`));
|
|
37
|
+
}
|
|
38
|
+
console.log('');
|
|
39
|
+
|
|
40
|
+
answers.installMode = await askChoice('Co chcesz zrobic?', [
|
|
41
|
+
'overwrite',
|
|
42
|
+
'skip-existing',
|
|
43
|
+
'mcp-only',
|
|
44
|
+
], [
|
|
45
|
+
`${boldYellow('Nadpisz wszystko')} ${dim('(pelna reinstalacja)')}`,
|
|
46
|
+
`${boldGreen('Tylko nowe pliki')} ${dim('(zachowaj istniejace)')}`,
|
|
47
|
+
`${boldCyan('Tylko konfiguracja MCP')}`,
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
if (answers.installMode === 'mcp-only') {
|
|
51
|
+
answers.packs = [];
|
|
52
|
+
await askToolSelection(answers);
|
|
53
|
+
await askMcpServers(answers);
|
|
54
|
+
const proceed = await askYN('\n Kontynuowac?', true);
|
|
55
|
+
if (!proceed) throw new Error('USER_ABORT');
|
|
56
|
+
return answers;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
answers.installMode = 'fresh';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// --- Pack selection (checkboxes!) ---
|
|
63
|
+
const allPacks = await loadPacks(packsDir);
|
|
64
|
+
await askPackSelection(answers, allPacks);
|
|
65
|
+
|
|
66
|
+
// --- AI Tools selection ---
|
|
67
|
+
await askToolSelection(answers);
|
|
68
|
+
|
|
69
|
+
// --- Skill selection ---
|
|
70
|
+
await askSkillSelection(answers, allPacks, join(packsDir, '..', 'base', '_ai', 'skills'));
|
|
71
|
+
|
|
72
|
+
// --- Pack-specific placeholders ---
|
|
73
|
+
await askPackPlaceholders(answers, allPacks);
|
|
74
|
+
|
|
75
|
+
// --- Commands ---
|
|
76
|
+
section('Komendy');
|
|
77
|
+
|
|
78
|
+
const hasVue = answers.packs.includes('vue3');
|
|
79
|
+
const hasLaravel = answers.packs.includes('laravel');
|
|
80
|
+
answers.testCommand = await ask('Komenda testow', hasVue ? 'npm run test' : hasLaravel ? 'php artisan test' : 'npm test');
|
|
81
|
+
answers.buildCommand = await ask('Komenda builda', hasVue ? 'npm run build' : hasLaravel ? 'composer build' : 'npm run build');
|
|
82
|
+
answers.lintCommand = await ask('Komenda lintera', hasVue ? 'npm run lint' : hasLaravel ? 'php artisan pint' : 'npm run lint');
|
|
83
|
+
|
|
84
|
+
// --- CI ---
|
|
85
|
+
section('CI / Quality');
|
|
86
|
+
answers.ciCommand = await ask('Komenda CI (wszystkie checki)', `${answers.lintCommand} && ${answers.testCommand}`);
|
|
87
|
+
|
|
88
|
+
// --- JIRA ---
|
|
89
|
+
section('JIRA');
|
|
90
|
+
answers.jiraPrefix = await ask('Prefix taskow JIRA', 'PROJ');
|
|
91
|
+
|
|
92
|
+
// --- MCP Servers ---
|
|
93
|
+
await askMcpServers(answers);
|
|
94
|
+
|
|
95
|
+
// --- CLI Tools ---
|
|
96
|
+
await askCliInstall(answers);
|
|
97
|
+
|
|
98
|
+
// --- Summary ---
|
|
99
|
+
printSummary(answers, allPacks);
|
|
100
|
+
|
|
101
|
+
const proceed = await askYN('\n Kontynuowac instalacje?', true);
|
|
102
|
+
if (!proceed) throw new Error('USER_ABORT');
|
|
103
|
+
|
|
104
|
+
return answers;
|
|
105
|
+
} finally {
|
|
106
|
+
closeRL();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function askPackSelection(answers, allPacks) {
|
|
111
|
+
section('Pakiety instrukcji i skilli');
|
|
112
|
+
|
|
113
|
+
const options = Object.entries(allPacks).map(([id, pack]) => {
|
|
114
|
+
// Count instructions and skills
|
|
115
|
+
const parts = [];
|
|
116
|
+
const instrCount = pack._instrCount || 0;
|
|
117
|
+
const skillCount = pack._skillCount || 0;
|
|
118
|
+
if (instrCount > 0) parts.push(`${instrCount} instr.`);
|
|
119
|
+
if (skillCount > 0) parts.push(`${skillCount} ${skillCount === 1 ? 'skill' : 'skilli'}`);
|
|
120
|
+
const stats = parts.length > 0 ? dim(` (${parts.join(' · ')})`) : '';
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
value: id,
|
|
124
|
+
label: `${bold(pack.name)}${stats}`,
|
|
125
|
+
checked: pack.default || false,
|
|
126
|
+
};
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
answers.packs = await askCheckbox('Wybierz pakiety:', options);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function askSkillSelection(answers, allPacks, baseSkillsDir) {
|
|
133
|
+
section('Skille');
|
|
134
|
+
|
|
135
|
+
const baseSkills = await loadBaseSkills(baseSkillsDir);
|
|
136
|
+
const packSkills = await loadPackSkills(answers.packs, allPacks);
|
|
137
|
+
|
|
138
|
+
const options = [];
|
|
139
|
+
|
|
140
|
+
if (baseSkills.length > 0) {
|
|
141
|
+
for (const skill of baseSkills) {
|
|
142
|
+
options.push({
|
|
143
|
+
value: skill,
|
|
144
|
+
label: `${bold(skill)} ${dim('(bazowy)')}`,
|
|
145
|
+
checked: true,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (packSkills.length > 0) {
|
|
151
|
+
for (const { skill, packName } of packSkills) {
|
|
152
|
+
options.push({
|
|
153
|
+
value: skill,
|
|
154
|
+
label: `${bold(skill)} ${dim(`(${packName})`)}`,
|
|
155
|
+
checked: true,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (options.length === 0) {
|
|
161
|
+
console.log(` ${dim('Brak dostepnych skilli')}`);
|
|
162
|
+
answers.selectedSkills = [];
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
answers.selectedSkills = await askCheckbox('Wybierz skille:', options);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function askPackPlaceholders(answers, allPacks) {
|
|
170
|
+
// Collect unique placeholders from selected packs
|
|
171
|
+
const seen = new Set();
|
|
172
|
+
const questions = [];
|
|
173
|
+
|
|
174
|
+
for (const packId of answers.packs) {
|
|
175
|
+
const pack = allPacks[packId];
|
|
176
|
+
if (!pack || !pack.placeholders) continue;
|
|
177
|
+
|
|
178
|
+
for (const [key, config] of Object.entries(pack.placeholders)) {
|
|
179
|
+
if (seen.has(key)) continue;
|
|
180
|
+
seen.add(key);
|
|
181
|
+
questions.push({ key, ...config });
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (questions.length === 0) return;
|
|
186
|
+
|
|
187
|
+
section('Sciezki i konfiguracja');
|
|
188
|
+
|
|
189
|
+
for (const q of questions) {
|
|
190
|
+
answers[placeholderToAnswerKey(q.key)] = await ask(q.question, q.default);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function placeholderToAnswerKey(placeholder) {
|
|
195
|
+
// MODULE_PATH → modulePath
|
|
196
|
+
return placeholder.toLowerCase().replace(/_([a-z])/g, (_, c) => c.toUpperCase());
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
async function askToolSelection(answers) {
|
|
200
|
+
section('Narzedzia AI');
|
|
201
|
+
|
|
202
|
+
const selected = await askCheckbox('Wybierz narzedzia:', [
|
|
203
|
+
{ value: 'toolClaude', label: bold('Claude Code'), checked: true },
|
|
204
|
+
{ value: 'toolCopilot', label: bold('GitHub Copilot'), checked: true },
|
|
205
|
+
{ value: 'toolCursor', label: bold('Cursor IDE'), checked: false },
|
|
206
|
+
{ value: 'toolGemini', label: bold('Gemini'), checked: false },
|
|
207
|
+
{ value: 'toolJunie', label: bold('JetBrains Junie'), checked: false },
|
|
208
|
+
]);
|
|
209
|
+
|
|
210
|
+
answers.toolClaude = selected.includes('toolClaude');
|
|
211
|
+
answers.toolCopilot = selected.includes('toolCopilot');
|
|
212
|
+
answers.toolCursor = selected.includes('toolCursor');
|
|
213
|
+
answers.toolGemini = selected.includes('toolGemini');
|
|
214
|
+
answers.toolJunie = selected.includes('toolJunie');
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function askMcpServers(answers) {
|
|
218
|
+
const hasMcpTool = answers.toolClaude || answers.toolCursor;
|
|
219
|
+
if (!hasMcpTool) {
|
|
220
|
+
answers.mcpJira = false;
|
|
221
|
+
answers.mcpBitbucket = false;
|
|
222
|
+
answers.mcpFigma = false;
|
|
223
|
+
answers.mcpContext7 = false;
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
section('Serwery MCP');
|
|
228
|
+
|
|
229
|
+
const mcpTargets = [];
|
|
230
|
+
if (answers.toolClaude) mcpTargets.push(cyan('Claude Code'));
|
|
231
|
+
if (answers.toolCursor) mcpTargets.push(cyan('Cursor'));
|
|
232
|
+
console.log(info(`MCP dla: ${mcpTargets.join(dim(' · '))}`));
|
|
233
|
+
console.log('');
|
|
234
|
+
|
|
235
|
+
const selected = await askCheckbox('Wybierz serwery MCP:', [
|
|
236
|
+
{ value: 'mcpJira', label: `${bold('Jira Cloud')} ${dim('(wymaga tokena)')}`, checked: true },
|
|
237
|
+
{ value: 'mcpBitbucket', label: `${bold('Bitbucket Cloud')} ${dim('(wymaga tokena)')}`, checked: false },
|
|
238
|
+
{ value: 'mcpFigma', label: `${bold('Figma')} ${dim('(wymaga tokena)')}`, checked: false },
|
|
239
|
+
{ value: 'mcpContext7', label: `${bold('Context7')} ${dim('(bez tokenow)')}`, checked: true },
|
|
240
|
+
]);
|
|
241
|
+
|
|
242
|
+
answers.mcpJira = selected.includes('mcpJira');
|
|
243
|
+
answers.mcpBitbucket = selected.includes('mcpBitbucket');
|
|
244
|
+
answers.mcpFigma = selected.includes('mcpFigma');
|
|
245
|
+
answers.mcpContext7 = selected.includes('mcpContext7');
|
|
246
|
+
|
|
247
|
+
// Ask for credentials of selected servers
|
|
248
|
+
if (answers.mcpJira) {
|
|
249
|
+
console.log('');
|
|
250
|
+
console.log(info(`Konfiguracja ${bold('Jira Cloud')}:`));
|
|
251
|
+
answers.jiraUrl = await ask(' Jira URL', 'https://team.atlassian.net');
|
|
252
|
+
answers.jiraEmail = await ask(' Jira email');
|
|
253
|
+
answers.jiraToken = await ask(' Jira API token');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (answers.mcpBitbucket) {
|
|
257
|
+
console.log('');
|
|
258
|
+
console.log(info(`Konfiguracja ${bold('Bitbucket Cloud')}:`));
|
|
259
|
+
answers.bitbucketEmail = await ask(' Bitbucket email');
|
|
260
|
+
answers.bitbucketToken = await ask(' Bitbucket app password');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
if (answers.mcpFigma) {
|
|
264
|
+
console.log('');
|
|
265
|
+
console.log(info(`Konfiguracja ${bold('Figma')}:`));
|
|
266
|
+
answers.figmaToken = await ask(' Figma access token');
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function askCliInstall(answers) {
|
|
271
|
+
section('Instalacja CLI');
|
|
272
|
+
|
|
273
|
+
const options = [];
|
|
274
|
+
if (answers.toolClaude) {
|
|
275
|
+
options.push({ value: 'installClaudeCli', label: bold('Claude Code CLI'), checked: false });
|
|
276
|
+
}
|
|
277
|
+
options.push({ value: 'installGsd', label: `${bold('GSD')} ${dim('(Get Shit Done)')}`, checked: true });
|
|
278
|
+
|
|
279
|
+
const selected = await askCheckbox('Zainstalowac/zaktualizowac:', options);
|
|
280
|
+
|
|
281
|
+
answers.installClaudeCli = selected.includes('installClaudeCli');
|
|
282
|
+
answers.installGsd = selected.includes('installGsd');
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function printSummary(answers, allPacks) {
|
|
286
|
+
section('Podsumowanie');
|
|
287
|
+
|
|
288
|
+
tableRow('Projekt', bold(answers.projectName));
|
|
289
|
+
if (answers.projectDescription) tableRow('Opis', answers.projectDescription);
|
|
290
|
+
if (answers.techStack) tableRow('Tech stack', cyan(answers.techStack));
|
|
291
|
+
tableRow('Katalog', answers.targetDir);
|
|
292
|
+
tableRow('Tryb', answers.installMode === 'fresh'
|
|
293
|
+
? boldGreen('nowy')
|
|
294
|
+
: boldYellow(answers.installMode));
|
|
295
|
+
|
|
296
|
+
const packNames = answers.packs.map(id => allPacks[id]?.name || id);
|
|
297
|
+
tableRow('Pakiety', packNames.length > 0 ? cyan(packNames.join(dim(' · '))) : dim('brak'));
|
|
298
|
+
|
|
299
|
+
const tools = [];
|
|
300
|
+
if (answers.toolClaude) tools.push('Claude');
|
|
301
|
+
if (answers.toolCopilot) tools.push('Copilot');
|
|
302
|
+
if (answers.toolCursor) tools.push('Cursor');
|
|
303
|
+
if (answers.toolGemini) tools.push('Gemini');
|
|
304
|
+
if (answers.toolJunie) tools.push('Junie');
|
|
305
|
+
tableRow('Narzedzia', tools.length > 0 ? cyan(tools.join(dim(' · '))) : dim('brak'));
|
|
306
|
+
|
|
307
|
+
const skills = answers.selectedSkills || [];
|
|
308
|
+
tableRow('Skille', skills.length > 0 ? cyan(skills.join(dim(' · '))) : dim('brak'));
|
|
309
|
+
|
|
310
|
+
tableRow('JIRA prefix', answers.jiraPrefix);
|
|
311
|
+
|
|
312
|
+
const mcps = [];
|
|
313
|
+
if (answers.mcpJira) mcps.push('Jira');
|
|
314
|
+
if (answers.mcpBitbucket) mcps.push('Bitbucket');
|
|
315
|
+
if (answers.mcpFigma) mcps.push('Figma');
|
|
316
|
+
if (answers.mcpContext7) mcps.push('Context7');
|
|
317
|
+
tableRow('MCP serwery', mcps.length > 0 ? cyan(mcps.join(dim(' · '))) : dim('brak'));
|
|
318
|
+
|
|
319
|
+
const cliInstalls = [];
|
|
320
|
+
if (answers.installClaudeCli) cliInstalls.push('Claude CLI');
|
|
321
|
+
if (answers.installGsd) cliInstalls.push('GSD');
|
|
322
|
+
if (cliInstalls.length > 0) {
|
|
323
|
+
tableRow('Instalacja', green(cliInstalls.join(dim(' · '))));
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
if (answers.toolClaude) {
|
|
327
|
+
const gsdSteps = [];
|
|
328
|
+
if (answers.projectType === 'existing') gsdSteps.push('/gsd:map-codebase');
|
|
329
|
+
gsdSteps.push('/gsd:new-project');
|
|
330
|
+
tableRow('GSD bootstrap', cyan(gsdSteps.join(dim(' → '))));
|
|
331
|
+
}
|
|
332
|
+
}
|
package/lib/ui.mjs
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// ANSI color helpers — zero dependencies
|
|
2
|
+
const esc = (code) => `\x1b[${code}m`;
|
|
3
|
+
const reset = esc(0);
|
|
4
|
+
|
|
5
|
+
// Colors
|
|
6
|
+
export const dim = (s) => `${esc(2)}${s}${reset}`;
|
|
7
|
+
export const bold = (s) => `${esc(1)}${s}${reset}`;
|
|
8
|
+
export const italic = (s) => `${esc(3)}${s}${reset}`;
|
|
9
|
+
export const cyan = (s) => `${esc(36)}${s}${reset}`;
|
|
10
|
+
export const green = (s) => `${esc(32)}${s}${reset}`;
|
|
11
|
+
export const yellow = (s) => `${esc(33)}${s}${reset}`;
|
|
12
|
+
export const red = (s) => `${esc(31)}${s}${reset}`;
|
|
13
|
+
export const magenta = (s) => `${esc(35)}${s}${reset}`;
|
|
14
|
+
export const blue = (s) => `${esc(34)}${s}${reset}`;
|
|
15
|
+
export const white = (s) => `${esc(97)}${s}${reset}`;
|
|
16
|
+
export const gray = (s) => `${esc(90)}${s}${reset}`;
|
|
17
|
+
|
|
18
|
+
// Bold + color combos
|
|
19
|
+
export const boldCyan = (s) => `${esc(1)}${esc(36)}${s}${reset}`;
|
|
20
|
+
export const boldGreen = (s) => `${esc(1)}${esc(32)}${s}${reset}`;
|
|
21
|
+
export const boldYellow = (s) => `${esc(1)}${esc(33)}${s}${reset}`;
|
|
22
|
+
export const boldRed = (s) => `${esc(1)}${esc(31)}${s}${reset}`;
|
|
23
|
+
export const boldMagenta = (s) => `${esc(1)}${esc(35)}${s}${reset}`;
|
|
24
|
+
export const boldWhite = (s) => `${esc(1)}${esc(97)}${s}${reset}`;
|
|
25
|
+
|
|
26
|
+
// Background
|
|
27
|
+
export const bgCyan = (s) => `${esc(46)}${esc(30)}${s}${reset}`;
|
|
28
|
+
export const bgGreen = (s) => `${esc(42)}${esc(30)}${s}${reset}`;
|
|
29
|
+
export const bgYellow = (s) => `${esc(43)}${esc(30)}${s}${reset}`;
|
|
30
|
+
export const bgMagenta = (s) => `${esc(45)}${esc(97)}${s}${reset}`;
|
|
31
|
+
|
|
32
|
+
// Semantic
|
|
33
|
+
export const success = (s) => green(` ✓ ${s}`);
|
|
34
|
+
export const error = (s) => red(` ✗ ${s}`);
|
|
35
|
+
export const warn = (s) => yellow(` ⚠ ${s}`);
|
|
36
|
+
export const info = (s) => cyan(` ℹ ${s}`);
|
|
37
|
+
export const file = (s) => ` ${green('+')} ${gray(s)}`;
|
|
38
|
+
export const fileSkip = (s) => ` ${yellow('~')} ${gray(s)} ${dim('(pominieto)')}`;
|
|
39
|
+
export const fileMirror = (s) => ` ${blue('»')} ${gray(s)} ${dim('(mirror)')}`;
|
|
40
|
+
|
|
41
|
+
// Section header
|
|
42
|
+
export function section(title) {
|
|
43
|
+
console.log('');
|
|
44
|
+
console.log(` ${boldCyan('─── ' + title + ' ───')}`);
|
|
45
|
+
console.log('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Box with border
|
|
49
|
+
export function box(lines, color = boldCyan) {
|
|
50
|
+
const maxLen = Math.max(...lines.map(l => stripAnsi(l).length));
|
|
51
|
+
const pad = (s) => s + ' '.repeat(maxLen - stripAnsi(s).length);
|
|
52
|
+
const border = color('┌─' + '─'.repeat(maxLen + 2) + '─┐');
|
|
53
|
+
const bottom = color('└─' + '─'.repeat(maxLen + 2) + '─┘');
|
|
54
|
+
console.log(border);
|
|
55
|
+
for (const line of lines) {
|
|
56
|
+
console.log(color('│') + ' ' + pad(line) + ' ' + color('│'));
|
|
57
|
+
}
|
|
58
|
+
console.log(bottom);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Spinner-like step indicator
|
|
62
|
+
export function step(n, total, label) {
|
|
63
|
+
const progress = gray(`[${n}/${total}]`);
|
|
64
|
+
console.log(` ${progress} ${label}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Table row
|
|
68
|
+
export function tableRow(label, value, labelWidth = 16) {
|
|
69
|
+
const paddedLabel = (label + ':').padEnd(labelWidth);
|
|
70
|
+
console.log(` ${dim(paddedLabel)} ${white(value)}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Strip ANSI codes for length calculation
|
|
74
|
+
function stripAnsi(s) {
|
|
75
|
+
return s.replace(/\x1b\[[0-9;]*m/g, '');
|
|
76
|
+
}
|
package/lib/utils.mjs
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import * as readline from 'node:readline/promises';
|
|
2
|
+
import { stdin as input, stdout as output } from 'node:process';
|
|
3
|
+
import { bold, cyan, dim, green, yellow, boldCyan, boldWhite, gray } from './ui.mjs';
|
|
4
|
+
|
|
5
|
+
let rl = null;
|
|
6
|
+
|
|
7
|
+
export function getRL() {
|
|
8
|
+
if (!rl) {
|
|
9
|
+
rl = readline.createInterface({ input, output });
|
|
10
|
+
}
|
|
11
|
+
return rl;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function closeRL() {
|
|
15
|
+
if (rl) {
|
|
16
|
+
rl.close();
|
|
17
|
+
rl = null;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function ask(question, defaultValue) {
|
|
22
|
+
const r = getRL();
|
|
23
|
+
const suffix = defaultValue ? ` ${dim(`[${defaultValue}]`)}` : '';
|
|
24
|
+
const answer = await r.question(` ${cyan('?')} ${question}${suffix}${dim(':')} `);
|
|
25
|
+
if (answer.toLowerCase() === 'q') throw new Error('USER_ABORT');
|
|
26
|
+
return answer || defaultValue || '';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export async function askYN(question, defaultYes = true) {
|
|
30
|
+
const r = getRL();
|
|
31
|
+
const hint = defaultYes
|
|
32
|
+
? `${green('T')}${dim('/')}${dim('n')}`
|
|
33
|
+
: `${dim('t')}${dim('/')}${green('N')}`;
|
|
34
|
+
const answer = await r.question(` ${cyan('?')} ${question} ${dim('[')}${hint}${dim(']')}${dim(':')} `);
|
|
35
|
+
if (answer.toLowerCase() === 'q') throw new Error('USER_ABORT');
|
|
36
|
+
if (!answer) return defaultYes;
|
|
37
|
+
return answer.toLowerCase().startsWith('t') || answer.toLowerCase().startsWith('y');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Interactive radio selector using raw stdin (single choice).
|
|
42
|
+
* @param {string} question - Header text
|
|
43
|
+
* @param {string[]} choices - Values
|
|
44
|
+
* @param {string[]} labels - Display labels
|
|
45
|
+
* @param {number} defaultIdx - Default selected index
|
|
46
|
+
* @returns {Promise<string>} Selected value
|
|
47
|
+
*/
|
|
48
|
+
export async function askChoice(question, choices, labels, defaultIdx = 0) {
|
|
49
|
+
closeRL();
|
|
50
|
+
|
|
51
|
+
const { stdin, stdout } = process;
|
|
52
|
+
let cursor = defaultIdx;
|
|
53
|
+
let linesDrawn = 0;
|
|
54
|
+
|
|
55
|
+
function render() {
|
|
56
|
+
if (linesDrawn > 0) {
|
|
57
|
+
stdout.write(`\x1b[${linesDrawn}A\x1b[0J`);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const lines = [];
|
|
61
|
+
lines.push(` ${cyan('?')} ${bold(question)}`);
|
|
62
|
+
lines.push('');
|
|
63
|
+
|
|
64
|
+
for (let i = 0; i < choices.length; i++) {
|
|
65
|
+
const radio = i === cursor ? green(' ● ') : dim(' ○ ');
|
|
66
|
+
const pointer = i === cursor ? cyan('❯') : ' ';
|
|
67
|
+
const label = labels ? labels[i] : choices[i];
|
|
68
|
+
const text = i === cursor ? boldWhite(label) : label;
|
|
69
|
+
lines.push(` ${pointer}${radio}${text}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
lines.push('');
|
|
73
|
+
lines.push(` ${dim('↑↓ nawigacja · enter = zatwierdz')}`);
|
|
74
|
+
|
|
75
|
+
stdout.write(lines.join('\n') + '\n');
|
|
76
|
+
linesDrawn = lines.length;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return new Promise((resolve, reject) => {
|
|
80
|
+
if (!stdin.isTTY) {
|
|
81
|
+
resolve(choices[defaultIdx]);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
stdin.setRawMode(true);
|
|
86
|
+
stdin.resume();
|
|
87
|
+
stdin.setEncoding('utf8');
|
|
88
|
+
|
|
89
|
+
let buf = '';
|
|
90
|
+
|
|
91
|
+
function onData(data) {
|
|
92
|
+
buf += data;
|
|
93
|
+
|
|
94
|
+
while (buf.length > 0) {
|
|
95
|
+
if (buf.startsWith('\x1b[A')) {
|
|
96
|
+
cursor = Math.max(0, cursor - 1);
|
|
97
|
+
buf = buf.slice(3);
|
|
98
|
+
} else if (buf.startsWith('\x1b[B')) {
|
|
99
|
+
cursor = Math.min(choices.length - 1, cursor + 1);
|
|
100
|
+
buf = buf.slice(3);
|
|
101
|
+
} else if (buf.startsWith('\x1b')) {
|
|
102
|
+
if (buf.length < 3) return;
|
|
103
|
+
buf = buf.slice(3);
|
|
104
|
+
} else if (buf[0] === '\r' || buf[0] === '\n') {
|
|
105
|
+
cleanup();
|
|
106
|
+
resolve(choices[cursor]);
|
|
107
|
+
return;
|
|
108
|
+
} else if (buf[0] === 'q' || buf[0] === '\x03') {
|
|
109
|
+
cleanup();
|
|
110
|
+
reject(new Error('USER_ABORT'));
|
|
111
|
+
return;
|
|
112
|
+
} else {
|
|
113
|
+
buf = buf.slice(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
render();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function cleanup() {
|
|
121
|
+
stdin.removeListener('data', onData);
|
|
122
|
+
stdin.setRawMode(false);
|
|
123
|
+
stdin.pause();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
stdin.on('data', onData);
|
|
127
|
+
render();
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Interactive checkbox selector using raw stdin.
|
|
133
|
+
* @param {string} question - Header text
|
|
134
|
+
* @param {{value: string, label: string, checked: boolean}[]} options
|
|
135
|
+
* @returns {Promise<string[]>} Selected values
|
|
136
|
+
*/
|
|
137
|
+
export async function askCheckbox(question, options) {
|
|
138
|
+
// Close readline — raw mode conflicts with it
|
|
139
|
+
closeRL();
|
|
140
|
+
|
|
141
|
+
const { stdin, stdout } = process;
|
|
142
|
+
const items = options.map(o => ({ ...o }));
|
|
143
|
+
let cursor = 0;
|
|
144
|
+
let linesDrawn = 0;
|
|
145
|
+
|
|
146
|
+
function render() {
|
|
147
|
+
// Clear previous render
|
|
148
|
+
if (linesDrawn > 0) {
|
|
149
|
+
stdout.write(`\x1b[${linesDrawn}A\x1b[0J`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const lines = [];
|
|
153
|
+
lines.push(` ${cyan('?')} ${bold(question)}`);
|
|
154
|
+
lines.push('');
|
|
155
|
+
|
|
156
|
+
for (let i = 0; i < items.length; i++) {
|
|
157
|
+
const box = items[i].checked ? green(' ■ ') : dim(' □ ');
|
|
158
|
+
const pointer = i === cursor ? cyan('❯') : ' ';
|
|
159
|
+
const label = i === cursor ? boldWhite(items[i].label) : items[i].label;
|
|
160
|
+
lines.push(` ${pointer}${box}${label}`);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
lines.push('');
|
|
164
|
+
lines.push(` ${dim('↑↓ nawigacja · spacja = przelacz · enter = zatwierdz')}`);
|
|
165
|
+
|
|
166
|
+
stdout.write(lines.join('\n') + '\n');
|
|
167
|
+
linesDrawn = lines.length;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return new Promise((resolve, reject) => {
|
|
171
|
+
if (!stdin.isTTY) {
|
|
172
|
+
// Fallback for non-TTY (piped input) — return defaults
|
|
173
|
+
resolve(items.filter(i => i.checked).map(i => i.value));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
stdin.setRawMode(true);
|
|
178
|
+
stdin.resume();
|
|
179
|
+
stdin.setEncoding('utf8');
|
|
180
|
+
|
|
181
|
+
let buf = '';
|
|
182
|
+
|
|
183
|
+
function onData(data) {
|
|
184
|
+
buf += data;
|
|
185
|
+
|
|
186
|
+
// Parse escape sequences
|
|
187
|
+
while (buf.length > 0) {
|
|
188
|
+
if (buf.startsWith('\x1b[A')) { // Arrow up
|
|
189
|
+
cursor = Math.max(0, cursor - 1);
|
|
190
|
+
buf = buf.slice(3);
|
|
191
|
+
} else if (buf.startsWith('\x1b[B')) { // Arrow down
|
|
192
|
+
cursor = Math.min(items.length - 1, cursor + 1);
|
|
193
|
+
buf = buf.slice(3);
|
|
194
|
+
} else if (buf.startsWith('\x1b')) {
|
|
195
|
+
// Incomplete escape sequence — wait for more data
|
|
196
|
+
if (buf.length < 3) return;
|
|
197
|
+
// Unknown escape — skip
|
|
198
|
+
buf = buf.slice(3);
|
|
199
|
+
} else if (buf[0] === ' ') { // Space — toggle
|
|
200
|
+
items[cursor].checked = !items[cursor].checked;
|
|
201
|
+
buf = buf.slice(1);
|
|
202
|
+
} else if (buf[0] === '\r' || buf[0] === '\n') { // Enter — confirm
|
|
203
|
+
cleanup();
|
|
204
|
+
resolve(items.filter(i => i.checked).map(i => i.value));
|
|
205
|
+
return;
|
|
206
|
+
} else if (buf[0] === 'a') { // 'a' — toggle all
|
|
207
|
+
const allChecked = items.every(i => i.checked);
|
|
208
|
+
items.forEach(i => { i.checked = !allChecked; });
|
|
209
|
+
buf = buf.slice(1);
|
|
210
|
+
} else if (buf[0] === 'q' || buf[0] === '\x03') { // q or ctrl-c
|
|
211
|
+
cleanup();
|
|
212
|
+
reject(new Error('USER_ABORT'));
|
|
213
|
+
return;
|
|
214
|
+
} else {
|
|
215
|
+
buf = buf.slice(1);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
render();
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function cleanup() {
|
|
223
|
+
stdin.removeListener('data', onData);
|
|
224
|
+
stdin.setRawMode(false);
|
|
225
|
+
stdin.pause();
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
stdin.on('data', onData);
|
|
229
|
+
render();
|
|
230
|
+
});
|
|
231
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "symfonia-ai-tools",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI tooling setup for your project - Claude Code, GitHub Copilot, Cursor, Gemini, Junie, GSD",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"symfonia-ai-tools": "./bin/cli.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin/",
|
|
11
|
+
"lib/",
|
|
12
|
+
"templates/"
|
|
13
|
+
],
|
|
14
|
+
"keywords": [
|
|
15
|
+
"ai",
|
|
16
|
+
"claude",
|
|
17
|
+
"copilot",
|
|
18
|
+
"cursor",
|
|
19
|
+
"gemini",
|
|
20
|
+
"gsd",
|
|
21
|
+
"developer-tools",
|
|
22
|
+
"scaffolding"
|
|
23
|
+
],
|
|
24
|
+
"author": "",
|
|
25
|
+
"license": "MIT"
|
|
26
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# {{PROJECT_NAME}}
|
|
2
|
+
|
|
3
|
+
**{{PROJECT_DESCRIPTION}}**
|
|
4
|
+
|
|
5
|
+
**Tech Stack**: {{TECH_STACK}}
|
|
6
|
+
|
|
7
|
+
## Guidelines
|
|
8
|
+
Read and follow `.ai/guidelines.md` as mandatory context for all work.
|
|
9
|
+
|
|
10
|
+
## Commands
|
|
11
|
+
- Build: `{{BUILD_COMMAND}}`
|
|
12
|
+
- Test: `{{TEST_COMMAND}}`
|
|
13
|
+
- Lint: `{{LINT_COMMAND}}`
|
|
14
|
+
- CI (all checks): `{{CI_COMMAND}}`
|
|
15
|
+
|
|
16
|
+
## Prompts & Skills
|
|
17
|
+
- Custom prompts: `.ai/prompts/`
|
|
18
|
+
- Skills: `.ai/skills/` — check for a matching workflow before starting any task
|
|
19
|
+
|
|
20
|
+
## Workflow
|
|
21
|
+
This project uses GSD (Get Shit Done) for structured development.
|
|
22
|
+
- `/gsd:progress` - Check project status
|
|
23
|
+
- `/gsd:plan-phase` - Plan next phase
|
|
24
|
+
- `/gsd:execute-phase` - Execute current phase
|
|
25
|
+
- `/gsd:verify-work` - Verify completed work
|
|
26
|
+
- `/gsd:debug` - Systematic debugging
|
|
27
|
+
|
|
28
|
+
## Commits
|
|
29
|
+
Extract issue number from branch name and use as prefix.
|
|
30
|
+
Example: `feature/{{JIRA_PREFIX}}-1234-name` → `{{JIRA_PREFIX}}-1234: description`
|
|
31
|
+
|
|
32
|
+
Follow conventions from `commit-instructions.md`.
|
|
33
|
+
Never add `Co-Authored-By` or AI attribution lines.
|
|
34
|
+
Do not commit without developer approval.
|