@openwebf/webf 0.24.0 → 0.24.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/README.md CHANGED
@@ -10,6 +10,14 @@ npm install -g @openwebf/webf
10
10
 
11
11
  ## Usage
12
12
 
13
+ ### Initialize Claude Code Skills
14
+
15
+ The `webf agents init` command injects WebF Claude Code skills (from `@openwebf/claude-code-skills`) into your project and updates `CLAUDE.md` to reference them.
16
+
17
+ ```bash
18
+ webf agents init [project-dir]
19
+ ```
20
+
13
21
  ### Generate Code
14
22
 
15
23
  The `webf codegen` command generates Dart abstract classes and React/Vue components from TypeScript definitions. It automatically creates a project if needed.
package/bin/webf.js CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { Command } = require('commander');
4
4
  const version = require('../package.json').version;
5
- const { generateCommand, generateModuleCommand } = require('../dist/commands');
5
+ const { generateCommand, generateModuleCommand, agentsInitCommand } = require('../dist/commands');
6
6
 
7
7
  const program = new Command();
8
8
 
@@ -35,4 +35,12 @@ program
35
35
  .description('Generate NPM package and Dart bindings for a WebF module from TypeScript interfaces (*.module.d.ts)')
36
36
  .action(generateModuleCommand);
37
37
 
38
+ const agents = program.command('agents').description('Manage Claude Code agent configs');
39
+
40
+ agents
41
+ .command('init')
42
+ .argument('[projectDir]', 'Target project directory', '.')
43
+ .description('Initialize Claude Code skills config for a project')
44
+ .action(agentsInitCommand);
45
+
38
46
  program.parse();
package/dist/agents.js ADDED
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4
+ return new (P || (P = Promise))(function (resolve, reject) {
5
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
9
+ });
10
+ };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.agentsInitCommand = agentsInitCommand;
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const os_1 = __importDefault(require("os"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const yaml_1 = __importDefault(require("yaml"));
20
+ const WEBF_AGENTS_BLOCK_START = '<!-- webf-agents:init start -->';
21
+ const WEBF_AGENTS_BLOCK_END = '<!-- webf-agents:init end -->';
22
+ function ensureDirSync(dirPath) {
23
+ fs_1.default.mkdirSync(dirPath, { recursive: true });
24
+ }
25
+ function readFileIfExists(filePath) {
26
+ try {
27
+ return fs_1.default.readFileSync(filePath);
28
+ }
29
+ catch (error) {
30
+ if ((error === null || error === void 0 ? void 0 : error.code) === 'ENOENT')
31
+ return null;
32
+ throw error;
33
+ }
34
+ }
35
+ function backupFileSync(filePath) {
36
+ const timestamp = new Date()
37
+ .toISOString()
38
+ .replace(/[:.]/g, '')
39
+ .replace('T', '')
40
+ .replace('Z', 'Z');
41
+ const backupPath = `${filePath}.bak.${timestamp}`;
42
+ fs_1.default.copyFileSync(filePath, backupPath);
43
+ return backupPath;
44
+ }
45
+ function copyFileWithBackupSync(srcPath, destPath) {
46
+ const src = fs_1.default.readFileSync(srcPath);
47
+ const dest = readFileIfExists(destPath);
48
+ if (dest && Buffer.compare(src, dest) === 0)
49
+ return { changed: false, backupPath: null };
50
+ ensureDirSync(path_1.default.dirname(destPath));
51
+ let backupPath = null;
52
+ if (dest) {
53
+ backupPath = backupFileSync(destPath);
54
+ }
55
+ fs_1.default.writeFileSync(destPath, src);
56
+ return { changed: true, backupPath };
57
+ }
58
+ function copyDirRecursiveSync(srcDir, destDir, stats) {
59
+ ensureDirSync(destDir);
60
+ const entries = fs_1.default.readdirSync(srcDir, { withFileTypes: true });
61
+ for (const entry of entries) {
62
+ const srcPath = path_1.default.join(srcDir, entry.name);
63
+ const destPath = path_1.default.join(destDir, entry.name);
64
+ if (entry.isDirectory()) {
65
+ copyDirRecursiveSync(srcPath, destPath, stats);
66
+ continue;
67
+ }
68
+ if (entry.isFile()) {
69
+ const { changed, backupPath } = copyFileWithBackupSync(srcPath, destPath);
70
+ if (changed)
71
+ stats.filesWritten += 1;
72
+ else
73
+ stats.filesUnchanged += 1;
74
+ if (backupPath)
75
+ stats.backupsCreated += 1;
76
+ }
77
+ }
78
+ }
79
+ function listFilesRecursiveSync(dirPath) {
80
+ const out = [];
81
+ const entries = fs_1.default.readdirSync(dirPath, { withFileTypes: true });
82
+ for (const entry of entries) {
83
+ const entryPath = path_1.default.join(dirPath, entry.name);
84
+ if (entry.isDirectory()) {
85
+ out.push(...listFilesRecursiveSync(entryPath));
86
+ continue;
87
+ }
88
+ if (entry.isFile())
89
+ out.push(entryPath);
90
+ }
91
+ return out;
92
+ }
93
+ function toPosixRelativePath(fromDir, absolutePath) {
94
+ const rel = path_1.default.relative(fromDir, absolutePath);
95
+ return rel.split(path_1.default.sep).join('/');
96
+ }
97
+ function parseSkillFrontmatter(skillMd) {
98
+ var _a;
99
+ const trimmed = skillMd.trimStart();
100
+ if (!trimmed.startsWith('---'))
101
+ return {};
102
+ const endIndex = trimmed.indexOf('\n---', 3);
103
+ if (endIndex === -1)
104
+ return {};
105
+ const fm = trimmed.slice(3, endIndex).trim();
106
+ try {
107
+ const parsed = (_a = yaml_1.default.parse(fm)) !== null && _a !== void 0 ? _a : {};
108
+ return { name: parsed === null || parsed === void 0 ? void 0 : parsed.name, description: parsed === null || parsed === void 0 ? void 0 : parsed.description };
109
+ }
110
+ catch (_b) {
111
+ return {};
112
+ }
113
+ }
114
+ function updateOrAppendMarkedBlock(existing, newBlock) {
115
+ const startIndex = existing.indexOf(WEBF_AGENTS_BLOCK_START);
116
+ const endIndex = existing.indexOf(WEBF_AGENTS_BLOCK_END);
117
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
118
+ const before = existing.slice(0, startIndex).trimEnd();
119
+ const after = existing.slice(endIndex + WEBF_AGENTS_BLOCK_END.length).trimStart();
120
+ const next = [before, newBlock.trim(), after].filter(Boolean).join('\n\n');
121
+ return { content: next + '\n', action: 'replaced' };
122
+ }
123
+ const content = existing.trimEnd();
124
+ return { content: (content ? content + '\n\n' : '') + newBlock.trim() + '\n', action: 'appended' };
125
+ }
126
+ function buildClaudeBlock(sourcePackageName, sourcePackageVersion, skills) {
127
+ const lines = [];
128
+ lines.push(WEBF_AGENTS_BLOCK_START);
129
+ lines.push('## WebF Claude Code Skills');
130
+ lines.push('');
131
+ lines.push(`Source: \`${sourcePackageName}@${sourcePackageVersion}\``);
132
+ lines.push('');
133
+ lines.push('### Skills');
134
+ for (const skill of skills) {
135
+ const desc = skill.description ? ` — ${skill.description}` : '';
136
+ lines.push(`- \`${skill.name}\`${desc} (\`${skill.skillFileRelativePath}\`)`);
137
+ }
138
+ const anyReferences = skills.some(s => s.referenceRelativePaths.length > 0);
139
+ if (anyReferences) {
140
+ lines.push('');
141
+ lines.push('### References');
142
+ for (const skill of skills) {
143
+ if (skill.referenceRelativePaths.length === 0)
144
+ continue;
145
+ const refs = skill.referenceRelativePaths.map(r => `\`${r}\``).join(', ');
146
+ lines.push(`- \`${skill.name}\`: ${refs}`);
147
+ }
148
+ }
149
+ lines.push(WEBF_AGENTS_BLOCK_END);
150
+ return lines.join('\n');
151
+ }
152
+ function resolveSkillsPackageRoot() {
153
+ var _a, _b;
154
+ const packageJsonPath = require.resolve('@openwebf/claude-code-skills/package.json');
155
+ const packageRoot = path_1.default.dirname(packageJsonPath);
156
+ const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf-8'));
157
+ return {
158
+ packageRoot,
159
+ packageName: (_a = packageJson === null || packageJson === void 0 ? void 0 : packageJson.name) !== null && _a !== void 0 ? _a : '@openwebf/claude-code-skills',
160
+ packageVersion: (_b = packageJson === null || packageJson === void 0 ? void 0 : packageJson.version) !== null && _b !== void 0 ? _b : 'unknown',
161
+ };
162
+ }
163
+ function listSkillDirectories(skillsPackageRoot) {
164
+ const entries = fs_1.default.readdirSync(skillsPackageRoot, { withFileTypes: true });
165
+ const dirs = entries
166
+ .filter(e => e.isDirectory())
167
+ .map(e => e.name)
168
+ .filter(name => fs_1.default.existsSync(path_1.default.join(skillsPackageRoot, name, 'SKILL.md')))
169
+ .sort();
170
+ return dirs;
171
+ }
172
+ function collectSkillInfo(projectRoot, skillsDir, skillDirectoryName) {
173
+ var _a;
174
+ const skillDirAbs = path_1.default.join(skillsDir, skillDirectoryName);
175
+ const skillMdAbs = path_1.default.join(skillDirAbs, 'SKILL.md');
176
+ const skillMd = fs_1.default.readFileSync(skillMdAbs, 'utf-8');
177
+ const fm = parseSkillFrontmatter(skillMd);
178
+ const referenceRelativePaths = [];
179
+ const files = listFilesRecursiveSync(skillDirAbs);
180
+ for (const fileAbs of files) {
181
+ if (path_1.default.basename(fileAbs) === 'SKILL.md')
182
+ continue;
183
+ referenceRelativePaths.push(toPosixRelativePath(projectRoot, fileAbs));
184
+ }
185
+ referenceRelativePaths.sort();
186
+ return {
187
+ directoryName: skillDirectoryName,
188
+ name: (_a = fm.name) !== null && _a !== void 0 ? _a : skillDirectoryName,
189
+ description: fm.description,
190
+ skillFileRelativePath: toPosixRelativePath(projectRoot, skillMdAbs),
191
+ referenceRelativePaths,
192
+ };
193
+ }
194
+ function agentsInitCommand(projectDir) {
195
+ return __awaiter(this, void 0, void 0, function* () {
196
+ var _a, _b;
197
+ const startedAt = Date.now();
198
+ const resolvedProjectDir = path_1.default.resolve(process.cwd(), projectDir || '.');
199
+ const claudeMdPath = path_1.default.join(resolvedProjectDir, 'CLAUDE.md');
200
+ const claudeDir = path_1.default.join(resolvedProjectDir, '.claude');
201
+ const projectSkillsDir = path_1.default.join(claudeDir, 'skills');
202
+ const hasClaudeMd = fs_1.default.existsSync(claudeMdPath);
203
+ const hasClaudeDir = fs_1.default.existsSync(claudeDir);
204
+ const isNewProject = !hasClaudeMd && !hasClaudeDir;
205
+ console.log('webf agents init');
206
+ console.log(`Project: ${resolvedProjectDir}`);
207
+ if (isNewProject) {
208
+ console.log('Detected: no CLAUDE.md and no .claude/ (new project)');
209
+ }
210
+ else {
211
+ console.log(`Detected: CLAUDE.md=${hasClaudeMd ? 'yes' : 'no'}, .claude/=${hasClaudeDir ? 'yes' : 'no'} (existing project)`);
212
+ }
213
+ const { packageRoot, packageName, packageVersion } = resolveSkillsPackageRoot();
214
+ const skillDirectories = listSkillDirectories(packageRoot);
215
+ if (skillDirectories.length === 0) {
216
+ throw new Error(`No skills found in ${packageName} (resolved at ${packageRoot}).`);
217
+ }
218
+ console.log(`Skills source: ${packageName}@${packageVersion}`);
219
+ console.log(`Skills destination: ${toPosixRelativePath(resolvedProjectDir, projectSkillsDir)}`);
220
+ ensureDirSync(projectSkillsDir);
221
+ const copyStats = { filesWritten: 0, filesUnchanged: 0, backupsCreated: 0 };
222
+ for (const skillDirName of skillDirectories) {
223
+ const srcSkillDir = path_1.default.join(packageRoot, skillDirName);
224
+ const destSkillDir = path_1.default.join(projectSkillsDir, skillDirName);
225
+ copyDirRecursiveSync(srcSkillDir, destSkillDir, copyStats);
226
+ }
227
+ const installedSkills = skillDirectories.map(skillDirName => collectSkillInfo(resolvedProjectDir, projectSkillsDir, skillDirName));
228
+ const block = buildClaudeBlock(packageName, packageVersion, installedSkills);
229
+ if (isNewProject) {
230
+ const initial = ['# Claude Code', '', block, ''].join('\n');
231
+ fs_1.default.writeFileSync(claudeMdPath, initial, 'utf-8');
232
+ console.log(`Created ${toPosixRelativePath(resolvedProjectDir, claudeMdPath)}`);
233
+ }
234
+ else {
235
+ const existing = (_b = (_a = readFileIfExists(claudeMdPath)) === null || _a === void 0 ? void 0 : _a.toString('utf-8')) !== null && _b !== void 0 ? _b : '';
236
+ const { content, action } = updateOrAppendMarkedBlock(existing, block);
237
+ fs_1.default.writeFileSync(claudeMdPath, content, 'utf-8');
238
+ console.log(`${action === 'replaced' ? 'Updated' : 'Appended'} WebF skills block in ${toPosixRelativePath(resolvedProjectDir, claudeMdPath)}`);
239
+ }
240
+ const versionFilePath = path_1.default.join(claudeDir, 'webf-claude-code-skills.version');
241
+ fs_1.default.writeFileSync(versionFilePath, `${packageName}@${packageVersion}${os_1.default.EOL}`, 'utf-8');
242
+ console.log(`Wrote ${toPosixRelativePath(resolvedProjectDir, versionFilePath)}`);
243
+ console.log(`Installed ${installedSkills.length} skills (${copyStats.filesWritten} files written, ${copyStats.filesUnchanged} unchanged, ${copyStats.backupsCreated} backups) in ${Date.now() - startedAt}ms`);
244
+ });
245
+ }
package/dist/commands.js CHANGED
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  return (mod && mod.__esModule) ? mod : { "default": mod };
13
13
  };
14
14
  Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.agentsInitCommand = void 0;
15
16
  exports.generateCommand = generateCommand;
16
17
  exports.generateModuleCommand = generateModuleCommand;
17
18
  const child_process_1 = require("child_process");
@@ -25,6 +26,8 @@ const glob_1 = require("glob");
25
26
  const lodash_1 = __importDefault(require("lodash"));
26
27
  const inquirer_1 = __importDefault(require("inquirer"));
27
28
  const yaml_1 = __importDefault(require("yaml"));
29
+ const agents_1 = require("./agents");
30
+ Object.defineProperty(exports, "agentsInitCommand", { enumerable: true, get: function () { return agents_1.agentsInitCommand; } });
28
31
  /**
29
32
  * Sanitize a package name to comply with npm naming rules
30
33
  * NPM package name rules:
@@ -178,33 +181,6 @@ function readFlutterPackageMetadata(packagePath) {
178
181
  return null;
179
182
  }
180
183
  }
181
- function copyReadmeToPackageRoot(params) {
182
- const { sourceRoot, targetRoot } = params;
183
- const targetPath = path_1.default.join(targetRoot, 'README.md');
184
- if (fs_1.default.existsSync(targetPath)) {
185
- return { copied: false, targetPath };
186
- }
187
- const candidateNames = ['README.md', 'Readme.md', 'readme.md'];
188
- let sourcePath = null;
189
- for (const candidate of candidateNames) {
190
- const abs = path_1.default.join(sourceRoot, candidate);
191
- if (fs_1.default.existsSync(abs)) {
192
- sourcePath = abs;
193
- break;
194
- }
195
- }
196
- if (!sourcePath) {
197
- return { copied: false, targetPath };
198
- }
199
- try {
200
- const content = fs_1.default.readFileSync(sourcePath, 'utf-8');
201
- writeFileIfChanged(targetPath, content);
202
- return { copied: true, sourcePath, targetPath };
203
- }
204
- catch (_a) {
205
- return { copied: false, targetPath };
206
- }
207
- }
208
184
  // Copy markdown docs that match .d.ts basenames from source to the built dist folder,
209
185
  // and generate an aggregated README.md in the dist directory.
210
186
  function copyMarkdownDocsToDist(params) {
@@ -684,16 +660,6 @@ function generateCommand(distPath, options) {
684
660
  }
685
661
  // Auto-initialize typings in the output directory if needed
686
662
  ensureInitialized(resolvedDistPath);
687
- // Copy README.md from the source Flutter package into the npm package root (so `npm publish` includes it).
688
- if (options.flutterPackageSrc) {
689
- const { copied } = copyReadmeToPackageRoot({
690
- sourceRoot: options.flutterPackageSrc,
691
- targetRoot: resolvedDistPath,
692
- });
693
- if (copied) {
694
- console.log('📄 Copied README.md to package root');
695
- }
696
- }
697
663
  console.log(`\nGenerating ${framework} code from ${options.flutterPackageSrc}...`);
698
664
  yield (0, generator_1.dartGen)({
699
665
  source: options.flutterPackageSrc,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwebf/webf",
3
- "version": "0.24.0",
3
+ "version": "0.24.1",
4
4
  "description": "Command line tools for WebF",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -39,6 +39,7 @@
39
39
  "dependencies": {
40
40
  "@microsoft/tsdoc": "^0.15.1",
41
41
  "@microsoft/tsdoc-config": "^0.17.1",
42
+ "@openwebf/claude-code-skills": "^1.0.1",
42
43
  "commander": "^14.0.0",
43
44
  "glob": "^10.4.5",
44
45
  "inquirer": "^8.2.6",
package/src/agents.ts ADDED
@@ -0,0 +1,267 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import yaml from 'yaml';
5
+
6
+ type SkillInfo = {
7
+ directoryName: string;
8
+ name: string;
9
+ description?: string;
10
+ skillFileRelativePath: string;
11
+ referenceRelativePaths: string[];
12
+ };
13
+
14
+ type CopyStats = {
15
+ filesWritten: number;
16
+ filesUnchanged: number;
17
+ backupsCreated: number;
18
+ };
19
+
20
+ const WEBF_AGENTS_BLOCK_START = '<!-- webf-agents:init start -->';
21
+ const WEBF_AGENTS_BLOCK_END = '<!-- webf-agents:init end -->';
22
+
23
+ function ensureDirSync(dirPath: string) {
24
+ fs.mkdirSync(dirPath, { recursive: true });
25
+ }
26
+
27
+ function readFileIfExists(filePath: string): Buffer | null {
28
+ try {
29
+ return fs.readFileSync(filePath);
30
+ } catch (error: any) {
31
+ if (error?.code === 'ENOENT') return null;
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ function backupFileSync(filePath: string) {
37
+ const timestamp = new Date()
38
+ .toISOString()
39
+ .replace(/[:.]/g, '')
40
+ .replace('T', '')
41
+ .replace('Z', 'Z');
42
+ const backupPath = `${filePath}.bak.${timestamp}`;
43
+ fs.copyFileSync(filePath, backupPath);
44
+ return backupPath;
45
+ }
46
+
47
+ function copyFileWithBackupSync(srcPath: string, destPath: string) {
48
+ const src = fs.readFileSync(srcPath);
49
+ const dest = readFileIfExists(destPath);
50
+
51
+ if (dest && Buffer.compare(src, dest) === 0) return { changed: false, backupPath: null as string | null };
52
+
53
+ ensureDirSync(path.dirname(destPath));
54
+ let backupPath: string | null = null;
55
+ if (dest) {
56
+ backupPath = backupFileSync(destPath);
57
+ }
58
+ fs.writeFileSync(destPath, src);
59
+ return { changed: true, backupPath };
60
+ }
61
+
62
+ function copyDirRecursiveSync(srcDir: string, destDir: string, stats: CopyStats) {
63
+ ensureDirSync(destDir);
64
+ const entries = fs.readdirSync(srcDir, { withFileTypes: true });
65
+ for (const entry of entries) {
66
+ const srcPath = path.join(srcDir, entry.name);
67
+ const destPath = path.join(destDir, entry.name);
68
+ if (entry.isDirectory()) {
69
+ copyDirRecursiveSync(srcPath, destPath, stats);
70
+ continue;
71
+ }
72
+ if (entry.isFile()) {
73
+ const { changed, backupPath } = copyFileWithBackupSync(srcPath, destPath);
74
+ if (changed) stats.filesWritten += 1;
75
+ else stats.filesUnchanged += 1;
76
+ if (backupPath) stats.backupsCreated += 1;
77
+ }
78
+ }
79
+ }
80
+
81
+ function listFilesRecursiveSync(dirPath: string): string[] {
82
+ const out: string[] = [];
83
+ const entries = fs.readdirSync(dirPath, { withFileTypes: true });
84
+ for (const entry of entries) {
85
+ const entryPath = path.join(dirPath, entry.name);
86
+ if (entry.isDirectory()) {
87
+ out.push(...listFilesRecursiveSync(entryPath));
88
+ continue;
89
+ }
90
+ if (entry.isFile()) out.push(entryPath);
91
+ }
92
+ return out;
93
+ }
94
+
95
+ function toPosixRelativePath(fromDir: string, absolutePath: string) {
96
+ const rel = path.relative(fromDir, absolutePath);
97
+ return rel.split(path.sep).join('/');
98
+ }
99
+
100
+ function parseSkillFrontmatter(skillMd: string): { name?: string; description?: string } {
101
+ const trimmed = skillMd.trimStart();
102
+ if (!trimmed.startsWith('---')) return {};
103
+ const endIndex = trimmed.indexOf('\n---', 3);
104
+ if (endIndex === -1) return {};
105
+ const fm = trimmed.slice(3, endIndex).trim();
106
+ try {
107
+ const parsed = yaml.parse(fm) ?? {};
108
+ return { name: parsed?.name, description: parsed?.description };
109
+ } catch {
110
+ return {};
111
+ }
112
+ }
113
+
114
+ function updateOrAppendMarkedBlock(existing: string, newBlock: string): { content: string; action: 'replaced' | 'appended' } {
115
+ const startIndex = existing.indexOf(WEBF_AGENTS_BLOCK_START);
116
+ const endIndex = existing.indexOf(WEBF_AGENTS_BLOCK_END);
117
+
118
+ if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
119
+ const before = existing.slice(0, startIndex).trimEnd();
120
+ const after = existing.slice(endIndex + WEBF_AGENTS_BLOCK_END.length).trimStart();
121
+ const next = [before, newBlock.trim(), after].filter(Boolean).join('\n\n');
122
+ return { content: next + '\n', action: 'replaced' };
123
+ }
124
+
125
+ const content = existing.trimEnd();
126
+ return { content: (content ? content + '\n\n' : '') + newBlock.trim() + '\n', action: 'appended' };
127
+ }
128
+
129
+ function buildClaudeBlock(sourcePackageName: string, sourcePackageVersion: string, skills: SkillInfo[]) {
130
+ const lines: string[] = [];
131
+ lines.push(WEBF_AGENTS_BLOCK_START);
132
+ lines.push('## WebF Claude Code Skills');
133
+ lines.push('');
134
+ lines.push(`Source: \`${sourcePackageName}@${sourcePackageVersion}\``);
135
+ lines.push('');
136
+ lines.push('### Skills');
137
+ for (const skill of skills) {
138
+ const desc = skill.description ? ` — ${skill.description}` : '';
139
+ lines.push(`- \`${skill.name}\`${desc} (\`${skill.skillFileRelativePath}\`)`);
140
+ }
141
+
142
+ const anyReferences = skills.some(s => s.referenceRelativePaths.length > 0);
143
+ if (anyReferences) {
144
+ lines.push('');
145
+ lines.push('### References');
146
+ for (const skill of skills) {
147
+ if (skill.referenceRelativePaths.length === 0) continue;
148
+ const refs = skill.referenceRelativePaths.map(r => `\`${r}\``).join(', ');
149
+ lines.push(`- \`${skill.name}\`: ${refs}`);
150
+ }
151
+ }
152
+
153
+ lines.push(WEBF_AGENTS_BLOCK_END);
154
+ return lines.join('\n');
155
+ }
156
+
157
+ function resolveSkillsPackageRoot() {
158
+ const packageJsonPath = require.resolve('@openwebf/claude-code-skills/package.json');
159
+ const packageRoot = path.dirname(packageJsonPath);
160
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
161
+ return {
162
+ packageRoot,
163
+ packageName: packageJson?.name ?? '@openwebf/claude-code-skills',
164
+ packageVersion: packageJson?.version ?? 'unknown',
165
+ };
166
+ }
167
+
168
+ function listSkillDirectories(skillsPackageRoot: string) {
169
+ const entries = fs.readdirSync(skillsPackageRoot, { withFileTypes: true });
170
+ const dirs = entries
171
+ .filter(e => e.isDirectory())
172
+ .map(e => e.name)
173
+ .filter(name => fs.existsSync(path.join(skillsPackageRoot, name, 'SKILL.md')))
174
+ .sort();
175
+ return dirs;
176
+ }
177
+
178
+ function collectSkillInfo(projectRoot: string, skillsDir: string, skillDirectoryName: string): SkillInfo {
179
+ const skillDirAbs = path.join(skillsDir, skillDirectoryName);
180
+ const skillMdAbs = path.join(skillDirAbs, 'SKILL.md');
181
+ const skillMd = fs.readFileSync(skillMdAbs, 'utf-8');
182
+ const fm = parseSkillFrontmatter(skillMd);
183
+
184
+ const referenceRelativePaths: string[] = [];
185
+ const files = listFilesRecursiveSync(skillDirAbs);
186
+ for (const fileAbs of files) {
187
+ if (path.basename(fileAbs) === 'SKILL.md') continue;
188
+ referenceRelativePaths.push(toPosixRelativePath(projectRoot, fileAbs));
189
+ }
190
+ referenceRelativePaths.sort();
191
+
192
+ return {
193
+ directoryName: skillDirectoryName,
194
+ name: fm.name ?? skillDirectoryName,
195
+ description: fm.description,
196
+ skillFileRelativePath: toPosixRelativePath(projectRoot, skillMdAbs),
197
+ referenceRelativePaths,
198
+ };
199
+ }
200
+
201
+ async function agentsInitCommand(projectDir: string): Promise<void> {
202
+ const startedAt = Date.now();
203
+ const resolvedProjectDir = path.resolve(process.cwd(), projectDir || '.');
204
+ const claudeMdPath = path.join(resolvedProjectDir, 'CLAUDE.md');
205
+ const claudeDir = path.join(resolvedProjectDir, '.claude');
206
+ const projectSkillsDir = path.join(claudeDir, 'skills');
207
+
208
+ const hasClaudeMd = fs.existsSync(claudeMdPath);
209
+ const hasClaudeDir = fs.existsSync(claudeDir);
210
+
211
+ const isNewProject = !hasClaudeMd && !hasClaudeDir;
212
+
213
+ console.log('webf agents init');
214
+ console.log(`Project: ${resolvedProjectDir}`);
215
+ if (isNewProject) {
216
+ console.log('Detected: no CLAUDE.md and no .claude/ (new project)');
217
+ } else {
218
+ console.log(`Detected: CLAUDE.md=${hasClaudeMd ? 'yes' : 'no'}, .claude/=${hasClaudeDir ? 'yes' : 'no'} (existing project)`);
219
+ }
220
+
221
+ const { packageRoot, packageName, packageVersion } = resolveSkillsPackageRoot();
222
+ const skillDirectories = listSkillDirectories(packageRoot);
223
+
224
+ if (skillDirectories.length === 0) {
225
+ throw new Error(`No skills found in ${packageName} (resolved at ${packageRoot}).`);
226
+ }
227
+
228
+ console.log(`Skills source: ${packageName}@${packageVersion}`);
229
+ console.log(`Skills destination: ${toPosixRelativePath(resolvedProjectDir, projectSkillsDir)}`);
230
+
231
+ ensureDirSync(projectSkillsDir);
232
+
233
+ const copyStats: CopyStats = { filesWritten: 0, filesUnchanged: 0, backupsCreated: 0 };
234
+ for (const skillDirName of skillDirectories) {
235
+ const srcSkillDir = path.join(packageRoot, skillDirName);
236
+ const destSkillDir = path.join(projectSkillsDir, skillDirName);
237
+
238
+ copyDirRecursiveSync(srcSkillDir, destSkillDir, copyStats);
239
+ }
240
+
241
+ const installedSkills = skillDirectories.map(skillDirName =>
242
+ collectSkillInfo(resolvedProjectDir, projectSkillsDir, skillDirName)
243
+ );
244
+
245
+ const block = buildClaudeBlock(packageName, packageVersion, installedSkills);
246
+
247
+ if (isNewProject) {
248
+ const initial = ['# Claude Code', '', block, ''].join('\n');
249
+ fs.writeFileSync(claudeMdPath, initial, 'utf-8');
250
+ console.log(`Created ${toPosixRelativePath(resolvedProjectDir, claudeMdPath)}`);
251
+ } else {
252
+ const existing = readFileIfExists(claudeMdPath)?.toString('utf-8') ?? '';
253
+ const { content, action } = updateOrAppendMarkedBlock(existing, block);
254
+ fs.writeFileSync(claudeMdPath, content, 'utf-8');
255
+ console.log(`${action === 'replaced' ? 'Updated' : 'Appended'} WebF skills block in ${toPosixRelativePath(resolvedProjectDir, claudeMdPath)}`);
256
+ }
257
+
258
+ const versionFilePath = path.join(claudeDir, 'webf-claude-code-skills.version');
259
+ fs.writeFileSync(versionFilePath, `${packageName}@${packageVersion}${os.EOL}`, 'utf-8');
260
+ console.log(`Wrote ${toPosixRelativePath(resolvedProjectDir, versionFilePath)}`);
261
+
262
+ console.log(
263
+ `Installed ${installedSkills.length} skills (${copyStats.filesWritten} files written, ${copyStats.filesUnchanged} unchanged, ${copyStats.backupsCreated} backups) in ${Date.now() - startedAt}ms`
264
+ );
265
+ }
266
+
267
+ export { agentsInitCommand };
package/src/commands.ts CHANGED
@@ -9,6 +9,7 @@ import { globSync } from 'glob';
9
9
  import _ from 'lodash';
10
10
  import inquirer from 'inquirer';
11
11
  import yaml from 'yaml';
12
+ import { agentsInitCommand } from './agents';
12
13
 
13
14
  interface GenerateOptions {
14
15
  flutterPackageSrc?: string;
@@ -238,40 +239,6 @@ function readFlutterPackageMetadata(packagePath: string): FlutterPackageMetadata
238
239
  }
239
240
  }
240
241
 
241
- function copyReadmeToPackageRoot(params: {
242
- sourceRoot: string;
243
- targetRoot: string;
244
- }): { copied: boolean; sourcePath?: string; targetPath: string } {
245
- const { sourceRoot, targetRoot } = params;
246
- const targetPath = path.join(targetRoot, 'README.md');
247
-
248
- if (fs.existsSync(targetPath)) {
249
- return { copied: false, targetPath };
250
- }
251
-
252
- const candidateNames = ['README.md', 'Readme.md', 'readme.md'];
253
- let sourcePath: string | null = null;
254
- for (const candidate of candidateNames) {
255
- const abs = path.join(sourceRoot, candidate);
256
- if (fs.existsSync(abs)) {
257
- sourcePath = abs;
258
- break;
259
- }
260
- }
261
-
262
- if (!sourcePath) {
263
- return { copied: false, targetPath };
264
- }
265
-
266
- try {
267
- const content = fs.readFileSync(sourcePath, 'utf-8');
268
- writeFileIfChanged(targetPath, content);
269
- return { copied: true, sourcePath, targetPath };
270
- } catch {
271
- return { copied: false, targetPath };
272
- }
273
- }
274
-
275
242
  // Copy markdown docs that match .d.ts basenames from source to the built dist folder,
276
243
  // and generate an aggregated README.md in the dist directory.
277
244
  async function copyMarkdownDocsToDist(params: {
@@ -813,17 +780,6 @@ async function generateCommand(distPath: string, options: GenerateOptions): Prom
813
780
  // Auto-initialize typings in the output directory if needed
814
781
  ensureInitialized(resolvedDistPath);
815
782
 
816
- // Copy README.md from the source Flutter package into the npm package root (so `npm publish` includes it).
817
- if (options.flutterPackageSrc) {
818
- const { copied } = copyReadmeToPackageRoot({
819
- sourceRoot: options.flutterPackageSrc,
820
- targetRoot: resolvedDistPath,
821
- });
822
- if (copied) {
823
- console.log('📄 Copied README.md to package root');
824
- }
825
- }
826
-
827
783
  console.log(`\nGenerating ${framework} code from ${options.flutterPackageSrc}...`);
828
784
 
829
785
  await dartGen({
@@ -1446,4 +1402,4 @@ async function buildAndPublishPackage(packagePath: string, registry?: string, is
1446
1402
  }
1447
1403
  }
1448
1404
 
1449
- export { generateCommand, generateModuleCommand };
1405
+ export { generateCommand, generateModuleCommand, agentsInitCommand };
@@ -5,7 +5,7 @@
5
5
  "main": "dist/index.js",
6
6
  "module": "dist/index.mjs",
7
7
  "types": "dist/index.d.ts",
8
- "files": ["dist", "README.md"],
8
+ "files": ["dist"],
9
9
  "scripts": {
10
10
  "build": "tsup"
11
11
  },
@@ -4,7 +4,7 @@
4
4
  "description": "<%= description %>",
5
5
  "main": "",
6
6
  "types": "index.d.ts",
7
- "files": ["index.d.ts", "README.md"],
7
+ "files": ["index.d.ts"],
8
8
  "keywords": [],
9
9
  "author": "",
10
10
  "license": "ISC",
@@ -0,0 +1,80 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
4
+ import { agentsInitCommand } from '../src/agents';
5
+
6
+ function readText(filePath: string) {
7
+ return fs.readFileSync(filePath, 'utf-8');
8
+ }
9
+
10
+ describe('webf agents init', () => {
11
+ let tempDir: string;
12
+ let consoleSpy: jest.SpyInstance;
13
+
14
+ beforeEach(() => {
15
+ tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'webf-agents-init-'));
16
+ consoleSpy = jest.spyOn(console, 'log').mockImplementation(() => {});
17
+ });
18
+
19
+ afterEach(() => {
20
+ consoleSpy.mockRestore();
21
+ fs.rmSync(tempDir, { recursive: true, force: true });
22
+ });
23
+
24
+ it('initializes a new project with skills and CLAUDE.md', async () => {
25
+ await agentsInitCommand(tempDir);
26
+
27
+ expect(fs.existsSync(path.join(tempDir, 'CLAUDE.md'))).toBe(true);
28
+ expect(fs.existsSync(path.join(tempDir, '.claude', 'skills', 'webf-quickstart', 'SKILL.md'))).toBe(true);
29
+
30
+ const claude = readText(path.join(tempDir, 'CLAUDE.md'));
31
+ expect(claude).toContain('<!-- webf-agents:init start -->');
32
+ expect(claude).toContain('Source: `@openwebf/claude-code-skills@');
33
+ expect(claude).toContain('### Skills');
34
+
35
+ const version = readText(path.join(tempDir, '.claude', 'webf-claude-code-skills.version'));
36
+ expect(version).toMatch(/^@openwebf\/claude-code-skills@/);
37
+ });
38
+
39
+ it('updates an existing CLAUDE.md without removing existing content', async () => {
40
+ fs.writeFileSync(path.join(tempDir, 'CLAUDE.md'), '# Existing\n\nHello\n', 'utf-8');
41
+
42
+ await agentsInitCommand(tempDir);
43
+
44
+ const claude = readText(path.join(tempDir, 'CLAUDE.md'));
45
+ expect(claude).toContain('# Existing');
46
+ expect(claude).toContain('Hello');
47
+ expect(claude).toContain('<!-- webf-agents:init start -->');
48
+ });
49
+
50
+ it('is idempotent (does not duplicate the injected block)', async () => {
51
+ await agentsInitCommand(tempDir);
52
+ await agentsInitCommand(tempDir);
53
+
54
+ const claude = readText(path.join(tempDir, 'CLAUDE.md'));
55
+ const occurrences = claude.split('<!-- webf-agents:init start -->').length - 1;
56
+ expect(occurrences).toBe(1);
57
+ });
58
+
59
+ it('backs up modified skill files before overwriting', async () => {
60
+ const skillDir = path.join(tempDir, '.claude', 'skills', 'webf-quickstart');
61
+ fs.mkdirSync(skillDir, { recursive: true });
62
+ fs.writeFileSync(path.join(skillDir, 'SKILL.md'), 'local edits', 'utf-8');
63
+
64
+ await agentsInitCommand(tempDir);
65
+
66
+ const files = fs.readdirSync(skillDir);
67
+ expect(files.some(f => f.startsWith('SKILL.md.bak.'))).toBe(true);
68
+
69
+ const sourceSkill = fs.readFileSync(
70
+ path.join(
71
+ path.dirname(require.resolve('@openwebf/claude-code-skills/package.json')),
72
+ 'webf-quickstart',
73
+ 'SKILL.md'
74
+ ),
75
+ 'utf-8'
76
+ );
77
+ const installedSkill = readText(path.join(skillDir, 'SKILL.md'));
78
+ expect(installedSkill).toBe(sourceSkill);
79
+ });
80
+ });
@@ -456,78 +456,6 @@ describe('Commands', () => {
456
456
  }));
457
457
  });
458
458
 
459
- it('should copy Flutter README.md into generated React package root', async () => {
460
- const options = {
461
- flutterPackageSrc: '/flutter/src',
462
- framework: 'react',
463
- packageName: 'test-package'
464
- };
465
-
466
- // Mock TypeScript validation
467
- mockTypeScriptValidation('/flutter/src');
468
-
469
- const originalExistsSync = mockFs.existsSync as jest.Mock;
470
- mockFs.existsSync = jest.fn().mockImplementation((filePath: any) => {
471
- const pathStr = filePath.toString();
472
- if (pathStr === '/flutter/src/README.md') return true;
473
- if (pathStr === path.join(path.resolve('/dist'), 'README.md')) return false;
474
- return originalExistsSync(filePath);
475
- });
476
-
477
- const originalReadFileSync = mockFs.readFileSync as jest.Mock;
478
- mockFs.readFileSync = jest.fn().mockImplementation((filePath: any, encoding?: any) => {
479
- const pathStr = filePath.toString();
480
- if (pathStr === '/flutter/src/README.md') {
481
- return '# Flutter README\n\nHello';
482
- }
483
- return originalReadFileSync(filePath, encoding);
484
- });
485
-
486
- await generateCommand('/dist', options);
487
-
488
- expect(mockFs.writeFileSync).toHaveBeenCalledWith(
489
- path.join(path.resolve('/dist'), 'README.md'),
490
- '# Flutter README\n\nHello',
491
- 'utf-8'
492
- );
493
- });
494
-
495
- it('should copy Flutter README.md into generated Vue package root', async () => {
496
- const options = {
497
- flutterPackageSrc: '/flutter/src',
498
- framework: 'vue',
499
- packageName: 'test-package'
500
- };
501
-
502
- // Mock TypeScript validation
503
- mockTypeScriptValidation('/flutter/src');
504
-
505
- const originalExistsSync = mockFs.existsSync as jest.Mock;
506
- mockFs.existsSync = jest.fn().mockImplementation((filePath: any) => {
507
- const pathStr = filePath.toString();
508
- if (pathStr === '/flutter/src/README.md') return true;
509
- if (pathStr === path.join(path.resolve('/dist'), 'README.md')) return false;
510
- return originalExistsSync(filePath);
511
- });
512
-
513
- const originalReadFileSync = mockFs.readFileSync as jest.Mock;
514
- mockFs.readFileSync = jest.fn().mockImplementation((filePath: any, encoding?: any) => {
515
- const pathStr = filePath.toString();
516
- if (pathStr === '/flutter/src/README.md') {
517
- return '# Flutter README\n\nHello';
518
- }
519
- return originalReadFileSync(filePath, encoding);
520
- });
521
-
522
- await generateCommand('/dist', options);
523
-
524
- expect(mockFs.writeFileSync).toHaveBeenCalledWith(
525
- path.join(path.resolve('/dist'), 'README.md'),
526
- '# Flutter README\n\nHello',
527
- 'utf-8'
528
- );
529
- });
530
-
531
459
  it('should generate an aggregated README in dist from markdown docs', async () => {
532
460
  const options = {
533
461
  flutterPackageSrc: '/flutter/src',