@tsfpp/agents 1.2.3 → 1.3.1
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 +34 -0
- package/copilot/agents/tsfpp-audit.agent.md +88 -10
- package/copilot/agents/tsfpp-guarded-coding.agent.md +58 -4
- package/copilot/agents/tsfpp-tdd.agent.md +292 -0
- package/copilot/copilot-instructions.md +143 -66
- package/copilot/instructions/tsfpp-api.instructions.md +104 -39
- package/copilot/instructions/tsfpp-base.instructions.md +18 -7
- package/copilot/instructions/tsfpp-prelude.instructions.md +95 -47
- package/copilot/instructions/tsfpp-react.instructions.md +152 -40
- package/copilot/instructions/tsfpp-testing.instructions.md +154 -0
- package/copilot/skills/test-standard/SKILL.md +238 -0
- package/init.mjs +67 -84
- package/package.json +1 -1
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: test-standard
|
|
3
|
+
description: >
|
|
4
|
+
Normative TSF++ testing rules and idioms for all test files. Load when writing
|
|
5
|
+
or reviewing *.test.ts or *.test.tsx files: toolchain per layer (Vitest,
|
|
6
|
+
fast-check, RTL, MSW, testcontainers), AAA structure, property-based test
|
|
7
|
+
patterns, in-memory port stubs, factory conventions, coverage requirements,
|
|
8
|
+
and forbidden patterns (data-testid, vi.fn() for ports, snapshot tests,
|
|
9
|
+
implementation assertions). Load alongside prelude-api when writing core tests.
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# TSF++ test standard
|
|
13
|
+
|
|
14
|
+
Full standard: `node_modules/@tsfpp/standard/spec/TEST_CODING_STANDARD.md`
|
|
15
|
+
|
|
16
|
+
All base TSF++ rules apply to test code. No `any`, no `let`, no forbidden constructs.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Toolchain per layer
|
|
21
|
+
|
|
22
|
+
| Layer | Runner | Property tests | Network | DB |
|
|
23
|
+
|---|---|---|---|---|
|
|
24
|
+
| Core | Vitest | fast-check — required | — | — |
|
|
25
|
+
| Use-case | Vitest | fast-check — optional | — | In-memory stub |
|
|
26
|
+
| API / handler | Vitest | — | MSW | In-memory stub |
|
|
27
|
+
| DAL | Vitest | — | — | Real / containerised |
|
|
28
|
+
| React | Vitest + RTL | — | MSW | — |
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Structure — AAA
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
it('returns None when the input string is empty', () => {
|
|
36
|
+
const raw = '' // Arrange
|
|
37
|
+
|
|
38
|
+
const result = mkTrackId(raw) // Act
|
|
39
|
+
|
|
40
|
+
expect(result).toEqual(none) // Assert
|
|
41
|
+
})
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
One blank line between phases. One logical assertion concept per test.
|
|
45
|
+
Test descriptions are full sentences describing behaviour — never implementation echoes.
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
// Good
|
|
49
|
+
it('returns None when the input string is empty')
|
|
50
|
+
it('responds with 422 when title is missing')
|
|
51
|
+
|
|
52
|
+
// Bad
|
|
53
|
+
it('mkTrackId empty')
|
|
54
|
+
it('handler validation test')
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Describe block structure
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
describe('mkTrackId', () => {
|
|
63
|
+
describe('when the input is valid', () => {
|
|
64
|
+
it('returns Some containing a branded TrackId', () => { ... })
|
|
65
|
+
})
|
|
66
|
+
describe('when the input is empty', () => {
|
|
67
|
+
it('returns None', () => { ... })
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
Max two levels of nesting. No branching or loops in test bodies.
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Property-based tests — fast-check
|
|
77
|
+
|
|
78
|
+
Required for every pure function and every `@law` in JSDoc.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import * as fc from 'fast-check'
|
|
82
|
+
|
|
83
|
+
// Specific case
|
|
84
|
+
it('returns Some for a non-empty string', () => {
|
|
85
|
+
expect(isSome(mkTrackId('abc'))).toBe(true)
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Law — holds for all inputs
|
|
89
|
+
it('satisfies: any non-empty string is accepted', () => {
|
|
90
|
+
fc.assert(
|
|
91
|
+
fc.property(fc.string({ minLength: 1 }), (s) => {
|
|
92
|
+
expect(isSome(mkTrackId(s))).toBe(true)
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
// Result identity law
|
|
98
|
+
it('satisfies map(id) ≡ id', () => {
|
|
99
|
+
fc.assert(
|
|
100
|
+
fc.property(fc.integer(), (n) => {
|
|
101
|
+
expect(pipe(ok(n), map(x => x))).toEqual(ok(n))
|
|
102
|
+
}),
|
|
103
|
+
)
|
|
104
|
+
})
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## React components — RTL
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { render, screen } from '@testing-library/react'
|
|
113
|
+
import userEvent from '@testing-library/user-event'
|
|
114
|
+
|
|
115
|
+
it('calls onSelect with the track id when clicked', async () => {
|
|
116
|
+
const onSelect = vi.fn()
|
|
117
|
+
render(<TrackCard track={makeTrack()} onSelect={some(onSelect)} />)
|
|
118
|
+
|
|
119
|
+
await userEvent.click(screen.getByRole('article'))
|
|
120
|
+
|
|
121
|
+
expect(onSelect).toHaveBeenCalledWith(expect.any(String))
|
|
122
|
+
})
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Query hierarchy — use the first that works:
|
|
126
|
+
1. `getByRole`
|
|
127
|
+
2. `getByLabelText`
|
|
128
|
+
3. `getByText`
|
|
129
|
+
4. `getByPlaceholderText`
|
|
130
|
+
|
|
131
|
+
Never `getByTestId`.
|
|
132
|
+
|
|
133
|
+
---
|
|
134
|
+
|
|
135
|
+
## Network — MSW
|
|
136
|
+
|
|
137
|
+
```ts
|
|
138
|
+
import { http, HttpResponse } from 'msw'
|
|
139
|
+
import { setupServer } from 'msw/node'
|
|
140
|
+
|
|
141
|
+
const server = setupServer(
|
|
142
|
+
http.get('/api/tracks', () => HttpResponse.json(fixtures)),
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
beforeAll(() => server.listen())
|
|
146
|
+
afterEach(() => server.resetHandlers())
|
|
147
|
+
afterAll(() => server.close())
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Never stub `fetch`, `axios`, or any HTTP client directly.
|
|
151
|
+
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
## Port stubs — in-memory only
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
// Good — typed in-memory implementation
|
|
158
|
+
const repo = mkInMemoryTrackRepository()
|
|
159
|
+
|
|
160
|
+
// Bad — partial vi.fn() mock
|
|
161
|
+
const repo = { findById: vi.fn().mockResolvedValue(track) }
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`vi.fn()` is permitted only for standalone callbacks (`onClose`, `onSelect`, etc.).
|
|
165
|
+
|
|
166
|
+
---
|
|
167
|
+
|
|
168
|
+
## API / handler tests
|
|
169
|
+
|
|
170
|
+
```ts
|
|
171
|
+
it('responds with 201 and a Location header on valid input', async () => {
|
|
172
|
+
const req = new Request('http://localhost/v1/tracks', {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: JSON.stringify({ title: 'Test', artistId: 'a1' }),
|
|
175
|
+
headers: { 'Content-Type': 'application/json' },
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
const res = await handler(req)
|
|
179
|
+
|
|
180
|
+
expect(res.status).toBe(201)
|
|
181
|
+
expect(res.headers.get('Location')).toMatch(/\/v1\/tracks\//)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('responds with 422 when title is missing', async () => {
|
|
185
|
+
const req = new Request('http://localhost/v1/tracks', {
|
|
186
|
+
method: 'POST',
|
|
187
|
+
body: JSON.stringify({ artistId: 'a1' }),
|
|
188
|
+
headers: { 'Content-Type': 'application/json' },
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
const res = await handler(req)
|
|
192
|
+
|
|
193
|
+
expect(res.status).toBe(422)
|
|
194
|
+
})
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Factories
|
|
200
|
+
|
|
201
|
+
```ts
|
|
202
|
+
// tests/factories/track.factory.ts
|
|
203
|
+
const makeTrack = (overrides: Partial<Track> = {}): Track => ({
|
|
204
|
+
id: mkTrackId('test-track-001'),
|
|
205
|
+
title: 'Default Title',
|
|
206
|
+
artistId: mkArtistId('test-artist-001'),
|
|
207
|
+
...overrides,
|
|
208
|
+
})
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
- Factories live in `tests/factories/` — never inline raw object literals
|
|
212
|
+
- IDs are deterministic strings that cannot collide with real data
|
|
213
|
+
- Never copy IDs from production or staging
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## Coverage requirements
|
|
218
|
+
|
|
219
|
+
- Every public export has at least one test for the primary success case
|
|
220
|
+
- Every error path (`Err`, `None`, non-2xx) has a corresponding test
|
|
221
|
+
- Every branch, switch case, and ternary arm is exercised
|
|
222
|
+
- Minimum enforced: 80 % statements, 80 % branches per package
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Never
|
|
227
|
+
|
|
228
|
+
| Forbidden | Use instead |
|
|
229
|
+
|---|---|
|
|
230
|
+
| `getByTestId` | `getByRole`, `getByLabelText`, `getByText` |
|
|
231
|
+
| `vi.fn()` for a port interface | In-memory implementation |
|
|
232
|
+
| Assert internal function was called | Assert observable outcome |
|
|
233
|
+
| `any` in test code | Typed fixtures and factories |
|
|
234
|
+
| `setTimeout` delays | `waitFor` / `findBy*` |
|
|
235
|
+
| `beforeAll` for mutable state | `beforeEach` |
|
|
236
|
+
| Snapshot tests for components or API shape | Explicit assertions |
|
|
237
|
+
| Shallow rendering | RTL full render |
|
|
238
|
+
| Access non-exported symbols | Test through the public API |
|
package/init.mjs
CHANGED
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* Usage:
|
|
10
10
|
* pnpm dlx @tsfpp/agents (one-shot, no install)
|
|
11
11
|
* node node_modules/@tsfpp/agents/init.mjs
|
|
12
|
-
* node node_modules/@tsfpp/agents/init.mjs --yes (overwrite all without prompting)
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import { copyFile, mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
|
|
@@ -21,8 +20,6 @@ import { createInterface } from 'node:readline';
|
|
|
21
20
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
22
21
|
const cwd = process.cwd();
|
|
23
22
|
|
|
24
|
-
const yes = process.argv.includes('--yes') || process.argv.includes('-y');
|
|
25
|
-
|
|
26
23
|
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
27
24
|
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
28
25
|
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
@@ -33,37 +30,36 @@ const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
|
33
30
|
|
|
34
31
|
const FILES = [
|
|
35
32
|
// Always-on workspace instructions
|
|
36
|
-
['copilot/copilot-instructions.md',
|
|
33
|
+
['copilot/copilot-instructions.md', '.github/copilot-instructions.md'],
|
|
37
34
|
|
|
38
35
|
// Scoped instruction files
|
|
39
|
-
['copilot/instructions/tsfpp-base.instructions.md',
|
|
40
|
-
['copilot/instructions/tsfpp-
|
|
41
|
-
['copilot/instructions/tsfpp-api.instructions.md',
|
|
42
|
-
['copilot/instructions/tsfpp-
|
|
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'],
|
|
43
42
|
|
|
44
43
|
// Agents
|
|
45
|
-
['copilot/agents/tsfpp-
|
|
46
|
-
['copilot/agents/tsfpp-
|
|
47
|
-
['copilot/agents/tsfpp-
|
|
48
|
-
['copilot/agents/tsfpp-
|
|
49
|
-
['copilot/agents/
|
|
50
|
-
['copilot/agents/trunk-release.agent.md', '.github/agents/trunk-release.agent.md'],
|
|
51
|
-
|
|
52
|
-
// Trunk workflow instructions
|
|
53
|
-
['copilot/instructions/trunk.instructions.md', '.github/instructions/trunk.instructions.md'],
|
|
44
|
+
['copilot/agents/tsfpp-tdd.agent.md', '.github/agents/tsfpp-tdd.agent.md'],
|
|
45
|
+
['copilot/agents/tsfpp-guarded-coding.agent.md', '.github/agents/tsfpp-guarded-coding.agent.md'],
|
|
46
|
+
['copilot/agents/tsfpp-audit.agent.md', '.github/agents/tsfpp-audit.agent.md'],
|
|
47
|
+
['copilot/agents/tsfpp-refactor-engineer.agent.md', '.github/agents/tsfpp-refactor-engineer.agent.md'],
|
|
48
|
+
['copilot/agents/tsfpp-annotate.agent.md', '.github/agents/tsfpp-annotate.agent.md'],
|
|
54
49
|
|
|
55
50
|
// Reusable prompts
|
|
56
|
-
['copilot/prompts/tsfpp-new-module.prompt.md',
|
|
57
|
-
['copilot/prompts/tsfpp-boundary-review.prompt.md',
|
|
51
|
+
['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
|
|
52
|
+
['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
|
|
58
53
|
|
|
59
54
|
// Reusable skills
|
|
60
|
-
['copilot/skills/coding-standard/SKILL.md',
|
|
61
|
-
['copilot/skills/prelude-api/SKILL.md',
|
|
62
|
-
['copilot/skills/boundary-api/SKILL.md',
|
|
63
|
-
['copilot/skills/react-coding-standard/SKILL.md',
|
|
55
|
+
['copilot/skills/coding-standard/SKILL.md', '.github/skills/coding-standard/SKILL.md'],
|
|
56
|
+
['copilot/skills/prelude-api/SKILL.md', '.github/skills/prelude-api/SKILL.md'],
|
|
57
|
+
['copilot/skills/boundary-api/SKILL.md', '.github/skills/boundary-api/SKILL.md'],
|
|
58
|
+
['copilot/skills/react-coding-standard/SKILL.md', '.github/skills/react-coding-standard/SKILL.md'],
|
|
59
|
+
['copilot/skills/test-standard/SKILL.md', '.github/skills/test-standard/SKILL.md'],
|
|
64
60
|
|
|
65
61
|
// Claude Code
|
|
66
|
-
['claude/CLAUDE.md',
|
|
62
|
+
['claude/CLAUDE.md', '.claude/CLAUDE.md'],
|
|
67
63
|
];
|
|
68
64
|
|
|
69
65
|
// ─── ESLint config generation ─────────────────────────────────────────────────
|
|
@@ -99,9 +95,7 @@ async function askProfile(label) {
|
|
|
99
95
|
console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
|
|
100
96
|
console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
|
|
101
97
|
console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
|
|
102
|
-
|
|
103
|
-
const choice = await ask(` ${dim('[1/2/3/n, default: 1]')} `);
|
|
104
|
-
if (choice === 'n') return null;
|
|
98
|
+
const choice = await ask(` ${dim('[1/2/3, default: 1]')} `);
|
|
105
99
|
return choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
|
|
106
100
|
}
|
|
107
101
|
|
|
@@ -148,7 +142,7 @@ function generateSingleConfig(profile) {
|
|
|
148
142
|
return `${imp}\nexport default [...${spread}]\n`;
|
|
149
143
|
}
|
|
150
144
|
|
|
151
|
-
async function writeEslintConfig(
|
|
145
|
+
async function writeEslintConfig() {
|
|
152
146
|
const packages = await detectWorkspacePackages();
|
|
153
147
|
|
|
154
148
|
let content;
|
|
@@ -160,17 +154,12 @@ async function writeEslintConfig(results) {
|
|
|
160
154
|
for (const pkg of packages) {
|
|
161
155
|
packageProfiles[pkg] = await askProfile(pkg);
|
|
162
156
|
}
|
|
163
|
-
|
|
164
|
-
Object.entries(packageProfiles).filter(([, p]) => p !== null)
|
|
165
|
-
);
|
|
166
|
-
if (Object.keys(activeProfiles).length === 0) return;
|
|
167
|
-
content = generateMonorepoConfig(activeProfiles);
|
|
157
|
+
content = generateMonorepoConfig(packageProfiles);
|
|
168
158
|
description = 'monorepo';
|
|
169
159
|
} else {
|
|
170
160
|
const profile = await askProfile('this project');
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
description = `profile: ${profile}`;
|
|
161
|
+
content = generateSingleConfig(profile);
|
|
162
|
+
description = `profile: ${profile}`;
|
|
174
163
|
}
|
|
175
164
|
|
|
176
165
|
try {
|
|
@@ -200,23 +189,16 @@ async function ask(question) {
|
|
|
200
189
|
}
|
|
201
190
|
|
|
202
191
|
async function confirm(question) {
|
|
203
|
-
if (yes) return true;
|
|
204
192
|
return (await ask(question)) === 'y';
|
|
205
193
|
}
|
|
206
194
|
|
|
207
195
|
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
208
196
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
async function main() {
|
|
215
|
-
console.log();
|
|
216
|
-
console.log(bold(' @tsfpp/agents — init'));
|
|
217
|
-
console.log(dim(' Sets up Copilot agents, instructions, prompts, skills, and ESLint config.\n'));
|
|
197
|
+
console.log();
|
|
198
|
+
console.log(bold(' @tsfpp/agents — init'));
|
|
199
|
+
console.log(dim(' Sets up Copilot agents, instructions, prompts, skills, and ESLint config.\n'));
|
|
218
200
|
|
|
219
|
-
|
|
201
|
+
const results = { copied: [], skipped: [], failed: [] };
|
|
220
202
|
|
|
221
203
|
// ── Copy files ────────────────────────────────────────────────────────────────
|
|
222
204
|
|
|
@@ -253,32 +235,45 @@ console.log();
|
|
|
253
235
|
const eslintDest = join(cwd, 'eslint.config.js');
|
|
254
236
|
|
|
255
237
|
if (existsSync(eslintDest)) {
|
|
256
|
-
|
|
238
|
+
const overwrite = await confirm(
|
|
239
|
+
` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
|
|
240
|
+
);
|
|
241
|
+
if (!overwrite) {
|
|
257
242
|
results.skipped.push('eslint.config.js');
|
|
258
|
-
console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped
|
|
243
|
+
console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
|
|
259
244
|
} else {
|
|
260
|
-
|
|
261
|
-
` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
|
|
262
|
-
);
|
|
263
|
-
if (overwrite) await writeEslintConfig(results);
|
|
264
|
-
else {
|
|
265
|
-
results.skipped.push('eslint.config.js');
|
|
266
|
-
console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
|
|
267
|
-
}
|
|
245
|
+
await writeEslintConfig();
|
|
268
246
|
}
|
|
269
247
|
} else {
|
|
270
|
-
await writeEslintConfig(
|
|
248
|
+
await writeEslintConfig();
|
|
271
249
|
}
|
|
272
250
|
|
|
251
|
+
async function writeEslintConfig() {
|
|
252
|
+
console.log(` Which ESLint profile does this project use?`);
|
|
253
|
+
console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
|
|
254
|
+
console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
|
|
255
|
+
console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
|
|
273
256
|
|
|
257
|
+
const choice = await ask(` ${dim('[1/2/3, default: 1]')} `);
|
|
258
|
+
const profile = choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
|
|
259
|
+
|
|
260
|
+
try {
|
|
261
|
+
await writeFile(eslintDest, ESLINT_PROFILES[profile], 'utf8');
|
|
262
|
+
results.copied.push('eslint.config.js');
|
|
263
|
+
console.log(` ${green('✓')} eslint.config.js ${dim(`(profile: ${profile})`)}`);
|
|
264
|
+
} catch (err) {
|
|
265
|
+
results.failed.push('eslint.config.js');
|
|
266
|
+
console.log(` \x1b[31m✗\x1b[0m eslint.config.js ${dim(`(${err.message})`)}`);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
274
269
|
|
|
275
270
|
// ── Generate tsconfig.json ────────────────────────────────────────────────────
|
|
276
271
|
|
|
277
272
|
console.log();
|
|
278
273
|
|
|
279
|
-
await writeTsConfigs(await detectWorkspacePackages()
|
|
274
|
+
await writeTsConfigs(await detectWorkspacePackages());
|
|
280
275
|
|
|
281
|
-
async function writeTsConfigs(packages
|
|
276
|
+
async function writeTsConfigs(packages) {
|
|
282
277
|
const PRESETS = {
|
|
283
278
|
app: { extends: '@tsfpp/tsconfig/app', label: 'app — application / tool (noEmit)' },
|
|
284
279
|
lib: { extends: '@tsfpp/tsconfig/lib', label: 'lib — publishable package (declaration, composite)' },
|
|
@@ -288,9 +283,7 @@ async function writeTsConfigs(packages, results) {
|
|
|
288
283
|
console.log(`\n tsconfig preset for ${bold(label)}:`);
|
|
289
284
|
console.log(` ${dim('1')} app ${dim('— application / tool (noEmit: true)')}`);
|
|
290
285
|
console.log(` ${dim('2')} lib ${dim('— publishable package (declaration, composite)')}`);
|
|
291
|
-
|
|
292
|
-
const choice = await ask(` ${dim('[1/2/n, default: 1]')} `);
|
|
293
|
-
if (choice === 'n') return null;
|
|
286
|
+
const choice = await ask(` ${dim('[1/2, default: 1]')} `);
|
|
294
287
|
return choice === '2' ? 'lib' : 'app';
|
|
295
288
|
}
|
|
296
289
|
|
|
@@ -311,11 +304,6 @@ async function writeTsConfigs(packages, results) {
|
|
|
311
304
|
|
|
312
305
|
async function writeIfConfirmed(destPath, content, label) {
|
|
313
306
|
if (existsSync(destPath)) {
|
|
314
|
-
if (yes) {
|
|
315
|
-
results.skipped.push(label);
|
|
316
|
-
console.log(` ${dim('–')} ${dim(label)} ${dim('(skipped — project-managed)')}`);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
307
|
const overwrite = await confirm(
|
|
320
308
|
` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
|
|
321
309
|
);
|
|
@@ -345,7 +333,6 @@ async function writeTsConfigs(packages, results) {
|
|
|
345
333
|
}
|
|
346
334
|
|
|
347
335
|
for (const [pkg, preset] of Object.entries(packagePresets)) {
|
|
348
|
-
if (preset === null) continue;
|
|
349
336
|
const destPath = join(cwd, pkg, 'tsconfig.json');
|
|
350
337
|
const content = generateTsConfig(preset);
|
|
351
338
|
await writeIfConfirmed(destPath, content, `${pkg}/tsconfig.json`);
|
|
@@ -357,26 +344,22 @@ async function writeTsConfigs(packages, results) {
|
|
|
357
344
|
await writeIfConfirmed(rootDest, rootContent, 'tsconfig.json (root references)');
|
|
358
345
|
} else {
|
|
359
346
|
// Single package
|
|
360
|
-
const preset
|
|
361
|
-
if (preset === null) return;
|
|
347
|
+
const preset = await askPreset('this project');
|
|
362
348
|
const dest = join(cwd, 'tsconfig.json');
|
|
363
349
|
const content = generateTsConfig(preset);
|
|
364
350
|
await writeIfConfirmed(dest, content, 'tsconfig.json');
|
|
365
351
|
}
|
|
366
352
|
}
|
|
367
353
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
354
|
+
console.log();
|
|
355
|
+
console.log(dim(' ─────────────────────────────────────────'));
|
|
356
|
+
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')}`);
|
|
357
|
+
console.log();
|
|
372
358
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
}
|
|
359
|
+
if (results.failed.length === 0) {
|
|
360
|
+
console.log(' ' + bold('Done.') + ' Reload VS Code to activate Copilot instructions.');
|
|
361
|
+
console.log(dim(' Commit the generated files — they are workspace configuration.\n'));
|
|
362
|
+
} else {
|
|
363
|
+
console.log(' Some files could not be copied. Check the errors above.\n');
|
|
364
|
+
process.exit(1);
|
|
380
365
|
}
|
|
381
|
-
|
|
382
|
-
main();
|