joyskills-cli 0.2.6 → 0.2.7
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 +81 -195
- package/package.json +21 -12
- package/src/commands/install.js +616 -211
- package/src/commands/list.js +54 -25
- package/src/commands/manage.js +102 -0
- package/src/commands/read.js +61 -118
- package/src/commands/remove.js +28 -20
- package/src/commands/status.js +31 -41
- package/src/commands/sync.js +155 -244
- package/src/commands/team.js +267 -47
- package/src/commands/upgrade.js +311 -0
- package/src/index.js +10 -12
- package/src/local.js +2 -9
- package/LICENSE +0 -21
- package/spec/cli-spec.md +0 -167
- package/spec/lockfile-spec.md +0 -108
- package/spec/registry-spec.md +0 -117
package/src/commands/list.js
CHANGED
|
@@ -1,8 +1,39 @@
|
|
|
1
|
-
import { LocalManager } from '../local.js';
|
|
2
1
|
import { RegistryManager } from '../registry.js';
|
|
3
2
|
import { LockfileManager } from '../lockfile.js';
|
|
4
3
|
import * as path from 'path';
|
|
5
4
|
import * as fs from 'fs';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
/** Scan all standard skill directories and return unique skill names */
|
|
9
|
+
function scanAllSkillDirs(projectRoot) {
|
|
10
|
+
const dirs = [
|
|
11
|
+
path.join(projectRoot, '.agent', 'skills'),
|
|
12
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
13
|
+
path.join(os.homedir(), '.agent', 'skills'),
|
|
14
|
+
path.join(os.homedir(), '.claude', 'skills'),
|
|
15
|
+
path.join(projectRoot, 'skills'), // legacy
|
|
16
|
+
];
|
|
17
|
+
const seen = new Set();
|
|
18
|
+
const skills = [];
|
|
19
|
+
for (const dir of dirs) {
|
|
20
|
+
if (!fs.existsSync(dir)) continue;
|
|
21
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
22
|
+
if (entry.isDirectory() && !seen.has(entry.name)) {
|
|
23
|
+
seen.add(entry.name);
|
|
24
|
+
const skillMd = path.join(dir, entry.name, 'SKILL.md');
|
|
25
|
+
let description = '', version = '';
|
|
26
|
+
if (fs.existsSync(skillMd)) {
|
|
27
|
+
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
28
|
+
description = (content.match(/description:\s*(.+)/) || [])[1]?.trim() || '';
|
|
29
|
+
version = (content.match(/version:\s*(.+)/) || [])[1]?.trim() || '';
|
|
30
|
+
}
|
|
31
|
+
skills.push({ name: entry.name, dir, description, version });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return skills;
|
|
36
|
+
}
|
|
6
37
|
|
|
7
38
|
export function listCommand(program) {
|
|
8
39
|
program
|
|
@@ -16,42 +47,40 @@ export function listCommand(program) {
|
|
|
16
47
|
.option('-r, --registry <name>', 'Show skills from specific registry')
|
|
17
48
|
.action(async (options) => {
|
|
18
49
|
const projectRoot = process.cwd();
|
|
19
|
-
const localManager = new LocalManager(projectRoot);
|
|
20
50
|
const lockfileManager = new LockfileManager(projectRoot);
|
|
21
51
|
|
|
22
|
-
console.log('Available Skills:');
|
|
23
|
-
console.log('=================');
|
|
52
|
+
console.log(chalk.blue('Available Skills:'));
|
|
53
|
+
console.log(chalk.gray('================='));
|
|
24
54
|
|
|
25
55
|
try {
|
|
26
|
-
// Load lockfile to know what's installed
|
|
27
56
|
await lockfileManager.load();
|
|
28
57
|
const installedSkills = lockfileManager.lockData.skills;
|
|
29
58
|
|
|
30
|
-
//
|
|
31
|
-
const localSkills =
|
|
59
|
+
// Scan all skill directories
|
|
60
|
+
const localSkills = scanAllSkillDirs(projectRoot);
|
|
32
61
|
|
|
33
62
|
if (!options.registry) {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const isInstalled = installedSkills[skill] ? '✅' : ' ';
|
|
49
|
-
const version = installedSkills[skill] ? `v${installedSkills[skill].version}` : '';
|
|
50
|
-
console.log(` ${isInstalled} ${skill} ${version}`);
|
|
63
|
+
if (localSkills.length > 0) {
|
|
64
|
+
console.log(chalk.blue('\nInstalled Skills:'));
|
|
65
|
+
let filtered = localSkills;
|
|
66
|
+
if (options.search) {
|
|
67
|
+
const q = options.search.toLowerCase();
|
|
68
|
+
filtered = filtered.filter(s => s.name.toLowerCase().includes(q) || s.description.toLowerCase().includes(q));
|
|
69
|
+
}
|
|
70
|
+
if (filtered.length > 0) {
|
|
71
|
+
filtered.forEach(skill => {
|
|
72
|
+
const lock = installedSkills[skill.name];
|
|
73
|
+
const ver = lock ? chalk.gray(`v${lock.version}`) : (skill.version ? chalk.gray(`v${skill.version}`) : '');
|
|
74
|
+
const src = lock ? chalk.gray(`[${lock.source}]`) : '';
|
|
75
|
+
console.log(` ✅ ${chalk.bold(skill.name)} ${ver} ${src}`);
|
|
76
|
+
if (skill.description) console.log(` ${chalk.gray(skill.description)}`);
|
|
51
77
|
});
|
|
52
78
|
} else {
|
|
53
|
-
console.log(' No matching
|
|
79
|
+
console.log(' No matching skills found.');
|
|
54
80
|
}
|
|
81
|
+
} else {
|
|
82
|
+
console.log(chalk.yellow('\n No skills installed.'));
|
|
83
|
+
console.log(chalk.gray(' Run: joySkills install <skill>'));
|
|
55
84
|
}
|
|
56
85
|
}
|
|
57
86
|
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import { checkbox, confirm } from '@inquirer/prompts';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { LockfileManager } from '../lockfile.js';
|
|
7
|
+
|
|
8
|
+
export function manageCommand(program) {
|
|
9
|
+
program
|
|
10
|
+
.command('manage')
|
|
11
|
+
.description('Interactively manage (remove) installed skills')
|
|
12
|
+
.option('-g, --global', 'Manage global ~/.claude/skills')
|
|
13
|
+
.option('--universal', 'Manage .agent/skills/')
|
|
14
|
+
.option('-y, --yes', 'Skip confirmation')
|
|
15
|
+
.action(async (options) => {
|
|
16
|
+
try {
|
|
17
|
+
const projectRoot = process.cwd();
|
|
18
|
+
|
|
19
|
+
// Determine which dirs to scan
|
|
20
|
+
const dirsToScan = options.global
|
|
21
|
+
? [path.join(os.homedir(), '.claude', 'skills')]
|
|
22
|
+
: options.universal
|
|
23
|
+
? [path.join(projectRoot, '.agent', 'skills')]
|
|
24
|
+
: [
|
|
25
|
+
path.join(projectRoot, '.agent', 'skills'),
|
|
26
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
// Collect skills from all relevant dirs
|
|
30
|
+
const seen = new Set();
|
|
31
|
+
const skills = [];
|
|
32
|
+
for (const skillsDir of dirsToScan) {
|
|
33
|
+
if (!fs.existsSync(skillsDir)) continue;
|
|
34
|
+
for (const entry of fs.readdirSync(skillsDir, { withFileTypes: true })) {
|
|
35
|
+
if (!entry.isDirectory() || seen.has(entry.name)) continue;
|
|
36
|
+
seen.add(entry.name);
|
|
37
|
+
const skillPath = path.join(skillsDir, entry.name);
|
|
38
|
+
const skillMd = path.join(skillPath, 'SKILL.md');
|
|
39
|
+
let description = '';
|
|
40
|
+
if (fs.existsSync(skillMd)) {
|
|
41
|
+
const content = fs.readFileSync(skillMd, 'utf-8');
|
|
42
|
+
description = (content.match(/description:\s*(.+)/) || [])[1]?.trim() || '';
|
|
43
|
+
}
|
|
44
|
+
skills.push({ name: entry.name, path: skillPath, description });
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (skills.length === 0) {
|
|
49
|
+
console.log(chalk.yellow('⚠️ No skills installed'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
let toRemove;
|
|
54
|
+
if (options.yes) {
|
|
55
|
+
// -y: remove all without interaction
|
|
56
|
+
toRemove = skills;
|
|
57
|
+
console.log(chalk.blue(`\n📦 Removing all ${skills.length} skill(s) (--yes mode):`));
|
|
58
|
+
for (const s of toRemove) console.log(` - ${s.name}`);
|
|
59
|
+
} else {
|
|
60
|
+
console.log(chalk.blue(`\n📦 Installed skills:`));
|
|
61
|
+
|
|
62
|
+
const choices = skills.map(s => ({
|
|
63
|
+
name: s.description ? `${s.name} — ${chalk.gray(s.description)}` : s.name,
|
|
64
|
+
value: s,
|
|
65
|
+
checked: false
|
|
66
|
+
}));
|
|
67
|
+
|
|
68
|
+
toRemove = await checkbox({
|
|
69
|
+
message: 'Select skills to remove (space to select, enter to confirm):',
|
|
70
|
+
choices
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
if (toRemove.length === 0) {
|
|
74
|
+
console.log(chalk.gray(' Nothing selected'));
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
console.log(chalk.yellow(`\nAbout to remove ${toRemove.length} skill(s):`));
|
|
79
|
+
for (const s of toRemove) console.log(` - ${s.name}`);
|
|
80
|
+
const ok = await confirm({ message: 'Continue?', default: false });
|
|
81
|
+
if (!ok) { console.log(chalk.gray(' Cancelled')); return; }
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Remove
|
|
85
|
+
const lockfileManager = new LockfileManager(projectRoot);
|
|
86
|
+
await lockfileManager.load();
|
|
87
|
+
|
|
88
|
+
for (const skill of toRemove) {
|
|
89
|
+
fs.rmSync(skill.path, { recursive: true, force: true });
|
|
90
|
+
lockfileManager.removeSkill(skill.name);
|
|
91
|
+
console.log(chalk.green(` ✓ Removed ${skill.name}`));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
await lockfileManager.save();
|
|
95
|
+
console.log(chalk.green(`\n✅ Removed ${toRemove.length} skill(s)`));
|
|
96
|
+
|
|
97
|
+
} catch (error) {
|
|
98
|
+
console.error(chalk.red(`❌ Manage failed: ${error.message}`));
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
package/src/commands/read.js
CHANGED
|
@@ -1,127 +1,70 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
const content = fs.readFileSync(skillMdPath, 'utf-8');
|
|
52
|
-
|
|
53
|
-
// 输出到 stdout(供 AI 编辑器解析)
|
|
54
|
-
console.log(content);
|
|
55
|
-
|
|
56
|
-
} catch (error) {
|
|
57
|
-
console.error('❌ 读取失败:', error.message);
|
|
58
|
-
process.exit(1);
|
|
59
|
-
}
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as os from 'os';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
|
|
6
|
+
export function readCommand(program) {
|
|
7
|
+
program
|
|
8
|
+
.command('read <skills>')
|
|
9
|
+
.description('Load and print skill content (comma-separated names supported)')
|
|
10
|
+
.option('-g, --global', 'Read from global ~/.claude/skills')
|
|
11
|
+
.option('--universal', 'Read from .agent/skills/')
|
|
12
|
+
.action(async (skillsInput, options) => {
|
|
13
|
+
try {
|
|
14
|
+
const projectRoot = process.cwd();
|
|
15
|
+
|
|
16
|
+
// Build search paths in priority order (openskill compatible)
|
|
17
|
+
const searchPaths = [
|
|
18
|
+
path.join(projectRoot, '.agent', 'skills'),
|
|
19
|
+
path.join(os.homedir(), '.agent', 'skills'),
|
|
20
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
21
|
+
path.join(os.homedir(), '.claude', 'skills'),
|
|
22
|
+
// legacy
|
|
23
|
+
path.join(projectRoot, 'skills'),
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// Support comma-separated skill names
|
|
27
|
+
const skillNames = skillsInput.split(',').map(s => s.trim()).filter(Boolean);
|
|
28
|
+
|
|
29
|
+
for (const skillName of skillNames) {
|
|
30
|
+
const content = findSkillContent(skillName, searchPaths);
|
|
31
|
+
|
|
32
|
+
if (!content) {
|
|
33
|
+
console.error(chalk.red(`❌ Skill not found: ${skillName}`));
|
|
34
|
+
console.error(chalk.gray(` Searched in: ${searchPaths.filter(p => fs.existsSync(p)).join(', ')}`));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Print skill content (for agent consumption)
|
|
39
|
+
if (skillNames.length > 1) {
|
|
40
|
+
console.log(chalk.blue(`\n── ${skillName} ──────────────────────────────────`));
|
|
41
|
+
}
|
|
42
|
+
console.log(content);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error(chalk.red(`❌ Read failed: ${error.message}`));
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
});
|
|
60
50
|
}
|
|
61
51
|
|
|
62
|
-
|
|
63
|
-
* 查找 skill 路径(按优先级)
|
|
64
|
-
*
|
|
65
|
-
* 优先级与 OpenSkills sync 一致:
|
|
66
|
-
* 1. 项目 .agent > 全局 .agent
|
|
67
|
-
* 2. 项目 .claude > 全局 .claude
|
|
68
|
-
*/
|
|
69
|
-
function findSkillPath(skillName) {
|
|
70
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
71
|
-
const cwd = process.cwd();
|
|
72
|
-
|
|
73
|
-
const searchPaths = [
|
|
74
|
-
// 1. 项目级 .agent/skills(joySkills 默认 / OpenSkills --universal)
|
|
75
|
-
path.join(cwd, '.agent', 'skills', skillName),
|
|
76
|
-
|
|
77
|
-
// 2. 全局 ~/.agent/skills(joySkills --global)
|
|
78
|
-
path.join(homeDir, '.agent', 'skills', skillName),
|
|
79
|
-
|
|
80
|
-
// 3. 项目级 .claude/skills(OpenSkills --project)
|
|
81
|
-
path.join(cwd, '.claude', 'skills', skillName),
|
|
82
|
-
|
|
83
|
-
// 4. 全局 ~/.claude/skills(OpenSkills --global)
|
|
84
|
-
path.join(homeDir, '.claude', 'skills', skillName),
|
|
85
|
-
];
|
|
86
|
-
|
|
52
|
+
function findSkillContent(skillName, searchPaths) {
|
|
87
53
|
for (const searchPath of searchPaths) {
|
|
88
|
-
if (fs.existsSync(searchPath))
|
|
89
|
-
return searchPath;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
return null;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 列出所有可读取的 skills(辅助命令)
|
|
98
|
-
*/
|
|
99
|
-
export function listReadableSkills() {
|
|
100
|
-
const homeDir = process.env.HOME || process.env.USERPROFILE;
|
|
101
|
-
const cwd = process.cwd();
|
|
54
|
+
if (!fs.existsSync(searchPath)) continue;
|
|
102
55
|
|
|
103
|
-
|
|
104
|
-
path.join(
|
|
105
|
-
path.join(homeDir, '.agent', 'skills'),
|
|
106
|
-
path.join(cwd, '.claude', 'skills'),
|
|
107
|
-
path.join(homeDir, '.claude', 'skills'),
|
|
108
|
-
];
|
|
56
|
+
const skillDir = path.join(searchPath, skillName);
|
|
57
|
+
const skillMd = path.join(skillDir, 'SKILL.md');
|
|
109
58
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
if (!fs.existsSync(dir)) continue;
|
|
114
|
-
|
|
115
|
-
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
116
|
-
for (const entry of entries) {
|
|
117
|
-
if (!entry.isDirectory()) continue;
|
|
59
|
+
if (fs.existsSync(skillMd)) {
|
|
60
|
+
return fs.readFileSync(skillMd, 'utf-8');
|
|
61
|
+
}
|
|
118
62
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
63
|
+
// Also check if skillName is a direct SKILL.md path
|
|
64
|
+
const directMd = path.join(searchPath, `${skillName}.md`);
|
|
65
|
+
if (fs.existsSync(directMd)) {
|
|
66
|
+
return fs.readFileSync(directMd, 'utf-8');
|
|
123
67
|
}
|
|
124
68
|
}
|
|
125
|
-
|
|
126
|
-
return Array.from(skills).sort();
|
|
69
|
+
return null;
|
|
127
70
|
}
|
package/src/commands/remove.js
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
|
-
import { Command } from 'commander';
|
|
2
|
-
import { LocalManager } from '../local.js';
|
|
3
1
|
import { LockfileManager } from '../lockfile.js';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import * as fs from 'fs';
|
|
4
|
+
import * as os from 'os';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
|
|
7
|
+
function findSkillOnDisk(skillName, projectRoot) {
|
|
8
|
+
const dirs = [
|
|
9
|
+
path.join(projectRoot, '.agent', 'skills'),
|
|
10
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
11
|
+
path.join(os.homedir(), '.agent', 'skills'),
|
|
12
|
+
path.join(os.homedir(), '.claude', 'skills'),
|
|
13
|
+
path.join(projectRoot, 'skills'),
|
|
14
|
+
];
|
|
15
|
+
for (const d of dirs) {
|
|
16
|
+
const p = path.join(d, skillName);
|
|
17
|
+
if (fs.existsSync(p)) return p;
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
4
21
|
|
|
5
22
|
export function removeCommand(program) {
|
|
6
23
|
program
|
|
@@ -8,35 +25,26 @@ export function removeCommand(program) {
|
|
|
8
25
|
.description('Remove a skill')
|
|
9
26
|
.action(async (skillName) => {
|
|
10
27
|
const projectRoot = process.cwd();
|
|
11
|
-
const localManager = new LocalManager(projectRoot);
|
|
12
28
|
const lockfileManager = new LockfileManager(projectRoot);
|
|
13
29
|
|
|
14
|
-
console.log(`Removing skill: ${skillName}`);
|
|
30
|
+
console.log(chalk.blue(`Removing skill: ${skillName}`));
|
|
15
31
|
|
|
16
32
|
try {
|
|
17
|
-
// Load existing lockfile if exists
|
|
18
33
|
await lockfileManager.load();
|
|
19
34
|
|
|
20
|
-
|
|
21
|
-
if (!
|
|
22
|
-
console.log(`Skill ${skillName} does not exist
|
|
35
|
+
const skillPath = findSkillOnDisk(skillName, projectRoot);
|
|
36
|
+
if (!skillPath) {
|
|
37
|
+
console.log(chalk.yellow(`Skill ${skillName} does not exist.`));
|
|
23
38
|
return;
|
|
24
39
|
}
|
|
25
40
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (removed) {
|
|
30
|
-
// Update lockfile
|
|
31
|
-
lockfileManager.removeSkill(skillName);
|
|
32
|
-
await lockfileManager.save();
|
|
41
|
+
fs.rmSync(skillPath, { recursive: true, force: true });
|
|
42
|
+
lockfileManager.removeSkill(skillName);
|
|
43
|
+
await lockfileManager.save();
|
|
33
44
|
|
|
34
|
-
|
|
35
|
-
} else {
|
|
36
|
-
console.log(`❌ Failed to remove ${skillName}`);
|
|
37
|
-
}
|
|
45
|
+
console.log(chalk.green(`✅ Successfully removed ${skillName}`));
|
|
38
46
|
} catch (error) {
|
|
39
|
-
console.error(`❌ Failed to remove ${skillName}
|
|
47
|
+
console.error(chalk.red(`❌ Failed to remove ${skillName}:`), error.message);
|
|
40
48
|
}
|
|
41
49
|
});
|
|
42
50
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
1
|
import { LockfileManager } from '../lockfile.js';
|
|
2
2
|
import { RegistryManager } from '../registry.js';
|
|
3
|
-
import { LocalManager } from '../local.js';
|
|
4
3
|
import * as path from 'path';
|
|
5
4
|
import * as fs from 'fs';
|
|
5
|
+
import * as os from 'os';
|
|
6
|
+
import chalk from 'chalk';
|
|
7
|
+
|
|
8
|
+
function skillExistsOnDisk(skillName, projectRoot) {
|
|
9
|
+
const dirs = [
|
|
10
|
+
path.join(projectRoot, '.agent', 'skills'),
|
|
11
|
+
path.join(projectRoot, '.claude', 'skills'),
|
|
12
|
+
path.join(os.homedir(), '.agent', 'skills'),
|
|
13
|
+
path.join(os.homedir(), '.claude', 'skills'),
|
|
14
|
+
path.join(projectRoot, 'skills'),
|
|
15
|
+
];
|
|
16
|
+
return dirs.some(d => fs.existsSync(path.join(d, skillName)));
|
|
17
|
+
}
|
|
6
18
|
|
|
7
19
|
export function statusCommand(program) {
|
|
8
20
|
program
|
|
@@ -11,10 +23,9 @@ export function statusCommand(program) {
|
|
|
11
23
|
.action(async () => {
|
|
12
24
|
const projectRoot = process.cwd();
|
|
13
25
|
const lockfileManager = new LockfileManager(projectRoot);
|
|
14
|
-
const localManager = new LocalManager(projectRoot);
|
|
15
26
|
|
|
16
|
-
console.log('Skill Status Report:');
|
|
17
|
-
console.log('====================');
|
|
27
|
+
console.log(chalk.blue('Skill Status Report:'));
|
|
28
|
+
console.log(chalk.gray('===================='));
|
|
18
29
|
|
|
19
30
|
try {
|
|
20
31
|
await lockfileManager.load();
|
|
@@ -23,7 +34,7 @@ export function statusCommand(program) {
|
|
|
23
34
|
const skillIds = Object.keys(installedSkills);
|
|
24
35
|
|
|
25
36
|
if (skillIds.length === 0) {
|
|
26
|
-
console.log('No skills installed.');
|
|
37
|
+
console.log(chalk.yellow('No skills installed.'));
|
|
27
38
|
return;
|
|
28
39
|
}
|
|
29
40
|
|
|
@@ -40,17 +51,17 @@ export function statusCommand(program) {
|
|
|
40
51
|
}
|
|
41
52
|
}
|
|
42
53
|
|
|
43
|
-
console.log(`\nInstalled Skills (${skillIds.length} total):`);
|
|
44
|
-
console.log('
|
|
54
|
+
console.log(chalk.blue(`\nInstalled Skills (${skillIds.length} total):`));
|
|
55
|
+
console.log(chalk.gray('─'.repeat(50)));
|
|
45
56
|
|
|
46
57
|
for (const skillId of skillIds) {
|
|
47
58
|
const skillEntry = installedSkills[skillId];
|
|
48
59
|
|
|
49
|
-
let status = '✅ Installed';
|
|
60
|
+
let status = chalk.green('✅ Installed');
|
|
50
61
|
let details = '';
|
|
51
62
|
|
|
52
|
-
// Check if skill exists
|
|
53
|
-
const hasLocal =
|
|
63
|
+
// Check if skill exists on disk (check all standard dirs)
|
|
64
|
+
const hasLocal = skillExistsOnDisk(skillId, projectRoot);
|
|
54
65
|
|
|
55
66
|
// Check registry status
|
|
56
67
|
if (registryManager) {
|
|
@@ -59,56 +70,35 @@ export function statusCommand(program) {
|
|
|
59
70
|
const version = registryManager.getVersion(skillId, skillEntry.version);
|
|
60
71
|
if (version) {
|
|
61
72
|
if (version.state === 'deprecated') {
|
|
62
|
-
status = '⚠️ Deprecated';
|
|
73
|
+
status = chalk.yellow('⚠️ Deprecated');
|
|
63
74
|
details = ' (upgrade recommended)';
|
|
64
75
|
} else if (version.state === 'draft' || version.state === 'pending_review') {
|
|
65
|
-
status = '❌ Not Approved';
|
|
76
|
+
status = chalk.red('❌ Not Approved');
|
|
66
77
|
details = ` (${version.state})`;
|
|
67
78
|
}
|
|
68
|
-
|
|
69
|
-
// Check for newer versions
|
|
70
79
|
const latest = registryManager.getLatestVersion(skillId);
|
|
71
80
|
if (latest && latest.version !== skillEntry.version) {
|
|
72
|
-
details += `
|
|
81
|
+
details += chalk.gray(` → v${latest.version} available`);
|
|
73
82
|
}
|
|
74
83
|
}
|
|
75
|
-
} else {
|
|
76
|
-
status = '❓ Not in Registry';
|
|
77
|
-
details = ' (local/external skill)';
|
|
78
84
|
}
|
|
79
85
|
}
|
|
80
86
|
|
|
81
87
|
if (!hasLocal) {
|
|
82
|
-
status = '
|
|
83
|
-
details = ' (files
|
|
88
|
+
status = chalk.red('❌ Missing Files');
|
|
89
|
+
details = ' (files deleted, run joySkills install to restore)';
|
|
84
90
|
}
|
|
85
91
|
|
|
86
|
-
|
|
92
|
+
const src = skillEntry.source ? chalk.gray(` [${skillEntry.source}]`) : '';
|
|
93
|
+
console.log(` ${status} ${chalk.bold(skillId)} v${skillEntry.version}${src}${details}`);
|
|
87
94
|
}
|
|
88
95
|
|
|
89
|
-
// Show registry info
|
|
90
96
|
if (registryManager) {
|
|
91
|
-
console.log('\nRegistry
|
|
92
|
-
console.log(
|
|
93
|
-
console.log(`
|
|
94
|
-
console.log(`Total skills in registry: ${registryManager.getAllSkills().length}`);
|
|
95
|
-
} else {
|
|
96
|
-
console.log('\nRegistry Status:');
|
|
97
|
-
console.log('---------------');
|
|
98
|
-
console.log('No registry found. Using local skills only.');
|
|
97
|
+
console.log(chalk.blue('\nRegistry:'));
|
|
98
|
+
console.log(` Loaded: ${chalk.gray(registryPath)}`);
|
|
99
|
+
console.log(` Skills: ${chalk.blue(registryManager.getAllSkills().length)}`);
|
|
99
100
|
}
|
|
100
101
|
|
|
101
|
-
// Show local skills
|
|
102
|
-
const localSkills = localManager.listSkills();
|
|
103
|
-
const localOnlySkills = localSkills.filter(skillId => !installedSkills[skillId]);
|
|
104
|
-
|
|
105
|
-
if (localOnlySkills.length > 0) {
|
|
106
|
-
console.log(`\nLocal Skills (${localOnlySkills.length} not installed):`);
|
|
107
|
-
console.log('-------------------------');
|
|
108
|
-
for (const skillId of localOnlySkills) {
|
|
109
|
-
console.log(`${skillId} (local only)`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
102
|
} catch (error) {
|
|
113
103
|
console.error('Error checking status:', error);
|
|
114
104
|
}
|