create-thally-docs 0.10.30 → 0.10.32

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.
@@ -3,19 +3,19 @@
3
3
  // src/stable-scaffold-release.json
4
4
  var stable_scaffold_release_default = {
5
5
  schemaVersion: 1,
6
- id: "2026-09-06.95ad2d40.928cbdb0",
6
+ id: "2026-09-07.e15b143d.9b3be068",
7
7
  source: {
8
8
  repository: "thallylabs/starter",
9
- commitSha: "95ad2d4079675f94cd8cc398b3dbd7bfa171c839",
10
- treeSha: "fade1339ec1776b9f54707648f548ac223f76c0d",
11
- archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/95ad2d4079675f94cd8cc398b3dbd7bfa171c839",
9
+ commitSha: "e15b143d39935f3d6e5fe6127bffc2c3a7591318",
10
+ treeSha: "fff5cb413f83c8da17d2e4ec3df933d29a5613de",
11
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/e15b143d39935f3d6e5fe6127bffc2c3a7591318",
12
12
  manifestPath: "starter-release.json",
13
- manifestSha256: "ddb2d5b8b96f8d772b1a69ca283cda02fb515b26da3675a62d869f83aee8458b"
13
+ manifestSha256: "07fb1d733700c547874b88eaefcbfc4decef6acd204c765deb81ae3462ee66ba"
14
14
  },
15
15
  runtime: {
16
16
  repository: "thallylabs/thally",
17
- commitSha: "928cbdb04d247e8422eff5ca117ae6ad9bfdf2c1",
18
- treeSha: "090e1e5186e49e3686cc7434efb8835fbb31be8f",
17
+ commitSha: "9b3be06826c9bc3a8b6402b25e597b3b7cc80cfe",
18
+ treeSha: "1a87aa11c382e3d8d88785b85af37e1b823dad86",
19
19
  contentSource: "assets",
20
20
  identityContractVersion: 1
21
21
  },
@@ -24,6 +24,46 @@ var stable_scaffold_release_default = {
24
24
 
25
25
  // src/previous-scaffold-releases.json
26
26
  var previous_scaffold_releases_default = [
27
+ {
28
+ schemaVersion: 1,
29
+ id: "2026-09-07.06a4aca7.ad30d29f",
30
+ source: {
31
+ repository: "thallylabs/starter",
32
+ commitSha: "06a4aca7978ef3e0fc80389095db35f2ff37c96b",
33
+ treeSha: "13f3a315752259e58dac314233a4519dc2e0ec42",
34
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/06a4aca7978ef3e0fc80389095db35f2ff37c96b",
35
+ manifestPath: "starter-release.json",
36
+ manifestSha256: "3a7cfa791c92ec24c6b0b755e2f1fe988e9fb408015ccc69f78681102e8bf65d"
37
+ },
38
+ runtime: {
39
+ repository: "thallylabs/thally",
40
+ commitSha: "ad30d29f79221092c873c611953097161fed0f60",
41
+ treeSha: "1a217f1304557264ed231774f5bb9d81dc639ed0",
42
+ contentSource: "assets",
43
+ identityContractVersion: 1
44
+ },
45
+ starterVersion: 2
46
+ },
47
+ {
48
+ schemaVersion: 1,
49
+ id: "2026-09-06.95ad2d40.928cbdb0",
50
+ source: {
51
+ repository: "thallylabs/starter",
52
+ commitSha: "95ad2d4079675f94cd8cc398b3dbd7bfa171c839",
53
+ treeSha: "fade1339ec1776b9f54707648f548ac223f76c0d",
54
+ archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/95ad2d4079675f94cd8cc398b3dbd7bfa171c839",
55
+ manifestPath: "starter-release.json",
56
+ manifestSha256: "ddb2d5b8b96f8d772b1a69ca283cda02fb515b26da3675a62d869f83aee8458b"
57
+ },
58
+ runtime: {
59
+ repository: "thallylabs/thally",
60
+ commitSha: "928cbdb04d247e8422eff5ca117ae6ad9bfdf2c1",
61
+ treeSha: "090e1e5186e49e3686cc7434efb8835fbb31be8f",
62
+ contentSource: "assets",
63
+ identityContractVersion: 1
64
+ },
65
+ starterVersion: 2
66
+ },
27
67
  {
28
68
  schemaVersion: 1,
29
69
  id: "2026-09-06.323199bb.6c240f71",
package/dist/index.js CHANGED
@@ -1,24 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- migrateDocs
4
- } from "./chunk-UYGDCVX3.js";
3
+ migrateDocs,
4
+ parseFrontmatter,
5
+ runCheck
6
+ } from "./chunk-E5URHAYU.js";
5
7
  import {
6
8
  logo,
7
- readDocsJson,
8
9
  scaffold,
9
10
  slugify,
10
- success,
11
- writeDocsJson
12
- } from "./chunk-4RPHM4NA.js";
11
+ success
12
+ } from "./chunk-74SGZLEY.js";
13
13
  import "./chunk-ORUAMPNF.js";
14
- import "./chunk-XAUCGJ6B.js";
15
- import "./chunk-PMAJ4BVM.js";
16
- import "./chunk-SSKXNB6D.js";
14
+ import "./chunk-FLSOVTVO.js";
15
+ import "./chunk-HYGEXPZG.js";
16
+ import "./chunk-ZFCZE7M7.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
+ import { confirm as confirm2 } from "@inquirer/prompts";
22
23
 
23
24
  // src/prompts.ts
24
25
  import { confirm, input, select } from "@inquirer/prompts";
@@ -199,393 +200,15 @@ async function gatherAnswers(dirArg, useDefaults, installPreference) {
199
200
  // src/index.ts
200
201
  import { parseGitHubRepositoryUrl as parseGitHubRepositoryUrl2 } from "@thallylabs/migrate";
201
202
 
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
203
  // src/translate.ts
581
- import { readFileSync as readFileSync2, writeFileSync, existsSync as existsSync2, mkdirSync } from "fs";
582
- import { join as join2, dirname } from "path";
204
+ import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
205
+ import { join, dirname } from "path";
583
206
  import { input as input2 } from "@inquirer/prompts";
584
207
  import Anthropic from "@anthropic-ai/sdk";
585
208
  import pLimit from "p-limit";
586
- function readDocsJson2(projectDir) {
587
- const docsPath = join2(projectDir, "docs.json");
588
- const raw = readFileSync2(docsPath, "utf8");
209
+ function readDocsJson(projectDir) {
210
+ const docsPath = join(projectDir, "docs.json");
211
+ const raw = readFileSync(docsPath, "utf8");
589
212
  return JSON.parse(raw);
590
213
  }
591
214
  function collectPageIds(pages) {
@@ -628,12 +251,12 @@ function getAllPageIds(config) {
628
251
  return { ids, skippedApiTabs, hrefOnlyPages };
629
252
  }
630
253
  function findSourceFile(projectDir, pageId) {
631
- const contentRoot = join2(projectDir, "src", "content");
254
+ const contentRoot = join(projectDir, "src", "content");
632
255
  const candidates = [
633
- join2(contentRoot, `${pageId}.mdx`),
634
- join2(contentRoot, `${pageId}/index.mdx`)
256
+ join(contentRoot, `${pageId}.mdx`),
257
+ join(contentRoot, `${pageId}/index.mdx`)
635
258
  ];
636
- return candidates.find((p) => existsSync2(p)) ?? null;
259
+ return candidates.find((p) => existsSync(p)) ?? null;
637
260
  }
638
261
  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
262
 
@@ -689,7 +312,7 @@ ${sourceContent}`
689
312
  return text.trim();
690
313
  }
691
314
  async function runTranslateCommand(locale, pages, force, apiKey, model, yes, projectDir) {
692
- const config = readDocsJson2(projectDir);
315
+ const config = readDocsJson(projectDir);
693
316
  if (!config.i18n) {
694
317
  console.error("\n \u274C No i18n config found in docs.json.");
695
318
  console.error(' Add an "i18n" block to docs.json first:');
@@ -730,7 +353,7 @@ async function runTranslateCommand(locale, pages, force, apiKey, model, yes, pro
730
353
  console.log("");
731
354
  }
732
355
  const targetPageIds = pages ?? allPageIds;
733
- const contentRoot = join2(projectDir, "src", "content");
356
+ const contentRoot = join(projectDir, "src", "content");
734
357
  const toTranslate = [];
735
358
  for (const pageId of targetPageIds) {
736
359
  const sourceFile = findSourceFile(projectDir, pageId);
@@ -739,8 +362,8 @@ async function runTranslateCommand(locale, pages, force, apiKey, model, yes, pro
739
362
  continue;
740
363
  }
741
364
  const relativeFromContent = sourceFile.slice(contentRoot.length + 1);
742
- const targetFile = join2(contentRoot, locale, relativeFromContent);
743
- if (existsSync2(targetFile) && !force) {
365
+ const targetFile = join(contentRoot, locale, relativeFromContent);
366
+ if (existsSync(targetFile) && !force) {
744
367
  console.log(` \u23ED ${pageId} (already translated, use --force to overwrite)`);
745
368
  continue;
746
369
  }
@@ -757,11 +380,11 @@ async function runTranslateCommand(locale, pages, force, apiKey, model, yes, pro
757
380
  }
758
381
  console.log("");
759
382
  if (!yes) {
760
- const confirm2 = await input2({
383
+ const confirm3 = await input2({
761
384
  message: " Proceed? (Y/n):",
762
385
  default: "Y"
763
386
  });
764
- if (confirm2.toLowerCase() === "n") {
387
+ if (confirm3.toLowerCase() === "n") {
765
388
  console.log("\n Aborted.");
766
389
  return;
767
390
  }
@@ -774,7 +397,7 @@ async function runTranslateCommand(locale, pages, force, apiKey, model, yes, pro
774
397
  toTranslate.map(
775
398
  ({ pageId, sourceFile, targetFile }) => limit(async () => {
776
399
  try {
777
- const sourceContent = readFileSync2(sourceFile, "utf8");
400
+ const sourceContent = readFileSync(sourceFile, "utf8");
778
401
  const parsed = parseFrontmatter(sourceContent);
779
402
  if (!parsed.data.title) {
780
403
  console.warn(` \u26A0 ${pageId}: missing title in frontmatter \u2014 translating anyway`);
@@ -830,6 +453,8 @@ var commandFlags = {
830
453
  "--into",
831
454
  "--max-pages",
832
455
  "--platform",
456
+ "--skip-validation",
457
+ "--trust-source",
833
458
  "--yes",
834
459
  "-y"
835
460
  ]),
@@ -876,6 +501,8 @@ Options:
876
501
  --docs-dir <path> Override the detected documentation directory
877
502
  --max-pages <count> Limit a public URL crawl to 1-1000 pages
878
503
  --platform <name> Use mintlify, docusaurus, or auto
504
+ --skip-validation Import only; explicitly skip content and build verification
505
+ --trust-source Allow installation and build execution of trusted source code
879
506
  --api-key <key> Anthropic API key for non-Markdown conversion
880
507
  -y, --yes Skip interactive prompts
881
508
  -h, --help Show this help
@@ -992,7 +619,14 @@ async function runMigrateCommand() {
992
619
  if (docsDir) console.log(` Docs dir: ${docsDir}`);
993
620
  console.log(` Platform: ${platform ?? "auto-detect"}`);
994
621
  console.log("");
995
- await migrateDocs({
622
+ let trustSource = flags.includes("--trust-source");
623
+ if (!trustSource && !yes && !flags.includes("--skip-validation") && process.stdin.isTTY && process.stdout.isTTY) {
624
+ trustSource = await confirm2({
625
+ message: "Installation and build validation execute imported code on this machine. Do you trust this source and authorize execution?",
626
+ default: false
627
+ });
628
+ }
629
+ const result = await migrateDocs({
996
630
  sourceUrl,
997
631
  projectDir,
998
632
  into: isInto,
@@ -1001,8 +635,11 @@ async function runMigrateCommand() {
1001
635
  docsDir,
1002
636
  maxPages,
1003
637
  platform,
1004
- yes
638
+ yes,
639
+ skipValidation: flags.includes("--skip-validation"),
640
+ trustSource
1005
641
  });
642
+ if (result.validation.content === "failed" || result.validation.build === "failed") process.exitCode = 1;
1006
643
  }
1007
644
  async function runScaffoldCommand() {
1008
645
  const useDefaults = flags.includes("--yes") || flags.includes("-y");
@@ -1010,7 +647,7 @@ async function runScaffoldCommand() {
1010
647
  const dirArg = positional[0];
1011
648
  if (dirArg) {
1012
649
  const resolved = resolve2(dirArg);
1013
- if (existsSync3(resolved) && readdirSync2(resolved).length > 0) {
650
+ if (existsSync2(resolved) && readdirSync(resolved).length > 0) {
1014
651
  console.error(`
1015
652
  \u274C Directory "${resolved}" already exists and is not empty.`);
1016
653
  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,10 @@ 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;
45
+ /** Explicitly allow local installation/build execution of reviewed source. Never implied by yes. */
46
+ trustSource?: boolean;
24
47
  }
25
48
  interface MigrateResult {
26
49
  pagesWritten: number;
@@ -28,6 +51,8 @@ interface MigrateResult {
28
51
  projectDir: string;
29
52
  platform: MigrationBundle['platform'];
30
53
  warnings: Array<MigrationWarning>;
54
+ validation: MigrationValidation;
55
+ reportPath: string;
31
56
  }
32
57
  /** Import a GitHub docs repository or public docs URL into a Thally project. */
33
58
  declare function migrateDocs(options: MigrateOptions): Promise<MigrateResult>;