ts-procedures 10.2.0 → 10.2.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.
Files changed (24) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +9 -6
  3. package/agent_config/bin/postinstall.mjs +20 -89
  4. package/agent_config/bin/setup.mjs +51 -264
  5. package/agent_config/lib/install-skills.mjs +108 -0
  6. package/docs/ai-agent-setup.md +19 -27
  7. package/docs/client-error-handling.md +1 -1
  8. package/docs/http-integrations.md +1 -1
  9. package/package.json +1 -1
  10. package/agent_config/claude-code/.claude-plugin/plugin.json +0 -5
  11. package/agent_config/claude-code/agents/ts-procedures-architect.md +0 -138
  12. package/agent_config/copilot/copilot-instructions.md +0 -521
  13. package/agent_config/cursor/cursorrules +0 -521
  14. package/agent_config/lib/install-claude.mjs +0 -57
  15. /package/agent_config/{claude-code/skills → skills}/ts-procedures/SKILL.md +0 -0
  16. /package/agent_config/{claude-code/skills → skills}/ts-procedures/anti-patterns.md +0 -0
  17. /package/agent_config/{claude-code/skills → skills}/ts-procedures/api-reference.md +0 -0
  18. /package/agent_config/{claude-code/skills → skills}/ts-procedures/checklist.md +0 -0
  19. /package/agent_config/{claude-code/skills → skills}/ts-procedures/patterns.md +0 -0
  20. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/astro-catchall.md +0 -0
  21. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/client.md +0 -0
  22. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/hono.md +0 -0
  23. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/procedure.md +0 -0
  24. /package/agent_config/{claude-code/skills → skills}/ts-procedures/templates/stream-procedure.md +0 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,18 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
 
7
7
  ## Unreleased
8
8
 
9
+ ## 10.2.1 — 2026-06-26
10
+
11
+ ### Changed
12
+
13
+ - **AI agent config is now a single spec-compliant [Agent Skill](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills).** Replaced the per-tool config directories (`claude-code/`, `cursor/`, `copilot/`) with one source of truth at `agent_config/skills/ts-procedures/`. `npx ts-procedures-setup` now installs that one skill into the standard agent roots — `.agents/skills/ts-procedures/` (read by Codex, Cursor, GitHub Copilot, Gemini CLI, and 30+ tools) and `.claude/skills/ts-procedures/` (Claude Code native) — eliminating the hand-synced duplicate `.cursorrules` / `.github/copilot-instructions.md` files and their marker-merge machinery.
14
+ - `ts-procedures-setup` CLI simplified to `--dry-run` / `--check` / `--help` (per-tool `targets` and `--force` removed; the install is a force-overwrite copy). The postinstall auto-update remains opt-in (only refreshes once the skill is already installed).
15
+
16
+ ### Removed
17
+
18
+ - The Claude-only `ts-procedures-architect` agent and the Claude Code plugin manifest (`.claude-plugin/plugin.json`). The unified `ts-procedures` skill covers reference, scaffold, and review modes.
19
+ - Downstream cleanup note: a previously installed `.claude/agents/ts-procedures-architect.md` is now orphaned and can be deleted manually.
20
+
9
21
  ## 10.2.0 — 2026-06-25
10
22
 
11
23
  ### Added
package/README.md CHANGED
@@ -191,17 +191,20 @@ implementation.
191
191
 
192
192
  ## AI assistant setup
193
193
 
194
- The package ships rules and skills for Claude Code, Cursor, and GitHub
195
- Copilot so your AI tooling knows the framework's patterns and footguns:
194
+ The package ships a single spec-compliant [Agent Skill](https://www.anthropic.com/engineering/equipping-agents-for-the-real-world-with-agent-skills)
195
+ so your AI tooling knows the framework's patterns and footguns. One skill is
196
+ installed into the standard agent directories (`.agents/skills/` — read by
197
+ Codex, Cursor, Copilot, Gemini CLI and 30+ others — plus `.claude/skills/` for
198
+ Claude Code):
196
199
 
197
200
  ```bash
198
- npx ts-procedures-setup # all targets
199
- npx ts-procedures-setup claude # Claude Code only
201
+ npx ts-procedures-setup # install the skill
200
202
  npx ts-procedures-setup --dry-run # preview
203
+ npx ts-procedures-setup --check # CI gate: fail if outdated
201
204
  ```
202
205
 
203
- Installed files auto-update on subsequent `npm install` via the package's
204
- postinstall hook.
206
+ The installed skill auto-updates on subsequent `npm install` via the package's
207
+ postinstall hook. See [docs/ai-agent-setup.md](docs/ai-agent-setup.md).
205
208
 
206
209
  ## Migrating from v8
207
210
 
@@ -1,105 +1,36 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, writeFileSync, existsSync } from 'node:fs';
4
- import { resolve, join, dirname } from 'node:path';
3
+ import { resolve, dirname } from 'node:path';
5
4
  import { fileURLToPath } from 'node:url';
5
+ import { installSkills, isInstalled } from '../lib/install-skills.mjs';
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = dirname(__filename);
9
- const AGENT_CONFIG_DIR = resolve(__dirname, '..');
10
9
 
11
- const MARKER_BEGIN = '<!-- BEGIN ts-procedures -->';
12
- const MARKER_END = '<!-- END ts-procedures -->';
10
+ // ─── Per-project config (keep in sync with bin/setup.mjs) ────────────────────
11
+ const CONFIG = {
12
+ packageName: 'ts-procedures',
13
+ sourceDir: resolve(__dirname, '..', 'skills'),
14
+ skills: ['ts-procedures'],
15
+ targetRoots: ['.agents/skills', '.claude/skills'],
16
+ };
13
17
 
14
- // INIT_CWD is set by npm/yarn/pnpm during lifecycle scripts — points to the project root
18
+ // INIT_CWD is set by npm/yarn/pnpm during lifecycle scripts — the project root.
15
19
  const projectRoot = process.env.INIT_CWD;
20
+ if (!projectRoot) process.exit(0);
16
21
 
17
- if (!projectRoot) {
18
- process.exit(0);
19
- }
20
-
21
- // Detect self-install (developing ts-procedures itself) — skip
22
- const ownPackageRoot = resolve(__dirname, '../..');
23
- if (resolve(projectRoot) === ownPackageRoot) {
24
- process.exit(0);
25
- }
26
-
27
- // ─── Helpers ──────────────────────────────────────────────
28
-
29
- function readSourceFile(relativePath) {
30
- return readFileSync(join(AGENT_CONFIG_DIR, relativePath), 'utf-8');
31
- }
32
-
33
- function wrapWithMarkers(content) {
34
- return `${MARKER_BEGIN}\n${content.trim()}\n${MARKER_END}`;
35
- }
36
-
37
- function replaceMarkerSection(existingContent, newContent) {
38
- const beginIdx = existingContent.indexOf(MARKER_BEGIN);
39
- const endIdx = existingContent.indexOf(MARKER_END);
40
-
41
- if (beginIdx !== -1 && endIdx !== -1) {
42
- const before = existingContent.slice(0, beginIdx);
43
- const after = existingContent.slice(endIdx + MARKER_END.length);
44
- return before + wrapWithMarkers(newContent) + after;
45
- }
46
-
47
- return null; // No existing markers — don't modify
48
- }
22
+ // Skip when developing this package itself.
23
+ if (resolve(projectRoot) === resolve(__dirname, '..', '..')) process.exit(0);
49
24
 
50
- function updateMarkedFile(targetPath, sourceRelativePath) {
51
- if (!existsSync(targetPath)) return false;
25
+ const cfg = { ...CONFIG, projectRoot };
52
26
 
53
- const existing = readFileSync(targetPath, 'utf-8');
54
- if (!existing.includes(MARKER_BEGIN)) return false;
55
-
56
- const sourceContent = readSourceFile(sourceRelativePath);
57
- const updated = replaceMarkerSection(existing, sourceContent);
58
- if (updated) {
59
- writeFileSync(targetPath, updated, 'utf-8');
60
- return true;
61
- }
62
- return false;
63
- }
64
-
65
- // ─── Auto-update ──────────────────────────────────────────
66
-
67
- const claudeSkillDir = join(projectRoot, '.claude', 'skills', 'ts-procedures');
68
- const cursorFile = join(projectRoot, '.cursorrules');
69
- const copilotFile = join(projectRoot, '.github', 'copilot-instructions.md');
70
-
71
- // Check if any target was previously set up
72
- const hasAnySetup = existsSync(claudeSkillDir) || existsSync(cursorFile) || existsSync(copilotFile);
73
-
74
- if (hasAnySetup) {
27
+ // Opt-in only: refresh skills the user has already installed; never create on
28
+ // first install (rely on README / `npx <pkg>-setup`).
29
+ if (isInstalled(cfg)) {
75
30
  try {
76
- const updated = [];
77
-
78
- // Auto-update Claude Code files
79
- if (existsSync(claudeSkillDir)) {
80
- const { installClaude } = await import('../lib/install-claude.mjs');
81
- installClaude(projectRoot);
82
- updated.push('Claude Code');
83
- }
84
-
85
- // Auto-update Cursor rules (only if markers present)
86
- if (updateMarkedFile(cursorFile, 'cursor/cursorrules')) {
87
- updated.push('Cursor');
88
- }
89
-
90
- // Auto-update Copilot instructions (only if markers present)
91
- if (updateMarkedFile(copilotFile, 'copilot/copilot-instructions.md')) {
92
- updated.push('Copilot');
93
- }
94
-
95
- if (updated.length > 0) {
96
- console.log('');
97
- console.log(` ts-procedures — AI agent rules updated (${updated.join(', ')})`);
98
- console.log('');
99
- }
31
+ installSkills(cfg);
32
+ console.log(`\n ${CONFIG.packageName} — Agent Skill updated\n`);
100
33
  } catch {
101
- // Never break npm install
34
+ // Never break npm install.
102
35
  }
103
36
  }
104
-
105
- // No invitation message if not opted in — rely on README / npx ts-procedures-setup --help
@@ -1,297 +1,84 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { readFileSync, writeFileSync, mkdirSync, existsSync, readdirSync, statSync } from 'node:fs';
4
- import { join, dirname, resolve } from 'node:path';
3
+ import { existsSync } from 'node:fs';
4
+ import { dirname, resolve } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
- import { createInterface } from 'node:readline';
7
- import { installClaude } from '../lib/install-claude.mjs';
6
+ import { installSkills, planSkills, outdatedSkillFiles } from '../lib/install-skills.mjs';
8
7
 
9
8
  const __filename = fileURLToPath(import.meta.url);
10
9
  const __dirname = dirname(__filename);
11
- const AGENT_CONFIG_DIR = resolve(__dirname, '..');
12
- const PROJECT_DIR = process.cwd();
13
-
14
- const MARKER_BEGIN = '<!-- BEGIN ts-procedures -->';
15
- const MARKER_END = '<!-- END ts-procedures -->';
16
10
 
17
- const VALID_TARGETS = ['claude', 'cursor', 'copilot', 'all'];
11
+ // ─── Per-project config (the only thing a consuming library customizes) ──────
12
+ const CONFIG = {
13
+ packageName: 'ts-procedures',
14
+ sourceDir: resolve(__dirname, '..', 'skills'),
15
+ skills: ['ts-procedures'],
16
+ // `.agents/skills` is read by 30+ agent tools (Codex, Cursor, Copilot,
17
+ // Gemini CLI, …); `.claude/skills` is Claude Code's native project location.
18
+ // One source, fanned out to both.
19
+ targetRoots: ['.agents/skills', '.claude/skills'],
20
+ };
18
21
 
19
- // ─── Helpers ──────────────────────────────────────────────
22
+ const PROJECT_DIR = process.cwd();
20
23
 
21
24
  function printUsage() {
22
25
  console.log(`
23
- Usage: npx ts-procedures-setup [targets...] [options]
26
+ Usage: npx ${CONFIG.packageName}-setup [options]
24
27
 
25
- Targets:
26
- claude Install Claude Code skills and architect agent into .claude/
27
- cursor Copy ts-procedures rules to .cursorrules
28
- copilot Copy ts-procedures instructions to .github/copilot-instructions.md
29
- all Set up all targets (default)
28
+ Installs the ${CONFIG.packageName} Agent Skill into the standard agent
29
+ directories so any compatible AI coding tool picks it up automatically:
30
+
31
+ ${CONFIG.targetRoots.map((r) => `${r}/${CONFIG.skills.join(', ')}`).join('\n ')}
30
32
 
31
33
  Options:
32
34
  --help Show this help message
33
- --force Overwrite existing files without prompting
34
35
  --dry-run Show what would be created/updated without writing files
35
- --check Exit with code 1 if any files are outdated (useful in CI)
36
+ --check Exit with code 1 if any installed files are outdated (CI gate)
36
37
 
37
- Examples:
38
- npx ts-procedures-setup # Set up all targets
39
- npx ts-procedures-setup cursor # Set up Cursor only
40
- npx ts-procedures-setup cursor copilot # Set up Cursor and Copilot
41
- npx ts-procedures-setup --force # Overwrite without prompting
42
- npx ts-procedures-setup --dry-run # Preview changes
43
- npx ts-procedures-setup --check # Verify files are up to date
38
+ The files auto-update on every \`npm install\` once installed.
44
39
  `);
45
40
  }
46
41
 
47
- function readSourceFile(relativePath) {
48
- const fullPath = join(AGENT_CONFIG_DIR, relativePath);
49
- return readFileSync(fullPath, 'utf-8');
50
- }
51
-
52
- function wrapWithMarkers(content) {
53
- return `${MARKER_BEGIN}\n${content.trim()}\n${MARKER_END}`;
54
- }
55
-
56
- function replaceMarkerSection(existingContent, newContent) {
57
- const beginIdx = existingContent.indexOf(MARKER_BEGIN);
58
- const endIdx = existingContent.indexOf(MARKER_END);
59
-
60
- if (beginIdx !== -1 && endIdx !== -1) {
61
- const before = existingContent.slice(0, beginIdx);
62
- const after = existingContent.slice(endIdx + MARKER_END.length);
63
- return before + wrapWithMarkers(newContent) + after;
64
- }
65
-
66
- const separator = existingContent.trim() ? '\n\n' : '';
67
- return existingContent.trim() + separator + wrapWithMarkers(newContent) + '\n';
68
- }
69
-
70
- async function confirm(message) {
71
- const rl = createInterface({ input: process.stdin, output: process.stdout });
72
- return new Promise((resolve) => {
73
- rl.question(`${message} (y/N) `, (answer) => {
74
- rl.close();
75
- resolve(answer.toLowerCase() === 'y');
76
- });
77
- });
78
- }
79
-
80
- // ─── Check Mode ───────────────────────────────────────────
81
-
82
- function isFileOutdated(targetPath, expectedContent) {
83
- if (!existsSync(targetPath)) return true;
84
- const current = readFileSync(targetPath, 'utf-8');
85
- return current !== expectedContent;
86
- }
87
-
88
- // ─── Target Handlers ──────────────────────────────────────
89
-
90
- function setupClaude({ dryRun, check } = {}) {
91
- if (dryRun) {
92
- // Mirror install-claude.mjs: copy every file under each source skill dir, plus the agent.
93
- const claudeFiles = [];
94
- const skillsSrc = join(AGENT_CONFIG_DIR, 'claude-code', 'skills');
95
- for (const skill of ['ts-procedures']) {
96
- const walk = (dir, prefix) => {
97
- for (const entry of readdirSync(dir)) {
98
- const full = join(dir, entry);
99
- if (statSync(full).isDirectory()) {
100
- walk(full, `${prefix}/${entry}`);
101
- } else {
102
- claudeFiles.push(`${prefix}/${entry}`);
103
- }
104
- }
105
- };
106
- walk(join(skillsSrc, skill), `.claude/skills/${skill}`);
107
- }
108
- claudeFiles.push('.claude/agents/ts-procedures-architect.md');
109
-
110
- console.log('\n Claude Code (dry run)');
111
- console.log(' ─────────────────────');
112
- for (const f of claudeFiles) {
113
- const status = existsSync(join(PROJECT_DIR, f)) ? 'update' : 'create';
114
- console.log(` [${status}] ${f}`);
115
- }
116
- console.log('');
117
- return false;
118
- }
119
-
120
- const { files } = installClaude(PROJECT_DIR);
121
-
122
- if (check) return false; // installClaude always writes — check mode handled at main level
123
-
124
- console.log('\n Claude Code');
125
- console.log(' ───────────');
126
- console.log(' Installed (updates automatically on npm install/update):\n');
127
- for (const f of files) {
128
- console.log(` ${f}`);
129
- }
130
- console.log('');
131
- console.log(' Skills: ts-procedures (use `/ts-procedures review <path>` and `/ts-procedures scaffold <type> <Name>`; Kotlin/Swift codegen reference included)');
132
- console.log(' Agent: ts-procedures-architect (architecture planning)\n');
133
- return false;
134
- }
135
-
136
- async function setupCursor(force, { dryRun, check } = {}) {
137
- const targetPath = join(PROJECT_DIR, '.cursorrules');
138
- const sourceContent = readSourceFile('cursor/cursorrules');
139
-
140
- if (dryRun || check) {
141
- const exists = existsSync(targetPath);
142
- const hasMarkers = exists && readFileSync(targetPath, 'utf-8').includes(MARKER_BEGIN);
143
- const label = dryRun ? 'dry run' : 'check';
144
- const status = !exists ? 'create' : hasMarkers ? 'update' : 'append';
145
- console.log(`\n Cursor (${label})`);
146
- console.log(` ──────`);
147
- console.log(` [${status}] .cursorrules\n`);
148
- return check && (status === 'create' || status === 'update');
149
- }
150
-
151
- console.log('\n Cursor');
152
- console.log(' ──────');
153
-
154
- if (existsSync(targetPath)) {
155
- const existing = readFileSync(targetPath, 'utf-8');
156
-
157
- if (existing.includes(MARKER_BEGIN)) {
158
- const updated = replaceMarkerSection(existing, sourceContent);
159
- writeFileSync(targetPath, updated, 'utf-8');
160
- console.log(' Updated ts-procedures section in .cursorrules\n');
161
- return false;
162
- }
163
-
164
- if (!force) {
165
- const ok = await confirm(' .cursorrules already exists. Append ts-procedures rules?');
166
- if (!ok) {
167
- console.log(' Skipped.\n');
168
- return false;
169
- }
170
- }
171
-
172
- const updated = replaceMarkerSection(existing, sourceContent);
173
- writeFileSync(targetPath, updated, 'utf-8');
174
- console.log(' Appended ts-procedures rules to .cursorrules\n');
175
- } else {
176
- writeFileSync(targetPath, wrapWithMarkers(sourceContent) + '\n', 'utf-8');
177
- console.log(' Created .cursorrules\n');
178
- }
179
- return false;
180
- }
181
-
182
- async function setupCopilot(force, { dryRun, check } = {}) {
183
- const githubDir = join(PROJECT_DIR, '.github');
184
- const targetPath = join(githubDir, 'copilot-instructions.md');
185
- const sourceContent = readSourceFile('copilot/copilot-instructions.md');
186
-
187
- if (dryRun || check) {
188
- const exists = existsSync(targetPath);
189
- const hasMarkers = exists && readFileSync(targetPath, 'utf-8').includes(MARKER_BEGIN);
190
- const label = dryRun ? 'dry run' : 'check';
191
- const status = !exists ? 'create' : hasMarkers ? 'update' : 'append';
192
- console.log(`\n GitHub Copilot (${label})`);
193
- console.log(` ──────────────`);
194
- console.log(` [${status}] .github/copilot-instructions.md\n`);
195
- return check && (status === 'create' || status === 'update');
196
- }
197
-
198
- console.log('\n GitHub Copilot');
199
- console.log(' ──────────────');
200
-
201
- if (!existsSync(githubDir)) {
202
- mkdirSync(githubDir, { recursive: true });
203
- }
204
-
205
- if (existsSync(targetPath)) {
206
- const existing = readFileSync(targetPath, 'utf-8');
207
-
208
- if (existing.includes(MARKER_BEGIN)) {
209
- const updated = replaceMarkerSection(existing, sourceContent);
210
- writeFileSync(targetPath, updated, 'utf-8');
211
- console.log(' Updated ts-procedures section in .github/copilot-instructions.md\n');
212
- return false;
213
- }
214
-
215
- if (!force) {
216
- const ok = await confirm(' .github/copilot-instructions.md already exists. Append ts-procedures instructions?');
217
- if (!ok) {
218
- console.log(' Skipped.\n');
219
- return false;
220
- }
221
- }
222
-
223
- const updated = replaceMarkerSection(existing, sourceContent);
224
- writeFileSync(targetPath, updated, 'utf-8');
225
- console.log(' Appended ts-procedures instructions to .github/copilot-instructions.md\n');
226
- } else {
227
- writeFileSync(targetPath, wrapWithMarkers(sourceContent) + '\n', 'utf-8');
228
- console.log(' Created .github/copilot-instructions.md\n');
229
- }
230
- return false;
231
- }
232
-
233
- // ─── Main ─────────────────────────────────────────────────
234
-
235
- async function main() {
42
+ function main() {
236
43
  const args = process.argv.slice(2);
237
- const force = args.includes('--force');
238
- const dryRun = args.includes('--dry-run');
239
- const check = args.includes('--check');
240
- const help = args.includes('--help') || args.includes('-h');
241
- const targets = args.filter(a => !a.startsWith('--') && !a.startsWith('-'));
242
-
243
- if (help) {
44
+ if (args.includes('--help') || args.includes('-h')) {
244
45
  printUsage();
245
46
  process.exit(0);
246
47
  }
247
-
248
- for (const t of targets) {
249
- if (!VALID_TARGETS.includes(t)) {
250
- console.error(`Unknown target: "${t}". Valid targets: ${VALID_TARGETS.join(', ')}`);
251
- process.exit(1);
252
- }
253
- }
254
-
255
- const selectedTargets = targets.length === 0 || targets.includes('all')
256
- ? ['claude', 'cursor', 'copilot']
257
- : targets;
48
+ const dryRun = args.includes('--dry-run');
49
+ const check = args.includes('--check');
50
+ const cfg = { ...CONFIG, projectRoot: PROJECT_DIR };
258
51
 
259
52
  const mode = dryRun ? ' (dry run)' : check ? ' (check)' : '';
260
- console.log(`\nts-procedures AI Agent Setup${mode}`);
261
- console.log('════════════════════════════');
262
-
263
- const opts = { dryRun, check };
264
- let outdated = false;
265
-
266
- if (selectedTargets.includes('claude')) {
267
- outdated = setupClaude(opts) || outdated;
268
- }
269
-
270
- if (selectedTargets.includes('cursor')) {
271
- outdated = (await setupCursor(force, opts)) || outdated;
272
- }
273
-
274
- if (selectedTargets.includes('copilot')) {
275
- outdated = (await setupCopilot(force, opts)) || outdated;
276
- }
53
+ console.log(`\n${CONFIG.packageName} Agent Skill setup${mode}`);
54
+ console.log(''.repeat(40));
277
55
 
278
56
  if (check) {
279
- if (outdated) {
280
- console.log(' Files are outdated. Run: npx ts-procedures-setup\n');
57
+ const outdated = outdatedSkillFiles(cfg);
58
+ if (outdated.length) {
59
+ console.log('\n Outdated or missing:');
60
+ for (const f of outdated) console.log(` ${f}`);
61
+ console.log(`\n Run: npx ${CONFIG.packageName}-setup\n`);
281
62
  process.exit(1);
282
- } else {
283
- console.log(' All files are up to date.\n');
284
63
  }
285
- } else if (dryRun) {
286
- console.log(' No files were modified (dry run).\n');
287
- } else {
288
- console.log(' Tip: Auto-generated .claude/ files can be added to .gitignore');
289
- console.log(' (they are regenerated on npm install).\n');
290
- console.log('Done!\n');
64
+ console.log('\n All files are up to date.\n');
65
+ return;
66
+ }
67
+
68
+ if (dryRun) {
69
+ console.log('');
70
+ for (const { rel, dest } of planSkills(cfg)) {
71
+ console.log(` [${existsSync(dest) ? 'update' : 'create'}] ${rel}`);
72
+ }
73
+ console.log('\n No files were modified (dry run).\n');
74
+ return;
291
75
  }
76
+
77
+ const { files } = installSkills(cfg);
78
+ console.log('\n Installed (auto-updates on npm install):\n');
79
+ for (const f of files) console.log(` ${f}`);
80
+ console.log('\n Tip: these are generated — you may add the install roots to .gitignore.\n');
81
+ console.log('Done!\n');
292
82
  }
293
83
 
294
- main().catch((err) => {
295
- console.error('Error:', err.message);
296
- process.exit(1);
297
- });
84
+ main();
@@ -0,0 +1,108 @@
1
+ import { cpSync, mkdirSync, readdirSync, readFileSync, existsSync, statSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+
4
+ // ─────────────────────────────────────────────────────────────────────────────
5
+ // Generic, project-agnostic skill installer.
6
+ //
7
+ // This file is intentionally free of any ts-procedures specifics so it can be
8
+ // dropped verbatim into any library that ships Agent Skills. The only
9
+ // per-project surface is the config object the caller passes in (see
10
+ // bin/setup.mjs / bin/postinstall.mjs).
11
+ //
12
+ // A "skill" is a spec-compliant directory containing a SKILL.md (name +
13
+ // description frontmatter) plus optional references/templates. The same
14
+ // directory is read by 30+ agent tools from `.agents/skills/<name>/` and by
15
+ // Claude Code from `.claude/skills/<name>/`, so we fan the one source out to
16
+ // every configured target root.
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ /** Recursively list file paths under `dir`, relative to `dir`. */
20
+ export function listFilesRecursive(dir) {
21
+ const out = [];
22
+ for (const entry of readdirSync(dir)) {
23
+ const full = join(dir, entry);
24
+ if (statSync(full).isDirectory()) {
25
+ for (const nested of listFilesRecursive(full)) out.push(join(entry, nested));
26
+ } else {
27
+ out.push(entry);
28
+ }
29
+ }
30
+ return out;
31
+ }
32
+
33
+ /**
34
+ * Enumerate the install plan without touching the filesystem.
35
+ *
36
+ * @param {object} cfg
37
+ * @param {string} cfg.projectRoot — consuming project root (absolute)
38
+ * @param {string} cfg.sourceDir — dir containing the source skill folders (absolute)
39
+ * @param {string[]} cfg.skills — skill folder names under sourceDir
40
+ * @param {string[]} cfg.targetRoots — install roots relative to projectRoot
41
+ * (e.g. ['.agents/skills', '.claude/skills'])
42
+ * @returns {Array<{ skill: string, root: string, rel: string, src: string, dest: string }>}
43
+ */
44
+ export function planSkills({ projectRoot, sourceDir, skills, targetRoots }) {
45
+ const plan = [];
46
+ for (const skill of skills) {
47
+ const skillSrc = join(sourceDir, skill);
48
+ for (const rel of listFilesRecursive(skillSrc)) {
49
+ for (const root of targetRoots) {
50
+ plan.push({
51
+ skill,
52
+ root,
53
+ rel: join(root, skill, rel),
54
+ src: join(skillSrc, rel),
55
+ dest: join(projectRoot, root, skill, rel),
56
+ });
57
+ }
58
+ }
59
+ }
60
+ return plan;
61
+ }
62
+
63
+ /**
64
+ * Copy each source skill into every target root (force-overwrite).
65
+ *
66
+ * @param {object} cfg — same shape as planSkills
67
+ * @returns {{ files: string[] }} — installed paths relative to projectRoot
68
+ */
69
+ export function installSkills({ projectRoot, sourceDir, skills, targetRoots }) {
70
+ const files = [];
71
+ for (const root of targetRoots) {
72
+ const rootDest = join(projectRoot, root);
73
+ mkdirSync(rootDest, { recursive: true });
74
+ for (const skill of skills) {
75
+ cpSync(join(sourceDir, skill), join(rootDest, skill), { recursive: true, force: true });
76
+ }
77
+ }
78
+ for (const { rel } of planSkills({ projectRoot, sourceDir, skills, targetRoots })) {
79
+ files.push(rel);
80
+ }
81
+ return { files };
82
+ }
83
+
84
+ /**
85
+ * Report which planned files are missing or differ from source (for --check).
86
+ *
87
+ * @param {object} cfg — same shape as planSkills
88
+ * @returns {string[]} — relative paths that are outdated (empty == up to date)
89
+ */
90
+ export function outdatedSkillFiles(cfg) {
91
+ const outdated = [];
92
+ for (const { rel, src, dest } of planSkills(cfg)) {
93
+ if (!existsSync(dest) || readFileSync(dest, 'utf-8') !== readFileSync(src, 'utf-8')) {
94
+ outdated.push(rel);
95
+ }
96
+ }
97
+ return outdated;
98
+ }
99
+
100
+ /** True if any skill was previously installed into any target root. */
101
+ export function isInstalled({ projectRoot, skills, targetRoots }) {
102
+ for (const root of targetRoots) {
103
+ for (const skill of skills) {
104
+ if (existsSync(join(projectRoot, root, skill))) return true;
105
+ }
106
+ }
107
+ return false;
108
+ }