zima-blue-cli 0.1.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.
- zima/__init__.py +3 -0
- zima/cli.py +471 -0
- zima/commands/__init__.py +1 -0
- zima/commands/agent.py +450 -0
- zima/commands/env.py +563 -0
- zima/commands/pjob.py +1045 -0
- zima/commands/pmg.py +505 -0
- zima/commands/schedule.py +248 -0
- zima/commands/variable.py +483 -0
- zima/commands/workflow.py +757 -0
- zima/config/__init__.py +5 -0
- zima/config/manager.py +274 -0
- zima/core/__init__.py +7 -0
- zima/core/claude_runner.py +316 -0
- zima/core/daemon.py +106 -0
- zima/core/daemon_scheduler.py +240 -0
- zima/core/kimi_runner.py +249 -0
- zima/core/runner.py +75 -0
- zima/core/scheduler.py +352 -0
- zima/core/state_manager.py +131 -0
- zima/daemon_runner.py +69 -0
- zima/execution/__init__.py +12 -0
- zima/execution/background_runner.py +71 -0
- zima/execution/executor.py +641 -0
- zima/execution/history.py +276 -0
- zima/models/__init__.py +52 -0
- zima/models/agent.py +516 -0
- zima/models/base.py +219 -0
- zima/models/config_bundle.py +321 -0
- zima/models/env.py +612 -0
- zima/models/pjob.py +428 -0
- zima/models/pmg.py +604 -0
- zima/models/schedule.py +197 -0
- zima/models/variable.py +238 -0
- zima/models/workflow.py +552 -0
- zima/templates/__init__.py +4 -0
- zima/templates/examples.py +190 -0
- zima/utils.py +277 -0
- zima_blue_cli-0.1.0.dist-info/METADATA +270 -0
- zima_blue_cli-0.1.0.dist-info/RECORD +43 -0
- zima_blue_cli-0.1.0.dist-info/WHEEL +4 -0
- zima_blue_cli-0.1.0.dist-info/entry_points.txt +2 -0
- zima_blue_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
zima/__init__.py
ADDED
zima/cli.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
"""ZimaBlue CLI - v2 Simplified"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
# Fix Windows UTF-8 encoding issue
|
|
12
|
+
from zima.utils import get_zima_home, setup_windows_utf8
|
|
13
|
+
|
|
14
|
+
setup_windows_utf8()
|
|
15
|
+
|
|
16
|
+
import typer
|
|
17
|
+
from rich.console import Console
|
|
18
|
+
from rich.table import Table
|
|
19
|
+
|
|
20
|
+
from zima.commands import agent as agent_cmd
|
|
21
|
+
from zima.commands import env as env_cmd
|
|
22
|
+
from zima.commands import pjob as pjob_cmd
|
|
23
|
+
from zima.commands import pmg as pmg_cmd
|
|
24
|
+
from zima.commands import schedule as schedule_cmd
|
|
25
|
+
from zima.commands import variable as variable_cmd
|
|
26
|
+
from zima.commands import workflow as workflow_cmd
|
|
27
|
+
from zima.config.manager import ConfigManager
|
|
28
|
+
from zima.core import AgentRunner
|
|
29
|
+
from zima.models import AgentConfig
|
|
30
|
+
from zima.models.schedule import ScheduleConfig
|
|
31
|
+
|
|
32
|
+
app = typer.Typer(
|
|
33
|
+
name="zima",
|
|
34
|
+
help="ZimaBlue CLI - Agent Runner",
|
|
35
|
+
add_completion=False,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Register subcommands
|
|
39
|
+
app.add_typer(agent_cmd.app, name="agent")
|
|
40
|
+
app.add_typer(workflow_cmd.app, name="workflow")
|
|
41
|
+
app.add_typer(variable_cmd.app, name="variable")
|
|
42
|
+
app.add_typer(env_cmd.app, name="env")
|
|
43
|
+
app.add_typer(pmg_cmd.app, name="pmg")
|
|
44
|
+
app.add_typer(pjob_cmd.app, name="pjob")
|
|
45
|
+
app.add_typer(schedule_cmd.app, name="schedule")
|
|
46
|
+
console = Console(legacy_windows=False, force_terminal=True)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def get_agents_dir() -> Path:
|
|
50
|
+
"""Get agents directory - global in ~/.zima/agents"""
|
|
51
|
+
zima_home = os.environ.get("ZIMA_HOME")
|
|
52
|
+
if zima_home:
|
|
53
|
+
agents_dir = Path(zima_home) / "agents"
|
|
54
|
+
else:
|
|
55
|
+
agents_dir = Path.home() / ".zima" / "agents"
|
|
56
|
+
|
|
57
|
+
agents_dir.mkdir(parents=True, exist_ok=True)
|
|
58
|
+
return agents_dir
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@app.callback()
|
|
62
|
+
def main():
|
|
63
|
+
"""ZimaBlue CLI - Agent Runner"""
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
@app.command()
|
|
68
|
+
def create(
|
|
69
|
+
name: str = typer.Argument(..., help="Agent name"),
|
|
70
|
+
workspace: Optional[Path] = typer.Option(None, "--workspace", "-w", help="Workspace directory"),
|
|
71
|
+
prompt: Optional[Path] = typer.Option(None, "--prompt", "-p", help="Prompt file"),
|
|
72
|
+
):
|
|
73
|
+
"""Create a new agent"""
|
|
74
|
+
agents_dir = get_agents_dir()
|
|
75
|
+
agent_dir = agents_dir / name
|
|
76
|
+
|
|
77
|
+
if agent_dir.exists():
|
|
78
|
+
console.print(f"[red]✗ Agent '{name}' already exists[/red]")
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
|
|
81
|
+
# Create directories
|
|
82
|
+
agent_dir.mkdir(parents=True)
|
|
83
|
+
ws_dir = agent_dir / "workspace"
|
|
84
|
+
ws_dir.mkdir()
|
|
85
|
+
(agent_dir / "logs").mkdir()
|
|
86
|
+
|
|
87
|
+
# Create default agent.yaml
|
|
88
|
+
prompt_file = prompt.name if prompt else "prompt.md"
|
|
89
|
+
agent_yaml = agent_dir / "agent.yaml"
|
|
90
|
+
agent_yaml.write_text(
|
|
91
|
+
f"""metadata:
|
|
92
|
+
name: {name}
|
|
93
|
+
description: Auto-generated agent
|
|
94
|
+
|
|
95
|
+
spec:
|
|
96
|
+
workspace: ./workspace
|
|
97
|
+
prompt:
|
|
98
|
+
file: {prompt_file}
|
|
99
|
+
vars: {{}}
|
|
100
|
+
execution:
|
|
101
|
+
maxTime: 900
|
|
102
|
+
maxStepsPerTurn: 50
|
|
103
|
+
maxRalphIterations: 10
|
|
104
|
+
""",
|
|
105
|
+
encoding="utf-8",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
# Copy prompt file if provided
|
|
109
|
+
if prompt and prompt.exists():
|
|
110
|
+
import shutil
|
|
111
|
+
|
|
112
|
+
shutil.copy(prompt, agent_dir / prompt_file)
|
|
113
|
+
else:
|
|
114
|
+
# Create default prompt
|
|
115
|
+
(agent_dir / "prompt.md").write_text(f"# {name}\n\nYour task here.\n", encoding="utf-8")
|
|
116
|
+
|
|
117
|
+
console.print(f"[green]✓ Created agent '{name}'[/green]")
|
|
118
|
+
console.print(f" Directory: {agent_dir}")
|
|
119
|
+
console.print(f" Workspace: {ws_dir}")
|
|
120
|
+
console.print(f"\nEdit {agent_dir / 'agent.yaml'} to configure")
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@app.command()
|
|
124
|
+
def run(
|
|
125
|
+
name: str = typer.Argument(..., help="Agent name"),
|
|
126
|
+
timeout: Optional[int] = typer.Option(
|
|
127
|
+
None, "--timeout", "-t", help="Max execution time (seconds)"
|
|
128
|
+
),
|
|
129
|
+
):
|
|
130
|
+
"""Run an agent once"""
|
|
131
|
+
agents_dir = get_agents_dir()
|
|
132
|
+
agent_dir = agents_dir / name
|
|
133
|
+
|
|
134
|
+
if not agent_dir.exists():
|
|
135
|
+
console.print(f"[red]✗ Agent '{name}' not found[/red]")
|
|
136
|
+
raise typer.Exit(1)
|
|
137
|
+
|
|
138
|
+
# Load config
|
|
139
|
+
config = AgentConfig.from_yaml(agent_dir / "agent.yaml")
|
|
140
|
+
|
|
141
|
+
# Override timeout if provided
|
|
142
|
+
if timeout:
|
|
143
|
+
config.max_execution_time = timeout
|
|
144
|
+
|
|
145
|
+
# Run
|
|
146
|
+
runner = AgentRunner(config, agent_dir)
|
|
147
|
+
result = runner.run()
|
|
148
|
+
|
|
149
|
+
# Show result
|
|
150
|
+
console.print("\n[bold]Result:[/bold]")
|
|
151
|
+
console.print(f" Status: {result.status}")
|
|
152
|
+
console.print(f" Time: {result.elapsed_time:.1f}s")
|
|
153
|
+
console.print(f" Summary: {result.summary}")
|
|
154
|
+
|
|
155
|
+
if result.return_code != 0:
|
|
156
|
+
raise typer.Exit(1)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@app.command()
|
|
160
|
+
def list(
|
|
161
|
+
verbose: bool = typer.Option(False, "--verbose", "-v"),
|
|
162
|
+
):
|
|
163
|
+
"""List all agents"""
|
|
164
|
+
agents_dir = get_agents_dir()
|
|
165
|
+
|
|
166
|
+
if not agents_dir.exists():
|
|
167
|
+
console.print("[yellow]No agents found. Create one with: zima create <name>[/yellow]")
|
|
168
|
+
return
|
|
169
|
+
|
|
170
|
+
agent_dirs = [d for d in agents_dir.iterdir() if d.is_dir()]
|
|
171
|
+
|
|
172
|
+
if not agent_dirs:
|
|
173
|
+
console.print("[yellow]No agents found. Create one with: zima create <name>[/yellow]")
|
|
174
|
+
return
|
|
175
|
+
|
|
176
|
+
table = Table(title="Agents")
|
|
177
|
+
table.add_column("Name", style="cyan")
|
|
178
|
+
table.add_column("Description", style="green")
|
|
179
|
+
|
|
180
|
+
for agent_dir in sorted(agent_dirs):
|
|
181
|
+
name = agent_dir.name
|
|
182
|
+
try:
|
|
183
|
+
config = AgentConfig.from_yaml(agent_dir / "agent.yaml")
|
|
184
|
+
desc = (
|
|
185
|
+
config.description[:50] + "..."
|
|
186
|
+
if len(config.description) > 50
|
|
187
|
+
else config.description
|
|
188
|
+
)
|
|
189
|
+
except Exception:
|
|
190
|
+
desc = "-"
|
|
191
|
+
|
|
192
|
+
table.add_row(name, desc)
|
|
193
|
+
|
|
194
|
+
console.print(table)
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
@app.command()
|
|
198
|
+
def show(
|
|
199
|
+
name: str = typer.Argument(..., help="Agent name"),
|
|
200
|
+
):
|
|
201
|
+
"""Show agent configuration"""
|
|
202
|
+
agents_dir = get_agents_dir()
|
|
203
|
+
agent_dir = agents_dir / name
|
|
204
|
+
|
|
205
|
+
if not agent_dir.exists():
|
|
206
|
+
console.print(f"[red]✗ Agent '{name}' not found[/red]")
|
|
207
|
+
raise typer.Exit(1)
|
|
208
|
+
|
|
209
|
+
config = AgentConfig.from_yaml(agent_dir / "agent.yaml")
|
|
210
|
+
|
|
211
|
+
table = Table(title=f"Agent: {name}")
|
|
212
|
+
table.add_column("Property", style="cyan")
|
|
213
|
+
table.add_column("Value", style="green")
|
|
214
|
+
|
|
215
|
+
table.add_row("Name", config.name)
|
|
216
|
+
table.add_row("Description", config.description)
|
|
217
|
+
table.add_row("Workspace", str(config.workspace))
|
|
218
|
+
table.add_row("Prompt File", config.prompt_file)
|
|
219
|
+
table.add_row("Max Time", f"{config.max_execution_time}s")
|
|
220
|
+
table.add_row("Max Steps/Turn", str(config.max_steps_per_turn))
|
|
221
|
+
table.add_row("Max Ralph Iter", str(config.max_ralph_iterations))
|
|
222
|
+
|
|
223
|
+
console.print(table)
|
|
224
|
+
|
|
225
|
+
if config.prompt_vars:
|
|
226
|
+
console.print("\n[bold]Prompt Variables:[/bold]")
|
|
227
|
+
for k, v in config.prompt_vars.items():
|
|
228
|
+
console.print(f" {k}: {v}")
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
@app.command()
|
|
232
|
+
def logs(
|
|
233
|
+
name: str = typer.Argument(..., help="Agent name"),
|
|
234
|
+
n: int = typer.Option(20, "--lines", "-n", help="Number of lines"),
|
|
235
|
+
):
|
|
236
|
+
"""Show agent logs"""
|
|
237
|
+
agents_dir = get_agents_dir()
|
|
238
|
+
agent_dir = agents_dir / name
|
|
239
|
+
|
|
240
|
+
if not agent_dir.exists():
|
|
241
|
+
console.print(f"[red]✗ Agent '{name}' not found[/red]")
|
|
242
|
+
raise typer.Exit(1)
|
|
243
|
+
|
|
244
|
+
logs_dir = agent_dir / "logs"
|
|
245
|
+
if not logs_dir.exists():
|
|
246
|
+
console.print("[yellow]No logs found[/yellow]")
|
|
247
|
+
return
|
|
248
|
+
|
|
249
|
+
# Get latest log
|
|
250
|
+
log_files = sorted(logs_dir.glob("*.log"), reverse=True)
|
|
251
|
+
if not log_files:
|
|
252
|
+
console.print("[yellow]No logs found[/yellow]")
|
|
253
|
+
return
|
|
254
|
+
|
|
255
|
+
latest = log_files[0]
|
|
256
|
+
console.print(f"[bold]Latest log ({latest.name}):[/bold]\n")
|
|
257
|
+
|
|
258
|
+
try:
|
|
259
|
+
content = latest.read_text(encoding="utf-8")
|
|
260
|
+
lines = content.split("\n")
|
|
261
|
+
for line in lines[-n:]:
|
|
262
|
+
console.print(line)
|
|
263
|
+
except Exception as e:
|
|
264
|
+
console.print(f"[red]Error reading log: {e}[/red]")
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# ---------------------------------------------------------------------------
|
|
268
|
+
# Daemon commands
|
|
269
|
+
# ---------------------------------------------------------------------------
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
@app.command()
|
|
273
|
+
def daemon_start(
|
|
274
|
+
schedule: str = typer.Option(..., "--schedule", "-s", help="Schedule code"),
|
|
275
|
+
):
|
|
276
|
+
"""Start the global daemon"""
|
|
277
|
+
daemon_dir = get_zima_home() / "daemon"
|
|
278
|
+
pid_file = daemon_dir / "daemon.pid"
|
|
279
|
+
|
|
280
|
+
if pid_file.exists():
|
|
281
|
+
try:
|
|
282
|
+
pid = int(pid_file.read_text(encoding="utf-8").strip())
|
|
283
|
+
# Check if process is alive
|
|
284
|
+
import ctypes
|
|
285
|
+
|
|
286
|
+
kernel32 = ctypes.windll.kernel32
|
|
287
|
+
handle = kernel32.OpenProcess(1, False, pid)
|
|
288
|
+
if handle:
|
|
289
|
+
kernel32.CloseHandle(handle)
|
|
290
|
+
console.print(f"[yellow]⚠[/yellow] Daemon already running (PID {pid})")
|
|
291
|
+
raise typer.Exit(1)
|
|
292
|
+
except Exception:
|
|
293
|
+
pass
|
|
294
|
+
pid_file.unlink(missing_ok=True)
|
|
295
|
+
|
|
296
|
+
manager = ConfigManager()
|
|
297
|
+
if not manager.config_exists("schedule", schedule):
|
|
298
|
+
console.print(f"[red]✗[/red] Schedule '{schedule}' not found")
|
|
299
|
+
raise typer.Exit(1)
|
|
300
|
+
|
|
301
|
+
data = manager.load_config("schedule", schedule)
|
|
302
|
+
cfg = ScheduleConfig.from_dict(data)
|
|
303
|
+
errors = cfg.validate(resolve_refs=True)
|
|
304
|
+
if errors:
|
|
305
|
+
console.print("[red]✗[/red] Schedule validation failed:")
|
|
306
|
+
for e in errors:
|
|
307
|
+
console.print(f" [red]•[/red] {e}")
|
|
308
|
+
raise typer.Exit(1)
|
|
309
|
+
|
|
310
|
+
log_file = daemon_dir / "daemon.log"
|
|
311
|
+
daemon_dir.mkdir(parents=True, exist_ok=True)
|
|
312
|
+
|
|
313
|
+
cmd = [
|
|
314
|
+
sys.executable,
|
|
315
|
+
"-m",
|
|
316
|
+
"zima.daemon_runner",
|
|
317
|
+
"--schedule",
|
|
318
|
+
schedule,
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
log_fh = open(log_file, "w", encoding="utf-8")
|
|
322
|
+
try:
|
|
323
|
+
if sys.platform == "win32":
|
|
324
|
+
proc = subprocess.Popen(
|
|
325
|
+
cmd,
|
|
326
|
+
stdout=log_fh,
|
|
327
|
+
stderr=subprocess.STDOUT,
|
|
328
|
+
creationflags=subprocess.CREATE_NEW_PROCESS_GROUP | subprocess.CREATE_NO_WINDOW,
|
|
329
|
+
close_fds=True,
|
|
330
|
+
)
|
|
331
|
+
else:
|
|
332
|
+
proc = subprocess.Popen(
|
|
333
|
+
cmd,
|
|
334
|
+
stdout=log_fh,
|
|
335
|
+
stderr=subprocess.STDOUT,
|
|
336
|
+
start_new_session=True,
|
|
337
|
+
close_fds=True,
|
|
338
|
+
)
|
|
339
|
+
except Exception as e:
|
|
340
|
+
log_fh.close()
|
|
341
|
+
console.print(f"[red]✗[/red] Failed to start daemon: {e}")
|
|
342
|
+
raise typer.Exit(1)
|
|
343
|
+
# Detach file handle — daemon process owns it now
|
|
344
|
+
log_fh.close()
|
|
345
|
+
|
|
346
|
+
# Brief check that child didn't exit immediately (e.g. validation failure)
|
|
347
|
+
import time
|
|
348
|
+
|
|
349
|
+
time.sleep(0.5)
|
|
350
|
+
if proc.poll() is not None:
|
|
351
|
+
pid_file.unlink(missing_ok=True)
|
|
352
|
+
console.print(f"[red]✗[/red] Daemon exited immediately (code {proc.returncode})")
|
|
353
|
+
console.print(f" Check log: {log_file}")
|
|
354
|
+
raise typer.Exit(1)
|
|
355
|
+
|
|
356
|
+
pid_file.write_text(str(proc.pid), encoding="utf-8")
|
|
357
|
+
console.print(f"[green]✓[/green] Daemon started (PID {proc.pid})")
|
|
358
|
+
console.print(f" Schedule: {schedule}")
|
|
359
|
+
console.print(f" Log: {log_file}")
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@app.command()
|
|
363
|
+
def daemon_stop():
|
|
364
|
+
"""Stop the global daemon"""
|
|
365
|
+
daemon_dir = get_zima_home() / "daemon"
|
|
366
|
+
pid_file = daemon_dir / "daemon.pid"
|
|
367
|
+
|
|
368
|
+
if not pid_file.exists():
|
|
369
|
+
console.print("[yellow]⚠[/yellow] Daemon is not running")
|
|
370
|
+
raise typer.Exit(0)
|
|
371
|
+
|
|
372
|
+
try:
|
|
373
|
+
pid = int(pid_file.read_text(encoding="utf-8").strip())
|
|
374
|
+
if sys.platform == "win32":
|
|
375
|
+
# Try graceful shutdown first, then force after 5s
|
|
376
|
+
# /T kills the entire process tree (PJobs spawned with CREATE_NEW_PROCESS_GROUP)
|
|
377
|
+
subprocess.run(["taskkill", "/PID", str(pid), "/T"], check=False)
|
|
378
|
+
import time
|
|
379
|
+
|
|
380
|
+
time.sleep(5)
|
|
381
|
+
# Force kill if still alive
|
|
382
|
+
try:
|
|
383
|
+
import ctypes
|
|
384
|
+
|
|
385
|
+
kernel32 = ctypes.windll.kernel32
|
|
386
|
+
handle = kernel32.OpenProcess(1, False, pid)
|
|
387
|
+
if handle:
|
|
388
|
+
kernel32.CloseHandle(handle)
|
|
389
|
+
subprocess.run(["taskkill", "/PID", str(pid), "/T", "/F"], check=False)
|
|
390
|
+
except Exception:
|
|
391
|
+
pass
|
|
392
|
+
else:
|
|
393
|
+
import os
|
|
394
|
+
import signal
|
|
395
|
+
|
|
396
|
+
os.kill(pid, signal.SIGTERM)
|
|
397
|
+
pid_file.unlink(missing_ok=True)
|
|
398
|
+
console.print(f"[green]✓[/green] Daemon stopped (PID {pid})")
|
|
399
|
+
except Exception as e:
|
|
400
|
+
console.print(f"[red]✗[/red] Failed to stop daemon: {e}")
|
|
401
|
+
raise typer.Exit(1)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
@app.command()
|
|
405
|
+
def daemon_status():
|
|
406
|
+
"""Show daemon status"""
|
|
407
|
+
daemon_dir = get_zima_home() / "daemon"
|
|
408
|
+
pid_file = daemon_dir / "daemon.pid"
|
|
409
|
+
state_file = daemon_dir / "state.json"
|
|
410
|
+
|
|
411
|
+
if not pid_file.exists():
|
|
412
|
+
console.print("[yellow]Daemon is not running[/yellow]")
|
|
413
|
+
raise typer.Exit(0)
|
|
414
|
+
|
|
415
|
+
try:
|
|
416
|
+
pid = int(pid_file.read_text(encoding="utf-8").strip())
|
|
417
|
+
except ValueError:
|
|
418
|
+
console.print("[red]Invalid PID file[/red]")
|
|
419
|
+
raise typer.Exit(1)
|
|
420
|
+
|
|
421
|
+
# Check if alive
|
|
422
|
+
alive = False
|
|
423
|
+
if sys.platform == "win32":
|
|
424
|
+
import ctypes
|
|
425
|
+
|
|
426
|
+
kernel32 = ctypes.windll.kernel32
|
|
427
|
+
handle = kernel32.OpenProcess(1, False, pid)
|
|
428
|
+
if handle:
|
|
429
|
+
kernel32.CloseHandle(handle)
|
|
430
|
+
alive = True
|
|
431
|
+
else:
|
|
432
|
+
import os
|
|
433
|
+
|
|
434
|
+
try:
|
|
435
|
+
os.kill(pid, 0)
|
|
436
|
+
alive = True
|
|
437
|
+
except OSError:
|
|
438
|
+
pass
|
|
439
|
+
|
|
440
|
+
if not alive:
|
|
441
|
+
console.print(f"[yellow]Daemon PID {pid} is not alive[/yellow]")
|
|
442
|
+
raise typer.Exit(0)
|
|
443
|
+
|
|
444
|
+
console.print(f"[green]Daemon is running[/green] (PID {pid})")
|
|
445
|
+
|
|
446
|
+
if state_file.exists():
|
|
447
|
+
import json
|
|
448
|
+
|
|
449
|
+
state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
450
|
+
console.print(f" Current cycle: {state.get('currentCycle', 'unknown')}")
|
|
451
|
+
console.print(f" Current stage: {state.get('currentStage', 'unknown')}")
|
|
452
|
+
console.print(f" Active PJobs: {state.get('activePjobs', [])}")
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
@app.command()
|
|
456
|
+
def daemon_logs(
|
|
457
|
+
tail: int = typer.Option(20, "--tail", "-n", help="Number of lines"),
|
|
458
|
+
):
|
|
459
|
+
"""Show daemon logs"""
|
|
460
|
+
log_file = get_zima_home() / "daemon" / "daemon.log"
|
|
461
|
+
if not log_file.exists():
|
|
462
|
+
console.print("[yellow]No daemon logs found[/yellow]")
|
|
463
|
+
raise typer.Exit(0)
|
|
464
|
+
|
|
465
|
+
lines = log_file.read_text(encoding="utf-8").splitlines()
|
|
466
|
+
for line in lines[-tail:]:
|
|
467
|
+
console.print(line)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
if __name__ == "__main__":
|
|
471
|
+
app()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""CLI commands package."""
|