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/dist/setup.js CHANGED
@@ -138,6 +138,35 @@ export async function setupProject(projectDir, config, options = {}) {
138
138
  writeFileSync(gitignorePath, gitignore.trimEnd() + '\n\n# Claude Code local preferences\nCLAUDE.local.md\n');
139
139
  }
140
140
  }
141
+ // 11c. Save standards review results
142
+ if (config.standardsReview) {
143
+ const reviewPath = join(claudeDir, 'config', 'standards-review.json');
144
+ writeFileSync(reviewPath, JSON.stringify(config.standardsReview, null, 2));
145
+ if (config.standardsReview.status === 'adapted' && config.standardsReview.patterns.length > 0) {
146
+ const claudeMdPath = join(projectDir, 'CLAUDE.md');
147
+ if (existsSync(claudeMdPath)) {
148
+ let claudeContent = readFileSync(claudeMdPath, 'utf8');
149
+ const categories = new Map();
150
+ for (const p of config.standardsReview.patterns) {
151
+ const list = categories.get(p.category) || [];
152
+ list.push(p.name);
153
+ categories.set(p.category, list);
154
+ }
155
+ let standardsSection = '\n\n## Project Standards (imported)\n\n';
156
+ standardsSection += `> Scanned from: ${config.standardsReview.sources.join(', ')}\n\n`;
157
+ for (const [category, items] of categories) {
158
+ standardsSection += `### ${category.charAt(0).toUpperCase() + category.slice(1)}\n\n`;
159
+ for (const item of items) {
160
+ standardsSection += `- ${item}\n`;
161
+ }
162
+ standardsSection += '\n';
163
+ }
164
+ claudeContent += standardsSection;
165
+ writeFileSync(claudeMdPath, claudeContent);
166
+ }
167
+ spinner.text = `Imported ${config.standardsReview.patterns.length} project standards`;
168
+ }
169
+ }
141
170
  // 12. Copy commands
142
171
  const sharedCommandsDir = join(PACKAGE_ROOT, 'stacks', '_shared', 'commands');
143
172
  if (existsSync(sharedCommandsDir)) {
package/dist/types.d.ts CHANGED
@@ -31,6 +31,7 @@ export interface FrameworkOption {
31
31
  name: string;
32
32
  icon: string;
33
33
  detectFiles?: string[];
34
+ skills?: string[];
34
35
  extra?: Record<string, unknown>;
35
36
  }
36
37
  export interface DatabaseOption {
@@ -42,6 +43,7 @@ export interface FrontendOption {
42
43
  id: string;
43
44
  name: string;
44
45
  icon: string;
46
+ frameworks?: string[];
45
47
  }
46
48
  export interface DeployTarget {
47
49
  id: string;
@@ -74,6 +76,7 @@ export interface ProjectConfig {
74
76
  domains: Record<string, {
75
77
  patterns: string[];
76
78
  }>;
79
+ standardsReview?: StandardsReview;
77
80
  }
78
81
  export interface DetectionResult {
79
82
  files: string[];
@@ -86,3 +89,20 @@ export interface DetectionResult {
86
89
  hasClaudeMd: boolean;
87
90
  hasGit: boolean;
88
91
  }
92
+ export interface PatternMatch {
93
+ category: string;
94
+ name: string;
95
+ confidence: number;
96
+ detail?: string;
97
+ }
98
+ export interface ScanResult {
99
+ source: string;
100
+ patterns: PatternMatch[];
101
+ }
102
+ export interface StandardsReview {
103
+ status: 'adapted' | 'defaults' | 'pending';
104
+ scannedAt: string;
105
+ sources: string[];
106
+ patterns: PatternMatch[];
107
+ userChoice: 'adapt' | 'defaults' | 'pending';
108
+ }
package/dist/ui.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Start Vibing Stacks — Terminal UI
3
3
  */
4
4
  import chalk from 'chalk';
5
- const VERSION = '2.1.1';
5
+ const VERSION = '2.2.0';
6
6
  const gradient = (text) => {
7
7
  const colors = [chalk.hex('#FF6B6B'), chalk.hex('#FF8E53'), chalk.hex('#FFBD2E'), chalk.hex('#48BB78'), chalk.hex('#4299E1'), chalk.hex('#9F7AEA')];
8
8
  return text.split('').map((c, i) => colors[i % colors.length](c)).join('');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing-stacks",
3
- "version": "2.1.1",
3
+ "version": "2.3.0",
4
4
  "description": "AI-powered multi-stack dev workflow for Claude Code. Supports PHP, Node.js, Python and more.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@ import { join } from 'path';
11
11
 
12
12
  const PROJECT_DIR = process.env['CLAUDE_PROJECT_DIR'] || process.cwd();
13
13
  const ACTIVE_PROJECT = join(PROJECT_DIR, '.claude', 'config', 'active-project.json');
14
+ const STANDARDS_REVIEW = join(PROJECT_DIR, '.claude', 'config', 'standards-review.json');
14
15
 
15
16
  let stackName = 'Unknown';
16
17
  let qualityCmd = 'Run quality gates';
@@ -18,7 +19,6 @@ try {
18
19
  if (existsSync(ACTIVE_PROJECT)) {
19
20
  const config = JSON.parse(readFileSync(ACTIVE_PROJECT, 'utf8'));
20
21
  stackName = config.stack || 'Unknown';
21
- // Read quality gates command from stack config
22
22
  const stackConfig = join(PROJECT_DIR, '.claude', 'config', 'quality-gates.json');
23
23
  if (existsSync(stackConfig)) {
24
24
  const gates = JSON.parse(readFileSync(stackConfig, 'utf8'));
@@ -27,6 +27,30 @@ try {
27
27
  }
28
28
  } catch {}
29
29
 
30
+ interface ReviewFile {
31
+ status: string;
32
+ sources?: string[];
33
+ patterns?: { category: string; name: string }[];
34
+ }
35
+
36
+ let standardsContext = '';
37
+ try {
38
+ if (!existsSync(STANDARDS_REVIEW)) {
39
+ standardsContext = `\n\nSTANDARDS REVIEW NEEDED: No standards-review.json found. ` +
40
+ `This project may have existing coding standards (.cursorrules, composer.json configs). ` +
41
+ `Ask the user: "I noticed this project hasn't been scanned for existing standards. ` +
42
+ `Would you like me to review your codebase patterns and adapt my behavior, ` +
43
+ `or should I use the default standards?"`;
44
+ } else {
45
+ const review: ReviewFile = JSON.parse(readFileSync(STANDARDS_REVIEW, 'utf8'));
46
+ if (review.status === 'adapted' && review.patterns && review.patterns.length > 0) {
47
+ const patternList = review.patterns.map(p => `- [${p.category}] ${p.name}`).join('\n');
48
+ standardsContext = `\n\nPROJECT STANDARDS (scanned from ${(review.sources || []).join(', ')}):\n${patternList}\n` +
49
+ `Follow these project-specific patterns. They take priority over generic defaults.`;
50
+ }
51
+ }
52
+ } catch {}
53
+
30
54
  async function main(): Promise<void> {
31
55
  let hookInput: any = {};
32
56
  try {
@@ -65,7 +89,7 @@ async function main(): Promise<void> {
65
89
  a. "## Last Change" (date: ${today}, branch, summary)
66
90
  b. Update ALL affected rule/flow sections
67
91
 
68
- 6. Run stop-validator before finishing.`;
92
+ 6. Run stop-validator before finishing.${standardsContext}`;
69
93
 
70
94
  console.log(JSON.stringify({ continue: true, systemMessage }));
71
95
  process.exit(0);
@@ -0,0 +1,342 @@
1
+ # Inertia.js + React Integration
2
+
3
+ ## Architecture Overview
4
+
5
+ Inertia.js acts as a bridge between Laravel (backend) and React (frontend). There is NO separate API layer — controllers return Inertia responses that render React page components with server-side data as props.
6
+
7
+ ```
8
+ Request Flow:
9
+ Browser → Laravel Router → Controller → Inertia::render('Page', $props)
10
+
11
+ HandleInertiaRequests (middleware)
12
+
13
+ InertiaShare (shared props: auth, translations, menu)
14
+
15
+ React Page Component (receives all props)
16
+ ```
17
+
18
+ ## Backend: Middleware
19
+
20
+ ### HandleInertiaRequests
21
+
22
+ The Inertia middleware merges shared props (auth, translations, flash messages) into every response:
23
+
24
+ ```php
25
+ namespace App\Http\Middleware;
26
+
27
+ use Inertia\Middleware;
28
+ use App\Support\InertiaShare;
29
+
30
+ class HandleInertiaRequests extends Middleware
31
+ {
32
+ protected $rootView = 'app';
33
+
34
+ public function version(Request $request): ?string
35
+ {
36
+ return parent::version($request);
37
+ }
38
+
39
+ public function share(Request $request): array
40
+ {
41
+ return array_merge(
42
+ parent::share($request),
43
+ InertiaShare::getProps($request),
44
+ );
45
+ }
46
+ }
47
+ ```
48
+
49
+ **Rules:**
50
+ - Keep `share()` lean — delegate to `InertiaShare` helper
51
+ - `parent::share()` provides validation errors automatically
52
+ - `version()` triggers full page reload on asset changes
53
+
54
+ ### InertiaShare Support Class
55
+
56
+ Centralize all shared props in `App\Support\InertiaShare`:
57
+
58
+ ```php
59
+ namespace App\Support;
60
+
61
+ class InertiaShare
62
+ {
63
+ public static function getProps(Request $request): array
64
+ {
65
+ $locale = App::getLocale();
66
+
67
+ return [
68
+ 'auth' => [
69
+ 'user' => $request->user() ? [
70
+ 'id' => $request->user()->id,
71
+ 'name' => $request->user()->name,
72
+ 'role' => $request->user()->role,
73
+ 'email' => $request->user()->email,
74
+ 'timezone' => $request->user()->timezone,
75
+ ] : null,
76
+ ],
77
+ 'locale' => $locale,
78
+ 'translations' => static::getTranslations(
79
+ $locale,
80
+ $request->route()->uri,
81
+ ),
82
+ 'flash' => $request->hasSession() ? [
83
+ 'success' => Session::get('success'),
84
+ 'error' => Session::get('error'),
85
+ ] : [],
86
+ ];
87
+ }
88
+ }
89
+ ```
90
+
91
+ **Rules:**
92
+ - Auth data: expose only necessary fields (never passwords, tokens)
93
+ - Translations loaded on-demand per page (not all at once)
94
+ - Flash messages via Laravel session
95
+ - Menu structure loaded from DB, filtered by permissions, cached per user
96
+
97
+ ## Backend: Controllers with Inertia
98
+
99
+ Controllers render React page components via `Inertia::render()`:
100
+
101
+ ```php
102
+ use Inertia\Inertia;
103
+ use Inertia\Response as InertiaResponse;
104
+
105
+ class DashboardController extends Controller
106
+ {
107
+ public function __construct(
108
+ private readonly DashboardService $service,
109
+ ) {}
110
+
111
+ public function index(Request $request): InertiaResponse
112
+ {
113
+ return Inertia::render('Dashboard/Index', [
114
+ 'stats' => $this->service->getStats($request->user()),
115
+ 'recentOrders' => fn () => OrderResource::collection(
116
+ $request->user()->orders()->latest()->limit(10)->get()
117
+ ),
118
+ ]);
119
+ }
120
+
121
+ public function show(Order $order): InertiaResponse
122
+ {
123
+ return Inertia::render('Orders/Show', [
124
+ 'order' => OrderResource::make($order->load('items')),
125
+ ]);
126
+ }
127
+ }
128
+ ```
129
+
130
+ **Rules:**
131
+ - Return type: `Inertia\Response` (not `JsonResponse`)
132
+ - Page component path maps to: `resources/js/Pages/{path}`
133
+ - Use lazy props with `fn ()` for data not needed on first render
134
+ - Use API Resources to format complex data before passing as props
135
+ - Keep controller thin — delegate to services
136
+
137
+ ### Inertia Redirects
138
+
139
+ ```php
140
+ // After mutations, redirect (Inertia handles SPA navigation)
141
+ public function store(StoreOrderRequest $request): RedirectResponse
142
+ {
143
+ $order = $this->service->create($request->validated());
144
+
145
+ return redirect()
146
+ ->route('orders.show', $order)
147
+ ->with('success', __('orders.created'));
148
+ }
149
+
150
+ public function destroy(Order $order): RedirectResponse
151
+ {
152
+ $this->service->delete($order);
153
+
154
+ return redirect()
155
+ ->route('orders.index')
156
+ ->with('success', __('orders.deleted'));
157
+ }
158
+ ```
159
+
160
+ **Rule:** POST/PUT/DELETE actions return `redirect()` with flash messages, never `Inertia::render()`.
161
+
162
+ ## Backend: Translations On-Demand
163
+
164
+ Translations are loaded per-page via a config file that maps routes to translation files:
165
+
166
+ ```php
167
+ // config/translations_inertia.php
168
+ return [
169
+ 'global' => ['common', 'errors', 'validation'],
170
+
171
+ 'pages' => [
172
+ 'dashboard' => ['dashboard'],
173
+ 'orders/*' => ['orders', 'products'],
174
+ 'settings/*' => ['settings'],
175
+ ],
176
+ ];
177
+ ```
178
+
179
+ The `InertiaShare::getTranslations()` method:
180
+ 1. Loads global translation files (always sent)
181
+ 2. Loads page-specific files based on route URI
182
+ 3. Merges PHP files (`lang/{locale}/*.php`) + JSON file (`lang/{locale}.json`)
183
+ 4. Caches result per locale + page combination
184
+
185
+ **Rules:**
186
+ - Global files: `common`, `errors`, `validation` (always loaded)
187
+ - Page files: only load what the page needs
188
+ - Cache invalidation: clear on deploy (`php artisan cache:clear`)
189
+ - Store translations in `lang/en/*.php` and `lang/pt/*.php`
190
+
191
+ ## Frontend: Translation Helper
192
+
193
+ The `__()` function resolves translation keys from Inertia shared props:
194
+
195
+ ```js
196
+ // resources/js/Utils/translate.js
197
+ import { usePage } from '@inertiajs/react';
198
+
199
+ export default function __(key, replacements = {}, pageProps = null) {
200
+ const propsSource = pageProps ? { props: pageProps } : usePage();
201
+ const translations = propsSource.props.translations || {};
202
+
203
+ let translation = key.split('.').reduce((obj, part) => {
204
+ return obj && typeof obj[part] !== 'undefined' ? obj[part] : null;
205
+ }, translations);
206
+
207
+ if (translation === null) {
208
+ return key;
209
+ }
210
+
211
+ if (typeof translation === 'string' && Object.keys(replacements).length > 0) {
212
+ Object.keys(replacements).forEach((placeholder) => {
213
+ translation = translation.replace(
214
+ new RegExp(`:${placeholder}`, 'g'),
215
+ replacements[placeholder],
216
+ );
217
+ });
218
+ }
219
+
220
+ return translation;
221
+ }
222
+ ```
223
+
224
+ ### Usage in React Components
225
+
226
+ ```tsx
227
+ import __ from '@/Utils/translate';
228
+
229
+ // CORRECT: Define translations as CONST before hooks
230
+ const LABELS = {
231
+ title: __('dashboard.title'),
232
+ welcome: __('dashboard.welcome', { name: 'User' }),
233
+ save: __('common.save'),
234
+ };
235
+
236
+ export default function Dashboard({ stats }) {
237
+ const [loading, setLoading] = useState(false);
238
+
239
+ return (
240
+ <div>
241
+ <h1>{LABELS.title}</h1>
242
+ <p>{LABELS.welcome}</p>
243
+ </div>
244
+ );
245
+ }
246
+
247
+ // WRONG: Calling __() inside JSX (React Hook violation)
248
+ return <h1>{__('dashboard.title')}</h1>; // NEVER
249
+ ```
250
+
251
+ **Rules:**
252
+ - ALWAYS define translations as `CONST` at the top of the component, BEFORE hooks
253
+ - NEVER call `__()` inside JSX or render methods
254
+ - New strings must be added to both `lang/en/*.php` and `lang/pt/*.php`
255
+ - Error strings centralized in `lang/*/errors.php`
256
+ - Use replacements for dynamic values: `__('greeting', { name: userName })`
257
+
258
+ ## Frontend: Page Component Structure
259
+
260
+ ```
261
+ resources/js/
262
+ ├── Pages/
263
+ │ ├── Dashboard/
264
+ │ │ └── Index.jsx
265
+ │ ├── Orders/
266
+ │ │ ├── Index.jsx
267
+ │ │ ├── Show.jsx
268
+ │ │ └── _components/ # Page-specific components
269
+ │ │ ├── OrderTable.jsx
270
+ │ │ └── OrderFilters.jsx
271
+ │ ├── Auth/
272
+ │ │ ├── Login.jsx
273
+ │ │ └── Register.jsx
274
+ │ └── Users/
275
+ │ └── Admin/
276
+ │ └── Dashboard.jsx
277
+ ├── Components/
278
+ │ ├── UI/ # Reusable UI primitives
279
+ │ ├── Layout/ # Header, Sidebar, Footer
280
+ │ └── Shared/ # Cross-feature components
281
+ ├── Icons/
282
+ │ ├── index.js # Barrel export
283
+ │ ├── CheckIcon.svg
284
+ │ └── AlertIcon.svg
285
+ ├── Layouts/
286
+ │ ├── AuthenticatedLayout.jsx
287
+ │ └── GuestLayout.jsx
288
+ └── Utils/
289
+ └── translate.js # __() helper
290
+ ```
291
+
292
+ **Rules:**
293
+ - Pages map 1:1 to `Inertia::render('Path/Component')`
294
+ - Page-specific components in `_components/` folder
295
+ - Shared components in `Components/`
296
+ - Icons as separate `.svg` files, imported with `?react` suffix
297
+
298
+ ## Frontend: Inertia Hooks
299
+
300
+ ```tsx
301
+ import { usePage, useForm, router, Link } from '@inertiajs/react';
302
+
303
+ // Access shared props
304
+ const { auth, flash, locale } = usePage().props;
305
+
306
+ // Form handling with Inertia
307
+ const { data, setData, post, processing, errors } = useForm({
308
+ name: '',
309
+ email: '',
310
+ });
311
+
312
+ const handleSubmit = (e) => {
313
+ e.preventDefault();
314
+ post(route('users.store'));
315
+ };
316
+
317
+ // Programmatic navigation
318
+ router.visit(route('dashboard'));
319
+ router.reload({ only: ['stats'] }); // Partial reload
320
+
321
+ // Links (SPA navigation, no full page reload)
322
+ <Link href={route('orders.index')}>Orders</Link>
323
+ ```
324
+
325
+ **Rules:**
326
+ - Use `useForm` for all form submissions (handles CSRF, errors, loading state)
327
+ - Use `router.reload({ only: [...] })` for partial page updates
328
+ - Use `<Link>` instead of `<a>` for SPA navigation
329
+ - Access shared props via `usePage().props`
330
+ - `processing` boolean from `useForm` for button loading states
331
+
332
+ ## Forbidden Patterns
333
+
334
+ | Pattern | Reason | Use Instead |
335
+ |---------|--------|-------------|
336
+ | `fetch()` / `axios` for page data | Bypasses Inertia | `Inertia::render()` with props |
337
+ | `__()` inside JSX | React Hook violation | CONST at top of component |
338
+ | Inline SVGs in JSX | Bloats components | SVG files with `?react` import |
339
+ | `<a href>` for internal links | Full page reload | `<Link href>` |
340
+ | `window.location` for navigation | Full page reload | `router.visit()` |
341
+ | `Inertia::render()` after POST | Breaks Inertia protocol | `redirect()->route()` |
342
+ | Loading all translations globally | Performance waste | On-demand per page route |