Skip to content

Commit 9d67c90

Browse files
committed
feat(cli): add --git-token label selection for clone/create auth
1 parent d4cb2ea commit 9d67c90

10 files changed

Lines changed: 93 additions & 4 deletions

File tree

packages/app/src/docker-git/cli/parser-options.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ interface ValueOptionSpec {
2323
| "archivePath"
2424
| "scrapMode"
2525
| "label"
26+
| "gitTokenLabel"
2627
| "token"
2728
| "scopes"
2829
| "message"
@@ -51,6 +52,7 @@ const valueOptionSpecs: ReadonlyArray<ValueOptionSpec> = [
5152
{ flag: "--archive", key: "archivePath" },
5253
{ flag: "--mode", key: "scrapMode" },
5354
{ flag: "--label", key: "label" },
55+
{ flag: "--git-token", key: "gitTokenLabel" },
5456
{ flag: "--token", key: "token" },
5557
{ flag: "--scopes", key: "scopes" },
5658
{ flag: "--message", key: "message" },
@@ -99,6 +101,7 @@ const valueFlagUpdaters: { readonly [K in ValueKey]: (raw: RawOptions, value: st
99101
archivePath: (raw, value) => ({ ...raw, archivePath: value }),
100102
scrapMode: (raw, value) => ({ ...raw, scrapMode: value }),
101103
label: (raw, value) => ({ ...raw, label: value }),
104+
gitTokenLabel: (raw, value) => ({ ...raw, gitTokenLabel: value }),
102105
token: (raw, value) => ({ ...raw, token: value }),
103106
scopes: (raw, value) => ({ ...raw, scopes: value }),
104107
message: (raw, value) => ({ ...raw, message: value }),
@@ -132,6 +135,19 @@ export const parseRawOptions = (args: ReadonlyArray<string>): Either.Either<RawO
132135

133136
while (index < args.length) {
134137
const token = args[index] ?? ""
138+
const equalIndex = token.indexOf("=")
139+
if (equalIndex > 0 && token.startsWith("-")) {
140+
const flag = token.slice(0, equalIndex)
141+
const inlineValue = token.slice(equalIndex + 1)
142+
const nextRaw = applyCommandValueFlag(raw, flag, inlineValue)
143+
if (Either.isLeft(nextRaw)) {
144+
return Either.left(nextRaw.left)
145+
}
146+
raw = nextRaw.right
147+
index += 1
148+
continue
149+
}
150+
135151
const booleanApplied = applyCommandBooleanFlag(raw, token)
136152
if (booleanApplied !== null) {
137153
raw = booleanApplied

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Options:
4949
--project-dir <path> Project directory for attach (default: .)
5050
--archive <path> Scrap snapshot directory (default: .orch/scrap/session)
5151
--mode <session> Scrap mode (default: session)
52+
--git-token <label> Token label for clone/create (maps to GITHUB_TOKEN__<LABEL>, example: agiens)
5253
--wipe | --no-wipe Wipe workspace before scrap import (default: --wipe)
5354
--lines <n> Tail last N lines for sessions logs (default: 200)
5455
--include-default Show default/system processes in sessions list

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,11 @@ describe("parseArgs", () => {
101101
expect(command.openSsh).toBe(false)
102102
}))
103103

104+
it.effect("parses clone git token label from inline option and normalizes it", () =>
105+
expectCreateCommand(["clone", "https://github.com/org/repo.git", "--git-token=#agiens"], (command) => {
106+
expect(command.config.gitTokenLabel).toBe("AGIENS")
107+
}))
108+
104109
it.effect("supports enabling SSH auto-open for create", () =>
105110
expectCreateCommand(["create", "--repo-url", "https://github.com/org/repo.git", "--ssh"], (command) => {
106111
expect(command.openSsh).toBe(true)

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

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,22 @@ export const nonEmpty = (
4848

4949
const normalizeSecretsRoot = (value: string): string => trimRightChar(value, "/")
5050

51+
const normalizeGitTokenLabel = (value: string | undefined): string | undefined => {
52+
const trimmed = value?.trim() ?? ""
53+
if (trimmed.length === 0) {
54+
return undefined
55+
}
56+
const normalized = trimmed
57+
.toUpperCase()
58+
.replaceAll(/[^A-Z0-9]+/g, "_")
59+
const withoutLeading = normalized.replace(/^_+/, "")
60+
const cleaned = withoutLeading.replace(/_+$/, "")
61+
if (cleaned.length === 0 || cleaned === "DEFAULT") {
62+
return undefined
63+
}
64+
return cleaned
65+
}
66+
5167
type RepoBasics = {
5268
readonly repoUrl: string
5369
readonly repoSlug: string
@@ -206,6 +222,7 @@ export const buildCreateCommand = (
206222
const force = raw.force ?? false
207223
const forceEnv = raw.forceEnv ?? false
208224
const enableMcpPlaywright = raw.enableMcpPlaywright ?? false
225+
const gitTokenLabel = normalizeGitTokenLabel(raw.gitTokenLabel)
209226

210227
return {
211228
_tag: "Create",
@@ -222,6 +239,7 @@ export const buildCreateCommand = (
222239
sshPort: repo.sshPort,
223240
repoUrl: repo.repoUrl,
224241
repoRef: repo.repoRef,
242+
...(gitTokenLabel === undefined ? {} : { gitTokenLabel }),
225243
targetDir: repo.targetDir,
226244
volumeName: names.volumeName,
227245
dockerGitPath: paths.dockerGitPath,

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ export interface RawOptions {
3030
readonly scrapMode?: string
3131
readonly wipe?: boolean
3232
readonly label?: string
33+
readonly gitTokenLabel?: string
3334
readonly token?: string
3435
readonly scopes?: string
3536
readonly message?: string

packages/lib/src/core/domain.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ export interface TemplateConfig {
1010
readonly repoUrl: string
1111
readonly repoRef: string
1212
readonly forkRepoUrl?: string
13+
readonly gitTokenLabel?: string | undefined
1314
readonly targetDir: string
1415
readonly volumeName: string
1516
readonly dockerGitPath: string

packages/lib/src/core/templates-entrypoint/tasks.ts

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,45 @@ else
4343
fi
4444
chown -R 1000:1000 /home/${config.sshUser}
4545
46+
RESOLVED_GIT_AUTH_USER="$GIT_AUTH_USER"
47+
RESOLVED_GIT_AUTH_TOKEN="$GIT_AUTH_TOKEN"
48+
RESOLVED_GIT_AUTH_LABEL=""
49+
GIT_TOKEN_LABEL_RAW="\${GIT_AUTH_LABEL:-\${GITHUB_AUTH_LABEL:-}}"
50+
51+
if [[ -z "$GIT_TOKEN_LABEL_RAW" && "$REPO_URL" == https://github.com/* ]]; then
52+
GIT_TOKEN_LABEL_RAW="$(printf "%s" "$REPO_URL" | sed -E 's#^https://github.com/##; s#[.]git$##; s#/*$##' | cut -d/ -f1)"
53+
fi
54+
55+
if [[ -n "$GIT_TOKEN_LABEL_RAW" ]]; then
56+
RESOLVED_GIT_AUTH_LABEL="$(printf "%s" "$GIT_TOKEN_LABEL_RAW" | tr '[:lower:]' '[:upper:]' | sed -E 's/[^A-Z0-9]+/_/g; s/^_+//; s/_+$//')"
57+
if [[ "$RESOLVED_GIT_AUTH_LABEL" == "DEFAULT" ]]; then
58+
RESOLVED_GIT_AUTH_LABEL=""
59+
fi
60+
fi
61+
62+
if [[ -n "$RESOLVED_GIT_AUTH_LABEL" ]]; then
63+
LABELED_GIT_TOKEN_KEY="GIT_AUTH_TOKEN__$RESOLVED_GIT_AUTH_LABEL"
64+
LABELED_GITHUB_TOKEN_KEY="GITHUB_TOKEN__$RESOLVED_GIT_AUTH_LABEL"
65+
LABELED_GIT_USER_KEY="GIT_AUTH_USER__$RESOLVED_GIT_AUTH_LABEL"
66+
67+
LABELED_GIT_TOKEN="\${!LABELED_GIT_TOKEN_KEY-}"
68+
LABELED_GITHUB_TOKEN="\${!LABELED_GITHUB_TOKEN_KEY-}"
69+
LABELED_GIT_USER="\${!LABELED_GIT_USER_KEY-}"
70+
71+
if [[ -n "$LABELED_GIT_TOKEN" ]]; then
72+
RESOLVED_GIT_AUTH_TOKEN="$LABELED_GIT_TOKEN"
73+
elif [[ -n "$LABELED_GITHUB_TOKEN" ]]; then
74+
RESOLVED_GIT_AUTH_TOKEN="$LABELED_GITHUB_TOKEN"
75+
fi
76+
77+
if [[ -n "$LABELED_GIT_USER" ]]; then
78+
RESOLVED_GIT_AUTH_USER="$LABELED_GIT_USER"
79+
fi
80+
fi
81+
4682
AUTH_REPO_URL="$REPO_URL"
47-
if [[ -n "$GIT_AUTH_TOKEN" && "$REPO_URL" == https://* ]]; then
48-
AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${GIT_AUTH_USER}:\${GIT_AUTH_TOKEN}@#")"
83+
if [[ -n "$RESOLVED_GIT_AUTH_TOKEN" && "$REPO_URL" == https://* ]]; then
84+
AUTH_REPO_URL="$(printf "%s" "$REPO_URL" | sed "s#^https://#https://\${RESOLVED_GIT_AUTH_USER}:\${RESOLVED_GIT_AUTH_TOKEN}@#")"
4985
fi
5086
5187
CLONE_CACHE_ARGS=""

packages/lib/src/core/templates/docker-compose.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,10 @@ import type { TemplateConfig } from "../domain.js"
33
export const renderDockerCompose = (config: TemplateConfig): string => {
44
const networkName = `${config.serviceName}-net`
55
const forkRepoUrl = config.forkRepoUrl ?? ""
6+
const gitTokenLabel = config.gitTokenLabel?.trim() ?? ""
7+
const maybeGitTokenLabelEnv = gitTokenLabel.length > 0
8+
? ` GITHUB_AUTH_LABEL: "${gitTokenLabel}"\n GIT_AUTH_LABEL: "${gitTokenLabel}"\n`
9+
: ""
610

711
const browserServiceName = `${config.serviceName}-browser`
812
const browserContainerName = `${config.containerName}-browser`
@@ -29,6 +33,7 @@ export const renderDockerCompose = (config: TemplateConfig): string => {
2933
REPO_URL: "${config.repoUrl}"
3034
REPO_REF: "${config.repoRef}"
3135
FORK_REPO_URL: "${forkRepoUrl}"
36+
${maybeGitTokenLabelEnv} # Optional token label selector (maps to GITHUB_TOKEN__<LABEL>/GIT_AUTH_TOKEN__<LABEL>)
3237
TARGET_DIR: "${config.targetDir}"
3338
CODEX_HOME: "${config.codexHome}"
3439
${maybePlaywrightEnv}${maybeDependsOn} env_file:

packages/lib/src/shell/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const TemplateConfigSchema = Schema.Struct({
1717
sshPort: Schema.Number.pipe(Schema.int()),
1818
repoUrl: Schema.String,
1919
repoRef: Schema.String,
20+
gitTokenLabel: Schema.optional(Schema.String),
2021
targetDir: Schema.String,
2122
volumeName: Schema.String,
2223
dockerGitPath: Schema.optionalWith(Schema.String, {

packages/lib/tests/usecases/prepare-files.test.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const makeGlobalConfig = (root: string, path: Path.Path): TemplateConfig => ({
2929
sshPort: 2222,
3030
repoUrl: "https://github.com/org/repo.git",
3131
repoRef: "main",
32+
gitTokenLabel: undefined,
3233
targetDir: "/home/dev/org/repo",
3334
volumeName: "dg-test-home",
3435
dockerGitPath: path.join(root, ".docker-git"),
@@ -45,14 +46,16 @@ const makeGlobalConfig = (root: string, path: Path.Path): TemplateConfig => ({
4546
const makeProjectConfig = (
4647
outDir: string,
4748
enableMcpPlaywright: boolean,
48-
path: Path.Path
49+
path: Path.Path,
50+
gitTokenLabel?: string
4951
): TemplateConfig => ({
5052
containerName: "dg-test",
5153
serviceName: "dg-test",
5254
sshUser: "dev",
5355
sshPort: 2222,
5456
repoUrl: "https://github.com/org/repo.git",
5557
repoRef: "main",
58+
gitTokenLabel,
5659
targetDir: "/home/dev/org/repo",
5760
volumeName: "dg-test-home",
5861
dockerGitPath: path.join(outDir, ".docker-git"),
@@ -92,7 +95,7 @@ describe("prepareProjectFiles", () => {
9295
const outDir = path.join(root, "project")
9396
const globalConfig = makeGlobalConfig(root, path)
9497
const withoutMcp = makeProjectConfig(outDir, false, path)
95-
const withMcp = makeProjectConfig(outDir, true, path)
98+
const withMcp = makeProjectConfig(outDir, true, path, "AGIENS")
9699

97100
yield* _(
98101
prepareProjectFiles(outDir, root, globalConfig, withoutMcp, {
@@ -128,6 +131,8 @@ describe("prepareProjectFiles", () => {
128131

129132
expect(composeAfter).toContain("dg-test-browser")
130133
expect(composeAfter).toContain('MCP_PLAYWRIGHT_ENABLE: "1"')
134+
expect(composeAfter).toContain('GITHUB_AUTH_LABEL: "AGIENS"')
135+
expect(composeAfter).toContain('GIT_AUTH_LABEL: "AGIENS"')
131136
expect(readEnableMcpPlaywrightFlag(configAfter)).toBe(true)
132137
})
133138
).pipe(Effect.provide(NodeContext.layer)))

0 commit comments

Comments
 (0)