@open-agent-toolkit/cli 0.3.25 → 0.3.27

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.
Files changed (72) hide show
  1. package/README.md +6 -0
  2. package/assets/bundle-metadata.json +1 -1
  3. package/assets/docs/contributing/documentation.md +39 -6
  4. package/assets/docs/contributing/markdown-features.md +14 -2
  5. package/assets/docs/docs-tooling/add-docs-to-a-repo.md +26 -15
  6. package/assets/docs/docs-tooling/commands.md +13 -10
  7. package/assets/docs/docs-tooling/workflows.md +14 -10
  8. package/assets/docs/getting-started/tool-packs.md +5 -3
  9. package/assets/docs/reference/cli-reference.md +1 -1
  10. package/assets/docs/reference/configuration.md +2 -1
  11. package/assets/docs/reference/docs-index-contract.md +19 -6
  12. package/assets/docs/reference/oat-directory-structure.md +13 -0
  13. package/assets/docs/skills/index.md +8 -8
  14. package/assets/public-package-versions.json +4 -4
  15. package/assets/skills/oat-docs-analyze/SKILL.md +2 -2
  16. package/assets/skills/oat-docs-analyze/references/legacy-scaffold-checks.md +4 -0
  17. package/assets/skills/oat-docs-bootstrap/SKILL.md +49 -62
  18. package/assets/templates/docs-app-fuma/AGENTS.md +50 -0
  19. package/assets/templates/docs-app-fuma/app/[[...slug]]/page.tsx +7 -3
  20. package/assets/templates/docs-app-fuma/app/llms/[...slug]/route.ts +15 -0
  21. package/assets/templates/docs-app-fuma/app/llms-full.txt/route.ts +7 -0
  22. package/assets/templates/docs-app-fuma/app/llms.txt/route.ts +7 -0
  23. package/assets/templates/docs-app-fuma/docs/contributing/docs-toolchain.md +48 -0
  24. package/assets/templates/docs-app-fuma/docs/contributing/index.md +13 -0
  25. package/assets/templates/docs-app-fuma/docs/contributing.md +77 -41
  26. package/assets/templates/docs-app-fuma/docs/getting-started.md +3 -33
  27. package/assets/templates/docs-app-fuma/docs/index.md +2 -2
  28. package/assets/templates/docs-app-fuma/lib/source.ts +5 -0
  29. package/assets/templates/docs-app-fuma/package.json.template +1 -1
  30. package/assets/templates/docs-app-fuma/source.config.ts +6 -1
  31. package/assets/templates/docs-app-mkdocs/AGENTS.md +46 -0
  32. package/assets/templates/docs-app-mkdocs/docs/contributing/docs-toolchain.md +44 -0
  33. package/assets/templates/docs-app-mkdocs/docs/contributing/index.md +13 -0
  34. package/assets/templates/docs-app-mkdocs/docs/contributing.md +105 -21
  35. package/assets/templates/docs-app-mkdocs/docs/getting-started.md +7 -25
  36. package/assets/templates/docs-app-mkdocs/docs/index.md +7 -2
  37. package/assets/templates/docs-app-mkdocs/mkdocs.yml +1 -0
  38. package/assets/templates/docs-app-mkdocs/package.json.template +2 -2
  39. package/dist/commands/config/index.d.ts.map +1 -1
  40. package/dist/commands/config/index.js +21 -3
  41. package/dist/commands/docs/index-generate/generator.d.ts +4 -34
  42. package/dist/commands/docs/index-generate/generator.d.ts.map +1 -1
  43. package/dist/commands/docs/index-generate/generator.js +51 -157
  44. package/dist/commands/docs/index-generate/index.d.ts +1 -1
  45. package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
  46. package/dist/commands/docs/index-generate/index.js +26 -8
  47. package/dist/commands/docs/init/docs-commands.d.ts +1 -0
  48. package/dist/commands/docs/init/docs-commands.d.ts.map +1 -1
  49. package/dist/commands/docs/init/docs-commands.js +8 -3
  50. package/dist/commands/docs/init/index.d.ts.map +1 -1
  51. package/dist/commands/docs/init/index.js +21 -1
  52. package/dist/commands/docs/init/scaffold.d.ts.map +1 -1
  53. package/dist/commands/docs/init/scaffold.js +37 -16
  54. package/dist/commands/docs/nav/contents.d.ts +13 -3
  55. package/dist/commands/docs/nav/contents.d.ts.map +1 -1
  56. package/dist/commands/docs/nav/contents.js +85 -24
  57. package/dist/commands/docs/nav/fumadocs.d.ts +1 -0
  58. package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
  59. package/dist/commands/docs/nav/fumadocs.js +56 -17
  60. package/dist/commands/docs/nav/sync.d.ts +2 -0
  61. package/dist/commands/docs/nav/sync.d.ts.map +1 -1
  62. package/dist/commands/docs/nav/sync.js +21 -1
  63. package/dist/commands/docs/shared/exclusions.d.ts +37 -0
  64. package/dist/commands/docs/shared/exclusions.d.ts.map +1 -0
  65. package/dist/commands/docs/shared/exclusions.js +161 -0
  66. package/dist/config/oat-config.d.ts +2 -0
  67. package/dist/config/oat-config.d.ts.map +1 -1
  68. package/dist/config/oat-config.js +6 -3
  69. package/dist/config/resolve.d.ts.map +1 -1
  70. package/dist/config/resolve.js +1 -0
  71. package/package.json +2 -2
  72. package/assets/skills/oat-docs-bootstrap/assets/AGENTS.md.template +0 -64
@@ -2,10 +2,15 @@ import { readdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import { basename, dirname, isAbsolute, join, relative, resolve, } from 'node:path';
3
3
  import { dirExists, ensureDir, fileExists } from '../../../fs/io.js';
4
4
  import { OAT_VERSION } from '../../../shared/oat-version.js';
5
- import { buildDocsCommands } from './docs-commands.js';
5
+ import { buildDocsCommands, shellQuote } from './docs-commands.js';
6
6
  import { applyMarkdownDocsPlan, planMarkdownDocs } from './markdown.js';
7
7
  import { getTemplateDir } from './resolve-options.js';
8
8
  const MKDOCS_TEMPLATE_FILES = [
9
+ { source: 'AGENTS.md', destination: 'AGENTS.md' },
10
+ ...['index.md', 'docs-toolchain.md'].map((name) => ({
11
+ source: join('docs', 'contributing', name),
12
+ destination: join('docs', 'contributing', name),
13
+ })),
9
14
  { source: '.gitignore', destination: '.gitignore' },
10
15
  { source: 'mkdocs.yml', destination: 'mkdocs.yml' },
11
16
  { source: 'package.json.template', destination: 'package.json' },
@@ -22,6 +27,15 @@ const MKDOCS_TEMPLATE_FILES = [
22
27
  },
23
28
  ];
24
29
  const FUMA_TEMPLATE_FILES = [
30
+ { source: 'AGENTS.md', destination: 'AGENTS.md' },
31
+ ...['index.md', 'docs-toolchain.md'].map((name) => ({
32
+ source: join('docs', 'contributing', name),
33
+ destination: join('docs', 'contributing', name),
34
+ })),
35
+ ...['llms.txt', 'llms-full.txt', 'llms/[...slug]'].map((route) => ({
36
+ source: join('app', route, 'route.ts'),
37
+ destination: join('app', route, 'route.ts'),
38
+ })),
25
39
  { source: '.gitignore', destination: '.gitignore' },
26
40
  { source: 'next.config.js', destination: 'next.config.js' },
27
41
  { source: 'postcss.config.mjs', destination: 'postcss.config.mjs' },
@@ -135,16 +149,6 @@ export async function resolveOatDepContext(repoRoot, assetsRoot) {
135
149
  };
136
150
  return { isOatRepo, localPackages, oatPackageVersions };
137
151
  }
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
152
  function buildFumaDevDependencies(lint, format) {
149
153
  const entries = [];
150
154
  if (lint === 'markdownlint-cli2') {
@@ -158,9 +162,17 @@ function buildFumaDevDependencies(lint, format) {
158
162
  }
159
163
  return `,\n${entries.join(',\n')}`;
160
164
  }
165
+ function jsonStringContent(value) {
166
+ return JSON.stringify(value).slice(1, -1);
167
+ }
168
+ function buildNavSyncCheckCmd(isOatRepo, targetDir) {
169
+ return isOatRepo
170
+ ? `pnpm -w run cli:source -- docs nav sync --target-dir ${shellQuote(targetDir)} --check`
171
+ : 'oat docs nav sync --target-dir . --check';
172
+ }
161
173
  function buildGenerateIndexCmd(isOatRepo, targetDir) {
162
174
  if (isOatRepo) {
163
- return `pnpm -w run cli:source -- docs generate-index --docs-dir ${targetDir}/docs --output ${targetDir}/index.md`;
175
+ return `pnpm -w run cli:source -- docs generate-index --docs-dir ${shellQuote(`${targetDir}/docs`)} --output ${shellQuote(`${targetDir}/index.md`)}`;
164
176
  }
165
177
  return 'oat docs generate-index --docs-dir docs --output index.md';
166
178
  }
@@ -221,9 +233,10 @@ async function buildPackageManagerField(options, appRoot, dependencies) {
221
233
  const version = normalizePnpmVersion(await dependencies.detectPnpmVersion());
222
234
  return `,\n "packageManager": ${JSON.stringify(`pnpm@${version}`)}`;
223
235
  }
224
- function renderTemplate(template, options, depContext, packageManagerField, nodeRequirement) {
236
+ function renderTemplate(template, options, depContext, packageManagerField, nodeRequirement, packageJson) {
225
237
  const repoName = basename(options.repoRoot);
226
238
  const commands = buildDocsCommands(options.repoShape, options.targetDir, options.appName);
239
+ const command = packageJson ? jsonStringContent : (value) => value;
227
240
  const replacements = {
228
241
  '// {{TURBOPACK_ROOT_IMPORTS}}\n': options.repoShape === 'nested-standalone'
229
242
  ? "import { dirname } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\n"
@@ -231,6 +244,7 @@ function renderTemplate(template, options, depContext, packageManagerField, node
231
244
  ' // {{TURBOPACK_ROOT_OPTION}}\n': options.repoShape === 'nested-standalone'
232
245
  ? ' turbopack: { root: dirname(fileURLToPath(import.meta.url)) },\n'
233
246
  : '',
247
+ '{{OAT_CONFIG_RELATIVE_PATH}}': `./${relative(resolve(options.repoRoot, options.targetDir), join(options.repoRoot, '.oat/config.json')).replaceAll('\\', '/')}`,
234
248
  '{{APP_NAME}}': options.appName,
235
249
  '{{NODE_REQUIREMENT}}': nodeRequirement,
236
250
  '{{PACKAGE_NAME}}': options.appName,
@@ -252,11 +266,16 @@ function renderTemplate(template, options, depContext, packageManagerField, node
252
266
  '{{DOCS_FORMAT_CHECK_SCRIPT}}': options.format === 'oxfmt'
253
267
  ? "oxfmt --check 'docs/**/*.md'"
254
268
  : "echo 'docs format check disabled'",
255
- '{{DEV_DEPENDENCIES}}': buildDevDependencies(options.lint, options.format),
269
+ '{{DEV_DEPENDENCIES}}': buildFumaDevDependencies(options.lint, options.format),
256
270
  '{{FUMA_DEV_DEPENDENCIES}}': buildFumaDevDependencies(options.lint, options.format),
257
271
  '{{REPO_NAME}}': repoName,
258
272
  '{{APP_DIR}}': options.targetDir,
259
- '{{GENERATE_INDEX_CMD}}': buildGenerateIndexCmd(depContext.isOatRepo, options.targetDir),
273
+ '{{APP_CWD}}': shellQuote(options.targetDir),
274
+ '{{LINT_STATUS}}': options.lint === 'none' ? 'disabled' : 'enabled (markdownlint-cli2)',
275
+ '{{FORMAT_STATUS}}': options.format === 'none' ? 'disabled' : 'enabled (oxfmt)',
276
+ '{{NAV_SYNC_CMD}}': command(buildNavSyncCheckCmd(depContext.isOatRepo, options.targetDir).replace(/ --check$/, '')),
277
+ '{{GENERATE_INDEX_CMD}}': command(buildGenerateIndexCmd(depContext.isOatRepo, options.targetDir)),
278
+ '{{NAV_SYNC_CHECK_CMD}}': command(buildNavSyncCheckCmd(depContext.isOatRepo, options.targetDir)),
260
279
  '{{OAT_DOCS_CONFIG_DEP}}': oatDepVersion(depContext, 'docs-config'),
261
280
  '{{OAT_DOCS_THEME_DEP}}': oatDepVersion(depContext, 'docs-theme'),
262
281
  '{{OAT_DOCS_TRANSFORMS_DEP}}': oatDepVersion(depContext, 'docs-transforms'),
@@ -285,12 +304,14 @@ export function buildDocumentationConfig(framework, targetDir) {
285
304
  return {
286
305
  root: targetDir,
287
306
  tooling: 'fumadocs',
307
+ navOwner: 'nav-sync',
288
308
  index: join(targetDir, 'index.md'),
289
309
  };
290
310
  }
291
311
  return {
292
312
  root: targetDir,
293
313
  tooling: 'mkdocs',
314
+ navOwner: 'nav-sync',
294
315
  config: join(targetDir, 'mkdocs.yml'),
295
316
  index: join(targetDir, 'mkdocs.yml'),
296
317
  };
@@ -354,7 +375,7 @@ export async function scaffoldDocsApp(options, overrides = {}) {
354
375
  const source = join(templateRoot, templateFile.source);
355
376
  const destination = join(appRoot, templateFile.destination);
356
377
  const template = await readFile(source, 'utf8');
357
- const rendered = renderTemplate(template, options, depContext, packageManagerField, nodeRequirement);
378
+ const rendered = renderTemplate(template, options, depContext, packageManagerField, nodeRequirement, templateFile.destination === 'package.json');
358
379
  await ensureDir(dirname(destination));
359
380
  await writeFile(destination, rendered, 'utf8');
360
381
  createdFiles.push(templateFile.destination);
@@ -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
- export declare function parseIndexContents(markdown: string, sourcePath?: string): DocsContentsEntry[];
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":"AAKA,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;CACd;AAED,wBAAgB,kBAAkB,CAChC,QAAQ,EAAE,MAAM,EAChB,UAAU,SAAa,GACtB,iBAAiB,EAAE,CAqCrB;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;CAClC;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,GAC1C,OAAO,CAAC;IAAE,IAAI,EAAE,MAAM,GAAG,SAAS,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAmErD;AAED,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,WAAW,CAAC,CA8DtB"}
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
- export function parseIndexContents(markdown, sourcePath = 'index.md') {
5
- const lines = markdown.split(/\r?\n/);
6
- const contentsHeadingIndex = lines.findIndex((line) => /^##\s+Contents\s*$/.test(line.trim()));
7
- if (contentsHeadingIndex < 0) {
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 sectionLines = lines.slice(contentsHeadingIndex + 1);
11
- const nextHeadingIndex = sectionLines.findIndex((line) => /^##\s+/.test(line.trim()));
12
- const contentsLines = nextHeadingIndex >= 0
13
- ? sectionLines.slice(0, nextHeadingIndex)
14
- : sectionLines;
15
- const entries = contentsLines
16
- .map((line) => line.match(/^\s*-\s+\[([^\]]+)\]\(([^)]+)\)/))
17
- .filter((match) => match !== null)
18
- .map((match) => {
19
- const [, title = '', href = ''] = match;
20
- return {
21
- title: title.trim(),
22
- href: href.trim(),
23
- };
24
- });
25
- if (entries.length === 0) {
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
- nav.push({ [entry.title]: resolvedTarget.path });
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
- nav.push({
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":"AAYA;;;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;AAyDD;;;;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;AAoLD;;;;;;;GAOG;AACH,wBAAsB,sBAAsB,CAAC,OAAO,EAAE;IACpD,QAAQ,EAAE,MAAM,CAAC;IACjB,6CAA6C;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,GAAG,OAAO,CAAC,4BAA4B,CAAC,CAkExC"}
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;AAsCD;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAEvD;AAqND;;;;;;;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,CA8ExC"}
@@ -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
- pages.push(entryPath);
42
+ if (!matcher.excludesFile(entryPath))
43
+ pages.push(entryPath);
40
44
  }
41
45
  }
42
46
  }
@@ -100,7 +104,7 @@ export function resolveFolderTitle(markdown) {
100
104
  * slugs plugin with the scaffolded `baseUrl: '/'`: group folders are dropped,
101
105
  * segments are URI-encoded, and an `index` file maps to its folder.
102
106
  */
103
- function pageUrl(pagePath) {
107
+ function pageUrl(pagePath, nonIndexUrls = new Set()) {
104
108
  const slugs = parentDir(pagePath)
105
109
  .split('/')
106
110
  .filter((segment) => segment.length > 0 && segment !== '.')
@@ -110,7 +114,12 @@ function pageUrl(pagePath) {
110
114
  if (name !== 'index') {
111
115
  slugs.push(encodeURI(name));
112
116
  }
113
- return `/${slugs.join('/')}`;
117
+ const url = `/${slugs.join('/')}`;
118
+ // core16.10.2 defers index slugs and appends /index when a non-index
119
+ // page has already claimed the folder URL.
120
+ return name === 'index' && nonIndexUrls.has(url)
121
+ ? `${url.replace(/\/$/, '')}/index`
122
+ : url;
114
123
  }
115
124
  /** Whether the Fumadocs page-tree builder reads `item` as a directive, not a path. */
116
125
  function isFumadocsDirective(item) {
@@ -135,9 +144,11 @@ function hrefFragment(href) {
135
144
  /**
136
145
  * Build one folder's `pages` from its index.md Contents map.
137
146
  *
138
- * A page in this folder or an immediate subfolder is referenced by name. Any
139
- * other target becomes a Fumadocs link entry, so no page is claimed by two
140
- * folders. There is never a `"..."` rest entry: pages the Contents map does
147
+ * A page in this folder or an immediate subfolder is referenced by name.
148
+ * Local same-name page/folder collisions use a precise source-file item, as
149
+ * does `../contributing.md` from its own contributing/ map: pinned core16.10.2
150
+ * selects the folder first for an extensionless reference. Other non-local
151
+ * targets become link entries. There is never a `"..."` rest entry: pages the Contents map does
141
152
  * not list stay out of the tree and are reported instead.
142
153
  */
143
154
  /**
@@ -150,13 +161,15 @@ function hrefFragment(href) {
150
161
  * only if `pages` lists it.
151
162
  */
152
163
  function hasImplicitIndex(relativeDir, existing) {
153
- if (relativeDir === '.' || existing?.root === true) {
164
+ if (relativeDir === '.' ||
165
+ existing?.root === true ||
166
+ (typeof existing?.root === 'string' && existing.root.trim().length > 0)) {
154
167
  return false;
155
168
  }
156
169
  const pagesIndex = existing?.pagesIndex;
157
170
  return pagesIndex === undefined || pagesIndex === 'index';
158
171
  }
159
- async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex) {
172
+ async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex, matcher, inventory, nonIndexUrls) {
160
173
  const indexPath = join(docsRoot, relativeDir, 'index.md');
161
174
  const entries = parseIndexContents(markdown, indexPath);
162
175
  // A folder the loader gives no implicit index (the docs root, a `root: true`
@@ -164,11 +177,23 @@ async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex)
164
177
  // `index` would demote the folder index to an ordinary child, so it is left
165
178
  // implicit.
166
179
  const pages = implicitIndex ? [] : ['index'];
180
+ let currentGroup;
167
181
  for (const entry of entries) {
168
- const target = await resolveEntryTarget(docsRoot, join(docsRoot, relativeDir), relativeDir, entry, PAGE_EXTENSIONS);
182
+ const target = await resolveEntryTarget(docsRoot, join(docsRoot, relativeDir), relativeDir, entry, PAGE_EXTENSIONS, matcher);
183
+ if (target.kind === 'excluded')
184
+ continue;
169
185
  const targetDir = parentDir(target.path);
170
186
  let item;
171
- if (target.kind === 'section' && targetDir === relativeDir) {
187
+ if (target.kind === 'page' &&
188
+ (targetDir === relativeDir ||
189
+ target.path.slice(0, -extname(target.path).length) === relativeDir) &&
190
+ inventory.directories.includes(target.path.slice(0, -extname(target.path).length))) {
191
+ // An extensionless reference selects the same-name folder first.
192
+ // Keep source identity only in local/owning placement. A nonlocal map
193
+ // needs an authored URL link: core16.10.2 globally dedups source refs.
194
+ item = toFumadocsPageItem(posix.relative(relativeDir, target.path));
195
+ }
196
+ else if (target.kind === 'section' && targetDir === relativeDir) {
172
197
  continue;
173
198
  }
174
199
  else if (target.kind === 'section' &&
@@ -179,9 +204,15 @@ async function buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex)
179
204
  item = toFumadocsPageItem(basename(target.path, extname(target.path)));
180
205
  }
181
206
  else {
182
- item = `[${entry.title}](${pageUrl(target.path)}${hrefFragment(entry.href)})`;
207
+ item = `[${entry.title}](${pageUrl(target.path, nonIndexUrls)}${hrefFragment(entry.href)})`;
183
208
  }
184
209
  if (!pages.includes(item)) {
210
+ if (entry.group !== currentGroup) {
211
+ currentGroup = entry.group;
212
+ // A leading space prevents the loader treating a literal [label] as an icon.
213
+ const title = currentGroup.title;
214
+ pages.push(`---${title.startsWith('[') ? ' ' : ''}${title}---`);
215
+ }
185
216
  pages.push(item);
186
217
  }
187
218
  }
@@ -213,6 +244,7 @@ function mergeMeta(existing, meta) {
213
244
  };
214
245
  }
215
246
  function collectUnlisted(inventory, metas, implicitIndexDirs) {
247
+ const pagePaths = new Set(inventory.pages);
216
248
  const reachableDirs = new Set();
217
249
  const reachablePages = new Set();
218
250
  const pagesByFlattenPath = new Map(inventory.pages.map((page) => [
@@ -242,7 +274,9 @@ function collectUnlisted(inventory, metas, implicitIndexDirs) {
242
274
  visit(itemPath);
243
275
  continue;
244
276
  }
245
- const page = pagesByFlattenPath.get(itemPath);
277
+ const page = pagePaths.has(itemPath)
278
+ ? itemPath
279
+ : pagesByFlattenPath.get(itemPath);
246
280
  if (page) {
247
281
  reachablePages.add(page);
248
282
  }
@@ -274,13 +308,18 @@ function collectUnlisted(inventory, metas, implicitIndexDirs) {
274
308
  */
275
309
  export async function syncFumadocsNavigation(options) {
276
310
  const { docsRoot } = options;
277
- const inventory = await inventoryDocs(docsRoot);
311
+ const matcher = createExclusionMatcher(options.nonReaderPaths ?? []);
312
+ const inventory = await inventoryDocs(docsRoot, matcher);
313
+ const nonIndexUrls = new Set(inventory.pages
314
+ .filter((page) => basename(page, extname(page)) !== 'index')
315
+ .map((page) => pageUrl(page)));
278
316
  const metas = new Map();
279
317
  const existingMetas = new Map();
280
318
  const implicitIndexDirs = new Set();
281
319
  for (const relativeDir of inventory.directories) {
282
320
  const indexPath = join(docsRoot, relativeDir, 'index.md');
283
- if (!(await fileExists(indexPath))) {
321
+ if (matcher.excludesFile(posix.join(relativeDir, 'index.md')) ||
322
+ !(await fileExists(indexPath))) {
284
323
  continue;
285
324
  }
286
325
  // Index handling depends on the effective metadata, which keeps the
@@ -295,7 +334,7 @@ export async function syncFumadocsNavigation(options) {
295
334
  const title = resolveFolderTitle(markdown);
296
335
  metas.set(relativeDir, {
297
336
  ...(title === undefined ? {} : { title }),
298
- pages: await buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex),
337
+ pages: await buildFolderPages(docsRoot, relativeDir, markdown, implicitIndex, matcher, inventory, nonIndexUrls),
299
338
  });
300
339
  }
301
340
  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;CACjB;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,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;AAqDD,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA0CnC;AAmJD,wBAAgB,wBAAwB,CACtC,SAAS,GAAE,OAAO,CAAC,uBAAuB,CAAM,GAC/C,OAAO,CA4BT"}
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({ docsRoot });
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"}