Fix user-invokable -> user-invocable spelling across entire codebase

PR #50 fixed the output SKILL.md files but the source files, build
scripts, tests, docs, and server code still used the wrong spelling.
Claude Code expects `user-invocable` (with c) for slash command
autocomplete to work.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Paul Bakaus
2026-03-20 23:33:22 -07:00
co-authored by Claude Opus 4.6
parent d6b1a56bc5
commit df1c26ce63
46 changed files with 226 additions and 207 deletions
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "impeccable",
"description": "Design vocabulary and skills for frontend development. Includes 21 skills (20 user-invokable: /polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced frontend-design skill with curated anti-patterns.",
"description": "Design vocabulary and skills for frontend development. Includes 21 skills (20 user-invocable: /polish, /distill, /audit, /typeset, /overdrive, etc.) and an enhanced frontend-design skill with curated anti-patterns.",
"version": "1.5.1",
"author": {
"name": "Paul Bakaus",
+19
View File
@@ -0,0 +1,19 @@
## Design Context
### Users
Frontend and full-stack developers who use AI coding tools (Cursor, Claude Code, Gemini CLI, Codex CLI, etc.) and want better design output from their AI. They land on the site from GitHub, social media, or word of mouth, already aware that AI-generated UIs have quality problems. They're looking for a practical solution, not education about the problem.
### Brand Personality
Expert, opinionated, refined. Impeccable speaks with an authoritative design voice -- confident taste, editorial quality, zero hedging. It's the design director in the room who knows exactly what's wrong and how to fix it.
### Aesthetic Direction
- **Visual tone**: Editorial precision meets fluid motion. Light, high-contrast, typographically bold. The site itself must be proof of what Impeccable enables -- it should practice what it preaches.
- **Theme**: Light mode with warm paper tones, vibrant magenta/rose accent, serif display + clean sans body
- **Anti-references**: Generic AI tool marketing (dark mode, purple gradients, glassmorphism, neon accents). The site should be the antithesis of everything it critiques.
### Design Principles
1. **Practice what you preach** -- the site must pass its own anti-pattern tests with flying colors
2. **Show, don't tell** -- demonstrate design quality through execution, not just words
3. **Expert confidence** -- no hedging, no "maybe consider." Direct, opinionated, decisive.
4. **Editorial over marketing** -- feels like a design publication, not a SaaS landing page
5. **Purposeful restraint** -- every element earns its place; nothing decorative without function
+1 -1
View File
@@ -53,7 +53,7 @@ When bumping the version, update **all** of these locations to keep them in sync
## Adding New Skills
When adding a new user-invokable skill, update the command count in **all** of these locations:
When adding a new user-invocable skill, update the command count in **all** of these locations:
- `public/index.html` → meta descriptions, hero box, section lead
- `public/cheatsheet.html` → meta description, subtitle, `commandCategories`, `commandRelationships`
+9 -9
View File
@@ -9,7 +9,7 @@ This repository uses a **feature-rich source format** that transforms into provi
### Why This Approach?
Different providers have different capabilities:
- **Claude Code, OpenCode**: Full metadata — args, user-invokable, allowed-tools, license, compatibility
- **Claude Code, OpenCode**: Full metadata — args, user-invocable, allowed-tools, license, compatibility
- **Codex, Agents**: Args converted to `argument-hint` format
- **Gemini**: Minimal frontmatter, `{{arg}}` placeholders become `{{args}}`
- **Cursor, Kiro, Pi**: Basic frontmatter (name, description, license/compatibility)
@@ -34,8 +34,8 @@ Your skill instructions here...
**Frontmatter fields** (based on [Agent Skills spec](https://agentskills.io/specification)):
- `name` (required): Skill identifier (1-64 chars, lowercase/numbers/hyphens)
- `description` (required): What the skill provides (1-1024 chars)
- `user-invokable` (optional): Boolean — if `true`, the skill can be invoked as a slash command
- `args` (optional): Array of argument objects (for user-invokable skills)
- `user-invocable` (optional): Boolean — if `true`, the skill can be invoked as a slash command
- `args` (optional): Array of argument objects (for user-invocable skills)
- `name`: Argument identifier
- `description`: What it's for
- `required`: Boolean (defaults to false)
@@ -90,27 +90,27 @@ All providers output skills to `dist/{provider}/.{config}/skills/{name}/SKILL.md
### Claude Code (Full Featured)
- Output: `dist/claude-code/.claude/skills/{name}/SKILL.md`
- Frontmatter: name, description, user-invokable, args, license, compatibility, metadata, allowed-tools
- Frontmatter: name, description, user-invocable, args, license, compatibility, metadata, allowed-tools
- Preserves `{{arg}}` placeholders in body
### OpenCode (Full Featured)
- Output: `dist/opencode/.opencode/skills/{name}/SKILL.md`
- Frontmatter: name, description, user-invokable, args, license, compatibility, metadata, allowed-tools
- Frontmatter: name, description, user-invocable, args, license, compatibility, metadata, allowed-tools
- Same format as Claude Code
### Gemini CLI
- Output: `dist/gemini/.gemini/skills/{name}/SKILL.md`
- Frontmatter: name, description
- For user-invokable skills: remaining `{{arg}}` placeholders become `{{args}}`
- For user-invocable skills: remaining `{{arg}}` placeholders become `{{args}}`
### Codex CLI
- Output: `dist/codex/.codex/skills/{name}/SKILL.md`
- Frontmatter: name, description, argument-hint, license
- For user-invokable skills: `{{argname}}``$ARGNAME` (uppercase)
- For user-invocable skills: `{{argname}}``$ARGNAME` (uppercase)
### Agents (VS Code Copilot, Antigravity)
- Output: `dist/agents/.agents/skills/{name}/SKILL.md`
- Frontmatter: name, description, user-invokable, argument-hint
- Frontmatter: name, description, user-invocable, argument-hint
- Args converted to `argument-hint` format (e.g., `<target> [FORMAT=<value>]`)
### Kiro
@@ -170,7 +170,7 @@ The build system uses a modular architecture under `scripts/`:
- `transformClaudeCode()`: Full metadata with args and allowed-tools
- `transformGemini()`: Minimal frontmatter, `{{arg}}``{{args}}`
- `transformCodex()`: Args → argument-hint, `{{arg}}``$ARGNAME`
- `transformAgents()`: Args → argument-hint, user-invokable flag
- `transformAgents()`: Args → argument-hint, user-invocable flag
- `transformKiro()`: Basic frontmatter with license/compatibility/metadata
- `transformOpenCode()`: Full metadata (same as Claude Code)
- `transformPi()`: Basic frontmatter with license/compatibility/metadata
+1 -1
View File
@@ -362,7 +362,7 @@
<span class="changelog-date">March 4, 2026</span>
</div>
<ul class="changelog-items">
<li>Unified skills architecture — commands are now skills with <code>user-invokable: true</code></li>
<li>Unified skills architecture — commands are now skills with <code>user-invocable: true</code></li>
<li>Added VS Code Copilot and Google Antigravity support (<code>.agents/skills/</code>)</li>
<li>New install flow: <code>npx skills add</code> as primary, universal ZIP as fallback</li>
<li>Added universal ZIP containing all 5 provider directories</li>
+5 -5
View File
@@ -200,12 +200,12 @@ function generateApiData(buildDir, skills, patterns) {
id: path.basename(path.dirname(s.filePath)),
name: s.name,
description: s.description,
userInvokable: s.userInvokable,
userInvocable: s.userInvocable,
}));
fs.writeFileSync(path.join(apiDir, 'skills.json'), JSON.stringify(skillsData));
// commands.json (user-invokable skills only)
const commandsData = skillsData.filter(s => s.userInvokable);
// commands.json (user-invocable skills only)
const commandsData = skillsData.filter(s => s.userInvocable);
fs.writeFileSync(path.join(apiDir, 'commands.json'), JSON.stringify(commandsData));
// patterns.json
@@ -337,8 +337,8 @@ async function build() {
// Read source files (unified skills architecture)
const { skills } = readSourceFiles(ROOT_DIR);
const patterns = readPatterns(ROOT_DIR);
const userInvokableCount = skills.filter(s => s.userInvokable).length;
console.log(`📖 Read ${skills.length} skills (${userInvokableCount} user-invokable) and ${patterns.patterns.length + patterns.antipatterns.length} pattern categories\n`);
const userInvocableCount = skills.filter(s => s.userInvocable).length;
console.log(`📖 Read ${skills.length} skills (${userInvocableCount} user-invocable) and ${patterns.patterns.length + patterns.antipatterns.length} pattern categories\n`);
// Transform for each provider
transformCursor(skills, DIST_DIR, patterns);
+8 -8
View File
@@ -5,9 +5,9 @@ import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceho
* Agents Transformer (VS Code Copilot + Antigravity)
*
* All skills output to .agents/skills/{name}/SKILL.md
* Frontmatter: name, description, user-invokable (if true), argument-hint (from args)
* Frontmatter: name, description, user-invocable (if true), argument-hint (from args)
*
* @param {Array} skills - All skills (including user-invokable ones)
* @param {Array} skills - All skills (including user-invocable ones)
* @param {string} distDir - Distribution output directory
* @param {Object} patterns - Design patterns data (unused)
* @param {Object} options - Optional settings
@@ -21,7 +21,7 @@ export function transformAgents(skills, distDir, patterns = null, options = {})
ensureDir(skillsDir);
const allSkillNames = skills.map(s => s.name);
const commandNames = skills.filter(s => s.userInvokable).map(s => `${prefix}${s.name}`);
const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
@@ -32,10 +32,10 @@ export function transformAgents(skills, distDir, patterns = null, options = {})
description: skill.description,
};
if (skill.userInvokable) frontmatterObj['user-invokable'] = true;
if (skill.userInvocable) frontmatterObj['user-invocable'] = true;
// Build argument-hint from args array for user-invokable skills
if (skill.userInvokable && skill.args && skill.args.length > 0) {
// Build argument-hint from args array for user-invocable skills
if (skill.userInvocable && skill.args && skill.args.length > 0) {
const hints = skill.args.map(arg => {
return arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`;
});
@@ -62,8 +62,8 @@ export function transformAgents(skills, distDir, patterns = null, options = {})
}
}
const userInvokableCount = skills.filter(s => s.userInvokable).length;
const userInvocableCount = skills.filter(s => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ Agents${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
console.log(`✓ Agents${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
}
+6 -6
View File
@@ -7,11 +7,11 @@ import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceho
* All skills output to .claude/skills/{name}/SKILL.md
* User-invokable skills get args support in frontmatter.
*
* @param {Array} skills - All skills (including user-invokable ones)
* @param {Array} skills - All skills (including user-invocable ones)
* @param {string} distDir - Distribution output directory
* @param {Object} patterns - Design patterns data (unused, kept for interface consistency)
* @param {Object} options - Optional settings
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
* @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-')
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
*/
export function transformClaudeCode(skills, distDir, patterns = null, options = {}) {
@@ -23,7 +23,7 @@ export function transformClaudeCode(skills, distDir, patterns = null, options =
ensureDir(skillsDir);
const allSkillNames = skills.map(s => s.name);
const commandNames = skills.filter(s => s.userInvokable).map(s => `${prefix}${s.name}`);
const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
@@ -34,7 +34,7 @@ export function transformClaudeCode(skills, distDir, patterns = null, options =
description: skill.description,
};
if (skill.userInvokable) frontmatterObj['user-invokable'] = true;
if (skill.userInvocable) frontmatterObj['user-invocable'] = true;
if (skill.args && skill.args.length > 0) frontmatterObj.args = skill.args;
if (skill.license) frontmatterObj.license = skill.license;
if (skill.compatibility) frontmatterObj.compatibility = skill.compatibility;
@@ -61,8 +61,8 @@ export function transformClaudeCode(skills, distDir, patterns = null, options =
}
}
const userInvokableCount = skills.filter(s => s.userInvokable).length;
const userInvocableCount = skills.filter(s => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ Claude Code${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
console.log(`✓ Claude Code${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
}
+11 -11
View File
@@ -5,14 +5,14 @@ import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceho
* Codex Transformer (Skills Only)
*
* All skills output to .codex/skills/{name}/SKILL.md
* Frontmatter: name, description, argument-hint (from args for user-invokable)
* For user-invokable skills: {{argname}} becomes $ARGNAME in body
* Frontmatter: name, description, argument-hint (from args for user-invocable)
* For user-invocable skills: {{argname}} becomes $ARGNAME in body
*
* @param {Array} skills - All skills (including user-invokable ones)
* @param {Array} skills - All skills (including user-invocable ones)
* @param {string} distDir - Distribution output directory
* @param {Object} patterns - Design patterns data (unused)
* @param {Object} options - Optional settings
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
* @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-')
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
*/
export function transformCodex(skills, distDir, patterns = null, options = {}) {
@@ -24,7 +24,7 @@ export function transformCodex(skills, distDir, patterns = null, options = {}) {
ensureDir(skillsDir);
const allSkillNames = skills.map(s => s.name);
const commandNames = skills.filter(s => s.userInvokable).map(s => `${prefix}${s.name}`);
const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
@@ -35,8 +35,8 @@ export function transformCodex(skills, distDir, patterns = null, options = {}) {
description: skill.description,
};
// Build argument-hint from args array for user-invokable skills
if (skill.userInvokable && skill.args && skill.args.length > 0) {
// Build argument-hint from args array for user-invocable skills
if (skill.userInvocable && skill.args && skill.args.length > 0) {
const hints = skill.args.map(arg => {
return arg.required ? `<${arg.name}>` : `[${arg.name.toUpperCase()}=<value>]`;
});
@@ -48,8 +48,8 @@ export function transformCodex(skills, distDir, patterns = null, options = {}) {
let skillBody = replacePlaceholders(skill.body, 'codex', commandNames);
if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames);
// For user-invokable skills, transform remaining {{argname}} to $ARGNAME
if (skill.userInvokable) {
// For user-invocable skills, transform remaining {{argname}} to $ARGNAME
if (skill.userInvocable) {
skillBody = skillBody.replace(/\{\{([^}]+)\}\}/g, (match, argName) => {
return `$${argName.toUpperCase()}`;
});
@@ -72,8 +72,8 @@ export function transformCodex(skills, distDir, patterns = null, options = {}) {
}
}
const userInvokableCount = skills.filter(s => s.userInvokable).length;
const userInvocableCount = skills.filter(s => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ Codex${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
console.log(`✓ Codex${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
}
+5 -5
View File
@@ -7,11 +7,11 @@ import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceho
* All skills output to .cursor/skills/{name}/SKILL.md
* Frontmatter: name, description, license
*
* @param {Array} skills - All skills (including user-invokable ones)
* @param {Array} skills - All skills (including user-invocable ones)
* @param {string} distDir - Distribution output directory
* @param {Object} patterns - Design patterns data (unused)
* @param {Object} options - Optional settings
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
* @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-')
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
*/
export function transformCursor(skills, distDir, patterns = null, options = {}) {
@@ -23,7 +23,7 @@ export function transformCursor(skills, distDir, patterns = null, options = {})
ensureDir(skillsDir);
const allSkillNames = skills.map(s => s.name);
const commandNames = skills.filter(s => s.userInvokable).map(s => `${prefix}${s.name}`);
const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
@@ -55,8 +55,8 @@ export function transformCursor(skills, distDir, patterns = null, options = {})
}
}
const userInvokableCount = skills.filter(s => s.userInvokable).length;
const userInvocableCount = skills.filter(s => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ Cursor${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
console.log(`✓ Cursor${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
}
+8 -8
View File
@@ -6,13 +6,13 @@ import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceho
*
* All skills output to .gemini/skills/{name}/SKILL.md
* Frontmatter: name, description
* For user-invokable skills: {{arg}} placeholders become {{args}} in body
* For user-invocable skills: {{arg}} placeholders become {{args}} in body
*
* @param {Array} skills - All skills (including user-invokable ones)
* @param {Array} skills - All skills (including user-invocable ones)
* @param {string} distDir - Distribution output directory
* @param {Object} patterns - Design patterns data (unused)
* @param {Object} options - Optional settings
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
* @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-')
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
*/
export function transformGemini(skills, distDir, patterns = null, options = {}) {
@@ -24,7 +24,7 @@ export function transformGemini(skills, distDir, patterns = null, options = {})
ensureDir(skillsDir);
const allSkillNames = skills.map(s => s.name);
const commandNames = skills.filter(s => s.userInvokable).map(s => `${prefix}${s.name}`);
const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
@@ -37,8 +37,8 @@ export function transformGemini(skills, distDir, patterns = null, options = {})
let skillBody = replacePlaceholders(skill.body, 'gemini', commandNames);
if (prefix) skillBody = prefixSkillReferences(skillBody, prefix, allSkillNames);
// For user-invokable skills, replace remaining {{arg}} placeholders with {{args}}
if (skill.userInvokable) {
// For user-invocable skills, replace remaining {{arg}} placeholders with {{args}}
if (skill.userInvocable) {
skillBody = skillBody.replace(/\{\{[^}]+\}\}/g, '{{args}}');
}
@@ -59,8 +59,8 @@ export function transformGemini(skills, distDir, patterns = null, options = {})
}
}
const userInvokableCount = skills.filter(s => s.userInvokable).length;
const userInvocableCount = skills.filter(s => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ Gemini${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
console.log(`✓ Gemini${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
}
+4 -4
View File
@@ -7,7 +7,7 @@ import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceho
* All skills output to .kiro/skills/{name}/SKILL.md
* Frontmatter: name, description, license, compatibility, metadata
*
* @param {Array} skills - All skills (including user-invokable ones)
* @param {Array} skills - All skills (including user-invocable ones)
* @param {string} distDir - Distribution output directory
* @param {Object} patterns - Design patterns data (unused)
* @param {Object} options - Optional settings
@@ -23,7 +23,7 @@ export function transformKiro(skills, distDir, patterns = null, options = {}) {
ensureDir(skillsDir);
const allSkillNames = skills.map(s => s.name);
const commandNames = skills.filter(s => s.userInvokable).map(s => `${prefix}${s.name}`);
const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
@@ -58,8 +58,8 @@ export function transformKiro(skills, distDir, patterns = null, options = {}) {
}
}
const userInvokableCount = skills.filter(s => s.userInvokable).length;
const userInvocableCount = skills.filter(s => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ Kiro${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
console.log(`✓ Kiro${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
}
+6 -6
View File
@@ -14,11 +14,11 @@ import {
* All skills output to .opencode/skills/{name}/SKILL.md
* User-invokable skills get args support in frontmatter.
*
* @param {Array} skills - All skills (including user-invokable ones)
* @param {Array} skills - All skills (including user-invocable ones)
* @param {string} distDir - Distribution output directory
* @param {Object} patterns - Design patterns data (unused, kept for interface consistency)
* @param {Object} options - Optional settings
* @param {string} options.prefix - Prefix to add to user-invokable skill names (e.g., 'i-')
* @param {string} options.prefix - Prefix to add to user-invocable skill names (e.g., 'i-')
* @param {string} options.outputSuffix - Suffix for output directory (e.g., '-prefixed')
*/
export function transformOpenCode(
@@ -36,7 +36,7 @@ export function transformOpenCode(
const allSkillNames = skills.map((s) => s.name);
const commandNames = skills
.filter((s) => s.userInvokable)
.filter((s) => s.userInvocable)
.map((s) => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
@@ -48,7 +48,7 @@ export function transformOpenCode(
description: skill.description,
};
if (skill.userInvokable) frontmatterObj['user-invokable'] = true;
if (skill.userInvocable) frontmatterObj['user-invocable'] = true;
if (skill.args && skill.args.length > 0) frontmatterObj.args = skill.args;
if (skill.license) frontmatterObj.license = skill.license;
if (skill.compatibility) frontmatterObj.compatibility = skill.compatibility;
@@ -77,8 +77,8 @@ export function transformOpenCode(
}
}
const userInvokableCount = skills.filter((s) => s.userInvokable).length;
const userInvocableCount = skills.filter((s) => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ OpenCode${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
console.log(`✓ OpenCode${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
}
+4 -4
View File
@@ -7,7 +7,7 @@ import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceho
* All skills output to .pi/skills/{name}/SKILL.md
* Frontmatter: name, description, license, compatibility, metadata
*
* @param {Array} skills - All skills (including user-invokable ones)
* @param {Array} skills - All skills (including user-invocable ones)
* @param {string} distDir - Distribution output directory
* @param {Object} patterns - Design patterns data (unused)
* @param {Object} options - Optional settings
@@ -23,7 +23,7 @@ export function transformPi(skills, distDir, patterns = null, options = {}) {
ensureDir(skillsDir);
const allSkillNames = skills.map(s => s.name);
const commandNames = skills.filter(s => s.userInvokable).map(s => `${prefix}${s.name}`);
const commandNames = skills.filter(s => s.userInvocable).map(s => `${prefix}${s.name}`);
let refCount = 0;
for (const skill of skills) {
const skillName = `${prefix}${skill.name}`;
@@ -58,8 +58,8 @@ export function transformPi(skills, distDir, patterns = null, options = {}) {
}
}
const userInvokableCount = skills.filter(s => s.userInvokable).length;
const userInvocableCount = skills.filter(s => s.userInvocable).length;
const refInfo = refCount > 0 ? ` (${refCount} reference files)` : '';
const prefixInfo = prefix ? ` [${prefix}prefixed]` : '';
console.log(`✓ Pi${prefixInfo}: ${skills.length} skills (${userInvokableCount} user-invokable)${refInfo}`);
console.log(`✓ Pi${prefixInfo}: ${skills.length} skills (${userInvocableCount} user-invocable)${refInfo}`);
}
+2 -2
View File
@@ -104,7 +104,7 @@ export function readFilesRecursive(dir, fileList = []) {
/**
* Read and parse all source files (unified skills architecture)
* All source lives in source/skills/{name}/SKILL.md
* Returns { skills } where each skill has userInvokable flag
* Returns { skills } where each skill has userInvocable flag
*/
export function readSourceFiles(rootDir) {
const skillsDir = path.join(rootDir, 'source/skills');
@@ -147,7 +147,7 @@ export function readSourceFiles(rootDir) {
compatibility: frontmatter.compatibility || '',
metadata: frontmatter.metadata || null,
allowedTools: frontmatter['allowed-tools'] || '',
userInvokable: frontmatter['user-invokable'] === true || frontmatter['user-invokable'] === 'true',
userInvocable: frontmatter['user-invocable'] === true || frontmatter['user-invocable'] === 'true',
args: frontmatter.args || [],
context: frontmatter.context || null,
body,
+3 -3
View File
@@ -40,17 +40,17 @@ export async function getSkills() {
id: entry.name,
name: frontmatter.name || entry.name,
description: frontmatter.description || "No description available",
userInvokable: frontmatter['user-invokable'] === true || frontmatter['user-invokable'] === 'true',
userInvocable: frontmatter['user-invocable'] === true || frontmatter['user-invocable'] === 'true',
});
}
return skills;
}
// Read commands (user-invokable skills)
// Read commands (user-invocable skills)
export async function getCommands() {
const allSkills = await getSkills();
return allSkills.filter(s => s.userInvokable);
return allSkills.filter(s => s.userInvocable);
}
// Get command/skill source content
+1 -1
View File
@@ -8,7 +8,7 @@ args:
- name: context
description: What to adapt for (mobile, tablet, desktop, print, email, etc.)
required: false
user-invokable: true
user-invocable: true
---
Adapt existing designs to work effectively across different contexts - different screen sizes, devices, platforms, or use cases.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or component to animate (optional)
required: false
user-invokable: true
user-invocable: true
---
Analyze a feature and strategically add animations and micro-interactions that enhance understanding, provide feedback, and create delight.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or component to improve layout for (optional)
required: false
user-invokable: true
user-invocable: true
---
Assess and improve layout and spacing that feels monotonous, crowded, or structurally weak — turning generic arrangements into intentional, rhythmic compositions.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: area
description: The feature or area to audit (optional)
required: false
user-invokable: true
user-invocable: true
---
Run systematic quality checks and generate a comprehensive audit report with prioritized issues and actionable recommendations. Don't fix issues - document them for other commands to address.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or component to make bolder (optional)
required: false
user-invokable: true
user-invocable: true
---
Increase visual impact and personality in designs that are too safe, generic, or visually underwhelming, creating more engaging and memorable experiences.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or component with unclear copy (optional)
required: false
user-invokable: true
user-invocable: true
---
Identify and improve unclear, confusing, or poorly written interface text to make the product easier to understand and use.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or component to colorize (optional)
required: false
user-invokable: true
user-invocable: true
---
Strategically introduce color to designs that are too monochromatic, gray, or lacking in visual warmth and personality.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: area
description: The feature or area to critique (optional)
required: false
user-invokable: true
user-invocable: true
---
## MANDATORY PREPARATION
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or area to add delight to (optional)
required: false
user-invokable: true
user-invocable: true
---
Identify opportunities to add moments of joy, personality, and unexpected polish that transform functional interfaces into delightful experiences.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or component to distill (optional)
required: false
user-invokable: true
user-invocable: true
---
Remove unnecessary complexity from designs, revealing the essential elements and creating clarity through ruthless simplification.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature, component, or area to extract from (optional)
required: false
user-invokable: true
user-invocable: true
---
Identify reusable patterns, components, and design tokens, then extract and consolidate them into the design system for systematic reuse.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or area to harden (optional)
required: false
user-invokable: true
user-invocable: true
---
Strengthen interfaces against edge cases, errors, internationalization issues, and real-world usage scenarios that break idealized designs.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: feature
description: The page, route, or feature to normalize (optional)
required: false
user-invokable: true
user-invocable: true
---
Analyze and redesign the feature to perfectly match our design system standards, aesthetics, and established patterns.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or area needing onboarding (optional)
required: false
user-invokable: true
user-invocable: true
---
## MANDATORY PREPARATION
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or area to optimize (optional)
required: false
user-invokable: true
user-invocable: true
---
Identify and fix performance issues to create faster, smoother user experiences.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or area to push into overdrive (optional)
required: false
user-invokable: true
user-invocable: true
---
Start your response with:
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or area to polish (optional)
required: false
user-invokable: true
user-invocable: true
---
## MANDATORY PREPARATION
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or component to make quieter (optional)
required: false
user-invokable: true
user-invocable: true
---
Reduce visual intensity in designs that are too bold, aggressive, or overstimulating, creating a more refined and approachable aesthetic without losing effectiveness.
+1 -1
View File
@@ -1,7 +1,7 @@
---
name: teach-impeccable
description: One-time setup that gathers design context for your project and saves it to your AI config file. Run once to establish persistent design guidelines.
user-invokable: true
user-invocable: true
---
Gather design context for this project, then persist it for all future sessions.
+1 -1
View File
@@ -5,7 +5,7 @@ args:
- name: target
description: The feature or component to improve typography for (optional)
required: false
user-invokable: true
user-invocable: true
---
Assess and improve typography that feels generic, inconsistent, or poorly structured — turning default-looking text into intentional, well-crafted type.
+6 -6
View File
@@ -166,7 +166,7 @@ This is a test skill body.`;
const skillContent = `---
name: audit
description: Run technical quality checks
user-invokable: true
user-invocable: true
args:
- name: target
description: Target element
@@ -188,31 +188,31 @@ Please audit {{target}} for technical quality. Ask {{model}} for help.`;
transformers.transformGemini(skills, DIST_DIR, patterns);
transformers.transformCodex(skills, DIST_DIR, patterns);
// Verify Cursor: full frontmatter with user-invokable
// Verify Cursor: full frontmatter with user-invocable
const cursorContent = fs.readFileSync(path.join(DIST_DIR, 'cursor/.cursor/skills/audit/SKILL.md'), 'utf-8');
expect(cursorContent).toContain('---');
expect(cursorContent).toContain('name: audit');
expect(cursorContent).toContain('{{target}}');
expect(cursorContent).toContain('the model');
// Verify Claude Code: full frontmatter with user-invokable and args
// Verify Claude Code: full frontmatter with user-invocable and args
const claudeContent = fs.readFileSync(path.join(DIST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8');
expect(claudeContent).toContain('---');
expect(claudeContent).toContain('name: audit');
expect(claudeContent).toContain('user-invokable: true');
expect(claudeContent).toContain('user-invocable: true');
expect(claudeContent).toContain('{{target}}');
expect(claudeContent).toContain('Claude');
// Verify Gemini: skill in skills directory
expect(fs.existsSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'))).toBe(true);
const geminiContent = fs.readFileSync(path.join(DIST_DIR, 'gemini/.gemini/skills/audit/SKILL.md'), 'utf-8');
expect(geminiContent).toContain('{{args}}'); // Replaced for user-invokable in Gemini
expect(geminiContent).toContain('{{args}}'); // Replaced for user-invocable in Gemini
expect(geminiContent).toContain('Gemini');
// Verify Codex: skill in skills directory
expect(fs.existsSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'))).toBe(true);
const codexContent = fs.readFileSync(path.join(DIST_DIR, 'codex/.codex/skills/audit/SKILL.md'), 'utf-8');
expect(codexContent).toContain('$TARGET'); // Replaced for user-invokable in Codex
expect(codexContent).toContain('$TARGET'); // Replaced for user-invocable in Codex
expect(codexContent).toContain('GPT');
});
+17 -17
View File
@@ -47,12 +47,12 @@ describe('transformAgents', () => {
expect(parsed.body).toBe('Skill instructions.');
});
test('should add user-invokable flag for user-invokable skills', () => {
test('should add user-invocable flag for user-invocable skills', () => {
const skills = [
{
name: 'audit',
description: 'Audit command',
userInvokable: true,
userInvocable: true,
body: 'Audit the code.'
}
];
@@ -62,10 +62,10 @@ describe('transformAgents', () => {
const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/audit/SKILL.md'), 'utf-8');
const parsed = parseFrontmatter(content);
expect(parsed.frontmatter['user-invokable']).toBe(true);
expect(parsed.frontmatter['user-invocable']).toBe(true);
});
test('should not add user-invokable flag for non-user-invokable skills', () => {
test('should not add user-invocable flag for non-user-invocable skills', () => {
const skills = [
{
name: 'helper',
@@ -79,7 +79,7 @@ describe('transformAgents', () => {
const content = fs.readFileSync(path.join(TEST_DIR, 'agents/.agents/skills/helper/SKILL.md'), 'utf-8');
const parsed = parseFrontmatter(content);
expect(parsed.frontmatter['user-invokable']).toBeUndefined();
expect(parsed.frontmatter['user-invocable']).toBeUndefined();
});
test('should create argument-hint for required args', () => {
@@ -87,7 +87,7 @@ describe('transformAgents', () => {
{
name: 'with-args',
description: 'Command with args',
userInvokable: true,
userInvocable: true,
args: [
{ name: 'target', description: 'Target', required: true },
{ name: 'format', description: 'Format', required: false }
@@ -109,7 +109,7 @@ describe('transformAgents', () => {
{
name: 'no-args',
description: 'No args',
userInvokable: true,
userInvocable: true,
args: [],
body: 'Simple body.'
}
@@ -123,12 +123,12 @@ describe('transformAgents', () => {
expect(parsed.frontmatter['argument-hint']).toBeUndefined();
});
test('should not add argument-hint for non-user-invokable skills with args', () => {
test('should not add argument-hint for non-user-invocable skills with args', () => {
const skills = [
{
name: 'internal',
description: 'Internal skill',
userInvokable: false,
userInvocable: false,
args: [{ name: 'target', description: 'Target', required: true }],
body: 'Body.'
}
@@ -188,8 +188,8 @@ describe('transformAgents', () => {
test('should replace {{available_commands}} placeholder', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Available: {{available_commands}}' },
{ name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Available: {{available_commands}}' },
{ name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' }
];
transformAgents(skills, TEST_DIR);
@@ -241,7 +241,7 @@ describe('transformAgents', () => {
test('should support prefix option', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Audit body' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Audit body' }
];
transformAgents(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
@@ -255,8 +255,8 @@ describe('transformAgents', () => {
test('should prefix skill references in body when prefix is set', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Run /polish after the audit skill.' },
{ name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Run /polish after the audit skill.' },
{ name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' }
];
transformAgents(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
@@ -284,8 +284,8 @@ describe('transformAgents', () => {
console.log = consoleMock;
const skills = [
{ name: 'skill1', description: 'Test', userInvokable: true, body: 'body' },
{ name: 'skill2', description: 'Test', userInvokable: false, body: 'body' }
{ name: 'skill1', description: 'Test', userInvocable: true, body: 'body' },
{ name: 'skill2', description: 'Test', userInvocable: false, body: 'body' }
];
transformAgents(skills, TEST_DIR);
@@ -294,7 +294,7 @@ describe('transformAgents', () => {
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Agents:'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable'));
});
test('should log reference file count', () => {
+12 -12
View File
@@ -51,12 +51,12 @@ describe('transformClaudeCode', () => {
expect(parsed.body).toBe('Skill instructions.');
});
test('should add user-invokable flag for user-invokable skills', () => {
test('should add user-invocable flag for user-invocable skills', () => {
const skills = [
{
name: 'audit',
description: 'Audit command',
userInvokable: true,
userInvocable: true,
body: 'Audit the code.'
}
];
@@ -66,15 +66,15 @@ describe('transformClaudeCode', () => {
const content = fs.readFileSync(path.join(TEST_DIR, 'claude-code/.claude/skills/audit/SKILL.md'), 'utf-8');
const parsed = parseFrontmatter(content);
expect(parsed.frontmatter['user-invokable']).toBe(true);
expect(parsed.frontmatter['user-invocable']).toBe(true);
});
test('should include args in frontmatter for user-invokable skills', () => {
test('should include args in frontmatter for user-invocable skills', () => {
const skills = [
{
name: 'test-command',
description: 'A test command',
userInvokable: true,
userInvocable: true,
args: [
{ name: 'target', description: 'The target', required: false },
{ name: 'output', description: 'Output format', required: true }
@@ -100,7 +100,7 @@ describe('transformClaudeCode', () => {
{
name: 'simple-skill',
description: 'Simple skill',
userInvokable: true,
userInvocable: true,
args: [],
body: 'Simple body.'
}
@@ -162,7 +162,7 @@ describe('transformClaudeCode', () => {
{
name: 'with-placeholder',
description: 'Has placeholder',
userInvokable: true,
userInvocable: true,
args: [{ name: 'target', description: 'Target', required: false }],
body: 'Process {{target}} and generate output.'
}
@@ -203,8 +203,8 @@ describe('transformClaudeCode', () => {
console.log = consoleMock;
const skills = [
{ name: 'skill1', description: 'Test', license: '', userInvokable: true, body: 'body' },
{ name: 'skill2', description: 'Test', license: '', userInvokable: false, body: 'body' }
{ name: 'skill1', description: 'Test', license: '', userInvocable: true, body: 'body' },
{ name: 'skill2', description: 'Test', license: '', userInvocable: false, body: 'body' }
];
transformClaudeCode(skills, TEST_DIR);
@@ -213,7 +213,7 @@ describe('transformClaudeCode', () => {
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Claude Code:'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable'));
});
test('should handle empty arrays', () => {
@@ -228,7 +228,7 @@ describe('transformClaudeCode', () => {
{
name: 'test',
description: 'Test command',
userInvokable: true,
userInvocable: true,
args: [
{ name: 'arg1', description: 'First arg', required: true },
{ name: 'arg2', description: 'Second arg', required: false }
@@ -279,7 +279,7 @@ Second paragraph with details.
test('should support prefix option', () => {
const skills = [
{ name: 'audit', description: 'Audit', license: '', userInvokable: true, body: 'Audit body' }
{ name: 'audit', description: 'Audit', license: '', userInvocable: true, body: 'Audit body' }
];
transformClaudeCode(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
+12 -12
View File
@@ -55,7 +55,7 @@ describe('transformCodex', () => {
{
name: 'with-args',
description: 'Command with args',
userInvokable: true,
userInvocable: true,
args: [
{ name: 'target', description: 'Target', required: true },
{ name: 'output', description: 'Output', required: true }
@@ -77,7 +77,7 @@ describe('transformCodex', () => {
{
name: 'optional-args',
description: 'Command with optional args',
userInvokable: true,
userInvocable: true,
args: [
{ name: 'format', description: 'Format', required: false }
],
@@ -98,7 +98,7 @@ describe('transformCodex', () => {
{
name: 'mixed-args',
description: 'Mixed args',
userInvokable: true,
userInvocable: true,
args: [
{ name: 'input', description: 'Input', required: true },
{ name: 'format', description: 'Format', required: false },
@@ -116,12 +116,12 @@ describe('transformCodex', () => {
expect(parsed.frontmatter['argument-hint']).toBe('<input> [FORMAT=<value>] <output>');
});
test('should transform {{argname}} to $ARGNAME for user-invokable skills', () => {
test('should transform {{argname}} to $ARGNAME for user-invocable skills', () => {
const skills = [
{
name: 'normalize',
description: 'Normalize',
userInvokable: true,
userInvocable: true,
args: [{ name: 'target', description: 'Target', required: false }],
body: 'Please normalize {{target}} to match the design system.'
}
@@ -141,7 +141,7 @@ describe('transformCodex', () => {
{
name: 'multi-arg',
description: 'Multiple args',
userInvokable: true,
userInvocable: true,
args: [],
body: 'Process {{input}} and output to {{output}} with {{format}}.'
}
@@ -198,8 +198,8 @@ describe('transformCodex', () => {
console.log = consoleMock;
const skills = [
{ name: 'skill1', description: 'Test', license: '', userInvokable: true, body: 'body' },
{ name: 'skill2', description: 'Test', license: '', userInvokable: false, body: 'body' }
{ name: 'skill1', description: 'Test', license: '', userInvocable: true, body: 'body' },
{ name: 'skill2', description: 'Test', license: '', userInvocable: false, body: 'body' }
];
transformCodex(skills, TEST_DIR);
@@ -208,7 +208,7 @@ describe('transformCodex', () => {
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Codex:'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable'));
});
test('should handle empty arrays', () => {
@@ -218,12 +218,12 @@ describe('transformCodex', () => {
expect(skillDirs).toHaveLength(0);
});
test('should handle user-invokable skills without args', () => {
test('should handle user-invocable skills without args', () => {
const skills = [
{
name: 'no-args',
description: 'No args command',
userInvokable: true,
userInvocable: true,
args: [],
body: 'Body content'
}
@@ -263,7 +263,7 @@ Second line after blank.
test('should support prefix option', () => {
const skills = [
{ name: 'audit', description: 'Audit', license: '', userInvokable: true, body: 'Audit body' }
{ name: 'audit', description: 'Audit', license: '', userInvocable: true, body: 'Audit body' }
];
transformCodex(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
+3 -3
View File
@@ -123,8 +123,8 @@ describe('transformCursor', () => {
console.log = consoleMock;
const skills = [
{ name: 'skill1', description: '', license: '', userInvokable: true, body: 'body1' },
{ name: 'skill2', description: '', license: '', userInvokable: false, body: 'body2' }
{ name: 'skill1', description: '', license: '', userInvocable: true, body: 'body1' },
{ name: 'skill2', description: '', license: '', userInvocable: false, body: 'body2' }
];
transformCursor(skills, TEST_DIR);
@@ -133,7 +133,7 @@ describe('transformCursor', () => {
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Cursor:'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable'));
});
test('should handle empty skills array', () => {
+10 -10
View File
@@ -62,12 +62,12 @@ describe('transformGemini', () => {
expect(fs.existsSync(path.join(TEST_DIR, 'gemini/.gemini/skills/skill3/SKILL.md'))).toBe(true);
});
test('should handle user-invokable skills with args', () => {
test('should handle user-invocable skills with args', () => {
const skills = [
{
name: 'normalize',
description: 'Normalize design',
userInvokable: true,
userInvocable: true,
args: [{ name: 'target', description: 'Target', required: false }],
body: 'Please normalize {{target}} to match the design system.'
}
@@ -76,17 +76,17 @@ describe('transformGemini', () => {
transformGemini(skills, TEST_DIR);
const content = fs.readFileSync(path.join(TEST_DIR, 'gemini/.gemini/skills/normalize/SKILL.md'), 'utf-8');
// For user-invokable skills, {{arg}} placeholders become {{args}}
// For user-invocable skills, {{arg}} placeholders become {{args}}
expect(content).toContain('{{args}}');
expect(content).not.toContain('{{target}}');
});
test('should replace multiple different placeholders with {{args}} for user-invokable skills', () => {
test('should replace multiple different placeholders with {{args}} for user-invocable skills', () => {
const skills = [
{
name: 'multi-arg',
description: 'Multiple args',
userInvokable: true,
userInvocable: true,
args: [],
body: 'Process {{input}} and output to {{output}} with {{format}}.'
}
@@ -99,12 +99,12 @@ describe('transformGemini', () => {
expect(argsMatches).toHaveLength(3);
});
test('should not replace placeholders for non-user-invokable skills', () => {
test('should not replace placeholders for non-user-invocable skills', () => {
const skills = [
{
name: 'passive-skill',
description: 'Passive skill',
userInvokable: false,
userInvocable: false,
body: 'Process {{target}} normally.'
}
];
@@ -143,8 +143,8 @@ describe('transformGemini', () => {
console.log = consoleMock;
const skills = [
{ name: 'skill1', description: 'Test', license: '', userInvokable: true, body: 'body' },
{ name: 'skill2', description: 'Test', license: '', userInvokable: false, body: 'body' }
{ name: 'skill1', description: 'Test', license: '', userInvocable: true, body: 'body' },
{ name: 'skill2', description: 'Test', license: '', userInvocable: false, body: 'body' }
];
transformGemini(skills, TEST_DIR);
@@ -153,7 +153,7 @@ describe('transformGemini', () => {
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Gemini:'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable'));
});
test('should handle empty arrays', () => {
+12 -12
View File
@@ -80,12 +80,12 @@ describe('transformKiro', () => {
expect(content).toContain('metadata: some-metadata');
});
test('should not include user-invokable in frontmatter (Kiro does not use it)', () => {
test('should not include user-invocable in frontmatter (Kiro does not use it)', () => {
const skills = [
{
name: 'test',
description: 'Test',
userInvokable: true,
userInvocable: true,
body: 'Body'
}
];
@@ -93,7 +93,7 @@ describe('transformKiro', () => {
transformKiro(skills, TEST_DIR);
const content = fs.readFileSync(path.join(TEST_DIR, 'kiro/.kiro/skills/test/SKILL.md'), 'utf-8');
expect(content).not.toContain('user-invokable');
expect(content).not.toContain('user-invocable');
});
test('should handle multiple skills', () => {
@@ -142,8 +142,8 @@ describe('transformKiro', () => {
test('should replace {{available_commands}} placeholder', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Available: {{available_commands}}' },
{ name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Available: {{available_commands}}' },
{ name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' }
];
transformKiro(skills, TEST_DIR);
@@ -180,7 +180,7 @@ describe('transformKiro', () => {
{
name: 'test',
description: 'Test',
userInvokable: true,
userInvocable: true,
body: 'Body with {{available_commands}}.',
references: [
{ name: 'ref', content: 'Use {{model}} with {{config_file}}. Commands: {{available_commands}}.', filePath: '/fake/ref.md' }
@@ -198,7 +198,7 @@ describe('transformKiro', () => {
test('should support prefix option', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Audit body' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Audit body' }
];
transformKiro(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
@@ -212,8 +212,8 @@ describe('transformKiro', () => {
test('should prefix skill references in body when prefix is set', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Run /polish after the audit skill.' },
{ name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Run /polish after the audit skill.' },
{ name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' }
];
transformKiro(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
@@ -241,8 +241,8 @@ describe('transformKiro', () => {
console.log = consoleMock;
const skills = [
{ name: 'skill1', description: 'Test', userInvokable: true, body: 'body' },
{ name: 'skill2', description: 'Test', userInvokable: false, body: 'body' }
{ name: 'skill1', description: 'Test', userInvocable: true, body: 'body' },
{ name: 'skill2', description: 'Test', userInvocable: false, body: 'body' }
];
transformKiro(skills, TEST_DIR);
@@ -251,7 +251,7 @@ describe('transformKiro', () => {
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Kiro:'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable'));
});
test('should log reference file count', () => {
+16 -16
View File
@@ -48,12 +48,12 @@ describe('transformOpenCode', () => {
expect(parsed.body).toBe('Skill instructions.');
});
test('should add user-invokable flag for user-invokable skills', () => {
test('should add user-invocable flag for user-invocable skills', () => {
const skills = [
{
name: 'audit',
description: 'Audit command',
userInvokable: true,
userInvocable: true,
body: 'Audit the code.'
}
];
@@ -63,10 +63,10 @@ describe('transformOpenCode', () => {
const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/audit/SKILL.md'), 'utf-8');
const parsed = parseFrontmatter(content);
expect(parsed.frontmatter['user-invokable']).toBe(true);
expect(parsed.frontmatter['user-invocable']).toBe(true);
});
test('should not add user-invokable flag for non-user-invokable skills', () => {
test('should not add user-invocable flag for non-user-invocable skills', () => {
const skills = [
{
name: 'helper',
@@ -80,7 +80,7 @@ describe('transformOpenCode', () => {
const content = fs.readFileSync(path.join(TEST_DIR, 'opencode/.opencode/skills/helper/SKILL.md'), 'utf-8');
const parsed = parseFrontmatter(content);
expect(parsed.frontmatter['user-invokable']).toBeUndefined();
expect(parsed.frontmatter['user-invocable']).toBeUndefined();
});
test('should include args in frontmatter', () => {
@@ -88,7 +88,7 @@ describe('transformOpenCode', () => {
{
name: 'with-args',
description: 'Command with args',
userInvokable: true,
userInvocable: true,
args: [
{ name: 'target', description: 'Target element', required: false }
],
@@ -111,7 +111,7 @@ describe('transformOpenCode', () => {
{
name: 'no-args',
description: 'No args',
userInvokable: true,
userInvocable: true,
args: [],
body: 'Simple body.'
}
@@ -234,8 +234,8 @@ describe('transformOpenCode', () => {
test('should replace {{available_commands}} placeholder', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Available: {{available_commands}}' },
{ name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Available: {{available_commands}}' },
{ name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' }
];
transformOpenCode(skills, TEST_DIR);
@@ -272,7 +272,7 @@ describe('transformOpenCode', () => {
{
name: 'test',
description: 'Test',
userInvokable: true,
userInvocable: true,
body: 'Body with {{available_commands}}.',
references: [
{ name: 'ref', content: 'Use {{model}} with {{config_file}}. Commands: {{available_commands}}.', filePath: '/fake/ref.md' }
@@ -290,7 +290,7 @@ describe('transformOpenCode', () => {
test('should support prefix option', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Audit body' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Audit body' }
];
transformOpenCode(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
@@ -304,8 +304,8 @@ describe('transformOpenCode', () => {
test('should prefix skill references in body when prefix is set', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Run /polish after the audit skill.' },
{ name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Run /polish after the audit skill.' },
{ name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' }
];
transformOpenCode(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
@@ -333,8 +333,8 @@ describe('transformOpenCode', () => {
console.log = consoleMock;
const skills = [
{ name: 'skill1', description: 'Test', userInvokable: true, body: 'body' },
{ name: 'skill2', description: 'Test', userInvokable: false, body: 'body' }
{ name: 'skill1', description: 'Test', userInvocable: true, body: 'body' },
{ name: 'skill2', description: 'Test', userInvocable: false, body: 'body' }
];
transformOpenCode(skills, TEST_DIR);
@@ -343,7 +343,7 @@ describe('transformOpenCode', () => {
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ OpenCode:'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable'));
});
test('should log reference file count', () => {
+12 -12
View File
@@ -80,12 +80,12 @@ describe('transformPi', () => {
expect(content).toContain('metadata: some-metadata');
});
test('should not include user-invokable in frontmatter (Pi does not use it)', () => {
test('should not include user-invocable in frontmatter (Pi does not use it)', () => {
const skills = [
{
name: 'test',
description: 'Test',
userInvokable: true,
userInvocable: true,
body: 'Body'
}
];
@@ -93,7 +93,7 @@ describe('transformPi', () => {
transformPi(skills, TEST_DIR);
const content = fs.readFileSync(path.join(TEST_DIR, 'pi/.pi/skills/test/SKILL.md'), 'utf-8');
expect(content).not.toContain('user-invokable');
expect(content).not.toContain('user-invocable');
});
test('should handle multiple skills', () => {
@@ -142,8 +142,8 @@ describe('transformPi', () => {
test('should replace {{available_commands}} placeholder', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Available: {{available_commands}}' },
{ name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Available: {{available_commands}}' },
{ name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' }
];
transformPi(skills, TEST_DIR);
@@ -180,7 +180,7 @@ describe('transformPi', () => {
{
name: 'test',
description: 'Test',
userInvokable: true,
userInvocable: true,
body: 'Body with {{available_commands}}.',
references: [
{ name: 'ref', content: 'Use {{model}} with {{config_file}}. Commands: {{available_commands}}.', filePath: '/fake/ref.md' }
@@ -198,7 +198,7 @@ describe('transformPi', () => {
test('should support prefix option', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Audit body' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Audit body' }
];
transformPi(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
@@ -212,8 +212,8 @@ describe('transformPi', () => {
test('should prefix skill references in body when prefix is set', () => {
const skills = [
{ name: 'audit', description: 'Audit', userInvokable: true, body: 'Run /polish after the audit skill.' },
{ name: 'polish', description: 'Polish', userInvokable: true, body: 'Polish body.' }
{ name: 'audit', description: 'Audit', userInvocable: true, body: 'Run /polish after the audit skill.' },
{ name: 'polish', description: 'Polish', userInvocable: true, body: 'Polish body.' }
];
transformPi(skills, TEST_DIR, null, { prefix: 'i-', outputSuffix: '-prefixed' });
@@ -241,8 +241,8 @@ describe('transformPi', () => {
console.log = consoleMock;
const skills = [
{ name: 'skill1', description: 'Test', userInvokable: true, body: 'body' },
{ name: 'skill2', description: 'Test', userInvokable: false, body: 'body' }
{ name: 'skill1', description: 'Test', userInvocable: true, body: 'body' },
{ name: 'skill2', description: 'Test', userInvocable: false, body: 'body' }
];
transformPi(skills, TEST_DIR);
@@ -251,7 +251,7 @@ describe('transformPi', () => {
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('✓ Pi:'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('2 skills'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invokable'));
expect(consoleMock).toHaveBeenCalledWith(expect.stringContaining('1 user-invocable'));
});
test('should log reference file count', () => {
+13 -13
View File
@@ -89,29 +89,29 @@ Skill body.`;
expect(result.frontmatter.license).toBe('MIT');
});
test('should parse user-invokable boolean', () => {
test('should parse user-invocable boolean', () => {
const content = `---
name: test-skill
user-invokable: true
user-invocable: true
---
Body.`;
const result = parseFrontmatter(content);
expect(result.frontmatter['user-invokable']).toBe(true);
expect(result.frontmatter['user-invocable']).toBe(true);
});
test('should parse user-invokable as string true (code behavior)', () => {
test('should parse user-invocable as string true (code behavior)', () => {
const content = `---
name: test-skill
user-invokable: 'true'
user-invocable: 'true'
---
Body.`;
const result = parseFrontmatter(content);
// The parseFrontmatter function doesn't strip quotes from YAML string values
expect(result.frontmatter['user-invokable']).toBe("'true'");
expect(result.frontmatter['user-invocable']).toBe("'true'");
});
test('should parse allowed-tools field', () => {
@@ -162,11 +162,11 @@ describe('generateYamlFrontmatter', () => {
const data = {
name: 'test',
description: 'Test',
'user-invokable': true
'user-invocable': true
};
const result = generateYamlFrontmatter(data);
expect(result).toContain('user-invokable: true');
expect(result).toContain('user-invocable: true');
});
test('should roundtrip: generate and parse back', () => {
@@ -372,11 +372,11 @@ Skill instructions here.`;
expect(skills[0].body).toBe('Skill instructions here.');
});
test('should read skill with user-invokable flag', () => {
test('should read skill with user-invocable flag', () => {
const skillContent = `---
name: audit
description: Run technical quality checks
user-invokable: true
user-invocable: true
---
Audit the code.`;
@@ -388,7 +388,7 @@ Audit the code.`;
const { skills } = readSourceFiles(testRootDir);
expect(skills).toHaveLength(1);
expect(skills[0].userInvokable).toBe(true);
expect(skills[0].userInvocable).toBe(true);
});
test('should read skill with reference files', () => {
@@ -478,7 +478,7 @@ name: test-skill
description: A comprehensive test skill
license: Apache-2.0
compatibility: claude-code
user-invokable: true
user-invocable: true
allowed-tools: Bash,Edit
---
@@ -494,7 +494,7 @@ Body content.`;
expect(skills[0].description).toBe('A comprehensive test skill');
expect(skills[0].license).toBe('Apache-2.0');
expect(skills[0].compatibility).toBe('claude-code');
expect(skills[0].userInvokable).toBe(true);
expect(skills[0].userInvocable).toBe(true);
expect(skills[0].allowedTools).toBe('Bash,Edit');
});
});