@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/LICENSE +21 -0
- package/README.md +142 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.mjs +1273 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/lsp-bin.d.mts +1 -0
- package/dist/lsp-bin.mjs +8 -0
- package/dist/lsp-bin.mjs.map +1 -0
- package/dist/lsp-server-uxE48S7X.mjs +325 -0
- package/dist/lsp-server-uxE48S7X.mjs.map +1 -0
- package/dist/lsp-server.d.mts +7 -0
- package/dist/lsp-server.mjs +2 -0
- package/package.json +62 -0
- package/schema/kekonic-diagrams.config.schema.json +43 -0
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
|