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.
Files changed (74) hide show
  1. package/README.md +489 -0
  2. package/bin/cli.mjs +35 -0
  3. package/lib/installer.mjs +495 -0
  4. package/lib/questions.mjs +332 -0
  5. package/lib/ui.mjs +76 -0
  6. package/lib/utils.mjs +231 -0
  7. package/package.json +26 -0
  8. package/templates/base/CLAUDE.md +34 -0
  9. package/templates/base/_ai/_guidelines_header.md +70 -0
  10. package/templates/base/_ai/context/README.md +20 -0
  11. package/templates/base/_ai/prompts/codereview.prompt.md +324 -0
  12. package/templates/base/_ai/prompts/duplicate-code-analysis.prompt.md +128 -0
  13. package/templates/base/_ai/prompts/figma-analysis.prompt.md +155 -0
  14. package/templates/base/_ai/prompts/security-review.prompt.md +46 -0
  15. package/templates/base/_ai/skills/README.md +80 -0
  16. package/templates/base/_ai/skills/TEMPLATE.md +106 -0
  17. package/templates/base/_ai/skills/babysit-prs/SKILL.md +105 -0
  18. package/templates/base/_ai/skills/debug/SKILL.md +93 -0
  19. package/templates/base/_ai/skills/fill-worklogs/SKILL.md +158 -0
  20. package/templates/base/_ai/skills/hotfix/SKILL.md +52 -0
  21. package/templates/base/_ai/skills/jira-task/SKILL.md +170 -0
  22. package/templates/base/_ai/skills/my-prs/SKILL.md +78 -0
  23. package/templates/base/_ai/skills/pr-dashboard/SKILL.md +43 -0
  24. package/templates/base/_ai/skills/pr-prepare/SKILL.md +106 -0
  25. package/templates/base/_ai/skills/refactor/SKILL.md +87 -0
  26. package/templates/base/_ai/skills/write-tests/SKILL.md +109 -0
  27. package/templates/base/_claude/settings.local.json +37 -0
  28. package/templates/base/_cursor/rules/global.mdc +7 -0
  29. package/templates/base/_editorconfig +18 -0
  30. package/templates/base/_gemini/settings.json +3 -0
  31. package/templates/base/_github/copilot-instructions.md +1 -0
  32. package/templates/base/_github/pull_request_template.md +23 -0
  33. package/templates/base/_gitignore +22 -0
  34. package/templates/base/_junie/guidelines.md +1 -0
  35. package/templates/base/commit-instructions.md +92 -0
  36. package/templates/packs/docker/_ai/instructions/docker.instructions.md +193 -0
  37. package/templates/packs/docker/_guidelines.md +10 -0
  38. package/templates/packs/docker/pack.json +8 -0
  39. package/templates/packs/laravel/_ai/instructions/api-resource.instructions.md +251 -0
  40. package/templates/packs/laravel/_ai/instructions/module.instructions.md +133 -0
  41. package/templates/packs/laravel/_ai/instructions/service-repository.instructions.md +215 -0
  42. package/templates/packs/laravel/_ai/instructions/testing.instructions.md +278 -0
  43. package/templates/packs/laravel/_ai/skills/migration/SKILL.md +172 -0
  44. package/templates/packs/laravel/_ai/skills/new-endpoint/SKILL.md +165 -0
  45. package/templates/packs/laravel/_ai/skills/new-module/SKILL.md +208 -0
  46. package/templates/packs/laravel/_ai/skills/queued-job/SKILL.md +248 -0
  47. package/templates/packs/laravel/_ai/skills/testing-feature/SKILL.md +196 -0
  48. package/templates/packs/laravel/_ai/skills/testing-manual/SKILL.md +186 -0
  49. package/templates/packs/laravel/_ai/skills/testing-unit/SKILL.md +200 -0
  50. package/templates/packs/laravel/_guidelines.md +25 -0
  51. package/templates/packs/laravel/pack.json +6 -0
  52. package/templates/packs/playwright/_ai/instructions/playwright.instructions.md +219 -0
  53. package/templates/packs/playwright/_ai/skills/playwright/README.md +194 -0
  54. package/templates/packs/playwright/_ai/skills/playwright/SKILL.md +1245 -0
  55. package/templates/packs/playwright/_ai/skills/playwright-codereview/SKILL.md +642 -0
  56. package/templates/packs/playwright/_ai/skills/playwright-record/README.md +87 -0
  57. package/templates/packs/playwright/_ai/skills/playwright-record/SKILL.md +564 -0
  58. package/templates/packs/playwright/_guidelines.md +12 -0
  59. package/templates/packs/playwright/pack.json +9 -0
  60. package/templates/packs/storybook/_ai/instructions/storybook.instructions.md +181 -0
  61. package/templates/packs/storybook/pack.json +6 -0
  62. package/templates/packs/vitest/_ai/instructions/vitest.instructions.md +688 -0
  63. package/templates/packs/vitest/pack.json +6 -0
  64. package/templates/packs/vue3/_ai/instructions/api.instructions.md +163 -0
  65. package/templates/packs/vue3/_ai/instructions/coding-conventions.instructions.md +160 -0
  66. package/templates/packs/vue3/_ai/instructions/composables.instructions.md +218 -0
  67. package/templates/packs/vue3/_ai/instructions/forms.instructions.md +227 -0
  68. package/templates/packs/vue3/_ai/instructions/store.instructions.md +504 -0
  69. package/templates/packs/vue3/_ai/instructions/vue.instructions.md +339 -0
  70. package/templates/packs/vue3/_ai/skills/api-integration/SKILL.md +195 -0
  71. package/templates/packs/vue3/_ai/skills/new-component/SKILL.md +133 -0
  72. package/templates/packs/vue3/_ai/skills/new-module/SKILL.md +177 -0
  73. package/templates/packs/vue3/_guidelines.md +45 -0
  74. 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.