diff --git a/.src/utils/file-utils.ts b/.src/utils/file-utils.ts new file mode 100644 index 0000000..efdfdc8 --- /dev/null +++ b/.src/utils/file-utils.ts @@ -0,0 +1,239 @@ +import * as fs from 'fs-extra'; +import * as path from 'path'; +import * as os from 'os'; +import { GlobalConfig, WorkspaceConfig, WorkspaceInfo } from '../types/index'; +import { GitAgentSyncError } from './errors'; + +const GLOBAL_CONFIG_DIR = '.git-agent-sync'; +const GLOBAL_CONFIG_FILE = 'config.json'; +const WORKSPACE_CONFIG_FILE = '.agent-workspace.json'; +const CHANGE_TRACKING_FILE = '.agent-changes.json'; + +export function getGlobalConfigDir(): string { + return process.env.GIT_AGENT_SYNC_CONFIG + ? path.dirname(process.env.GIT_AGENT_SYNC_CONFIG) + : path.join(os.homedir(), GLOBAL_CONFIG_DIR); +} + +export function getGlobalConfigPath(): string { + return process.env.GIT_AGENT_SYNC_CONFIG || path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE); +} + +export async function loadGlobalConfig(): Promise { + const configPath = getGlobalConfigPath(); + + if (await fs.pathExists(configPath)) { + const config = await fs.readJson(configPath); + return { ...getDefaultGlobalConfig(), ...config }; + } + + return getDefaultGlobalConfig(); +} + +export function getDefaultGlobalConfig(): GlobalConfig { + return { + workspacePath: process.env.GIT_AGENT_SYNC_PATH || './.agent-workspaces', + defaultBranch: 'main', + autoInstall: process.env.GIT_AGENT_SYNC_AUTO_INSTALL !== 'false', + templates: { + default: process.env.GIT_AGENT_SYNC_TEMPLATE || path.join(os.homedir(), GLOBAL_CONFIG_DIR, 'templates', 'default') + } + }; +} + +export async function saveGlobalConfig(config: GlobalConfig): Promise { + const configDir = getGlobalConfigDir(); + await fs.ensureDir(configDir); + await fs.writeJson(getGlobalConfigPath(), config, { spaces: 2 }); +} + +export function getWorkspaceConfigPath(workspacePath: string): string { + return path.join(workspacePath, WORKSPACE_CONFIG_FILE); +} + +export async function loadWorkspaceConfig(workspacePath: string): Promise { + const configPath = getWorkspaceConfigPath(workspacePath); + + if (await fs.pathExists(configPath)) { + return fs.readJson(configPath); + } + + return null; +} + +export async function saveWorkspaceConfig(workspacePath: string, config: WorkspaceConfig): Promise { + await fs.writeJson(getWorkspaceConfigPath(workspacePath), config, { spaces: 2 }); +} + +export async function loadChangeTracking(workspacePath: string): Promise> { + const trackingPath = path.join(workspacePath, CHANGE_TRACKING_FILE); + + if (await fs.pathExists(trackingPath)) { + return fs.readJson(trackingPath); + } + + return {}; +} + +export async function saveChangeTracking( + workspacePath: string, + data: Record +): Promise { + await fs.writeJson(path.join(workspacePath, CHANGE_TRACKING_FILE), data, { spaces: 2 }); +} + +export function getWorkspacePath(basePath: string, agentName: string): string { + const workspacePath = process.env.GIT_AGENT_SYNC_PATH || './.agent-workspaces'; + return path.resolve(basePath, workspacePath, `agent-${agentName}`); +} + +export async function getAllWorkspaces(basePath: string): Promise { + const globalConfig = await loadGlobalConfig(); + const workspaceRoot = path.resolve(basePath, globalConfig.workspacePath); + + if (!(await fs.pathExists(workspaceRoot))) { + return []; + } + + const workspaces: WorkspaceInfo[] = []; + const entries = await fs.readdir(workspaceRoot, { withFileTypes: true }); + + for (const entry of entries) { + if (entry.isDirectory() && entry.name.startsWith('agent-')) { + const workspacePath = path.join(workspaceRoot, entry.name); + const config = await loadWorkspaceConfig(workspacePath); + + if (config) { + workspaces.push({ + name: config.agentName, + path: workspacePath, + branch: config.branch, + exists: await fs.pathExists(workspacePath), + hasChanges: false, + changeCount: 0 + }); + } + } + } + + return workspaces; +} + +export async function createEnvironmentFile( + workspacePath: string, + variables: Record +): Promise { + const lines: string[] = []; + + for (const [key, value] of Object.entries(variables)) { + const safeValue = value.includes(' ') ? `"${value}"` : value; + lines.push(`${key}=${safeValue}`); + } + + const envContent = lines.join('\n'); + await fs.writeFile(path.join(workspacePath, '.env'), envContent, 'utf-8'); +} + +export async function readEnvironmentFile(workspacePath: string): Promise> { + const envPath = path.join(workspacePath, '.env'); + + if (!(await fs.pathExists(envPath))) { + return {}; + } + + const content = await fs.readFile(envPath, 'utf-8'); + const result: Record = {}; + + for (const line of content.split('\n')) { + const trimmed = line.trim(); + if (trimmed && !trimmed.startsWith('#')) { + const eqIndex = trimmed.indexOf('='); + if (eqIndex > 0) { + let value = trimmed.substring(eqIndex + 1); + if (value.startsWith('"') && value.endsWith('"')) { + value = value.slice(1, -1); + } + result[trimmed.substring(0, eqIndex)] = value; + } + } + } + + return result; +} + +export async function detectPackageJson(workspacePath: string): Promise { + return fs.pathExists(path.join(workspacePath, 'package.json')); +} + +export async function readPackageJson(workspacePath: string): Promise<{ scripts?: Record } | null> { + const pkgPath = path.join(workspacePath, 'package.json'); + + if (await fs.pathExists(pkgPath)) { + return fs.readJson(pkgPath); + } + + return null; +} + +export async function copyTemplate( + templatePath: string, + workspacePath: string +): Promise { + if (await fs.pathExists(templatePath)) { + await fs.copy(templatePath, workspacePath); + } +} + +export async function getTemplateDirectories(): Promise { + const configDir = getGlobalConfigDir(); + const templatesDir = path.join(configDir, 'templates'); + + if (!(await fs.pathExists(templatesDir))) { + return []; + } + + const entries = await fs.readdir(templatesDir, { withFileTypes: true }); + return entries + .filter(e => e.isDirectory()) + .map(e => e.name); +} + +export async function ensureDirectory(dirPath: string): Promise { + await fs.ensureDir(dirPath); +} + +export async function removeDirectory(dirPath: string): Promise { + if (await fs.pathExists(dirPath)) { + await fs.remove(dirPath); + } +} + +export function sanitizeAgentName(name: string): string { + return name + .toLowerCase() + .replace(/[^a-z0-9]/g, '-') + .replace(/-+/g, '-') + .replace(/^-|-$/g, ''); +} + +export function isValidAgentName(name: string): boolean { + const sanitized = sanitizeAgentName(name); + return sanitized.length > 0 && sanitized.length <= 100 && /^[a-z][a-z0-9-]*[a-z0-9]$/.test(sanitized); +} + +export async function readFileContent(filePath: string): Promise { + if (!(await fs.pathExists(filePath))) { + throw new GitAgentSyncError(`File not found: ${filePath}`); + } + return fs.readFile(filePath, 'utf-8'); +} + +export async function writeFileContent(filePath: string, content: string): Promise { + await fs.ensureDir(path.dirname(filePath)); + await fs.writeFile(filePath, content, 'utf-8'); +} + +export { hasUncommittedChanges } from './git-utils'; +export { exportChangesAsPatch } from './git-utils'; +export { installDependencies } from './env-utils'; +export { getGitUserInfo } from './env-utils';