@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.
Files changed (48) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/docs-tooling/add-docs-to-a-repo.md +14 -6
  3. package/assets/docs/docs-tooling/commands.md +12 -9
  4. package/assets/docs/docs-tooling/workflows.md +53 -8
  5. package/assets/docs/reference/cli-reference.md +1 -1
  6. package/assets/docs/reference/configuration.md +3 -0
  7. package/assets/docs/reference/docs-index-contract.md +19 -6
  8. package/assets/docs/reference/oat-directory-structure.md +13 -0
  9. package/assets/public-package-versions.json +4 -4
  10. package/assets/skills/oat-docs-analyze/SKILL.md +106 -6
  11. package/assets/skills/oat-docs-analyze/references/analysis-artifact-template.md +42 -2
  12. package/assets/skills/oat-docs-analyze/scripts/package-drift.mjs +129 -0
  13. package/assets/skills/oat-docs-apply/SKILL.md +131 -12
  14. package/assets/skills/oat-docs-apply/references/apply-plan-template.md +58 -14
  15. package/assets/skills/oat-docs-bootstrap/SKILL.md +33 -8
  16. package/assets/templates/docs-app-fuma/package.json.template +1 -1
  17. package/assets/templates/docs-app-fuma/source.config.ts +6 -1
  18. package/assets/templates/docs-app-mkdocs/package.json.template +2 -2
  19. package/dist/commands/config/index.d.ts.map +1 -1
  20. package/dist/commands/config/index.js +67 -9
  21. package/dist/commands/docs/index-generate/generator.d.ts +4 -34
  22. package/dist/commands/docs/index-generate/generator.d.ts.map +1 -1
  23. package/dist/commands/docs/index-generate/generator.js +51 -157
  24. package/dist/commands/docs/index-generate/index.d.ts +1 -1
  25. package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
  26. package/dist/commands/docs/index-generate/index.js +26 -8
  27. package/dist/commands/docs/init/index.d.ts.map +1 -1
  28. package/dist/commands/docs/init/index.js +21 -1
  29. package/dist/commands/docs/init/scaffold.d.ts.map +1 -1
  30. package/dist/commands/docs/init/scaffold.js +20 -13
  31. package/dist/commands/docs/nav/contents.d.ts +13 -3
  32. package/dist/commands/docs/nav/contents.d.ts.map +1 -1
  33. package/dist/commands/docs/nav/contents.js +85 -24
  34. package/dist/commands/docs/nav/fumadocs.d.ts +1 -0
  35. package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
  36. package/dist/commands/docs/nav/fumadocs.js +26 -9
  37. package/dist/commands/docs/nav/sync.d.ts +2 -0
  38. package/dist/commands/docs/nav/sync.d.ts.map +1 -1
  39. package/dist/commands/docs/nav/sync.js +21 -1
  40. package/dist/commands/docs/shared/exclusions.d.ts +37 -0
  41. package/dist/commands/docs/shared/exclusions.d.ts.map +1 -0
  42. package/dist/commands/docs/shared/exclusions.js +161 -0
  43. package/dist/config/oat-config.d.ts +18 -0
  44. package/dist/config/oat-config.d.ts.map +1 -1
  45. package/dist/config/oat-config.js +64 -3
  46. package/dist/config/resolve.d.ts.map +1 -1
  47. package/dist/config/resolve.js +5 -1
  48. 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}}': buildDevDependencies(options.lint, options.format),
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
- 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;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
- pages.push(entryPath);
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 === '.' || existing?.root === true) {
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 inventory = await inventoryDocs(docsRoot);
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 (!(await fileExists(indexPath))) {
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;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"}