specky-sdd 3.4.0 → 3.6.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.
Files changed (114) hide show
  1. package/.apm/hooks/scripts/ears-validator.sh +56 -17
  2. package/.apm/prompts/specky-api.prompt.md +1 -1
  3. package/.apm/prompts/specky-research.prompt.md +2 -2
  4. package/.apm/skills/research-analyst/SKILL.md +2 -2
  5. package/.apm/skills/specky-onboarding/SKILL.md +6 -4
  6. package/CHANGELOG.md +58 -0
  7. package/README.md +39 -10
  8. package/SECURITY.md +15 -8
  9. package/apm.yml +1 -1
  10. package/config.yml +1 -1
  11. package/dist/cli/commands/serve.d.ts.map +1 -1
  12. package/dist/cli/commands/serve.js +3 -2
  13. package/dist/cli/commands/serve.js.map +1 -1
  14. package/dist/cli/index.js +3 -1
  15. package/dist/cli/index.js.map +1 -1
  16. package/dist/config.d.ts +24 -1
  17. package/dist/config.d.ts.map +1 -1
  18. package/dist/config.js +92 -6
  19. package/dist/config.js.map +1 -1
  20. package/dist/index.js +63 -14
  21. package/dist/index.js.map +1 -1
  22. package/dist/services/audit-logger.d.ts +54 -5
  23. package/dist/services/audit-logger.d.ts.map +1 -1
  24. package/dist/services/audit-logger.js +121 -9
  25. package/dist/services/audit-logger.js.map +1 -1
  26. package/dist/services/diagram-generator.d.ts +53 -4
  27. package/dist/services/diagram-generator.d.ts.map +1 -1
  28. package/dist/services/diagram-generator.js +565 -178
  29. package/dist/services/diagram-generator.js.map +1 -1
  30. package/dist/services/doc-generator.d.ts +44 -0
  31. package/dist/services/doc-generator.d.ts.map +1 -1
  32. package/dist/services/doc-generator.js +352 -50
  33. package/dist/services/doc-generator.js.map +1 -1
  34. package/dist/services/document-converter.d.ts +10 -7
  35. package/dist/services/document-converter.d.ts.map +1 -1
  36. package/dist/services/document-converter.js +113 -38
  37. package/dist/services/document-converter.js.map +1 -1
  38. package/dist/services/file-manager.d.ts.map +1 -1
  39. package/dist/services/file-manager.js +9 -3
  40. package/dist/services/file-manager.js.map +1 -1
  41. package/dist/services/iac-generator.d.ts +31 -3
  42. package/dist/services/iac-generator.d.ts.map +1 -1
  43. package/dist/services/iac-generator.js +509 -41
  44. package/dist/services/iac-generator.js.map +1 -1
  45. package/dist/services/pbt-generator.d.ts +12 -0
  46. package/dist/services/pbt-generator.d.ts.map +1 -1
  47. package/dist/services/pbt-generator.js +296 -145
  48. package/dist/services/pbt-generator.js.map +1 -1
  49. package/dist/services/state-machine.d.ts +15 -1
  50. package/dist/services/state-machine.d.ts.map +1 -1
  51. package/dist/services/state-machine.js +21 -3
  52. package/dist/services/state-machine.js.map +1 -1
  53. package/dist/services/test-generator.d.ts +8 -0
  54. package/dist/services/test-generator.d.ts.map +1 -1
  55. package/dist/services/test-generator.js +134 -33
  56. package/dist/services/test-generator.js.map +1 -1
  57. package/dist/services/test-traceability-mapper.d.ts.map +1 -1
  58. package/dist/services/test-traceability-mapper.js +8 -3
  59. package/dist/services/test-traceability-mapper.js.map +1 -1
  60. package/dist/services/work-item-exporter.d.ts +70 -9
  61. package/dist/services/work-item-exporter.d.ts.map +1 -1
  62. package/dist/services/work-item-exporter.js +115 -53
  63. package/dist/services/work-item-exporter.js.map +1 -1
  64. package/dist/templates/checklist.md +2 -3
  65. package/dist/templates/cross-analysis.md +2 -4
  66. package/dist/tools/audit.js +1 -1
  67. package/dist/tools/audit.js.map +1 -1
  68. package/dist/tools/documentation.d.ts.map +1 -1
  69. package/dist/tools/documentation.js +9 -3
  70. package/dist/tools/documentation.js.map +1 -1
  71. package/dist/tools/environment.d.ts.map +1 -1
  72. package/dist/tools/environment.js +75 -12
  73. package/dist/tools/environment.js.map +1 -1
  74. package/dist/tools/infrastructure.d.ts.map +1 -1
  75. package/dist/tools/infrastructure.js +7 -39
  76. package/dist/tools/infrastructure.js.map +1 -1
  77. package/dist/tools/input.js +4 -4
  78. package/dist/tools/input.js.map +1 -1
  79. package/dist/tools/integration.js +4 -4
  80. package/dist/tools/integration.js.map +1 -1
  81. package/dist/tools/pipeline.d.ts.map +1 -1
  82. package/dist/tools/pipeline.js +42 -4
  83. package/dist/tools/pipeline.js.map +1 -1
  84. package/dist/tools/quality.d.ts.map +1 -1
  85. package/dist/tools/quality.js +36 -9
  86. package/dist/tools/quality.js.map +1 -1
  87. package/dist/tools/rbac.d.ts.map +1 -1
  88. package/dist/tools/rbac.js +18 -11
  89. package/dist/tools/rbac.js.map +1 -1
  90. package/dist/tools/testing.d.ts +10 -0
  91. package/dist/tools/testing.d.ts.map +1 -1
  92. package/dist/tools/testing.js +66 -13
  93. package/dist/tools/testing.js.map +1 -1
  94. package/dist/tools/tool-enforcement.d.ts +15 -1
  95. package/dist/tools/tool-enforcement.d.ts.map +1 -1
  96. package/dist/tools/tool-enforcement.js +70 -9
  97. package/dist/tools/tool-enforcement.js.map +1 -1
  98. package/dist/tools/transcript.d.ts.map +1 -1
  99. package/dist/tools/transcript.js +66 -41
  100. package/dist/tools/transcript.js.map +1 -1
  101. package/dist/tools/utility.d.ts.map +1 -1
  102. package/dist/tools/utility.js +22 -12
  103. package/dist/tools/utility.js.map +1 -1
  104. package/dist/tools/visualization.d.ts +21 -0
  105. package/dist/tools/visualization.d.ts.map +1 -1
  106. package/dist/tools/visualization.js +130 -29
  107. package/dist/tools/visualization.js.map +1 -1
  108. package/dist/utils/token-table.d.ts +41 -0
  109. package/dist/utils/token-table.d.ts.map +1 -0
  110. package/dist/utils/token-table.js +103 -0
  111. package/dist/utils/token-table.js.map +1 -0
  112. package/package.json +1 -1
  113. package/templates/checklist.md +2 -3
  114. package/templates/cross-analysis.md +2 -4
@@ -1,3 +1,109 @@
1
+ /**
2
+ * Detect the tech stack named in DESIGN.md prose. Returns undefined when the
3
+ * design names no recognizable language/runtime so callers can pick their own
4
+ * fallback (codebase scan result, generic image, TypeScript default, ...).
5
+ */
6
+ export function detectTechStackFromDesign(designContent) {
7
+ const lower = designContent.toLowerCase();
8
+ if (!lower.trim())
9
+ return undefined;
10
+ if (/\bpython\b/.test(lower)) {
11
+ const framework = /\bdjango\b/.test(lower)
12
+ ? "Django"
13
+ : /\bfastapi\b/.test(lower)
14
+ ? "FastAPI"
15
+ : /\bflask\b/.test(lower)
16
+ ? "Flask"
17
+ : undefined;
18
+ return { language: "Python", framework, runtime: "python3.12" };
19
+ }
20
+ const namesJsRuntime = /\btypescript\b|\bjavascript\b|\bnode(\.js)?\b|\bdeno\b|\bbun\b|\bexpress\b|\bfastify\b|\bnext\.?js\b|\breact\b|\bnpm\b/.test(lower);
21
+ // Bare "go" is common English prose; only treat it as Go when no other
22
+ // language is named (or when "golang" is explicit).
23
+ if (/\bgolang\b/.test(lower) || (/\bgo\b/.test(lower) && !namesJsRuntime && !/\bjava\b/.test(lower))) {
24
+ const framework = /\bgin\b/.test(lower) ? "Gin" : /\becho\b/.test(lower) ? "Echo" : undefined;
25
+ return { language: "Go", framework, runtime: "go1.22" };
26
+ }
27
+ if (/\bjava\b/.test(lower) && !/\bjavascript\b/.test(lower)) {
28
+ const framework = /\bspring\b/.test(lower) ? "Spring Boot" : undefined;
29
+ return { language: "Java", framework, runtime: "java21" };
30
+ }
31
+ if (namesJsRuntime) {
32
+ const framework = /\bnext\.?js\b/.test(lower)
33
+ ? "Next.js"
34
+ : /\bexpress\b/.test(lower)
35
+ ? "Express"
36
+ : /\bfastify\b/.test(lower)
37
+ ? "Fastify"
38
+ : undefined;
39
+ return { language: "TypeScript", framework, runtime: "node22" };
40
+ }
41
+ return undefined;
42
+ }
43
+ /**
44
+ * Detect docker-compose sidecar services from DESIGN.md prose and package.json
45
+ * dependencies (dependencies + devDependencies). Deterministic keyword scan.
46
+ */
47
+ export function detectServicesFromDesign(designContent, packageJsonContent) {
48
+ const lower = designContent.toLowerCase();
49
+ let deps = {};
50
+ if (packageJsonContent) {
51
+ try {
52
+ const pkg = JSON.parse(packageJsonContent);
53
+ deps = {
54
+ ...pkg["dependencies"],
55
+ ...pkg["devDependencies"],
56
+ };
57
+ }
58
+ catch {
59
+ // Unparseable package.json — fall back to design keywords only
60
+ }
61
+ }
62
+ const hasDep = (name) => name in deps;
63
+ const services = [];
64
+ if (/\bpostgres(ql)?\b/.test(lower) || hasDep("pg") || hasDep("postgres") || hasDep("pg-promise"))
65
+ services.push("postgres");
66
+ if (/\b(mysql|mariadb)\b/.test(lower) || hasDep("mysql") || hasDep("mysql2") || hasDep("mariadb"))
67
+ services.push("mysql");
68
+ if (/\bmongo(db)?\b/.test(lower) || hasDep("mongodb") || hasDep("mongoose"))
69
+ services.push("mongodb");
70
+ if (/\bredis\b/.test(lower) || hasDep("redis") || hasDep("ioredis"))
71
+ services.push("redis");
72
+ if (/\b(rabbitmq|amqp)\b/.test(lower) || hasDep("amqplib"))
73
+ services.push("rabbitmq");
74
+ if (/\bkafka\b/.test(lower) || hasDep("kafkajs"))
75
+ services.push("kafka");
76
+ return services;
77
+ }
78
+ const DEFAULT_SERVICE_FOR_MODULE = {
79
+ networking: "network",
80
+ compute: "container",
81
+ database: "postgres",
82
+ cache: "redis",
83
+ storage: "object",
84
+ messaging: "queue",
85
+ identity: "identity",
86
+ monitoring: "logs",
87
+ };
88
+ /** Compose sidecar templates for the services detectServicesFromDesign can emit. */
89
+ const COMPOSE_SERVICES = {
90
+ postgres: {
91
+ image: "postgres:16-alpine",
92
+ ports: ["5432:5432"],
93
+ environment: ["POSTGRES_USER=app", "POSTGRES_PASSWORD=app", "POSTGRES_DB=app"],
94
+ volume: { name: "postgres-data", mountPath: "/var/lib/postgresql/data" },
95
+ },
96
+ mysql: {
97
+ image: "mysql:8",
98
+ ports: ["3306:3306"],
99
+ environment: ["MYSQL_ROOT_PASSWORD=app", "MYSQL_DATABASE=app"],
100
+ volume: { name: "mysql-data", mountPath: "/var/lib/mysql" },
101
+ },
102
+ mongodb: { image: "mongo:7", ports: ["27017:27017"], volume: { name: "mongo-data", mountPath: "/data/db" } },
103
+ redis: { image: "redis:7-alpine", ports: ["6379:6379"] },
104
+ rabbitmq: { image: "rabbitmq:3-management-alpine", ports: ["5672:5672", "15672:15672"] },
105
+ kafka: { image: "apache/kafka:3.7.0", ports: ["9092:9092"] },
106
+ };
1
107
  export class IacGenerator {
2
108
  fileManager;
3
109
  constructor(fileManager) {
@@ -9,26 +115,24 @@ export class IacGenerator {
9
115
  designContent = await this.fileManager.readSpecFile(featureDir, "DESIGN.md");
10
116
  }
11
117
  catch { /* empty */ }
12
- const detectedModules = modules || this.detectModules(designContent);
118
+ const explicitModules = Boolean(modules && modules.length > 0);
119
+ const components = this.detectComponents(designContent, modules);
13
120
  const files = [];
14
- const variables = [];
15
- // main.tf
16
- files.push({ path: "terraform/main.tf", content: this.generateMainTf(cloud, detectedModules), description: "Main Terraform configuration" });
17
- // variables.tf
18
- const vars = this.generateVariables(cloud, detectedModules);
19
- variables.push(...vars);
121
+ const { mainTf, outputs } = this.renderCloud(cloud, components);
122
+ files.push({ path: "terraform/main.tf", content: mainTf, description: "Main Terraform configuration" });
123
+ const vars = this.generateVariables(cloud, components);
20
124
  files.push({ path: "terraform/variables.tf", content: this.generateVariablesTf(vars), description: "Terraform variable definitions" });
21
- // outputs.tf
22
- files.push({ path: "terraform/outputs.tf", content: this.generateOutputsTf(detectedModules), description: "Terraform output definitions" });
23
- // terraform.tfvars.example
125
+ files.push({ path: "terraform/outputs.tf", content: this.generateOutputsTf(outputs), description: "Terraform output definitions" });
24
126
  files.push({ path: "terraform/terraform.tfvars.example", content: vars.map(v => `${v.name} = ${v.default || `"<${v.name}>"`}`).join("\n"), description: "Example variable values" });
25
- const diagram = `flowchart TD\n TF[Terraform] --> ${detectedModules.map((m, i) => `M${i}[${m}]`).join("\n TF --> ")}`;
127
+ const diagram = `flowchart TD\n TF[Terraform: ${cloud}]\n${components.map((c, i) => ` TF --> M${i}[${c.module}: ${c.service}]`).join("\n")}`;
128
+ const resourceCount = (mainTf.match(/^resource "/gm) || []).length;
129
+ const componentList = components.map(c => `${c.module} (${c.service})`).join(", ");
26
130
  return {
27
131
  provider: "terraform",
28
132
  files,
29
133
  variables: vars,
30
- explanation: `Generated Terraform for ${cloud} with ${detectedModules.length} modules: ${detectedModules.join(", ")}`,
31
- next_steps: "Run sdd_validate_iac to validate the Terraform configuration via Terraform MCP.",
134
+ explanation: `Generated Terraform for ${cloud} with ${resourceCount} resource blocks across ${components.length} modules: ${componentList}. ${explicitModules ? "Modules were requested explicitly." : "Components detected from DESIGN.md."}`,
135
+ next_steps: "Set required variables (see terraform.tfvars.example), then run sdd_validate_iac to validate the configuration via Terraform MCP.",
32
136
  diagram,
33
137
  };
34
138
  }
@@ -43,16 +147,18 @@ export class IacGenerator {
43
147
  explanation: `Validation payload ready for ${provider} on ${cloud}. Route to ${routing.mcp_server} MCP.`,
44
148
  };
45
149
  }
46
- generateDockerfile(techStack, includeCompose, multiStage) {
150
+ generateDockerfile(techStack, includeCompose, multiStage, services = []) {
47
151
  const files = [];
48
152
  const lang = techStack.language.toLowerCase();
153
+ const isNode = lang.includes("typescript") || lang.includes("javascript");
154
+ const isPython = lang.includes("python");
49
155
  let dockerfile = "";
50
- if (lang.includes("typescript") || lang.includes("javascript")) {
156
+ if (isNode) {
51
157
  dockerfile = multiStage
52
158
  ? `FROM node:22-slim AS builder\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\n\nFROM node:22-slim\nWORKDIR /app\nCOPY --from=builder /app/dist ./dist\nCOPY --from=builder /app/node_modules ./node_modules\nCOPY --from=builder /app/package.json ./\nEXPOSE 3000\nCMD ["node", "dist/index.js"]`
53
159
  : `FROM node:22-slim\nWORKDIR /app\nCOPY package*.json ./\nRUN npm ci\nCOPY . .\nRUN npm run build\nEXPOSE 3000\nCMD ["node", "dist/index.js"]`;
54
160
  }
55
- else if (lang.includes("python")) {
161
+ else if (isPython) {
56
162
  dockerfile = multiStage
57
163
  ? `FROM python:3.12-slim AS builder\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\n\nFROM python:3.12-slim\nWORKDIR /app\nCOPY --from=builder /app .\nEXPOSE 8000\nCMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]`
58
164
  : `FROM python:3.12-slim\nWORKDIR /app\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\nCOPY . .\nEXPOSE 8000\nCMD ["python", "-m", "uvicorn", "main:app", "--host", "0.0.0.0"]`;
@@ -60,16 +166,19 @@ export class IacGenerator {
60
166
  else {
61
167
  dockerfile = `FROM ubuntu:22.04\nWORKDIR /app\nCOPY . .\nEXPOSE 3000\nCMD ["./start.sh"]`;
62
168
  }
169
+ // The generic ubuntu fallback is single-stage regardless of the flag —
170
+ // report what was actually generated, not what was requested.
171
+ const multiStageUsed = multiStage && (isNode || isPython);
63
172
  files.push({ path: "Dockerfile", content: dockerfile, description: "Application Dockerfile" });
64
173
  if (includeCompose) {
65
- const compose = `version: "3.8"\nservices:\n app:\n build: .\n ports:\n - "3000:3000"\n environment:\n - NODE_ENV=production\n restart: unless-stopped`;
66
- files.push({ path: "docker-compose.yml", content: compose, description: "Docker Compose configuration" });
174
+ files.push({ path: "docker-compose.yml", content: this.generateCompose(services), description: "Docker Compose configuration" });
67
175
  }
68
176
  files.push({ path: ".dockerignore", content: "node_modules\ndist\n.git\n*.md\n.specs\n.env*\ncoverage", description: "Docker ignore file" });
177
+ const serviceNote = includeCompose && services.length > 0 ? ` (services: app, ${services.join(", ")})` : "";
69
178
  return {
70
179
  type: "docker",
71
180
  files,
72
- explanation: `Generated ${multiStage ? "multi-stage " : ""}Dockerfile for ${techStack.language}${techStack.framework ? ` (${techStack.framework})` : ""}${includeCompose ? " with docker-compose.yml" : ""}.`,
181
+ explanation: `Generated ${multiStageUsed ? "multi-stage " : ""}Dockerfile for ${techStack.language}${techStack.framework ? ` (${techStack.framework})` : ""}${includeCompose ? ` with docker-compose.yml${serviceNote}` : ""}.`,
73
182
  next_steps: "Run docker build to test the image, or use sdd_setup_local_env to create a full dev environment.",
74
183
  };
75
184
  }
@@ -78,17 +187,21 @@ export class IacGenerator {
78
187
  let image = "mcr.microsoft.com/devcontainers/base:ubuntu";
79
188
  const devFeatures = {};
80
189
  const devExtensions = extensions || [];
190
+ let postCreateCommand;
81
191
  if (lang.includes("typescript") || lang.includes("javascript")) {
82
192
  image = "mcr.microsoft.com/devcontainers/typescript-node:22";
83
193
  devExtensions.push("dbaeumer.vscode-eslint", "esbenp.prettier-vscode");
194
+ postCreateCommand = "npm install";
84
195
  }
85
196
  else if (lang.includes("python")) {
86
197
  image = "mcr.microsoft.com/devcontainers/python:3.12";
87
198
  devExtensions.push("ms-python.python", "ms-python.vscode-pylance");
199
+ postCreateCommand = "pip install -r requirements.txt";
88
200
  }
89
201
  else if (lang.includes("go")) {
90
202
  image = "mcr.microsoft.com/devcontainers/go:1.22";
91
203
  devExtensions.push("golang.go");
204
+ postCreateCommand = "go mod download";
92
205
  }
93
206
  if (features) {
94
207
  for (const f of features)
@@ -100,8 +213,11 @@ export class IacGenerator {
100
213
  features: devFeatures,
101
214
  customizations: { vscode: { extensions: devExtensions } },
102
215
  forwardPorts: [3000],
103
- postCreateCommand: lang.includes("python") ? "pip install -r requirements.txt" : "npm install",
104
216
  };
217
+ // Only known stacks get a package-manager bootstrap; the generic ubuntu
218
+ // image must not hardcode "npm install".
219
+ if (postCreateCommand)
220
+ config.postCreateCommand = postCreateCommand;
105
221
  return {
106
222
  type: "devcontainer",
107
223
  files: [{ path: ".devcontainer/devcontainer.json", content: JSON.stringify(config, null, 2), description: "Dev container configuration" }],
@@ -110,28 +226,354 @@ export class IacGenerator {
110
226
  };
111
227
  }
112
228
  // ─── Private helpers ───
113
- detectModules(designContent) {
114
- const modules = [];
115
- const patterns = {
116
- networking: /\b(vpc|vnet|subnet|network|load.?balancer|cdn|dns|firewall)\b/i,
117
- compute: /\b(vm|container|kubernetes|aks|ecs|app.?service|function|lambda|compute)\b/i,
118
- database: /\b(database|db|sql|cosmos|dynamo|postgres|mysql|redis|mongo|storage.?account)\b/i,
119
- storage: /\b(blob|s3|bucket|file.?share|storage)\b/i,
120
- identity: /\b(identity|iam|rbac|entra|active.?directory|cognito|auth)\b/i,
121
- monitoring: /\b(monitor|log|metric|alert|insight|grafana|datadog|observability)\b/i,
229
+ generateCompose(services) {
230
+ // Sanitize service names — they end up as YAML keys.
231
+ const names = services.map(s => s.toLowerCase().replace(/[^a-z0-9_-]/g, "-")).filter(s => s.length > 0);
232
+ const lines = [
233
+ 'version: "3.8"',
234
+ "services:",
235
+ " app:",
236
+ " build: .",
237
+ " ports:",
238
+ ' - "3000:3000"',
239
+ " environment:",
240
+ " - NODE_ENV=production",
241
+ ];
242
+ if (names.length > 0) {
243
+ lines.push(" depends_on:");
244
+ for (const name of names)
245
+ lines.push(` - ${name}`);
246
+ }
247
+ lines.push(" restart: unless-stopped");
248
+ const volumes = [];
249
+ for (const name of names) {
250
+ const svc = COMPOSE_SERVICES[name] ?? { image: `${name}:latest`, ports: [] };
251
+ lines.push(` ${name}:`);
252
+ lines.push(` image: ${svc.image}`);
253
+ if (svc.environment && svc.environment.length > 0) {
254
+ lines.push(" environment:");
255
+ for (const e of svc.environment)
256
+ lines.push(` - ${e}`);
257
+ }
258
+ if (svc.ports.length > 0) {
259
+ lines.push(" ports:");
260
+ for (const p of svc.ports)
261
+ lines.push(` - "${p}"`);
262
+ }
263
+ if (svc.volume) {
264
+ lines.push(" volumes:");
265
+ lines.push(` - ${svc.volume.name}:${svc.volume.mountPath}`);
266
+ volumes.push(svc.volume.name);
267
+ }
268
+ lines.push(" restart: unless-stopped");
269
+ }
270
+ if (volumes.length > 0) {
271
+ lines.push("volumes:");
272
+ for (const v of volumes)
273
+ lines.push(` ${v}:`);
274
+ }
275
+ return lines.join("\n");
276
+ }
277
+ /**
278
+ * Detect concrete infrastructure components from DESIGN.md. Each component
279
+ * carries its module/category AND the specific service flavor named in the
280
+ * design (postgres vs mysql, container vs kubernetes, ...), so the emitted
281
+ * Terraform contains real resource blocks for what the design describes.
282
+ */
283
+ detectComponents(designContent, requestedModules) {
284
+ const lower = designContent.toLowerCase();
285
+ const detected = [];
286
+ const add = (module, service) => {
287
+ if (!detected.some(c => c.module === module && c.service === service))
288
+ detected.push({ module, service });
122
289
  };
123
- for (const [name, regex] of Object.entries(patterns)) {
124
- if (regex.test(designContent))
125
- modules.push(name);
290
+ if (/\b(vpc|vnet|virtual network|subnet|load balancer|application gateway|front door|firewall)\b/.test(lower))
291
+ add("networking", "network");
292
+ if (/\b(kubernetes|k8s|aks|eks|gke)\b/.test(lower))
293
+ add("compute", "kubernetes");
294
+ else if (/\b(lambda|serverless|azure functions?|cloud functions?)\b/.test(lower))
295
+ add("compute", "serverless");
296
+ else if (/\b(container apps?|containers?|docker|ecs|fargate|cloud run|app service)\b/.test(lower))
297
+ add("compute", "container");
298
+ if (/\bpostgres(ql)?\b/.test(lower))
299
+ add("database", "postgres");
300
+ if (/\b(mysql|mariadb)\b/.test(lower))
301
+ add("database", "mysql");
302
+ if (/\b(cosmos ?db|mongodb|mongo|dynamodb|firestore|documentdb)\b/.test(lower))
303
+ add("database", "nosql");
304
+ if (/\b(sql server|azure sql|mssql)\b/.test(lower))
305
+ add("database", "sqlserver");
306
+ if (/\b(redis|memcached|elasticache|cache)\b/.test(lower))
307
+ add("cache", "redis");
308
+ if (/\b(blob storage|s3|bucket|object storage|storage account|file share|storage)\b/.test(lower))
309
+ add("storage", "object");
310
+ if (/\b(service bus|sqs|sns|pub\/?sub|rabbitmq|kafka|event hubs?|message queue|queue)\b/.test(lower))
311
+ add("messaging", "queue");
312
+ if (/\b(managed identity|iam|rbac|entra|active directory|service account)\b/.test(lower))
313
+ add("identity", "identity");
314
+ if (/\b(monitoring|log analytics|application insights|cloudwatch|observability|alerting|metrics)\b/.test(lower))
315
+ add("monitoring", "logs");
316
+ // Explicit module list: honor it, refining each module with the flavor
317
+ // detected in the design (requested "database" + design says mysql → mysql).
318
+ if (requestedModules && requestedModules.length > 0) {
319
+ const result = [];
320
+ for (const m of requestedModules) {
321
+ const hits = detected.filter(c => c.module === m);
322
+ if (hits.length > 0)
323
+ result.push(...hits);
324
+ else
325
+ result.push({ module: m, service: DEFAULT_SERVICE_FOR_MODULE[m] ?? m });
326
+ }
327
+ return result;
328
+ }
329
+ return detected.length > 0 ? detected : [{ module: "compute", service: "container" }, { module: "networking", service: "network" }];
330
+ }
331
+ renderCloud(cloud, components) {
332
+ const providerName = cloud === "azure" ? "azurerm" : cloud === "aws" ? "aws" : "google";
333
+ const providerSource = cloud === "azure" ? "hashicorp/azurerm" : cloud === "aws" ? "hashicorp/aws" : "hashicorp/google";
334
+ const providerBlock = cloud === "azure"
335
+ ? 'provider "azurerm" {\n features {}\n}'
336
+ : cloud === "aws"
337
+ ? 'provider "aws" {\n region = var.region\n}'
338
+ : 'provider "google" {\n project = var.project_id\n region = var.region\n}';
339
+ const blocks = [];
340
+ const outputs = [];
341
+ if (cloud === "azure") {
342
+ blocks.push(`resource "azurerm_resource_group" "main" {\n name = "\${var.project_name}-\${var.environment}-rg"\n location = var.location\n}`);
343
+ outputs.push({ name: "resource_group_name", description: "Resource group name", value: "azurerm_resource_group.main.name" });
344
+ }
345
+ for (const component of components) {
346
+ const rendered = this.componentResources(cloud, component);
347
+ if (rendered) {
348
+ blocks.push(`# ── Module: ${component.module} (${component.service}) ──\n\n${rendered.hcl}`);
349
+ outputs.push(...rendered.outputs);
350
+ }
351
+ else {
352
+ blocks.push(`# ── Module: ${component.module} ──\n# No ${cloud} template exists for module "${component.module}" — define its resources in a dedicated .tf file.`);
353
+ }
354
+ }
355
+ const mainTf = `terraform {\n required_version = ">= 1.5"\n required_providers {\n ${providerName} = {\n source = "${providerSource}"\n }\n }\n}\n\n${providerBlock}\n\n${blocks.join("\n\n")}`;
356
+ return { mainTf, outputs };
357
+ }
358
+ componentResources(cloud, c) {
359
+ const key = `${c.module}:${c.service}`;
360
+ if (cloud === "azure")
361
+ return this.azureComponent(key);
362
+ if (cloud === "aws")
363
+ return this.awsComponent(key);
364
+ return this.gcpComponent(key);
365
+ }
366
+ azureComponent(key) {
367
+ switch (key) {
368
+ case "networking:network":
369
+ return {
370
+ hcl: `resource "azurerm_virtual_network" "network" {\n name = "\${var.project_name}-\${var.environment}-vnet"\n address_space = ["10.0.0.0/16"]\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n}\n\nresource "azurerm_subnet" "app" {\n name = "app"\n resource_group_name = azurerm_resource_group.main.name\n virtual_network_name = azurerm_virtual_network.network.name\n address_prefixes = ["10.0.1.0/24"]\n}`,
371
+ outputs: [{ name: "network_id", description: "Virtual network ID", value: "azurerm_virtual_network.network.id" }],
372
+ };
373
+ case "compute:container":
374
+ return {
375
+ hcl: `resource "azurerm_container_app_environment" "main" {\n name = "\${var.project_name}-\${var.environment}-cae"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n}\n\nresource "azurerm_container_app" "app" {\n name = "\${var.project_name}-\${var.environment}-app"\n container_app_environment_id = azurerm_container_app_environment.main.id\n resource_group_name = azurerm_resource_group.main.name\n revision_mode = "Single"\n\n template {\n container {\n name = var.project_name\n image = var.container_image\n cpu = 0.25\n memory = "0.5Gi"\n }\n }\n\n ingress {\n external_enabled = true\n target_port = var.container_port\n }\n}`,
376
+ outputs: [{ name: "app_fqdn", description: "Container app FQDN", value: "azurerm_container_app.app.latest_revision_fqdn" }],
377
+ };
378
+ case "compute:kubernetes":
379
+ return {
380
+ hcl: `resource "azurerm_kubernetes_cluster" "k8s" {\n name = "\${var.project_name}-\${var.environment}-aks"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n dns_prefix = var.project_name\n\n default_node_pool {\n name = "default"\n node_count = 1\n vm_size = "Standard_B2s"\n }\n\n identity {\n type = "SystemAssigned"\n }\n}`,
381
+ outputs: [{ name: "kubernetes_cluster_name", description: "AKS cluster name", value: "azurerm_kubernetes_cluster.k8s.name" }],
382
+ };
383
+ case "compute:serverless":
384
+ return {
385
+ hcl: `resource "azurerm_storage_account" "function" {\n name = replace("\${var.project_name}\${var.environment}fn", "-", "")\n resource_group_name = azurerm_resource_group.main.name\n location = azurerm_resource_group.main.location\n account_tier = "Standard"\n account_replication_type = "LRS"\n}\n\nresource "azurerm_service_plan" "function" {\n name = "\${var.project_name}-\${var.environment}-plan"\n resource_group_name = azurerm_resource_group.main.name\n location = azurerm_resource_group.main.location\n os_type = "Linux"\n sku_name = "Y1"\n}\n\nresource "azurerm_linux_function_app" "app" {\n name = "\${var.project_name}-\${var.environment}-func"\n resource_group_name = azurerm_resource_group.main.name\n location = azurerm_resource_group.main.location\n service_plan_id = azurerm_service_plan.function.id\n storage_account_name = azurerm_storage_account.function.name\n storage_account_access_key = azurerm_storage_account.function.primary_access_key\n\n site_config {}\n}`,
386
+ outputs: [{ name: "function_app_name", description: "Function app name", value: "azurerm_linux_function_app.app.name" }],
387
+ };
388
+ case "database:postgres":
389
+ return {
390
+ hcl: `resource "azurerm_postgresql_flexible_server" "postgres" {\n name = "\${var.project_name}-\${var.environment}-pg"\n resource_group_name = azurerm_resource_group.main.name\n location = azurerm_resource_group.main.location\n version = "16"\n administrator_login = var.db_admin_username\n administrator_password = var.db_admin_password\n storage_mb = 32768\n sku_name = "B_Standard_B1ms"\n}\n\nresource "azurerm_postgresql_flexible_server_database" "app" {\n name = var.project_name\n server_id = azurerm_postgresql_flexible_server.postgres.id\n}`,
391
+ outputs: [{ name: "postgres_fqdn", description: "PostgreSQL server FQDN", value: "azurerm_postgresql_flexible_server.postgres.fqdn" }],
392
+ };
393
+ case "database:mysql":
394
+ return {
395
+ hcl: `resource "azurerm_mysql_flexible_server" "mysql" {\n name = "\${var.project_name}-\${var.environment}-mysql"\n resource_group_name = azurerm_resource_group.main.name\n location = azurerm_resource_group.main.location\n administrator_login = var.db_admin_username\n administrator_password = var.db_admin_password\n sku_name = "B_Standard_B1ms"\n}`,
396
+ outputs: [{ name: "mysql_fqdn", description: "MySQL server FQDN", value: "azurerm_mysql_flexible_server.mysql.fqdn" }],
397
+ };
398
+ case "database:nosql":
399
+ return {
400
+ hcl: `resource "azurerm_cosmosdb_account" "nosql" {\n name = "\${var.project_name}-\${var.environment}-cosmos"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n offer_type = "Standard"\n kind = "GlobalDocumentDB"\n\n consistency_policy {\n consistency_level = "Session"\n }\n\n geo_location {\n location = azurerm_resource_group.main.location\n failover_priority = 0\n }\n}`,
401
+ outputs: [{ name: "nosql_endpoint", description: "Cosmos DB endpoint", value: "azurerm_cosmosdb_account.nosql.endpoint" }],
402
+ };
403
+ case "database:sqlserver":
404
+ return {
405
+ hcl: `resource "azurerm_mssql_server" "mssql" {\n name = "\${var.project_name}-\${var.environment}-sql"\n resource_group_name = azurerm_resource_group.main.name\n location = azurerm_resource_group.main.location\n version = "12.0"\n administrator_login = var.db_admin_username\n administrator_login_password = var.db_admin_password\n}\n\nresource "azurerm_mssql_database" "app" {\n name = var.project_name\n server_id = azurerm_mssql_server.mssql.id\n sku_name = "Basic"\n}`,
406
+ outputs: [{ name: "sqlserver_fqdn", description: "SQL Server FQDN", value: "azurerm_mssql_server.mssql.fully_qualified_domain_name" }],
407
+ };
408
+ case "cache:redis":
409
+ return {
410
+ hcl: `resource "azurerm_redis_cache" "redis" {\n name = "\${var.project_name}-\${var.environment}-redis"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n capacity = 0\n family = "C"\n sku_name = "Basic"\n minimum_tls_version = "1.2"\n}`,
411
+ outputs: [{ name: "redis_hostname", description: "Redis cache hostname", value: "azurerm_redis_cache.redis.hostname" }],
412
+ };
413
+ case "storage:object":
414
+ return {
415
+ hcl: `resource "azurerm_storage_account" "storage" {\n name = replace("\${var.project_name}\${var.environment}st", "-", "")\n resource_group_name = azurerm_resource_group.main.name\n location = azurerm_resource_group.main.location\n account_tier = "Standard"\n account_replication_type = "LRS"\n}\n\nresource "azurerm_storage_container" "app" {\n name = "app-data"\n storage_account_name = azurerm_storage_account.storage.name\n container_access_type = "private"\n}`,
416
+ outputs: [{ name: "storage_account_id", description: "Storage account ID", value: "azurerm_storage_account.storage.id" }],
417
+ };
418
+ case "messaging:queue":
419
+ return {
420
+ hcl: `resource "azurerm_servicebus_namespace" "queue" {\n name = "\${var.project_name}-\${var.environment}-bus"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n sku = "Basic"\n}\n\nresource "azurerm_servicebus_queue" "app" {\n name = "\${var.project_name}-queue"\n namespace_id = azurerm_servicebus_namespace.queue.id\n}`,
421
+ outputs: [{ name: "servicebus_namespace_id", description: "Service Bus namespace ID", value: "azurerm_servicebus_namespace.queue.id" }],
422
+ };
423
+ case "identity:identity":
424
+ return {
425
+ hcl: `resource "azurerm_user_assigned_identity" "app" {\n name = "\${var.project_name}-\${var.environment}-id"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n}`,
426
+ outputs: [{ name: "identity_client_id", description: "Managed identity client ID", value: "azurerm_user_assigned_identity.app.client_id" }],
427
+ };
428
+ case "monitoring:logs":
429
+ return {
430
+ hcl: `resource "azurerm_log_analytics_workspace" "logs" {\n name = "\${var.project_name}-\${var.environment}-logs"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n sku = "PerGB2018"\n retention_in_days = 30\n}\n\nresource "azurerm_application_insights" "app" {\n name = "\${var.project_name}-\${var.environment}-appi"\n location = azurerm_resource_group.main.location\n resource_group_name = azurerm_resource_group.main.name\n workspace_id = azurerm_log_analytics_workspace.logs.id\n application_type = "web"\n}`,
431
+ outputs: [{ name: "log_analytics_workspace_id", description: "Log Analytics workspace ID", value: "azurerm_log_analytics_workspace.logs.id" }],
432
+ };
433
+ default:
434
+ return null;
435
+ }
436
+ }
437
+ awsComponent(key) {
438
+ switch (key) {
439
+ case "networking:network":
440
+ return {
441
+ hcl: `resource "aws_vpc" "network" {\n cidr_block = "10.0.0.0/16"\n enable_dns_support = true\n enable_dns_hostnames = true\n\n tags = {\n Name = "\${var.project_name}-\${var.environment}"\n }\n}\n\nresource "aws_subnet" "app" {\n vpc_id = aws_vpc.network.id\n cidr_block = "10.0.1.0/24"\n\n tags = {\n Name = "\${var.project_name}-\${var.environment}-app"\n }\n}`,
442
+ outputs: [{ name: "vpc_id", description: "VPC ID", value: "aws_vpc.network.id" }],
443
+ };
444
+ case "compute:container":
445
+ return {
446
+ hcl: `resource "aws_ecs_cluster" "app" {\n name = "\${var.project_name}-\${var.environment}"\n}\n\nresource "aws_ecs_task_definition" "app" {\n family = "\${var.project_name}-\${var.environment}"\n requires_compatibilities = ["FARGATE"]\n network_mode = "awsvpc"\n cpu = "256"\n memory = "512"\n\n container_definitions = jsonencode([\n {\n name = var.project_name\n image = var.container_image\n essential = true\n portMappings = [\n {\n containerPort = var.container_port\n }\n ]\n }\n ])\n}`,
447
+ outputs: [{ name: "ecs_cluster_arn", description: "ECS cluster ARN", value: "aws_ecs_cluster.app.arn" }],
448
+ };
449
+ case "compute:kubernetes":
450
+ return {
451
+ hcl: `resource "aws_iam_role" "eks" {\n name = "\${var.project_name}-\${var.environment}-eks"\n\n assume_role_policy = jsonencode({\n Version = "2012-10-17"\n Statement = [\n {\n Action = "sts:AssumeRole"\n Effect = "Allow"\n Principal = { Service = "eks.amazonaws.com" }\n }\n ]\n })\n}\n\nresource "aws_eks_cluster" "k8s" {\n name = "\${var.project_name}-\${var.environment}"\n role_arn = aws_iam_role.eks.arn\n\n vpc_config {\n subnet_ids = var.subnet_ids\n }\n}`,
452
+ outputs: [{ name: "kubernetes_cluster_name", description: "EKS cluster name", value: "aws_eks_cluster.k8s.name" }],
453
+ };
454
+ case "compute:serverless":
455
+ return {
456
+ hcl: `resource "aws_iam_role" "lambda" {\n name = "\${var.project_name}-\${var.environment}-lambda"\n\n assume_role_policy = jsonencode({\n Version = "2012-10-17"\n Statement = [\n {\n Action = "sts:AssumeRole"\n Effect = "Allow"\n Principal = { Service = "lambda.amazonaws.com" }\n }\n ]\n })\n}\n\nresource "aws_lambda_function" "app" {\n function_name = "\${var.project_name}-\${var.environment}"\n role = aws_iam_role.lambda.arn\n package_type = "Image"\n image_uri = var.container_image\n}`,
457
+ outputs: [{ name: "lambda_function_name", description: "Lambda function name", value: "aws_lambda_function.app.function_name" }],
458
+ };
459
+ case "database:postgres":
460
+ return {
461
+ hcl: `resource "aws_db_instance" "postgres" {\n identifier = "\${var.project_name}-\${var.environment}-pg"\n engine = "postgres"\n engine_version = "16"\n instance_class = "db.t3.micro"\n allocated_storage = 20\n db_name = replace(var.project_name, "-", "")\n username = var.db_admin_username\n password = var.db_admin_password\n skip_final_snapshot = true\n}`,
462
+ outputs: [{ name: "postgres_endpoint", description: "PostgreSQL endpoint", value: "aws_db_instance.postgres.endpoint" }],
463
+ };
464
+ case "database:mysql":
465
+ return {
466
+ hcl: `resource "aws_db_instance" "mysql" {\n identifier = "\${var.project_name}-\${var.environment}-mysql"\n engine = "mysql"\n engine_version = "8.0"\n instance_class = "db.t3.micro"\n allocated_storage = 20\n db_name = replace(var.project_name, "-", "")\n username = var.db_admin_username\n password = var.db_admin_password\n skip_final_snapshot = true\n}`,
467
+ outputs: [{ name: "mysql_endpoint", description: "MySQL endpoint", value: "aws_db_instance.mysql.endpoint" }],
468
+ };
469
+ case "database:nosql":
470
+ return {
471
+ hcl: `resource "aws_dynamodb_table" "nosql" {\n name = "\${var.project_name}-\${var.environment}"\n billing_mode = "PAY_PER_REQUEST"\n hash_key = "id"\n\n attribute {\n name = "id"\n type = "S"\n }\n}`,
472
+ outputs: [{ name: "dynamodb_table_name", description: "DynamoDB table name", value: "aws_dynamodb_table.nosql.name" }],
473
+ };
474
+ case "database:sqlserver":
475
+ return {
476
+ hcl: `resource "aws_db_instance" "mssql" {\n identifier = "\${var.project_name}-\${var.environment}-sql"\n engine = "sqlserver-ex"\n instance_class = "db.t3.small"\n allocated_storage = 20\n license_model = "license-included"\n username = var.db_admin_username\n password = var.db_admin_password\n skip_final_snapshot = true\n}`,
477
+ outputs: [{ name: "sqlserver_endpoint", description: "SQL Server endpoint", value: "aws_db_instance.mssql.endpoint" }],
478
+ };
479
+ case "cache:redis":
480
+ return {
481
+ hcl: `resource "aws_elasticache_cluster" "redis" {\n cluster_id = "\${var.project_name}-\${var.environment}-redis"\n engine = "redis"\n node_type = "cache.t3.micro"\n num_cache_nodes = 1\n}`,
482
+ outputs: [{ name: "redis_endpoint", description: "Redis endpoint address", value: "aws_elasticache_cluster.redis.cache_nodes[0].address" }],
483
+ };
484
+ case "storage:object":
485
+ return {
486
+ hcl: `resource "aws_s3_bucket" "storage" {\n bucket = "\${var.project_name}-\${var.environment}-data"\n}\n\nresource "aws_s3_bucket_versioning" "storage" {\n bucket = aws_s3_bucket.storage.id\n\n versioning_configuration {\n status = "Enabled"\n }\n}`,
487
+ outputs: [{ name: "s3_bucket_name", description: "S3 bucket name", value: "aws_s3_bucket.storage.bucket" }],
488
+ };
489
+ case "messaging:queue":
490
+ return {
491
+ hcl: `resource "aws_sqs_queue" "queue" {\n name = "\${var.project_name}-\${var.environment}-queue"\n}`,
492
+ outputs: [{ name: "sqs_queue_url", description: "SQS queue URL", value: "aws_sqs_queue.queue.url" }],
493
+ };
494
+ case "identity:identity":
495
+ return {
496
+ hcl: `resource "aws_iam_role" "app" {\n name = "\${var.project_name}-\${var.environment}-app"\n\n assume_role_policy = jsonencode({\n Version = "2012-10-17"\n Statement = [\n {\n Action = "sts:AssumeRole"\n Effect = "Allow"\n Principal = { Service = "ecs-tasks.amazonaws.com" }\n }\n ]\n })\n}`,
497
+ outputs: [{ name: "app_role_arn", description: "Application IAM role ARN", value: "aws_iam_role.app.arn" }],
498
+ };
499
+ case "monitoring:logs":
500
+ return {
501
+ hcl: `resource "aws_cloudwatch_log_group" "logs" {\n name = "/app/\${var.project_name}-\${var.environment}"\n retention_in_days = 30\n}`,
502
+ outputs: [{ name: "log_group_name", description: "CloudWatch log group name", value: "aws_cloudwatch_log_group.logs.name" }],
503
+ };
504
+ default:
505
+ return null;
126
506
  }
127
- return modules.length > 0 ? modules : ["compute", "networking"];
128
507
  }
129
- generateMainTf(cloud, modules) {
130
- const provider = cloud === "azure" ? 'provider "azurerm" {\n features {}\n}' : cloud === "aws" ? 'provider "aws" {\n region = var.region\n}' : 'provider "google" {\n project = var.project_id\n}';
131
- const resources = modules.map(m => `# Module: ${m}\n# TODO: Add ${m} resources`).join("\n\n");
132
- return `terraform {\n required_version = ">= 1.5"\n required_providers {\n ${cloud === "azure" ? 'azurerm' : cloud === "aws" ? 'aws' : 'google'} = {\n source = "${cloud === "azure" ? 'hashicorp/azurerm' : cloud === "aws" ? 'hashicorp/aws' : 'hashicorp/google'}"\n }\n }\n}\n\n${provider}\n\n${resources}`;
508
+ gcpComponent(key) {
509
+ switch (key) {
510
+ case "networking:network":
511
+ return {
512
+ hcl: `resource "google_compute_network" "network" {\n name = "\${var.project_name}-\${var.environment}"\n auto_create_subnetworks = false\n}\n\nresource "google_compute_subnetwork" "app" {\n name = "\${var.project_name}-\${var.environment}-app"\n network = google_compute_network.network.id\n ip_cidr_range = "10.0.1.0/24"\n region = var.region\n}`,
513
+ outputs: [{ name: "network_id", description: "VPC network ID", value: "google_compute_network.network.id" }],
514
+ };
515
+ case "compute:container":
516
+ case "compute:serverless":
517
+ // Cloud Run is GCP's serverless container platform — it covers both flavors.
518
+ return {
519
+ hcl: `resource "google_cloud_run_v2_service" "app" {\n name = "\${var.project_name}-\${var.environment}"\n location = var.region\n\n template {\n containers {\n image = var.container_image\n\n ports {\n container_port = var.container_port\n }\n }\n }\n}`,
520
+ outputs: [{ name: "cloud_run_url", description: "Cloud Run service URL", value: "google_cloud_run_v2_service.app.uri" }],
521
+ };
522
+ case "compute:kubernetes":
523
+ return {
524
+ hcl: `resource "google_container_cluster" "k8s" {\n name = "\${var.project_name}-\${var.environment}"\n location = var.region\n enable_autopilot = true\n}`,
525
+ outputs: [{ name: "kubernetes_cluster_name", description: "GKE cluster name", value: "google_container_cluster.k8s.name" }],
526
+ };
527
+ case "database:postgres":
528
+ return {
529
+ hcl: `resource "google_sql_database_instance" "postgres" {\n name = "\${var.project_name}-\${var.environment}-pg"\n database_version = "POSTGRES_16"\n region = var.region\n\n settings {\n tier = "db-f1-micro"\n }\n}\n\nresource "google_sql_user" "postgres" {\n name = var.db_admin_username\n instance = google_sql_database_instance.postgres.name\n password = var.db_admin_password\n}`,
530
+ outputs: [{ name: "postgres_connection_name", description: "Cloud SQL connection name", value: "google_sql_database_instance.postgres.connection_name" }],
531
+ };
532
+ case "database:mysql":
533
+ return {
534
+ hcl: `resource "google_sql_database_instance" "mysql" {\n name = "\${var.project_name}-\${var.environment}-mysql"\n database_version = "MYSQL_8_0"\n region = var.region\n\n settings {\n tier = "db-f1-micro"\n }\n}\n\nresource "google_sql_user" "mysql" {\n name = var.db_admin_username\n instance = google_sql_database_instance.mysql.name\n password = var.db_admin_password\n}`,
535
+ outputs: [{ name: "mysql_connection_name", description: "Cloud SQL connection name", value: "google_sql_database_instance.mysql.connection_name" }],
536
+ };
537
+ case "database:nosql":
538
+ return {
539
+ hcl: `resource "google_firestore_database" "nosql" {\n name = "\${var.project_name}-\${var.environment}"\n location_id = var.region\n type = "FIRESTORE_NATIVE"\n}`,
540
+ outputs: [{ name: "firestore_database_name", description: "Firestore database name", value: "google_firestore_database.nosql.name" }],
541
+ };
542
+ case "database:sqlserver":
543
+ return {
544
+ hcl: `resource "google_sql_database_instance" "mssql" {\n name = "\${var.project_name}-\${var.environment}-sql"\n database_version = "SQLSERVER_2019_EXPRESS"\n region = var.region\n root_password = var.db_admin_password\n\n settings {\n tier = "db-custom-1-3840"\n }\n}`,
545
+ outputs: [{ name: "sqlserver_connection_name", description: "Cloud SQL connection name", value: "google_sql_database_instance.mssql.connection_name" }],
546
+ };
547
+ case "cache:redis":
548
+ return {
549
+ hcl: `resource "google_redis_instance" "redis" {\n name = "\${var.project_name}-\${var.environment}-redis"\n memory_size_gb = 1\n region = var.region\n}`,
550
+ outputs: [{ name: "redis_host", description: "Redis instance host", value: "google_redis_instance.redis.host" }],
551
+ };
552
+ case "storage:object":
553
+ return {
554
+ hcl: `resource "google_storage_bucket" "storage" {\n name = "\${var.project_name}-\${var.environment}-data"\n location = var.region\n uniform_bucket_level_access = true\n}`,
555
+ outputs: [{ name: "bucket_name", description: "Storage bucket name", value: "google_storage_bucket.storage.name" }],
556
+ };
557
+ case "messaging:queue":
558
+ return {
559
+ hcl: `resource "google_pubsub_topic" "queue" {\n name = "\${var.project_name}-\${var.environment}"\n}\n\nresource "google_pubsub_subscription" "app" {\n name = "\${var.project_name}-\${var.environment}-sub"\n topic = google_pubsub_topic.queue.id\n}`,
560
+ outputs: [{ name: "pubsub_topic_id", description: "Pub/Sub topic ID", value: "google_pubsub_topic.queue.id" }],
561
+ };
562
+ case "identity:identity":
563
+ return {
564
+ hcl: `resource "google_service_account" "app" {\n account_id = "\${var.project_name}-app"\n display_name = "\${var.project_name} application"\n}`,
565
+ outputs: [{ name: "service_account_email", description: "Service account email", value: "google_service_account.app.email" }],
566
+ };
567
+ case "monitoring:logs":
568
+ return {
569
+ hcl: `resource "google_logging_metric" "logs" {\n name = "\${var.project_name}-\${var.environment}-errors"\n filter = "severity>=ERROR"\n\n metric_descriptor {\n metric_kind = "DELTA"\n value_type = "INT64"\n }\n}`,
570
+ outputs: [{ name: "logging_metric_name", description: "Log-based metric name", value: "google_logging_metric.logs.name" }],
571
+ };
572
+ default:
573
+ return null;
574
+ }
133
575
  }
134
- generateVariables(cloud, _modules) {
576
+ generateVariables(cloud, components) {
135
577
  const vars = [
136
578
  { name: "project_name", type: "string", description: "Project name", required: true },
137
579
  { name: "environment", type: "string", description: "Environment (dev/staging/prod)", default: '"dev"', required: false },
@@ -140,13 +582,39 @@ export class IacGenerator {
140
582
  vars.push({ name: "location", type: "string", description: "Azure region", default: '"eastus2"', required: false });
141
583
  if (cloud === "aws")
142
584
  vars.push({ name: "region", type: "string", description: "AWS region", default: '"us-east-1"', required: false });
585
+ if (cloud === "gcp") {
586
+ vars.push({ name: "project_id", type: "string", description: "GCP project ID", required: true });
587
+ vars.push({ name: "region", type: "string", description: "GCP region", default: '"us-central1"', required: false });
588
+ }
589
+ const needsContainerVars = components.some(c => c.module === "compute" && (c.service === "container" || (c.service === "serverless" && cloud !== "azure")));
590
+ if (needsContainerVars) {
591
+ vars.push({ name: "container_image", type: "string", description: "Container image to deploy", default: '"nginx:latest"', required: false });
592
+ vars.push({ name: "container_port", type: "number", description: "Port the application listens on", default: "3000", required: false });
593
+ }
594
+ const needsDbCredentials = components.some(c => c.module === "database" && ["postgres", "mysql", "sqlserver"].includes(c.service));
595
+ if (needsDbCredentials) {
596
+ vars.push({ name: "db_admin_username", type: "string", description: "Database administrator login", default: '"sddadmin"', required: false });
597
+ vars.push({ name: "db_admin_password", type: "string", description: "Database administrator password (sensitive)", required: true });
598
+ }
599
+ if (cloud === "aws" && components.some(c => c.module === "compute" && c.service === "kubernetes")) {
600
+ vars.push({ name: "subnet_ids", type: "list(string)", description: "Subnet IDs for the EKS cluster", required: true });
601
+ }
143
602
  return vars;
144
603
  }
145
604
  generateVariablesTf(vars) {
146
- return vars.map(v => `variable "${v.name}" {\n type = ${v.type}\n description = "${v.description}"\n${v.default ? ` default = ${v.default}\n` : ""}}`).join("\n\n");
605
+ return vars
606
+ .map(v => {
607
+ const sensitive = /password|secret/.test(v.name) ? " sensitive = true\n" : "";
608
+ return `variable "${v.name}" {\n type = ${v.type}\n description = "${v.description}"\n${sensitive}${v.default ? ` default = ${v.default}\n` : ""}}`;
609
+ })
610
+ .join("\n\n");
147
611
  }
148
- generateOutputsTf(modules) {
149
- return modules.map(m => `output "${m}_id" {\n description = "${m} resource ID"\n value = "" # TODO\n}`).join("\n\n");
612
+ generateOutputsTf(outputs) {
613
+ if (outputs.length === 0)
614
+ return "# No outputs defined for the detected components.";
615
+ return outputs
616
+ .map(o => `output "${o.name}" {\n description = "${o.description}"\n value = ${o.value}\n}`)
617
+ .join("\n\n");
150
618
  }
151
619
  }
152
620
  //# sourceMappingURL=iac-generator.js.map