@tsfpp/agents 1.3.3 → 1.3.5
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/CHANGELOG.md +17 -0
- package/copilot/agents/tsfpp-audit.agent.md +182 -36
- package/copilot/agents/tsfpp-backfill-tests.agent.md +48 -3
- package/copilot/agents/tsfpp-refactor-engineer.agent.md +2 -0
- package/copilot/agents/tsfpp-tdd.agent.md +59 -6
- package/init.mjs +209 -197
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -10,6 +10,23 @@ Versioning follows [Semantic Versioning](https://semver.org/).
|
|
|
10
10
|
|
|
11
11
|
## [Unreleased]
|
|
12
12
|
|
|
13
|
+
## [1.3.5] - 2026-05-18
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
|
|
17
|
+
- Fixed an `init.mjs` regression by restoring the main-clause wrapper to prevent dangling awaits and improve idempotent handling of existing files.
|
|
18
|
+
- Updated `tdd` and `backfill-tests` agents to include a standards-enforcing self-review step.
|
|
19
|
+
- Expanded `audit` agent checklist coverage with an extensive backfill of checklist items.
|
|
20
|
+
|
|
21
|
+
## [1.3.4] - 2026-05-18
|
|
22
|
+
|
|
23
|
+
### Changed
|
|
24
|
+
|
|
25
|
+
- Updated `audit`, `backfill-tests`, and `refactor-engineer` so they do not hand off too easily when workable slices of implementation are readily available.
|
|
26
|
+
- Updated audit/backfill log filename convention to include both time and focus.
|
|
27
|
+
- Improved handover flow from `backfill-tests` to `audit`.
|
|
28
|
+
- Added additional summary domains to report on in the audit log.
|
|
29
|
+
|
|
13
30
|
## [1.3.3] - 2026-05-17
|
|
14
31
|
|
|
15
32
|
### Added
|
|
@@ -39,13 +39,13 @@ If any referenced file is missing, stop immediately and report the path. Do not
|
|
|
39
39
|
|
|
40
40
|
## Session start
|
|
41
41
|
|
|
42
|
-
If
|
|
42
|
+
If `target` and `focus` are present in the message (e.g. `target=src/ focus=test`) or can be inferred from handoff context (e.g. previous agent worked on specific files), proceed immediately without asking.
|
|
43
|
+
|
|
44
|
+
If and only if either is missing and cannot be inferred, ask once:
|
|
43
45
|
|
|
44
46
|
> **Target** — path, package name, or layer to audit (e.g. `src/domain`, `@tsfpp/prelude`, `api layer`)?
|
|
45
47
|
> **Focus** — `all` · `types` · `boundary` · `complexity` · `loc` · `annotations` · `security` · `react` · `data` · `prelude` · `test` · or comma-separated combination?
|
|
46
48
|
|
|
47
|
-
Do not proceed until both are confirmed.
|
|
48
|
-
|
|
49
49
|
---
|
|
50
50
|
|
|
51
51
|
## Mission
|
|
@@ -59,7 +59,7 @@ Systematically inspect the target for TSF++ violations. Slice the work into mana
|
|
|
59
59
|
Create the report file **before starting any inspection**:
|
|
60
60
|
|
|
61
61
|
```
|
|
62
|
-
docs/audits/<target-slug>-<YYYYMMDD-HHmm>.md
|
|
62
|
+
docs/audits/<target-slug>-<focus>-<YYYYMMDD-HHmm>.md
|
|
63
63
|
```
|
|
64
64
|
|
|
65
65
|
Use this template exactly:
|
|
@@ -79,13 +79,20 @@ Use this template exactly:
|
|
|
79
79
|
|
|
80
80
|
> Fill in after all slices are complete.
|
|
81
81
|
|
|
82
|
-
| Category | Violations | Deviations | Passed |
|
|
83
|
-
|
|
84
|
-
| Types | — | — | — |
|
|
85
|
-
| Purity | — | — | — |
|
|
86
|
-
| Boundary | — | — | — |
|
|
87
|
-
| Annotations | — | — | — |
|
|
88
|
-
| Complexity | — | — | — |
|
|
82
|
+
| Category | Violations | Deviations | Passed | N/A |
|
|
83
|
+
|-------------|-----------|------------|--------|-----|
|
|
84
|
+
| Types | — | — | — | — |
|
|
85
|
+
| Purity | — | — | — | — |
|
|
86
|
+
| Boundary | — | — | — | — |
|
|
87
|
+
| Annotations | — | — | — | — |
|
|
88
|
+
| Complexity | — | — | — | — |
|
|
89
|
+
| Prelude | — | — | — | — |
|
|
90
|
+
| React | — | — | — | — |
|
|
91
|
+
| Data | — | — | — | — |
|
|
92
|
+
| Security | — | — | — | — |
|
|
93
|
+
| Tests | — | — | — | — |
|
|
94
|
+
|
|
95
|
+
_N/A — focus not applicable to this target (e.g. React row when no `.tsx` files in scope)_
|
|
89
96
|
|
|
90
97
|
---
|
|
91
98
|
|
|
@@ -122,17 +129,48 @@ Append each completed slice to the report:
|
|
|
122
129
|
|
|
123
130
|
#### Checklist
|
|
124
131
|
|
|
125
|
-
|
|
126
|
-
- [ ] 1.
|
|
127
|
-
- [
|
|
128
|
-
- [
|
|
129
|
-
- [
|
|
130
|
-
- [
|
|
131
|
-
- [ ]
|
|
132
|
-
- [
|
|
133
|
-
- [
|
|
134
|
-
- [
|
|
135
|
-
- [
|
|
132
|
+
**Types and ADTs (§1)**
|
|
133
|
+
- [ ] 1.1 — Sum types modelled as tagged discriminated union with literal discriminant
|
|
134
|
+
- [ ] 1.2 — Exhaustive `switch` ends in `default: return absurd(x)`
|
|
135
|
+
- [ ] 1.3 — Nominal distinctions via branded types; only smart constructors (`mk*`, `from*`, `as*`) cast with `as`
|
|
136
|
+
- [ ] 1.4 — No bare `interface` (or `// DEVIATION(1.4): <reason>` present)
|
|
137
|
+
- [ ] 1.5 — No `any`; `unknown` used at I/O boundaries, narrowed in scope
|
|
138
|
+
- [ ] 1.6 — No `!`; no `as` outside smart constructor bodies
|
|
139
|
+
- [ ] 1.8 — No `enum`; use string literal unions or `as const`
|
|
140
|
+
- [ ] 1.9 — No `class` · `this` · `new` · `instanceof` · `namespace`
|
|
141
|
+
- [ ] 1.11 — Prelude ADT discriminants accessed via exported guards only (`isOk`, `isSome`)
|
|
142
|
+
- [ ] 1.12 — Discriminant convention: `_tag` for prelude ADTs · `kind` for domain ADTs
|
|
143
|
+
|
|
144
|
+
**Immutability (§2–§3)**
|
|
145
|
+
- [ ] 2.1 — `const` for every binding; no `let` / `var`
|
|
146
|
+
- [ ] 2.2 — `ReadonlyArray<T>` everywhere; no mutable arrays
|
|
147
|
+
- [ ] 2.3 — No mutating methods (`push`, `pop`, `splice`, `sort`, `reverse`, `fill`, `copyWithin`)
|
|
148
|
+
- [ ] 2.4 — No property assignment or `delete` after construction
|
|
149
|
+
- [ ] 2.5 — `as const` for literal narrowing and config tables
|
|
150
|
+
- [ ] 3.x — `readonly` on every record field
|
|
151
|
+
|
|
152
|
+
**Control flow (§4)**
|
|
153
|
+
- [ ] 4.1 — Every sum-type `switch` is exhaustive; `default: return absurd(x)`
|
|
154
|
+
- [ ] 4.5 — No truthiness checks on non-booleans (`if (str)`, `if (value)`)
|
|
155
|
+
- [ ] No `for` · `while` · `do..while`; use `map`, `filter`, `reduce`, `pipe`, or traversal combinators
|
|
156
|
+
|
|
157
|
+
**Pipelines and effects (§5–§6)**
|
|
158
|
+
- [ ] 5.1 — Pipelines via `pipe` from `@tsfpp/prelude`
|
|
159
|
+
- [ ] 6.2 — `throw` only at adapter boundaries; core uses `Result<T, E>`
|
|
160
|
+
- [ ] 6.3 — No `null`/`undefined` propagation; use `Option<A>`
|
|
161
|
+
- [ ] 6.6 — `Promise.allSettled` over `Promise.all` when partial failure is meaningful
|
|
162
|
+
|
|
163
|
+
**Annotations (§7)**
|
|
164
|
+
- [ ] 7.x — JSDoc on every exported symbol (`@param`, `@returns`; `@law` on combinators)
|
|
165
|
+
|
|
166
|
+
**Boundary and imports (§8–§9)**
|
|
167
|
+
- [ ] 8.4 — Parse, don't validate: `unknown` converted to domain types at the boundary
|
|
168
|
+
- [ ] 9.x — No `import from 'ramda'`; use `@tsfpp/prelude`
|
|
169
|
+
|
|
170
|
+
**Size limits (§11)**
|
|
171
|
+
- [ ] 11.1 — One type / one responsibility per file
|
|
172
|
+
- [ ] 11.2 — File ≤ 400 LOC (800 absolute max with deviation)
|
|
173
|
+
- [ ] Function body ≤ 40 lines · cyclomatic complexity ≤ 10 · nesting ≤ 4 · arity ≤ 3
|
|
136
174
|
|
|
137
175
|
#### Deviation register
|
|
138
176
|
|
|
@@ -146,29 +184,134 @@ Append each completed slice to the report:
|
|
|
146
184
|
## Focus-specific rule sets
|
|
147
185
|
|
|
148
186
|
### `types`
|
|
149
|
-
|
|
187
|
+
Checklist:
|
|
188
|
+
|
|
189
|
+
- [ ] 1.1 — Sum types are tagged discriminated unions with a literal discriminant field
|
|
190
|
+
- [ ] 1.2 — Every exhaustive `switch` ends in `default: return absurd(x)`
|
|
191
|
+
- [ ] 1.3 — Domain primitives use branded types; only smart constructors may cast with `as`
|
|
192
|
+
- [ ] 1.4 — No bare `interface`; `type` aliases used throughout (or DEVIATION documented)
|
|
193
|
+
- [ ] 1.5 — No `any`; `unknown` at I/O boundaries, narrowed before use
|
|
194
|
+
- [ ] 1.6 — No `!`; no `as` outside smart constructor bodies
|
|
195
|
+
- [ ] 1.8 — No `enum`; string literal unions or `as const` objects used instead
|
|
196
|
+
- [ ] 1.9 — No `class` · `this` · `new` · `instanceof` · `namespace`
|
|
197
|
+
- [ ] 1.11 — Prelude ADTs accessed via exported guards only (`isOk`, `isSome`, `isNone`, `isErr`)
|
|
198
|
+
- [ ] 1.12 — `_tag` on prelude ADTs · `kind` on domain ADTs — no cross-contamination
|
|
199
|
+
- [ ] 2.2 — `ReadonlyArray<T>` throughout; no mutable arrays
|
|
200
|
+
- [ ] 3.x — Every record field is `readonly`
|
|
201
|
+
- [ ] 6.3 — No `null` / `undefined` in domain types; `Option<A>` used instead
|
|
202
|
+
- [ ] Smart constructors cover all valid input cases; invalid inputs return `None` or `Err`
|
|
203
|
+
- [ ] No missing variant in sum-type definitions relative to the domain model
|
|
150
204
|
|
|
151
205
|
### `boundary`
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
206
|
+
Full reference: `node_modules/@tsfpp/standard/spec/API_CODING_STANDARD.md`
|
|
207
|
+
|
|
208
|
+
**Request handling**
|
|
209
|
+
- [ ] `extractContext(req, routeTemplate)` called first in every handler
|
|
210
|
+
- [ ] `routeTemplate` is the parameterised path (`/v1/tracks/:id`), never the resolved URL
|
|
211
|
+
- [ ] All input validated with Zod `safeParse` at the boundary; never `parse` (throws)
|
|
212
|
+
- [ ] `fromZodError(zodError)` used to lift Zod errors into `ValidationError`
|
|
213
|
+
- [ ] No unvalidated `req.json()` passed into domain or use-case code
|
|
214
|
+
|
|
215
|
+
**Error handling**
|
|
216
|
+
- [ ] `apiErrorToResponse(error, ctx)` used for all error paths; no manual `new Response()`
|
|
217
|
+
- [ ] `dependency` and `internal` `ApiError` variants: `cause` logged before calling mapper
|
|
218
|
+
- [ ] No raw `throw` in handlers; all errors returned as `Result<T, ApiError>`
|
|
219
|
+
- [ ] `fromZodError` used, not manual `ValidationError` construction for Zod errors
|
|
220
|
+
|
|
221
|
+
**Response builders**
|
|
222
|
+
- [ ] `okResponse` / `createdResponse` / `noContentResponse` / `acceptedResponse` used — no `new Response()`
|
|
223
|
+
- [ ] `createdResponse` sets `Location` header with the resource URL
|
|
224
|
+
- [ ] `acceptedResponse` used for async / LRO operations; polling URL provided
|
|
225
|
+
- [ ] `bulkResponse` + `mkBulkOkItem` / `mkBulkErrorItem` for batch endpoints
|
|
226
|
+
|
|
227
|
+
**Handler architecture**
|
|
228
|
+
- [ ] Handler shape: parse → domain map → use-case → response map (nothing else)
|
|
229
|
+
- [ ] No domain logic or business rules in handler body
|
|
230
|
+
- [ ] No direct DB or infrastructure access in handler body
|
|
231
|
+
|
|
232
|
+
**Security headers**
|
|
233
|
+
- [ ] `baselineSecurityHeaders` merged into every response
|
|
234
|
+
- [ ] `corsHeaders` used; never reflects `Origin` blindly; `allowedOrigins` from config
|
|
235
|
+
- [ ] `rateLimitHeaders` attached to all responses on rate-limited endpoints, not just 429s
|
|
236
|
+
|
|
237
|
+
**Middleware**
|
|
238
|
+
- [ ] Middleware composed via `pipe`, outermost-last
|
|
239
|
+
- [ ] `withRequestLog` is always the outermost wrapper
|
|
240
|
+
- [ ] `withIdempotency` present on all state-mutating operations
|
|
241
|
+
|
|
242
|
+
**Pagination**
|
|
243
|
+
- [ ] `parsePaginationQuery` used; result checked for `Err` before use
|
|
244
|
+
- [ ] `mkPaginated` used; `totalCount` is `null` unless precomputed
|
|
245
|
+
- [ ] `encodeCursor` / `decodeCursor` used; no hand-rolled base64
|
|
160
246
|
|
|
161
247
|
### `complexity`
|
|
162
|
-
|
|
248
|
+
Checklist:
|
|
249
|
+
|
|
250
|
+
- [ ] Function body ≤ 40 lines (excluding blank lines and comments)
|
|
251
|
+
- [ ] Cyclomatic complexity ≤ 10 per function
|
|
252
|
+
- [ ] Nesting depth ≤ 4 (ternaries, callbacks, and blocks combined)
|
|
253
|
+
- [ ] Positional arity ≤ 3; ≥ 3 parameters use a readonly record
|
|
254
|
+
- [ ] Pipeline depth ≤ 8 stages in a single `pipe` call
|
|
255
|
+
- [ ] File ≤ 400 LOC; 800 absolute maximum (requires DEVIATION)
|
|
256
|
+
- [ ] No god-module (one file handling multiple unrelated concerns)
|
|
257
|
+
- [ ] No function doing more than one named thing (single responsibility)
|
|
163
258
|
|
|
164
259
|
### `loc`
|
|
165
|
-
|
|
260
|
+
Checklist:
|
|
261
|
+
|
|
262
|
+
- [ ] File LOC ≤ 400 (flag at 300; hard limit 800 with DEVIATION)
|
|
263
|
+
- [ ] Function body ≤ 40 lines
|
|
264
|
+
- [ ] No file with more than one primary exported concern (god-module)
|
|
265
|
+
- [ ] No function longer than 40 lines that could be decomposed
|
|
266
|
+
- [ ] No deeply nested anonymous functions or callbacks (extract and name them)
|
|
267
|
+
- [ ] Test files excluded from LOC limits but flagged if > 600 lines
|
|
166
268
|
|
|
167
269
|
### `annotations`
|
|
168
|
-
|
|
270
|
+
Checklist:
|
|
271
|
+
|
|
272
|
+
**JSDoc (§7)**
|
|
273
|
+
- [ ] Every exported symbol has a JSDoc comment
|
|
274
|
+
- [ ] `@param` present for every parameter on exported functions
|
|
275
|
+
- [ ] `@returns` present on every exported function with a non-void return
|
|
276
|
+
- [ ] `@law` present on every combinator with algebraic laws
|
|
277
|
+
- [ ] No JSDoc on non-exported symbols (unnecessary noise)
|
|
278
|
+
|
|
279
|
+
**Code markers**
|
|
280
|
+
- [ ] `TODO` / `FIXME` / `HACK` / `NOTE` / `OPTIMIZE` / `BUG` / `XXX` all have format: `(author, YYYY-MM-DD[, TICKET]): description`
|
|
281
|
+
- [ ] No marker missing author or date
|
|
282
|
+
- [ ] No stale TODO older than one release cycle without a ticket reference
|
|
283
|
+
|
|
284
|
+
**Deviations**
|
|
285
|
+
- [ ] Every rule violation has `// DEVIATION(N.M): <one-line justification>` immediately before the offending line
|
|
286
|
+
- [ ] DEVIATION format is exact: `DEVIATION(N.M)` — not `deviation`, not `Deviation`, not `DEVIATION N.M`
|
|
287
|
+
- [ ] Project-wide deviations are documented in `DEVIATIONS.md`
|
|
169
288
|
|
|
170
289
|
### `security`
|
|
171
|
-
SECURITY_CODING_STANDARD.md
|
|
290
|
+
Full reference: `node_modules/@tsfpp/standard/spec/SECURITY_CODING_STANDARD.md`
|
|
291
|
+
|
|
292
|
+
**Input validation**
|
|
293
|
+
- [ ] All external input validated at the boundary before entering the domain
|
|
294
|
+
- [ ] No `unknown` values passed into domain functions without prior narrowing
|
|
295
|
+
- [ ] Dynamic sort/filter fields allow-listed before use in queries
|
|
296
|
+
|
|
297
|
+
**Secrets and sensitive data**
|
|
298
|
+
- [ ] No secrets, credentials, or tokens in source code or committed config files
|
|
299
|
+
- [ ] No sensitive data (PII, credentials, tokens) in error messages or log output
|
|
300
|
+
- [ ] No sensitive data in `console.log` or structured log `info` entries
|
|
301
|
+
|
|
302
|
+
**Authentication and authorisation**
|
|
303
|
+
- [ ] Auth/authz enforced at the correct layer (handler / middleware), not inside use-cases
|
|
304
|
+
- [ ] No route accessible without authentication unless explicitly marked `// PUBLIC`
|
|
305
|
+
- [ ] Principal ID never trusted from request body; always extracted from verified context
|
|
306
|
+
|
|
307
|
+
**Dependencies**
|
|
308
|
+
- [ ] No known vulnerable dependencies (`pnpm audit` clean)
|
|
309
|
+
- [ ] No direct use of `eval`, `Function()`, or dynamic `import()` with user-controlled input
|
|
310
|
+
|
|
311
|
+
**Output safety**
|
|
312
|
+
- [ ] No user input reflected in error responses without sanitisation
|
|
313
|
+
- [ ] CORS: `allowedOrigins` from config; never reflects `Origin` header blindly
|
|
314
|
+
- [ ] `baselineSecurityHeaders` applied to every response
|
|
172
315
|
|
|
173
316
|
### `prelude`
|
|
174
317
|
Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@tsfpp/prelude` already provides.
|
|
@@ -259,7 +402,10 @@ All focus areas above in sequence. For `.tsx` files, include `react` automatical
|
|
|
259
402
|
List all files in scope. Group into logical slices (≤ 300 LOC per slice, or one cohesive module). Populate the slice index table in the report.
|
|
260
403
|
|
|
261
404
|
**Step 2 — Create report**
|
|
262
|
-
Write `docs/audits/<slug>-<
|
|
405
|
+
Write `docs/audits/<target-slug>-<focus>-<YYYYMMDD-HHmm>.md` with the template above before touching any source file.
|
|
406
|
+
Example: `docs/audits/src-domain-prelude-20260517-1430.md` or `docs/audits/src-all-20260517-0900.md`.
|
|
407
|
+
|
|
408
|
+
> **Do not suggest handoffs or pause between slices.** Work through all slices without interruption. Update the report after each slice. Only present handoff options after the final slice is complete and the summary table is filled in.
|
|
263
409
|
|
|
264
410
|
**Step 3 — Inspect slice by slice**
|
|
265
411
|
For each slice:
|
|
@@ -20,7 +20,7 @@ tools:
|
|
|
20
20
|
handoffs:
|
|
21
21
|
- label: Audit test coverage
|
|
22
22
|
agent: tsfpp-audit
|
|
23
|
-
prompt: "Audit the test files just written for TSF++ compliance. Focus: test."
|
|
23
|
+
prompt: "Audit the test files just written for TSF++ compliance. Use the same target as this backfill session. Focus: test. Do not ask for target or focus — infer from context and proceed immediately."
|
|
24
24
|
send: false
|
|
25
25
|
- label: Fix uncovered paths in implementation
|
|
26
26
|
agent: tsfpp-guarded-coding
|
|
@@ -41,6 +41,13 @@ Full coding standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
|
41
41
|
|
|
42
42
|
---
|
|
43
43
|
|
|
44
|
+
## Before writing any test
|
|
45
|
+
|
|
46
|
+
Load and apply the `/test-standard` skill. Every test you write must conform to
|
|
47
|
+
all rules in that skill. Do not write a single test before the skill is loaded.
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
44
51
|
## Session start
|
|
45
52
|
|
|
46
53
|
Infer the layer per file from the path and contents:
|
|
@@ -71,6 +78,11 @@ You succeed when:
|
|
|
71
78
|
- Every branch and switch case is exercised
|
|
72
79
|
- All tests pass green
|
|
73
80
|
|
|
81
|
+
> **Do not suggest handoffs or pause between slices.** Work through all slices
|
|
82
|
+
> without interruption. Intermediate lint/typecheck runs and report updates are
|
|
83
|
+
> expected and correct. Only present handoff options after the final slice is
|
|
84
|
+
> complete and the backfill report is finished.
|
|
85
|
+
|
|
74
86
|
---
|
|
75
87
|
|
|
76
88
|
## Execution workflow
|
|
@@ -118,7 +130,8 @@ All tests must be green. If a test fails, the contract derivation was wrong —
|
|
|
118
130
|
|
|
119
131
|
**Step 5 — Backfill report**
|
|
120
132
|
|
|
121
|
-
Append a section to `docs/audits/backfill-<slug>-<
|
|
133
|
+
Append a section to `docs/audits/backfill-<target-slug>-<YYYYMMDD-HHmm>.md`:
|
|
134
|
+
Example: `docs/audits/backfill-src-domain-20260517-1430.md`.
|
|
122
135
|
|
|
123
136
|
````markdown
|
|
124
137
|
## Backfill — `<file>`
|
|
@@ -157,6 +170,36 @@ All rules from `TEST_CODING_STANDARD.md` apply:
|
|
|
157
170
|
|
|
158
171
|
---
|
|
159
172
|
|
|
173
|
+
## Factories
|
|
174
|
+
|
|
175
|
+
Always use typed factory functions from `tests/factories/` for test data.
|
|
176
|
+
Never write raw object literals inline.
|
|
177
|
+
|
|
178
|
+
```ts
|
|
179
|
+
// tests/factories/track.factory.ts
|
|
180
|
+
const makeTrack = (overrides: Partial<Track> = {}): Track => ({
|
|
181
|
+
id: mkTrackId('test-track-001'),
|
|
182
|
+
title: 'Default Title',
|
|
183
|
+
artistId: mkArtistId('test-artist-001'),
|
|
184
|
+
...overrides,
|
|
185
|
+
})
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Import and use with overrides for the specific case under test:
|
|
189
|
+
|
|
190
|
+
```ts
|
|
191
|
+
// Specific case — override only what matters for this test
|
|
192
|
+
const track = makeTrack({ title: 'Blue Flame' })
|
|
193
|
+
|
|
194
|
+
// Default case — the specific values don't matter
|
|
195
|
+
const track = makeTrack()
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
Never hard-code raw objects like `{ id: 'abc', title: 'Test', artistId: 'xyz' }` in test bodies.
|
|
199
|
+
Never use production or staging IDs in fixtures.
|
|
200
|
+
|
|
201
|
+
---
|
|
202
|
+
|
|
160
203
|
## Layer-specific patterns
|
|
161
204
|
|
|
162
205
|
### `core`
|
|
@@ -193,9 +236,11 @@ describe('mkTrackId', () => {
|
|
|
193
236
|
|
|
194
237
|
```ts
|
|
195
238
|
it('responds with 201 and a Location header on valid input', async () => {
|
|
239
|
+
const input = makeCreateTrackInput() // from tests/factories/track.factory.ts
|
|
240
|
+
|
|
196
241
|
const req = new Request('http://localhost/v1/tracks', {
|
|
197
242
|
method: 'POST',
|
|
198
|
-
body: JSON.stringify(
|
|
243
|
+
body: JSON.stringify(input),
|
|
199
244
|
headers: { 'Content-Type': 'application/json' },
|
|
200
245
|
})
|
|
201
246
|
|
|
@@ -117,6 +117,8 @@ If a function exceeds 40 lines, cyclomatic complexity 10, or nesting depth 4: de
|
|
|
117
117
|
**Step 1 — Read the report**
|
|
118
118
|
Parse the audit report. Build a todo list of all open violations grouped by slice. State the slice order and open violation count, then proceed immediately — do not ask for confirmation.
|
|
119
119
|
|
|
120
|
+
> **Do not suggest handoffs or pause between slices.** Work through all violations without interruption. Intermediate lint/typecheck runs and report updates are expected and correct. Only present handoff options after every violation is resolved and the report is marked complete.
|
|
121
|
+
|
|
120
122
|
**Step 2 — Work slice by slice**
|
|
121
123
|
For each slice with open violations:
|
|
122
124
|
1. Read the file.
|
|
@@ -36,6 +36,13 @@ Full coding standard: `node_modules/@tsfpp/standard/spec/CODING_STANDARD.md`
|
|
|
36
36
|
|
|
37
37
|
---
|
|
38
38
|
|
|
39
|
+
## Before writing any test
|
|
40
|
+
|
|
41
|
+
Load and apply the `/test-standard` skill. Every test you write must conform to
|
|
42
|
+
all rules in that skill. Do not write a single test before the skill is loaded.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
39
46
|
## Session start
|
|
40
47
|
|
|
41
48
|
Infer the layer per task from the user's message:
|
|
@@ -153,6 +160,36 @@ All rules from `TEST_CODING_STANDARD.md` apply. The most critical during test au
|
|
|
153
160
|
|
|
154
161
|
---
|
|
155
162
|
|
|
163
|
+
## Factories
|
|
164
|
+
|
|
165
|
+
Always use typed factory functions from `tests/factories/` for test data.
|
|
166
|
+
Never write raw object literals inline.
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
// tests/factories/track.factory.ts
|
|
170
|
+
const makeTrack = (overrides: Partial<Track> = {}): Track => ({
|
|
171
|
+
id: mkTrackId('test-track-001'),
|
|
172
|
+
title: 'Default Title',
|
|
173
|
+
artistId: mkArtistId('test-artist-001'),
|
|
174
|
+
...overrides,
|
|
175
|
+
})
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Import and use with overrides for the specific case under test:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
// Specific case — override only what matters for this test
|
|
182
|
+
const track = makeTrack({ title: 'Blue Flame' })
|
|
183
|
+
|
|
184
|
+
// Default case — the specific values don't matter
|
|
185
|
+
const track = makeTrack()
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
Never hard-code raw objects like `{ id: 'abc', title: 'Test', artistId: 'xyz' }` in test bodies.
|
|
189
|
+
Never use production or staging IDs in fixtures.
|
|
190
|
+
|
|
191
|
+
---
|
|
192
|
+
|
|
156
193
|
## Layer-specific test patterns
|
|
157
194
|
|
|
158
195
|
### `core`
|
|
@@ -164,14 +201,21 @@ import { isSome, isNone, isOk, isErr } from '@tsfpp/prelude'
|
|
|
164
201
|
describe('mkTrackId', () => {
|
|
165
202
|
describe('when the input is a non-empty string', () => {
|
|
166
203
|
it('returns Some containing a branded TrackId', () => {
|
|
167
|
-
const
|
|
204
|
+
const raw = 'abc'
|
|
205
|
+
|
|
206
|
+
const result = mkTrackId(raw)
|
|
207
|
+
|
|
168
208
|
expect(isSome(result)).toBe(true)
|
|
169
209
|
})
|
|
170
210
|
})
|
|
171
211
|
|
|
172
212
|
describe('when the input is empty', () => {
|
|
173
213
|
it('returns None', () => {
|
|
174
|
-
|
|
214
|
+
const raw = ''
|
|
215
|
+
|
|
216
|
+
const result = mkTrackId(raw)
|
|
217
|
+
|
|
218
|
+
expect(result).toEqual(none)
|
|
175
219
|
})
|
|
176
220
|
})
|
|
177
221
|
|
|
@@ -200,11 +244,13 @@ describe('POST /v1/tracks', () => {
|
|
|
200
244
|
describe('when the request body is valid', () => {
|
|
201
245
|
it('responds with 201 and a Location header', async () => {
|
|
202
246
|
const req = new Request('http://localhost/v1/tracks', {
|
|
203
|
-
method:
|
|
204
|
-
body:
|
|
247
|
+
method: 'POST',
|
|
248
|
+
body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
|
|
205
249
|
headers: { 'Content-Type': 'application/json' },
|
|
206
250
|
})
|
|
251
|
+
|
|
207
252
|
const res = await handler(req)
|
|
253
|
+
|
|
208
254
|
expect(res.status).toBe(201)
|
|
209
255
|
expect(res.headers.get('Location')).toMatch(/\/v1\/tracks\//)
|
|
210
256
|
})
|
|
@@ -212,12 +258,16 @@ describe('POST /v1/tracks', () => {
|
|
|
212
258
|
|
|
213
259
|
describe('when title is missing', () => {
|
|
214
260
|
it('responds with 422', async () => {
|
|
261
|
+
const input = makeCreateTrackInput({ title: undefined }) // override to trigger validation failure
|
|
262
|
+
|
|
215
263
|
const req = new Request('http://localhost/v1/tracks', {
|
|
216
|
-
method:
|
|
217
|
-
body:
|
|
264
|
+
method: 'POST',
|
|
265
|
+
body: JSON.stringify(input),
|
|
218
266
|
headers: { 'Content-Type': 'application/json' },
|
|
219
267
|
})
|
|
268
|
+
|
|
220
269
|
const res = await handler(req)
|
|
270
|
+
|
|
221
271
|
expect(res.status).toBe(422)
|
|
222
272
|
})
|
|
223
273
|
})
|
|
@@ -266,7 +316,9 @@ describe('TrackRepository', () => {
|
|
|
266
316
|
it('returns Some containing the track', async () => {
|
|
267
317
|
const track = makeTrack()
|
|
268
318
|
await repo.save(track)
|
|
319
|
+
|
|
269
320
|
const result = await repo.findById(track.id)
|
|
321
|
+
|
|
270
322
|
expect(isSome(result)).toBe(true)
|
|
271
323
|
})
|
|
272
324
|
})
|
|
@@ -274,6 +326,7 @@ describe('TrackRepository', () => {
|
|
|
274
326
|
describe('when the track does not exist', () => {
|
|
275
327
|
it('returns None', async () => {
|
|
276
328
|
const result = await repo.findById(mkTrackId('nonexistent'))
|
|
329
|
+
|
|
277
330
|
expect(isNone(result)).toBe(true) // will fail — findById not implemented
|
|
278
331
|
})
|
|
279
332
|
})
|
package/init.mjs
CHANGED
|
@@ -2,23 +2,27 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* @tsfpp/agents init
|
|
4
4
|
*
|
|
5
|
-
* Copies Copilot agents, instructions, prompts, skills, and Claude Code
|
|
6
|
-
* into the correct locations in the consumer's project.
|
|
7
|
-
* eslint.config.js if not already present.
|
|
5
|
+
* Copies Copilot agents, instructions, prompts, skills, and Claude Code
|
|
6
|
+
* configuration into the correct locations in the consumer's project.
|
|
8
7
|
*
|
|
9
8
|
* Usage:
|
|
10
|
-
*
|
|
11
|
-
* node node_modules/@tsfpp/agents/init.mjs
|
|
9
|
+
* node node_modules/@tsfpp/agents/init.mjs (interactive)
|
|
10
|
+
* node node_modules/@tsfpp/agents/init.mjs --yes (non-interactive / postinstall)
|
|
11
|
+
*
|
|
12
|
+
* --yes mode: copies all package-managed files (agents, instructions, skills,
|
|
13
|
+
* prompts, copilot-instructions.md) without prompting. Skips eslint.config.js
|
|
14
|
+
* and tsconfig.json — those are workspace-owned and never touched automatically.
|
|
12
15
|
*/
|
|
13
16
|
|
|
14
17
|
import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
15
|
-
import { existsSync }
|
|
16
|
-
import { join, dirname }
|
|
17
|
-
import { fileURLToPath }
|
|
18
|
-
import { createInterface }
|
|
18
|
+
import { existsSync } from 'node:fs';
|
|
19
|
+
import { join, dirname } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
import { createInterface } from 'node:readline';
|
|
19
22
|
|
|
20
23
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
24
|
const cwd = process.cwd();
|
|
25
|
+
const YES = process.argv.includes('--yes');
|
|
22
26
|
|
|
23
27
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
24
28
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
@@ -26,51 +30,66 @@ const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
|
26
30
|
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
27
31
|
|
|
28
32
|
// ─── File map ─────────────────────────────────────────────────────────────────
|
|
29
|
-
//
|
|
33
|
+
// [source (relative to this file), destination (relative to cwd)]
|
|
34
|
+
// All entries are package-managed — always overwritten in --yes mode.
|
|
30
35
|
|
|
31
36
|
const FILES = [
|
|
32
37
|
// Always-on workspace instructions
|
|
33
|
-
['copilot/copilot-instructions.md',
|
|
38
|
+
['copilot/copilot-instructions.md', '.github/copilot-instructions.md'],
|
|
34
39
|
|
|
35
40
|
// Scoped instruction files
|
|
36
|
-
['copilot/instructions/tsfpp-base.instructions.md',
|
|
37
|
-
['copilot/instructions/tsfpp-prelude.instructions.md',
|
|
38
|
-
['copilot/instructions/tsfpp-api.instructions.md',
|
|
39
|
-
['copilot/instructions/tsfpp-react.instructions.md',
|
|
40
|
-
['copilot/instructions/tsfpp-testing.instructions.md',
|
|
41
|
-
['copilot/instructions/trunk.instructions.md',
|
|
41
|
+
['copilot/instructions/tsfpp-base.instructions.md', '.github/instructions/tsfpp-base.instructions.md'],
|
|
42
|
+
['copilot/instructions/tsfpp-prelude.instructions.md', '.github/instructions/tsfpp-prelude.instructions.md'],
|
|
43
|
+
['copilot/instructions/tsfpp-api.instructions.md', '.github/instructions/tsfpp-api.instructions.md'],
|
|
44
|
+
['copilot/instructions/tsfpp-react.instructions.md', '.github/instructions/tsfpp-react.instructions.md'],
|
|
45
|
+
['copilot/instructions/tsfpp-testing.instructions.md', '.github/instructions/tsfpp-testing.instructions.md'],
|
|
46
|
+
['copilot/instructions/trunk.instructions.md', '.github/instructions/trunk.instructions.md'],
|
|
42
47
|
|
|
43
48
|
// Agents
|
|
44
|
-
['copilot/agents/tsfpp-tdd.agent.md',
|
|
45
|
-
['copilot/agents/tsfpp-backfill-tests.agent.md',
|
|
46
|
-
['copilot/agents/tsfpp-guarded-coding.agent.md',
|
|
47
|
-
['copilot/agents/tsfpp-audit.agent.md',
|
|
48
|
-
['copilot/agents/tsfpp-refactor-engineer.agent.md',
|
|
49
|
-
['copilot/agents/tsfpp-annotate.agent.md',
|
|
50
|
-
|
|
51
|
-
//
|
|
52
|
-
['copilot/prompts/trunk-init-repo.prompt.md',
|
|
53
|
-
['copilot/prompts/tsfpp-new-module.prompt.md',
|
|
54
|
-
['copilot/prompts/tsfpp-boundary-review.prompt.md',
|
|
55
|
-
|
|
56
|
-
//
|
|
57
|
-
['copilot/skills/coding-standard/SKILL.md',
|
|
58
|
-
['copilot/skills/prelude-api/SKILL.md',
|
|
59
|
-
['copilot/skills/boundary-api/SKILL.md',
|
|
60
|
-
['copilot/skills/react-coding-standard/SKILL.md',
|
|
61
|
-
['copilot/skills/test-standard/SKILL.md',
|
|
49
|
+
['copilot/agents/tsfpp-tdd.agent.md', '.github/agents/tsfpp-tdd.agent.md'],
|
|
50
|
+
['copilot/agents/tsfpp-backfill-tests.agent.md', '.github/agents/tsfpp-backfill-tests.agent.md'],
|
|
51
|
+
['copilot/agents/tsfpp-guarded-coding.agent.md', '.github/agents/tsfpp-guarded-coding.agent.md'],
|
|
52
|
+
['copilot/agents/tsfpp-audit.agent.md', '.github/agents/tsfpp-audit.agent.md'],
|
|
53
|
+
['copilot/agents/tsfpp-refactor-engineer.agent.md', '.github/agents/tsfpp-refactor-engineer.agent.md'],
|
|
54
|
+
['copilot/agents/tsfpp-annotate.agent.md', '.github/agents/tsfpp-annotate.agent.md'],
|
|
55
|
+
|
|
56
|
+
// Prompts
|
|
57
|
+
['copilot/prompts/trunk-init-repo.prompt.md', '.github/prompts/trunk-init-repo.prompt.md'],
|
|
58
|
+
['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
|
|
59
|
+
['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
|
|
60
|
+
|
|
61
|
+
// Skills
|
|
62
|
+
['copilot/skills/coding-standard/SKILL.md', '.github/skills/coding-standard/SKILL.md'],
|
|
63
|
+
['copilot/skills/prelude-api/SKILL.md', '.github/skills/prelude-api/SKILL.md'],
|
|
64
|
+
['copilot/skills/boundary-api/SKILL.md', '.github/skills/boundary-api/SKILL.md'],
|
|
65
|
+
['copilot/skills/react-coding-standard/SKILL.md', '.github/skills/react-coding-standard/SKILL.md'],
|
|
66
|
+
['copilot/skills/test-standard/SKILL.md', '.github/skills/test-standard/SKILL.md'],
|
|
62
67
|
|
|
63
68
|
// Claude Code
|
|
64
|
-
['claude/CLAUDE.md',
|
|
69
|
+
['claude/CLAUDE.md', '.claude/CLAUDE.md'],
|
|
65
70
|
];
|
|
66
71
|
|
|
67
|
-
// ───
|
|
72
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
68
73
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
async function ensureDir(filePath) {
|
|
75
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function ask(question) {
|
|
79
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
rl.question(question, (answer) => {
|
|
82
|
+
rl.close();
|
|
83
|
+
resolve(answer.trim().toLowerCase());
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function confirm(question) {
|
|
89
|
+
return (await ask(question)) === 'y';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// ─── Workspace detection ──────────────────────────────────────────────────────
|
|
74
93
|
|
|
75
94
|
async function detectWorkspacePackages() {
|
|
76
95
|
const wsFile = join(cwd, 'pnpm-workspace.yaml');
|
|
@@ -78,7 +97,7 @@ async function detectWorkspacePackages() {
|
|
|
78
97
|
|
|
79
98
|
const yaml = await readFile(wsFile, 'utf8');
|
|
80
99
|
const patterns = [...yaml.matchAll(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/gm)]
|
|
81
|
-
.map(m => m[1].trim().replace(/\/\*\*?$/, ''));
|
|
100
|
+
.map(m => m[1].trim().replace(/\/\*\*?$/, ''));
|
|
82
101
|
|
|
83
102
|
const IGNORE = new Set(['dist', 'build', 'out', 'coverage', 'node_modules', '.git', '.turbo', 'tmp']);
|
|
84
103
|
|
|
@@ -96,8 +115,16 @@ async function detectWorkspacePackages() {
|
|
|
96
115
|
return packages.length > 0 ? packages : null;
|
|
97
116
|
}
|
|
98
117
|
|
|
118
|
+
// ─── ESLint config ────────────────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
const ESLINT_PROFILES = {
|
|
121
|
+
base: { import: `import tsfpp from '@tsfpp/eslint-config'`, spread: 'tsfpp' },
|
|
122
|
+
react: { import: `import tsfppReact from '@tsfpp/eslint-config/react'`, spread: 'tsfppReact' },
|
|
123
|
+
api: { import: `import tsfppApi from '@tsfpp/eslint-config/api'`, spread: 'tsfppApi' },
|
|
124
|
+
};
|
|
125
|
+
|
|
99
126
|
async function askProfile(label) {
|
|
100
|
-
console.log(`\n
|
|
127
|
+
console.log(`\n ESLint profile for ${bold(label)}:`);
|
|
101
128
|
console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
|
|
102
129
|
console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
|
|
103
130
|
console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
|
|
@@ -105,66 +132,48 @@ async function askProfile(label) {
|
|
|
105
132
|
return choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
|
|
106
133
|
}
|
|
107
134
|
|
|
108
|
-
function
|
|
109
|
-
// Collect which profiles are used
|
|
135
|
+
function generateMonorepoEslint(packageProfiles) {
|
|
110
136
|
const usedProfiles = [...new Set(Object.values(packageProfiles))];
|
|
137
|
+
const imports = usedProfiles.map(p => ESLINT_PROFILES[p].import).join('\n');
|
|
111
138
|
|
|
112
|
-
const imports = usedProfiles.map(p => PROFILES[p].import).join('\n');
|
|
113
|
-
|
|
114
|
-
// base spreads globally (no files filter); react/api are scoped per package
|
|
115
139
|
const basePackages = Object.entries(packageProfiles)
|
|
116
140
|
.filter(([, p]) => p === 'base')
|
|
117
141
|
.map(([pkg]) => `'${pkg}/src/**'`);
|
|
118
142
|
|
|
119
|
-
const
|
|
143
|
+
const scopedBlocks = ['react', 'api'].flatMap(profile => {
|
|
120
144
|
const pkgs = Object.entries(packageProfiles)
|
|
121
145
|
.filter(([, p]) => p === profile)
|
|
122
146
|
.map(([pkg]) => `'${pkg}/src/**'`);
|
|
123
147
|
if (pkgs.length === 0) return [];
|
|
124
|
-
const spread =
|
|
125
|
-
return [
|
|
126
|
-
` // ${profile}`,
|
|
127
|
-
` ...${spread}.map(c => ({ ...c, files: [${pkgs.join(', ')}] })),`,
|
|
128
|
-
];
|
|
148
|
+
const spread = ESLINT_PROFILES[profile].spread;
|
|
149
|
+
return [` // ${profile}`, ` ...${spread}.map(c => ({ ...c, files: [${pkgs.join(', ')}] })),`];
|
|
129
150
|
});
|
|
130
151
|
|
|
131
152
|
const baseSpread = basePackages.length > 0
|
|
132
153
|
? ` // base\n ...tsfpp.map(c => ({ ...c, files: [${basePackages.join(', ')}] })),`
|
|
133
154
|
: ` // base — applies to all files not matched by a scoped profile\n ...tsfpp,`;
|
|
134
155
|
|
|
135
|
-
return [
|
|
136
|
-
imports,
|
|
137
|
-
'',
|
|
138
|
-
'export default [',
|
|
139
|
-
baseSpread,
|
|
140
|
-
...scopedProfiles,
|
|
141
|
-
']',
|
|
142
|
-
'',
|
|
143
|
-
].join('\n');
|
|
156
|
+
return [imports, '', 'export default [', baseSpread, ...scopedBlocks, ']', ''].join('\n');
|
|
144
157
|
}
|
|
145
158
|
|
|
146
|
-
function
|
|
147
|
-
const { import: imp, spread } =
|
|
159
|
+
function generateSingleEslint(profile) {
|
|
160
|
+
const { import: imp, spread } = ESLINT_PROFILES[profile];
|
|
148
161
|
return `${imp}\nexport default [...${spread}]\n`;
|
|
149
162
|
}
|
|
150
163
|
|
|
151
|
-
async function writeEslintConfig() {
|
|
164
|
+
async function writeEslintConfig(results) {
|
|
152
165
|
const packages = await detectWorkspacePackages();
|
|
153
|
-
|
|
154
|
-
let content;
|
|
155
|
-
let description;
|
|
166
|
+
let content, description;
|
|
156
167
|
|
|
157
168
|
if (packages) {
|
|
158
169
|
console.log(`\n Monorepo detected — ${packages.length} package(s) found.\n`);
|
|
159
170
|
const packageProfiles = {};
|
|
160
|
-
for (const pkg of packages)
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
content = generateMonorepoConfig(packageProfiles);
|
|
171
|
+
for (const pkg of packages) packageProfiles[pkg] = await askProfile(pkg);
|
|
172
|
+
content = generateMonorepoEslint(packageProfiles);
|
|
164
173
|
description = 'monorepo';
|
|
165
174
|
} else {
|
|
166
175
|
const profile = await askProfile('this project');
|
|
167
|
-
content =
|
|
176
|
+
content = generateSingleEslint(profile);
|
|
168
177
|
description = `profile: ${profile}`;
|
|
169
178
|
}
|
|
170
179
|
|
|
@@ -178,175 +187,178 @@ async function writeEslintConfig() {
|
|
|
178
187
|
}
|
|
179
188
|
}
|
|
180
189
|
|
|
181
|
-
// ───
|
|
190
|
+
// ─── tsconfig generation ──────────────────────────────────────────────────────
|
|
182
191
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
const TSCONFIG_PRESETS = {
|
|
193
|
+
app: '@tsfpp/tsconfig/app',
|
|
194
|
+
lib: '@tsfpp/tsconfig/lib',
|
|
195
|
+
};
|
|
186
196
|
|
|
187
|
-
async function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
197
|
+
async function askPreset(label) {
|
|
198
|
+
console.log(`\n tsconfig preset for ${bold(label)}:`);
|
|
199
|
+
console.log(` ${dim('1')} app ${dim('— application / tool (noEmit: true)')}`);
|
|
200
|
+
console.log(` ${dim('2')} lib ${dim('— publishable package (declaration, composite)')}`);
|
|
201
|
+
console.log(` ${dim('N')} skip`);
|
|
202
|
+
const choice = await ask(` ${dim('[1/2/N, default: 1]')} `);
|
|
203
|
+
if (choice === 'n') return null;
|
|
204
|
+
return choice === '2' ? 'lib' : 'app';
|
|
195
205
|
}
|
|
196
206
|
|
|
197
|
-
|
|
198
|
-
return (
|
|
207
|
+
function generateTsConfig(preset) {
|
|
208
|
+
return JSON.stringify(
|
|
209
|
+
{ extends: TSCONFIG_PRESETS[preset], compilerOptions: { rootDir: 'src' }, include: ['src'] },
|
|
210
|
+
null, 2
|
|
211
|
+
) + '\n';
|
|
199
212
|
}
|
|
200
213
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
const results = { copied: [], skipped: [], failed: [] };
|
|
208
|
-
|
|
209
|
-
// ── Copy files ────────────────────────────────────────────────────────────────
|
|
210
|
-
|
|
211
|
-
for (const [src, dest] of FILES) {
|
|
212
|
-
const srcPath = join(__dirname, src);
|
|
213
|
-
const destPath = join(cwd, dest);
|
|
214
|
+
function generateRootTsConfig(packagePaths) {
|
|
215
|
+
return JSON.stringify(
|
|
216
|
+
{ files: [], references: packagePaths.map(p => ({ path: p })) },
|
|
217
|
+
null, 2
|
|
218
|
+
) + '\n';
|
|
219
|
+
}
|
|
214
220
|
|
|
221
|
+
async function writeIfConfirmed(destPath, content, label, results) {
|
|
215
222
|
if (existsSync(destPath)) {
|
|
216
223
|
const overwrite = await confirm(
|
|
217
|
-
` ${yellow('!')} ${
|
|
224
|
+
` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
|
|
218
225
|
);
|
|
219
226
|
if (!overwrite) {
|
|
220
|
-
results.skipped.push(
|
|
221
|
-
console.log(` ${dim('–')} ${dim(
|
|
222
|
-
|
|
227
|
+
results.skipped.push(label);
|
|
228
|
+
console.log(` ${dim('–')} ${dim(label)} ${dim('(skipped)')}`);
|
|
229
|
+
return;
|
|
223
230
|
}
|
|
224
231
|
}
|
|
225
|
-
|
|
226
232
|
try {
|
|
227
233
|
await ensureDir(destPath);
|
|
228
|
-
await
|
|
229
|
-
results.copied.push(
|
|
230
|
-
console.log(` ${green('✓')} ${
|
|
234
|
+
await writeFile(destPath, content, 'utf8');
|
|
235
|
+
results.copied.push(label);
|
|
236
|
+
console.log(` ${green('✓')} ${label}`);
|
|
231
237
|
} catch (err) {
|
|
232
|
-
results.failed.push(
|
|
233
|
-
console.log(` \x1b[31m✗\x1b[0m ${
|
|
238
|
+
results.failed.push(label);
|
|
239
|
+
console.log(` \x1b[31m✗\x1b[0m ${label} ${dim(`(${err.message})`)}`);
|
|
234
240
|
}
|
|
235
241
|
}
|
|
236
242
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
console.log();
|
|
243
|
+
async function writeTsConfigs(results) {
|
|
244
|
+
const packages = await detectWorkspacePackages();
|
|
240
245
|
|
|
241
|
-
|
|
246
|
+
if (packages) {
|
|
247
|
+
console.log(` Generating tsconfig.json per package.\n`);
|
|
248
|
+
const packagePresets = {};
|
|
249
|
+
for (const pkg of packages) packagePresets[pkg] = await askPreset(pkg);
|
|
242
250
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
251
|
+
for (const [pkg, preset] of Object.entries(packagePresets)) {
|
|
252
|
+
if (preset === null) {
|
|
253
|
+
results.skipped.push(`${pkg}/tsconfig.json`);
|
|
254
|
+
console.log(` ${dim('–')} ${dim(`${pkg}/tsconfig.json`)} ${dim('(skipped)')}`);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
await writeIfConfirmed(join(cwd, pkg, 'tsconfig.json'), generateTsConfig(preset), `${pkg}/tsconfig.json`, results);
|
|
258
|
+
}
|
|
259
|
+
await writeIfConfirmed(join(cwd, 'tsconfig.json'), generateRootTsConfig(packages), 'tsconfig.json (root references)', results);
|
|
250
260
|
} else {
|
|
251
|
-
await
|
|
261
|
+
const preset = await askPreset('this project');
|
|
262
|
+
if (preset === null) {
|
|
263
|
+
results.skipped.push('tsconfig.json');
|
|
264
|
+
console.log(` ${dim('–')} ${dim('tsconfig.json')} ${dim('(skipped)')}`);
|
|
265
|
+
} else {
|
|
266
|
+
await writeIfConfirmed(join(cwd, 'tsconfig.json'), generateTsConfig(preset), 'tsconfig.json', results);
|
|
267
|
+
}
|
|
252
268
|
}
|
|
253
|
-
} else {
|
|
254
|
-
await writeEslintConfig();
|
|
255
269
|
}
|
|
256
270
|
|
|
257
|
-
//
|
|
271
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
258
272
|
|
|
259
|
-
|
|
273
|
+
async function main() {
|
|
274
|
+
console.log();
|
|
275
|
+
console.log(bold(' @tsfpp/agents — init') + (YES ? dim(' (--yes)') : ''));
|
|
276
|
+
if (YES) {
|
|
277
|
+
console.log(dim(' Copying package-managed files. eslint.config.js and tsconfig.json are not touched.\n'));
|
|
278
|
+
} else {
|
|
279
|
+
console.log(dim(' Sets up Copilot agents, instructions, prompts, skills, and ESLint config.\n'));
|
|
280
|
+
}
|
|
260
281
|
|
|
261
|
-
|
|
282
|
+
const results = { copied: [], skipped: [], failed: [] };
|
|
262
283
|
|
|
263
|
-
|
|
264
|
-
const PRESETS = {
|
|
265
|
-
app: { extends: '@tsfpp/tsconfig/app', label: 'app — application / tool (noEmit)' },
|
|
266
|
-
lib: { extends: '@tsfpp/tsconfig/lib', label: 'lib — publishable package (declaration, composite)' },
|
|
267
|
-
};
|
|
284
|
+
// ── Copy package-managed files ─────────────────────────────────────────────
|
|
268
285
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
console.log(` ${dim('2')} lib ${dim('— publishable package (declaration, composite)')}`);
|
|
273
|
-
const choice = await ask(` ${dim('[1/2, default: 1]')} `);
|
|
274
|
-
return choice === '2' ? 'lib' : 'app';
|
|
275
|
-
}
|
|
286
|
+
for (const [src, dest] of FILES) {
|
|
287
|
+
const srcPath = join(__dirname, src);
|
|
288
|
+
const destPath = join(cwd, dest);
|
|
276
289
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
}, null, 2) + '\n';
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
function generateRootTsConfig(packagePaths) {
|
|
286
|
-
return JSON.stringify({
|
|
287
|
-
files: [],
|
|
288
|
-
references: packagePaths.map(p => ({ path: p })),
|
|
289
|
-
}, null, 2) + '\n';
|
|
290
|
-
}
|
|
290
|
+
if (!existsSync(srcPath)) {
|
|
291
|
+
results.skipped.push(dest);
|
|
292
|
+
console.log(` ${dim('–')} ${dim(dest)} ${dim('(source not found — skipped)')}`);
|
|
293
|
+
continue;
|
|
294
|
+
}
|
|
291
295
|
|
|
292
|
-
|
|
293
|
-
if (existsSync(destPath)) {
|
|
296
|
+
if (existsSync(destPath) && !YES) {
|
|
294
297
|
const overwrite = await confirm(
|
|
295
|
-
` ${yellow('!')} ${
|
|
298
|
+
` ${yellow('!')} ${dest} already exists. Overwrite? ${dim('[y/N]')} `
|
|
296
299
|
);
|
|
297
300
|
if (!overwrite) {
|
|
298
|
-
results.skipped.push(
|
|
299
|
-
console.log(` ${dim('–')} ${dim(
|
|
300
|
-
|
|
301
|
+
results.skipped.push(dest);
|
|
302
|
+
console.log(` ${dim('–')} ${dim(dest)} ${dim('(skipped)')}`);
|
|
303
|
+
continue;
|
|
301
304
|
}
|
|
302
305
|
}
|
|
306
|
+
|
|
303
307
|
try {
|
|
304
308
|
await ensureDir(destPath);
|
|
305
|
-
await
|
|
306
|
-
results.copied.push(
|
|
307
|
-
console.log(` ${green('✓')} ${
|
|
309
|
+
await copyFile(srcPath, destPath);
|
|
310
|
+
results.copied.push(dest);
|
|
311
|
+
console.log(` ${green('✓')} ${dest}`);
|
|
308
312
|
} catch (err) {
|
|
309
|
-
results.failed.push(
|
|
310
|
-
console.log(` \x1b[31m✗\x1b[0m ${
|
|
313
|
+
results.failed.push(dest);
|
|
314
|
+
console.log(` \x1b[31m✗\x1b[0m ${dest} ${dim(`(${err.message})`)}`);
|
|
311
315
|
}
|
|
312
316
|
}
|
|
313
317
|
|
|
314
|
-
|
|
315
|
-
// Monorepo: tsconfig per package + root references
|
|
316
|
-
console.log(` Generating tsconfig.json per package.\n`);
|
|
317
|
-
const packagePresets = {};
|
|
318
|
-
for (const pkg of packages) {
|
|
319
|
-
packagePresets[pkg] = await askPreset(pkg);
|
|
320
|
-
}
|
|
318
|
+
// ── ESLint (interactive only) ──────────────────────────────────────────────
|
|
321
319
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
320
|
+
if (!YES) {
|
|
321
|
+
console.log();
|
|
322
|
+
const eslintDest = join(cwd, 'eslint.config.js');
|
|
323
|
+
if (existsSync(eslintDest)) {
|
|
324
|
+
const overwrite = await confirm(
|
|
325
|
+
` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
|
|
326
|
+
);
|
|
327
|
+
if (!overwrite) {
|
|
328
|
+
results.skipped.push('eslint.config.js');
|
|
329
|
+
console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
|
|
330
|
+
} else {
|
|
331
|
+
await writeEslintConfig(results);
|
|
332
|
+
}
|
|
333
|
+
} else {
|
|
334
|
+
await writeEslintConfig(results);
|
|
326
335
|
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// ── tsconfig (interactive only) ────────────────────────────────────────────
|
|
327
339
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
if (!YES) {
|
|
341
|
+
console.log();
|
|
342
|
+
await writeTsConfigs(results);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Summary ────────────────────────────────────────────────────────────────
|
|
346
|
+
|
|
347
|
+
console.log();
|
|
348
|
+
console.log(dim(' ─────────────────────────────────────────'));
|
|
349
|
+
console.log(` ${green(results.copied.length + ' copied')} ${yellow(results.skipped.length + ' skipped')} ${results.failed.length > 0 ? `\x1b[31m${results.failed.length} failed\x1b[0m` : dim('0 failed')}`);
|
|
350
|
+
console.log();
|
|
351
|
+
|
|
352
|
+
if (results.failed.length === 0) {
|
|
353
|
+
console.log(' ' + bold('Done.') + ' Reload VS Code to activate Copilot instructions.');
|
|
354
|
+
console.log(dim(' Commit the generated files — they are workspace configuration.\n'));
|
|
332
355
|
} else {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const dest = join(cwd, 'tsconfig.json');
|
|
336
|
-
const content = generateTsConfig(preset);
|
|
337
|
-
await writeIfConfirmed(dest, content, 'tsconfig.json');
|
|
356
|
+
console.log(' Some files could not be copied. Check the errors above.\n');
|
|
357
|
+
process.exit(1);
|
|
338
358
|
}
|
|
339
359
|
}
|
|
340
360
|
|
|
341
|
-
|
|
342
|
-
console.
|
|
343
|
-
console.log(` ${green(results.copied.length + ' copied')} ${yellow(results.skipped.length + ' skipped')} ${results.failed.length > 0 ? `\x1b[31m${results.failed.length} failed\x1b[0m` : dim('0 failed')}`);
|
|
344
|
-
console.log();
|
|
345
|
-
|
|
346
|
-
if (results.failed.length === 0) {
|
|
347
|
-
console.log(' ' + bold('Done.') + ' Reload VS Code to activate Copilot instructions.');
|
|
348
|
-
console.log(dim(' Commit the generated files — they are workspace configuration.\n'));
|
|
349
|
-
} else {
|
|
350
|
-
console.log(' Some files could not be copied. Check the errors above.\n');
|
|
361
|
+
main().catch(err => {
|
|
362
|
+
console.error(err);
|
|
351
363
|
process.exit(1);
|
|
352
|
-
}
|
|
364
|
+
});
|