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/dist/index.js CHANGED
@@ -1,22 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- migrateDocs
4
- } from "./chunk-DXD5Q42N.js";
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
- writeDocsJson
12
- } from "./chunk-NIYRBHH7.js";
11
+ success
12
+ } from "./chunk-5TBIIZHQ.js";
13
13
  import "./chunk-ORUAMPNF.js";
14
- import "./chunk-EXH4PYPC.js";
15
- import "./chunk-IRFT5MUJ.js";
16
- import "./chunk-VLLFRIPP.js";
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 existsSync3, readdirSync as readdirSync2 } from "fs";
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 as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
582
- import { join as join2, dirname } from "path";
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 readDocsJson2(projectDir) {
587
- const docsPath = join2(projectDir, "docs.json");
588
- const raw = readFileSync2(docsPath, "utf8");
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 = join2(projectDir, "src", "content");
253
+ const contentRoot = join(projectDir, "src", "content");
632
254
  const candidates = [
633
- join2(contentRoot, `${pageId}.mdx`),
634
- join2(contentRoot, `${pageId}/index.mdx`)
255
+ join(contentRoot, `${pageId}.mdx`),
256
+ join(contentRoot, `${pageId}/index.mdx`)
635
257
  ];
636
- return candidates.find((p) => existsSync2(p)) ?? null;
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 = readDocsJson2(projectDir);
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 = join2(projectDir, "src", "content");
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 = join2(contentRoot, locale, relativeFromContent);
743
- if (existsSync2(targetFile) && !force) {
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 = readFileSync2(sourceFile, "utf8");
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 (existsSync3(resolved) && readdirSync2(resolved).length > 0) {
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);
@@ -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>;
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  migrateDocs
4
- } from "../chunk-DXD5Q42N.js";
5
- import "../chunk-NIYRBHH7.js";
4
+ } from "../chunk-V5PERCVX.js";
5
+ import "../chunk-5TBIIZHQ.js";
6
6
  import "../chunk-ORUAMPNF.js";
7
- import "../chunk-EXH4PYPC.js";
8
- import "../chunk-IRFT5MUJ.js";
9
- import "../chunk-VLLFRIPP.js";
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
@@ -3,7 +3,7 @@ import {
3
3
  STABLE_SCAFFOLD_RELEASE,
4
4
  SUPPORTED_SCAFFOLD_RELEASES,
5
5
  isStableScaffoldRelease
6
- } from "./chunk-VLLFRIPP.js";
6
+ } from "./chunk-I2OURFVJ.js";
7
7
  export {
8
8
  STABLE_SCAFFOLD_RELEASE,
9
9
  SUPPORTED_SCAFFOLD_RELEASES,
package/dist/scaffold.js CHANGED
@@ -1,18 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  scaffold
4
- } from "./chunk-NIYRBHH7.js";
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-EXH4PYPC.js";
12
- import "./chunk-IRFT5MUJ.js";
11
+ } from "./chunk-DPYULA35.js";
12
+ import "./chunk-HOQBNMQ4.js";
13
13
  import {
14
14
  STABLE_SCAFFOLD_RELEASE
15
- } from "./chunk-VLLFRIPP.js";
15
+ } from "./chunk-I2OURFVJ.js";
16
16
  export {
17
17
  STABLE_SCAFFOLD_RELEASE,
18
18
  STARTER_ARCHIVE_ROOT,
@@ -8,8 +8,8 @@ import {
8
8
  planStarterRuntimeSync,
9
9
  readStarterReleaseManifest,
10
10
  starterManifestSha256
11
- } from "./chunk-IRFT5MUJ.js";
12
- import "./chunk-VLLFRIPP.js";
11
+ } from "./chunk-HOQBNMQ4.js";
12
+ import "./chunk-I2OURFVJ.js";
13
13
  export {
14
14
  applyStarterRuntimeSyncPlan,
15
15
  classifyStarterPath,
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  downloadStarter
4
- } from "./chunk-EXH4PYPC.js";
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-IRFT5MUJ.js";
12
+ } from "./chunk-HOQBNMQ4.js";
13
13
  import {
14
14
  STABLE_SCAFFOLD_RELEASE,
15
15
  SUPPORTED_SCAFFOLD_RELEASES
16
- } from "./chunk-VLLFRIPP.js";
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.31",
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.4",
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"