start-vibing-stacks 2.1.1 → 2.3.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 CHANGED
@@ -20,7 +20,7 @@ svs # shortcut
20
20
  ## ✨ Features
21
21
 
22
22
  - **🔍 Auto-detection** — Scans your project files to suggest the right stack
23
- - **🐘 PHP 8.3+** — PHPStan, PHPUnit, Composer, Laravel/Octane, Symfony, Vanilla
23
+ - **🐘 PHP 8.3+** — PHPStan, PHPUnit, Composer, Laravel/Octane
24
24
  - **📦 Node.js/TS** — Vitest, TypeScript, Bun, Next.js, Express
25
25
  - **🎯 Multi-framework** — Choose your framework, database, frontend, deploy target
26
26
  - **🤖 6 Universal Agents** — research, documenter, domain-updater, commit-manager, tester, compactor
@@ -33,7 +33,7 @@ svs # shortcut
33
33
 
34
34
  | Stack | Status | Frameworks |
35
35
  |-------|--------|------------|
36
- | 🐘 PHP 8.3+ | ✅ Ready | Laravel, Laravel+Octane, Symfony, CodeIgniter, Vanilla |
36
+ | 🐘 PHP 8.3+ | ✅ Ready | Laravel, Laravel+Octane |
37
37
  | 📦 Node.js/TS | ✅ Ready | Next.js, Nuxt, Astro, Express, Fastify |
38
38
  | 🐍 Python | 🔜 Soon | Django, FastAPI, Flask |
39
39
  | 🦀 Rust | 🔜 Soon | Actix, Axum |
package/dist/detector.js CHANGED
@@ -11,7 +11,6 @@ const STACK_SIGNATURES = [
11
11
  { id: 'php', files: ['composer.json'], weight: 90, reason: 'composer.json detected' },
12
12
  { id: 'php', files: ['index.php'], weight: 60, reason: 'index.php detected' },
13
13
  { id: 'php', files: ['artisan'], weight: 95, reason: 'Laravel artisan detected' },
14
- { id: 'php', files: ['symfony.lock'], weight: 95, reason: 'Symfony lock detected' },
15
14
  { id: 'php', files: ['public/index.php'], weight: 70, reason: 'public/index.php detected' },
16
15
  { id: 'php', files: ['.php-version'], weight: 80, reason: '.php-version detected' },
17
16
  // Node.js / TypeScript
@@ -79,12 +78,11 @@ export function detectProject(projectDir) {
79
78
  * Detect framework within a PHP project
80
79
  */
81
80
  export function detectPhpFramework(projectDir) {
82
- if (existsSync(join(projectDir, 'artisan')))
81
+ if (existsSync(join(projectDir, 'artisan'))) {
82
+ if (existsSync(join(projectDir, 'rr.yaml')))
83
+ return 'laravel-octane';
83
84
  return 'laravel';
84
- if (existsSync(join(projectDir, 'symfony.lock')))
85
- return 'symfony';
86
- if (existsSync(join(projectDir, 'spark')))
87
- return 'codeigniter';
85
+ }
88
86
  return null;
89
87
  }
90
88
  /**
package/dist/index.js CHANGED
@@ -14,6 +14,7 @@ import { detectProject, detectPhpFramework, detectNodeFramework, detectPythonFra
14
14
  import { autoInstall, installComposer, installClaudeCode } from './installer.js';
15
15
  import { loadStackConfig, setupProject } from './setup.js';
16
16
  import { selectMcpServers, installMcpServers } from './mcp.js';
17
+ import { scanProjectStandards } from './scanner.js';
17
18
  // =============================================================================
18
19
  // CLI Arguments
19
20
  // =============================================================================
@@ -169,6 +170,7 @@ async function main() {
169
170
  })),
170
171
  },
171
172
  ]);
173
+ const selectedFramework = stackConfig.frameworks.find((f) => f.id === framework);
172
174
  // ─── Step 4: Select Database ───────────────────────────────────────────
173
175
  const { database } = await inquirer.prompt([
174
176
  {
@@ -182,12 +184,13 @@ async function main() {
182
184
  },
183
185
  ]);
184
186
  // ─── Step 5: Frontend ──────────────────────────────────────────────────
187
+ const compatibleFrontends = stackConfig.frontendOptions.filter((f) => !f.frameworks || f.frameworks.includes(framework));
185
188
  const { frontend } = await inquirer.prompt([
186
189
  {
187
190
  type: 'list',
188
191
  name: 'frontend',
189
192
  message: 'Frontend included?',
190
- choices: stackConfig.frontendOptions.map((f) => ({
193
+ choices: compatibleFrontends.map((f) => ({
191
194
  name: `${f.icon} ${f.name}`,
192
195
  value: f.id,
193
196
  })),
@@ -197,6 +200,54 @@ async function main() {
197
200
  const deploy = 'github';
198
201
  // ─── Step 6b: MCP Servers ────────────────────────────────────────────
199
202
  const selectedMcps = FLAGS.noMcp ? [] : await selectMcpServers(stackId, database);
203
+ // ─── Step 6c: Standards Review ──────────────────────────────────────────
204
+ let standardsReview;
205
+ const scanResults = scanProjectStandards(projectDir);
206
+ if (scanResults.patterns.length > 0) {
207
+ console.log('');
208
+ ui.header('📋 Standards Review');
209
+ ui.info(`Found ${scanResults.patterns.length} patterns from: ${scanResults.sources.join(', ')}`);
210
+ console.log('');
211
+ const categories = new Map();
212
+ for (const p of scanResults.patterns) {
213
+ const list = categories.get(p.category) || [];
214
+ list.push(p.name);
215
+ categories.set(p.category, list);
216
+ }
217
+ for (const [category, items] of categories) {
218
+ console.log(chalk.cyan(` ${category}:`));
219
+ for (const item of items.slice(0, 3)) {
220
+ console.log(chalk.dim(` • ${item}`));
221
+ }
222
+ if (items.length > 3) {
223
+ console.log(chalk.dim(` ... and ${items.length - 3} more`));
224
+ }
225
+ }
226
+ console.log('');
227
+ const { adaptStandards } = await inquirer.prompt([
228
+ {
229
+ type: 'list',
230
+ name: 'adaptStandards',
231
+ message: 'Adapt AI skills to match your project standards?',
232
+ choices: [
233
+ {
234
+ name: `🎯 Use my standards ${chalk.dim('— import patterns into AI context')}`,
235
+ value: 'adapt',
236
+ },
237
+ {
238
+ name: `📦 Use defaults ${chalk.dim('— start with plugin defaults')}`,
239
+ value: 'defaults',
240
+ },
241
+ ],
242
+ },
243
+ ]);
244
+ scanResults.status = adaptStandards === 'adapt' ? 'adapted' : 'defaults';
245
+ scanResults.userChoice = adaptStandards;
246
+ standardsReview = scanResults;
247
+ if (adaptStandards === 'adapt') {
248
+ ui.success(`${scanResults.patterns.length} patterns will be imported into AI context`);
249
+ }
250
+ }
200
251
  // ─── Step 7: Show Summary & Confirm ────────────────────────────────────
201
252
  const config = {
202
253
  name: projectName,
@@ -207,17 +258,27 @@ async function main() {
207
258
  deploy,
208
259
  path: projectDir,
209
260
  createdAt: new Date().toISOString(),
210
- skills: [...stackConfig.skills],
261
+ skills: [
262
+ ...stackConfig.skills,
263
+ ...(selectedFramework?.skills ?? []),
264
+ ],
211
265
  qualityGates: `stacks/${stackId}/config/quality-gates.json`,
212
266
  cursorRules: detection.hasCursorRules,
213
267
  domains: {},
268
+ standardsReview,
214
269
  };
270
+ const standardsLabel = standardsReview
271
+ ? standardsReview.status === 'adapted'
272
+ ? `${standardsReview.patterns.length} patterns imported`
273
+ : 'Using defaults'
274
+ : 'No patterns found';
215
275
  ui.configSummary({
216
276
  'Stack': `${stackConfig.icon} ${stackConfig.name} (${stackConfig.runtime})`,
217
277
  'Framework': framework,
218
278
  'Database': database,
219
279
  'Frontend': frontend,
220
280
  'Deploy': '🐙 GitHub (git push)',
281
+ 'Standards': standardsLabel,
221
282
  'Agents': '6 universal',
222
283
  'Skills': `${stackConfig.skills.length} stack + shared`,
223
284
  'Hooks': 'stop-validator + prompt-inject',
@@ -0,0 +1,12 @@
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 type { StandardsReview } from './types.js';
8
+ /**
9
+ * Run all scanners against a project directory.
10
+ * Returns combined results from all sources.
11
+ */
12
+ export declare function scanProjectStandards(projectDir: string): StandardsReview;
@@ -0,0 +1,480 @@
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
+ };
106
+ function readFileIfExists(path) {
107
+ if (!existsSync(path))
108
+ return null;
109
+ try {
110
+ return readFileSync(path, 'utf8');
111
+ }
112
+ catch {
113
+ return null;
114
+ }
115
+ }
116
+ function scanCursorRules(projectDir) {
117
+ const content = readFileIfExists(join(projectDir, '.cursorrules'));
118
+ if (!content)
119
+ return null;
120
+ const patterns = [];
121
+ const sectionRegex = /^##\s+(.+)/gm;
122
+ let match;
123
+ while ((match = sectionRegex.exec(content)) !== null) {
124
+ patterns.push({
125
+ category: 'coding-style',
126
+ name: `Cursor rule section: ${match[1].trim()}`,
127
+ confidence: 90,
128
+ });
129
+ }
130
+ if (/octane/i.test(content)) {
131
+ patterns.push({ category: 'server', name: 'Octane patterns defined in .cursorrules', confidence: 95 });
132
+ }
133
+ if (/inertia/i.test(content)) {
134
+ patterns.push({ category: 'frontend', name: 'Inertia.js patterns defined in .cursorrules', confidence: 95 });
135
+ }
136
+ if (/sanctum/i.test(content)) {
137
+ patterns.push({ category: 'auth', name: 'Sanctum auth patterns defined in .cursorrules', confidence: 95 });
138
+ }
139
+ if (/uuid/i.test(content)) {
140
+ patterns.push({ category: 'database', name: 'UUIDs as primary keys', confidence: 90 });
141
+ }
142
+ if (/snake_case/i.test(content) || /camelCase/i.test(content)) {
143
+ patterns.push({ category: 'coding-style', name: 'Naming conventions defined', confidence: 85 });
144
+ }
145
+ if (/tailwind/i.test(content)) {
146
+ patterns.push({ category: 'frontend', name: 'TailwindCSS styling', confidence: 90 });
147
+ }
148
+ if (/react/i.test(content)) {
149
+ patterns.push({ category: 'frontend', name: 'ReactJS frontend', confidence: 90 });
150
+ }
151
+ if (/migration/i.test(content) && /never|forbidden|fresh/i.test(content)) {
152
+ patterns.push({ category: 'database', name: 'Migration safety rules defined', confidence: 95 });
153
+ }
154
+ if (patterns.length === 0)
155
+ return null;
156
+ return { source: '.cursorrules', patterns };
157
+ }
158
+ function scanComposerJson(projectDir) {
159
+ const content = readFileIfExists(join(projectDir, 'composer.json'));
160
+ if (!content)
161
+ return null;
162
+ let composer;
163
+ try {
164
+ composer = JSON.parse(content);
165
+ }
166
+ catch {
167
+ return null;
168
+ }
169
+ const patterns = [];
170
+ const allDeps = { ...composer.require, ...composer['require-dev'] };
171
+ for (const [pkg, meta] of Object.entries(COMPOSER_PACKAGES)) {
172
+ if (allDeps[pkg]) {
173
+ patterns.push({
174
+ category: meta.category,
175
+ name: meta.name,
176
+ confidence: 100,
177
+ detail: `${pkg}: ${allDeps[pkg]}`,
178
+ });
179
+ }
180
+ }
181
+ const phpVersion = allDeps['php'];
182
+ if (phpVersion) {
183
+ patterns.push({
184
+ category: 'runtime',
185
+ name: `PHP version constraint: ${phpVersion}`,
186
+ confidence: 100,
187
+ });
188
+ }
189
+ if (patterns.length === 0)
190
+ return null;
191
+ return { source: 'composer.json', patterns };
192
+ }
193
+ function scanPhpStan(projectDir) {
194
+ const content = readFileIfExists(join(projectDir, 'phpstan.neon')) ||
195
+ readFileIfExists(join(projectDir, 'phpstan.neon.dist'));
196
+ if (!content)
197
+ return null;
198
+ const patterns = [];
199
+ const levelMatch = content.match(/level:\s*(\d+|max)/);
200
+ if (levelMatch) {
201
+ patterns.push({
202
+ category: 'quality',
203
+ name: `PHPStan level: ${levelMatch[1]}`,
204
+ confidence: 100,
205
+ detail: `level: ${levelMatch[1]}`,
206
+ });
207
+ }
208
+ const baselineMatch = content.match(/baseline/i);
209
+ if (baselineMatch) {
210
+ patterns.push({
211
+ category: 'quality',
212
+ name: 'PHPStan baseline in use',
213
+ confidence: 90,
214
+ });
215
+ }
216
+ if (patterns.length === 0)
217
+ return null;
218
+ return { source: 'phpstan.neon', patterns };
219
+ }
220
+ // ─── Node.js Scanners ──────────────────────────────────────────────────────
221
+ function scanPackageJson(projectDir) {
222
+ const content = readFileIfExists(join(projectDir, 'package.json'));
223
+ if (!content)
224
+ return null;
225
+ let pkg;
226
+ try {
227
+ pkg = JSON.parse(content);
228
+ }
229
+ catch {
230
+ return null;
231
+ }
232
+ const patterns = [];
233
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies };
234
+ for (const [dep, meta] of Object.entries(NPM_PACKAGES)) {
235
+ if (allDeps[dep]) {
236
+ patterns.push({
237
+ category: meta.category,
238
+ name: meta.name,
239
+ confidence: 100,
240
+ detail: `${dep}: ${allDeps[dep]}`,
241
+ });
242
+ }
243
+ }
244
+ const nodeVersion = pkg.engines?.['node'];
245
+ if (nodeVersion) {
246
+ patterns.push({
247
+ category: 'runtime',
248
+ name: `Node.js version constraint: ${nodeVersion}`,
249
+ confidence: 100,
250
+ });
251
+ }
252
+ const bunVersion = pkg.engines?.['bun'];
253
+ if (bunVersion) {
254
+ patterns.push({
255
+ category: 'runtime',
256
+ name: `Bun version constraint: ${bunVersion}`,
257
+ confidence: 100,
258
+ });
259
+ }
260
+ if (pkg.scripts) {
261
+ if (pkg.scripts['typecheck'] || pkg.scripts['type-check']) {
262
+ patterns.push({ category: 'quality', name: 'TypeScript type checking configured', confidence: 90 });
263
+ }
264
+ if (pkg.scripts['lint']) {
265
+ patterns.push({ category: 'quality', name: 'Lint script configured', confidence: 85 });
266
+ }
267
+ if (pkg.scripts['test']) {
268
+ patterns.push({ category: 'testing', name: 'Test script configured', confidence: 85 });
269
+ }
270
+ }
271
+ if (patterns.length === 0)
272
+ return null;
273
+ return { source: 'package.json', patterns };
274
+ }
275
+ function scanTsConfig(projectDir) {
276
+ const content = readFileIfExists(join(projectDir, 'tsconfig.json'));
277
+ if (!content)
278
+ return null;
279
+ const patterns = [];
280
+ if (/"strict"\s*:\s*true/i.test(content)) {
281
+ patterns.push({ category: 'quality', name: 'TypeScript strict mode enabled', confidence: 100 });
282
+ }
283
+ if (/"paths"\s*:/i.test(content)) {
284
+ patterns.push({ category: 'coding-style', name: 'Path aliases configured', confidence: 90 });
285
+ }
286
+ const targetMatch = content.match(/"target"\s*:\s*"(\w+)"/i);
287
+ if (targetMatch) {
288
+ patterns.push({
289
+ category: 'runtime',
290
+ name: `TypeScript target: ${targetMatch[1]}`,
291
+ confidence: 95,
292
+ detail: `target: ${targetMatch[1]}`,
293
+ });
294
+ }
295
+ if (/"noUncheckedIndexedAccess"\s*:\s*true/i.test(content)) {
296
+ patterns.push({ category: 'quality', name: 'Strict index access enabled', confidence: 95 });
297
+ }
298
+ if (patterns.length === 0)
299
+ return null;
300
+ return { source: 'tsconfig.json', patterns };
301
+ }
302
+ function scanEslintConfig(projectDir) {
303
+ const configFiles = [
304
+ 'eslint.config.js', 'eslint.config.mjs', 'eslint.config.ts',
305
+ '.eslintrc.js', '.eslintrc.cjs', '.eslintrc.json', '.eslintrc.yml',
306
+ ];
307
+ let found = false;
308
+ for (const file of configFiles) {
309
+ if (existsSync(join(projectDir, file))) {
310
+ found = true;
311
+ break;
312
+ }
313
+ }
314
+ if (!found)
315
+ return null;
316
+ return {
317
+ source: 'eslint config',
318
+ patterns: [
319
+ { category: 'quality', name: 'ESLint config present', confidence: 100 },
320
+ ],
321
+ };
322
+ }
323
+ // ─── Shared Scanners ───────────────────────────────────────────────────────
324
+ function scanProjectFiles(projectDir) {
325
+ const patterns = [];
326
+ if (existsSync(join(projectDir, 'rr.yaml')) || existsSync(join(projectDir, '.rr.yaml'))) {
327
+ patterns.push({ category: 'server', name: 'RoadRunner config present (Octane)', confidence: 100 });
328
+ }
329
+ const csFixerExists = existsSync(join(projectDir, '.php-cs-fixer.php')) ||
330
+ existsSync(join(projectDir, '.php-cs-fixer.dist.php'));
331
+ if (csFixerExists) {
332
+ patterns.push({ category: 'quality', name: 'PHP-CS-Fixer config present', confidence: 100 });
333
+ }
334
+ const phpunitExists = existsSync(join(projectDir, 'phpunit.xml')) ||
335
+ existsSync(join(projectDir, 'phpunit.xml.dist'));
336
+ if (phpunitExists) {
337
+ patterns.push({ category: 'testing', name: 'PHPUnit config present', confidence: 100 });
338
+ }
339
+ const dockerExists = existsSync(join(projectDir, 'docker-compose.yml')) ||
340
+ existsSync(join(projectDir, 'docker-compose.yaml'));
341
+ if (dockerExists) {
342
+ patterns.push({ category: 'deploy', name: 'Docker Compose setup present', confidence: 90 });
343
+ }
344
+ // ── Node.js: Framework configs ──
345
+ const nextConfigExists = existsSync(join(projectDir, 'next.config.js')) ||
346
+ existsSync(join(projectDir, 'next.config.mjs')) ||
347
+ existsSync(join(projectDir, 'next.config.ts'));
348
+ if (nextConfigExists) {
349
+ patterns.push({ category: 'framework', name: 'Next.js config present', confidence: 100 });
350
+ }
351
+ if (existsSync(join(projectDir, 'nuxt.config.ts')) || existsSync(join(projectDir, 'nuxt.config.js'))) {
352
+ patterns.push({ category: 'framework', name: 'Nuxt config present', confidence: 100 });
353
+ }
354
+ if (existsSync(join(projectDir, 'astro.config.mjs')) || existsSync(join(projectDir, 'astro.config.ts'))) {
355
+ patterns.push({ category: 'framework', name: 'Astro config present', confidence: 100 });
356
+ }
357
+ if (existsSync(join(projectDir, 'svelte.config.js')) || existsSync(join(projectDir, 'svelte.config.ts'))) {
358
+ patterns.push({ category: 'framework', name: 'SvelteKit config present', confidence: 100 });
359
+ }
360
+ // ── Node.js: Frontend & UI ──
361
+ if (existsSync(join(projectDir, 'tailwind.config.ts')) || existsSync(join(projectDir, 'tailwind.config.js'))) {
362
+ patterns.push({ category: 'frontend', name: 'TailwindCSS config present', confidence: 100 });
363
+ }
364
+ if (existsSync(join(projectDir, 'postcss.config.js')) || existsSync(join(projectDir, 'postcss.config.mjs'))) {
365
+ patterns.push({ category: 'frontend', name: 'PostCSS config present', confidence: 85 });
366
+ }
367
+ if (existsSync(join(projectDir, 'components.json'))) {
368
+ patterns.push({ category: 'ui', name: 'shadcn/ui components.json present', confidence: 95 });
369
+ }
370
+ // ── Node.js: Database configs ──
371
+ if (existsSync(join(projectDir, 'prisma/schema.prisma'))) {
372
+ patterns.push({ category: 'database', name: 'Prisma schema present', confidence: 100 });
373
+ }
374
+ if (existsSync(join(projectDir, 'drizzle.config.ts')) || existsSync(join(projectDir, 'drizzle.config.js'))) {
375
+ patterns.push({ category: 'database', name: 'Drizzle config present', confidence: 100 });
376
+ }
377
+ // ── Node.js: Testing configs ──
378
+ if (existsSync(join(projectDir, 'vitest.config.ts')) || existsSync(join(projectDir, 'vitest.config.js'))) {
379
+ patterns.push({ category: 'testing', name: 'Vitest config present', confidence: 100 });
380
+ }
381
+ if (existsSync(join(projectDir, 'playwright.config.ts'))) {
382
+ patterns.push({ category: 'testing', name: 'Playwright config present', confidence: 100 });
383
+ }
384
+ if (existsSync(join(projectDir, 'cypress.config.ts')) || existsSync(join(projectDir, 'cypress.config.js'))) {
385
+ patterns.push({ category: 'testing', name: 'Cypress config present', confidence: 100 });
386
+ }
387
+ if (existsSync(join(projectDir, 'jest.config.ts')) || existsSync(join(projectDir, 'jest.config.js'))) {
388
+ patterns.push({ category: 'testing', name: 'Jest config present', confidence: 100 });
389
+ }
390
+ // ── Node.js: Package manager ──
391
+ if (existsSync(join(projectDir, 'bun.lockb')) || existsSync(join(projectDir, 'bun.lock'))) {
392
+ patterns.push({ category: 'runtime', name: 'Bun package manager in use', confidence: 100 });
393
+ }
394
+ else if (existsSync(join(projectDir, 'pnpm-lock.yaml'))) {
395
+ patterns.push({ category: 'runtime', name: 'pnpm package manager in use', confidence: 100 });
396
+ }
397
+ else if (existsSync(join(projectDir, 'yarn.lock'))) {
398
+ patterns.push({ category: 'runtime', name: 'Yarn package manager in use', confidence: 100 });
399
+ }
400
+ // ── Deploy targets ──
401
+ if (existsSync(join(projectDir, 'vercel.json'))) {
402
+ patterns.push({ category: 'deploy', name: 'Vercel deployment config', confidence: 100 });
403
+ }
404
+ if (existsSync(join(projectDir, 'netlify.toml'))) {
405
+ patterns.push({ category: 'deploy', name: 'Netlify deployment config', confidence: 100 });
406
+ }
407
+ if (existsSync(join(projectDir, 'fly.toml'))) {
408
+ patterns.push({ category: 'deploy', name: 'Fly.io deployment config', confidence: 100 });
409
+ }
410
+ if (existsSync(join(projectDir, 'railway.toml')) || existsSync(join(projectDir, 'railway.json'))) {
411
+ patterns.push({ category: 'deploy', name: 'Railway deployment config', confidence: 100 });
412
+ }
413
+ if (existsSync(join(projectDir, 'Dockerfile'))) {
414
+ patterns.push({ category: 'deploy', name: 'Dockerfile present', confidence: 95 });
415
+ }
416
+ // ── Quality / Git hooks ──
417
+ if (existsSync(join(projectDir, '.husky'))) {
418
+ patterns.push({ category: 'quality', name: 'Husky git hooks configured', confidence: 100 });
419
+ }
420
+ if (existsSync(join(projectDir, '.lintstagedrc')) || existsSync(join(projectDir, '.lintstagedrc.json'))) {
421
+ patterns.push({ category: 'quality', name: 'lint-staged config present', confidence: 95 });
422
+ }
423
+ if (existsSync(join(projectDir, '.prettierrc')) || existsSync(join(projectDir, '.prettierrc.json')) || existsSync(join(projectDir, 'prettier.config.js'))) {
424
+ patterns.push({ category: 'quality', name: 'Prettier config present', confidence: 100 });
425
+ }
426
+ // Environment files (works for both PHP and Node.js projects)
427
+ const envContent = readFileIfExists(join(projectDir, '.env.example')) ||
428
+ readFileIfExists(join(projectDir, '.env'));
429
+ if (envContent) {
430
+ if (/REDIS_HOST|REDIS_URL/i.test(envContent)) {
431
+ patterns.push({ category: 'cache', name: 'Redis configured', confidence: 85 });
432
+ }
433
+ if (/PUSHER_|BROADCAST_|NEXT_PUBLIC_PUSHER/i.test(envContent)) {
434
+ patterns.push({ category: 'realtime', name: 'Broadcasting/Pusher configured', confidence: 80 });
435
+ }
436
+ if (/MAIL_MAILER|RESEND_API|SENDGRID/i.test(envContent)) {
437
+ patterns.push({ category: 'mail', name: 'Mail service configured', confidence: 75 });
438
+ }
439
+ if (/DATABASE_URL|DB_CONNECTION/i.test(envContent)) {
440
+ patterns.push({ category: 'database', name: 'Database connection configured', confidence: 80 });
441
+ }
442
+ if (/STRIPE_SECRET|STRIPE_KEY/i.test(envContent)) {
443
+ patterns.push({ category: 'billing', name: 'Stripe payments configured', confidence: 85 });
444
+ }
445
+ }
446
+ if (patterns.length === 0)
447
+ return null;
448
+ return { source: 'project files', patterns };
449
+ }
450
+ /**
451
+ * Run all scanners against a project directory.
452
+ * Returns combined results from all sources.
453
+ */
454
+ export function scanProjectStandards(projectDir) {
455
+ const scanners = [
456
+ scanCursorRules,
457
+ scanComposerJson,
458
+ scanPhpStan,
459
+ scanPackageJson,
460
+ scanTsConfig,
461
+ scanEslintConfig,
462
+ scanProjectFiles,
463
+ ];
464
+ const results = [];
465
+ for (const scanner of scanners) {
466
+ const result = scanner(projectDir);
467
+ if (result) {
468
+ results.push(result);
469
+ }
470
+ }
471
+ const allPatterns = results.flatMap((r) => r.patterns);
472
+ const sources = results.map((r) => r.source);
473
+ return {
474
+ status: results.length > 0 ? 'pending' : 'defaults',
475
+ scannedAt: new Date().toISOString().split('T')[0],
476
+ sources,
477
+ patterns: allPatterns,
478
+ userChoice: 'pending',
479
+ };
480
+ }