bpsai-pair 0.2.1__tar.gz → 0.2.2__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.

Files changed (54) hide show
  1. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/PKG-INFO +1 -1
  2. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/__init__.py +1 -1
  3. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.agentpackignore +7 -0
  4. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/ops.py +70 -71
  5. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair.egg-info/PKG-INFO +1 -1
  6. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/pyproject.toml +1 -1
  7. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/tests/test_ops.py +27 -0
  8. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/MANIFEST.in +0 -0
  9. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/README.md +0 -0
  10. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/__main__.py +0 -0
  11. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/adapters.py +0 -0
  12. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/cli.py +0 -0
  13. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/config.py +0 -0
  14. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/cookiecutter.json +0 -0
  15. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.editorconfig +0 -0
  16. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  17. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.github/workflows/ci.yml +0 -0
  18. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.github/workflows/project_tree.yml +0 -0
  19. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.gitignore +0 -0
  20. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.gitleaks.toml +0 -0
  21. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/.pre-commit-config.yaml +0 -0
  22. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/AGENTS.md +0 -0
  23. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/CLAUDE.md +0 -0
  24. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/CODEOWNERS +0 -0
  25. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/CONTRIBUTING.md +0 -0
  26. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/SECURITY.md +0 -0
  27. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/agents.md +0 -0
  28. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/development.md +0 -0
  29. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/directory_notes/.gitkeep +0 -0
  30. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/context/project_tree.md +0 -0
  31. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/prompts/deep_research.yml +0 -0
  32. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/prompts/implementation.yml +0 -0
  33. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/prompts/roadmap.yml +0 -0
  34. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/scripts/README.md +0 -0
  35. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/src/.gitkeep +0 -0
  36. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/templates/adr.md +0 -0
  37. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/templates/directory_note.md +0 -0
  38. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/tests/example_contract/README.md +0 -0
  39. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/data/cookiecutter-paircoder/{{cookiecutter.project_slug}}/tests/example_integration/README.md +0 -0
  40. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/init_bundled_cli.py +0 -0
  41. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/jsonio.py +0 -0
  42. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/pyutils.py +0 -0
  43. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair/utils.py +0 -0
  44. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair.egg-info/SOURCES.txt +0 -0
  45. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair.egg-info/dependency_links.txt +0 -0
  46. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair.egg-info/entry_points.txt +0 -0
  47. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair.egg-info/requires.txt +0 -0
  48. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/bpsai_pair.egg-info/top_level.txt +0 -0
  49. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/setup.cfg +0 -0
  50. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/tests/test_cli.py +0 -0
  51. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/tests/test_config.py +0 -0
  52. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/tests/test_context_sync.py +0 -0
  53. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/tests/test_feature_branch_type.py +0 -0
  54. {bpsai_pair-0.2.1 → bpsai_pair-0.2.2}/tests/test_pack_preview.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bpsai-pair
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: CLI for AI pair-coding workflow
5
5
  Author: BPS AI Software
6
6
  Requires-Python: >=3.9
@@ -2,7 +2,7 @@
2
2
  bpsai_pair package
3
3
  """
4
4
 
5
- __version__ = "0.2.1"
5
+ __version__ = "0.2.2"
6
6
 
7
7
  # Make modules available at package level
8
8
  from . import cli
@@ -1,6 +1,8 @@
1
1
  # Default agent pack exclusions
2
2
  .git/
3
3
  .venv/
4
+ venv/
5
+ env/
4
6
  __pycache__/
5
7
  node_modules/
6
8
  dist/
@@ -10,3 +12,8 @@ build/
10
12
  *.tgz
11
13
  *.tar.gz
12
14
  *.zip
15
+ .env
16
+ .env.*
17
+ *.pyc
18
+ .DS_Store
19
+ Thumbs.db
@@ -17,12 +17,12 @@ import json
17
17
 
18
18
  class GitOps:
19
19
  """Git operations helper."""
20
-
20
+
21
21
  @staticmethod
22
22
  def is_repo(path: Path) -> bool:
23
23
  """Check if path is a git repo."""
24
24
  return (path / ".git").exists()
25
-
25
+
26
26
  @staticmethod
27
27
  def is_clean(path: Path) -> bool:
28
28
  """Check if working tree is clean."""
@@ -58,7 +58,7 @@ class GitOps:
58
58
  return True
59
59
  except:
60
60
  return False
61
-
61
+
62
62
  @staticmethod
63
63
  def current_branch(path: Path) -> str:
64
64
  """Get current branch name."""
@@ -69,7 +69,7 @@ class GitOps:
69
69
  text=True
70
70
  )
71
71
  return result.stdout.strip() if result.returncode == 0 else ""
72
-
72
+
73
73
  @staticmethod
74
74
  def create_branch(path: Path, branch: str, from_branch: str = "main") -> bool:
75
75
  """Create and checkout a new branch."""
@@ -81,10 +81,10 @@ class GitOps:
81
81
  )
82
82
  if check.returncode != 0:
83
83
  return False
84
-
84
+
85
85
  # Checkout source branch
86
86
  subprocess.run(["git", "checkout", from_branch], cwd=path, capture_output=True)
87
-
87
+
88
88
  # Pull if upstream exists
89
89
  upstream = subprocess.run(
90
90
  ["git", "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
@@ -93,7 +93,7 @@ class GitOps:
93
93
  )
94
94
  if upstream.returncode == 0:
95
95
  subprocess.run(["git", "pull", "--ff-only"], cwd=path, capture_output=True)
96
-
96
+
97
97
  # Create new branch
98
98
  result = subprocess.run(
99
99
  ["git", "checkout", "-b", branch],
@@ -101,13 +101,13 @@ class GitOps:
101
101
  capture_output=True
102
102
  )
103
103
  return result.returncode == 0
104
-
104
+
105
105
  @staticmethod
106
106
  def add_commit(path: Path, files: List[Path], message: str) -> bool:
107
107
  """Add files and commit."""
108
108
  for f in files:
109
109
  subprocess.run(["git", "add", str(f)], cwd=path, capture_output=True)
110
-
110
+
111
111
  result = subprocess.run(
112
112
  ["git", "commit", "-m", message],
113
113
  cwd=path,
@@ -118,19 +118,19 @@ class GitOps:
118
118
 
119
119
  class ProjectTree:
120
120
  """Generate project tree snapshots."""
121
-
121
+
122
122
  @staticmethod
123
123
  def generate(root: Path, excludes: Optional[Set[str]] = None) -> str:
124
124
  """Generate a tree structure of the project."""
125
125
  if excludes is None:
126
126
  excludes = {
127
- '.git', '.venv', 'venv', '__pycache__',
127
+ '.git', '.venv', 'venv', '__pycache__',
128
128
  'node_modules', 'dist', 'build', '.mypy_cache',
129
129
  '.pytest_cache', '.tox', '*.egg-info', '.DS_Store'
130
130
  }
131
-
131
+
132
132
  tree_lines = []
133
-
133
+
134
134
  def should_skip(path: Path) -> bool:
135
135
  name = path.name
136
136
  for pattern in excludes:
@@ -139,20 +139,20 @@ class ProjectTree:
139
139
  if name == pattern:
140
140
  return True
141
141
  return False
142
-
142
+
143
143
  def walk_dir(dir_path: Path, prefix: str = ""):
144
144
  items = sorted(dir_path.iterdir(), key=lambda x: (x.is_file(), x.name))
145
145
  items = [i for i in items if not should_skip(i)]
146
-
146
+
147
147
  for i, item in enumerate(items):
148
148
  is_last = i == len(items) - 1
149
149
  current = "└── " if is_last else "├── "
150
150
  tree_lines.append(f"{prefix}{current}{item.name}")
151
-
151
+
152
152
  if item.is_dir():
153
153
  extension = " " if is_last else "│ "
154
154
  walk_dir(item, prefix + extension)
155
-
155
+
156
156
  tree_lines.append(".")
157
157
  walk_dir(root)
158
158
  return "\n".join(tree_lines)
@@ -160,7 +160,7 @@ class ProjectTree:
160
160
 
161
161
  class ContextPacker:
162
162
  """Package context files for AI agents."""
163
-
163
+
164
164
  @staticmethod
165
165
  def read_ignore_patterns(ignore_file: Path) -> Set[str]:
166
166
  """Read patterns from .agentpackignore file."""
@@ -170,42 +170,41 @@ class ContextPacker:
170
170
  for line in f:
171
171
  line = line.strip()
172
172
  if line and not line.startswith('#'):
173
- patterns.add(line.rstrip('/'))
173
+ patterns.add(line)
174
174
  else:
175
175
  # Default patterns
176
176
  patterns = {
177
177
  '.git', '.venv', '__pycache__', 'node_modules',
178
- 'dist', 'build', '*.log', '*.bak', '*.tgz',
178
+ 'dist', 'build', '*.log', '*.bak', '*.tgz',
179
179
  '*.tar.gz', '*.zip', '.env*'
180
180
  }
181
181
  return patterns
182
-
182
+
183
183
  @staticmethod
184
184
  def should_exclude(path: Path, patterns: Set[str]) -> bool:
185
185
  """Check if path should be excluded based on patterns."""
186
- path_str = str(path).replace('\\', '/')
187
-
186
+ from pathlib import PurePath
187
+
188
+ p = PurePath(path.as_posix())
189
+
188
190
  for pattern in patterns:
189
- # Handle wildcards
190
- if '*' in pattern:
191
- import fnmatch
192
- if fnmatch.fnmatch(path.name, pattern):
191
+ # Handle directory patterns (ending with /)
192
+ if pattern.endswith('/'):
193
+ dir_pattern = pattern.rstrip('/')
194
+ # Check if this is the directory itself or if it's inside the directory
195
+ if path.is_dir() and p.match(dir_pattern):
196
+ return True
197
+ if any(parent.match(dir_pattern) for parent in p.parents):
193
198
  return True
194
- if fnmatch.fnmatch(path_str, pattern):
199
+ # Handle file/general patterns
200
+ else:
201
+ if p.match(pattern):
195
202
  return True
196
- # Handle directory patterns
197
- elif pattern.endswith('/'):
198
- if path.is_dir() and path.name == pattern[:-1]:
203
+ if any(parent.match(pattern) for parent in p.parents):
199
204
  return True
200
- # Exact match
201
- elif path.name == pattern:
202
- return True
203
- # Path contains pattern
204
- elif pattern in path_str.split('/'):
205
- return True
206
-
205
+
207
206
  return False
208
-
207
+
209
208
  @staticmethod
210
209
  def pack(
211
210
  root: Path,
@@ -217,33 +216,33 @@ class ContextPacker:
217
216
  # Default files to include
218
217
  context_files = [
219
218
  root / "context" / "development.md",
220
- root / "context" / "agents.md",
219
+ root / "context" / "agents.md",
221
220
  root / "context" / "project_tree.md",
222
221
  ]
223
-
222
+
224
223
  # Add directory_notes if it exists
225
224
  dir_notes = root / "context" / "directory_notes"
226
225
  if dir_notes.exists():
227
226
  for note in dir_notes.rglob("*.md"):
228
227
  context_files.append(note)
229
-
228
+
230
229
  # Add extra files
231
230
  if extra_files:
232
231
  for extra in extra_files:
233
232
  extra_path = root / extra
234
233
  if extra_path.exists():
235
234
  context_files.append(extra_path)
236
-
235
+
237
236
  # Filter out non-existent files
238
237
  context_files = [f for f in context_files if f.exists()]
239
-
238
+
240
239
  if dry_run:
241
240
  return context_files
242
-
241
+
243
242
  # Read ignore patterns
244
243
  ignore_file = root / ".agentpackignore"
245
244
  patterns = ContextPacker.read_ignore_patterns(ignore_file)
246
-
245
+
247
246
  # Create tarball
248
247
  with tarfile.open(output, "w:gz") as tar:
249
248
  for file_path in context_files:
@@ -251,13 +250,13 @@ class ContextPacker:
251
250
  if not ContextPacker.should_exclude(file_path, patterns):
252
251
  arcname = file_path.relative_to(root)
253
252
  tar.add(file_path, arcname=str(arcname))
254
-
253
+
255
254
  return context_files
256
255
 
257
256
 
258
257
  class FeatureOps:
259
258
  """Operations for feature branch management."""
260
-
259
+
261
260
  @staticmethod
262
261
  def create_feature(
263
262
  root: Path,
@@ -271,17 +270,17 @@ class FeatureOps:
271
270
  # Check if working tree is clean
272
271
  if not force and not GitOps.is_clean(root):
273
272
  raise ValueError("Working tree not clean. Commit or stash changes, or use --force")
274
-
273
+
275
274
  # Create branch
276
275
  branch_name = f"{branch_type}/{name}"
277
276
  if not GitOps.create_branch(root, branch_name):
278
277
  raise ValueError(f"Failed to create branch {branch_name}")
279
-
278
+
280
279
  # Ensure context directory structure
281
280
  context_dir = root / "context"
282
281
  context_dir.mkdir(exist_ok=True)
283
282
  (context_dir / "directory_notes").mkdir(exist_ok=True)
284
-
283
+
285
284
  # Update or create development.md
286
285
  dev_file = context_dir / "development.md"
287
286
  if not dev_file.exists():
@@ -301,7 +300,7 @@ class FeatureOps:
301
300
  else:
302
301
  # Update existing file
303
302
  content = dev_file.read_text()
304
-
303
+
305
304
  # Update Primary Goal
306
305
  if primary_goal:
307
306
  import re
@@ -315,7 +314,7 @@ class FeatureOps:
315
314
  f'Overall goal is: {primary_goal}',
316
315
  content
317
316
  )
318
-
317
+
319
318
  # Update Phase
320
319
  if phase:
321
320
  content = re.sub(
@@ -328,16 +327,16 @@ class FeatureOps:
328
327
  f'Next action will be: {phase}',
329
328
  content
330
329
  )
331
-
330
+
332
331
  # Update Last action
333
332
  content = re.sub(
334
333
  r'Last action was:.*',
335
334
  f'Last action was: Created feature branch {branch_name}',
336
335
  content
337
336
  )
338
-
337
+
339
338
  dev_file.write_text(content)
340
-
339
+
341
340
  # Create agents.md if missing
342
341
  agents_file = context_dir / "agents.md"
343
342
  if not agents_file.exists():
@@ -360,7 +359,7 @@ This project uses a **Context Loop**. Always keep these fields current:
360
359
  Run `bpsai-pair pack --out agent_pack.tgz` and upload to your session.
361
360
  """
362
361
  agents_file.write_text(agents_content)
363
-
362
+
364
363
  # Generate project tree
365
364
  tree_file = context_dir / "project_tree.md"
366
365
  tree_content = f"""# Project Tree (snapshot)
@@ -371,29 +370,29 @@ _Generated: {datetime.now(timezone.utc).isoformat()}Z_
371
370
  ```
372
371
  """
373
372
  tree_file.write_text(tree_content)
374
-
373
+
375
374
  # Commit changes
376
375
  GitOps.add_commit(
377
376
  root,
378
377
  [dev_file, agents_file, tree_file],
379
378
  f"feat(context): start {branch_name} — Primary Goal: {primary_goal or 'TBD'}"
380
379
  )
381
-
380
+
382
381
  return True
383
382
 
384
383
 
385
384
  class LocalCI:
386
385
  """Cross-platform local CI runner."""
387
-
386
+
388
387
  @staticmethod
389
388
  def run_python_checks(root: Path) -> dict:
390
389
  """Run Python linting, formatting, and tests."""
391
390
  results = {}
392
-
391
+
393
392
  # Check if Python project
394
393
  if not ((root / "pyproject.toml").exists() or (root / "requirements.txt").exists()):
395
394
  return results
396
-
395
+
397
396
  # Try to run ruff
398
397
  try:
399
398
  subprocess.run(["ruff", "format", "--check", "."], cwd=root, check=True)
@@ -401,46 +400,46 @@ class LocalCI:
401
400
  results["ruff"] = "passed"
402
401
  except:
403
402
  results["ruff"] = "failed or not installed"
404
-
403
+
405
404
  # Try to run mypy
406
405
  try:
407
406
  subprocess.run(["mypy", "."], cwd=root, check=True)
408
407
  results["mypy"] = "passed"
409
408
  except:
410
409
  results["mypy"] = "failed or not installed"
411
-
410
+
412
411
  # Try to run pytest
413
412
  try:
414
413
  subprocess.run(["pytest", "-q"], cwd=root, check=True)
415
414
  results["pytest"] = "passed"
416
415
  except:
417
416
  results["pytest"] = "failed or not installed"
418
-
417
+
419
418
  return results
420
-
419
+
421
420
  @staticmethod
422
421
  def run_node_checks(root: Path) -> dict:
423
422
  """Run Node.js linting, formatting, and tests."""
424
423
  results = {}
425
-
424
+
426
425
  if not (root / "package.json").exists():
427
426
  return results
428
-
427
+
429
428
  # Try npm commands
430
429
  try:
431
430
  subprocess.run(["npm", "run", "lint"], cwd=root, check=True)
432
431
  results["eslint"] = "passed"
433
432
  except:
434
433
  results["eslint"] = "failed or not configured"
435
-
434
+
436
435
  try:
437
436
  subprocess.run(["npm", "test"], cwd=root, check=True)
438
437
  results["npm test"] = "passed"
439
438
  except:
440
439
  results["npm test"] = "failed or not configured"
441
-
440
+
442
441
  return results
443
-
442
+
444
443
  @staticmethod
445
444
  def run_all(root: Path) -> dict:
446
445
  """Run all applicable CI checks."""
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: bpsai-pair
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: CLI for AI pair-coding workflow
5
5
  Author: BPS AI Software
6
6
  Requires-Python: >=3.9
@@ -1,7 +1,7 @@
1
1
  [project]
2
2
  readme = "README.md"
3
3
  name = "bpsai-pair"
4
- version = "0.2.1"
4
+ version = "0.2.2"
5
5
  description = "CLI for AI pair-coding workflow"
6
6
  requires-python = ">=3.9"
7
7
  dependencies = [ "typer>=0.12", "rich>=13.7", "pyyaml>=6.0",]
@@ -2,6 +2,7 @@
2
2
  from pathlib import Path
3
3
  import subprocess
4
4
  import sys
5
+ import tarfile
5
6
 
6
7
  import pytest
7
8
 
@@ -68,3 +69,29 @@ def test_context_packer_patterns():
68
69
  assert ops.ContextPacker.should_exclude(Path(".git"), patterns) == True
69
70
  assert ops.ContextPacker.should_exclude(Path("test.log"), patterns) == True
70
71
  assert ops.ContextPacker.should_exclude(Path("main.py"), patterns) == False
72
+
73
+
74
+ def test_pack_respects_directory_ignore(tmp_path):
75
+ """Ensure directory patterns exclude contents from pack."""
76
+ root = tmp_path
77
+
78
+ context_dir = root / "context"
79
+ context_dir.mkdir()
80
+ (context_dir / "development.md").write_text("dev")
81
+ (context_dir / "agents.md").write_text("agents")
82
+ (context_dir / "project_tree.md").write_text("tree")
83
+
84
+ private_dir = root / "private"
85
+ private_dir.mkdir()
86
+ (private_dir / "secret.txt").write_text("shh")
87
+
88
+ (root / ".agentpackignore").write_text("private/\n")
89
+
90
+ output = root / "pack.tgz"
91
+ ops.ContextPacker.pack(root, output, extra_files=["private/secret.txt"])
92
+
93
+ with tarfile.open(output, "r:gz") as tar:
94
+ names = tar.getnames()
95
+
96
+ assert "context/development.md" in names
97
+ assert "private/secret.txt" not in names
File without changes
File without changes
File without changes
File without changes
File without changes