Canvas: Process pre-build commands

Change-Id: I236f154c430b9ea29a4e0e491e1de27e78438440
diff --git a/apps/canvas/front/src/components/node-app.tsx b/apps/canvas/front/src/components/node-app.tsx
index 3598a4d..3eea939 100644
--- a/apps/canvas/front/src/components/node-app.tsx
+++ b/apps/canvas/front/src/components/node-app.tsx
@@ -12,6 +12,7 @@
 import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "./ui/select";
 import { PencilIcon, XIcon } from "lucide-react";
 import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "./ui/tooltip";
+import { Textarea } from "./ui/textarea";
 
 export function NodeApp(node: ServiceNode) {
   const { id, selected } = node;
@@ -270,6 +271,11 @@
       envVars: (data.envVars || []).filter((ev) => !(ev.source === null && "portId" in ev && ev.portId === portId)),
     });
   }, [id, data, store]);
+  const setPreBuildCommands = useCallback((e: React.ChangeEvent<HTMLTextAreaElement>) => {
+    store.updateNodeData<"app">(id, {
+      preBuildCommands: e.currentTarget.value,
+    });
+  }, [id, store]);
   return (
     <>
       <Form {...form}>
@@ -368,5 +374,7 @@
           }
         })}
       </ul>
+      Pre-Build Commands
+      <Textarea placeholder="new line separated list of commands to run before running the service" value={data.preBuildCommands} onChange={setPreBuildCommands} />
     </>);
 }
\ No newline at end of file
diff --git a/apps/canvas/front/src/components/ui/textarea.tsx b/apps/canvas/front/src/components/ui/textarea.tsx
new file mode 100644
index 0000000..e56b0af
--- /dev/null
+++ b/apps/canvas/front/src/components/ui/textarea.tsx
@@ -0,0 +1,22 @@
+import * as React from "react"
+
+import { cn } from "@/lib/utils"
+
+const Textarea = React.forwardRef<
+  HTMLTextAreaElement,
+  React.ComponentProps<"textarea">
+>(({ className, ...props }, ref) => {
+  return (
+    <textarea
+      className={cn(
+        "flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
+        className
+      )}
+      ref={ref}
+      {...props}
+    />
+  )
+})
+Textarea.displayName = "Textarea"
+
+export { Textarea }
diff --git a/apps/canvas/front/src/lib/config.ts b/apps/canvas/front/src/lib/config.ts
index f8d82ed..3eed93e 100644
--- a/apps/canvas/front/src/lib/config.ts
+++ b/apps/canvas/front/src/lib/config.ts
@@ -54,6 +54,7 @@
     ingress?: Ingress[];
     expose?: PortDomain[];
     volume?: string[];
+    preBuildCommands?: string[];
 };
 
 export type Volume = {
@@ -123,6 +124,7 @@
                         auth: { enabled: false },
                     })),
                     expose: findExpose(n),
+                    preBuildCommands: [n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))],
                 };
             }),
             volume: nodes.filter((n) => n.type === "volume").map((n): Volume => ({
diff --git a/apps/canvas/front/src/lib/state.ts b/apps/canvas/front/src/lib/state.ts
index 8d728d0..664ba6e 100644
--- a/apps/canvas/front/src/lib/state.ts
+++ b/apps/canvas/front/src/lib/state.ts
@@ -67,7 +67,16 @@
   value: number;
 };
 
-export const ServiceTypes = ["node-23.1.0", "nextjs:deno-2.0.0"] as const;
+export const ServiceTypes = [
+  "deno:2.2.0",
+  "golang:1.20.0",
+  "golang:1.22.0",
+  "golang:1.24.0",
+  "hugo:latest",
+  "php:8.2-apache",
+  "nextjs:deno-2.0.0", 
+  "node-23.1.0"
+] as const;
 export type ServiceType = typeof ServiceTypes[number];
 
 export type ServiceData = NodeData & {
@@ -79,6 +88,7 @@
   };
   env: string[];
   volume: string[];
+  preBuildCommands: string;
   isChoosingPortToConnect: boolean;
 };