mirror of
https://github.com/catlog22/Claude-Code-Workflow.git
synced 2026-02-10 02:24:35 +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:
@@ -5,17 +5,18 @@ import { resolve } from 'path';
|
||||
/**
|
||||
* Launch a URL or file in the default browser
|
||||
* Cross-platform compatible (Windows/macOS/Linux)
|
||||
* @param {string} urlOrPath - HTTP URL or path to HTML file
|
||||
* @returns {Promise<void>}
|
||||
* @param urlOrPath - HTTP URL or path to HTML file
|
||||
* @returns Promise that resolves when browser is launched
|
||||
*/
|
||||
export async function launchBrowser(urlOrPath) {
|
||||
export async function launchBrowser(urlOrPath: string): Promise<void> {
|
||||
// Check if it's already a URL (http:// or https://)
|
||||
if (urlOrPath.startsWith('http://') || urlOrPath.startsWith('https://')) {
|
||||
try {
|
||||
await open(urlOrPath);
|
||||
return;
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to open browser: ${error.message}`);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to open browser: ${message}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +24,7 @@ export async function launchBrowser(urlOrPath) {
|
||||
const absolutePath = resolve(urlOrPath);
|
||||
|
||||
// Construct file:// URL based on platform
|
||||
let url;
|
||||
let url: string;
|
||||
if (platform() === 'win32') {
|
||||
// Windows: file:///C:/path/to/file.html
|
||||
url = `file:///${absolutePath.replace(/\\/g, '/')}`;
|
||||
@@ -40,16 +41,17 @@ export async function launchBrowser(urlOrPath) {
|
||||
try {
|
||||
await open(absolutePath);
|
||||
} catch (fallbackError) {
|
||||
throw new Error(`Failed to open browser: ${error.message}`);
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
throw new Error(`Failed to open browser: ${message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we're running in a headless/CI environment
|
||||
* @returns {boolean}
|
||||
* @returns True if running in headless environment
|
||||
*/
|
||||
export function isHeadlessEnvironment() {
|
||||
export function isHeadlessEnvironment(): boolean {
|
||||
return !!(
|
||||
process.env.CI ||
|
||||
process.env.CONTINUOUS_INTEGRATION ||
|
||||
@@ -3,10 +3,10 @@ import { join } from 'path';
|
||||
|
||||
/**
|
||||
* Safely read a JSON file
|
||||
* @param {string} filePath - Path to JSON file
|
||||
* @returns {Object|null} - Parsed JSON or null on error
|
||||
* @param filePath - Path to JSON file
|
||||
* @returns Parsed JSON or null on error
|
||||
*/
|
||||
export function readJsonFile(filePath) {
|
||||
export function readJsonFile(filePath: string): unknown | null {
|
||||
if (!existsSync(filePath)) return null;
|
||||
try {
|
||||
return JSON.parse(readFileSync(filePath, 'utf8'));
|
||||
@@ -17,10 +17,10 @@ export function readJsonFile(filePath) {
|
||||
|
||||
/**
|
||||
* Safely read a text file
|
||||
* @param {string} filePath - Path to text file
|
||||
* @returns {string|null} - File contents or null on error
|
||||
* @param filePath - Path to text file
|
||||
* @returns File contents or null on error
|
||||
*/
|
||||
export function readTextFile(filePath) {
|
||||
export function readTextFile(filePath: string): string | null {
|
||||
if (!existsSync(filePath)) return null;
|
||||
try {
|
||||
return readFileSync(filePath, 'utf8');
|
||||
@@ -31,18 +31,18 @@ export function readTextFile(filePath) {
|
||||
|
||||
/**
|
||||
* Write content to a file
|
||||
* @param {string} filePath - Path to file
|
||||
* @param {string} content - Content to write
|
||||
* @param filePath - Path to file
|
||||
* @param content - Content to write
|
||||
*/
|
||||
export function writeTextFile(filePath, content) {
|
||||
export function writeTextFile(filePath: string, content: string): void {
|
||||
writeFileSync(filePath, content, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a path exists
|
||||
* @param {string} filePath - Path to check
|
||||
* @returns {boolean}
|
||||
* @param filePath - Path to check
|
||||
* @returns True if path exists
|
||||
*/
|
||||
export function pathExists(filePath) {
|
||||
export function pathExists(filePath: string): boolean {
|
||||
return existsSync(filePath);
|
||||
}
|
||||
@@ -3,11 +3,29 @@ import { existsSync, mkdirSync, realpathSync, statSync, readFileSync, writeFileS
|
||||
import { homedir } from 'os';
|
||||
|
||||
/**
|
||||
* Resolve a path, handling ~ for home directory
|
||||
* @param {string} inputPath - Path to resolve
|
||||
* @returns {string} - Absolute path
|
||||
* Validation result for path operations
|
||||
*/
|
||||
export function resolvePath(inputPath) {
|
||||
export interface PathValidationResult {
|
||||
valid: boolean;
|
||||
path: string | null;
|
||||
error: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Options for path validation
|
||||
*/
|
||||
export interface ValidatePathOptions {
|
||||
baseDir?: string | null;
|
||||
mustExist?: boolean;
|
||||
allowHome?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a path, handling ~ for home directory
|
||||
* @param inputPath - Path to resolve
|
||||
* @returns Absolute path
|
||||
*/
|
||||
export function resolvePath(inputPath: string): string {
|
||||
if (!inputPath) return process.cwd();
|
||||
|
||||
// Handle ~ for home directory
|
||||
@@ -21,14 +39,11 @@ export function resolvePath(inputPath) {
|
||||
/**
|
||||
* Validate and sanitize a user-provided path
|
||||
* Prevents path traversal attacks and validates path is within allowed boundaries
|
||||
* @param {string} inputPath - User-provided path
|
||||
* @param {Object} options - Validation options
|
||||
* @param {string} options.baseDir - Base directory to restrict paths within (optional)
|
||||
* @param {boolean} options.mustExist - Whether path must exist (default: false)
|
||||
* @param {boolean} options.allowHome - Whether to allow home directory paths (default: true)
|
||||
* @returns {Object} - { valid: boolean, path: string|null, error: string|null }
|
||||
* @param inputPath - User-provided path
|
||||
* @param options - Validation options
|
||||
* @returns Validation result with path or error
|
||||
*/
|
||||
export function validatePath(inputPath, options = {}) {
|
||||
export function validatePath(inputPath: string, options: ValidatePathOptions = {}): PathValidationResult {
|
||||
const { baseDir = null, mustExist = false, allowHome = true } = options;
|
||||
|
||||
// Check for empty/null input
|
||||
@@ -45,11 +60,12 @@ export function validatePath(inputPath, options = {}) {
|
||||
}
|
||||
|
||||
// Resolve the path
|
||||
let resolvedPath;
|
||||
let resolvedPath: string;
|
||||
try {
|
||||
resolvedPath = resolvePath(trimmedPath);
|
||||
} catch (err) {
|
||||
return { valid: false, path: null, error: `Invalid path: ${err.message}` };
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { valid: false, path: null, error: `Invalid path: ${message}` };
|
||||
}
|
||||
|
||||
// Check if path exists when required
|
||||
@@ -63,7 +79,8 @@ export function validatePath(inputPath, options = {}) {
|
||||
try {
|
||||
realPath = realpathSync(resolvedPath);
|
||||
} catch (err) {
|
||||
return { valid: false, path: null, error: `Cannot resolve path: ${err.message}` };
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { valid: false, path: null, error: `Cannot resolve path: ${message}` };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -95,11 +112,11 @@ export function validatePath(inputPath, options = {}) {
|
||||
|
||||
/**
|
||||
* Validate output file path for writing
|
||||
* @param {string} outputPath - Output file path
|
||||
* @param {string} defaultDir - Default directory if path is relative
|
||||
* @returns {Object} - { valid: boolean, path: string|null, error: string|null }
|
||||
* @param outputPath - Output file path
|
||||
* @param defaultDir - Default directory if path is relative
|
||||
* @returns Validation result with path or error
|
||||
*/
|
||||
export function validateOutputPath(outputPath, defaultDir = process.cwd()) {
|
||||
export function validateOutputPath(outputPath: string, defaultDir: string = process.cwd()): PathValidationResult {
|
||||
if (!outputPath || typeof outputPath !== 'string') {
|
||||
return { valid: false, path: null, error: 'Output path is required' };
|
||||
}
|
||||
@@ -112,12 +129,13 @@ export function validateOutputPath(outputPath, defaultDir = process.cwd()) {
|
||||
}
|
||||
|
||||
// Resolve the path
|
||||
let resolvedPath;
|
||||
let resolvedPath: string;
|
||||
try {
|
||||
resolvedPath = isAbsolute(trimmedPath) ? trimmedPath : join(defaultDir, trimmedPath);
|
||||
resolvedPath = resolve(resolvedPath);
|
||||
} catch (err) {
|
||||
return { valid: false, path: null, error: `Invalid output path: ${err.message}` };
|
||||
const message = err instanceof Error ? err.message : String(err);
|
||||
return { valid: false, path: null, error: `Invalid output path: ${message}` };
|
||||
}
|
||||
|
||||
// Ensure it's not a directory
|
||||
@@ -137,9 +155,9 @@ export function validateOutputPath(outputPath, defaultDir = process.cwd()) {
|
||||
|
||||
/**
|
||||
* Get potential template locations
|
||||
* @returns {string[]} - Array of existing template directories
|
||||
* @returns Array of existing template directories
|
||||
*/
|
||||
export function getTemplateLocations() {
|
||||
export function getTemplateLocations(): string[] {
|
||||
const locations = [
|
||||
join(homedir(), '.claude', 'templates'),
|
||||
join(process.cwd(), '.claude', 'templates')
|
||||
@@ -150,10 +168,10 @@ export function getTemplateLocations() {
|
||||
|
||||
/**
|
||||
* Find a template file in known locations
|
||||
* @param {string} templateName - Name of template file (e.g., 'workflow-dashboard.html')
|
||||
* @returns {string|null} - Path to template or null if not found
|
||||
* @param templateName - Name of template file (e.g., 'workflow-dashboard.html')
|
||||
* @returns Path to template or null if not found
|
||||
*/
|
||||
export function findTemplate(templateName) {
|
||||
export function findTemplate(templateName: string): string | null {
|
||||
const locations = getTemplateLocations();
|
||||
|
||||
for (const loc of locations) {
|
||||
@@ -168,9 +186,9 @@ export function findTemplate(templateName) {
|
||||
|
||||
/**
|
||||
* Ensure directory exists, creating if necessary
|
||||
* @param {string} dirPath - Directory path to ensure
|
||||
* @param dirPath - Directory path to ensure
|
||||
*/
|
||||
export function ensureDir(dirPath) {
|
||||
export function ensureDir(dirPath: string): void {
|
||||
if (!existsSync(dirPath)) {
|
||||
mkdirSync(dirPath, { recursive: true });
|
||||
}
|
||||
@@ -178,19 +196,19 @@ export function ensureDir(dirPath) {
|
||||
|
||||
/**
|
||||
* Get the .workflow directory path from project path
|
||||
* @param {string} projectPath - Path to project
|
||||
* @returns {string} - Path to .workflow directory
|
||||
* @param projectPath - Path to project
|
||||
* @returns Path to .workflow directory
|
||||
*/
|
||||
export function getWorkflowDir(projectPath) {
|
||||
export function getWorkflowDir(projectPath: string): string {
|
||||
return join(resolvePath(projectPath), '.workflow');
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize path for display (handle Windows backslashes)
|
||||
* @param {string} filePath - Path to normalize
|
||||
* @returns {string}
|
||||
* @param filePath - Path to normalize
|
||||
* @returns Normalized path with forward slashes
|
||||
*/
|
||||
export function normalizePathForDisplay(filePath) {
|
||||
export function normalizePathForDisplay(filePath: string): string {
|
||||
return filePath.replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
@@ -199,14 +217,21 @@ const RECENT_PATHS_FILE = join(homedir(), '.ccw-recent-paths.json');
|
||||
const MAX_RECENT_PATHS = 10;
|
||||
|
||||
/**
|
||||
* Get recent project paths
|
||||
* @returns {string[]} - Array of recent paths
|
||||
* Recent paths data structure
|
||||
*/
|
||||
export function getRecentPaths() {
|
||||
interface RecentPathsData {
|
||||
paths: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent project paths
|
||||
* @returns Array of recent paths
|
||||
*/
|
||||
export function getRecentPaths(): string[] {
|
||||
try {
|
||||
if (existsSync(RECENT_PATHS_FILE)) {
|
||||
const content = readFileSync(RECENT_PATHS_FILE, 'utf8');
|
||||
const data = JSON.parse(content);
|
||||
const data = JSON.parse(content) as RecentPathsData;
|
||||
return Array.isArray(data.paths) ? data.paths : [];
|
||||
}
|
||||
} catch {
|
||||
@@ -217,9 +242,9 @@ export function getRecentPaths() {
|
||||
|
||||
/**
|
||||
* Track a project path (add to recent paths)
|
||||
* @param {string} projectPath - Path to track
|
||||
* @param projectPath - Path to track
|
||||
*/
|
||||
export function trackRecentPath(projectPath) {
|
||||
export function trackRecentPath(projectPath: string): void {
|
||||
try {
|
||||
const normalized = normalizePathForDisplay(resolvePath(projectPath));
|
||||
let paths = getRecentPaths();
|
||||
@@ -243,7 +268,7 @@ export function trackRecentPath(projectPath) {
|
||||
/**
|
||||
* Clear recent paths
|
||||
*/
|
||||
export function clearRecentPaths() {
|
||||
export function clearRecentPaths(): void {
|
||||
try {
|
||||
if (existsSync(RECENT_PATHS_FILE)) {
|
||||
writeFileSync(RECENT_PATHS_FILE, JSON.stringify({ paths: [] }, null, 2), 'utf8');
|
||||
@@ -255,10 +280,10 @@ export function clearRecentPaths() {
|
||||
|
||||
/**
|
||||
* Remove a specific path from recent paths
|
||||
* @param {string} pathToRemove - Path to remove
|
||||
* @returns {boolean} - True if removed, false if not found
|
||||
* @param pathToRemove - Path to remove
|
||||
* @returns True if removed, false if not found
|
||||
*/
|
||||
export function removeRecentPath(pathToRemove) {
|
||||
export function removeRecentPath(pathToRemove: string): boolean {
|
||||
try {
|
||||
const normalized = normalizePathForDisplay(resolvePath(pathToRemove));
|
||||
let paths = getRecentPaths();
|
||||
@@ -3,16 +3,26 @@ import figlet from 'figlet';
|
||||
import boxen from 'boxen';
|
||||
import gradient from 'gradient-string';
|
||||
import ora from 'ora';
|
||||
import type { Ora } from 'ora';
|
||||
|
||||
// Custom gradient colors
|
||||
const claudeGradient = gradient(['#00d4ff', '#00ff88']);
|
||||
const codeGradient = gradient(['#00ff88', '#ffff00']);
|
||||
const workflowGradient = gradient(['#ffff00', '#ff8800']);
|
||||
|
||||
/**
|
||||
* Options for summary box display
|
||||
*/
|
||||
export interface SummaryBoxOptions {
|
||||
title: string;
|
||||
lines: string[];
|
||||
borderColor?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Display ASCII art banner
|
||||
*/
|
||||
export function showBanner() {
|
||||
export function showBanner(): void {
|
||||
console.log('');
|
||||
|
||||
// CLAUDE in cyan gradient
|
||||
@@ -44,10 +54,10 @@ export function showBanner() {
|
||||
|
||||
/**
|
||||
* Display header with version info
|
||||
* @param {string} version - Version number
|
||||
* @param {string} mode - Installation mode
|
||||
* @param version - Version number
|
||||
* @param mode - Installation mode
|
||||
*/
|
||||
export function showHeader(version, mode = '') {
|
||||
export function showHeader(version: string, mode: string = ''): void {
|
||||
showBanner();
|
||||
|
||||
const versionText = version ? `v${version}` : '';
|
||||
@@ -68,10 +78,10 @@ export function showHeader(version, mode = '') {
|
||||
|
||||
/**
|
||||
* Create a spinner
|
||||
* @param {string} text - Spinner text
|
||||
* @returns {ora.Ora}
|
||||
* @param text - Spinner text
|
||||
* @returns Ora spinner instance
|
||||
*/
|
||||
export function createSpinner(text) {
|
||||
export function createSpinner(text: string): Ora {
|
||||
return ora({
|
||||
text,
|
||||
color: 'cyan',
|
||||
@@ -81,54 +91,51 @@ export function createSpinner(text) {
|
||||
|
||||
/**
|
||||
* Display success message
|
||||
* @param {string} message
|
||||
* @param message - Success message
|
||||
*/
|
||||
export function success(message) {
|
||||
export function success(message: string): void {
|
||||
console.log(chalk.green('✓') + ' ' + chalk.green(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display info message
|
||||
* @param {string} message
|
||||
* @param message - Info message
|
||||
*/
|
||||
export function info(message) {
|
||||
export function info(message: string): void {
|
||||
console.log(chalk.cyan('ℹ') + ' ' + chalk.cyan(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display warning message
|
||||
* @param {string} message
|
||||
* @param message - Warning message
|
||||
*/
|
||||
export function warning(message) {
|
||||
export function warning(message: string): void {
|
||||
console.log(chalk.yellow('⚠') + ' ' + chalk.yellow(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display error message
|
||||
* @param {string} message
|
||||
* @param message - Error message
|
||||
*/
|
||||
export function error(message) {
|
||||
export function error(message: string): void {
|
||||
console.log(chalk.red('✖') + ' ' + chalk.red(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display step message
|
||||
* @param {number} step - Step number
|
||||
* @param {number} total - Total steps
|
||||
* @param {string} message - Step message
|
||||
* @param stepNum - Step number
|
||||
* @param total - Total steps
|
||||
* @param message - Step message
|
||||
*/
|
||||
export function step(stepNum, total, message) {
|
||||
export function step(stepNum: number, total: number, message: string): void {
|
||||
console.log(chalk.gray(`[${stepNum}/${total}]`) + ' ' + chalk.white(message));
|
||||
}
|
||||
|
||||
/**
|
||||
* Display summary box
|
||||
* @param {Object} options
|
||||
* @param {string} options.title - Box title
|
||||
* @param {string[]} options.lines - Content lines
|
||||
* @param {string} options.borderColor - Border color
|
||||
* @param options - Summary box options
|
||||
*/
|
||||
export function summaryBox({ title, lines, borderColor = 'green' }) {
|
||||
export function summaryBox({ title, lines, borderColor = 'green' }: SummaryBoxOptions): void {
|
||||
const content = lines.join('\n');
|
||||
console.log(boxen(content, {
|
||||
title,
|
||||
@@ -143,6 +150,6 @@ export function summaryBox({ title, lines, borderColor = 'green' }) {
|
||||
/**
|
||||
* Display a divider line
|
||||
*/
|
||||
export function divider() {
|
||||
export function divider(): void {
|
||||
console.log(chalk.gray('─'.repeat(60)));
|
||||
}
|
||||
Reference in New Issue
Block a user