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';