start-vibing-stacks 2.2.0 → 2.4.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.
@@ -0,0 +1,501 @@
1
+ /**
2
+ * Start Vibing Stacks — Project Standards Scanner
3
+ *
4
+ * Scans existing project files to extract coding patterns
5
+ * and standards already in use.
6
+ */
7
+ import { existsSync, readFileSync } from 'fs';
8
+ import { join } from 'path';
9
+ // ─── PHP: Composer dependency map ──────────────────────────────────────────
10
+ const COMPOSER_PACKAGES = {
11
+ 'laravel/octane': { category: 'server', name: 'Laravel Octane (long-lived workers)' },
12
+ 'laravel/sanctum': { category: 'auth', name: 'Laravel Sanctum for API token auth' },
13
+ 'inertiajs/inertia-laravel': { category: 'frontend', name: 'Inertia.js (server-driven SPA)' },
14
+ 'laravel/horizon': { category: 'queue', name: 'Laravel Horizon for queue monitoring' },
15
+ 'laravel/telescope': { category: 'debug', name: 'Laravel Telescope for debugging' },
16
+ 'laravel/cashier': { category: 'billing', name: 'Laravel Cashier for payments' },
17
+ 'laravel/scout': { category: 'search', name: 'Laravel Scout for full-text search' },
18
+ 'laravel/socialite': { category: 'auth', name: 'Laravel Socialite for OAuth' },
19
+ 'spatie/laravel-permission': { category: 'auth', name: 'Spatie Permissions for RBAC' },
20
+ 'spatie/laravel-medialibrary': { category: 'media', name: 'Spatie Media Library for file uploads' },
21
+ 'spatie/laravel-activitylog': { category: 'audit', name: 'Spatie Activity Log for auditing' },
22
+ 'tightenco/ziggy': { category: 'frontend', name: 'Ziggy for Laravel routes in JS' },
23
+ 'predis/predis': { category: 'cache', name: 'Predis Redis client' },
24
+ 'phpstan/phpstan': { category: 'quality', name: 'PHPStan static analysis' },
25
+ 'friendsofphp/php-cs-fixer': { category: 'quality', name: 'PHP-CS-Fixer code style' },
26
+ 'pestphp/pest': { category: 'testing', name: 'Pest PHP testing framework' },
27
+ 'nunomaduro/larastan': { category: 'quality', name: 'Larastan (PHPStan for Laravel)' },
28
+ };
29
+ // ─── Node.js: npm/bun dependency map ───────────────────────────────────────
30
+ const NPM_PACKAGES = {
31
+ // Frameworks
32
+ 'next': { category: 'framework', name: 'Next.js framework' },
33
+ 'nuxt': { category: 'framework', name: 'Nuxt framework' },
34
+ 'astro': { category: 'framework', name: 'Astro framework' },
35
+ 'express': { category: 'framework', name: 'Express.js server' },
36
+ 'fastify': { category: 'framework', name: 'Fastify server' },
37
+ 'hono': { category: 'framework', name: 'Hono lightweight framework' },
38
+ // Frontend
39
+ 'react': { category: 'frontend', name: 'React library' },
40
+ 'vue': { category: 'frontend', name: 'Vue.js framework' },
41
+ 'svelte': { category: 'frontend', name: 'Svelte framework' },
42
+ '@sveltejs/kit': { category: 'frontend', name: 'SvelteKit framework' },
43
+ 'tailwindcss': { category: 'frontend', name: 'TailwindCSS styling' },
44
+ '@tanstack/react-query': { category: 'data', name: 'TanStack Query for data fetching' },
45
+ 'react-hook-form': { category: 'forms', name: 'React Hook Form' },
46
+ // UI
47
+ 'sonner': { category: 'ui', name: 'Sonner toast notifications' },
48
+ 'framer-motion': { category: 'ui', name: 'Framer Motion animations' },
49
+ 'shadcn-ui': { category: 'ui', name: 'shadcn/ui components' },
50
+ '@radix-ui/react-dialog': { category: 'ui', name: 'Radix UI primitives' },
51
+ // API
52
+ '@trpc/server': { category: 'api', name: 'tRPC type-safe API' },
53
+ // Validation
54
+ 'zod': { category: 'validation', name: 'Zod schema validation' },
55
+ // Database — MongoDB
56
+ 'mongoose': { category: 'database', name: 'Mongoose (MongoDB)' },
57
+ // Database — PostgreSQL
58
+ 'pg': { category: 'database', name: 'pg (PostgreSQL client)' },
59
+ '@neondatabase/serverless': { category: 'database', name: 'Neon serverless PostgreSQL' },
60
+ '@vercel/postgres': { category: 'database', name: 'Vercel Postgres' },
61
+ // Database — MySQL
62
+ 'mysql2': { category: 'database', name: 'mysql2 (MySQL client)' },
63
+ '@planetscale/database': { category: 'database', name: 'PlanetScale MySQL' },
64
+ // Database — SQLite / Turso
65
+ 'better-sqlite3': { category: 'database', name: 'better-sqlite3 (SQLite)' },
66
+ '@libsql/client': { category: 'database', name: 'libSQL / Turso client' },
67
+ // Database — ORMs
68
+ 'prisma': { category: 'database', name: 'Prisma ORM' },
69
+ '@prisma/client': { category: 'database', name: 'Prisma Client' },
70
+ 'drizzle-orm': { category: 'database', name: 'Drizzle ORM' },
71
+ 'typeorm': { category: 'database', name: 'TypeORM' },
72
+ 'sequelize': { category: 'database', name: 'Sequelize ORM' },
73
+ 'kysely': { category: 'database', name: 'Kysely query builder' },
74
+ // Cache / Redis
75
+ 'ioredis': { category: 'cache', name: 'Redis client (ioredis)' },
76
+ '@upstash/redis': { category: 'cache', name: 'Upstash Redis (serverless)' },
77
+ 'redis': { category: 'cache', name: 'Redis client (node-redis)' },
78
+ // Auth
79
+ '@auth/core': { category: 'auth', name: 'Auth.js authentication' },
80
+ 'next-auth': { category: 'auth', name: 'NextAuth.js authentication' },
81
+ 'lucia': { category: 'auth', name: 'Lucia auth library' },
82
+ '@clerk/nextjs': { category: 'auth', name: 'Clerk authentication' },
83
+ // Testing
84
+ 'vitest': { category: 'testing', name: 'Vitest testing framework' },
85
+ 'jest': { category: 'testing', name: 'Jest testing framework' },
86
+ 'playwright': { category: 'testing', name: 'Playwright E2E testing' },
87
+ '@playwright/test': { category: 'testing', name: 'Playwright test runner' },
88
+ 'cypress': { category: 'testing', name: 'Cypress E2E testing' },
89
+ // Quality
90
+ 'eslint': { category: 'quality', name: 'ESLint linting' },
91
+ 'prettier': { category: 'quality', name: 'Prettier code formatting' },
92
+ 'husky': { category: 'quality', name: 'Husky git hooks' },
93
+ 'lint-staged': { category: 'quality', name: 'lint-staged pre-commit' },
94
+ // Billing
95
+ 'stripe': { category: 'billing', name: 'Stripe payments' },
96
+ // Queue
97
+ 'bullmq': { category: 'queue', name: 'BullMQ job queue' },
98
+ // Realtime
99
+ 'socket.io': { category: 'realtime', name: 'Socket.IO realtime' },
100
+ 'pusher': { category: 'realtime', name: 'Pusher realtime' },
101
+ 'pusher-js': { category: 'realtime', name: 'Pusher JS client' },
102
+ // Media
103
+ 'uploadthing': { category: 'media', name: 'UploadThing file uploads' },
104
+ 'sharp': { category: 'media', name: 'Sharp image processing' },
105
+ // Design System
106
+ 'preline': { category: 'ui', name: 'Preline UI design system' },
107
+ };
108
+ function readFileIfExists(path) {
109
+ if (!existsSync(path))
110
+ return null;
111
+ try {
112
+ return readFileSync(path, 'utf8');
113
+ }
114
+ catch {
115
+ return null;
116
+ }
117
+ }
118
+ function scanCursorRules(projectDir) {
119
+ const content = readFileIfExists(join(projectDir, '.cursorrules'));
120
+ if (!content)
121
+ return null;
122
+ const patterns = [];
123
+ const sectionRegex = /^##\s+(.+)/gm;
124
+ let match;
125
+ while ((match = sectionRegex.exec(content)) !== null) {
126
+ patterns.push({
127
+ category: 'coding-style',
128
+ name: `Cursor rule section: ${match[1].trim()}`,
129
+ confidence: 90,
130
+ });
131
+ }
132
+ if (/octane/i.test(content)) {
133
+ patterns.push({ category: 'server', name: 'Octane patterns defined in .cursorrules', confidence: 95 });
134
+ }
135
+ if (/inertia/i.test(content)) {
136
+ patterns.push({ category: 'frontend', name: 'Inertia.js patterns defined in .cursorrules', confidence: 95 });
137
+ }
138
+ if (/sanctum/i.test(content)) {
139
+ patterns.push({ category: 'auth', name: 'Sanctum auth patterns defined in .cursorrules', confidence: 95 });
140
+ }
141
+ if (/uuid/i.test(content)) {
142
+ patterns.push({ category: 'database', name: 'UUIDs as primary keys', confidence: 90 });
143
+ }
144
+ if (/snake_case/i.test(content) || /camelCase/i.test(content)) {
145
+ patterns.push({ category: 'coding-style', name: 'Naming conventions defined', confidence: 85 });
146
+ }
147
+ if (/tailwind/i.test(content)) {
148
+ patterns.push({ category: 'frontend', name: 'TailwindCSS styling', confidence: 90 });
149
+ }
150
+ if (/react/i.test(content)) {
151
+ patterns.push({ category: 'frontend', name: 'ReactJS frontend', confidence: 90 });
152
+ }
153
+ if (/migration/i.test(content) && /never|forbidden|fresh/i.test(content)) {
154
+ patterns.push({ category: 'database', name: 'Migration safety rules defined', confidence: 95 });
155
+ }
156
+ if (patterns.length === 0)
157
+ return null;
158
+ return { source: '.cursorrules', patterns };
159
+ }
160
+ function scanComposerJson(projectDir) {
161
+ const content = readFileIfExists(join(projectDir, 'composer.json'));
162
+ if (!content)
163
+ return null;
164
+ let composer;
165
+ try {
166
+ composer = JSON.parse(content);
167
+ }
168
+ catch {
169
+ return null;
170
+ }
171
+ const patterns = [];
172
+ const allDeps = { ...composer.require, ...composer['require-dev'] };
173
+ for (const [pkg, meta] of Object.entries(COMPOSER_PACKAGES)) {
174
+ if (allDeps[pkg]) {
175
+ patterns.push({
176
+ category: meta.category,
177
+ name: meta.name,
178
+ confidence: 100,
179
+ detail: `${pkg}: ${allDeps[pkg]}`,
180
+ });
181
+ }
182
+ }
183
+ const phpVersion = allDeps['php'];
184
+ if (phpVersion) {
185
+ patterns.push({
186
+ category: 'runtime',
187
+ name: `PHP version constraint: ${phpVersion}`,
188
+ confidence: 100,
189
+ });
190
+ }
191
+ if (patterns.length === 0)
192
+ return null;
193
+ return { source: 'composer.json', patterns };
194
+ }
195
+ function scanPhpStan(projectDir) {
196
+ const content = readFileIfExists(join(projectDir, 'phpstan.neon')) ||
197
+ readFileIfExists(join(projectDir, 'phpstan.neon.dist'));
198
+ if (!content)
199
+ return null;
200
+ const patterns = [];
201
+ const levelMatch = content.match(/level:\s*(\d+|max)/);
202
+ if (levelMatch) {
203
+ patterns.push({
204
+ category: 'quality',
205
+ name: `PHPStan level: ${levelMatch[1]}`,
206
+ confidence: 100,
207
+ detail: `level: ${levelMatch[1]}`,
208
+ });
209
+ }
210
+ const baselineMatch = content.match(/baseline/i);
211
+ if (baselineMatch) {
212
+ patterns.push({
213
+ category: 'quality',
214
+ name: 'PHPStan baseline in use',
215
+ confidence: 90,
216
+ });
217
+ }
218
+ if (patterns.length === 0)
219
+ return null;
220
+ return { source: 'phpstan.neon', patterns };
221
+ }
222
+ // ─── Node.js Scanners ──────────────────────────────────────────────────────
223
+ function scanPackageJson(projectDir) {
224
+ const content = readFileIfExists(join(projectDir, 'package.json'));
225
+ if (!content)
226
+ return null;
227
+ let pkg;
228
+ try {
229
+ pkg = JSON.parse(content);
230
+ }
231
+ catch {
232
+ return null;
233
+ }
234
+ const patterns = [];
235
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
236
+ for (const [dep, meta] of Object.entries(NPM_PACKAGES)) {
237
+ if (allDeps[dep]) {
238
+ patterns.push({
239
+ category: meta.category,
240
+ name: meta.name,
241
+ confidence: 100,
242
+ detail: `${dep}: ${allDeps[dep]}`,
243
+ });
244
+ }
245
+ }
246
+ const nodeVersion = pkg.engines?.['node'];
247
+ if (nodeVersion) {
248
+ patterns.push({
249
+ category: 'runtime',
250
+ name: `Node.js version constraint: ${nodeVersion}`,
251
+ confidence: 100,
252
+ });
253
+ }
254
+ const bunVersion = pkg.engines?.['bun'];
255
+ if (bunVersion) {
256
+ patterns.push({
257
+ category: 'runtime',
258
+ name: `Bun version constraint: ${bunVersion}`,
259
+ confidence: 100,
260
+ });
261
+ }
262
+ if (pkg.scripts) {
263
+ if (pkg.scripts['typecheck'] || pkg.scripts['type-check']) {
264
+ patterns.push({ category: 'quality', name: 'TypeScript type checking configured', confidence: 90 });
265
+ }
266
+ if (pkg.scripts['lint']) {
267
+ patterns.push({ category: 'quality', name: 'Lint script configured', confidence: 85 });
268
+ }
269
+ if (pkg.scripts['test']) {
270
+ patterns.push({ category: 'testing', name: 'Test script configured', confidence: 85 });
271
+ }
272
+ }
273
+ if (patterns.length === 0)
274
+ return null;
275
+ return { source: 'package.json', patterns };
276
+ }
277
+ function scanTsConfig(projectDir) {
278
+ const content = readFileIfExists(join(projectDir, 'tsconfig.json'));
279
+ if (!content)
280
+ return null;
281
+ const patterns = [];
282
+ if (/"strict"\s*:\s*true/i.test(content)) {
283
+ patterns.push({ category: 'quality', name: 'TypeScript strict mode enabled', confidence: 100 });
284
+ }
285
+ if (/"paths"\s*:/i.test(content)) {
286
+ patterns.push({ category: 'coding-style', name: 'Path aliases configured', confidence: 90 });
287
+ }
288
+ const targetMatch = content.match(/"target"\s*:\s*"(\w+)"/i);
289
+ if (targetMatch) {
290
+ patterns.push({
291
+ category: 'runtime',
292
+ name: `TypeScript target: ${targetMatch[1]}`,
293
+ confidence: 95,
294
+ detail: `target: ${targetMatch[1]}`,
295
+ });
296
+ }
297
+ if (/"noUncheckedIndexedAccess"\s*:\s*true/i.test(content)) {
298
+ patterns.push({ category: 'quality', name: 'Strict index access enabled', confidence: 95 });
299
+ }
300
+ if (patterns.length === 0)
301
+ return null;
302
+ return { source: 'tsconfig.json', patterns };
303
+ }
304
+ function scanEslintConfig(projectDir) {
305
+ const configFiles = [
306
+ 'eslint.config.js', 'eslint.config.mjs', 'eslint.config.ts',
307
+ '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json', '.eslintrc.yml',
308
+ ];
309
+ let found = false;
310
+ for (const file of configFiles) {
311
+ if (existsSync(join(projectDir, file))) {
312
+ found = true;
313
+ break;
314
+ }
315
+ }
316
+ if (!found)
317
+ return null;
318
+ return {
319
+ source: 'eslint config',
320
+ patterns: [
321
+ { category: 'quality', name: 'ESLint config present', confidence: 100 },
322
+ ],
323
+ };
324
+ }
325
+ // ─── Shared Scanners ───────────────────────────────────────────────────────
326
+ function scanProjectFiles(projectDir) {
327
+ const patterns = [];
328
+ if (existsSync(join(projectDir, 'rr.yaml')) || existsSync(join(projectDir, '.rr.yaml'))) {
329
+ patterns.push({ category: 'server', name: 'RoadRunner config present (Octane)', confidence: 100 });
330
+ }
331
+ const csFixerExists = existsSync(join(projectDir, '.php-cs-fixer.php')) ||
332
+ existsSync(join(projectDir, '.php-cs-fixer.dist.php'));
333
+ if (csFixerExists) {
334
+ patterns.push({ category: 'quality', name: 'PHP-CS-Fixer config present', confidence: 100 });
335
+ }
336
+ const phpunitExists = existsSync(join(projectDir, 'phpunit.xml')) ||
337
+ existsSync(join(projectDir, 'phpunit.xml.dist'));
338
+ if (phpunitExists) {
339
+ patterns.push({ category: 'testing', name: 'PHPUnit config present', confidence: 100 });
340
+ }
341
+ const dockerExists = existsSync(join(projectDir, 'docker-compose.yml')) ||
342
+ existsSync(join(projectDir, 'docker-compose.yaml'));
343
+ if (dockerExists) {
344
+ patterns.push({ category: 'deploy', name: 'Docker Compose setup present', confidence: 90 });
345
+ }
346
+ // ── Node.js: Framework configs ──
347
+ const nextConfigExists = existsSync(join(projectDir, 'next.config.js')) ||
348
+ existsSync(join(projectDir, 'next.config.mjs')) ||
349
+ existsSync(join(projectDir, 'next.config.ts'));
350
+ if (nextConfigExists) {
351
+ patterns.push({ category: 'framework', name: 'Next.js config present', confidence: 100 });
352
+ }
353
+ if (existsSync(join(projectDir, 'nuxt.config.ts')) || existsSync(join(projectDir, 'nuxt.config.js'))) {
354
+ patterns.push({ category: 'framework', name: 'Nuxt config present', confidence: 100 });
355
+ }
356
+ if (existsSync(join(projectDir, 'astro.config.mjs')) || existsSync(join(projectDir, 'astro.config.ts'))) {
357
+ patterns.push({ category: 'framework', name: 'Astro config present', confidence: 100 });
358
+ }
359
+ if (existsSync(join(projectDir, 'svelte.config.js')) || existsSync(join(projectDir, 'svelte.config.ts'))) {
360
+ patterns.push({ category: 'framework', name: 'SvelteKit config present', confidence: 100 });
361
+ }
362
+ // ── Node.js: Frontend & UI ──
363
+ if (existsSync(join(projectDir, 'tailwind.config.ts')) || existsSync(join(projectDir, 'tailwind.config.js'))) {
364
+ patterns.push({ category: 'frontend', name: 'TailwindCSS config present', confidence: 100 });
365
+ }
366
+ if (existsSync(join(projectDir, 'postcss.config.js')) || existsSync(join(projectDir, 'postcss.config.mjs'))) {
367
+ patterns.push({ category: 'frontend', name: 'PostCSS config present', confidence: 85 });
368
+ }
369
+ if (existsSync(join(projectDir, 'components.json'))) {
370
+ patterns.push({ category: 'ui', name: 'shadcn/ui components.json present', confidence: 95 });
371
+ }
372
+ // ── Node.js: Database configs ──
373
+ if (existsSync(join(projectDir, 'prisma/schema.prisma'))) {
374
+ patterns.push({ category: 'database', name: 'Prisma schema present', confidence: 100 });
375
+ }
376
+ if (existsSync(join(projectDir, 'drizzle.config.ts')) || existsSync(join(projectDir, 'drizzle.config.js'))) {
377
+ patterns.push({ category: 'database', name: 'Drizzle config present', confidence: 100 });
378
+ }
379
+ // ── Node.js: Testing configs ──
380
+ if (existsSync(join(projectDir, 'vitest.config.ts')) || existsSync(join(projectDir, 'vitest.config.js'))) {
381
+ patterns.push({ category: 'testing', name: 'Vitest config present', confidence: 100 });
382
+ }
383
+ if (existsSync(join(projectDir, 'playwright.config.ts'))) {
384
+ patterns.push({ category: 'testing', name: 'Playwright config present', confidence: 100 });
385
+ }
386
+ if (existsSync(join(projectDir, 'cypress.config.ts')) || existsSync(join(projectDir, 'cypress.config.js'))) {
387
+ patterns.push({ category: 'testing', name: 'Cypress config present', confidence: 100 });
388
+ }
389
+ if (existsSync(join(projectDir, 'jest.config.ts')) || existsSync(join(projectDir, 'jest.config.js'))) {
390
+ patterns.push({ category: 'testing', name: 'Jest config present', confidence: 100 });
391
+ }
392
+ // ── Node.js: Package manager ──
393
+ if (existsSync(join(projectDir, 'bun.lockb')) || existsSync(join(projectDir, 'bun.lock'))) {
394
+ patterns.push({ category: 'runtime', name: 'Bun package manager in use', confidence: 100 });
395
+ }
396
+ else if (existsSync(join(projectDir, 'pnpm-lock.yaml'))) {
397
+ patterns.push({ category: 'runtime', name: 'pnpm package manager in use', confidence: 100 });
398
+ }
399
+ else if (existsSync(join(projectDir, 'yarn.lock'))) {
400
+ patterns.push({ category: 'runtime', name: 'Yarn package manager in use', confidence: 100 });
401
+ }
402
+ // ── Deploy targets ──
403
+ if (existsSync(join(projectDir, 'vercel.json'))) {
404
+ patterns.push({ category: 'deploy', name: 'Vercel deployment config', confidence: 100 });
405
+ }
406
+ if (existsSync(join(projectDir, 'netlify.toml'))) {
407
+ patterns.push({ category: 'deploy', name: 'Netlify deployment config', confidence: 100 });
408
+ }
409
+ if (existsSync(join(projectDir, 'fly.toml'))) {
410
+ patterns.push({ category: 'deploy', name: 'Fly.io deployment config', confidence: 100 });
411
+ }
412
+ if (existsSync(join(projectDir, 'railway.toml')) || existsSync(join(projectDir, 'railway.json'))) {
413
+ patterns.push({ category: 'deploy', name: 'Railway deployment config', confidence: 100 });
414
+ }
415
+ if (existsSync(join(projectDir, 'Dockerfile'))) {
416
+ patterns.push({ category: 'deploy', name: 'Dockerfile present', confidence: 95 });
417
+ }
418
+ // ── Quality / Git hooks ──
419
+ if (existsSync(join(projectDir, '.husky'))) {
420
+ patterns.push({ category: 'quality', name: 'Husky git hooks configured', confidence: 100 });
421
+ }
422
+ if (existsSync(join(projectDir, '.lintstagedrc')) || existsSync(join(projectDir, '.lintstagedrc.json'))) {
423
+ patterns.push({ category: 'quality', name: 'lint-staged config present', confidence: 95 });
424
+ }
425
+ if (existsSync(join(projectDir, '.prettierrc')) || existsSync(join(projectDir, '.prettierrc.json')) || existsSync(join(projectDir, 'prettier.config.js'))) {
426
+ patterns.push({ category: 'quality', name: 'Prettier config present', confidence: 100 });
427
+ }
428
+ // Environment files (works for both PHP and Node.js projects)
429
+ const envFiles = ['.env.example', '.env', '.env.local', '.env.development', '.env.production'];
430
+ let envContent = null;
431
+ for (const envFile of envFiles) {
432
+ const content = readFileIfExists(join(projectDir, envFile));
433
+ if (content) {
434
+ envContent = (envContent || '') + '\n' + content;
435
+ }
436
+ }
437
+ if (envContent) {
438
+ if (/REDIS_HOST|REDIS_URL/i.test(envContent)) {
439
+ patterns.push({ category: 'cache', name: 'Redis configured', confidence: 85 });
440
+ }
441
+ if (/PUSHER_|BROADCAST_|NEXT_PUBLIC_PUSHER/i.test(envContent)) {
442
+ patterns.push({ category: 'realtime', name: 'Broadcasting/Pusher configured', confidence: 80 });
443
+ }
444
+ if (/MAIL_MAILER|RESEND_API|SENDGRID/i.test(envContent)) {
445
+ patterns.push({ category: 'mail', name: 'Mail service configured', confidence: 75 });
446
+ }
447
+ if (/DATABASE_URL|DB_CONNECTION/i.test(envContent)) {
448
+ patterns.push({ category: 'database', name: 'Database connection configured', confidence: 80 });
449
+ }
450
+ if (/STRIPE_SECRET|STRIPE_KEY/i.test(envContent)) {
451
+ patterns.push({ category: 'billing', name: 'Stripe payments configured', confidence: 85 });
452
+ }
453
+ // Security: detect NEXT_PUBLIC_ with sensitive values
454
+ const sensitivePublicEnv = envContent.match(/^NEXT_PUBLIC_\w*(SECRET|TOKEN|PRIVATE|PASSWORD|CREDENTIAL|OPENAI|API_KEY|DATABASE)\w*\s*=/gmi);
455
+ if (sensitivePublicEnv) {
456
+ for (const match of sensitivePublicEnv) {
457
+ const varName = match.split('=')[0].trim();
458
+ patterns.push({
459
+ category: 'security',
460
+ name: `EXPOSED SECRET: ${varName} is public (visible in browser)`,
461
+ confidence: 100,
462
+ detail: `${varName} uses NEXT_PUBLIC_ prefix which embeds the value in the client JS bundle. Remove NEXT_PUBLIC_ and access via Route Handler or Server Action.`,
463
+ });
464
+ }
465
+ }
466
+ }
467
+ if (patterns.length === 0)
468
+ return null;
469
+ return { source: 'project files', patterns };
470
+ }
471
+ /**
472
+ * Run all scanners against a project directory.
473
+ * Returns combined results from all sources.
474
+ */
475
+ export function scanProjectStandards(projectDir) {
476
+ const scanners = [
477
+ scanCursorRules,
478
+ scanComposerJson,
479
+ scanPhpStan,
480
+ scanPackageJson,
481
+ scanTsConfig,
482
+ scanEslintConfig,
483
+ scanProjectFiles,
484
+ ];
485
+ const results = [];
486
+ for (const scanner of scanners) {
487
+ const result = scanner(projectDir);
488
+ if (result) {
489
+ results.push(result);
490
+ }
491
+ }
492
+ const allPatterns = results.flatMap((r) => r.patterns);
493
+ const sources = results.map((r) => r.source);
494
+ return {
495
+ status: results.length > 0 ? 'pending' : 'defaults',
496
+ scannedAt: new Date().toISOString().split('T')[0],
497
+ sources,
498
+ patterns: allPatterns,
499
+ userChoice: 'pending',
500
+ };
501
+ }
package/dist/setup.js CHANGED
@@ -7,6 +7,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync, statSy
7
7
  import { join, dirname, resolve } from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import ora from 'ora';
10
+ import * as ui from './ui.js';
10
11
  const __filename = fileURLToPath(import.meta.url);
11
12
  const __dirname = dirname(__filename);
12
13
  const PACKAGE_ROOT = resolve(__dirname, '..');
@@ -89,7 +90,8 @@ export async function setupProject(projectDir, config, options = {}) {
89
90
  copyDirRecursive(stackConfigDir, join(claudeDir, 'config'), options.force);
90
91
  // 8. Copy frontend-specific skills if applicable
91
92
  if (config.frontend && config.frontend !== 'none') {
92
- const frontendDir = join(PACKAGE_ROOT, 'stacks', 'frontend', config.frontend);
93
+ const frontendId = config.frontendSkillsDir || config.frontend;
94
+ const frontendDir = join(PACKAGE_ROOT, 'stacks', 'frontend', frontendId);
93
95
  if (existsSync(frontendDir)) {
94
96
  const feSkillCount = copyDirRecursive(join(frontendDir, 'skills'), join(claudeDir, 'skills'), options.force);
95
97
  spinner.text = `Loaded ${feSkillCount} frontend skills`;
@@ -138,6 +140,35 @@ export async function setupProject(projectDir, config, options = {}) {
138
140
  writeFileSync(gitignorePath, gitignore.trimEnd() + '\n\n# Claude Code local preferences\nCLAUDE.local.md\n');
139
141
  }
140
142
  }
143
+ // 11c. Save standards review results
144
+ if (config.standardsReview) {
145
+ const reviewPath = join(claudeDir, 'config', 'standards-review.json');
146
+ writeFileSync(reviewPath, JSON.stringify(config.standardsReview, null, 2));
147
+ if (config.standardsReview.status === 'adapted' && config.standardsReview.patterns.length > 0) {
148
+ const claudeMdPath = join(projectDir, 'CLAUDE.md');
149
+ if (existsSync(claudeMdPath)) {
150
+ let claudeContent = readFileSync(claudeMdPath, 'utf8');
151
+ const categories = new Map();
152
+ for (const p of config.standardsReview.patterns) {
153
+ const list = categories.get(p.category) || [];
154
+ list.push(p.name);
155
+ categories.set(p.category, list);
156
+ }
157
+ let standardsSection = '\n\n## Project Standards (imported)\n\n';
158
+ standardsSection += `> Scanned from: ${config.standardsReview.sources.join(', ')}\n\n`;
159
+ for (const [category, items] of categories) {
160
+ standardsSection += `### ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`;
161
+ for (const item of items) {
162
+ standardsSection += `- ${item}\n`;
163
+ }
164
+ standardsSection += '\n';
165
+ }
166
+ claudeContent += standardsSection;
167
+ writeFileSync(claudeMdPath, claudeContent);
168
+ }
169
+ spinner.text = `Imported ${config.standardsReview.patterns.length} project standards`;
170
+ }
171
+ }
141
172
  // 12. Copy commands
142
173
  const sharedCommandsDir = join(PACKAGE_ROOT, 'stacks', '_shared', 'commands');
143
174
  if (existsSync(sharedCommandsDir)) {
@@ -257,6 +288,20 @@ export async function setupProject(projectDir, config, options = {}) {
257
288
  settings.quality_gates = gates;
258
289
  }
259
290
  writeFileSync(join(claudeDir, 'settings.json'), JSON.stringify(settings, null, '\t'));
291
+ // 14. Detect Preline and suggest official agent skills
292
+ const projectPkgPath = join(projectDir, 'package.json');
293
+ if (existsSync(projectPkgPath)) {
294
+ try {
295
+ const projectPkg = JSON.parse(readFileSync(projectPkgPath, 'utf8'));
296
+ const allDeps = { ...projectPkg.dependencies, ...projectPkg.devDependencies };
297
+ if (allDeps['preline'] && !existsSync(join(claudeDir, 'skills', 'preline-theme-generator'))) {
298
+ ui.info('Preline UI detected — run: npx skills add htmlstreamofficial/preline');
299
+ }
300
+ }
301
+ catch {
302
+ // package.json parse error, skip
303
+ }
304
+ }
260
305
  spinner.succeed(`Setup complete: ${agentCount} agents, ${sharedSkillCount + stackSkillCount} skills, ${hookCount} hooks`);
261
306
  return true;
262
307
  }
package/dist/types.d.ts CHANGED
@@ -31,6 +31,8 @@ export interface FrameworkOption {
31
31
  name: string;
32
32
  icon: string;
33
33
  detectFiles?: string[];
34
+ skills?: string[];
35
+ default?: boolean;
34
36
  extra?: Record<string, unknown>;
35
37
  }
36
38
  export interface DatabaseOption {
@@ -42,6 +44,9 @@ export interface FrontendOption {
42
44
  id: string;
43
45
  name: string;
44
46
  icon: string;
47
+ frameworks?: string[];
48
+ skillsDir?: string;
49
+ default?: boolean;
45
50
  }
46
51
  export interface DeployTarget {
47
52
  id: string;
@@ -65,6 +70,7 @@ export interface ProjectConfig {
65
70
  framework: string;
66
71
  database: string;
67
72
  frontend: string;
73
+ frontendSkillsDir?: string;
68
74
  deploy: string;
69
75
  path: string;
70
76
  createdAt: string;
@@ -74,6 +80,7 @@ export interface ProjectConfig {
74
80
  domains: Record<string, {
75
81
  patterns: string[];
76
82
  }>;
83
+ standardsReview?: StandardsReview;
77
84
  }
78
85
  export interface DetectionResult {
79
86
  files: string[];
@@ -86,3 +93,20 @@ export interface DetectionResult {
86
93
  hasClaudeMd: boolean;
87
94
  hasGit: boolean;
88
95
  }
96
+ export interface PatternMatch {
97
+ category: string;
98
+ name: string;
99
+ confidence: number;
100
+ detail?: string;
101
+ }
102
+ export interface ScanResult {
103
+ source: string;
104
+ patterns: PatternMatch[];
105
+ }
106
+ export interface StandardsReview {
107
+ status: 'adapted' | 'defaults' | 'pending';
108
+ scannedAt: string;
109
+ sources: string[];
110
+ patterns: PatternMatch[];
111
+ userChoice: 'adapt' | 'defaults' | 'pending';
112
+ }
package/dist/ui.js CHANGED
@@ -1,12 +1,13 @@
1
1
  /**
2
2
  * Start Vibing Stacks — Terminal UI
3
3
  */
4
+ import { readFileSync } from 'fs';
5
+ import { join, dirname, resolve } from 'path';
6
+ import { fileURLToPath } from 'url';
4
7
  import chalk from 'chalk';
5
- const VERSION = '2.2.0';
6
- const gradient = (text) => {
7
- const colors = [chalk.hex('#FF6B6B'), chalk.hex('#FF8E53'), chalk.hex('#FFBD2E'), chalk.hex('#48BB78'), chalk.hex('#4299E1'), chalk.hex('#9F7AEA')];
8
- return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
9
- };
8
+ const __ui_filename = fileURLToPath(import.meta.url);
9
+ const __ui_dirname = dirname(__ui_filename);
10
+ const VERSION = JSON.parse(readFileSync(join(resolve(__ui_dirname, '..'), 'package.json'), 'utf8')).version;
10
11
  export const LOGO = `
11
12
  ${chalk.hex('#9F7AEA')(' ╔═══════════════════════════════════════════════╗')}
12
13
  ${chalk.hex('#9F7AEA')(' ║')} ${chalk.hex('#9F7AEA')('║')}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing-stacks",
3
- "version": "2.2.0",
3
+ "version": "2.4.0",
4
4
  "description": "AI-powered multi-stack dev workflow for Claude Code. Supports PHP, Node.js, Python and more.",
5
5
  "type": "module",
6
6
  "bin": {