forked from ProverCoderAI/docker-git
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub-fork.ts
More file actions
129 lines (121 loc) · 5.22 KB
/
github-fork.ts
File metadata and controls
129 lines (121 loc) · 5.22 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
import type * as CommandExecutor from "@effect/platform/CommandExecutor"
import type { PlatformError } from "@effect/platform/Error"
import type * as FileSystem from "@effect/platform/FileSystem"
import type * as Path from "@effect/platform/Path"
import { Effect } from "effect"
import type { CreateCommand } from "../core/domain.js"
import { parseGithubRepoUrl } from "../core/repo.js"
import { CommandFailedError } from "../shell/errors.js"
import { parseEnvEntries, readEnvText } from "./env-file.js"
import { runGhApiCapture, runGhApiNullable } from "./github-api-helpers.js"
import { ensureGhAuthImage, ghAuthRoot } from "./github-auth-image.js"
import { resolvePathFromCwd } from "./path-helpers.js"
import { withFsPathContext } from "./runtime.js"
type GithubForkRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
const resolveGithubToken = (envText: string): string | null => {
const entries = parseEnvEntries(envText)
const direct = entries.find((entry) => entry.key === "GITHUB_TOKEN" || entry.key === "GH_TOKEN")
if (direct && direct.value.trim().length > 0) {
return direct.value.trim()
}
const labeled = entries.find((entry) => entry.key.startsWith("GITHUB_TOKEN__"))
return labeled && labeled.value.trim().length > 0 ? labeled.value.trim() : null
}
const resolveViewerLogin = (
cwd: string,
hostPath: string,
token: string
): Effect.Effect<string, CommandFailedError | PlatformError, CommandExecutor.CommandExecutor> =>
Effect.gen(function*(_) {
const command = "gh api /user --jq .login"
const raw = yield* _(runGhApiCapture(cwd, hostPath, token, ["/user", "--jq", ".login"]))
if (raw.length === 0) {
return yield* _(Effect.fail(new CommandFailedError({ command, exitCode: 1 })))
}
return raw
})
const resolveRepoCloneUrl = (
cwd: string,
hostPath: string,
token: string,
fullName: string
): Effect.Effect<string | null, PlatformError, CommandExecutor.CommandExecutor> =>
runGhApiNullable(cwd, hostPath, token, [`/repos/${fullName}`, "--jq", ".clone_url"])
const createFork = (
cwd: string,
hostPath: string,
token: string,
owner: string,
repo: string
): Effect.Effect<string | null, PlatformError, CommandExecutor.CommandExecutor> =>
runGhApiNullable(cwd, hostPath, token, [
"-X",
"POST",
`/repos/${owner}/${repo}/forks`,
"--jq",
".clone_url"
])
// CHANGE: resolve a fork URL for GitHub repos when a token is available
// WHY: allow docker-git clone to auto-fork issue URLs for push access
// QUOTE(ТЗ): "Сразу на issues и он бы делал форк репы если это надо"
// REF: user-request-2026-02-05-issues-fork
// SOURCE: n/a
// FORMAT THEOREM: ∀r: github(r) ∧ token → fork(r)=url ∨ null
// PURITY: SHELL
// EFFECT: Effect<string | null, PlatformError | CommandFailedError, CommandExecutor>
// INVARIANT: returns null when token or repo parsing is missing
// COMPLEXITY: O(1) API calls
export const resolveGithubForkUrl = (
repoUrl: string,
envGlobalPath: string
): Effect.Effect<string | null, PlatformError | CommandFailedError, GithubForkRuntime> =>
withFsPathContext(({ cwd, fs, path }) =>
Effect.gen(function*(_) {
const repo = parseGithubRepoUrl(repoUrl)
if (!repo) {
return null
}
const envPath = resolvePathFromCwd(path, cwd, envGlobalPath)
const envText = yield* _(readEnvText(fs, envPath))
const token = resolveGithubToken(envText)
if (!token) {
yield* _(Effect.logWarning("GitHub token missing; skipping auto-fork."))
return null
}
const ghRoot = resolvePathFromCwd(path, cwd, ghAuthRoot)
yield* _(fs.makeDirectory(ghRoot, { recursive: true }))
yield* _(ensureGhAuthImage(fs, path, cwd, "gh api"))
const viewer = yield* _(resolveViewerLogin(cwd, ghRoot, token))
if (viewer.toLowerCase() === repo.owner.toLowerCase()) {
return null
}
const forkFullName = `${viewer}/${repo.repo}`
const existingFork = yield* _(resolveRepoCloneUrl(cwd, ghRoot, token, forkFullName))
if (existingFork !== null) {
return existingFork
}
return yield* _(createFork(cwd, ghRoot, token, repo.owner, repo.repo))
})
)
// CHANGE: apply auto-fork URL to create configs when available
// WHY: keep create flow small while enabling fork-aware remotes
// QUOTE(ТЗ): "Сразу на issues и он бы делал форк репы если это надо"
// REF: user-request-2026-02-05-issues-fork
// SOURCE: n/a
// FORMAT THEOREM: ∀c: fork(c) → config(c)=config(c)+forkUrl
// PURITY: SHELL
// EFFECT: Effect<TemplateConfig, never, FileSystem | Path | CommandExecutor>
// INVARIANT: failures do not abort project creation
// COMPLEXITY: O(1) API calls
export const applyGithubForkConfig = (
config: CreateCommand["config"]
): Effect.Effect<CreateCommand["config"], never, GithubForkRuntime> =>
resolveGithubForkUrl(config.repoUrl, config.envGlobalPath).pipe(
Effect.matchEffect({
onFailure: (error) =>
Effect.logWarning(
`Auto-fork failed; continuing without fork. (${error instanceof Error ? error.message : String(error)})`
).pipe(Effect.as(config)),
onSuccess: (forkUrl) => Effect.succeed(forkUrl ? { ...config, forkRepoUrl: forkUrl } : config)
})
)