create-thally-docs 0.10.31 → 0.10.33
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/README.md +24 -0
- package/dist/{chunk-NIYRBHH7.js → chunk-5TBIIZHQ.js} +1 -1
- package/dist/{chunk-EXH4PYPC.js → chunk-DPYULA35.js} +2 -2
- package/dist/{chunk-IRFT5MUJ.js → chunk-HOQBNMQ4.js} +1 -1
- package/dist/{chunk-VLLFRIPP.js → chunk-I2OURFVJ.js} +47 -7
- package/dist/chunk-V5PERCVX.js +608 -0
- package/dist/index.js +31 -405
- package/dist/migrate/index.d.ts +23 -0
- package/dist/migrate/index.js +5 -5
- package/dist/release.js +1 -1
- package/dist/scaffold.js +4 -4
- package/dist/starter-sync.js +2 -2
- package/dist/starter-update.js +3 -3
- package/package.json +2 -2
- package/dist/chunk-DXD5Q42N.js +0 -113
package/dist/index.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
migrateDocs
|
|
4
|
-
|
|
3
|
+
migrateDocs,
|
|
4
|
+
parseFrontmatter,
|
|
5
|
+
runCheck
|
|
6
|
+
} from "./chunk-V5PERCVX.js";
|
|
5
7
|
import {
|
|
6
8
|
logo,
|
|
7
|
-
readDocsJson,
|
|
8
9
|
scaffold,
|
|
9
10
|
slugify,
|
|
10
|
-
success
|
|
11
|
-
|
|
12
|
-
} from "./chunk-NIYRBHH7.js";
|
|
11
|
+
success
|
|
12
|
+
} from "./chunk-5TBIIZHQ.js";
|
|
13
13
|
import "./chunk-ORUAMPNF.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
14
|
+
import "./chunk-DPYULA35.js";
|
|
15
|
+
import "./chunk-HOQBNMQ4.js";
|
|
16
|
+
import "./chunk-I2OURFVJ.js";
|
|
17
17
|
|
|
18
18
|
// src/index.ts
|
|
19
|
-
import { existsSync as
|
|
19
|
+
import { existsSync as existsSync2, readdirSync } from "fs";
|
|
20
20
|
import { createRequire } from "module";
|
|
21
21
|
import { resolve as resolve2 } from "path";
|
|
22
22
|
|
|
@@ -199,393 +199,15 @@ async function gatherAnswers(dirArg, useDefaults, installPreference) {
|
|
|
199
199
|
// src/index.ts
|
|
200
200
|
import { parseGitHubRepositoryUrl as parseGitHubRepositoryUrl2 } from "@thallylabs/migrate";
|
|
201
201
|
|
|
202
|
-
// src/check.ts
|
|
203
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
204
|
-
import { join, extname, relative } from "path";
|
|
205
|
-
import { execFileSync } from "child_process";
|
|
206
|
-
|
|
207
|
-
// src/frontmatter.ts
|
|
208
|
-
import { parse as parseYaml } from "yaml";
|
|
209
|
-
function parseFrontmatter(raw) {
|
|
210
|
-
const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
|
|
211
|
-
const opening = /^---([^\r\n]*)\r?\n/.exec(source);
|
|
212
|
-
if (!opening || opening[1].startsWith("-")) return { content: source, data: {} };
|
|
213
|
-
const language = opening[1].trim().toLowerCase();
|
|
214
|
-
const remainder = source.slice(opening[0].length);
|
|
215
|
-
const closing = /^---[ \t]*\r?$/m.exec(remainder);
|
|
216
|
-
const matter = closing ? remainder.slice(0, closing.index) : remainder;
|
|
217
|
-
let content = closing ? remainder.slice(closing.index + closing[0].length) : "";
|
|
218
|
-
if (content.startsWith("\r\n")) content = content.slice(2);
|
|
219
|
-
else if (content.startsWith("\n")) content = content.slice(1);
|
|
220
|
-
if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
|
|
221
|
-
return { content, data: {} };
|
|
222
|
-
}
|
|
223
|
-
const parsed = parseYaml(matter);
|
|
224
|
-
return {
|
|
225
|
-
content,
|
|
226
|
-
data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
|
|
227
|
-
};
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
// src/check.ts
|
|
231
|
-
import { parse as parseYaml2 } from "yaml";
|
|
232
|
-
function gitLocal(projectDir, args2) {
|
|
233
|
-
try {
|
|
234
|
-
const out = execFileSync("git", args2, { cwd: projectDir, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
235
|
-
return { ok: true, out: out.trim() };
|
|
236
|
-
} catch {
|
|
237
|
-
return { ok: false, out: "" };
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
function checkDrift(projectDir, file, data, issues) {
|
|
241
|
-
const sources = data.sources;
|
|
242
|
-
const verifiedCommit = data.verifiedCommit;
|
|
243
|
-
if (!Array.isArray(sources) || sources.length === 0 || typeof verifiedCommit !== "string" || !verifiedCommit.trim()) {
|
|
244
|
-
return;
|
|
245
|
-
}
|
|
246
|
-
const commit = verifiedCommit.trim();
|
|
247
|
-
if (!gitLocal(projectDir, ["cat-file", "-e", `${commit}^{commit}`]).ok) {
|
|
248
|
-
issues.push({
|
|
249
|
-
severity: "warning",
|
|
250
|
-
message: `Cannot verify freshness: verifiedCommit "${commit.slice(0, 8)}" is not in git history \u2014 run with a full clone (fetch-depth: 0).`,
|
|
251
|
-
file
|
|
252
|
-
});
|
|
253
|
-
return;
|
|
254
|
-
}
|
|
255
|
-
for (const src of sources) {
|
|
256
|
-
if (typeof src !== "string" || !src.trim()) continue;
|
|
257
|
-
const colon = src.indexOf(":");
|
|
258
|
-
let filePath = src;
|
|
259
|
-
if (colon > 0) {
|
|
260
|
-
const alias = src.slice(0, colon);
|
|
261
|
-
if (alias !== "." && alias !== "self") {
|
|
262
|
-
issues.push({
|
|
263
|
-
severity: "warning",
|
|
264
|
-
message: `Cross-repo source "${src}" \u2014 drift check skipped (needs the referenced repo; see multi-repo setup).`,
|
|
265
|
-
file
|
|
266
|
-
});
|
|
267
|
-
continue;
|
|
268
|
-
}
|
|
269
|
-
filePath = src.slice(colon + 1);
|
|
270
|
-
}
|
|
271
|
-
filePath = filePath.replace(/^\.\//, "").replace(/#.*$/, "");
|
|
272
|
-
const changed = gitLocal(projectDir, ["log", "--format=%H", `${commit}..HEAD`, "--", filePath]).out;
|
|
273
|
-
if (changed) {
|
|
274
|
-
const n = changed.split("\n").filter(Boolean).length;
|
|
275
|
-
issues.push({
|
|
276
|
-
severity: "warning",
|
|
277
|
-
message: `Drift: source "${src}" changed in ${n} commit(s) since it was verified \u2014 this page may be stale.`,
|
|
278
|
-
file
|
|
279
|
-
});
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
}
|
|
283
|
-
function collectNavPageIds(groups, seen, duplicates) {
|
|
284
|
-
for (const page of groups) {
|
|
285
|
-
if (typeof page === "string") {
|
|
286
|
-
if (seen.has(page)) duplicates.add(page);
|
|
287
|
-
else seen.add(page);
|
|
288
|
-
} else if (page.pages) {
|
|
289
|
-
collectNavPageIds(page.pages, seen, duplicates);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
function scanMdx(dir, results) {
|
|
294
|
-
let entries;
|
|
295
|
-
try {
|
|
296
|
-
entries = readdirSync(dir);
|
|
297
|
-
} catch {
|
|
298
|
-
return;
|
|
299
|
-
}
|
|
300
|
-
for (const entry of entries) {
|
|
301
|
-
const fullPath = join(dir, entry);
|
|
302
|
-
try {
|
|
303
|
-
const stat = statSync(fullPath);
|
|
304
|
-
if (stat.isDirectory()) scanMdx(fullPath, results);
|
|
305
|
-
else if (extname(entry).toLowerCase() === ".mdx") results.push(fullPath);
|
|
306
|
-
} catch {
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
function addOrphanToNav(projectDir, pageId) {
|
|
311
|
-
const config = readDocsJson(projectDir);
|
|
312
|
-
const tab = config.tabs.find((candidate) => !candidate.href && !candidate.api && (candidate.pages?.length || candidate.groups?.length));
|
|
313
|
-
if (!tab) return;
|
|
314
|
-
if (tab.pages) {
|
|
315
|
-
if (!tab.pages.includes(pageId)) {
|
|
316
|
-
tab.pages.push(pageId);
|
|
317
|
-
writeDocsJson(projectDir, config);
|
|
318
|
-
}
|
|
319
|
-
return;
|
|
320
|
-
}
|
|
321
|
-
if (!tab.groups) return;
|
|
322
|
-
const lastGroup = tab.groups[tab.groups.length - 1];
|
|
323
|
-
const existing = lastGroup.pages.filter((p) => typeof p === "string");
|
|
324
|
-
if (!existing.includes(pageId)) {
|
|
325
|
-
lastGroup.pages.push(pageId);
|
|
326
|
-
writeDocsJson(projectDir, config);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
function slugify2(text) {
|
|
330
|
-
return text.toLowerCase().trim().replace(/[^\w\s-]/g, "").replace(/\s+/g, "-").replace(/-+/g, "-");
|
|
331
|
-
}
|
|
332
|
-
function extractHeadingAnchors(content) {
|
|
333
|
-
const anchors = /* @__PURE__ */ new Set();
|
|
334
|
-
for (const line of content.split("\n")) {
|
|
335
|
-
const m = /^#{1,6}\s+(.+?)\s*#*\s*$/.exec(line);
|
|
336
|
-
if (m) anchors.add(slugify2(m[1]));
|
|
337
|
-
}
|
|
338
|
-
return anchors;
|
|
339
|
-
}
|
|
340
|
-
function extractLinks(content) {
|
|
341
|
-
const links = [];
|
|
342
|
-
const lines = content.split("\n");
|
|
343
|
-
let inFence = false;
|
|
344
|
-
for (let i = 0; i < lines.length; i++) {
|
|
345
|
-
if (/^\s*(```|~~~)/.test(lines[i])) {
|
|
346
|
-
inFence = !inFence;
|
|
347
|
-
continue;
|
|
348
|
-
}
|
|
349
|
-
if (inFence) continue;
|
|
350
|
-
const line = lines[i].replace(/`[^`]*`/g, "");
|
|
351
|
-
for (const m of line.matchAll(/\[[^\]]*\]\(([^)\s]+)(?:\s+"[^"]*")?\)/g)) {
|
|
352
|
-
links.push({ target: m[1], line: i + 1 });
|
|
353
|
-
}
|
|
354
|
-
for (const m of line.matchAll(/href=["']([^"']+)["']/g)) {
|
|
355
|
-
links.push({ target: m[1], line: i + 1 });
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
return links;
|
|
359
|
-
}
|
|
360
|
-
function localizedPage(pageId, secondaryLocales) {
|
|
361
|
-
const [first, ...rest] = pageId.split("/");
|
|
362
|
-
if (secondaryLocales.has(first) && rest.length > 0) {
|
|
363
|
-
return { navPageId: rest.join("/"), locale: first };
|
|
364
|
-
}
|
|
365
|
-
return { navPageId: pageId };
|
|
366
|
-
}
|
|
367
|
-
function pageIdToPath(pageId, secondaryLocales) {
|
|
368
|
-
const { navPageId, locale } = localizedPage(pageId, secondaryLocales);
|
|
369
|
-
const basePath = navPageId === "introduction" ? "/" : `/${navPageId}`;
|
|
370
|
-
return locale ? `/${locale}${basePath === "/" ? "" : basePath}` : basePath;
|
|
371
|
-
}
|
|
372
|
-
function validateOpenApi(projectDir, source, issues) {
|
|
373
|
-
const specPath = source.startsWith("/") ? join(projectDir, "public", source.slice(1)) : join(projectDir, source);
|
|
374
|
-
if (!existsSync(specPath)) {
|
|
375
|
-
issues.push({ severity: "error", message: `API reference points at "${source}" but the file does not exist`, file: source });
|
|
376
|
-
return;
|
|
377
|
-
}
|
|
378
|
-
let spec;
|
|
379
|
-
try {
|
|
380
|
-
const raw = readFileSync(specPath, "utf8");
|
|
381
|
-
spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml2(raw);
|
|
382
|
-
} catch (err) {
|
|
383
|
-
issues.push({ severity: "error", message: `OpenAPI spec is not valid ${source.endsWith(".json") ? "JSON" : "YAML"}: ${err.message}`, file: source });
|
|
384
|
-
return;
|
|
385
|
-
}
|
|
386
|
-
const s = spec;
|
|
387
|
-
if (typeof s?.openapi !== "string" && typeof s?.swagger !== "string") {
|
|
388
|
-
issues.push({ severity: "error", message: 'OpenAPI spec is missing the "openapi" (or "swagger") version field', file: source });
|
|
389
|
-
}
|
|
390
|
-
if (typeof s?.info !== "object" || s.info === null) {
|
|
391
|
-
issues.push({ severity: "error", message: 'OpenAPI spec is missing the "info" object', file: source });
|
|
392
|
-
}
|
|
393
|
-
const paths = s?.paths;
|
|
394
|
-
if (typeof paths !== "object" || paths === null) {
|
|
395
|
-
issues.push({ severity: "error", message: 'OpenAPI spec is missing the "paths" object', file: source });
|
|
396
|
-
} else {
|
|
397
|
-
const methods = /* @__PURE__ */ new Set(["get", "post", "put", "patch", "delete", "options", "head", "trace"]);
|
|
398
|
-
for (const [p, ops] of Object.entries(paths)) {
|
|
399
|
-
if (typeof ops !== "object" || ops === null) {
|
|
400
|
-
issues.push({ severity: "error", message: `OpenAPI path "${p}" is not an object`, file: source });
|
|
401
|
-
continue;
|
|
402
|
-
}
|
|
403
|
-
const hasOp = Object.keys(ops).some((k) => methods.has(k.toLowerCase()));
|
|
404
|
-
if (!hasOp) {
|
|
405
|
-
issues.push({ severity: "warning", message: `OpenAPI path "${p}" has no operations`, file: source });
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
}
|
|
410
|
-
async function runCheck(projectDir, options) {
|
|
411
|
-
const { fix, ci } = options;
|
|
412
|
-
if (!existsSync(join(projectDir, "docs.json"))) {
|
|
413
|
-
console.error(`
|
|
414
|
-
\u274C Not a Thally project: docs.json not found in ${projectDir}
|
|
415
|
-
`);
|
|
416
|
-
return 1;
|
|
417
|
-
}
|
|
418
|
-
const contentDir = join(projectDir, "src", "content");
|
|
419
|
-
const issues = [];
|
|
420
|
-
const config = readDocsJson(projectDir);
|
|
421
|
-
const secondaryLocales = new Set(
|
|
422
|
-
(config.i18n?.locales ?? []).map((locale) => locale.code).filter((code) => code !== config.i18n?.defaultLocale)
|
|
423
|
-
);
|
|
424
|
-
const generatedApiPaths = /* @__PURE__ */ new Set([
|
|
425
|
-
"/api",
|
|
426
|
-
...Array.from(secondaryLocales, (locale) => `/${locale}/api`)
|
|
427
|
-
]);
|
|
428
|
-
const redirectDestinations = new Map(
|
|
429
|
-
(config.redirects ?? []).map((redirect) => [
|
|
430
|
-
redirect.source.replace(/\/$/, "") || "/",
|
|
431
|
-
redirect.destination.replace(/\/$/, "") || "/"
|
|
432
|
-
])
|
|
433
|
-
);
|
|
434
|
-
const navPageIds = /* @__PURE__ */ new Set();
|
|
435
|
-
const duplicates = /* @__PURE__ */ new Set();
|
|
436
|
-
for (const tab of config.tabs) {
|
|
437
|
-
const hasNavigationNodes = Boolean(tab.pages?.length || tab.groups?.length);
|
|
438
|
-
if (tab.href && !hasNavigationNodes) {
|
|
439
|
-
if (tab.href.startsWith("/")) navPageIds.add(tab.href.slice(1) || "introduction");
|
|
440
|
-
continue;
|
|
441
|
-
}
|
|
442
|
-
if (tab.api && tab.api.navigation !== false && !hasNavigationNodes) continue;
|
|
443
|
-
if (!hasNavigationNodes) {
|
|
444
|
-
issues.push({ severity: "error", message: `Tab "${tab.tab}" has no groups and no href \u2014 it will render empty` });
|
|
445
|
-
continue;
|
|
446
|
-
}
|
|
447
|
-
collectNavPageIds(tab.pages ?? [], navPageIds, duplicates);
|
|
448
|
-
collectNavPageIds(tab.groups ?? [], navPageIds, duplicates);
|
|
449
|
-
}
|
|
450
|
-
for (const dup of duplicates) {
|
|
451
|
-
issues.push({ severity: "warning", message: `[duplicate] "${dup}" appears more than once in docs.json` });
|
|
452
|
-
}
|
|
453
|
-
for (const pageId of navPageIds) {
|
|
454
|
-
const candidates = [join(contentDir, `${pageId}.mdx`), join(contentDir, `${pageId}/index.mdx`)];
|
|
455
|
-
if (!candidates.some((c) => existsSync(c))) {
|
|
456
|
-
issues.push({ severity: "error", message: `"${pageId}" is in docs.json but has no MDX file`, file: `src/content/${pageId}.mdx` });
|
|
457
|
-
}
|
|
458
|
-
}
|
|
459
|
-
const allFiles = [];
|
|
460
|
-
if (existsSync(contentDir)) scanMdx(contentDir, allFiles);
|
|
461
|
-
const fixedOrphans = [];
|
|
462
|
-
const validPaths = /* @__PURE__ */ new Set(["/"]);
|
|
463
|
-
const anchorsByPath = /* @__PURE__ */ new Map();
|
|
464
|
-
const linksByFile = [];
|
|
465
|
-
for (const filePath of allFiles) {
|
|
466
|
-
const rel = filePath.slice(contentDir.length + 1).replace(/\.mdx$/, "").replace(/\\/g, "/");
|
|
467
|
-
const pageId = rel.endsWith("/index") ? rel.slice(0, -6) : rel;
|
|
468
|
-
const { navPageId } = localizedPage(pageId, secondaryLocales);
|
|
469
|
-
if (!navPageIds.has(navPageId)) {
|
|
470
|
-
if (fix) {
|
|
471
|
-
addOrphanToNav(projectDir, pageId);
|
|
472
|
-
fixedOrphans.push(pageId);
|
|
473
|
-
} else {
|
|
474
|
-
issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative(projectDir, filePath) });
|
|
475
|
-
}
|
|
476
|
-
}
|
|
477
|
-
let data = {};
|
|
478
|
-
let content = "";
|
|
479
|
-
let lineOffset = 0;
|
|
480
|
-
try {
|
|
481
|
-
const raw = readFileSync(filePath, "utf8");
|
|
482
|
-
const parsed = parseFrontmatter(raw);
|
|
483
|
-
data = parsed.data;
|
|
484
|
-
content = parsed.content;
|
|
485
|
-
lineOffset = raw.slice(0, raw.indexOf(content)).split("\n").length - 1;
|
|
486
|
-
} catch {
|
|
487
|
-
issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative(projectDir, filePath) });
|
|
488
|
-
continue;
|
|
489
|
-
}
|
|
490
|
-
const rel2 = relative(projectDir, filePath);
|
|
491
|
-
if (!data.title) issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
|
|
492
|
-
if (!data.description) issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
|
|
493
|
-
if (typeof data.openapi !== "string" && content.trim().length < 50) {
|
|
494
|
-
issues.push({ severity: "warning", message: `Very short body (${content.trim().length} chars) \u2014 page may be empty`, file: rel2 });
|
|
495
|
-
}
|
|
496
|
-
if (options.drift) checkDrift(projectDir, rel2, data, issues);
|
|
497
|
-
const path = pageIdToPath(pageId, secondaryLocales);
|
|
498
|
-
const anchors = extractHeadingAnchors(content);
|
|
499
|
-
validPaths.add(path);
|
|
500
|
-
anchorsByPath.set(path, anchors);
|
|
501
|
-
linksByFile.push({ file: rel2, path, anchors, links: extractLinks(content), offset: lineOffset });
|
|
502
|
-
}
|
|
503
|
-
for (const { file, anchors, links, offset } of linksByFile) {
|
|
504
|
-
for (const { target, line: contentLine } of links) {
|
|
505
|
-
const line = contentLine + offset;
|
|
506
|
-
if (/^(https?:|mailto:|tel:)/i.test(target)) continue;
|
|
507
|
-
if (target.startsWith("#")) {
|
|
508
|
-
const anchor2 = target.slice(1);
|
|
509
|
-
if (anchor2 && !anchors.has(anchor2)) {
|
|
510
|
-
issues.push({ severity: "warning", message: `Broken anchor: "${target}" not found on this page`, file, line });
|
|
511
|
-
}
|
|
512
|
-
continue;
|
|
513
|
-
}
|
|
514
|
-
if (!target.startsWith("/")) continue;
|
|
515
|
-
const [beforeHash, anchor] = target.split("#");
|
|
516
|
-
let path = beforeHash.split("?")[0];
|
|
517
|
-
if (path.length > 1) path = path.replace(/\/$/, "");
|
|
518
|
-
const isGeneratedApiPath = Array.from(generatedApiPaths).some(
|
|
519
|
-
(prefix) => path === prefix || path.startsWith(`${prefix}/`)
|
|
520
|
-
);
|
|
521
|
-
if (isGeneratedApiPath || path.startsWith("/_next") || /\.[a-z0-9]+$/i.test(path)) continue;
|
|
522
|
-
const redirectedPath = redirectDestinations.get(path);
|
|
523
|
-
if (!validPaths.has(path) && !(redirectedPath && validPaths.has(redirectedPath))) {
|
|
524
|
-
issues.push({ severity: "error", message: `Broken link: "${target}" \u2014 no page at "${path}"`, file, line });
|
|
525
|
-
} else if (anchor && !anchorsByPath.get(path)?.has(anchor)) {
|
|
526
|
-
issues.push({ severity: "warning", message: `Broken anchor: "${target}" \u2014 no heading "#${anchor}" on that page`, file, line });
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
for (const tab of config.tabs) {
|
|
531
|
-
if (tab.api?.source) validateOpenApi(projectDir, tab.api.source, issues);
|
|
532
|
-
}
|
|
533
|
-
const errors = issues.filter((i) => i.severity === "error");
|
|
534
|
-
const warnings = issues.filter((i) => i.severity === "warning");
|
|
535
|
-
if (ci) {
|
|
536
|
-
for (const issue of issues) {
|
|
537
|
-
const loc = issue.file ? `file=${issue.file}${issue.line ? `,line=${issue.line}` : ""}` : "";
|
|
538
|
-
console.log(`::${issue.severity} ${loc}::${issue.message}`);
|
|
539
|
-
}
|
|
540
|
-
console.log(`
|
|
541
|
-
thally check: ${errors.length} error(s), ${warnings.length} warning(s)`);
|
|
542
|
-
return errors.length > 0 ? 1 : 0;
|
|
543
|
-
}
|
|
544
|
-
console.log(`
|
|
545
|
-
Linting ${projectDir}...
|
|
546
|
-
`);
|
|
547
|
-
if (errors.length === 0 && warnings.length === 0 && fixedOrphans.length === 0) {
|
|
548
|
-
console.log(" \u2705 No issues found.\n");
|
|
549
|
-
return 0;
|
|
550
|
-
}
|
|
551
|
-
console.log(` \u274C ${errors.length} error${errors.length !== 1 ? "s" : ""}, \u26A0\uFE0F ${warnings.length} warning${warnings.length !== 1 ? "s" : ""}
|
|
552
|
-
`);
|
|
553
|
-
if (errors.length > 0) {
|
|
554
|
-
console.log(" ERRORS:");
|
|
555
|
-
for (const issue of errors) {
|
|
556
|
-
console.log(` ${issue.message}`);
|
|
557
|
-
if (issue.file) console.log(` \u2192 ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
|
|
558
|
-
}
|
|
559
|
-
console.log("");
|
|
560
|
-
}
|
|
561
|
-
if (warnings.length > 0) {
|
|
562
|
-
console.log(" WARNINGS:");
|
|
563
|
-
for (const issue of warnings) {
|
|
564
|
-
console.log(` ${issue.message}`);
|
|
565
|
-
if (issue.file) console.log(` \u2192 ${issue.file}${issue.line ? `:${issue.line}` : ""}`);
|
|
566
|
-
}
|
|
567
|
-
console.log("");
|
|
568
|
-
}
|
|
569
|
-
if (fixedOrphans.length > 0) {
|
|
570
|
-
console.log(` \u2705 Auto-fixed ${fixedOrphans.length} orphan page${fixedOrphans.length > 1 ? "s" : ""} (added to nav):`);
|
|
571
|
-
for (const p of fixedOrphans) console.log(` + ${p}`);
|
|
572
|
-
console.log("");
|
|
573
|
-
}
|
|
574
|
-
if (!fix && warnings.some((w) => w.message.includes("orphan"))) {
|
|
575
|
-
console.log(" Tip: run with --fix to auto-add orphan pages to navigation.\n");
|
|
576
|
-
}
|
|
577
|
-
return errors.length > 0 ? 1 : 0;
|
|
578
|
-
}
|
|
579
|
-
|
|
580
202
|
// src/translate.ts
|
|
581
|
-
import { readFileSync
|
|
582
|
-
import { join
|
|
203
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
|
|
204
|
+
import { join, dirname } from "path";
|
|
583
205
|
import { input as input2 } from "@inquirer/prompts";
|
|
584
206
|
import Anthropic from "@anthropic-ai/sdk";
|
|
585
207
|
import pLimit from "p-limit";
|
|
586
|
-
function
|
|
587
|
-
const docsPath =
|
|
588
|
-
const raw =
|
|
208
|
+
function readDocsJson(projectDir) {
|
|
209
|
+
const docsPath = join(projectDir, "docs.json");
|
|
210
|
+
const raw = readFileSync(docsPath, "utf8");
|
|
589
211
|
return JSON.parse(raw);
|
|
590
212
|
}
|
|
591
213
|
function collectPageIds(pages) {
|
|
@@ -628,12 +250,12 @@ function getAllPageIds(config) {
|
|
|
628
250
|
return { ids, skippedApiTabs, hrefOnlyPages };
|
|
629
251
|
}
|
|
630
252
|
function findSourceFile(projectDir, pageId) {
|
|
631
|
-
const contentRoot =
|
|
253
|
+
const contentRoot = join(projectDir, "src", "content");
|
|
632
254
|
const candidates = [
|
|
633
|
-
|
|
634
|
-
|
|
255
|
+
join(contentRoot, `${pageId}.mdx`),
|
|
256
|
+
join(contentRoot, `${pageId}/index.mdx`)
|
|
635
257
|
];
|
|
636
|
-
return candidates.find((p) =>
|
|
258
|
+
return candidates.find((p) => existsSync(p)) ?? null;
|
|
637
259
|
}
|
|
638
260
|
var TRANSLATION_SYSTEM_PROMPT = `You are a professional documentation translator. You will receive an MDX documentation file and translate it into the target language.
|
|
639
261
|
|
|
@@ -689,7 +311,7 @@ ${sourceContent}`
|
|
|
689
311
|
return text.trim();
|
|
690
312
|
}
|
|
691
313
|
async function runTranslateCommand(locale, pages, force, apiKey, model, yes, projectDir) {
|
|
692
|
-
const config =
|
|
314
|
+
const config = readDocsJson(projectDir);
|
|
693
315
|
if (!config.i18n) {
|
|
694
316
|
console.error("\n \u274C No i18n config found in docs.json.");
|
|
695
317
|
console.error(' Add an "i18n" block to docs.json first:');
|
|
@@ -730,7 +352,7 @@ async function runTranslateCommand(locale, pages, force, apiKey, model, yes, pro
|
|
|
730
352
|
console.log("");
|
|
731
353
|
}
|
|
732
354
|
const targetPageIds = pages ?? allPageIds;
|
|
733
|
-
const contentRoot =
|
|
355
|
+
const contentRoot = join(projectDir, "src", "content");
|
|
734
356
|
const toTranslate = [];
|
|
735
357
|
for (const pageId of targetPageIds) {
|
|
736
358
|
const sourceFile = findSourceFile(projectDir, pageId);
|
|
@@ -739,8 +361,8 @@ async function runTranslateCommand(locale, pages, force, apiKey, model, yes, pro
|
|
|
739
361
|
continue;
|
|
740
362
|
}
|
|
741
363
|
const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
|
|
742
|
-
const targetFile =
|
|
743
|
-
if (
|
|
364
|
+
const targetFile = join(contentRoot, locale, relativeFromContent);
|
|
365
|
+
if (existsSync(targetFile) && !force) {
|
|
744
366
|
console.log(` \u23ED ${pageId} (already translated, use --force to overwrite)`);
|
|
745
367
|
continue;
|
|
746
368
|
}
|
|
@@ -774,7 +396,7 @@ async function runTranslateCommand(locale, pages, force, apiKey, model, yes, pro
|
|
|
774
396
|
toTranslate.map(
|
|
775
397
|
({ pageId, sourceFile, targetFile }) => limit(async () => {
|
|
776
398
|
try {
|
|
777
|
-
const sourceContent =
|
|
399
|
+
const sourceContent = readFileSync(sourceFile, "utf8");
|
|
778
400
|
const parsed = parseFrontmatter(sourceContent);
|
|
779
401
|
if (!parsed.data.title) {
|
|
780
402
|
console.warn(` \u26A0 ${pageId}: missing title in frontmatter \u2014 translating anyway`);
|
|
@@ -830,6 +452,7 @@ var commandFlags = {
|
|
|
830
452
|
"--into",
|
|
831
453
|
"--max-pages",
|
|
832
454
|
"--platform",
|
|
455
|
+
"--skip-validation",
|
|
833
456
|
"--yes",
|
|
834
457
|
"-y"
|
|
835
458
|
]),
|
|
@@ -876,6 +499,7 @@ Options:
|
|
|
876
499
|
--docs-dir <path> Override the detected documentation directory
|
|
877
500
|
--max-pages <count> Limit a public URL crawl to 1-1000 pages
|
|
878
501
|
--platform <name> Use mintlify, docusaurus, or auto
|
|
502
|
+
--skip-validation Import only; explicitly skip content and build verification
|
|
879
503
|
--api-key <key> Anthropic API key for non-Markdown conversion
|
|
880
504
|
-y, --yes Skip interactive prompts
|
|
881
505
|
-h, --help Show this help
|
|
@@ -992,7 +616,7 @@ async function runMigrateCommand() {
|
|
|
992
616
|
if (docsDir) console.log(` Docs dir: ${docsDir}`);
|
|
993
617
|
console.log(` Platform: ${platform ?? "auto-detect"}`);
|
|
994
618
|
console.log("");
|
|
995
|
-
await migrateDocs({
|
|
619
|
+
const result = await migrateDocs({
|
|
996
620
|
sourceUrl,
|
|
997
621
|
projectDir,
|
|
998
622
|
into: isInto,
|
|
@@ -1001,8 +625,10 @@ async function runMigrateCommand() {
|
|
|
1001
625
|
docsDir,
|
|
1002
626
|
maxPages,
|
|
1003
627
|
platform,
|
|
1004
|
-
yes
|
|
628
|
+
yes,
|
|
629
|
+
skipValidation: flags.includes("--skip-validation")
|
|
1005
630
|
});
|
|
631
|
+
if (result.validation.content === "failed" || result.validation.build === "failed") process.exitCode = 1;
|
|
1006
632
|
}
|
|
1007
633
|
async function runScaffoldCommand() {
|
|
1008
634
|
const useDefaults = flags.includes("--yes") || flags.includes("-y");
|
|
@@ -1010,7 +636,7 @@ async function runScaffoldCommand() {
|
|
|
1010
636
|
const dirArg = positional[0];
|
|
1011
637
|
if (dirArg) {
|
|
1012
638
|
const resolved = resolve2(dirArg);
|
|
1013
|
-
if (
|
|
639
|
+
if (existsSync2(resolved) && readdirSync(resolved).length > 0) {
|
|
1014
640
|
console.error(`
|
|
1015
641
|
\u274C Directory "${resolved}" already exists and is not empty.`);
|
|
1016
642
|
process.exit(1);
|
package/dist/migrate/index.d.ts
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
import { MigrationPlatform, MigrationFetcher, MigrationBundle, MigrationWarning } from '@thallylabs/migrate';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Validate authored navigation and links against the routes the runtime can
|
|
5
|
+
* serve, including locale fallback and literal redirect chains. Validation is
|
|
6
|
+
* read-only unless the caller explicitly enables orphan-navigation fixes.
|
|
7
|
+
*/
|
|
8
|
+
interface LintIssue {
|
|
9
|
+
severity: 'error' | 'warning';
|
|
10
|
+
message: string;
|
|
11
|
+
file?: string;
|
|
12
|
+
line?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface MigrationValidation {
|
|
16
|
+
content: 'passed' | 'failed' | 'skipped';
|
|
17
|
+
build: 'passed' | 'failed' | 'skipped';
|
|
18
|
+
messages: Array<string>;
|
|
19
|
+
diagnostics: Array<LintIssue>;
|
|
20
|
+
}
|
|
21
|
+
|
|
3
22
|
/**
|
|
4
23
|
* CLI materializer for the shared Thally migration engine. Discovery completes
|
|
5
24
|
* before scaffolding or writing, and every generated path is proven to remain
|
|
@@ -21,6 +40,8 @@ interface MigrateOptions {
|
|
|
21
40
|
platform?: MigrationPlatform;
|
|
22
41
|
/** Optional host fetch boundary; used by Thally Cloud adapters and tests. */
|
|
23
42
|
fetcher?: MigrationFetcher;
|
|
43
|
+
/** Explicitly opt out of content/build gates; the report remains unverified. */
|
|
44
|
+
skipValidation?: boolean;
|
|
24
45
|
}
|
|
25
46
|
interface MigrateResult {
|
|
26
47
|
pagesWritten: number;
|
|
@@ -28,6 +49,8 @@ interface MigrateResult {
|
|
|
28
49
|
projectDir: string;
|
|
29
50
|
platform: MigrationBundle['platform'];
|
|
30
51
|
warnings: Array<MigrationWarning>;
|
|
52
|
+
validation: MigrationValidation;
|
|
53
|
+
reportPath: string;
|
|
31
54
|
}
|
|
32
55
|
/** Import a GitHub docs repository or public docs URL into a Thally project. */
|
|
33
56
|
declare function migrateDocs(options: MigrateOptions): Promise<MigrateResult>;
|
package/dist/migrate/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
migrateDocs
|
|
4
|
-
} from "../chunk-
|
|
5
|
-
import "../chunk-
|
|
4
|
+
} from "../chunk-V5PERCVX.js";
|
|
5
|
+
import "../chunk-5TBIIZHQ.js";
|
|
6
6
|
import "../chunk-ORUAMPNF.js";
|
|
7
|
-
import "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
7
|
+
import "../chunk-DPYULA35.js";
|
|
8
|
+
import "../chunk-HOQBNMQ4.js";
|
|
9
|
+
import "../chunk-I2OURFVJ.js";
|
|
10
10
|
export {
|
|
11
11
|
migrateDocs
|
|
12
12
|
};
|
package/dist/release.js
CHANGED
package/dist/scaffold.js
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
scaffold
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-5TBIIZHQ.js";
|
|
5
5
|
import "./chunk-ORUAMPNF.js";
|
|
6
6
|
import {
|
|
7
7
|
STARTER_ARCHIVE_ROOT,
|
|
8
8
|
STARTER_COMMIT_SHA,
|
|
9
9
|
STARTER_REPOSITORY,
|
|
10
10
|
validateStarterArchiveEntry
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-DPYULA35.js";
|
|
12
|
+
import "./chunk-HOQBNMQ4.js";
|
|
13
13
|
import {
|
|
14
14
|
STABLE_SCAFFOLD_RELEASE
|
|
15
|
-
} from "./chunk-
|
|
15
|
+
} from "./chunk-I2OURFVJ.js";
|
|
16
16
|
export {
|
|
17
17
|
STABLE_SCAFFOLD_RELEASE,
|
|
18
18
|
STARTER_ARCHIVE_ROOT,
|
package/dist/starter-sync.js
CHANGED
|
@@ -8,8 +8,8 @@ import {
|
|
|
8
8
|
planStarterRuntimeSync,
|
|
9
9
|
readStarterReleaseManifest,
|
|
10
10
|
starterManifestSha256
|
|
11
|
-
} from "./chunk-
|
|
12
|
-
import "./chunk-
|
|
11
|
+
} from "./chunk-HOQBNMQ4.js";
|
|
12
|
+
import "./chunk-I2OURFVJ.js";
|
|
13
13
|
export {
|
|
14
14
|
applyStarterRuntimeSyncPlan,
|
|
15
15
|
classifyStarterPath,
|
package/dist/starter-update.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
downloadStarter
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-DPYULA35.js";
|
|
5
5
|
import {
|
|
6
6
|
applyStarterRuntimeSyncPlan,
|
|
7
7
|
mergeStarterOwnershipContracts,
|
|
@@ -9,11 +9,11 @@ import {
|
|
|
9
9
|
planStarterRuntimeSync,
|
|
10
10
|
readStarterReleaseManifest,
|
|
11
11
|
starterManifestSha256
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-HOQBNMQ4.js";
|
|
13
13
|
import {
|
|
14
14
|
STABLE_SCAFFOLD_RELEASE,
|
|
15
15
|
SUPPORTED_SCAFFOLD_RELEASES
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-I2OURFVJ.js";
|
|
17
17
|
|
|
18
18
|
// src/starter-update.ts
|
|
19
19
|
import {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-thally-docs",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.33",
|
|
4
4
|
"description": "Scaffold an open-source Thally docs site for people, search engines, and AI tools.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"dependencies": {
|
|
45
45
|
"@anthropic-ai/sdk": "^0.36.0",
|
|
46
46
|
"@inquirer/prompts": "^7.0.0",
|
|
47
|
-
"@thallylabs/migrate": "0.2.
|
|
47
|
+
"@thallylabs/migrate": "0.2.5",
|
|
48
48
|
"p-limit": "^6.1.0",
|
|
49
49
|
"tar": "^7.5.22",
|
|
50
50
|
"yaml": "^2.6.0"
|