nexus-dev-toolkit 3.1.2__tar.gz → 3.1.3__tar.gz

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.
Files changed (39) hide show
  1. {nexus_dev_toolkit-3.1.2/nexus_dev_toolkit.egg-info → nexus_dev_toolkit-3.1.3}/PKG-INFO +3 -1
  2. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/README.md +2 -0
  3. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/nexus_cli.py +223 -1
  4. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3/nexus_dev_toolkit.egg-info}/PKG-INFO +3 -1
  5. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/pyproject.toml +1 -1
  6. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tests/test_cli.py +73 -0
  7. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/LICENSE +0 -0
  8. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/nexus_dev_toolkit.egg-info/SOURCES.txt +0 -0
  9. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/nexus_dev_toolkit.egg-info/dependency_links.txt +0 -0
  10. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/nexus_dev_toolkit.egg-info/entry_points.txt +0 -0
  11. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/nexus_dev_toolkit.egg-info/requires.txt +0 -0
  12. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/nexus_dev_toolkit.egg-info/top_level.txt +0 -0
  13. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/nexus_server.py +0 -0
  14. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/setup.cfg +0 -0
  15. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tests/test_server.py +0 -0
  16. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/__init__.py +0 -0
  17. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/agents/__init__.py +0 -0
  18. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/agents/code-reviewer.md +0 -0
  19. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/agents/database-reviewer.md +0 -0
  20. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/agents/deployment-reviewer.md +0 -0
  21. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/agents/monitoring-reviewer.md +0 -0
  22. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/agents/performance-reviewer.md +0 -0
  23. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/__init__.py +0 -0
  24. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/arch_ingest.py +0 -0
  25. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/package_resolver.py +0 -0
  26. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/project_rules.py +0 -0
  27. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/__init__.py +0 -0
  28. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/apply.md +0 -0
  29. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/code-review.md +0 -0
  30. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/database-review.md +0 -0
  31. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/deployment-review.md +0 -0
  32. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/epav.md +0 -0
  33. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/evaluate.md +0 -0
  34. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/monitoring-review.md +0 -0
  35. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/performance-review.md +0 -0
  36. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/plan.md +0 -0
  37. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/scaffold.md +0 -0
  38. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/skills/validate.md +0 -0
  39. {nexus_dev_toolkit-3.1.2 → nexus_dev_toolkit-3.1.3}/tools/epav/task_loader.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nexus-dev-toolkit
3
- Version: 3.1.2
3
+ Version: 3.1.3
4
4
  Summary: Developer workflow toolkit for Claude Code — Day 0 scaffold + Day 1 EPAV
5
5
  Author-email: Ronald dela Cruz <rcdelacruz@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -172,6 +172,8 @@ nexus init . # Claude Code — set up .claude/ + knowledge/
172
172
  nexus init . --tool opencode # OpenCode — set up .opencode/ + knowledge/ + opencode.json
173
173
  nexus --version # show installed version
174
174
  nexus update # update to latest version
175
+ nexus sync # sync built-in skills & agents to latest (custom files untouched)
176
+ nexus doctor # validate project setup
175
177
 
176
178
  nexus skill list # list all skills in .claude/commands/
177
179
  nexus skill add my-review # create a custom skill
@@ -151,6 +151,8 @@ nexus init . # Claude Code — set up .claude/ + knowledge/
151
151
  nexus init . --tool opencode # OpenCode — set up .opencode/ + knowledge/ + opencode.json
152
152
  nexus --version # show installed version
153
153
  nexus update # update to latest version
154
+ nexus sync # sync built-in skills & agents to latest (custom files untouched)
155
+ nexus doctor # validate project setup
154
156
 
155
157
  nexus skill list # list all skills in .claude/commands/
156
158
  nexus skill add my-review # create a custom skill
@@ -18,7 +18,7 @@ app.add_typer(agent_app, name="agent")
18
18
 
19
19
  console = Console()
20
20
 
21
- _VERSION = "3.1.2"
21
+ _VERSION = "3.1.3"
22
22
 
23
23
  _LOGO = """\
24
24
  [cyan]███╗ ██╗███████╗██╗ ██╗██╗ ██╗███████╗[/cyan]
@@ -301,6 +301,11 @@ def _check_and_offer_install(tool: str) -> None:
301
301
  return
302
302
 
303
303
  console.print(f" [yellow]⚠[/yellow] {info['name']} not found — you need it to use nexus.\n")
304
+
305
+ if not sys.stdin.isatty():
306
+ console.print(f" [dim]Install manually: {info['install']}[/dim]\n")
307
+ return
308
+
304
309
  answer = console.input(f" Install {info['name']} now? [y/N] ").strip().lower()
305
310
  if answer != "y":
306
311
  console.print(f" [dim]Skipping. Install manually: {info['install']}[/dim]\n")
@@ -364,6 +369,223 @@ def update() -> None:
364
369
  console.print("\n [green]✓[/green] Done.\n")
365
370
 
366
371
 
372
+ @app.command()
373
+ def sync(
374
+ project_dir: str = typer.Argument(".", help="Project directory to sync"),
375
+ ) -> None:
376
+ """Sync built-in skills and agents to their latest versions (custom files untouched)."""
377
+ root = Path(project_dir).resolve()
378
+ has_claude = (root / ".claude").is_dir()
379
+ has_opencode = (root / ".opencode").is_dir()
380
+
381
+ if not has_claude and not has_opencode:
382
+ console.print(" [red]✗[/red] Not a nexus project. Run [cyan]nexus init[/cyan] first.\n")
383
+ raise typer.Exit(1)
384
+
385
+ console.print(f"\n [cyan]▶[/cyan] Syncing built-ins in [bold]{root}[/bold]\n")
386
+
387
+ table = Table(show_header=True, header_style="dim")
388
+ table.add_column("File")
389
+ table.add_column("Status")
390
+
391
+ if has_claude:
392
+ (root / ".claude" / "commands").mkdir(parents=True, exist_ok=True)
393
+ (root / ".claude" / "agents").mkdir(parents=True, exist_ok=True)
394
+
395
+ for fname in _BUILTIN_SKILLS:
396
+ src = _SKILLS_SRC / fname
397
+ if not src.exists():
398
+ continue
399
+ dst = root / ".claude" / "commands" / fname
400
+ src_text = src.read_text(encoding="utf-8")
401
+ if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
402
+ table.add_row(f".claude/commands/{fname}", "[dim green]ok[/dim green]")
403
+ else:
404
+ dst.write_text(src_text, encoding="utf-8")
405
+ table.add_row(f".claude/commands/{fname}", "[cyan]updated[/cyan]")
406
+
407
+ for fname in _BUILTIN_AGENTS:
408
+ src = _AGENTS_SRC / fname
409
+ if not src.exists():
410
+ continue
411
+ dst = root / ".claude" / "agents" / fname
412
+ src_text = src.read_text(encoding="utf-8")
413
+ if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
414
+ table.add_row(f".claude/agents/{fname}", "[dim green]ok[/dim green]")
415
+ else:
416
+ dst.write_text(src_text, encoding="utf-8")
417
+ table.add_row(f".claude/agents/{fname}", "[cyan]updated[/cyan]")
418
+
419
+ if has_opencode:
420
+ (root / ".opencode" / "commands").mkdir(parents=True, exist_ok=True)
421
+ (root / ".opencode" / "agents").mkdir(parents=True, exist_ok=True)
422
+ (root / ".opencode" / "plugins").mkdir(parents=True, exist_ok=True)
423
+
424
+ for fname in _BUILTIN_SKILLS:
425
+ src = _SKILLS_SRC / fname
426
+ if not src.exists():
427
+ continue
428
+ dst = root / ".opencode" / "commands" / fname
429
+ src_text = src.read_text(encoding="utf-8")
430
+ if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
431
+ table.add_row(f".opencode/commands/{fname}", "[dim green]ok[/dim green]")
432
+ else:
433
+ dst.write_text(src_text, encoding="utf-8")
434
+ table.add_row(f".opencode/commands/{fname}", "[cyan]updated[/cyan]")
435
+
436
+ for fname in _BUILTIN_AGENTS:
437
+ src = _AGENTS_SRC / fname
438
+ if not src.exists():
439
+ continue
440
+ dst = root / ".opencode" / "agents" / fname
441
+ src_text = _strip_claude_frontmatter(src.read_text(encoding="utf-8"))
442
+ if dst.exists() and dst.read_text(encoding="utf-8") == src_text:
443
+ table.add_row(f".opencode/agents/{fname}", "[dim green]ok[/dim green]")
444
+ else:
445
+ dst.write_text(src_text, encoding="utf-8")
446
+ table.add_row(f".opencode/agents/{fname}", "[cyan]updated[/cyan]")
447
+
448
+ plugin_dst = root / ".opencode" / "plugins" / "graphify.js"
449
+ if plugin_dst.exists() and plugin_dst.read_text(encoding="utf-8") == _OPENCODE_GRAPHIFY_PLUGIN:
450
+ table.add_row(".opencode/plugins/graphify.js", "[dim green]ok[/dim green]")
451
+ else:
452
+ plugin_dst.write_text(_OPENCODE_GRAPHIFY_PLUGIN, encoding="utf-8")
453
+ table.add_row(".opencode/plugins/graphify.js", "[cyan]updated[/cyan]")
454
+
455
+ console.print(table)
456
+ console.print()
457
+
458
+
459
+ @app.command()
460
+ def doctor(
461
+ project_dir: str = typer.Argument(".", help="Project directory to check"),
462
+ ) -> None:
463
+ """Validate the nexus project setup and show what's working or missing."""
464
+ root = Path(project_dir).resolve()
465
+
466
+ table = Table(show_header=True, header_style="dim", show_lines=False)
467
+ table.add_column("Check", style="dim white")
468
+ table.add_column("Status")
469
+
470
+ has_failure = False
471
+
472
+ def ok(msg: str) -> str:
473
+ return f"[green]✓[/green] {msg}"
474
+
475
+ def warn(msg: str) -> str:
476
+ return f"[yellow]⚠[/yellow] {msg}"
477
+
478
+ def fail(msg: str) -> str:
479
+ nonlocal has_failure
480
+ has_failure = True
481
+ return f"[red]✗[/red] {msg}"
482
+
483
+ # ── Tool ─────────────────────────────────────────────────────────────────
484
+ has_claude_bin = bool(shutil.which("claude"))
485
+ has_opencode_bin = bool(shutil.which("opencode"))
486
+ table.add_row("Claude Code", ok("installed") if has_claude_bin else warn("not found — install: npm install -g @anthropic-ai/claude-code"))
487
+ table.add_row("OpenCode", ok("installed") if has_opencode_bin else warn("not found — install: curl -fsSL https://opencode.ai/install | bash"))
488
+
489
+ # ── Project ──────────────────────────────────────────────────────────────
490
+ has_claude_dir = (root / ".claude").is_dir()
491
+ has_opencode_dir = (root / ".opencode").is_dir()
492
+ initialized = has_claude_dir or has_opencode_dir
493
+ table.add_row(
494
+ "nexus initialized",
495
+ ok(f"yes ({', '.join(filter(None, ['claude' if has_claude_dir else '', 'opencode' if has_opencode_dir else '']))})")
496
+ if initialized else fail("no — run nexus init")
497
+ )
498
+
499
+ # MCP config — Claude Code
500
+ if has_claude_dir:
501
+ mcp_path = root / ".mcp.json"
502
+ if not mcp_path.exists():
503
+ table.add_row(".mcp.json", fail("missing"))
504
+ else:
505
+ try:
506
+ mcp_data = json.loads(mcp_path.read_text())
507
+ if "nexus" in mcp_data.get("mcpServers", {}):
508
+ table.add_row(".mcp.json", ok("nexus entry present"))
509
+ else:
510
+ table.add_row(".mcp.json", warn("exists but no nexus entry"))
511
+ except Exception:
512
+ table.add_row(".mcp.json", warn("invalid JSON"))
513
+
514
+ # MCP config — OpenCode
515
+ if has_opencode_dir:
516
+ oc_json = root / "opencode.json"
517
+ if not oc_json.exists():
518
+ table.add_row("opencode.json", fail("missing"))
519
+ else:
520
+ try:
521
+ oc_data = json.loads(oc_json.read_text())
522
+ if "nexus-mcp" in oc_data.get("mcp", {}):
523
+ table.add_row("opencode.json", ok("nexus entry present"))
524
+ else:
525
+ table.add_row("opencode.json", warn("exists but no nexus-mcp entry"))
526
+ except Exception:
527
+ table.add_row("opencode.json", warn("invalid JSON"))
528
+
529
+ # ── Skills & Agents ──────────────────────────────────────────────────────
530
+ if has_claude_dir:
531
+ cmd_dir = root / ".claude" / "commands"
532
+ present = [f for f in _BUILTIN_SKILLS if (cmd_dir / f).exists()]
533
+ missing = len(_BUILTIN_SKILLS) - len(present)
534
+ if missing == 0:
535
+ table.add_row("Built-in skills (Claude)", ok(f"all {len(_BUILTIN_SKILLS)} present"))
536
+ else:
537
+ table.add_row("Built-in skills (Claude)", warn(f"{missing} missing — run nexus sync"))
538
+
539
+ ag_dir = root / ".claude" / "agents"
540
+ present_ag = [f for f in _BUILTIN_AGENTS if (ag_dir / f).exists()]
541
+ missing_ag = len(_BUILTIN_AGENTS) - len(present_ag)
542
+ if missing_ag == 0:
543
+ table.add_row("Built-in agents (Claude)", ok(f"all {len(_BUILTIN_AGENTS)} present"))
544
+ else:
545
+ table.add_row("Built-in agents (Claude)", warn(f"{missing_ag} missing — run nexus sync"))
546
+
547
+ if has_opencode_dir:
548
+ oc_cmd_dir = root / ".opencode" / "commands"
549
+ present_oc = [f for f in _BUILTIN_SKILLS if (oc_cmd_dir / f).exists()]
550
+ missing_oc = len(_BUILTIN_SKILLS) - len(present_oc)
551
+ if missing_oc == 0:
552
+ table.add_row("Built-in skills (OpenCode)", ok(f"all {len(_BUILTIN_SKILLS)} present"))
553
+ else:
554
+ table.add_row("Built-in skills (OpenCode)", warn(f"{missing_oc} missing — run nexus sync"))
555
+
556
+ oc_ag_dir = root / ".opencode" / "agents"
557
+ present_oc_ag = [f for f in _BUILTIN_AGENTS if (oc_ag_dir / f).exists()]
558
+ missing_oc_ag = len(_BUILTIN_AGENTS) - len(present_oc_ag)
559
+ if missing_oc_ag == 0:
560
+ table.add_row("Built-in agents (OpenCode)", ok(f"all {len(_BUILTIN_AGENTS)} present"))
561
+ else:
562
+ table.add_row("Built-in agents (OpenCode)", warn(f"{missing_oc_ag} missing — run nexus sync"))
563
+
564
+ # ── Knowledge ────────────────────────────────────────────────────────────
565
+ missing_dirs = [d for d in _KNOWLEDGE_DIRS if not (root / d).is_dir()]
566
+ if not missing_dirs:
567
+ table.add_row("Knowledge dirs", ok(f"all {len(_KNOWLEDGE_DIRS)} present"))
568
+ else:
569
+ table.add_row("Knowledge dirs", warn(f"missing: {', '.join(missing_dirs)}"))
570
+
571
+ # ── graphify ─────────────────────────────────────────────────────────────
572
+ has_graphify = bool(shutil.which("graphify"))
573
+ table.add_row("graphify", ok("installed") if has_graphify else warn("not installed — uv tool install graphifyy"))
574
+
575
+ graph_path = root / "graphify-out" / "graph.json"
576
+ if graph_path.exists():
577
+ table.add_row("Knowledge graph", ok("graphify-out/graph.json exists"))
578
+ else:
579
+ table.add_row("Knowledge graph", warn("not built — run: graphify ."))
580
+
581
+ console.print()
582
+ console.print(table)
583
+ console.print()
584
+
585
+ if has_failure:
586
+ raise typer.Exit(1)
587
+
588
+
367
589
  # ── skill subcommands ─────────────────────────────────────────────────────────
368
590
 
369
591
  _SKILL_TEMPLATE = """\
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: nexus-dev-toolkit
3
- Version: 3.1.2
3
+ Version: 3.1.3
4
4
  Summary: Developer workflow toolkit for Claude Code — Day 0 scaffold + Day 1 EPAV
5
5
  Author-email: Ronald dela Cruz <rcdelacruz@users.noreply.github.com>
6
6
  License-Expression: MIT
@@ -172,6 +172,8 @@ nexus init . # Claude Code — set up .claude/ + knowledge/
172
172
  nexus init . --tool opencode # OpenCode — set up .opencode/ + knowledge/ + opencode.json
173
173
  nexus --version # show installed version
174
174
  nexus update # update to latest version
175
+ nexus sync # sync built-in skills & agents to latest (custom files untouched)
176
+ nexus doctor # validate project setup
175
177
 
176
178
  nexus skill list # list all skills in .claude/commands/
177
179
  nexus skill add my-review # create a custom skill
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "nexus-dev-toolkit"
3
- version = "3.1.2"
3
+ version = "3.1.3"
4
4
  description = "Developer workflow toolkit for Claude Code — Day 0 scaffold + Day 1 EPAV"
5
5
  authors = [
6
6
  {name = "Ronald dela Cruz", email = "rcdelacruz@users.noreply.github.com"}
@@ -150,3 +150,76 @@ def test_init_opencode_idempotent(tmp_project):
150
150
  def test_init_unknown_tool(tmp_project):
151
151
  result = runner.invoke(app, ["init", str(tmp_project), "--tool", "cursor"])
152
152
  assert result.exit_code == 1
153
+
154
+
155
+ # ── sync ──────────────────────────────────────────────────────────────────────
156
+
157
+ def test_sync_not_nexus_project(tmp_project):
158
+ result = runner.invoke(app, ["sync", str(tmp_project)])
159
+ assert result.exit_code == 1
160
+ assert "Not a nexus project" in result.output
161
+
162
+
163
+ def test_sync_shows_ok_when_current(tmp_project):
164
+ runner.invoke(app, ["init", str(tmp_project)])
165
+ result = runner.invoke(app, ["sync", str(tmp_project)])
166
+ assert result.exit_code == 0
167
+ assert "ok" in result.output
168
+
169
+
170
+ def test_sync_updates_modified_builtin(tmp_project):
171
+ runner.invoke(app, ["init", str(tmp_project)])
172
+ skill_file = tmp_project / ".claude" / "commands" / "scaffold.md"
173
+ skill_file.write_text("# stale content")
174
+ result = runner.invoke(app, ["sync", str(tmp_project)])
175
+ assert result.exit_code == 0
176
+ assert "updated" in result.output
177
+ assert "stale content" not in skill_file.read_text()
178
+
179
+
180
+ def test_sync_skips_custom_files(tmp_project):
181
+ runner.invoke(app, ["init", str(tmp_project)])
182
+ custom = tmp_project / ".claude" / "commands" / "my-custom.md"
183
+ custom.write_text("# custom skill")
184
+ runner.invoke(app, ["sync", str(tmp_project)])
185
+ assert custom.exists()
186
+ assert custom.read_text() == "# custom skill"
187
+
188
+
189
+ def test_sync_opencode(tmp_project):
190
+ runner.invoke(app, ["init", str(tmp_project), "--tool", "opencode"])
191
+ result = runner.invoke(app, ["sync", str(tmp_project)])
192
+ assert result.exit_code == 0
193
+ assert ".opencode/commands" in result.output
194
+
195
+
196
+ # ── doctor ────────────────────────────────────────────────────────────────────
197
+
198
+ def test_doctor_not_initialized(tmp_project):
199
+ result = runner.invoke(app, ["doctor", str(tmp_project)])
200
+ assert result.exit_code == 1
201
+ assert "Not a nexus project" in result.output or "✗" in result.output
202
+
203
+
204
+ def test_doctor_healthy_project(tmp_project):
205
+ runner.invoke(app, ["init", str(tmp_project)])
206
+ result = runner.invoke(app, ["doctor", str(tmp_project)])
207
+ assert result.exit_code == 0
208
+ assert "✓" in result.output
209
+ assert "all 11 present" in result.output
210
+ assert "all 5 present" in result.output
211
+
212
+
213
+ def test_doctor_missing_skills(tmp_project):
214
+ runner.invoke(app, ["init", str(tmp_project)])
215
+ (tmp_project / ".claude" / "commands" / "scaffold.md").unlink()
216
+ result = runner.invoke(app, ["doctor", str(tmp_project)])
217
+ assert result.exit_code == 0
218
+ assert "missing" in result.output
219
+
220
+
221
+ def test_doctor_missing_graph(tmp_project):
222
+ runner.invoke(app, ["init", str(tmp_project)])
223
+ result = runner.invoke(app, ["doctor", str(tmp_project)])
224
+ assert result.exit_code == 0
225
+ assert "not built" in result.output