kungeskill 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -0
- package/package.json +31 -0
- package/src/cli.js +129 -0
- package/src/commands/add.js +81 -0
- package/src/commands/doctor.js +115 -0
- package/src/commands/init.js +44 -0
- package/src/commands/list.js +89 -0
- package/src/commands/remove.js +40 -0
- package/src/commands/shared.js +34 -0
- package/src/commands/update.js +37 -0
- package/src/commands/view.js +61 -0
- package/src/core/cache.js +35 -0
- package/src/core/config.js +56 -0
- package/src/core/registry.js +68 -0
- package/src/core/symlink.js +158 -0
- package/src/utils/git.js +33 -0
- package/src/utils/logger.js +85 -0
package/README.md
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# kunge-skills
|
|
2
|
+
|
|
3
|
+
OpenSpec-based development workflow skills for Claude Code.
|
|
4
|
+
|
|
5
|
+
This marketplace provides reusable skills that enable a spec-driven approach to software development: explore ideas, propose changes, implement tasks, and archive results -- plus a knowledge base for code analysis and business logic documentation.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
### Add the marketplace
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
/plugin marketplace add kunge2013/skills
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
### Install individual plugins
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
# Spec-driven change lifecycle (explore, propose, apply, archive)
|
|
19
|
+
/plugin install openspec-workflow@kunge-skills
|
|
20
|
+
|
|
21
|
+
# Code analysis and business logic knowledge base
|
|
22
|
+
/plugin install openspec-trace@kunge-skills
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Update to latest
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
/plugin marketplace update kunge-skills
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Available Plugins
|
|
32
|
+
|
|
33
|
+
### openspec-workflow
|
|
34
|
+
|
|
35
|
+
Spec-driven change lifecycle for Claude Code. Enables you to:
|
|
36
|
+
|
|
37
|
+
- **Explore** ideas and requirements before writing code
|
|
38
|
+
- **Propose** changes with structured proposal, design, and task artifacts
|
|
39
|
+
- **Apply** tasks through sequential, verifiable implementation
|
|
40
|
+
- **Archive** completed changes with spec synchronization
|
|
41
|
+
|
|
42
|
+
| Skill | Description |
|
|
43
|
+
|-------|-------------|
|
|
44
|
+
| `openspec-explore` | Enter explore mode -- a thinking partner for exploring ideas, investigating problems, and clarifying requirements |
|
|
45
|
+
| `openspec-propose` | Propose a new change with all artifacts (proposal, design, tasks) generated in one step |
|
|
46
|
+
| `openspec-apply-change` | Implement tasks from an OpenSpec change, working through them sequentially with progress tracking |
|
|
47
|
+
| `openspec-archive-change` | Archive a completed change, with optional delta spec sync to main specs |
|
|
48
|
+
|
|
49
|
+
### openspec-trace
|
|
50
|
+
|
|
51
|
+
Code analysis and business logic knowledge base. Enables you to:
|
|
52
|
+
|
|
53
|
+
- **Archive** code changes from OpenSpec changes into a versioned knowledge base
|
|
54
|
+
- **Search** archived business logic documents by keyword, domain, or exact module
|
|
55
|
+
|
|
56
|
+
| Skill | Description |
|
|
57
|
+
|-------|-------------|
|
|
58
|
+
| `opst-code-anysic` | Analyze archived OpenSpec change code, extract business logic, and archive into a versioned knowledge base with five-section design documents |
|
|
59
|
+
| `opst-business-search` | Search the archived business logic knowledge base by keyword, browse by domain, or view exact modules |
|
|
60
|
+
|
|
61
|
+
## Tech Stack
|
|
62
|
+
|
|
63
|
+
- OpenSpec (spec-driven development workflow)
|
|
64
|
+
- Claude Code skills framework
|
|
65
|
+
|
|
66
|
+
## License
|
|
67
|
+
|
|
68
|
+
MIT
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "kungeskill",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Manage Claude Code skills via symlinks — install, remove, update skills from a marketplace cache",
|
|
5
|
+
"main": "src/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kungeskill": "src/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"engines": {
|
|
10
|
+
"node": ">=18"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"src/**/*.js"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"start": "node src/cli.js",
|
|
17
|
+
"prepublishOnly": "node src/cli.js --version",
|
|
18
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
19
|
+
},
|
|
20
|
+
"keywords": ["claude-code", "skills", "marketplace", "symlink", "skill-manager"],
|
|
21
|
+
"author": "kungeskills",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/kunge2013/skills"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/kunge2013/skills/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/kunge2013/skills#readme"
|
|
31
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// [AGC:START] tool=Cc author=fangkun
|
|
5
|
+
const { cmdInit } = require('./commands/init');
|
|
6
|
+
const { cmdList } = require('./commands/list');
|
|
7
|
+
const { cmdAdd } = require('./commands/add');
|
|
8
|
+
const { cmdRemove } = require('./commands/remove');
|
|
9
|
+
const { cmdView } = require('./commands/view');
|
|
10
|
+
const { cmdUpdate } = require('./commands/update');
|
|
11
|
+
const { cmdDoctor } = require('./commands/doctor');
|
|
12
|
+
const logger = require('./utils/logger');
|
|
13
|
+
|
|
14
|
+
const VERSION = '0.1.0';
|
|
15
|
+
const USAGE = `
|
|
16
|
+
Usage: kungeskill <command> [options]
|
|
17
|
+
|
|
18
|
+
Commands:
|
|
19
|
+
init Initialize marketplace cache
|
|
20
|
+
list List available skills in the marketplace
|
|
21
|
+
--installed Show only installed skills in this project
|
|
22
|
+
add <skill> Install a skill via symlink
|
|
23
|
+
--force Reinstall even if already installed
|
|
24
|
+
remove <skill> Remove a skill symlink from this project
|
|
25
|
+
view Show installed skills with health status
|
|
26
|
+
update Update marketplace cache via git pull
|
|
27
|
+
doctor Check symlink health and cache status
|
|
28
|
+
|
|
29
|
+
Options:
|
|
30
|
+
--help, -h Show help
|
|
31
|
+
--version, -v Show version
|
|
32
|
+
`;
|
|
33
|
+
|
|
34
|
+
function parseArgs(argv) {
|
|
35
|
+
const args = argv.slice(2); // skip node and script path
|
|
36
|
+
const result = { command: null, positional: [], flags: {} };
|
|
37
|
+
|
|
38
|
+
for (let i = 0; i < args.length; i++) {
|
|
39
|
+
const arg = args[i];
|
|
40
|
+
if (arg === '--help' || arg === '-h') {
|
|
41
|
+
result.flags.help = true;
|
|
42
|
+
} else if (arg === '--version' || arg === '-v') {
|
|
43
|
+
result.flags.version = true;
|
|
44
|
+
} else if (arg === '--installed') {
|
|
45
|
+
result.flags.installed = true;
|
|
46
|
+
} else if (arg === '--force') {
|
|
47
|
+
result.flags.force = true;
|
|
48
|
+
} else if (!result.command) {
|
|
49
|
+
result.command = arg;
|
|
50
|
+
} else {
|
|
51
|
+
result.positional.push(arg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function main() {
|
|
58
|
+
const parsed = parseArgs(process.argv);
|
|
59
|
+
|
|
60
|
+
if (parsed.flags.help) {
|
|
61
|
+
console.log(USAGE.trim());
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (parsed.flags.version) {
|
|
66
|
+
console.log(`kungeskill v${VERSION}`);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!parsed.command) {
|
|
71
|
+
console.log(USAGE.trim());
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
switch (parsed.command) {
|
|
77
|
+
case 'init':
|
|
78
|
+
await cmdInit();
|
|
79
|
+
break;
|
|
80
|
+
|
|
81
|
+
case 'list':
|
|
82
|
+
cmdList({ installed: parsed.flags.installed });
|
|
83
|
+
break;
|
|
84
|
+
|
|
85
|
+
case 'add': {
|
|
86
|
+
const skillName = parsed.positional[0];
|
|
87
|
+
if (!skillName) {
|
|
88
|
+
logger.error('Missing skill name. Usage: kungeskill add <skill>');
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
await cmdAdd(skillName, { force: parsed.flags.force });
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
case 'remove': {
|
|
96
|
+
const skillName = parsed.positional[0];
|
|
97
|
+
if (!skillName) {
|
|
98
|
+
logger.error('Missing skill name. Usage: kungeskill remove <skill>');
|
|
99
|
+
process.exit(1);
|
|
100
|
+
}
|
|
101
|
+
cmdRemove(skillName);
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
case 'view':
|
|
106
|
+
cmdView();
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
case 'update':
|
|
110
|
+
await cmdUpdate();
|
|
111
|
+
break;
|
|
112
|
+
|
|
113
|
+
case 'doctor':
|
|
114
|
+
cmdDoctor();
|
|
115
|
+
break;
|
|
116
|
+
|
|
117
|
+
default:
|
|
118
|
+
logger.error(`Unknown command: ${parsed.command}`);
|
|
119
|
+
console.log(USAGE.trim());
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
logger.error(err.message || String(err));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
main();
|
|
129
|
+
// [AGC:END]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getMarketplaceSourceDir, isCacheValid } = require('../core/cache');
|
|
6
|
+
const { parseMarketplace, findSkill } = require('../core/registry');
|
|
7
|
+
const { createSkillSymlink, canCreateJunction, copyDirRecursive } = require('../core/symlink');
|
|
8
|
+
const { findProjectSkillsDir } = require('./shared');
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
|
|
11
|
+
async function cmdAdd(skillName, opts) {
|
|
12
|
+
const force = opts.force || false;
|
|
13
|
+
|
|
14
|
+
// Ensure cache exists
|
|
15
|
+
if (!isCacheValid()) {
|
|
16
|
+
logger.info('Marketplace cache not initialized. Running init...');
|
|
17
|
+
const { cmdInit } = require('./init');
|
|
18
|
+
await cmdInit();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const cacheDir = getMarketplaceSourceDir();
|
|
22
|
+
const marketplace = parseMarketplace(cacheDir);
|
|
23
|
+
|
|
24
|
+
if (!marketplace) {
|
|
25
|
+
logger.error('Invalid marketplace manifest.');
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// Find the skill in the marketplace
|
|
30
|
+
const skill = findSkill(marketplace, cacheDir, skillName);
|
|
31
|
+
if (!skill) {
|
|
32
|
+
logger.error(`Skill '${skillName}' not found in marketplace.`);
|
|
33
|
+
logger.info('Run "kungeskill list" to see available skills.');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Verify source has SKILL.md
|
|
38
|
+
const skillMdPath = path.join(skill.sourcePath, 'SKILL.md');
|
|
39
|
+
if (!fs.existsSync(skillMdPath)) {
|
|
40
|
+
logger.error(`Skill source directory is missing SKILL.md: ${skill.sourcePath}`);
|
|
41
|
+
process.exit(1);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// Find or create project skills directory
|
|
45
|
+
const skillsDir = findProjectSkillsDir();
|
|
46
|
+
const linkPath = path.join(skillsDir, skillName);
|
|
47
|
+
|
|
48
|
+
// Check if already installed
|
|
49
|
+
if (fs.existsSync(linkPath)) {
|
|
50
|
+
if (force) {
|
|
51
|
+
logger.info(`Skill '${skillName}' already installed. Reinstalling (--force)...`);
|
|
52
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
53
|
+
} else {
|
|
54
|
+
logger.warn(`Skill '${skillName}' is already installed.`);
|
|
55
|
+
logger.info('Use --force to reinstall.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Try symlink first, fallback to copy on cross-drive Windows
|
|
61
|
+
try {
|
|
62
|
+
createSkillSymlink(skill.sourcePath, linkPath);
|
|
63
|
+
logger.success(`Installed ${skillName}`);
|
|
64
|
+
logger.dim(` Link: ${linkPath}`);
|
|
65
|
+
logger.dim(` Source: ${skill.sourcePath} (symlink)`);
|
|
66
|
+
} catch (err) {
|
|
67
|
+
// Fallback: copy directory if symlink fails (cross-drive on Windows)
|
|
68
|
+
if (err.message.includes('across drives')) {
|
|
69
|
+
logger.warn(`Cannot create symlink (cross-drive). Falling back to copy mode.`);
|
|
70
|
+
logger.warn(` Updates will NOT auto-sync. Run "kungeskill add --force ${skillName}" after update.`);
|
|
71
|
+
copyDirRecursive(skill.sourcePath, linkPath);
|
|
72
|
+
logger.success(`Installed ${skillName} (copied)`);
|
|
73
|
+
logger.dim(` Path: ${linkPath}`);
|
|
74
|
+
} else {
|
|
75
|
+
logger.error(`Failed to install '${skillName}': ${err.message}`);
|
|
76
|
+
process.exit(1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = { cmdAdd };
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getConfig } = require('../core/config');
|
|
6
|
+
const { getCacheDir, isCacheValid } = require('../core/cache');
|
|
7
|
+
const { parseMarketplace, listAllSkills } = require('../core/registry');
|
|
8
|
+
const { getSymlinkStatus } = require('../core/symlink');
|
|
9
|
+
const { findProjectSkillsDir } = require('./shared');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
|
|
12
|
+
function cmdDoctor() {
|
|
13
|
+
const config = getConfig();
|
|
14
|
+
const cacheDir = getCacheDir();
|
|
15
|
+
let issues = 0;
|
|
16
|
+
let warnings = 0;
|
|
17
|
+
|
|
18
|
+
logger.header('kungeskill doctor');
|
|
19
|
+
console.log();
|
|
20
|
+
|
|
21
|
+
// Check 1: Cache directory
|
|
22
|
+
logger.header('Cache:');
|
|
23
|
+
if (isCacheValid()) {
|
|
24
|
+
logger.status(' \u2713', `Cache directory: ${cacheDir}`);
|
|
25
|
+
} else {
|
|
26
|
+
logger.status(' \u2717', `Cache directory missing or not a git repo`);
|
|
27
|
+
issues++;
|
|
28
|
+
logger.warn(' Run "kungeskill init" to initialize.');
|
|
29
|
+
}
|
|
30
|
+
console.log();
|
|
31
|
+
|
|
32
|
+
// Check 2: Marketplace manifest
|
|
33
|
+
logger.header('Marketplace:');
|
|
34
|
+
if (isCacheValid()) {
|
|
35
|
+
const marketplace = parseMarketplace(cacheDir);
|
|
36
|
+
if (marketplace && marketplace.plugins) {
|
|
37
|
+
const pluginCount = marketplace.plugins.length;
|
|
38
|
+
const skills = listAllSkills(marketplace, cacheDir);
|
|
39
|
+
logger.status(' \u2713', `Manifest valid (${pluginCount} plugins, ${skills.length} skills)`);
|
|
40
|
+
} else {
|
|
41
|
+
logger.status(' \u2717', 'Invalid marketplace manifest');
|
|
42
|
+
issues++;
|
|
43
|
+
}
|
|
44
|
+
} else {
|
|
45
|
+
logger.status(' \u2717', 'Cannot check manifest (cache not initialized)');
|
|
46
|
+
issues++;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (config.marketplace.lastSync) {
|
|
50
|
+
logger.status(' ', `Last synced: ${new Date(config.marketplace.lastSync).toLocaleString()}`);
|
|
51
|
+
} else {
|
|
52
|
+
logger.status(' ', 'Never synced');
|
|
53
|
+
}
|
|
54
|
+
console.log();
|
|
55
|
+
|
|
56
|
+
// Check 3: Project skills
|
|
57
|
+
logger.header('Project Skills:');
|
|
58
|
+
const skillsDir = findProjectSkillsDir();
|
|
59
|
+
|
|
60
|
+
if (!fs.existsSync(skillsDir)) {
|
|
61
|
+
logger.status(' ', 'No skills installed in this project');
|
|
62
|
+
console.log();
|
|
63
|
+
logger.header('Summary:');
|
|
64
|
+
logger.status(' ', `${issues} issues, ${warnings} warnings`);
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
69
|
+
const skills = entries
|
|
70
|
+
.filter(e => e.isDirectory() || e.isSymbolicLink())
|
|
71
|
+
.map(e => e.name);
|
|
72
|
+
|
|
73
|
+
let okCount = 0;
|
|
74
|
+
let brokenCount = 0;
|
|
75
|
+
|
|
76
|
+
for (const name of skills) {
|
|
77
|
+
const linkPath = path.join(skillsDir, name);
|
|
78
|
+
const status = getSymlinkStatus(linkPath);
|
|
79
|
+
|
|
80
|
+
if (!status.exists) {
|
|
81
|
+
logger.status(' \u2717', `${name}: BROKEN (target not found)`);
|
|
82
|
+
brokenCount++;
|
|
83
|
+
issues++;
|
|
84
|
+
} else if (!status.isLink) {
|
|
85
|
+
logger.status(' \u26a0', `${name}: not a symlink (may be a copied directory)`);
|
|
86
|
+
warnings++;
|
|
87
|
+
} else if (status.isValid && status.hasSkillMd) {
|
|
88
|
+
okCount++;
|
|
89
|
+
} else {
|
|
90
|
+
logger.status(' \u26a0', `${name}: SKILL.md missing`);
|
|
91
|
+
warnings++;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (skills.length > 0 && brokenCount === 0) {
|
|
96
|
+
logger.status(' \u2713', `${okCount} skills installed, all valid`);
|
|
97
|
+
}
|
|
98
|
+
console.log();
|
|
99
|
+
|
|
100
|
+
// Summary
|
|
101
|
+
logger.header('Summary:');
|
|
102
|
+
logger.status(' ', `${skills.length} total, ${okCount} ok, ${brokenCount} broken, ${warnings} warnings, ${issues} issues`);
|
|
103
|
+
|
|
104
|
+
if (issues > 0 || warnings > 0) {
|
|
105
|
+
console.log();
|
|
106
|
+
if (brokenCount > 0) {
|
|
107
|
+
logger.info('Run "kungeskill remove <name>" to clean broken links');
|
|
108
|
+
}
|
|
109
|
+
if (!isCacheValid()) {
|
|
110
|
+
logger.info('Run "kungeskill init" to initialize the marketplace cache');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = { cmdDoctor };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const { getConfig, saveConfig } = require('../core/config');
|
|
5
|
+
const { getCacheDir, ensureCacheDir, isCacheValid } = require('../core/cache');
|
|
6
|
+
const { cloneRepo } = require('../utils/git');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
|
|
9
|
+
async function cmdInit() {
|
|
10
|
+
const config = getConfig();
|
|
11
|
+
|
|
12
|
+
if (isCacheValid()) {
|
|
13
|
+
logger.success('Marketplace cache already initialized');
|
|
14
|
+
logger.dim(` Location: ${getCacheDir()}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const { url, branch } = config.marketplace;
|
|
19
|
+
logger.info(`Cloning marketplace from ${url} (${branch})...`);
|
|
20
|
+
|
|
21
|
+
ensureCacheDir();
|
|
22
|
+
|
|
23
|
+
try {
|
|
24
|
+
await cloneRepo(url, getCacheDir(), branch);
|
|
25
|
+
|
|
26
|
+
config.marketplace.cloned = true;
|
|
27
|
+
config.marketplace.lastSync = new Date().toISOString();
|
|
28
|
+
saveConfig(config);
|
|
29
|
+
|
|
30
|
+
logger.success('Marketplace cached');
|
|
31
|
+
logger.dim(` Location: ${getCacheDir()}`);
|
|
32
|
+
} catch (err) {
|
|
33
|
+
// Clean up on failure
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
const cacheDir = getCacheDir();
|
|
36
|
+
if (fs.existsSync(cacheDir)) {
|
|
37
|
+
fs.rmSync(cacheDir, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
logger.error(`Failed to clone marketplace: ${err.message}`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { cmdInit };
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getMarketplaceSourceDir, isCacheValid } = require('../core/cache');
|
|
6
|
+
const { parseMarketplace, listAllSkills } = require('../core/registry');
|
|
7
|
+
const { findProjectSkillsDir } = require('./shared');
|
|
8
|
+
const logger = require('../utils/logger');
|
|
9
|
+
|
|
10
|
+
function cmdList(opts) {
|
|
11
|
+
if (!isCacheValid()) {
|
|
12
|
+
logger.error('Marketplace cache not initialized. Run "kungeskill init" first.');
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const cacheDir = getMarketplaceSourceDir();
|
|
17
|
+
const marketplace = parseMarketplace(cacheDir);
|
|
18
|
+
|
|
19
|
+
if (!marketplace) {
|
|
20
|
+
logger.error('Invalid marketplace manifest. Run "kungeskill init" to reinitialize.');
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const showInstalled = opts.installed;
|
|
25
|
+
|
|
26
|
+
if (showInstalled) {
|
|
27
|
+
// Show only installed skills
|
|
28
|
+
const skillsDir = findProjectSkillsDir();
|
|
29
|
+
if (!fs.existsSync(skillsDir)) {
|
|
30
|
+
logger.info('No skills installed in this project.');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const installed = fs.readdirSync(skillsDir, { withFileTypes: true })
|
|
35
|
+
.filter(e => e.isDirectory() || e.isSymbolicLink())
|
|
36
|
+
.map(e => e.name);
|
|
37
|
+
|
|
38
|
+
if (installed.length === 0) {
|
|
39
|
+
logger.info('No skills installed in this project.');
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
logger.header('Installed skills in this project:');
|
|
44
|
+
console.log();
|
|
45
|
+
for (const name of installed) {
|
|
46
|
+
console.log(` ${name}`);
|
|
47
|
+
}
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Show all available skills grouped by plugin
|
|
52
|
+
const skills = listAllSkills(marketplace, cacheDir);
|
|
53
|
+
|
|
54
|
+
if (skills.length === 0) {
|
|
55
|
+
logger.info('No skills found in marketplace.');
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Group by plugin
|
|
60
|
+
const grouped = {};
|
|
61
|
+
for (const skill of skills) {
|
|
62
|
+
if (!grouped[skill.pluginName]) {
|
|
63
|
+
grouped[skill.pluginName] = [];
|
|
64
|
+
}
|
|
65
|
+
grouped[skill.pluginName].push(skill.skillName);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
logger.header('Available skills:');
|
|
69
|
+
console.log();
|
|
70
|
+
|
|
71
|
+
const headers = ['PLUGIN', 'SKILLS'];
|
|
72
|
+
const rows = [];
|
|
73
|
+
|
|
74
|
+
let first = true;
|
|
75
|
+
for (const [plugin, skillNames] of Object.entries(grouped)) {
|
|
76
|
+
if (!first) {
|
|
77
|
+
rows.push(['', '']);
|
|
78
|
+
}
|
|
79
|
+
rows.push([plugin, skillNames[0]]);
|
|
80
|
+
for (let i = 1; i < skillNames.length; i++) {
|
|
81
|
+
rows.push(['', skillNames[i]]);
|
|
82
|
+
}
|
|
83
|
+
first = false;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
logger.table(headers, rows);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = { cmdList };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { removeSkillSymlink } = require('../core/symlink');
|
|
6
|
+
const { findProjectSkillsDir } = require('./shared');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
|
|
9
|
+
function cmdRemove(skillName) {
|
|
10
|
+
const skillsDir = findProjectSkillsDir();
|
|
11
|
+
const linkPath = path.join(skillsDir, skillName);
|
|
12
|
+
|
|
13
|
+
if (!fs.existsSync(linkPath)) {
|
|
14
|
+
logger.error(`Skill '${skillName}' is not installed in this project.`);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// Check if it's a symlink
|
|
19
|
+
const stat = fs.lstatSync(linkPath);
|
|
20
|
+
if (stat.isSymbolicLink()) {
|
|
21
|
+
// Safe to remove — it's a symlink
|
|
22
|
+
try {
|
|
23
|
+
removeSkillSymlink(linkPath);
|
|
24
|
+
logger.success(`Removed ${skillName}`);
|
|
25
|
+
} catch (err) {
|
|
26
|
+
logger.error(`Failed to remove '${skillName}': ${err.message}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
} else if (stat.isDirectory()) {
|
|
30
|
+
// Not a symlink — could be a copied directory. Remove with warning.
|
|
31
|
+
logger.warn(`'${skillName}' is not a symlink (copied directory). Removing anyway.`);
|
|
32
|
+
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
33
|
+
logger.success(`Removed ${skillName}`);
|
|
34
|
+
} else {
|
|
35
|
+
logger.error(`'${skillName}' is an unexpected file type, refusing to remove.`);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
module.exports = { cmdRemove };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Find the project's .claude/skills/ directory.
|
|
8
|
+
* Walks up from cwd until it finds one, or creates it at cwd.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} [cwd] - Starting directory (defaults to process.cwd())
|
|
11
|
+
* @returns {string} Absolute path to .claude/skills/
|
|
12
|
+
*/
|
|
13
|
+
function findProjectSkillsDir(cwd) {
|
|
14
|
+
cwd = cwd || process.cwd();
|
|
15
|
+
let dir = path.resolve(cwd);
|
|
16
|
+
const root = path.parse(dir).root;
|
|
17
|
+
|
|
18
|
+
while (dir !== root) {
|
|
19
|
+
const skillsDir = path.join(dir, '.claude', 'skills');
|
|
20
|
+
if (fs.existsSync(skillsDir)) {
|
|
21
|
+
return skillsDir;
|
|
22
|
+
}
|
|
23
|
+
dir = path.dirname(dir);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Not found — create at current working directory
|
|
27
|
+
const skillsDir = path.join(cwd, '.claude', 'skills');
|
|
28
|
+
fs.mkdirSync(skillsDir, { recursive: true });
|
|
29
|
+
return skillsDir;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module.exports = {
|
|
33
|
+
findProjectSkillsDir
|
|
34
|
+
};
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { getConfig, saveConfig } = require('../core/config');
|
|
4
|
+
const { getCacheDir, isCacheValid } = require('../core/cache');
|
|
5
|
+
const { pullRepo } = require('../utils/git');
|
|
6
|
+
const logger = require('../utils/logger');
|
|
7
|
+
|
|
8
|
+
async function cmdUpdate() {
|
|
9
|
+
if (!isCacheValid()) {
|
|
10
|
+
logger.error('Marketplace cache not initialized. Run "kungeskill init" first.');
|
|
11
|
+
process.exit(1);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const config = getConfig();
|
|
15
|
+
logger.info('Updating marketplace cache...');
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const result = await pullRepo(getCacheDir());
|
|
19
|
+
|
|
20
|
+
config.marketplace.lastSync = new Date().toISOString();
|
|
21
|
+
saveConfig(config);
|
|
22
|
+
|
|
23
|
+
// Parse output for summary
|
|
24
|
+
const output = (result.stdout || '').trim();
|
|
25
|
+
if (output) {
|
|
26
|
+
logger.dim(` ${output}`);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
logger.success('Marketplace updated');
|
|
30
|
+
logger.info('Symlinks automatically point to updated content. No reinstallation needed.');
|
|
31
|
+
} catch (err) {
|
|
32
|
+
logger.error(`Failed to update marketplace: ${err.message}`);
|
|
33
|
+
process.exit(1);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = { cmdUpdate };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getSymlinkStatus } = require('../core/symlink');
|
|
6
|
+
const { findProjectSkillsDir } = require('./shared');
|
|
7
|
+
const logger = require('../utils/logger');
|
|
8
|
+
|
|
9
|
+
function cmdView() {
|
|
10
|
+
const skillsDir = findProjectSkillsDir();
|
|
11
|
+
|
|
12
|
+
if (!fs.existsSync(skillsDir)) {
|
|
13
|
+
logger.info('No skills installed in this project.');
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
18
|
+
const skills = entries
|
|
19
|
+
.filter(e => e.isDirectory() || e.isSymbolicLink())
|
|
20
|
+
.map(e => e.name);
|
|
21
|
+
|
|
22
|
+
if (skills.length === 0) {
|
|
23
|
+
logger.info('No skills installed in this project.');
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
logger.header(`Skills installed in this project:`);
|
|
28
|
+
console.log();
|
|
29
|
+
|
|
30
|
+
const headers = ['NAME', 'STATUS', 'TARGET'];
|
|
31
|
+
const rows = [];
|
|
32
|
+
|
|
33
|
+
for (const name of skills) {
|
|
34
|
+
const linkPath = path.join(skillsDir, name);
|
|
35
|
+
const status = getSymlinkStatus(linkPath);
|
|
36
|
+
|
|
37
|
+
let statusIcon;
|
|
38
|
+
if (!status.exists) {
|
|
39
|
+
statusIcon = '\u2717 broken'; // ✗
|
|
40
|
+
} else if (status.isValid && status.hasSkillMd) {
|
|
41
|
+
statusIcon = '\u2713 ok'; // ✓
|
|
42
|
+
} else if (status.isValid) {
|
|
43
|
+
statusIcon = '\u26a0 no SKILL.md'; // ⚠
|
|
44
|
+
} else {
|
|
45
|
+
statusIcon = '? unknown';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const target = status.target || '(not found)';
|
|
49
|
+
// Show relative path for readability
|
|
50
|
+
const homeDir = require('os').homedir();
|
|
51
|
+
const displayTarget = target.startsWith(homeDir)
|
|
52
|
+
? target.replace(homeDir, '~')
|
|
53
|
+
: target;
|
|
54
|
+
|
|
55
|
+
rows.push([name, statusIcon, displayTarget]);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logger.table(headers, rows);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { cmdView };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const { getKungeskillsDir, getConfig, saveConfig } = require('./config');
|
|
6
|
+
|
|
7
|
+
function getCacheDir() {
|
|
8
|
+
return path.join(getKungeskillsDir(), 'cache', 'marketplace');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function ensureCacheDir() {
|
|
12
|
+
const cacheDir = getCacheDir();
|
|
13
|
+
if (!fs.existsSync(cacheDir)) {
|
|
14
|
+
fs.mkdirSync(cacheDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
return cacheDir;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function isCacheValid() {
|
|
20
|
+
const cacheDir = getCacheDir();
|
|
21
|
+
const gitDir = path.join(cacheDir, '.git');
|
|
22
|
+
return fs.existsSync(gitDir);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getMarketplaceSourceDir() {
|
|
26
|
+
// The root of the cloned marketplace within the cache
|
|
27
|
+
return getCacheDir();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
getCacheDir,
|
|
32
|
+
ensureCacheDir,
|
|
33
|
+
isCacheValid,
|
|
34
|
+
getMarketplaceSourceDir
|
|
35
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
|
|
7
|
+
const KUNGESKILLS_DIR = path.join(os.homedir(), '.kungeskills');
|
|
8
|
+
const CONFIG_PATH = path.join(KUNGESKILLS_DIR, 'config.json');
|
|
9
|
+
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
marketplace: {
|
|
12
|
+
url: 'https://github.com/kunge2013/skills',
|
|
13
|
+
branch: 'main',
|
|
14
|
+
cloned: false,
|
|
15
|
+
lastSync: null
|
|
16
|
+
}
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function getKungeskillsDir() {
|
|
20
|
+
return KUNGESKILLS_DIR;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function loadConfig() {
|
|
24
|
+
if (!fs.existsSync(CONFIG_PATH)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
const raw = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
28
|
+
return JSON.parse(raw);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function saveConfig(config) {
|
|
32
|
+
if (!fs.existsSync(KUNGESKILLS_DIR)) {
|
|
33
|
+
fs.mkdirSync(KUNGESKILLS_DIR, { recursive: true });
|
|
34
|
+
}
|
|
35
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function getConfig() {
|
|
39
|
+
const existing = loadConfig();
|
|
40
|
+
if (existing) {
|
|
41
|
+
// Merge with defaults to ensure new fields exist
|
|
42
|
+
return {
|
|
43
|
+
marketplace: { ...DEFAULT_CONFIG.marketplace, ...existing.marketplace }
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return { ...DEFAULT_CONFIG };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = {
|
|
50
|
+
getKungeskillsDir,
|
|
51
|
+
loadConfig,
|
|
52
|
+
saveConfig,
|
|
53
|
+
getConfig,
|
|
54
|
+
CONFIG_PATH,
|
|
55
|
+
KUNGESKILLS_DIR
|
|
56
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Parse marketplace.json from the cache directory.
|
|
8
|
+
* @param {string} cacheDir - Path to the marketplace cache root
|
|
9
|
+
* @returns {object|null} Parsed marketplace data or null
|
|
10
|
+
*/
|
|
11
|
+
function parseMarketplace(cacheDir) {
|
|
12
|
+
const manifestPath = path.join(cacheDir, '.claude-plugin', 'marketplace.json');
|
|
13
|
+
if (!fs.existsSync(manifestPath)) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const raw = fs.readFileSync(manifestPath, 'utf-8');
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* List all skills from a parsed marketplace manifest.
|
|
22
|
+
* @param {object} marketplace - Parsed marketplace.json
|
|
23
|
+
* @param {string} cacheDir - Path to the marketplace cache root (for resolving source paths)
|
|
24
|
+
* @returns {Array<{skillName: string, pluginName: string, sourcePath: string}>}
|
|
25
|
+
*/
|
|
26
|
+
function listAllSkills(marketplace, cacheDir) {
|
|
27
|
+
if (!marketplace || !marketplace.plugins) return [];
|
|
28
|
+
|
|
29
|
+
const skills = [];
|
|
30
|
+
for (const plugin of marketplace.plugins) {
|
|
31
|
+
const skillsDir = path.resolve(cacheDir, plugin.source, 'skills');
|
|
32
|
+
if (!fs.existsSync(skillsDir)) continue;
|
|
33
|
+
|
|
34
|
+
const entries = fs.readdirSync(skillsDir, { withFileTypes: true });
|
|
35
|
+
for (const entry of entries) {
|
|
36
|
+
if (entry.isDirectory()) {
|
|
37
|
+
const skillPath = path.join(skillsDir, entry.name);
|
|
38
|
+
// Verify it has SKILL.md
|
|
39
|
+
if (fs.existsSync(path.join(skillPath, 'SKILL.md'))) {
|
|
40
|
+
skills.push({
|
|
41
|
+
skillName: entry.name,
|
|
42
|
+
pluginName: plugin.name,
|
|
43
|
+
sourcePath: skillPath
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return skills;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Find a specific skill by name.
|
|
54
|
+
* @param {object} marketplace - Parsed marketplace.json
|
|
55
|
+
* @param {string} cacheDir - Path to the marketplace cache root
|
|
56
|
+
* @param {string} skillName - Skill name to find
|
|
57
|
+
* @returns {object|null} { skillName, pluginName, sourcePath } or null
|
|
58
|
+
*/
|
|
59
|
+
function findSkill(marketplace, cacheDir, skillName) {
|
|
60
|
+
const skills = listAllSkills(marketplace, cacheDir);
|
|
61
|
+
return skills.find(s => s.skillName === skillName) || null;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
parseMarketplace,
|
|
66
|
+
listAllSkills,
|
|
67
|
+
findSkill
|
|
68
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Check if target and link are on the same drive (Windows only).
|
|
8
|
+
* @param {string} targetPath
|
|
9
|
+
* @param {string} linkPath
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
function canCreateJunction(targetPath, linkPath) {
|
|
13
|
+
if (process.platform !== 'win32') return true;
|
|
14
|
+
return path.parse(path.resolve(targetPath)).root === path.parse(path.resolve(linkPath)).root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Create a cross-platform symlink for a skill.
|
|
19
|
+
* On Windows: uses junction points (no admin required).
|
|
20
|
+
* On Linux/macOS: uses standard directory symlinks.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} targetAbsPath - Absolute path to the skill source directory
|
|
23
|
+
* @param {string} linkAbsPath - Absolute path for the symlink
|
|
24
|
+
* @returns {boolean} True on success
|
|
25
|
+
* @throws {Error} On failure
|
|
26
|
+
*/
|
|
27
|
+
function createSkillSymlink(targetAbsPath, linkAbsPath) {
|
|
28
|
+
// Ensure target exists
|
|
29
|
+
if (!fs.existsSync(targetAbsPath)) {
|
|
30
|
+
throw new Error(`Target does not exist: ${targetAbsPath}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Windows cross-drive check
|
|
34
|
+
if (process.platform === 'win32' && !canCreateJunction(targetAbsPath, linkAbsPath)) {
|
|
35
|
+
throw new Error(
|
|
36
|
+
`Cannot create junction across drives: ${path.parse(targetAbsPath).root} -> ${path.parse(linkAbsPath).root}. ` +
|
|
37
|
+
'Move the cache to the same drive as the project, or use a different approach.'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const opts = process.platform === 'win32' ? 'junction' : 'dir';
|
|
42
|
+
fs.symlinkSync(path.resolve(targetAbsPath), linkAbsPath, opts);
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Remove a skill symlink safely (does not touch the source).
|
|
48
|
+
* @param {string} linkAbsPath
|
|
49
|
+
*/
|
|
50
|
+
function removeSkillSymlink(linkAbsPath) {
|
|
51
|
+
if (!fs.existsSync(linkAbsPath)) {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
// lstat to check if it's a symlink itself
|
|
55
|
+
const stat = fs.lstatSync(linkAbsPath);
|
|
56
|
+
if (stat.isSymbolicLink()) {
|
|
57
|
+
fs.unlinkSync(linkAbsPath);
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
// Not a symlink — don't delete a real directory
|
|
61
|
+
throw new Error(`Not a symlink, refusing to delete: ${linkAbsPath}`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Check if a path is a valid symlink (target exists).
|
|
66
|
+
* @param {string} linkAbsPath
|
|
67
|
+
* @returns {boolean}
|
|
68
|
+
*/
|
|
69
|
+
function isSymlinkValid(linkAbsPath) {
|
|
70
|
+
if (!fs.existsSync(linkAbsPath)) return false;
|
|
71
|
+
const stat = fs.lstatSync(linkAbsPath);
|
|
72
|
+
if (!stat.isSymbolicLink()) return false;
|
|
73
|
+
return fs.existsSync(linkAbsPath); // follows the link
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get detailed status of a skill symlink.
|
|
78
|
+
* @param {string} linkAbsPath
|
|
79
|
+
* @returns {{isLink: boolean, isValid: boolean, target: string|null, exists: boolean, hasSkillMd: boolean}}
|
|
80
|
+
*/
|
|
81
|
+
function getSymlinkStatus(linkAbsPath) {
|
|
82
|
+
const result = {
|
|
83
|
+
isLink: false,
|
|
84
|
+
isValid: false,
|
|
85
|
+
target: null,
|
|
86
|
+
exists: false,
|
|
87
|
+
hasSkillMd: false
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (!fs.existsSync(linkAbsPath) && !fs.lstatSync(linkAbsPath)) {
|
|
91
|
+
return result;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
let stat;
|
|
95
|
+
try {
|
|
96
|
+
stat = fs.lstatSync(linkAbsPath);
|
|
97
|
+
} catch {
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
result.isLink = stat.isSymbolicLink();
|
|
102
|
+
|
|
103
|
+
if (!result.isLink) {
|
|
104
|
+
// It exists but isn't a symlink
|
|
105
|
+
result.exists = true;
|
|
106
|
+
result.isValid = true;
|
|
107
|
+
result.target = linkAbsPath;
|
|
108
|
+
result.hasSkillMd = fs.existsSync(path.join(linkAbsPath, 'SKILL.md'));
|
|
109
|
+
return result;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// It's a symlink
|
|
113
|
+
try {
|
|
114
|
+
result.target = fs.readlinkSync(linkAbsPath);
|
|
115
|
+
} catch {
|
|
116
|
+
return result;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if target exists (follows link)
|
|
120
|
+
result.exists = fs.existsSync(linkAbsPath);
|
|
121
|
+
|
|
122
|
+
if (result.exists) {
|
|
123
|
+
result.isValid = true;
|
|
124
|
+
result.hasSkillMd = fs.existsSync(path.join(linkAbsPath, 'SKILL.md'));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Copy a directory recursively (fallback for cross-drive Windows scenario).
|
|
132
|
+
* @param {string} src
|
|
133
|
+
* @param {string} dest
|
|
134
|
+
*/
|
|
135
|
+
function copyDirRecursive(src, dest) {
|
|
136
|
+
if (!fs.existsSync(dest)) {
|
|
137
|
+
fs.mkdirSync(dest, { recursive: true });
|
|
138
|
+
}
|
|
139
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const srcPath = path.join(src, entry.name);
|
|
142
|
+
const destPath = path.join(dest, entry.name);
|
|
143
|
+
if (entry.isDirectory()) {
|
|
144
|
+
copyDirRecursive(srcPath, destPath);
|
|
145
|
+
} else {
|
|
146
|
+
fs.copyFileSync(srcPath, destPath);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
createSkillSymlink,
|
|
153
|
+
removeSkillSymlink,
|
|
154
|
+
isSymlinkValid,
|
|
155
|
+
getSymlinkStatus,
|
|
156
|
+
canCreateJunction,
|
|
157
|
+
copyDirRecursive
|
|
158
|
+
};
|
package/src/utils/git.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { exec } = require('child_process');
|
|
4
|
+
const { promisify } = require('util');
|
|
5
|
+
|
|
6
|
+
const execAsync = promisify(exec);
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Clone a git repository to a target path.
|
|
10
|
+
* @param {string} url - Repository URL
|
|
11
|
+
* @param {string} targetPath - Destination directory
|
|
12
|
+
* @param {string} branch - Branch to clone
|
|
13
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
14
|
+
*/
|
|
15
|
+
async function cloneRepo(url, targetPath, branch = 'main') {
|
|
16
|
+
const cmd = `git clone --branch "${branch}" "${url}" "${targetPath}"`;
|
|
17
|
+
return execAsync(cmd);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pull the latest changes in a git repository.
|
|
22
|
+
* @param {string} cwd - Working directory (repo root)
|
|
23
|
+
* @returns {Promise<{stdout: string, stderr: string}>}
|
|
24
|
+
*/
|
|
25
|
+
async function pullRepo(cwd) {
|
|
26
|
+
const cmd = 'git pull';
|
|
27
|
+
return execAsync(cmd, { cwd });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = {
|
|
31
|
+
cloneRepo,
|
|
32
|
+
pullRepo
|
|
33
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ANSI color codes
|
|
4
|
+
const COLORS = {
|
|
5
|
+
reset: '\x1b[0m',
|
|
6
|
+
green: '\x1b[32m',
|
|
7
|
+
red: '\x1b[31m',
|
|
8
|
+
yellow: '\x1b[33m',
|
|
9
|
+
cyan: '\x1b[36m',
|
|
10
|
+
white: '\x1b[37m',
|
|
11
|
+
gray: '\x1b[90m',
|
|
12
|
+
bold: '\x1b[1m'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
function _colorize(color, msg) {
|
|
16
|
+
return `${COLORS[color]}${msg}${COLORS.reset}`;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function info(msg) {
|
|
20
|
+
console.log(_colorize('white', msg));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function success(msg) {
|
|
24
|
+
console.log(_colorize('green', msg));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function error(msg) {
|
|
28
|
+
console.error(_colorize('red', msg));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function warn(msg) {
|
|
32
|
+
console.log(_colorize('yellow', msg));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function status(icon, msg) {
|
|
36
|
+
console.log(`${icon} ${msg}`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function dim(msg) {
|
|
40
|
+
console.log(_colorize('gray', msg));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function header(msg) {
|
|
44
|
+
console.log(_colorize('bold', msg));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function table(headers, rows) {
|
|
48
|
+
// Calculate column widths
|
|
49
|
+
const widths = headers.map((h, i) => {
|
|
50
|
+
const maxInCol = Math.max(
|
|
51
|
+
h.length,
|
|
52
|
+
...rows.map(r => (r[i] || '').length)
|
|
53
|
+
);
|
|
54
|
+
return maxInCol;
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
// Print headers
|
|
58
|
+
const headerLine = headers
|
|
59
|
+
.map((h, i) => h.padEnd(widths[i]))
|
|
60
|
+
.join(' ');
|
|
61
|
+
console.log(_colorize('bold', headerLine));
|
|
62
|
+
|
|
63
|
+
// Print separator
|
|
64
|
+
const sepLine = widths.map(w => '─'.repeat(w)).join(' ');
|
|
65
|
+
console.log(_colorize('gray', sepLine));
|
|
66
|
+
|
|
67
|
+
// Print rows
|
|
68
|
+
for (const row of rows) {
|
|
69
|
+
const line = row
|
|
70
|
+
.map((cell, i) => (cell || '').padEnd(widths[i]))
|
|
71
|
+
.join(' ');
|
|
72
|
+
console.log(line);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
info,
|
|
78
|
+
success,
|
|
79
|
+
error,
|
|
80
|
+
warn,
|
|
81
|
+
status,
|
|
82
|
+
dim,
|
|
83
|
+
header,
|
|
84
|
+
table
|
|
85
|
+
};
|