@emmraan/ai-skills 1.0.2 → 1.2.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/.eslintrc.cjs +21 -0
- package/README.md +1 -12
- package/dist/commands/generate-local.d.ts.map +1 -1
- package/dist/commands/generate-local.js +18 -3
- package/dist/commands/generate-local.js.map +1 -1
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +18 -3
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/list.d.ts.map +1 -1
- package/dist/commands/list.js +57 -11
- package/dist/commands/list.js.map +1 -1
- package/dist/commands/remove.d.ts.map +1 -1
- package/dist/commands/remove.js +32 -2
- package/dist/commands/remove.js.map +1 -1
- package/dist/commands/search.d.ts +8 -0
- package/dist/commands/search.d.ts.map +1 -0
- package/dist/commands/search.js +69 -0
- package/dist/commands/search.js.map +1 -0
- package/dist/commands/update.d.ts.map +1 -1
- package/dist/commands/update.js +49 -8
- package/dist/commands/update.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +20 -5
- package/dist/index.js.map +1 -1
- package/dist/utils/logger.d.ts +7 -0
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +39 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/ui.d.ts +13 -0
- package/dist/utils/ui.d.ts.map +1 -1
- package/dist/utils/ui.js +101 -8
- package/dist/utils/ui.js.map +1 -1
- package/package.json +6 -4
- package/src/commands/generate-local.ts +18 -3
- package/src/commands/install.ts +21 -3
- package/src/commands/list.ts +80 -11
- package/src/commands/remove.ts +36 -2
- package/src/commands/search.ts +94 -0
- package/src/commands/update.ts +54 -10
- package/src/index.ts +19 -5
- package/src/utils/logger.ts +46 -3
- package/src/utils/ui.ts +192 -57
package/src/commands/remove.ts
CHANGED
|
@@ -5,9 +5,11 @@ import {
|
|
|
5
5
|
updateSkillInstallPathsInLockfile,
|
|
6
6
|
} from '../core/lockfile.js';
|
|
7
7
|
import { AGENT_PLATFORMS, type InstallLocation, type InstallOptions } from '../core/config.js';
|
|
8
|
-
import { error,
|
|
8
|
+
import { error, info, section, highlight } from '../utils/logger.js';
|
|
9
9
|
import { ui } from '../utils/ui.js';
|
|
10
10
|
import inquirer from 'inquirer';
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import pc from 'picocolors';
|
|
11
13
|
|
|
12
14
|
export interface CliRemoveFlags {
|
|
13
15
|
local?: boolean;
|
|
@@ -145,9 +147,15 @@ async function promptRemoveOptions(): Promise<InstallOptions> {
|
|
|
145
147
|
|
|
146
148
|
export async function remove(skillName: string, options: InstallOptions = {}): Promise<boolean> {
|
|
147
149
|
try {
|
|
150
|
+
section(`Removing ${chalk.bold.cyan(skillName)}`);
|
|
151
|
+
|
|
148
152
|
const entry = await getSkillEntry(skillName);
|
|
149
153
|
if (!entry) {
|
|
154
|
+
// eslint-disable-next-line no-console
|
|
155
|
+
console.log('');
|
|
150
156
|
info(`${skillName} is not installed`);
|
|
157
|
+
// eslint-disable-next-line no-console
|
|
158
|
+
console.log('');
|
|
151
159
|
return true;
|
|
152
160
|
}
|
|
153
161
|
|
|
@@ -162,7 +170,33 @@ export async function remove(skillName: string, options: InstallOptions = {}): P
|
|
|
162
170
|
}
|
|
163
171
|
|
|
164
172
|
ui.stopSpinnerSuccess(`Removed files for ${skillName}`);
|
|
165
|
-
|
|
173
|
+
|
|
174
|
+
// Print summary
|
|
175
|
+
// eslint-disable-next-line no-console
|
|
176
|
+
console.log('');
|
|
177
|
+
highlight(`✨ Successfully removed ${skillName}`);
|
|
178
|
+
// eslint-disable-next-line no-console
|
|
179
|
+
console.log('');
|
|
180
|
+
// eslint-disable-next-line no-console
|
|
181
|
+
console.log(pc.gray('Removed from locations:'));
|
|
182
|
+
removedPaths.forEach((path) => {
|
|
183
|
+
// eslint-disable-next-line no-console
|
|
184
|
+
console.log(` ${pc.red('✗')} ${pc.cyan(path)}`);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
if (remaining.length > 0) {
|
|
188
|
+
// eslint-disable-next-line no-console
|
|
189
|
+
console.log('');
|
|
190
|
+
// eslint-disable-next-line no-console
|
|
191
|
+
console.log(pc.gray('Still installed at:'));
|
|
192
|
+
remaining.forEach((path) => {
|
|
193
|
+
// eslint-disable-next-line no-console
|
|
194
|
+
console.log(` ${pc.green('✓')} ${pc.cyan(path)}`);
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
// eslint-disable-next-line no-console
|
|
198
|
+
console.log('');
|
|
199
|
+
|
|
166
200
|
return true;
|
|
167
201
|
} catch (err) {
|
|
168
202
|
ui.stopSpinner();
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { fetchRegistryIndex } from '../core/downloader.js';
|
|
2
|
+
import { log, warn } from '../utils/logger.js';
|
|
3
|
+
import { ui } from '../utils/ui.js';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export interface SearchOptions {
|
|
7
|
+
json?: boolean;
|
|
8
|
+
domain?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SearchResult {
|
|
12
|
+
name: string;
|
|
13
|
+
version: string;
|
|
14
|
+
domains: string[];
|
|
15
|
+
lastGenerated: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseSearchFlags(args: string[]): SearchOptions {
|
|
19
|
+
const options: SearchOptions = { json: false };
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < args.length; i++) {
|
|
22
|
+
if (args[i] === '--json') {
|
|
23
|
+
options.json = true;
|
|
24
|
+
} else if (args[i] === '--domain' && args[i + 1]) {
|
|
25
|
+
options.domain = args[i + 1];
|
|
26
|
+
i++;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return options;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function matchesQuery(skillName: string, domains: string[], query: string): boolean {
|
|
34
|
+
const lowerQuery = query.toLowerCase();
|
|
35
|
+
|
|
36
|
+
// Match on skill name
|
|
37
|
+
if (skillName.toLowerCase().includes(lowerQuery)) {
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Match on domains
|
|
42
|
+
if (domains.some((domain) => domain.toLowerCase().includes(lowerQuery))) {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export async function search(query: string, options: SearchOptions = {}): Promise<void> {
|
|
50
|
+
const flags = parseSearchFlags([]);
|
|
51
|
+
const mergedOptions = { ...flags, ...options };
|
|
52
|
+
|
|
53
|
+
const index = await fetchRegistryIndex();
|
|
54
|
+
const skillNames = Object.keys(index.skills);
|
|
55
|
+
|
|
56
|
+
// Filter by query
|
|
57
|
+
let results: SearchResult[] = skillNames
|
|
58
|
+
.filter((name) => matchesQuery(name, index.skills[name].domains, query))
|
|
59
|
+
.map((name) => ({
|
|
60
|
+
name,
|
|
61
|
+
version: index.skills[name].version,
|
|
62
|
+
domains: index.skills[name].domains,
|
|
63
|
+
lastGenerated: index.skills[name].lastGenerated,
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// Filter by domain if provided
|
|
67
|
+
if (mergedOptions.domain) {
|
|
68
|
+
results = results.filter((skill) => skill.domains.includes(mergedOptions.domain!));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Output
|
|
72
|
+
if (mergedOptions.json) {
|
|
73
|
+
log(JSON.stringify(results, null, 2));
|
|
74
|
+
} else {
|
|
75
|
+
if (results.length === 0) {
|
|
76
|
+
warn(
|
|
77
|
+
`No skills found matching "${query}"${mergedOptions.domain ? ` in domain "${mergedOptions.domain}"` : ''}`
|
|
78
|
+
);
|
|
79
|
+
} else {
|
|
80
|
+
const formattedResults = results.map((r) => {
|
|
81
|
+
const domainStr = r.domains.length > 0 ? chalk.gray(` [${r.domains.join(', ')}]`) : '';
|
|
82
|
+
return `${chalk.cyan(r.name)}${chalk.gray(` v${r.version}`)}${domainStr}`;
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
ui.printBox(formattedResults, '🔍 Search Results');
|
|
86
|
+
|
|
87
|
+
log('');
|
|
88
|
+
log(chalk.gray(`💡 Tip: Use ${chalk.cyan('ai-skills install <skill>')} to install a skill`));
|
|
89
|
+
log('');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export { parseSearchFlags };
|
package/src/commands/update.ts
CHANGED
|
@@ -2,7 +2,9 @@ import { fetchSkill, fetchRegistryIndex } from '../core/downloader.js';
|
|
|
2
2
|
import { installSkill } from '../core/installer.js';
|
|
3
3
|
import { addSkillToLockfile, getInstalledSkills, getSkillEntry } from '../core/lockfile.js';
|
|
4
4
|
import { sha256 } from '../utils/hash.js';
|
|
5
|
-
import { error, success, info, warn } from '../utils/logger.js';
|
|
5
|
+
import { error, success, info, warn, section, highlight, divider } from '../utils/logger.js';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
import pc from 'picocolors';
|
|
6
8
|
|
|
7
9
|
export async function update(options: { force?: boolean; skill?: string } = {}): Promise<boolean> {
|
|
8
10
|
try {
|
|
@@ -12,10 +14,16 @@ export async function update(options: { force?: boolean; skill?: string } = {}):
|
|
|
12
14
|
if (options.skill) {
|
|
13
15
|
// Update specific skill
|
|
14
16
|
const skillName = options.skill;
|
|
17
|
+
section(`Updating ${chalk.bold.cyan(skillName)}`);
|
|
18
|
+
|
|
15
19
|
const entry = await getSkillEntry(skillName);
|
|
16
20
|
|
|
17
21
|
if (!entry) {
|
|
22
|
+
// eslint-disable-next-line no-console
|
|
23
|
+
console.log('');
|
|
18
24
|
warn(`${skillName} is not installed`);
|
|
25
|
+
// eslint-disable-next-line no-console
|
|
26
|
+
console.log('');
|
|
19
27
|
return false;
|
|
20
28
|
}
|
|
21
29
|
|
|
@@ -23,7 +31,11 @@ export async function update(options: { force?: boolean; skill?: string } = {}):
|
|
|
23
31
|
const newHash = sha256(skillContent);
|
|
24
32
|
|
|
25
33
|
if (newHash === entry.hash && !options.force) {
|
|
26
|
-
|
|
34
|
+
// eslint-disable-next-line no-console
|
|
35
|
+
console.log('');
|
|
36
|
+
info(`${skillName} is already up-to-date (v${entry.version})`);
|
|
37
|
+
// eslint-disable-next-line no-console
|
|
38
|
+
console.log('');
|
|
27
39
|
return true;
|
|
28
40
|
}
|
|
29
41
|
|
|
@@ -31,36 +43,68 @@ export async function update(options: { force?: boolean; skill?: string } = {}):
|
|
|
31
43
|
const installPaths = await installSkill(skillName, skillContent);
|
|
32
44
|
const version = index.skills[skillName]?.version || 'unknown';
|
|
33
45
|
await addSkillToLockfile(skillName, version, newHash, installPaths);
|
|
34
|
-
|
|
46
|
+
|
|
47
|
+
// eslint-disable-next-line no-console
|
|
48
|
+
console.log('');
|
|
49
|
+
highlight(`✨ Successfully updated ${skillName} to v${version}`);
|
|
50
|
+
// eslint-disable-next-line no-console
|
|
51
|
+
console.log('');
|
|
52
|
+
success(`Updated at ${installPaths.length} location(s)`);
|
|
53
|
+
// eslint-disable-next-line no-console
|
|
54
|
+
console.log('');
|
|
35
55
|
return true;
|
|
36
56
|
}
|
|
37
57
|
|
|
38
58
|
// Update all installed skills
|
|
59
|
+
section('Updating all installed skills');
|
|
60
|
+
// eslint-disable-next-line no-console
|
|
61
|
+
console.log('');
|
|
62
|
+
|
|
39
63
|
let updateCount = 0;
|
|
64
|
+
let alreadyUpToDate = 0;
|
|
65
|
+
|
|
40
66
|
for (const entry of installed) {
|
|
41
67
|
try {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.log(pc.cyan(`► ${entry.name} (v${entry.version})`));
|
|
42
70
|
const skillContent = await fetchSkill(entry.name);
|
|
43
71
|
const newHash = sha256(skillContent);
|
|
44
72
|
|
|
45
73
|
if (newHash === entry.hash && !options.force) {
|
|
46
|
-
|
|
74
|
+
// eslint-disable-next-line no-console
|
|
75
|
+
console.log(` ${pc.gray('→')} Already up-to-date`);
|
|
76
|
+
alreadyUpToDate++;
|
|
47
77
|
continue;
|
|
48
78
|
}
|
|
49
79
|
|
|
50
|
-
info(`Updating ${entry.name}...`);
|
|
51
80
|
const installPaths = await installSkill(entry.name, skillContent);
|
|
52
81
|
const version = index.skills[entry.name]?.version || 'unknown';
|
|
53
82
|
await addSkillToLockfile(entry.name, version, newHash, installPaths);
|
|
54
|
-
|
|
83
|
+
// eslint-disable-next-line no-console
|
|
84
|
+
console.log(` ${pc.green('✓')} Updated to v${version}`);
|
|
55
85
|
updateCount++;
|
|
56
86
|
} catch (err) {
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
);
|
|
87
|
+
// eslint-disable-next-line no-console
|
|
88
|
+
console.log(` ${pc.red('✗')} Error: ${err instanceof Error ? err.message : String(err)}`);
|
|
60
89
|
}
|
|
61
90
|
}
|
|
62
91
|
|
|
63
|
-
|
|
92
|
+
// eslint-disable-next-line no-console
|
|
93
|
+
console.log('');
|
|
94
|
+
divider();
|
|
95
|
+
// eslint-disable-next-line no-console
|
|
96
|
+
console.log('');
|
|
97
|
+
// eslint-disable-next-line no-console
|
|
98
|
+
console.log(chalk.bold('Update Summary:'));
|
|
99
|
+
// eslint-disable-next-line no-console
|
|
100
|
+
console.log(` ${pc.green('✓')} Updated: ${chalk.bold(updateCount.toString())} skill(s)`);
|
|
101
|
+
// eslint-disable-next-line no-console
|
|
102
|
+
console.log(` ${pc.gray('→')} Up-to-date: ${chalk.bold(alreadyUpToDate.toString())} skill(s)`);
|
|
103
|
+
// eslint-disable-next-line no-console
|
|
104
|
+
console.log('');
|
|
105
|
+
// eslint-disable-next-line no-console
|
|
106
|
+
console.log('');
|
|
107
|
+
|
|
64
108
|
return true;
|
|
65
109
|
} catch (err) {
|
|
66
110
|
error(`Update failed: ${err instanceof Error ? err.message : String(err)}`);
|
package/src/index.ts
CHANGED
|
@@ -3,6 +3,7 @@ import { remove, parseRemoveFlags, resolveRemoveOptionsFromFlags } from './comma
|
|
|
3
3
|
import { list } from './commands/list.js';
|
|
4
4
|
import { update } from './commands/update.js';
|
|
5
5
|
import { generateLocal } from './commands/generate-local.js';
|
|
6
|
+
import { search, parseSearchFlags } from './commands/search.js';
|
|
6
7
|
import { error, info } from './utils/logger.js';
|
|
7
8
|
import { ui } from './utils/ui.js';
|
|
8
9
|
|
|
@@ -35,6 +36,11 @@ export async function main(): Promise<void> {
|
|
|
35
36
|
const jsonFlag = args.includes('--json');
|
|
36
37
|
await list(jsonFlag);
|
|
37
38
|
process.exit(0);
|
|
39
|
+
} else if (command === 'search' && args.length >= 2) {
|
|
40
|
+
const query = args[1];
|
|
41
|
+
const flags = parseSearchFlags(args.slice(2));
|
|
42
|
+
await search(query, flags);
|
|
43
|
+
process.exit(0);
|
|
38
44
|
} else if (command === 'update') {
|
|
39
45
|
const forceFlag = args.includes('--force');
|
|
40
46
|
let skillArg: string | undefined;
|
|
@@ -70,11 +76,12 @@ AI Skills CLI - Install framework-agnostic SKILLS.md files
|
|
|
70
76
|
Usage:
|
|
71
77
|
ai-skills <skill> [options] Install a skill
|
|
72
78
|
ai-skills install <skill> [options] Install a skill
|
|
73
|
-
ai-skills list [--json]
|
|
74
|
-
ai-skills
|
|
75
|
-
ai-skills
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
ai-skills list [--json] List installed and available skills
|
|
80
|
+
ai-skills search <query> [options] Search for skills
|
|
81
|
+
ai-skills remove <skill> [options] Remove an installed skill
|
|
82
|
+
ai-skills update [--force] Update all installed skills
|
|
83
|
+
[--skill <name>] Update a specific skill
|
|
84
|
+
ai-skills generate-local [skill] Run local backend generator
|
|
78
85
|
|
|
79
86
|
Install options:
|
|
80
87
|
--local Install in current project (.ai-skills/skills)
|
|
@@ -88,12 +95,19 @@ Remove options:
|
|
|
88
95
|
--all Remove from all platforms for selected scope
|
|
89
96
|
--platform <a,b,c> Remove from selected platforms for selected scope
|
|
90
97
|
|
|
98
|
+
Search options:
|
|
99
|
+
--domain <name> Filter results by domain
|
|
100
|
+
--json Output results as JSON
|
|
101
|
+
|
|
91
102
|
Examples:
|
|
92
103
|
ai-skills react
|
|
93
104
|
ai-skills react --local
|
|
94
105
|
ai-skills react --platform claude,gemini,opencode,vscode,codex,agents
|
|
95
106
|
ai-skills install typescript
|
|
96
107
|
ai-skills list
|
|
108
|
+
ai-skills search react
|
|
109
|
+
ai-skills search "testing" --domain frontend
|
|
110
|
+
ai-skills search python --json
|
|
97
111
|
ai-skills remove python
|
|
98
112
|
ai-skills update --force
|
|
99
113
|
ai-skills update --skill react
|
package/src/utils/logger.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/* eslint-disable no-console */
|
|
2
2
|
|
|
3
3
|
import chalk from 'chalk';
|
|
4
|
+
import pc from 'picocolors';
|
|
4
5
|
|
|
5
6
|
export function log(message: string) {
|
|
6
7
|
console.log(message);
|
|
@@ -11,13 +12,55 @@ export function info(message: string) {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function success(message: string) {
|
|
14
|
-
console.log(`${
|
|
15
|
+
console.log(`${pc.green('✓')} ${chalk.green(message)}`);
|
|
15
16
|
}
|
|
16
17
|
|
|
17
18
|
export function error(message: string) {
|
|
18
|
-
console.error(`${
|
|
19
|
+
console.error(`${pc.red('✗')} ${chalk.red(message)}`);
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export function warn(message: string) {
|
|
22
|
-
console.warn(`${
|
|
23
|
+
console.warn(`${pc.yellow('⚠')} ${chalk.yellow(message)}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function debug(message: string) {
|
|
27
|
+
if (process.env.DEBUG) {
|
|
28
|
+
console.log(`${chalk.gray('→')} ${chalk.gray(message)}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function divider() {
|
|
33
|
+
console.log(pc.gray('─'.repeat(60)));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function section(title: string) {
|
|
37
|
+
console.log('');
|
|
38
|
+
console.log(chalk.bold.cyan(`► ${title}`));
|
|
39
|
+
console.log(pc.gray('─'.repeat(title.length + 2)));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function highlight(message: string) {
|
|
43
|
+
console.log(chalk.bold.cyan(message));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function badge(label: string, content: string) {
|
|
47
|
+
console.log(`${chalk.bgCyan.black(` ${label} `)} ${pc.cyan(content)}`);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function card(title: string, content: string[]) {
|
|
51
|
+
console.log('');
|
|
52
|
+
console.log(chalk.bold.cyan(`┌─ ${title}`));
|
|
53
|
+
content.forEach((line) => {
|
|
54
|
+
console.log(chalk.cyan('│') + ` ${line}`);
|
|
55
|
+
});
|
|
56
|
+
console.log(chalk.cyan('└' + '─'.repeat(title.length + 3)));
|
|
57
|
+
console.log('');
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function progressBar(label: string, current: number, total: number) {
|
|
61
|
+
const percentage = Math.round((current / total) * 100);
|
|
62
|
+
const filled = Math.floor((percentage / 100) * 20);
|
|
63
|
+
const empty = 20 - filled;
|
|
64
|
+
const bar = `${pc.green('█'.repeat(filled))}${pc.gray('░'.repeat(empty))}`;
|
|
65
|
+
console.log(`${label}: [${bar}] ${pc.cyan(`${percentage}%`)}`);
|
|
23
66
|
}
|
package/src/utils/ui.ts
CHANGED
|
@@ -1,57 +1,192 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!process.stdout.isTTY) {
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
},
|
|
56
|
-
|
|
57
|
-
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
import ora, { type Ora } from 'ora';
|
|
4
|
+
import { log } from '../utils/logger.js';
|
|
5
|
+
|
|
6
|
+
let activeSpinner: Ora | null = null;
|
|
7
|
+
|
|
8
|
+
const BOX_CHARS = {
|
|
9
|
+
TOP_LEFT: '╭',
|
|
10
|
+
TOP_RIGHT: '╮',
|
|
11
|
+
BOTTOM_LEFT: '╰',
|
|
12
|
+
BOTTOM_RIGHT: '╯',
|
|
13
|
+
HORIZONTAL: '─',
|
|
14
|
+
VERTICAL: '│',
|
|
15
|
+
T_DOWN: '┬',
|
|
16
|
+
T_UP: '┴',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export const ui = {
|
|
20
|
+
bannerShown: false,
|
|
21
|
+
|
|
22
|
+
printBanner(): void {
|
|
23
|
+
if (this.bannerShown || !process.stdout.isTTY) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const title = 'AI Skills CLI';
|
|
28
|
+
const subtitle = 'Install framework-agnostic SKILLS.md files';
|
|
29
|
+
const width = Math.max(title.length, subtitle.length) + 6;
|
|
30
|
+
|
|
31
|
+
log('');
|
|
32
|
+
log(
|
|
33
|
+
pc.cyan(
|
|
34
|
+
`${BOX_CHARS.TOP_LEFT}${BOX_CHARS.HORIZONTAL.repeat(width - 2)}${BOX_CHARS.TOP_RIGHT}`
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
log(
|
|
38
|
+
pc.cyan(`${BOX_CHARS.VERTICAL}`) +
|
|
39
|
+
` ${chalk.bold.cyan(title)}${' '.repeat(width - 3 - title.length)}` +
|
|
40
|
+
pc.cyan(`${BOX_CHARS.VERTICAL}`)
|
|
41
|
+
);
|
|
42
|
+
log(
|
|
43
|
+
pc.cyan(`${BOX_CHARS.VERTICAL}`) +
|
|
44
|
+
` ${pc.gray(subtitle)}${' '.repeat(width - 3 - subtitle.length)}` +
|
|
45
|
+
pc.cyan(`${BOX_CHARS.VERTICAL}`)
|
|
46
|
+
);
|
|
47
|
+
log(
|
|
48
|
+
pc.cyan(
|
|
49
|
+
`${BOX_CHARS.BOTTOM_LEFT}${BOX_CHARS.HORIZONTAL.repeat(width - 2)}${BOX_CHARS.BOTTOM_RIGHT}`
|
|
50
|
+
)
|
|
51
|
+
);
|
|
52
|
+
log('');
|
|
53
|
+
|
|
54
|
+
this.bannerShown = true;
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
printSection(title: string): void {
|
|
58
|
+
log('');
|
|
59
|
+
log(chalk.bold.cyan(`${BOX_CHARS.VERTICAL} ${title}`));
|
|
60
|
+
log(chalk.cyan(`${BOX_CHARS.VERTICAL}${BOX_CHARS.HORIZONTAL.repeat(title.length + 2)}`));
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
printDivider(char: string = '─'): void {
|
|
64
|
+
log(chalk.gray(char.repeat(60)));
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
printBox(content: string[], title?: string): void {
|
|
68
|
+
// eslint-disable-next-line no-control-regex
|
|
69
|
+
const maxLength = Math.max(
|
|
70
|
+
// eslint-disable-next-line no-control-regex
|
|
71
|
+
...content.map((line) => line.replace(/\u001b\[[0-9;]*m/g, '').length)
|
|
72
|
+
);
|
|
73
|
+
const width = Math.min(maxLength + 4, 70);
|
|
74
|
+
|
|
75
|
+
if (title) {
|
|
76
|
+
log(
|
|
77
|
+
chalk.cyan(
|
|
78
|
+
`${BOX_CHARS.TOP_LEFT}${BOX_CHARS.HORIZONTAL.repeat(width - 2)}${BOX_CHARS.TOP_RIGHT}`
|
|
79
|
+
)
|
|
80
|
+
);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
content.forEach((line) => {
|
|
84
|
+
// eslint-disable-next-line no-control-regex
|
|
85
|
+
const padding = width - 4 - line.replace(/\u001b\[[0-9;]*m/g, '').length;
|
|
86
|
+
log(
|
|
87
|
+
chalk.cyan(`${BOX_CHARS.VERTICAL} `) +
|
|
88
|
+
line +
|
|
89
|
+
' '.repeat(Math.max(0, padding)) +
|
|
90
|
+
chalk.cyan(` ${BOX_CHARS.VERTICAL}`)
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
log(
|
|
95
|
+
chalk.cyan(
|
|
96
|
+
`${BOX_CHARS.BOTTOM_LEFT}${BOX_CHARS.HORIZONTAL.repeat(width - 2)}${BOX_CHARS.BOTTOM_RIGHT}`
|
|
97
|
+
)
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
startSpinner(text: string): void {
|
|
102
|
+
if (!process.stdout.isTTY) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (activeSpinner) {
|
|
107
|
+
activeSpinner.stop();
|
|
108
|
+
}
|
|
109
|
+
activeSpinner = ora({
|
|
110
|
+
text: chalk.cyan(text),
|
|
111
|
+
spinner: 'dots2',
|
|
112
|
+
}).start();
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
stopSpinnerSuccess(text?: string): void {
|
|
116
|
+
if (!activeSpinner) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
activeSpinner.succeed(chalk.green(text || 'Done'));
|
|
120
|
+
activeSpinner = null;
|
|
121
|
+
},
|
|
122
|
+
|
|
123
|
+
stopSpinnerFail(text?: string): void {
|
|
124
|
+
if (!activeSpinner) {
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
activeSpinner.fail(chalk.red(text || 'Failed'));
|
|
128
|
+
activeSpinner = null;
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
stopSpinner(): void {
|
|
132
|
+
if (!activeSpinner) {
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
activeSpinner.stop();
|
|
136
|
+
activeSpinner = null;
|
|
137
|
+
},
|
|
138
|
+
|
|
139
|
+
updateSpinner(text: string): void {
|
|
140
|
+
if (activeSpinner) {
|
|
141
|
+
activeSpinner.text = chalk.cyan(text);
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
|
|
145
|
+
printTable(rows: Array<string[]>, headers?: string[]): void {
|
|
146
|
+
if (headers) {
|
|
147
|
+
const headerRow = headers.map((h) => chalk.bold.cyan(h));
|
|
148
|
+
const separators = headers.map((h) => chalk.gray('─'.repeat(Math.max(h.length, 15))));
|
|
149
|
+
|
|
150
|
+
const maxWidths = headers.map((h, i) =>
|
|
151
|
+
Math.max(h.length, ...rows.map((r) => r[i]?.length || 0))
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
log(headerRow.map((h, i) => h.padEnd(maxWidths[i])).join(' ' + chalk.gray('│') + ' '));
|
|
155
|
+
log(separators.join('─' + chalk.gray('┼') + '─'));
|
|
156
|
+
|
|
157
|
+
rows.forEach((row) => {
|
|
158
|
+
log(row.map((cell, i) => cell.padEnd(maxWidths[i])).join(' ' + chalk.gray('│') + ' '));
|
|
159
|
+
});
|
|
160
|
+
} else {
|
|
161
|
+
rows.forEach((row) => {
|
|
162
|
+
log(' ' + row.join(' '));
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
printSummary(items: Array<{ label: string; value: string | number }>): void {
|
|
168
|
+
log('');
|
|
169
|
+
const maxLabelLength = Math.max(...items.map((i) => i.label.length));
|
|
170
|
+
items.forEach((item) => {
|
|
171
|
+
const paddedLabel = item.label.padEnd(maxLabelLength);
|
|
172
|
+
log(chalk.gray(` ${paddedLabel} `) + chalk.bold(String(item.value)));
|
|
173
|
+
});
|
|
174
|
+
log('');
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
printSuccess(message: string): void {
|
|
178
|
+
log(chalk.green(`✓ ${message}`));
|
|
179
|
+
},
|
|
180
|
+
|
|
181
|
+
printError(message: string): void {
|
|
182
|
+
log(chalk.red(`✗ ${message}`));
|
|
183
|
+
},
|
|
184
|
+
|
|
185
|
+
printWarning(message: string): void {
|
|
186
|
+
log(chalk.yellow(`⚠ ${message}`));
|
|
187
|
+
},
|
|
188
|
+
|
|
189
|
+
printInfo(message: string): void {
|
|
190
|
+
log(chalk.cyan(`ℹ ${message}`));
|
|
191
|
+
},
|
|
192
|
+
};
|