whycode-cli 0.2.5__tar.gz → 0.2.6__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 (30) hide show
  1. {whycode_cli-0.2.5/src/whycode_cli.egg-info → whycode_cli-0.2.6}/PKG-INFO +3 -2
  2. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/README.md +2 -1
  3. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/pyproject.toml +1 -1
  4. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/__init__.py +1 -1
  5. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/cli.py +126 -0
  6. {whycode_cli-0.2.5 → whycode_cli-0.2.6/src/whycode_cli.egg-info}/PKG-INFO +3 -2
  7. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/tests/test_cli.py +48 -0
  8. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/LICENSE +0 -0
  9. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/setup.cfg +0 -0
  10. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/__main__.py +0 -0
  11. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/git_facts.py +0 -0
  12. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/ignore.py +0 -0
  13. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/mcp_server.py +0 -0
  14. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/risk_card.py +0 -0
  15. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/scorer.py +0 -0
  16. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/signals.py +0 -0
  17. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/suppressions.py +0 -0
  18. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/templates/__init__.py +0 -0
  19. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/templates/github-workflow.yml +0 -0
  20. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode/templates/pre-commit +0 -0
  21. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/SOURCES.txt +0 -0
  22. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/dependency_links.txt +0 -0
  23. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/entry_points.txt +0 -0
  24. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/requires.txt +0 -0
  25. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/src/whycode_cli.egg-info/top_level.txt +0 -0
  26. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/tests/test_git_facts.py +0 -0
  27. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/tests/test_ignore.py +0 -0
  28. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/tests/test_scorer.py +0 -0
  29. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/tests/test_signals.py +0 -0
  30. {whycode_cli-0.2.5 → whycode_cli-0.2.6}/tests/test_suppressions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whycode-cli
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Tells you what to be afraid of before you touch a file.
5
5
  Author: Kevin
6
6
  License-Expression: MIT
@@ -87,8 +87,9 @@ Requires Python 3.11+.
87
87
  ```bash
88
88
  cd /path/to/your/repo
89
89
 
90
+ whycode tour # the one command to run first
90
91
  whycode init # one-command setup: CI workflow + pre-commit gate
91
- whycode highlights # first-run treasure map: top decisions + incidents
92
+ whycode highlights # repo-wide treasure map: top decisions + incidents
92
93
  whycode why src/some/file.py # the Risk Card for one file
93
94
  whycode why src/some/file.py -b # one-line summary (for triage / scripts)
94
95
  whycode why src/some/file.py --at <sha> # risk as of a past commit
@@ -59,8 +59,9 @@ Requires Python 3.11+.
59
59
  ```bash
60
60
  cd /path/to/your/repo
61
61
 
62
+ whycode tour # the one command to run first
62
63
  whycode init # one-command setup: CI workflow + pre-commit gate
63
- whycode highlights # first-run treasure map: top decisions + incidents
64
+ whycode highlights # repo-wide treasure map: top decisions + incidents
64
65
  whycode why src/some/file.py # the Risk Card for one file
65
66
  whycode why src/some/file.py -b # one-line summary (for triage / scripts)
66
67
  whycode why src/some/file.py --at <sha> # risk as of a past commit
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "whycode-cli"
7
- version = "0.2.5"
7
+ version = "0.2.6"
8
8
  description = "Tells you what to be afraid of before you touch a file."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -1,3 +1,3 @@
1
1
  """WhyCode — tells you what to be afraid of before touching a file."""
2
2
 
3
- __version__ = "0.2.5"
3
+ __version__ = "0.2.6"
@@ -2,6 +2,7 @@
2
2
 
3
3
  Commands
4
4
  --------
5
+ - ``whycode tour`` — first-run walkthrough: highlights + top risk + MCP setup.
5
6
  - ``whycode why <path>`` — print the Risk Card for a single file.
6
7
  - ``whycode why <path> --at SHA`` — risk card as of a past commit.
7
8
  - ``whycode why <path> --mute KIND`` — locally suppress a noisy signal kind.
@@ -845,6 +846,131 @@ def _install_template(
845
846
  return f"[green]wrote:[/green] {rel_label}"
846
847
 
847
848
 
849
+ _MCP_SNIPPET = ''' {
850
+ "mcpServers": {
851
+ "whycode": {"command": "whycode", "args": ["mcp"]}
852
+ }
853
+ }'''
854
+
855
+
856
+ @app.command()
857
+ def tour(
858
+ repo: Path = typer.Option(Path("."), "--repo", help="Path inside the repo."),
859
+ ) -> None:
860
+ """First-run walkthrough: highlights + top risky files + MCP setup snippet.
861
+
862
+ The single command to run after installing WhyCode. Skips straight to
863
+ the most concrete things in the repo (verbatim invariants and
864
+ incident-flagged commits) and ends with the one snippet you'll need to
865
+ wire WhyCode into an MCP-aware editor.
866
+ """
867
+ try:
868
+ repo_root = gf.discover_repo_root(repo.resolve())
869
+ except gf.GitError as exc:
870
+ err.print(f"[red]error:[/red] {exc}")
871
+ raise typer.Exit(2) from exc
872
+
873
+ console.print("[bold]Welcome to WhyCode.[/bold]")
874
+ console.print(f"[dim]Reading the history of {repo_root.name}…[/dim]\n")
875
+
876
+ # Section 1 — invariants and incidents (cheap; one git log call).
877
+ with console.status("Looking for stated decisions…", spinner="dots"):
878
+ commits = gf.all_commits(repo_root, max_count=2000)
879
+ if not commits:
880
+ console.print("[yellow]This repo has no commits yet — nothing to learn from.[/yellow]")
881
+ return
882
+
883
+ inv_pairs = gf.extract_invariant_quotes(commits)
884
+ sha_to_commit = {c.sha: c for c in commits}
885
+ seen_lines: dict[str, str] = {}
886
+ for sha, line in inv_pairs:
887
+ seen_lines.setdefault(line, sha)
888
+ invariants_top = [
889
+ (line, sha_to_commit[sha])
890
+ for line, sha in seen_lines.items()
891
+ if sha in sha_to_commit
892
+ ][:3]
893
+ incidents_top = gf.find_incidents(commits)[:3]
894
+
895
+ if invariants_top or incidents_top:
896
+ console.print("[bold yellow]Decisions and incidents[/bold yellow]")
897
+ for line, c in invariants_top:
898
+ console.print(f" [italic]{line}[/italic]")
899
+ console.print(
900
+ f" [dim]{c.sha[:7]} {c.authored_at.date()} {c.author_name}[/dim]\n"
901
+ )
902
+ for c in incidents_top:
903
+ subj = c.subject if len(c.subject) <= 70 else c.subject[:69] + "…"
904
+ console.print(f" [red]{subj}[/red]")
905
+ console.print(
906
+ f" [dim]{c.sha[:7]} {c.authored_at.date()} {c.author_name}[/dim]\n"
907
+ )
908
+ else:
909
+ console.print(
910
+ "[dim]No headline decisions or incidents in recent history.[/dim]"
911
+ )
912
+ console.print(
913
+ "[dim]Commit messages may be too terse — describing 'why' in commit "
914
+ "bodies (or using `hotfix:` / `BREAKING CHANGE:` prefixes) makes WhyCode "
915
+ "much more useful.[/dim]\n"
916
+ )
917
+
918
+ # Section 2 — top risky files. Slimmer scan: 100 files, depth 50 commits.
919
+ raw = gf.run_git(repo_root, "ls-files")
920
+ patterns = ign.effective_patterns(repo_root)
921
+ paths = [p for p in raw.splitlines() if p.strip() and not ign.is_ignored(p, patterns)][
922
+ :100
923
+ ]
924
+ cards: list[rc.RiskCard] = []
925
+ if paths:
926
+ with console.status(
927
+ f"Risk-ranking {len(paths)} files (slim scan)…", spinner="dots"
928
+ ):
929
+ for p in paths:
930
+ try:
931
+ card = rc.build(repo_root, p, max_commits=50)
932
+ except gf.GitError:
933
+ continue
934
+ useful = [s for s in card.signals if s.kind is not sig.SignalKind.NEWBORN]
935
+ if useful:
936
+ cards.append(card)
937
+ cards.sort(key=lambda c: -c.score.value)
938
+
939
+ if cards:
940
+ console.print("[bold red]Top 3 risky files[/bold red]")
941
+ for top in cards[:3]:
942
+ console.print(
943
+ f" [bold]{top.score.value:>3}[/bold] "
944
+ f"{top.score.band.value:<20} [cyan]{top.path}[/cyan]"
945
+ )
946
+ console.print(f" [dim]{top.signals[0].headline}[/dim]")
947
+ console.print()
948
+
949
+ # Section 3 — MCP setup snippet (vendor-neutral phrasing).
950
+ console.print("[bold magenta]Wire WhyCode into your AI editor[/bold magenta]")
951
+ console.print(
952
+ " WhyCode ships an MCP server. Any MCP-aware editor or assistant\n"
953
+ " can call it — just add this snippet to your editor's MCP config:\n"
954
+ )
955
+ console.print(_MCP_SNIPPET)
956
+ console.print(
957
+ "\n [dim](See your editor's docs for the exact config-file location.)[/dim]\n"
958
+ )
959
+
960
+ # Section 4 — what to do next.
961
+ console.print("[bold]Next:[/bold]")
962
+ if cards:
963
+ console.print(
964
+ f" [dim]·[/dim] [bold]whycode why {cards[0].path}[/bold] the full Risk Card"
965
+ )
966
+ console.print(
967
+ " [dim]·[/dim] [bold]whycode init[/bold] install CI + pre-commit"
968
+ )
969
+ console.print(
970
+ " [dim]·[/dim] [bold]whycode highlights[/bold] more invariants and incidents"
971
+ )
972
+
973
+
848
974
  @app.command()
849
975
  def init(
850
976
  force: bool = typer.Option(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: whycode-cli
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: Tells you what to be afraid of before you touch a file.
5
5
  Author: Kevin
6
6
  License-Expression: MIT
@@ -87,8 +87,9 @@ Requires Python 3.11+.
87
87
  ```bash
88
88
  cd /path/to/your/repo
89
89
 
90
+ whycode tour # the one command to run first
90
91
  whycode init # one-command setup: CI workflow + pre-commit gate
91
- whycode highlights # first-run treasure map: top decisions + incidents
92
+ whycode highlights # repo-wide treasure map: top decisions + incidents
92
93
  whycode why src/some/file.py # the Risk Card for one file
93
94
  whycode why src/some/file.py -b # one-line summary (for triage / scripts)
94
95
  whycode why src/some/file.py --at <sha> # risk as of a past commit
@@ -589,3 +589,51 @@ def test_scan_lists_top_files(repo, days_ago) -> None: # type: ignore[no-untype
589
589
  result = _invoke(repo.root, "scan", "--top", "3")
590
590
  assert result.exit_code == 0
591
591
  assert "a.py" in result.output
592
+
593
+
594
+ def test_tour_runs_and_emits_all_sections(repo, days_ago) -> None: # type: ignore[no-untyped-def]
595
+ repo.commit(
596
+ "compat: keep sync path",
597
+ {"a.py": "1"},
598
+ body="Do not switch to async — v1 clients break.",
599
+ when=days_ago(60),
600
+ )
601
+ repo.commit(
602
+ "hotfix: refund regression",
603
+ {"b.py": "1"},
604
+ body="See INC-447.",
605
+ when=days_ago(20),
606
+ )
607
+ sha = repo.commit("feat: A", {"a.py": "2"}, when=days_ago(40))
608
+ repo.revert(sha, when=days_ago(15))
609
+ result = _invoke(repo.root, "tour")
610
+ assert result.exit_code == 0
611
+ out = result.output
612
+ assert "Welcome to WhyCode" in out
613
+ assert "Decisions and incidents" in out
614
+ assert "Do not switch to async" in out
615
+ assert "hotfix: refund regression" in out
616
+ assert "Wire WhyCode into your AI editor" in out
617
+ # MCP snippet appears verbatim so users can copy-paste.
618
+ assert '"command": "whycode"' in out
619
+
620
+
621
+ def test_tour_quiet_repo_explains_why(repo) -> None: # type: ignore[no-untyped-def]
622
+ repo.commit("init", {"a.py": "1"})
623
+ result = _invoke(repo.root, "tour")
624
+ assert result.exit_code == 0
625
+ out = result.output
626
+ # MCP section appears regardless — most useful next step.
627
+ assert "Wire WhyCode into your AI editor" in out
628
+ # And the empty-state explanation should mention why nothing fires.
629
+ assert "terse" in out.lower() or "no headline" in out.lower()
630
+
631
+
632
+ def test_tour_outside_repo_errors(tmp_path) -> None: # type: ignore[no-untyped-def]
633
+ cwd = os.getcwd()
634
+ os.chdir(tmp_path)
635
+ try:
636
+ result = runner.invoke(app, ["tour"], catch_exceptions=False)
637
+ finally:
638
+ os.chdir(cwd)
639
+ assert result.exit_code != 0
File without changes
File without changes