@girardmedia/bootspring 2.0.21 → 2.0.22

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.
Files changed (147) hide show
  1. package/cli/preseed/index.js +16 -0
  2. package/cli/preseed/interactive.js +143 -0
  3. package/cli/preseed/templates.js +227 -0
  4. package/cli/seed/builders/ai-context-builder.js +85 -0
  5. package/cli/seed/builders/index.js +13 -0
  6. package/cli/seed/builders/seed-builder.js +272 -0
  7. package/cli/seed/extractors/content-extractors.js +383 -0
  8. package/cli/seed/extractors/index.js +47 -0
  9. package/cli/seed/extractors/metadata-extractors.js +167 -0
  10. package/cli/seed/extractors/section-extractor.js +54 -0
  11. package/cli/seed/extractors/stack-extractors.js +228 -0
  12. package/cli/seed/index.js +18 -0
  13. package/cli/seed/utils/folder-structure.js +84 -0
  14. package/cli/seed/utils/index.js +11 -0
  15. package/dist/cli/index.d.ts +3 -0
  16. package/dist/cli/index.js +3220 -0
  17. package/dist/cli/index.js.map +1 -0
  18. package/dist/context-McpJQa_2.d.ts +5710 -0
  19. package/dist/core/index.d.ts +635 -0
  20. package/dist/core/index.js +2593 -0
  21. package/dist/core/index.js.map +1 -0
  22. package/dist/index-QqbeEiDm.d.ts +857 -0
  23. package/dist/index-UiYCgwiH.d.ts +174 -0
  24. package/dist/index.d.ts +453 -0
  25. package/dist/index.js +44228 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/mcp/index.d.ts +1 -0
  28. package/dist/mcp/index.js +41173 -0
  29. package/dist/mcp/index.js.map +1 -0
  30. package/generators/index.ts +82 -0
  31. package/intelligence/orchestrator/config/failure-signatures.js +48 -0
  32. package/intelligence/orchestrator/config/index.js +20 -0
  33. package/intelligence/orchestrator/config/phases.js +111 -0
  34. package/intelligence/orchestrator/config/remediation.js +150 -0
  35. package/intelligence/orchestrator/config/workflows.js +168 -0
  36. package/intelligence/orchestrator/core/index.js +16 -0
  37. package/intelligence/orchestrator/core/state-manager.js +88 -0
  38. package/intelligence/orchestrator/core/telemetry.js +24 -0
  39. package/intelligence/orchestrator/index.js +17 -0
  40. package/mcp/contracts/mcp-contract.v1.json +1 -1
  41. package/package.json +16 -3
  42. package/src/cli/agent.ts +703 -0
  43. package/src/cli/analyze.ts +640 -0
  44. package/src/cli/audit.ts +707 -0
  45. package/src/cli/auth.ts +930 -0
  46. package/src/cli/billing.ts +364 -0
  47. package/src/cli/build.ts +1089 -0
  48. package/src/cli/business.ts +508 -0
  49. package/src/cli/checkpoint-utils.ts +236 -0
  50. package/src/cli/checkpoint.ts +757 -0
  51. package/src/cli/cloud-sync.ts +534 -0
  52. package/src/cli/content.ts +273 -0
  53. package/src/cli/context.ts +667 -0
  54. package/src/cli/dashboard.ts +133 -0
  55. package/src/cli/deploy.ts +704 -0
  56. package/src/cli/doctor.ts +480 -0
  57. package/src/cli/fundraise.ts +494 -0
  58. package/src/cli/generate.ts +346 -0
  59. package/src/cli/github-cmd.ts +566 -0
  60. package/src/cli/health.ts +599 -0
  61. package/src/cli/index.ts +113 -0
  62. package/src/cli/init.ts +838 -0
  63. package/src/cli/legal.ts +495 -0
  64. package/src/cli/log.ts +316 -0
  65. package/src/cli/loop.ts +1660 -0
  66. package/src/cli/manager.ts +878 -0
  67. package/src/cli/mcp.ts +275 -0
  68. package/src/cli/memory.ts +346 -0
  69. package/src/cli/metrics.ts +590 -0
  70. package/src/cli/monitor.ts +960 -0
  71. package/src/cli/mvp.ts +662 -0
  72. package/src/cli/onboard.ts +663 -0
  73. package/src/cli/orchestrator.ts +622 -0
  74. package/src/cli/plugin.ts +483 -0
  75. package/src/cli/prd.ts +671 -0
  76. package/src/cli/preseed-start.ts +1633 -0
  77. package/src/cli/preseed.ts +2434 -0
  78. package/src/cli/project.ts +526 -0
  79. package/src/cli/quality.ts +885 -0
  80. package/src/cli/security.ts +1079 -0
  81. package/src/cli/seed.ts +1224 -0
  82. package/src/cli/skill.ts +537 -0
  83. package/src/cli/suggest.ts +1225 -0
  84. package/src/cli/switch.ts +518 -0
  85. package/src/cli/task.ts +780 -0
  86. package/src/cli/telemetry.ts +172 -0
  87. package/src/cli/todo.ts +627 -0
  88. package/src/cli/types.ts +15 -0
  89. package/src/cli/update.ts +334 -0
  90. package/src/cli/visualize.ts +609 -0
  91. package/src/cli/watch.ts +895 -0
  92. package/src/cli/workspace.ts +709 -0
  93. package/src/core/action-recorder.ts +673 -0
  94. package/src/core/analyze-workflow.ts +1453 -0
  95. package/src/core/api-client.ts +1120 -0
  96. package/src/core/audit-workflow.ts +1681 -0
  97. package/src/core/auth.ts +471 -0
  98. package/src/core/build-orchestrator.ts +509 -0
  99. package/src/core/build-state.ts +621 -0
  100. package/src/core/checkpoint-engine.ts +482 -0
  101. package/src/core/config.ts +1285 -0
  102. package/src/core/context-loader.ts +694 -0
  103. package/src/core/context.ts +410 -0
  104. package/src/core/deploy-workflow.ts +1085 -0
  105. package/src/core/entitlements.ts +322 -0
  106. package/src/core/github-sync.ts +720 -0
  107. package/src/core/index.ts +981 -0
  108. package/src/core/ingest.ts +1186 -0
  109. package/src/core/metrics-engine.ts +886 -0
  110. package/src/core/mvp.ts +847 -0
  111. package/src/core/onboard-workflow.ts +1293 -0
  112. package/src/core/policies.ts +81 -0
  113. package/src/core/preseed-workflow.ts +1163 -0
  114. package/src/core/preseed.ts +1826 -0
  115. package/src/core/project-context.ts +380 -0
  116. package/src/core/project-state.ts +699 -0
  117. package/src/core/r2-sync.ts +691 -0
  118. package/src/core/scaffold.ts +1715 -0
  119. package/src/core/session.ts +286 -0
  120. package/src/core/task-extractor.ts +799 -0
  121. package/src/core/telemetry.ts +371 -0
  122. package/src/core/tier-enforcement.ts +737 -0
  123. package/src/core/utils.ts +437 -0
  124. package/src/index.ts +29 -0
  125. package/src/intelligence/agent-collab.ts +2376 -0
  126. package/src/intelligence/auto-suggest.ts +713 -0
  127. package/src/intelligence/content-gen.ts +1351 -0
  128. package/src/intelligence/cross-project.ts +1692 -0
  129. package/src/intelligence/git-memory.ts +529 -0
  130. package/src/intelligence/index.ts +318 -0
  131. package/src/intelligence/orchestrator.ts +534 -0
  132. package/src/intelligence/prd.ts +466 -0
  133. package/src/intelligence/recommendations.ts +982 -0
  134. package/src/intelligence/workflow-composer.ts +1472 -0
  135. package/src/mcp/capabilities.ts +233 -0
  136. package/src/mcp/index.ts +37 -0
  137. package/src/mcp/registry.ts +1268 -0
  138. package/src/mcp/response-formatter.ts +797 -0
  139. package/src/mcp/server.ts +240 -0
  140. package/src/types/agent.ts +69 -0
  141. package/src/types/config.ts +86 -0
  142. package/src/types/context.ts +77 -0
  143. package/src/types/index.ts +53 -0
  144. package/src/types/mcp.ts +91 -0
  145. package/src/types/skills.ts +47 -0
  146. package/src/types/workflow.ts +155 -0
  147. package/generators/index.js +0 -18
@@ -0,0 +1,1085 @@
1
+ /**
2
+ * Bootspring Deploy Workflow Engine
3
+ *
4
+ * Handles deployment workflows with pre-deployment validation,
5
+ * target detection, and execution.
6
+ *
7
+ * @package bootspring
8
+ * @module core/deploy-workflow
9
+ */
10
+
11
+ import * as path from 'path';
12
+ import * as fs from 'fs';
13
+ import { execSync, spawn, ChildProcess } from 'child_process';
14
+ import * as http from 'http';
15
+ import * as https from 'https';
16
+
17
+ // ============================================================================
18
+ // Types
19
+ // ============================================================================
20
+
21
+ export interface DeployTarget {
22
+ name: string;
23
+ description: string;
24
+ detect: string[];
25
+ cli: string;
26
+ installCmd: string;
27
+ deployCmd: string;
28
+ previewCmd: string;
29
+ frameworks: string[];
30
+ envFile: string;
31
+ docs: string;
32
+ }
33
+
34
+ export interface DeployPhase {
35
+ name: string;
36
+ description: string;
37
+ required: boolean;
38
+ }
39
+
40
+ export interface DeployPhaseState {
41
+ name: string;
42
+ status: PhaseStatus;
43
+ required: boolean;
44
+ startedAt: string | null;
45
+ completedAt: string | null;
46
+ result: unknown;
47
+ error: string | null;
48
+ }
49
+
50
+ export interface DeployConfig {
51
+ dryRun: boolean;
52
+ skipQuality: boolean;
53
+ env: string;
54
+ }
55
+
56
+ export interface DeployHistoryEntry {
57
+ phase: string;
58
+ action: string;
59
+ timestamp: string;
60
+ }
61
+
62
+ export interface DeployWorkflowState {
63
+ version: string;
64
+ startedAt: string;
65
+ lastUpdated: string;
66
+ target: string | null;
67
+ phases: Record<string, DeployPhaseState>;
68
+ config: DeployConfig;
69
+ history: DeployHistoryEntry[];
70
+ }
71
+
72
+ export interface ValidationResult {
73
+ passed: boolean;
74
+ checks: {
75
+ packageJson: boolean;
76
+ nodeModules: boolean;
77
+ buildScript: boolean;
78
+ gitClean: boolean;
79
+ envFile: boolean;
80
+ };
81
+ issues: string[];
82
+ warnings: string[];
83
+ summary: string;
84
+ }
85
+
86
+ export interface DetectedTarget {
87
+ id: string;
88
+ name: string;
89
+ file: string;
90
+ confidence: string;
91
+ }
92
+
93
+ export interface AvailableTarget {
94
+ id: string;
95
+ name: string;
96
+ cli: string;
97
+ }
98
+
99
+ export interface RecommendedTarget {
100
+ id: string;
101
+ name: string;
102
+ reason: string;
103
+ }
104
+
105
+ export interface TargetDetectionResult {
106
+ detected: DetectedTarget[];
107
+ available: AvailableTarget[];
108
+ recommendations: RecommendedTarget[];
109
+ framework: string | null;
110
+ selected: string | null;
111
+ }
112
+
113
+ export interface BuildResult {
114
+ success?: boolean | undefined;
115
+ dryRun?: boolean | undefined;
116
+ command: string;
117
+ skipped?: boolean | undefined;
118
+ output?: string | undefined;
119
+ }
120
+
121
+ export interface QualityCheckResult {
122
+ passed: boolean | null;
123
+ output: string;
124
+ }
125
+
126
+ export interface QualityChecksResult {
127
+ passed?: boolean | undefined;
128
+ skipped?: boolean | undefined;
129
+ checks?: Record<string, QualityCheckResult> | undefined;
130
+ summary?: string | undefined;
131
+ }
132
+
133
+ export interface DeployResult {
134
+ success?: boolean | undefined;
135
+ dryRun?: boolean | undefined;
136
+ target: string;
137
+ command?: string | undefined;
138
+ skipped?: boolean | undefined;
139
+ url?: string | null | undefined;
140
+ output?: string | undefined;
141
+ }
142
+
143
+ export interface VerificationResult {
144
+ passed?: boolean | undefined;
145
+ skipped?: boolean | undefined;
146
+ reason?: string | undefined;
147
+ statusCode?: number | undefined;
148
+ url?: string | undefined;
149
+ error?: string | undefined;
150
+ }
151
+
152
+ export interface DeployProgress {
153
+ phases: Array<DeployPhaseState & { id: string }>;
154
+ target: string | null;
155
+ overall: {
156
+ completed: number;
157
+ total: number;
158
+ percentage: number;
159
+ };
160
+ startedAt: string;
161
+ lastUpdated: string;
162
+ isComplete: boolean;
163
+ }
164
+
165
+ export interface TargetConfig {
166
+ file: string;
167
+ content: string;
168
+ }
169
+
170
+ export interface TargetInfo {
171
+ id: string;
172
+ name: string;
173
+ description: string;
174
+ detect: string[];
175
+ cli: string;
176
+ installCmd: string;
177
+ deployCmd: string;
178
+ previewCmd: string;
179
+ frameworks: string[];
180
+ envFile: string;
181
+ docs: string;
182
+ }
183
+
184
+ export interface DeployWorkflowOptions {
185
+ dryRun?: boolean | undefined;
186
+ skipQuality?: boolean | undefined;
187
+ env?: string | undefined;
188
+ }
189
+
190
+ type PhaseStatus = 'pending' | 'in_progress' | 'completed' | 'skipped' | 'failed';
191
+
192
+ // ============================================================================
193
+ // Constants
194
+ // ============================================================================
195
+
196
+ /**
197
+ * Deployment targets configuration
198
+ */
199
+ export const DEPLOY_TARGETS: Record<string, DeployTarget> = {
200
+ vercel: {
201
+ name: 'Vercel',
202
+ description: 'Deploy to Vercel (recommended for Next.js)',
203
+ detect: ['vercel.json', '.vercel'],
204
+ cli: 'vercel',
205
+ installCmd: 'npm install -g vercel',
206
+ deployCmd: 'vercel --prod',
207
+ previewCmd: 'vercel',
208
+ frameworks: ['nextjs', 'react', 'vue', 'svelte', 'nuxt', 'astro'],
209
+ envFile: '.env.production',
210
+ docs: 'https://vercel.com/docs'
211
+ },
212
+ railway: {
213
+ name: 'Railway',
214
+ description: 'Deploy to Railway (great for full-stack apps)',
215
+ detect: ['railway.json', 'railway.toml', '.railway'],
216
+ cli: 'railway',
217
+ installCmd: 'npm install -g @railway/cli',
218
+ deployCmd: 'railway up',
219
+ previewCmd: 'railway up --detach',
220
+ frameworks: ['nextjs', 'node', 'python', 'go', 'rust'],
221
+ envFile: '.env.production',
222
+ docs: 'https://docs.railway.app'
223
+ },
224
+ fly: {
225
+ name: 'Fly.io',
226
+ description: 'Deploy to Fly.io (edge deployment)',
227
+ detect: ['fly.toml'],
228
+ cli: 'fly',
229
+ installCmd: 'curl -L https://fly.io/install.sh | sh',
230
+ deployCmd: 'fly deploy',
231
+ previewCmd: 'fly deploy --build-only',
232
+ frameworks: ['docker', 'node', 'go', 'rust', 'python'],
233
+ envFile: '.env.production',
234
+ docs: 'https://fly.io/docs'
235
+ },
236
+ netlify: {
237
+ name: 'Netlify',
238
+ description: 'Deploy to Netlify (static sites & serverless)',
239
+ detect: ['netlify.toml', '.netlify'],
240
+ cli: 'netlify',
241
+ installCmd: 'npm install -g netlify-cli',
242
+ deployCmd: 'netlify deploy --prod',
243
+ previewCmd: 'netlify deploy',
244
+ frameworks: ['react', 'vue', 'svelte', 'gatsby', 'astro', 'nextjs'],
245
+ envFile: '.env.production',
246
+ docs: 'https://docs.netlify.com'
247
+ },
248
+ docker: {
249
+ name: 'Docker',
250
+ description: 'Build Docker image for deployment',
251
+ detect: ['Dockerfile', 'docker-compose.yml', 'docker-compose.yaml'],
252
+ cli: 'docker',
253
+ installCmd: 'Install Docker Desktop from https://docker.com',
254
+ deployCmd: 'docker build -t app . && docker push',
255
+ previewCmd: 'docker build -t app .',
256
+ frameworks: ['any'],
257
+ envFile: '.env.production',
258
+ docs: 'https://docs.docker.com'
259
+ },
260
+ aws: {
261
+ name: 'AWS Amplify',
262
+ description: 'Deploy to AWS Amplify',
263
+ detect: ['amplify.yml', 'amplify'],
264
+ cli: 'amplify',
265
+ installCmd: 'npm install -g @aws-amplify/cli',
266
+ deployCmd: 'amplify push',
267
+ previewCmd: 'amplify status',
268
+ frameworks: ['react', 'nextjs', 'vue', 'angular'],
269
+ envFile: '.env.production',
270
+ docs: 'https://docs.amplify.aws'
271
+ }
272
+ };
273
+
274
+ /**
275
+ * Deployment phases
276
+ */
277
+ export const DEPLOY_PHASES: Record<string, DeployPhase> = {
278
+ validate: {
279
+ name: 'Pre-flight Validation',
280
+ description: 'Validate project readiness for deployment',
281
+ required: true
282
+ },
283
+ target: {
284
+ name: 'Target Detection',
285
+ description: 'Detect or configure deployment target',
286
+ required: true
287
+ },
288
+ build: {
289
+ name: 'Build',
290
+ description: 'Build production assets',
291
+ required: true
292
+ },
293
+ quality: {
294
+ name: 'Quality Checks',
295
+ description: 'Run pre-deploy quality gates',
296
+ required: false
297
+ },
298
+ deploy: {
299
+ name: 'Deploy',
300
+ description: 'Execute deployment',
301
+ required: true
302
+ },
303
+ verify: {
304
+ name: 'Verification',
305
+ description: 'Verify deployment success',
306
+ required: false
307
+ }
308
+ };
309
+
310
+ // ============================================================================
311
+ // DeployWorkflowEngine Class
312
+ // ============================================================================
313
+
314
+ /**
315
+ * DeployWorkflowEngine class
316
+ */
317
+ export class DeployWorkflowEngine {
318
+ readonly projectRoot: string;
319
+ readonly bootspringDir: string;
320
+ readonly deployDir: string;
321
+ readonly statePath: string;
322
+ readonly options: DeployWorkflowOptions;
323
+ state: DeployWorkflowState | null;
324
+
325
+ constructor(projectRoot: string, options: DeployWorkflowOptions = {}) {
326
+ this.projectRoot = projectRoot;
327
+ this.bootspringDir = path.join(projectRoot, '.bootspring');
328
+ this.deployDir = path.join(this.bootspringDir, 'deploy');
329
+ this.statePath = path.join(this.deployDir, 'workflow-state.json');
330
+ this.options = options;
331
+ this.state = null;
332
+ }
333
+
334
+ /**
335
+ * Check if workflow exists
336
+ */
337
+ hasWorkflow(): boolean {
338
+ return fs.existsSync(this.statePath);
339
+ }
340
+
341
+ /**
342
+ * Initialize workflow
343
+ */
344
+ initializeWorkflow(target: string | null = null): void {
345
+ // Ensure directories exist
346
+ if (!fs.existsSync(this.deployDir)) {
347
+ fs.mkdirSync(this.deployDir, { recursive: true });
348
+ }
349
+
350
+ this.state = {
351
+ version: '1.0.0',
352
+ startedAt: new Date().toISOString(),
353
+ lastUpdated: new Date().toISOString(),
354
+ target: target,
355
+ phases: {},
356
+ config: {
357
+ dryRun: this.options.dryRun ?? false,
358
+ skipQuality: this.options.skipQuality ?? false,
359
+ env: this.options.env ?? 'production'
360
+ },
361
+ history: []
362
+ };
363
+
364
+ // Initialize phases
365
+ for (const [phaseId, phase] of Object.entries(DEPLOY_PHASES)) {
366
+ this.state.phases[phaseId] = {
367
+ name: phase.name,
368
+ status: 'pending',
369
+ required: phase.required,
370
+ startedAt: null,
371
+ completedAt: null,
372
+ result: null,
373
+ error: null
374
+ };
375
+ }
376
+
377
+ this.saveState();
378
+ }
379
+
380
+ /**
381
+ * Load state
382
+ */
383
+ loadState(): DeployWorkflowState | null {
384
+ if (fs.existsSync(this.statePath)) {
385
+ this.state = JSON.parse(fs.readFileSync(this.statePath, 'utf-8')) as DeployWorkflowState;
386
+ }
387
+ return this.state;
388
+ }
389
+
390
+ /**
391
+ * Save state
392
+ */
393
+ saveState(): void {
394
+ if (!this.state) return;
395
+ this.state.lastUpdated = new Date().toISOString();
396
+ fs.writeFileSync(this.statePath, JSON.stringify(this.state, null, 2));
397
+ }
398
+
399
+ /**
400
+ * Get next phase
401
+ */
402
+ getNextPhase(): string | null {
403
+ if (!this.state) return null;
404
+
405
+ const phaseOrder = ['validate', 'target', 'build', 'quality', 'deploy', 'verify'];
406
+
407
+ for (const phaseId of phaseOrder) {
408
+ const phase = this.state.phases[phaseId];
409
+ if (phase && (phase.status === 'pending' || phase.status === 'in_progress')) {
410
+ return phaseId;
411
+ }
412
+ }
413
+
414
+ return null;
415
+ }
416
+
417
+ /**
418
+ * Start phase
419
+ */
420
+ startPhase(phaseId: string): void {
421
+ if (!this.state) return;
422
+ const phase = this.state.phases[phaseId];
423
+ if (!phase) return;
424
+
425
+ phase.status = 'in_progress';
426
+ phase.startedAt = new Date().toISOString();
427
+ this.saveState();
428
+ }
429
+
430
+ /**
431
+ * Complete phase
432
+ */
433
+ completePhase(phaseId: string, result: unknown = null): void {
434
+ if (!this.state) return;
435
+ const phase = this.state.phases[phaseId];
436
+ if (!phase) return;
437
+
438
+ phase.status = 'completed';
439
+ phase.completedAt = new Date().toISOString();
440
+ phase.result = result;
441
+ this.state.history.push({
442
+ phase: phaseId,
443
+ action: 'completed',
444
+ timestamp: new Date().toISOString()
445
+ });
446
+ this.saveState();
447
+ }
448
+
449
+ /**
450
+ * Fail phase
451
+ */
452
+ failPhase(phaseId: string, error: string): void {
453
+ if (!this.state) return;
454
+ const phase = this.state.phases[phaseId];
455
+ if (!phase) return;
456
+
457
+ phase.status = 'failed';
458
+ phase.completedAt = new Date().toISOString();
459
+ phase.error = error;
460
+ this.saveState();
461
+ }
462
+
463
+ /**
464
+ * Skip phase
465
+ */
466
+ skipPhase(phaseId: string): void {
467
+ if (!this.state) return;
468
+ const phase = this.state.phases[phaseId];
469
+ if (!phase) return;
470
+
471
+ phase.status = 'skipped';
472
+ this.saveState();
473
+ }
474
+
475
+ /**
476
+ * Get progress
477
+ */
478
+ getProgress(): DeployProgress | null {
479
+ if (!this.state) return null;
480
+
481
+ const phases = Object.entries(this.state.phases).map(([id, phase]) => ({
482
+ id,
483
+ ...phase
484
+ }));
485
+
486
+ const completed = phases.filter(p => p.status === 'completed').length;
487
+ const total = phases.filter(p => p.required).length;
488
+
489
+ return {
490
+ phases,
491
+ target: this.state.target,
492
+ overall: {
493
+ completed,
494
+ total,
495
+ percentage: Math.round((completed / total) * 100)
496
+ },
497
+ startedAt: this.state.startedAt,
498
+ lastUpdated: this.state.lastUpdated,
499
+ isComplete: phases.every(p =>
500
+ p.status === 'completed' || p.status === 'skipped' || !p.required
501
+ )
502
+ };
503
+ }
504
+
505
+ /**
506
+ * Reset workflow
507
+ */
508
+ resetWorkflow(): void {
509
+ if (fs.existsSync(this.deployDir)) {
510
+ fs.rmSync(this.deployDir, { recursive: true });
511
+ }
512
+ this.state = null;
513
+ }
514
+
515
+ // ==========================================
516
+ // Phase Implementations
517
+ // ==========================================
518
+
519
+ /**
520
+ * Run validation phase
521
+ */
522
+ async runValidation(): Promise<ValidationResult> {
523
+ const checks = {
524
+ packageJson: false,
525
+ nodeModules: false,
526
+ buildScript: false,
527
+ gitClean: false,
528
+ envFile: false
529
+ };
530
+
531
+ const issues: string[] = [];
532
+ const warnings: string[] = [];
533
+
534
+ // Check package.json
535
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
536
+ if (fs.existsSync(packageJsonPath)) {
537
+ checks.packageJson = true;
538
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as {
539
+ scripts?: Record<string, string>;
540
+ };
541
+
542
+ // Check build script
543
+ if (pkg.scripts?.build) {
544
+ checks.buildScript = true;
545
+ } else {
546
+ issues.push('No build script found in package.json');
547
+ }
548
+ } else {
549
+ issues.push('No package.json found');
550
+ }
551
+
552
+ // Check node_modules
553
+ if (fs.existsSync(path.join(this.projectRoot, 'node_modules'))) {
554
+ checks.nodeModules = true;
555
+ } else {
556
+ warnings.push('node_modules not found - run npm install first');
557
+ }
558
+
559
+ // Check git status
560
+ try {
561
+ const gitStatus = execSync('git status --porcelain', {
562
+ cwd: this.projectRoot,
563
+ encoding: 'utf-8'
564
+ });
565
+ if (!gitStatus.trim()) {
566
+ checks.gitClean = true;
567
+ } else {
568
+ warnings.push('Uncommitted changes detected');
569
+ }
570
+ } catch {
571
+ warnings.push('Not a git repository');
572
+ }
573
+
574
+ // Check .env.production or .env.local
575
+ const envPaths = ['.env.production', '.env.local', '.env'];
576
+ for (const envPath of envPaths) {
577
+ if (fs.existsSync(path.join(this.projectRoot, envPath))) {
578
+ checks.envFile = true;
579
+ break;
580
+ }
581
+ }
582
+ if (!checks.envFile) {
583
+ warnings.push('No environment file found (.env.production, .env.local, or .env)');
584
+ }
585
+
586
+ const passed = issues.length === 0;
587
+
588
+ return {
589
+ passed,
590
+ checks,
591
+ issues,
592
+ warnings,
593
+ summary: passed ? 'Validation passed' : `${issues.length} issues found`
594
+ };
595
+ }
596
+
597
+ /**
598
+ * Detect deployment target
599
+ */
600
+ async detectTarget(): Promise<TargetDetectionResult> {
601
+ const detected: DetectedTarget[] = [];
602
+ const recommendations: RecommendedTarget[] = [];
603
+
604
+ // Check for existing configs
605
+ for (const [targetId, target] of Object.entries(DEPLOY_TARGETS)) {
606
+ for (const detectFile of target.detect) {
607
+ const filePath = path.join(this.projectRoot, detectFile);
608
+ if (fs.existsSync(filePath)) {
609
+ detected.push({
610
+ id: targetId,
611
+ name: target.name,
612
+ file: detectFile,
613
+ confidence: 'high'
614
+ });
615
+ break;
616
+ }
617
+ }
618
+ }
619
+
620
+ // Check CLI availability
621
+ const available: AvailableTarget[] = [];
622
+ for (const [targetId, target] of Object.entries(DEPLOY_TARGETS)) {
623
+ if (this.checkCliInstalled(target.cli)) {
624
+ available.push({
625
+ id: targetId,
626
+ name: target.name,
627
+ cli: target.cli
628
+ });
629
+ }
630
+ }
631
+
632
+ // Detect framework and recommend
633
+ const framework = this.detectFramework();
634
+ if (framework) {
635
+ for (const [targetId, target] of Object.entries(DEPLOY_TARGETS)) {
636
+ if (target.frameworks.includes(framework) || target.frameworks.includes('any')) {
637
+ recommendations.push({
638
+ id: targetId,
639
+ name: target.name,
640
+ reason: `Works well with ${framework}`
641
+ });
642
+ }
643
+ }
644
+ }
645
+
646
+ // Set state target if exactly one detected
647
+ const firstDetected = detected[0];
648
+ if (detected.length === 1 && firstDetected && this.state && !this.state.target) {
649
+ this.state.target = firstDetected.id;
650
+ }
651
+
652
+ return {
653
+ detected,
654
+ available,
655
+ recommendations,
656
+ framework,
657
+ selected: this.state?.target ?? null
658
+ };
659
+ }
660
+
661
+ /**
662
+ * Check if CLI is installed
663
+ */
664
+ checkCliInstalled(cli: string): boolean {
665
+ try {
666
+ execSync(`which ${cli}`, { encoding: 'utf-8', stdio: 'pipe' });
667
+ return true;
668
+ } catch {
669
+ return false;
670
+ }
671
+ }
672
+
673
+ /**
674
+ * Detect framework
675
+ */
676
+ detectFramework(): string | null {
677
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
678
+ if (!fs.existsSync(packageJsonPath)) return null;
679
+
680
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as {
681
+ dependencies?: Record<string, string>;
682
+ devDependencies?: Record<string, string>;
683
+ };
684
+ const deps: Record<string, string> = { ...pkg.dependencies, ...pkg.devDependencies };
685
+
686
+ if (deps.next) return 'nextjs';
687
+ if (deps['@angular/core']) return 'angular';
688
+ if (deps.vue) return 'vue';
689
+ if (deps.svelte) return 'svelte';
690
+ if (deps.gatsby) return 'gatsby';
691
+ if (deps.astro) return 'astro';
692
+ if (deps.nuxt) return 'nuxt';
693
+ if (deps.react) return 'react';
694
+ if (deps.express) return 'node';
695
+
696
+ return null;
697
+ }
698
+
699
+ /**
700
+ * Set target
701
+ */
702
+ setTarget(targetId: string): void {
703
+ if (!DEPLOY_TARGETS[targetId]) {
704
+ throw new Error(`Unknown target: ${targetId}`);
705
+ }
706
+ if (this.state) {
707
+ this.state.target = targetId;
708
+ this.saveState();
709
+ }
710
+ }
711
+
712
+ /**
713
+ * Run build phase
714
+ */
715
+ async runBuild(): Promise<BuildResult> {
716
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
717
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as {
718
+ scripts?: Record<string, string>;
719
+ };
720
+
721
+ const buildScript = pkg.scripts?.build;
722
+ if (!buildScript) {
723
+ throw new Error('No build script found');
724
+ }
725
+
726
+ if (this.state?.config.dryRun) {
727
+ return {
728
+ dryRun: true,
729
+ command: 'npm run build',
730
+ skipped: true
731
+ };
732
+ }
733
+
734
+ // Execute build
735
+ return new Promise((resolve, reject) => {
736
+ const buildProcess: ChildProcess = spawn('npm', ['run', 'build'], {
737
+ cwd: this.projectRoot,
738
+ stdio: 'pipe',
739
+ shell: true
740
+ });
741
+
742
+ let stdout = '';
743
+ let stderr = '';
744
+
745
+ buildProcess.stdout?.on('data', (data: Buffer) => {
746
+ stdout += data.toString();
747
+ });
748
+
749
+ buildProcess.stderr?.on('data', (data: Buffer) => {
750
+ stderr += data.toString();
751
+ });
752
+
753
+ buildProcess.on('close', (code) => {
754
+ if (code === 0) {
755
+ resolve({
756
+ success: true,
757
+ command: 'npm run build',
758
+ output: stdout.slice(-500) // Last 500 chars
759
+ });
760
+ } else {
761
+ reject(new Error(`Build failed with code ${code}: ${stderr.slice(-200)}`));
762
+ }
763
+ });
764
+ });
765
+ }
766
+
767
+ /**
768
+ * Run quality checks
769
+ */
770
+ async runQualityChecks(): Promise<QualityChecksResult> {
771
+ if (this.state?.config.skipQuality) {
772
+ return { skipped: true };
773
+ }
774
+
775
+ const lintCheck: QualityCheckResult = { passed: null, output: '' };
776
+ const typecheckCheck: QualityCheckResult = { passed: null, output: '' };
777
+ const testCheck: QualityCheckResult = { passed: null, output: '' };
778
+
779
+ const packageJsonPath = path.join(this.projectRoot, 'package.json');
780
+ const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as {
781
+ scripts?: Record<string, string>;
782
+ };
783
+
784
+ // Run lint if available
785
+ if (pkg.scripts?.lint) {
786
+ try {
787
+ execSync('npm run lint', { cwd: this.projectRoot, encoding: 'utf-8', stdio: 'pipe' });
788
+ lintCheck.passed = true;
789
+ } catch (error) {
790
+ lintCheck.passed = false;
791
+ lintCheck.output = (error as Error).message;
792
+ }
793
+ }
794
+
795
+ // Run typecheck if TypeScript
796
+ if (pkg.scripts?.typecheck || pkg.scripts?.['type-check']) {
797
+ const script = pkg.scripts.typecheck ? 'typecheck' : 'type-check';
798
+ try {
799
+ execSync(`npm run ${script}`, { cwd: this.projectRoot, encoding: 'utf-8', stdio: 'pipe' });
800
+ typecheckCheck.passed = true;
801
+ } catch (error) {
802
+ typecheckCheck.passed = false;
803
+ typecheckCheck.output = (error as Error).message;
804
+ }
805
+ }
806
+
807
+ // Run tests if available (but don't fail deploy on test failure)
808
+ if (pkg.scripts?.test) {
809
+ try {
810
+ execSync('npm test -- --passWithNoTests', { cwd: this.projectRoot, encoding: 'utf-8', stdio: 'pipe' });
811
+ testCheck.passed = true;
812
+ } catch {
813
+ testCheck.passed = false;
814
+ // Don't fail on test failures, just warn
815
+ }
816
+ }
817
+
818
+ const checks: Record<string, QualityCheckResult> = {
819
+ lint: lintCheck,
820
+ typecheck: typecheckCheck,
821
+ test: testCheck
822
+ };
823
+
824
+ const allPassed = Object.values(checks).every(c => c.passed === null || c.passed === true);
825
+
826
+ return {
827
+ passed: allPassed,
828
+ checks,
829
+ summary: allPassed ? 'All quality checks passed' : 'Some checks failed'
830
+ };
831
+ }
832
+
833
+ /**
834
+ * Run deployment
835
+ */
836
+ async runDeploy(): Promise<DeployResult> {
837
+ if (!this.state?.target) {
838
+ throw new Error('No deployment target configured');
839
+ }
840
+
841
+ const target = DEPLOY_TARGETS[this.state.target];
842
+ if (!target) {
843
+ throw new Error('No deployment target configured');
844
+ }
845
+
846
+ // Check CLI is installed
847
+ if (!this.checkCliInstalled(target.cli)) {
848
+ throw new Error(`${target.cli} CLI not installed. Run: ${target.installCmd}`);
849
+ }
850
+
851
+ if (this.state.config.dryRun) {
852
+ return {
853
+ dryRun: true,
854
+ target: target.name,
855
+ command: target.deployCmd,
856
+ skipped: true
857
+ };
858
+ }
859
+
860
+ // Execute deployment
861
+ const deployCmd = this.state.config.env === 'preview' ? target.previewCmd : target.deployCmd;
862
+
863
+ return new Promise((resolve, reject) => {
864
+ const deployProcess: ChildProcess = spawn(deployCmd, [], {
865
+ cwd: this.projectRoot,
866
+ stdio: 'pipe',
867
+ shell: true
868
+ });
869
+
870
+ let stdout = '';
871
+ let stderr = '';
872
+
873
+ deployProcess.stdout?.on('data', (data: Buffer) => {
874
+ stdout += data.toString();
875
+ process.stdout.write(data);
876
+ });
877
+
878
+ deployProcess.stderr?.on('data', (data: Buffer) => {
879
+ stderr += data.toString();
880
+ process.stderr.write(data);
881
+ });
882
+
883
+ deployProcess.on('close', (code) => {
884
+ if (code === 0) {
885
+ // Try to extract URL from output
886
+ const urlMatch = stdout.match(/https?:\/\/[^\s]+/);
887
+ resolve({
888
+ success: true,
889
+ target: target.name,
890
+ url: urlMatch ? urlMatch[0] : null,
891
+ output: stdout.slice(-500)
892
+ });
893
+ } else {
894
+ reject(new Error(`Deployment failed with code ${code}`));
895
+ }
896
+ });
897
+ });
898
+ }
899
+
900
+ /**
901
+ * Run verification
902
+ */
903
+ async runVerification(): Promise<VerificationResult> {
904
+ if (!this.state) {
905
+ return {
906
+ skipped: true,
907
+ reason: 'No workflow state'
908
+ };
909
+ }
910
+
911
+ const deployResult = this.state.phases.deploy?.result as { url?: string } | null | undefined;
912
+
913
+ if (!deployResult?.url) {
914
+ return {
915
+ skipped: true,
916
+ reason: 'No deployment URL available'
917
+ };
918
+ }
919
+
920
+ // Simple HTTP check
921
+ try {
922
+ const protocol = deployResult.url.startsWith('https') ? https : http;
923
+
924
+ return new Promise((resolve) => {
925
+ const req = protocol.get(deployResult.url!, (res) => {
926
+ resolve({
927
+ passed: (res.statusCode ?? 0) >= 200 && (res.statusCode ?? 0) < 400,
928
+ statusCode: res.statusCode,
929
+ url: deployResult.url
930
+ });
931
+ });
932
+
933
+ req.on('error', () => {
934
+ resolve({
935
+ passed: false,
936
+ error: 'Could not reach deployment URL'
937
+ });
938
+ });
939
+
940
+ req.setTimeout(10000, () => {
941
+ req.destroy();
942
+ resolve({
943
+ passed: false,
944
+ error: 'Request timed out'
945
+ });
946
+ });
947
+ });
948
+ } catch {
949
+ return {
950
+ skipped: true,
951
+ reason: 'Verification not available'
952
+ };
953
+ }
954
+ }
955
+
956
+ /**
957
+ * Get target info
958
+ */
959
+ getTargetInfo(targetId: string): TargetInfo | null {
960
+ const target = DEPLOY_TARGETS[targetId];
961
+ if (!target) return null;
962
+
963
+ return {
964
+ id: targetId,
965
+ ...target
966
+ };
967
+ }
968
+
969
+ /**
970
+ * Get all targets
971
+ */
972
+ getAllTargets(): TargetInfo[] {
973
+ return Object.entries(DEPLOY_TARGETS).map(([id, target]) => ({
974
+ id,
975
+ ...target
976
+ }));
977
+ }
978
+
979
+ /**
980
+ * Generate deployment config for target
981
+ */
982
+ generateTargetConfig(targetId: string): TargetConfig | null {
983
+ const target = DEPLOY_TARGETS[targetId];
984
+ if (!target) return null;
985
+
986
+ switch (targetId) {
987
+ case 'vercel':
988
+ return {
989
+ file: 'vercel.json',
990
+ content: JSON.stringify({
991
+ '$schema': 'https://openapi.vercel.sh/vercel.json',
992
+ 'buildCommand': 'npm run build',
993
+ 'devCommand': 'npm run dev',
994
+ 'installCommand': 'npm install'
995
+ }, null, 2)
996
+ };
997
+
998
+ case 'railway':
999
+ return {
1000
+ file: 'railway.toml',
1001
+ content: `[build]
1002
+ builder = "nixpacks"
1003
+
1004
+ [deploy]
1005
+ startCommand = "npm start"
1006
+ restartPolicyType = "on_failure"
1007
+ restartPolicyMaxRetries = 3
1008
+ `
1009
+ };
1010
+
1011
+ case 'fly':
1012
+ return {
1013
+ file: 'fly.toml',
1014
+ content: `app = "my-app"
1015
+ primary_region = "iad"
1016
+
1017
+ [build]
1018
+ builder = "heroku/buildpacks:20"
1019
+
1020
+ [http_service]
1021
+ internal_port = 3000
1022
+ force_https = true
1023
+ auto_stop_machines = true
1024
+ auto_start_machines = true
1025
+ min_machines_running = 0
1026
+ `
1027
+ };
1028
+
1029
+ case 'netlify':
1030
+ return {
1031
+ file: 'netlify.toml',
1032
+ content: `[build]
1033
+ command = "npm run build"
1034
+ publish = ".next"
1035
+
1036
+ [[plugins]]
1037
+ package = "@netlify/plugin-nextjs"
1038
+ `
1039
+ };
1040
+
1041
+ case 'docker':
1042
+ return {
1043
+ file: 'Dockerfile',
1044
+ content: `FROM node:20-alpine AS base
1045
+
1046
+ FROM base AS deps
1047
+ WORKDIR /app
1048
+ COPY package*.json ./
1049
+ RUN npm ci
1050
+
1051
+ FROM base AS builder
1052
+ WORKDIR /app
1053
+ COPY --from=deps /app/node_modules ./node_modules
1054
+ COPY . .
1055
+ RUN npm run build
1056
+
1057
+ FROM base AS runner
1058
+ WORKDIR /app
1059
+ ENV NODE_ENV production
1060
+ COPY --from=builder /app/public ./public
1061
+ COPY --from=builder /app/.next/standalone ./
1062
+ COPY --from=builder /app/.next/static ./.next/static
1063
+
1064
+ EXPOSE 3000
1065
+ ENV PORT 3000
1066
+ CMD ["node", "server.js"]
1067
+ `
1068
+ };
1069
+
1070
+ default:
1071
+ return null;
1072
+ }
1073
+ }
1074
+ }
1075
+
1076
+ // ============================================================================
1077
+ // Factory Function
1078
+ // ============================================================================
1079
+
1080
+ export function createDeployWorkflowEngine(
1081
+ projectRoot: string,
1082
+ options: DeployWorkflowOptions = {}
1083
+ ): DeployWorkflowEngine {
1084
+ return new DeployWorkflowEngine(projectRoot, options);
1085
+ }