skiller 0.5.6 → 0.6.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/dist/cli/index.js +0 -0
- package/dist/core/ConfigLoader.js +5 -0
- package/dist/core/SkillsProcessor.js +101 -1
- package/dist/core/UnifiedConfigLoader.js +21 -3
- package/dist/lib.js +4 -2
- package/package.json +5 -3
package/dist/cli/index.js
CHANGED
|
File without changes
|
|
@@ -78,6 +78,8 @@ const skillerConfigSchema = zod_1.z.object({
|
|
|
78
78
|
skills: zod_1.z
|
|
79
79
|
.object({
|
|
80
80
|
enabled: zod_1.z.boolean().optional(),
|
|
81
|
+
generate_from_rules: zod_1.z.boolean().optional(),
|
|
82
|
+
prune: zod_1.z.boolean().optional(),
|
|
81
83
|
})
|
|
82
84
|
.optional(),
|
|
83
85
|
rules: zod_1.z
|
|
@@ -234,6 +236,9 @@ async function loadConfig(options) {
|
|
|
234
236
|
if (typeof rawSkillsSection.generate_from_rules === 'boolean') {
|
|
235
237
|
skillsConfig.generate_from_rules = rawSkillsSection.generate_from_rules;
|
|
236
238
|
}
|
|
239
|
+
if (typeof rawSkillsSection.prune === 'boolean') {
|
|
240
|
+
skillsConfig.prune = rawSkillsSection.prune;
|
|
241
|
+
}
|
|
237
242
|
const rawRulesSection = raw.rules && typeof raw.rules === 'object' && !Array.isArray(raw.rules)
|
|
238
243
|
? raw.rules
|
|
239
244
|
: {};
|
|
@@ -41,6 +41,7 @@ exports.propagateSkillsForSkillz = propagateSkillsForSkillz;
|
|
|
41
41
|
exports.buildSkillzMcpConfig = buildSkillzMcpConfig;
|
|
42
42
|
const path = __importStar(require("path"));
|
|
43
43
|
const fs = __importStar(require("fs/promises"));
|
|
44
|
+
const readline = __importStar(require("readline"));
|
|
44
45
|
const constants_1 = require("../constants");
|
|
45
46
|
const SkillsUtils_1 = require("./SkillsUtils");
|
|
46
47
|
const FrontmatterParser_1 = require("./FrontmatterParser");
|
|
@@ -153,11 +154,106 @@ async function findMdcFiles(dir) {
|
|
|
153
154
|
}
|
|
154
155
|
return mdcFiles;
|
|
155
156
|
}
|
|
157
|
+
/**
|
|
158
|
+
* Prompts the user via readline to confirm deletion of orphaned skills.
|
|
159
|
+
* Returns true if user confirms, false otherwise.
|
|
160
|
+
*/
|
|
161
|
+
async function promptForPrune(orphanedSkills) {
|
|
162
|
+
// If not running in a TTY (e.g., CI/CD), skip prompting
|
|
163
|
+
if (!process.stdin.isTTY) {
|
|
164
|
+
(0, constants_1.logWarn)(`Found ${orphanedSkills.length} orphaned skill(s) but not in interactive mode. Set prune = true/false in skiller.toml to handle automatically.`, false);
|
|
165
|
+
return false;
|
|
166
|
+
}
|
|
167
|
+
const rl = readline.createInterface({
|
|
168
|
+
input: process.stdin,
|
|
169
|
+
output: process.stdout,
|
|
170
|
+
});
|
|
171
|
+
return new Promise((resolve) => {
|
|
172
|
+
console.log(`\nFound ${orphanedSkills.length} orphaned skill(s) not generated from rules:`);
|
|
173
|
+
orphanedSkills.forEach((skill) => console.log(` - ${skill}`));
|
|
174
|
+
console.log('');
|
|
175
|
+
rl.question('Delete these orphaned skills? [y/N]: ', (answer) => {
|
|
176
|
+
rl.close();
|
|
177
|
+
const confirmed = answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes';
|
|
178
|
+
if (confirmed) {
|
|
179
|
+
console.log('');
|
|
180
|
+
}
|
|
181
|
+
else {
|
|
182
|
+
console.log("\nTip: Add 'prune = true' or 'prune = false' to [skills] in skiller.toml to avoid this prompt.\n");
|
|
183
|
+
}
|
|
184
|
+
resolve(confirmed);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Prunes orphaned skills (skills that exist in .claude/skills but are not generated from any .mdc file).
|
|
190
|
+
* @param skillsDir Path to the skills directory
|
|
191
|
+
* @param generatedSkillNames Set of skill names that were generated from .mdc files
|
|
192
|
+
* @param prune Prune setting: true=auto-delete, false=keep, undefined=prompt
|
|
193
|
+
* @param verbose Whether to log verbose output
|
|
194
|
+
* @param dryRun Whether to perform a dry run
|
|
195
|
+
*/
|
|
196
|
+
async function pruneOrphanedSkills(skillsDir, generatedSkillNames, prune, verbose, dryRun) {
|
|
197
|
+
// Check if skills directory exists
|
|
198
|
+
try {
|
|
199
|
+
await fs.access(skillsDir);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// No skills directory, nothing to prune
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
// Get all existing skill directories
|
|
206
|
+
const entries = await fs.readdir(skillsDir, { withFileTypes: true });
|
|
207
|
+
const existingSkillDirs = entries
|
|
208
|
+
.filter((entry) => entry.isDirectory())
|
|
209
|
+
.map((entry) => entry.name);
|
|
210
|
+
// Find orphaned skills (exist but not in generatedSkillNames)
|
|
211
|
+
const orphanedSkills = existingSkillDirs.filter((name) => !generatedSkillNames.has(name));
|
|
212
|
+
if (orphanedSkills.length === 0) {
|
|
213
|
+
(0, constants_1.logVerboseInfo)('No orphaned skills found', verbose, dryRun);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
// Determine whether to delete based on prune setting
|
|
217
|
+
let shouldDelete = false;
|
|
218
|
+
if (prune === true) {
|
|
219
|
+
// Auto-delete
|
|
220
|
+
shouldDelete = true;
|
|
221
|
+
(0, constants_1.logVerboseInfo)(`Auto-pruning ${orphanedSkills.length} orphaned skill(s) (prune = true)`, verbose, dryRun);
|
|
222
|
+
}
|
|
223
|
+
else if (prune === false) {
|
|
224
|
+
// Keep orphans
|
|
225
|
+
(0, constants_1.logVerboseInfo)(`Keeping ${orphanedSkills.length} orphaned skill(s) (prune = false)`, verbose, dryRun);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
else {
|
|
229
|
+
// prune is undefined - prompt user
|
|
230
|
+
shouldDelete = await promptForPrune(orphanedSkills);
|
|
231
|
+
if (!shouldDelete) {
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// Delete orphaned skills
|
|
236
|
+
for (const skillName of orphanedSkills) {
|
|
237
|
+
const skillPath = path.join(skillsDir, skillName);
|
|
238
|
+
if (dryRun) {
|
|
239
|
+
(0, constants_1.logVerboseInfo)(`DRY RUN: Would remove orphaned skill ${skillName}`, verbose, dryRun);
|
|
240
|
+
}
|
|
241
|
+
else {
|
|
242
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
243
|
+
(0, constants_1.logVerboseInfo)(`Removed orphaned skill ${skillName}`, verbose, dryRun);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
156
247
|
/**
|
|
157
248
|
* Generates skills from .mdc rule files with frontmatter.
|
|
158
249
|
* Creates skill files in the skills directory with @filename references to the original .mdc files.
|
|
250
|
+
* @param projectRoot Root directory of the project
|
|
251
|
+
* @param skillerDir Path to the skiller directory (.claude)
|
|
252
|
+
* @param verbose Whether to log verbose output
|
|
253
|
+
* @param dryRun Whether to perform a dry run
|
|
254
|
+
* @param prune Prune setting: true=auto-delete orphans, false=keep orphans, undefined=prompt
|
|
159
255
|
*/
|
|
160
|
-
async function generateSkillsFromRules(projectRoot, skillerDir, verbose, dryRun) {
|
|
256
|
+
async function generateSkillsFromRules(projectRoot, skillerDir, verbose, dryRun, prune) {
|
|
161
257
|
// Determine skills directory based on skillerDir
|
|
162
258
|
const skillsDir = path.join(skillerDir, 'skills');
|
|
163
259
|
// Find all .mdc files in the skiller directory
|
|
@@ -168,6 +264,7 @@ async function generateSkillsFromRules(projectRoot, skillerDir, verbose, dryRun)
|
|
|
168
264
|
}
|
|
169
265
|
let generatedCount = 0;
|
|
170
266
|
const skillsToRemove = [];
|
|
267
|
+
const generatedSkillNames = new Set();
|
|
171
268
|
for (const mdcFile of mdcFiles) {
|
|
172
269
|
// Read file content
|
|
173
270
|
const content = await fs.readFile(mdcFile, 'utf8');
|
|
@@ -249,6 +346,7 @@ ${fileReference}
|
|
|
249
346
|
}
|
|
250
347
|
}
|
|
251
348
|
generatedCount++;
|
|
349
|
+
generatedSkillNames.add(fileName);
|
|
252
350
|
}
|
|
253
351
|
if (generatedCount > 0) {
|
|
254
352
|
(0, constants_1.logVerboseInfo)(`Generated ${generatedCount} skill(s) from .mdc files`, verbose, dryRun);
|
|
@@ -277,6 +375,8 @@ ${fileReference}
|
|
|
277
375
|
}
|
|
278
376
|
}
|
|
279
377
|
}
|
|
378
|
+
// Prune orphaned skills if prune setting is configured
|
|
379
|
+
await pruneOrphanedSkills(skillsDir, generatedSkillNames, prune, verbose, dryRun);
|
|
280
380
|
}
|
|
281
381
|
/**
|
|
282
382
|
* Cleans up skills directories (.claude/skills and .skillz) when skills are disabled.
|
|
@@ -41,6 +41,19 @@ const FileSystemUtils = __importStar(require("./FileSystemUtils"));
|
|
|
41
41
|
const FileSystemUtils_1 = require("./FileSystemUtils");
|
|
42
42
|
const hash_1 = require("./hash");
|
|
43
43
|
const RuleProcessor_1 = require("./RuleProcessor");
|
|
44
|
+
/**
|
|
45
|
+
* Expand environment variables in a string.
|
|
46
|
+
* Supports ${VAR} syntax, replacing with process.env[VAR] or empty string if not found.
|
|
47
|
+
*/
|
|
48
|
+
function expandEnvVars(value) {
|
|
49
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, key) => process.env[key] || '');
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Expand environment variables in all values of a Record<string, string>.
|
|
53
|
+
*/
|
|
54
|
+
function expandEnvRecord(record) {
|
|
55
|
+
return Object.fromEntries(Object.entries(record).map(([k, v]) => [k, expandEnvVars(v)]));
|
|
56
|
+
}
|
|
44
57
|
async function loadUnifiedConfig(options) {
|
|
45
58
|
// Resolve the effective .claude directory (local or global), mirroring the main loader behavior
|
|
46
59
|
const resolvedSkillerDir = (await FileSystemUtils.findSkillerDir(options.projectRoot, true)) ||
|
|
@@ -99,6 +112,9 @@ async function loadUnifiedConfig(options) {
|
|
|
99
112
|
if (typeof skillsObj.generate_from_rules === 'boolean') {
|
|
100
113
|
skillsConfig.generate_from_rules = skillsObj.generate_from_rules;
|
|
101
114
|
}
|
|
115
|
+
if (typeof skillsObj.prune === 'boolean') {
|
|
116
|
+
skillsConfig.prune = skillsObj.prune;
|
|
117
|
+
}
|
|
102
118
|
}
|
|
103
119
|
}
|
|
104
120
|
// Parse rules configuration
|
|
@@ -227,9 +243,10 @@ async function loadUnifiedConfig(options) {
|
|
|
227
243
|
if (Array.isArray(serverDef.args)) {
|
|
228
244
|
server.args = serverDef.args.map(String);
|
|
229
245
|
}
|
|
230
|
-
// Parse env
|
|
246
|
+
// Parse env with ${VAR} expansion
|
|
231
247
|
if (serverDef.env && typeof serverDef.env === 'object') {
|
|
232
|
-
|
|
248
|
+
const rawEnv = Object.fromEntries(Object.entries(serverDef.env).filter(([, v]) => typeof v === 'string'));
|
|
249
|
+
server.env = expandEnvRecord(rawEnv);
|
|
233
250
|
}
|
|
234
251
|
// Parse URL and headers
|
|
235
252
|
if (typeof serverDef.url === 'string') {
|
|
@@ -350,7 +367,8 @@ async function loadUnifiedConfig(options) {
|
|
|
350
367
|
if (Array.isArray(def.args))
|
|
351
368
|
server.args = def.args.map(String);
|
|
352
369
|
if (def.env && typeof def.env === 'object') {
|
|
353
|
-
|
|
370
|
+
const rawEnv = Object.fromEntries(Object.entries(def.env).filter(([, v]) => typeof v === 'string'));
|
|
371
|
+
server.env = expandEnvRecord(rawEnv);
|
|
354
372
|
}
|
|
355
373
|
if (typeof def.url === 'string')
|
|
356
374
|
server.url = def.url;
|
package/dist/lib.js
CHANGED
|
@@ -107,10 +107,11 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
107
107
|
if (skillsEnabledResolved) {
|
|
108
108
|
const generateFromRules = rootConfig.skills?.generate_from_rules ?? false;
|
|
109
109
|
if (generateFromRules) {
|
|
110
|
+
const pruneConfig = rootConfig.skills?.prune;
|
|
110
111
|
for (const configEntry of hierarchicalConfigs) {
|
|
111
112
|
const nestedRoot = path.dirname(configEntry.skillerDir);
|
|
112
113
|
(0, constants_1.logVerbose)(`Generating skills from .mdc files for nested directory: ${nestedRoot}`, verbose);
|
|
113
|
-
await generateSkillsFromRules(nestedRoot, configEntry.skillerDir, verbose, dryRun);
|
|
114
|
+
await generateSkillsFromRules(nestedRoot, configEntry.skillerDir, verbose, dryRun, pruneConfig);
|
|
114
115
|
}
|
|
115
116
|
}
|
|
116
117
|
}
|
|
@@ -141,7 +142,8 @@ async function applyAllAgentConfigs(projectRoot, includedAgents, configPath, cli
|
|
|
141
142
|
const generateFromRules = singleConfig.config.skills?.generate_from_rules ?? false;
|
|
142
143
|
if (generateFromRules) {
|
|
143
144
|
(0, constants_1.logVerbose)('Generating skills from .mdc files', verbose);
|
|
144
|
-
|
|
145
|
+
const pruneConfig = singleConfig.config.skills?.prune;
|
|
146
|
+
await generateSkillsFromRules(projectRoot, singleConfig.skillerDir, verbose, dryRun, pruneConfig);
|
|
145
147
|
}
|
|
146
148
|
}
|
|
147
149
|
// Always call propagateSkills - it handles cleanup when disabled
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "skiller",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Skiller — apply the same rules to all coding agents",
|
|
5
5
|
"main": "dist/lib.js",
|
|
6
6
|
"publishConfig": {
|
|
7
7
|
"access": "public"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
+
"postinstall": "[ -n \"$CI\" ] || npx skiller@latest apply",
|
|
10
11
|
"lint": "eslint \"src/**/*.{ts,tsx}\"",
|
|
11
12
|
"lint:fix": "eslint \"src/**/*.{ts,tsx}\" --fix",
|
|
12
13
|
"format": "prettier --write \"src/**/*.{ts,tsx,json,md}\"",
|
|
@@ -15,7 +16,7 @@
|
|
|
15
16
|
"test:coverage": "jest --coverage",
|
|
16
17
|
"test:integration": "jest tests/e2e/skiller.integration.test.ts --verbose",
|
|
17
18
|
"build": "tsc",
|
|
18
|
-
"release": "pnpm build && pnpm changeset
|
|
19
|
+
"release": "pnpm build && pnpm changeset publish"
|
|
19
20
|
},
|
|
20
21
|
"repository": {
|
|
21
22
|
"type": "git",
|
|
@@ -50,7 +51,8 @@
|
|
|
50
51
|
"skiller": "dist/cli/index.js"
|
|
51
52
|
},
|
|
52
53
|
"devDependencies": {
|
|
53
|
-
"@changesets/
|
|
54
|
+
"@changesets/changelog-github": "^0.5.2",
|
|
55
|
+
"@changesets/cli": "2.29.8",
|
|
54
56
|
"@eslint/js": "^9.39.1",
|
|
55
57
|
"@types/iarna__toml": "^2.0.5",
|
|
56
58
|
"@types/jest": "^29.5.14",
|