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/__init__.py +1 -0
- dap_cli/__main__.py +99 -0
- dap_cli/_dashboard/.gitkeep +5 -0
- dap_cli/_dashboard/BUNDLE_INFO.txt +3 -0
- dap_cli/_dashboard/package.json +52 -0
- dap_cli/_dashboard/server.js +38 -0
- dap_cli/bootstrap.py +262 -0
- dap_cli/commands/__init__.py +0 -0
- dap_cli/commands/cortex.py +688 -0
- dap_cli/commands/init.py +183 -0
- dap_cli/commands/project.py +134 -0
- dap_cli/commands/start.py +124 -0
- dap_cli/commands/status.py +136 -0
- dap_cli/commands/stop.py +35 -0
- dap_cli/dashboard.py +99 -0
- dap_cli/paths.py +27 -0
- dap_cli/process.py +130 -0
- dap_cli/py.typed +0 -0
- dap_cli-0.3.0.dist-info/METADATA +43 -0
- dap_cli-0.3.0.dist-info/RECORD +22 -0
- dap_cli-0.3.0.dist-info/WHEEL +4 -0
- dap_cli-0.3.0.dist-info/entry_points.txt +2 -0
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,,
|