@guiho/xdocs 0.3.1 → 0.4.2
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/CHANGELOG.md +9 -0
- package/DOCS.md +60 -27
- package/README.md +49 -18
- package/docs/2026-07-05-xdocs-document-model.md +77 -0
- package/docs/docs.xdocs.md +22 -0
- package/jsr.json +1 -1
- package/library/agents.d.ts +1 -1
- package/library/agents.d.ts.map +1 -1
- package/library/agents.js +10 -6
- package/library/commands/generate.d.ts.map +1 -1
- package/library/commands/generate.js +23 -1
- package/library/commands/list.d.ts.map +1 -1
- package/library/commands/list.js +14 -4
- package/library/commands/merge.d.ts.map +1 -1
- package/library/commands/merge.js +12 -1
- package/library/commands/scan.d.ts.map +1 -1
- package/library/commands/scan.js +13 -2
- package/library/config.d.ts.map +1 -1
- package/library/config.js +7 -3
- package/library/discovery.d.ts +8 -4
- package/library/discovery.d.ts.map +1 -1
- package/library/discovery.js +107 -17
- package/library/flags.d.ts +1 -1
- package/library/flags.js +1 -1
- package/library/guiho-xdocs.d.ts +2 -2
- package/library/guiho-xdocs.d.ts.map +1 -1
- package/library/guiho-xdocs.js +1 -1
- package/library/help.js +16 -15
- package/library/metadata.d.ts +1 -1
- package/library/metadata.d.ts.map +1 -1
- package/library/metadata.js +23 -3
- package/library/tree.d.ts +1 -1
- package/library/tree.d.ts.map +1 -1
- package/library/tree.js +2 -2
- package/library/types.d.ts +14 -2
- package/library/types.d.ts.map +1 -1
- package/package.json +4 -1
- package/prompts/agents.md +9 -2
- package/prompts/generate.md +10 -2
- package/prompts/prompts.xdocs.md +27 -0
- package/prompts/update.md +19 -7
- package/prompts/write.md +18 -5
- package/scripts/scripts.xdocs.md +5 -0
- package/skills/guiho-s-xdocs/SKILL.md +133 -32
- package/skills/guiho-s-xdocs/guiho-s-xdocs.xdocs.md +26 -0
- package/skills/skills.xdocs.md +9 -4
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../source/commands/list.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAInE,4BAA4B;AAC5B,eAAO,MAAM,OAAO,GAAU,SAAS,eAAe,EAAE,QAAQ,eAAe,KAAG,OAAO,CAAC,IAAI,
|
|
1
|
+
{"version":3,"file":"list.d.ts","sourceRoot":"","sources":["../../source/commands/list.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAInE,4BAA4B;AAC5B,eAAO,MAAM,OAAO,GAAU,SAAS,eAAe,EAAE,QAAQ,eAAe,KAAG,OAAO,CAAC,IAAI,CAsD7F,CAAA"}
|
package/library/commands/list.js
CHANGED
|
@@ -10,13 +10,22 @@ export const runList = async (options, parsed) => {
|
|
|
10
10
|
const targetPath = parsed.positionals[0] ? resolve(options.cwd, parsed.positionals[0]) : options.cwd;
|
|
11
11
|
const result = await scanProject(config);
|
|
12
12
|
const relevantFiles = result.xdocsFiles.filter((f) => f.path.startsWith(targetPath));
|
|
13
|
-
// Collect all files
|
|
13
|
+
// Collect all source files and Markdown companion documents from metadata.
|
|
14
14
|
const fileList = [];
|
|
15
15
|
for (const xdocsFile of relevantFiles) {
|
|
16
16
|
if (!xdocsFile.metadata)
|
|
17
17
|
continue;
|
|
18
18
|
for (const [fileName, description] of Object.entries(xdocsFile.metadata.files)) {
|
|
19
19
|
fileList.push({
|
|
20
|
+
kind: 'file',
|
|
21
|
+
file: fileName,
|
|
22
|
+
description,
|
|
23
|
+
source: xdocsFile.relativePath,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
for (const [fileName, description] of Object.entries(xdocsFile.metadata.documents)) {
|
|
27
|
+
fileList.push({
|
|
28
|
+
kind: 'document',
|
|
20
29
|
file: fileName,
|
|
21
30
|
description,
|
|
22
31
|
source: xdocsFile.relativePath,
|
|
@@ -29,13 +38,14 @@ export const runList = async (options, parsed) => {
|
|
|
29
38
|
}
|
|
30
39
|
if (fileList.length === 0) {
|
|
31
40
|
const scope = targetPath === options.cwd ? 'project' : relative(options.cwd, targetPath);
|
|
32
|
-
process.stdout.write(`No documented files found in ${scope}.\n`);
|
|
41
|
+
process.stdout.write(`No documented files or documents found in ${scope}.\n`);
|
|
33
42
|
return;
|
|
34
43
|
}
|
|
35
44
|
const scope = targetPath === options.cwd ? 'project' : relative(options.cwd, targetPath);
|
|
36
|
-
process.stdout.write(`\
|
|
45
|
+
process.stdout.write(`\nentries in ${scope}:\n\n`);
|
|
37
46
|
for (const entry of fileList) {
|
|
38
|
-
|
|
47
|
+
const label = entry.kind === 'document' ? 'document' : 'file';
|
|
48
|
+
process.stdout.write(` ${label} ${entry.file}: ${entry.description}\n`);
|
|
39
49
|
}
|
|
40
50
|
process.stdout.write('\n');
|
|
41
51
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../source/commands/merge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAKnE,6BAA6B;AAC7B,eAAO,MAAM,QAAQ,GAAU,SAAS,eAAe,EAAE,QAAQ,eAAe,KAAG,OAAO,CAAC,IAAI,
|
|
1
|
+
{"version":3,"file":"merge.d.ts","sourceRoot":"","sources":["../../source/commands/merge.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAKnE,6BAA6B;AAC7B,eAAO,MAAM,QAAQ,GAAU,SAAS,eAAe,EAAE,QAAQ,eAAe,KAAG,OAAO,CAAC,IAAI,CAsE9F,CAAA"}
|
|
@@ -14,7 +14,7 @@ export const runMerge = async (options, parsed) => {
|
|
|
14
14
|
const result = await scanProject(config);
|
|
15
15
|
const relevantFiles = result.xdocsFiles.filter((f) => f.path.startsWith(targetPath));
|
|
16
16
|
if (relevantFiles.length === 0) {
|
|
17
|
-
process.stdout.write('No xdocs
|
|
17
|
+
process.stdout.write('No xdocs descriptors found in the specified path.\n');
|
|
18
18
|
return;
|
|
19
19
|
}
|
|
20
20
|
const lines = [];
|
|
@@ -23,6 +23,9 @@ export const runMerge = async (options, parsed) => {
|
|
|
23
23
|
if (file.metadata) {
|
|
24
24
|
lines.push(`# ${file.metadata.subject}`, '');
|
|
25
25
|
lines.push(file.metadata.description, '');
|
|
26
|
+
if (file.metadata.keywords.length > 0) {
|
|
27
|
+
lines.push(`Keywords: ${file.metadata.keywords.map((keyword) => `\`${keyword}\``).join(', ')}`, '');
|
|
28
|
+
}
|
|
26
29
|
const fileEntries = Object.entries(file.metadata.files);
|
|
27
30
|
if (fileEntries.length > 0) {
|
|
28
31
|
lines.push('## Files', '');
|
|
@@ -31,6 +34,14 @@ export const runMerge = async (options, parsed) => {
|
|
|
31
34
|
}
|
|
32
35
|
lines.push('');
|
|
33
36
|
}
|
|
37
|
+
const documentEntries = Object.entries(file.metadata.documents);
|
|
38
|
+
if (documentEntries.length > 0) {
|
|
39
|
+
lines.push('## Documents', '');
|
|
40
|
+
for (const [name, desc] of documentEntries) {
|
|
41
|
+
lines.push(`- \`${name}\`: ${desc}`);
|
|
42
|
+
}
|
|
43
|
+
lines.push('');
|
|
44
|
+
}
|
|
34
45
|
if (file.metadata.children.length > 0) {
|
|
35
46
|
lines.push('## Submodules', '');
|
|
36
47
|
for (const child of file.metadata.children) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../source/commands/scan.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAInE,4BAA4B;AAC5B,eAAO,MAAM,OAAO,GAAU,SAAS,eAAe,EAAE,SAAS,eAAe,KAAG,OAAO,CAAC,IAAI,
|
|
1
|
+
{"version":3,"file":"scan.d.ts","sourceRoot":"","sources":["../../source/commands/scan.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,aAAa,CAAA;AAInE,4BAA4B;AAC5B,eAAO,MAAM,OAAO,GAAU,SAAS,eAAe,EAAE,SAAS,eAAe,KAAG,OAAO,CAAC,IAAI,CAiE9F,CAAA"}
|
package/library/commands/scan.js
CHANGED
|
@@ -11,25 +11,31 @@ export const runScan = async (options, _parsed) => {
|
|
|
11
11
|
process.stdout.write(JSON.stringify({
|
|
12
12
|
totalFiles: result.totalFiles,
|
|
13
13
|
totalDirectories: result.totalDirectories,
|
|
14
|
+
totalMarkdownDocuments: result.totalMarkdownDocuments,
|
|
14
15
|
coveredDirectories: result.coveredDirectories,
|
|
15
16
|
uncoveredDirectories: result.uncoveredDirectories,
|
|
16
17
|
xdocsFiles: result.xdocsFiles.map((f) => ({
|
|
17
18
|
path: f.relativePath,
|
|
18
19
|
valid: f.valid,
|
|
19
20
|
subject: f.metadata?.subject ?? null,
|
|
21
|
+
keywords: f.metadata?.keywords ?? null,
|
|
22
|
+
documents: f.metadata?.documents ?? null,
|
|
23
|
+
discoveredDocuments: f.documents.map((document) => document.relativePath),
|
|
20
24
|
errors: f.errors,
|
|
21
25
|
})),
|
|
26
|
+
markdownDocuments: result.markdownDocuments.map((document) => document.relativePath),
|
|
22
27
|
uncoveredPaths: result.uncoveredPaths,
|
|
23
28
|
}, null, 2) + '\n');
|
|
24
29
|
return;
|
|
25
30
|
}
|
|
26
31
|
process.stdout.write(`\nxdocs scan\n\n`);
|
|
27
|
-
process.stdout.write(`
|
|
32
|
+
process.stdout.write(`descriptor extension: ${config.extensions.supported.join(', ')}\n`);
|
|
28
33
|
process.stdout.write(`total files scanned: ${result.totalFiles}\n`);
|
|
29
34
|
process.stdout.write(`total directories: ${result.totalDirectories}\n`);
|
|
35
|
+
process.stdout.write(`markdown documents found: ${result.totalMarkdownDocuments}\n`);
|
|
30
36
|
process.stdout.write(`covered directories: ${result.coveredDirectories}\n`);
|
|
31
37
|
process.stdout.write(`uncovered directories: ${result.uncoveredDirectories}\n`);
|
|
32
|
-
process.stdout.write(`xdocs
|
|
38
|
+
process.stdout.write(`xdocs descriptors found: ${result.xdocsFiles.length}\n`);
|
|
33
39
|
if (result.xdocsFiles.length > 0) {
|
|
34
40
|
process.stdout.write(`\nfiles:\n`);
|
|
35
41
|
for (const file of result.xdocsFiles) {
|
|
@@ -42,6 +48,11 @@ export const runScan = async (options, _parsed) => {
|
|
|
42
48
|
process.stdout.write(` error: ${error}\n`);
|
|
43
49
|
}
|
|
44
50
|
}
|
|
51
|
+
if (options.verbose && file.documents.length > 0) {
|
|
52
|
+
for (const document of file.documents) {
|
|
53
|
+
process.stdout.write(` document: ${document.name}\n`);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
45
56
|
}
|
|
46
57
|
}
|
|
47
58
|
if (options.verbose && result.uncoveredPaths.length > 0) {
|
package/library/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../source/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAA+B,eAAe,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAU/H,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,EAAE,kBAIpC,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,GAAI,KAAK,cAAc,CAAC,QAAQ,CAAC,KAAG,kBAgBtE,CAAA;AAQD,2CAA2C;AAC3C,eAAO,MAAM,cAAc,GAAU,KAAK,MAAM,EAAE,eAAe,MAAM,KAAG,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,cAAc,CAAA;CAAE,CAaxH,CAAA;AAED,kDAAkD;AAClD,eAAO,MAAM,UAAU,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,WAAW,CAS9E,CAAA;AAED,2DAA2D;AAC3D,eAAO,MAAM,oBAAoB,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,WAAW,CAOxF,CAAA;AAED,4DAA4D;AAC5D,eAAO,MAAM,eAAe,GAAI,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,aAAa,MAAM,KAAG,
|
|
1
|
+
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../source/config.ts"],"names":[],"mappings":"AAAA;;GAEG;AAKH,OAAO,KAAK,EAAE,kBAAkB,EAA+B,eAAe,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAU/H,gFAAgF;AAChF,eAAO,MAAM,sBAAsB,EAAE,kBAIpC,CAAA;AAED,yEAAyE;AACzE,eAAO,MAAM,sBAAsB,GAAI,KAAK,cAAc,CAAC,QAAQ,CAAC,KAAG,kBAgBtE,CAAA;AAQD,2CAA2C;AAC3C,eAAO,MAAM,cAAc,GAAU,KAAK,MAAM,EAAE,eAAe,MAAM,KAAG,OAAO,CAAC;IAAE,IAAI,CAAC,EAAE,MAAM,CAAC;IAAC,GAAG,CAAC,EAAE,cAAc,CAAA;CAAE,CAaxH,CAAA;AAED,kDAAkD;AAClD,eAAO,MAAM,UAAU,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,WAAW,CAS9E,CAAA;AAED,2DAA2D;AAC3D,eAAO,MAAM,oBAAoB,GAAU,SAAS,eAAe,KAAG,OAAO,CAAC,WAAW,CAOxF,CAAA;AAED,4DAA4D;AAC5D,eAAO,MAAM,eAAe,GAAI,KAAK,cAAc,EAAE,KAAK,MAAM,EAAE,aAAa,MAAM,KAAG,WAkCvF,CAAA;AAED,sDAAsD;AACtD,eAAO,MAAM,aAAa,GAAI,KAAK,MAAM,KAAG,WAQ1C,CAAA;AAEF,sDAAsD;AACtD,eAAO,MAAM,0BAA0B,GAAI,KAAK,MAAM,KAAG,MAsBxD,CAAA;AAED,mDAAmD;AACnD,eAAO,MAAM,kBAAkB,GAAU,KAAK,MAAM,EAAE,mBAAiB,KAAG,OAAO,CAAC,MAAM,CASvF,CAAA;AAuBD,sDAAsD;AACtD,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,EAAE,MAAM,MAAM,WACP,CAAA"}
|
package/library/config.js
CHANGED
|
@@ -5,7 +5,7 @@ import { existsSync } from 'node:fs';
|
|
|
5
5
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
6
6
|
import { basename, isAbsolute, resolve } from 'node:path';
|
|
7
7
|
import { XDocsError } from './errors.js';
|
|
8
|
-
const DEFAULT_EXTENSIONS = ['.
|
|
8
|
+
const DEFAULT_EXTENSIONS = ['.xdocs.md'];
|
|
9
9
|
const DEFAULT_EXCLUDE = ['node_modules', '.git', 'dist', 'build', 'library', 'bin', 'bundle'];
|
|
10
10
|
const DEFAULT_AI_MODE = 'prompt';
|
|
11
11
|
const CONFIG_FILENAME = 'xdocs.config.toml';
|
|
@@ -83,6 +83,10 @@ export const normalizeConfig = (raw, cwd, configPath) => {
|
|
|
83
83
|
if (!Array.isArray(extensions) || extensions.some((ext) => typeof ext !== 'string')) {
|
|
84
84
|
throw new XDocsError('Invalid extensions.supported. Expected an array of strings.');
|
|
85
85
|
}
|
|
86
|
+
const normalizedExtensions = extensions.map((ext) => ext.toLowerCase());
|
|
87
|
+
if (normalizedExtensions.length !== 1 || normalizedExtensions[0] !== '.xdocs.md') {
|
|
88
|
+
throw new XDocsError('Invalid extensions.supported. xdocs supports only named "*.xdocs.md" descriptor files.');
|
|
89
|
+
}
|
|
86
90
|
const exclude = raw.scan?.exclude ?? DEFAULT_EXCLUDE;
|
|
87
91
|
if (!Array.isArray(exclude) || exclude.some((dir) => typeof dir !== 'string')) {
|
|
88
92
|
throw new XDocsError('Invalid scan.exclude. Expected an array of strings.');
|
|
@@ -91,7 +95,7 @@ export const normalizeConfig = (raw, cwd, configPath) => {
|
|
|
91
95
|
schema: 1,
|
|
92
96
|
cwd,
|
|
93
97
|
configPath,
|
|
94
|
-
extensions: { supported:
|
|
98
|
+
extensions: { supported: DEFAULT_EXTENSIONS },
|
|
95
99
|
ai: { mode: aiMode ?? DEFAULT_AI_MODE },
|
|
96
100
|
scan: { exclude },
|
|
97
101
|
project: { name: raw.project?.name ?? basename(cwd) },
|
|
@@ -114,7 +118,7 @@ export const createDefaultConfigContent = (cwd) => {
|
|
|
114
118
|
return `schema = 1
|
|
115
119
|
|
|
116
120
|
[extensions]
|
|
117
|
-
supported = [".
|
|
121
|
+
supported = [".xdocs.md"]
|
|
118
122
|
|
|
119
123
|
[ai]
|
|
120
124
|
mode = "prompt"
|
package/library/discovery.d.ts
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* @copyright Copyright (c) 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
|
|
3
3
|
*/
|
|
4
4
|
import type { XDocsConfig, XDocsFile, XDocsScanResult } from './types.js';
|
|
5
|
-
/** Scan the project for xdocs files
|
|
5
|
+
/** Scan the project for xdocs descriptor files and sibling Markdown documents. */
|
|
6
6
|
export declare const scanProject: (config: XDocsConfig) => Promise<XDocsScanResult>;
|
|
7
|
-
/** Check if a file path
|
|
8
|
-
export declare const isXDocsFile: (filePath: string,
|
|
9
|
-
/**
|
|
7
|
+
/** Check if a file path is a root index or xdocs descriptor file. */
|
|
8
|
+
export declare const isXDocsFile: (filePath: string, _extensions?: string[]) => boolean;
|
|
9
|
+
/** Check if a file path is an xdocs module descriptor. */
|
|
10
|
+
export declare const isXDocsDescriptorFile: (filePath: string) => boolean;
|
|
11
|
+
/** Check if a file path is a companion Markdown document, not an xdocs descriptor. */
|
|
12
|
+
export declare const isPlainMarkdownDocument: (filePath: string) => boolean;
|
|
13
|
+
/** Scan a specific directory (not recursive) for xdocs descriptors. */
|
|
10
14
|
export declare const scanDirectory: (dirPath: string, config: XDocsConfig) => Promise<XDocsFile[]>;
|
|
11
15
|
/** List all files in a directory (non-recursive). */
|
|
12
16
|
export declare const listDirectoryFiles: (dirPath: string, config: XDocsConfig) => Promise<string[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../source/discovery.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,
|
|
1
|
+
{"version":3,"file":"discovery.d.ts","sourceRoot":"","sources":["../source/discovery.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,WAAW,EAAE,SAAS,EAAyB,eAAe,EAAE,MAAM,YAAY,CAAA;AAMhG,kFAAkF;AAClF,eAAO,MAAM,WAAW,GAAU,QAAQ,WAAW,KAAG,OAAO,CAAC,eAAe,CA4D9E,CAAA;AAED,qEAAqE;AACrE,eAAO,MAAM,WAAW,GAAI,UAAU,MAAM,EAAE,cAAa,MAAM,EAAiC,KAAG,OACtB,CAAA;AAE/E,0DAA0D;AAC1D,eAAO,MAAM,qBAAqB,GAAI,UAAU,MAAM,KAAG,OACc,CAAA;AAEvE,sFAAsF;AACtF,eAAO,MAAM,uBAAuB,GAAI,UAAU,MAAM,KAAG,OAI1D,CAAA;AAmCD,uEAAuE;AACvE,eAAO,MAAM,aAAa,GAAU,SAAS,MAAM,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,SAAS,EAAE,CA6B7F,CAAA;AAED,qDAAqD;AACrD,eAAO,MAAM,kBAAkB,GAAU,SAAS,MAAM,EAAE,QAAQ,WAAW,KAAG,OAAO,CAAC,MAAM,EAAE,CAiB/F,CAAA"}
|
package/library/discovery.js
CHANGED
|
@@ -2,24 +2,32 @@
|
|
|
2
2
|
* @copyright Copyright (c) 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
|
|
3
3
|
*/
|
|
4
4
|
import { readdir, stat } from 'node:fs/promises';
|
|
5
|
-
import { join, relative } from 'node:path';
|
|
5
|
+
import { basename, dirname, join, relative, resolve } from 'node:path';
|
|
6
6
|
import { parseXDocsFile } from './metadata.js';
|
|
7
|
-
|
|
7
|
+
const XDOCS_DESCRIPTOR_EXTENSION = '.xdocs.md';
|
|
8
|
+
const ROOT_XDOCS_FILENAME = 'XDOCS.md';
|
|
9
|
+
/** Scan the project for xdocs descriptor files and sibling Markdown documents. */
|
|
8
10
|
export const scanProject = async (config) => {
|
|
9
11
|
const xdocsFiles = [];
|
|
12
|
+
const markdownDocuments = [];
|
|
13
|
+
const markdownDocumentsByDirectory = new Map();
|
|
10
14
|
const allDirectories = [];
|
|
11
|
-
const coveredDirectories = new Set();
|
|
12
15
|
let totalFiles = 0;
|
|
13
|
-
await walkDirectory(config.cwd, config, async (filePath,
|
|
16
|
+
await walkDirectory(config.cwd, config, async (filePath, _dir) => {
|
|
14
17
|
totalFiles += 1;
|
|
15
|
-
if (
|
|
18
|
+
if (isXDocsDescriptorFile(filePath)) {
|
|
16
19
|
const file = await parseXDocsFile(filePath, config.cwd);
|
|
17
20
|
xdocsFiles.push(file);
|
|
18
|
-
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (isPlainMarkdownDocument(filePath)) {
|
|
24
|
+
const document = createMarkdownDocument(filePath, config.cwd);
|
|
25
|
+
markdownDocuments.push(document);
|
|
26
|
+
addMarkdownDocument(markdownDocumentsByDirectory, document.directory, document);
|
|
19
27
|
}
|
|
20
28
|
}, allDirectories);
|
|
21
29
|
// The root XDOCS.md is always recognized
|
|
22
|
-
const rootXDocs = join(config.cwd,
|
|
30
|
+
const rootXDocs = join(config.cwd, ROOT_XDOCS_FILENAME);
|
|
23
31
|
const rootAlreadyFound = xdocsFiles.some((f) => f.path === rootXDocs);
|
|
24
32
|
if (!rootAlreadyFound) {
|
|
25
33
|
try {
|
|
@@ -27,30 +35,43 @@ export const scanProject = async (config) => {
|
|
|
27
35
|
if (rootStat.isFile()) {
|
|
28
36
|
const file = await parseXDocsFile(rootXDocs, config.cwd);
|
|
29
37
|
xdocsFiles.push(file);
|
|
30
|
-
coveredDirectories.add(config.cwd);
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
catch {
|
|
34
41
|
// Root XDOCS.md does not exist, that's fine
|
|
35
42
|
}
|
|
36
43
|
}
|
|
44
|
+
enrichDescriptorFiles(xdocsFiles, markdownDocumentsByDirectory);
|
|
45
|
+
const coveredDirectories = new Set();
|
|
46
|
+
if (xdocsFiles.some((file) => file.path === rootXDocs))
|
|
47
|
+
coveredDirectories.add(config.cwd);
|
|
48
|
+
for (const file of xdocsFiles) {
|
|
49
|
+
if (file.path === rootXDocs)
|
|
50
|
+
continue;
|
|
51
|
+
if (file.valid)
|
|
52
|
+
coveredDirectories.add(file.directory);
|
|
53
|
+
}
|
|
37
54
|
const uncoveredPaths = allDirectories.filter((dir) => !coveredDirectories.has(dir));
|
|
38
55
|
return {
|
|
39
56
|
totalFiles,
|
|
40
57
|
totalDirectories: allDirectories.length,
|
|
58
|
+
totalMarkdownDocuments: markdownDocuments.length,
|
|
41
59
|
coveredDirectories: coveredDirectories.size,
|
|
42
60
|
uncoveredDirectories: uncoveredPaths.length,
|
|
43
61
|
xdocsFiles,
|
|
62
|
+
markdownDocuments,
|
|
44
63
|
uncoveredPaths,
|
|
45
64
|
};
|
|
46
65
|
};
|
|
47
|
-
/** Check if a file path
|
|
48
|
-
export const isXDocsFile = (filePath,
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
66
|
+
/** Check if a file path is a root index or xdocs descriptor file. */
|
|
67
|
+
export const isXDocsFile = (filePath, _extensions = [XDOCS_DESCRIPTOR_EXTENSION]) => basename(filePath) === ROOT_XDOCS_FILENAME || isXDocsDescriptorFile(filePath);
|
|
68
|
+
/** Check if a file path is an xdocs module descriptor. */
|
|
69
|
+
export const isXDocsDescriptorFile = (filePath) => basename(filePath).toLowerCase().endsWith(XDOCS_DESCRIPTOR_EXTENSION);
|
|
70
|
+
/** Check if a file path is a companion Markdown document, not an xdocs descriptor. */
|
|
71
|
+
export const isPlainMarkdownDocument = (filePath) => {
|
|
72
|
+
const name = basename(filePath);
|
|
73
|
+
const lower = name.toLowerCase();
|
|
74
|
+
return lower.endsWith('.md') && lower !== 'xdocs.md' && !isXDocsDescriptorFile(filePath);
|
|
54
75
|
};
|
|
55
76
|
/** Recursively walk a directory, skipping excluded directories. */
|
|
56
77
|
const walkDirectory = async (dir, config, onFile, allDirectories) => {
|
|
@@ -76,9 +97,10 @@ const walkDirectory = async (dir, config, onFile, allDirectories) => {
|
|
|
76
97
|
};
|
|
77
98
|
/** Check if a directory name is in the exclude list. */
|
|
78
99
|
const isExcluded = (name, exclude) => exclude.includes(name) || name.startsWith('.');
|
|
79
|
-
/** Scan a specific directory (not recursive) for xdocs
|
|
100
|
+
/** Scan a specific directory (not recursive) for xdocs descriptors. */
|
|
80
101
|
export const scanDirectory = async (dirPath, config) => {
|
|
81
102
|
const files = [];
|
|
103
|
+
const markdownDocumentsByDirectory = new Map();
|
|
82
104
|
let entries;
|
|
83
105
|
try {
|
|
84
106
|
entries = await readdir(dirPath, { withFileTypes: true });
|
|
@@ -90,11 +112,17 @@ export const scanDirectory = async (dirPath, config) => {
|
|
|
90
112
|
if (!entry.isFile())
|
|
91
113
|
continue;
|
|
92
114
|
const fullPath = join(dirPath, entry.name);
|
|
93
|
-
if (
|
|
115
|
+
if (isXDocsDescriptorFile(fullPath) || isRootXDocsFile(fullPath, config.cwd)) {
|
|
94
116
|
const file = await parseXDocsFile(fullPath, config.cwd);
|
|
95
117
|
files.push(file);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
if (isPlainMarkdownDocument(fullPath)) {
|
|
121
|
+
const document = createMarkdownDocument(fullPath, config.cwd);
|
|
122
|
+
addMarkdownDocument(markdownDocumentsByDirectory, document.directory, document);
|
|
96
123
|
}
|
|
97
124
|
}
|
|
125
|
+
enrichDescriptorFiles(files, markdownDocumentsByDirectory);
|
|
98
126
|
return files;
|
|
99
127
|
};
|
|
100
128
|
/** List all files in a directory (non-recursive). */
|
|
@@ -116,3 +144,65 @@ export const listDirectoryFiles = async (dirPath, config) => {
|
|
|
116
144
|
}
|
|
117
145
|
return result;
|
|
118
146
|
};
|
|
147
|
+
const isRootXDocsFile = (filePath, cwd) => resolve(filePath) === resolve(cwd, ROOT_XDOCS_FILENAME);
|
|
148
|
+
const createMarkdownDocument = (filePath, cwd) => ({
|
|
149
|
+
path: filePath,
|
|
150
|
+
relativePath: relative(cwd, filePath),
|
|
151
|
+
directory: dirname(filePath),
|
|
152
|
+
name: basename(filePath),
|
|
153
|
+
});
|
|
154
|
+
const addMarkdownDocument = (documentsByDirectory, directory, document) => {
|
|
155
|
+
const documents = documentsByDirectory.get(directory) ?? [];
|
|
156
|
+
documents.push(document);
|
|
157
|
+
documentsByDirectory.set(directory, documents);
|
|
158
|
+
};
|
|
159
|
+
const enrichDescriptorFiles = (files, documentsByDirectory) => {
|
|
160
|
+
const descriptorsByDirectory = new Map();
|
|
161
|
+
for (const file of files) {
|
|
162
|
+
if (!isXDocsDescriptorFile(file.path))
|
|
163
|
+
continue;
|
|
164
|
+
const descriptors = descriptorsByDirectory.get(file.directory) ?? [];
|
|
165
|
+
descriptors.push(file);
|
|
166
|
+
descriptorsByDirectory.set(file.directory, descriptors);
|
|
167
|
+
}
|
|
168
|
+
for (const descriptors of descriptorsByDirectory.values()) {
|
|
169
|
+
if (descriptors.length <= 1)
|
|
170
|
+
continue;
|
|
171
|
+
for (const file of descriptors) {
|
|
172
|
+
file.errors.push('Multiple xdocs descriptors found in this directory. Keep exactly one named "*.xdocs.md" file per directory.');
|
|
173
|
+
file.valid = false;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
for (const file of files) {
|
|
177
|
+
if (!isXDocsDescriptorFile(file.path))
|
|
178
|
+
continue;
|
|
179
|
+
file.documents = documentsByDirectory.get(file.directory) ?? [];
|
|
180
|
+
validateDocumentReferences(file);
|
|
181
|
+
if (file.errors.length > 0)
|
|
182
|
+
file.valid = false;
|
|
183
|
+
}
|
|
184
|
+
};
|
|
185
|
+
const validateDocumentReferences = (file) => {
|
|
186
|
+
if (!file.metadata)
|
|
187
|
+
return;
|
|
188
|
+
const actualDocuments = new Set(file.documents.map((document) => document.name));
|
|
189
|
+
const declaredDocuments = file.metadata.documents;
|
|
190
|
+
for (const document of file.documents) {
|
|
191
|
+
if (Object.hasOwn(declaredDocuments, document.name))
|
|
192
|
+
continue;
|
|
193
|
+
file.errors.push(`Undocumented Markdown document: "${document.name}" must be listed in the "documents" metadata map.`);
|
|
194
|
+
}
|
|
195
|
+
for (const name of Object.keys(declaredDocuments)) {
|
|
196
|
+
if (!isPlainMarkdownDocumentName(name)) {
|
|
197
|
+
file.errors.push(`Invalid document entry: "${name}" must be a sibling plain "*.md" filename, not an xdocs descriptor or path.`);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (!actualDocuments.has(name)) {
|
|
201
|
+
file.errors.push(`Missing Markdown document: "${name}" is listed in metadata but does not exist beside the descriptor.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
};
|
|
205
|
+
const isPlainMarkdownDocumentName = (name) => {
|
|
206
|
+
const lower = name.toLowerCase();
|
|
207
|
+
return !name.includes('/') && !name.includes('\\') && lower.endsWith('.md') && lower !== 'xdocs.md' && !lower.endsWith(XDOCS_DESCRIPTOR_EXTENSION);
|
|
208
|
+
};
|
package/library/flags.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ import type { XDocsParsedArgs } from './types.js';
|
|
|
10
10
|
* - Long flags with value: `--name=write` or `--name write`
|
|
11
11
|
* - Long boolean flags: `--verbose`
|
|
12
12
|
* - Short flags: `-h`, `-v`
|
|
13
|
-
* - List values as comma-separated: `--extensions=.
|
|
13
|
+
* - List values as comma-separated: `--extensions=.xdocs.md,.custom.md`
|
|
14
14
|
* - Positional arguments after the command
|
|
15
15
|
*/
|
|
16
16
|
export declare const parseArgs: (rawArgs: string[]) => XDocsParsedArgs;
|
package/library/flags.js
CHANGED
|
@@ -24,7 +24,7 @@ const listFlags = new Set([
|
|
|
24
24
|
* - Long flags with value: `--name=write` or `--name write`
|
|
25
25
|
* - Long boolean flags: `--verbose`
|
|
26
26
|
* - Short flags: `-h`, `-v`
|
|
27
|
-
* - List values as comma-separated: `--extensions=.
|
|
27
|
+
* - List values as comma-separated: `--extensions=.xdocs.md,.custom.md`
|
|
28
28
|
* - Positional arguments after the command
|
|
29
29
|
*/
|
|
30
30
|
export const parseArgs = (rawArgs) => {
|
package/library/guiho-xdocs.d.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* @copyright Copyright (c) 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
|
|
3
3
|
*/
|
|
4
4
|
export { XDocsError, invariant } from './errors.js';
|
|
5
|
-
export type { XDocsAgentAutomationResult, XDocsAgentSettings, XDocsAgentTool, XDocsAgentsInstructionsResult, XDocsAiMode, XDocsCliOptions, XDocsCommand, XDocsConfig, XDocsFile, XDocsFormat, XDocsMetadata, XDocsParsedArgs, XDocsPrompt, XDocsPromptName, XDocsRawConfig, XDocsScanResult, XDocsSkillInstallResult, XDocsSkillScope, XDocsTreeNode, XDocsTreeValidation, } from './types.js';
|
|
5
|
+
export type { XDocsAgentAutomationResult, XDocsAgentSettings, XDocsAgentTool, XDocsAgentsInstructionsResult, XDocsAiMode, XDocsCliOptions, XDocsCommand, XDocsConfig, XDocsFile, XDocsFormat, XDocsMarkdownDocument, XDocsMetadata, XDocsParsedArgs, XDocsPrompt, XDocsPromptName, XDocsRawConfig, XDocsScanResult, XDocsSkillInstallResult, XDocsSkillScope, XDocsTreeNode, XDocsTreeValidation, } from './types.js';
|
|
6
6
|
export { parseArgs, stringFlag, booleanFlag, listFlag } from './flags.js';
|
|
7
7
|
export { createDefaultConfigContent, DEFAULT_AGENT_SETTINGS, defaultConfig, discoverConfig, loadConfig, loadConfigOrDefaults, normalizeAgentSettings, normalizeConfig, resolvePath, writeDefaultConfig, } from './config.js';
|
|
8
|
-
export { isXDocsFile, listDirectoryFiles, scanDirectory, scanProject } from './discovery.js';
|
|
8
|
+
export { isPlainMarkdownDocument, isXDocsDescriptorFile, isXDocsFile, listDirectoryFiles, scanDirectory, scanProject } from './discovery.js';
|
|
9
9
|
export { extractFrontmatter, parseXDocsFile, validateMetadata } from './metadata.js';
|
|
10
10
|
export { buildTree, renderTree, renderTreeMarkdown, validateTree } from './tree.js';
|
|
11
11
|
export { readPackageVersion, showCommandHelp, showHelp, showVersion } from './help.js';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"guiho-xdocs.d.ts","sourceRoot":"","sources":["../source/guiho-xdocs.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAGnD,YAAY,EACV,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,6BAA6B,EAC7B,WAAW,EACX,eAAe,EACf,YAAY,EACZ,WAAW,EACX,SAAS,EACT,WAAW,EACX,aAAa,EACb,eAAe,EACf,WAAW,EACX,eAAe,EACf,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,eAAe,EACf,aAAa,EACb,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAGzE,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,EACtB,aAAa,EACb,cAAc,EACd,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,WAAW,EACX,kBAAkB,GACnB,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;
|
|
1
|
+
{"version":3,"file":"guiho-xdocs.d.ts","sourceRoot":"","sources":["../source/guiho-xdocs.ts"],"names":[],"mappings":"AAAA;;GAEG;AAGH,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,aAAa,CAAA;AAGnD,YAAY,EACV,0BAA0B,EAC1B,kBAAkB,EAClB,cAAc,EACd,6BAA6B,EAC7B,WAAW,EACX,eAAe,EACf,YAAY,EACZ,WAAW,EACX,SAAS,EACT,WAAW,EACX,qBAAqB,EACrB,aAAa,EACb,eAAe,EACf,WAAW,EACX,eAAe,EACf,cAAc,EACd,eAAe,EACf,uBAAuB,EACvB,eAAe,EACf,aAAa,EACb,mBAAmB,GACpB,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,WAAW,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAA;AAGzE,OAAO,EACL,0BAA0B,EAC1B,sBAAsB,EACtB,aAAa,EACb,cAAc,EACd,UAAU,EACV,oBAAoB,EACpB,sBAAsB,EACtB,eAAe,EACf,WAAW,EACX,kBAAkB,GACnB,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,uBAAuB,EAAE,qBAAqB,EAAE,WAAW,EAAE,kBAAkB,EAAE,aAAa,EAAE,WAAW,EAAE,MAAM,gBAAgB,CAAA;AAG5I,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAA;AAGpF,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,kBAAkB,EAAE,YAAY,EAAE,MAAM,WAAW,CAAA;AAGnF,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,QAAQ,EAAE,WAAW,EAAE,MAAM,WAAW,CAAA;AAGtF,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,cAAc,CAAA;AAGjE,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,cAAc,EACd,YAAY,EACZ,aAAa,EACb,gBAAgB,EAChB,qBAAqB,EACrB,eAAe,EACf,gBAAgB,EAChB,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,kBAAkB,EAClB,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,iBAAiB,EACjB,cAAc,EACd,iBAAiB,GAClB,MAAM,aAAa,CAAA;AAGpB,OAAO,EAAE,MAAM,EAAE,uBAAuB,EAAE,MAAM,UAAU,CAAA"}
|
package/library/guiho-xdocs.js
CHANGED
|
@@ -8,7 +8,7 @@ export { parseArgs, stringFlag, booleanFlag, listFlag } from './flags.js';
|
|
|
8
8
|
// Config
|
|
9
9
|
export { createDefaultConfigContent, DEFAULT_AGENT_SETTINGS, defaultConfig, discoverConfig, loadConfig, loadConfigOrDefaults, normalizeAgentSettings, normalizeConfig, resolvePath, writeDefaultConfig, } from './config.js';
|
|
10
10
|
// Discovery
|
|
11
|
-
export { isXDocsFile, listDirectoryFiles, scanDirectory, scanProject } from './discovery.js';
|
|
11
|
+
export { isPlainMarkdownDocument, isXDocsDescriptorFile, isXDocsFile, listDirectoryFiles, scanDirectory, scanProject } from './discovery.js';
|
|
12
12
|
// Metadata
|
|
13
13
|
export { extractFrontmatter, parseXDocsFile, validateMetadata } from './metadata.js';
|
|
14
14
|
// Tree
|
package/library/help.js
CHANGED
|
@@ -26,12 +26,12 @@ Usage: xdocs <command> [options]
|
|
|
26
26
|
|
|
27
27
|
Commands:
|
|
28
28
|
init Initialize xdocs in a project
|
|
29
|
-
scan Scan
|
|
29
|
+
scan Scan for xdocs descriptors and Markdown documents
|
|
30
30
|
generate [path] Generate documentation for a directory or the project
|
|
31
31
|
prompt Output a ready-made prompt for AI
|
|
32
|
-
merge [path] Merge xdocs
|
|
32
|
+
merge [path] Merge xdocs descriptors from a directory into one file
|
|
33
33
|
tree Display the project hierarchy tree
|
|
34
|
-
list [path] List files
|
|
34
|
+
list [path] List documented files and documents
|
|
35
35
|
agents Install the guiho-s-xdocs skill and AGENTS.md instructions
|
|
36
36
|
|
|
37
37
|
Global Flags:
|
|
@@ -82,12 +82,13 @@ Flags:
|
|
|
82
82
|
--verbose Show detailed output
|
|
83
83
|
`.trim(),
|
|
84
84
|
scan: `
|
|
85
|
-
xdocs scan - Scan the project for xdocs
|
|
85
|
+
xdocs scan - Scan the project for xdocs descriptors
|
|
86
86
|
|
|
87
87
|
Usage: xdocs scan
|
|
88
88
|
|
|
89
|
-
Walks every directory and subdirectory (respecting exclude rules),
|
|
90
|
-
|
|
89
|
+
Walks every directory and subdirectory (respecting exclude rules), finds named
|
|
90
|
+
*.xdocs.md descriptor files, and reports plain sibling *.md documents that those
|
|
91
|
+
descriptors must list in their documents metadata.
|
|
91
92
|
|
|
92
93
|
Flags:
|
|
93
94
|
--format <format> Output format: text, json (default: text)
|
|
@@ -100,7 +101,7 @@ xdocs generate - Generate documentation
|
|
|
100
101
|
|
|
101
102
|
Usage: xdocs generate [path]
|
|
102
103
|
|
|
103
|
-
When a path is given, generates
|
|
104
|
+
When a path is given, generates documentation for that directory/module.
|
|
104
105
|
When no path is given, generates documentation for the entire project.
|
|
105
106
|
|
|
106
107
|
Flags:
|
|
@@ -122,7 +123,7 @@ a specific xdocs task. Both flag styles are supported:
|
|
|
122
123
|
|
|
123
124
|
Available prompts:
|
|
124
125
|
write How to scan a directory and write xdocs documentation
|
|
125
|
-
update How to update existing xdocs
|
|
126
|
+
update How to update existing xdocs descriptors after code changes
|
|
126
127
|
agents How to update AGENTS.md with xdocs instructions
|
|
127
128
|
generate How to generate comprehensive documentation
|
|
128
129
|
|
|
@@ -132,12 +133,12 @@ Flags:
|
|
|
132
133
|
--config <path> Path to xdocs.config.toml
|
|
133
134
|
`.trim(),
|
|
134
135
|
merge: `
|
|
135
|
-
xdocs merge - Merge xdocs
|
|
136
|
+
xdocs merge - Merge xdocs descriptors into a single file
|
|
136
137
|
|
|
137
138
|
Usage: xdocs merge [path]
|
|
138
139
|
|
|
139
|
-
Takes all xdocs files within the given directory and produces
|
|
140
|
-
one consolidated Markdown document.
|
|
140
|
+
Takes all xdocs descriptor files within the given directory and produces
|
|
141
|
+
one consolidated Markdown document, including listed companion documents.
|
|
141
142
|
|
|
142
143
|
Flags:
|
|
143
144
|
--output <path> Output file path (default: stdout)
|
|
@@ -150,7 +151,7 @@ xdocs tree - Display the project hierarchy tree
|
|
|
150
151
|
|
|
151
152
|
Usage: xdocs tree
|
|
152
153
|
|
|
153
|
-
Scans all xdocs
|
|
154
|
+
Scans all xdocs descriptors, reads their metadata, and assembles the
|
|
154
155
|
parent-child hierarchy. Shows modules only, not individual files.
|
|
155
156
|
|
|
156
157
|
Flags:
|
|
@@ -161,12 +162,12 @@ Flags:
|
|
|
161
162
|
--verbose Show detailed output
|
|
162
163
|
`.trim(),
|
|
163
164
|
list: `
|
|
164
|
-
xdocs list - List files
|
|
165
|
+
xdocs list - List documented files and documents
|
|
165
166
|
|
|
166
167
|
Usage: xdocs list [path]
|
|
167
168
|
|
|
168
|
-
Lists every file in the given scope with
|
|
169
|
-
|
|
169
|
+
Lists every source file and companion Markdown document in the given scope with
|
|
170
|
+
a short description pulled from xdocs metadata.
|
|
170
171
|
|
|
171
172
|
Flags:
|
|
172
173
|
--format <format> Output format: text, json (default: text)
|
package/library/metadata.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* @copyright Copyright (c) 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
|
|
3
3
|
*/
|
|
4
4
|
import type { XDocsFile, XDocsMetadata } from './types.js';
|
|
5
|
-
/** Parse an xdocs
|
|
5
|
+
/** Parse an xdocs descriptor from disk into an XDocsFile object. */
|
|
6
6
|
export declare const parseXDocsFile: (filePath: string, cwd: string) => Promise<XDocsFile>;
|
|
7
7
|
/** Extract YAML frontmatter and body from a Markdown string. */
|
|
8
8
|
export declare const extractFrontmatter: (content: string) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../source/metadata.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1D,
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../source/metadata.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,YAAY,CAAA;AAE1D,oEAAoE;AACpE,eAAO,MAAM,cAAc,GAAU,UAAU,MAAM,EAAE,KAAK,MAAM,KAAG,OAAO,CAAC,SAAS,CA0CrF,CAAA;AAED,gEAAgE;AAChE,eAAO,MAAM,kBAAkB,GAAI,SAAS,MAAM,KAAG;IAAE,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAiB9F,CAAA;AAED,6CAA6C;AAC7C,eAAO,MAAM,gBAAgB,GAAI,QAAQ,OAAO,KAAG;IAAE,KAAK,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,aAAa,CAAC;IAAC,MAAM,EAAE,KAAK,EAAE,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,MAAM,EAAE,CAAA;CAwE9J,CAAA"}
|
package/library/metadata.js
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* @copyright Copyright (c) 2026 GUIHO Technologies as represented by Cristóvão GUIHO. All Rights Reserved.
|
|
3
3
|
*/
|
|
4
4
|
import { readFile } from 'node:fs/promises';
|
|
5
|
-
import { dirname, relative } from 'node:path';
|
|
6
|
-
/** Parse an xdocs
|
|
5
|
+
import { basename, dirname, relative } from 'node:path';
|
|
6
|
+
/** Parse an xdocs descriptor from disk into an XDocsFile object. */
|
|
7
7
|
export const parseXDocsFile = async (filePath, cwd) => {
|
|
8
8
|
const content = await readFile(filePath, 'utf8');
|
|
9
9
|
const errors = [];
|
|
10
|
+
if (basename(filePath).toLowerCase() === '.xdocs.md') {
|
|
11
|
+
errors.push('Invalid xdocs descriptor filename. Use a named file such as "authentication.xdocs.md"; ".xdocs.md" is only the extension.');
|
|
12
|
+
}
|
|
10
13
|
const { frontmatter, body } = extractFrontmatter(content);
|
|
11
14
|
let metadata = null;
|
|
12
15
|
if (frontmatter) {
|
|
@@ -33,6 +36,7 @@ export const parseXDocsFile = async (filePath, cwd) => {
|
|
|
33
36
|
relativePath: relative(cwd, filePath),
|
|
34
37
|
directory: dirname(filePath),
|
|
35
38
|
metadata,
|
|
39
|
+
documents: [],
|
|
36
40
|
body,
|
|
37
41
|
valid: metadata !== null && errors.length === 0,
|
|
38
42
|
errors,
|
|
@@ -74,15 +78,24 @@ export const validateMetadata = (parsed) => {
|
|
|
74
78
|
else if (record['children'].some((child) => typeof child !== 'string')) {
|
|
75
79
|
errors.push('Invalid "children" field. All entries must be strings.');
|
|
76
80
|
}
|
|
77
|
-
if (
|
|
81
|
+
if (!isStringMap(record['files'])) {
|
|
78
82
|
errors.push('Missing or invalid "files" field. Expected an object mapping filenames to descriptions.');
|
|
79
83
|
}
|
|
84
|
+
if (!isStringMap(record['documents'])) {
|
|
85
|
+
errors.push('Missing or invalid "documents" field. Expected an object mapping sibling Markdown filenames to descriptions.');
|
|
86
|
+
}
|
|
80
87
|
if (!Array.isArray(record['tags'])) {
|
|
81
88
|
errors.push('Missing or invalid "tags" field. Expected an array of strings.');
|
|
82
89
|
}
|
|
83
90
|
else if (record['tags'].some((tag) => typeof tag !== 'string')) {
|
|
84
91
|
errors.push('Invalid "tags" field. All entries must be strings.');
|
|
85
92
|
}
|
|
93
|
+
if (!Array.isArray(record['keywords'])) {
|
|
94
|
+
errors.push('Missing or invalid "keywords" field. Expected an array of strings.');
|
|
95
|
+
}
|
|
96
|
+
else if (record['keywords'].some((keyword) => typeof keyword !== 'string')) {
|
|
97
|
+
errors.push('Invalid "keywords" field. All entries must be strings.');
|
|
98
|
+
}
|
|
86
99
|
if (!Array.isArray(record['flags'])) {
|
|
87
100
|
errors.push('Missing or invalid "flags" field. Expected an array of strings.');
|
|
88
101
|
}
|
|
@@ -101,9 +114,16 @@ export const validateMetadata = (parsed) => {
|
|
|
101
114
|
parent: record['parent'] ?? null,
|
|
102
115
|
children: record['children'],
|
|
103
116
|
files: record['files'],
|
|
117
|
+
documents: record['documents'],
|
|
104
118
|
tags: record['tags'],
|
|
119
|
+
keywords: record['keywords'],
|
|
105
120
|
flags: record['flags'],
|
|
106
121
|
status: typeof record['status'] === 'string' ? record['status'] : undefined,
|
|
107
122
|
},
|
|
108
123
|
};
|
|
109
124
|
};
|
|
125
|
+
const isStringMap = (value) => {
|
|
126
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
127
|
+
return false;
|
|
128
|
+
return Object.entries(value).every(([key, description]) => typeof key === 'string' && typeof description === 'string');
|
|
129
|
+
};
|