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,121 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from pathlib import Path
5
+ import typer
6
+
7
+ from ...graph.builder import build_graph_with_counts, get_hub_files_by_ratio
8
+ from ...core.resolve_target import resolve_target_file
9
+ from ...db.cache import CacheManager
10
+
11
+
12
+ def _layered_related(
13
+ graph: dict[Path, set[Path]],
14
+ start: Path,
15
+ depth: int,
16
+ include_reverse: bool,
17
+ hubs: set[Path],
18
+ ) -> list[list[Path]]:
19
+ adj: dict[Path, set[Path]] = {k: set(v) for k, v in graph.items()}
20
+
21
+ if include_reverse:
22
+ rev: dict[Path, set[Path]] = {}
23
+ for src, deps in adj.items():
24
+ for dst in deps:
25
+ rev.setdefault(dst, set()).add(src)
26
+ for node, incoming in rev.items():
27
+ adj.setdefault(node, set()).update(incoming)
28
+
29
+ layers: list[list[Path]] = []
30
+ visited: set[Path] = {start}
31
+ q: deque[tuple[Path, int]] = deque([(start, 0)])
32
+ by_depth: dict[int, list[Path]] = {}
33
+
34
+ while q:
35
+ node, d = q.popleft()
36
+ if d == depth:
37
+ continue
38
+
39
+ for nxt in adj.get(node, set()):
40
+ if nxt in visited:
41
+ continue
42
+ if nxt in hubs:
43
+ continue
44
+ visited.add(nxt)
45
+ nd = d + 1
46
+ by_depth.setdefault(nd, []).append(nxt)
47
+ q.append((nxt, nd))
48
+
49
+ for d in range(1, depth + 1):
50
+ items = sorted(by_depth.get(d, []))
51
+ layers.append(items)
52
+
53
+ return layers
54
+
55
+
56
+ def register_related(app: typer.Typer) -> None:
57
+ @app.command(help="Show related files using depth traversal (layered).")
58
+ def related(
59
+ file: str = typer.Argument(...),
60
+ root: str = typer.Argument("."),
61
+ depth: int = typer.Option(2, "--depth", "-d"),
62
+ forward_only: bool = typer.Option(False, "--forward-only"),
63
+ include_hubs: bool = typer.Option(False, "--include-hubs"),
64
+ use_sqlite_cache: bool = typer.Option(True, "--use-sqlite-cache/--no-sqlite-cache"),
65
+ ):
66
+ target, root_path = resolve_target_file(file, root=root)
67
+
68
+ if use_sqlite_cache:
69
+ with CacheManager(root_path) as cache:
70
+ if cache.needs_rescan():
71
+ cache.scan_project(verbose=False)
72
+ files = cache.get_cached_files()
73
+ else:
74
+ files = []
75
+ for p in root_path.rglob("*"):
76
+ if p.is_file() and p.suffix.lower() in {".py", ".java"}:
77
+ files.append(p.resolve())
78
+ files = sorted(set(files))
79
+
80
+ graph, dependents_count = build_graph_with_counts(
81
+ files,
82
+ root_path,
83
+ use_sqlite_cache=use_sqlite_cache,
84
+ )
85
+
86
+ hubs: set[Path] = set()
87
+ if not include_hubs:
88
+ hubs = get_hub_files_by_ratio(dependents_count, len(files), 0.5)
89
+
90
+ layers = _layered_related(
91
+ graph=graph,
92
+ start=target,
93
+ depth=depth,
94
+ include_reverse=not forward_only,
95
+ hubs=hubs,
96
+ )
97
+
98
+ total = sum(len(x) for x in layers)
99
+ if total == 0:
100
+ typer.echo("No related files.")
101
+ raise typer.Exit(0)
102
+
103
+ typer.echo(f"TARGET: {target.relative_to(root_path)}")
104
+ typer.echo(f"DEPTH: {depth} ({'forward-only' if forward_only else 'forward+reverse'})")
105
+ typer.echo(f"FILES: {total}")
106
+ typer.echo("")
107
+
108
+ for i, items in enumerate(layers, 1):
109
+ typer.echo(f"LEVEL {i} ({len(items)})")
110
+ if not items:
111
+ typer.echo(" (none)")
112
+ typer.echo("")
113
+ continue
114
+
115
+ for p in items:
116
+ try:
117
+ typer.echo(f" {p.relative_to(root_path)}")
118
+ except Exception:
119
+ typer.echo(f" {p}")
120
+
121
+ typer.echo("")
@@ -0,0 +1,347 @@
1
+ from __future__ import annotations
2
+
3
+ from collections import deque
4
+ from pathlib import Path
5
+ import re
6
+ import typer
7
+
8
+ from ...graph.builder import build_graph_with_counts, get_hub_files_by_ratio
9
+ from ...parser.symbols import extract_classes_from_file, extract_funcs_from_file
10
+ from ...core.resolve_target import resolve_target_file
11
+ from ...db.cache import CacheManager
12
+
13
+
14
+ _JAVA_TYPE_RE = re.compile(r"\b(class|interface|enum|record)\s+([A-Za-z_][A-Za-z0-9_]*)\b")
15
+ _JAVA_METHOD_RE = re.compile(
16
+ r"^\s*(?:@\w+(?:\([^)]*\))?\s*)*(?:public|protected|private)\s+(?:static\s+)?"
17
+ r"(?:final\s+)?([A-Za-z0-9_<>\[\].,?]+)\s+([A-Za-z_][A-Za-z0-9_]*)\s*\(([^)]*)\)\s*"
18
+ )
19
+
20
+
21
+ def _detect_lang(p: Path) -> str:
22
+ return "java" if p.suffix.lower() == ".java" else "python"
23
+
24
+
25
+ def _short_path(p: Path, root: Path) -> str:
26
+ try:
27
+ return str(p.relative_to(root))
28
+ except Exception:
29
+ return str(p)
30
+
31
+
32
+ def _java_types(path: Path) -> dict[str, list[str]]:
33
+ try:
34
+ text = path.read_text(encoding="utf-8", errors="ignore")
35
+ except Exception:
36
+ return {"class": [], "interface": [], "enum": [], "record": []}
37
+
38
+ out: dict[str, list[str]] = {"class": [], "interface": [], "enum": [], "record": []}
39
+ seen: set[str] = set()
40
+
41
+ for line in text.splitlines():
42
+ m = _JAVA_TYPE_RE.search(line)
43
+ if not m:
44
+ continue
45
+ kind = m.group(1)
46
+ name = m.group(2)
47
+ key = f"{kind}:{name}"
48
+ if key in seen:
49
+ continue
50
+ seen.add(key)
51
+ out.setdefault(kind, []).append(name)
52
+
53
+ return out
54
+
55
+
56
+ def _java_methods(path: Path) -> list[str]:
57
+ try:
58
+ lines = path.read_text(encoding="utf-8", errors="ignore").splitlines()
59
+ except Exception:
60
+ return []
61
+
62
+ out: list[str] = []
63
+ seen: set[str] = set()
64
+
65
+ for line in lines:
66
+ s = line.strip()
67
+ if not s:
68
+ continue
69
+ if s.startswith(("class ", "interface ", "record ", "enum ")):
70
+ continue
71
+ m = _JAVA_METHOD_RE.match(line)
72
+ if not m:
73
+ continue
74
+ ret = m.group(1).strip()
75
+ name = m.group(2).strip()
76
+ args = m.group(3).strip()
77
+ sig = f"{name}({args}) → {ret}"
78
+ if sig not in seen:
79
+ seen.add(sig)
80
+ out.append(sig)
81
+
82
+ return out
83
+
84
+
85
+ def _py_symbols(path: Path) -> tuple[list[str], list[str]]:
86
+ classes: list[str] = []
87
+ funcs: list[str] = []
88
+
89
+ for c in extract_classes_from_file(path):
90
+ if not c.name.startswith("_"):
91
+ classes.append(c.name)
92
+
93
+ for fn in extract_funcs_from_file(path):
94
+ if fn.name.startswith("_") or fn.name.startswith("register_"):
95
+ continue
96
+ funcs.append(fn.signature)
97
+
98
+ return classes, funcs
99
+
100
+
101
+ def _build_layers(
102
+ graph: dict[Path, set[Path]],
103
+ start: Path,
104
+ depth: int,
105
+ *,
106
+ include_reverse: bool,
107
+ hubs: set[Path],
108
+ ) -> dict[int, list[Path]]:
109
+ adj: dict[Path, set[Path]] = {k: set(v) for k, v in graph.items()}
110
+
111
+ if include_reverse:
112
+ rev: dict[Path, set[Path]] = {}
113
+ for src, deps in adj.items():
114
+ for dst in deps:
115
+ rev.setdefault(dst, set()).add(src)
116
+ for node, incoming in rev.items():
117
+ adj.setdefault(node, set()).update(incoming)
118
+
119
+ visited: set[Path] = {start}
120
+ q: deque[tuple[Path, int]] = deque([(start, 0)])
121
+ by_depth: dict[int, list[Path]] = {}
122
+
123
+ while q:
124
+ node, d = q.popleft()
125
+ if d >= depth:
126
+ continue
127
+
128
+ for nxt in adj.get(node, set()):
129
+ if nxt in visited:
130
+ continue
131
+ if hubs and nxt in hubs:
132
+ continue
133
+ visited.add(nxt)
134
+ nd = d + 1
135
+ by_depth.setdefault(nd, []).append(nxt)
136
+ q.append((nxt, nd))
137
+
138
+ for k in list(by_depth.keys()):
139
+ by_depth[k] = sorted(set(by_depth[k]))
140
+
141
+ return by_depth
142
+
143
+
144
+ def _print_java_target(path: Path, root: Path, *, all: bool) -> None:
145
+ typer.echo(f"TARGET: {_short_path(path, root)}")
146
+ typer.echo("")
147
+
148
+ types_map = _java_types(path)
149
+ methods = _java_methods(path)
150
+
151
+ if types_map["interface"]:
152
+ typer.echo("INTERFACES")
153
+ for n in types_map["interface"]:
154
+ typer.echo(f" {n}")
155
+ typer.echo("")
156
+
157
+ if all:
158
+ if types_map["class"]:
159
+ typer.echo("CLASSES")
160
+ for n in types_map["class"]:
161
+ typer.echo(f" {n}")
162
+ typer.echo("")
163
+
164
+ if types_map["record"]:
165
+ typer.echo("RECORDS")
166
+ for n in types_map["record"]:
167
+ typer.echo(f" {n}")
168
+ typer.echo("")
169
+
170
+ if types_map["enum"]:
171
+ typer.echo("ENUMS")
172
+ for n in types_map["enum"]:
173
+ typer.echo(f" {n}")
174
+ typer.echo("")
175
+
176
+ if methods:
177
+ typer.echo("METHODS")
178
+ for m in methods:
179
+ typer.echo(f" {m}")
180
+ typer.echo("")
181
+
182
+ if not types_map["interface"] and not (
183
+ all and (types_map["class"] or types_map["enum"] or types_map["record"] or methods)
184
+ ):
185
+ typer.echo("(no symbols)")
186
+ typer.echo("")
187
+
188
+
189
+ def _print_python_target(path: Path, root: Path) -> None:
190
+ typer.echo(f"TARGET: {_short_path(path, root)}")
191
+ typer.echo("")
192
+
193
+ classes, funcs = _py_symbols(path)
194
+
195
+ if classes:
196
+ typer.echo("CLASSES")
197
+ for c in classes:
198
+ typer.echo(f" {c}")
199
+ typer.echo("")
200
+
201
+ if funcs:
202
+ typer.echo("FUNCTIONS")
203
+ for f in funcs:
204
+ typer.echo(f" {f}")
205
+ typer.echo("")
206
+
207
+ if not classes and not funcs:
208
+ typer.echo("(no symbols)")
209
+ typer.echo("")
210
+
211
+
212
+ def _print_java_related(path: Path, root: Path, *, all: bool) -> None:
213
+ typer.echo(_short_path(path, root))
214
+
215
+ types_map = _java_types(path)
216
+
217
+ if types_map["interface"]:
218
+ typer.echo(" INTERFACES")
219
+ for n in types_map["interface"]:
220
+ typer.echo(f" {n}")
221
+
222
+ if all:
223
+ if types_map["class"]:
224
+ typer.echo(" CLASSES")
225
+ for n in types_map["class"]:
226
+ typer.echo(f" {n}")
227
+
228
+ if types_map["record"]:
229
+ typer.echo(" RECORDS")
230
+ for n in types_map["record"]:
231
+ typer.echo(f" {n}")
232
+
233
+ if types_map["enum"]:
234
+ typer.echo(" ENUMS")
235
+ for n in types_map["enum"]:
236
+ typer.echo(f" {n}")
237
+
238
+ methods = _java_methods(path)
239
+ if methods:
240
+ typer.echo(" METHODS")
241
+ for m in methods:
242
+ typer.echo(f" {m}")
243
+
244
+ typer.echo("")
245
+
246
+
247
+ def _print_python_related(path: Path, root: Path) -> None:
248
+ typer.echo(_short_path(path, root))
249
+
250
+ classes, funcs = _py_symbols(path)
251
+
252
+ if classes:
253
+ typer.echo(" CLASSES")
254
+ for c in classes:
255
+ typer.echo(f" {c}")
256
+
257
+ if funcs:
258
+ typer.echo(" FUNCTIONS")
259
+ for f in funcs:
260
+ typer.echo(f" {f}")
261
+
262
+ typer.echo("")
263
+
264
+
265
+ def register_relsymbols(app: typer.Typer) -> None:
266
+ @app.command(help="Show related classes/functions/interfaces")
267
+ def relsymbols(
268
+ file: str = typer.Argument(...),
269
+ root: str = typer.Argument("."),
270
+ depth: int = typer.Option(1, "--depth", "-d"),
271
+ forward_only: bool = typer.Option(False, "--forward-only"),
272
+ include_hubs: bool = typer.Option(False, "--include-hubs"),
273
+ all: bool = typer.Option(False, "--all", help="Show all symbols (Java: classes+methods, Python: unchanged)"),
274
+ use_sqlite_cache: bool = typer.Option(True, "--use-sqlite-cache/--no-sqlite-cache"),
275
+ ):
276
+ target, root_path = resolve_target_file(file, root=root)
277
+
278
+ if use_sqlite_cache:
279
+ with CacheManager(root_path) as cache:
280
+ if cache.needs_rescan():
281
+ cache.scan_project(verbose=False)
282
+ files = cache.get_cached_files()
283
+ else:
284
+ files = []
285
+ for p in root_path.rglob("*"):
286
+ if p.is_file() and p.suffix.lower() in {".py", ".java"}:
287
+ files.append(p.resolve())
288
+ files = sorted(set(files))
289
+
290
+ graph, dependents_count = build_graph_with_counts(
291
+ files,
292
+ root_path,
293
+ use_sqlite_cache=use_sqlite_cache,
294
+ )
295
+
296
+ hubs: set[Path] = set()
297
+ if not include_hubs:
298
+ hubs = get_hub_files_by_ratio(dependents_count, len(files), 0.5)
299
+
300
+ depth = max(1, depth)
301
+
302
+ layers = _build_layers(
303
+ graph=graph,
304
+ start=target,
305
+ depth=depth,
306
+ include_reverse=not forward_only,
307
+ hubs=hubs,
308
+ )
309
+
310
+ lang = _detect_lang(target)
311
+ if lang == "java":
312
+ _print_java_target(target, root_path, all=all)
313
+ else:
314
+ _print_python_target(target, root_path)
315
+
316
+ printed_any = False
317
+
318
+ for lvl in sorted(layers.keys()):
319
+ items = layers[lvl]
320
+ if not items:
321
+ continue
322
+
323
+ printed_any = True
324
+ typer.echo(f"LEVEL {lvl}")
325
+ typer.echo("")
326
+
327
+ for item_path in items:
328
+ if item_path.name == "__init__.py":
329
+ continue
330
+
331
+ item_lang = _detect_lang(item_path)
332
+
333
+ if item_lang == "java":
334
+ types_map = _java_types(item_path)
335
+ if not types_map["interface"] and not (
336
+ all and (types_map["class"] or types_map["enum"] or types_map["record"])
337
+ ):
338
+ continue
339
+ _print_java_related(item_path, root_path, all=all)
340
+ else:
341
+ classes, funcs = _py_symbols(item_path)
342
+ if not classes and not funcs:
343
+ continue
344
+ _print_python_related(item_path, root_path)
345
+
346
+ if not printed_any:
347
+ typer.echo("(no related symbols)")
@@ -0,0 +1,54 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from ...graph.builder import build_graph_with_counts, get_hub_files_by_ratio
6
+ from ...graph.traverse import build_reverse_graph, bfs_related
7
+ from ...core.resolve_target import resolve_target_file
8
+ from ...db.cache import CacheManager
9
+
10
+
11
+ def register_rrelated(app: typer.Typer) -> None:
12
+ @app.command(help="Show reverse-related files (who depends on this) using depth traversal.")
13
+ def rrelated(
14
+ file: str = typer.Argument(..., help="Target source file (path or filename)"),
15
+ depth: int = typer.Option(2, "--depth", "-d", help="Traversal depth"),
16
+ include_hubs: bool = typer.Option(False, "--include-hubs"),
17
+ use_sqlite_cache: bool = typer.Option(True, "--use-sqlite-cache/--no-sqlite-cache"),
18
+ ):
19
+ target_file, root_path = resolve_target_file(file)
20
+
21
+ if use_sqlite_cache:
22
+ with CacheManager(root_path) as cache:
23
+ if cache.needs_rescan():
24
+ cache.scan_project(verbose=False)
25
+ files = cache.get_cached_files()
26
+ else:
27
+ files = []
28
+ for p in root_path.rglob("*"):
29
+ if p.is_file() and p.suffix.lower() in {".py", ".java"}:
30
+ files.append(p.resolve())
31
+ files = sorted(set(files))
32
+
33
+ graph, dependents_count = build_graph_with_counts(
34
+ files,
35
+ root_path,
36
+ use_sqlite_cache=use_sqlite_cache,
37
+ )
38
+
39
+ hubs: set = set()
40
+ if not include_hubs:
41
+ hubs = get_hub_files_by_ratio(dependents_count, len(files), 0.5)
42
+
43
+ rev = build_reverse_graph(graph)
44
+ rel = bfs_related(rev, target_file, depth, skip=hubs)
45
+
46
+ if not rel:
47
+ typer.echo("No reverse-related files.")
48
+ raise typer.Exit(0)
49
+
50
+ for p in sorted(rel):
51
+ try:
52
+ typer.echo(str(p.relative_to(root_path)))
53
+ except Exception:
54
+ typer.echo(str(p))
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+ import typer
3
+
4
+ from .copy_cmd import register_copy
5
+ from .open_cmd import register_open
6
+ from .where_cmd import register_where
7
+
8
+
9
+ def register_nav_commands(app: typer.Typer) -> None:
10
+ register_copy(app)
11
+ register_open(app)
12
+ register_where(app)
@@ -0,0 +1,101 @@
1
+ from __future__ import annotations
2
+ from pathlib import Path
3
+ import typer
4
+
5
+ from ...core.resolve_target import resolve_target_file
6
+ from ...core.project import find_project_root
7
+
8
+
9
+ def _format_for_ai(files_content: list[tuple[Path, str]], root: Path) -> str:
10
+ lines: list[str] = []
11
+
12
+ lines.append("=" * 80)
13
+ lines.append(f"COPIED {len(files_content)} FILE(S)")
14
+ lines.append("=" * 80)
15
+ lines.append("")
16
+
17
+ for i, (path, content) in enumerate(files_content, 1):
18
+ try:
19
+ rel_path = path.relative_to(root)
20
+ except Exception:
21
+ rel_path = path
22
+
23
+ file_name = path.name
24
+ full_path = rel_path.as_posix()
25
+ lang = "java" if path.suffix == ".java" else "python"
26
+ line_count = content.count("\n") + 1
27
+
28
+ lines.append("=" * 80)
29
+ lines.append(f"FILE {i}/{len(files_content)}: {file_name}")
30
+ lines.append(f"Path: {full_path}")
31
+ lines.append(f"Lines: {line_count}")
32
+ lines.append("=" * 80)
33
+ lines.append("")
34
+ lines.append(f"```{lang}")
35
+ lines.append(content.rstrip())
36
+ lines.append("```")
37
+ lines.append("")
38
+
39
+ return "\n".join(lines)
40
+
41
+
42
+ def register_copy(app: typer.Typer) -> None:
43
+ @app.command(help="Copy file(s) contents to clipboard")
44
+ def copy(
45
+ files: list[str] = typer.Argument(..., help="File(s) to copy"),
46
+ root: str = typer.Option(".", "--root", "-r"),
47
+ raw: bool = typer.Option(False, "--raw", help="Raw content without formatting"),
48
+ ) -> None:
49
+ try:
50
+ import pyperclip
51
+ except ImportError:
52
+ typer.echo("Error: pyperclip not installed")
53
+ typer.echo("Install: pip install pyperclip")
54
+ raise typer.Exit(1)
55
+
56
+ root_path = Path(root).resolve()
57
+ project_root = find_project_root(root_path)
58
+
59
+ files_content: list[tuple[Path, str]] = []
60
+ total_lines = 0
61
+ total_chars = 0
62
+
63
+ for file_query in files:
64
+ try:
65
+ target, _ = resolve_target_file(file_query, root=str(project_root))
66
+ except Exception as e:
67
+ typer.echo(f"✗ Could not find: {file_query}")
68
+ continue
69
+
70
+ try:
71
+ content = target.read_text(encoding="utf-8", errors="ignore")
72
+ except Exception as e:
73
+ typer.echo(f"✗ Error reading {target.name}: {e}")
74
+ continue
75
+
76
+ files_content.append((target, content))
77
+ total_lines += content.count("\n") + 1
78
+ total_chars += len(content)
79
+
80
+ if not files_content:
81
+ typer.echo("✗ No files copied")
82
+ raise typer.Exit(1)
83
+
84
+ if raw:
85
+ combined = "\n\n".join(content for _, content in files_content)
86
+ else:
87
+ combined = _format_for_ai(files_content, project_root)
88
+
89
+ try:
90
+ pyperclip.copy(combined)
91
+ except Exception as e:
92
+ typer.echo(f"✗ Error copying to clipboard: {e}")
93
+ raise typer.Exit(1)
94
+
95
+ typer.echo(f"✓ Copied {len(files_content)} file(s) to clipboard")
96
+ typer.echo("")
97
+ for path, content in files_content:
98
+ lines = content.count("\n") + 1
99
+ typer.echo(f" • {path.name} ({lines:,} lines)")
100
+ typer.echo("")
101
+ typer.echo(f"Total: {total_lines:,} lines, {total_chars:,} characters")
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from ...core.opener import open_file
6
+ from ...core.resolve_target import resolve_target_file
7
+
8
+
9
+ def register_open(app: typer.Typer) -> None:
10
+ @app.command(help="Open a file by name or partial path (project-local).")
11
+ def open(
12
+ query: str = typer.Argument(..., help="Example: 'imports.py' or 'parser/imports.py'"),
13
+ yes: bool = typer.Option(False, "--yes", "-y", help="Open without confirmation"),
14
+ ):
15
+ target, _root = resolve_target_file(query)
16
+
17
+ if yes or typer.confirm(f"Open this file?\n{target}"):
18
+ open_file(target)
@@ -0,0 +1,21 @@
1
+ from __future__ import annotations
2
+
3
+ import typer
4
+
5
+ from ...errors import InvalidPathError
6
+ from ...core.where import file_to_module
7
+ from ...core.resolve_target import resolve_target_file
8
+
9
+
10
+ def register_where(app: typer.Typer) -> None:
11
+ @app.command(help="Convert file path (or filename) to import path.")
12
+ def where(
13
+ file: str = typer.Argument(..., help="File path OR filename like 'imports.py' or 'UserService.java'"),
14
+ ):
15
+ target, root_path = resolve_target_file(file)
16
+
17
+ mod = file_to_module(target, root_path)
18
+ if not mod:
19
+ raise InvalidPathError(message="Cannot convert to module path", path=target)
20
+
21
+ typer.echo(mod)
@@ -0,0 +1,26 @@
1
+ from __future__ import annotations
2
+ import typer
3
+
4
+ from .scan_cmd import register_scan
5
+ from .imports_cmd import register_imports
6
+ from .version_cmd import register_version
7
+ from .resolve_cmd import register_resolve
8
+ from .context_cmd import register_context
9
+ from .folder_cmd import register_folder
10
+ from .tree_cmd import register_tree
11
+ from .modeltree_cmd import register_modeltree
12
+ from .models_cmd import register_models
13
+ from .servicemap_cmd import register_servicemap
14
+
15
+
16
+ def register_project_commands(app: typer.Typer) -> None:
17
+ register_scan(app)
18
+ register_imports(app)
19
+ register_version(app)
20
+ register_resolve(app)
21
+ register_context(app)
22
+ register_folder(app)
23
+ register_tree(app)
24
+ register_modeltree(app)
25
+ register_models(app)
26
+ register_servicemap(app)