@dotdotgod/cli 0.3.0 → 0.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dotdotgod/cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "CLI for dotdotgod validation, local documentation search, and graph indexing.",
5
5
  "type": "module",
6
6
  "license": "Elastic-2.0",
package/src/cli/usage.mjs CHANGED
@@ -36,6 +36,11 @@ Create dotdotgod.config.json from a bundled or user template. Without --template
36
36
  dotdotgod query <root> <query> [--limit n] [--json]
37
37
 
38
38
  Search shared project documentation with the resolved local or remote embedding provider.`;
39
+ case 'map':
40
+ return `Usage:
41
+ dotdotgod map <root> [--depth <positive-integer>] [--json]
42
+
43
+ Render the configured shared Markdown documentation map without modifying the project.`;
39
44
  case 'resolve':
40
45
  return `Usage:
41
46
  dotdotgod resolve <root> <ref> [--max-results n] [--include-archive] [--json]`;
@@ -73,6 +78,7 @@ Commands:
73
78
  init Initialize project memory files.
74
79
  config Inspect or initialize project configuration.
75
80
  query Search project documentation.
81
+ map Render the project documentation map.
76
82
  resolve Resolve a project reference.
77
83
  expand Expand references in a prompt.
78
84
  validate Validate project memory and documentation.
@@ -0,0 +1,41 @@
1
+ import { resolve } from 'node:path';
2
+ import { buildDocumentationMap } from '../memory/documentation-map.mjs';
3
+
4
+ function fail(json, code, message) {
5
+ if (json) console.log(JSON.stringify({ ok: false, error: { code, message } }, null, 2));
6
+ else console.error(message);
7
+ process.exit(2);
8
+ }
9
+
10
+ function parseMapOptions(argv) {
11
+ const options = { root: '.', depth: 5, json: false };
12
+ let rootSet = false;
13
+ for (let index = 0; index < argv.length; index += 1) {
14
+ const arg = argv[index];
15
+ if (arg === '--json') options.json = true;
16
+ }
17
+ for (let index = 0; index < argv.length; index += 1) {
18
+ const arg = argv[index];
19
+ if (arg === '--json') continue;
20
+ if (arg === '--depth') {
21
+ const value = argv[index + 1];
22
+ if (!value || !/^\d+$/.test(value) || Number(value) < 1) fail(options.json, 'INVALID_DEPTH', '--depth requires a positive integer.');
23
+ options.depth = Number(value);
24
+ index += 1;
25
+ } else if (!arg.startsWith('-') && !rootSet) {
26
+ options.root = arg;
27
+ rootSet = true;
28
+ } else if (!arg.startsWith('-')) fail(options.json, 'UNEXPECTED_ARGUMENT', `Unexpected argument: ${arg}`);
29
+ else fail(options.json, 'UNKNOWN_OPTION', `Unknown option: ${arg}`);
30
+ }
31
+ options.root = resolve(options.root);
32
+ return options;
33
+ }
34
+
35
+ export function runMap(argv) {
36
+ const options = parseMapOptions(argv);
37
+ const result = buildDocumentationMap(options.root, { depth: options.depth });
38
+ if (!result.ok) fail(options.json, result.error.code, result.error.message);
39
+ if (options.json) console.log(JSON.stringify(result, null, 2));
40
+ else console.log(result.tree);
41
+ }
package/src/core.mjs CHANGED
@@ -4,6 +4,7 @@ import { runValidate } from './validate/run.mjs';
4
4
  import { runIndex, runStatus } from './index/cache.mjs';
5
5
  import { runConfig } from './commands/config.mjs';
6
6
  import { runQuery } from './commands/query.mjs';
7
+ import { runMap } from './commands/map.mjs';
7
8
  import { runResolve, runExpand } from './reference/resolve.mjs';
8
9
  import { runTraceability } from './commands/traceability.mjs';
9
10
  import { runGraph } from './commands/graph.mjs';
@@ -19,6 +20,7 @@ export { buildCommunities, relationWeight } from './graph/communities.mjs';
19
20
  export { addEdge, addNode, compactGraph, expandGraph, graphStats, jsonSize, shardFile, writeJson } from './graph/store.mjs';
20
21
  export { cacheFile, collectIndexFiles, fingerprint, shouldIndexPath } from './index/files.mjs';
21
22
  export { buildMemoryAreas, detectCommandGuidance, detectPackageManager } from './memory/summary.mjs';
23
+ export { buildDocumentationMap, formatDocumentationTree } from './memory/documentation-map.mjs';
22
24
  export { isReadmeIndexPath, retrievalMetadataForPath } from './graph/metadata.mjs';
23
25
  export { buildGraph } from './graph/extract.mjs';
24
26
  export { addDeterministicSemanticEdges } from './graph/semantic.mjs';
@@ -29,6 +31,7 @@ export { buildChangedFileProfile, MAX_VECTOR_PROFILE_CHARS, MAX_VECTOR_PROFILE_F
29
31
  export { buildVectorImpactOverlay, VECTOR_RELATION_WEIGHT } from './impact/vector-overlay.mjs';
30
32
  export { formatCompactImpactOutput, formatYmlGraphImpactError, formatYmlImpactOutput } from './impact/format.mjs';
31
33
  export { runConfig } from './commands/config.mjs';
34
+ export { runMap } from './commands/map.mjs';
32
35
  export { buildVectorIndex, cosineScore, parseQueryOptions, queryDocumentation, rankVectorFiles, runQuery } from './commands/query.mjs';
33
36
  export { chunkMarkdown, collectDocumentationChunks, collectDocumentationMarkdown, textFingerprint } from './query/chunks.mjs';
34
37
  export { readVectorCache, VECTOR_DIMENSIONS, VECTOR_MODEL, VECTOR_SCHEMA_VERSION, profileFingerprint, vectorCachePaths, writeVectorCache } from './query/store.mjs';
@@ -51,6 +54,7 @@ export async function runCli(argv = process.argv.slice(2)) {
51
54
  else if (command === 'config') runConfig(args);
52
55
  else if (command === 'status') runStatus(args);
53
56
  else if (command === 'query') await runQuery(args);
57
+ else if (command === 'map') runMap(args);
54
58
  else if (command === 'resolve') runResolve(args);
55
59
  else if (command === 'expand') runExpand(args);
56
60
  else if (command === 'traceability') runTraceability(args);
@@ -0,0 +1,111 @@
1
+ import { existsSync, readdirSync, statSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { isSecretLikePathPattern, matchPathPattern, readMemoryConfig } from './config.mjs';
4
+
5
+ const SKIPPED_DIRECTORIES = new Set(['node_modules', 'dist', 'build', 'coverage', '.git', '.dotdotgod']);
6
+
7
+ function node() {
8
+ return { directories: new Map(), files: [] };
9
+ }
10
+
11
+ function buildTree(paths) {
12
+ const root = node();
13
+ for (const path of [...new Set(paths)].sort()) {
14
+ const parts = path.split('/');
15
+ let current = root;
16
+ for (const part of parts.slice(0, -1)) {
17
+ if (!current.directories.has(part)) current.directories.set(part, node());
18
+ current = current.directories.get(part);
19
+ }
20
+ current.files.push(parts.at(-1));
21
+ }
22
+ return root;
23
+ }
24
+
25
+ function descendantCounts(current) {
26
+ let directories = 0;
27
+ let files = current.files.length;
28
+ for (const child of current.directories.values()) {
29
+ directories += 1;
30
+ const nested = descendantCounts(child);
31
+ directories += nested.directories;
32
+ files += nested.files;
33
+ }
34
+ return { directories, files };
35
+ }
36
+
37
+ function plural(count, singular, pluralForm = `${singular}s`) {
38
+ return `${count} ${count === 1 ? singular : pluralForm}`;
39
+ }
40
+
41
+ export function formatDocumentationTree(paths, documentationRoot = 'docs', maxDepth = 5) {
42
+ const tree = buildTree(paths);
43
+ let docs = tree;
44
+ for (const part of documentationRoot.split('/')) {
45
+ docs = docs.directories.get(part);
46
+ if (!docs) return `- ${documentationRoot}/: missing`;
47
+ }
48
+ const lines = [`${documentationRoot}/`];
49
+ const render = (current, depth, indent) => {
50
+ for (const file of current.files.sort()) lines.push(`${indent}- ${file}`);
51
+ if (depth >= maxDepth && current.directories.size > 0) {
52
+ for (const [name, child] of [...current.directories.entries()].sort(([a], [b]) => a.localeCompare(b))) {
53
+ const counts = descendantCounts(child);
54
+ lines.push(`${indent}- ${name}/`);
55
+ lines.push(`${indent} - … ${plural(counts.directories, 'directory', 'directories')}, ${plural(counts.files, 'Markdown file')}`);
56
+ }
57
+ return;
58
+ }
59
+ for (const [name, child] of [...current.directories.entries()].sort(([a], [b]) => a.localeCompare(b))) {
60
+ lines.push(`${indent}- ${name}/`);
61
+ render(child, depth + 1, `${indent} `);
62
+ }
63
+ };
64
+ render(docs, 1, ' ');
65
+ return lines.join('\n');
66
+ }
67
+
68
+ function discoverMarkdown(root, documentationRoot, exclude) {
69
+ const paths = [];
70
+ const walk = (relativeDirectory) => {
71
+ let entries;
72
+ try {
73
+ entries = readdirSync(join(root, relativeDirectory), { withFileTypes: true, encoding: 'utf8' });
74
+ } catch {
75
+ return;
76
+ }
77
+ entries.sort((a, b) => a.name.localeCompare(b.name));
78
+ for (const entry of entries) {
79
+ const path = `${relativeDirectory}/${entry.name}`.replaceAll('\\', '/');
80
+ if (entry.name.startsWith('.') || isSecretLikePathPattern(path) || exclude.some(pattern => matchPathPattern(path, pattern))) continue;
81
+ if (entry.isDirectory()) {
82
+ if (!SKIPPED_DIRECTORIES.has(entry.name)) walk(path);
83
+ } else if (entry.isFile() && entry.name.toLowerCase().endsWith('.md')) paths.push(path);
84
+ }
85
+ };
86
+ walk(documentationRoot);
87
+ return paths.sort();
88
+ }
89
+
90
+ export function buildDocumentationMap(projectRoot = '.', { depth = 5 } = {}) {
91
+ const root = resolve(projectRoot);
92
+ if (!existsSync(root)) return { ok: false, error: { code: 'ROOT_NOT_FOUND', message: `Project root not found: ${root}` } };
93
+ try {
94
+ if (!statSync(root).isDirectory()) return { ok: false, error: { code: 'ROOT_NOT_FOUND', message: `Project root not found: ${root}` } };
95
+ } catch {
96
+ return { ok: false, error: { code: 'ROOT_NOT_FOUND', message: `Project root not found: ${root}` } };
97
+ }
98
+ const config = readMemoryConfig(root);
99
+ const documentationRoot = config.documentation?.root ?? 'docs';
100
+ const exclude = [...(config.load?.documentationSummary?.exclude ?? [])].sort();
101
+ const paths = discoverMarkdown(root, documentationRoot, exclude);
102
+ return {
103
+ ok: true,
104
+ root,
105
+ documentationRoot,
106
+ depth,
107
+ exclude,
108
+ paths,
109
+ tree: formatDocumentationTree(paths, documentationRoot, depth),
110
+ };
111
+ }