@open-agent-toolkit/cli 0.3.32 → 0.3.34

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 (47) hide show
  1. package/assets/bundle-metadata.json +1 -1
  2. package/assets/docs/contributing/documentation.md +1 -0
  3. package/assets/docs/docs-tooling/check.md +35 -11
  4. package/assets/docs/docs-tooling/commands.md +20 -2
  5. package/assets/docs/reference/cli-reference.md +2 -2
  6. package/assets/docs/reference/config-and-local-state.md +1 -1
  7. package/assets/docs/reference/configuration.md +1 -1
  8. package/assets/docs/reference/docs-index-contract.md +3 -2
  9. package/assets/docs/reference/oat-directory-structure.md +1 -1
  10. package/assets/public-package-versions.json +4 -4
  11. package/assets/skills/oat-docs-analyze/SKILL.md +35 -11
  12. package/assets/skills/oat-docs-analyze/references/analysis-artifact-template.md +2 -2
  13. package/assets/skills/oat-docs-analyze/references/quality-checklist.md +3 -1
  14. package/assets/skills/oat-docs-analyze/scripts/package-drift.mjs +303 -30
  15. package/assets/skills/oat-docs-apply/SKILL.md +3 -1
  16. package/assets/skills/oat-docs-authoring/SKILL.md +1 -1
  17. package/assets/skills/oat-docs-authoring/references/oat-fumadocs-contract.md +6 -1
  18. package/dist/commands/config/index.d.ts.map +1 -1
  19. package/dist/commands/config/index.js +42 -9
  20. package/dist/commands/docs/check/check.d.ts.map +1 -1
  21. package/dist/commands/docs/check/check.js +1 -0
  22. package/dist/commands/docs/check/checker.d.ts +2 -0
  23. package/dist/commands/docs/check/checker.d.ts.map +1 -1
  24. package/dist/commands/docs/check/checker.js +6 -1
  25. package/dist/commands/docs/check/commands.d.ts.map +1 -1
  26. package/dist/commands/docs/check/commands.js +74 -30
  27. package/dist/commands/docs/check/files.d.ts +2 -0
  28. package/dist/commands/docs/check/files.d.ts.map +1 -1
  29. package/dist/commands/docs/check/files.js +16 -0
  30. package/dist/commands/docs/check/navigation.d.ts +11 -1
  31. package/dist/commands/docs/check/navigation.d.ts.map +1 -1
  32. package/dist/commands/docs/check/navigation.js +40 -69
  33. package/dist/commands/docs/check/reader-rules.d.ts.map +1 -1
  34. package/dist/commands/docs/check/reader-rules.js +12 -9
  35. package/dist/commands/docs/nav/contents.d.ts +50 -2
  36. package/dist/commands/docs/nav/contents.d.ts.map +1 -1
  37. package/dist/commands/docs/nav/contents.js +137 -14
  38. package/dist/commands/docs/nav/fumadocs.d.ts +29 -0
  39. package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
  40. package/dist/commands/docs/nav/fumadocs.js +16 -41
  41. package/dist/commands/docs/nav/membership.d.ts +37 -0
  42. package/dist/commands/docs/nav/membership.d.ts.map +1 -0
  43. package/dist/commands/docs/nav/membership.js +474 -0
  44. package/dist/commands/docs/nav/sync.d.ts +26 -2
  45. package/dist/commands/docs/nav/sync.d.ts.map +1 -1
  46. package/dist/commands/docs/nav/sync.js +160 -43
  47. package/package.json +3 -3
@@ -1,5 +1,5 @@
1
- import { readFile, writeFile } from 'node:fs/promises';
2
- import { join, resolve } from 'node:path';
1
+ import { readFile, realpath, writeFile } from 'node:fs/promises';
2
+ import { join, relative, resolve } from 'node:path';
3
3
  import { buildCommandContext, } from '../../../app/command-context.js';
4
4
  import { readGlobalOptions } from '../../shared/shared.utils.js';
5
5
  import { readOatConfig } from '../../../config/oat-config.js';
@@ -9,6 +9,7 @@ import { Command, Option } from 'commander';
9
9
  import YAML from 'yaml';
10
10
  import { buildDocsNavTree } from './contents.js';
11
11
  import { syncFumadocsNavigation } from './fumadocs.js';
12
+ import { compareFumadocsMembership, compareMkDocsMembership, } from './membership.js';
12
13
  const MKDOCS_MARKERS = ['mkdocs.yml'];
13
14
  const FUMADOCS_MARKERS = [
14
15
  'source.config.ts',
@@ -44,39 +45,64 @@ export async function detectDocsNavFramework(appRoot, configuredTooling) {
44
45
  throw new Error(`Could not detect the docs framework in ${appRoot}: expected mkdocs.yml (MkDocs) ` +
45
46
  'or source.config.ts (Fumadocs).');
46
47
  }
47
- async function resolveConfiguredTooling(cwd, appRoot) {
48
- try {
49
- const repoRoot = await resolveProjectRoot(cwd);
50
- const config = await readOatConfig(repoRoot);
51
- const root = config.documentation?.root?.trim();
52
- if (!root || resolve(repoRoot, root) !== appRoot) {
53
- return null;
54
- }
55
- return config.documentation?.tooling?.trim() || null;
56
- }
57
- catch {
58
- return null;
59
- }
60
- }
61
- async function resolveNonReaderPaths(cwd, appRoot) {
48
+ async function resolveConfiguredDocumentation(cwd) {
62
49
  let repoRoot;
63
50
  try {
64
51
  repoRoot = await resolveProjectRoot(cwd);
65
52
  }
66
53
  catch {
67
- return [];
54
+ return null;
68
55
  } // 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
- : [];
56
+ const documentation = (await readOatConfig(repoRoot)).documentation;
57
+ const configuredRoot = documentation?.root?.trim();
58
+ if (!configuredRoot)
59
+ return null;
60
+ return {
61
+ root: resolve(repoRoot, configuredRoot),
62
+ configuredRoot,
63
+ tooling: documentation?.tooling?.trim() || null,
64
+ navOwner: documentation?.navOwner,
65
+ nonReaderPaths: documentation?.nonReaderPaths ?? [],
66
+ };
67
+ }
68
+ /**
69
+ * Read-only comparison for `navOwner: hand-authored`: the same
70
+ * membership-and-order semantics as the `oat docs check` navigation rule,
71
+ * never strict generator output.
72
+ */
73
+ export async function checkHandAuthoredNavigation(options) {
74
+ const docsRoot = join(options.appRoot, 'docs');
75
+ const framework = await detectDocsNavFramework(options.appRoot, options.configuredTooling);
76
+ if (framework === 'mkdocs')
77
+ return {
78
+ framework,
79
+ appRoot: options.appRoot,
80
+ docsRoot,
81
+ issues: await compareMkDocsMembership({
82
+ appRoot: options.appRoot,
83
+ docsRoot,
84
+ nonReaderPaths: options.nonReaderPaths,
85
+ }),
86
+ unlisted: [],
87
+ };
88
+ const generated = await syncFumadocsNavigation({
89
+ docsRoot,
90
+ check: true,
91
+ nonReaderPaths: options.nonReaderPaths,
92
+ });
93
+ return {
94
+ framework,
95
+ appRoot: options.appRoot,
96
+ docsRoot,
97
+ issues: await compareFumadocsMembership(docsRoot, generated.metaFiles),
98
+ unlisted: generated.unlisted,
99
+ };
74
100
  }
75
101
  const DEFAULT_DEPENDENCIES = {
76
102
  buildCommandContext,
77
- resolveConfiguredTooling,
78
- resolveNonReaderPaths,
103
+ resolveConfiguredDocumentation,
79
104
  syncDocsNavigation,
105
+ checkHandAuthoredNavigation,
80
106
  };
81
107
  function replaceTopLevelYamlSection(source, key, replacement) {
82
108
  const lines = source.split(/\r?\n/);
@@ -135,7 +161,7 @@ export async function syncDocsNavigation(options) {
135
161
  stale: nextMkdocsSource === mkdocsSource ? [] : ['mkdocs.yml'],
136
162
  };
137
163
  }
138
- function reportFumadocsResult(context, targetDir, result) {
164
+ function reportFumadocsResult(context, targetDir, result, warnings) {
139
165
  if (context.json) {
140
166
  context.logger.json({
141
167
  status: 'ok',
@@ -145,6 +171,7 @@ function reportFumadocsResult(context, targetDir, result) {
145
171
  metaFiles: result.metaFiles,
146
172
  written: result.written,
147
173
  unlisted: result.unlisted,
174
+ ...(warnings.length ? { warnings } : {}),
148
175
  });
149
176
  return;
150
177
  }
@@ -167,15 +194,18 @@ function reportFumadocsResult(context, targetDir, result) {
167
194
  context.logger.warn(` ${path}`);
168
195
  }
169
196
  }
197
+ for (const warning of warnings)
198
+ context.logger.warn(warning);
170
199
  }
171
200
  /**
172
201
  * `--check`: report drift without writing. Fails (exit 1) when any generated
173
- * navigation file would change or, for Fumadocs, any page or folder is
174
- * unlisted, since strict navigation hides it from the sidebar.
202
+ * navigation file is missing or would change or, for Fumadocs, any page or
203
+ * folder is unlisted, since strict navigation hides it from the sidebar.
175
204
  */
176
- function reportCheckResult(context, targetDir, result) {
205
+ function reportCheckResult(context, targetDir, result, missing, warnings) {
177
206
  const unlisted = result.framework === 'fumadocs' ? result.unlisted : [];
178
207
  const current = result.stale.length === 0 && unlisted.length === 0;
208
+ const differing = result.stale.filter((path) => !missing.includes(path));
179
209
  if (context.json) {
180
210
  context.logger.json({
181
211
  status: current ? 'ok' : 'drift',
@@ -184,20 +214,28 @@ function reportCheckResult(context, targetDir, result) {
184
214
  appRoot: result.appRoot,
185
215
  docsRoot: result.docsRoot,
186
216
  stale: result.stale,
217
+ missing,
187
218
  unlisted,
219
+ ...(warnings.length ? { warnings } : {}),
188
220
  });
189
221
  return current;
190
222
  }
223
+ for (const warning of warnings)
224
+ context.logger.warn(warning);
191
225
  if (current) {
192
226
  context.logger.info(`Docs navigation is up to date in ${targetDir}`);
193
227
  return current;
194
228
  }
195
229
  context.logger.error(`Docs navigation is out of date in ${targetDir}`);
196
- if (result.stale.length > 0) {
197
- context.logger.error(` ${result.stale.length} generated file(s) differ from the index.md Contents maps:`);
198
- for (const path of result.stale) {
230
+ if (missing.length > 0) {
231
+ context.logger.error(` ${missing.length} generated file(s) are missing:`);
232
+ for (const path of missing)
233
+ context.logger.error(` ${path}`);
234
+ }
235
+ if (differing.length > 0) {
236
+ context.logger.error(` ${differing.length} generated file(s) differ from the index.md Contents maps:`);
237
+ for (const path of differing)
199
238
  context.logger.error(` ${path}`);
200
- }
201
239
  }
202
240
  if (unlisted.length > 0) {
203
241
  context.logger.error(` ${unlisted.length} path(s) are not listed in any index.md Contents map ` +
@@ -205,26 +243,102 @@ function reportCheckResult(context, targetDir, result) {
205
243
  for (const path of unlisted) {
206
244
  context.logger.error(` ${path}`);
207
245
  }
246
+ context.logger.error(` List every page in its directory's ## Contents, then run \`oat docs nav sync --target-dir ${targetDir}\`.`);
247
+ }
248
+ else {
249
+ context.logger.error(` Run \`oat docs nav sync --target-dir ${targetDir}\` to regenerate the navigation.`);
250
+ }
251
+ return current;
252
+ }
253
+ function reportHandAuthoredCheck(context, targetDir, result) {
254
+ const current = result.issues.length === 0 && result.unlisted.length === 0;
255
+ if (context.json) {
256
+ context.logger.json({
257
+ status: current ? 'ok' : 'drift',
258
+ check: true,
259
+ navOwner: 'hand-authored',
260
+ framework: result.framework,
261
+ appRoot: result.appRoot,
262
+ docsRoot: result.docsRoot,
263
+ stale: [...new Set(result.issues.map((issue) => issue.path))].sort(),
264
+ missing: [],
265
+ unlisted: result.unlisted,
266
+ issues: result.issues,
267
+ });
268
+ return current;
269
+ }
270
+ if (current) {
271
+ context.logger.info(`Hand-authored docs navigation matches the index.md Contents maps in ${targetDir} (membership and order).`);
272
+ return current;
208
273
  }
209
- context.logger.error(` List every page in its directory's ## Contents, then run \`oat docs nav sync --target-dir ${targetDir}\`.`);
274
+ context.logger.error(`Hand-authored docs navigation differs from the index.md Contents maps in ${targetDir}:`);
275
+ for (const issue of result.issues)
276
+ context.logger.error(` ${issue.path}${issue.line === undefined ? '' : `:${issue.line}`} ${issue.message}`);
277
+ if (result.unlisted.length > 0) {
278
+ context.logger.error(` ${result.unlisted.length} path(s) are not listed in any index.md Contents map:`);
279
+ for (const path of result.unlisted)
280
+ context.logger.error(` ${path}`);
281
+ }
282
+ context.logger.error(' Edit the hand-authored navigation or the Contents maps; nav sync does not regenerate hand-authored navigation.');
210
283
  return current;
211
284
  }
285
+ const MARKDOWN_NOT_APPLICABLE = 'oat docs nav sync is not applicable to Markdown docs (documentation.tooling: markdown): the index.md Contents maps are the navigation and nothing is generated. Use oat docs check to validate them.';
212
286
  async function runDocsNavSyncCommand(context, options, dependencies) {
213
287
  try {
214
- const targetDir = options.targetDir ?? '.';
288
+ const configured = await dependencies.resolveConfiguredDocumentation(context.cwd);
289
+ // Like `oat docs check`, an omitted target is the configured root.
290
+ const targetDir = options.targetDir ??
291
+ (configured ? relative(context.cwd, configured.root) || '.' : '.');
215
292
  const appRoot = resolve(context.cwd, targetDir);
216
- const result = await dependencies.syncDocsNavigation({
293
+ // Compare filesystem identity so a symlinked alias of the configured
294
+ // app keeps its ownership and exclusions.
295
+ const canonical = async (path) => realpath(path).catch(() => resolve(path));
296
+ const documentation = configured &&
297
+ (await canonical(configured.root)) === (await canonical(appRoot))
298
+ ? configured
299
+ : null;
300
+ if (documentation?.tooling?.toLowerCase() === 'markdown')
301
+ throw new Error(MARKDOWN_NOT_APPLICABLE);
302
+ const navOwner = documentation?.navOwner;
303
+ const syncOptions = {
217
304
  appRoot,
218
- configuredTooling: await dependencies.resolveConfiguredTooling(context.cwd, appRoot),
305
+ configuredTooling: documentation?.tooling ?? null,
219
306
  check: options.check === true,
220
- nonReaderPaths: await dependencies.resolveNonReaderPaths?.(context.cwd, appRoot),
221
- });
307
+ nonReaderPaths: documentation?.nonReaderPaths ?? [],
308
+ };
309
+ if (navOwner === 'hand-authored') {
310
+ if (!options.check)
311
+ throw new Error(`documentation.navOwner is hand-authored, so oat docs nav sync will not overwrite the navigation in ${targetDir}. ` +
312
+ 'Edit meta.json (Fumadocs) or the mkdocs.yml nav (MkDocs) directly and run oat docs check, ' +
313
+ 'or compare it with `oat docs nav sync --check`. To hand navigation to nav sync, first run ' +
314
+ '`oat config set documentation.navOwner nav-sync`.');
315
+ const result = await dependencies.checkHandAuthoredNavigation(syncOptions);
316
+ process.exitCode = reportHandAuthoredCheck(context, targetDir, result)
317
+ ? 0
318
+ : 1;
319
+ return;
320
+ }
321
+ const warnings = navOwner === undefined
322
+ ? [
323
+ options.check
324
+ ? `No documentation.navOwner is declared for ${targetDir}, so this check compares with strict nav sync output. Declare ownership with \`oat config set documentation.navOwner nav-sync\`, or \`hand-authored\` for a membership-and-order comparison.`
325
+ : `No documentation.navOwner is declared for ${targetDir}, so nav sync regenerated the navigation as its owner. Declare ownership with \`oat config set documentation.navOwner nav-sync\`, or \`hand-authored\` to protect hand-written navigation from regeneration.`,
326
+ ]
327
+ : [];
328
+ const result = await dependencies.syncDocsNavigation(syncOptions);
222
329
  if (options.check) {
223
- process.exitCode = reportCheckResult(context, targetDir, result) ? 0 : 1;
330
+ const missing = [];
331
+ if (result.framework === 'fumadocs')
332
+ for (const path of result.stale)
333
+ if (!(await fileExists(join(result.docsRoot, path))))
334
+ missing.push(path);
335
+ process.exitCode = reportCheckResult(context, targetDir, result, missing, warnings)
336
+ ? 0
337
+ : 1;
224
338
  return;
225
339
  }
226
340
  if (result.framework === 'fumadocs') {
227
- reportFumadocsResult(context, targetDir, result);
341
+ reportFumadocsResult(context, targetDir, result, warnings);
228
342
  }
229
343
  else if (context.json) {
230
344
  context.logger.json({
@@ -233,12 +347,15 @@ async function runDocsNavSyncCommand(context, options, dependencies) {
233
347
  docsRoot: result.docsRoot,
234
348
  mkdocsPath: result.mkdocsPath,
235
349
  nav: result.nav,
350
+ ...(warnings.length ? { warnings } : {}),
236
351
  });
237
352
  }
238
353
  else {
239
354
  context.logger.info(`Synced docs navigation in ${targetDir}`);
240
355
  context.logger.info(` MkDocs config: ${result.mkdocsPath}`);
241
356
  context.logger.info(` Docs root: ${result.docsRoot}`);
357
+ for (const warning of warnings)
358
+ context.logger.warn(warning);
242
359
  }
243
360
  process.exitCode = 0;
244
361
  }
@@ -260,8 +377,8 @@ export function createDocsNavSyncCommand(overrides = {}) {
260
377
  };
261
378
  return new Command('sync')
262
379
  .description('Regenerate docs navigation from index.md contents: mkdocs.yml nav (MkDocs) or strict meta.json files (Fumadocs)')
263
- .addOption(new Option('--target-dir <path>', 'Docs app directory containing mkdocs.yml or source.config.ts'))
264
- .addOption(new Option('--check', 'Write nothing; exit 1 if any navigation file is stale or any page is unlisted'))
380
+ .addOption(new Option('--target-dir <path>', 'Docs app directory containing mkdocs.yml or source.config.ts (default: documentation.root, else the current directory)'))
381
+ .addOption(new Option('--check', 'Write nothing; exit 1 if any navigation file is missing or stale or any page is unlisted (hand-authored navigation: membership and order only)'))
265
382
  .action(async (options, command) => {
266
383
  const context = dependencies.buildCommandContext(readGlobalOptions(command));
267
384
  await runDocsNavSyncCommand(context, options, dependencies);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-agent-toolkit/cli",
3
- "version": "0.3.32",
3
+ "version": "0.3.34",
4
4
  "private": false,
5
5
  "description": "Open Agent Toolkit CLI",
6
6
  "homepage": "https://github.com/voxmedia/open-agent-toolkit/tree/main/packages/cli",
@@ -42,8 +42,8 @@
42
42
  "unified": "11.0.5",
43
43
  "yaml": "2.8.2",
44
44
  "zod": "^3.25.76",
45
- "@open-agent-toolkit/docs-transforms": "0.3.32",
46
- "@open-agent-toolkit/control-plane": "0.3.32"
45
+ "@open-agent-toolkit/control-plane": "0.3.34",
46
+ "@open-agent-toolkit/docs-transforms": "0.3.34"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/jsdom": "28.0.3",