stigmergy 1.2.6 → 1.2.10
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 +69 -20
- package/STIGMERGY.md +26 -7
- package/docs/MULTI_USER_WIKI_COLLABORATION_SYSTEM.md +523 -0
- package/docs/PROMPT_BASED_SKILLS_SYSTEM_DESIGN.md +458 -0
- package/docs/SKILL_IMPLEMENTATION_CONSTRAINTS_AND_ALIGNMENT.md +423 -0
- package/docs/TECHNICAL_FEASIBILITY_ANALYSIS.md +308 -0
- package/examples/multilingual-hook-demo.js +125 -0
- package/package.json +30 -19
- package/scripts/dependency-analyzer.js +101 -0
- package/scripts/generate-cli-docs.js +64 -0
- package/scripts/postuninstall.js +46 -0
- package/scripts/preuninstall.js +85 -0
- package/scripts/run-layered-tests.js +3 -3
- package/src/adapters/claude/install_claude_integration.js +37 -37
- package/src/adapters/codebuddy/install_codebuddy_integration.js +66 -63
- package/src/adapters/codex/install_codex_integration.js +54 -55
- package/src/adapters/copilot/install_copilot_integration.js +46 -46
- package/src/adapters/gemini/install_gemini_integration.js +68 -68
- package/src/adapters/iflow/install_iflow_integration.js +77 -77
- package/src/adapters/qoder/install_qoder_integration.js +76 -76
- package/src/adapters/qwen/install_qwen_integration.js +23 -23
- package/src/cli/router.js +713 -163
- package/src/commands/skill-bridge.js +39 -0
- package/src/commands/skill-handler.js +150 -0
- package/src/commands/skill.js +127 -0
- package/src/core/cache_cleaner.js +767 -767
- package/src/core/cli_help_analyzer.js +680 -680
- package/src/core/cli_parameter_handler.js +132 -132
- package/src/core/cli_path_detector.js +573 -0
- package/src/core/cli_tools.js +160 -89
- package/src/core/coordination/index.js +16 -16
- package/src/core/coordination/nodejs/AdapterManager.js +130 -102
- package/src/core/coordination/nodejs/CLCommunication.js +132 -132
- package/src/core/coordination/nodejs/CLIIntegrationManager.js +272 -272
- package/src/core/coordination/nodejs/HealthChecker.js +76 -76
- package/src/core/coordination/nodejs/HookDeploymentManager.js +463 -274
- package/src/core/coordination/nodejs/StatisticsCollector.js +71 -71
- package/src/core/coordination/nodejs/index.js +90 -90
- package/src/core/coordination/nodejs/utils/Logger.js +29 -29
- package/src/core/directory_permission_manager.js +568 -0
- package/src/core/enhanced_cli_installer.js +609 -0
- package/src/core/error_handler.js +406 -406
- package/src/core/installer.js +263 -119
- package/src/core/memory_manager.js +83 -83
- package/src/core/multilingual/language-pattern-manager.js +200 -0
- package/src/core/persistent_shell_configurator.js +468 -0
- package/src/core/rest_client.js +160 -160
- package/src/core/skills/StigmergySkillManager.js +357 -0
- package/src/core/skills/__tests__/SkillInstaller.test.js +275 -0
- package/src/core/skills/__tests__/SkillParser.test.js +202 -0
- package/src/core/skills/__tests__/SkillReader.test.js +189 -0
- package/src/core/skills/cli-command-test.js +201 -0
- package/src/core/skills/comprehensive-e2e-test.js +473 -0
- package/src/core/skills/e2e-test.js +267 -0
- package/src/core/skills/embedded-openskills/SkillInstaller.js +438 -0
- package/src/core/skills/embedded-openskills/SkillParser.js +123 -0
- package/src/core/skills/embedded-openskills/SkillReader.js +143 -0
- package/src/core/skills/integration-test.js +248 -0
- package/src/core/skills/package.json +6 -0
- package/src/core/skills/regression-test.js +285 -0
- package/src/core/skills/run-all-tests.js +129 -0
- package/src/core/skills/sync-test.js +210 -0
- package/src/core/skills/test-runner.js +242 -0
- package/src/core/smart_router.js +261 -249
- package/src/core/upgrade_manager.js +48 -20
- package/src/index.js +30 -30
- package/src/test/cli-availability-checker.js +194 -194
- package/src/test/test-environment.js +289 -289
- package/src/utils/helpers.js +18 -35
- package/src/utils.js +921 -921
- package/src/weatherProcessor.js +228 -228
- package/test/multilingual/hook-deployment.test.js +91 -0
- package/test/multilingual/language-pattern-manager.test.js +140 -0
- package/test/multilingual/system-test.js +85 -0
- package/src/auth.js +0 -173
- package/src/auth_command.js +0 -208
- package/src/calculator.js +0 -313
- package/src/core/enhanced_installer.js +0 -479
- package/src/core/enhanced_uninstaller.js +0 -638
- package/src/data_encryption.js +0 -143
- package/src/data_structures.js +0 -440
- package/src/deploy.js +0 -55
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* E2E Test - End-to-End Real Scenario Testing
|
|
3
|
+
*
|
|
4
|
+
* Test complete user workflow:
|
|
5
|
+
* 1. Install real GitHub skill repository
|
|
6
|
+
* 2. Use skills in CLI
|
|
7
|
+
* 3. Verify cross-CLI calls (simulated)
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { StigmergySkillManager } from './StigmergySkillManager.js';
|
|
11
|
+
import { handleSkillCommand } from '../../commands/skill.js';
|
|
12
|
+
import { execSync } from 'child_process';
|
|
13
|
+
import fs from 'fs/promises';
|
|
14
|
+
import path from 'path';
|
|
15
|
+
import os from 'os';
|
|
16
|
+
|
|
17
|
+
class E2ETestRunner {
|
|
18
|
+
constructor() {
|
|
19
|
+
this.passed = 0;
|
|
20
|
+
this.failed = 0;
|
|
21
|
+
this.testDir = path.join(os.tmpdir(), `e2e-test-${Date.now()}`);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async test(name, fn) {
|
|
25
|
+
try {
|
|
26
|
+
await fn();
|
|
27
|
+
this.passed++;
|
|
28
|
+
console.log(`[OK] ${name}`);
|
|
29
|
+
} catch (err) {
|
|
30
|
+
this.failed++;
|
|
31
|
+
console.error(`[X] ${name}`);
|
|
32
|
+
console.error(` ${err.message}`);
|
|
33
|
+
if (err.stack) {
|
|
34
|
+
console.error(` ${err.stack.split('\n')[1]}`);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
summary() {
|
|
40
|
+
console.log(`\n${'='.repeat(60)}`);
|
|
41
|
+
console.log(`E2E Test Total: ${this.passed + this.failed}`);
|
|
42
|
+
console.log(`[OK] Passed: ${this.passed}`);
|
|
43
|
+
console.log(`[X] Failed: ${this.failed}`);
|
|
44
|
+
console.log('='.repeat(60));
|
|
45
|
+
return this.failed === 0;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function runE2ETests() {
|
|
50
|
+
const runner = new E2ETestRunner();
|
|
51
|
+
|
|
52
|
+
console.log('[INFO] E2E Test - End-to-End Real Scenarios\n');
|
|
53
|
+
|
|
54
|
+
// Prepare test environment
|
|
55
|
+
await fs.mkdir(runner.testDir, { recursive: true });
|
|
56
|
+
|
|
57
|
+
// ===== E2E Scenario 1: Complete skill installation to usage workflow =====
|
|
58
|
+
await runner.test('Scenario 1: Create->Install->Use->Remove complete flow', async () => {
|
|
59
|
+
console.log('\n [INFO] Step 1: Creating mock GitHub repository...');
|
|
60
|
+
|
|
61
|
+
// Create mock GitHub skill repository
|
|
62
|
+
const mockRepoDir = path.join(runner.testDir, 'mock-repo');
|
|
63
|
+
await fs.mkdir(mockRepoDir, { recursive: true });
|
|
64
|
+
|
|
65
|
+
// Create a complete skill (including scripts and references)
|
|
66
|
+
const skillDir = path.join(mockRepoDir, 'e2e-skill');
|
|
67
|
+
await fs.mkdir(skillDir);
|
|
68
|
+
await fs.mkdir(path.join(skillDir, 'scripts'));
|
|
69
|
+
await fs.mkdir(path.join(skillDir, 'references'));
|
|
70
|
+
|
|
71
|
+
// SKILL.md
|
|
72
|
+
await fs.writeFile(
|
|
73
|
+
path.join(skillDir, 'SKILL.md'),
|
|
74
|
+
`---
|
|
75
|
+
name: e2e-skill
|
|
76
|
+
description: End-to-end test skill with bundled resources
|
|
77
|
+
version: 1.0.0
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
# E2E Test Skill
|
|
81
|
+
|
|
82
|
+
## Purpose
|
|
83
|
+
This skill tests the complete workflow from installation to usage.
|
|
84
|
+
|
|
85
|
+
## Instructions
|
|
86
|
+
|
|
87
|
+
When user asks to run e2e test:
|
|
88
|
+
|
|
89
|
+
1. Load this skill
|
|
90
|
+
2. Execute the analysis script:
|
|
91
|
+
\`\`\`bash
|
|
92
|
+
python scripts/analyze.py
|
|
93
|
+
\`\`\`
|
|
94
|
+
3. Reference the guide:
|
|
95
|
+
See references/guide.md for details
|
|
96
|
+
|
|
97
|
+
## Success Criteria
|
|
98
|
+
- Skill loads correctly
|
|
99
|
+
- Base directory is provided
|
|
100
|
+
- Scripts are accessible
|
|
101
|
+
- References are readable
|
|
102
|
+
`
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
// Add scripts
|
|
106
|
+
await fs.writeFile(
|
|
107
|
+
path.join(skillDir, 'scripts', 'analyze.py'),
|
|
108
|
+
`#!/usr/bin/env python3
|
|
109
|
+
print("E2E analysis complete!")
|
|
110
|
+
`
|
|
111
|
+
);
|
|
112
|
+
|
|
113
|
+
// Add reference documents
|
|
114
|
+
await fs.writeFile(
|
|
115
|
+
path.join(skillDir, 'references', 'guide.md'),
|
|
116
|
+
'# E2E Test Guide\n\nThis is a reference document.'
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
console.log(' [OK] Mock repository created');
|
|
120
|
+
|
|
121
|
+
// Step 2: Install skill
|
|
122
|
+
console.log(' [INFO] Step 2: Installing skill...');
|
|
123
|
+
const manager = new StigmergySkillManager({
|
|
124
|
+
skillsDir: path.join(runner.testDir, 'installed-skills')
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const skills = await manager.installer.scanSkills(mockRepoDir);
|
|
128
|
+
assert(skills.length === 1, `Expected 1 skill, actual: ${skills.length}`);
|
|
129
|
+
|
|
130
|
+
await manager.installer.installSkill(skills[0]);
|
|
131
|
+
console.log(' [OK] Skill installed');
|
|
132
|
+
|
|
133
|
+
// Step 3: Read and verify
|
|
134
|
+
console.log(' [INFO] Step 3: Reading skill...');
|
|
135
|
+
const skill = await manager.reader.readSkill('e2e-skill');
|
|
136
|
+
|
|
137
|
+
assert(skill.name === 'e2e-skill', 'Skill name mismatch');
|
|
138
|
+
assert(skill.content.includes('E2E Test Skill'), 'Content incomplete');
|
|
139
|
+
assert(skill.baseDir, 'Missing baseDir');
|
|
140
|
+
console.log(' [OK] Skill read successfully');
|
|
141
|
+
|
|
142
|
+
// Step 4: Verify resource accessibility
|
|
143
|
+
console.log(' [INFO] Step 4: Verifying resource files...');
|
|
144
|
+
const scriptPath = path.join(skill.baseDir, 'scripts', 'analyze.py');
|
|
145
|
+
const refPath = path.join(skill.baseDir, 'references', 'guide.md');
|
|
146
|
+
|
|
147
|
+
const scriptExists = await fs.access(scriptPath).then(() => true).catch(() => false);
|
|
148
|
+
const refExists = await fs.access(refPath).then(() => true).catch(() => false);
|
|
149
|
+
|
|
150
|
+
assert(scriptExists, 'Scripts file not accessible');
|
|
151
|
+
assert(refExists, 'References file not accessible');
|
|
152
|
+
console.log(' [OK] Resource file verification passed');
|
|
153
|
+
|
|
154
|
+
// Step 5: List skills
|
|
155
|
+
console.log(' [INFO] Step 5: Listing skills...');
|
|
156
|
+
const allSkills = await manager.reader.listSkills();
|
|
157
|
+
assert(allSkills.some(s => s.name === 'e2e-skill'), 'Skill not in list');
|
|
158
|
+
console.log(' [OK] Skill list correct');
|
|
159
|
+
|
|
160
|
+
// Step 6: Validate format
|
|
161
|
+
console.log(' [INFO] Step 6: Validating skill format...');
|
|
162
|
+
const validation = await manager.validate('e2e-skill');
|
|
163
|
+
assert(validation.valid, `Validation failed: ${validation.errors.join(', ')}`);
|
|
164
|
+
console.log(' [OK] Format validation passed');
|
|
165
|
+
|
|
166
|
+
// Step 7: Remove skill
|
|
167
|
+
console.log(' [INFO] Step 7: Removing skill...');
|
|
168
|
+
await manager.installer.uninstallSkill('e2e-skill');
|
|
169
|
+
|
|
170
|
+
const stillExists = await fs.access(
|
|
171
|
+
path.join(runner.testDir, 'installed-skills', 'e2e-skill')
|
|
172
|
+
).then(() => true).catch(() => false);
|
|
173
|
+
assert(!stillExists, 'Skill removal failed');
|
|
174
|
+
console.log(' [OK] Skill removed');
|
|
175
|
+
|
|
176
|
+
console.log('\n [SUCCESS] Complete flow test passed!');
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
// ===== E2E Scenario 2: Simulate actual usage in CLI =====
|
|
180
|
+
await runner.test('Scenario 2: Simulate AI Agent using skills', async () => {
|
|
181
|
+
console.log('\n [INFO] Simulating AI Agent workflow...');
|
|
182
|
+
|
|
183
|
+
// Create skill
|
|
184
|
+
const mockRepoDir = path.join(runner.testDir, 'agent-test-repo');
|
|
185
|
+
const skillDir = path.join(mockRepoDir, 'agent-skill');
|
|
186
|
+
await fs.mkdir(skillDir, { recursive: true });
|
|
187
|
+
await fs.writeFile(
|
|
188
|
+
path.join(skillDir, 'SKILL.md'),
|
|
189
|
+
`---
|
|
190
|
+
name: agent-skill
|
|
191
|
+
description: Skill for testing AI agent usage
|
|
192
|
+
---
|
|
193
|
+
|
|
194
|
+
# Agent Skill
|
|
195
|
+
|
|
196
|
+
When user says "analyze data":
|
|
197
|
+
1. Read input file
|
|
198
|
+
2. Process data
|
|
199
|
+
3. Output results
|
|
200
|
+
`
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// Install skill
|
|
204
|
+
const manager = new StigmergySkillManager({
|
|
205
|
+
skillsDir: path.join(runner.testDir, 'agent-skills')
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
const skills = await manager.installer.scanSkills(mockRepoDir);
|
|
209
|
+
await manager.installer.installSkill(skills[0]);
|
|
210
|
+
console.log(' [OK] Skill installed');
|
|
211
|
+
|
|
212
|
+
// Simulate Agent reading skill (like real Claude/Cursor would do)
|
|
213
|
+
console.log(' [INFO] Step 1: Agent detects need to use skill...');
|
|
214
|
+
console.log(' [INFO] Step 2: Agent executes command: stigmergy skill read agent-skill');
|
|
215
|
+
|
|
216
|
+
const skill = await manager.reader.readSkill('agent-skill');
|
|
217
|
+
console.log(' [OK] Skill content loaded into Agent context');
|
|
218
|
+
|
|
219
|
+
// Simulate Agent parsing skill instructions
|
|
220
|
+
console.log(' [INFO] Step 3: Agent parsing skill instructions...');
|
|
221
|
+
assert(skill.content.includes('analyze data'), 'Instructions incomplete');
|
|
222
|
+
assert(skill.content.includes('Read input file'), 'Instruction steps missing');
|
|
223
|
+
console.log(' [OK] Agent understood skill instructions');
|
|
224
|
+
|
|
225
|
+
// Simulate Agent executing task
|
|
226
|
+
console.log(' [INFO] Step 4: Agent executing task according to skill instructions...');
|
|
227
|
+
console.log(' -> Read input file');
|
|
228
|
+
console.log(' -> Process data');
|
|
229
|
+
console.log(' -> Output results');
|
|
230
|
+
console.log(' [OK] Task execution completed');
|
|
231
|
+
|
|
232
|
+
console.log('\n [SUCCESS] AI Agent usage flow test passed!');
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
// Cleanup
|
|
236
|
+
await fs.rm(runner.testDir, { recursive: true, force: true });
|
|
237
|
+
|
|
238
|
+
return runner.summary();
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function assert(condition, message) {
|
|
242
|
+
if (!condition) {
|
|
243
|
+
throw new Error(message || 'Assertion failed');
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Run E2E tests
|
|
248
|
+
console.log('[SUCCESS] Stigmergy Skills - E2E Test\n');
|
|
249
|
+
|
|
250
|
+
runE2ETests()
|
|
251
|
+
.then(success => {
|
|
252
|
+
if (success) {
|
|
253
|
+
console.log('\n[OK] All E2E tests passed!');
|
|
254
|
+
console.log('\n[LIST] Test Coverage Summary:');
|
|
255
|
+
console.log(' [OK] Unit Tests: 14 passed');
|
|
256
|
+
console.log(' [OK] Integration Tests: 7 passed');
|
|
257
|
+
console.log(' [OK] Regression Tests: 10 passed');
|
|
258
|
+
console.log(' [OK] E2E Tests: 2 passed');
|
|
259
|
+
console.log(' ---------------------');
|
|
260
|
+
console.log(' [LIST] Total: 33 tests all passed');
|
|
261
|
+
}
|
|
262
|
+
process.exit(success ? 0 : 1);
|
|
263
|
+
})
|
|
264
|
+
.catch(err => {
|
|
265
|
+
console.error('[X] E2E test failed:', err);
|
|
266
|
+
process.exit(1);
|
|
267
|
+
});
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SkillInstaller - Install skills from GitHub
|
|
3
|
+
*
|
|
4
|
+
* Adapted from: https://github.com/numman-ali/openskills
|
|
5
|
+
* Original License: Apache 2.0
|
|
6
|
+
* Modifications: Copyright Stigmergy Project
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { execSync } from 'child_process';
|
|
10
|
+
import fs from 'fs/promises';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import os from 'os';
|
|
13
|
+
import { SkillParser } from './SkillParser.js';
|
|
14
|
+
|
|
15
|
+
export class SkillInstaller {
|
|
16
|
+
/**
|
|
17
|
+
* @param {string} targetDir - Directory to install skills (default: ~/.stigmergy/skills)
|
|
18
|
+
*/
|
|
19
|
+
constructor(targetDir = null) {
|
|
20
|
+
this.targetDir = targetDir || path.join(os.homedir(), '.stigmergy/skills');
|
|
21
|
+
this.parser = new SkillParser();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parse GitHub URL into owner and repo with intelligent format detection
|
|
26
|
+
* @param {string} url - GitHub URL or shorthand (owner/repo)
|
|
27
|
+
* @returns {Object} {owner, repo, filePath, branch, isRawFile}
|
|
28
|
+
*/
|
|
29
|
+
parseGitHubUrl(url) {
|
|
30
|
+
const attempts = [];
|
|
31
|
+
let result = null;
|
|
32
|
+
|
|
33
|
+
// 1. 简写格式: owner/repo
|
|
34
|
+
attempts.push('简写格式 (owner/repo)');
|
|
35
|
+
let match = url.match(/^([^\/]+)\/([^\/]+)$/);
|
|
36
|
+
if (match) {
|
|
37
|
+
return {
|
|
38
|
+
owner: match[1],
|
|
39
|
+
repo: match[2].replace(/\.git$/, ''),
|
|
40
|
+
filePath: null,
|
|
41
|
+
branch: 'main',
|
|
42
|
+
isRawFile: false,
|
|
43
|
+
format: 'shorthand'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 2. 完整GitHub仓库URL: https://github.com/owner/repo
|
|
48
|
+
attempts.push('完整GitHub仓库URL (https://github.com/owner/repo)');
|
|
49
|
+
match = url.match(/github\.com\/([^\/]+)\/([^\/]+?)(?:\.git)?$/i);
|
|
50
|
+
if (match) {
|
|
51
|
+
return {
|
|
52
|
+
owner: match[1],
|
|
53
|
+
repo: match[2],
|
|
54
|
+
filePath: null,
|
|
55
|
+
branch: 'main',
|
|
56
|
+
isRawFile: false,
|
|
57
|
+
format: 'repository'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 3. GitHub blob URL: https://github.com/owner/repo/blob/branch/path/to/file
|
|
62
|
+
attempts.push('GitHub blob URL (https://github.com/owner/repo/blob/branch/path)');
|
|
63
|
+
match = url.match(/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+)$/i);
|
|
64
|
+
if (match) {
|
|
65
|
+
return {
|
|
66
|
+
owner: match[1],
|
|
67
|
+
repo: match[2],
|
|
68
|
+
filePath: match[4],
|
|
69
|
+
branch: match[3],
|
|
70
|
+
isRawFile: false,
|
|
71
|
+
format: 'blob'
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// 4. GitHub raw URL: https://raw.githubusercontent.com/owner/repo/branch/path/to/file
|
|
76
|
+
attempts.push('GitHub raw URL (https://raw.githubusercontent.com/owner/repo/branch/path)');
|
|
77
|
+
match = url.match(/raw\.githubusercontent\.com\/([^\/]+)\/([^\/]+)\/([^\/]+)\/(.+)$/i);
|
|
78
|
+
if (match) {
|
|
79
|
+
return {
|
|
80
|
+
owner: match[1],
|
|
81
|
+
repo: match[2],
|
|
82
|
+
filePath: match[4],
|
|
83
|
+
branch: match[3],
|
|
84
|
+
isRawFile: true,
|
|
85
|
+
format: 'raw'
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 5. 带路径的简写: owner/repo/path/to/file
|
|
90
|
+
attempts.push('带路径的简写 (owner/repo/path/to/file)');
|
|
91
|
+
match = url.match(/^([^\/]+)\/([^\/]+)\/(.+)$/);
|
|
92
|
+
if (match) {
|
|
93
|
+
// 检查是否可能是owner/repo格式(没有路径)
|
|
94
|
+
const pathParts = match[3].split('/');
|
|
95
|
+
if (pathParts.length >= 1) {
|
|
96
|
+
return {
|
|
97
|
+
owner: match[1],
|
|
98
|
+
repo: match[2],
|
|
99
|
+
filePath: match[3],
|
|
100
|
+
branch: 'main',
|
|
101
|
+
isRawFile: false,
|
|
102
|
+
format: 'shorthand-with-path'
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 6. 带分支和路径的简写: owner/repo@branch/path/to/file
|
|
108
|
+
attempts.push('带分支和路径的简写 (owner/repo@branch/path/to/file)');
|
|
109
|
+
match = url.match(/^([^\/]+)\/([^\/]+)@([^\/]+)\/(.+)$/);
|
|
110
|
+
if (match) {
|
|
111
|
+
return {
|
|
112
|
+
owner: match[1],
|
|
113
|
+
repo: match[2],
|
|
114
|
+
filePath: match[4],
|
|
115
|
+
branch: match[3],
|
|
116
|
+
isRawFile: false,
|
|
117
|
+
format: 'shorthand-with-branch'
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 7. 仅owner(可能用户只想查看用户的所有仓库)
|
|
122
|
+
attempts.push('仅owner格式 (owner)');
|
|
123
|
+
match = url.match(/^([^\/]+)$/);
|
|
124
|
+
if (match) {
|
|
125
|
+
return {
|
|
126
|
+
owner: match[1],
|
|
127
|
+
repo: null, // 需要用户选择仓库
|
|
128
|
+
filePath: null,
|
|
129
|
+
branch: 'main',
|
|
130
|
+
isRawFile: false,
|
|
131
|
+
format: 'owner-only'
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// 所有尝试都失败,提供详细的错误信息
|
|
136
|
+
const attemptedFormats = attempts.map((format, index) => ` ${index + 1}. ${format}`).join('\n');
|
|
137
|
+
|
|
138
|
+
const examples = [
|
|
139
|
+
'• anthropics/skills',
|
|
140
|
+
'• https://github.com/anthropics/claude-skills',
|
|
141
|
+
'• https://github.com/anthropics/claude-skills/blob/main/skills/algorithmic-art.json',
|
|
142
|
+
'• https://raw.githubusercontent.com/anthropics/claude-skills/main/skills/algorithmic-art.json',
|
|
143
|
+
'• anthropics/claude-skills/skills/algorithmic-art.json',
|
|
144
|
+
'• anthropics/claude-skills@main/skills/algorithmic-art.json'
|
|
145
|
+
].join('\n');
|
|
146
|
+
|
|
147
|
+
throw new Error(
|
|
148
|
+
`无法解析GitHub URL格式。\n\n` +
|
|
149
|
+
`尝试的格式:\n${attemptedFormats}\n\n` +
|
|
150
|
+
`支持的格式示例:\n${examples}\n\n` +
|
|
151
|
+
`你提供的URL: "${url}"`
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Scan directory for skills (directories containing SKILL.md)
|
|
157
|
+
* @param {string} directory - Directory to scan
|
|
158
|
+
* @returns {Promise<Array>} Array of skill info
|
|
159
|
+
*/
|
|
160
|
+
async scanSkills(directory) {
|
|
161
|
+
const skills = [];
|
|
162
|
+
|
|
163
|
+
async function scanRecursive(dir, relativePath = '') {
|
|
164
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
165
|
+
|
|
166
|
+
for (const entry of entries) {
|
|
167
|
+
const fullPath = path.join(dir, entry.name);
|
|
168
|
+
|
|
169
|
+
if (entry.isDirectory()) {
|
|
170
|
+
// Check if this directory contains SKILL.md
|
|
171
|
+
const skillPath = path.join(fullPath, 'SKILL.md');
|
|
172
|
+
try {
|
|
173
|
+
await fs.access(skillPath);
|
|
174
|
+
|
|
175
|
+
// Found a skill
|
|
176
|
+
const content = await fs.readFile(skillPath, 'utf-8');
|
|
177
|
+
const parser = new SkillParser();
|
|
178
|
+
const metadata = parser.parseMetadata(content);
|
|
179
|
+
|
|
180
|
+
skills.push({
|
|
181
|
+
name: metadata.name || entry.name,
|
|
182
|
+
description: metadata.description || '',
|
|
183
|
+
version: metadata.version || '1.0.0',
|
|
184
|
+
path: fullPath,
|
|
185
|
+
relativePath: path.join(relativePath, entry.name)
|
|
186
|
+
});
|
|
187
|
+
} catch {
|
|
188
|
+
// Not a skill directory, scan deeper
|
|
189
|
+
await scanRecursive(fullPath, path.join(relativePath, entry.name));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
await scanRecursive(directory);
|
|
196
|
+
return skills;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Calculate directory size in bytes
|
|
201
|
+
* @param {string} dirPath - Directory path
|
|
202
|
+
* @returns {Promise<number>} Size in bytes
|
|
203
|
+
*/
|
|
204
|
+
async calculateSize(dirPath) {
|
|
205
|
+
let totalSize = 0;
|
|
206
|
+
|
|
207
|
+
async function calcRecursive(dir) {
|
|
208
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
209
|
+
|
|
210
|
+
for (const entry of entries) {
|
|
211
|
+
const fullPath = path.join(dir, entry.name);
|
|
212
|
+
|
|
213
|
+
if (entry.isDirectory()) {
|
|
214
|
+
await calcRecursive(fullPath);
|
|
215
|
+
} else {
|
|
216
|
+
const stats = await fs.stat(fullPath);
|
|
217
|
+
totalSize += stats.size;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
await calcRecursive(dirPath);
|
|
223
|
+
return totalSize;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Install a single skill
|
|
228
|
+
* @param {Object} skill - Skill info from scanSkills
|
|
229
|
+
* @param {boolean} force - Overwrite if exists
|
|
230
|
+
* @returns {Promise<void>}
|
|
231
|
+
*/
|
|
232
|
+
async installSkill(skill, force = false) {
|
|
233
|
+
const targetPath = path.join(this.targetDir, skill.name);
|
|
234
|
+
|
|
235
|
+
// Check if already exists
|
|
236
|
+
try {
|
|
237
|
+
await fs.access(targetPath);
|
|
238
|
+
if (!force) {
|
|
239
|
+
throw new Error(`Skill '${skill.name}' already exists. Use --force to overwrite.`);
|
|
240
|
+
}
|
|
241
|
+
// Remove existing
|
|
242
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
243
|
+
} catch (err) {
|
|
244
|
+
if (err.message.includes('already exists')) {
|
|
245
|
+
throw err;
|
|
246
|
+
}
|
|
247
|
+
// Doesn't exist, that's fine
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Create target directory
|
|
251
|
+
await fs.mkdir(this.targetDir, { recursive: true });
|
|
252
|
+
|
|
253
|
+
// Copy skill directory
|
|
254
|
+
await fs.cp(skill.path, targetPath, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Install skills from GitHub repository with intelligent URL handling
|
|
259
|
+
* @param {string} repoUrl - GitHub URL or shorthand
|
|
260
|
+
* @param {Object} options - Installation options
|
|
261
|
+
* @returns {Promise<Array>} Installed skills
|
|
262
|
+
*/
|
|
263
|
+
async installFromGitHub(repoUrl, options = {}) {
|
|
264
|
+
// 智能解析URL
|
|
265
|
+
const urlInfo = this.parseGitHubUrl(repoUrl);
|
|
266
|
+
const { owner, repo, filePath, branch, isRawFile, format } = urlInfo;
|
|
267
|
+
|
|
268
|
+
// 处理特殊情况
|
|
269
|
+
if (!repo) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`仅提供了GitHub用户/组织名 "${owner}",但未指定仓库。\n\n` +
|
|
272
|
+
`请使用以下格式之一:\n` +
|
|
273
|
+
`• ${owner}/<repo-name> - 安装特定仓库\n` +
|
|
274
|
+
`• ${owner}/<repo-name>/<path> - 安装仓库中的特定文件/目录\n` +
|
|
275
|
+
`\n示例:\n` +
|
|
276
|
+
`• ${owner}/claude-skills\n` +
|
|
277
|
+
`• ${owner}/claude-skills/skills/algorithmic-art.json`
|
|
278
|
+
);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// 处理单个文件的情况
|
|
282
|
+
if (filePath) {
|
|
283
|
+
if (isRawFile) {
|
|
284
|
+
// 处理raw文件URL
|
|
285
|
+
return await this.installFromRawFile(urlInfo, options);
|
|
286
|
+
} else {
|
|
287
|
+
// 处理GitHub文件或blob URL
|
|
288
|
+
return await this.installFromFileOrDirectory(urlInfo, options);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// 默认情况:克隆整个仓库
|
|
293
|
+
return await this.installFromRepository(urlInfo, options);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Install skills by cloning entire repository
|
|
298
|
+
* @param {Object} urlInfo - Parsed URL information
|
|
299
|
+
* @param {Object} options - Installation options
|
|
300
|
+
* @returns {Promise<Array>} Installed skills
|
|
301
|
+
*/
|
|
302
|
+
async installFromRepository(urlInfo, options = {}) {
|
|
303
|
+
const { owner, repo } = urlInfo;
|
|
304
|
+
const fullUrl = `https://github.com/${owner}/${repo}.git`;
|
|
305
|
+
|
|
306
|
+
console.log(`[INFO] Installing skills from ${owner}/${repo}...`);
|
|
307
|
+
|
|
308
|
+
// Create temp directory
|
|
309
|
+
const tempDir = path.join(os.tmpdir(), `stigmergy-skill-install-${Date.now()}`);
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
// Clone repository
|
|
313
|
+
console.log(`[INFO] Cloning repository...`);
|
|
314
|
+
execSync(`git clone --depth 1 ${fullUrl} ${tempDir}`, {
|
|
315
|
+
stdio: options.verbose ? 'inherit' : 'ignore'
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
// Scan for skills
|
|
319
|
+
const skills = await this.scanSkills(tempDir);
|
|
320
|
+
|
|
321
|
+
if (skills.length === 0) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`在仓库 ${owner}/${repo} 中未找到技能。\n\n` +
|
|
324
|
+
`技能目录必须包含 SKILL.md 文件。\n` +
|
|
325
|
+
`请确认仓库中包含有效的技能。`
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
console.log(`[INFO] 找到 ${skills.length} 个技能`);
|
|
330
|
+
|
|
331
|
+
// Install each skill
|
|
332
|
+
const installed = [];
|
|
333
|
+
for (const skill of skills) {
|
|
334
|
+
try {
|
|
335
|
+
await this.installSkill(skill, options.force);
|
|
336
|
+
console.log(`[OK] 已安装: ${skill.name}`);
|
|
337
|
+
installed.push(skill);
|
|
338
|
+
} catch (err) {
|
|
339
|
+
console.error(`[X] 安装失败 ${skill.name}: ${err.message}`);
|
|
340
|
+
if (!options.continueOnError) {
|
|
341
|
+
throw err;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log(`[SUCCESS] 成功安装 ${installed.length}/${skills.length} 个技能`);
|
|
347
|
+
return installed;
|
|
348
|
+
} catch (error) {
|
|
349
|
+
// 提供友好的错误信息
|
|
350
|
+
if (error.message.includes('Command failed')) {
|
|
351
|
+
throw new Error(
|
|
352
|
+
`克隆仓库失败: ${owner}/${repo}\n\n` +
|
|
353
|
+
`可能的原因:\n` +
|
|
354
|
+
`1. 仓库不存在或无权访问\n` +
|
|
355
|
+
`2. Git未安装或不可用\n` +
|
|
356
|
+
`3. 网络连接问题\n\n` +
|
|
357
|
+
`原始错误: ${error.message}`
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
throw error;
|
|
361
|
+
} finally {
|
|
362
|
+
// Cleanup temp directory
|
|
363
|
+
try {
|
|
364
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
365
|
+
} catch {
|
|
366
|
+
// Ignore cleanup errors
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Install from raw file URL
|
|
373
|
+
* @param {Object} urlInfo - Parsed URL information
|
|
374
|
+
* @param {Object} options - Installation options
|
|
375
|
+
* @returns {Promise<Array>} Installed skills
|
|
376
|
+
*/
|
|
377
|
+
async installFromRawFile(urlInfo, options = {}) {
|
|
378
|
+
const { owner, repo, filePath, branch } = urlInfo;
|
|
379
|
+
|
|
380
|
+
console.log(`[INFO] 从 raw 文件安装: ${owner}/${repo}/${branch}/${filePath}`);
|
|
381
|
+
|
|
382
|
+
// 目前仅支持仓库级别的安装
|
|
383
|
+
// 对于单个文件,建议用户使用仓库安装
|
|
384
|
+
throw new Error(
|
|
385
|
+
`单个文件安装功能正在开发中。\n\n` +
|
|
386
|
+
`当前仅支持安装整个技能仓库。\n` +
|
|
387
|
+
`请使用以下格式之一:\n` +
|
|
388
|
+
`• ${owner}/${repo} - 安装整个仓库\n` +
|
|
389
|
+
`• https://github.com/${owner}/${repo} - GitHub仓库URL\n\n` +
|
|
390
|
+
`如果你想安装特定技能,请先确认该技能在仓库的SKILL.md文件中定义。`
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Install from file or directory path within repository
|
|
396
|
+
* @param {Object} urlInfo - Parsed URL information
|
|
397
|
+
* @param {Object} options - Installation options
|
|
398
|
+
* @returns {Promise<Array>} Installed skills
|
|
399
|
+
*/
|
|
400
|
+
async installFromFileOrDirectory(urlInfo, options = {}) {
|
|
401
|
+
const { owner, repo, filePath, branch } = urlInfo;
|
|
402
|
+
|
|
403
|
+
console.log(`[INFO] 从仓库路径安装: ${owner}/${repo}/${branch}/${filePath}`);
|
|
404
|
+
|
|
405
|
+
// 目前仅支持仓库级别的安装
|
|
406
|
+
// 对于特定路径,我们可以克隆仓库后只处理该路径
|
|
407
|
+
// 这里先提供有用的错误信息
|
|
408
|
+
const suggestion = filePath.endsWith('.json') ?
|
|
409
|
+
`你提供的似乎是JSON文件。技能通常以目录形式组织,包含SKILL.md文件。\n` :
|
|
410
|
+
`你提供了路径 "${filePath}"。`;
|
|
411
|
+
|
|
412
|
+
throw new Error(
|
|
413
|
+
`特定路径安装功能正在开发中。\n\n` +
|
|
414
|
+
`${suggestion}\n` +
|
|
415
|
+
`当前建议:\n` +
|
|
416
|
+
`1. 安装整个仓库: stigmergy skill install ${owner}/${repo}\n` +
|
|
417
|
+
`2. 然后查看可用技能: stigmergy skill list\n` +
|
|
418
|
+
`3. 或者直接使用完整仓库URL: stigmergy skill install https://github.com/${owner}/${repo}`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/**
|
|
423
|
+
* Uninstall a skill
|
|
424
|
+
* @param {string} skillName - Name of skill to uninstall
|
|
425
|
+
* @returns {Promise<void>}
|
|
426
|
+
*/
|
|
427
|
+
async uninstallSkill(skillName) {
|
|
428
|
+
const skillPath = path.join(this.targetDir, skillName);
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
await fs.access(skillPath);
|
|
432
|
+
await fs.rm(skillPath, { recursive: true, force: true });
|
|
433
|
+
console.log(`[OK] Uninstalled: ${skillName}`);
|
|
434
|
+
} catch {
|
|
435
|
+
throw new Error(`Skill '${skillName}' not found`);
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|