@tsfpp/agents 1.3.4 → 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 +8 -0
- package/copilot/agents/tsfpp-audit.agent.md +160 -24
- package/copilot/agents/tsfpp-backfill-tests.agent.md +40 -1
- 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,14 @@ 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
|
+
|
|
13
21
|
## [1.3.4] - 2026-05-18
|
|
14
22
|
|
|
15
23
|
### Changed
|
|
@@ -129,17 +129,48 @@ Append each completed slice to the report:
|
|
|
129
129
|
|
|
130
130
|
#### Checklist
|
|
131
131
|
|
|
132
|
-
|
|
133
|
-
- [ ] 1.
|
|
134
|
-
- [
|
|
135
|
-
- [
|
|
136
|
-
- [
|
|
137
|
-
- [
|
|
138
|
-
- [ ]
|
|
139
|
-
- [
|
|
140
|
-
- [
|
|
141
|
-
- [
|
|
142
|
-
- [
|
|
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
|
|
143
174
|
|
|
144
175
|
#### Deviation register
|
|
145
176
|
|
|
@@ -153,29 +184,134 @@ Append each completed slice to the report:
|
|
|
153
184
|
## Focus-specific rule sets
|
|
154
185
|
|
|
155
186
|
### `types`
|
|
156
|
-
|
|
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
|
|
157
204
|
|
|
158
205
|
### `boundary`
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
|
167
246
|
|
|
168
247
|
### `complexity`
|
|
169
|
-
|
|
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)
|
|
170
258
|
|
|
171
259
|
### `loc`
|
|
172
|
-
|
|
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
|
|
173
268
|
|
|
174
269
|
### `annotations`
|
|
175
|
-
|
|
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`
|
|
176
288
|
|
|
177
289
|
### `security`
|
|
178
|
-
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
|
|
179
315
|
|
|
180
316
|
### `prelude`
|
|
181
317
|
Cross-cutting — applies to all layers. Check for hand-rolled patterns that `@tsfpp/prelude` already provides.
|
|
@@ -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:
|
|
@@ -163,6 +170,36 @@ All rules from `TEST_CODING_STANDARD.md` apply:
|
|
|
163
170
|
|
|
164
171
|
---
|
|
165
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
|
+
|
|
166
203
|
## Layer-specific patterns
|
|
167
204
|
|
|
168
205
|
### `core`
|
|
@@ -199,9 +236,11 @@ describe('mkTrackId', () => {
|
|
|
199
236
|
|
|
200
237
|
```ts
|
|
201
238
|
it('responds with 201 and a Location header on valid input', async () => {
|
|
239
|
+
const input = makeCreateTrackInput() // from tests/factories/track.factory.ts
|
|
240
|
+
|
|
202
241
|
const req = new Request('http://localhost/v1/tracks', {
|
|
203
242
|
method: 'POST',
|
|
204
|
-
body: JSON.stringify(
|
|
243
|
+
body: JSON.stringify(input),
|
|
205
244
|
headers: { 'Content-Type': 'application/json' },
|
|
206
245
|
})
|
|
207
246
|
|
|
@@ -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
|
+
});
|