@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 +8 -0
- package/bin/webf.js +9 -1
- package/dist/agents.js +245 -0
- package/dist/commands.js +3 -37
- package/package.json +2 -1
- package/src/agents.ts +267 -0
- package/src/commands.ts +2 -46
- package/templates/react.package.json.tpl +1 -1
- package/templates/vue.package.json.tpl +1 -1
- package/test/agents-init.test.ts +80 -0
- package/test/commands.test.ts +0 -72
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.
|
|
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 };
|
|
@@ -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
|
+
});
|
package/test/commands.test.ts
CHANGED
|
@@ -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',
|