@gustavobrunodev/ai-tools 1.0.1 → 1.1.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 (106) hide show
  1. package/CHANGELOG.md +0 -0
  2. package/jest.config.ts +26 -0
  3. package/package.json +1 -1
  4. package/project.json +74 -0
  5. package/src/app.tsx +56 -0
  6. package/src/atoms/deprecatedSkills.ts +11 -0
  7. package/src/atoms/environmentCheck.ts +51 -0
  8. package/src/atoms/installedSkills.ts +36 -0
  9. package/src/atoms/wizard.ts +6 -0
  10. package/src/cli/audit.ts +21 -0
  11. package/src/cli/cache.ts +28 -0
  12. package/src/cli/install.ts +93 -0
  13. package/src/cli/list.ts +41 -0
  14. package/src/cli/remove.ts +70 -0
  15. package/src/cli/update.ts +107 -0
  16. package/src/components/AnimatedTransition.tsx +42 -0
  17. package/src/components/AuditLogViewer.tsx +85 -0
  18. package/src/components/CategoryHeader.tsx +39 -0
  19. package/src/components/ConfirmPrompt.tsx +34 -0
  20. package/src/components/FooterBar.tsx +36 -0
  21. package/src/components/Header.tsx +97 -0
  22. package/src/components/InstallResults.tsx +110 -0
  23. package/src/components/KeyboardShortcutsOverlay.tsx +112 -0
  24. package/src/components/MultiSelectPrompt.tsx +219 -0
  25. package/src/components/SearchInput.tsx +36 -0
  26. package/src/components/SelectPrompt.tsx +108 -0
  27. package/src/components/SkillCard.tsx +74 -0
  28. package/src/components/SkillDetailPanel.tsx +233 -0
  29. package/src/components/StatusBadge.tsx +45 -0
  30. package/src/components/__tests__/AnimatedTransition.pbt.test.tsx +51 -0
  31. package/src/components/__tests__/CategoryHeader.pbt.test.tsx +107 -0
  32. package/src/components/__tests__/CategoryHeader.test.tsx +105 -0
  33. package/src/components/__tests__/KeyboardShortcutsOverlay.pbt.test.tsx +155 -0
  34. package/src/components/__tests__/KeyboardShortcutsOverlay.test.tsx +136 -0
  35. package/src/components/__tests__/SkillDetailPanel.test.tsx +273 -0
  36. package/src/components/index.ts +12 -0
  37. package/src/hooks/__tests__/useConfig.test.ts +242 -0
  38. package/src/hooks/index.ts +9 -0
  39. package/src/hooks/useAgents.ts +28 -0
  40. package/src/hooks/useConfig.ts +114 -0
  41. package/src/hooks/useFilter.ts +31 -0
  42. package/src/hooks/useInstaller.ts +39 -0
  43. package/src/hooks/useKeyboardNav.ts +39 -0
  44. package/src/hooks/useKonamiCode.ts +48 -0
  45. package/src/hooks/useRemover.ts +59 -0
  46. package/src/hooks/useSkillContent.ts +67 -0
  47. package/src/hooks/useSkills.ts +38 -0
  48. package/src/hooks/useWizardStep.ts +19 -0
  49. package/src/index.ts +129 -0
  50. package/src/services/__tests__/audit-log.spec.ts +220 -0
  51. package/src/services/__tests__/badge-format.test.ts +102 -0
  52. package/src/services/__tests__/category-colors.test.ts +253 -0
  53. package/src/services/__tests__/config.test.ts +184 -0
  54. package/src/services/__tests__/installer.security.spec.ts +151 -0
  55. package/src/services/__tests__/lockfile.security.spec.ts +132 -0
  56. package/src/services/__tests__/markdown-parser.spec.ts +185 -0
  57. package/src/services/__tests__/terminal-dimensions.pbt.test.ts +246 -0
  58. package/src/services/__tests__/terminal-dimensions.test.ts +109 -0
  59. package/src/services/__tests__/update-cache.pbt.test.ts +214 -0
  60. package/src/services/__tests__/update-cache.test.ts +215 -0
  61. package/src/services/agents.ts +42 -0
  62. package/src/services/audio-player.ts +55 -0
  63. package/src/services/audit-log.ts +69 -0
  64. package/src/services/badge-format.ts +4 -0
  65. package/src/services/categories.ts +176 -0
  66. package/src/services/category-colors.ts +19 -0
  67. package/src/services/config.ts +84 -0
  68. package/src/services/github-contributors.ts +56 -0
  69. package/src/services/global-path.ts +20 -0
  70. package/src/services/index.ts +21 -0
  71. package/src/services/installer.ts +371 -0
  72. package/src/services/lockfile.ts +177 -0
  73. package/src/services/markdown-parser.ts +108 -0
  74. package/src/services/package-info.ts +19 -0
  75. package/src/services/project-root.ts +18 -0
  76. package/src/services/registry.ts +382 -0
  77. package/src/services/skills-provider.ts +169 -0
  78. package/src/services/terminal-dimensions.ts +18 -0
  79. package/src/services/update-cache.ts +65 -0
  80. package/src/services/update-check.ts +26 -0
  81. package/src/theme/colors.ts +24 -0
  82. package/src/theme/index.ts +2 -0
  83. package/src/theme/symbols.ts +22 -0
  84. package/src/types.ts +38 -0
  85. package/src/utils/constants.ts +49 -0
  86. package/src/utils/paths.ts +52 -0
  87. package/src/views/ActionSelector.tsx +45 -0
  88. package/src/views/AgentSelector.tsx +105 -0
  89. package/src/views/CreditsView.tsx +332 -0
  90. package/src/views/InstallConfig.tsx +162 -0
  91. package/src/views/InstallWizard.tsx +181 -0
  92. package/src/views/ListView.tsx +41 -0
  93. package/src/views/RemoveWizard.tsx +237 -0
  94. package/src/views/SkillBrowser.tsx +504 -0
  95. package/src/views/UpdateView.tsx +272 -0
  96. package/src/views/arcade/ArcadeMenu.tsx +89 -0
  97. package/src/views/arcade/VibeInvaders.tsx +339 -0
  98. package/src/views/arcade/index.ts +2 -0
  99. package/src/views/index.ts +11 -0
  100. package/tsconfig.json +19 -0
  101. package/tsconfig.spec.json +25 -0
  102. package/LICENSE +0 -26
  103. package/README.md +0 -257
  104. package/index.js +0 -12
  105. package/index.js.map +0 -7
  106. /package/{assets → src/assets}/chiptune.mp3 +0 -0
package/CHANGELOG.md ADDED
File without changes
package/jest.config.ts ADDED
@@ -0,0 +1,26 @@
1
+ import type { Config } from 'jest'
2
+
3
+ const config: Config = {
4
+ displayName: 'cli',
5
+ preset: '../../jest.preset.js',
6
+ testEnvironment: 'node',
7
+ testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[jt]s?(x)'],
8
+ transform: {
9
+ '^.+\\.[tj]sx?$': [
10
+ 'ts-jest',
11
+ {
12
+ useESM: true,
13
+ tsconfig: '<rootDir>/tsconfig.spec.json',
14
+ diagnostics: {
15
+ ignoreCodes: [151002],
16
+ warnOnly: true
17
+ }
18
+ },
19
+ ],
20
+ },
21
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'],
22
+ coverageDirectory: '../../coverage/packages/cli',
23
+ moduleNameMapper: { '^@gustavobrunodev/core$': '<rootDir>/../../libs/core/src/index.ts' },
24
+ }
25
+
26
+ export default config
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gustavobrunodev/ai-tools",
3
- "version": "1.0.1",
3
+ "version": "1.1.0",
4
4
  "description": "CLI to install and manage AI tools (skills, MCPs, agents) for AI coding agents",
5
5
  "author": "gustavobrunodev",
6
6
  "license": "MIT",
package/project.json ADDED
@@ -0,0 +1,74 @@
1
+ {
2
+ "name": "@gustavobrunodev/ai-tools",
3
+ "$schema": "../../node_modules/nx/schemas/project-schema.json",
4
+ "sourceRoot": "packages/cli/src",
5
+ "projectType": "library",
6
+ "tags": ["scope:cli"],
7
+ "targets": {
8
+ "build": {
9
+ "executor": "@nx/esbuild:esbuild",
10
+ "outputs": ["{options.outputPath}"],
11
+ "defaultConfiguration": "production",
12
+ "inputs": ["production", "^production"],
13
+ "options": {
14
+ "platform": "node",
15
+ "outputPath": "packages/cli/dist",
16
+ "format": ["esm"],
17
+ "bundle": true,
18
+ "main": "packages/cli/src/index.ts",
19
+ "tsConfig": "packages/cli/tsconfig.json",
20
+ "assets": [
21
+ {
22
+ "glob": "README.md",
23
+ "input": ".",
24
+ "output": "."
25
+ },
26
+ {
27
+ "glob": "LICENSE",
28
+ "input": ".",
29
+ "output": "."
30
+ },
31
+ {
32
+ "glob": "package.json",
33
+ "input": ".",
34
+ "output": "."
35
+ },
36
+ {
37
+ "glob": "**/*.mp3",
38
+ "input": "packages/cli/src/assets",
39
+ "output": "assets"
40
+ }
41
+ ],
42
+ "esbuildOptions": {
43
+ "sourcemap": true,
44
+ "outExtension": {
45
+ ".js": ".js"
46
+ },
47
+ "banner": {
48
+ "js": "#!/usr/bin/env node"
49
+ },
50
+ "platform": "node",
51
+ "jsx": "automatic",
52
+ "loader": {
53
+ ".tsx": "tsx"
54
+ }
55
+ }
56
+ },
57
+ "configurations": {
58
+ "development": {
59
+ "minify": false
60
+ },
61
+ "production": {
62
+ "minify": true
63
+ }
64
+ }
65
+ },
66
+ "post-build": {
67
+ "executor": "nx:run-commands",
68
+ "options": {
69
+ "command": "chmod +x packages/cli/dist/index.js"
70
+ },
71
+ "dependsOn": ["build"]
72
+ }
73
+ }
74
+ }
package/src/app.tsx ADDED
@@ -0,0 +1,56 @@
1
+ import { Box, useApp } from 'ink'
2
+ import { useEffect, useState } from 'react'
3
+
4
+ import { useKonamiCode } from './hooks'
5
+ import { ArcadeMenu, CreditsView, InstallWizard, ListView, RemoveWizard, UpdateView } from './views'
6
+
7
+ interface AppProps {
8
+ command?: string
9
+ args?: string[]
10
+ }
11
+
12
+ export const App = ({ command = 'install' }: AppProps) => {
13
+ const { exit } = useApp()
14
+ const [arcade, setArcade] = useState(command === 'arcade')
15
+ const { activated, reset } = useKonamiCode()
16
+
17
+ useEffect(() => {
18
+ if (activated && !arcade) {
19
+ setArcade(true)
20
+ reset()
21
+ }
22
+ }, [activated, arcade, reset])
23
+
24
+ if (command === 'credits') {
25
+ return (
26
+ <Box flexDirection="column" padding={1}>
27
+ <CreditsView onExit={exit} />
28
+ </Box>
29
+ )
30
+ }
31
+
32
+ if (arcade) {
33
+ return (
34
+ <Box flexDirection="column" padding={1}>
35
+ <ArcadeMenu
36
+ onExit={() => {
37
+ if (command === 'arcade') {
38
+ exit()
39
+ } else {
40
+ setArcade(false)
41
+ }
42
+ }}
43
+ />
44
+ </Box>
45
+ )
46
+ }
47
+
48
+ return (
49
+ <Box flexDirection="column" padding={1}>
50
+ {command === 'list' && <ListView onExit={exit} />}
51
+ {command === 'remove' && <RemoveWizard onExit={exit} />}
52
+ {command === 'update' && <UpdateView onExit={exit} />}
53
+ {(command === 'install' || !command) && <InstallWizard onExit={exit} />}
54
+ </Box>
55
+ )
56
+ }
@@ -0,0 +1,11 @@
1
+ import { atom } from 'jotai'
2
+ import { unwrap } from 'jotai/utils'
3
+
4
+ import { getDeprecatedMap } from '../services/registry'
5
+ import type { DeprecatedEntry } from '../types'
6
+
7
+ const deprecatedSkillsAsyncAtom = atom(async (): Promise<Map<string, DeprecatedEntry>> => {
8
+ return getDeprecatedMap()
9
+ })
10
+
11
+ export const deprecatedSkillsAtom = unwrap(deprecatedSkillsAsyncAtom, (prev) => prev ?? new Map())
@@ -0,0 +1,51 @@
1
+ import { atom } from 'jotai'
2
+ import { unwrap } from 'jotai/utils'
3
+
4
+ import { isGloballyInstalled } from '../services/global-path'
5
+ import { getCachedUpdate, setCachedUpdate } from '../services/update-cache'
6
+ import { checkForUpdates, getCurrentVersion } from '../services/update-check'
7
+ import { UPDATE_CHECK_TIMEOUT_MS } from '../utils/constants'
8
+
9
+ export interface EnvironmentCheckState {
10
+ updateAvailable: string | null
11
+ currentVersion: string
12
+ isGlobal: boolean
13
+ isLoading?: boolean
14
+ }
15
+
16
+ async function resolveUpdateAvailable(currentVersion: string): Promise<string | null> {
17
+ const cached = await getCachedUpdate()
18
+ const cachedUpdate = cached && cached.latestVersion !== currentVersion ? cached.latestVersion : null
19
+
20
+ try {
21
+ const update = await Promise.race([
22
+ checkForUpdates(currentVersion),
23
+ new Promise<string | null>((_, reject) =>
24
+ setTimeout(() => reject(new Error('timeout')), UPDATE_CHECK_TIMEOUT_MS),
25
+ ),
26
+ ])
27
+
28
+ setCachedUpdate(update ?? currentVersion).catch(() => {})
29
+ return update
30
+ } catch {
31
+ return cachedUpdate
32
+ }
33
+ }
34
+
35
+ const runCheck = async (): Promise<EnvironmentCheckState> => {
36
+ const currentVersion = getCurrentVersion()
37
+
38
+ const [updateAvailable, isGlobal] = await Promise.all([
39
+ resolveUpdateAvailable(currentVersion).catch(() => null),
40
+ Promise.resolve(isGloballyInstalled()).catch(() => false),
41
+ ])
42
+
43
+ return { updateAvailable, currentVersion, isGlobal: isGlobal as boolean, isLoading: false }
44
+ }
45
+
46
+ const environmentCheckAsyncAtom = atom<Promise<EnvironmentCheckState>>(runCheck())
47
+
48
+ export const environmentCheckAtom = unwrap(
49
+ environmentCheckAsyncAtom,
50
+ (prev) => prev ?? { updateAvailable: null, currentVersion: getCurrentVersion(), isGlobal: false, isLoading: true },
51
+ )
@@ -0,0 +1,36 @@
1
+ import { atom } from 'jotai'
2
+ import { unwrap } from 'jotai/utils'
3
+
4
+ import { detectInstalledAgents } from '../services/agents'
5
+ import { listInstalledSkills } from '../services/installer'
6
+ import type { AgentType } from '../types'
7
+
8
+ export type InstallationMap = Record<string, AgentType[]>
9
+
10
+ const fetchInstalledSkills = async (): Promise<InstallationMap> => {
11
+ const agents = detectInstalledAgents()
12
+ const status: InstallationMap = {}
13
+
14
+ for (const agent of agents) {
15
+ const [local, global] = await Promise.all([
16
+ listInstalledSkills(agent, false).catch(() => []),
17
+ listInstalledSkills(agent, true).catch(() => []),
18
+ ])
19
+
20
+ for (const skill of new Set([...local, ...global])) {
21
+ if (!status[skill]) status[skill] = []
22
+ if (!status[skill].includes(agent)) status[skill].push(agent)
23
+ }
24
+ }
25
+
26
+ return status
27
+ }
28
+
29
+ export const installedSkillsRefreshAtom = atom(0)
30
+
31
+ const installedSkillsAsyncAtom = atom(async (get) => {
32
+ get(installedSkillsRefreshAtom)
33
+ return fetchInstalledSkills()
34
+ })
35
+
36
+ export const installedSkillsAtom = unwrap(installedSkillsAsyncAtom, (prev) => prev ?? {})
@@ -0,0 +1,6 @@
1
+ import { atom } from 'jotai'
2
+
3
+ import type { AgentType, SkillInfo } from '../types'
4
+
5
+ export const selectedAgentsAtom = atom<AgentType[]>([])
6
+ export const selectedSkillsAtom = atom<SkillInfo[]>([])
@@ -0,0 +1,21 @@
1
+ import { render } from 'ink'
2
+ import React from 'react'
3
+
4
+ import { AuditLogViewer } from '../components/AuditLogViewer'
5
+ import { getAuditLogPath, readAuditLog } from '../services/audit-log'
6
+
7
+ interface AuditOptions {
8
+ limit?: string
9
+ path?: boolean
10
+ }
11
+
12
+ export async function runCliAudit(options: AuditOptions) {
13
+ if (options.path) {
14
+ console.log(getAuditLogPath())
15
+ return
16
+ }
17
+
18
+ const limit = options.limit ? parseInt(options.limit, 10) : 10
19
+ const entries = await readAuditLog(limit)
20
+ render(React.createElement(AuditLogViewer, { entries, limit }))
21
+ }
@@ -0,0 +1,28 @@
1
+ import chalk from 'chalk'
2
+
3
+ import { clearCache, clearRegistryCache, getCacheDir } from '../services/registry'
4
+
5
+ interface CacheCliOptions {
6
+ clear?: boolean
7
+ clearRegistry?: boolean
8
+ path?: boolean
9
+ }
10
+
11
+ export function runCliCache(options: CacheCliOptions): void {
12
+ if (options.clear) {
13
+ clearCache()
14
+ console.log(chalk.green('✅ Cache cleared'))
15
+ } else if (options.clearRegistry) {
16
+ clearRegistryCache()
17
+ console.log(chalk.green('✅ Registry cache cleared'))
18
+ } else if (options.path) {
19
+ console.log(getCacheDir())
20
+ } else {
21
+ console.log(chalk.bold('Cache management:'))
22
+ console.log(` ${chalk.blue('--clear')} Clear all cached skills and registry`)
23
+ console.log(` ${chalk.blue('--clear-registry')} Clear only the registry cache`)
24
+ console.log(` ${chalk.blue('--path')} Show cache directory path`)
25
+ console.log()
26
+ console.log(chalk.dim(`Cache location: ${getCacheDir()}`))
27
+ }
28
+ }
@@ -0,0 +1,93 @@
1
+ import chalk from 'chalk'
2
+
3
+ import { installSkills } from '../services/installer'
4
+ import { ensureSkillDownloaded, forceDownloadSkill, getRemoteSkills } from '../services/registry'
5
+ import type { AgentType, InstallOptions, SkillInfo } from '../types'
6
+
7
+ interface InstallCliOptions {
8
+ skill?: string[]
9
+ agent?: string[]
10
+ global?: boolean
11
+ symlink?: boolean
12
+ force?: boolean
13
+ }
14
+
15
+ async function downloadSkills(skillNames: string[], forceDownload: boolean): Promise<SkillInfo[]> {
16
+ const allSkills = await getRemoteSkills()
17
+ const selectedSkills: SkillInfo[] = []
18
+
19
+ for (const skillName of skillNames) {
20
+ const skill = allSkills.find((s) => s.name === skillName)
21
+ if (!skill) {
22
+ console.error(chalk.red(`❌ Skill "${skillName}" not found`))
23
+ continue
24
+ }
25
+
26
+ const path = forceDownload ? await forceDownloadSkill(skillName) : await ensureSkillDownloaded(skillName)
27
+ if (path) {
28
+ selectedSkills.push({ ...skill, path })
29
+ } else {
30
+ console.error(chalk.red(`❌ Failed to download skill "${skillName}"`))
31
+ }
32
+ }
33
+
34
+ return selectedSkills
35
+ }
36
+
37
+ function showInstallResults(results: Awaited<ReturnType<typeof installSkills>>): void {
38
+ const successful = results.filter((r) => r.success)
39
+ const failed = results.filter((r) => !r.success)
40
+
41
+ if (successful.length > 0) {
42
+ console.log(chalk.green(`\n✅ Successfully installed ${successful.length} skill(s):`))
43
+ successful.forEach((r) => {
44
+ console.log(chalk.dim(` • ${r.skill} → ${r.agent} (${r.method})`))
45
+ })
46
+ }
47
+
48
+ if (failed.length > 0) {
49
+ console.log(chalk.red(`\n❌ Failed to install ${failed.length} skill(s):`))
50
+ failed.forEach((r) => {
51
+ console.log(chalk.dim(` • ${r.skill} → ${r.agent}: ${r.error}`))
52
+ })
53
+ }
54
+ }
55
+
56
+ export async function runCliInstall(options: InstallCliOptions): Promise<void> {
57
+ if (!options.skill || options.skill.length === 0) {
58
+ console.error(chalk.red('❌ --skill is required in CLI mode'))
59
+ console.error(
60
+ chalk.dim('Usage: ai-tools install --skill <name1> [name2...] [--agent <agents...>] [--global] [--symlink]'),
61
+ )
62
+ process.exit(1)
63
+ }
64
+
65
+ const skillNames = Array.isArray(options.skill) ? options.skill : [options.skill]
66
+
67
+ console.log(chalk.blue(`⏳ Loading ${skillNames.length} skill(s) from catalog...`))
68
+ const skills = await downloadSkills(skillNames, options.force || false)
69
+
70
+ if (skills.length === 0) {
71
+ console.error(chalk.red('❌ No skills were successfully downloaded'))
72
+ process.exit(1)
73
+ }
74
+
75
+ const agents = (options.agent || ['github-copilot', 'claude-code']) as AgentType[]
76
+ const method = options.symlink ? 'symlink' : 'copy'
77
+
78
+ console.log(chalk.blue(`⏳ Installing ${skills.length} skill(s) to ${agents.length} agent(s)...`))
79
+
80
+ const installOptions: InstallOptions = {
81
+ agents,
82
+ skills: skills.map((s) => s.name),
83
+ method,
84
+ global: options.global || false,
85
+ }
86
+
87
+ const results = await installSkills(skills, installOptions)
88
+ showInstallResults(results)
89
+
90
+ if (results.some((r) => !r.success)) {
91
+ process.exit(1)
92
+ }
93
+ }
@@ -0,0 +1,41 @@
1
+ import chalk from 'chalk'
2
+
3
+ import { getRemoteCategories, getRemoteSkills } from '../services/registry'
4
+
5
+ export async function runCliList(): Promise<void> {
6
+ console.log(chalk.blue('⏳ Fetching skills registry...'))
7
+
8
+ const [skills, categories] = await Promise.all([getRemoteSkills(), getRemoteCategories()])
9
+
10
+ if (skills.length === 0) {
11
+ console.log(chalk.yellow('No skills found in the registry.'))
12
+ return
13
+ }
14
+
15
+ const categoryMap = new Map(categories.map((c) => [c.id, c.name]))
16
+
17
+ // Group skills by category
18
+ const byCategory = new Map<string, typeof skills>()
19
+ for (const skill of skills) {
20
+ const cat = skill.category ?? 'uncategorized'
21
+ if (!byCategory.has(cat)) byCategory.set(cat, [])
22
+ byCategory.get(cat)!.push(skill)
23
+ }
24
+
25
+ console.log(chalk.bold(`\n📦 Available Skills (${skills.length} total)\n`))
26
+
27
+ for (const [catId, catSkills] of byCategory) {
28
+ const catName = categoryMap.get(catId) ?? catId
29
+ console.log(chalk.cyan.bold(` ${catName}`))
30
+
31
+ for (const skill of catSkills) {
32
+ const name = chalk.white(skill.name.padEnd(36))
33
+ const desc = chalk.dim(skill.description ?? '')
34
+ console.log(` ${name} ${desc}`)
35
+ }
36
+
37
+ console.log()
38
+ }
39
+
40
+ console.log(chalk.dim(`Run ${chalk.white('ai-tools install -s <skill-name>')} to install a skill.`))
41
+ }
@@ -0,0 +1,70 @@
1
+ import chalk from 'chalk'
2
+
3
+ import { removeSkill } from '../services/installer'
4
+ import type { AgentType } from '../types'
5
+
6
+ interface RemoveCliOptions {
7
+ skill?: string[]
8
+ agent?: string[]
9
+ global?: boolean
10
+ force?: boolean
11
+ }
12
+
13
+ export async function runCliRemove(options: RemoveCliOptions): Promise<void> {
14
+ if (!options.skill || options.skill.length === 0) {
15
+ console.error(chalk.red('❌ --skill is required in CLI mode'))
16
+ console.error(
17
+ chalk.dim('Usage: ai-tools remove --skill <name1> [name2...] [--agent <agents...>] [--global] [--force]'),
18
+ )
19
+ process.exit(1)
20
+ }
21
+
22
+ const skillNames = Array.isArray(options.skill) ? options.skill : [options.skill]
23
+ const agents = (options.agent || ['github-copilot', 'claude-code']) as AgentType[]
24
+
25
+ if (options.force) {
26
+ console.log(chalk.yellow('⚠️ Force mode enabled - bypassing lockfile check'))
27
+ }
28
+
29
+ console.log(chalk.blue(`⏳ Removing ${skillNames.length} skill(s) from ${agents.length} agent(s)...`))
30
+
31
+ let totalSuccess = 0
32
+ let totalFailed = 0
33
+ let hasLockfileError = false
34
+
35
+ for (const skillName of skillNames) {
36
+ const results = await removeSkill(skillName, agents, {
37
+ global: options.global,
38
+ force: options.force,
39
+ })
40
+
41
+ const successful = results.filter((r) => r.success)
42
+ const failed = results.filter((r) => !r.success)
43
+
44
+ if (successful.length > 0) {
45
+ console.log(chalk.green(`✅ ${skillName}: Removed from ${successful.length} agent(s)`))
46
+ successful.forEach((r) => console.log(chalk.dim(` • ${r.agent}`)))
47
+ totalSuccess += successful.length
48
+ }
49
+
50
+ if (failed.length > 0) {
51
+ console.log(chalk.red(`❌ ${skillName}: Failed to remove from ${failed.length} agent(s)`))
52
+ failed.forEach((r) => console.log(chalk.dim(` • ${r.agent}: ${r.error}`)))
53
+ totalFailed += failed.length
54
+
55
+ if (failed.some((r) => r.error?.includes('lockfile'))) {
56
+ hasLockfileError = true
57
+ }
58
+ }
59
+ }
60
+
61
+ console.log(chalk.dim(`\n${totalSuccess} succeeded, ${totalFailed} failed`))
62
+
63
+ if (hasLockfileError && !options.force) {
64
+ console.log(chalk.yellow('\n💡 Tip: Use --force to bypass lockfile check'))
65
+ }
66
+
67
+ if (totalFailed > 0) {
68
+ process.exit(1)
69
+ }
70
+ }
@@ -0,0 +1,107 @@
1
+ import chalk from 'chalk'
2
+ import {
3
+ fetchRegistry,
4
+ forceDownloadSkill,
5
+ getDeprecatedMap,
6
+ getRemoteSkills,
7
+ getUpdatableSkills,
8
+ needsUpdate,
9
+ } from '../services/registry'
10
+
11
+ interface UpdateCliOptions {
12
+ skill?: string
13
+ }
14
+
15
+ export async function runCliUpdate(options: UpdateCliOptions): Promise<void> {
16
+ console.log(chalk.blue('⏳ Fetching latest registry...'))
17
+ await fetchRegistry(true)
18
+
19
+ if (options.skill) {
20
+ const outdated = await needsUpdate(options.skill)
21
+ if (!outdated) {
22
+ console.log(chalk.green(`✅ ${options.skill} is already up to date`))
23
+ return
24
+ }
25
+
26
+ console.log(chalk.blue(`⏳ Updating ${options.skill}...`))
27
+ const path = await forceDownloadSkill(options.skill)
28
+
29
+ if (path) {
30
+ console.log(chalk.green(`✅ Updated ${options.skill}`))
31
+ } else {
32
+ console.error(chalk.red(`❌ Failed to update ${options.skill}`))
33
+ process.exit(1)
34
+ }
35
+ } else {
36
+ const { readSkillLock } = await import('../services/lockfile')
37
+ const lock = await readSkillLock()
38
+ const installedNames = Object.keys(lock.skills)
39
+
40
+ if (installedNames.length === 0) {
41
+ console.log(chalk.yellow('No installed skills found. Run ai-tools install first.'))
42
+ return
43
+ }
44
+
45
+ const { toUpdate, upToDate } = await getUpdatableSkills(installedNames)
46
+
47
+ if (toUpdate.length === 0) {
48
+ console.log(chalk.green(`✅ All ${upToDate.length} installed skills are up to date`))
49
+ return
50
+ }
51
+
52
+ console.log(chalk.blue(`⏳ Updating ${toUpdate.length} of ${installedNames.length} skills...`))
53
+ let updated = 0
54
+ let failed = 0
55
+
56
+ for (const name of toUpdate) {
57
+ const path = await forceDownloadSkill(name)
58
+ if (path) {
59
+ updated++
60
+ } else {
61
+ failed++
62
+ console.error(chalk.red(` ❌ Failed to update ${name}`))
63
+ }
64
+ }
65
+
66
+ console.log(
67
+ chalk.green(
68
+ `✅ ${updated} updated, ${upToDate.length} already up to date${failed > 0 ? chalk.red(`, ${failed} failed`) : ''}`,
69
+ ),
70
+ )
71
+
72
+ // Check for deprecated/orphaned skills
73
+ const deprecatedMap = await getDeprecatedMap()
74
+ const remoteSkills = await getRemoteSkills()
75
+ const registryNames = new Set(remoteSkills.map((s) => s.name))
76
+
77
+ const deprecated = installedNames.filter((name) => deprecatedMap.has(name) || !registryNames.has(name))
78
+
79
+ if (deprecated.length > 0) {
80
+ console.log('')
81
+ console.log(chalk.yellow(`⚠ ${deprecated.length} deprecated skill${deprecated.length > 1 ? 's' : ''} detected:`))
82
+
83
+ const renderers: Record<
84
+ 'withEntry' | 'noEntry',
85
+ (name: string, entry?: { message: string; alternatives?: string[] }) => void
86
+ > = {
87
+ withEntry: (name, entry) => {
88
+ console.log(chalk.yellow(` › ${name} — ${entry!.message}`))
89
+ if (entry!.alternatives?.length) {
90
+ console.log(chalk.dim(` Try: ai-tools install --skill ${entry!.alternatives.join(', ')}`))
91
+ }
92
+ },
93
+ noEntry: (name) => {
94
+ console.log(chalk.yellow(` › ${name} — no longer available in the registry`))
95
+ },
96
+ }
97
+
98
+ deprecated.forEach((name) => {
99
+ const entry = deprecatedMap.get(name)
100
+ const rendererKey = entry ? 'withEntry' : 'noEntry'
101
+ renderers[rendererKey](name, entry)
102
+ })
103
+
104
+ console.log(chalk.dim(` Run: ai-tools remove --skill <name> to clean up`))
105
+ }
106
+ }
107
+ }
@@ -0,0 +1,42 @@
1
+ import React, { useEffect, useState } from 'react'
2
+
3
+ export interface AnimatedTransitionProps {
4
+ visible: boolean
5
+ duration?: number
6
+ children: React.ReactNode
7
+ }
8
+
9
+ export const AnimatedTransition: React.FC<AnimatedTransitionProps> = ({ visible, duration = 250, children }) => {
10
+ const [opacity, setOpacity] = useState(visible ? 1 : 0)
11
+
12
+ useEffect(() => {
13
+ if ((visible && opacity === 1) || (!visible && opacity === 0)) return
14
+
15
+ const startTime = Date.now()
16
+ const startOpacity = opacity
17
+ const targetOpacity = visible ? 1 : 0
18
+ const frameDuration = 16
19
+
20
+ const interval = setInterval(() => {
21
+ const now = Date.now()
22
+ const elapsed = now - startTime
23
+ const progress = Math.min(elapsed / duration, 1)
24
+ const currentOpacity = startOpacity + (visible ? progress : -progress) * Math.abs(targetOpacity - startOpacity)
25
+ const clampedOpacity = Math.max(0, Math.min(1, currentOpacity))
26
+
27
+ setOpacity(clampedOpacity)
28
+
29
+ if (progress >= 1) {
30
+ clearInterval(interval)
31
+ setOpacity(targetOpacity)
32
+ }
33
+ }, frameDuration)
34
+
35
+ return () => {
36
+ clearInterval(interval)
37
+ }
38
+ }, [visible, duration])
39
+
40
+ if (!visible && opacity <= 0.05) return null
41
+ return <>{children}</>
42
+ }