dodo: Support Sketch agent

Change-Id: I4dcd6aab7d7a2c2e86aaf1ad8d36d30a649ab31d
diff --git a/apps/app-runner/Dockerfile.sketch.latest b/apps/app-runner/Dockerfile.sketch.latest
new file mode 100644
index 0000000..82aba7a
--- /dev/null
+++ b/apps/app-runner/Dockerfile.sketch.latest
@@ -0,0 +1,9 @@
+FROM alpine:3.22.0
+
+ARG TARGETARCH
+
+RUN apk update
+RUN apk add bash curl git nodejs npm
+
+COPY --from=giolekva/sketch:latest /usr/bin/sketch /usr/bin/sketch
+COPY app-runner_${TARGETARCH} /usr/bin/app-runner
diff --git a/apps/app-runner/Makefile b/apps/app-runner/Makefile
index 187ca18..ef1c483 100644
--- a/apps/app-runner/Makefile
+++ b/apps/app-runner/Makefile
@@ -11,6 +11,7 @@
 manifest_dest_deno_2_2_0=docker://docker.io/$(repo_name)/app-runner:deno-2.2.0
 manifest_dest_nodejs_23_1_0=docker://docker.io/$(repo_name)/app-runner:nodejs-23.1.0
 manifest_dest_nodejs_24_0_2=docker://docker.io/$(repo_name)/app-runner:nodejs-24.0.2
+manifest_dest_sketch_latest=docker://docker.io/$(repo_name)/app-runner:sketch-latest
 endif
 
 clean:
@@ -166,6 +167,24 @@
 	$(podman) manifest push $(repo_name)/app-runner:deno-2.2.0 $(manifest_dest_deno_2_2_0)
 	$(podman) manifest rm $(repo_name)/app-runner:deno-2.2.0
 
+# Sketch
+
+push_sketch_arm64: clean build_arm64
+	$(podman) build --platform linux/arm64 --tag=$(repo_name)/app-runner:sketch-latest-arm64 -f Dockerfile.sketch.latest $(docker_flags) .
+	$(podman) push $(repo_name)/app-runner:sketch-latest-arm64
+
+push_sketch_amd64: clean build_amd64
+	$(podman) build --platform linux/amd64 --tag=$(repo_name)/app-runner:sketch-latest-amd64 -f Dockerfile.sketch.latest $(docker_flags) .
+	$(podman) push $(repo_name)/app-runner:sketch-latest-amd64
+
+push_sketch:
+	make -C ../../../sketch push
+	make push_sketch_arm64
+	make push_sketch_amd64
+	$(podman) manifest create $(repo_name)/app-runner:sketch-latest $(repo_name)/app-runner:sketch-latest-arm64 $(repo_name)/app-runner:sketch-latest-amd64
+	$(podman) manifest push $(repo_name)/app-runner:sketch-latest $(manifest_dest_sketch_latest)
+	$(podman) manifest rm $(repo_name)/app-runner:sketch-latest
+
 
 # all
-push: push_golang_1_22_0 push_golang_1_20_0 push_hugo push_php_8_2_apache push_nextjs_deno_2_0_0 push_nodejs_23_1_0 push_nodejs_24_0_2 push_deno_2_2_0
+push: push_golang_1_22_0 push_golang_1_20_0 push_hugo push_php_8_2_apache push_nextjs_deno_2_0_0 push_nodejs_23_1_0 push_nodejs_24_0_2 push_deno_2_2_0 push_sketch
diff --git a/apps/app-runner/main.go b/apps/app-runner/main.go
index 96a647e..ebd0ef8 100644
--- a/apps/app-runner/main.go
+++ b/apps/app-runner/main.go
@@ -21,6 +21,7 @@
 var port = flag.Int("port", 3000, "Port to listen on")
 var appId = flag.String("app-id", "", "Application ID")
 var service = flag.String("service", "", "Service name")
+var agentMode = flag.Bool("agent-mode", false, "Sketch agent mode")
 var repoAddr = flag.String("repo-addr", "", "Git repository address")
 var branch = flag.String("branch", "", "Name of the branch to process")
 var rootDir = flag.String("root-dir", "/", "Path to the app code")
@@ -104,8 +105,10 @@
 			panic(err)
 		}
 	}
-	if err := os.Mkdir(*appDir, os.ModePerm); err != nil {
-		panic(err)
+	if !*agentMode {
+		if err := os.Mkdir(*appDir, os.ModePerm); err != nil {
+			panic(err)
+		}
 	}
 	r, err := os.Open(*runCfg)
 	if err != nil {
@@ -116,7 +119,7 @@
 	if err := json.NewDecoder(r).Decode(&cmds); err != nil {
 		panic(err)
 	}
-	s := NewServer(*port, *appId, *service, id, *repoAddr, *branch, *rootDir, signer, *appDir, cmds, self, *managerAddr)
+	s := NewServer(*agentMode, *port, *appId, *service, id, *repoAddr, *branch, *rootDir, signer, *appDir, cmds, self, *managerAddr)
 	if err := s.Start(); err != nil {
 		log.Fatal(err)
 	}
diff --git a/apps/app-runner/server.go b/apps/app-runner/server.go
index 1f7fe01..89f621a 100644
--- a/apps/app-runner/server.go
+++ b/apps/app-runner/server.go
@@ -31,6 +31,7 @@
 
 type Server struct {
 	l           sync.Locker
+	agentMode   bool
 	port        int
 	appId       string
 	service     string
@@ -50,9 +51,10 @@
 	status      *Status
 }
 
-func NewServer(port int, appId, service, id, repoAddr, branch, rootDir string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
+func NewServer(agentMode bool, port int, appId, service, id, repoAddr, branch, rootDir string, signer ssh.Signer, appDir string, runCommands []Command, self string, manager string) *Server {
 	return &Server{
 		l:           &sync.Mutex{},
+		agentMode:   agentMode,
 		port:        port,
 		ready:       false,
 		appId:       appId,
@@ -114,30 +116,80 @@
 	s.l.Unlock()
 }
 
+type command struct {
+	cmd string
+	env []string
+}
+
 func (s *Server) run() error {
-	newDir, err := os.MkdirTemp(s.appDir, "code-*")
-	if err != nil {
-		return err
-	}
-	commit, err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir)
-	if err != nil {
-		fmt.Fprintf(s.logs, "!!! dodo: Failed to clone repository\n")
-		s.status = &Status{
-			Commit: nil,
+	newDir := s.appDir
+	commands := []command{}
+	if !s.agentMode {
+		var err error
+		newDir, err = os.MkdirTemp(s.appDir, "code-*")
+		if err != nil {
+			return err
 		}
-		return err
 	}
-	fmt.Fprintf(s.logs, "!!! dodo: Successfully cloned repository %s\n", commit.Hash)
-	s.status = &Status{
-		Commit:   commit,
-		Commands: []CommandStatus{},
+	if s.repoAddr != "" {
+		commit, err := CloneRepositoryBranch(s.repoAddr, s.branch, s.rootDir, s.signer, newDir)
+		if err != nil {
+			fmt.Fprintf(s.logs, "!!! dodo: Failed to clone repository\n")
+			s.status = &Status{
+				Commit: nil,
+			}
+			return err
+		}
+		fmt.Fprintf(s.logs, "!!! dodo: Successfully cloned repository %s\n", commit.Hash)
+		s.status = &Status{
+			Commit:   commit,
+			Commands: []CommandStatus{},
+		}
+	} else {
+		s.status = &Status{
+			Commit:   nil,
+			Commands: []CommandStatus{},
+		}
 	}
-	commands := []string{}
+	if s.agentMode {
+		if _, err := os.Stat(filepath.Join(newDir, ".git")); err != nil && os.IsNotExist(err) {
+			commands = append(commands, command{cmd: "git config --global user.name dodo"})
+			s.status.Commands = append(s.status.Commands, CommandStatus{
+				Command: commands[len(commands)-1].cmd,
+				State:   "waiting",
+			})
+			commands = append(commands, command{cmd: "git config --global user.email dodo@dodo.cloud"})
+			s.status.Commands = append(s.status.Commands, CommandStatus{
+				Command: commands[len(commands)-1].cmd,
+				State:   "waiting",
+			})
+			commands = append(commands, command{cmd: "git init ."})
+			s.status.Commands = append(s.status.Commands, CommandStatus{
+				Command: commands[len(commands)-1].cmd,
+				State:   "waiting",
+			})
+			commands = append(commands, command{cmd: "echo \"TODO: Describe project\" > README.md"})
+			s.status.Commands = append(s.status.Commands, CommandStatus{
+				Command: commands[len(commands)-1].cmd,
+				State:   "waiting",
+			})
+			commands = append(commands, command{cmd: "git add README.md"})
+			s.status.Commands = append(s.status.Commands, CommandStatus{
+				Command: commands[len(commands)-1].cmd,
+				State:   "waiting",
+			})
+			commands = append(commands, command{cmd: "git commit -m \"init\""})
+			s.status.Commands = append(s.status.Commands, CommandStatus{
+				Command: commands[len(commands)-1].cmd,
+				State:   "waiting",
+			})
+		}
+	}
 	for _, c := range s.runCommands {
 		args := []string{c.Bin}
 		args = append(args, c.Args...)
 		cmd := strings.Join(args, " ")
-		commands = append(commands, cmd)
+		commands = append(commands, command{cmd, c.Env})
 		s.status.Commands = append(s.status.Commands, CommandStatus{
 			Command: cmd,
 			State:   "waiting",
@@ -151,8 +203,8 @@
 		cmd := &exec.Cmd{
 			Dir:    filepath.Join(newDir, s.rootDir),
 			Path:   "/bin/sh",
-			Args:   []string{"/bin/sh", "-c", c},
-			Env:    append(os.Environ(), s.runCommands[i].Env...),
+			Args:   []string{"/bin/sh", "-c", c.cmd},
+			Env:    append(os.Environ(), c.env...),
 			Stdout: logM,
 			Stderr: logM,
 		}
@@ -160,7 +212,7 @@
 		fmt.Printf("Running: %s\n", c)
 		fmt.Fprintf(s.logs, "!!! dodo: Running: %s\n", c)
 		s.status.Commands[i].State = "running"
-		if i < len(s.runCommands)-1 {
+		if i < len(commands)-1 {
 			if err := cmd.Run(); err != nil {
 				return err
 			}
@@ -170,7 +222,7 @@
 				if err := s.kill(); err != nil {
 					return err
 				}
-				if s.currDir != "" {
+				if s.currDir != "" && !s.agentMode {
 					if err := os.RemoveAll(s.currDir); err != nil {
 						return err
 					}