mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-13 02:41:50 +08:00
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:
231
ccw/src/core/manifest.ts
Normal file
231
ccw/src/core/manifest.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user