@open-agent-toolkit/cli 0.3.24 → 0.3.26
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/assets/bundle-metadata.json +1 -1
- package/assets/docs/docs-tooling/add-docs-to-a-repo.md +14 -6
- package/assets/docs/docs-tooling/commands.md +12 -9
- package/assets/docs/docs-tooling/workflows.md +53 -8
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/configuration.md +3 -0
- package/assets/docs/reference/docs-index-contract.md +19 -6
- package/assets/docs/reference/oat-directory-structure.md +13 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-docs-analyze/SKILL.md +106 -6
- package/assets/skills/oat-docs-analyze/references/analysis-artifact-template.md +42 -2
- package/assets/skills/oat-docs-analyze/scripts/package-drift.mjs +129 -0
- package/assets/skills/oat-docs-apply/SKILL.md +131 -12
- package/assets/skills/oat-docs-apply/references/apply-plan-template.md +58 -14
- package/assets/skills/oat-docs-bootstrap/SKILL.md +33 -8
- package/assets/templates/docs-app-fuma/package.json.template +1 -1
- package/assets/templates/docs-app-fuma/source.config.ts +6 -1
- package/assets/templates/docs-app-mkdocs/package.json.template +2 -2
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +67 -9
- package/dist/commands/docs/index-generate/generator.d.ts +4 -34
- package/dist/commands/docs/index-generate/generator.d.ts.map +1 -1
- package/dist/commands/docs/index-generate/generator.js +51 -157
- package/dist/commands/docs/index-generate/index.d.ts +1 -1
- package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
- package/dist/commands/docs/index-generate/index.js +26 -8
- package/dist/commands/docs/init/index.d.ts.map +1 -1
- package/dist/commands/docs/init/index.js +21 -1
- package/dist/commands/docs/init/scaffold.d.ts.map +1 -1
- package/dist/commands/docs/init/scaffold.js +20 -13
- package/dist/commands/docs/nav/contents.d.ts +13 -3
- package/dist/commands/docs/nav/contents.d.ts.map +1 -1
- package/dist/commands/docs/nav/contents.js +85 -24
- package/dist/commands/docs/nav/fumadocs.d.ts +1 -0
- package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
- package/dist/commands/docs/nav/fumadocs.js +26 -9
- package/dist/commands/docs/nav/sync.d.ts +2 -0
- package/dist/commands/docs/nav/sync.d.ts.map +1 -1
- package/dist/commands/docs/nav/sync.js +21 -1
- package/dist/commands/docs/shared/exclusions.d.ts +37 -0
- package/dist/commands/docs/shared/exclusions.d.ts.map +1 -0
- package/dist/commands/docs/shared/exclusions.js +161 -0
- package/dist/config/oat-config.d.ts +18 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +64 -3
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +5 -1
- package/package.json +2 -2
|
@@ -135,16 +135,6 @@ export async function resolveOatDepContext(repoRoot, assetsRoot) {
|
|
|
135
135
|
};
|
|
136
136
|
return { isOatRepo, localPackages, oatPackageVersions };
|
|
137
137
|
}
|
|
138
|
-
function buildDevDependencies(lint, format) {
|
|
139
|
-
const entries = [];
|
|
140
|
-
if (lint === 'markdownlint-cli2') {
|
|
141
|
-
entries.push(' "markdownlint-cli2": "^0.13.0"');
|
|
142
|
-
}
|
|
143
|
-
if (format === 'oxfmt') {
|
|
144
|
-
entries.push(' "oxfmt": "^0.36.0"');
|
|
145
|
-
}
|
|
146
|
-
return entries.join(',\n');
|
|
147
|
-
}
|
|
148
138
|
function buildFumaDevDependencies(lint, format) {
|
|
149
139
|
const entries = [];
|
|
150
140
|
if (lint === 'markdownlint-cli2') {
|
|
@@ -158,9 +148,22 @@ function buildFumaDevDependencies(lint, format) {
|
|
|
158
148
|
}
|
|
159
149
|
return `,\n${entries.join(',\n')}`;
|
|
160
150
|
}
|
|
151
|
+
function shellQuote(value) {
|
|
152
|
+
return /^[a-zA-Z0-9_./-]+$/.test(value)
|
|
153
|
+
? value
|
|
154
|
+
: `'${value.replaceAll("'", "'\\''")}'`;
|
|
155
|
+
}
|
|
156
|
+
function jsonStringContent(value) {
|
|
157
|
+
return JSON.stringify(value).slice(1, -1);
|
|
158
|
+
}
|
|
159
|
+
function buildNavSyncCheckCmd(isOatRepo, targetDir) {
|
|
160
|
+
return isOatRepo
|
|
161
|
+
? `pnpm -w run cli:source -- docs nav sync --target-dir ${shellQuote(targetDir)} --check`
|
|
162
|
+
: 'oat docs nav sync --target-dir . --check';
|
|
163
|
+
}
|
|
161
164
|
function buildGenerateIndexCmd(isOatRepo, targetDir) {
|
|
162
165
|
if (isOatRepo) {
|
|
163
|
-
return `pnpm -w run cli:source -- docs generate-index --docs-dir ${targetDir}/docs --output ${targetDir}/index.md`;
|
|
166
|
+
return `pnpm -w run cli:source -- docs generate-index --docs-dir ${shellQuote(`${targetDir}/docs`)} --output ${shellQuote(`${targetDir}/index.md`)}`;
|
|
164
167
|
}
|
|
165
168
|
return 'oat docs generate-index --docs-dir docs --output index.md';
|
|
166
169
|
}
|
|
@@ -231,6 +234,7 @@ function renderTemplate(template, options, depContext, packageManagerField, node
|
|
|
231
234
|
' // {{TURBOPACK_ROOT_OPTION}}\n': options.repoShape === 'nested-standalone'
|
|
232
235
|
? ' turbopack: { root: dirname(fileURLToPath(import.meta.url)) },\n'
|
|
233
236
|
: '',
|
|
237
|
+
'{{OAT_CONFIG_RELATIVE_PATH}}': `./${relative(resolve(options.repoRoot, options.targetDir), join(options.repoRoot, '.oat/config.json')).replaceAll('\\', '/')}`,
|
|
234
238
|
'{{APP_NAME}}': options.appName,
|
|
235
239
|
'{{NODE_REQUIREMENT}}': nodeRequirement,
|
|
236
240
|
'{{PACKAGE_NAME}}': options.appName,
|
|
@@ -252,11 +256,12 @@ function renderTemplate(template, options, depContext, packageManagerField, node
|
|
|
252
256
|
'{{DOCS_FORMAT_CHECK_SCRIPT}}': options.format === 'oxfmt'
|
|
253
257
|
? "oxfmt --check 'docs/**/*.md'"
|
|
254
258
|
: "echo 'docs format check disabled'",
|
|
255
|
-
'{{DEV_DEPENDENCIES}}':
|
|
259
|
+
'{{DEV_DEPENDENCIES}}': buildFumaDevDependencies(options.lint, options.format),
|
|
256
260
|
'{{FUMA_DEV_DEPENDENCIES}}': buildFumaDevDependencies(options.lint, options.format),
|
|
257
261
|
'{{REPO_NAME}}': repoName,
|
|
258
262
|
'{{APP_DIR}}': options.targetDir,
|
|
259
|
-
'{{GENERATE_INDEX_CMD}}': buildGenerateIndexCmd(depContext.isOatRepo, options.targetDir),
|
|
263
|
+
'{{GENERATE_INDEX_CMD}}': jsonStringContent(buildGenerateIndexCmd(depContext.isOatRepo, options.targetDir)),
|
|
264
|
+
'{{NAV_SYNC_CHECK_CMD}}': jsonStringContent(buildNavSyncCheckCmd(depContext.isOatRepo, options.targetDir)),
|
|
260
265
|
'{{OAT_DOCS_CONFIG_DEP}}': oatDepVersion(depContext, 'docs-config'),
|
|
261
266
|
'{{OAT_DOCS_THEME_DEP}}': oatDepVersion(depContext, 'docs-theme'),
|
|
262
267
|
'{{OAT_DOCS_TRANSFORMS_DEP}}': oatDepVersion(depContext, 'docs-transforms'),
|
|
@@ -285,12 +290,14 @@ export function buildDocumentationConfig(framework, targetDir) {
|
|
|
285
290
|
return {
|
|
286
291
|
root: targetDir,
|
|
287
292
|
tooling: 'fumadocs',
|
|
293
|
+
navOwner: 'nav-sync',
|
|
288
294
|
index: join(targetDir, 'index.md'),
|
|
289
295
|
};
|
|
290
296
|
}
|
|
291
297
|
return {
|
|
292
298
|
root: targetDir,
|
|
293
299
|
tooling: 'mkdocs',
|
|
300
|
+
navOwner: 'nav-sync',
|
|
294
301
|
config: join(targetDir, 'mkdocs.yml'),
|
|
295
302
|
index: join(targetDir, 'mkdocs.yml'),
|
|
296
303
|
};
|
|
@@ -1,21 +1,31 @@
|
|
|
1
|
+
import { type ExclusionMatcher } from '../../docs/shared/exclusions.js';
|
|
1
2
|
export interface DocsContentsEntry {
|
|
2
3
|
title: string;
|
|
3
4
|
href: string;
|
|
5
|
+
group?: {
|
|
6
|
+
title: string;
|
|
7
|
+
line: number;
|
|
8
|
+
};
|
|
4
9
|
}
|
|
5
|
-
|
|
10
|
+
/** Shared discovery lets inventory generation retain its no-map fallback. */
|
|
11
|
+
export declare function hasIndexContents(markdown: string): boolean;
|
|
12
|
+
export declare function parseIndexContents(markdown: string, sourcePath?: string, options?: {
|
|
13
|
+
allowEmpty?: boolean;
|
|
14
|
+
}): DocsContentsEntry[];
|
|
6
15
|
export type DocsNavTree = Array<Record<string, DocsNavTree | string> | string>;
|
|
7
16
|
interface BuildDocsNavTreeOptions {
|
|
8
17
|
docsRoot: string;
|
|
9
18
|
dirRelativePath?: string;
|
|
10
19
|
visitedDirectories?: Set<string>;
|
|
20
|
+
nonReaderPaths?: readonly string[];
|
|
11
21
|
}
|
|
12
22
|
export declare function resolveEntryTarget(docsRoot: string, directoryPath: string, dirRelativePath: string, entry: DocsContentsEntry,
|
|
13
23
|
/**
|
|
14
24
|
* Page extensions a Contents link may target. MkDocs renders only `.md`;
|
|
15
25
|
* Fumadocs also renders `.mdx` pages.
|
|
16
26
|
*/
|
|
17
|
-
pageExtensions?: readonly string[]): Promise<{
|
|
18
|
-
kind: 'page' | 'section';
|
|
27
|
+
pageExtensions?: readonly string[], matcher?: ExclusionMatcher): Promise<{
|
|
28
|
+
kind: 'page' | 'section' | 'excluded';
|
|
19
29
|
path: string;
|
|
20
30
|
}>;
|
|
21
31
|
export declare function buildDocsNavTree(options: BuildDocsNavTreeOptions): Promise<DocsNavTree>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contents.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/contents.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"contents.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/contents.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAC;AAG1C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC;CACzC;AA8BD,6EAA6E;AAC7E,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,SAAa,EACvB,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,iBAAiB,EAAE,CAoDrB;AAED,MAAM,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,GAAG,MAAM,CAAC,CAAC;AAE/E,UAAU,uBAAuB;IAC/B,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,kBAAkB,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IACjC,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAMD,wBAAsB,kBAAkB,CACtC,QAAQ,EAAE,MAAM,EAChB,aAAa,EAAE,MAAM,EACrB,eAAe,EAAE,MAAM,EACvB,KAAK,EAAE,iBAAiB;AACxB;;;GAGG;AACH,cAAc,GAAE,SAAS,MAAM,EAAY,EAC3C,OAAO,CAAC,EAAE,gBAAgB,GACzB,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,UAAU,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAuElE;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC,CA0EtB"}
|
|
@@ -1,28 +1,75 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, dirname, extname, join, relative, resolve } from 'node:path';
|
|
3
|
+
import { createExclusionMatcher, isExcludedPath, } from '../../docs/shared/exclusions.js';
|
|
3
4
|
import { dirExists, fileExists } from '../../../fs/io.js';
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
/** Mask fenced examples without changing authored source line numbers. */
|
|
6
|
+
function unfencedLines(markdown) {
|
|
7
|
+
let fence;
|
|
8
|
+
return markdown.split(/\r?\n/).map((line) => {
|
|
9
|
+
if (fence) {
|
|
10
|
+
const closing = line.match(/^ {0,3}(`{3,}|~{3,})[ \t]*$/);
|
|
11
|
+
if (closing &&
|
|
12
|
+
closing[1][0] === fence.marker &&
|
|
13
|
+
closing[1].length >= fence.length) {
|
|
14
|
+
fence = undefined;
|
|
15
|
+
}
|
|
16
|
+
return '';
|
|
17
|
+
}
|
|
18
|
+
const opening = line.match(/^ {0,3}(`{3,}|~{3,})(.*)$/);
|
|
19
|
+
if (opening && (opening[1][0] === '~' || !opening[2].includes('`'))) {
|
|
20
|
+
fence = { marker: opening[1][0], length: opening[1].length };
|
|
21
|
+
return '';
|
|
22
|
+
}
|
|
23
|
+
return line;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
function contentsHeadingIndex(lines) {
|
|
27
|
+
return lines.findIndex((line) => /^##\s+Contents\s*$/.test(line.trim()));
|
|
28
|
+
}
|
|
29
|
+
/** Shared discovery lets inventory generation retain its no-map fallback. */
|
|
30
|
+
export function hasIndexContents(markdown) {
|
|
31
|
+
return contentsHeadingIndex(unfencedLines(markdown)) >= 0;
|
|
32
|
+
}
|
|
33
|
+
export function parseIndexContents(markdown, sourcePath = 'index.md', options = {}) {
|
|
34
|
+
const lines = unfencedLines(markdown);
|
|
35
|
+
const headingIndex = contentsHeadingIndex(lines);
|
|
36
|
+
if (headingIndex < 0) {
|
|
8
37
|
throw new Error(`Missing required ## Contents section in ${sourcePath}`);
|
|
9
38
|
}
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
39
|
+
const entries = [];
|
|
40
|
+
let group;
|
|
41
|
+
let groupEntries = 0;
|
|
42
|
+
const checkGroup = () => {
|
|
43
|
+
if (group && groupEntries === 0) {
|
|
44
|
+
throw new Error(`${sourcePath}:${group.line}: Contents group "${group.title}" has no entries`);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
for (let i = headingIndex + 1; i < lines.length; i += 1) {
|
|
48
|
+
const line = lines[i];
|
|
49
|
+
if (/^##\s+/.test(line.trim()))
|
|
50
|
+
break;
|
|
51
|
+
const heading = line.match(/^###\s+(.+?)\s*$/);
|
|
52
|
+
if (heading) {
|
|
53
|
+
checkGroup();
|
|
54
|
+
group = { title: heading[1].trim(), line: i + 1 };
|
|
55
|
+
groupEntries = 0;
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (/^\s*[*+]\s+\[[^\]]+\]\([^)]+\)/.test(line)) {
|
|
59
|
+
throw new Error(`${sourcePath}:${i + 1}: Contents links must use a dash (-) bullet`);
|
|
60
|
+
}
|
|
61
|
+
const match = line.match(/^\s*-\s+\[([^\]]+)\]\(([^)]+)\)/);
|
|
62
|
+
if (match) {
|
|
63
|
+
entries.push({
|
|
64
|
+
title: match[1].trim(),
|
|
65
|
+
href: match[2].trim(),
|
|
66
|
+
...(group ? { group } : {}),
|
|
67
|
+
});
|
|
68
|
+
groupEntries += 1;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
checkGroup();
|
|
72
|
+
if (entries.length === 0 && !options.allowEmpty) {
|
|
26
73
|
throw new Error(`No machine-readable links found under ## Contents in ${sourcePath}`);
|
|
27
74
|
}
|
|
28
75
|
return entries;
|
|
@@ -35,7 +82,7 @@ export async function resolveEntryTarget(docsRoot, directoryPath, dirRelativePat
|
|
|
35
82
|
* Page extensions a Contents link may target. MkDocs renders only `.md`;
|
|
36
83
|
* Fumadocs also renders `.mdx` pages.
|
|
37
84
|
*/
|
|
38
|
-
pageExtensions = ['.md']) {
|
|
85
|
+
pageExtensions = ['.md'], matcher) {
|
|
39
86
|
const href = entry.href.split('#', 1)[0]?.trim() ?? '';
|
|
40
87
|
if (!href) {
|
|
41
88
|
throw new Error(`Invalid contents link "${entry.href}" in ${join(docsRoot, dirRelativePath, 'index.md')}`);
|
|
@@ -53,6 +100,9 @@ pageExtensions = ['.md']) {
|
|
|
53
100
|
relativeTargetPath.length === 0) {
|
|
54
101
|
throw new Error(`Contents link "${entry.href}" escapes the docs directory from ${join(docsRoot, dirRelativePath, 'index.md')}`);
|
|
55
102
|
}
|
|
103
|
+
if (matcher && isExcludedPath(matcher, relativeTargetPath)) {
|
|
104
|
+
return { kind: 'excluded', path: relativeTargetPath };
|
|
105
|
+
}
|
|
56
106
|
if (!(await fileExists(targetPath))) {
|
|
57
107
|
throw new Error(`Contents link "${entry.href}" in ${join(docsRoot, dirRelativePath, 'index.md')} does not resolve to a Markdown file`);
|
|
58
108
|
}
|
|
@@ -80,17 +130,27 @@ export async function buildDocsNavTree(options) {
|
|
|
80
130
|
}
|
|
81
131
|
const indexMarkdown = await readFile(indexPath, 'utf8');
|
|
82
132
|
const contentsEntries = parseIndexContents(indexMarkdown, indexPath);
|
|
133
|
+
const matcher = createExclusionMatcher(options.nonReaderPaths ?? []);
|
|
83
134
|
const nav = dirRelativePath === '.'
|
|
84
135
|
? [{ Home: 'index.md' }]
|
|
85
136
|
: [toPosixPath(join(dirRelativePath, 'index.md'))];
|
|
137
|
+
let currentGroup;
|
|
138
|
+
let groupNav = nav;
|
|
86
139
|
for (const entry of contentsEntries) {
|
|
87
|
-
const resolvedTarget = await resolveEntryTarget(options.docsRoot, directoryPath, dirRelativePath, entry);
|
|
140
|
+
const resolvedTarget = await resolveEntryTarget(options.docsRoot, directoryPath, dirRelativePath, entry, ['.md'], matcher);
|
|
141
|
+
if (resolvedTarget.kind === 'excluded')
|
|
142
|
+
continue;
|
|
88
143
|
if (resolvedTarget.kind === 'section' &&
|
|
89
144
|
resolvedTarget.path === toPosixPath(join(dirRelativePath, 'index.md'))) {
|
|
90
145
|
continue;
|
|
91
146
|
}
|
|
147
|
+
if (entry.group !== currentGroup) {
|
|
148
|
+
currentGroup = entry.group;
|
|
149
|
+
groupNav = [];
|
|
150
|
+
nav.push({ [currentGroup.title]: groupNav });
|
|
151
|
+
}
|
|
92
152
|
if (resolvedTarget.kind === 'page') {
|
|
93
|
-
|
|
153
|
+
groupNav.push({ [entry.title]: resolvedTarget.path });
|
|
94
154
|
continue;
|
|
95
155
|
}
|
|
96
156
|
const childDirRelativePath = toPosixPath(dirname(resolvedTarget.path));
|
|
@@ -99,11 +159,12 @@ export async function buildDocsNavTree(options) {
|
|
|
99
159
|
}
|
|
100
160
|
const childVisitedDirectories = new Set(visitedDirectories);
|
|
101
161
|
childVisitedDirectories.add(childDirRelativePath);
|
|
102
|
-
|
|
162
|
+
groupNav.push({
|
|
103
163
|
[entry.title]: await buildDocsNavTree({
|
|
104
164
|
docsRoot: options.docsRoot,
|
|
105
165
|
dirRelativePath: childDirRelativePath,
|
|
106
166
|
visitedDirectories: childVisitedDirectories,
|
|
167
|
+
nonReaderPaths: options.nonReaderPaths,
|
|
107
168
|
}),
|
|
108
169
|
});
|
|
109
170
|
}
|
|
@@ -54,5 +54,6 @@ export declare function syncFumadocsNavigation(options: {
|
|
|
54
54
|
docsRoot: string;
|
|
55
55
|
/** Compute and compare only; never write. */
|
|
56
56
|
check?: boolean;
|
|
57
|
+
nonReaderPaths?: readonly string[];
|
|
57
58
|
}): Promise<SyncFumadocsNavigationResult>;
|
|
58
59
|
//# sourceMappingURL=fumadocs.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fumadocs.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/fumadocs.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fumadocs.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/fumadocs.ts"],"names":[],"mappings":"AAoBA;;;GAGG;AACH,MAAM,WAAW,YAAY;IAC3B,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,YAAY,CAAC;IACnB,OAAO,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,4BAA4B;IAC3C,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,mEAAmE;IACnE,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB;;;OAGG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB;;;OAGG;IACH,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AA6DD;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQxD;AAED,gFAAgF;AAChF,wBAAgB,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAoCvE;AA8BD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAkMD;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAuExC"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { readdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { basename, extname, join, posix } from 'node:path';
|
|
3
3
|
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
+
import { createExclusionMatcher, } from '../../docs/shared/exclusions.js';
|
|
4
5
|
import { getFrontmatterBlock, parseFrontmatterScalarFields, } from '../../shared/frontmatter.js';
|
|
5
6
|
import { fileExists } from '../../../fs/io.js';
|
|
6
|
-
import { parseIndexContents, resolveEntryTarget } from './contents.js';
|
|
7
|
+
import { parseIndexContents, resolveEntryTarget, } from './contents.js';
|
|
7
8
|
const PAGE_EXTENSIONS = ['.md', '.mdx'];
|
|
8
9
|
const GROUP_FOLDER = /^\(.+\)$/;
|
|
9
10
|
// The `pages` item grammar of the fumadocs-core 16.10.2 page-tree builder
|
|
@@ -18,7 +19,7 @@ const FENCE = /^\s*(```|~~~)/;
|
|
|
18
19
|
function parentDir(path) {
|
|
19
20
|
return posix.dirname(path);
|
|
20
21
|
}
|
|
21
|
-
async function inventoryDocs(docsRoot) {
|
|
22
|
+
async function inventoryDocs(docsRoot, matcher) {
|
|
22
23
|
const directories = [];
|
|
23
24
|
const pages = [];
|
|
24
25
|
async function walk(relativeDir) {
|
|
@@ -32,11 +33,14 @@ async function inventoryDocs(docsRoot) {
|
|
|
32
33
|
}
|
|
33
34
|
const entryPath = posix.join(relativeDir, entry.name);
|
|
34
35
|
if (entry.isDirectory()) {
|
|
36
|
+
if (matcher.excludesDirectory(entryPath))
|
|
37
|
+
continue;
|
|
35
38
|
await walk(entryPath);
|
|
36
39
|
}
|
|
37
40
|
else if (entry.isFile() &&
|
|
38
41
|
PAGE_EXTENSIONS.includes(extname(entry.name))) {
|
|
39
|
-
|
|
42
|
+
if (!matcher.excludesFile(entryPath))
|
|
43
|
+
pages.push(entryPath);
|
|
40
44
|
}
|
|
41
45
|
}
|
|
42
46
|
}
|
|
@@ -150,13 +154,15 @@ function hrefFragment(href) {
|
|
|
150
154
|
* only if `pages` lists it.
|
|
151
155
|
*/
|
|
152
156
|
function hasImplicitIndex(relativeDir, existing) {
|
|
153
|
-
if (relativeDir === '.' ||
|
|
157
|
+
if (relativeDir === '.' ||
|
|
158
|
+
existing?.root === true ||
|
|
159
|
+
(typeof existing?.root === 'string' && existing.root.trim().length > 0)) {
|
|
154
160
|
return false;
|
|
155
161
|
}
|
|
156
162
|
const pagesIndex = existing?.pagesIndex;
|
|
157
163
|
return pagesIndex === undefined || pagesIndex === 'index';
|
|
158
164
|
}
|
|
159
|
-
async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex) {
|
|
165
|
+
async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex, matcher) {
|
|
160
166
|
const indexPath = join(docsRoot, relativeDir, 'index.md');
|
|
161
167
|
const entries = parseIndexContents(markdown, indexPath);
|
|
162
168
|
// A folder the loader gives no implicit index (the docs root, a `root: true`
|
|
@@ -164,8 +170,11 @@ async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex)
|
|
|
164
170
|
// `index` would demote the folder index to an ordinary child, so it is left
|
|
165
171
|
// implicit.
|
|
166
172
|
const pages = implicitIndex ? [] : ['index'];
|
|
173
|
+
let currentGroup;
|
|
167
174
|
for (const entry of entries) {
|
|
168
|
-
const target = await resolveEntryTarget(docsRoot, join(docsRoot, relativeDir), relativeDir, entry, PAGE_EXTENSIONS);
|
|
175
|
+
const target = await resolveEntryTarget(docsRoot, join(docsRoot, relativeDir), relativeDir, entry, PAGE_EXTENSIONS, matcher);
|
|
176
|
+
if (target.kind === 'excluded')
|
|
177
|
+
continue;
|
|
169
178
|
const targetDir = parentDir(target.path);
|
|
170
179
|
let item;
|
|
171
180
|
if (target.kind === 'section' && targetDir === relativeDir) {
|
|
@@ -182,6 +191,12 @@ async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex)
|
|
|
182
191
|
item = `[${entry.title}](${pageUrl(target.path)}${hrefFragment(entry.href)})`;
|
|
183
192
|
}
|
|
184
193
|
if (!pages.includes(item)) {
|
|
194
|
+
if (entry.group !== currentGroup) {
|
|
195
|
+
currentGroup = entry.group;
|
|
196
|
+
// A leading space prevents the loader treating a literal [label] as an icon.
|
|
197
|
+
const title = currentGroup.title;
|
|
198
|
+
pages.push(`---${title.startsWith('[') ? ' ' : ''}${title}---`);
|
|
199
|
+
}
|
|
185
200
|
pages.push(item);
|
|
186
201
|
}
|
|
187
202
|
}
|
|
@@ -274,13 +289,15 @@ function collectUnlisted(inventory, metas, implicitIndexDirs) {
|
|
|
274
289
|
*/
|
|
275
290
|
export async function syncFumadocsNavigation(options) {
|
|
276
291
|
const { docsRoot } = options;
|
|
277
|
-
const
|
|
292
|
+
const matcher = createExclusionMatcher(options.nonReaderPaths ?? []);
|
|
293
|
+
const inventory = await inventoryDocs(docsRoot, matcher);
|
|
278
294
|
const metas = new Map();
|
|
279
295
|
const existingMetas = new Map();
|
|
280
296
|
const implicitIndexDirs = new Set();
|
|
281
297
|
for (const relativeDir of inventory.directories) {
|
|
282
298
|
const indexPath = join(docsRoot, relativeDir, 'index.md');
|
|
283
|
-
if (
|
|
299
|
+
if (matcher.excludesFile(posix.join(relativeDir, 'index.md')) ||
|
|
300
|
+
!(await fileExists(indexPath))) {
|
|
284
301
|
continue;
|
|
285
302
|
}
|
|
286
303
|
// Index handling depends on the effective metadata, which keeps the
|
|
@@ -295,7 +312,7 @@ export async function syncFumadocsNavigation(options) {
|
|
|
295
312
|
const title = resolveFolderTitle(markdown);
|
|
296
313
|
metas.set(relativeDir, {
|
|
297
314
|
...(title === undefined ? {} : { title }),
|
|
298
|
-
pages: await buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex),
|
|
315
|
+
pages: await buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex, matcher),
|
|
299
316
|
});
|
|
300
317
|
}
|
|
301
318
|
if (!metas.has('.')) {
|
|
@@ -12,6 +12,7 @@ interface SyncDocsNavigationOptions {
|
|
|
12
12
|
configuredTooling?: string | null;
|
|
13
13
|
/** Compute the navigation and compare it with the files; write nothing. */
|
|
14
14
|
check?: boolean;
|
|
15
|
+
nonReaderPaths?: readonly string[];
|
|
15
16
|
}
|
|
16
17
|
interface MkDocsNavigationResult {
|
|
17
18
|
framework: 'mkdocs';
|
|
@@ -35,6 +36,7 @@ type SyncDocsNavigationResult = MkDocsNavigationResult | FumadocsNavigationResul
|
|
|
35
36
|
interface DocsNavSyncDependencies {
|
|
36
37
|
buildCommandContext: (options: GlobalOptions) => CommandContext;
|
|
37
38
|
resolveConfiguredTooling: (cwd: string, appRoot: string) => Promise<string | null>;
|
|
39
|
+
resolveNonReaderPaths?: (cwd: string, appRoot: string) => Promise<string[]>;
|
|
38
40
|
syncDocsNavigation: (options: SyncDocsNavigationOptions) => Promise<SyncDocsNavigationResult>;
|
|
39
41
|
}
|
|
40
42
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/sync.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAG5C,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAOrD,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"sync.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/nav/sync.ts"],"names":[],"mappings":"AAGA,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,aAAa,EACnB,MAAM,sBAAsB,CAAC;AAK9B,OAAO,EAAE,OAAO,EAAU,MAAM,WAAW,CAAC;AAG5C,OAAO,EAAoB,KAAK,WAAW,EAAE,MAAM,YAAY,CAAC;AAChE,OAAO,EAA0B,KAAK,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAE3E,MAAM,MAAM,gBAAgB,GAAG,UAAU,GAAG,QAAQ,CAAC;AAOrD,UAAU,yBAAyB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAClC,2EAA2E;IAC3E,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACpC;AAED,UAAU,sBAAsB;IAC9B,SAAS,EAAE,QAAQ,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,GAAG,EAAE,WAAW,CAAC;IACjB,uEAAuE;IACvE,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,UAAU,wBAAwB;IAChC,SAAS,EAAE,UAAU,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,gBAAgB,EAAE,CAAC;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED,KAAK,wBAAwB,GACzB,sBAAsB,GACtB,wBAAwB,CAAC;AAE7B,UAAU,uBAAuB;IAC/B,mBAAmB,EAAE,CAAC,OAAO,EAAE,aAAa,KAAK,cAAc,CAAC;IAChE,wBAAwB,EAAE,CACxB,GAAG,EAAE,MAAM,EACX,OAAO,EAAE,MAAM,KACZ,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC5B,qBAAqB,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC5E,kBAAkB,EAAE,CAClB,OAAO,EAAE,yBAAyB,KAC/B,OAAO,CAAC,wBAAwB,CAAC,CAAC;CACxC;AAsBD;;;;;GAKG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,MAAM,EACf,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI,GAChC,OAAO,CAAC,gBAAgB,CAAC,CAe3B;AAuED,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA8CnC;AAuJD,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CA4BT"}
|
|
@@ -58,9 +58,24 @@ async function resolveConfiguredTooling(cwd, appRoot) {
|
|
|
58
58
|
return null;
|
|
59
59
|
}
|
|
60
60
|
}
|
|
61
|
+
async function resolveNonReaderPaths(cwd, appRoot) {
|
|
62
|
+
let repoRoot;
|
|
63
|
+
try {
|
|
64
|
+
repoRoot = await resolveProjectRoot(cwd);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
} // Explicit app targets also work outside a configured repository.
|
|
69
|
+
const config = await readOatConfig(repoRoot);
|
|
70
|
+
const root = config.documentation?.root?.trim();
|
|
71
|
+
return root && resolve(repoRoot, root) === appRoot
|
|
72
|
+
? (config.documentation?.nonReaderPaths ?? [])
|
|
73
|
+
: [];
|
|
74
|
+
}
|
|
61
75
|
const DEFAULT_DEPENDENCIES = {
|
|
62
76
|
buildCommandContext,
|
|
63
77
|
resolveConfiguredTooling,
|
|
78
|
+
resolveNonReaderPaths,
|
|
64
79
|
syncDocsNavigation,
|
|
65
80
|
};
|
|
66
81
|
function replaceTopLevelYamlSection(source, key, replacement) {
|
|
@@ -90,6 +105,7 @@ export async function syncDocsNavigation(options) {
|
|
|
90
105
|
const result = await syncFumadocsNavigation({
|
|
91
106
|
docsRoot,
|
|
92
107
|
check: options.check,
|
|
108
|
+
nonReaderPaths: options.nonReaderPaths,
|
|
93
109
|
});
|
|
94
110
|
return {
|
|
95
111
|
framework,
|
|
@@ -99,7 +115,10 @@ export async function syncDocsNavigation(options) {
|
|
|
99
115
|
};
|
|
100
116
|
}
|
|
101
117
|
const mkdocsPath = join(options.appRoot, 'mkdocs.yml');
|
|
102
|
-
const nav = await buildDocsNavTree({
|
|
118
|
+
const nav = await buildDocsNavTree({
|
|
119
|
+
docsRoot,
|
|
120
|
+
nonReaderPaths: options.nonReaderPaths,
|
|
121
|
+
});
|
|
103
122
|
const mkdocsSource = await readFile(mkdocsPath, 'utf8');
|
|
104
123
|
const navSection = YAML.stringify({ nav }).trimEnd();
|
|
105
124
|
const updatedMkdocsSource = replaceTopLevelYamlSection(mkdocsSource, 'nav', navSection);
|
|
@@ -198,6 +217,7 @@ async function runDocsNavSyncCommand(context, options, dependencies) {
|
|
|
198
217
|
appRoot,
|
|
199
218
|
configuredTooling: await dependencies.resolveConfiguredTooling(context.cwd, appRoot),
|
|
200
219
|
check: options.check === true,
|
|
220
|
+
nonReaderPaths: await dependencies.resolveNonReaderPaths?.(context.cwd, appRoot),
|
|
201
221
|
});
|
|
202
222
|
if (options.check) {
|
|
203
223
|
process.exitCode = reportCheckResult(context, targetDir, result) ? 0 : 1;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A compiled exclusion list.
|
|
3
|
+
*
|
|
4
|
+
* Files and directories are asked separately so a trailing-slash pattern can
|
|
5
|
+
* mean "directories only" while every other pattern applies to both.
|
|
6
|
+
*/
|
|
7
|
+
export interface ExclusionMatcher {
|
|
8
|
+
excludesFile: (relativePath: string) => boolean;
|
|
9
|
+
excludesDirectory: (relativePath: string) => boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Compile exclusion globs matched against **docs-root-relative POSIX paths**
|
|
13
|
+
* (`api/auth.md`, `api/nested`), never against absolute or CWD-relative ones.
|
|
14
|
+
*
|
|
15
|
+
* Grammar, defined once and documented in
|
|
16
|
+
* `apps/oat-docs/docs/reference/oat-directory-structure.md`:
|
|
17
|
+
*
|
|
18
|
+
* - `*` matches any run of characters inside a single path segment.
|
|
19
|
+
* - `**` spans `/` only as a whole segment, where it covers zero or more
|
|
20
|
+
* segments: `**\/CLAUDE.md` matches `CLAUDE.md` and `api/CLAUDE.md`. Inside a
|
|
21
|
+
* segment (`a**b`) it is an ordinary single-segment wildcard.
|
|
22
|
+
* - Patterns are anchored at both ends against the whole relative path, so a
|
|
23
|
+
* bare `CLAUDE.md` matches only the root-level file. Use `**\/CLAUDE.md` for
|
|
24
|
+
* every depth.
|
|
25
|
+
* - A trailing `/` restricts a pattern to directories: `drafts/` prunes the
|
|
26
|
+
* `drafts` directory and everything beneath it, and never matches a file.
|
|
27
|
+
* Without it, a pattern that matches a directory path prunes it too.
|
|
28
|
+
* - Matching is case-sensitive, `/` is the separator on every platform, and
|
|
29
|
+
* `*` is the only metacharacter — `.`, `+`, and friends are literal.
|
|
30
|
+
*
|
|
31
|
+
* Entries are trimmed; a leading `./` or `/` is stripped so both spellings
|
|
32
|
+
* anchor at the docs root. Blank entries are ignored.
|
|
33
|
+
*/
|
|
34
|
+
export declare function createExclusionMatcher(patterns: readonly string[]): ExclusionMatcher;
|
|
35
|
+
/** Explicit Contents targets must also respect pruned ancestor directories. */
|
|
36
|
+
export declare function isExcludedPath(matcher: ExclusionMatcher, path: string, directory?: boolean): boolean;
|
|
37
|
+
//# sourceMappingURL=exclusions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"exclusions.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/shared/exclusions.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,WAAW,gBAAgB;IAC/B,YAAY,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;IAChD,iBAAiB,EAAE,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC;CACtD;AAmHD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,SAAS,MAAM,EAAE,GAC1B,gBAAgB,CAwClB;AAED,+EAA+E;AAC/E,wBAAgB,cAAc,CAC5B,OAAO,EAAE,gBAAgB,EACzB,IAAI,EAAE,MAAM,EACZ,SAAS,UAAQ,GAChB,OAAO,CAQT"}
|