@open-agent-toolkit/cli 0.3.27 → 0.3.29
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 +8 -0
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/contributing/documentation.md +11 -2
- package/assets/docs/docs-tooling/add-docs-to-a-repo.md +12 -0
- package/assets/docs/docs-tooling/check.md +154 -0
- package/assets/docs/docs-tooling/commands.md +61 -11
- package/assets/docs/docs-tooling/index.md +1 -0
- package/assets/docs/docs-tooling/meta.json +7 -1
- package/assets/docs/docs-tooling/workflows.md +4 -0
- package/assets/docs/reference/cli-reference.md +2 -0
- package/assets/docs/reference/configuration.md +1 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/authoring-docs/SKILL.md +2 -2
- package/assets/skills/authoring-docs/references/information-architecture.md +5 -2
- package/assets/skills/authoring-docs/references/page-templates/alert-runbook.md +63 -0
- package/assets/skills/authoring-docs/references/page-templates/command-reference.md +55 -0
- package/assets/skills/authoring-docs/references/page-templates/decisions-pointer.md +44 -0
- package/assets/skills/authoring-docs/references/page-templates/engineering-feature.md +53 -0
- package/assets/skills/authoring-docs/references/page-templates/per-area-agents.md +42 -0
- package/assets/skills/authoring-docs/references/page-templates/product-feature.md +49 -0
- package/assets/skills/authoring-docs/references/page-templates/qa-validation.md +59 -0
- package/assets/skills/authoring-docs/references/page-templates/screen-spec.md +51 -0
- package/assets/skills/authoring-docs/references/page-templates/skill-command-guide.md +44 -0
- package/assets/skills/authoring-docs/references/page-templates/solution-record.md +43 -0
- package/assets/skills/authoring-docs/references/templates.md +22 -2
- package/assets/skills/oat-docs-analyze/SKILL.md +13 -2
- package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
- package/assets/skills/oat-docs-authoring/references/validation.md +4 -0
- package/assets/skills/oat-docs-bootstrap/SKILL.md +63 -6
- package/assets/skills/oat-docs-bootstrap/references/pr-ci.md +89 -0
- package/assets/templates/docs-app-fuma/AGENTS.md +9 -2
- package/assets/templates/docs-app-fuma/docs/contributing/docs-toolchain.md +40 -3
- package/assets/templates/docs-app-fuma/docs/contributing.md +7 -1
- package/assets/templates/docs-app-fuma/docs/index.md +1 -1
- package/assets/templates/docs-app-fuma/gitignore.template +12 -0
- package/assets/templates/docs-app-fuma/package.json.template +2 -1
- package/assets/templates/docs-app-fuma/pnpm-workspace.yaml +5 -0
- package/assets/templates/docs-app-mkdocs/AGENTS.md +4 -2
- package/assets/templates/docs-app-mkdocs/docs/contributing/docs-toolchain.md +4 -2
- package/assets/templates/docs-app-mkdocs/docs/contributing.md +7 -1
- package/assets/templates/docs-app-mkdocs/docs/index.md +1 -1
- package/assets/templates/docs-app-mkdocs/gitignore.template +8 -0
- package/assets/templates/docs-app-mkdocs/package.json.template +3 -2
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +35 -3
- package/dist/commands/docs/check/check.d.ts +3 -0
- package/dist/commands/docs/check/check.d.ts.map +1 -0
- package/dist/commands/docs/check/check.js +48 -0
- package/dist/commands/docs/check/check.types.d.ts +17 -0
- package/dist/commands/docs/check/check.types.d.ts.map +1 -0
- package/dist/commands/docs/check/check.types.js +1 -0
- package/dist/commands/docs/check/checker.d.ts +8 -0
- package/dist/commands/docs/check/checker.d.ts.map +1 -0
- package/dist/commands/docs/check/checker.js +82 -0
- package/dist/commands/docs/check/commands.d.ts +5 -0
- package/dist/commands/docs/check/commands.d.ts.map +1 -0
- package/dist/commands/docs/check/commands.js +232 -0
- package/dist/commands/docs/check/diagrams.d.ts +5 -0
- package/dist/commands/docs/check/diagrams.d.ts.map +1 -0
- package/dist/commands/docs/check/diagrams.js +197 -0
- package/dist/commands/docs/check/files.d.ts +21 -0
- package/dist/commands/docs/check/files.d.ts.map +1 -0
- package/dist/commands/docs/check/files.js +121 -0
- package/dist/commands/docs/check/markdown.d.ts +60 -0
- package/dist/commands/docs/check/markdown.d.ts.map +1 -0
- package/dist/commands/docs/check/markdown.js +125 -0
- package/dist/commands/docs/check/mermaid-runtime.d.ts +3 -0
- package/dist/commands/docs/check/mermaid-runtime.d.ts.map +1 -0
- package/dist/commands/docs/check/mermaid-runtime.js +57 -0
- package/dist/commands/docs/check/navigation.d.ts +4 -0
- package/dist/commands/docs/check/navigation.d.ts.map +1 -0
- package/dist/commands/docs/check/navigation.js +104 -0
- package/dist/commands/docs/check/reader-rules.d.ts +6 -0
- package/dist/commands/docs/check/reader-rules.d.ts.map +1 -0
- package/dist/commands/docs/check/reader-rules.js +214 -0
- package/dist/commands/docs/index.d.ts.map +1 -1
- package/dist/commands/docs/index.js +2 -0
- package/dist/commands/docs/init/content.d.ts +23 -0
- package/dist/commands/docs/init/content.d.ts.map +1 -0
- package/dist/commands/docs/init/content.js +361 -0
- package/dist/commands/docs/init/index.d.ts +1 -0
- package/dist/commands/docs/init/index.d.ts.map +1 -1
- package/dist/commands/docs/init/index.js +40 -1
- package/dist/commands/docs/init/markdown.d.ts.map +1 -1
- package/dist/commands/docs/init/markdown.js +21 -2
- package/dist/commands/docs/init/resolve-options.d.ts +4 -1
- package/dist/commands/docs/init/resolve-options.d.ts.map +1 -1
- package/dist/commands/docs/init/resolve-options.js +4 -0
- package/dist/commands/docs/init/scaffold.d.ts +1 -0
- package/dist/commands/docs/init/scaffold.d.ts.map +1 -1
- package/dist/commands/docs/init/scaffold.js +57 -4
- package/dist/commands/docs/nav/contents.d.ts +4 -0
- package/dist/commands/docs/nav/contents.d.ts.map +1 -1
- package/dist/commands/docs/nav/contents.js +10 -0
- package/dist/config/documentation-check.d.ts +38 -0
- package/dist/config/documentation-check.d.ts.map +1 -0
- package/dist/config/documentation-check.js +132 -0
- package/dist/config/oat-config.d.ts +2 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +4 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +1 -0
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +13 -0
- package/package.json +11 -2
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { readFile, readdir, realpath, stat } from 'node:fs/promises';
|
|
2
|
+
import { dirname, join, relative, resolve, isAbsolute } from 'node:path';
|
|
3
|
+
import { createExclusionMatcher, isExcludedPath, } from '../../docs/shared/exclusions.js';
|
|
4
|
+
import { readOatConfig, resolveDocumentationContentRoot, } from '../../../config/oat-config.js';
|
|
5
|
+
import { resolveProjectRoot } from '../../../fs/paths.js';
|
|
6
|
+
import { parsePage } from './markdown.js';
|
|
7
|
+
export function posix(path) {
|
|
8
|
+
return path.replaceAll('\\', '/');
|
|
9
|
+
}
|
|
10
|
+
export function inside(root, path) {
|
|
11
|
+
const rel = relative(root, path);
|
|
12
|
+
return (rel !== '..' &&
|
|
13
|
+
!rel.startsWith('../') &&
|
|
14
|
+
!rel.startsWith('..\\') &&
|
|
15
|
+
!isAbsolute(rel));
|
|
16
|
+
}
|
|
17
|
+
export async function present(path) {
|
|
18
|
+
try {
|
|
19
|
+
await stat(path);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (['ENOENT', 'ENOTDIR'].includes(error.code ?? ''))
|
|
24
|
+
return false;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
// Preserve the lexical missing-path diagnostic, but compare existing aliases by identity.
|
|
29
|
+
async function canonicalPath(path) {
|
|
30
|
+
return (await present(path)) ? realpath(path) : path;
|
|
31
|
+
}
|
|
32
|
+
export async function resolveCheckTarget(cwd, targetDir) {
|
|
33
|
+
let repoRoot;
|
|
34
|
+
try {
|
|
35
|
+
repoRoot = await resolveProjectRoot(cwd);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
repoRoot = cwd;
|
|
39
|
+
}
|
|
40
|
+
repoRoot = await realpath(repoRoot);
|
|
41
|
+
const config = await readOatConfig(repoRoot);
|
|
42
|
+
const doc = config.documentation ?? {};
|
|
43
|
+
const configured = doc.root
|
|
44
|
+
? await canonicalPath(resolve(repoRoot, doc.root))
|
|
45
|
+
: undefined;
|
|
46
|
+
const requested = resolve(cwd, targetDir ?? configured ?? join(repoRoot, 'docs'));
|
|
47
|
+
const appRoot = await canonicalPath(requested);
|
|
48
|
+
const matching = configured === appRoot;
|
|
49
|
+
const framework = matching && doc.tooling === 'markdown'
|
|
50
|
+
? 'markdown'
|
|
51
|
+
: (await present(join(appRoot, 'mkdocs.yml')))
|
|
52
|
+
? 'mkdocs'
|
|
53
|
+
: (await Promise.all(['ts', 'mts', 'js', 'mjs'].map((ext) => present(join(appRoot, `source.config.${ext}`))))).some(Boolean)
|
|
54
|
+
? 'fumadocs'
|
|
55
|
+
: matching && (doc.tooling === 'mkdocs' || doc.tooling === 'fumadocs')
|
|
56
|
+
? doc.tooling
|
|
57
|
+
: 'markdown';
|
|
58
|
+
// The established resolver owns configured nested docs semantics. An
|
|
59
|
+
// unrelated explicit Markdown root remains literal, even with a docs child.
|
|
60
|
+
const configuredContent = await resolveDocumentationContentRoot(repoRoot, config);
|
|
61
|
+
const configuredContentPath = configuredContent
|
|
62
|
+
? await canonicalPath(resolve(repoRoot, configuredContent))
|
|
63
|
+
: undefined;
|
|
64
|
+
const contentRoot = matching && configuredContentPath
|
|
65
|
+
? configuredContentPath
|
|
66
|
+
: framework === 'markdown'
|
|
67
|
+
? appRoot
|
|
68
|
+
: join(appRoot, 'docs');
|
|
69
|
+
if (!(await present(contentRoot)))
|
|
70
|
+
throw new Error(`Docs content directory does not exist: ${contentRoot}. Set documentation.root or pass --target-dir.`);
|
|
71
|
+
const matchesContent = configuredContentPath !== undefined && configuredContentPath === appRoot;
|
|
72
|
+
const documentation = matching || matchesContent ? doc : {};
|
|
73
|
+
return {
|
|
74
|
+
repoRoot,
|
|
75
|
+
appRoot,
|
|
76
|
+
contentRoot: await realpath(contentRoot),
|
|
77
|
+
framework,
|
|
78
|
+
documentation,
|
|
79
|
+
matcher: createExclusionMatcher(documentation.nonReaderPaths ?? []),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export async function readerFiles(target) {
|
|
83
|
+
const files = [];
|
|
84
|
+
const walk = async (dir) => {
|
|
85
|
+
const entries = await readdir(join(target.contentRoot, dir), {
|
|
86
|
+
withFileTypes: true,
|
|
87
|
+
});
|
|
88
|
+
for (const e of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
89
|
+
const path = posix(join(dir, e.name));
|
|
90
|
+
if (e.isDirectory() && !isExcludedPath(target.matcher, path, true))
|
|
91
|
+
await walk(path);
|
|
92
|
+
else if (e.isFile() &&
|
|
93
|
+
/\.mdx?$/.test(e.name) &&
|
|
94
|
+
!isExcludedPath(target.matcher, path))
|
|
95
|
+
files.push(path);
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
await walk('');
|
|
99
|
+
return files;
|
|
100
|
+
}
|
|
101
|
+
export async function readPage(target, path) {
|
|
102
|
+
return parsePage(path, await readFile(join(target.contentRoot, path), 'utf8'));
|
|
103
|
+
}
|
|
104
|
+
/** Never follow a link outside its allowed root, including symlink escapes. */
|
|
105
|
+
export async function containedFile(root, path) {
|
|
106
|
+
if (!inside(root, path) || !(await present(path)))
|
|
107
|
+
return false;
|
|
108
|
+
return (inside(await realpath(root), await realpath(path)) &&
|
|
109
|
+
(await stat(path)).isFile());
|
|
110
|
+
}
|
|
111
|
+
export function readerDirectories(paths) {
|
|
112
|
+
const dirs = new Set(['.']);
|
|
113
|
+
for (const path of paths) {
|
|
114
|
+
let dir = dirname(path);
|
|
115
|
+
while (dir !== '.') {
|
|
116
|
+
dirs.add(posix(dir));
|
|
117
|
+
dir = dirname(dir);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return [...dirs].sort();
|
|
121
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
export interface MarkdownNode {
|
|
2
|
+
type: string;
|
|
3
|
+
name?: string;
|
|
4
|
+
value?: string;
|
|
5
|
+
depth?: number;
|
|
6
|
+
url?: string;
|
|
7
|
+
alt?: string;
|
|
8
|
+
identifier?: string;
|
|
9
|
+
lang?: string | null;
|
|
10
|
+
children?: MarkdownNode[];
|
|
11
|
+
position?: {
|
|
12
|
+
start: {
|
|
13
|
+
line: number;
|
|
14
|
+
column: number;
|
|
15
|
+
offset?: number;
|
|
16
|
+
};
|
|
17
|
+
end: {
|
|
18
|
+
line: number;
|
|
19
|
+
column: number;
|
|
20
|
+
offset?: number;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
export interface PageLink {
|
|
25
|
+
href: string;
|
|
26
|
+
line: number;
|
|
27
|
+
image: boolean;
|
|
28
|
+
alt?: string;
|
|
29
|
+
html?: boolean;
|
|
30
|
+
describedBy?: string;
|
|
31
|
+
}
|
|
32
|
+
export interface ParsedPage {
|
|
33
|
+
path: string;
|
|
34
|
+
source: string;
|
|
35
|
+
body: string;
|
|
36
|
+
frontmatter: Record<string, unknown>;
|
|
37
|
+
frontmatterError?: string;
|
|
38
|
+
nodes: MarkdownNode[];
|
|
39
|
+
anchors: Set<string>;
|
|
40
|
+
links: PageLink[];
|
|
41
|
+
text: string;
|
|
42
|
+
}
|
|
43
|
+
export declare function nodeText(node: MarkdownNode): string;
|
|
44
|
+
interface HtmlNode {
|
|
45
|
+
nodeName: string;
|
|
46
|
+
value?: string;
|
|
47
|
+
attrs?: Array<{
|
|
48
|
+
name: string;
|
|
49
|
+
value: string;
|
|
50
|
+
}>;
|
|
51
|
+
childNodes?: HtmlNode[];
|
|
52
|
+
sourceCodeLocation?: {
|
|
53
|
+
startLine: number;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
export declare function htmlNodes(html: string): HtmlNode[];
|
|
57
|
+
export declare function attributes(node: HtmlNode): Record<string, string>;
|
|
58
|
+
export declare function parsePage(path: string, source: string): ParsedPage;
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=markdown.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"markdown.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/markdown.ts"],"names":[],"mappings":"AAQA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,CAAC,EAAE,YAAY,EAAE,CAAC;IAC1B,QAAQ,CAAC,EAAE;QACT,KAAK,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;QACzD,GAAG,EAAE;YAAE,IAAI,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAC;YAAC,MAAM,CAAC,EAAE,MAAM,CAAA;SAAE,CAAC;KACxD,CAAC;CACH;AACD,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,OAAO,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AACD,MAAM,WAAW,UAAU;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACrC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACrB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,IAAI,EAAE,MAAM,CAAC;CACd;AACD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,YAAY,GAAG,MAAM,CAInD;AACD,UAAU,QAAQ;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAC/C,UAAU,CAAC,EAAE,QAAQ,EAAE,CAAC;IACxB,kBAAkB,CAAC,EAAE;QAAE,SAAS,EAAE,MAAM,CAAA;KAAE,CAAC;CAC5C;AACD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,QAAQ,EAAE,CAQlD;AACD,wBAAgB,UAAU,CAAC,IAAI,EAAE,QAAQ,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEjE;AACD,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,UAAU,CAoGlE"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { remarkTabs } from '@open-agent-toolkit/docs-transforms';
|
|
2
|
+
import Slugger from 'github-slugger';
|
|
3
|
+
import { parseFragment } from 'parse5';
|
|
4
|
+
import remarkGfm from 'remark-gfm';
|
|
5
|
+
import remarkParse from 'remark-parse';
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
export function nodeText(node) {
|
|
9
|
+
return node.children
|
|
10
|
+
? node.children.map(nodeText).join('')
|
|
11
|
+
: (node.value ?? '');
|
|
12
|
+
}
|
|
13
|
+
export function htmlNodes(html) {
|
|
14
|
+
const nodes = [];
|
|
15
|
+
const walk = (n) => {
|
|
16
|
+
nodes.push(n);
|
|
17
|
+
n.childNodes?.forEach(walk);
|
|
18
|
+
};
|
|
19
|
+
walk(parseFragment(html, { sourceCodeLocationInfo: true }));
|
|
20
|
+
return nodes;
|
|
21
|
+
}
|
|
22
|
+
export function attributes(node) {
|
|
23
|
+
return Object.fromEntries(node.attrs?.map((a) => [a.name, a.value]) ?? []);
|
|
24
|
+
}
|
|
25
|
+
export function parsePage(path, source) {
|
|
26
|
+
let body = source;
|
|
27
|
+
let frontmatter = {};
|
|
28
|
+
let frontmatterError;
|
|
29
|
+
const match = /^(?:\uFEFF)?---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(source);
|
|
30
|
+
if (match) {
|
|
31
|
+
// Preserve physical positions for both AST and Contents diagnostics.
|
|
32
|
+
body = match[0].replace(/[^\r\n]/g, ' ') + source.slice(match[0].length);
|
|
33
|
+
try {
|
|
34
|
+
const doc = YAML.parseDocument(match[1]);
|
|
35
|
+
if (doc.errors.length)
|
|
36
|
+
frontmatterError = `Invalid YAML frontmatter: ${doc.errors[0].message}`;
|
|
37
|
+
else {
|
|
38
|
+
const data = doc.toJS();
|
|
39
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data))
|
|
40
|
+
frontmatterError = 'Frontmatter must be a YAML mapping';
|
|
41
|
+
else
|
|
42
|
+
frontmatter = data;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
frontmatterError = `Invalid YAML frontmatter: ${error.message}`;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const processor = unified().use(remarkParse).use(remarkGfm).use(remarkTabs);
|
|
50
|
+
const tree = processor.runSync(processor.parse(body), {
|
|
51
|
+
value: body,
|
|
52
|
+
});
|
|
53
|
+
const nodes = [];
|
|
54
|
+
const walk = (node) => {
|
|
55
|
+
nodes.push(node);
|
|
56
|
+
node.children?.forEach(walk);
|
|
57
|
+
};
|
|
58
|
+
walk(tree);
|
|
59
|
+
const definitions = new Map();
|
|
60
|
+
for (const n of nodes)
|
|
61
|
+
if (n.type === 'definition' &&
|
|
62
|
+
n.identifier &&
|
|
63
|
+
n.url &&
|
|
64
|
+
!definitions.has(n.identifier))
|
|
65
|
+
definitions.set(n.identifier, n.url);
|
|
66
|
+
const anchors = new Set();
|
|
67
|
+
const slugger = new Slugger();
|
|
68
|
+
const links = [];
|
|
69
|
+
for (const n of nodes) {
|
|
70
|
+
const line = n.position?.start.line ?? 1;
|
|
71
|
+
if (n.type === 'heading') {
|
|
72
|
+
const text = nodeText(n);
|
|
73
|
+
const custom = /\s*(?:\[#([^\]]+)\]|\{#([^}]+)\})\s*$/.exec(text);
|
|
74
|
+
anchors.add(custom?.[1] ?? custom?.[2] ?? slugger.slug(text));
|
|
75
|
+
}
|
|
76
|
+
if (['link', 'image', 'linkReference', 'imageReference'].includes(n.type)) {
|
|
77
|
+
const href = n.url ?? definitions.get(n.identifier ?? '');
|
|
78
|
+
if (href !== undefined)
|
|
79
|
+
links.push({
|
|
80
|
+
href,
|
|
81
|
+
line,
|
|
82
|
+
image: n.type.startsWith('image'),
|
|
83
|
+
alt: n.alt,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (n.type === 'html')
|
|
87
|
+
for (const element of htmlNodes(n.value ?? '')) {
|
|
88
|
+
const a = attributes(element);
|
|
89
|
+
if (a.id)
|
|
90
|
+
anchors.add(a.id);
|
|
91
|
+
if (element.nodeName === 'a' && a.name)
|
|
92
|
+
anchors.add(a.name);
|
|
93
|
+
if (element.nodeName === 'a' && a.href)
|
|
94
|
+
links.push({
|
|
95
|
+
href: a.href,
|
|
96
|
+
image: false,
|
|
97
|
+
html: true,
|
|
98
|
+
line: line + (element.sourceCodeLocation?.startLine ?? 1) - 1,
|
|
99
|
+
});
|
|
100
|
+
if (element.nodeName === 'img' && a.src)
|
|
101
|
+
links.push({
|
|
102
|
+
href: a.src,
|
|
103
|
+
alt: a.alt,
|
|
104
|
+
image: true,
|
|
105
|
+
html: true,
|
|
106
|
+
describedBy: a['aria-describedby'],
|
|
107
|
+
line: line + (element.sourceCodeLocation?.startLine ?? 1) - 1,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
path,
|
|
113
|
+
source,
|
|
114
|
+
body,
|
|
115
|
+
frontmatter,
|
|
116
|
+
...(frontmatterError ? { frontmatterError } : {}),
|
|
117
|
+
nodes,
|
|
118
|
+
anchors,
|
|
119
|
+
links,
|
|
120
|
+
text: nodes
|
|
121
|
+
.filter((n) => n.type === 'text')
|
|
122
|
+
.map((n) => n.value ?? '')
|
|
123
|
+
.join(' '),
|
|
124
|
+
};
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mermaid-runtime.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/mermaid-runtime.ts"],"names":[],"mappings":"AA6CA,8EAA8E;AAC9E,wBAAsB,YAAY,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAehE"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
let runtime;
|
|
2
|
+
let queue = Promise.resolve();
|
|
3
|
+
async function withDom(dom, action) {
|
|
4
|
+
const keys = ['window', 'document'];
|
|
5
|
+
const saved = keys.map((key) => Object.getOwnPropertyDescriptor(globalThis, key));
|
|
6
|
+
Object.defineProperty(globalThis, 'window', {
|
|
7
|
+
configurable: true,
|
|
8
|
+
value: dom.window,
|
|
9
|
+
});
|
|
10
|
+
Object.defineProperty(globalThis, 'document', {
|
|
11
|
+
configurable: true,
|
|
12
|
+
value: dom.window.document,
|
|
13
|
+
});
|
|
14
|
+
try {
|
|
15
|
+
return await action();
|
|
16
|
+
}
|
|
17
|
+
finally {
|
|
18
|
+
keys.forEach((key, index) => {
|
|
19
|
+
const descriptor = saved[index];
|
|
20
|
+
if (descriptor)
|
|
21
|
+
Object.defineProperty(globalThis, key, descriptor);
|
|
22
|
+
else
|
|
23
|
+
Reflect.deleteProperty(globalThis, key);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
async function loadRuntime() {
|
|
28
|
+
const { JSDOM } = await import('jsdom');
|
|
29
|
+
// Defaults intentionally disable script execution and external resources.
|
|
30
|
+
const dom = new JSDOM('');
|
|
31
|
+
try {
|
|
32
|
+
const mermaid = await withDom(dom, async () => (await import('mermaid')).default);
|
|
33
|
+
process.once('beforeExit', () => dom.window.close());
|
|
34
|
+
return { dom, mermaid };
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
dom.window.close();
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Lazy, real Node DOM for Mermaid syntax parsing only. Never renders SVG. */
|
|
42
|
+
export async function parseMermaid(source) {
|
|
43
|
+
const result = queue.then(async () => {
|
|
44
|
+
runtime ??= loadRuntime();
|
|
45
|
+
const { dom, mermaid } = await runtime;
|
|
46
|
+
await withDom(dom, async () => {
|
|
47
|
+
mermaid.initialize({
|
|
48
|
+
startOnLoad: false,
|
|
49
|
+
securityLevel: 'strict',
|
|
50
|
+
logLevel: 'fatal',
|
|
51
|
+
});
|
|
52
|
+
await mermaid.parse(source);
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
queue = result.catch(() => undefined);
|
|
56
|
+
await result;
|
|
57
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/navigation.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAW,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AAgCpD,wBAAsB,eAAe,CACnC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAmGf"}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { buildDocsNavTree } from '../../docs/nav/contents.js';
|
|
5
|
+
import { syncFumadocsNavigation } from '../../docs/nav/fumadocs.js';
|
|
6
|
+
import { syncDocsNavigation } from '../../docs/nav/sync.js';
|
|
7
|
+
import YAML from 'yaml';
|
|
8
|
+
import { present } from './files.js';
|
|
9
|
+
function pageIdentity(item) {
|
|
10
|
+
// Hand-authored collision links can use their own display labels.
|
|
11
|
+
return item.replace(/^\[[^\]]*\]\((.*)\)$/, '[]($1)').replace(/^\.\//, '');
|
|
12
|
+
}
|
|
13
|
+
function navIdentity(value, expected) {
|
|
14
|
+
if (!Array.isArray(value))
|
|
15
|
+
return value;
|
|
16
|
+
return value.map((item, index) => {
|
|
17
|
+
if (typeof item === 'string')
|
|
18
|
+
return item;
|
|
19
|
+
if (typeof item !== 'object' || item === null)
|
|
20
|
+
return item;
|
|
21
|
+
const entry = Object.entries(item)[0];
|
|
22
|
+
if (!entry)
|
|
23
|
+
return item;
|
|
24
|
+
const expectedItem = Array.isArray(expected) ? expected[index] : undefined;
|
|
25
|
+
const children = expectedItem && typeof expectedItem === 'object'
|
|
26
|
+
? Object.values(expectedItem)[0]
|
|
27
|
+
: undefined;
|
|
28
|
+
// The Contents-derived tree identifies sections by their direct landing path.
|
|
29
|
+
// Actual landing/page labels are presentation; explicit group labels remain meaningful.
|
|
30
|
+
const section = Array.isArray(children) &&
|
|
31
|
+
children.some((child) => typeof child === 'string' && child.endsWith('/index.md'));
|
|
32
|
+
return Array.isArray(entry[1])
|
|
33
|
+
? {
|
|
34
|
+
[section ? '$section' : entry[0]]: navIdentity(entry[1], children),
|
|
35
|
+
}
|
|
36
|
+
: entry[1];
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
export async function checkNavigation(target, report) {
|
|
40
|
+
if (target.framework === 'markdown')
|
|
41
|
+
return;
|
|
42
|
+
try {
|
|
43
|
+
const generated = target.documentation.navOwner === 'nav-sync';
|
|
44
|
+
const nonReaderPaths = target.documentation.nonReaderPaths;
|
|
45
|
+
if (target.framework === 'fumadocs') {
|
|
46
|
+
const result = await syncFumadocsNavigation({
|
|
47
|
+
docsRoot: target.contentRoot,
|
|
48
|
+
check: true,
|
|
49
|
+
nonReaderPaths,
|
|
50
|
+
});
|
|
51
|
+
for (const file of result.metaFiles) {
|
|
52
|
+
let stale = file.changed;
|
|
53
|
+
if (!generated) {
|
|
54
|
+
if (!(await present(join(target.contentRoot, file.path))))
|
|
55
|
+
continue;
|
|
56
|
+
try {
|
|
57
|
+
const actual = JSON.parse(await readFile(join(target.contentRoot, file.path), 'utf8'));
|
|
58
|
+
if (actual &&
|
|
59
|
+
typeof actual === 'object' &&
|
|
60
|
+
!Array.isArray(actual) &&
|
|
61
|
+
!Object.hasOwn(actual, 'pages'))
|
|
62
|
+
continue;
|
|
63
|
+
stale =
|
|
64
|
+
!Array.isArray(actual.pages) ||
|
|
65
|
+
!isDeepStrictEqual(actual.pages.map((p) => typeof p === 'string' ? pageIdentity(p) : p), file.meta.pages.map(pageIdentity));
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
report('navigation', 'error', file.path, 'Cannot read or parse meta.json; check file permissions and JSON syntax.');
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (stale)
|
|
73
|
+
report('navigation', 'error', file.path, generated
|
|
74
|
+
? 'Generated navigation is stale; run oat docs nav sync.'
|
|
75
|
+
: 'Navigation membership/order differs from authored Contents; update pages or Contents.');
|
|
76
|
+
}
|
|
77
|
+
for (const path of result.unlisted)
|
|
78
|
+
report('navigation', 'error', path, 'Reader page/folder is unlisted in authored Contents and would be hidden from navigation.');
|
|
79
|
+
}
|
|
80
|
+
else if (generated) {
|
|
81
|
+
const result = await syncDocsNavigation({
|
|
82
|
+
appRoot: target.appRoot,
|
|
83
|
+
check: true,
|
|
84
|
+
nonReaderPaths,
|
|
85
|
+
});
|
|
86
|
+
for (const path of result.stale)
|
|
87
|
+
report('navigation', 'error', path, 'Generated navigation is stale; run oat docs nav sync.');
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
const expected = await buildDocsNavTree({
|
|
91
|
+
docsRoot: target.contentRoot,
|
|
92
|
+
nonReaderPaths,
|
|
93
|
+
});
|
|
94
|
+
const actual = YAML.parse(await readFile(join(target.appRoot, 'mkdocs.yml'), 'utf8'));
|
|
95
|
+
if (!actual || !Object.hasOwn(actual, 'nav'))
|
|
96
|
+
return;
|
|
97
|
+
if (!isDeepStrictEqual(navIdentity(actual.nav, expected), navIdentity(expected, expected)))
|
|
98
|
+
report('navigation', 'error', 'mkdocs.yml', 'Navigation membership/order differs from authored Contents; update nav or Contents.');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
report('navigation', 'error', 'index.md', error.message);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Report } from './check.types.js';
|
|
2
|
+
import { type CheckTarget } from './files.js';
|
|
3
|
+
import { type ParsedPage } from './markdown.js';
|
|
4
|
+
export declare function checkReaders(target: CheckTarget, pages: ParsedPage[], report: Report): Promise<void>;
|
|
5
|
+
export declare function checkProvenance(target: CheckTarget, pages: ParsedPage[], report: Report): void;
|
|
6
|
+
//# sourceMappingURL=reader-rules.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reader-rules.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/reader-rules.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAML,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAuVf;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,IAAI,CA+CN"}
|