@lon-ask/dockit 0.1.0

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 (78) hide show
  1. package/LICENSE +674 -0
  2. package/README.md +496 -0
  3. package/SKILL.md +154 -0
  4. package/apps/client/dist/assets/index-CqOXxsEZ.js +240 -0
  5. package/apps/client/dist/assets/index-DwvaANnI.css +1 -0
  6. package/apps/client/dist/index.html +13 -0
  7. package/apps/server/src/core/domain/entry.ts +22 -0
  8. package/apps/server/src/core/domain/errors.ts +27 -0
  9. package/apps/server/src/core/domain/knowledge-graph.ts +51 -0
  10. package/apps/server/src/core/domain/types.ts +168 -0
  11. package/apps/server/src/core/ports/IBuildRepository.ts +7 -0
  12. package/apps/server/src/core/ports/IDocumentNormalizer.ts +6 -0
  13. package/apps/server/src/core/ports/IDocumentStore.ts +4 -0
  14. package/apps/server/src/core/ports/IEntryReadModel.ts +9 -0
  15. package/apps/server/src/core/ports/IEntryRepository.ts +11 -0
  16. package/apps/server/src/core/ports/IKnowledgeGraph.ts +10 -0
  17. package/apps/server/src/core/ports/IPathResolver.ts +3 -0
  18. package/apps/server/src/core/ports/ISearchEngine.ts +9 -0
  19. package/apps/server/src/core/ports/ISourceProcessor.ts +7 -0
  20. package/apps/server/src/core/ports/ISourceRepository.ts +11 -0
  21. package/apps/server/src/core/usecases/BuildUseCase.ts +98 -0
  22. package/apps/server/src/core/usecases/ConfigUseCase.ts +64 -0
  23. package/apps/server/src/core/usecases/SearchUseCase.ts +16 -0
  24. package/apps/server/src/index.ts +98 -0
  25. package/apps/server/src/infrastructure/filesystem/FileSystemDocumentStore.ts +27 -0
  26. package/apps/server/src/infrastructure/graph/GraphSearchDecorator.ts +53 -0
  27. package/apps/server/src/infrastructure/graph/GraphifyKnowledgeGraph.ts +172 -0
  28. package/apps/server/src/infrastructure/graph/index.ts +2 -0
  29. package/apps/server/src/infrastructure/persistence/sqlite/SqliteBuildRepository.ts +34 -0
  30. package/apps/server/src/infrastructure/persistence/sqlite/SqliteEntryReadModel.ts +17 -0
  31. package/apps/server/src/infrastructure/persistence/sqlite/SqliteEntryRepository.ts +81 -0
  32. package/apps/server/src/infrastructure/persistence/sqlite/SqliteSourceRepository.ts +65 -0
  33. package/apps/server/src/infrastructure/persistence/sqlite/connection.ts +52 -0
  34. package/apps/server/src/infrastructure/search/SearchEngineFactory.ts +43 -0
  35. package/apps/server/src/infrastructure/search/json/JsonSearchEngine.ts +164 -0
  36. package/apps/server/src/infrastructure/search/vector/EmbeddingService.ts +23 -0
  37. package/apps/server/src/infrastructure/search/vector/VectorSearchEngine.ts +480 -0
  38. package/apps/server/src/infrastructure/source-processors/AntoraSourceProcessor.ts +14 -0
  39. package/apps/server/src/infrastructure/source-processors/AsciidocSourceProcessor.ts +12 -0
  40. package/apps/server/src/infrastructure/source-processors/DocumentNormalizer.ts +16 -0
  41. package/apps/server/src/infrastructure/source-processors/GithubMarkdownSourceProcessor.ts +12 -0
  42. package/apps/server/src/infrastructure/source-processors/MavenSourceProcessor.ts +12 -0
  43. package/apps/server/src/infrastructure/source-processors/PathResolver.ts +6 -0
  44. package/apps/server/src/infrastructure/source-processors/SourceCodeSourceProcessor.ts +260 -0
  45. package/apps/server/src/infrastructure/source-processors/ZipSourceProcessor.ts +12 -0
  46. package/apps/server/src/mcp-http.ts +102 -0
  47. package/apps/server/src/mcp.ts +432 -0
  48. package/apps/server/src/routes/build.ts +105 -0
  49. package/apps/server/src/routes/entries.ts +62 -0
  50. package/apps/server/src/routes/graph.ts +57 -0
  51. package/apps/server/src/routes/search.ts +28 -0
  52. package/apps/server/src/routes/sources.ts +105 -0
  53. package/apps/server/src/routes/viewer.ts +28 -0
  54. package/apps/server/src/services/antora.ts +238 -0
  55. package/apps/server/src/services/asciidoc.ts +221 -0
  56. package/apps/server/src/services/configLoader.ts +207 -0
  57. package/apps/server/src/services/githubMarkdown.ts +236 -0
  58. package/apps/server/src/services/maven.ts +178 -0
  59. package/apps/server/src/services/normalizer.ts +63 -0
  60. package/apps/server/src/services/paths.ts +5 -0
  61. package/apps/server/src/services/textExtractor.ts +49 -0
  62. package/apps/server/src/services/zip.ts +84 -0
  63. package/bin/commands/build.ts +85 -0
  64. package/bin/commands/dev.ts +36 -0
  65. package/bin/commands/get.ts +36 -0
  66. package/bin/commands/graph.ts +153 -0
  67. package/bin/commands/init.ts +170 -0
  68. package/bin/commands/list.ts +47 -0
  69. package/bin/commands/mcp.ts +32 -0
  70. package/bin/commands/search.ts +185 -0
  71. package/bin/commands/serve.ts +23 -0
  72. package/bin/commands/status.ts +46 -0
  73. package/bin/dockit-cli.ts +92 -0
  74. package/bin/dockit.js +17 -0
  75. package/bin/utils.ts +85 -0
  76. package/dockit.yaml +154 -0
  77. package/package.json +60 -0
  78. package/scripts/mcp-wrapper.sh +44 -0
@@ -0,0 +1,49 @@
1
+ export function extractTextFromHtml(html: string, maxLength: number = 50000): string {
2
+ const withoutStyles = html
3
+ .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, '')
4
+ .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, '')
5
+ .replace(/<link[^>]*>/gi, '')
6
+ .replace(/<meta[^>]*>/gi, '');
7
+
8
+ const withoutTags = withoutStyles
9
+ .replace(/<h1[^>]*>/gi, '\n\n## ')
10
+ .replace(/<\/h1>/gi, '\n')
11
+ .replace(/<h2[^>]*>/gi, '\n\n### ')
12
+ .replace(/<\/h2>/gi, '\n')
13
+ .replace(/<h3[^>]*>/gi, '\n\n#### ')
14
+ .replace(/<\/h3>/gi, '\n')
15
+ .replace(/<h4[^>]*>/gi, '\n\n**')
16
+ .replace(/<\/h4>/gi, '**\n')
17
+ .replace(/<li[^>]*>/gi, '\n- ')
18
+ .replace(/<\/li>/gi, '')
19
+ .replace(/<\/?p[^>]*>/gi, '\n\n')
20
+ .replace(/<br\s*\/?>/gi, '\n')
21
+ .replace(/<hr\s*\/?>/gi, '\n---\n')
22
+ .replace(/<\/?pre[^>]*>/gi, '\n```\n')
23
+ .replace(/<\/?code[^>]*>/gi, '`')
24
+ .replace(/<a[^>]*href="([^"]*)"[^>]*>/gi, ' [$1] ')
25
+ .replace(/<\/a>/gi, '')
26
+ .replace(/<[^>]+>/g, ' ')
27
+ .replace(/&amp;/g, '&')
28
+ .replace(/&lt;/g, '<')
29
+ .replace(/&gt;/g, '>')
30
+ .replace(/&quot;/g, '"')
31
+ .replace(/&#39;/g, "'")
32
+ .replace(/&nbsp;/g, ' ')
33
+ .replace(/&#x2F;/g, '/')
34
+ .replace(/&#(\d+);/g, (_, d) => String.fromCharCode(Number(d)));
35
+
36
+ const cleaned = withoutTags
37
+ .replace(/\n{3,}/g, '\n\n')
38
+ .replace(/[ \t]{3,}/g, ' ')
39
+ .trim();
40
+
41
+ if (cleaned.length <= maxLength) return cleaned;
42
+
43
+ const truncated = cleaned.slice(0, maxLength);
44
+ const lastNewline = truncated.lastIndexOf('\n');
45
+ const cutPoint = lastNewline > maxLength * 0.8 ? lastNewline : truncated.lastIndexOf(' ');
46
+ const final = cutPoint > maxLength * 0.8 ? truncated.slice(0, cutPoint) : truncated;
47
+
48
+ return final + '\n\n[... content truncated ...]';
49
+ }
@@ -0,0 +1,84 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { Readable } from 'node:stream';
4
+ import { pipeline } from 'node:stream/promises';
5
+ import unzipper from 'unzipper';
6
+ import type { ZipSourceConfig } from '../core/domain/types.js';
7
+
8
+ export async function extractLocalZip(
9
+ localPath: string,
10
+ targetDir: string,
11
+ log: (msg: string) => void
12
+ ): Promise<void> {
13
+ if (!fs.existsSync(localPath)) {
14
+ throw new Error(`Local file not found: ${localPath}`);
15
+ }
16
+ const stat = fs.statSync(localPath);
17
+ if (!stat.isFile()) {
18
+ throw new Error(`localPath must be a ZIP file, got directory: ${localPath}`);
19
+ }
20
+
21
+ log(`Extracting local ZIP from ${localPath}`);
22
+ fs.mkdirSync(targetDir, { recursive: true });
23
+ const data = fs.readFileSync(localPath);
24
+ const stream = Readable.from(data);
25
+ await pipeline(stream, unzipper.Extract({ path: targetDir }));
26
+ log(`Extracted ZIP to ${targetDir}`);
27
+ const files = countFilesRecursive(targetDir);
28
+ log(`Found ${files} files in extracted archive`);
29
+ }
30
+
31
+ export async function downloadAndExtractZip(
32
+ configOrUrl: ZipSourceConfig | string,
33
+ targetDir: string,
34
+ log: (msg: string) => void
35
+ ): Promise<void> {
36
+ const config = typeof configOrUrl === 'string'
37
+ ? { url: configOrUrl }
38
+ : configOrUrl;
39
+
40
+ if (config.localPath) {
41
+ await extractLocalZip(config.localPath, targetDir, log);
42
+ return;
43
+ }
44
+
45
+ if (!config.url) {
46
+ throw new Error('ZIP source requires url or localPath');
47
+ }
48
+
49
+ log(`Downloading ZIP from ${config.url}`);
50
+ const response = await fetch(config.url);
51
+ if (!response.ok) {
52
+ throw new Error(`Failed to download ZIP: ${response.status} ${response.statusText}`);
53
+ }
54
+
55
+ fs.mkdirSync(targetDir, { recursive: true });
56
+
57
+ if (!response.body) {
58
+ throw new Error('No response body');
59
+ }
60
+
61
+ const nodeStream = Readable.fromWeb(response.body as never);
62
+ await pipeline(
63
+ nodeStream,
64
+ unzipper.Extract({ path: targetDir })
65
+ );
66
+
67
+ log(`Extracted ZIP to ${targetDir}`);
68
+ const files = countFilesRecursive(targetDir);
69
+ log(`Found ${files} files in extracted archive`);
70
+ }
71
+
72
+ function countFilesRecursive(dir: string): number {
73
+ let count = 0;
74
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
75
+ for (const entry of entries) {
76
+ const fullPath = path.join(dir, entry.name);
77
+ if (entry.isDirectory()) {
78
+ count += countFilesRecursive(fullPath);
79
+ } else {
80
+ count++;
81
+ }
82
+ }
83
+ return count;
84
+ }
@@ -0,0 +1,85 @@
1
+ import path from 'node:path';
2
+ import { resolveConfigPath } from '../utils.js';
3
+
4
+ export default async function build(root, positional, flags) {
5
+ const entryId = positional[0];
6
+ if (!entryId) {
7
+ console.error('Error: entry ID is required');
8
+ console.error('Usage: dockit build <entry>');
9
+ process.exit(1);
10
+ }
11
+
12
+ const { getDb } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/connection.js'));
13
+ const { SqliteEntryRepository } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteEntryRepository.js'));
14
+ const { SqliteSourceRepository } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteSourceRepository.js'));
15
+ const { SqliteBuildRepository } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteBuildRepository.js'));
16
+ const { SqliteEntryReadModel } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteEntryReadModel.js'));
17
+ const { createSearchEngine } = await import(path.join(root, 'apps/server/src/infrastructure/search/SearchEngineFactory.js'));
18
+ const { ConfigUseCase } = await import(path.join(root, 'apps/server/src/core/usecases/ConfigUseCase.js'));
19
+ const { BuildUseCase } = await import(path.join(root, 'apps/server/src/core/usecases/BuildUseCase.js'));
20
+ const { ZipSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/ZipSourceProcessor.js'));
21
+ const { AntoraSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/AntoraSourceProcessor.js'));
22
+ const { AsciidocSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/AsciidocSourceProcessor.js'));
23
+ const { MavenSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/MavenSourceProcessor.js'));
24
+ const { GithubMarkdownSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/GithubMarkdownSourceProcessor.js'));
25
+ const { SourceCodeSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/SourceCodeSourceProcessor.js'));
26
+ const { DocumentNormalizer } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/DocumentNormalizer.js'));
27
+ const { PathResolver } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/PathResolver.js'));
28
+ const { loadConfig, syncConfigToDb } = await import(path.join(root, 'apps/server/src/services/configLoader.js'));
29
+
30
+ // Ensure DB is initialized (getDb calls initDb internally)
31
+ const db = getDb();
32
+
33
+ const entryRepo = new SqliteEntryRepository(db);
34
+ const sourceRepo = new SqliteSourceRepository(db);
35
+ const buildRepo = new SqliteBuildRepository(db);
36
+
37
+ // Sync config to DB
38
+ const configPath = resolveConfigPath(root);
39
+ const config = loadConfig(configPath);
40
+ syncConfigToDb(config, entryRepo, sourceRepo);
41
+ const entryReadModel = new SqliteEntryReadModel(db);
42
+ const searchEngine = await createSearchEngine(entryReadModel, config.search?.engine);
43
+
44
+ const configUseCase = new ConfigUseCase(entryRepo, sourceRepo);
45
+
46
+ const processors = [
47
+ new ZipSourceProcessor(),
48
+ new AntoraSourceProcessor(),
49
+ new AsciidocSourceProcessor(),
50
+ new MavenSourceProcessor(),
51
+ new GithubMarkdownSourceProcessor(),
52
+ new SourceCodeSourceProcessor(),
53
+ ];
54
+ const documentNormalizer = new DocumentNormalizer();
55
+ const pathResolver = new PathResolver();
56
+
57
+ const buildUseCase = new BuildUseCase(
58
+ buildRepo, sourceRepo, entryRepo, searchEngine,
59
+ processors, documentNormalizer, pathResolver,
60
+ );
61
+
62
+ const entry = await configUseCase.getEntry(entryId);
63
+
64
+ if (!entry) {
65
+ console.error(`Entry not found: ${entryId}`);
66
+ process.exit(1);
67
+ }
68
+
69
+ const entryWithSources = await configUseCase.getEntryWithSources(entryId);
70
+ const sources = entryWithSources?.sources || [];
71
+ if (sources.length === 0) {
72
+ console.error(`Entry "${entry.name}" has no sources configured.`);
73
+ process.exit(1);
74
+ }
75
+
76
+ console.log(`Building documentation for ${entry.name} ${entry.version}...`);
77
+ console.log('');
78
+
79
+ const result = await buildUseCase.build(entryId);
80
+ console.log(`Build ${entryId}: ${result.status}`);
81
+ if (result.status === 'error') {
82
+ console.error(result.log);
83
+ process.exit(1);
84
+ }
85
+ }
@@ -0,0 +1,36 @@
1
+ import { spawn } from 'node:child_process';
2
+
3
+ export default function dev(root, positional, flags) {
4
+ console.log('Starting Dockit dev servers...');
5
+ console.log('');
6
+
7
+ const server = spawn('npm', ['run', 'dev:server'], {
8
+ cwd: root,
9
+ stdio: 'inherit',
10
+ shell: true,
11
+ });
12
+
13
+ const client = spawn('npm', ['run', 'dev:client'], {
14
+ cwd: root,
15
+ stdio: 'inherit',
16
+ shell: true,
17
+ });
18
+
19
+ const cleanup = () => {
20
+ server.kill();
21
+ client.kill();
22
+ };
23
+
24
+ process.on('SIGINT', cleanup);
25
+ process.on('SIGTERM', cleanup);
26
+
27
+ server.on('close', (code) => {
28
+ console.log(`Server exited with code ${code}`);
29
+ cleanup();
30
+ });
31
+
32
+ client.on('close', (code) => {
33
+ console.log(`Client exited with code ${code}`);
34
+ cleanup();
35
+ });
36
+ }
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+
4
+ export default async function getDoc(root, positional, flags) {
5
+ const entryId = positional[0];
6
+ const docPath = positional[1];
7
+
8
+ if (!entryId || !docPath) {
9
+ console.error('Error: entry ID and document path are required');
10
+ console.error('Usage: dockit get <entry> <path>');
11
+ console.error('Example: dockit get react asciidoc/getting-started.html');
12
+ process.exit(1);
13
+ }
14
+
15
+ const asJson = !!flags.json;
16
+
17
+ const { DATA_ROOT } = await import(path.join(root, 'apps/server/src/services/paths.js'));
18
+ const { extractTextFromHtml } = await import(path.join(root, 'apps/server/src/services/textExtractor.js'));
19
+
20
+ const filePath = path.join(DATA_ROOT, entryId, 'bundle', docPath);
21
+
22
+ if (!fs.existsSync(filePath)) {
23
+ console.error(`Document not found: ${docPath}`);
24
+ console.error(`Has the entry "${entryId}" been built?`);
25
+ process.exit(1);
26
+ }
27
+
28
+ const html = fs.readFileSync(filePath, 'utf-8');
29
+ const text = extractTextFromHtml(html);
30
+
31
+ if (asJson) {
32
+ console.log(JSON.stringify({ entryId, path: docPath, content: text }, null, 2));
33
+ } else {
34
+ console.log(text);
35
+ }
36
+ }
@@ -0,0 +1,153 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { formatTable } from '../utils.js';
4
+
5
+ const SUBCOMMANDS = ['query', 'path', 'gods', 'explain'];
6
+
7
+ export default async function graph(root, positional, flags) {
8
+ const sub = positional[0];
9
+ if (!sub || !SUBCOMMANDS.includes(sub)) {
10
+ console.error('Usage: dockit graph <query|path|gods|explain> <entry> [args...]');
11
+ console.error('');
12
+ console.error('Commands:');
13
+ console.error(' dockit graph query <entry> <query> Search graph nodes by name, file, or type');
14
+ console.error(' dockit graph path <entry> <from> <to> Find shortest dependency path between two nodes');
15
+ console.error(' dockit graph gods <entry> List most connected nodes');
16
+ console.error(' dockit graph explain <entry> <node> Get node details with edges and connections');
17
+ process.exit(1);
18
+ }
19
+
20
+ const entry = positional[1];
21
+ if (!entry) {
22
+ console.error('Error: entry ID is required');
23
+ console.error(`Usage: dockit graph ${sub} <entry> ...`);
24
+ process.exit(1);
25
+ }
26
+
27
+ const { GraphifyKnowledgeGraph } = await import(path.join(root, 'apps/server/src/infrastructure/graph/GraphifyKnowledgeGraph.js'));
28
+ const { DATA_ROOT } = await import(path.join(root, 'apps/server/src/services/paths.js'));
29
+
30
+ const kg = new GraphifyKnowledgeGraph(path.join(DATA_ROOT, entry));
31
+
32
+ if (!kg.exists()) {
33
+ console.error(`No knowledge graph found for entry "${entry}". Build the entry first.`);
34
+ process.exit(1);
35
+ }
36
+
37
+ const asJson = !!flags.json;
38
+
39
+ switch (sub) {
40
+ case 'query': {
41
+ const query = positional.slice(2).join(' ');
42
+ if (!query) {
43
+ console.error('Error: query string is required');
44
+ console.error(`Usage: dockit graph query ${entry} \"<query>\"`);
45
+ process.exit(1);
46
+ }
47
+ const limit = parseInt(flags.limit || '20', 10);
48
+ const result = kg.query(query, limit);
49
+ if (asJson) {
50
+ console.log(JSON.stringify(result, null, 2));
51
+ } else {
52
+ console.log(`Query: "${query}" — ${result.totalNodes} node(s), ${result.totalEdges} edge(s) found\n`);
53
+ if (result.nodes.length === 0) {
54
+ console.log('No matching nodes.');
55
+ } else {
56
+ const rows = result.nodes.map((n) => [
57
+ n.name.slice(0, 40),
58
+ n.type.slice(0, 12),
59
+ n.file.slice(0, 50),
60
+ ]);
61
+ console.log(formatTable(['Name', 'Type', 'File'], rows));
62
+ }
63
+ }
64
+ break;
65
+ }
66
+
67
+ case 'path': {
68
+ const from = positional[2];
69
+ const to = positional[3];
70
+ if (!from || !to) {
71
+ console.error('Error: from and to node names are required');
72
+ console.error(`Usage: dockit graph path ${entry} <from> <to>`);
73
+ process.exit(1);
74
+ }
75
+ const result = kg.findPath(from, to);
76
+ if (asJson) {
77
+ console.log(JSON.stringify(result, null, 2));
78
+ } else {
79
+ if (result.found) {
80
+ console.log(`Path found (length: ${result.length}):`);
81
+ result.nodes.forEach((n, i) => {
82
+ const prefix = i === 0 ? ' Start' : i === result.nodes.length - 1 ? ' End ' : ' -> ';
83
+ console.log(`${prefix} ${n.name} (${n.file})`);
84
+ });
85
+ } else {
86
+ console.log(`No path found between "${from}" and "${to}".`);
87
+ }
88
+ }
89
+ break;
90
+ }
91
+
92
+ case 'gods': {
93
+ const limit = parseInt(flags.limit || '10', 10);
94
+ const nodes = kg.findGodNodes(limit);
95
+ const meta = kg.getMetadata();
96
+ if (asJson) {
97
+ console.log(JSON.stringify({ nodes, metadata: meta }, null, 2));
98
+ } else {
99
+ console.log(`Top ${nodes.length} most connected nodes (${meta.nodeCount} total nodes):\n`);
100
+ const rows = nodes.map((n) => [
101
+ n.name.slice(0, 40),
102
+ String(n.degree ?? 0),
103
+ n.file.slice(0, 50),
104
+ ]);
105
+ console.log(formatTable(['Name', 'Degree', 'File'], rows));
106
+ }
107
+ break;
108
+ }
109
+
110
+ case 'explain': {
111
+ const nodeName = positional.slice(2).join(' ');
112
+ if (!nodeName) {
113
+ console.error('Error: node name is required');
114
+ console.error(`Usage: dockit graph explain ${entry} <node>`);
115
+ process.exit(1);
116
+ }
117
+ const queryResult = kg.query(nodeName);
118
+ const node = queryResult.nodes[0] || null;
119
+ if (asJson) {
120
+ const result = {
121
+ node,
122
+ connectedNodes: queryResult.nodes.slice(1, 11),
123
+ edges: queryResult.edges.filter((e) => e.source === node?.id || e.target === node?.id).slice(0, 20),
124
+ totalConnections: queryResult.totalEdges,
125
+ };
126
+ console.log(JSON.stringify(result, null, 2));
127
+ } else {
128
+ if (!node) {
129
+ console.log(`Node "${nodeName}" not found.`);
130
+ } else {
131
+ console.log(`Node: ${node.name}`);
132
+ console.log(` Type: ${node.type}`);
133
+ console.log(` File: ${node.file}`);
134
+ if (node.community !== undefined) console.log(` Community: ${node.community}`);
135
+ console.log('');
136
+ const nodeEdges = queryResult.edges.filter((e) => e.source === node.id || e.target === node.id);
137
+ if (nodeEdges.length > 0) {
138
+ console.log(`Connections (${nodeEdges.length}):`);
139
+ nodeEdges.slice(0, 20).forEach((e) => {
140
+ const direction = e.source === node.id ? '->' : '<-';
141
+ const other = e.source === node.id ? e.target : e.source;
142
+ console.log(` ${node.name} ${direction} ${other} [${e.type}]`);
143
+ });
144
+ if (nodeEdges.length > 20) console.log(` ... and ${nodeEdges.length - 20} more`);
145
+ } else {
146
+ console.log('No connections.');
147
+ }
148
+ }
149
+ }
150
+ break;
151
+ }
152
+ }
153
+ }
@@ -0,0 +1,170 @@
1
+ import path from 'node:path';
2
+ import fs from 'node:fs';
3
+ import { load as loadYaml, dump as dumpYaml } from 'js-yaml';
4
+ import { resolveDockitHome, resolveConfigPath } from '../utils.js';
5
+
6
+ export default async function init(root, positional, flags) {
7
+ const sourcePath = path.resolve(flags.path || flags.dir || '.');
8
+ const name = flags.name || path.basename(sourcePath);
9
+ const version = flags.version || '1.0';
10
+ const codePath = flags['code-path'] || '';
11
+ const entryId = name.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-+|-+$/g, '') || 'project';
12
+
13
+ if (!fs.existsSync(sourcePath)) {
14
+ console.error(`Path not found: ${sourcePath}`);
15
+ process.exit(1);
16
+ }
17
+ if (!fs.statSync(sourcePath).isDirectory()) {
18
+ console.error(`Path must be a directory: ${sourcePath}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log(`Initializing dockit for ${name} ${version}`);
23
+ console.log(` Source: ${sourcePath}`);
24
+ console.log(` Entry: ${entryId}`);
25
+ console.log('');
26
+
27
+ const { getDb } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/connection.js'));
28
+ const { SqliteEntryRepository } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteEntryRepository.js'));
29
+ const { SqliteSourceRepository } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteSourceRepository.js'));
30
+ const { SqliteBuildRepository } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteBuildRepository.js'));
31
+ const { SqliteEntryReadModel } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteEntryReadModel.js'));
32
+ const { createSearchEngine } = await import(path.join(root, 'apps/server/src/infrastructure/search/SearchEngineFactory.js'));
33
+ const { ConfigUseCase } = await import(path.join(root, 'apps/server/src/core/usecases/ConfigUseCase.js'));
34
+ const { BuildUseCase } = await import(path.join(root, 'apps/server/src/core/usecases/BuildUseCase.js'));
35
+ const { GithubMarkdownSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/GithubMarkdownSourceProcessor.js'));
36
+ const { SourceCodeSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/SourceCodeSourceProcessor.js'));
37
+ const { DocumentNormalizer } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/DocumentNormalizer.js'));
38
+ const { PathResolver } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/PathResolver.js'));
39
+ const { ZipSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/ZipSourceProcessor.js'));
40
+ const { AntoraSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/AntoraSourceProcessor.js'));
41
+ const { AsciidocSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/AsciidocSourceProcessor.js'));
42
+ const { MavenSourceProcessor } = await import(path.join(root, 'apps/server/src/infrastructure/source-processors/MavenSourceProcessor.js'));
43
+ const { loadConfig } = await import(path.join(root, 'apps/server/src/services/configLoader.js'));
44
+
45
+ const db = getDb();
46
+ const entryRepo = new SqliteEntryRepository(db);
47
+ const sourceRepo = new SqliteSourceRepository(db);
48
+ const buildRepo = new SqliteBuildRepository(db);
49
+ const configUseCase = new ConfigUseCase(entryRepo, sourceRepo);
50
+
51
+ const existing = await configUseCase.getEntry(entryId);
52
+ if (existing) {
53
+ console.log(`Removing existing entry "${entryId}"...`);
54
+ const sources = await sourceRepo.findByEntryId(entryId);
55
+ for (const s of sources) await sourceRepo.delete(s.id);
56
+ await entryRepo.delete(entryId);
57
+ }
58
+
59
+ const now = new Date().toISOString();
60
+ await entryRepo.save({
61
+ id: entryId, name, version,
62
+ description: `${name} source code and documentation`,
63
+ status: 'pending', created_at: now, updated_at: now,
64
+ });
65
+
66
+ const label = path.basename(sourcePath);
67
+ const sourceCodeId = `${entryId}-src-code`;
68
+ const markdownId = `${entryId}-src-md`;
69
+
70
+ await sourceRepo.save({
71
+ id: sourceCodeId, entry_id: entryId,
72
+ type: 'source-code', label: `${label} Code`,
73
+ config: { localPath: sourcePath, sourcePath: codePath || undefined },
74
+ status: 'pending', created_at: now,
75
+ });
76
+ console.log(` + Source Code: ${codePath ? path.join(sourcePath, codePath) : sourcePath}`);
77
+
78
+ await sourceRepo.save({
79
+ id: markdownId, entry_id: entryId,
80
+ type: 'github-markdown', label: `${label} Markdown`,
81
+ config: { localPath: sourcePath },
82
+ status: 'pending', created_at: now,
83
+ });
84
+ console.log(` + Markdown Docs: ${sourcePath}`);
85
+
86
+ console.log('');
87
+ console.log('Building...');
88
+ console.log('');
89
+
90
+ const dockitHome = resolveDockitHome();
91
+ fs.mkdirSync(dockitHome, { recursive: true });
92
+
93
+ let searchEngineType: 'json' | 'vector' | undefined;
94
+ try {
95
+ const configPath = resolveConfigPath(root);
96
+ const config = loadConfig(configPath);
97
+ searchEngineType = config.search?.engine;
98
+ } catch {
99
+ // No config file yet, use defaults
100
+ }
101
+ const entryReadModel = new SqliteEntryReadModel(db);
102
+ const searchEngine = await createSearchEngine(entryReadModel, searchEngineType);
103
+
104
+ const processors = [
105
+ new ZipSourceProcessor(),
106
+ new AntoraSourceProcessor(),
107
+ new AsciidocSourceProcessor(),
108
+ new MavenSourceProcessor(),
109
+ new GithubMarkdownSourceProcessor(),
110
+ new SourceCodeSourceProcessor(),
111
+ ];
112
+ const documentNormalizer = new DocumentNormalizer();
113
+ const pathResolver = new PathResolver();
114
+ const buildUseCase = new BuildUseCase(
115
+ buildRepo, sourceRepo, entryRepo, searchEngine,
116
+ processors, documentNormalizer, pathResolver,
117
+ );
118
+
119
+ const result = await buildUseCase.build(entryId);
120
+ console.log(`Build ${entryId}: ${result.status}`);
121
+ if (result.status === 'error') {
122
+ console.error(result.log);
123
+ process.exit(1);
124
+ }
125
+
126
+ // Write entry config to dockit home
127
+ const homeConfigPath = path.join(dockitHome, 'dockit.yaml');
128
+ const entryYamlObj: Record<string, unknown> = {
129
+ id: entryId,
130
+ name,
131
+ version,
132
+ description: `${name} source code and documentation`,
133
+ sources: [
134
+ {
135
+ type: 'source-code',
136
+ label: `${label} Code`,
137
+ localPath: sourcePath,
138
+ ...(codePath ? { sourcePath: codePath } : {}),
139
+ },
140
+ {
141
+ type: 'github-markdown',
142
+ label: `${label} Markdown`,
143
+ localPath: sourcePath,
144
+ },
145
+ ],
146
+ };
147
+
148
+ if (fs.existsSync(homeConfigPath)) {
149
+ const existing = loadYaml(fs.readFileSync(homeConfigPath, 'utf-8')) as Record<string, unknown>;
150
+ const entries = (existing.entries as Record<string, unknown>[]) || [];
151
+ const existingIdx = entries.findIndex((e) => e.id === entryId);
152
+ if (existingIdx !== -1) {
153
+ entries[existingIdx] = entryYamlObj;
154
+ } else {
155
+ entries.push(entryYamlObj);
156
+ }
157
+ existing.entries = entries;
158
+ fs.writeFileSync(homeConfigPath, dumpYaml(existing, { noRefs: true }));
159
+ } else {
160
+ fs.writeFileSync(homeConfigPath, dumpYaml({ entries: [entryYamlObj] }, { noRefs: true }));
161
+ }
162
+
163
+ console.log('');
164
+ console.log(`Entry "${name}" (${entryId}) is ready.`);
165
+ console.log('');
166
+ console.log('Search docs: dockit search ' + entryId + ' "<query>"');
167
+ console.log('Graph query: dockit graph query ' + entryId + ' "<query>"');
168
+ console.log('God nodes: dockit graph gods ' + entryId);
169
+ console.log('Web UI: http://localhost:5173/entries/' + entryId);
170
+ }
@@ -0,0 +1,47 @@
1
+ import path from 'node:path';
2
+ import { formatTable } from '../utils.js';
3
+
4
+ export default async function list(root, positional, flags) {
5
+ const asJson = !!flags.json;
6
+
7
+ const { getDb } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/connection.js'));
8
+ const { SqliteEntryRepository } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteEntryRepository.js'));
9
+ const { SqliteSourceRepository } = await import(path.join(root, 'apps/server/src/infrastructure/persistence/sqlite/SqliteSourceRepository.js'));
10
+
11
+ const db = getDb();
12
+ const entryRepo = new SqliteEntryRepository(db);
13
+ const sourceRepo = new SqliteSourceRepository(db);
14
+
15
+ const entries = await entryRepo.findAll();
16
+
17
+ const output = [];
18
+ for (const e of entries) {
19
+ const sources = await sourceRepo.findByEntryId(e.id);
20
+ output.push({
21
+ id: e.id,
22
+ name: e.name,
23
+ version: e.version,
24
+ description: e.description,
25
+ status: e.status,
26
+ sourceCount: sources.length,
27
+ });
28
+ }
29
+
30
+ if (asJson) {
31
+ console.log(JSON.stringify(output, null, 2));
32
+ } else if (output.length === 0) {
33
+ console.log('No documentation entries found.');
34
+ } else {
35
+ const headers = ['Name', 'Version', 'Status', 'Sources', 'ID'];
36
+ const rows = output.map((e) => [
37
+ e.name,
38
+ e.version,
39
+ e.status,
40
+ String(e.sourceCount),
41
+ e.id,
42
+ ]);
43
+
44
+ console.log(formatTable(headers, rows));
45
+ console.log(`\n${output.length} entry/entries.`);
46
+ }
47
+ }