aact 1.0.0 → 2.0.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/README.md ADDED
@@ -0,0 +1,171 @@
1
+ <img width="150" height="150" alt="aact logo" src="https://github.com/user-attachments/assets/abbcea49-51c9-4e57-8cbe-a1ed11d1fa48" />
2
+
3
+ # Architecture As Code Tools (aact)
4
+
5
+ [![test workflow](https://github.com/razonrus/ArchAsCode_Tests/actions/workflows/test.yaml/badge.svg?branch=main)](https://github.com/razonrus/ArchAsCode_Tests/actions/workflows/test.yaml)
6
+
7
+ CLI и библиотека для валидации, анализа и генерации архитектуры микросервисных систем, описанной "as Code" (PlantUML C4, Structurizr).
8
+
9
+ Инструменты для работы с архитектурой в формате "as Code":
10
+
11
+ 1. Код и примеры покрытия тестами микросервисной архитектуры, описанной в plantuml ([#](#покрытие-архитектуры-тестами))
12
+ 2. Автогенерация архитектуры ([#](#автогенерация-архитектуры-1))
13
+ 3. Тестирование архитектуры модульного монолита ([#](#тестирование-модульного-монолита))
14
+
15
+ [Планы развития инструментов и репозитория](roadmap.md). PullRequest'ы и Issues'ы приветствуются.
16
+
17
+ [Справочник](patterns.md) принципов и паттернов проектирования с примерами покрытия их тестами (пополняется...)
18
+
19
+ <img src="https://github.com/Byndyusoft/aact/assets/1096954/a3c3b3b0-a09b-4da7-aca4-5538159b371c" width="15"/> Телеграм-канал: [Архитектура распределённых систем](https://t.me/rsa_enc)
20
+
21
+ ## Quick Start (CLI)
22
+
23
+ ```bash
24
+ # Инициализация конфига
25
+ npx aact init
26
+
27
+ # Проверка правил архитектуры
28
+ npx aact check
29
+
30
+ # Анализ метрик
31
+ npx aact analyze
32
+
33
+ # Генерация артефактов
34
+ npx aact generate --format plantuml
35
+ npx aact generate --format kubernetes
36
+ ```
37
+
38
+ ### Конфигурация
39
+
40
+ `aact init` создаст файл `aact.config.ts`:
41
+
42
+ ```ts
43
+ import { defineConfig } from "aact";
44
+
45
+ export default defineConfig({
46
+ source: {
47
+ type: "plantuml", // "plantuml" | "structurizr"
48
+ path: "./architecture.puml",
49
+ },
50
+ rules: {
51
+ acl: true,
52
+ acyclic: true,
53
+ crud: true,
54
+ dbPerService: true,
55
+ cohesion: true,
56
+ },
57
+ });
58
+ ```
59
+
60
+ ## Использование как библиотеки
61
+
62
+ ```ts
63
+ import {
64
+ loadPlantumlElements,
65
+ mapContainersFromPlantumlElements,
66
+ checkAcl,
67
+ checkAcyclic,
68
+ checkCrud,
69
+ analyzeArchitecture,
70
+ } from "aact";
71
+
72
+ const elements = await loadPlantumlElements("architecture.puml");
73
+ const model = mapContainersFromPlantumlElements(elements);
74
+
75
+ // Проверка правил
76
+ const aclViolations = checkAcl(model.allContainers);
77
+ const cyclicViolations = checkAcyclic(model.allContainers);
78
+
79
+ // Анализ метрик
80
+ const { report } = analyzeArchitecture(model);
81
+ console.log(`Elements: ${report.elementsCount}`);
82
+ ```
83
+
84
+ ## Примеры
85
+
86
+ - [Banking (PlantUML)](examples/banking-plantuml/) — проверка правил, CCR-анализ, генерация K8s-конфигов
87
+ - [Microservices (Structurizr)](examples/microservices-structurizr/) — полный цикл: правила, анализ, генерация
88
+
89
+ ## Документация
90
+
91
+ - [Справочник паттернов](patterns.md) — принципы и паттерны с примерами тестов
92
+ - [ADR](ADRs/) — Architecture Decision Records
93
+ - [Roadmap](roadmap.md) — планы развития
94
+
95
+ ## Публичные материалы
96
+
97
+ ### Раз архитектура — «as Code», почему бы её не покрыть тестами?!
98
+
99
+ <a href="https://www.youtube.com/watch?v=POIbWZh68Cg"><img src="https://github.com/Byndyusoft/aact/assets/1096954/e011958e-12c8-4fb9-97f4-a61779408e4f" width="400"/></a>
100
+ <a href="https://www.youtube.com/watch?v=tZ-FQeObSjY"><img src="https://github.com/Byndyusoft/aact/assets/1096954/daea29de-776b-49a0-b781-ad4eba9a2221" width="400"/></a>
101
+ https://www.youtube.com/watch?v=POIbWZh68Cg $~~~~~~~~~~$ https://www.youtube.com/watch?v=tZ-FQeObSjY
102
+
103
+ [Статья на Хабре](https://habr.com/ru/articles/800205/)
104
+
105
+ ### Автогенерация архитектуры
106
+
107
+ <a href="https://www.youtube.com/watch?v=fb2UjqjHGUE"><img src="https://github.com/Byndyusoft/aact/assets/1096954/ecb54a6f-f6c1-4816-972b-c845069e9f4a" width="400"/></a><br/>
108
+ https://www.youtube.com/watch?v=fb2UjqjHGUE
109
+
110
+ # Покрытие архитектуры тестами
111
+
112
+ ## Что это, какую боль решает, и с чего начать?
113
+
114
+ Раз архитектура — «as Code», почему бы её не покрыть тестами?!
115
+
116
+ Тема идеи и данный открытый репозиторий вызвал неожиданную волну позитивных отзывов о попадании в яблочко болей и о применимости и полезности решения :)
117
+
118
+ Подход помогает решить **проблемы неактуальности, декларативности и отсутствия контроля ИТ-архитектур и инфраструктуры** (ограничение и требование — архитектура и инфраструктура должны быть "as code").
119
+
120
+ Тесты проверяют 2 больших блока:
121
+
122
+ - актуальность архитектуры реальному работающему в продакшне решению
123
+ - соответствие "нарисованной" архитектуры выбранным принципам и паттернам проектирования
124
+
125
+ Подробнее о подходе, решаемых проблемах, схеме работы представленного в репозитории примера и проверяемых в тестах репозитория принципах — на [слайдах](https://docs.google.com/presentation/d/16_3h1BTIRyREXO_oSqnjEbRJAnN3Z4aX/edit?usp=sharing&ouid=106100367728328513490&rtpof=true&sd=true).
126
+
127
+ ### Схема работы
128
+
129
+ <img src="https://github.com/Byndyusoft/aact/assets/1096954/9b0ad909-b789-4395-a580-9fb44397afa0" height="350">
130
+
131
+ ### Визуализация примера автоматически проверяемого принципа (отсутствие бизнес-логики в CRUD-сервисах)
132
+
133
+ <img src="https://github.com/Byndyusoft/aact/assets/1096954/292b1bbd-0f18-40be-9560-65385a1d4df9" height="300">
134
+
135
+ ## Пример архитектуры, которую покроем тестами
136
+
137
+ [![C4](resources/architecture/Demo%20Tests.svg)](resources/architecture/Demo%20Tests.svg)
138
+
139
+ ## Пример тестов
140
+
141
+ 1. [find diff in configs and uml containers](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность списка микросервисов на архитектуре и в [конфигурации инфраструктуры](resources/kubernetes/microservices)
142
+ 2. [find diff in configs and uml dependencies](examples/banking-plantuml/architecture.test.ts) — проверяет актуальность зависимостей (связей) микросервисов на архитектуре и в [конфигурации инфраструктуры](resources/kubernetes/microservices)
143
+ 3. [check that urls and topics from relations exist in config](examples/banking-plantuml/architecture.test.ts) — проверяет соответствие между параметрами связей микросервисов (REST-урлы, топики kafka) на архитектуре и в [конфигурации инфраструктуры](resources/kubernetes/microservices)
144
+ 4. [only acl can depend on external systems](test/rules/acl.test.ts) — проверяет, что не нарушен выбранный принцип построения интеграций с внешними системами только через ACL (Anti Corruption Layer). Проверяет, что только acl-микросервисы имеют зависимости от внешних систем.
145
+ 5. [connect to external systems only by API Gateway or kafka](examples/banking-plantuml/architecture.test.ts) — проверяет, что все внешние интеграции идут через API Gateway или через kafka
146
+
147
+ # Автогенерация архитектуры
148
+
149
+ ## Генерация архитектуры из описанной «as Code» инфраструктуры
150
+
151
+ Сравнение ~~белковой~~ составленной вручную архитектуры и сгенерированной.
152
+
153
+ ### Ручная:
154
+
155
+ [![C4](resources/architecture/Demo%20Tests.svg)](resources/architecture/Demo%20Tests.svg)
156
+
157
+ ### Сгенерированная:
158
+
159
+ [![C4](resources/architecture/Demo%20Generated.svg)](resources/architecture/Demo%20Generated.svg)
160
+
161
+ # Тестирование модульного монолита
162
+
163
+ Тестами можно покрывать не только архитектуру микросервисов, но архитектуру монолитов, особенно, если они модульные.
164
+
165
+ - [Тест архитектуры модульного монолита на C#](https://github.com/Byndyusoft/aact/tree/main/ModularMonolith)
166
+
167
+ # Тестирование на основе информации из кода
168
+
169
+ Информацию об архитектуре реализованной системы можно извлечь и из ее кода, особенно, если он написан качественно;)
170
+
171
+ - [Извлечение информации об архитектуры системы из ее кода](https://github.com/Byndyusoft/byndyusoft-architecture-testing)
@@ -0,0 +1,2 @@
1
+
2
+ export { };
@@ -0,0 +1,2 @@
1
+
2
+ export { };
@@ -0,0 +1,506 @@
1
+ #!/usr/bin/env node
2
+ import { defineCommand, runMain } from 'citty';
3
+ import consola from 'consola';
4
+ import { A as AactConfigSchema, m as loadStructurizrElements, l as loadPlantumlElements, o as mapContainersFromPlantumlElements, a as analyzeArchitecture, c as checkAcl, b as checkAcyclic, d as checkApiGateway, f as checkCrud, g as checkDbPerService, e as checkCohesion, h as checkStableDependencies, j as generateKubernetes, k as generatePlantumlFromModel } from '../shared/aact.Dqryafrg.mjs';
5
+ import { loadConfig } from 'c12';
6
+ import * as v from 'valibot';
7
+ import path from 'node:path';
8
+ import fs, { readFile, writeFile } from 'node:fs/promises';
9
+ import 'yaml';
10
+ import 'plantuml-parser';
11
+
12
+ const loadAndValidateConfig = async () => {
13
+ const { config } = await loadConfig({ name: "aact" });
14
+ if (!config) {
15
+ throw new Error("No source configured. Create an aact.config.ts file.");
16
+ }
17
+ return v.parse(AactConfigSchema, config);
18
+ };
19
+
20
+ const loadModel = async (config) => {
21
+ const resolvedPath = path.resolve(config.source.path);
22
+ switch (config.source.type) {
23
+ case "plantuml": {
24
+ const elements = await loadPlantumlElements(resolvedPath);
25
+ return mapContainersFromPlantumlElements(elements);
26
+ }
27
+ case "structurizr": {
28
+ return loadStructurizrElements(resolvedPath);
29
+ }
30
+ default: {
31
+ const sourceType = config.source.type;
32
+ throw new Error(`Unsupported source type: ${String(sourceType)}`);
33
+ }
34
+ }
35
+ };
36
+
37
+ const analyze = defineCommand({
38
+ meta: { description: "Analyze architecture metrics" },
39
+ args: {
40
+ format: {
41
+ type: "string",
42
+ description: "Output format: text, json"
43
+ }
44
+ },
45
+ async run({ args }) {
46
+ const config = await loadAndValidateConfig();
47
+ const model = await loadModel(config);
48
+ const { report } = analyzeArchitecture(model);
49
+ if (args.format === "json") {
50
+ console.log(JSON.stringify(report, void 0, 2));
51
+ return;
52
+ }
53
+ consola.info(`Elements: ${report.elementsCount}`);
54
+ consola.info(`Sync API calls: ${report.syncApiCalls}`);
55
+ consola.info(`Async API calls: ${report.asyncApiCalls}`);
56
+ consola.info(
57
+ `Databases: ${report.databases.count} (consumed by ${report.databases.consumes} relation(s))`
58
+ );
59
+ for (const b of report.boundaries) {
60
+ consola.info(
61
+ `Boundary "${b.label}": cohesion=${b.cohesion}, coupling=${b.coupling}`
62
+ );
63
+ if (b.couplingRelations.length > 0) {
64
+ for (const r of b.couplingRelations) {
65
+ consola.log(` ${r.from} \u2192 ${r.to}`);
66
+ }
67
+ }
68
+ }
69
+ }
70
+ });
71
+
72
+ const plantumlSyntax = {
73
+ containerPattern: (name) => `Container(${name},`,
74
+ containerDecl: (name, label, tags) => {
75
+ const tagsPart = tags ? `, "", "", $tags="${tags}"` : "";
76
+ return `Container(${name}, "${label}"${tagsPart})`;
77
+ },
78
+ relationPattern: (from, to) => `Rel(${from}, ${to}`,
79
+ relationDecl: (from, to, tech, tags) => {
80
+ const tagsPart = tags ? `, $tags="${tags}"` : "";
81
+ return `Rel(${from}, ${to}, "${tech ?? ""}"${tagsPart})`;
82
+ }
83
+ };
84
+
85
+ const applyEdits = (source, edits) => {
86
+ let lines = source.split("\n");
87
+ for (const edit of edits) {
88
+ const idx = lines.findIndex((line) => line.includes(edit.search));
89
+ if (idx === -1) continue;
90
+ switch (edit.type) {
91
+ case "remove": {
92
+ lines.splice(idx, 1);
93
+ break;
94
+ }
95
+ case "replace": {
96
+ lines[idx] = edit.content ?? "";
97
+ break;
98
+ }
99
+ case "add": {
100
+ lines = [
101
+ ...lines.slice(0, idx + 1),
102
+ edit.content ?? "",
103
+ ...lines.slice(idx + 1)
104
+ ];
105
+ break;
106
+ }
107
+ }
108
+ }
109
+ return lines.join("\n");
110
+ };
111
+
112
+ const fixAcl = (model, violations, syntax, options) => {
113
+ const tag = options?.tag ?? "acl";
114
+ const externalType = options?.externalType ?? "System_Ext";
115
+ const results = [];
116
+ for (const violation of violations) {
117
+ const container = model.allContainers.find(
118
+ (c) => c.name === violation.container
119
+ );
120
+ if (!container) continue;
121
+ const externalRels = container.relations.filter(
122
+ (r) => r.to.type === externalType
123
+ );
124
+ if (externalRels.length === 0) continue;
125
+ const aclName = `${container.name}_acl`;
126
+ const fix = {
127
+ rule: "acl",
128
+ description: `Add ACL layer for ${container.name}`,
129
+ edits: []
130
+ };
131
+ fix.edits.push({
132
+ type: "add",
133
+ search: syntax.containerPattern(container.name),
134
+ content: syntax.containerDecl(aclName, `${container.label} ACL`, tag)
135
+ });
136
+ for (const rel of externalRels) {
137
+ const tech = rel.technology ?? "";
138
+ fix.edits.push(
139
+ {
140
+ type: "replace",
141
+ search: syntax.relationPattern(container.name, rel.to.name),
142
+ content: syntax.relationDecl(container.name, aclName, tech)
143
+ },
144
+ {
145
+ type: "add",
146
+ search: syntax.relationPattern(container.name, aclName),
147
+ content: syntax.relationDecl(aclName, rel.to.name, tech)
148
+ }
149
+ );
150
+ }
151
+ results.push(fix);
152
+ }
153
+ return results;
154
+ };
155
+
156
+ const fixDbPerService = (model, violations, syntax, options) => {
157
+ const dbType = options?.dbType ?? "ContainerDb";
158
+ const results = [];
159
+ for (const violation of violations) {
160
+ const db = model.allContainers.find(
161
+ (c) => c.name === violation.container && c.type === dbType
162
+ );
163
+ if (!db) continue;
164
+ const accessors = model.allContainers.filter(
165
+ (c) => c.relations.some((r) => r.to.name === db.name)
166
+ );
167
+ if (accessors.length <= 1) continue;
168
+ const owner = accessors[0];
169
+ const fix = {
170
+ rule: "dbPerService",
171
+ description: `Redirect access to ${db.name} through ${owner.name}`,
172
+ edits: []
173
+ };
174
+ for (const accessor of accessors.slice(1)) {
175
+ const rel = accessor.relations.find((r) => r.to.name === db.name);
176
+ if (!rel) continue;
177
+ const tags = rel.tags && rel.tags.length > 0 ? rel.tags.join("+") : void 0;
178
+ const oldRel = syntax.relationPattern(accessor.name, db.name);
179
+ const newRel = syntax.relationDecl(
180
+ accessor.name,
181
+ owner.name,
182
+ rel.technology ?? "",
183
+ tags
184
+ );
185
+ fix.edits.push({ type: "replace", search: oldRel, content: newRel });
186
+ }
187
+ results.push(fix);
188
+ }
189
+ return results;
190
+ };
191
+
192
+ const defineRule = (def) => def;
193
+ const ruleRegistry = [
194
+ defineRule({
195
+ name: "acl",
196
+ check: (m, o) => checkAcl(m.allContainers, o),
197
+ fix: fixAcl
198
+ }),
199
+ defineRule({ name: "acyclic", check: (m) => checkAcyclic(m.allContainers) }),
200
+ defineRule({
201
+ name: "apiGateway",
202
+ check: (m, o) => checkApiGateway(m.allContainers, o)
203
+ }),
204
+ defineRule({
205
+ name: "crud",
206
+ check: (m, o) => checkCrud(m.allContainers, o)
207
+ }),
208
+ defineRule({
209
+ name: "dbPerService",
210
+ check: (m, o) => checkDbPerService(m.allContainers, o),
211
+ fix: fixDbPerService
212
+ }),
213
+ defineRule({
214
+ name: "cohesion",
215
+ check: (m, o) => checkCohesion(m, o)
216
+ }),
217
+ defineRule({
218
+ name: "stableDependencies",
219
+ check: (m, o) => checkStableDependencies(m.allContainers, o)
220
+ })
221
+ ];
222
+
223
+ const runRules = (model, rules) => {
224
+ const results = [];
225
+ for (const rule of ruleRegistry) {
226
+ const configValue = rules?.[rule.name];
227
+ if (configValue === false) continue;
228
+ const options = typeof configValue === "object" ? configValue : void 0;
229
+ results.push({ name: rule.name, violations: rule.check(model, options) });
230
+ }
231
+ return results;
232
+ };
233
+ const getSyntax = (sourceType) => {
234
+ if (sourceType === "plantuml") {
235
+ return plantumlSyntax;
236
+ }
237
+ throw new Error(`Write-back not supported for ${sourceType}`);
238
+ };
239
+ const generateFixes = (model, results, rules, syntax) => {
240
+ const fixes = [];
241
+ for (const result of results) {
242
+ if (result.violations.length === 0) continue;
243
+ const ruleDef = ruleRegistry.find((r) => r.name === result.name);
244
+ if (!ruleDef?.fix) continue;
245
+ const configValue = rules?.[ruleDef.name];
246
+ const options = typeof configValue === "object" ? configValue : void 0;
247
+ fixes.push(...ruleDef.fix(model, result.violations, syntax, options));
248
+ }
249
+ return fixes;
250
+ };
251
+ const formatText = (results) => {
252
+ for (const result of results) {
253
+ if (result.violations.length === 0) {
254
+ consola.success(`${result.name} \u2014 passed`);
255
+ } else {
256
+ consola.error(
257
+ `${result.name} \u2014 ${result.violations.length} violation(s)`
258
+ );
259
+ for (const v of result.violations) {
260
+ consola.log(` ${v.container}: ${v.message}`);
261
+ }
262
+ }
263
+ }
264
+ };
265
+ const formatJson = (results) => {
266
+ const output = {
267
+ results: results.map((r) => ({
268
+ rule: r.name,
269
+ passed: r.violations.length === 0,
270
+ violations: r.violations
271
+ }))
272
+ };
273
+ console.log(JSON.stringify(output, void 0, 2));
274
+ };
275
+ const formatGithub = (results) => {
276
+ for (const result of results) {
277
+ for (const v of result.violations) {
278
+ console.log(`::error title=${result.name}::${v.container}: ${v.message}`);
279
+ }
280
+ }
281
+ };
282
+ const formatFixes = (fixes) => {
283
+ for (const fix of fixes) {
284
+ consola.info(`[${fix.rule}] ${fix.description}`);
285
+ for (const edit of fix.edits) {
286
+ switch (edit.type) {
287
+ case "remove": {
288
+ consola.log(` - ${edit.search}`);
289
+ break;
290
+ }
291
+ case "replace": {
292
+ consola.log(` - ${edit.search}`);
293
+ consola.log(` + ${edit.content}`);
294
+ break;
295
+ }
296
+ case "add": {
297
+ consola.log(` (after "${edit.search}")`);
298
+ consola.log(` + ${edit.content}`);
299
+ break;
300
+ }
301
+ }
302
+ }
303
+ }
304
+ };
305
+ const detectFormat = (format) => {
306
+ if (format) return format;
307
+ if (process.env.GITHUB_ACTIONS) return "github";
308
+ return "text";
309
+ };
310
+ const check = defineCommand({
311
+ meta: { description: "Check architecture rules" },
312
+ args: {
313
+ format: {
314
+ type: "string",
315
+ description: "Output format: text, json, github"
316
+ },
317
+ fix: {
318
+ type: "boolean",
319
+ description: "Apply auto-fixes to the source file"
320
+ },
321
+ "dry-run": {
322
+ type: "boolean",
323
+ description: "Show fixes without applying them"
324
+ }
325
+ },
326
+ // eslint-disable-next-line sonarjs/cognitive-complexity
327
+ async run({ args }) {
328
+ const config = await loadAndValidateConfig();
329
+ const model = await loadModel(config);
330
+ const results = runRules(model, config.rules);
331
+ const format = detectFormat(args.format);
332
+ switch (format) {
333
+ case "json": {
334
+ formatJson(results);
335
+ break;
336
+ }
337
+ case "github": {
338
+ formatGithub(results);
339
+ break;
340
+ }
341
+ default: {
342
+ formatText(results);
343
+ }
344
+ }
345
+ const hasViolations = results.some((r) => r.violations.length > 0);
346
+ if (args.fix || args["dry-run"]) {
347
+ if (!hasViolations) {
348
+ consola.success("No violations to fix");
349
+ return;
350
+ }
351
+ const syntax = getSyntax(config.source.type);
352
+ const fixes = generateFixes(model, results, config.rules, syntax);
353
+ if (fixes.length === 0) {
354
+ consola.info("No auto-fixes available for these violations");
355
+ if (hasViolations) {
356
+ throw new Error("Architecture rule violations found");
357
+ }
358
+ return;
359
+ }
360
+ formatFixes(fixes);
361
+ if (args["dry-run"]) {
362
+ return;
363
+ }
364
+ const sourcePath = path.resolve(config.source.path);
365
+ let source = await readFile(sourcePath, "utf8");
366
+ for (const fix of fixes) {
367
+ source = applyEdits(source, fix.edits);
368
+ }
369
+ await writeFile(sourcePath, source, "utf8");
370
+ const reModel = await loadModel(config);
371
+ const reResults = runRules(reModel, config.rules);
372
+ const remaining = reResults.reduce((n, r) => n + r.violations.length, 0);
373
+ consola.success(
374
+ `Applied ${fixes.length} fix(es), wrote ${sourcePath}` + (remaining > 0 ? ` (${remaining} violation(s) remain)` : "")
375
+ );
376
+ return;
377
+ }
378
+ if (hasViolations) {
379
+ try {
380
+ const syntax = getSyntax(config.source.type);
381
+ const fixes = generateFixes(model, results, config.rules, syntax);
382
+ if (fixes.length > 0) {
383
+ consola.info("Suggested fixes (run with --fix to apply):");
384
+ formatFixes(fixes);
385
+ }
386
+ } catch {
387
+ }
388
+ throw new Error("Architecture rule violations found");
389
+ }
390
+ }
391
+ });
392
+
393
+ const runPlantuml = async (config, outputPath) => {
394
+ const model = await loadModel(config);
395
+ const puml = generatePlantumlFromModel(model, {
396
+ boundaryLabel: config.generate?.boundaryLabel
397
+ });
398
+ if (outputPath) {
399
+ await fs.writeFile(outputPath, puml);
400
+ consola.success(`Written to ${outputPath}`);
401
+ } else {
402
+ console.log(puml);
403
+ }
404
+ };
405
+ const runKubernetes = async (config, outputDir) => {
406
+ const model = await loadModel(config);
407
+ const outputs = generateKubernetes(model);
408
+ const targetDir = outputDir ?? config.generate?.kubernetes?.path ?? "resources/kubernetes/microservices";
409
+ await fs.mkdir(targetDir, { recursive: true });
410
+ for (const output of outputs) {
411
+ const filePath = path.join(targetDir, output.fileName);
412
+ await fs.writeFile(filePath, output.content);
413
+ }
414
+ consola.success(`Generated ${outputs.length} file(s) in ${targetDir}`);
415
+ };
416
+ const generate = defineCommand({
417
+ meta: { description: "Generate architecture artifacts" },
418
+ args: {
419
+ output: {
420
+ type: "string",
421
+ description: "Output path (file for plantuml, directory for kubernetes)"
422
+ },
423
+ format: {
424
+ type: "string",
425
+ description: "Output format: plantuml, kubernetes"
426
+ }
427
+ },
428
+ async run({ args }) {
429
+ const config = await loadAndValidateConfig();
430
+ const format = args.format ?? "plantuml";
431
+ switch (format) {
432
+ case "plantuml": {
433
+ await runPlantuml(config, args.output);
434
+ break;
435
+ }
436
+ case "kubernetes": {
437
+ await runKubernetes(config, args.output);
438
+ break;
439
+ }
440
+ default: {
441
+ throw new Error(`Unknown format: ${format}`);
442
+ }
443
+ }
444
+ }
445
+ });
446
+
447
+ const template = `import { defineConfig } from "aact";
448
+
449
+ export default defineConfig({
450
+ // Source of architecture description
451
+ source: {
452
+ type: "structurizr", // "plantuml" | "structurizr"
453
+ path: "./workspace.json",
454
+ },
455
+
456
+ // Validation rules (true = enabled with defaults, false = disabled)
457
+ rules: {
458
+ acl: true, // Anti-Corruption Layer: only tagged containers depend on externals
459
+ // acl: { tag: "acl", externalType: "System_Ext" },
460
+
461
+ acyclic: true, // No circular dependencies
462
+
463
+ crud: true, // Only repo/relay containers access databases
464
+ // crud: { repoTags: ["repo", "relay"], dbType: "ContainerDb" },
465
+
466
+ dbPerService: true, // Each database accessed by single service
467
+ // dbPerService: { dbType: "ContainerDb" },
468
+
469
+ cohesion: true, // Boundary cohesion > coupling
470
+ // cohesion: { externalType: "System_Ext", internalType: "Container" },
471
+ },
472
+
473
+ // PlantUML generation from Kubernetes configs (aact generate)
474
+ // generate: {
475
+ // kubernetes: {
476
+ // path: "resources/kubernetes/microservices",
477
+ // },
478
+ // boundaryLabel: "Our system",
479
+ // },
480
+ });
481
+ `;
482
+ const configFileName = "aact.config.ts";
483
+ const init = defineCommand({
484
+ meta: { description: "Create aact.config.ts with default settings" },
485
+ async run() {
486
+ const configPath = path.resolve(process.cwd(), configFileName);
487
+ try {
488
+ await fs.access(configPath);
489
+ consola.warn(`${configFileName} already exists. Skipping.`);
490
+ return;
491
+ } catch {
492
+ }
493
+ await fs.writeFile(configPath, template);
494
+ consola.success(`Created ${configFileName}`);
495
+ }
496
+ });
497
+
498
+ const main = defineCommand({
499
+ meta: {
500
+ name: "aact",
501
+ version: "2.0.0",
502
+ description: "Architecture analysis and compliance tool"
503
+ },
504
+ subCommands: { init, check, analyze, generate }
505
+ });
506
+ void runMain(main);