mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-15 07:36:50 +03:00
Add teach-impeccable and review commands, dynamic placeholders
New commands:
- /teach-impeccable: One-time setup that gathers UX-focused design
context (explores codebase first, then asks targeted questions)
- /review: UX design review evaluating hierarchy, clarity, emotional
resonance (complements technical /audit)
Build system improvements:
- Dynamic {{model}}, {{ask_instruction}}, {{config_file}} placeholders
per provider (Claude/Gemini/GPT/the model)
- context: fork support for Claude Code sub-agents (audit, extract)
- Skill reference auto-added to design commands
Skill refinements:
- Merged Design Thinking and Context Awareness sections
- Removed redundant patterns and technical questions
- More direct feedback guidance (radical candor)
Website updates:
- 17 commands (was 16)
- Review demo showing UX issues (HIERARCHY, NO PRIMARY, DEAD END)
- teach-impeccable in System group of periodic table
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.5
parent
8186f2a226
commit
0b9c1846a6
@@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter } from '../utils.js';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generate markdown from structured patterns/antipatterns data
|
||||
@@ -66,10 +66,12 @@ export function transformClaudeCode(commands, skills, distDir, patterns = null)
|
||||
const frontmatter = generateYamlFrontmatter({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
...(command.context && { context: command.context }),
|
||||
...(command.args.length > 0 && { args: command.args })
|
||||
});
|
||||
|
||||
const content = `${frontmatter}\n\n${command.body}`;
|
||||
const commandBody = replacePlaceholders(command.body, 'claude-code');
|
||||
const content = `${frontmatter}\n\n${commandBody}`;
|
||||
const outputPath = path.join(commandsDir, `${command.name}.md`);
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
@@ -92,22 +94,8 @@ export function transformClaudeCode(commands, skills, distDir, patterns = null)
|
||||
|
||||
const frontmatter = generateYamlFrontmatter(frontmatterObj);
|
||||
|
||||
let body = skill.body;
|
||||
|
||||
// Generate and merge patterns into frontend-design skill (before Domain Reference Files section)
|
||||
if (skill.name === 'frontend-design' && patterns) {
|
||||
const patternsMarkdown = generatePatternsMarkdown(patterns);
|
||||
if (patternsMarkdown) {
|
||||
const insertPoint = body.indexOf('---\n\n## Domain Reference Files');
|
||||
if (insertPoint > -1) {
|
||||
body = body.slice(0, insertPoint) + '\n\n' + patternsMarkdown + '\n\n' + body.slice(insertPoint);
|
||||
} else {
|
||||
body += '\n\n' + patternsMarkdown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = `${frontmatter}\n\n${body}`;
|
||||
const skillBody = replacePlaceholders(skill.body, 'claude-code');
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
|
||||
@@ -117,7 +105,8 @@ export function transformClaudeCode(commands, skills, distDir, patterns = null)
|
||||
ensureDir(refDir);
|
||||
for (const ref of skill.references) {
|
||||
const refOutputPath = path.join(refDir, `${ref.name}.md`);
|
||||
writeFile(refOutputPath, ref.content);
|
||||
const refContent = replacePlaceholders(ref.content, 'claude-code');
|
||||
writeFile(refOutputPath, refContent);
|
||||
refCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter } from '../utils.js';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generate markdown from structured patterns/antipatterns data
|
||||
@@ -77,8 +77,8 @@ export function transformCodex(commands, skills, distDir, patterns = null) {
|
||||
|
||||
yamlLines.push('---');
|
||||
|
||||
// Transform {{argname}} to $ARGNAME for Codex
|
||||
let body = command.body;
|
||||
// First replace our placeholders, then transform remaining {{argname}} to $ARGNAME
|
||||
let body = replacePlaceholders(command.body, 'codex');
|
||||
body = body.replace(/\{\{([^}]+)\}\}/g, (match, argName) => {
|
||||
return `$${argName.toUpperCase()}`;
|
||||
});
|
||||
@@ -99,22 +99,8 @@ export function transformCodex(commands, skills, distDir, patterns = null) {
|
||||
...(skill.license && { license: skill.license })
|
||||
});
|
||||
|
||||
let body = skill.body;
|
||||
|
||||
// Generate and merge patterns into frontend-design skill
|
||||
if (skill.name === 'frontend-design' && patterns) {
|
||||
const patternsMarkdown = generatePatternsMarkdown(patterns);
|
||||
if (patternsMarkdown) {
|
||||
const insertPoint = body.indexOf('---\n\n## Domain Reference Files');
|
||||
if (insertPoint > -1) {
|
||||
body = body.slice(0, insertPoint) + '\n\n' + patternsMarkdown + '\n\n' + body.slice(insertPoint);
|
||||
} else {
|
||||
body += '\n\n' + patternsMarkdown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = `${frontmatter}\n\n${body}`;
|
||||
const skillBody = replacePlaceholders(skill.body, 'codex');
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
|
||||
@@ -124,7 +110,8 @@ export function transformCodex(commands, skills, distDir, patterns = null) {
|
||||
ensureDir(refDir);
|
||||
for (const ref of skill.references) {
|
||||
const refOutputPath = path.join(refDir, `${ref.name}.md`);
|
||||
writeFile(refOutputPath, ref.content);
|
||||
const refContent = replacePlaceholders(ref.content, 'codex');
|
||||
writeFile(refOutputPath, refContent);
|
||||
refCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter } from '../utils.js';
|
||||
import { cleanDir, ensureDir, writeFile, generateYamlFrontmatter, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Generate markdown from structured patterns/antipatterns data
|
||||
@@ -65,8 +65,9 @@ export function transformCursor(commands, skills, distDir, patterns = null) {
|
||||
|
||||
// Commands: Body only (Cursor doesn't support command frontmatter/args)
|
||||
for (const command of commands) {
|
||||
const commandBody = replacePlaceholders(command.body, 'cursor');
|
||||
const outputPath = path.join(commandsDir, `${command.name}.md`);
|
||||
writeFile(outputPath, command.body);
|
||||
writeFile(outputPath, commandBody);
|
||||
}
|
||||
|
||||
// Skills: Agent Skills standard with SKILL.md in subdirectories
|
||||
@@ -80,22 +81,8 @@ export function transformCursor(commands, skills, distDir, patterns = null) {
|
||||
...(skill.license && { license: skill.license })
|
||||
});
|
||||
|
||||
let body = skill.body;
|
||||
|
||||
// Generate and merge patterns into frontend-design skill
|
||||
if (skill.name === 'frontend-design' && patterns) {
|
||||
const patternsMarkdown = generatePatternsMarkdown(patterns);
|
||||
if (patternsMarkdown) {
|
||||
const insertPoint = body.indexOf('---\n\n## Domain Reference Files');
|
||||
if (insertPoint > -1) {
|
||||
body = body.slice(0, insertPoint) + '\n\n' + patternsMarkdown + '\n\n' + body.slice(insertPoint);
|
||||
} else {
|
||||
body += '\n\n' + patternsMarkdown;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const content = `${frontmatter}\n\n${body}`;
|
||||
const skillBody = replacePlaceholders(skill.body, 'cursor');
|
||||
const content = `${frontmatter}\n\n${skillBody}`;
|
||||
const outputPath = path.join(skillDir, 'SKILL.md');
|
||||
writeFile(outputPath, content);
|
||||
|
||||
@@ -105,7 +92,8 @@ export function transformCursor(commands, skills, distDir, patterns = null) {
|
||||
ensureDir(refDir);
|
||||
for (const ref of skill.references) {
|
||||
const refOutputPath = path.join(refDir, `${ref.name}.md`);
|
||||
writeFile(refOutputPath, ref.content);
|
||||
const refContent = replacePlaceholders(ref.content, 'cursor');
|
||||
writeFile(refOutputPath, refContent);
|
||||
refCount++;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import path from 'path';
|
||||
import { cleanDir, ensureDir, writeFile } from '../utils.js';
|
||||
import { cleanDir, ensureDir, writeFile, replacePlaceholders } from '../utils.js';
|
||||
|
||||
/**
|
||||
* Gemini Transformer (Full Featured - TOML + Modular Skills)
|
||||
@@ -17,8 +17,9 @@ export function transformGemini(commands, skills, distDir, patterns = null) {
|
||||
|
||||
// Commands: Transform to TOML
|
||||
for (const command of commands) {
|
||||
// Replace named placeholders with {{args}}
|
||||
let prompt = command.body.replace(/\{\{[^}]+\}\}/g, '{{args}}');
|
||||
// First replace our placeholders, then replace remaining {{arg}} with {{args}}
|
||||
let prompt = replacePlaceholders(command.body, 'gemini');
|
||||
prompt = prompt.replace(/\{\{[^}]+\}\}/g, '{{args}}');
|
||||
|
||||
const toml = [
|
||||
`description = "${command.description.replace(/"/g, '\\"')}"`,
|
||||
@@ -50,11 +51,15 @@ export function transformGemini(commands, skills, distDir, patterns = null) {
|
||||
if (skill.references && skill.references.length > 0) {
|
||||
const refSections = skill.references.map(ref => {
|
||||
refCount++;
|
||||
return `\n\n---\n\n## Reference: ${ref.name}\n\n${ref.content}`;
|
||||
const refContent = replacePlaceholders(ref.content, 'gemini');
|
||||
return `\n\n---\n\n## Reference: ${ref.name}\n\n${refContent}`;
|
||||
});
|
||||
content += refSections.join('');
|
||||
}
|
||||
|
||||
// Replace all placeholders
|
||||
content = replacePlaceholders(content, 'gemini');
|
||||
|
||||
const outputPath = path.join(geminiDir, `GEMINI.${skill.name}.md`);
|
||||
writeFile(outputPath, content);
|
||||
}
|
||||
|
||||
+92
-58
@@ -122,6 +122,7 @@ export function readSourceFiles(rootDir) {
|
||||
name: frontmatter.name || name,
|
||||
description: frontmatter.description || '',
|
||||
args: frontmatter.args || [],
|
||||
context: frontmatter.context || null,
|
||||
body,
|
||||
filePath
|
||||
};
|
||||
@@ -223,84 +224,117 @@ export function writeFile(filePath, content) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse patterns.md
|
||||
* Returns { patterns: [...], antipatterns: [...], body: string }
|
||||
* Extract patterns from frontend-design SKILL.md
|
||||
* Parses **DO**: and **DON'T**: lines, grouped by section headings
|
||||
* Returns { patterns: [...], antipatterns: [...] }
|
||||
*/
|
||||
export function readPatterns(rootDir) {
|
||||
const filePath = path.join(rootDir, 'source/patterns.md');
|
||||
const skillPath = path.join(rootDir, 'source/skills/frontend-design/SKILL.md');
|
||||
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { patterns: [], antipatterns: [], body: '' };
|
||||
if (!fs.existsSync(skillPath)) {
|
||||
return { patterns: [], antipatterns: [] };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const content = fs.readFileSync(skillPath, 'utf-8');
|
||||
const lines = content.split('\n');
|
||||
|
||||
// Split frontmatter and body
|
||||
const frontmatterRegex = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/;
|
||||
const match = content.match(frontmatterRegex);
|
||||
|
||||
if (!match) {
|
||||
return { patterns: [], antipatterns: [], body: content };
|
||||
}
|
||||
|
||||
const [, frontmatterText, body] = match;
|
||||
|
||||
// Parse both patterns and antipatterns sections
|
||||
const patterns = [];
|
||||
const antipatterns = [];
|
||||
const lines = frontmatterText.split('\n');
|
||||
let currentSection = null; // 'patterns' or 'antipatterns'
|
||||
let currentCategory = null;
|
||||
let inItems = false;
|
||||
const patternsMap = {}; // category -> items[]
|
||||
const antipatternsMap = {}; // category -> items[]
|
||||
let currentSection = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const trimmed = line.trim();
|
||||
if (!trimmed) continue;
|
||||
|
||||
const indent = line.length - line.trimStart().length;
|
||||
|
||||
// Top-level section declaration
|
||||
if (indent === 0 && trimmed === 'patterns:') {
|
||||
currentSection = 'patterns';
|
||||
currentCategory = null;
|
||||
inItems = false;
|
||||
continue;
|
||||
}
|
||||
if (indent === 0 && trimmed === 'antipatterns:') {
|
||||
currentSection = 'antipatterns';
|
||||
currentCategory = null;
|
||||
inItems = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// New category starts with "- name:"
|
||||
if (trimmed.startsWith('- name:') && currentSection) {
|
||||
currentCategory = {
|
||||
name: trimmed.slice(7).trim(),
|
||||
items: []
|
||||
};
|
||||
if (currentSection === 'patterns') {
|
||||
patterns.push(currentCategory);
|
||||
} else {
|
||||
antipatterns.push(currentCategory);
|
||||
// Track section headings (### Typography, ### Color & Theme, etc.)
|
||||
if (trimmed.startsWith('### ')) {
|
||||
currentSection = trimmed.slice(4).trim();
|
||||
// Normalize "Color & Theme" to "Color & Contrast" for consistency
|
||||
if (currentSection === 'Color & Theme') {
|
||||
currentSection = 'Color & Contrast';
|
||||
}
|
||||
inItems = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Items array declaration
|
||||
if (trimmed === 'items:' && currentCategory) {
|
||||
inItems = true;
|
||||
// Parse **DO**: lines
|
||||
if (trimmed.startsWith('**DO**:') && currentSection) {
|
||||
const item = trimmed.slice(7).trim();
|
||||
if (!patternsMap[currentSection]) {
|
||||
patternsMap[currentSection] = [];
|
||||
}
|
||||
patternsMap[currentSection].push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Item within items array (indented with "- ")
|
||||
if (trimmed.startsWith('- ') && inItems && currentCategory && indent >= 6) {
|
||||
currentCategory.items.push(trimmed.slice(2).trim());
|
||||
// Parse **DON'T**: lines
|
||||
if (trimmed.startsWith("**DON'T**:") && currentSection) {
|
||||
const item = trimmed.slice(10).trim();
|
||||
if (!antipatternsMap[currentSection]) {
|
||||
antipatternsMap[currentSection] = [];
|
||||
}
|
||||
antipatternsMap[currentSection].push(item);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return { patterns, antipatterns, body: body.trim() };
|
||||
// Convert maps to arrays in consistent order
|
||||
const sectionOrder = ['Typography', 'Color & Contrast', 'Layout & Space', 'Motion', 'Interaction', 'Responsive', 'UX Writing', 'Visual Details'];
|
||||
|
||||
const patterns = [];
|
||||
const antipatterns = [];
|
||||
|
||||
for (const section of sectionOrder) {
|
||||
if (patternsMap[section] && patternsMap[section].length > 0) {
|
||||
patterns.push({ name: section, items: patternsMap[section] });
|
||||
}
|
||||
if (antipatternsMap[section] && antipatternsMap[section].length > 0) {
|
||||
antipatterns.push({ name: section, items: antipatternsMap[section] });
|
||||
}
|
||||
}
|
||||
|
||||
return { patterns, antipatterns };
|
||||
}
|
||||
|
||||
/**
|
||||
* Provider-specific placeholders
|
||||
*/
|
||||
export const PROVIDER_PLACEHOLDERS = {
|
||||
'claude-code': {
|
||||
model: 'Claude',
|
||||
config_file: 'CLAUDE.md',
|
||||
ask_instruction: 'use the AskUserQuestion tool to clarify what you cannot infer.'
|
||||
},
|
||||
'cursor': {
|
||||
model: 'the model',
|
||||
config_file: '.cursorrules',
|
||||
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
|
||||
},
|
||||
'gemini': {
|
||||
model: 'Gemini',
|
||||
config_file: 'GEMINI.md',
|
||||
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
|
||||
},
|
||||
'codex': {
|
||||
model: 'GPT',
|
||||
config_file: 'AGENTS.md',
|
||||
ask_instruction: 'ask the user directly to clarify what you cannot infer.'
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Replace all {{placeholder}} tokens with provider-specific values
|
||||
*/
|
||||
export function replacePlaceholders(content, provider) {
|
||||
const placeholders = PROVIDER_PLACEHOLDERS[provider] || PROVIDER_PLACEHOLDERS['cursor'];
|
||||
|
||||
return content
|
||||
.replace(/\{\{model\}\}/g, placeholders.model)
|
||||
.replace(/\{\{config_file\}\}/g, placeholders.config_file)
|
||||
.replace(/\{\{ask_instruction\}\}/g, placeholders.ask_instruction);
|
||||
}
|
||||
|
||||
// Legacy alias for backward compatibility
|
||||
export function replaceModelPlaceholder(content, provider) {
|
||||
return replacePlaceholders(content, provider);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user