chiltepin 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/dist/bin.js +3582 -0
- package/dist/bin.js.map +1 -0
- package/package.json +93 -0
- package/templates/chiltepin.config.json +5 -0
- package/templates/demo.md +2161 -0
- package/templates/docs/getting-started.md +155 -0
- package/templates/docs/tutorial.md +559 -0
- package/templates/skill/SKILL.md +172 -0
- package/templates/skill/reference/blocks/INDEX.md +141 -0
- package/templates/skill/reference/blocks/agentic.md +63 -0
- package/templates/skill/reference/blocks/algorithms.md +49 -0
- package/templates/skill/reference/blocks/api.md +40 -0
- package/templates/skill/reference/blocks/architecture.md +94 -0
- package/templates/skill/reference/blocks/business.md +70 -0
- package/templates/skill/reference/blocks/charts-overviews.md +74 -0
- package/templates/skill/reference/blocks/data-model.md +34 -0
- package/templates/skill/reference/blocks/design-system.md +50 -0
- package/templates/skill/reference/blocks/flows.md +74 -0
- package/templates/skill/reference/blocks/narrative.md +65 -0
- package/templates/skill/reference/blocks/planning.md +74 -0
- package/templates/skill/reference/blocks/quality.md +43 -0
- package/templates/skill/reference/blocks/tables-data.md +55 -0
- package/templates/skill/reference/check.md +62 -0
- package/templates/skill/reference/decks.md +198 -0
- package/templates/skill/reference/exemplars/adr.md +87 -0
- package/templates/skill/reference/exemplars/agent-system.md +113 -0
- package/templates/skill/reference/exemplars/api-reference.md +110 -0
- package/templates/skill/reference/exemplars/backend-arch.md +117 -0
- package/templates/skill/reference/exemplars/data-pipeline.md +107 -0
- package/templates/skill/reference/exemplars/frontend-arch.md +93 -0
- package/templates/skill/reference/exemplars/incident-postmortem.md +93 -0
- package/templates/skill/reference/exemplars/migration-plan.md +95 -0
- package/templates/skill/reference/exemplars/onboarding.md +78 -0
- package/templates/skill/reference/exemplars/product-spec.md +81 -0
- package/templates/skill/reference/intake.md +140 -0
- package/templates/skill/reference/mermaid.md +216 -0
- package/templates/skill/reference/organizing.md +118 -0
- package/templates/skill/reference/patterns-design.md +59 -0
- package/templates/skill/reference/patterns.md +167 -0
- package/templates/skill/reference/recipes.md +153 -0
- package/templates/skill/reference/style-ste.md +119 -0
- package/templates/skill/reference/system-design.md +161 -0
- package/templates/skill/reference/writing.md +132 -0
package/dist/bin.js
ADDED
|
@@ -0,0 +1,3582 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import pc5 from 'picocolors';
|
|
4
|
+
import { useState } from 'react';
|
|
5
|
+
import { render, Box, Text, useApp, useInput } from 'ink';
|
|
6
|
+
import { existsSync, readFileSync, statSync, watch, readdirSync } from 'fs';
|
|
7
|
+
import { resolve, dirname, join, parse, relative, sep, basename, posix, extname } from 'path';
|
|
8
|
+
import { parse as parse$1 } from 'yaml';
|
|
9
|
+
import { createJiti } from 'jiti';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
import { PROSE_CHECK_CODES, DOC_TEMPLATE_INFO, BLOCK_FAMILIES, familyBlocks, isBlockFamily, parseDocument, validateDocument, lintProse, lintDensity, resolveRefs, helpUrl, isDocTemplate, BLOCK_TYPES, BLOCK_ALIASES, DOC_TEMPLATES, BLOCK_DESCRIPTIONS, parseOpenApi, openapiToMarkdown, suggestCsvImport, csvToStatustable, csvToChart, csvToTable, blockContract, formatBlockContract, BLOCK_TEMPLATES, convertSqlDdl, convertDbml, convertPrisma, erdFence, BLOCK_FAMILY } from 'chiltepin-core';
|
|
12
|
+
import { readFile, mkdir, writeFile, cp, realpath, stat, unlink, rmdir, readdir, rename } from 'fs/promises';
|
|
13
|
+
import fg from 'fast-glob';
|
|
14
|
+
import { createHash, randomBytes } from 'crypto';
|
|
15
|
+
import { tmpdir } from 'os';
|
|
16
|
+
import open from 'open';
|
|
17
|
+
import { renderDocument, toSlides, houseCss, buildThemeVars, DEFAULT_THEME, escapeHtml, renderDocumentParts, schemeMarkup, FAVICON_LINK, htmlRenderers } from 'chiltepin-render';
|
|
18
|
+
import { spawnSync, spawn } from 'child_process';
|
|
19
|
+
import { createRequire } from 'module';
|
|
20
|
+
import SelectInput2 from 'ink-select-input';
|
|
21
|
+
import { jsx, jsxs } from 'react/jsx-runtime';
|
|
22
|
+
import { createServer } from 'http';
|
|
23
|
+
import cfonts from 'cfonts';
|
|
24
|
+
|
|
25
|
+
var DEFAULTS = {
|
|
26
|
+
docsDir: "docs",
|
|
27
|
+
outDir: "dist",
|
|
28
|
+
richIndex: true,
|
|
29
|
+
colorScheme: "dark"
|
|
30
|
+
};
|
|
31
|
+
var CONFIG_EXTENSIONS = ["ts", "js", "mjs", "json", "yml", "yaml"];
|
|
32
|
+
var CONFIG_FILES = [
|
|
33
|
+
...CONFIG_EXTENSIONS.map((ext) => `chiltepin.config.${ext}`),
|
|
34
|
+
...CONFIG_EXTENSIONS.map((ext) => `avodado.config.${ext}`)
|
|
35
|
+
];
|
|
36
|
+
var isLegacyConfigName = (name) => name.startsWith("avodado.config.");
|
|
37
|
+
var warnedLegacy = false;
|
|
38
|
+
function findConfig(cwd) {
|
|
39
|
+
return CONFIG_FILES.find((name) => existsSync(resolve(cwd, name)));
|
|
40
|
+
}
|
|
41
|
+
async function loadConfig(cwd) {
|
|
42
|
+
for (const name of CONFIG_FILES) {
|
|
43
|
+
const path = resolve(cwd, name);
|
|
44
|
+
if (!existsSync(path)) continue;
|
|
45
|
+
if (isLegacyConfigName(name) && !warnedLegacy) {
|
|
46
|
+
warnedLegacy = true;
|
|
47
|
+
process.stderr.write(
|
|
48
|
+
`${name} is the old name \u2014 rename it to ${name.replace("avodado", "chiltepin")}. It still loads for now.
|
|
49
|
+
`
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
const raw = await readConfig(path);
|
|
53
|
+
return mergeWithDefaults(raw);
|
|
54
|
+
}
|
|
55
|
+
return DEFAULTS;
|
|
56
|
+
}
|
|
57
|
+
async function readConfig(path) {
|
|
58
|
+
if (path.endsWith(".json")) {
|
|
59
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
60
|
+
}
|
|
61
|
+
if (path.endsWith(".yml") || path.endsWith(".yaml")) {
|
|
62
|
+
return parse$1(readFileSync(path, "utf8"));
|
|
63
|
+
}
|
|
64
|
+
const jiti = createJiti(import.meta.url);
|
|
65
|
+
const mod = await jiti.import(path);
|
|
66
|
+
return mod.default ?? mod;
|
|
67
|
+
}
|
|
68
|
+
function mergeWithDefaults(raw) {
|
|
69
|
+
if (raw === null || typeof raw !== "object") return DEFAULTS;
|
|
70
|
+
const r = raw;
|
|
71
|
+
const scheme = r.colorScheme === "light" || r.colorScheme === "system" || r.colorScheme === "dark" ? r.colorScheme : DEFAULTS.colorScheme;
|
|
72
|
+
return {
|
|
73
|
+
docsDir: typeof r.docsDir === "string" ? r.docsDir : DEFAULTS.docsDir,
|
|
74
|
+
outDir: typeof r.outDir === "string" ? r.outDir : DEFAULTS.outDir,
|
|
75
|
+
richIndex: typeof r.richIndex === "boolean" ? r.richIndex : DEFAULTS.richIndex,
|
|
76
|
+
colorScheme: scheme
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function cliVersion() {
|
|
80
|
+
try {
|
|
81
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
82
|
+
for (let i = 0; i < 6; i++) {
|
|
83
|
+
const p = join(dir, "package.json");
|
|
84
|
+
if (existsSync(p)) {
|
|
85
|
+
const j = JSON.parse(readFileSync(p, "utf8"));
|
|
86
|
+
if ((j.name === "chiltepin" || j.name === "avodado" || j.name === "@avodado/cli") && typeof j.version === "string") {
|
|
87
|
+
return j.version;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
const parent = dirname(dir);
|
|
91
|
+
if (parent === dir) break;
|
|
92
|
+
dir = parent;
|
|
93
|
+
}
|
|
94
|
+
} catch {
|
|
95
|
+
}
|
|
96
|
+
return "0.0.0";
|
|
97
|
+
}
|
|
98
|
+
async function loadDocs(patterns, cwd, docsRoot) {
|
|
99
|
+
const matches = await fg(patterns, {
|
|
100
|
+
cwd,
|
|
101
|
+
absolute: true,
|
|
102
|
+
onlyFiles: true,
|
|
103
|
+
dot: false,
|
|
104
|
+
followSymbolicLinks: false
|
|
105
|
+
});
|
|
106
|
+
const docsRootAbs = resolve(cwd, docsRoot);
|
|
107
|
+
const seen = /* @__PURE__ */ new Set();
|
|
108
|
+
const unique = [];
|
|
109
|
+
for (const absolute of matches) {
|
|
110
|
+
let real;
|
|
111
|
+
try {
|
|
112
|
+
real = await realpath(absolute);
|
|
113
|
+
} catch {
|
|
114
|
+
real = absolute;
|
|
115
|
+
}
|
|
116
|
+
if (seen.has(real)) continue;
|
|
117
|
+
seen.add(real);
|
|
118
|
+
unique.push(absolute);
|
|
119
|
+
}
|
|
120
|
+
const files = await Promise.all(
|
|
121
|
+
unique.map(async (absolute) => {
|
|
122
|
+
const file = relative(cwd, absolute);
|
|
123
|
+
const slug = deriveSlug(absolute, docsRootAbs);
|
|
124
|
+
const decoded = decodeUtf8(await readFile(absolute));
|
|
125
|
+
return { absolute, file, slug, ...decoded };
|
|
126
|
+
})
|
|
127
|
+
);
|
|
128
|
+
files.sort((a, b) => a.file.localeCompare(b.file));
|
|
129
|
+
return files;
|
|
130
|
+
}
|
|
131
|
+
function encodingDiagnostics(docs) {
|
|
132
|
+
const out = [];
|
|
133
|
+
for (const d of docs) {
|
|
134
|
+
if (d.encodingError === void 0) continue;
|
|
135
|
+
out.push({
|
|
136
|
+
file: d.file,
|
|
137
|
+
...d.encodingLine !== void 0 ? { line: d.encodingLine } : {},
|
|
138
|
+
level: "error",
|
|
139
|
+
code: "E_ENCODING",
|
|
140
|
+
message: d.encodingError,
|
|
141
|
+
hint: "Re-save the file as UTF-8 (no byte order mark)."
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return out;
|
|
145
|
+
}
|
|
146
|
+
var BOMS = [
|
|
147
|
+
{ bytes: [255, 254, 0, 0], label: "UTF-32 LE" },
|
|
148
|
+
{ bytes: [0, 0, 254, 255], label: "UTF-32 BE" },
|
|
149
|
+
{ bytes: [255, 254], label: "UTF-16 LE" },
|
|
150
|
+
{ bytes: [254, 255], label: "UTF-16 BE" }
|
|
151
|
+
];
|
|
152
|
+
var startsWith = (buf, bytes) => buf.length >= bytes.length && bytes.every((b, i) => buf[i] === b);
|
|
153
|
+
function decodeUtf8(buf) {
|
|
154
|
+
for (const bom of BOMS) {
|
|
155
|
+
if (startsWith(buf, bom.bytes)) {
|
|
156
|
+
return {
|
|
157
|
+
source: "",
|
|
158
|
+
encodingError: `The file is not UTF-8: it starts with a ${bom.label} byte order mark.`,
|
|
159
|
+
encodingLine: 1
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const body = startsWith(buf, [239, 187, 191]) ? buf.subarray(3) : buf;
|
|
164
|
+
const bad = firstInvalidUtf8(body);
|
|
165
|
+
if (bad !== void 0) {
|
|
166
|
+
let line = 1;
|
|
167
|
+
for (let i = 0; i < bad; i += 1) if (body[i] === 10) line += 1;
|
|
168
|
+
return {
|
|
169
|
+
source: "",
|
|
170
|
+
encodingError: `The file is not UTF-8: byte ${bad} is not part of a valid UTF-8 sequence.`,
|
|
171
|
+
encodingLine: line
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return { source: body.toString("utf8") };
|
|
175
|
+
}
|
|
176
|
+
function firstInvalidUtf8(buf) {
|
|
177
|
+
const len = buf.length;
|
|
178
|
+
let i = 0;
|
|
179
|
+
while (i < len) {
|
|
180
|
+
const b = buf[i];
|
|
181
|
+
if (b < 128) {
|
|
182
|
+
i += 1;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
let need;
|
|
186
|
+
let cp2;
|
|
187
|
+
if (b >= 194 && b <= 223) {
|
|
188
|
+
need = 1;
|
|
189
|
+
cp2 = b & 31;
|
|
190
|
+
} else if (b >= 224 && b <= 239) {
|
|
191
|
+
need = 2;
|
|
192
|
+
cp2 = b & 15;
|
|
193
|
+
} else if (b >= 240 && b <= 244) {
|
|
194
|
+
need = 3;
|
|
195
|
+
cp2 = b & 7;
|
|
196
|
+
} else {
|
|
197
|
+
return i;
|
|
198
|
+
}
|
|
199
|
+
if (i + need > len - 1) return i;
|
|
200
|
+
for (let k = 1; k <= need; k += 1) {
|
|
201
|
+
const c = buf[i + k];
|
|
202
|
+
if ((c & 192) !== 128) return i;
|
|
203
|
+
cp2 = cp2 << 6 | c & 63;
|
|
204
|
+
}
|
|
205
|
+
if (need === 2 && (cp2 < 2048 || cp2 >= 55296 && cp2 <= 57343)) return i;
|
|
206
|
+
if (need === 3 && (cp2 < 65536 || cp2 > 1114111)) return i;
|
|
207
|
+
i += need + 1;
|
|
208
|
+
}
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
function deriveSlug(absolute, docsRootAbs) {
|
|
212
|
+
const rel2 = relative(docsRootAbs, absolute);
|
|
213
|
+
const inside2 = !rel2.startsWith("..") && !rel2.startsWith(sep) && rel2.length > 0;
|
|
214
|
+
const path = inside2 ? rel2 : basename(absolute);
|
|
215
|
+
return path.replace(/\\/g, "/").replace(/\.md$/i, "");
|
|
216
|
+
}
|
|
217
|
+
var KEBAB_FILE = /^[a-z0-9]+(-[a-z0-9]+)*\.md$/;
|
|
218
|
+
var MAX_DEPTH = 2;
|
|
219
|
+
function kebabSuggestion(filename) {
|
|
220
|
+
const stem = filename.replace(/\.md$/i, "");
|
|
221
|
+
const slug = stem.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
222
|
+
return `${slug.length > 0 ? slug : "doc"}.md`;
|
|
223
|
+
}
|
|
224
|
+
function lintConventions(file, absolute, cwd, docsRoot) {
|
|
225
|
+
const rel2 = relative(resolve(cwd, docsRoot), absolute);
|
|
226
|
+
const inside2 = rel2.length > 0 && !rel2.startsWith("..") && !rel2.startsWith(sep);
|
|
227
|
+
if (!inside2) return [];
|
|
228
|
+
const diagnostics = [];
|
|
229
|
+
const name = basename(absolute);
|
|
230
|
+
if (!KEBAB_FILE.test(name)) {
|
|
231
|
+
diagnostics.push({
|
|
232
|
+
file,
|
|
233
|
+
level: "warn",
|
|
234
|
+
code: "W_DOC_CONVENTION",
|
|
235
|
+
message: `The file name "${name}" is not kebab-case. Doc names use lowercase a-z, 0-9, and hyphens. The name is the reference prefix, so refs to this doc inherit it.`,
|
|
236
|
+
value: name,
|
|
237
|
+
hint: `Rename the file to kebab-case: ${kebabSuggestion(name)}.`
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
const depth = rel2.split(sep).length;
|
|
241
|
+
if (depth > MAX_DEPTH) {
|
|
242
|
+
diagnostics.push({
|
|
243
|
+
file,
|
|
244
|
+
level: "warn",
|
|
245
|
+
code: "W_DOC_CONVENTION",
|
|
246
|
+
message: `The file is ${depth} levels below ${docsRoot}/. The convention permits one group level: ${docsRoot}/<area>/<doc>.md.`,
|
|
247
|
+
value: rel2.split(sep).join("/"),
|
|
248
|
+
hint: `Move the file to ${docsRoot}/<area>/${name}, then update refs to its old slug.`
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
return diagnostics;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/commands/check.tsx
|
|
255
|
+
var PROSE_CODES = new Set(PROSE_CHECK_CODES);
|
|
256
|
+
async function runCheck(opts) {
|
|
257
|
+
const docs = await loadDocs(opts.patterns, opts.cwd, opts.docsRoot);
|
|
258
|
+
const parsed = docs.filter((d) => d.encodingError === void 0).map((d) => ({
|
|
259
|
+
doc: parseDocument(d.source, d.slug),
|
|
260
|
+
file: d.file
|
|
261
|
+
}));
|
|
262
|
+
const diagnostics = [...encodingDiagnostics(docs)];
|
|
263
|
+
for (const { doc, file } of parsed) {
|
|
264
|
+
diagnostics.push(...validateDocument(doc, file));
|
|
265
|
+
diagnostics.push(...lintProse(doc, file));
|
|
266
|
+
diagnostics.push(...lintDensity(doc, file));
|
|
267
|
+
}
|
|
268
|
+
for (const d of docs) {
|
|
269
|
+
diagnostics.push(...lintConventions(d.file, d.absolute, opts.cwd, opts.docsRoot));
|
|
270
|
+
}
|
|
271
|
+
const resolved = resolveRefs(parsed);
|
|
272
|
+
diagnostics.push(...resolved.diagnostics);
|
|
273
|
+
const escalated = opts.strictProse === true ? diagnostics.map(
|
|
274
|
+
(d) => d.level === "warn" && PROSE_CODES.has(d.code) ? { ...d, level: "error" } : d
|
|
275
|
+
) : diagnostics;
|
|
276
|
+
escalated.sort((a, b) => {
|
|
277
|
+
const f = a.file.localeCompare(b.file);
|
|
278
|
+
if (f !== 0) return f;
|
|
279
|
+
return (a.line ?? 0) - (b.line ?? 0);
|
|
280
|
+
});
|
|
281
|
+
const exitCode = escalated.some((d) => d.level === "error") ? 1 : 0;
|
|
282
|
+
const sources = /* @__PURE__ */ new Map();
|
|
283
|
+
for (const d of docs) {
|
|
284
|
+
if (d.encodingError !== void 0) continue;
|
|
285
|
+
sources.set(d.file, d.source.split(/\r\n|\r|\n/));
|
|
286
|
+
}
|
|
287
|
+
return {
|
|
288
|
+
diagnostics: escalated,
|
|
289
|
+
files: docs.map((d) => d.file),
|
|
290
|
+
sources,
|
|
291
|
+
exitCode
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
var require2 = createRequire(import.meta.url);
|
|
295
|
+
function isMissingBrowserError(message) {
|
|
296
|
+
return /Executable doesn't exist|playwright install|just installed or updated/i.test(message);
|
|
297
|
+
}
|
|
298
|
+
function playwrightCliPath() {
|
|
299
|
+
try {
|
|
300
|
+
return join(dirname(require2.resolve("playwright/package.json")), "cli.js");
|
|
301
|
+
} catch {
|
|
302
|
+
return void 0;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
async function installChromium(log = () => {
|
|
306
|
+
}) {
|
|
307
|
+
const cli = playwrightCliPath();
|
|
308
|
+
if (cli === void 0) {
|
|
309
|
+
throw new Error("Could not locate the Playwright CLI. Run `npx playwright install chromium`.");
|
|
310
|
+
}
|
|
311
|
+
log("Downloading Chromium for PDF export (one-time, ~100 MB)\u2026");
|
|
312
|
+
await new Promise((resolvePromise, rejectPromise) => {
|
|
313
|
+
const child = spawn(process.execPath, [cli, "install", "chromium"], {
|
|
314
|
+
stdio: ["ignore", "inherit", "inherit"]
|
|
315
|
+
});
|
|
316
|
+
child.on("error", rejectPromise);
|
|
317
|
+
child.on(
|
|
318
|
+
"exit",
|
|
319
|
+
(code2) => code2 === 0 ? resolvePromise() : rejectPromise(new Error(`playwright install exited with code ${String(code2)}`))
|
|
320
|
+
);
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
async function toPdf(input, opts = {}) {
|
|
324
|
+
const html = typeof input === "string" ? input : renderDocument(input);
|
|
325
|
+
const pw = await loadPlaywright();
|
|
326
|
+
const browser = await launchChromium(pw, opts);
|
|
327
|
+
try {
|
|
328
|
+
const page = await browser.newPage();
|
|
329
|
+
await page.setContent(html, { waitUntil: "networkidle" });
|
|
330
|
+
const buffer = await page.pdf({
|
|
331
|
+
// A custom pixel width wins over the named format (Playwright ignores
|
|
332
|
+
// `format` when width/height are given): portrait A-series proportions
|
|
333
|
+
// keep the page shape familiar at every preset.
|
|
334
|
+
...opts.pageWidthPx !== void 0 ? {
|
|
335
|
+
width: `${String(opts.pageWidthPx)}px`,
|
|
336
|
+
height: `${String(Math.round(opts.pageWidthPx * Math.SQRT2))}px`
|
|
337
|
+
} : { format: opts.format ?? "A4" },
|
|
338
|
+
printBackground: true,
|
|
339
|
+
...opts.margin !== void 0 ? { margin: opts.margin } : {}
|
|
340
|
+
});
|
|
341
|
+
return new Uint8Array(buffer);
|
|
342
|
+
} finally {
|
|
343
|
+
await browser.close();
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
async function launchChromium(pw, opts) {
|
|
347
|
+
try {
|
|
348
|
+
return await pw.chromium.launch({ headless: true });
|
|
349
|
+
} catch (err) {
|
|
350
|
+
const message = err.message;
|
|
351
|
+
if (!isMissingBrowserError(message)) throw err;
|
|
352
|
+
if (opts.autoInstallBrowser === true) {
|
|
353
|
+
await installChromium(opts.log);
|
|
354
|
+
return await pw.chromium.launch({ headless: true });
|
|
355
|
+
}
|
|
356
|
+
const cli = playwrightCliPath();
|
|
357
|
+
const cmd = cli !== void 0 ? `node "${cli}" install chromium` : "npx playwright install chromium";
|
|
358
|
+
throw new Error(
|
|
359
|
+
`Chromium isn't installed for PDF export. Install it once with:
|
|
360
|
+
${cmd}
|
|
361
|
+
Original error: ${message}`
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
async function loadPlaywright() {
|
|
366
|
+
try {
|
|
367
|
+
return await import('playwright');
|
|
368
|
+
} catch (err) {
|
|
369
|
+
throw new Error(
|
|
370
|
+
`Playwright is required for PDF export. Run: pnpm add playwright && npx playwright install chromium
|
|
371
|
+
Original error: ${err.message}`
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function overwriteRefusal(abs, guard = {}) {
|
|
376
|
+
if (guard.force === true) return void 0;
|
|
377
|
+
if (!existsSync(abs)) return void 0;
|
|
378
|
+
const lower = abs.toLowerCase();
|
|
379
|
+
const regenerates = guard.regenerates ?? [];
|
|
380
|
+
if (regenerates.some((ext) => lower.endsWith(ext.toLowerCase()))) return void 0;
|
|
381
|
+
const flag = guard.flag ?? "--force";
|
|
382
|
+
return `${abs} already exists \u2014 refusing to overwrite it. Pass ${flag} to replace it, or write to another path.`;
|
|
383
|
+
}
|
|
384
|
+
var OverwriteRefusedError = class extends Error {
|
|
385
|
+
name = "OverwriteRefusedError";
|
|
386
|
+
};
|
|
387
|
+
function assertWritable(abs, guard = {}) {
|
|
388
|
+
const refusal = overwriteRefusal(abs, guard);
|
|
389
|
+
if (refusal !== void 0) throw new OverwriteRefusedError(refusal);
|
|
390
|
+
}
|
|
391
|
+
async function writeFileSafe(abs, data, guard = {}) {
|
|
392
|
+
assertWritable(abs, guard);
|
|
393
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
394
|
+
await writeFile(abs, data);
|
|
395
|
+
}
|
|
396
|
+
function errorMessage(err) {
|
|
397
|
+
if (err instanceof Error) return err.message;
|
|
398
|
+
return String(err);
|
|
399
|
+
}
|
|
400
|
+
var renderers = htmlRenderers;
|
|
401
|
+
function failingBlock(doc) {
|
|
402
|
+
for (const seg of doc.segments) {
|
|
403
|
+
if (seg.kind === "markdown") continue;
|
|
404
|
+
if (seg.data === void 0) continue;
|
|
405
|
+
const render = renderers[seg.kind];
|
|
406
|
+
if (render === void 0) continue;
|
|
407
|
+
try {
|
|
408
|
+
render(seg.data);
|
|
409
|
+
} catch {
|
|
410
|
+
return seg;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return void 0;
|
|
414
|
+
}
|
|
415
|
+
function renderFailure(doc, file, what, err) {
|
|
416
|
+
const reason = errorMessage(err);
|
|
417
|
+
const block = failingBlock(doc);
|
|
418
|
+
if (block === void 0) {
|
|
419
|
+
return {
|
|
420
|
+
file,
|
|
421
|
+
level: "error",
|
|
422
|
+
code: "E_RENDER",
|
|
423
|
+
message: `The ${what} could not be rendered: ${reason}`,
|
|
424
|
+
hint: "No single block reproduces the failure \u2014 the whole document does."
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const label = block.sourceType ?? block.kind;
|
|
428
|
+
return {
|
|
429
|
+
file,
|
|
430
|
+
line: block.line,
|
|
431
|
+
level: "error",
|
|
432
|
+
code: "E_RENDER",
|
|
433
|
+
message: `The ${what} could not be rendered: the \`${label}\` block failed \u2014 ${reason}`,
|
|
434
|
+
...block.id !== void 0 ? { value: block.id } : {},
|
|
435
|
+
hint: "Fix the values in this block, or remove it and re-run `chiltepin build`."
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
function guardRender(doc, file, what, render) {
|
|
439
|
+
try {
|
|
440
|
+
return { ok: true, value: render() };
|
|
441
|
+
} catch (err) {
|
|
442
|
+
return { ok: false, diagnostic: renderFailure(doc, file, what, err) };
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// src/commands/single.ts
|
|
447
|
+
var SIZE_WIDTHS = {
|
|
448
|
+
sm: 720,
|
|
449
|
+
md: 960,
|
|
450
|
+
lg: 1280,
|
|
451
|
+
xl: 1600
|
|
452
|
+
};
|
|
453
|
+
function parseExportSize(value) {
|
|
454
|
+
return value === "sm" || value === "md" || value === "lg" || value === "xl" ? value : void 0;
|
|
455
|
+
}
|
|
456
|
+
var EXT = {
|
|
457
|
+
html: "html",
|
|
458
|
+
slides: "slides.html",
|
|
459
|
+
pdf: "pdf"
|
|
460
|
+
};
|
|
461
|
+
var REGENERATES = {
|
|
462
|
+
html: [".html", ".htm"],
|
|
463
|
+
slides: [".html", ".htm"],
|
|
464
|
+
pdf: [".pdf"]
|
|
465
|
+
};
|
|
466
|
+
async function runSingle(opts) {
|
|
467
|
+
const inputAbs = resolve(opts.cwd, opts.input);
|
|
468
|
+
const source = await readFile(inputAbs, "utf8");
|
|
469
|
+
const slug = parse(inputAbs).name;
|
|
470
|
+
const doc = parseDocument(source, slug);
|
|
471
|
+
const sizePx = opts.size !== void 0 && (opts.format === "html" || opts.format === "pdf") ? SIZE_WIDTHS[opts.size] : void 0;
|
|
472
|
+
const config = await loadConfig(opts.cwd);
|
|
473
|
+
const themeOpts = {
|
|
474
|
+
colorScheme: config.colorScheme,
|
|
475
|
+
...sizePx !== void 0 ? { themeVars: { "--page-max": `${String(sizePx)}px` } } : {}
|
|
476
|
+
};
|
|
477
|
+
let outputAbs;
|
|
478
|
+
if (opts.preview === true) {
|
|
479
|
+
const hash = createHash("sha1").update(`${source}\0${opts.format}\0${JSON.stringify(themeOpts)}`).digest("hex").slice(0, 10);
|
|
480
|
+
const dir = join(tmpdir(), "chiltepin-preview");
|
|
481
|
+
await mkdir(dir, { recursive: true });
|
|
482
|
+
outputAbs = join(dir, `${slug}-${hash}.${EXT[opts.format]}`);
|
|
483
|
+
} else {
|
|
484
|
+
outputAbs = opts.output !== void 0 ? resolve(opts.cwd, opts.output) : inputAbs.replace(/\.md$/i, `.${EXT[opts.format]}`);
|
|
485
|
+
if (opts.output !== void 0) {
|
|
486
|
+
assertWritable(outputAbs, {
|
|
487
|
+
...opts.force === true ? { force: true } : {},
|
|
488
|
+
regenerates: REGENERATES[opts.format]
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
await mkdir(dirname(outputAbs), { recursive: true });
|
|
492
|
+
}
|
|
493
|
+
const named = async (what, produce) => {
|
|
494
|
+
try {
|
|
495
|
+
return await produce();
|
|
496
|
+
} catch (err) {
|
|
497
|
+
const d = renderFailure(doc, relative(opts.cwd, inputAbs) || opts.input, what, err);
|
|
498
|
+
const where = d.line !== void 0 ? `${d.file}:${d.line}` : d.file;
|
|
499
|
+
throw new Error(`${where} ${d.message}`, { cause: err });
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
let bytes;
|
|
503
|
+
if (opts.format === "pdf") {
|
|
504
|
+
const page = await named("PDF", () => renderDocument(doc, themeOpts));
|
|
505
|
+
const pdf = await toPdf(page, {
|
|
506
|
+
autoInstallBrowser: true,
|
|
507
|
+
log: (m) => console.error(m),
|
|
508
|
+
// With a size preset the PDF page itself takes the preset width (portrait
|
|
509
|
+
// A-series proportions); without one the page stays A4.
|
|
510
|
+
...sizePx !== void 0 ? { pageWidthPx: sizePx } : {}
|
|
511
|
+
});
|
|
512
|
+
await writeFile(outputAbs, pdf);
|
|
513
|
+
bytes = pdf.byteLength;
|
|
514
|
+
} else {
|
|
515
|
+
const html = await named(
|
|
516
|
+
opts.format === "slides" ? "slide deck" : "page",
|
|
517
|
+
() => opts.format === "slides" ? toSlides(doc, themeOpts) : renderDocument(doc, themeOpts)
|
|
518
|
+
);
|
|
519
|
+
await writeFile(outputAbs, html, "utf8");
|
|
520
|
+
bytes = html.length;
|
|
521
|
+
}
|
|
522
|
+
const doOpen = opts.preview === true && opts.open !== false;
|
|
523
|
+
if (doOpen) await open(outputAbs);
|
|
524
|
+
return { output: outputAbs, bytes, opened: doOpen };
|
|
525
|
+
}
|
|
526
|
+
var SKILL_REFERENCE_FILES = [
|
|
527
|
+
"reference/blocks/INDEX.md",
|
|
528
|
+
"reference/blocks/narrative.md",
|
|
529
|
+
"reference/blocks/tables-data.md",
|
|
530
|
+
"reference/blocks/api.md",
|
|
531
|
+
"reference/blocks/architecture.md",
|
|
532
|
+
"reference/blocks/flows.md",
|
|
533
|
+
"reference/blocks/data-model.md",
|
|
534
|
+
"reference/blocks/charts-overviews.md",
|
|
535
|
+
"reference/blocks/planning.md",
|
|
536
|
+
"reference/blocks/business.md",
|
|
537
|
+
"reference/blocks/design-system.md",
|
|
538
|
+
"reference/blocks/algorithms.md",
|
|
539
|
+
"reference/blocks/agentic.md",
|
|
540
|
+
"reference/blocks/quality.md",
|
|
541
|
+
"reference/recipes.md",
|
|
542
|
+
"reference/patterns.md",
|
|
543
|
+
"reference/patterns-design.md",
|
|
544
|
+
"reference/mermaid.md",
|
|
545
|
+
"reference/writing.md",
|
|
546
|
+
"reference/check.md",
|
|
547
|
+
"reference/system-design.md",
|
|
548
|
+
"reference/decks.md",
|
|
549
|
+
"reference/intake.md",
|
|
550
|
+
"reference/organizing.md",
|
|
551
|
+
"reference/style-ste.md"
|
|
552
|
+
];
|
|
553
|
+
var SKILL_FILES = ["SKILL.md", ...SKILL_REFERENCE_FILES];
|
|
554
|
+
var BASE_FILES = [
|
|
555
|
+
"chiltepin.config.json",
|
|
556
|
+
"docs/getting-started.md",
|
|
557
|
+
"docs/tutorial.md"
|
|
558
|
+
];
|
|
559
|
+
function templatesDir() {
|
|
560
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
561
|
+
for (let i = 0; i < 6; i++) {
|
|
562
|
+
const candidate = join(dir, "templates");
|
|
563
|
+
if (existsSync(candidate) && statSync(candidate).isDirectory()) return candidate;
|
|
564
|
+
const parent = dirname(dir);
|
|
565
|
+
if (parent === dir) break;
|
|
566
|
+
dir = parent;
|
|
567
|
+
}
|
|
568
|
+
throw new Error(`Could not locate chiltepin/cli templates directory near ${import.meta.url}`);
|
|
569
|
+
}
|
|
570
|
+
function skillDir() {
|
|
571
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
572
|
+
for (let i = 0; i < 8; i++) {
|
|
573
|
+
const candidate = join(dir, "skills", "chiltepin");
|
|
574
|
+
if (existsSync(join(candidate, "SKILL.md")) && existsSync(join(dir, "pnpm-workspace.yaml"))) {
|
|
575
|
+
return candidate;
|
|
576
|
+
}
|
|
577
|
+
const parent = dirname(dir);
|
|
578
|
+
if (parent === dir) break;
|
|
579
|
+
dir = parent;
|
|
580
|
+
}
|
|
581
|
+
const packaged = join(templatesDir(), "skill");
|
|
582
|
+
if (existsSync(join(packaged, "SKILL.md"))) return packaged;
|
|
583
|
+
throw new Error("Could not locate the Chiltepin skill (templates/skill or skills/chiltepin)");
|
|
584
|
+
}
|
|
585
|
+
async function stitchSkill(srcRoot = skillDir()) {
|
|
586
|
+
const parts = await Promise.all(SKILL_FILES.map((f) => readFile(resolve(srcRoot, f), "utf8")));
|
|
587
|
+
return (parts.map((p) => p.trimEnd()).join("\n\n---\n\n") + "\n").replaceAll(
|
|
588
|
+
"live beside this file \u2014 read them on demand",
|
|
589
|
+
"are included in full below \u2014 read them on demand"
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
async function runInit(opts) {
|
|
593
|
+
const srcRoot = templatesDir();
|
|
594
|
+
const created = [];
|
|
595
|
+
const skipped = [];
|
|
596
|
+
for (const rel2 of BASE_FILES) {
|
|
597
|
+
const dst = join(opts.cwd, rel2);
|
|
598
|
+
if (existsSync(dst) && opts.force !== true) {
|
|
599
|
+
skipped.push(rel2);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
602
|
+
await mkdir(dirname(dst), { recursive: true });
|
|
603
|
+
await cp(resolve(srcRoot, rel2), dst);
|
|
604
|
+
created.push(rel2);
|
|
605
|
+
}
|
|
606
|
+
return { created, skipped };
|
|
607
|
+
}
|
|
608
|
+
function filterDemoSource(source, family) {
|
|
609
|
+
const doc = parseDocument(source, "demo");
|
|
610
|
+
const label = BLOCK_FAMILIES.find((f) => f.id === family)?.label ?? family;
|
|
611
|
+
const out = [];
|
|
612
|
+
let pendingMarkdown;
|
|
613
|
+
for (const seg of doc.segments) {
|
|
614
|
+
if (seg.kind === "markdown") {
|
|
615
|
+
pendingMarkdown = seg.text;
|
|
616
|
+
continue;
|
|
617
|
+
}
|
|
618
|
+
const keep = seg.kind === "meta" || BLOCK_FAMILY[seg.kind] === family;
|
|
619
|
+
if (keep) {
|
|
620
|
+
if (seg.kind === "meta") {
|
|
621
|
+
out.push("```meta\n" + retagMeta(seg.raw, label) + "\n```");
|
|
622
|
+
} else {
|
|
623
|
+
if (pendingMarkdown !== void 0) out.push(pendingMarkdown.trim());
|
|
624
|
+
out.push("```" + seg.kind + "\n" + seg.raw + "\n```");
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
pendingMarkdown = void 0;
|
|
628
|
+
}
|
|
629
|
+
return out.join("\n\n") + "\n";
|
|
630
|
+
}
|
|
631
|
+
function retagMeta(raw, label) {
|
|
632
|
+
const tagged = `tag: DEMO \xB7 ${label.toUpperCase()}`;
|
|
633
|
+
if (/^tag:.*$/m.test(raw)) return raw.replace(/^tag:.*$/m, tagged);
|
|
634
|
+
return `${raw}
|
|
635
|
+
${tagged}`;
|
|
636
|
+
}
|
|
637
|
+
async function runDemo(opts) {
|
|
638
|
+
const format = opts.format ?? "html";
|
|
639
|
+
let source = await readFile(join(templatesDir(), "demo.md"), "utf8");
|
|
640
|
+
if (opts.family !== void 0) source = filterDemoSource(source, opts.family);
|
|
641
|
+
const dir = join(tmpdir(), "chiltepin-demo");
|
|
642
|
+
await mkdir(dir, { recursive: true });
|
|
643
|
+
const input = join(dir, opts.family === void 0 ? "demo.md" : `demo-${opts.family}.md`);
|
|
644
|
+
await writeFile(input, source, "utf8");
|
|
645
|
+
return runSingle({
|
|
646
|
+
cwd: dir,
|
|
647
|
+
input,
|
|
648
|
+
format,
|
|
649
|
+
...opts.output !== void 0 ? { output: opts.output } : { preview: opts.preview ?? true },
|
|
650
|
+
...opts.force === true ? { force: true } : {}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
var EVERYTHING = "__all__";
|
|
654
|
+
function DemoApp({ onPick }) {
|
|
655
|
+
const { exit } = useApp();
|
|
656
|
+
useInput((input, key) => {
|
|
657
|
+
if (input === "q" || key.escape) exit();
|
|
658
|
+
});
|
|
659
|
+
const items = [
|
|
660
|
+
{ label: `Everything \u2014 the full showcase, all ${BLOCK_TYPES.length} blocks`, value: EVERYTHING },
|
|
661
|
+
...BLOCK_FAMILIES.map((f) => ({
|
|
662
|
+
label: `${f.label} \u2014 ${familyBlocks(f.id).length} blocks`,
|
|
663
|
+
value: f.id
|
|
664
|
+
}))
|
|
665
|
+
];
|
|
666
|
+
function pick(value) {
|
|
667
|
+
onPick(value === EVERYTHING ? {} : { family: value });
|
|
668
|
+
exit();
|
|
669
|
+
}
|
|
670
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
671
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
672
|
+
"What do you want to see? ",
|
|
673
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(\u2191\u2193 move \xB7 enter to render \xB7 q to quit)" })
|
|
674
|
+
] }),
|
|
675
|
+
/* @__PURE__ */ jsx(SelectInput2, { items, onSelect: (item) => pick(item.value) })
|
|
676
|
+
] });
|
|
677
|
+
}
|
|
678
|
+
var MANIFEST_FILE = ".chiltepin-build.json";
|
|
679
|
+
var LEGACY_MANIFEST_FILE = ".avodado-build.json";
|
|
680
|
+
async function readManifest(outDir) {
|
|
681
|
+
let raw;
|
|
682
|
+
try {
|
|
683
|
+
raw = await readFile(join(outDir, MANIFEST_FILE), "utf8");
|
|
684
|
+
} catch {
|
|
685
|
+
try {
|
|
686
|
+
raw = await readFile(join(outDir, LEGACY_MANIFEST_FILE), "utf8");
|
|
687
|
+
} catch {
|
|
688
|
+
return void 0;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
const parsed = JSON.parse(raw);
|
|
693
|
+
if (parsed.version !== 1 || !Array.isArray(parsed.files)) return void 0;
|
|
694
|
+
const files = parsed.files.filter((f) => typeof f === "string");
|
|
695
|
+
return { version: 1, generator: String(parsed.generator ?? ""), files };
|
|
696
|
+
} catch {
|
|
697
|
+
return void 0;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
async function writeManifest(outDir, files, generator) {
|
|
701
|
+
const manifest = { version: 1, generator, files: [...files].sort() };
|
|
702
|
+
await writeFile(join(outDir, MANIFEST_FILE), JSON.stringify(manifest, null, 2) + "\n", "utf8");
|
|
703
|
+
try {
|
|
704
|
+
await unlink(join(outDir, LEGACY_MANIFEST_FILE));
|
|
705
|
+
} catch {
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
function inside(root, abs) {
|
|
709
|
+
const rel2 = relative(root, abs);
|
|
710
|
+
return rel2 !== "" && !rel2.startsWith("..") && !rel2.startsWith(sep);
|
|
711
|
+
}
|
|
712
|
+
async function pruneStale(outDir, previous, current) {
|
|
713
|
+
const root = resolve(outDir);
|
|
714
|
+
const removed = [];
|
|
715
|
+
const parents = /* @__PURE__ */ new Set();
|
|
716
|
+
for (const rel2 of previous) {
|
|
717
|
+
if (current.has(rel2)) continue;
|
|
718
|
+
const abs = resolve(root, rel2);
|
|
719
|
+
if (!inside(root, abs)) continue;
|
|
720
|
+
try {
|
|
721
|
+
await unlink(abs);
|
|
722
|
+
} catch {
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
removed.push(rel2);
|
|
726
|
+
parents.add(dirname(abs));
|
|
727
|
+
}
|
|
728
|
+
for (const dir of [...parents].sort((a, b) => b.length - a.length)) {
|
|
729
|
+
let cur = dir;
|
|
730
|
+
while (inside(root, cur)) {
|
|
731
|
+
try {
|
|
732
|
+
await rmdir(cur);
|
|
733
|
+
} catch {
|
|
734
|
+
break;
|
|
735
|
+
}
|
|
736
|
+
cur = dirname(cur);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
removed.sort();
|
|
740
|
+
return removed;
|
|
741
|
+
}
|
|
742
|
+
var LIVE_RELOAD_SCRIPT = `<script>new EventSource('/__events').onmessage=()=>location.reload()</script>`;
|
|
743
|
+
var SITE_CSS = `
|
|
744
|
+
body{margin:0;}
|
|
745
|
+
.site{display:flex;align-items:flex-start;max-width:1440px;margin:0 auto;}
|
|
746
|
+
.site-nav{position:sticky;top:0;flex:none;width:248px;max-height:100vh;overflow-y:auto;padding:28px 18px 48px;border-right:1px solid var(--rule);font-family:var(--font-body);font-size:13px;color:var(--ink);}
|
|
747
|
+
.site-nav .nav-brand{display:block;font-family:var(--font-display);font-weight:600;font-size:14px;letter-spacing:.01em;color:var(--ink);text-decoration:none;padding:4px 8px;margin-bottom:6px;}
|
|
748
|
+
.site-eyebrow{font-family:var(--font-mono);font-size:10px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--soft);}
|
|
749
|
+
.site-nav .nav-head{padding:10px 8px 6px;border-top:1px solid var(--rule);}
|
|
750
|
+
.site-nav a{display:block;color:var(--ink);text-decoration:none;padding:4px 8px;border-radius:4px;line-height:1.45;}
|
|
751
|
+
.site-nav a:hover{background:var(--paper-2);}
|
|
752
|
+
.site-nav a.current{color:var(--ink);font-weight:600;background:var(--paper-2);box-shadow:inset 2px 0 0 var(--ink);}
|
|
753
|
+
.site-nav .nav-sections{margin:2px 0 8px 12px;padding-left:10px;border-left:1px solid var(--rule);}
|
|
754
|
+
.site-nav .nav-sections a{font-size:12px;color:var(--muted);}
|
|
755
|
+
.site-nav .nav-sections a:hover{color:var(--ink);}
|
|
756
|
+
.site-main{flex:1;min-width:0;}
|
|
757
|
+
.site-main .docskin{padding-top:40px;}
|
|
758
|
+
@media (max-width:900px){
|
|
759
|
+
.site{display:block;}
|
|
760
|
+
.site-nav{position:static;width:auto;max-height:none;border-right:0;border-bottom:1px solid var(--rule);padding:20px 24px;}
|
|
761
|
+
}
|
|
762
|
+
.idx-head{padding:16px 0 28px;margin-bottom:36px;border-bottom:1px solid var(--rule);}
|
|
763
|
+
.idx-eyebrow{margin-bottom:10px;}
|
|
764
|
+
.idx-title{font-family:var(--font-display);font-weight:700;font-size:clamp(32px,4.4vw,48px);line-height:1.1;letter-spacing:-.015em;color:var(--ink);margin:0;}
|
|
765
|
+
.idx-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(280px,1fr));gap:16px;}
|
|
766
|
+
.idx-card{display:block;border:1px solid var(--rule-solid);border-radius:6px;background:var(--paper);padding:20px 22px 16px;text-decoration:none;color:var(--ink);box-shadow:none;}
|
|
767
|
+
.idx-card:hover{border-color:var(--ink);}
|
|
768
|
+
.idx-card .idx-tag{display:inline-block;font-family:var(--font-mono);font-size:10px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;line-height:1.3;padding:2px 7px;border:1px solid var(--rule-solid);border-radius:2px;background:var(--paper);color:var(--muted);margin-bottom:12px;}
|
|
769
|
+
.idx-card h2{font-family:var(--font-body);font-weight:600;font-size:16px;line-height:1.3;color:var(--ink);margin:0 0 6px;}
|
|
770
|
+
.idx-card p{font-size:13px;line-height:1.55;color:var(--muted);margin:0 0 12px;}
|
|
771
|
+
.idx-card .idx-slug{font-family:var(--font-mono);font-size:11px;color:var(--soft);}
|
|
772
|
+
.site-main{position:relative;}
|
|
773
|
+
.view-toggle{position:absolute;top:30px;right:32px;z-index:30;display:inline-flex;border:1px solid var(--rule-solid);border-radius:6px;background:var(--paper);overflow:hidden;font-family:var(--font-body);font-size:12px;font-weight:600;letter-spacing:.01em;box-shadow:none;}
|
|
774
|
+
.view-toggle a{display:flex;align-items:center;padding:5px 14px;color:var(--muted);text-decoration:none;line-height:1.4;}
|
|
775
|
+
.view-toggle a + a{border-left:1px solid var(--rule-solid);}
|
|
776
|
+
.view-toggle a:hover{color:var(--ink);background:var(--paper-2);}
|
|
777
|
+
.view-toggle a[aria-current]{background:var(--ink);color:var(--paper);}
|
|
778
|
+
.view-toggle a[aria-current]:hover{background:var(--ink);color:var(--paper);}
|
|
779
|
+
@media (max-width:900px){.view-toggle{top:14px;right:16px;}}
|
|
780
|
+
`;
|
|
781
|
+
var DECK_BACK_CSS = `
|
|
782
|
+
.deck-doc-link{position:fixed;top:14px;right:18px;z-index:20;display:inline-flex;align-items:center;gap:7px;
|
|
783
|
+
padding:6px 14px;border:1px solid var(--rule-solid);border-radius:6px;
|
|
784
|
+
background:var(--paper);
|
|
785
|
+
font-family:var(--font-body);font-size:12px;font-weight:600;letter-spacing:.01em;color:var(--ink);
|
|
786
|
+
text-decoration:none;box-shadow:none;}
|
|
787
|
+
.deck-doc-link:hover{background:var(--paper-2);border-color:var(--ink);}
|
|
788
|
+
@media print{.deck-doc-link{display:none;}}
|
|
789
|
+
`;
|
|
790
|
+
function rootPrefix(slug) {
|
|
791
|
+
const depth = slug.split("/").length - 1;
|
|
792
|
+
return "../".repeat(depth);
|
|
793
|
+
}
|
|
794
|
+
var REF_CHIP_RE = / data-ref="([^"]+)" href="#[^"]*"/g;
|
|
795
|
+
var REF_RE = /^([\w/.-]+)?#([\w.-]+)$/;
|
|
796
|
+
function rewriteRefs(html, slug, nodes) {
|
|
797
|
+
const prefix = rootPrefix(slug);
|
|
798
|
+
return html.replace(REF_CHIP_RE, (whole, ref) => {
|
|
799
|
+
const m = REF_RE.exec(ref);
|
|
800
|
+
if (m === null) return whole;
|
|
801
|
+
const targetDoc = m[1] ?? slug;
|
|
802
|
+
const id = m[2] ?? "";
|
|
803
|
+
const node = nodes.get(id);
|
|
804
|
+
if (node === void 0 || node.doc !== targetDoc) return ` data-ref="${ref}"`;
|
|
805
|
+
if (node.doc === slug) return ` data-ref="${ref}" href="#${id}"`;
|
|
806
|
+
return ` data-ref="${ref}" href="${prefix}${node.doc}.html#${id}"`;
|
|
807
|
+
});
|
|
808
|
+
}
|
|
809
|
+
function sidebar(navDocs, current, sections) {
|
|
810
|
+
const prefix = current !== void 0 ? rootPrefix(current) : "";
|
|
811
|
+
const items = navDocs.map((d) => {
|
|
812
|
+
const isCurrent = d.slug === current;
|
|
813
|
+
const cls = isCurrent ? ' class="current"' : "";
|
|
814
|
+
const link = `<a${cls} href="${prefix}${escapeHtml(d.slug)}.html">${escapeHtml(d.title)}</a>`;
|
|
815
|
+
if (!isCurrent || sections.length === 0) return link;
|
|
816
|
+
const secs = sections.map((s) => `<a href="#${s.id}">${escapeHtml(s.title ?? s.label)}</a>`).join("");
|
|
817
|
+
return link + `<div class="nav-sections">${secs}</div>`;
|
|
818
|
+
}).join("");
|
|
819
|
+
return `<aside class="site-nav"><a class="nav-brand" href="${prefix}index.html">Documentation</a><div class="nav-head site-eyebrow">Documents</div><nav>${items}</nav></aside>`;
|
|
820
|
+
}
|
|
821
|
+
function slugBase(slug) {
|
|
822
|
+
return slug.split("/").pop() ?? slug;
|
|
823
|
+
}
|
|
824
|
+
function viewToggle(slug) {
|
|
825
|
+
const base = escapeHtml(slugBase(slug));
|
|
826
|
+
return `<nav class="view-toggle" aria-label="View as"><a aria-current="page" href="${base}.html">Doc</a><a href="${base}.slides.html">Slides</a></nav>`;
|
|
827
|
+
}
|
|
828
|
+
function deckWithChrome(deckHtml, slug, liveReload) {
|
|
829
|
+
const base = escapeHtml(slugBase(slug));
|
|
830
|
+
const pill = `<a class="deck-doc-link" href="${base}.html">Document <span aria-hidden="true">\u25B8</span></a>`;
|
|
831
|
+
const chrome = `<style>${DECK_BACK_CSS}</style>${pill}${liveReload ? LIVE_RELOAD_SCRIPT : ""}`;
|
|
832
|
+
return deckHtml.replace("</head>", `${FAVICON_LINK}
|
|
833
|
+
</head>`).replace("</body>", `${chrome}</body>`);
|
|
834
|
+
}
|
|
835
|
+
function pageShell(args) {
|
|
836
|
+
const themeBlock = args.themeVars.length > 0 ? `<style>:root{${args.themeVars}}</style>` : "";
|
|
837
|
+
const reload = args.liveReload ? LIVE_RELOAD_SCRIPT : "";
|
|
838
|
+
const scheme = schemeMarkup(args.colorScheme);
|
|
839
|
+
return `<!doctype html>
|
|
840
|
+
<html lang="en"${scheme.stamp}>
|
|
841
|
+
<head>
|
|
842
|
+
<meta charset="utf-8">
|
|
843
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
844
|
+
<title>${escapeHtml(args.title)}</title>
|
|
845
|
+
${FAVICON_LINK}
|
|
846
|
+
<style>${args.css}</style>` + themeBlock + scheme.style + `<style>${SITE_CSS}</style>
|
|
847
|
+
</head>
|
|
848
|
+
<body>
|
|
849
|
+
<div class="site">
|
|
850
|
+
` + args.nav + `
|
|
851
|
+
<main class="site-main">${args.toggle ?? ""}<div class="docskin">${args.main}</div></main>
|
|
852
|
+
</div>
|
|
853
|
+
` + reload + `</body>
|
|
854
|
+
</html>
|
|
855
|
+
`;
|
|
856
|
+
}
|
|
857
|
+
function indexCard(d, tagDisplay) {
|
|
858
|
+
const meta = d.doc.meta;
|
|
859
|
+
const shownTag = tagDisplay ?? meta?.tag;
|
|
860
|
+
const tag = shownTag !== void 0 ? `<span class="idx-tag">${escapeHtml(shownTag)}</span>` : "";
|
|
861
|
+
const title = escapeHtml(meta?.title ?? d.slug);
|
|
862
|
+
const sub = meta?.subtitle !== void 0 ? `<p>${escapeHtml(meta.subtitle)}</p>` : "";
|
|
863
|
+
return `<a class="idx-card" href="${escapeHtml(d.slug)}.html">` + tag + `<h2>${title}</h2>` + sub + `<span class="idx-slug">${escapeHtml(d.slug)}</span></a>`;
|
|
864
|
+
}
|
|
865
|
+
function indexCards(docs) {
|
|
866
|
+
const cards = docs.map((d) => indexCard(d)).join("");
|
|
867
|
+
return `<div class="idx-head"><div class="idx-eyebrow site-eyebrow">${docs.length} document${docs.length === 1 ? "" : "s"}</div><h1 class="idx-title">Documentation</h1></div><div class="idx-grid">${cards}</div>`;
|
|
868
|
+
}
|
|
869
|
+
var RICH_INDEX_CSS = `
|
|
870
|
+
.idx-tldr{display:grid;gap:5px;margin-top:16px;font-size:13px;line-height:1.55;color:var(--muted);}
|
|
871
|
+
.idx-tldr a{color:var(--muted);text-decoration:none;}
|
|
872
|
+
.idx-tldr a:hover{color:var(--ink);}
|
|
873
|
+
.idx-tldr strong{font-family:var(--font-mono);font-size:10px;font-weight:500;letter-spacing:.14em;text-transform:uppercase;color:var(--ink);}
|
|
874
|
+
.idx-group{margin-bottom:40px;}
|
|
875
|
+
.idx-group-head{display:flex;align-items:baseline;gap:10px;font-family:var(--font-body);font-weight:600;font-size:18px;color:var(--ink);margin:0 0 14px;padding-bottom:8px;border-bottom:1px solid var(--rule);}
|
|
876
|
+
.idx-group-count{font-family:var(--font-mono);font-size:10px;font-weight:500;letter-spacing:.14em;color:var(--soft);}
|
|
877
|
+
.idx-graph{margin-top:8px;}
|
|
878
|
+
.idx-graph-legend{list-style:none;margin:14px 0 0;padding:0;display:grid;gap:6px;font-size:13px;}
|
|
879
|
+
.idx-graph-legend a{color:var(--link);text-decoration:none;font-weight:600;}
|
|
880
|
+
.idx-graph-legend a:hover{text-decoration:underline;}
|
|
881
|
+
.idx-graph-legend .idx-slug{font-family:var(--font-mono);font-size:11px;color:var(--soft);margin-left:8px;}
|
|
882
|
+
`;
|
|
883
|
+
function tagToken(tag) {
|
|
884
|
+
return (tag.split("\xB7")[0] ?? tag).trim();
|
|
885
|
+
}
|
|
886
|
+
function groupOf(d) {
|
|
887
|
+
const tag = d.doc.meta?.tag?.trim();
|
|
888
|
+
if (tag !== void 0 && tag.length > 0) {
|
|
889
|
+
const token = tagToken(tag);
|
|
890
|
+
if (token.length > 0) return { key: `tag:${token.toLowerCase()}`, label: token };
|
|
891
|
+
}
|
|
892
|
+
const slash = d.slug.indexOf("/");
|
|
893
|
+
if (slash > 0) {
|
|
894
|
+
const folder = d.slug.slice(0, slash);
|
|
895
|
+
return { key: `dir:${folder.toLowerCase()}`, label: folder };
|
|
896
|
+
}
|
|
897
|
+
return { key: "default", label: "Documents" };
|
|
898
|
+
}
|
|
899
|
+
function groupDocs(docs) {
|
|
900
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
901
|
+
for (const d of docs) {
|
|
902
|
+
const { key, label } = groupOf(d);
|
|
903
|
+
const g = byKey.get(key);
|
|
904
|
+
if (g !== void 0) g.docs.push(d);
|
|
905
|
+
else byKey.set(key, { label, docs: [d] });
|
|
906
|
+
}
|
|
907
|
+
const groups = [...byKey.values()].sort(
|
|
908
|
+
(a, b) => b.docs.length - a.docs.length || (a.label < b.label ? -1 : 1)
|
|
909
|
+
);
|
|
910
|
+
const seen = /* @__PURE__ */ new Set();
|
|
911
|
+
return groups.map((g) => {
|
|
912
|
+
const base = `group-${g.label.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || "docs"}`;
|
|
913
|
+
let anchor = base;
|
|
914
|
+
for (let n = 2; seen.has(anchor); n += 1) anchor = `${base}-${n}`;
|
|
915
|
+
seen.add(anchor);
|
|
916
|
+
return { label: g.label, anchor, docs: g.docs };
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
function tldrDigest(groups) {
|
|
920
|
+
const lines = groups.map((g) => {
|
|
921
|
+
const sub = g.docs.length === 1 ? g.docs[0]?.doc.meta?.subtitle : void 0;
|
|
922
|
+
const tail = sub !== void 0 ? ` \u2014 ${escapeHtml(sub)}` : "";
|
|
923
|
+
const count = `${g.docs.length} document${g.docs.length === 1 ? "" : "s"}`;
|
|
924
|
+
return `<a href="#${g.anchor}"><strong>${escapeHtml(g.label)}</strong> \xB7 ${count}${tail}</a>`;
|
|
925
|
+
}).join("");
|
|
926
|
+
return `<nav class="idx-tldr">${lines}</nav>`;
|
|
927
|
+
}
|
|
928
|
+
function crossRefGraphSection(docs, refEdges) {
|
|
929
|
+
const counts = /* @__PURE__ */ new Map();
|
|
930
|
+
for (const e of refEdges) {
|
|
931
|
+
const from = e.from.split(/[#@]/, 1)[0] ?? "";
|
|
932
|
+
const to = e.to.split("#", 1)[0] ?? "";
|
|
933
|
+
if (from.length === 0 || to.length === 0 || from === to) continue;
|
|
934
|
+
const key = `${from}\0${to}`;
|
|
935
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
936
|
+
}
|
|
937
|
+
if (counts.size === 0) return "";
|
|
938
|
+
const involved = /* @__PURE__ */ new Set();
|
|
939
|
+
for (const key of counts.keys()) {
|
|
940
|
+
const [from, to] = key.split("\0");
|
|
941
|
+
if (from !== void 0) involved.add(from);
|
|
942
|
+
if (to !== void 0) involved.add(to);
|
|
943
|
+
}
|
|
944
|
+
const graphDocs = docs.filter((d) => involved.has(d.slug));
|
|
945
|
+
const data = {
|
|
946
|
+
title: "Cross-references",
|
|
947
|
+
nodes: graphDocs.map((d) => ({ id: d.slug, label: d.doc.meta?.title ?? d.slug })),
|
|
948
|
+
edges: [...counts.entries()].map(([key, count]) => {
|
|
949
|
+
const sep6 = key.indexOf("\0");
|
|
950
|
+
return {
|
|
951
|
+
from: key.slice(0, sep6),
|
|
952
|
+
to: key.slice(sep6 + 1),
|
|
953
|
+
...count > 1 ? { weight: count } : {}
|
|
954
|
+
};
|
|
955
|
+
})
|
|
956
|
+
};
|
|
957
|
+
const legend = graphDocs.map(
|
|
958
|
+
(d) => `<li><a href="${escapeHtml(d.slug)}.html">${escapeHtml(d.doc.meta?.title ?? d.slug)}</a><span class="idx-slug">${escapeHtml(d.slug)}</span></li>`
|
|
959
|
+
).join("");
|
|
960
|
+
return `<section class="idx-graph">` + htmlRenderers.graph(data) + `<ul class="idx-graph-legend">${legend}</ul></section>`;
|
|
961
|
+
}
|
|
962
|
+
function groupCards(g) {
|
|
963
|
+
const firstSeen = /* @__PURE__ */ new Map();
|
|
964
|
+
return g.docs.map((d) => {
|
|
965
|
+
const tag = d.doc.meta?.tag;
|
|
966
|
+
if (tag === void 0) return indexCard(d);
|
|
967
|
+
const key = tag.trim().toLowerCase();
|
|
968
|
+
const display = firstSeen.get(key) ?? tag;
|
|
969
|
+
if (!firstSeen.has(key)) firstSeen.set(key, tag);
|
|
970
|
+
return indexCard(d, display);
|
|
971
|
+
}).join("");
|
|
972
|
+
}
|
|
973
|
+
var SINGLETON_LIMIT = 0.6;
|
|
974
|
+
function richIndexMain(docs, refEdges) {
|
|
975
|
+
const groups = groupDocs(docs);
|
|
976
|
+
const singletons = groups.filter((g) => g.docs.length === 1).length;
|
|
977
|
+
const degenerate = groups.length > 0 && singletons / groups.length > SINGLETON_LIMIT;
|
|
978
|
+
const head = `<div class="idx-head"><div class="idx-eyebrow site-eyebrow">${docs.length} document${docs.length === 1 ? "" : "s"}</div><h1 class="idx-title">Documentation</h1>` + (groups.length > 0 && !degenerate ? tldrDigest(groups) : "") + `</div>`;
|
|
979
|
+
const body = degenerate ? `<div class="idx-grid">${docs.map((d) => indexCard(d)).join("")}</div>` : groups.map(
|
|
980
|
+
(g) => `<section class="idx-group" id="${g.anchor}"><h2 class="idx-group-head">${escapeHtml(g.label)}<span class="idx-group-count">${g.docs.length}</span></h2><div class="idx-grid">${groupCards(g)}</div></section>`
|
|
981
|
+
).join("");
|
|
982
|
+
return `<style>${RICH_INDEX_CSS}</style>` + head + body + crossRefGraphSection(docs, refEdges);
|
|
983
|
+
}
|
|
984
|
+
function failedBody(d, message) {
|
|
985
|
+
return `<div class="idx-head"><div class="idx-eyebrow site-eyebrow">Render failed</div><h1 class="idx-title">${escapeHtml(d.doc.meta?.title ?? d.slug)}</h1></div><p>${escapeHtml(d.file)} could not be rendered.</p><pre><code>${escapeHtml(message)}</code></pre>`;
|
|
986
|
+
}
|
|
987
|
+
function failedParts(d, message) {
|
|
988
|
+
return {
|
|
989
|
+
css: houseCss,
|
|
990
|
+
themeVars: "",
|
|
991
|
+
body: failedBody(d, message),
|
|
992
|
+
title: d.doc.meta?.title ?? d.slug,
|
|
993
|
+
sections: []
|
|
994
|
+
};
|
|
995
|
+
}
|
|
996
|
+
function buildSite(docs, opts = {}) {
|
|
997
|
+
const diagnostics = [];
|
|
998
|
+
for (const d of docs) diagnostics.push(...validateDocument(d.doc, d.file));
|
|
999
|
+
const resolved = resolveRefs(docs.map((d) => ({ doc: d.doc, file: d.file })));
|
|
1000
|
+
diagnostics.push(...resolved.diagnostics);
|
|
1001
|
+
const themeOpts = {
|
|
1002
|
+
...opts.themeVars !== void 0 ? { themeVars: opts.themeVars } : {},
|
|
1003
|
+
...opts.colorScheme !== void 0 ? { colorScheme: opts.colorScheme } : {}
|
|
1004
|
+
};
|
|
1005
|
+
const liveReload = opts.liveReload === true;
|
|
1006
|
+
const rendered = docs.map((d) => {
|
|
1007
|
+
const guarded = guardRender(
|
|
1008
|
+
d.doc,
|
|
1009
|
+
d.file,
|
|
1010
|
+
"page",
|
|
1011
|
+
() => renderDocumentParts(d.doc, themeOpts)
|
|
1012
|
+
);
|
|
1013
|
+
if (guarded.ok) return { doc: d, parts: guarded.value, failed: false };
|
|
1014
|
+
diagnostics.push(guarded.diagnostic);
|
|
1015
|
+
return { doc: d, parts: failedParts(d, guarded.diagnostic.message), failed: true };
|
|
1016
|
+
});
|
|
1017
|
+
const navDocs = rendered.map((r) => ({
|
|
1018
|
+
slug: r.doc.slug,
|
|
1019
|
+
title: r.doc.doc.meta?.title ?? r.doc.slug
|
|
1020
|
+
}));
|
|
1021
|
+
const first = rendered[0];
|
|
1022
|
+
const css = first?.parts.css ?? houseCss;
|
|
1023
|
+
const themeVars = first?.parts.themeVars ?? buildThemeVars(DEFAULT_THEME, opts.themeVars);
|
|
1024
|
+
const pages = [];
|
|
1025
|
+
pages.push({
|
|
1026
|
+
path: "index.html",
|
|
1027
|
+
title: "Documentation",
|
|
1028
|
+
html: pageShell({
|
|
1029
|
+
title: "Documentation",
|
|
1030
|
+
css,
|
|
1031
|
+
themeVars,
|
|
1032
|
+
nav: sidebar(navDocs, void 0, []),
|
|
1033
|
+
main: opts.richIndex !== false ? richIndexMain(docs, resolved.graph.edges) : indexCards(docs),
|
|
1034
|
+
liveReload,
|
|
1035
|
+
...opts.colorScheme !== void 0 ? { colorScheme: opts.colorScheme } : {}
|
|
1036
|
+
})
|
|
1037
|
+
});
|
|
1038
|
+
for (const { doc, parts, failed } of rendered) {
|
|
1039
|
+
const shell = pageShell({
|
|
1040
|
+
title: parts.title,
|
|
1041
|
+
css: parts.css,
|
|
1042
|
+
themeVars: parts.themeVars,
|
|
1043
|
+
nav: sidebar(navDocs, doc.slug, parts.sections),
|
|
1044
|
+
main: parts.body,
|
|
1045
|
+
liveReload,
|
|
1046
|
+
...opts.colorScheme !== void 0 ? { colorScheme: opts.colorScheme } : {},
|
|
1047
|
+
toggle: viewToggle(doc.slug)
|
|
1048
|
+
});
|
|
1049
|
+
pages.push({
|
|
1050
|
+
path: `${doc.slug}.html`,
|
|
1051
|
+
title: parts.title,
|
|
1052
|
+
html: rewriteRefs(shell, doc.slug, resolved.graph.nodes)
|
|
1053
|
+
});
|
|
1054
|
+
let deckHtml;
|
|
1055
|
+
let deckError = "The page could not be rendered.";
|
|
1056
|
+
if (!failed) {
|
|
1057
|
+
const deck = guardRender(
|
|
1058
|
+
doc.doc,
|
|
1059
|
+
doc.file,
|
|
1060
|
+
"slide deck",
|
|
1061
|
+
() => deckWithChrome(toSlides(doc.doc, themeOpts), doc.slug, liveReload)
|
|
1062
|
+
);
|
|
1063
|
+
if (deck.ok) deckHtml = deck.value;
|
|
1064
|
+
else {
|
|
1065
|
+
diagnostics.push(deck.diagnostic);
|
|
1066
|
+
deckError = deck.diagnostic.message;
|
|
1067
|
+
}
|
|
1068
|
+
}
|
|
1069
|
+
pages.push({
|
|
1070
|
+
path: `${doc.slug}.slides.html`,
|
|
1071
|
+
title: `${parts.title} \u2014 Slides`,
|
|
1072
|
+
html: deckHtml ?? pageShell({
|
|
1073
|
+
title: `${parts.title} \u2014 Slides`,
|
|
1074
|
+
css: parts.css,
|
|
1075
|
+
themeVars: parts.themeVars,
|
|
1076
|
+
nav: sidebar(navDocs, doc.slug, []),
|
|
1077
|
+
main: failedBody(doc, deckError),
|
|
1078
|
+
liveReload,
|
|
1079
|
+
...opts.colorScheme !== void 0 ? { colorScheme: opts.colorScheme } : {}
|
|
1080
|
+
})
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
return { pages, diagnostics };
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
// src/commands/build.ts
|
|
1087
|
+
async function runBuild(opts) {
|
|
1088
|
+
const config = await loadConfig(opts.cwd);
|
|
1089
|
+
const outDir = resolve(opts.cwd, opts.out ?? config.outDir);
|
|
1090
|
+
const files = await loadDocs([`${config.docsDir}/**/*.md`], opts.cwd, config.docsDir);
|
|
1091
|
+
const docs = files.filter((f) => f.encodingError === void 0).map((f) => ({
|
|
1092
|
+
slug: f.slug,
|
|
1093
|
+
file: f.file,
|
|
1094
|
+
doc: parseDocument(f.source, f.slug)
|
|
1095
|
+
}));
|
|
1096
|
+
const site = buildSite(docs, { richIndex: opts.richIndex ?? config.richIndex, colorScheme: config.colorScheme });
|
|
1097
|
+
const diagnostics = [...encodingDiagnostics(files), ...site.diagnostics];
|
|
1098
|
+
const previous = await readManifest(outDir);
|
|
1099
|
+
const pages = [];
|
|
1100
|
+
for (const page of site.pages) {
|
|
1101
|
+
const abs = join(outDir, page.path);
|
|
1102
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
1103
|
+
await writeFile(abs, page.html, "utf8");
|
|
1104
|
+
pages.push({ path: page.path, bytes: page.html.length });
|
|
1105
|
+
}
|
|
1106
|
+
const generated = site.pages.map((p) => p.path);
|
|
1107
|
+
const removed = previous === void 0 ? [] : await pruneStale(outDir, previous.files, new Set(generated));
|
|
1108
|
+
await writeManifest(outDir, generated, `chiltepin ${cliVersion()}`);
|
|
1109
|
+
return {
|
|
1110
|
+
outDir,
|
|
1111
|
+
outDirRel: relative(opts.cwd, outDir) || ".",
|
|
1112
|
+
pages,
|
|
1113
|
+
removed,
|
|
1114
|
+
pruneDeferred: previous === void 0 && await hadContent(outDir, generated),
|
|
1115
|
+
diagnostics,
|
|
1116
|
+
exitCode: diagnostics.some((d) => d.code === "E_RENDER") ? 1 : 0
|
|
1117
|
+
};
|
|
1118
|
+
}
|
|
1119
|
+
async function hadContent(outDir, generated) {
|
|
1120
|
+
const own = /* @__PURE__ */ new Set([
|
|
1121
|
+
...generated.map((p) => p.split("/")[0]),
|
|
1122
|
+
MANIFEST_FILE
|
|
1123
|
+
]);
|
|
1124
|
+
try {
|
|
1125
|
+
const entries = await readdir(outDir);
|
|
1126
|
+
return entries.some((e) => !own.has(e));
|
|
1127
|
+
} catch {
|
|
1128
|
+
return false;
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
function walkDirs(root) {
|
|
1132
|
+
const out = [root];
|
|
1133
|
+
const walk2 = (dir) => {
|
|
1134
|
+
let entries;
|
|
1135
|
+
try {
|
|
1136
|
+
entries = readdirSync(dir, { withFileTypes: true });
|
|
1137
|
+
} catch {
|
|
1138
|
+
return;
|
|
1139
|
+
}
|
|
1140
|
+
for (const e of entries) {
|
|
1141
|
+
if (!e.isDirectory()) continue;
|
|
1142
|
+
const p = join(dir, e.name);
|
|
1143
|
+
out.push(p);
|
|
1144
|
+
walk2(p);
|
|
1145
|
+
}
|
|
1146
|
+
};
|
|
1147
|
+
walk2(root);
|
|
1148
|
+
return out;
|
|
1149
|
+
}
|
|
1150
|
+
function createDocsWatcher(dirAbs, onEvent) {
|
|
1151
|
+
const dirWatchers = /* @__PURE__ */ new Map();
|
|
1152
|
+
let rootWatcher;
|
|
1153
|
+
let usingFallback = false;
|
|
1154
|
+
const syncDirWatchers = () => {
|
|
1155
|
+
const dirs = new Set(walkDirs(dirAbs));
|
|
1156
|
+
for (const [dir, w] of dirWatchers) {
|
|
1157
|
+
if (!dirs.has(dir)) {
|
|
1158
|
+
w.close();
|
|
1159
|
+
dirWatchers.delete(dir);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
for (const dir of dirs) {
|
|
1163
|
+
if (dirWatchers.has(dir)) continue;
|
|
1164
|
+
try {
|
|
1165
|
+
const w = watch(dir, (_event, filename) => {
|
|
1166
|
+
onEvent(filename === null ? void 0 : join(dir, filename));
|
|
1167
|
+
});
|
|
1168
|
+
w.on("error", () => dirWatchers.delete(dir));
|
|
1169
|
+
dirWatchers.set(dir, w);
|
|
1170
|
+
} catch {
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
};
|
|
1174
|
+
if (existsSync(dirAbs)) {
|
|
1175
|
+
try {
|
|
1176
|
+
rootWatcher = watch(dirAbs, { recursive: true }, (_event, filename) => {
|
|
1177
|
+
onEvent(filename === null ? void 0 : join(dirAbs, filename));
|
|
1178
|
+
});
|
|
1179
|
+
rootWatcher.on("error", () => {
|
|
1180
|
+
rootWatcher = void 0;
|
|
1181
|
+
usingFallback = true;
|
|
1182
|
+
syncDirWatchers();
|
|
1183
|
+
});
|
|
1184
|
+
} catch {
|
|
1185
|
+
usingFallback = true;
|
|
1186
|
+
syncDirWatchers();
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return {
|
|
1190
|
+
resync() {
|
|
1191
|
+
if (usingFallback) syncDirWatchers();
|
|
1192
|
+
},
|
|
1193
|
+
close() {
|
|
1194
|
+
rootWatcher?.close();
|
|
1195
|
+
for (const w of dirWatchers.values()) w.close();
|
|
1196
|
+
dirWatchers.clear();
|
|
1197
|
+
}
|
|
1198
|
+
};
|
|
1199
|
+
}
|
|
1200
|
+
function createConfigWatcher(cwd, onEvent) {
|
|
1201
|
+
let watcher;
|
|
1202
|
+
if (existsSync(cwd)) {
|
|
1203
|
+
try {
|
|
1204
|
+
watcher = watch(cwd, (_event, filename) => {
|
|
1205
|
+
if (filename !== null && /^(chiltepin|avodado)\.config\./.test(filename)) onEvent();
|
|
1206
|
+
});
|
|
1207
|
+
watcher.on("error", () => {
|
|
1208
|
+
watcher?.close();
|
|
1209
|
+
watcher = void 0;
|
|
1210
|
+
});
|
|
1211
|
+
} catch {
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
return {
|
|
1215
|
+
close() {
|
|
1216
|
+
watcher?.close();
|
|
1217
|
+
watcher = void 0;
|
|
1218
|
+
}
|
|
1219
|
+
};
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
// src/commands/serve.ts
|
|
1223
|
+
var DEBOUNCE_MS = 150;
|
|
1224
|
+
var HEARTBEAT_MS = 25e3;
|
|
1225
|
+
var MAX_BANNER_ITEMS = 4;
|
|
1226
|
+
function fmtDiag(d) {
|
|
1227
|
+
const loc = d.line !== void 0 ? `${d.file}:${d.line}` : d.file;
|
|
1228
|
+
return `${loc} ${d.code} ${d.message}`;
|
|
1229
|
+
}
|
|
1230
|
+
function bannerHtml(diagnostics, fatal) {
|
|
1231
|
+
const items = fatal !== void 0 ? [fatal] : diagnostics.map(fmtDiag);
|
|
1232
|
+
if (items.length === 0) return "";
|
|
1233
|
+
const isError = fatal !== void 0 || diagnostics.some((d) => d.level === "error");
|
|
1234
|
+
const bg = isError ? "#7f1d1d" : "#78350f";
|
|
1235
|
+
const shown = items.slice(0, MAX_BANNER_ITEMS);
|
|
1236
|
+
const more = items.length - shown.length;
|
|
1237
|
+
const rows = shown.map((t) => `<div>${escapeHtml(t)}</div>`).join("");
|
|
1238
|
+
const moreRow = more > 0 ? `<div style="opacity:.75">\u2026and ${more} more</div>` : "";
|
|
1239
|
+
return `<div id="chiltepin-diagnostics" style="position:fixed;left:0;right:0;bottom:0;z-index:2147483647;background:${bg};color:#fff;font:12px/1.6 ui-monospace,Menlo,Consolas,monospace;padding:10px 18px;box-shadow:0 -2px 10px rgba(0,0,0,.3);white-space:pre-wrap;"><div style="font-weight:700;letter-spacing:.06em;text-transform:uppercase;font-size:10px;opacity:.85;margin-bottom:4px;">chiltepin \u2014 ${isError ? "errors" : "warnings"}</div>` + rows + moreRow + `</div>`;
|
|
1240
|
+
}
|
|
1241
|
+
async function runServe(opts) {
|
|
1242
|
+
const config = await loadConfig(opts.cwd);
|
|
1243
|
+
const docsDirAbs = resolve(opts.cwd, config.docsDir);
|
|
1244
|
+
const state = { pages: /* @__PURE__ */ new Map(), diagnostics: [], fatal: void 0 };
|
|
1245
|
+
const rebuild = async () => {
|
|
1246
|
+
try {
|
|
1247
|
+
const files = await loadDocs([`${config.docsDir}/**/*.md`], opts.cwd, config.docsDir);
|
|
1248
|
+
const docs = files.map((f) => ({
|
|
1249
|
+
slug: f.slug,
|
|
1250
|
+
file: f.file,
|
|
1251
|
+
doc: parseDocument(f.source, f.slug)
|
|
1252
|
+
}));
|
|
1253
|
+
const site = buildSite(docs, {
|
|
1254
|
+
colorScheme: config.colorScheme,
|
|
1255
|
+
liveReload: true,
|
|
1256
|
+
richIndex: opts.richIndex ?? config.richIndex
|
|
1257
|
+
});
|
|
1258
|
+
const next = /* @__PURE__ */ new Map();
|
|
1259
|
+
for (const p of site.pages) next.set(p.path, p);
|
|
1260
|
+
state.pages = next;
|
|
1261
|
+
state.diagnostics = site.diagnostics;
|
|
1262
|
+
state.fatal = void 0;
|
|
1263
|
+
} catch (err) {
|
|
1264
|
+
state.fatal = err instanceof Error ? err.message : String(err);
|
|
1265
|
+
}
|
|
1266
|
+
};
|
|
1267
|
+
await rebuild();
|
|
1268
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
1269
|
+
const server = createServer((req, res) => {
|
|
1270
|
+
const url2 = (req.url ?? "/").split("?")[0] ?? "/";
|
|
1271
|
+
if (url2 === "/__events") {
|
|
1272
|
+
res.writeHead(200, {
|
|
1273
|
+
"Content-Type": "text/event-stream",
|
|
1274
|
+
"Cache-Control": "no-store",
|
|
1275
|
+
Connection: "keep-alive"
|
|
1276
|
+
});
|
|
1277
|
+
res.write(":connected\n\n");
|
|
1278
|
+
sseClients.add(res);
|
|
1279
|
+
req.on("close", () => sseClients.delete(res));
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
let path;
|
|
1283
|
+
try {
|
|
1284
|
+
path = url2 === "/" ? "index.html" : decodeURIComponent(url2.replace(/^\//, ""));
|
|
1285
|
+
} catch {
|
|
1286
|
+
path = "index.html";
|
|
1287
|
+
}
|
|
1288
|
+
const page = state.pages.get(path);
|
|
1289
|
+
if (page === void 0) {
|
|
1290
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1291
|
+
res.end(`404 \u2014 no page at ${url2}
|
|
1292
|
+
`);
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const banner2 = bannerHtml(state.diagnostics, state.fatal);
|
|
1296
|
+
const html = banner2 === "" ? page.html : page.html.replace("<body>", `<body>
|
|
1297
|
+
${banner2}`);
|
|
1298
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
1299
|
+
res.end(html);
|
|
1300
|
+
});
|
|
1301
|
+
const heartbeat = setInterval(() => {
|
|
1302
|
+
for (const c of sseClients) c.write(":heartbeat\n\n");
|
|
1303
|
+
}, HEARTBEAT_MS);
|
|
1304
|
+
heartbeat.unref();
|
|
1305
|
+
const broadcastReload = () => {
|
|
1306
|
+
for (const c of sseClients) c.write("data: reload\n\n");
|
|
1307
|
+
};
|
|
1308
|
+
let debounce;
|
|
1309
|
+
const onFsEvent = () => {
|
|
1310
|
+
if (debounce !== void 0) clearTimeout(debounce);
|
|
1311
|
+
debounce = setTimeout(() => {
|
|
1312
|
+
void (async () => {
|
|
1313
|
+
await rebuild();
|
|
1314
|
+
docsWatcher.resync();
|
|
1315
|
+
broadcastReload();
|
|
1316
|
+
})();
|
|
1317
|
+
}, DEBOUNCE_MS);
|
|
1318
|
+
};
|
|
1319
|
+
const docsWatcher = createDocsWatcher(docsDirAbs, onFsEvent);
|
|
1320
|
+
const configWatcher = createConfigWatcher(opts.cwd, onFsEvent);
|
|
1321
|
+
await new Promise((ready, fail) => {
|
|
1322
|
+
server.once("error", fail);
|
|
1323
|
+
server.listen(opts.port, () => {
|
|
1324
|
+
server.removeListener("error", fail);
|
|
1325
|
+
ready();
|
|
1326
|
+
});
|
|
1327
|
+
});
|
|
1328
|
+
const address = server.address();
|
|
1329
|
+
const port = address !== null && typeof address === "object" ? address.port : opts.port;
|
|
1330
|
+
const url = `http://localhost:${port}`;
|
|
1331
|
+
console.log(`Serving ${config.docsDir}/ at ${url} (Ctrl-C to stop)`);
|
|
1332
|
+
if (opts.open) await open(url);
|
|
1333
|
+
await new Promise((done) => {
|
|
1334
|
+
let closed = false;
|
|
1335
|
+
const shutdown = () => {
|
|
1336
|
+
if (closed) return;
|
|
1337
|
+
closed = true;
|
|
1338
|
+
if (debounce !== void 0) clearTimeout(debounce);
|
|
1339
|
+
clearInterval(heartbeat);
|
|
1340
|
+
docsWatcher.close();
|
|
1341
|
+
configWatcher.close();
|
|
1342
|
+
for (const c of sseClients) c.end();
|
|
1343
|
+
server.close(() => done());
|
|
1344
|
+
server.closeAllConnections();
|
|
1345
|
+
setTimeout(done, 1e3).unref();
|
|
1346
|
+
};
|
|
1347
|
+
process.once("SIGINT", shutdown);
|
|
1348
|
+
process.once("SIGTERM", shutdown);
|
|
1349
|
+
});
|
|
1350
|
+
}
|
|
1351
|
+
var DEBOUNCE_MS2 = 150;
|
|
1352
|
+
var HEARTBEAT_MS2 = 25e3;
|
|
1353
|
+
var MAX_BODY_BYTES = 10 * 1024 * 1024;
|
|
1354
|
+
var normalizeLf = (s) => s.replace(/\r\n?/g, "\n");
|
|
1355
|
+
var hashOf = (s) => createHash("sha256").update(s, "utf8").digest("hex");
|
|
1356
|
+
function sendJson(res, status, body) {
|
|
1357
|
+
res.writeHead(status, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
1358
|
+
res.end(JSON.stringify(body));
|
|
1359
|
+
}
|
|
1360
|
+
function readBody(req, limit) {
|
|
1361
|
+
return new Promise((done) => {
|
|
1362
|
+
const chunks = [];
|
|
1363
|
+
let total = 0;
|
|
1364
|
+
let overflowed = false;
|
|
1365
|
+
req.on("data", (chunk) => {
|
|
1366
|
+
if (overflowed) return;
|
|
1367
|
+
total += chunk.length;
|
|
1368
|
+
if (total > limit) {
|
|
1369
|
+
overflowed = true;
|
|
1370
|
+
chunks.length = 0;
|
|
1371
|
+
return;
|
|
1372
|
+
}
|
|
1373
|
+
chunks.push(chunk);
|
|
1374
|
+
});
|
|
1375
|
+
req.on("end", () => done(overflowed ? null : Buffer.concat(chunks)));
|
|
1376
|
+
req.on("error", () => done(null));
|
|
1377
|
+
});
|
|
1378
|
+
}
|
|
1379
|
+
function resolveDocPath(docsDirAbs, remainder) {
|
|
1380
|
+
let slug;
|
|
1381
|
+
try {
|
|
1382
|
+
slug = decodeURIComponent(remainder);
|
|
1383
|
+
} catch {
|
|
1384
|
+
return { kind: "bad" };
|
|
1385
|
+
}
|
|
1386
|
+
if (slug === "") return { kind: "bad" };
|
|
1387
|
+
const abs = resolve(join(docsDirAbs, slug + ".md"));
|
|
1388
|
+
if (!abs.startsWith(docsDirAbs + sep)) return { kind: "forbidden" };
|
|
1389
|
+
return { kind: "ok", slug, abs };
|
|
1390
|
+
}
|
|
1391
|
+
var MIME = {
|
|
1392
|
+
".html": "text/html; charset=utf-8",
|
|
1393
|
+
".js": "text/javascript; charset=utf-8",
|
|
1394
|
+
".css": "text/css; charset=utf-8",
|
|
1395
|
+
".svg": "image/svg+xml",
|
|
1396
|
+
".png": "image/png",
|
|
1397
|
+
".woff2": "font/woff2",
|
|
1398
|
+
".json": "application/json",
|
|
1399
|
+
".map": "application/json"
|
|
1400
|
+
};
|
|
1401
|
+
var FALLBACK_HTML = '<!doctype html><html><head><meta charset="utf-8"><title>Chiltepin Studio</title></head><body style="font:16px/1.6 system-ui,sans-serif;max-width:38rem;margin:4rem auto;padding:0 1rem;"><h1>Chiltepin Studio</h1><p>The studio web app (<code>chiltepin-studio</code>) is not installed, so there is nothing to show here. Reinstall <code>chiltepin</code> to get the bundled assets.</p><p>The file-bridge API is still running: <code>/api/meta</code>, <code>/api/docs</code>, <code>/api/doc/<slug></code> and the <code>/__events</code> stream all work.</p></body></html>';
|
|
1402
|
+
async function runStudio(opts) {
|
|
1403
|
+
const config = await loadConfig(opts.cwd);
|
|
1404
|
+
const docsDirAbs = resolve(opts.cwd, config.docsDir);
|
|
1405
|
+
const recentWrites = /* @__PURE__ */ new Map();
|
|
1406
|
+
let assetsRootPromise;
|
|
1407
|
+
const assetsRoot = () => {
|
|
1408
|
+
assetsRootPromise ??= import('chiltepin-studio').then((mod) => resolve(mod.assetsPath())).catch(() => null);
|
|
1409
|
+
return assetsRootPromise;
|
|
1410
|
+
};
|
|
1411
|
+
let sitePages = null;
|
|
1412
|
+
let siteFresh = false;
|
|
1413
|
+
let siteBuild = null;
|
|
1414
|
+
const rebuildSite = async () => {
|
|
1415
|
+
const files = await loadDocs([`${config.docsDir}/**/*.md`], opts.cwd, config.docsDir);
|
|
1416
|
+
const docs = files.map((f) => ({
|
|
1417
|
+
slug: f.slug,
|
|
1418
|
+
file: f.file,
|
|
1419
|
+
doc: parseDocument(f.source, f.slug)
|
|
1420
|
+
}));
|
|
1421
|
+
const site = buildSite(docs, {
|
|
1422
|
+
colorScheme: config.colorScheme,
|
|
1423
|
+
liveReload: true,
|
|
1424
|
+
// the script hits /__events — same origin here
|
|
1425
|
+
richIndex: config.richIndex
|
|
1426
|
+
// on by default; config `false` opts out
|
|
1427
|
+
});
|
|
1428
|
+
const next = /* @__PURE__ */ new Map();
|
|
1429
|
+
for (const p of site.pages) next.set(p.path, p);
|
|
1430
|
+
return next;
|
|
1431
|
+
};
|
|
1432
|
+
const getSitePages = () => {
|
|
1433
|
+
if (siteFresh && sitePages !== null) return Promise.resolve(sitePages);
|
|
1434
|
+
siteBuild ??= rebuildSite().then((pages) => {
|
|
1435
|
+
sitePages = pages;
|
|
1436
|
+
siteFresh = true;
|
|
1437
|
+
return pages;
|
|
1438
|
+
}).catch((err) => {
|
|
1439
|
+
if (sitePages !== null) return sitePages;
|
|
1440
|
+
throw err;
|
|
1441
|
+
}).finally(() => {
|
|
1442
|
+
siteBuild = null;
|
|
1443
|
+
});
|
|
1444
|
+
return siteBuild;
|
|
1445
|
+
};
|
|
1446
|
+
const invalidateSite = () => {
|
|
1447
|
+
siteFresh = false;
|
|
1448
|
+
};
|
|
1449
|
+
const sseClients = /* @__PURE__ */ new Set();
|
|
1450
|
+
const broadcast = (payload) => {
|
|
1451
|
+
const line = `data: ${JSON.stringify(payload)}
|
|
1452
|
+
|
|
1453
|
+
`;
|
|
1454
|
+
for (const c of sseClients) c.write(line);
|
|
1455
|
+
};
|
|
1456
|
+
let docsDebounce;
|
|
1457
|
+
let metaDebounce;
|
|
1458
|
+
const pendingPaths = /* @__PURE__ */ new Set();
|
|
1459
|
+
let pendingUnattributed = false;
|
|
1460
|
+
const flushDocsEvents = async () => {
|
|
1461
|
+
docsWatcher.resync();
|
|
1462
|
+
invalidateSite();
|
|
1463
|
+
const paths = [...pendingPaths];
|
|
1464
|
+
const unattributed = pendingUnattributed;
|
|
1465
|
+
pendingPaths.clear();
|
|
1466
|
+
pendingUnattributed = false;
|
|
1467
|
+
for (const abs of paths) {
|
|
1468
|
+
const slug = relative(docsDirAbs, abs).split(sep).join("/").replace(/\.md$/i, "");
|
|
1469
|
+
let hash;
|
|
1470
|
+
try {
|
|
1471
|
+
hash = hashOf(normalizeLf(await readFile(abs, "utf8")));
|
|
1472
|
+
} catch {
|
|
1473
|
+
}
|
|
1474
|
+
if (hash !== void 0 && recentWrites.get(slug) === hash) {
|
|
1475
|
+
recentWrites.delete(slug);
|
|
1476
|
+
continue;
|
|
1477
|
+
}
|
|
1478
|
+
broadcast({ type: "fs", slug, ...hash !== void 0 ? { hash } : {} });
|
|
1479
|
+
}
|
|
1480
|
+
if (unattributed) broadcast({ type: "fs" });
|
|
1481
|
+
};
|
|
1482
|
+
const onDocsEvent = (absPath) => {
|
|
1483
|
+
if (absPath === void 0) pendingUnattributed = true;
|
|
1484
|
+
else if (/\.md$/i.test(absPath)) pendingPaths.add(absPath);
|
|
1485
|
+
if (docsDebounce !== void 0) clearTimeout(docsDebounce);
|
|
1486
|
+
docsDebounce = setTimeout(() => void flushDocsEvents(), DEBOUNCE_MS2);
|
|
1487
|
+
};
|
|
1488
|
+
const onMetaEvent = () => {
|
|
1489
|
+
if (metaDebounce !== void 0) clearTimeout(metaDebounce);
|
|
1490
|
+
metaDebounce = setTimeout(() => {
|
|
1491
|
+
invalidateSite();
|
|
1492
|
+
broadcast({ type: "meta" });
|
|
1493
|
+
}, DEBOUNCE_MS2);
|
|
1494
|
+
};
|
|
1495
|
+
const docsWatcher = createDocsWatcher(docsDirAbs, onDocsEvent);
|
|
1496
|
+
const configWatcher = createConfigWatcher(opts.cwd, onMetaEvent);
|
|
1497
|
+
const handleMeta = (res) => {
|
|
1498
|
+
sendJson(res, 200, { version: cliVersion(), docsDir: config.docsDir, colorScheme: config.colorScheme });
|
|
1499
|
+
};
|
|
1500
|
+
const docListCache = /* @__PURE__ */ new Map();
|
|
1501
|
+
const handleDocs = async (res) => {
|
|
1502
|
+
const files = await loadDocs([`${config.docsDir}/**/*.md`], opts.cwd, config.docsDir);
|
|
1503
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1504
|
+
const docs = await Promise.all(
|
|
1505
|
+
files.map(async (f) => {
|
|
1506
|
+
const st = await stat(f.absolute);
|
|
1507
|
+
seen.add(f.slug);
|
|
1508
|
+
let entry = docListCache.get(f.slug);
|
|
1509
|
+
if (entry === void 0 || entry.mtimeMs !== st.mtimeMs) {
|
|
1510
|
+
let title = f.slug;
|
|
1511
|
+
let errorCount = 0;
|
|
1512
|
+
try {
|
|
1513
|
+
const doc = parseDocument(f.source, f.slug);
|
|
1514
|
+
title = doc.meta?.title ?? f.slug;
|
|
1515
|
+
errorCount = validateDocument(doc, f.file).filter((d) => d.level === "error").length;
|
|
1516
|
+
} catch {
|
|
1517
|
+
errorCount = 1;
|
|
1518
|
+
}
|
|
1519
|
+
entry = { mtimeMs: st.mtimeMs, title, errorCount };
|
|
1520
|
+
docListCache.set(f.slug, entry);
|
|
1521
|
+
}
|
|
1522
|
+
return {
|
|
1523
|
+
slug: f.slug,
|
|
1524
|
+
file: f.file,
|
|
1525
|
+
title: entry.title,
|
|
1526
|
+
mtimeMs: st.mtimeMs,
|
|
1527
|
+
errorCount: entry.errorCount
|
|
1528
|
+
};
|
|
1529
|
+
})
|
|
1530
|
+
);
|
|
1531
|
+
for (const slug of [...docListCache.keys()]) {
|
|
1532
|
+
if (!seen.has(slug)) docListCache.delete(slug);
|
|
1533
|
+
}
|
|
1534
|
+
sendJson(res, 200, docs);
|
|
1535
|
+
};
|
|
1536
|
+
const handleDocGet = async (res, abs) => {
|
|
1537
|
+
if (!existsSync(abs)) {
|
|
1538
|
+
sendJson(res, 404, { error: "document not found" });
|
|
1539
|
+
return;
|
|
1540
|
+
}
|
|
1541
|
+
const [raw, st] = await Promise.all([readFile(abs, "utf8"), stat(abs)]);
|
|
1542
|
+
const source = normalizeLf(raw);
|
|
1543
|
+
sendJson(res, 200, { source, hash: hashOf(source), mtimeMs: st.mtimeMs });
|
|
1544
|
+
};
|
|
1545
|
+
const handleDocPut = async (req, res, slug, abs, force) => {
|
|
1546
|
+
const body = await readBody(req, MAX_BODY_BYTES);
|
|
1547
|
+
if (body === null) {
|
|
1548
|
+
sendJson(res, 413, { error: `body exceeds ${MAX_BODY_BYTES} bytes` });
|
|
1549
|
+
return;
|
|
1550
|
+
}
|
|
1551
|
+
let parsed;
|
|
1552
|
+
try {
|
|
1553
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
1554
|
+
} catch {
|
|
1555
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
1556
|
+
return;
|
|
1557
|
+
}
|
|
1558
|
+
if (parsed === null || typeof parsed !== "object") {
|
|
1559
|
+
sendJson(res, 400, { error: "body must be a JSON object" });
|
|
1560
|
+
return;
|
|
1561
|
+
}
|
|
1562
|
+
const { source: rawSource, baseHash } = parsed;
|
|
1563
|
+
if (typeof rawSource !== "string") {
|
|
1564
|
+
sendJson(res, 400, { error: 'missing "source" (string)' });
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
if (baseHash !== void 0 && typeof baseHash !== "string") {
|
|
1568
|
+
sendJson(res, 400, { error: '"baseHash" must be a string when present' });
|
|
1569
|
+
return;
|
|
1570
|
+
}
|
|
1571
|
+
const source = normalizeLf(rawSource);
|
|
1572
|
+
if (existsSync(abs)) {
|
|
1573
|
+
const currentSource = normalizeLf(await readFile(abs, "utf8"));
|
|
1574
|
+
const currentHash = hashOf(currentSource);
|
|
1575
|
+
if (baseHash !== void 0 && baseHash !== currentHash && !force) {
|
|
1576
|
+
sendJson(res, 409, { currentHash, currentSource });
|
|
1577
|
+
return;
|
|
1578
|
+
}
|
|
1579
|
+
} else if (baseHash !== void 0 && !force) {
|
|
1580
|
+
sendJson(res, 409, { error: "file no longer exists on disk (use ?force=1 to recreate)" });
|
|
1581
|
+
return;
|
|
1582
|
+
}
|
|
1583
|
+
await mkdir(dirname(abs), { recursive: true });
|
|
1584
|
+
const tmp = join(dirname(abs), `.${basename(abs)}.${randomBytes(6).toString("hex")}.tmp`);
|
|
1585
|
+
await writeFile(tmp, source, "utf8");
|
|
1586
|
+
await rename(tmp, abs);
|
|
1587
|
+
const hash = hashOf(source);
|
|
1588
|
+
recentWrites.set(slug, hash);
|
|
1589
|
+
const st = await stat(abs);
|
|
1590
|
+
sendJson(res, 200, { hash, mtimeMs: st.mtimeMs });
|
|
1591
|
+
};
|
|
1592
|
+
const chromiumExport = (convert, contentType) => async (req, res) => {
|
|
1593
|
+
const body = await readBody(req, MAX_BODY_BYTES);
|
|
1594
|
+
if (body === null) {
|
|
1595
|
+
sendJson(res, 413, { error: `body exceeds ${MAX_BODY_BYTES} bytes` });
|
|
1596
|
+
return;
|
|
1597
|
+
}
|
|
1598
|
+
let parsed;
|
|
1599
|
+
try {
|
|
1600
|
+
parsed = JSON.parse(body.toString("utf8"));
|
|
1601
|
+
} catch {
|
|
1602
|
+
sendJson(res, 400, { error: "invalid JSON body" });
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
const html = parsed?.html;
|
|
1606
|
+
if (typeof html !== "string") {
|
|
1607
|
+
sendJson(res, 400, { error: 'missing "html" (string)' });
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
try {
|
|
1611
|
+
const bytes = await convert(html);
|
|
1612
|
+
res.writeHead(200, { "Content-Type": contentType, "Cache-Control": "no-store" });
|
|
1613
|
+
res.end(Buffer.from(bytes));
|
|
1614
|
+
} catch (err) {
|
|
1615
|
+
sendJson(res, 500, { error: err instanceof Error ? err.message : String(err) });
|
|
1616
|
+
}
|
|
1617
|
+
};
|
|
1618
|
+
const handleExportPdf = chromiumExport(
|
|
1619
|
+
(html) => toPdf(html, { autoInstallBrowser: true, log: (m) => console.log(m) }),
|
|
1620
|
+
"application/pdf"
|
|
1621
|
+
);
|
|
1622
|
+
const handleSite = async (res, pathname) => {
|
|
1623
|
+
let rel2;
|
|
1624
|
+
try {
|
|
1625
|
+
rel2 = decodeURIComponent(pathname.slice("/site/".length));
|
|
1626
|
+
} catch {
|
|
1627
|
+
rel2 = "";
|
|
1628
|
+
}
|
|
1629
|
+
const path = rel2 === "" ? "index.html" : rel2;
|
|
1630
|
+
const pages = await getSitePages();
|
|
1631
|
+
const page = pages.get(path);
|
|
1632
|
+
if (page === void 0) {
|
|
1633
|
+
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
|
|
1634
|
+
res.end(`404 \u2014 no page at ${pathname}
|
|
1635
|
+
`);
|
|
1636
|
+
return;
|
|
1637
|
+
}
|
|
1638
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
1639
|
+
res.end(page.html);
|
|
1640
|
+
};
|
|
1641
|
+
const handleStatic = async (res, pathname) => {
|
|
1642
|
+
const root = await assetsRoot();
|
|
1643
|
+
let rel2;
|
|
1644
|
+
try {
|
|
1645
|
+
rel2 = decodeURIComponent(pathname).replace(/^\/+/, "");
|
|
1646
|
+
} catch {
|
|
1647
|
+
rel2 = "";
|
|
1648
|
+
}
|
|
1649
|
+
const isAsset = extname(rel2) !== "";
|
|
1650
|
+
if (root === null) {
|
|
1651
|
+
if (isAsset) {
|
|
1652
|
+
sendJson(res, 404, { error: "studio assets not installed" });
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" });
|
|
1656
|
+
res.end(FALLBACK_HTML);
|
|
1657
|
+
return;
|
|
1658
|
+
}
|
|
1659
|
+
const target = isAsset ? resolve(join(root, rel2)) : join(root, "index.html");
|
|
1660
|
+
if (isAsset && !target.startsWith(root + sep)) {
|
|
1661
|
+
sendJson(res, 403, { error: "forbidden" });
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
const content = await readFile(target);
|
|
1666
|
+
const type = MIME[extname(target).toLowerCase()] ?? "application/octet-stream";
|
|
1667
|
+
res.writeHead(200, { "Content-Type": type, "Cache-Control": "no-store" });
|
|
1668
|
+
res.end(content);
|
|
1669
|
+
} catch {
|
|
1670
|
+
sendJson(res, 404, { error: "not found" });
|
|
1671
|
+
}
|
|
1672
|
+
};
|
|
1673
|
+
const handle = async (req, res) => {
|
|
1674
|
+
const method = req.method ?? "GET";
|
|
1675
|
+
const rawUrl = req.url ?? "/";
|
|
1676
|
+
const q = rawUrl.indexOf("?");
|
|
1677
|
+
const pathname = q === -1 ? rawUrl : rawUrl.slice(0, q);
|
|
1678
|
+
const query = new URLSearchParams(q === -1 ? "" : rawUrl.slice(q + 1));
|
|
1679
|
+
if (method === "OPTIONS") {
|
|
1680
|
+
res.writeHead(204);
|
|
1681
|
+
res.end();
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
1684
|
+
if (pathname === "/__events") {
|
|
1685
|
+
res.writeHead(200, {
|
|
1686
|
+
"Content-Type": "text/event-stream",
|
|
1687
|
+
"Cache-Control": "no-store",
|
|
1688
|
+
Connection: "keep-alive"
|
|
1689
|
+
});
|
|
1690
|
+
res.write(":connected\n\n");
|
|
1691
|
+
sseClients.add(res);
|
|
1692
|
+
req.on("close", () => sseClients.delete(res));
|
|
1693
|
+
return;
|
|
1694
|
+
}
|
|
1695
|
+
if (pathname === "/api/meta") {
|
|
1696
|
+
if (method !== "GET") return sendJson(res, 405, { error: "method not allowed" });
|
|
1697
|
+
return handleMeta(res);
|
|
1698
|
+
}
|
|
1699
|
+
if (pathname === "/api/docs") {
|
|
1700
|
+
if (method !== "GET") return sendJson(res, 405, { error: "method not allowed" });
|
|
1701
|
+
return handleDocs(res);
|
|
1702
|
+
}
|
|
1703
|
+
if (pathname === "/api/export/pdf") {
|
|
1704
|
+
if (method !== "POST") return sendJson(res, 405, { error: "method not allowed" });
|
|
1705
|
+
return handleExportPdf(req, res);
|
|
1706
|
+
}
|
|
1707
|
+
if (pathname.startsWith("/api/doc/")) {
|
|
1708
|
+
const hit = resolveDocPath(docsDirAbs, pathname.slice("/api/doc/".length));
|
|
1709
|
+
if (hit.kind === "bad") return sendJson(res, 400, { error: "invalid slug" });
|
|
1710
|
+
if (hit.kind === "forbidden") return sendJson(res, 403, { error: "forbidden" });
|
|
1711
|
+
if (method === "GET") return handleDocGet(res, hit.abs);
|
|
1712
|
+
if (method === "PUT") {
|
|
1713
|
+
return handleDocPut(req, res, hit.slug, hit.abs, query.get("force") === "1");
|
|
1714
|
+
}
|
|
1715
|
+
return sendJson(res, 405, { error: "method not allowed" });
|
|
1716
|
+
}
|
|
1717
|
+
if (pathname.startsWith("/api/")) return sendJson(res, 404, { error: "unknown API route" });
|
|
1718
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
1719
|
+
return sendJson(res, 405, { error: "method not allowed" });
|
|
1720
|
+
}
|
|
1721
|
+
if (pathname === "/site" || pathname.startsWith("/site/")) {
|
|
1722
|
+
return handleSite(res, pathname);
|
|
1723
|
+
}
|
|
1724
|
+
return handleStatic(res, pathname);
|
|
1725
|
+
};
|
|
1726
|
+
const server = createServer((req, res) => {
|
|
1727
|
+
void handle(req, res).catch((err) => {
|
|
1728
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1729
|
+
if (!res.headersSent) sendJson(res, 500, { error: message });
|
|
1730
|
+
else res.end();
|
|
1731
|
+
});
|
|
1732
|
+
});
|
|
1733
|
+
const heartbeat = setInterval(() => {
|
|
1734
|
+
for (const c of sseClients) c.write(":heartbeat\n\n");
|
|
1735
|
+
}, HEARTBEAT_MS2);
|
|
1736
|
+
heartbeat.unref();
|
|
1737
|
+
await new Promise((ready, fail) => {
|
|
1738
|
+
server.once("error", fail);
|
|
1739
|
+
server.listen(opts.port, "127.0.0.1", () => {
|
|
1740
|
+
server.removeListener("error", fail);
|
|
1741
|
+
ready();
|
|
1742
|
+
});
|
|
1743
|
+
});
|
|
1744
|
+
const address = server.address();
|
|
1745
|
+
const port = address !== null && typeof address === "object" ? address.port : opts.port;
|
|
1746
|
+
const url = `http://localhost:${port}`;
|
|
1747
|
+
console.log(`Studio at ${url} (Ctrl-C to stop)`);
|
|
1748
|
+
console.log(`Editing ${config.docsDir}/ \u2014 the files stay the source of truth`);
|
|
1749
|
+
if (opts.open) await open(url);
|
|
1750
|
+
await new Promise((done) => {
|
|
1751
|
+
let closed = false;
|
|
1752
|
+
const shutdown = () => {
|
|
1753
|
+
if (closed) return;
|
|
1754
|
+
closed = true;
|
|
1755
|
+
if (docsDebounce !== void 0) clearTimeout(docsDebounce);
|
|
1756
|
+
if (metaDebounce !== void 0) clearTimeout(metaDebounce);
|
|
1757
|
+
clearInterval(heartbeat);
|
|
1758
|
+
docsWatcher.close();
|
|
1759
|
+
configWatcher.close();
|
|
1760
|
+
for (const c of sseClients) c.end();
|
|
1761
|
+
server.close(() => done());
|
|
1762
|
+
server.closeAllConnections();
|
|
1763
|
+
setTimeout(done, 1e3).unref();
|
|
1764
|
+
};
|
|
1765
|
+
process.once("SIGINT", shutdown);
|
|
1766
|
+
process.once("SIGTERM", shutdown);
|
|
1767
|
+
});
|
|
1768
|
+
}
|
|
1769
|
+
function resolveBlockName(name) {
|
|
1770
|
+
if (BLOCK_TYPES.includes(name)) return { type: name };
|
|
1771
|
+
const alias = BLOCK_ALIASES[name];
|
|
1772
|
+
if (alias !== void 0) return { type: alias.type, alias: name };
|
|
1773
|
+
return void 0;
|
|
1774
|
+
}
|
|
1775
|
+
function blockIndex() {
|
|
1776
|
+
const out = [
|
|
1777
|
+
`${BLOCK_TYPES.length} block types \u2014 chiltepin block <type> for fields + example`,
|
|
1778
|
+
""
|
|
1779
|
+
];
|
|
1780
|
+
for (const fam of BLOCK_FAMILIES) {
|
|
1781
|
+
const types = familyBlocks(fam.id);
|
|
1782
|
+
out.push(`${fam.label} (${types.length})`);
|
|
1783
|
+
for (const t of types) out.push(` ${t.padEnd(14)}${BLOCK_DESCRIPTIONS[t]}`);
|
|
1784
|
+
out.push("");
|
|
1785
|
+
}
|
|
1786
|
+
const aliases = Object.entries(BLOCK_ALIASES).map(([a, d]) => `${a}\u2192${d.type}`);
|
|
1787
|
+
out.push(`Old names still accepted: ${aliases.join(" ")}`);
|
|
1788
|
+
return out.join("\n") + "\n";
|
|
1789
|
+
}
|
|
1790
|
+
function blockReference(type, json) {
|
|
1791
|
+
return json ? JSON.stringify(blockContract(type), null, 2) + "\n" : formatBlockContract(type);
|
|
1792
|
+
}
|
|
1793
|
+
function copyToClipboard(text) {
|
|
1794
|
+
const candidates = process.platform === "darwin" ? [["pbcopy", []]] : process.platform === "win32" ? [["clip", []]] : [
|
|
1795
|
+
["wl-copy", []],
|
|
1796
|
+
["xclip", ["-selection", "clipboard"]],
|
|
1797
|
+
["xsel", ["--clipboard", "--input"]]
|
|
1798
|
+
];
|
|
1799
|
+
for (const [cmd, args] of candidates) {
|
|
1800
|
+
try {
|
|
1801
|
+
const r = spawnSync(cmd, [...args], { input: text });
|
|
1802
|
+
if (r.status === 0) return true;
|
|
1803
|
+
} catch {
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
return false;
|
|
1807
|
+
}
|
|
1808
|
+
|
|
1809
|
+
// src/commands/skill.ts
|
|
1810
|
+
var SYSTEM_HEADER = `# Chiltepin authoring \u2014 system prompt
|
|
1811
|
+
|
|
1812
|
+
You are an expert author of **Chiltepin** documents: Markdown files that mix prose
|
|
1813
|
+
with typed, fenced YAML blocks, where the \`.md\` file on disk is the single source
|
|
1814
|
+
of truth. Whenever you create or edit documentation in a Chiltepin project, follow
|
|
1815
|
+
the grammar and rules below exactly:
|
|
1816
|
+
|
|
1817
|
+
- Keep narrative in plain Markdown; put every structured thing (diagram, table,
|
|
1818
|
+
roadmap, story) in a documented typed block. Never paste raw HTML or SVG.
|
|
1819
|
+
- Use only the documented block types and their documented fields \u2014 the schemas
|
|
1820
|
+
are strict, so an unknown block or field is an error.
|
|
1821
|
+
- Give a block an \`id:\` when something references it; reference it as \`doc#id\`.
|
|
1822
|
+
- Quote any YAML value containing \`,\` \`:\` \`#\` \`[\` \`]\` \`{\` \`}\` or a leading special character.
|
|
1823
|
+
- Edit blocks surgically \u2014 don't regenerate whole files.
|
|
1824
|
+
- A change is done only when \`chiltepin check\` passes (if the tooling is available).
|
|
1825
|
+
|
|
1826
|
+
The complete block grammar, field contract, and authoring recipe follow.
|
|
1827
|
+
|
|
1828
|
+
---
|
|
1829
|
+
`;
|
|
1830
|
+
async function readSkill() {
|
|
1831
|
+
return stitchSkill();
|
|
1832
|
+
}
|
|
1833
|
+
function stripFrontmatter(md) {
|
|
1834
|
+
const m = /^---\n[\s\S]*?\n---\n/.exec(md);
|
|
1835
|
+
return m !== null ? md.slice(m[0].length).replace(/^\s+/, "") : md;
|
|
1836
|
+
}
|
|
1837
|
+
async function systemPrompt(opts = {}) {
|
|
1838
|
+
const skill = await readSkill();
|
|
1839
|
+
if (opts.raw === true) return skill;
|
|
1840
|
+
return SYSTEM_HEADER + stripFrontmatter(skill);
|
|
1841
|
+
}
|
|
1842
|
+
function slugFromPath(path) {
|
|
1843
|
+
const base = basename(path, extname(path));
|
|
1844
|
+
return base.length > 0 ? base : "api";
|
|
1845
|
+
}
|
|
1846
|
+
async function runSyncOpenApi(opts) {
|
|
1847
|
+
if (opts.out === void 0 && opts.check === void 0) {
|
|
1848
|
+
return { exitCode: 2, message: "chiltepin sync openapi: must specify --out <path> or --check <path>" };
|
|
1849
|
+
}
|
|
1850
|
+
if (opts.out !== void 0 && opts.check !== void 0) {
|
|
1851
|
+
return { exitCode: 2, message: "chiltepin sync openapi: --out and --check are mutually exclusive" };
|
|
1852
|
+
}
|
|
1853
|
+
const specAbs = resolve(opts.cwd, opts.spec);
|
|
1854
|
+
if (!existsSync(specAbs)) {
|
|
1855
|
+
return { exitCode: 2, message: `Spec not found: ${specAbs}` };
|
|
1856
|
+
}
|
|
1857
|
+
const source = await readFile(specAbs, "utf8");
|
|
1858
|
+
let spec;
|
|
1859
|
+
try {
|
|
1860
|
+
spec = parseOpenApi(source);
|
|
1861
|
+
} catch (err) {
|
|
1862
|
+
return { exitCode: 1, message: `Failed to parse spec: ${err.message}` };
|
|
1863
|
+
}
|
|
1864
|
+
const targetPath = opts.out ?? opts.check;
|
|
1865
|
+
if (targetPath === void 0) {
|
|
1866
|
+
return { exitCode: 2, message: "no target path" };
|
|
1867
|
+
}
|
|
1868
|
+
const slug = opts.slug ?? slugFromPath(targetPath);
|
|
1869
|
+
const generated = openapiToMarkdown(spec, { slug });
|
|
1870
|
+
if (opts.out !== void 0) {
|
|
1871
|
+
const outAbs = resolve(opts.cwd, opts.out);
|
|
1872
|
+
const refusal = overwriteRefusal(outAbs, { force: opts.force });
|
|
1873
|
+
if (refusal !== void 0) return { exitCode: 1, message: refusal };
|
|
1874
|
+
await mkdir(dirname(outAbs), { recursive: true });
|
|
1875
|
+
await writeFile(outAbs, generated, "utf8");
|
|
1876
|
+
return {
|
|
1877
|
+
exitCode: 0,
|
|
1878
|
+
message: `Wrote ${outAbs} (${generated.length} bytes)`
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
const checkAbs = resolve(opts.cwd, opts.check ?? "");
|
|
1882
|
+
if (!existsSync(checkAbs)) {
|
|
1883
|
+
return {
|
|
1884
|
+
exitCode: 1,
|
|
1885
|
+
message: `Drift: ${checkAbs} does not exist. Run with --out ${opts.check} to generate it.`
|
|
1886
|
+
};
|
|
1887
|
+
}
|
|
1888
|
+
const existing = await readFile(checkAbs, "utf8");
|
|
1889
|
+
if (existing === generated) {
|
|
1890
|
+
return {
|
|
1891
|
+
exitCode: 0,
|
|
1892
|
+
message: `OK: ${checkAbs} matches ${specAbs} (${generated.length} bytes)`
|
|
1893
|
+
};
|
|
1894
|
+
}
|
|
1895
|
+
return {
|
|
1896
|
+
exitCode: 1,
|
|
1897
|
+
message: `Drift: ${checkAbs} differs from what ${specAbs} would generate.`,
|
|
1898
|
+
diff: simpleDiff(existing, generated)
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
function simpleDiff(a, b) {
|
|
1902
|
+
const aLines = a.split("\n");
|
|
1903
|
+
const bLines = b.split("\n");
|
|
1904
|
+
const out = [];
|
|
1905
|
+
const max = Math.max(aLines.length, bLines.length);
|
|
1906
|
+
for (let i = 0; i < max; i++) {
|
|
1907
|
+
const av = aLines[i];
|
|
1908
|
+
const bv = bLines[i];
|
|
1909
|
+
if (av === bv) continue;
|
|
1910
|
+
if (av !== void 0) out.push(`- ${av}`);
|
|
1911
|
+
if (bv !== void 0) out.push(`+ ${bv}`);
|
|
1912
|
+
if (out.length >= 40) {
|
|
1913
|
+
out.push(`\u2026 (truncated; ${max - i - 1} more lines)`);
|
|
1914
|
+
break;
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
return out.join("\n");
|
|
1918
|
+
}
|
|
1919
|
+
function idFromFile(file) {
|
|
1920
|
+
const slug = basename(file, extname(file)).toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
|
|
1921
|
+
return slug === "" ? "schema" : slug;
|
|
1922
|
+
}
|
|
1923
|
+
async function runSyncSchema(opts) {
|
|
1924
|
+
const fileAbs = resolve(opts.cwd, opts.file);
|
|
1925
|
+
if (!existsSync(fileAbs)) {
|
|
1926
|
+
return { exitCode: 2, entities: 0, relations: 0, message: `Schema not found: ${fileAbs}` };
|
|
1927
|
+
}
|
|
1928
|
+
const source = await readFile(fileAbs, "utf8");
|
|
1929
|
+
const result = opts.dialect === "sql" ? convertSqlDdl(source) : opts.dialect === "dbml" ? convertDbml(source) : convertPrisma(source);
|
|
1930
|
+
if (!result.ok) {
|
|
1931
|
+
const where = result.line !== void 0 ? `${opts.file}:${result.line}` : opts.file;
|
|
1932
|
+
return { exitCode: 1, entities: 0, relations: 0, message: `Could not read ${where}: ${result.message}` };
|
|
1933
|
+
}
|
|
1934
|
+
const data = result.data;
|
|
1935
|
+
const entities = Array.isArray(data["entities"]) ? data["entities"].length : 0;
|
|
1936
|
+
const relations = Array.isArray(data["relations"]) ? data["relations"].length : 0;
|
|
1937
|
+
const fence = erdFence(data, opts.id ?? idFromFile(opts.file));
|
|
1938
|
+
if (opts.out === void 0) {
|
|
1939
|
+
return { exitCode: 0, fence, entities, relations };
|
|
1940
|
+
}
|
|
1941
|
+
const title = opts.title ?? titleFromFile(opts.file);
|
|
1942
|
+
const doc = "```meta\ntitle: " + JSON.stringify(title) + "\n```\n\n" + fence;
|
|
1943
|
+
const outAbs = resolve(opts.cwd, opts.out);
|
|
1944
|
+
const refusal = overwriteRefusal(outAbs, { force: opts.force });
|
|
1945
|
+
if (refusal !== void 0) {
|
|
1946
|
+
return { exitCode: 1, entities, relations, message: refusal };
|
|
1947
|
+
}
|
|
1948
|
+
await mkdir(dirname(outAbs), { recursive: true });
|
|
1949
|
+
await writeFile(outAbs, doc, "utf8");
|
|
1950
|
+
const check = await runCheck({
|
|
1951
|
+
patterns: [relative(opts.cwd, outAbs) || opts.out],
|
|
1952
|
+
cwd: opts.cwd,
|
|
1953
|
+
docsRoot: dirname(relative(opts.cwd, outAbs)) || "."
|
|
1954
|
+
});
|
|
1955
|
+
return { exitCode: check.exitCode, outPath: outAbs, entities, relations, check };
|
|
1956
|
+
}
|
|
1957
|
+
function parseDelimiter(raw) {
|
|
1958
|
+
if (raw === void 0) return void 0;
|
|
1959
|
+
if (raw === "," || raw === ";" || raw === " ") return raw;
|
|
1960
|
+
if (raw === "tab" || raw === "\\t") return " ";
|
|
1961
|
+
return null;
|
|
1962
|
+
}
|
|
1963
|
+
function titleFromFile(file) {
|
|
1964
|
+
const stem = basename(file, extname(file));
|
|
1965
|
+
const spaced = stem.replace(/[-_]+/g, " ").trim();
|
|
1966
|
+
return spaced === "" ? "Imported schema" : spaced.replace(/^\w/, (c) => c.toUpperCase());
|
|
1967
|
+
}
|
|
1968
|
+
var warnMessages = (diags) => diags.filter((d) => d.level === "warn").map((d) => d.message);
|
|
1969
|
+
var errorMessages = (diags) => diags.filter((d) => d.level === "error").map((d) => d.message);
|
|
1970
|
+
async function runSyncCsv(opts) {
|
|
1971
|
+
const fallback = opts.block ?? "table";
|
|
1972
|
+
const delimiter = parseDelimiter(opts.delimiter);
|
|
1973
|
+
if (delimiter === null) {
|
|
1974
|
+
return {
|
|
1975
|
+
exitCode: 2,
|
|
1976
|
+
block: fallback,
|
|
1977
|
+
warnings: [],
|
|
1978
|
+
message: `chiltepin sync csv: unknown delimiter ${JSON.stringify(opts.delimiter)} \u2014 use "," ";" or "tab"`
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
const fileAbs = resolve(opts.cwd, opts.file);
|
|
1982
|
+
if (!existsSync(fileAbs)) {
|
|
1983
|
+
return { exitCode: 2, block: fallback, warnings: [], message: `CSV not found: ${fileAbs}` };
|
|
1984
|
+
}
|
|
1985
|
+
const csv = await readFile(fileAbs, "utf8");
|
|
1986
|
+
let block;
|
|
1987
|
+
let reason;
|
|
1988
|
+
if (opts.block !== void 0) {
|
|
1989
|
+
block = opts.block;
|
|
1990
|
+
} else {
|
|
1991
|
+
const suggestion = suggestCsvImport(csv);
|
|
1992
|
+
block = suggestion.kind;
|
|
1993
|
+
reason = suggestion.reason;
|
|
1994
|
+
}
|
|
1995
|
+
const convertOpts = delimiter !== void 0 ? { delimiter } : {};
|
|
1996
|
+
const result = block === "statustable" ? csvToStatustable(csv, convertOpts) : block === "chart" ? csvToChart(csv, convertOpts) : csvToTable(csv, convertOpts);
|
|
1997
|
+
const warnings = warnMessages(result.diagnostics);
|
|
1998
|
+
if (result.data === null) {
|
|
1999
|
+
const errors = errorMessages(result.diagnostics);
|
|
2000
|
+
return {
|
|
2001
|
+
exitCode: 1,
|
|
2002
|
+
block,
|
|
2003
|
+
...reason !== void 0 ? { reason } : {},
|
|
2004
|
+
warnings,
|
|
2005
|
+
message: `Could not import ${opts.file} as ${block}: ${errors.join("; ")}`
|
|
2006
|
+
};
|
|
2007
|
+
}
|
|
2008
|
+
const parseErrors = errorMessages(result.diagnostics);
|
|
2009
|
+
if (parseErrors.length > 0) {
|
|
2010
|
+
return {
|
|
2011
|
+
exitCode: 1,
|
|
2012
|
+
block,
|
|
2013
|
+
...reason !== void 0 ? { reason } : {},
|
|
2014
|
+
warnings,
|
|
2015
|
+
message: `CSV parse failed for ${opts.file}: ${parseErrors.join("; ")}`
|
|
2016
|
+
};
|
|
2017
|
+
}
|
|
2018
|
+
if (opts.out === void 0) {
|
|
2019
|
+
return {
|
|
2020
|
+
exitCode: 0,
|
|
2021
|
+
block,
|
|
2022
|
+
...reason !== void 0 ? { reason } : {},
|
|
2023
|
+
fence: result.fence,
|
|
2024
|
+
warnings
|
|
2025
|
+
};
|
|
2026
|
+
}
|
|
2027
|
+
const title = opts.title ?? titleFromFile(opts.file);
|
|
2028
|
+
const doc = "```meta\ntitle: " + JSON.stringify(title) + "\n```\n\n" + result.fence;
|
|
2029
|
+
const outAbs = resolve(opts.cwd, opts.out);
|
|
2030
|
+
const refusal = overwriteRefusal(outAbs, { force: opts.force });
|
|
2031
|
+
if (refusal !== void 0) {
|
|
2032
|
+
return {
|
|
2033
|
+
exitCode: 1,
|
|
2034
|
+
block,
|
|
2035
|
+
...reason !== void 0 ? { reason } : {},
|
|
2036
|
+
warnings,
|
|
2037
|
+
message: refusal
|
|
2038
|
+
};
|
|
2039
|
+
}
|
|
2040
|
+
await mkdir(dirname(outAbs), { recursive: true });
|
|
2041
|
+
await writeFile(outAbs, doc, "utf8");
|
|
2042
|
+
const check = await runCheck({
|
|
2043
|
+
patterns: [relative(opts.cwd, outAbs) || opts.out],
|
|
2044
|
+
cwd: opts.cwd,
|
|
2045
|
+
docsRoot: dirname(relative(opts.cwd, outAbs)) || "."
|
|
2046
|
+
});
|
|
2047
|
+
return {
|
|
2048
|
+
exitCode: check.exitCode,
|
|
2049
|
+
block,
|
|
2050
|
+
...reason !== void 0 ? { reason } : {},
|
|
2051
|
+
outPath: outAbs,
|
|
2052
|
+
warnings,
|
|
2053
|
+
check
|
|
2054
|
+
};
|
|
2055
|
+
}
|
|
2056
|
+
function templateFor(type) {
|
|
2057
|
+
return `\`\`\`meta
|
|
2058
|
+
title: New document
|
|
2059
|
+
tag: DRAFT
|
|
2060
|
+
\`\`\`
|
|
2061
|
+
|
|
2062
|
+
${BLOCK_TEMPLATES[type]}`;
|
|
2063
|
+
}
|
|
2064
|
+
async function writeNewDoc(opts) {
|
|
2065
|
+
const outAbs = resolve(opts.cwd, opts.out);
|
|
2066
|
+
assertWritable(outAbs, opts.force === true ? { force: true } : {});
|
|
2067
|
+
await mkdir(dirname(outAbs), { recursive: true });
|
|
2068
|
+
const content = isDocTemplate(opts.type) ? DOC_TEMPLATES[opts.type] : templateFor(opts.type);
|
|
2069
|
+
await writeFile(outAbs, content, "utf8");
|
|
2070
|
+
return outAbs;
|
|
2071
|
+
}
|
|
2072
|
+
var DOCS_SECTION = "__docs__";
|
|
2073
|
+
function NewPickerApp({ onPick }) {
|
|
2074
|
+
const { exit } = useApp();
|
|
2075
|
+
const [section, setSection] = useState("top");
|
|
2076
|
+
useInput((input, key) => {
|
|
2077
|
+
if (input === "q" || key.escape) exit();
|
|
2078
|
+
});
|
|
2079
|
+
if (section === "top") {
|
|
2080
|
+
const items2 = [
|
|
2081
|
+
{
|
|
2082
|
+
label: `Doc templates \u2014 a complete starting doc (${Object.keys(DOC_TEMPLATES).length})`,
|
|
2083
|
+
value: DOCS_SECTION
|
|
2084
|
+
},
|
|
2085
|
+
...BLOCK_FAMILIES.map((f) => ({
|
|
2086
|
+
label: `Blocks \xB7 ${f.label} \u2014 ${familyBlocks(f.id).length} scaffolds`,
|
|
2087
|
+
value: f.id
|
|
2088
|
+
}))
|
|
2089
|
+
];
|
|
2090
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2091
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
2092
|
+
"What do you want to create? ",
|
|
2093
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(\u2191\u2193 move \xB7 enter \xB7 q to quit)" })
|
|
2094
|
+
] }),
|
|
2095
|
+
/* @__PURE__ */ jsx(
|
|
2096
|
+
SelectInput2,
|
|
2097
|
+
{
|
|
2098
|
+
items: items2,
|
|
2099
|
+
onSelect: (item) => setSection(item.value)
|
|
2100
|
+
}
|
|
2101
|
+
)
|
|
2102
|
+
] });
|
|
2103
|
+
}
|
|
2104
|
+
const pick = (name) => {
|
|
2105
|
+
onPick(name);
|
|
2106
|
+
exit();
|
|
2107
|
+
};
|
|
2108
|
+
if (section === DOCS_SECTION) {
|
|
2109
|
+
const items2 = Object.keys(DOC_TEMPLATES).map((name) => {
|
|
2110
|
+
const info = DOC_TEMPLATE_INFO[name];
|
|
2111
|
+
return { label: `${(info?.title ?? name).padEnd(14)} ${info?.description ?? ""}`, value: name };
|
|
2112
|
+
});
|
|
2113
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2114
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
2115
|
+
"Doc templates ",
|
|
2116
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(\u2191\u2193 move \xB7 enter \xB7 q to quit)" })
|
|
2117
|
+
] }),
|
|
2118
|
+
/* @__PURE__ */ jsx(SelectInput2, { items: items2, onSelect: (item) => pick(item.value) })
|
|
2119
|
+
] });
|
|
2120
|
+
}
|
|
2121
|
+
const items = familyBlocks(section).map((t) => ({
|
|
2122
|
+
label: `${t.padEnd(13)} ${BLOCK_DESCRIPTIONS[t]}`,
|
|
2123
|
+
value: t
|
|
2124
|
+
}));
|
|
2125
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2126
|
+
/* @__PURE__ */ jsxs(Text, { bold: true, children: [
|
|
2127
|
+
"Blocks \xB7 ",
|
|
2128
|
+
BLOCK_FAMILIES.find((f) => f.id === section)?.label ?? section,
|
|
2129
|
+
" ",
|
|
2130
|
+
/* @__PURE__ */ jsx(Text, { dimColor: true, children: "(\u2191\u2193 move \xB7 enter \xB7 q to quit)" })
|
|
2131
|
+
] }),
|
|
2132
|
+
/* @__PURE__ */ jsx(SelectInput2, { items, onSelect: (item) => pick(item.value) })
|
|
2133
|
+
] });
|
|
2134
|
+
}
|
|
2135
|
+
async function projectStatus(cwd) {
|
|
2136
|
+
const config = await loadConfig(cwd);
|
|
2137
|
+
const result = await runCheck({
|
|
2138
|
+
patterns: [`${config.docsDir}/**/*.md`],
|
|
2139
|
+
cwd,
|
|
2140
|
+
docsRoot: config.docsDir
|
|
2141
|
+
});
|
|
2142
|
+
const errors = result.diagnostics.filter((d) => d.level === "error").length;
|
|
2143
|
+
return {
|
|
2144
|
+
docCount: result.files.length,
|
|
2145
|
+
errors,
|
|
2146
|
+
warnings: result.diagnostics.length - errors,
|
|
2147
|
+
docsDir: config.docsDir
|
|
2148
|
+
};
|
|
2149
|
+
}
|
|
2150
|
+
var NEXT_ACTIONS = [
|
|
2151
|
+
["chiltepin check", "validate every doc"],
|
|
2152
|
+
["chiltepin <file.md>", "render + open one doc in the browser"],
|
|
2153
|
+
["chiltepin studio", "edit visually \xB7 Site mode previews the docs site live"],
|
|
2154
|
+
["chiltepin build", "build the static docs site"],
|
|
2155
|
+
["chiltepin block", "every block type, or one block's fields + example"]
|
|
2156
|
+
];
|
|
2157
|
+
function formatStatus(status, plain = false) {
|
|
2158
|
+
const dim = (s) => plain ? s : pc5.dim(s);
|
|
2159
|
+
const cyan = (s) => plain ? s : pc5.cyan(s);
|
|
2160
|
+
const bold = (s) => plain ? s : pc5.bold(s);
|
|
2161
|
+
const checkLine = status.errors > 0 ? (plain ? "" : pc5.red("\u2717 ")) + `${status.errors} error(s)` + (status.warnings > 0 ? ` \xB7 ${status.warnings} warning(s)` : "") + dim(" \u2014 run chiltepin check") : (plain ? "" : pc5.green("\u2713 ")) + "clean" + (status.warnings > 0 ? dim(` (${status.warnings} warning(s))`) : "");
|
|
2162
|
+
const width = Math.max(...NEXT_ACTIONS.map(([cmd]) => cmd.length));
|
|
2163
|
+
return [
|
|
2164
|
+
` ${dim("docs".padEnd(8))}${status.docCount} document(s) ${dim(`in ${status.docsDir}/`)}`,
|
|
2165
|
+
` ${dim("check".padEnd(8))}${checkLine}`,
|
|
2166
|
+
"",
|
|
2167
|
+
` ${bold("Next:")}`,
|
|
2168
|
+
...NEXT_ACTIONS.map(([cmd, note]) => ` ${cyan(cmd.padEnd(width))} ${dim(note)}`),
|
|
2169
|
+
""
|
|
2170
|
+
].join("\n");
|
|
2171
|
+
}
|
|
2172
|
+
var FILE_CAP = 2e3;
|
|
2173
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2174
|
+
"node_modules",
|
|
2175
|
+
".git",
|
|
2176
|
+
"dist",
|
|
2177
|
+
"build",
|
|
2178
|
+
".scratch",
|
|
2179
|
+
"graphify-out",
|
|
2180
|
+
"coverage",
|
|
2181
|
+
".next",
|
|
2182
|
+
".turbo",
|
|
2183
|
+
"__pycache__",
|
|
2184
|
+
".venv",
|
|
2185
|
+
"venv",
|
|
2186
|
+
"vendor",
|
|
2187
|
+
"target",
|
|
2188
|
+
"out"
|
|
2189
|
+
]);
|
|
2190
|
+
var CODE_EXTS = /* @__PURE__ */ new Set(["ts", "tsx", "js", "jsx", "mjs", "cjs", "mts", "cts", "py", "go", "rb"]);
|
|
2191
|
+
function langOf(ext) {
|
|
2192
|
+
switch (ext) {
|
|
2193
|
+
case "tsx":
|
|
2194
|
+
case "mts":
|
|
2195
|
+
case "cts":
|
|
2196
|
+
return "ts";
|
|
2197
|
+
case "jsx":
|
|
2198
|
+
case "mjs":
|
|
2199
|
+
case "cjs":
|
|
2200
|
+
return "js";
|
|
2201
|
+
case "yml":
|
|
2202
|
+
return "yaml";
|
|
2203
|
+
default:
|
|
2204
|
+
return ext;
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
function extOf(name) {
|
|
2208
|
+
const i = name.lastIndexOf(".");
|
|
2209
|
+
if (i <= 0) return "";
|
|
2210
|
+
return name.slice(i + 1).toLowerCase();
|
|
2211
|
+
}
|
|
2212
|
+
function rel(root, abs) {
|
|
2213
|
+
return relative(root, abs).split(sep).join("/");
|
|
2214
|
+
}
|
|
2215
|
+
async function walk(root) {
|
|
2216
|
+
const files = [];
|
|
2217
|
+
const queue = [root];
|
|
2218
|
+
let truncated = false;
|
|
2219
|
+
while (queue.length > 0 && !truncated) {
|
|
2220
|
+
const dir = queue.shift();
|
|
2221
|
+
let entries;
|
|
2222
|
+
try {
|
|
2223
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
2224
|
+
} catch {
|
|
2225
|
+
continue;
|
|
2226
|
+
}
|
|
2227
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
2228
|
+
for (const entry of entries) {
|
|
2229
|
+
const abs = join(dir, entry.name);
|
|
2230
|
+
if (entry.isDirectory()) {
|
|
2231
|
+
if (!entry.name.startsWith(".") && !SKIP_DIRS.has(entry.name)) queue.push(abs);
|
|
2232
|
+
continue;
|
|
2233
|
+
}
|
|
2234
|
+
if (!entry.isFile()) continue;
|
|
2235
|
+
if (files.length >= FILE_CAP) {
|
|
2236
|
+
truncated = true;
|
|
2237
|
+
break;
|
|
2238
|
+
}
|
|
2239
|
+
files.push({ abs, rel: rel(root, abs), ext: extOf(entry.name) });
|
|
2240
|
+
}
|
|
2241
|
+
}
|
|
2242
|
+
return { files, truncated };
|
|
2243
|
+
}
|
|
2244
|
+
var KNOWN_EXTERNALS = [
|
|
2245
|
+
"axios",
|
|
2246
|
+
"node-fetch",
|
|
2247
|
+
"got",
|
|
2248
|
+
"undici",
|
|
2249
|
+
"ky",
|
|
2250
|
+
"stripe",
|
|
2251
|
+
"openai",
|
|
2252
|
+
"@anthropic-ai/",
|
|
2253
|
+
"@aws-sdk/",
|
|
2254
|
+
"aws-sdk",
|
|
2255
|
+
"@octokit/",
|
|
2256
|
+
"twilio",
|
|
2257
|
+
"@sendgrid/",
|
|
2258
|
+
"@supabase/",
|
|
2259
|
+
"firebase",
|
|
2260
|
+
"pg",
|
|
2261
|
+
"mysql2",
|
|
2262
|
+
"mongodb",
|
|
2263
|
+
"mongoose",
|
|
2264
|
+
"redis",
|
|
2265
|
+
"ioredis",
|
|
2266
|
+
"@prisma/client",
|
|
2267
|
+
// Python
|
|
2268
|
+
"requests",
|
|
2269
|
+
"httpx",
|
|
2270
|
+
"boto3",
|
|
2271
|
+
"anthropic"
|
|
2272
|
+
];
|
|
2273
|
+
var TEST_SEGMENT_RE = /(^|\/)(__tests__|__fixtures__|__mocks__)(\/|$)/;
|
|
2274
|
+
var TEST_FILE_RE = /\.(test|spec)\./i;
|
|
2275
|
+
function isTestPath(rel2) {
|
|
2276
|
+
return TEST_SEGMENT_RE.test(rel2) || TEST_FILE_RE.test(posix.basename(rel2));
|
|
2277
|
+
}
|
|
2278
|
+
function importSpecifiers(text) {
|
|
2279
|
+
const out = [];
|
|
2280
|
+
const re = /(?:\bfrom\s+|\bimport\s+|\brequire\(\s*)['"]([^'"\n]+)['"]/g;
|
|
2281
|
+
for (const m of text.matchAll(re)) out.push(m[1]);
|
|
2282
|
+
return out;
|
|
2283
|
+
}
|
|
2284
|
+
var JS_ROUTE_RE = /\b(?:app|router|server|fastify|api)\s*\.\s*(get|post|put|patch|delete|options|head)\s*\(\s*['"`]([^'"`\n]+)['"`]/g;
|
|
2285
|
+
var FASTAPI_ROUTE_RE = /@\w+\.(get|post|put|patch|delete)\(\s*['"]([^'"\n]+)['"]/g;
|
|
2286
|
+
var FLASK_ROUTE_RE = /@\w+\.route\(\s*['"]([^'"\n]+)['"]([^)\n]*)/g;
|
|
2287
|
+
async function collectBuiltin(root) {
|
|
2288
|
+
const { files, truncated } = await walk(root);
|
|
2289
|
+
const languages = {};
|
|
2290
|
+
for (const f of files) {
|
|
2291
|
+
if (f.ext === "") continue;
|
|
2292
|
+
if (f.ext.length > 6) continue;
|
|
2293
|
+
const lang = langOf(f.ext);
|
|
2294
|
+
languages[lang] = (languages[lang] ?? 0) + 1;
|
|
2295
|
+
}
|
|
2296
|
+
const entrypoints = [];
|
|
2297
|
+
const routes = [];
|
|
2298
|
+
const schemas = [];
|
|
2299
|
+
const packages = [];
|
|
2300
|
+
const externals = /* @__PURE__ */ new Map();
|
|
2301
|
+
const composeServices = [];
|
|
2302
|
+
const importers = /* @__PURE__ */ new Map();
|
|
2303
|
+
const seenEntry = /* @__PURE__ */ new Set();
|
|
2304
|
+
let readmeLines;
|
|
2305
|
+
let readmeFile;
|
|
2306
|
+
const addEntry = (file, why) => {
|
|
2307
|
+
if (seenEntry.has(file)) return;
|
|
2308
|
+
seenEntry.add(file);
|
|
2309
|
+
entrypoints.push({ file, why });
|
|
2310
|
+
};
|
|
2311
|
+
const fileSet = new Set(files.map((f) => f.rel));
|
|
2312
|
+
for (const f of files) {
|
|
2313
|
+
if (isTestPath(f.rel)) continue;
|
|
2314
|
+
const base = posix.basename(f.rel);
|
|
2315
|
+
if (f.ext === "prisma") schemas.push({ file: f.rel, kind: "prisma" });
|
|
2316
|
+
else if (f.ext === "proto") schemas.push({ file: f.rel, kind: "proto" });
|
|
2317
|
+
else if (f.ext === "sql" && /(^|\/)migrations?\//.test(f.rel))
|
|
2318
|
+
schemas.push({ file: f.rel, kind: "sql" });
|
|
2319
|
+
else if (/^(openapi|swagger)\./i.test(base) && ["yaml", "yml", "json"].includes(f.ext))
|
|
2320
|
+
schemas.push({ file: f.rel, kind: "openapi" });
|
|
2321
|
+
else if (/^drizzle\.config\./.test(base)) schemas.push({ file: f.rel, kind: "other" });
|
|
2322
|
+
if (/^src\/index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(f.rel)) addEntry(f.rel, "src/index convention");
|
|
2323
|
+
if (/^(main|app|manage)\.py$/.test(f.rel)) addEntry(f.rel, "python entry convention");
|
|
2324
|
+
if (/^cmd\/[^/]+\/main\.go$/.test(f.rel)) addEntry(f.rel, "Go cmd/ convention");
|
|
2325
|
+
if (f.rel.toLowerCase() === "readme.md") {
|
|
2326
|
+
try {
|
|
2327
|
+
const text2 = await readFile(f.abs, "utf8");
|
|
2328
|
+
const lines = text2.split("\n");
|
|
2329
|
+
if (lines[lines.length - 1] === "") lines.pop();
|
|
2330
|
+
readmeLines = lines.length;
|
|
2331
|
+
readmeFile = f.rel;
|
|
2332
|
+
} catch {
|
|
2333
|
+
}
|
|
2334
|
+
}
|
|
2335
|
+
if (base === "package.json") {
|
|
2336
|
+
try {
|
|
2337
|
+
const pkg = JSON.parse(await readFile(f.abs, "utf8"));
|
|
2338
|
+
const dir = posix.dirname(f.rel);
|
|
2339
|
+
if (typeof pkg["name"] === "string") {
|
|
2340
|
+
packages.push({ name: pkg["name"], dir: dir === "." ? "." : dir });
|
|
2341
|
+
}
|
|
2342
|
+
const inPkg = (p) => posix.normalize(posix.join(dir === "." ? "" : dir, p));
|
|
2343
|
+
if (typeof pkg["main"] === "string") addEntry(inPkg(pkg["main"]), "package.json main");
|
|
2344
|
+
const bin = pkg["bin"];
|
|
2345
|
+
if (typeof bin === "string") addEntry(inPkg(bin), "package.json bin");
|
|
2346
|
+
else if (bin !== null && typeof bin === "object") {
|
|
2347
|
+
for (const v of Object.values(bin)) {
|
|
2348
|
+
if (typeof v === "string") addEntry(inPkg(v), "package.json bin");
|
|
2349
|
+
}
|
|
2350
|
+
}
|
|
2351
|
+
const scripts = pkg["scripts"];
|
|
2352
|
+
if (scripts !== null && typeof scripts === "object") {
|
|
2353
|
+
const start = scripts["start"];
|
|
2354
|
+
if (typeof start === "string") {
|
|
2355
|
+
const m = start.match(/(\S+\.(?:[mc]?[jt]sx?|py))\b/);
|
|
2356
|
+
if (m !== null) addEntry(inPkg(m[1]), "package.json start script");
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
} catch {
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
if (/^(docker-)?compose(\.[\w-]+)?\.ya?ml$/.test(base) && posix.dirname(f.rel) === ".") {
|
|
2363
|
+
try {
|
|
2364
|
+
const lines = (await readFile(f.abs, "utf8")).split("\n");
|
|
2365
|
+
const start = lines.findIndex((l) => /^services:\s*$/.test(l));
|
|
2366
|
+
if (start >= 0) {
|
|
2367
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
2368
|
+
const line = lines[i];
|
|
2369
|
+
if (/^\S/.test(line)) break;
|
|
2370
|
+
const m = line.match(/^ {2}([\w-]+):\s*$/);
|
|
2371
|
+
if (m !== null) composeServices.push({ name: m[1], file: f.rel });
|
|
2372
|
+
}
|
|
2373
|
+
}
|
|
2374
|
+
} catch {
|
|
2375
|
+
}
|
|
2376
|
+
}
|
|
2377
|
+
if (!CODE_EXTS.has(f.ext)) continue;
|
|
2378
|
+
let text;
|
|
2379
|
+
try {
|
|
2380
|
+
text = await readFile(f.abs, "utf8");
|
|
2381
|
+
} catch {
|
|
2382
|
+
continue;
|
|
2383
|
+
}
|
|
2384
|
+
for (const m of text.matchAll(JS_ROUTE_RE)) {
|
|
2385
|
+
routes.push({ method: m[1].toUpperCase(), path: m[2], file: f.rel });
|
|
2386
|
+
}
|
|
2387
|
+
if (f.ext === "py") {
|
|
2388
|
+
for (const m of text.matchAll(FASTAPI_ROUTE_RE)) {
|
|
2389
|
+
routes.push({ method: m[1].toUpperCase(), path: m[2], file: f.rel });
|
|
2390
|
+
}
|
|
2391
|
+
for (const m of text.matchAll(FLASK_ROUTE_RE)) {
|
|
2392
|
+
const methods = m[2].match(/methods\s*=\s*\[([^\]]*)\]/);
|
|
2393
|
+
const list = methods === null ? ["GET"] : methods[1].split(",").map((s) => s.replace(/['"\s]/g, "")).filter((s) => s !== "");
|
|
2394
|
+
for (const method of list) {
|
|
2395
|
+
routes.push({ method: method.toUpperCase(), path: m[1], file: f.rel });
|
|
2396
|
+
}
|
|
2397
|
+
}
|
|
2398
|
+
for (const m of text.matchAll(/^(?:import|from)\s+([\w.]+)/gm)) {
|
|
2399
|
+
const mod = m[1].split(".")[0];
|
|
2400
|
+
if (KNOWN_EXTERNALS.includes(mod) && !externals.has(mod)) {
|
|
2401
|
+
externals.set(mod, { name: mod, file: f.rel });
|
|
2402
|
+
}
|
|
2403
|
+
}
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
if (/\bfetch\s*\(/.test(text) && !externals.has("fetch")) {
|
|
2407
|
+
externals.set("fetch", { name: "fetch", file: f.rel });
|
|
2408
|
+
}
|
|
2409
|
+
for (const spec of importSpecifiers(text)) {
|
|
2410
|
+
if (spec.startsWith(".")) {
|
|
2411
|
+
const target = posix.normalize(posix.join(posix.dirname(f.rel), spec)).replace(/\.(ts|tsx|js|jsx|mjs|cjs|mts|cts)$/, "").replace(/\/index$/, "");
|
|
2412
|
+
let set = importers.get(target);
|
|
2413
|
+
if (set === void 0) {
|
|
2414
|
+
set = /* @__PURE__ */ new Set();
|
|
2415
|
+
importers.set(target, set);
|
|
2416
|
+
}
|
|
2417
|
+
set.add(f.rel);
|
|
2418
|
+
continue;
|
|
2419
|
+
}
|
|
2420
|
+
const hit = KNOWN_EXTERNALS.find(
|
|
2421
|
+
(k) => k.endsWith("/") ? spec.startsWith(k) : spec === k || spec.startsWith(`${k}/`)
|
|
2422
|
+
);
|
|
2423
|
+
if (hit !== void 0) {
|
|
2424
|
+
const name = hit.endsWith("/") ? spec : hit;
|
|
2425
|
+
if (!externals.has(name)) externals.set(name, { name, file: f.rel });
|
|
2426
|
+
}
|
|
2427
|
+
}
|
|
2428
|
+
}
|
|
2429
|
+
for (const f of files) {
|
|
2430
|
+
if (isTestPath(f.rel)) continue;
|
|
2431
|
+
const api = f.rel.match(/(?:^|\/)pages\/api\/(.+)\.(?:[jt]sx?)$/);
|
|
2432
|
+
if (api !== null) {
|
|
2433
|
+
const p = `/${api[1].replace(/\/index$/, "")}`;
|
|
2434
|
+
routes.push({ method: "ANY", path: `/api${p === "/index" ? "" : p}`, file: f.rel });
|
|
2435
|
+
continue;
|
|
2436
|
+
}
|
|
2437
|
+
const appRoute = f.rel.match(/(?:^|\/)app\/(.*?)route\.(?:[jt]s)$/);
|
|
2438
|
+
if (appRoute !== null) {
|
|
2439
|
+
const p = `/${appRoute[1].replace(/\/$/, "")}`;
|
|
2440
|
+
routes.push({ method: "ANY", path: p === "/" ? "/" : p, file: f.rel });
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
const godNodes = [...importers.entries()].map(([target, set]) => ({ target, degree: set.size })).filter((g) => g.degree >= 3).sort((a, b) => b.degree - a.degree || a.target.localeCompare(b.target)).slice(0, 10).map((g) => {
|
|
2444
|
+
const candidates = [
|
|
2445
|
+
g.target,
|
|
2446
|
+
...["ts", "tsx", "js", "jsx", "mjs", "cjs"].flatMap((e) => [
|
|
2447
|
+
`${g.target}.${e}`,
|
|
2448
|
+
`${g.target}/index.${e}`
|
|
2449
|
+
])
|
|
2450
|
+
];
|
|
2451
|
+
const file = candidates.find((c) => fileSet.has(c)) ?? g.target;
|
|
2452
|
+
return { name: g.target, degree: g.degree, file };
|
|
2453
|
+
});
|
|
2454
|
+
const stats = { files: files.length, languages };
|
|
2455
|
+
return {
|
|
2456
|
+
stats,
|
|
2457
|
+
evidence: { entrypoints, routes, schemas, packages, externals: [...externals.values()], godNodes },
|
|
2458
|
+
composeServices,
|
|
2459
|
+
readmeLines,
|
|
2460
|
+
readmeFile,
|
|
2461
|
+
truncated
|
|
2462
|
+
};
|
|
2463
|
+
}
|
|
2464
|
+
async function isUsableRoot(p) {
|
|
2465
|
+
try {
|
|
2466
|
+
return (await stat(p)).isDirectory();
|
|
2467
|
+
} catch {
|
|
2468
|
+
return false;
|
|
2469
|
+
}
|
|
2470
|
+
}
|
|
2471
|
+
var GRAPH_PATH = "graphify-out/graph.json";
|
|
2472
|
+
async function loadGraphify(root) {
|
|
2473
|
+
const path = join(root, GRAPH_PATH);
|
|
2474
|
+
if (!existsSync(path)) return { ok: false, missing: true };
|
|
2475
|
+
let parsed;
|
|
2476
|
+
try {
|
|
2477
|
+
parsed = JSON.parse(await readFile(path, "utf8"));
|
|
2478
|
+
} catch (err) {
|
|
2479
|
+
return {
|
|
2480
|
+
ok: false,
|
|
2481
|
+
missing: false,
|
|
2482
|
+
reason: `${GRAPH_PATH} did not parse as JSON (${err.message}).`
|
|
2483
|
+
};
|
|
2484
|
+
}
|
|
2485
|
+
const obj = parsed;
|
|
2486
|
+
if (!Array.isArray(obj.nodes) || !Array.isArray(obj.links)) {
|
|
2487
|
+
return {
|
|
2488
|
+
ok: false,
|
|
2489
|
+
missing: false,
|
|
2490
|
+
reason: `${GRAPH_PATH} has no nodes/links arrays.`
|
|
2491
|
+
};
|
|
2492
|
+
}
|
|
2493
|
+
const n0 = obj.nodes[0];
|
|
2494
|
+
if (n0 !== void 0 && (typeof n0["id"] !== "string" || typeof n0["label"] !== "string")) {
|
|
2495
|
+
return {
|
|
2496
|
+
ok: false,
|
|
2497
|
+
missing: false,
|
|
2498
|
+
reason: `${GRAPH_PATH} nodes lack the id/label fields.`
|
|
2499
|
+
};
|
|
2500
|
+
}
|
|
2501
|
+
const l0 = obj.links[0];
|
|
2502
|
+
if (l0 !== void 0 && (l0["source"] === void 0 || l0["target"] === void 0)) {
|
|
2503
|
+
return {
|
|
2504
|
+
ok: false,
|
|
2505
|
+
missing: false,
|
|
2506
|
+
reason: `${GRAPH_PATH} links lack the source/target fields.`
|
|
2507
|
+
};
|
|
2508
|
+
}
|
|
2509
|
+
return { ok: true, graph: { nodes: obj.nodes, links: obj.links } };
|
|
2510
|
+
}
|
|
2511
|
+
function extOf2(path) {
|
|
2512
|
+
const base = path.slice(path.lastIndexOf("/") + 1);
|
|
2513
|
+
const i = base.lastIndexOf(".");
|
|
2514
|
+
return i <= 0 ? "" : base.slice(i + 1).toLowerCase();
|
|
2515
|
+
}
|
|
2516
|
+
function graphifyStats(graph) {
|
|
2517
|
+
const files = /* @__PURE__ */ new Set();
|
|
2518
|
+
for (const node of graph.nodes) {
|
|
2519
|
+
if (typeof node.source_file === "string" && node.source_file !== "") files.add(node.source_file);
|
|
2520
|
+
}
|
|
2521
|
+
const languages = {};
|
|
2522
|
+
for (const file of files) {
|
|
2523
|
+
const ext = extOf2(file);
|
|
2524
|
+
if (ext === "" || ext.length > 6) continue;
|
|
2525
|
+
languages[ext] = (languages[ext] ?? 0) + 1;
|
|
2526
|
+
}
|
|
2527
|
+
return { files: files.size, languages };
|
|
2528
|
+
}
|
|
2529
|
+
function graphifyGodNodes(graph, top = 10) {
|
|
2530
|
+
const inDegree = /* @__PURE__ */ new Map();
|
|
2531
|
+
for (const link of graph.links) {
|
|
2532
|
+
if (link.relation !== "calls" && link.relation !== "imports") continue;
|
|
2533
|
+
const target = String(link.target);
|
|
2534
|
+
inDegree.set(target, (inDegree.get(target) ?? 0) + 1);
|
|
2535
|
+
}
|
|
2536
|
+
const byId = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
2537
|
+
return [...inDegree.entries()].filter(([, degree]) => degree >= 3).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, top).map(([id, degree]) => {
|
|
2538
|
+
const node = byId.get(id);
|
|
2539
|
+
return {
|
|
2540
|
+
name: node?.label ?? id,
|
|
2541
|
+
degree,
|
|
2542
|
+
file: node?.source_file ?? ""
|
|
2543
|
+
};
|
|
2544
|
+
});
|
|
2545
|
+
}
|
|
2546
|
+
|
|
2547
|
+
// src/commands/audit/rules.ts
|
|
2548
|
+
var CITE_CAP = 8;
|
|
2549
|
+
function cite(files) {
|
|
2550
|
+
return [...new Set(files)].slice(0, CITE_CAP);
|
|
2551
|
+
}
|
|
2552
|
+
function deriveRecommendations(collected, source) {
|
|
2553
|
+
const { evidence, composeServices, readmeLines, readmeFile } = collected;
|
|
2554
|
+
const out = [];
|
|
2555
|
+
const pkgCount = evidence.packages.length;
|
|
2556
|
+
const svcCount = composeServices.length;
|
|
2557
|
+
const entryCount = evidence.entrypoints.length;
|
|
2558
|
+
const archCites = cite([
|
|
2559
|
+
...evidence.packages.map((p) => p.dir === "." ? "package.json" : `${p.dir}/package.json`),
|
|
2560
|
+
...composeServices.map((s) => s.file),
|
|
2561
|
+
...evidence.entrypoints.map((e) => e.file)
|
|
2562
|
+
]);
|
|
2563
|
+
if (pkgCount >= 2 || svcCount >= 2 || entryCount >= 3) {
|
|
2564
|
+
const parts = [];
|
|
2565
|
+
if (pkgCount >= 2) parts.push(`${pkgCount} packages`);
|
|
2566
|
+
if (svcCount >= 2) parts.push(`${svcCount} compose services`);
|
|
2567
|
+
if (entryCount >= 3) parts.push(`${entryCount} entrypoints`);
|
|
2568
|
+
out.push({
|
|
2569
|
+
kind: "architecture-overview",
|
|
2570
|
+
title: "Architecture overview",
|
|
2571
|
+
rationale: `The repo has ${parts.join(" and ")}. An overview shows how they connect.`,
|
|
2572
|
+
confidence: "high",
|
|
2573
|
+
citations: archCites,
|
|
2574
|
+
template: "system-design"
|
|
2575
|
+
});
|
|
2576
|
+
} else if (entryCount >= 1 && evidence.externals.length >= 5) {
|
|
2577
|
+
out.push({
|
|
2578
|
+
kind: "architecture-overview",
|
|
2579
|
+
title: "Architecture overview",
|
|
2580
|
+
rationale: `The code talks to ${evidence.externals.length} external services from ${entryCount} entrypoint(s). An overview maps the boundary.`,
|
|
2581
|
+
confidence: "medium",
|
|
2582
|
+
citations: cite([...archCites, ...evidence.externals.map((e) => e.file)]),
|
|
2583
|
+
template: "system-design"
|
|
2584
|
+
});
|
|
2585
|
+
}
|
|
2586
|
+
if (evidence.schemas.length >= 1) {
|
|
2587
|
+
out.push({
|
|
2588
|
+
kind: "data-model",
|
|
2589
|
+
title: "Data model",
|
|
2590
|
+
rationale: `The repo defines ${evidence.schemas.length} schema file(s). A data-model doc explains the entities.`,
|
|
2591
|
+
confidence: "high",
|
|
2592
|
+
citations: cite(evidence.schemas.map((s) => s.file)),
|
|
2593
|
+
template: "data-model"
|
|
2594
|
+
});
|
|
2595
|
+
}
|
|
2596
|
+
const routeCount = evidence.routes.length;
|
|
2597
|
+
if (routeCount >= 1) {
|
|
2598
|
+
out.push({
|
|
2599
|
+
kind: "request-flows",
|
|
2600
|
+
title: "Request flows",
|
|
2601
|
+
rationale: `The audit found ${routeCount} route(s). A flow doc traces each request path.`,
|
|
2602
|
+
confidence: routeCount >= 3 ? "high" : "low",
|
|
2603
|
+
citations: cite(evidence.routes.map((r) => r.file)),
|
|
2604
|
+
template: "service-overview"
|
|
2605
|
+
});
|
|
2606
|
+
}
|
|
2607
|
+
const openapi = evidence.schemas.filter((s) => s.kind === "openapi");
|
|
2608
|
+
if (openapi.length >= 1) {
|
|
2609
|
+
out.push({
|
|
2610
|
+
kind: "api-reference",
|
|
2611
|
+
title: "API reference",
|
|
2612
|
+
rationale: `An OpenAPI spec exists (${openapi[0]?.file}). An API reference doc can stay in sync with it.`,
|
|
2613
|
+
confidence: "high",
|
|
2614
|
+
citations: cite(openapi.map((s) => s.file)),
|
|
2615
|
+
template: "api-spec"
|
|
2616
|
+
});
|
|
2617
|
+
} else if (routeCount >= 5) {
|
|
2618
|
+
out.push({
|
|
2619
|
+
kind: "api-reference",
|
|
2620
|
+
title: "API reference",
|
|
2621
|
+
rationale: `The audit found ${routeCount} routes and no OpenAPI spec. An API reference documents them.`,
|
|
2622
|
+
confidence: "medium",
|
|
2623
|
+
citations: cite(evidence.routes.map((r) => r.file)),
|
|
2624
|
+
template: "api-spec"
|
|
2625
|
+
});
|
|
2626
|
+
}
|
|
2627
|
+
const godThreshold = source === "graphify" ? 15 : 10;
|
|
2628
|
+
const god = evidence.godNodes.find((g) => g.degree >= godThreshold);
|
|
2629
|
+
if (god !== void 0) {
|
|
2630
|
+
out.push({
|
|
2631
|
+
kind: "dependency-map",
|
|
2632
|
+
title: "Dependency map",
|
|
2633
|
+
rationale: `${god.name} has ${god.degree} inbound dependencies. A dependency map shows what breaks when it changes.`,
|
|
2634
|
+
confidence: "medium",
|
|
2635
|
+
citations: cite([god.file].filter((f) => f !== "")),
|
|
2636
|
+
template: "system-design"
|
|
2637
|
+
});
|
|
2638
|
+
}
|
|
2639
|
+
if (readmeLines === void 0 || readmeLines < 30) {
|
|
2640
|
+
const rationale = readmeLines === void 0 ? "The repo has no README. An onboarding guide gives new contributors a start." : `The README has only ${readmeLines} line(s). An onboarding guide gives new contributors a start.`;
|
|
2641
|
+
out.push({
|
|
2642
|
+
kind: "onboarding-guide",
|
|
2643
|
+
title: "Onboarding guide",
|
|
2644
|
+
rationale,
|
|
2645
|
+
confidence: "medium",
|
|
2646
|
+
citations: cite(
|
|
2647
|
+
[readmeFile, ...evidence.entrypoints.map((e) => e.file)].filter(
|
|
2648
|
+
(f) => f !== void 0
|
|
2649
|
+
)
|
|
2650
|
+
),
|
|
2651
|
+
template: "onboarding"
|
|
2652
|
+
});
|
|
2653
|
+
}
|
|
2654
|
+
return out;
|
|
2655
|
+
}
|
|
2656
|
+
|
|
2657
|
+
// src/commands/audit/run.ts
|
|
2658
|
+
async function runAudit(options) {
|
|
2659
|
+
const root = resolve(options.cwd, options.path ?? ".");
|
|
2660
|
+
if (!await isUsableRoot(root)) {
|
|
2661
|
+
return { ok: false, error: `Cannot audit ${options.path ?? root}: not a directory.` };
|
|
2662
|
+
}
|
|
2663
|
+
const collected = await collectBuiltin(root);
|
|
2664
|
+
const graph = await loadGraphify(root);
|
|
2665
|
+
let source = "builtin";
|
|
2666
|
+
const notices = [];
|
|
2667
|
+
let stats = collected.stats;
|
|
2668
|
+
let evidence = collected.evidence;
|
|
2669
|
+
if (graph.ok) {
|
|
2670
|
+
source = "graphify";
|
|
2671
|
+
stats = graphifyStats(graph.graph);
|
|
2672
|
+
evidence = { ...evidence, godNodes: graphifyGodNodes(graph.graph) };
|
|
2673
|
+
} else if (graph.missing) {
|
|
2674
|
+
notices.push("Install graphify to get a richer audit (call graph, communities).");
|
|
2675
|
+
} else {
|
|
2676
|
+
notices.push(`${graph.reason} The audit used the builtin extractor.`);
|
|
2677
|
+
}
|
|
2678
|
+
if (collected.truncated) {
|
|
2679
|
+
notices.push(`The scan stopped at ${FILE_CAP} files. Counts are lower bounds.`);
|
|
2680
|
+
}
|
|
2681
|
+
const merged = { ...collected, evidence };
|
|
2682
|
+
const recommendations = deriveRecommendations(merged, source);
|
|
2683
|
+
const report = {
|
|
2684
|
+
version: 1,
|
|
2685
|
+
source,
|
|
2686
|
+
...notices.length > 0 ? { notice: notices.join(" ") } : {},
|
|
2687
|
+
stats,
|
|
2688
|
+
evidence,
|
|
2689
|
+
recommendations
|
|
2690
|
+
};
|
|
2691
|
+
return { ok: true, report };
|
|
2692
|
+
}
|
|
2693
|
+
function languagesLine(languages, cap = 6) {
|
|
2694
|
+
const top = Object.entries(languages).sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0])).slice(0, cap);
|
|
2695
|
+
return top.map(([lang, n]) => `${lang} ${n}`).join(" \xB7 ");
|
|
2696
|
+
}
|
|
2697
|
+
function formatAudit(report, plain = false) {
|
|
2698
|
+
const dim = (s) => plain ? s : pc5.dim(s);
|
|
2699
|
+
const bold = (s) => plain ? s : pc5.bold(s);
|
|
2700
|
+
const cyan = (s) => plain ? s : pc5.cyan(s);
|
|
2701
|
+
const paint = (confidence) => {
|
|
2702
|
+
if (plain) return confidence;
|
|
2703
|
+
if (confidence === "high") return pc5.green(confidence);
|
|
2704
|
+
if (confidence === "medium") return pc5.yellow(confidence);
|
|
2705
|
+
return pc5.dim(confidence);
|
|
2706
|
+
};
|
|
2707
|
+
const lines = [];
|
|
2708
|
+
const langs = languagesLine(report.stats.languages);
|
|
2709
|
+
lines.push(` ${dim("source".padEnd(8))}${report.source}`);
|
|
2710
|
+
lines.push(
|
|
2711
|
+
` ${dim("files".padEnd(8))}${report.stats.files}${langs === "" ? "" : dim(` \xB7 ${langs}`)}`
|
|
2712
|
+
);
|
|
2713
|
+
const e = report.evidence;
|
|
2714
|
+
const counts = [
|
|
2715
|
+
`${e.entrypoints.length} entrypoint(s)`,
|
|
2716
|
+
`${e.routes.length} route(s)`,
|
|
2717
|
+
`${e.schemas.length} schema(s)`,
|
|
2718
|
+
`${e.packages.length} package(s)`,
|
|
2719
|
+
`${e.externals.length} external(s)`,
|
|
2720
|
+
`${e.godNodes.length} god node(s)`
|
|
2721
|
+
].join(" \xB7 ");
|
|
2722
|
+
lines.push(` ${dim("found".padEnd(8))}${counts}`);
|
|
2723
|
+
lines.push("");
|
|
2724
|
+
if (report.recommendations.length === 0) {
|
|
2725
|
+
lines.push(` ${bold("No recommendations.")} ${dim("The audit found too little evidence.")}`);
|
|
2726
|
+
} else {
|
|
2727
|
+
lines.push(` ${bold("Recommended docs:")}`);
|
|
2728
|
+
const kindWidth = Math.max(...report.recommendations.map((r) => r.kind.length));
|
|
2729
|
+
for (const r of report.recommendations) {
|
|
2730
|
+
lines.push(
|
|
2731
|
+
` ${paint(r.confidence.padEnd(7))} ${cyan(r.kind.padEnd(kindWidth))} ${r.rationale} ${dim(`(${r.citations.length} citation(s))`)}`
|
|
2732
|
+
);
|
|
2733
|
+
}
|
|
2734
|
+
}
|
|
2735
|
+
lines.push("");
|
|
2736
|
+
lines.push(` ${dim("Next:")} run ${cyan("/chiltepin audit")} in Claude Code to generate the docs you pick.`);
|
|
2737
|
+
if (report.notice !== void 0) lines.push(` ${dim(`note: ${report.notice}`)}`);
|
|
2738
|
+
lines.push("");
|
|
2739
|
+
return lines.join("\n");
|
|
2740
|
+
}
|
|
2741
|
+
function renderCodeFrame(input) {
|
|
2742
|
+
const { lines, line } = input;
|
|
2743
|
+
if (line < 1 || line > lines.length) return "";
|
|
2744
|
+
const contextBefore = input.contextBefore ?? 1;
|
|
2745
|
+
const start = Math.max(1, line - contextBefore);
|
|
2746
|
+
const gutterWidth = String(line).length + 1;
|
|
2747
|
+
const tint = input.level === "warn" ? pc5.yellow : pc5.red;
|
|
2748
|
+
const out = [];
|
|
2749
|
+
for (let n = start; n <= line; n++) {
|
|
2750
|
+
const text = lines[n - 1] ?? "";
|
|
2751
|
+
const gutter = pc5.dim(`${String(n).padStart(gutterWidth)} | `);
|
|
2752
|
+
out.push(gutter + text);
|
|
2753
|
+
}
|
|
2754
|
+
if (input.column !== void 0 && input.column >= 1) {
|
|
2755
|
+
const col = input.column;
|
|
2756
|
+
const span = input.endColumn !== void 0 ? Math.max(1, input.endColumn - col) : 1;
|
|
2757
|
+
const pad = " ".repeat(gutterWidth) + pc5.dim(" | ") + " ".repeat(col - 1);
|
|
2758
|
+
out.push(pad + tint("^".repeat(span)));
|
|
2759
|
+
}
|
|
2760
|
+
return out.join("\n");
|
|
2761
|
+
}
|
|
2762
|
+
var COLOR = {
|
|
2763
|
+
error: "red",
|
|
2764
|
+
warn: "yellow"
|
|
2765
|
+
};
|
|
2766
|
+
function frameFor(d, sources) {
|
|
2767
|
+
if (d.line === void 0) return "";
|
|
2768
|
+
const lines = sources.get(d.file);
|
|
2769
|
+
if (lines === void 0) return "";
|
|
2770
|
+
return renderCodeFrame({
|
|
2771
|
+
lines,
|
|
2772
|
+
line: d.line,
|
|
2773
|
+
...d.column !== void 0 ? { column: d.column } : {},
|
|
2774
|
+
...d.endColumn !== void 0 ? { endColumn: d.endColumn } : {},
|
|
2775
|
+
level: d.level
|
|
2776
|
+
});
|
|
2777
|
+
}
|
|
2778
|
+
function DiagnosticsTable({ diagnostics, fileCount, sources }) {
|
|
2779
|
+
const errorCount = diagnostics.filter((d) => d.level === "error").length;
|
|
2780
|
+
const warnCount = diagnostics.filter((d) => d.level === "warn").length;
|
|
2781
|
+
if (diagnostics.length === 0) {
|
|
2782
|
+
return /* @__PURE__ */ jsx(Box, { flexDirection: "column", children: /* @__PURE__ */ jsxs(Text, { color: "green", children: [
|
|
2783
|
+
"\u2713 ",
|
|
2784
|
+
fileCount,
|
|
2785
|
+
" ",
|
|
2786
|
+
fileCount === 1 ? "file" : "files",
|
|
2787
|
+
" checked \u2014 no diagnostics"
|
|
2788
|
+
] }) });
|
|
2789
|
+
}
|
|
2790
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", children: [
|
|
2791
|
+
diagnostics.map((d, i) => {
|
|
2792
|
+
const frame = frameFor(d, sources);
|
|
2793
|
+
return /* @__PURE__ */ jsxs(Box, { flexDirection: "column", marginBottom: 1, children: [
|
|
2794
|
+
/* @__PURE__ */ jsxs(Box, { children: [
|
|
2795
|
+
/* @__PURE__ */ jsxs(Text, { color: COLOR[d.level], bold: true, children: [
|
|
2796
|
+
d.level === "error" ? "\u2716" : "\u26A0",
|
|
2797
|
+
" "
|
|
2798
|
+
] }),
|
|
2799
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
2800
|
+
d.file,
|
|
2801
|
+
d.line !== void 0 ? `:${d.line}` : "",
|
|
2802
|
+
d.column !== void 0 ? `:${d.column}` : "",
|
|
2803
|
+
" "
|
|
2804
|
+
] }),
|
|
2805
|
+
/* @__PURE__ */ jsxs(Text, { color: COLOR[d.level], children: [
|
|
2806
|
+
d.code,
|
|
2807
|
+
" "
|
|
2808
|
+
] }),
|
|
2809
|
+
/* @__PURE__ */ jsx(Text, { children: d.message })
|
|
2810
|
+
] }),
|
|
2811
|
+
frame.length > 0 ? /* @__PURE__ */ jsx(Text, { children: frame }) : null,
|
|
2812
|
+
d.hint !== void 0 ? /* @__PURE__ */ jsxs(Text, { children: [
|
|
2813
|
+
" ",
|
|
2814
|
+
/* @__PURE__ */ jsx(Text, { color: "cyan", children: "hint:" }),
|
|
2815
|
+
" ",
|
|
2816
|
+
d.hint
|
|
2817
|
+
] }) : null,
|
|
2818
|
+
/* @__PURE__ */ jsxs(Text, { dimColor: true, children: [
|
|
2819
|
+
" ",
|
|
2820
|
+
helpUrl(d.code)
|
|
2821
|
+
] })
|
|
2822
|
+
] }, `${d.file}:${d.line ?? "-"}:${i}`);
|
|
2823
|
+
}),
|
|
2824
|
+
/* @__PURE__ */ jsx(Box, { children: /* @__PURE__ */ jsxs(Text, { bold: true, color: errorCount > 0 ? "red" : warnCount > 0 ? "yellow" : "green", children: [
|
|
2825
|
+
errorCount,
|
|
2826
|
+
" ",
|
|
2827
|
+
errorCount === 1 ? "error" : "errors",
|
|
2828
|
+
", ",
|
|
2829
|
+
warnCount,
|
|
2830
|
+
" ",
|
|
2831
|
+
warnCount === 1 ? "warning" : "warnings",
|
|
2832
|
+
" across ",
|
|
2833
|
+
fileCount,
|
|
2834
|
+
" ",
|
|
2835
|
+
fileCount === 1 ? "file" : "files"
|
|
2836
|
+
] }) })
|
|
2837
|
+
] });
|
|
2838
|
+
}
|
|
2839
|
+
function formatDiagnosticsPlain(diagnostics, fileCount, sources) {
|
|
2840
|
+
if (diagnostics.length === 0) {
|
|
2841
|
+
return `OK: ${fileCount} ${fileCount === 1 ? "file" : "files"} checked, no diagnostics
|
|
2842
|
+
`;
|
|
2843
|
+
}
|
|
2844
|
+
const blocks = diagnostics.map((d) => {
|
|
2845
|
+
const loc = d.line !== void 0 ? `${d.file}:${d.line}${d.column !== void 0 ? `:${d.column}` : ""}` : d.file;
|
|
2846
|
+
const parts = [`${loc} ${d.level} ${d.code} ${d.message}`];
|
|
2847
|
+
const frame = frameFor(d, sources);
|
|
2848
|
+
if (frame.length > 0) parts.push(frame);
|
|
2849
|
+
if (d.hint !== void 0) parts.push(` hint: ${d.hint}`);
|
|
2850
|
+
return parts.join("\n");
|
|
2851
|
+
});
|
|
2852
|
+
const errors = diagnostics.filter((d) => d.level === "error").length;
|
|
2853
|
+
const warns = diagnostics.filter((d) => d.level === "warn").length;
|
|
2854
|
+
blocks.push(`${errors} error(s), ${warns} warning(s) across ${fileCount} file(s)`);
|
|
2855
|
+
return blocks.join("\n\n") + "\n";
|
|
2856
|
+
}
|
|
2857
|
+
var TAGLINE = "Documentation-as-code \u2014 Markdown with typed, fenced YAML blocks.";
|
|
2858
|
+
var BRAND_GRADIENT = ["#e4744c", "#b04a25"];
|
|
2859
|
+
var plainOutput = () => process.stdout.isTTY !== true || process.env["CHILTEPIN_PLAIN"] === "1";
|
|
2860
|
+
var plainLine = (version) => `chiltepin v${version} \u2014 ${TAGLINE}`;
|
|
2861
|
+
function actionBanner(word) {
|
|
2862
|
+
try {
|
|
2863
|
+
const out = cfonts.render(word, {
|
|
2864
|
+
font: "tiny",
|
|
2865
|
+
gradient: BRAND_GRADIENT,
|
|
2866
|
+
transitionGradient: true,
|
|
2867
|
+
space: false,
|
|
2868
|
+
env: "node"
|
|
2869
|
+
});
|
|
2870
|
+
if (out !== false && typeof out === "object" && out.string !== void 0) {
|
|
2871
|
+
return `
|
|
2872
|
+
${out.string}
|
|
2873
|
+
`;
|
|
2874
|
+
}
|
|
2875
|
+
} catch {
|
|
2876
|
+
}
|
|
2877
|
+
return `
|
|
2878
|
+
${pc5.red(pc5.bold(word))}
|
|
2879
|
+
`;
|
|
2880
|
+
}
|
|
2881
|
+
var FUN_LINES = {
|
|
2882
|
+
html: "Roasting your doc into HTML\u2026",
|
|
2883
|
+
slides: "Slicing the pepper into slides\u2026",
|
|
2884
|
+
pdf: "Drying one PDF in the sun\u2026",
|
|
2885
|
+
preview: "Tasting the preview\u2026",
|
|
2886
|
+
check: "Checking the crop for bad pods\u2026",
|
|
2887
|
+
new: "Planting a fresh doc\u2026",
|
|
2888
|
+
demo: "Serving up the chiltepin demo\u2026",
|
|
2889
|
+
build: "Grinding the whole harvest into a site\u2026",
|
|
2890
|
+
serve: "Serving fresh docs \u2014 reloads on save\u2026",
|
|
2891
|
+
studio: "Opening the studio \u2014 the harvest goes visual\u2026"
|
|
2892
|
+
};
|
|
2893
|
+
function funLine(action) {
|
|
2894
|
+
return pc5.dim(` ${FUN_LINES[action] ?? `${action}\u2026`}`);
|
|
2895
|
+
}
|
|
2896
|
+
function banner(version = "0.0.2") {
|
|
2897
|
+
if (plainOutput()) return `
|
|
2898
|
+
${plainLine(version)}
|
|
2899
|
+
`;
|
|
2900
|
+
const glyph = pc5.red("\u25CF");
|
|
2901
|
+
const name = pc5.bold(pc5.red("chiltepin"));
|
|
2902
|
+
return `
|
|
2903
|
+
${glyph} ${name} ${pc5.dim(`v${version} \u2014 ${TAGLINE}`)}
|
|
2904
|
+
`;
|
|
2905
|
+
}
|
|
2906
|
+
var HELP_GROUPS = [
|
|
2907
|
+
{ header: "WORK", commands: ["init", "new", "check", "studio"] },
|
|
2908
|
+
{ header: "OUTPUT", commands: ["html", "slides", "pdf", "build"] },
|
|
2909
|
+
{ header: "REFERENCE", commands: ["block", "demo"] },
|
|
2910
|
+
{ header: "SETUP", commands: ["sync"] }
|
|
2911
|
+
];
|
|
2912
|
+
function examples() {
|
|
2913
|
+
const p = plainOutput();
|
|
2914
|
+
const dim = (s) => p ? s : pc5.dim(s);
|
|
2915
|
+
const cyan = (s) => p ? s : pc5.cyan(s);
|
|
2916
|
+
const rows = HELP_GROUPS.map(
|
|
2917
|
+
(g) => ` ${dim(g.header.padEnd(10))}${g.commands.map((c) => cyan(c)).join(dim(" \xB7 "))}`
|
|
2918
|
+
);
|
|
2919
|
+
return [
|
|
2920
|
+
"",
|
|
2921
|
+
...rows,
|
|
2922
|
+
"",
|
|
2923
|
+
` ${cyan("chiltepin <file.md>")} ${dim("renders + opens a doc \u2014 the fastest preview")}`,
|
|
2924
|
+
` ${cyan("npx skills add jdiejim/chiltepin")} ${dim("installs the authoring skill into your AI agent")}`,
|
|
2925
|
+
` ${dim("Docs:")} https://github.com/jdiejim/chiltepin`,
|
|
2926
|
+
""
|
|
2927
|
+
].join("\n");
|
|
2928
|
+
}
|
|
2929
|
+
var COMMAND_EXAMPLES = {
|
|
2930
|
+
init: [
|
|
2931
|
+
["chiltepin init", "scaffold docs/ and chiltepin.config.json"],
|
|
2932
|
+
["chiltepin init --force", "overwrite the starter files"]
|
|
2933
|
+
],
|
|
2934
|
+
new: [
|
|
2935
|
+
["chiltepin new", "pick a doc template or block scaffold interactively"],
|
|
2936
|
+
["chiltepin new adr -o docs/decisions/001-queue.md", "scaffold an ADR into a file"],
|
|
2937
|
+
["chiltepin new sequence", "print a sequence-block scaffold to paste"]
|
|
2938
|
+
],
|
|
2939
|
+
check: [
|
|
2940
|
+
["chiltepin check", "validate every doc under docs/"],
|
|
2941
|
+
["chiltepin check docs/api.md", "validate one file"],
|
|
2942
|
+
["chiltepin check --json", "machine-readable diagnostics (CI)"]
|
|
2943
|
+
],
|
|
2944
|
+
studio: [
|
|
2945
|
+
["chiltepin studio", "open the studio \u2014 Edit \xB7 Site \xB7 Present, files stay the source of truth"],
|
|
2946
|
+
["chiltepin studio --port 5000 --no-open", "pick the port, skip the browser"]
|
|
2947
|
+
],
|
|
2948
|
+
html: [
|
|
2949
|
+
["chiltepin html docs/design.md", "write docs/design.html next to the source"],
|
|
2950
|
+
["chiltepin html docs/design.md -p", "render to a temp file and open it"]
|
|
2951
|
+
],
|
|
2952
|
+
slides: [
|
|
2953
|
+
["chiltepin slides docs/roadmap.md -p", "open the doc as a slide deck"],
|
|
2954
|
+
["chiltepin slides docs/roadmap.md -o deck.html", "write the deck to a file"]
|
|
2955
|
+
],
|
|
2956
|
+
pdf: [
|
|
2957
|
+
["chiltepin pdf docs/design.md", "write docs/design.pdf (downloads Chromium once)"],
|
|
2958
|
+
["chiltepin pdf docs/design.md -o out/design.pdf", "choose the output path"]
|
|
2959
|
+
],
|
|
2960
|
+
build: [
|
|
2961
|
+
["chiltepin build", "build the whole site into dist/"],
|
|
2962
|
+
["chiltepin build --out site", "build into a different directory"]
|
|
2963
|
+
],
|
|
2964
|
+
block: [
|
|
2965
|
+
["chiltepin block", "every block type, one line each, by family"],
|
|
2966
|
+
["chiltepin block sequence", "fields, enums, terse forms, and a validating example"],
|
|
2967
|
+
["chiltepin block erd --json", "the same contract as JSON"]
|
|
2968
|
+
],
|
|
2969
|
+
demo: [
|
|
2970
|
+
["chiltepin demo", "render the built-in showcase of every block and open it"],
|
|
2971
|
+
["chiltepin demo charts -s", "one family, as a slide deck"]
|
|
2972
|
+
],
|
|
2973
|
+
sync: [
|
|
2974
|
+
["chiltepin sync openapi api.yaml -o docs/api.md", "generate a doc from an OpenAPI spec"],
|
|
2975
|
+
["chiltepin sync openapi api.yaml --check docs/api.md", "fail on drift (CI)"],
|
|
2976
|
+
[
|
|
2977
|
+
"chiltepin sync csv sales.csv",
|
|
2978
|
+
"print a ready-to-paste block (auto-picks table/statustable/chart)"
|
|
2979
|
+
],
|
|
2980
|
+
[
|
|
2981
|
+
"chiltepin sync csv sales.csv -o docs/sales.md",
|
|
2982
|
+
"wrap the block in a doc, write it, and validate"
|
|
2983
|
+
],
|
|
2984
|
+
["chiltepin sync csv sales.csv --block chart", "force the target block type"]
|
|
2985
|
+
]
|
|
2986
|
+
};
|
|
2987
|
+
function commandExamples(name) {
|
|
2988
|
+
const rows = COMMAND_EXAMPLES[name];
|
|
2989
|
+
if (rows === void 0) return "";
|
|
2990
|
+
const p = plainOutput();
|
|
2991
|
+
const dim = (s) => p ? s : pc5.dim(s);
|
|
2992
|
+
const cyan = (s) => p ? s : pc5.cyan(s);
|
|
2993
|
+
const width = Math.max(...rows.map(([cmd]) => cmd.length));
|
|
2994
|
+
return [
|
|
2995
|
+
"",
|
|
2996
|
+
"Examples:",
|
|
2997
|
+
...rows.map(([cmd, note]) => ` ${cyan("$ " + cmd.padEnd(width))} ${dim(note)}`),
|
|
2998
|
+
""
|
|
2999
|
+
].join("\n");
|
|
3000
|
+
}
|
|
3001
|
+
|
|
3002
|
+
// src/tty.ts
|
|
3003
|
+
var isInteractive = process.stdout.isTTY === true && process.env["CI"] !== "true" && process.env["CHILTEPIN_PLAIN"] !== "1";
|
|
3004
|
+
function flourish(word, lineKey = word) {
|
|
3005
|
+
if (!isInteractive) return;
|
|
3006
|
+
console.log(actionBanner(word));
|
|
3007
|
+
console.log(funLine(lineKey) + "\n");
|
|
3008
|
+
}
|
|
3009
|
+
function printInitSummary(result) {
|
|
3010
|
+
for (const f of result.created) console.log(pc5.green("+ ") + f);
|
|
3011
|
+
for (const f of result.skipped) console.log(pc5.dim(" skip ") + f + pc5.dim(" (exists)"));
|
|
3012
|
+
console.log(
|
|
3013
|
+
pc5.bold(`
|
|
3014
|
+
Created ${result.created.length} file(s), skipped ${result.skipped.length}.`)
|
|
3015
|
+
);
|
|
3016
|
+
console.log(
|
|
3017
|
+
pc5.dim(
|
|
3018
|
+
"Layout: docs/<area>/<doc>.md, kebab-case names \xB7 output goes to dist/ \u2014 do not commit it."
|
|
3019
|
+
)
|
|
3020
|
+
);
|
|
3021
|
+
console.log(
|
|
3022
|
+
`Next: ${pc5.cyan("chiltepin check")} ${pc5.dim("\xB7")} ${pc5.cyan("chiltepin docs/getting-started.md")} ${pc5.dim("(render + open)")}`
|
|
3023
|
+
);
|
|
3024
|
+
console.log(
|
|
3025
|
+
`AI: ${pc5.cyan("npx skills add jdiejim/chiltepin")} ${pc5.dim("installs the authoring skill into Claude Code, Cursor, Codex, and 70+ agents")}`
|
|
3026
|
+
);
|
|
3027
|
+
}
|
|
3028
|
+
async function main(argv) {
|
|
3029
|
+
argv = argv.map((a) => a === "-V" ? "--version" : a);
|
|
3030
|
+
const version = cliVersion();
|
|
3031
|
+
const program = new Command();
|
|
3032
|
+
program.name("chiltepin").description("Author, validate, render, and export Chiltepin documentation.").version(version, "-v, --version", "print the version").addHelpText("beforeAll", (ctx) => ctx.command.name() === "chiltepin" ? banner(version) : "").addHelpText("after", (ctx) => ctx.command.name() === "chiltepin" ? examples() : "").exitOverride();
|
|
3033
|
+
let exitCode = 0;
|
|
3034
|
+
program.argument("[file]", "a .md file to render + open in the browser (same as `chiltepin html <file> -p`)").action(async (file) => {
|
|
3035
|
+
const cwd = process.cwd();
|
|
3036
|
+
if (file !== void 0) {
|
|
3037
|
+
if (!/\.md$/i.test(file)) {
|
|
3038
|
+
console.error(`error: unknown command '${file}'`);
|
|
3039
|
+
exitCode = 1;
|
|
3040
|
+
return;
|
|
3041
|
+
}
|
|
3042
|
+
flourish("preview");
|
|
3043
|
+
const result = await runSingle({
|
|
3044
|
+
cwd,
|
|
3045
|
+
input: file,
|
|
3046
|
+
format: "html",
|
|
3047
|
+
preview: true,
|
|
3048
|
+
open: isInteractive
|
|
3049
|
+
// script-safe: piped output renders but doesn't open
|
|
3050
|
+
});
|
|
3051
|
+
const verb = result.opened ? "Opened" : "Wrote";
|
|
3052
|
+
console.log(`${pc5.green(verb)} ${result.output} ${pc5.dim(`(${result.bytes} bytes)`)}`);
|
|
3053
|
+
return;
|
|
3054
|
+
}
|
|
3055
|
+
if (!isInteractive) {
|
|
3056
|
+
program.outputHelp();
|
|
3057
|
+
return;
|
|
3058
|
+
}
|
|
3059
|
+
console.log(banner(version));
|
|
3060
|
+
if (findConfig(cwd) === void 0) {
|
|
3061
|
+
console.log(` Not a Chiltepin project yet \u2014 run ${pc5.cyan("chiltepin init")} to scaffold one.`);
|
|
3062
|
+
console.log(
|
|
3063
|
+
` ${pc5.dim("Curious first?")} ${pc5.cyan("chiltepin demo")} ${pc5.dim("renders every block \xB7 ")}${pc5.cyan("chiltepin block")} ${pc5.dim("lists them.")}`
|
|
3064
|
+
);
|
|
3065
|
+
console.log("");
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
const status = await projectStatus(cwd);
|
|
3069
|
+
console.log(formatStatus(status));
|
|
3070
|
+
});
|
|
3071
|
+
program.command("init").description("Scaffold a new Chiltepin project in the current directory").option("--force", "overwrite existing files").option("-y, --yes", "accepted for compatibility (init has no prompts)", void 0).action(async (opts) => {
|
|
3072
|
+
const result = await runInit({
|
|
3073
|
+
cwd: process.cwd(),
|
|
3074
|
+
...opts.force === true ? { force: true } : {}
|
|
3075
|
+
});
|
|
3076
|
+
printInitSummary(result);
|
|
3077
|
+
});
|
|
3078
|
+
program.command("check [globs...]").description("Validate documents (default: docs/**/*.md)").option("--json", "emit machine-readable JSON").option("--strict-prose", "treat prose-lint warnings (W_PROSE_*) as errors").action(async (globs, opts) => {
|
|
3079
|
+
const cwd = process.cwd();
|
|
3080
|
+
const config = await loadConfig(cwd);
|
|
3081
|
+
const patterns = globs.length > 0 ? globs : [`${config.docsDir}/**/*.md`];
|
|
3082
|
+
const result = await runCheck({
|
|
3083
|
+
patterns,
|
|
3084
|
+
cwd,
|
|
3085
|
+
docsRoot: config.docsDir,
|
|
3086
|
+
...opts.strictProse === true ? { strictProse: true } : {}
|
|
3087
|
+
});
|
|
3088
|
+
if (opts.json === true) {
|
|
3089
|
+
process.stdout.write(
|
|
3090
|
+
JSON.stringify({ diagnostics: result.diagnostics, files: result.files }, null, 2) + "\n"
|
|
3091
|
+
);
|
|
3092
|
+
} else if (isInteractive) {
|
|
3093
|
+
flourish("check");
|
|
3094
|
+
const { waitUntilExit } = render(
|
|
3095
|
+
/* @__PURE__ */ jsx(
|
|
3096
|
+
DiagnosticsTable,
|
|
3097
|
+
{
|
|
3098
|
+
diagnostics: result.diagnostics,
|
|
3099
|
+
fileCount: result.files.length,
|
|
3100
|
+
sources: result.sources
|
|
3101
|
+
}
|
|
3102
|
+
)
|
|
3103
|
+
);
|
|
3104
|
+
await waitUntilExit();
|
|
3105
|
+
} else {
|
|
3106
|
+
process.stdout.write(
|
|
3107
|
+
formatDiagnosticsPlain(result.diagnostics, result.files.length, result.sources)
|
|
3108
|
+
);
|
|
3109
|
+
}
|
|
3110
|
+
exitCode = result.exitCode;
|
|
3111
|
+
});
|
|
3112
|
+
program.command("audit [path]").description("Audit a codebase and recommend which Chiltepin docs to write (evidence-cited)").option("--json", "emit machine-readable JSON (schema version 1)").action(async (pathArg, opts) => {
|
|
3113
|
+
const result = await runAudit({
|
|
3114
|
+
cwd: process.cwd(),
|
|
3115
|
+
...pathArg !== void 0 ? { path: pathArg } : {}
|
|
3116
|
+
});
|
|
3117
|
+
if (!result.ok) {
|
|
3118
|
+
console.error(pc5.red(result.error));
|
|
3119
|
+
exitCode = 2;
|
|
3120
|
+
return;
|
|
3121
|
+
}
|
|
3122
|
+
if (opts.json === true) {
|
|
3123
|
+
process.stdout.write(JSON.stringify(result.report, null, 2) + "\n");
|
|
3124
|
+
return;
|
|
3125
|
+
}
|
|
3126
|
+
if (isInteractive) {
|
|
3127
|
+
flourish("audit");
|
|
3128
|
+
console.log(formatAudit(result.report, false));
|
|
3129
|
+
} else {
|
|
3130
|
+
process.stdout.write(formatAudit(result.report, true) + "\n");
|
|
3131
|
+
}
|
|
3132
|
+
});
|
|
3133
|
+
program.command("preview <input>", { hidden: true }).description("Render a document to a temp HTML file and open it (same as `chiltepin <file.md>`)").action(async (input) => {
|
|
3134
|
+
flourish("preview");
|
|
3135
|
+
const result = await runSingle({
|
|
3136
|
+
cwd: process.cwd(),
|
|
3137
|
+
input,
|
|
3138
|
+
format: "html",
|
|
3139
|
+
preview: true,
|
|
3140
|
+
open: isInteractive
|
|
3141
|
+
});
|
|
3142
|
+
const verb = result.opened ? "Opened" : "Wrote";
|
|
3143
|
+
console.log(`${pc5.green(verb)} ${result.output} ${pc5.dim(`(${result.bytes} bytes)`)}`);
|
|
3144
|
+
});
|
|
3145
|
+
program.command("new [name]").description(
|
|
3146
|
+
"Create from a template \u2014 a full doc (adr, runbook, \u2026) or a single block (sequence, erd, \u2026)"
|
|
3147
|
+
).option("-o, --output <path>", "write to a file instead of printing to stdout").option("--force", "with -o, replace the file if it already exists").action(async (nameArg, opts) => {
|
|
3148
|
+
const cwd = process.cwd();
|
|
3149
|
+
const resolveName = (n) => {
|
|
3150
|
+
if (isDocTemplate(n) || BLOCK_TYPES.includes(n)) return { type: n };
|
|
3151
|
+
const alias = BLOCK_ALIASES[n];
|
|
3152
|
+
if (alias !== void 0) {
|
|
3153
|
+
const patch = Object.entries(alias.patch ?? {}).map(([k, v]) => `${k}: ${String(v)}`).join(", ");
|
|
3154
|
+
return {
|
|
3155
|
+
type: alias.type,
|
|
3156
|
+
note: `\`${n}\` now lives in \`${alias.type}\`${patch === "" ? "" : ` (${patch})`} \u2014 both spellings work; no change needed.`
|
|
3157
|
+
};
|
|
3158
|
+
}
|
|
3159
|
+
return void 0;
|
|
3160
|
+
};
|
|
3161
|
+
const emit = async (type) => {
|
|
3162
|
+
if (opts.output !== void 0) {
|
|
3163
|
+
const p = await writeNewDoc({
|
|
3164
|
+
cwd,
|
|
3165
|
+
type,
|
|
3166
|
+
out: opts.output,
|
|
3167
|
+
...opts.force === true ? { force: true } : {}
|
|
3168
|
+
});
|
|
3169
|
+
console.log(`${pc5.green("\u2713")} Wrote ${p}`);
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
const content = isDocTemplate(type) ? DOC_TEMPLATES[type] : templateFor(type);
|
|
3173
|
+
if (isInteractive)
|
|
3174
|
+
console.log(pc5.dim(`# ${type} \u2014 paste into a docs/*.md (or re-run with -o <path>)
|
|
3175
|
+
`));
|
|
3176
|
+
process.stdout.write(content);
|
|
3177
|
+
if (isInteractive && copyToClipboard(content))
|
|
3178
|
+
console.log(pc5.green("\n\u2713 copied to clipboard"));
|
|
3179
|
+
};
|
|
3180
|
+
if (nameArg !== void 0) {
|
|
3181
|
+
const hit = resolveName(nameArg);
|
|
3182
|
+
if (hit === void 0) {
|
|
3183
|
+
console.error(
|
|
3184
|
+
pc5.red(`Unknown template or block: ${nameArg}. Run \`chiltepin new\` to list them.`)
|
|
3185
|
+
);
|
|
3186
|
+
exitCode = 2;
|
|
3187
|
+
return;
|
|
3188
|
+
}
|
|
3189
|
+
if (hit.note !== void 0) console.error(pc5.yellow(hit.note));
|
|
3190
|
+
await emit(hit.type);
|
|
3191
|
+
return;
|
|
3192
|
+
}
|
|
3193
|
+
if (!isInteractive) {
|
|
3194
|
+
console.log("Doc templates:");
|
|
3195
|
+
for (const [name, info] of Object.entries(DOC_TEMPLATE_INFO)) {
|
|
3196
|
+
console.log(` ${name.padEnd(14)}${info.description}`);
|
|
3197
|
+
}
|
|
3198
|
+
console.log("\nBlocks:");
|
|
3199
|
+
for (const fam of BLOCK_FAMILIES) {
|
|
3200
|
+
console.log(` ${fam.label}: ${familyBlocks(fam.id).join(" ")}`);
|
|
3201
|
+
}
|
|
3202
|
+
console.log("\nUsage: chiltepin new <name> [-o <path>]");
|
|
3203
|
+
return;
|
|
3204
|
+
}
|
|
3205
|
+
flourish("new");
|
|
3206
|
+
let picked;
|
|
3207
|
+
const { waitUntilExit } = render(
|
|
3208
|
+
/* @__PURE__ */ jsx(
|
|
3209
|
+
NewPickerApp,
|
|
3210
|
+
{
|
|
3211
|
+
onPick: (n) => {
|
|
3212
|
+
picked = n;
|
|
3213
|
+
}
|
|
3214
|
+
}
|
|
3215
|
+
)
|
|
3216
|
+
);
|
|
3217
|
+
await waitUntilExit();
|
|
3218
|
+
if (picked === void 0) return;
|
|
3219
|
+
await emit(picked);
|
|
3220
|
+
});
|
|
3221
|
+
program.command("build").description("Build a static HTML site from all docs \u2014 index, sidebar nav, cross-doc links").option("--out <dir>", 'output directory (default: config outDir, "dist")').option(
|
|
3222
|
+
"--rich-index",
|
|
3223
|
+
"build the rich index page \u2014 project TLDR, doc map by tag, cross-reference graph (default)"
|
|
3224
|
+
).option("--no-rich-index", "build the plain card-grid index instead").action(async (opts) => {
|
|
3225
|
+
flourish("build");
|
|
3226
|
+
const result = await runBuild({
|
|
3227
|
+
cwd: process.cwd(),
|
|
3228
|
+
...opts.out !== void 0 ? { out: opts.out } : {},
|
|
3229
|
+
...opts.richIndex !== void 0 ? { richIndex: opts.richIndex } : {}
|
|
3230
|
+
});
|
|
3231
|
+
if (result.diagnostics.length > 0) {
|
|
3232
|
+
for (const d of result.diagnostics) {
|
|
3233
|
+
const loc = d.line !== void 0 ? `${d.file}:${d.line}` : d.file;
|
|
3234
|
+
const fatal = d.code === "E_RENDER" || d.code === "E_ENCODING";
|
|
3235
|
+
const line = `${fatal ? "error" : "warn "} ${loc} ${d.code} ${d.message}`;
|
|
3236
|
+
console.error(fatal ? pc5.red(line) : pc5.yellow(line));
|
|
3237
|
+
}
|
|
3238
|
+
const errors = result.diagnostics.filter(
|
|
3239
|
+
(d) => d.code === "E_RENDER" || d.code === "E_ENCODING"
|
|
3240
|
+
).length;
|
|
3241
|
+
const warnings = result.diagnostics.length - errors;
|
|
3242
|
+
const parts = [];
|
|
3243
|
+
if (errors > 0) parts.push(`${errors} error(s)`);
|
|
3244
|
+
if (warnings > 0) parts.push(`${warnings} warning(s)`);
|
|
3245
|
+
console.error(
|
|
3246
|
+
(errors > 0 ? pc5.red : pc5.yellow)(`${parts.join(", ")} \u2014 run \`chiltepin check\` for details`)
|
|
3247
|
+
);
|
|
3248
|
+
}
|
|
3249
|
+
const bytes = result.pages.reduce((sum, p) => sum + p.bytes, 0);
|
|
3250
|
+
const decks = result.pages.filter((p) => p.path.endsWith(".slides.html")).length;
|
|
3251
|
+
const deckPart = decks > 0 ? ` + ${decks} deck(s)` : "";
|
|
3252
|
+
console.log(
|
|
3253
|
+
`${pc5.green("\u2713")} ${result.pages.length - decks} page(s)${deckPart} \u2192 ${result.outDirRel}/ ${pc5.dim(`(${bytes} bytes)`)}`
|
|
3254
|
+
);
|
|
3255
|
+
if (result.removed.length > 0) {
|
|
3256
|
+
for (const p of result.removed) console.log(pc5.dim(` removed ${p}`));
|
|
3257
|
+
console.log(
|
|
3258
|
+
`${pc5.green("\u2713")} ${result.removed.length} stale file(s) removed ${pc5.dim("(no longer generated)")}`
|
|
3259
|
+
);
|
|
3260
|
+
}
|
|
3261
|
+
if (result.pruneDeferred) {
|
|
3262
|
+
console.log(
|
|
3263
|
+
pc5.dim(
|
|
3264
|
+
`Note: ${result.outDirRel}/ had no build manifest, so nothing was pruned. The next build prunes what this one generated.`
|
|
3265
|
+
)
|
|
3266
|
+
);
|
|
3267
|
+
}
|
|
3268
|
+
exitCode = result.exitCode;
|
|
3269
|
+
});
|
|
3270
|
+
program.command("serve", { hidden: true }).description(
|
|
3271
|
+
"Serve the docs site locally with live reload (compat \u2014 `chiltepin studio` includes this as Site mode)"
|
|
3272
|
+
).option("--port <n>", "port to listen on (0 = pick a free port)", "4173").option("--no-open", "don't open the browser").option(
|
|
3273
|
+
"--rich-index",
|
|
3274
|
+
"serve the rich index page \u2014 project TLDR, doc map by tag, cross-reference graph (default)"
|
|
3275
|
+
).option("--no-rich-index", "serve the plain card-grid index instead").action(async (opts) => {
|
|
3276
|
+
flourish("serve");
|
|
3277
|
+
const parsed = Number.parseInt(opts.port, 10);
|
|
3278
|
+
await runServe({
|
|
3279
|
+
cwd: process.cwd(),
|
|
3280
|
+
port: Number.isNaN(parsed) ? 4173 : parsed,
|
|
3281
|
+
open: opts.open,
|
|
3282
|
+
...opts.richIndex !== void 0 ? { richIndex: opts.richIndex } : {}
|
|
3283
|
+
});
|
|
3284
|
+
});
|
|
3285
|
+
program.command("studio").description(
|
|
3286
|
+
"open the local studio \u2014 a Home page of your docs, edit in place, present as slides; the built site is one click away; files stay the source of truth"
|
|
3287
|
+
).option("--port <n>", "port to listen on (0 = pick a free port)", "4174").option("--no-open", "don't open the browser").action(async (opts) => {
|
|
3288
|
+
flourish("studio");
|
|
3289
|
+
const parsed = Number.parseInt(opts.port, 10);
|
|
3290
|
+
await runStudio({
|
|
3291
|
+
cwd: process.cwd(),
|
|
3292
|
+
port: Number.isNaN(parsed) ? 4174 : parsed,
|
|
3293
|
+
open: opts.open
|
|
3294
|
+
});
|
|
3295
|
+
});
|
|
3296
|
+
const syncCmd = program.command("sync").description(
|
|
3297
|
+
"Generate Chiltepin docs from external sources (OpenAPI, CSV, SQL / DBML / Prisma schemas)"
|
|
3298
|
+
);
|
|
3299
|
+
syncCmd.command("openapi <spec>").description("Generate (or drift-check) a doc from an OpenAPI 3.x spec").option("-o, --out <path>", "write generated markdown to this path").option("--check <path>", "compare against an existing doc and fail on drift").option("--slug <slug>", "block-id namespace (defaults to the output basename)").option("--force", "with --out, replace the file if it already exists").action(
|
|
3300
|
+
async (spec, opts) => {
|
|
3301
|
+
const result = await runSyncOpenApi({
|
|
3302
|
+
cwd: process.cwd(),
|
|
3303
|
+
spec,
|
|
3304
|
+
...opts.out !== void 0 ? { out: opts.out } : {},
|
|
3305
|
+
...opts.check !== void 0 ? { check: opts.check } : {},
|
|
3306
|
+
...opts.slug !== void 0 ? { slug: opts.slug } : {},
|
|
3307
|
+
...opts.force === true ? { force: true } : {}
|
|
3308
|
+
});
|
|
3309
|
+
if (result.exitCode === 0) {
|
|
3310
|
+
console.log(pc5.green("\u2713 ") + result.message);
|
|
3311
|
+
} else {
|
|
3312
|
+
console.error(pc5.red(result.message));
|
|
3313
|
+
if (result.diff !== void 0) console.error(result.diff);
|
|
3314
|
+
}
|
|
3315
|
+
exitCode = result.exitCode;
|
|
3316
|
+
}
|
|
3317
|
+
);
|
|
3318
|
+
syncCmd.command("csv <file>").description("Turn a CSV into a table/statustable/chart block (stdout), or a whole doc (--out)").option("-o, --out <path>", "write a minimal doc (meta + block) to this path and validate it").option("--block <type>", "target block: table | statustable | chart (default: auto-suggest)").option("--title <title>", "doc title with --out (default: prettified file name)").option("--delimiter <d>", 'field delimiter: "," ";" or "tab" (default: auto-detect)').option("--force", "with --out, replace the file if it already exists").action(
|
|
3319
|
+
async (file, opts) => {
|
|
3320
|
+
if (opts.block !== void 0 && opts.block !== "table" && opts.block !== "statustable" && opts.block !== "chart") {
|
|
3321
|
+
console.error(pc5.red(`Unknown block: ${opts.block}. Use table | statustable | chart.`));
|
|
3322
|
+
exitCode = 2;
|
|
3323
|
+
return;
|
|
3324
|
+
}
|
|
3325
|
+
const result = await runSyncCsv({
|
|
3326
|
+
cwd: process.cwd(),
|
|
3327
|
+
file,
|
|
3328
|
+
...opts.out !== void 0 ? { out: opts.out } : {},
|
|
3329
|
+
...opts.block !== void 0 ? { block: opts.block } : {},
|
|
3330
|
+
...opts.title !== void 0 ? { title: opts.title } : {},
|
|
3331
|
+
...opts.delimiter !== void 0 ? { delimiter: opts.delimiter } : {},
|
|
3332
|
+
...opts.force === true ? { force: true } : {}
|
|
3333
|
+
});
|
|
3334
|
+
if (result.reason !== void 0) {
|
|
3335
|
+
console.error(pc5.dim(`\u2192 ${result.block}: ${result.reason}`));
|
|
3336
|
+
}
|
|
3337
|
+
for (const w of result.warnings) console.error(pc5.yellow(`warn ${w}`));
|
|
3338
|
+
if (result.message !== void 0) {
|
|
3339
|
+
console.error(pc5.red(result.message));
|
|
3340
|
+
exitCode = result.exitCode;
|
|
3341
|
+
return;
|
|
3342
|
+
}
|
|
3343
|
+
if (result.fence !== void 0) {
|
|
3344
|
+
process.stdout.write(result.fence);
|
|
3345
|
+
if (isInteractive && copyToClipboard(result.fence)) {
|
|
3346
|
+
console.log(pc5.green("\n\u2713 copied to clipboard"));
|
|
3347
|
+
}
|
|
3348
|
+
}
|
|
3349
|
+
if (result.outPath !== void 0) {
|
|
3350
|
+
console.log(`${pc5.green("\u2713")} Wrote ${result.outPath} ${pc5.dim(`(${result.block})`)}`);
|
|
3351
|
+
const diags = result.check?.diagnostics ?? [];
|
|
3352
|
+
if (diags.length === 0) {
|
|
3353
|
+
console.log(`${pc5.green("\u2713")} chiltepin check: clean`);
|
|
3354
|
+
} else {
|
|
3355
|
+
for (const d of diags) {
|
|
3356
|
+
const loc = d.line !== void 0 ? `${d.file}:${d.line}` : d.file;
|
|
3357
|
+
const paint = d.level === "error" ? pc5.red : pc5.yellow;
|
|
3358
|
+
console.error(paint(`${d.level} ${loc} ${d.code} ${d.message}`));
|
|
3359
|
+
}
|
|
3360
|
+
console.error(pc5.dim(`chiltepin check: ${diags.length} diagnostic(s)`));
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
exitCode = result.exitCode;
|
|
3364
|
+
}
|
|
3365
|
+
);
|
|
3366
|
+
const schemaSync = (dialect, what) => {
|
|
3367
|
+
syncCmd.command(`${dialect} <file>`).description(`Turn ${what} into an erd block (stdout), or a whole doc (--out)`).option("-o, --out <path>", "write a minimal doc (meta + erd) to this path and validate it").option("--title <title>", "doc title with --out (default: prettified file name)").option("--id <id>", "block id (default: the file stem as a slug)").option("--force", "with --out, replace the file if it already exists").action(
|
|
3368
|
+
async (file, opts) => {
|
|
3369
|
+
const result = await runSyncSchema({
|
|
3370
|
+
cwd: process.cwd(),
|
|
3371
|
+
file,
|
|
3372
|
+
dialect,
|
|
3373
|
+
...opts.out !== void 0 ? { out: opts.out } : {},
|
|
3374
|
+
...opts.title !== void 0 ? { title: opts.title } : {},
|
|
3375
|
+
...opts.id !== void 0 ? { id: opts.id } : {},
|
|
3376
|
+
...opts.force === true ? { force: true } : {}
|
|
3377
|
+
});
|
|
3378
|
+
if (result.message !== void 0) {
|
|
3379
|
+
console.error(pc5.red(result.message));
|
|
3380
|
+
exitCode = result.exitCode;
|
|
3381
|
+
return;
|
|
3382
|
+
}
|
|
3383
|
+
if (result.fence !== void 0) {
|
|
3384
|
+
process.stdout.write(result.fence);
|
|
3385
|
+
if (isInteractive && copyToClipboard(result.fence)) {
|
|
3386
|
+
console.log(pc5.green("\n\u2713 copied to clipboard"));
|
|
3387
|
+
}
|
|
3388
|
+
}
|
|
3389
|
+
if (result.outPath !== void 0) {
|
|
3390
|
+
console.log(
|
|
3391
|
+
`${pc5.green("\u2713")} Wrote ${result.outPath} ${pc5.dim(`(erd \xB7 ${result.entities} entities \xB7 ${result.relations} relations)`)}`
|
|
3392
|
+
);
|
|
3393
|
+
const diags = result.check?.diagnostics ?? [];
|
|
3394
|
+
if (diags.length === 0) {
|
|
3395
|
+
console.log(`${pc5.green("\u2713")} chiltepin check: clean`);
|
|
3396
|
+
} else {
|
|
3397
|
+
for (const d of diags) {
|
|
3398
|
+
const loc = d.line !== void 0 ? `${d.file}:${d.line}` : d.file;
|
|
3399
|
+
const paint = d.level === "error" ? pc5.red : pc5.yellow;
|
|
3400
|
+
console.error(paint(`${d.level} ${loc} ${d.code} ${d.message}`));
|
|
3401
|
+
}
|
|
3402
|
+
console.error(pc5.dim(`chiltepin check: ${diags.length} diagnostic(s)`));
|
|
3403
|
+
}
|
|
3404
|
+
}
|
|
3405
|
+
exitCode = result.exitCode;
|
|
3406
|
+
}
|
|
3407
|
+
);
|
|
3408
|
+
};
|
|
3409
|
+
schemaSync("sql", "SQL DDL (CREATE TABLE \u2026)");
|
|
3410
|
+
schemaSync("dbml", "a DBML schema");
|
|
3411
|
+
schemaSync("prisma", "a Prisma schema");
|
|
3412
|
+
const single = (name, desc) => {
|
|
3413
|
+
const cmd = program.command(`${name} <input>`).description(desc).option("-o, --output <path>", "output file path").option("-p, --preview", "render to a temp file and open it in the browser").option(
|
|
3414
|
+
"--force",
|
|
3415
|
+
`with -o, replace a file that is not already a .${name === "slides" ? "html" : name} file`
|
|
3416
|
+
);
|
|
3417
|
+
if (name === "html" || name === "pdf") {
|
|
3418
|
+
cmd.option(
|
|
3419
|
+
"--size <preset>",
|
|
3420
|
+
"set the page width: sm (720 px) | md (960 px) | lg (1280 px) | xl (1600 px). Without this option, the page keeps the default width."
|
|
3421
|
+
);
|
|
3422
|
+
}
|
|
3423
|
+
cmd.action(
|
|
3424
|
+
async (input, opts) => {
|
|
3425
|
+
let size;
|
|
3426
|
+
if (opts.size !== void 0) {
|
|
3427
|
+
size = parseExportSize(opts.size);
|
|
3428
|
+
if (size === void 0) {
|
|
3429
|
+
console.error(pc5.red(`Unknown size: ${opts.size}. Use one of: sm | md | lg | xl`));
|
|
3430
|
+
exitCode = 2;
|
|
3431
|
+
return;
|
|
3432
|
+
}
|
|
3433
|
+
}
|
|
3434
|
+
const word = opts.preview === true ? "preview" : name;
|
|
3435
|
+
if (isInteractive) {
|
|
3436
|
+
console.log(actionBanner(word));
|
|
3437
|
+
console.log(funLine(word) + "\n");
|
|
3438
|
+
}
|
|
3439
|
+
const result = await runSingle({
|
|
3440
|
+
cwd: process.cwd(),
|
|
3441
|
+
input,
|
|
3442
|
+
format: name,
|
|
3443
|
+
...opts.output !== void 0 ? { output: opts.output } : {},
|
|
3444
|
+
...opts.preview === true ? { preview: true } : {},
|
|
3445
|
+
...size !== void 0 ? { size } : {},
|
|
3446
|
+
...opts.force === true ? { force: true } : {}
|
|
3447
|
+
});
|
|
3448
|
+
const verb = result.opened ? "Opened" : "Wrote";
|
|
3449
|
+
console.log(`${pc5.green(verb)} ${result.output} ${pc5.dim(`(${result.bytes} bytes)`)}`);
|
|
3450
|
+
}
|
|
3451
|
+
);
|
|
3452
|
+
};
|
|
3453
|
+
single("html", "Render one document to a standalone HTML file");
|
|
3454
|
+
single("slides", "Render one document to a self-contained slide deck");
|
|
3455
|
+
single("pdf", "Render one document to a PDF (needs Chromium once)");
|
|
3456
|
+
program.command("demo [family]").description(
|
|
3457
|
+
`Render the built-in showcase and open it \u2014 all blocks or one family (${BLOCK_FAMILIES.map((f) => f.id).join(" | ")}); -s for a slide deck`
|
|
3458
|
+
).option("-s, --slides", "render as a slide deck").option("-o, --output <path>", "write the rendered file to a path (implies --no-open)").option("--no-open", "write the file but don't open it").option("--force", "with -o, replace a file that is not already an .html file").action(
|
|
3459
|
+
async (familyArg, opts) => {
|
|
3460
|
+
let family;
|
|
3461
|
+
if (familyArg !== void 0) {
|
|
3462
|
+
if (!isBlockFamily(familyArg)) {
|
|
3463
|
+
const choices = BLOCK_FAMILIES.map((f) => f.id).join(" | ");
|
|
3464
|
+
console.error(pc5.red(`Unknown family: ${familyArg}. Try one of: ${choices}`));
|
|
3465
|
+
exitCode = 2;
|
|
3466
|
+
return;
|
|
3467
|
+
}
|
|
3468
|
+
family = familyArg;
|
|
3469
|
+
} else if (isInteractive) {
|
|
3470
|
+
let picked;
|
|
3471
|
+
const { waitUntilExit } = render(
|
|
3472
|
+
/* @__PURE__ */ jsx(
|
|
3473
|
+
DemoApp,
|
|
3474
|
+
{
|
|
3475
|
+
onPick: (p) => {
|
|
3476
|
+
picked = p;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
)
|
|
3480
|
+
);
|
|
3481
|
+
await waitUntilExit();
|
|
3482
|
+
if (picked === void 0) return;
|
|
3483
|
+
family = picked.family;
|
|
3484
|
+
}
|
|
3485
|
+
flourish("demo");
|
|
3486
|
+
const result = await runDemo({
|
|
3487
|
+
format: opts.slides === true ? "slides" : "html",
|
|
3488
|
+
...family !== void 0 ? { family } : {},
|
|
3489
|
+
...opts.output !== void 0 ? { output: resolve(process.cwd(), opts.output) } : { preview: opts.open !== false },
|
|
3490
|
+
...opts.force === true ? { force: true } : {}
|
|
3491
|
+
});
|
|
3492
|
+
const verb = result.opened ? "Opened" : "Wrote";
|
|
3493
|
+
console.log(`${pc5.green(verb)} ${result.output} ${pc5.dim(`(${result.bytes} bytes)`)}`);
|
|
3494
|
+
}
|
|
3495
|
+
);
|
|
3496
|
+
program.command("block [type]").description(
|
|
3497
|
+
"Block reference \u2014 every type on one line, or one type's fields, enums, terse forms, and example"
|
|
3498
|
+
).option("--json", "emit the contract as JSON").action((typeArg, opts) => {
|
|
3499
|
+
if (typeArg === void 0) {
|
|
3500
|
+
process.stdout.write(blockIndex());
|
|
3501
|
+
return;
|
|
3502
|
+
}
|
|
3503
|
+
const hit = resolveBlockName(typeArg);
|
|
3504
|
+
if (hit === void 0) {
|
|
3505
|
+
console.error(pc5.red(`Unknown block: ${typeArg}. Run \`chiltepin block\` to list them.`));
|
|
3506
|
+
exitCode = 2;
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
if (hit.alias !== void 0) {
|
|
3510
|
+
console.error(
|
|
3511
|
+
pc5.yellow(
|
|
3512
|
+
`\`${hit.alias}\` is an old spelling of \`${hit.type}\` \u2014 both work; showing \`${hit.type}\`.`
|
|
3513
|
+
)
|
|
3514
|
+
);
|
|
3515
|
+
}
|
|
3516
|
+
process.stdout.write(blockReference(hit.type, opts.json === true));
|
|
3517
|
+
});
|
|
3518
|
+
program.command("skill", { hidden: true }).description(
|
|
3519
|
+
"Print the Chiltepin authoring grammar as a copy-paste system prompt (for Copilot / custom GPTs / any AI)"
|
|
3520
|
+
).option("-o, --output <path>", "write the system prompt to a file instead of printing it").option(
|
|
3521
|
+
"--raw",
|
|
3522
|
+
"emit the raw skill file verbatim (with frontmatter) instead of the wrapped prompt"
|
|
3523
|
+
).option("--force", "with -o, replace the file if it already exists").action(async (opts) => {
|
|
3524
|
+
const text = await systemPrompt({ ...opts.raw === true ? { raw: true } : {} });
|
|
3525
|
+
if (opts.output !== void 0) {
|
|
3526
|
+
await writeFileSafe(
|
|
3527
|
+
resolve(process.cwd(), opts.output),
|
|
3528
|
+
text,
|
|
3529
|
+
opts.force === true ? { force: true } : {}
|
|
3530
|
+
);
|
|
3531
|
+
console.log(`${pc5.green("\u2713")} Wrote ${opts.output} ${pc5.dim(`(${text.length} chars)`)}`);
|
|
3532
|
+
return;
|
|
3533
|
+
}
|
|
3534
|
+
if (isInteractive) {
|
|
3535
|
+
console.log(
|
|
3536
|
+
pc5.dim(
|
|
3537
|
+
"# Chiltepin system prompt \u2014 paste into your tool's system / custom-instructions box\n"
|
|
3538
|
+
)
|
|
3539
|
+
);
|
|
3540
|
+
}
|
|
3541
|
+
console.log(text);
|
|
3542
|
+
if (isInteractive && copyToClipboard(text)) {
|
|
3543
|
+
console.log(pc5.green("\n\u2713 copied to clipboard") + pc5.dim(` (${text.length} chars)`));
|
|
3544
|
+
}
|
|
3545
|
+
});
|
|
3546
|
+
for (const cmd of program.commands) {
|
|
3547
|
+
const name = cmd.name();
|
|
3548
|
+
if (commandExamples(name) !== "") {
|
|
3549
|
+
cmd.addHelpText("after", () => commandExamples(name));
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
try {
|
|
3553
|
+
await program.parseAsync(argv, { from: "node" });
|
|
3554
|
+
} catch (err) {
|
|
3555
|
+
const e = err;
|
|
3556
|
+
if (e.code === "commander.helpDisplayed" || e.code === "commander.version") {
|
|
3557
|
+
return 0;
|
|
3558
|
+
}
|
|
3559
|
+
if (e.code === "commander.help") return 0;
|
|
3560
|
+
if (typeof e.exitCode === "number" && e.code?.startsWith("commander.")) {
|
|
3561
|
+
return e.exitCode;
|
|
3562
|
+
}
|
|
3563
|
+
console.error(pc5.red(e.message ?? String(err)));
|
|
3564
|
+
return 1;
|
|
3565
|
+
}
|
|
3566
|
+
return exitCode;
|
|
3567
|
+
}
|
|
3568
|
+
|
|
3569
|
+
// src/bin.ts
|
|
3570
|
+
function flush(stream) {
|
|
3571
|
+
if (stream.writableLength === 0) return Promise.resolve();
|
|
3572
|
+
return new Promise((resolve13) => {
|
|
3573
|
+
stream.write("", () => resolve13());
|
|
3574
|
+
stream.once("error", () => resolve13());
|
|
3575
|
+
stream.once("close", () => resolve13());
|
|
3576
|
+
});
|
|
3577
|
+
}
|
|
3578
|
+
var code = await main(process.argv);
|
|
3579
|
+
await Promise.all([flush(process.stdout), flush(process.stderr)]);
|
|
3580
|
+
process.exit(code);
|
|
3581
|
+
//# sourceMappingURL=bin.js.map
|
|
3582
|
+
//# sourceMappingURL=bin.js.map
|