@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.
Files changed (73) hide show
  1. package/README.md +531 -0
  2. package/bin/aiskills.js +76 -0
  3. package/lib/cache.js +49 -0
  4. package/lib/cleanup.js +77 -0
  5. package/lib/commands/auto-update.js +131 -0
  6. package/lib/commands/doctor.js +139 -0
  7. package/lib/commands/list.js +77 -0
  8. package/lib/commands/skills.js +263 -0
  9. package/lib/commands/status.js +94 -0
  10. package/lib/commands/uninstall.js +182 -0
  11. package/lib/commands/update.js +149 -0
  12. package/lib/config.js +90 -0
  13. package/lib/downloader.js +110 -0
  14. package/lib/hooks.js +74 -0
  15. package/lib/installer.js +114 -0
  16. package/lib/platform.js +112 -0
  17. package/lib/prompts/checkboxCancel.js +264 -0
  18. package/lib/prompts/selectCancel.js +204 -0
  19. package/lib/symlink.js +154 -0
  20. package/lib/utils.js +49 -0
  21. package/package.json +61 -0
  22. package/skills/humaniza/SKILL.md +51 -0
  23. package/skills/humaniza/agents/openai.yaml +4 -0
  24. package/skills/humaniza/references/ai-patterns-es.md +51 -0
  25. package/skills/humaniza/references/checklist.md +9 -0
  26. package/skills/humaniza/references/examples.md +17 -0
  27. package/skills/humaniza/references/lexicon-es-mx.md +36 -0
  28. package/skills/humaniza/references/modes-es-mx.md +41 -0
  29. package/skills/humaniza/references/voice-es-mx.md +24 -0
  30. package/skills/refactoring-ui/SKILL.md +59 -0
  31. package/skills/refactoring-ui/references/01-design-process.md +72 -0
  32. package/skills/refactoring-ui/references/02-visual-hierarchy.md +84 -0
  33. package/skills/refactoring-ui/references/03-layout-spacing.md +69 -0
  34. package/skills/refactoring-ui/references/04-typography.md +70 -0
  35. package/skills/refactoring-ui/references/05-color.md +96 -0
  36. package/skills/refactoring-ui/references/06-depth-shadows.md +74 -0
  37. package/skills/refactoring-ui/references/07-images.md +75 -0
  38. package/skills/refactoring-ui/references/08-finishing-touches.md +91 -0
  39. package/skills/stitch-showcase/SKILL.md +411 -0
  40. package/skills/stitch-showcase/references/01-navbar.md +52 -0
  41. package/skills/stitch-showcase/references/02-hero.md +56 -0
  42. package/skills/stitch-showcase/references/03-design-system.md +102 -0
  43. package/skills/stitch-showcase/references/04-screen-gallery.md +102 -0
  44. package/skills/stitch-showcase/references/05-viewer-web.md +105 -0
  45. package/skills/stitch-showcase/references/06-viewer-mobile.md +104 -0
  46. package/skills/stitch-showcase/references/07-theme-system.md +77 -0
  47. package/skills/stitch-showcase/references/08-type-detection.md +81 -0
  48. package/skills/stitch-showcase/references/09-quality-standards.md +126 -0
  49. package/skills/stitch-showcase/references/10-component-standardization.md +40 -0
  50. package/skills/stitch-showcase/references/11-component-catalog.md +70 -0
  51. package/skills/stitch-showcase/references/catalog-template.html +841 -0
  52. package/skills/stitch-showcase/references/index.html +299 -0
  53. package/skills/stitch-showcase/references/viewer.html +412 -0
  54. package/skills/stitch-showcase/scripts/__pycache__/build_showcase.cpython-314.pyc +0 -0
  55. package/skills/stitch-showcase/scripts/__pycache__/component_utils.cpython-313.pyc +0 -0
  56. package/skills/stitch-showcase/scripts/__pycache__/detect_components.cpython-313.pyc +0 -0
  57. package/skills/stitch-showcase/scripts/__pycache__/extract_catalog.cpython-313.pyc +0 -0
  58. package/skills/stitch-showcase/scripts/__pycache__/extract_text.cpython-313.pyc +0 -0
  59. package/skills/stitch-showcase/scripts/__pycache__/extract_zips.cpython-313.pyc +0 -0
  60. package/skills/stitch-showcase/scripts/__pycache__/parse_design_md.cpython-313.pyc +0 -0
  61. package/skills/stitch-showcase/scripts/apply_canonical.py +238 -0
  62. package/skills/stitch-showcase/scripts/build_showcase.py +2103 -0
  63. package/skills/stitch-showcase/scripts/component_utils.py +398 -0
  64. package/skills/stitch-showcase/scripts/detect_components.py +284 -0
  65. package/skills/stitch-showcase/scripts/extract_catalog.py +913 -0
  66. package/skills/stitch-showcase/scripts/extract_text.py +268 -0
  67. package/skills/stitch-showcase/scripts/extract_zips.py +178 -0
  68. package/skills/stitch-showcase/scripts/parse_design_md.py +397 -0
  69. package/skills/vscode-extension-dev/SKILL.md +114 -0
  70. package/skills/vscode-extension-dev/references/api-patterns.md +625 -0
  71. package/skills/vscode-extension-dev/references/architecture.md +287 -0
  72. package/skills/vscode-extension-dev/references/package-json-schema.md +345 -0
  73. package/skills/vscode-extension-dev/references/publishing.md +251 -0
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Status command
3
+ * Shows a quick overview of what's installed (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
+
23
+ function countInstalledSkills(skillsDir) {
24
+ let count = 0;
25
+ for (const skill of SKILLS) {
26
+ if (existsSync(join(skillsDir, skill))) {
27
+ count++;
28
+ }
29
+ }
30
+ return count;
31
+ }
32
+
33
+ function countLinkedSkills(platformSkillsDir) {
34
+ let count = 0;
35
+ for (const skill of SKILLS) {
36
+ const linkPath = join(platformSkillsDir, skill);
37
+ try {
38
+ lstatSync(linkPath);
39
+ count++;
40
+ } catch {
41
+ // not found
42
+ }
43
+ }
44
+ return count;
45
+ }
46
+
47
+ function formatLastCheck(data) {
48
+ if (!data) return chalk.gray('never');
49
+ const date = new Date(data.lastCheck);
50
+ const formatted = date.toISOString().replace('T', ' ').slice(0, 16);
51
+ return `${formatted} (v${data.latestVersion})`;
52
+ }
53
+
54
+ export async function statusCommand() {
55
+ const homeDir = os.homedir();
56
+ const skillsDir = getAgentsSkillsDir(homeDir);
57
+ const claudeDir = join(homeDir, '.claude');
58
+ const cacheDir = getConfigDir();
59
+
60
+ // Skills count
61
+ const installedCount = countInstalledSkills(skillsDir);
62
+ const totalCount = SKILLS.length;
63
+
64
+ // Hook check
65
+ const hookExists = hasHook(claudeDir);
66
+
67
+ // Cache
68
+ const lastCheck = readLastCheck(cacheDir);
69
+
70
+ console.log('');
71
+ console.log(chalk.bold('AI Skills Status'));
72
+ console.log('');
73
+ console.log(` Version: v${PACKAGE_VERSION}`);
74
+ console.log(` Skills: ${installedCount}/${totalCount} installed`);
75
+ console.log(` Hook: Claude Code SessionStart ${hookExists ? CHECK : CROSS}`);
76
+ console.log(` Last check: ${formatLastCheck(lastCheck)}`);
77
+
78
+ // Platforms
79
+ console.log('');
80
+ console.log(' Platforms:');
81
+ const platforms = getPlatforms(homeDir);
82
+ for (const platform of platforms) {
83
+ const linked = countLinkedSkills(platform.skillsDir);
84
+ if (linked > 0) {
85
+ console.log(` ${platform.displayName.padEnd(13)} ${CHECK} ${linked} skills linked`);
86
+ } else {
87
+ console.log(` ${platform.displayName.padEnd(13)} ${CROSS} ${chalk.gray('not linked')}`);
88
+ }
89
+ }
90
+
91
+ console.log('');
92
+ }
93
+
94
+ export default { statusCommand };
@@ -0,0 +1,182 @@
1
+ /**
2
+ * Uninstall command
3
+ * Removes installed skills and symlinks
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import os from 'os';
9
+ import { SKILLS, getConfigDir } from '../config.js';
10
+ import {
11
+ detectPlatforms,
12
+ } from '../platform.js';
13
+ import {
14
+ removeSkillSymlinks,
15
+ removeSkills,
16
+ } from '../cleanup.js';
17
+ import { removeHook } from '../hooks.js';
18
+ import checkbox, { Separator } from '../prompts/checkboxCancel.js';
19
+ import { existsSync } from 'fs';
20
+ import { rm } from 'fs/promises';
21
+ import { join, resolve } from 'path';
22
+ import { getAgentsSkillsDir } from '../config.js';
23
+ import { getSkillList } from '../cleanup.js';
24
+
25
+ /**
26
+ * Uninstall command handler
27
+ */
28
+ export async function uninstallCommand(options) {
29
+ console.log('');
30
+ console.log(chalk.bold.blue('AI Skills Uninstaller'));
31
+ console.log('');
32
+
33
+ const projectDir = resolve(process.cwd());
34
+ const baseDir = options.local ? projectDir : undefined;
35
+ if (options.local) {
36
+ console.log(chalk.cyan('Mode: Local uninstallation (current project)'));
37
+ console.log('');
38
+ }
39
+
40
+ const detectedPlatforms = detectPlatforms(baseDir);
41
+
42
+ const skillList = getSkillList();
43
+ const homeSkillsDir = getAgentsSkillsDir();
44
+ const projectSkillsDir = getAgentsSkillsDir(projectDir);
45
+
46
+ const hasAnyInDir = (dir, names) =>
47
+ !!dir && existsSync(dir) && names.some((name) => existsSync(join(dir, name)));
48
+
49
+ const hasHomeSkills = hasAnyInDir(homeSkillsDir, skillList);
50
+ const hasProjectSkills = options.local && hasAnyInDir(projectSkillsDir, skillList);
51
+
52
+ const hasHomeSymlinks = detectedPlatforms.some((platform) =>
53
+ hasAnyInDir(platform.skillsDir, skillList)
54
+ );
55
+ const hasProjectSymlinks = options.local && detectPlatforms(projectDir).some((platform) =>
56
+ hasAnyInDir(platform.skillsDir, skillList)
57
+ );
58
+
59
+ const choices = [];
60
+ if (hasHomeSkills) {
61
+ choices.push({ name: 'Skills from the `home` directory', value: 'skills-home', checked: false });
62
+ }
63
+ if (hasProjectSkills) {
64
+ choices.push({ name: 'Skills from the `project` directory', value: 'skills-project', checked: false });
65
+ }
66
+ if (hasHomeSymlinks) {
67
+ choices.push({ name: 'Skill symlinks from `home` directory', value: 'symlinks-home', checked: true });
68
+ }
69
+ if (hasProjectSymlinks) {
70
+ choices.push({ name: 'Skill symlinks from `project` directory', value: 'symlinks-project', checked: false });
71
+ }
72
+
73
+ if (choices.length === 0) {
74
+ console.log(chalk.yellow('No skills or symlinks found.'));
75
+ console.log('');
76
+ return;
77
+ }
78
+
79
+ let targets = [];
80
+ try {
81
+ targets = await checkbox({
82
+ message: 'What do you want to uninstall:',
83
+ choices: [
84
+ ...choices,
85
+ new Separator(' '),
86
+ ],
87
+ shortcuts: { invert: null },
88
+ theme: {
89
+ style: {
90
+ renderSelectedChoices: () => '',
91
+ },
92
+ },
93
+ });
94
+ } catch (error) {
95
+ console.log('\nCancelled.');
96
+ process.exit(0);
97
+ }
98
+
99
+ if (targets.includes('cancel')) {
100
+ console.log('Cancelled.');
101
+ return;
102
+ }
103
+
104
+ if (targets.length === 0) {
105
+ console.log(chalk.yellow('Nothing to uninstall. Cancelled.'));
106
+ return;
107
+ }
108
+
109
+ const spinner = ora();
110
+ let actionTaken = false;
111
+
112
+ if (targets.includes('symlinks-home')) {
113
+ for (const platform of detectedPlatforms) {
114
+ spinner.start(`Removing ${platform.displayName} symlinks...`);
115
+ const symlinkResult = removeSkillSymlinks(platform.skillsDir);
116
+ if (symlinkResult.removed.length > 0) {
117
+ spinner.succeed(`${platform.displayName}: Skills unlinked`);
118
+ actionTaken = true;
119
+ } else {
120
+ spinner.info(`${platform.displayName}: No symlinks found`);
121
+ }
122
+ }
123
+ }
124
+
125
+ if (targets.includes('symlinks-project')) {
126
+ const projectPlatforms = detectPlatforms(projectDir);
127
+ for (const platform of projectPlatforms) {
128
+ spinner.start(`Removing ${platform.displayName} symlinks...`);
129
+ const symlinkResult = removeSkillSymlinks(platform.skillsDir);
130
+ if (symlinkResult.removed.length > 0) {
131
+ spinner.succeed(`${platform.displayName}: Skills unlinked`);
132
+ actionTaken = true;
133
+ } else {
134
+ spinner.info(`${platform.displayName}: No symlinks found`);
135
+ }
136
+ }
137
+ }
138
+
139
+ if (targets.includes('skills-home')) {
140
+ spinner.start('Removing skills...');
141
+ const skillsResult = removeSkills(undefined);
142
+ if (skillsResult.removed.length > 0) {
143
+ spinner.succeed(`${SKILLS.length} skills removed`);
144
+ actionTaken = true;
145
+ } else {
146
+ spinner.info('No skills to remove');
147
+ }
148
+ }
149
+
150
+ if (targets.includes('skills-project')) {
151
+ spinner.start('Removing skills...');
152
+ const skillsResult = removeSkills(projectDir);
153
+ if (skillsResult.removed.length > 0) {
154
+ spinner.succeed(`${SKILLS.length} skills removed`);
155
+ actionTaken = true;
156
+ } else {
157
+ spinner.info('No skills to remove');
158
+ }
159
+ }
160
+
161
+ // Remove Claude Code SessionStart hook
162
+ const claudeDir = join(os.homedir(), '.claude');
163
+ removeHook(claudeDir);
164
+
165
+ // Clean up cache directory
166
+ const cacheDir = getConfigDir();
167
+ if (existsSync(cacheDir)) {
168
+ await rm(cacheDir, { recursive: true, force: true });
169
+ }
170
+
171
+ if (actionTaken) {
172
+ console.log('');
173
+ console.log(chalk.green('✓ Uninstallation complete!'));
174
+ console.log('');
175
+ } else {
176
+ console.log('');
177
+ console.log(chalk.yellow('No changes were necessary.'));
178
+ console.log('');
179
+ }
180
+ }
181
+
182
+ export default uninstallCommand;
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Update command
3
+ * Checks for a newer CLI version, then syncs skills from the installed package
4
+ */
5
+
6
+ import chalk from 'chalk';
7
+ import ora from 'ora';
8
+ import {
9
+ PACKAGE_VERSION,
10
+ REPO_URL,
11
+ SKILLS,
12
+ } from '../config.js';
13
+ import {
14
+ detectPlatforms,
15
+ } from '../platform.js';
16
+ import { cleanupLegacyArtifacts, getSkillList } from '../cleanup.js';
17
+ import {
18
+ installSkills,
19
+ getLocalRepoDir,
20
+ } from '../installer.js';
21
+ import {
22
+ checkForUpdate,
23
+ fetchLatestNpmVersion,
24
+ } from '../downloader.js';
25
+ import { createSkillSymlinks } from '../symlink.js';
26
+ import { getAgentsSkillsDir } from '../config.js';
27
+ import { existsSync } from 'fs';
28
+ import { join } from 'path';
29
+
30
+ /**
31
+ * Check if a platform has any skill symlinks installed
32
+ */
33
+ function hasAnySkillSymlink(platformSkillsDir) {
34
+ if (!platformSkillsDir || !existsSync(platformSkillsDir)) return false;
35
+ const skillList = getSkillList();
36
+ return skillList.some((skill) => existsSync(join(platformSkillsDir, skill)));
37
+ }
38
+
39
+ /**
40
+ * Perform the actual update for a specific scope
41
+ */
42
+ async function performUpdate(baseDir, repoDir, spinner) {
43
+ const detectedPlatforms = detectPlatforms(baseDir);
44
+ const platformsWithSymlinks = detectedPlatforms.filter((p) =>
45
+ hasAnySkillSymlink(p.skillsDir)
46
+ );
47
+
48
+ spinner.start('Syncing skills...');
49
+ const skillsResult = await installSkills(repoDir, baseDir);
50
+ spinner.succeed(`${skillsResult.installed.length} skills updated`);
51
+
52
+ cleanupLegacyArtifacts(baseDir);
53
+
54
+ for (const platform of platformsWithSymlinks) {
55
+ await createSkillSymlinks(
56
+ platform.skillsDir,
57
+ SKILLS,
58
+ baseDir
59
+ );
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Update command handler
65
+ */
66
+ export async function updateCommand(options) {
67
+ console.log('');
68
+ console.log(chalk.bold.blue('AI Skills Updater'));
69
+ console.log('');
70
+
71
+ const spinner = ora();
72
+
73
+ const baseDir = options.local ? process.cwd() : undefined;
74
+
75
+ if (baseDir) {
76
+ console.log(chalk.cyan('Mode: Local update (current project)'));
77
+ } else {
78
+ console.log(chalk.cyan('Mode: Global update (user home)'));
79
+ }
80
+ console.log('');
81
+
82
+ // Verify skills are installed
83
+ const skillsDir = getAgentsSkillsDir(baseDir);
84
+ const hasSkillsInstalled = skillsDir && SKILLS.some((skill) => existsSync(join(skillsDir, skill)));
85
+ if (!hasSkillsInstalled) {
86
+ console.log(chalk.yellow('No skills installed at this location.'));
87
+ console.log('Install them first with:');
88
+ console.log(' aiskills install');
89
+ console.log('');
90
+ return;
91
+ }
92
+
93
+ // Check for updates
94
+ spinner.start('Checking for updates...');
95
+
96
+ try {
97
+ const hasUpdate = await checkForUpdate(PACKAGE_VERSION);
98
+
99
+ if (hasUpdate) {
100
+ let latestVersion = '(newer)';
101
+ try {
102
+ latestVersion = await fetchLatestNpmVersion();
103
+ } catch {
104
+ // Ignore
105
+ }
106
+
107
+ spinner.warn('New version available');
108
+ console.log('');
109
+ console.log(chalk.yellow('A newer version of aiskills is available on npm:'));
110
+ console.log(` Current: ${chalk.gray('v' + PACKAGE_VERSION)}`);
111
+ console.log(` Latest: ${chalk.green(latestVersion)}`);
112
+ console.log('');
113
+ console.log('Update the CLI with:');
114
+ console.log(` ${chalk.cyan('npm update -g @maccesar/aiskills')}`);
115
+ console.log('');
116
+ console.log('After updating, run this command again:');
117
+ console.log(` ${chalk.cyan('aiskills update')}`);
118
+ console.log('');
119
+ return;
120
+ }
121
+
122
+ spinner.succeed(`CLI is up to date (v${PACKAGE_VERSION})`);
123
+
124
+ const repoDir = getLocalRepoDir();
125
+ if (!repoDir) {
126
+ console.log('');
127
+ console.log(chalk.red('Error: Could not locate skills source directory.'));
128
+ console.log('Try reinstalling with:');
129
+ console.log(` ${chalk.cyan('npm install -g @maccesar/aiskills')}`);
130
+ return;
131
+ }
132
+
133
+ await performUpdate(baseDir, repoDir, spinner);
134
+
135
+ console.log('');
136
+ console.log(chalk.green('✓ Update complete!'));
137
+ console.log('');
138
+
139
+ } catch (error) {
140
+ spinner.fail('Update failed');
141
+ console.error(chalk.red(error.message));
142
+ console.log('');
143
+ console.log('You can try manually installing from:');
144
+ console.log(chalk.cyan(REPO_URL));
145
+ process.exit(1);
146
+ }
147
+ }
148
+
149
+ export default updateCommand;
package/lib/config.js ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Configuration constants for AI Skills
3
+ * Single source of truth for version management
4
+ */
5
+
6
+ import path from 'path';
7
+ import os from 'os';
8
+ import { readFileSync } from 'fs';
9
+
10
+ // Read package.json version dynamically
11
+ let packageVersion = '1.0.0';
12
+ try {
13
+ const packagePath = new URL('../package.json', import.meta.url);
14
+ const pkg = JSON.parse(readFileSync(packagePath, 'utf8'));
15
+ packageVersion = pkg.version;
16
+ } catch {
17
+ // Use default version if package.json not found
18
+ }
19
+
20
+ // Version management
21
+ export const PACKAGE_VERSION = packageVersion;
22
+
23
+ // Repository configuration
24
+ export const REPO_URL = 'https://github.com/macCesar/aiskills';
25
+ export const REPO_RAW_URL = 'https://raw.githubusercontent.com/macCesar/aiskills/main';
26
+ export const REPO_API_URL = 'https://api.github.com/repos/macCesar/aiskills';
27
+
28
+ // Skills to install
29
+ export const SKILLS = [
30
+ 'humaniza',
31
+ 'refactoring-ui',
32
+ 'stitch-showcase',
33
+ 'vscode-extension-dev',
34
+ ];
35
+
36
+ // Legacy skills to remove during updates/uninstall
37
+ export const LEGACY_SKILLS = [];
38
+
39
+ // Cache/config directory
40
+ export const getConfigDir = () => path.join(os.homedir(), '.aiskills');
41
+
42
+ // Directory paths
43
+ export const getAgentsSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.agents', 'skills');
44
+ export const getClaudeSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.claude', 'skills');
45
+ export const getGeminiSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.gemini', 'skills');
46
+ export const getCodexSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.codex', 'skills');
47
+
48
+ // AI platform detection
49
+ export const getPlatforms = (baseDir = os.homedir()) => [
50
+ {
51
+ name: 'claude',
52
+ displayName: 'Claude Code',
53
+ skillsDir: getClaudeSkillsDir(baseDir),
54
+ configDir: path.join(baseDir, '.claude'),
55
+ },
56
+ {
57
+ name: 'gemini',
58
+ displayName: 'Gemini CLI',
59
+ skillsDir: getGeminiSkillsDir(baseDir),
60
+ configDir: path.join(baseDir, '.gemini'),
61
+ },
62
+ {
63
+ name: 'codex',
64
+ displayName: 'Codex CLI',
65
+ skillsDir: getCodexSkillsDir(baseDir),
66
+ configDir: path.join(baseDir, '.codex'),
67
+ },
68
+ ];
69
+
70
+ // API configuration
71
+ export const GITHUB_API_HEADERS = {
72
+ Accept: 'application/vnd.github.v3+json',
73
+ 'User-Agent': 'aiskills',
74
+ };
75
+
76
+ export default {
77
+ PACKAGE_VERSION,
78
+ REPO_URL,
79
+ REPO_RAW_URL,
80
+ REPO_API_URL,
81
+ SKILLS,
82
+ LEGACY_SKILLS,
83
+ getConfigDir,
84
+ getAgentsSkillsDir,
85
+ getClaudeSkillsDir,
86
+ getGeminiSkillsDir,
87
+ getCodexSkillsDir,
88
+ getPlatforms,
89
+ GITHUB_API_HEADERS,
90
+ };
@@ -0,0 +1,110 @@
1
+ /**
2
+ * GitHub download utilities
3
+ * Fetches releases and archives from GitHub
4
+ */
5
+
6
+ import { createWriteStream, existsSync, mkdirSync } from 'fs';
7
+ import { tmpdir } from 'os';
8
+ import { join, dirname } from 'path';
9
+ import { pipeline } from 'stream/promises';
10
+ import { unlink } from 'fs/promises';
11
+ import { extract } from 'tar';
12
+ import { REPO_API_URL, REPO_RAW_URL, GITHUB_API_HEADERS } from './config.js';
13
+
14
+ /**
15
+ * Download a file from URL to local path
16
+ * @param {string} url - URL to download
17
+ * @param {string} destPath - Destination path
18
+ * @returns {Promise<void>}
19
+ */
20
+ export async function downloadFile(url, destPath) {
21
+ const response = await fetch(url);
22
+
23
+ if (!response.ok) {
24
+ throw new Error(`Failed to download: ${response.statusText}`);
25
+ }
26
+
27
+ const dir = dirname(destPath);
28
+ if (!existsSync(dir)) {
29
+ mkdirSync(dir, { recursive: true });
30
+ }
31
+
32
+ await pipeline(response.body, createWriteStream(destPath));
33
+ }
34
+
35
+ /**
36
+ * Download and extract GitHub repository archive
37
+ * @param {string} destDir - Destination directory
38
+ * @param {string} ref - Git ref (branch, tag, commit)
39
+ * @returns {Promise<string>} Path to extracted directory
40
+ */
41
+ export async function downloadRepoArchive(destDir, ref = 'main') {
42
+ const archiveUrl = `${REPO_API_URL}/tarball/${ref}`;
43
+ const tempFile = join(tmpdir(), `aiskills-${Date.now()}.tar.gz`);
44
+
45
+ try {
46
+ await downloadFile(archiveUrl, tempFile);
47
+
48
+ await extract({
49
+ file: tempFile,
50
+ cwd: destDir,
51
+ strip: 1,
52
+ });
53
+
54
+ return destDir;
55
+ } finally {
56
+ if (existsSync(tempFile)) {
57
+ await unlink(tempFile);
58
+ }
59
+ }
60
+ }
61
+
62
+ /**
63
+ * Fetch latest version from npm registry
64
+ * @returns {Promise<string>} Latest version number
65
+ */
66
+ export async function fetchLatestNpmVersion() {
67
+ const response = await fetch('https://registry.npmjs.org/@maccesar/aiskills');
68
+
69
+ if (!response.ok) {
70
+ throw new Error(`Failed to fetch npm info: ${response.statusText}`);
71
+ }
72
+
73
+ const data = await response.json();
74
+ return data['dist-tags'].latest;
75
+ }
76
+
77
+ /**
78
+ * Check if an update is available (checks npm)
79
+ * @param {string} currentVersion - Current version
80
+ * @returns {Promise<boolean>} True if update available
81
+ */
82
+ export async function checkForUpdate(currentVersion) {
83
+ try {
84
+ const latestVersion = await fetchLatestNpmVersion();
85
+ const latest = latestVersion.replace(/^v/, '');
86
+ const current = currentVersion.replace(/^v/, '');
87
+
88
+ const latestParts = latest.split('.').map((v) => parseInt(v, 10));
89
+ const currentParts = current.split('.').map((v) => parseInt(v, 10));
90
+
91
+ for (let i = 0; i < Math.max(latestParts.length, currentParts.length); i++) {
92
+ const l = latestParts[i] || 0;
93
+ const c = currentParts[i] || 0;
94
+
95
+ if (l > c) return true;
96
+ if (l < c) return false;
97
+ }
98
+
99
+ return false;
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+
105
+ export default {
106
+ downloadFile,
107
+ downloadRepoArchive,
108
+ fetchLatestNpmVersion,
109
+ checkForUpdate,
110
+ };
package/lib/hooks.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Claude Code hook management
3
+ * Installs/removes the SessionStart hook for auto-update
4
+ */
5
+
6
+ import { existsSync, readFileSync, writeFileSync } from 'fs';
7
+ import { join } from 'path';
8
+
9
+ const HOOK_COMMAND = 'aiskills auto-update --silent';
10
+ const SETTINGS_FILE = 'settings.json';
11
+
12
+ function readSettings(claudeDir) {
13
+ const settingsPath = join(claudeDir, SETTINGS_FILE);
14
+ if (!existsSync(settingsPath)) return {};
15
+ try {
16
+ return JSON.parse(readFileSync(settingsPath, 'utf8'));
17
+ } catch {
18
+ return {};
19
+ }
20
+ }
21
+
22
+ function writeSettings(claudeDir, settings) {
23
+ writeFileSync(
24
+ join(claudeDir, SETTINGS_FILE),
25
+ JSON.stringify(settings, null, 2) + '\n',
26
+ 'utf8'
27
+ );
28
+ }
29
+
30
+ /**
31
+ * Find the hook entry that contains our command
32
+ * Claude Code hook format: { hooks: [{ type: "command", command: "..." }] }
33
+ */
34
+ function findHookEntry(sessionStartHooks) {
35
+ if (!Array.isArray(sessionStartHooks)) return -1;
36
+ return sessionStartHooks.findIndex((entry) =>
37
+ Array.isArray(entry.hooks) &&
38
+ entry.hooks.some((h) => h.type === 'command' && h.command === HOOK_COMMAND)
39
+ );
40
+ }
41
+
42
+ export function hasHook(claudeDir) {
43
+ const settings = readSettings(claudeDir);
44
+ const hooks = settings.hooks?.SessionStart;
45
+ return findHookEntry(hooks) !== -1;
46
+ }
47
+
48
+ export function installHook(claudeDir) {
49
+ if (hasHook(claudeDir)) return;
50
+ const settings = readSettings(claudeDir);
51
+ if (!settings.hooks) settings.hooks = {};
52
+ if (!Array.isArray(settings.hooks.SessionStart)) settings.hooks.SessionStart = [];
53
+ settings.hooks.SessionStart.push({
54
+ hooks: [
55
+ {
56
+ type: 'command',
57
+ command: HOOK_COMMAND,
58
+ },
59
+ ],
60
+ });
61
+ writeSettings(claudeDir, settings);
62
+ }
63
+
64
+ export function removeHook(claudeDir) {
65
+ if (!hasHook(claudeDir)) return;
66
+ const settings = readSettings(claudeDir);
67
+ const idx = findHookEntry(settings.hooks.SessionStart);
68
+ if (idx !== -1) {
69
+ settings.hooks.SessionStart.splice(idx, 1);
70
+ }
71
+ writeSettings(claudeDir, settings);
72
+ }
73
+
74
+ export default { installHook, removeHook, hasHook };