@maccesar/aiskills 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +531 -0
- package/bin/aiskills.js +76 -0
- package/lib/cache.js +49 -0
- package/lib/cleanup.js +77 -0
- package/lib/commands/auto-update.js +131 -0
- package/lib/commands/doctor.js +139 -0
- package/lib/commands/list.js +77 -0
- package/lib/commands/skills.js +263 -0
- package/lib/commands/status.js +94 -0
- package/lib/commands/uninstall.js +182 -0
- package/lib/commands/update.js +149 -0
- package/lib/config.js +90 -0
- package/lib/downloader.js +110 -0
- package/lib/hooks.js +74 -0
- package/lib/installer.js +114 -0
- package/lib/platform.js +112 -0
- package/lib/prompts/checkboxCancel.js +264 -0
- package/lib/prompts/selectCancel.js +204 -0
- package/lib/symlink.js +154 -0
- package/lib/utils.js +49 -0
- package/package.json +61 -0
- package/skills/humaniza/SKILL.md +51 -0
- package/skills/humaniza/agents/openai.yaml +4 -0
- package/skills/humaniza/references/ai-patterns-es.md +51 -0
- package/skills/humaniza/references/checklist.md +9 -0
- package/skills/humaniza/references/examples.md +17 -0
- package/skills/humaniza/references/lexicon-es-mx.md +36 -0
- package/skills/humaniza/references/modes-es-mx.md +41 -0
- package/skills/humaniza/references/voice-es-mx.md +24 -0
- package/skills/refactoring-ui/SKILL.md +59 -0
- package/skills/refactoring-ui/references/01-design-process.md +72 -0
- package/skills/refactoring-ui/references/02-visual-hierarchy.md +84 -0
- package/skills/refactoring-ui/references/03-layout-spacing.md +69 -0
- package/skills/refactoring-ui/references/04-typography.md +70 -0
- package/skills/refactoring-ui/references/05-color.md +96 -0
- package/skills/refactoring-ui/references/06-depth-shadows.md +74 -0
- package/skills/refactoring-ui/references/07-images.md +75 -0
- package/skills/refactoring-ui/references/08-finishing-touches.md +91 -0
- package/skills/stitch-showcase/SKILL.md +411 -0
- package/skills/stitch-showcase/references/01-navbar.md +52 -0
- package/skills/stitch-showcase/references/02-hero.md +56 -0
- package/skills/stitch-showcase/references/03-design-system.md +102 -0
- package/skills/stitch-showcase/references/04-screen-gallery.md +102 -0
- package/skills/stitch-showcase/references/05-viewer-web.md +105 -0
- package/skills/stitch-showcase/references/06-viewer-mobile.md +104 -0
- package/skills/stitch-showcase/references/07-theme-system.md +77 -0
- package/skills/stitch-showcase/references/08-type-detection.md +81 -0
- package/skills/stitch-showcase/references/09-quality-standards.md +126 -0
- package/skills/stitch-showcase/references/10-component-standardization.md +40 -0
- package/skills/stitch-showcase/references/11-component-catalog.md +70 -0
- package/skills/stitch-showcase/references/catalog-template.html +841 -0
- package/skills/stitch-showcase/references/index.html +299 -0
- package/skills/stitch-showcase/references/viewer.html +412 -0
- package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-313.pyc +0 -0
- package/skills/stitch-showcase/scripts/apply_canonical.py +238 -0
- package/skills/stitch-showcase/scripts/build_showcase.py +2103 -0
- package/skills/stitch-showcase/scripts/component_utils.py +398 -0
- package/skills/stitch-showcase/scripts/detect_components.py +284 -0
- package/skills/stitch-showcase/scripts/extract_catalog.py +913 -0
- package/skills/stitch-showcase/scripts/extract_text.py +268 -0
- package/skills/stitch-showcase/scripts/extract_zips.py +178 -0
- package/skills/stitch-showcase/scripts/parse_design_md.py +397 -0
- package/skills/vscode-extension-dev/SKILL.md +114 -0
- package/skills/vscode-extension-dev/references/api-patterns.md +625 -0
- package/skills/vscode-extension-dev/references/architecture.md +287 -0
- package/skills/vscode-extension-dev/references/package-json-schema.md +345 -0
- package/skills/vscode-extension-dev/references/publishing.md +251 -0
package/lib/cleanup.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cleanup helpers for skills and symlinks
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
SKILLS,
|
|
7
|
+
LEGACY_SKILLS,
|
|
8
|
+
getAgentsSkillsDir,
|
|
9
|
+
} from './config.js';
|
|
10
|
+
import { detectPlatforms } from './platform.js';
|
|
11
|
+
import { existsSync, lstatSync, rmSync } from 'fs';
|
|
12
|
+
import { join } from 'path';
|
|
13
|
+
|
|
14
|
+
export function getSkillList({ includeLegacy = true, legacyOnly = false } = {}) {
|
|
15
|
+
if (legacyOnly) return [...LEGACY_SKILLS];
|
|
16
|
+
return includeLegacy ? [...SKILLS, ...LEGACY_SKILLS] : [...SKILLS];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function removeEntriesAtDir(dir, names, { suffix = '', recursive = true } = {}) {
|
|
20
|
+
const results = { removed: [], failed: [] };
|
|
21
|
+
if (!dir || !existsSync(dir)) return results;
|
|
22
|
+
|
|
23
|
+
for (const name of names) {
|
|
24
|
+
const target = join(dir, suffix ? `${name}${suffix}` : name);
|
|
25
|
+
try {
|
|
26
|
+
lstatSync(target);
|
|
27
|
+
rmSync(target, { recursive, force: true });
|
|
28
|
+
results.removed.push(name);
|
|
29
|
+
} catch (error) {
|
|
30
|
+
if (error.code && error.code !== 'ENOENT') {
|
|
31
|
+
results.failed.push(name);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return results;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function removeSkillSymlinks(platformSkillsDir, options = {}) {
|
|
40
|
+
const skillList = getSkillList(options);
|
|
41
|
+
return removeEntriesAtDir(platformSkillsDir, skillList, { recursive: true });
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function removeLegacySkillSymlinks(platformSkillsDir) {
|
|
45
|
+
return removeSkillSymlinks(platformSkillsDir, { legacyOnly: true });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function removeSkills(baseDir, options = {}) {
|
|
49
|
+
const skillsDir = getAgentsSkillsDir(baseDir);
|
|
50
|
+
const skillList = getSkillList(options);
|
|
51
|
+
return removeEntriesAtDir(skillsDir, skillList, { recursive: true });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function cleanupLegacyArtifacts(baseDir) {
|
|
55
|
+
removeSkills(baseDir, { legacyOnly: true });
|
|
56
|
+
|
|
57
|
+
const platforms = detectPlatforms(baseDir);
|
|
58
|
+
for (const platform of platforms) {
|
|
59
|
+
removeLegacySkillSymlinks(platform.skillsDir);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (baseDir) {
|
|
63
|
+
removeSkills(undefined, { legacyOnly: true });
|
|
64
|
+
const globalPlatforms = detectPlatforms();
|
|
65
|
+
for (const platform of globalPlatforms) {
|
|
66
|
+
removeLegacySkillSymlinks(platform.skillsDir);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export default {
|
|
72
|
+
getSkillList,
|
|
73
|
+
removeSkillSymlinks,
|
|
74
|
+
removeLegacySkillSymlinks,
|
|
75
|
+
removeSkills,
|
|
76
|
+
cleanupLegacyArtifacts,
|
|
77
|
+
};
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-update command
|
|
3
|
+
* Full pipeline: check npm -> update CLI -> sync skills
|
|
4
|
+
* Designed to run silently from hooks or manually with progress
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
import ora from 'ora';
|
|
9
|
+
import { execFile as execFileCb } from 'child_process';
|
|
10
|
+
import { promisify } from 'util';
|
|
11
|
+
|
|
12
|
+
const execFile = promisify(execFileCb);
|
|
13
|
+
import { existsSync } from 'fs';
|
|
14
|
+
import { join } from 'path';
|
|
15
|
+
import {
|
|
16
|
+
PACKAGE_VERSION,
|
|
17
|
+
SKILLS,
|
|
18
|
+
getConfigDir,
|
|
19
|
+
} from '../config.js';
|
|
20
|
+
import {
|
|
21
|
+
shouldCheckForUpdate as shouldCheck,
|
|
22
|
+
writeLastCheck,
|
|
23
|
+
} from '../cache.js';
|
|
24
|
+
import {
|
|
25
|
+
checkForUpdate,
|
|
26
|
+
fetchLatestNpmVersion,
|
|
27
|
+
} from '../downloader.js';
|
|
28
|
+
import {
|
|
29
|
+
installSkills,
|
|
30
|
+
getLocalRepoDir,
|
|
31
|
+
} from '../installer.js';
|
|
32
|
+
import { createSkillSymlinks } from '../symlink.js';
|
|
33
|
+
import { detectPlatforms } from '../platform.js';
|
|
34
|
+
import { cleanupLegacyArtifacts, getSkillList } from '../cleanup.js';
|
|
35
|
+
|
|
36
|
+
function isDevMode() {
|
|
37
|
+
const repoDir = getLocalRepoDir();
|
|
38
|
+
if (!repoDir) return false;
|
|
39
|
+
return existsSync(join(repoDir, '.git'));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function noopSpinner() {
|
|
43
|
+
const noop = () => noopSpinner;
|
|
44
|
+
return { start: noop, succeed: noop, fail: noop, warn: noop, info: noop };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function autoUpdateCommand(options) {
|
|
48
|
+
const silent = !!options.silent;
|
|
49
|
+
const log = silent ? () => {} : console.log;
|
|
50
|
+
const spinner = silent ? noopSpinner() : ora();
|
|
51
|
+
|
|
52
|
+
const cacheDir = getConfigDir();
|
|
53
|
+
|
|
54
|
+
// Step 1: Check cache
|
|
55
|
+
if (!shouldCheck(cacheDir)) {
|
|
56
|
+
log(chalk.green('✔'), `Up to date (v${PACKAGE_VERSION})`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Step 2: Check npm for latest version
|
|
61
|
+
spinner.start('Checking for updates...');
|
|
62
|
+
|
|
63
|
+
let latestVersion;
|
|
64
|
+
try {
|
|
65
|
+
latestVersion = await fetchLatestNpmVersion();
|
|
66
|
+
} catch {
|
|
67
|
+
if (!silent) spinner.fail('Could not reach npm registry');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Step 3: Write cache
|
|
72
|
+
try {
|
|
73
|
+
writeLastCheck(cacheDir, latestVersion);
|
|
74
|
+
} catch {
|
|
75
|
+
// Cache write failed — continue anyway
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Step 4: Compare versions
|
|
79
|
+
const hasUpdate = await checkForUpdate(PACKAGE_VERSION);
|
|
80
|
+
|
|
81
|
+
if (!hasUpdate) {
|
|
82
|
+
spinner.succeed(`Up to date (v${PACKAGE_VERSION})`);
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Step 5: Update CLI (skip in dev mode)
|
|
87
|
+
spinner.succeed(`Update available: v${PACKAGE_VERSION} → ${latestVersion}`);
|
|
88
|
+
|
|
89
|
+
if (isDevMode()) {
|
|
90
|
+
spinner.info(`Dev mode — skipping npm update`);
|
|
91
|
+
} else {
|
|
92
|
+
spinner.start(`Downloading and installing v${latestVersion}...`);
|
|
93
|
+
try {
|
|
94
|
+
await execFile('npm', ['update', '-g', '@maccesar/aiskills'], {
|
|
95
|
+
timeout: 60000,
|
|
96
|
+
});
|
|
97
|
+
spinner.succeed(`Updated to v${latestVersion}`);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
spinner.fail(`npm update failed: ${error.message}`);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Step 6: Sync skills and refresh symlinks
|
|
105
|
+
const repoDir = getLocalRepoDir();
|
|
106
|
+
if (repoDir) {
|
|
107
|
+
spinner.start('Syncing skills...');
|
|
108
|
+
try {
|
|
109
|
+
await installSkills(repoDir);
|
|
110
|
+
cleanupLegacyArtifacts();
|
|
111
|
+
|
|
112
|
+
const detectedPlatforms = detectPlatforms();
|
|
113
|
+
const skillList = getSkillList({ includeLegacy: false });
|
|
114
|
+
for (const platform of detectedPlatforms) {
|
|
115
|
+
const hasSymlinks = skillList.some((skill) =>
|
|
116
|
+
existsSync(join(platform.skillsDir, skill))
|
|
117
|
+
);
|
|
118
|
+
if (hasSymlinks) {
|
|
119
|
+
await createSkillSymlinks(platform.skillsDir, SKILLS);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
spinner.succeed('Skills synced');
|
|
123
|
+
} catch (error) {
|
|
124
|
+
spinner.fail(`Skill sync failed: ${error.message}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
log('');
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export default autoUpdateCommand;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Doctor command
|
|
3
|
+
* Diagnoses installation health (read-only)
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { existsSync, lstatSync } from 'fs';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import {
|
|
11
|
+
SKILLS,
|
|
12
|
+
PACKAGE_VERSION,
|
|
13
|
+
getAgentsSkillsDir,
|
|
14
|
+
getConfigDir,
|
|
15
|
+
getPlatforms,
|
|
16
|
+
} from '../config.js';
|
|
17
|
+
import { hasHook } from '../hooks.js';
|
|
18
|
+
import { readLastCheck } from '../cache.js';
|
|
19
|
+
|
|
20
|
+
const CHECK = chalk.green('✓');
|
|
21
|
+
const CROSS = chalk.red('✗');
|
|
22
|
+
const WARN = chalk.yellow('⚠');
|
|
23
|
+
|
|
24
|
+
export async function doctorCommand() {
|
|
25
|
+
const homeDir = os.homedir();
|
|
26
|
+
const skillsDir = getAgentsSkillsDir(homeDir);
|
|
27
|
+
const claudeDir = join(homeDir, '.claude');
|
|
28
|
+
const cacheDir = getConfigDir();
|
|
29
|
+
|
|
30
|
+
let issues = 0;
|
|
31
|
+
const symlinkIssues = [];
|
|
32
|
+
|
|
33
|
+
console.log('');
|
|
34
|
+
console.log(chalk.bold('AI Skills Doctor'));
|
|
35
|
+
console.log('');
|
|
36
|
+
console.log(' Checking installation health...');
|
|
37
|
+
console.log('');
|
|
38
|
+
|
|
39
|
+
// CLI version
|
|
40
|
+
console.log(` ${CHECK} CLI version: v${PACKAGE_VERSION}`);
|
|
41
|
+
|
|
42
|
+
// Skills check
|
|
43
|
+
const missingSkills = [];
|
|
44
|
+
for (const skill of SKILLS) {
|
|
45
|
+
if (!existsSync(join(skillsDir, skill))) {
|
|
46
|
+
missingSkills.push(skill);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
const installedCount = SKILLS.length - missingSkills.length;
|
|
50
|
+
if (missingSkills.length === 0) {
|
|
51
|
+
console.log(` ${CHECK} Skills: ${installedCount}/${SKILLS.length} installed in ~/.agents/skills/`);
|
|
52
|
+
} else {
|
|
53
|
+
console.log(` ${CROSS} Skills: ${installedCount}/${SKILLS.length} installed in ~/.agents/skills/ (missing: ${missingSkills.join(', ')})`);
|
|
54
|
+
issues += missingSkills.length;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Hook check
|
|
58
|
+
if (hasHook(claudeDir)) {
|
|
59
|
+
console.log(` ${CHECK} Hook: SessionStart configured`);
|
|
60
|
+
} else {
|
|
61
|
+
console.log(` ${CROSS} Hook: SessionStart not configured`);
|
|
62
|
+
issues++;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Cache check
|
|
66
|
+
const lastCheck = readLastCheck(cacheDir);
|
|
67
|
+
if (lastCheck) {
|
|
68
|
+
const hoursAgo = Math.round((Date.now() - lastCheck.lastCheck) / (1000 * 60 * 60));
|
|
69
|
+
const timeLabel = hoursAgo < 1 ? 'less than an hour ago' : `${hoursAgo} hour${hoursAgo === 1 ? '' : 's'} ago`;
|
|
70
|
+
console.log(` ${CHECK} Cache: last check ${timeLabel}`);
|
|
71
|
+
} else {
|
|
72
|
+
console.log(` ${WARN} Cache: no check recorded`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Platforms
|
|
76
|
+
console.log('');
|
|
77
|
+
console.log(' Platforms:');
|
|
78
|
+
const platforms = getPlatforms(homeDir);
|
|
79
|
+
for (const platform of platforms) {
|
|
80
|
+
const missing = [];
|
|
81
|
+
const broken = [];
|
|
82
|
+
|
|
83
|
+
for (const skill of SKILLS) {
|
|
84
|
+
const linkPath = join(platform.skillsDir, skill);
|
|
85
|
+
try {
|
|
86
|
+
const stat = lstatSync(linkPath);
|
|
87
|
+
if (stat.isSymbolicLink()) {
|
|
88
|
+
// Check if target exists
|
|
89
|
+
if (!existsSync(linkPath)) {
|
|
90
|
+
broken.push(skill);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// exists (symlink or directory), count as linked
|
|
94
|
+
} catch {
|
|
95
|
+
missing.push(skill);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const linkedCount = SKILLS.length - missing.length - broken.length;
|
|
100
|
+
|
|
101
|
+
if (missing.length === 0 && broken.length === 0) {
|
|
102
|
+
console.log(` ${CHECK} ${platform.displayName}: ${SKILLS.length}/${SKILLS.length} skills linked`);
|
|
103
|
+
} else {
|
|
104
|
+
const problems = [];
|
|
105
|
+
if (missing.length > 0) problems.push(`missing: ${missing.join(', ')}`);
|
|
106
|
+
if (broken.length > 0) problems.push(`broken: ${broken.join(', ')}`);
|
|
107
|
+
console.log(` ${CROSS} ${platform.displayName}: ${linkedCount}/${SKILLS.length} skills linked (${problems.join('; ')})`);
|
|
108
|
+
issues += missing.length + broken.length;
|
|
109
|
+
|
|
110
|
+
// Collect symlink issues for detailed report
|
|
111
|
+
for (const skill of broken) {
|
|
112
|
+
symlinkIssues.push(`${CROSS} ~/${platform.name === 'claude' ? '.claude' : `.${platform.name}`}/skills/${skill} → broken symlink (target missing)`);
|
|
113
|
+
}
|
|
114
|
+
for (const skill of missing) {
|
|
115
|
+
symlinkIssues.push(`${CROSS} ~/${platform.name === 'claude' ? '.claude' : `.${platform.name}`}/skills/${skill} → not found`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Symlink issues detail
|
|
121
|
+
if (symlinkIssues.length > 0) {
|
|
122
|
+
console.log('');
|
|
123
|
+
console.log(` ${WARN} Symlink issues:`);
|
|
124
|
+
for (const issue of symlinkIssues) {
|
|
125
|
+
console.log(` ${issue}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Summary
|
|
130
|
+
console.log('');
|
|
131
|
+
if (issues === 0) {
|
|
132
|
+
console.log(chalk.green(` No issues found.`));
|
|
133
|
+
} else {
|
|
134
|
+
console.log(chalk.yellow(` ${issues} issue${issues === 1 ? '' : 's'} found. Run 'aiskills install' to fix.`));
|
|
135
|
+
}
|
|
136
|
+
console.log('');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export default { doctorCommand };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* List command
|
|
3
|
+
* Shows available skills with their descriptions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import { SKILLS } from '../config.js';
|
|
8
|
+
import { readFileSync } from 'fs';
|
|
9
|
+
import { join, dirname } from 'path';
|
|
10
|
+
import { fileURLToPath } from 'url';
|
|
11
|
+
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
|
|
14
|
+
function parseSkillDescription(skillName) {
|
|
15
|
+
try {
|
|
16
|
+
const skillPath = join(__dirname, '..', '..', 'skills', skillName, 'SKILL.md');
|
|
17
|
+
const content = readFileSync(skillPath, 'utf8');
|
|
18
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
19
|
+
if (!match) return null;
|
|
20
|
+
|
|
21
|
+
const frontmatter = match[1];
|
|
22
|
+
|
|
23
|
+
// Extract name
|
|
24
|
+
const nameMatch = frontmatter.match(/^name:\s*(.+)$/m);
|
|
25
|
+
const name = nameMatch ? nameMatch[1].trim() : skillName;
|
|
26
|
+
|
|
27
|
+
// Extract description (handles multi-line YAML >)
|
|
28
|
+
let description = '';
|
|
29
|
+
const descMatch = frontmatter.match(/description:\s*>\s*\n([\s\S]*?)(?=\n\w+:|$)/);
|
|
30
|
+
if (descMatch) {
|
|
31
|
+
description = descMatch[1]
|
|
32
|
+
.split('\n')
|
|
33
|
+
.map(line => line.trim())
|
|
34
|
+
.filter(Boolean)
|
|
35
|
+
.join(' ');
|
|
36
|
+
} else {
|
|
37
|
+
const singleMatch = frontmatter.match(/^description:\s*(.+)$/m);
|
|
38
|
+
if (singleMatch) {
|
|
39
|
+
description = singleMatch[1].trim();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Trim long descriptions
|
|
44
|
+
const maxLen = 80;
|
|
45
|
+
if (description.length > maxLen) {
|
|
46
|
+
description = description.substring(0, maxLen - 1).trimEnd() + '…';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { name, description };
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* List command handler
|
|
57
|
+
*/
|
|
58
|
+
export async function listCommand() {
|
|
59
|
+
console.log('');
|
|
60
|
+
console.log(chalk.bold.blue('Available Skills'));
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
63
|
+
const maxNameLen = Math.max(...SKILLS.map(s => s.length));
|
|
64
|
+
|
|
65
|
+
for (const skill of SKILLS) {
|
|
66
|
+
const info = parseSkillDescription(skill);
|
|
67
|
+
const name = chalk.cyan(skill.padEnd(maxNameLen));
|
|
68
|
+
const desc = info?.description ? chalk.gray(info.description) : chalk.gray('(no description)');
|
|
69
|
+
console.log(` ${name} ${desc}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
console.log('');
|
|
73
|
+
console.log(chalk.gray(`${SKILLS.length} skill${SKILLS.length !== 1 ? 's' : ''} available`));
|
|
74
|
+
console.log('');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default listCommand;
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skills command
|
|
3
|
+
* Installs and manages skills in AI coding assistant directories
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import ora from 'ora';
|
|
8
|
+
import {
|
|
9
|
+
SKILLS,
|
|
10
|
+
getPlatforms,
|
|
11
|
+
getAgentsSkillsDir,
|
|
12
|
+
} from '../config.js';
|
|
13
|
+
import select from '../prompts/selectCancel.js';
|
|
14
|
+
import checkbox, { Separator } from '../prompts/checkboxCancel.js';
|
|
15
|
+
import {
|
|
16
|
+
detectPlatforms,
|
|
17
|
+
detectOS,
|
|
18
|
+
} from '../platform.js';
|
|
19
|
+
import {
|
|
20
|
+
getSkillList,
|
|
21
|
+
removeSkillSymlinks,
|
|
22
|
+
removeLegacySkillSymlinks,
|
|
23
|
+
removeSkills,
|
|
24
|
+
} from '../cleanup.js';
|
|
25
|
+
import {
|
|
26
|
+
installSkills,
|
|
27
|
+
getLocalRepoDir,
|
|
28
|
+
} from '../installer.js';
|
|
29
|
+
import { downloadRepoArchive } from '../downloader.js';
|
|
30
|
+
import { createSkillSymlinks } from '../symlink.js';
|
|
31
|
+
import { installHook } from '../hooks.js';
|
|
32
|
+
import { mkdtemp } from 'fs/promises';
|
|
33
|
+
import { existsSync } from 'fs';
|
|
34
|
+
import { join, resolve } from 'path';
|
|
35
|
+
import { tmpdir } from 'os';
|
|
36
|
+
|
|
37
|
+
function hasAnySkillSymlink(platformSkillsDir) {
|
|
38
|
+
if (!platformSkillsDir || !existsSync(platformSkillsDir)) return false;
|
|
39
|
+
const skillList = getSkillList();
|
|
40
|
+
return skillList.some((skill) => existsSync(join(platformSkillsDir, skill)));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Skills command handler
|
|
45
|
+
* @param {Object} options - Command options
|
|
46
|
+
*/
|
|
47
|
+
export async function skillsCommand(options) {
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.bold.blue('AI Skills Manager'));
|
|
50
|
+
console.log('');
|
|
51
|
+
|
|
52
|
+
const isLocal = options.local;
|
|
53
|
+
const customPath = options.path;
|
|
54
|
+
|
|
55
|
+
// Determine base directory (Local vs Global)
|
|
56
|
+
const baseDir = isLocal ? process.cwd() : (customPath ? resolve(customPath) : undefined);
|
|
57
|
+
|
|
58
|
+
if (isLocal) {
|
|
59
|
+
console.log(chalk.cyan('Mode: Local installation (current project)'));
|
|
60
|
+
} else if (customPath) {
|
|
61
|
+
console.log(chalk.cyan(`Mode: Custom path (${baseDir})`));
|
|
62
|
+
} else {
|
|
63
|
+
console.log(chalk.cyan('Mode: Global installation (user home)'));
|
|
64
|
+
}
|
|
65
|
+
console.log('');
|
|
66
|
+
|
|
67
|
+
// Detect installed platforms at the target base directory
|
|
68
|
+
const localPlatforms = detectPlatforms(baseDir);
|
|
69
|
+
const globalPlatforms = detectPlatforms();
|
|
70
|
+
let detectedPlatforms = localPlatforms;
|
|
71
|
+
|
|
72
|
+
if (isLocal || options.path) {
|
|
73
|
+
const localNames = new Set(localPlatforms.map((platform) => platform.name));
|
|
74
|
+
const globalNames = new Set(globalPlatforms.map((platform) => platform.name));
|
|
75
|
+
const allPlatforms = getPlatforms(baseDir);
|
|
76
|
+
const merged = [...localPlatforms];
|
|
77
|
+
for (const platform of allPlatforms) {
|
|
78
|
+
if (globalNames.has(platform.name) && !localNames.has(platform.name)) {
|
|
79
|
+
merged.push(platform);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
detectedPlatforms = merged;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (detectedPlatforms.length === 0 && !options.path && !isLocal) {
|
|
86
|
+
console.log(chalk.yellow('No AI coding assistants detected globally.'));
|
|
87
|
+
console.log('Install one of: Claude Code, Gemini CLI, or Codex CLI');
|
|
88
|
+
console.log('Or use: aiskills install --local');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Show detected platforms
|
|
93
|
+
if (detectedPlatforms.length > 0) {
|
|
94
|
+
for (const platform of detectedPlatforms) {
|
|
95
|
+
console.log(chalk.green('✓'), `${platform.displayName} detected`);
|
|
96
|
+
}
|
|
97
|
+
console.log('');
|
|
98
|
+
} else if (isLocal) {
|
|
99
|
+
detectedPlatforms = getPlatforms(baseDir);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Select platforms to install
|
|
103
|
+
let selectedPlatforms = [];
|
|
104
|
+
|
|
105
|
+
if (options.path) {
|
|
106
|
+
selectedPlatforms = detectedPlatforms;
|
|
107
|
+
} else if (options.all) {
|
|
108
|
+
selectedPlatforms = detectedPlatforms;
|
|
109
|
+
} else {
|
|
110
|
+
try {
|
|
111
|
+
const platformChoices = await checkbox({
|
|
112
|
+
message: 'Select platforms to sync:',
|
|
113
|
+
choices: [
|
|
114
|
+
...detectedPlatforms.map((p) => ({
|
|
115
|
+
name: p.displayName,
|
|
116
|
+
value: p.name,
|
|
117
|
+
checked: hasAnySkillSymlink(p.skillsDir),
|
|
118
|
+
})),
|
|
119
|
+
new Separator(' ')
|
|
120
|
+
],
|
|
121
|
+
shortcuts: { invert: null },
|
|
122
|
+
theme: {
|
|
123
|
+
style: {
|
|
124
|
+
renderSelectedChoices: () => '',
|
|
125
|
+
prefix: () => chalk.cyan('?'),
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
if (platformChoices.includes('cancel')) {
|
|
131
|
+
console.log('Cancelled.');
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
selectedPlatforms = detectedPlatforms.filter((p) =>
|
|
136
|
+
platformChoices.includes(p.name)
|
|
137
|
+
);
|
|
138
|
+
} catch (error) {
|
|
139
|
+
console.log('\nCancelled.');
|
|
140
|
+
process.exit(0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const removeOnly = selectedPlatforms.length === 0;
|
|
145
|
+
if (removeOnly) {
|
|
146
|
+
console.log(chalk.yellow('No platforms selected. Removing all platform symlinks.'));
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Get repository directory (local or download)
|
|
150
|
+
const spinner = ora();
|
|
151
|
+
let repoDir = null;
|
|
152
|
+
let tempDir = null;
|
|
153
|
+
|
|
154
|
+
if (!removeOnly) {
|
|
155
|
+
repoDir = getLocalRepoDir();
|
|
156
|
+
|
|
157
|
+
if (!repoDir) {
|
|
158
|
+
spinner.start('Downloading from GitHub...');
|
|
159
|
+
try {
|
|
160
|
+
tempDir = await mkdtemp(join(tmpdir(), 'aiskills-'));
|
|
161
|
+
repoDir = await downloadRepoArchive(tempDir);
|
|
162
|
+
spinner.succeed('Downloaded from GitHub');
|
|
163
|
+
} catch (error) {
|
|
164
|
+
spinner.fail('Failed to download');
|
|
165
|
+
console.error(chalk.red(error.message));
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
} else {
|
|
169
|
+
console.log(chalk.green('Using local repository'));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const selectedPlatformNames = new Set(selectedPlatforms.map((platform) => platform.name));
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
if (!removeOnly) {
|
|
177
|
+
// Install skills
|
|
178
|
+
spinner.start('Installing skills...');
|
|
179
|
+
const skillsResult = await installSkills(repoDir, baseDir);
|
|
180
|
+
spinner.succeed(`${SKILLS.length} skill${SKILLS.length !== 1 ? 's' : ''} installed`);
|
|
181
|
+
|
|
182
|
+
// Create symlinks for selected platforms
|
|
183
|
+
for (const platform of selectedPlatforms) {
|
|
184
|
+
removeLegacySkillSymlinks(platform.skillsDir);
|
|
185
|
+
spinner.start(`Linking ${platform.displayName}...`);
|
|
186
|
+
const symlinkResult = await createSkillSymlinks(
|
|
187
|
+
platform.skillsDir,
|
|
188
|
+
SKILLS,
|
|
189
|
+
baseDir
|
|
190
|
+
);
|
|
191
|
+
if (symlinkResult.linked.length === SKILLS.length) {
|
|
192
|
+
spinner.succeed(`${platform.displayName}: Skills linked`);
|
|
193
|
+
} else {
|
|
194
|
+
spinner.warn(
|
|
195
|
+
`${platform.displayName}: ${symlinkResult.linked.length}/${SKILLS.length} skills linked`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
const skillsResult = removeSkills(baseDir);
|
|
201
|
+
const platformResults = detectedPlatforms.map((platform) => {
|
|
202
|
+
const symlinkResult = removeSkillSymlinks(platform.skillsDir);
|
|
203
|
+
return {
|
|
204
|
+
displayName: platform.displayName,
|
|
205
|
+
removedCount: symlinkResult.removed.length,
|
|
206
|
+
};
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
if (skillsResult.removed.length > 0) {
|
|
210
|
+
console.log(chalk.green('✓'), `${skillsResult.removed.length} skills removed`);
|
|
211
|
+
} else {
|
|
212
|
+
console.log(chalk.gray('ℹ'), 'No skills to remove');
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
for (const result of platformResults) {
|
|
216
|
+
if (result.removedCount > 0) {
|
|
217
|
+
console.log(chalk.green('✓'), `${result.displayName}: Skills unlinked`);
|
|
218
|
+
} else {
|
|
219
|
+
console.log(chalk.gray('ℹ'), `${result.displayName}: No symlinks found`);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Install SessionStart hook for Claude Code auto-update
|
|
225
|
+
if (!removeOnly && selectedPlatformNames.has('claude')) {
|
|
226
|
+
const claudePlatform = selectedPlatforms.find((p) => p.name === 'claude');
|
|
227
|
+
if (claudePlatform) {
|
|
228
|
+
installHook(claudePlatform.configDir);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Remove symlinks for unselected detected platforms
|
|
233
|
+
if (!removeOnly) {
|
|
234
|
+
for (const platform of detectedPlatforms) {
|
|
235
|
+
if (selectedPlatformNames.has(platform.name)) continue;
|
|
236
|
+
spinner.start(`Unlinking ${platform.displayName}...`);
|
|
237
|
+
const symlinkResult = removeSkillSymlinks(platform.skillsDir);
|
|
238
|
+
if (symlinkResult.removed.length > 0) {
|
|
239
|
+
spinner.succeed(`${platform.displayName}: Skills unlinked`);
|
|
240
|
+
} else {
|
|
241
|
+
spinner.info(`${platform.displayName}: No symlinks found`);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Summary
|
|
247
|
+
console.log('');
|
|
248
|
+
console.log(chalk.green('✓ Skills sync complete!'));
|
|
249
|
+
console.log('');
|
|
250
|
+
|
|
251
|
+
if (!removeOnly && !isLocal && detectOS() === 'windows') {
|
|
252
|
+
console.log(chalk.yellow('▸'), 'Windows: Ensure ~/bin is in your PATH');
|
|
253
|
+
console.log('');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
} finally {
|
|
257
|
+
if (tempDir) {
|
|
258
|
+
await import('fs-extra').then(({ remove }) => remove(tempDir));
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export default skillsCommand;
|