Alexander ef5f5ba0e6
fix(cli): install all 7 skills via uipro init, not just the orchestrator (#362) (#387)
* fix(cli): install all 7 skills via uipro init, not just the orchestrator

`uipro init` rendered only the orchestrator (ui-ux-pro-max) and never
delivered the 6 sibling skills (banner-design, brand, design,
design-system, slides, ui-styling), so users got 1 of 7 skills (#362).

- sync-assets.mjs: bundle the 6 sub-skills into cli/assets/skills/ as
  static copies (source of truth: .claude/skills/), with sync + check
  coverage. Excludes ui-styling/canvas-fonts (~5.8MB of TTF) and
  __pycache__/.pyc cruft — a skill registers from its SKILL.md, not its
  fonts — so the bundle adds ~0.9MB, not ~6.6MB.
- template.ts: after rendering the orchestrator, install each bundled
  sub-skill as a sibling. The skills parent is derived from the
  platform's skillPath (skills/ for most, prompts/ for copilot,
  steering/ for kiro) rather than hardcoded.
- uninstall.ts: remove the sub-skills too.

Verified: check:assets in sync, tsc passes, and a per-platform install
harness delivers all 7 skills to the correct parent dir with no fonts.

Closes #362

* fix(cli): filter excluded files from target side of check:assets

check:assets filtered sourceFiles with isExcludedAssetFile but not
targetFiles, so a stray cli/assets/scripts/__pycache__/*.pyc (generated
by a local Python run) was reported as an "extra asset file" and failed
the gate. Apply the same predicate to targetFiles in both the
dirsToSync and sub-skill loops.

Verified: check:assets now passes with __pycache__/*.pyc present in the
target tree; typecheck passes.

* fix(cli): uninstall from each platform's real skills dir, not hardcoded skills/

removeSkillDir() hardcoded <folder>/skills/<name>, but the installer
places skills under each platform config's skillPath parent — copilot in
.github/prompts/, kiro in .kiro/steering/. So uninstall left those
platforms' skills (orchestrator + sub-skills) behind.

Derive the install parent from loadPlatformConfig(aiType).folderStructure
(same source the installer uses), and keep the legacy <folder>/skills/
cleanup (incl. .shared/) for older installs. Deduped via a Set.

Verified: typecheck passes; an install+uninstall harness removes all 7
skills with zero leftovers for claude (.claude/skills), copilot
(.github/prompts) and kiro (.kiro/steering).

* fix(cli): re-sync bundled sub-skills after #385 stripped ckm- names

#385 merged to main and removed the ckm- prefix from the six
.claude/skills/*/SKILL.md name fields. This branch's bundled copies
under cli/assets/skills/ still carried the old ckm- names, so after the
PR merges with main the source no longer matched the bundle and the
check-asset-sync CI gate failed (stale asset file: skills/*/SKILL.md).

Merge main and regenerate the bundle so cli/assets/skills matches the
current .claude/skills source of truth. check:assets and typecheck pass.
2026-06-25 17:33:23 +07:00

206 lines
4.9 KiB
JavaScript

#!/usr/bin/env node
/**
* Generate CSS variables from design tokens JSON
*
* Usage:
* node generate-tokens.cjs --config tokens.json -o tokens.css
* node generate-tokens.cjs --config tokens.json --format tailwind
*/
const fs = require('fs');
const path = require('path');
/**
* Parse command line arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const options = {
config: null,
output: null,
format: 'css' // css | tailwind
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--config' || args[i] === '-c') {
options.config = args[++i];
} else if (args[i] === '--output' || args[i] === '-o') {
options.output = args[++i];
} else if (args[i] === '--format' || args[i] === '-f') {
options.format = args[++i];
} else if (args[i] === '--help' || args[i] === '-h') {
console.log(`
Usage: node generate-tokens.cjs [options]
Options:
-c, --config <file> Input JSON token file (required)
-o, --output <file> Output file (default: stdout)
-f, --format <type> Output format: css | tailwind (default: css)
-h, --help Show this help
`);
process.exit(0);
}
}
return options;
}
/**
* Resolve token references like {primitive.color.blue.600}
*/
function resolveReference(value, tokens) {
if (typeof value !== 'string' || !value.startsWith('{')) {
return value;
}
const path = value.slice(1, -1).split('.');
let result = tokens;
for (const key of path) {
result = result?.[key];
}
if (result?.$value) {
return resolveReference(result.$value, tokens);
}
return result || value;
}
/**
* Convert token name to CSS variable name
*/
function toCssVarName(path) {
return '--' + path.join('-').replace(/\./g, '-');
}
/**
* Flatten tokens into CSS variables
*/
function flattenTokens(obj, tokens, prefix = [], result = {}) {
for (const [key, value] of Object.entries(obj)) {
const currentPath = [...prefix, key];
if (value && typeof value === 'object') {
if (value.$value !== undefined) {
// This is a token
const cssVar = toCssVarName(currentPath);
const resolvedValue = resolveReference(value.$value, tokens);
result[cssVar] = resolvedValue;
} else {
// Recurse into nested object
flattenTokens(value, tokens, currentPath, result);
}
}
}
return result;
}
/**
* Generate CSS output
*/
function generateCSS(tokens) {
const primitive = flattenTokens(tokens.primitive || {}, tokens, ['primitive']);
const semantic = flattenTokens(tokens.semantic || {}, tokens, []);
const component = flattenTokens(tokens.component || {}, tokens, []);
const darkSemantic = flattenTokens(tokens.dark?.semantic || {}, tokens, []);
let css = `/* Design Tokens - Auto-generated */
/* Do not edit directly - modify tokens.json instead */
/* === PRIMITIVES === */
:root {
${Object.entries(primitive).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
}
/* === SEMANTIC === */
:root {
${Object.entries(semantic).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
}
/* === COMPONENTS === */
:root {
${Object.entries(component).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
}
`;
if (Object.keys(darkSemantic).length > 0) {
css += `
/* === DARK MODE === */
.dark {
${Object.entries(darkSemantic).map(([k, v]) => ` ${k}: ${v};`).join('\n')}
}
`;
}
return css;
}
/**
* Generate Tailwind config output
*/
function generateTailwind(tokens) {
const semantic = flattenTokens(tokens.semantic || {}, tokens, []);
// Extract colors for Tailwind
const colors = {};
for (const [key, value] of Object.entries(semantic)) {
if (key.includes('color')) {
const name = key.replace('--color-', '').replace(/-/g, '.');
colors[name] = `var(${key})`;
}
}
return `// Tailwind color config - Auto-generated
// Add to tailwind.config.ts theme.extend.colors
module.exports = {
colors: ${JSON.stringify(colors, null, 2).replace(/"/g, "'")}
};
`;
}
/**
* Main
*/
function main() {
const options = parseArgs();
if (!options.config) {
console.error('Error: --config is required');
process.exit(1);
}
// Resolve config path
const configPath = path.resolve(process.cwd(), options.config);
if (!fs.existsSync(configPath)) {
console.error(`Error: Config file not found: ${configPath}`);
process.exit(1);
}
// Read and parse tokens
const tokens = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
// Generate output
let output;
if (options.format === 'tailwind') {
output = generateTailwind(tokens);
} else {
output = generateCSS(tokens);
}
// Write output
if (options.output) {
const outputPath = path.resolve(process.cwd(), options.output);
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
fs.writeFileSync(outputPath, output);
console.log(`Generated: ${outputPath}`);
} else {
console.log(output);
}
}
main();