@openwebf/webf 0.24.0 → 0.24.2

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/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:
@@ -146,10 +149,10 @@ const tsConfig = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '..
146
149
  const gitignore = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/gitignore.tpl'), 'utf-8');
147
150
  const modulePackageJson = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/module.package.json.tpl'), 'utf-8');
148
151
  const moduleTsConfig = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/module.tsconfig.json.tpl'), 'utf-8');
149
- const moduleTsUpConfig = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/module.tsup.config.ts.tpl'), 'utf-8');
152
+ const moduleTsDownConfig = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/module.tsdown.config.ts.tpl'), 'utf-8');
150
153
  const reactPackageJson = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/react.package.json.tpl'), 'utf-8');
151
154
  const reactTsConfig = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/react.tsconfig.json.tpl'), 'utf-8');
152
- const reactTsUpConfig = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/react.tsup.config.ts.tpl'), 'utf-8');
155
+ const reactTsDownConfig = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/react.tsdown.config.ts.tpl'), 'utf-8');
153
156
  const reactIndexTpl = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/react.index.ts.tpl'), 'utf-8');
154
157
  const vuePackageJson = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/vue.package.json.tpl'), 'utf-8');
155
158
  const vueTsConfig = fs_1.default.readFileSync(path_1.default.resolve(__dirname, '../templates/vue.tsconfig.json.tpl'), 'utf-8');
@@ -357,9 +360,9 @@ function createCommand(target, options) {
357
360
  const tsConfigPath = path_1.default.join(target, 'tsconfig.json');
358
361
  const tsConfigContent = lodash_1.default.template(reactTsConfig)({});
359
362
  writeFileIfChanged(tsConfigPath, tsConfigContent);
360
- const tsupConfigPath = path_1.default.join(target, 'tsup.config.ts');
361
- const tsupConfigContent = lodash_1.default.template(reactTsUpConfig)({});
362
- writeFileIfChanged(tsupConfigPath, tsupConfigContent);
363
+ const tsdownConfigPath = path_1.default.join(target, 'tsdown.config.ts');
364
+ const tsdownConfigContent = lodash_1.default.template(reactTsDownConfig)({});
365
+ writeFileIfChanged(tsdownConfigPath, tsdownConfigContent);
363
366
  const gitignorePath = path_1.default.join(target, '.gitignore');
364
367
  const gitignoreContent = lodash_1.default.template(gitignore)({});
365
368
  writeFileIfChanged(gitignorePath, gitignoreContent);
@@ -424,9 +427,9 @@ function createModuleProject(target, options) {
424
427
  const tsConfigPath = path_1.default.join(target, 'tsconfig.json');
425
428
  const tsConfigContent = lodash_1.default.template(moduleTsConfig)({});
426
429
  writeFileIfChanged(tsConfigPath, tsConfigContent);
427
- const tsupConfigPath = path_1.default.join(target, 'tsup.config.ts');
428
- const tsupConfigContent = lodash_1.default.template(moduleTsUpConfig)({});
429
- writeFileIfChanged(tsupConfigPath, tsupConfigContent);
430
+ const tsdownConfigPath = path_1.default.join(target, 'tsdown.config.ts');
431
+ const tsdownConfigContent = lodash_1.default.template(moduleTsDownConfig)({});
432
+ writeFileIfChanged(tsdownConfigPath, tsdownConfigContent);
430
433
  if (!skipGitignore) {
431
434
  const gitignorePath = path_1.default.join(target, '.gitignore');
432
435
  const gitignoreContent = lodash_1.default.template(gitignore)({});
@@ -914,7 +917,7 @@ function generateModuleCommand(distPath, options) {
914
917
  }]);
915
918
  packageName = packageNameAnswer.packageName;
916
919
  }
917
- // Prevent npm scaffolding (package.json, tsup.config.ts, etc.) from being written into
920
+ // Prevent npm scaffolding (package.json, tsdown.config.ts, etc.) from being written into
918
921
  // the Flutter package itself. Force users to choose a separate output directory.
919
922
  if (resolvedDistPath === flutterPackageSrc) {
920
923
  console.error('\n❌ Output directory must not be the Flutter package root.');
@@ -954,6 +957,14 @@ function generateModuleCommand(distPath, options) {
954
957
  flutterPackageDir: flutterPackageSrc,
955
958
  command,
956
959
  });
960
+ // Copy README.md from the source Flutter package into the npm package root
961
+ const { copied } = copyReadmeToPackageRoot({
962
+ sourceRoot: flutterPackageSrc,
963
+ targetRoot: resolvedDistPath,
964
+ });
965
+ if (copied) {
966
+ console.log('📄 Copied README.md to package root');
967
+ }
957
968
  console.log('\nModule code generation completed successfully!');
958
969
  try {
959
970
  yield buildPackage(resolvedDistPath);
package/dist/generator.js CHANGED
@@ -220,6 +220,7 @@ function dartGen(_a) {
220
220
  }
221
221
  function reactGen(_a) {
222
222
  return __awaiter(this, arguments, void 0, function* ({ source, target, exclude, packageName }) {
223
+ var _b, _c;
223
224
  (0, logger_1.group)('React Code Generation');
224
225
  (0, logger_1.time)('reactGen');
225
226
  const { source: normalizedSource, target: normalizedTarget } = validatePaths(source, target);
@@ -296,7 +297,8 @@ function reactGen(_a) {
296
297
  // Always build the full index content string for downstream tooling/logging
297
298
  const newExports = (0, react_1.generateReactIndex)(blobs);
298
299
  // Build desired export map: moduleSpecifier -> Set of names
299
- const desiredExports = new Map();
300
+ const desiredValueExports = new Map();
301
+ const desiredTypeExports = new Map();
300
302
  const components = blobs.flatMap(blob => {
301
303
  const classObjects = blob.objects.filter(obj => obj instanceof declaration_1.ClassObject);
302
304
  const properties = classObjects.filter(object => object.name.endsWith('Properties'));
@@ -318,11 +320,12 @@ function reactGen(_a) {
318
320
  }
319
321
  for (const { className, fileName, relativeDir } of unique.values()) {
320
322
  const spec = `./${relativeDir ? `${relativeDir}/` : ''}${fileName}`;
321
- if (!desiredExports.has(spec))
322
- desiredExports.set(spec, new Set());
323
- const set = desiredExports.get(spec);
324
- set.add(className);
325
- set.add(`${className}Element`);
323
+ if (!desiredValueExports.has(spec))
324
+ desiredValueExports.set(spec, new Set());
325
+ if (!desiredTypeExports.has(spec))
326
+ desiredTypeExports.set(spec, new Set());
327
+ desiredValueExports.get(spec).add(className);
328
+ desiredTypeExports.get(spec).add(`${className}Element`);
326
329
  }
327
330
  if (!fs_1.default.existsSync(indexFilePath)) {
328
331
  // No index.ts -> generate fresh file from template
@@ -344,24 +347,44 @@ function reactGen(_a) {
344
347
  : undefined;
345
348
  if (!moduleSpecifier)
346
349
  continue;
347
- const desired = desiredExports.get(moduleSpecifier);
348
- if (!desired)
350
+ const desiredValues = desiredValueExports.get(moduleSpecifier);
351
+ const desiredTypes = desiredTypeExports.get(moduleSpecifier);
352
+ if (!desiredValues && !desiredTypes)
349
353
  continue;
354
+ const declIsTypeOnly = Boolean(stmt.isTypeOnly);
350
355
  for (const el of stmt.exportClause.elements) {
351
356
  const name = el.name.getText(sourceFile);
352
- if (desired.has(name))
353
- desired.delete(name);
357
+ const specIsTypeOnly = Boolean(el.isTypeOnly);
358
+ const isTypeOnly = declIsTypeOnly || specIsTypeOnly;
359
+ if (isTypeOnly) {
360
+ if (desiredTypes === null || desiredTypes === void 0 ? void 0 : desiredTypes.has(name))
361
+ desiredTypes.delete(name);
362
+ }
363
+ else {
364
+ if (desiredValues === null || desiredValues === void 0 ? void 0 : desiredValues.has(name))
365
+ desiredValues.delete(name);
366
+ }
354
367
  }
355
368
  }
356
369
  }
357
370
  // Prepare new export lines for any remaining names
358
371
  const lines = [];
359
- for (const [spec, names] of desiredExports) {
360
- const missing = Array.from(names);
361
- if (missing.length === 0)
372
+ const specs = new Set([
373
+ ...desiredValueExports.keys(),
374
+ ...desiredTypeExports.keys()
375
+ ]);
376
+ for (const spec of specs) {
377
+ const missingValues = Array.from((_b = desiredValueExports.get(spec)) !== null && _b !== void 0 ? _b : []);
378
+ const missingTypes = Array.from((_c = desiredTypeExports.get(spec)) !== null && _c !== void 0 ? _c : []);
379
+ if (missingValues.length === 0 && missingTypes.length === 0)
362
380
  continue;
363
381
  const specEscaped = spec.replace(/\\/g, '/');
364
- lines.push(`export { ${missing.join(', ')} } from "${specEscaped}";`);
382
+ if (missingValues.length > 0) {
383
+ lines.push(`export { ${missingValues.join(', ')} } from "${specEscaped}";`);
384
+ }
385
+ if (missingTypes.length > 0) {
386
+ lines.push(`export type { ${missingTypes.join(', ')} } from "${specEscaped}";`);
387
+ }
365
388
  }
366
389
  if (lines.length > 0) {
367
390
  const appended = (existing.endsWith('\n') ? '' : '\n') + lines.join('\n') + '\n';
@@ -420,7 +443,7 @@ function reactGen(_a) {
420
443
  (0, logger_1.debug)(`[react] Aggregated types - consts: ${constNames.join(', ') || '(none)'}; typeAliases: ${aliasNames.join(', ') || '(none)'}; enums: ${enumNames.join(', ') || '(none)'}\n`);
421
444
  (0, logger_1.debug)(`[react] src/types.ts preview:\n` + typesContent.split('\n').slice(0, 20).join('\n'));
422
445
  }
423
- catch (_b) { }
446
+ catch (_d) { }
424
447
  }
425
448
  // Only re-export from index.ts when there are actual declarations to surface.
426
449
  if (hasAny) {
@@ -438,7 +461,7 @@ function reactGen(_a) {
438
461
  }
439
462
  }
440
463
  }
441
- catch (_c) { }
464
+ catch (_e) { }
442
465
  }
443
466
  }
444
467
  catch (e) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openwebf/webf",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
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 };