@rejot-dev/thalo-cli 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,271 @@
1
+ import { relativePath, resolveFilesSync } from "../files.js";
2
+ import pc from "picocolors";
3
+ import { formatDiagnostic, runCheck } from "@rejot-dev/thalo";
4
+ import * as fs from "node:fs";
5
+ import * as path from "node:path";
6
+ import { createWorkspace } from "@rejot-dev/thalo/native";
7
+
8
+ //#region src/commands/check.ts
9
+ const SEVERITY_ORDER = {
10
+ error: 0,
11
+ warning: 1,
12
+ info: 2
13
+ };
14
+ const severityColor = {
15
+ error: pc.red,
16
+ warning: pc.yellow,
17
+ info: pc.cyan
18
+ };
19
+ function formatDiagnosticDefault(d) {
20
+ const color = severityColor[d.severity];
21
+ const loc = `${d.line}:${d.column}`.padEnd(8);
22
+ const severityLabel = color(d.severity.padEnd(7));
23
+ const codeLabel = pc.dim(d.code);
24
+ return ` ${pc.dim(loc)} ${severityLabel} ${d.message} ${codeLabel}`;
25
+ }
26
+ function formatDiagnostic$1(d, format) {
27
+ switch (format) {
28
+ case "compact":
29
+ case "github": return formatDiagnostic(d, format);
30
+ default: return formatDiagnosticDefault(d);
31
+ }
32
+ }
33
+ function executeCheck(files, config) {
34
+ const workspace = createWorkspace();
35
+ for (const file of files) try {
36
+ const source = fs.readFileSync(file, "utf-8");
37
+ workspace.addDocument(source, { filename: file });
38
+ } catch (err) {
39
+ console.error(pc.red(`Error reading ${file}: ${err instanceof Error ? err.message : err}`));
40
+ }
41
+ return {
42
+ files,
43
+ result: runCheck(workspace, { config })
44
+ };
45
+ }
46
+ function outputResults(runResult, options) {
47
+ const { result, files } = runResult;
48
+ const { diagnosticsByFile, errorCount, warningCount, infoCount } = result;
49
+ const minSeverity = SEVERITY_ORDER[options.severity];
50
+ const filtered = [];
51
+ for (const diagnostics of diagnosticsByFile.values()) for (const d of diagnostics) if (SEVERITY_ORDER[d.severity] <= minSeverity) filtered.push(d);
52
+ const filesWithIssues = new Set(filtered.map((d) => d.file));
53
+ if (options.format === "json") {
54
+ const output = {
55
+ files: files.length,
56
+ issues: filtered.length,
57
+ errors: errorCount,
58
+ warnings: warningCount,
59
+ info: infoCount,
60
+ diagnostics: filtered.map((d) => ({
61
+ file: d.file,
62
+ line: d.line,
63
+ column: d.column,
64
+ endLine: d.endLine,
65
+ endColumn: d.endColumn,
66
+ severity: d.severity,
67
+ code: d.code,
68
+ message: d.message
69
+ }))
70
+ };
71
+ console.log(JSON.stringify(output, null, 2));
72
+ return;
73
+ }
74
+ if (options.format === "default") {
75
+ for (const file of files) if (filesWithIssues.has(file)) console.log(pc.bold(pc.red(`✗`) + ` ${relativePath(file)}`));
76
+ else console.log(pc.green(`✓`) + ` ${relativePath(file)}`);
77
+ if (filtered.length > 0) {
78
+ console.log();
79
+ const byFile = /* @__PURE__ */ new Map();
80
+ for (const d of filtered) {
81
+ const existing = byFile.get(d.file) || [];
82
+ existing.push(d);
83
+ byFile.set(d.file, existing);
84
+ }
85
+ for (const [file, fileDiagnostics] of byFile) {
86
+ console.log();
87
+ console.log(pc.underline(relativePath(file)));
88
+ for (const diagnostic of fileDiagnostics) console.log(formatDiagnosticDefault(diagnostic));
89
+ }
90
+ }
91
+ } else for (const diagnostic of filtered) console.log(formatDiagnostic$1(diagnostic, options.format));
92
+ if (options.format !== "github") {
93
+ console.log();
94
+ const parts = [];
95
+ if (errorCount > 0) parts.push(pc.red(`${errorCount} error${errorCount !== 1 ? "s" : ""}`));
96
+ if (warningCount > 0) parts.push(pc.yellow(`${warningCount} warning${warningCount !== 1 ? "s" : ""}`));
97
+ if (infoCount > 0) parts.push(pc.cyan(`${infoCount} info`));
98
+ const summary = parts.length > 0 ? parts.join(", ") : pc.green("no issues");
99
+ console.log(`${pc.bold(String(files.length))} files checked, ${summary}`);
100
+ }
101
+ }
102
+ function watchFiles(paths, fileTypes, options, config) {
103
+ console.log(pc.dim("Watching for file changes..."));
104
+ console.log();
105
+ const runAndReport = () => {
106
+ console.clear();
107
+ console.log(pc.dim(`[${(/* @__PURE__ */ new Date()).toLocaleTimeString()}] Checking...`));
108
+ console.log();
109
+ const files = resolveFilesSync(paths, fileTypes);
110
+ if (files.length === 0) {
111
+ const fileTypesStr = fileTypes.join(", ");
112
+ console.log(`No .${fileTypesStr} files found.`);
113
+ return;
114
+ }
115
+ outputResults(executeCheck(files, config), options);
116
+ console.log();
117
+ console.log(pc.dim("Watching for file changes... (Ctrl+C to exit)"));
118
+ };
119
+ runAndReport();
120
+ const watchedDirs = /* @__PURE__ */ new Set();
121
+ for (const targetPath of paths) {
122
+ const resolved = path.resolve(targetPath);
123
+ const dir = fs.statSync(resolved).isDirectory() ? resolved : path.dirname(resolved);
124
+ watchedDirs.add(dir);
125
+ }
126
+ let debounceTimer = null;
127
+ const extensions = fileTypes.map((type) => `.${type}`);
128
+ for (const dir of watchedDirs) fs.watch(dir, { recursive: true }, (_eventType, filename) => {
129
+ if (!filename) return;
130
+ if (!extensions.some((ext) => filename.endsWith(ext))) return;
131
+ if (debounceTimer) clearTimeout(debounceTimer);
132
+ debounceTimer = setTimeout(() => {
133
+ runAndReport();
134
+ }, 100);
135
+ });
136
+ }
137
+ function parseRuleOverrides(ruleArgs) {
138
+ const rules = /* @__PURE__ */ new Map();
139
+ if (!ruleArgs) return rules;
140
+ const ruleList = Array.isArray(ruleArgs) ? ruleArgs : [ruleArgs];
141
+ for (const ruleArg of ruleList) {
142
+ const match = ruleArg.match(/^([^=]+)=(.+)$/);
143
+ if (!match) {
144
+ console.error(`Invalid rule format: ${ruleArg}. Use: --rule <rule>=<severity>`);
145
+ process.exit(2);
146
+ }
147
+ const [, ruleCode, ruleSev] = match;
148
+ if (![
149
+ "error",
150
+ "warning",
151
+ "info",
152
+ "off"
153
+ ].includes(ruleSev)) {
154
+ console.error(`Invalid rule severity: ${ruleSev}. Use: error, warning, info, off`);
155
+ process.exit(2);
156
+ }
157
+ rules.set(ruleCode, ruleSev);
158
+ }
159
+ return rules;
160
+ }
161
+ function checkAction(ctx) {
162
+ const { options, args } = ctx;
163
+ const format = options["format"] || "default";
164
+ if (format === "json" || format === "github") process.env["NO_COLOR"] = "1";
165
+ let severity = options["severity"] || "info";
166
+ if (options["quiet"]) severity = "error";
167
+ const fileTypes = (options["file-type"] || "md,thalo").split(",").map((t) => t.trim());
168
+ const rules = parseRuleOverrides(options["rule"]);
169
+ const config = {};
170
+ if (rules.size > 0) config.rules = Object.fromEntries(rules);
171
+ const targetPaths = args.length > 0 ? args : ["."];
172
+ if (options["watch"]) {
173
+ watchFiles(targetPaths, fileTypes, {
174
+ format,
175
+ severity
176
+ }, config);
177
+ return;
178
+ }
179
+ const files = resolveFilesSync(targetPaths, fileTypes);
180
+ if (files.length === 0) {
181
+ const fileTypesStr = fileTypes.join(", ");
182
+ console.log(`No .${fileTypesStr} files found.`);
183
+ process.exit(0);
184
+ }
185
+ const runResult = executeCheck(files, config);
186
+ outputResults(runResult, {
187
+ format,
188
+ severity
189
+ });
190
+ if (runResult.result.errorCount > 0) process.exit(1);
191
+ const maxWarnings = options["max-warnings"];
192
+ if (maxWarnings !== void 0) {
193
+ const maxWarningsNum = parseInt(maxWarnings, 10);
194
+ if (isNaN(maxWarningsNum) || maxWarningsNum < 0) {
195
+ console.error(`Invalid max-warnings value: ${maxWarnings}`);
196
+ process.exit(2);
197
+ }
198
+ if (runResult.result.warningCount > maxWarningsNum) {
199
+ if (format !== "json") {
200
+ console.log();
201
+ console.error(pc.red(`Warning threshold exceeded: ${runResult.result.warningCount} warnings (max: ${maxWarningsNum})`));
202
+ }
203
+ process.exit(1);
204
+ }
205
+ }
206
+ }
207
+ const checkCommand = {
208
+ name: "check",
209
+ description: "Check and lint thalo and markdown files for errors and warnings",
210
+ args: {
211
+ name: "paths",
212
+ description: "Files or directories to check",
213
+ required: false,
214
+ multiple: true
215
+ },
216
+ options: {
217
+ quiet: {
218
+ type: "boolean",
219
+ short: "q",
220
+ description: "Only show errors, suppress warnings and info",
221
+ default: false
222
+ },
223
+ format: {
224
+ type: "string",
225
+ short: "f",
226
+ description: "Output format",
227
+ choices: [
228
+ "default",
229
+ "json",
230
+ "compact",
231
+ "github"
232
+ ],
233
+ default: "default"
234
+ },
235
+ severity: {
236
+ type: "string",
237
+ description: "Minimum severity to report",
238
+ choices: [
239
+ "error",
240
+ "warning",
241
+ "info"
242
+ ],
243
+ default: "info"
244
+ },
245
+ "max-warnings": {
246
+ type: "string",
247
+ description: "Exit with error if warnings exceed threshold"
248
+ },
249
+ rule: {
250
+ type: "string",
251
+ description: "Set rule severity (e.g., unknown-entity=off)",
252
+ multiple: true
253
+ },
254
+ watch: {
255
+ type: "boolean",
256
+ short: "w",
257
+ description: "Watch files for changes and re-run",
258
+ default: false
259
+ },
260
+ "file-type": {
261
+ type: "string",
262
+ description: "Comma-separated list of file types to check (e.g., 'md,thalo')",
263
+ default: "md,thalo"
264
+ }
265
+ },
266
+ action: checkAction
267
+ };
268
+
269
+ //#endregion
270
+ export { checkCommand };
271
+ //# sourceMappingURL=check.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"check.js","names":["SEVERITY_ORDER: Record<SeverityKey, number>","formatDiagnostic","formatDiagnosticPlain","filtered: DiagnosticInfo[]","parts: string[]","debounceTimer: NodeJS.Timeout | null","severity: SeverityKey","config: CheckConfig","checkCommand: CommandDef"],"sources":["../../src/commands/check.ts"],"sourcesContent":["import * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport {\n runCheck,\n formatDiagnostic as formatDiagnosticPlain,\n type Severity,\n type CheckConfig,\n type CheckResult,\n type DiagnosticInfo,\n type DiagnosticSeverity,\n} from \"@rejot-dev/thalo\";\nimport { createWorkspace } from \"@rejot-dev/thalo/native\";\nimport pc from \"picocolors\";\nimport type { CommandDef, CommandContext } from \"../cli.js\";\nimport { resolveFilesSync, relativePath } from \"../files.js\";\n\ntype SeverityKey = DiagnosticSeverity;\ntype OutputFormat = \"default\" | \"json\" | \"compact\" | \"github\";\n\nconst SEVERITY_ORDER: Record<SeverityKey, number> = {\n error: 0,\n warning: 1,\n info: 2,\n};\n\nconst severityColor = {\n error: pc.red,\n warning: pc.yellow,\n info: pc.cyan,\n} as const;\n\nfunction formatDiagnosticDefault(d: DiagnosticInfo): string {\n const color = severityColor[d.severity];\n\n const loc = `${d.line}:${d.column}`.padEnd(8);\n const severityLabel = color(d.severity.padEnd(7));\n const codeLabel = pc.dim(d.code);\n\n return ` ${pc.dim(loc)} ${severityLabel} ${d.message} ${codeLabel}`;\n}\n\nfunction formatDiagnostic(d: DiagnosticInfo, format: OutputFormat): string {\n switch (format) {\n case \"compact\":\n case \"github\":\n // Use shared formatter for compact/github formats (no colors needed)\n return formatDiagnosticPlain(d, format);\n default:\n return formatDiagnosticDefault(d);\n }\n}\n\ninterface RunResult {\n files: string[];\n result: CheckResult;\n}\n\nfunction executeCheck(files: string[], config: CheckConfig): RunResult {\n const workspace = createWorkspace();\n\n for (const file of files) {\n try {\n const source = fs.readFileSync(file, \"utf-8\");\n workspace.addDocument(source, { filename: file });\n } catch (err) {\n console.error(pc.red(`Error reading ${file}: ${err instanceof Error ? err.message : err}`));\n }\n }\n\n const result = runCheck(workspace, { config });\n\n return { files, result };\n}\n\ninterface OutputOptions {\n format: OutputFormat;\n severity: SeverityKey;\n}\n\nfunction outputResults(runResult: RunResult, options: OutputOptions): void {\n const { result, files } = runResult;\n const { diagnosticsByFile, errorCount, warningCount, infoCount } = result;\n\n // Collect and filter diagnostics by severity\n const minSeverity = SEVERITY_ORDER[options.severity];\n const filtered: DiagnosticInfo[] = [];\n for (const diagnostics of diagnosticsByFile.values()) {\n for (const d of diagnostics) {\n if (SEVERITY_ORDER[d.severity] <= minSeverity) {\n filtered.push(d);\n }\n }\n }\n\n // Track which files have issues\n const filesWithIssues = new Set(filtered.map((d) => d.file));\n\n if (options.format === \"json\") {\n const output = {\n files: files.length,\n issues: filtered.length,\n errors: errorCount,\n warnings: warningCount,\n info: infoCount,\n diagnostics: filtered.map((d) => ({\n file: d.file,\n line: d.line,\n column: d.column,\n endLine: d.endLine,\n endColumn: d.endColumn,\n severity: d.severity,\n code: d.code,\n message: d.message,\n })),\n };\n console.log(JSON.stringify(output, null, 2));\n return;\n }\n\n // Always print all files that were checked\n if (options.format === \"default\") {\n // Print all files first\n for (const file of files) {\n const hasIssues = filesWithIssues.has(file);\n if (hasIssues) {\n // Make files with issues bold\n console.log(pc.bold(pc.red(`✗`) + ` ${relativePath(file)}`));\n } else {\n // Files without issues in regular text\n console.log(pc.green(`✓`) + ` ${relativePath(file)}`);\n }\n }\n\n // Then show diagnostics grouped by file\n if (filtered.length > 0) {\n console.log();\n const byFile = new Map<string, DiagnosticInfo[]>();\n for (const d of filtered) {\n const existing = byFile.get(d.file) || [];\n existing.push(d);\n byFile.set(d.file, existing);\n }\n\n for (const [file, fileDiagnostics] of byFile) {\n console.log();\n console.log(pc.underline(relativePath(file)));\n for (const diagnostic of fileDiagnostics) {\n console.log(formatDiagnosticDefault(diagnostic));\n }\n }\n }\n } else {\n for (const diagnostic of filtered) {\n console.log(formatDiagnostic(diagnostic, options.format));\n }\n }\n\n if (options.format !== \"github\") {\n console.log();\n const parts: string[] = [];\n if (errorCount > 0) {\n parts.push(pc.red(`${errorCount} error${errorCount !== 1 ? \"s\" : \"\"}`));\n }\n if (warningCount > 0) {\n parts.push(pc.yellow(`${warningCount} warning${warningCount !== 1 ? \"s\" : \"\"}`));\n }\n if (infoCount > 0) {\n parts.push(pc.cyan(`${infoCount} info`));\n }\n\n const summary = parts.length > 0 ? parts.join(\", \") : pc.green(\"no issues\");\n console.log(`${pc.bold(String(files.length))} files checked, ${summary}`);\n }\n}\n\nfunction watchFiles(\n paths: string[],\n fileTypes: string[],\n options: OutputOptions,\n config: CheckConfig,\n): void {\n console.log(pc.dim(\"Watching for file changes...\"));\n console.log();\n\n const runAndReport = (): void => {\n console.clear();\n console.log(pc.dim(`[${new Date().toLocaleTimeString()}] Checking...`));\n console.log();\n\n const files = resolveFilesSync(paths, fileTypes);\n if (files.length === 0) {\n const fileTypesStr = fileTypes.join(\", \");\n console.log(`No .${fileTypesStr} files found.`);\n return;\n }\n\n const runResult = executeCheck(files, config);\n outputResults(runResult, options);\n\n console.log();\n console.log(pc.dim(\"Watching for file changes... (Ctrl+C to exit)\"));\n };\n\n runAndReport();\n\n const watchedDirs = new Set<string>();\n for (const targetPath of paths) {\n const resolved = path.resolve(targetPath);\n const stat = fs.statSync(resolved);\n const dir = stat.isDirectory() ? resolved : path.dirname(resolved);\n watchedDirs.add(dir);\n }\n\n let debounceTimer: NodeJS.Timeout | null = null;\n const extensions = fileTypes.map((type) => `.${type}`);\n\n for (const dir of watchedDirs) {\n fs.watch(dir, { recursive: true }, (_eventType, filename) => {\n if (!filename) {\n return;\n }\n if (!extensions.some((ext) => filename.endsWith(ext))) {\n return;\n }\n\n if (debounceTimer) {\n clearTimeout(debounceTimer);\n }\n\n debounceTimer = setTimeout(() => {\n runAndReport();\n }, 100);\n });\n }\n}\n\nfunction parseRuleOverrides(ruleArgs: string | string[] | undefined): Map<string, Severity> {\n const rules = new Map<string, Severity>();\n\n if (!ruleArgs) {\n return rules;\n }\n\n const ruleList = Array.isArray(ruleArgs) ? ruleArgs : [ruleArgs];\n\n for (const ruleArg of ruleList) {\n const match = ruleArg.match(/^([^=]+)=(.+)$/);\n if (!match) {\n console.error(`Invalid rule format: ${ruleArg}. Use: --rule <rule>=<severity>`);\n process.exit(2);\n }\n const [, ruleCode, ruleSev] = match;\n if (![\"error\", \"warning\", \"info\", \"off\"].includes(ruleSev)) {\n console.error(`Invalid rule severity: ${ruleSev}. Use: error, warning, info, off`);\n process.exit(2);\n }\n rules.set(ruleCode, ruleSev as Severity);\n }\n\n return rules;\n}\n\nfunction checkAction(ctx: CommandContext): void {\n const { options, args } = ctx;\n\n // Handle format-dependent color disabling\n const format = (options[\"format\"] as OutputFormat) || \"default\";\n if (format === \"json\" || format === \"github\") {\n process.env[\"NO_COLOR\"] = \"1\";\n }\n\n // Determine severity level\n let severity: SeverityKey = (options[\"severity\"] as SeverityKey) || \"info\";\n if (options[\"quiet\"]) {\n severity = \"error\";\n }\n\n // Parse file types\n const fileTypeStr = (options[\"file-type\"] as string) || \"md,thalo\";\n const fileTypes = fileTypeStr.split(\",\").map((t) => t.trim());\n\n // Parse rule overrides\n const rules = parseRuleOverrides(options[\"rule\"] as string | string[] | undefined);\n\n // Build check config\n const config: CheckConfig = {};\n if (rules.size > 0) {\n config.rules = Object.fromEntries(rules);\n }\n\n // Determine target paths\n const targetPaths = args.length > 0 ? args : [\".\"];\n\n // Watch mode\n if (options[\"watch\"]) {\n watchFiles(targetPaths, fileTypes, { format, severity }, config);\n return;\n }\n\n // Collect files\n const files = resolveFilesSync(targetPaths, fileTypes);\n\n if (files.length === 0) {\n const fileTypesStr = fileTypes.join(\", \");\n console.log(`No .${fileTypesStr} files found.`);\n process.exit(0);\n }\n\n // Run checks\n const runResult = executeCheck(files, config);\n\n // Output results\n outputResults(runResult, { format, severity });\n\n // Determine exit code\n if (runResult.result.errorCount > 0) {\n process.exit(1);\n }\n\n const maxWarnings = options[\"max-warnings\"];\n if (maxWarnings !== undefined) {\n const maxWarningsNum = parseInt(maxWarnings as string, 10);\n if (isNaN(maxWarningsNum) || maxWarningsNum < 0) {\n console.error(`Invalid max-warnings value: ${maxWarnings}`);\n process.exit(2);\n }\n\n if (runResult.result.warningCount > maxWarningsNum) {\n if (format !== \"json\") {\n console.log();\n console.error(\n pc.red(\n `Warning threshold exceeded: ${runResult.result.warningCount} warnings (max: ${maxWarningsNum})`,\n ),\n );\n }\n process.exit(1);\n }\n }\n}\n\nexport const checkCommand: CommandDef = {\n name: \"check\",\n description: \"Check and lint thalo and markdown files for errors and warnings\",\n args: {\n name: \"paths\",\n description: \"Files or directories to check\",\n required: false,\n multiple: true,\n },\n options: {\n quiet: {\n type: \"boolean\",\n short: \"q\",\n description: \"Only show errors, suppress warnings and info\",\n default: false,\n },\n format: {\n type: \"string\",\n short: \"f\",\n description: \"Output format\",\n choices: [\"default\", \"json\", \"compact\", \"github\"],\n default: \"default\",\n },\n severity: {\n type: \"string\",\n description: \"Minimum severity to report\",\n choices: [\"error\", \"warning\", \"info\"],\n default: \"info\",\n },\n \"max-warnings\": {\n type: \"string\",\n description: \"Exit with error if warnings exceed threshold\",\n },\n rule: {\n type: \"string\",\n description: \"Set rule severity (e.g., unknown-entity=off)\",\n multiple: true,\n },\n watch: {\n type: \"boolean\",\n short: \"w\",\n description: \"Watch files for changes and re-run\",\n default: false,\n },\n \"file-type\": {\n type: \"string\",\n description: \"Comma-separated list of file types to check (e.g., 'md,thalo')\",\n default: \"md,thalo\",\n },\n },\n action: checkAction,\n};\n"],"mappings":"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"}
@@ -0,0 +1,220 @@
1
+ import { relativePath } from "../files.js";
2
+ import pc from "picocolors";
3
+ import { runFormat } from "@rejot-dev/thalo";
4
+ import * as fs from "node:fs/promises";
5
+ import * as path from "node:path";
6
+ import { createWorkspace } from "@rejot-dev/thalo/native";
7
+ import ignore from "ignore";
8
+
9
+ //#region src/commands/format.ts
10
+ async function loadIgnoreFile(filePath) {
11
+ try {
12
+ return (await fs.readFile(filePath, "utf-8")).split("\n").filter((line) => line.trim() && !line.startsWith("#"));
13
+ } catch {
14
+ return [];
15
+ }
16
+ }
17
+ async function createIgnoreFilter(dir) {
18
+ const ig = ignore();
19
+ ig.add(await loadIgnoreFile(path.join(dir, ".gitignore")));
20
+ ig.add(await loadIgnoreFile(path.join(dir, ".prettierignore")));
21
+ return ig;
22
+ }
23
+ async function collectFilesWithIgnore(dir, fileTypes) {
24
+ const files = [];
25
+ const ig = await createIgnoreFilter(dir);
26
+ const patterns = fileTypes.map((type) => `**/*.${type}`);
27
+ for (const pattern of patterns) for await (const entry of fs.glob(pattern, {
28
+ cwd: dir,
29
+ exclude: (name) => name === "node_modules" || name.startsWith(".")
30
+ })) {
31
+ const igPath = entry.split(path.sep).join("/");
32
+ if (!ig.ignores(igPath)) files.push(path.join(dir, entry));
33
+ }
34
+ return files;
35
+ }
36
+ async function resolveFormatFiles(paths, fileTypes) {
37
+ const files = [];
38
+ for (const targetPath of paths) {
39
+ const resolved = path.resolve(targetPath);
40
+ try {
41
+ const stat$1 = await fs.stat(resolved);
42
+ if (stat$1.isDirectory()) files.push(...await collectFilesWithIgnore(resolved, fileTypes));
43
+ else if (stat$1.isFile()) {
44
+ const ext = path.extname(resolved).slice(1);
45
+ if (fileTypes.includes(ext)) files.push(resolved);
46
+ }
47
+ } catch {
48
+ console.error(pc.red(`Error: Path not found: ${targetPath}`));
49
+ process.exit(2);
50
+ }
51
+ }
52
+ return files;
53
+ }
54
+ function formatSyntaxError(error) {
55
+ const loc = `${error.line}:${error.column}`.padEnd(8);
56
+ const severityLabel = pc.red("error".padEnd(7));
57
+ const codeLabel = pc.dim(error.code);
58
+ return ` ${pc.dim(loc)} ${severityLabel} ${error.message} ${codeLabel}`;
59
+ }
60
+ function getParser(filePath) {
61
+ const ext = path.extname(filePath).slice(1);
62
+ if (ext === "thalo") return "thalo";
63
+ if (ext === "md") return "markdown";
64
+ return "thalo";
65
+ }
66
+ async function createPrettierFormatter() {
67
+ const prettier = await import("prettier");
68
+ const thaloPrettier = await import("@rejot-dev/thalo-prettier");
69
+ return async (source, filepath) => {
70
+ const parser = getParser(filepath);
71
+ const resolvedConfig = await prettier.resolveConfig(filepath);
72
+ return prettier.format(source, {
73
+ ...resolvedConfig,
74
+ filepath,
75
+ parser,
76
+ plugins: [thaloPrettier]
77
+ });
78
+ };
79
+ }
80
+ async function readStdin() {
81
+ return new Promise((resolve$1, reject) => {
82
+ let data = "";
83
+ let settled = false;
84
+ process.stdin.setEncoding("utf-8");
85
+ const cleanup = () => {
86
+ process.stdin.removeListener("data", onData);
87
+ process.stdin.removeListener("end", onEnd);
88
+ process.stdin.removeListener("error", onError);
89
+ process.stdin.removeListener("close", onClose);
90
+ };
91
+ const settle = (fn) => {
92
+ if (!settled) {
93
+ settled = true;
94
+ cleanup();
95
+ fn();
96
+ }
97
+ };
98
+ const onData = (chunk) => {
99
+ data += chunk;
100
+ };
101
+ const onEnd = () => {
102
+ settle(() => resolve$1(data));
103
+ };
104
+ const onError = (error) => {
105
+ settle(() => reject(error));
106
+ };
107
+ const onClose = () => {
108
+ settle(() => resolve$1(data));
109
+ };
110
+ process.stdin.on("data", onData);
111
+ process.stdin.once("end", onEnd);
112
+ process.stdin.once("error", onError);
113
+ process.stdin.once("close", onClose);
114
+ });
115
+ }
116
+ async function formatAction(ctx) {
117
+ const { options, args } = ctx;
118
+ const checkOnly = options["check"];
119
+ const writeBack = options["write"];
120
+ const useStdin = options["stdin"];
121
+ const fileTypes = (options["file-type"] || "md,thalo").split(",").map((t) => t.trim());
122
+ if (useStdin) {
123
+ const content = await readStdin();
124
+ const workspace = createWorkspace();
125
+ const formatter = await createPrettierFormatter();
126
+ const result$1 = await runFormat(workspace, [{
127
+ file: args[0] || "stdin.thalo",
128
+ content
129
+ }], { formatter });
130
+ const fileResult = result$1.fileResults[0];
131
+ if (fileResult) process.stdout.write(fileResult.formatted);
132
+ if (result$1.syntaxErrorCount > 0) process.exit(1);
133
+ return;
134
+ }
135
+ const filePaths = await resolveFormatFiles(args.length > 0 ? args : ["."], fileTypes);
136
+ if (filePaths.length === 0) {
137
+ const fileTypesStr = fileTypes.join(", ");
138
+ console.log(`No .${fileTypesStr} files found.`);
139
+ process.exit(0);
140
+ }
141
+ const files = await Promise.all(filePaths.map(async (file) => ({
142
+ file,
143
+ content: await fs.readFile(file, "utf-8")
144
+ })));
145
+ const result = await runFormat(createWorkspace(), files, { formatter: await createPrettierFormatter() });
146
+ let writeCount = 0;
147
+ for (const fileResult of result.fileResults) {
148
+ const relPath = relativePath(fileResult.file);
149
+ if (fileResult.hasSyntaxErrors) console.log(pc.bold(pc.red(`✗`) + ` ${relPath}`));
150
+ else if (checkOnly) if (fileResult.isChanged) console.log(pc.bold(pc.red(`✗`) + ` ${relPath}`));
151
+ else console.log(pc.green(`✓`) + ` ${relPath}`);
152
+ else if (writeBack) if (fileResult.isChanged) {
153
+ await fs.writeFile(fileResult.file, fileResult.formatted, "utf-8");
154
+ console.log(pc.bold(pc.green(`✓`) + ` ${relPath}`));
155
+ writeCount++;
156
+ } else console.log(pc.green(`✓`) + ` ${relPath}`);
157
+ else if (fileResult.isChanged) console.log(pc.yellow(`⚠`) + ` ${relPath} (needs formatting)`);
158
+ else console.log(pc.green(`✓`) + ` ${relPath}`);
159
+ }
160
+ const filesWithErrors = result.fileResults.filter((r) => r.hasSyntaxErrors);
161
+ if (filesWithErrors.length > 0) {
162
+ console.log();
163
+ for (const fileResult of filesWithErrors) {
164
+ console.log();
165
+ console.log(pc.underline(relativePath(fileResult.file)));
166
+ for (const error of fileResult.syntaxErrors) console.log(formatSyntaxError(error));
167
+ }
168
+ }
169
+ if (result.filesProcessed > 1 || checkOnly) {
170
+ console.log();
171
+ if (checkOnly) if (result.changedCount + result.syntaxErrorCount > 0) {
172
+ const parts = [];
173
+ if (result.syntaxErrorCount > 0) parts.push(pc.red(`${result.syntaxErrorCount} file${result.syntaxErrorCount !== 1 ? "s" : ""} with syntax errors`));
174
+ if (result.changedCount > 0) parts.push(pc.yellow(`${result.changedCount} file${result.changedCount !== 1 ? "s" : ""} need${result.changedCount === 1 ? "s" : ""} formatting`));
175
+ console.log(parts.join(", "));
176
+ process.exit(1);
177
+ } else console.log(pc.green(`All ${result.filesProcessed} files are properly formatted`));
178
+ else if (writeBack) console.log(`Formatted ${writeCount} file${writeCount !== 1 ? "s" : ""}`);
179
+ }
180
+ if (result.syntaxErrorCount > 0) process.exit(1);
181
+ }
182
+ const formatCommand = {
183
+ name: "format",
184
+ description: "Format thalo and markdown files using Prettier",
185
+ args: {
186
+ name: "paths",
187
+ description: "Files or directories to format",
188
+ required: false,
189
+ multiple: true
190
+ },
191
+ options: {
192
+ check: {
193
+ type: "boolean",
194
+ short: "c",
195
+ description: "Check if files are formatted (exit 1 if not)",
196
+ default: false
197
+ },
198
+ write: {
199
+ type: "boolean",
200
+ short: "w",
201
+ description: "Write formatted output back to files",
202
+ default: true
203
+ },
204
+ stdin: {
205
+ type: "boolean",
206
+ description: "Read from stdin and output to stdout (for editor integration)",
207
+ default: false
208
+ },
209
+ "file-type": {
210
+ type: "string",
211
+ description: "Comma-separated list of file types to format (e.g., 'md,thalo')",
212
+ default: "md,thalo"
213
+ }
214
+ },
215
+ action: formatAction
216
+ };
217
+
218
+ //#endregion
219
+ export { formatCommand };
220
+ //# sourceMappingURL=format.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"format.js","names":["files: string[]","stat","resolve","result","files: FormatFileInput[]","result: FormatResult","parts: string[]","formatCommand: CommandDef"],"sources":["../../src/commands/format.ts"],"sourcesContent":["import * as fs from \"node:fs/promises\";\nimport * as path from \"node:path\";\nimport ignore from \"ignore\";\nimport pc from \"picocolors\";\nimport type { CommandDef, CommandContext } from \"../cli.js\";\nimport { createWorkspace } from \"@rejot-dev/thalo/native\";\nimport {\n runFormat,\n type FormatResult,\n type FormatFileInput,\n type SyntaxErrorInfo,\n} from \"@rejot-dev/thalo\";\nimport { relativePath } from \"../files.js\";\n\n// ===================\n// File Collection (format-specific with ignore patterns)\n// ===================\n\nasync function loadIgnoreFile(filePath: string): Promise<string[]> {\n try {\n const content = await fs.readFile(filePath, \"utf-8\");\n return content.split(\"\\n\").filter((line) => line.trim() && !line.startsWith(\"#\"));\n } catch {\n return [];\n }\n}\n\nasync function createIgnoreFilter(dir: string) {\n const ig = ignore();\n ig.add(await loadIgnoreFile(path.join(dir, \".gitignore\")));\n ig.add(await loadIgnoreFile(path.join(dir, \".prettierignore\")));\n return ig;\n}\n\nasync function collectFilesWithIgnore(dir: string, fileTypes: string[]): Promise<string[]> {\n const files: string[] = [];\n const ig = await createIgnoreFilter(dir);\n\n // Build glob patterns for each file type\n const patterns = fileTypes.map((type) => `**/*.${type}`);\n\n // exclude prevents traversing into node_modules/.git (perf), ig.ignores handles user patterns\n for (const pattern of patterns) {\n for await (const entry of fs.glob(pattern, {\n cwd: dir,\n exclude: (name) => name === \"node_modules\" || name.startsWith(\".\"),\n })) {\n // Normalize to forward slashes for ignore matching (ignore lib expects posix paths)\n const igPath = entry.split(path.sep).join(\"/\");\n if (!ig.ignores(igPath)) {\n files.push(path.join(dir, entry));\n }\n }\n }\n\n return files;\n}\n\nasync function resolveFormatFiles(paths: string[], fileTypes: string[]): Promise<string[]> {\n const files: string[] = [];\n\n for (const targetPath of paths) {\n const resolved = path.resolve(targetPath);\n\n try {\n const stat = await fs.stat(resolved);\n if (stat.isDirectory()) {\n files.push(...(await collectFilesWithIgnore(resolved, fileTypes)));\n } else if (stat.isFile()) {\n const ext = path.extname(resolved).slice(1); // Remove leading dot\n if (fileTypes.includes(ext)) {\n files.push(resolved);\n }\n }\n } catch {\n console.error(pc.red(`Error: Path not found: ${targetPath}`));\n process.exit(2);\n }\n }\n\n return files;\n}\n\nfunction formatSyntaxError(error: SyntaxErrorInfo): string {\n const loc = `${error.line}:${error.column}`.padEnd(8);\n const severityLabel = pc.red(\"error\".padEnd(7));\n const codeLabel = pc.dim(error.code);\n\n return ` ${pc.dim(loc)} ${severityLabel} ${error.message} ${codeLabel}`;\n}\n\n// ===================\n// Prettier Integration\n// ===================\n\nfunction getParser(filePath: string): string {\n const ext = path.extname(filePath).slice(1);\n if (ext === \"thalo\") {\n return \"thalo\";\n }\n if (ext === \"md\") {\n return \"markdown\";\n }\n return \"thalo\"; // default\n}\n\nasync function createPrettierFormatter(): Promise<\n (source: string, filepath: string) => Promise<string>\n> {\n const prettier = await import(\"prettier\");\n const thaloPrettier = await import(\"@rejot-dev/thalo-prettier\");\n\n return async (source: string, filepath: string): Promise<string> => {\n const parser = getParser(filepath);\n // Load project's prettier config (prettier.config.mjs, .prettierrc, etc.)\n const resolvedConfig = await prettier.resolveConfig(filepath);\n return prettier.format(source, {\n ...resolvedConfig,\n filepath,\n parser,\n plugins: [thaloPrettier],\n });\n };\n}\n\n// ===================\n// Command Action\n// ===================\n\nasync function readStdin(): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = \"\";\n let settled = false;\n process.stdin.setEncoding(\"utf-8\");\n\n const cleanup = () => {\n process.stdin.removeListener(\"data\", onData);\n process.stdin.removeListener(\"end\", onEnd);\n process.stdin.removeListener(\"error\", onError);\n process.stdin.removeListener(\"close\", onClose);\n };\n\n const settle = (fn: () => void) => {\n if (!settled) {\n settled = true;\n cleanup();\n fn();\n }\n };\n\n const onData = (chunk: string) => {\n data += chunk;\n };\n\n const onEnd = () => {\n settle(() => resolve(data));\n };\n\n const onError = (error: Error) => {\n settle(() => reject(error));\n };\n\n const onClose = () => {\n // This handles cases where stdin closes without 'end' (e.g., EOF)\n settle(() => resolve(data));\n };\n\n // 'data' can fire multiple times, so use 'on'\n process.stdin.on(\"data\", onData);\n // 'end', 'error', and 'close' should only fire once, so use 'once'\n process.stdin.once(\"end\", onEnd);\n process.stdin.once(\"error\", onError);\n process.stdin.once(\"close\", onClose);\n });\n}\n\nasync function formatAction(ctx: CommandContext): Promise<void> {\n const { options, args } = ctx;\n const checkOnly = options[\"check\"] as boolean;\n const writeBack = options[\"write\"] as boolean;\n const useStdin = options[\"stdin\"] as boolean;\n const fileTypeStr = (options[\"file-type\"] as string) || \"md,thalo\";\n const fileTypes = fileTypeStr.split(\",\").map((t) => t.trim());\n\n // Handle stdin mode - read from stdin, output to stdout\n if (useStdin) {\n const content = await readStdin();\n const workspace = createWorkspace();\n const formatter = await createPrettierFormatter();\n\n // Use a placeholder filepath for parser detection (default to .thalo)\n const filepath = args[0] || \"stdin.thalo\";\n const files: FormatFileInput[] = [{ file: filepath, content }];\n const result = await runFormat(workspace, files, { formatter });\n\n const fileResult = result.fileResults[0];\n if (fileResult) {\n // Output formatted content to stdout\n process.stdout.write(fileResult.formatted);\n }\n\n // Exit with error code if there were syntax errors\n if (result.syntaxErrorCount > 0) {\n process.exit(1);\n }\n return;\n }\n\n const targetPaths = args.length > 0 ? args : [\".\"];\n const filePaths = await resolveFormatFiles(targetPaths, fileTypes);\n\n if (filePaths.length === 0) {\n const fileTypesStr = fileTypes.join(\", \");\n console.log(`No .${fileTypesStr} files found.`);\n process.exit(0);\n }\n\n // Read all file contents\n const files: FormatFileInput[] = await Promise.all(\n filePaths.map(async (file) => ({\n file,\n content: await fs.readFile(file, \"utf-8\"),\n })),\n );\n\n // Create workspace and formatter\n const workspace = createWorkspace();\n const formatter = await createPrettierFormatter();\n\n // Run format\n const result: FormatResult = await runFormat(workspace, files, { formatter });\n\n // Output results\n let writeCount = 0;\n\n for (const fileResult of result.fileResults) {\n const relPath = relativePath(fileResult.file);\n\n if (fileResult.hasSyntaxErrors) {\n // File has syntax errors - mark as failed\n console.log(pc.bold(pc.red(`✗`) + ` ${relPath}`));\n } else if (checkOnly) {\n if (fileResult.isChanged) {\n // Make files needing formatting bold with ✗\n console.log(pc.bold(pc.red(`✗`) + ` ${relPath}`));\n } else {\n // Files already formatted\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n } else if (writeBack) {\n if (fileResult.isChanged) {\n await fs.writeFile(fileResult.file, fileResult.formatted, \"utf-8\");\n // Make formatted files bold (like prettier)\n console.log(pc.bold(pc.green(`✓`) + ` ${relPath}`));\n writeCount++;\n } else {\n // Print unchanged files in regular text\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n } else {\n // This branch shouldn't happen since write defaults to true, but keep for safety\n if (fileResult.isChanged) {\n console.log(pc.yellow(`⚠`) + ` ${relPath} (needs formatting)`);\n } else {\n console.log(pc.green(`✓`) + ` ${relPath}`);\n }\n }\n }\n\n // Print syntax errors grouped by file (like check command does)\n const filesWithErrors = result.fileResults.filter((r) => r.hasSyntaxErrors);\n if (filesWithErrors.length > 0) {\n console.log();\n for (const fileResult of filesWithErrors) {\n console.log();\n console.log(pc.underline(relativePath(fileResult.file)));\n for (const error of fileResult.syntaxErrors) {\n console.log(formatSyntaxError(error));\n }\n }\n }\n\n // Print summary\n if (result.filesProcessed > 1 || checkOnly) {\n console.log();\n if (checkOnly) {\n const totalIssues = result.changedCount + result.syntaxErrorCount;\n if (totalIssues > 0) {\n const parts: string[] = [];\n if (result.syntaxErrorCount > 0) {\n parts.push(\n pc.red(\n `${result.syntaxErrorCount} file${result.syntaxErrorCount !== 1 ? \"s\" : \"\"} with syntax errors`,\n ),\n );\n }\n if (result.changedCount > 0) {\n parts.push(\n pc.yellow(\n `${result.changedCount} file${result.changedCount !== 1 ? \"s\" : \"\"} need${result.changedCount === 1 ? \"s\" : \"\"} formatting`,\n ),\n );\n }\n console.log(parts.join(\", \"));\n process.exit(1);\n } else {\n console.log(pc.green(`All ${result.filesProcessed} files are properly formatted`));\n }\n } else if (writeBack) {\n console.log(`Formatted ${writeCount} file${writeCount !== 1 ? \"s\" : \"\"}`);\n }\n }\n\n if (result.syntaxErrorCount > 0) {\n process.exit(1);\n }\n}\n\nexport const formatCommand: CommandDef = {\n name: \"format\",\n description: \"Format thalo and markdown files using Prettier\",\n args: {\n name: \"paths\",\n description: \"Files or directories to format\",\n required: false,\n multiple: true,\n },\n options: {\n check: {\n type: \"boolean\",\n short: \"c\",\n description: \"Check if files are formatted (exit 1 if not)\",\n default: false,\n },\n write: {\n type: \"boolean\",\n short: \"w\",\n description: \"Write formatted output back to files\",\n default: true,\n },\n stdin: {\n type: \"boolean\",\n description: \"Read from stdin and output to stdout (for editor integration)\",\n default: false,\n },\n \"file-type\": {\n type: \"string\",\n description: \"Comma-separated list of file types to format (e.g., 'md,thalo')\",\n default: \"md,thalo\",\n },\n },\n action: formatAction,\n};\n"],"mappings":";;;;;;;;;AAkBA,eAAe,eAAe,UAAqC;AACjE,KAAI;AAEF,UADgB,MAAM,GAAG,SAAS,UAAU,QAAQ,EACrC,MAAM,KAAK,CAAC,QAAQ,SAAS,KAAK,MAAM,IAAI,CAAC,KAAK,WAAW,IAAI,CAAC;SAC3E;AACN,SAAO,EAAE;;;AAIb,eAAe,mBAAmB,KAAa;CAC7C,MAAM,KAAK,QAAQ;AACnB,IAAG,IAAI,MAAM,eAAe,KAAK,KAAK,KAAK,aAAa,CAAC,CAAC;AAC1D,IAAG,IAAI,MAAM,eAAe,KAAK,KAAK,KAAK,kBAAkB,CAAC,CAAC;AAC/D,QAAO;;AAGT,eAAe,uBAAuB,KAAa,WAAwC;CACzF,MAAMA,QAAkB,EAAE;CAC1B,MAAM,KAAK,MAAM,mBAAmB,IAAI;CAGxC,MAAM,WAAW,UAAU,KAAK,SAAS,QAAQ,OAAO;AAGxD,MAAK,MAAM,WAAW,SACpB,YAAW,MAAM,SAAS,GAAG,KAAK,SAAS;EACzC,KAAK;EACL,UAAU,SAAS,SAAS,kBAAkB,KAAK,WAAW,IAAI;EACnE,CAAC,EAAE;EAEF,MAAM,SAAS,MAAM,MAAM,KAAK,IAAI,CAAC,KAAK,IAAI;AAC9C,MAAI,CAAC,GAAG,QAAQ,OAAO,CACrB,OAAM,KAAK,KAAK,KAAK,KAAK,MAAM,CAAC;;AAKvC,QAAO;;AAGT,eAAe,mBAAmB,OAAiB,WAAwC;CACzF,MAAMA,QAAkB,EAAE;AAE1B,MAAK,MAAM,cAAc,OAAO;EAC9B,MAAM,WAAW,KAAK,QAAQ,WAAW;AAEzC,MAAI;GACF,MAAMC,SAAO,MAAM,GAAG,KAAK,SAAS;AACpC,OAAIA,OAAK,aAAa,CACpB,OAAM,KAAK,GAAI,MAAM,uBAAuB,UAAU,UAAU,CAAE;YACzDA,OAAK,QAAQ,EAAE;IACxB,MAAM,MAAM,KAAK,QAAQ,SAAS,CAAC,MAAM,EAAE;AAC3C,QAAI,UAAU,SAAS,IAAI,CACzB,OAAM,KAAK,SAAS;;UAGlB;AACN,WAAQ,MAAM,GAAG,IAAI,0BAA0B,aAAa,CAAC;AAC7D,WAAQ,KAAK,EAAE;;;AAInB,QAAO;;AAGT,SAAS,kBAAkB,OAAgC;CACzD,MAAM,MAAM,GAAG,MAAM,KAAK,GAAG,MAAM,SAAS,OAAO,EAAE;CACrD,MAAM,gBAAgB,GAAG,IAAI,QAAQ,OAAO,EAAE,CAAC;CAC/C,MAAM,YAAY,GAAG,IAAI,MAAM,KAAK;AAEpC,QAAO,KAAK,GAAG,IAAI,IAAI,CAAC,GAAG,cAAc,GAAG,MAAM,QAAQ,IAAI;;AAOhE,SAAS,UAAU,UAA0B;CAC3C,MAAM,MAAM,KAAK,QAAQ,SAAS,CAAC,MAAM,EAAE;AAC3C,KAAI,QAAQ,QACV,QAAO;AAET,KAAI,QAAQ,KACV,QAAO;AAET,QAAO;;AAGT,eAAe,0BAEb;CACA,MAAM,WAAW,MAAM,OAAO;CAC9B,MAAM,gBAAgB,MAAM,OAAO;AAEnC,QAAO,OAAO,QAAgB,aAAsC;EAClE,MAAM,SAAS,UAAU,SAAS;EAElC,MAAM,iBAAiB,MAAM,SAAS,cAAc,SAAS;AAC7D,SAAO,SAAS,OAAO,QAAQ;GAC7B,GAAG;GACH;GACA;GACA,SAAS,CAAC,cAAc;GACzB,CAAC;;;AAQN,eAAe,YAA6B;AAC1C,QAAO,IAAI,SAAS,WAAS,WAAW;EACtC,IAAI,OAAO;EACX,IAAI,UAAU;AACd,UAAQ,MAAM,YAAY,QAAQ;EAElC,MAAM,gBAAgB;AACpB,WAAQ,MAAM,eAAe,QAAQ,OAAO;AAC5C,WAAQ,MAAM,eAAe,OAAO,MAAM;AAC1C,WAAQ,MAAM,eAAe,SAAS,QAAQ;AAC9C,WAAQ,MAAM,eAAe,SAAS,QAAQ;;EAGhD,MAAM,UAAU,OAAmB;AACjC,OAAI,CAAC,SAAS;AACZ,cAAU;AACV,aAAS;AACT,QAAI;;;EAIR,MAAM,UAAU,UAAkB;AAChC,WAAQ;;EAGV,MAAM,cAAc;AAClB,gBAAaC,UAAQ,KAAK,CAAC;;EAG7B,MAAM,WAAW,UAAiB;AAChC,gBAAa,OAAO,MAAM,CAAC;;EAG7B,MAAM,gBAAgB;AAEpB,gBAAaA,UAAQ,KAAK,CAAC;;AAI7B,UAAQ,MAAM,GAAG,QAAQ,OAAO;AAEhC,UAAQ,MAAM,KAAK,OAAO,MAAM;AAChC,UAAQ,MAAM,KAAK,SAAS,QAAQ;AACpC,UAAQ,MAAM,KAAK,SAAS,QAAQ;GACpC;;AAGJ,eAAe,aAAa,KAAoC;CAC9D,MAAM,EAAE,SAAS,SAAS;CAC1B,MAAM,YAAY,QAAQ;CAC1B,MAAM,YAAY,QAAQ;CAC1B,MAAM,WAAW,QAAQ;CAEzB,MAAM,aADe,QAAQ,gBAA2B,YAC1B,MAAM,IAAI,CAAC,KAAK,MAAM,EAAE,MAAM,CAAC;AAG7D,KAAI,UAAU;EACZ,MAAM,UAAU,MAAM,WAAW;EACjC,MAAM,YAAY,iBAAiB;EACnC,MAAM,YAAY,MAAM,yBAAyB;EAKjD,MAAMC,WAAS,MAAM,UAAU,WADE,CAAC;GAAE,MADnB,KAAK,MAAM;GACwB;GAAS,CAAC,EACb,EAAE,WAAW,CAAC;EAE/D,MAAM,aAAaA,SAAO,YAAY;AACtC,MAAI,WAEF,SAAQ,OAAO,MAAM,WAAW,UAAU;AAI5C,MAAIA,SAAO,mBAAmB,EAC5B,SAAQ,KAAK,EAAE;AAEjB;;CAIF,MAAM,YAAY,MAAM,mBADJ,KAAK,SAAS,IAAI,OAAO,CAAC,IAAI,EACM,UAAU;AAElE,KAAI,UAAU,WAAW,GAAG;EAC1B,MAAM,eAAe,UAAU,KAAK,KAAK;AACzC,UAAQ,IAAI,OAAO,aAAa,eAAe;AAC/C,UAAQ,KAAK,EAAE;;CAIjB,MAAMC,QAA2B,MAAM,QAAQ,IAC7C,UAAU,IAAI,OAAO,UAAU;EAC7B;EACA,SAAS,MAAM,GAAG,SAAS,MAAM,QAAQ;EAC1C,EAAE,CACJ;CAOD,MAAMC,SAAuB,MAAM,UAJjB,iBAAiB,EAIqB,OAAO,EAAE,WAH/C,MAAM,yBAAyB,EAG2B,CAAC;CAG7E,IAAI,aAAa;AAEjB,MAAK,MAAM,cAAc,OAAO,aAAa;EAC3C,MAAM,UAAU,aAAa,WAAW,KAAK;AAE7C,MAAI,WAAW,gBAEb,SAAQ,IAAI,GAAG,KAAK,GAAG,IAAI,IAAI,GAAG,IAAI,UAAU,CAAC;WACxC,UACT,KAAI,WAAW,UAEb,SAAQ,IAAI,GAAG,KAAK,GAAG,IAAI,IAAI,GAAG,IAAI,UAAU,CAAC;MAGjD,SAAQ,IAAI,GAAG,MAAM,IAAI,GAAG,IAAI,UAAU;WAEnC,UACT,KAAI,WAAW,WAAW;AACxB,SAAM,GAAG,UAAU,WAAW,MAAM,WAAW,WAAW,QAAQ;AAElE,WAAQ,IAAI,GAAG,KAAK,GAAG,MAAM,IAAI,GAAG,IAAI,UAAU,CAAC;AACnD;QAGA,SAAQ,IAAI,GAAG,MAAM,IAAI,GAAG,IAAI,UAAU;WAIxC,WAAW,UACb,SAAQ,IAAI,GAAG,OAAO,IAAI,GAAG,IAAI,QAAQ,qBAAqB;MAE9D,SAAQ,IAAI,GAAG,MAAM,IAAI,GAAG,IAAI,UAAU;;CAMhD,MAAM,kBAAkB,OAAO,YAAY,QAAQ,MAAM,EAAE,gBAAgB;AAC3E,KAAI,gBAAgB,SAAS,GAAG;AAC9B,UAAQ,KAAK;AACb,OAAK,MAAM,cAAc,iBAAiB;AACxC,WAAQ,KAAK;AACb,WAAQ,IAAI,GAAG,UAAU,aAAa,WAAW,KAAK,CAAC,CAAC;AACxD,QAAK,MAAM,SAAS,WAAW,aAC7B,SAAQ,IAAI,kBAAkB,MAAM,CAAC;;;AAM3C,KAAI,OAAO,iBAAiB,KAAK,WAAW;AAC1C,UAAQ,KAAK;AACb,MAAI,UAEF,KADoB,OAAO,eAAe,OAAO,mBAC/B,GAAG;GACnB,MAAMC,QAAkB,EAAE;AAC1B,OAAI,OAAO,mBAAmB,EAC5B,OAAM,KACJ,GAAG,IACD,GAAG,OAAO,iBAAiB,OAAO,OAAO,qBAAqB,IAAI,MAAM,GAAG,qBAC5E,CACF;AAEH,OAAI,OAAO,eAAe,EACxB,OAAM,KACJ,GAAG,OACD,GAAG,OAAO,aAAa,OAAO,OAAO,iBAAiB,IAAI,MAAM,GAAG,OAAO,OAAO,iBAAiB,IAAI,MAAM,GAAG,aAChH,CACF;AAEH,WAAQ,IAAI,MAAM,KAAK,KAAK,CAAC;AAC7B,WAAQ,KAAK,EAAE;QAEf,SAAQ,IAAI,GAAG,MAAM,OAAO,OAAO,eAAe,+BAA+B,CAAC;WAE3E,UACT,SAAQ,IAAI,aAAa,WAAW,OAAO,eAAe,IAAI,MAAM,KAAK;;AAI7E,KAAI,OAAO,mBAAmB,EAC5B,SAAQ,KAAK,EAAE;;AAInB,MAAaC,gBAA4B;CACvC,MAAM;CACN,aAAa;CACb,MAAM;EACJ,MAAM;EACN,aAAa;EACb,UAAU;EACV,UAAU;EACX;CACD,SAAS;EACP,OAAO;GACL,MAAM;GACN,OAAO;GACP,aAAa;GACb,SAAS;GACV;EACD,OAAO;GACL,MAAM;GACN,OAAO;GACP,aAAa;GACb,SAAS;GACV;EACD,OAAO;GACL,MAAM;GACN,aAAa;GACb,SAAS;GACV;EACD,aAAa;GACX,MAAM;GACN,aAAa;GACb,SAAS;GACV;EACF;CACD,QAAQ;CACT"}