@@ -7,6 +7,7 @@ import { type AppError } from "@effect-template/lib/usecases/errors"
77import { defaultProjectsRoot } from "@effect-template/lib/usecases/menu-helpers"
88import { autoSyncState } from "@effect-template/lib/usecases/state-repo"
99
10+ import { countAuthAccountDirectories } from "./menu-auth-helpers.js"
1011import { buildLabeledEnvKey , countKeyEntries , normalizeLabel } from "./menu-labeled-env.js"
1112import type { AuthFlow , AuthSnapshot , MenuEnv } from "./menu-types.js"
1213
@@ -17,8 +18,10 @@ type AuthMenuItem = {
1718 readonly label : string
1819}
1920
21+ export type AuthEnvFlow = Extract < AuthFlow , "GithubRemove" | "GitSet" | "GitRemove" >
22+
2023export type AuthPromptStep = {
21- readonly key : "label" | "token" | "user" | "apiKey"
24+ readonly key : "label" | "token" | "user"
2225 readonly label : string
2326 readonly required : boolean
2427 readonly secret : boolean
@@ -29,8 +32,8 @@ const authMenuItems: ReadonlyArray<AuthMenuItem> = [
2932 { action : "GithubRemove" , label : "GitHub: remove token" } ,
3033 { action : "GitSet" , label : "Git: add/update credentials" } ,
3134 { action : "GitRemove" , label : "Git: remove credentials" } ,
32- { action : "ClaudeSet " , label : "Claude: add/update API key " } ,
33- { action : "ClaudeRemove " , label : "Claude: remove API key " } ,
35+ { action : "ClaudeOauth " , label : "Claude Code: login via OAuth (web) " } ,
36+ { action : "ClaudeLogout " , label : "Claude Code: logout (clear cache) " } ,
3437 { action : "Refresh" , label : "Refresh snapshot" } ,
3538 { action : "Back" , label : "Back to main menu" }
3639]
@@ -50,12 +53,11 @@ const flowSteps: Readonly<Record<AuthFlow, ReadonlyArray<AuthPromptStep>>> = {
5053 GitRemove : [
5154 { key : "label" , label : "Label to remove (empty = default)" , required : false , secret : false }
5255 ] ,
53- ClaudeSet : [
54- { key : "label" , label : "Label (empty = default)" , required : false , secret : false } ,
55- { key : "apiKey" , label : "Claude API key" , required : true , secret : true }
56+ ClaudeOauth : [
57+ { key : "label" , label : "Label (empty = default)" , required : false , secret : false }
5658 ] ,
57- ClaudeRemove : [
58- { key : "label" , label : "Label to remove (empty = default)" , required : false , secret : false }
59+ ClaudeLogout : [
60+ { key : "label" , label : "Label to logout (empty = default)" , required : false , secret : false }
5961 ]
6062}
6163
@@ -65,8 +67,8 @@ const flowTitle = (flow: AuthFlow): string =>
6567 Match . when ( "GithubRemove" , ( ) => "GitHub remove" ) ,
6668 Match . when ( "GitSet" , ( ) => "Git credentials" ) ,
6769 Match . when ( "GitRemove" , ( ) => "Git remove" ) ,
68- Match . when ( "ClaudeSet " , ( ) => "Claude API key " ) ,
69- Match . when ( "ClaudeRemove " , ( ) => "Claude remove " ) ,
70+ Match . when ( "ClaudeOauth " , ( ) => "Claude Code OAuth " ) ,
71+ Match . when ( "ClaudeLogout " , ( ) => "Claude Code logout " ) ,
7072 Match . exhaustive
7173 )
7274
@@ -76,16 +78,19 @@ export const successMessage = (flow: AuthFlow, label: string): string =>
7678 Match . when ( "GithubRemove" , ( ) => `Removed GitHub token (${ label } ).` ) ,
7779 Match . when ( "GitSet" , ( ) => `Saved Git credentials (${ label } ).` ) ,
7880 Match . when ( "GitRemove" , ( ) => `Removed Git credentials (${ label } ).` ) ,
79- Match . when ( "ClaudeSet " , ( ) => `Saved Claude key (${ label } ).` ) ,
80- Match . when ( "ClaudeRemove " , ( ) => `Removed Claude key (${ label } ).` ) ,
81+ Match . when ( "ClaudeOauth " , ( ) => `Saved Claude Code login (${ label } ).` ) ,
82+ Match . when ( "ClaudeLogout " , ( ) => `Logged out Claude Code (${ label } ).` ) ,
8183 Match . exhaustive
8284 )
8385
8486const buildGlobalEnvPath = ( cwd : string ) : string => `${ defaultProjectsRoot ( cwd ) } /.orch/env/global.env`
87+ const buildClaudeAuthPath = ( cwd : string ) : string => `${ defaultProjectsRoot ( cwd ) } /.orch/auth/claude`
8588
8689type AuthEnvText = {
8790 readonly fs : FileSystem . FileSystem
91+ readonly path : Path . Path
8892 readonly globalEnvPath : string
93+ readonly claudeAuthPath : string
8994 readonly envText : string
9095}
9196
@@ -96,29 +101,36 @@ const loadAuthEnvText = (
96101 const fs = yield * _ ( FileSystem . FileSystem )
97102 const path = yield * _ ( Path . Path )
98103 const globalEnvPath = buildGlobalEnvPath ( cwd )
104+ const claudeAuthPath = buildClaudeAuthPath ( cwd )
99105 yield * _ ( ensureEnvFile ( fs , path , globalEnvPath ) )
100106 const envText = yield * _ ( readEnvText ( fs , globalEnvPath ) )
101- return { fs, globalEnvPath, envText }
107+ return { fs, path , globalEnvPath, claudeAuthPath , envText }
102108 } )
103109
104110export const readAuthSnapshot = (
105111 cwd : string
106112) : Effect . Effect < AuthSnapshot , AppError , MenuEnv > =>
107113 pipe (
108114 loadAuthEnvText ( cwd ) ,
109- Effect . map ( ( { envText, globalEnvPath } ) => ( {
110- globalEnvPath,
111- totalEntries : parseEnvEntries ( envText ) . filter ( ( entry ) => entry . value . trim ( ) . length > 0 ) . length ,
112- githubTokenEntries : countKeyEntries ( envText , "GITHUB_TOKEN" ) ,
113- gitTokenEntries : countKeyEntries ( envText , "GIT_AUTH_TOKEN" ) ,
114- gitUserEntries : countKeyEntries ( envText , "GIT_AUTH_USER" ) ,
115- claudeKeyEntries : countKeyEntries ( envText , "ANTHROPIC_API_KEY" )
116- } ) )
115+ Effect . flatMap ( ( { claudeAuthPath, envText, fs, globalEnvPath, path } ) =>
116+ pipe (
117+ countAuthAccountDirectories ( fs , path , claudeAuthPath ) ,
118+ Effect . map ( ( claudeAuthEntries ) => ( {
119+ globalEnvPath,
120+ claudeAuthPath,
121+ totalEntries : parseEnvEntries ( envText ) . filter ( ( entry ) => entry . value . trim ( ) . length > 0 ) . length ,
122+ githubTokenEntries : countKeyEntries ( envText , "GITHUB_TOKEN" ) ,
123+ gitTokenEntries : countKeyEntries ( envText , "GIT_AUTH_TOKEN" ) ,
124+ gitUserEntries : countKeyEntries ( envText , "GIT_AUTH_USER" ) ,
125+ claudeAuthEntries
126+ } ) )
127+ )
128+ )
117129 )
118130
119131export const writeAuthFlow = (
120132 cwd : string ,
121- flow : AuthFlow ,
133+ flow : AuthEnvFlow ,
122134 values : Readonly < Record < string , string > >
123135) : Effect . Effect < void , AppError , MenuEnv > =>
124136 pipe (
@@ -131,9 +143,7 @@ export const writeAuthFlow = (
131143 } ) ( )
132144 const token = ( values [ "token" ] ?? "" ) . trim ( )
133145 const user = ( values [ "user" ] ?? "" ) . trim ( )
134- const apiKey = ( values [ "apiKey" ] ?? "" ) . trim ( )
135146 const nextText = Match . value ( flow ) . pipe (
136- Match . when ( "GithubOauth" , ( ) => envText ) ,
137147 Match . when ( "GithubRemove" , ( ) => upsertEnvKey ( envText , buildLabeledEnvKey ( "GITHUB_TOKEN" , label ) , "" ) ) ,
138148 Match . when ( "GitSet" , ( ) => {
139149 const withToken = upsertEnvKey ( envText , buildLabeledEnvKey ( "GIT_AUTH_TOKEN" , label ) , token )
@@ -144,17 +154,12 @@ export const writeAuthFlow = (
144154 const withoutToken = upsertEnvKey ( envText , buildLabeledEnvKey ( "GIT_AUTH_TOKEN" , label ) , "" )
145155 return upsertEnvKey ( withoutToken , buildLabeledEnvKey ( "GIT_AUTH_USER" , label ) , "" )
146156 } ) ,
147- Match . when ( "ClaudeSet" , ( ) => upsertEnvKey ( envText , buildLabeledEnvKey ( "ANTHROPIC_API_KEY" , label ) , apiKey ) ) ,
148- Match . when ( "ClaudeRemove" , ( ) => upsertEnvKey ( envText , buildLabeledEnvKey ( "ANTHROPIC_API_KEY" , label ) , "" ) ) ,
149157 Match . exhaustive
150158 )
151159 const syncMessage = Match . value ( flow ) . pipe (
152- Match . when ( "GithubOauth" , ( ) => `chore(state): auth gh ${ canonicalLabel } ` ) ,
153160 Match . when ( "GithubRemove" , ( ) => `chore(state): auth gh logout ${ canonicalLabel } ` ) ,
154161 Match . when ( "GitSet" , ( ) => `chore(state): auth git ${ canonicalLabel } ` ) ,
155162 Match . when ( "GitRemove" , ( ) => `chore(state): auth git logout ${ canonicalLabel } ` ) ,
156- Match . when ( "ClaudeSet" , ( ) => `chore(state): auth claude ${ canonicalLabel } ` ) ,
157- Match . when ( "ClaudeRemove" , ( ) => `chore(state): auth claude logout ${ canonicalLabel } ` ) ,
158163 Match . exhaustive
159164 )
160165 return pipe (
0 commit comments