Canvas: Add VM/PROXY dev modes support

- Update ServiceSchema to discriminate between VM and PROXY dev modes
- Add DevDisabled, DevVM, DevProxy TypeScript types
- Update ServiceData type in graph.ts for new dev structure
- Update generateDodoConfig to handle both VM and PROXY modes
- Update configToGraph to properly convert dev configurations
- Maintain backward compatibility with existing dev configurations
- Update UI and introduce two new DevVM and DevProxy components
- Fetch user machine list from headscale API

Change-Id: I8f9df4ab9bd34c049fffadb748115335e8260a54
diff --git a/apps/canvas/config/src/config.ts b/apps/canvas/config/src/config.ts
index f5957d4..7fd7066 100644
--- a/apps/canvas/config/src/config.ts
+++ b/apps/canvas/config/src/config.ts
@@ -112,25 +112,35 @@
 						? n.data.preBuildCommands.split("\n").map((cmd) => ({ bin: cmd }))
 						: [],
 					dev: n.data.dev?.enabled
-						? {
-								enabled: true,
-								mode: "VM",
-								username: env.user.username,
-								codeServer:
-									n.data.dev.expose != null
-										? {
-												network: networkMap.get(n.data.dev.expose.network)!,
-												subdomain: n.data.dev.expose.subdomain,
-											}
-										: undefined,
-								ssh:
-									n.data.dev.expose != null
-										? {
-												network: networkMap.get(n.data.dev.expose.network)!,
-												subdomain: n.data.dev.expose.subdomain,
-											}
-										: undefined,
-							}
+						? n.data.dev.mode === "VM"
+							? {
+									enabled: true,
+									mode: "VM",
+									username: env.user.username,
+									codeServer:
+										n.data.dev.expose != null
+											? {
+													network: networkMap.get(n.data.dev.expose.network)!,
+													subdomain: n.data.dev.expose.subdomain,
+												}
+											: undefined,
+									ssh:
+										n.data.dev.expose != null
+											? {
+													network: networkMap.get(n.data.dev.expose.network)!,
+													subdomain: n.data.dev.expose.subdomain,
+												}
+											: undefined,
+								}
+							: {
+									enabled: true,
+									mode: "PROXY",
+									address: n.data.dev.address,
+									vpn: {
+										enabled: true,
+										username: env.user.username,
+									},
+								}
 						: {
 								enabled: false,
 							},
@@ -311,7 +321,28 @@
 							? { name: "gemini", apiKey: s.model.geminiApiKey }
 							: { name: "claude", apiKey: s.model.anthropicApiKey },
 				}),
-				// TODO(gio): dev
+				dev: s.dev?.enabled
+					? s.dev.mode === "VM"
+						? {
+								enabled: true,
+								mode: "VM",
+								expose: s.dev.ssh
+									? {
+											network: s.dev.ssh.network,
+											subdomain: s.dev.ssh.subdomain,
+										}
+									: undefined,
+								codeServerNodeId: uuidv4(), // TODO: proper node tracking
+								sshNodeId: uuidv4(), // TODO: proper node tracking
+							}
+						: {
+								enabled: true,
+								mode: "PROXY",
+								address: s.dev.address,
+							}
+					: {
+							enabled: false,
+						},
 				isChoosingPortToConnect: false,
 			},
 			// TODO(gio): generate position
diff --git a/apps/canvas/config/src/graph.ts b/apps/canvas/config/src/graph.ts
index a4634cb..fc3258e 100644
--- a/apps/canvas/config/src/graph.ts
+++ b/apps/canvas/config/src/graph.ts
@@ -210,9 +210,15 @@
 		  }
 		| {
 				enabled: true;
+				mode: "VM";
 				expose?: Domain;
 				codeServerNodeId: string;
 				sshNodeId: string;
+		  }
+		| {
+				enabled: true;
+				mode: "PROXY";
+				address: string;
 		  };
 	model?: {
 		name: "gemini" | "claude";
@@ -496,17 +502,23 @@
 				preBuildCommands: z.string().optional(),
 				isChoosingPortToConnect: z.boolean().optional(),
 				dev: z
-					.discriminatedUnion("enabled", [
+					.union([
 						z.object({
 							enabled: z.literal(false),
 							expose: DomainSchema.optional(),
 						}),
 						z.object({
 							enabled: z.literal(true),
+							mode: z.literal("VM"),
 							expose: DomainSchema.optional(),
 							codeServerNodeId: z.string(),
 							sshNodeId: z.string(),
 						}),
+						z.object({
+							enabled: z.literal(true),
+							mode: z.literal("PROXY"),
+							address: z.string(),
+						}),
 					])
 					.optional(),
 				model: z
diff --git a/apps/canvas/config/src/index.ts b/apps/canvas/config/src/index.ts
index f6cc9a5..df1be47 100644
--- a/apps/canvas/config/src/index.ts
+++ b/apps/canvas/config/src/index.ts
@@ -7,6 +7,10 @@
 	ConfigWithInputSchema,
 	Domain,
 	Ingress,
+	Machine,
+	MachineSchema,
+	Machines,
+	MachinesSchema,
 	MongoDB,
 	PortDomain,
 	PortValue,
diff --git a/apps/canvas/config/src/types.ts b/apps/canvas/config/src/types.ts
index a49c2d5..4146f75 100644
--- a/apps/canvas/config/src/types.ts
+++ b/apps/canvas/config/src/types.ts
@@ -98,15 +98,26 @@
 	expose: z.array(PortDomainSchema).optional(),
 	volume: z.array(z.string()).optional(),
 	preBuildCommands: z.array(z.object({ bin: z.string() })).optional(),
-	dev: z.discriminatedUnion("enabled", [
+	dev: z.union([
 		z.object({ enabled: z.literal(false) }),
 		z.object({
 			enabled: z.literal(true),
-			mode: z.string(),
+			mode: z.literal("VM"),
 			username: z.string().optional(),
 			ssh: DomainSchema.optional(),
 			codeServer: DomainSchema.optional(),
 		}),
+		z.object({
+			enabled: z.literal(true),
+			mode: z.literal("PROXY"),
+			address: z.string(),
+			vpn: z
+				.discriminatedUnion("enabled", [
+					z.object({ enabled: z.literal(false) }),
+					z.object({ enabled: z.literal(true), username: z.string() }),
+				])
+				.optional(),
+		}),
 	]),
 	model: ModelSchema.optional(),
 });
@@ -139,6 +150,12 @@
 	expose: z.array(DomainSchema).optional(),
 });
 
+export const MachineSchema = z.object({
+	name: z.string(),
+});
+
+export const MachinesSchema = z.array(MachineSchema);
+
 export const ConfigSchema = z.object({
 	service: z.array(ServiceSchema).optional(),
 	agent: z.array(AgentSchema).optional(),
@@ -180,5 +197,23 @@
 export type MongoDB = z.infer<typeof MongoDBSchema>;
 export type Config = z.infer<typeof ConfigSchema>;
 export type ConfigWithInput = z.infer<typeof ConfigWithInputSchema>;
+export type Machine = z.infer<typeof MachineSchema>;
+export type Machines = z.infer<typeof MachinesSchema>;
+
+// Dev configuration types
+export type DevDisabled = { enabled: false };
+export type DevVM = {
+	enabled: true;
+	mode: "VM";
+	username?: string;
+	ssh?: Domain;
+	codeServer?: Domain;
+};
+export type DevProxy = {
+	enabled: true;
+	mode: "PROXY";
+	address: string;
+};
+export type Dev = DevDisabled | DevVM | DevProxy;
 
 export const isAgent = (s: Service): s is Agent => s.type === "sketch:latest";