Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"scripts": {
"lint": "eslint src/",
"build": "tsc --build",
"test": "jest --testRegex=\"[^\\.]+\\.spec\\.js$\" --passWithNoTests",
"test": "jest --testRegex=\"[^\\.]+\\.spec\\.(js|ts)$\" --passWithNoTests",
"tsc:w": "tsc -w"
},
"keywords": [],
Expand Down
148 changes: 148 additions & 0 deletions src/commands/clean/CleanRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { existsSync, rmSync, readdirSync, statSync } from 'fs';
import { join } from 'path';
import { Command } from 'commander';

export interface CleanRunnerOptions {
all?: boolean;
dryRun?: boolean;
project?: string;
}

export class CleanRunner {
protected all = false;
protected dryRun = false;
protected project?: string;
protected cachePath = '.zenith-cache';

constructor(options: CleanRunnerOptions = {}) {
this.all = options.all ?? false;
this.dryRun = options.dryRun ?? false;
this.project = options.project;
}

async run(): Promise<void> {
const rootPath = process.cwd();

// Try to get cache path from zenith.json
const configPath = join(rootPath, 'zenith.json');
if (existsSync(configPath)) {
try {
const config = JSON.parse(require('fs').readFileSync(configPath, 'utf-8'));
if (config.buildConfig?.cachePath) {
this.cachePath = config.buildConfig.cachePath;
}
} catch (e) {
// Use default cache path
}
}

const fullCachePath = join(rootPath, this.cachePath);

if (!existsSync(fullCachePath)) {
console.log(`✅ Cache directory does not exist: ${this.cachePath}`);
console.log(' Nothing to clean.');
return;
}

if (this.all) {
// Clean entire cache directory
await this.cleanDirectory(fullCachePath, 'entire cache');
} else if (this.project) {
// Clean specific project cache
const projectCachePath = join(fullCachePath, this.project);
if (existsSync(projectCachePath)) {
await this.cleanDirectory(projectCachePath, `project "${this.project}"`);
} else {
console.log(`⚠️ No cache found for project: ${this.project}`);
}
} else {
// Show cache info and clean with confirmation
await this.showCacheInfo(fullCachePath);
await this.cleanDirectory(fullCachePath, 'entire cache');
}
}

private async showCacheInfo(cachePath: string): Promise<void> {
try {
const items = readdirSync(cachePath);
const stats = this.getCacheStats(cachePath);

console.log('📊 Cache Statistics:');
console.log(` Path: ${this.cachePath}`);
console.log(` Projects: ${items.length}`);
console.log(` Total size: ${this.formatBytes(stats.totalSize)}`);
console.log(` Files: ${stats.fileCount}`);
console.log('');
} catch (e) {
// Ignore errors when showing info
}
}

private getCacheStats(dirPath: string): { totalSize: number; fileCount: number } {
let totalSize = 0;
let fileCount = 0;

const scan = (currentPath: string) => {
try {
const items = readdirSync(currentPath);
for (const item of items) {
const itemPath = join(currentPath, item);
const stat = statSync(itemPath);
if (stat.isDirectory()) {
scan(itemPath);
} else {
totalSize += stat.size;
fileCount++;
}
}
} catch (e) {
// Skip inaccessible directories
}
};

scan(dirPath);
return { totalSize, fileCount };
}

private formatBytes(bytes: number): string {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

private async cleanDirectory(dirPath: string, description: string): Promise<void> {
if (this.dryRun) {
console.log(`🔍 [DRY RUN] Would delete ${description}: ${dirPath}`);
return;
}

console.log(`🧹 Cleaning ${description}...`);
try {
rmSync(dirPath, { recursive: true, force: true });
console.log(`✅ Successfully cleaned ${description}`);
} catch (error) {
console.error(`❌ Failed to clean ${description}:`, error);
throw error;
}
}
}

// CLI wrapper for commander (used as default export)
export default class CleanRunnerCLI extends CleanRunner {
constructor(...args: readonly string[]) {
const program = new Command();
program
.option('-a, --all', 'Clean all cache without prompting', false)
.option('-d, --dry-run', 'Show what would be deleted without actually deleting', false)
.option('-p, --project <name>', 'Clean cache for a specific project');
program.parse(args);
const opts = program.opts();
super({
all: opts.all as boolean,
dryRun: opts.dryRun as boolean,
project: opts.project as string | undefined,
});
}
}
203 changes: 203 additions & 0 deletions src/commands/init/InitRunner.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
import { existsSync, readFileSync, writeFileSync, readdirSync, statSync } from 'fs';
import { join, relative } from 'path';
import { Command } from 'commander';

interface ZenithJsonConfig {
projects: Record<string, string>;
buildConfig: {
cachePath: string;
appConfig: Record<string, {
script: string;
outputs: string[];
}>;
};
}

export interface InitRunnerOptions {
force?: boolean;
output?: string;
}

export class InitRunner {
protected force = false;
protected outputFile = 'zenith.json';

constructor(options: InitRunnerOptions = {}) {
this.force = options.force ?? false;
this.outputFile = options.output ?? 'zenith.json';
}

async run(): Promise<void> {
const rootPath = process.cwd();
const configPath = join(rootPath, this.outputFile);

// Check if zenith.json already exists
if (existsSync(configPath) && !this.force) {
const message = `${this.outputFile} already exists. Use --force to overwrite.`;
console.log(`❌ ${message}`);
throw new Error(message);
}

console.log('🚀 Initializing Zenith configuration...\n');

// Try to detect projects
const projects = this.detectProjects(rootPath);

if (Object.keys(projects).length === 0) {
console.log('⚠️ No projects detected. Creating minimal configuration.');
} else {
console.log(`✅ Detected ${Object.keys(projects).length} project(s):`);
Object.entries(projects).forEach(([name, path]) => {
console.log(` - ${name}: ${path}`);
});
}

// Create default config
const config: ZenithJsonConfig = {
projects,
buildConfig: {
cachePath: '.zenith-cache',
appConfig: {
build: {
script: 'build',
outputs: ['build', 'dist']
},
test: {
script: 'test',
outputs: ['stdout']
},
lint: {
script: 'lint',
outputs: ['stdout']
}
}
}
};

// Write config file
writeFileSync(configPath, JSON.stringify(config, null, 2));
console.log(`\n✅ Created ${this.outputFile}`);
console.log('\n📝 Next steps:');
console.log(' 1. Review and customize zenith.json');
console.log(' 2. Run: pnpm zenith --target=build --project=all');
}

protected detectProjects(rootPath: string): Record<string, string> {
const projects: Record<string, string> = {};
const packageJsonPath = join(rootPath, 'package.json');

// Try to read workspaces from package.json
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));

// Check for pnpm workspaces
const pnpmWorkspacePath = join(rootPath, 'pnpm-workspace.yaml');
if (existsSync(pnpmWorkspacePath)) {
const workspaceContent = readFileSync(pnpmWorkspacePath, 'utf-8');
const workspacePatterns = this.parsePnpmWorkspace(workspaceContent);
this.resolveWorkspacePatterns(rootPath, workspacePatterns, projects);
}
// Check for yarn/npm workspaces in package.json
else if (packageJson.workspaces) {
const workspaces = Array.isArray(packageJson.workspaces)
? packageJson.workspaces
: packageJson.workspaces.packages || [];
this.resolveWorkspacePatterns(rootPath, workspaces, projects);
}
} catch (e) {
console.warn('⚠️ Could not parse package.json');
}
}

// If no workspaces found, look for common monorepo structures
if (Object.keys(projects).length === 0) {
const commonDirs = ['packages', 'apps', 'libs', 'projects'];
for (const dir of commonDirs) {
const dirPath = join(rootPath, dir);
if (existsSync(dirPath) && statSync(dirPath).isDirectory()) {
this.scanDirectory(rootPath, dirPath, projects);
}
}
}

return projects;
}

protected parsePnpmWorkspace(content: string): string[] {
const patterns: string[] = [];
const lines = content.split('\n');
let inPackages = false;

for (const line of lines) {
if (line.trim() === 'packages:') {
inPackages = true;
continue;
}
if (inPackages && line.trim().startsWith('-')) {
const pattern = line.trim().replace(/^-\s*['"]?/, '').replace(/['"]?\s*$/, '');
patterns.push(pattern);
}
}

return patterns;
}

protected resolveWorkspacePatterns(
rootPath: string,
patterns: string[],
projects: Record<string, string>
): void {
for (const pattern of patterns) {
// Handle glob patterns like "packages/*"
const basePath = pattern.replace(/\/\*.*$/, '');
const fullBasePath = join(rootPath, basePath);

if (existsSync(fullBasePath) && statSync(fullBasePath).isDirectory()) {
this.scanDirectory(rootPath, fullBasePath, projects);
}
}
}

protected scanDirectory(
rootPath: string,
dirPath: string,
projects: Record<string, string>
): void {
try {
const items = readdirSync(dirPath);
for (const item of items) {
const itemPath = join(dirPath, item);
if (statSync(itemPath).isDirectory()) {
const packageJsonPath = join(itemPath, 'package.json');
if (existsSync(packageJsonPath)) {
try {
const pkg = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
const projectName = pkg.name || item;
const relativePath = relative(rootPath, itemPath);
projects[projectName] = relativePath;
} catch (e) {
// Skip invalid package.json
}
}
}
}
} catch (e) {
// Skip inaccessible directories
}
}
}

// CLI wrapper for commander (used as default export)
export default class InitRunnerCLI extends InitRunner {
constructor(...args: readonly string[]) {
const program = new Command();
program
.option('-f, --force', 'Overwrite existing zenith.json', false)
.option('-o, --output <file>', 'Output file name', 'zenith.json');
program.parse(args);
const opts = program.opts();
super({ force: opts.force as boolean, output: opts.output as string });
}
}

20 changes: 17 additions & 3 deletions src/run.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
/* eslint-disable no-case-declarations */
import Runner from './classes/Runner.js';
import GraphRunner from './commands/graph/Runner.js';

export const run = async () => {
try {
const args = process.argv;
switch (args[2]) {
case 'graph':
// Dynamic import to avoid ConfigHelper loading before zenith.json exists
const { default: GraphRunner } = await import('./commands/graph/Runner.js');
const gRunner = new GraphRunner(...args);
await gRunner.run();
break;
default:
case 'init':
// Dynamic import to avoid ConfigHelper loading before zenith.json exists
const { default: InitRunner } = await import('./commands/init/InitRunner.js');
const initRunner = new InitRunner(...args);
await initRunner.run();
break;
case 'clean':
// Dynamic import to avoid ConfigHelper loading before zenith.json exists
const { default: CleanRunner } = await import('./commands/clean/CleanRunner.js');
const cleanRunner = new CleanRunner(...args);
await cleanRunner.run();
break;
default:
// Dynamic import to avoid ConfigHelper loading before zenith.json exists
const { default: Runner } = await import('./classes/Runner.js');
const RunnerHelper = new Runner(...args);
await RunnerHelper.runWrapper();
}
Expand Down
Loading