@tsfpp/agents 1.3.4 → 1.4.0
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 +23 -0
- package/README.md +216 -31
- package/copilot/agents/tsfpp-annotate.agent.md +99 -119
- package/copilot/agents/tsfpp-audit.agent.md +182 -25
- package/copilot/agents/tsfpp-backfill-tests.agent.md +40 -1
- package/copilot/agents/tsfpp-guarded-coding.agent.md +22 -0
- package/copilot/agents/tsfpp-tdd.agent.md +59 -6
- package/copilot/prompts/trunk-changelog.prompt.md +160 -0
- package/copilot/skills/annotation-standard/SKILL.md +196 -0
- package/init.mjs +211 -197
- package/package.json +1 -1
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,68 @@ 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/
|
|
54
|
-
['copilot/prompts/tsfpp-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
['copilot/skills/
|
|
59
|
-
['copilot/skills/
|
|
60
|
-
['copilot/skills/
|
|
61
|
-
['copilot/skills/
|
|
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/trunk-changelog.prompt.md', '.github/prompts/trunk-changelog.prompt.md'],
|
|
59
|
+
['copilot/prompts/tsfpp-new-module.prompt.md', '.github/prompts/tsfpp-new-module.prompt.md'],
|
|
60
|
+
['copilot/prompts/tsfpp-boundary-review.prompt.md', '.github/prompts/tsfpp-boundary-review.prompt.md'],
|
|
61
|
+
|
|
62
|
+
// Skills
|
|
63
|
+
['copilot/skills/coding-standard/SKILL.md', '.github/skills/coding-standard/SKILL.md'],
|
|
64
|
+
['copilot/skills/prelude-api/SKILL.md', '.github/skills/prelude-api/SKILL.md'],
|
|
65
|
+
['copilot/skills/boundary-api/SKILL.md', '.github/skills/boundary-api/SKILL.md'],
|
|
66
|
+
['copilot/skills/react-coding-standard/SKILL.md', '.github/skills/react-coding-standard/SKILL.md'],
|
|
67
|
+
['copilot/skills/test-standard/SKILL.md', '.github/skills/test-standard/SKILL.md'],
|
|
68
|
+
['copilot/skills/annotation-standard/SKILL.md', '.github/skills/annotation-standard/SKILL.md'],
|
|
62
69
|
|
|
63
70
|
// Claude Code
|
|
64
|
-
['claude/CLAUDE.md',
|
|
71
|
+
['claude/CLAUDE.md', '.claude/CLAUDE.md'],
|
|
65
72
|
];
|
|
66
73
|
|
|
67
|
-
// ───
|
|
74
|
+
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
|
68
75
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
async function ensureDir(filePath) {
|
|
77
|
+
await mkdir(dirname(filePath), { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function ask(question) {
|
|
81
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
82
|
+
return new Promise((resolve) => {
|
|
83
|
+
rl.question(question, (answer) => {
|
|
84
|
+
rl.close();
|
|
85
|
+
resolve(answer.trim().toLowerCase());
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function confirm(question) {
|
|
91
|
+
return (await ask(question)) === 'y';
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Workspace detection ──────────────────────────────────────────────────────
|
|
74
95
|
|
|
75
96
|
async function detectWorkspacePackages() {
|
|
76
97
|
const wsFile = join(cwd, 'pnpm-workspace.yaml');
|
|
@@ -78,7 +99,7 @@ async function detectWorkspacePackages() {
|
|
|
78
99
|
|
|
79
100
|
const yaml = await readFile(wsFile, 'utf8');
|
|
80
101
|
const patterns = [...yaml.matchAll(/^\s*-\s*['"]?([^'"#\n]+?)['"]?\s*$/gm)]
|
|
81
|
-
.map(m => m[1].trim().replace(/\/\*\*?$/, ''));
|
|
102
|
+
.map(m => m[1].trim().replace(/\/\*\*?$/, ''));
|
|
82
103
|
|
|
83
104
|
const IGNORE = new Set(['dist', 'build', 'out', 'coverage', 'node_modules', '.git', '.turbo', 'tmp']);
|
|
84
105
|
|
|
@@ -96,8 +117,16 @@ async function detectWorkspacePackages() {
|
|
|
96
117
|
return packages.length > 0 ? packages : null;
|
|
97
118
|
}
|
|
98
119
|
|
|
120
|
+
// ─── ESLint config ────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
const ESLINT_PROFILES = {
|
|
123
|
+
base: { import: `import tsfpp from '@tsfpp/eslint-config'`, spread: 'tsfpp' },
|
|
124
|
+
react: { import: `import tsfppReact from '@tsfpp/eslint-config/react'`, spread: 'tsfppReact' },
|
|
125
|
+
api: { import: `import tsfppApi from '@tsfpp/eslint-config/api'`, spread: 'tsfppApi' },
|
|
126
|
+
};
|
|
127
|
+
|
|
99
128
|
async function askProfile(label) {
|
|
100
|
-
console.log(`\n
|
|
129
|
+
console.log(`\n ESLint profile for ${bold(label)}:`);
|
|
101
130
|
console.log(` ${dim('1')} base ${dim('— TypeScript / Node.js')}`);
|
|
102
131
|
console.log(` ${dim('2')} react ${dim('— React / TSX')}`);
|
|
103
132
|
console.log(` ${dim('3')} api ${dim('— HTTP API / Node.js servers')}`);
|
|
@@ -105,66 +134,48 @@ async function askProfile(label) {
|
|
|
105
134
|
return choice === '2' ? 'react' : choice === '3' ? 'api' : 'base';
|
|
106
135
|
}
|
|
107
136
|
|
|
108
|
-
function
|
|
109
|
-
// Collect which profiles are used
|
|
137
|
+
function generateMonorepoEslint(packageProfiles) {
|
|
110
138
|
const usedProfiles = [...new Set(Object.values(packageProfiles))];
|
|
139
|
+
const imports = usedProfiles.map(p => ESLINT_PROFILES[p].import).join('\n');
|
|
111
140
|
|
|
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
141
|
const basePackages = Object.entries(packageProfiles)
|
|
116
142
|
.filter(([, p]) => p === 'base')
|
|
117
143
|
.map(([pkg]) => `'${pkg}/src/**'`);
|
|
118
144
|
|
|
119
|
-
const
|
|
145
|
+
const scopedBlocks = ['react', 'api'].flatMap(profile => {
|
|
120
146
|
const pkgs = Object.entries(packageProfiles)
|
|
121
147
|
.filter(([, p]) => p === profile)
|
|
122
148
|
.map(([pkg]) => `'${pkg}/src/**'`);
|
|
123
149
|
if (pkgs.length === 0) return [];
|
|
124
|
-
const spread =
|
|
125
|
-
return [
|
|
126
|
-
` // ${profile}`,
|
|
127
|
-
` ...${spread}.map(c => ({ ...c, files: [${pkgs.join(', ')}] })),`,
|
|
128
|
-
];
|
|
150
|
+
const spread = ESLINT_PROFILES[profile].spread;
|
|
151
|
+
return [` // ${profile}`, ` ...${spread}.map(c => ({ ...c, files: [${pkgs.join(', ')}] })),`];
|
|
129
152
|
});
|
|
130
153
|
|
|
131
154
|
const baseSpread = basePackages.length > 0
|
|
132
155
|
? ` // base\n ...tsfpp.map(c => ({ ...c, files: [${basePackages.join(', ')}] })),`
|
|
133
156
|
: ` // base — applies to all files not matched by a scoped profile\n ...tsfpp,`;
|
|
134
157
|
|
|
135
|
-
return [
|
|
136
|
-
imports,
|
|
137
|
-
'',
|
|
138
|
-
'export default [',
|
|
139
|
-
baseSpread,
|
|
140
|
-
...scopedProfiles,
|
|
141
|
-
']',
|
|
142
|
-
'',
|
|
143
|
-
].join('\n');
|
|
158
|
+
return [imports, '', 'export default [', baseSpread, ...scopedBlocks, ']', ''].join('\n');
|
|
144
159
|
}
|
|
145
160
|
|
|
146
|
-
function
|
|
147
|
-
const { import: imp, spread } =
|
|
161
|
+
function generateSingleEslint(profile) {
|
|
162
|
+
const { import: imp, spread } = ESLINT_PROFILES[profile];
|
|
148
163
|
return `${imp}\nexport default [...${spread}]\n`;
|
|
149
164
|
}
|
|
150
165
|
|
|
151
|
-
async function writeEslintConfig() {
|
|
166
|
+
async function writeEslintConfig(results) {
|
|
152
167
|
const packages = await detectWorkspacePackages();
|
|
153
|
-
|
|
154
|
-
let content;
|
|
155
|
-
let description;
|
|
168
|
+
let content, description;
|
|
156
169
|
|
|
157
170
|
if (packages) {
|
|
158
171
|
console.log(`\n Monorepo detected — ${packages.length} package(s) found.\n`);
|
|
159
172
|
const packageProfiles = {};
|
|
160
|
-
for (const pkg of packages)
|
|
161
|
-
|
|
162
|
-
}
|
|
163
|
-
content = generateMonorepoConfig(packageProfiles);
|
|
173
|
+
for (const pkg of packages) packageProfiles[pkg] = await askProfile(pkg);
|
|
174
|
+
content = generateMonorepoEslint(packageProfiles);
|
|
164
175
|
description = 'monorepo';
|
|
165
176
|
} else {
|
|
166
177
|
const profile = await askProfile('this project');
|
|
167
|
-
content =
|
|
178
|
+
content = generateSingleEslint(profile);
|
|
168
179
|
description = `profile: ${profile}`;
|
|
169
180
|
}
|
|
170
181
|
|
|
@@ -178,175 +189,178 @@ async function writeEslintConfig() {
|
|
|
178
189
|
}
|
|
179
190
|
}
|
|
180
191
|
|
|
181
|
-
// ───
|
|
192
|
+
// ─── tsconfig generation ──────────────────────────────────────────────────────
|
|
182
193
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
194
|
+
const TSCONFIG_PRESETS = {
|
|
195
|
+
app: '@tsfpp/tsconfig/app',
|
|
196
|
+
lib: '@tsfpp/tsconfig/lib',
|
|
197
|
+
};
|
|
186
198
|
|
|
187
|
-
async function
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
199
|
+
async function askPreset(label) {
|
|
200
|
+
console.log(`\n tsconfig preset for ${bold(label)}:`);
|
|
201
|
+
console.log(` ${dim('1')} app ${dim('— application / tool (noEmit: true)')}`);
|
|
202
|
+
console.log(` ${dim('2')} lib ${dim('— publishable package (declaration, composite)')}`);
|
|
203
|
+
console.log(` ${dim('N')} skip`);
|
|
204
|
+
const choice = await ask(` ${dim('[1/2/N, default: 1]')} `);
|
|
205
|
+
if (choice === 'n') return null;
|
|
206
|
+
return choice === '2' ? 'lib' : 'app';
|
|
195
207
|
}
|
|
196
208
|
|
|
197
|
-
|
|
198
|
-
return (
|
|
209
|
+
function generateTsConfig(preset) {
|
|
210
|
+
return JSON.stringify(
|
|
211
|
+
{ extends: TSCONFIG_PRESETS[preset], compilerOptions: { rootDir: 'src' }, include: ['src'] },
|
|
212
|
+
null, 2
|
|
213
|
+
) + '\n';
|
|
199
214
|
}
|
|
200
215
|
|
|
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);
|
|
216
|
+
function generateRootTsConfig(packagePaths) {
|
|
217
|
+
return JSON.stringify(
|
|
218
|
+
{ files: [], references: packagePaths.map(p => ({ path: p })) },
|
|
219
|
+
null, 2
|
|
220
|
+
) + '\n';
|
|
221
|
+
}
|
|
214
222
|
|
|
223
|
+
async function writeIfConfirmed(destPath, content, label, results) {
|
|
215
224
|
if (existsSync(destPath)) {
|
|
216
225
|
const overwrite = await confirm(
|
|
217
|
-
` ${yellow('!')} ${
|
|
226
|
+
` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
|
|
218
227
|
);
|
|
219
228
|
if (!overwrite) {
|
|
220
|
-
results.skipped.push(
|
|
221
|
-
console.log(` ${dim('–')} ${dim(
|
|
222
|
-
|
|
229
|
+
results.skipped.push(label);
|
|
230
|
+
console.log(` ${dim('–')} ${dim(label)} ${dim('(skipped)')}`);
|
|
231
|
+
return;
|
|
223
232
|
}
|
|
224
233
|
}
|
|
225
|
-
|
|
226
234
|
try {
|
|
227
235
|
await ensureDir(destPath);
|
|
228
|
-
await
|
|
229
|
-
results.copied.push(
|
|
230
|
-
console.log(` ${green('✓')} ${
|
|
236
|
+
await writeFile(destPath, content, 'utf8');
|
|
237
|
+
results.copied.push(label);
|
|
238
|
+
console.log(` ${green('✓')} ${label}`);
|
|
231
239
|
} catch (err) {
|
|
232
|
-
results.failed.push(
|
|
233
|
-
console.log(` \x1b[31m✗\x1b[0m ${
|
|
240
|
+
results.failed.push(label);
|
|
241
|
+
console.log(` \x1b[31m✗\x1b[0m ${label} ${dim(`(${err.message})`)}`);
|
|
234
242
|
}
|
|
235
243
|
}
|
|
236
244
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
console.log();
|
|
245
|
+
async function writeTsConfigs(results) {
|
|
246
|
+
const packages = await detectWorkspacePackages();
|
|
240
247
|
|
|
241
|
-
|
|
248
|
+
if (packages) {
|
|
249
|
+
console.log(` Generating tsconfig.json per package.\n`);
|
|
250
|
+
const packagePresets = {};
|
|
251
|
+
for (const pkg of packages) packagePresets[pkg] = await askPreset(pkg);
|
|
242
252
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
);
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
253
|
+
for (const [pkg, preset] of Object.entries(packagePresets)) {
|
|
254
|
+
if (preset === null) {
|
|
255
|
+
results.skipped.push(`${pkg}/tsconfig.json`);
|
|
256
|
+
console.log(` ${dim('–')} ${dim(`${pkg}/tsconfig.json`)} ${dim('(skipped)')}`);
|
|
257
|
+
continue;
|
|
258
|
+
}
|
|
259
|
+
await writeIfConfirmed(join(cwd, pkg, 'tsconfig.json'), generateTsConfig(preset), `${pkg}/tsconfig.json`, results);
|
|
260
|
+
}
|
|
261
|
+
await writeIfConfirmed(join(cwd, 'tsconfig.json'), generateRootTsConfig(packages), 'tsconfig.json (root references)', results);
|
|
250
262
|
} else {
|
|
251
|
-
await
|
|
263
|
+
const preset = await askPreset('this project');
|
|
264
|
+
if (preset === null) {
|
|
265
|
+
results.skipped.push('tsconfig.json');
|
|
266
|
+
console.log(` ${dim('–')} ${dim('tsconfig.json')} ${dim('(skipped)')}`);
|
|
267
|
+
} else {
|
|
268
|
+
await writeIfConfirmed(join(cwd, 'tsconfig.json'), generateTsConfig(preset), 'tsconfig.json', results);
|
|
269
|
+
}
|
|
252
270
|
}
|
|
253
|
-
} else {
|
|
254
|
-
await writeEslintConfig();
|
|
255
271
|
}
|
|
256
272
|
|
|
257
|
-
//
|
|
273
|
+
// ─── Main ─────────────────────────────────────────────────────────────────────
|
|
258
274
|
|
|
259
|
-
|
|
275
|
+
async function main() {
|
|
276
|
+
console.log();
|
|
277
|
+
console.log(bold(' @tsfpp/agents — init') + (YES ? dim(' (--yes)') : ''));
|
|
278
|
+
if (YES) {
|
|
279
|
+
console.log(dim(' Copying package-managed files. eslint.config.js and tsconfig.json are not touched.\n'));
|
|
280
|
+
} else {
|
|
281
|
+
console.log(dim(' Sets up Copilot agents, instructions, prompts, skills, and ESLint config.\n'));
|
|
282
|
+
}
|
|
260
283
|
|
|
261
|
-
|
|
284
|
+
const results = { copied: [], skipped: [], failed: [] };
|
|
262
285
|
|
|
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
|
-
};
|
|
286
|
+
// ── Copy package-managed files ─────────────────────────────────────────────
|
|
268
287
|
|
|
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
|
-
}
|
|
288
|
+
for (const [src, dest] of FILES) {
|
|
289
|
+
const srcPath = join(__dirname, src);
|
|
290
|
+
const destPath = join(cwd, dest);
|
|
276
291
|
|
|
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
|
-
}
|
|
292
|
+
if (!existsSync(srcPath)) {
|
|
293
|
+
results.skipped.push(dest);
|
|
294
|
+
console.log(` ${dim('–')} ${dim(dest)} ${dim('(source not found — skipped)')}`);
|
|
295
|
+
continue;
|
|
296
|
+
}
|
|
291
297
|
|
|
292
|
-
|
|
293
|
-
if (existsSync(destPath)) {
|
|
298
|
+
if (existsSync(destPath) && !YES) {
|
|
294
299
|
const overwrite = await confirm(
|
|
295
|
-
` ${yellow('!')} ${
|
|
300
|
+
` ${yellow('!')} ${dest} already exists. Overwrite? ${dim('[y/N]')} `
|
|
296
301
|
);
|
|
297
302
|
if (!overwrite) {
|
|
298
|
-
results.skipped.push(
|
|
299
|
-
console.log(` ${dim('–')} ${dim(
|
|
300
|
-
|
|
303
|
+
results.skipped.push(dest);
|
|
304
|
+
console.log(` ${dim('–')} ${dim(dest)} ${dim('(skipped)')}`);
|
|
305
|
+
continue;
|
|
301
306
|
}
|
|
302
307
|
}
|
|
308
|
+
|
|
303
309
|
try {
|
|
304
310
|
await ensureDir(destPath);
|
|
305
|
-
await
|
|
306
|
-
results.copied.push(
|
|
307
|
-
console.log(` ${green('✓')} ${
|
|
311
|
+
await copyFile(srcPath, destPath);
|
|
312
|
+
results.copied.push(dest);
|
|
313
|
+
console.log(` ${green('✓')} ${dest}`);
|
|
308
314
|
} catch (err) {
|
|
309
|
-
results.failed.push(
|
|
310
|
-
console.log(` \x1b[31m✗\x1b[0m ${
|
|
315
|
+
results.failed.push(dest);
|
|
316
|
+
console.log(` \x1b[31m✗\x1b[0m ${dest} ${dim(`(${err.message})`)}`);
|
|
311
317
|
}
|
|
312
318
|
}
|
|
313
319
|
|
|
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
|
-
}
|
|
320
|
+
// ── ESLint (interactive only) ──────────────────────────────────────────────
|
|
321
321
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
322
|
+
if (!YES) {
|
|
323
|
+
console.log();
|
|
324
|
+
const eslintDest = join(cwd, 'eslint.config.js');
|
|
325
|
+
if (existsSync(eslintDest)) {
|
|
326
|
+
const overwrite = await confirm(
|
|
327
|
+
` ${yellow('!')} eslint.config.js already exists. Overwrite? ${dim('[y/N]')} `
|
|
328
|
+
);
|
|
329
|
+
if (!overwrite) {
|
|
330
|
+
results.skipped.push('eslint.config.js');
|
|
331
|
+
console.log(` ${dim('–')} ${dim('eslint.config.js')} ${dim('(skipped)')}`);
|
|
332
|
+
} else {
|
|
333
|
+
await writeEslintConfig(results);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
await writeEslintConfig(results);
|
|
326
337
|
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// ── tsconfig (interactive only) ────────────────────────────────────────────
|
|
327
341
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
342
|
+
if (!YES) {
|
|
343
|
+
console.log();
|
|
344
|
+
await writeTsConfigs(results);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// ── Summary ────────────────────────────────────────────────────────────────
|
|
348
|
+
|
|
349
|
+
console.log();
|
|
350
|
+
console.log(dim(' ─────────────────────────────────────────'));
|
|
351
|
+
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')}`);
|
|
352
|
+
console.log();
|
|
353
|
+
|
|
354
|
+
if (results.failed.length === 0) {
|
|
355
|
+
console.log(' ' + bold('Done.') + ' Reload VS Code to activate Copilot instructions.');
|
|
356
|
+
console.log(dim(' Commit the generated files — they are workspace configuration.\n'));
|
|
332
357
|
} else {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
const dest = join(cwd, 'tsconfig.json');
|
|
336
|
-
const content = generateTsConfig(preset);
|
|
337
|
-
await writeIfConfirmed(dest, content, 'tsconfig.json');
|
|
358
|
+
console.log(' Some files could not be copied. Check the errors above.\n');
|
|
359
|
+
process.exit(1);
|
|
338
360
|
}
|
|
339
361
|
}
|
|
340
362
|
|
|
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');
|
|
363
|
+
main().catch(err => {
|
|
364
|
+
console.error(err);
|
|
351
365
|
process.exit(1);
|
|
352
|
-
}
|
|
366
|
+
});
|