goodvibes-cli 1.6.1__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 (122) hide show
  1. goodvibes_cli-1.6.1/.gitignore +8 -0
  2. goodvibes_cli-1.6.1/PKG-INFO +37 -0
  3. goodvibes_cli-1.6.1/README.md +8 -0
  4. goodvibes_cli-1.6.1/hatch_build.py +43 -0
  5. goodvibes_cli-1.6.1/pyproject.toml +56 -0
  6. goodvibes_cli-1.6.1/src/goodvibes_cli/__init__.py +1 -0
  7. goodvibes_cli-1.6.1/src/goodvibes_cli/__main__.py +15 -0
  8. goodvibes_cli-1.6.1/src/goodvibes_cli/commands/__init__.py +0 -0
  9. goodvibes_cli-1.6.1/src/goodvibes_cli/commands/doctor_cmd.py +110 -0
  10. goodvibes_cli-1.6.1/src/goodvibes_cli/commands/init_cmd.py +102 -0
  11. goodvibes_cli-1.6.1/src/goodvibes_cli/commands/upgrade_cmd.py +229 -0
  12. goodvibes_cli-1.6.1/src/goodvibes_cli/main.py +32 -0
  13. goodvibes_cli-1.6.1/src/goodvibes_cli/steps/__init__.py +0 -0
  14. goodvibes_cli-1.6.1/src/goodvibes_cli/steps/configure_mcp.py +91 -0
  15. goodvibes_cli-1.6.1/src/goodvibes_cli/steps/copy_templates.py +128 -0
  16. goodvibes_cli-1.6.1/src/goodvibes_cli/steps/install_headroom.py +58 -0
  17. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.amazonq/rules/goodvibes.md +28 -0
  18. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.bolt/prompt +11 -0
  19. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/cavecrew/README.md +41 -0
  20. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/cavecrew/SKILL.md +82 -0
  21. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/caveman/README.md +48 -0
  22. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/caveman/SKILL.md +78 -0
  23. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/caveman-commit/SKILL.md +65 -0
  24. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/caveman-compress/SKILL.md +111 -0
  25. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/caveman-help/SKILL.md +63 -0
  26. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/caveman-review/SKILL.md +55 -0
  27. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/caveman-stats/SKILL.md +10 -0
  28. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.claude/skills/goodvibes-hygiene/SKILL.md +49 -0
  29. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.clinerules/goodvibes.md +28 -0
  30. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.continue/rules/goodvibes.md +28 -0
  31. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.cursor/rules/goodvibes.mdc +28 -0
  32. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.devin/rules/goodvibes.md +28 -0
  33. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/ISSUE_TEMPLATE/bug_report.yml +54 -0
  34. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/ISSUE_TEMPLATE/feature_request.yml +41 -0
  35. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  36. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/copilot-instructions.md +28 -0
  37. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/dependabot.yml +20 -0
  38. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/workflows/ci-both.yml +66 -0
  39. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/workflows/ci-node.yml +36 -0
  40. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/workflows/ci-python.yml +38 -0
  41. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/workflows/dependency-review.yml +15 -0
  42. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.github/workflows/security.yml +46 -0
  43. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.kiro/steering/goodvibes.md +28 -0
  44. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/.windsurfrules +28 -0
  45. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/AGENTS.md +28 -0
  46. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/CHANGELOG.md +11 -0
  47. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/CLAUDE.md +131 -0
  48. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/CONTRIBUTING.md +55 -0
  49. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/GEMINI.md +28 -0
  50. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/JOURNAL.md +32 -0
  51. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/SECURITY.md +16 -0
  52. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/getting-started.md +31 -0
  53. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/onboarding.md +113 -0
  54. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/platform-setup/base44.md +40 -0
  55. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/platform-setup/bolt.md +22 -0
  56. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/platform-setup/chatgpt.md +38 -0
  57. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/platform-setup/cursor.md +23 -0
  58. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/platform-setup/kiro.md +19 -0
  59. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/platform-setup/replit.md +26 -0
  60. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/docs/platform-setup/windsurf.md +19 -0
  61. goodvibes_cli-1.6.1/src/goodvibes_cli/templates/replit.md +32 -0
  62. goodvibes_cli-1.6.1/src/goodvibes_cli/utils/__init__.py +0 -0
  63. goodvibes_cli-1.6.1/src/goodvibes_cli/utils/detect_project_type.py +17 -0
  64. goodvibes_cli-1.6.1/src/goodvibes_cli/utils/detect_python.py +37 -0
  65. goodvibes_cli-1.6.1/src/goodvibes_cli/utils/sentinel_merge.py +80 -0
  66. goodvibes_cli-1.6.1/templates/.amazonq/rules/goodvibes.md +28 -0
  67. goodvibes_cli-1.6.1/templates/.bolt/prompt +11 -0
  68. goodvibes_cli-1.6.1/templates/.claude/skills/cavecrew/README.md +41 -0
  69. goodvibes_cli-1.6.1/templates/.claude/skills/cavecrew/SKILL.md +82 -0
  70. goodvibes_cli-1.6.1/templates/.claude/skills/caveman/README.md +48 -0
  71. goodvibes_cli-1.6.1/templates/.claude/skills/caveman/SKILL.md +78 -0
  72. goodvibes_cli-1.6.1/templates/.claude/skills/caveman-commit/SKILL.md +65 -0
  73. goodvibes_cli-1.6.1/templates/.claude/skills/caveman-compress/SKILL.md +111 -0
  74. goodvibes_cli-1.6.1/templates/.claude/skills/caveman-help/SKILL.md +63 -0
  75. goodvibes_cli-1.6.1/templates/.claude/skills/caveman-review/SKILL.md +55 -0
  76. goodvibes_cli-1.6.1/templates/.claude/skills/caveman-stats/SKILL.md +10 -0
  77. goodvibes_cli-1.6.1/templates/.claude/skills/goodvibes-hygiene/SKILL.md +49 -0
  78. goodvibes_cli-1.6.1/templates/.clinerules/goodvibes.md +28 -0
  79. goodvibes_cli-1.6.1/templates/.continue/rules/goodvibes.md +28 -0
  80. goodvibes_cli-1.6.1/templates/.cursor/rules/goodvibes.mdc +28 -0
  81. goodvibes_cli-1.6.1/templates/.devin/rules/goodvibes.md +28 -0
  82. goodvibes_cli-1.6.1/templates/.github/ISSUE_TEMPLATE/bug_report.yml +54 -0
  83. goodvibes_cli-1.6.1/templates/.github/ISSUE_TEMPLATE/feature_request.yml +41 -0
  84. goodvibes_cli-1.6.1/templates/.github/PULL_REQUEST_TEMPLATE.md +13 -0
  85. goodvibes_cli-1.6.1/templates/.github/copilot-instructions.md +28 -0
  86. goodvibes_cli-1.6.1/templates/.github/dependabot.yml +20 -0
  87. goodvibes_cli-1.6.1/templates/.github/workflows/ci-both.yml +66 -0
  88. goodvibes_cli-1.6.1/templates/.github/workflows/ci-node.yml +36 -0
  89. goodvibes_cli-1.6.1/templates/.github/workflows/ci-python.yml +38 -0
  90. goodvibes_cli-1.6.1/templates/.github/workflows/dependency-review.yml +15 -0
  91. goodvibes_cli-1.6.1/templates/.github/workflows/security.yml +46 -0
  92. goodvibes_cli-1.6.1/templates/.kiro/steering/goodvibes.md +28 -0
  93. goodvibes_cli-1.6.1/templates/.windsurfrules +28 -0
  94. goodvibes_cli-1.6.1/templates/AGENTS.md +28 -0
  95. goodvibes_cli-1.6.1/templates/CHANGELOG.md +11 -0
  96. goodvibes_cli-1.6.1/templates/CLAUDE.md +131 -0
  97. goodvibes_cli-1.6.1/templates/CONTRIBUTING.md +55 -0
  98. goodvibes_cli-1.6.1/templates/GEMINI.md +28 -0
  99. goodvibes_cli-1.6.1/templates/JOURNAL.md +32 -0
  100. goodvibes_cli-1.6.1/templates/SECURITY.md +16 -0
  101. goodvibes_cli-1.6.1/templates/docs/getting-started.md +31 -0
  102. goodvibes_cli-1.6.1/templates/docs/onboarding.md +113 -0
  103. goodvibes_cli-1.6.1/templates/docs/platform-setup/base44.md +40 -0
  104. goodvibes_cli-1.6.1/templates/docs/platform-setup/bolt.md +22 -0
  105. goodvibes_cli-1.6.1/templates/docs/platform-setup/chatgpt.md +38 -0
  106. goodvibes_cli-1.6.1/templates/docs/platform-setup/cursor.md +23 -0
  107. goodvibes_cli-1.6.1/templates/docs/platform-setup/kiro.md +19 -0
  108. goodvibes_cli-1.6.1/templates/docs/platform-setup/replit.md +26 -0
  109. goodvibes_cli-1.6.1/templates/docs/platform-setup/windsurf.md +19 -0
  110. goodvibes_cli-1.6.1/templates/replit.md +32 -0
  111. goodvibes_cli-1.6.1/tests/__init__.py +0 -0
  112. goodvibes_cli-1.6.1/tests/conftest.py +69 -0
  113. goodvibes_cli-1.6.1/tests/fixtures.py +28 -0
  114. goodvibes_cli-1.6.1/tests/test_configure_mcp.py +220 -0
  115. goodvibes_cli-1.6.1/tests/test_copy_templates.py +468 -0
  116. goodvibes_cli-1.6.1/tests/test_detect_project_type.py +34 -0
  117. goodvibes_cli-1.6.1/tests/test_doctor_cmd.py +128 -0
  118. goodvibes_cli-1.6.1/tests/test_init_cmd.py +82 -0
  119. goodvibes_cli-1.6.1/tests/test_install_headroom.py +289 -0
  120. goodvibes_cli-1.6.1/tests/test_main.py +90 -0
  121. goodvibes_cli-1.6.1/tests/test_sentinel_merge.py +138 -0
  122. goodvibes_cli-1.6.1/tests/test_upgrade_cmd.py +121 -0
@@ -0,0 +1,8 @@
1
+ __pycache__/
2
+ *.pyc
3
+ *.pyo
4
+ .venv/
5
+ dist/
6
+ *.egg-info/
7
+ # Generated by upgrade_cmd tests (pytest cwd = packages/pip/ when merge_claude runs with empty template)
8
+ /CLAUDE.md
@@ -0,0 +1,37 @@
1
+ Metadata-Version: 2.4
2
+ Name: goodvibes-cli
3
+ Version: 1.6.1
4
+ Summary: One-command bootstrap for vibe coding projects
5
+ Project-URL: Homepage, https://github.com/jgiox/goodvibes
6
+ Project-URL: Repository, https://github.com/jgiox/goodvibes
7
+ License: Apache-2.0
8
+ Keywords: ai-coding,claude,claude-code,cli,copilot,llm,scaffold,starter-kit,vibe-coding
9
+ Classifier: Development Status :: 5 - Production/Stable
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development
18
+ Classifier: Topic :: Software Development :: Code Generators
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: rich>=14
22
+ Requires-Dist: typer>=0.15
23
+ Provides-Extra: dev
24
+ Requires-Dist: pytest-asyncio>=0.25; extra == 'dev'
25
+ Requires-Dist: pytest-cov>=7; extra == 'dev'
26
+ Requires-Dist: pytest-mock>=3; extra == 'dev'
27
+ Requires-Dist: pytest>=9; extra == 'dev'
28
+ Description-Content-Type: text/markdown
29
+
30
+ # goodvibes
31
+
32
+ One-command bootstrap for vibe coding projects.
33
+
34
+ ```
35
+ pip install goodvibes-cli
36
+ goodvibes init
37
+ ```
@@ -0,0 +1,8 @@
1
+ # goodvibes
2
+
3
+ One-command bootstrap for vibe coding projects.
4
+
5
+ ```
6
+ pip install goodvibes-cli
7
+ goodvibes init
8
+ ```
@@ -0,0 +1,43 @@
1
+ """Hatchling build hook — copies templates into the wheel when building from sdist."""
2
+ from __future__ import annotations
3
+
4
+ import pathlib
5
+ import shutil
6
+
7
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
8
+
9
+
10
+ class CustomBuildHook(BuildHookInterface):
11
+ """Resolve and inject the templates directory into the wheel build.
12
+
13
+ When building directly from source, ../../templates resolves correctly.
14
+ When building from an sdist, the templates are at <sdist-root>/templates/.
15
+ This hook copies whichever location exists into the wheel's goodvibes_cli/templates/.
16
+ """
17
+
18
+ def initialize(self, version: str, build_data: dict) -> None:
19
+ root = pathlib.Path(self.root)
20
+
21
+ # Direct source build: root = packages/pip/, templates at ../../templates
22
+ templates_source = root / ".." / ".." / "templates"
23
+ # Sdist build: templates were included at the sdist root level
24
+ templates_sdist = root / "templates"
25
+
26
+ if templates_source.exists():
27
+ src = templates_source.resolve()
28
+ elif templates_sdist.exists():
29
+ src = templates_sdist.resolve()
30
+ else:
31
+ return # no templates to bundle
32
+
33
+ dest = root / "src" / "goodvibes_cli" / "templates"
34
+ if dest.exists():
35
+ shutil.rmtree(dest)
36
+ shutil.copytree(src, dest)
37
+
38
+ # Clean up after wheel build completes (cleanup hook handles this)
39
+ self._templates_dest = dest
40
+
41
+ def finalize(self, version: str, build_data: dict, artifact_path: str) -> None:
42
+ if hasattr(self, "_templates_dest") and self._templates_dest.exists():
43
+ shutil.rmtree(self._templates_dest)
@@ -0,0 +1,56 @@
1
+ [project]
2
+ name = "goodvibes-cli"
3
+ version = "1.6.1"
4
+ description = "One-command bootstrap for vibe coding projects"
5
+ readme = "README.md"
6
+ requires-python = ">=3.10"
7
+ license = { text = "Apache-2.0" }
8
+ keywords = ["scaffold", "vibe-coding", "claude", "llm", "starter-kit", "cli", "ai-coding", "claude-code", "copilot"]
9
+ classifiers = [
10
+ "Development Status :: 5 - Production/Stable",
11
+ "Intended Audience :: Developers",
12
+ "Environment :: Console",
13
+ "Operating System :: OS Independent",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.10",
16
+ "Programming Language :: Python :: 3.11",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Topic :: Software Development",
19
+ "Topic :: Software Development :: Code Generators",
20
+ "Topic :: Utilities",
21
+ ]
22
+ dependencies = ["typer>=0.15", "rich>=14"]
23
+
24
+ [project.scripts]
25
+ goodvibes = "goodvibes_cli.main:app"
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/jgiox/goodvibes"
29
+ Repository = "https://github.com/jgiox/goodvibes"
30
+
31
+ [build-system]
32
+ requires = ["hatchling"]
33
+ build-backend = "hatchling.build"
34
+
35
+ [tool.hatch.build.hooks.custom]
36
+ path = "hatch_build.py"
37
+
38
+ [tool.hatch.build.targets.wheel]
39
+ packages = ["src/goodvibes_cli"]
40
+
41
+ [tool.hatch.build.targets.sdist]
42
+ include = ["src/goodvibes_cli/**", "tests/**", "hatch_build.py"]
43
+ # ponytail: include templates in sdist so hook can find them when building from sdist
44
+ force-include = {"../../templates" = "templates"}
45
+
46
+ [project.optional-dependencies]
47
+ dev = [
48
+ "pytest>=9",
49
+ "pytest-mock>=3",
50
+ "pytest-asyncio>=0.25",
51
+ "pytest-cov>=7",
52
+ ]
53
+
54
+ [tool.pytest.ini_options]
55
+ testpaths = ["tests"]
56
+ addopts = "-x -q"
@@ -0,0 +1 @@
1
+ __version__ = "1.0.0"
@@ -0,0 +1,15 @@
1
+ import sys
2
+
3
+ if sys.version_info < (3, 10):
4
+ print(
5
+ f"goodvibes requires Python 3.10 or higher. "
6
+ f"You have Python {sys.version_info.major}.{sys.version_info.minor}.",
7
+ file=sys.stderr,
8
+ )
9
+ sys.exit(1)
10
+
11
+ import typer # noqa: E402 — version guard must run before any import
12
+ from goodvibes_cli.main import app # noqa: E402
13
+
14
+ if __name__ == "__main__":
15
+ app()
@@ -0,0 +1,110 @@
1
+ """goodvibes doctor command — checks that goodvibes setup is complete."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.metadata
5
+ import pathlib
6
+ import shutil
7
+ import subprocess
8
+ from dataclasses import dataclass, field
9
+
10
+ import typer
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+
14
+ # ponytail: not imported from sentinel_merge — define locally to avoid coupling
15
+ SENTINEL_START = "<!-- goodvibes:start -->"
16
+ SENTINEL_END = "<!-- goodvibes:end -->"
17
+
18
+ console = Console()
19
+
20
+
21
+ def _installed_version() -> str:
22
+ try:
23
+ return importlib.metadata.version("goodvibes-cli")
24
+ except importlib.metadata.PackageNotFoundError:
25
+ return "unknown"
26
+
27
+
28
+ @dataclass
29
+ class CheckResult:
30
+ label: str
31
+ passed: bool
32
+ remedy: str = field(default="")
33
+
34
+
35
+ def _check_headroom() -> CheckResult:
36
+ present = shutil.which("headroom") is not None
37
+ return CheckResult(
38
+ label="headroom on PATH",
39
+ passed=present,
40
+ remedy="" if present else 'Run: uv tool install "headroom-ai[all]" (or re-run goodvibes init)',
41
+ )
42
+
43
+
44
+ def _check_git_config(key: str) -> CheckResult:
45
+ try:
46
+ result = subprocess.run(
47
+ ["git", "config", key],
48
+ capture_output=True,
49
+ text=True,
50
+ check=True,
51
+ )
52
+ passed = bool(result.stdout.strip())
53
+ return CheckResult(
54
+ label=f"git {key}",
55
+ passed=passed,
56
+ remedy="" if passed else f'Run: git config --global {key} "Your Value"',
57
+ )
58
+ except (subprocess.CalledProcessError, FileNotFoundError):
59
+ return CheckResult(
60
+ label=f"git {key}",
61
+ passed=False,
62
+ remedy=f'Run: git config --global {key} "Your Value"',
63
+ )
64
+
65
+
66
+ def _check_claude_md(cwd: pathlib.Path) -> CheckResult:
67
+ present = (cwd / "CLAUDE.md").exists()
68
+ return CheckResult(
69
+ label="CLAUDE.md present",
70
+ passed=present,
71
+ remedy="" if present else "Run: goodvibes init",
72
+ )
73
+
74
+
75
+ def _check_sentinel(cwd: pathlib.Path) -> CheckResult:
76
+ path = cwd / "CLAUDE.md"
77
+ if not path.exists():
78
+ return CheckResult(label="goodvibes sentinel block", passed=False, remedy="Run: goodvibes init")
79
+ content = path.read_text(encoding="utf-8")
80
+ ok = SENTINEL_START in content and SENTINEL_END in content
81
+ return CheckResult(
82
+ label="goodvibes sentinel block",
83
+ passed=ok,
84
+ remedy="" if ok else "Run: goodvibes init (will merge sentinel block)",
85
+ )
86
+
87
+
88
+ def doctor_cmd() -> None:
89
+ """Check that goodvibes setup is complete."""
90
+ cwd = pathlib.Path.cwd()
91
+
92
+ results = [
93
+ _check_headroom(),
94
+ _check_git_config("user.name"),
95
+ _check_git_config("user.email"),
96
+ _check_claude_md(cwd),
97
+ _check_sentinel(cwd),
98
+ ]
99
+
100
+ version = _installed_version()
101
+ lines = [f"goodvibes v{version}"] + [f"{'✓' if r.passed else '✗'} {r.label}" for r in results]
102
+ console.print(Panel("\n".join(lines), title="goodvibes doctor"))
103
+
104
+ failures = [r for r in results if not r.passed]
105
+ if failures:
106
+ remediation = "\n".join(r.remedy for r in failures if r.remedy)
107
+ console.print(Panel(remediation, title="How to fix"))
108
+ raise typer.Exit(1)
109
+
110
+ console.rule("[green]All checks passed.[/green]")
@@ -0,0 +1,102 @@
1
+ """goodvibes init command — port of init.ts."""
2
+ import pathlib
3
+ from typing import Annotated
4
+
5
+ import typer
6
+ from rich.console import Console
7
+ from rich.panel import Panel
8
+
9
+ from goodvibes_cli.steps.configure_mcp import configure_mcp
10
+ from goodvibes_cli.steps.copy_templates import copy_templates, list_template_files, resolve_templates_dir
11
+ from goodvibes_cli.steps.install_headroom import install_headroom
12
+ from goodvibes_cli.utils.detect_project_type import detect_project_type
13
+
14
+ console = Console()
15
+
16
+ _NEXT_STEPS = (
17
+ "1. Open this project in your AI coding tool\n"
18
+ "2. Claude Code users: /plugin marketplace add DietrichGebert/ponytail\n"
19
+ " Other IDEs (Cursor, Windsurf, Kiro, Antigravity, etc.): rules already active\n"
20
+ "3. Start coding — CLAUDE.md rules are already active"
21
+ )
22
+
23
+
24
+ def init_cmd(
25
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="Preview files without writing")] = False,
26
+ minimal: Annotated[bool, typer.Option("--minimal", help="Skip headroom install and CI workflows")] = False,
27
+ ) -> None:
28
+ """Bootstrap a project with goodvibes configuration."""
29
+ template_dir = resolve_templates_dir()
30
+ cwd = pathlib.Path.cwd()
31
+ project_type = detect_project_type(cwd)
32
+
33
+ console.rule("[bold]goodvibes init[/bold]")
34
+
35
+ if dry_run:
36
+ if minimal:
37
+ all_files = list_template_files(template_dir)
38
+ files = [f for f in all_files if not f.startswith(".github") and not f.startswith("docs")]
39
+ else:
40
+ files_tuple = copy_templates(template_dir, cwd, dry_run=True, minimal=False, project_type=project_type)
41
+ files = files_tuple[0]
42
+ file_list = "\n".join(f" Would write: {f}" for f in files)
43
+ console.print(Panel(file_list, title="Dry run — no files written"))
44
+ if minimal:
45
+ console.print(Panel(
46
+ "CI workflows and docs were skipped.\nRun goodvibes init without --minimal to add them.",
47
+ title="Skipped layers"
48
+ ))
49
+ console.print(Panel(_NEXT_STEPS, title="Next steps"))
50
+ console.rule("Run without --dry-run to apply these changes.")
51
+ return
52
+
53
+ # Non-empty directory notice (UX-01)
54
+ existing = [e for e in cwd.iterdir() if e.name not in (".git", ".DS_Store")]
55
+ if existing:
56
+ console.print(Panel("Existing files will not be overwritten.", title="Non-empty project detected"))
57
+
58
+ created_files: list[str] = []
59
+ skipped_files_list: list[str] = []
60
+
61
+ try:
62
+ with console.status("Copying template files") as status:
63
+ def log_copy(msg: str) -> None:
64
+ status.update(msg)
65
+
66
+ written, skipped = copy_templates(template_dir, cwd, dry_run=False, minimal=minimal, project_type=project_type)
67
+ created_files.extend(written)
68
+ skipped_files_list.extend(skipped)
69
+
70
+ if not minimal:
71
+ with console.status("Installing headroom") as status:
72
+ def log_install(msg: str) -> None:
73
+ status.update(msg)
74
+
75
+ install_headroom(log_install)
76
+
77
+ with console.status("Configuring headroom MCP") as status:
78
+ def log_mcp(msg: str) -> None:
79
+ status.update(msg)
80
+
81
+ configure_mcp(log_mcp)
82
+ except PermissionError as e:
83
+ console.print(f"[red]Error:[/red] {e}")
84
+ console.print("[yellow]Fix:[/yellow] Make sure you are inside your project directory before running this command.")
85
+ console.print(" If permissions are the issue: [bold]chmod u+w .[/bold] (macOS/Linux)")
86
+ raise typer.Exit(1)
87
+ except (OSError, Exception) as e:
88
+ console.print(f"[red]Unexpected error:[/red] {e}")
89
+ raise typer.Exit(1)
90
+
91
+ written_str = "\n".join(created_files) if created_files else "(none)"
92
+ console.print(Panel(written_str, title=f"Files written ({len(created_files)})"))
93
+ if skipped_files_list:
94
+ skipped_str = "\n".join(skipped_files_list)
95
+ console.print(Panel(skipped_str, title=f"Files skipped ({len(skipped_files_list)})"))
96
+ console.print(Panel(_NEXT_STEPS, title="Next steps"))
97
+ if minimal:
98
+ console.print(Panel(
99
+ "CI workflows and docs were skipped.\nRun goodvibes init without --minimal to add them.",
100
+ title="Skipped layers"
101
+ ))
102
+ console.rule("[green]You're all set![/green]")
@@ -0,0 +1,229 @@
1
+ """goodvibes upgrade command — port of upgrade.ts."""
2
+ from __future__ import annotations
3
+
4
+ import importlib.metadata
5
+ import json
6
+ import os
7
+ import pathlib
8
+ import shutil
9
+ import subprocess
10
+ import sys
11
+ import urllib.request
12
+ from typing import Annotated
13
+
14
+ import typer
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+
18
+ from goodvibes_cli.steps.copy_templates import list_template_files, resolve_templates_dir
19
+ from goodvibes_cli.utils.detect_project_type import detect_project_type
20
+ from goodvibes_cli.utils.sentinel_merge import (
21
+ SENTINEL_END,
22
+ SENTINEL_START,
23
+ extract_version,
24
+ merge_claude,
25
+ version_gte,
26
+ )
27
+
28
+ console = Console()
29
+
30
+ _PYPI_URL = "https://pypi.org/pypi/goodvibes-cli/json"
31
+ _UPGRADING_ENV = "_GV_UPGRADING"
32
+
33
+
34
+ def _get_package_version() -> str | None:
35
+ try:
36
+ return importlib.metadata.version("goodvibes-cli")
37
+ except importlib.metadata.PackageNotFoundError:
38
+ return None
39
+
40
+
41
+ def _check_pypi_version() -> str | None:
42
+ try:
43
+ with urllib.request.urlopen(_PYPI_URL, timeout=5) as resp: # noqa: S310
44
+ return json.loads(resp.read())["info"]["version"]
45
+ except Exception:
46
+ return None
47
+
48
+
49
+ def _self_update_pip() -> None:
50
+ # try uv tool upgrade first; fall back to pip install --upgrade
51
+ try:
52
+ subprocess.run(["uv", "tool", "upgrade", "goodvibes-cli"], check=True)
53
+ except (subprocess.CalledProcessError, FileNotFoundError):
54
+ subprocess.run(
55
+ [sys.executable, "-m", "pip", "install", "--upgrade", "goodvibes-cli"],
56
+ check=True,
57
+ )
58
+
59
+ _MANAGED_FIXED = {
60
+ "CLAUDE.md",
61
+ ".github/workflows/ci.yml",
62
+ ".github/workflows/security.yml",
63
+ ".github/workflows/dependency-review.yml",
64
+ ".github/dependabot.yml",
65
+ }
66
+
67
+
68
+ def _detect_installed_version(cwd: pathlib.Path) -> str | None:
69
+ claude_path = cwd / "CLAUDE.md"
70
+ if not claude_path.exists():
71
+ return None
72
+ return extract_version(claude_path.read_text(encoding="utf-8"))
73
+
74
+
75
+ def compute_changes(
76
+ template_dir: pathlib.Path,
77
+ dest_dir: pathlib.Path,
78
+ project_type: str,
79
+ ) -> list[tuple[str, str]]:
80
+ """Return list of (relative_path, status) for managed files."""
81
+ if not template_dir:
82
+ return []
83
+ all_files = list_template_files(template_dir)
84
+ ci_variant = f".github/workflows/ci-{project_type}.yml"
85
+ managed = [
86
+ f for f in all_files
87
+ if f.startswith(".claude/skills/")
88
+ or f in _MANAGED_FIXED
89
+ or f == ci_variant
90
+ ]
91
+
92
+ results: list[tuple[str, str]] = []
93
+ for rel in managed:
94
+ # Map CI variant to its destination path
95
+ dest_rel = ".github/workflows/ci.yml" if rel == ci_variant else rel
96
+ dest_path = dest_dir / dest_rel
97
+ src_path = template_dir / rel
98
+
99
+ if not dest_path.exists():
100
+ results.append((dest_rel, "new"))
101
+ continue
102
+
103
+ src_content = src_path.read_text(encoding="utf-8")
104
+ dest_content = dest_path.read_text(encoding="utf-8")
105
+ status = "unchanged" if src_content == dest_content else "changed"
106
+ results.append((dest_rel, status))
107
+
108
+ return sorted(results, key=lambda t: t[0])
109
+
110
+
111
+ def format_change_summary(changes: list[tuple[str, str]]) -> str:
112
+ if not changes:
113
+ return "(no managed files found)"
114
+ symbol = {"changed": "updated", "unchanged": "unchanged", "new": "new"}
115
+ return "\n".join(f"{symbol.get(s, '?')} {p}" for p, s in changes)
116
+
117
+
118
+ def upgrade_templates(
119
+ template_dir: pathlib.Path,
120
+ dest_dir: pathlib.Path,
121
+ project_type: str,
122
+ ) -> list[str]:
123
+ """Overwrite managed files from template_dir into dest_dir."""
124
+ ci_variants = {"ci-node.yml", "ci-python.yml", "ci-both.yml"}
125
+ selected_variant = f"ci-{project_type}.yml"
126
+ claude_dest = dest_dir / "CLAUDE.md"
127
+
128
+ if template_dir:
129
+ def ignore_fn(directory: str, contents: list[str]) -> set[str]:
130
+ ignored: set[str] = set()
131
+ for name in contents:
132
+ full = pathlib.Path(directory) / name
133
+ try:
134
+ full.resolve().relative_to(template_dir.resolve()) # raises if symlink escapes
135
+ except ValueError:
136
+ ignored.add(name)
137
+ continue
138
+ try:
139
+ rel = full.relative_to(template_dir)
140
+ except ValueError:
141
+ ignored.add(name)
142
+ continue
143
+ if name == "CLAUDE.md":
144
+ ignored.add(name) # handled by merge_claude
145
+ rel_str = str(rel)
146
+ # Skip files outside the managed set
147
+ if not any(
148
+ [".claude/skills" in rel_str, ".github/workflows" in rel_str]
149
+ ) and name not in {"CLAUDE.md"}:
150
+ ignored.add(name)
151
+ # Skip non-selected CI variants
152
+ if name in ci_variants and name != selected_variant:
153
+ ignored.add(name)
154
+ return ignored
155
+
156
+ shutil.copytree(str(template_dir), str(dest_dir), ignore=ignore_fn, dirs_exist_ok=True)
157
+
158
+ # Rename selected CI variant to ci.yml
159
+ variant_path = dest_dir / ".github" / "workflows" / selected_variant
160
+ ci_path = dest_dir / ".github" / "workflows" / "ci.yml"
161
+ if variant_path.exists():
162
+ variant_path.rename(ci_path)
163
+
164
+ template_content = (template_dir / "CLAUDE.md").read_text(encoding="utf-8")
165
+ merge_claude(claude_dest, template_content)
166
+ else:
167
+ # template_dir unavailable — still call merge_claude so CLAUDE.md update path is exercised
168
+ if claude_dest.exists():
169
+ merge_claude(claude_dest, claude_dest.read_text(encoding="utf-8"))
170
+
171
+ # Return relative paths written — only managed prefixes
172
+ if not dest_dir.exists():
173
+ return []
174
+ return sorted(
175
+ str(f.relative_to(dest_dir))
176
+ for f in dest_dir.rglob("*")
177
+ if f.is_file()
178
+ and (
179
+ str(f.relative_to(dest_dir)).startswith(".claude/skills/")
180
+ or str(f.relative_to(dest_dir)).startswith(".github/workflows/")
181
+ or str(f.relative_to(dest_dir)) == "CLAUDE.md"
182
+ )
183
+ )
184
+
185
+
186
+ def upgrade_cmd(
187
+ dry_run: Annotated[bool, typer.Option("--dry-run", help="Preview changes without writing")] = False,
188
+ ) -> None:
189
+ """Re-sync goodvibes-managed files to the latest version."""
190
+ console.rule("[bold]goodvibes upgrade[/bold]")
191
+
192
+ # Self-update: check PyPI for a newer package version and re-exec if found.
193
+ # _GV_UPGRADING prevents infinite re-exec if the new binary still sees itself as outdated.
194
+ if not os.environ.get(_UPGRADING_ENV):
195
+ current = _get_package_version()
196
+ latest = _check_pypi_version()
197
+ if latest and current and not version_gte(current, latest):
198
+ console.print(f"New version available: [bold]{latest}[/bold] (installed: {current})")
199
+ with console.status(f"Updating goodvibes {current} → {latest}…"):
200
+ _self_update_pip()
201
+ console.print(f"[green]✓ Updated to {latest}[/green] — re-applying templates…")
202
+ os.execve(sys.argv[0], sys.argv, {**os.environ, _UPGRADING_ENV: "1"})
203
+ return # unreachable; satisfies type checker
204
+
205
+ template_dir = resolve_templates_dir()
206
+ cwd = pathlib.Path.cwd()
207
+ project_type = detect_project_type(cwd)
208
+
209
+ installed_version = _detect_installed_version(cwd)
210
+ bundled_version = _get_package_version()
211
+
212
+ if installed_version and bundled_version and version_gte(installed_version, bundled_version):
213
+ console.rule(f"[green]Already up to date (v{installed_version})[/green]")
214
+ return
215
+
216
+ changes = compute_changes(template_dir, cwd, project_type)
217
+ console.print(Panel(format_change_summary(changes), title="What will change"))
218
+
219
+ if dry_run:
220
+ console.rule("Run without --dry-run to apply these changes.")
221
+ return
222
+
223
+ updated: list[str] = []
224
+ with console.status("Upgrading managed files"):
225
+ updated = upgrade_templates(template_dir, cwd, project_type)
226
+
227
+ file_list = "\n".join(updated) if updated else "(no files changed)"
228
+ console.print(Panel(file_list, title="Files updated"))
229
+ console.rule("[green]Upgrade complete![/green]")
@@ -0,0 +1,32 @@
1
+ import importlib.metadata
2
+
3
+ import typer
4
+
5
+ from goodvibes_cli.commands.doctor_cmd import doctor_cmd
6
+ from goodvibes_cli.commands.init_cmd import init_cmd
7
+ from goodvibes_cli.commands.upgrade_cmd import upgrade_cmd
8
+
9
+ app = typer.Typer(help="goodvibes — one-command bootstrap for vibe coding projects")
10
+
11
+
12
+ def _version_callback(value: bool) -> None:
13
+ if value:
14
+ version = importlib.metadata.version("goodvibes-cli")
15
+ typer.echo(f"goodvibes {version}")
16
+ raise typer.Exit()
17
+
18
+
19
+ @app.callback()
20
+ def _callback(
21
+ version: bool = typer.Option(None, "--version", callback=_version_callback, is_eager=True, help="Show version"),
22
+ ) -> None:
23
+ """goodvibes CLI — run 'goodvibes init' to bootstrap a project"""
24
+
25
+
26
+ app.command("init")(init_cmd)
27
+ app.command("upgrade")(upgrade_cmd)
28
+ app.command("update")(upgrade_cmd)
29
+ app.command("doctor")(doctor_cmd)
30
+
31
+ if __name__ == "__main__":
32
+ app()