This commit is contained in:
@@ -7,233 +7,98 @@ import { GitAgentSyncError } from './errors';
|
|||||||
const GLOBAL_CONFIG_DIR = '.git-agent-sync';
|
const GLOBAL_CONFIG_DIR = '.git-agent-sync';
|
||||||
const GLOBAL_CONFIG_FILE = 'config.json';
|
const GLOBAL_CONFIG_FILE = 'config.json';
|
||||||
const WORKSPACE_CONFIG_FILE = '.agent-workspace.json';
|
const WORKSPACE_CONFIG_FILE = '.agent-workspace.json';
|
||||||
const CHANGE_TRACKING_FILE = '.agent-changes.json';
|
|
||||||
|
|
||||||
export function getGlobalConfigDir(): string {
|
export function getGlobalConfigDir(): string {
|
||||||
return process.env.GIT_AGENT_SYNC_CONFIG
|
return process.env.GIT_AGENT_SYNC_CONFIG ? path.dirname(process.env.GIT_AGENT_SYNC_CONFIG) : path.join(os.homedir(), GLOBAL_CONFIG_DIR);
|
||||||
? path.dirname(process.env.GIT_AGENT_SYNC_CONFIG)
|
|
||||||
: path.join(os.homedir(), GLOBAL_CONFIG_DIR);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGlobalConfigPath(): string {
|
export function getGlobalConfigPath(): string {
|
||||||
return process.env.GIT_AGENT_SYNC_CONFIG || path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE);
|
return process.env.GIT_AGENT_SYNC_CONFIG || path.join(getGlobalConfigDir(), GLOBAL_CONFIG_FILE);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadGlobalConfig(): Promise<GlobalConfig> {
|
export async function loadGlobalConfig(): Promise<GlobalConfig> {
|
||||||
const configPath = getGlobalConfigPath();
|
const configPath = getGlobalConfigPath();
|
||||||
|
|
||||||
if (await fs.pathExists(configPath)) {
|
if (await fs.pathExists(configPath)) {
|
||||||
const config = await fs.readJson(configPath);
|
const config = await fs.readJson(configPath);
|
||||||
return { ...getDefaultGlobalConfig(), ...config };
|
return { ...getDefaultGlobalConfig(), ...config };
|
||||||
}
|
}
|
||||||
|
|
||||||
return getDefaultGlobalConfig();
|
return getDefaultGlobalConfig();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDefaultGlobalConfig(): GlobalConfig {
|
export function getDefaultGlobalConfig(): GlobalConfig {
|
||||||
return {
|
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') } };
|
||||||
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<void> {
|
export async function saveGlobalConfig(config: GlobalConfig): Promise<void> {
|
||||||
const configDir = getGlobalConfigDir();
|
await fs.ensureDir(getGlobalConfigDir());
|
||||||
await fs.ensureDir(configDir);
|
|
||||||
await fs.writeJson(getGlobalConfigPath(), config, { spaces: 2 });
|
await fs.writeJson(getGlobalConfigPath(), config, { spaces: 2 });
|
||||||
}
|
}
|
||||||
|
export function getWorkspacePath(basePath: string, agentName: string): string {
|
||||||
export function getWorkspaceConfigPath(workspacePath: string): string {
|
return path.join(basePath, '.agent-workspaces', `agent-${agentName}`);
|
||||||
return path.join(workspacePath, WORKSPACE_CONFIG_FILE);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadWorkspaceConfig(workspacePath: string): Promise<WorkspaceConfig | null> {
|
export async function loadWorkspaceConfig(workspacePath: string): Promise<WorkspaceConfig | null> {
|
||||||
const configPath = getWorkspaceConfigPath(workspacePath);
|
const configPath = path.join(workspacePath, WORKSPACE_CONFIG_FILE);
|
||||||
|
|
||||||
if (await fs.pathExists(configPath)) {
|
if (await fs.pathExists(configPath)) {
|
||||||
return fs.readJson(configPath);
|
return fs.readJson(configPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveWorkspaceConfig(workspacePath: string, config: WorkspaceConfig): Promise<void> {
|
export async function saveWorkspaceConfig(workspacePath: string, config: WorkspaceConfig): Promise<void> {
|
||||||
await fs.writeJson(getWorkspaceConfigPath(workspacePath), config, { spaces: 2 });
|
const configPath = path.join(workspacePath, WORKSPACE_CONFIG_FILE);
|
||||||
|
await fs.writeJson(configPath, config, { spaces: 2 });
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function loadChangeTracking(workspacePath: string): Promise<Record<string, any>> {
|
|
||||||
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<string, any>
|
|
||||||
): Promise<void> {
|
|
||||||
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<WorkspaceInfo[]> {
|
export async function getAllWorkspaces(basePath: string): Promise<WorkspaceInfo[]> {
|
||||||
const globalConfig = await loadGlobalConfig();
|
const workspaceRoot = path.join(basePath, '.agent-workspaces');
|
||||||
const workspaceRoot = path.resolve(basePath, globalConfig.workspacePath);
|
if (!await fs.pathExists(workspaceRoot)) return [];
|
||||||
|
const entries = await fs.readdir(workspaceRoot);
|
||||||
if (!(await fs.pathExists(workspaceRoot))) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const workspaces: WorkspaceInfo[] = [];
|
const workspaces: WorkspaceInfo[] = [];
|
||||||
const entries = await fs.readdir(workspaceRoot, { withFileTypes: true });
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
if (entry.isDirectory() && entry.name.startsWith('agent-')) {
|
if (entry.startsWith('agent-')) {
|
||||||
const workspacePath = path.join(workspaceRoot, entry.name);
|
const workspacePath = path.join(workspaceRoot, entry);
|
||||||
const config = await loadWorkspaceConfig(workspacePath);
|
const stat = await fs.stat(workspacePath);
|
||||||
|
if (stat.isDirectory()) {
|
||||||
if (config) {
|
workspaces.push({ name: entry, path: workspacePath, branch: entry, exists: true, hasChanges: false, changeCount: 0 });
|
||||||
workspaces.push({
|
|
||||||
name: config.agentName,
|
|
||||||
path: workspacePath,
|
|
||||||
branch: config.branch,
|
|
||||||
exists: await fs.pathExists(workspacePath),
|
|
||||||
hasChanges: false,
|
|
||||||
changeCount: 0
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return workspaces;
|
return workspaces;
|
||||||
}
|
}
|
||||||
|
export function sanitizeAgentName(name: string): string {
|
||||||
export async function createEnvironmentFile(
|
return name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
|
||||||
workspacePath: string,
|
|
||||||
variables: Record<string, string>
|
|
||||||
): Promise<void> {
|
|
||||||
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 function isValidAgentName(name: string): boolean {
|
||||||
export async function readEnvironmentFile(workspacePath: string): Promise<Record<string, string>> {
|
return /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/.test(name) && name.length >= 1 && name.length <= 100;
|
||||||
const envPath = path.join(workspacePath, '.env');
|
}
|
||||||
|
export async function isGitRepository(basePath: string): Promise<boolean> {
|
||||||
if (!(await fs.pathExists(envPath))) {
|
try {
|
||||||
return {};
|
await fs.access(path.join(basePath, '.git'), fs.constants.R_OK);
|
||||||
}
|
return true;
|
||||||
|
} catch { return false; }
|
||||||
const content = await fs.readFile(envPath, 'utf-8');
|
}
|
||||||
const result: Record<string, string> = {};
|
export async function hasUncommittedChanges(workspacePath: string): Promise<boolean> {
|
||||||
|
const { execa } = await import('execa');
|
||||||
for (const line of content.split('\n')) {
|
try {
|
||||||
const trimmed = line.trim();
|
const result = await execa('git', ['status', '--porcelain'], { cwd: workspacePath });
|
||||||
if (trimmed && !trimmed.startsWith('#')) {
|
return result.stdout.trim().length > 0;
|
||||||
const eqIndex = trimmed.indexOf('=');
|
} catch { return false; }
|
||||||
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<boolean> {
|
export async function detectPackageJson(workspacePath: string): Promise<boolean> {
|
||||||
return fs.pathExists(path.join(workspacePath, 'package.json'));
|
return fs.pathExists(path.join(workspacePath, 'package.json'));
|
||||||
}
|
}
|
||||||
|
export async function installDependencies(workspacePath: string): Promise<void> {
|
||||||
|
const { execa } = await import('execa');
|
||||||
|
await execa('npm', ['install'], { cwd: workspacePath });
|
||||||
|
}
|
||||||
export async function readPackageJson(workspacePath: string): Promise<{ scripts?: Record<string, string> } | null> {
|
export async function readPackageJson(workspacePath: string): Promise<{ scripts?: Record<string, string> } | null> {
|
||||||
const pkgPath = path.join(workspacePath, 'package.json');
|
const pkgPath = path.join(workspacePath, 'package.json');
|
||||||
|
|
||||||
if (await fs.pathExists(pkgPath)) {
|
if (await fs.pathExists(pkgPath)) {
|
||||||
return fs.readJson(pkgPath);
|
return fs.readJson(pkgPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
export async function getGitUserInfo(): Promise<{ name: string; email: string }> {
|
||||||
export async function copyTemplate(
|
const { execa } = await import('execa');
|
||||||
templatePath: string,
|
try {
|
||||||
workspacePath: string
|
const nameResult = await execa('git', ['config', 'user.name']);
|
||||||
): Promise<void> {
|
const emailResult = await execa('git', ['config', 'user.email']);
|
||||||
if (await fs.pathExists(templatePath)) {
|
return { name: nameResult.stdout.trim(), email: emailResult.stdout.trim() };
|
||||||
await fs.copy(templatePath, workspacePath);
|
} catch {
|
||||||
|
return { name: 'Unknown', email: 'unknown@example.com' };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getTemplateDirectories(): Promise<string[]> {
|
|
||||||
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<void> {
|
|
||||||
await fs.ensureDir(dirPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function removeDirectory(dirPath: string): Promise<void> {
|
|
||||||
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<string> {
|
|
||||||
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<void> {
|
|
||||||
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';
|
|
||||||
Reference in New Issue
Block a user