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