Skip to content

Commit a7379e1

Browse files
authored
feat(core): allow empty docker-git create workspaces (#80)
1 parent da0bd31 commit a7379e1

6 files changed

Lines changed: 32 additions & 19 deletions

File tree

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# docker-git
22

3-
`docker-git` generates a disposable Docker development environment per repository and stores it under a single projects root (default: `~/.docker-git`).
3+
`docker-git` generates a disposable Docker development environment per repository (or empty workspace) and stores it under a single projects root (default: `~/.docker-git`).
44

55
Key goals:
66
- Functional Core, Imperative Shell implementation (pure templates + typed orchestration).
@@ -18,6 +18,9 @@ pnpm install
1818
# Interactive TUI menu (default)
1919
pnpm run docker-git
2020

21+
# Create an empty workspace container (no git clone)
22+
pnpm run docker-git create
23+
2124
# Clone a repo into its own container (creates under ~/.docker-git)
2225
pnpm run docker-git clone https://github.com/agiens/crm/tree/vova-fork --force
2326

packages/app/src/docker-git/cli/usage.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Match } from "effect"
33
import type { ParseError } from "@effect-template/lib/core/domain"
44

55
export const usageText = `docker-git menu
6-
docker-git create --repo-url <url> [options]
6+
docker-git create [--repo-url <url>] [options]
77
docker-git clone <url> [options]
88
docker-git apply [<url>] [options]
99
docker-git mcp-playwright [<url>] [options]
@@ -20,7 +20,7 @@ docker-git state <action> [options]
2020
2121
Commands:
2222
menu Interactive menu (default when no args)
23-
create, init Generate docker development environment
23+
create, init Generate docker development environment (repo URL optional)
2424
clone Create + run container and clone repo
2525
apply Apply docker-git config to an existing project/container (current dir by default)
2626
mcp-playwright Enable Playwright MCP + Chromium sidecar for an existing project dir
@@ -34,6 +34,7 @@ Commands:
3434
state Manage docker-git state directory via git (sync across machines)
3535
3636
Options:
37+
--repo-url <url> Repository URL (create: optional; clone: required via positional arg or flag)
3738
--repo-ref <ref> Git ref/branch (default: main)
3839
--branch, -b <ref> Alias for --repo-ref
3940
--target-dir <path> Target dir inside container (create default: /home/dev/app, clone default: ~/workspaces/<org>/<repo>[/issue-<id>|/pr-<id>])

packages/app/src/docker-git/menu-create.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,16 @@ type CreateReturnContext = CreateContext & {
4646
}
4747

4848
export const buildCreateArgs = (input: CreateInputs): ReadonlyArray<string> => {
49-
const args: Array<string> = ["create", "--repo-url", input.repoUrl]
49+
const args: Array<string> = ["create"]
50+
if (input.repoUrl.length > 0) {
51+
args.push("--repo-url", input.repoUrl)
52+
}
5053
if (input.repoRef.length > 0) {
5154
args.push("--repo-ref", input.repoRef)
5255
}
53-
args.push("--out-dir", input.outDir)
56+
if (input.outDir.length > 0) {
57+
args.push("--out-dir", input.outDir)
58+
}
5459
if (!input.runUp) {
5560
args.push("--no-up")
5661
}
@@ -106,8 +111,8 @@ export const resolveCreateInputs = (
106111
values: Partial<CreateInputs>
107112
): CreateInputs => {
108113
const repoUrl = values.repoUrl ?? ""
109-
const resolvedRepoRef = repoUrl.length > 0 ? resolveRepoInput(repoUrl).repoRef : undefined
110-
const outDir = values.outDir ?? (repoUrl.length > 0 ? resolveDefaultOutDir(cwd, repoUrl) : "")
114+
const resolvedRepoRef = resolveRepoInput(repoUrl).repoRef
115+
const outDir = values.outDir ?? resolveDefaultOutDir(cwd, repoUrl)
111116

112117
return {
113118
repoUrl,
@@ -179,10 +184,6 @@ const applyCreateStep = (input: {
179184
}): boolean =>
180185
Match.value(input.step).pipe(
181186
Match.when("repoUrl", () => {
182-
if (input.buffer.length === 0) {
183-
input.setMessage("Repo URL is required.")
184-
return false
185-
}
186187
input.nextValues.repoUrl = input.buffer
187188
input.nextValues.outDir = resolveDefaultOutDir(input.cwd, input.buffer)
188189
return true
@@ -222,11 +223,6 @@ const finalizeCreateFlow = (input: {
222223
readonly setActiveDir: (dir: string | null) => void
223224
}) => {
224225
const inputs = resolveCreateInputs(input.state.cwd, input.nextValues)
225-
if (inputs.repoUrl.length === 0) {
226-
input.setMessage("Repo URL is required.")
227-
return
228-
}
229-
230226
const parsed = parseArgs(buildCreateArgs(inputs))
231227
if (Either.isLeft(parsed)) {
232228
input.setMessage(formatParseError(parsed.left))

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import { createSteps, menuItems } from "./menu-types.js"
2828

2929
export const renderStepLabel = (step: CreateStep, defaults: CreateInputs): string =>
3030
Match.value(step).pipe(
31-
Match.when("repoUrl", () => "Repo URL"),
31+
Match.when("repoUrl", () => "Repo URL (optional for empty workspace)"),
3232
Match.when("repoRef", () => `Repo ref [${defaults.repoRef}]`),
3333
Match.when("outDir", () => `Output dir [${defaults.outDir}]`),
3434
Match.when("runUp", () => `Run docker compose up now? [${defaults.runUp ? "Y" : "n"}]`),

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,20 @@ describe("parseArgs", () => {
101101
expect(command.config.volumeName).toBe("dg-repo-issue-9-home")
102102
}))
103103

104-
it.effect("fails on missing repo url", () => expectParseErrorTag(["create"], "MissingRequiredOption"))
104+
it.effect("parses create command without repo url into empty workspace defaults", () =>
105+
expectCreateCommand(["create"], (command) => {
106+
expect(command.config.repoUrl).toBe("")
107+
expect(command.config.repoRef).toBe(defaultTemplateConfig.repoRef)
108+
expect(command.outDir).toBe(".docker-git/app")
109+
expect(command.openSsh).toBe(false)
110+
expect(command.waitForClone).toBe(false)
111+
expect(command.config.containerName).toBe("dg-app")
112+
expect(command.config.serviceName).toBe("dg-app")
113+
expect(command.config.volumeName).toBe("dg-app-home")
114+
expect(command.config.targetDir).toBe(expandDefaultTargetDir(defaultTemplateConfig.targetDir))
115+
}))
116+
117+
it.effect("fails clone when repo url is missing", () => expectParseErrorTag(["clone"], "MissingRequiredOption"))
105118

106119
it.effect("parses clone command with positional repo url", () =>
107120
expectCreateCommand(["clone", "https://github.com/org/repo.git"], (command) => {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ type RepoBasics = {
7777

7878
const resolveRepoBasics = (raw: RawOptions): Either.Either<RepoBasics, ParseError> =>
7979
Either.gen(function*(_) {
80-
const rawRepoUrl = yield* _(nonEmpty("--repo-url", raw.repoUrl))
80+
const rawRepoUrl = raw.repoUrl?.trim() ?? ""
8181
const resolvedRepo = resolveRepoInput(rawRepoUrl)
8282
const repoUrl = resolvedRepo.repoUrl
8383
const repoSlug = deriveRepoSlug(repoUrl)

0 commit comments

Comments
 (0)