@codebakers/cli 3.7.2 → 3.8.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.
@@ -0,0 +1,823 @@
1
+ /**
2
+ * ENGINEERING STATE PERSISTENCE
3
+ *
4
+ * Manages the .codebakers/ folder structure for local state persistence.
5
+ * This keeps the project state in sync between local and server.
6
+ *
7
+ * Folder structure:
8
+ * .codebakers/
9
+ * project.json - Main project configuration and scope
10
+ * state.json - Current build state (phase, progress, etc.)
11
+ * graph.json - Dependency graph
12
+ * decisions/ - Agent decision log
13
+ * 001-scoping.json
14
+ * 002-architecture.json
15
+ * artifacts/ - Generated documents
16
+ * prd.md
17
+ * tech-spec.md
18
+ * api-docs.md
19
+ * messages/ - Agent communication log
20
+ * session-xxx.json
21
+ * snapshots/ - Rollback points
22
+ * snap-001/
23
+ */
24
+
25
+ import { existsSync, mkdirSync, readFileSync, writeFileSync, readdirSync } from 'fs';
26
+ import { join } from 'path';
27
+ import { createHash } from 'crypto';
28
+
29
+ // =============================================================================
30
+ // TYPES
31
+ // =============================================================================
32
+
33
+ export interface ProjectConfig {
34
+ // Identity
35
+ id: string;
36
+ name: string;
37
+ description: string;
38
+ projectHash: string;
39
+
40
+ // Scope
41
+ scope: ProjectScope;
42
+
43
+ // Stack (locked after first detection)
44
+ stack: StackConfig;
45
+
46
+ // Timestamps
47
+ createdAt: string;
48
+ updatedAt: string;
49
+ }
50
+
51
+ export interface ProjectScope {
52
+ targetAudience: 'consumers' | 'businesses' | 'internal' | 'developers';
53
+ isFullBusiness: boolean;
54
+ needsMarketing: boolean;
55
+ needsAnalytics: boolean;
56
+ needsTeamFeatures: boolean;
57
+ needsAdminDashboard: boolean;
58
+ platforms: ('web' | 'mobile' | 'api')[];
59
+ hasRealtime: boolean;
60
+ hasPayments: boolean;
61
+ hasAuth: boolean;
62
+ hasFileUploads: boolean;
63
+ compliance: {
64
+ hipaa: boolean;
65
+ pci: boolean;
66
+ gdpr: boolean;
67
+ soc2: boolean;
68
+ coppa: boolean;
69
+ };
70
+ expectedUsers: 'small' | 'medium' | 'large' | 'enterprise';
71
+ launchTimeline: 'asap' | 'weeks' | 'months' | 'flexible';
72
+ }
73
+
74
+ export interface StackConfig {
75
+ framework: string;
76
+ database: string;
77
+ orm: string;
78
+ auth: string;
79
+ ui: string;
80
+ payments?: string;
81
+ }
82
+
83
+ export interface BuildState {
84
+ // Current status
85
+ sessionId: string | null;
86
+ currentPhase: EngineeringPhase;
87
+ currentAgent: AgentRole;
88
+ isRunning: boolean;
89
+
90
+ // Gate status for each phase
91
+ gates: Record<EngineeringPhase, GateStatus>;
92
+
93
+ // Progress
94
+ overallProgress: number;
95
+ lastActivity: string; // ISO timestamp
96
+
97
+ // Pending items
98
+ pendingApprovals: string[];
99
+ blockers: string[];
100
+ }
101
+
102
+ export type EngineeringPhase =
103
+ | 'scoping'
104
+ | 'requirements'
105
+ | 'architecture'
106
+ | 'design_review'
107
+ | 'implementation'
108
+ | 'code_review'
109
+ | 'testing'
110
+ | 'security_review'
111
+ | 'documentation'
112
+ | 'staging'
113
+ | 'launch';
114
+
115
+ export type AgentRole =
116
+ | 'orchestrator'
117
+ | 'pm'
118
+ | 'architect'
119
+ | 'engineer'
120
+ | 'qa'
121
+ | 'security'
122
+ | 'documentation'
123
+ | 'devops';
124
+
125
+ export interface GateStatus {
126
+ status: 'pending' | 'in_progress' | 'passed' | 'failed' | 'skipped';
127
+ passedAt?: string;
128
+ failedReason?: string;
129
+ approvedBy?: string;
130
+ artifacts?: string[];
131
+ }
132
+
133
+ export interface DependencyNode {
134
+ id: string;
135
+ type: 'schema' | 'api' | 'component' | 'service' | 'page' | 'util' | 'config';
136
+ name: string;
137
+ filePath: string;
138
+ createdAt: string;
139
+ modifiedAt: string;
140
+ createdByFeature?: string;
141
+ }
142
+
143
+ export interface DependencyEdge {
144
+ id: string;
145
+ sourceId: string;
146
+ targetId: string;
147
+ type: 'import' | 'api-call' | 'db-query' | 'event' | 'config';
148
+ }
149
+
150
+ export interface DependencyGraph {
151
+ nodes: DependencyNode[];
152
+ edges: DependencyEdge[];
153
+ }
154
+
155
+ export interface AgentDecision {
156
+ id: string;
157
+ timestamp: string;
158
+ agent: AgentRole;
159
+ phase: EngineeringPhase;
160
+ decision: string;
161
+ reasoning: string;
162
+ alternatives: string[];
163
+ confidence: number;
164
+ reversible: boolean;
165
+ impact: 'low' | 'medium' | 'high' | 'critical';
166
+ }
167
+
168
+ export interface AgentMessage {
169
+ id: string;
170
+ timestamp: string;
171
+ fromAgent: AgentRole | 'user';
172
+ toAgent: AgentRole | 'user' | 'all';
173
+ messageType: 'request' | 'response' | 'review' | 'approval' | 'rejection' | 'question' | 'update' | 'handoff';
174
+ content: string;
175
+ metadata?: Record<string, unknown>;
176
+ }
177
+
178
+ // =============================================================================
179
+ // STATE MANAGER
180
+ // =============================================================================
181
+
182
+ export class EngineeringStateManager {
183
+ private cwd: string;
184
+ private stateDir: string;
185
+
186
+ constructor(cwd: string = process.cwd()) {
187
+ this.cwd = cwd;
188
+ this.stateDir = join(cwd, '.codebakers');
189
+ }
190
+
191
+ // ========================================
192
+ // INITIALIZATION
193
+ // ========================================
194
+
195
+ /**
196
+ * Initialize the .codebakers folder structure
197
+ */
198
+ init(): void {
199
+ // Create main directory
200
+ if (!existsSync(this.stateDir)) {
201
+ mkdirSync(this.stateDir, { recursive: true });
202
+ }
203
+
204
+ // Create subdirectories
205
+ const subdirs = ['decisions', 'artifacts', 'messages', 'snapshots'];
206
+ for (const subdir of subdirs) {
207
+ const path = join(this.stateDir, subdir);
208
+ if (!existsSync(path)) {
209
+ mkdirSync(path, { recursive: true });
210
+ }
211
+ }
212
+ }
213
+
214
+ /**
215
+ * Check if project is initialized
216
+ */
217
+ isInitialized(): boolean {
218
+ return existsSync(join(this.stateDir, 'project.json'));
219
+ }
220
+
221
+ /**
222
+ * Get project hash from current directory
223
+ */
224
+ getProjectHash(): string {
225
+ // Try to get git remote first
226
+ try {
227
+ const gitDir = join(this.cwd, '.git');
228
+ if (existsSync(gitDir)) {
229
+ const configPath = join(gitDir, 'config');
230
+ if (existsSync(configPath)) {
231
+ const config = readFileSync(configPath, 'utf-8');
232
+ const remoteMatch = config.match(/url = (.+)/);
233
+ if (remoteMatch) {
234
+ return createHash('sha256').update(remoteMatch[1]).digest('hex').slice(0, 16);
235
+ }
236
+ }
237
+ }
238
+ } catch {
239
+ // Ignore git errors
240
+ }
241
+
242
+ // Fall back to directory path hash
243
+ return createHash('sha256').update(this.cwd).digest('hex').slice(0, 16);
244
+ }
245
+
246
+ // ========================================
247
+ // PROJECT CONFIG
248
+ // ========================================
249
+
250
+ /**
251
+ * Create a new project configuration
252
+ */
253
+ createProject(name: string, description: string, scope: Partial<ProjectScope> = {}): ProjectConfig {
254
+ this.init();
255
+
256
+ const defaultScope: ProjectScope = {
257
+ targetAudience: 'consumers',
258
+ isFullBusiness: false,
259
+ needsMarketing: false,
260
+ needsAnalytics: false,
261
+ needsTeamFeatures: false,
262
+ needsAdminDashboard: false,
263
+ platforms: ['web'],
264
+ hasRealtime: false,
265
+ hasPayments: false,
266
+ hasAuth: true,
267
+ hasFileUploads: false,
268
+ compliance: {
269
+ hipaa: false,
270
+ pci: false,
271
+ gdpr: false,
272
+ soc2: false,
273
+ coppa: false,
274
+ },
275
+ expectedUsers: 'small',
276
+ launchTimeline: 'flexible',
277
+ };
278
+
279
+ const project: ProjectConfig = {
280
+ id: createHash('sha256').update(Date.now().toString() + Math.random()).digest('hex').slice(0, 16),
281
+ name,
282
+ description,
283
+ projectHash: this.getProjectHash(),
284
+ scope: { ...defaultScope, ...scope },
285
+ stack: this.detectStack(),
286
+ createdAt: new Date().toISOString(),
287
+ updatedAt: new Date().toISOString(),
288
+ };
289
+
290
+ this.saveProject(project);
291
+ this.initializeState();
292
+
293
+ return project;
294
+ }
295
+
296
+ /**
297
+ * Get project configuration
298
+ */
299
+ getProject(): ProjectConfig | null {
300
+ const path = join(this.stateDir, 'project.json');
301
+ if (!existsSync(path)) return null;
302
+
303
+ try {
304
+ return JSON.parse(readFileSync(path, 'utf-8'));
305
+ } catch {
306
+ return null;
307
+ }
308
+ }
309
+
310
+ /**
311
+ * Save project configuration
312
+ */
313
+ saveProject(project: ProjectConfig): void {
314
+ project.updatedAt = new Date().toISOString();
315
+ writeFileSync(
316
+ join(this.stateDir, 'project.json'),
317
+ JSON.stringify(project, null, 2)
318
+ );
319
+ }
320
+
321
+ /**
322
+ * Update project scope
323
+ */
324
+ updateScope(scope: Partial<ProjectScope>): ProjectConfig | null {
325
+ const project = this.getProject();
326
+ if (!project) return null;
327
+
328
+ project.scope = { ...project.scope, ...scope };
329
+ this.saveProject(project);
330
+
331
+ return project;
332
+ }
333
+
334
+ // ========================================
335
+ // BUILD STATE
336
+ // ========================================
337
+
338
+ /**
339
+ * Initialize build state
340
+ */
341
+ private initializeState(): void {
342
+ const initialGates: Record<EngineeringPhase, GateStatus> = {
343
+ scoping: { status: 'pending' },
344
+ requirements: { status: 'pending' },
345
+ architecture: { status: 'pending' },
346
+ design_review: { status: 'pending' },
347
+ implementation: { status: 'pending' },
348
+ code_review: { status: 'pending' },
349
+ testing: { status: 'pending' },
350
+ security_review: { status: 'pending' },
351
+ documentation: { status: 'pending' },
352
+ staging: { status: 'pending' },
353
+ launch: { status: 'pending' },
354
+ };
355
+
356
+ const state: BuildState = {
357
+ sessionId: null,
358
+ currentPhase: 'scoping',
359
+ currentAgent: 'orchestrator',
360
+ isRunning: false,
361
+ gates: initialGates,
362
+ overallProgress: 0,
363
+ lastActivity: new Date().toISOString(),
364
+ pendingApprovals: [],
365
+ blockers: [],
366
+ };
367
+
368
+ this.saveState(state);
369
+ }
370
+
371
+ /**
372
+ * Get build state
373
+ */
374
+ getState(): BuildState | null {
375
+ const path = join(this.stateDir, 'state.json');
376
+ if (!existsSync(path)) return null;
377
+
378
+ try {
379
+ return JSON.parse(readFileSync(path, 'utf-8'));
380
+ } catch {
381
+ return null;
382
+ }
383
+ }
384
+
385
+ /**
386
+ * Save build state
387
+ */
388
+ saveState(state: BuildState): void {
389
+ state.lastActivity = new Date().toISOString();
390
+ writeFileSync(
391
+ join(this.stateDir, 'state.json'),
392
+ JSON.stringify(state, null, 2)
393
+ );
394
+ }
395
+
396
+ /**
397
+ * Update current phase
398
+ */
399
+ setPhase(phase: EngineeringPhase, agent: AgentRole): void {
400
+ const state = this.getState();
401
+ if (!state) return;
402
+
403
+ state.currentPhase = phase;
404
+ state.currentAgent = agent;
405
+ state.gates[phase] = { status: 'in_progress' };
406
+
407
+ this.saveState(state);
408
+ }
409
+
410
+ /**
411
+ * Pass a gate
412
+ */
413
+ passGate(phase: EngineeringPhase, artifacts: string[] = [], approvedBy = 'auto'): void {
414
+ const state = this.getState();
415
+ if (!state) return;
416
+
417
+ state.gates[phase] = {
418
+ status: 'passed',
419
+ passedAt: new Date().toISOString(),
420
+ approvedBy,
421
+ artifacts,
422
+ };
423
+
424
+ // Calculate progress
425
+ const phases = Object.keys(state.gates) as EngineeringPhase[];
426
+ const passed = phases.filter(p => state.gates[p].status === 'passed').length;
427
+ state.overallProgress = Math.round((passed / phases.length) * 100);
428
+
429
+ this.saveState(state);
430
+ }
431
+
432
+ /**
433
+ * Fail a gate
434
+ */
435
+ failGate(phase: EngineeringPhase, reason: string): void {
436
+ const state = this.getState();
437
+ if (!state) return;
438
+
439
+ state.gates[phase] = {
440
+ status: 'failed',
441
+ failedReason: reason,
442
+ };
443
+
444
+ this.saveState(state);
445
+ }
446
+
447
+ // ========================================
448
+ // DEPENDENCY GRAPH
449
+ // ========================================
450
+
451
+ /**
452
+ * Get dependency graph
453
+ */
454
+ getGraph(): DependencyGraph {
455
+ const path = join(this.stateDir, 'graph.json');
456
+ if (!existsSync(path)) {
457
+ return { nodes: [], edges: [] };
458
+ }
459
+
460
+ try {
461
+ return JSON.parse(readFileSync(path, 'utf-8'));
462
+ } catch {
463
+ return { nodes: [], edges: [] };
464
+ }
465
+ }
466
+
467
+ /**
468
+ * Save dependency graph
469
+ */
470
+ saveGraph(graph: DependencyGraph): void {
471
+ writeFileSync(
472
+ join(this.stateDir, 'graph.json'),
473
+ JSON.stringify(graph, null, 2)
474
+ );
475
+ }
476
+
477
+ /**
478
+ * Add a node to the graph
479
+ */
480
+ addNode(node: Omit<DependencyNode, 'id' | 'createdAt' | 'modifiedAt'>): DependencyNode {
481
+ const graph = this.getGraph();
482
+
483
+ const newNode: DependencyNode = {
484
+ id: createHash('sha256').update(node.filePath + Date.now()).digest('hex').slice(0, 12),
485
+ ...node,
486
+ createdAt: new Date().toISOString(),
487
+ modifiedAt: new Date().toISOString(),
488
+ };
489
+
490
+ // Check if node with same path already exists
491
+ const existingIndex = graph.nodes.findIndex(n => n.filePath === node.filePath);
492
+ if (existingIndex >= 0) {
493
+ graph.nodes[existingIndex] = {
494
+ ...graph.nodes[existingIndex],
495
+ ...newNode,
496
+ id: graph.nodes[existingIndex].id, // Keep original ID
497
+ createdAt: graph.nodes[existingIndex].createdAt,
498
+ };
499
+ } else {
500
+ graph.nodes.push(newNode);
501
+ }
502
+
503
+ this.saveGraph(graph);
504
+ return newNode;
505
+ }
506
+
507
+ /**
508
+ * Add an edge to the graph
509
+ */
510
+ addEdge(edge: Omit<DependencyEdge, 'id'>): DependencyEdge {
511
+ const graph = this.getGraph();
512
+
513
+ // Check if edge already exists
514
+ const exists = graph.edges.some(
515
+ e => e.sourceId === edge.sourceId && e.targetId === edge.targetId && e.type === edge.type
516
+ );
517
+ if (exists) {
518
+ return graph.edges.find(
519
+ e => e.sourceId === edge.sourceId && e.targetId === edge.targetId && e.type === edge.type
520
+ )!;
521
+ }
522
+
523
+ const newEdge: DependencyEdge = {
524
+ id: createHash('sha256').update(edge.sourceId + edge.targetId + Date.now()).digest('hex').slice(0, 12),
525
+ ...edge,
526
+ };
527
+
528
+ graph.edges.push(newEdge);
529
+ this.saveGraph(graph);
530
+
531
+ return newEdge;
532
+ }
533
+
534
+ /**
535
+ * Find nodes affected by a change
536
+ */
537
+ findAffectedNodes(nodeId: string): { direct: DependencyNode[]; transitive: DependencyNode[] } {
538
+ const graph = this.getGraph();
539
+
540
+ // Find direct dependents (nodes that import this one)
541
+ const directEdges = graph.edges.filter(e => e.targetId === nodeId);
542
+ const direct = directEdges
543
+ .map(e => graph.nodes.find(n => n.id === e.sourceId))
544
+ .filter((n): n is DependencyNode => n !== undefined);
545
+
546
+ // Find transitive dependents (BFS)
547
+ const visited = new Set<string>([nodeId]);
548
+ const queue = [...direct.map(n => n.id)];
549
+ const transitive: DependencyNode[] = [];
550
+
551
+ while (queue.length > 0) {
552
+ const currentId = queue.shift()!;
553
+ if (visited.has(currentId)) continue;
554
+ visited.add(currentId);
555
+
556
+ const current = graph.nodes.find(n => n.id === currentId);
557
+ if (current && !direct.includes(current)) {
558
+ transitive.push(current);
559
+ }
560
+
561
+ // Add nodes that depend on current
562
+ const dependentEdges = graph.edges.filter(e => e.targetId === currentId);
563
+ for (const edge of dependentEdges) {
564
+ if (!visited.has(edge.sourceId)) {
565
+ queue.push(edge.sourceId);
566
+ }
567
+ }
568
+ }
569
+
570
+ return { direct, transitive };
571
+ }
572
+
573
+ // ========================================
574
+ // DECISIONS
575
+ // ========================================
576
+
577
+ /**
578
+ * Record a decision
579
+ */
580
+ recordDecision(decision: Omit<AgentDecision, 'id' | 'timestamp'>): AgentDecision {
581
+ const decisionsDir = join(this.stateDir, 'decisions');
582
+ const files = existsSync(decisionsDir) ? readdirSync(decisionsDir) : [];
583
+ const index = String(files.length + 1).padStart(3, '0');
584
+
585
+ const fullDecision: AgentDecision = {
586
+ id: createHash('sha256').update(Date.now().toString() + Math.random()).digest('hex').slice(0, 12),
587
+ timestamp: new Date().toISOString(),
588
+ ...decision,
589
+ };
590
+
591
+ const filename = `${index}-${decision.phase}.json`;
592
+ writeFileSync(
593
+ join(decisionsDir, filename),
594
+ JSON.stringify(fullDecision, null, 2)
595
+ );
596
+
597
+ return fullDecision;
598
+ }
599
+
600
+ /**
601
+ * Get all decisions
602
+ */
603
+ getDecisions(): AgentDecision[] {
604
+ const decisionsDir = join(this.stateDir, 'decisions');
605
+ if (!existsSync(decisionsDir)) return [];
606
+
607
+ const files = readdirSync(decisionsDir).filter(f => f.endsWith('.json'));
608
+ return files.map(f => {
609
+ try {
610
+ return JSON.parse(readFileSync(join(decisionsDir, f), 'utf-8'));
611
+ } catch {
612
+ return null;
613
+ }
614
+ }).filter((d): d is AgentDecision => d !== null);
615
+ }
616
+
617
+ // ========================================
618
+ // ARTIFACTS
619
+ // ========================================
620
+
621
+ /**
622
+ * Save an artifact (PRD, tech spec, etc.)
623
+ */
624
+ saveArtifact(name: string, content: string): void {
625
+ writeFileSync(
626
+ join(this.stateDir, 'artifacts', name),
627
+ content
628
+ );
629
+ }
630
+
631
+ /**
632
+ * Get an artifact
633
+ */
634
+ getArtifact(name: string): string | null {
635
+ const path = join(this.stateDir, 'artifacts', name);
636
+ if (!existsSync(path)) return null;
637
+
638
+ try {
639
+ return readFileSync(path, 'utf-8');
640
+ } catch {
641
+ return null;
642
+ }
643
+ }
644
+
645
+ /**
646
+ * List all artifacts
647
+ */
648
+ listArtifacts(): string[] {
649
+ const artifactsDir = join(this.stateDir, 'artifacts');
650
+ if (!existsSync(artifactsDir)) return [];
651
+
652
+ return readdirSync(artifactsDir);
653
+ }
654
+
655
+ // ========================================
656
+ // MESSAGES
657
+ // ========================================
658
+
659
+ /**
660
+ * Record a message
661
+ */
662
+ recordMessage(message: Omit<AgentMessage, 'id' | 'timestamp'>): AgentMessage {
663
+ const state = this.getState();
664
+ const sessionId = state?.sessionId || 'default';
665
+
666
+ const messagesPath = join(this.stateDir, 'messages', `${sessionId}.json`);
667
+ let messages: AgentMessage[] = [];
668
+
669
+ if (existsSync(messagesPath)) {
670
+ try {
671
+ messages = JSON.parse(readFileSync(messagesPath, 'utf-8'));
672
+ } catch {
673
+ messages = [];
674
+ }
675
+ }
676
+
677
+ const fullMessage: AgentMessage = {
678
+ id: createHash('sha256').update(Date.now().toString() + Math.random()).digest('hex').slice(0, 12),
679
+ timestamp: new Date().toISOString(),
680
+ ...message,
681
+ };
682
+
683
+ messages.push(fullMessage);
684
+ writeFileSync(messagesPath, JSON.stringify(messages, null, 2));
685
+
686
+ return fullMessage;
687
+ }
688
+
689
+ /**
690
+ * Get messages for current session
691
+ */
692
+ getMessages(): AgentMessage[] {
693
+ const state = this.getState();
694
+ const sessionId = state?.sessionId || 'default';
695
+
696
+ const messagesPath = join(this.stateDir, 'messages', `${sessionId}.json`);
697
+ if (!existsSync(messagesPath)) return [];
698
+
699
+ try {
700
+ return JSON.parse(readFileSync(messagesPath, 'utf-8'));
701
+ } catch {
702
+ return [];
703
+ }
704
+ }
705
+
706
+ // ========================================
707
+ // STACK DETECTION
708
+ // ========================================
709
+
710
+ /**
711
+ * Detect the tech stack from package.json
712
+ */
713
+ private detectStack(): StackConfig {
714
+ const stack: StackConfig = {
715
+ framework: 'nextjs',
716
+ database: 'supabase',
717
+ orm: 'drizzle',
718
+ auth: 'supabase',
719
+ ui: 'shadcn',
720
+ };
721
+
722
+ const packageJsonPath = join(this.cwd, 'package.json');
723
+ if (!existsSync(packageJsonPath)) return stack;
724
+
725
+ try {
726
+ const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
727
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies };
728
+
729
+ // Framework detection
730
+ if (deps['next']) stack.framework = 'nextjs';
731
+ else if (deps['@remix-run/react']) stack.framework = 'remix';
732
+ else if (deps['react']) stack.framework = 'react';
733
+ else if (deps['vue']) stack.framework = 'vue';
734
+ else if (deps['svelte']) stack.framework = 'svelte';
735
+
736
+ // ORM detection
737
+ if (deps['drizzle-orm']) stack.orm = 'drizzle';
738
+ else if (deps['prisma']) stack.orm = 'prisma';
739
+ else if (deps['typeorm']) stack.orm = 'typeorm';
740
+ else if (deps['mongoose']) stack.orm = 'mongoose';
741
+
742
+ // Database detection
743
+ if (deps['@supabase/supabase-js']) stack.database = 'supabase';
744
+ else if (deps['@planetscale/database']) stack.database = 'planetscale';
745
+ else if (deps['firebase']) stack.database = 'firebase';
746
+ else if (deps['pg']) stack.database = 'postgres';
747
+ else if (deps['mysql2']) stack.database = 'mysql';
748
+ else if (deps['mongodb']) stack.database = 'mongodb';
749
+
750
+ // Auth detection
751
+ if (deps['@supabase/auth-helpers-nextjs'] || deps['@supabase/supabase-js']) stack.auth = 'supabase';
752
+ else if (deps['@clerk/nextjs']) stack.auth = 'clerk';
753
+ else if (deps['next-auth']) stack.auth = 'next-auth';
754
+ else if (deps['@auth/core']) stack.auth = 'authjs';
755
+ else if (deps['firebase']) stack.auth = 'firebase';
756
+
757
+ // UI detection
758
+ if (deps['@radix-ui/react-slot'] || existsSync(join(this.cwd, 'components', 'ui'))) stack.ui = 'shadcn';
759
+ else if (deps['@chakra-ui/react']) stack.ui = 'chakra';
760
+ else if (deps['@mui/material']) stack.ui = 'mui';
761
+ else if (deps['antd']) stack.ui = 'antd';
762
+
763
+ // Payments detection
764
+ if (deps['stripe']) stack.payments = 'stripe';
765
+ else if (deps['@paypal/react-paypal-js']) stack.payments = 'paypal';
766
+ else if (deps['square']) stack.payments = 'square';
767
+ } catch {
768
+ // Return default stack on error
769
+ }
770
+
771
+ return stack;
772
+ }
773
+
774
+ // ========================================
775
+ // SUMMARY
776
+ // ========================================
777
+
778
+ /**
779
+ * Get a summary of current engineering state
780
+ */
781
+ getSummary(): {
782
+ project: ProjectConfig | null;
783
+ state: BuildState | null;
784
+ graphStats: { nodes: number; edges: number };
785
+ decisions: number;
786
+ artifacts: string[];
787
+ } {
788
+ const graph = this.getGraph();
789
+
790
+ return {
791
+ project: this.getProject(),
792
+ state: this.getState(),
793
+ graphStats: { nodes: graph.nodes.length, edges: graph.edges.length },
794
+ decisions: this.getDecisions().length,
795
+ artifacts: this.listArtifacts(),
796
+ };
797
+ }
798
+ }
799
+
800
+ // =============================================================================
801
+ // CONVENIENCE FUNCTIONS
802
+ // =============================================================================
803
+
804
+ /**
805
+ * Get the state manager for current directory
806
+ */
807
+ export function getStateManager(cwd?: string): EngineeringStateManager {
808
+ return new EngineeringStateManager(cwd);
809
+ }
810
+
811
+ /**
812
+ * Check if engineering project exists in current directory
813
+ */
814
+ export function hasEngineeringProject(cwd?: string): boolean {
815
+ return getStateManager(cwd).isInitialized();
816
+ }
817
+
818
+ /**
819
+ * Quick summary of current project state
820
+ */
821
+ export function getProjectSummary(cwd?: string) {
822
+ return getStateManager(cwd).getSummary();
823
+ }