// Secret Sauce installer (https://secretsauce.shinyrock.app) // // curl -fsSL https://secretsauce.shinyrock.app/i | node - // curl -fsSL https://secretsauce.shinyrock.app/i | node - --21st-key=YOUR_KEY (adds the optional 21st connector) // // Installs the design kit for every AI coding tool it finds on this computer (Claude Code, Codex, Cursor, // Gemini CLI, Windsurf, OpenCode, VS Code and the other tools the `skills` CLI knows): // - 32 design skills, downloaded from their authors' own GitHub repos; // - the Magic UI and BoardUI connectors (MCP servers), and 21st when a key is given; // - the Uiverse library and Bencho's blocks, saved once in ~/.secretsauce; // - one line in each tool's global rules file that says to load the `secret-sauce` skill for design work. // Safe to run again: it updates what's there. Needs Node 18+ and `tar` (built into macOS, Linux and Windows 10+). // No git, no accounts, nothing sent anywhere except the downloads. 'use strict'; const fs = require('fs'); const os = require('os'); const path = require('path'); const cp = require('child_process'); const { Readable } = require('stream'); const { pipeline } = require('stream/promises'); const SITE = (process.env.SECRETSAUCE_SITE || 'https://secretsauce.shinyrock.app').replace(/\/$/, ''); const SKILLS_CLI = 'skills@1.7.5'; const HOME = os.homedir(); const WIN = process.platform === 'win32'; const ROOT = path.join(HOME, '.secretsauce'); const argv = process.argv.slice(2); const arg = (k) => { const eq = argv.find((a) => a.startsWith(`--${k}=`)); if (eq) return eq.slice(k.length + 3); const i = argv.indexOf(`--${k}`); return i >= 0 ? argv[i + 1] : undefined; }; const KEY21 = (arg('21st-key') || process.env.TWENTYFIRST_API_KEY || '').trim(); const SOURCES = [ { repo: 'anthropics/skills', by: 'Anthropic', skills: ['frontend-design'] }, { repo: 'nextlevelbuilder/ui-ux-pro-max-skill', by: 'UI UX Pro Max', skills: ['ui-ux-pro-max', 'design-system', 'ui-styling', 'design', 'brand', 'banner-design', 'slides'] }, { repo: 'Leonxlnx/taste-skill', by: 'tasteskill', skills: ['design-taste-frontend'] }, { repo: 'pbakaus/impeccable', by: 'Paul Bakaus', skills: ['impeccable'] }, { repo: 'heydamir/unslop', by: 'Damir Sokolovsky', skills: ['unslop'] }, { repo: 'benjitaylor/skills', by: 'Benji Taylor', skills: ['stageshow'] }, { repo: 'emilkowalski/skills', by: 'Emil Kowalski', skills: ['animate', 'animate-expo', 'animation-performance', 'animation-vocabulary', 'apple-design', 'ask-sonner', 'break-ui', 'emil-design-eng', 'find-animation-opportunities', 'improve-animations', 'mobile-native', 'pick-ui-library', 'prototype', 'remove-animations', 'review-animations', 'write-swift'] }, { repo: 'Appllama/appllama-skills', by: 'Appllama', skills: ['appllama-app-design-skill'] }, ]; const KIT = ['secret-sauce', 'ui-sources', 'bencho']; const LIBRARIES = [ { repo: 'uiverse-io/galaxy', dir: 'uiverse', label: 'Uiverse library' }, { repo: 'cabralorenzo/bencho', dir: 'bencho', label: 'Bencho blocks and sounds' }, ]; // Connectors. On Windows, tools start npx through cmd. const npxCmd = (args) => (WIN ? { command: 'cmd', args: ['/c', 'npx', ...args] } : { command: 'npx', args }); const SERVERS = [ { name: 'magicui', label: 'Magic UI', ...npxCmd(['-y', '@magicuidesign/mcp@latest']) }, { name: 'boardui', label: 'BoardUI', ...npxCmd(['-y', 'boardui@latest', 'mcp']) }, ]; if (KEY21) SERVERS.push({ name: '21st', label: '21st', url: 'https://21st.dev/api/mcp', headers: { 'x-api-key': KEY21 } }); const RULE_START = ''; const RULE = `${RULE_START}\nFor any design or UI work, load the \`secret-sauce\` skill first and follow it.\n\n`; const needs = []; // things only the person can do, one line each const notes = []; // things worth knowing, one line each const done = { skills: 0, skillTools: new Set(), servers: {}, rules: [], libs: [] }; const say = (s) => process.stdout.write(s + '\n'); const tilde = (p) => (p.startsWith(HOME) ? '~' + p.slice(HOME.length) : p); // ---------- small helpers ---------- function which(cmd) { const dirs = (process.env.PATH || '').split(path.delimiter); const exts = WIN ? ['.cmd', '.exe', '.bat', ''] : ['']; for (const d of dirs) for (const e of exts) { const f = path.join(d, cmd + e); try { if (fs.statSync(f).isFile()) return f; } catch { /* keep looking */ } } return null; } function run(cmd, args, opts = {}) { const r = cp.spawnSync(cmd, args, { encoding: 'utf8', shell: WIN && /\.(cmd|bat)$/i.test(cmd), timeout: opts.timeout || 600000, env: { ...process.env, DISABLE_TELEMETRY: '1', DO_NOT_TRACK: '1', npm_config_yes: 'true', CI: '1', ...(opts.env || {}) }, cwd: opts.cwd, }); const out = ((r.stdout || '') + (r.stderr || '')).replace(/\x1b\[[0-9;?]*[A-Za-z]/g, ''); return { ok: r.status === 0, out, error: r.error }; } async function download(url, file, tries = 3) { for (let i = 1; ; i++) { try { const res = await fetch(url, { redirect: 'follow' }); if (!res.ok) throw new Error(`HTTP ${res.status}`); await pipeline(Readable.fromWeb(res.body), fs.createWriteStream(file)); return; } catch (e) { if (i >= tries) throw new Error(`${url}: ${e.message}`); await new Promise((r) => setTimeout(r, 1500 * i)); } } } async function fetchRepo(repo, dest, tmp) { const tgz = path.join(tmp, repo.replace('/', '__') + '.tgz'); await download(`https://codeload.github.com/${repo}/tar.gz/HEAD`, tgz); fs.rmSync(dest, { recursive: true, force: true }); fs.mkdirSync(dest, { recursive: true }); const t = run('tar', ['-xzf', tgz, '-C', dest, '--strip-components=1']); fs.rmSync(tgz, { force: true }); if (!t.ok) throw new Error(`couldn't unpack ${repo}: ${t.out.trim().split('\n').pop()}`); } function readJSON(file) { if (!fs.existsSync(file)) return {}; const text = fs.readFileSync(file, 'utf8'); if (!text.trim()) return {}; return JSON.parse(text); // a file with comments throws, and is then left alone } function backupOnce(file) { const b = file + '.before-secretsauce'; if (fs.existsSync(file) && !fs.existsSync(b)) fs.copyFileSync(file, b); } function writeJSON(file, data) { backupOnce(file); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, JSON.stringify(data, null, 2) + '\n'); } const exists = (...p) => fs.existsSync(path.join(...p)); const xdg = process.env.XDG_CONFIG_HOME || path.join(HOME, '.config'); const appData = process.env.APPDATA || path.join(HOME, 'AppData', 'Roaming'); const codexHome = process.env.CODEX_HOME || path.join(HOME, '.codex'); const vscodeUser = process.platform === 'darwin' ? path.join(HOME, 'Library', 'Application Support', 'Code', 'User') : WIN ? path.join(appData, 'Code', 'User') : path.join(xdg, 'Code', 'User'); // ---------- which AI tools are on this computer ---------- function findClaude() { const onPath = which('claude'); if (onPath) return onPath; const spots = [path.join(HOME, '.local', 'bin', 'claude'), path.join(HOME, '.claude', 'local', 'claude'), '/opt/homebrew/bin/claude', '/usr/local/bin/claude']; for (const vm of ['claude-code-vm', 'claude-code']) { const d = path.join(HOME, 'Library', 'Application Support', 'Claude', vm); try { fs.readdirSync(d).sort((a, b) => b.localeCompare(a, undefined, { numeric: true })) .forEach((v) => spots.push(path.join(d, v, 'claude'))); } catch { /* not the desktop app */ } } return spots.find((f) => fs.existsSync(f)) || null; } const TOOLS = [ { id: 'claude', label: 'Claude Code', found: () => exists(HOME, '.claude') || exists(HOME, '.claude.json') || !!which('claude'), rules: path.join(HOME, '.claude', 'CLAUDE.md'), mcp: mcpClaude }, { id: 'codex', label: 'Codex', found: () => fs.existsSync(codexHome) || !!which('codex'), rules: path.join(codexHome, 'AGENTS.md'), mcp: mcpCodex }, { id: 'cursor', label: 'Cursor', found: () => exists(HOME, '.cursor'), mcp: (s) => mcpJSON(path.join(HOME, '.cursor', 'mcp.json'), 'mcpServers', s, cursorEntry) }, { id: 'gemini', label: 'Gemini CLI', found: () => exists(HOME, '.gemini', 'settings.json') || !!which('gemini'), rules: path.join(HOME, '.gemini', 'GEMINI.md'), mcp: (s) => mcpJSON(path.join(HOME, '.gemini', 'settings.json'), 'mcpServers', s, geminiEntry) }, { id: 'windsurf', label: 'Windsurf', found: () => exists(HOME, '.codeium', 'windsurf'), rules: path.join(HOME, '.codeium', 'windsurf', 'memories', 'global_rules.md'), mcp: (s) => mcpJSON(path.join(HOME, '.codeium', 'windsurf', 'mcp_config.json'), 'mcpServers', s, windsurfEntry) }, { id: 'opencode', label: 'OpenCode', found: () => fs.existsSync(path.join(xdg, 'opencode')), rules: path.join(xdg, 'opencode', 'AGENTS.md'), mcp: mcpOpenCode }, { id: 'vscode', label: 'VS Code', found: () => fs.existsSync(vscodeUser), mcp: (s) => mcpJSON(path.join(vscodeUser, 'mcp.json'), 'servers', s, vscodeEntry) }, ]; // Where each AI tool keeps its settings, as the `skills` CLI checks it. Skills go only to the tools found here // (left to itself, the CLI installs into all 78 tools it knows when it finds none). const claudeHome = process.env.CLAUDE_CONFIG_DIR || path.join(HOME, '.claude'); const SKILL_AGENTS = [ ['claude-code', claudeHome], ['codex', codexHome], ['cursor', path.join(HOME, '.cursor')], ['gemini-cli', path.join(HOME, '.gemini')], ['windsurf', path.join(HOME, '.codeium', 'windsurf')], ['opencode', path.join(xdg, 'opencode')], ['github-copilot', path.join(HOME, '.copilot')], ['cline', path.join(HOME, '.cline')], ['amp', path.join(xdg, 'amp')], ['augment', path.join(HOME, '.augment')], ['continue', path.join(HOME, '.continue')], ['crush', path.join(HOME, '.config', 'crush')], ['droid', path.join(HOME, '.factory')], ['goose', path.join(xdg, 'goose')], ['junie', path.join(HOME, '.junie')], ['kilo', path.join(HOME, '.kilocode')], ['kilo', path.join(HOME, '.kilo')], ['kiro-cli', path.join(HOME, '.kiro')], ['openhands', path.join(HOME, '.openhands')], ['qwen-code', path.join(HOME, '.qwen')], ['roo', path.join(HOME, '.roo')], ['trae', path.join(HOME, '.trae')], ['warp', path.join(HOME, '.warp')], ['zed', path.join(xdg, 'zed')], ['kimi-code-cli', path.join(HOME, '.kimi')], ['qoder', path.join(HOME, '.qoder')], ['tabnine-cli', path.join(HOME, '.tabnine')], ['rovodev', path.join(HOME, '.rovodev')], ['zencoder', path.join(HOME, '.zencoder')], ['pi', path.join(HOME, '.pi', 'agent')], ['antigravity', path.join(HOME, '.gemini', 'antigravity')], ]; function skillAgents(tools) { const ids = new Set(SKILL_AGENTS.filter(([, dir]) => fs.existsSync(dir)).map(([id]) => id)); const map = { claude: 'claude-code', codex: 'codex', cursor: 'cursor', gemini: 'gemini-cli', windsurf: 'windsurf', opencode: 'opencode', vscode: 'github-copilot' }; for (const t of tools) ids.add(map[t.id]); return [...ids]; } // ---------- connector writers, one per tool ---------- const cursorEntry = (s) => (s.url ? { url: s.url, headers: s.headers } : { command: s.command, args: s.args }); const geminiEntry = (s) => (s.url ? { httpUrl: s.url, headers: s.headers } : { command: s.command, args: s.args }); const windsurfEntry = (s) => (s.url ? { serverUrl: s.url, headers: s.headers } : { command: s.command, args: s.args }); const vscodeEntry = (s) => (s.url ? { type: 'http', url: s.url, headers: s.headers } : { type: 'stdio', command: s.command, args: s.args }); function mcpJSON(file, key, servers, entry) { const cfg = readJSON(file); cfg[key] = cfg[key] || {}; for (const s of servers) cfg[key][s.name] = entry(s); writeJSON(file, cfg); } function mcpOpenCode(servers) { const file = ['opencode.json', 'opencode.jsonc'].map((f) => path.join(xdg, 'opencode', f)).find((f) => fs.existsSync(f)) || path.join(xdg, 'opencode', 'opencode.json'); const cfg = readJSON(file); if (!cfg.$schema) cfg.$schema = 'https://opencode.ai/config.json'; cfg.mcp = cfg.mcp || {}; for (const s of servers) { cfg.mcp[s.name] = s.url ? { type: 'remote', url: s.url, headers: s.headers, enabled: true } : { type: 'local', command: [s.command, ...s.args], enabled: true }; } writeJSON(file, cfg); } function mcpClaude(servers) { const bin = findClaude(); const left = []; for (const s of servers) { if (!bin) { left.push(s); continue; } run(bin, ['mcp', 'remove', '--scope', 'user', s.name], { timeout: 60000 }); const args = s.url ? ['mcp', 'add', '--scope', 'user', '--transport', 'http', s.name, s.url, ...Object.entries(s.headers).flatMap(([k, v]) => ['--header', `${k}: ${v}`])] : ['mcp', 'add', '--scope', 'user', s.name, '--', s.command, ...s.args]; if (!run(bin, args, { timeout: 60000 }).ok) left.push(s); } if (!left.length) return; // No `claude` command to call: write the same entries it would write. const file = path.join(HOME, '.claude.json'); const cfg = readJSON(file); cfg.mcpServers = cfg.mcpServers || {}; for (const s of left) cfg.mcpServers[s.name] = s.url ? { type: 'http', url: s.url, headers: s.headers } : { type: 'stdio', command: s.command, args: s.args, env: {} }; writeJSON(file, cfg); } function mcpCodex(servers) { const file = path.join(codexHome, 'config.toml'); let text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''; const q = (v) => JSON.stringify(v); for (const s of servers) { // Drop an older copy of this server's table, then add the current one. const head = new RegExp(`^\\[mcp_servers\\.(?:${s.name}|"${s.name}")\\]\\s*$`, 'm'); const m = text.match(head); if (m) { const start = m.index; const rest = text.slice(start + m[0].length); const next = rest.search(/^\[/m); text = text.slice(0, start) + (next >= 0 ? rest.slice(next) : ''); } const lines = [`[mcp_servers.${s.name}]`]; if (s.url) { lines.push(`url = ${q(s.url)}`, `http_headers = { ${Object.entries(s.headers).map(([k, v]) => `${q(k)} = ${q(v)}`).join(', ')} }`); } else { lines.push(`command = ${q(s.command)}`, `args = [${s.args.map(q).join(', ')}]`); } text = text.replace(/\s*$/, '') + (text.trim() ? '\n\n' : '') + lines.join('\n') + '\n'; } backupOnce(file); fs.mkdirSync(codexHome, { recursive: true }); fs.writeFileSync(file, text); } function addRule(file) { const text = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''; if (text.includes(RULE_START)) return false; fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, text + (text && !text.endsWith('\n') ? '\n' : '') + (text ? '\n' : '') + RULE); return true; } // ---------- the install ---------- async function main() { say('Secret Sauce: installing the design kit…'); const major = Number(process.versions.node.split('.')[0]); if (major < 18) { say(`\nNeeds you:\n - Node.js is ${process.versions.node}; this needs 18 or newer. Install the LTS version from https://nodejs.org, then run this again.`); process.exit(1); } if (!run('tar', ['--version']).ok) { say('\nNeeds you:\n - The `tar` tool is missing. Update the operating system (it ships with macOS, Linux and Windows 10+), then run this again.'); process.exit(1); } const npx = which('npx'); if (!npx) { say('\nNeeds you:\n - `npx` is missing. Reinstall Node.js from https://nodejs.org (npx comes with it), then run this again.'); process.exit(1); } const tools = TOOLS.filter((t) => { try { return t.found(); } catch { return false; } }); const agents = skillAgents(tools); if (!agents.length) { say('\nNeeds you:\n - No AI coding tool was found on this computer. Install one (Claude Code, Codex, Cursor…), open it once, then run this again.'); process.exit(1); } const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'secretsauce-')); fs.mkdirSync(ROOT, { recursive: true }); // 1. Download everything at once: skill repos, the kit's own skills, the libraries. say(' downloading skills and libraries'); const kitDir = path.join(tmp, 'kit'); const jobs = [ ...SOURCES.map(async (s) => { s.dir = path.join(tmp, 'src', s.repo.replace('/', '__')); try { await fetchRepo(s.repo, s.dir, tmp); } catch (e) { s.error = e.message; } }), ...KIT.map(async (k) => { fs.mkdirSync(path.join(kitDir, k), { recursive: true }); try { await download(`${SITE}/kit/${k}/SKILL.md`, path.join(kitDir, k, 'SKILL.md')); } catch (e) { notes.push(`couldn't download the ${k} skill (${e.message})`); fs.rmSync(path.join(kitDir, k), { recursive: true, force: true }); } }), ...LIBRARIES.map(async (l) => { try { await fetchRepo(l.repo, path.join(ROOT, l.dir), tmp); done.libs.push(l); } catch (e) { notes.push(`couldn't download the ${l.label} (${e.message}); run this again later`); } }), ]; await Promise.all(jobs); // 2. Install the skills with the open `skills` CLI, which knows where each AI tool keeps them. const groups = [...SOURCES.filter((s) => !s.error), ...(fs.existsSync(kitDir) && fs.readdirSync(kitDir).length ? [{ by: 'Secret Sauce', dir: kitDir, skills: fs.readdirSync(kitDir) }] : [])]; for (const s of SOURCES.filter((x) => x.error)) notes.push(`couldn't download ${s.repo} (${s.error}); run this again later`); let first = true; for (const g of groups) { say(` installing ${g.skills.length} skill${g.skills.length > 1 ? 's' : ''} by ${g.by}${first ? ' (the first one takes a moment)' : ''}`); first = false; const r = run(npx, ['-y', SKILLS_CLI, 'add', g.dir, '-g', '-y', '--copy', '-a', ...agents, '-s', ...g.skills], { cwd: HOME }); const to = r.out.match(/Installing to:\s*([^\n]+)/); if (to) to[1].split(',').map((x) => x.trim()).filter(Boolean).forEach((x) => done.skillTools.add(x)); const got = (r.out.match(/✓ /g) || []).length; if (r.ok) done.skills += got || g.skills.length; else notes.push(`skills by ${g.by} didn't install: ${(r.out.trim().split('\n').filter((l) => l.trim()).pop() || r.error?.message || 'unknown error').trim()}`); } // 3. Connectors and the rules line, for each tool found. for (const t of tools) { if (t.mcp) { try { t.mcp(SERVERS); done.servers[t.label] = true; } catch (e) { notes.push(`couldn't add the connectors to ${t.label} (${e instanceof SyntaxError ? 'its settings file has comments or is damaged; add them by hand from ' + SITE + '/install.md' : e.message})`); } } if (t.rules) { try { if (addRule(t.rules) || fs.readFileSync(t.rules, 'utf8').includes(RULE_START)) done.rules.push(tilde(t.rules)); } catch (e) { notes.push(`couldn't add the rules line to ${tilde(t.rules)} (${e.message})`); } } } fs.rmSync(tmp, { recursive: true, force: true }); // 4. Report. const count = (dir) => { try { return fs.readdirSync(dir, { withFileTypes: true }).filter((d) => d.isDirectory() && !d.name.startsWith('.')).reduce((n, d) => n + fs.readdirSync(path.join(dir, d.name)).filter((f) => f.endsWith('.html')).length, 0); } catch { return 0; } }; const uiverse = count(path.join(ROOT, 'uiverse')); const toolNames = [...new Set([...done.skillTools, ...tools.map((t) => t.label)])]; say(''); if (!done.skills) { say('Secret Sauce did not install.'); } else { const expected = SOURCES.reduce((n, s) => n + s.skills.length, 0) + KIT.length; say(done.skills >= expected ? 'Secret Sauce is installed.' : 'Secret Sauce is partly installed.'); say(` ${done.skills} design skills${toolNames.length ? ' for ' + toolNames.join(', ') : ''}`); const withServers = Object.keys(done.servers); if (withServers.length) say(` connectors ${SERVERS.map((s) => s.label).join(', ')} in ${withServers.join(', ')}`); if (uiverse) say(` ${uiverse.toLocaleString('en-US')} Uiverse pieces and Bencho's blocks in ~/.secretsauce`); if (done.rules.length) say(` rule added to ${done.rules.join(', ')}`); } if (notes.length) { say('\nProblems:'); notes.forEach((n) => say(' - ' + n)); } if (needs.length) { say('\nNeeds you:'); needs.forEach((n) => say(' - ' + n)); } if (!KEY21 && done.skills) { say('\nOptional: the 21st connector (component search and real brand logos) needs a free key from https://21st.dev/mcp. With it, run:'); say(` curl -fsSL ${SITE}/i | node - --21st-key=YOUR_KEY`); } if (done.skills) say('\nOpen a new chat to use it.'); process.exit(done.skills && !notes.length && !needs.length ? 0 : 1); } main().catch((e) => { say(`\nSecret Sauce stopped: ${e.message}`); say('Run it again. If it stops the same way, the details above say why.'); process.exit(1); });