joyskills-cli 0.2.9 → 0.3.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/package.json +10 -3
- package/src/agents.js +198 -0
- package/src/commands/check.js +69 -0
- package/src/commands/install.js +106 -22
- package/src/commands/list.js +66 -124
- package/src/commands/remove.js +12 -2
- package/src/commands/sync.js +10 -4
- package/src/commands/update.js +114 -0
- package/src/commands/upgrade.js +7 -4
- package/src/index.js +4 -0
- package/src/installer.js +190 -0
- package/src/skill-loader.js +207 -0
- package/src/version-checker.js +129 -0
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "joyskills-cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.3.0",
|
|
4
|
+
"description": "JoySkills CLI v2.0 - Multi-agent skill management with JoyCode native support",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"joySkills": "src/index.js"
|
|
@@ -9,9 +9,16 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "echo 'No build needed for JavaScript version'",
|
|
11
11
|
"start": "node src/index.js",
|
|
12
|
-
"test": "jest",
|
|
12
|
+
"test": "NODE_OPTIONS=--experimental-vm-modules jest",
|
|
13
13
|
"dev": "node src/index.js"
|
|
14
14
|
},
|
|
15
|
+
"jest": {
|
|
16
|
+
"testEnvironment": "node",
|
|
17
|
+
"transform": {},
|
|
18
|
+
"moduleNameMapper": {
|
|
19
|
+
"^(\\.{1,2}/.*)\\.js$": "$1"
|
|
20
|
+
}
|
|
21
|
+
},
|
|
15
22
|
"keywords": [
|
|
16
23
|
"claude",
|
|
17
24
|
"skills",
|
package/src/agents.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent 配置系统 - 支持多编辑器
|
|
3
|
+
* 定义各编辑器的项目级和用户级 Skill 目录
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as os from 'os';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import * as fs from 'fs';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Agent 定义
|
|
12
|
+
* id: 唯一标识
|
|
13
|
+
* name: 显示名称
|
|
14
|
+
* projectPath: 项目级目录(相对路径)
|
|
15
|
+
* globalPath: 用户级目录(绝对路径,使用 ~ 表示 home)
|
|
16
|
+
* detectFiles: 用于自动检测的文件/目录
|
|
17
|
+
*/
|
|
18
|
+
export const AGENTS = [
|
|
19
|
+
{
|
|
20
|
+
id: 'joycode',
|
|
21
|
+
name: 'JoyCode',
|
|
22
|
+
projectPath: '.joycode/skills',
|
|
23
|
+
globalPath: '~/.joycode/skills',
|
|
24
|
+
detectFiles: ['.joycode'],
|
|
25
|
+
priority: 100, // 最高优先级,默认使用
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'claude-code',
|
|
29
|
+
name: 'Claude Code',
|
|
30
|
+
projectPath: '.claude/skills',
|
|
31
|
+
globalPath: '~/.claude/skills',
|
|
32
|
+
detectFiles: ['.claude', '.claude-plugin'],
|
|
33
|
+
priority: 90,
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'cursor',
|
|
37
|
+
name: 'Cursor',
|
|
38
|
+
projectPath: '.agents/skills',
|
|
39
|
+
globalPath: '~/.cursor/skills',
|
|
40
|
+
detectFiles: ['.cursor', '.agents'],
|
|
41
|
+
priority: 80,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
id: 'codex',
|
|
45
|
+
name: 'Codex',
|
|
46
|
+
projectPath: '.agents/skills',
|
|
47
|
+
globalPath: '~/.codex/skills',
|
|
48
|
+
detectFiles: ['.codex'],
|
|
49
|
+
priority: 70,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'qoder',
|
|
53
|
+
name: 'Qoder',
|
|
54
|
+
projectPath: '.qoder/skills',
|
|
55
|
+
globalPath: '~/.qoder/skills',
|
|
56
|
+
detectFiles: ['.qoder'],
|
|
57
|
+
priority: 60,
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'opencode',
|
|
61
|
+
name: 'OpenCode',
|
|
62
|
+
projectPath: '.agents/skills',
|
|
63
|
+
globalPath: '~/.config/opencode/skills',
|
|
64
|
+
detectFiles: ['.opencode'],
|
|
65
|
+
priority: 50,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'universal',
|
|
69
|
+
name: 'Universal',
|
|
70
|
+
projectPath: '.agents/skills',
|
|
71
|
+
globalPath: '~/.config/agents/skills',
|
|
72
|
+
detectFiles: ['.agents'],
|
|
73
|
+
priority: 10,
|
|
74
|
+
},
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* 获取 Agent 配置
|
|
79
|
+
*/
|
|
80
|
+
export function getAgent(agentId) {
|
|
81
|
+
return AGENTS.find(a => a.id === agentId) || AGENTS[0]; // 默认返回 joycode
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* 获取所有 Agent
|
|
86
|
+
*/
|
|
87
|
+
export function getAllAgents() {
|
|
88
|
+
return AGENTS;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* 解析路径中的 ~ 为 home 目录
|
|
93
|
+
*/
|
|
94
|
+
export function resolvePath(pathWithTilde) {
|
|
95
|
+
if (pathWithTilde.startsWith('~/')) {
|
|
96
|
+
return path.join(os.homedir(), pathWithTilde.slice(2));
|
|
97
|
+
}
|
|
98
|
+
return pathWithTilde;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 获取 Agent 的项目级目录(绝对路径)
|
|
103
|
+
*/
|
|
104
|
+
export function getAgentProjectPath(agentId, projectRoot) {
|
|
105
|
+
const agent = getAgent(agentId);
|
|
106
|
+
return path.join(projectRoot, agent.projectPath);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* 获取 Agent 的用户级目录(绝对路径)
|
|
111
|
+
*/
|
|
112
|
+
export function getAgentGlobalPath(agentId) {
|
|
113
|
+
const agent = getAgent(agentId);
|
|
114
|
+
return resolvePath(agent.globalPath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* 检测项目中存在的 Agent
|
|
119
|
+
* 返回按优先级排序的 Agent ID 列表
|
|
120
|
+
*/
|
|
121
|
+
export function detectAgents(projectRoot) {
|
|
122
|
+
const detected = [];
|
|
123
|
+
|
|
124
|
+
for (const agent of AGENTS) {
|
|
125
|
+
// 检查项目级目录是否存在
|
|
126
|
+
const projectPath = getAgentProjectPath(agent.id, projectRoot);
|
|
127
|
+
if (fs.existsSync(projectPath)) {
|
|
128
|
+
detected.push(agent.id);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// 检查检测文件是否存在
|
|
133
|
+
for (const detectFile of agent.detectFiles) {
|
|
134
|
+
const detectPath = path.join(projectRoot, detectFile);
|
|
135
|
+
if (fs.existsSync(detectPath)) {
|
|
136
|
+
detected.push(agent.id);
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// 按优先级排序
|
|
143
|
+
detected.sort((a, b) => {
|
|
144
|
+
const agentA = getAgent(a);
|
|
145
|
+
const agentB = getAgent(b);
|
|
146
|
+
return agentB.priority - agentA.priority;
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
return detected;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* 获取默认 Agent
|
|
154
|
+
* 始终返回 joycode 作为默认,不管检测到什么其他 Agent
|
|
155
|
+
* 这是 JoyCode 的品牌策略
|
|
156
|
+
*/
|
|
157
|
+
export function getDefaultAgent(projectRoot) {
|
|
158
|
+
// 始终返回 joycode 作为默认
|
|
159
|
+
return getAgent('joycode');
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 获取所有 Skill 目录(项目级 + 用户级)
|
|
164
|
+
* 用于 SkillLoader 加载
|
|
165
|
+
*/
|
|
166
|
+
export function getAllSkillPaths(projectRoot, agentId = null) {
|
|
167
|
+
const paths = [];
|
|
168
|
+
const agents = agentId ? [getAgent(agentId)] : AGENTS;
|
|
169
|
+
|
|
170
|
+
for (const agent of agents) {
|
|
171
|
+
// 项目级目录
|
|
172
|
+
const projectPath = getAgentProjectPath(agent.id, projectRoot);
|
|
173
|
+
if (fs.existsSync(projectPath)) {
|
|
174
|
+
paths.push({
|
|
175
|
+
path: projectPath,
|
|
176
|
+
scope: 'project',
|
|
177
|
+
agent: agent.id,
|
|
178
|
+
priority: agent.priority,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// 用户级目录
|
|
183
|
+
const globalPath = getAgentGlobalPath(agent.id);
|
|
184
|
+
if (fs.existsSync(globalPath)) {
|
|
185
|
+
paths.push({
|
|
186
|
+
path: globalPath,
|
|
187
|
+
scope: 'global',
|
|
188
|
+
agent: agent.id,
|
|
189
|
+
priority: agent.priority - 1, // 用户级优先级略低
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// 按优先级排序
|
|
195
|
+
paths.sort((a, b) => b.priority - a.priority);
|
|
196
|
+
|
|
197
|
+
return paths;
|
|
198
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check Command - 检查技能更新
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Command } from 'commander';
|
|
6
|
+
import { checkAllSkills, getUpdatableSkills } from '../version-checker.js';
|
|
7
|
+
import chalk from 'chalk';
|
|
8
|
+
|
|
9
|
+
export function checkCommand(program) {
|
|
10
|
+
program
|
|
11
|
+
.command('check')
|
|
12
|
+
.description('Check for available skill updates')
|
|
13
|
+
.option('-u, --updatable', 'Show only skills with updates')
|
|
14
|
+
.action(async (options) => {
|
|
15
|
+
try {
|
|
16
|
+
const projectRoot = process.cwd();
|
|
17
|
+
|
|
18
|
+
console.log(chalk.blue('Checking for skill updates...'));
|
|
19
|
+
console.log(chalk.gray('==============================\n'));
|
|
20
|
+
|
|
21
|
+
const results = options.updatable
|
|
22
|
+
? await getUpdatableSkills(projectRoot)
|
|
23
|
+
: await checkAllSkills(projectRoot);
|
|
24
|
+
|
|
25
|
+
if (results.length === 0) {
|
|
26
|
+
if (options.updatable) {
|
|
27
|
+
console.log(chalk.green('✅ All skills are up to date!'));
|
|
28
|
+
} else {
|
|
29
|
+
console.log(chalk.yellow('No skills found.'));
|
|
30
|
+
}
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let updatableCount = 0;
|
|
35
|
+
|
|
36
|
+
for (const result of results) {
|
|
37
|
+
if (result.hasUpdate) {
|
|
38
|
+
updatableCount++;
|
|
39
|
+
console.log(chalk.yellow(`📦 ${result.name}`));
|
|
40
|
+
console.log(` Current: v${result.currentVersion}`);
|
|
41
|
+
console.log(chalk.green(` ${result.commitsBehind} commit(s) behind`));
|
|
42
|
+
if (result.latestCommit) {
|
|
43
|
+
console.log(chalk.gray(` Latest: ${result.latestCommit.message.substring(0, 50)}...`));
|
|
44
|
+
}
|
|
45
|
+
console.log();
|
|
46
|
+
} else if (!options.updatable) {
|
|
47
|
+
console.log(chalk.green(`✅ ${result.name} v${result.currentVersion}`));
|
|
48
|
+
if (result.error) {
|
|
49
|
+
console.log(chalk.gray(` ${result.error}`));
|
|
50
|
+
} else {
|
|
51
|
+
console.log(chalk.gray(' Up to date'));
|
|
52
|
+
}
|
|
53
|
+
console.log();
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (updatableCount > 0) {
|
|
58
|
+
console.log(chalk.blue(`\nFound ${updatableCount} skill(s) with updates.`));
|
|
59
|
+
console.log(chalk.gray('Run `joySkills update` to update all skills.'));
|
|
60
|
+
} else if (!options.updatable) {
|
|
61
|
+
console.log(chalk.green('\n✅ All skills are up to date!'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(chalk.red('❌ Failed to check updates:'), error.message);
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
}
|
package/src/commands/install.js
CHANGED
|
@@ -2,6 +2,8 @@ import { Command } from 'commander';
|
|
|
2
2
|
import { LocalManager } from '../local.js';
|
|
3
3
|
import { LockfileManager } from '../lockfile.js';
|
|
4
4
|
import { RegistryManager } from '../registry.js';
|
|
5
|
+
import { getAgent, getAgentProjectPath, getAgentGlobalPath, detectAgents } from '../agents.js';
|
|
6
|
+
import { installSkill, InstallMethod } from '../installer.js';
|
|
5
7
|
import simpleGit from 'simple-git';
|
|
6
8
|
import * as fs from 'fs';
|
|
7
9
|
import * as path from 'path';
|
|
@@ -15,25 +17,22 @@ const CACHE_DIR = process.env.JOYSKILL_CACHE_DIR || path.join(os.homedir(), '.jo
|
|
|
15
17
|
export function installCommand(program) {
|
|
16
18
|
program
|
|
17
19
|
.command('install [skill]')
|
|
20
|
+
.alias('add')
|
|
18
21
|
.description('Install a skill from registry or GitHub')
|
|
19
22
|
.option('-v, --version <version>', 'Specify version to install')
|
|
20
23
|
.option('-r, --registry <name>', 'Install from specific registry')
|
|
21
|
-
.option('-g, --global', 'Install
|
|
22
|
-
.option('--
|
|
24
|
+
.option('-g, --global', 'Install to user-level directory (~/.joycode/skills/)')
|
|
25
|
+
.option('-a, --agent <agent>', 'Target specific agent (joycode, claude-code, cursor, etc.)')
|
|
26
|
+
.option('-d, --dir <path>', 'Install to custom directory')
|
|
27
|
+
.option('--copy', 'Copy files instead of symlinking')
|
|
23
28
|
.option('--force', 'Force installation even if version is not recommended')
|
|
24
29
|
.option('-y, --yes', 'Skip all prompts (CI mode)')
|
|
25
30
|
.action(async (skillInput, options) => {
|
|
26
31
|
try {
|
|
27
32
|
const projectRoot = process.cwd();
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
// default → .claude/skills/
|
|
32
|
-
const targetDir = options.global
|
|
33
|
-
? path.join(os.homedir(), '.claude', 'skills')
|
|
34
|
-
: options.universal
|
|
35
|
-
? path.join(projectRoot, '.agent', 'skills')
|
|
36
|
-
: path.join(projectRoot, '.claude', 'skills');
|
|
33
|
+
|
|
34
|
+
// 解析目标目录
|
|
35
|
+
const targetDir = resolveTargetDir(projectRoot, options);
|
|
37
36
|
|
|
38
37
|
if (!fs.existsSync(targetDir)) {
|
|
39
38
|
fs.mkdirSync(targetDir, { recursive: true });
|
|
@@ -54,6 +53,35 @@ export function installCommand(program) {
|
|
|
54
53
|
});
|
|
55
54
|
}
|
|
56
55
|
|
|
56
|
+
/**
|
|
57
|
+
* 解析目标目录
|
|
58
|
+
* 优先级: --dir > --agent > --global > 默认(joycode)
|
|
59
|
+
*/
|
|
60
|
+
function resolveTargetDir(projectRoot, options) {
|
|
61
|
+
// 1. 自定义目录最高优先级
|
|
62
|
+
if (options.dir) {
|
|
63
|
+
return path.resolve(options.dir);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 2. 指定 Agent
|
|
67
|
+
if (options.agent) {
|
|
68
|
+
const agent = getAgent(options.agent);
|
|
69
|
+
if (options.global) {
|
|
70
|
+
return getAgentGlobalPath(agent.id);
|
|
71
|
+
}
|
|
72
|
+
return getAgentProjectPath(agent.id, projectRoot);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 3. 仅 --global,使用 joycode 用户级目录
|
|
76
|
+
if (options.global) {
|
|
77
|
+
return getAgentGlobalPath('joycode');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// 4. 默认: 始终使用 joycode 项目级目录
|
|
81
|
+
// 这是 JoyCode 的品牌策略,不随检测到的其他 Agent 改变
|
|
82
|
+
return getAgentProjectPath('joycode', projectRoot);
|
|
83
|
+
}
|
|
84
|
+
|
|
57
85
|
/**
|
|
58
86
|
* Priority resolution chain:
|
|
59
87
|
* 1. Local absolute/relative path
|
|
@@ -65,10 +93,24 @@ async function resolveAndInstall(skillInput, targetDir, projectRoot, options) {
|
|
|
65
93
|
const JOYSKILL_CONFIG_DIR = process.env.JOYSKILL_CONFIG_DIR || path.join(os.homedir(), '.joyskill');
|
|
66
94
|
|
|
67
95
|
// ── Step 0: Direct Git URL (git@xxx or https://github.com/...) ─────────
|
|
96
|
+
// Support: git@host:org/repo.git or git@host:org/repo.git/subpath
|
|
68
97
|
if (skillInput.startsWith('git@') || skillInput.startsWith('https://') || skillInput.startsWith('http://')) {
|
|
69
|
-
//
|
|
70
|
-
|
|
71
|
-
|
|
98
|
+
// Parse URL and optional subpath
|
|
99
|
+
// Format: git@host:org/repo.git/subpath/to/skill
|
|
100
|
+
let gitUrl = skillInput;
|
|
101
|
+
let subPath = '';
|
|
102
|
+
|
|
103
|
+
// Check for subpath after .git
|
|
104
|
+
const gitExtMatch = skillInput.match(/^(.+?\.git)(\/.*)$/);
|
|
105
|
+
if (gitExtMatch) {
|
|
106
|
+
gitUrl = gitExtMatch[1];
|
|
107
|
+
subPath = gitExtMatch[2].replace(/^\//, ''); // Remove leading slash
|
|
108
|
+
} else if (!skillInput.endsWith('.git')) {
|
|
109
|
+
// No .git extension, treat entire input as URL
|
|
110
|
+
gitUrl = skillInput;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
await installFromGitUrl(gitUrl, targetDir, options, subPath);
|
|
72
114
|
return;
|
|
73
115
|
}
|
|
74
116
|
|
|
@@ -151,13 +193,26 @@ async function resolveAndInstall(skillInput, targetDir, projectRoot, options) {
|
|
|
151
193
|
|
|
152
194
|
// ── Fallback: create template ────────────────────────────────────
|
|
153
195
|
console.log(chalk.yellow(`⚠️ "${skillInput}" not found in any registry, creating template...`));
|
|
154
|
-
|
|
196
|
+
|
|
197
|
+
// 使用 targetDir 安装模板
|
|
198
|
+
const skillPath = path.join(targetDir, skillInput);
|
|
199
|
+
if (!fs.existsSync(skillPath)) {
|
|
200
|
+
fs.mkdirSync(skillPath, { recursive: true });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const skillMdPath = path.join(skillPath, 'SKILL.md');
|
|
204
|
+
fs.writeFileSync(skillMdPath, generateSkillTemplate(skillInput));
|
|
205
|
+
|
|
155
206
|
const lockfileManager = new LockfileManager(projectRoot);
|
|
156
|
-
localManager.installSkill(skillInput, generateSkillTemplate(skillInput));
|
|
157
207
|
await lockfileManager.load();
|
|
158
|
-
lockfileManager.updateSkill(skillInput, {
|
|
208
|
+
lockfileManager.updateSkill(skillInput, {
|
|
209
|
+
version: '1.0.0',
|
|
210
|
+
source: 'template',
|
|
211
|
+
path: skillPath,
|
|
212
|
+
installedAt: new Date().toISOString()
|
|
213
|
+
});
|
|
159
214
|
await lockfileManager.save();
|
|
160
|
-
console.log(chalk.green(`✅ Created template for ${skillInput}`));
|
|
215
|
+
console.log(chalk.green(`✅ Created template for ${skillInput} in ${targetDir}`));
|
|
161
216
|
}
|
|
162
217
|
|
|
163
218
|
/**
|
|
@@ -188,7 +243,14 @@ async function tryInstallFromRegistryDir(skillName, registryDirPath, targetDir,
|
|
|
188
243
|
if (!fs.existsSync(sourcePath)) return false;
|
|
189
244
|
|
|
190
245
|
const targetPath = path.join(targetDir, skillName);
|
|
191
|
-
|
|
246
|
+
|
|
247
|
+
// 使用新的 installer 模块,支持 symlink/copy
|
|
248
|
+
const method = options.copy ? InstallMethod.COPY : InstallMethod.SYMLINK;
|
|
249
|
+
installSkill(sourcePath, targetPath, method);
|
|
250
|
+
|
|
251
|
+
// 获取安装方式信息
|
|
252
|
+
const installInfo = fs.lstatSync(targetPath);
|
|
253
|
+
const installMethod = installInfo.isSymbolicLink() ? 'symlink' : 'copy';
|
|
192
254
|
|
|
193
255
|
const lockfileManager = new LockfileManager(projectRoot);
|
|
194
256
|
await lockfileManager.load();
|
|
@@ -196,11 +258,13 @@ async function tryInstallFromRegistryDir(skillName, registryDirPath, targetDir,
|
|
|
196
258
|
version,
|
|
197
259
|
source: sourceLabel,
|
|
198
260
|
registry: registryManager.getRegistryInfo().registryId,
|
|
261
|
+
installMethod,
|
|
199
262
|
installedAt: new Date().toISOString()
|
|
200
263
|
});
|
|
201
264
|
await lockfileManager.save();
|
|
202
265
|
|
|
203
|
-
|
|
266
|
+
const methodLabel = installMethod === 'symlink' ? chalk.gray('(symlink)') : chalk.gray('(copy)');
|
|
267
|
+
console.log(chalk.green(`✅ Installed ${skillName} v${version} from ${sourceLabel}`) + ' ' + methodLabel);
|
|
204
268
|
return true;
|
|
205
269
|
} catch (e) {
|
|
206
270
|
return false;
|
|
@@ -346,7 +410,7 @@ async function installFromLocalPath(localPath, targetDir, options) {
|
|
|
346
410
|
}
|
|
347
411
|
}
|
|
348
412
|
|
|
349
|
-
async function installFromGitUrl(gitUrl, targetDir, options) {
|
|
413
|
+
async function installFromGitUrl(gitUrl, targetDir, options, subPath = '') {
|
|
350
414
|
// Derive cache key from URL
|
|
351
415
|
const cacheKey = gitUrl
|
|
352
416
|
.replace(/^git@/, '')
|
|
@@ -355,8 +419,12 @@ async function installFromGitUrl(gitUrl, targetDir, options) {
|
|
|
355
419
|
.replace(/[:/]/g, '-');
|
|
356
420
|
|
|
357
421
|
const cachePath = path.join(CACHE_DIR, cacheKey);
|
|
422
|
+
const sourcePath = subPath ? path.join(cachePath, subPath) : cachePath;
|
|
358
423
|
|
|
359
424
|
console.log(chalk.blue(`📦 Installing from: ${gitUrl}`));
|
|
425
|
+
if (subPath) {
|
|
426
|
+
console.log(chalk.gray(` Subpath: ${subPath}`));
|
|
427
|
+
}
|
|
360
428
|
|
|
361
429
|
// Check if cache is valid (non-empty)
|
|
362
430
|
const cacheValid = fs.existsSync(cachePath) && fs.readdirSync(cachePath).length > 0;
|
|
@@ -377,9 +445,17 @@ async function installFromGitUrl(gitUrl, targetDir, options) {
|
|
|
377
445
|
console.log(chalk.green(' ✓ Repository cloned'));
|
|
378
446
|
} else {
|
|
379
447
|
console.log(chalk.gray(' Using cached repository'));
|
|
448
|
+
// Try to update cache
|
|
449
|
+
try {
|
|
450
|
+
const git = simpleGit(cachePath);
|
|
451
|
+
await git.pull('origin', 'main', ['--depth', '1']);
|
|
452
|
+
console.log(chalk.gray(' ✓ Cache updated'));
|
|
453
|
+
} catch (e) {
|
|
454
|
+
// Ignore update errors, use cached version
|
|
455
|
+
}
|
|
380
456
|
}
|
|
381
457
|
|
|
382
|
-
const skills = await findSkills(
|
|
458
|
+
const skills = await findSkills(sourcePath);
|
|
383
459
|
if (skills.length === 0) throw new Error('No skills found in repository');
|
|
384
460
|
|
|
385
461
|
console.log(chalk.green(` Found ${skills.length} skill(s)`));
|
|
@@ -439,6 +515,14 @@ async function installFromGitHub(owner, repo, skillPath, targetDir, options) {
|
|
|
439
515
|
console.log(chalk.green(' ✓ Repository cloned'));
|
|
440
516
|
} else {
|
|
441
517
|
console.log(chalk.gray(' Using cached repository'));
|
|
518
|
+
// Try to update cache
|
|
519
|
+
try {
|
|
520
|
+
const git = simpleGit(cachePath);
|
|
521
|
+
await git.pull('origin', 'main', ['--depth', '1']);
|
|
522
|
+
console.log(chalk.gray(' ✓ Cache updated'));
|
|
523
|
+
} catch (e) {
|
|
524
|
+
// Ignore update errors, use cached version
|
|
525
|
+
}
|
|
442
526
|
}
|
|
443
527
|
|
|
444
528
|
// Find skills in the repo
|