bpsai-pair 0.2.0__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.
Potentially problematic release.
This version of bpsai-pair might be problematic. Click here for more details.
- bpsai_pair-0.2.0/MANIFEST.in +3 -0
- bpsai_pair-0.2.0/PKG-INFO +29 -0
- bpsai_pair-0.2.0/README.md +18 -0
- bpsai_pair-0.2.0/bpsai_pair/__init__.py +25 -0
- bpsai_pair-0.2.0/bpsai_pair/__main__.py +4 -0
- bpsai_pair-0.2.0/bpsai_pair/adapters.py +9 -0
- bpsai_pair-0.2.0/bpsai_pair/cli.py +514 -0
- bpsai_pair-0.2.0/bpsai_pair/config.py +310 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/cookiecutter.json +12 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.agentpackignore +1 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.editorconfig +17 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.github/PULL_REQUEST_TEMPLATE.md +47 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +90 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.github/workflows/project_tree.yml +33 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.gitignore +5 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.gitleaks.toml +17 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +38 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/CODEOWNERS +9 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/CONTRIBUTING.md +35 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/SECURITY.md +14 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/agents.md +6 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/agents.md.bak +196 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/development.md +1 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/development.md.bak +10 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/directory_notes/.gitkeep +1 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/project_tree.md +7 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/prompts/deep_research.yml +28 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/prompts/implementation.yml +25 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/prompts/roadmap.yml +14 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/scripts/README.md +11 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/src/.gitkeep +1 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/templates/adr.md +19 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/templates/directory_note.md +17 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/tests/example_contract/README.md +3 -0
- bpsai_pair-0.2.0/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/tests/example_integration/README.md +3 -0
- bpsai_pair-0.2.0/bpsai_pair/init_bundled_cli.py +47 -0
- bpsai_pair-0.2.0/bpsai_pair/jsonio.py +6 -0
- bpsai_pair-0.2.0/bpsai_pair/ops.py +451 -0
- bpsai_pair-0.2.0/bpsai_pair/pyutils.py +26 -0
- bpsai_pair-0.2.0/bpsai_pair/utils.py +11 -0
- bpsai_pair-0.2.0/bpsai_pair.egg-info/PKG-INFO +29 -0
- bpsai_pair-0.2.0/bpsai_pair.egg-info/SOURCES.txt +52 -0
- bpsai_pair-0.2.0/bpsai_pair.egg-info/dependency_links.txt +1 -0
- bpsai_pair-0.2.0/bpsai_pair.egg-info/entry_points.txt +3 -0
- bpsai_pair-0.2.0/bpsai_pair.egg-info/requires.txt +3 -0
- bpsai_pair-0.2.0/bpsai_pair.egg-info/top_level.txt +1 -0
- bpsai_pair-0.2.0/pyproject.toml +25 -0
- bpsai_pair-0.2.0/setup.cfg +4 -0
- bpsai_pair-0.2.0/tests/test_cli.py +115 -0
- bpsai_pair-0.2.0/tests/test_config.py +54 -0
- bpsai_pair-0.2.0/tests/test_context_sync.py +52 -0
- bpsai_pair-0.2.0/tests/test_feature_branch_type.py +36 -0
- bpsai_pair-0.2.0/tests/test_ops.py +70 -0
- bpsai_pair-0.2.0/tests/test_pack_preview.py +51 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bpsai-pair
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CLI for AI pair-coding workflow
|
|
5
|
+
Author: BPS AI Software
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Requires-Dist: typer>=0.12
|
|
9
|
+
Requires-Dist: rich>=13.7
|
|
10
|
+
Requires-Dist: pyyaml>=6.0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# bpsai-pair CLI
|
|
14
|
+
|
|
15
|
+
## Quick start (local, un-packaged)
|
|
16
|
+
```
|
|
17
|
+
python -m tools.cli.bpsai_pair --help
|
|
18
|
+
python -m tools.cli.bpsai_pair init tools/cookiecutter-paircoder
|
|
19
|
+
python -m tools.cli.bpsai_pair feature auth-di --primary "Decouple auth via DI" --phase "Refactor auth + tests"
|
|
20
|
+
python -m tools.cli.bpsai_pair pack --extra README.md
|
|
21
|
+
python -m tools.cli.bpsai_pair context-sync --last "initialized scaffolding" --nxt "set up CI secrets" --blockers "none"
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Install as a CLI
|
|
25
|
+
```
|
|
26
|
+
cd tools/cli
|
|
27
|
+
pip install -e .
|
|
28
|
+
# now available as: bpsai-pair --help
|
|
29
|
+
```
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
|
|
2
|
+
# bpsai-pair CLI
|
|
3
|
+
|
|
4
|
+
## Quick start (local, un-packaged)
|
|
5
|
+
```
|
|
6
|
+
python -m tools.cli.bpsai_pair --help
|
|
7
|
+
python -m tools.cli.bpsai_pair init tools/cookiecutter-paircoder
|
|
8
|
+
python -m tools.cli.bpsai_pair feature auth-di --primary "Decouple auth via DI" --phase "Refactor auth + tests"
|
|
9
|
+
python -m tools.cli.bpsai_pair pack --extra README.md
|
|
10
|
+
python -m tools.cli.bpsai_pair context-sync --last "initialized scaffolding" --nxt "set up CI secrets" --blockers "none"
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Install as a CLI
|
|
14
|
+
```
|
|
15
|
+
cd tools/cli
|
|
16
|
+
pip install -e .
|
|
17
|
+
# now available as: bpsai-pair --help
|
|
18
|
+
```
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bpsai_pair package
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
__version__ = "0.2.0"
|
|
6
|
+
|
|
7
|
+
# Make modules available at package level
|
|
8
|
+
from . import cli
|
|
9
|
+
from . import ops
|
|
10
|
+
from . import config
|
|
11
|
+
from . import utils
|
|
12
|
+
from . import jsonio
|
|
13
|
+
from . import pyutils
|
|
14
|
+
from . import init_bundled_cli
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"__version__",
|
|
18
|
+
"cli",
|
|
19
|
+
"ops",
|
|
20
|
+
"config",
|
|
21
|
+
"utils",
|
|
22
|
+
"jsonio",
|
|
23
|
+
"pyutils",
|
|
24
|
+
"init_bundled_cli"
|
|
25
|
+
]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import subprocess
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
from typing import List
|
|
4
|
+
|
|
5
|
+
class Shell:
|
|
6
|
+
@staticmethod
|
|
7
|
+
def run(cmd: List[str], cwd: Path | None = None, check: bool = True) -> str:
|
|
8
|
+
res = subprocess.run(cmd, cwd=cwd, check=check, text=True, capture_output=True)
|
|
9
|
+
return (res.stdout or "") + (res.stderr or "")
|
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Enhanced bpsai-pair CLI with cross-platform support and improved UX.
|
|
3
|
+
"""
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import os
|
|
7
|
+
import json
|
|
8
|
+
import sys
|
|
9
|
+
import subprocess
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import List, Optional
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
|
|
14
|
+
import typer
|
|
15
|
+
from rich import print
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
from rich.table import Table
|
|
18
|
+
from rich.progress import Progress, SpinnerColumn, TextColumn
|
|
19
|
+
|
|
20
|
+
# Try relative imports first, fall back to absolute
|
|
21
|
+
try:
|
|
22
|
+
from . import __version__
|
|
23
|
+
from . import init_bundled_cli
|
|
24
|
+
from . import ops
|
|
25
|
+
from .config import Config
|
|
26
|
+
except ImportError:
|
|
27
|
+
# For development/testing when running as script
|
|
28
|
+
import sys
|
|
29
|
+
sys.path.insert(0, str(Path(__file__).parent.parent))
|
|
30
|
+
from bpsai_pair import __version__
|
|
31
|
+
from bpsai_pair import init_bundled_cli
|
|
32
|
+
from bpsai_pair import ops
|
|
33
|
+
from bpsai_pair.config import Config
|
|
34
|
+
|
|
35
|
+
# Initialize Rich console
|
|
36
|
+
console = Console()
|
|
37
|
+
|
|
38
|
+
# Environment variable support
|
|
39
|
+
MAIN_BRANCH = os.getenv("PAIRCODER_MAIN_BRANCH", "main")
|
|
40
|
+
CONTEXT_DIR = os.getenv("PAIRCODER_CONTEXT_DIR", "context")
|
|
41
|
+
|
|
42
|
+
app = typer.Typer(
|
|
43
|
+
add_completion=False,
|
|
44
|
+
help="bpsai-pair: AI pair-coding workflow CLI",
|
|
45
|
+
context_settings={"help_option_names": ["-h", "--help"]}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def version_callback(value: bool):
|
|
50
|
+
"""Show version and exit."""
|
|
51
|
+
if value:
|
|
52
|
+
console.print(f"[bold blue]bpsai-pair[/bold blue] version {__version__}")
|
|
53
|
+
raise typer.Exit()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@app.callback()
|
|
57
|
+
def main(
|
|
58
|
+
version: bool = typer.Option(
|
|
59
|
+
False,
|
|
60
|
+
"--version",
|
|
61
|
+
"-v",
|
|
62
|
+
callback=version_callback,
|
|
63
|
+
help="Show version and exit"
|
|
64
|
+
)
|
|
65
|
+
):
|
|
66
|
+
"""bpsai-pair: AI pair-coding workflow CLI"""
|
|
67
|
+
pass
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def repo_root() -> Path:
|
|
71
|
+
"""Get repo root with better error message."""
|
|
72
|
+
p = Path.cwd()
|
|
73
|
+
if not ops.GitOps.is_repo(p):
|
|
74
|
+
console.print(
|
|
75
|
+
"[red]✗ Not in a git repository.[/red]\n"
|
|
76
|
+
"Please run from your project root directory (where .git exists).\n"
|
|
77
|
+
"[dim]Hint: cd to your project directory first[/dim]"
|
|
78
|
+
)
|
|
79
|
+
raise typer.Exit(1)
|
|
80
|
+
return p
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@app.command()
|
|
84
|
+
def init(
|
|
85
|
+
template: Optional[str] = typer.Argument(
|
|
86
|
+
None, help="Path to template (optional, uses bundled template if not provided)"
|
|
87
|
+
),
|
|
88
|
+
interactive: bool = typer.Option(
|
|
89
|
+
False, "--interactive", "-i", help="Interactive mode to gather project info"
|
|
90
|
+
)
|
|
91
|
+
):
|
|
92
|
+
"""Initialize repo with governance, context, prompts, scripts, and workflows."""
|
|
93
|
+
root = repo_root()
|
|
94
|
+
|
|
95
|
+
if interactive:
|
|
96
|
+
# Interactive mode to gather project information
|
|
97
|
+
project_name = typer.prompt("Project name", default="My Project")
|
|
98
|
+
primary_goal = typer.prompt("Primary goal", default="Build awesome software")
|
|
99
|
+
coverage = typer.prompt("Coverage target (%)", default="80")
|
|
100
|
+
|
|
101
|
+
# Create a config file
|
|
102
|
+
config = Config(
|
|
103
|
+
project_name=project_name,
|
|
104
|
+
primary_goal=primary_goal,
|
|
105
|
+
coverage_target=int(coverage)
|
|
106
|
+
)
|
|
107
|
+
config.save(root)
|
|
108
|
+
|
|
109
|
+
# Use bundled template if none provided
|
|
110
|
+
if template is None:
|
|
111
|
+
with Progress(
|
|
112
|
+
SpinnerColumn(),
|
|
113
|
+
TextColumn("[progress.description]{task.description}"),
|
|
114
|
+
console=console
|
|
115
|
+
) as progress:
|
|
116
|
+
task = progress.add_task("Initializing scaffolding...", total=None)
|
|
117
|
+
result = init_bundled_cli.main()
|
|
118
|
+
progress.update(task, completed=True)
|
|
119
|
+
|
|
120
|
+
console.print("[green]✓[/green] Initialized repo with pair-coding scaffolding")
|
|
121
|
+
console.print("[dim]Review diffs and commit changes[/dim]")
|
|
122
|
+
else:
|
|
123
|
+
# Use provided template (simplified for now)
|
|
124
|
+
console.print(f"[yellow]Using template: {template}[/yellow]")
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
@app.command()
|
|
128
|
+
def feature(
|
|
129
|
+
name: str = typer.Argument(..., help="Feature branch name (without prefix)"),
|
|
130
|
+
primary: str = typer.Option("", "--primary", "-p", help="Primary goal to stamp into context"),
|
|
131
|
+
phase: str = typer.Option("", "--phase", help="Phase goal for Next action"),
|
|
132
|
+
force: bool = typer.Option(False, "--force", "-f", help="Bypass dirty-tree check"),
|
|
133
|
+
type: str = typer.Option(
|
|
134
|
+
"feature",
|
|
135
|
+
"--type",
|
|
136
|
+
"-t",
|
|
137
|
+
help="Branch type: feature|fix|refactor",
|
|
138
|
+
case_sensitive=False,
|
|
139
|
+
),
|
|
140
|
+
):
|
|
141
|
+
"""Create feature branch and scaffold context (cross-platform)."""
|
|
142
|
+
root = repo_root()
|
|
143
|
+
|
|
144
|
+
# Validate branch type
|
|
145
|
+
branch_type = type.lower()
|
|
146
|
+
if branch_type not in {"feature", "fix", "refactor"}:
|
|
147
|
+
console.print(
|
|
148
|
+
f"[red]✗ Invalid branch type: {type}[/red]\n"
|
|
149
|
+
"Must be one of: feature, fix, refactor"
|
|
150
|
+
)
|
|
151
|
+
raise typer.Exit(1)
|
|
152
|
+
|
|
153
|
+
# Use Python ops instead of shell script
|
|
154
|
+
with Progress(
|
|
155
|
+
SpinnerColumn(),
|
|
156
|
+
TextColumn("[progress.description]{task.description}"),
|
|
157
|
+
console=console
|
|
158
|
+
) as progress:
|
|
159
|
+
task = progress.add_task(f"Creating {branch_type}/{name}...", total=None)
|
|
160
|
+
|
|
161
|
+
try:
|
|
162
|
+
ops.FeatureOps.create_feature(
|
|
163
|
+
root=root,
|
|
164
|
+
name=name,
|
|
165
|
+
branch_type=branch_type,
|
|
166
|
+
primary_goal=primary,
|
|
167
|
+
phase=phase,
|
|
168
|
+
force=force
|
|
169
|
+
)
|
|
170
|
+
progress.update(task, completed=True)
|
|
171
|
+
|
|
172
|
+
console.print(f"[green]✓[/green] Created branch [bold]{branch_type}/{name}[/bold]")
|
|
173
|
+
console.print(f"[green]✓[/green] Updated context with primary goal and phase")
|
|
174
|
+
console.print("[dim]Next: Connect your agent and share /context files[/dim]")
|
|
175
|
+
|
|
176
|
+
except ValueError as e:
|
|
177
|
+
progress.update(task, completed=True)
|
|
178
|
+
console.print(f"[red]✗ {e}[/red]")
|
|
179
|
+
raise typer.Exit(1)
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@app.command()
|
|
183
|
+
def pack(
|
|
184
|
+
output: str = typer.Option("agent_pack.tgz", "--out", "-o", help="Output archive name"),
|
|
185
|
+
extra: Optional[List[str]] = typer.Option(None, "--extra", "-e", help="Additional paths to include"),
|
|
186
|
+
dry_run: bool = typer.Option(False, "--dry-run", help="Preview files without creating archive"),
|
|
187
|
+
list_only: bool = typer.Option(False, "--list", "-l", help="List files to be included"),
|
|
188
|
+
json_out: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
189
|
+
):
|
|
190
|
+
"""Create agent context package (cross-platform)."""
|
|
191
|
+
root = repo_root()
|
|
192
|
+
output_path = root / output
|
|
193
|
+
|
|
194
|
+
# Use Python ops for packing
|
|
195
|
+
files = ops.ContextPacker.pack(
|
|
196
|
+
root=root,
|
|
197
|
+
output=output_path,
|
|
198
|
+
extra_files=extra,
|
|
199
|
+
dry_run=(dry_run or list_only)
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
if json_out:
|
|
203
|
+
result = {
|
|
204
|
+
"files": [str(f.relative_to(root)) for f in files],
|
|
205
|
+
"count": len(files),
|
|
206
|
+
"dry_run": dry_run,
|
|
207
|
+
"list_only": list_only
|
|
208
|
+
}
|
|
209
|
+
if not (dry_run or list_only):
|
|
210
|
+
result["output"] = str(output)
|
|
211
|
+
result["size"] = output_path.stat().st_size if output_path.exists() else 0
|
|
212
|
+
print(json.dumps(result, indent=2))
|
|
213
|
+
elif list_only:
|
|
214
|
+
for f in files:
|
|
215
|
+
console.print(str(f.relative_to(root)))
|
|
216
|
+
elif dry_run:
|
|
217
|
+
console.print(f"[yellow]Would pack {len(files)} files:[/yellow]")
|
|
218
|
+
for f in files[:10]: # Show first 10
|
|
219
|
+
console.print(f" • {f.relative_to(root)}")
|
|
220
|
+
if len(files) > 10:
|
|
221
|
+
console.print(f" [dim]... and {len(files) - 10} more[/dim]")
|
|
222
|
+
else:
|
|
223
|
+
console.print(f"[green]✓[/green] Created [bold]{output}[/bold]")
|
|
224
|
+
size_kb = output_path.stat().st_size / 1024
|
|
225
|
+
console.print(f" Size: {size_kb:.1f} KB")
|
|
226
|
+
console.print(f" Files: {len(files)}")
|
|
227
|
+
console.print("[dim]Upload this archive to your agent session[/dim]")
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
@app.command("context-sync")
|
|
231
|
+
def context_sync(
|
|
232
|
+
overall: Optional[str] = typer.Option(None, "--overall", help="Overall goal override"),
|
|
233
|
+
last: str = typer.Option(..., "--last", "-l", help="What changed and why"),
|
|
234
|
+
next: str = typer.Option(..., "--next", "--nxt", "-n", help="Next smallest valuable step"),
|
|
235
|
+
blockers: str = typer.Option("", "--blockers", "-b", help="Blockers/Risks"),
|
|
236
|
+
json_out: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
237
|
+
):
|
|
238
|
+
"""Update the Context Loop in /context/development.md."""
|
|
239
|
+
root = repo_root()
|
|
240
|
+
context_dir = root / CONTEXT_DIR
|
|
241
|
+
dev_file = context_dir / "development.md"
|
|
242
|
+
|
|
243
|
+
if not dev_file.exists():
|
|
244
|
+
console.print(
|
|
245
|
+
f"[red]✗ {dev_file} not found[/red]\n"
|
|
246
|
+
"Run 'bpsai-pair init' first to set up the project structure"
|
|
247
|
+
)
|
|
248
|
+
raise typer.Exit(1)
|
|
249
|
+
|
|
250
|
+
# Update context
|
|
251
|
+
content = dev_file.read_text()
|
|
252
|
+
import re
|
|
253
|
+
|
|
254
|
+
if overall:
|
|
255
|
+
content = re.sub(r'Overall goal is:.*', f'Overall goal is: {overall}', content)
|
|
256
|
+
content = re.sub(r'Last action was:.*', f'Last action was: {last}', content)
|
|
257
|
+
content = re.sub(r'Next action will be:.*', f'Next action will be: {next}', content)
|
|
258
|
+
if blockers:
|
|
259
|
+
content = re.sub(r'Blockers(/Risks)?:.*', f'Blockers/Risks: {blockers}', content)
|
|
260
|
+
|
|
261
|
+
dev_file.write_text(content)
|
|
262
|
+
|
|
263
|
+
if json_out:
|
|
264
|
+
result = {
|
|
265
|
+
"updated": True,
|
|
266
|
+
"file": str(dev_file.relative_to(root)),
|
|
267
|
+
"context": {
|
|
268
|
+
"overall": overall,
|
|
269
|
+
"last": last,
|
|
270
|
+
"next": next,
|
|
271
|
+
"blockers": blockers
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
print(json.dumps(result, indent=2))
|
|
275
|
+
else:
|
|
276
|
+
console.print("[green]✓[/green] Context Sync updated")
|
|
277
|
+
console.print(f" [dim]Last: {last}[/dim]")
|
|
278
|
+
console.print(f" [dim]Next: {next}[/dim]")
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
# Alias for context-sync
|
|
282
|
+
app.command("sync", hidden=True)(context_sync)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
@app.command()
|
|
286
|
+
def status(
|
|
287
|
+
json_out: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
288
|
+
):
|
|
289
|
+
"""Show current context loop status and recent changes."""
|
|
290
|
+
root = repo_root()
|
|
291
|
+
context_dir = root / CONTEXT_DIR
|
|
292
|
+
dev_file = context_dir / "development.md"
|
|
293
|
+
|
|
294
|
+
# Get current branch
|
|
295
|
+
current_branch = ops.GitOps.current_branch(root)
|
|
296
|
+
is_clean = ops.GitOps.is_clean(root)
|
|
297
|
+
|
|
298
|
+
# Parse context sync
|
|
299
|
+
context_data = {}
|
|
300
|
+
if dev_file.exists():
|
|
301
|
+
content = dev_file.read_text()
|
|
302
|
+
import re
|
|
303
|
+
|
|
304
|
+
# Extract context sync fields
|
|
305
|
+
overall_match = re.search(r'Overall goal is:\s*(.*)', content)
|
|
306
|
+
last_match = re.search(r'Last action was:\s*(.*)', content)
|
|
307
|
+
next_match = re.search(r'Next action will be:\s*(.*)', content)
|
|
308
|
+
blockers_match = re.search(r'Blockers(/Risks)?:\s*(.*)', content)
|
|
309
|
+
phase_match = re.search(r'\*\*Phase:\*\*\s*(.*)', content)
|
|
310
|
+
|
|
311
|
+
context_data = {
|
|
312
|
+
"phase": phase_match.group(1) if phase_match else "Not set",
|
|
313
|
+
"overall": overall_match.group(1) if overall_match else "Not set",
|
|
314
|
+
"last": last_match.group(1) if last_match else "Not set",
|
|
315
|
+
"next": next_match.group(1) if next_match else "Not set",
|
|
316
|
+
"blockers": blockers_match.group(2) if blockers_match else "None"
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
# Check for recent pack
|
|
320
|
+
pack_files = list(root.glob("*.tgz"))
|
|
321
|
+
latest_pack = None
|
|
322
|
+
if pack_files:
|
|
323
|
+
latest_pack = max(pack_files, key=lambda p: p.stat().st_mtime)
|
|
324
|
+
|
|
325
|
+
if json_out:
|
|
326
|
+
age_hours = None
|
|
327
|
+
if latest_pack:
|
|
328
|
+
age_hours = (datetime.now() - datetime.fromtimestamp(latest_pack.stat().st_mtime)).total_seconds() / 3600
|
|
329
|
+
|
|
330
|
+
result = {
|
|
331
|
+
"branch": current_branch,
|
|
332
|
+
"clean": is_clean,
|
|
333
|
+
"context": context_data,
|
|
334
|
+
"latest_pack": str(latest_pack.name) if latest_pack else None,
|
|
335
|
+
"pack_age": age_hours
|
|
336
|
+
}
|
|
337
|
+
print(json.dumps(result, indent=2))
|
|
338
|
+
else:
|
|
339
|
+
# Create a nice table
|
|
340
|
+
table = Table(title="PairCoder Status", show_header=False)
|
|
341
|
+
table.add_column("Field", style="cyan", width=20)
|
|
342
|
+
table.add_column("Value", style="white")
|
|
343
|
+
|
|
344
|
+
# Git status
|
|
345
|
+
table.add_row("Branch", f"[bold]{current_branch}[/bold]")
|
|
346
|
+
table.add_row("Working Tree", "[green]Clean[/green]" if is_clean else "[yellow]Modified[/yellow]")
|
|
347
|
+
|
|
348
|
+
# Context status
|
|
349
|
+
if context_data:
|
|
350
|
+
table.add_row("Phase", context_data["phase"])
|
|
351
|
+
table.add_row("Overall Goal", context_data["overall"][:60] + "..." if len(context_data["overall"]) > 60 else context_data["overall"])
|
|
352
|
+
table.add_row("Last Action", context_data["last"][:60] + "..." if len(context_data["last"]) > 60 else context_data["last"])
|
|
353
|
+
table.add_row("Next Action", context_data["next"][:60] + "..." if len(context_data["next"]) > 60 else context_data["next"])
|
|
354
|
+
if context_data["blockers"] and context_data["blockers"] != "None":
|
|
355
|
+
table.add_row("Blockers", f"[red]{context_data['blockers']}[/red]")
|
|
356
|
+
|
|
357
|
+
# Pack status
|
|
358
|
+
if latest_pack:
|
|
359
|
+
age_hours = (datetime.now() - datetime.fromtimestamp(latest_pack.stat().st_mtime)).total_seconds() / 3600
|
|
360
|
+
age_str = f"{age_hours:.1f} hours ago" if age_hours < 24 else f"{age_hours/24:.1f} days ago"
|
|
361
|
+
table.add_row("Latest Pack", f"{latest_pack.name} ({age_str})")
|
|
362
|
+
|
|
363
|
+
console.print(table)
|
|
364
|
+
|
|
365
|
+
# Suggestions
|
|
366
|
+
if not is_clean:
|
|
367
|
+
console.print("\n[yellow]⚠ Working tree has uncommitted changes[/yellow]")
|
|
368
|
+
console.print("[dim]Consider committing or stashing before creating a pack[/dim]")
|
|
369
|
+
|
|
370
|
+
if not latest_pack or (latest_pack and age_hours and age_hours > 24):
|
|
371
|
+
console.print("\n[dim]Tip: Run 'bpsai-pair pack' to create a fresh context pack[/dim]")
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
@app.command()
|
|
375
|
+
def validate(
|
|
376
|
+
fix: bool = typer.Option(False, "--fix", help="Attempt to fix issues"),
|
|
377
|
+
json_out: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
378
|
+
):
|
|
379
|
+
"""Validate repo structure and context consistency."""
|
|
380
|
+
root = repo_root()
|
|
381
|
+
issues = []
|
|
382
|
+
fixes = []
|
|
383
|
+
|
|
384
|
+
# Check required files
|
|
385
|
+
required_files = [
|
|
386
|
+
Path(CONTEXT_DIR) / "development.md",
|
|
387
|
+
Path(CONTEXT_DIR) / "agents.md",
|
|
388
|
+
Path(".agentpackignore"),
|
|
389
|
+
Path(".editorconfig"),
|
|
390
|
+
Path("CONTRIBUTING.md"),
|
|
391
|
+
]
|
|
392
|
+
|
|
393
|
+
for file_path in required_files:
|
|
394
|
+
full_path = root / file_path
|
|
395
|
+
if not full_path.exists():
|
|
396
|
+
issues.append(f"Missing required file: {file_path}")
|
|
397
|
+
if fix:
|
|
398
|
+
# Create with minimal content
|
|
399
|
+
full_path.parent.mkdir(parents=True, exist_ok=True)
|
|
400
|
+
if file_path.name == "development.md":
|
|
401
|
+
full_path.write_text("# Development Log\n\n## Context Sync (AUTO-UPDATED)\n")
|
|
402
|
+
elif file_path.name == "agents.md":
|
|
403
|
+
full_path.write_text("# Agents Guide\n")
|
|
404
|
+
elif file_path.name == ".agentpackignore":
|
|
405
|
+
full_path.write_text(".git/\n.venv/\n__pycache__/\nnode_modules/\n")
|
|
406
|
+
else:
|
|
407
|
+
full_path.touch()
|
|
408
|
+
fixes.append(f"Created {file_path}")
|
|
409
|
+
|
|
410
|
+
# Check context sync format
|
|
411
|
+
dev_file = root / CONTEXT_DIR / "development.md"
|
|
412
|
+
if dev_file.exists():
|
|
413
|
+
content = dev_file.read_text()
|
|
414
|
+
required_sections = [
|
|
415
|
+
"Overall goal is:",
|
|
416
|
+
"Last action was:",
|
|
417
|
+
"Next action will be:",
|
|
418
|
+
]
|
|
419
|
+
for section in required_sections:
|
|
420
|
+
if section not in content:
|
|
421
|
+
issues.append(f"Missing context sync section: {section}")
|
|
422
|
+
if fix:
|
|
423
|
+
content += f"\n{section} (to be updated)\n"
|
|
424
|
+
dev_file.write_text(content)
|
|
425
|
+
fixes.append(f"Added section: {section}")
|
|
426
|
+
|
|
427
|
+
# Check for uncommitted context changes
|
|
428
|
+
if not ops.GitOps.is_clean(root):
|
|
429
|
+
context_files = ["context/development.md", "context/agents.md"]
|
|
430
|
+
for cf in context_files:
|
|
431
|
+
result = subprocess.run(
|
|
432
|
+
["git", "diff", "--name-only", cf],
|
|
433
|
+
cwd=root,
|
|
434
|
+
capture_output=True,
|
|
435
|
+
text=True
|
|
436
|
+
)
|
|
437
|
+
if result.stdout.strip():
|
|
438
|
+
issues.append(f"Uncommitted changes in {cf}")
|
|
439
|
+
|
|
440
|
+
if json_out:
|
|
441
|
+
result = {
|
|
442
|
+
"valid": len(issues) == 0,
|
|
443
|
+
"issues": issues,
|
|
444
|
+
"fixes_applied": fixes if fix else []
|
|
445
|
+
}
|
|
446
|
+
print(json.dumps(result, indent=2))
|
|
447
|
+
else:
|
|
448
|
+
if issues:
|
|
449
|
+
console.print("[red]✗ Validation failed[/red]")
|
|
450
|
+
console.print("\nIssues found:")
|
|
451
|
+
for issue in issues:
|
|
452
|
+
console.print(f" • {issue}")
|
|
453
|
+
|
|
454
|
+
if fixes:
|
|
455
|
+
console.print("\n[green]Fixed:[/green]")
|
|
456
|
+
for fix_msg in fixes:
|
|
457
|
+
console.print(f" ✓ {fix_msg}")
|
|
458
|
+
elif not fix:
|
|
459
|
+
console.print("\n[dim]Run with --fix to attempt automatic fixes[/dim]")
|
|
460
|
+
else:
|
|
461
|
+
console.print("[green]✓ All validation checks passed[/green]")
|
|
462
|
+
|
|
463
|
+
|
|
464
|
+
@app.command()
|
|
465
|
+
def ci(
|
|
466
|
+
json_out: bool = typer.Option(False, "--json", help="Output in JSON format"),
|
|
467
|
+
):
|
|
468
|
+
"""Run local CI checks (cross-platform)."""
|
|
469
|
+
root = repo_root()
|
|
470
|
+
|
|
471
|
+
with Progress(
|
|
472
|
+
SpinnerColumn(),
|
|
473
|
+
TextColumn("[progress.description]{task.description}"),
|
|
474
|
+
console=console
|
|
475
|
+
) as progress:
|
|
476
|
+
task = progress.add_task("Running CI checks...", total=None)
|
|
477
|
+
|
|
478
|
+
results = ops.LocalCI.run_all(root)
|
|
479
|
+
|
|
480
|
+
progress.update(task, completed=True)
|
|
481
|
+
|
|
482
|
+
if json_out:
|
|
483
|
+
print(json.dumps(results, indent=2))
|
|
484
|
+
else:
|
|
485
|
+
console.print("[bold]Local CI Results[/bold]\n")
|
|
486
|
+
|
|
487
|
+
# Python results
|
|
488
|
+
if results["python"]:
|
|
489
|
+
console.print("[cyan]Python:[/cyan]")
|
|
490
|
+
for check, status in results["python"].items():
|
|
491
|
+
icon = "✓" if "passed" in status else "✗"
|
|
492
|
+
color = "green" if "passed" in status else "yellow"
|
|
493
|
+
console.print(f" [{color}]{icon}[/{color}] {check}: {status}")
|
|
494
|
+
|
|
495
|
+
# Node results
|
|
496
|
+
if results["node"]:
|
|
497
|
+
console.print("\n[cyan]Node.js:[/cyan]")
|
|
498
|
+
for check, status in results["node"].items():
|
|
499
|
+
icon = "✓" if "passed" in status else "✗"
|
|
500
|
+
color = "green" if "passed" in status else "yellow"
|
|
501
|
+
console.print(f" [{color}]{icon}[/{color}] {check}: {status}")
|
|
502
|
+
|
|
503
|
+
if not results["python"] and not results["node"]:
|
|
504
|
+
console.print("[dim]No Python or Node.js project detected[/dim]")
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# Export for entry point
|
|
508
|
+
def run():
|
|
509
|
+
"""Entry point for the CLI."""
|
|
510
|
+
app()
|
|
511
|
+
|
|
512
|
+
|
|
513
|
+
if __name__ == "__main__":
|
|
514
|
+
run()
|