Skip to content

Commit cb6ca65

Browse files
konardclaude
andcommitted
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: <hostname>" in connection info and project list - Surface clonedOnHostname in API ProjectDetails - Add tests for hostname recording and display Fixes #187 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 3391323 commit cb6ca65

10 files changed

Lines changed: 76 additions & 17 deletions

File tree

packages/api/src/api/contracts.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export type ProjectDetails = ProjectSummary & {
2525
readonly envProjectPath: string
2626
readonly codexAuthPath: string
2727
readonly codexHome: string
28+
readonly clonedOnHostname?: string | undefined
2829
}
2930

3031
export type CreateProjectRequest = {

packages/api/src/services/projects.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ const toProjectDetails = (
114114
envGlobalPath: project.envGlobalPath,
115115
envProjectPath: project.envProjectPath,
116116
codexAuthPath: project.codexAuthPath,
117-
codexHome: project.codexHome
117+
codexHome: project.codexHome,
118+
clonedOnHostname: project.clonedOnHostname
118119
})
119120

120121
const findProjectById = (projectId: string) =>

packages/app/src/docker-git/menu-render-select.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,12 @@ export const buildSelectLabels = (
9090
items.map((item, index) => {
9191
const prefix = index === selected ? ">" : " "
9292
const refLabel = formatRepoRef(item.repoRef)
93+
const hostLabel = item.clonedOnHostname === undefined ? "" : ` @${item.clonedOnHostname}`
9394
const runtime = runtimeForProject(runtimeByProject, item)
9495
const runtimeSuffix = purpose === "Down" || purpose === "Delete"
9596
? ` [${renderRuntimeLabel(runtime)}]`
9697
: ` [started=${renderStartedAtCompact(runtime)}]`
97-
return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${runtimeSuffix}`
98+
return `${prefix} ${index + 1}. ${item.displayName} (${refLabel})${hostLabel}${runtimeSuffix}`
9899
})
99100

100101
export type SelectListWindow = {

packages/app/tests/docker-git/parser.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { describe, expect, it } from "@effect/vitest"
22
import { Effect } from "effect"
3+
import { hostname } from "node:os"
34

45
import { defaultTemplateConfig } from "@effect-template/lib/core/domain"
56
import { expandContainerHome } from "@effect-template/lib/usecases/scrap-path"
@@ -36,6 +37,7 @@ describe("parseArgs", () => {
3637
expect(command.config.serviceName).toBe("dg-repo")
3738
expect(command.config.volumeName).toBe("dg-repo-home")
3839
expect(command.config.sshPort).toBe(defaultTemplateConfig.sshPort)
40+
expect(command.config.clonedOnHostname).toBe(hostname())
3941
}))
4042

4143
it.effect("parses create resource limit flags", () =>

packages/lib/src/core/command-builders.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Either } from "effect"
2+
import { hostname } from "node:os"
23

34
import { expandContainerHome } from "../usecases/scrap-path.js"
45
import { resolveAutoAgentFlags } from "./auto-agent-flags.js"
@@ -198,12 +199,14 @@ type BuildTemplateConfigInput = {
198199
readonly enableMcpPlaywright: boolean
199200
readonly agentMode: AgentMode | undefined
200201
readonly agentAuto: boolean
202+
readonly clonedOnHostname: string
201203
}
202204

203205
const buildTemplateConfig = ({
204206
agentAuto,
205207
agentMode,
206208
claudeAuthLabel,
209+
clonedOnHostname,
207210
codexAuthLabel,
208211
cpuLimit,
209212
dockerNetworkMode,
@@ -242,7 +245,8 @@ const buildTemplateConfig = ({
242245
enableMcpPlaywright,
243246
pnpmVersion: defaultTemplateConfig.pnpmVersion,
244247
agentMode,
245-
agentAuto
248+
agentAuto,
249+
clonedOnHostname
246250
})
247251

248252
// CHANGE: build a typed create command from raw options (CLI or API)
@@ -295,7 +299,8 @@ export const buildCreateCommand = (
295299
claudeAuthLabel,
296300
enableMcpPlaywright: behavior.enableMcpPlaywright,
297301
agentMode,
298-
agentAuto
302+
agentAuto,
303+
clonedOnHostname: hostname()
299304
})
300305
}
301306
})

packages/lib/src/core/domain.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ export interface TemplateConfig {
4747
readonly pnpmVersion: string
4848
readonly agentMode?: AgentMode | undefined
4949
readonly agentAuto?: boolean | undefined
50+
readonly clonedOnHostname?: string | undefined
5051
}
5152

5253
export interface ProjectConfig {

packages/lib/src/shell/config.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,8 @@ const TemplateConfigSchema = Schema.Struct({
5959
enableMcpPlaywright: Schema.optionalWith(Schema.Boolean, {
6060
default: () => defaultTemplateConfig.enableMcpPlaywright
6161
}),
62-
pnpmVersion: Schema.String
62+
pnpmVersion: Schema.String,
63+
clonedOnHostname: Schema.optional(Schema.String)
6364
})
6465

6566
const ProjectConfigSchema = Schema.Struct({

packages/lib/src/usecases/menu-helpers.ts

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,23 +16,28 @@ export const formatConnectionInfo = (
1616
authorizedKeysPath: string,
1717
authorizedKeysExists: boolean,
1818
sshCommand: string
19-
): string =>
20-
`Project directory: ${cwd}
19+
): string => {
20+
const hostnameLabel = config.template.clonedOnHostname === undefined
21+
? ""
22+
: `\nCloned on device: ${config.template.clonedOnHostname}`
23+
return `Project directory: ${cwd}
2124
` +
22-
`Container: ${config.template.containerName}
25+
`Container: ${config.template.containerName}
2326
` +
24-
`Service: ${config.template.serviceName}
27+
`Service: ${config.template.serviceName}
2528
` +
26-
`SSH command: ${sshCommand}
29+
`SSH command: ${sshCommand}
2730
` +
28-
`Repo: ${config.template.repoUrl} (${config.template.repoRef})
31+
`Repo: ${config.template.repoUrl} (${config.template.repoRef})
2932
` +
30-
`Workspace: ${config.template.targetDir}
33+
`Workspace: ${config.template.targetDir}
3134
` +
32-
`Authorized keys: ${authorizedKeysPath}${authorizedKeysExists ? "" : " (missing)"}
35+
`Authorized keys: ${authorizedKeysPath}${authorizedKeysExists ? "" : " (missing)"}
3336
` +
34-
`Env global: ${config.template.envGlobalPath}
37+
`Env global: ${config.template.envGlobalPath}
3538
` +
36-
`Env project: ${config.template.envProjectPath}
39+
`Env project: ${config.template.envProjectPath}
3740
` +
38-
`Codex auth: ${config.template.codexAuthPath} -> ${config.template.codexHome}`
41+
`Codex auth: ${config.template.codexAuthPath} -> ${config.template.codexHome}` +
42+
hostnameLabel
43+
}

packages/lib/src/usecases/projects-core.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ export type ProjectItem = {
6060
readonly envProjectPath: string
6161
readonly codexAuthPath: string
6262
readonly codexHome: string
63+
readonly clonedOnHostname?: string | undefined
6364
}
6465

6566
export type ProjectStatus = {
@@ -203,7 +204,8 @@ export const loadProjectItem = (
203204
envGlobalPath: resolvePathFromCwd(path, projectDir, template.envGlobalPath),
204205
envProjectPath: resolvePathFromCwd(path, projectDir, template.envProjectPath),
205206
codexAuthPath: resolvePathFromCwd(path, projectDir, template.codexAuthPath),
206-
codexHome: template.codexHome
207+
codexHome: template.codexHome,
208+
clonedOnHostname: template.clonedOnHostname
207209
}
208210
})
209211

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
import { describe, expect, it } from "@effect/vitest"
2+
3+
import type { ProjectConfig } from "../../src/core/domain.js"
4+
import { defaultTemplateConfig } from "../../src/core/domain.js"
5+
import { formatConnectionInfo } from "../../src/usecases/menu-helpers.js"
6+
7+
const makeProjectConfig = (overrides: Partial<ProjectConfig["template"]> = {}): ProjectConfig => ({
8+
schemaVersion: 1,
9+
template: {
10+
...defaultTemplateConfig,
11+
repoUrl: "https://github.com/org/repo.git",
12+
containerName: "dg-test",
13+
serviceName: "dg-test",
14+
sshUser: "dev",
15+
targetDir: "/home/dev/org/repo",
16+
volumeName: "dg-test-home",
17+
dockerGitPath: "/workspace/.docker-git",
18+
authorizedKeysPath: "/workspace/authorized_keys",
19+
envGlobalPath: "/workspace/.orch/env/global.env",
20+
envProjectPath: "/workspace/.orch/env/project.env",
21+
codexAuthPath: "/workspace/.orch/auth/codex",
22+
codexSharedAuthPath: "/workspace/.orch/auth/codex-shared",
23+
geminiAuthPath: "/workspace/.orch/auth/gemini",
24+
...overrides
25+
}
26+
})
27+
28+
describe("formatConnectionInfo", () => {
29+
it("includes clonedOnHostname when present", () => {
30+
const config = makeProjectConfig({ clonedOnHostname: "my-laptop" })
31+
const output = formatConnectionInfo("/project", config, "/keys", true, "ssh dev@localhost")
32+
expect(output).toContain("Cloned on device: my-laptop")
33+
})
34+
35+
it("omits clonedOnHostname line when undefined", () => {
36+
const config = makeProjectConfig()
37+
const output = formatConnectionInfo("/project", config, "/keys", true, "ssh dev@localhost")
38+
expect(output).not.toContain("Cloned on device")
39+
})
40+
})

0 commit comments

Comments
 (0)