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 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
+ };
@@ -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
+ };