agentmap 0.8.0 → 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 +37 -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 +7 -2
- package/dist/extract/git-status.d.ts.map +1 -1
- package/dist/extract/git-status.js +12 -18
- 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/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -4
- 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.map +1 -1
- package/dist/map/builder.js +23 -12
- 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 +80 -11
- package/dist/map/truncate.js.map +1 -1
- package/dist/scanner.d.ts.map +1 -1
- package/dist/scanner.js +164 -65
- 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 +15 -1
- 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
package/src/scanner.ts
ADDED
|
@@ -0,0 +1,364 @@
|
|
|
1
|
+
// Scan directory for files with header comments/docstrings and recurse into submodules.
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
import { realpathSync } from 'fs'
|
|
5
|
+
import pLimit from 'p-limit'
|
|
6
|
+
import picomatch from 'picomatch'
|
|
7
|
+
import { readFile } from 'fs/promises'
|
|
8
|
+
import { join, normalize } from 'path'
|
|
9
|
+
import { extractMarkerFromCode, extractMarkdownDescription } from './extract/marker.js'
|
|
10
|
+
import { extractDefinitions } from './extract/definitions.js'
|
|
11
|
+
import { getAllDiffData, applyDiffToDefinitions } from './extract/git-status.js'
|
|
12
|
+
import { getSubmodules, getSubmodulePaths } from './extract/submodules.js'
|
|
13
|
+
import { createConsoleLogger } from './logger.js'
|
|
14
|
+
import { parseCode, detectLanguage, LANGUAGE_EXTENSIONS } from './parser/index.js'
|
|
15
|
+
import type { FileResult, GenerateOptions, FileDiff, FileDiffStats, SubmoduleInfo } from './types.js'
|
|
16
|
+
import type { Logger } from './logger.js'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Maximum number of files to process (safety limit)
|
|
20
|
+
* If exceeded, returns empty results to avoid scanning huge directories
|
|
21
|
+
*/
|
|
22
|
+
const MAX_FILES = 5_000_000
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Supported file extensions (from LANGUAGE_EXTENSIONS)
|
|
26
|
+
*/
|
|
27
|
+
const SUPPORTED_EXTENSIONS = new Set(Object.keys(LANGUAGE_EXTENSIONS))
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if a file has a supported extension
|
|
31
|
+
*/
|
|
32
|
+
function isSupportedFile(filepath: string): boolean {
|
|
33
|
+
const ext = filepath.slice(filepath.lastIndexOf('.'))
|
|
34
|
+
return SUPPORTED_EXTENSIONS.has(ext)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if a file is a README file (case-insensitive, with or without .md extension)
|
|
39
|
+
*/
|
|
40
|
+
function isReadmeFile(filepath: string): boolean {
|
|
41
|
+
const filename = filepath.split(/[/\\]/).pop()?.toLowerCase() ?? ''
|
|
42
|
+
return filename === 'readme.md' || filename === 'readme'
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* A tracked file with its blob SHA (for dedup) and normalized path.
|
|
47
|
+
*/
|
|
48
|
+
interface GitFileEntry {
|
|
49
|
+
path: string
|
|
50
|
+
sha: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get tracked files using git ls-files -z -s.
|
|
55
|
+
* NUL-delimited for safe cross-platform parsing of any filename.
|
|
56
|
+
* Filters out symlinks (mode 120000).
|
|
57
|
+
* Returns path + blob SHA for duplicate detection.
|
|
58
|
+
*
|
|
59
|
+
* Format per entry: "<mode> <sha> <stage>\t<path>\0"
|
|
60
|
+
*/
|
|
61
|
+
function getGitFiles(dir: string): GitFileEntry[] {
|
|
62
|
+
const maxBuffer = 1024 * 10000000
|
|
63
|
+
try {
|
|
64
|
+
const stdout = execSync('git ls-files -z -s', {
|
|
65
|
+
cwd: dir,
|
|
66
|
+
maxBuffer,
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const results: GitFileEntry[] = []
|
|
71
|
+
|
|
72
|
+
// Split on NUL byte, filter empty trailing entry
|
|
73
|
+
const entries = stdout.split('\0').filter(Boolean)
|
|
74
|
+
for (const entry of entries) {
|
|
75
|
+
// Format: "<mode> <sha> <stage>\t<path>"
|
|
76
|
+
const tabIdx = entry.indexOf('\t')
|
|
77
|
+
if (tabIdx === -1) continue
|
|
78
|
+
|
|
79
|
+
const meta = entry.slice(0, tabIdx)
|
|
80
|
+
const path = entry.slice(tabIdx + 1)
|
|
81
|
+
|
|
82
|
+
const spaceIdx = meta.indexOf(' ')
|
|
83
|
+
if (spaceIdx === -1) continue
|
|
84
|
+
|
|
85
|
+
const mode = meta.slice(0, spaceIdx)
|
|
86
|
+
const sha = meta.slice(spaceIdx + 1, meta.indexOf(' ', spaceIdx + 1))
|
|
87
|
+
|
|
88
|
+
// Skip symlinks (mode 120000)
|
|
89
|
+
if (mode === '120000') continue
|
|
90
|
+
|
|
91
|
+
results.push({ path: normalize(path), sha })
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return results
|
|
95
|
+
} catch {
|
|
96
|
+
return []
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Build a map of blob SHA → shortest path for duplicate detection.
|
|
102
|
+
* Files sharing the same blob SHA are exact duplicates in git.
|
|
103
|
+
*/
|
|
104
|
+
function buildDuplicateMap(files: GitFileEntry[], pathPrefix: string): Map<string, string> {
|
|
105
|
+
// Group paths by SHA
|
|
106
|
+
const shaToEntries = new Map<string, string[]>()
|
|
107
|
+
for (const { path, sha } of files) {
|
|
108
|
+
const prefixed = joinRelativePath(pathPrefix, path)
|
|
109
|
+
const existing = shaToEntries.get(sha)
|
|
110
|
+
if (existing) {
|
|
111
|
+
existing.push(prefixed)
|
|
112
|
+
} else {
|
|
113
|
+
shaToEntries.set(sha, [prefixed])
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// For each group with >1 file, shortest path is the original
|
|
118
|
+
const duplicateOf = new Map<string, string>()
|
|
119
|
+
for (const paths of shaToEntries.values()) {
|
|
120
|
+
if (paths.length < 2) continue
|
|
121
|
+
paths.sort((a, b) => a.length - b.length || a.localeCompare(b))
|
|
122
|
+
const original = paths[0]
|
|
123
|
+
for (let i = 1; i < paths.length; i++) {
|
|
124
|
+
duplicateOf.set(paths[i], original)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return duplicateOf
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Result of scanning a directory, including both file results and submodule info
|
|
135
|
+
*/
|
|
136
|
+
export interface ScanResult {
|
|
137
|
+
files: FileResult[]
|
|
138
|
+
submodules: SubmoduleInfo[]
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
interface ScanRepoOptions {
|
|
142
|
+
repoDir: string
|
|
143
|
+
pathPrefix: string
|
|
144
|
+
includeDiff: boolean
|
|
145
|
+
includeSubmodules: boolean
|
|
146
|
+
logger: Logger
|
|
147
|
+
isIncluded?: (path: string) => boolean
|
|
148
|
+
isIgnored?: (path: string) => boolean
|
|
149
|
+
visitedRepoDirs: Set<string>
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function normalizeRelativePath(path: string): string {
|
|
153
|
+
return path.replace(/\\/g, '/')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function joinRelativePath(prefix: string, path: string): string {
|
|
157
|
+
const normalizedPath = normalizeRelativePath(path)
|
|
158
|
+
return prefix ? `${prefix}/${normalizedPath}` : normalizedPath
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function getCanonicalRepoDir(dir: string): string {
|
|
162
|
+
try {
|
|
163
|
+
return realpathSync(dir)
|
|
164
|
+
} catch {
|
|
165
|
+
return dir
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function scanRepo(options: ScanRepoOptions): Promise<ScanResult> {
|
|
170
|
+
const canonicalRepoDir = getCanonicalRepoDir(options.repoDir)
|
|
171
|
+
if (options.visitedRepoDirs.has(canonicalRepoDir)) {
|
|
172
|
+
return { files: [], submodules: [] }
|
|
173
|
+
}
|
|
174
|
+
options.visitedRepoDirs.add(canonicalRepoDir)
|
|
175
|
+
|
|
176
|
+
let submodules: SubmoduleInfo[] = []
|
|
177
|
+
const directSubmodules = options.includeSubmodules ? getSubmodules(options.repoDir) : []
|
|
178
|
+
const directSubmodulePathSet = options.includeSubmodules
|
|
179
|
+
? new Set(directSubmodules.map(submodule => submodule.path))
|
|
180
|
+
: getSubmodulePaths(options.repoDir)
|
|
181
|
+
|
|
182
|
+
if (options.includeSubmodules) {
|
|
183
|
+
submodules = directSubmodules.map(submodule => ({
|
|
184
|
+
...submodule,
|
|
185
|
+
path: joinRelativePath(options.pathPrefix, submodule.path),
|
|
186
|
+
}))
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const normalizedSubmodulePaths = new Set<string>()
|
|
190
|
+
for (const path of directSubmodulePathSet) {
|
|
191
|
+
normalizedSubmodulePaths.add(normalize(path))
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const allGitFiles = getGitFiles(options.repoDir)
|
|
195
|
+
|
|
196
|
+
// Build duplicate map before filtering (needs all files to detect dupes)
|
|
197
|
+
const duplicateOf = buildDuplicateMap(allGitFiles, options.pathPrefix)
|
|
198
|
+
|
|
199
|
+
let gitFiles = allGitFiles.filter(f => !normalizedSubmodulePaths.has(f.path))
|
|
200
|
+
gitFiles = gitFiles.filter(f => isSupportedFile(f.path) || isReadmeFile(f.path))
|
|
201
|
+
|
|
202
|
+
gitFiles = gitFiles.filter(f => {
|
|
203
|
+
const relativePath = joinRelativePath(options.pathPrefix, f.path)
|
|
204
|
+
|
|
205
|
+
if (options.isIncluded && !options.isIncluded(relativePath)) {
|
|
206
|
+
return false
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (options.isIgnored && options.isIgnored(relativePath)) {
|
|
210
|
+
return false
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return true
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
if (gitFiles.length > MAX_FILES) {
|
|
217
|
+
options.logger?.warn(`Warning: Too many files (${gitFiles.length} > ${MAX_FILES}), skipping scan`)
|
|
218
|
+
return { files: [], submodules }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let fileStats: Map<string, FileDiffStats> | null = null
|
|
222
|
+
let fileDiffs: Map<string, FileDiff> | null = null
|
|
223
|
+
|
|
224
|
+
if (options.includeDiff) {
|
|
225
|
+
try {
|
|
226
|
+
const diffData = getAllDiffData(options.repoDir, directSubmodulePathSet, options.logger)
|
|
227
|
+
fileStats = diffData.fileStats
|
|
228
|
+
fileDiffs = diffData.fileDiffs
|
|
229
|
+
} catch {
|
|
230
|
+
fileStats = null
|
|
231
|
+
fileDiffs = null
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
const limit = pLimit(20)
|
|
236
|
+
const resultPromises = gitFiles.map(({ path: relativePath }) => {
|
|
237
|
+
const fullPath = join(options.repoDir, relativePath)
|
|
238
|
+
const normalizedPath = normalizeRelativePath(relativePath)
|
|
239
|
+
const prefixedRelativePath = joinRelativePath(options.pathPrefix, relativePath)
|
|
240
|
+
const fileDiff = fileDiffs?.get(normalizedPath)
|
|
241
|
+
const stats = fileStats?.get(normalizedPath)
|
|
242
|
+
const dupOriginal = duplicateOf.get(prefixedRelativePath)
|
|
243
|
+
|
|
244
|
+
return limit(async () => {
|
|
245
|
+
try {
|
|
246
|
+
// If this file is a duplicate, return a stub pointing to the original
|
|
247
|
+
if (dupOriginal) {
|
|
248
|
+
return {
|
|
249
|
+
relativePath: prefixedRelativePath,
|
|
250
|
+
duplicateOf: dupOriginal,
|
|
251
|
+
definitions: [],
|
|
252
|
+
} satisfies FileResult
|
|
253
|
+
}
|
|
254
|
+
return await processFile(fullPath, prefixedRelativePath, fileDiff, stats)
|
|
255
|
+
} catch {
|
|
256
|
+
return null
|
|
257
|
+
}
|
|
258
|
+
})
|
|
259
|
+
})
|
|
260
|
+
|
|
261
|
+
const nestedResults = options.includeSubmodules
|
|
262
|
+
? await Promise.all(directSubmodules.map(async submodule => {
|
|
263
|
+
if (!submodule.initialized) {
|
|
264
|
+
return { files: [], submodules: [] }
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return scanRepo({
|
|
268
|
+
...options,
|
|
269
|
+
repoDir: join(options.repoDir, submodule.path),
|
|
270
|
+
pathPrefix: joinRelativePath(options.pathPrefix, submodule.path),
|
|
271
|
+
})
|
|
272
|
+
}))
|
|
273
|
+
: []
|
|
274
|
+
|
|
275
|
+
const results = await Promise.all(resultPromises)
|
|
276
|
+
|
|
277
|
+
for (const nested of nestedResults) {
|
|
278
|
+
submodules.push(...nested.submodules)
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return {
|
|
282
|
+
files: [
|
|
283
|
+
...results.filter((result): result is FileResult => result !== null),
|
|
284
|
+
...nestedResults.flatMap(result => result.files),
|
|
285
|
+
],
|
|
286
|
+
submodules,
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Scan directory and process files with header comments
|
|
292
|
+
*/
|
|
293
|
+
export async function scanDirectory(options: GenerateOptions = {}): Promise<ScanResult> {
|
|
294
|
+
const dir = options.dir ?? process.cwd()
|
|
295
|
+
const logger = options.logger ?? createConsoleLogger()
|
|
296
|
+
const ignorePatterns = (options.ignore ?? []).filter((p): p is string => !!p)
|
|
297
|
+
const filterPatterns = (options.filter ?? []).filter((p): p is string => !!p)
|
|
298
|
+
return scanRepo({
|
|
299
|
+
repoDir: dir,
|
|
300
|
+
pathPrefix: '',
|
|
301
|
+
includeDiff: options.diff ?? false,
|
|
302
|
+
includeSubmodules: options.submodules !== false,
|
|
303
|
+
logger,
|
|
304
|
+
isIncluded: filterPatterns.length > 0 ? picomatch(filterPatterns) : undefined,
|
|
305
|
+
isIgnored: ignorePatterns.length > 0 ? picomatch(ignorePatterns) : undefined,
|
|
306
|
+
visitedRepoDirs: new Set(),
|
|
307
|
+
})
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Process a single file - check for marker and extract definitions
|
|
312
|
+
*/
|
|
313
|
+
async function processFile(
|
|
314
|
+
fullPath: string,
|
|
315
|
+
relativePath: string,
|
|
316
|
+
fileDiff?: FileDiff,
|
|
317
|
+
fileStats?: FileDiffStats
|
|
318
|
+
): Promise<FileResult | null> {
|
|
319
|
+
// Handle README.md files specially
|
|
320
|
+
if (isReadmeFile(relativePath)) {
|
|
321
|
+
const description = await extractMarkdownDescription(fullPath)
|
|
322
|
+
if (!description) {
|
|
323
|
+
return null
|
|
324
|
+
}
|
|
325
|
+
return {
|
|
326
|
+
relativePath,
|
|
327
|
+
description,
|
|
328
|
+
definitions: [],
|
|
329
|
+
diff: fileStats,
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Detect language first
|
|
334
|
+
const language = detectLanguage(relativePath)
|
|
335
|
+
if (!language) {
|
|
336
|
+
return null
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// Read file once for both marker extraction and definition parsing
|
|
340
|
+
const code = await readFile(fullPath, 'utf8')
|
|
341
|
+
|
|
342
|
+
// Check for marker using the code we already read
|
|
343
|
+
const marker = await extractMarkerFromCode(code, language)
|
|
344
|
+
if (!marker.found) {
|
|
345
|
+
return null
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// Parse and extract definitions using the same code
|
|
349
|
+
const tree = await parseCode(code, language)
|
|
350
|
+
let definitions = extractDefinitions(tree.rootNode, language)
|
|
351
|
+
|
|
352
|
+
// Apply diff info if available (for definition-level stats)
|
|
353
|
+
if (fileDiff) {
|
|
354
|
+
definitions = applyDiffToDefinitions(definitions, fileDiff)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
return {
|
|
358
|
+
relativePath,
|
|
359
|
+
description: marker.description,
|
|
360
|
+
definitions,
|
|
361
|
+
// Use pre-calculated file stats from --numstat (more reliable)
|
|
362
|
+
diff: fileStats,
|
|
363
|
+
}
|
|
364
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// Helpers for creating temporary git repositories in tests.
|
|
2
|
+
|
|
3
|
+
import { execSync } from 'child_process'
|
|
4
|
+
import { mkdtempSync, mkdirSync, writeFileSync } from 'fs'
|
|
5
|
+
import { tmpdir } from 'os'
|
|
6
|
+
import { join } from 'path'
|
|
7
|
+
|
|
8
|
+
export interface TempRepo {
|
|
9
|
+
dir: string
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function run(command: string, cwd: string): string {
|
|
13
|
+
return execSync(command, {
|
|
14
|
+
cwd,
|
|
15
|
+
encoding: 'utf8',
|
|
16
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function createTempDir(prefix: string): string {
|
|
21
|
+
return mkdtempSync(join(tmpdir(), prefix))
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function initRepo(dir: string): TempRepo {
|
|
25
|
+
run('git init', dir)
|
|
26
|
+
run('git config user.name "agentmap tests"', dir)
|
|
27
|
+
run('git config user.email "agentmap@example.com"', dir)
|
|
28
|
+
return { dir }
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function createRepo(prefix: string): TempRepo {
|
|
32
|
+
const dir = createTempDir(prefix)
|
|
33
|
+
mkdirSync(dir, { recursive: true })
|
|
34
|
+
return initRepo(dir)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeTrackedFile(repo: TempRepo, relativePath: string, content: string): void {
|
|
38
|
+
const filePath = join(repo.dir, relativePath)
|
|
39
|
+
mkdirSync(join(filePath, '..'), { recursive: true })
|
|
40
|
+
writeFileSync(filePath, content, 'utf8')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function commitAll(repo: TempRepo, message: string): void {
|
|
44
|
+
run('git add .', repo.dir)
|
|
45
|
+
run(`git commit -m ${JSON.stringify(message)}`, repo.dir)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function shortHead(repo: TempRepo): string {
|
|
49
|
+
return run('git rev-parse --short HEAD', repo.dir).trim()
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function addSubmodule(repo: TempRepo, sourceDir: string, targetPath: string): void {
|
|
53
|
+
run(`git -c protocol.file.allow=always submodule add ${JSON.stringify(sourceDir)} ${JSON.stringify(targetPath)}`, repo.dir)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function updateSubmodulesRecursive(repo: TempRepo): void {
|
|
57
|
+
run('git -c protocol.file.allow=always submodule update --init --recursive', repo.dir)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function deinitSubmodule(repo: TempRepo, targetPath: string): void {
|
|
61
|
+
run(`git submodule deinit -f ${JSON.stringify(targetPath)}`, repo.dir)
|
|
62
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
// Core type definitions for the codebase map.
|
|
2
|
+
|
|
3
|
+
import type Parser from 'web-tree-sitter'
|
|
4
|
+
import type { Logger } from './logger.js'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Supported programming languages
|
|
8
|
+
*/
|
|
9
|
+
export type Language =
|
|
10
|
+
| 'typescript'
|
|
11
|
+
| 'javascript'
|
|
12
|
+
| 'python'
|
|
13
|
+
| 'rust'
|
|
14
|
+
| 'go'
|
|
15
|
+
| 'zig'
|
|
16
|
+
| 'cpp'
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Symbol definitions mapping: name -> description string
|
|
20
|
+
*/
|
|
21
|
+
export interface DefEntry {
|
|
22
|
+
[symbolName: string]: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Git diff stats for a file (total lines added/deleted)
|
|
27
|
+
*/
|
|
28
|
+
export interface FileDiffStats {
|
|
29
|
+
added: number
|
|
30
|
+
deleted: number
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* A file entry in the map
|
|
35
|
+
*/
|
|
36
|
+
export interface FileEntry {
|
|
37
|
+
description?: string
|
|
38
|
+
diff?: string // formatted as "+N-M" or "+N" or "-M"
|
|
39
|
+
defs?: DefEntry
|
|
40
|
+
exports?: DefEntry // used instead of defs when truncating files with exported symbols
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A submodule entry in the map
|
|
45
|
+
*/
|
|
46
|
+
export interface SubmoduleEntry {
|
|
47
|
+
submodule: string // "branch @ sha" or "detached @ sha"
|
|
48
|
+
dirty?: string // "modified" if submodule has uncommitted changes
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A submodule node in the map.
|
|
53
|
+
* Carries submodule metadata and can also contain nested files/directories.
|
|
54
|
+
*/
|
|
55
|
+
export interface SubmoduleNode extends SubmoduleEntry {
|
|
56
|
+
[name: string]: MapNode | FileEntry | SubmoduleNode | string | undefined
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Info about a git submodule discovered in the repo
|
|
61
|
+
*/
|
|
62
|
+
export interface SubmoduleInfo {
|
|
63
|
+
/** Relative path of the submodule in the parent repo */
|
|
64
|
+
path: string
|
|
65
|
+
/** Current HEAD commit SHA (short) */
|
|
66
|
+
commit: string
|
|
67
|
+
/** Checked-out branch name, or undefined if detached HEAD */
|
|
68
|
+
branch?: string
|
|
69
|
+
/** Remote URL from .gitmodules */
|
|
70
|
+
url?: string
|
|
71
|
+
/** Whether the submodule has uncommitted changes */
|
|
72
|
+
dirty?: boolean
|
|
73
|
+
/** Whether the submodule is initialized */
|
|
74
|
+
initialized: boolean
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Recursive map node - either a directory (with children), a file entry, or a submodule entry
|
|
79
|
+
*/
|
|
80
|
+
export interface MapNode {
|
|
81
|
+
[name: string]: MapNode | FileEntry | SubmoduleNode
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Result of extracting marker and description from a file
|
|
86
|
+
*/
|
|
87
|
+
export interface MarkerResult {
|
|
88
|
+
found: boolean
|
|
89
|
+
description?: string
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Types of definitions we extract
|
|
94
|
+
*/
|
|
95
|
+
export type DefinitionType =
|
|
96
|
+
| 'function'
|
|
97
|
+
| 'class'
|
|
98
|
+
| 'struct'
|
|
99
|
+
| 'union'
|
|
100
|
+
| 'trait'
|
|
101
|
+
| 'type'
|
|
102
|
+
| 'interface'
|
|
103
|
+
| 'const'
|
|
104
|
+
| 'enum'
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Git status for a definition
|
|
108
|
+
*/
|
|
109
|
+
export type DefinitionStatus = 'added' | 'updated'
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Git diff stats for a definition
|
|
113
|
+
*/
|
|
114
|
+
export interface DefinitionDiff {
|
|
115
|
+
status: DefinitionStatus
|
|
116
|
+
added: number // lines added
|
|
117
|
+
deleted: number // lines deleted
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* A definition extracted from source code
|
|
122
|
+
*/
|
|
123
|
+
export interface Definition {
|
|
124
|
+
name: string
|
|
125
|
+
line: number // 1-based start line
|
|
126
|
+
endLine: number // 1-based end line
|
|
127
|
+
type: DefinitionType
|
|
128
|
+
exported: boolean
|
|
129
|
+
extern?: boolean // true for extern declarations (C/C++/Zig)
|
|
130
|
+
diff?: DefinitionDiff // only present when --diff flag used
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Result of processing a single file
|
|
135
|
+
*/
|
|
136
|
+
export interface FileResult {
|
|
137
|
+
relativePath: string
|
|
138
|
+
description?: string
|
|
139
|
+
definitions: Definition[]
|
|
140
|
+
diff?: FileDiffStats // only present when --diff flag used
|
|
141
|
+
/** If set, this file is an exact duplicate of another file (by git blob SHA) */
|
|
142
|
+
duplicateOf?: string
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Options for generating the map
|
|
147
|
+
*/
|
|
148
|
+
export interface GenerateOptions {
|
|
149
|
+
/** Directory to scan (default: cwd) */
|
|
150
|
+
dir?: string
|
|
151
|
+
/** Glob patterns to ignore */
|
|
152
|
+
ignore?: string[]
|
|
153
|
+
/** Glob patterns to filter - only include matching files */
|
|
154
|
+
filter?: string[]
|
|
155
|
+
/** Include git diff status for definitions */
|
|
156
|
+
diff?: boolean
|
|
157
|
+
/** Git ref to diff against (default: HEAD for unstaged, --cached for staged) */
|
|
158
|
+
diffBase?: string
|
|
159
|
+
/** Max definitions per file before truncation (default: 25) */
|
|
160
|
+
maxDefs?: number
|
|
161
|
+
/** Max characters for file descriptions before truncation (default: 300). Rounds up to full line. */
|
|
162
|
+
maxDescChars?: number
|
|
163
|
+
/** Include submodule info in the map (default: true) */
|
|
164
|
+
submodules?: boolean
|
|
165
|
+
/** Logger implementation (default: console logger) */
|
|
166
|
+
logger?: Logger
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* A hunk from git diff output
|
|
171
|
+
*/
|
|
172
|
+
export interface DiffHunk {
|
|
173
|
+
oldStart: number
|
|
174
|
+
oldCount: number
|
|
175
|
+
newStart: number
|
|
176
|
+
newCount: number
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Parsed diff for a single file
|
|
181
|
+
*/
|
|
182
|
+
export interface FileDiff {
|
|
183
|
+
path: string
|
|
184
|
+
hunks: DiffHunk[]
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Re-export parser types
|
|
189
|
+
*/
|
|
190
|
+
export type SyntaxNode = Parser.SyntaxNode
|
|
191
|
+
export type SyntaxTree = Parser.Tree
|