@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.
- package/cli/preseed/index.js +16 -0
- package/cli/preseed/interactive.js +143 -0
- package/cli/preseed/templates.js +227 -0
- package/cli/seed/builders/ai-context-builder.js +85 -0
- package/cli/seed/builders/index.js +13 -0
- package/cli/seed/builders/seed-builder.js +272 -0
- package/cli/seed/extractors/content-extractors.js +383 -0
- package/cli/seed/extractors/index.js +47 -0
- package/cli/seed/extractors/metadata-extractors.js +167 -0
- package/cli/seed/extractors/section-extractor.js +54 -0
- package/cli/seed/extractors/stack-extractors.js +228 -0
- package/cli/seed/index.js +18 -0
- package/cli/seed/utils/folder-structure.js +84 -0
- package/cli/seed/utils/index.js +11 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3220 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/context-McpJQa_2.d.ts +5710 -0
- package/dist/core/index.d.ts +635 -0
- package/dist/core/index.js +2593 -0
- package/dist/core/index.js.map +1 -0
- package/dist/index-QqbeEiDm.d.ts +857 -0
- package/dist/index-UiYCgwiH.d.ts +174 -0
- package/dist/index.d.ts +453 -0
- package/dist/index.js +44228 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp/index.d.ts +1 -0
- package/dist/mcp/index.js +41173 -0
- package/dist/mcp/index.js.map +1 -0
- package/generators/index.ts +82 -0
- package/intelligence/orchestrator/config/failure-signatures.js +48 -0
- package/intelligence/orchestrator/config/index.js +20 -0
- package/intelligence/orchestrator/config/phases.js +111 -0
- package/intelligence/orchestrator/config/remediation.js +150 -0
- package/intelligence/orchestrator/config/workflows.js +168 -0
- package/intelligence/orchestrator/core/index.js +16 -0
- package/intelligence/orchestrator/core/state-manager.js +88 -0
- package/intelligence/orchestrator/core/telemetry.js +24 -0
- package/intelligence/orchestrator/index.js +17 -0
- package/mcp/contracts/mcp-contract.v1.json +1 -1
- package/package.json +16 -3
- package/src/cli/agent.ts +703 -0
- package/src/cli/analyze.ts +640 -0
- package/src/cli/audit.ts +707 -0
- package/src/cli/auth.ts +930 -0
- package/src/cli/billing.ts +364 -0
- package/src/cli/build.ts +1089 -0
- package/src/cli/business.ts +508 -0
- package/src/cli/checkpoint-utils.ts +236 -0
- package/src/cli/checkpoint.ts +757 -0
- package/src/cli/cloud-sync.ts +534 -0
- package/src/cli/content.ts +273 -0
- package/src/cli/context.ts +667 -0
- package/src/cli/dashboard.ts +133 -0
- package/src/cli/deploy.ts +704 -0
- package/src/cli/doctor.ts +480 -0
- package/src/cli/fundraise.ts +494 -0
- package/src/cli/generate.ts +346 -0
- package/src/cli/github-cmd.ts +566 -0
- package/src/cli/health.ts +599 -0
- package/src/cli/index.ts +113 -0
- package/src/cli/init.ts +838 -0
- package/src/cli/legal.ts +495 -0
- package/src/cli/log.ts +316 -0
- package/src/cli/loop.ts +1660 -0
- package/src/cli/manager.ts +878 -0
- package/src/cli/mcp.ts +275 -0
- package/src/cli/memory.ts +346 -0
- package/src/cli/metrics.ts +590 -0
- package/src/cli/monitor.ts +960 -0
- package/src/cli/mvp.ts +662 -0
- package/src/cli/onboard.ts +663 -0
- package/src/cli/orchestrator.ts +622 -0
- package/src/cli/plugin.ts +483 -0
- package/src/cli/prd.ts +671 -0
- package/src/cli/preseed-start.ts +1633 -0
- package/src/cli/preseed.ts +2434 -0
- package/src/cli/project.ts +526 -0
- package/src/cli/quality.ts +885 -0
- package/src/cli/security.ts +1079 -0
- package/src/cli/seed.ts +1224 -0
- package/src/cli/skill.ts +537 -0
- package/src/cli/suggest.ts +1225 -0
- package/src/cli/switch.ts +518 -0
- package/src/cli/task.ts +780 -0
- package/src/cli/telemetry.ts +172 -0
- package/src/cli/todo.ts +627 -0
- package/src/cli/types.ts +15 -0
- package/src/cli/update.ts +334 -0
- package/src/cli/visualize.ts +609 -0
- package/src/cli/watch.ts +895 -0
- package/src/cli/workspace.ts +709 -0
- package/src/core/action-recorder.ts +673 -0
- package/src/core/analyze-workflow.ts +1453 -0
- package/src/core/api-client.ts +1120 -0
- package/src/core/audit-workflow.ts +1681 -0
- package/src/core/auth.ts +471 -0
- package/src/core/build-orchestrator.ts +509 -0
- package/src/core/build-state.ts +621 -0
- package/src/core/checkpoint-engine.ts +482 -0
- package/src/core/config.ts +1285 -0
- package/src/core/context-loader.ts +694 -0
- package/src/core/context.ts +410 -0
- package/src/core/deploy-workflow.ts +1085 -0
- package/src/core/entitlements.ts +322 -0
- package/src/core/github-sync.ts +720 -0
- package/src/core/index.ts +981 -0
- package/src/core/ingest.ts +1186 -0
- package/src/core/metrics-engine.ts +886 -0
- package/src/core/mvp.ts +847 -0
- package/src/core/onboard-workflow.ts +1293 -0
- package/src/core/policies.ts +81 -0
- package/src/core/preseed-workflow.ts +1163 -0
- package/src/core/preseed.ts +1826 -0
- package/src/core/project-context.ts +380 -0
- package/src/core/project-state.ts +699 -0
- package/src/core/r2-sync.ts +691 -0
- package/src/core/scaffold.ts +1715 -0
- package/src/core/session.ts +286 -0
- package/src/core/task-extractor.ts +799 -0
- package/src/core/telemetry.ts +371 -0
- package/src/core/tier-enforcement.ts +737 -0
- package/src/core/utils.ts +437 -0
- package/src/index.ts +29 -0
- package/src/intelligence/agent-collab.ts +2376 -0
- package/src/intelligence/auto-suggest.ts +713 -0
- package/src/intelligence/content-gen.ts +1351 -0
- package/src/intelligence/cross-project.ts +1692 -0
- package/src/intelligence/git-memory.ts +529 -0
- package/src/intelligence/index.ts +318 -0
- package/src/intelligence/orchestrator.ts +534 -0
- package/src/intelligence/prd.ts +466 -0
- package/src/intelligence/recommendations.ts +982 -0
- package/src/intelligence/workflow-composer.ts +1472 -0
- package/src/mcp/capabilities.ts +233 -0
- package/src/mcp/index.ts +37 -0
- package/src/mcp/registry.ts +1268 -0
- package/src/mcp/response-formatter.ts +797 -0
- package/src/mcp/server.ts +240 -0
- package/src/types/agent.ts +69 -0
- package/src/types/config.ts +86 -0
- package/src/types/context.ts +77 -0
- package/src/types/index.ts +53 -0
- package/src/types/mcp.ts +91 -0
- package/src/types/skills.ts +47 -0
- package/src/types/workflow.ts +155 -0
- 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
|
+
}
|