@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,417 @@
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 __commonJS = (cb, mod) => function __require() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+ var __export = (target, all) => {
12
+ for (var name in all)
13
+ __defProp(target, name, { get: all[name], enumerable: true });
14
+ };
15
+ var __copyProps = (to, from, except, desc) => {
16
+ if (from && typeof from === "object" || typeof from === "function") {
17
+ for (let key of __getOwnPropNames(from))
18
+ if (!__hasOwnProp.call(to, key) && key !== except)
19
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
20
+ }
21
+ return to;
22
+ };
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
24
+ // If the importer is in node compatibility mode or this is not an ESM
25
+ // file that has been converted to a CommonJS file using a Babel-
26
+ // compatible transform (i.e. "__esModule" has not been set), then set
27
+ // "default" to the CommonJS "module.exports" for node compatibility.
28
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
29
+ mod
30
+ ));
31
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
+
33
+ // ../dashboard/dist/ui.js
34
+ var require_ui = __commonJS({
35
+ "../dashboard/dist/ui.js"(exports2) {
36
+ "use strict";
37
+ Object.defineProperty(exports2, "__esModule", { value: true });
38
+ exports2.generateHtml = generateHtml;
39
+ function escapeHtml(str) {
40
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
41
+ }
42
+ function renderStackCard(stack) {
43
+ const resourceRows = stack.resources.map((r) => `
44
+ <tr>
45
+ <td class="res-type">${escapeHtml(r.type)}</td>
46
+ <td class="res-id">${escapeHtml(r.id)}</td>
47
+ </tr>`).join("");
48
+ const resourceTable = stack.resources.length > 0 ? `
49
+ <table class="res-table">
50
+ <thead>
51
+ <tr><th>Tipo</th><th>ID l\xF3gico</th></tr>
52
+ </thead>
53
+ <tbody>${resourceRows}
54
+ </tbody>
55
+ </table>` : `<p class="empty">Nenhum recurso sintetizado.</p>`;
56
+ return `
57
+ <div class="card">
58
+ <div class="card-header">
59
+ <span class="stack-name">${escapeHtml(stack.name)}</span>
60
+ <span class="badge">${stack.resources.length} recurso(s)</span>
61
+ </div>
62
+ ${resourceTable}
63
+ </div>`;
64
+ }
65
+ function generateHtml(info) {
66
+ const cards = info.stacks.length > 0 ? info.stacks.map(renderStackCard).join("\n") : '<p class="empty-project">Nenhuma stack sintetizada. Rode <code>iacmp synth</code>.</p>';
67
+ return `<!DOCTYPE html>
68
+ <html lang="pt-BR">
69
+ <head>
70
+ <meta charset="UTF-8">
71
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
72
+ <title>iacmp Dashboard \u2014 ${escapeHtml(info.name)}</title>
73
+ <style>
74
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
75
+ body {
76
+ background: #0f1117;
77
+ color: #e2e8f0;
78
+ font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
79
+ font-size: 14px;
80
+ line-height: 1.6;
81
+ min-height: 100vh;
82
+ }
83
+ header {
84
+ background: #1a1f2e;
85
+ border-bottom: 1px solid #2d3748;
86
+ padding: 16px 32px;
87
+ display: flex;
88
+ align-items: center;
89
+ gap: 24px;
90
+ }
91
+ .logo {
92
+ font-size: 18px;
93
+ font-weight: 700;
94
+ color: #63b3ed;
95
+ letter-spacing: -0.5px;
96
+ }
97
+ .logo span { color: #e2e8f0; }
98
+ .meta {
99
+ display: flex;
100
+ gap: 16px;
101
+ color: #718096;
102
+ font-size: 13px;
103
+ }
104
+ .meta strong { color: #a0aec0; }
105
+ main {
106
+ padding: 32px;
107
+ max-width: 960px;
108
+ margin: 0 auto;
109
+ }
110
+ h2 {
111
+ font-size: 13px;
112
+ text-transform: uppercase;
113
+ letter-spacing: 1px;
114
+ color: #718096;
115
+ margin-bottom: 16px;
116
+ }
117
+ .card {
118
+ background: #1a1f2e;
119
+ border: 1px solid #2d3748;
120
+ border-radius: 8px;
121
+ margin-bottom: 20px;
122
+ overflow: hidden;
123
+ }
124
+ .card-header {
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: space-between;
128
+ padding: 14px 20px;
129
+ border-bottom: 1px solid #2d3748;
130
+ background: #161b27;
131
+ }
132
+ .stack-name {
133
+ font-weight: 600;
134
+ font-size: 15px;
135
+ color: #e2e8f0;
136
+ }
137
+ .badge {
138
+ background: #2d3748;
139
+ color: #a0aec0;
140
+ padding: 2px 10px;
141
+ border-radius: 12px;
142
+ font-size: 12px;
143
+ }
144
+ .res-table {
145
+ width: 100%;
146
+ border-collapse: collapse;
147
+ }
148
+ .res-table thead tr {
149
+ background: #141820;
150
+ }
151
+ .res-table th {
152
+ text-align: left;
153
+ padding: 8px 20px;
154
+ font-size: 11px;
155
+ text-transform: uppercase;
156
+ letter-spacing: 0.5px;
157
+ color: #4a5568;
158
+ font-weight: 600;
159
+ }
160
+ .res-table tbody tr {
161
+ border-top: 1px solid #1e2533;
162
+ }
163
+ .res-table tbody tr:hover { background: #1e2533; }
164
+ .res-type {
165
+ padding: 10px 20px;
166
+ color: #68d391;
167
+ font-family: 'Cascadia Code', 'Fira Code', monospace;
168
+ font-size: 13px;
169
+ }
170
+ .res-id {
171
+ padding: 10px 20px;
172
+ color: #a0aec0;
173
+ font-family: 'Cascadia Code', 'Fira Code', monospace;
174
+ font-size: 13px;
175
+ }
176
+ .empty { padding: 20px; color: #4a5568; }
177
+ .empty-project { color: #4a5568; padding: 16px 0; }
178
+ .empty-project code {
179
+ background: #1a1f2e;
180
+ padding: 2px 6px;
181
+ border-radius: 4px;
182
+ color: #63b3ed;
183
+ }
184
+ footer {
185
+ text-align: center;
186
+ padding: 24px;
187
+ color: #2d3748;
188
+ font-size: 12px;
189
+ }
190
+ </style>
191
+ </head>
192
+ <body>
193
+ <header>
194
+ <div class="logo">iacmp <span>Dashboard</span></div>
195
+ <div class="meta">
196
+ <span><strong>Projeto:</strong> ${escapeHtml(info.name)}</span>
197
+ <span><strong>Provider:</strong> ${escapeHtml(info.provider)}</span>
198
+ <span><strong>Regi\xE3o:</strong> ${escapeHtml(info.region)}</span>
199
+ <span><strong>Stacks:</strong> ${info.stacks.length}</span>
200
+ </div>
201
+ </header>
202
+ <main>
203
+ <h2>Stacks sintetizadas</h2>
204
+ ${cards}
205
+ </main>
206
+ <footer>iacmp v0.4.0 \u2014 atualizado em ${(/* @__PURE__ */ new Date()).toLocaleString("pt-BR")}</footer>
207
+ </body>
208
+ </html>
209
+ `;
210
+ }
211
+ }
212
+ });
213
+
214
+ // ../dashboard/dist/server.js
215
+ var require_server = __commonJS({
216
+ "../dashboard/dist/server.js"(exports2) {
217
+ "use strict";
218
+ var __createBinding = exports2 && exports2.__createBinding || (Object.create ? (function(o, m, k, k2) {
219
+ if (k2 === void 0) k2 = k;
220
+ var desc = Object.getOwnPropertyDescriptor(m, k);
221
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
222
+ desc = { enumerable: true, get: function() {
223
+ return m[k];
224
+ } };
225
+ }
226
+ Object.defineProperty(o, k2, desc);
227
+ }) : (function(o, m, k, k2) {
228
+ if (k2 === void 0) k2 = k;
229
+ o[k2] = m[k];
230
+ }));
231
+ var __setModuleDefault = exports2 && exports2.__setModuleDefault || (Object.create ? (function(o, v) {
232
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
233
+ }) : function(o, v) {
234
+ o["default"] = v;
235
+ });
236
+ var __importStar = exports2 && exports2.__importStar || /* @__PURE__ */ (function() {
237
+ var ownKeys = function(o) {
238
+ ownKeys = Object.getOwnPropertyNames || function(o2) {
239
+ var ar = [];
240
+ for (var k in o2) if (Object.prototype.hasOwnProperty.call(o2, k)) ar[ar.length] = k;
241
+ return ar;
242
+ };
243
+ return ownKeys(o);
244
+ };
245
+ return function(mod) {
246
+ if (mod && mod.__esModule) return mod;
247
+ var result = {};
248
+ if (mod != null) {
249
+ for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
250
+ }
251
+ __setModuleDefault(result, mod);
252
+ return result;
253
+ };
254
+ })();
255
+ Object.defineProperty(exports2, "__esModule", { value: true });
256
+ exports2.createServer = createServer;
257
+ var http = __importStar(require("http"));
258
+ var ui_1 = require_ui();
259
+ function createServer(info) {
260
+ return http.createServer((_req, res) => {
261
+ const html = (0, ui_1.generateHtml)(info);
262
+ res.writeHead(200, {
263
+ "Content-Type": "text/html; charset=utf-8",
264
+ "Content-Length": Buffer.byteLength(html)
265
+ });
266
+ res.end(html);
267
+ });
268
+ }
269
+ }
270
+ });
271
+
272
+ // ../dashboard/dist/index.js
273
+ var require_dist = __commonJS({
274
+ "../dashboard/dist/index.js"(exports2) {
275
+ "use strict";
276
+ Object.defineProperty(exports2, "__esModule", { value: true });
277
+ exports2.startDashboard = startDashboard2;
278
+ var server_1 = require_server();
279
+ function startDashboard2(info, port) {
280
+ return new Promise((resolve, reject) => {
281
+ const server = (0, server_1.createServer)(info);
282
+ server.on("error", reject);
283
+ server.listen(port, () => resolve());
284
+ });
285
+ }
286
+ }
287
+ });
288
+
289
+ // src/commands/dashboard.ts
290
+ var dashboard_exports = {};
291
+ __export(dashboard_exports, {
292
+ default: () => Dashboard
293
+ });
294
+ module.exports = __toCommonJS(dashboard_exports);
295
+ var import_core = require("@oclif/core");
296
+ var fs2 = __toESM(require("fs"));
297
+ var path2 = __toESM(require("path"));
298
+ var import_child_process = require("child_process");
299
+ var import_dashboard = __toESM(require_dist());
300
+
301
+ // src/synth-out.ts
302
+ var fs = __toESM(require("fs"));
303
+ var path = __toESM(require("path"));
304
+ function templateExt(provider) {
305
+ return provider === "terraform" ? ".tf" : ".json";
306
+ }
307
+ function synthRoot(cwd) {
308
+ return path.join(cwd, "synth-out");
309
+ }
310
+ function providerOutDir(cwd, provider) {
311
+ return path.join(synthRoot(cwd), provider);
312
+ }
313
+ function hasTemplates(dir, provider) {
314
+ const ext = templateExt(provider);
315
+ try {
316
+ return fs.readdirSync(dir).some((f) => f.endsWith(ext) && fs.statSync(path.join(dir, f)).isFile());
317
+ } catch {
318
+ return false;
319
+ }
320
+ }
321
+ function resolveTemplateDir(cwd, provider) {
322
+ const dir = providerOutDir(cwd, provider);
323
+ if (fs.existsSync(dir)) return dir;
324
+ const root = synthRoot(cwd);
325
+ if (fs.existsSync(root) && hasTemplates(root, provider)) return root;
326
+ return null;
327
+ }
328
+ function listTemplates(cwd, provider, stack) {
329
+ const dir = resolveTemplateDir(cwd, provider);
330
+ if (!dir) return [];
331
+ const ext = templateExt(provider);
332
+ return fs.readdirSync(dir).filter((f) => f.endsWith(ext) && fs.statSync(path.join(dir, f)).isFile()).map((f) => ({
333
+ stackName: f.slice(0, -ext.length),
334
+ filePath: path.join(dir, f),
335
+ fileName: f
336
+ })).filter((t) => !stack || t.stackName === stack);
337
+ }
338
+
339
+ // src/commands/dashboard.ts
340
+ function parseResources(filePath, provider) {
341
+ try {
342
+ const content = fs2.readFileSync(filePath, "utf-8");
343
+ const parsed = JSON.parse(content);
344
+ if (provider === "aws") {
345
+ const resources = parsed.Resources;
346
+ if (resources) {
347
+ return Object.entries(resources).map(([id, r]) => ({ type: r.Type, id }));
348
+ }
349
+ }
350
+ if (provider === "azure") {
351
+ const resources = parsed.resources;
352
+ if (Array.isArray(resources)) {
353
+ return resources.map((r) => ({ type: r.type, id: r.name }));
354
+ }
355
+ }
356
+ if (provider === "gcp") {
357
+ const resources = parsed.resources;
358
+ if (Array.isArray(resources)) {
359
+ return resources.map((r) => ({ type: r.type, id: r.name }));
360
+ }
361
+ }
362
+ if (Array.isArray(parsed.resources)) {
363
+ return parsed.resources.map((r) => ({
364
+ type: r.type ?? "unknown",
365
+ id: r.name ?? r.id ?? "unknown"
366
+ }));
367
+ }
368
+ return [];
369
+ } catch {
370
+ return [];
371
+ }
372
+ }
373
+ var Dashboard = class _Dashboard extends import_core.Command {
374
+ static description = "Inicia o dashboard web de visualiza\xE7\xE3o das stacks";
375
+ static flags = {
376
+ port: import_core.Flags.integer({ char: "p", description: "Porta do servidor", default: 4e3 }),
377
+ open: import_core.Flags.boolean({ description: "Abre o browser automaticamente", default: false })
378
+ };
379
+ static examples = [
380
+ "$ iacmp dashboard",
381
+ "$ iacmp dashboard --port 3000",
382
+ "$ iacmp dashboard --open"
383
+ ];
384
+ async run() {
385
+ const { flags } = await this.parse(_Dashboard);
386
+ const cwd = process.cwd();
387
+ const configPath = path2.join(cwd, "iacmp.json");
388
+ if (!fs2.existsSync(configPath)) {
389
+ this.error("Projeto n\xE3o inicializado. Rode: iacmp init");
390
+ }
391
+ const config = JSON.parse(fs2.readFileSync(configPath, "utf-8"));
392
+ const stacks = [];
393
+ for (const t of listTemplates(cwd, config.provider)) {
394
+ const resources = parseResources(t.filePath, config.provider);
395
+ stacks.push({ name: t.stackName, provider: config.provider, resources });
396
+ }
397
+ const info = {
398
+ name: config.name,
399
+ provider: config.provider,
400
+ region: config.region,
401
+ stacks
402
+ };
403
+ const port = flags.port;
404
+ await (0, import_dashboard.startDashboard)(info, port);
405
+ const url = `http://localhost:${port}`;
406
+ this.log(`Dashboard dispon\xEDvel em ${url}`);
407
+ if (flags.open) {
408
+ try {
409
+ const openCmd = process.platform === "win32" ? `start ${url}` : `open ${url}`;
410
+ (0, import_child_process.execSync)(openCmd);
411
+ } catch {
412
+ }
413
+ }
414
+ await new Promise(() => {
415
+ });
416
+ }
417
+ };
@@ -0,0 +1,188 @@
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/deploy.ts
31
+ var deploy_exports = {};
32
+ __export(deploy_exports, {
33
+ default: () => Deploy
34
+ });
35
+ module.exports = __toCommonJS(deploy_exports);
36
+ var import_core = require("@oclif/core");
37
+ var fs3 = __toESM(require("fs"));
38
+ var path2 = __toESM(require("path"));
39
+ var import_chalk = __toESM(require("chalk"));
40
+
41
+ // src/synth-out.ts
42
+ var fs = __toESM(require("fs"));
43
+ var path = __toESM(require("path"));
44
+ function templateExt(provider) {
45
+ return provider === "terraform" ? ".tf" : ".json";
46
+ }
47
+ function synthRoot(cwd) {
48
+ return path.join(cwd, "synth-out");
49
+ }
50
+ function providerOutDir(cwd, provider) {
51
+ return path.join(synthRoot(cwd), provider);
52
+ }
53
+ function hasTemplates(dir, provider) {
54
+ const ext = templateExt(provider);
55
+ try {
56
+ return fs.readdirSync(dir).some((f) => f.endsWith(ext) && fs.statSync(path.join(dir, f)).isFile());
57
+ } catch {
58
+ return false;
59
+ }
60
+ }
61
+ function resolveTemplateDir(cwd, provider) {
62
+ const dir = providerOutDir(cwd, provider);
63
+ if (fs.existsSync(dir)) return dir;
64
+ const root = synthRoot(cwd);
65
+ if (fs.existsSync(root) && hasTemplates(root, provider)) return root;
66
+ return null;
67
+ }
68
+ function listTemplates(cwd, provider, stack) {
69
+ const dir = resolveTemplateDir(cwd, provider);
70
+ if (!dir) return [];
71
+ const ext = templateExt(provider);
72
+ return fs.readdirSync(dir).filter((f) => f.endsWith(ext) && fs.statSync(path.join(dir, f)).isFile()).map((f) => ({
73
+ stackName: f.slice(0, -ext.length),
74
+ filePath: path.join(dir, f),
75
+ fileName: f
76
+ })).filter((t) => !stack || t.stackName === stack);
77
+ }
78
+ function countResources(filePath, provider) {
79
+ let content;
80
+ try {
81
+ content = fs.readFileSync(filePath, "utf-8");
82
+ } catch {
83
+ return 0;
84
+ }
85
+ if (provider === "terraform") {
86
+ const matches = content.match(/^resource\s+"/gm);
87
+ return matches ? matches.length : 0;
88
+ }
89
+ let parsed;
90
+ try {
91
+ parsed = JSON.parse(content);
92
+ } catch {
93
+ return 0;
94
+ }
95
+ if (parsed && typeof parsed === "object") {
96
+ const obj = parsed;
97
+ if (Array.isArray(obj.resources)) return obj.resources.length;
98
+ if (obj.Resources && typeof obj.Resources === "object") {
99
+ return Object.keys(obj.Resources).length;
100
+ }
101
+ }
102
+ return 0;
103
+ }
104
+
105
+ // src/utils.ts
106
+ var fs2 = __toESM(require("fs"));
107
+ function readJsonFile(filePath) {
108
+ let content;
109
+ try {
110
+ content = fs2.readFileSync(filePath, "utf-8");
111
+ } catch (e) {
112
+ throw new Error(`Falha ao ler '${filePath}': ${errMessage(e)}`);
113
+ }
114
+ try {
115
+ return JSON.parse(content);
116
+ } catch (e) {
117
+ throw new Error(`JSON inv\xE1lido em '${filePath}': ${errMessage(e)}`);
118
+ }
119
+ }
120
+ function errMessage(e) {
121
+ if (e instanceof Error) return e.message;
122
+ if (typeof e === "string") return e;
123
+ try {
124
+ return JSON.stringify(e);
125
+ } catch {
126
+ return String(e);
127
+ }
128
+ }
129
+
130
+ // src/commands/deploy.ts
131
+ var MVP_BANNER = "MVP: deploy/destroy real ainda n\xE3o implementado nesta fase. Os arquivos foram impressos como dry-run.";
132
+ var PROVIDER_LABELS = {
133
+ aws: "AWS (CloudFormation)",
134
+ azure: "Azure (ARM Template)",
135
+ gcp: "GCP (Deployment Manager)",
136
+ terraform: "Terraform"
137
+ };
138
+ var Deploy = class _Deploy extends import_core.Command {
139
+ static description = "Faz deploy das stacks no provider configurado";
140
+ static flags = {
141
+ provider: import_core.Flags.string({ char: "p", description: "Provider alvo (aws, azure, gcp, terraform)", default: "aws" }),
142
+ stack: import_core.Flags.string({ char: "s", description: "Nome da stack espec\xEDfica" })
143
+ };
144
+ static examples = [
145
+ "$ iacmp deploy",
146
+ "$ iacmp deploy --provider aws --stack minha-stack",
147
+ "$ iacmp deploy --provider azure",
148
+ "$ iacmp deploy --provider gcp",
149
+ "$ iacmp deploy --provider terraform"
150
+ ];
151
+ async run() {
152
+ const { flags } = await this.parse(_Deploy);
153
+ const cwd = process.cwd();
154
+ const configPath = path2.join(cwd, "iacmp.json");
155
+ if (!fs3.existsSync(configPath)) {
156
+ this.error("Projeto n\xE3o inicializado. Rode: iacmp init");
157
+ }
158
+ let config;
159
+ try {
160
+ config = readJsonFile(configPath);
161
+ } catch (err) {
162
+ this.error(errMessage(err));
163
+ }
164
+ const provider = flags.provider ?? config.provider ?? "aws";
165
+ const label = PROVIDER_LABELS[provider] ?? provider.toUpperCase();
166
+ this.log(import_chalk.default.yellow.bold(MVP_BANNER));
167
+ this.log("");
168
+ this.log(`Sintetizando stacks para ${provider}...`);
169
+ const templates = listTemplates(cwd, provider, flags.stack);
170
+ if (templates.length === 0) {
171
+ this.error(`Nenhum template encontrado para '${provider}'. Rode: iacmp synth --provider ${provider}`);
172
+ }
173
+ let totalResources = 0;
174
+ for (const t of templates) {
175
+ const resourceCount = countResources(t.filePath, provider);
176
+ totalResources += resourceCount;
177
+ this.log(` Stack: ${t.stackName} \u2014 ${resourceCount} recurso(s)`);
178
+ }
179
+ this.log("");
180
+ if (provider === "terraform") {
181
+ this.log(`Would apply ${totalResources} resource(s) (Terraform)`);
182
+ } else {
183
+ this.log(`Would deploy ${totalResources} resource(s) to ${label}`);
184
+ }
185
+ this.log("");
186
+ this.log("(MVP: deploy real n\xE3o implementado nesta fase)");
187
+ }
188
+ };