@codingdev/skills 1.0.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/bin/skills.js +39 -0
- package/lib/install.js +190 -0
- package/lib/registry.js +49 -0
- package/lib/utils.js +40 -0
- package/package.json +26 -0
package/bin/skills.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const install = require('../lib/install');
|
|
5
|
+
|
|
6
|
+
program
|
|
7
|
+
.name('skills')
|
|
8
|
+
.description('Qoder Skills CLI - Install and manage AI Coding Workflow skills')
|
|
9
|
+
.version('1.0.0');
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.command('add <source>')
|
|
13
|
+
.description('Install skills from a Git repository URL or local path')
|
|
14
|
+
.option('-s, --skill <name>', 'Install a specific skill by name')
|
|
15
|
+
.option('-g, --global', 'Install to global skills directory (~/.qoder-cn/skills/)', true)
|
|
16
|
+
.option('-l, --local', 'Install to local project (.qoder/skills/)')
|
|
17
|
+
.option('-f, --force', 'Overwrite existing skills')
|
|
18
|
+
.action(async (source, options) => {
|
|
19
|
+
try {
|
|
20
|
+
await install.run(source, options);
|
|
21
|
+
} catch (err) {
|
|
22
|
+
console.error(`❌ Error: ${err.message}`);
|
|
23
|
+
process.exit(1);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
program
|
|
28
|
+
.command('list <source>')
|
|
29
|
+
.description('List available skills from a source without installing')
|
|
30
|
+
.action(async (source) => {
|
|
31
|
+
try {
|
|
32
|
+
await install.list(source);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
console.error(`❌ Error: ${err.message}`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
program.parse();
|
package/lib/install.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core installation logic for skills-cli
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const { execSync } = require('child_process');
|
|
9
|
+
const registry = require('./registry');
|
|
10
|
+
const utils = require('./utils');
|
|
11
|
+
|
|
12
|
+
const TEMP_PREFIX = 'skills-cli-';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Determine target installation directory
|
|
16
|
+
*/
|
|
17
|
+
function getTargetDir(options) {
|
|
18
|
+
if (options.local) {
|
|
19
|
+
const projectRoot = utils.findProjectRoot(process.cwd());
|
|
20
|
+
if (!projectRoot) {
|
|
21
|
+
throw new Error(
|
|
22
|
+
'No .qoder directory found in current project. ' +
|
|
23
|
+
'Run this command inside a project with ai-workspace initialized, ' +
|
|
24
|
+
'or use --global to install globally.'
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
return path.join(projectRoot, '.qoder', 'skills');
|
|
28
|
+
}
|
|
29
|
+
// Default: global
|
|
30
|
+
const homeDir = os.homedir();
|
|
31
|
+
return path.join(homeDir, '.qoder-cn', 'skills');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Download source repository to temp directory
|
|
36
|
+
*/
|
|
37
|
+
async function downloadSource(source, tempDir) {
|
|
38
|
+
if (source.startsWith('http://') || source.startsWith('https://')) {
|
|
39
|
+
console.log(`⬇️ Cloning from ${source} ...`);
|
|
40
|
+
try {
|
|
41
|
+
execSync(`git clone --depth 1 "${source}" "${tempDir}"`, {
|
|
42
|
+
stdio: 'pipe',
|
|
43
|
+
timeout: 120000
|
|
44
|
+
});
|
|
45
|
+
} catch (err) {
|
|
46
|
+
throw new Error(`Failed to clone repository: ${err.message}`);
|
|
47
|
+
}
|
|
48
|
+
} else if (await fs.pathExists(source)) {
|
|
49
|
+
const absSource = path.resolve(source);
|
|
50
|
+
console.log(`📁 Copying from local path: ${absSource}`);
|
|
51
|
+
const entries = await fs.readdir(absSource);
|
|
52
|
+
for (const entry of entries) {
|
|
53
|
+
const src = path.join(absSource, entry);
|
|
54
|
+
const dest = path.join(tempDir, entry);
|
|
55
|
+
await fs.copy(src, dest);
|
|
56
|
+
}
|
|
57
|
+
} else {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Unsupported source: ${source}. ` +
|
|
60
|
+
`Use a Git URL (https://github.com/...) or a local file path.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Main install command handler
|
|
67
|
+
*/
|
|
68
|
+
async function run(source, options) {
|
|
69
|
+
const isGlobal = !options.local;
|
|
70
|
+
const targetDir = getTargetDir(options);
|
|
71
|
+
|
|
72
|
+
console.log(`📦 Qoder Skills CLI v1.0.0`);
|
|
73
|
+
console.log(` Source: ${source}`);
|
|
74
|
+
console.log(` Target: ${targetDir} (${isGlobal ? 'global' : 'local'})`);
|
|
75
|
+
console.log('');
|
|
76
|
+
|
|
77
|
+
// 1. Ensure target directory exists
|
|
78
|
+
await fs.ensureDir(targetDir);
|
|
79
|
+
|
|
80
|
+
// 2. Create temp directory
|
|
81
|
+
const tempDir = path.join(os.tmpdir(), `${TEMP_PREFIX}${Date.now()}`);
|
|
82
|
+
await fs.ensureDir(tempDir);
|
|
83
|
+
|
|
84
|
+
try {
|
|
85
|
+
// 3. Download / clone source
|
|
86
|
+
await downloadSource(source, tempDir);
|
|
87
|
+
|
|
88
|
+
// 4. Load manifest
|
|
89
|
+
const manifest = await registry.loadManifest(tempDir);
|
|
90
|
+
|
|
91
|
+
console.log(`📋 Found ${manifest.skills.length} skill(s) in "${manifest.name}" v${manifest.version}`);
|
|
92
|
+
if (manifest.description) {
|
|
93
|
+
console.log(` ${manifest.description}`);
|
|
94
|
+
}
|
|
95
|
+
console.log('');
|
|
96
|
+
|
|
97
|
+
// 5. Determine skills to install
|
|
98
|
+
const skillsToInstall = options.skill
|
|
99
|
+
? manifest.skills.filter(s => s.name === options.skill)
|
|
100
|
+
: manifest.skills;
|
|
101
|
+
|
|
102
|
+
if (skillsToInstall.length === 0) {
|
|
103
|
+
if (options.skill) {
|
|
104
|
+
const available = manifest.skills.map(s => s.name).join(', ');
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Skill "${options.skill}" not found in manifest. ` +
|
|
107
|
+
`Available: ${available}`
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
throw new Error('No skills found in manifest.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// 6. Install each skill
|
|
114
|
+
let installed = 0;
|
|
115
|
+
let skipped = 0;
|
|
116
|
+
|
|
117
|
+
for (const skill of skillsToInstall) {
|
|
118
|
+
const srcPath = path.join(tempDir, skill.path);
|
|
119
|
+
const destPath = path.join(targetDir, skill.name);
|
|
120
|
+
|
|
121
|
+
// Check source exists
|
|
122
|
+
if (!await fs.pathExists(srcPath)) {
|
|
123
|
+
console.log(`⚠️ Skip: ${skill.name} (source path not found: ${skill.path})`);
|
|
124
|
+
skipped++;
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Check destination exists
|
|
129
|
+
if (await fs.pathExists(destPath)) {
|
|
130
|
+
if (!options.force) {
|
|
131
|
+
console.log(`⏭️ Skip: ${skill.name} (already exists, use --force to overwrite)`);
|
|
132
|
+
skipped++;
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
console.log(`🔄 Overwriting: ${skill.name}`);
|
|
136
|
+
await fs.remove(destPath);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Copy
|
|
140
|
+
await fs.copy(srcPath, destPath);
|
|
141
|
+
console.log(`✅ Installed: ${skill.name}`);
|
|
142
|
+
installed++;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
console.log('');
|
|
146
|
+
console.log(`🎉 Done! ${installed} installed, ${skipped} skipped.`);
|
|
147
|
+
console.log(` Location: ${targetDir}`);
|
|
148
|
+
|
|
149
|
+
} finally {
|
|
150
|
+
// 7. Cleanup temp directory
|
|
151
|
+
await fs.remove(tempDir).catch(() => {});
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* List command handler - preview without installing
|
|
157
|
+
*/
|
|
158
|
+
async function list(source) {
|
|
159
|
+
const tempDir = path.join(os.tmpdir(), `${TEMP_PREFIX}list-${Date.now()}`);
|
|
160
|
+
await fs.ensureDir(tempDir);
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
await downloadSource(source, tempDir);
|
|
164
|
+
const manifest = await registry.loadManifest(tempDir);
|
|
165
|
+
|
|
166
|
+
console.log(`📋 Skills in "${manifest.name}" v${manifest.version}`);
|
|
167
|
+
if (manifest.description) {
|
|
168
|
+
console.log(` ${manifest.description}`);
|
|
169
|
+
}
|
|
170
|
+
console.log('');
|
|
171
|
+
|
|
172
|
+
const maxNameLen = Math.max(...manifest.skills.map(s => s.name.length));
|
|
173
|
+
|
|
174
|
+
manifest.skills.forEach((skill, i) => {
|
|
175
|
+
const paddedName = skill.name.padEnd(maxNameLen);
|
|
176
|
+
console.log(` ${i + 1}. ${paddedName} ${skill.description || ''}`);
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
console.log('');
|
|
180
|
+
console.log(` Total: ${manifest.skills.length} skill(s)`);
|
|
181
|
+
|
|
182
|
+
} finally {
|
|
183
|
+
await fs.remove(tempDir).catch(() => {});
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
module.exports = {
|
|
188
|
+
run,
|
|
189
|
+
list
|
|
190
|
+
};
|
package/lib/registry.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skill manifest registry - parses and validates skills.json
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const MANIFEST_FILE = 'skills.json';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Load and validate skills manifest from a directory
|
|
12
|
+
*/
|
|
13
|
+
async function loadManifest(dir) {
|
|
14
|
+
const manifestPath = path.join(dir, MANIFEST_FILE);
|
|
15
|
+
|
|
16
|
+
if (!await fs.pathExists(manifestPath)) {
|
|
17
|
+
throw new Error(
|
|
18
|
+
`Manifest file not found: ${MANIFEST_FILE}. ` +
|
|
19
|
+
`Make sure the source contains a valid skills.json at its root.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let manifest;
|
|
24
|
+
try {
|
|
25
|
+
manifest = await fs.readJson(manifestPath);
|
|
26
|
+
} catch (err) {
|
|
27
|
+
throw new Error(`Failed to parse ${MANIFEST_FILE}: ${err.message}`);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Basic validation
|
|
31
|
+
if (!manifest.skills || !Array.isArray(manifest.skills)) {
|
|
32
|
+
throw new Error('Invalid manifest: "skills" array is required.');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
manifest.skills.forEach((skill, i) => {
|
|
36
|
+
if (!skill.name) {
|
|
37
|
+
throw new Error(`Invalid manifest: skill #${i} is missing "name".`);
|
|
38
|
+
}
|
|
39
|
+
if (!skill.path) {
|
|
40
|
+
throw new Error(`Invalid manifest: skill "${skill.name}" is missing "path".`);
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return manifest;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
loadManifest
|
|
49
|
+
};
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for skills-cli
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const fs = require('fs-extra');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Check if a path exists and is a directory
|
|
10
|
+
*/
|
|
11
|
+
async function isDirectory(dirPath) {
|
|
12
|
+
try {
|
|
13
|
+
const stat = await fs.stat(dirPath);
|
|
14
|
+
return stat.isDirectory();
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Find project root by looking for .qoder directory
|
|
22
|
+
*/
|
|
23
|
+
async function findProjectRoot(startDir = process.cwd()) {
|
|
24
|
+
let current = path.resolve(startDir);
|
|
25
|
+
const root = path.parse(current).root;
|
|
26
|
+
|
|
27
|
+
while (current !== root) {
|
|
28
|
+
if (await fs.pathExists(path.join(current, '.qoder'))) {
|
|
29
|
+
return current;
|
|
30
|
+
}
|
|
31
|
+
current = path.dirname(current);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
module.exports = {
|
|
38
|
+
isDirectory,
|
|
39
|
+
findProjectRoot
|
|
40
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codingdev/skills",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "AI Coding Workflow Skills CLI - Install and manage workflow skills from Git repositories",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"skills": "bin/skills.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"skills",
|
|
14
|
+
"ai-coding",
|
|
15
|
+
"workflow"
|
|
16
|
+
],
|
|
17
|
+
"author": "",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"dependencies": {
|
|
20
|
+
"commander": "^11.1.0",
|
|
21
|
+
"fs-extra": "^11.2.0"
|
|
22
|
+
},
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=16.0.0"
|
|
25
|
+
}
|
|
26
|
+
}
|