mirror of
https://github.com/tiennm99/ccs.git
synced 2026-10-12 20:15:40 +00:00
166 lines
4.6 KiB
TypeScript
166 lines
4.6 KiB
TypeScript
/**
|
|
* Shared Data Routes (Phase 07)
|
|
*
|
|
* API routes for commands, skills, agents from ~/.ccs/shared/
|
|
*/
|
|
|
|
import { Router, Request, Response } from 'express';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import { getCcsDir } from '../utils/config-manager';
|
|
import { getClaudeConfigDir } from '../utils/claude-config-path';
|
|
|
|
export const sharedRoutes = Router();
|
|
|
|
interface SharedItem {
|
|
name: string;
|
|
description: string;
|
|
path: string;
|
|
type: 'command' | 'skill' | 'agent';
|
|
}
|
|
|
|
/**
|
|
* GET /api/shared/commands
|
|
*/
|
|
sharedRoutes.get('/commands', (_req: Request, res: Response) => {
|
|
const items = getSharedItems('commands');
|
|
res.json({ items });
|
|
});
|
|
|
|
/**
|
|
* GET /api/shared/skills
|
|
*/
|
|
sharedRoutes.get('/skills', (_req: Request, res: Response) => {
|
|
const items = getSharedItems('skills');
|
|
res.json({ items });
|
|
});
|
|
|
|
/**
|
|
* GET /api/shared/agents
|
|
*/
|
|
sharedRoutes.get('/agents', (_req: Request, res: Response) => {
|
|
const items = getSharedItems('agents');
|
|
res.json({ items });
|
|
});
|
|
|
|
/**
|
|
* GET /api/shared/summary
|
|
*/
|
|
sharedRoutes.get('/summary', (_req: Request, res: Response) => {
|
|
const commands = getSharedItems('commands').length;
|
|
const skills = getSharedItems('skills').length;
|
|
const agents = getSharedItems('agents').length;
|
|
|
|
res.json({
|
|
commands,
|
|
skills,
|
|
agents,
|
|
total: commands + skills + agents,
|
|
symlinkStatus: checkSymlinkStatus(),
|
|
});
|
|
});
|
|
|
|
function getSharedItems(type: 'commands' | 'skills' | 'agents'): SharedItem[] {
|
|
const ccsDir = getCcsDir();
|
|
const sharedDir = path.join(ccsDir, 'shared', type);
|
|
|
|
if (!fs.existsSync(sharedDir)) {
|
|
return [];
|
|
}
|
|
|
|
const items: SharedItem[] = [];
|
|
|
|
try {
|
|
const entries = fs.readdirSync(sharedDir, { withFileTypes: true });
|
|
|
|
for (const entry of entries) {
|
|
if (entry.isDirectory()) {
|
|
// Skill/Agent: look for SKILL.md for skills, prompt.md for agents
|
|
const markdownFile = type === 'skills' ? 'SKILL.md' : 'prompt.md';
|
|
const promptPath = path.join(sharedDir, entry.name, markdownFile);
|
|
if (fs.existsSync(promptPath)) {
|
|
const content = fs.readFileSync(promptPath, 'utf8');
|
|
const description = extractDescription(content);
|
|
items.push({
|
|
name: entry.name,
|
|
description,
|
|
path: path.join(sharedDir, entry.name),
|
|
type: type === 'commands' ? 'command' : (type.slice(0, -1) as 'skill' | 'agent'),
|
|
});
|
|
}
|
|
} else if (entry.name.endsWith('.md')) {
|
|
// Command: .md file
|
|
const filePath = path.join(sharedDir, entry.name);
|
|
const content = fs.readFileSync(filePath, 'utf8');
|
|
const description = extractDescription(content);
|
|
items.push({
|
|
name: entry.name.replace('.md', ''),
|
|
description,
|
|
path: filePath,
|
|
type: 'command',
|
|
});
|
|
}
|
|
}
|
|
} catch {
|
|
// Directory read failed
|
|
}
|
|
|
|
return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
}
|
|
|
|
function extractDescription(content: string): string {
|
|
// Extract first non-empty, non-heading line
|
|
const lines = content.split('\n');
|
|
for (const line of lines) {
|
|
const trimmed = line.trim();
|
|
if (trimmed && !trimmed.startsWith('#') && !trimmed.startsWith('---')) {
|
|
return trimmed.slice(0, 100);
|
|
}
|
|
}
|
|
return 'No description';
|
|
}
|
|
|
|
function checkSymlinkStatus(): { valid: boolean; message: string } {
|
|
const ccsDir = getCcsDir();
|
|
const sharedDir = path.join(ccsDir, 'shared');
|
|
|
|
if (!fs.existsSync(sharedDir)) {
|
|
return { valid: false, message: 'Shared directory not found' };
|
|
}
|
|
|
|
// Check all three symlinks: commands, skills, agents
|
|
const linkTypes = ['commands', 'skills', 'agents'];
|
|
let validLinks = 0;
|
|
|
|
for (const linkType of linkTypes) {
|
|
const linkPath = path.join(sharedDir, linkType);
|
|
|
|
try {
|
|
if (fs.existsSync(linkPath)) {
|
|
const stats = fs.lstatSync(linkPath);
|
|
if (stats.isSymbolicLink()) {
|
|
const target = fs.readlinkSync(linkPath);
|
|
// Check if it points to ~/.claude/{linkType}
|
|
const expectedTarget = path.join(getClaudeConfigDir(), linkType);
|
|
if (path.resolve(path.dirname(linkPath), target) === path.resolve(expectedTarget)) {
|
|
validLinks++;
|
|
}
|
|
}
|
|
}
|
|
} catch {
|
|
// Not a symlink or read error
|
|
}
|
|
}
|
|
|
|
if (validLinks === linkTypes.length) {
|
|
return { valid: true, message: 'Symlinks active' };
|
|
} else if (validLinks > 0) {
|
|
return {
|
|
valid: false,
|
|
message: `Symlinks partially configured (${validLinks}/${linkTypes.length})`,
|
|
};
|
|
}
|
|
|
|
return { valid: false, message: 'Symlinks not configured' };
|
|
}
|