@@ -3,6 +3,7 @@ import type * as FileSystem from "@effect/platform/FileSystem"
33import type * as Path from "@effect/platform/Path"
44import { Effect } from "effect"
55
6+ import { parseEnvEntries , removeEnvKey , upsertEnvKey } from "./env-file.js"
67import { withFsPathContext } from "./runtime.js"
78
89type CopyDecision = "skip" | "copy"
@@ -69,6 +70,69 @@ const shouldCopyEnv = (sourceText: string, targetText: string): CopyDecision =>
6970 return "skip"
7071}
7172
73+ const isGithubTokenKey = ( key : string ) : boolean =>
74+ key === "GITHUB_TOKEN" || key === "GH_TOKEN" || key . startsWith ( "GITHUB_TOKEN__" )
75+
76+ // CHANGE: synchronize GitHub auth keys between env files
77+ // WHY: avoid stale per-project tokens that cause clone auth failures after token rotation
78+ // QUOTE(ТЗ): n/a
79+ // REF: user-request-2026-02-11-clone-invalid-token
80+ // SOURCE: n/a
81+ // FORMAT THEOREM: ∀k ∈ github_token_keys: source(k)=v → merged(k)=v
82+ // PURITY: CORE
83+ // INVARIANT: non-auth keys in target are preserved
84+ // COMPLEXITY: O(n) where n = |env entries|
85+ export const syncGithubAuthKeys = ( sourceText : string , targetText : string ) : string => {
86+ const sourceTokenEntries = parseEnvEntries ( sourceText ) . filter ( ( entry ) => isGithubTokenKey ( entry . key ) )
87+ if ( sourceTokenEntries . length === 0 ) {
88+ return targetText
89+ }
90+
91+ const targetTokenKeys = parseEnvEntries ( targetText )
92+ . filter ( ( entry ) => isGithubTokenKey ( entry . key ) )
93+ . map ( ( entry ) => entry . key )
94+
95+ let next = targetText
96+ for ( const key of targetTokenKeys ) {
97+ next = removeEnvKey ( next , key )
98+ }
99+ for ( const entry of sourceTokenEntries ) {
100+ next = upsertEnvKey ( next , entry . key , entry . value )
101+ }
102+
103+ return next
104+ }
105+
106+ const syncGithubTokenKeysInFile = (
107+ sourcePath : string ,
108+ targetPath : string
109+ ) : Effect . Effect < void , PlatformError , FileSystem . FileSystem | Path . Path > =>
110+ withFsPathContext ( ( { fs } ) =>
111+ Effect . gen ( function * ( _ ) {
112+ const sourceExists = yield * _ ( fs . exists ( sourcePath ) )
113+ if ( ! sourceExists ) {
114+ return
115+ }
116+ const targetExists = yield * _ ( fs . exists ( targetPath ) )
117+ if ( ! targetExists ) {
118+ return
119+ }
120+ const sourceInfo = yield * _ ( fs . stat ( sourcePath ) )
121+ const targetInfo = yield * _ ( fs . stat ( targetPath ) )
122+ if ( sourceInfo . type !== "File" || targetInfo . type !== "File" ) {
123+ return
124+ }
125+
126+ const sourceText = yield * _ ( fs . readFileString ( sourcePath ) )
127+ const targetText = yield * _ ( fs . readFileString ( targetPath ) )
128+ const mergedText = syncGithubAuthKeys ( sourceText , targetText )
129+ if ( mergedText !== targetText ) {
130+ yield * _ ( fs . writeFileString ( targetPath , mergedText ) )
131+ yield * _ ( Effect . log ( `Synced GitHub auth keys from ${ sourcePath } to ${ targetPath } ` ) )
132+ }
133+ } )
134+ )
135+
72136const copyFileIfNeeded = (
73137 sourcePath : string ,
74138 targetPath : string
@@ -249,6 +313,7 @@ export const syncAuthArtifacts = (
249313 const targetCodex = resolvePathFromBase ( path , spec . targetBase , spec . target . codexAuthPath )
250314
251315 yield * _ ( copyFileIfNeeded ( sourceGlobal , targetGlobal ) )
316+ yield * _ ( syncGithubTokenKeysInFile ( sourceGlobal , targetGlobal ) )
252317 yield * _ ( copyFileIfNeeded ( sourceProject , targetProject ) )
253318 yield * _ ( fs . makeDirectory ( targetCodex , { recursive : true } ) )
254319 if ( sourceCodex !== targetCodex ) {
0 commit comments