feat(ccw): migrate backend to TypeScript

- Convert 40 JS files to TypeScript (CLI, tools, core, MCP server)
- Add Zod for runtime parameter validation
- Add type definitions in src/types/
- Keep src/templates/ as JavaScript (dashboard frontend)
- Update bin entries to use dist/
- Add tsconfig.json with strict mode
- Add backward-compatible exports for tests
- All 39 tests passing

Breaking changes: None (backward compatible)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
catlog22
2025-12-13 10:43:15 +08:00
parent d4e59770d0
commit 25ac862f46
93 changed files with 5531 additions and 9302 deletions

231
ccw/src/core/manifest.ts Normal file
View File

@@ -0,0 +1,231 @@
import { readFileSync, writeFileSync, existsSync, mkdirSync, readdirSync, unlinkSync } from 'fs';
import { join } from 'path';
import { homedir } from 'os';
// Manifest directory location
const MANIFEST_DIR = join(homedir(), '.claude-manifests');
export interface ManifestFileEntry {
path: string;
type: 'File';
timestamp: string;
}
export interface ManifestDirectoryEntry {
path: string;
type: 'Directory';
timestamp: string;
}
export interface Manifest {
manifest_id: string;
version: string;
installation_mode: string;
installation_path: string;
installation_date: string;
installer_version: string;
files: ManifestFileEntry[];
directories: ManifestDirectoryEntry[];
}
export interface ManifestWithMetadata extends Manifest {
manifest_file: string;
application_version: string;
files_count: number;
directories_count: number;
}
/**
* Ensure manifest directory exists
*/
function ensureManifestDir(): void {
if (!existsSync(MANIFEST_DIR)) {
mkdirSync(MANIFEST_DIR, { recursive: true });
}
}
/**
* Create a new installation manifest
* @param mode - Installation mode (Global/Path)
* @param installPath - Installation path
* @returns New manifest object
*/
export function createManifest(mode: string, installPath: string): Manifest {
ensureManifestDir();
const timestamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').split('.')[0];
const modePrefix = mode === 'Global' ? 'manifest-global' : 'manifest-path';
const manifestId = `${modePrefix}-${timestamp}`;
return {
manifest_id: manifestId,
version: '1.0',
installation_mode: mode,
installation_path: installPath,
installation_date: new Date().toISOString(),
installer_version: '1.0.0',
files: [],
directories: []
};
}
/**
* Add file entry to manifest
* @param manifest - Manifest object
* @param filePath - File path
*/
export function addFileEntry(manifest: Manifest, filePath: string): void {
manifest.files.push({
path: filePath,
type: 'File',
timestamp: new Date().toISOString()
});
}
/**
* Add directory entry to manifest
* @param manifest - Manifest object
* @param dirPath - Directory path
*/
export function addDirectoryEntry(manifest: Manifest, dirPath: string): void {
manifest.directories.push({
path: dirPath,
type: 'Directory',
timestamp: new Date().toISOString()
});
}
/**
* Save manifest to disk
* @param manifest - Manifest object
* @returns Path to saved manifest
*/
export function saveManifest(manifest: Manifest): string {
ensureManifestDir();
// Remove old manifests for same path and mode
removeOldManifests(manifest.installation_path, manifest.installation_mode);
const manifestPath = join(MANIFEST_DIR, `${manifest.manifest_id}.json`);
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), 'utf8');
return manifestPath;
}
/**
* Remove old manifests for the same installation path and mode
* @param installPath - Installation path
* @param mode - Installation mode
*/
function removeOldManifests(installPath: string, mode: string): void {
if (!existsSync(MANIFEST_DIR)) return;
const normalizedPath = installPath.toLowerCase().replace(/[\\/]+$/, '');
try {
const files = readdirSync(MANIFEST_DIR).filter(f => f.endsWith('.json'));
for (const file of files) {
try {
const filePath = join(MANIFEST_DIR, file);
const content = JSON.parse(readFileSync(filePath, 'utf8')) as Partial<Manifest>;
const manifestPath = (content.installation_path || '').toLowerCase().replace(/[\\/]+$/, '');
const manifestMode = content.installation_mode || 'Global';
if (manifestPath === normalizedPath && manifestMode === mode) {
unlinkSync(filePath);
}
} catch {
// Skip invalid manifest files
}
}
} catch {
// Ignore errors
}
}
/**
* Get all installation manifests
* @returns Array of manifest objects
*/
export function getAllManifests(): ManifestWithMetadata[] {
if (!existsSync(MANIFEST_DIR)) return [];
const manifests: ManifestWithMetadata[] = [];
try {
const files = readdirSync(MANIFEST_DIR).filter(f => f.endsWith('.json'));
for (const file of files) {
try {
const filePath = join(MANIFEST_DIR, file);
const content = JSON.parse(readFileSync(filePath, 'utf8')) as Manifest;
// Try to read version.json for application version
let appVersion = 'unknown';
try {
const versionPath = join(content.installation_path, '.claude', 'version.json');
if (existsSync(versionPath)) {
const versionInfo = JSON.parse(readFileSync(versionPath, 'utf8')) as { version?: string };
appVersion = versionInfo.version || 'unknown';
}
} catch {
// Ignore
}
manifests.push({
...content,
manifest_file: filePath,
application_version: appVersion,
files_count: content.files?.length || 0,
directories_count: content.directories?.length || 0
});
} catch {
// Skip invalid manifest files
}
}
// Sort by installation date (newest first)
manifests.sort((a, b) => new Date(b.installation_date).getTime() - new Date(a.installation_date).getTime());
} catch {
// Ignore errors
}
return manifests;
}
/**
* Find manifest for a specific path and mode
* @param installPath - Installation path
* @param mode - Installation mode
* @returns Manifest or null
*/
export function findManifest(installPath: string, mode: string): ManifestWithMetadata | null {
const manifests = getAllManifests();
const normalizedPath = installPath.toLowerCase().replace(/[\\/]+$/, '');
return manifests.find(m => {
const manifestPath = (m.installation_path || '').toLowerCase().replace(/[\\/]+$/, '');
return manifestPath === normalizedPath && m.installation_mode === mode;
}) || null;
}
/**
* Delete a manifest file
* @param manifestFile - Path to manifest file
*/
export function deleteManifest(manifestFile: string): void {
if (existsSync(manifestFile)) {
unlinkSync(manifestFile);
}
}
/**
* Get manifest directory path
* @returns Manifest directory path
*/
export function getManifestDir(): string {
return MANIFEST_DIR;
}