@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.
@@ -0,0 +1,373 @@
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/audit-improvements.ts
31
+ var audit_improvements_exports = {};
32
+ __export(audit_improvements_exports, {
33
+ default: () => AuditImprovements
34
+ });
35
+ module.exports = __toCommonJS(audit_improvements_exports);
36
+ var import_core = require("@oclif/core");
37
+ var import_chalk = __toESM(require("chalk"));
38
+
39
+ // src/audit.ts
40
+ var fs2 = __toESM(require("fs"));
41
+ var path = __toESM(require("path"));
42
+
43
+ // src/utils.ts
44
+ var fs = __toESM(require("fs"));
45
+ function readJsonFile(filePath) {
46
+ let content;
47
+ try {
48
+ content = fs.readFileSync(filePath, "utf-8");
49
+ } catch (e) {
50
+ throw new Error(`Falha ao ler '${filePath}': ${errMessage(e)}`);
51
+ }
52
+ try {
53
+ return JSON.parse(content);
54
+ } catch (e) {
55
+ throw new Error(`JSON inv\xE1lido em '${filePath}': ${errMessage(e)}`);
56
+ }
57
+ }
58
+ function errMessage(e) {
59
+ if (e instanceof Error) return e.message;
60
+ if (typeof e === "string") return e;
61
+ try {
62
+ return JSON.stringify(e);
63
+ } catch {
64
+ return String(e);
65
+ }
66
+ }
67
+
68
+ // src/audit.ts
69
+ function readConfig(cwd) {
70
+ const configPath = path.join(cwd, "iacmp.json");
71
+ if (!fs2.existsSync(configPath)) throw new Error("iacmp.json n\xE3o encontrado. Rode: iacmp init");
72
+ const config = readJsonFile(configPath);
73
+ return {
74
+ name: config.name ?? path.basename(cwd),
75
+ provider: config.provider ?? "aws"
76
+ };
77
+ }
78
+ function resolveTsNode(projectDir) {
79
+ const dirs = [];
80
+ let dir = projectDir;
81
+ for (let i = 0; i < 5; i++) {
82
+ dirs.push(dir);
83
+ const parent = path.dirname(dir);
84
+ if (parent === dir) break;
85
+ dir = parent;
86
+ }
87
+ for (const d of dirs) {
88
+ const tsNodePath = path.join(d, "node_modules", "ts-node");
89
+ if (fs2.existsSync(tsNodePath)) return tsNodePath;
90
+ }
91
+ return null;
92
+ }
93
+ function findStackFiles(dir) {
94
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
95
+ const files = [];
96
+ for (const entry of entries) {
97
+ const full = path.join(dir, entry.name);
98
+ if (entry.isDirectory()) {
99
+ files.push(...findStackFiles(full));
100
+ } else if (entry.name.endsWith(".ts") || entry.name.endsWith(".js")) {
101
+ files.push(full);
102
+ }
103
+ }
104
+ return files;
105
+ }
106
+ function loadStacks(cwd) {
107
+ const stacksDir = path.join(cwd, "stacks");
108
+ if (!fs2.existsSync(stacksDir)) throw new Error("Diret\xF3rio stacks/ n\xE3o encontrado.");
109
+ const stackFiles = findStackFiles(stacksDir);
110
+ if (stackFiles.length === 0) throw new Error("Nenhuma stack encontrada em stacks/");
111
+ const tsNodePath = resolveTsNode(cwd);
112
+ if (tsNodePath) {
113
+ require(tsNodePath).register({
114
+ transpileOnly: true,
115
+ skipProject: true,
116
+ compilerOptions: {
117
+ target: "ES2022",
118
+ module: "commonjs",
119
+ moduleResolution: "node",
120
+ esModuleInterop: true,
121
+ strict: false,
122
+ skipLibCheck: true
123
+ }
124
+ });
125
+ }
126
+ const result = [];
127
+ for (const stackPath of stackFiles) {
128
+ const stackName = path.basename(stackPath).replace(/\.(ts|js)$/, "");
129
+ try {
130
+ const mod = require(stackPath);
131
+ const raw = mod.default ?? mod.stack ?? mod;
132
+ if (!raw || typeof raw !== "object" || !("constructs" in raw)) {
133
+ console.warn(`[audit] ${path.basename(stackPath)} n\xE3o exporta uma Stack v\xE1lida \u2014 ignorado.`);
134
+ continue;
135
+ }
136
+ result.push({ name: stackName, stack: raw });
137
+ } catch (err) {
138
+ console.warn(`[audit] falha ao carregar ${path.basename(stackPath)}: ${errMessage(err)}`);
139
+ }
140
+ }
141
+ return result;
142
+ }
143
+ function saveReport(cwd, commandName, content) {
144
+ const auditDir = path.join(cwd, "audit");
145
+ if (!fs2.existsSync(auditDir)) fs2.mkdirSync(auditDir, { recursive: true });
146
+ const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
147
+ const fileName = `${commandName}-${date}.md`;
148
+ const filePath = path.join(auditDir, fileName);
149
+ fs2.writeFileSync(filePath, content, "utf-8");
150
+ return path.join("audit", fileName);
151
+ }
152
+ function today() {
153
+ return (/* @__PURE__ */ new Date()).toLocaleDateString("pt-BR");
154
+ }
155
+
156
+ // src/commands/audit-improvements.ts
157
+ function shouldFail(failOn, critical, warnings) {
158
+ if (failOn === "critical") return critical > 0;
159
+ if (failOn === "warning") return critical > 0 || warnings > 0;
160
+ return false;
161
+ }
162
+ function analyzeStack(stackName, stack) {
163
+ const constructs = stack.constructs;
164
+ const improvements = [];
165
+ const ok = [];
166
+ if (constructs.length === 0) {
167
+ improvements.push({
168
+ category: "CONFIGURATION",
169
+ constructId: stackName,
170
+ constructType: "Stack",
171
+ stackName,
172
+ title: `Stack '${stackName}' \u2014 empty`,
173
+ impact: "High",
174
+ current: "The stack has no constructs defined.",
175
+ suggestion: "Add constructs to the stack to make it functional.",
176
+ effort: "Varies by use case"
177
+ });
178
+ return { improvements, ok };
179
+ }
180
+ const instances = constructs.filter((c) => c.type === "Compute.Instance");
181
+ const buckets = constructs.filter((c) => c.type === "Storage.Bucket");
182
+ const dbs = constructs.filter((c) => c.type === "Database.SQL");
183
+ const lambdas = constructs.filter((c) => c.type === "Function.Lambda");
184
+ const vpcs = constructs.filter((c) => c.type === "Network.VPC");
185
+ for (const inst of instances) {
186
+ if (inst.props.instanceType === "small") {
187
+ improvements.push({
188
+ category: "PERFORMANCE",
189
+ constructId: inst.id,
190
+ constructType: inst.type,
191
+ stackName,
192
+ title: `Compute.Instance '${inst.id}' \u2014 small instance type`,
193
+ impact: "Medium",
194
+ current: `instanceType 'small' may be insufficient for production workloads.`,
195
+ suggestion: `Use 'medium' or 'large' for production. Consider adding auto-scaling.`,
196
+ effort: "Low (change 1 field in the stack)"
197
+ });
198
+ } else {
199
+ ok.push({ id: inst.id, type: inst.type, reason: "adequate instance type" });
200
+ }
201
+ }
202
+ if (instances.length > 1) {
203
+ improvements.push({
204
+ category: "ARCHITECTURE",
205
+ constructId: "stack",
206
+ constructType: "Stack",
207
+ stackName,
208
+ title: "Multiple instances without a load balancer",
209
+ impact: "High",
210
+ current: `${instances.length} compute instances detected with no load balancer defined.`,
211
+ suggestion: "Add a load balancer to distribute traffic and increase resilience. (future feature in iacmp)",
212
+ effort: "Medium (requires new resource in iacmp)"
213
+ });
214
+ }
215
+ for (const b of buckets) {
216
+ if (b.props.versioning !== true) {
217
+ improvements.push({
218
+ category: "DATA PROTECTION",
219
+ constructId: b.id,
220
+ constructType: b.type,
221
+ stackName,
222
+ title: `Storage.Bucket '${b.id}' \u2014 versioning disabled`,
223
+ impact: "Medium",
224
+ current: "Versioning is disabled. Deleted or overwritten objects are unrecoverable.",
225
+ suggestion: "Enable `versioning: true` to protect against accidental deletion.",
226
+ effort: "Low (change 1 field in the stack)"
227
+ });
228
+ } else {
229
+ ok.push({ id: b.id, type: b.type, reason: "versioning enabled" });
230
+ }
231
+ }
232
+ for (const db of dbs) {
233
+ if (db.props.multiAz !== true) {
234
+ improvements.push({
235
+ category: "AVAILABILITY",
236
+ constructId: db.id,
237
+ constructType: db.type,
238
+ stackName,
239
+ title: `Database.SQL '${db.id}' \u2014 no Multi-AZ or read replica`,
240
+ impact: "High",
241
+ current: "Single-AZ database with no read replica.",
242
+ suggestion: "Set `multiAz: true` and consider adding a read replica for query performance.",
243
+ effort: "Low (change 1 field; replica requires a new construct)"
244
+ });
245
+ } else {
246
+ ok.push({ id: db.id, type: db.type, reason: "Multi-AZ enabled" });
247
+ }
248
+ }
249
+ for (const vpc of vpcs) {
250
+ const maxAzs = vpc.props.maxAzs;
251
+ if (maxAzs === void 0 || maxAzs === null) {
252
+ improvements.push({
253
+ category: "ARCHITECTURE",
254
+ constructId: vpc.id,
255
+ constructType: vpc.type,
256
+ stackName,
257
+ title: `Network.VPC '${vpc.id}' \u2014 maxAzs not defined`,
258
+ impact: "Medium",
259
+ current: "maxAzs not defined. The provider may default to 1 AZ.",
260
+ suggestion: "Set `maxAzs: 3` for production with high availability.",
261
+ effort: "Low (change 1 field in the stack)"
262
+ });
263
+ } else {
264
+ ok.push({ id: vpc.id, type: vpc.type, reason: `${maxAzs} AZs configured` });
265
+ }
266
+ }
267
+ if (lambdas.length === 0 && instances.length > 0) {
268
+ improvements.push({
269
+ category: "ARCHITECTURE",
270
+ constructId: "stack",
271
+ constructType: "Stack",
272
+ stackName,
273
+ title: "No serverless functions detected",
274
+ impact: "Low",
275
+ current: `Stack has ${instances.length} compute instance(s) but no Function.Lambda.`,
276
+ suggestion: "For event-driven tasks, scheduled jobs, or webhooks, consider Function.Lambda instead of always-on instances.",
277
+ effort: "Medium (requires partial logic refactoring)"
278
+ });
279
+ }
280
+ for (const fn of lambdas) {
281
+ ok.push({ id: fn.id, type: fn.type, reason: "serverless \u2014 cost-efficient by nature" });
282
+ }
283
+ return { improvements, ok };
284
+ }
285
+ var AuditImprovements = class _AuditImprovements extends import_core.Command {
286
+ static description = "Suggest architecture and performance improvements for stacks";
287
+ static examples = [
288
+ "$ iacmp audit-improvements",
289
+ "$ iacmp audit-improvements --fail-on=critical"
290
+ ];
291
+ static flags = {
292
+ "fail-on": import_core.Flags.string({
293
+ description: "Sai com exit 1 quando h\xE1 melhorias no n\xEDvel indicado (critical = High impact, warning = qualquer)",
294
+ options: ["critical", "warning", "none"],
295
+ default: "none"
296
+ })
297
+ };
298
+ async run() {
299
+ const { flags } = await this.parse(_AuditImprovements);
300
+ const failOn = flags["fail-on"];
301
+ const cwd = process.cwd();
302
+ let config;
303
+ try {
304
+ config = readConfig(cwd);
305
+ } catch (err) {
306
+ this.error(err.message);
307
+ }
308
+ let stacks;
309
+ try {
310
+ stacks = loadStacks(cwd);
311
+ } catch (err) {
312
+ this.error(err.message);
313
+ }
314
+ const allImprovements = [];
315
+ const allOk = [];
316
+ for (const { name, stack } of stacks) {
317
+ const { improvements, ok } = analyzeStack(name, stack);
318
+ allImprovements.push(...improvements);
319
+ for (const o of ok) allOk.push({ ...o, stackName: name });
320
+ }
321
+ this.log(import_chalk.default.bold("\nImprovements Audit"));
322
+ this.log("\u2500".repeat(40));
323
+ this.log(`Improvements found: ${allImprovements.length > 0 ? import_chalk.default.yellow(allImprovements.length) : import_chalk.default.green(0)}`);
324
+ this.log("");
325
+ for (const m of allImprovements) {
326
+ const impactColor = m.impact === "High" ? import_chalk.default.red : m.impact === "Medium" ? import_chalk.default.yellow : import_chalk.default.cyan;
327
+ this.log(`${import_chalk.default.yellow("\u26A0")} [${m.category}] ${m.title} \u2014 Impact: ${impactColor(m.impact)}`);
328
+ }
329
+ for (const o of allOk) {
330
+ this.log(`${import_chalk.default.green("\u2713")} ${o.type} '${o.id}' \u2014 ${o.reason}`);
331
+ }
332
+ let md = `# Improvements Audit Report \u2014 ${config.name}
333
+ `;
334
+ md += `Date: ${today()}
335
+
336
+ `;
337
+ md += `## Improvements found: ${allImprovements.length}
338
+
339
+ `;
340
+ for (const m of allImprovements) {
341
+ md += `### [${m.category}] ${m.title}
342
+ `;
343
+ md += `Impact: ${m.impact}
344
+ `;
345
+ md += `Stack: ${m.stackName}
346
+ `;
347
+ md += `Current situation: ${m.current}
348
+ `;
349
+ md += `Suggestion: ${m.suggestion}
350
+ `;
351
+ md += `Estimated effort: ${m.effort}
352
+
353
+ `;
354
+ }
355
+ if (allOk.length > 0) {
356
+ md += `## No suggestions
357
+ `;
358
+ for (const o of allOk) {
359
+ md += `- ${o.type} '${o.id}' \u2014 ${o.reason}
360
+ `;
361
+ }
362
+ md += "\n";
363
+ }
364
+ const relPath = saveReport(cwd, "improvements", md);
365
+ this.log(`
366
+ Report saved to ${relPath}`);
367
+ const highImpact = allImprovements.filter((m) => m.impact === "High").length;
368
+ const otherImpact = allImprovements.length - highImpact;
369
+ if (shouldFail(failOn, highImpact, otherImpact)) {
370
+ this.exit(1);
371
+ }
372
+ }
373
+ };