@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/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,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
- // 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/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', '.claude/CLAUDE.md'],
71
+ ['claude/CLAUDE.md', '.claude/CLAUDE.md'],
65
72
  ];
66
73
 
67
- // ─── ESLint config generation ─────────────────────────────────────────────────
74
+ // ─── Helpers ──────────────────────────────────────────────────────────────────
68
75
 
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
- };
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(/\/\*\*?$/, '')); // strip trailing /* or /**
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 Profile for ${bold(label)}:`);
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 generateMonorepoConfig(packageProfiles) {
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 scopedProfiles = ['react', 'api'].flatMap(profile => {
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 = PROFILES[profile].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 generateSingleConfig(profile) {
147
- const { import: imp, spread } = PROFILES[profile];
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
- packageProfiles[pkg] = await askProfile(pkg);
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 = generateSingleConfig(profile);
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
- // ─── Helpers ─────────────────────────────────────────────────────────────────
192
+ // ─── tsconfig generation ──────────────────────────────────────────────────────
182
193
 
183
- async function ensureDir(filePath) {
184
- await mkdir(dirname(filePath), { recursive: true });
185
- }
194
+ const TSCONFIG_PRESETS = {
195
+ app: '@tsfpp/tsconfig/app',
196
+ lib: '@tsfpp/tsconfig/lib',
197
+ };
186
198
 
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
- });
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
- async function confirm(question) {
198
- return (await ask(question)) === 'y';
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
- // ─── 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);
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('!')} ${dest} already exists. Overwrite? ${dim('[y/N]')} `
226
+ ` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
218
227
  );
219
228
  if (!overwrite) {
220
- results.skipped.push(dest);
221
- console.log(` ${dim('–')} ${dim(dest)} ${dim('(skipped)')}`);
222
- continue;
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 copyFile(srcPath, destPath);
229
- results.copied.push(dest);
230
- console.log(` ${green('✓')} ${dest}`);
236
+ await writeFile(destPath, content, 'utf8');
237
+ results.copied.push(label);
238
+ console.log(` ${green('✓')} ${label}`);
231
239
  } catch (err) {
232
- results.failed.push(dest);
233
- console.log(` \x1b[31m✗\x1b[0m ${dest} ${dim(`(${err.message})`)}`);
240
+ results.failed.push(label);
241
+ console.log(` \x1b[31m✗\x1b[0m ${label} ${dim(`(${err.message})`)}`);
234
242
  }
235
243
  }
236
244
 
237
- // ── Generate eslint.config.js ─────────────────────────────────────────────────
238
-
239
- console.log();
245
+ async function writeTsConfigs(results) {
246
+ const packages = await detectWorkspacePackages();
240
247
 
241
- const eslintDest = join(cwd, 'eslint.config.js');
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
- 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)')}`);
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 writeEslintConfig();
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
- // ── Generate tsconfig.json ────────────────────────────────────────────────────
273
+ // ─── Main ─────────────────────────────────────────────────────────────────────
258
274
 
259
- console.log();
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
- await writeTsConfigs(await detectWorkspacePackages());
284
+ const results = { copied: [], skipped: [], failed: [] };
262
285
 
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
- };
286
+ // ── Copy package-managed files ─────────────────────────────────────────────
268
287
 
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
- }
288
+ for (const [src, dest] of FILES) {
289
+ const srcPath = join(__dirname, src);
290
+ const destPath = join(cwd, dest);
276
291
 
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
- }
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
- async function writeIfConfirmed(destPath, content, label) {
293
- if (existsSync(destPath)) {
298
+ if (existsSync(destPath) && !YES) {
294
299
  const overwrite = await confirm(
295
- ` ${yellow('!')} ${label} already exists. Overwrite? ${dim('[y/N]')} `
300
+ ` ${yellow('!')} ${dest} already exists. Overwrite? ${dim('[y/N]')} `
296
301
  );
297
302
  if (!overwrite) {
298
- results.skipped.push(label);
299
- console.log(` ${dim('–')} ${dim(label)} ${dim('(skipped)')}`);
300
- return;
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 writeFile(destPath, content, 'utf8');
306
- results.copied.push(label);
307
- console.log(` ${green('✓')} ${label}`);
311
+ await copyFile(srcPath, destPath);
312
+ results.copied.push(dest);
313
+ console.log(` ${green('✓')} ${dest}`);
308
314
  } catch (err) {
309
- results.failed.push(label);
310
- console.log(` \x1b[31m✗\x1b[0m ${label} ${dim(`(${err.message})`)}`);
315
+ results.failed.push(dest);
316
+ console.log(` \x1b[31m✗\x1b[0m ${dest} ${dim(`(${err.message})`)}`);
311
317
  }
312
318
  }
313
319
 
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
- }
320
+ // ── ESLint (interactive only) ──────────────────────────────────────────────
321
321
 
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`);
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
- // Root references tsconfig
329
- const rootDest = join(cwd, 'tsconfig.json');
330
- const rootContent = generateRootTsConfig(packages);
331
- await writeIfConfirmed(rootDest, rootContent, 'tsconfig.json (root references)');
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
- // 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');
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
- 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');
363
+ main().catch(err => {
364
+ console.error(err);
351
365
  process.exit(1);
352
- }
366
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsfpp/agents",
3
- "version": "1.3.4",
3
+ "version": "1.4.0",
4
4
  "description": "Workspace AI tooling for TSF++ projects: scoped instructions, coding agents, and reusable prompts",
5
5
  "keywords": [
6
6
  "tsfpp",