@prover-coder-ai/docker-git 1.0.20 → 1.0.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,22 @@
1
+ import { Either } from "effect";
2
+ import {} from "@effect-template/lib/core/domain";
3
+ import { parseProjectDirWithOptions } from "./parser-shared.js";
4
+ // CHANGE: parse "apply" command for existing docker-git projects
5
+ // WHY: update managed docker-git config on the current project/container without creating a new project
6
+ // QUOTE(ТЗ): "Не создавать новый... а прямо в текущем обновить её на актуальную"
7
+ // REF: issue-72-followup-apply-current-config
8
+ // SOURCE: n/a
9
+ // FORMAT THEOREM: forall argv: parseApply(argv) = cmd -> deterministic(cmd)
10
+ // PURITY: CORE
11
+ // EFFECT: Effect<ApplyCommand, ParseError, never>
12
+ // INVARIANT: projectDir is never empty
13
+ // COMPLEXITY: O(n) where n = |argv|
14
+ export const parseApply = (args) => Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({
15
+ _tag: "Apply",
16
+ projectDir,
17
+ runUp: raw.up ?? true,
18
+ gitTokenLabel: raw.gitTokenLabel,
19
+ codexTokenLabel: raw.codexTokenLabel,
20
+ claudeTokenLabel: raw.claudeTokenLabel,
21
+ enableMcpPlaywright: raw.enableMcpPlaywright
22
+ }));
@@ -1,17 +1,16 @@
1
1
  import { Either } from "effect";
2
2
  import { buildCreateCommand, nonEmpty } from "@effect-template/lib/core/command-builders";
3
- import { defaultTemplateConfig, resolveRepoInput } from "@effect-template/lib/core/domain";
3
+ import { resolveRepoInput } from "@effect-template/lib/core/domain";
4
4
  import { parseRawOptions } from "./parser-options.js";
5
5
  import { resolveWorkspaceRepoPath, splitPositionalRepo } from "./parser-shared.js";
6
6
  const applyCloneDefaults = (raw, rawRepoUrl, resolvedRepo) => {
7
7
  const repoPath = resolveWorkspaceRepoPath(resolvedRepo);
8
- const sshUser = raw.sshUser?.trim() ?? defaultTemplateConfig.sshUser;
9
- const homeDir = `/home/${sshUser}`;
8
+ const targetHome = "~";
10
9
  return {
11
10
  ...raw,
12
11
  repoUrl: rawRepoUrl,
13
12
  outDir: raw.outDir ?? `.docker-git/${repoPath}`,
14
- targetDir: raw.targetDir ?? `${homeDir}/${repoPath}`
13
+ targetDir: raw.targetDir ?? `${targetHome}/workspaces/${repoPath}`
15
14
  };
16
15
  };
17
16
  // CHANGE: parse clone command with positional repo url
@@ -19,6 +19,9 @@ const valueOptionSpecs = [
19
19
  { flag: "--archive", key: "archivePath" },
20
20
  { flag: "--mode", key: "scrapMode" },
21
21
  { flag: "--label", key: "label" },
22
+ { flag: "--git-token", key: "gitTokenLabel" },
23
+ { flag: "--codex-token", key: "codexTokenLabel" },
24
+ { flag: "--claude-token", key: "claudeTokenLabel" },
22
25
  { flag: "--token", key: "token" },
23
26
  { flag: "--scopes", key: "scopes" },
24
27
  { flag: "--message", key: "message" },
@@ -60,6 +63,9 @@ const valueFlagUpdaters = {
60
63
  archivePath: (raw, value) => ({ ...raw, archivePath: value }),
61
64
  scrapMode: (raw, value) => ({ ...raw, scrapMode: value }),
62
65
  label: (raw, value) => ({ ...raw, label: value }),
66
+ gitTokenLabel: (raw, value) => ({ ...raw, gitTokenLabel: value }),
67
+ codexTokenLabel: (raw, value) => ({ ...raw, codexTokenLabel: value }),
68
+ claudeTokenLabel: (raw, value) => ({ ...raw, claudeTokenLabel: value }),
63
69
  token: (raw, value) => ({ ...raw, token: value }),
64
70
  scopes: (raw, value) => ({ ...raw, scopes: value }),
65
71
  message: (raw, value) => ({ ...raw, message: value }),
@@ -79,30 +85,49 @@ export const applyCommandValueFlag = (raw, token, value) => {
79
85
  const update = valueFlagUpdaters[valueSpec.key];
80
86
  return Either.right(update(raw, value));
81
87
  };
88
+ const parseInlineValueToken = (raw, token) => {
89
+ const equalIndex = token.indexOf("=");
90
+ if (equalIndex <= 0 || !token.startsWith("-")) {
91
+ return null;
92
+ }
93
+ const flag = token.slice(0, equalIndex);
94
+ const inlineValue = token.slice(equalIndex + 1);
95
+ return applyCommandValueFlag(raw, flag, inlineValue);
96
+ };
97
+ const parseRawOptionsStep = (args, index, raw) => {
98
+ const token = args[index] ?? "";
99
+ const inlineApplied = parseInlineValueToken(raw, token);
100
+ if (inlineApplied !== null) {
101
+ return Either.isLeft(inlineApplied)
102
+ ? { _tag: "error", error: inlineApplied.left }
103
+ : { _tag: "ok", raw: inlineApplied.right, nextIndex: index + 1 };
104
+ }
105
+ const booleanApplied = applyCommandBooleanFlag(raw, token);
106
+ if (booleanApplied !== null) {
107
+ return { _tag: "ok", raw: booleanApplied, nextIndex: index + 1 };
108
+ }
109
+ if (!token.startsWith("-")) {
110
+ return { _tag: "error", error: { _tag: "UnexpectedArgument", value: token } };
111
+ }
112
+ const value = args[index + 1];
113
+ if (value === undefined) {
114
+ return { _tag: "error", error: { _tag: "MissingOptionValue", option: token } };
115
+ }
116
+ const nextRaw = applyCommandValueFlag(raw, token, value);
117
+ return Either.isLeft(nextRaw)
118
+ ? { _tag: "error", error: nextRaw.left }
119
+ : { _tag: "ok", raw: nextRaw.right, nextIndex: index + 2 };
120
+ };
82
121
  export const parseRawOptions = (args) => {
83
122
  let index = 0;
84
123
  let raw = {};
85
124
  while (index < args.length) {
86
- const token = args[index] ?? "";
87
- const booleanApplied = applyCommandBooleanFlag(raw, token);
88
- if (booleanApplied !== null) {
89
- raw = booleanApplied;
90
- index += 1;
91
- continue;
92
- }
93
- if (!token.startsWith("-")) {
94
- return Either.left({ _tag: "UnexpectedArgument", value: token });
95
- }
96
- const value = args[index + 1];
97
- if (value === undefined) {
98
- return Either.left({ _tag: "MissingOptionValue", option: token });
99
- }
100
- const nextRaw = applyCommandValueFlag(raw, token, value);
101
- if (Either.isLeft(nextRaw)) {
102
- return Either.left(nextRaw.left);
125
+ const step = parseRawOptionsStep(args, index, raw);
126
+ if (step._tag === "error") {
127
+ return Either.left(step.error);
103
128
  }
104
- raw = nextRaw.right;
105
- index += 2;
129
+ raw = step.raw;
130
+ index = step.nextIndex;
106
131
  }
107
132
  return Either.right(raw);
108
133
  };
@@ -1,5 +1,6 @@
1
1
  import { Either, Match } from "effect";
2
2
  import {} from "@effect-template/lib/core/domain";
3
+ import { parseApply } from "./parser-apply.js";
3
4
  import { parseAttach } from "./parser-attach.js";
4
5
  import { parseAuth } from "./parser-auth.js";
5
6
  import { parseClone } from "./parser-clone.js";
@@ -42,5 +43,5 @@ export const parseArgs = (args) => {
42
43
  };
43
44
  return Match.value(command)
44
45
  .pipe(Match.when("create", () => parseCreate(rest)), Match.when("init", () => parseCreate(rest)), Match.when("clone", () => parseClone(rest)), Match.when("attach", () => parseAttach(rest)), Match.when("tmux", () => parseAttach(rest)), Match.when("panes", () => parsePanes(rest)), Match.when("terms", () => parsePanes(rest)), Match.when("terminals", () => parsePanes(rest)), Match.when("sessions", () => parseSessions(rest)), Match.when("scrap", () => parseScrap(rest)), Match.when("mcp-playwright", () => parseMcpPlaywright(rest)), Match.when("help", () => Either.right(helpCommand)), Match.when("ps", () => Either.right(statusCommand)), Match.when("status", () => Either.right(statusCommand)), Match.when("down-all", () => Either.right(downAllCommand)), Match.when("stop-all", () => Either.right(downAllCommand)), Match.when("kill-all", () => Either.right(downAllCommand)), Match.when("menu", () => Either.right(menuCommand)), Match.when("ui", () => Either.right(menuCommand)), Match.when("auth", () => parseAuth(rest)))
45
- .pipe(Match.when("state", () => parseState(rest)), Match.orElse(() => Either.left(unknownCommandError)));
46
+ .pipe(Match.when("apply", () => parseApply(rest)), Match.when("state", () => parseState(rest)), Match.orElse(() => Either.left(unknownCommandError)));
46
47
  };
@@ -2,6 +2,7 @@ import { Match } from "effect";
2
2
  export const usageText = `docker-git menu
3
3
  docker-git create --repo-url <url> [options]
4
4
  docker-git clone <url> [options]
5
+ docker-git apply [<url>] [options]
5
6
  docker-git mcp-playwright [<url>] [options]
6
7
  docker-git attach [<url>] [options]
7
8
  docker-git panes [<url>] [options]
@@ -18,6 +19,7 @@ Commands:
18
19
  menu Interactive menu (default when no args)
19
20
  create, init Generate docker development environment
20
21
  clone Create + run container and clone repo
22
+ apply Apply docker-git config to an existing project/container (current dir by default)
21
23
  mcp-playwright Enable Playwright MCP + Chromium sidecar for an existing project dir
22
24
  attach, tmux Open tmux workspace for a docker-git project
23
25
  panes, terms List tmux panes for a docker-git project
@@ -31,7 +33,7 @@ Commands:
31
33
  Options:
32
34
  --repo-ref <ref> Git ref/branch (default: main)
33
35
  --branch, -b <ref> Alias for --repo-ref
34
- --target-dir <path> Target dir inside container (create default: /home/dev/app, clone default: /home/dev/<org>/<repo>[/issue-<id>|/pr-<id>])
36
+ --target-dir <path> Target dir inside container (create default: /home/dev/app, clone default: ~/workspaces/<org>/<repo>[/issue-<id>|/pr-<id>])
35
37
  --ssh-port <port> Local SSH port (default: 2222)
36
38
  --ssh-user <user> SSH user inside container (default: dev)
37
39
  --container-name <name> Docker container name (default: dg-<repo>)
@@ -46,6 +48,9 @@ Options:
46
48
  --project-dir <path> Project directory for attach (default: .)
47
49
  --archive <path> Scrap snapshot directory (default: .orch/scrap/session)
48
50
  --mode <session> Scrap mode (default: session)
51
+ --git-token <label> Token label for clone/create (maps to GITHUB_TOKEN__<LABEL>, example: agiens)
52
+ --codex-token <label> Codex auth label for clone/create (maps to CODEX_AUTH_LABEL, example: agien)
53
+ --claude-token <label> Claude auth label for clone/create (maps to CLAUDE_AUTH_LABEL, example: agien)
49
54
  --wipe | --no-wipe Wipe workspace before scrap import (default: --wipe)
50
55
  --lines <n> Tail last N lines for sessions logs (default: 200)
51
56
  --include-default Show default/system processes in sessions list
@@ -1,4 +1,5 @@
1
1
  import { createProject } from "@effect-template/lib/usecases/actions";
2
+ import { applyProjectConfig } from "@effect-template/lib/usecases/apply";
2
3
  import { authClaudeLogin, authClaudeLogout, authClaudeStatus, authCodexLogin, authCodexLogout, authCodexStatus, authGithubLogin, authGithubLogout, authGithubStatus } from "@effect-template/lib/usecases/auth";
3
4
  import { renderError } from "@effect-template/lib/usecases/errors";
4
5
  import { mcpPlaywrightUp } from "@effect-template/lib/usecases/mcp-playwright";
@@ -23,7 +24,7 @@ const logWarningAndExit = (error) => pipe(Effect.logWarning(renderError(error)),
23
24
  const logErrorAndExit = (error) => pipe(Effect.logError(renderError(error)), Effect.tap(() => setExitCode(1)), Effect.asVoid);
24
25
  const handleNonBaseCommand = (command) => Match.value(command)
25
26
  .pipe(Match.when({ _tag: "StatePath" }, () => statePath), Match.when({ _tag: "StateInit" }, (cmd) => stateInit(cmd)), Match.when({ _tag: "StateStatus" }, () => stateStatus), Match.when({ _tag: "StatePull" }, () => statePull), Match.when({ _tag: "StateCommit" }, (cmd) => stateCommit(cmd.message)), Match.when({ _tag: "StatePush" }, () => statePush), Match.when({ _tag: "StateSync" }, (cmd) => stateSync(cmd.message)), Match.when({ _tag: "AuthGithubLogin" }, (cmd) => authGithubLogin(cmd)), Match.when({ _tag: "AuthGithubStatus" }, (cmd) => authGithubStatus(cmd)), Match.when({ _tag: "AuthGithubLogout" }, (cmd) => authGithubLogout(cmd)), Match.when({ _tag: "AuthCodexLogin" }, (cmd) => authCodexLogin(cmd)), Match.when({ _tag: "AuthCodexStatus" }, (cmd) => authCodexStatus(cmd)), Match.when({ _tag: "AuthCodexLogout" }, (cmd) => authCodexLogout(cmd)), Match.when({ _tag: "AuthClaudeLogin" }, (cmd) => authClaudeLogin(cmd)), Match.when({ _tag: "AuthClaudeStatus" }, (cmd) => authClaudeStatus(cmd)), Match.when({ _tag: "AuthClaudeLogout" }, (cmd) => authClaudeLogout(cmd)), Match.when({ _tag: "Attach" }, (cmd) => attachTmux(cmd)), Match.when({ _tag: "Panes" }, (cmd) => listTmuxPanes(cmd)), Match.when({ _tag: "SessionsList" }, (cmd) => listTerminalSessions(cmd)), Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd)))
26
- .pipe(Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)), Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)), Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)), Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)), Match.exhaustive);
27
+ .pipe(Match.when({ _tag: "Apply" }, (cmd) => applyProjectConfig(cmd)), Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)), Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)), Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)), Match.when({ _tag: "McpPlaywrightUp" }, (cmd) => mcpPlaywrightUp(cmd)), Match.exhaustive);
27
28
  // CHANGE: compose CLI program with typed errors and shell effects
28
29
  // WHY: keep a thin entry layer over pure parsing and template generation
29
30
  // QUOTE(ТЗ): "CLI команду... создавать докер образы"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prover-coder-ai/docker-git",
3
- "version": "1.0.20",
3
+ "version": "1.0.21",
4
4
  "description": "Minimal Vite-powered TypeScript console starter using Effect",
5
5
  "main": "dist/src/docker-git/main.js",
6
6
  "bin": {
@@ -0,0 +1,28 @@
1
+ import { Either } from "effect"
2
+
3
+ import { type ApplyCommand, type ParseError } from "@effect-template/lib/core/domain"
4
+
5
+ import { parseProjectDirWithOptions } from "./parser-shared.js"
6
+
7
+ // CHANGE: parse "apply" command for existing docker-git projects
8
+ // WHY: update managed docker-git config on the current project/container without creating a new project
9
+ // QUOTE(ТЗ): "Не создавать новый... а прямо в текущем обновить её на актуальную"
10
+ // REF: issue-72-followup-apply-current-config
11
+ // SOURCE: n/a
12
+ // FORMAT THEOREM: forall argv: parseApply(argv) = cmd -> deterministic(cmd)
13
+ // PURITY: CORE
14
+ // EFFECT: Effect<ApplyCommand, ParseError, never>
15
+ // INVARIANT: projectDir is never empty
16
+ // COMPLEXITY: O(n) where n = |argv|
17
+ export const parseApply = (
18
+ args: ReadonlyArray<string>
19
+ ): Either.Either<ApplyCommand, ParseError> =>
20
+ Either.map(parseProjectDirWithOptions(args), ({ projectDir, raw }) => ({
21
+ _tag: "Apply",
22
+ projectDir,
23
+ runUp: raw.up ?? true,
24
+ gitTokenLabel: raw.gitTokenLabel,
25
+ codexTokenLabel: raw.codexTokenLabel,
26
+ claudeTokenLabel: raw.claudeTokenLabel,
27
+ enableMcpPlaywright: raw.enableMcpPlaywright
28
+ }))
@@ -2,12 +2,7 @@ import { Either } from "effect"
2
2
 
3
3
  import { buildCreateCommand, nonEmpty } from "@effect-template/lib/core/command-builders"
4
4
  import type { RawOptions } from "@effect-template/lib/core/command-options"
5
- import {
6
- type Command,
7
- defaultTemplateConfig,
8
- type ParseError,
9
- resolveRepoInput
10
- } from "@effect-template/lib/core/domain"
5
+ import { type Command, type ParseError, resolveRepoInput } from "@effect-template/lib/core/domain"
11
6
 
12
7
  import { parseRawOptions } from "./parser-options.js"
13
8
  import { resolveWorkspaceRepoPath, splitPositionalRepo } from "./parser-shared.js"
@@ -18,13 +13,12 @@ const applyCloneDefaults = (
18
13
  resolvedRepo: ReturnType<typeof resolveRepoInput>
19
14
  ): RawOptions => {
20
15
  const repoPath = resolveWorkspaceRepoPath(resolvedRepo)
21
- const sshUser = raw.sshUser?.trim() ?? defaultTemplateConfig.sshUser
22
- const homeDir = `/home/${sshUser}`
16
+ const targetHome = "~"
23
17
  return {
24
18
  ...raw,
25
19
  repoUrl: rawRepoUrl,
26
20
  outDir: raw.outDir ?? `.docker-git/${repoPath}`,
27
- targetDir: raw.targetDir ?? `${homeDir}/${repoPath}`
21
+ targetDir: raw.targetDir ?? `${targetHome}/workspaces/${repoPath}`
28
22
  }
29
23
  }
30
24
 
@@ -23,6 +23,9 @@ interface ValueOptionSpec {
23
23
  | "archivePath"
24
24
  | "scrapMode"
25
25
  | "label"
26
+ | "gitTokenLabel"
27
+ | "codexTokenLabel"
28
+ | "claudeTokenLabel"
26
29
  | "token"
27
30
  | "scopes"
28
31
  | "message"
@@ -51,6 +54,9 @@ const valueOptionSpecs: ReadonlyArray<ValueOptionSpec> = [
51
54
  { flag: "--archive", key: "archivePath" },
52
55
  { flag: "--mode", key: "scrapMode" },
53
56
  { flag: "--label", key: "label" },
57
+ { flag: "--git-token", key: "gitTokenLabel" },
58
+ { flag: "--codex-token", key: "codexTokenLabel" },
59
+ { flag: "--claude-token", key: "claudeTokenLabel" },
54
60
  { flag: "--token", key: "token" },
55
61
  { flag: "--scopes", key: "scopes" },
56
62
  { flag: "--message", key: "message" },
@@ -99,6 +105,9 @@ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: st
99
105
  archivePath: (raw, value) => ({ ...raw, archivePath: value }),
100
106
  scrapMode: (raw, value) => ({ ...raw, scrapMode: value }),
101
107
  label: (raw, value) => ({ ...raw, label: value }),
108
+ gitTokenLabel: (raw, value) => ({ ...raw, gitTokenLabel: value }),
109
+ codexTokenLabel: (raw, value) => ({ ...raw, codexTokenLabel: value }),
110
+ claudeTokenLabel: (raw, value) => ({ ...raw, claudeTokenLabel: value }),
102
111
  token: (raw, value) => ({ ...raw, token: value }),
103
112
  scopes: (raw, value) => ({ ...raw, scopes: value }),
104
113
  message: (raw, value) => ({ ...raw, message: value }),
@@ -126,34 +135,68 @@ export const applyCommandValueFlag = (
126
135
  return Either.right(update(raw, value))
127
136
  }
128
137
 
138
+ type ParseRawOptionsStep =
139
+ | { readonly _tag: "ok"; readonly raw: RawOptions; readonly nextIndex: number }
140
+ | { readonly _tag: "error"; readonly error: ParseError }
141
+
142
+ const parseInlineValueToken = (
143
+ raw: RawOptions,
144
+ token: string
145
+ ): Either.Either<RawOptions, ParseError> | null => {
146
+ const equalIndex = token.indexOf("=")
147
+ if (equalIndex <= 0 || !token.startsWith("-")) {
148
+ return null
149
+ }
150
+
151
+ const flag = token.slice(0, equalIndex)
152
+ const inlineValue = token.slice(equalIndex + 1)
153
+ return applyCommandValueFlag(raw, flag, inlineValue)
154
+ }
155
+
156
+ const parseRawOptionsStep = (
157
+ args: ReadonlyArray<string>,
158
+ index: number,
159
+ raw: RawOptions
160
+ ): ParseRawOptionsStep => {
161
+ const token = args[index] ?? ""
162
+ const inlineApplied = parseInlineValueToken(raw, token)
163
+ if (inlineApplied !== null) {
164
+ return Either.isLeft(inlineApplied)
165
+ ? { _tag: "error", error: inlineApplied.left }
166
+ : { _tag: "ok", raw: inlineApplied.right, nextIndex: index + 1 }
167
+ }
168
+
169
+ const booleanApplied = applyCommandBooleanFlag(raw, token)
170
+ if (booleanApplied !== null) {
171
+ return { _tag: "ok", raw: booleanApplied, nextIndex: index + 1 }
172
+ }
173
+
174
+ if (!token.startsWith("-")) {
175
+ return { _tag: "error", error: { _tag: "UnexpectedArgument", value: token } }
176
+ }
177
+
178
+ const value = args[index + 1]
179
+ if (value === undefined) {
180
+ return { _tag: "error", error: { _tag: "MissingOptionValue", option: token } }
181
+ }
182
+
183
+ const nextRaw = applyCommandValueFlag(raw, token, value)
184
+ return Either.isLeft(nextRaw)
185
+ ? { _tag: "error", error: nextRaw.left }
186
+ : { _tag: "ok", raw: nextRaw.right, nextIndex: index + 2 }
187
+ }
188
+
129
189
  export const parseRawOptions = (args: ReadonlyArray<string>): Either.Either<RawOptions, ParseError> => {
130
190
  let index = 0
131
191
  let raw: RawOptions = {}
132
192
 
133
193
  while (index < args.length) {
134
- const token = args[index] ?? ""
135
- const booleanApplied = applyCommandBooleanFlag(raw, token)
136
- if (booleanApplied !== null) {
137
- raw = booleanApplied
138
- index += 1
139
- continue
140
- }
141
-
142
- if (!token.startsWith("-")) {
143
- return Either.left({ _tag: "UnexpectedArgument", value: token })
144
- }
145
-
146
- const value = args[index + 1]
147
- if (value === undefined) {
148
- return Either.left({ _tag: "MissingOptionValue", option: token })
149
- }
150
-
151
- const nextRaw = applyCommandValueFlag(raw, token, value)
152
- if (Either.isLeft(nextRaw)) {
153
- return Either.left(nextRaw.left)
194
+ const step = parseRawOptionsStep(args, index, raw)
195
+ if (step._tag === "error") {
196
+ return Either.left(step.error)
154
197
  }
155
- raw = nextRaw.right
156
- index += 2
198
+ raw = step.raw
199
+ index = step.nextIndex
157
200
  }
158
201
 
159
202
  return Either.right(raw)
@@ -2,6 +2,7 @@ import { Either, Match } from "effect"
2
2
 
3
3
  import { type Command, type ParseError } from "@effect-template/lib/core/domain"
4
4
 
5
+ import { parseApply } from "./parser-apply.js"
5
6
  import { parseAttach } from "./parser-attach.js"
6
7
  import { parseAuth } from "./parser-auth.js"
7
8
  import { parseClone } from "./parser-clone.js"
@@ -74,6 +75,7 @@ export const parseArgs = (args: ReadonlyArray<string>): Either.Either<Command, P
74
75
  Match.when("auth", () => parseAuth(rest))
75
76
  )
76
77
  .pipe(
78
+ Match.when("apply", () => parseApply(rest)),
77
79
  Match.when("state", () => parseState(rest)),
78
80
  Match.orElse(() => Either.left(unknownCommandError))
79
81
  )
@@ -5,6 +5,7 @@ import type { ParseError } from "@effect-template/lib/core/domain"
5
5
  export const usageText = `docker-git menu
6
6
  docker-git create --repo-url <url> [options]
7
7
  docker-git clone <url> [options]
8
+ docker-git apply [<url>] [options]
8
9
  docker-git mcp-playwright [<url>] [options]
9
10
  docker-git attach [<url>] [options]
10
11
  docker-git panes [<url>] [options]
@@ -21,6 +22,7 @@ Commands:
21
22
  menu Interactive menu (default when no args)
22
23
  create, init Generate docker development environment
23
24
  clone Create + run container and clone repo
25
+ apply Apply docker-git config to an existing project/container (current dir by default)
24
26
  mcp-playwright Enable Playwright MCP + Chromium sidecar for an existing project dir
25
27
  attach, tmux Open tmux workspace for a docker-git project
26
28
  panes, terms List tmux panes for a docker-git project
@@ -34,7 +36,7 @@ Commands:
34
36
  Options:
35
37
  --repo-ref <ref> Git ref/branch (default: main)
36
38
  --branch, -b <ref> Alias for --repo-ref
37
- --target-dir <path> Target dir inside container (create default: /home/dev/app, clone default: /home/dev/<org>/<repo>[/issue-<id>|/pr-<id>])
39
+ --target-dir <path> Target dir inside container (create default: /home/dev/app, clone default: ~/workspaces/<org>/<repo>[/issue-<id>|/pr-<id>])
38
40
  --ssh-port <port> Local SSH port (default: 2222)
39
41
  --ssh-user <user> SSH user inside container (default: dev)
40
42
  --container-name <name> Docker container name (default: dg-<repo>)
@@ -49,6 +51,9 @@ Options:
49
51
  --project-dir <path> Project directory for attach (default: .)
50
52
  --archive <path> Scrap snapshot directory (default: .orch/scrap/session)
51
53
  --mode <session> Scrap mode (default: session)
54
+ --git-token <label> Token label for clone/create (maps to GITHUB_TOKEN__<LABEL>, example: agiens)
55
+ --codex-token <label> Codex auth label for clone/create (maps to CODEX_AUTH_LABEL, example: agien)
56
+ --claude-token <label> Claude auth label for clone/create (maps to CLAUDE_AUTH_LABEL, example: agien)
52
57
  --wipe | --no-wipe Wipe workspace before scrap import (default: --wipe)
53
58
  --lines <n> Tail last N lines for sessions logs (default: 200)
54
59
  --include-default Show default/system processes in sessions list
@@ -1,5 +1,6 @@
1
1
  import type { Command, ParseError } from "@effect-template/lib/core/domain"
2
2
  import { createProject } from "@effect-template/lib/usecases/actions"
3
+ import { applyProjectConfig } from "@effect-template/lib/usecases/apply"
3
4
  import {
4
5
  authClaudeLogin,
5
6
  authClaudeLogout,
@@ -97,6 +98,7 @@ const handleNonBaseCommand = (command: NonBaseCommand) =>
97
98
  Match.when({ _tag: "SessionsKill" }, (cmd) => killTerminalProcess(cmd))
98
99
  )
99
100
  .pipe(
101
+ Match.when({ _tag: "Apply" }, (cmd) => applyProjectConfig(cmd)),
100
102
  Match.when({ _tag: "SessionsLogs" }, (cmd) => tailTerminalLogs(cmd)),
101
103
  Match.when({ _tag: "ScrapExport" }, (cmd) => exportScrap(cmd)),
102
104
  Match.when({ _tag: "ScrapImport" }, (cmd) => importScrap(cmd)),
@@ -17,12 +17,23 @@ describe("renderEntrypoint auth bridge", () => {
17
17
  "GIT_AUTH_TOKEN=\"${GIT_AUTH_TOKEN:-${GITHUB_TOKEN:-${GH_TOKEN:-}}}\""
18
18
  )
19
19
  expect(entrypoint).toContain("GITHUB_TOKEN=\"${GITHUB_TOKEN:-${GH_TOKEN:-}}\"")
20
- expect(entrypoint).toContain("if [[ -n \"$GH_TOKEN\" || -n \"$GITHUB_TOKEN\" ]]; then")
20
+ expect(entrypoint).toContain("AUTH_LABEL_RAW=\"${GIT_AUTH_LABEL:-${GITHUB_AUTH_LABEL:-}}\"")
21
+ expect(entrypoint).toContain("LABELED_GITHUB_TOKEN_KEY=\"GITHUB_TOKEN__$RESOLVED_AUTH_LABEL\"")
22
+ expect(entrypoint).toContain("LABELED_GIT_TOKEN_KEY=\"GIT_AUTH_TOKEN__$RESOLVED_AUTH_LABEL\"")
23
+ expect(entrypoint).toContain("if [[ -n \"$EFFECTIVE_GH_TOKEN\" ]]; then")
21
24
  expect(entrypoint).toContain(String.raw`printf "export GITHUB_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`)
25
+ expect(entrypoint).toContain(String.raw`printf "export GH_TOKEN=%q\n" "$EFFECTIVE_GH_TOKEN"`)
26
+ expect(entrypoint).toContain(String.raw`printf "export GIT_AUTH_TOKEN=%q\n" "$EFFECTIVE_GITHUB_TOKEN"`)
22
27
  expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GITHUB_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"")
28
+ expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GH_TOKEN\" \"$EFFECTIVE_GH_TOKEN\"")
29
+ expect(entrypoint).toContain("docker_git_upsert_ssh_env \"GIT_AUTH_TOKEN\" \"$EFFECTIVE_GITHUB_TOKEN\"")
23
30
  expect(entrypoint).toContain("GIT_CREDENTIAL_HELPER_PATH=\"/usr/local/bin/docker-git-credential-helper\"")
24
- expect(entrypoint).toContain("token=\"$GITHUB_TOKEN\"")
25
- expect(entrypoint).toContain("token=\"$GH_TOKEN\"")
31
+ expect(entrypoint).toContain("CLAUDE_REAL_BIN=\"/usr/local/bin/.docker-git-claude-real\"")
32
+ expect(entrypoint).toContain("CLAUDE_WRAPPER_BIN=\"/usr/local/bin/claude\"")
33
+ expect(entrypoint).toContain("cat <<'EOF' > \"$CLAUDE_WRAPPER_BIN\"")
34
+ expect(entrypoint).toContain("CLAUDE_CONFIG_DIR=\"${CLAUDE_CONFIG_DIR:-$HOME/.claude}\"")
35
+ expect(entrypoint).toContain("token=\"${GITHUB_TOKEN:-}\"")
36
+ expect(entrypoint).toContain("token=\"${GH_TOKEN:-}\"")
26
37
  expect(entrypoint).toContain(String.raw`printf "%s\n" "password=$token"`)
27
38
  expect(entrypoint).toContain("git config --global credential.helper")
28
39
  }))