@iacmp/cli 1.1.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/LICENSE +21 -0
- package/bin/run.js +73 -0
- package/dist/chat.js +4068 -0
- package/dist/commands/ai.js +3985 -0
- package/dist/commands/audit-all.js +1039 -0
- package/dist/commands/audit-dr.js +351 -0
- package/dist/commands/audit-ha.js +375 -0
- package/dist/commands/audit-improvements.js +373 -0
- package/dist/commands/audit-security.js +351 -0
- package/dist/commands/dashboard.js +417 -0
- package/dist/commands/deploy.js +188 -0
- package/dist/commands/destroy.js +194 -0
- package/dist/commands/diagram.js +896 -0
- package/dist/commands/diff.js +4420 -0
- package/dist/commands/doctor.js +191 -0
- package/dist/commands/init.js +507 -0
- package/dist/commands/ls.js +75 -0
- package/dist/commands/registry.js +170 -0
- package/dist/commands/registry.json +29 -0
- package/dist/commands/synth.js +4458 -0
- package/dist/commands/watch.js +133 -0
- package/dist/index.js +30 -0
- package/oclif.manifest.json +727 -0
- package/package.json +95 -0
|
@@ -0,0 +1,896 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/commands/diagram.ts
|
|
31
|
+
var diagram_exports = {};
|
|
32
|
+
__export(diagram_exports, {
|
|
33
|
+
default: () => Diagram
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(diagram_exports);
|
|
36
|
+
var import_core = require("@oclif/core");
|
|
37
|
+
var import_chalk = __toESM(require("chalk"));
|
|
38
|
+
var fs3 = __toESM(require("fs"));
|
|
39
|
+
var path2 = __toESM(require("path"));
|
|
40
|
+
|
|
41
|
+
// src/audit.ts
|
|
42
|
+
var fs2 = __toESM(require("fs"));
|
|
43
|
+
var path = __toESM(require("path"));
|
|
44
|
+
|
|
45
|
+
// src/utils.ts
|
|
46
|
+
var fs = __toESM(require("fs"));
|
|
47
|
+
function readJsonFile(filePath) {
|
|
48
|
+
let content;
|
|
49
|
+
try {
|
|
50
|
+
content = fs.readFileSync(filePath, "utf-8");
|
|
51
|
+
} catch (e) {
|
|
52
|
+
throw new Error(`Falha ao ler '${filePath}': ${errMessage(e)}`);
|
|
53
|
+
}
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(content);
|
|
56
|
+
} catch (e) {
|
|
57
|
+
throw new Error(`JSON inv\xE1lido em '${filePath}': ${errMessage(e)}`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function errMessage(e) {
|
|
61
|
+
if (e instanceof Error) return e.message;
|
|
62
|
+
if (typeof e === "string") return e;
|
|
63
|
+
try {
|
|
64
|
+
return JSON.stringify(e);
|
|
65
|
+
} catch {
|
|
66
|
+
return String(e);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// src/audit.ts
|
|
71
|
+
function readConfig(cwd) {
|
|
72
|
+
const configPath = path.join(cwd, "iacmp.json");
|
|
73
|
+
if (!fs2.existsSync(configPath)) throw new Error("iacmp.json n\xE3o encontrado. Rode: iacmp init");
|
|
74
|
+
const config = readJsonFile(configPath);
|
|
75
|
+
return {
|
|
76
|
+
name: config.name ?? path.basename(cwd),
|
|
77
|
+
provider: config.provider ?? "aws"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
function resolveTsNode(projectDir) {
|
|
81
|
+
const dirs = [];
|
|
82
|
+
let dir = projectDir;
|
|
83
|
+
for (let i = 0; i < 5; i++) {
|
|
84
|
+
dirs.push(dir);
|
|
85
|
+
const parent = path.dirname(dir);
|
|
86
|
+
if (parent === dir) break;
|
|
87
|
+
dir = parent;
|
|
88
|
+
}
|
|
89
|
+
for (const d of dirs) {
|
|
90
|
+
const tsNodePath = path.join(d, "node_modules", "ts-node");
|
|
91
|
+
if (fs2.existsSync(tsNodePath)) return tsNodePath;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
function findStackFiles(dir) {
|
|
96
|
+
const entries = fs2.readdirSync(dir, { withFileTypes: true });
|
|
97
|
+
const files = [];
|
|
98
|
+
for (const entry of entries) {
|
|
99
|
+
const full = path.join(dir, entry.name);
|
|
100
|
+
if (entry.isDirectory()) {
|
|
101
|
+
files.push(...findStackFiles(full));
|
|
102
|
+
} else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
|
|
103
|
+
files.push(full);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return files;
|
|
107
|
+
}
|
|
108
|
+
function loadStacks(cwd) {
|
|
109
|
+
const stacksDir = path.join(cwd, "stacks");
|
|
110
|
+
if (!fs2.existsSync(stacksDir)) throw new Error("Diret\xF3rio stacks/ n\xE3o encontrado.");
|
|
111
|
+
const stackFiles = findStackFiles(stacksDir);
|
|
112
|
+
if (stackFiles.length === 0) throw new Error("Nenhuma stack encontrada em stacks/");
|
|
113
|
+
const tsNodePath = resolveTsNode(cwd);
|
|
114
|
+
if (tsNodePath) {
|
|
115
|
+
require(tsNodePath).register({
|
|
116
|
+
transpileOnly: true,
|
|
117
|
+
skipProject: true,
|
|
118
|
+
compilerOptions: {
|
|
119
|
+
target: "ES2022",
|
|
120
|
+
module: "commonjs",
|
|
121
|
+
moduleResolution: "node",
|
|
122
|
+
esModuleInterop: true,
|
|
123
|
+
strict: false,
|
|
124
|
+
skipLibCheck: true
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
const result = [];
|
|
129
|
+
for (const stackPath of stackFiles) {
|
|
130
|
+
const stackName = path.basename(stackPath).replace(/\.(ts|js)$/, "");
|
|
131
|
+
try {
|
|
132
|
+
const mod = require(stackPath);
|
|
133
|
+
const raw = mod.default ?? mod.stack ?? mod;
|
|
134
|
+
if (!raw || typeof raw !== "object" || !("constructs" in raw)) {
|
|
135
|
+
console.warn(`[audit] ${path.basename(stackPath)} n\xE3o exporta uma Stack v\xE1lida \u2014 ignorado.`);
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
result.push({ name: stackName, stack: raw });
|
|
139
|
+
} catch (err) {
|
|
140
|
+
console.warn(`[audit] falha ao carregar ${path.basename(stackPath)}: ${errMessage(err)}`);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return result;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// src/diagram/builder.ts
|
|
147
|
+
var TYPE_META = {
|
|
148
|
+
"Compute.Instance": { emoji: "\u2699\uFE0F", technology: "Virtual Machine" },
|
|
149
|
+
"Compute.AutoScaling": { emoji: "\u2699\uFE0F", technology: "Auto Scaling Group" },
|
|
150
|
+
"Compute.Container": { emoji: "\u{1F4E6}", technology: "Container" },
|
|
151
|
+
"Compute.Kubernetes": { emoji: "\u2638\uFE0F", technology: "Kubernetes" },
|
|
152
|
+
"Storage.Bucket": { emoji: "\u{1F5C2}\uFE0F", technology: "Object Storage" },
|
|
153
|
+
"Storage.FileSystem": { emoji: "\u{1F5C4}\uFE0F", technology: "File System" },
|
|
154
|
+
"Storage.Archive": { emoji: "\u{1F5C3}\uFE0F", technology: "Archive Storage" },
|
|
155
|
+
"Network.VPC": { emoji: "\u{1F310}", technology: "Virtual Network" },
|
|
156
|
+
"Network.Subnet": { emoji: "\u{1F500}", technology: "Subnet" },
|
|
157
|
+
"Network.SecurityGroup": { emoji: "\u{1F6E1}\uFE0F", technology: "Security Group" },
|
|
158
|
+
"Network.WAF": { emoji: "\u{1F512}", technology: "WAF" },
|
|
159
|
+
"Network.LoadBalancer": { emoji: "\u2696\uFE0F", technology: "Load Balancer" },
|
|
160
|
+
"Network.CDN": { emoji: "\u{1F30D}", technology: "CDN" },
|
|
161
|
+
"Network.Dns": { emoji: "\u{1F310}", technology: "DNS" },
|
|
162
|
+
"Database.SQL": { emoji: "\u{1F5C4}\uFE0F", technology: "Relational DB" },
|
|
163
|
+
"Database.DocumentDB": { emoji: "\u{1F4C4}", technology: "Document DB" },
|
|
164
|
+
"Database.DynamoDB": { emoji: "\u26A1", technology: "NoSQL Database" },
|
|
165
|
+
"Cache.Redis": { emoji: "\u26A1", technology: "Redis Cache" },
|
|
166
|
+
"Cache.Memcached": { emoji: "\u26A1", technology: "Memcached Cache" },
|
|
167
|
+
"Function.Lambda": { emoji: "\u26A1", technology: "Serverless" },
|
|
168
|
+
"Function.ApiGateway": { emoji: "\u{1F50C}", technology: "API Gateway" },
|
|
169
|
+
"Policy.IAM": { emoji: "\u{1F511}", technology: "IAM Policy" },
|
|
170
|
+
"Events.EventBridge": { emoji: "\u{1F4E1}", technology: "Event Bus" },
|
|
171
|
+
"Workflow.StepFunctions": { emoji: "\u{1F504}", technology: "Step Functions" },
|
|
172
|
+
"Messaging.Queue": { emoji: "\u{1F4E8}", technology: "Queue" },
|
|
173
|
+
"Messaging.Topic": { emoji: "\u{1F4E2}", technology: "Topic" },
|
|
174
|
+
"Secret.Vault": { emoji: "\u{1F510}", technology: "Secrets Manager" },
|
|
175
|
+
"Certificate.TLS": { emoji: "\u{1F50F}", technology: "TLS Certificate" },
|
|
176
|
+
"Monitoring.Alarm": { emoji: "\u{1F6A8}", technology: "Monitoring Alarm" },
|
|
177
|
+
"Monitoring.Dashboard": { emoji: "\u{1F4CA}", technology: "Monitoring Dashboard" },
|
|
178
|
+
"Logging.Stream": { emoji: "\u{1F4CB}", technology: "Log Stream" }
|
|
179
|
+
};
|
|
180
|
+
var PROVIDER_TECH_OVERRIDE = {
|
|
181
|
+
aws: {
|
|
182
|
+
"Compute.Container": "Container (ECS/Fargate)",
|
|
183
|
+
"Compute.Kubernetes": "Kubernetes (EKS)",
|
|
184
|
+
"Storage.FileSystem": "File System (EFS)",
|
|
185
|
+
"Storage.Archive": "Archive (Glacier)",
|
|
186
|
+
"Network.CDN": "CDN (CloudFront)",
|
|
187
|
+
"Network.Dns": "DNS (Route53)",
|
|
188
|
+
"Database.DynamoDB": "DynamoDB",
|
|
189
|
+
"Function.ApiGateway": "API Gateway",
|
|
190
|
+
"Messaging.Queue": "Queue (SQS)",
|
|
191
|
+
"Messaging.Topic": "Topic (SNS)",
|
|
192
|
+
"Certificate.TLS": "TLS Certificate (ACM)",
|
|
193
|
+
"Monitoring.Alarm": "CloudWatch Alarm",
|
|
194
|
+
"Monitoring.Dashboard": "CloudWatch Dashboard",
|
|
195
|
+
"Logging.Stream": "CloudWatch Logs"
|
|
196
|
+
},
|
|
197
|
+
azure: {
|
|
198
|
+
"Compute.Container": "Container Instances",
|
|
199
|
+
"Compute.Kubernetes": "Kubernetes Service (AKS)",
|
|
200
|
+
"Storage.FileSystem": "Azure Files",
|
|
201
|
+
"Storage.Archive": "Archive Storage",
|
|
202
|
+
"Network.CDN": "CDN Profile",
|
|
203
|
+
"Network.Dns": "DNS Zone",
|
|
204
|
+
"Database.DynamoDB": "Table Storage",
|
|
205
|
+
"Function.ApiGateway": "API Management",
|
|
206
|
+
"Messaging.Queue": "Queue (Service Bus)",
|
|
207
|
+
"Messaging.Topic": "Topic (Service Bus)",
|
|
208
|
+
"Certificate.TLS": "TLS Certificate (Key Vault)",
|
|
209
|
+
"Monitoring.Alarm": "Monitor Alert",
|
|
210
|
+
"Monitoring.Dashboard": "Monitor Dashboard",
|
|
211
|
+
"Logging.Stream": "Log Analytics"
|
|
212
|
+
},
|
|
213
|
+
gcp: {
|
|
214
|
+
"Compute.Container": "Cloud Run",
|
|
215
|
+
"Compute.Kubernetes": "Kubernetes Engine (GKE)",
|
|
216
|
+
"Storage.FileSystem": "Filestore",
|
|
217
|
+
"Storage.Archive": "Archive Storage",
|
|
218
|
+
"Network.CDN": "Cloud CDN",
|
|
219
|
+
"Network.Dns": "Cloud DNS",
|
|
220
|
+
"Database.DynamoDB": "Bigtable",
|
|
221
|
+
"Function.ApiGateway": "Cloud Endpoints",
|
|
222
|
+
"Messaging.Queue": "Pub/Sub Queue",
|
|
223
|
+
"Messaging.Topic": "Pub/Sub Topic",
|
|
224
|
+
"Certificate.TLS": "TLS Certificate",
|
|
225
|
+
"Monitoring.Alarm": "Cloud Monitoring Alert",
|
|
226
|
+
"Monitoring.Dashboard": "Cloud Monitoring Dashboard",
|
|
227
|
+
"Logging.Stream": "Cloud Logging"
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
function safeId(raw) {
|
|
231
|
+
return raw.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
232
|
+
}
|
|
233
|
+
function describeProps(c) {
|
|
234
|
+
const p = c.props;
|
|
235
|
+
const parts = [];
|
|
236
|
+
if (c.type === "Compute.Instance") {
|
|
237
|
+
if (p.instanceType) parts.push(`size: ${p.instanceType}`);
|
|
238
|
+
if (p.image) parts.push(`image: ${p.image}`);
|
|
239
|
+
}
|
|
240
|
+
if (c.type === "Storage.Bucket") {
|
|
241
|
+
parts.push(p.versioning ? "versioning: on" : "versioning: off");
|
|
242
|
+
parts.push(p.publicAccess ? "public" : "private");
|
|
243
|
+
}
|
|
244
|
+
if (c.type === "Network.VPC") {
|
|
245
|
+
if (p.cidr) parts.push(`cidr: ${p.cidr}`);
|
|
246
|
+
if (p.maxAzs) parts.push(`maxAzs: ${p.maxAzs}`);
|
|
247
|
+
}
|
|
248
|
+
if (c.type === "Database.SQL") {
|
|
249
|
+
if (p.engine) parts.push(`engine: ${p.engine}`);
|
|
250
|
+
if (p.multiAz) parts.push("Multi-AZ");
|
|
251
|
+
if (p.instanceType) parts.push(`size: ${p.instanceType}`);
|
|
252
|
+
}
|
|
253
|
+
if (c.type === "Function.Lambda") {
|
|
254
|
+
if (p.runtime) parts.push(`runtime: ${p.runtime}`);
|
|
255
|
+
if (p.memory) parts.push(`memory: ${p.memory}MB`);
|
|
256
|
+
if (p.handler) parts.push(`handler: ${p.handler}`);
|
|
257
|
+
}
|
|
258
|
+
if (c.type === "Network.SecurityGroup") {
|
|
259
|
+
const ingress = p.ingressRules ?? [];
|
|
260
|
+
if (ingress.length > 0) parts.push(`${ingress.length} ingress rules`);
|
|
261
|
+
}
|
|
262
|
+
if (c.type === "Network.WAF") {
|
|
263
|
+
const rules = p.rules ?? [];
|
|
264
|
+
parts.push(`${rules.length} rules`);
|
|
265
|
+
if (p.scope) parts.push(`scope: ${p.scope}`);
|
|
266
|
+
}
|
|
267
|
+
if (c.type === "Cache.Redis") {
|
|
268
|
+
if (p.nodeType) parts.push(`size: ${p.nodeType}`);
|
|
269
|
+
if (p.numCacheNodes) parts.push(`nodes: ${p.numCacheNodes}`);
|
|
270
|
+
}
|
|
271
|
+
if (c.type === "Database.DocumentDB") {
|
|
272
|
+
if (p.instances) parts.push(`instances: ${p.instances}`);
|
|
273
|
+
}
|
|
274
|
+
if (c.type === "Policy.IAM") {
|
|
275
|
+
if (p.attachTo) parts.push(`attachTo: ${p.attachTo}`);
|
|
276
|
+
if (p.attachType) parts.push(`type: ${p.attachType}`);
|
|
277
|
+
}
|
|
278
|
+
if (c.type === "Events.EventBridge") {
|
|
279
|
+
const rules = p.rules ?? [];
|
|
280
|
+
if (rules.length > 0) parts.push(`${rules.length} rules`);
|
|
281
|
+
}
|
|
282
|
+
if (c.type === "Workflow.StepFunctions") {
|
|
283
|
+
const steps = p.steps ?? [];
|
|
284
|
+
parts.push(`${steps.length} steps`);
|
|
285
|
+
if (p.type) parts.push(p.type);
|
|
286
|
+
}
|
|
287
|
+
if (c.type === "Messaging.Queue") {
|
|
288
|
+
if (p.fifo) parts.push("FIFO");
|
|
289
|
+
if (p.encrypted) parts.push("encrypted");
|
|
290
|
+
if (p.dlqArn) parts.push("DLQ configured");
|
|
291
|
+
}
|
|
292
|
+
if (c.type === "Messaging.Topic") {
|
|
293
|
+
if (p.fifo) parts.push("FIFO");
|
|
294
|
+
const subs = p.subscriptions ?? [];
|
|
295
|
+
if (subs.length > 0) parts.push(`${subs.length} subscriptions`);
|
|
296
|
+
}
|
|
297
|
+
if (c.type === "Compute.AutoScaling") {
|
|
298
|
+
if (p.minCapacity !== void 0) parts.push(`min: ${p.minCapacity}`);
|
|
299
|
+
if (p.maxCapacity !== void 0) parts.push(`max: ${p.maxCapacity}`);
|
|
300
|
+
if (p.targetCpuUtilization) parts.push(`cpu: ${p.targetCpuUtilization}%`);
|
|
301
|
+
}
|
|
302
|
+
if (c.type === "Compute.Container") {
|
|
303
|
+
if (p.image) parts.push(`image: ${p.image}`);
|
|
304
|
+
if (p.cpu) parts.push(`cpu: ${p.cpu}`);
|
|
305
|
+
if (p.memory) parts.push(`mem: ${p.memory}MB`);
|
|
306
|
+
}
|
|
307
|
+
if (c.type === "Compute.Kubernetes") {
|
|
308
|
+
if (p.version) parts.push(`k8s: ${p.version}`);
|
|
309
|
+
if (p.desiredNodes) parts.push(`nodes: ${p.desiredNodes}`);
|
|
310
|
+
if (p.nodeInstanceType) parts.push(`size: ${p.nodeInstanceType}`);
|
|
311
|
+
}
|
|
312
|
+
if (c.type === "Storage.FileSystem") {
|
|
313
|
+
if (p.performanceMode) parts.push(`perf: ${p.performanceMode}`);
|
|
314
|
+
if (p.encrypted) parts.push("encrypted");
|
|
315
|
+
const aps = p.accessPoints ?? [];
|
|
316
|
+
if (aps.length > 0) parts.push(`${aps.length} access points`);
|
|
317
|
+
}
|
|
318
|
+
if (c.type === "Storage.Archive") {
|
|
319
|
+
if (p.retentionDays) parts.push(`retention: ${p.retentionDays}d`);
|
|
320
|
+
if (p.lockEnabled) parts.push("lock enabled");
|
|
321
|
+
}
|
|
322
|
+
if (c.type === "Network.LoadBalancer") {
|
|
323
|
+
if (p.type) parts.push(`type: ${p.type}`);
|
|
324
|
+
if (p.scheme) parts.push(p.scheme);
|
|
325
|
+
const tgs = p.targetGroups ?? [];
|
|
326
|
+
if (tgs.length > 0) parts.push(`${tgs.length} target groups`);
|
|
327
|
+
}
|
|
328
|
+
if (c.type === "Network.CDN") {
|
|
329
|
+
const origins = p.origins ?? [];
|
|
330
|
+
if (origins.length > 0) parts.push(`${origins.length} origins`);
|
|
331
|
+
if (p.priceClass) parts.push(p.priceClass);
|
|
332
|
+
}
|
|
333
|
+
if (c.type === "Network.Dns") {
|
|
334
|
+
if (p.zoneName) parts.push(`zone: ${p.zoneName}`);
|
|
335
|
+
const records = p.records ?? [];
|
|
336
|
+
if (records.length > 0) parts.push(`${records.length} records`);
|
|
337
|
+
}
|
|
338
|
+
if (c.type === "Database.DynamoDB") {
|
|
339
|
+
if (p.partitionKey) parts.push(`pk: ${p.partitionKey}`);
|
|
340
|
+
if (p.billingMode) parts.push(p.billingMode);
|
|
341
|
+
if (p.streamEnabled) parts.push("streams on");
|
|
342
|
+
}
|
|
343
|
+
if (c.type === "Cache.Memcached") {
|
|
344
|
+
if (p.nodeType) parts.push(`size: ${p.nodeType}`);
|
|
345
|
+
if (p.numCacheNodes) parts.push(`nodes: ${p.numCacheNodes}`);
|
|
346
|
+
}
|
|
347
|
+
if (c.type === "Function.ApiGateway") {
|
|
348
|
+
if (p.type) parts.push(`type: ${p.type}`);
|
|
349
|
+
if (p.stageName) parts.push(`stage: ${p.stageName}`);
|
|
350
|
+
const routes = p.routes ?? [];
|
|
351
|
+
if (routes.length > 0) parts.push(`${routes.length} routes`);
|
|
352
|
+
}
|
|
353
|
+
if (c.type === "Secret.Vault") {
|
|
354
|
+
if (p.rotationDays) parts.push(`rotation: ${p.rotationDays}d`);
|
|
355
|
+
if (p.kmsKeyId) parts.push("KMS encrypted");
|
|
356
|
+
}
|
|
357
|
+
if (c.type === "Certificate.TLS") {
|
|
358
|
+
if (p.domainName) parts.push(`domain: ${p.domainName}`);
|
|
359
|
+
if (p.validationMethod) parts.push(p.validationMethod);
|
|
360
|
+
}
|
|
361
|
+
if (c.type === "Monitoring.Alarm") {
|
|
362
|
+
if (p.metricName) parts.push(`metric: ${p.metricName}`);
|
|
363
|
+
if (p.threshold !== void 0) parts.push(`threshold: ${p.threshold}`);
|
|
364
|
+
}
|
|
365
|
+
if (c.type === "Monitoring.Dashboard") {
|
|
366
|
+
const widgets = p.widgets ?? [];
|
|
367
|
+
parts.push(`${widgets.length} widgets`);
|
|
368
|
+
}
|
|
369
|
+
if (c.type === "Logging.Stream") {
|
|
370
|
+
if (p.retentionDays) parts.push(`retention: ${p.retentionDays}d`);
|
|
371
|
+
const filters = p.subscriptionFilters ?? [];
|
|
372
|
+
if (filters.length > 0) parts.push(`${filters.length} filters`);
|
|
373
|
+
}
|
|
374
|
+
return parts.join(", ");
|
|
375
|
+
}
|
|
376
|
+
function buildStackDiagram(name, stack, provider) {
|
|
377
|
+
const techOverride = PROVIDER_TECH_OVERRIDE[provider] ?? {};
|
|
378
|
+
const nodes = stack.constructs.map((c) => {
|
|
379
|
+
const meta = TYPE_META[c.type] ?? { emoji: "\u25A1", technology: c.type };
|
|
380
|
+
const technology = techOverride[c.type] ?? meta.technology;
|
|
381
|
+
return {
|
|
382
|
+
id: safeId(`${name}_${c.id}`),
|
|
383
|
+
label: c.id,
|
|
384
|
+
constructType: c.type,
|
|
385
|
+
technology,
|
|
386
|
+
description: describeProps(c),
|
|
387
|
+
props: c.props
|
|
388
|
+
};
|
|
389
|
+
});
|
|
390
|
+
const relationships = [];
|
|
391
|
+
const vpcs = nodes.filter((n) => n.constructType === "Network.VPC");
|
|
392
|
+
const lambdas = nodes.filter((n) => n.constructType === "Function.Lambda");
|
|
393
|
+
const databases = nodes.filter((n) => n.constructType === "Database.SQL");
|
|
394
|
+
if (vpcs.length === 1) {
|
|
395
|
+
const vpcId = vpcs[0].id;
|
|
396
|
+
for (const node of nodes) {
|
|
397
|
+
if (node.id === vpcId) continue;
|
|
398
|
+
relationships.push({ sourceId: vpcId, targetId: node.id, label: "inferred", inferred: true });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (lambdas.length > 0 && databases.length > 0) {
|
|
402
|
+
for (const lambda of lambdas) {
|
|
403
|
+
for (const db of databases) {
|
|
404
|
+
relationships.push({ sourceId: lambda.id, targetId: db.id, label: "reads", inferred: true });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
const apiGateways = stack.constructs.filter((c) => c.type === "Function.ApiGateway");
|
|
409
|
+
for (const gw of apiGateways) {
|
|
410
|
+
const routes = gw.props?.routes ?? [];
|
|
411
|
+
const gwNode = nodes.find((n) => n.label === gw.id);
|
|
412
|
+
if (!gwNode) continue;
|
|
413
|
+
for (const route of routes) {
|
|
414
|
+
if (!route.lambdaId) continue;
|
|
415
|
+
const lambdaNode = nodes.find((n) => n.label === route.lambdaId);
|
|
416
|
+
if (lambdaNode) {
|
|
417
|
+
relationships.push({ sourceId: gwNode.id, targetId: lambdaNode.id, label: "invokes", inferred: false });
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
const authorizerLambdaId = gw.props?.authorizerLambdaId;
|
|
421
|
+
if (authorizerLambdaId) {
|
|
422
|
+
const authorizerNode = nodes.find((n) => n.label === authorizerLambdaId);
|
|
423
|
+
if (authorizerNode) {
|
|
424
|
+
relationships.push({ sourceId: gwNode.id, targetId: authorizerNode.id, label: "authorizes", inferred: false });
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
return { name, nodes, relationships };
|
|
429
|
+
}
|
|
430
|
+
function inferCrossStackRelationships(builtStacks) {
|
|
431
|
+
const relationships = [];
|
|
432
|
+
const nodeById = {};
|
|
433
|
+
for (const s of builtStacks) {
|
|
434
|
+
for (const n of s.nodes) nodeById[n.id] = n;
|
|
435
|
+
}
|
|
436
|
+
const ENV_HINTS = [
|
|
437
|
+
{ pattern: /TABLE_NAME|DYNAMO|DYNAMODB/i, targetType: "Database.DynamoDB", label: "reads table" },
|
|
438
|
+
{ pattern: /DB_HOST|DB_URL|DATABASE_URL/i, targetType: "Database.SQL", label: "connects db" },
|
|
439
|
+
{ pattern: /BUCKET_NAME|S3_BUCKET/i, targetType: "Storage.Bucket", label: "reads bucket" },
|
|
440
|
+
{ pattern: /VPC_ID|VPC_CIDR/i, targetType: "Network.VPC", label: "uses vpc" },
|
|
441
|
+
{ pattern: /REDIS_URL|REDIS_HOST|CACHE_URL/i, targetType: "Cache.Redis", label: "uses cache" },
|
|
442
|
+
{ pattern: /MEMCACHED_URL|MEMCACHE_HOST/i, targetType: "Cache.Memcached", label: "uses memcached" },
|
|
443
|
+
{ pattern: /DOCDB_URL|MONGO_URL/i, targetType: "Database.DocumentDB", label: "reads docdb" },
|
|
444
|
+
{ pattern: /QUEUE_URL|SQS_URL/i, targetType: "Messaging.Queue", label: "sends to queue" },
|
|
445
|
+
{ pattern: /TOPIC_ARN|SNS_ARN/i, targetType: "Messaging.Topic", label: "publishes to" },
|
|
446
|
+
{ pattern: /API_URL|API_ENDPOINT|APIGW/i, targetType: "Function.ApiGateway", label: "calls api" },
|
|
447
|
+
{ pattern: /SECRET_ARN|SECRETS_MANAGER/i, targetType: "Secret.Vault", label: "reads secret" },
|
|
448
|
+
{ pattern: /EFS_ID|FILESYSTEM_ID/i, targetType: "Storage.FileSystem", label: "mounts filesystem" },
|
|
449
|
+
{ pattern: /LOG_GROUP|LOG_STREAM/i, targetType: "Logging.Stream", label: "writes logs" }
|
|
450
|
+
];
|
|
451
|
+
const ENV_CAPABLE_TYPES = /* @__PURE__ */ new Set([
|
|
452
|
+
"Function.Lambda",
|
|
453
|
+
"Compute.Container",
|
|
454
|
+
"Compute.Instance",
|
|
455
|
+
"Compute.AutoScaling"
|
|
456
|
+
]);
|
|
457
|
+
for (const srcStack of builtStacks) {
|
|
458
|
+
for (const srcNode of srcStack.nodes) {
|
|
459
|
+
if (!ENV_CAPABLE_TYPES.has(srcNode.constructType)) continue;
|
|
460
|
+
const env = srcNode.props?.environment;
|
|
461
|
+
if (!env || Object.keys(env).length === 0) continue;
|
|
462
|
+
for (const hint of ENV_HINTS) {
|
|
463
|
+
const matched = Object.keys(env).some((k) => hint.pattern.test(k));
|
|
464
|
+
if (!matched) continue;
|
|
465
|
+
for (const tgtStack of builtStacks) {
|
|
466
|
+
if (tgtStack.name === srcStack.name) continue;
|
|
467
|
+
for (const tgtNode of tgtStack.nodes) {
|
|
468
|
+
if (tgtNode.constructType !== hint.targetType) continue;
|
|
469
|
+
relationships.push({
|
|
470
|
+
sourceId: srcNode.id,
|
|
471
|
+
targetId: tgtNode.id,
|
|
472
|
+
label: hint.label,
|
|
473
|
+
inferred: true
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
for (const srcStack of builtStacks) {
|
|
481
|
+
for (const srcNode of srcStack.nodes) {
|
|
482
|
+
if (srcNode.constructType !== "Function.ApiGateway") continue;
|
|
483
|
+
const routes = srcNode.props?.routes ?? [];
|
|
484
|
+
for (const route of routes) {
|
|
485
|
+
if (!route.lambdaId) continue;
|
|
486
|
+
for (const tgtStack of builtStacks) {
|
|
487
|
+
if (tgtStack.name === srcStack.name) continue;
|
|
488
|
+
const tgtNode = tgtStack.nodes.find((n) => n.label === route.lambdaId);
|
|
489
|
+
if (tgtNode) {
|
|
490
|
+
relationships.push({ sourceId: srcNode.id, targetId: tgtNode.id, label: "invokes", inferred: false });
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
const authorizerLambdaId = srcNode.props?.authorizerLambdaId;
|
|
495
|
+
if (authorizerLambdaId) {
|
|
496
|
+
for (const tgtStack of builtStacks) {
|
|
497
|
+
if (tgtStack.name === srcStack.name) continue;
|
|
498
|
+
const tgtNode = tgtStack.nodes.find((n) => n.label === authorizerLambdaId);
|
|
499
|
+
if (tgtNode) {
|
|
500
|
+
relationships.push({ sourceId: srcNode.id, targetId: tgtNode.id, label: "authorizes", inferred: false });
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
return relationships;
|
|
507
|
+
}
|
|
508
|
+
function buildModel(projectName, provider, region, stacks) {
|
|
509
|
+
const builtStacks = stacks.map(({ name, stack }) => buildStackDiagram(name, stack, provider));
|
|
510
|
+
const seenNodeIds = /* @__PURE__ */ new Set();
|
|
511
|
+
for (const s of builtStacks) {
|
|
512
|
+
s.nodes = s.nodes.filter((n) => {
|
|
513
|
+
if (seenNodeIds.has(n.id)) return false;
|
|
514
|
+
seenNodeIds.add(n.id);
|
|
515
|
+
return true;
|
|
516
|
+
});
|
|
517
|
+
s.relationships = s.relationships.filter(
|
|
518
|
+
(r) => seenNodeIds.has(r.sourceId) && seenNodeIds.has(r.targetId)
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
const mergedByName = /* @__PURE__ */ new Map();
|
|
522
|
+
for (const s of builtStacks) {
|
|
523
|
+
const existing = mergedByName.get(s.name);
|
|
524
|
+
if (existing) {
|
|
525
|
+
existing.nodes.push(...s.nodes);
|
|
526
|
+
existing.relationships.push(...s.relationships);
|
|
527
|
+
} else {
|
|
528
|
+
mergedByName.set(s.name, { name: s.name, nodes: [...s.nodes], relationships: [...s.relationships] });
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
const mergedStacks = [...mergedByName.values()];
|
|
532
|
+
const nonEmptyStacks = mergedStacks.filter((s) => s.nodes.length > 0);
|
|
533
|
+
const crossRelationships = inferCrossStackRelationships(builtStacks);
|
|
534
|
+
for (const rel of crossRelationships) {
|
|
535
|
+
for (const s of nonEmptyStacks) {
|
|
536
|
+
if (s.nodes.some((n) => n.id === rel.sourceId)) {
|
|
537
|
+
s.relationships.push(rel);
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return {
|
|
543
|
+
projectName,
|
|
544
|
+
provider,
|
|
545
|
+
region,
|
|
546
|
+
stacks: nonEmptyStacks
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
// src/diagram/structurizr.ts
|
|
551
|
+
var AWS_TYPE_TAG = {
|
|
552
|
+
"Compute.Instance": "Amazon Web Services - EC2 Instance",
|
|
553
|
+
"Compute.AutoScaling": "Amazon Web Services - EC2 Auto Scaling",
|
|
554
|
+
"Compute.Container": "Amazon Web Services - Elastic Container Service",
|
|
555
|
+
"Compute.Kubernetes": "Amazon Web Services - Elastic Kubernetes Service",
|
|
556
|
+
"Storage.Bucket": "Amazon Web Services - Simple Storage Service",
|
|
557
|
+
"Storage.FileSystem": "Amazon Web Services - EFS",
|
|
558
|
+
"Storage.Archive": "Amazon Web Services - Simple Storage Service Glacier",
|
|
559
|
+
"Network.VPC": "Amazon Web Services - VPC Virtual private cloud VPC",
|
|
560
|
+
"Network.Subnet": "Amazon Web Services - VPC Virtual private cloud VPC",
|
|
561
|
+
"Network.SecurityGroup": "Amazon Web Services - VPC Virtual private cloud VPC",
|
|
562
|
+
"Network.WAF": "Amazon Web Services - WAF",
|
|
563
|
+
"Network.LoadBalancer": "Amazon Web Services - Elastic Load Balancing",
|
|
564
|
+
"Network.CDN": "Amazon Web Services - CloudFront",
|
|
565
|
+
"Network.Dns": "Amazon Web Services - Route 53",
|
|
566
|
+
"Database.SQL": "Amazon Web Services - RDS",
|
|
567
|
+
"Database.DocumentDB": "Amazon Web Services - DocumentDB",
|
|
568
|
+
"Database.DynamoDB": "Amazon Web Services - DynamoDB",
|
|
569
|
+
"Cache.Redis": "Amazon Web Services - ElastiCache ElastiCache for Redis",
|
|
570
|
+
"Cache.Memcached": "Amazon Web Services - ElastiCache ElastiCache for Memcached",
|
|
571
|
+
"Function.Lambda": "Amazon Web Services - Lambda",
|
|
572
|
+
"Function.ApiGateway": "Amazon Web Services - API Gateway",
|
|
573
|
+
"Policy.IAM": "Amazon Web Services - Identity and Access Management",
|
|
574
|
+
"Events.EventBridge": "Amazon Web Services - EventBridge",
|
|
575
|
+
"Workflow.StepFunctions": "Amazon Web Services - Step Functions",
|
|
576
|
+
"Messaging.Queue": "Amazon Web Services - Simple Queue Service Queue",
|
|
577
|
+
"Messaging.Topic": "Amazon Web Services - Simple Notification Service Topic",
|
|
578
|
+
"Secret.Vault": "Amazon Web Services - Secrets Manager",
|
|
579
|
+
"Certificate.TLS": "Amazon Web Services - Certificate Manager",
|
|
580
|
+
"Monitoring.Alarm": "Amazon Web Services - CloudWatch Alarm",
|
|
581
|
+
"Monitoring.Dashboard": "Amazon Web Services - CloudWatch",
|
|
582
|
+
"Logging.Stream": "Amazon Web Services - CloudWatch Logs"
|
|
583
|
+
};
|
|
584
|
+
var AZURE_TYPE_TAG = {
|
|
585
|
+
"Compute.Instance": "Microsoft Azure - Virtual Machine",
|
|
586
|
+
"Compute.AutoScaling": "Microsoft Azure - VM Scale Sets",
|
|
587
|
+
"Compute.Container": "Microsoft Azure - Container Instances",
|
|
588
|
+
"Compute.Kubernetes": "Microsoft Azure - Kubernetes Services",
|
|
589
|
+
"Storage.Bucket": "Microsoft Azure - Storage Accounts",
|
|
590
|
+
"Storage.FileSystem": "Microsoft Azure - Files",
|
|
591
|
+
"Storage.Archive": "Microsoft Azure - Storage Accounts",
|
|
592
|
+
"Network.VPC": "Microsoft Azure - Virtual Networks",
|
|
593
|
+
"Network.Subnet": "Microsoft Azure - Subnet",
|
|
594
|
+
"Network.SecurityGroup": "Microsoft Azure - Network Security Groups",
|
|
595
|
+
"Network.LoadBalancer": "Microsoft Azure - Load Balancers",
|
|
596
|
+
"Network.WAF": "Microsoft Azure - Application Gateways",
|
|
597
|
+
"Network.CDN": "Microsoft Azure - CDN Profiles",
|
|
598
|
+
"Network.Dns": "Microsoft Azure - DNS Zones",
|
|
599
|
+
"Database.SQL": "Microsoft Azure - SQL Database",
|
|
600
|
+
"Database.DocumentDB": "Microsoft Azure - Azure Cosmos DB",
|
|
601
|
+
"Database.DynamoDB": "Microsoft Azure - Table",
|
|
602
|
+
"Cache.Redis": "Microsoft Azure - Cache Redis",
|
|
603
|
+
"Cache.Memcached": "Microsoft Azure - Cache Redis",
|
|
604
|
+
"Function.Lambda": "Microsoft Azure - Function Apps",
|
|
605
|
+
"Function.ApiGateway": "Microsoft Azure - API Management Services",
|
|
606
|
+
"Policy.IAM": "Microsoft Azure - Azure Active Directory",
|
|
607
|
+
"Events.EventBridge": "Microsoft Azure - Event Grid Topics",
|
|
608
|
+
"Workflow.StepFunctions": "Microsoft Azure - Logic Apps",
|
|
609
|
+
"Messaging.Queue": "Microsoft Azure - Azure Service Bus",
|
|
610
|
+
"Messaging.Topic": "Microsoft Azure - Azure Service Bus",
|
|
611
|
+
"Secret.Vault": "Microsoft Azure - Key Vaults",
|
|
612
|
+
"Certificate.TLS": "Microsoft Azure - Key Vaults",
|
|
613
|
+
"Monitoring.Alarm": "Microsoft Azure - Monitor",
|
|
614
|
+
"Monitoring.Dashboard": "Microsoft Azure - Monitor",
|
|
615
|
+
"Logging.Stream": "Microsoft Azure - Monitor"
|
|
616
|
+
};
|
|
617
|
+
var GCP_TYPE_TAG = {
|
|
618
|
+
"Compute.Instance": "Google Cloud Platform - Compute Engine",
|
|
619
|
+
"Compute.AutoScaling": "Google Cloud Platform - Compute Engine",
|
|
620
|
+
"Compute.Container": "Google Cloud Platform - Cloud Run",
|
|
621
|
+
"Compute.Kubernetes": "Google Cloud Platform - Kubernetes Engine",
|
|
622
|
+
"Storage.Bucket": "Google Cloud Platform - Cloud Storage",
|
|
623
|
+
"Storage.FileSystem": "Google Cloud Platform - Cloud Filestore",
|
|
624
|
+
"Storage.Archive": "Google Cloud Platform - Cloud Storage",
|
|
625
|
+
"Network.VPC": "Google Cloud Platform - Virtual Private Cloud",
|
|
626
|
+
"Network.Subnet": "Google Cloud Platform - Virtual Private Cloud",
|
|
627
|
+
"Network.SecurityGroup": "Google Cloud Platform - Cloud Firewall Rules",
|
|
628
|
+
"Network.WAF": "Google Cloud Platform - Cloud Armor",
|
|
629
|
+
"Network.LoadBalancer": "Google Cloud Platform - Cloud Load Balancing",
|
|
630
|
+
"Network.CDN": "Google Cloud Platform - Cloud CDN",
|
|
631
|
+
"Network.Dns": "Google Cloud Platform - Cloud DNS",
|
|
632
|
+
"Database.SQL": "Google Cloud Platform - Cloud SQL",
|
|
633
|
+
"Database.DocumentDB": "Google Cloud Platform - Cloud Firestore",
|
|
634
|
+
"Database.DynamoDB": "Google Cloud Platform - Cloud Bigtable",
|
|
635
|
+
"Cache.Redis": "Google Cloud Platform - Cloud Memorystore",
|
|
636
|
+
"Cache.Memcached": "Google Cloud Platform - Cloud Memorystore",
|
|
637
|
+
"Function.Lambda": "Google Cloud Platform - Cloud Functions",
|
|
638
|
+
"Function.ApiGateway": "Google Cloud Platform - Cloud Endpoints",
|
|
639
|
+
"Policy.IAM": "Google Cloud Platform - Cloud IAM",
|
|
640
|
+
"Events.EventBridge": "Google Cloud Platform - Cloud PubSub",
|
|
641
|
+
"Workflow.StepFunctions": "Google Cloud Platform - Cloud Tasks",
|
|
642
|
+
"Messaging.Queue": "Google Cloud Platform - Cloud PubSub",
|
|
643
|
+
"Messaging.Topic": "Google Cloud Platform - Cloud PubSub",
|
|
644
|
+
"Secret.Vault": "Google Cloud Platform - Key Management Service",
|
|
645
|
+
"Certificate.TLS": "Google Cloud Platform - Key Management Service",
|
|
646
|
+
"Monitoring.Alarm": "Google Cloud Platform - Monitoring",
|
|
647
|
+
"Monitoring.Dashboard": "Google Cloud Platform - Monitoring",
|
|
648
|
+
"Logging.Stream": "Google Cloud Platform - Logging"
|
|
649
|
+
};
|
|
650
|
+
var PROVIDER_THEME = {
|
|
651
|
+
"aws": "https://static.structurizr.com/themes/amazon-web-services-2023.01.31/theme.json",
|
|
652
|
+
"azure": "https://static.structurizr.com/themes/microsoft-azure-2023.01.24/theme.json",
|
|
653
|
+
"gcp": "https://static.structurizr.com/themes/google-cloud-platform-v1.5/theme.json",
|
|
654
|
+
"terraform": "https://static.structurizr.com/themes/amazon-web-services-2023.01.31/theme.json"
|
|
655
|
+
};
|
|
656
|
+
var PROVIDER_TAGS = {
|
|
657
|
+
"aws": AWS_TYPE_TAG,
|
|
658
|
+
"azure": AZURE_TYPE_TAG,
|
|
659
|
+
"gcp": GCP_TYPE_TAG,
|
|
660
|
+
"terraform": AWS_TYPE_TAG
|
|
661
|
+
};
|
|
662
|
+
function ind(n) {
|
|
663
|
+
return " ".repeat(n);
|
|
664
|
+
}
|
|
665
|
+
function escapeStructurizr(s) {
|
|
666
|
+
return s.replace(/"/g, "'").replace(/[\r\n]+/g, " ");
|
|
667
|
+
}
|
|
668
|
+
function containerBlock(node, depth, tagMap) {
|
|
669
|
+
const tag = tagMap[node.constructType] ?? "Resource";
|
|
670
|
+
const desc = node.description || "";
|
|
671
|
+
const lines = [
|
|
672
|
+
`${ind(depth)}${node.id} = container "${escapeStructurizr(node.label)}" "${escapeStructurizr(desc)}" "${escapeStructurizr(node.technology)}" {`,
|
|
673
|
+
`${ind(depth + 1)}tags "${escapeStructurizr(tag)}"`,
|
|
674
|
+
`${ind(depth)}}`
|
|
675
|
+
];
|
|
676
|
+
return lines.join("\n");
|
|
677
|
+
}
|
|
678
|
+
function renderStructurizr(model) {
|
|
679
|
+
const lines = [];
|
|
680
|
+
const themeUrl = PROVIDER_THEME[model.provider] ?? PROVIDER_THEME["aws"];
|
|
681
|
+
const tagMap = PROVIDER_TAGS[model.provider] ?? AWS_TYPE_TAG;
|
|
682
|
+
lines.push(`workspace "${escapeStructurizr(model.projectName)}" {`);
|
|
683
|
+
lines.push("");
|
|
684
|
+
lines.push(`${ind(1)}model {`);
|
|
685
|
+
lines.push(`${ind(2)}${sanitize(model.projectName)} = softwareSystem "${escapeStructurizr(model.projectName)}" "Provider: ${escapeStructurizr(model.provider)}, Region: ${escapeStructurizr(model.region)}" {`);
|
|
686
|
+
for (const stack of model.stacks) {
|
|
687
|
+
lines.push("");
|
|
688
|
+
lines.push(`${ind(3)}group "${escapeStructurizr(stack.name)}" {`);
|
|
689
|
+
for (const node of stack.nodes) {
|
|
690
|
+
lines.push(containerBlock(node, 4, tagMap));
|
|
691
|
+
}
|
|
692
|
+
lines.push(`${ind(3)}}`);
|
|
693
|
+
}
|
|
694
|
+
lines.push(`${ind(2)}}`);
|
|
695
|
+
for (const stack of model.stacks) {
|
|
696
|
+
for (const rel of stack.relationships) {
|
|
697
|
+
if (rel.inferred) {
|
|
698
|
+
lines.push(`${ind(2)}${rel.sourceId} -> ${rel.targetId} "[inferred]" "" "Inferred"`);
|
|
699
|
+
} else {
|
|
700
|
+
lines.push(`${ind(2)}${rel.sourceId} -> ${rel.targetId} "${escapeStructurizr(rel.label)}"`);
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
lines.push(`${ind(1)}}`);
|
|
705
|
+
lines.push("");
|
|
706
|
+
lines.push(`${ind(1)}views {`);
|
|
707
|
+
const sysId = sanitize(model.projectName);
|
|
708
|
+
for (const stack of model.stacks) {
|
|
709
|
+
const viewId = sanitize(`${stack.name}View`);
|
|
710
|
+
lines.push("");
|
|
711
|
+
lines.push(`${ind(2)}container ${sysId} "${viewId}" "${escapeStructurizr(stack.name)}" {`);
|
|
712
|
+
lines.push(`${ind(3)}include *`);
|
|
713
|
+
lines.push(`${ind(3)}autoLayout`);
|
|
714
|
+
lines.push(`${ind(2)}}`);
|
|
715
|
+
}
|
|
716
|
+
lines.push("");
|
|
717
|
+
lines.push(`${ind(2)}theme "${themeUrl}"`);
|
|
718
|
+
lines.push("");
|
|
719
|
+
lines.push(`${ind(2)}styles {`);
|
|
720
|
+
lines.push(`${ind(3)}relationship "Inferred" {`);
|
|
721
|
+
lines.push(`${ind(4)}dashed true`);
|
|
722
|
+
lines.push(`${ind(4)}colour #999999`);
|
|
723
|
+
lines.push(`${ind(3)}}`);
|
|
724
|
+
lines.push(`${ind(2)}}`);
|
|
725
|
+
lines.push(`${ind(1)}}`);
|
|
726
|
+
lines.push("}");
|
|
727
|
+
lines.push("");
|
|
728
|
+
return lines.join("\n");
|
|
729
|
+
}
|
|
730
|
+
function sanitize(s) {
|
|
731
|
+
return s.replace(/[^a-zA-Z0-9]/g, "_");
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
// src/diagram/mermaid.ts
|
|
735
|
+
function escapeMermaid(s) {
|
|
736
|
+
return s.replace(/&/g, "&").replace(/"/g, """).replace(/\[/g, "[").replace(/\]/g, "]");
|
|
737
|
+
}
|
|
738
|
+
var TYPE_EMOJI = {
|
|
739
|
+
"Compute.Instance": "\u2699\uFE0F",
|
|
740
|
+
"Storage.Bucket": "\u{1F5C2}\uFE0F",
|
|
741
|
+
"Network.VPC": "\u{1F310}",
|
|
742
|
+
"Database.SQL": "\u{1F5C4}\uFE0F",
|
|
743
|
+
"Function.Lambda": "\u26A1"
|
|
744
|
+
};
|
|
745
|
+
function nodeLabel(node) {
|
|
746
|
+
const emoji = TYPE_EMOJI[node.constructType] ?? "\u25A1";
|
|
747
|
+
const lines = [
|
|
748
|
+
`${emoji} ${escapeMermaid(node.label)}`,
|
|
749
|
+
escapeMermaid(node.constructType)
|
|
750
|
+
];
|
|
751
|
+
if (node.description) lines.push(escapeMermaid(node.description));
|
|
752
|
+
return `["${lines.join("<br/>")}"]`;
|
|
753
|
+
}
|
|
754
|
+
function escapeRelLabel(s) {
|
|
755
|
+
return escapeMermaid(s).replace(/\|/g, "|");
|
|
756
|
+
}
|
|
757
|
+
function renderMermaid(model) {
|
|
758
|
+
const sections = [];
|
|
759
|
+
sections.push(`# Diagramas de Arquitetura \u2014 ${model.projectName}`);
|
|
760
|
+
sections.push("");
|
|
761
|
+
sections.push(`**Provider:** ${model.provider} \xB7 **Region:** ${model.region}`);
|
|
762
|
+
sections.push("");
|
|
763
|
+
sections.push("---");
|
|
764
|
+
for (const stack of model.stacks) {
|
|
765
|
+
sections.push("");
|
|
766
|
+
sections.push(`## Stack: ${stack.name}`);
|
|
767
|
+
sections.push("");
|
|
768
|
+
sections.push("```mermaid");
|
|
769
|
+
sections.push("graph TD");
|
|
770
|
+
for (const node of stack.nodes) {
|
|
771
|
+
sections.push(` ${node.id}${nodeLabel(node)}`);
|
|
772
|
+
}
|
|
773
|
+
if (stack.relationships.length > 0) {
|
|
774
|
+
sections.push("");
|
|
775
|
+
for (const rel of stack.relationships) {
|
|
776
|
+
if (rel.inferred) {
|
|
777
|
+
sections.push(` ${rel.sourceId} -.->|inferred| ${rel.targetId}`);
|
|
778
|
+
} else {
|
|
779
|
+
sections.push(` ${rel.sourceId} -->|${escapeRelLabel(rel.label)}| ${rel.targetId}`);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
sections.push("```");
|
|
784
|
+
sections.push("");
|
|
785
|
+
sections.push("**Recursos:**");
|
|
786
|
+
sections.push("");
|
|
787
|
+
for (const node of stack.nodes) {
|
|
788
|
+
const emoji = TYPE_EMOJI[node.constructType] ?? "\u25A1";
|
|
789
|
+
const detail = node.description ? ` \u2014 ${node.description}` : "";
|
|
790
|
+
sections.push(`- ${emoji} **${node.label}** \`${node.constructType}\`${detail}`);
|
|
791
|
+
}
|
|
792
|
+
if (stack.relationships.some((r) => r.inferred)) {
|
|
793
|
+
sections.push("");
|
|
794
|
+
sections.push("> Setas tracejadas indicam rela\xE7\xF5es inferidas a partir da topologia da stack, n\xE3o declaradas explicitamente no c\xF3digo.");
|
|
795
|
+
}
|
|
796
|
+
sections.push("");
|
|
797
|
+
sections.push("---");
|
|
798
|
+
}
|
|
799
|
+
return sections.join("\n") + "\n";
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
// src/commands/diagram.ts
|
|
803
|
+
var FORMATS = ["structurizr", "mermaid"];
|
|
804
|
+
var OUTPUT_FILE = {
|
|
805
|
+
structurizr: "workspace.dsl",
|
|
806
|
+
mermaid: "workspace.md"
|
|
807
|
+
};
|
|
808
|
+
var Diagram = class _Diagram extends import_core.Command {
|
|
809
|
+
static description = "Gera diagramas de arquitetura a partir das stacks do projeto.";
|
|
810
|
+
static flags = {
|
|
811
|
+
format: import_core.Flags.string({
|
|
812
|
+
char: "f",
|
|
813
|
+
description: "Formato de sa\xEDda: structurizr, mermaid",
|
|
814
|
+
default: "structurizr"
|
|
815
|
+
}),
|
|
816
|
+
provider: import_core.Flags.string({
|
|
817
|
+
char: "p",
|
|
818
|
+
description: "Provider para o theme do diagrama: aws, azure, gcp, terraform (padr\xE3o: lido do iacmp.json)"
|
|
819
|
+
}),
|
|
820
|
+
stack: import_core.Flags.string({
|
|
821
|
+
char: "s",
|
|
822
|
+
description: "Nome de uma stack espec\xEDfica (padr\xE3o: todas)"
|
|
823
|
+
}),
|
|
824
|
+
out: import_core.Flags.string({
|
|
825
|
+
char: "o",
|
|
826
|
+
description: "Diret\xF3rio de sa\xEDda",
|
|
827
|
+
default: "diagrams"
|
|
828
|
+
})
|
|
829
|
+
};
|
|
830
|
+
static examples = [
|
|
831
|
+
"$ iacmp diagram",
|
|
832
|
+
"$ iacmp diagram --provider azure",
|
|
833
|
+
"$ iacmp diagram --provider gcp",
|
|
834
|
+
"$ iacmp diagram --format mermaid",
|
|
835
|
+
"$ iacmp diagram --stack database",
|
|
836
|
+
"$ iacmp diagram --provider azure --format mermaid"
|
|
837
|
+
];
|
|
838
|
+
async run() {
|
|
839
|
+
const { flags } = await this.parse(_Diagram);
|
|
840
|
+
const cwd = process.cwd();
|
|
841
|
+
if (!FORMATS.includes(flags.format)) {
|
|
842
|
+
this.error(`Formato '${flags.format}' inv\xE1lido. Use: ${FORMATS.join(", ")}`);
|
|
843
|
+
}
|
|
844
|
+
const format = flags.format;
|
|
845
|
+
let config;
|
|
846
|
+
try {
|
|
847
|
+
config = readConfig(cwd);
|
|
848
|
+
} catch (err) {
|
|
849
|
+
this.error(err.message);
|
|
850
|
+
}
|
|
851
|
+
let allStacks;
|
|
852
|
+
try {
|
|
853
|
+
allStacks = loadStacks(cwd);
|
|
854
|
+
} catch (err) {
|
|
855
|
+
this.error(err.message);
|
|
856
|
+
}
|
|
857
|
+
const stacks = flags.stack ? allStacks.filter((s) => s.name === flags.stack) : allStacks;
|
|
858
|
+
if (stacks.length === 0) {
|
|
859
|
+
this.error(
|
|
860
|
+
flags.stack ? `Stack '${flags.stack}' n\xE3o encontrada. Stacks dispon\xEDveis: ${allStacks.map((s) => s.name).join(", ")}` : "Nenhuma stack encontrada em stacks/"
|
|
861
|
+
);
|
|
862
|
+
}
|
|
863
|
+
const provider = flags.provider ?? config.provider;
|
|
864
|
+
const model = buildModel(config.name, provider, "us-east-1", stacks);
|
|
865
|
+
const content = format === "structurizr" ? renderStructurizr(model) : renderMermaid(model);
|
|
866
|
+
const outDir = path2.join(cwd, flags.out);
|
|
867
|
+
if (!fs3.existsSync(outDir)) fs3.mkdirSync(outDir, { recursive: true });
|
|
868
|
+
const outFile = path2.join(outDir, OUTPUT_FILE[format]);
|
|
869
|
+
fs3.writeFileSync(outFile, content, "utf-8");
|
|
870
|
+
const relOut = path2.join(flags.out, OUTPUT_FILE[format]);
|
|
871
|
+
this.log("");
|
|
872
|
+
this.log(import_chalk.default.bold("Diagrama gerado"));
|
|
873
|
+
this.log("\u2500".repeat(40));
|
|
874
|
+
this.log(`Projeto: ${config.name}`);
|
|
875
|
+
this.log(`Provider: ${provider}${flags.provider ? " (via --provider)" : ""}`);
|
|
876
|
+
this.log(`Formato: ${format}`);
|
|
877
|
+
this.log(`Stacks: ${model.stacks.map((s) => s.name).join(", ")}`);
|
|
878
|
+
this.log(`Nodes: ${model.stacks.reduce((n, s) => n + s.nodes.length, 0)}`);
|
|
879
|
+
this.log("");
|
|
880
|
+
for (const s of model.stacks) {
|
|
881
|
+
this.log(` ${import_chalk.default.green("\u2713")} ${s.name} \u2014 ${s.nodes.length} recurso(s)`);
|
|
882
|
+
for (const node of s.nodes) {
|
|
883
|
+
const desc = node.description ? import_chalk.default.dim(` \u2014 ${node.description}`) : "";
|
|
884
|
+
this.log(` ${node.label} ${import_chalk.default.dim(`(${node.constructType})`)}${desc}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
this.log("");
|
|
888
|
+
this.log(`Arquivo salvo em ${import_chalk.default.cyan(relOut)}`);
|
|
889
|
+
if (format === "structurizr") {
|
|
890
|
+
this.log(import_chalk.default.dim("\nAbra em: https://structurizr.com/dsl"));
|
|
891
|
+
} else {
|
|
892
|
+
this.log(import_chalk.default.dim("\nRenderizado automaticamente no GitHub, GitLab e Notion."));
|
|
893
|
+
}
|
|
894
|
+
this.log("");
|
|
895
|
+
}
|
|
896
|
+
};
|