@punks/cli 1.0.0 → 1.0.2
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.
- package/README.md +38 -4
- package/dist/data/AGENTS.md +0 -6
- package/dist/data/catalog/hooks.ts +26 -0
- package/dist/data/catalog/lint.ts +1 -8
- package/dist/data/catalog/packs.ts +3 -1
- package/dist/data/catalog/skills.ts +8 -1
- package/dist/data/catalog/tools.ts +13 -1
- package/dist/data/subagents/manifest.mjs +119 -0
- package/dist/index.js +2563 -1925
- package/dist/skills/agnostic/cli/dp-cli/SKILL.md +65 -0
- package/dist/skills/agnostic/cli/dp-cli/references/commands.md +33 -0
- package/dist/skills/agnostic/cli/dp-cli/references/post-command-flow.md +45 -0
- package/dist/skills/agnostic/debug/debug-agent/SKILL.md +184 -0
- package/dist/skills/languages/python/async-python-patterns/SKILL.md +735 -0
- package/dist/skills/languages/python/python-code-style/SKILL.md +360 -0
- package/dist/skills/languages/python/python-design-patterns/SKILL.md +433 -0
- package/dist/skills/languages/python/python-project-structure/SKILL.md +252 -0
- package/dist/skills/languages/python/python-testing-patterns/SKILL.md +622 -0
- package/dist/skills/languages/python/python-testing-patterns/references/advanced-patterns.md +411 -0
- package/dist/skills/languages/typescript/quality-types/SKILL.md +93 -0
- package/docs/README.md +9 -5
- package/docs/runbooks/dp-cli-scaffolding.md +58 -5
- package/package.json +8 -4
- package/dist/tsconfig.tsbuildinfo +0 -1
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
# Python Testing Patterns — Advanced Reference
|
|
2
|
+
|
|
3
|
+
Advanced testing patterns including async code, monkeypatching, temporary files, conftest setup, property-based testing, database testing, CI/CD integration, and configuration.
|
|
4
|
+
|
|
5
|
+
## Pattern 6: Testing Async Code
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
# test_async.py
|
|
9
|
+
import pytest
|
|
10
|
+
import asyncio
|
|
11
|
+
|
|
12
|
+
async def fetch_data(url: str) -> dict:
|
|
13
|
+
"""Fetch data asynchronously."""
|
|
14
|
+
await asyncio.sleep(0.1)
|
|
15
|
+
return {"url": url, "data": "result"}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@pytest.mark.asyncio
|
|
19
|
+
async def test_fetch_data():
|
|
20
|
+
"""Test async function."""
|
|
21
|
+
result = await fetch_data("https://api.example.com")
|
|
22
|
+
assert result["url"] == "https://api.example.com"
|
|
23
|
+
assert "data" in result
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@pytest.mark.asyncio
|
|
27
|
+
async def test_concurrent_fetches():
|
|
28
|
+
"""Test concurrent async operations."""
|
|
29
|
+
urls = ["url1", "url2", "url3"]
|
|
30
|
+
tasks = [fetch_data(url) for url in urls]
|
|
31
|
+
results = await asyncio.gather(*tasks)
|
|
32
|
+
|
|
33
|
+
assert len(results) == 3
|
|
34
|
+
assert all("data" in r for r in results)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
async def async_client():
|
|
39
|
+
"""Async fixture."""
|
|
40
|
+
client = {"connected": True}
|
|
41
|
+
yield client
|
|
42
|
+
client["connected"] = False
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@pytest.mark.asyncio
|
|
46
|
+
async def test_with_async_fixture(async_client):
|
|
47
|
+
"""Test using async fixture."""
|
|
48
|
+
assert async_client["connected"] is True
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## Pattern 7: Monkeypatch for Testing
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
# test_environment.py
|
|
55
|
+
import os
|
|
56
|
+
import pytest
|
|
57
|
+
|
|
58
|
+
def get_database_url() -> str:
|
|
59
|
+
"""Get database URL from environment."""
|
|
60
|
+
return os.environ.get("DATABASE_URL", "sqlite:///:memory:")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_database_url_default():
|
|
64
|
+
"""Test default database URL."""
|
|
65
|
+
# Will use actual environment variable if set
|
|
66
|
+
url = get_database_url()
|
|
67
|
+
assert url
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_database_url_custom(monkeypatch):
|
|
71
|
+
"""Test custom database URL with monkeypatch."""
|
|
72
|
+
monkeypatch.setenv("DATABASE_URL", "postgresql://localhost/test")
|
|
73
|
+
assert get_database_url() == "postgresql://localhost/test"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_database_url_not_set(monkeypatch):
|
|
77
|
+
"""Test when env var is not set."""
|
|
78
|
+
monkeypatch.delenv("DATABASE_URL", raising=False)
|
|
79
|
+
assert get_database_url() == "sqlite:///:memory:"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
class Config:
|
|
83
|
+
"""Configuration class."""
|
|
84
|
+
|
|
85
|
+
def __init__(self):
|
|
86
|
+
self.api_key = "production-key"
|
|
87
|
+
|
|
88
|
+
def get_api_key(self):
|
|
89
|
+
return self.api_key
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_monkeypatch_attribute(monkeypatch):
|
|
93
|
+
"""Test monkeypatching object attributes."""
|
|
94
|
+
config = Config()
|
|
95
|
+
monkeypatch.setattr(config, "api_key", "test-key")
|
|
96
|
+
assert config.get_api_key() == "test-key"
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Pattern 8: Temporary Files and Directories
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
# test_file_operations.py
|
|
103
|
+
import pytest
|
|
104
|
+
from pathlib import Path
|
|
105
|
+
|
|
106
|
+
def save_data(filepath: Path, data: str):
|
|
107
|
+
"""Save data to file."""
|
|
108
|
+
filepath.write_text(data)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def load_data(filepath: Path) -> str:
|
|
112
|
+
"""Load data from file."""
|
|
113
|
+
return filepath.read_text()
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_file_operations(tmp_path):
|
|
117
|
+
"""Test file operations with temporary directory."""
|
|
118
|
+
# tmp_path is a pathlib.Path object
|
|
119
|
+
test_file = tmp_path / "test_data.txt"
|
|
120
|
+
|
|
121
|
+
# Save data
|
|
122
|
+
save_data(test_file, "Hello, World!")
|
|
123
|
+
|
|
124
|
+
# Verify file exists
|
|
125
|
+
assert test_file.exists()
|
|
126
|
+
|
|
127
|
+
# Load and verify data
|
|
128
|
+
data = load_data(test_file)
|
|
129
|
+
assert data == "Hello, World!"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_multiple_files(tmp_path):
|
|
133
|
+
"""Test with multiple temporary files."""
|
|
134
|
+
files = {
|
|
135
|
+
"file1.txt": "Content 1",
|
|
136
|
+
"file2.txt": "Content 2",
|
|
137
|
+
"file3.txt": "Content 3"
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
for filename, content in files.items():
|
|
141
|
+
filepath = tmp_path / filename
|
|
142
|
+
save_data(filepath, content)
|
|
143
|
+
|
|
144
|
+
# Verify all files created
|
|
145
|
+
assert len(list(tmp_path.iterdir())) == 3
|
|
146
|
+
|
|
147
|
+
# Verify contents
|
|
148
|
+
for filename, expected_content in files.items():
|
|
149
|
+
filepath = tmp_path / filename
|
|
150
|
+
assert load_data(filepath) == expected_content
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## Pattern 9: Custom Fixtures and Conftest
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
# conftest.py
|
|
157
|
+
"""Shared fixtures for all tests."""
|
|
158
|
+
import pytest
|
|
159
|
+
|
|
160
|
+
@pytest.fixture(scope="session")
|
|
161
|
+
def database_url():
|
|
162
|
+
"""Provide database URL for all tests."""
|
|
163
|
+
return "postgresql://localhost/test_db"
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@pytest.fixture(autouse=True)
|
|
167
|
+
def reset_database(database_url):
|
|
168
|
+
"""Auto-use fixture that runs before each test."""
|
|
169
|
+
# Setup: Clear database
|
|
170
|
+
print(f"Clearing database: {database_url}")
|
|
171
|
+
yield
|
|
172
|
+
# Teardown: Clean up
|
|
173
|
+
print("Test completed")
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.fixture
|
|
177
|
+
def sample_user():
|
|
178
|
+
"""Provide sample user data."""
|
|
179
|
+
return {
|
|
180
|
+
"id": 1,
|
|
181
|
+
"name": "Test User",
|
|
182
|
+
"email": "test@example.com"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@pytest.fixture
|
|
187
|
+
def sample_users():
|
|
188
|
+
"""Provide list of sample users."""
|
|
189
|
+
return [
|
|
190
|
+
{"id": 1, "name": "User 1"},
|
|
191
|
+
{"id": 2, "name": "User 2"},
|
|
192
|
+
{"id": 3, "name": "User 3"},
|
|
193
|
+
]
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Parametrized fixture
|
|
197
|
+
@pytest.fixture(params=["sqlite", "postgresql", "mysql"])
|
|
198
|
+
def db_backend(request):
|
|
199
|
+
"""Fixture that runs tests with different database backends."""
|
|
200
|
+
return request.param
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def test_with_db_backend(db_backend):
|
|
204
|
+
"""This test will run 3 times with different backends."""
|
|
205
|
+
print(f"Testing with {db_backend}")
|
|
206
|
+
assert db_backend in ["sqlite", "postgresql", "mysql"]
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
## Pattern 10: Property-Based Testing
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
# test_properties.py
|
|
213
|
+
from hypothesis import given, strategies as st
|
|
214
|
+
import pytest
|
|
215
|
+
|
|
216
|
+
def reverse_string(s: str) -> str:
|
|
217
|
+
"""Reverse a string."""
|
|
218
|
+
return s[::-1]
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@given(st.text())
|
|
222
|
+
def test_reverse_twice_is_original(s):
|
|
223
|
+
"""Property: reversing twice returns original."""
|
|
224
|
+
assert reverse_string(reverse_string(s)) == s
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
@given(st.text())
|
|
228
|
+
def test_reverse_length(s):
|
|
229
|
+
"""Property: reversed string has same length."""
|
|
230
|
+
assert len(reverse_string(s)) == len(s)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@given(st.integers(), st.integers())
|
|
234
|
+
def test_addition_commutative(a, b):
|
|
235
|
+
"""Property: addition is commutative."""
|
|
236
|
+
assert a + b == b + a
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
@given(st.lists(st.integers()))
|
|
240
|
+
def test_sorted_list_properties(lst):
|
|
241
|
+
"""Property: sorted list is ordered."""
|
|
242
|
+
sorted_lst = sorted(lst)
|
|
243
|
+
|
|
244
|
+
# Same length
|
|
245
|
+
assert len(sorted_lst) == len(lst)
|
|
246
|
+
|
|
247
|
+
# All elements present
|
|
248
|
+
assert set(sorted_lst) == set(lst)
|
|
249
|
+
|
|
250
|
+
# Is ordered
|
|
251
|
+
for i in range(len(sorted_lst) - 1):
|
|
252
|
+
assert sorted_lst[i] <= sorted_lst[i + 1]
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Testing Database Code
|
|
256
|
+
|
|
257
|
+
```python
|
|
258
|
+
# test_database_models.py
|
|
259
|
+
import pytest
|
|
260
|
+
from sqlalchemy import create_engine, Column, Integer, String
|
|
261
|
+
from sqlalchemy.ext.declarative import declarative_base
|
|
262
|
+
from sqlalchemy.orm import sessionmaker, Session
|
|
263
|
+
|
|
264
|
+
Base = declarative_base()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class User(Base):
|
|
268
|
+
"""User model."""
|
|
269
|
+
__tablename__ = "users"
|
|
270
|
+
|
|
271
|
+
id = Column(Integer, primary_key=True)
|
|
272
|
+
name = Column(String(50))
|
|
273
|
+
email = Column(String(100), unique=True)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
@pytest.fixture(scope="function")
|
|
277
|
+
def db_session() -> Session:
|
|
278
|
+
"""Create in-memory database for testing."""
|
|
279
|
+
engine = create_engine("sqlite:///:memory:")
|
|
280
|
+
Base.metadata.create_all(engine)
|
|
281
|
+
|
|
282
|
+
SessionLocal = sessionmaker(bind=engine)
|
|
283
|
+
session = SessionLocal()
|
|
284
|
+
|
|
285
|
+
yield session
|
|
286
|
+
|
|
287
|
+
session.close()
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def test_create_user(db_session):
|
|
291
|
+
"""Test creating a user."""
|
|
292
|
+
user = User(name="Test User", email="test@example.com")
|
|
293
|
+
db_session.add(user)
|
|
294
|
+
db_session.commit()
|
|
295
|
+
|
|
296
|
+
assert user.id is not None
|
|
297
|
+
assert user.name == "Test User"
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
def test_query_user(db_session):
|
|
301
|
+
"""Test querying users."""
|
|
302
|
+
user1 = User(name="User 1", email="user1@example.com")
|
|
303
|
+
user2 = User(name="User 2", email="user2@example.com")
|
|
304
|
+
|
|
305
|
+
db_session.add_all([user1, user2])
|
|
306
|
+
db_session.commit()
|
|
307
|
+
|
|
308
|
+
users = db_session.query(User).all()
|
|
309
|
+
assert len(users) == 2
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def test_unique_email_constraint(db_session):
|
|
313
|
+
"""Test unique email constraint."""
|
|
314
|
+
from sqlalchemy.exc import IntegrityError
|
|
315
|
+
|
|
316
|
+
user1 = User(name="User 1", email="same@example.com")
|
|
317
|
+
user2 = User(name="User 2", email="same@example.com")
|
|
318
|
+
|
|
319
|
+
db_session.add(user1)
|
|
320
|
+
db_session.commit()
|
|
321
|
+
|
|
322
|
+
db_session.add(user2)
|
|
323
|
+
|
|
324
|
+
with pytest.raises(IntegrityError):
|
|
325
|
+
db_session.commit()
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
## CI/CD Integration
|
|
329
|
+
|
|
330
|
+
```yaml
|
|
331
|
+
# .github/workflows/test.yml
|
|
332
|
+
name: Tests
|
|
333
|
+
|
|
334
|
+
on: [push, pull_request]
|
|
335
|
+
|
|
336
|
+
jobs:
|
|
337
|
+
test:
|
|
338
|
+
runs-on: ubuntu-latest
|
|
339
|
+
|
|
340
|
+
strategy:
|
|
341
|
+
matrix:
|
|
342
|
+
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
|
343
|
+
|
|
344
|
+
steps:
|
|
345
|
+
- uses: actions/checkout@v3
|
|
346
|
+
|
|
347
|
+
- name: Set up Python
|
|
348
|
+
uses: actions/setup-python@v4
|
|
349
|
+
with:
|
|
350
|
+
python-version: ${{ matrix.python-version }}
|
|
351
|
+
|
|
352
|
+
- name: Install dependencies
|
|
353
|
+
run: |
|
|
354
|
+
pip install -e ".[dev]"
|
|
355
|
+
pip install pytest pytest-cov
|
|
356
|
+
|
|
357
|
+
- name: Run tests
|
|
358
|
+
run: |
|
|
359
|
+
pytest --cov=myapp --cov-report=xml
|
|
360
|
+
|
|
361
|
+
- name: Upload coverage
|
|
362
|
+
uses: codecov/codecov-action@v3
|
|
363
|
+
with:
|
|
364
|
+
file: ./coverage.xml
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
## Configuration Files
|
|
368
|
+
|
|
369
|
+
```ini
|
|
370
|
+
# pytest.ini
|
|
371
|
+
[pytest]
|
|
372
|
+
testpaths = tests
|
|
373
|
+
python_files = test_*.py
|
|
374
|
+
python_classes = Test*
|
|
375
|
+
python_functions = test_*
|
|
376
|
+
addopts =
|
|
377
|
+
-v
|
|
378
|
+
--strict-markers
|
|
379
|
+
--tb=short
|
|
380
|
+
--cov=myapp
|
|
381
|
+
--cov-report=term-missing
|
|
382
|
+
markers =
|
|
383
|
+
slow: marks tests as slow
|
|
384
|
+
integration: marks integration tests
|
|
385
|
+
unit: marks unit tests
|
|
386
|
+
e2e: marks end-to-end tests
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
```toml
|
|
390
|
+
# pyproject.toml
|
|
391
|
+
[tool.pytest.ini_options]
|
|
392
|
+
testpaths = ["tests"]
|
|
393
|
+
python_files = ["test_*.py"]
|
|
394
|
+
addopts = [
|
|
395
|
+
"-v",
|
|
396
|
+
"--cov=myapp",
|
|
397
|
+
"--cov-report=term-missing",
|
|
398
|
+
]
|
|
399
|
+
|
|
400
|
+
[tool.coverage.run]
|
|
401
|
+
source = ["myapp"]
|
|
402
|
+
omit = ["*/tests/*", "*/migrations/*"]
|
|
403
|
+
|
|
404
|
+
[tool.coverage.report]
|
|
405
|
+
exclude_lines = [
|
|
406
|
+
"pragma: no cover",
|
|
407
|
+
"def __repr__",
|
|
408
|
+
"raise AssertionError",
|
|
409
|
+
"raise NotImplementedError",
|
|
410
|
+
]
|
|
411
|
+
```
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: quality-types
|
|
3
|
+
description: Use when writing or reviewing TypeScript/full-stack code. Encodes principles for type safety (branded types, discriminated unions, end-to-end types), real tests over mocks, OpenTelemetry observability, and picking the right abstractions instead of premature ones.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Writing quality full-stack TypeScript
|
|
7
|
+
|
|
8
|
+
Apply these principles when writing or reviewing TypeScript code.
|
|
9
|
+
|
|
10
|
+
## Make impossible states unrepresentable
|
|
11
|
+
|
|
12
|
+
Use the type system to make invalid states fail at compile time. Fewer reachable states = easier code to read and change.
|
|
13
|
+
|
|
14
|
+
### Branded types
|
|
15
|
+
|
|
16
|
+
Brand primitives so they can't be mixed up. Validate once at the boundary; downstream code trusts the type.
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
type PhoneNumber = string & { __brand: "PhoneNumber" };
|
|
20
|
+
|
|
21
|
+
function parsePhone(input: string): PhoneNumber {
|
|
22
|
+
if (!/^\+?\d{10,15}$/.test(input)) throw new Error(`Invalid: ${input}`);
|
|
23
|
+
return input as PhoneNumber;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function sendSMS(to: PhoneNumber, body: string) {
|
|
27
|
+
/* input is trusted */
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
If the project already uses a library with native branded-type support (e.g. Effect), use their primitives instead of rolling your own.
|
|
32
|
+
|
|
33
|
+
### Discriminated unions over flag bags
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// Don't — invalid combos representable
|
|
37
|
+
type State = { loading: boolean; user?: User; error?: string };
|
|
38
|
+
|
|
39
|
+
// Do — only valid states exist
|
|
40
|
+
type State =
|
|
41
|
+
| { status: "loading" }
|
|
42
|
+
| { status: "success"; user: User }
|
|
43
|
+
| { status: "error"; error: string };
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
## Let the types flow end-to-end
|
|
47
|
+
|
|
48
|
+
DB schema → server → client should share types without manual duplication. Use whatever end-to-end type tool the project already has (tRPC, oRPC, Elysia, TanStack Start). A `users.email` branded as `Email` should arrive on the client still branded.
|
|
49
|
+
|
|
50
|
+
Don't restate types you can derive. Reach for `Pick`, `Omit`, `Parameters`, `ReturnType`, `Awaited`, `typeof` etc. before writing a new interface. For function arguments, infer from the source instead of typing them by hand:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
// Don't — duplicate shape, drifts when the row changes
|
|
54
|
+
type UserSummary = { id: string; email: Email };
|
|
55
|
+
function renderUser(u: UserSummary) {
|
|
56
|
+
/* ... */
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Do — derive from the source of truth
|
|
60
|
+
type User = Awaited<ReturnType<typeof db.query.users.findFirst>>;
|
|
61
|
+
function renderUser(u: Pick<User, "id" | "email">) {
|
|
62
|
+
/* ... */
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
## Pass objects, not positional args
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// Don't — swap two args, still compiles
|
|
70
|
+
sendEmail("Welcome!", "Hi there");
|
|
71
|
+
// Do — order-independent, self-documenting
|
|
72
|
+
sendEmail({ to: "alice@x.com", body: "Hi there" });
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Skip on hot perf-critical paths; use elsewhere by default.
|
|
76
|
+
|
|
77
|
+
## Standard Schema for shared validation
|
|
78
|
+
|
|
79
|
+
For libraries or code that doesn't want to pick a validator, accept `StandardSchemaV1<unknown, T>`.
|
|
80
|
+
|
|
81
|
+
## Tests as real as possible
|
|
82
|
+
|
|
83
|
+
Don't mock things you can run. Spin up real services:
|
|
84
|
+
|
|
85
|
+
- LocalStack for AWS
|
|
86
|
+
- Miniflare for Cloudflare Workers
|
|
87
|
+
- Real Postgres/SQLite (e.g. `bun:sqlite`), not a mock DB
|
|
88
|
+
|
|
89
|
+
Mock only third-party services that have no test environment.
|
|
90
|
+
|
|
91
|
+
## OpenTelemetry, not print logging
|
|
92
|
+
|
|
93
|
+
When adding observability, instrument with OTel spans. The setup cost pays back the first time a user sends a request ID and you can answer instead of guess.
|
package/docs/README.md
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
1
|
# CLI Docs
|
|
2
2
|
|
|
3
|
-
- [Requirements](./reference/
|
|
4
|
-
- [Scaffolding runbook](./runbooks/
|
|
3
|
+
- [Requirements](./reference/dp-requirements.md)
|
|
4
|
+
- [Scaffolding runbook](./runbooks/dp-cli-scaffolding.md)
|
|
5
5
|
|
|
6
6
|
Implementation notes:
|
|
7
7
|
|
|
8
|
-
- canonical scaffold metadata and shared assets live in `src/data/`
|
|
8
|
+
- canonical bundled scaffold metadata and shared assets live in `src/data/`
|
|
9
|
+
- runtime scaffold content resolves from a `Baseline`: latest remote stable by default, bundled fallback when offline or forced with `--baseline bundled`
|
|
9
10
|
- pack-owned lint asset metadata lives in `src/data/catalog/lint.ts`
|
|
10
11
|
- distributed skill assets live in `skills/`
|
|
11
12
|
- runtime projection/writing logic lives in `src/scaffold/`
|
|
12
13
|
- `punks update` refreshes scaffold-managed assets from `.devpunks/scaffold-manifest.json`
|
|
14
|
+
- CLI startup checks npm's `latest` dist-tag for `@punks/cli` and prints a self-update notice when the installed CLI is behind. Set `DP_NO_UPDATE_CHECK=1` to skip the check or `DP_UPDATE_TAG=next` to compare against another dist-tag.
|
|
15
|
+
- baseline releases use `baseline/stable/*` GitHub release tags, separate from npm executable tags such as `v1.0.1`
|
|
13
16
|
- shared neutral hook and sync assets live in `src/data/hooks/` and `src/data/scripts/`
|
|
14
|
-
- scaffolded required tools always include `portless` so generated guidance can standardize local dev origins and
|
|
17
|
+
- scaffolded required tools always include `portless` and `skills` so generated guidance can standardize local dev origins and keep skill entrypoints up to date
|
|
18
|
+
- the default debug pack scaffolds the local `debug-agent` skill and installs/verifies the `debug-agent` CLI without running its agent-install wizard
|
|
15
19
|
- scaffolded repos keep project-local skills in `.agents/skills/`; only `.claude/skills` is a compatibility symlink mirror
|
|
16
20
|
- React scaffold surfaces include `async-react-patterns` alongside the existing React composition, structure, and Vercel guidance so agents avoid outdated manual async state patterns.
|
|
17
21
|
- Next.js detection implies the React pack even when `react` / `react-dom` are not directly listed in scanned manifests.
|
|
18
22
|
- Frontend surface detection scaffolds `frontend-domain-structure` with the frontend pack when React or Next.js is detected.
|
|
19
23
|
- Backend surface detection scaffolds `backend-domain-structure` and `backend-recoverable-actions` when backend framework/data/auth packages are detected, or when workspace names/paths clearly identify API, backend, server, or service packages.
|
|
20
|
-
- Non-surface agnostic skill groups are mandatory scaffold packs (`docs`, `planning`, `quality`, `research`, `requirements`, `subagents`); `frontend` and `backend` remain detection-driven.
|
|
24
|
+
- Non-surface agnostic skill groups are mandatory scaffold packs (`debug`, `docs`, `planning`, `quality`, `research`, `requirements`, `subagents`); `frontend` and `backend` remain detection-driven.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
This repository owns the `punks` command used to scaffold AI operating context across the project lifecycle.
|
|
4
4
|
|
|
5
|
-
For the durable product contract, see [requirements](../reference/
|
|
5
|
+
For the durable product contract, see [requirements](../reference/dp-requirements.md). This runbook focuses on how the implementation behaves today.
|
|
6
6
|
|
|
7
7
|
## Current Command Surface
|
|
8
8
|
|
|
@@ -72,7 +72,7 @@ Current scope:
|
|
|
72
72
|
- treat React, Next.js, and TanStack Query as separate frontend capability layers; React is the plain framework pack triggered by `react` / `react-dom` or implied by `next`, and pulls in `async-react-patterns` to prevent outdated manual async state modeling
|
|
73
73
|
- include the `frontend` agnostic surface pack only when React or Next.js is detected; it carries product UI, browser validation, and frontend domain-structure guidance
|
|
74
74
|
- include the `backend` agnostic surface pack when backend framework/data/auth packages are detected (`elysia`, `@trpc/*`, `drizzle-orm`, `drizzle-kit`, `better-auth`) or when package names/manifest paths clearly mark an API/backend/server/service workspace
|
|
75
|
-
- keep all non-surface agnostic skill packs mandatory (`docs`, `planning`, `quality`, `research`, `requirements`, `subagents`)
|
|
75
|
+
- keep all non-surface agnostic skill packs mandatory (`debug`, `docs`, `planning`, `quality`, `research`, `requirements`, `subagents`)
|
|
76
76
|
- detect monorepo signals from `pnpm-workspace.yaml`, `turbo.json`, root workspaces, and workspace package count
|
|
77
77
|
- resolve predefined packs from those facts
|
|
78
78
|
- run a branded interactive confirmation flow
|
|
@@ -81,11 +81,17 @@ Current scope:
|
|
|
81
81
|
- support `-o, --output <path>` to target a different output location
|
|
82
82
|
- support `--json` to print the full machine-readable scaffold payload on demand
|
|
83
83
|
- support `--repo-shape auto|monorepo|single` to override repo-shape detection when needed
|
|
84
|
+
- support `--baseline stable|bundled` to choose the scaffold content baseline
|
|
85
|
+
- support `--refresh-baseline` to refetch the stable remote baseline instead of reusing the cache
|
|
84
86
|
- copy shared prompt/manifest assets, selected skill directories, shared hook files, harness sync scripts, and subagent templates
|
|
85
87
|
- generate harness-native agent/config/mirror surfaces for `.claude`, `.codex`, `.cursor`, and `.opencode`
|
|
86
88
|
- emit prompt specs, selected lint asset specs, subagent manifest specs, a required-tools manifest, and an agent handoff file
|
|
87
89
|
- write a paste-ready next-agent prompt file and try to copy it to the clipboard automatically
|
|
88
90
|
- include `portless` in the required tools manifest so follow-up agents can standardize local dev URLs across worktrees and avoid raw port collisions
|
|
91
|
+
- include `skills` in the required tools manifest so follow-up agents can install/update public skill entrypoints such as `dp-cli`
|
|
92
|
+
- include `debug-agent` through the default debug pack and install/verify the `debug-agent` CLI without running `debug-agent init`, because the CLI already scaffolds the project-local skill
|
|
93
|
+
- select language packs separately from framework packs; TypeScript is selected from a `typescript` package dependency or nested `.ts` / `.tsx` files, and Python is selected from nested `.py` files, while ignoring root config files plus vendor, virtualenv, scaffold, docs, examples, scripts, `opensrc`, cache, and build output
|
|
94
|
+
- seed Python subagent templates that combine the Python language skills into `python-app`, `python-async`, and `python-testing` specialists
|
|
89
95
|
|
|
90
96
|
If `-o, --output` points at a path that does not exist yet, `punks scaffold setup` creates it before writing the generated files.
|
|
91
97
|
|
|
@@ -146,13 +152,60 @@ Current scope:
|
|
|
146
152
|
- supports `--check` to write nothing and exit nonzero when managed updates are available
|
|
147
153
|
- supports `--write` to apply without prompting
|
|
148
154
|
- supports `--json` to print the machine-readable update result
|
|
155
|
+
- supports `--baseline stable|bundled` and `--refresh-baseline`, matching setup behavior
|
|
149
156
|
- restores missing managed files that still belong to the current scaffold output
|
|
150
157
|
- reports stale managed files from the old manifest but does not delete them
|
|
151
158
|
- installs/ensures required tools when applying updates, matching setup behavior
|
|
152
159
|
|
|
153
160
|
`punks update` updates scaffold-managed assets such as `.agents/AGENTS.md`, selected skills, prompt/lint/subagent specs, hook/script files, harness surfaces, required-tools metadata, handoff prompts, and `.devpunks/scaffold-manifest.json`.
|
|
154
161
|
|
|
155
|
-
It does **not** silently accept a newly resolved pack set. When current package manifests resolve to different packs than the recorded manifest, update reports pack drift and prints an operator prompt for an agent to rerun `punks scaffold setup` intentionally.
|
|
162
|
+
It does **not** silently accept a newly resolved pack set. When current package manifests resolve to different packs than the recorded manifest, update reports pack drift and prints an operator prompt for an agent to rerun `punks scaffold setup` intentionally. It also reports baseline drift when the active baseline version differs from the version recorded in `.devpunks/scaffold-manifest.json`.
|
|
163
|
+
|
|
164
|
+
## Dynamic Baselines
|
|
165
|
+
|
|
166
|
+
The npm package is the executable engine. Scaffoldable content resolves through a baseline:
|
|
167
|
+
|
|
168
|
+
- default: latest remote stable baseline from `wearedevpunks/cli` GitHub releases
|
|
169
|
+
- fallback: bundled baseline shipped in npm when offline, invalid, or unavailable
|
|
170
|
+
- forced fallback: `--baseline bundled` or `DP_BASELINE=bundled`
|
|
171
|
+
- forced refresh: `--refresh-baseline` or `DP_BASELINE_REFRESH=1`
|
|
172
|
+
- local testing: `DP_BASELINE_URL=file:///absolute/path/to/scaffold-baseline`
|
|
173
|
+
|
|
174
|
+
Baseline artifacts include dynamic scaffoldable data:
|
|
175
|
+
|
|
176
|
+
- `data/catalog/packs.json`
|
|
177
|
+
- `data/catalog/skills.json`
|
|
178
|
+
- `data/catalog/tools.json`
|
|
179
|
+
- `data/catalog/hooks.json`
|
|
180
|
+
- `data/catalog/lint-assets.json`
|
|
181
|
+
- `data/hooks/`
|
|
182
|
+
- `data/scripts/`
|
|
183
|
+
- `data/subagents/`
|
|
184
|
+
- `skills/`
|
|
185
|
+
|
|
186
|
+
New packs in a baseline release appear in CLI pack resolution/output without an npm publish when they use the existing pack fields: `id`, `category`, `triggerPackages`, `skills`, `hooks`, `lintAssets`, `promptSurfaces`, and `promptDetails`. Brand-new detection algorithms still require an npm CLI release.
|
|
187
|
+
|
|
188
|
+
Baseline release tags must never reuse npm semver tags. Use the `baseline/stable/*` namespace for baseline content releases and keep npm executable releases on `v*` tags.
|
|
189
|
+
|
|
190
|
+
Build and publish the stable baseline release assets with:
|
|
191
|
+
|
|
192
|
+
```bash
|
|
193
|
+
bun run baseline:build
|
|
194
|
+
bun run baseline:publish
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`baseline:build` creates `dist/baseline/scaffold-baseline.json` and `dist/baseline/scaffold-baseline.tgz`. `baseline:publish` creates a GitHub release tag under `baseline/stable/*` and uploads both assets.
|
|
198
|
+
|
|
199
|
+
## CLI Self-Update Detection
|
|
200
|
+
|
|
201
|
+
The CLI also performs a best-effort package self-update check on normal command startup. This is separate from `punks update`, which updates scaffold-managed repo assets.
|
|
202
|
+
|
|
203
|
+
- checks `https://registry.npmjs.org/%40punks%2Fcli/latest`
|
|
204
|
+
- compares that dist-tag version with the bundled CLI version
|
|
205
|
+
- prints `npm install -g @punks/cli@latest` only when the registry version is newer
|
|
206
|
+
- silently skips the notice when npm is unreachable, the registry response is invalid, or the command is `--help` / `--version`
|
|
207
|
+
- skips in CI and when `DP_NO_UPDATE_CHECK=1`
|
|
208
|
+
- supports `DP_UPDATE_TAG=next` for canary/operator testing against another dist-tag
|
|
156
209
|
|
|
157
210
|
To test the built command locally without preparing another repo first, use the committed fixtures:
|
|
158
211
|
|
|
@@ -167,7 +220,7 @@ Those fixtures include minimal `.devpunks/scaffold-manifest.json` files and inte
|
|
|
167
220
|
|
|
168
221
|
## Content Base
|
|
169
222
|
|
|
170
|
-
The standalone CLI repo is the canonical source of scaffolded content.
|
|
223
|
+
The standalone CLI repo is the canonical source of scaffolded baseline content.
|
|
171
224
|
|
|
172
225
|
Bundled source-of-truth assets live under:
|
|
173
226
|
|
|
@@ -178,7 +231,7 @@ Bundled source-of-truth assets live under:
|
|
|
178
231
|
- `src/data/catalog/`
|
|
179
232
|
- `src/content/stage-scaffold.ts`
|
|
180
233
|
|
|
181
|
-
`skills/` is refreshed from the public [wearedevpunks/skills](https://github.com/wearedevpunks/skills) repo through a shallow local cache clone. Refresh
|
|
234
|
+
`skills/` is refreshed from the public [wearedevpunks/skills](https://github.com/wearedevpunks/skills) repo through a shallow local cache clone. That public repo remains the Vercel skills CLI interface, not the full scaffold baseline source. Refresh local skills with:
|
|
182
235
|
|
|
183
236
|
```bash
|
|
184
237
|
bun run sync:skills
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@punks/cli",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "DevPunks AI scaffolding CLI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
|
-
"devpunks": "
|
|
8
|
-
"dp": "
|
|
9
|
-
"punks": "
|
|
7
|
+
"devpunks": "dist/index.js",
|
|
8
|
+
"dp": "dist/index.js",
|
|
9
|
+
"punks": "dist/index.js"
|
|
10
10
|
},
|
|
11
11
|
"files": [
|
|
12
12
|
"dist",
|
|
@@ -15,8 +15,12 @@
|
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
17
|
"build": "node ./scripts/build-dist.mjs",
|
|
18
|
+
"baseline:build": "bun ./scripts/build-baseline.mjs",
|
|
19
|
+
"baseline:publish": "node ./scripts/publish-baseline.mjs",
|
|
18
20
|
"check-types": "tsc --noEmit",
|
|
19
21
|
"dev": "bun run ./src/index.ts scaffold",
|
|
22
|
+
"local": "bun run build && bun run ./dist/index.js",
|
|
23
|
+
"release:publish": "node ./scripts/publish-release.mjs",
|
|
20
24
|
"sync:skills": "node ./scripts/sync-skills-repo.mjs",
|
|
21
25
|
"test": "vitest run --config vitest.config.ts"
|
|
22
26
|
},
|