ai-dev-cli-tools 0.5.0a1__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 (60) hide show
  1. ai_dev_cli_tools-0.5.0a1.dist-info/METADATA +240 -0
  2. ai_dev_cli_tools-0.5.0a1.dist-info/RECORD +60 -0
  3. ai_dev_cli_tools-0.5.0a1.dist-info/WHEEL +4 -0
  4. ai_dev_cli_tools-0.5.0a1.dist-info/entry_points.txt +2 -0
  5. ai_dev_cli_tools-0.5.0a1.dist-info/licenses/LICENSE +21 -0
  6. ai_dev_tools/__init__.py +3 -0
  7. ai_dev_tools/cache/__init__.py +11 -0
  8. ai_dev_tools/cache/graph.py +136 -0
  9. ai_dev_tools/cache/repository.py +169 -0
  10. ai_dev_tools/cache/validation.py +154 -0
  11. ai_dev_tools/cli.py +387 -0
  12. ai_dev_tools/completion.py +72 -0
  13. ai_dev_tools/config.py +223 -0
  14. ai_dev_tools/context/__init__.py +5 -0
  15. ai_dev_tools/context/builder.py +506 -0
  16. ai_dev_tools/context/incremental.py +107 -0
  17. ai_dev_tools/context/models.py +59 -0
  18. ai_dev_tools/context/profiles.py +49 -0
  19. ai_dev_tools/context/selection.py +270 -0
  20. ai_dev_tools/context/symbols.py +178 -0
  21. ai_dev_tools/detectors/__init__.py +1 -0
  22. ai_dev_tools/detectors/environment.py +125 -0
  23. ai_dev_tools/detectors/project.py +189 -0
  24. ai_dev_tools/detectors/repository_map.py +129 -0
  25. ai_dev_tools/detectors/runtime.py +190 -0
  26. ai_dev_tools/detectors/workspaces.py +228 -0
  27. ai_dev_tools/git/__init__.py +1 -0
  28. ai_dev_tools/git/inspect.py +219 -0
  29. ai_dev_tools/models/__init__.py +1 -0
  30. ai_dev_tools/models/report.py +95 -0
  31. ai_dev_tools/models/workspace.py +48 -0
  32. ai_dev_tools/parsers/__init__.py +1 -0
  33. ai_dev_tools/parsers/logs.py +372 -0
  34. ai_dev_tools/parsers/registry.py +60 -0
  35. ai_dev_tools/reporters/__init__.py +1 -0
  36. ai_dev_tools/reporters/progressive.py +161 -0
  37. ai_dev_tools/reporters/writer.py +74 -0
  38. ai_dev_tools/runners/__init__.py +1 -0
  39. ai_dev_tools/runners/baseline.py +190 -0
  40. ai_dev_tools/runners/bootstrap.py +191 -0
  41. ai_dev_tools/runners/bootstrap_models.py +64 -0
  42. ai_dev_tools/runners/bootstrap_strategies.py +444 -0
  43. ai_dev_tools/runners/cache.py +23 -0
  44. ai_dev_tools/runners/check.py +509 -0
  45. ai_dev_tools/runners/check_checkpoint.py +50 -0
  46. ai_dev_tools/runners/check_models.py +51 -0
  47. ai_dev_tools/runners/check_scheduler.py +94 -0
  48. ai_dev_tools/runners/check_selection.py +267 -0
  49. ai_dev_tools/runners/diagnostics.py +96 -0
  50. ai_dev_tools/runners/feedback.py +193 -0
  51. ai_dev_tools/runners/finish.py +105 -0
  52. ai_dev_tools/runners/focused.py +37 -0
  53. ai_dev_tools/runners/index.py +44 -0
  54. ai_dev_tools/runtime/__init__.py +3 -0
  55. ai_dev_tools/runtime/runner.py +380 -0
  56. ai_dev_tools/runtime/supervisor.py +145 -0
  57. ai_dev_tools/security/__init__.py +1 -0
  58. ai_dev_tools/security/secrets.py +58 -0
  59. ai_dev_tools/utils/__init__.py +1 -0
  60. ai_dev_tools/utils/subprocess.py +74 -0
@@ -0,0 +1,444 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import sys
5
+ import tomllib
6
+ from dataclasses import replace
7
+ from pathlib import Path
8
+
9
+ from ai_dev_tools.config import BootstrapSettings, Settings, load_settings
10
+ from ai_dev_tools.detectors.workspaces import detect_workspaces
11
+ from ai_dev_tools.runners.bootstrap_models import BootstrapOptions, BootstrapPlan, BootstrapStep
12
+
13
+
14
+ def build_bootstrap_plan(settings: Settings, options: BootstrapOptions) -> BootstrapPlan:
15
+ root = settings.project_root
16
+ configured_create_env = settings.bootstrap.create_env or options.create_env
17
+ before = _custom_steps(
18
+ settings.bootstrap.commands_before, "configured bootstrap before command"
19
+ )
20
+ after = _custom_steps(settings.bootstrap.commands_after, "configured bootstrap after command")
21
+ env_available = (root / ".env.example").exists()
22
+ env_will_create = configured_create_env and env_available and not (root / ".env").exists()
23
+ env_step = (
24
+ [
25
+ BootstrapStep(
26
+ "create .env from example",
27
+ ["copy", ".env.example", ".env"],
28
+ "--create-env or [bootstrap].create_env enabled and .env is missing",
29
+ "detected",
30
+ modifies_project=True,
31
+ action="copy_env",
32
+ )
33
+ ]
34
+ if env_will_create
35
+ else []
36
+ )
37
+
38
+ strategy = _strategy_for_root(settings)
39
+ monorepo = _detect_subprojects(root)
40
+ workspace_steps, workspace_smoke, workspace_tools = _workspace_bootstrap_steps(settings)
41
+ if strategy is None and not workspace_steps:
42
+ return BootstrapPlan(
43
+ "unknown",
44
+ None,
45
+ [*before, *env_step, *after],
46
+ [],
47
+ [],
48
+ env_available,
49
+ env_will_create,
50
+ monorepo,
51
+ )
52
+ if strategy is None:
53
+ project_type, package_manager = "monorepo", "multiple"
54
+ steps: list[BootstrapStep] = []
55
+ smoke: list[BootstrapStep] = []
56
+ tools: list[str] = []
57
+ else:
58
+ project_type, package_manager, steps, smoke, tools = strategy
59
+ if workspace_steps:
60
+ project_type = "monorepo"
61
+ package_manager = "multiple"
62
+ return BootstrapPlan(
63
+ project_type,
64
+ package_manager,
65
+ [*before, *env_step, *steps, *workspace_steps, *after],
66
+ [*smoke, *workspace_smoke],
67
+ sorted({*tools, *workspace_tools}),
68
+ env_available,
69
+ env_will_create,
70
+ monorepo,
71
+ )
72
+
73
+
74
+ def _workspace_bootstrap_steps(
75
+ settings: Settings,
76
+ ) -> tuple[list[BootstrapStep], list[BootstrapStep], list[str]]:
77
+ steps: list[BootstrapStep] = []
78
+ smoke_steps: list[BootstrapStep] = []
79
+ tools: list[str] = []
80
+ for workspace in detect_workspaces(settings.project_root):
81
+ if not workspace.root:
82
+ continue
83
+ child_settings = load_settings(settings.project_root / workspace.root)
84
+ strategy = _strategy_for_root(child_settings)
85
+ if strategy is None:
86
+ continue
87
+ _, _, child_steps, child_smoke, child_tools = strategy
88
+ steps.extend(
89
+ replace(
90
+ step,
91
+ name=f"{workspace.workspace_id}: {step.name}",
92
+ workspace=workspace.root,
93
+ )
94
+ for step in child_steps
95
+ )
96
+ smoke_steps.extend(
97
+ replace(
98
+ step,
99
+ name=f"{workspace.workspace_id}: {step.name}",
100
+ workspace=workspace.root,
101
+ )
102
+ for step in child_smoke
103
+ )
104
+ tools.extend(child_tools)
105
+ return steps, smoke_steps, sorted(set(tools))
106
+
107
+
108
+ def _strategy_for_root(
109
+ settings: Settings,
110
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]] | None:
111
+ root = settings.project_root
112
+ if (root / "uv.lock").exists() and (root / "pyproject.toml").exists():
113
+ return _python_uv_strategy(root)
114
+ if _is_poetry_project(root):
115
+ return _python_poetry_strategy(root)
116
+ if (root / "pyproject.toml").exists():
117
+ return _python_pip_strategy(settings)
118
+ if (root / "requirements.txt").exists():
119
+ return _python_requirements_strategy(settings)
120
+ if (root / "package.json").exists():
121
+ return _node_strategy(root, settings.bootstrap)
122
+ if (root / "pom.xml").exists():
123
+ return _maven_strategy(root)
124
+ if (root / "build.gradle").exists() or (root / "build.gradle.kts").exists():
125
+ return _gradle_strategy(root)
126
+ if (root / "Cargo.toml").exists():
127
+ return _simple_strategy("rust", "cargo", "cargo fetch", ["cargo", "fetch"], "cargo")
128
+ if (root / "composer.json").exists():
129
+ return _simple_strategy(
130
+ "php",
131
+ "composer",
132
+ "composer install --no-interaction",
133
+ ["composer", "install", "--no-interaction"],
134
+ "composer",
135
+ )
136
+ return None
137
+
138
+
139
+ def _python_uv_strategy(
140
+ root: Path,
141
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]]:
142
+ return (
143
+ "python",
144
+ "uv",
145
+ [
146
+ BootstrapStep(
147
+ "uv sync",
148
+ ["uv", "sync"],
149
+ "uv.lock and pyproject.toml detected",
150
+ "detected",
151
+ required_tool="uv",
152
+ )
153
+ ],
154
+ _pytest_smoke(["uv", "run", "pytest", "--collect-only"], root, "uv"),
155
+ ["uv"],
156
+ )
157
+
158
+
159
+ def _python_poetry_strategy(
160
+ root: Path,
161
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]]:
162
+ return (
163
+ "python",
164
+ "poetry",
165
+ [
166
+ BootstrapStep(
167
+ "poetry install",
168
+ ["poetry", "install"],
169
+ "Poetry configuration detected",
170
+ "detected",
171
+ required_tool="poetry",
172
+ )
173
+ ],
174
+ _pytest_smoke(["poetry", "run", "pytest", "--collect-only"], root, "poetry"),
175
+ ["poetry"],
176
+ )
177
+
178
+
179
+ def _python_pip_strategy(
180
+ settings: Settings,
181
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]]:
182
+ root = settings.project_root
183
+ venv_dir = root / settings.bootstrap.python_venv
184
+ venv_python = _venv_python(settings.bootstrap.python_venv)
185
+ steps: list[BootstrapStep] = []
186
+ if not venv_dir.exists():
187
+ steps.append(
188
+ BootstrapStep(
189
+ "create virtual environment",
190
+ [sys.executable, "-m", "venv", settings.bootstrap.python_venv],
191
+ "pip bootstrap uses an isolated local virtual environment",
192
+ "detected",
193
+ action="create_venv",
194
+ )
195
+ )
196
+ steps.append(
197
+ BootstrapStep(
198
+ "upgrade pip",
199
+ [venv_python, "-m", "pip", "install", "--upgrade", "pip"],
200
+ "ensure local virtual environment has a current pip",
201
+ "detected",
202
+ )
203
+ )
204
+ if _is_installable_python_project(root):
205
+ steps.append(
206
+ BootstrapStep(
207
+ "install project editable",
208
+ [venv_python, "-m", "pip", "install", "-e", "."],
209
+ "installable Python project metadata detected",
210
+ "detected",
211
+ )
212
+ )
213
+ return (
214
+ "python",
215
+ "pip",
216
+ steps,
217
+ _pytest_smoke([venv_python, "-m", "pytest", "--collect-only"], root, None),
218
+ ["python"],
219
+ )
220
+
221
+
222
+ def _python_requirements_strategy(
223
+ settings: Settings,
224
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]]:
225
+ project_type, package_manager, steps, smoke, tools = _python_pip_strategy(settings)
226
+ venv_python = _venv_python(settings.bootstrap.python_venv)
227
+ steps.append(
228
+ BootstrapStep(
229
+ "install requirements",
230
+ [venv_python, "-m", "pip", "install", "-r", "requirements.txt"],
231
+ "requirements.txt detected",
232
+ "detected",
233
+ )
234
+ )
235
+ return project_type, package_manager, steps, smoke, tools
236
+
237
+
238
+ def _node_strategy(
239
+ root: Path, bootstrap: BootstrapSettings
240
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]]:
241
+ package_text = (root / "package.json").read_text(encoding="utf-8", errors="replace")
242
+ package_manager = _node_package_manager(root, package_text)
243
+ command: list[str]
244
+ reason: str
245
+ if package_manager == "pnpm":
246
+ command = ["pnpm", "install"]
247
+ if bootstrap.node_frozen_lockfile and (root / "pnpm-lock.yaml").exists():
248
+ command.append("--frozen-lockfile")
249
+ reason = "pnpm lockfile or packageManager detected"
250
+ elif package_manager == "yarn":
251
+ command = ["yarn", "install"]
252
+ if bootstrap.node_frozen_lockfile and (root / "yarn.lock").exists():
253
+ command.append("--immutable")
254
+ reason = "yarn lockfile or packageManager detected"
255
+ else:
256
+ command = ["npm", "ci"] if (root / "package-lock.json").exists() else ["npm", "install"]
257
+ reason = "npm lockfile detected" if command[-1] == "ci" else "npm fallback without lockfile"
258
+ return (
259
+ "node",
260
+ package_manager,
261
+ [
262
+ BootstrapStep(
263
+ "install node dependencies",
264
+ command,
265
+ reason,
266
+ "detected",
267
+ required_tool=package_manager,
268
+ )
269
+ ],
270
+ [],
271
+ [package_manager],
272
+ )
273
+
274
+
275
+ def _maven_strategy(
276
+ root: Path,
277
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]]:
278
+ wrapper = _wrapper(root, "mvnw", "mvnw.cmd")
279
+ command = [wrapper or "mvn", "dependency:go-offline"]
280
+ tool = None if wrapper else "maven"
281
+ return (
282
+ "java",
283
+ "maven",
284
+ [
285
+ BootstrapStep(
286
+ "maven go offline",
287
+ command,
288
+ "pom.xml detected; wrapper preferred when present",
289
+ "detected",
290
+ required_tool=tool,
291
+ )
292
+ ],
293
+ [],
294
+ [] if wrapper else ["maven"],
295
+ )
296
+
297
+
298
+ def _gradle_strategy(
299
+ root: Path,
300
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]]:
301
+ wrapper = _wrapper(root, "gradlew", "gradlew.bat")
302
+ command = [wrapper or "gradle", "dependencies"]
303
+ tool = None if wrapper else "gradle"
304
+ return (
305
+ "java",
306
+ "gradle",
307
+ [
308
+ BootstrapStep(
309
+ "gradle dependencies",
310
+ command,
311
+ "Gradle build file detected; wrapper preferred when present",
312
+ "detected",
313
+ required_tool=tool,
314
+ )
315
+ ],
316
+ [],
317
+ [] if wrapper else ["gradle"],
318
+ )
319
+
320
+
321
+ def _simple_strategy(
322
+ project_type: str, package_manager: str, name: str, command: list[str], tool: str
323
+ ) -> tuple[str, str, list[BootstrapStep], list[BootstrapStep], list[str]]:
324
+ return (
325
+ project_type,
326
+ package_manager,
327
+ [
328
+ BootstrapStep(
329
+ name,
330
+ command,
331
+ f"{package_manager} project file detected",
332
+ "detected",
333
+ required_tool=tool,
334
+ )
335
+ ],
336
+ [],
337
+ [tool],
338
+ )
339
+
340
+
341
+ def _pytest_smoke(command: list[str], root: Path, tool: str | None) -> list[BootstrapStep]:
342
+ if (root / "tests").exists() or "pytest" in _pyproject_text(root):
343
+ return [
344
+ BootstrapStep(
345
+ "pytest collect only",
346
+ command,
347
+ "pytest configuration or tests directory detected",
348
+ "detected",
349
+ modifies_project=False,
350
+ required_tool=tool,
351
+ )
352
+ ]
353
+ return []
354
+
355
+
356
+ def _custom_steps(commands: list[list[str]], reason: str) -> list[BootstrapStep]:
357
+ return [
358
+ BootstrapStep(
359
+ "configured command",
360
+ command,
361
+ reason,
362
+ "configured",
363
+ required_tool=command[0] if command else None,
364
+ )
365
+ for command in commands
366
+ ]
367
+
368
+
369
+ def _is_poetry_project(root: Path) -> bool:
370
+ text = _pyproject_text(root)
371
+ return "[tool.poetry]" in text or "poetry-core" in text
372
+
373
+
374
+ def _is_installable_python_project(root: Path) -> bool:
375
+ if (root / "setup.py").exists() or (root / "setup.cfg").exists():
376
+ return True
377
+ try:
378
+ data = tomllib.loads(_pyproject_text(root))
379
+ except tomllib.TOMLDecodeError:
380
+ return False
381
+ return isinstance(data.get("project"), dict)
382
+
383
+
384
+ def _pyproject_text(root: Path) -> str:
385
+ path = root / "pyproject.toml"
386
+ return path.read_text(encoding="utf-8", errors="replace") if path.exists() else ""
387
+
388
+
389
+ def _node_package_manager(root: Path, package_text: str) -> str:
390
+ if (root / "pnpm-lock.yaml").exists():
391
+ return "pnpm"
392
+ if (root / "yarn.lock").exists():
393
+ return "yarn"
394
+ if (root / "package-lock.json").exists():
395
+ return "npm"
396
+ lowered = package_text.lower()
397
+ if '"packagemanager"' in lowered:
398
+ if "pnpm@" in lowered:
399
+ return "pnpm"
400
+ if "yarn@" in lowered:
401
+ return "yarn"
402
+ return "npm"
403
+
404
+
405
+ def _wrapper(root: Path, posix_name: str, windows_name: str) -> str | None:
406
+ windows = root / windows_name
407
+ posix = root / posix_name
408
+ if os.name == "nt" and windows.exists():
409
+ return windows_name
410
+ if posix.exists():
411
+ return f"./{posix_name}"
412
+ if windows.exists():
413
+ return windows_name
414
+ return None
415
+
416
+
417
+ def _venv_python(venv: str) -> str:
418
+ if os.name == "nt":
419
+ return str(Path(venv) / "Scripts" / "python.exe")
420
+ return str(Path(venv) / "bin" / "python")
421
+
422
+
423
+ def _detect_subprojects(root: Path) -> list[str]:
424
+ signals = {
425
+ "pyproject.toml",
426
+ "requirements.txt",
427
+ "package.json",
428
+ "pom.xml",
429
+ "build.gradle",
430
+ "build.gradle.kts",
431
+ "Cargo.toml",
432
+ "composer.json",
433
+ }
434
+ found: list[str] = []
435
+ for path in root.rglob("*"):
436
+ if not path.is_file() or path.name not in signals:
437
+ continue
438
+ rel = path.relative_to(root)
439
+ if len(rel.parts) <= 1 or any(
440
+ part in {".git", "node_modules", ".venv"} for part in rel.parts
441
+ ):
442
+ continue
443
+ found.append(str(rel.parent.as_posix()))
444
+ return sorted(set(found))[:25]
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+
5
+ from ai_dev_tools.cache.validation import (
6
+ clear_validation_cache,
7
+ prune_validation_cache,
8
+ validation_cache_stats,
9
+ )
10
+ from ai_dev_tools.models.report import Report
11
+
12
+
13
+ def run_cache(project_root: Path, action: str) -> Report:
14
+ root = project_root.resolve()
15
+ report = Report(command=f"cache {action}", project_root=root)
16
+ if action == "status":
17
+ result = validation_cache_stats(root)
18
+ elif action == "prune":
19
+ result = prune_validation_cache(root)
20
+ else:
21
+ result = clear_validation_cache(root)
22
+ report.summary = {"validation_cache": result}
23
+ return report