start-vibing-stacks 1.8.1 โ†’ 1.9.1

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/index.js CHANGED
@@ -45,11 +45,9 @@ if (FLAGS.help) {
45
45
  --version, -v Show version
46
46
 
47
47
  ${chalk.bold('Supported Stacks:')}
48
- ๐Ÿ˜ PHP 8.3+ Laravel, Symfony, CodeIgniter, Vanilla
48
+ ๐Ÿ˜ PHP 8.3+ Laravel 12 + Octane + Inertia.js (React)
49
49
  ๐Ÿ“ฆ Node.js/TS Next.js, Nuxt, Express, Fastify
50
- ๐Ÿ Python Django, FastAPI, Flask (coming soon)
51
- ๐Ÿฆ€ Rust (coming soon)
52
- ๐Ÿน Go (coming soon)
50
+ ๐Ÿ Python 3.12+ FastAPI, Django, Flask
53
51
  `);
54
52
  process.exit(0);
55
53
  }
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 = '1.8.1';
5
+ const VERSION = '1.9.1';
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('');
@@ -12,7 +12,7 @@ ${chalk.hex('#9F7AEA')(' โ•”โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•โ•
12
12
  ${chalk.hex('#9F7AEA')(' โ•‘')} ${chalk.hex('#9F7AEA')('โ•‘')}
13
13
  ${chalk.hex('#9F7AEA')(' โ•‘')} ${chalk.hex('#FF6B6B').bold('โšก')} ${chalk.bold.white('S T A R T')} ${chalk.bold.white('V I B I N G')} ${chalk.bold.white('S T A C K S')} ${chalk.hex('#FF6B6B').bold('โšก')} ${chalk.hex('#9F7AEA')('โ•‘')}
14
14
  ${chalk.hex('#9F7AEA')(' โ•‘')} ${chalk.hex('#9F7AEA')('โ•‘')}
15
- ${chalk.hex('#9F7AEA')(' โ•‘')} ${chalk.hex('#FF8E53')('๐Ÿ˜ PHP')} ${chalk.hex('#48BB78')('๐Ÿ“ฆ Node')} ${chalk.hex('#4299E1')('๐Ÿ Python')} ${chalk.hex('#FF6B6B')('๐Ÿฆ€ Rust')} ${chalk.hex('#FFBD2E')('๐Ÿน Go')} ${chalk.hex('#9F7AEA')('โ•‘')}
15
+ ${chalk.hex('#9F7AEA')(' โ•‘')} ${chalk.hex('#FF8E53')('๐Ÿ˜ PHP')} ${chalk.hex('#48BB78')('๐Ÿ“ฆ Node.js')} ${chalk.hex('#4299E1')('๐Ÿ Python')} ${chalk.hex('#9F7AEA')('โ•‘')}
16
16
  ${chalk.hex('#9F7AEA')(' โ•‘')} ${chalk.hex('#9F7AEA')('โ•‘')}
17
17
  ${chalk.hex('#9F7AEA')(' โ•‘')} ${chalk.dim('AI-powered dev workflow ยท 33 skills ยท v' + VERSION)} ${chalk.hex('#9F7AEA')('โ•‘')}
18
18
  ${chalk.hex('#9F7AEA')(' โ•‘')} ${chalk.hex('#9F7AEA')('โ•‘')}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "start-vibing-stacks",
3
- "version": "1.8.1",
3
+ "version": "1.9.1",
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": {
@@ -0,0 +1,198 @@
1
+ # Inertia.js + React โ€” Laravel Frontend
2
+
3
+ **ALWAYS invoke when writing Inertia.js pages, components, or shared data.**
4
+
5
+ ## How Inertia Works
6
+
7
+ ```
8
+ Browser โ†โ†’ Inertia.js โ†โ†’ Laravel Controller
9
+ (no API needed)
10
+
11
+ - First request: full HTML (SSR or SPA)
12
+ - Subsequent: XHR with JSON props โ†’ React re-renders
13
+ - No API routes needed โ€” controllers return Inertia::render()
14
+ ```
15
+
16
+ ## Controller Pattern
17
+
18
+ ```php
19
+ use Inertia\Inertia;
20
+
21
+ class UserController extends Controller
22
+ {
23
+ public function index(): \Inertia\Response
24
+ {
25
+ return Inertia::render('Users/Index', [
26
+ 'users' => User::query()
27
+ ->select('id', 'name', 'email', 'created_at')
28
+ ->orderByDesc('created_at')
29
+ ->paginate(20),
30
+ 'filters' => request()->only(['search', 'role']),
31
+ ]);
32
+ }
33
+
34
+ public function store(StoreUserRequest $request): \Illuminate\Http\RedirectResponse
35
+ {
36
+ User::create($request->validated());
37
+ return redirect()->route('users.index')
38
+ ->with('success', 'User created.');
39
+ }
40
+ }
41
+ ```
42
+
43
+ ## React Page Component
44
+
45
+ ```tsx
46
+ // resources/js/Pages/Users/Index.tsx
47
+ import { Head, Link, router } from '@inertiajs/react';
48
+ import { PageProps, User, PaginatedData } from '@/types';
49
+
50
+ interface Props extends PageProps {
51
+ users: PaginatedData<User>;
52
+ filters: { search?: string; role?: string };
53
+ }
54
+
55
+ export default function UsersIndex({ users, filters }: Props) {
56
+ return (
57
+ <>
58
+ <Head title="Users" />
59
+
60
+ <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
61
+ {/* Search */}
62
+ <input
63
+ defaultValue={filters.search}
64
+ onChange={(e) => router.get('/users', { search: e.target.value }, {
65
+ preserveState: true,
66
+ replace: true,
67
+ })}
68
+ placeholder="Search..."
69
+ className="border rounded-lg px-4 py-2"
70
+ />
71
+
72
+ {/* List */}
73
+ {users.data.map((user) => (
74
+ <div key={user.id} className="p-4 border-b">
75
+ <Link href={`/users/${user.id}`} className="text-blue-600 hover:underline">
76
+ {user.name}
77
+ </Link>
78
+ </div>
79
+ ))}
80
+
81
+ {/* Pagination */}
82
+ {users.links.map((link, i) => (
83
+ <Link key={i} href={link.url ?? ''} className={link.active ? 'font-bold' : ''}>
84
+ <span dangerouslySetInnerHTML={{ __html: link.label }} />
85
+ </Link>
86
+ ))}
87
+ </div>
88
+ </>
89
+ );
90
+ }
91
+ ```
92
+
93
+ ## Shared Data (Layout Props)
94
+
95
+ ```php
96
+ // app/Http/Middleware/HandleInertiaRequests.php
97
+ public function share(Request $request): array
98
+ {
99
+ return [
100
+ ...parent::share($request),
101
+ 'auth' => [
102
+ 'user' => $request->user()?->only('id', 'name', 'email', 'avatar'),
103
+ ],
104
+ 'flash' => [
105
+ 'success' => session('success'),
106
+ 'error' => session('error'),
107
+ ],
108
+ ];
109
+ }
110
+ ```
111
+
112
+ ```tsx
113
+ // Access in any component
114
+ import { usePage } from '@inertiajs/react';
115
+
116
+ const { auth, flash } = usePage().props;
117
+ ```
118
+
119
+ ## Forms (useForm hook)
120
+
121
+ ```tsx
122
+ import { useForm } from '@inertiajs/react';
123
+
124
+ export default function CreateUser() {
125
+ const { data, setData, post, processing, errors } = useForm({
126
+ name: '',
127
+ email: '',
128
+ password: '',
129
+ });
130
+
131
+ const submit = (e: React.FormEvent) => {
132
+ e.preventDefault();
133
+ post('/users');
134
+ };
135
+
136
+ return (
137
+ <form onSubmit={submit}>
138
+ <input value={data.name} onChange={e => setData('name', e.target.value)} />
139
+ {errors.name && <span className="text-red-500 text-sm">{errors.name}</span>}
140
+
141
+ <input value={data.email} onChange={e => setData('email', e.target.value)} />
142
+ {errors.email && <span className="text-red-500 text-sm">{errors.email}</span>}
143
+
144
+ <button type="submit" disabled={processing} className="bg-blue-600 text-white px-4 py-2 rounded-lg disabled:opacity-50">
145
+ {processing ? 'Saving...' : 'Create'}
146
+ </button>
147
+ </form>
148
+ );
149
+ }
150
+ ```
151
+
152
+ ## TypeScript Types
153
+
154
+ ```tsx
155
+ // resources/js/types/index.d.ts
156
+ export interface PageProps {
157
+ auth: { user: User | null };
158
+ flash: { success?: string; error?: string };
159
+ }
160
+
161
+ export interface User {
162
+ id: string;
163
+ name: string;
164
+ email: string;
165
+ avatar?: string;
166
+ created_at: string;
167
+ }
168
+
169
+ export interface PaginatedData<T> {
170
+ data: T[];
171
+ links: { url: string | null; label: string; active: boolean }[];
172
+ current_page: number;
173
+ last_page: number;
174
+ per_page: number;
175
+ total: number;
176
+ }
177
+ ```
178
+
179
+ ## TailwindCSS 4 Setup
180
+
181
+ ```css
182
+ /* resources/css/app.css */
183
+ @import "tailwindcss";
184
+
185
+ @theme {
186
+ --color-primary: #3b82f6;
187
+ --color-primary-foreground: #ffffff;
188
+ --font-sans: 'Inter', sans-serif;
189
+ }
190
+ ```
191
+
192
+ ## FORBIDDEN
193
+
194
+ 1. **API routes for Inertia pages** โ€” use `Inertia::render()` in controllers
195
+ 2. **`window.location` for navigation** โ€” use `router.visit()` or `<Link>`
196
+ 3. **Fetching data in useEffect** โ€” pass as props from controller
197
+ 4. **Duplicating validation** โ€” validate in FormRequest, show errors from `useForm`
198
+ 5. **`any` types for page props** โ€” always type with `interface Props extends PageProps`
@@ -0,0 +1,385 @@
1
+ # MariaDB + Octane โ€” Database Patterns for Persistent Workers
2
+
3
+ **ALWAYS invoke when writing queries, migrations, models, or DB config in Laravel Octane.**
4
+
5
+ ## Why Octane Changes Everything
6
+
7
+ ```
8
+ Traditional PHP: Octane (RoadRunner):
9
+ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
10
+ โ”‚ Request โ†’ โ”‚ โ”‚ Worker boots ONCE โ”‚
11
+ โ”‚ Boot app โ”‚ โ”‚ โ†“ โ”‚
12
+ โ”‚ DB connectโ”‚ โ”‚ Request 1 โ†’ process โ”‚
13
+ โ”‚ Process โ”‚ โ”‚ Request 2 โ†’ process โ”‚ โ† SAME connection
14
+ โ”‚ DB close โ”‚ โ”‚ Request 3 โ†’ process โ”‚ โ† SAME state
15
+ โ”‚ Die โ”‚ โ”‚ ...500 requests... โ”‚
16
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ”‚ Worker restarts โ”‚
17
+ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜
18
+
19
+ Problems in Octane:
20
+ - Connection stays open โ†’ stale connections, gone away errors
21
+ - Transaction leaks โ†’ uncommitted TX bleeds to next request
22
+ - Query builder state โ†’ leftover bindings
23
+ - String truncation โ†’ silent data loss in strict mode OFF
24
+ ```
25
+
26
+ ## Connection Configuration
27
+
28
+ ```php
29
+ // config/database.php
30
+ 'mysql' => [
31
+ 'driver' => 'mysql',
32
+ 'host' => env('DB_HOST', '127.0.0.1'),
33
+ 'port' => env('DB_PORT', '3306'),
34
+ 'database' => env('DB_DATABASE'),
35
+ 'username' => env('DB_USERNAME'),
36
+ 'password' => env('DB_PASSWORD'),
37
+ 'charset' => 'utf8mb4',
38
+ 'collation' => 'utf8mb4_unicode_ci',
39
+ 'prefix' => '',
40
+ 'strict' => true, // โ† MANDATORY
41
+ 'engine' => 'InnoDB',
42
+ 'options' => [
43
+ PDO::ATTR_PERSISTENT => true, // โ† Octane reuses connections
44
+ PDO::ATTR_EMULATE_PREPARES => false, // โ† Real prepared statements
45
+ PDO::ATTR_STRINGIFY_FETCHES => false, // โ† Preserve int/float types
46
+ PDO::MYSQL_ATTR_FOUND_ROWS => true, // โ† Accurate affected rows
47
+ ],
48
+ 'modes' => [
49
+ 'STRICT_TRANS_TABLES', // โ† Errors instead of truncation
50
+ 'NO_ZERO_IN_DATE', // โ† No 0000-00-00 dates
51
+ 'NO_ZERO_DATE',
52
+ 'ERROR_FOR_DIVISION_BY_ZERO',
53
+ 'NO_ENGINE_SUBSTITUTION',
54
+ 'ONLY_FULL_GROUP_BY', // โ† Force explicit GROUP BY
55
+ ],
56
+ ],
57
+ ```
58
+
59
+ ### Why `strict => true` + SQL Modes
60
+
61
+ ```
62
+ strict: false (DEFAULT MariaDB):
63
+ INSERT INTO users (name) VALUES ('This is a very long name that exceeds the column limit')
64
+ โ†’ Silently TRUNCATES to column length! No error! DATA LOST!
65
+
66
+ strict: true (OUR STANDARD):
67
+ โ†’ ERROR 1406: Data too long for column 'name'
68
+ โ†’ You KNOW the problem immediately
69
+ ```
70
+
71
+ **Rule:** `strict => true` is NON-NEGOTIABLE. Every silent truncation is a bug waiting to explode.
72
+
73
+ ## Octane Connection Flush (MANDATORY)
74
+
75
+ ```php
76
+ // app/Providers/AppServiceProvider.php
77
+ use Laravel\Octane\Facades\Octane;
78
+
79
+ public function boot(): void
80
+ {
81
+ // Flush stale connections between requests
82
+ Octane::prepare(function ($sandbox) {
83
+ $sandbox->flushDatabaseConnections();
84
+ });
85
+
86
+ // Also flush on tick (long-running operations)
87
+ Octane::tick('db-health', function () {
88
+ try {
89
+ DB::connection()->getPdo();
90
+ } catch (\Exception $e) {
91
+ DB::reconnect();
92
+ }
93
+ })->seconds(30);
94
+ }
95
+ ```
96
+
97
+ ## Migration Patterns
98
+
99
+ ### Column Definitions โ€” Be Explicit
100
+
101
+ ```php
102
+ // โœ… ALWAYS define exact lengths โ€” no surprises with strict mode
103
+ Schema::create('leads', function (Blueprint $table) {
104
+ $table->uuid('id')->primary();
105
+
106
+ // Strings: ALWAYS define max length
107
+ $table->string('name', 255); // varchar(255)
108
+ $table->string('email', 320); // RFC 5321 max email
109
+ $table->string('phone', 20); // E.164 max
110
+ $table->string('status', 30); // enum-like but flexible
111
+ $table->string('country_code', 3); // ISO 3166-1 alpha-2/3
112
+ $table->string('currency', 3); // ISO 4217
113
+
114
+ // Use text() for unbounded content
115
+ $table->text('notes'); // 64KB
116
+ $table->mediumText('description'); // 16MB
117
+ $table->longText('raw_payload'); // 4GB
118
+
119
+ // Decimals: ALWAYS precision + scale
120
+ $table->decimal('price', 10, 2); // 99,999,999.99
121
+ $table->decimal('commission_rate', 5, 4); // 0.0000 to 9.9999
122
+ $table->unsignedBigInteger('impressions')->default(0);
123
+
124
+ // Dates
125
+ $table->timestamp('converted_at')->nullable();
126
+ $table->timestamps(); // created_at, updated_at
127
+ $table->softDeletes(); // deleted_at
128
+
129
+ // JSON (MariaDB 10.2+)
130
+ $table->json('metadata')->nullable();
131
+
132
+ // Indexes
133
+ $table->index('status');
134
+ $table->index(['status', 'created_at']); // Composite for common queries
135
+ $table->unique('email');
136
+ });
137
+ ```
138
+
139
+ ### Index Strategy for MariaDB
140
+
141
+ ```php
142
+ // โœ… Composite indexes โ€” leftmost prefix rule
143
+ $table->index(['user_id', 'status', 'created_at']);
144
+ // Covers: WHERE user_id = ?
145
+ // Covers: WHERE user_id = ? AND status = ?
146
+ // Covers: WHERE user_id = ? AND status = ? ORDER BY created_at
147
+ // Does NOT cover: WHERE status = ? (user_id not in query)
148
+
149
+ // โœ… Covering index for frequent queries
150
+ $table->index(['domain_id', 'status', 'created_at', 'id'], 'idx_leads_dashboard');
151
+
152
+ // โœ… Partial-like with generated column (MariaDB 10.2+)
153
+ // For: WHERE JSON_EXTRACT(metadata, '$.source') = 'google'
154
+ $table->string('metadata_source', 50)->virtualAs("JSON_UNQUOTE(JSON_EXTRACT(metadata, '$.source'))");
155
+ $table->index('metadata_source');
156
+ ```
157
+
158
+ ## Eloquent Patterns for Octane
159
+
160
+ ### Scoped Queries (avoid stale state)
161
+
162
+ ```php
163
+ class Lead extends Model
164
+ {
165
+ use HasUuids;
166
+
167
+ // โœ… Scopes โ€” reusable, composable
168
+ public function scopeActive(Builder $query): Builder
169
+ {
170
+ return $query->where('status', 'active');
171
+ }
172
+
173
+ public function scopeForUser(Builder $query, string $userId): Builder
174
+ {
175
+ return $query->where('user_id', $userId);
176
+ }
177
+
178
+ public function scopeCreatedBetween(Builder $query, Carbon $from, Carbon $to): Builder
179
+ {
180
+ return $query->whereBetween('created_at', [$from, $to]);
181
+ }
182
+ }
183
+
184
+ // Usage (always fresh query, no state leaks):
185
+ Lead::query()
186
+ ->active()
187
+ ->forUser($request->user()->id)
188
+ ->createdBetween(now()->subDays(30), now())
189
+ ->paginate(20);
190
+ ```
191
+
192
+ ### N+1 Prevention
193
+
194
+ ```php
195
+ // โœ… ALWAYS eager load relationships
196
+ class LeadController extends Controller
197
+ {
198
+ public function index(Request $request): JsonResponse
199
+ {
200
+ $leads = Lead::query()
201
+ ->with(['domain:id,name', 'user:id,name,email']) // Select only needed columns
202
+ ->select('id', 'name', 'email', 'status', 'domain_id', 'user_id', 'created_at')
203
+ ->forUser($request->user()->id)
204
+ ->latest()
205
+ ->paginate(20);
206
+
207
+ return LeadResource::collection($leads);
208
+ }
209
+ }
210
+
211
+ // โŒ NEVER in Octane (N+1 with persistent connections = compounding slowness)
212
+ foreach ($leads as $lead) {
213
+ echo $lead->domain->name; // N+1 query PER lead, PER request, ALL DAY
214
+ }
215
+ ```
216
+
217
+ ### Bulk Operations (Octane-safe)
218
+
219
+ ```php
220
+ // โœ… Chunked processing โ€” controls memory in long-lived worker
221
+ Lead::query()
222
+ ->where('status', 'pending')
223
+ ->chunkById(500, function ($leads) {
224
+ foreach ($leads as $lead) {
225
+ ProcessLeadJob::dispatch($lead);
226
+ }
227
+ });
228
+
229
+ // โœ… Bulk insert (single query)
230
+ Lead::insert(
231
+ collect($rows)->map(fn ($row) => [
232
+ 'id' => Str::uuid()->toString(),
233
+ 'name' => $row['name'],
234
+ 'email' => $row['email'],
235
+ 'created_at' => now(),
236
+ 'updated_at' => now(),
237
+ ])->toArray()
238
+ );
239
+
240
+ // โœ… Bulk update
241
+ Lead::query()
242
+ ->whereIn('id', $ids)
243
+ ->update(['status' => 'processed', 'processed_at' => now()]);
244
+
245
+ // โŒ NEVER in Octane (memory grows with each request)
246
+ $allLeads = Lead::all(); // Loads EVERYTHING into worker memory
247
+ ```
248
+
249
+ ### Transaction Safety in Octane
250
+
251
+ ```php
252
+ // โœ… Always explicit transactions with try/catch
253
+ // In Octane, an uncaught exception leaves the TX open for the NEXT request!
254
+ try {
255
+ DB::beginTransaction();
256
+
257
+ $lead = Lead::create($validated);
258
+ $lead->attempts()->create(['status' => 'new']);
259
+ ConversionService::fire($lead);
260
+
261
+ DB::commit();
262
+ } catch (\Throwable $e) {
263
+ DB::rollBack(); // โ† CRITICAL in Octane โ€” prevents TX leak
264
+ report($e);
265
+ throw $e;
266
+ }
267
+
268
+ // โœ… Or use the closure syntax (auto-rollback on exception)
269
+ $lead = DB::transaction(function () use ($validated) {
270
+ $lead = Lead::create($validated);
271
+ $lead->attempts()->create(['status' => 'new']);
272
+ return $lead;
273
+ }, attempts: 3); // Retry on deadlock
274
+ ```
275
+
276
+ ## Query Performance
277
+
278
+ ### Explain Before Deploy
279
+
280
+ ```php
281
+ // In Tinker or test:
282
+ DB::enableQueryLog();
283
+ $leads = Lead::query()->active()->forUser($userId)->paginate(20);
284
+ $queries = DB::getQueryLog();
285
+
286
+ // Check for full table scans:
287
+ // EXPLAIN SELECT * FROM leads WHERE user_id = ? AND status = 'active'
288
+ // Look for: type=ref or type=range (GOOD), type=ALL (BAD โ€” full scan)
289
+ ```
290
+
291
+ ### Pagination (NEVER offset for large tables)
292
+
293
+ ```php
294
+ // โœ… Cursor pagination (fast on large tables)
295
+ $leads = Lead::query()
296
+ ->where('user_id', $userId)
297
+ ->orderBy('id')
298
+ ->cursorPaginate(20);
299
+
300
+ // โœ… Standard pagination (OK for admin panels)
301
+ $leads = Lead::query()->paginate(20);
302
+
303
+ // โŒ NEVER manual offset on large tables
304
+ Lead::query()->offset(100000)->limit(20)->get();
305
+ // Full scan of 100,000 rows just to skip them!
306
+ ```
307
+
308
+ ### Count Optimization
309
+
310
+ ```php
311
+ // โŒ SLOW on large tables
312
+ $total = Lead::count(); // Full table scan
313
+
314
+ // โœ… Approximate count (MariaDB)
315
+ $total = DB::selectOne("SELECT TABLE_ROWS FROM information_schema.TABLES WHERE TABLE_NAME = 'leads'")->TABLE_ROWS;
316
+
317
+ // โœ… Exact count with cache
318
+ $total = Cache::remember("leads:count:{$userId}", 60, function () use ($userId) {
319
+ return Lead::where('user_id', $userId)->count();
320
+ });
321
+ ```
322
+
323
+ ## Model Casts (Strict Types)
324
+
325
+ ```php
326
+ class Lead extends Model
327
+ {
328
+ protected $casts = [
329
+ 'metadata' => 'array', // JSON โ†’ array (auto encode/decode)
330
+ 'price' => 'decimal:2', // Always 2 decimal places
331
+ 'is_active' => 'boolean', // 1/0 โ†’ true/false
332
+ 'converted_at' => 'immutable_datetime', // Carbon Immutable (Octane-safe)
333
+ 'impressions' => 'integer', // String โ†’ int
334
+ 'status' => LeadStatus::class, // Backed enum
335
+ ];
336
+ }
337
+
338
+ // โœ… Backed enums for status columns
339
+ enum LeadStatus: string
340
+ {
341
+ case Pending = 'pending';
342
+ case Active = 'active';
343
+ case Converted = 'converted';
344
+ case Rejected = 'rejected';
345
+ }
346
+ ```
347
+
348
+ ## Health Check
349
+
350
+ ```php
351
+ // routes/web.php โ€” Octane worker health
352
+ Route::get('/health', function () {
353
+ try {
354
+ DB::connection()->getPdo();
355
+ $dbOk = true;
356
+ } catch (\Exception $e) {
357
+ DB::reconnect();
358
+ $dbOk = false;
359
+ }
360
+
361
+ return response()->json([
362
+ 'status' => $dbOk ? 'healthy' : 'degraded',
363
+ 'db' => $dbOk,
364
+ 'worker_memory' => memory_get_usage(true) / 1024 / 1024 . ' MB',
365
+ 'worker_peak' => memory_get_peak_usage(true) / 1024 / 1024 . ' MB',
366
+ ], $dbOk ? 200 : 503);
367
+ });
368
+ ```
369
+
370
+ ## FORBIDDEN
371
+
372
+ | โŒ Don't | โœ… Do |
373
+ |---|---|
374
+ | `strict => false` | `strict => true` + SQL modes |
375
+ | `$table->string('name')` (no length) | `$table->string('name', 255)` |
376
+ | `Lead::all()` in Octane | `Lead::query()->paginate()` or `chunkById()` |
377
+ | Uncaught exception in transaction | `DB::transaction()` closure or explicit rollBack |
378
+ | `$_GET`, `$_POST` | `$request->input()` |
379
+ | `static $cache = []` | Instance property or Redis cache |
380
+ | `offset()` on large tables | `cursorPaginate()` |
381
+ | `migrate:fresh`, `db:wipe` | Incremental migrations only |
382
+ | No index on WHERE/ORDER columns | Composite indexes matching query patterns |
383
+ | `Lead::count()` on millions of rows | Cached count or approximate count |
384
+ | `'metadata' => 'json'` cast | `'metadata' => 'array'` (auto decode) |
385
+ | `datetime` cast | `immutable_datetime` (Octane-safe, no mutation) |
@@ -1,159 +1,66 @@
1
1
  {
2
2
  "id": "php",
3
3
  "name": "PHP",
4
- "icon": "\ud83d\udc18",
4
+ "icon": "๐Ÿ˜",
5
5
  "runtime": "PHP 8.3+",
6
6
  "minVersion": "8.3.0",
7
7
  "packageManager": "composer",
8
- "extensions": [
9
- ".php",
10
- ".blade.php",
11
- ".twig",
12
- ".phtml"
13
- ],
14
- "testExtensions": [
15
- "*Test.php",
16
- "*_test.php"
17
- ],
18
- "detectFiles": [
19
- "composer.json",
20
- "index.php",
21
- "artisan",
22
- "public/index.php"
23
- ],
8
+ "extensions": [".php", ".blade.php"],
9
+ "testExtensions": ["*Test.php", "*_test.php"],
10
+ "detectFiles": ["composer.json", "artisan", "public/index.php"],
24
11
  "commands": {
25
12
  "test": "vendor/bin/phpunit",
26
13
  "lint": "vendor/bin/phpstan analyse --level=6",
27
14
  "format": "vendor/bin/php-cs-fixer fix",
28
- "serve": "php -S localhost:8000 -t public/",
29
- "build": null
15
+ "serve": "php artisan octane:start --watch",
16
+ "build": "npm run build"
30
17
  },
31
18
  "qualityGates": [
32
- {
33
- "name": "PHPStan",
34
- "command": "vendor/bin/phpstan analyse --level=6",
35
- "required": true,
36
- "order": 1
37
- },
38
- {
39
- "name": "PHPUnit",
40
- "command": "vendor/bin/phpunit",
41
- "required": true,
42
- "order": 2
43
- },
44
- {
45
- "name": "CS Fixer",
46
- "command": "vendor/bin/php-cs-fixer fix --dry-run --diff",
47
- "required": false,
48
- "order": 3
49
- }
19
+ { "name": "PHPStan", "command": "vendor/bin/phpstan analyse --level=6", "required": true, "order": 1 },
20
+ { "name": "PHPUnit", "command": "vendor/bin/phpunit", "required": true, "order": 2 },
21
+ { "name": "TypeCheck", "command": "npx tsc --noEmit", "required": true, "order": 3 },
22
+ { "name": "Lint", "command": "npx eslint resources/js/", "required": false, "order": 4 }
50
23
  ],
51
24
  "frameworks": [
52
- {
53
- "id": "laravel",
54
- "name": "Laravel",
55
- "icon": "\ud83c\udfd7\ufe0f",
56
- "detectFiles": [
57
- "artisan",
58
- "bootstrap/app.php"
59
- ]
60
- },
61
25
  {
62
26
  "id": "laravel-octane",
63
- "name": "Laravel + Octane (RoadRunner)",
64
- "icon": "\ud83d\ude80",
65
- "detectFiles": [
66
- "artisan",
67
- "rr.yaml"
68
- ]
69
- },
70
- {
71
- "id": "symfony",
72
- "name": "Symfony",
73
- "icon": "\ud83c\udfb5",
74
- "detectFiles": [
75
- "symfony.lock"
76
- ]
27
+ "name": "Laravel 12 + Octane (RoadRunner) + Inertia.js",
28
+ "icon": "๐Ÿš€",
29
+ "detectFiles": ["artisan", "rr.yaml"],
30
+ "default": true
77
31
  },
78
32
  {
79
- "id": "codeigniter",
80
- "name": "CodeIgniter",
81
- "icon": "\ud83e\udde9",
82
- "detectFiles": [
83
- "spark"
84
- ]
85
- },
86
- {
87
- "id": "vanilla",
88
- "name": "Vanilla PHP",
89
- "icon": "\ud83d\udcc4"
33
+ "id": "laravel",
34
+ "name": "Laravel 12 (standard)",
35
+ "icon": "๐Ÿ—๏ธ",
36
+ "detectFiles": ["artisan", "bootstrap/app.php"]
90
37
  }
91
38
  ],
92
39
  "databases": [
93
- {
94
- "id": "mysql",
95
- "name": "MySQL / MariaDB",
96
- "icon": "\ud83d\udc2c"
97
- },
98
- {
99
- "id": "postgresql",
100
- "name": "PostgreSQL",
101
- "icon": "\ud83d\udc18"
102
- },
103
- {
104
- "id": "mongodb",
105
- "name": "MongoDB",
106
- "icon": "\ud83c\udf43"
107
- },
108
- {
109
- "id": "sqlite",
110
- "name": "SQLite",
111
- "icon": "\ud83d\udcc1"
112
- },
113
- {
114
- "id": "none",
115
- "name": "None",
116
- "icon": "\u274c"
117
- }
40
+ { "id": "mysql", "name": "MySQL / MariaDB", "icon": "๐Ÿฌ" },
41
+ { "id": "postgresql", "name": "PostgreSQL", "icon": "๐Ÿ˜" },
42
+ { "id": "sqlite", "name": "SQLite", "icon": "๐Ÿ“" }
118
43
  ],
119
44
  "frontendOptions": [
120
45
  {
121
- "id": "react-tailwind",
122
- "name": "ReactJS 19+ / TailwindCSS 4+",
123
- "icon": "\u269b\ufe0f"
124
- },
125
- {
126
- "id": "tailwind-vanilla",
127
- "name": "TailwindCSS 4+ / Vanilla JS",
128
- "icon": "\ud83c\udfa8"
46
+ "id": "react-inertia",
47
+ "name": "ReactJS 19 + Inertia.js + TailwindCSS 4",
48
+ "icon": "โš›๏ธ",
49
+ "default": true
129
50
  },
130
51
  {
131
52
  "id": "blade",
132
- "name": "Blade Templates (Laravel)",
133
- "icon": "\ud83d\uddbc\ufe0f"
134
- },
135
- {
136
- "id": "livewire",
137
- "name": "Livewire + Alpine.js",
138
- "icon": "\u26a1"
139
- },
140
- {
141
- "id": "twig",
142
- "name": "Twig Templates (Symfony)",
143
- "icon": "\ud83c\udf3f"
53
+ "name": "Blade + TailwindCSS 4",
54
+ "icon": "๐Ÿ–ผ๏ธ"
144
55
  },
145
56
  {
146
57
  "id": "none",
147
- "name": "API only \u2014 no frontend",
148
- "icon": "\u274c"
58
+ "name": "API only โ€” no frontend",
59
+ "icon": "๐Ÿ”Œ"
149
60
  }
150
61
  ],
151
62
  "deployTargets": [
152
- {
153
- "id": "github",
154
- "name": "GitHub (git push)",
155
- "icon": "\ud83d\udc19"
156
- }
63
+ { "id": "github", "name": "GitHub (git push)", "icon": "๐Ÿ™" }
157
64
  ],
158
65
  "skills": [
159
66
  "php-patterns",
@@ -192,10 +99,10 @@
192
99
  "name": "Node.js",
193
100
  "command": "node",
194
101
  "versionFlag": "--version",
195
- "minVersion": "18.0.0",
102
+ "minVersion": "20.0.0",
196
103
  "installCommand": {
197
104
  "macos": "brew install node",
198
- "linux": "curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash - && sudo apt install -y nodejs"
105
+ "linux": "curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - && sudo apt install -y nodejs"
199
106
  },
200
107
  "versionRegex": "v?(\\d+\\.\\d+\\.\\d+)"
201
108
  }