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 ADDED
@@ -0,0 +1,3 @@
1
+ """dev — Development Engine Vector."""
2
+
3
+ __version__ = "0.3.0"
dev/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow `python -m dev` execution."""
2
+ from dev.cli.cli import cli
3
+
4
+ if __name__ == "__main__":
5
+ cli()
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