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.
- package/README.md +2 -2
- package/dist/detector.js +23 -11
- package/dist/index.js +78 -5
- package/dist/scanner.d.ts +12 -0
- package/dist/scanner.js +501 -0
- package/dist/setup.js +46 -1
- package/dist/types.d.ts +24 -0
- package/dist/ui.js +6 -5
- package/package.json +1 -1
- package/stacks/_shared/config/security-rules.json +27 -5
- package/stacks/_shared/hooks/user-prompt-submit.ts +26 -2
- package/stacks/frontend/react/skills/preline-ui/SKILL.md +31 -35
- package/stacks/frontend/react/skills/react-standards/SKILL.md +20 -20
- package/stacks/frontend/react/skills/react-ui-patterns/SKILL.md +78 -42
- package/stacks/frontend/react/skills/tailwind-patterns/SKILL.md +1 -1
- package/stacks/frontend/react/skills/zod-validation/SKILL.md +84 -18
- 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/nodejs/skills/nextjs-app-router/SKILL.md +101 -0
- package/stacks/nodejs/stack.json +43 -121
- 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-nodejs.md +323 -0
- package/templates/CLAUDE-php.md +233 -33
package/dist/scanner.js
ADDED
|
@@ -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
|
|
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
|
|
6
|
-
const
|
|
7
|
-
|
|
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')('║')}
|