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.
- package/README.md +27 -0
- package/dist/{chunk-4RPHM4NA.js → chunk-74SGZLEY.js} +1 -1
- package/dist/chunk-E5URHAYU.js +611 -0
- package/dist/{chunk-XAUCGJ6B.js → chunk-FLSOVTVO.js} +2 -2
- package/dist/{chunk-PMAJ4BVM.js → chunk-HYGEXPZG.js} +1 -1
- package/dist/{chunk-SSKXNB6D.js → chunk-ZFCZE7M7.js} +47 -7
- package/dist/index.js +44 -407
- package/dist/migrate/index.d.ts +25 -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-UYGDCVX3.js +0 -113
|
@@ -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-
|
|
6
|
+
id: "2026-09-07.e15b143d.9b3be068",
|
|
7
7
|
source: {
|
|
8
8
|
repository: "thallylabs/starter",
|
|
9
|
-
commitSha: "
|
|
10
|
-
treeSha: "
|
|
11
|
-
archiveUrl: "https://codeload.github.com/thallylabs/starter/tar.gz/
|
|
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: "
|
|
13
|
+
manifestSha256: "07fb1d733700c547874b88eaefcbfc4decef6acd204c765deb81ae3462ee66ba"
|
|
14
14
|
},
|
|
15
15
|
runtime: {
|
|
16
16
|
repository: "thallylabs/thally",
|
|
17
|
-
commitSha: "
|
|
18
|
-
treeSha: "
|
|
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
|
-
|
|
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
|
-
|
|
12
|
-
} from "./chunk-4RPHM4NA.js";
|
|
11
|
+
success
|
|
12
|
+
} from "./chunk-74SGZLEY.js";
|
|
13
13
|
import "./chunk-ORUAMPNF.js";
|
|
14
|
-
import "./chunk-
|
|
15
|
-
import "./chunk-
|
|
16
|
-
import "./chunk-
|
|
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
|
|
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
|
|
582
|
-
import { join
|
|
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
|
|
587
|
-
const docsPath =
|
|
588
|
-
const raw =
|
|
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 =
|
|
254
|
+
const contentRoot = join(projectDir, "src", "content");
|
|
632
255
|
const candidates = [
|
|
633
|
-
|
|
634
|
-
|
|
256
|
+
join(contentRoot, `${pageId}.mdx`),
|
|
257
|
+
join(contentRoot, `${pageId}/index.mdx`)
|
|
635
258
|
];
|
|
636
|
-
return candidates.find((p) =>
|
|
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 =
|
|
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 =
|
|
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 =
|
|
743
|
-
if (
|
|
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
|
|
383
|
+
const confirm3 = await input2({
|
|
761
384
|
message: " Proceed? (Y/n):",
|
|
762
385
|
default: "Y"
|
|
763
386
|
});
|
|
764
|
-
if (
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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);
|
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,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>;
|