aja-codeintel 0.1.0__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 (68) hide show
  1. aja_codeintel-0.1.0.dist-info/METADATA +436 -0
  2. aja_codeintel-0.1.0.dist-info/RECORD +68 -0
  3. aja_codeintel-0.1.0.dist-info/WHEEL +5 -0
  4. aja_codeintel-0.1.0.dist-info/entry_points.txt +3 -0
  5. aja_codeintel-0.1.0.dist-info/licenses/LICENSE +21 -0
  6. aja_codeintel-0.1.0.dist-info/top_level.txt +1 -0
  7. codeintel_cli/__init__.py +1 -0
  8. codeintel_cli/__main__.py +4 -0
  9. codeintel_cli/cli.py +41 -0
  10. codeintel_cli/commands/__init__.py +1 -0
  11. codeintel_cli/commands/graph/__init__.py +18 -0
  12. codeintel_cli/commands/graph/deps_cmd.py +35 -0
  13. codeintel_cli/commands/graph/related_cmd.py +121 -0
  14. codeintel_cli/commands/graph/relsymbols_cmd.py +347 -0
  15. codeintel_cli/commands/graph/reverse_related_cmd.py +54 -0
  16. codeintel_cli/commands/nav/__init__.py +12 -0
  17. codeintel_cli/commands/nav/copy_cmd.py +101 -0
  18. codeintel_cli/commands/nav/open_cmd.py +18 -0
  19. codeintel_cli/commands/nav/where_cmd.py +21 -0
  20. codeintel_cli/commands/project/__init__.py +26 -0
  21. codeintel_cli/commands/project/context_cmd.py +326 -0
  22. codeintel_cli/commands/project/folder_cmd.py +51 -0
  23. codeintel_cli/commands/project/imports_cmd.py +90 -0
  24. codeintel_cli/commands/project/models_cmd.py +98 -0
  25. codeintel_cli/commands/project/modeltree_cmd.py +476 -0
  26. codeintel_cli/commands/project/new.py +0 -0
  27. codeintel_cli/commands/project/resolve_cmd.py +29 -0
  28. codeintel_cli/commands/project/scan_cmd.py +51 -0
  29. codeintel_cli/commands/project/servicemap_cmd.py +180 -0
  30. codeintel_cli/commands/project/tree_cmd.py +203 -0
  31. codeintel_cli/commands/project/version_cmd.py +14 -0
  32. codeintel_cli/context/java_context.py +180 -0
  33. codeintel_cli/context/java_rel.py +299 -0
  34. codeintel_cli/context/java_service.py +291 -0
  35. codeintel_cli/context/python_context.py +91 -0
  36. codeintel_cli/context/python_rel.py +251 -0
  37. codeintel_cli/context/python_service.py +205 -0
  38. codeintel_cli/core/fuzzy.py +72 -0
  39. codeintel_cli/core/opener.py +37 -0
  40. codeintel_cli/core/project.py +34 -0
  41. codeintel_cli/core/resolve_folder.py +68 -0
  42. codeintel_cli/core/resolve_model_target.py +92 -0
  43. codeintel_cli/core/resolve_target.py +53 -0
  44. codeintel_cli/core/timing.py +13 -0
  45. codeintel_cli/core/where.py +77 -0
  46. codeintel_cli/db/__init__.py +7 -0
  47. codeintel_cli/db/cache.py +224 -0
  48. codeintel_cli/db/operations.py +333 -0
  49. codeintel_cli/db/schema.py +102 -0
  50. codeintel_cli/errors.py +78 -0
  51. codeintel_cli/graph/__init__.py +1 -0
  52. codeintel_cli/graph/builder.py +149 -0
  53. codeintel_cli/graph/query.py +30 -0
  54. codeintel_cli/graph/traverse.py +49 -0
  55. codeintel_cli/lang/__init__.py +0 -0
  56. codeintel_cli/lang/java/__init__.py +0 -0
  57. codeintel_cli/lang/java/engine.py +18 -0
  58. codeintel_cli/lang/java/models.py +105 -0
  59. codeintel_cli/lang/java/resolve.py +49 -0
  60. codeintel_cli/lang/python/__init__.py +0 -0
  61. codeintel_cli/lang/python/engine.py +8 -0
  62. codeintel_cli/lang/python/models.py +86 -0
  63. codeintel_cli/lang/router.py +24 -0
  64. codeintel_cli/parser/imports.py +26 -0
  65. codeintel_cli/parser/resolve.py +49 -0
  66. codeintel_cli/parser/symbols.py +92 -0
  67. codeintel_cli/scanner/__init__.py +0 -0
  68. codeintel_cli/scanner/scanner.py +41 -0
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import typer
4
+
5
+ from ...errors import InvalidPathError
6
+ from ...core.project import find_project_root
7
+ from ...core.fuzzy import rank_paths, fuzzy_is_confident
8
+ from ...context.java_service import analyze_java_service
9
+ from ...context.python_service import analyze_python_service
10
+ from ...db.cache import CacheManager
11
+
12
+ PYTHON_SERVICE_DIRS = {"service", "services", "api", "views", "routes"}
13
+
14
+ ANALYZERS = {
15
+ ".java": analyze_java_service,
16
+ ".py": analyze_python_service,
17
+ }
18
+
19
+
20
+ def _is_excluded_java_service_path(path: Path) -> bool:
21
+ parts = {p.lower() for p in path.parts}
22
+ stem = path.stem.lower()
23
+ name = path.name.lower()
24
+
25
+ if name == "package-info.java":
26
+ return True
27
+ if "dto" in parts or "mapper" in parts or "vm" in parts:
28
+ return True
29
+ if stem.endswith("dto") or "dto" in stem:
30
+ return True
31
+ if stem.endswith("mapper") or "mapper" in stem:
32
+ return True
33
+ return False
34
+
35
+
36
+ def _java_has_service_annotation(path: Path) -> bool:
37
+ try:
38
+ txt = path.read_text(encoding="utf-8", errors="ignore")
39
+ except OSError:
40
+ return False
41
+ return "@Service" in txt
42
+
43
+
44
+ def _is_service_file(path: Path) -> bool:
45
+ suffix = path.suffix.lower()
46
+ stem_lower = path.stem.lower()
47
+
48
+ if suffix == ".java":
49
+ if _is_excluded_java_service_path(path):
50
+ return False
51
+ if stem_lower.endswith("service"):
52
+ return True
53
+ return _java_has_service_annotation(path)
54
+
55
+ if suffix == ".py":
56
+ parts = {p.lower() for p in path.parts}
57
+ return bool(parts & PYTHON_SERVICE_DIRS)
58
+
59
+ return False
60
+
61
+
62
+ def _short_rel(path: Path, root: Path, keep: int = 5) -> str:
63
+ try:
64
+ parts = list(path.relative_to(root).parts)
65
+ except ValueError:
66
+ return str(path)
67
+ tail = parts[-keep:] if len(parts) > keep else parts
68
+ return "/".join(tail)
69
+
70
+
71
+ def _resolve_service_file(query: str, root: str = ".", top: int = 6) -> tuple[Path, Path]:
72
+ root_path = Path(root).resolve()
73
+ project_root = find_project_root(root_path)
74
+
75
+ direct = Path(query)
76
+ if direct.is_file():
77
+ return direct.resolve(), project_root
78
+
79
+ with CacheManager(project_root) as cache:
80
+ if cache.needs_rescan():
81
+ cache.scan_project(verbose=False)
82
+ all_files = cache.get_cached_files()
83
+
84
+ service_files = [p for p in all_files if _is_service_file(p)]
85
+ if not service_files:
86
+ raise InvalidPathError(message="No service files found", path=project_root)
87
+
88
+ query_stem = Path(query).stem.lower()
89
+ exact = [f for f in service_files if f.stem.lower() == query_stem]
90
+ if len(exact) == 1:
91
+ return exact[0].resolve(), project_root
92
+
93
+ ranked = rank_paths(query, service_files, project_root, top=top)
94
+ if not ranked:
95
+ raise InvalidPathError(message="Service not found", path=Path(query))
96
+
97
+ if fuzzy_is_confident(ranked):
98
+ top_score = ranked[0][1]
99
+ if sum(1 for _, s in ranked if abs(s - top_score) < 0.01) == 1:
100
+ return ranked[0][0].resolve(), project_root
101
+
102
+ typer.echo("Multiple matches found — select a service:")
103
+ for i, (p, score) in enumerate(ranked, 1):
104
+ typer.echo(f" {i}. {_short_rel(p, project_root)} (score={score:.2f})")
105
+
106
+ choice = typer.prompt("Enter number (0 to cancel)", type=int, default=0)
107
+ if not (1 <= choice <= len(ranked)):
108
+ raise typer.Exit()
109
+
110
+ return ranked[choice - 1][0].resolve(), project_root
111
+
112
+
113
+ def _render_service_map(data: dict) -> list[str]:
114
+ lines: list[str] = [data["service_name"], "━" * 80, ""]
115
+
116
+ if endpoints := data.get("endpoints", []):
117
+ lines.append("ENDPOINTS")
118
+ for ep in endpoints:
119
+ lines.append(f" {ep.get('method', '').ljust(7)} {ep.get('path', '')}")
120
+ if handler := ep.get("handler"):
121
+ lines.append(f" → {handler}")
122
+ lines.append("")
123
+
124
+ if methods := data.get("methods", []):
125
+ lines.append("SERVICE METHODS")
126
+ for m in methods:
127
+ sig = f"{m.get('name', '')}({m.get('params', '')})"
128
+ if ret := m.get("return"):
129
+ sig += f" → {ret}"
130
+ lines.append(f" • {sig}")
131
+ lines.append("")
132
+
133
+ if models := data.get("models", {}):
134
+ lines.append("MODELS")
135
+ for model_name, model_data in models.items():
136
+ lines.append(f" {model_name}")
137
+ for fname, ftype, is_pk in model_data.get("fields", []):
138
+ marker = "🔑" if is_pk else " "
139
+ lines.append(f" {marker} {fname}: {ftype}")
140
+ for rel in model_data.get("relationships", []):
141
+ lines.append(f" → {rel.get('kind', '')} {rel.get('target', '')} (via {rel.get('field', '')})")
142
+ lines.append("")
143
+
144
+ if repos := data.get("repositories", []):
145
+ lines.append("REPOSITORIES")
146
+ for repo in repos:
147
+ lines.append(f" {repo.get('name', '')}")
148
+ for method in repo.get("methods", [])[:3]:
149
+ lines.append(f" • {method}")
150
+ lines.append("")
151
+
152
+ if summary := data.get("summary", {}):
153
+ lines.append("SUMMARY")
154
+ lines.extend(f" {k}: {v}" for k, v in summary.items())
155
+
156
+ return lines
157
+
158
+
159
+ def register_servicemap(app: typer.Typer) -> None:
160
+ @app.command(help="Service endpoint-model relationship map")
161
+ def servicemap(
162
+ service: str = typer.Argument(..., help="Service name or path"),
163
+ root: str = typer.Option(".", help="Project root directory"),
164
+ save: bool = typer.Option(False, "--save", "-s", help="Save output to file"),
165
+ ) -> None:
166
+ target, project_root = _resolve_service_file(service, root=root)
167
+
168
+ analyzer = ANALYZERS.get(target.suffix.lower())
169
+ if analyzer is None:
170
+ raise InvalidPathError(message="Unsupported file type", path=target)
171
+
172
+ result = analyzer(target, project_root)
173
+ lines = _render_service_map(result)
174
+
175
+ if save:
176
+ out_path = project_root / f"codeintel-servicemap-{result['service_name']}.txt"
177
+ out_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
178
+ typer.echo(f"Saved: {out_path.as_posix()}")
179
+ else:
180
+ typer.echo("\n" + "\n".join(lines))
@@ -0,0 +1,203 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from pathlib import Path
5
+ import typer
6
+
7
+ # Optional: pip install pathspec
8
+ try:
9
+ import pathspec # type: ignore
10
+ except Exception: # pragma: no cover
11
+ pathspec = None
12
+
13
+
14
+ DEFAULT_IGNORE_NAMES = {
15
+ ".git", ".idea", ".vscode",
16
+ "__pycache__", ".mypy_cache", ".pytest_cache", ".ruff_cache",
17
+ "venv", ".venv",
18
+ "node_modules", "dist", "build",
19
+ ".DS_Store",
20
+ }
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class TreeOptions:
25
+ depth: int # -1 = unlimited
26
+ show_hidden: bool # show dotfiles/dirs
27
+ keep_dotenv: bool # always show ".env"
28
+ use_gitignore: bool # apply .gitignore patterns
29
+ exts: tuple[str, ...] # file extensions to include (".py", ".java")
30
+
31
+
32
+ def _load_gitignore_spec(root: Path):
33
+ if pathspec is None:
34
+ return None
35
+ gi = root / ".gitignore"
36
+ if not gi.exists():
37
+ return None
38
+
39
+ lines: list[str] = []
40
+ for line in gi.read_text(encoding="utf-8", errors="ignore").splitlines():
41
+ s = line.strip()
42
+ if not s or s.startswith("#"):
43
+ continue
44
+ lines.append(s)
45
+
46
+ if not lines:
47
+ return None
48
+ return pathspec.PathSpec.from_lines("gitwildmatch", lines)
49
+
50
+
51
+ def _is_hidden(name: str) -> bool:
52
+ return name.startswith(".")
53
+
54
+
55
+ def _gitignore_matches(spec, rel_posix: str, is_dir: bool) -> bool:
56
+ if spec is None:
57
+ return False
58
+ if spec.match_file(rel_posix):
59
+ return True
60
+ if is_dir and spec.match_file(rel_posix.rstrip("/") + "/"):
61
+ return True
62
+ return False
63
+
64
+
65
+ def _is_ignored(root: Path, p: Path, spec, opts: TreeOptions) -> bool:
66
+ name = p.name
67
+
68
+ if opts.keep_dotenv and name == ".env":
69
+ return False
70
+
71
+ if name in DEFAULT_IGNORE_NAMES:
72
+ return True
73
+
74
+ if _is_hidden(name) and not opts.show_hidden:
75
+ return True
76
+
77
+ if opts.use_gitignore and spec is not None:
78
+ rel = p.relative_to(root).as_posix()
79
+ if _gitignore_matches(spec, rel, p.is_dir()):
80
+ return True
81
+
82
+ return False
83
+
84
+
85
+ def _is_source_file(p: Path, opts: TreeOptions) -> bool:
86
+ if not p.is_file():
87
+ return False
88
+ # include only selected extensions
89
+ return p.suffix.lower() in opts.exts
90
+
91
+
92
+ def _list_children(root: Path, cur: Path, spec, opts: TreeOptions) -> list[Path]:
93
+ try:
94
+ entries = list(cur.iterdir())
95
+ except (PermissionError, FileNotFoundError):
96
+ return []
97
+
98
+ kept: list[Path] = []
99
+ for p in entries:
100
+ if _is_ignored(root, p, spec, opts):
101
+ continue
102
+ if p.is_dir():
103
+ kept.append(p)
104
+ else:
105
+ if _is_source_file(p, opts) or (opts.keep_dotenv and p.name == ".env"):
106
+ kept.append(p)
107
+
108
+ # dirs first, then files; alpha
109
+ kept.sort(key=lambda x: (x.is_file(), x.name.lower()))
110
+ return kept
111
+
112
+
113
+ def _dir_has_visible_sources(root: Path, cur: Path, spec, opts: TreeOptions, remaining_depth: int) -> bool:
114
+ """
115
+ True if this directory contains any visible source file (directly or deeper).
116
+ remaining_depth: how many more levels we can descend; -1 for unlimited.
117
+ """
118
+ children = _list_children(root, cur, spec, opts)
119
+ if not children:
120
+ return False
121
+
122
+ for c in children:
123
+ if c.is_file():
124
+ return True
125
+
126
+ if remaining_depth == 0:
127
+ # we can't descend further; if it has dirs but no files shown, treat as empty
128
+ return False
129
+
130
+ next_depth = -1 if remaining_depth == -1 else remaining_depth - 1
131
+ for c in children:
132
+ if c.is_dir() and _dir_has_visible_sources(root, c, spec, opts, next_depth):
133
+ return True
134
+
135
+ return False
136
+
137
+
138
+ def _print_tree(root: Path, opts: TreeOptions) -> None:
139
+ spec = _load_gitignore_spec(root) if opts.use_gitignore else None
140
+
141
+ def walk(cur: Path, prefix: str, level: int) -> None:
142
+ if opts.depth != -1 and level > opts.depth:
143
+ return
144
+
145
+ items = _list_children(root, cur, spec, opts)
146
+
147
+ # prune empty dirs (dirs that contain no visible sources)
148
+ pruned: list[Path] = []
149
+ for p in items:
150
+ if p.is_dir():
151
+ remaining = -1 if opts.depth == -1 else max(opts.depth - level, 0)
152
+ if _dir_has_visible_sources(root, p, spec, opts, remaining):
153
+ pruned.append(p)
154
+ else:
155
+ pruned.append(p)
156
+
157
+ items = pruned
158
+
159
+ for i, p in enumerate(items):
160
+ last = i == len(items) - 1
161
+ branch = "└── " if last else "├── "
162
+ typer.echo(prefix + branch + p.name)
163
+
164
+ if p.is_dir():
165
+ extension = " " if last else "│ "
166
+ walk(p, prefix + extension, level + 1)
167
+
168
+ typer.echo(root.name)
169
+ walk(root, prefix="", level=1)
170
+
171
+
172
+ def register_tree(app: typer.Typer) -> None:
173
+ @app.command(help="Tree view of source files only (python/java).")
174
+ def tree(
175
+ root: str = typer.Argument(".", help="Folder to print tree for"),
176
+ depth: int = typer.Option(12, "--depth", help="Max depth (-1 = unlimited)"),
177
+ hidden: bool = typer.Option(False, "--hidden", help="Show hidden dotfiles/folders"),
178
+ no_gitignore: bool = typer.Option(False, "--no-gitignore", help="Do not apply .gitignore rules"),
179
+ keep_dotenv: bool = typer.Option(True, "--keep-dotenv/--no-keep-dotenv", help="Always show .env"),
180
+ lang: str = typer.Option("all", "--lang", help="all | py | java"),
181
+ ):
182
+ root_path = Path(root).resolve()
183
+ if not root_path.exists() or not root_path.is_dir():
184
+ raise typer.BadParameter(f"Invalid folder: {root_path}")
185
+
186
+ lang_norm = lang.strip().lower()
187
+ if lang_norm == "py":
188
+ exts = (".py",)
189
+ elif lang_norm == "java":
190
+ exts = (".java",)
191
+ elif lang_norm == "all":
192
+ exts = (".py", ".java")
193
+ else:
194
+ raise typer.BadParameter("Invalid --lang. Use: all | py | java")
195
+
196
+ opts = TreeOptions(
197
+ depth=depth,
198
+ show_hidden=hidden,
199
+ keep_dotenv=keep_dotenv,
200
+ use_gitignore=not no_gitignore,
201
+ exts=exts,
202
+ )
203
+ _print_tree(root_path, opts)
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+ import importlib.metadata as md
5
+ from ...core.resolve_target import resolve_target_file
6
+
7
+
8
+ def register_version(app: typer.Typer) -> None:
9
+ @app.command(help="Show CodeIntel version.")
10
+ def version():
11
+ try:
12
+ typer.echo(md.version("codeintel"))
13
+ except md.PackageNotFoundError:
14
+ typer.echo("codeintel (not installed as a package)")
@@ -0,0 +1,180 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections import deque
5
+ from pathlib import Path
6
+ import typer
7
+
8
+ from ..graph.builder import build_graph_with_counts, get_hub_files_by_ratio
9
+ from ..lang.router import extract_imports
10
+
11
+ TYPE = re.compile(r"\b(class|interface|enum|record)\s+([A-Za-z_][A-Za-z0-9_]*)\b")
12
+
13
+
14
+ def _java_types_only(path: Path) -> list[str]:
15
+ out: list[str] = []
16
+ try:
17
+ for line in path.read_text(errors="ignore").splitlines():
18
+ m = TYPE.search(line)
19
+ if m:
20
+ kind = m.group(1)
21
+ name = m.group(2)
22
+ label = f"{name} ({kind})"
23
+ if label not in out:
24
+ out.append(label)
25
+ except Exception:
26
+ return out
27
+ return out
28
+
29
+
30
+ def _collect_java_imports(files: list[Path]) -> list[str]:
31
+ acc: set[str] = set()
32
+ for f in files:
33
+ if f.suffix != ".java":
34
+ continue
35
+ for imp in extract_imports(f, "java"):
36
+ acc.add(str(imp))
37
+ return sorted(acc)
38
+
39
+
40
+ def _print_files_with_types(title: str, files: list[Path], root: Path) -> None:
41
+ typer.echo(title)
42
+ if not files:
43
+ typer.echo(" (none)")
44
+ typer.echo("")
45
+ return
46
+
47
+ any_printed = False
48
+ for f in files:
49
+ if f.suffix != ".java":
50
+ continue
51
+ types_ = _java_types_only(f)
52
+ if not types_:
53
+ continue
54
+ any_printed = True
55
+ typer.echo(f"• {f.relative_to(root)}")
56
+ for t in types_:
57
+ typer.echo(f" {t}")
58
+ typer.echo("")
59
+
60
+ if not any_printed:
61
+ typer.echo(" (none)")
62
+ typer.echo("")
63
+ return
64
+
65
+
66
+ def _print_imports(title: str, files: list[Path]) -> None:
67
+ typer.echo(title)
68
+ imps = _collect_java_imports(files)
69
+ if imps:
70
+ for x in imps:
71
+ typer.echo(f" {x}")
72
+ else:
73
+ typer.echo(" (none)")
74
+ typer.echo("")
75
+
76
+
77
+ def _build_reverse_graph(graph: dict[Path, set[Path]]) -> dict[Path, set[Path]]:
78
+ rev: dict[Path, set[Path]] = {}
79
+ for src, deps in graph.items():
80
+ for dep in deps:
81
+ rev.setdefault(dep, set()).add(src)
82
+ return rev
83
+
84
+
85
+ def _bfs_layers(
86
+ graph: dict[Path, set[Path]],
87
+ start: Path,
88
+ depth: int,
89
+ *,
90
+ include_reverse: bool,
91
+ hubs: set[Path],
92
+ ) -> dict[int, set[Path]]:
93
+ if depth <= 0:
94
+ return {}
95
+
96
+ rev = _build_reverse_graph(graph) if include_reverse else {}
97
+ layers: dict[int, set[Path]] = {}
98
+ visited: set[Path] = {start}
99
+
100
+ q: deque[tuple[Path, int]] = deque()
101
+ q.append((start, 0))
102
+
103
+ while q:
104
+ node, d = q.popleft()
105
+ if d >= depth:
106
+ continue
107
+
108
+ neighbors: set[Path] = set()
109
+ neighbors |= graph.get(node, set())
110
+ if include_reverse:
111
+ neighbors |= rev.get(node, set())
112
+
113
+ for nb in neighbors:
114
+ if nb in visited:
115
+ continue
116
+ if hubs and nb in hubs:
117
+ continue
118
+ visited.add(nb)
119
+ nd = d + 1
120
+ layers.setdefault(nd, set()).add(nb)
121
+ q.append((nb, nd))
122
+
123
+ return layers
124
+
125
+
126
+ def run_java_context(
127
+ *,
128
+ target: Path,
129
+ root: Path,
130
+ all_files: list[Path],
131
+ depth: int,
132
+ forward_only: bool,
133
+ include_hubs: bool,
134
+ ) -> None:
135
+ graph, dependents_count = build_graph_with_counts(all_files, root)
136
+
137
+ hubs: set[Path] = set()
138
+ if not include_hubs:
139
+ hubs = get_hub_files_by_ratio(dependents_count, len(all_files), 0.5)
140
+
141
+ same_folder = sorted(
142
+ f
143
+ for f in all_files
144
+ if f.suffix == ".java" and f.parent == target.parent and f != target
145
+ )
146
+
147
+ layers = _bfs_layers(
148
+ graph,
149
+ target,
150
+ depth=max(depth, 1),
151
+ include_reverse=not forward_only,
152
+ hubs=hubs,
153
+ )
154
+
155
+ level1_files = sorted(layers.get(1, set()))
156
+ level2_files = sorted(layers.get(2, set())) if depth >= 2 else []
157
+
158
+ typer.echo("")
159
+ typer.echo("CONTEXT")
160
+ typer.echo(f"Target: {target.relative_to(root)}")
161
+ typer.echo(f"Depth: {depth} Mode: {'forward-only' if forward_only else 'forward+reverse'}")
162
+ typer.echo("")
163
+
164
+ typer.echo("TARGET IMPORTS")
165
+ t_imps = extract_imports(target, "java")
166
+ if t_imps:
167
+ for x in t_imps:
168
+ typer.echo(f" {x}")
169
+ else:
170
+ typer.echo(" (none)")
171
+ typer.echo("")
172
+
173
+ _print_files_with_types("SAME FOLDER TYPES", same_folder, root)
174
+
175
+ _print_files_with_types("RELATED LEVEL 1 TYPES", level1_files, root)
176
+ _print_imports("RELATED LEVEL 1 IMPORTS", level1_files)
177
+
178
+ if depth >= 2:
179
+ _print_files_with_types("RELATED LEVEL 2 TYPES", level2_files, root)
180
+ _print_imports("RELATED LEVEL 2 IMPORTS", level2_files)