agent-wiki-cli 0.3.28__py3-none-any.whl

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 (47) hide show
  1. agent_wiki_cli-0.3.28.dist-info/METADATA +425 -0
  2. agent_wiki_cli-0.3.28.dist-info/RECORD +47 -0
  3. agent_wiki_cli-0.3.28.dist-info/WHEEL +5 -0
  4. agent_wiki_cli-0.3.28.dist-info/entry_points.txt +2 -0
  5. agent_wiki_cli-0.3.28.dist-info/licenses/LICENSE +21 -0
  6. agent_wiki_cli-0.3.28.dist-info/top_level.txt +1 -0
  7. llm_wiki_cli/__init__.py +7 -0
  8. llm_wiki_cli/cli.py +231 -0
  9. llm_wiki_cli/commands/__init__.py +1 -0
  10. llm_wiki_cli/commands/bootstrap_cmd.py +1072 -0
  11. llm_wiki_cli/commands/bump_cmd.py +55 -0
  12. llm_wiki_cli/commands/context_cmd.py +427 -0
  13. llm_wiki_cli/commands/extract_cmd.py +745 -0
  14. llm_wiki_cli/commands/generate_prompt_cmd.py +89 -0
  15. llm_wiki_cli/commands/hook_cmd.py +161 -0
  16. llm_wiki_cli/commands/init_cmd.py +92 -0
  17. llm_wiki_cli/commands/lint_cmd.py +294 -0
  18. llm_wiki_cli/commands/migrate_cmd.py +892 -0
  19. llm_wiki_cli/commands/release_cmd.py +163 -0
  20. llm_wiki_cli/commands/status_cmd.py +70 -0
  21. llm_wiki_cli/commands/sync_cmd.py +521 -0
  22. llm_wiki_cli/commands/trigger_cmd.py +205 -0
  23. llm_wiki_cli/commands/uninstall_cmd.py +221 -0
  24. llm_wiki_cli/commands/upgrade_cmd.py +196 -0
  25. llm_wiki_cli/config.py +318 -0
  26. llm_wiki_cli/extractors/__init__.py +46 -0
  27. llm_wiki_cli/extractors/common.py +90 -0
  28. llm_wiki_cli/extractors/go_extractor.py +143 -0
  29. llm_wiki_cli/extractors/go_scripts/go.mod +3 -0
  30. llm_wiki_cli/extractors/go_scripts/main.go +668 -0
  31. llm_wiki_cli/extractors/python_extractor.py +346 -0
  32. llm_wiki_cli/extractors/rust_extractor.py +143 -0
  33. llm_wiki_cli/extractors/rust_scripts/Cargo.lock +110 -0
  34. llm_wiki_cli/extractors/rust_scripts/Cargo.toml +11 -0
  35. llm_wiki_cli/extractors/rust_scripts/src/main.rs +803 -0
  36. llm_wiki_cli/extractors/ts_extractor.py +206 -0
  37. llm_wiki_cli/extractors/ts_scripts/extract.js +485 -0
  38. llm_wiki_cli/extractors/ts_scripts/package.json +10 -0
  39. llm_wiki_cli/services/__init__.py +0 -0
  40. llm_wiki_cli/services/circuit_breaker.py +79 -0
  41. llm_wiki_cli/services/io.py +47 -0
  42. llm_wiki_cli/services/lockfile.py +60 -0
  43. llm_wiki_cli/services/packages.py +173 -0
  44. llm_wiki_cli/services/paths.py +31 -0
  45. llm_wiki_cli/services/schema.py +214 -0
  46. llm_wiki_cli/services/secure_file.py +22 -0
  47. llm_wiki_cli/services/versioning.py +193 -0
@@ -0,0 +1,668 @@
1
+ // Go AST extractor for agent-wiki-cli.
2
+ //
3
+ // Usage: go run . --src-dir <path> [--only-files <f1,f2,...>] [--deep]
4
+ //
5
+ // Outputs a JSON inventory to stdout (same canonical schema as PythonExtractor
6
+ // and TypeScriptExtractor). The "language" field is intentionally absent —
7
+ // GoExtractor stamps it in Python.
8
+ // Errors/warnings go to stderr. Exit 0 on success.
9
+ package main
10
+
11
+ import (
12
+ "encoding/json"
13
+ "flag"
14
+ "fmt"
15
+ "go/ast"
16
+ "go/parser"
17
+ "go/token"
18
+ "os"
19
+ "path/filepath"
20
+ "strings"
21
+ "unicode"
22
+ )
23
+
24
+ // ── Excluded directories ──────────────────────────────────────────────────────
25
+
26
+ var excludedDirs = map[string]bool{
27
+ ".cache": true,
28
+ ".direnv": true,
29
+ ".eggs": true,
30
+ ".env": true,
31
+ ".git": true,
32
+ ".mypy_cache": true,
33
+ ".next": true,
34
+ ".nox": true,
35
+ ".npm": true,
36
+ ".nuxt": true,
37
+ ".parcel-cache": true,
38
+ ".pnpm-store": true,
39
+ ".pyre": true,
40
+ ".pytest_cache": true,
41
+ ".ruff_cache": true,
42
+ ".svelte-kit": true,
43
+ ".tox": true,
44
+ ".venv": true,
45
+ ".virtualenv": true,
46
+ ".vite": true,
47
+ ".yarn": true,
48
+ "__pycache__": true,
49
+ "__pypackages__": true,
50
+ "bower_components": true,
51
+ "build": true,
52
+ "coverage": true,
53
+ "dist": true,
54
+ "env": true,
55
+ "htmlcov": true,
56
+ "jspm_packages": true,
57
+ "node_modules": true,
58
+ "out": true,
59
+ "site-packages": true,
60
+ "target": true,
61
+ "testdata": true,
62
+ "vendor": true,
63
+ "venv": true,
64
+ "virtualenv": true,
65
+ }
66
+
67
+ // ── Schema types ──────────────────────────────────────────────────────────────
68
+
69
+ type ParamInfo struct {
70
+ Name string `json:"name"`
71
+ Type string `json:"type,omitempty"`
72
+ }
73
+
74
+ type MethodInfo struct {
75
+ Name string `json:"name"`
76
+ Line int `json:"line"`
77
+ IsAsync bool `json:"is_async"`
78
+ Docstring string `json:"docstring,omitempty"`
79
+ Decorators []string `json:"decorators,omitempty"`
80
+ Params []ParamInfo `json:"params,omitempty"`
81
+ ReturnType string `json:"return_type,omitempty"`
82
+ }
83
+
84
+ type AttributeInfo struct {
85
+ Name string `json:"name"`
86
+ Type string `json:"type,omitempty"`
87
+ Default string `json:"default,omitempty"`
88
+ Tag string `json:"tag,omitempty"`
89
+ }
90
+
91
+ type ClassInfo struct {
92
+ Name string `json:"name"`
93
+ Kind string `json:"kind"`
94
+ Bases []string `json:"bases"`
95
+ Line int `json:"line"`
96
+ Docstring string `json:"docstring,omitempty"`
97
+ Decorators []string `json:"decorators,omitempty"`
98
+ Attributes []AttributeInfo `json:"attributes,omitempty"`
99
+ Methods []MethodInfo `json:"methods,omitempty"`
100
+ }
101
+
102
+ type FunctionInfo struct {
103
+ Name string `json:"name"`
104
+ Line int `json:"line"`
105
+ IsAsync bool `json:"is_async"`
106
+ Receiver string `json:"receiver,omitempty"`
107
+ Docstring string `json:"docstring,omitempty"`
108
+ Decorators []string `json:"decorators,omitempty"`
109
+ Params []ParamInfo `json:"params,omitempty"`
110
+ ReturnType string `json:"return_type,omitempty"`
111
+ }
112
+
113
+ type ImportInfo struct {
114
+ Module string `json:"module"`
115
+ Name string `json:"name"`
116
+ Alias *string `json:"alias"`
117
+ Type string `json:"type"`
118
+ }
119
+
120
+ type FileEntry struct {
121
+ Classes []ClassInfo `json:"classes"`
122
+ Functions []FunctionInfo `json:"functions"`
123
+ Imports []ImportInfo `json:"imports,omitempty"`
124
+ }
125
+
126
+ // ── Helpers ───────────────────────────────────────────────────────────────────
127
+
128
+ func isExported(name string) bool {
129
+ if name == "" {
130
+ return false
131
+ }
132
+ return unicode.IsUpper(rune(name[0]))
133
+ }
134
+
135
+ func exprToString(expr ast.Expr) string {
136
+ if expr == nil {
137
+ return ""
138
+ }
139
+ switch e := expr.(type) {
140
+ case *ast.Ident:
141
+ return e.Name
142
+ case *ast.SelectorExpr:
143
+ return exprToString(e.X) + "." + e.Sel.Name
144
+ case *ast.StarExpr:
145
+ return "*" + exprToString(e.X)
146
+ case *ast.ArrayType:
147
+ return "[]" + exprToString(e.Elt)
148
+ case *ast.MapType:
149
+ return "map[" + exprToString(e.Key) + "]" + exprToString(e.Value)
150
+ case *ast.InterfaceType:
151
+ return "interface{}"
152
+ case *ast.ChanType:
153
+ switch e.Dir {
154
+ case ast.SEND:
155
+ return "chan<- " + exprToString(e.Value)
156
+ case ast.RECV:
157
+ return "<-chan " + exprToString(e.Value)
158
+ default:
159
+ return "chan " + exprToString(e.Value)
160
+ }
161
+ case *ast.FuncType:
162
+ return "func(...)"
163
+ case *ast.Ellipsis:
164
+ return "..." + exprToString(e.Elt)
165
+ case *ast.IndexExpr:
166
+ return exprToString(e.X) + "[" + exprToString(e.Index) + "]"
167
+ case *ast.IndexListExpr:
168
+ parts := make([]string, len(e.Indices))
169
+ for i, idx := range e.Indices {
170
+ parts[i] = exprToString(idx)
171
+ }
172
+ return exprToString(e.X) + "[" + strings.Join(parts, ", ") + "]"
173
+ default:
174
+ return ""
175
+ }
176
+ }
177
+
178
+ func docText(doc *ast.CommentGroup) string {
179
+ if doc == nil {
180
+ return ""
181
+ }
182
+ return strings.TrimSpace(doc.Text())
183
+ }
184
+
185
+ func extractParams(fields *ast.FieldList) []ParamInfo {
186
+ if fields == nil {
187
+ return nil
188
+ }
189
+ var params []ParamInfo
190
+ for _, f := range fields.List {
191
+ typeStr := exprToString(f.Type)
192
+ if len(f.Names) == 0 {
193
+ // Unnamed parameter (e.g. in interface method signatures)
194
+ params = append(params, ParamInfo{Name: "", Type: typeStr})
195
+ } else {
196
+ for _, name := range f.Names {
197
+ params = append(params, ParamInfo{Name: name.Name, Type: typeStr})
198
+ }
199
+ }
200
+ }
201
+ return params
202
+ }
203
+
204
+ func extractReturnType(fields *ast.FieldList) string {
205
+ if fields == nil || len(fields.List) == 0 {
206
+ return ""
207
+ }
208
+ if len(fields.List) == 1 && len(fields.List[0].Names) == 0 {
209
+ return exprToString(fields.List[0].Type)
210
+ }
211
+ // Multiple return values: (Type1, Type2, ...)
212
+ parts := make([]string, 0, len(fields.List))
213
+ for _, f := range fields.List {
214
+ typeStr := exprToString(f.Type)
215
+ if len(f.Names) == 0 {
216
+ parts = append(parts, typeStr)
217
+ } else {
218
+ for _, name := range f.Names {
219
+ parts = append(parts, name.Name+" "+typeStr)
220
+ }
221
+ }
222
+ }
223
+ return "(" + strings.Join(parts, ", ") + ")"
224
+ }
225
+
226
+ func receiverTypeName(fn *ast.FuncDecl) string {
227
+ if fn.Recv == nil || len(fn.Recv.List) == 0 {
228
+ return ""
229
+ }
230
+ t := fn.Recv.List[0].Type
231
+ // Dereference pointer receivers: *T → T
232
+ if star, ok := t.(*ast.StarExpr); ok {
233
+ t = star.X
234
+ }
235
+ if ident, ok := t.(*ast.Ident); ok {
236
+ return ident.Name
237
+ }
238
+ // Generic receiver: T[K] — use the base type name.
239
+ if idx, ok := t.(*ast.IndexExpr); ok {
240
+ if ident, ok := idx.X.(*ast.Ident); ok {
241
+ return ident.Name
242
+ }
243
+ }
244
+ if idx, ok := t.(*ast.IndexListExpr); ok {
245
+ if ident, ok := idx.X.(*ast.Ident); ok {
246
+ return ident.Name
247
+ }
248
+ }
249
+ return ""
250
+ }
251
+
252
+ // ── File collection ───────────────────────────────────────────────────────────
253
+
254
+ func collectGoFiles(root string) ([]string, error) {
255
+ var files []string
256
+ err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
257
+ if err != nil {
258
+ return nil // skip unreadable entries
259
+ }
260
+ if info.IsDir() {
261
+ name := info.Name()
262
+ if excludedDirs[name] || strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") {
263
+ return filepath.SkipDir
264
+ }
265
+ return nil
266
+ }
267
+ if filepath.Ext(path) == ".go" && !strings.HasSuffix(info.Name(), "_test.go") {
268
+ files = append(files, path)
269
+ }
270
+ return nil
271
+ })
272
+ return files, err
273
+ }
274
+
275
+ func isOutsideRoot(rel string) bool {
276
+ return rel == ".." || filepath.IsAbs(rel) || strings.HasPrefix(rel, ".."+string(filepath.Separator))
277
+ }
278
+
279
+ func hasExcludedDir(relPath string) bool {
280
+ dir := filepath.Dir(relPath)
281
+ if dir == "." || dir == "" {
282
+ return false
283
+ }
284
+ for _, part := range strings.Split(filepath.ToSlash(dir), "/") {
285
+ if part == "" {
286
+ continue
287
+ }
288
+ if excludedDirs[part] || strings.HasPrefix(part, ".") || strings.HasPrefix(part, "_") {
289
+ return true
290
+ }
291
+ }
292
+ return false
293
+ }
294
+
295
+ // ── Per-file extraction ───────────────────────────────────────────────────────
296
+
297
+ func extractFile(filename string, fset *token.FileSet, deep bool) (*FileEntry, error) {
298
+ src, err := os.ReadFile(filename)
299
+ if err != nil {
300
+ return nil, err
301
+ }
302
+
303
+ mode := parser.ParseComments
304
+ f, err := parser.ParseFile(fset, filename, src, mode)
305
+ if err != nil {
306
+ fmt.Fprintf(os.Stderr, "Warning: could not parse %s: %v\n", filename, err)
307
+ return nil, err
308
+ }
309
+
310
+ entry := &FileEntry{
311
+ Classes: []ClassInfo{},
312
+ Functions: []FunctionInfo{},
313
+ }
314
+
315
+ // Collect receiver methods so we can attach them to structs in deep mode.
316
+ type methodEntry struct {
317
+ receiver string
318
+ method MethodInfo
319
+ }
320
+ var receiverMethods []methodEntry
321
+
322
+ // Map struct/interface name → index in entry.Classes for attaching methods.
323
+ classIndex := map[string]int{}
324
+
325
+ for _, decl := range f.Decls {
326
+ switch d := decl.(type) {
327
+ case *ast.GenDecl:
328
+ for _, spec := range d.Specs {
329
+ ts, ok := spec.(*ast.TypeSpec)
330
+ if !ok {
331
+ continue
332
+ }
333
+ if !isExported(ts.Name.Name) {
334
+ continue
335
+ }
336
+
337
+ switch st := ts.Type.(type) {
338
+ case *ast.StructType:
339
+ ci := ClassInfo{
340
+ Name: ts.Name.Name,
341
+ Kind: "struct",
342
+ Bases: []string{},
343
+ Line: fset.Position(ts.Pos()).Line,
344
+ }
345
+ if deep {
346
+ ci.Docstring = docText(d.Doc)
347
+ if ci.Docstring == "" {
348
+ ci.Docstring = docText(ts.Doc)
349
+ }
350
+ ci.Decorators = []string{}
351
+ ci.Attributes = []AttributeInfo{}
352
+ ci.Methods = []MethodInfo{}
353
+ }
354
+ // Extract embedded types as bases + attributes.
355
+ if st.Fields != nil {
356
+ for _, field := range st.Fields.List {
357
+ if len(field.Names) == 0 {
358
+ // Embedded type → base
359
+ ci.Bases = append(ci.Bases, exprToString(field.Type))
360
+ } else if deep {
361
+ // Named field → attribute
362
+ tag := ""
363
+ if field.Tag != nil {
364
+ tag = field.Tag.Value
365
+ }
366
+ for _, name := range field.Names {
367
+ if isExported(name.Name) {
368
+ ci.Attributes = append(ci.Attributes, AttributeInfo{
369
+ Name: name.Name,
370
+ Type: exprToString(field.Type),
371
+ Tag: tag,
372
+ })
373
+ }
374
+ }
375
+ }
376
+ }
377
+ }
378
+ classIndex[ci.Name] = len(entry.Classes)
379
+ entry.Classes = append(entry.Classes, ci)
380
+
381
+ case *ast.InterfaceType:
382
+ ci := ClassInfo{
383
+ Name: ts.Name.Name,
384
+ Kind: "interface",
385
+ Bases: []string{},
386
+ Line: fset.Position(ts.Pos()).Line,
387
+ }
388
+ if deep {
389
+ ci.Docstring = docText(d.Doc)
390
+ if ci.Docstring == "" {
391
+ ci.Docstring = docText(ts.Doc)
392
+ }
393
+ ci.Decorators = []string{}
394
+ ci.Attributes = []AttributeInfo{}
395
+ ci.Methods = []MethodInfo{}
396
+ }
397
+ if st.Methods != nil {
398
+ for _, m := range st.Methods.List {
399
+ if len(m.Names) == 0 {
400
+ // Embedded interface
401
+ ci.Bases = append(ci.Bases, exprToString(m.Type))
402
+ } else if deep {
403
+ ft, ok := m.Type.(*ast.FuncType)
404
+ if !ok {
405
+ continue
406
+ }
407
+ ci.Methods = append(ci.Methods, MethodInfo{
408
+ Name: m.Names[0].Name,
409
+ Line: fset.Position(m.Pos()).Line,
410
+ IsAsync: false,
411
+ Docstring: docText(m.Doc),
412
+ Decorators: []string{},
413
+ Params: extractParams(ft.Params),
414
+ ReturnType: extractReturnType(ft.Results),
415
+ })
416
+ }
417
+ }
418
+ }
419
+ classIndex[ci.Name] = len(entry.Classes)
420
+ entry.Classes = append(entry.Classes, ci)
421
+
422
+ default:
423
+ // Type alias (type X = Y) or named type (type X Y).
424
+ kind := "named_type"
425
+ if ts.Assign.IsValid() {
426
+ kind = "type_alias"
427
+ }
428
+ ci := ClassInfo{
429
+ Name: ts.Name.Name,
430
+ Kind: kind,
431
+ Bases: []string{},
432
+ Line: fset.Position(ts.Pos()).Line,
433
+ }
434
+ if deep {
435
+ ci.Docstring = docText(d.Doc)
436
+ if ci.Docstring == "" {
437
+ ci.Docstring = docText(ts.Doc)
438
+ }
439
+ ci.Decorators = []string{}
440
+ ci.Attributes = []AttributeInfo{}
441
+ ci.Methods = []MethodInfo{}
442
+ }
443
+ classIndex[ci.Name] = len(entry.Classes)
444
+ entry.Classes = append(entry.Classes, ci)
445
+ }
446
+ }
447
+
448
+ case *ast.FuncDecl:
449
+ if !isExported(d.Name.Name) {
450
+ continue
451
+ }
452
+ recv := receiverTypeName(d)
453
+ if recv != "" {
454
+ // Receiver method — collect for later attachment.
455
+ mi := MethodInfo{
456
+ Name: d.Name.Name,
457
+ Line: fset.Position(d.Pos()).Line,
458
+ IsAsync: false,
459
+ }
460
+ if deep {
461
+ mi.Docstring = docText(d.Doc)
462
+ mi.Decorators = []string{}
463
+ mi.Params = extractParams(d.Type.Params)
464
+ mi.ReturnType = extractReturnType(d.Type.Results)
465
+ }
466
+ receiverMethods = append(receiverMethods, methodEntry{
467
+ receiver: recv,
468
+ method: mi,
469
+ })
470
+ } else {
471
+ // Top-level function.
472
+ fi := FunctionInfo{
473
+ Name: d.Name.Name,
474
+ Line: fset.Position(d.Pos()).Line,
475
+ IsAsync: false,
476
+ }
477
+ if deep {
478
+ fi.Docstring = docText(d.Doc)
479
+ fi.Decorators = []string{}
480
+ fi.Params = extractParams(d.Type.Params)
481
+ fi.ReturnType = extractReturnType(d.Type.Results)
482
+ }
483
+ entry.Functions = append(entry.Functions, fi)
484
+ }
485
+ }
486
+ }
487
+
488
+ // Attach receiver methods to their struct in deep mode.
489
+ if deep {
490
+ for _, rm := range receiverMethods {
491
+ if idx, ok := classIndex[rm.receiver]; ok {
492
+ entry.Classes[idx].Methods = append(entry.Classes[idx].Methods, rm.method)
493
+ } else {
494
+ // Receiver type not found as a class (e.g. unexported struct) —
495
+ // expose as a standalone function with receiver field.
496
+ entry.Functions = append(entry.Functions, FunctionInfo{
497
+ Name: rm.method.Name,
498
+ Line: rm.method.Line,
499
+ IsAsync: false,
500
+ Receiver: rm.receiver,
501
+ Docstring: rm.method.Docstring,
502
+ Decorators: rm.method.Decorators,
503
+ Params: rm.method.Params,
504
+ ReturnType: rm.method.ReturnType,
505
+ })
506
+ }
507
+ }
508
+ } else {
509
+ // Shallow mode: receiver methods as standalone functions with receiver field.
510
+ for _, rm := range receiverMethods {
511
+ entry.Functions = append(entry.Functions, FunctionInfo{
512
+ Name: rm.method.Name,
513
+ Line: rm.method.Line,
514
+ IsAsync: false,
515
+ Receiver: rm.receiver,
516
+ })
517
+ }
518
+ }
519
+
520
+ // ── Imports (deep only) ──────────────────────────────────────────────────
521
+ if deep && f.Imports != nil {
522
+ entry.Imports = []ImportInfo{}
523
+ for _, imp := range f.Imports {
524
+ path := strings.Trim(imp.Path.Value, `"`)
525
+ // Use the last segment as the name (e.g. "fmt" from "fmt",
526
+ // "json" from "encoding/json").
527
+ parts := strings.Split(path, "/")
528
+ name := parts[len(parts)-1]
529
+ var alias *string
530
+ impType := "import"
531
+ if imp.Name != nil {
532
+ switch imp.Name.Name {
533
+ case ".":
534
+ impType = "dot"
535
+ case "_":
536
+ impType = "blank"
537
+ default:
538
+ a := imp.Name.Name
539
+ alias = &a
540
+ }
541
+ }
542
+ entry.Imports = append(entry.Imports, ImportInfo{
543
+ Module: path,
544
+ Name: name,
545
+ Alias: alias,
546
+ Type: impType,
547
+ })
548
+ }
549
+ }
550
+
551
+ return entry, nil
552
+ }
553
+
554
+ // ── Main ──────────────────────────────────────────────────────────────────────
555
+
556
+ func main() {
557
+ srcDir := flag.String("src-dir", ".", "Root directory to scan")
558
+ onlyFiles := flag.String("only-files", "", "Comma-separated list of files to extract")
559
+ deep := flag.Bool("deep", false, "Include enriched data (docs, attributes, methods, imports)")
560
+ flag.Parse()
561
+
562
+ fset := token.NewFileSet()
563
+ inventory := map[string]*FileEntry{}
564
+
565
+ var files []string
566
+ if *onlyFiles != "" {
567
+ for _, f := range strings.Split(*onlyFiles, ",") {
568
+ f = strings.TrimSpace(f)
569
+ if f == "" {
570
+ continue
571
+ }
572
+ abs := f
573
+ if !filepath.IsAbs(f) {
574
+ abs = filepath.Join(*srcDir, f)
575
+ }
576
+ rel, err := filepath.Rel(*srcDir, abs)
577
+ if err != nil || isOutsideRoot(rel) || hasExcludedDir(rel) {
578
+ continue
579
+ }
580
+ if _, err := os.Stat(abs); err == nil && filepath.Ext(abs) == ".go" && !strings.HasSuffix(filepath.Base(abs), "_test.go") {
581
+ files = append(files, abs)
582
+ }
583
+ }
584
+ } else {
585
+ var err error
586
+ files, err = collectGoFiles(*srcDir)
587
+ if err != nil {
588
+ fmt.Fprintf(os.Stderr, "Error walking %s: %v\n", *srcDir, err)
589
+ os.Exit(1)
590
+ }
591
+ }
592
+
593
+ for _, file := range files {
594
+ entry, err := extractFile(file, fset, *deep)
595
+ if err != nil {
596
+ continue // warning already printed inside extractFile
597
+ }
598
+ // Use relative path from srcDir as key.
599
+ rel, err := filepath.Rel(*srcDir, file)
600
+ if err != nil {
601
+ rel = file
602
+ }
603
+ inventory[rel] = entry
604
+ }
605
+
606
+ // ── Cross-file receiver attachment (deep mode, per-package) ────────────────
607
+ // Go methods are often defined in a different file from the type they extend.
608
+ // After all files are processed, attach any lingering receiver-method functions
609
+ // to their type within the same directory (= package boundary).
610
+ if *deep {
611
+ // Build: dir → typeName → {relPath, classIdx}
612
+ type classRef struct {
613
+ relPath string
614
+ classIdx int
615
+ }
616
+ dirClassIndex := map[string]map[string]classRef{}
617
+ for relPath, entry := range inventory {
618
+ dir := filepath.Dir(relPath)
619
+ if _, ok := dirClassIndex[dir]; !ok {
620
+ dirClassIndex[dir] = map[string]classRef{}
621
+ }
622
+ for idx, cls := range entry.Classes {
623
+ dirClassIndex[dir][cls.Name] = classRef{relPath: relPath, classIdx: idx}
624
+ }
625
+ }
626
+
627
+ // For each file, find functions with a Receiver resolvable within
628
+ // the same directory and move them into the target class's methods.
629
+ for relPath, entry := range inventory {
630
+ dir := filepath.Dir(relPath)
631
+ remaining := make([]FunctionInfo, 0, len(entry.Functions))
632
+ for _, fn := range entry.Functions {
633
+ if fn.Receiver == "" {
634
+ remaining = append(remaining, fn)
635
+ continue
636
+ }
637
+ ref, ok := dirClassIndex[dir][fn.Receiver]
638
+ if !ok {
639
+ // Unexported or unknown type — leave as a function.
640
+ remaining = append(remaining, fn)
641
+ continue
642
+ }
643
+ // Attach to the target class as a method.
644
+ target := inventory[ref.relPath]
645
+ target.Classes[ref.classIdx].Methods = append(
646
+ target.Classes[ref.classIdx].Methods,
647
+ MethodInfo{
648
+ Name: fn.Name,
649
+ Line: fn.Line,
650
+ IsAsync: false,
651
+ Docstring: fn.Docstring,
652
+ Decorators: fn.Decorators,
653
+ Params: fn.Params,
654
+ ReturnType: fn.ReturnType,
655
+ },
656
+ )
657
+ }
658
+ entry.Functions = remaining
659
+ }
660
+ }
661
+
662
+ enc := json.NewEncoder(os.Stdout)
663
+ enc.SetIndent("", " ")
664
+ if err := enc.Encode(inventory); err != nil {
665
+ fmt.Fprintf(os.Stderr, "Error encoding JSON: %v\n", err)
666
+ os.Exit(1)
667
+ }
668
+ }