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 +2 -2
- package/dist/detector.js +4 -6
- package/dist/index.js +63 -2
- package/dist/scanner.d.ts +12 -0
- package/dist/scanner.js +480 -0
- package/dist/setup.js +29 -0
- package/dist/types.d.ts +20 -0
- package/dist/ui.js +1 -1
- package/package.json +1 -1
- package/stacks/_shared/hooks/user-prompt-submit.ts +26 -2
- package/stacks/frontend/react-inertia/skills/inertia-react/SKILL.md +342 -0
- package/stacks/frontend/react-inertia/skills/react-standards/SKILL.md +267 -0
- package/stacks/php/skills/api-security/SKILL.md +431 -0
- package/stacks/php/skills/laravel-octane/SKILL.md +155 -53
- package/stacks/php/skills/laravel-patterns/SKILL.md +244 -39
- package/stacks/php/skills/php-patterns/SKILL.md +113 -53
- package/stacks/php/skills/security-scan-php/SKILL.md +161 -43
- package/stacks/php/stack.json +19 -6
- package/templates/CLAUDE-php.md +108 -29
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
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: [
|
|
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;
|
package/dist/scanner.js
ADDED
|
@@ -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
|
+
}
|