@maccesar/aiskills 1.7.0 → 1.9.2

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 CHANGED
@@ -8,13 +8,22 @@
8
8
 
9
9
  </div>
10
10
 
11
- `aiskills` is a CLI for installing curated skills for AI coding assistants. It installs the skill files and links them to Claude Code, Gemini CLI, or Codex CLI.
11
+ `aiskills` is a toolkit of curated skills for AI coding assistants. It provides skill files for Claude Code, Gemini CLI, or Codex CLI.
12
12
 
13
13
  Each skill is a small knowledge package: a `SKILL.md` file with YAML frontmatter plus a set of reference files. When a prompt matches the skill, the assistant reads those files and answers from the source material.
14
14
 
15
15
  ---
16
16
 
17
- ## Quick setup
17
+ ## Installation
18
+
19
+ ### Option A: Plugin Marketplace (Claude Code only)
20
+
21
+ ```bash
22
+ /plugin marketplace add maccesar/aiskills
23
+ /plugin install aiskills@maccesar-aiskills
24
+ ```
25
+
26
+ ### Option B: CLI (Claude Code, Gemini CLI, Codex CLI)
18
27
 
19
28
  ```bash
20
29
  # 1) Install the CLI
@@ -33,10 +42,14 @@ Installed files:
33
42
  - All skills to `~/.agents/skills/`
34
43
  - Platform symlinks in `~/.claude/skills/`, `~/.gemini/skills/`, or `~/.codex/skills/`
35
44
 
36
- Why install with npm?
37
- - Cross-platform (macOS, Linux, Windows)
38
- - No sudo required
39
- - Simple updates with `aiskills update`
45
+ ### Which option should I use?
46
+
47
+ | | Plugin (Option A) | CLI (Option B) |
48
+ |---|---|---|
49
+ | **Claude Code** | Recommended | Supported |
50
+ | **Gemini CLI** | Not available | Supported |
51
+ | **Codex CLI** | Not available | Supported |
52
+ | **Auto-updates** | Via marketplace | `aiskills update` |
40
53
 
41
54
  ---
42
55
 
@@ -65,6 +78,58 @@ Use `aiskills list` to see available skills from the command line. Pull requests
65
78
 
66
79
  ---
67
80
 
81
+ ## Available commands
82
+
83
+ Slash commands are Claude Code-only. They ship with the plugin (Option A). The CLI distribution (Option B) installs skills only.
84
+
85
+ | Command | Purpose |
86
+ | ---------- | ------------------------------------------------------------------------------ |
87
+ | `/release` | Full release workflow: detect project, bump semver, update CHANGELOG + README, commit, push, tag, GitHub release |
88
+
89
+ ### /release
90
+
91
+ End-to-end release janitor that works across project types: npm, Titanium (`tiapp.xml`), Composer, Cargo, CocoaPods, or versionless (git-tag-only) repos. **Designed for a dirty working tree** — it groups your uncommitted work into semantic commits, then ships the release on top.
92
+
93
+ When to use it:
94
+ - You have weeks of work in the working tree (with maybe a few interim commits you made along the way) and want one command to clean everything into proper semantic commits and ship a release.
95
+ - You maintain `CHANGELOG.md` in Keep-a-Changelog format and want the `[Unreleased]` section promoted automatically.
96
+ - You want the bump level inferred from Conventional Commits across both your existing commits and the proposed new ones, with the option to override.
97
+
98
+ Example prompts:
99
+ ```
100
+ /release
101
+ /release minor
102
+ /release major
103
+ ```
104
+
105
+ How it works:
106
+ 1. **Detect** — reads git status, last tag, existing commits since the tag, version file, `CHANGELOG.md`, `README.md`, and `gh` availability.
107
+ 2. **Group the working tree** — reads each modified/untracked file's diff, infers intent, and groups files into N proposed semantic commits (`feat`, `fix`, `refactor`, `chore`, `docs`, `test`, `build`, `ci`). Excludes screenshots in repo root, scratch files, suspicious binaries — and lists them so you can override.
108
+ 3. **Infer bump** — across the union of (existing commits since tag) + (proposed semantic commits): `BREAKING CHANGE` / `!:` → major, any `feat:` → minor, otherwise patch. An argument overrides.
109
+ 4. **Compose CHANGELOG** — promotes `[Unreleased]` if present, or generates a Keep-a-Changelog entry from the union of all commits being shipped.
110
+ 5. **Show one compact plan and stop** — header line, optional warnings, the N proposed commits with their files, the CHANGELOG entry, the release commit summary, the push/tag/release lines. If the current branch is not main/master, also offers to fast-forward merge or open a PR. **Waits for explicit confirmation.** You can ask it to merge, split, or skip any of the N commits before confirming.
111
+ 6. **Execute** — lands each semantic commit (one at a time, with explicit `git add <files>` per commit, never `git add -A`), then the release commit (bump + CHANGELOG + README), pushes the branch, tags, and creates the GitHub release via `gh`. Optionally fast-forward merges to main or opens a PR if you confirmed that mode.
112
+
113
+ Confirmations:
114
+ - `proceed` / `sí` / `commitea` → release on current branch only.
115
+ - `merge` → release + fast-forward merge to main + push main, and leaves you on `main`. Aborts cleanly if main has diverged.
116
+ - `PR` → release + open pull request to main via `gh`.
117
+
118
+ Language policy (two independent axes):
119
+ - **Axis 1 — Interaction with you** — always in your language. The command detects the language from your messages and locks it before printing anything; if you switch, it switches with you.
120
+ - **Axis 2 — Project artifacts** (CHANGELOG entry, README edits, commit message, tag annotation, GitHub release title **and** body) — all share **one** language, detected from `README.md`. A Spanish README means everything in Spanish (typical for private / local projects); an English README means everything in English (typical for open source). No mixed-language releases. Tie-breaks fall back to `CHANGELOG.md` then `git log`; you can override before confirming.
121
+
122
+ Hard restrictions:
123
+ - Never `--force-push`, `--amend` published commits, or `--no-verify`.
124
+ - Aborts on merge conflicts or rebase-in-progress.
125
+ - Asks before creating the **first** tag on `main` / `master`.
126
+ - Skips push / tag / release gracefully when the repo has no remote or `gh` is not installed.
127
+
128
+ Distribution note:
129
+ - Available via the plugin install (Option A above). Slash commands are not distributed by the npm CLI (Option B) because they are a Claude Code feature.
130
+
131
+ ---
132
+
68
133
  ## How skills work
69
134
 
70
135
  Skills activate based on what you ask. You can write prompts normally:
package/lib/cleanup.js CHANGED
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Cleanup helpers for skills and symlinks
2
+ * Cleanup helpers for skills, commands, and symlinks
3
3
  */
4
4
 
5
5
  import {
6
6
  SKILLS,
7
7
  LEGACY_SKILLS,
8
+ COMMANDS,
9
+ LEGACY_COMMANDS,
8
10
  getAgentsSkillsDir,
11
+ getClaudeCommandsDir,
9
12
  } from './config.js';
10
13
  import { detectPlatforms } from './platform.js';
11
14
  import { existsSync, lstatSync, rmSync } from 'fs';
@@ -16,6 +19,11 @@ export function getSkillList({ includeLegacy = true, legacyOnly = false } = {})
16
19
  return includeLegacy ? [...SKILLS, ...LEGACY_SKILLS] : [...SKILLS];
17
20
  }
18
21
 
22
+ export function getCommandList({ includeLegacy = true, legacyOnly = false } = {}) {
23
+ if (legacyOnly) return [...LEGACY_COMMANDS];
24
+ return includeLegacy ? [...COMMANDS, ...LEGACY_COMMANDS] : [...COMMANDS];
25
+ }
26
+
19
27
  function removeEntriesAtDir(dir, names, { suffix = '', recursive = true } = {}) {
20
28
  const results = { removed: [], failed: [] };
21
29
  if (!dir || !existsSync(dir)) return results;
@@ -51,8 +59,15 @@ export function removeSkills(baseDir, options = {}) {
51
59
  return removeEntriesAtDir(skillsDir, skillList, { recursive: true });
52
60
  }
53
61
 
62
+ export function removeCommands(baseDir, options = {}) {
63
+ const commandsDir = getClaudeCommandsDir(baseDir);
64
+ const commandList = getCommandList(options);
65
+ return removeEntriesAtDir(commandsDir, commandList, { suffix: '.md', recursive: false });
66
+ }
67
+
54
68
  export function cleanupLegacyArtifacts(baseDir) {
55
69
  removeSkills(baseDir, { legacyOnly: true });
70
+ removeCommands(baseDir, { legacyOnly: true });
56
71
 
57
72
  const platforms = detectPlatforms(baseDir);
58
73
  for (const platform of platforms) {
@@ -61,6 +76,7 @@ export function cleanupLegacyArtifacts(baseDir) {
61
76
 
62
77
  if (baseDir) {
63
78
  removeSkills(undefined, { legacyOnly: true });
79
+ removeCommands(undefined, { legacyOnly: true });
64
80
  const globalPlatforms = detectPlatforms();
65
81
  for (const platform of globalPlatforms) {
66
82
  removeLegacySkillSymlinks(platform.skillsDir);
@@ -70,8 +86,10 @@ export function cleanupLegacyArtifacts(baseDir) {
70
86
 
71
87
  export default {
72
88
  getSkillList,
89
+ getCommandList,
73
90
  removeSkillSymlinks,
74
91
  removeLegacySkillSymlinks,
75
92
  removeSkills,
93
+ removeCommands,
76
94
  cleanupLegacyArtifacts,
77
95
  };
@@ -1,76 +1,101 @@
1
1
  /**
2
2
  * List command
3
- * Shows available skills with their descriptions
3
+ * Enumerates available skills with a short description.
4
+ * Reads each skill's SKILL.md frontmatter — no hardcoded list.
4
5
  */
5
6
 
6
7
  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';
8
+ import { existsSync, readFileSync } from 'fs';
9
+ import { join } from 'path';
10
+ import {
11
+ SKILLS,
12
+ PACKAGE_VERSION,
13
+ getAgentsSkillsDir,
14
+ } from '../config.js';
11
15
 
12
- const __dirname = dirname(fileURLToPath(import.meta.url));
16
+ const CHECK = chalk.green('✓');
17
+ const CROSS = chalk.red('✗');
18
+
19
+ /**
20
+ * Extract the skill's human-readable name and short description from SKILL.md.
21
+ * Returns { description, installed } where description is the first sentence of
22
+ * the frontmatter description, trimmed for terminal display.
23
+ */
24
+ function readSkillMetadata(skillDir) {
25
+ const skillMd = join(skillDir, 'SKILL.md');
26
+ if (!existsSync(skillMd)) {
27
+ return { description: null, installed: false };
28
+ }
13
29
 
14
- function parseSkillDescription(skillName) {
15
30
  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
- }
31
+ const content = readFileSync(skillMd, 'utf8');
32
+ const frontmatter = content.match(/^---\n([\s\S]*?)\n---/);
33
+ if (!frontmatter) return { description: null, installed: true };
42
34
 
43
- // Trim long descriptions
44
- const maxLen = 80;
45
- if (description.length > maxLen) {
46
- description = description.substring(0, maxLen - 1).trimEnd() + '…';
47
- }
35
+ const descMatch = frontmatter[1].match(/description:\s*"([^"]+)"|description:\s*(.+)/);
36
+ if (!descMatch) return { description: null, installed: true };
48
37
 
49
- return { name, description };
38
+ const full = descMatch[1] || descMatch[2] || '';
39
+ let short;
40
+ const firstSentence = full.match(/^([^]*?\.)\s/);
41
+ if (firstSentence) {
42
+ short = firstSentence[1];
43
+ } else if (full.length > 80) {
44
+ const cut = full.slice(0, 80);
45
+ short = cut.slice(0, cut.lastIndexOf(' ')) + '…';
46
+ } else {
47
+ short = full;
48
+ }
49
+ return { description: short.trim(), installed: true };
50
50
  } catch {
51
- return null;
51
+ return { description: null, installed: true };
52
52
  }
53
53
  }
54
54
 
55
- /**
56
- * List command handler
57
- */
58
55
  export async function listCommand() {
59
56
  console.log('');
60
- console.log(chalk.bold.blue('Available Skills'));
57
+ console.log(chalk.bold.blue(`AI Skills (v${PACKAGE_VERSION})`));
61
58
  console.log('');
62
59
 
63
- const maxNameLen = Math.max(...SKILLS.map(s => s.length));
60
+ const skillsDir = getAgentsSkillsDir();
61
+
62
+ if (!existsSync(skillsDir)) {
63
+ console.log(chalk.yellow('No skills installed yet.'));
64
+ console.log('Install with:');
65
+ console.log(chalk.cyan(' aiskills install'));
66
+ console.log('');
67
+ return;
68
+ }
69
+
70
+ const rows = [];
71
+ let installedCount = 0;
72
+ let maxNameLen = 0;
73
+
74
+ for (const name of SKILLS) {
75
+ const skillDir = join(skillsDir, name);
76
+ const { description, installed } = readSkillMetadata(skillDir);
64
77
 
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}`);
78
+ if (installed) installedCount++;
79
+ if (name.length > maxNameLen) maxNameLen = name.length;
80
+
81
+ rows.push({ name, description, installed });
70
82
  }
71
83
 
84
+ for (const row of rows) {
85
+ const mark = row.installed ? CHECK : CROSS;
86
+ const paddedName = row.name.padEnd(maxNameLen + 2);
87
+ const desc = row.description
88
+ ? chalk.gray(row.description)
89
+ : chalk.gray(row.installed ? '(no description)' : 'not installed');
90
+
91
+ console.log(` ${mark} ${chalk.cyan(paddedName)} ${desc}`);
92
+ }
93
+
94
+ console.log('');
95
+ console.log(chalk.gray(`${installedCount}/${SKILLS.length} installed at ${skillsDir}`));
72
96
  console.log('');
73
- console.log(chalk.gray(`${SKILLS.length} skill${SKILLS.length !== 1 ? 's' : ''} available`));
97
+ console.log(chalk.gray('Run `aiskills status` for installation health.'));
98
+ console.log(chalk.gray('Run `aiskills doctor` to diagnose issues.'));
74
99
  console.log('');
75
100
  }
76
101
 
@@ -21,9 +21,11 @@ import {
21
21
  removeSkillSymlinks,
22
22
  removeLegacySkillSymlinks,
23
23
  removeSkills,
24
+ removeCommands,
24
25
  } from '../cleanup.js';
25
26
  import {
26
27
  installSkills,
28
+ installCommands,
27
29
  getLocalRepoDir,
28
30
  } from '../installer.js';
29
31
  import { downloadRepoArchive } from '../downloader.js';
@@ -179,6 +181,24 @@ export async function skillsCommand(options) {
179
181
  const skillsResult = await installSkills(repoDir, baseDir);
180
182
  spinner.succeed(`${SKILLS.length} skill${SKILLS.length !== 1 ? 's' : ''} installed`);
181
183
 
184
+ // Install or remove slash commands based on Claude Code selection
185
+ if (selectedPlatformNames.has('claude')) {
186
+ spinner.start('Installing slash commands...');
187
+ const commandsResult = await installCommands(repoDir, baseDir);
188
+ if (commandsResult.installed.length > 0) {
189
+ spinner.succeed(
190
+ `${commandsResult.installed.length} slash command${commandsResult.installed.length !== 1 ? 's' : ''} installed`
191
+ );
192
+ } else {
193
+ spinner.info('No slash commands to install');
194
+ }
195
+ } else {
196
+ const removed = removeCommands(baseDir);
197
+ if (removed.removed.length > 0) {
198
+ console.log(chalk.green('✓'), `${removed.removed.length} slash commands removed`);
199
+ }
200
+ }
201
+
182
202
  // Create symlinks for selected platforms
183
203
  for (const platform of selectedPlatforms) {
184
204
  removeLegacySkillSymlinks(platform.skillsDir);
@@ -198,6 +218,7 @@ export async function skillsCommand(options) {
198
218
  }
199
219
  } else {
200
220
  const skillsResult = removeSkills(baseDir);
221
+ const commandsResult = removeCommands(baseDir);
201
222
  const platformResults = detectedPlatforms.map((platform) => {
202
223
  const symlinkResult = removeSkillSymlinks(platform.skillsDir);
203
224
  return {
@@ -212,6 +233,10 @@ export async function skillsCommand(options) {
212
233
  console.log(chalk.gray('ℹ'), 'No skills to remove');
213
234
  }
214
235
 
236
+ if (commandsResult.removed.length > 0) {
237
+ console.log(chalk.green('✓'), `${commandsResult.removed.length} slash commands removed`);
238
+ }
239
+
215
240
  for (const result of platformResults) {
216
241
  if (result.removedCount > 0) {
217
242
  console.log(chalk.green('✓'), `${result.displayName}: Skills unlinked`);
@@ -73,7 +73,7 @@ export async function statusCommand() {
73
73
  console.log(` Version: v${PACKAGE_VERSION}`);
74
74
  console.log(` Skills: ${installedCount}/${totalCount} installed`);
75
75
  console.log(` Hook: Claude Code SessionStart ${hookExists ? CHECK : CROSS}`);
76
- console.log(` Last check: ${formatLastCheck(lastCheck)}`);
76
+ console.log(` Last npm check: ${formatLastCheck(lastCheck)}`);
77
77
 
78
78
  // Platforms
79
79
  console.log('');
@@ -13,14 +13,15 @@ import {
13
13
  import {
14
14
  removeSkillSymlinks,
15
15
  removeSkills,
16
+ removeCommands,
16
17
  } from '../cleanup.js';
17
18
  import { removeHook } from '../hooks.js';
18
19
  import checkbox, { Separator } from '../prompts/checkboxCancel.js';
19
20
  import { existsSync } from 'fs';
20
21
  import { rm } from 'fs/promises';
21
22
  import { join, resolve } from 'path';
22
- import { getAgentsSkillsDir } from '../config.js';
23
- import { getSkillList } from '../cleanup.js';
23
+ import { getAgentsSkillsDir, getClaudeCommandsDir } from '../config.js';
24
+ import { getSkillList, getCommandList } from '../cleanup.js';
24
25
 
25
26
  /**
26
27
  * Uninstall command handler
@@ -40,14 +41,19 @@ export async function uninstallCommand(options) {
40
41
  const detectedPlatforms = detectPlatforms(baseDir);
41
42
 
42
43
  const skillList = getSkillList();
44
+ const commandList = getCommandList().map((cmd) => `${cmd}.md`);
43
45
  const homeSkillsDir = getAgentsSkillsDir();
44
46
  const projectSkillsDir = getAgentsSkillsDir(projectDir);
47
+ const homeCommandsDir = getClaudeCommandsDir();
48
+ const projectCommandsDir = getClaudeCommandsDir(projectDir);
45
49
 
46
50
  const hasAnyInDir = (dir, names) =>
47
51
  !!dir && existsSync(dir) && names.some((name) => existsSync(join(dir, name)));
48
52
 
49
53
  const hasHomeSkills = hasAnyInDir(homeSkillsDir, skillList);
50
54
  const hasProjectSkills = options.local && hasAnyInDir(projectSkillsDir, skillList);
55
+ const hasHomeCommands = hasAnyInDir(homeCommandsDir, commandList);
56
+ const hasProjectCommands = options.local && hasAnyInDir(projectCommandsDir, commandList);
51
57
 
52
58
  const hasHomeSymlinks = detectedPlatforms.some((platform) =>
53
59
  hasAnyInDir(platform.skillsDir, skillList)
@@ -63,6 +69,12 @@ export async function uninstallCommand(options) {
63
69
  if (hasProjectSkills) {
64
70
  choices.push({ name: 'Skills from the `project` directory', value: 'skills-project', checked: false });
65
71
  }
72
+ if (hasHomeCommands) {
73
+ choices.push({ name: 'Slash commands from `home` directory', value: 'commands-home', checked: true });
74
+ }
75
+ if (hasProjectCommands) {
76
+ choices.push({ name: 'Slash commands from `project` directory', value: 'commands-project', checked: false });
77
+ }
66
78
  if (hasHomeSymlinks) {
67
79
  choices.push({ name: 'Skill symlinks from `home` directory', value: 'symlinks-home', checked: true });
68
80
  }
@@ -71,7 +83,7 @@ export async function uninstallCommand(options) {
71
83
  }
72
84
 
73
85
  if (choices.length === 0) {
74
- console.log(chalk.yellow('No skills or symlinks found.'));
86
+ console.log(chalk.yellow('No skills, commands, or symlinks found.'));
75
87
  console.log('');
76
88
  return;
77
89
  }
@@ -158,6 +170,28 @@ export async function uninstallCommand(options) {
158
170
  }
159
171
  }
160
172
 
173
+ if (targets.includes('commands-home')) {
174
+ spinner.start('Removing slash commands...');
175
+ const result = removeCommands(undefined);
176
+ if (result.removed.length > 0) {
177
+ spinner.succeed(`${result.removed.length} slash commands removed`);
178
+ actionTaken = true;
179
+ } else {
180
+ spinner.info('No slash commands to remove');
181
+ }
182
+ }
183
+
184
+ if (targets.includes('commands-project')) {
185
+ spinner.start('Removing slash commands...');
186
+ const result = removeCommands(projectDir);
187
+ if (result.removed.length > 0) {
188
+ spinner.succeed(`${result.removed.length} slash commands removed`);
189
+ actionTaken = true;
190
+ } else {
191
+ spinner.info('No slash commands to remove');
192
+ }
193
+ }
194
+
161
195
  // Remove Claude Code SessionStart hook
162
196
  const claudeDir = join(os.homedir(), '.claude');
163
197
  removeHook(claudeDir);
@@ -10,12 +10,14 @@ import {
10
10
  REPO_URL,
11
11
  SKILLS,
12
12
  } from '../config.js';
13
+ import select from '../prompts/selectCancel.js';
13
14
  import {
14
15
  detectPlatforms,
15
16
  } from '../platform.js';
16
17
  import { cleanupLegacyArtifacts, getSkillList } from '../cleanup.js';
17
18
  import {
18
19
  installSkills,
20
+ installCommands,
19
21
  getLocalRepoDir,
20
22
  } from '../installer.js';
21
23
  import {
@@ -26,9 +28,12 @@ import { createSkillSymlinks } from '../symlink.js';
26
28
  import { getAgentsSkillsDir } from '../config.js';
27
29
  import { existsSync } from 'fs';
28
30
  import { join } from 'path';
31
+ import os from 'os';
29
32
 
30
33
  /**
31
34
  * Check if a platform has any skill symlinks installed
35
+ * @param {string} platformSkillsDir - Platform skills directory
36
+ * @returns {boolean} True if any skill symlink exists
32
37
  */
33
38
  function hasAnySkillSymlink(platformSkillsDir) {
34
39
  if (!platformSkillsDir || !existsSync(platformSkillsDir)) return false;
@@ -38,6 +43,10 @@ function hasAnySkillSymlink(platformSkillsDir) {
38
43
 
39
44
  /**
40
45
  * Perform the actual update for a specific scope
46
+ * @param {string|undefined} baseDir - Base directory (undefined = global, path = local)
47
+ * @param {string} repoDir - Repository directory
48
+ * @param {Object} spinner - Ora spinner instance
49
+ * @returns {Promise<void>}
41
50
  */
42
51
  async function performUpdate(baseDir, repoDir, spinner) {
43
52
  const detectedPlatforms = detectPlatforms(baseDir);
@@ -49,6 +58,20 @@ async function performUpdate(baseDir, repoDir, spinner) {
49
58
  const skillsResult = await installSkills(repoDir, baseDir);
50
59
  spinner.succeed(`${skillsResult.installed.length} skills updated`);
51
60
 
61
+ // Sync slash commands when Claude Code has skill symlinks installed
62
+ const claudePlatform = platformsWithSymlinks.find((p) => p.name === 'claude');
63
+ if (claudePlatform) {
64
+ spinner.start('Syncing slash commands...');
65
+ const commandsResult = await installCommands(repoDir, baseDir);
66
+ if (commandsResult.installed.length > 0) {
67
+ spinner.succeed(
68
+ `${commandsResult.installed.length} slash command${commandsResult.installed.length !== 1 ? 's' : ''} synced`
69
+ );
70
+ } else {
71
+ spinner.info('No slash commands to sync');
72
+ }
73
+ }
74
+
52
75
  cleanupLegacyArtifacts(baseDir);
53
76
 
54
77
  for (const platform of platformsWithSymlinks) {
@@ -62,6 +85,7 @@ async function performUpdate(baseDir, repoDir, spinner) {
62
85
 
63
86
  /**
64
87
  * Update command handler
88
+ * @param {Object} options - Command options
65
89
  */
66
90
  export async function updateCommand(options) {
67
91
  console.log('');
@@ -70,27 +94,74 @@ export async function updateCommand(options) {
70
94
 
71
95
  const spinner = ora();
72
96
 
73
- const baseDir = options.local ? process.cwd() : undefined;
97
+ let baseDir = options.local ? process.cwd() : undefined;
98
+ const hasSkillsAt = (dir) =>
99
+ SKILLS.some((skill) => existsSync(join(getAgentsSkillsDir(dir), skill)));
100
+
101
+ if (!options.local) {
102
+ const projectDir = process.cwd();
103
+ // When cwd === home, "local" and "global" point to the same .agents/skills dir
104
+ const isHomeDir = projectDir === os.homedir();
105
+ const hasLocalSkills = !isHomeDir && hasSkillsAt(projectDir);
106
+ const hasGlobalSkills = hasSkillsAt(undefined);
107
+
108
+ if (hasLocalSkills && hasGlobalSkills) {
109
+ try {
110
+ const scope = await select({
111
+ message: 'Both local and global skills detected. What do you want to update:',
112
+ choices: [
113
+ { name: 'Global skills (user home)', value: 'global' },
114
+ { name: 'Local skills (current project)', value: 'local' },
115
+ { name: 'Both locations', value: 'both' },
116
+ ],
117
+ theme: {
118
+ style: {
119
+ answer: () => '',
120
+ prefix: () => chalk.cyan('?'),
121
+ },
122
+ },
123
+ });
124
+ if (scope === 'cancel') {
125
+ console.log('Cancelled.');
126
+ process.exit(0);
127
+ }
128
+ if (scope === 'local') {
129
+ baseDir = projectDir;
130
+ } else if (scope === 'both') {
131
+ baseDir = 'both';
132
+ }
133
+ } catch (error) {
134
+ console.log('\nCancelled.');
135
+ process.exit(0);
136
+ }
137
+ } else if (hasLocalSkills && !hasGlobalSkills) {
138
+ baseDir = projectDir;
139
+ }
140
+ }
74
141
 
75
- if (baseDir) {
142
+ if (baseDir === 'both') {
143
+ console.log(chalk.cyan('Mode: Updating both global and local skills'));
144
+ } else if (baseDir) {
76
145
  console.log(chalk.cyan('Mode: Local update (current project)'));
77
146
  } else {
78
147
  console.log(chalk.cyan('Mode: Global update (user home)'));
79
148
  }
80
149
  console.log('');
81
150
 
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;
151
+ if (baseDir !== 'both') {
152
+ const skillsDir = getAgentsSkillsDir(baseDir);
153
+ const hasSkillsInstalled = skillsDir && SKILLS.some((skill) => existsSync(join(skillsDir, skill)));
154
+ if (!hasSkillsInstalled) {
155
+ console.log(chalk.yellow('No skills installed at this location.'));
156
+ console.log('Install them first with:');
157
+ console.log(' aiskills install');
158
+ console.log('');
159
+ console.log('Looked for skills in:');
160
+ console.log(` ${baseDir ? 'Local' : 'Global'}: ${skillsDir}`);
161
+ return;
162
+ }
91
163
  }
92
164
 
93
- // Check for updates
94
165
  spinner.start('Checking for updates...');
95
166
 
96
167
  try {
@@ -101,7 +172,7 @@ export async function updateCommand(options) {
101
172
  try {
102
173
  latestVersion = await fetchLatestNpmVersion();
103
174
  } catch {
104
- // Ignore
175
+ // Ignore error, we already know there's an update
105
176
  }
106
177
 
107
178
  spinner.warn('New version available');
@@ -130,7 +201,16 @@ export async function updateCommand(options) {
130
201
  return;
131
202
  }
132
203
 
133
- await performUpdate(baseDir, repoDir, spinner);
204
+ if (baseDir === 'both') {
205
+ console.log(chalk.bold('Updating global skills...'));
206
+ await performUpdate(undefined, repoDir, spinner);
207
+ console.log('');
208
+
209
+ console.log(chalk.bold('Updating local skills...'));
210
+ await performUpdate(process.cwd(), repoDir, spinner);
211
+ } else {
212
+ await performUpdate(baseDir, repoDir, spinner);
213
+ }
134
214
 
135
215
  console.log('');
136
216
  console.log(chalk.green('✓ Update complete!'));
package/lib/config.js CHANGED
@@ -36,12 +36,21 @@ export const SKILLS = [
36
36
  // Legacy skills to remove during updates/uninstall
37
37
  export const LEGACY_SKILLS = [];
38
38
 
39
+ // Slash commands to install (Claude Code only — copied to ~/.claude/commands/)
40
+ export const COMMANDS = [
41
+ 'release',
42
+ ];
43
+
44
+ // Legacy commands to remove during updates/uninstall
45
+ export const LEGACY_COMMANDS = [];
46
+
39
47
  // Cache/config directory
40
48
  export const getConfigDir = () => path.join(os.homedir(), '.aiskills');
41
49
 
42
50
  // Directory paths
43
51
  export const getAgentsSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.agents', 'skills');
44
52
  export const getClaudeSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.claude', 'skills');
53
+ export const getClaudeCommandsDir = (baseDir = os.homedir()) => path.join(baseDir, '.claude', 'commands');
45
54
  export const getGeminiSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.gemini', 'skills');
46
55
  export const getCodexSkillsDir = (baseDir = os.homedir()) => path.join(baseDir, '.codex', 'skills');
47
56
 
@@ -80,9 +89,12 @@ export default {
80
89
  REPO_API_URL,
81
90
  SKILLS,
82
91
  LEGACY_SKILLS,
92
+ COMMANDS,
93
+ LEGACY_COMMANDS,
83
94
  getConfigDir,
84
95
  getAgentsSkillsDir,
85
96
  getClaudeSkillsDir,
97
+ getClaudeCommandsDir,
86
98
  getGeminiSkillsDir,
87
99
  getCodexSkillsDir,
88
100
  getPlatforms,
package/lib/downloader.js CHANGED
@@ -11,6 +11,39 @@ import { unlink } from 'fs/promises';
11
11
  import { extract } from 'tar';
12
12
  import { REPO_API_URL, REPO_RAW_URL, GITHUB_API_HEADERS } from './config.js';
13
13
 
14
+ function getTestLatestNpmVersion() {
15
+ const value = process.env.AISKILLS_TEST_NPM_LATEST_VERSION;
16
+ return value && value.trim() ? value.trim() : null;
17
+ }
18
+
19
+ /**
20
+ * Fetch latest release info from GitHub API
21
+ * @returns {Promise<Object>} Release information
22
+ */
23
+ export async function fetchLatestRelease() {
24
+ const response = await fetch(
25
+ `${REPO_API_URL}/releases/latest`,
26
+ {
27
+ headers: GITHUB_API_HEADERS,
28
+ }
29
+ );
30
+
31
+ if (!response.ok) {
32
+ throw new Error(`Failed to fetch release info: ${response.statusText}`);
33
+ }
34
+
35
+ return response.json();
36
+ }
37
+
38
+ /**
39
+ * Fetch latest version from GitHub
40
+ * @returns {Promise<string>} Latest version tag
41
+ */
42
+ export async function fetchLatestVersion() {
43
+ const release = await fetchLatestRelease();
44
+ return release.tag_name;
45
+ }
46
+
14
47
  /**
15
48
  * Download a file from URL to local path
16
49
  * @param {string} url - URL to download
@@ -59,11 +92,34 @@ export async function downloadRepoArchive(destDir, ref = 'main') {
59
92
  }
60
93
  }
61
94
 
95
+ /**
96
+ * Download a single file from GitHub raw content
97
+ * @param {string} filePath - Path in repository
98
+ * @param {string} destPath - Local destination path
99
+ * @param {string} ref - Git ref (branch, tag, commit)
100
+ * @returns {Promise<void>}
101
+ */
102
+ export async function downloadRawFile(filePath, destPath, ref = 'main') {
103
+ const url = `${REPO_RAW_URL}/${ref}/${filePath}`;
104
+
105
+ const dir = dirname(destPath);
106
+ if (!existsSync(dir)) {
107
+ mkdirSync(dir, { recursive: true });
108
+ }
109
+
110
+ await downloadFile(url, destPath);
111
+ }
112
+
62
113
  /**
63
114
  * Fetch latest version from npm registry
64
115
  * @returns {Promise<string>} Latest version number
65
116
  */
66
117
  export async function fetchLatestNpmVersion() {
118
+ const testVersion = getTestLatestNpmVersion();
119
+ if (testVersion) {
120
+ return testVersion;
121
+ }
122
+
67
123
  const response = await fetch('https://registry.npmjs.org/@maccesar/aiskills');
68
124
 
69
125
  if (!response.ok) {
@@ -103,8 +159,11 @@ export async function checkForUpdate(currentVersion) {
103
159
  }
104
160
 
105
161
  export default {
162
+ fetchLatestRelease,
163
+ fetchLatestVersion,
106
164
  downloadFile,
107
165
  downloadRepoArchive,
166
+ downloadRawFile,
108
167
  fetchLatestNpmVersion,
109
168
  checkForUpdate,
110
169
  };
package/lib/installer.js CHANGED
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * File installation utilities
3
- * Installs skills to their respective directories
3
+ * Installs skills and slash commands to their respective directories
4
4
  */
5
5
 
6
6
  import {
7
+ copyFileSync,
7
8
  existsSync,
8
9
  mkdirSync,
9
10
  } from 'fs';
@@ -12,9 +13,11 @@ import { remove, copy } from 'fs-extra';
12
13
  import os from 'os';
13
14
  import {
14
15
  SKILLS,
16
+ COMMANDS,
15
17
  getAgentsSkillsDir,
18
+ getClaudeCommandsDir,
16
19
  } from './config.js';
17
- import { removeSkills } from './cleanup.js';
20
+ import { removeSkills, removeCommands } from './cleanup.js';
18
21
 
19
22
  /**
20
23
  * Recursively copy a directory
@@ -91,6 +94,68 @@ export async function installSkills(repoDir, baseDir = os.homedir()) {
91
94
  return results;
92
95
  }
93
96
 
97
+ /**
98
+ * Install a single slash command to the Claude commands directory
99
+ * @param {string} repoDir - Repository directory
100
+ * @param {string} commandName - Name of the command (without .md)
101
+ * @param {string} baseDir - Base directory for installation
102
+ * @returns {Promise<boolean>} True if installed successfully
103
+ */
104
+ export async function installCommand(repoDir, commandName, baseDir = os.homedir()) {
105
+ const commandsDir = getClaudeCommandsDir(baseDir);
106
+ const src = join(repoDir, 'commands', `${commandName}.md`);
107
+ const dest = join(commandsDir, `${commandName}.md`);
108
+
109
+ if (!existsSync(commandsDir)) {
110
+ mkdirSync(commandsDir, { recursive: true });
111
+ }
112
+
113
+ if (!existsSync(src)) {
114
+ return false;
115
+ }
116
+
117
+ if (existsSync(dest)) {
118
+ await remove(dest);
119
+ }
120
+
121
+ copyFileSync(src, dest);
122
+ return true;
123
+ }
124
+
125
+ /**
126
+ * Install all slash commands to the Claude commands directory
127
+ * @param {string} repoDir - Repository directory
128
+ * @param {string} baseDir - Base directory for installation
129
+ * @returns {Promise<Object>} Results object with success/failure counts
130
+ */
131
+ export async function installCommands(repoDir, baseDir = os.homedir()) {
132
+ const results = {
133
+ installed: [],
134
+ failed: [],
135
+ removed: [],
136
+ };
137
+
138
+ const legacyLocal = removeCommands(baseDir, { legacyOnly: true });
139
+ results.removed.push(...legacyLocal.removed);
140
+ results.failed.push(...legacyLocal.failed);
141
+
142
+ if (baseDir && baseDir !== os.homedir()) {
143
+ const legacyGlobal = removeCommands(undefined, { legacyOnly: true });
144
+ results.removed.push(...legacyGlobal.removed);
145
+ results.failed.push(...legacyGlobal.failed);
146
+ }
147
+
148
+ for (const cmd of COMMANDS) {
149
+ if (await installCommand(repoDir, cmd, baseDir)) {
150
+ results.installed.push(cmd);
151
+ } else {
152
+ results.failed.push(cmd);
153
+ }
154
+ }
155
+
156
+ return results;
157
+ }
158
+
94
159
  /**
95
160
  * Get the local repository directory if running from source
96
161
  * @returns {string|null} Local repo directory or null
@@ -110,5 +175,7 @@ export default {
110
175
  copyDirectory,
111
176
  installSkill,
112
177
  installSkills,
178
+ installCommand,
179
+ installCommands,
113
180
  getLocalRepoDir,
114
181
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maccesar/aiskills",
3
- "version": "1.7.0",
3
+ "version": "1.9.2",
4
4
  "description": "AI coding assistant skills for Claude Code, Gemini CLI, and Codex CLI",
5
5
  "type": "module",
6
6
  "bin": {
@@ -1,10 +1,6 @@
1
1
  ---
2
2
  name: refactoring-ui
3
- description: >
4
- Design advisor based exclusively on "Refactoring UI" by Adam Wathan & Steve Schoger.
5
- Use when the user asks for UI/UX design advice, design reviews, visual hierarchy
6
- improvements, color system help, typography guidance, spacing decisions, depth/shadow
7
- usage, image handling, or finishing touches on any interface.
3
+ description: Design advisor based exclusively on "Refactoring UI" by Adam Wathan & Steve Schoger. Use when the user asks for UI/UX design advice, design reviews, visual hierarchy improvements, color system help, typography guidance, spacing decisions, depth/shadow usage, image handling, or finishing touches on any interface.
8
4
  when_to_use: >
9
5
  - User asks "how do I make this look better?"
10
6
  - User asks about color palettes, type scales, or spacing systems
@@ -1,15 +1,6 @@
1
1
  ---
2
2
  name: stitch-showcase
3
- description: >
4
- Use this skill whenever the user has Google Stitch design exports and wants to do anything with them —
5
- build a gallery, organize screens, generate a navigable showcase, process zip files, or browse designs.
6
- Triggers on: "organiza mis diseños de Stitch", "arma el muestrario", "organize my Stitch designs",
7
- "build the showcase", "I have the Stitch zips in X", "process this design folder",
8
- "generate the index for my Stitch screens", "tengo los zips de Stitch", "quiero ver mis pantallas",
9
- or any mention of Stitch exports, screen.png + code.html pairs, or design zip files.
10
- Even if the user just says "I have a zip from Stitch" or "mis exports de Stitch" — use this skill.
11
- Also triggers on: "optimiza el showcase", "mejora las descripciones", "enrich the showcase",
12
- "optimize titles", "optimiza", or any request to improve an existing showcase's content.
3
+ description: Use this skill whenever the user has Google Stitch design exports and wants to do anything with them — build a gallery, organize screens, generate a navigable showcase, process zip files, or browse designs. Triggers on "organiza mis diseños de Stitch", "arma el muestrario", "organize my Stitch designs", "build the showcase", "I have the Stitch zips in X", "process this design folder", "generate the index for my Stitch screens", "tengo los zips de Stitch", "quiero ver mis pantallas", or any mention of Stitch exports, screen.png + code.html pairs, or design zip files. Even if the user just says "I have a zip from Stitch" or "mis exports de Stitch" — use this skill. Also triggers on "optimiza el showcase", "mejora las descripciones", "enrich the showcase", "optimize titles", "optimiza", or any request to improve an existing showcase's content.
13
4
  ---
14
5
 
15
6
  # stitch-showcase
@@ -1,10 +1,6 @@
1
1
  ---
2
2
  name: vscode-extension-dev
3
- description: >
4
- Guide for building VS Code extensions from scratch. Use when the user is creating,
5
- scaffolding, designing, debugging, testing, bundling, or publishing a VS Code extension.
6
- Covers all major API patterns: TreeView, QuickPick, Webview, StatusBar, commands,
7
- configuration, SecretStorage, progress indicators, and esbuild bundling.
3
+ description: Guide for building VS Code extensions from scratch. Use when the user is creating, scaffolding, designing, debugging, testing, bundling, or publishing a VS Code extension. Covers all major API patterns — TreeView, QuickPick, Webview, StatusBar, commands, configuration, SecretStorage, progress indicators, and esbuild bundling.
8
4
  when_to_use: >
9
5
  - User wants to create a new VS Code extension
10
6
  - User asks about VS Code extension APIs (TreeView, Webview, QuickPick, etc.)