development-engine-vector 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.
- dev/__init__.py +3 -0
- dev/__main__.py +5 -0
- dev/cli/__init__.py +0 -0
- dev/cli/cli.py +674 -0
- dev/kernel/__init__.py +0 -0
- dev/kernel/config.py +46 -0
- dev/kernel/db.py +129 -0
- dev/kernel/paths.py +85 -0
- dev/kernel/pmf_kernel.py +432 -0
- dev/kernel/selfcheck.py +137 -0
- dev/ui/__init__.py +0 -0
- dev/ui/actions.py +47 -0
- dev/ui/db/__init__.py +26 -0
- dev/ui/db/base.py +75 -0
- dev/ui/db/checks.py +16 -0
- dev/ui/db/events.py +18 -0
- dev/ui/db/health.py +34 -0
- dev/ui/db/identity.py +12 -0
- dev/ui/db/manifest.py +35 -0
- dev/ui/db/overview.py +47 -0
- dev/ui/db/runs.py +47 -0
- dev/ui/routes.py +114 -0
- dev/ui/static/__init__.py +0 -0
- dev/ui/static/web.css +722 -0
- dev/ui/static/web.js +528 -0
- dev/ui/templates.py +231 -0
- dev/ui/web.py +28 -0
- dev/utils.py +93 -0
- dev/vcs/__init__.py +0 -0
- dev/vcs/git.py +107 -0
- dev/vcs/github.py +77 -0
- dev/workflow/__init__.py +0 -0
- dev/workflow/cda.py +143 -0
- dev/workflow/changelog.py +102 -0
- dev/workflow/preflight.py +307 -0
- dev/workflow/release.py +217 -0
- dev/workflow/versioning.py +87 -0
- development_engine_vector-0.3.0.dist-info/METADATA +252 -0
- development_engine_vector-0.3.0.dist-info/RECORD +41 -0
- development_engine_vector-0.3.0.dist-info/WHEEL +4 -0
- development_engine_vector-0.3.0.dist-info/entry_points.txt +2 -0
dev/__init__.py
ADDED
dev/__main__.py
ADDED
dev/cli/__init__.py
ADDED
|
File without changes
|
dev/cli/cli.py
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""dev.cli.cli — thin click interface for the Development Engine Vector."""
|
|
3
|
+
|
|
4
|
+
import json
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import click
|
|
10
|
+
|
|
11
|
+
from dev.kernel.config import DevConfig
|
|
12
|
+
from dev.kernel.paths import ensure_dirs
|
|
13
|
+
from dev.kernel.pmf_kernel import (
|
|
14
|
+
PMFKernel, PMFKernelError,
|
|
15
|
+
install_launchd, uninstall_launchd, plist_path,
|
|
16
|
+
wait_for_port_and_open_browser,
|
|
17
|
+
)
|
|
18
|
+
from dev.utils import green, header, error, info, success
|
|
19
|
+
from dev.workflow.changelog import ChangelogManager
|
|
20
|
+
from dev.workflow.preflight import PreflightChecks
|
|
21
|
+
from dev.workflow.release import ReleaseOrchestrator
|
|
22
|
+
from dev.workflow.versioning import VersionManager
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def resolve_project_dir(project):
|
|
26
|
+
if project:
|
|
27
|
+
return Path(project).resolve()
|
|
28
|
+
return Path.cwd().resolve()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def load_config(ctx, project_dir):
|
|
32
|
+
config_path = ctx.obj.get("config") if ctx.obj else None
|
|
33
|
+
return DevConfig(project_dir, config_path)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@click.group()
|
|
37
|
+
@click.option("--config", type=click.Path(dir_okay=False, exists=True), default=None,
|
|
38
|
+
help="Path to a dev-cli TOML config file.")
|
|
39
|
+
@click.version_option(package_name="development-engine-vector", prog_name="dev")
|
|
40
|
+
@click.pass_context
|
|
41
|
+
def cli(ctx, config):
|
|
42
|
+
"""Developer workflow coordination CLI."""
|
|
43
|
+
ensure_dirs()
|
|
44
|
+
ctx.ensure_object(dict)
|
|
45
|
+
ctx.obj["config"] = config
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@cli.command("pf")
|
|
49
|
+
@click.option("--project", default=".", help="Project root path")
|
|
50
|
+
@click.option("--full", is_flag=True, help="Run the full set of preflight checks.")
|
|
51
|
+
@click.option("--report", default=None, help="Write a JSON report to this file.")
|
|
52
|
+
@click.pass_context
|
|
53
|
+
def pf(ctx, project, full, report):
|
|
54
|
+
"""Run release preflight checks for a project."""
|
|
55
|
+
project_dir = resolve_project_dir(project)
|
|
56
|
+
header(f"Preflight checks for {project_dir}")
|
|
57
|
+
checks = PreflightChecks(project_dir)
|
|
58
|
+
checks.run_all(full=full, report_path=report)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@cli.command("preflight")
|
|
62
|
+
@click.option("--project", default=".", help="Project root path")
|
|
63
|
+
@click.option("--full", is_flag=True, help="Run the full set of preflight checks.")
|
|
64
|
+
@click.option("--report", default=None, help="Write a JSON report to this file.")
|
|
65
|
+
@click.pass_context
|
|
66
|
+
def preflight(ctx, project, full, report):
|
|
67
|
+
"""Alias for dev pf."""
|
|
68
|
+
ctx.invoke(pf, project=project, full=full, report=report)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@cli.group()
|
|
72
|
+
def version():
|
|
73
|
+
"""Version management commands."""
|
|
74
|
+
pass
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@version.command("show")
|
|
78
|
+
@click.option("--project", default=".", help="Project root path")
|
|
79
|
+
def version_show(project):
|
|
80
|
+
"""Show the current version."""
|
|
81
|
+
project_dir = resolve_project_dir(project)
|
|
82
|
+
vm = VersionManager(project_dir)
|
|
83
|
+
print(green(vm.read()))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@version.command("bump")
|
|
87
|
+
@click.option("--level", type=click.Choice(["major", "minor", "patch"]),
|
|
88
|
+
default="patch", show_default=True)
|
|
89
|
+
@click.option("--project", default=".", help="Project root path")
|
|
90
|
+
def version_bump(level, project):
|
|
91
|
+
"""Bump the project version."""
|
|
92
|
+
project_dir = resolve_project_dir(project)
|
|
93
|
+
vm = VersionManager(project_dir)
|
|
94
|
+
old_version = vm.read()
|
|
95
|
+
new_version = vm.bump(level)
|
|
96
|
+
vm.write(new_version)
|
|
97
|
+
info(f"Bumped version {old_version} → {new_version}")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@version.command("sync")
|
|
101
|
+
@click.option("--project", default=".", help="Project root path")
|
|
102
|
+
def version_sync(project):
|
|
103
|
+
"""Sync version into project files."""
|
|
104
|
+
project_dir = resolve_project_dir(project)
|
|
105
|
+
vm = VersionManager(project_dir)
|
|
106
|
+
ver = vm.read()
|
|
107
|
+
targets = vm.get_sync_targets()
|
|
108
|
+
vm.sync_to_files(ver, targets)
|
|
109
|
+
success(f"Synced {ver} to project files.")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
@cli.command("build")
|
|
113
|
+
@click.option("--project", default=".", help="Project root path")
|
|
114
|
+
def build(project):
|
|
115
|
+
"""Build source and wheel distributions for a project."""
|
|
116
|
+
project_dir = resolve_project_dir(project)
|
|
117
|
+
header(f"Build {project_dir}")
|
|
118
|
+
try:
|
|
119
|
+
subprocess.run(
|
|
120
|
+
["python", "-m", "build", "--sdist", "--wheel"],
|
|
121
|
+
cwd=project_dir,
|
|
122
|
+
check=True,
|
|
123
|
+
)
|
|
124
|
+
success("Build complete.")
|
|
125
|
+
except subprocess.CalledProcessError as exc:
|
|
126
|
+
error(f"Build failed: {exc}")
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@cli.command("release")
|
|
130
|
+
@click.option("--project", default=".", help="Project root path")
|
|
131
|
+
@click.option("--bump", type=click.Choice(["major", "minor", "patch"]),
|
|
132
|
+
default=None, help="Version bump level")
|
|
133
|
+
@click.option("--version", default=None, help="Custom version to release")
|
|
134
|
+
@click.option("--skip-build/--no-skip-build", default=None, help="Skip or run build step")
|
|
135
|
+
@click.option("--skip-publish/--no-skip-publish", default=None, help="Skip or run publish step")
|
|
136
|
+
@click.option("--dry-run/--no-dry-run", default=None, help="Run release without committing or publishing")
|
|
137
|
+
@click.pass_context
|
|
138
|
+
def release(ctx, project, bump, version, skip_build, skip_publish, dry_run):
|
|
139
|
+
"""Run the full release workflow."""
|
|
140
|
+
project_dir = resolve_project_dir(project)
|
|
141
|
+
config = load_config(ctx, project_dir)
|
|
142
|
+
|
|
143
|
+
bump = bump or config.get("release", "default_bump", "patch")
|
|
144
|
+
skip_build = skip_build if skip_build is not None else config.get_bool("release", "skip_build", False)
|
|
145
|
+
skip_publish = skip_publish if skip_publish is not None else config.get_bool("release", "skip_publish", False)
|
|
146
|
+
dry_run = dry_run if dry_run is not None else config.get_bool("release", "dry_run", False)
|
|
147
|
+
|
|
148
|
+
orch = ReleaseOrchestrator(project_dir)
|
|
149
|
+
orch.full_release(level=bump, version=version, skip_build=skip_build,
|
|
150
|
+
skip_publish=skip_publish, dry_run=dry_run)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
@cli.command("sync")
|
|
154
|
+
@click.option("--project", default=".", help="Project root path")
|
|
155
|
+
@click.option("--install-editable/--no-install-editable", default=None,
|
|
156
|
+
help="Install or skip editable project install")
|
|
157
|
+
@click.option("--install-dependencies/--no-install-dependencies", default=None,
|
|
158
|
+
help="Install or skip requirements.txt")
|
|
159
|
+
@click.option("--install-dev/--no-install-dev", default=None,
|
|
160
|
+
help="Install or skip dev-requirements.txt")
|
|
161
|
+
@click.pass_context
|
|
162
|
+
def sync(ctx, project, install_editable, install_dependencies, install_dev):
|
|
163
|
+
"""Bootstrap or sync project dependencies."""
|
|
164
|
+
project_dir = resolve_project_dir(project)
|
|
165
|
+
config = load_config(ctx, project_dir)
|
|
166
|
+
|
|
167
|
+
install_editable = install_editable if install_editable is not None else config.get_bool("bootstrap", "install_editable", True)
|
|
168
|
+
install_dependencies = install_dependencies if install_dependencies is not None else config.get_bool("bootstrap", "install_dependencies", True)
|
|
169
|
+
install_dev = install_dev if install_dev is not None else config.get_bool("bootstrap", "install_dev_dependencies", False)
|
|
170
|
+
|
|
171
|
+
orch = ReleaseOrchestrator(project_dir)
|
|
172
|
+
orch.bootstrap(install_editable=install_editable,
|
|
173
|
+
install_dependencies=install_dependencies,
|
|
174
|
+
install_dev_dependencies=install_dev)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@cli.command("check")
|
|
178
|
+
@click.option("--project", default=".", help="Project root path")
|
|
179
|
+
@click.option("--compile/--no-compile", "compile_check", default=None,
|
|
180
|
+
help="Run or skip Python compile check")
|
|
181
|
+
@click.option("--tests/--no-tests", default=None, help="Run or skip pytest")
|
|
182
|
+
@click.option("--lint/--no-lint", default=None, help="Run or skip lint checks")
|
|
183
|
+
@click.pass_context
|
|
184
|
+
def check(ctx, project, compile_check, tests, lint):
|
|
185
|
+
"""Run project health checks."""
|
|
186
|
+
project_dir = resolve_project_dir(project)
|
|
187
|
+
config = load_config(ctx, project_dir)
|
|
188
|
+
|
|
189
|
+
compile_check = compile_check if compile_check is not None else config.get_bool("check", "compile", True)
|
|
190
|
+
tests = tests if tests is not None else config.get_bool("check", "tests", False)
|
|
191
|
+
lint = lint if lint is not None else config.get_bool("check", "lint", False)
|
|
192
|
+
|
|
193
|
+
orch = ReleaseOrchestrator(project_dir)
|
|
194
|
+
orch.preflight()
|
|
195
|
+
if compile_check:
|
|
196
|
+
orch.run_compile()
|
|
197
|
+
if lint:
|
|
198
|
+
orch.run_lint()
|
|
199
|
+
if tests:
|
|
200
|
+
orch.run_tests()
|
|
201
|
+
success("Health checks complete")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@cli.group()
|
|
205
|
+
def config():
|
|
206
|
+
"""Show or initialize dev-cli configuration."""
|
|
207
|
+
pass
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
@config.command("show")
|
|
211
|
+
@click.option("--project", default=".", help="Project root path")
|
|
212
|
+
@click.pass_context
|
|
213
|
+
def config_show(ctx, project):
|
|
214
|
+
"""Show current config."""
|
|
215
|
+
project_dir = resolve_project_dir(project)
|
|
216
|
+
cfg = load_config(ctx, project_dir)
|
|
217
|
+
|
|
218
|
+
if cfg.config_path is None:
|
|
219
|
+
info("No dev-cli config found.")
|
|
220
|
+
return
|
|
221
|
+
|
|
222
|
+
info(f"Config file: {cfg.config_path}")
|
|
223
|
+
print(json.dumps(cfg.data, indent=2))
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
@cli.command("workflow")
|
|
227
|
+
@click.argument("name")
|
|
228
|
+
@click.option("--project", default=".", help="Project root path")
|
|
229
|
+
def workflow(name, project):
|
|
230
|
+
"""Run a custom saved workflow."""
|
|
231
|
+
project_dir = resolve_project_dir(project)
|
|
232
|
+
known = {
|
|
233
|
+
"release": ["release"],
|
|
234
|
+
"build": ["build"],
|
|
235
|
+
"preflight": ["preflight"],
|
|
236
|
+
"bootstrap": ["sync"],
|
|
237
|
+
"check": ["check"],
|
|
238
|
+
}
|
|
239
|
+
if name not in known:
|
|
240
|
+
error(f"Unknown workflow: {name}")
|
|
241
|
+
|
|
242
|
+
command = [sys.executable, str(Path(__file__).resolve())] + known[name]
|
|
243
|
+
info(f"Running workflow: {name}")
|
|
244
|
+
subprocess.run(command, cwd=project_dir, check=True)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
@cli.command("changelog")
|
|
248
|
+
@click.option("--project", default=".", help="Project root path")
|
|
249
|
+
@click.option("--version", default=None, help="Version header to read")
|
|
250
|
+
def changelog(project, version):
|
|
251
|
+
"""Inspect changelog entries."""
|
|
252
|
+
project_dir = resolve_project_dir(project)
|
|
253
|
+
cm = ChangelogManager(project_dir)
|
|
254
|
+
if version:
|
|
255
|
+
entries = cm.extract_entries(version)
|
|
256
|
+
print(entries)
|
|
257
|
+
else:
|
|
258
|
+
print(cm.read())
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
@cli.command("selfcheck")
|
|
262
|
+
def selfcheck():
|
|
263
|
+
"""Run engine selfcheck diagnostics."""
|
|
264
|
+
from dev.kernel.selfcheck import run_all
|
|
265
|
+
result = run_all()
|
|
266
|
+
for c in result["checks"]:
|
|
267
|
+
status = green("PASS") if c["passed"] else "\033[31mFAIL\033[0m"
|
|
268
|
+
print(f" [{status}] {c['name']}: {c['message']}")
|
|
269
|
+
if c.get("details"):
|
|
270
|
+
print(f" {c['details']}")
|
|
271
|
+
print()
|
|
272
|
+
print(f" {result['passed']} passed, {result['failed']} failed")
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
_kernel = PMFKernel()
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
# ── helper ────────────────────────────────────────────────────────────────────
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _bold(s):
|
|
282
|
+
return f"\033[1m{s}\033[0m"
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def _dim(s):
|
|
286
|
+
return f"\033[2m{s}\033[0m"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _yellow(s):
|
|
290
|
+
return f"\033[33m{s}\033[0m"
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _red(s):
|
|
294
|
+
return f"\033[31m{s}\033[0m"
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def _hr():
|
|
298
|
+
return "─" * 52
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def _pmf_warn_if_not_installed():
|
|
302
|
+
if not plist_path().exists():
|
|
303
|
+
click.echo(_yellow(" ⚠ LaunchAgent not installed — run `dev pmf install` to auto-start on login."))
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
# ── ui group ─────────────────────────────────────────────────────────────────
|
|
307
|
+
|
|
308
|
+
@cli.group()
|
|
309
|
+
def ui():
|
|
310
|
+
"""Manage the embedded web UI (port 9001)."""
|
|
311
|
+
pass
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
@ui.command("start")
|
|
315
|
+
@click.option("--host", default="127.0.0.1", show_default=True, help="Host to bind")
|
|
316
|
+
@click.option("--port", default=9001, show_default=True, help="Port for the web UI")
|
|
317
|
+
@click.option("--no-browser", "no_browser", is_flag=True, default=False,
|
|
318
|
+
help="Don't open browser automatically")
|
|
319
|
+
def ui_start(host, port, no_browser):
|
|
320
|
+
"""Start the web UI via PMF."""
|
|
321
|
+
_pmf_warn_if_not_installed()
|
|
322
|
+
try:
|
|
323
|
+
result = _kernel.start_service("ui", options={"host": host, "port": port})
|
|
324
|
+
click.echo(green(f" Started {result['label']} pid={result['pid']}"))
|
|
325
|
+
if not no_browser:
|
|
326
|
+
url = f"http://{host}:{port}"
|
|
327
|
+
click.echo(_dim(" Opening browser when server is ready..."))
|
|
328
|
+
wait_for_port_and_open_browser(url, host, port)
|
|
329
|
+
except PMFKernelError as exc:
|
|
330
|
+
click.echo(_red(f" {exc}"))
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
@ui.command("stop")
|
|
334
|
+
def ui_stop():
|
|
335
|
+
"""Stop the web UI."""
|
|
336
|
+
try:
|
|
337
|
+
result = _kernel.stop_service("ui")
|
|
338
|
+
click.echo(green(f" Stopped {result['label']}"))
|
|
339
|
+
except PMFKernelError as exc:
|
|
340
|
+
click.echo(_red(f" {exc}"))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
@ui.command("status")
|
|
344
|
+
def ui_status():
|
|
345
|
+
"""Show web UI status."""
|
|
346
|
+
svc = _kernel.service_status("ui")
|
|
347
|
+
status_str = green(svc["status"]) if svc["status"] == "running" else _yellow(svc["status"])
|
|
348
|
+
click.echo()
|
|
349
|
+
click.echo(_bold(f" {svc['label']}"))
|
|
350
|
+
click.echo(f" Status: {status_str}")
|
|
351
|
+
click.echo(f" PID: {svc['pid'] or '—'}")
|
|
352
|
+
click.echo(f" Started: {svc['started_at'] or '—'}")
|
|
353
|
+
click.echo(f" Log: {svc['log_file'] or '—'}")
|
|
354
|
+
click.echo()
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
@ui.command("restart")
|
|
358
|
+
@click.option("--host", default="127.0.0.1", show_default=True)
|
|
359
|
+
@click.option("--port", default=9001, show_default=True)
|
|
360
|
+
@click.option("--no-browser", "no_browser", is_flag=True, default=False)
|
|
361
|
+
def ui_restart(host, port, no_browser):
|
|
362
|
+
"""Restart the web UI."""
|
|
363
|
+
try:
|
|
364
|
+
result = _kernel.restart_service("ui", options={"host": host, "port": port})
|
|
365
|
+
click.echo(green(f" Restarted {result['label']} pid={result['pid']}"))
|
|
366
|
+
if not no_browser:
|
|
367
|
+
url = f"http://{host}:{port}"
|
|
368
|
+
click.echo(_dim(" Opening browser when server is ready..."))
|
|
369
|
+
wait_for_port_and_open_browser(url, host, port)
|
|
370
|
+
except PMFKernelError as exc:
|
|
371
|
+
click.echo(_red(f" {exc}"))
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
# ── pmf group ─────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
@cli.group()
|
|
377
|
+
def pmf():
|
|
378
|
+
"""Manage the embedded PMF kernel and dev services."""
|
|
379
|
+
pass
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
@pmf.command("services")
|
|
383
|
+
def pmf_services():
|
|
384
|
+
"""List PMF services and runtime status."""
|
|
385
|
+
rows = _kernel.services()
|
|
386
|
+
click.echo()
|
|
387
|
+
click.echo(_bold(" PMF Runtime Services"))
|
|
388
|
+
click.echo(_hr())
|
|
389
|
+
for svc in rows:
|
|
390
|
+
status_str = green(svc["status"]) if svc["status"] == "running" else _yellow(svc["status"])
|
|
391
|
+
click.echo(f" {_bold(svc['label']):<20} {status_str:<10} pid={svc['pid'] or '—'}")
|
|
392
|
+
click.echo(f" {svc['description']}")
|
|
393
|
+
click.echo()
|
|
394
|
+
|
|
395
|
+
|
|
396
|
+
@pmf.command("status")
|
|
397
|
+
@click.argument("service_id", required=False)
|
|
398
|
+
def pmf_status(service_id):
|
|
399
|
+
"""Show PMF runtime status for one or all services."""
|
|
400
|
+
if service_id:
|
|
401
|
+
try:
|
|
402
|
+
svc = _kernel.service_status(service_id)
|
|
403
|
+
click.echo()
|
|
404
|
+
click.echo(_bold(f" {svc['label']}"))
|
|
405
|
+
click.echo(f" Status: {svc['status']}")
|
|
406
|
+
click.echo(f" PID: {svc['pid'] or '—'}")
|
|
407
|
+
click.echo(f" Started: {svc['started_at'] or '—'}")
|
|
408
|
+
click.echo(f" Log: {svc['log_file'] or '—'}")
|
|
409
|
+
click.echo()
|
|
410
|
+
except PMFKernelError as exc:
|
|
411
|
+
click.echo(_red(f" {exc}"))
|
|
412
|
+
else:
|
|
413
|
+
ctx = click.get_current_context()
|
|
414
|
+
ctx.invoke(pmf_services)
|
|
415
|
+
|
|
416
|
+
|
|
417
|
+
@pmf.command("start")
|
|
418
|
+
@click.argument("service_id")
|
|
419
|
+
@click.option("--host", default="127.0.0.1", help="Host override for UI service")
|
|
420
|
+
@click.option("--port", default=9001, help="Port override for UI service")
|
|
421
|
+
@click.option("--no-browser", "no_browser", is_flag=True, default=False)
|
|
422
|
+
def pmf_start(service_id, host, port, no_browser):
|
|
423
|
+
"""Start a PMF-managed service."""
|
|
424
|
+
options = {"host": host, "port": port} if service_id == "ui" else None
|
|
425
|
+
try:
|
|
426
|
+
result = _kernel.start_service(service_id, options=options)
|
|
427
|
+
click.echo(green(f" Started {result['label']} pid={result['pid']}"))
|
|
428
|
+
if service_id == "ui" and not no_browser:
|
|
429
|
+
url = f"http://{host}:{port}"
|
|
430
|
+
click.echo(_dim(" Opening browser when server is ready..."))
|
|
431
|
+
wait_for_port_and_open_browser(url, host, port)
|
|
432
|
+
except PMFKernelError as exc:
|
|
433
|
+
click.echo(_red(f" {exc}"))
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
@pmf.command("stop")
|
|
437
|
+
@click.argument("service_id")
|
|
438
|
+
def pmf_stop(service_id):
|
|
439
|
+
"""Stop a PMF-managed service."""
|
|
440
|
+
try:
|
|
441
|
+
result = _kernel.stop_service(service_id)
|
|
442
|
+
click.echo(green(f" Stopped {result['label']}"))
|
|
443
|
+
except PMFKernelError as exc:
|
|
444
|
+
click.echo(_red(f" {exc}"))
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
@pmf.command("restart")
|
|
448
|
+
@click.argument("service_id")
|
|
449
|
+
def pmf_restart(service_id):
|
|
450
|
+
"""Restart a PMF-managed service."""
|
|
451
|
+
try:
|
|
452
|
+
result = _kernel.restart_service(service_id)
|
|
453
|
+
click.echo(green(f" Restarted {result['label']} pid={result['pid']}"))
|
|
454
|
+
except PMFKernelError as exc:
|
|
455
|
+
click.echo(_red(f" {exc}"))
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
@pmf.command("logs")
|
|
459
|
+
@click.argument("service_id")
|
|
460
|
+
@click.option("--tail", default=50, show_default=True, help="Lines to tail")
|
|
461
|
+
def pmf_logs(service_id, tail):
|
|
462
|
+
"""Display the last lines from a PMF service log."""
|
|
463
|
+
try:
|
|
464
|
+
output = _kernel.tail_log(service_id, lines=tail)
|
|
465
|
+
click.echo(output)
|
|
466
|
+
except PMFKernelError as exc:
|
|
467
|
+
click.echo(_red(f" {exc}"))
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
@pmf.command("up")
|
|
471
|
+
@click.option("--host", default="127.0.0.1", show_default=True)
|
|
472
|
+
@click.option("--port", default=9001, show_default=True)
|
|
473
|
+
@click.option("--no-browser", "no_browser", is_flag=True, default=False)
|
|
474
|
+
def pmf_up(host, port, no_browser):
|
|
475
|
+
"""Start all dev services (web UI). Called automatically by launchd on login."""
|
|
476
|
+
url = f"http://{host}:{port}"
|
|
477
|
+
click.echo(_bold(" Development Engine Vector — starting services"))
|
|
478
|
+
click.echo(_hr())
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
result = _kernel.start_service("ui", options={"host": host, "port": port})
|
|
482
|
+
click.echo(green(f" Web UI started pid={result['pid']} → {url}"))
|
|
483
|
+
if not no_browser:
|
|
484
|
+
click.echo(_dim(" Opening browser when server is ready..."))
|
|
485
|
+
wait_for_port_and_open_browser(url, host, port)
|
|
486
|
+
except PMFKernelError as exc:
|
|
487
|
+
click.echo(_yellow(f" Web UI {exc}"))
|
|
488
|
+
|
|
489
|
+
click.echo()
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
@pmf.command("install")
|
|
493
|
+
def pmf_install():
|
|
494
|
+
"""Install dev as a macOS launchd LaunchAgent (auto-start on login)."""
|
|
495
|
+
from dev.kernel.paths import DEV_HOME as _dev_home
|
|
496
|
+
click.echo()
|
|
497
|
+
click.echo(_bold(" Installing dev LaunchAgent"))
|
|
498
|
+
click.echo(_hr())
|
|
499
|
+
try:
|
|
500
|
+
target = install_launchd(_dev_home)
|
|
501
|
+
click.echo(green(f" Plist: {target}"))
|
|
502
|
+
click.echo(green(" Label: com.gocosmix.dev"))
|
|
503
|
+
click.echo(green(" Loaded: yes — dev will start automatically on next login"))
|
|
504
|
+
click.echo()
|
|
505
|
+
click.echo(_dim(" To start services now without logging out:"))
|
|
506
|
+
click.echo(_dim(" dev pmf up"))
|
|
507
|
+
click.echo()
|
|
508
|
+
except PMFKernelError as exc:
|
|
509
|
+
click.echo(_red(f" {exc}"))
|
|
510
|
+
click.echo(_yellow(" Make sure `dev` is on PATH: export PATH=\"$HOME/Library/Python/3.9/bin:$PATH\""))
|
|
511
|
+
click.echo()
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
@pmf.command("uninstall")
|
|
515
|
+
def pmf_uninstall():
|
|
516
|
+
"""Remove the dev launchd LaunchAgent."""
|
|
517
|
+
target = plist_path()
|
|
518
|
+
if not target.exists():
|
|
519
|
+
click.echo(_yellow(" No LaunchAgent plist found — nothing to uninstall."))
|
|
520
|
+
return
|
|
521
|
+
uninstall_launchd()
|
|
522
|
+
click.echo(green(f" Removed: {target}"))
|
|
523
|
+
click.echo(green(" dev will no longer start automatically on login."))
|
|
524
|
+
|
|
525
|
+
|
|
526
|
+
# ── setup ─────────────────────────────────────────────────────────────────────
|
|
527
|
+
|
|
528
|
+
@cli.command("setup")
|
|
529
|
+
@click.option("--no-browser", "no_browser", is_flag=True, default=False,
|
|
530
|
+
help="Don't open browser when the web UI starts")
|
|
531
|
+
def setup(no_browser):
|
|
532
|
+
"""
|
|
533
|
+
Full onboarding in three steps: init → pmf install → up.
|
|
534
|
+
|
|
535
|
+
\b
|
|
536
|
+
Run this once after `pip install dev-cli`.
|
|
537
|
+
If `dev` isn't on PATH yet, use the fallback:
|
|
538
|
+
|
|
539
|
+
python3 -m dev setup
|
|
540
|
+
|
|
541
|
+
\b
|
|
542
|
+
What each step does:
|
|
543
|
+
1. Init — create ~/Library/goCosmix/tools/dev/, patch PATH
|
|
544
|
+
2. Install — register a macOS LaunchAgent so dev starts on every login
|
|
545
|
+
3. Up — start the web UI via PMF, open browser
|
|
546
|
+
|
|
547
|
+
All processes are managed by the PMF kernel. The LaunchAgent calls
|
|
548
|
+
`dev pmf up` on every login — no manual interaction needed after setup.
|
|
549
|
+
"""
|
|
550
|
+
import os as _os
|
|
551
|
+
import shutil as _shutil
|
|
552
|
+
from dev.kernel.paths import (
|
|
553
|
+
DEV_HOME, DATA_DIR, RUN_DIR, LOG_DIR,
|
|
554
|
+
CONFIG_DIR, PMF_DIR, PMF_LOG_DIR,
|
|
555
|
+
GOCOSMIX_HOME, GOCOSMIX_APPS, GOCOSMIX_TOOLS, GOCOSMIX_SYSTEM,
|
|
556
|
+
)
|
|
557
|
+
|
|
558
|
+
W = 52
|
|
559
|
+
BAR = "═" * W
|
|
560
|
+
bar = "─" * W
|
|
561
|
+
host, port = "127.0.0.1", 9001
|
|
562
|
+
url = f"http://{host}:{port}"
|
|
563
|
+
|
|
564
|
+
click.echo()
|
|
565
|
+
click.echo(_bold(BAR))
|
|
566
|
+
click.echo(_bold(" Development Engine Vector — setup"))
|
|
567
|
+
click.echo(_bold(BAR))
|
|
568
|
+
click.echo()
|
|
569
|
+
click.echo(_dim(" Three steps to a fully operational dev installation:"))
|
|
570
|
+
click.echo(_dim(f" 1. Init — create {DEV_HOME}"))
|
|
571
|
+
click.echo(_dim(" 2. Install — register macOS LaunchAgent (auto-start on login)"))
|
|
572
|
+
click.echo(_dim(" 3. Up — start web UI via PMF, open browser"))
|
|
573
|
+
click.echo()
|
|
574
|
+
|
|
575
|
+
# ── Step 1: Init ─────────────────────────────────────────────
|
|
576
|
+
click.echo(_bold(bar))
|
|
577
|
+
click.echo(_bold(" Step 1/3 — Init"))
|
|
578
|
+
click.echo(_bold(bar))
|
|
579
|
+
click.echo()
|
|
580
|
+
|
|
581
|
+
dirs = [GOCOSMIX_HOME, GOCOSMIX_APPS, GOCOSMIX_TOOLS, GOCOSMIX_SYSTEM,
|
|
582
|
+
DATA_DIR, RUN_DIR, LOG_DIR, CONFIG_DIR, PMF_DIR, PMF_LOG_DIR]
|
|
583
|
+
for d in dirs:
|
|
584
|
+
existed = d.exists()
|
|
585
|
+
d.mkdir(parents=True, exist_ok=True)
|
|
586
|
+
marker = green("✓") if existed else green("+")
|
|
587
|
+
click.echo(f" {marker} {d}")
|
|
588
|
+
|
|
589
|
+
click.echo()
|
|
590
|
+
click.echo(f" {green('✓')} DEV_HOME: {DEV_HOME}")
|
|
591
|
+
click.echo()
|
|
592
|
+
|
|
593
|
+
# PATH patch
|
|
594
|
+
dev_bin = _shutil.which("dev")
|
|
595
|
+
dev_bin_dir = None
|
|
596
|
+
if dev_bin:
|
|
597
|
+
dev_bin_dir = str(Path(dev_bin).parent)
|
|
598
|
+
else:
|
|
599
|
+
py_bin_dir = Path(sys.executable).parent
|
|
600
|
+
candidate = py_bin_dir / "dev"
|
|
601
|
+
if candidate.exists():
|
|
602
|
+
dev_bin_dir = str(py_bin_dir)
|
|
603
|
+
|
|
604
|
+
if dev_bin_dir and dev_bin_dir not in _os.environ.get("PATH", "").split(":"):
|
|
605
|
+
export_line = f'export PATH="{dev_bin_dir}:$PATH"'
|
|
606
|
+
zprofile = Path.home() / ".zprofile"
|
|
607
|
+
existing = zprofile.read_text() if zprofile.exists() else ""
|
|
608
|
+
if export_line not in existing:
|
|
609
|
+
with open(zprofile, "a") as f:
|
|
610
|
+
f.write(f"\n# goCosmix — added by dev setup\n{export_line}\n")
|
|
611
|
+
click.echo(f" {green('+')} PATH updated in ~/.zprofile")
|
|
612
|
+
click.echo(_yellow(" Run `source ~/.zprofile` or open a new terminal to activate."))
|
|
613
|
+
click.echo(f" {green('✓')} dev binary: {dev_bin_dir}/dev")
|
|
614
|
+
elif dev_bin:
|
|
615
|
+
click.echo(f" {green('✓')} dev binary on PATH: {dev_bin}")
|
|
616
|
+
|
|
617
|
+
click.echo()
|
|
618
|
+
|
|
619
|
+
# ── Step 2: PMF install ──────────────────────────────────────
|
|
620
|
+
click.echo(_bold(bar))
|
|
621
|
+
click.echo(_bold(" Step 2/3 — PMF install"))
|
|
622
|
+
click.echo(_bold(bar))
|
|
623
|
+
click.echo()
|
|
624
|
+
click.echo(_dim(" The LaunchAgent registers dev with macOS launchd. On every login,"))
|
|
625
|
+
click.echo(_dim(" launchd calls `dev pmf up` — starts the web UI via PMF kernel."))
|
|
626
|
+
click.echo(_dim(" No terminal required after this."))
|
|
627
|
+
click.echo()
|
|
628
|
+
|
|
629
|
+
pmf_ok = False
|
|
630
|
+
try:
|
|
631
|
+
target = install_launchd(DEV_HOME)
|
|
632
|
+
click.echo(f" {green('✓')} LaunchAgent: {target}")
|
|
633
|
+
click.echo(f" {green('✓')} Loaded — dev starts automatically on every login")
|
|
634
|
+
pmf_ok = True
|
|
635
|
+
except PMFKernelError as exc:
|
|
636
|
+
click.echo(f" {_yellow('⚠')} LaunchAgent registration failed: {exc}")
|
|
637
|
+
click.echo(_yellow(" Fix PATH then run `dev pmf install` to retry."))
|
|
638
|
+
click.echo()
|
|
639
|
+
|
|
640
|
+
# ── Step 3: Up ───────────────────────────────────────────────
|
|
641
|
+
click.echo(_bold(bar))
|
|
642
|
+
click.echo(_bold(" Step 3/3 — Up"))
|
|
643
|
+
click.echo(_bold(bar))
|
|
644
|
+
click.echo()
|
|
645
|
+
click.echo(_dim(f" Starting web UI on {url}"))
|
|
646
|
+
click.echo()
|
|
647
|
+
|
|
648
|
+
try:
|
|
649
|
+
result = _kernel.start_service("ui", options={"host": host, "port": port})
|
|
650
|
+
click.echo(f" {green('✓')} Web UI started pid={result['pid']} → {url}")
|
|
651
|
+
if not no_browser:
|
|
652
|
+
click.echo(_dim(" Opening browser when server is ready..."))
|
|
653
|
+
wait_for_port_and_open_browser(url, host, port)
|
|
654
|
+
except PMFKernelError as exc:
|
|
655
|
+
click.echo(f" {_yellow('⚠')} Web UI: {exc}")
|
|
656
|
+
click.echo(_dim(" Try `dev ui start` once PMF is installed."))
|
|
657
|
+
|
|
658
|
+
click.echo()
|
|
659
|
+
click.echo(_bold(BAR))
|
|
660
|
+
click.echo(_bold(" Setup complete"))
|
|
661
|
+
click.echo(_bold(BAR))
|
|
662
|
+
click.echo()
|
|
663
|
+
if pmf_ok:
|
|
664
|
+
click.echo(_dim(" dev will start automatically on your next login."))
|
|
665
|
+
click.echo(_dim(f" Dashboard: {url}"))
|
|
666
|
+
click.echo()
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
def main():
|
|
670
|
+
cli()
|
|
671
|
+
|
|
672
|
+
|
|
673
|
+
if __name__ == "__main__":
|
|
674
|
+
main()
|
dev/kernel/__init__.py
ADDED
|
File without changes
|