From 3391323e1504737f3e4cd0436f3a659d3f717585 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 24 Mar 2026 07:37:22 +0000 Subject: [PATCH 1/4] Initial commit with task details Adding .gitkeep for PR creation (default mode). This file will be removed when the task is complete. Issue: https://github.com/ProverCoderAI/docker-git/issues/187 --- .gitkeep | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..05e6013 --- /dev/null +++ b/.gitkeep @@ -0,0 +1 @@ +# .gitkeep file auto-generated at 2026-03-24T07:37:21.980Z for PR creation at branch issue-187-5d4f617e5a5a for issue https://github.com/ProverCoderAI/docker-git/issues/187 \ No newline at end of file From cb6ca65ece00dd9c1161c97ace94d52028f3bcae Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 24 Mar 2026 07:48:33 +0000 Subject: [PATCH 2/4] feat(core): record device hostname when cloning repositories Add clonedOnHostname field to TemplateConfig that captures os.hostname() during project creation. This allows users to see which device cloned which repository when working with shared .docker-git state across multiple machines. - Add clonedOnHostname to TemplateConfig interface and schema (optional for backward compat) - Populate hostname in buildCreateCommand via os.hostname() - Display "Cloned on device: " in connection info and project list - Surface clonedOnHostname in API ProjectDetails - Add tests for hostname recording and display Fixes ProverCoderAI/docker-git#187 Co-Authored-By: Claude Opus 4.6 --- packages/api/src/api/contracts.ts | 1 + packages/api/src/services/projects.ts | 3 +- .../app/src/docker-git/menu-render-select.ts | 3 +- packages/app/tests/docker-git/parser.test.ts | 2 + packages/lib/src/core/command-builders.ts | 9 ++++- packages/lib/src/core/domain.ts | 1 + packages/lib/src/shell/config.ts | 3 +- packages/lib/src/usecases/menu-helpers.ts | 27 ++++++++----- packages/lib/src/usecases/projects-core.ts | 4 +- .../tests/usecases/connection-info.test.ts | 40 +++++++++++++++++++ 10 files changed, 76 insertions(+), 17 deletions(-) create mode 100644 packages/lib/tests/usecases/connection-info.test.ts diff --git a/packages/api/src/api/contracts.ts b/packages/api/src/api/contracts.ts index 9a3f0d6..7604153 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 c91f7e4..0afa1be 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 17bef47..daa1aef 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 419855c..9c62b15 100644 --- a/packages/app/tests/docker-git/parser.test.ts +++ b/packages/app/tests/docker-git/parser.test.ts @@ -1,5 +1,6 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" +import { hostname } from "node:os" import { defaultTemplateConfig } from "@effect-template/lib/core/domain" import { expandContainerHome } from "@effect-template/lib/usecases/scrap-path" @@ -36,6 +37,7 @@ 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(command.config.clonedOnHostname).toBe(hostname()) })) 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 71a83d1..74d7350 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 95ebe05..15abf7c 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 dccd173..ea499e3 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 16735da..d745d5a 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 aa57a2f..786d5f8 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 0000000..e6c8414 --- /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") + }) +}) From 5a80436df72b371699045ba69509b681403d5175 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 24 Mar 2026 07:49:38 +0000 Subject: [PATCH 3/4] Revert "Initial commit with task details" This reverts commit 3391323e1504737f3e4cd0436f3a659d3f717585. --- .gitkeep | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .gitkeep diff --git a/.gitkeep b/.gitkeep deleted file mode 100644 index 05e6013..0000000 --- a/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# .gitkeep file auto-generated at 2026-03-24T07:37:21.980Z for PR creation at branch issue-187-5d4f617e5a5a for issue https://github.com/ProverCoderAI/docker-git/issues/187 \ No newline at end of file From db785cedadbe15872d4f36fad3b034b06f56de64 Mon Sep 17 00:00:00 2001 From: konard Date: Tue, 24 Mar 2026 07:57:09 +0000 Subject: [PATCH 4/4] fix(app): remove restricted node:os import from parser test Replace direct hostname() assertion with type-safe string check to comply with Effect-TS no-restricted-imports lint rule. Co-Authored-By: Claude Opus 4.6 --- packages/app/tests/docker-git/parser.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/tests/docker-git/parser.test.ts b/packages/app/tests/docker-git/parser.test.ts index 9c62b15..22fc26b 100644 --- a/packages/app/tests/docker-git/parser.test.ts +++ b/packages/app/tests/docker-git/parser.test.ts @@ -1,6 +1,5 @@ import { describe, expect, it } from "@effect/vitest" import { Effect } from "effect" -import { hostname } from "node:os" import { defaultTemplateConfig } from "@effect-template/lib/core/domain" import { expandContainerHome } from "@effect-template/lib/usecases/scrap-path" @@ -37,7 +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(command.config.clonedOnHostname).toBe(hostname()) + expect(typeof command.config.clonedOnHostname).toBe("string") + expect(String(command.config.clonedOnHostname).length).toBeGreaterThan(0) })) it.effect("parses create resource limit flags", () =>