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/commands/init.py
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""dap init — bootstrap ./.dap/ + admin user (#302 sub-D4)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import getpass
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from rich.console import Console
|
|
12
|
+
|
|
13
|
+
from dap_cli.bootstrap import (
|
|
14
|
+
bootstrap_marker_path,
|
|
15
|
+
ensure_admin_user,
|
|
16
|
+
validate_email,
|
|
17
|
+
validate_password,
|
|
18
|
+
write_bootstrap_marker,
|
|
19
|
+
)
|
|
20
|
+
from dap_cli.paths import (
|
|
21
|
+
DEFAULT_DASHBOARD_PORT,
|
|
22
|
+
DEFAULT_ENGINE_PORT,
|
|
23
|
+
local_config_path,
|
|
24
|
+
local_dap_dir,
|
|
25
|
+
local_db_path,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
console = Console()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def init_command(
|
|
32
|
+
force: bool = False,
|
|
33
|
+
admin_email: str | None = None,
|
|
34
|
+
admin_password: str | None = None,
|
|
35
|
+
admin_password_stdin: bool = False,
|
|
36
|
+
) -> None:
|
|
37
|
+
"""Initialise ``.dap/`` and create / promote the bootstrap admin.
|
|
38
|
+
|
|
39
|
+
Three input modes for the admin credentials, in order of
|
|
40
|
+
precedence:
|
|
41
|
+
|
|
42
|
+
1. ``--admin-email`` + ``--admin-password-stdin`` (read password
|
|
43
|
+
from stdin) — automation-friendly, mirrors ``kubectl``.
|
|
44
|
+
2. ``--admin-email`` + ``--admin-password=...`` — quick, suitable
|
|
45
|
+
for local dev only (the password lands in shell history).
|
|
46
|
+
3. Interactive prompt — if neither flag is set, prompt for both.
|
|
47
|
+
Empty password → generate a random one and print once.
|
|
48
|
+
|
|
49
|
+
Re-running is idempotent: an existing user with the supplied
|
|
50
|
+
email gets promoted to admin without changing their password.
|
|
51
|
+
"""
|
|
52
|
+
dap_dir = local_dap_dir()
|
|
53
|
+
config_path = local_config_path()
|
|
54
|
+
|
|
55
|
+
if dap_dir.exists() and not force:
|
|
56
|
+
console.print(f"[yellow]⚠ .dap/ already exists at {dap_dir}[/yellow]")
|
|
57
|
+
console.print("[dim] Use --force to reinitialize.[/dim]")
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
(dap_dir / "pipelines").mkdir(parents=True, exist_ok=True)
|
|
61
|
+
(dap_dir / "agents").mkdir(parents=True, exist_ok=True)
|
|
62
|
+
(dap_dir / "runs").mkdir(parents=True, exist_ok=True)
|
|
63
|
+
|
|
64
|
+
default_config = {
|
|
65
|
+
"version": 1,
|
|
66
|
+
"engine": {"host": "127.0.0.1", "port": DEFAULT_ENGINE_PORT},
|
|
67
|
+
"dashboard": {"port": DEFAULT_DASHBOARD_PORT},
|
|
68
|
+
"runtimes": {"paths": {}},
|
|
69
|
+
}
|
|
70
|
+
config_path.write_text(json.dumps(default_config, indent=2) + "\n", encoding="utf-8")
|
|
71
|
+
|
|
72
|
+
# Bootstrap the admin user. Errors here are user-visible — we
|
|
73
|
+
# leave the ``.dap/`` skeleton on disk (better than rolling back
|
|
74
|
+
# to "no project at all" and forcing a re-init) but report the
|
|
75
|
+
# admin step as failed so the operator can fix and retry.
|
|
76
|
+
try:
|
|
77
|
+
email, password = _collect_credentials(
|
|
78
|
+
admin_email=admin_email,
|
|
79
|
+
admin_password=admin_password,
|
|
80
|
+
admin_password_stdin=admin_password_stdin,
|
|
81
|
+
)
|
|
82
|
+
except (ValueError, EOFError, KeyboardInterrupt) as exc:
|
|
83
|
+
console.print(f"[red]✗ Admin bootstrap aborted: {exc}[/red]")
|
|
84
|
+
console.print("[dim] Re-run `dap init --force` to retry.[/dim]")
|
|
85
|
+
raise SystemExit(2) from exc
|
|
86
|
+
|
|
87
|
+
# ``DAP_DB_PATH`` (set by the standalone compose to ``/data/state.db``)
|
|
88
|
+
# wins over the in-CWD default so ``dap init`` inside the container
|
|
89
|
+
# writes to the same SQLite the engine reads from. Otherwise the
|
|
90
|
+
# bootstrap row lands in an unrelated ``./.dap/state.db`` file and
|
|
91
|
+
# the operator can't actually log in.
|
|
92
|
+
env_db_path = os.environ.get("DAP_DB_PATH", "").strip()
|
|
93
|
+
db_path = Path(env_db_path) if env_db_path else local_db_path()
|
|
94
|
+
result = ensure_admin_user(
|
|
95
|
+
db_path,
|
|
96
|
+
email=email,
|
|
97
|
+
password=password,
|
|
98
|
+
)
|
|
99
|
+
write_bootstrap_marker(bootstrap_marker_path(dap_dir), result)
|
|
100
|
+
|
|
101
|
+
console.print("[green]✓ Initialized DAP project[/green]")
|
|
102
|
+
console.print(f"[dim] {dap_dir}/[/dim]")
|
|
103
|
+
console.print("[dim] ├─ config.json[/dim]")
|
|
104
|
+
console.print("[dim] ├─ bootstrap.json[/dim] [dim](admin metadata)[/dim]")
|
|
105
|
+
console.print("[dim] ├─ pipelines/[/dim]")
|
|
106
|
+
console.print("[dim] ├─ agents/[/dim]")
|
|
107
|
+
console.print("[dim] └─ runs/[/dim]")
|
|
108
|
+
console.print()
|
|
109
|
+
if result.promoted_existing:
|
|
110
|
+
console.print(
|
|
111
|
+
f"[green]✓ Admin: {result.email}[/green] [dim](existing user — promoted)[/dim]"
|
|
112
|
+
)
|
|
113
|
+
else:
|
|
114
|
+
console.print(f"[green]✓ Admin: {result.email}[/green]")
|
|
115
|
+
if result.generated_password is not None:
|
|
116
|
+
# Print exactly once. We don't store it anywhere — the
|
|
117
|
+
# operator must capture it now or run a reset later.
|
|
118
|
+
console.print()
|
|
119
|
+
console.print("[yellow]⚠ Generated random password — copy it now:[/yellow]")
|
|
120
|
+
console.print(f" [bold]{result.generated_password}[/bold]")
|
|
121
|
+
console.print(
|
|
122
|
+
"[dim] This is the only time it will be shown. "
|
|
123
|
+
"Re-run `dap init --force --admin-email=... --admin-password=...`[/dim]"
|
|
124
|
+
)
|
|
125
|
+
console.print("[dim] to set your own.[/dim]")
|
|
126
|
+
console.print()
|
|
127
|
+
console.print("[cyan]Next: dap start[/cyan]")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _collect_credentials(
|
|
131
|
+
*,
|
|
132
|
+
admin_email: str | None,
|
|
133
|
+
admin_password: str | None,
|
|
134
|
+
admin_password_stdin: bool,
|
|
135
|
+
) -> tuple[str, str | None]:
|
|
136
|
+
"""Resolve the email + password from flags or interactive prompts.
|
|
137
|
+
|
|
138
|
+
Returns ``(email, password)`` where ``password`` may be ``None``
|
|
139
|
+
when neither a flag nor an interactive entry produced one — in
|
|
140
|
+
that case the bootstrap helper generates a random value.
|
|
141
|
+
"""
|
|
142
|
+
# Email — flag wins; otherwise prompt. We never accept email
|
|
143
|
+
# via stdin because mixing stdin email + stdin password would
|
|
144
|
+
# be ambiguous, and emails are short enough to type live.
|
|
145
|
+
if admin_email is None:
|
|
146
|
+
if not sys.stdin.isatty():
|
|
147
|
+
raise ValueError(
|
|
148
|
+
"No --admin-email and stdin is not a TTY — can't prompt. "
|
|
149
|
+
"Pass --admin-email=... explicitly for non-interactive runs."
|
|
150
|
+
)
|
|
151
|
+
admin_email = input("Admin email: ").strip()
|
|
152
|
+
email = validate_email(admin_email)
|
|
153
|
+
|
|
154
|
+
# Password — three paths.
|
|
155
|
+
if admin_password_stdin:
|
|
156
|
+
# Read the WHOLE stdin (operator pipes via ``echo ... | dap init``).
|
|
157
|
+
# Strip trailing newline only — other whitespace is part of the
|
|
158
|
+
# password, the operator picks the format.
|
|
159
|
+
password: str | None = sys.stdin.read().rstrip("\n")
|
|
160
|
+
if not password:
|
|
161
|
+
raise ValueError("--admin-password-stdin set but stdin was empty")
|
|
162
|
+
validate_password(password)
|
|
163
|
+
return email, password
|
|
164
|
+
|
|
165
|
+
if admin_password is not None:
|
|
166
|
+
validate_password(admin_password)
|
|
167
|
+
return email, admin_password
|
|
168
|
+
|
|
169
|
+
if not sys.stdin.isatty():
|
|
170
|
+
# Non-interactive with no password provided → generate one.
|
|
171
|
+
# The bootstrap helper handles the generation; we just signal
|
|
172
|
+
# by returning ``None``.
|
|
173
|
+
return email, None
|
|
174
|
+
|
|
175
|
+
# Interactive prompt with getpass (no echo).
|
|
176
|
+
typed = getpass.getpass("Admin password (leave empty to auto-generate a random one): ")
|
|
177
|
+
if typed == "":
|
|
178
|
+
return email, None
|
|
179
|
+
validate_password(typed)
|
|
180
|
+
confirm = getpass.getpass("Re-enter password: ")
|
|
181
|
+
if confirm != typed:
|
|
182
|
+
raise ValueError("passwords did not match")
|
|
183
|
+
return email, typed
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""dap project — sub-commands for project-scoped pipeline operations.
|
|
2
|
+
|
|
3
|
+
Current pipeline types: cortex
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
|
|
12
|
+
from dap_cli.commands import cortex as _cortex
|
|
13
|
+
from dap_cli.paths import DEFAULT_ENGINE_PORT
|
|
14
|
+
|
|
15
|
+
project_app = typer.Typer(
|
|
16
|
+
name="project",
|
|
17
|
+
help="Project-scoped pipeline operations",
|
|
18
|
+
no_args_is_help=True,
|
|
19
|
+
add_completion=False,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
_ENGINE_DEFAULT = f"http://localhost:{DEFAULT_ENGINE_PORT}"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@project_app.command("run")
|
|
26
|
+
def cmd_run(
|
|
27
|
+
pipeline: Annotated[str, typer.Argument(help="Pipeline type (currently: cortex)")],
|
|
28
|
+
issue_url: Annotated[str, typer.Argument(help="GitHub issue URL to process")],
|
|
29
|
+
engine: Annotated[
|
|
30
|
+
str,
|
|
31
|
+
typer.Option("--engine", help="DAP engine base URL", envvar="DAP_ENGINE_URL"),
|
|
32
|
+
] = _ENGINE_DEFAULT,
|
|
33
|
+
no_interactive: Annotated[
|
|
34
|
+
bool,
|
|
35
|
+
typer.Option("--no-interactive", help="Approve all gates automatically"),
|
|
36
|
+
] = False,
|
|
37
|
+
watch: Annotated[
|
|
38
|
+
bool,
|
|
39
|
+
typer.Option("--watch", help="Monitor only — do not approve gates"),
|
|
40
|
+
] = False,
|
|
41
|
+
workspace: Annotated[
|
|
42
|
+
str | None,
|
|
43
|
+
typer.Option("--workspace", help="Local workspace path for the pipeline"),
|
|
44
|
+
] = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
"""Trigger a pipeline run for a GitHub issue.
|
|
47
|
+
|
|
48
|
+
Example:
|
|
49
|
+
|
|
50
|
+
dap project run cortex https://github.com/Dixter999/cortex-project/issues/332
|
|
51
|
+
"""
|
|
52
|
+
if pipeline != "cortex":
|
|
53
|
+
typer.echo(f"Unknown pipeline type: {pipeline!r}. Supported: cortex", err=True)
|
|
54
|
+
raise typer.Exit(1)
|
|
55
|
+
_cortex.cortex_run(
|
|
56
|
+
issue_url=issue_url,
|
|
57
|
+
engine_url=engine,
|
|
58
|
+
no_interactive=no_interactive,
|
|
59
|
+
watch=watch,
|
|
60
|
+
workspace=workspace,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@project_app.command("approve")
|
|
65
|
+
def cmd_approve(
|
|
66
|
+
pipeline: Annotated[str, typer.Argument(help="Pipeline type (currently: cortex)")],
|
|
67
|
+
run_id: Annotated[str, typer.Argument(help="Run ID to approve")],
|
|
68
|
+
engine: Annotated[
|
|
69
|
+
str,
|
|
70
|
+
typer.Option("--engine", help="DAP engine base URL", envvar="DAP_ENGINE_URL"),
|
|
71
|
+
] = _ENGINE_DEFAULT,
|
|
72
|
+
) -> None:
|
|
73
|
+
"""Approve the current gate for a paused run.
|
|
74
|
+
|
|
75
|
+
Example:
|
|
76
|
+
|
|
77
|
+
dap project approve cortex <run-id>
|
|
78
|
+
"""
|
|
79
|
+
if pipeline != "cortex":
|
|
80
|
+
typer.echo(f"Unknown pipeline type: {pipeline!r}. Supported: cortex", err=True)
|
|
81
|
+
raise typer.Exit(1)
|
|
82
|
+
_cortex.cortex_approve(run_id=run_id, engine_url=engine)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@project_app.command("reject")
|
|
86
|
+
def cmd_reject(
|
|
87
|
+
pipeline: Annotated[str, typer.Argument(help="Pipeline type (currently: cortex)")],
|
|
88
|
+
run_id: Annotated[str, typer.Argument(help="Run ID to reject")],
|
|
89
|
+
reason: Annotated[str, typer.Argument(help="Rejection reason")] = "",
|
|
90
|
+
engine: Annotated[
|
|
91
|
+
str,
|
|
92
|
+
typer.Option("--engine", help="DAP engine base URL", envvar="DAP_ENGINE_URL"),
|
|
93
|
+
] = _ENGINE_DEFAULT,
|
|
94
|
+
) -> None:
|
|
95
|
+
"""Reject (abort) the current gate for a paused run.
|
|
96
|
+
|
|
97
|
+
Example:
|
|
98
|
+
|
|
99
|
+
dap project reject cortex <run-id> "Phase 1 spec incomplete"
|
|
100
|
+
"""
|
|
101
|
+
if pipeline != "cortex":
|
|
102
|
+
typer.echo(f"Unknown pipeline type: {pipeline!r}. Supported: cortex", err=True)
|
|
103
|
+
raise typer.Exit(1)
|
|
104
|
+
_cortex.cortex_reject(run_id=run_id, reason=reason, engine_url=engine)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
@project_app.command("state")
|
|
108
|
+
def cmd_state(
|
|
109
|
+
pipeline: Annotated[str, typer.Argument(help="Pipeline type (currently: cortex)")],
|
|
110
|
+
run_id: Annotated[str, typer.Argument(help="Run ID to inspect")],
|
|
111
|
+
engine: Annotated[
|
|
112
|
+
str,
|
|
113
|
+
typer.Option("--engine", help="DAP engine base URL", envvar="DAP_ENGINE_URL"),
|
|
114
|
+
] = _ENGINE_DEFAULT,
|
|
115
|
+
fmt: Annotated[
|
|
116
|
+
str,
|
|
117
|
+
typer.Option(
|
|
118
|
+
"--format",
|
|
119
|
+
help="Output format: table (default) or json",
|
|
120
|
+
click_type=__import__("click").Choice(["table", "json"], case_sensitive=False),
|
|
121
|
+
),
|
|
122
|
+
] = "table",
|
|
123
|
+
) -> None:
|
|
124
|
+
"""Show current state of a pipeline run.
|
|
125
|
+
|
|
126
|
+
Example:
|
|
127
|
+
|
|
128
|
+
dap project state cortex <run-id>
|
|
129
|
+
dap project state cortex <run-id> --format json
|
|
130
|
+
"""
|
|
131
|
+
if pipeline != "cortex":
|
|
132
|
+
typer.echo(f"Unknown pipeline type: {pipeline!r}. Supported: cortex", err=True)
|
|
133
|
+
raise typer.Exit(1)
|
|
134
|
+
_cortex.cortex_state(run_id=run_id, engine_url=engine, fmt=fmt)
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""dap start — odpala FastAPI engine in-process + bundled dashboard."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
import os
|
|
7
|
+
|
|
8
|
+
import uvicorn
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from dap_cli.dashboard import find_bundle, find_node, spawn_dashboard
|
|
12
|
+
from dap_cli.paths import (
|
|
13
|
+
DEFAULT_DASHBOARD_PORT,
|
|
14
|
+
DEFAULT_ENGINE_PORT,
|
|
15
|
+
local_dap_dir,
|
|
16
|
+
local_db_path,
|
|
17
|
+
)
|
|
18
|
+
from dap_cli.process import (
|
|
19
|
+
install_pid_cleanup_handlers,
|
|
20
|
+
is_process_alive,
|
|
21
|
+
read_pid_file,
|
|
22
|
+
remove_pid_file,
|
|
23
|
+
write_pid_file,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
console = Console()
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def start_command(
|
|
30
|
+
port: int = DEFAULT_DASHBOARD_PORT,
|
|
31
|
+
engine_port: int = DEFAULT_ENGINE_PORT,
|
|
32
|
+
headless: bool = False,
|
|
33
|
+
) -> None:
|
|
34
|
+
if not local_dap_dir().exists():
|
|
35
|
+
console.print("[red]✗ No .dap/ found in current directory.[/red]")
|
|
36
|
+
console.print("[dim] Run `dap init` first.[/dim]")
|
|
37
|
+
raise SystemExit(1)
|
|
38
|
+
|
|
39
|
+
existing = read_pid_file()
|
|
40
|
+
if existing is not None:
|
|
41
|
+
if is_process_alive(existing["pid"]):
|
|
42
|
+
console.print(
|
|
43
|
+
f"[red]✗ DAP already running (PID {existing['pid']}, "
|
|
44
|
+
f"port {existing['port']}).[/red]",
|
|
45
|
+
)
|
|
46
|
+
console.print("[dim] Use `dap stop` first if you want to restart.[/dim]")
|
|
47
|
+
raise SystemExit(1)
|
|
48
|
+
console.print(
|
|
49
|
+
f"[yellow]⚠ Stale PID file "
|
|
50
|
+
f"(process {existing['pid']} not running) — cleaning up.[/yellow]",
|
|
51
|
+
)
|
|
52
|
+
remove_pid_file()
|
|
53
|
+
|
|
54
|
+
# Lazy import — dap --version / --help nie ładują FastAPI/SQLAlchemy
|
|
55
|
+
from dap_engine.app import EngineConfig, create_app # noqa: PLC0415
|
|
56
|
+
|
|
57
|
+
logging.basicConfig(
|
|
58
|
+
level=logging.INFO,
|
|
59
|
+
format="%(asctime)s %(levelname)-7s %(name)s | %(message)s",
|
|
60
|
+
datefmt="%H:%M:%S",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
config = EngineConfig(
|
|
64
|
+
db_path=str(local_db_path()),
|
|
65
|
+
host="127.0.0.1",
|
|
66
|
+
port=engine_port,
|
|
67
|
+
)
|
|
68
|
+
app = create_app(config)
|
|
69
|
+
|
|
70
|
+
write_pid_file(pid=os.getpid(), port=engine_port)
|
|
71
|
+
install_pid_cleanup_handlers()
|
|
72
|
+
|
|
73
|
+
console.print("[cyan]Starting DAP...[/cyan]")
|
|
74
|
+
console.print(f"[green]✓ engine[/green] http://127.0.0.1:{engine_port}")
|
|
75
|
+
|
|
76
|
+
# Try to spawn the bundled dashboard. ``spawn_dashboard`` returns
|
|
77
|
+
# None when either the bundle (``_dashboard/server.js``) or
|
|
78
|
+
# ``node`` is missing — both are expected in dev installs that
|
|
79
|
+
# haven't run ``scripts/build-dashboard-bundle.sh``.
|
|
80
|
+
dashboard_proc = spawn_dashboard(
|
|
81
|
+
port=port,
|
|
82
|
+
engine_url=f"http://127.0.0.1:{engine_port}",
|
|
83
|
+
)
|
|
84
|
+
if dashboard_proc is not None:
|
|
85
|
+
console.print(f"[green]✓ dashboard[/green] http://127.0.0.1:{port}")
|
|
86
|
+
else:
|
|
87
|
+
# Distinguish the two no-dashboard cases so the operator
|
|
88
|
+
# knows which knob to turn.
|
|
89
|
+
if find_bundle() is None:
|
|
90
|
+
reason = (
|
|
91
|
+
"no bundle in this wheel — run "
|
|
92
|
+
"[bold]scripts/build-dashboard-bundle.sh[/bold] from a checkout"
|
|
93
|
+
)
|
|
94
|
+
elif find_node() is None:
|
|
95
|
+
reason = "Node.js not on PATH — install Node 20+ to enable"
|
|
96
|
+
else:
|
|
97
|
+
reason = "unknown (check logs)"
|
|
98
|
+
console.print(
|
|
99
|
+
f"[yellow]○ dashboard[/yellow] not started [dim]({reason})[/dim]",
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
if not headless and dashboard_proc is not None:
|
|
103
|
+
console.print(f"[dim] Open http://127.0.0.1:{port} in your browser.[/dim]")
|
|
104
|
+
console.print()
|
|
105
|
+
console.print("[dim]Press Ctrl+C to stop.[/dim]")
|
|
106
|
+
|
|
107
|
+
try:
|
|
108
|
+
uvicorn.run(
|
|
109
|
+
app,
|
|
110
|
+
host=config.host,
|
|
111
|
+
port=config.port,
|
|
112
|
+
log_level="info",
|
|
113
|
+
access_log=False,
|
|
114
|
+
)
|
|
115
|
+
finally:
|
|
116
|
+
# Stop the dashboard before clearing our PID file so the
|
|
117
|
+
# operator never sees the dashboard outlive ``dap stop``.
|
|
118
|
+
if dashboard_proc is not None and dashboard_proc.poll() is None:
|
|
119
|
+
dashboard_proc.terminate()
|
|
120
|
+
try:
|
|
121
|
+
dashboard_proc.wait(timeout=5)
|
|
122
|
+
except Exception:
|
|
123
|
+
dashboard_proc.kill()
|
|
124
|
+
remove_pid_file()
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
"""dap status — pokazuje stan projektu DAP, runtime info gdy działa."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.table import Table
|
|
11
|
+
|
|
12
|
+
from dap_cli.bootstrap import bootstrap_marker_path, read_bootstrap_marker
|
|
13
|
+
from dap_cli.paths import local_dap_dir
|
|
14
|
+
from dap_cli.process import (
|
|
15
|
+
is_process_alive,
|
|
16
|
+
read_pid_file,
|
|
17
|
+
remove_pid_file,
|
|
18
|
+
uptime_from_started_at,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
|
|
23
|
+
HTTP_REQUEST_TIMEOUT_SECONDS = 2.0
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _fetch_runtimes(port: int) -> list[dict[str, Any]] | None:
|
|
27
|
+
base = f"http://127.0.0.1:{port}"
|
|
28
|
+
try:
|
|
29
|
+
with httpx.Client(timeout=HTTP_REQUEST_TIMEOUT_SECONDS) as client:
|
|
30
|
+
response = client.get(f"{base}/runtimes")
|
|
31
|
+
response.raise_for_status()
|
|
32
|
+
data = response.json()
|
|
33
|
+
except (httpx.HTTPError, ValueError):
|
|
34
|
+
return None
|
|
35
|
+
if not isinstance(data, list):
|
|
36
|
+
return None
|
|
37
|
+
return data
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _fetch_runtime_health(port: int, runtime_id: str) -> dict[str, Any] | None:
|
|
41
|
+
base = f"http://127.0.0.1:{port}"
|
|
42
|
+
try:
|
|
43
|
+
with httpx.Client(timeout=HTTP_REQUEST_TIMEOUT_SECONDS) as client:
|
|
44
|
+
response = client.get(f"{base}/runtimes/{runtime_id}/health")
|
|
45
|
+
response.raise_for_status()
|
|
46
|
+
data = response.json()
|
|
47
|
+
except (httpx.HTTPError, ValueError):
|
|
48
|
+
return None
|
|
49
|
+
if not isinstance(data, dict):
|
|
50
|
+
return None
|
|
51
|
+
return data
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _print_bootstrap_section(dap_dir: Path) -> None:
|
|
55
|
+
"""Print admin-bootstrap state (one line) above the engine status.
|
|
56
|
+
|
|
57
|
+
Reads ``.dap/bootstrap.json`` (written by ``dap init``). Stays
|
|
58
|
+
silent when it's not present *and* the project is otherwise
|
|
59
|
+
initialised — we don't want to nag every ``dap status`` call when
|
|
60
|
+
the operator hasn't bootstrapped yet, so we just show a hint."""
|
|
61
|
+
marker = read_bootstrap_marker(bootstrap_marker_path(dap_dir))
|
|
62
|
+
if marker is None:
|
|
63
|
+
console.print("[yellow]○ admin bootstrap:[/yellow] none — run `dap init` to create one")
|
|
64
|
+
return
|
|
65
|
+
email = marker.get("email", "?")
|
|
66
|
+
created = marker.get("created_at", "?")
|
|
67
|
+
console.print(f"[green]✓ admin bootstrap:[/green] {email} [dim]({created})[/dim]")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def status_command() -> None:
|
|
71
|
+
dap_dir = local_dap_dir()
|
|
72
|
+
|
|
73
|
+
if not dap_dir.exists():
|
|
74
|
+
console.print("[red]✗ Not a DAP project[/red]")
|
|
75
|
+
console.print("[dim] Run `dap init` to initialize.[/dim]")
|
|
76
|
+
return
|
|
77
|
+
|
|
78
|
+
_print_bootstrap_section(dap_dir)
|
|
79
|
+
|
|
80
|
+
pid_info = read_pid_file()
|
|
81
|
+
|
|
82
|
+
if pid_info is None:
|
|
83
|
+
console.print("[green]✓ DAP project[/green]")
|
|
84
|
+
console.print(f"[dim] {dap_dir}[/dim]")
|
|
85
|
+
console.print()
|
|
86
|
+
console.print("[yellow]○ engine: stopped[/yellow]")
|
|
87
|
+
console.print("[dim] Run `dap start` to launch.[/dim]")
|
|
88
|
+
return
|
|
89
|
+
|
|
90
|
+
pid = pid_info["pid"]
|
|
91
|
+
port = pid_info["port"]
|
|
92
|
+
started_at = pid_info["started_at"]
|
|
93
|
+
|
|
94
|
+
if not is_process_alive(pid):
|
|
95
|
+
console.print("[green]✓ DAP project[/green]")
|
|
96
|
+
console.print(f"[dim] {dap_dir}[/dim]")
|
|
97
|
+
console.print()
|
|
98
|
+
console.print(f"[red]✗ engine: stale PID file (process {pid} not running)[/red]")
|
|
99
|
+
remove_pid_file()
|
|
100
|
+
console.print("[dim] Cleaned up. Run `dap start`.[/dim]")
|
|
101
|
+
return
|
|
102
|
+
|
|
103
|
+
uptime = uptime_from_started_at(started_at)
|
|
104
|
+
console.print("[green]✓ DAP project[/green]")
|
|
105
|
+
console.print(f"[dim] {dap_dir}[/dim]")
|
|
106
|
+
console.print()
|
|
107
|
+
console.print(f"[green]● engine: running[/green] PID {pid} port {port} uptime {uptime}")
|
|
108
|
+
|
|
109
|
+
runtimes = _fetch_runtimes(port)
|
|
110
|
+
if runtimes is None:
|
|
111
|
+
console.print(
|
|
112
|
+
"[yellow] ⚠ Engine process running but /runtimes endpoint unreachable.[/yellow]"
|
|
113
|
+
)
|
|
114
|
+
return
|
|
115
|
+
|
|
116
|
+
table = Table(title="Runtime adapters", show_lines=False, header_style="bold")
|
|
117
|
+
table.add_column("ID")
|
|
118
|
+
table.add_column("Display name")
|
|
119
|
+
table.add_column("Kind")
|
|
120
|
+
table.add_column("Available")
|
|
121
|
+
|
|
122
|
+
for entry in runtimes:
|
|
123
|
+
rt_id = str(entry.get("id", "?"))
|
|
124
|
+
health = _fetch_runtime_health(port, rt_id)
|
|
125
|
+
available = (
|
|
126
|
+
"[green]✓[/green]" if health is not None and health.get("available") else "[red]✗[/red]"
|
|
127
|
+
)
|
|
128
|
+
table.add_row(
|
|
129
|
+
rt_id,
|
|
130
|
+
str(entry.get("displayName", "?")),
|
|
131
|
+
str(entry.get("kind", "?")),
|
|
132
|
+
available,
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
console.print()
|
|
136
|
+
console.print(table)
|
dap_cli/commands/stop.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""dap stop — wysyła SIGTERM (potem SIGKILL) procesowi z PID file."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from rich.console import Console
|
|
6
|
+
|
|
7
|
+
from dap_cli.process import is_process_alive, read_pid_file, remove_pid_file, stop_process
|
|
8
|
+
|
|
9
|
+
console = Console()
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def stop_command() -> None:
|
|
13
|
+
pid_info = read_pid_file()
|
|
14
|
+
if pid_info is None:
|
|
15
|
+
console.print("[yellow]⚠ No PID file found — DAP is not running.[/yellow]")
|
|
16
|
+
raise SystemExit(1)
|
|
17
|
+
|
|
18
|
+
pid = pid_info["pid"]
|
|
19
|
+
port = pid_info["port"]
|
|
20
|
+
|
|
21
|
+
if not is_process_alive(pid):
|
|
22
|
+
console.print(
|
|
23
|
+
f"[yellow]⚠ Stale PID file (process {pid} not running) — cleaning up.[/yellow]",
|
|
24
|
+
)
|
|
25
|
+
remove_pid_file()
|
|
26
|
+
raise SystemExit(1)
|
|
27
|
+
|
|
28
|
+
console.print(f"[cyan]Stopping DAP (PID {pid}, port {port})...[/cyan]")
|
|
29
|
+
terminated = stop_process(pid)
|
|
30
|
+
remove_pid_file()
|
|
31
|
+
|
|
32
|
+
if terminated:
|
|
33
|
+
console.print("[green]✓ Stopped.[/green]")
|
|
34
|
+
else:
|
|
35
|
+
console.print("[yellow]⚠ Process already exited before SIGTERM.[/yellow]")
|