Auto-sync: Local changes
Change-Id: Ia2b0b18d94901b18943603fdfe991b50b0733071
diff --git a/server/.env b/server/.env
new file mode 100644
index 0000000..9407122
--- /dev/null
+++ b/server/.env
@@ -0,0 +1,2 @@
+REMOTE_REPO_URL=https://github.com/iomodo/staff.git
+WORKING_DIR=../
\ No newline at end of file
diff --git a/server/agent/agent.go b/server/agent/agent.go
new file mode 100644
index 0000000..e901cf6
--- /dev/null
+++ b/server/agent/agent.go
@@ -0,0 +1,23 @@
+package agent
+
+type AgentConfig struct {
+ Name string
+ Role string
+ GitUsername string
+ GitEmail string
+ WorkingDir string
+}
+
+type Agent struct {
+ Config AgentConfig
+}
+
+func NewAgent(config AgentConfig) *Agent {
+ return &Agent{
+ Config: config,
+ }
+}
+
+func (a *Agent) Run() {
+
+}
diff --git a/server/server/server.go b/server/server/server.go
index 9f95f10..ed3c62f 100644
--- a/server/server/server.go
+++ b/server/server/server.go
@@ -1,9 +1,13 @@
package server
import (
+ "context"
+ "fmt"
"log/slog"
"os"
+ "path/filepath"
+ "github.com/iomodo/staff/git"
"github.com/joho/godotenv"
)
@@ -28,7 +32,36 @@
// Start method starts an app
func (a *Server) Start() error {
a.logger.Info("Server is starting...")
- return nil
+
+ // Get environment variables
+ remoteRepoURL := os.Getenv("REMOTE_REPO_URL")
+ workingDir := os.Getenv("WORKING_DIR")
+
+ if remoteRepoURL == "" {
+ return fmt.Errorf("REMOTE_REPO_URL environment variable is required")
+ }
+
+ if workingDir == "" {
+ return fmt.Errorf("WORKING_DIR environment variable is required")
+ }
+
+ a.logger.Info("Environment variables loaded",
+ slog.String("remoteRepoURL", remoteRepoURL),
+ slog.String("workingDir", workingDir))
+
+ // Check if working directory is empty
+ isEmpty, err := a.isDirectoryEmpty(workingDir)
+ if err != nil {
+ return fmt.Errorf("failed to check if directory is empty: %w", err)
+ }
+
+ if isEmpty {
+ a.logger.Info("Working directory is empty, initializing new repository")
+ return a.initializeNewRepository(workingDir, remoteRepoURL)
+ } else {
+ a.logger.Info("Working directory is not empty, syncing with remote")
+ return a.syncWithRemote(workingDir, remoteRepoURL)
+ }
}
// Shutdown method shuts server down
@@ -36,3 +69,216 @@
a.logger.Info("Stoping Server...")
a.logger.Info("Server stopped")
}
+
+// isDirectoryEmpty checks if a directory is empty
+func (a *Server) isDirectoryEmpty(dir string) (bool, error) {
+ // Create directory if it doesn't exist
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return false, fmt.Errorf("failed to create directory: %w", err)
+ }
+
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ return false, fmt.Errorf("failed to read directory: %w", err)
+ }
+
+ // Directory is empty if it has no entries or only has .git directory
+ if len(entries) == 0 {
+ return true, nil
+ }
+
+ // Check if directory only contains .git (which might be a git repo)
+ if len(entries) == 1 && entries[0].Name() == ".git" {
+ return true, nil
+ }
+
+ return false, nil
+}
+
+// initializeNewRepository creates a new git repository with prepopulated data
+func (a *Server) initializeNewRepository(workingDir, remoteRepoURL string) error {
+ ctx := context.Background()
+
+ // Initialize git repository
+ gitRepo := git.DefaultGit(workingDir)
+
+ a.logger.Info("Initializing git repository", slog.String("path", workingDir))
+ if err := gitRepo.Init(ctx, workingDir); err != nil {
+ return fmt.Errorf("failed to initialize git repository: %w", err)
+ }
+
+ // Set up git user configuration
+ userConfig := git.UserConfig{
+ Name: "Staff System",
+ Email: "system@staff.com",
+ }
+ if err := gitRepo.SetUserConfig(ctx, userConfig); err != nil {
+ return fmt.Errorf("failed to set git user config: %w", err)
+ }
+
+ // Add remote origin
+ if err := gitRepo.AddRemote(ctx, "origin", remoteRepoURL); err != nil {
+ return fmt.Errorf("failed to add remote origin: %w", err)
+ }
+
+ // Create prepopulated directory structure and files
+ if err := a.createPrepopulatedStructure(workingDir); err != nil {
+ return fmt.Errorf("failed to create prepopulated structure: %w", err)
+ }
+
+ // Add all files to git
+ if err := gitRepo.AddAll(ctx); err != nil {
+ return fmt.Errorf("failed to add files to git: %w", err)
+ }
+
+ // Commit the initial structure
+ if err := gitRepo.Commit(ctx, "Initial commit: Add prepopulated project structure", git.CommitOptions{}); err != nil {
+ return fmt.Errorf("failed to commit initial structure: %w", err)
+ }
+
+ // Push to remote
+ if err := gitRepo.Push(ctx, "origin", "main", git.PushOptions{SetUpstream: true}); err != nil {
+ return fmt.Errorf("failed to push to remote: %w", err)
+ }
+
+ a.logger.Info("Successfully initialized repository and pushed to remote")
+ return nil
+}
+
+// syncWithRemote synchronizes local repository with remote
+func (a *Server) syncWithRemote(workingDir, remoteRepoURL string) error {
+ ctx := context.Background()
+
+ // Check if it's a git repository
+ gitRepo := git.DefaultGit(workingDir)
+ isRepo, err := gitRepo.IsRepository(ctx, workingDir)
+ if err != nil {
+ return fmt.Errorf("failed to check if directory is git repository: %w", err)
+ }
+
+ if !isRepo {
+ return fmt.Errorf("working directory is not a git repository")
+ }
+
+ // Get current status
+ status, err := gitRepo.Status(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to get git status: %w", err)
+ }
+
+ a.logger.Info("Current git status",
+ slog.String("branch", status.Branch),
+ slog.Bool("isClean", status.IsClean),
+ slog.Int("stagedFiles", len(status.Staged)),
+ slog.Int("unstagedFiles", len(status.Unstaged)),
+ slog.Int("untrackedFiles", len(status.Untracked)))
+
+ // Check if remote origin exists, if not add it
+ remotes, err := gitRepo.ListRemotes(ctx)
+ if err != nil {
+ return fmt.Errorf("failed to list remotes: %w", err)
+ }
+
+ originExists := false
+ for _, remote := range remotes {
+ if remote.Name == "origin" {
+ originExists = true
+ break
+ }
+ }
+
+ if !originExists {
+ a.logger.Info("Adding remote origin")
+ if err := gitRepo.AddRemote(ctx, "origin", remoteRepoURL); err != nil {
+ return fmt.Errorf("failed to add remote origin: %w", err)
+ }
+ }
+
+ // Fetch latest changes from remote
+ a.logger.Info("Fetching latest changes from remote")
+ if err := gitRepo.Fetch(ctx, "origin", git.FetchOptions{}); err != nil {
+ return fmt.Errorf("failed to fetch from remote: %w", err)
+ }
+
+ // If there are local changes, commit and push them
+ if !status.IsClean {
+ a.logger.Info("Local changes detected, committing and pushing")
+
+ if err := gitRepo.AddAll(ctx); err != nil {
+ return fmt.Errorf("failed to add local changes: %w", err)
+ }
+
+ if err := gitRepo.Commit(ctx, "Auto-sync: Local changes", git.CommitOptions{}); err != nil {
+ return fmt.Errorf("failed to commit local changes: %w", err)
+ }
+
+ if err := gitRepo.Push(ctx, "origin", status.Branch, git.PushOptions{}); err != nil {
+ return fmt.Errorf("failed to push local changes: %w", err)
+ }
+ }
+
+ // Pull latest changes from remote
+ a.logger.Info("Pulling latest changes from remote")
+ if err := gitRepo.Pull(ctx, "origin", status.Branch); err != nil {
+ return fmt.Errorf("failed to pull from remote: %w", err)
+ }
+
+ a.logger.Info("Successfully synchronized with remote repository")
+ return nil
+}
+
+// createPrepopulatedStructure creates the required directory structure and files
+func (a *Server) createPrepopulatedStructure(workingDir string) error {
+ // Create directories
+ dirs := []string{
+ filepath.Join(workingDir, "operations", "agents", "ceo"),
+ filepath.Join(workingDir, "operations", "agents", "pm"),
+ filepath.Join(workingDir, "operations", "tasks"),
+ filepath.Join(workingDir, "server"),
+ filepath.Join(workingDir, "webapp"),
+ }
+
+ for _, dir := range dirs {
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return fmt.Errorf("failed to create directory %s: %w", dir, err)
+ }
+ }
+
+ // Read agent system files from the current project
+ ceoSystemPath := filepath.Join("operations", "agents", "ceo", "system.md")
+ pmSystemPath := filepath.Join("operations", "agents", "pm", "system.md")
+ taskExamplePath := filepath.Join("operations", "tasks", "example-task-file.md")
+
+ // Read CEO system file
+ ceoContent, err := os.ReadFile(ceoSystemPath)
+ if err != nil {
+ return fmt.Errorf("failed to read CEO system file: %w", err)
+ }
+
+ // Read PM system file
+ pmContent, err := os.ReadFile(pmSystemPath)
+ if err != nil {
+ return fmt.Errorf("failed to read PM system file: %w", err)
+ }
+
+ // Read task example file
+ taskExampleContent, err := os.ReadFile(taskExamplePath)
+ if err != nil {
+ return fmt.Errorf("failed to read task example file: %w", err)
+ }
+
+ // Create prepopulated files
+ files := map[string][]byte{
+ filepath.Join(workingDir, "operations", "agents", "ceo", "system.md"): ceoContent,
+ filepath.Join(workingDir, "operations", "agents", "pm", "system.md"): pmContent,
+ filepath.Join(workingDir, "operations", "tasks", "example-task-file.md"): taskExampleContent,
+ }
+ for filePath, content := range files {
+ if err := os.WriteFile(filePath, content, 0644); err != nil {
+ return fmt.Errorf("failed to create file %s: %w", filePath, err)
+ }
+ }
+
+ a.logger.Info("Created prepopulated directory structure and files")
+ return nil
+}