@open-agent-toolkit/cli 0.3.31 → 0.3.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/docs-tooling/check.md +54 -30
- package/assets/docs/docs-tooling/commands.md +21 -2
- package/assets/docs/docs-tooling/workflows.md +14 -6
- package/assets/docs/reference/cli-reference.md +2 -2
- package/assets/docs/reference/config-and-local-state.md +1 -1
- package/assets/docs/reference/configuration.md +2 -2
- package/assets/docs/reference/docs-index-contract.md +3 -2
- package/assets/docs/reference/oat-directory-structure.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-agent-instructions-analyze/SKILL.md +2 -2
- package/assets/skills/oat-docs-analyze/SKILL.md +63 -25
- package/assets/skills/oat-docs-analyze/references/analysis-artifact-template.md +9 -6
- package/assets/skills/oat-docs-analyze/references/quality-checklist.md +2 -2
- package/assets/skills/oat-docs-analyze/scripts/package-drift.mjs +358 -40
- package/assets/skills/oat-docs-apply/SKILL.md +22 -12
- package/assets/skills/oat-docs-apply/references/apply-plan-template.md +3 -2
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +47 -9
- package/dist/commands/docs/check/check.d.ts.map +1 -1
- package/dist/commands/docs/check/check.js +1 -0
- package/dist/commands/docs/check/checker.d.ts +2 -0
- package/dist/commands/docs/check/checker.d.ts.map +1 -1
- package/dist/commands/docs/check/checker.js +2 -0
- package/dist/commands/docs/check/commands.d.ts.map +1 -1
- package/dist/commands/docs/check/commands.js +74 -30
- package/dist/commands/docs/check/files.d.ts +2 -0
- package/dist/commands/docs/check/files.d.ts.map +1 -1
- package/dist/commands/docs/check/files.js +16 -0
- package/dist/commands/docs/check/navigation.d.ts +7 -1
- package/dist/commands/docs/check/navigation.d.ts.map +1 -1
- package/dist/commands/docs/check/navigation.js +19 -68
- package/dist/commands/docs/check/reader-rules.d.ts.map +1 -1
- package/dist/commands/docs/check/reader-rules.js +8 -3
- package/dist/commands/docs/index-generate/generator.d.ts +9 -1
- package/dist/commands/docs/index-generate/generator.d.ts.map +1 -1
- package/dist/commands/docs/index-generate/generator.js +17 -6
- package/dist/commands/docs/index-generate/index.d.ts +1 -1
- package/dist/commands/docs/index-generate/index.d.ts.map +1 -1
- package/dist/commands/docs/index-generate/index.js +24 -12
- package/dist/commands/docs/init/content.d.ts +2 -3
- package/dist/commands/docs/init/content.d.ts.map +1 -1
- package/dist/commands/docs/init/content.js +3 -10
- package/dist/commands/docs/nav/fumadocs.d.ts +29 -0
- package/dist/commands/docs/nav/fumadocs.d.ts.map +1 -1
- package/dist/commands/docs/nav/fumadocs.js +8 -5
- package/dist/commands/docs/nav/membership.d.ts +37 -0
- package/dist/commands/docs/nav/membership.d.ts.map +1 -0
- package/dist/commands/docs/nav/membership.js +475 -0
- package/dist/commands/docs/nav/sync.d.ts +26 -2
- package/dist/commands/docs/nav/sync.d.ts.map +1 -1
- package/dist/commands/docs/nav/sync.js +160 -43
- package/dist/config/documentation-ia.d.ts +19 -0
- package/dist/config/documentation-ia.d.ts.map +1 -0
- package/dist/config/documentation-ia.js +42 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +5 -1
- package/package.json +3 -3
|
@@ -12,6 +12,7 @@ import { normalizeDispatchMatrix, validateDispatchRouteTarget, walkDispatchMatri
|
|
|
12
12
|
import { formatDispatchNotices, terminalReviewerNoticesForMatrix, } from '../../config/dispatch-notices.js';
|
|
13
13
|
import { dispatchPolicyModeDescription, dispatchPolicyPolicyDescription, managedDispatchPolicyValueList, } from '../../config/dispatch-policy-options.js';
|
|
14
14
|
import { normalizeDocumentationCheck } from '../../config/documentation-check.js';
|
|
15
|
+
import { DocumentationIaPresetError } from '../../config/documentation-ia.js';
|
|
15
16
|
import { parseJsonConfig } from '../../config/json.js';
|
|
16
17
|
import { DEFAULT_INSTRUCTION_SYNC_STRATEGY, INSTRUCTION_SYNC_STRATEGIES, VALID_DISPATCH_POLICY_MODES, VALID_MANAGED_DISPATCH_POLICIES, MAX_GATE_TIMEOUT_MS, MIN_GATE_TIMEOUT_MS, isValidGateTimeoutMs, isValidProjectLogDigestBound, normalizeWorkflowPostImplementSequence, normalizeDocumentationGuidance, normalizeDocumentationIa, normalizeDocumentationNavOwner, readOatConfig, readOatConfigForDefaultScopeRepair, readOatConfigForDocumentationExcludesRepair, readOatConfigForInstructionPointerExcludesRepair, readOatConfigForInstructionSyncStrategyRepair, readOatConfigWithWarnings, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, VALID_POST_IMPLEMENT_LEGACY_SEQUENCES, } from '../../config/oat-config.js';
|
|
17
18
|
import { getOwnKey, setOwnKey } from '../../config/own-keys.js';
|
|
@@ -76,6 +77,7 @@ const KEY_ORDER = [
|
|
|
76
77
|
'documentation.root',
|
|
77
78
|
'documentation.tooling',
|
|
78
79
|
'documentation.config',
|
|
80
|
+
'documentation.index',
|
|
79
81
|
'documentation.excludes',
|
|
80
82
|
'documentation.requireForProjectCompletion',
|
|
81
83
|
'explainers.defaults.style',
|
|
@@ -321,6 +323,17 @@ const CONFIG_CATALOG = [
|
|
|
321
323
|
owningCommand: 'oat config set documentation.config <value>',
|
|
322
324
|
description: 'Repository-relative path to the primary documentation tool config file.',
|
|
323
325
|
},
|
|
326
|
+
{
|
|
327
|
+
key: 'documentation.index',
|
|
328
|
+
group: 'Shared Repo (.oat/config.json)',
|
|
329
|
+
file: '.oat/config.json',
|
|
330
|
+
scope: 'shared repo',
|
|
331
|
+
type: 'string',
|
|
332
|
+
defaultValue: 'unset',
|
|
333
|
+
mutability: 'read/write',
|
|
334
|
+
owningCommand: 'oat config set documentation.index <value>',
|
|
335
|
+
description: 'Repository-relative docs index path. Markdown: the authored root index.md. Fumadocs: the generated app-root manifest; MkDocs: mkdocs.yml. oat docs init writes it and oat docs generate-index updates the Fumadocs transition.',
|
|
336
|
+
},
|
|
324
337
|
{
|
|
325
338
|
key: 'documentation.nonReaderPaths',
|
|
326
339
|
group: 'Shared Repo (.oat/config.json)',
|
|
@@ -329,7 +342,7 @@ const CONFIG_CATALOG = [
|
|
|
329
342
|
type: 'string[]',
|
|
330
343
|
defaultValue: 'unset',
|
|
331
344
|
mutability: 'read/write',
|
|
332
|
-
owningCommand: 'oat config set documentation.nonReaderPaths <glob[,glob...]>',
|
|
345
|
+
owningCommand: 'oat config set documentation.nonReaderPaths <glob[,glob...]|JSON-array>',
|
|
333
346
|
description: 'Content-root-relative globs excluded from reader navigation, manifest and supported framework rendering. Empty clears the key.',
|
|
334
347
|
},
|
|
335
348
|
{
|
|
@@ -340,7 +353,7 @@ const CONFIG_CATALOG = [
|
|
|
340
353
|
type: 'string[]',
|
|
341
354
|
defaultValue: 'unset',
|
|
342
355
|
mutability: 'read/write',
|
|
343
|
-
owningCommand: 'oat config set documentation.excludes <glob[,glob...]>',
|
|
356
|
+
owningCommand: 'oat config set documentation.excludes <glob[,glob...]|JSON-array>',
|
|
344
357
|
description: 'Comma-separated globs, relative to the docs directory, excluded from `oat docs generate-index`. Repeated `--exclude` flags extend this list; an empty value clears the key.',
|
|
345
358
|
},
|
|
346
359
|
{
|
|
@@ -1112,7 +1125,24 @@ function parseInstructionPointerExcludes(rawValue) {
|
|
|
1112
1125
|
}
|
|
1113
1126
|
return normalized;
|
|
1114
1127
|
}
|
|
1115
|
-
function parseDocumentationExcludes(rawValue) {
|
|
1128
|
+
function parseDocumentationExcludes(rawValue, key) {
|
|
1129
|
+
const trimmed = rawValue.trim();
|
|
1130
|
+
// A glob list may be given as a JSON array (which also keeps braces such as
|
|
1131
|
+
// `{a,b}` intact). Anything else that looks like JSON is a mistake, never a
|
|
1132
|
+
// literal glob.
|
|
1133
|
+
if (trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
1134
|
+
let parsed;
|
|
1135
|
+
try {
|
|
1136
|
+
parsed = JSON.parse(trimmed);
|
|
1137
|
+
}
|
|
1138
|
+
catch {
|
|
1139
|
+
parsed = undefined;
|
|
1140
|
+
}
|
|
1141
|
+
if (!Array.isArray(parsed) ||
|
|
1142
|
+
parsed.some((entry) => typeof entry !== 'string' || entry.trim().length === 0))
|
|
1143
|
+
throw new Error(`Invalid ${key} value: a value starting with [ or { must be a JSON array of non-empty strings; otherwise use comma-separated globs.`);
|
|
1144
|
+
return [...new Set(parsed.map((entry) => entry.trim()))];
|
|
1145
|
+
}
|
|
1116
1146
|
const parsed = rawValue
|
|
1117
1147
|
.split(',')
|
|
1118
1148
|
.map((entry) => entry.trim())
|
|
@@ -1852,7 +1882,7 @@ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, d
|
|
|
1852
1882
|
value = normalizeDocumentationCheck(parsed);
|
|
1853
1883
|
}
|
|
1854
1884
|
else if (key === 'documentation.nonReaderPaths') {
|
|
1855
|
-
const paths = parseDocumentationExcludes(rawValue);
|
|
1885
|
+
const paths = parseDocumentationExcludes(rawValue, 'documentation.nonReaderPaths');
|
|
1856
1886
|
value = paths.length > 0 ? paths : undefined;
|
|
1857
1887
|
}
|
|
1858
1888
|
else if (key === 'documentation.ia') {
|
|
@@ -1869,8 +1899,13 @@ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, d
|
|
|
1869
1899
|
}
|
|
1870
1900
|
}
|
|
1871
1901
|
catch (error) {
|
|
1872
|
-
if (key === 'documentation.check'
|
|
1902
|
+
if (key === 'documentation.check' ||
|
|
1903
|
+
key === 'documentation.nonReaderPaths')
|
|
1873
1904
|
throw error;
|
|
1905
|
+
if (error instanceof DocumentationIaPresetError)
|
|
1906
|
+
throw new Error(`Invalid ${key} value: ${error.detail}.`, {
|
|
1907
|
+
cause: error,
|
|
1908
|
+
});
|
|
1874
1909
|
throw new Error(`Invalid ${key} value: ${key === 'documentation.ia' ? 'expected IA JSON with preset, repo-relative decision, optional tracks and verification' : key === 'documentation.navOwner' ? 'expected nav-sync or hand-authored' : 'expected a non-empty path/URL string or ordered non-empty string array'}.`, { cause: error });
|
|
1875
1910
|
}
|
|
1876
1911
|
let raw = { version: 1 };
|
|
@@ -2101,8 +2136,11 @@ async function setConfigValue(repoRoot, userConfigDir, key, rawValue, surface, d
|
|
|
2101
2136
|
else if (key === 'documentation.config') {
|
|
2102
2137
|
doc.config = normalizeSharedRoot(rawValue);
|
|
2103
2138
|
}
|
|
2139
|
+
else if (key === 'documentation.index') {
|
|
2140
|
+
doc.index = normalizeSharedRoot(rawValue);
|
|
2141
|
+
}
|
|
2104
2142
|
else if (key === 'documentation.excludes') {
|
|
2105
|
-
const excludes = parseDocumentationExcludes(rawValue);
|
|
2143
|
+
const excludes = parseDocumentationExcludes(rawValue, 'documentation.excludes');
|
|
2106
2144
|
if (excludes.length === 0) {
|
|
2107
2145
|
// Unlike the scalar shared keys, an empty value is the documented way
|
|
2108
2146
|
// to clear this list rather than an error.
|
|
@@ -2260,9 +2298,9 @@ function withoutOwnKey(record, key) {
|
|
|
2260
2298
|
*
|
|
2261
2299
|
* Pruning is strictly "the object has no remaining own keys". That is what
|
|
2262
2300
|
* keeps a prune from destroying a sibling the `oat config` catalog does not
|
|
2263
|
-
* expose
|
|
2264
|
-
*
|
|
2265
|
-
*
|
|
2301
|
+
* expose (such as a key a newer CLI wrote): an unset of the last catalogued
|
|
2302
|
+
* `documentation.*` key leaves the parent in place while any other key is
|
|
2303
|
+
* still present.
|
|
2266
2304
|
*
|
|
2267
2305
|
* A missing path, a `null`/`undefined` leaf, or a non-object in the middle of
|
|
2268
2306
|
* the path all report `removed: false`, which the caller surfaces as the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/check.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,sBAAsB,IAAI,OAAO,
|
|
1
|
+
{"version":3,"file":"check.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/check.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AAKpC,wBAAgB,sBAAsB,IAAI,OAAO,CA6DhD"}
|
|
@@ -4,5 +4,7 @@ export declare function checkDocs(options: {
|
|
|
4
4
|
cwd: string;
|
|
5
5
|
targetDir?: string;
|
|
6
6
|
rules?: string[];
|
|
7
|
+
/** Receives human notices; they never enter the JSON result. */
|
|
8
|
+
onNotice?: (message: string) => void;
|
|
7
9
|
}): Promise<DocsCheckResult>;
|
|
8
10
|
//# sourceMappingURL=checker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checker.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/checker.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAuB,MAAM,eAAe,CAAC;AAQ1E,gFAAgF;AAChF,wBAAsB,SAAS,CAAC,OAAO,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;
|
|
1
|
+
{"version":3,"file":"checker.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/checker.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,eAAe,EAAuB,MAAM,eAAe,CAAC;AAQ1E,gFAAgF;AAChF,wBAAsB,SAAS,CAAC,OAAO,EAAE;IACvC,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,gEAAgE;IAChE,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CACtC,GAAG,OAAO,CAAC,eAAe,CAAC,CAyF3B"}
|
|
@@ -11,6 +11,8 @@ export async function checkDocs(options) {
|
|
|
11
11
|
if (unknown)
|
|
12
12
|
throw new Error(`Unknown docs check rule "${unknown}". Choose: ${DOCS_CHECK_RULES.join(', ')}.`);
|
|
13
13
|
const target = await resolveCheckTarget(options.cwd, options.targetDir);
|
|
14
|
+
for (const notice of target.notices)
|
|
15
|
+
options.onNotice?.(notice);
|
|
14
16
|
const policy = target.documentation.check;
|
|
15
17
|
const findings = [];
|
|
16
18
|
const exceptions = policy?.exceptions?.map((e) => ({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/commands.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgI7C,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/commands.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAA0B,KAAK,WAAW,EAAE,MAAM,SAAS,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAgI7C,wBAAsB,aAAa,CACjC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CAkNf"}
|
|
@@ -111,6 +111,38 @@ export async function checkCommands(target, pages, report) {
|
|
|
111
111
|
if (!docs.length)
|
|
112
112
|
return;
|
|
113
113
|
const manifests = await packages(target);
|
|
114
|
+
// A framework app's own contributing pages usually speak from the app
|
|
115
|
+
// directory. Its package is tried first; the repository root remains the
|
|
116
|
+
// fallback context, so pages written from the repository root still resolve.
|
|
117
|
+
// Literal Markdown roots keep the repository-root context only.
|
|
118
|
+
const appContext = target.framework !== 'markdown' &&
|
|
119
|
+
target.appRoot !== target.repoRoot &&
|
|
120
|
+
manifests.some((item) => item.root === target.appRoot)
|
|
121
|
+
? target.appRoot
|
|
122
|
+
: undefined;
|
|
123
|
+
const manifestName = (pkg, labelled) => {
|
|
124
|
+
const name = posix(relative(target.repoRoot, join(pkg.root, 'package.json'))) ||
|
|
125
|
+
'package.json';
|
|
126
|
+
if (!labelled)
|
|
127
|
+
return name;
|
|
128
|
+
return pkg.root === target.appRoot
|
|
129
|
+
? `${name} (app root)`
|
|
130
|
+
: pkg.root === target.repoRoot
|
|
131
|
+
? `${name} (repository root)`
|
|
132
|
+
: name;
|
|
133
|
+
};
|
|
134
|
+
const lookup = async (root) => {
|
|
135
|
+
if (!root || !inside(target.repoRoot, root) || !(await present(root)))
|
|
136
|
+
return undefined;
|
|
137
|
+
let current = root;
|
|
138
|
+
while (inside(target.repoRoot, current)) {
|
|
139
|
+
const pkg = manifests.find((item) => item.root === current);
|
|
140
|
+
if (pkg || current === target.repoRoot)
|
|
141
|
+
return pkg;
|
|
142
|
+
current = dirname(current);
|
|
143
|
+
}
|
|
144
|
+
return undefined;
|
|
145
|
+
};
|
|
114
146
|
for (const p of docs)
|
|
115
147
|
for (const node of p.nodes) {
|
|
116
148
|
if (node.type !== 'code' && node.type !== 'inlineCode')
|
|
@@ -119,7 +151,11 @@ export async function checkCommands(target, pages, report) {
|
|
|
119
151
|
node.lang &&
|
|
120
152
|
!['sh', 'shell', 'bash', 'zsh', 'console', 'powershell'].includes(node.lang))
|
|
121
153
|
continue;
|
|
122
|
-
|
|
154
|
+
// Each block starts in the repository-root context and, for framework
|
|
155
|
+
// apps, also in the app-root context; literal `cd` moves both.
|
|
156
|
+
let contexts = appContext
|
|
157
|
+
? [appContext, target.repoRoot]
|
|
158
|
+
: [target.repoRoot];
|
|
123
159
|
const start = node.position?.start;
|
|
124
160
|
const openingLine = p.source.split(/\r?\n/).at((start?.line ?? 1) - 1) ?? '';
|
|
125
161
|
const fenced = node.type === 'code' &&
|
|
@@ -137,36 +173,34 @@ export async function checkCommands(target, pages, report) {
|
|
|
137
173
|
continue;
|
|
138
174
|
const words = tokens(segment);
|
|
139
175
|
if (words[0] === 'cd') {
|
|
140
|
-
cwd
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
: undefined;
|
|
176
|
+
contexts = contexts.map((cwd) => words[1] && !illustrative(words[1]) && cwd
|
|
177
|
+
? resolve(cwd, words[1])
|
|
178
|
+
: undefined);
|
|
144
179
|
continue;
|
|
145
180
|
}
|
|
146
181
|
const manager = words.findIndex((w) => w === 'pnpm' || w === 'npm');
|
|
147
182
|
if (manager < 0)
|
|
148
183
|
continue;
|
|
149
184
|
const kind = words[manager];
|
|
150
|
-
|
|
185
|
+
const moves = [];
|
|
151
186
|
let filter;
|
|
152
187
|
let index = manager + 1;
|
|
153
188
|
while (words[index]?.startsWith('-')) {
|
|
154
189
|
const option = words[index++];
|
|
155
190
|
if (['-w', '--workspace-root'].includes(option))
|
|
156
|
-
|
|
191
|
+
moves.push(() => target.repoRoot);
|
|
157
192
|
else if (option === '--filter' || option === '-F')
|
|
158
193
|
filter = words[index++];
|
|
159
194
|
else if (option.startsWith('--filter='))
|
|
160
195
|
filter = option.slice(9);
|
|
161
196
|
else if (['--dir', '-C', '--prefix'].includes(option)) {
|
|
162
197
|
const dir = words[index++];
|
|
163
|
-
root
|
|
164
|
-
root
|
|
165
|
-
|
|
166
|
-
: undefined;
|
|
198
|
+
moves.push((root) => root && dir && !illustrative(dir)
|
|
199
|
+
? resolve(root, dir)
|
|
200
|
+
: undefined);
|
|
167
201
|
}
|
|
168
202
|
else if (['-r', '--recursive', '--workspace'].includes(option))
|
|
169
|
-
|
|
203
|
+
moves.push(() => undefined);
|
|
170
204
|
}
|
|
171
205
|
let script = words[index];
|
|
172
206
|
if (!script)
|
|
@@ -190,29 +224,32 @@ export async function checkCommands(target, pages, report) {
|
|
|
190
224
|
illustrative(script) ||
|
|
191
225
|
(filter && illustrative(filter)))
|
|
192
226
|
continue;
|
|
193
|
-
|
|
227
|
+
const candidates = [];
|
|
194
228
|
if (filter) {
|
|
195
229
|
const selected = manifests.filter((item) => item.name === filter ||
|
|
196
230
|
(filter.startsWith('./') &&
|
|
197
231
|
posix(relative(target.repoRoot, item.root)) ===
|
|
198
232
|
filter.slice(2)));
|
|
199
233
|
if (selected.length === 1)
|
|
200
|
-
|
|
234
|
+
candidates.push(selected[0]);
|
|
201
235
|
}
|
|
202
|
-
else
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
pkg = manifests.find((item) => item.root === current);
|
|
208
|
-
if (pkg || current === target.repoRoot)
|
|
209
|
-
break;
|
|
210
|
-
current = dirname(current);
|
|
236
|
+
else {
|
|
237
|
+
for (const cwd of contexts) {
|
|
238
|
+
const pkg = await lookup(moves.reduce((root, move) => move(root), cwd));
|
|
239
|
+
if (pkg && !candidates.includes(pkg))
|
|
240
|
+
candidates.push(pkg);
|
|
211
241
|
}
|
|
212
242
|
}
|
|
213
|
-
if (!
|
|
243
|
+
if (!candidates.length) {
|
|
214
244
|
report('commands', 'error', p.path, `Cannot resolve package context for ${kind} ${script}${filter ? ` (--filter ${filter})` : ''}; use literal cd, -w or --filter.`, line);
|
|
215
|
-
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
let pkg;
|
|
248
|
+
for (const candidate of candidates) {
|
|
249
|
+
if (Object.hasOwn(candidate.scripts ?? {}, script)) {
|
|
250
|
+
pkg = candidate;
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
216
253
|
// pnpm shorthand can invoke an installed local binary. Explicit run
|
|
217
254
|
// remains script-only; never execute a binary or search system PATH.
|
|
218
255
|
const installedBinary = kind === 'pnpm' &&
|
|
@@ -220,12 +257,19 @@ export async function checkCommands(target, pages, report) {
|
|
|
220
257
|
!/[\\/]/.test(script) &&
|
|
221
258
|
script !== '.' &&
|
|
222
259
|
script !== '..' &&
|
|
223
|
-
(await Promise.all([
|
|
224
|
-
if (
|
|
225
|
-
|
|
260
|
+
(await Promise.all([candidate.root, target.repoRoot].map((binRoot) => present(join(binRoot, 'node_modules/.bin', script))))).some(Boolean);
|
|
261
|
+
if (installedBinary) {
|
|
262
|
+
pkg = candidate;
|
|
263
|
+
break;
|
|
264
|
+
}
|
|
226
265
|
}
|
|
227
|
-
|
|
228
|
-
report('commands', '
|
|
266
|
+
if (!pkg)
|
|
267
|
+
report('commands', 'error', p.path, `Script "${script}" does not exist in ${candidates
|
|
268
|
+
.map((candidate) => manifestName(candidate, candidates.length > 1))
|
|
269
|
+
.join(' or ')}.`, line);
|
|
270
|
+
else if (Object.hasOwn(pkg.scripts ?? {}, script) &&
|
|
271
|
+
/^\s*echo(?:\s|$)/.test(pkg.scripts[script]))
|
|
272
|
+
report('commands', 'warn', p.path, `Script "${script}" is an echo stub in ${manifestName(pkg, false)}.`, line);
|
|
229
273
|
}
|
|
230
274
|
}
|
|
231
275
|
}
|
|
@@ -11,6 +11,8 @@ export interface CheckTarget {
|
|
|
11
11
|
framework: 'markdown' | 'fumadocs' | 'mkdocs';
|
|
12
12
|
documentation: OatDocumentationConfig;
|
|
13
13
|
matcher: ExclusionMatcher;
|
|
14
|
+
/** Human notices about how an explicit target was interpreted. */
|
|
15
|
+
notices: string[];
|
|
14
16
|
}
|
|
15
17
|
export declare function resolveCheckTarget(cwd: string, targetDir?: string): Promise<CheckTarget>;
|
|
16
18
|
export declare function readerFiles(target: CheckTarget): Promise<string[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/files.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAExD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AACD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAQ1D;AACD,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa5D;AAKD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC9C,aAAa,EAAE,sBAAsB,CAAC;IACtC,OAAO,EAAE,gBAAgB,CAAC;
|
|
1
|
+
{"version":3,"file":"files.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/files.ts"],"names":[],"mappings":"AAGA,OAAO,EAGL,KAAK,gBAAgB,EACtB,MAAM,kCAAkC,CAAC;AAC1C,OAAO,EAGL,KAAK,sBAAsB,EAC5B,MAAM,oBAAoB,CAAC;AAG5B,OAAO,EAAa,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAExD,wBAAgB,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE1C;AACD,wBAAgB,MAAM,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAQ1D;AACD,wBAAsB,OAAO,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa5D;AAKD,MAAM,WAAW,WAAW;IAC1B,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAC;IAC9C,aAAa,EAAE,sBAAsB,CAAC;IACtC,OAAO,EAAE,gBAAgB,CAAC;IAC1B,kEAAkE;IAClE,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AACD,wBAAsB,kBAAkB,CACtC,GAAG,EAAE,MAAM,EACX,SAAS,CAAC,EAAE,MAAM,GACjB,OAAO,CAAC,WAAW,CAAC,CAiFtB;AACD,wBAAsB,WAAW,CAAC,MAAM,EAAE,WAAW,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CA6BxE;AACD,wBAAsB,QAAQ,CAC5B,MAAM,EAAE,WAAW,EACnB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,UAAU,CAAC,CAKrB;AACD,+EAA+E;AAC/E,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,OAAO,CAAC,CAMlB;AACD,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAU3D"}
|
|
@@ -70,6 +70,16 @@ export async function resolveCheckTarget(cwd, targetDir) {
|
|
|
70
70
|
throw new Error(`Docs content directory does not exist: ${contentRoot}. Set documentation.root or pass --target-dir.`);
|
|
71
71
|
const matchesContent = configuredContentPath !== undefined && configuredContentPath === appRoot;
|
|
72
72
|
const documentation = matching || matchesContent ? doc : {};
|
|
73
|
+
const notices = [];
|
|
74
|
+
if (targetDir !== undefined && configured && doc.root && !matching) {
|
|
75
|
+
const configuredTooling = doc.tooling === 'fumadocs' || doc.tooling === 'mkdocs'
|
|
76
|
+
? doc.tooling
|
|
77
|
+
: undefined;
|
|
78
|
+
if (matchesContent && configuredTooling && framework === 'markdown')
|
|
79
|
+
notices.push(`Notice: --target-dir ${targetDir} is the content directory of the configured ${configuredTooling} app ${doc.root}; it is checked as Markdown, so navigation is skipped. Pass --target-dir ${doc.root} to include navigation.`);
|
|
80
|
+
else if (!matchesContent)
|
|
81
|
+
notices.push(`Notice: --target-dir ${targetDir} is not the configured documentation.root (${doc.root}); checking it as ${framework === 'markdown' ? 'a literal Markdown root' : `a ${framework} app`} without the configured documentation policy. Omit --target-dir to check ${doc.root}.`);
|
|
82
|
+
}
|
|
73
83
|
return {
|
|
74
84
|
repoRoot,
|
|
75
85
|
appRoot,
|
|
@@ -77,6 +87,7 @@ export async function resolveCheckTarget(cwd, targetDir) {
|
|
|
77
87
|
framework,
|
|
78
88
|
documentation,
|
|
79
89
|
matcher: createExclusionMatcher(documentation.nonReaderPaths ?? []),
|
|
90
|
+
notices,
|
|
80
91
|
};
|
|
81
92
|
}
|
|
82
93
|
export async function readerFiles(target) {
|
|
@@ -87,6 +98,11 @@ export async function readerFiles(target) {
|
|
|
87
98
|
});
|
|
88
99
|
for (const e of entries.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0)) {
|
|
89
100
|
const path = posix(join(dir, e.name));
|
|
101
|
+
// Installed dependencies and hidden tool state (.git, .oat, .next, ...)
|
|
102
|
+
// are never reader content, even under a broad explicit target.
|
|
103
|
+
if (e.isDirectory() &&
|
|
104
|
+
(e.name === 'node_modules' || e.name.startsWith('.')))
|
|
105
|
+
continue;
|
|
90
106
|
if (e.isDirectory() && !isExcludedPath(target.matcher, path, true))
|
|
91
107
|
await walk(path);
|
|
92
108
|
else if (e.isFile() &&
|
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import type { Report } from './check.types.js';
|
|
2
|
-
import {
|
|
2
|
+
import type { CheckTarget } from './files.js';
|
|
3
|
+
/**
|
|
4
|
+
* `navOwner: nav-sync` keeps strict generated freshness. Hand-authored and
|
|
5
|
+
* undeclared navigation gets the membership-and-order comparison (issue #367
|
|
6
|
+
* P21/G5): separators, labels, links, rest entries and borrowed pages are the
|
|
7
|
+
* author's presentation, never generator output to match.
|
|
8
|
+
*/
|
|
3
9
|
export declare function checkNavigation(target: CheckTarget, report: Report): Promise<void>;
|
|
4
10
|
//# sourceMappingURL=navigation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/navigation.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"navigation.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/navigation.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AAE3C;;;;;GAKG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,WAAW,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CA0Df"}
|
|
@@ -1,41 +1,12 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { join } from 'node:path';
|
|
3
|
-
import { isDeepStrictEqual } from 'node:util';
|
|
4
|
-
import { buildDocsNavTree } from '../../docs/nav/contents.js';
|
|
5
1
|
import { syncFumadocsNavigation } from '../../docs/nav/fumadocs.js';
|
|
2
|
+
import { compareFumadocsMembership, compareMkDocsMembership, } from '../../docs/nav/membership.js';
|
|
6
3
|
import { syncDocsNavigation } from '../../docs/nav/sync.js';
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
function navIdentity(value, expected) {
|
|
14
|
-
if (!Array.isArray(value))
|
|
15
|
-
return value;
|
|
16
|
-
return value.map((item, index) => {
|
|
17
|
-
if (typeof item === 'string')
|
|
18
|
-
return item;
|
|
19
|
-
if (typeof item !== 'object' || item === null)
|
|
20
|
-
return item;
|
|
21
|
-
const entry = Object.entries(item)[0];
|
|
22
|
-
if (!entry)
|
|
23
|
-
return item;
|
|
24
|
-
const expectedItem = Array.isArray(expected) ? expected[index] : undefined;
|
|
25
|
-
const children = expectedItem && typeof expectedItem === 'object'
|
|
26
|
-
? Object.values(expectedItem)[0]
|
|
27
|
-
: undefined;
|
|
28
|
-
// The Contents-derived tree identifies sections by their direct landing path.
|
|
29
|
-
// Actual landing/page labels are presentation; explicit group labels remain meaningful.
|
|
30
|
-
const section = Array.isArray(children) &&
|
|
31
|
-
children.some((child) => typeof child === 'string' && child.endsWith('/index.md'));
|
|
32
|
-
return Array.isArray(entry[1])
|
|
33
|
-
? {
|
|
34
|
-
[section ? '$section' : entry[0]]: navIdentity(entry[1], children),
|
|
35
|
-
}
|
|
36
|
-
: entry[1];
|
|
37
|
-
});
|
|
38
|
-
}
|
|
4
|
+
/**
|
|
5
|
+
* `navOwner: nav-sync` keeps strict generated freshness. Hand-authored and
|
|
6
|
+
* undeclared navigation gets the membership-and-order comparison (issue #367
|
|
7
|
+
* P21/G5): separators, labels, links, rest entries and borrowed pages are the
|
|
8
|
+
* author's presentation, never generator output to match.
|
|
9
|
+
*/
|
|
39
10
|
export async function checkNavigation(target, report) {
|
|
40
11
|
if (target.framework === 'markdown')
|
|
41
12
|
return;
|
|
@@ -48,31 +19,14 @@ export async function checkNavigation(target, report) {
|
|
|
48
19
|
check: true,
|
|
49
20
|
nonReaderPaths,
|
|
50
21
|
});
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
typeof actual === 'object' &&
|
|
60
|
-
!Array.isArray(actual) &&
|
|
61
|
-
!Object.hasOwn(actual, 'pages'))
|
|
62
|
-
continue;
|
|
63
|
-
stale =
|
|
64
|
-
!Array.isArray(actual.pages) ||
|
|
65
|
-
!isDeepStrictEqual(actual.pages.map((p) => typeof p === 'string' ? pageIdentity(p) : p), file.meta.pages.map(pageIdentity));
|
|
66
|
-
}
|
|
67
|
-
catch {
|
|
68
|
-
report('navigation', 'error', file.path, 'Cannot read or parse meta.json; check file permissions and JSON syntax.');
|
|
69
|
-
continue;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (stale)
|
|
73
|
-
report('navigation', 'error', file.path, generated
|
|
74
|
-
? 'Generated navigation is stale; run oat docs nav sync.'
|
|
75
|
-
: 'Navigation membership/order differs from authored Contents; update pages or Contents.');
|
|
22
|
+
if (generated) {
|
|
23
|
+
for (const file of result.metaFiles)
|
|
24
|
+
if (file.changed)
|
|
25
|
+
report('navigation', 'error', file.path, 'Generated navigation is stale; run oat docs nav sync.');
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
for (const issue of await compareFumadocsMembership(target.contentRoot, result.metaFiles))
|
|
29
|
+
report('navigation', 'error', issue.path, issue.message, issue.line);
|
|
76
30
|
}
|
|
77
31
|
for (const path of result.unlisted)
|
|
78
32
|
report('navigation', 'error', path, 'Reader page/folder is unlisted in authored Contents and would be hidden from navigation.');
|
|
@@ -87,15 +41,12 @@ export async function checkNavigation(target, report) {
|
|
|
87
41
|
report('navigation', 'error', path, 'Generated navigation is stale; run oat docs nav sync.');
|
|
88
42
|
}
|
|
89
43
|
else {
|
|
90
|
-
const
|
|
44
|
+
for (const issue of await compareMkDocsMembership({
|
|
45
|
+
appRoot: target.appRoot,
|
|
91
46
|
docsRoot: target.contentRoot,
|
|
92
47
|
nonReaderPaths,
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
if (!actual || !Object.hasOwn(actual, 'nav'))
|
|
96
|
-
return;
|
|
97
|
-
if (!isDeepStrictEqual(navIdentity(actual.nav, expected), navIdentity(expected, expected)))
|
|
98
|
-
report('navigation', 'error', 'mkdocs.yml', 'Navigation membership/order differs from authored Contents; update nav or Contents.');
|
|
48
|
+
}))
|
|
49
|
+
report('navigation', 'error', issue.path, issue.message, issue.line);
|
|
99
50
|
}
|
|
100
51
|
}
|
|
101
52
|
catch (error) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reader-rules.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/reader-rules.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAML,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"reader-rules.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/check/reader-rules.ts"],"names":[],"mappings":"AASA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAML,KAAK,WAAW,EACjB,MAAM,SAAS,CAAC;AACjB,OAAO,EAAY,KAAK,UAAU,EAAE,MAAM,YAAY,CAAC;AAEvD,wBAAsB,YAAY,CAChC,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,IAAI,CAAC,CA4Vf;AAED,wBAAgB,eAAe,CAC7B,MAAM,EAAE,WAAW,EACnB,KAAK,EAAE,UAAU,EAAE,EACnB,MAAM,EAAE,MAAM,GACb,IAAI,CA+CN"}
|
|
@@ -116,10 +116,15 @@ export async function checkReaders(target, pages, report) {
|
|
|
116
116
|
const lines = p.source.split(/\r?\n/).length - (p.source.endsWith('\n') ? 1 : 0);
|
|
117
117
|
const limit = reference ? t.referenceLines : t.pageLines;
|
|
118
118
|
if (lines > limit)
|
|
119
|
-
report('page-length', 'warn', p.path, `Page has ${lines} physical source lines; limit ${limit}${reference ? ' for reference pages' : ''}
|
|
120
|
-
|
|
119
|
+
report('page-length', 'warn', p.path, `Page has ${lines} physical source lines; limit ${limit}${reference ? ' for reference pages' : ''}.`,
|
|
120
|
+
// The first physical line past the limit.
|
|
121
|
+
limit + 1);
|
|
122
|
+
const h3Nodes = p.nodes.filter((n) => n.type === 'heading' && n.depth === 3);
|
|
123
|
+
const h3 = h3Nodes.length;
|
|
121
124
|
if (reference && h3 > t.referenceH3)
|
|
122
|
-
report('page-length', 'warn', p.path, `Reference page has ${h3} real H3 headings; limit ${t.referenceH3}
|
|
125
|
+
report('page-length', 'warn', p.path, `Reference page has ${h3} real H3 headings; limit ${t.referenceH3}.`,
|
|
126
|
+
// The first H3 past the limit.
|
|
127
|
+
h3Nodes[t.referenceH3]?.position?.start.line ?? 1);
|
|
123
128
|
for (const link of p.links) {
|
|
124
129
|
const href = link.href;
|
|
125
130
|
if (/^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(href))
|
|
@@ -17,5 +17,13 @@ export interface GenerateIndexOptions {
|
|
|
17
17
|
excludes?: string[];
|
|
18
18
|
}
|
|
19
19
|
export declare function generateIndex(docsDir: string, options?: GenerateIndexOptions): Promise<IndexEntry[]>;
|
|
20
|
-
export
|
|
20
|
+
export interface RenderIndexOptions {
|
|
21
|
+
/**
|
|
22
|
+
* POSIX path from the manifest's directory to the indexed docs directory.
|
|
23
|
+
* Entry paths are docs-relative; this prefix makes each link resolve from
|
|
24
|
+
* the file the manifest is written to.
|
|
25
|
+
*/
|
|
26
|
+
linkBase?: string;
|
|
27
|
+
}
|
|
28
|
+
export declare function renderIndex(entries: IndexEntry[], options?: RenderIndexOptions): string;
|
|
21
29
|
//# sourceMappingURL=generator.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/index-generate/generator.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,YAAY,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAgLD,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,UAAU,EAAE,CAAC,CAMvB;
|
|
1
|
+
{"version":3,"file":"generator.d.ts","sourceRoot":"","sources":["../../../../src/commands/docs/index-generate/generator.ts"],"names":[],"mappings":"AAYA,OAAO,EAAE,sBAAsB,EAAE,MAAM,kCAAkC,CAAC;AAC1E,YAAY,EAAE,gBAAgB,EAAE,MAAM,kCAAkC,CAAC;AAEzE,MAAM,WAAW,UAAU;IACzB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,UAAU,EAAE,CAAC;IACxB,8DAA8D;IAC9D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;CACrB;AAgLD,wBAAsB,aAAa,CACjC,OAAO,EAAE,MAAM,EACf,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,UAAU,EAAE,CAAC,CAMvB;AAED,MAAM,WAAW,kBAAkB;IACjC;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAyCD,wBAAgB,WAAW,CACzB,OAAO,EAAE,UAAU,EAAE,EACrB,OAAO,GAAE,kBAAuB,GAC/B,MAAM,CAMR"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { readdir, readFile } from 'node:fs/promises';
|
|
2
|
-
import { join, relative, resolve } from 'node:path';
|
|
2
|
+
import { join, posix, relative, resolve } from 'node:path';
|
|
3
3
|
import { hasIndexContents, parseIndexContents, } from '../../docs/nav/contents.js';
|
|
4
4
|
import { resolveFolderTitle } from '../../docs/nav/fumadocs.js';
|
|
5
5
|
import { createExclusionMatcher, } from '../../docs/shared/exclusions.js';
|
|
@@ -151,24 +151,35 @@ async function collectEntries(docsDir, relativeBase, matcher) {
|
|
|
151
151
|
export async function generateIndex(docsDir, options = {}) {
|
|
152
152
|
return collectEntries(docsDir, '', createExclusionMatcher(options.excludes ?? []));
|
|
153
153
|
}
|
|
154
|
-
|
|
154
|
+
/**
|
|
155
|
+
* Percent-encode the characters that end, split or rewrite a CommonMark link
|
|
156
|
+
* destination (whitespace, `#`, `?`, parentheses, angle brackets, `&` entity
|
|
157
|
+
* starts and `\` escapes) and `%` itself, as UTF-8 bytes, so any supported
|
|
158
|
+
* filesystem path renders as a resolvable link.
|
|
159
|
+
*/
|
|
160
|
+
function linkDestination(path) {
|
|
161
|
+
return path.replace(/[%\s#?()<>&\\]/g, (char) => [...Buffer.from(char, 'utf8')]
|
|
162
|
+
.map((byte) => `%${byte.toString(16).toUpperCase().padStart(2, '0')}`)
|
|
163
|
+
.join(''));
|
|
164
|
+
}
|
|
165
|
+
function renderEntries(entries, indent, linkBase) {
|
|
155
166
|
const lines = [];
|
|
156
167
|
const prefix = ' '.repeat(indent);
|
|
157
168
|
for (const entry of entries) {
|
|
158
169
|
if (entry.children) {
|
|
159
170
|
lines.push(`${prefix}- ${entry.title}${entry.unlisted ? ' [unlisted in Contents]' : ''}`);
|
|
160
|
-
lines.push(...renderEntries(entry.children, indent + 1));
|
|
171
|
+
lines.push(...renderEntries(entry.children, indent + 1, linkBase));
|
|
161
172
|
}
|
|
162
173
|
else {
|
|
163
174
|
const desc = entry.description ? ` — ${entry.description}` : '';
|
|
164
|
-
lines.push(`${prefix}- [${entry.title}](${entry.path})${desc}${entry.unlisted ? ' [unlisted in Contents]' : ''}`);
|
|
175
|
+
lines.push(`${prefix}- [${entry.title}](${linkDestination(linkBase ? posix.join(linkBase, entry.path) : entry.path)})${desc}${entry.unlisted ? ' [unlisted in Contents]' : ''}`);
|
|
165
176
|
}
|
|
166
177
|
}
|
|
167
178
|
return lines;
|
|
168
179
|
}
|
|
169
|
-
export function renderIndex(entries) {
|
|
180
|
+
export function renderIndex(entries, options = {}) {
|
|
170
181
|
if (entries.length === 0) {
|
|
171
182
|
return '';
|
|
172
183
|
}
|
|
173
|
-
return `${renderEntries(entries, 0).join('\n')}\n`;
|
|
184
|
+
return `${renderEntries(entries, 0, options.linkBase ?? '').join('\n')}\n`;
|
|
174
185
|
}
|