@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.
- package/CHANGELOG.md +0 -0
- package/jest.config.ts +26 -0
- package/package.json +1 -1
- package/project.json +74 -0
- package/src/app.tsx +56 -0
- package/src/atoms/deprecatedSkills.ts +11 -0
- package/src/atoms/environmentCheck.ts +51 -0
- package/src/atoms/installedSkills.ts +36 -0
- package/src/atoms/wizard.ts +6 -0
- package/src/cli/audit.ts +21 -0
- package/src/cli/cache.ts +28 -0
- package/src/cli/install.ts +93 -0
- package/src/cli/list.ts +41 -0
- package/src/cli/remove.ts +70 -0
- package/src/cli/update.ts +107 -0
- package/src/components/AnimatedTransition.tsx +42 -0
- package/src/components/AuditLogViewer.tsx +85 -0
- package/src/components/CategoryHeader.tsx +39 -0
- package/src/components/ConfirmPrompt.tsx +34 -0
- package/src/components/FooterBar.tsx +36 -0
- package/src/components/Header.tsx +97 -0
- package/src/components/InstallResults.tsx +110 -0
- package/src/components/KeyboardShortcutsOverlay.tsx +112 -0
- package/src/components/MultiSelectPrompt.tsx +219 -0
- package/src/components/SearchInput.tsx +36 -0
- package/src/components/SelectPrompt.tsx +108 -0
- package/src/components/SkillCard.tsx +74 -0
- package/src/components/SkillDetailPanel.tsx +233 -0
- package/src/components/StatusBadge.tsx +45 -0
- package/src/components/__tests__/AnimatedTransition.pbt.test.tsx +51 -0
- package/src/components/__tests__/CategoryHeader.pbt.test.tsx +107 -0
- package/src/components/__tests__/CategoryHeader.test.tsx +105 -0
- package/src/components/__tests__/KeyboardShortcutsOverlay.pbt.test.tsx +155 -0
- package/src/components/__tests__/KeyboardShortcutsOverlay.test.tsx +136 -0
- package/src/components/__tests__/SkillDetailPanel.test.tsx +273 -0
- package/src/components/index.ts +12 -0
- package/src/hooks/__tests__/useConfig.test.ts +242 -0
- package/src/hooks/index.ts +9 -0
- package/src/hooks/useAgents.ts +28 -0
- package/src/hooks/useConfig.ts +114 -0
- package/src/hooks/useFilter.ts +31 -0
- package/src/hooks/useInstaller.ts +39 -0
- package/src/hooks/useKeyboardNav.ts +39 -0
- package/src/hooks/useKonamiCode.ts +48 -0
- package/src/hooks/useRemover.ts +59 -0
- package/src/hooks/useSkillContent.ts +67 -0
- package/src/hooks/useSkills.ts +38 -0
- package/src/hooks/useWizardStep.ts +19 -0
- package/src/index.ts +129 -0
- package/src/services/__tests__/audit-log.spec.ts +220 -0
- package/src/services/__tests__/badge-format.test.ts +102 -0
- package/src/services/__tests__/category-colors.test.ts +253 -0
- package/src/services/__tests__/config.test.ts +184 -0
- package/src/services/__tests__/installer.security.spec.ts +151 -0
- package/src/services/__tests__/lockfile.security.spec.ts +132 -0
- package/src/services/__tests__/markdown-parser.spec.ts +185 -0
- package/src/services/__tests__/terminal-dimensions.pbt.test.ts +246 -0
- package/src/services/__tests__/terminal-dimensions.test.ts +109 -0
- package/src/services/__tests__/update-cache.pbt.test.ts +214 -0
- package/src/services/__tests__/update-cache.test.ts +215 -0
- package/src/services/agents.ts +42 -0
- package/src/services/audio-player.ts +55 -0
- package/src/services/audit-log.ts +69 -0
- package/src/services/badge-format.ts +4 -0
- package/src/services/categories.ts +176 -0
- package/src/services/category-colors.ts +19 -0
- package/src/services/config.ts +84 -0
- package/src/services/github-contributors.ts +56 -0
- package/src/services/global-path.ts +20 -0
- package/src/services/index.ts +21 -0
- package/src/services/installer.ts +371 -0
- package/src/services/lockfile.ts +177 -0
- package/src/services/markdown-parser.ts +108 -0
- package/src/services/package-info.ts +19 -0
- package/src/services/project-root.ts +18 -0
- package/src/services/registry.ts +382 -0
- package/src/services/skills-provider.ts +169 -0
- package/src/services/terminal-dimensions.ts +18 -0
- package/src/services/update-cache.ts +65 -0
- package/src/services/update-check.ts +26 -0
- package/src/theme/colors.ts +24 -0
- package/src/theme/index.ts +2 -0
- package/src/theme/symbols.ts +22 -0
- package/src/types.ts +38 -0
- package/src/utils/constants.ts +49 -0
- package/src/utils/paths.ts +52 -0
- package/src/views/ActionSelector.tsx +45 -0
- package/src/views/AgentSelector.tsx +105 -0
- package/src/views/CreditsView.tsx +332 -0
- package/src/views/InstallConfig.tsx +162 -0
- package/src/views/InstallWizard.tsx +181 -0
- package/src/views/ListView.tsx +41 -0
- package/src/views/RemoveWizard.tsx +237 -0
- package/src/views/SkillBrowser.tsx +504 -0
- package/src/views/UpdateView.tsx +272 -0
- package/src/views/arcade/ArcadeMenu.tsx +89 -0
- package/src/views/arcade/VibeInvaders.tsx +339 -0
- package/src/views/arcade/index.ts +2 -0
- package/src/views/index.ts +11 -0
- package/tsconfig.json +19 -0
- package/tsconfig.spec.json +25 -0
- package/LICENSE +0 -26
- package/README.md +0 -257
- package/index.js +0 -12
- package/index.js.map +0 -7
- /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
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 ?? {})
|
package/src/cli/audit.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/cache.ts
ADDED
|
@@ -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
|
+
}
|
package/src/cli/list.ts
ADDED
|
@@ -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
|
+
}
|