ruch
Version:
Revolutionary React TypeScript CLI with hexagonal architecture & AI-powered development assistance. Create maintainable, scalable applications with domain-driven design and integrated AI tooling.
279 lines (237 loc) • 8.65 kB
text/typescript
import fs from 'fs-extra';
import path from 'path';
import readline from 'readline';
import type { FileSystem } from '../utils/file-operations';
import {
validateFileExists,
createDirectory,
writeFile,
readFile
} from '../utils/file-operations';
import type { Logger } from '../utils/logging';
import {
logError,
logSuccess,
logInfo,
logWarning
} from '../utils/logging';
import {
buildContextPath,
buildServiceContextFilePath,
buildServiceContextTestFilePath,
getExistingDomains,
parseExistingServiceContext,
findMissingDomains
} from '../utils/domain-analyzer';
import {
getEmptyServiceContextTemplate,
getServiceContextTemplate,
getServiceContextTestTemplate
} from '../templates/context';
import type { DomainInfo } from '../templates/context';
/**
* Pure function to validate context creation preconditions
*/
const validateContextCreationPreconditions = async (
fileSystem: FileSystem,
contextFilePath: string
): Promise<{ canCreate: boolean; reason?: string }> => {
const contextExists = await fileSystem.exists(contextFilePath);
if (contextExists) {
return {
canCreate: false,
reason: 'Service context already exists'
};
}
return { canCreate: true };
};
/**
* Pure function to validate context update preconditions
*/
const validateContextUpdatePreconditions = async (
fileSystem: FileSystem,
contextFilePath: string
): Promise<{ canUpdate: boolean; reason?: string }> => {
const contextExists = await fileSystem.exists(contextFilePath);
if (!contextExists) {
return {
canUpdate: false,
reason: 'Service context does not exist. Use "context init" or "context generate" first.'
};
}
return { canUpdate: true };
};
/**
* Pure function to create context files
*/
const createContextFiles = async (
fileSystem: FileSystem,
contextPath: string,
contextFilePath: string,
testFilePath: string,
contextContent: string,
testContent: string
): Promise<void> => {
await fileSystem.ensureDir(contextPath);
await fileSystem.writeFile(contextFilePath, contextContent, 'utf8');
await fileSystem.writeFile(testFilePath, testContent, 'utf8');
};
/**
* Pure function to log context creation success
*/
const logContextCreationSuccess = (
log: Logger,
contextFilePath: string,
domains: DomainInfo[]
): void => {
logSuccess(log, 'Service context created successfully!');
logInfo(log, `Location: ${contextFilePath}`);
if (domains.length > 0) {
logInfo(log, `Integrated ${domains.length} domain(s):`);
domains.forEach(domain => logInfo(log, ` - ${domain.name}`));
}
logInfo(log, '💡 Import with: import { useServices } from \'./context/ServiceContext\'');
};
/**
* Pure function to ask for user confirmation
*/
const askForConfirmation = (question: string): Promise<boolean> => {
return new Promise((resolve) => {
const rl = readline.createInterface({
input: process.stdin,
output: process.stdout
});
rl.question(`${question} (y/N): `, (answer) => {
rl.close();
resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
});
});
};
/**
* Initialize empty service context
*/
export const initServiceContext = async (
fileSystem: FileSystem = fs,
log: Logger = require('../utils/logger').logger,
projectRoot: string = process.cwd()
): Promise<void> => {
const contextPath = buildContextPath(projectRoot);
const contextFilePath = buildServiceContextFilePath(projectRoot);
const testFilePath = buildServiceContextTestFilePath(projectRoot);
try {
const validation = await validateContextCreationPreconditions(fileSystem, contextFilePath);
if (!validation.canCreate) {
logWarning(log, validation.reason || 'Cannot create service context');
return;
}
logInfo(log, 'Initializing empty service context...');
const contextContent = getEmptyServiceContextTemplate();
const testContent = getServiceContextTestTemplate([]);
await createContextFiles(
fileSystem,
contextPath,
contextFilePath,
testFilePath,
contextContent,
testContent
);
logContextCreationSuccess(log, contextFilePath, []);
} catch (error) {
logError(log, `Error initializing service context: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Generate service context with all existing domains
*/
export const generateServiceContext = async (
fileSystem: FileSystem = fs,
log: Logger = require('../utils/logger').logger,
projectRoot: string = process.cwd()
): Promise<void> => {
const contextPath = buildContextPath(projectRoot);
const contextFilePath = buildServiceContextFilePath(projectRoot);
const testFilePath = buildServiceContextTestFilePath(projectRoot);
try {
const contextExists = await fileSystem.exists(contextFilePath);
if (contextExists) {
logWarning(log, 'Service context already exists. Use "context update" to add new domains.');
return;
}
logInfo(log, 'Generating service context with existing domains...');
const domains = await getExistingDomains(fileSystem, projectRoot);
if (domains.length === 0) {
logWarning(log, 'No domains found. Creating empty service context.');
await initServiceContext(fileSystem, log, projectRoot);
return;
}
const contextContent = getServiceContextTemplate(domains);
const testContent = getServiceContextTestTemplate(domains);
await createContextFiles(
fileSystem,
contextPath,
contextFilePath,
testFilePath,
contextContent,
testContent
);
logContextCreationSuccess(log, contextFilePath, domains);
} catch (error) {
logError(log, `Error generating service context: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};
/**
* Update service context with missing domains
*/
export const updateServiceContext = async (
fileSystem: FileSystem = fs,
log: Logger = require('../utils/logger').logger,
projectRoot: string = process.cwd(),
skipConfirmation: boolean = false
): Promise<void> => {
const contextFilePath = buildServiceContextFilePath(projectRoot);
const testFilePath = buildServiceContextTestFilePath(projectRoot);
try {
const validation = await validateContextUpdatePreconditions(fileSystem, contextFilePath);
if (!validation.canUpdate) {
logError(log, validation.reason || 'Cannot update service context');
return;
}
logInfo(log, 'Checking for missing domains...');
const missingDomains = await findMissingDomains(fileSystem, projectRoot);
if (missingDomains.length === 0) {
logInfo(log, 'Service context is already up to date. No missing domains found.');
return;
}
logInfo(log, `Found ${missingDomains.length} missing domain(s):`);
missingDomains.forEach(domain => logInfo(log, ` - ${domain.name}`));
// Create backup before overwriting
const backupPath = `${contextFilePath}.backup.${Date.now()}`;
const existingContent = await fileSystem.readFile(contextFilePath, 'utf8');
await fileSystem.writeFile(backupPath, existingContent, 'utf8');
logWarning(log, '⚠️ WARNING: This will overwrite your existing ServiceContext.tsx');
logInfo(log, `📁 Backup created at: ${backupPath}`);
logInfo(log, '💡 Review the backup if you had custom modifications');
// Ask for confirmation unless skipped (for tests)
if (!skipConfirmation) {
const confirmed = await askForConfirmation('Do you want to continue?');
if (!confirmed) {
logInfo(log, '❌ Operation cancelled by user');
// Remove the backup since we're not proceeding
await fileSystem.remove(backupPath);
return;
}
}
// Get all domains (existing + missing) for regeneration
const allDomains = await getExistingDomains(fileSystem, projectRoot);
const contextContent = getServiceContextTemplate(allDomains);
const testContent = getServiceContextTestTemplate(allDomains);
await fileSystem.writeFile(contextFilePath, contextContent, 'utf8');
await fileSystem.writeFile(testFilePath, testContent, 'utf8');
logSuccess(log, 'Service context updated successfully!');
logInfo(log, `Added ${missingDomains.length} domain(s) to service context`);
logInfo(log, `Total domains: ${allDomains.length}`);
logInfo(log, `🔄 If you had custom code, restore it from: ${backupPath}`);
} catch (error) {
logError(log, `Error updating service context: ${error instanceof Error ? error.message : 'Unknown error'}`);
}
};