agentmap 0.7.1 → 0.9.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.
- package/CHANGELOG.md +96 -0
- package/README.md +24 -0
- package/dist/cli.js +44 -12
- package/dist/cli.js.map +1 -1
- package/dist/extract/definitions.js +12 -12
- package/dist/extract/definitions.js.map +1 -1
- package/dist/extract/definitions.test.js +30 -259
- package/dist/extract/definitions.test.js.map +1 -1
- package/dist/extract/git-status.d.ts +11 -4
- package/dist/extract/git-status.d.ts.map +1 -1
- package/dist/extract/git-status.js +21 -16
- package/dist/extract/git-status.js.map +1 -1
- package/dist/extract/markdown.js +1 -1
- package/dist/extract/markdown.test.js +3 -3
- package/dist/extract/markdown.test.js.map +1 -1
- package/dist/extract/marker.js +1 -1
- package/dist/extract/marker.test.js +4 -4
- package/dist/extract/marker.test.js.map +1 -1
- package/dist/extract/submodules.d.ts +12 -0
- package/dist/extract/submodules.d.ts.map +1 -0
- package/dist/extract/submodules.js +234 -0
- package/dist/extract/submodules.js.map +1 -0
- package/dist/extract/submodules.test.d.ts +2 -0
- package/dist/extract/submodules.test.d.ts.map +1 -0
- package/dist/extract/submodules.test.js +84 -0
- package/dist/extract/submodules.test.js.map +1 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -9
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +10 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +41 -0
- package/dist/logger.js.map +1 -0
- package/dist/map/builder.d.ts +3 -3
- package/dist/map/builder.d.ts.map +1 -1
- package/dist/map/builder.js +59 -9
- package/dist/map/builder.js.map +1 -1
- package/dist/map/builder.test.d.ts +2 -0
- package/dist/map/builder.test.d.ts.map +1 -0
- package/dist/map/builder.test.js +66 -0
- package/dist/map/builder.test.js.map +1 -0
- package/dist/map/truncate.d.ts +7 -3
- package/dist/map/truncate.d.ts.map +1 -1
- package/dist/map/truncate.js +90 -9
- package/dist/map/truncate.js.map +1 -1
- package/dist/map/yaml.d.ts.map +1 -1
- package/dist/map/yaml.js +13 -3
- package/dist/map/yaml.js.map +1 -1
- package/dist/scanner.d.ts +9 -2
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +172 -49
- package/dist/scanner.js.map +1 -1
- package/dist/scanner.test.d.ts +2 -0
- package/dist/scanner.test.d.ts.map +1 -0
- package/dist/scanner.test.js +84 -0
- package/dist/scanner.test.js.map +1 -0
- package/dist/test-helpers/git-test-helpers.d.ts +13 -0
- package/dist/test-helpers/git-test-helpers.d.ts.map +1 -0
- package/dist/test-helpers/git-test-helpers.js +48 -0
- package/dist/test-helpers/git-test-helpers.js.map +1 -0
- package/dist/types.d.ts +42 -2
- package/dist/types.d.ts.map +1 -1
- package/package.json +15 -3
- package/src/cli.ts +164 -0
- package/src/extract/definitions.test.ts +2040 -0
- package/src/extract/definitions.ts +379 -0
- package/src/extract/git-status.test.ts +507 -0
- package/src/extract/git-status.ts +359 -0
- package/src/extract/markdown.test.ts +159 -0
- package/src/extract/markdown.ts +202 -0
- package/src/extract/marker.test.ts +566 -0
- package/src/extract/marker.ts +398 -0
- package/src/extract/submodules.test.ts +95 -0
- package/src/extract/submodules.ts +269 -0
- package/src/extract/utils.ts +27 -0
- package/src/index.ts +106 -0
- package/src/languages/cpp.ts +129 -0
- package/src/languages/go.ts +72 -0
- package/src/languages/index.ts +231 -0
- package/src/languages/javascript.ts +33 -0
- package/src/languages/python.ts +41 -0
- package/src/languages/rust.ts +72 -0
- package/src/languages/typescript.ts +74 -0
- package/src/languages/zig.ts +106 -0
- package/src/logger.ts +55 -0
- package/src/map/builder.test.ts +72 -0
- package/src/map/builder.ts +175 -0
- package/src/map/truncate.ts +188 -0
- package/src/map/yaml.ts +66 -0
- package/src/parser/index.ts +53 -0
- package/src/parser/languages.ts +64 -0
- package/src/scanner.test.ts +95 -0
- package/src/scanner.ts +364 -0
- package/src/test-helpers/git-test-helpers.ts +62 -0
- package/src/types.ts +191 -0
|
@@ -0,0 +1,398 @@
|
|
|
1
|
+
// Extract file header comment/docstring using tree-sitter.
|
|
2
|
+
// Detects standard comment styles from existing projects.
|
|
3
|
+
// Automatically skips license headers (Copyright, SPDX, etc.).
|
|
4
|
+
|
|
5
|
+
import { parseCode, detectLanguage } from '../parser/index.js'
|
|
6
|
+
import { readFirstLines } from './utils.js'
|
|
7
|
+
import type { MarkerResult, Language, SyntaxNode } from '../types.js'
|
|
8
|
+
|
|
9
|
+
export { extractMarkdownDescription } from './markdown.js'
|
|
10
|
+
|
|
11
|
+
const MAX_LINES = 50
|
|
12
|
+
const MAX_DESC_LINES = 20
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Patterns that strongly indicate a license/copyright comment.
|
|
16
|
+
* These are checked against comment text.
|
|
17
|
+
*/
|
|
18
|
+
const LICENSE_PATTERNS = [
|
|
19
|
+
/\bcopyright\s*(?:\(c\)|©|\d{4})/i, // "Copyright (c)", "Copyright ©", "Copyright 2024"
|
|
20
|
+
/\bspdx-license-identifier\s*:/i, // "SPDX-License-Identifier: MIT"
|
|
21
|
+
/\ball rights reserved\b/i, // Common in copyright notices
|
|
22
|
+
/\blicensed under\b/i, // "Licensed under the MIT License", "Licensed under Apache 2.0"
|
|
23
|
+
/\bpermission is hereby granted\b/i, // MIT license text
|
|
24
|
+
/\bredistribution and use\b/i, // BSD license text
|
|
25
|
+
/\bthis source code is licensed\b/i, // Meta/Facebook style
|
|
26
|
+
/\bwithout warranty\b/i, // Common in license text
|
|
27
|
+
/\bthe software is provided "as is"\b/i, // MIT license text
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Check if comment text looks like a license/copyright header.
|
|
32
|
+
* Uses patterns specific to actual license text to avoid false positives.
|
|
33
|
+
*/
|
|
34
|
+
function isLicenseComment(text: string): boolean {
|
|
35
|
+
return LICENSE_PATTERNS.some(pattern => pattern.test(text))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Truncate lines to MAX_DESC_LINES, adding indicator if truncated
|
|
40
|
+
*/
|
|
41
|
+
function truncateDescription(lines: string[]): string {
|
|
42
|
+
const trimmed = lines.join('\n').trim()
|
|
43
|
+
const trimmedLines = trimmed.split('\n')
|
|
44
|
+
|
|
45
|
+
if (trimmedLines.length <= MAX_DESC_LINES) {
|
|
46
|
+
return trimmed
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const truncated = trimmedLines.slice(0, MAX_DESC_LINES)
|
|
50
|
+
const remaining = trimmedLines.length - MAX_DESC_LINES
|
|
51
|
+
truncated.push(`... and ${remaining} more lines`)
|
|
52
|
+
return truncated.join('\n')
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract header comment/docstring from a file.
|
|
57
|
+
* Uses tree-sitter for clean AST-based extraction.
|
|
58
|
+
*
|
|
59
|
+
* Supports:
|
|
60
|
+
* - // line comments (JS/TS/Go/Rust)
|
|
61
|
+
* - /* block comments (JS/TS/Go/Rust)
|
|
62
|
+
* - # line comments (Python)
|
|
63
|
+
* - """ docstrings (Python)
|
|
64
|
+
* - //! inner doc comments (Rust)
|
|
65
|
+
*/
|
|
66
|
+
export async function extractMarker(filepath: string): Promise<MarkerResult> {
|
|
67
|
+
const language = detectLanguage(filepath)
|
|
68
|
+
if (!language) {
|
|
69
|
+
return { found: false }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const head = await readFirstLines(filepath, MAX_LINES)
|
|
73
|
+
if (head === null) {
|
|
74
|
+
// File couldn't be read - skip silently
|
|
75
|
+
return { found: false }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return extractMarkerFromCode(head, language)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Extract header comment/docstring from code string.
|
|
83
|
+
* Use this when you already have the file content to avoid re-reading.
|
|
84
|
+
*/
|
|
85
|
+
export async function extractMarkerFromCode(code: string, language: Language): Promise<MarkerResult> {
|
|
86
|
+
// Only parse first MAX_LINES worth of content for efficiency
|
|
87
|
+
const lines = code.split('\n').slice(0, MAX_LINES)
|
|
88
|
+
const head = lines.join('\n')
|
|
89
|
+
|
|
90
|
+
const tree = await parseCode(head, language)
|
|
91
|
+
const description = extractHeaderFromAST(tree.rootNode, language)
|
|
92
|
+
|
|
93
|
+
if (description === null) {
|
|
94
|
+
return { found: false }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
found: true,
|
|
99
|
+
description: description || undefined,
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Check if a node is a JS/TS directive like "use strict" or "use client"
|
|
105
|
+
*/
|
|
106
|
+
function isDirective(node: SyntaxNode): boolean {
|
|
107
|
+
if (node.type !== 'expression_statement') return false
|
|
108
|
+
const str = node.child(0)
|
|
109
|
+
if (str?.type !== 'string') return false
|
|
110
|
+
const text = str.text
|
|
111
|
+
// Check for known directives (with quotes)
|
|
112
|
+
return /^["']use (strict|client|server)["']$/.test(text)
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Extract header comment from AST root node
|
|
117
|
+
*/
|
|
118
|
+
function extractHeaderFromAST(root: SyntaxNode, language: Language): string | null {
|
|
119
|
+
const children = getChildren(root)
|
|
120
|
+
if (children.length === 0) {
|
|
121
|
+
return null
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let startIdx = 0
|
|
125
|
+
let shebang: string | null = null
|
|
126
|
+
|
|
127
|
+
// Capture shebang if present
|
|
128
|
+
// Python/shell: comment node starting with #!
|
|
129
|
+
// JS/TS: hash_bang_line node
|
|
130
|
+
const firstChild = children[0]
|
|
131
|
+
if (firstChild?.type === 'hash_bang_line' ||
|
|
132
|
+
(firstChild?.type === 'comment' && firstChild.text.startsWith('#!'))) {
|
|
133
|
+
shebang = firstChild.text.trim()
|
|
134
|
+
startIdx = 1
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Skip JS/TS directives like "use strict", "use client"
|
|
138
|
+
while (startIdx < children.length && isDirective(children[startIdx])) {
|
|
139
|
+
startIdx++
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (startIdx >= children.length) {
|
|
143
|
+
// Only shebang, no description
|
|
144
|
+
return shebang
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const first = children[startIdx]
|
|
148
|
+
|
|
149
|
+
// Helper to prepend shebang to description
|
|
150
|
+
const withShebang = (desc: string | null): string | null => {
|
|
151
|
+
if (!desc) return shebang
|
|
152
|
+
if (!shebang) return desc
|
|
153
|
+
return `${shebang}\n${desc}`
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Python: check for module docstring (expression_statement containing string)
|
|
157
|
+
if (language === 'python' && first.type === 'expression_statement') {
|
|
158
|
+
const str = first.childForFieldName('expression') ?? first.child(0)
|
|
159
|
+
if (str?.type === 'string') {
|
|
160
|
+
const docstring = extractPythonDocstring(str)
|
|
161
|
+
// Skip if it looks like a license
|
|
162
|
+
if (docstring && isLicenseComment(docstring)) {
|
|
163
|
+
// Try to find next comment after this docstring
|
|
164
|
+
return withShebang(extractConsecutiveComments(children, startIdx + 1, language))
|
|
165
|
+
}
|
|
166
|
+
return withShebang(docstring)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// Collect consecutive comment nodes at the start, skipping license comments
|
|
171
|
+
if (isCommentNode(first)) {
|
|
172
|
+
return withShebang(extractConsecutiveCommentsSkipLicense(children, startIdx, language))
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return shebang
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Extract consecutive comments, skipping leading license comments
|
|
180
|
+
*/
|
|
181
|
+
function extractConsecutiveCommentsSkipLicense(
|
|
182
|
+
children: SyntaxNode[],
|
|
183
|
+
startIdx: number,
|
|
184
|
+
language: Language
|
|
185
|
+
): string | null {
|
|
186
|
+
let idx = startIdx
|
|
187
|
+
|
|
188
|
+
while (idx < children.length) {
|
|
189
|
+
const node = children[idx]
|
|
190
|
+
|
|
191
|
+
// Skip non-comment nodes (might be blank lines, etc.)
|
|
192
|
+
if (!isCommentNode(node)) {
|
|
193
|
+
idx++
|
|
194
|
+
continue
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const text = extractCommentText(node, language)
|
|
198
|
+
if (text === null) {
|
|
199
|
+
idx++
|
|
200
|
+
continue
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Check if this comment is a license
|
|
204
|
+
if (isLicenseComment(text)) {
|
|
205
|
+
// Skip this license comment
|
|
206
|
+
idx++
|
|
207
|
+
// Continue to skip any consecutive license comments
|
|
208
|
+
continue
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// Found a non-license comment - extract from here
|
|
212
|
+
return extractConsecutiveComments(children, idx, language)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return null
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Check if a node is a comment
|
|
220
|
+
*/
|
|
221
|
+
function isCommentNode(node: SyntaxNode): boolean {
|
|
222
|
+
return (
|
|
223
|
+
node.type === 'comment' ||
|
|
224
|
+
node.type === 'line_comment' ||
|
|
225
|
+
node.type === 'block_comment'
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Extract consecutive comment nodes and combine their text
|
|
231
|
+
*/
|
|
232
|
+
function extractConsecutiveComments(
|
|
233
|
+
children: SyntaxNode[],
|
|
234
|
+
startIdx: number,
|
|
235
|
+
language: Language
|
|
236
|
+
): string {
|
|
237
|
+
const lines: string[] = []
|
|
238
|
+
|
|
239
|
+
for (let i = startIdx; i < children.length; i++) {
|
|
240
|
+
const node = children[i]
|
|
241
|
+
if (!isCommentNode(node)) {
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const text = extractCommentText(node, language)
|
|
246
|
+
if (text !== null) {
|
|
247
|
+
lines.push(...text.split('\n'))
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return truncateDescription(lines)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if comment is a TypeScript triple-slash reference directive
|
|
256
|
+
* These are compiler directives, not actual comments
|
|
257
|
+
*/
|
|
258
|
+
function isReferenceDirective(text: string): boolean {
|
|
259
|
+
return /^\/\/\/\s*<reference\s/.test(text)
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Extract text content from a comment node
|
|
264
|
+
*/
|
|
265
|
+
function extractCommentText(node: SyntaxNode, language: Language): string | null {
|
|
266
|
+
const text = node.text
|
|
267
|
+
|
|
268
|
+
// Skip TypeScript triple-slash reference directives
|
|
269
|
+
if (isReferenceDirective(text)) {
|
|
270
|
+
return null
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Rust: line_comment may have doc_comment child with actual content
|
|
274
|
+
if (language === 'rust' && node.type === 'line_comment') {
|
|
275
|
+
const docComment = findChild(node, 'doc_comment')
|
|
276
|
+
if (docComment) {
|
|
277
|
+
return docComment.text.trim()
|
|
278
|
+
}
|
|
279
|
+
// Regular // comment - strip prefix
|
|
280
|
+
return stripLinePrefix(text, '//')
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Block comment /* */ or /** */ (including Rust block_comment)
|
|
284
|
+
if (text.startsWith('/*') || node.type === 'block_comment') {
|
|
285
|
+
return extractBlockCommentText(text)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Line comment // or #
|
|
289
|
+
if (text.startsWith('//')) {
|
|
290
|
+
return stripLinePrefix(text, '//')
|
|
291
|
+
}
|
|
292
|
+
if (text.startsWith('#')) {
|
|
293
|
+
return stripLinePrefix(text, '#')
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
return text.trim()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Strip comment prefix and optional following space
|
|
301
|
+
* Handles //!, ///, //, ##, #
|
|
302
|
+
*/
|
|
303
|
+
function stripLinePrefix(text: string, prefix: string): string {
|
|
304
|
+
let content = text.slice(prefix.length)
|
|
305
|
+
// Strip optional ! or / after // (for //! and ///)
|
|
306
|
+
if (prefix === '//' && (content.startsWith('!') || content.startsWith('/'))) {
|
|
307
|
+
content = content.slice(1)
|
|
308
|
+
}
|
|
309
|
+
// Strip optional extra # after # (for ##)
|
|
310
|
+
if (prefix === '#' && content.startsWith('#')) {
|
|
311
|
+
content = content.slice(1)
|
|
312
|
+
}
|
|
313
|
+
// Strip optional leading space
|
|
314
|
+
if (content.startsWith(' ')) {
|
|
315
|
+
content = content.slice(1)
|
|
316
|
+
}
|
|
317
|
+
return content.trimEnd()
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Extract text from block comment, stripping delimiters and * prefixes
|
|
322
|
+
*/
|
|
323
|
+
function extractBlockCommentText(text: string): string {
|
|
324
|
+
// Remove /* and */
|
|
325
|
+
let content = text.slice(2)
|
|
326
|
+
if (content.endsWith('*/')) {
|
|
327
|
+
content = content.slice(0, -2)
|
|
328
|
+
}
|
|
329
|
+
// Remove leading * for JSDoc style
|
|
330
|
+
if (content.startsWith('*')) {
|
|
331
|
+
content = content.slice(1)
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Process lines, removing * prefixes
|
|
335
|
+
const lines = content.split('\n').map(line => {
|
|
336
|
+
const trimmed = line.trim()
|
|
337
|
+
if (trimmed.startsWith('* ')) {
|
|
338
|
+
return trimmed.slice(2)
|
|
339
|
+
}
|
|
340
|
+
if (trimmed === '*') {
|
|
341
|
+
return ''
|
|
342
|
+
}
|
|
343
|
+
if (trimmed.startsWith('*')) {
|
|
344
|
+
return trimmed.slice(1).trim()
|
|
345
|
+
}
|
|
346
|
+
return trimmed
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
return lines.join('\n').trim()
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Extract Python docstring content from string node
|
|
354
|
+
*/
|
|
355
|
+
function extractPythonDocstring(node: SyntaxNode): string {
|
|
356
|
+
// Find string_content child which has the actual text
|
|
357
|
+
const content = findChild(node, 'string_content')
|
|
358
|
+
if (content) {
|
|
359
|
+
const lines = content.text.trim().split('\n')
|
|
360
|
+
return truncateDescription(lines)
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Fallback: extract from full text
|
|
364
|
+
let text = node.text
|
|
365
|
+
// Remove triple quotes
|
|
366
|
+
if (text.startsWith('"""') || text.startsWith("'''")) {
|
|
367
|
+
text = text.slice(3)
|
|
368
|
+
}
|
|
369
|
+
if (text.endsWith('"""') || text.endsWith("'''")) {
|
|
370
|
+
text = text.slice(0, -3)
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
const lines = text.trim().split('\n')
|
|
374
|
+
return truncateDescription(lines)
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Get all children of a node as array
|
|
379
|
+
*/
|
|
380
|
+
function getChildren(node: SyntaxNode): SyntaxNode[] {
|
|
381
|
+
const children: SyntaxNode[] = []
|
|
382
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
383
|
+
const child = node.child(i)
|
|
384
|
+
if (child) children.push(child)
|
|
385
|
+
}
|
|
386
|
+
return children
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Find first child of given type
|
|
391
|
+
*/
|
|
392
|
+
function findChild(node: SyntaxNode, type: string): SyntaxNode | null {
|
|
393
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
394
|
+
const child = node.child(i)
|
|
395
|
+
if (child?.type === type) return child
|
|
396
|
+
}
|
|
397
|
+
return null
|
|
398
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// Tests for submodule detection and parsing logic.
|
|
2
|
+
|
|
3
|
+
import { describe, expect, test } from 'bun:test'
|
|
4
|
+
import { getSubmodules, getSubmodulePaths } from './submodules.js'
|
|
5
|
+
import { getAllDiffData, parseNumstat, parseDiff } from './git-status.js'
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Integration: submodule detection in current repo (no submodules expected)
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
describe('getSubmodules', () => {
|
|
12
|
+
test('returns empty array when repo has no submodules', () => {
|
|
13
|
+
const result = getSubmodules(process.cwd())
|
|
14
|
+
expect(result).toMatchInlineSnapshot(`[]`)
|
|
15
|
+
})
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
describe('getSubmodulePaths', () => {
|
|
19
|
+
test('returns empty set when repo has no submodules', () => {
|
|
20
|
+
const result = getSubmodulePaths(process.cwd())
|
|
21
|
+
expect(result.size).toBe(0)
|
|
22
|
+
})
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
// ============================================================================
|
|
26
|
+
// Diff filtering: submodule paths should be removed from diff output
|
|
27
|
+
// ============================================================================
|
|
28
|
+
|
|
29
|
+
describe('diff submodule filtering', () => {
|
|
30
|
+
test('parseNumstat includes submodule pointer changes as 1/1', () => {
|
|
31
|
+
// Simulates what git diff --numstat outputs for a submodule pointer change
|
|
32
|
+
const output = `1\t1\tvendor/some-lib
|
|
33
|
+
10\t5\tsrc/main.ts`
|
|
34
|
+
const result = parseNumstat(output)
|
|
35
|
+
// Without filtering, the submodule shows as 1 added / 1 deleted
|
|
36
|
+
expect(result.size).toBe(2)
|
|
37
|
+
expect(result.get('vendor/some-lib')).toMatchInlineSnapshot(`
|
|
38
|
+
{
|
|
39
|
+
"added": 1,
|
|
40
|
+
"deleted": 1,
|
|
41
|
+
}
|
|
42
|
+
`)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
test('getAllDiffData filters out submodule paths', () => {
|
|
46
|
+
const submodulePaths = new Set(['vendor/some-lib', 'external/utils'])
|
|
47
|
+
// This tests the filtering logic with real git commands on the current repo.
|
|
48
|
+
// Since this repo has no submodules, the filter set won't match anything,
|
|
49
|
+
// but we verify the function accepts the parameter without error.
|
|
50
|
+
const result = getAllDiffData(process.cwd(), submodulePaths)
|
|
51
|
+
expect(result.fileStats).toBeDefined()
|
|
52
|
+
expect(result.fileDiffs).toBeDefined()
|
|
53
|
+
// Verify submodule paths are not in the results
|
|
54
|
+
expect(result.fileStats.has('vendor/some-lib')).toBe(false)
|
|
55
|
+
expect(result.fileDiffs.has('vendor/some-lib')).toBe(false)
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('parseDiff handles submodule pseudo-diff gracefully', () => {
|
|
59
|
+
// Git produces this pseudo-diff for submodule pointer changes
|
|
60
|
+
const diffOutput = `diff --git a/vendor/lib b/vendor/lib
|
|
61
|
+
index abc1234..def5678 160000
|
|
62
|
+
--- a/vendor/lib
|
|
63
|
+
+++ b/vendor/lib
|
|
64
|
+
@@ -1 +1 @@
|
|
65
|
+
-Subproject commit abc1234567890abcdef1234567890abcdef123456
|
|
66
|
+
+Subproject commit def5678901234567890abcdef1234567890abcdef`
|
|
67
|
+
const result = parseDiff(diffOutput)
|
|
68
|
+
// Parser will extract a hunk but it's meaningless for submodules.
|
|
69
|
+
// The important thing is it doesn't crash.
|
|
70
|
+
expect(result.has('vendor/lib')).toBe(true)
|
|
71
|
+
// In practice, getAllDiffData filters this out via submodulePaths
|
|
72
|
+
})
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
// ============================================================================
|
|
76
|
+
// Builder: submodule entry formatting (tested via types)
|
|
77
|
+
// ============================================================================
|
|
78
|
+
|
|
79
|
+
describe('SubmoduleEntry format', () => {
|
|
80
|
+
test('formats initialized submodule with branch', () => {
|
|
81
|
+
// This tests the format that builder.ts produces
|
|
82
|
+
const label = 'main @ a1b2c3d'
|
|
83
|
+
expect(label).toMatchInlineSnapshot(`"main @ a1b2c3d"`)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('formats detached HEAD submodule', () => {
|
|
87
|
+
const label = 'detached @ f4e5d6c'
|
|
88
|
+
expect(label).toMatchInlineSnapshot(`"detached @ f4e5d6c"`)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
test('formats uninitialized submodule', () => {
|
|
92
|
+
const label = 'uninitialized @ abc1234'
|
|
93
|
+
expect(label).toMatchInlineSnapshot(`"uninitialized @ abc1234"`)
|
|
94
|
+
})
|
|
95
|
+
})
|