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,145 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import signal
7
+ import subprocess
8
+ import time
9
+ from contextlib import suppress
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from threading import Event, Thread
13
+ from typing import TextIO
14
+
15
+ from ai_dev_tools.security.secrets import mask_text
16
+
17
+ _STOP = Event()
18
+
19
+
20
+ def main(argv: list[str] | None = None) -> int:
21
+ parser = argparse.ArgumentParser(prog="ai-dev-runtime-supervisor")
22
+ parser.add_argument("--metadata", type=Path, required=True)
23
+ parser.add_argument("--request", type=Path, required=True)
24
+ parser.add_argument("--token", required=True)
25
+ parser.add_argument("--cwd", type=Path, required=True)
26
+ parser.add_argument("--log", type=Path, required=True)
27
+ parser.add_argument("command", nargs=argparse.REMAINDER)
28
+ args = parser.parse_args(argv)
29
+ command = args.command[1:] if args.command[:1] == ["--"] else args.command
30
+ if not command:
31
+ return 2
32
+
33
+ for signal_name in ("SIGINT", "SIGTERM"):
34
+ value = getattr(signal, signal_name, None)
35
+ if value is not None:
36
+ # Embedded callers may run the supervisor in a non-main thread.
37
+ with suppress(ValueError):
38
+ signal.signal(value, _request_stop)
39
+
40
+ args.log.parent.mkdir(parents=True, exist_ok=True)
41
+ args.metadata.parent.mkdir(parents=True, exist_ok=True)
42
+ with args.log.open("a", encoding="utf-8", errors="replace") as log:
43
+ child = subprocess.Popen(
44
+ command,
45
+ cwd=args.cwd,
46
+ stdin=subprocess.DEVNULL,
47
+ stdout=subprocess.PIPE,
48
+ stderr=subprocess.STDOUT,
49
+ shell=False,
50
+ env=os.environ.copy(),
51
+ text=True,
52
+ encoding="utf-8",
53
+ errors="replace",
54
+ )
55
+ if child.stdout is None:
56
+ return 2
57
+ output_thread = Thread(target=_copy_output, args=(child.stdout, log), daemon=True)
58
+ output_thread.start()
59
+ state: dict[str, object] = {
60
+ "schema_version": "1",
61
+ "status": "running",
62
+ "supervisor_pid": os.getpid(),
63
+ "child_pid": child.pid,
64
+ "command": command,
65
+ "cwd": str(args.cwd),
66
+ "log": str(args.log),
67
+ "token": args.token,
68
+ "started_at": _now(),
69
+ "heartbeat_at": _now(),
70
+ }
71
+ _write_json(args.metadata, state)
72
+ try:
73
+ while child.poll() is None:
74
+ if _STOP.is_set() or _stop_requested(args.request, args.token):
75
+ state["status"] = "stopping"
76
+ _write_json(args.metadata, state)
77
+ _terminate(child)
78
+ break
79
+ state["heartbeat_at"] = _now()
80
+ _write_json(args.metadata, state)
81
+ time.sleep(0.25)
82
+ finally:
83
+ exit_code = child.poll()
84
+ if exit_code is None:
85
+ _terminate(child)
86
+ exit_code = child.poll()
87
+ output_thread.join(timeout=1)
88
+ state["status"] = "stopped" if _STOP.is_set() or args.request.exists() else "exited"
89
+ state["exit_code"] = exit_code
90
+ state["finished_at"] = _now()
91
+ state["heartbeat_at"] = _now()
92
+ _write_json(args.metadata, state)
93
+ if args.request.exists():
94
+ with suppress(OSError):
95
+ args.request.unlink()
96
+ return 0 if state["status"] == "stopped" else int(exit_code or 0)
97
+
98
+
99
+ def _request_stop(signum: int, frame: object) -> None:
100
+ del signum, frame
101
+ _STOP.set()
102
+
103
+
104
+ def _stop_requested(path: Path, token: str) -> bool:
105
+ try:
106
+ return path.read_text(encoding="utf-8").strip() == token
107
+ except OSError:
108
+ return False
109
+
110
+
111
+ def _terminate(process: subprocess.Popen[str]) -> None:
112
+ process.terminate()
113
+ try:
114
+ process.wait(timeout=5)
115
+ except subprocess.TimeoutExpired:
116
+ process.kill()
117
+ process.wait(timeout=5)
118
+
119
+
120
+ def _copy_output(stream: TextIO, log: TextIO) -> None:
121
+ for line in stream:
122
+ log.write(mask_text(line))
123
+ log.flush()
124
+
125
+
126
+ def _write_json(path: Path, data: dict[str, object]) -> None:
127
+ temporary = path.with_suffix(path.suffix + ".tmp")
128
+ temporary.write_text(json.dumps(data, indent=2, sort_keys=True) + "\n", encoding="utf-8")
129
+ deadline = time.monotonic() + 1.0
130
+ while True:
131
+ try:
132
+ os.replace(temporary, path)
133
+ return
134
+ except PermissionError:
135
+ if time.monotonic() >= deadline:
136
+ raise
137
+ time.sleep(0.01)
138
+
139
+
140
+ def _now() -> str:
141
+ return datetime.now(UTC).isoformat()
142
+
143
+
144
+ if __name__ == "__main__":
145
+ raise SystemExit(main())
@@ -0,0 +1 @@
1
+ """Security scanning."""
@@ -0,0 +1,58 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from dataclasses import dataclass
5
+ from pathlib import Path
6
+
7
+ SECRET_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = (
8
+ ("github_token", re.compile(r"gh[pousr]_[A-Za-z0-9_]{20,}")),
9
+ ("openai_key", re.compile(r"sk-[A-Za-z0-9_-]{20,}")),
10
+ ("openrouter_key", re.compile(r"sk-or-[A-Za-z0-9_-]{20,}")),
11
+ ("aws_access_key", re.compile(r"AKIA[0-9A-Z]{16}")),
12
+ ("private_key", re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----")),
13
+ ("connection_string", re.compile(r"(?i)(postgres|mysql|mongodb|redis)://[^\s]+")),
14
+ ("password_assignment", re.compile(r"(?i)(password|passwd|pwd)\s*=\s*['\"]?[^'\"\s]{8,}")),
15
+ ("api_key_assignment", re.compile(r"(?i)(api[_-]?key|token|secret)\s*=\s*['\"]?[^'\"\s]{12,}")),
16
+ )
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class SecretFinding:
21
+ path: str
22
+ line: int
23
+ kind: str
24
+ value: str
25
+
26
+ def masked_value(self) -> str:
27
+ return "***" if len(self.value) <= 8 else f"{self.value[:4]}...{self.value[-4:]}"
28
+
29
+ def masked_dict(self) -> dict[str, object]:
30
+ return {
31
+ "path": self.path,
32
+ "line": self.line,
33
+ "kind": self.kind,
34
+ "masked_value": self.masked_value(),
35
+ }
36
+
37
+
38
+ def scan_paths_for_secrets(root: Path, paths: list[Path]) -> list[SecretFinding]:
39
+ findings: list[SecretFinding] = []
40
+ for path in paths:
41
+ if not path.exists() or not path.is_file() or path.stat().st_size > 1_000_000:
42
+ continue
43
+ rel = str(path.relative_to(root))
44
+ if path.name == ".env":
45
+ findings.append(SecretFinding(rel, 1, "env_file", ".env"))
46
+ text = path.read_text(encoding="utf-8", errors="replace")
47
+ for line_number, line in enumerate(text.splitlines(), start=1):
48
+ for kind, pattern in SECRET_PATTERNS:
49
+ for match in pattern.finditer(line):
50
+ findings.append(SecretFinding(rel, line_number, kind, match.group(0)))
51
+ return findings
52
+
53
+
54
+ def mask_text(text: str) -> str:
55
+ masked = text
56
+ for kind, pattern in SECRET_PATTERNS:
57
+ masked = pattern.sub(f"***MASKED_{kind.upper()}***", masked)
58
+ return masked
@@ -0,0 +1 @@
1
+ """Utility helpers."""
@@ -0,0 +1,74 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+ import shlex
5
+ import shutil
6
+ import subprocess
7
+ import time
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+
11
+
12
+ @dataclass(slots=True)
13
+ class CommandResult:
14
+ command: list[str]
15
+ exit_code: int
16
+ stdout: str
17
+ stderr: str
18
+ duration_seconds: float
19
+ timed_out: bool = False
20
+ cached: bool = False
21
+
22
+ @property
23
+ def combined_output(self) -> str:
24
+ if self.stderr:
25
+ return f"{self.stdout}\n{self.stderr}".strip()
26
+ return self.stdout.strip()
27
+
28
+
29
+ def split_command(command: str) -> list[str]:
30
+ return shlex.split(command, posix=os.name != "nt")
31
+
32
+
33
+ def run_command(command: list[str], cwd: Path, timeout_seconds: int = 300) -> CommandResult:
34
+ started = time.monotonic()
35
+ executable_command = _windows_batch_command(command)
36
+ try:
37
+ completed = subprocess.run(
38
+ executable_command,
39
+ cwd=cwd,
40
+ text=True,
41
+ encoding="utf-8",
42
+ errors="replace",
43
+ capture_output=True,
44
+ timeout=timeout_seconds,
45
+ shell=False,
46
+ check=False,
47
+ )
48
+ return CommandResult(
49
+ command=command,
50
+ exit_code=completed.returncode,
51
+ stdout=completed.stdout,
52
+ stderr=completed.stderr,
53
+ duration_seconds=round(time.monotonic() - started, 3),
54
+ )
55
+ except FileNotFoundError as exc:
56
+ return CommandResult(command, 127, "", str(exc), round(time.monotonic() - started, 3))
57
+ except subprocess.TimeoutExpired as exc:
58
+ stdout = exc.stdout if isinstance(exc.stdout, str) else ""
59
+ stderr = exc.stderr if isinstance(exc.stderr, str) else ""
60
+ return CommandResult(
61
+ command, 124, stdout, stderr, round(time.monotonic() - started, 3), True
62
+ )
63
+
64
+
65
+ def _windows_batch_command(command: list[str]) -> list[str]:
66
+ if os.name != "nt" or not command:
67
+ return command
68
+ resolved = shutil.which(command[0]) or command[0]
69
+ executable = resolved.lower()
70
+ if executable.endswith((".cmd", ".bat")):
71
+ comspec = os.environ.get("COMSPEC", "cmd.exe")
72
+ executable_command = [resolved, *command[1:]]
73
+ return [comspec, "/d", "/c", *executable_command]
74
+ return command