1/**
2 * `impeccable skills` subcommand
3 *
4 * Usage:
5 * impeccable skills help Show all available skills and commands
6 * impeccable skills install Install skills via npx skills add
7 * impeccable skills update Update skills to latest version
8 */
9
10import { execSync } from 'node:child_process';
11import { existsSync, readFileSync, readdirSync, statSync, lstatSync, symlinkSync, readlinkSync, unlinkSync, mkdirSync, writeFileSync, rmSync, renameSync, createWriteStream, realpathSync } from 'node:fs';
12import { join, resolve, dirname } from 'node:path';
13import { createInterface } from 'node:readline';
14import { fileURLToPath } from 'node:url';
15import { get } from 'node:https';
16import { createHash } from 'node:crypto';
17import { tmpdir } from 'node:os';
18
19const __dirname = dirname(fileURLToPath(import.meta.url));
20const API_BASE = 'https://impeccable.style';
21
22// Provider folder names in project roots
23const PROVIDER_DIRS = ['.claude', '.cursor', '.gemini', '.codex', '.agents', '.kiro', '.opencode', '.pi', '.trae', '.trae-cn'];
24
25function ask(question) {
26 const rl = createInterface({ input: process.stdin, output: process.stdout });
27 return new Promise(r => rl.question(question, ans => { rl.close(); r(ans.trim().toLowerCase()); }));
28}
29
30// ─── skills help ──────────────────────────────────────────────────────────────
31
32async function showHelp() {
33 let commands;
34 try {
35 const res = await fetch(`${API_BASE}/api/commands`);
36 commands = await res.json();
37 } catch {
38 console.error('Could not fetch command list from impeccable.style. Check your network connection.');
39 process.exit(1);
40 }
41
42 const pad = (s, n) => s + ' '.repeat(Math.max(0, n - s.length));
43
44 console.log('\n Impeccable Skills & Commands\n');
45 console.log(' Install: npx impeccable skills install');
46 console.log(' Update: npx impeccable skills update');
47 console.log(' Docs: https://impeccable.style/cheatsheet\n');
48 console.log(` ${pad('Command', 22)} Description`);
49 console.log(` ${'-'.repeat(22)} ${'-'.repeat(52)}`);
50
51 for (const cmd of commands.sort((a, b) => a.id.localeCompare(b.id))) {
52 // Trim description to fit terminal
53 const desc = cmd.description.length > 72
54 ? cmd.description.substring(0, 69) + '...'
55 : cmd.description;
56 console.log(` ${pad('/' + cmd.id, 22)} ${desc}`);
57 }
58 console.log(`\n ${commands.length} commands available. Run /<command> in your AI harness.\n`);
59}
60
61// ─── version helpers ─────────────────────────────────────────────────────────
62
63/**
64 * Read the skills version from the impeccable SKILL.md frontmatter.
65 */
66function getSkillsVersion(root) {
67 for (const d of PROVIDER_DIRS) {
68 const skillMd = join(root, d, 'skills', 'impeccable', 'SKILL.md');
69 if (!existsSync(skillMd)) continue;
70 const content = readFileSync(skillMd, 'utf-8');
71 const match = content.match(/^version:\s*(.+)$/m);
72 if (match) return match[1].trim().replace(/^["']|["']$/g, '');
73 }
74 return null;
75}
76
77/**
78 * Hash all SKILL.md files in a directory tree for comparison.
79 * Returns a sorted string of "name:hash" pairs.
80 */
81function hashSkillsDir(skillsDir) {
82 if (!existsSync(skillsDir)) return '';
83 const entries = [];
84 for (const name of readdirSync(skillsDir).sort()) {
85 const skillMd = join(skillsDir, name, 'SKILL.md');
86 if (!existsSync(skillMd)) continue;
87 const hash = createHash('sha256').update(readFileSync(skillMd)).digest('hex').slice(0, 12);
88 entries.push(`${name}:${hash}`);
89 }
90 return entries.join(',');
91}
92
93/**
94 * Download the universal bundle to a temp dir and return its path.
95 * Caller is responsible for cleanup.
96 */
97async function downloadAndExtractBundle() {
98 const tmpZip = join(tmpdir(), `impeccable-update-${Date.now()}.zip`);
99 const tmpDir = join(tmpdir(), `impeccable-update-${Date.now()}`);
100 await downloadFile(`${API_BASE}/api/download/bundle/universal`, tmpZip);
101 mkdirSync(tmpDir, { recursive: true });
102 execSync(`unzip -qo "${tmpZip}" -d "${tmpDir}"`, { encoding: 'utf8' });
103 rmSync(tmpZip, { force: true });
104 return tmpDir;
105}
106
107/**
108 * Normalize a SKILL.md's content for comparison by stripping
109 * provider-specific paths. Different install methods (npx skills add
110 * vs our bundle) resolve {{scripts_path}} to different provider dirs
111 * (e.g. .agents vs .claude), so we strip those differences.
112 */
113function normalizeForHash(content) {
114 return content
115 .replace(/\.(claude|cursor|agents|gemini|codex|kiro|opencode|pi|trae|trae-cn|rovodev)\/skills\//g, '.PROVIDER/skills/')
116 .replace(/^version:\s*.+$/m, 'version: NORMALIZED');
117}
118
119/**
120 * Deduplicate providers by resolved path. When .claude/skills is a
121 * symlink to ../.agents/skills, both resolve to the same directory.
122 * Returns an array of { provider, localSkillsDir } with one entry
123 * per unique real path. The first provider that maps to a real path
124 * wins (so the bundle uses that provider's build).
125 */
126function deduplicateProviders(root, providers) {
127 const seen = new Map(); // realPath -> { provider, localSkillsDir }
128 for (const provider of providers) {
129 const skillsDir = join(root, provider, 'skills');
130 if (!existsSync(skillsDir)) continue;
131 const real = realpathSync(skillsDir);
132 if (!seen.has(real)) {
133 seen.set(real, { provider, localSkillsDir: skillsDir });
134 }
135 }
136 return [...seen.values()];
137}
138
139/**
140 * Compare local skills against a downloaded bundle.
141 * Only checks skills that exist in the bundle (ignores user's custom
142 * skills that aren't part of impeccable). Deduplicates providers that
143 * share the same real path (symlinks). Normalizes provider-specific
144 * paths and version fields before comparing.
145 * Returns true if every bundle skill matches the local copy.
146 */
147function isUpToDate(root, providers, bundleDir) {
148 const unique = deduplicateProviders(root, providers);
149 if (unique.length === 0) return false;
150
151 for (const { provider, localSkillsDir } of unique) {
152 const bundleSkillsDir = join(bundleDir, provider, 'skills');
153 if (!existsSync(bundleSkillsDir)) continue;
154
155 for (const name of readdirSync(bundleSkillsDir)) {
156 const bundleMd = join(bundleSkillsDir, name, 'SKILL.md');
157 const localMd = join(localSkillsDir, name, 'SKILL.md');
158 if (!existsSync(bundleMd)) continue;
159 if (!existsSync(localMd)) return false;
160
161 const bundleHash = createHash('sha256').update(normalizeForHash(readFileSync(bundleMd, 'utf-8'))).digest('hex');
162 const localHash = createHash('sha256').update(normalizeForHash(readFileSync(localMd, 'utf-8'))).digest('hex');
163 if (bundleHash !== localHash) return false;
164 }
165 }
166 return true;
167}
168
169// ─── skills check ────────────────────────────────────────────────────────────
170
171async function check() {
172 const root = findProjectRoot();
173 const installed = isAlreadyInstalled(root);
174
175 if (!installed) {
176 console.log('Impeccable is not installed in this project.');
177 console.log('Run `npx impeccable skills install` to install.');
178 process.exit(0);
179 }
180
181 const providers = findInstalledProviders(root);
182
183 console.log('Checking for updates...\n');
184 try {
185 const bundleDir = await downloadAndExtractBundle();
186 const upToDate = isUpToDate(root, providers, bundleDir);
187 rmSync(bundleDir, { recursive: true, force: true });
188
189 if (upToDate) {
190 const v = getSkillsVersion(root);
191 console.log(`Skills are up to date${v ? ` (v${v})` : ''}.`);
192 } else {
193 console.log('Updates available.');
194 console.log('Run `npx impeccable skills update` to update.');
195 }
196 } catch (e) {
197 console.error(`Could not check for updates: ${e.message}`);
198 process.exit(1);
199 }
200}
201
202// ─── skills install ───────────────────────────────────────────────────────────
203
204// Check if impeccable skills are already present in any provider folder
205function isAlreadyInstalled(root) {
206 for (const d of PROVIDER_DIRS) {
207 const skillsDir = join(root, d, 'skills');
208 if (!existsSync(skillsDir)) continue;
209 try {
210 const entries = readdirSync(skillsDir);
211 // Look for 'impeccable' skill (or prefixed variant, or legacy 'teach-impeccable')
212 if (entries.some(e =>
213 e === 'impeccable' || e.endsWith('-impeccable') ||
214 e === 'teach-impeccable' || e.endsWith('-teach-impeccable')
215 )) {
216 return d;
217 }
218 } catch {}
219 }
220 return null;
221}
222
223function escapeRegex(str) {
224 return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
225}
226
227function prefixSkillContent(content, prefix, allSkillNames) {
228 // Prefix the name in frontmatter
229 let result = content.replace(/^name:\s*(.+)$/m, (_, name) => `name: ${prefix}${name.trim()}`);
230
231 // Prefix cross-references: /skillname -> /prefix-skillname
232 const sorted = [...allSkillNames].sort((a, b) => b.length - a.length);
233 for (const name of sorted) {
234 // Command invocations: /skillname
235 result = result.replace(
236 new RegExp(`/(?=${escapeRegex(name)}(?:[^a-zA-Z0-9_-]|$))`, 'g'),
237 `/${prefix}`
238 );
239 // Prose references: "the skillname skill"
240 result = result.replace(
241 new RegExp(`(the) ${escapeRegex(name)} skill`, 'gi'),
242 (_, article) => `${article} ${prefix}${name} skill`
243 );
244 }
245 return result;
246}
247
248function isSkillDir(skillsDir, name) {
249 // Skill entries can be real directories or symlinks to directories (npx skills uses symlinks)
250 const full = join(skillsDir, name);
251 try {
252 return statSync(full).isDirectory() && existsSync(join(full, 'SKILL.md'));
253 } catch { return false; }
254}
255
256function isRealSkillDir(skillsDir, name) {
257 // Only real directories, not symlinks -- renaming the real dir renames the symlink targets too
258 const full = join(skillsDir, name);
259 try {
260 const lstat = lstatSync(full);
261 return lstat.isDirectory() && !lstat.isSymbolicLink() && existsSync(join(full, 'SKILL.md'));
262 } catch { return false; }
263}
264
265function renameSkillsWithPrefix(root, prefix) {
266 // First pass: collect all skill names across all providers (use first provider found)
267 let allSkillNames = [];
268 for (const d of PROVIDER_DIRS) {
269 const skillsDir = join(root, d, 'skills');
270 if (!existsSync(skillsDir)) continue;
271 const entries = readdirSync(skillsDir);
272 allSkillNames = entries.filter(name => isSkillDir(skillsDir, name));
273 if (allSkillNames.length > 0) break;
274 }
275
276 // Second pass: rename real dirs and update their content
277 let count = 0;
278 for (const d of PROVIDER_DIRS) {
279 const skillsDir = join(root, d, 'skills');
280 if (!existsSync(skillsDir)) continue;
281 try {
282 const entries = readdirSync(skillsDir);
283 for (const name of entries) {
284 if (name.startsWith(prefix)) continue;
285 if (!isRealSkillDir(skillsDir, name)) continue;
286
287 const src = join(skillsDir, name);
288 const dest = join(skillsDir, prefix + name);
289
290 renameSync(src, dest);
291
292 // Prefix frontmatter name + all cross-references in SKILL.md
293 let content = readFileSync(join(dest, 'SKILL.md'), 'utf8');
294 content = prefixSkillContent(content, prefix, allSkillNames);
295 writeFileSync(join(dest, 'SKILL.md'), content);
296 count++;
297 }
298 } catch {}
299 }
300
301 // Third pass: fix symlinks that now point to renamed targets (npx skills uses these)
302 for (const d of PROVIDER_DIRS) {
303 const skillsDir = join(root, d, 'skills');
304 if (!existsSync(skillsDir)) continue;
305 try {
306 const entries = readdirSync(skillsDir);
307 for (const name of entries) {
308 if (name.startsWith(prefix)) continue;
309 const full = join(skillsDir, name);
310 try {
311 if (!lstatSync(full).isSymbolicLink()) continue;
312 const target = readlinkSync(full);
313 const newTarget = target.replace(new RegExp(`/${escapeRegex(name)}$`), `/${prefix}${name}`);
314 unlinkSync(full);
315 symlinkSync(newTarget, join(skillsDir, prefix + name));
316 } catch {}
317 }
318 } catch {}
319 }
320
321 return count;
322}
323
324async function install(flags) {
325 const force = flags.includes('--force');
326 const yes = flags.includes('-y') || flags.includes('--yes');
327 const prefixFlag = flags.find(f => f.startsWith('--prefix='));
328 const root = findProjectRoot();
329 const existing = isAlreadyInstalled(root);
330
331 if (existing && !force) {
332 console.log(`Impeccable skills are already installed (found in ${existing}/).`);
333 console.log('Run with --force to reinstall.\n');
334 process.exit(0);
335 }
336
337 console.log('Installing impeccable skills via npx skills...\n');
338 try {
339 execSync(`npx skills add pbakaus/impeccable${yes ? ' -y' : ''}`, { stdio: 'inherit' });
340 } catch (e) {
341 process.exit(e.status ?? 1);
342 }
343
344 // Ask about prefixing (skip in CI mode unless --prefix= is set)
345 let prefix = '';
346 if (prefixFlag) {
347 prefix = prefixFlag.split('=')[1] || 'i-';
348 } else if (!yes) {
349 console.log();
350 const wantPrefix = await ask('Prefix commands to avoid conflicts? e.g. /i-audit instead of /audit (y/N) ');
351 if (wantPrefix === 'y' || wantPrefix === 'yes') {
352 const custom = await ask('Prefix (default: i-): ');
353 prefix = custom || 'i-';
354 }
355 }
356
357 if (prefix) {
358 const count = renameSkillsWithPrefix(root, prefix);
359 if (count > 0) {
360 console.log(`\nRenamed ${count} skills with "${prefix}" prefix.`);
361 console.log(`Commands are now available as /${prefix}<command> (e.g. /${prefix}audit).`);
362 }
363 }
364
365 // Clean up deprecated skills from previous versions
366 try {
367 const { cleanup } = await import('../../source/skills/impeccable/scripts/cleanup-deprecated.mjs');
368 const result = cleanup(root);
369 const total = result.deletedPaths.length + result.removedLockEntries.length;
370 if (total > 0) {
371 console.log(`Cleaned up ${total} deprecated skill(s) from previous versions.`);
372 }
373 } catch {
374 // Cleanup script not available -- skip
375 }
376
377 console.log(`\nDone! Run /${prefix}impeccable teach in your AI harness to set up design context.\n`);
378}
379
380/** Detect prefix by looking for the 'impeccable' skill (or legacy 'teach-impeccable') */
381function detectPrefix(root) {
382 for (const d of PROVIDER_DIRS) {
383 const skillsDir = join(root, d, 'skills');
384 if (!existsSync(skillsDir)) continue;
385 for (const name of readdirSync(skillsDir)) {
386 if (name === 'impeccable') return '';
387 if (name.endsWith('-impeccable') && name !== 'teach-impeccable') return name.slice(0, -'impeccable'.length);
388 // Legacy fallback
389 if (name === 'teach-impeccable') return '';
390 if (name.endsWith('-teach-impeccable')) return name.slice(0, -'teach-impeccable'.length);
391 }
392 }
393 return '';
394}
395
396/** Undo prefixing: rename folders back and strip prefix from SKILL.md content */
397function undoPrefix(root, prefix) {
398 if (!prefix) return;
399 // Collect the unprefixed names (strip our prefix)
400 let allPrefixedNames = [];
401 for (const d of PROVIDER_DIRS) {
402 const skillsDir = join(root, d, 'skills');
403 if (!existsSync(skillsDir)) continue;
404 allPrefixedNames = readdirSync(skillsDir).filter(n => n.startsWith(prefix) && isRealSkillDir(skillsDir, n));
405 if (allPrefixedNames.length > 0) break;
406 }
407 const unprefixedNames = allPrefixedNames.map(n => n.slice(prefix.length));
408
409 for (const d of PROVIDER_DIRS) {
410 const skillsDir = join(root, d, 'skills');
411 if (!existsSync(skillsDir)) continue;
412 for (const name of readdirSync(skillsDir)) {
413 if (!name.startsWith(prefix)) continue;
414 const unprefixed = name.slice(prefix.length);
415 const src = join(skillsDir, name);
416 const dest = join(skillsDir, unprefixed);
417
418 if (lstatSync(src).isSymbolicLink()) {
419 const target = readlinkSync(src);
420 const newTarget = target.replace(`/${name}`, `/${unprefixed}`);
421 unlinkSync(src);
422 symlinkSync(newTarget, dest);
423 } else {
424 renameSync(src, dest);
425 // Strip prefix from SKILL.md content
426 const skillMd = join(dest, 'SKILL.md');
427 if (existsSync(skillMd)) {
428 let content = readFileSync(skillMd, 'utf8');
429 // Reverse the prefixing: replace prefixed names with unprefixed
430 content = content.replace(new RegExp(`^name:\\s*${escapeRegex(prefix)}`, 'm'), 'name: ');
431 const sorted = [...allPrefixedNames].sort((a, b) => b.length - a.length);
432 for (const pName of sorted) {
433 const uName = pName.slice(prefix.length);
434 content = content.replace(new RegExp(`/${escapeRegex(pName)}(?=[^a-zA-Z0-9_-]|$)`, 'g'), `/${uName}`);
435 content = content.replace(new RegExp(`(the) ${escapeRegex(pName)} skill`, 'gi'), `$1 ${uName} skill`);
436 }
437 writeFileSync(skillMd, content);
438 }
439 }
440 }
441 }
442}
443
444// ─── skills update ────────────────────────────────────────────────────────────
445
446function findProjectRoot() {
447 let dir = process.cwd();
448 while (dir !== dirname(dir)) {
449 if (existsSync(join(dir, '.git'))) return dir;
450 dir = dirname(dir);
451 }
452 return process.cwd();
453}
454
455function findInstalledProviders(root) {
456 const found = [];
457 for (const d of PROVIDER_DIRS) {
458 const skillsDir = join(root, d, 'skills');
459 if (!existsSync(skillsDir)) continue;
460 try {
461 const entries = readdirSync(skillsDir);
462 if (entries.some(name => isSkillDir(skillsDir, name))) found.push(d);
463 } catch {}
464 }
465 return found;
466}
467
468function getModifiedSkillFiles(root, providerDirs) {
469 // Use git to check if any skill files have local modifications
470 const modified = [];
471 try {
472 const status = execSync('git status --porcelain', { cwd: root, encoding: 'utf8' });
473 for (const line of status.split('\n')) {
474 if (!line.trim()) continue;
475 const file = line.substring(3);
476 for (const d of providerDirs) {
477 if (file.startsWith(`${d}/skills/`)) {
478 const flag = line.substring(0, 2).trim();
479 modified.push({ file, flag });
480 }
481 }
482 }
483 } catch {
484 // Not a git repo or git not available
485 }
486 return modified;
487}
488
489function downloadFile(url, dest) {
490 return new Promise((resolve, reject) => {
491 const file = createWriteStream(dest);
492 get(url, (res) => {
493 if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
494 // Follow redirect
495 get(res.headers.location, (res2) => {
496 res2.pipe(file);
497 file.on('finish', () => { file.close(); resolve(); });
498 }).on('error', reject);
499 return;
500 }
501 if (res.statusCode !== 200) {
502 reject(new Error(`HTTP ${res.statusCode}`));
503 return;
504 }
505 res.pipe(file);
506 file.on('finish', () => { file.close(); resolve(); });
507 }).on('error', reject);
508 });
509}
510
511async function update(flags = []) {
512 const yes = flags.includes('-y') || flags.includes('--yes');
513
514 // Clean up deprecated skills from previous versions.
515 try {
516 const { cleanup } = await import('../../source/skills/impeccable/scripts/cleanup-deprecated.mjs');
517 const root = findProjectRoot();
518 const result = cleanup(root);
519 const total = result.deletedPaths.length + result.removedLockEntries.length;
520 if (total > 0) {
521 console.log(`Cleaned up ${total} deprecated skill(s) from previous versions.\n`);
522 }
523 } catch {
524 // Cleanup script not available (e.g. running from npm package) -- skip
525 }
526
527 // Download the latest skills directly from impeccable.style.
528 // We skip `npx skills update` because it has a known upstream bug
529 // (vercel-labs/skills#775) where it can't find the lock file.
530 const root = findProjectRoot();
531 const providers = findInstalledProviders(root);
532
533 if (providers.length === 0) {
534 console.log('No impeccable skill folders found in this project.');
535 console.log('Run `npx impeccable skills install` to install first.');
536 process.exit(1);
537 }
538
539 console.log('Checking for updates...');
540
541 let tmpDir;
542 try {
543 tmpDir = await downloadAndExtractBundle();
544 } catch (e) {
545 console.error(`Download failed: ${e.message}`);
546 process.exit(1);
547 }
548
549 // Compare local vs remote -- skip if already up to date
550 if (isUpToDate(root, providers, tmpDir)) {
551 rmSync(tmpDir, { recursive: true, force: true });
552 const v = getSkillsVersion(root);
553 console.log(`Skills are up to date${v ? ` (v${v})` : ''}. Nothing to do.`);
554 process.exit(0);
555 }
556
557 console.log(`Found skills in: ${providers.join(', ')}`);
558
559 if (!yes) {
560 const ans = await ask(`Update skills in ${providers.length} provider folder(s)? (Y/n) `);
561 if (ans === 'n' || ans === 'no') {
562 rmSync(tmpDir, { recursive: true, force: true });
563 console.log('Aborted.');
564 process.exit(0);
565 }
566 }
567
568 try {
569
570 // Copy from the bundle to each unique provider folder.
571 // Deduplicate so symlinked dirs (e.g. .claude/skills -> .agents/skills)
572 // are only written once with the correct provider's content.
573 const unique = deduplicateProviders(root, providers);
574 let updated = 0;
575 for (const { provider, localSkillsDir } of unique) {
576 const srcDir = join(tmpDir, provider, 'skills');
577 if (!existsSync(srcDir)) continue;
578
579 const skills = readdirSync(srcDir, { withFileTypes: true });
580 for (const skill of skills) {
581 if (!skill.isDirectory()) continue;
582 const src = join(srcDir, skill.name);
583 const dest = join(localSkillsDir, skill.name);
584 if (existsSync(dest)) rmSync(dest, { recursive: true });
585 copyDirSync(src, dest);
586 updated++;
587 }
588 }
589
590 rmSync(tmpDir, { recursive: true, force: true });
591
592 // Re-apply prefix if detected
593 const prefix = detectPrefix(root);
594 if (prefix) {
595 const count = renameSkillsWithPrefix(root, prefix);
596 if (count > 0) console.log(`Re-applied "${prefix}" prefix to ${count} skills.`);
597 }
598
599 // Run cleanup to remove deprecated stubs from the fresh download
600 try {
601 const { cleanup: postCleanup } = await import('../../source/skills/impeccable/scripts/cleanup-deprecated.mjs');
602 postCleanup(root);
603 } catch {
604 // Not available -- skip
605 }
606
607 const v = getSkillsVersion(root);
608 console.log(`Updated ${updated} skill(s)${v ? ` to v${v}` : ''}.`);
609 console.log('Done!\n');
610 } catch (e) {
611 console.error(`Update failed: ${e.message}`);
612 if (tmpDir) rmSync(tmpDir, { recursive: true, force: true });
613 process.exit(1);
614 }
615}
616
617function copyDirSync(src, dest) {
618 mkdirSync(dest, { recursive: true });
619 for (const entry of readdirSync(src, { withFileTypes: true })) {
620 const s = join(src, entry.name);
621 const d = join(dest, entry.name);
622 if (entry.isDirectory()) {
623 copyDirSync(s, d);
624 } else {
625 writeFileSync(d, readFileSync(s));
626 }
627 }
628}
629
630// ─── Router ───────────────────────────────────────────────────────────────────
631
632export async function run(args) {
633 const sub = args[0];
634
635 if (!sub || sub === 'help' || sub === '--help' || sub === '-h') {
636 await showHelp();
637 } else if (sub === 'install') {
638 await install(args.slice(1));
639 } else if (sub === 'update') {
640 await update(args.slice(1));
641 } else if (sub === 'check') {
642 await check();
643 } else {
644 console.error(`Unknown skills command: ${sub}`);
645 console.error(`Run 'impeccable skills --help' for available commands.`);
646 process.exit(1);
647 }
648}