@clarity-tools/cli 0.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.
Files changed (3) hide show
  1. package/README.md +158 -0
  2. package/dist/index.js +2610 -0
  3. package/package.json +47 -0
package/dist/index.js ADDED
@@ -0,0 +1,2610 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { Command as Command2 } from "commander";
5
+
6
+ // ../core/src/graph/schema.ts
7
+ import { z } from "zod";
8
+ var PortMappingSchema = z.object({
9
+ internal: z.number(),
10
+ external: z.number().optional()
11
+ });
12
+ var VolumeMountSchema = z.object({
13
+ source: z.string(),
14
+ target: z.string(),
15
+ type: z.enum(["volume", "bind", "tmpfs"]).optional()
16
+ });
17
+ var SourceInfoSchema = z.object({
18
+ file: z.string(),
19
+ format: z.enum(["docker-compose", "helm", "terraform", "ansible"]),
20
+ line: z.number().optional()
21
+ });
22
+ var ServiceNodeSchema = z.object({
23
+ id: z.string().min(1),
24
+ name: z.string(),
25
+ type: z.enum([
26
+ "container",
27
+ "database",
28
+ "cache",
29
+ "queue",
30
+ "storage",
31
+ "proxy",
32
+ "ui"
33
+ ]),
34
+ source: SourceInfoSchema,
35
+ image: z.string().optional(),
36
+ ports: z.array(PortMappingSchema).optional(),
37
+ volumes: z.array(VolumeMountSchema).optional(),
38
+ // Environment values can be strings, numbers, booleans, or null (pass-through from host)
39
+ environment: z.record(z.union([z.string(), z.number(), z.boolean(), z.null()])).optional(),
40
+ replicas: z.number().optional(),
41
+ resourceRequests: z.object({
42
+ cpu: z.string().optional(),
43
+ memory: z.string().optional()
44
+ }).optional(),
45
+ storageSize: z.string().optional(),
46
+ external: z.boolean().optional(),
47
+ description: z.string().optional(),
48
+ group: z.string().optional(),
49
+ queueRole: z.enum(["producer", "consumer", "both"]).optional()
50
+ });
51
+ var DependencyEdgeSchema = z.object({
52
+ from: z.string(),
53
+ to: z.string(),
54
+ type: z.enum([
55
+ "depends_on",
56
+ "network",
57
+ "volume",
58
+ "link",
59
+ "inferred",
60
+ "subchart"
61
+ ]),
62
+ port: z.number().optional(),
63
+ protocol: z.string().optional()
64
+ });
65
+ var GraphMetadataSchema = z.object({
66
+ project: z.string(),
67
+ parsedAt: z.string().datetime(),
68
+ sourceFiles: z.array(z.string()),
69
+ parserVersion: z.string()
70
+ });
71
+ var InfraGraphSchema = z.object({
72
+ nodes: z.array(ServiceNodeSchema),
73
+ edges: z.array(DependencyEdgeSchema),
74
+ metadata: GraphMetadataSchema
75
+ }).refine(
76
+ (graph) => {
77
+ const nodeIds = new Set(graph.nodes.map((n) => n.id));
78
+ return graph.edges.every((e) => nodeIds.has(e.from) && nodeIds.has(e.to));
79
+ },
80
+ { message: "Edge references non-existent node" }
81
+ ).refine(
82
+ (graph) => {
83
+ const ids = graph.nodes.map((n) => n.id);
84
+ return new Set(ids).size === ids.length;
85
+ },
86
+ { message: "Duplicate node IDs detected" }
87
+ );
88
+
89
+ // ../core/src/graph/builder.ts
90
+ var GraphBuilder = class {
91
+ nodes = /* @__PURE__ */ new Map();
92
+ edges = [];
93
+ sourceFiles = [];
94
+ project;
95
+ constructor(project) {
96
+ this.project = project;
97
+ }
98
+ addSourceFile(file) {
99
+ if (!this.sourceFiles.includes(file)) {
100
+ this.sourceFiles.push(file);
101
+ }
102
+ return this;
103
+ }
104
+ addNode(id, name, type, source, options) {
105
+ this.nodes.set(id, {
106
+ id,
107
+ name,
108
+ type,
109
+ source,
110
+ ...options
111
+ });
112
+ return this;
113
+ }
114
+ addEdge(from, to, type, options) {
115
+ if (this.nodes.has(from) && this.nodes.has(to)) {
116
+ const existingEdge = this.edges.find(
117
+ (e) => e.from === from && e.to === to
118
+ );
119
+ if (type === "inferred" && existingEdge) {
120
+ return this;
121
+ }
122
+ const duplicateExists = this.edges.some(
123
+ (e) => e.from === from && e.to === to && e.type === type
124
+ );
125
+ if (duplicateExists) {
126
+ return this;
127
+ }
128
+ this.edges.push({
129
+ from,
130
+ to,
131
+ type,
132
+ ...options
133
+ });
134
+ }
135
+ return this;
136
+ }
137
+ hasNode(id) {
138
+ return this.nodes.has(id);
139
+ }
140
+ getNode(id) {
141
+ return this.nodes.get(id);
142
+ }
143
+ build() {
144
+ return {
145
+ nodes: Array.from(this.nodes.values()),
146
+ edges: this.edges,
147
+ metadata: {
148
+ project: this.project,
149
+ parsedAt: (/* @__PURE__ */ new Date()).toISOString(),
150
+ sourceFiles: this.sourceFiles,
151
+ parserVersion: "0.1.0"
152
+ }
153
+ };
154
+ }
155
+ };
156
+
157
+ // ../core/src/parsers/docker-compose.ts
158
+ import { parse as parseYaml } from "yaml";
159
+
160
+ // ../core/src/parsers/utils.ts
161
+ function inferServiceType(serviceName, image) {
162
+ const name = (image ?? serviceName).toLowerCase();
163
+ if (name.includes("postgres") || name.includes("mysql") || name.includes("mariadb") || name.includes("mongo") || name.includes("clickhouse") || name.includes("cassandra") || name.includes("cockroach") || name.includes("schema")) {
164
+ return "database";
165
+ }
166
+ if (name.includes("redis") || name.includes("memcache") || name.includes("keydb") || name.includes("elasticsearch") || name.includes("opensearch")) {
167
+ return "cache";
168
+ }
169
+ if (name.includes("kafka") || name.includes("rabbitmq") || name.includes("nats") || name.includes("pulsar") || name.includes("zookeeper")) {
170
+ return "queue";
171
+ }
172
+ if (name.includes("minio") || name.includes("seaweedfs") || name.includes("seaweed") || name.includes("objectstorage") || name.includes("s3") || name.includes("gcs") || name.includes("ceph")) {
173
+ return "storage";
174
+ }
175
+ if (name.includes("nginx") || name.includes("traefik") || name.includes("haproxy") || name.includes("envoy") || name.includes("caddy")) {
176
+ return "proxy";
177
+ }
178
+ if ((name.includes("web") || name.includes("frontend") || name.includes("ui") || name.includes("console") || name.includes("dashboard") || name.includes("portal")) && !name.includes("grafana") && !name.includes("kibana")) {
179
+ return "ui";
180
+ }
181
+ return "container";
182
+ }
183
+
184
+ // ../core/src/parsers/docker-compose.ts
185
+ var yamlParseOptions = {
186
+ // Allow unlimited aliases for complex docker-compose files like Sentry
187
+ maxAliasCount: -1,
188
+ // Enable YAML merge key (<<) support for docker-compose anchor inheritance
189
+ merge: true
190
+ };
191
+ function parsePort(port) {
192
+ if (typeof port === "object") {
193
+ if (port.target) {
194
+ return {
195
+ internal: port.target,
196
+ external: port.published
197
+ };
198
+ }
199
+ return null;
200
+ }
201
+ const str = String(port);
202
+ const parts = str.split(":").map((p) => p.split("/")[0]);
203
+ if (parts.length === 1) {
204
+ const num = Number.parseInt(parts[0] ?? "", 10);
205
+ return Number.isNaN(num) ? null : { internal: num };
206
+ }
207
+ if (parts.length === 2) {
208
+ const ext = Number.parseInt(parts[0] ?? "", 10);
209
+ const int = Number.parseInt(parts[1] ?? "", 10);
210
+ return Number.isNaN(ext) || Number.isNaN(int) ? null : { internal: int, external: ext };
211
+ }
212
+ if (parts.length === 3) {
213
+ const ext = Number.parseInt(parts[1] ?? "", 10);
214
+ const int = Number.parseInt(parts[2] ?? "", 10);
215
+ return Number.isNaN(ext) || Number.isNaN(int) ? null : { internal: int, external: ext };
216
+ }
217
+ return null;
218
+ }
219
+ function parseVolume(vol) {
220
+ if (typeof vol === "object") {
221
+ if (vol.source && vol.target) {
222
+ return {
223
+ source: vol.source,
224
+ target: vol.target,
225
+ type: vol.type
226
+ };
227
+ }
228
+ return null;
229
+ }
230
+ const str = String(vol);
231
+ const parts = str.split(":");
232
+ if (parts.length >= 2) {
233
+ const source = parts[0];
234
+ const target = parts[1];
235
+ if (source && target) {
236
+ const type = source.startsWith("/") || source.startsWith(".") ? "bind" : "volume";
237
+ return { source, target, type };
238
+ }
239
+ }
240
+ return null;
241
+ }
242
+ function parseEnvironment(env) {
243
+ if (!env) return void 0;
244
+ if (Array.isArray(env)) {
245
+ const record = {};
246
+ for (const item of env) {
247
+ const idx = item.indexOf("=");
248
+ if (idx > 0) {
249
+ const key = item.slice(0, idx);
250
+ const value = item.slice(idx + 1);
251
+ record[key] = value;
252
+ }
253
+ }
254
+ return Object.keys(record).length > 0 ? record : void 0;
255
+ }
256
+ return Object.keys(env).length > 0 ? env : void 0;
257
+ }
258
+ function parseDockerCompose(content, filename, project) {
259
+ const compose = parseYaml(content, yamlParseOptions);
260
+ if (!compose?.services) {
261
+ return {
262
+ nodes: [],
263
+ edges: [],
264
+ metadata: {
265
+ project,
266
+ parsedAt: (/* @__PURE__ */ new Date()).toISOString(),
267
+ sourceFiles: [filename],
268
+ parserVersion: "0.1.0"
269
+ }
270
+ };
271
+ }
272
+ const builder = new GraphBuilder(project);
273
+ builder.addSourceFile(filename);
274
+ for (const [serviceName, service] of Object.entries(compose.services)) {
275
+ const image = service.image ?? (typeof service.build === "string" ? `build:${service.build}` : void 0);
276
+ const type = inferServiceType(serviceName, image);
277
+ const ports = service.ports?.map(parsePort).filter((p) => p !== null);
278
+ const volumes = service.volumes?.map(parseVolume).filter((v) => v !== null);
279
+ const environment = parseEnvironment(service.environment);
280
+ builder.addNode(
281
+ serviceName,
282
+ serviceName,
283
+ type,
284
+ { file: filename, format: "docker-compose" },
285
+ {
286
+ image,
287
+ ports: ports?.length ? ports : void 0,
288
+ volumes: volumes?.length ? volumes : void 0,
289
+ environment,
290
+ replicas: service.deploy?.replicas
291
+ }
292
+ );
293
+ }
294
+ for (const [serviceName, service] of Object.entries(compose.services)) {
295
+ if (service.depends_on) {
296
+ const deps = Array.isArray(service.depends_on) ? service.depends_on : Object.keys(service.depends_on);
297
+ for (const dep of deps) {
298
+ builder.addEdge(serviceName, dep, "depends_on");
299
+ }
300
+ }
301
+ if (service.links) {
302
+ for (const link of service.links) {
303
+ const target = link.split(":")[0];
304
+ if (target) {
305
+ builder.addEdge(serviceName, target, "link");
306
+ }
307
+ }
308
+ }
309
+ if (service.environment) {
310
+ const envVars = Array.isArray(service.environment) ? service.environment : Object.entries(service.environment).map(([k, v]) => `${k}=${v}`);
311
+ for (const envVar of envVars) {
312
+ for (const otherService of Object.keys(compose.services)) {
313
+ if (otherService === serviceName) continue;
314
+ const envLower = envVar.toLowerCase();
315
+ const servicePattern = otherService.toLowerCase().replace(/-/g, "_");
316
+ if (envLower.includes(`${servicePattern}_host`) || envLower.includes(`${servicePattern}_url`) || envLower.includes(`${servicePattern}:`) || envVar.includes(`://${otherService}:`) || envVar.includes(`://${otherService}/`)) {
317
+ builder.addEdge(serviceName, otherService, "inferred");
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+ return builder.build();
324
+ }
325
+
326
+ // ../core/src/parsers/helm/index.ts
327
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
328
+ import { join as join2, relative } from "path";
329
+
330
+ // ../core/src/parsers/helm/chart.ts
331
+ import { parse as parseYaml2 } from "yaml";
332
+ var yamlParseOptions2 = {
333
+ maxAliasCount: -1,
334
+ merge: true
335
+ };
336
+ function isRecord(value) {
337
+ return typeof value === "object" && value !== null && !Array.isArray(value);
338
+ }
339
+ function parseDependencies(value) {
340
+ if (!Array.isArray(value)) return [];
341
+ const deps = [];
342
+ for (const item of value) {
343
+ if (!isRecord(item)) continue;
344
+ const name = typeof item.name === "string" ? item.name : void 0;
345
+ if (!name) continue;
346
+ const tags = Array.isArray(item.tags) && item.tags.every((t) => typeof t === "string") ? item.tags : void 0;
347
+ deps.push({
348
+ name,
349
+ version: typeof item.version === "string" ? item.version : void 0,
350
+ repository: typeof item.repository === "string" ? item.repository : void 0,
351
+ condition: typeof item.condition === "string" ? item.condition : void 0,
352
+ tags
353
+ });
354
+ }
355
+ return deps;
356
+ }
357
+ function parseChartYaml(content) {
358
+ const chart = parseYaml2(content, yamlParseOptions2);
359
+ if (!isRecord(chart)) return null;
360
+ const name = typeof chart.name === "string" ? chart.name : void 0;
361
+ if (!name) return null;
362
+ return {
363
+ name,
364
+ version: typeof chart.version === "string" ? chart.version : void 0,
365
+ appVersion: typeof chart.appVersion === "string" ? chart.appVersion : void 0,
366
+ description: typeof chart.description === "string" ? chart.description : void 0,
367
+ dependencies: parseDependencies(chart.dependencies)
368
+ };
369
+ }
370
+
371
+ // ../core/src/parsers/helm/values.ts
372
+ import { parse as parseYaml3 } from "yaml";
373
+ var yamlParseOptions3 = {
374
+ maxAliasCount: -1,
375
+ merge: true
376
+ };
377
+ function parseValuesYaml(content) {
378
+ const values = parseYaml3(content, yamlParseOptions3);
379
+ if (!isRecord2(values)) return {};
380
+ return values;
381
+ }
382
+ function isRecord2(value) {
383
+ return typeof value === "object" && value !== null && !Array.isArray(value);
384
+ }
385
+ function getValueAtPath(values, path) {
386
+ const parts = path.split(".").filter(Boolean);
387
+ let current = values;
388
+ for (const part of parts) {
389
+ if (!isRecord2(current)) return void 0;
390
+ current = current[part];
391
+ }
392
+ return current;
393
+ }
394
+ function getString(value) {
395
+ return typeof value === "string" ? value : void 0;
396
+ }
397
+ function getNumber(value) {
398
+ if (typeof value === "number")
399
+ return Number.isFinite(value) ? value : void 0;
400
+ if (typeof value === "string") {
401
+ const parsed = Number.parseFloat(value);
402
+ return Number.isFinite(parsed) ? parsed : void 0;
403
+ }
404
+ return void 0;
405
+ }
406
+ function getBoolean(value) {
407
+ if (typeof value === "boolean") return value;
408
+ if (typeof value === "string") {
409
+ if (value.toLowerCase() === "true") return true;
410
+ if (value.toLowerCase() === "false") return false;
411
+ }
412
+ return void 0;
413
+ }
414
+ function normalizeRegistry(repository, registry) {
415
+ if (!registry) return repository;
416
+ if (repository.includes("/")) return repository;
417
+ return `${registry}/${repository}`;
418
+ }
419
+ function extractImage(values) {
420
+ const image = values.image;
421
+ if (typeof image === "string") {
422
+ return image;
423
+ }
424
+ if (isRecord2(image)) {
425
+ const repository = getString(image.repository) ?? getString(image.name);
426
+ if (!repository) return void 0;
427
+ const registry = getString(image.registry) ?? getString(getValueAtPath(values, "global.imageRegistry"));
428
+ const normalizedRepo = normalizeRegistry(repository, registry);
429
+ const tag = getString(image.tag);
430
+ if (tag && !normalizedRepo.includes(":")) {
431
+ return `${normalizedRepo}:${tag}`;
432
+ }
433
+ return normalizedRepo;
434
+ }
435
+ return void 0;
436
+ }
437
+ function parsePortValue(value) {
438
+ const mappings = [];
439
+ if (typeof value === "number") {
440
+ mappings.push({ internal: value });
441
+ return mappings;
442
+ }
443
+ if (typeof value === "string") {
444
+ const parsed = Number.parseInt(value, 10);
445
+ if (!Number.isNaN(parsed)) {
446
+ mappings.push({ internal: parsed });
447
+ }
448
+ return mappings;
449
+ }
450
+ if (Array.isArray(value)) {
451
+ for (const entry of value) {
452
+ mappings.push(...parsePortValue(entry));
453
+ }
454
+ return mappings;
455
+ }
456
+ if (isRecord2(value)) {
457
+ const port = getNumber(value.port);
458
+ const targetPort = getNumber(value.targetPort) ?? getNumber(value.containerPort) ?? getNumber(value.containerPorts);
459
+ if (port && targetPort) {
460
+ mappings.push({ internal: targetPort, external: port });
461
+ return mappings;
462
+ }
463
+ if (targetPort) {
464
+ mappings.push({ internal: targetPort });
465
+ return mappings;
466
+ }
467
+ if (port) {
468
+ mappings.push({ internal: port });
469
+ return mappings;
470
+ }
471
+ for (const entry of Object.values(value)) {
472
+ mappings.push(...parsePortValue(entry));
473
+ }
474
+ }
475
+ return mappings;
476
+ }
477
+ function extractPorts(values) {
478
+ const ports = [];
479
+ const service = isRecord2(values.service) ? values.service : void 0;
480
+ if (service) {
481
+ if ("ports" in service) {
482
+ ports.push(...parsePortValue(service.ports));
483
+ }
484
+ if ("port" in service) {
485
+ ports.push(...parsePortValue(service.port));
486
+ }
487
+ }
488
+ if ("containerPorts" in values) {
489
+ ports.push(...parsePortValue(values.containerPorts));
490
+ }
491
+ if ("containerPort" in values) {
492
+ ports.push(...parsePortValue(values.containerPort));
493
+ }
494
+ if ("ports" in values) {
495
+ ports.push(...parsePortValue(values.ports));
496
+ }
497
+ const seen = /* @__PURE__ */ new Set();
498
+ const deduped = [];
499
+ for (const mapping of ports) {
500
+ const key = `${mapping.external ?? "internal"}-${mapping.internal}`;
501
+ if (seen.has(key)) continue;
502
+ seen.add(key);
503
+ deduped.push(mapping);
504
+ }
505
+ return deduped.length ? deduped : void 0;
506
+ }
507
+ function extractReplicas(values) {
508
+ const replicaCount = getNumber(values.replicaCount) ?? getNumber(values.replicas);
509
+ return replicaCount !== void 0 ? Math.round(replicaCount) : void 0;
510
+ }
511
+ function extractResourceRequests(values) {
512
+ const resources = isRecord2(values.resources) ? values.resources : void 0;
513
+ const requests = resources && isRecord2(resources.requests) ? resources.requests : void 0;
514
+ const cpu = getString(requests?.cpu);
515
+ const memory = getString(requests?.memory);
516
+ if (!cpu && !memory) return void 0;
517
+ return {
518
+ cpu: cpu || void 0,
519
+ memory: memory || void 0
520
+ };
521
+ }
522
+ function extractStorageSize(values) {
523
+ const persistence = isRecord2(values.persistence) ? values.persistence : void 0;
524
+ if (!persistence) return void 0;
525
+ return getString(persistence.size) ?? getString(persistence.storageSize) ?? getString(persistence.storage);
526
+ }
527
+ function extractServiceConfig(values) {
528
+ return {
529
+ image: extractImage(values),
530
+ ports: extractPorts(values),
531
+ replicas: extractReplicas(values),
532
+ resourceRequests: extractResourceRequests(values),
533
+ storageSize: extractStorageSize(values)
534
+ };
535
+ }
536
+
537
+ // ../core/src/parsers/helm/components.ts
538
+ var SKIP_KEYS = /* @__PURE__ */ new Set([
539
+ "global",
540
+ "image",
541
+ "init",
542
+ "service",
543
+ "resources",
544
+ "persistence",
545
+ "ingress",
546
+ "rbac",
547
+ "serviceAccount",
548
+ "nodeSelector",
549
+ "tolerations",
550
+ "affinity",
551
+ "autoscaling",
552
+ "metrics",
553
+ "networkPolicy",
554
+ "securityContext",
555
+ "podSecurityContext",
556
+ "volumePermissions",
557
+ "extraEnvVars",
558
+ "extraVolumeMounts",
559
+ "extraVolumes",
560
+ "fullnameOverride",
561
+ "nameOverride"
562
+ ]);
563
+ function hasComponentMarkers(values) {
564
+ return "replicaCount" in values || "replicas" in values || "containerPorts" in values || "containerPort" in values || "service" in values && isRecord2(values.service) || "image" in values || "resources" in values;
565
+ }
566
+ function detectComponents(values, options) {
567
+ const components = [];
568
+ const dependencyNames = new Set(
569
+ options?.dependencyNames?.map((name) => name.toLowerCase()) ?? []
570
+ );
571
+ for (const [key, value] of Object.entries(values)) {
572
+ if (!isRecord2(value)) continue;
573
+ if (SKIP_KEYS.has(key)) continue;
574
+ if (dependencyNames.has(key.toLowerCase())) continue;
575
+ if (key.startsWith("external")) continue;
576
+ const enabled = getBoolean(value.enabled);
577
+ if (enabled === false) continue;
578
+ if (!hasComponentMarkers(value)) continue;
579
+ components.push({ name: key, values: value });
580
+ }
581
+ return components;
582
+ }
583
+
584
+ // ../core/src/parsers/helm/rendered.ts
585
+ import { execFileSync } from "child_process";
586
+ import { readFileSync, readdirSync } from "fs";
587
+ import { join } from "path";
588
+ import { parseAllDocuments } from "yaml";
589
+ var yamlParseOptions4 = {
590
+ maxAliasCount: -1,
591
+ merge: true
592
+ };
593
+ function isRecord3(value) {
594
+ return typeof value === "object" && value !== null && !Array.isArray(value);
595
+ }
596
+ function normalizeName(value) {
597
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
598
+ }
599
+ function renderHelmTemplate(chartDir, releaseName, valuesFiles = [], showOnly) {
600
+ try {
601
+ const args = ["template", releaseName, chartDir, "--namespace", "default"];
602
+ for (const file of valuesFiles) {
603
+ args.push("--values", file);
604
+ }
605
+ if (showOnly) {
606
+ args.push("--show-only", showOnly);
607
+ }
608
+ return execFileSync("helm", args, {
609
+ encoding: "utf-8",
610
+ maxBuffer: 20 * 1024 * 1024,
611
+ stdio: ["ignore", "pipe", "pipe"]
612
+ });
613
+ } catch {
614
+ return null;
615
+ }
616
+ }
617
+ function listTemplateFiles(chartDir) {
618
+ const templatesDir = join(chartDir, "templates");
619
+ try {
620
+ const files = [];
621
+ const queue = [
622
+ { dir: templatesDir, relativeDir: "templates" }
623
+ ];
624
+ while (queue.length) {
625
+ const current = queue.pop();
626
+ if (!current) continue;
627
+ const dirEntries = readdirSync(current.dir, { withFileTypes: true });
628
+ for (const entry of dirEntries) {
629
+ if (entry.name.startsWith("_")) continue;
630
+ if (entry.name === "NOTES.txt") continue;
631
+ const fullPath = join(current.dir, entry.name);
632
+ const relPath = join(current.relativeDir, entry.name);
633
+ if (entry.isDirectory()) {
634
+ queue.push({ dir: fullPath, relativeDir: relPath });
635
+ continue;
636
+ }
637
+ if (entry.isFile() && (entry.name.endsWith(".yaml") || entry.name.endsWith(".yml"))) {
638
+ files.push(relPath);
639
+ }
640
+ }
641
+ }
642
+ return files;
643
+ } catch {
644
+ return [];
645
+ }
646
+ }
647
+ function normalizeTemplateText(text, releaseName) {
648
+ let normalized = text;
649
+ normalized = normalized.replace(
650
+ /{{\s*include\s+"[^"]+\.fullname"[^}]*}}/g,
651
+ releaseName
652
+ );
653
+ normalized = normalized.replace(
654
+ /{{\s*include\s+"[^"]+\.componentname"\s*\(list\s+\$\s+"([^"]+)"\s*\)\s*}}/g,
655
+ `${releaseName}-$1`
656
+ );
657
+ return normalized;
658
+ }
659
+ function resolveSourceFromTemplatePath(templatePath, componentMap, aliasMap) {
660
+ const base = templatePath.split("/").pop() ?? templatePath;
661
+ const normalized = normalizeName(base.replace(/\.ya?ml$/, ""));
662
+ const aliasEntries = Array.from(aliasMap.entries()).sort(
663
+ (a, b) => b[0].length - a[0].length
664
+ );
665
+ for (const [alias, nodeId] of aliasEntries) {
666
+ if (normalized.includes(alias)) {
667
+ return nodeId;
668
+ }
669
+ }
670
+ const componentEntries = Array.from(componentMap.entries()).sort(
671
+ (a, b) => b[0].length - a[0].length
672
+ );
673
+ for (const [component, nodeId] of componentEntries) {
674
+ if (normalized.includes(component)) {
675
+ return nodeId;
676
+ }
677
+ }
678
+ return void 0;
679
+ }
680
+ function buildServiceTargets(releaseName, componentMap, aliasMap) {
681
+ const targets = /* @__PURE__ */ new Map();
682
+ for (const [component, nodeId] of componentMap) {
683
+ targets.set(`${releaseName}-${component}`, nodeId);
684
+ }
685
+ for (const [alias, nodeId] of aliasMap) {
686
+ targets.set(`${releaseName}-${alias}`, nodeId);
687
+ }
688
+ return targets;
689
+ }
690
+ function inferEdgesFromTemplates(options) {
691
+ const templates = listTemplateFiles(options.chartDir);
692
+ if (templates.length === 0) {
693
+ return {
694
+ edges: [],
695
+ externalServices: [],
696
+ renderedComponents: [],
697
+ workloadComponents: [],
698
+ jobComponents: []
699
+ };
700
+ }
701
+ const serviceTargets = buildServiceTargets(
702
+ options.releaseName,
703
+ options.componentMap,
704
+ options.aliasMap
705
+ );
706
+ const edges = [];
707
+ const edgeKeys = /* @__PURE__ */ new Set();
708
+ const externalServices = /* @__PURE__ */ new Map();
709
+ for (const template of templates) {
710
+ const sourceNode = resolveSourceFromTemplatePath(
711
+ template,
712
+ options.componentMap,
713
+ options.aliasMap
714
+ );
715
+ if (!sourceNode) continue;
716
+ let contents = "";
717
+ try {
718
+ contents = readFileSync(join(options.chartDir, template), "utf-8");
719
+ } catch {
720
+ continue;
721
+ }
722
+ const normalized = normalizeTemplateText(contents, options.releaseName);
723
+ const lines = normalized.split("\n");
724
+ for (const line of lines) {
725
+ for (const [serviceName, targetNode] of serviceTargets) {
726
+ if (sourceNode === targetNode) continue;
727
+ if (!valueIncludesService(line, serviceName)) continue;
728
+ const key = `${sourceNode}->${targetNode}`;
729
+ if (edgeKeys.has(key)) continue;
730
+ edgeKeys.add(key);
731
+ edges.push({ from: sourceNode, to: targetNode, type: "inferred" });
732
+ }
733
+ const matches = line.matchAll(/([a-zA-Z0-9.-]+):(\d{2,5})/g);
734
+ for (const match of matches) {
735
+ const host = match[1];
736
+ const port = Number.parseInt(match[2] ?? "", 10);
737
+ if (!host || Number.isNaN(port)) continue;
738
+ const normalizedHost = host.toLowerCase();
739
+ const hostBase = normalizedHost.split(".")[0] ?? normalizedHost;
740
+ if (!hostBase) continue;
741
+ if (normalizedHost === "localhost" || /^\d{1,3}(\.\d{1,3}){3}$/.test(normalizedHost) || /^\d+$/.test(hostBase)) {
742
+ continue;
743
+ }
744
+ if (serviceTargets.has(hostBase) || serviceTargets.has(normalizedHost)) {
745
+ const targetNode = serviceTargets.get(hostBase) ?? serviceTargets.get(normalizedHost);
746
+ if (!targetNode || sourceNode === targetNode) continue;
747
+ const key2 = `${sourceNode}->${targetNode}`;
748
+ if (edgeKeys.has(key2)) continue;
749
+ edgeKeys.add(key2);
750
+ edges.push({ from: sourceNode, to: targetNode, type: "inferred" });
751
+ continue;
752
+ }
753
+ const externalId = `external-${hostBase}`;
754
+ if (!externalServices.has(externalId)) {
755
+ externalServices.set(externalId, {
756
+ id: externalId,
757
+ name: hostBase,
758
+ port
759
+ });
760
+ }
761
+ const key = `${sourceNode}->${externalId}`;
762
+ if (edgeKeys.has(key)) continue;
763
+ edgeKeys.add(key);
764
+ edges.push({ from: sourceNode, to: externalId, type: "inferred" });
765
+ }
766
+ }
767
+ }
768
+ return {
769
+ edges,
770
+ externalServices: Array.from(externalServices.values()),
771
+ renderedComponents: [],
772
+ workloadComponents: [],
773
+ jobComponents: []
774
+ };
775
+ }
776
+ function parseRenderedResources(content) {
777
+ const documents = parseAllDocuments(content, yamlParseOptions4);
778
+ const resources = [];
779
+ for (const doc of documents) {
780
+ const data = doc.toJSON();
781
+ if (!isRecord3(data)) continue;
782
+ resources.push(data);
783
+ }
784
+ return resources;
785
+ }
786
+ function getLabels(resource) {
787
+ const metadataLabels = resource.metadata?.labels && isRecord3(resource.metadata.labels) ? resource.metadata.labels : {};
788
+ const templateLabels = (() => {
789
+ const spec = resource.spec;
790
+ if (!spec || !isRecord3(spec)) return {};
791
+ const template = spec.template;
792
+ if (!isRecord3(template)) return {};
793
+ const metadata = template.metadata;
794
+ if (!isRecord3(metadata)) return {};
795
+ const labels = metadata.labels;
796
+ if (!isRecord3(labels)) return {};
797
+ return labels;
798
+ })();
799
+ return { ...metadataLabels, ...templateLabels };
800
+ }
801
+ function getPodSpec(resource) {
802
+ const spec = resource.spec;
803
+ if (!spec || !isRecord3(spec)) return null;
804
+ switch (resource.kind) {
805
+ case "Deployment":
806
+ case "StatefulSet":
807
+ case "DaemonSet":
808
+ case "ReplicaSet":
809
+ if (isRecord3(spec.template) && isRecord3(spec.template.spec) && spec.template.spec) {
810
+ return spec.template.spec;
811
+ }
812
+ return null;
813
+ case "Job":
814
+ if (isRecord3(spec.template) && isRecord3(spec.template.spec) && spec.template.spec) {
815
+ return spec.template.spec;
816
+ }
817
+ return null;
818
+ case "CronJob": {
819
+ const jobTemplate = spec.jobTemplate;
820
+ if (isRecord3(jobTemplate) && isRecord3(jobTemplate.spec) && isRecord3(jobTemplate.spec.template) && isRecord3(jobTemplate.spec.template.spec)) {
821
+ return jobTemplate.spec.template.spec;
822
+ }
823
+ return null;
824
+ }
825
+ case "Pod":
826
+ if (isRecord3(spec)) {
827
+ return spec;
828
+ }
829
+ return null;
830
+ default:
831
+ return null;
832
+ }
833
+ }
834
+ function collectConfigMapNames(podSpec) {
835
+ const names = [];
836
+ for (const volume of podSpec.volumes ?? []) {
837
+ const name = volume.configMap?.name;
838
+ if (name) names.push(name);
839
+ }
840
+ const containers = [
841
+ ...podSpec.containers ?? [],
842
+ ...podSpec.initContainers ?? []
843
+ ];
844
+ for (const container of containers) {
845
+ for (const envFrom of container.envFrom ?? []) {
846
+ const name = envFrom.configMapRef?.name;
847
+ if (name) names.push(name);
848
+ }
849
+ }
850
+ return names;
851
+ }
852
+ function collectStringsFromPodSpec(podSpec, configMaps) {
853
+ const values = [];
854
+ const containers = [
855
+ ...podSpec.containers ?? [],
856
+ ...podSpec.initContainers ?? []
857
+ ];
858
+ for (const container of containers) {
859
+ for (const env of container.env ?? []) {
860
+ if (typeof env.value === "string") {
861
+ values.push(env.value);
862
+ }
863
+ }
864
+ if (Array.isArray(container.args)) {
865
+ values.push(
866
+ ...container.args.filter((value) => typeof value === "string")
867
+ );
868
+ }
869
+ if (Array.isArray(container.command)) {
870
+ values.push(
871
+ ...container.command.filter((value) => typeof value === "string")
872
+ );
873
+ }
874
+ }
875
+ for (const name of collectConfigMapNames(podSpec)) {
876
+ const data = configMaps.get(name);
877
+ if (data) {
878
+ values.push(...data);
879
+ }
880
+ }
881
+ return values;
882
+ }
883
+ function valueIncludesService(value, serviceName) {
884
+ const normalizedValue = value.toLowerCase();
885
+ const normalizedService = serviceName.toLowerCase();
886
+ const pattern = new RegExp(
887
+ `(^|[\\s:/"'=\\(])${normalizedService.replace(/[-/\\^$*+?.()|[\\]{}]/g, "\\$&")}(?=$|[\\s:/"'=.\\)])`,
888
+ "i"
889
+ );
890
+ return normalizedValue.includes(normalizedService) && pattern.test(normalizedValue);
891
+ }
892
+ function resolveNodeIdFromComponent(component, componentMap, aliasMap) {
893
+ if (!component) return void 0;
894
+ const normalized = normalizeName(component);
895
+ return componentMap.get(normalized) ?? aliasMap.get(normalized);
896
+ }
897
+ function resolveNodeIdFromName(resourceName, componentMap, aliasMap) {
898
+ if (!resourceName) return void 0;
899
+ const normalized = normalizeName(resourceName);
900
+ for (const [component, nodeId] of componentMap) {
901
+ if (normalized === component || normalized.endsWith(`-${component}`)) {
902
+ return nodeId;
903
+ }
904
+ }
905
+ for (const [alias, nodeId] of aliasMap) {
906
+ if (normalized === alias || normalized.endsWith(`-${alias}`)) {
907
+ return nodeId;
908
+ }
909
+ }
910
+ return void 0;
911
+ }
912
+ function inferEdgesFromRenderedManifests(options) {
913
+ const manifest = renderHelmTemplate(
914
+ options.chartDir,
915
+ options.releaseName,
916
+ options.valuesFiles ?? []
917
+ );
918
+ if (!manifest) {
919
+ console.warn(
920
+ "Helm render failed; falling back to static template scanning. Some dependencies may be missing."
921
+ );
922
+ return inferEdgesFromTemplates(options);
923
+ }
924
+ const resources = parseRenderedResources(manifest);
925
+ const serviceTargets = /* @__PURE__ */ new Map();
926
+ const configMaps = /* @__PURE__ */ new Map();
927
+ const externalServices = /* @__PURE__ */ new Map();
928
+ const renderedComponents = /* @__PURE__ */ new Set();
929
+ const workloadComponents = /* @__PURE__ */ new Set();
930
+ const jobComponents = /* @__PURE__ */ new Set();
931
+ const longRunningKinds = /* @__PURE__ */ new Set([
932
+ "Deployment",
933
+ "StatefulSet",
934
+ "DaemonSet",
935
+ "ReplicaSet",
936
+ "Pod"
937
+ ]);
938
+ for (const resource of resources) {
939
+ if (resource.kind === "ConfigMap" && resource.metadata?.name) {
940
+ const rawData = resource.data;
941
+ if (isRecord3(rawData)) {
942
+ const strings = Object.values(rawData).filter(
943
+ (value) => typeof value === "string"
944
+ );
945
+ if (strings.length > 0) {
946
+ configMaps.set(resource.metadata.name, strings);
947
+ }
948
+ }
949
+ }
950
+ const labels = getLabels(resource);
951
+ const componentLabel = labels["app.kubernetes.io/component"] ?? labels.app;
952
+ const resolvedNode = resolveNodeIdFromComponent(
953
+ componentLabel,
954
+ options.componentMap,
955
+ options.aliasMap
956
+ ) ?? resolveNodeIdFromName(
957
+ resource.metadata?.name,
958
+ options.componentMap,
959
+ options.aliasMap
960
+ );
961
+ if (!resolvedNode) continue;
962
+ renderedComponents.add(resolvedNode);
963
+ if (resource.kind === "Service" && resource.metadata?.name) {
964
+ serviceTargets.set(resource.metadata.name, resolvedNode);
965
+ workloadComponents.add(resolvedNode);
966
+ }
967
+ if (resource.kind && longRunningKinds.has(resource.kind)) {
968
+ workloadComponents.add(resolvedNode);
969
+ }
970
+ if (resource.kind === "Job" || resource.kind === "CronJob") {
971
+ jobComponents.add(resolvedNode);
972
+ }
973
+ }
974
+ const edges = [];
975
+ const edgeKeys = /* @__PURE__ */ new Set();
976
+ const serviceNames = /* @__PURE__ */ new Set();
977
+ for (const [serviceName] of serviceTargets) {
978
+ serviceNames.add(serviceName.toLowerCase());
979
+ }
980
+ for (const resource of resources) {
981
+ if (![
982
+ "Deployment",
983
+ "StatefulSet",
984
+ "DaemonSet",
985
+ "ReplicaSet",
986
+ "Job",
987
+ "CronJob",
988
+ "Pod"
989
+ ].includes(resource.kind ?? "")) {
990
+ continue;
991
+ }
992
+ const labels = getLabels(resource);
993
+ const componentLabel = labels["app.kubernetes.io/component"] ?? labels.app;
994
+ const sourceNode = resolveNodeIdFromComponent(
995
+ componentLabel,
996
+ options.componentMap,
997
+ options.aliasMap
998
+ ) ?? resolveNodeIdFromName(
999
+ resource.metadata?.name,
1000
+ options.componentMap,
1001
+ options.aliasMap
1002
+ );
1003
+ if (!sourceNode) continue;
1004
+ const podSpec = getPodSpec(resource);
1005
+ if (!podSpec) continue;
1006
+ const strings = collectStringsFromPodSpec(podSpec, configMaps);
1007
+ if (strings.length === 0) continue;
1008
+ for (const [serviceName, targetNode] of serviceTargets) {
1009
+ if (sourceNode === targetNode) continue;
1010
+ for (const value of strings) {
1011
+ if (!valueIncludesService(value, serviceName)) continue;
1012
+ const key = `${sourceNode}->${targetNode}`;
1013
+ if (edgeKeys.has(key)) continue;
1014
+ edgeKeys.add(key);
1015
+ edges.push({ from: sourceNode, to: targetNode, type: "inferred" });
1016
+ }
1017
+ }
1018
+ for (const value of strings) {
1019
+ const matches = value.matchAll(/([a-zA-Z0-9.-]+):(\d{2,5})/g);
1020
+ for (const match of matches) {
1021
+ const host = match[1];
1022
+ const port = Number.parseInt(match[2] ?? "", 10);
1023
+ if (!host || Number.isNaN(port)) continue;
1024
+ const normalizedHost = host.toLowerCase();
1025
+ const hostBase = normalizedHost.split(".")[0] ?? normalizedHost;
1026
+ if (!hostBase) continue;
1027
+ if (normalizedHost === "localhost" || /^\d{1,3}(\.\d{1,3}){3}$/.test(normalizedHost) || /^\d+$/.test(hostBase)) {
1028
+ continue;
1029
+ }
1030
+ if (serviceNames.has(hostBase) || serviceNames.has(normalizedHost)) {
1031
+ const targetNode = serviceTargets.get(hostBase) ?? serviceTargets.get(normalizedHost);
1032
+ if (!targetNode || sourceNode === targetNode) continue;
1033
+ const key2 = `${sourceNode}->${targetNode}`;
1034
+ if (edgeKeys.has(key2)) continue;
1035
+ edgeKeys.add(key2);
1036
+ edges.push({ from: sourceNode, to: targetNode, type: "inferred" });
1037
+ continue;
1038
+ }
1039
+ const externalId = `external-${hostBase}`;
1040
+ if (!externalServices.has(externalId)) {
1041
+ externalServices.set(externalId, {
1042
+ id: externalId,
1043
+ name: hostBase,
1044
+ port
1045
+ });
1046
+ }
1047
+ const key = `${sourceNode}->${externalId}`;
1048
+ if (edgeKeys.has(key)) continue;
1049
+ edgeKeys.add(key);
1050
+ edges.push({ from: sourceNode, to: externalId, type: "inferred" });
1051
+ }
1052
+ }
1053
+ }
1054
+ return {
1055
+ edges,
1056
+ externalServices: Array.from(externalServices.values()),
1057
+ renderedComponents: Array.from(renderedComponents),
1058
+ workloadComponents: Array.from(workloadComponents),
1059
+ jobComponents: Array.from(jobComponents)
1060
+ };
1061
+ }
1062
+
1063
+ // ../core/src/parsers/helm/index.ts
1064
+ function buildEmptyGraph(project, sourceFiles = []) {
1065
+ return {
1066
+ nodes: [],
1067
+ edges: [],
1068
+ metadata: {
1069
+ project,
1070
+ parsedAt: (/* @__PURE__ */ new Date()).toISOString(),
1071
+ sourceFiles,
1072
+ parserVersion: "0.1.0"
1073
+ }
1074
+ };
1075
+ }
1076
+ function resolveChartPath(chartDir) {
1077
+ const yamlPath = join2(chartDir, "Chart.yaml");
1078
+ if (existsSync(yamlPath)) return yamlPath;
1079
+ const ymlPath = join2(chartDir, "Chart.yml");
1080
+ if (existsSync(ymlPath)) return ymlPath;
1081
+ return null;
1082
+ }
1083
+ function resolveSourcePath(sourceRoot, filepath) {
1084
+ if (!sourceRoot) return filepath;
1085
+ const rel = relative(sourceRoot, filepath);
1086
+ return rel || filepath;
1087
+ }
1088
+ function normalizeName2(value) {
1089
+ return value.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/_/g, "-").toLowerCase();
1090
+ }
1091
+ function buildComponentMap(chartName, components) {
1092
+ const map = /* @__PURE__ */ new Map();
1093
+ for (const component of components) {
1094
+ map.set(normalizeName2(component.name), `${chartName}-${component.name}`);
1095
+ }
1096
+ return map;
1097
+ }
1098
+ function hasComponentMarkers2(values) {
1099
+ return "replicaCount" in values || "replicas" in values || "containerPorts" in values || "containerPort" in values || "service" in values && isRecord2(values.service) || "image" in values || "resources" in values;
1100
+ }
1101
+ function buildAliasMap(values, componentMap) {
1102
+ const aliases = /* @__PURE__ */ new Map();
1103
+ const serverNode = componentMap.get("server");
1104
+ if (serverNode && isRecord2(values.server)) {
1105
+ for (const [key, value] of Object.entries(values.server)) {
1106
+ if (!isRecord2(value)) continue;
1107
+ if (!hasComponentMarkers2(value)) continue;
1108
+ aliases.set(normalizeName2(key), serverNode);
1109
+ }
1110
+ }
1111
+ return aliases;
1112
+ }
1113
+ function parseConditionPaths(condition) {
1114
+ if (!condition) return [];
1115
+ return condition.split(",").map((part) => part.trim()).filter(Boolean);
1116
+ }
1117
+ function evaluateConditions(values, condition) {
1118
+ const paths = parseConditionPaths(condition);
1119
+ if (paths.length === 0) return void 0;
1120
+ const results = paths.map((path) => getBoolean(getValueAtPath(values, path))).filter((val) => val !== void 0);
1121
+ if (results.length === 0) return void 0;
1122
+ if (results.some((val) => val)) return true;
1123
+ return false;
1124
+ }
1125
+ function evaluateTags(values, tags) {
1126
+ if (!tags || tags.length === 0) return void 0;
1127
+ const results = tags.map((tag) => getBoolean(getValueAtPath(values, `tags.${tag}`))).filter((val) => val !== void 0);
1128
+ if (results.length === 0) return void 0;
1129
+ if (results.some((val) => val === false)) return false;
1130
+ if (results.some((val) => val === true)) return true;
1131
+ return void 0;
1132
+ }
1133
+ function isDependencyEnabled(dependency, values) {
1134
+ const conditionResult = evaluateConditions(values, dependency.condition);
1135
+ if (conditionResult !== void 0) return conditionResult;
1136
+ const tagResult = evaluateTags(values, dependency.tags);
1137
+ if (tagResult !== void 0) return tagResult;
1138
+ const explicitEnabled = getBoolean(
1139
+ getValueAtPath(values, `${dependency.name}.enabled`)
1140
+ );
1141
+ return explicitEnabled ?? true;
1142
+ }
1143
+ function extractExternalServiceConfig(values, dependencyName) {
1144
+ const nameLower = dependencyName.toLowerCase();
1145
+ const candidates = [];
1146
+ if (nameLower.includes("postgres") || nameLower.includes("postgresql")) {
1147
+ candidates.push(
1148
+ "externalDatabase",
1149
+ "externalPostgresql",
1150
+ "externalPostgres"
1151
+ );
1152
+ } else if (nameLower.includes("redis")) {
1153
+ candidates.push("externalRedis");
1154
+ } else if (nameLower.includes("mysql")) {
1155
+ candidates.push("externalDatabase", "externalMysql");
1156
+ } else if (nameLower.includes("mariadb")) {
1157
+ candidates.push("externalDatabase", "externalMariadb");
1158
+ } else if (nameLower.includes("mongo")) {
1159
+ candidates.push("externalMongo");
1160
+ } else if (nameLower.includes("elasticsearch")) {
1161
+ candidates.push("externalElasticsearch");
1162
+ } else if (nameLower.includes("opensearch")) {
1163
+ candidates.push("externalOpensearch");
1164
+ } else if (nameLower.includes("rabbitmq")) {
1165
+ candidates.push("externalRabbitmq");
1166
+ } else if (nameLower.includes("kafka")) {
1167
+ candidates.push("externalKafka");
1168
+ } else {
1169
+ candidates.push(`external${dependencyName}`);
1170
+ }
1171
+ const externalRoot = isRecord2(values.external) ? values.external : void 0;
1172
+ for (const key of candidates) {
1173
+ const config = getValueAtPath(values, key);
1174
+ if (!isRecord2(config)) continue;
1175
+ const host = getString(config.host) ?? getString(config.hostname);
1176
+ if (!host) continue;
1177
+ const port = getNumber(config.port) ?? getNumber(config.servicePort) ?? getNumber(config.redisPort) ?? getNumber(config.databasePort);
1178
+ return {
1179
+ name: `${dependencyName} (external)`,
1180
+ port: port ? Math.round(port) : void 0
1181
+ };
1182
+ }
1183
+ if (externalRoot && isRecord2(externalRoot[dependencyName])) {
1184
+ const config = externalRoot[dependencyName];
1185
+ const host = getString(config.host) ?? getString(config.hostname);
1186
+ if (host) {
1187
+ const port = getNumber(config.port);
1188
+ return {
1189
+ name: `${dependencyName} (external)`,
1190
+ port: port ? Math.round(port) : void 0
1191
+ };
1192
+ }
1193
+ }
1194
+ return null;
1195
+ }
1196
+ function buildPortMappings(port) {
1197
+ if (!port) return void 0;
1198
+ return [{ internal: port }];
1199
+ }
1200
+ function parseHelmChart(chartDir, project, sourceRoot, options) {
1201
+ const chartPath = resolveChartPath(chartDir);
1202
+ if (!chartPath) {
1203
+ return buildEmptyGraph(project);
1204
+ }
1205
+ const chartContent = readFileSync2(chartPath, "utf-8");
1206
+ const chart = parseChartYaml(chartContent);
1207
+ if (!chart) {
1208
+ return buildEmptyGraph(project, [resolveSourcePath(sourceRoot, chartPath)]);
1209
+ }
1210
+ const valuesPath = join2(chartDir, "values.yaml");
1211
+ let values = {};
1212
+ if (existsSync(valuesPath)) {
1213
+ const valuesContent = readFileSync2(valuesPath, "utf-8");
1214
+ values = parseValuesYaml(valuesContent);
1215
+ }
1216
+ const builder = new GraphBuilder(project);
1217
+ builder.addSourceFile(resolveSourcePath(sourceRoot, chartPath));
1218
+ if (existsSync(valuesPath)) {
1219
+ builder.addSourceFile(resolveSourcePath(sourceRoot, valuesPath));
1220
+ }
1221
+ const dependencyNames = chart.dependencies?.map((dep) => dep.name) ?? [];
1222
+ const detectedComponents = detectComponents(values, { dependencyNames });
1223
+ const componentMap = buildComponentMap(chart.name, detectedComponents);
1224
+ const aliasMap = buildAliasMap(values, componentMap);
1225
+ const renderedInference = inferEdgesFromRenderedManifests({
1226
+ chartDir,
1227
+ releaseName: chart.name,
1228
+ componentMap,
1229
+ aliasMap,
1230
+ valuesFiles: options?.valuesFiles
1231
+ });
1232
+ let components = detectedComponents;
1233
+ if (renderedInference.renderedComponents.length > 0) {
1234
+ const renderedSet = new Set(renderedInference.renderedComponents);
1235
+ const jobSet = new Set(renderedInference.jobComponents);
1236
+ components = detectedComponents.filter((component) => {
1237
+ const nodeId = `${chart.name}-${component.name}`;
1238
+ if (!renderedSet.has(nodeId)) return false;
1239
+ if (jobSet.has(nodeId)) return false;
1240
+ return true;
1241
+ });
1242
+ }
1243
+ const chartConfig = extractServiceConfig(values);
1244
+ const chartImage = chartConfig.image;
1245
+ const chartSource = {
1246
+ file: resolveSourcePath(sourceRoot, chartPath),
1247
+ format: "helm"
1248
+ };
1249
+ const hasComponents = components.length > 0;
1250
+ if (!hasComponents) {
1251
+ builder.addNode(
1252
+ chart.name,
1253
+ chart.name,
1254
+ inferServiceType(chart.name, chartImage),
1255
+ chartSource,
1256
+ {
1257
+ image: chartImage,
1258
+ ports: chartConfig.ports,
1259
+ replicas: chartConfig.replicas,
1260
+ resourceRequests: chartConfig.resourceRequests,
1261
+ storageSize: chartConfig.storageSize
1262
+ }
1263
+ );
1264
+ }
1265
+ const dependencyTargets = [];
1266
+ for (const dependency of chart.dependencies ?? []) {
1267
+ const enabled = isDependencyEnabled(dependency, values);
1268
+ if (enabled) {
1269
+ const depId = dependency.name;
1270
+ builder.addNode(
1271
+ depId,
1272
+ dependency.name,
1273
+ inferServiceType(dependency.name, dependency.name),
1274
+ chartSource
1275
+ );
1276
+ dependencyTargets.push({ id: depId, edgeType: "subchart" });
1277
+ } else {
1278
+ const external = extractExternalServiceConfig(values, dependency.name);
1279
+ if (external) {
1280
+ const externalId = `external-${dependency.name}`;
1281
+ builder.addNode(
1282
+ externalId,
1283
+ external.name,
1284
+ inferServiceType(dependency.name, dependency.name),
1285
+ chartSource,
1286
+ {
1287
+ external: true,
1288
+ ports: buildPortMappings(external.port)
1289
+ }
1290
+ );
1291
+ dependencyTargets.push({ id: externalId, edgeType: "depends_on" });
1292
+ }
1293
+ }
1294
+ }
1295
+ if (hasComponents) {
1296
+ for (const component of components) {
1297
+ const componentConfig = extractServiceConfig(component.values);
1298
+ const componentImage = componentConfig.image ?? chartImage;
1299
+ const componentName = `${chart.name}-${component.name}`;
1300
+ const componentType = inferServiceType(component.name, componentImage);
1301
+ builder.addNode(
1302
+ componentName,
1303
+ componentName,
1304
+ componentType,
1305
+ chartSource,
1306
+ {
1307
+ image: componentImage,
1308
+ ports: componentConfig.ports ?? chartConfig.ports,
1309
+ replicas: componentConfig.replicas ?? chartConfig.replicas,
1310
+ resourceRequests: componentConfig.resourceRequests ?? chartConfig.resourceRequests,
1311
+ storageSize: componentConfig.storageSize ?? chartConfig.storageSize,
1312
+ group: chart.name
1313
+ }
1314
+ );
1315
+ for (const target of dependencyTargets) {
1316
+ builder.addEdge(componentName, target.id, target.edgeType);
1317
+ }
1318
+ }
1319
+ } else {
1320
+ for (const target of dependencyTargets) {
1321
+ builder.addEdge(chart.name, target.id, target.edgeType);
1322
+ }
1323
+ }
1324
+ for (const service of renderedInference.externalServices) {
1325
+ if (!builder.hasNode(service.id)) {
1326
+ builder.addNode(
1327
+ service.id,
1328
+ service.name,
1329
+ inferServiceType(service.name, service.name),
1330
+ chartSource,
1331
+ {
1332
+ external: true,
1333
+ ports: service.port ? [{ internal: service.port }] : void 0
1334
+ }
1335
+ );
1336
+ }
1337
+ }
1338
+ for (const edge of renderedInference.edges) {
1339
+ builder.addEdge(edge.from, edge.to, edge.type);
1340
+ }
1341
+ return builder.build();
1342
+ }
1343
+
1344
+ // ../core/src/pipeline/index.ts
1345
+ import { join as join4 } from "path";
1346
+
1347
+ // ../core/src/elk/types.ts
1348
+ var ELK_LAYOUT_OPTIONS = {
1349
+ /** Standard left-to-right layered layout */
1350
+ standard: {
1351
+ "elk.algorithm": "layered",
1352
+ "elk.direction": "RIGHT",
1353
+ "elk.edgeRouting": "ORTHOGONAL",
1354
+ "elk.spacing.nodeNode": "50",
1355
+ "elk.layered.spacing.nodeNodeBetweenLayers": "80",
1356
+ "elk.layered.mergeEdges": "false"
1357
+ },
1358
+ /** Semantic partitioning enabled */
1359
+ semantic: {
1360
+ "elk.algorithm": "layered",
1361
+ "elk.direction": "RIGHT",
1362
+ "elk.edgeRouting": "ORTHOGONAL",
1363
+ "elk.spacing.nodeNode": "50",
1364
+ "elk.layered.spacing.nodeNodeBetweenLayers": "80",
1365
+ "elk.layered.mergeEdges": "false",
1366
+ "elk.partitioning.activate": "true"
1367
+ },
1368
+ /** Compound node (for grouping related services) */
1369
+ compound: {
1370
+ "elk.algorithm": "layered",
1371
+ "elk.direction": "DOWN",
1372
+ "elk.edgeRouting": "ORTHOGONAL",
1373
+ "elk.spacing.nodeNode": "20",
1374
+ "elk.layered.spacing.nodeNodeBetweenLayers": "30",
1375
+ "elk.hierarchyHandling": "INCLUDE_CHILDREN"
1376
+ }
1377
+ };
1378
+
1379
+ // ../core/src/elk/convert.ts
1380
+ var LAYER_PARTITIONS = {
1381
+ entry: 0,
1382
+ // Proxies, load balancers, external-facing
1383
+ ui: 1,
1384
+ // Web frontends, UI services
1385
+ api: 2,
1386
+ // API services, gateways, streaming
1387
+ worker: 3,
1388
+ // Background job processors (sidekiq, celery)
1389
+ queue: 4,
1390
+ // Message queues, brokers
1391
+ data: 5
1392
+ // Databases, caches, storage
1393
+ };
1394
+ var DEFAULT_NODE_SIZE = { width: 140, height: 50 };
1395
+ var NODE_SIZES = {
1396
+ database: { width: 160, height: 80 },
1397
+ cache: { width: 140, height: 70 },
1398
+ storage: { width: 160, height: 80 },
1399
+ queue: { width: 120, height: 60 },
1400
+ proxy: { width: 100, height: 50 }
1401
+ };
1402
+ function parseCpuCores(value) {
1403
+ if (!value) return void 0;
1404
+ if (value.endsWith("m")) {
1405
+ const milli = Number.parseFloat(value.slice(0, -1));
1406
+ return Number.isFinite(milli) ? milli / 1e3 : void 0;
1407
+ }
1408
+ const cores = Number.parseFloat(value);
1409
+ return Number.isFinite(cores) ? cores : void 0;
1410
+ }
1411
+ function parseMemoryMi(value) {
1412
+ if (!value) return void 0;
1413
+ const match = value.match(/^(\d+(?:\.\d+)?)([a-zA-Z]+)?$/);
1414
+ if (!match) return void 0;
1415
+ const amount = Number.parseFloat(match[1] ?? "");
1416
+ if (!Number.isFinite(amount)) return void 0;
1417
+ const unit = (match[2] ?? "").toLowerCase();
1418
+ switch (unit) {
1419
+ case "ki":
1420
+ return amount / 1024;
1421
+ case "mi":
1422
+ return amount;
1423
+ case "gi":
1424
+ return amount * 1024;
1425
+ case "ti":
1426
+ return amount * 1024 * 1024;
1427
+ case "k":
1428
+ return amount / 1024;
1429
+ case "m":
1430
+ return amount;
1431
+ case "g":
1432
+ return amount * 1024;
1433
+ default:
1434
+ return amount;
1435
+ }
1436
+ }
1437
+ function getResourceScale(node) {
1438
+ const cpu = parseCpuCores(node.resourceRequests?.cpu);
1439
+ const memory = parseMemoryMi(node.resourceRequests?.memory);
1440
+ if (cpu === void 0 && memory === void 0) return 1;
1441
+ if ((cpu ?? 0) >= 2 || (memory ?? 0) >= 2048) return 1.4;
1442
+ if ((cpu ?? 0) >= 1 || (memory ?? 0) >= 1024) return 1.25;
1443
+ if ((cpu ?? 0) >= 0.5 || (memory ?? 0) >= 512) return 1.1;
1444
+ return 1;
1445
+ }
1446
+ function getSemanticLayer(node) {
1447
+ switch (node.type) {
1448
+ case "proxy":
1449
+ return "entry";
1450
+ case "ui":
1451
+ return "ui";
1452
+ case "database":
1453
+ case "storage":
1454
+ return "data";
1455
+ case "cache":
1456
+ return "data";
1457
+ case "queue":
1458
+ return "queue";
1459
+ }
1460
+ const nameLower = node.name.toLowerCase();
1461
+ if (nameLower.includes("nginx") || nameLower.includes("haproxy") || nameLower.includes("traefik") || nameLower.includes("ingress") || nameLower.includes("caddy")) {
1462
+ return "entry";
1463
+ }
1464
+ if (nameLower === "web" || nameLower.includes("frontend") || nameLower.includes("dashboard") || nameLower.includes("ui") || nameLower.includes("client") || nameLower.includes("app")) {
1465
+ return "ui";
1466
+ }
1467
+ if (nameLower.includes("sidekiq") || nameLower.includes("celery") || nameLower.includes("resque") || nameLower.includes("worker") || nameLower.includes("job") || nameLower.includes("consumer") || nameLower.includes("processor")) {
1468
+ return "worker";
1469
+ }
1470
+ if (nameLower.includes("api") || nameLower.includes("gateway") || nameLower.includes("relay") || nameLower.includes("streaming") || nameLower.includes("graphql") || nameLower.includes("grpc")) {
1471
+ return "api";
1472
+ }
1473
+ if (nameLower.includes("seaweedfs") || nameLower.includes("objectstorage") || nameLower.includes("minio") || nameLower.includes("s3")) {
1474
+ return "data";
1475
+ }
1476
+ return "api";
1477
+ }
1478
+ function getNodeSize(node) {
1479
+ const base = NODE_SIZES[node.type] ?? DEFAULT_NODE_SIZE;
1480
+ const scale = getResourceScale(node);
1481
+ return {
1482
+ width: Math.round(base.width * scale),
1483
+ height: Math.round(base.height * scale)
1484
+ };
1485
+ }
1486
+ function getLabelDimensions(text) {
1487
+ return {
1488
+ width: Math.max(40, text.length * 8),
1489
+ height: 20
1490
+ };
1491
+ }
1492
+ function convertNode(node, layer, enablePartitioning) {
1493
+ const size = getNodeSize(node);
1494
+ const labelDims = getLabelDimensions(node.name);
1495
+ const elkNode = {
1496
+ id: node.id,
1497
+ width: Math.max(size.width, labelDims.width + 20),
1498
+ height: size.height,
1499
+ labels: [
1500
+ {
1501
+ text: node.name,
1502
+ width: labelDims.width,
1503
+ height: labelDims.height
1504
+ }
1505
+ ]
1506
+ };
1507
+ if (enablePartitioning) {
1508
+ elkNode.layoutOptions = {
1509
+ "elk.partitioning.partition": String(LAYER_PARTITIONS[layer])
1510
+ };
1511
+ }
1512
+ return elkNode;
1513
+ }
1514
+ function infraGraphToElk(graph, options = {}) {
1515
+ const enablePartitioning = options.semanticLayers !== false;
1516
+ const nodeTypeMap = /* @__PURE__ */ new Map();
1517
+ for (const node of graph.nodes) {
1518
+ nodeTypeMap.set(node.id, node.type);
1519
+ }
1520
+ const cacheNodes = /* @__PURE__ */ new Set();
1521
+ for (const node of graph.nodes) {
1522
+ if (node.type === "cache") {
1523
+ cacheNodes.add(node.id);
1524
+ }
1525
+ }
1526
+ const nodePorts = /* @__PURE__ */ new Map();
1527
+ const layerAssignments = /* @__PURE__ */ new Map();
1528
+ for (const node of graph.nodes) {
1529
+ layerAssignments.set(node.id, getSemanticLayer(node));
1530
+ }
1531
+ const edges = graph.edges.map((edge, index) => {
1532
+ const edgeId = `e${index}`;
1533
+ const targetIsCache = cacheNodes.has(edge.to);
1534
+ if (targetIsCache) {
1535
+ const sourcePortId = `${edge.from}-south-${index}`;
1536
+ const targetPortId = `${edge.to}-north-${index}`;
1537
+ if (!nodePorts.has(edge.from)) {
1538
+ nodePorts.set(edge.from, { south: [], north: [] });
1539
+ }
1540
+ nodePorts.get(edge.from).south.push(sourcePortId);
1541
+ if (!nodePorts.has(edge.to)) {
1542
+ nodePorts.set(edge.to, { south: [], north: [] });
1543
+ }
1544
+ nodePorts.get(edge.to).north.push(targetPortId);
1545
+ return {
1546
+ id: edgeId,
1547
+ sources: [sourcePortId],
1548
+ targets: [targetPortId]
1549
+ };
1550
+ }
1551
+ return {
1552
+ id: edgeId,
1553
+ sources: [edge.from],
1554
+ targets: [edge.to]
1555
+ };
1556
+ });
1557
+ const children = graph.nodes.map((node) => {
1558
+ const layer = layerAssignments.get(node.id) ?? "api";
1559
+ const elkNode = convertNode(node, layer, enablePartitioning);
1560
+ const ports = nodePorts.get(node.id);
1561
+ if (ports) {
1562
+ const elkPorts = [];
1563
+ for (const portId of ports.south) {
1564
+ elkPorts.push({
1565
+ id: portId,
1566
+ layoutOptions: { "elk.port.side": "SOUTH" }
1567
+ });
1568
+ }
1569
+ for (const portId of ports.north) {
1570
+ elkPorts.push({
1571
+ id: portId,
1572
+ layoutOptions: { "elk.port.side": "NORTH" }
1573
+ });
1574
+ }
1575
+ if (elkPorts.length > 0) {
1576
+ elkNode.ports = elkPorts;
1577
+ elkNode.layoutOptions = {
1578
+ ...elkNode.layoutOptions,
1579
+ "elk.portConstraints": "FIXED_SIDE"
1580
+ };
1581
+ }
1582
+ }
1583
+ return elkNode;
1584
+ });
1585
+ const layoutOptions = enablePartitioning ? ELK_LAYOUT_OPTIONS.semantic : ELK_LAYOUT_OPTIONS.standard;
1586
+ const elkGraph = {
1587
+ id: "root",
1588
+ layoutOptions: { ...layoutOptions },
1589
+ children,
1590
+ edges
1591
+ };
1592
+ return {
1593
+ graph: elkGraph,
1594
+ layerAssignments
1595
+ };
1596
+ }
1597
+
1598
+ // ../core/src/elk/layout.ts
1599
+ import ELK from "elkjs";
1600
+ async function runLayout(graph) {
1601
+ const elk = new ELK();
1602
+ const result = await elk.layout(graph);
1603
+ return {
1604
+ graph: result,
1605
+ width: result.width ?? 0,
1606
+ height: result.height ?? 0
1607
+ };
1608
+ }
1609
+
1610
+ // ../core/src/excalidraw/types.ts
1611
+ var SERVICE_COLORS = {
1612
+ database: {
1613
+ stroke: "#1971c2",
1614
+ background: "#a5d8ff"
1615
+ },
1616
+ cache: {
1617
+ stroke: "#e03131",
1618
+ background: "#ffc9c9"
1619
+ },
1620
+ queue: {
1621
+ stroke: "#f08c00",
1622
+ background: "#ffec99"
1623
+ },
1624
+ storage: {
1625
+ stroke: "#2f9e44",
1626
+ background: "#b2f2bb"
1627
+ },
1628
+ proxy: {
1629
+ stroke: "#7950f2",
1630
+ background: "#d0bfff"
1631
+ },
1632
+ container: {
1633
+ stroke: "#495057",
1634
+ background: "#dee2e6"
1635
+ },
1636
+ ui: {
1637
+ stroke: "#0c8599",
1638
+ background: "#99e9f2"
1639
+ }
1640
+ };
1641
+ var SERVICE_SHAPES = {
1642
+ database: "ellipse",
1643
+ cache: "ellipse",
1644
+ storage: "ellipse",
1645
+ queue: "diamond",
1646
+ proxy: "rectangle",
1647
+ container: "rectangle",
1648
+ ui: "rectangle"
1649
+ };
1650
+ var LAYOUT_CONFIG = {
1651
+ nodeWidth: 180,
1652
+ nodeHeight: 80,
1653
+ horizontalGap: 100,
1654
+ verticalGap: 80,
1655
+ textPadding: 10,
1656
+ fontSize: 16,
1657
+ fontFamily: 1
1658
+ // 1 = Virgil (hand-drawn), 2 = Helvetica, 3 = Cascadia
1659
+ };
1660
+
1661
+ // ../core/src/excalidraw/elk-render.ts
1662
+ function generateSeed() {
1663
+ return Math.floor(Math.random() * 2147483647);
1664
+ }
1665
+ function getServiceColors(type) {
1666
+ return SERVICE_COLORS[type] ?? {
1667
+ stroke: "#495057",
1668
+ background: "#dee2e6"
1669
+ };
1670
+ }
1671
+ function getShapeType(serviceType) {
1672
+ const shape = SERVICE_SHAPES[serviceType];
1673
+ if (shape === "ellipse" || shape === "diamond" || shape === "rectangle") {
1674
+ return shape;
1675
+ }
1676
+ return "rectangle";
1677
+ }
1678
+ function createNodeElement(node, elkNode) {
1679
+ const colors = getServiceColors(node.type);
1680
+ const textId = `${node.id}-text`;
1681
+ const shapeType = getShapeType(node.type);
1682
+ return {
1683
+ id: node.id,
1684
+ type: shapeType,
1685
+ x: elkNode.x ?? 0,
1686
+ y: elkNode.y ?? 0,
1687
+ width: elkNode.width ?? 140,
1688
+ height: elkNode.height ?? 50,
1689
+ angle: 0,
1690
+ strokeColor: colors.stroke,
1691
+ backgroundColor: colors.background,
1692
+ fillStyle: "solid",
1693
+ strokeWidth: 2,
1694
+ strokeStyle: node.external ? "dashed" : "solid",
1695
+ roughness: 1,
1696
+ opacity: 100,
1697
+ groupIds: [],
1698
+ frameId: null,
1699
+ roundness: shapeType === "rectangle" ? { type: 3 } : null,
1700
+ seed: generateSeed(),
1701
+ version: 1,
1702
+ versionNonce: generateSeed(),
1703
+ isDeleted: false,
1704
+ boundElements: [{ id: textId, type: "text" }],
1705
+ updated: Date.now(),
1706
+ link: null,
1707
+ locked: false
1708
+ };
1709
+ }
1710
+ function createNodeText(node, elkNode) {
1711
+ const x = elkNode.x ?? 0;
1712
+ const y = elkNode.y ?? 0;
1713
+ const width = elkNode.width ?? 140;
1714
+ const height = elkNode.height ?? 50;
1715
+ const fontSize = LAYOUT_CONFIG.fontSize;
1716
+ const lineHeight = 1.25;
1717
+ const textHeight = fontSize * lineHeight;
1718
+ const avgCharWidth = fontSize * 0.6;
1719
+ const textWidth = Math.min(node.name.length * avgCharWidth, width - 20);
1720
+ return {
1721
+ id: `${node.id}-text`,
1722
+ type: "text",
1723
+ x: x + (width - textWidth) / 2,
1724
+ y: y + (height - textHeight) / 2,
1725
+ width: textWidth,
1726
+ height: textHeight,
1727
+ angle: 0,
1728
+ strokeColor: "#1e1e1e",
1729
+ backgroundColor: "transparent",
1730
+ fillStyle: "solid",
1731
+ strokeWidth: 1,
1732
+ strokeStyle: "solid",
1733
+ roughness: 1,
1734
+ opacity: 100,
1735
+ groupIds: [],
1736
+ frameId: null,
1737
+ roundness: null,
1738
+ seed: generateSeed(),
1739
+ version: 1,
1740
+ versionNonce: generateSeed(),
1741
+ isDeleted: false,
1742
+ boundElements: null,
1743
+ updated: Date.now(),
1744
+ link: null,
1745
+ locked: false,
1746
+ text: node.name,
1747
+ fontSize,
1748
+ fontFamily: LAYOUT_CONFIG.fontFamily,
1749
+ textAlign: "center",
1750
+ verticalAlign: "middle",
1751
+ baseline: fontSize,
1752
+ containerId: node.id,
1753
+ originalText: node.name,
1754
+ autoResize: true,
1755
+ lineHeight
1756
+ };
1757
+ }
1758
+ function elkSectionToArrowPoints(section) {
1759
+ const startX = section.startPoint.x;
1760
+ const startY = section.startPoint.y;
1761
+ const points = [[0, 0]];
1762
+ if (section.bendPoints) {
1763
+ for (const bend of section.bendPoints) {
1764
+ points.push([bend.x - startX, bend.y - startY]);
1765
+ }
1766
+ }
1767
+ points.push([section.endPoint.x - startX, section.endPoint.y - startY]);
1768
+ return { startX, startY, points };
1769
+ }
1770
+ function extractNodeId(portOrNodeId) {
1771
+ const portPattern = /^(.+)-(north|south|east|west)-\d+$/;
1772
+ const match = portOrNodeId.match(portPattern);
1773
+ return match ? match[1] : portOrNodeId;
1774
+ }
1775
+ function calculateFixedPoint(connectionPoint, elkNode) {
1776
+ const nodeX = elkNode.x ?? 0;
1777
+ const nodeY = elkNode.y ?? 0;
1778
+ const nodeWidth = elkNode.width ?? 140;
1779
+ const nodeHeight = elkNode.height ?? 50;
1780
+ let normalizedX = (connectionPoint.x - nodeX) / nodeWidth;
1781
+ let normalizedY = (connectionPoint.y - nodeY) / nodeHeight;
1782
+ normalizedX = Math.max(0, Math.min(1, normalizedX));
1783
+ normalizedY = Math.max(0, Math.min(1, normalizedY));
1784
+ return [normalizedX, normalizedY];
1785
+ }
1786
+ function createArrowElement(edgeId, sourcePortOrNodeId, targetPortOrNodeId, sections, elkNodeMap) {
1787
+ if (!sections || sections.length === 0) {
1788
+ return null;
1789
+ }
1790
+ const section = sections[0];
1791
+ if (!section) return null;
1792
+ const { startX, startY, points } = elkSectionToArrowPoints(section);
1793
+ const sourceNodeId = extractNodeId(sourcePortOrNodeId);
1794
+ const targetNodeId = extractNodeId(targetPortOrNodeId);
1795
+ const sourceNode = elkNodeMap.get(sourceNodeId);
1796
+ const targetNode = elkNodeMap.get(targetNodeId);
1797
+ const startFixedPoint = sourceNode ? calculateFixedPoint(section.startPoint, sourceNode) : null;
1798
+ const endFixedPoint = targetNode ? calculateFixedPoint(section.endPoint, targetNode) : null;
1799
+ return {
1800
+ id: edgeId,
1801
+ type: "arrow",
1802
+ x: startX,
1803
+ y: startY,
1804
+ width: Math.abs(points[points.length - 1]?.[0] ?? 0),
1805
+ height: Math.abs(points[points.length - 1]?.[1] ?? 0),
1806
+ angle: 0,
1807
+ strokeColor: "#868e96",
1808
+ backgroundColor: "transparent",
1809
+ fillStyle: "solid",
1810
+ strokeWidth: 2,
1811
+ strokeStyle: "solid",
1812
+ roughness: 1,
1813
+ opacity: 100,
1814
+ groupIds: [],
1815
+ frameId: null,
1816
+ roundness: { type: 2 },
1817
+ seed: generateSeed(),
1818
+ version: 1,
1819
+ versionNonce: generateSeed(),
1820
+ isDeleted: false,
1821
+ boundElements: null,
1822
+ updated: Date.now(),
1823
+ link: null,
1824
+ locked: false,
1825
+ points,
1826
+ startBinding: {
1827
+ elementId: sourceNodeId,
1828
+ focus: 0,
1829
+ gap: 1,
1830
+ fixedPoint: startFixedPoint
1831
+ },
1832
+ endBinding: {
1833
+ elementId: targetNodeId,
1834
+ focus: 0,
1835
+ gap: 1,
1836
+ fixedPoint: endFixedPoint
1837
+ },
1838
+ startArrowhead: null,
1839
+ endArrowhead: "arrow",
1840
+ elbowed: true
1841
+ };
1842
+ }
1843
+ function buildElkNodeMap(elkGraph) {
1844
+ const map = /* @__PURE__ */ new Map();
1845
+ function addNodes(nodes) {
1846
+ if (!nodes) return;
1847
+ for (const node of nodes) {
1848
+ map.set(node.id, node);
1849
+ if (node.children) {
1850
+ addNodes(node.children);
1851
+ }
1852
+ }
1853
+ }
1854
+ addNodes(elkGraph.children);
1855
+ return map;
1856
+ }
1857
+ function renderWithElkLayout(graph, elkGraph, options = {}) {
1858
+ const padding = options.padding ?? 50;
1859
+ const elements = [];
1860
+ const elkNodeMap = buildElkNodeMap(elkGraph);
1861
+ const offsetElkNodeMap = /* @__PURE__ */ new Map();
1862
+ for (const node of graph.nodes) {
1863
+ const elkNode = elkNodeMap.get(node.id);
1864
+ if (!elkNode || elkNode.x === void 0 || elkNode.y === void 0) {
1865
+ console.warn(`No ELK position for node: ${node.id}`);
1866
+ continue;
1867
+ }
1868
+ const offsetNode = {
1869
+ ...elkNode,
1870
+ x: (elkNode.x ?? 0) + padding,
1871
+ y: (elkNode.y ?? 0) + padding
1872
+ };
1873
+ offsetElkNodeMap.set(node.id, offsetNode);
1874
+ elements.push(createNodeElement(node, offsetNode));
1875
+ elements.push(createNodeText(node, offsetNode));
1876
+ }
1877
+ if (elkGraph.edges) {
1878
+ for (const edge of elkGraph.edges) {
1879
+ const sourceId = edge.sources[0];
1880
+ const targetId = edge.targets[0];
1881
+ if (!sourceId || !targetId) continue;
1882
+ const offsetSections = edge.sections?.map((section) => ({
1883
+ ...section,
1884
+ startPoint: {
1885
+ x: section.startPoint.x + padding,
1886
+ y: section.startPoint.y + padding
1887
+ },
1888
+ endPoint: {
1889
+ x: section.endPoint.x + padding,
1890
+ y: section.endPoint.y + padding
1891
+ },
1892
+ bendPoints: section.bendPoints?.map((bp) => ({
1893
+ x: bp.x + padding,
1894
+ y: bp.y + padding
1895
+ }))
1896
+ }));
1897
+ const arrow = createArrowElement(
1898
+ edge.id,
1899
+ sourceId,
1900
+ targetId,
1901
+ offsetSections ?? [],
1902
+ offsetElkNodeMap
1903
+ );
1904
+ if (arrow) {
1905
+ elements.push(arrow);
1906
+ const sourceNodeId = extractNodeId(sourceId);
1907
+ const targetNodeId = extractNodeId(targetId);
1908
+ const sourceEl = elements.find((e) => e.id === sourceNodeId);
1909
+ const targetEl = elements.find((e) => e.id === targetNodeId);
1910
+ if (sourceEl && "boundElements" in sourceEl) {
1911
+ sourceEl.boundElements = [
1912
+ ...sourceEl.boundElements ?? [],
1913
+ { id: edge.id, type: "arrow" }
1914
+ ];
1915
+ }
1916
+ if (targetEl && "boundElements" in targetEl) {
1917
+ targetEl.boundElements = [
1918
+ ...targetEl.boundElements ?? [],
1919
+ { id: edge.id, type: "arrow" }
1920
+ ];
1921
+ }
1922
+ }
1923
+ }
1924
+ }
1925
+ return {
1926
+ type: "excalidraw",
1927
+ version: 2,
1928
+ source: "clarity-elk",
1929
+ elements,
1930
+ appState: {
1931
+ viewBackgroundColor: "#ffffff",
1932
+ gridSize: null
1933
+ },
1934
+ files: {}
1935
+ };
1936
+ }
1937
+
1938
+ // ../core/src/llm/client.ts
1939
+ var OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1";
1940
+ var DEFAULT_MODEL = "anthropic/claude-opus-4.5";
1941
+ var DEFAULT_MAX_TOKENS = 4096;
1942
+ async function sendMessage(apiKey, prompt, config) {
1943
+ const messages = [
1944
+ {
1945
+ role: "user",
1946
+ content: prompt
1947
+ }
1948
+ ];
1949
+ const response = await fetch(`${OPENROUTER_BASE_URL}/chat/completions`, {
1950
+ method: "POST",
1951
+ headers: {
1952
+ Authorization: `Bearer ${apiKey}`,
1953
+ "Content-Type": "application/json",
1954
+ "HTTP-Referer": "https://github.com/clarity-diagrams/clarity",
1955
+ "X-Title": "Clarity Diagrams"
1956
+ },
1957
+ body: JSON.stringify({
1958
+ model: config?.model ?? DEFAULT_MODEL,
1959
+ max_tokens: config?.maxTokens ?? DEFAULT_MAX_TOKENS,
1960
+ messages
1961
+ })
1962
+ });
1963
+ if (!response.ok) {
1964
+ const errorText = await response.text();
1965
+ throw new Error(`OpenRouter API error: ${response.status} ${errorText}`);
1966
+ }
1967
+ const data = await response.json();
1968
+ if (data.error) {
1969
+ throw new Error(`OpenRouter error: ${data.error.message}`);
1970
+ }
1971
+ const content = data.choices[0]?.message?.content;
1972
+ if (!content) {
1973
+ throw new Error("No response content from OpenRouter");
1974
+ }
1975
+ return content;
1976
+ }
1977
+ function findBalancedJson(text, startChar, endChar) {
1978
+ const startIdx = text.indexOf(startChar);
1979
+ if (startIdx === -1) return null;
1980
+ let depth = 0;
1981
+ let inString = false;
1982
+ let escapeNext = false;
1983
+ for (let i = startIdx; i < text.length; i++) {
1984
+ const char = text[i];
1985
+ if (escapeNext) {
1986
+ escapeNext = false;
1987
+ continue;
1988
+ }
1989
+ if (char === "\\") {
1990
+ escapeNext = true;
1991
+ continue;
1992
+ }
1993
+ if (char === '"') {
1994
+ inString = !inString;
1995
+ continue;
1996
+ }
1997
+ if (inString) continue;
1998
+ if (char === startChar) {
1999
+ depth++;
2000
+ } else if (char === endChar) {
2001
+ depth--;
2002
+ if (depth === 0) {
2003
+ return text.slice(startIdx, i + 1);
2004
+ }
2005
+ }
2006
+ }
2007
+ return null;
2008
+ }
2009
+ function parseJsonResponse(response) {
2010
+ const jsonMatch = response.match(/```(?:json)?\s*([\s\S]*?)```/);
2011
+ const jsonString = jsonMatch ? jsonMatch[1]?.trim() : response.trim();
2012
+ if (!jsonString) {
2013
+ throw new Error("Empty response from LLM");
2014
+ }
2015
+ try {
2016
+ return JSON.parse(jsonString);
2017
+ } catch {
2018
+ const objectIdx = response.indexOf("{");
2019
+ const arrayIdx = response.indexOf("[");
2020
+ const tryObjectFirst = objectIdx !== -1 && (arrayIdx === -1 || objectIdx < arrayIdx);
2021
+ if (tryObjectFirst) {
2022
+ const objectJson = findBalancedJson(response, "{", "}");
2023
+ if (objectJson) {
2024
+ try {
2025
+ return JSON.parse(objectJson);
2026
+ } catch {
2027
+ }
2028
+ }
2029
+ const arrayJson = findBalancedJson(response, "[", "]");
2030
+ if (arrayJson) {
2031
+ try {
2032
+ return JSON.parse(arrayJson);
2033
+ } catch {
2034
+ }
2035
+ }
2036
+ } else {
2037
+ const arrayJson = findBalancedJson(response, "[", "]");
2038
+ if (arrayJson) {
2039
+ try {
2040
+ return JSON.parse(arrayJson);
2041
+ } catch {
2042
+ }
2043
+ }
2044
+ const objectJson = findBalancedJson(response, "{", "}");
2045
+ if (objectJson) {
2046
+ try {
2047
+ return JSON.parse(objectJson);
2048
+ } catch {
2049
+ }
2050
+ }
2051
+ }
2052
+ throw new Error(
2053
+ `Failed to parse JSON from LLM response: ${response.slice(0, 200)}`
2054
+ );
2055
+ }
2056
+ }
2057
+
2058
+ // ../core/src/llm/prompts.ts
2059
+ function buildEnhancePrompt(graph) {
2060
+ const servicesInfo = graph.nodes.map((node) => formatServiceInfo(node)).join("\n\n");
2061
+ const edgesInfo = graph.edges.map((edge) => `- ${edge.from} -> ${edge.to} (${edge.type})`).join("\n");
2062
+ return `You are an infrastructure architect analyzing a system to create clear, informative architecture diagrams.
2063
+
2064
+ ## Task
2065
+ Analyze the following infrastructure services and enhance each with:
2066
+ 1. A brief description (what it does)
2067
+ 2. A logical group name (for visual grouping)
2068
+ 3. For services that interact with message queues: their queue role (producer, consumer, or both)
2069
+
2070
+ ## Services to Analyze
2071
+
2072
+ ${servicesInfo}
2073
+
2074
+ ## Dependencies
2075
+ ${edgesInfo || "No explicit dependencies defined"}
2076
+
2077
+ ## Response Format
2078
+ Return ONLY valid JSON matching this structure:
2079
+ \`\`\`json
2080
+ {
2081
+ "services": [
2082
+ {
2083
+ "id": "service-id",
2084
+ "description": "Brief description of what this service does",
2085
+ "group": "Group Name",
2086
+ "queueRole": "producer"
2087
+ }
2088
+ ],
2089
+ "groups": [
2090
+ {
2091
+ "name": "Group Name",
2092
+ "description": "Brief description of this logical group"
2093
+ }
2094
+ ]
2095
+ }
2096
+ \`\`\`
2097
+
2098
+ ## Guidelines
2099
+ - Keep descriptions under 50 words
2100
+ - Group related services together (e.g., all databases in "Data Stores")
2101
+ - Use clear, non-technical group names suitable for architecture diagrams
2102
+ - Infer purpose from service name, image, ports, and environment variables
2103
+ - Check every service name for role hints (e.g., web/ui for frontends, schema/migrations for data tasks)
2104
+ - Create 3-5 groups for typical infrastructure (avoid too many or too few)
2105
+ - Only include queueRole for services that produce to or consume from message queues (Kafka, RabbitMQ, etc.)`;
2106
+ }
2107
+ function formatServiceInfo(node) {
2108
+ const lines = [`### ${node.name} (id: ${node.id})`];
2109
+ if (node.image) {
2110
+ lines.push(`- Image: ${node.image}`);
2111
+ }
2112
+ lines.push(`- Type: ${node.type}`);
2113
+ if (node.ports?.length) {
2114
+ const ports = node.ports.map((p) => `${p.external ?? "?"}:${p.internal}`).join(", ");
2115
+ lines.push(`- Ports: ${ports}`);
2116
+ }
2117
+ if (node.environment && Object.keys(node.environment).length > 0) {
2118
+ const envKeys = Object.keys(node.environment).slice(0, 10).join(", ");
2119
+ lines.push(
2120
+ `- Environment: ${envKeys}${Object.keys(node.environment).length > 10 ? "..." : ""}`
2121
+ );
2122
+ }
2123
+ if (node.volumes?.length) {
2124
+ const volumes = node.volumes.slice(0, 3).map((v) => `${v.source}:${v.target}`).join(", ");
2125
+ lines.push(`- Volumes: ${volumes}${node.volumes.length > 3 ? "..." : ""}`);
2126
+ }
2127
+ return lines.join("\n");
2128
+ }
2129
+ function applyEnhancements(graph, enhancements) {
2130
+ const enhancementMap = new Map(enhancements.services.map((s) => [s.id, s]));
2131
+ const enhancedNodes = graph.nodes.map((node) => {
2132
+ const enhancement = enhancementMap.get(node.id);
2133
+ if (enhancement) {
2134
+ return {
2135
+ ...node,
2136
+ description: enhancement.description,
2137
+ group: enhancement.group,
2138
+ queueRole: enhancement.queueRole
2139
+ };
2140
+ }
2141
+ return node;
2142
+ });
2143
+ return {
2144
+ ...graph,
2145
+ nodes: enhancedNodes
2146
+ };
2147
+ }
2148
+
2149
+ // ../core/src/output/png.ts
2150
+ async function checkBrowserAvailability() {
2151
+ try {
2152
+ const puppeteer = await import("puppeteer");
2153
+ const browser = await puppeteer.default.launch({
2154
+ headless: true,
2155
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
2156
+ });
2157
+ const executablePath = browser.process()?.spawnfile;
2158
+ await browser.close();
2159
+ return {
2160
+ available: true,
2161
+ browserPath: executablePath
2162
+ };
2163
+ } catch (err) {
2164
+ const error = err instanceof Error ? err.message : String(err);
2165
+ if (error.includes("Could not find Chromium") || error.includes("ENOENT")) {
2166
+ return {
2167
+ available: false,
2168
+ error: `Chromium not found. Puppeteer should auto-download it on first run.
2169
+
2170
+ If download failed, try:
2171
+ 1. Run: npx puppeteer browsers install chrome
2172
+ 2. Or set PUPPETEER_EXECUTABLE_PATH to your Chrome/Chromium installation
2173
+
2174
+ On macOS: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
2175
+ On Linux: /usr/bin/chromium-browser or /usr/bin/google-chrome`
2176
+ };
2177
+ }
2178
+ if (error.includes("EACCES") || error.includes("permission")) {
2179
+ return {
2180
+ available: false,
2181
+ error: `Permission error launching browser.
2182
+
2183
+ Try running with appropriate permissions or set PUPPETEER_EXECUTABLE_PATH
2184
+ to a browser you have access to.`
2185
+ };
2186
+ }
2187
+ return {
2188
+ available: false,
2189
+ error: `Failed to launch browser: ${error}`
2190
+ };
2191
+ }
2192
+ }
2193
+ var DEFAULT_OPTIONS = {
2194
+ width: 1920,
2195
+ height: 1080,
2196
+ backgroundColor: "#ffffff",
2197
+ scale: 2,
2198
+ padding: 50
2199
+ };
2200
+ async function renderExcalidrawToPng(excalidraw, options) {
2201
+ const opts = { ...DEFAULT_OPTIONS, ...options };
2202
+ const puppeteer = await import("puppeteer");
2203
+ const browser = await puppeteer.default.launch({
2204
+ headless: true,
2205
+ args: ["--no-sandbox", "--disable-setuid-sandbox"]
2206
+ });
2207
+ try {
2208
+ const page = await browser.newPage();
2209
+ await page.setViewport({
2210
+ width: 1920,
2211
+ height: 1080,
2212
+ deviceScaleFactor: opts.scale
2213
+ });
2214
+ const html = createExcalidrawExportHtml(excalidraw, opts);
2215
+ await page.setContent(html, { waitUntil: "networkidle2", timeout: 6e4 });
2216
+ await page.waitForFunction(
2217
+ () => window.__EXCALIDRAW_LOADED__,
2218
+ { timeout: 6e4 }
2219
+ );
2220
+ await page.evaluate(() => {
2221
+ ;
2222
+ window.exportToPng();
2223
+ });
2224
+ await page.waitForFunction(
2225
+ () => window.__EXPORT_COMPLETE__,
2226
+ { timeout: 6e4 }
2227
+ );
2228
+ const result = await page.evaluate(() => {
2229
+ return {
2230
+ data: window.__EXPORT_DATA__,
2231
+ error: window.__EXPORT_ERROR__
2232
+ };
2233
+ });
2234
+ if (!result.data) {
2235
+ const errorMsg = result.error || "Unknown error";
2236
+ throw new Error(`Failed to export PNG from Excalidraw: ${errorMsg}`);
2237
+ }
2238
+ return Buffer.from(result.data, "base64");
2239
+ } finally {
2240
+ await browser.close();
2241
+ }
2242
+ }
2243
+ function createExcalidrawExportHtml(excalidraw, options) {
2244
+ const data = JSON.stringify(excalidraw);
2245
+ return `<!DOCTYPE html>
2246
+ <html>
2247
+ <head>
2248
+ <meta charset="utf-8">
2249
+ <script type="importmap">
2250
+ {
2251
+ "imports": {
2252
+ "react": "https://esm.sh/react@18.2.0",
2253
+ "react/jsx-runtime": "https://esm.sh/react@18.2.0/jsx-runtime",
2254
+ "react/jsx-dev-runtime": "https://esm.sh/react@18.2.0/jsx-dev-runtime",
2255
+ "react-dom": "https://esm.sh/react-dom@18.2.0",
2256
+ "react-dom/client": "https://esm.sh/react-dom@18.2.0/client"
2257
+ }
2258
+ }
2259
+ </script>
2260
+ <style>
2261
+ * { margin: 0; padding: 0; }
2262
+ body { background: ${options.backgroundColor}; }
2263
+ #root { width: 100vw; height: 100vh; }
2264
+ </style>
2265
+ </head>
2266
+ <body>
2267
+ <div id="root"></div>
2268
+
2269
+ <script type="module">
2270
+ import React from 'react';
2271
+ import { createRoot } from 'react-dom/client';
2272
+ import { Excalidraw, exportToBlob } from 'https://esm.sh/@excalidraw/excalidraw@0.18.0?external=react,react-dom';
2273
+
2274
+ window.__EXPORT_COMPLETE__ = false;
2275
+ window.__EXPORT_DATA__ = null;
2276
+ window.__EXPORT_ERROR__ = null;
2277
+ window.__EXCALIDRAW_LOADED__ = false;
2278
+
2279
+ const data = ${data};
2280
+
2281
+ // Reference to the Excalidraw API
2282
+ let excalidrawAPI = null;
2283
+
2284
+ // Create the Excalidraw component
2285
+ const App = () => {
2286
+ return React.createElement(Excalidraw, {
2287
+ excalidrawAPI: (api) => {
2288
+ excalidrawAPI = api;
2289
+ window.__EXCALIDRAW_LOADED__ = true;
2290
+ },
2291
+ initialData: {
2292
+ elements: data.elements || [],
2293
+ appState: {
2294
+ viewBackgroundColor: "${options.backgroundColor}",
2295
+ },
2296
+ files: data.files || {},
2297
+ },
2298
+ UIOptions: {
2299
+ canvasActions: {
2300
+ export: false,
2301
+ loadScene: false,
2302
+ saveToActiveFile: false,
2303
+ },
2304
+ },
2305
+ });
2306
+ };
2307
+
2308
+ // Render the app
2309
+ const root = createRoot(document.getElementById('root'));
2310
+ root.render(React.createElement(App));
2311
+
2312
+ window.exportToPng = async function() {
2313
+ try {
2314
+ if (!excalidrawAPI) {
2315
+ window.__EXPORT_ERROR__ = 'Excalidraw API not ready';
2316
+ window.__EXPORT_COMPLETE__ = true;
2317
+ return;
2318
+ }
2319
+
2320
+ // Get the processed elements from Excalidraw (with elbow routing applied)
2321
+ const elements = excalidrawAPI.getSceneElements();
2322
+ const appState = excalidrawAPI.getAppState();
2323
+ const files = excalidrawAPI.getFiles();
2324
+
2325
+ const blob = await exportToBlob({
2326
+ elements,
2327
+ appState: {
2328
+ ...appState,
2329
+ exportBackground: true,
2330
+ viewBackgroundColor: "${options.backgroundColor}",
2331
+ },
2332
+ files,
2333
+ });
2334
+
2335
+ const reader = new FileReader();
2336
+ reader.onloadend = () => {
2337
+ window.__EXPORT_DATA__ = reader.result.split(',')[1];
2338
+ window.__EXPORT_COMPLETE__ = true;
2339
+ };
2340
+ reader.onerror = (err) => {
2341
+ window.__EXPORT_ERROR__ = 'FileReader error: ' + err;
2342
+ window.__EXPORT_COMPLETE__ = true;
2343
+ };
2344
+ reader.readAsDataURL(blob);
2345
+ } catch (error) {
2346
+ window.__EXPORT_ERROR__ = 'Export failed: ' + error.message;
2347
+ window.__EXPORT_COMPLETE__ = true;
2348
+ }
2349
+ };
2350
+ </script>
2351
+ </body>
2352
+ </html>`;
2353
+ }
2354
+
2355
+ // ../core/src/pipeline/storage.ts
2356
+ import { mkdir, readFile, readdir, writeFile } from "fs/promises";
2357
+ import { join as join3 } from "path";
2358
+
2359
+ // ../core/src/config/index.ts
2360
+ import { existsSync as existsSync2, mkdirSync, readFileSync as readFileSync3, writeFileSync } from "fs";
2361
+ import { homedir } from "os";
2362
+ import { dirname, join as join5 } from "path";
2363
+ function getConfigPath() {
2364
+ return join5(homedir(), ".config", "clarity", "config.json");
2365
+ }
2366
+ function loadConfig() {
2367
+ const configPath = getConfigPath();
2368
+ if (!existsSync2(configPath)) {
2369
+ return {};
2370
+ }
2371
+ try {
2372
+ const content = readFileSync3(configPath, "utf-8");
2373
+ return JSON.parse(content);
2374
+ } catch {
2375
+ return {};
2376
+ }
2377
+ }
2378
+ function saveConfig(config) {
2379
+ const configPath = getConfigPath();
2380
+ const configDir = dirname(configPath);
2381
+ if (!existsSync2(configDir)) {
2382
+ mkdirSync(configDir, { recursive: true });
2383
+ }
2384
+ writeFileSync(configPath, JSON.stringify(config, null, 2));
2385
+ }
2386
+ function getApiKey() {
2387
+ const config = loadConfig();
2388
+ return config.openRouterApiKey ?? process.env.OPENROUTER_API_KEY;
2389
+ }
2390
+ function maskApiKey(key) {
2391
+ if (key.length <= 12) {
2392
+ return "*".repeat(key.length);
2393
+ }
2394
+ return `${key.slice(0, 8)}...${key.slice(-4)}`;
2395
+ }
2396
+
2397
+ // src/commands/config.ts
2398
+ import { Command } from "commander";
2399
+ var configCommand = new Command("config").description(
2400
+ "Manage Clarity configuration"
2401
+ );
2402
+ configCommand.command("set-key <key>").description("Set your OpenRouter API key").action((key) => {
2403
+ const config = loadConfig();
2404
+ config.openRouterApiKey = key;
2405
+ saveConfig(config);
2406
+ console.log(`API key saved to ${getConfigPath()}`);
2407
+ });
2408
+ configCommand.command("show").description("Show current configuration").action(() => {
2409
+ const configPath = getConfigPath();
2410
+ const config = loadConfig();
2411
+ const envKey = process.env.OPENROUTER_API_KEY;
2412
+ console.log(`Config file: ${configPath}
2413
+ `);
2414
+ if (config.openRouterApiKey) {
2415
+ console.log(
2416
+ `OpenRouter API key (config): ${maskApiKey(config.openRouterApiKey)}`
2417
+ );
2418
+ } else if (envKey) {
2419
+ console.log(`OpenRouter API key (env): ${maskApiKey(envKey)}`);
2420
+ } else {
2421
+ console.log("OpenRouter API key: not set");
2422
+ console.log("\nTo enable LLM enhancement, run:");
2423
+ console.log(" clarity config set-key <your-openrouter-api-key>");
2424
+ }
2425
+ });
2426
+ configCommand.command("clear").description("Clear the stored API key").action(() => {
2427
+ const config = loadConfig();
2428
+ delete config.openRouterApiKey;
2429
+ saveConfig(config);
2430
+ console.log("API key cleared");
2431
+ if (process.env.OPENROUTER_API_KEY) {
2432
+ console.log(
2433
+ "\nNote: OPENROUTER_API_KEY environment variable is still set"
2434
+ );
2435
+ }
2436
+ });
2437
+ configCommand.command("path").description("Show the config file path").action(() => {
2438
+ console.log(getConfigPath());
2439
+ });
2440
+
2441
+ // src/generate.ts
2442
+ import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, stat, writeFile as writeFile2 } from "fs/promises";
2443
+ import { basename, join as join6, resolve } from "path";
2444
+ async function detectIaCFiles(dirPath) {
2445
+ const files = [];
2446
+ const entries = await readdir2(dirPath, { withFileTypes: true });
2447
+ for (const entry of entries) {
2448
+ const fullPath = join6(dirPath, entry.name);
2449
+ if (entry.isFile()) {
2450
+ const name = entry.name.toLowerCase();
2451
+ if (name.includes("docker-compose") || name === "compose.yml" || name === "compose.yaml") {
2452
+ files.push({ type: "docker-compose", path: fullPath, name: entry.name });
2453
+ }
2454
+ }
2455
+ if (entry.isDirectory()) {
2456
+ const chartPath = join6(fullPath, "Chart.yaml");
2457
+ try {
2458
+ await stat(chartPath);
2459
+ files.push({ type: "helm", path: fullPath, name: entry.name });
2460
+ } catch {
2461
+ try {
2462
+ await stat(join6(fullPath, "Chart.yml"));
2463
+ files.push({ type: "helm", path: fullPath, name: entry.name });
2464
+ } catch {
2465
+ }
2466
+ }
2467
+ }
2468
+ }
2469
+ try {
2470
+ await stat(join6(dirPath, "Chart.yaml"));
2471
+ files.push({ type: "helm", path: dirPath, name: basename(dirPath) });
2472
+ } catch {
2473
+ try {
2474
+ await stat(join6(dirPath, "Chart.yml"));
2475
+ files.push({ type: "helm", path: dirPath, name: basename(dirPath) });
2476
+ } catch {
2477
+ }
2478
+ }
2479
+ return files;
2480
+ }
2481
+ async function parseIaC(detected, verbose) {
2482
+ if (detected.type === "docker-compose") {
2483
+ if (verbose) console.log(` Parsing Docker Compose: ${detected.path}`);
2484
+ const content = await readFile2(detected.path, "utf-8");
2485
+ return parseDockerCompose(content, detected.name, detected.name);
2486
+ }
2487
+ if (detected.type === "helm") {
2488
+ if (verbose) console.log(` Parsing Helm chart: ${detected.path}`);
2489
+ return parseHelmChart(detected.path, detected.name, detected.path);
2490
+ }
2491
+ throw new Error(`Unknown IaC type: ${detected.type}`);
2492
+ }
2493
+ async function enhanceGraph(graph, apiKey, verbose) {
2494
+ if (verbose) console.log(" Enhancing with LLM...");
2495
+ const prompt = buildEnhancePrompt(graph);
2496
+ const response = await sendMessage(apiKey, prompt);
2497
+ const enhancements = parseJsonResponse(response);
2498
+ if (!enhancements?.services) {
2499
+ if (verbose) console.log(" Warning: No enhancements returned from LLM");
2500
+ return graph;
2501
+ }
2502
+ return applyEnhancements(graph, enhancements);
2503
+ }
2504
+ async function generate(inputPath, options = {}) {
2505
+ const resolvedInput = resolve(inputPath);
2506
+ const outputDir = resolve(options.output ?? "./docs/diagrams");
2507
+ const verbose = options.verbose ?? false;
2508
+ const skipPng = options.png === false;
2509
+ const skipLlm = options.llm === false;
2510
+ if (!skipPng) {
2511
+ const browserCheck = await checkBrowserAvailability();
2512
+ if (!browserCheck.available) {
2513
+ console.error(
2514
+ "\x1B[31m\u2717\x1B[0m Browser not available for PNG rendering\n"
2515
+ );
2516
+ console.error(browserCheck.error);
2517
+ console.error("\nUse --no-png to skip PNG generation");
2518
+ process.exit(1);
2519
+ }
2520
+ }
2521
+ const inputStat = await stat(resolvedInput);
2522
+ let detected;
2523
+ if (inputStat.isFile()) {
2524
+ const name = basename(resolvedInput).toLowerCase();
2525
+ if (name.includes("docker-compose") || name === "compose.yml" || name === "compose.yaml") {
2526
+ detected = [
2527
+ {
2528
+ type: "docker-compose",
2529
+ path: resolvedInput,
2530
+ name: basename(resolvedInput)
2531
+ }
2532
+ ];
2533
+ } else {
2534
+ console.error(`\x1B[31m\u2717\x1B[0m Unrecognized file type: ${resolvedInput}`);
2535
+ console.error("Supported: docker-compose.yml, compose.yml");
2536
+ process.exit(1);
2537
+ }
2538
+ } else if (inputStat.isDirectory()) {
2539
+ detected = await detectIaCFiles(resolvedInput);
2540
+ if (detected.length === 0) {
2541
+ console.error(`\x1B[31m\u2717\x1B[0m No IaC files found in: ${resolvedInput}`);
2542
+ console.error("Looking for: docker-compose.yml, compose.yml, Chart.yaml");
2543
+ process.exit(1);
2544
+ }
2545
+ } else {
2546
+ console.error(`\x1B[31m\u2717\x1B[0m Invalid path: ${resolvedInput}`);
2547
+ process.exit(1);
2548
+ }
2549
+ await mkdir2(outputDir, { recursive: true });
2550
+ const apiKey = getApiKey();
2551
+ const llmEnabled = !skipLlm && !!apiKey;
2552
+ if (!skipLlm && !apiKey && verbose) {
2553
+ console.log(" Note: No API key configured, skipping LLM enhancement");
2554
+ console.log(" Run: iac-diagrams config set-key <your-openrouter-key>");
2555
+ }
2556
+ for (const file of detected) {
2557
+ console.log(`
2558
+ Generating diagram for: ${file.name}`);
2559
+ if (verbose) console.log(" Step 1: Parsing...");
2560
+ let graph = await parseIaC(file, verbose);
2561
+ if (verbose) {
2562
+ console.log(` Found ${graph.nodes.length} services`);
2563
+ console.log(` Found ${graph.edges.length} dependencies`);
2564
+ }
2565
+ if (llmEnabled && apiKey) {
2566
+ if (verbose) console.log(" Step 2: Enhancing with LLM...");
2567
+ try {
2568
+ graph = await enhanceGraph(graph, apiKey, verbose);
2569
+ } catch (err) {
2570
+ console.error(
2571
+ ` \x1B[33m!\x1B[0m LLM enhancement failed: ${err instanceof Error ? err.message : String(err)}`
2572
+ );
2573
+ }
2574
+ } else if (verbose) {
2575
+ console.log(" Step 2: Skipping LLM enhancement");
2576
+ }
2577
+ if (verbose) console.log(" Step 3: Computing layout...");
2578
+ const elkConversion = infraGraphToElk(graph);
2579
+ const elkLayoutResult = await runLayout(elkConversion.graph);
2580
+ if (verbose) console.log(" Step 4: Generating Excalidraw...");
2581
+ const excalidraw = renderWithElkLayout(graph, elkLayoutResult.graph);
2582
+ const baseName = file.name.replace(/\.(yml|yaml)$/i, "");
2583
+ const excalidrawPath = join6(outputDir, `${baseName}.excalidraw`);
2584
+ await writeFile2(excalidrawPath, JSON.stringify(excalidraw, null, 2));
2585
+ console.log(` \x1B[32m\u2713\x1B[0m Saved: ${excalidrawPath}`);
2586
+ if (!skipPng) {
2587
+ if (verbose) console.log(" Step 5: Rendering PNG...");
2588
+ const pngBuffer = await renderExcalidrawToPng(excalidraw);
2589
+ const pngPath = join6(outputDir, `${baseName}.png`);
2590
+ await writeFile2(pngPath, pngBuffer);
2591
+ console.log(` \x1B[32m\u2713\x1B[0m Saved: ${pngPath}`);
2592
+ }
2593
+ }
2594
+ console.log("\n\x1B[32m\u2713\x1B[0m Done!");
2595
+ }
2596
+
2597
+ // src/index.ts
2598
+ var program = new Command2().name("iac-diagrams").description(
2599
+ "Generate architecture diagrams from Infrastructure-as-Code files"
2600
+ ).version("0.1.0").argument(
2601
+ "[path]",
2602
+ "File or directory to process (default: current directory)",
2603
+ "."
2604
+ ).option("-o, --output <dir>", "Output directory", "./docs/diagrams").option("--no-llm", "Disable LLM enhancement").option("--no-png", "Skip PNG rendering (output .excalidraw only)").option("-v, --verbose", "Show detailed output").action(
2605
+ async (path, options) => {
2606
+ await generate(path, options);
2607
+ }
2608
+ );
2609
+ program.addCommand(configCommand);
2610
+ program.parse();