agentic-skill-mill 1.0.3
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 +135 -0
- package/dist/cache/cache-manager.d.ts +27 -0
- package/dist/cache/cache-manager.d.ts.map +1 -0
- package/dist/cache/cache-manager.js +139 -0
- package/dist/cache/cache-manager.js.map +1 -0
- package/dist/cli/commands/check-exports.d.ts +6 -0
- package/dist/cli/commands/check-exports.d.ts.map +1 -0
- package/dist/cli/commands/check-exports.js +7 -0
- package/dist/cli/commands/check-exports.js.map +1 -0
- package/dist/cli/commands/list-project.d.ts +6 -0
- package/dist/cli/commands/list-project.d.ts.map +1 -0
- package/dist/cli/commands/list-project.js +7 -0
- package/dist/cli/commands/list-project.js.map +1 -0
- package/dist/cli/commands/scaffold.d.ts +6 -0
- package/dist/cli/commands/scaffold.d.ts.map +1 -0
- package/dist/cli/commands/scaffold.js +11 -0
- package/dist/cli/commands/scaffold.js.map +1 -0
- package/dist/cli/commands/validate-manifest.d.ts +6 -0
- package/dist/cli/commands/validate-manifest.d.ts.map +1 -0
- package/dist/cli/commands/validate-manifest.js +7 -0
- package/dist/cli/commands/validate-manifest.js.map +1 -0
- package/dist/cli/commands/validate-skill.d.ts +6 -0
- package/dist/cli/commands/validate-skill.d.ts.map +1 -0
- package/dist/cli/commands/validate-skill.js +8 -0
- package/dist/cli/commands/validate-skill.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +268 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/output-formatter.d.ts +6 -0
- package/dist/cli/output-formatter.d.ts.map +1 -0
- package/dist/cli/output-formatter.js +19 -0
- package/dist/cli/output-formatter.js.map +1 -0
- package/dist/core/check-exports.d.ts +21 -0
- package/dist/core/check-exports.d.ts.map +1 -0
- package/dist/core/check-exports.js +67 -0
- package/dist/core/check-exports.js.map +1 -0
- package/dist/core/list-project.d.ts +31 -0
- package/dist/core/list-project.d.ts.map +1 -0
- package/dist/core/list-project.js +118 -0
- package/dist/core/list-project.js.map +1 -0
- package/dist/core/scaffold.d.ts +24 -0
- package/dist/core/scaffold.d.ts.map +1 -0
- package/dist/core/scaffold.js +168 -0
- package/dist/core/scaffold.js.map +1 -0
- package/dist/core/validate-manifest.d.ts +21 -0
- package/dist/core/validate-manifest.d.ts.map +1 -0
- package/dist/core/validate-manifest.js +151 -0
- package/dist/core/validate-manifest.js.map +1 -0
- package/dist/core/validate-skill.d.ts +24 -0
- package/dist/core/validate-skill.d.ts.map +1 -0
- package/dist/core/validate-skill.js +149 -0
- package/dist/core/validate-skill.js.map +1 -0
- package/dist/errors/types.d.ts +19 -0
- package/dist/errors/types.d.ts.map +1 -0
- package/dist/errors/types.js +40 -0
- package/dist/errors/types.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/package.json +56 -0
- package/skill/build/compile.mjs +385 -0
- package/skill/build/manifest.json +29 -0
- package/skill/fragments/meta/architecture-overview.md +37 -0
- package/skill/fragments/meta/cli-command-pattern.md +83 -0
- package/skill/fragments/meta/core-module-pattern.md +38 -0
- package/skill/fragments/meta/fragment-composition.md +40 -0
- package/skill/fragments/meta/rename-workflow.md +51 -0
- package/skill/fragments/meta/research-to-code.md +44 -0
- package/skill/skills/agentic-skill-mill/agentic-skill-mill.md +133 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Core domain exports
|
|
2
|
+
export { validateManifest } from './core/validate-manifest.js';
|
|
3
|
+
export { validateSkill } from './core/validate-skill.js';
|
|
4
|
+
export { listProject } from './core/list-project.js';
|
|
5
|
+
export { scaffold } from './core/scaffold.js';
|
|
6
|
+
export { checkExports } from './core/check-exports.js';
|
|
7
|
+
// Cache exports
|
|
8
|
+
export { CacheManager } from './cache/cache-manager.js';
|
|
9
|
+
// Error exports
|
|
10
|
+
export { AppError, NotFoundError, CommandError, CacheError, ConfigError, } from './errors/types.js';
|
|
11
|
+
// CLI exports
|
|
12
|
+
export { OutputFormatter } from './cli/output-formatter.js';
|
|
13
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sBAAsB;AACtB,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAGzD,OAAO,EAAE,WAAW,EAAE,MAAM,wBAAwB,CAAC;AAGrD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAG9C,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAGvD,gBAAgB;AAChB,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAGxD,gBAAgB;AAChB,OAAO,EACL,QAAQ,EACR,aAAa,EACb,YAAY,EACZ,UAAU,EACV,WAAW,GACZ,MAAM,mBAAmB,CAAC;AAE3B,cAAc;AACd,OAAO,EAAE,eAAe,EAAE,MAAM,2BAA2B,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "agentic-skill-mill",
|
|
3
|
+
"version": "1.0.3",
|
|
4
|
+
"description": "Forge and refine agent skill projects -- fragment-composed skills compiled to 7 IDE targets with a companion CLI",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skillmill": "dist/cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"exports": {
|
|
12
|
+
".": {
|
|
13
|
+
"import": "./dist/index.js",
|
|
14
|
+
"types": "./dist/index.d.ts"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"dist",
|
|
19
|
+
"skill",
|
|
20
|
+
"README.md"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc -p tsconfig.build.json",
|
|
24
|
+
"test": "vitest run",
|
|
25
|
+
"test:watch": "vitest",
|
|
26
|
+
"test:coverage": "vitest run --coverage",
|
|
27
|
+
"lint": "eslint src",
|
|
28
|
+
"typecheck": "tsc --noEmit",
|
|
29
|
+
"setup": "bash install.sh",
|
|
30
|
+
"setup:all": "bash install.sh --all",
|
|
31
|
+
"install-skills": "bash install.sh --skills-only",
|
|
32
|
+
"uninstall-skills": "bash install.sh --uninstall",
|
|
33
|
+
"compile": "node skill/build/compile.mjs",
|
|
34
|
+
"compile:validate": "node skill/build/compile.mjs --validate",
|
|
35
|
+
"compile:watch": "node skill/build/compile.mjs --watch"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"chalk": "^5.3.0",
|
|
39
|
+
"commander": "^11.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/node": "^20.10.0",
|
|
43
|
+
"@typescript-eslint/eslint-plugin": "^8.58.0",
|
|
44
|
+
"@typescript-eslint/parser": "^8.58.0",
|
|
45
|
+
"@vitest/coverage-v8": "^4.1.0",
|
|
46
|
+
"eslint": "^8.57.0",
|
|
47
|
+
"typescript": "^5.3.0",
|
|
48
|
+
"vitest": "^4.1.0"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18.0.0"
|
|
52
|
+
},
|
|
53
|
+
"publishConfig": {
|
|
54
|
+
"access": "public"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,385 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill compiler: resolves {{include:...}} markers, compiles to IDE-specific
|
|
5
|
+
* targets, and validates the output.
|
|
6
|
+
*
|
|
7
|
+
* This compiler is the core of the fragment-composition architecture.
|
|
8
|
+
* It is intentionally a single self-contained file with no runtime
|
|
9
|
+
* dependencies beyond Node.js builtins.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
* node skill/build/compile.mjs # compile all
|
|
13
|
+
* node skill/build/compile.mjs --targets claude,cursor # specific targets
|
|
14
|
+
* node skill/build/compile.mjs --validate # validate only
|
|
15
|
+
* node skill/build/compile.mjs --watch # recompile on change
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { readFileSync, writeFileSync, mkdirSync, rmSync, readdirSync, statSync, existsSync, watch as fsWatch } from 'node:fs';
|
|
19
|
+
import { join, dirname, resolve, relative } from 'node:path';
|
|
20
|
+
import { fileURLToPath } from 'node:url';
|
|
21
|
+
|
|
22
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
23
|
+
const __dirname = dirname(__filename);
|
|
24
|
+
const SKILL_ROOT = resolve(__dirname, '..');
|
|
25
|
+
const PROJECT_ROOT = resolve(SKILL_ROOT, '..');
|
|
26
|
+
const FRAGMENTS_DIR = join(SKILL_ROOT, 'fragments');
|
|
27
|
+
const COMPILED_DIR = join(PROJECT_ROOT, 'compiled');
|
|
28
|
+
const MANIFEST_PATH = join(__dirname, 'manifest.json');
|
|
29
|
+
|
|
30
|
+
const MANAGED_BY = 'managed_by: agentic-skill-mill';
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// CLI
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
const validateOnly = args.includes('--validate');
|
|
38
|
+
const watchMode = args.includes('--watch');
|
|
39
|
+
const targetFlag = args.find(a => a.startsWith('--targets=')) || args[args.indexOf('--targets') + 1];
|
|
40
|
+
const requestedTargets = targetFlag ? targetFlag.replace('--targets=', '').split(',').map(t => t.trim()) : null;
|
|
41
|
+
|
|
42
|
+
// ---------------------------------------------------------------------------
|
|
43
|
+
// Load manifest
|
|
44
|
+
// ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
const manifest = JSON.parse(readFileSync(MANIFEST_PATH, 'utf-8'));
|
|
47
|
+
const targets = requestedTargets || manifest.targets;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Step 1: Resolve includes
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
function resolveIncludes(content, sourceFile) {
|
|
54
|
+
const includePattern = /\{\{include:([^}]+)\}\}/g;
|
|
55
|
+
const errors = [];
|
|
56
|
+
|
|
57
|
+
const resolved = content.replace(includePattern, (match, fragmentPath) => {
|
|
58
|
+
const fullPath = join(FRAGMENTS_DIR, fragmentPath.trim());
|
|
59
|
+
if (!existsSync(fullPath)) {
|
|
60
|
+
errors.push(`Missing fragment: ${fragmentPath} (referenced in ${sourceFile})`);
|
|
61
|
+
return match;
|
|
62
|
+
}
|
|
63
|
+
const fragmentContent = readFileSync(fullPath, 'utf-8').trimEnd();
|
|
64
|
+
|
|
65
|
+
if (/\{\{include:[^}]+\}\}/.test(fragmentContent)) {
|
|
66
|
+
errors.push(`Nested include in fragment: ${fragmentPath} (referenced in ${sourceFile}). Only one level of includes is supported.`);
|
|
67
|
+
return match;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return fragmentContent;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return { resolved, errors };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// Step 2: Frontmatter transforms
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
function extractFrontmatter(content) {
|
|
81
|
+
const match = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
|
|
82
|
+
if (!match) return { frontmatter: {}, body: content };
|
|
83
|
+
|
|
84
|
+
const fm = {};
|
|
85
|
+
for (const line of match[1].split('\n')) {
|
|
86
|
+
const idx = line.indexOf(':');
|
|
87
|
+
if (idx === -1) continue;
|
|
88
|
+
const key = line.slice(0, idx).trim();
|
|
89
|
+
let val = line.slice(idx + 1).trim();
|
|
90
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
91
|
+
val = val.slice(1, -1);
|
|
92
|
+
}
|
|
93
|
+
fm[key] = val;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return { frontmatter: fm, body: match[2] };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function buildFrontmatter(fields) {
|
|
100
|
+
const lines = ['---'];
|
|
101
|
+
lines.push(MANAGED_BY);
|
|
102
|
+
for (const [k, v] of Object.entries(fields)) {
|
|
103
|
+
if (v !== null && typeof v === 'object' && !Array.isArray(v)) {
|
|
104
|
+
lines.push(`${k}:`);
|
|
105
|
+
for (const [nk, nv] of Object.entries(v)) {
|
|
106
|
+
lines.push(` ${nk}: ${nv}`);
|
|
107
|
+
}
|
|
108
|
+
} else if (typeof v === 'string' && (v.includes(':') || v.includes('"') || v.includes("'"))) {
|
|
109
|
+
lines.push(`${k}: "${v.replace(/"/g, '\\"')}"`);
|
|
110
|
+
} else {
|
|
111
|
+
lines.push(`${k}: ${v}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
lines.push('---');
|
|
115
|
+
return lines.join('\n');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function compileTarget(targetName, skillName, resolvedContent) {
|
|
119
|
+
const { frontmatter: fm, body } = extractFrontmatter(resolvedContent);
|
|
120
|
+
const desc = fm.description || fm.name || skillName;
|
|
121
|
+
|
|
122
|
+
function injectMarker(content) {
|
|
123
|
+
return content.replace(/^---\n/, `---\n${MANAGED_BY}\n`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
switch (targetName) {
|
|
127
|
+
case 'claude':
|
|
128
|
+
return injectMarker(resolvedContent);
|
|
129
|
+
|
|
130
|
+
case 'cursor-rules':
|
|
131
|
+
return buildFrontmatter({ description: `"${desc}"`, alwaysApply: 'false' }) + '\n' + body;
|
|
132
|
+
|
|
133
|
+
case 'cursor-skills':
|
|
134
|
+
return injectMarker(resolvedContent);
|
|
135
|
+
|
|
136
|
+
case 'windsurf-rules':
|
|
137
|
+
return buildFrontmatter({ trigger: 'manual', description: `"${desc}"` }) + '\n' + body;
|
|
138
|
+
|
|
139
|
+
case 'windsurf-skills':
|
|
140
|
+
return injectMarker(resolvedContent);
|
|
141
|
+
|
|
142
|
+
case 'opencode':
|
|
143
|
+
return buildFrontmatter({
|
|
144
|
+
mode: 'subagent',
|
|
145
|
+
description: `"${desc}"`,
|
|
146
|
+
permission: { bash: 'allow', edit: 'allow', webfetch: 'allow' }
|
|
147
|
+
}) + '\n' + body;
|
|
148
|
+
|
|
149
|
+
case 'codex':
|
|
150
|
+
return injectMarker(resolvedContent);
|
|
151
|
+
|
|
152
|
+
default:
|
|
153
|
+
throw new Error(`Unknown target: ${targetName}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getOutputPath(targetName, skillName) {
|
|
158
|
+
switch (targetName) {
|
|
159
|
+
case 'claude':
|
|
160
|
+
return join(COMPILED_DIR, 'claude', skillName, 'SKILL.md');
|
|
161
|
+
case 'cursor-rules':
|
|
162
|
+
return join(COMPILED_DIR, 'cursor', 'rules', `${skillName}.mdc`);
|
|
163
|
+
case 'cursor-skills':
|
|
164
|
+
return join(COMPILED_DIR, 'cursor', 'skills', skillName, 'SKILL.md');
|
|
165
|
+
case 'windsurf-rules':
|
|
166
|
+
return join(COMPILED_DIR, 'windsurf', 'rules', `${skillName}.md`);
|
|
167
|
+
case 'windsurf-skills':
|
|
168
|
+
return join(COMPILED_DIR, 'windsurf', 'skills', skillName, 'SKILL.md');
|
|
169
|
+
case 'opencode':
|
|
170
|
+
return join(COMPILED_DIR, 'opencode', `${skillName}.md`);
|
|
171
|
+
case 'codex':
|
|
172
|
+
return join(COMPILED_DIR, 'codex', skillName, 'SKILL.md');
|
|
173
|
+
default:
|
|
174
|
+
throw new Error(`Unknown target: ${targetName}`);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Step 3: Validation
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
function validateNaming(skillName) {
|
|
183
|
+
const errors = [];
|
|
184
|
+
if (skillName !== skillName.toLowerCase()) {
|
|
185
|
+
errors.push(`Skill name not lowercase: ${skillName}`);
|
|
186
|
+
}
|
|
187
|
+
if (/\s/.test(skillName)) {
|
|
188
|
+
errors.push(`Skill name contains spaces: ${skillName}`);
|
|
189
|
+
}
|
|
190
|
+
for (const suffix of manifest.naming_rules.forbidden_suffixes) {
|
|
191
|
+
if (skillName.endsWith(`-${suffix}`) || skillName === suffix) {
|
|
192
|
+
errors.push(`Skill name uses forbidden suffix '${suffix}': ${skillName}`);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return errors;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function validateResolved(content, skillName) {
|
|
199
|
+
const errors = [];
|
|
200
|
+
const unresolvedPattern = /\{\{include:[^}]+\}\}/g;
|
|
201
|
+
const matches = content.match(unresolvedPattern);
|
|
202
|
+
if (matches) {
|
|
203
|
+
for (const m of matches) {
|
|
204
|
+
errors.push(`Unresolved include in ${skillName}: ${m}`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return errors;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function validateFrontmatter(content, skillName) {
|
|
211
|
+
const errors = [];
|
|
212
|
+
if (!content.startsWith('---\n')) {
|
|
213
|
+
errors.push(`Missing frontmatter in ${skillName}`);
|
|
214
|
+
}
|
|
215
|
+
return errors;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
// Main
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
function main() {
|
|
223
|
+
const allErrors = [];
|
|
224
|
+
const stats = { skills: 0, targets: 0, fragments: 0 };
|
|
225
|
+
|
|
226
|
+
function countFiles(dir) {
|
|
227
|
+
let count = 0;
|
|
228
|
+
if (!existsSync(dir)) return 0;
|
|
229
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
230
|
+
if (entry.isDirectory()) count += countFiles(join(dir, entry.name));
|
|
231
|
+
else if (entry.name.endsWith('.md')) count++;
|
|
232
|
+
}
|
|
233
|
+
return count;
|
|
234
|
+
}
|
|
235
|
+
stats.fragments = countFiles(FRAGMENTS_DIR);
|
|
236
|
+
|
|
237
|
+
if (!validateOnly) {
|
|
238
|
+
if (existsSync(COMPILED_DIR)) {
|
|
239
|
+
rmSync(COMPILED_DIR, { recursive: true });
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const [skillName, skillDef] of Object.entries(manifest.skills)) {
|
|
244
|
+
stats.skills++;
|
|
245
|
+
|
|
246
|
+
allErrors.push(...validateNaming(skillName));
|
|
247
|
+
|
|
248
|
+
const sourcePath = join(SKILL_ROOT, skillDef.source);
|
|
249
|
+
if (!existsSync(sourcePath)) {
|
|
250
|
+
allErrors.push(`Missing source: ${skillDef.source} (skill: ${skillName})`);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const source = readFileSync(sourcePath, 'utf-8');
|
|
254
|
+
|
|
255
|
+
const { resolved, errors: includeErrors } = resolveIncludes(source, skillDef.source);
|
|
256
|
+
allErrors.push(...includeErrors);
|
|
257
|
+
|
|
258
|
+
allErrors.push(...validateResolved(resolved, skillName));
|
|
259
|
+
allErrors.push(...validateFrontmatter(resolved, skillName));
|
|
260
|
+
|
|
261
|
+
if (validateOnly) continue;
|
|
262
|
+
|
|
263
|
+
for (const target of targets) {
|
|
264
|
+
stats.targets++;
|
|
265
|
+
const compiled = compileTarget(target, skillName, resolved);
|
|
266
|
+
const outPath = getOutputPath(target, skillName);
|
|
267
|
+
|
|
268
|
+
mkdirSync(dirname(outPath), { recursive: true });
|
|
269
|
+
writeFileSync(outPath, compiled);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Cross-validate manifest fragment declarations against actual includes
|
|
274
|
+
for (const [skillName, skillDef] of Object.entries(manifest.skills)) {
|
|
275
|
+
const sourcePath = join(SKILL_ROOT, skillDef.source);
|
|
276
|
+
if (!existsSync(sourcePath)) continue;
|
|
277
|
+
const source = readFileSync(sourcePath, 'utf-8');
|
|
278
|
+
const includePattern = /\{\{include:([^}]+)\}\}/g;
|
|
279
|
+
const actualIncludes = new Set();
|
|
280
|
+
let match;
|
|
281
|
+
while ((match = includePattern.exec(source)) !== null) {
|
|
282
|
+
actualIncludes.add(match[1].trim());
|
|
283
|
+
}
|
|
284
|
+
const declared = new Set(skillDef.fragments);
|
|
285
|
+
|
|
286
|
+
for (const inc of actualIncludes) {
|
|
287
|
+
if (!declared.has(inc)) {
|
|
288
|
+
allErrors.push(`Undeclared fragment in ${skillName}: ${inc} (found in source but not in manifest.fragments)`);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
for (const dec of declared) {
|
|
292
|
+
if (!actualIncludes.has(dec)) {
|
|
293
|
+
allErrors.push(`Unused declared fragment in ${skillName}: ${dec} (in manifest.fragments but not in source)`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// Staleness check (validate-only mode)
|
|
299
|
+
if (validateOnly) {
|
|
300
|
+
for (const [skillName, skillDef] of Object.entries(manifest.skills)) {
|
|
301
|
+
const sourcePath = join(SKILL_ROOT, skillDef.source);
|
|
302
|
+
if (!existsSync(sourcePath)) continue;
|
|
303
|
+
const sourceMtime = statSync(sourcePath).mtimeMs;
|
|
304
|
+
|
|
305
|
+
let newestInput = sourceMtime;
|
|
306
|
+
for (const frag of skillDef.fragments) {
|
|
307
|
+
const fragPath = join(FRAGMENTS_DIR, frag);
|
|
308
|
+
if (existsSync(fragPath)) {
|
|
309
|
+
newestInput = Math.max(newestInput, statSync(fragPath).mtimeMs);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
newestInput = Math.max(newestInput, statSync(MANIFEST_PATH).mtimeMs);
|
|
313
|
+
|
|
314
|
+
for (const target of manifest.targets) {
|
|
315
|
+
const outPath = getOutputPath(target, skillName);
|
|
316
|
+
if (!existsSync(outPath)) {
|
|
317
|
+
allErrors.push(`Missing compiled output: ${relative(PROJECT_ROOT, outPath)} (run npm run compile)`);
|
|
318
|
+
} else {
|
|
319
|
+
const outMtime = statSync(outPath).mtimeMs;
|
|
320
|
+
if (outMtime < newestInput) {
|
|
321
|
+
allErrors.push(`Stale compiled output: ${relative(PROJECT_ROOT, outPath)} is older than its source or fragments (run npm run compile)`);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
if (allErrors.length > 0) {
|
|
329
|
+
console.error('\n==> Errors:');
|
|
330
|
+
for (const e of allErrors) {
|
|
331
|
+
console.error(` ERROR: ${e}`);
|
|
332
|
+
}
|
|
333
|
+
console.error(`\n${allErrors.length} error(s) found.`);
|
|
334
|
+
process.exit(1);
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (validateOnly) {
|
|
338
|
+
console.log(`==> Validation passed. ${stats.skills} skills, ${stats.fragments} fragments.`);
|
|
339
|
+
} else {
|
|
340
|
+
console.log(`==> Compiled ${stats.skills} skills to ${targets.length} targets (${stats.targets} files). ${stats.fragments} fragments resolved.`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
// Watch mode
|
|
346
|
+
// ---------------------------------------------------------------------------
|
|
347
|
+
|
|
348
|
+
function startWatch() {
|
|
349
|
+
console.log('==> Watch mode: compiling on change...');
|
|
350
|
+
console.log(` Watching: skill/fragments/, skill/skills/, skill/build/manifest.json`);
|
|
351
|
+
console.log(' Press Ctrl+C to stop.\n');
|
|
352
|
+
|
|
353
|
+
main();
|
|
354
|
+
|
|
355
|
+
let debounceTimer = null;
|
|
356
|
+
const recompile = (eventType, filename) => {
|
|
357
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
358
|
+
debounceTimer = setTimeout(() => {
|
|
359
|
+
console.log(`\n--- Change detected${filename ? `: ${filename}` : ''} ---`);
|
|
360
|
+
try {
|
|
361
|
+
main();
|
|
362
|
+
} catch (e) {
|
|
363
|
+
console.error(`Compile error: ${e.message}`);
|
|
364
|
+
}
|
|
365
|
+
}, 300);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
const watchDirs = [
|
|
369
|
+
FRAGMENTS_DIR,
|
|
370
|
+
join(SKILL_ROOT, 'skills'),
|
|
371
|
+
];
|
|
372
|
+
|
|
373
|
+
for (const dir of watchDirs) {
|
|
374
|
+
if (existsSync(dir)) {
|
|
375
|
+
fsWatch(dir, { recursive: true }, recompile);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
fsWatch(MANIFEST_PATH, recompile);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (watchMode) {
|
|
382
|
+
startWatch();
|
|
383
|
+
} else {
|
|
384
|
+
main();
|
|
385
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"targets": [
|
|
3
|
+
"claude",
|
|
4
|
+
"cursor-rules",
|
|
5
|
+
"cursor-skills",
|
|
6
|
+
"windsurf-rules",
|
|
7
|
+
"windsurf-skills",
|
|
8
|
+
"opencode",
|
|
9
|
+
"codex"
|
|
10
|
+
],
|
|
11
|
+
"skills": {
|
|
12
|
+
"agentic-skill-mill": {
|
|
13
|
+
"source": "skills/agentic-skill-mill/agentic-skill-mill.md",
|
|
14
|
+
"fragments": [
|
|
15
|
+
"meta/architecture-overview.md",
|
|
16
|
+
"meta/research-to-code.md",
|
|
17
|
+
"meta/core-module-pattern.md",
|
|
18
|
+
"meta/cli-command-pattern.md",
|
|
19
|
+
"meta/fragment-composition.md",
|
|
20
|
+
"meta/rename-workflow.md"
|
|
21
|
+
]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"naming_rules": {
|
|
25
|
+
"case": "lowercase",
|
|
26
|
+
"separator": "-",
|
|
27
|
+
"forbidden_suffixes": ["final", "new", "latest", "v2"]
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
The skill-system-template architecture has two halves that meet at the agent:
|
|
2
|
+
|
|
3
|
+
```
|
|
4
|
+
Skills (what to do) CLI Companion (tools to do it with)
|
|
5
|
+
skill/skills/*.md src/cli/commands/*.ts
|
|
6
|
+
skill/fragments/*.md src/core/*.ts
|
|
7
|
+
| |
|
|
8
|
+
v v
|
|
9
|
+
compiled/ (7 IDE formats) dist/ (npm link -> global CLI)
|
|
10
|
+
| |
|
|
11
|
+
+---------> Agent <----------+
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
**Skills** are step-by-step runbooks in markdown with YAML frontmatter. They tell the agent *what* to do. Skills reference the CLI by name so the agent can invoke structured commands.
|
|
15
|
+
|
|
16
|
+
**Fragments** are shared knowledge blocks included by multiple skills via include markers (`\{\{include:path\}\}`). Edit a fragment once, recompile, and every skill that includes it gets the update. Fragments have no frontmatter.
|
|
17
|
+
|
|
18
|
+
**The compiler** (`skill/build/compile.mjs`) resolves includes, applies IDE-specific frontmatter transforms, and writes to `compiled/` with one subdirectory per target. It validates naming rules, checks for unresolved includes, and cross-validates manifest declarations against actual includes in source files.
|
|
19
|
+
|
|
20
|
+
**The CLI companion** is a TypeScript package with:
|
|
21
|
+
- `src/core/` — Pure logic modules with typed interfaces
|
|
22
|
+
- `src/cli/commands/` — Thin command wrappers that delegate to core modules
|
|
23
|
+
- `src/cli/index.ts` — Commander.js entry point wiring all commands
|
|
24
|
+
- `src/cli/output-formatter.ts` — JSON, table, and key-value formatters
|
|
25
|
+
- `src/errors/types.ts` — Typed error hierarchy (AppError, NotFoundError, etc.)
|
|
26
|
+
- `src/cache/cache-manager.ts` — Two-tier cache (memory + disk) with TTL
|
|
27
|
+
|
|
28
|
+
**The installer** (`install.sh`) builds the CLI, compiles skills, and copies compiled outputs to IDE-specific directories (~/.claude/skills, ~/.cursor/rules, etc.) with marker-based stale file cleanup. The installer uses `set -e` for fail-fast behavior. Any function that uses an early-exit guard (`[[ -d ... ]] || return`, `[[ -z ... ]] && return`) **must** use `return 0`, never bare `return`. Bare `return` inherits the exit code of the last command, which for a failed conditional test is 1 -- and `set -e` treats that as a script-terminating failure with no error message.
|
|
29
|
+
|
|
30
|
+
### Key files to modify when augmenting a project
|
|
31
|
+
|
|
32
|
+
| What | File(s) |
|
|
33
|
+
|------|---------|
|
|
34
|
+
| Add a CLI command | `src/core/<name>.ts`, `src/cli/commands/<name>.ts`, `src/cli/index.ts`, `src/index.ts` |
|
|
35
|
+
| Add a fragment | `skill/fragments/<category>/<name>.md`, `skill/build/manifest.json`, skill source |
|
|
36
|
+
| Add a skill | `skill/skills/<name>/<name>.md`, `skill/build/manifest.json`, `install.sh` SKILLS array |
|
|
37
|
+
| Rename the project | See the rename workflow |
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
### Command wrapper pattern
|
|
2
|
+
|
|
3
|
+
Each CLI command lives in `src/cli/commands/<name>.ts` as a thin wrapper:
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// src/cli/commands/<name>.ts
|
|
7
|
+
import { doThing, type ThingOptions, type ThingResult } from '../../core/<name>.js';
|
|
8
|
+
|
|
9
|
+
export interface ThingCommandOptions extends ThingOptions {
|
|
10
|
+
json: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export async function thingCommand(
|
|
14
|
+
options: ThingCommandOptions,
|
|
15
|
+
): Promise<ThingResult> {
|
|
16
|
+
return doThing({
|
|
17
|
+
param: options.param,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
**Rules for command wrappers:**
|
|
23
|
+
- Import from core module using `.js` extension (ESM resolution)
|
|
24
|
+
- Extend the core options interface with `json: boolean`
|
|
25
|
+
- Delegate immediately to the core function -- no business logic in the wrapper
|
|
26
|
+
- Return the core result type -- formatting happens in `index.ts`
|
|
27
|
+
|
|
28
|
+
### Wiring into the CLI entry point
|
|
29
|
+
|
|
30
|
+
Add the command in `src/cli/index.ts`:
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { thingCommand } from './commands/<name>.js';
|
|
34
|
+
|
|
35
|
+
program
|
|
36
|
+
.command('<name>')
|
|
37
|
+
.description('One-line description of what this does')
|
|
38
|
+
.argument('<required>', 'Description of required positional arg') // if needed
|
|
39
|
+
.requiredOption('--param <value>', 'Required option description') // if needed
|
|
40
|
+
.option('--json', 'Output as JSON', false)
|
|
41
|
+
.option('--optional <value>', 'Optional flag', 'default')
|
|
42
|
+
.action(async (positionalArg, options) => {
|
|
43
|
+
try {
|
|
44
|
+
const result = await thingCommand({
|
|
45
|
+
param: positionalArg,
|
|
46
|
+
json: options.json,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
if (options.json) {
|
|
50
|
+
console.log(JSON.stringify(result, null, 2));
|
|
51
|
+
} else {
|
|
52
|
+
// Human-readable output using chalk
|
|
53
|
+
console.log();
|
|
54
|
+
console.log(chalk.bold('Title'));
|
|
55
|
+
for (const item of result.data) {
|
|
56
|
+
console.log(` ${item}`);
|
|
57
|
+
}
|
|
58
|
+
console.log();
|
|
59
|
+
}
|
|
60
|
+
} catch (error) {
|
|
61
|
+
handleError(error);
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Rules for CLI wiring:**
|
|
67
|
+
- Every command supports `--json` for structured agent consumption
|
|
68
|
+
- Human-readable output uses chalk for color and formatting
|
|
69
|
+
- Errors funnel through the shared `handleError()` function
|
|
70
|
+
- Use `.argument()` for required positional args, `.requiredOption()` for mandatory flags
|
|
71
|
+
- Parse string options to their real types (parseInt, parseFloat) in the action handler
|
|
72
|
+
- Comma-separated list options get `.split(',').map(s => s.trim())` in the action handler
|
|
73
|
+
|
|
74
|
+
### Updating exports
|
|
75
|
+
|
|
76
|
+
After adding a new core module, export its public API from `src/index.ts`:
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
export { doThing } from './core/<name>.js';
|
|
80
|
+
export type { ThingOptions, ThingResult } from './core/<name>.js';
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
This allows the package to be consumed as a library, not just a CLI.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
Every CLI command starts as a **core module** in `src/core/`. Core modules contain pure domain logic with no CLI concerns (no chalk, no console.log, no process.exit). This separation lets the logic be consumed as a library, tested independently, and composed by multiple commands.
|
|
2
|
+
|
|
3
|
+
### Structure of a core module
|
|
4
|
+
|
|
5
|
+
```typescript
|
|
6
|
+
// src/core/<name>.ts
|
|
7
|
+
|
|
8
|
+
/** Input options -- everything the caller needs to provide */
|
|
9
|
+
export interface <Name>Options {
|
|
10
|
+
requiredParam: string;
|
|
11
|
+
optionalParam?: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Output result -- everything the caller gets back */
|
|
15
|
+
export interface <Name>Result {
|
|
16
|
+
data: SomeType[];
|
|
17
|
+
warnings: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Pure function: options in, result out. No side effects on stdout. */
|
|
21
|
+
export function <name>(options: <Name>Options): <Name>Result {
|
|
22
|
+
return { data: [], warnings: [] };
|
|
23
|
+
}
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Rules
|
|
27
|
+
|
|
28
|
+
- **Export typed interfaces** for both input and output so consumers get autocomplete and type safety
|
|
29
|
+
- **Return structured data**, never print to stdout -- the CLI wrapper decides how to format
|
|
30
|
+
- **Include a `warnings` array** in results when the function can detect non-fatal issues
|
|
31
|
+
- **Use `async` only when needed** (file I/O, network) -- synchronous logic stays synchronous
|
|
32
|
+
- **Throw typed errors** from `src/errors/types.ts` for unrecoverable failures
|
|
33
|
+
- **Keep modules focused** -- one concept per file. A pace calculator doesn't also validate outlines
|
|
34
|
+
- **Accept paths and options, not raw CLI strings** -- parsing belongs in the command wrapper
|
|
35
|
+
|
|
36
|
+
### Shared parsers
|
|
37
|
+
|
|
38
|
+
When two or more commands need to parse the same input format, extract a shared parser module. The parser returns a structured type that both consumers work with.
|