@stackweld/core 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-lint.log +498 -0
- package/.turbo/turbo-test.log +21 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/dist/__tests__/compatibility-scorer.test.d.ts +2 -0
- package/dist/__tests__/compatibility-scorer.test.d.ts.map +1 -0
- package/dist/__tests__/compatibility-scorer.test.js +226 -0
- package/dist/__tests__/compatibility-scorer.test.js.map +1 -0
- package/dist/__tests__/rules-engine.test.d.ts +2 -0
- package/dist/__tests__/rules-engine.test.d.ts.map +1 -0
- package/dist/__tests__/rules-engine.test.js +161 -0
- package/dist/__tests__/rules-engine.test.js.map +1 -0
- package/dist/__tests__/scaffold-orchestrator.test.d.ts +2 -0
- package/dist/__tests__/scaffold-orchestrator.test.d.ts.map +1 -0
- package/dist/__tests__/scaffold-orchestrator.test.js +149 -0
- package/dist/__tests__/scaffold-orchestrator.test.js.map +1 -0
- package/dist/__tests__/stack-engine.test.d.ts +2 -0
- package/dist/__tests__/stack-engine.test.d.ts.map +1 -0
- package/dist/__tests__/stack-engine.test.js +278 -0
- package/dist/__tests__/stack-engine.test.js.map +1 -0
- package/dist/db/database.d.ts +9 -0
- package/dist/db/database.d.ts.map +1 -0
- package/dist/db/database.js +106 -0
- package/dist/db/database.js.map +1 -0
- package/dist/db/index.d.ts +2 -0
- package/dist/db/index.d.ts.map +1 -0
- package/dist/db/index.js +2 -0
- package/dist/db/index.js.map +1 -0
- package/dist/engine/compatibility-scorer.d.ts +37 -0
- package/dist/engine/compatibility-scorer.d.ts.map +1 -0
- package/dist/engine/compatibility-scorer.js +178 -0
- package/dist/engine/compatibility-scorer.js.map +1 -0
- package/dist/engine/compose-generator.d.ts +35 -0
- package/dist/engine/compose-generator.d.ts.map +1 -0
- package/dist/engine/compose-generator.js +95 -0
- package/dist/engine/compose-generator.js.map +1 -0
- package/dist/engine/cost-estimator.d.ts +22 -0
- package/dist/engine/cost-estimator.d.ts.map +1 -0
- package/dist/engine/cost-estimator.js +451 -0
- package/dist/engine/cost-estimator.js.map +1 -0
- package/dist/engine/env-analyzer.d.ts +36 -0
- package/dist/engine/env-analyzer.d.ts.map +1 -0
- package/dist/engine/env-analyzer.js +111 -0
- package/dist/engine/env-analyzer.js.map +1 -0
- package/dist/engine/health-checker.d.ts +20 -0
- package/dist/engine/health-checker.d.ts.map +1 -0
- package/dist/engine/health-checker.js +377 -0
- package/dist/engine/health-checker.js.map +1 -0
- package/dist/engine/index.d.ts +11 -0
- package/dist/engine/index.d.ts.map +1 -0
- package/dist/engine/index.js +7 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/engine/infra-generator.d.ts +26 -0
- package/dist/engine/infra-generator.d.ts.map +1 -0
- package/dist/engine/infra-generator.js +751 -0
- package/dist/engine/infra-generator.js.map +1 -0
- package/dist/engine/migration-planner.d.ts +34 -0
- package/dist/engine/migration-planner.d.ts.map +1 -0
- package/dist/engine/migration-planner.js +427 -0
- package/dist/engine/migration-planner.js.map +1 -0
- package/dist/engine/performance-profiler.d.ts +22 -0
- package/dist/engine/performance-profiler.d.ts.map +1 -0
- package/dist/engine/performance-profiler.js +292 -0
- package/dist/engine/performance-profiler.js.map +1 -0
- package/dist/engine/plugin-loader.d.ts +36 -0
- package/dist/engine/plugin-loader.d.ts.map +1 -0
- package/dist/engine/plugin-loader.js +157 -0
- package/dist/engine/plugin-loader.js.map +1 -0
- package/dist/engine/preferences.d.ts +24 -0
- package/dist/engine/preferences.d.ts.map +1 -0
- package/dist/engine/preferences.js +62 -0
- package/dist/engine/preferences.js.map +1 -0
- package/dist/engine/rules-engine.d.ts +31 -0
- package/dist/engine/rules-engine.d.ts.map +1 -0
- package/dist/engine/rules-engine.js +179 -0
- package/dist/engine/rules-engine.js.map +1 -0
- package/dist/engine/runtime-manager.d.ts +65 -0
- package/dist/engine/runtime-manager.d.ts.map +1 -0
- package/dist/engine/runtime-manager.js +181 -0
- package/dist/engine/runtime-manager.js.map +1 -0
- package/dist/engine/scaffold-orchestrator.d.ts +103 -0
- package/dist/engine/scaffold-orchestrator.d.ts.map +1 -0
- package/dist/engine/scaffold-orchestrator.js +934 -0
- package/dist/engine/scaffold-orchestrator.js.map +1 -0
- package/dist/engine/stack-detector.d.ts +21 -0
- package/dist/engine/stack-detector.d.ts.map +1 -0
- package/dist/engine/stack-detector.js +313 -0
- package/dist/engine/stack-detector.js.map +1 -0
- package/dist/engine/stack-differ.d.ts +26 -0
- package/dist/engine/stack-differ.d.ts.map +1 -0
- package/dist/engine/stack-differ.js +80 -0
- package/dist/engine/stack-differ.js.map +1 -0
- package/dist/engine/stack-engine.d.ts +54 -0
- package/dist/engine/stack-engine.d.ts.map +1 -0
- package/dist/engine/stack-engine.js +186 -0
- package/dist/engine/stack-engine.js.map +1 -0
- package/dist/engine/stack-serializer.d.ts +32 -0
- package/dist/engine/stack-serializer.d.ts.map +1 -0
- package/dist/engine/stack-serializer.js +75 -0
- package/dist/engine/stack-serializer.js.map +1 -0
- package/dist/engine/standards-linter.d.ts +34 -0
- package/dist/engine/standards-linter.d.ts.map +1 -0
- package/dist/engine/standards-linter.js +162 -0
- package/dist/engine/standards-linter.js.map +1 -0
- package/dist/engine/tech-installer.d.ts +37 -0
- package/dist/engine/tech-installer.d.ts.map +1 -0
- package/dist/engine/tech-installer.js +508 -0
- package/dist/engine/tech-installer.js.map +1 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +6 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +2 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/project.d.ts +33 -0
- package/dist/types/project.d.ts.map +1 -0
- package/dist/types/project.js +6 -0
- package/dist/types/project.js.map +1 -0
- package/dist/types/stack.d.ts +29 -0
- package/dist/types/stack.d.ts.map +1 -0
- package/dist/types/stack.js +6 -0
- package/dist/types/stack.js.map +1 -0
- package/dist/types/technology.d.ts +47 -0
- package/dist/types/technology.d.ts.map +1 -0
- package/dist/types/technology.js +6 -0
- package/dist/types/technology.js.map +1 -0
- package/dist/types/template.d.ts +34 -0
- package/dist/types/template.d.ts.map +1 -0
- package/dist/types/template.js +6 -0
- package/dist/types/template.js.map +1 -0
- package/dist/types/validation.d.ts +20 -0
- package/dist/types/validation.d.ts.map +1 -0
- package/dist/types/validation.js +5 -0
- package/dist/types/validation.js.map +1 -0
- package/package.json +39 -0
- package/src/__tests__/compatibility-scorer.test.ts +264 -0
- package/src/__tests__/rules-engine.test.ts +170 -0
- package/src/__tests__/scaffold-orchestrator.test.ts +161 -0
- package/src/__tests__/stack-engine.test.ts +328 -0
- package/src/db/database.ts +112 -0
- package/src/db/index.ts +1 -0
- package/src/engine/compatibility-scorer.ts +222 -0
- package/src/engine/compose-generator.ts +134 -0
- package/src/engine/cost-estimator.ts +498 -0
- package/src/engine/env-analyzer.ts +156 -0
- package/src/engine/health-checker.ts +421 -0
- package/src/engine/index.ts +17 -0
- package/src/engine/infra-generator.ts +837 -0
- package/src/engine/migration-planner.ts +496 -0
- package/src/engine/performance-profiler.ts +354 -0
- package/src/engine/plugin-loader.ts +216 -0
- package/src/engine/preferences.ts +85 -0
- package/src/engine/rules-engine.ts +204 -0
- package/src/engine/runtime-manager.ts +207 -0
- package/src/engine/scaffold-orchestrator.ts +1052 -0
- package/src/engine/stack-detector.ts +345 -0
- package/src/engine/stack-differ.ts +118 -0
- package/src/engine/stack-engine.ts +258 -0
- package/src/engine/stack-serializer.ts +95 -0
- package/src/engine/standards-linter.ts +210 -0
- package/src/engine/tech-installer.ts +650 -0
- package/src/index.ts +78 -0
- package/src/types/index.ts +10 -0
- package/src/types/project.ts +36 -0
- package/src/types/stack.ts +32 -0
- package/src/types/technology.ts +58 -0
- package/src/types/template.ts +37 -0
- package/src/types/validation.ts +22 -0
- package/tsconfig.json +10 -0
- package/tsconfig.tsbuildinfo +1 -0
|
@@ -0,0 +1,498 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cost Estimator — Estimates monthly hosting costs for a tech stack.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Technology } from "../types/technology.js";
|
|
6
|
+
|
|
7
|
+
// ─── Types ────────────────────────────────────────────
|
|
8
|
+
|
|
9
|
+
export interface CostItem {
|
|
10
|
+
service: string;
|
|
11
|
+
provider: string;
|
|
12
|
+
monthlyCost: string;
|
|
13
|
+
notes: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CostEstimate {
|
|
17
|
+
monthly: { min: number; max: number; currency: string };
|
|
18
|
+
breakdown: CostItem[];
|
|
19
|
+
tier: "free" | "budget" | "standard" | "premium";
|
|
20
|
+
notes: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ─── Cost Knowledge Map ───────────────────────────────
|
|
24
|
+
|
|
25
|
+
interface CostEntry {
|
|
26
|
+
min: number;
|
|
27
|
+
max: number;
|
|
28
|
+
items: CostItem[];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const DATABASE_COSTS: Record<string, CostEntry> = {
|
|
32
|
+
postgresql: {
|
|
33
|
+
min: 0,
|
|
34
|
+
max: 30,
|
|
35
|
+
items: [
|
|
36
|
+
{
|
|
37
|
+
service: "PostgreSQL",
|
|
38
|
+
provider: "Neon (free tier)",
|
|
39
|
+
monthlyCost: "$0",
|
|
40
|
+
notes: "500MB storage, 0.25 CU",
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
service: "PostgreSQL",
|
|
44
|
+
provider: "Supabase (free)",
|
|
45
|
+
monthlyCost: "$0",
|
|
46
|
+
notes: "500MB, 2 projects",
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
service: "PostgreSQL",
|
|
50
|
+
provider: "Self-hosted VPS",
|
|
51
|
+
monthlyCost: "$0-5",
|
|
52
|
+
notes: "Included in VPS cost",
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
service: "PostgreSQL",
|
|
56
|
+
provider: "AWS RDS",
|
|
57
|
+
monthlyCost: "$15-30/mo",
|
|
58
|
+
notes: "db.t3.micro, single-AZ",
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
},
|
|
62
|
+
mysql: {
|
|
63
|
+
min: 0,
|
|
64
|
+
max: 25,
|
|
65
|
+
items: [
|
|
66
|
+
{
|
|
67
|
+
service: "MySQL",
|
|
68
|
+
provider: "PlanetScale (free)",
|
|
69
|
+
monthlyCost: "$0",
|
|
70
|
+
notes: "5GB, 1B row reads/mo",
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
service: "MySQL",
|
|
74
|
+
provider: "Self-hosted VPS",
|
|
75
|
+
monthlyCost: "$0-5",
|
|
76
|
+
notes: "Included in VPS cost",
|
|
77
|
+
},
|
|
78
|
+
{ service: "MySQL", provider: "AWS RDS", monthlyCost: "$15-25/mo", notes: "db.t3.micro" },
|
|
79
|
+
],
|
|
80
|
+
},
|
|
81
|
+
mariadb: {
|
|
82
|
+
min: 0,
|
|
83
|
+
max: 25,
|
|
84
|
+
items: [
|
|
85
|
+
{
|
|
86
|
+
service: "MariaDB",
|
|
87
|
+
provider: "Self-hosted VPS",
|
|
88
|
+
monthlyCost: "$0-5",
|
|
89
|
+
notes: "Included in VPS cost",
|
|
90
|
+
},
|
|
91
|
+
{ service: "MariaDB", provider: "AWS RDS", monthlyCost: "$15-25/mo", notes: "db.t3.micro" },
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
mongodb: {
|
|
95
|
+
min: 0,
|
|
96
|
+
max: 10,
|
|
97
|
+
items: [
|
|
98
|
+
{
|
|
99
|
+
service: "MongoDB",
|
|
100
|
+
provider: "Atlas (free M0)",
|
|
101
|
+
monthlyCost: "$0",
|
|
102
|
+
notes: "512MB, shared cluster",
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
service: "MongoDB",
|
|
106
|
+
provider: "Self-hosted VPS",
|
|
107
|
+
monthlyCost: "$5-10",
|
|
108
|
+
notes: "Needs 1GB+ RAM",
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
redis: {
|
|
113
|
+
min: 0,
|
|
114
|
+
max: 25,
|
|
115
|
+
items: [
|
|
116
|
+
{
|
|
117
|
+
service: "Redis",
|
|
118
|
+
provider: "Upstash (free)",
|
|
119
|
+
monthlyCost: "$0",
|
|
120
|
+
notes: "10k commands/day",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
service: "Redis",
|
|
124
|
+
provider: "Self-hosted VPS",
|
|
125
|
+
monthlyCost: "$0-5",
|
|
126
|
+
notes: "Included in VPS cost",
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
service: "Redis",
|
|
130
|
+
provider: "AWS ElastiCache",
|
|
131
|
+
monthlyCost: "$10-25/mo",
|
|
132
|
+
notes: "cache.t3.micro",
|
|
133
|
+
},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
136
|
+
sqlite: {
|
|
137
|
+
min: 0,
|
|
138
|
+
max: 0,
|
|
139
|
+
items: [
|
|
140
|
+
{
|
|
141
|
+
service: "SQLite",
|
|
142
|
+
provider: "Embedded",
|
|
143
|
+
monthlyCost: "$0",
|
|
144
|
+
notes: "File-based, no server needed",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const HOSTING_COSTS: Record<string, CostEntry> = {
|
|
151
|
+
docker: {
|
|
152
|
+
min: 5,
|
|
153
|
+
max: 50,
|
|
154
|
+
items: [
|
|
155
|
+
{
|
|
156
|
+
service: "Docker VPS",
|
|
157
|
+
provider: "Basic (Hetzner/DO)",
|
|
158
|
+
monthlyCost: "$5-10/mo",
|
|
159
|
+
notes: "2GB RAM, shared CPU",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
service: "Docker VPS",
|
|
163
|
+
provider: "Production",
|
|
164
|
+
monthlyCost: "$20-50/mo",
|
|
165
|
+
notes: "4-8GB RAM, dedicated CPU",
|
|
166
|
+
},
|
|
167
|
+
],
|
|
168
|
+
},
|
|
169
|
+
vercel: {
|
|
170
|
+
min: 0,
|
|
171
|
+
max: 20,
|
|
172
|
+
items: [
|
|
173
|
+
{
|
|
174
|
+
service: "Vercel",
|
|
175
|
+
provider: "Hobby",
|
|
176
|
+
monthlyCost: "$0",
|
|
177
|
+
notes: "Personal projects, 100GB bandwidth",
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
service: "Vercel",
|
|
181
|
+
provider: "Pro",
|
|
182
|
+
monthlyCost: "$20/mo",
|
|
183
|
+
notes: "Team features, 1TB bandwidth",
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
},
|
|
187
|
+
aws: {
|
|
188
|
+
min: 25,
|
|
189
|
+
max: 60,
|
|
190
|
+
items: [
|
|
191
|
+
{
|
|
192
|
+
service: "AWS ECS Fargate",
|
|
193
|
+
provider: "AWS",
|
|
194
|
+
monthlyCost: "$25-60/mo",
|
|
195
|
+
notes: "0.25 vCPU, 512MB task",
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
},
|
|
199
|
+
gcp: {
|
|
200
|
+
min: 0,
|
|
201
|
+
max: 30,
|
|
202
|
+
items: [
|
|
203
|
+
{
|
|
204
|
+
service: "Cloud Run",
|
|
205
|
+
provider: "GCP",
|
|
206
|
+
monthlyCost: "$0-30/mo",
|
|
207
|
+
notes: "Pay per use, generous free tier",
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
// Categories that are always free (dev tools, frontend static hosting)
|
|
214
|
+
const FREE_CATEGORIES = new Set(["orm", "styling", "auth", "frontend"]);
|
|
215
|
+
|
|
216
|
+
const FREE_NOTES: Record<string, string> = {
|
|
217
|
+
orm: "Dev dependency, no hosting cost",
|
|
218
|
+
styling: "Build-time only, no hosting cost",
|
|
219
|
+
auth: "Library cost is $0, auth service may cost extra",
|
|
220
|
+
frontend: "Static hosting available free (Vercel, Netlify, CF Pages)",
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// ─── Service Costs ────────────────────────────────────
|
|
224
|
+
|
|
225
|
+
const SERVICE_COSTS: Record<string, CostEntry> = {
|
|
226
|
+
grafana: {
|
|
227
|
+
min: 0,
|
|
228
|
+
max: 10,
|
|
229
|
+
items: [
|
|
230
|
+
{
|
|
231
|
+
service: "Grafana",
|
|
232
|
+
provider: "Self-hosted",
|
|
233
|
+
monthlyCost: "$0",
|
|
234
|
+
notes: "OSS, included in VPS",
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
service: "Grafana",
|
|
238
|
+
provider: "Grafana Cloud (free)",
|
|
239
|
+
monthlyCost: "$0",
|
|
240
|
+
notes: "10k metrics, 50GB logs",
|
|
241
|
+
},
|
|
242
|
+
],
|
|
243
|
+
},
|
|
244
|
+
prometheus: {
|
|
245
|
+
min: 0,
|
|
246
|
+
max: 5,
|
|
247
|
+
items: [
|
|
248
|
+
{
|
|
249
|
+
service: "Prometheus",
|
|
250
|
+
provider: "Self-hosted",
|
|
251
|
+
monthlyCost: "$0",
|
|
252
|
+
notes: "OSS, included in VPS",
|
|
253
|
+
},
|
|
254
|
+
],
|
|
255
|
+
},
|
|
256
|
+
elasticsearch: {
|
|
257
|
+
min: 0,
|
|
258
|
+
max: 30,
|
|
259
|
+
items: [
|
|
260
|
+
{
|
|
261
|
+
service: "Elasticsearch",
|
|
262
|
+
provider: "Elastic Cloud (free)",
|
|
263
|
+
monthlyCost: "$0",
|
|
264
|
+
notes: "14-day trial",
|
|
265
|
+
},
|
|
266
|
+
{
|
|
267
|
+
service: "Elasticsearch",
|
|
268
|
+
provider: "Self-hosted",
|
|
269
|
+
monthlyCost: "$10-30/mo",
|
|
270
|
+
notes: "Needs 2-4GB RAM",
|
|
271
|
+
},
|
|
272
|
+
],
|
|
273
|
+
},
|
|
274
|
+
rabbitmq: {
|
|
275
|
+
min: 0,
|
|
276
|
+
max: 20,
|
|
277
|
+
items: [
|
|
278
|
+
{
|
|
279
|
+
service: "RabbitMQ",
|
|
280
|
+
provider: "CloudAMQP (free)",
|
|
281
|
+
monthlyCost: "$0",
|
|
282
|
+
notes: "Little Lemur, 1M msgs/mo",
|
|
283
|
+
},
|
|
284
|
+
{
|
|
285
|
+
service: "RabbitMQ",
|
|
286
|
+
provider: "Self-hosted",
|
|
287
|
+
monthlyCost: "$5-20/mo",
|
|
288
|
+
notes: "Needs 512MB+ RAM",
|
|
289
|
+
},
|
|
290
|
+
],
|
|
291
|
+
},
|
|
292
|
+
nginx: {
|
|
293
|
+
min: 0,
|
|
294
|
+
max: 0,
|
|
295
|
+
items: [
|
|
296
|
+
{
|
|
297
|
+
service: "Nginx",
|
|
298
|
+
provider: "Self-hosted",
|
|
299
|
+
monthlyCost: "$0",
|
|
300
|
+
notes: "Included in VPS, minimal resources",
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
traefik: {
|
|
305
|
+
min: 0,
|
|
306
|
+
max: 0,
|
|
307
|
+
items: [
|
|
308
|
+
{
|
|
309
|
+
service: "Traefik",
|
|
310
|
+
provider: "Self-hosted",
|
|
311
|
+
monthlyCost: "$0",
|
|
312
|
+
notes: "Included in Docker setup",
|
|
313
|
+
},
|
|
314
|
+
],
|
|
315
|
+
},
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
// ─── Tier Calculation ─────────────────────────────────
|
|
319
|
+
|
|
320
|
+
function calculateTier(min: number, max: number): CostEstimate["tier"] {
|
|
321
|
+
const avg = (min + max) / 2;
|
|
322
|
+
if (avg === 0) return "free";
|
|
323
|
+
if (avg < 20) return "budget";
|
|
324
|
+
if (avg <= 60) return "standard";
|
|
325
|
+
return "premium";
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// ─── Main Function ────────────────────────────────────
|
|
329
|
+
|
|
330
|
+
export function estimateCost(technologies: Technology[]): CostEstimate {
|
|
331
|
+
const breakdown: CostItem[] = [];
|
|
332
|
+
let totalMin = 0;
|
|
333
|
+
let totalMax = 0;
|
|
334
|
+
const notes: string[] = [];
|
|
335
|
+
|
|
336
|
+
let hasDocker = false;
|
|
337
|
+
let hasHostingPlatform = false;
|
|
338
|
+
let hasDatabase = false;
|
|
339
|
+
let hasFrontend = false;
|
|
340
|
+
|
|
341
|
+
for (const tech of technologies) {
|
|
342
|
+
// Check for Docker
|
|
343
|
+
if (tech.id === "docker" || tech.dockerImage) {
|
|
344
|
+
hasDocker = true;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Free categories
|
|
348
|
+
if (FREE_CATEGORIES.has(tech.category)) {
|
|
349
|
+
if (tech.category === "frontend") hasFrontend = true;
|
|
350
|
+
breakdown.push({
|
|
351
|
+
service: tech.name,
|
|
352
|
+
provider: "N/A",
|
|
353
|
+
monthlyCost: "$0",
|
|
354
|
+
notes: FREE_NOTES[tech.category] || "No hosting cost",
|
|
355
|
+
});
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Database costs
|
|
360
|
+
if (tech.category === "database") {
|
|
361
|
+
hasDatabase = true;
|
|
362
|
+
const dbCost = DATABASE_COSTS[tech.id];
|
|
363
|
+
if (dbCost) {
|
|
364
|
+
totalMin += dbCost.min;
|
|
365
|
+
totalMax += dbCost.max;
|
|
366
|
+
breakdown.push(...dbCost.items);
|
|
367
|
+
} else {
|
|
368
|
+
breakdown.push({
|
|
369
|
+
service: tech.name,
|
|
370
|
+
provider: "Self-hosted",
|
|
371
|
+
monthlyCost: "$5-15/mo",
|
|
372
|
+
notes: "Estimated for unknown database",
|
|
373
|
+
});
|
|
374
|
+
totalMin += 5;
|
|
375
|
+
totalMax += 15;
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Service costs
|
|
381
|
+
if (tech.category === "service") {
|
|
382
|
+
const svcCost = SERVICE_COSTS[tech.id];
|
|
383
|
+
if (svcCost) {
|
|
384
|
+
totalMin += svcCost.min;
|
|
385
|
+
totalMax += svcCost.max;
|
|
386
|
+
breakdown.push(...svcCost.items);
|
|
387
|
+
} else {
|
|
388
|
+
breakdown.push({
|
|
389
|
+
service: tech.name,
|
|
390
|
+
provider: "Varies",
|
|
391
|
+
monthlyCost: "$0-20/mo",
|
|
392
|
+
notes: "Cost depends on provider and usage",
|
|
393
|
+
});
|
|
394
|
+
totalMax += 20;
|
|
395
|
+
}
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
// Hosting platforms (devops category)
|
|
400
|
+
if (tech.category === "devops") {
|
|
401
|
+
const hostCost = HOSTING_COSTS[tech.id];
|
|
402
|
+
if (hostCost) {
|
|
403
|
+
hasHostingPlatform = true;
|
|
404
|
+
totalMin += hostCost.min;
|
|
405
|
+
totalMax += hostCost.max;
|
|
406
|
+
breakdown.push(...hostCost.items);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// Runtime / backend frameworks — no direct cost, but need hosting
|
|
412
|
+
if (tech.category === "runtime" || tech.category === "backend") {
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// If there's a backend/runtime but no hosting platform, add default VPS estimate
|
|
417
|
+
const hasBackend = technologies.some((t) => t.category === "runtime" || t.category === "backend");
|
|
418
|
+
if (hasBackend && !hasHostingPlatform) {
|
|
419
|
+
if (hasDocker) {
|
|
420
|
+
totalMin += 5;
|
|
421
|
+
totalMax += 50;
|
|
422
|
+
breakdown.push(
|
|
423
|
+
{
|
|
424
|
+
service: "Docker VPS",
|
|
425
|
+
provider: "Basic (Hetzner/DO)",
|
|
426
|
+
monthlyCost: "$5-10/mo",
|
|
427
|
+
notes: "2GB RAM, shared CPU",
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
service: "Docker VPS",
|
|
431
|
+
provider: "Production",
|
|
432
|
+
monthlyCost: "$20-50/mo",
|
|
433
|
+
notes: "4-8GB RAM, dedicated CPU",
|
|
434
|
+
},
|
|
435
|
+
);
|
|
436
|
+
} else {
|
|
437
|
+
totalMin += 0;
|
|
438
|
+
totalMax += 20;
|
|
439
|
+
breakdown.push(
|
|
440
|
+
{
|
|
441
|
+
service: "Hosting",
|
|
442
|
+
provider: "Vercel/Netlify (free)",
|
|
443
|
+
monthlyCost: "$0",
|
|
444
|
+
notes: "Serverless, limited",
|
|
445
|
+
},
|
|
446
|
+
{
|
|
447
|
+
service: "Hosting",
|
|
448
|
+
provider: "VPS (basic)",
|
|
449
|
+
monthlyCost: "$5-20/mo",
|
|
450
|
+
notes: "Small cloud instance",
|
|
451
|
+
},
|
|
452
|
+
);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Frontend static hosting
|
|
457
|
+
if (hasFrontend && !hasHostingPlatform) {
|
|
458
|
+
breakdown.push({
|
|
459
|
+
service: "Static hosting",
|
|
460
|
+
provider: "Vercel/Netlify/CF Pages",
|
|
461
|
+
monthlyCost: "$0",
|
|
462
|
+
notes: "Free tier covers most projects",
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// Domain + SSL
|
|
467
|
+
totalMin += 1;
|
|
468
|
+
totalMax += 1;
|
|
469
|
+
breakdown.push({
|
|
470
|
+
service: "Domain + SSL",
|
|
471
|
+
provider: "Namecheap/Cloudflare",
|
|
472
|
+
monthlyCost: "~$1/mo",
|
|
473
|
+
notes: "$10-15/yr domain, free SSL via Let's Encrypt",
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
// Notes
|
|
477
|
+
if (!hasDatabase) {
|
|
478
|
+
notes.push("No database detected — add one if your app needs persistence.");
|
|
479
|
+
}
|
|
480
|
+
if (hasDocker) {
|
|
481
|
+
notes.push("Docker detected — VPS hosting recommended for full control.");
|
|
482
|
+
}
|
|
483
|
+
if (totalMin === 0 || (totalMin <= 1 && !hasDatabase)) {
|
|
484
|
+
notes.push("This stack can run entirely on free tiers for development and small projects.");
|
|
485
|
+
}
|
|
486
|
+
if (totalMax > 60) {
|
|
487
|
+
notes.push("Production costs can be reduced with reserved instances or spot pricing.");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
const tier = calculateTier(totalMin, totalMax);
|
|
491
|
+
|
|
492
|
+
return {
|
|
493
|
+
monthly: { min: totalMin, max: totalMax, currency: "USD" },
|
|
494
|
+
breakdown,
|
|
495
|
+
tier,
|
|
496
|
+
notes,
|
|
497
|
+
};
|
|
498
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Environment Analyzer — Sync .env files and detect dangerous values.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export interface EnvVar {
|
|
6
|
+
key: string;
|
|
7
|
+
value: string;
|
|
8
|
+
line: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface EnvSyncResult {
|
|
12
|
+
missing: string[];
|
|
13
|
+
extra: string[];
|
|
14
|
+
dangerous: EnvDangerousVar[];
|
|
15
|
+
total: { example: number; actual: number };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface EnvDangerousVar {
|
|
19
|
+
key: string;
|
|
20
|
+
value: string;
|
|
21
|
+
reason: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// ─── Dangerous value patterns ──────────────────────────
|
|
25
|
+
|
|
26
|
+
const PLACEHOLDER_PATTERNS = [
|
|
27
|
+
/change[-_]?me/i,
|
|
28
|
+
/changeme/i,
|
|
29
|
+
/replace/i,
|
|
30
|
+
/your[-_]/i,
|
|
31
|
+
/xxx/i,
|
|
32
|
+
/todo/i,
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
const SENSITIVE_KEY_PATTERNS = [/SECRET/i, /KEY/i, /PASSWORD/i, /TOKEN/i];
|
|
36
|
+
const MIN_SECRET_LENGTH = 16;
|
|
37
|
+
|
|
38
|
+
const DEFAULT_PASSWORDS = new Set(["postgres", "admin", "root", "password", "123456"]);
|
|
39
|
+
|
|
40
|
+
// ─── Public functions ──────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Parse a .env file content into an array of key/value pairs.
|
|
44
|
+
* Skips blank lines and comments (lines starting with #).
|
|
45
|
+
*/
|
|
46
|
+
export function parseEnvFile(content: string): EnvVar[] {
|
|
47
|
+
const vars: EnvVar[] = [];
|
|
48
|
+
const lines = content.split("\n");
|
|
49
|
+
|
|
50
|
+
for (let i = 0; i < lines.length; i++) {
|
|
51
|
+
const raw = lines[i].trim();
|
|
52
|
+
if (raw === "" || raw.startsWith("#")) continue;
|
|
53
|
+
|
|
54
|
+
const eqIndex = raw.indexOf("=");
|
|
55
|
+
if (eqIndex === -1) continue;
|
|
56
|
+
|
|
57
|
+
const key = raw.slice(0, eqIndex).trim();
|
|
58
|
+
let value = raw.slice(eqIndex + 1).trim();
|
|
59
|
+
|
|
60
|
+
// Strip surrounding quotes
|
|
61
|
+
if (
|
|
62
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
63
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
64
|
+
) {
|
|
65
|
+
value = value.slice(1, -1);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (key) {
|
|
69
|
+
vars.push({ key, value, line: i + 1 });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return vars;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Compare .env.example against .env and detect mismatches.
|
|
78
|
+
*/
|
|
79
|
+
export function syncEnv(exampleContent: string, actualContent: string): EnvSyncResult {
|
|
80
|
+
const exampleVars = parseEnvFile(exampleContent);
|
|
81
|
+
const actualVars = parseEnvFile(actualContent);
|
|
82
|
+
|
|
83
|
+
const exampleKeys = new Set(exampleVars.map((v) => v.key));
|
|
84
|
+
const actualKeys = new Set(actualVars.map((v) => v.key));
|
|
85
|
+
|
|
86
|
+
const missing: string[] = [];
|
|
87
|
+
for (const key of exampleKeys) {
|
|
88
|
+
if (!actualKeys.has(key)) {
|
|
89
|
+
missing.push(key);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const extra: string[] = [];
|
|
94
|
+
for (const key of actualKeys) {
|
|
95
|
+
if (!exampleKeys.has(key)) {
|
|
96
|
+
extra.push(key);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const dangerous = checkDangerous(actualVars);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
missing,
|
|
104
|
+
extra,
|
|
105
|
+
dangerous,
|
|
106
|
+
total: { example: exampleVars.length, actual: actualVars.length },
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Detect environment variables with insecure or placeholder values.
|
|
112
|
+
*/
|
|
113
|
+
export function checkDangerous(vars: EnvVar[]): EnvDangerousVar[] {
|
|
114
|
+
const results: EnvDangerousVar[] = [];
|
|
115
|
+
|
|
116
|
+
for (const v of vars) {
|
|
117
|
+
const reason = detectDanger(v);
|
|
118
|
+
if (reason) {
|
|
119
|
+
results.push({ key: v.key, value: v.value, reason });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return results;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function detectDanger(v: EnvVar): string | null {
|
|
127
|
+
// Check placeholder patterns
|
|
128
|
+
for (const pattern of PLACEHOLDER_PATTERNS) {
|
|
129
|
+
if (pattern.test(v.value)) {
|
|
130
|
+
return "Placeholder detected";
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check default passwords
|
|
135
|
+
if (DEFAULT_PASSWORDS.has(v.value.toLowerCase())) {
|
|
136
|
+
return "Default password";
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Check DEBUG enabled
|
|
140
|
+
if (v.key.toUpperCase().includes("DEBUG") && (v.value === "true" || v.value === "1")) {
|
|
141
|
+
return "Debug enabled";
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check short secrets
|
|
145
|
+
const isSensitiveKey = SENSITIVE_KEY_PATTERNS.some((p) => p.test(v.key));
|
|
146
|
+
if (isSensitiveKey && v.value.length > 0 && v.value.length < MIN_SECRET_LENGTH) {
|
|
147
|
+
return `Secret too short (${v.value.length} chars, minimum ${MIN_SECRET_LENGTH})`;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Check DATABASE_URL with localhost
|
|
151
|
+
if (v.key === "DATABASE_URL" && v.value.includes("localhost")) {
|
|
152
|
+
return "Database URL points to localhost";
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return null;
|
|
156
|
+
}
|