@kekonic/diagrams-cli 1.0.0-rc.4

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/dist/cli.mjs ADDED
@@ -0,0 +1,1273 @@
1
+ #!/usr/bin/env node
2
+ import { t as runLanguageServer } from "./lsp-server-uxE48S7X.mjs";
3
+ import { createRequire } from "node:module";
4
+ import { createReadStream, existsSync, mkdirSync, readFileSync, statSync, watch, writeFileSync } from "node:fs";
5
+ import { basename, dirname, extname, isAbsolute, join, parse, relative, resolve, sep } from "node:path";
6
+ import { fileURLToPath } from "node:url";
7
+ import { analyzeDiagramQuality, compileSource, getCapabilities, parseSource, registerTheme, renderPipeline } from "@kekonic/diagrams";
8
+ import { KDiagramLanguageService } from "@kekonic/diagrams-language-service";
9
+ import { globSync, isDynamicPattern } from "tinyglobby";
10
+ import { spawn } from "node:child_process";
11
+ import { randomBytes } from "node:crypto";
12
+ import { createServer } from "node:http";
13
+ import { loadIconSubset } from "@kekonic/diagrams-icons";
14
+ import { STUDIO_PROTOCOL_VERSION, createStudioPreviewCoordinator, parseStudioClientMessage, studioMessageJson } from "@kekonic/diagrams-studio";
15
+ //#region src/command-model.ts
16
+ var CliUsageError = class extends Error {
17
+ constructor(..._args) {
18
+ super(..._args);
19
+ this.exitCode = 2;
20
+ }
21
+ };
22
+ const COMMANDS$1 = /* @__PURE__ */ new Set([
23
+ "render",
24
+ "check",
25
+ "analyze",
26
+ "capabilities",
27
+ "ast",
28
+ "graph",
29
+ "format",
30
+ "studio",
31
+ "lsp",
32
+ "doctor",
33
+ "completions"
34
+ ]);
35
+ const VALUE_OPTIONS = /* @__PURE__ */ new Map([
36
+ ["-o", "output"],
37
+ ["--output", "output"],
38
+ ["--out-dir", "outDir"],
39
+ ["--output-template", "outputTemplate"],
40
+ ["--theme", "theme"],
41
+ ["--exclude", "exclude"],
42
+ ["--ignore-file", "ignoreFile"],
43
+ ["--stdin-filename", "stdinFilename"],
44
+ ["--files-from", "filesFrom"],
45
+ ["--color", "color"],
46
+ ["--config", "config"],
47
+ ["--profile", "profile"],
48
+ ["--theme-file", "themeFile"],
49
+ ["--background", "background"],
50
+ ["--port", "port"]
51
+ ]);
52
+ const BOOLEAN_OPTIONS = /* @__PURE__ */ new Map([
53
+ ["--snapshot", "snapshot"],
54
+ ["--live-theme", "liveTheme"],
55
+ ["--json", "json"],
56
+ ["--pretty", "pretty"],
57
+ ["--write", "write"],
58
+ ["--check", "check"],
59
+ ["--quiet", "quiet"],
60
+ ["--verbose", "verbose"],
61
+ ["--debug", "debug"],
62
+ ["--embed-fonts", "embedFonts"],
63
+ ["--print-safe", "printSafe"],
64
+ ["--open", "open"],
65
+ ["--no-open", "noOpen"],
66
+ ["--allow-write", "allowWrite"],
67
+ ["--stdio", "stdio"]
68
+ ]);
69
+ const DISCOVERY = [
70
+ "exclude",
71
+ "ignoreFile",
72
+ "stdinFilename",
73
+ "filesFrom"
74
+ ];
75
+ const PRESENTATION = [
76
+ "color",
77
+ "quiet",
78
+ "verbose",
79
+ "debug"
80
+ ];
81
+ const ALLOWED_OPTIONS = {
82
+ render: /* @__PURE__ */ new Set([
83
+ "output",
84
+ "outDir",
85
+ "outputTemplate",
86
+ "theme",
87
+ "snapshot",
88
+ "liveTheme",
89
+ "config",
90
+ "profile",
91
+ "themeFile",
92
+ "background",
93
+ "embedFonts",
94
+ "printSafe",
95
+ ...DISCOVERY,
96
+ ...PRESENTATION
97
+ ]),
98
+ check: /* @__PURE__ */ new Set([
99
+ "json",
100
+ ...DISCOVERY,
101
+ ...PRESENTATION
102
+ ]),
103
+ analyze: /* @__PURE__ */ new Set([
104
+ "json",
105
+ "pretty",
106
+ ...DISCOVERY,
107
+ ...PRESENTATION
108
+ ]),
109
+ capabilities: /* @__PURE__ */ new Set(["pretty"]),
110
+ ast: /* @__PURE__ */ new Set([
111
+ "pretty",
112
+ "json",
113
+ ...DISCOVERY,
114
+ ...PRESENTATION
115
+ ]),
116
+ graph: /* @__PURE__ */ new Set([
117
+ "pretty",
118
+ "json",
119
+ ...DISCOVERY,
120
+ ...PRESENTATION
121
+ ]),
122
+ format: /* @__PURE__ */ new Set([
123
+ "output",
124
+ "write",
125
+ "check",
126
+ ...DISCOVERY,
127
+ ...PRESENTATION
128
+ ]),
129
+ studio: /* @__PURE__ */ new Set([
130
+ "open",
131
+ "noOpen",
132
+ "allowWrite",
133
+ "port",
134
+ ...DISCOVERY,
135
+ ...PRESENTATION
136
+ ]),
137
+ lsp: /* @__PURE__ */ new Set(["stdio"]),
138
+ doctor: /* @__PURE__ */ new Set(["json", ...PRESENTATION]),
139
+ completions: /* @__PURE__ */ new Set([])
140
+ };
141
+ function defaults() {
142
+ return {
143
+ snapshot: false,
144
+ liveTheme: false,
145
+ color: "auto",
146
+ quiet: false,
147
+ verbose: false,
148
+ debug: false,
149
+ embedFonts: false,
150
+ printSafe: false,
151
+ open: false,
152
+ noOpen: false,
153
+ allowWrite: false,
154
+ stdio: false,
155
+ json: false,
156
+ pretty: false,
157
+ write: false,
158
+ check: false,
159
+ excludes: []
160
+ };
161
+ }
162
+ function optionParts(raw) {
163
+ if (!raw.startsWith("--")) return { flag: raw };
164
+ const equals = raw.indexOf("=");
165
+ return equals < 0 ? { flag: raw } : {
166
+ flag: raw.slice(0, equals),
167
+ inlineValue: raw.slice(equals + 1)
168
+ };
169
+ }
170
+ function parseCommand(argv) {
171
+ const name = argv[0];
172
+ if (!COMMANDS$1.has(name)) {
173
+ const suggestion = name ? nearest(name, [...COMMANDS$1]) : void 0;
174
+ throw new CliUsageError(name ? `Unknown command: ${name}${suggestion ? `. Did you mean ${suggestion}?` : ""}` : "Missing command");
175
+ }
176
+ const command = name;
177
+ const inputs = [];
178
+ const options = defaults();
179
+ let positionalOnly = false;
180
+ for (let index = 1; index < argv.length; index++) {
181
+ const raw = argv[index];
182
+ if (raw === "--" && !positionalOnly) {
183
+ positionalOnly = true;
184
+ continue;
185
+ }
186
+ if (raw === "-" || positionalOnly || !raw.startsWith("-")) {
187
+ inputs.push(raw);
188
+ continue;
189
+ }
190
+ const { flag, inlineValue } = optionParts(raw);
191
+ const valueKey = VALUE_OPTIONS.get(flag);
192
+ const booleanKey = BOOLEAN_OPTIONS.get(flag);
193
+ const allowed = ALLOWED_OPTIONS[command];
194
+ if (valueKey) {
195
+ if (!allowed.has(valueKey)) throw new CliUsageError(`Unknown option for ${command}: ${flag}`);
196
+ const value = inlineValue ?? argv[++index];
197
+ if (!value || inlineValue == null && value.startsWith("-") && value !== "-") throw new CliUsageError(`Missing value for ${flag}`);
198
+ if (valueKey === "exclude") options.excludes.push(value);
199
+ else if (valueKey === "color") {
200
+ if (value !== "auto" && value !== "always" && value !== "never") throw new CliUsageError(`Invalid --color value: ${value} (expected auto, always, or never)`);
201
+ options.color = value;
202
+ } else if (valueKey === "background") {
203
+ if (value !== "transparent" && value !== "theme") throw new CliUsageError(`Invalid --background value: ${value} (expected transparent or theme)`);
204
+ options.background = value;
205
+ } else if (valueKey === "theme") options.theme = value;
206
+ else options[valueKey] = value;
207
+ continue;
208
+ }
209
+ if (booleanKey) {
210
+ if (inlineValue != null) throw new CliUsageError(`${flag} does not accept a value`);
211
+ if (!allowed.has(booleanKey)) throw new CliUsageError(`Unknown option for ${command}: ${flag}`);
212
+ options[booleanKey] = true;
213
+ continue;
214
+ }
215
+ const suggestion = nearest(flag, [
216
+ ...VALUE_OPTIONS.keys(),
217
+ ...BOOLEAN_OPTIONS.keys(),
218
+ "--help",
219
+ "--version"
220
+ ]);
221
+ throw new CliUsageError(`Unknown option: ${flag}${suggestion ? `. Did you mean ${suggestion}?` : ""}`);
222
+ }
223
+ validateOptions(command, options);
224
+ return {
225
+ name: command,
226
+ inputs,
227
+ options
228
+ };
229
+ }
230
+ function nearest(value, candidates) {
231
+ let best;
232
+ for (const candidate of candidates) {
233
+ const distance = editDistance(value, candidate);
234
+ if (!best || distance < best.distance) best = {
235
+ value: candidate,
236
+ distance
237
+ };
238
+ }
239
+ return best && best.distance <= Math.max(2, Math.floor(value.length / 3)) ? best.value : void 0;
240
+ }
241
+ function editDistance(left, right) {
242
+ const previous = Array.from({ length: right.length + 1 }, (_, index) => index);
243
+ for (let i = 1; i <= left.length; i++) {
244
+ const current = [i];
245
+ for (let j = 1; j <= right.length; j++) current[j] = Math.min(current[j - 1] + 1, previous[j] + 1, previous[j - 1] + (left[i - 1] === right[j - 1] ? 0 : 1));
246
+ previous.splice(0, previous.length, ...current);
247
+ }
248
+ return previous[right.length];
249
+ }
250
+ function validateOptions(command, options) {
251
+ if (command === "render") {
252
+ const destinations = [
253
+ options.output,
254
+ options.outDir,
255
+ options.outputTemplate
256
+ ].filter(Boolean);
257
+ if (options.output && destinations.length > 1) throw new CliUsageError("--output cannot be combined with --out-dir or --output-template");
258
+ if (options.snapshot && options.liveTheme) throw new CliUsageError("--snapshot and --live-theme cannot be combined");
259
+ }
260
+ if (command === "format") {
261
+ if (options.write && options.check) throw new CliUsageError("--write and --check cannot be combined");
262
+ if (options.output && (options.write || options.check)) throw new CliUsageError("--output cannot be combined with --write or --check");
263
+ }
264
+ if (command === "studio") {
265
+ if (options.open && options.noOpen) throw new CliUsageError("--open and --no-open cannot be combined");
266
+ if (options.port != null && (!/^\d+$/.test(options.port) || Number(options.port) > 65535)) throw new CliUsageError("--port must be an integer from 0 to 65535");
267
+ }
268
+ if (command === "lsp" && !options.stdio) throw new CliUsageError("lsp requires --stdio");
269
+ if (options.quiet && options.verbose) throw new CliUsageError("--quiet and --verbose cannot be combined");
270
+ if (command === "completions" && options.excludes.length > 0) throw new CliUsageError("completions does not accept discovery options");
271
+ }
272
+ //#endregion
273
+ //#region src/completions.ts
274
+ const COMMANDS = "render check analyze capabilities format studio lsp ast graph doctor completions";
275
+ const OPTIONS = "--help --version --color --quiet --verbose --debug --exclude --ignore-file --stdin-filename --files-from --output --out-dir --output-template --theme --theme-file --config --profile --live-theme --snapshot --background --embed-fonts --print-safe --json --pretty --check --write --open --no-open --allow-write --port --stdio";
276
+ function shellCompletions(shell) {
277
+ switch (shell) {
278
+ case "bash": return `# Kekonic Diagrams completion\n_kdiagrams() {\n local cur="\${COMP_WORDS[COMP_CWORD]}"\n COMPREPLY=( $(compgen -W "${COMMANDS} ${OPTIONS}" -- "$cur") )\n}\ncomplete -F _kdiagrams kdiagrams\n`;
279
+ case "zsh": return `#compdef kdiagrams\n_arguments '1:command:(${COMMANDS})' '*:option:(${OPTIONS})'\n`;
280
+ case "fish": return `${COMMANDS.split(" ").map((command) => `complete -c kdiagrams -f -n '__fish_use_subcommand' -a '${command}'`).join("\n")}\n`;
281
+ default: throw new CliUsageError("completions requires one shell: bash, zsh, or fish");
282
+ }
283
+ }
284
+ //#endregion
285
+ //#region src/project-config.ts
286
+ const DEFAULT_CONFIG = "kekonic-diagrams.config.json";
287
+ function resolveRenderSettings(command, cwd = process.cwd()) {
288
+ const loaded = loadProjectConfig(command.options.config, cwd);
289
+ const config = loaded?.config;
290
+ const configDir = loaded ? dirname(loaded.path) : cwd;
291
+ for (const [name, tokens] of Object.entries(config?.themes ?? {})) {
292
+ validateTokens(tokens, `themes.${name}`);
293
+ registerTheme(name, tokens);
294
+ }
295
+ if (command.options.themeFile) {
296
+ const themePath = resolve(configDir, command.options.themeFile);
297
+ const parsed = readJson(themePath);
298
+ const name = command.options.theme ?? "custom";
299
+ const tokens = isRecord(parsed) && isRecord(parsed.tokens) ? parsed.tokens : parsed;
300
+ validateTokens(tokens, themePath);
301
+ registerTheme(name, tokens);
302
+ }
303
+ const profileName = command.options.profile ?? config?.defaultProfile;
304
+ const profile = profileName ? config?.profiles?.[profileName] : void 0;
305
+ if (profileName && !profile) throw new CliUsageError(`Unknown export profile: ${profileName}`);
306
+ if (profile?.format && profile.format !== "svg") throw new CliUsageError(`Export profile ${profileName} requests ${profile.format}; this CLI build supports SVG output`);
307
+ const printSafe = command.options.printSafe || profile?.printSafe === true;
308
+ const snapshotTheme = command.options.liveTheme ? false : command.options.snapshot || profile?.snapshotTheme !== false;
309
+ const settings = {
310
+ theme: command.options.theme ?? profile?.theme ?? (printSafe ? "light" : "dark"),
311
+ snapshotTheme,
312
+ background: command.options.background ?? profile?.background ?? (printSafe ? "theme" : "transparent"),
313
+ embedFonts: command.options.embedFonts || profile?.embedFonts === true,
314
+ printSafe,
315
+ presentation: profile?.presentation,
316
+ configPath: loaded?.path,
317
+ profileName,
318
+ warnings: []
319
+ };
320
+ if (!snapshotTheme) settings.warnings.push("live-theme SVG retains unresolved CSS custom properties and requires KDiagram theme tokens from its host");
321
+ return settings;
322
+ }
323
+ function findProjectConfig(cwd) {
324
+ let current = resolve(cwd);
325
+ const root = parse(current).root;
326
+ while (true) {
327
+ const candidate = join(current, DEFAULT_CONFIG);
328
+ if (existsSync(candidate)) return candidate;
329
+ if (current === root) return void 0;
330
+ current = dirname(current);
331
+ }
332
+ }
333
+ function loadProjectConfig(explicitPath, cwd) {
334
+ const path = explicitPath ? isAbsolute(explicitPath) ? explicitPath : resolve(cwd, explicitPath) : findProjectConfig(cwd);
335
+ if (!path) return void 0;
336
+ if (!existsSync(path)) throw new CliUsageError(`Config file does not exist: ${path}`);
337
+ const parsed = readJson(path);
338
+ if (!isRecord(parsed) || parsed.version !== 1) throw new CliUsageError(`${path}: expected { "version": 1, ... }`);
339
+ validateConfigKeys(parsed, path);
340
+ return {
341
+ path,
342
+ config: parsed
343
+ };
344
+ }
345
+ function validateConfigKeys(value, path) {
346
+ const allowed = /* @__PURE__ */ new Set([
347
+ "version",
348
+ "defaultProfile",
349
+ "themes",
350
+ "profiles",
351
+ "$schema"
352
+ ]);
353
+ for (const key of Object.keys(value)) if (!allowed.has(key)) throw new CliUsageError(`${path}: unknown config property ${key}`);
354
+ if (value.profiles != null && !isRecord(value.profiles)) throw new CliUsageError(`${path}: profiles must be an object`);
355
+ for (const [name, profile] of Object.entries(value.profiles ?? {})) {
356
+ if (!isRecord(profile)) throw new CliUsageError(`${path}: profile ${name} must be an object`);
357
+ const profileKeys = /* @__PURE__ */ new Set([
358
+ "theme",
359
+ "snapshotTheme",
360
+ "background",
361
+ "embedFonts",
362
+ "printSafe",
363
+ "presentation",
364
+ "format"
365
+ ]);
366
+ for (const key of Object.keys(profile)) if (!profileKeys.has(key)) throw new CliUsageError(`${path}: unknown profiles.${name}.${key}`);
367
+ }
368
+ }
369
+ function readJson(path) {
370
+ try {
371
+ return JSON.parse(readFileSync(path, "utf8"));
372
+ } catch (error) {
373
+ throw new CliUsageError(`${path}: ${error instanceof Error ? error.message : "could not read JSON"}`);
374
+ }
375
+ }
376
+ function validateTokens(value, label) {
377
+ if (!isRecord(value)) throw new CliUsageError(`${label}: theme tokens must be an object`);
378
+ for (const [key, token] of Object.entries(value)) {
379
+ if (!/^--[a-zA-Z0-9_-]+$/.test(key) || typeof token !== "string") throw new CliUsageError(`${label}: theme tokens must map --custom-properties to strings`);
380
+ if (/[;{}<>@]/.test(token) || /url\s*\(/i.test(token)) throw new CliUsageError(`${label}.${key}: theme token contains unsupported CSS syntax`);
381
+ }
382
+ }
383
+ function isRecord(value) {
384
+ return value != null && typeof value === "object" && !Array.isArray(value);
385
+ }
386
+ //#endregion
387
+ //#region src/doctor.ts
388
+ const require$2 = createRequire(import.meta.url);
389
+ function runDoctor(cwd = process.cwd()) {
390
+ const nodeMajor = Number(process.versions.node.split(".")[0]);
391
+ const config = findProjectConfig(cwd);
392
+ let font = "unavailable";
393
+ try {
394
+ const path = require$2.resolve("@fontsource/inter/files/inter-latin-500-normal.woff");
395
+ if (existsSync(path)) font = path;
396
+ } catch {}
397
+ return [
398
+ {
399
+ name: "runtime",
400
+ status: nodeMajor >= 22 ? "pass" : "fail",
401
+ detail: `Node ${process.versions.node} (requires >=22.18.0)`
402
+ },
403
+ {
404
+ name: "font",
405
+ status: font === "unavailable" ? "fail" : "pass",
406
+ detail: font === "unavailable" ? "Bundled Inter measurement font not found" : "Bundled Inter font available"
407
+ },
408
+ {
409
+ name: "config",
410
+ status: config ? "pass" : "warn",
411
+ detail: config ?? "No kekonic-diagrams.config.json discovered; built-in export defaults apply"
412
+ },
413
+ {
414
+ name: "renderer",
415
+ status: "pass",
416
+ detail: "SVG renderer available; portable snapshot export is the default"
417
+ }
418
+ ];
419
+ }
420
+ //#endregion
421
+ //#region src/input-resolver.ts
422
+ const SOURCE_EXTENSION = ".kdiagram";
423
+ const DEFAULT_IGNORE_FILE = ".kdiagramignore";
424
+ const CRAWL_IGNORES = ["**/.git/**", "**/node_modules/**"];
425
+ const createIgnore = createRequire(import.meta.url)("ignore");
426
+ function resolveCommandInputs(command, options = {}) {
427
+ const cwd = resolve(options.cwd ?? process.cwd());
428
+ const readStdin = options.readStdin ?? (() => readFileSync(0, "utf-8"));
429
+ const requested = [...command.inputs];
430
+ if (command.options.filesFrom) {
431
+ const listSource = command.options.filesFrom;
432
+ let pathList;
433
+ if (listSource === "-") {
434
+ if (requested.includes("-")) throw new CliUsageError("stdin cannot contain both diagram source and --files-from paths");
435
+ pathList = readStdin();
436
+ } else pathList = readFileSync(resolve(cwd, listSource), "utf-8");
437
+ requested.push(...parsePathList(pathList));
438
+ }
439
+ if (requested.length === 0) {
440
+ if (command.options.filesFrom) throw new CliUsageError("--files-from did not provide any input paths");
441
+ if (options.stdinIsTTY ?? process.stdin.isTTY) throw new CliUsageError("Missing input (pass a path, '-' or pipe diagram source on stdin)");
442
+ requested.push("-");
443
+ }
444
+ const files = [];
445
+ let stdinInput;
446
+ for (const input of requested) {
447
+ if (input === "-") {
448
+ if (stdinInput) throw new CliUsageError("stdin source may be supplied only once");
449
+ const filename = command.options.stdinFilename;
450
+ const absolutePath = filename ? resolve(cwd, filename) : void 0;
451
+ stdinInput = {
452
+ kind: "stdin",
453
+ absolutePath,
454
+ displayPath: filename ?? "<stdin>",
455
+ relativePath: filename ? portableRelativePath(cwd, absolutePath) : "stdin.kdiagram",
456
+ source: readStdin()
457
+ };
458
+ continue;
459
+ }
460
+ files.push(...expandInput(input, cwd));
461
+ }
462
+ const filtered = filterIgnoredFiles(files, cwd, command.options.ignoreFile, command.options.excludes);
463
+ const unique = /* @__PURE__ */ new Map();
464
+ for (const candidate of filtered) unique.set(resolve(candidate.absolutePath), candidate);
465
+ const resolvedFiles = [...unique.values()].sort((left, right) => comparePaths(left.absolutePath, right.absolutePath)).map(({ absolutePath, relativePath }) => ({
466
+ kind: "file",
467
+ absolutePath,
468
+ displayPath: absolutePath,
469
+ relativePath
470
+ }));
471
+ const result = stdinInput ? [...resolvedFiles, stdinInput] : resolvedFiles;
472
+ if (result.length === 0) throw new CliUsageError("No .kdiagram input files matched");
473
+ return result;
474
+ }
475
+ function readResolvedInput(input) {
476
+ if (input.kind === "stdin") return input.source ?? "";
477
+ return readFileSync(input.absolutePath, "utf-8");
478
+ }
479
+ function expandInput(input, cwd) {
480
+ const absolute = resolve(cwd, input);
481
+ if (existsSync(absolute)) {
482
+ const stats = statSync(absolute);
483
+ if (stats.isDirectory()) {
484
+ const matches = globSync("**/*.kdiagram", {
485
+ cwd: absolute,
486
+ absolute: true,
487
+ dot: true,
488
+ followSymbolicLinks: false,
489
+ ignore: CRAWL_IGNORES
490
+ });
491
+ const insideCwd = isWithin(cwd, absolute);
492
+ return matches.map((file) => ({
493
+ absolutePath: file,
494
+ relativePath: insideCwd ? portableRelativePath(cwd, file) : toPosix(relative(absolute, file))
495
+ }));
496
+ }
497
+ if (!stats.isFile()) throw new CliUsageError(`Input is not a file or directory: ${input}`);
498
+ if (extname(absolute).toLowerCase() !== SOURCE_EXTENSION) throw new CliUsageError(`Input file must end in ${SOURCE_EXTENSION}: ${input}`);
499
+ return [{
500
+ absolutePath: absolute,
501
+ relativePath: portableRelativePath(cwd, absolute)
502
+ }];
503
+ }
504
+ if (!isDynamicPattern(input)) throw new CliUsageError(`Input path does not exist: ${input}`);
505
+ return globSync(input, {
506
+ cwd,
507
+ absolute: true,
508
+ dot: true,
509
+ followSymbolicLinks: false,
510
+ onlyFiles: true,
511
+ ignore: CRAWL_IGNORES
512
+ }).filter((file) => extname(file).toLowerCase() === SOURCE_EXTENSION).map((file) => ({
513
+ absolutePath: file,
514
+ relativePath: portableRelativePath(cwd, file)
515
+ }));
516
+ }
517
+ function filterIgnoredFiles(files, cwd, ignoreFileOption, excludes) {
518
+ const ignoreFile = resolve(cwd, ignoreFileOption ?? DEFAULT_IGNORE_FILE);
519
+ const ignoreRoot = dirname(ignoreFile);
520
+ const ignoreMatcher = existsSync(ignoreFile) ? createIgnore().add(readFileSync(ignoreFile, "utf-8")) : void 0;
521
+ const excludeMatcher = excludes.length > 0 ? createIgnore().add(excludes) : void 0;
522
+ if (!ignoreMatcher && !excludeMatcher) return [...files];
523
+ return files.filter((candidate) => {
524
+ if (ignoreMatcher && isWithin(ignoreRoot, candidate.absolutePath)) {
525
+ const ignoredPath = toPosix(relative(ignoreRoot, candidate.absolutePath));
526
+ if (ignoreMatcher.ignores(ignoredPath)) return false;
527
+ }
528
+ return !excludeMatcher?.ignores(candidate.relativePath);
529
+ });
530
+ }
531
+ function isWithin(parent, child) {
532
+ const rel = relative(parent, child);
533
+ return !rel.startsWith(`..${sep}`) && !isAbsolute(rel);
534
+ }
535
+ function parsePathList(contents) {
536
+ return contents.split(/\r?\n/).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#"));
537
+ }
538
+ function portableRelativePath(cwd, absolutePath) {
539
+ const rel = relative(cwd, absolutePath);
540
+ if (!rel || rel.startsWith(`..${sep}`) || isAbsolute(rel)) return basename(absolutePath);
541
+ return toPosix(rel);
542
+ }
543
+ function toPosix(path) {
544
+ return path.split(sep).join("/");
545
+ }
546
+ function comparePaths(left, right) {
547
+ const a = toPosix(left);
548
+ const b = toPosix(right);
549
+ return a < b ? -1 : a > b ? 1 : 0;
550
+ }
551
+ function createOutputContext(options, stderr = process.stderr, env = process.env) {
552
+ return {
553
+ color: shouldUseColor(options.color, stderr.isTTY === true, env),
554
+ quiet: options.quiet,
555
+ verbose: options.verbose,
556
+ debug: options.debug,
557
+ stderr
558
+ };
559
+ }
560
+ function shouldUseColor(mode, isTTY, env) {
561
+ if (mode === "always") return true;
562
+ if (mode === "never") return false;
563
+ if (env.NO_COLOR != null || env.FORCE_COLOR === "0") return false;
564
+ if (env.FORCE_COLOR != null) return true;
565
+ return isTTY;
566
+ }
567
+ function printDiagnostic(context, diagnostic, source, path) {
568
+ const { start, end } = diagnostic.range;
569
+ const severity = colorizeSeverity(context, diagnostic.severity);
570
+ context.stderr.write(`${bold(context, `${path}:${start.line}:${start.column}`)} ${severity}[${diagnostic.code}] ${diagnostic.message}\n`);
571
+ const line = source.split(/\r?\n/)[start.line - 1] ?? "";
572
+ const lineNumber = String(start.line);
573
+ const gutter = " ".repeat(lineNumber.length);
574
+ const startColumn = Math.max(1, start.column);
575
+ const endColumn = end.line === start.line ? Math.max(startColumn + 1, end.column) : line.length + 1;
576
+ const markerLength = Math.max(1, Math.min(endColumn - startColumn, line.length - startColumn + 2));
577
+ context.stderr.write(`${dim(context, `${gutter} |`)}\n`);
578
+ context.stderr.write(`${dim(context, `${lineNumber} |`)} ${line}\n`);
579
+ context.stderr.write(`${dim(context, `${gutter} |`)} ${" ".repeat(startColumn - 1)}${severityColor(context, diagnostic.severity, "^".repeat(markerLength))}\n`);
580
+ if (diagnostic.hint) context.stderr.write(`${dim(context, `${gutter} =`)} ${cyan(context, "hint:")} ${diagnostic.hint}\n`);
581
+ }
582
+ function printSummary(context, message) {
583
+ if (!context.quiet) context.stderr.write(`${message}\n`);
584
+ }
585
+ function printProgress(context, message) {
586
+ if (!context.quiet && context.verbose) context.stderr.write(`${dim(context, message)}\n`);
587
+ }
588
+ function machineEnvelope(command, payload) {
589
+ return {
590
+ version: 1,
591
+ command,
592
+ payload
593
+ };
594
+ }
595
+ function installPipeErrorHandlers() {
596
+ for (const stream of [process.stdout, process.stderr]) stream.on("error", (error) => {
597
+ if (error.code === "EPIPE") process.exit(0);
598
+ throw error;
599
+ });
600
+ }
601
+ function printCode(code) {
602
+ return `\u001b[${code}m`;
603
+ }
604
+ function styled(context, code, text) {
605
+ return context.color ? `${printCode(code)}${text}${printCode(0)}` : text;
606
+ }
607
+ function bold(context, text) {
608
+ return styled(context, 1, text);
609
+ }
610
+ function dim(context, text) {
611
+ return styled(context, 2, text);
612
+ }
613
+ function cyan(context, text) {
614
+ return styled(context, 36, text);
615
+ }
616
+ function severityColor(context, severity, text) {
617
+ return styled(context, severity === "error" ? 31 : severity === "warning" ? 33 : 36, text);
618
+ }
619
+ function colorizeSeverity(context, severity) {
620
+ return severityColor(context, severity, severity);
621
+ }
622
+ //#endregion
623
+ //#region src/output-paths.ts
624
+ function renderOutputPaths(command, inputs, cwd = process.cwd()) {
625
+ const { output, outDir, outputTemplate } = command.options;
626
+ if (inputs.length > 1 && output) throw new CliUsageError("--output accepts only one resolved input");
627
+ if (inputs.length > 1 && !outDir && !outputTemplate) throw new CliUsageError("Multiple render inputs require --out-dir or --output-template; SVG documents are never concatenated");
628
+ const paths = inputs.map((input) => {
629
+ if (output) return resolve(cwd, output);
630
+ if (!outDir && !outputTemplate) return void 0;
631
+ const rendered = applyTemplate(outputTemplate ?? "{path}.svg", input.relativePath);
632
+ return outDir ? resolve(cwd, outDir, rendered) : resolve(cwd, rendered);
633
+ });
634
+ rejectCollisions(paths);
635
+ return paths;
636
+ }
637
+ function applyTemplate(template, relativeInput) {
638
+ const extension = extname(relativeInput);
639
+ const pathWithoutExtension = extension ? relativeInput.slice(0, relativeInput.length - extension.length) : relativeInput;
640
+ const directory = dirname(pathWithoutExtension) === "." ? "" : dirname(pathWithoutExtension);
641
+ const name = basename(pathWithoutExtension);
642
+ const rendered = template.replaceAll("{path}", pathWithoutExtension).replaceAll("{dir}", directory).replaceAll("{name}", name).replaceAll("{ext}", "svg");
643
+ if (!rendered || rendered.endsWith("/")) throw new CliUsageError(`Invalid --output-template result for ${relativeInput}: ${rendered}`);
644
+ return rendered;
645
+ }
646
+ function rejectCollisions(paths) {
647
+ const seen = /* @__PURE__ */ new Set();
648
+ for (const path of paths) {
649
+ if (!path) continue;
650
+ if (seen.has(path)) throw new CliUsageError(`Multiple inputs resolve to the same output: ${path}`);
651
+ seen.add(path);
652
+ }
653
+ }
654
+ //#endregion
655
+ //#region src/portable-svg.ts
656
+ const require$1 = createRequire(import.meta.url);
657
+ function finalizePortableSvg(svg, settings) {
658
+ let output = svg;
659
+ if (settings.embedFonts) output = embedInter(output);
660
+ if (settings.background === "theme") output = output.replace(/(<desc\b[^>]*>[^<]*<\/desc>)/, `$1\n<rect class="kdiagram-export-background" width="100%" height="100%" fill="var(--kd-bg)"/>`);
661
+ return output;
662
+ }
663
+ function embedInter(svg) {
664
+ const css = `@font-face{font-family:"Inter";src:url(data:font/woff;base64,${readFileSync(require$1.resolve("@fontsource/inter/files/inter-latin-500-normal.woff")).toString("base64")}) format("woff");font-style:normal;font-weight:100 900;font-display:block;}`;
665
+ const styleIndex = svg.indexOf("<style>");
666
+ if (styleIndex >= 0) return svg.slice(0, styleIndex + 7) + css + svg.slice(styleIndex + 7);
667
+ return svg.replace(/(<svg\b[^>]*>)/, `$1\n<style>${css}</style>`);
668
+ }
669
+ //#endregion
670
+ //#region src/studio-server.ts
671
+ const require = createRequire(import.meta.url);
672
+ const ICON_NAME = /^[a-z0-9][a-z0-9-]*$/;
673
+ const MAX_ICONS_PER_REQUEST = 64;
674
+ async function startStudioServer(options) {
675
+ const files = [...new Set(options.files.map((file) => resolve(file)))].sort();
676
+ if (files.length === 0) throw new Error("Studio requires at least one .kdiagram file");
677
+ for (const file of files) if (!existsSync(file) || !statSync(file).isFile()) throw new Error(`Studio input is not a file: ${file}`);
678
+ const roots = minimalRoots(files.map(dirname));
679
+ const documents = /* @__PURE__ */ new Map();
680
+ for (const file of files) {
681
+ const id = relative(commonRoot(files), file).split(sep).join("/") || basename(file);
682
+ documents.set(id, {
683
+ id,
684
+ path: file,
685
+ label: id,
686
+ revision: 0,
687
+ source: readFileSync(file, "utf8")
688
+ });
689
+ }
690
+ let activeDocumentId = documents.keys().next().value;
691
+ let presentation = {
692
+ theme: "dark",
693
+ options: { theme: "dark" }
694
+ };
695
+ const token = randomBytes(32).toString("base64url");
696
+ const sessionId = randomBytes(16).toString("hex");
697
+ const browserRoot = resolve(options.browserRoot ?? dirname(require.resolve("@kekonic/diagrams-studio/browser")));
698
+ const clients = /* @__PURE__ */ new Set();
699
+ const renders = /* @__PURE__ */ new Map();
700
+ const coordinator = createStudioPreviewCoordinator((source, renderOptions) => renderPipeline(source, {
701
+ ...renderOptions,
702
+ snapshotTheme: true,
703
+ shadows: false
704
+ }));
705
+ const broadcast = (message) => {
706
+ const line = `data: ${studioMessageJson(message)}\n\n`;
707
+ for (const client of clients) client.write(line);
708
+ };
709
+ const renderDocument = async (document) => {
710
+ const result = await coordinator.render(document.id, document.revision, document.source, presentation.options);
711
+ if (result) {
712
+ renders.set(document.id, result);
713
+ broadcast({
714
+ version: STUDIO_PROTOCOL_VERSION,
715
+ type: "render",
716
+ ...result
717
+ });
718
+ }
719
+ };
720
+ const server = createServer(async (request, response) => {
721
+ try {
722
+ const url = new URL(request.url ?? "/", "http://127.0.0.1");
723
+ const cookieToken = request.headers.cookie?.split(";").map((item) => item.trim()).find((item) => item.startsWith("kdiagram_studio="))?.slice(16);
724
+ if (url.searchParams.get("token") !== token && cookieToken !== token) return respond(response, 403, "Forbidden");
725
+ if (request.method === "GET" && url.pathname === "/events") {
726
+ response.writeHead(200, {
727
+ "Content-Type": "text/event-stream",
728
+ "Cache-Control": "no-cache, no-transform",
729
+ Connection: "keep-alive",
730
+ "X-Content-Type-Options": "nosniff"
731
+ });
732
+ clients.add(response);
733
+ response.write(`data: ${studioMessageJson({
734
+ version: 1,
735
+ type: "ready",
736
+ sessionId,
737
+ documents: [...documents.values()],
738
+ activeDocumentId,
739
+ capabilities: {
740
+ write: options.allowWrite === true,
741
+ export: true
742
+ },
743
+ presentation
744
+ })}\n\n`);
745
+ request.on("close", () => clients.delete(response));
746
+ renderDocument(documents.get(activeDocumentId));
747
+ return;
748
+ }
749
+ if (request.method === "POST" && url.pathname === "/message") {
750
+ const message = parseStudioClientMessage(JSON.parse(await readBody(request)));
751
+ const document = "documentId" in message ? documents.get(message.documentId) : void 0;
752
+ if ("documentId" in message && !document) return respond(response, 404, "Unknown document");
753
+ switch (message.type) {
754
+ case "open":
755
+ activeDocumentId = message.documentId;
756
+ broadcast({
757
+ version: 1,
758
+ type: "document",
759
+ reason: "open",
760
+ ...document
761
+ });
762
+ renderDocument(document);
763
+ break;
764
+ case "source":
765
+ if (message.revision <= document.revision) return respond(response, 409, "Stale revision");
766
+ document.revision = message.revision;
767
+ document.source = message.source;
768
+ renderDocument(document);
769
+ break;
770
+ case "save":
771
+ if (!options.allowWrite) return respond(response, 403, "Studio writes are not authorized");
772
+ assertWithinRoots(document.path, roots);
773
+ if (message.revision < document.revision) return respond(response, 409, "Stale revision");
774
+ writeFileSync(document.path, message.source.endsWith("\n") ? message.source : `${message.source}\n`, "utf8");
775
+ document.source = readFileSync(document.path, "utf8");
776
+ document.revision = message.revision;
777
+ broadcast({
778
+ version: 1,
779
+ type: "saved",
780
+ documentId: document.id,
781
+ revision: document.revision
782
+ });
783
+ break;
784
+ case "selection":
785
+ if (message.selection.graphElement && !message.selection.range) {
786
+ const graph = renders.get(message.selection.documentId)?.graph;
787
+ const element = (message.selection.graphElement.type === "node" ? graph?.nodes : graph?.edges)?.find((item) => item.id === message.selection.graphElement?.id);
788
+ if (element?.sourceRange) message.selection.range = element.sourceRange;
789
+ }
790
+ broadcast({
791
+ version: 1,
792
+ type: "selection",
793
+ selection: message.selection
794
+ });
795
+ break;
796
+ case "viewport":
797
+ broadcast({
798
+ version: 1,
799
+ type: "viewport",
800
+ viewport: message.viewport
801
+ });
802
+ break;
803
+ case "presentation":
804
+ presentation = message.presentation;
805
+ broadcast({
806
+ version: 1,
807
+ type: "presentation",
808
+ presentation
809
+ });
810
+ renderDocument(documents.get(activeDocumentId));
811
+ break;
812
+ }
813
+ return respond(response, 204, "");
814
+ }
815
+ if (request.method === "GET" && url.pathname.startsWith("/__kdiagram/icons/")) return serveIconSubset(url, response);
816
+ if (request.method !== "GET") return respond(response, 405, "Method not allowed");
817
+ const requestedPath = url.pathname === "/" ? "index.html" : decodeURIComponent(url.pathname.slice(1));
818
+ const assetPath = resolve(browserRoot, requestedPath);
819
+ assertWithinRoots(assetPath, [browserRoot]);
820
+ if (!existsSync(assetPath) || !statSync(assetPath).isFile()) return respond(response, 404, "Not found");
821
+ response.writeHead(200, {
822
+ "Content-Type": contentType(assetPath),
823
+ "Cache-Control": requestedPath === "index.html" ? "no-store" : "public, max-age=31536000, immutable",
824
+ "Content-Security-Policy": "default-src 'self'; script-src 'self' 'wasm-unsafe-eval'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'; frame-ancestors 'none'",
825
+ "X-Content-Type-Options": "nosniff",
826
+ "Referrer-Policy": "no-referrer",
827
+ ...requestedPath === "index.html" ? { "Set-Cookie": `kdiagram_studio=${token}; HttpOnly; SameSite=Strict; Path=/` } : {}
828
+ });
829
+ createReadStream(assetPath).pipe(response);
830
+ } catch (error) {
831
+ respond(response, 400, error instanceof Error ? error.message : String(error));
832
+ }
833
+ });
834
+ const watchers = files.map((file) => watch(file, { persistent: false }, () => {
835
+ const document = [...documents.values()].find((item) => item.path === file);
836
+ if (!document) return;
837
+ const source = readFileSync(file, "utf8");
838
+ if (source === document.source) return;
839
+ document.source = source;
840
+ document.revision += 1;
841
+ broadcast({
842
+ version: 1,
843
+ type: "document",
844
+ reason: "external",
845
+ ...document
846
+ });
847
+ renderDocument(document);
848
+ }));
849
+ const heartbeat = setInterval(() => {
850
+ for (const client of clients) client.write(": heartbeat\n\n");
851
+ }, 15e3);
852
+ heartbeat.unref();
853
+ await new Promise((resolveListen, reject) => {
854
+ server.once("error", reject);
855
+ server.listen(options.port ?? 0, "127.0.0.1", resolveListen);
856
+ });
857
+ const address = server.address();
858
+ if (!address || typeof address === "string") throw new Error("Studio server did not bind a TCP port");
859
+ let resolveClosed;
860
+ const closed = new Promise((resolvePromise) => resolveClosed = resolvePromise);
861
+ server.once("close", resolveClosed);
862
+ const close = async () => {
863
+ clearInterval(heartbeat);
864
+ for (const watcher of watchers) watcher.close();
865
+ for (const client of clients) client.end();
866
+ await new Promise((resolveClose, reject) => server.close((error) => error ? reject(error) : resolveClose()));
867
+ };
868
+ return {
869
+ url: `http://127.0.0.1:${address.port}/?token=${encodeURIComponent(token)}`,
870
+ port: address.port,
871
+ token,
872
+ closed,
873
+ close
874
+ };
875
+ }
876
+ async function openStudioBrowser(url) {
877
+ const command = process.platform === "darwin" ? {
878
+ executable: "open",
879
+ args: [url]
880
+ } : process.platform === "win32" ? {
881
+ executable: "cmd",
882
+ args: [
883
+ "/c",
884
+ "start",
885
+ "",
886
+ url
887
+ ]
888
+ } : {
889
+ executable: "xdg-open",
890
+ args: [url]
891
+ };
892
+ const child = spawn(command.executable, command.args, {
893
+ detached: true,
894
+ stdio: "ignore"
895
+ });
896
+ child.unref();
897
+ await new Promise((resolveSpawn, reject) => {
898
+ child.once("spawn", resolveSpawn);
899
+ child.once("error", reject);
900
+ });
901
+ }
902
+ function respond(response, status, body) {
903
+ response.writeHead(status, {
904
+ "Content-Type": "text/plain; charset=utf-8",
905
+ "X-Content-Type-Options": "nosniff"
906
+ });
907
+ response.end(body);
908
+ }
909
+ async function serveIconSubset(url, response) {
910
+ const match = /^\/__kdiagram\/icons\/([a-z0-9-]+)\.json$/.exec(url.pathname);
911
+ const names = parseIconNames(url.searchParams.get("icons"));
912
+ if (!match || !names) return respondJson(response, 400, { error: "Invalid icon request" });
913
+ const subset = await loadIconSubset(match[1], names);
914
+ if (!subset) return respondJson(response, 404, { error: "Unknown icon collection" });
915
+ return respondJson(response, 200, subset);
916
+ }
917
+ function parseIconNames(value) {
918
+ if (!value) return null;
919
+ const names = [...new Set(value.split(","))];
920
+ if (names.length === 0 || names.length > MAX_ICONS_PER_REQUEST || names.some((name) => !ICON_NAME.test(name))) return null;
921
+ return names;
922
+ }
923
+ function respondJson(response, status, body) {
924
+ response.writeHead(status, {
925
+ "Content-Type": "application/json; charset=utf-8",
926
+ "Cache-Control": "public, max-age=86400",
927
+ "X-Content-Type-Options": "nosniff"
928
+ });
929
+ response.end(JSON.stringify(body));
930
+ }
931
+ async function readBody(request) {
932
+ const chunks = [];
933
+ let bytes = 0;
934
+ for await (const chunk of request) {
935
+ const buffer = Buffer.from(chunk);
936
+ bytes += buffer.byteLength;
937
+ if (bytes > 2e6) throw new Error("Studio message exceeds 2 MB");
938
+ chunks.push(buffer);
939
+ }
940
+ return Buffer.concat(chunks).toString("utf8");
941
+ }
942
+ function assertWithinRoots(path, roots) {
943
+ const absolute = resolve(path);
944
+ if (!roots.some((root) => absolute === root || absolute.startsWith(`${resolve(root)}${sep}`))) throw new Error("Path is outside the studio session roots");
945
+ }
946
+ function minimalRoots(directories) {
947
+ const sorted = [...new Set(directories.map((directory) => resolve(directory)))].sort();
948
+ return sorted.filter((directory, index) => !sorted.some((other, otherIndex) => otherIndex !== index && directory.startsWith(`${other}${sep}`)));
949
+ }
950
+ function commonRoot(files) {
951
+ let root = dirname(files[0]);
952
+ while (!files.every((file) => file === root || file.startsWith(`${root}${sep}`))) root = dirname(root);
953
+ return root;
954
+ }
955
+ function contentType(path) {
956
+ switch (extname(path)) {
957
+ case ".html": return "text/html; charset=utf-8";
958
+ case ".js": return "text/javascript; charset=utf-8";
959
+ case ".css": return "text/css; charset=utf-8";
960
+ case ".ttf": return "font/ttf";
961
+ case ".map": return "application/json";
962
+ default: return "application/octet-stream";
963
+ }
964
+ }
965
+ //#endregion
966
+ //#region src/cli.ts
967
+ const argv = process.argv.slice(2);
968
+ const languageService = new KDiagramLanguageService();
969
+ let activeCommand;
970
+ installPipeErrorHandlers();
971
+ function usage(stream = process.stdout) {
972
+ stream.write(`kdiagrams — deterministic text-to-diagram tooling
973
+
974
+ Common jobs:
975
+ kdiagrams check .
976
+ kdiagrams format diagrams/ --check
977
+ kdiagrams render diagrams/ --out-dir public/diagrams
978
+ kdiagrams render architecture.kdiagram -o architecture.svg --print-safe
979
+
980
+ Commands:
981
+ render [inputs...] Render portable SVG
982
+ check [inputs...] Validate source and semantics
983
+ analyze [inputs...] Analyze rendered layout quality as JSON
984
+ capabilities Describe the active language and renderer as JSON
985
+ format [inputs...] Format or check source
986
+ studio [inputs...] Launch the local browser authoring studio
987
+ lsp --stdio Run the Language Server Protocol over stdio
988
+ ast [input] Emit a versioned AST envelope
989
+ graph [input] Emit a versioned semantic-model envelope
990
+ doctor Inspect runtime, font, config, and renderer health
991
+ completions <shell> Print Bash, Zsh, or Fish completion source
992
+
993
+ Studio:
994
+ --no-open Start without opening the browser (opens by default)
995
+ --allow-write Authorize saving resolved input files
996
+ --port number Loopback port (default: random available port)
997
+
998
+ Input discovery:
999
+ --exclude pattern Git-ignore-style exclusion (repeatable)
1000
+ --ignore-file file Rules file (default: .kdiagramignore)
1001
+ --stdin-filename file Logical filename for piped diagram source
1002
+ --files-from file|- Read additional input paths, one per line
1003
+
1004
+ Portable render output:
1005
+ -o, --output file Single output file
1006
+ --out-dir dir Batch output directory; preserves relative paths
1007
+ --output-template text Template using {path}, {dir}, {name}, and {ext}
1008
+ --theme name Built-in or configured theme
1009
+ --config file Project config (default: discovered kekonic-diagrams.config.json)
1010
+ --profile name Named export profile from config
1011
+ --theme-file file JSON custom-property token map
1012
+ --live-theme Retain host-resolved CSS variables (snapshot is default)
1013
+ --background mode transparent or theme
1014
+ --embed-fonts Embed bundled Inter in SVG
1015
+ --print-safe Light snapshot with explicit theme background
1016
+
1017
+ Human and machine output:
1018
+ --json Versioned JSON envelope (check/doctor)
1019
+ --pretty Pretty versioned JSON (ast/graph)
1020
+ --color mode auto, always, or never
1021
+ --quiet Suppress summaries and progress
1022
+ --verbose Include progress details
1023
+ --debug Include stack traces for operational failures
1024
+ -v, --version Print installed version
1025
+ -h, --help Show help
1026
+
1027
+ Exit status: 0 success, 1 source/check failure, 2 usage error, 3 operational failure.
1028
+ `);
1029
+ }
1030
+ function version() {
1031
+ const manifestPath = fileURLToPath(new URL("../package.json", import.meta.url));
1032
+ return JSON.parse(readFileSync(manifestPath, "utf-8")).version ?? "unknown";
1033
+ }
1034
+ function printDiagnostics(context, diagnostics, source, path) {
1035
+ for (const diagnostic of diagnostics) printDiagnostic(context, diagnostic, source, path);
1036
+ }
1037
+ async function cmdRender(command, inputs, context) {
1038
+ const outputPaths = renderOutputPaths(command, inputs);
1039
+ const settings = resolveRenderSettings(command);
1040
+ for (const warning of settings.warnings) if (!context.quiet) context.stderr.write(`warning[FMCLI101] ${warning}\n`);
1041
+ if (settings.configPath) printProgress(context, `Using config ${settings.configPath}`);
1042
+ if (settings.profileName) printProgress(context, `Using export profile ${settings.profileName}`);
1043
+ let failed = false;
1044
+ for (let index = 0; index < inputs.length; index++) {
1045
+ const input = inputs[index];
1046
+ const source = readResolvedInput(input);
1047
+ printProgress(context, `Rendering ${input.displayPath}`);
1048
+ const result = await renderPipeline(source, {
1049
+ theme: settings.theme,
1050
+ snapshotTheme: settings.snapshotTheme,
1051
+ presentation: settings.presentation,
1052
+ shadows: false
1053
+ });
1054
+ printDiagnostics(context, result.diagnostics, source, input.displayPath);
1055
+ if (!result.ok || !result.svg) {
1056
+ failed = true;
1057
+ continue;
1058
+ }
1059
+ const svg = finalizePortableSvg(result.svg, settings);
1060
+ const outputPath = outputPaths[index];
1061
+ if (!outputPath) {
1062
+ process.stdout.write(svg);
1063
+ continue;
1064
+ }
1065
+ mkdirSync(dirname(outputPath), { recursive: true });
1066
+ writeFileSync(outputPath, svg, "utf-8");
1067
+ printSummary(context, `Wrote ${outputPath}`);
1068
+ }
1069
+ return failed ? 1 : 0;
1070
+ }
1071
+ async function cmdAnalyze(command, inputs) {
1072
+ const files = [];
1073
+ let errorCount = 0;
1074
+ let warningCount = 0;
1075
+ for (const input of inputs) {
1076
+ const result = await renderPipeline(readResolvedInput(input), { shadows: false });
1077
+ const diagnostics = result.diagnostics;
1078
+ errorCount += diagnostics.filter((item) => item.severity === "error").length;
1079
+ warningCount += diagnostics.filter((item) => item.severity === "warning").length;
1080
+ files.push({
1081
+ path: input.displayPath,
1082
+ diagnostics,
1083
+ artifact: result.layout && result.graph && result.routing ? {
1084
+ ...analyzeDiagramQuality(result.graph, result.layout, result.routing.edges).metrics,
1085
+ nodes: result.stats.nodeCount,
1086
+ edges: result.stats.edgeCount,
1087
+ layoutAlgorithm: result.stats.layoutAlgorithm,
1088
+ routerAlgorithm: result.stats.routerAlgorithm
1089
+ } : void 0
1090
+ });
1091
+ }
1092
+ process.stdout.write(`${JSON.stringify(machineEnvelope("analyze", {
1093
+ files,
1094
+ summary: {
1095
+ files: inputs.length,
1096
+ errors: errorCount,
1097
+ warnings: warningCount
1098
+ }
1099
+ }), null, command.options.pretty ? 2 : void 0)}\n`);
1100
+ return errorCount > 0 ? 1 : 0;
1101
+ }
1102
+ function cmdCheck(command, inputs, context) {
1103
+ const files = [];
1104
+ let errorCount = 0;
1105
+ let warningCount = 0;
1106
+ for (const input of inputs) {
1107
+ const source = readResolvedInput(input);
1108
+ const diagnostics = languageService.updateDocument(inputUri(input), source, 1).diagnostics;
1109
+ files.push({
1110
+ path: input.displayPath,
1111
+ diagnostics
1112
+ });
1113
+ errorCount += diagnostics.filter((diagnostic) => diagnostic.severity === "error").length;
1114
+ warningCount += diagnostics.filter((diagnostic) => diagnostic.severity === "warning").length;
1115
+ if (!command.options.json) printDiagnostics(context, diagnostics, source, input.displayPath);
1116
+ }
1117
+ if (command.options.json) process.stdout.write(`${JSON.stringify(machineEnvelope("check", {
1118
+ files,
1119
+ summary: {
1120
+ files: inputs.length,
1121
+ errors: errorCount,
1122
+ warnings: warningCount
1123
+ }
1124
+ }))}\n`);
1125
+ else printSummary(context, `${inputs.length} file(s): ${errorCount} error(s), ${warningCount} warning(s)`);
1126
+ return errorCount > 0 ? 1 : 0;
1127
+ }
1128
+ function cmdInspect(command, inputs, context) {
1129
+ requireSingleInput(command.name, inputs);
1130
+ const input = inputs[0];
1131
+ const source = readResolvedInput(input);
1132
+ if (command.name === "ast") {
1133
+ const result = parseSource(source);
1134
+ printDiagnostics(context, result.diagnostics, source, input.displayPath);
1135
+ writeInspectionEnvelope(command, input, result.ast, result.diagnostics);
1136
+ return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
1137
+ }
1138
+ const result = compileSource(source);
1139
+ printDiagnostics(context, result.diagnostics, source, input.displayPath);
1140
+ writeInspectionEnvelope(command, input, result.graph, result.diagnostics);
1141
+ return result.diagnostics.some((diagnostic) => diagnostic.severity === "error") ? 1 : 0;
1142
+ }
1143
+ function writeInspectionEnvelope(command, input, data, diagnostics) {
1144
+ process.stdout.write(`${JSON.stringify(machineEnvelope(command.name, {
1145
+ path: input.displayPath,
1146
+ data,
1147
+ diagnostics
1148
+ }), null, command.options.pretty ? 2 : void 0)}\n`);
1149
+ }
1150
+ function cmdFormat(command, inputs, context) {
1151
+ if (command.options.output) requireSingleInput(command.name, inputs);
1152
+ if (!command.options.write && !command.options.check && !command.options.output) requireSingleInput(command.name, inputs);
1153
+ if (command.options.write && inputs.some((input) => input.kind === "stdin")) throw new CliUsageError("--write cannot be used with stdin");
1154
+ let changed = false;
1155
+ for (const input of inputs) {
1156
+ const source = readResolvedInput(input);
1157
+ const uri = inputUri(input);
1158
+ languageService.updateDocument(uri, source, 1);
1159
+ const formatted = languageService.format(uri)[0]?.newText ?? source;
1160
+ if (command.options.check) {
1161
+ if (source !== formatted) {
1162
+ changed = true;
1163
+ context.stderr.write(`${input.displayPath}: not formatted\n`);
1164
+ }
1165
+ continue;
1166
+ }
1167
+ const target = command.options.output ? resolve(command.options.output) : command.options.write ? input.absolutePath : void 0;
1168
+ if (target) {
1169
+ mkdirSync(dirname(target), { recursive: true });
1170
+ writeFileSync(target, formatted, "utf-8");
1171
+ printSummary(context, `Wrote ${target}`);
1172
+ } else process.stdout.write(formatted);
1173
+ }
1174
+ return changed ? 1 : 0;
1175
+ }
1176
+ function cmdDoctor(command, context) {
1177
+ const checks = runDoctor();
1178
+ if (command.options.json) process.stdout.write(`${JSON.stringify(machineEnvelope("doctor", { checks }))}\n`);
1179
+ else for (const check of checks) context.stderr.write(`${check.status.toUpperCase().padEnd(4)} ${check.name}: ${check.detail}\n`);
1180
+ return checks.some((check) => check.status === "fail") ? 3 : 0;
1181
+ }
1182
+ function requireSingleInput(command, inputs) {
1183
+ if (inputs.length !== 1) throw new CliUsageError(`${command} requires exactly one resolved input; received ${inputs.length}`);
1184
+ }
1185
+ async function main() {
1186
+ if (argv.includes("-v") || argv.includes("--version")) {
1187
+ process.stdout.write(`${version()}\n`);
1188
+ return 0;
1189
+ }
1190
+ if (argv.includes("-h") || argv.includes("--help")) {
1191
+ usage();
1192
+ return 0;
1193
+ }
1194
+ if (argv.length === 0) {
1195
+ usage(process.stderr);
1196
+ return 2;
1197
+ }
1198
+ const command = parseCommand(argv);
1199
+ activeCommand = command;
1200
+ const context = createOutputContext(command.options);
1201
+ if (command.name === "completions") {
1202
+ if (command.inputs.length !== 1) throw new CliUsageError("completions requires one shell");
1203
+ process.stdout.write(shellCompletions(command.inputs[0]));
1204
+ return 0;
1205
+ }
1206
+ if (command.name === "doctor") {
1207
+ if (command.inputs.length > 0) throw new CliUsageError("doctor does not accept inputs");
1208
+ return cmdDoctor(command, context);
1209
+ }
1210
+ if (command.name === "capabilities") {
1211
+ if (command.inputs.length > 0) throw new CliUsageError("capabilities does not accept inputs");
1212
+ process.stdout.write(`${JSON.stringify(machineEnvelope("capabilities", getCapabilities()), null, command.options.pretty ? 2 : void 0)}\n`);
1213
+ return 0;
1214
+ }
1215
+ if (command.name === "lsp") {
1216
+ if (command.inputs.length !== 0) throw new CliUsageError("lsp does not accept inputs");
1217
+ return runLanguageServer();
1218
+ }
1219
+ if (command.name === "studio") {
1220
+ const studioInputs = resolveCommandInputs(command.inputs.length === 0 ? {
1221
+ ...command,
1222
+ inputs: ["."]
1223
+ } : command);
1224
+ if (studioInputs.some((input) => input.kind === "stdin" || !input.absolutePath)) throw new CliUsageError("studio accepts files, directories, and globs, not stdin");
1225
+ const server = await startStudioServer({
1226
+ files: studioInputs.map((input) => input.absolutePath),
1227
+ allowWrite: command.options.allowWrite,
1228
+ port: Number(command.options.port ?? 0)
1229
+ });
1230
+ context.stderr.write(`Kekonic Diagrams Studio: ${server.url}\n`);
1231
+ if (!command.options.noOpen) await openStudioBrowser(server.url);
1232
+ await server.closed;
1233
+ return 0;
1234
+ }
1235
+ const inputs = resolveCommandInputs(command);
1236
+ switch (command.name) {
1237
+ case "render": return cmdRender(command, inputs, context);
1238
+ case "check": return cmdCheck(command, inputs, context);
1239
+ case "analyze": return cmdAnalyze(command, inputs);
1240
+ case "ast":
1241
+ case "graph": return cmdInspect(command, inputs, context);
1242
+ case "format": return cmdFormat(command, inputs, context);
1243
+ }
1244
+ }
1245
+ main().then((exitCode) => {
1246
+ process.exitCode = exitCode;
1247
+ }).catch((error) => {
1248
+ if (error instanceof CliUsageError) {
1249
+ process.stderr.write(`${error.message}\n`);
1250
+ process.exitCode = 2;
1251
+ return;
1252
+ }
1253
+ const message = operationalMessage(error);
1254
+ process.stderr.write(`Operational error: ${message}\n`);
1255
+ if (activeCommand?.options.debug && error instanceof Error && error.stack) process.stderr.write(`${error.stack}\n`);
1256
+ process.exitCode = 3;
1257
+ });
1258
+ function operationalMessage(error) {
1259
+ if (error instanceof Error) {
1260
+ const code = error.code;
1261
+ if (code === "ENOENT") return `File not found: ${error.path ?? error.message}`;
1262
+ if (code === "EACCES") return `Permission denied: ${error.path ?? error.message}`;
1263
+ return error.message;
1264
+ }
1265
+ return String(error);
1266
+ }
1267
+ function inputUri(input) {
1268
+ return input.absolutePath ? `file://${input.absolutePath}` : `stdin://${input.displayPath}`;
1269
+ }
1270
+ //#endregion
1271
+ export {};
1272
+
1273
+ //# sourceMappingURL=cli.mjs.map