@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.
@@ -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/punks-requirements.md)
4
- - [Scaffolding runbook](./runbooks/punks-cli-scaffolding.md)
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 avoid worktree port collisions
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/punks-requirements.md). This runbook focuses on how the implementation behaves today.
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 it with:
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.0",
3
+ "version": "1.0.2",
4
4
  "description": "DevPunks AI scaffolding CLI",
5
5
  "type": "module",
6
6
  "bin": {
7
- "devpunks": "./dist/index.js",
8
- "dp": "./dist/index.js",
9
- "punks": "./dist/index.js"
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
  },