dogsbay 0.2.0-beta.37 → 0.2.0-beta.38
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,6 +3,8 @@ import { join, relative, resolve, basename, dirname } from "node:path";
|
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
import pc from "picocolors";
|
|
5
5
|
import YAML from "yaml";
|
|
6
|
+
import { emitAstroPages, emitAgentReadinessFiles, emitConfigDerivedFiles, emitSiteConfig, } from "@dogsbay/format-astro";
|
|
7
|
+
import { emitPluginRuntime } from "@dogsbay/format-astro";
|
|
6
8
|
export async function importMkdocs(source, options) {
|
|
7
9
|
const sourceDir = resolve(source);
|
|
8
10
|
const mkdocsYml = join(sourceDir, "mkdocs.yml");
|
|
@@ -131,9 +133,11 @@ export async function importMkdocs(source, options) {
|
|
|
131
133
|
}
|
|
132
134
|
console.log(pc.green(`Copied`) + ` ${Object.keys(macrosData).length} macro data files`);
|
|
133
135
|
}
|
|
134
|
-
// 3. Extract nav structure
|
|
136
|
+
// 3. Extract nav structure. nav.json is written later by
|
|
137
|
+
// emitAstroPages (Phase 1 of plans/mkdocs-import-architecture.md
|
|
138
|
+
// — single canonical emitter). nav.yml is written alongside as
|
|
139
|
+
// the human-editable source of truth.
|
|
135
140
|
const nav = convertNav(config.nav);
|
|
136
|
-
writeFileSync(join(outputDir, "src", "data", "nav.json"), JSON.stringify(nav, null, 2));
|
|
137
141
|
console.log(pc.green(`Extracted`) + ` navigation (${nav.length} top-level items)`);
|
|
138
142
|
// 4. Generate package.json
|
|
139
143
|
const dogsbayVersion = options.local ? "file:" : "^0.1.0";
|
|
@@ -167,13 +171,27 @@ export async function importMkdocs(source, options) {
|
|
|
167
171
|
dependencies: {
|
|
168
172
|
astro: "^6.0.0",
|
|
169
173
|
tailwindcss: "^4.0.0",
|
|
170
|
-
|
|
174
|
+
// Pinned exactly to 4.2.2 — every later release in the 4.2.x
|
|
175
|
+
// line (4.2.3, 4.2.4) and the 4.3.x line breaks Astro 6's
|
|
176
|
+
// rolldown-vite with "Missing field `tsconfigPaths` on
|
|
177
|
+
// BindingViteResolvePluginConfig.resolveOptions" inside
|
|
178
|
+
// oxcResolvePlugin. The tilde range ~4.2.2 wasn't strict enough
|
|
179
|
+
// — npm floated it to 4.2.4. Drop when Astro 6 picks up a
|
|
180
|
+
// compatible rolldown build OR @tailwindcss/vite restores the
|
|
181
|
+
// prior shape.
|
|
182
|
+
"@tailwindcss/vite": "4.2.2",
|
|
171
183
|
"tailwind-variants": "^0.3.0",
|
|
172
184
|
"markdown-it": "^14.0.0",
|
|
173
185
|
shiki: "^4.0.0",
|
|
174
186
|
"@shikijs/transformers": "^4.0.0",
|
|
175
187
|
...dogsbayDeps,
|
|
176
188
|
katex: "^0.16.44",
|
|
189
|
+
// Transitive of @dogsbay/primitives → @floating-ui/dom. npm
|
|
190
|
+
// doesn't hoist second-level transitives under `file:` links,
|
|
191
|
+
// and Rollup then can't resolve @floating-ui/core at build
|
|
192
|
+
// time. Listing it at the top level satisfies both linked
|
|
193
|
+
// and registry installs.
|
|
194
|
+
"@floating-ui/core": "^1.7.0",
|
|
177
195
|
},
|
|
178
196
|
}, null, 2) + "\n");
|
|
179
197
|
if (options.local) {
|
|
@@ -315,9 +333,50 @@ export default defineConfig({
|
|
|
315
333
|
}
|
|
316
334
|
console.log(pc.green(`Added`) + ` ${needed.length} components`);
|
|
317
335
|
}
|
|
318
|
-
// 13. Generate static .astro pages (default, skip with --dynamic)
|
|
336
|
+
// 13. Generate static .astro pages (default, skip with --dynamic).
|
|
337
|
+
// Phase 1 of plans/mkdocs-import-architecture.md routes emission
|
|
338
|
+
// through @dogsbay/format-astro's canonical emitters so this
|
|
339
|
+
// command gains llms.txt, .md mirrors, sitemap, _headers, and
|
|
340
|
+
// middleware.ts without duplicating logic.
|
|
319
341
|
if (!options.dynamic) {
|
|
320
|
-
await
|
|
342
|
+
const pages = await collectExportPages(outputDir, pluginOpts, mkdocstringsConfig, nav);
|
|
343
|
+
const siteUrlRaw = config.site_url;
|
|
344
|
+
const siteUrl = typeof siteUrlRaw === "string" ? siteUrlRaw : undefined;
|
|
345
|
+
const repoUrl = typeof config.repo_url === "string" ? config.repo_url : undefined;
|
|
346
|
+
const astroOptions = {
|
|
347
|
+
siteName,
|
|
348
|
+
basePath: "/docs",
|
|
349
|
+
siteUrl,
|
|
350
|
+
repoUrl,
|
|
351
|
+
sourceDir: fullDocsDir,
|
|
352
|
+
llmsTxt: true,
|
|
353
|
+
mdMirror: true,
|
|
354
|
+
};
|
|
355
|
+
// Empty plugin runtime — emits the MarkdownContentStack passthrough
|
|
356
|
+
// wrapper that emitAstroPages's page templates import. Without
|
|
357
|
+
// this, every generated page fails to compile.
|
|
358
|
+
emitPluginRuntime({
|
|
359
|
+
outputDir,
|
|
360
|
+
clientModules: [],
|
|
361
|
+
styles: [],
|
|
362
|
+
clientConfigs: [],
|
|
363
|
+
});
|
|
364
|
+
// Empty switcher map — single-source MkDocs imports have no
|
|
365
|
+
// multi-source axes. The file must exist because emitAstroPages
|
|
366
|
+
// page templates `import switcherMapData from "@/data/switcherMap.json"`.
|
|
367
|
+
writeFileSync(join(outputDir, "src", "data", "switcherMap.json"), "[]\n");
|
|
368
|
+
// Refresh site.json with the full SiteConfig shape that
|
|
369
|
+
// emitAstroPages page templates expect (siteName, siteUrl,
|
|
370
|
+
// repoUrl, …). Overwrites the minimal one written at step 8.
|
|
371
|
+
emitSiteConfig(outputDir, siteName, astroOptions);
|
|
372
|
+
const { generated } = await emitAstroPages(pages, nav, outputDir, astroOptions);
|
|
373
|
+
emitConfigDerivedFiles(outputDir, astroOptions);
|
|
374
|
+
emitAgentReadinessFiles(pages, nav, outputDir, siteName, astroOptions);
|
|
375
|
+
// nav.yml — human-editable source of truth (per plan: "edit
|
|
376
|
+
// nav.yml; nav.json is regenerated on every build"). Written
|
|
377
|
+
// AFTER emitAstroPages so it carries the same shape as nav.json.
|
|
378
|
+
writeFileSync(join(outputDir, "src", "data", "nav.yml"), YAML.stringify(nav));
|
|
379
|
+
console.log(pc.green(`Generated`) + ` ${generated} static .astro pages`);
|
|
321
380
|
}
|
|
322
381
|
// Remove the catch-all route unless --dynamic or --keep-dynamic
|
|
323
382
|
if (!options.dynamic && !options.keepDynamic) {
|
|
@@ -350,16 +409,27 @@ export default defineConfig({
|
|
|
350
409
|
* Generate static .astro pages from markdown content.
|
|
351
410
|
* Each .md file becomes a .astro page with real component imports.
|
|
352
411
|
*/
|
|
353
|
-
|
|
412
|
+
/**
|
|
413
|
+
* Parse each MkDocs `.md` source through `format-mkdocs`'s parser
|
|
414
|
+
* (plugin pipeline: admonitions, tabs, snippets, autodoc, macros,
|
|
415
|
+
* variants, ...) and collect into ExportPage[] for emission via
|
|
416
|
+
* `@dogsbay/format-astro.emitAstroPages`.
|
|
417
|
+
*
|
|
418
|
+
* Pre-Phase 1 this function wrote `.astro` pages directly with a
|
|
419
|
+
* hand-templated DocsLayout wrapper, which bypassed format-astro's
|
|
420
|
+
* agent-readiness emission (llms.txt, .md mirrors, sitemap,
|
|
421
|
+
* _headers). Now the parser output is handed to format-astro's
|
|
422
|
+
* canonical emitter so import-mkdocs gains those files for free.
|
|
423
|
+
*/
|
|
424
|
+
async function collectExportPages(outputDir, pluginOpts, mkdocstringsConfig, _nav) {
|
|
354
425
|
// Dynamic imports for the parser — these packages are workspace dependencies
|
|
355
426
|
// but may not have type declarations visible to the CLI's tsconfig
|
|
356
427
|
const MarkdownIt = (await import(/* @vite-ignore */ "markdown-it")).default;
|
|
357
428
|
const { mkdocsPlugin } = await import(/* @vite-ignore */ "@dogsbay/format-mkdocs");
|
|
358
429
|
const { parseMkdocsMarkdown } = await import(/* @vite-ignore */ "@dogsbay/format-mkdocs/loader");
|
|
359
|
-
const { treeToAstro } = await import(/* @vite-ignore */ "@dogsbay/format-mkdocs/export/to-astro");
|
|
360
430
|
const docsDir = join(outputDir, "src", "content", "docs");
|
|
361
|
-
|
|
362
|
-
|
|
431
|
+
// pages directory is created (and populated) by
|
|
432
|
+
// @dogsbay/format-astro.emitAstroPages — collector only reads.
|
|
363
433
|
// Build the same markdown-it instance as the catch-all route
|
|
364
434
|
const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
|
|
365
435
|
const mdOpts = {
|
|
@@ -429,77 +499,38 @@ async function generateStaticPages(outputDir, pluginOpts, mkdocstringsConfig, op
|
|
|
429
499
|
}
|
|
430
500
|
parseOpts.autodoc = autodocOpts;
|
|
431
501
|
}
|
|
432
|
-
// Walk docs directory and
|
|
502
|
+
// Walk docs directory and parse each .md into a TreeNode tree.
|
|
503
|
+
// Collection only — emission is the caller's job.
|
|
433
504
|
const mdFiles = findMarkdownFiles(docsDir);
|
|
434
|
-
|
|
435
|
-
// Read nav data
|
|
436
|
-
const navJson = JSON.stringify(nav);
|
|
505
|
+
const pages = [];
|
|
437
506
|
for (const mdPath of mdFiles) {
|
|
438
507
|
const relPath = relative(docsDir, mdPath);
|
|
439
|
-
|
|
440
|
-
//
|
|
441
|
-
|
|
508
|
+
// Slug = path relative to docs dir, without .md, with index files
|
|
509
|
+
// collapsed (mkdocs convention: foo/index.md → /foo). Root
|
|
510
|
+
// index.md keeps slug "index" so emitAstroPages writes it as
|
|
511
|
+
// src/pages/<basePath>/index.astro.
|
|
512
|
+
const rawSlug = relPath.replace(/\.md$/, "").replace(/\/index$/, "").replace(/^index$/, "");
|
|
513
|
+
const slug = rawSlug || "index";
|
|
442
514
|
try {
|
|
443
515
|
const source = readFileSync(mdPath, "utf-8");
|
|
444
516
|
const { tree, headings } = await parseMkdocsMarkdown(source, md, {
|
|
445
517
|
...parseOpts,
|
|
446
518
|
env: { filePath: relPath },
|
|
447
519
|
});
|
|
448
|
-
const result = treeToAstro(tree);
|
|
449
520
|
const title = headings.find((h) => h.depth === 1)?.text || "Documentation";
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
'import navData from "@/data/nav.json";',
|
|
458
|
-
'import siteConfig from "@/data/site.json";',
|
|
459
|
-
...result.imports,
|
|
460
|
-
"",
|
|
461
|
-
`const headings = ${JSON.stringify(headings)};`,
|
|
462
|
-
`const nav = navData;`,
|
|
463
|
-
`const currentPath = "${slug ? `/docs/${slug}` : "/docs"}";`,
|
|
464
|
-
`const { prev, next } = getPagination(currentPath, nav as any[]);`,
|
|
465
|
-
`const title = ${JSON.stringify(title)};`,
|
|
466
|
-
"---",
|
|
467
|
-
"",
|
|
468
|
-
`<DocsLayout`,
|
|
469
|
-
` siteName={siteConfig.siteName}`,
|
|
470
|
-
` title={title}`,
|
|
471
|
-
` nav={nav as any[]}`,
|
|
472
|
-
` headings={headings}`,
|
|
473
|
-
` prev={prev}`,
|
|
474
|
-
` next={next}`,
|
|
475
|
-
` repoUrl={siteConfig.repoUrl || undefined}`,
|
|
476
|
-
`>`,
|
|
477
|
-
` <article class="docs-prose">`,
|
|
478
|
-
result.body.split("\n").map((l) => ` ${l}`).join("\n"),
|
|
479
|
-
` </article>`,
|
|
480
|
-
`</DocsLayout>`,
|
|
481
|
-
];
|
|
482
|
-
// Add script tags for client-side elements
|
|
483
|
-
if (result.scripts.length > 0) {
|
|
484
|
-
pageLines.push("");
|
|
485
|
-
for (const script of result.scripts) {
|
|
486
|
-
pageLines.push(`<script>`);
|
|
487
|
-
pageLines.push(script);
|
|
488
|
-
pageLines.push(`</script>`);
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
const pageContent = pageLines.join("\n") + "\n";
|
|
492
|
-
// Write the page
|
|
493
|
-
const pagePath = join(pagesDir, `${pageSlug}.astro`);
|
|
494
|
-
mkdirSync(dirname(pagePath), { recursive: true });
|
|
495
|
-
writeFileSync(pagePath, pageContent);
|
|
496
|
-
generated++;
|
|
521
|
+
pages.push({
|
|
522
|
+
slug,
|
|
523
|
+
title,
|
|
524
|
+
tree,
|
|
525
|
+
headings,
|
|
526
|
+
frontmatter: {},
|
|
527
|
+
});
|
|
497
528
|
}
|
|
498
529
|
catch (err) {
|
|
499
|
-
console.log(pc.yellow(` Warning: failed to
|
|
530
|
+
console.log(pc.yellow(` Warning: failed to parse ${relPath}: ${err.message}`));
|
|
500
531
|
}
|
|
501
532
|
}
|
|
502
|
-
|
|
533
|
+
return pages;
|
|
503
534
|
}
|
|
504
535
|
// ── File Discovery ───────────────────────────────────
|
|
505
536
|
function findMarkdownFiles(dir) {
|
|
@@ -0,0 +1,536 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `dogsbay migrate-mkdocs` — one-way migration from a MkDocs site to a
|
|
3
|
+
* fully scaffolded Dogsbay site whose source-of-truth is Dogsbay-MD.
|
|
4
|
+
*
|
|
5
|
+
* Phase 3 of plans/mkdocs-import-architecture.md.
|
|
6
|
+
*
|
|
7
|
+
* Unlike `dogsbay import-mkdocs` (which keeps MkDocs as the editable
|
|
8
|
+
* source and re-renders to Astro on every run), `migrate-mkdocs` is
|
|
9
|
+
* one-shot: MkDocs goes in, a scaffolded Dogsbay project comes out,
|
|
10
|
+
* and the MkDocs source can be deleted. From this point on the user
|
|
11
|
+
* edits `./content/*.md` (canonical Dogsbay-MD) and runs `dogsbay
|
|
12
|
+
* site build` to produce the Astro site.
|
|
13
|
+
*
|
|
14
|
+
* Output layout:
|
|
15
|
+
*
|
|
16
|
+
* <output>/
|
|
17
|
+
* content/ ← NEW source-of-truth, Dogsbay-MD
|
|
18
|
+
* index.md
|
|
19
|
+
* guides/
|
|
20
|
+
* install.md
|
|
21
|
+
* public/ ← assets carried over from MkDocs docs/
|
|
22
|
+
* img/
|
|
23
|
+
* src/
|
|
24
|
+
* data/nav.yml ← human-editable nav (file:-shape)
|
|
25
|
+
* styles/{theme,global}.css
|
|
26
|
+
* components/ui/
|
|
27
|
+
* dogsbay.config.yml ← sources: [./content], agent on
|
|
28
|
+
* astro.config.mjs ← scaffolded by emitSiteScaffold
|
|
29
|
+
* package.json ← scaffolded
|
|
30
|
+
* tsconfig.json
|
|
31
|
+
* MIGRATION.md ← what survived, what didn't, re-run cmd
|
|
32
|
+
*
|
|
33
|
+
* Autodoc handling (Phase 3 default = inline):
|
|
34
|
+
* `::: module.Class` directives in MkDocs sources are resolved at
|
|
35
|
+
* migration time via @dogsbay/format-mkdocs's autodoc pipeline
|
|
36
|
+
* (which delegates to @dogsbay/autodoc-python). The resolved
|
|
37
|
+
* api-* TreeNodes are serialized into the Dogsbay-MD content; on
|
|
38
|
+
* subsequent `site build` they render through format-astro's
|
|
39
|
+
* serializer. Phase 4 will add a `:::autodoc` directive to
|
|
40
|
+
* Dogsbay-MD for live re-rendering at build time — until then
|
|
41
|
+
* migrated reference pages are a snapshot at migration time.
|
|
42
|
+
*/
|
|
43
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, cpSync, readdirSync, statSync, } from "node:fs";
|
|
44
|
+
import { join, relative, resolve, basename, dirname, posix } from "node:path";
|
|
45
|
+
import pc from "picocolors";
|
|
46
|
+
import YAML from "yaml";
|
|
47
|
+
import { emitSiteScaffold } from "@dogsbay/format-astro";
|
|
48
|
+
import { serializeConfig } from "../config/serialize.js";
|
|
49
|
+
export async function migrateMkdocs(source, options) {
|
|
50
|
+
const sourceDir = resolve(source);
|
|
51
|
+
const mkdocsYml = join(sourceDir, "mkdocs.yml");
|
|
52
|
+
if (!existsSync(mkdocsYml)) {
|
|
53
|
+
console.log(pc.red(`Error: No mkdocs.yml found in ${sourceDir}`));
|
|
54
|
+
console.log("Point this at a MkDocs project directory containing mkdocs.yml");
|
|
55
|
+
process.exit(1);
|
|
56
|
+
}
|
|
57
|
+
// Parse mkdocs.yml
|
|
58
|
+
const yml = readFileSync(mkdocsYml, "utf-8");
|
|
59
|
+
const config = YAML.parse(yml);
|
|
60
|
+
const siteName = config.site_name || "Documentation";
|
|
61
|
+
const docsDir = config.docs_dir || "docs";
|
|
62
|
+
const fullDocsDir = join(sourceDir, docsDir);
|
|
63
|
+
if (!existsSync(fullDocsDir)) {
|
|
64
|
+
console.log(pc.red(`Error: docs directory not found at ${fullDocsDir}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
const outputDir = resolve(options.output || `${basename(sourceDir)}-dogsbay`);
|
|
68
|
+
// Refuse to overwrite an existing site without --force. We probe
|
|
69
|
+
// for dogsbay.config.yml specifically rather than the bare
|
|
70
|
+
// directory — many users will migrate into a directory they've
|
|
71
|
+
// already created (`mkdir foo && cd foo`).
|
|
72
|
+
const existingConfig = ["yml", "yaml", "json"]
|
|
73
|
+
.map((ext) => join(outputDir, `dogsbay.config.${ext}`))
|
|
74
|
+
.find(existsSync);
|
|
75
|
+
if (existingConfig && !options.force) {
|
|
76
|
+
console.log(pc.red(`Error: ${outputDir} already contains a Dogsbay site (${basename(existingConfig)}).`));
|
|
77
|
+
console.log("Pass --force to overwrite.");
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
console.log();
|
|
81
|
+
console.log(pc.bold(`Migrating MkDocs site to Dogsbay-MD: ${siteName}`));
|
|
82
|
+
console.log(`Source: ${sourceDir}`);
|
|
83
|
+
console.log(`Docs: ${fullDocsDir}`);
|
|
84
|
+
console.log(`Output: ${outputDir}`);
|
|
85
|
+
console.log();
|
|
86
|
+
// 1. Output skeleton. The canonical Dogsbay layout (see
|
|
87
|
+
// packages/cli/skills/platform/migration-shape/SKILL.md) puts
|
|
88
|
+
// `content/` and `dogsbay.config.yml` at the root; the
|
|
89
|
+
// generated Astro project lives under `./astro/` (created by
|
|
90
|
+
// emitSiteScaffold below).
|
|
91
|
+
const contentDir = join(outputDir, "content");
|
|
92
|
+
const assetsDir = join(contentDir, "_assets");
|
|
93
|
+
const astroDir = join(outputDir, "astro");
|
|
94
|
+
mkdirSync(contentDir, { recursive: true });
|
|
95
|
+
mkdirSync(assetsDir, { recursive: true });
|
|
96
|
+
mkdirSync(astroDir, { recursive: true });
|
|
97
|
+
// 2. Parse each MkDocs .md and serialize to Dogsbay-MD under
|
|
98
|
+
// ./content/. The MkDocs parser pipeline (admonitions, tabs,
|
|
99
|
+
// snippets, macros, autodoc, variants — see @dogsbay/format-
|
|
100
|
+
// mkdocs's CLAUDE.md) runs identically to import-mkdocs; only
|
|
101
|
+
// the final write target differs.
|
|
102
|
+
const { pageCount, lossy } = await collectAndWriteContent(sourceDir, fullDocsDir, outputDir, config);
|
|
103
|
+
console.log(pc.green(`Wrote`) + ` ${pageCount} pages to ./content/ as Dogsbay-MD`);
|
|
104
|
+
// 3. Convert MkDocs nav: → ./content/nav.yml in canonical
|
|
105
|
+
// single-key-map shape (- Label: file.md / nested children).
|
|
106
|
+
// See packages/cli/skills/platform/nav-file/SKILL.md.
|
|
107
|
+
// Do NOT emit nav.json — runtime loader prefers it and would
|
|
108
|
+
// silently override the human-edited yml. The loader auto-
|
|
109
|
+
// detects nav.yml at the content root.
|
|
110
|
+
const mkdocsNav = config.nav;
|
|
111
|
+
const navFile = convertNav(mkdocsNav);
|
|
112
|
+
writeFileSync(join(contentDir, "nav.yml"), YAML.stringify(navFile));
|
|
113
|
+
console.log(pc.green(`Extracted`) + ` navigation to content/nav.yml (${navFile.length} top-level items)`);
|
|
114
|
+
// 4. Copy non-markdown assets into ./content/_assets/<rel>. Per
|
|
115
|
+
// plans/content-assets-folder.md, assets are content-rooted
|
|
116
|
+
// (move-resistant) and referenced as /_assets/<rel>/foo.png.
|
|
117
|
+
// format-astro's copyAssets walks content/ on every site
|
|
118
|
+
// build and propagates them to astro/public/ automatically.
|
|
119
|
+
const assetCount = copyAssets(fullDocsDir, assetsDir);
|
|
120
|
+
if (assetCount > 0) {
|
|
121
|
+
console.log(pc.green(`Copied`) + ` ${assetCount} asset files to content/_assets/`);
|
|
122
|
+
}
|
|
123
|
+
// 5. Build dogsbay.config.yml. agent.{llmsTxt, mdMirror} default
|
|
124
|
+
// to true in config/defaults.ts — listing them explicitly
|
|
125
|
+
// makes the migrated config self-documenting. NO `output:`
|
|
126
|
+
// field — the default (./astro) is what we want; flat layout
|
|
127
|
+
// (`output: "."`) triggers a rebuild loop in `site dev`.
|
|
128
|
+
const siteUrl = config.site_url || undefined;
|
|
129
|
+
const repoUrl = config.repo_url || undefined;
|
|
130
|
+
const siteDescription = config.site_description || undefined;
|
|
131
|
+
const dogsbayConfig = {
|
|
132
|
+
schemaVersion: 1,
|
|
133
|
+
site: {
|
|
134
|
+
name: siteName,
|
|
135
|
+
url: siteUrl,
|
|
136
|
+
basePath: "/docs",
|
|
137
|
+
description: siteDescription,
|
|
138
|
+
repoUrl,
|
|
139
|
+
},
|
|
140
|
+
content: {
|
|
141
|
+
sources: [{ path: "./content", from: "dogsbay-md" }],
|
|
142
|
+
},
|
|
143
|
+
agent: {
|
|
144
|
+
llmsTxt: true,
|
|
145
|
+
mdMirror: true,
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
writeFileSync(join(outputDir, "dogsbay.config.yml"), serializeConfig(dogsbayConfig, "yaml"));
|
|
149
|
+
console.log(pc.green(`Wrote`) + ` dogsbay.config.yml`);
|
|
150
|
+
// 6. Scaffold the Astro project under ./astro/ (theme,
|
|
151
|
+
// package.json, astro.config.mjs, tsconfig, copied UI
|
|
152
|
+
// components). Same emitter `dogsbay site init` uses, just
|
|
153
|
+
// targeting our migrated layout.
|
|
154
|
+
emitSiteScaffold(astroDir, siteName, {
|
|
155
|
+
siteName,
|
|
156
|
+
siteUrl,
|
|
157
|
+
basePath: "/docs",
|
|
158
|
+
repoUrl,
|
|
159
|
+
llmsTxt: true,
|
|
160
|
+
mdMirror: true,
|
|
161
|
+
local: options.local,
|
|
162
|
+
}, true);
|
|
163
|
+
console.log(pc.green(`Scaffolded`) + ` Astro project at ./astro/ (theme, components, package.json)`);
|
|
164
|
+
// 7. MIGRATION.md — humans-only summary of the conversion.
|
|
165
|
+
// Captures lossy items the parser flagged so the user can
|
|
166
|
+
// audit them, plus the exact command to re-run if they hit
|
|
167
|
+
// a bug we fix later.
|
|
168
|
+
writeFileSync(join(outputDir, "MIGRATION.md"), buildMigrationNotes({
|
|
169
|
+
sourceDir,
|
|
170
|
+
outputDir,
|
|
171
|
+
siteName,
|
|
172
|
+
pageCount,
|
|
173
|
+
assetCount,
|
|
174
|
+
lossy,
|
|
175
|
+
}));
|
|
176
|
+
console.log(pc.green(`Wrote`) + ` MIGRATION.md`);
|
|
177
|
+
if (!options.quiet) {
|
|
178
|
+
console.log();
|
|
179
|
+
console.log(pc.bold(`Done! Your Dogsbay-MD site is ready.`));
|
|
180
|
+
console.log();
|
|
181
|
+
console.log("Next steps:");
|
|
182
|
+
console.log(` cd ${relative(process.cwd(), outputDir) || "."}`);
|
|
183
|
+
console.log(` npx dogsbay site dev # live preview (auto-installs)`);
|
|
184
|
+
console.log();
|
|
185
|
+
console.log("Edit ./content/*.md (canonical Dogsbay-MD) from here on.");
|
|
186
|
+
console.log("Review MIGRATION.md for what survived and what didn't.");
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async function collectAndWriteContent(sourceDir, fullDocsDir, outputDir, mkdocsConfig) {
|
|
190
|
+
// Dynamic imports for the parser + serializer — same pattern
|
|
191
|
+
// used in import-mkdocs to avoid pulling these into the CLI's
|
|
192
|
+
// type-checking graph (they're workspace deps, not direct).
|
|
193
|
+
const MarkdownIt = (await import(/* @vite-ignore */ "markdown-it")).default;
|
|
194
|
+
const { mkdocsPlugin, configureFromMkdocs } = (await import(
|
|
195
|
+
/* @vite-ignore */ "@dogsbay/format-mkdocs"));
|
|
196
|
+
const { parseMkdocsMarkdown } = (await import(
|
|
197
|
+
/* @vite-ignore */ "@dogsbay/format-mkdocs/loader"));
|
|
198
|
+
const { treeToDogsbayMd } = (await import(
|
|
199
|
+
/* @vite-ignore */ "@dogsbay/format-dogsbay-md"));
|
|
200
|
+
// Match the MkDocs parser config to the source's mkdocs.yml so
|
|
201
|
+
// detection-driven options (snippets root, file-include, macros,
|
|
202
|
+
// variants) line up with what the project authored against.
|
|
203
|
+
const md = new MarkdownIt({ html: true, linkify: true, typographer: true });
|
|
204
|
+
// configureFromMkdocs tolerates both the list form and the
|
|
205
|
+
// mapping form of `markdown_extensions:` (FastAPI uses the
|
|
206
|
+
// mapping form — without that tolerance PyMdownX Blocks like
|
|
207
|
+
// `/// note` fall through to raw paragraphs in the output).
|
|
208
|
+
const detected = configureFromMkdocs(mkdocsConfig.markdown_extensions);
|
|
209
|
+
md.use(mkdocsPlugin, {
|
|
210
|
+
...detected,
|
|
211
|
+
snippets: {
|
|
212
|
+
root: join(sourceDir, "includes"),
|
|
213
|
+
autoAppend: ["mkdocs.md"],
|
|
214
|
+
},
|
|
215
|
+
footnotes: true,
|
|
216
|
+
math: true,
|
|
217
|
+
// Convert `.md` links to root-relative slugs at parse time.
|
|
218
|
+
// baseUrl: "" gives `/foo` / `/tutorial/quickstart` form (no
|
|
219
|
+
// basePath prefix — site build adds /docs at render time).
|
|
220
|
+
// See packages/cli/skills/platform/migration-shape/SKILL.md
|
|
221
|
+
// → "Internal links — root-relative absolute slugs".
|
|
222
|
+
linkRewrite: { baseUrl: "" },
|
|
223
|
+
});
|
|
224
|
+
// Auto-detect mkdocstrings sourceRoot. Phase 3 default = inline,
|
|
225
|
+
// so the autodoc pipeline runs at migration time. The resolved
|
|
226
|
+
// api-* TreeNodes serialize through Dogsbay-MD's `renderUnknown`
|
|
227
|
+
// (HTML fallback) — known limitation; first-class api-*
|
|
228
|
+
// serialization lands with Phase 4.
|
|
229
|
+
const autodocSourceRoot = detectAutodocSourceRoot(sourceDir, mkdocsConfig);
|
|
230
|
+
const parseOpts = {
|
|
231
|
+
collapse: false,
|
|
232
|
+
youtubeEmbed: true,
|
|
233
|
+
diagrams: true,
|
|
234
|
+
};
|
|
235
|
+
if (autodocSourceRoot) {
|
|
236
|
+
parseOpts.autodoc = { sourceRoot: autodocSourceRoot };
|
|
237
|
+
}
|
|
238
|
+
const mdFiles = findMarkdownFiles(fullDocsDir);
|
|
239
|
+
const lossy = [];
|
|
240
|
+
let pageCount = 0;
|
|
241
|
+
for (const mdPath of mdFiles) {
|
|
242
|
+
const relPath = relative(fullDocsDir, mdPath);
|
|
243
|
+
const destPath = join(outputDir, "content", relPath);
|
|
244
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
245
|
+
try {
|
|
246
|
+
const source = readFileSync(mdPath, "utf-8");
|
|
247
|
+
const { tree } = await parseMkdocsMarkdown(source, md, {
|
|
248
|
+
...parseOpts,
|
|
249
|
+
env: { filePath: relPath },
|
|
250
|
+
});
|
|
251
|
+
// Rewrite image references to /_assets/<rel>/... canonical
|
|
252
|
+
// form. format-mkdocs's linkRewrite (configured above) already
|
|
253
|
+
// resolved relative paths to root-relative `/img/...` form;
|
|
254
|
+
// this walker just adds the `_assets/` prefix so the refs line
|
|
255
|
+
// up with where copyAssets deposits files. See
|
|
256
|
+
// packages/cli/skills/platform/migration-shape/SKILL.md
|
|
257
|
+
// → "Asset folder".
|
|
258
|
+
rewriteImageRefsToAssets(tree);
|
|
259
|
+
// No frontmatter title lift in Phase 3: format-mkdocs's
|
|
260
|
+
// TreeNode shape (heading.html / heading.inline) doesn't
|
|
261
|
+
// round-trip through format-dogsbay-md's heading serializer
|
|
262
|
+
// cleanly when the H1 is duplicated in frontmatter. Site
|
|
263
|
+
// build derives the title from the first H1 in the body
|
|
264
|
+
// anyway — no behavioural difference.
|
|
265
|
+
const body = treeToDogsbayMd(tree);
|
|
266
|
+
writeFileSync(destPath, body.endsWith("\n") ? body : body + "\n");
|
|
267
|
+
// Flag the autodoc HTML-fallback case so MIGRATION.md can
|
|
268
|
+
// call it out per-file. Detect by scanning the source for
|
|
269
|
+
// mkdocstrings directives — if any were resolved at parse
|
|
270
|
+
// time, the page contains snapshotted api-* HTML.
|
|
271
|
+
if (/^:::\s+\S/m.test(source)) {
|
|
272
|
+
lossy.push({
|
|
273
|
+
file: relPath,
|
|
274
|
+
reason: "Contains mkdocstrings ::: directives. Resolved to a snapshot at " +
|
|
275
|
+
"migration time; renders as raw HTML until Phase 4 adds a " +
|
|
276
|
+
"first-class :::autodoc directive to Dogsbay-MD.",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
pageCount++;
|
|
280
|
+
}
|
|
281
|
+
catch (err) {
|
|
282
|
+
console.log(pc.yellow(` Warning: failed to migrate ${relPath}: ${err.message}`));
|
|
283
|
+
lossy.push({
|
|
284
|
+
file: relPath,
|
|
285
|
+
reason: `Parse error during migration: ${err.message}`,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
return { pageCount, lossy };
|
|
290
|
+
}
|
|
291
|
+
function detectAutodocSourceRoot(sourceDir, mkdocsConfig) {
|
|
292
|
+
// mkdocs.yml -> plugins -> mkdocstrings -> handlers -> python ->
|
|
293
|
+
// paths is the canonical declaration site. We accept either a
|
|
294
|
+
// string or array and resolve relative to the mkdocs project.
|
|
295
|
+
const plugins = mkdocsConfig.plugins;
|
|
296
|
+
if (!Array.isArray(plugins))
|
|
297
|
+
return null;
|
|
298
|
+
for (const entry of plugins) {
|
|
299
|
+
if (entry && typeof entry === "object" && "mkdocstrings" in entry) {
|
|
300
|
+
const handlers = entry.mkdocstrings?.handlers;
|
|
301
|
+
const paths = handlers?.python?.paths;
|
|
302
|
+
if (typeof paths === "string")
|
|
303
|
+
return resolve(sourceDir, paths);
|
|
304
|
+
if (Array.isArray(paths) && typeof paths[0] === "string") {
|
|
305
|
+
return resolve(sourceDir, paths[0]);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Fallback: many mkdocstrings users keep Python source at the
|
|
310
|
+
// project root next to mkdocs.yml.
|
|
311
|
+
return sourceDir;
|
|
312
|
+
}
|
|
313
|
+
function findMarkdownFiles(dir) {
|
|
314
|
+
const results = [];
|
|
315
|
+
function walk(d) {
|
|
316
|
+
for (const entry of readdirSync(d)) {
|
|
317
|
+
const full = join(d, entry);
|
|
318
|
+
if (statSync(full).isDirectory()) {
|
|
319
|
+
walk(full);
|
|
320
|
+
}
|
|
321
|
+
else if (entry.endsWith(".md")) {
|
|
322
|
+
results.push(full);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
walk(dir);
|
|
327
|
+
return results;
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Walk a TreeNode tree and prefix every image src with `_assets/`
|
|
331
|
+
* so refs match where copyAssets deposits files. format-mkdocs's
|
|
332
|
+
* linkRewrite has already resolved relative paths to root-relative
|
|
333
|
+
* form (`/img/foo.png`); this just adds the `_assets` segment.
|
|
334
|
+
*
|
|
335
|
+
* Touches:
|
|
336
|
+
* - inline `image` nodes' `src` field
|
|
337
|
+
* - inline `link` nodes recursively (anchor tags wrap images)
|
|
338
|
+
* - raw `html` strings on text nodes (`<img src="...">`)
|
|
339
|
+
* - `props.src` on block nodes (figures, etc.)
|
|
340
|
+
*/
|
|
341
|
+
export function rewriteImageRefsToAssets(nodes) {
|
|
342
|
+
for (const node of nodes) {
|
|
343
|
+
if (node.inline)
|
|
344
|
+
rewriteImagesInInline(node.inline);
|
|
345
|
+
if (node.html)
|
|
346
|
+
node.html = rewriteImagesInHtml(node.html);
|
|
347
|
+
if (typeof node.props?.src === "string") {
|
|
348
|
+
node.props.src = rewriteAssetHref(node.props.src);
|
|
349
|
+
}
|
|
350
|
+
if (node.children)
|
|
351
|
+
rewriteImageRefsToAssets(node.children);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
function rewriteImagesInInline(nodes) {
|
|
355
|
+
for (const node of nodes) {
|
|
356
|
+
if (node.type === "image" && typeof node.src === "string") {
|
|
357
|
+
node.src = rewriteAssetHref(node.src);
|
|
358
|
+
}
|
|
359
|
+
if (node.children)
|
|
360
|
+
rewriteImagesInInline(node.children);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
function rewriteImagesInHtml(html) {
|
|
364
|
+
return html.replace(/<img\b([^>]*?)\bsrc="([^"]+)"([^>]*)>/g, (_, pre, src, post) => `<img${pre} src="${rewriteAssetHref(src)}"${post}>`);
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Decide what to do with an image src:
|
|
368
|
+
* - external (http://, https://, data:, //) → leave alone
|
|
369
|
+
* - anchor (#foo) → leave alone (rare for images but safe)
|
|
370
|
+
* - already canonical (/_assets/...) → leave alone
|
|
371
|
+
* - root-relative (/img/foo.png) → prefix `/_assets`
|
|
372
|
+
* - relative (img/foo.png) → treat as root-relative-to-docs-root,
|
|
373
|
+
* prefix `/_assets/`. format-mkdocs's linkRewrite normally
|
|
374
|
+
* converts these to root-relative before we run, but stay
|
|
375
|
+
* defensive in case the parser missed one.
|
|
376
|
+
*/
|
|
377
|
+
export function rewriteAssetHref(src) {
|
|
378
|
+
if (!src)
|
|
379
|
+
return src;
|
|
380
|
+
// External / protocol-relative / data URIs.
|
|
381
|
+
if (/^[a-z][a-z0-9+.-]*:/i.test(src) || src.startsWith("//"))
|
|
382
|
+
return src;
|
|
383
|
+
// Anchor-only.
|
|
384
|
+
if (src.startsWith("#"))
|
|
385
|
+
return src;
|
|
386
|
+
// Already canonical.
|
|
387
|
+
if (src === "/_assets" || src.startsWith("/_assets/"))
|
|
388
|
+
return src;
|
|
389
|
+
// Normalize and prefix.
|
|
390
|
+
const trimmed = src.replace(/^\/+/, "").replace(/^\.\/+/, "");
|
|
391
|
+
return `/_assets/${posix.normalize(trimmed)}`;
|
|
392
|
+
}
|
|
393
|
+
function copyAssets(srcDocsDir, destPublicDir) {
|
|
394
|
+
const exts = new Set([
|
|
395
|
+
".png",
|
|
396
|
+
".jpg",
|
|
397
|
+
".jpeg",
|
|
398
|
+
".gif",
|
|
399
|
+
".svg",
|
|
400
|
+
".webp",
|
|
401
|
+
".ico",
|
|
402
|
+
".pdf",
|
|
403
|
+
]);
|
|
404
|
+
let count = 0;
|
|
405
|
+
function walk(d) {
|
|
406
|
+
for (const entry of readdirSync(d)) {
|
|
407
|
+
const full = join(d, entry);
|
|
408
|
+
if (statSync(full).isDirectory()) {
|
|
409
|
+
walk(full);
|
|
410
|
+
}
|
|
411
|
+
else {
|
|
412
|
+
const ext = entry.substring(entry.lastIndexOf(".")).toLowerCase();
|
|
413
|
+
if (!exts.has(ext))
|
|
414
|
+
continue;
|
|
415
|
+
const rel = relative(srcDocsDir, full);
|
|
416
|
+
const dest = join(destPublicDir, rel);
|
|
417
|
+
mkdirSync(dirname(dest), { recursive: true });
|
|
418
|
+
cpSync(full, dest);
|
|
419
|
+
count++;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
walk(srcDocsDir);
|
|
424
|
+
return count;
|
|
425
|
+
}
|
|
426
|
+
// ── MkDocs nav → single-key-map nav.yml ─────────────────────────
|
|
427
|
+
/**
|
|
428
|
+
* Convert MkDocs `nav:` to the canonical Dogsbay nav file shape —
|
|
429
|
+
* single-key map per entry:
|
|
430
|
+
*
|
|
431
|
+
* - Home: index.md
|
|
432
|
+
* - Guides:
|
|
433
|
+
* - Configuration: guides/configuration.md
|
|
434
|
+
*
|
|
435
|
+
* NOT the `{ label: "...", file: "..." }` shape — the runtime
|
|
436
|
+
* validator throws on multi-key entries. See
|
|
437
|
+
* packages/cli/skills/platform/nav-file/SKILL.md.
|
|
438
|
+
*/
|
|
439
|
+
function convertNav(nav) {
|
|
440
|
+
if (!Array.isArray(nav))
|
|
441
|
+
return [];
|
|
442
|
+
const out = [];
|
|
443
|
+
for (const entry of nav) {
|
|
444
|
+
const converted = navEntryToSingleKey(entry);
|
|
445
|
+
if (converted)
|
|
446
|
+
out.push(converted);
|
|
447
|
+
}
|
|
448
|
+
return out;
|
|
449
|
+
}
|
|
450
|
+
function navEntryToSingleKey(entry) {
|
|
451
|
+
// Plain string: "path.md" — auto-label from filename.
|
|
452
|
+
if (typeof entry === "string") {
|
|
453
|
+
return { [labelFromPath(entry)]: entry };
|
|
454
|
+
}
|
|
455
|
+
// Object form: { Label: "path.md" } or { Label: [...] }
|
|
456
|
+
const keys = Object.keys(entry);
|
|
457
|
+
if (keys.length === 0)
|
|
458
|
+
return null;
|
|
459
|
+
const label = keys[0];
|
|
460
|
+
const value = entry[label];
|
|
461
|
+
if (typeof value === "string") {
|
|
462
|
+
// Internal path or external URL — single-key map stores both
|
|
463
|
+
// the same way; the loader distinguishes them at read time
|
|
464
|
+
// (anything starting with a protocol or `/` is treated as
|
|
465
|
+
// external — see nav-file SKILL).
|
|
466
|
+
return { [label]: value };
|
|
467
|
+
}
|
|
468
|
+
if (Array.isArray(value)) {
|
|
469
|
+
const children = [];
|
|
470
|
+
for (const child of value) {
|
|
471
|
+
const c = navEntryToSingleKey(child);
|
|
472
|
+
if (c)
|
|
473
|
+
children.push(c);
|
|
474
|
+
}
|
|
475
|
+
return { [label]: children };
|
|
476
|
+
}
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
function labelFromPath(path) {
|
|
480
|
+
const base = basename(path, ".md");
|
|
481
|
+
return base
|
|
482
|
+
.replace(/[-_]+/g, " ")
|
|
483
|
+
.replace(/\b\w/g, (c) => c.toUpperCase());
|
|
484
|
+
}
|
|
485
|
+
// ── MIGRATION.md ────────────────────────────────────────
|
|
486
|
+
function buildMigrationNotes(args) {
|
|
487
|
+
const { sourceDir, outputDir, siteName, pageCount, assetCount, lossy } = args;
|
|
488
|
+
const lines = [
|
|
489
|
+
`# Migration: ${siteName}`,
|
|
490
|
+
"",
|
|
491
|
+
`Generated by \`dogsbay migrate-mkdocs\`.`,
|
|
492
|
+
"",
|
|
493
|
+
`- **Source**: \`${sourceDir}\``,
|
|
494
|
+
`- **Output**: \`${outputDir}\``,
|
|
495
|
+
`- **Pages migrated**: ${pageCount}`,
|
|
496
|
+
`- **Assets carried over**: ${assetCount}`,
|
|
497
|
+
"",
|
|
498
|
+
"## What changed",
|
|
499
|
+
"",
|
|
500
|
+
"Your source-of-truth is now `./content/*.md` (canonical Dogsbay-MD).",
|
|
501
|
+
"The MkDocs source at the path above is no longer consulted by the",
|
|
502
|
+
"build pipeline — you can archive or delete it once you're satisfied",
|
|
503
|
+
"with the migration.",
|
|
504
|
+
"",
|
|
505
|
+
"Edit content with any markdown editor. Rebuild with:",
|
|
506
|
+
"",
|
|
507
|
+
"```bash",
|
|
508
|
+
"npm install",
|
|
509
|
+
"npx dogsbay site build # one-shot",
|
|
510
|
+
"npx dogsbay site dev # watch + preview",
|
|
511
|
+
"```",
|
|
512
|
+
"",
|
|
513
|
+
"## Re-running the migration",
|
|
514
|
+
"",
|
|
515
|
+
"If you find a regression and we ship a fix, re-run with `--force`:",
|
|
516
|
+
"",
|
|
517
|
+
"```bash",
|
|
518
|
+
`npx dogsbay migrate-mkdocs ${sourceDir} --output ${outputDir} --force`,
|
|
519
|
+
"```",
|
|
520
|
+
"",
|
|
521
|
+
"Note that `--force` overwrites all generated files; any hand edits",
|
|
522
|
+
"to `./content/*.md` since the last migration will be lost. Commit",
|
|
523
|
+
"your work first.",
|
|
524
|
+
"",
|
|
525
|
+
];
|
|
526
|
+
if (lossy.length > 0) {
|
|
527
|
+
lines.push("## Known limitations", "");
|
|
528
|
+
lines.push("The following files have content that didn't fully round-trip", "into Dogsbay-MD. Review each one and fix manually:", "");
|
|
529
|
+
for (const item of lossy) {
|
|
530
|
+
lines.push(`- **\`${item.file}\`** — ${item.reason}`);
|
|
531
|
+
}
|
|
532
|
+
lines.push("");
|
|
533
|
+
}
|
|
534
|
+
lines.push("## Architecture", "", "Migration is one-way and runs the same parser pipeline as", "`dogsbay import-mkdocs` (Material plugins, mkdocstrings, snippets,", "macros, variants). The difference is the emission target: import", "writes `.astro` pages directly; migrate writes Dogsbay-MD source", "files. From there, `dogsbay site build` is the standard SSG", "pipeline with full agent-readiness output (llms.txt, .md mirrors,", "sitemap, Cloudflare _headers).", "", "See `plans/mkdocs-import-architecture.md` for the full design.", "");
|
|
535
|
+
return lines.join("\n");
|
|
536
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import { Command } from "commander";
|
|
|
6
6
|
import { init } from "./commands/init.js";
|
|
7
7
|
import { add, list } from "./commands/add.js";
|
|
8
8
|
import { importMkdocs } from "./commands/import-mkdocs.js";
|
|
9
|
+
import { migrateMkdocs } from "./commands/migrate-mkdocs.js";
|
|
9
10
|
import { preprocessVariants } from "./commands/preprocess-variants.js";
|
|
10
11
|
import { lighthouse } from "./commands/lighthouse.js";
|
|
11
12
|
import { convert } from "./commands/convert.js";
|
|
@@ -176,6 +177,15 @@ program
|
|
|
176
177
|
.option("--dynamic", "Use dynamic catch-all route instead of static .astro pages")
|
|
177
178
|
.option("--keep-dynamic", "Keep the dynamic [..slug] route alongside static pages (for comparison)")
|
|
178
179
|
.action((source, options) => importMkdocs(source, options));
|
|
180
|
+
program
|
|
181
|
+
.command("migrate-mkdocs")
|
|
182
|
+
.description("One-way migrate a MkDocs site to a scaffolded Dogsbay-MD project. " +
|
|
183
|
+
"Source-of-truth becomes ./content/*.md; the MkDocs source can be deleted.")
|
|
184
|
+
.argument("<source>", "Path to MkDocs project (containing mkdocs.yml)")
|
|
185
|
+
.option("-o, --output <dir>", "Output directory (default: {source}-dogsbay)")
|
|
186
|
+
.option("--force", "Overwrite an existing Dogsbay site at the output dir")
|
|
187
|
+
.option("--local", "Use file: references to local monorepo packages (for development)")
|
|
188
|
+
.action((source, options) => migrateMkdocs(source, options));
|
|
179
189
|
program
|
|
180
190
|
.command("preprocess-variants")
|
|
181
191
|
.description("Add variant tabs to file includes (e.g. Python 3.10+ / 3.9+ tabs)")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dogsbay",
|
|
3
|
-
"version": "0.2.0-beta.
|
|
3
|
+
"version": "0.2.0-beta.38",
|
|
4
4
|
"description": "CLI for Dogsbay — scaffold, build, and serve documentation sites with markdown / MkDocs / Obsidian / OpenAPI sources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -32,14 +32,14 @@
|
|
|
32
32
|
"picocolors": "^1.1.0",
|
|
33
33
|
"prompts": "^2.4.2",
|
|
34
34
|
"yaml": "^2.8.3",
|
|
35
|
-
"@dogsbay/format-mkdocs": "0.2.0-beta.
|
|
36
|
-
"@dogsbay/format-
|
|
37
|
-
"@dogsbay/format-mdx": "0.2.0-beta.
|
|
38
|
-
"@dogsbay/format-
|
|
39
|
-
"@dogsbay/format-
|
|
40
|
-
"@dogsbay/format-dogsbay-md": "0.2.0-beta.
|
|
41
|
-
"@dogsbay/format-openapi": "0.2.0-beta.
|
|
42
|
-
"@dogsbay/types": "0.2.0-beta.
|
|
35
|
+
"@dogsbay/format-mkdocs": "0.2.0-beta.38",
|
|
36
|
+
"@dogsbay/format-obsidian": "0.2.0-beta.38",
|
|
37
|
+
"@dogsbay/format-mdx": "0.2.0-beta.38",
|
|
38
|
+
"@dogsbay/format-astro": "0.2.0-beta.38",
|
|
39
|
+
"@dogsbay/format-starlight": "0.2.0-beta.38",
|
|
40
|
+
"@dogsbay/format-dogsbay-md": "0.2.0-beta.38",
|
|
41
|
+
"@dogsbay/format-openapi": "0.2.0-beta.38",
|
|
42
|
+
"@dogsbay/types": "0.2.0-beta.38"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/node": "^22.0.0",
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: dogsbay:migration-shape
|
|
3
|
+
description: Canonical output shape for any source-format → Dogsbay one-way migration command (migrate-mkdocs, future migrate-obsidian, migrate-jekyll, migrate-hugo, migrate-notion, etc.). Use when implementing a migration target, reviewing migration output, or diagnosing build issues in migrated sites. Captures: project layout, asset folder convention, link normalization, nav file shape, and frontmatter conventions.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Canonical migration output shape
|
|
7
|
+
|
|
8
|
+
When a migration command produces a Dogsbay site from another source
|
|
9
|
+
format, the output **must** match the shape `dogsbay site init`
|
|
10
|
+
produces. The platform's emitters, watchers, audit rules, plugin
|
|
11
|
+
runtime, and component renderers all assume that shape. Drift causes
|
|
12
|
+
subtle bugs (rebuild loops, broken images on subpath deploys, link
|
|
13
|
+
404s after page moves, malformed nav warnings).
|
|
14
|
+
|
|
15
|
+
This skill is the checklist. New migration targets read it once and
|
|
16
|
+
copy the shape; existing migrations read it before changing emit
|
|
17
|
+
logic.
|
|
18
|
+
|
|
19
|
+
## 1. Project layout (`output: ./astro`)
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
<output>/
|
|
23
|
+
├── dogsbay.config.yml ← top-level config; references ./content
|
|
24
|
+
├── content/ ← source-of-truth Dogsbay-MD
|
|
25
|
+
│ ├── index.md
|
|
26
|
+
│ ├── nav.yml ← single-key-map nav file
|
|
27
|
+
│ ├── _assets/ ← all assets, content-rooted
|
|
28
|
+
│ │ ├── diagrams/
|
|
29
|
+
│ │ └── screenshots/
|
|
30
|
+
│ ├── guides/
|
|
31
|
+
│ │ └── install.md
|
|
32
|
+
│ └── ...
|
|
33
|
+
├── astro/ ← generated Astro project (Dogsbay owns)
|
|
34
|
+
│ ├── package.json
|
|
35
|
+
│ ├── astro.config.mjs
|
|
36
|
+
│ ├── tsconfig.json
|
|
37
|
+
│ ├── src/...
|
|
38
|
+
│ ├── public/...
|
|
39
|
+
│ └── dist/ ← build output
|
|
40
|
+
└── MIGRATION.md ← (migrations only) lossy items + re-run cmd
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
**Required:**
|
|
44
|
+
- `dogsbay.config.yml` at root, references `sources: [{ path: ./content, from: dogsbay-md }]`.
|
|
45
|
+
- `content/` at root.
|
|
46
|
+
- `astro/` for the generated project — DO NOT use `output: "."`.
|
|
47
|
+
- Top-level files except the config + MIGRATION.md belong inside `astro/`.
|
|
48
|
+
|
|
49
|
+
**Why `output: "./astro"` is non-negotiable:**
|
|
50
|
+
|
|
51
|
+
When `output: "."`, siteRoot and outputDir are the same path. The dev
|
|
52
|
+
watcher recursively watches siteRoot; every `site build` writes
|
|
53
|
+
generated files (src/pages, src/data/*.json, src/middleware.ts,
|
|
54
|
+
public/llms.txt, public/<basePath>/sitemap-*.xml,
|
|
55
|
+
astro.config.{dogsbay,plugins}.mjs) back into siteRoot; the watcher
|
|
56
|
+
sees them, schedules the next build → infinite loop. The platform's
|
|
57
|
+
chokidar `ignored` patterns work cleanly only when `astro/` is a
|
|
58
|
+
sibling directory.
|
|
59
|
+
|
|
60
|
+
This was learned the hard way during Phase 3 of
|
|
61
|
+
`plans/mkdocs-import-architecture.md` — migrate-mkdocs initially
|
|
62
|
+
emitted `output: "."` and `dogsbay site dev` rebuild-looped on the
|
|
63
|
+
result. The fix was to revert to the standard `output: "./astro"`.
|
|
64
|
+
|
|
65
|
+
## 2. Asset folder (`content/_assets/`)
|
|
66
|
+
|
|
67
|
+
All images, diagrams, screenshots, icons, PDFs, and downloadable
|
|
68
|
+
assets the docs reference go under `content/_assets/`. Authors
|
|
69
|
+
reference them with a leading slash:
|
|
70
|
+
|
|
71
|
+
```markdown
|
|
72
|
+

|
|
73
|
+
|
|
74
|
+
The product onboarding wizard:
|
|
75
|
+
|
|
76
|
+

|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Required:**
|
|
80
|
+
- Migrations copy source assets under `<output>/content/_assets/<rel>/`.
|
|
81
|
+
- Rewrite every image reference to `/_assets/<rel>/...` form during
|
|
82
|
+
the TreeNode walk. Both `<image>` inline nodes AND `<img src="...">`
|
|
83
|
+
in raw HTML.
|
|
84
|
+
- Preserve external URLs (`https://...`, `//`, `data:`) unchanged.
|
|
85
|
+
- Preserve anchors (`#foo`) unchanged.
|
|
86
|
+
|
|
87
|
+
**Why `_assets/` with leading slash:**
|
|
88
|
+
|
|
89
|
+
Per `plans/content-assets-folder.md`: "The platform already chose
|
|
90
|
+
**identity over location** for xrefs. Image references should follow
|
|
91
|
+
the same logic: a stable identifier (path within the asset tree)
|
|
92
|
+
decoupled from where the referencing markdown lives."
|
|
93
|
+
|
|
94
|
+
- Move-resistant: a page can be reordered in nav or moved within
|
|
95
|
+
`content/` without breaking its image references.
|
|
96
|
+
- Underscore prefix is the unambiguous "reserved, not a content slug"
|
|
97
|
+
convention (Sphinx, Jekyll, MkDocs all use underscore-prefixed
|
|
98
|
+
reserved dirs).
|
|
99
|
+
- `format-astro`'s build-time `copyAssets` walks `content/` and copies
|
|
100
|
+
recognized extensions to `astro/public/<rel>/` automatically. The
|
|
101
|
+
migration command only has to deposit assets in `_assets/`; site
|
|
102
|
+
build wires the rest.
|
|
103
|
+
|
|
104
|
+
**Common source-format mappings:**
|
|
105
|
+
|
|
106
|
+
| Source | Source convention | Migration output |
|
|
107
|
+
|---|---|---|
|
|
108
|
+
| MkDocs | `docs/img/foo.png` | `content/_assets/img/foo.png` |
|
|
109
|
+
| Obsidian | `Attachments/foo.png` | `content/_assets/attachments/foo.png` |
|
|
110
|
+
| Jekyll | `assets/img/foo.png` | `content/_assets/img/foo.png` |
|
|
111
|
+
| Hugo | `static/foo.png` | `content/_assets/foo.png` |
|
|
112
|
+
| Notion | `<page-id>/foo.png` | `content/_assets/<page-slug>/foo.png` |
|
|
113
|
+
|
|
114
|
+
## 3. Internal links — root-relative absolute slugs
|
|
115
|
+
|
|
116
|
+
In `content/*.md`, internal links are **root-relative absolute slugs**
|
|
117
|
+
(no extension, no basePath):
|
|
118
|
+
|
|
119
|
+
```markdown
|
|
120
|
+
See [the install guide](/guides/install) for details.
|
|
121
|
+
|
|
122
|
+
Cross-reference into [the API reference](/api/endpoints).
|
|
123
|
+
|
|
124
|
+
Same-page anchor: [skip to setup](#setup).
|
|
125
|
+
|
|
126
|
+
External: [GitHub](https://github.com/example/repo).
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
**Required rewrites at migration time:**
|
|
130
|
+
|
|
131
|
+
| Source form | Migration output |
|
|
132
|
+
|---|---|
|
|
133
|
+
| `[foo](./bar.md)` | `[foo](/bar)` |
|
|
134
|
+
| `[foo](../tutorial/quickstart.md)` | `[foo](/tutorial/quickstart)` |
|
|
135
|
+
| `[foo](section/page.md)` | `[foo](/section/page)` (resolved against page's dir) |
|
|
136
|
+
| `[foo](#anchor)` | unchanged |
|
|
137
|
+
| `[foo](https://...)` | unchanged |
|
|
138
|
+
| `[foo](mailto:...)` | unchanged |
|
|
139
|
+
|
|
140
|
+
**Why root-relative absolute:**
|
|
141
|
+
|
|
142
|
+
- `format-astro.rewriteTreeHrefs` adds `combined` (urlBase + basePath)
|
|
143
|
+
at build time. Source links must NOT include the basePath — that
|
|
144
|
+
layer is the platform's job.
|
|
145
|
+
- Top-down absolute is move-resistant: rename a page, only the file
|
|
146
|
+
rename + nav entry need updating, not every other markdown file
|
|
147
|
+
that references it.
|
|
148
|
+
- format-mkdocs has a `linkRewritePlugin` (`rules/link-rewrite.ts`)
|
|
149
|
+
that does this conversion at parse time. Call it with
|
|
150
|
+
`linkRewrite: { baseUrl: "" }` from migration commands — `""` gives
|
|
151
|
+
`/foo` form, `/docs` gives `/docs/foo` (wrong for migrate; site
|
|
152
|
+
build adds /docs itself).
|
|
153
|
+
|
|
154
|
+
## 4. Nav file — single-key map shape
|
|
155
|
+
|
|
156
|
+
`content/nav.yml` is the canonical nav source. **Single-key map** per
|
|
157
|
+
entry, NOT `{label, file, children}` objects.
|
|
158
|
+
|
|
159
|
+
```yaml
|
|
160
|
+
# ✅ CORRECT
|
|
161
|
+
- Home: index.md
|
|
162
|
+
- Getting started: getting-started.md
|
|
163
|
+
- Guides:
|
|
164
|
+
- Configuration: guides/configuration.md
|
|
165
|
+
- Deployment: guides/deployment.md
|
|
166
|
+
- Source: https://github.com/example/repo
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
```yaml
|
|
170
|
+
# ❌ WRONG — emits malformed warning, falls back to directory scan
|
|
171
|
+
- label: Home
|
|
172
|
+
file: index.md
|
|
173
|
+
- label: Guides
|
|
174
|
+
children:
|
|
175
|
+
- label: Configuration
|
|
176
|
+
file: guides/configuration.md
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The validator throws `Nav entry must have exactly one key at [N]`.
|
|
180
|
+
|
|
181
|
+
**Migration logic:**
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
source nav entry → single-key-map entry
|
|
185
|
+
─────────────────────────────────────────────────
|
|
186
|
+
"path.md" → "<auto-label-from-filename>: path.md"
|
|
187
|
+
{ Label: "path.md" } → "Label: path.md"
|
|
188
|
+
{ Label: [ ... ] } → "Label:\n - <recursive>"
|
|
189
|
+
{ Label: "https://..." } → "Label: https://..."
|
|
190
|
+
{ Label: "/abs-path" } → "Label: https://..." (external, with leading slash preserved)
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Do NOT emit `nav.json` alongside `nav.yml`.** The runtime loader
|
|
194
|
+
prefers `nav.json` if present (`nav.json` > `nav.yml` > `nav.yaml`).
|
|
195
|
+
Emitting both means user edits to `nav.yml` are silently ignored.
|
|
196
|
+
Migrations emit `nav.yml` ONLY.
|
|
197
|
+
|
|
198
|
+
(Phase 1 of `plans/mkdocs-import-architecture.md` — the
|
|
199
|
+
`import-mkdocs` path — does emit both because page templates `import
|
|
200
|
+
nav from "@/data/nav.json"` directly. That's a separate caller pattern.
|
|
201
|
+
Migrations don't go through that template path; they hand off to
|
|
202
|
+
`dogsbay site build` which reads nav.yml.)
|
|
203
|
+
|
|
204
|
+
## 5. Frontmatter — DO NOT lift H1 to title
|
|
205
|
+
|
|
206
|
+
Source pages often have an H1 as the first content line. **Do not
|
|
207
|
+
lift it to a `title:` frontmatter field during migration.** Reasons:
|
|
208
|
+
|
|
209
|
+
1. format-mkdocs's heading TreeNode carries the title in `props.text`
|
|
210
|
+
(cross-shape divergence from canonical Dogsbay-MD's `heading.inline`
|
|
211
|
+
form). format-dogsbay-md's serializer reads `inline → html →
|
|
212
|
+
props.text` in that order. If you lift the title to frontmatter
|
|
213
|
+
AND keep the H1 in the body, the body's H1 may serialize as
|
|
214
|
+
`# {text="Title"}` — empty heading with text as an attribute.
|
|
215
|
+
2. Site build derives the page title from the first H1 anyway. There's
|
|
216
|
+
no behavioural benefit to duplicating it in frontmatter.
|
|
217
|
+
3. Duplicating creates a maintenance burden: the user edits the H1,
|
|
218
|
+
forgets to update frontmatter, browser tab + sidebar disagree.
|
|
219
|
+
|
|
220
|
+
Let the H1 stay in the body. Site build does the right thing.
|
|
221
|
+
|
|
222
|
+
## 6. MIGRATION.md — audit trail
|
|
223
|
+
|
|
224
|
+
Every migration writes a top-level `MIGRATION.md` documenting:
|
|
225
|
+
|
|
226
|
+
- The source path (so re-runs reproduce).
|
|
227
|
+
- The exact re-run command with `--force`.
|
|
228
|
+
- A list of files with lossy content (e.g. `mkdocstrings` snapshots,
|
|
229
|
+
source-format plugins not yet supported, Jinja templates dropped).
|
|
230
|
+
- Architectural notes (where you can go for more info — link to the
|
|
231
|
+
plan file).
|
|
232
|
+
|
|
233
|
+
This is the audit trail for the user to manually patch what didn't
|
|
234
|
+
round-trip. It's not optional.
|
|
235
|
+
|
|
236
|
+
## 7. Testing checklist
|
|
237
|
+
|
|
238
|
+
Every migration command MUST have a fixture test asserting all of:
|
|
239
|
+
|
|
240
|
+
| Assertion | Rationale |
|
|
241
|
+
|---|---|
|
|
242
|
+
| `<output>/content/*.md` exists for every source page | Content emission |
|
|
243
|
+
| `<output>/content/_assets/<rel>/` carries every source asset | Asset collection |
|
|
244
|
+
| `<output>/content/<page>.md` references images as `/_assets/...` | Asset rewrite |
|
|
245
|
+
| Internal `.md` links rewritten to root-relative slugs | Link normalization |
|
|
246
|
+
| `<output>/content/nav.yml` uses single-key-map shape | Nav contract |
|
|
247
|
+
| `<output>/content/nav.json` does NOT exist | Avoid loader precedence trap |
|
|
248
|
+
| `<output>/dogsbay.config.yml` has `sources: [{ path: ./content, from: dogsbay-md }]` | Config wires content to source |
|
|
249
|
+
| `<output>/dogsbay.config.yml` does NOT set `output:` | Use default `./astro` |
|
|
250
|
+
| `<output>/astro/package.json` exists | Scaffold under astro/ |
|
|
251
|
+
| `<output>/astro/astro.config.mjs` exists | Scaffold under astro/ |
|
|
252
|
+
| `<output>/astro/src/styles/theme.css` exists | Scaffold under astro/ |
|
|
253
|
+
| `<output>/MIGRATION.md` exists | Audit trail |
|
|
254
|
+
|
|
255
|
+
End-to-end smoke (manual, against real corpus):
|
|
256
|
+
|
|
257
|
+
```bash
|
|
258
|
+
dogsbay migrate-<source> <source-path> --output ./migrated --local
|
|
259
|
+
cd ./migrated
|
|
260
|
+
npm install # in ./astro? or root? See output below
|
|
261
|
+
npx dogsbay site build # produces astro/dist/
|
|
262
|
+
npx dogsbay site dev # MUST NOT loop — proves layout is correct
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
## 8. What this skill explicitly does NOT cover
|
|
266
|
+
|
|
267
|
+
These are intentional gaps for the migration phase:
|
|
268
|
+
|
|
269
|
+
- **`:::autodoc` directive emission** — Phase 4 of `plans/mkdocs-import-architecture.md`. Until then, migrations resolve autodoc at migration time (snapshot) and flag affected pages in MIGRATION.md.
|
|
270
|
+
- **Identity-based xrefs** (`xref:component:module:page[]`) — see `plans/identity-based-xrefs.md`. Not scheduled; migrations use markdown link syntax.
|
|
271
|
+
- **Plugin coverage** — what's preserved across the migration is bounded by what the source format's parser package (`format-mkdocs`, future `format-obsidian`, etc.) supports. Missing extensions degrade to raw markdown text and get flagged in MIGRATION.md.
|
|
272
|
+
|
|
273
|
+
## See also
|
|
274
|
+
|
|
275
|
+
- `plans/mkdocs-import-architecture.md` — the multi-phase migration plan that originated this convention
|
|
276
|
+
- `plans/content-assets-folder.md` — full design rationale for `_assets/`
|
|
277
|
+
- `plans/identity-based-xrefs.md` — strategic direction for move-resistant addressing
|
|
278
|
+
- `packages/cli/skills/platform/nav-file/SKILL.md` — nav file shape (referenced from this skill)
|
|
279
|
+
- `docs-dev/migrate-mkdocs.md` — implementation notes for the first migration command
|
|
280
|
+
- `research/formats/dogsbay-markdown-spec.md` — the Dogsbay-MD language spec
|