cli-ai-skills 1.6.0 → 1.7.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
@@ -1,15 +1,21 @@
1
- # cli-ai-skills v1.5.0
1
+ # cli-ai-skills v1.7.1
2
2
 
3
3
  🚀 **NPX Installer for AI Skills**
4
4
 
5
- Install reusable skills for GitHub Copilot CLI, Claude Code, and OpenAI Codex in one command.
5
+ Install reusable skills for GitHub Copilot CLI, Claude Code, OpenAI Codex, OpenCode, and Gemini CLI in one command.
6
6
 
7
- ![Version](https://img.shields.io/badge/version-1.5.0-blue.svg)
7
+ ![Version](https://img.shields.io/badge/version-1.7.1-blue.svg)
8
8
  ![License](https://img.shields.io/badge/license-MIT-green.svg)
9
9
  ![Node](https://img.shields.io/badge/node-%3E%3D14.0.0-green.svg)
10
10
 
11
11
  ## 🚀 Quick Start
12
12
 
13
+ **Shell installer (recommended):**
14
+ ```bash
15
+ curl -fsSL https://raw.githubusercontent.com/ericandrade/cli-ai-skills/main/scripts/install.sh | bash
16
+ ```
17
+
18
+ **Or use NPX:**
13
19
  ```bash
14
20
  # Zero-config installation (interactive)
15
21
  npx cli-ai-skills
@@ -21,9 +27,11 @@ npx cli-ai-skills -y -q
21
27
  npx cli-ai-skills --bundle essential -y
22
28
  ```
23
29
 
30
+ See [Installation Guide](../docs/INSTALLATION.md) for all methods.
31
+
24
32
  ## 📦 What It Does
25
33
 
26
- 1. 🔍 **Detects** installed AI CLI tools (Copilot, Claude, Codex)
34
+ 1. 🔍 **Detects** installed AI CLI tools (Copilot, Claude, Codex, OpenCode, Gemini)
27
35
  2. 📋 **Lists** all 4 available AI skills
28
36
  3. ⚙️ **Installs** skills to the correct platform directories
29
37
  4. ✅ **Validates** installation success
package/bin/cli.js CHANGED
@@ -1,15 +1,22 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  const { detectTools, getInstallInstructions } = require('../lib/detector');
4
- const { promptPlatforms } = require('../lib/interactive');
4
+ const { promptPlatforms, setupEscapeHandler } = require('../lib/interactive');
5
+ const { setupCleanupHandler } = require('../lib/cleanup');
5
6
  const { installCopilotSkills } = require('../lib/copilot');
6
7
  const { installClaudeSkills } = require('../lib/claude');
7
8
  const { install: installCodexSkills } = require('../lib/codex');
9
+ const { install: installOpenCodeSkills } = require('../lib/opencode');
10
+ const { install: installGeminiSkills } = require('../lib/gemini');
8
11
  const { listBundles, validateBundle } = require('../lib/bundles');
9
12
  const { searchSkills } = require('../lib/search');
13
+ const { displayToolsTable } = require('../lib/ui/table');
14
+ const { checkInstalledVersion, isUpdateAvailable } = require('../lib/version-checker');
15
+ const chalk = require('chalk');
16
+ const inquirer = require('inquirer');
10
17
  const path = require('path');
11
18
 
12
- const VERSION = '1.5.0';
19
+ const VERSION = '1.6.0';
13
20
 
14
21
  // Command aliases
15
22
  const commandAliases = {
@@ -32,7 +39,11 @@ const shortFlags = {
32
39
  async function main() {
33
40
  const args = process.argv.slice(2);
34
41
 
35
- console.log(`\n🚀 cli-ai-skills v${VERSION} - Tri-Platform Installer\n`);
42
+ // Setup handlers
43
+ setupEscapeHandler();
44
+ setupCleanupHandler();
45
+
46
+ console.log(chalk.cyan.bold(`\n🚀 cli-ai-skills v${VERSION} - Multi-Platform Installer\n`));
36
47
 
37
48
  // Handle help
38
49
  if (args.includes('--help') || args.includes('-h')) {
@@ -74,31 +85,32 @@ async function main() {
74
85
  const bundleName = args[bundleIdx + 1];
75
86
  const bundle = validateBundle(bundleName);
76
87
 
77
- console.log(`🔍 Detectando ferramentas AI CLI instaladas...\n`);
88
+ console.log(chalk.cyan('🔍 Detectando ferramentas AI CLI instaladas...\n'));
78
89
  const detected = detectTools();
79
- const hasAny = detected.copilot || detected.claude || detected.codex;
90
+
91
+ // Display tools table
92
+ displayToolsTable(detected);
93
+
94
+ const hasAny = detected.copilot.installed || detected.claude.installed ||
95
+ detected.codex.installed || detected.opencode.installed ||
96
+ detected.gemini.installed;
80
97
 
81
98
  if (!hasAny) {
82
99
  console.log(getInstallInstructions());
83
100
  process.exit(1);
84
101
  }
85
102
 
86
- // Show detected tools
87
- console.log('Ferramentas detectadas:');
88
- if (detected.copilot) console.log(' ✅ GitHub Copilot CLI');
89
- if (detected.claude) console.log(' ✅ Claude Code');
90
- if (detected.codex) console.log(' ✅ OpenAI Codex');
91
- console.log('');
92
-
93
103
  // Check for --yes flag (skip prompts)
94
104
  const skipPrompt = args.includes('-y') || args.includes('--yes');
95
105
 
96
106
  let platforms;
97
107
  if (skipPrompt) {
98
108
  platforms = [];
99
- if (detected.copilot) platforms.push('copilot');
100
- if (detected.claude) platforms.push('claude');
101
- if (detected.codex) platforms.push('codex');
109
+ if (detected.copilot.installed) platforms.push('copilot');
110
+ if (detected.claude.installed) platforms.push('claude');
111
+ if (detected.codex.installed) platforms.push('codex');
112
+ if (detected.opencode.installed) platforms.push('opencode');
113
+ if (detected.gemini.installed) platforms.push('gemini');
102
114
  } else {
103
115
  platforms = await promptPlatforms(detected);
104
116
  }
@@ -127,6 +139,12 @@ async function main() {
127
139
  if (platforms.includes('codex')) {
128
140
  installCodexSkills(repoPath, [skill], quiet);
129
141
  }
142
+ if (platforms.includes('opencode')) {
143
+ installOpenCodeSkills(repoPath, [skill], quiet);
144
+ }
145
+ if (platforms.includes('gemini')) {
146
+ installGeminiSkills(repoPath, [skill], quiet);
147
+ }
130
148
  });
131
149
 
132
150
  if (!quiet) {
@@ -137,22 +155,64 @@ async function main() {
137
155
 
138
156
  // Zero-config mode (no arguments)
139
157
  if (args.length === 0 || command === 'install') {
140
- console.log('🔍 Detectando ferramentas AI CLI instaladas...\n');
158
+ console.log(chalk.cyan('🔍 Detectando ferramentas AI CLI instaladas...\n'));
141
159
 
142
160
  const detected = detectTools();
143
- const hasAny = detected.copilot || detected.claude || detected.codex;
161
+
162
+ // Display tools table
163
+ displayToolsTable(detected);
164
+
165
+ const hasAny = detected.copilot.installed || detected.claude.installed ||
166
+ detected.codex.installed || detected.opencode.installed ||
167
+ detected.gemini.installed;
144
168
 
145
169
  if (!hasAny) {
146
170
  console.log(getInstallInstructions());
147
171
  process.exit(1);
148
172
  }
149
173
 
150
- // Show detected tools
151
- console.log('Ferramentas detectadas:');
152
- if (detected.copilot) console.log(' ✅ GitHub Copilot CLI');
153
- if (detected.claude) console.log(' ✅ Claude Code');
154
- if (detected.codex) console.log(' OpenAI Codex');
155
- console.log('');
174
+ // Check if already installed
175
+ const installInfo = checkInstalledVersion();
176
+
177
+ if (installInfo.installed) {
178
+ console.log(chalk.cyan(`\nℹ️ cli-ai-skills instalado nas seguintes plataformas:\n`));
179
+
180
+ for (const platform of installInfo.platforms) {
181
+ const version = installInfo.versions[platform];
182
+ console.log(chalk.dim(` • ${platform}: v${version}`));
183
+ }
184
+
185
+ if (isUpdateAvailable(installInfo)) {
186
+ console.log(chalk.yellow(`\n⚠️ Nova versão disponível: v${installInfo.latestVersion}\n`));
187
+
188
+ const { update } = await inquirer.prompt([{
189
+ type: 'confirm',
190
+ name: 'update',
191
+ message: 'Deseja atualizar agora?',
192
+ default: true
193
+ }]);
194
+
195
+ if (!update) {
196
+ console.log(chalk.dim('\n❌ Atualização cancelada.\n'));
197
+ process.exit(0);
198
+ }
199
+
200
+ console.log(chalk.cyan('\n🔄 Atualizando skills...\n'));
201
+ } else {
202
+ console.log(chalk.green(`\n✅ Você já possui a versão mais recente (v${installInfo.latestVersion})\n`));
203
+
204
+ const { reinstall } = await inquirer.prompt([{
205
+ type: 'confirm',
206
+ name: 'reinstall',
207
+ message: 'Deseja reinstalar?',
208
+ default: false
209
+ }]);
210
+
211
+ if (!reinstall) {
212
+ process.exit(0);
213
+ }
214
+ }
215
+ }
156
216
 
157
217
  // Check for --yes flag (zero-config mode)
158
218
  const skipPrompt = args.includes('-y') || args.includes('--yes');
@@ -161,20 +221,22 @@ async function main() {
161
221
  if (skipPrompt) {
162
222
  // Auto-select all detected platforms
163
223
  platforms = [];
164
- if (detected.copilot) platforms.push('copilot');
165
- if (detected.claude) platforms.push('claude');
166
- if (detected.codex) platforms.push('codex');
224
+ if (detected.copilot.installed) platforms.push('copilot');
225
+ if (detected.claude.installed) platforms.push('claude');
226
+ if (detected.codex.installed) platforms.push('codex');
227
+ if (detected.opencode.installed) platforms.push('opencode');
228
+ if (detected.gemini.installed) platforms.push('gemini');
167
229
  } else {
168
230
  // Interactive selection
169
231
  platforms = await promptPlatforms(detected);
170
232
  }
171
233
 
172
234
  if (platforms.length === 0) {
173
- console.log('\n❌ Instalação cancelada.\n');
235
+ console.log(chalk.red('\n❌ Instalação cancelada.\n'));
174
236
  process.exit(0);
175
237
  }
176
238
 
177
- console.log(`\n📦 Instalando skills para: ${platforms.join(', ')}\n`);
239
+ console.log(chalk.cyan(`\n📦 Instalando skills para: ${platforms.join(', ')}\n`));
178
240
 
179
241
  const repoPath = path.resolve(__dirname, '../..');
180
242
  const quiet = args.includes('-q') || args.includes('--quiet');
@@ -192,8 +254,16 @@ async function main() {
192
254
  installCodexSkills(repoPath, null, quiet);
193
255
  }
194
256
 
257
+ if (platforms.includes('opencode')) {
258
+ installOpenCodeSkills(repoPath, null, quiet);
259
+ }
260
+
261
+ if (platforms.includes('gemini')) {
262
+ installGeminiSkills(repoPath, null, quiet);
263
+ }
264
+
195
265
  if (!quiet) {
196
- console.log(`\n✅ Instalação concluída com sucesso!\n`);
266
+ console.log(chalk.green(`\n✅ Instalação concluída com sucesso!\n`));
197
267
  }
198
268
  return;
199
269
  }
package/lib/cleanup.js ADDED
@@ -0,0 +1,92 @@
1
+ const fs = require('fs-extra');
2
+ const chalk = require('chalk');
3
+
4
+ /**
5
+ * Track of partial installations for cleanup on cancellation
6
+ */
7
+ const partialInstalls = [];
8
+
9
+ /**
10
+ * Register a path as partially installed
11
+ * @param {string} installPath - Path to the installed skill
12
+ */
13
+ function registerPartialInstall(installPath) {
14
+ if (!partialInstalls.includes(installPath)) {
15
+ partialInstalls.push(installPath);
16
+ }
17
+ }
18
+
19
+ /**
20
+ * Clear the partial installs registry
21
+ */
22
+ function clearPartialInstalls() {
23
+ partialInstalls.length = 0;
24
+ }
25
+
26
+ /**
27
+ * Reverte instalações parciais em caso de cancelamento
28
+ * @returns {Promise<void>}
29
+ */
30
+ async function revertPartialInstalls() {
31
+ if (partialInstalls.length === 0) return;
32
+
33
+ console.log(chalk.cyan('\n🧹 Limpando instalações parciais...\n'));
34
+
35
+ let removed = 0;
36
+ let failed = 0;
37
+
38
+ for (const installPath of partialInstalls) {
39
+ try {
40
+ if (await fs.pathExists(installPath)) {
41
+ // Check if it's a symlink
42
+ const stats = await fs.lstat(installPath);
43
+
44
+ if (stats.isSymbolicLink()) {
45
+ await fs.unlink(installPath);
46
+ } else {
47
+ await fs.remove(installPath);
48
+ }
49
+
50
+ console.log(chalk.green(` ✓ Removido: ${installPath}`));
51
+ removed++;
52
+ }
53
+ } catch (err) {
54
+ console.log(chalk.red(` ✗ Erro ao remover ${installPath}: ${err.message}`));
55
+ failed++;
56
+ }
57
+ }
58
+
59
+ if (removed > 0) {
60
+ console.log(chalk.green(`\n✅ Cleanup concluído (${removed} removidos).\n`));
61
+ }
62
+
63
+ if (failed > 0) {
64
+ console.log(chalk.yellow(`⚠️ ${failed} itens falharam ao remover.\n`));
65
+ }
66
+
67
+ clearPartialInstalls();
68
+ }
69
+
70
+ /**
71
+ * Setup cleanup handler for process termination
72
+ */
73
+ function setupCleanupHandler() {
74
+ // Handle Ctrl+C
75
+ process.on('SIGINT', async () => {
76
+ await revertPartialInstalls();
77
+ process.exit(0);
78
+ });
79
+
80
+ // Handle termination
81
+ process.on('SIGTERM', async () => {
82
+ await revertPartialInstalls();
83
+ process.exit(0);
84
+ });
85
+ }
86
+
87
+ module.exports = {
88
+ registerPartialInstall,
89
+ clearPartialInstalls,
90
+ revertPartialInstalls,
91
+ setupCleanupHandler
92
+ };
@@ -27,8 +27,8 @@ async function uninstallCommand(skillName, options) {
27
27
  // If specific skill name provided, only check that one
28
28
  const skillsToCheck = skillName ? [skillName] : await getInstalledSkillNames();
29
29
 
30
- for (const platform of ['copilot', 'claude']) {
31
- if (!platforms[platform].installed) continue;
30
+ for (const platform of ['copilot', 'claude', 'opencode', 'gemini']) {
31
+ if (!platforms[platform] || !platforms[platform].installed) continue;
32
32
 
33
33
  for (const skill of skillsToCheck) {
34
34
  const status = await versionChecker.checkVersion(skill, platform, '999.999.999');
@@ -184,31 +184,38 @@ async function uninstallCommand(skillName, options) {
184
184
  function getSkillPath(skillName, platform) {
185
185
  const homeDir = os.homedir();
186
186
 
187
- if (platform === 'copilot') {
188
- return path.join(homeDir, '.copilot', 'skills', skillName);
189
- } else if (platform === 'claude') {
190
- return path.join(homeDir, '.claude', 'skills', skillName);
187
+ const platformPaths = {
188
+ copilot: path.join(homeDir, '.copilot', 'skills', skillName),
189
+ claude: path.join(homeDir, '.claude', 'skills', skillName),
190
+ opencode: path.join(homeDir, '.opencode', 'skills', skillName),
191
+ gemini: path.join(homeDir, '.gemini', 'skills', skillName)
192
+ };
193
+
194
+ if (!platformPaths[platform]) {
195
+ throw new Error(`Unknown platform: ${platform}`);
191
196
  }
192
197
 
193
- throw new Error(`Unknown platform: ${platform}`);
198
+ return platformPaths[platform];
194
199
  }
195
200
 
196
201
  async function getInstalledSkillNames() {
197
202
  const homeDir = os.homedir();
198
203
  const skills = new Set();
199
204
 
200
- // Check Copilot skills
201
- const copilotSkillsDir = path.join(homeDir, '.copilot', 'skills');
202
- if (await fs.pathExists(copilotSkillsDir)) {
203
- const copilotSkills = await fs.readdir(copilotSkillsDir);
204
- copilotSkills.forEach(s => skills.add(s));
205
- }
206
-
207
- // Check Claude skills
208
- const claudeSkillsDir = path.join(homeDir, '.claude', 'skills');
209
- if (await fs.pathExists(claudeSkillsDir)) {
210
- const claudeSkills = await fs.readdir(claudeSkillsDir);
211
- claudeSkills.forEach(s => skills.add(s));
205
+ const platforms = ['copilot', 'claude', 'opencode', 'gemini'];
206
+
207
+ for (const platform of platforms) {
208
+ const skillsDir = path.join(homeDir, `.${platform}`, 'skills');
209
+
210
+ if (await fs.pathExists(skillsDir)) {
211
+ const platformSkills = await fs.readdir(skillsDir);
212
+ platformSkills.forEach(s => {
213
+ const fullPath = path.join(skillsDir, s);
214
+ if (fs.statSync(fullPath).isDirectory()) {
215
+ skills.add(s);
216
+ }
217
+ });
218
+ }
212
219
  }
213
220
 
214
221
  return Array.from(skills);
@@ -31,8 +31,8 @@ async function updateCommand(skillNames, options) {
31
31
  const availableSkills = await downloader.listAvailableSkills();
32
32
  const outdatedSkills = [];
33
33
 
34
- for (const platform of ['copilot', 'claude']) {
35
- if (!platforms[platform].installed) continue;
34
+ for (const platform of ['copilot', 'claude', 'opencode', 'gemini']) {
35
+ if (!platforms[platform] || !platforms[platform].installed) continue;
36
36
 
37
37
  for (const skillMeta of availableSkills) {
38
38
  const status = await versionChecker.checkVersion(skillMeta.name, platform, skillMeta.version);
@@ -54,6 +54,21 @@ async function updateCommand(skillNames, options) {
54
54
  if (outdatedSkills.length === 0) {
55
55
  gauge.complete('All skills are up to date');
56
56
  console.log(chalk.green('\n✨ All skills are already at the latest version!\n'));
57
+
58
+ // Offer reinstall option
59
+ if (!options.yes) {
60
+ const { reinstall } = await inquirer.prompt([{
61
+ type: 'confirm',
62
+ name: 'reinstall',
63
+ message: 'Would you like to reinstall all skills?',
64
+ default: false
65
+ }]);
66
+
67
+ if (reinstall) {
68
+ return await reinstallAllSkills(platforms, availableSkills, options);
69
+ }
70
+ }
71
+
57
72
  return;
58
73
  }
59
74
 
@@ -140,3 +155,43 @@ async function updateCommand(skillNames, options) {
140
155
  }
141
156
 
142
157
  module.exports = updateCommand;
158
+
159
+ /**
160
+ * Reinstall all skills for all platforms
161
+ */
162
+ async function reinstallAllSkills(platforms, availableSkills, options) {
163
+ console.log(chalk.cyan('\n🔄 Reinstalling all skills...\n'));
164
+
165
+ const installer = new SkillInstaller();
166
+ const results = { reinstalled: 0, failed: 0 };
167
+
168
+ for (const platform of ['copilot', 'claude', 'opencode', 'gemini']) {
169
+ if (!platforms[platform] || !platforms[platform].installed) continue;
170
+
171
+ for (const skill of availableSkills) {
172
+ try {
173
+ await installer.install(skill.name, platform, 'global', 'symlink');
174
+ results.reinstalled++;
175
+ console.log(chalk.green(` ✓ ${skill.name} (${platform})`));
176
+ } catch (error) {
177
+ results.failed++;
178
+ console.log(chalk.red(` ✗ ${skill.name} (${platform}): ${error.message}`));
179
+ }
180
+ }
181
+ }
182
+
183
+ // Summary
184
+ console.log('\n' + ProgressGauge.summary({
185
+ completed: results.reinstalled,
186
+ failed: results.failed,
187
+ total: results.reinstalled + results.failed
188
+ }));
189
+
190
+ if (results.reinstalled > 0) {
191
+ console.log(chalk.green(`\n✨ Successfully reinstalled ${results.reinstalled} skill(s)!\n`));
192
+ }
193
+
194
+ if (results.failed > 0) {
195
+ console.log(chalk.yellow(`⚠️ ${results.failed} skill(s) failed to reinstall.\n`));
196
+ }
197
+ }
package/lib/detector.js CHANGED
@@ -2,40 +2,95 @@ const { execSync } = require('child_process');
2
2
 
3
3
  /**
4
4
  * Detecta ferramentas AI CLI instaladas no sistema
5
- * @returns {Object} { copilot: boolean, claude: boolean, codex: boolean }
5
+ * @returns {Object} { copilot: {installed, version, path}, claude: {...}, ... }
6
6
  */
7
7
  function detectTools() {
8
8
  const tools = {
9
- copilot: false,
10
- claude: false,
11
- codex: false
9
+ copilot: detectCopilot(),
10
+ claude: detectClaude(),
11
+ codex: detectCodex(),
12
+ opencode: detectOpenCode(),
13
+ gemini: detectGemini()
12
14
  };
13
15
 
14
- // Detectar GitHub Copilot CLI
16
+ return tools;
17
+ }
18
+
19
+ /**
20
+ * Detecta GitHub Copilot CLI
21
+ */
22
+ function detectCopilot() {
15
23
  try {
16
- execSync('gh copilot --version', { stdio: 'ignore' });
17
- tools.copilot = true;
24
+ const version = execSync('gh copilot --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
25
+ const path = execSync('which gh', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
26
+ return { installed: true, version, path };
18
27
  } catch (e) {
19
- // Não instalado
28
+ return { installed: false, version: null, path: null };
20
29
  }
30
+ }
21
31
 
22
- // Detectar Claude Code
32
+ /**
33
+ * Detecta Claude Code
34
+ */
35
+ function detectClaude() {
23
36
  try {
24
- execSync('claude --version', { stdio: 'ignore' });
25
- tools.claude = true;
37
+ const version = execSync('claude --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
38
+ const path = execSync('which claude', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
39
+ return { installed: true, version, path };
26
40
  } catch (e) {
27
- // Não instalado
41
+ return { installed: false, version: null, path: null };
28
42
  }
43
+ }
29
44
 
30
- // Detectar OpenAI Codex
45
+ /**
46
+ * Detecta OpenAI Codex
47
+ */
48
+ function detectCodex() {
31
49
  try {
32
- execSync('codex --version', { stdio: 'ignore' });
33
- tools.codex = true;
50
+ const version = execSync('codex --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
51
+ const path = execSync('which codex', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
52
+ return { installed: true, version, path };
34
53
  } catch (e) {
35
- // Não instalado
54
+ return { installed: false, version: null, path: null };
36
55
  }
56
+ }
37
57
 
38
- return tools;
58
+ /**
59
+ * Detecta OpenCode
60
+ */
61
+ function detectOpenCode() {
62
+ try {
63
+ const version = execSync('opencode --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
64
+ const path = execSync('which opencode', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
65
+ return { installed: true, version, path };
66
+ } catch (e) {
67
+ // Método alternativo: verificar via npm global
68
+ try {
69
+ execSync('npm list -g opencode', { stdio: 'ignore' });
70
+ return { installed: true, version: 'unknown', path: 'npm global' };
71
+ } catch {
72
+ return { installed: false, version: null, path: null };
73
+ }
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Detecta Gemini CLI
79
+ */
80
+ function detectGemini() {
81
+ try {
82
+ const version = execSync('gemini --version', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
83
+ const path = execSync('which gemini', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim();
84
+ return { installed: true, version, path };
85
+ } catch (e) {
86
+ // Método alternativo: verificar via npm global
87
+ try {
88
+ execSync('npm list -g gemini-cli', { stdio: 'ignore' });
89
+ return { installed: true, version: 'unknown', path: 'npm global' };
90
+ } catch {
91
+ return { installed: false, version: null, path: null };
92
+ }
93
+ }
39
94
  }
40
95
 
41
96
  /**
@@ -58,8 +113,15 @@ Instale ao menos uma das seguintes ferramentas:
58
113
  📦 OpenAI Codex:
59
114
  npm install -g @openai/codex
60
115
 
116
+ 📦 OpenCode:
117
+ npm install -g opencode
118
+
119
+ 📦 Gemini CLI:
120
+ npm install -g @google/gemini-cli
121
+
61
122
  Após instalar, execute novamente: npx cli-ai-skills
62
123
  `;
63
124
  }
64
125
 
65
126
  module.exports = { detectTools, getInstallInstructions };
127
+
package/lib/gemini.js ADDED
@@ -0,0 +1,73 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const os = require('os');
5
+
6
+ /**
7
+ * Install skills for Gemini CLI
8
+ * @param {string} repoPath - Path to the cli-ai-skills repository
9
+ * @param {Array<string>|null} skills - Skills to install (null = all)
10
+ * @param {boolean} quiet - Suppress output
11
+ */
12
+ function install(repoPath, skills = null, quiet = false) {
13
+ const homeDir = os.homedir();
14
+ const targetDir = path.join(homeDir, '.gemini', 'skills');
15
+
16
+ // Criar diretório se não existir
17
+ fs.ensureDirSync(targetDir);
18
+
19
+ const sourceDir = path.join(repoPath, '.gemini', 'skills');
20
+
21
+ if (!fs.existsSync(sourceDir)) {
22
+ if (!quiet) {
23
+ console.log(chalk.red('❌ Diretório .gemini/skills não encontrado no repositório'));
24
+ }
25
+ return;
26
+ }
27
+
28
+ // Listar skills disponíveis
29
+ const availableSkills = fs.readdirSync(sourceDir).filter(f => {
30
+ const fullPath = path.join(sourceDir, f);
31
+ return fs.statSync(fullPath).isDirectory() && f !== 'node_modules';
32
+ });
33
+
34
+ const skillsToInstall = skills || availableSkills;
35
+
36
+ let installed = 0;
37
+ let failed = 0;
38
+
39
+ skillsToInstall.forEach(skill => {
40
+ const sourcePath = path.join(sourceDir, skill);
41
+ const targetPath = path.join(targetDir, skill);
42
+
43
+ if (!fs.existsSync(sourcePath)) {
44
+ if (!quiet) {
45
+ console.log(chalk.yellow(`⚠️ Skill não encontrada: ${skill}`));
46
+ }
47
+ failed++;
48
+ return;
49
+ }
50
+
51
+ // Criar symlink
52
+ try {
53
+ if (fs.existsSync(targetPath)) {
54
+ fs.removeSync(targetPath);
55
+ }
56
+ fs.symlinkSync(sourcePath, targetPath);
57
+
58
+ if (!quiet) {
59
+ console.log(chalk.green(` ✓ Gemini: ${skill}`));
60
+ }
61
+ installed++;
62
+ } catch (err) {
63
+ if (!quiet) {
64
+ console.log(chalk.red(` ✗ Erro ao instalar ${skill}: ${err.message}`));
65
+ }
66
+ failed++;
67
+ }
68
+ });
69
+
70
+ return { installed, failed };
71
+ }
72
+
73
+ module.exports = { install };
@@ -1,8 +1,56 @@
1
1
  const inquirer = require('inquirer');
2
+ const chalk = require('chalk');
3
+
4
+ // ESC handler state
5
+ let escListenerActive = false;
6
+ let currentPrompt = null;
7
+
8
+ /**
9
+ * Setup ESC key handler for cancelling prompts
10
+ */
11
+ function setupEscapeHandler() {
12
+ if (escListenerActive) return;
13
+
14
+ const readline = require('readline');
15
+ readline.emitKeypressEvents(process.stdin);
16
+
17
+ if (process.stdin.isTTY) {
18
+ process.stdin.setRawMode(true);
19
+ }
20
+
21
+ process.stdin.on('keypress', async (str, key) => {
22
+ if (key && key.name === 'escape') {
23
+ await confirmCancel();
24
+ }
25
+ });
26
+
27
+ escListenerActive = true;
28
+ }
29
+
30
+ /**
31
+ * Confirm cancellation with user
32
+ */
33
+ async function confirmCancel() {
34
+ console.log('\n'); // New line for better UX
35
+
36
+ const { cancel } = await inquirer.prompt([{
37
+ type: 'confirm',
38
+ name: 'cancel',
39
+ message: chalk.yellow('⚠️ Deseja cancelar a instalação?'),
40
+ default: false
41
+ }]);
42
+
43
+ if (cancel) {
44
+ console.log(chalk.red('\n❌ Instalação cancelada pelo usuário.\n'));
45
+ process.exit(0);
46
+ } else {
47
+ console.log(chalk.dim('Continuando...\n'));
48
+ }
49
+ }
2
50
 
3
51
  /**
4
52
  * Pergunta ao usuário para quais plataformas instalar
5
- * @param {Object} detected - Ferramentas detectadas { copilot, claude, codex }
53
+ * @param {Object} detected - Ferramentas detectadas { copilot, claude, codex, opencode, gemini }
6
54
  * @returns {Promise<Array>} Plataformas escolhidas
7
55
  */
8
56
  async function promptPlatforms(detected) {
@@ -31,6 +79,22 @@ async function promptPlatforms(detected) {
31
79
  checked: true
32
80
  });
33
81
  }
82
+
83
+ if (detected.opencode) {
84
+ choices.push({
85
+ name: '✅ OpenCode (.opencode/skills/)',
86
+ value: 'opencode',
87
+ checked: true
88
+ });
89
+ }
90
+
91
+ if (detected.gemini) {
92
+ choices.push({
93
+ name: '✅ Gemini CLI (.gemini/skills/)',
94
+ value: 'gemini',
95
+ checked: true
96
+ });
97
+ }
34
98
 
35
99
  if (choices.length === 0) {
36
100
  return [];
@@ -40,7 +104,7 @@ async function promptPlatforms(detected) {
40
104
  {
41
105
  type: 'checkbox',
42
106
  name: 'platforms',
43
- message: 'Instalar skills para quais plataformas?',
107
+ message: 'Instalar skills para quais plataformas? (Pressione ESC para cancelar)',
44
108
  choices: choices,
45
109
  validate: (answer) => {
46
110
  if (answer.length < 1) {
@@ -54,4 +118,4 @@ async function promptPlatforms(detected) {
54
118
  return answers.platforms;
55
119
  }
56
120
 
57
- module.exports = { promptPlatforms };
121
+ module.exports = { promptPlatforms, setupEscapeHandler, confirmCancel };
@@ -0,0 +1,73 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const os = require('os');
5
+
6
+ /**
7
+ * Install skills for OpenCode
8
+ * @param {string} repoPath - Path to the cli-ai-skills repository
9
+ * @param {Array<string>|null} skills - Skills to install (null = all)
10
+ * @param {boolean} quiet - Suppress output
11
+ */
12
+ function install(repoPath, skills = null, quiet = false) {
13
+ const homeDir = os.homedir();
14
+ const targetDir = path.join(homeDir, '.opencode', 'skills');
15
+
16
+ // Criar diretório se não existir
17
+ fs.ensureDirSync(targetDir);
18
+
19
+ const sourceDir = path.join(repoPath, '.opencode', 'skills');
20
+
21
+ if (!fs.existsSync(sourceDir)) {
22
+ if (!quiet) {
23
+ console.log(chalk.red('❌ Diretório .opencode/skills não encontrado no repositório'));
24
+ }
25
+ return;
26
+ }
27
+
28
+ // Listar skills disponíveis
29
+ const availableSkills = fs.readdirSync(sourceDir).filter(f => {
30
+ const fullPath = path.join(sourceDir, f);
31
+ return fs.statSync(fullPath).isDirectory() && f !== 'node_modules';
32
+ });
33
+
34
+ const skillsToInstall = skills || availableSkills;
35
+
36
+ let installed = 0;
37
+ let failed = 0;
38
+
39
+ skillsToInstall.forEach(skill => {
40
+ const sourcePath = path.join(sourceDir, skill);
41
+ const targetPath = path.join(targetDir, skill);
42
+
43
+ if (!fs.existsSync(sourcePath)) {
44
+ if (!quiet) {
45
+ console.log(chalk.yellow(`⚠️ Skill não encontrada: ${skill}`));
46
+ }
47
+ failed++;
48
+ return;
49
+ }
50
+
51
+ // Criar symlink
52
+ try {
53
+ if (fs.existsSync(targetPath)) {
54
+ fs.removeSync(targetPath);
55
+ }
56
+ fs.symlinkSync(sourcePath, targetPath);
57
+
58
+ if (!quiet) {
59
+ console.log(chalk.green(` ✓ OpenCode: ${skill}`));
60
+ }
61
+ installed++;
62
+ } catch (err) {
63
+ if (!quiet) {
64
+ console.log(chalk.red(` ✗ Erro ao instalar ${skill}: ${err.message}`));
65
+ }
66
+ failed++;
67
+ }
68
+ });
69
+
70
+ return { installed, failed };
71
+ }
72
+
73
+ module.exports = { install };
@@ -0,0 +1,58 @@
1
+ const chalk = require('chalk');
2
+
3
+ /**
4
+ * Exibe tabela formatada de ferramentas detectadas
5
+ * @param {Object} tools - Objeto retornado por detectTools()
6
+ */
7
+ function displayToolsTable(tools) {
8
+ console.log('\n┌─────────────────────────────────────────────────────────────┐');
9
+ console.log('│ Ferramenta │ Status │ Versão │');
10
+ console.log('├─────────────────────────────────────────────────────────────┤');
11
+
12
+ const toolNames = {
13
+ copilot: 'GitHub Copilot CLI',
14
+ claude: 'Claude Code',
15
+ codex: 'OpenAI Codex',
16
+ opencode: 'OpenCode',
17
+ gemini: 'Gemini CLI'
18
+ };
19
+
20
+ for (const [key, name] of Object.entries(toolNames)) {
21
+ const tool = tools[key];
22
+ const status = tool.installed ? chalk.green('✓') : chalk.red('✗');
23
+ const version = tool.version || chalk.gray('-');
24
+
25
+ // Formatar linha com espaçamento fixo
26
+ const namePadded = name.padEnd(21);
27
+ const statusPadded = ' ' + status + ' ';
28
+ const versionStr = String(version).substring(0, 20);
29
+ const versionPadded = versionStr.padEnd(20);
30
+
31
+ console.log(`│ ${namePadded} │${statusPadded}│ ${versionPadded}│`);
32
+ }
33
+
34
+ console.log('└─────────────────────────────────────────────────────────────┘\n');
35
+ }
36
+
37
+ /**
38
+ * Retorna resumo de ferramentas detectadas (simples)
39
+ * @param {Object} tools - Objeto retornado por detectTools()
40
+ * @returns {Object} { total: number, installed: number, names: string[] }
41
+ */
42
+ function getToolsSummary(tools) {
43
+ const installed = [];
44
+
45
+ if (tools.copilot && tools.copilot.installed) installed.push('copilot');
46
+ if (tools.claude && tools.claude.installed) installed.push('claude');
47
+ if (tools.codex && tools.codex.installed) installed.push('codex');
48
+ if (tools.opencode && tools.opencode.installed) installed.push('opencode');
49
+ if (tools.gemini && tools.gemini.installed) installed.push('gemini');
50
+
51
+ return {
52
+ total: 5,
53
+ installed: installed.length,
54
+ names: installed
55
+ };
56
+ }
57
+
58
+ module.exports = { displayToolsTable, getToolsSummary };
@@ -0,0 +1,130 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const os = require('os');
4
+ const semver = require('semver');
5
+ const yaml = require('js-yaml');
6
+
7
+ /**
8
+ * Verifica se cli-ai-skills já está instalado em alguma plataforma
9
+ * @returns {Object} { installed: boolean, platforms: [], versions: {}, latestVersion: string }
10
+ */
11
+ function checkInstalledVersion() {
12
+ const result = {
13
+ installed: false,
14
+ platforms: [],
15
+ versions: {},
16
+ latestVersion: require('../package.json').version
17
+ };
18
+
19
+ const homeDir = os.homedir();
20
+
21
+ const skillDirs = {
22
+ copilot: path.join(homeDir, '.copilot', 'skills'),
23
+ claude: path.join(homeDir, '.claude', 'skills'),
24
+ codex: path.join(homeDir, '.codex', 'skills'),
25
+ opencode: path.join(homeDir, '.opencode', 'skills'),
26
+ gemini: path.join(homeDir, '.gemini', 'skills')
27
+ };
28
+
29
+ for (const [platform, skillDir] of Object.entries(skillDirs)) {
30
+ if (!fs.existsSync(skillDir)) continue;
31
+
32
+ // Verificar se há skills instaladas (procurar por skill-creator ou prompt-engineer)
33
+ const testSkillPath = path.join(skillDir, 'skill-creator', 'SKILL.md');
34
+
35
+ if (fs.existsSync(testSkillPath)) {
36
+ result.installed = true;
37
+ result.platforms.push(platform);
38
+
39
+ // Extrair versão do YAML frontmatter
40
+ try {
41
+ const content = fs.readFileSync(testSkillPath, 'utf-8');
42
+ const yamlMatch = content.match(/^---\n([\s\S]*?)\n---/);
43
+
44
+ if (yamlMatch) {
45
+ const metadata = yaml.load(yamlMatch[1]);
46
+ result.versions[platform] = metadata.version || 'unknown';
47
+ } else {
48
+ result.versions[platform] = 'unknown';
49
+ }
50
+ } catch (err) {
51
+ result.versions[platform] = 'unknown';
52
+ }
53
+ }
54
+ }
55
+
56
+ return result;
57
+ }
58
+
59
+ /**
60
+ * Compara versões e retorna se atualização está disponível
61
+ * @param {Object} installInfo - Retorno de checkInstalledVersion()
62
+ * @returns {boolean} true se nova versão disponível
63
+ */
64
+ function isUpdateAvailable(installInfo) {
65
+ if (!installInfo.installed) return false;
66
+
67
+ const { versions, latestVersion } = installInfo;
68
+
69
+ for (const installedVersion of Object.values(versions)) {
70
+ if (installedVersion === 'unknown') continue;
71
+
72
+ try {
73
+ if (semver.lt(installedVersion, latestVersion)) {
74
+ return true;
75
+ }
76
+ } catch (err) {
77
+ // Versão inválida, assumir atualização disponível
78
+ return true;
79
+ }
80
+ }
81
+
82
+ return false;
83
+ }
84
+
85
+ /**
86
+ * Verifica se uma plataforma específica tem cli-ai-skills instalado
87
+ * @param {string} platform - Nome da plataforma (copilot, claude, etc)
88
+ * @returns {Object} { installed: boolean, version: string }
89
+ */
90
+ function checkPlatformInstallation(platform) {
91
+ const homeDir = os.homedir();
92
+ const platformMap = {
93
+ copilot: '.copilot',
94
+ claude: '.claude',
95
+ codex: '.codex',
96
+ opencode: '.opencode',
97
+ gemini: '.gemini'
98
+ };
99
+
100
+ const dirName = platformMap[platform];
101
+ if (!dirName) {
102
+ return { installed: false, version: null };
103
+ }
104
+
105
+ const skillPath = path.join(homeDir, dirName, 'skills', 'skill-creator', 'SKILL.md');
106
+
107
+ if (!fs.existsSync(skillPath)) {
108
+ return { installed: false, version: null };
109
+ }
110
+
111
+ try {
112
+ const content = fs.readFileSync(skillPath, 'utf-8');
113
+ const yamlMatch = content.match(/^---\n([\s\S]*?)\n---/);
114
+
115
+ if (yamlMatch) {
116
+ const metadata = yaml.load(yamlMatch[1]);
117
+ return { installed: true, version: metadata.version || 'unknown' };
118
+ }
119
+ } catch (err) {
120
+ return { installed: true, version: 'unknown' };
121
+ }
122
+
123
+ return { installed: true, version: 'unknown' };
124
+ }
125
+
126
+ module.exports = {
127
+ checkInstalledVersion,
128
+ isUpdateAvailable,
129
+ checkPlatformInstallation
130
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cli-ai-skills",
3
- "version": "1.6.0",
3
+ "version": "1.7.2",
4
4
  "description": "Install AI skills for GitHub Copilot CLI, Claude Code, and OpenAI Codex",
5
5
  "main": "lib/index.js",
6
6
  "bin": {