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,190 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import re
5
+ from datetime import UTC, datetime
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ from ai_dev_tools.models.report import Artifact, Report
10
+
11
+ BASELINE_SCHEMA_VERSION = "1"
12
+ _SAFE_NAME = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,63}$")
13
+
14
+
15
+ def run_baseline(project_root: Path, action: str, name: str | None = None) -> Report:
16
+ root = project_root.resolve()
17
+ report = Report(command=f"baseline {action}" + (f" {name}" if name else ""), project_root=root)
18
+ directory = root / ".ai" / "cache" / "baselines"
19
+ if action == "list":
20
+ names = sorted(path.stem for path in directory.glob("*.json"))
21
+ report.summary = {"baselines": names, "count": len(names)}
22
+ return report
23
+ if not name or not _SAFE_NAME.fullmatch(name):
24
+ report.status = "failed"
25
+ report.exit_code = 2
26
+ report.summary = {
27
+ "message": "Baseline name must use 1-64 letters, digits, dots, dashes, or underscores.",
28
+ "reason_code": "INVALID_BASELINE_NAME",
29
+ }
30
+ return report
31
+ path = directory / f"{name}.json"
32
+ if action == "create":
33
+ snapshot = _snapshot(root, name)
34
+ directory.mkdir(parents=True, exist_ok=True)
35
+ path.write_text(json.dumps(snapshot, indent=2, sort_keys=True) + "\n", encoding="utf-8")
36
+ report.summary = {
37
+ "name": name,
38
+ "reports": len(snapshot["reports"]),
39
+ "failures": len(_all_failures(snapshot)),
40
+ "created_at": snapshot["created_at"],
41
+ }
42
+ report.artifacts.append(Artifact(str(path), "baseline", "Local report baseline"))
43
+ return report
44
+ if action == "compare":
45
+ baseline = _load_baseline(path)
46
+ if baseline is None:
47
+ report.status = "failed"
48
+ report.exit_code = 1
49
+ report.summary = {
50
+ "message": f"Baseline does not exist or is invalid: {name}",
51
+ "reason_code": "BASELINE_NOT_FOUND",
52
+ }
53
+ return report
54
+ current = _snapshot(root, name)
55
+ report.summary = _compare(baseline, current)
56
+ report.summary["name"] = name
57
+ report.summary["baseline_created_at"] = baseline.get("created_at")
58
+ report.summary["ready"] = not (
59
+ report.summary["new_failures"] or report.summary["status_regressions"]
60
+ )
61
+ report.status = "success" if report.summary["ready"] else "failed"
62
+ return report
63
+ raise ValueError(f"Unsupported baseline action: {action}")
64
+
65
+
66
+ def _snapshot(root: Path, name: str) -> dict[str, Any]:
67
+ reports: dict[str, dict[str, object]] = {}
68
+ paths = [
69
+ *sorted((root / ".ai" / "reports").glob("*latest.json")),
70
+ *sorted((root / ".ai" / "context").glob("*latest.json")),
71
+ ]
72
+ for path in paths:
73
+ try:
74
+ payload = json.loads(path.read_text(encoding="utf-8"))
75
+ except (OSError, json.JSONDecodeError):
76
+ continue
77
+ command = payload.get("command")
78
+ if not isinstance(command, str):
79
+ continue
80
+ reports[command] = {
81
+ "status": payload.get("status"),
82
+ "failures": sorted(_failure_signatures(payload)),
83
+ "issue_codes": sorted(_issue_codes(payload)),
84
+ "source": str(path.relative_to(root)),
85
+ }
86
+ return {
87
+ "schema_version": BASELINE_SCHEMA_VERSION,
88
+ "name": name,
89
+ "created_at": datetime.now(UTC).isoformat(),
90
+ "reports": reports,
91
+ }
92
+
93
+
94
+ def _compare(baseline: dict[str, Any], current: dict[str, Any]) -> dict[str, object]:
95
+ old_reports = _report_map(baseline)
96
+ new_reports = _report_map(current)
97
+ old_failures = _all_failures(baseline)
98
+ new_failures = _all_failures(current)
99
+ changed_statuses = [
100
+ {
101
+ "command": command,
102
+ "baseline": old_reports[command].get("status"),
103
+ "current": new_reports[command].get("status"),
104
+ }
105
+ for command in sorted(old_reports.keys() & new_reports.keys())
106
+ if old_reports[command].get("status") != new_reports[command].get("status")
107
+ ]
108
+ old_issues = _all_report_values(baseline, "issue_codes")
109
+ new_issues = _all_report_values(current, "issue_codes")
110
+ status_regressions = [
111
+ item
112
+ for item in changed_statuses
113
+ if item["current"] in {"failed", "blocked", "environment_error"}
114
+ ]
115
+ unchanged = sum(
116
+ old_reports[command] == new_reports[command]
117
+ for command in old_reports.keys() & new_reports.keys()
118
+ )
119
+ return {
120
+ "new_failures": sorted(new_failures - old_failures),
121
+ "resolved_failures": sorted(old_failures - new_failures),
122
+ "new_issue_codes": sorted(new_issues - old_issues),
123
+ "resolved_issue_codes": sorted(old_issues - new_issues),
124
+ "changed_statuses": changed_statuses,
125
+ "status_regressions": status_regressions,
126
+ "new_reports": sorted(new_reports.keys() - old_reports.keys()),
127
+ "missing_reports": sorted(old_reports.keys() - new_reports.keys()),
128
+ "unchanged_reports": unchanged,
129
+ "baseline_report_count": len(old_reports),
130
+ "current_report_count": len(new_reports),
131
+ }
132
+
133
+
134
+ def _load_baseline(path: Path) -> dict[str, Any] | None:
135
+ try:
136
+ payload = json.loads(path.read_text(encoding="utf-8"))
137
+ except (OSError, json.JSONDecodeError):
138
+ return None
139
+ if not isinstance(payload, dict) or payload.get("schema_version") != BASELINE_SCHEMA_VERSION:
140
+ return None
141
+ return payload
142
+
143
+
144
+ def _report_map(snapshot: dict[str, Any]) -> dict[str, dict[str, object]]:
145
+ reports = snapshot.get("reports")
146
+ if not isinstance(reports, dict):
147
+ return {}
148
+ return {str(command): value for command, value in reports.items() if isinstance(value, dict)}
149
+
150
+
151
+ def _all_report_values(snapshot: dict[str, Any], key: str) -> set[str]:
152
+ values: set[str] = set()
153
+ for command, report in _report_map(snapshot).items():
154
+ entries = report.get(key)
155
+ if isinstance(entries, list):
156
+ values.update(f"{command}:{entry}" for entry in entries if isinstance(entry, str))
157
+ return values
158
+
159
+
160
+ def _all_failures(snapshot: dict[str, Any]) -> set[str]:
161
+ failures: set[str] = set()
162
+ for command, report in _report_map(snapshot).items():
163
+ values = report.get("failures")
164
+ if isinstance(values, list):
165
+ failures.update(f"{command}:{value}" for value in values if isinstance(value, str))
166
+ return failures
167
+
168
+
169
+ def _failure_signatures(value: object) -> set[str]:
170
+ result: set[str] = set()
171
+ if isinstance(value, dict):
172
+ for key, child in value.items():
173
+ if key in {"failure_signature", "signature"} and isinstance(child, str) and child:
174
+ result.add(child)
175
+ else:
176
+ result.update(_failure_signatures(child))
177
+ elif isinstance(value, list):
178
+ for child in value:
179
+ result.update(_failure_signatures(child))
180
+ return result
181
+
182
+
183
+ def _issue_codes(payload: dict[str, object]) -> set[str]:
184
+ result: set[str] = set()
185
+ issues = payload.get("issues")
186
+ if isinstance(issues, list):
187
+ for issue in issues:
188
+ if isinstance(issue, dict) and isinstance(issue.get("code"), str):
189
+ result.add(str(issue["code"]))
190
+ return result
@@ -0,0 +1,191 @@
1
+ from __future__ import annotations
2
+
3
+ import shutil
4
+ from datetime import datetime
5
+ from pathlib import Path
6
+ from typing import Literal
7
+
8
+ from ai_dev_tools.config import Settings, load_settings
9
+ from ai_dev_tools.detectors.environment import run_doctor
10
+ from ai_dev_tools.detectors.project import scan_project
11
+ from ai_dev_tools.models.report import Artifact, Issue, Report
12
+ from ai_dev_tools.reporters.writer import write_json, write_markdown
13
+ from ai_dev_tools.runners.bootstrap_models import (
14
+ BootstrapOptions as BootstrapOptions,
15
+ )
16
+ from ai_dev_tools.runners.bootstrap_models import (
17
+ BootstrapPlan as BootstrapPlan,
18
+ )
19
+ from ai_dev_tools.runners.bootstrap_models import (
20
+ BootstrapStep as BootstrapStep,
21
+ )
22
+ from ai_dev_tools.runners.bootstrap_strategies import (
23
+ build_bootstrap_plan as build_bootstrap_plan,
24
+ )
25
+ from ai_dev_tools.security.secrets import mask_text
26
+ from ai_dev_tools.utils.subprocess import CommandResult, run_command
27
+
28
+ BootstrapStatus = Literal["planned", "executed", "skipped", "failed"]
29
+
30
+
31
+ def run_bootstrap(project_root: Path, options: BootstrapOptions) -> Report:
32
+ settings = load_settings(project_root)
33
+ report = Report(command="bootstrap", project_root=settings.project_root)
34
+ scan = scan_project(settings.project_root)
35
+ doctor = run_doctor(settings.project_root)
36
+ plan = build_bootstrap_plan(settings, options)
37
+ missing_tools = _missing_required_tools(plan, doctor)
38
+ incompatible_runtimes = list(doctor.summary.get("incompatible_runtimes", []))
39
+ log_path = _log_path(settings.logs_directory)
40
+
41
+ if settings.warnings:
42
+ report.issues.extend(
43
+ Issue("warning", warning, code="CONFIG_WARNING") for warning in settings.warnings
44
+ )
45
+ if incompatible_runtimes:
46
+ report.status = "blocked"
47
+ report.issues.extend(
48
+ Issue(
49
+ "error",
50
+ (
51
+ f"Incompatible {item.get('runtime')} runtime: "
52
+ f"requires {item.get('constraint')}, detected {item.get('detected')}."
53
+ ),
54
+ code="INCOMPATIBLE_RUNTIME",
55
+ )
56
+ for item in incompatible_runtimes
57
+ if isinstance(item, dict)
58
+ )
59
+ elif not plan.steps and plan.project_type == "unknown":
60
+ report.status = "blocked"
61
+ report.issues.append(
62
+ Issue("error", "No supported project bootstrap strategy detected.", code="NO_STRATEGY")
63
+ )
64
+ elif missing_tools:
65
+ report.status = "blocked"
66
+ report.issues.extend(
67
+ Issue("error", f"Missing required bootstrap tool: {tool}", code="MISSING_RUNTIME")
68
+ for tool in missing_tools
69
+ )
70
+ else:
71
+ report.status = "success"
72
+
73
+ executed: list[dict[str, object]] = []
74
+ created_venv = False
75
+ created_env = False
76
+ smoke_check = "skipped"
77
+
78
+ should_execute = report.status == "success" and not options.explain and not options.dry_run
79
+ if should_execute:
80
+ for step in plan.steps:
81
+ result = _execute_step(step, settings, log_path)
82
+ executed.append(_step_result(step, result))
83
+ if step.action == "create_venv" and result.exit_code == 0:
84
+ created_venv = True
85
+ if step.action == "copy_env" and result.exit_code == 0:
86
+ created_env = True
87
+ if result.exit_code != 0:
88
+ report.status = "failed"
89
+ report.exit_code = result.exit_code
90
+ report.issues.append(
91
+ Issue("error", f"Bootstrap command failed: {step.name}", code="COMMAND_FAILED")
92
+ )
93
+ break
94
+ if report.status == "success" and settings.bootstrap.run_smoke_check:
95
+ smoke_results = [_execute_step(step, settings, log_path) for step in plan.smoke_steps]
96
+ executed.extend(
97
+ _step_result(step, result)
98
+ for step, result in zip(plan.smoke_steps, smoke_results, strict=True)
99
+ )
100
+ smoke_check = (
101
+ "passed" if all(result.exit_code == 0 for result in smoke_results) else "failed"
102
+ )
103
+ if smoke_check == "failed":
104
+ report.status = "partial"
105
+ report.issues.append(
106
+ Issue("warning", "Bootstrap smoke check failed.", code="SMOKE_FAILED")
107
+ )
108
+ elif options.explain or options.dry_run:
109
+ smoke_check = "planned" if plan.smoke_steps else "skipped"
110
+
111
+ summary: dict[str, object] = {
112
+ "project_type": plan.project_type,
113
+ "package_manager": plan.package_manager,
114
+ "dry_run": options.dry_run,
115
+ "explain": options.explain,
116
+ "planned_commands": len(plan.steps),
117
+ "executed_commands": len([item for item in executed if item["exit_code"] == 0]),
118
+ "created_venv": created_venv,
119
+ "created_env": created_env,
120
+ "smoke_check": smoke_check,
121
+ "plan": plan.to_dict(),
122
+ "executed": executed,
123
+ "missing_tools": missing_tools,
124
+ "runtime_compatibility": doctor.summary.get("runtime_compatibility", []),
125
+ "scan": scan.summary,
126
+ "modifications": "NONE" if options.explain or options.dry_run else "PLANNED",
127
+ }
128
+ if log_path.exists():
129
+ summary["full_log"] = str(log_path)
130
+ report.artifacts.append(Artifact(str(log_path), "log", "Full bootstrap command output"))
131
+ report.summary = summary
132
+ report.finish()
133
+ write_markdown(report, settings.reports_directory / "bootstrap-latest.md")
134
+ write_json(report, settings.reports_directory / "bootstrap-latest.json")
135
+ return report
136
+
137
+
138
+ def _execute_step(step: BootstrapStep, settings: Settings, log_path: Path) -> CommandResult:
139
+ working_directory = (
140
+ settings.project_root / step.workspace if step.workspace else settings.project_root
141
+ )
142
+ if step.action == "copy_env":
143
+ source = working_directory / ".env.example"
144
+ target = working_directory / ".env"
145
+ if target.exists():
146
+ result = CommandResult(step.command, 0, "Skipped existing .env", "", 0.0)
147
+ else:
148
+ shutil.copyfile(source, target)
149
+ result = CommandResult(step.command, 0, "Created .env from .env.example", "", 0.0)
150
+ else:
151
+ result = run_command(step.command, working_directory, settings.bootstrap.timeout_seconds)
152
+ result.stdout = mask_text(result.stdout)
153
+ result.stderr = mask_text(result.stderr)
154
+ _append_log(log_path, step, result)
155
+ return result
156
+
157
+
158
+ def _append_log(log_path: Path, step: BootstrapStep, result: CommandResult) -> None:
159
+ log_path.parent.mkdir(parents=True, exist_ok=True)
160
+ with log_path.open("a", encoding="utf-8") as handle:
161
+ handle.write(f"$ {' '.join(step.command)}\n")
162
+ handle.write(result.combined_output + "\n")
163
+ handle.write(f"EXIT_CODE={result.exit_code} DURATION={result.duration_seconds}s\n\n")
164
+
165
+
166
+ def _step_result(step: BootstrapStep, result: CommandResult) -> dict[str, object]:
167
+ return {
168
+ "name": step.name,
169
+ "command": step.command,
170
+ "exit_code": result.exit_code,
171
+ "duration_seconds": result.duration_seconds,
172
+ "timed_out": result.timed_out,
173
+ }
174
+
175
+
176
+ def _missing_required_tools(plan: BootstrapPlan, doctor: Report) -> list[str]:
177
+ tools = doctor.summary.get("tools", {})
178
+ if not isinstance(tools, dict):
179
+ return plan.required_tools
180
+ missing: list[str] = []
181
+ for tool in plan.required_tools:
182
+ if tool == "python":
183
+ continue
184
+ tool_info = tools.get(tool)
185
+ if not isinstance(tool_info, dict) or tool_info.get("status") != "ok":
186
+ missing.append(tool)
187
+ return sorted(set(missing))
188
+
189
+
190
+ def _log_path(logs_dir: Path) -> Path:
191
+ return logs_dir / f"bootstrap-{datetime.now().strftime('%Y%m%d-%H%M%S-%f')}.log"
@@ -0,0 +1,64 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, dataclass
4
+ from typing import Literal
5
+
6
+ BootstrapStatus = Literal["planned", "executed", "skipped", "failed"]
7
+
8
+
9
+ @dataclass(frozen=True, slots=True)
10
+ class BootstrapOptions:
11
+ dry_run: bool = False
12
+ explain: bool = False
13
+ create_env: bool = False
14
+
15
+
16
+ @dataclass(frozen=True, slots=True)
17
+ class BootstrapStep:
18
+ name: str
19
+ command: list[str]
20
+ reason: str
21
+ source: str
22
+ modifies_project: bool = True
23
+ required_tool: str | None = None
24
+ action: str = "command"
25
+ workspace: str = ""
26
+
27
+ def to_dict(self) -> dict[str, object]:
28
+ return {**asdict(self), "reason_code": _bootstrap_reason_code(self)}
29
+
30
+
31
+ def _bootstrap_reason_code(step: BootstrapStep) -> str:
32
+ if step.action == "create_venv":
33
+ return "CREATE_VIRTUAL_ENVIRONMENT"
34
+ if step.action == "copy_env":
35
+ return "CREATE_ENVIRONMENT_FILE"
36
+ if step.workspace:
37
+ return "WORKSPACE_BOOTSTRAP"
38
+ if "smoke" in step.name.lower() or "verify" in step.reason.lower():
39
+ return "SMOKE_VALIDATION"
40
+ return "PROJECT_BOOTSTRAP"
41
+
42
+
43
+ @dataclass(frozen=True, slots=True)
44
+ class BootstrapPlan:
45
+ project_type: str
46
+ package_manager: str | None
47
+ steps: list[BootstrapStep]
48
+ smoke_steps: list[BootstrapStep]
49
+ required_tools: list[str]
50
+ env_available: bool = False
51
+ env_will_create: bool = False
52
+ monorepo_subprojects: list[str] | None = None
53
+
54
+ def to_dict(self) -> dict[str, object]:
55
+ return {
56
+ "project_type": self.project_type,
57
+ "package_manager": self.package_manager,
58
+ "steps": [step.to_dict() for step in self.steps],
59
+ "smoke_steps": [step.to_dict() for step in self.smoke_steps],
60
+ "required_tools": self.required_tools,
61
+ "env_available": self.env_available,
62
+ "env_will_create": self.env_will_create,
63
+ "monorepo_subprojects": self.monorepo_subprojects or [],
64
+ }