dap-cli 0.3.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.
dap_cli/dashboard.py ADDED
@@ -0,0 +1,99 @@
1
+ """Locate and launch the bundled dashboard (#302 sub-D2, #334).
2
+
3
+ The wheel ships the Next.js standalone bundle at
4
+ ``dap_cli/_dashboard/`` when built via the release pipeline (or after
5
+ running ``scripts/build-dashboard-bundle.sh`` locally). Dev wheels
6
+ built without the bundler ship an effectively-empty directory — this
7
+ module's ``find_bundle`` returns ``None`` in that case so ``dap
8
+ start`` can fall back to a friendly message instead of crashing.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ import shutil
15
+ import subprocess
16
+ import sys
17
+ from importlib.resources import files
18
+ from pathlib import Path
19
+
20
+
21
+ def find_bundle() -> Path | None:
22
+ """Return the absolute path of the dashboard bundle's ``server.js``,
23
+ or ``None`` when no real bundle is present.
24
+
25
+ The wheel always ships the ``_dashboard/`` directory (a
26
+ ``.gitkeep`` placeholder keeps it on disk) but only release /
27
+ Docker / locally-bundled builds carry ``server.js``. Probing for
28
+ ``server.js`` instead of the directory itself is the right
29
+ presence check.
30
+ """
31
+ try:
32
+ # ``importlib.resources.files`` resolves to the install location
33
+ # of the ``dap_cli`` package — works whether installed via wheel,
34
+ # pip install -e, or run from the source tree.
35
+ bundle_dir = Path(str(files("dap_cli") / "_dashboard"))
36
+ except (ModuleNotFoundError, FileNotFoundError):
37
+ return None
38
+ server_js = bundle_dir / "server.js"
39
+ return server_js if server_js.is_file() else None
40
+
41
+
42
+ def find_node() -> str | None:
43
+ """Return the path to the ``node`` binary, or ``None`` when Node isn't
44
+ installed.
45
+
46
+ The bundle runs on Node 20+ (matches the version the dashboard
47
+ was built with — see the Dockerfile). We don't verify the
48
+ version here because Next standalone is usually forgiving across
49
+ Node majors; the operator gets a clear runtime error from Node
50
+ itself on real incompatibility.
51
+ """
52
+ return shutil.which("node")
53
+
54
+
55
+ def spawn_dashboard(
56
+ *,
57
+ port: int,
58
+ engine_url: str,
59
+ ) -> subprocess.Popen[bytes] | None:
60
+ """Spawn ``node <bundle>/server.js`` in the background.
61
+
62
+ Returns the ``Popen`` handle on success, or ``None`` when either
63
+ the bundle or Node is missing — caller surfaces an appropriate
64
+ message and continues without the dashboard. Stdout/stderr are
65
+ inherited from the parent process so the operator sees engine
66
+ and dashboard logs interleaved without ``dap`` having to pump
67
+ bytes between pipes.
68
+ """
69
+ bundle = find_bundle()
70
+ if bundle is None:
71
+ return None
72
+ node = find_node()
73
+ if node is None:
74
+ return None
75
+
76
+ env = {
77
+ **os.environ,
78
+ "PORT": str(port),
79
+ "HOSTNAME": "127.0.0.1",
80
+ "DAP_ENGINE_URL": engine_url,
81
+ # Same flag the Dockerfile sets — keeps Next quiet on first run.
82
+ "NEXT_TELEMETRY_DISABLED": "1",
83
+ }
84
+
85
+ # ``Popen`` instead of ``run`` — we want the dashboard alive in
86
+ # the background while the engine runs in the foreground via
87
+ # uvicorn. The parent's ``finally`` block in ``dap start``
88
+ # terminates this process explicitly on shutdown.
89
+ return subprocess.Popen(
90
+ [node, str(bundle)],
91
+ env=env,
92
+ stdout=sys.stdout,
93
+ stderr=sys.stderr,
94
+ # New session so a SIGINT to the engine doesn't get duplicated
95
+ # to the dashboard before our own handler decides what to do.
96
+ # Best-effort: ``start_new_session`` is POSIX-only; on Windows
97
+ # the equivalent isn't available and Popen falls back gracefully.
98
+ start_new_session=True,
99
+ )
dap_cli/paths.py ADDED
@@ -0,0 +1,27 @@
1
+ """Standardowe ścieżki projektu DAP."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ LOCAL_DAP_DIR_NAME = ".dap"
8
+ USER_DAP_DIR = Path.home() / ".dap"
9
+
10
+ DEFAULT_ENGINE_PORT = 7333
11
+ DEFAULT_DASHBOARD_PORT = 7332
12
+
13
+
14
+ def local_dap_dir(cwd: Path | None = None) -> Path:
15
+ return (cwd or Path.cwd()) / LOCAL_DAP_DIR_NAME
16
+
17
+
18
+ def local_config_path(cwd: Path | None = None) -> Path:
19
+ return local_dap_dir(cwd) / "config.json"
20
+
21
+
22
+ def local_db_path(cwd: Path | None = None) -> Path:
23
+ return local_dap_dir(cwd) / "state.db"
24
+
25
+
26
+ def local_pid_path(cwd: Path | None = None) -> Path:
27
+ return local_dap_dir(cwd) / "dap.pid"
dap_cli/process.py ADDED
@@ -0,0 +1,130 @@
1
+ """PID file management + signal handling dla `dap start` / `stop` / `status`."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import contextlib
6
+ import json
7
+ import os
8
+ import signal
9
+ import time
10
+ from datetime import UTC, datetime
11
+ from pathlib import Path
12
+ from typing import TypedDict
13
+
14
+ import psutil
15
+
16
+ from dap_cli.paths import local_pid_path
17
+
18
+ GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS = 5
19
+ PID_KILL_POLL_INTERVAL_SECONDS = 0.1
20
+ SECONDS_PER_MINUTE = 60
21
+ SECONDS_PER_HOUR = 3600
22
+
23
+
24
+ class PidFile(TypedDict):
25
+ pid: int
26
+ port: int
27
+ started_at: str # ISO 8601 with timezone
28
+
29
+
30
+ def write_pid_file(pid: int, port: int, path: Path | None = None) -> Path:
31
+ pid_path = path or local_pid_path()
32
+ payload: PidFile = {
33
+ "pid": pid,
34
+ "port": port,
35
+ "started_at": datetime.now(UTC).isoformat(),
36
+ }
37
+ pid_path.parent.mkdir(parents=True, exist_ok=True)
38
+ pid_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")
39
+ return pid_path
40
+
41
+
42
+ def read_pid_file(path: Path | None = None) -> PidFile | None:
43
+ pid_path = path or local_pid_path()
44
+ if not pid_path.exists():
45
+ return None
46
+ try:
47
+ data = json.loads(pid_path.read_text(encoding="utf-8"))
48
+ except (json.JSONDecodeError, OSError):
49
+ return None
50
+ if not isinstance(data, dict):
51
+ return None
52
+ if not all(k in data for k in ("pid", "port", "started_at")):
53
+ return None
54
+ return PidFile(
55
+ pid=int(data["pid"]),
56
+ port=int(data["port"]),
57
+ started_at=str(data["started_at"]),
58
+ )
59
+
60
+
61
+ def remove_pid_file(path: Path | None = None) -> None:
62
+ pid_path = path or local_pid_path()
63
+ pid_path.unlink(missing_ok=True)
64
+
65
+
66
+ def is_process_alive(pid: int) -> bool:
67
+ """Check if process exists AND looks like ours (Python interpreter)."""
68
+ if not psutil.pid_exists(pid):
69
+ return False
70
+ try:
71
+ process = psutil.Process(pid)
72
+ return bool(process.is_running()) and process.status() != psutil.STATUS_ZOMBIE
73
+ except (psutil.NoSuchProcess, psutil.AccessDenied):
74
+ return False
75
+
76
+
77
+ def stop_process(pid: int, *, timeout: float = GRACEFUL_SHUTDOWN_TIMEOUT_SECONDS) -> bool:
78
+ """Send SIGTERM, wait for graceful exit, fall back to SIGKILL.
79
+
80
+ Returns True if process was alive and got terminated, False if already dead.
81
+ """
82
+ if not is_process_alive(pid):
83
+ return False
84
+
85
+ try:
86
+ os.kill(pid, signal.SIGTERM)
87
+ except ProcessLookupError:
88
+ return False
89
+
90
+ deadline = time.monotonic() + timeout
91
+ while time.monotonic() < deadline:
92
+ if not is_process_alive(pid):
93
+ return True
94
+ time.sleep(PID_KILL_POLL_INTERVAL_SECONDS)
95
+
96
+ with contextlib.suppress(ProcessLookupError):
97
+ os.kill(pid, signal.SIGKILL)
98
+
99
+ return True
100
+
101
+
102
+ def install_pid_cleanup_handlers(path: Path | None = None) -> None:
103
+ """Register SIGINT/SIGTERM handlers that remove PID file before exit."""
104
+ pid_path = path or local_pid_path()
105
+
106
+ def _handler(signum: int, _frame: object) -> None:
107
+ remove_pid_file(pid_path)
108
+ # Re-raise default handling so uvicorn / asyncio can shutdown cleanly.
109
+ signal.signal(signum, signal.SIG_DFL)
110
+ os.kill(os.getpid(), signum)
111
+
112
+ signal.signal(signal.SIGINT, _handler)
113
+ signal.signal(signal.SIGTERM, _handler)
114
+
115
+
116
+ def uptime_from_started_at(started_at_iso: str) -> str:
117
+ """Format uptime as human-readable from ISO timestamp."""
118
+ try:
119
+ started = datetime.fromisoformat(started_at_iso)
120
+ except ValueError:
121
+ return "?"
122
+ delta = datetime.now(UTC) - started
123
+ seconds = int(delta.total_seconds())
124
+ if seconds < SECONDS_PER_MINUTE:
125
+ return f"{seconds}s"
126
+ if seconds < SECONDS_PER_HOUR:
127
+ return f"{seconds // SECONDS_PER_MINUTE}m {seconds % SECONDS_PER_MINUTE}s"
128
+ hours = seconds // SECONDS_PER_HOUR
129
+ minutes = (seconds % SECONDS_PER_HOUR) // SECONDS_PER_MINUTE
130
+ return f"{hours}h {minutes}m"
dap_cli/py.typed ADDED
File without changes
@@ -0,0 +1,43 @@
1
+ Metadata-Version: 2.4
2
+ Name: dap-cli
3
+ Version: 0.3.0
4
+ Summary: DAP CLI launcher (dap init / start / stop / status)
5
+ Requires-Python: >=3.13
6
+ Requires-Dist: dap-engine
7
+ Requires-Dist: httpx>=0.27.2
8
+ Requires-Dist: psutil>=6.1.0
9
+ Requires-Dist: rich>=13.9.2
10
+ Requires-Dist: typer>=0.12.5
11
+ Requires-Dist: uvicorn[standard]>=0.32.0
12
+ Description-Content-Type: text/markdown
13
+
14
+ # dap-cli
15
+
16
+ Typer-based CLI launcher dla DAP. Spawn engine + dashboard, podobnie jak `npx paperclip`.
17
+
18
+ ## Komendy (F0)
19
+
20
+ | Komenda | Status | Opis |
21
+ | ---------------- | ----------- | ---------------------------------------------------------- |
22
+ | `dap --version` | ✅ | Wersja |
23
+ | `dap --help` | ✅ | Lista komend |
24
+ | `dap init` | ✅ | Tworzy `./.dap/` z config.json i podkatalogami |
25
+ | `dap start` | ✅ partial | Spawn engine na 127.0.0.1:7333 (dashboard: F6) |
26
+ | `dap stop` | 🚧 stub | F1 (PID management) |
27
+ | `dap status` | ✅ partial | Pokazuje czy projekt zainicjowany |
28
+
29
+ ## Lokalne uruchomienie z monorepo
30
+
31
+ ```bash
32
+ uv sync
33
+ uv run dap --version
34
+ uv run dap init
35
+ uv run dap start
36
+ ```
37
+
38
+ ## Po opublikowaniu (F12)
39
+
40
+ ```bash
41
+ uv tool install dap-cli
42
+ dap init && dap start
43
+ ```
@@ -0,0 +1,22 @@
1
+ dap_cli/__init__.py,sha256=VrXpHDu3erkzwl_WXrqINBm9xWkcyUy53IQOj042dOs,22
2
+ dap_cli/__main__.py,sha256=vrapAdppoP-ZYWPh7x7dMvUDk19sS5Oof0sA6CFKD24,2691
3
+ dap_cli/bootstrap.py,sha256=ZOI96412dqA48XIIo1TGTtSKfB2Cur8lk-Vp40O2p04,9883
4
+ dap_cli/dashboard.py,sha256=1E-fCMWs2S619jQgVMQuXJLUZ9H9wCEia-FB9NucGL0,3592
5
+ dap_cli/paths.py,sha256=1ntufmiUMAziTFnlgtTKVe1QcXdORpkBExWXqdGoP0E,631
6
+ dap_cli/process.py,sha256=pONKqgy3JYCa4926a29W5YJy-cdPCMdXvyx8DxrsGuQ,3900
7
+ dap_cli/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
+ dap_cli/commands/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ dap_cli/commands/cortex.py,sha256=T-zHfFfL9DTnb2WhCtYoKMBqC56K--fPlKL_zKvzi30,25562
10
+ dap_cli/commands/init.py,sha256=nJdrsAjEE1kjqwLofIe7vv3K0z3cdh8-poFGWJy_tbs,7009
11
+ dap_cli/commands/project.py,sha256=QrcEsYloPXVhzmOMXHue66ar7vhs9r9OEK6uocQ9TCA,4259
12
+ dap_cli/commands/start.py,sha256=cis0huwJ3NjsW91MBohP7clkBuzH7tFOXa6lVou8xK8,4042
13
+ dap_cli/commands/status.py,sha256=ejs9rfSkk5d1ffbqrPF3FgqJsHHAJErnch1hjagNzfY,4452
14
+ dap_cli/commands/stop.py,sha256=jakjwndZMZUJFtXhgPzDYx8zjZkJ75zhD8c9vnZ-UvA,1034
15
+ dap_cli/_dashboard/.gitkeep,sha256=igL1q83-AUb-mnv1Mk6prMvmC2rWUjgQM-_zpThBgDY,277
16
+ dap_cli/_dashboard/BUNDLE_INFO.txt,sha256=z1QSexjV5D3ha4ym8_9LFSpZn3mgjkRnevsRZmciAAU,70
17
+ dap_cli/_dashboard/package.json,sha256=rI4xjBFwOcqwXAZ6wcpKH9O1IB1U95vDrVxxJo6woKU,1489
18
+ dap_cli/_dashboard/server.js,sha256=vRc2-nBhHzD1fE-P14EJ7uU1xDhpbWMuHC9tAPh4Enw,5845
19
+ dap_cli-0.3.0.dist-info/METADATA,sha256=HQhzl9o0889PvZxaChrXjpbouON4Bx7Iltf0mhJRFXw,1450
20
+ dap_cli-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
21
+ dap_cli-0.3.0.dist-info/entry_points.txt,sha256=Jpf_ycfImek6vc3u-wUWXNDy-rx2HdAToKtNSQ2j0rY,45
22
+ dap_cli-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ dap = dap_cli.__main__:app