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