@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 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
- - [x] 1.4 — No bare `interface` (or DEVIATION documented)
133
- - [ ] 1.5No `any`
134
- - [x] 1.6No `!` assertions
135
- - [x] 2.x — `readonly` fields and `ReadonlyArray`
136
- - [x] 3.x — `const` bindings only
137
- - [x] 4.1Exhaustive `switch` with `absurd`
138
- - [ ] 4.5 — No truthiness checks on non-booleans
139
- - [x] 5.1Pipelines via `pipe` from prelude
140
- - [x] 6.x — No `throw` in core
141
- - [x] 7.xJSDoc on all exports
142
- - [x] 9.xNo direct `ramda` import
132
+ **Types and ADTs (§1)**
133
+ - [ ] 1.1Sum types modelled as tagged discriminated union with literal discriminant
134
+ - [ ] 1.2Exhaustive `switch` ends in `default: return absurd(x)`
135
+ - [ ] 1.3Nominal distinctions via branded types; only smart constructors (`mk*`, `from*`, `as*`) cast with `as`
136
+ - [ ] 1.4No bare `interface` (or `// DEVIATION(1.4): <reason>` present)
137
+ - [ ] 1.5No `any`; `unknown` used at I/O boundaries, narrowed in scope
138
+ - [ ] 1.6 — No `!`; no `as` outside smart constructor bodies
139
+ - [ ] 1.8No `enum`; use string literal unions or `as const`
140
+ - [ ] 1.9 — No `class` · `this` · `new` · `instanceof` · `namespace`
141
+ - [ ] 1.11Prelude ADT discriminants accessed via exported guards only (`isOk`, `isSome`)
142
+ - [ ] 1.12Discriminant 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
- 1.4 (no bare interface) · 1.5 (no `any`) · 1.6 (no `!` or `as`) · 3.x (readonly) · branded types on domain primitives · smart constructor completeness · exhaustive sum-type dispatch
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
- API_CODING_STANDARD.md (full) + `@tsfpp/boundary` surface:
160
- `extractContext` called at the top of every handler · Zod `safeParse` at every input boundary lifted via `fromZodError` ·
161
- all handlers return `Result<T, ApiError>` internally · `apiErrorToResponse` used for all error paths · no raw `throw` ·
162
- response builders (`okResponse`, `createdResponse`, `noContentResponse`, etc.) used; no hand-built `new Response()` ·
163
- `rateLimitHeaders` on all responses for rate-limited endpoints · `corsHeaders` never reflects `Origin` blindly ·
164
- `withIdempotency` + `withRequestLog` composed via `pipe` · pagination via `mkPaginated` + `parsePaginationQuery` ·
165
- LRO via `acceptedResponse` + `mkRunningOp`/`mkSucceededOp` · bulk via `bulkResponse` + `mkBulkOkItem`/`mkBulkErrorItem` ·
166
- handler architecture: parse domain map use-case → response map (nothing else)
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
- Function body ≤ 40 lines · cyclomatic complexity ≤ 10 · nesting ≤ 4 · arity ≤ 3 positional params · pipeline depth ≤ 8 stages
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
- File LOC · function LOC · god-module candidates · decomposition opportunities
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
- JSDoc on every export · `@param` + `@returns` present · `@law` on combinators · DEVIATION comments formatted correctly · TODO/HACK/FIXME/NOTE/OPTIMIZE/BUG/XXX have date + author + ticket
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: input validation at boundaries · no secrets in code · no sensitive data in errors · auth/authz at correct layer · dependency hygiene
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({ title: 'Test', artistId: 'a1' }),
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 result = mkTrackId('abc')
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
- expect(mkTrackId('')).toEqual(none)
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: 'POST',
204
- body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
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: 'POST',
217
- body: JSON.stringify({ artistId: 'a1' }),
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 configuration
6
- * into the correct locations in the consumer's project. Also generates
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
- * pnpm dlx @tsfpp/agents (one-shot, no install)
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 } from 'node:fs';
16
- import { join, dirname } from 'node:path';
17
- import { fileURLToPath } from 'node:url';
18
- import { createInterface } from 'node:readline';
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
- // Each entry: [source (relative to this file), destination (relative to cwd)]
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', '.github/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', '.github/instructions/tsfpp-base.instructions.md'],
37
- ['copilot/instructions/tsfpp-prelude.instructions.md', '.github/instructions/tsfpp-prelude.instructions.md'],
38
- ['copilot/instructions/tsfpp-api.instructions.md', '.github/instructions/tsfpp-api.instructions.md'],
39
- ['copilot/instructions/tsfpp-react.instructions.md', '.github/instructions/tsfpp-react.instructions.md'],
40
- ['copilot/instructions/tsfpp-testing.instructions.md', '.github/instructions/tsfpp-testing.instructions.md'],
41
- ['copilot/instructions/trunk.instructions.md', '.github/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', '.github/agents/tsfpp-tdd.agent.md'],
45
- ['copilot/agents/tsfpp-backfill-tests.agent.md', '.github/agents/tsfpp-backfill-tests.agent.md'],
46
- ['copilot/agents/tsfpp-guarded-coding.agent.md', '.github/agents/tsfpp-guarded-coding.agent.md'],
47
- ['copilot/agents/tsfpp-audit.agent.md', '.github/agents/tsfpp-audit.agent.md'],
48
- ['copilot/agents/tsfpp-refactor-engineer.agent.md', '.github/agents/tsfpp-refactor-engineer.agent.md'],
49
- ['copilot/agents/tsfpp-annotate.agent.md', '.github/agents/tsfpp-annotate.agent.md'],
50
-
51
- // Reusable prompts
52
- ['copilot/prompts/trunk-init-repo.prompt.md', '.github/prompts/trunk-init-repo.prompt.md'],
53
- ['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
54
- ['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
55
-
56
- // Reusable skills
57
- ['copilot/skills/coding-standard/SKILL.md', '.github/skills/coding-standard/SKILL.md'],
58
- ['copilot/skills/prelude-api/SKILL.md', '.github/skills/prelude-api/SKILL.md'],
59
- ['copilot/skills/boundary-api/SKILL.md', '.github/skills/boundary-api/SKILL.md'],
60
- ['copilot/skills/react-coding-standard/SKILL.md', '.github/skills/react-coding-standard/SKILL.md'],
61
- ['copilot/skills/test-standard/SKILL.md', '.github/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', '.claude/CLAUDE.md'],
69
+ ['claude/CLAUDE.md', '.claude/CLAUDE.md'],
65
70
  ];
66
71
 
67
- // ─── ESLint config generation ─────────────────────────────────────────────────
72
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
68
73
 
69
- const PROFILES = {
70
- base: { import: `import tsfpp from '@tsfpp/eslint-config'`, spread: 'tsfpp' },
71
- react: { import: `import tsfppReact from '@tsfpp/eslint-config/react'`, spread: 'tsfppReact' },
72
- api: { import: `import tsfppApi from '@tsfpp/eslint-config/api'`, spread: 'tsfppApi' },
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(/\/\*\*?$/, '')); // strip trailing /* or /**
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 Profile for ${bold(label)}:`);
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 generateMonorepoConfig(packageProfiles) {
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 scopedProfiles = ['react', 'api'].flatMap(profile => {
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 = PROFILES[profile].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 generateSingleConfig(profile) {
147
- const { import: imp, spread } = PROFILES[profile];
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
- packageProfiles[pkg] = await askProfile(pkg);
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 = generateSingleConfig(profile);
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
- // ─── Helpers ─────────────────────────────────────────────────────────────────
190
+ // ─── tsconfig generation ──────────────────────────────────────────────────────
182
191
 
183
- async function ensureDir(filePath) {
184
- await mkdir(dirname(filePath), { recursive: true });
185
- }
192
+ const TSCONFIG_PRESETS = {
193
+ app: '@tsfpp/tsconfig/app',
194
+ lib: '@tsfpp/tsconfig/lib',
195
+ };
186
196
 
187
- async function ask(question) {
188
- const rl = createInterface({ input: process.stdin, output: process.stdout });
189
- return new Promise((resolve) => {
190
- rl.question(question, (answer) => {
191
- rl.close();
192
- resolve(answer.trim().toLowerCase());
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
- async function confirm(question) {
198
- return (await ask(question)) === 'y';
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
- // ─── Main ─────────────────────────────────────────────────────────────────────
202
-
203
- console.log();
204
- console.log(bold(' @tsfpp/agents — init'));
205
- console.log(dim(' Sets up Copilot agents, instructions, prompts, skills, and ESLint config.\n'));
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('!')} ${dest} already exists. Overwrite? ${dim('[y/N]')} `
224
+ ` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
218
225
  );
219
226
  if (!overwrite) {
220
- results.skipped.push(dest);
221
- console.log(` ${dim('–')} ${dim(dest)} ${dim('(skipped)')}`);
222
- continue;
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 copyFile(srcPath, destPath);
229
- results.copied.push(dest);
230
- console.log(` ${green('✓')} ${dest}`);
234
+ await writeFile(destPath, content, 'utf8');
235
+ results.copied.push(label);
236
+ console.log(` ${green('✓')} ${label}`);
231
237
  } catch (err) {
232
- results.failed.push(dest);
233
- console.log(` \x1b[31m✗\x1b[0m ${dest} ${dim(`(${err.message})`)}`);
238
+ results.failed.push(label);
239
+ console.log(` \x1b[31m✗\x1b[0m ${label} ${dim(`(${err.message})`)}`);
234
240
  }
235
241
  }
236
242
 
237
- // ── Generate eslint.config.js ─────────────────────────────────────────────────
238
-
239
- console.log();
243
+ async function writeTsConfigs(results) {
244
+ const packages = await detectWorkspacePackages();
240
245
 
241
- const eslintDest = join(cwd, 'eslint.config.js');
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
- if (existsSync(eslintDest)) {
244
- const overwrite = await confirm(
245
- ` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
246
- );
247
- if (!overwrite) {
248
- results.skipped.push('eslint.config.js');
249
- console.log(` ${dim('')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
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 writeEslintConfig();
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
- // ── Generate tsconfig.json ────────────────────────────────────────────────────
271
+ // ─── Main ─────────────────────────────────────────────────────────────────────
258
272
 
259
- console.log();
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
- await writeTsConfigs(await detectWorkspacePackages());
282
+ const results = { copied: [], skipped: [], failed: [] };
262
283
 
263
- async function writeTsConfigs(packages) {
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
- async function askPreset(label) {
270
- console.log(`\n tsconfig preset for ${bold(label)}:`);
271
- console.log(` ${dim('1')} app ${dim('— application / tool (noEmit: true)')}`);
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
- function generateTsConfig(preset, extra = {}) {
278
- return JSON.stringify({
279
- extends: PRESETS[preset].extends,
280
- compilerOptions: { rootDir: 'src', ...extra },
281
- include: ['src'],
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
- async function writeIfConfirmed(destPath, content, label) {
293
- if (existsSync(destPath)) {
296
+ if (existsSync(destPath) && !YES) {
294
297
  const overwrite = await confirm(
295
- ` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
298
+ ` ${yellow('!')} ${dest} already exists. Overwrite? ${dim('[y/N]')} `
296
299
  );
297
300
  if (!overwrite) {
298
- results.skipped.push(label);
299
- console.log(` ${dim('–')} ${dim(label)} ${dim('(skipped)')}`);
300
- return;
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 writeFile(destPath, content, 'utf8');
306
- results.copied.push(label);
307
- console.log(` ${green('✓')} ${label}`);
309
+ await copyFile(srcPath, destPath);
310
+ results.copied.push(dest);
311
+ console.log(` ${green('✓')} ${dest}`);
308
312
  } catch (err) {
309
- results.failed.push(label);
310
- console.log(` \x1b[31m✗\x1b[0m ${label} ${dim(`(${err.message})`)}`);
313
+ results.failed.push(dest);
314
+ console.log(` \x1b[31m✗\x1b[0m ${dest} ${dim(`(${err.message})`)}`);
311
315
  }
312
316
  }
313
317
 
314
- if (packages) {
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
- for (const [pkg, preset] of Object.entries(packagePresets)) {
323
- const destPath = join(cwd, pkg, 'tsconfig.json');
324
- const content = generateTsConfig(preset);
325
- await writeIfConfirmed(destPath, content, `${pkg}/tsconfig.json`);
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
- // Root references tsconfig
329
- const rootDest = join(cwd, 'tsconfig.json');
330
- const rootContent = generateRootTsConfig(packages);
331
- await writeIfConfirmed(rootDest, rootContent, 'tsconfig.json (root references)');
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
- // Single package
334
- const preset = await askPreset('this project');
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
- console.log();
342
- console.log(dim(' ─────────────────────────────────────────'));
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
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsfpp/agents",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
4
4
  "description": "Workspace AI tooling for TSF++ projects: scoped instructions, coding agents, and reusable prompts",
5
5
  "keywords": [
6
6
  "tsfpp",