universal-dev-standards 3.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/LICENSE +21 -0
- package/README.md +248 -0
- package/bin/uds.js +56 -0
- package/package.json +63 -0
- package/src/commands/check.js +149 -0
- package/src/commands/configure.js +221 -0
- package/src/commands/init.js +665 -0
- package/src/commands/list.js +100 -0
- package/src/commands/update.js +186 -0
- package/src/index.js +7 -0
- package/src/prompts/init.js +702 -0
- package/src/prompts/integrations.js +453 -0
- package/src/utils/copier.js +143 -0
- package/src/utils/detector.js +159 -0
- package/src/utils/github.js +508 -0
- package/src/utils/integration-generator.js +1694 -0
- package/src/utils/registry.js +207 -0
- package/standards-registry.json +658 -0
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Detect the programming language of the project
|
|
6
|
+
* @param {string} projectPath - Path to the project
|
|
7
|
+
* @returns {Object} Detected languages
|
|
8
|
+
*/
|
|
9
|
+
export function detectLanguage(projectPath) {
|
|
10
|
+
const detected = {
|
|
11
|
+
csharp: false,
|
|
12
|
+
php: false,
|
|
13
|
+
typescript: false,
|
|
14
|
+
javascript: false,
|
|
15
|
+
python: false
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// Check for C# project files
|
|
19
|
+
if (existsSync(join(projectPath, '*.csproj')) ||
|
|
20
|
+
existsSync(join(projectPath, '*.sln')) ||
|
|
21
|
+
hasFileWithExtension(projectPath, '.csproj') ||
|
|
22
|
+
hasFileWithExtension(projectPath, '.cs')) {
|
|
23
|
+
detected.csharp = true;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for PHP
|
|
27
|
+
if (existsSync(join(projectPath, 'composer.json')) ||
|
|
28
|
+
hasFileWithExtension(projectPath, '.php')) {
|
|
29
|
+
detected.php = true;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Check for TypeScript
|
|
33
|
+
if (existsSync(join(projectPath, 'tsconfig.json')) ||
|
|
34
|
+
hasFileWithExtension(projectPath, '.ts') ||
|
|
35
|
+
hasFileWithExtension(projectPath, '.tsx')) {
|
|
36
|
+
detected.typescript = true;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Check for JavaScript
|
|
40
|
+
if (existsSync(join(projectPath, 'package.json')) ||
|
|
41
|
+
hasFileWithExtension(projectPath, '.js') ||
|
|
42
|
+
hasFileWithExtension(projectPath, '.jsx')) {
|
|
43
|
+
detected.javascript = true;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Check for Python
|
|
47
|
+
if (existsSync(join(projectPath, 'requirements.txt')) ||
|
|
48
|
+
existsSync(join(projectPath, 'setup.py')) ||
|
|
49
|
+
existsSync(join(projectPath, 'pyproject.toml')) ||
|
|
50
|
+
hasFileWithExtension(projectPath, '.py')) {
|
|
51
|
+
detected.python = true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return detected;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Detect the framework used in the project
|
|
59
|
+
* @param {string} projectPath - Path to the project
|
|
60
|
+
* @returns {Object} Detected frameworks
|
|
61
|
+
*/
|
|
62
|
+
export function detectFramework(projectPath) {
|
|
63
|
+
const detected = {
|
|
64
|
+
'fat-free': false,
|
|
65
|
+
react: false,
|
|
66
|
+
vue: false,
|
|
67
|
+
angular: false,
|
|
68
|
+
dotnet: false
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
// Check for Fat-Free Framework (PHP)
|
|
72
|
+
const composerPath = join(projectPath, 'composer.json');
|
|
73
|
+
if (existsSync(composerPath)) {
|
|
74
|
+
try {
|
|
75
|
+
const composer = JSON.parse(readFileSync(composerPath, 'utf-8'));
|
|
76
|
+
const deps = { ...composer.require, ...composer['require-dev'] };
|
|
77
|
+
if (deps['bcosca/fatfree'] || deps['bcosca/fatfree-core']) {
|
|
78
|
+
detected['fat-free'] = true;
|
|
79
|
+
}
|
|
80
|
+
} catch {
|
|
81
|
+
// Ignore parse errors
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Check for React/Vue/Angular
|
|
86
|
+
const packagePath = join(projectPath, 'package.json');
|
|
87
|
+
if (existsSync(packagePath)) {
|
|
88
|
+
try {
|
|
89
|
+
const pkg = JSON.parse(readFileSync(packagePath, 'utf-8'));
|
|
90
|
+
const deps = { ...pkg.dependencies, ...pkg.devDependencies };
|
|
91
|
+
if (deps['react'] || deps['react-dom']) {
|
|
92
|
+
detected.react = true;
|
|
93
|
+
}
|
|
94
|
+
if (deps['vue']) {
|
|
95
|
+
detected.vue = true;
|
|
96
|
+
}
|
|
97
|
+
if (deps['@angular/core']) {
|
|
98
|
+
detected.angular = true;
|
|
99
|
+
}
|
|
100
|
+
} catch {
|
|
101
|
+
// Ignore parse errors
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check for .NET
|
|
106
|
+
if (hasFileWithExtension(projectPath, '.csproj') ||
|
|
107
|
+
hasFileWithExtension(projectPath, '.sln')) {
|
|
108
|
+
detected.dotnet = true;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return detected;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Detect AI tools configured in the project
|
|
116
|
+
* @param {string} projectPath - Path to the project
|
|
117
|
+
* @returns {Object} Detected AI tools
|
|
118
|
+
*/
|
|
119
|
+
export function detectAITools(projectPath) {
|
|
120
|
+
const detected = {
|
|
121
|
+
cursor: existsSync(join(projectPath, '.cursorrules')),
|
|
122
|
+
windsurf: existsSync(join(projectPath, '.windsurfrules')),
|
|
123
|
+
cline: existsSync(join(projectPath, '.clinerules')),
|
|
124
|
+
copilot: existsSync(join(projectPath, '.github', 'copilot-instructions.md')),
|
|
125
|
+
claudeCode: existsSync(join(projectPath, '.claude')) ||
|
|
126
|
+
existsSync(join(projectPath, 'CLAUDE.md')),
|
|
127
|
+
antigravity: existsSync(join(projectPath, 'INSTRUCTIONS.md'))
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return detected;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if any file with the given extension exists in the directory
|
|
135
|
+
* @param {string} dirPath - Directory path
|
|
136
|
+
* @param {string} extension - File extension (with dot)
|
|
137
|
+
* @returns {boolean} True if file exists
|
|
138
|
+
*/
|
|
139
|
+
function hasFileWithExtension(dirPath, extension) {
|
|
140
|
+
try {
|
|
141
|
+
const files = readdirSync(dirPath);
|
|
142
|
+
return files.some(f => f.endsWith(extension));
|
|
143
|
+
} catch {
|
|
144
|
+
return false;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Get a summary of all detections
|
|
150
|
+
* @param {string} projectPath - Path to the project
|
|
151
|
+
* @returns {Object} Detection summary
|
|
152
|
+
*/
|
|
153
|
+
export function detectAll(projectPath) {
|
|
154
|
+
return {
|
|
155
|
+
languages: detectLanguage(projectPath),
|
|
156
|
+
frameworks: detectFramework(projectPath),
|
|
157
|
+
aiTools: detectAITools(projectPath)
|
|
158
|
+
};
|
|
159
|
+
}
|
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, readdirSync, copyFileSync } from 'fs';
|
|
2
|
+
import { dirname, join, basename } from 'path';
|
|
3
|
+
import { homedir } from 'os';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
import https from 'https';
|
|
6
|
+
|
|
7
|
+
const GITHUB_RAW_BASE = 'https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main';
|
|
8
|
+
const SKILLS_RAW_BASE = 'https://raw.githubusercontent.com/AsiaOstrich/universal-dev-standards/main/skills/claude-code';
|
|
9
|
+
|
|
10
|
+
// Get the CLI package root directory
|
|
11
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
12
|
+
const __dirname = dirname(__filename);
|
|
13
|
+
const CLI_ROOT = join(__dirname, '..', '..');
|
|
14
|
+
const SKILLS_LOCAL_DIR = join(CLI_ROOT, '..', 'skills', 'claude-code');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Download a file from GitHub raw content
|
|
18
|
+
* @param {string} filePath - Path relative to repo root (e.g., 'core/checkin-standards.md')
|
|
19
|
+
* @returns {Promise<string>} File content
|
|
20
|
+
*/
|
|
21
|
+
export function downloadFromGitHub(filePath) {
|
|
22
|
+
const url = `${GITHUB_RAW_BASE}/${filePath}`;
|
|
23
|
+
|
|
24
|
+
return new Promise((resolve, reject) => {
|
|
25
|
+
https.get(url, (res) => {
|
|
26
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
27
|
+
// Follow redirect
|
|
28
|
+
https.get(res.headers.location, (redirectRes) => {
|
|
29
|
+
if (redirectRes.statusCode !== 200) {
|
|
30
|
+
reject(new Error(`GitHub returned ${redirectRes.statusCode} for ${filePath}`));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let data = '';
|
|
35
|
+
redirectRes.on('data', chunk => data += chunk);
|
|
36
|
+
redirectRes.on('end', () => resolve(data));
|
|
37
|
+
redirectRes.on('error', reject);
|
|
38
|
+
}).on('error', reject);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (res.statusCode !== 200) {
|
|
43
|
+
reject(new Error(`GitHub returned ${res.statusCode} for ${filePath}`));
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
let data = '';
|
|
48
|
+
res.on('data', chunk => data += chunk);
|
|
49
|
+
res.on('end', () => resolve(data));
|
|
50
|
+
res.on('error', reject);
|
|
51
|
+
}).on('error', reject);
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Download and save a standard file to the target project
|
|
57
|
+
* @param {string} sourcePath - Relative path from repo root (e.g., 'core/checkin-standards.md')
|
|
58
|
+
* @param {string} targetDir - Target directory (usually '.standards')
|
|
59
|
+
* @param {string} projectPath - Project root path
|
|
60
|
+
* @returns {Promise<Object>} Result with success status and copied path
|
|
61
|
+
*/
|
|
62
|
+
export async function downloadStandard(sourcePath, targetDir, projectPath) {
|
|
63
|
+
const targetFolder = join(projectPath, targetDir);
|
|
64
|
+
const targetFile = join(targetFolder, basename(sourcePath));
|
|
65
|
+
|
|
66
|
+
// Ensure target directory exists
|
|
67
|
+
if (!existsSync(targetFolder)) {
|
|
68
|
+
mkdirSync(targetFolder, { recursive: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
try {
|
|
72
|
+
const content = await downloadFromGitHub(sourcePath);
|
|
73
|
+
writeFileSync(targetFile, content);
|
|
74
|
+
return {
|
|
75
|
+
success: true,
|
|
76
|
+
error: null,
|
|
77
|
+
path: targetFile
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: error.message,
|
|
83
|
+
path: null
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Download and save an integration file to its target location
|
|
90
|
+
* @param {string} sourcePath - Source path relative to repo root
|
|
91
|
+
* @param {string} targetPath - Target path relative to project root
|
|
92
|
+
* @param {string} projectPath - Project root path
|
|
93
|
+
* @returns {Promise<Object>} Result
|
|
94
|
+
*/
|
|
95
|
+
export async function downloadIntegration(sourcePath, targetPath, projectPath) {
|
|
96
|
+
const target = join(projectPath, targetPath);
|
|
97
|
+
|
|
98
|
+
// Ensure target directory exists
|
|
99
|
+
const targetDir = dirname(target);
|
|
100
|
+
if (!existsSync(targetDir)) {
|
|
101
|
+
mkdirSync(targetDir, { recursive: true });
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
const content = await downloadFromGitHub(sourcePath);
|
|
106
|
+
writeFileSync(target, content);
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
error: null,
|
|
110
|
+
path: target
|
|
111
|
+
};
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: error.message,
|
|
116
|
+
path: null
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Download a file from Skills repository
|
|
123
|
+
* @param {string} filePath - Path relative to skills repo root
|
|
124
|
+
* @returns {Promise<string>} File content
|
|
125
|
+
*/
|
|
126
|
+
export function downloadFromSkillsRepo(filePath) {
|
|
127
|
+
const url = `${SKILLS_RAW_BASE}/${filePath}`;
|
|
128
|
+
|
|
129
|
+
return new Promise((resolve, reject) => {
|
|
130
|
+
https.get(url, (res) => {
|
|
131
|
+
if (res.statusCode === 301 || res.statusCode === 302) {
|
|
132
|
+
https.get(res.headers.location, (redirectRes) => {
|
|
133
|
+
if (redirectRes.statusCode !== 200) {
|
|
134
|
+
reject(new Error(`GitHub returned ${redirectRes.statusCode} for ${filePath}`));
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
let data = '';
|
|
139
|
+
redirectRes.on('data', chunk => data += chunk);
|
|
140
|
+
redirectRes.on('end', () => resolve(data));
|
|
141
|
+
redirectRes.on('error', reject);
|
|
142
|
+
}).on('error', reject);
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (res.statusCode !== 200) {
|
|
147
|
+
reject(new Error(`GitHub returned ${res.statusCode} for ${filePath}`));
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let data = '';
|
|
152
|
+
res.on('data', chunk => data += chunk);
|
|
153
|
+
res.on('end', () => resolve(data));
|
|
154
|
+
res.on('error', reject);
|
|
155
|
+
}).on('error', reject);
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Get the Skills installation directory
|
|
161
|
+
* @returns {string} Path to ~/.claude/skills/
|
|
162
|
+
*/
|
|
163
|
+
export function getSkillsDir() {
|
|
164
|
+
return join(homedir(), '.claude', 'skills');
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Check if local skills directory exists
|
|
169
|
+
* @returns {boolean} True if local skills are available
|
|
170
|
+
*/
|
|
171
|
+
export function hasLocalSkills() {
|
|
172
|
+
return existsSync(SKILLS_LOCAL_DIR);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Get local skills directory path
|
|
177
|
+
* @returns {string} Path to local skills directory
|
|
178
|
+
*/
|
|
179
|
+
export function getLocalSkillsDir() {
|
|
180
|
+
return SKILLS_LOCAL_DIR;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Install a single Skill from local directory
|
|
185
|
+
* @param {string} skillName - Skill name (e.g., 'ai-collaboration-standards')
|
|
186
|
+
* @returns {Object} Result with success status
|
|
187
|
+
*/
|
|
188
|
+
export function installSkillFromLocal(skillName) {
|
|
189
|
+
const sourceDir = join(SKILLS_LOCAL_DIR, skillName);
|
|
190
|
+
const skillsDir = getSkillsDir();
|
|
191
|
+
const targetDir = join(skillsDir, skillName);
|
|
192
|
+
|
|
193
|
+
if (!existsSync(sourceDir)) {
|
|
194
|
+
return {
|
|
195
|
+
success: false,
|
|
196
|
+
skillName,
|
|
197
|
+
files: [],
|
|
198
|
+
error: `Skill directory not found: ${sourceDir}`,
|
|
199
|
+
path: null
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Ensure target directory exists
|
|
204
|
+
if (!existsSync(targetDir)) {
|
|
205
|
+
mkdirSync(targetDir, { recursive: true });
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const results = [];
|
|
209
|
+
try {
|
|
210
|
+
const files = readdirSync(sourceDir);
|
|
211
|
+
for (const fileName of files) {
|
|
212
|
+
const sourceFile = join(sourceDir, fileName);
|
|
213
|
+
const targetFile = join(targetDir, fileName);
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
copyFileSync(sourceFile, targetFile);
|
|
217
|
+
results.push({ file: fileName, success: true });
|
|
218
|
+
} catch (error) {
|
|
219
|
+
results.push({ file: fileName, success: false, error: error.message });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
} catch (error) {
|
|
223
|
+
return {
|
|
224
|
+
success: false,
|
|
225
|
+
skillName,
|
|
226
|
+
files: results,
|
|
227
|
+
error: error.message,
|
|
228
|
+
path: null
|
|
229
|
+
};
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const allSuccess = results.every(r => r.success);
|
|
233
|
+
return {
|
|
234
|
+
success: allSuccess,
|
|
235
|
+
skillName,
|
|
236
|
+
files: results,
|
|
237
|
+
path: targetDir
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Download and install a single Skill from remote repository
|
|
243
|
+
* @param {string} skillName - Skill name (e.g., 'ai-collaboration-standards')
|
|
244
|
+
* @param {string[]} skillFiles - Array of file paths relative to skills repo
|
|
245
|
+
* @returns {Promise<Object>} Result with success status
|
|
246
|
+
*/
|
|
247
|
+
export async function downloadSkill(skillName, skillFiles) {
|
|
248
|
+
// Prefer local installation if available
|
|
249
|
+
if (hasLocalSkills()) {
|
|
250
|
+
return installSkillFromLocal(skillName);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Fall back to remote download
|
|
254
|
+
const skillsDir = getSkillsDir();
|
|
255
|
+
const targetDir = join(skillsDir, skillName);
|
|
256
|
+
|
|
257
|
+
// Ensure target directory exists
|
|
258
|
+
if (!existsSync(targetDir)) {
|
|
259
|
+
mkdirSync(targetDir, { recursive: true });
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const results = [];
|
|
263
|
+
for (const filePath of skillFiles) {
|
|
264
|
+
const fileName = basename(filePath);
|
|
265
|
+
const targetFile = join(targetDir, fileName);
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
const content = await downloadFromSkillsRepo(filePath);
|
|
269
|
+
writeFileSync(targetFile, content);
|
|
270
|
+
results.push({ file: fileName, success: true });
|
|
271
|
+
} catch (error) {
|
|
272
|
+
results.push({ file: fileName, success: false, error: error.message });
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const allSuccess = results.every(r => r.success);
|
|
277
|
+
return {
|
|
278
|
+
success: allSuccess,
|
|
279
|
+
skillName,
|
|
280
|
+
files: results,
|
|
281
|
+
path: targetDir
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Check if Skills are already installed and get version info
|
|
287
|
+
* @returns {Object|null} Installed skills info or null
|
|
288
|
+
*/
|
|
289
|
+
export function getInstalledSkillsInfo() {
|
|
290
|
+
const skillsDir = getSkillsDir();
|
|
291
|
+
const manifestPath = join(skillsDir, '.manifest.json');
|
|
292
|
+
|
|
293
|
+
if (!existsSync(manifestPath)) {
|
|
294
|
+
// Check if any skill directories exist
|
|
295
|
+
if (!existsSync(skillsDir)) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Skills exist but no manifest - likely manually installed
|
|
300
|
+
return {
|
|
301
|
+
installed: true,
|
|
302
|
+
version: null,
|
|
303
|
+
source: 'unknown'
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
309
|
+
return {
|
|
310
|
+
installed: true,
|
|
311
|
+
version: manifest.version || null,
|
|
312
|
+
source: manifest.source || 'universal-dev-standards',
|
|
313
|
+
installedDate: manifest.installedDate || null
|
|
314
|
+
};
|
|
315
|
+
} catch {
|
|
316
|
+
return {
|
|
317
|
+
installed: true,
|
|
318
|
+
version: null,
|
|
319
|
+
source: 'unknown'
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Write Skills manifest file
|
|
326
|
+
* @param {string} version - Version of skills installed
|
|
327
|
+
* @param {string} targetDir - Optional target directory (defaults to user-level)
|
|
328
|
+
*/
|
|
329
|
+
export function writeSkillsManifest(version, targetDir = null) {
|
|
330
|
+
const skillsDir = targetDir || getSkillsDir();
|
|
331
|
+
const manifestPath = join(skillsDir, '.manifest.json');
|
|
332
|
+
|
|
333
|
+
if (!existsSync(skillsDir)) {
|
|
334
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const manifest = {
|
|
338
|
+
version,
|
|
339
|
+
source: 'universal-dev-standards',
|
|
340
|
+
installedDate: new Date().toISOString().split('T')[0]
|
|
341
|
+
};
|
|
342
|
+
|
|
343
|
+
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2));
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Get the project-level Skills installation directory
|
|
348
|
+
* @param {string} projectPath - Project root path
|
|
349
|
+
* @returns {string} Path to project/.claude/skills/
|
|
350
|
+
*/
|
|
351
|
+
export function getProjectSkillsDir(projectPath) {
|
|
352
|
+
return join(projectPath, '.claude', 'skills');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* Check if project-level Skills are installed and get version info
|
|
357
|
+
* @param {string} projectPath - Project root path
|
|
358
|
+
* @returns {Object|null} Installed skills info or null
|
|
359
|
+
*/
|
|
360
|
+
export function getProjectInstalledSkillsInfo(projectPath) {
|
|
361
|
+
const skillsDir = getProjectSkillsDir(projectPath);
|
|
362
|
+
const manifestPath = join(skillsDir, '.manifest.json');
|
|
363
|
+
|
|
364
|
+
if (!existsSync(manifestPath)) {
|
|
365
|
+
// Check if any skill directories exist
|
|
366
|
+
if (!existsSync(skillsDir)) {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Skills exist but no manifest - likely manually installed
|
|
371
|
+
return {
|
|
372
|
+
installed: true,
|
|
373
|
+
version: null,
|
|
374
|
+
source: 'unknown',
|
|
375
|
+
location: 'project'
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'));
|
|
381
|
+
return {
|
|
382
|
+
installed: true,
|
|
383
|
+
version: manifest.version || null,
|
|
384
|
+
source: manifest.source || 'universal-dev-standards',
|
|
385
|
+
installedDate: manifest.installedDate || null,
|
|
386
|
+
location: 'project'
|
|
387
|
+
};
|
|
388
|
+
} catch {
|
|
389
|
+
return {
|
|
390
|
+
installed: true,
|
|
391
|
+
version: null,
|
|
392
|
+
source: 'unknown',
|
|
393
|
+
location: 'project'
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Install a single Skill to a specific target directory
|
|
400
|
+
* @param {string} skillName - Skill name (e.g., 'ai-collaboration-standards')
|
|
401
|
+
* @param {string} targetBaseDir - Target base directory for skills
|
|
402
|
+
* @returns {Object} Result with success status
|
|
403
|
+
*/
|
|
404
|
+
export function installSkillToDir(skillName, targetBaseDir) {
|
|
405
|
+
const sourceDir = join(SKILLS_LOCAL_DIR, skillName);
|
|
406
|
+
const targetDir = join(targetBaseDir, skillName);
|
|
407
|
+
|
|
408
|
+
if (!existsSync(sourceDir)) {
|
|
409
|
+
return {
|
|
410
|
+
success: false,
|
|
411
|
+
skillName,
|
|
412
|
+
files: [],
|
|
413
|
+
error: `Skill directory not found: ${sourceDir}`,
|
|
414
|
+
path: null
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Ensure target directory exists
|
|
419
|
+
if (!existsSync(targetDir)) {
|
|
420
|
+
mkdirSync(targetDir, { recursive: true });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
const results = [];
|
|
424
|
+
try {
|
|
425
|
+
const files = readdirSync(sourceDir);
|
|
426
|
+
for (const fileName of files) {
|
|
427
|
+
const sourceFile = join(sourceDir, fileName);
|
|
428
|
+
const targetFile = join(targetDir, fileName);
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
copyFileSync(sourceFile, targetFile);
|
|
432
|
+
results.push({ file: fileName, success: true });
|
|
433
|
+
} catch (error) {
|
|
434
|
+
results.push({ file: fileName, success: false, error: error.message });
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
} catch (error) {
|
|
438
|
+
return {
|
|
439
|
+
success: false,
|
|
440
|
+
skillName,
|
|
441
|
+
files: results,
|
|
442
|
+
error: error.message,
|
|
443
|
+
path: null
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const allSuccess = results.every(r => r.success);
|
|
448
|
+
return {
|
|
449
|
+
success: allSuccess,
|
|
450
|
+
skillName,
|
|
451
|
+
files: results,
|
|
452
|
+
path: targetDir
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Download and install a single Skill to a specific target directory
|
|
458
|
+
* @param {string} skillName - Skill name
|
|
459
|
+
* @param {string[]} skillFiles - Array of file paths relative to skills repo
|
|
460
|
+
* @param {string} targetLocation - 'user' or 'project'
|
|
461
|
+
* @param {string} projectPath - Project path (required if targetLocation is 'project')
|
|
462
|
+
* @returns {Promise<Object>} Result with success status
|
|
463
|
+
*/
|
|
464
|
+
export async function downloadSkillToLocation(skillName, skillFiles, targetLocation = 'user', projectPath = null) {
|
|
465
|
+
// Determine target directory
|
|
466
|
+
const targetBaseDir = targetLocation === 'project' && projectPath
|
|
467
|
+
? getProjectSkillsDir(projectPath)
|
|
468
|
+
: getSkillsDir();
|
|
469
|
+
|
|
470
|
+
// Prefer local installation if available
|
|
471
|
+
if (hasLocalSkills()) {
|
|
472
|
+
return installSkillToDir(skillName, targetBaseDir);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// Fall back to remote download
|
|
476
|
+
const targetDir = join(targetBaseDir, skillName);
|
|
477
|
+
|
|
478
|
+
// Ensure target directory exists
|
|
479
|
+
if (!existsSync(targetDir)) {
|
|
480
|
+
mkdirSync(targetDir, { recursive: true });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const results = [];
|
|
484
|
+
for (const filePath of skillFiles) {
|
|
485
|
+
const fileName = basename(filePath);
|
|
486
|
+
const targetFile = join(targetDir, fileName);
|
|
487
|
+
|
|
488
|
+
try {
|
|
489
|
+
// For remote download, we need to extract just the skill-relative path
|
|
490
|
+
// skillFiles paths are like: skills/claude-code/ai-collaboration-standards/SKILL.md
|
|
491
|
+
// We need just: ai-collaboration-standards/SKILL.md
|
|
492
|
+
const relativePath = filePath.replace(/^skills\/claude-code\//, '');
|
|
493
|
+
const content = await downloadFromSkillsRepo(relativePath);
|
|
494
|
+
writeFileSync(targetFile, content);
|
|
495
|
+
results.push({ file: fileName, success: true });
|
|
496
|
+
} catch (error) {
|
|
497
|
+
results.push({ file: fileName, success: false, error: error.message });
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const allSuccess = results.every(r => r.success);
|
|
502
|
+
return {
|
|
503
|
+
success: allSuccess,
|
|
504
|
+
skillName,
|
|
505
|
+
files: results,
|
|
506
|
+
path: targetDir
|
|
507
|
+
};
|
|
508
|
+
}
|