diff --git a/packages/api/src/api/contracts.ts b/packages/api/src/api/contracts.ts index 9a3f0d60..76041536 100644 --- a/packages/api/src/api/contracts.ts +++ b/packages/api/src/api/contracts.ts @@ -25,6 +25,7 @@ export type ProjectDetails = ProjectSummary & { readonly envProjectPath: string readonly codexAuthPath: string readonly codexHome: string + readonly clonedOnHostname?: string | undefined } export type CreateProjectRequest = { diff --git a/packages/api/src/services/projects.ts b/packages/api/src/services/projects.ts index c91f7e4c..0afa1be8 100644 --- a/packages/api/src/services/projects.ts +++ b/packages/api/src/services/projects.ts @@ -114,7 +114,8 @@ const toProjectDetails = ( envGlobalPath: project.envGlobalPath, envProjectPath: project.envProjectPath, codexAuthPath: project.codexAuthPath, - codexHome: project.codexHome + codexHome: project.codexHome, + clonedOnHostname: project.clonedOnHostname }) const findProjectById = (projectId: string) => diff --git a/packages/app/src/docker-git/menu-render-select.ts b/packages/app/src/docker-git/menu-render-select.ts index 17bef476..daa1aef7 100644 --- a/packages/app/src/docker-git/menu-render-select.ts +++ b/packages/app/src/docker-git/menu-render-select.ts @@ -90,11 +90,12 @@ export const buildSelectLabels = ( items.map((item, index) => { const prefix = index === selected ? ">" : " " const refLabel = formatRepoRef(item.repoRef) + const hostLabel = item.clonedOnHostname === undefined ? "" : ` @${item.clonedOnHostname}` const runtime = runtimeForProject(runtimeByProject, item) const runtimeSuffix = purpose === "Down" || purpose === "Delete" ? ` [${renderRuntimeLabel(runtime)}]` : ` [started=${renderStartedAtCompact(runtime)}]` - return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${runtimeSuffix}` + return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${hostLabel}${runtimeSuffix}` }) export type SelectListWindow = { diff --git a/packages/app/tests/docker-git/parser.test.ts b/packages/app/tests/docker-git/parser.test.ts index 419855c4..22fc26be 100644 --- a/packages/app/tests/docker-git/parser.test.ts +++ b/packages/app/tests/docker-git/parser.test.ts @@ -36,6 +36,8 @@ describe("parseArgs", () => { expect(command.config.serviceName).toBe("dg-repo") expect(command.config.volumeName).toBe("dg-repo-home") expect(command.config.sshPort).toBe(defaultTemplateConfig.sshPort) + expect(typeof command.config.clonedOnHostname).toBe("string") + expect(String(command.config.clonedOnHostname).length).toBeGreaterThan(0) })) it.effect("parses create resource limit flags", () => diff --git a/packages/lib/src/core/command-builders.ts b/packages/lib/src/core/command-builders.ts index 71a83d17..74d73501 100644 --- a/packages/lib/src/core/command-builders.ts +++ b/packages/lib/src/core/command-builders.ts @@ -1,4 +1,5 @@ import { Either } from "effect" +import { hostname } from "node:os" import { expandContainerHome } from "../usecases/scrap-path.js" import { resolveAutoAgentFlags } from "./auto-agent-flags.js" @@ -198,12 +199,14 @@ type BuildTemplateConfigInput = { readonly enableMcpPlaywright: boolean readonly agentMode: AgentMode | undefined readonly agentAuto: boolean + readonly clonedOnHostname: string } const buildTemplateConfig = ({ agentAuto, agentMode, claudeAuthLabel, + clonedOnHostname, codexAuthLabel, cpuLimit, dockerNetworkMode, @@ -242,7 +245,8 @@ const buildTemplateConfig = ({ enableMcpPlaywright, pnpmVersion: defaultTemplateConfig.pnpmVersion, agentMode, - agentAuto + agentAuto, + clonedOnHostname }) // CHANGE: build a typed create command from raw options (CLI or API) @@ -295,7 +299,8 @@ export const buildCreateCommand = ( claudeAuthLabel, enableMcpPlaywright: behavior.enableMcpPlaywright, agentMode, - agentAuto + agentAuto, + clonedOnHostname: hostname() }) } }) diff --git a/packages/lib/src/core/domain.ts b/packages/lib/src/core/domain.ts index 95ebe056..15abf7ce 100644 --- a/packages/lib/src/core/domain.ts +++ b/packages/lib/src/core/domain.ts @@ -47,6 +47,7 @@ export interface TemplateConfig { readonly pnpmVersion: string readonly agentMode?: AgentMode | undefined readonly agentAuto?: boolean | undefined + readonly clonedOnHostname?: string | undefined } export interface ProjectConfig { diff --git a/packages/lib/src/shell/config.ts b/packages/lib/src/shell/config.ts index dccd173b..ea499e37 100644 --- a/packages/lib/src/shell/config.ts +++ b/packages/lib/src/shell/config.ts @@ -59,7 +59,8 @@ const TemplateConfigSchema = Schema.Struct({ enableMcpPlaywright: Schema.optionalWith(Schema.Boolean, { default: () => defaultTemplateConfig.enableMcpPlaywright }), - pnpmVersion: Schema.String + pnpmVersion: Schema.String, + clonedOnHostname: Schema.optional(Schema.String) }) const ProjectConfigSchema = Schema.Struct({ diff --git a/packages/lib/src/usecases/menu-helpers.ts b/packages/lib/src/usecases/menu-helpers.ts index 16735da1..d745d5ad 100644 --- a/packages/lib/src/usecases/menu-helpers.ts +++ b/packages/lib/src/usecases/menu-helpers.ts @@ -16,23 +16,28 @@ export const formatConnectionInfo = ( authorizedKeysPath: string, authorizedKeysExists: boolean, sshCommand: string -): string => - `Project directory: ${cwd} +): string => { + const hostnameLabel = config.template.clonedOnHostname === undefined + ? "" + : `\nCloned on device: ${config.template.clonedOnHostname}` + return `Project directory: ${cwd} ` + - `Container: ${config.template.containerName} + `Container: ${config.template.containerName} ` + - `Service: ${config.template.serviceName} + `Service: ${config.template.serviceName} ` + - `SSH command: ${sshCommand} + `SSH command: ${sshCommand} ` + - `Repo: ${config.template.repoUrl} (${config.template.repoRef}) + `Repo: ${config.template.repoUrl} (${config.template.repoRef}) ` + - `Workspace: ${config.template.targetDir} + `Workspace: ${config.template.targetDir} ` + - `Authorized keys: ${authorizedKeysPath}${authorizedKeysExists ? "" : " (missing)"} + `Authorized keys: ${authorizedKeysPath}${authorizedKeysExists ? "" : " (missing)"} ` + - `Env global: ${config.template.envGlobalPath} + `Env global: ${config.template.envGlobalPath} ` + - `Env project: ${config.template.envProjectPath} + `Env project: ${config.template.envProjectPath} ` + - `Codex auth: ${config.template.codexAuthPath} -> ${config.template.codexHome}` + `Codex auth: ${config.template.codexAuthPath} -> ${config.template.codexHome}` + + hostnameLabel +} diff --git a/packages/lib/src/usecases/projects-core.ts b/packages/lib/src/usecases/projects-core.ts index aa57a2f8..786d5f83 100644 --- a/packages/lib/src/usecases/projects-core.ts +++ b/packages/lib/src/usecases/projects-core.ts @@ -60,6 +60,7 @@ export type ProjectItem = { readonly envProjectPath: string readonly codexAuthPath: string readonly codexHome: string + readonly clonedOnHostname?: string | undefined } export type ProjectStatus = { @@ -203,7 +204,8 @@ export const loadProjectItem = ( envGlobalPath: resolvePathFromCwd(path, projectDir, template.envGlobalPath), envProjectPath: resolvePathFromCwd(path, projectDir, template.envProjectPath), codexAuthPath: resolvePathFromCwd(path, projectDir, template.codexAuthPath), - codexHome: template.codexHome + codexHome: template.codexHome, + clonedOnHostname: template.clonedOnHostname } }) diff --git a/packages/lib/tests/usecases/connection-info.test.ts b/packages/lib/tests/usecases/connection-info.test.ts new file mode 100644 index 00000000..e6c84142 --- /dev/null +++ b/packages/lib/tests/usecases/connection-info.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "@effect/vitest" + +import type { ProjectConfig } from "../../src/core/domain.js" +import { defaultTemplateConfig } from "../../src/core/domain.js" +import { formatConnectionInfo } from "../../src/usecases/menu-helpers.js" + +const makeProjectConfig = (overrides: Partial = {}): ProjectConfig => ({ + schemaVersion: 1, + template: { + ...defaultTemplateConfig, + repoUrl: "https://github.com/org/repo.git", + containerName: "dg-test", + serviceName: "dg-test", + sshUser: "dev", + targetDir: "/home/dev/org/repo", + volumeName: "dg-test-home", + dockerGitPath: "/workspace/.docker-git", + authorizedKeysPath: "/workspace/authorized_keys", + envGlobalPath: "/workspace/.orch/env/global.env", + envProjectPath: "/workspace/.orch/env/project.env", + codexAuthPath: "/workspace/.orch/auth/codex", + codexSharedAuthPath: "/workspace/.orch/auth/codex-shared", + geminiAuthPath: "/workspace/.orch/auth/gemini", + ...overrides + } +}) + +describe("formatConnectionInfo", () => { + it("includes clonedOnHostname when present", () => { + const config = makeProjectConfig({ clonedOnHostname: "my-laptop" }) + const output = formatConnectionInfo("/project", config, "/keys", true, "ssh dev@localhost") + expect(output).toContain("Cloned on device: my-laptop") + }) + + it("omits clonedOnHostname line when undefined", () => { + const config = makeProjectConfig() + const output = formatConnectionInfo("/project", config, "/keys", true, "ssh dev@localhost") + expect(output).not.toContain("Cloned on device") + }) +})