@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/README.md +9 -1
- package/bin/webf.js +9 -1
- package/dist/agents.js +245 -0
- package/dist/analyzer.js +183 -121
- package/dist/commands.js +20 -9
- package/dist/generator.js +39 -16
- package/package.json +2 -1
- package/src/agents.ts +267 -0
- package/src/analyzer.ts +186 -114
- package/src/commands.ts +22 -12
- package/src/generator.ts +32 -12
- package/templates/module.package.json.tpl +17 -6
- package/templates/{module.tsup.config.ts.tpl → module.tsdown.config.ts.tpl} +2 -7
- package/templates/react.component.tsx.tpl +18 -2
- package/templates/react.package.json.tpl +16 -4
- package/templates/{react.tsup.config.ts.tpl → react.tsdown.config.ts.tpl} +2 -4
- package/test/agents-init.test.ts +80 -0
- package/test/analyzer.test.ts +45 -1
- package/test/commands.test.ts +4 -4
- package/test/generator.test.ts +37 -0
- package/test/react.test.ts +5 -0
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
|
|
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
|
|
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
|
|
361
|
-
const
|
|
362
|
-
writeFileIfChanged(
|
|
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
|
|
428
|
-
const
|
|
429
|
-
writeFileIfChanged(
|
|
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,
|
|
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
|
|
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 (!
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
348
|
-
|
|
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
|
-
|
|
353
|
-
|
|
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
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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.
|
|
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 };
|