Skip to content

Commit 24c2372

Browse files
committed
fix: route claude auth status through controller
1 parent f601d55 commit 24c2372

12 files changed

Lines changed: 534 additions & 8 deletions

File tree

packages/api/src/api/contracts.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,14 @@ export type CodexAuthStatus = {
299299
readonly account: string | null
300300
}
301301

302+
export type ClaudeAuthStatus = {
303+
readonly label: string
304+
readonly message: string
305+
readonly connected: boolean
306+
readonly authPath: string
307+
readonly method: "none" | "oauth-token" | "claude-ai-session"
308+
}
309+
302310
export type GrokAuthStatus = {
303311
readonly label: string
304312
readonly message: string

packages/api/src/api/openapi.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,19 @@ export const CodexStatusResponseSchema = Schema.Struct({
251251
status: CodexAuthStatusSchema
252252
})
253253

254+
export const ClaudeAuthStatusSchema = Schema.Struct({
255+
authPath: Schema.String,
256+
connected: Schema.Boolean,
257+
label: Schema.String,
258+
message: Schema.String,
259+
method: Schema.Literal("none", "oauth-token", "claude-ai-session")
260+
})
261+
262+
export const ClaudeStatusResponseSchema = Schema.Struct({
263+
ok: OptionalOkSchema,
264+
status: ClaudeAuthStatusSchema
265+
})
266+
254267
export const GrokAuthStatusSchema = Schema.Struct({
255268
authPath: Schema.String,
256269
connected: Schema.Boolean,
@@ -601,6 +614,11 @@ const AuthGroup = HttpApiGroup.make("auth")
601614
.setUrlParams(QueryLabelSchema)
602615
.addSuccess(CodexStatusResponseSchema)
603616
)
617+
.add(
618+
endpoint.get("claudeStatus", "/auth/claude/status")
619+
.setUrlParams(QueryLabelSchema)
620+
.addSuccess(ClaudeStatusResponseSchema)
621+
)
604622
.add(
605623
endpoint.post("githubLogin", "/auth/github/login")
606624
.setPayload(GithubAuthLoginRequestSchema)

packages/api/src/http.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import {
5959
logoutGitAuth,
6060
logoutGitlabAuth,
6161
logoutGithubAuth,
62+
readClaudeAuthStatus,
6263
readCodexAuthStatus,
6364
readGrokAuthStatus,
6465
readGitAuthStatus,
@@ -1138,6 +1139,15 @@ export const makeRouter = () => {
11381139
return yield* _(jsonResponse({ status }, 200))
11391140
}).pipe(Effect.catchAll(errorResponse))
11401141
),
1142+
HttpRouter.get(
1143+
"/auth/claude/status",
1144+
Effect.gen(function*(_) {
1145+
const request = yield* _(HttpServerRequest.HttpServerRequest)
1146+
const label = new URL(request.url, "http://localhost").searchParams.get("label")
1147+
const status = yield* _(readClaudeAuthStatus(label))
1148+
return yield* _(jsonResponse({ status }, 200))
1149+
}).pipe(Effect.catchAll(errorResponse))
1150+
),
11411151
HttpRouter.get(
11421152
"/auth/menu",
11431153
Effect.gen(function*(_) {

packages/api/src/services/auth.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ import { defaultTemplateConfig } from "@effect-template/lib/core/template-defaul
66
import { parseGithubRepoUrl, parseGitlabRepoUrl } from "@effect-template/lib/core/repo"
77
import { CommandFailedError } from "@effect-template/lib/shell/errors"
88
import { authCodexLogin as runCodexLogin } from "@effect-template/lib/usecases/auth-codex"
9+
import {
10+
claudeAuthRoot,
11+
resolveClaudeAccountPath,
12+
resolveClaudeAuthMethod
13+
} from "@effect-template/lib/usecases/auth-claude"
914
import { authGrokLogout as runGrokLogout } from "@effect-template/lib/usecases/auth-grok-logout"
1015
import { authGitLogin as runGitLogin, authGitLogout as runGitLogout, listGitConnections } from "@effect-template/lib/usecases/auth-git"
1116
import { authGitlabLogin as runGitlabLogin, authGitlabLogout as runGitlabLogout, listGitlabTokens } from "@effect-template/lib/usecases/auth-gitlab"
@@ -37,6 +42,7 @@ import type {
3742
CodexAuthLoginRequest,
3843
CodexAuthLogoutRequest,
3944
CodexAuthStatus,
45+
ClaudeAuthStatus,
4046
GrokAuthLogoutRequest,
4147
GrokAuthStatus,
4248
GitAuthLoginRequest,
@@ -65,6 +71,7 @@ export const gitlabAuthRequiredMessage = [
6571
].join("\n")
6672
export const githubAuthEnvGlobalPath = defaultTemplateConfig.envGlobalPath
6773
export const codexAuthPath = defaultTemplateConfig.codexAuthPath
74+
export const claudeAuthPath = claudeAuthRoot
6875
export const grokAuthPath = defaultTemplateConfig.grokAuthPath
6976

7077
const githubTokenKey = "GITHUB_TOKEN"
@@ -503,6 +510,20 @@ const codexAuthStatus = (
503510
account
504511
})
505512

513+
const claudeAuthStatus = (
514+
label: string,
515+
authPath: string,
516+
method: ClaudeAuthStatus["method"]
517+
): ClaudeAuthStatus => ({
518+
label,
519+
message: method === "none"
520+
? `Claude not connected (${label}).`
521+
: `Claude connected (${label}, ${method}).`,
522+
connected: method !== "none",
523+
authPath,
524+
method
525+
})
526+
506527
const grokAuthStatus = (
507528
label: string,
508529
authPath: string,
@@ -517,6 +538,18 @@ const grokAuthStatus = (
517538
method
518539
})
519540

541+
export const readClaudeAuthStatus = (
542+
label?: string | null | undefined
543+
): Effect.Effect<ClaudeAuthStatus, PlatformError, FileSystem.FileSystem | Path.Path> =>
544+
Effect.gen(function*(_) {
545+
const fs = yield* _(FileSystem.FileSystem)
546+
const path = yield* _(Path.Path)
547+
const rootPath = resolvePathFromCwd(path, process.cwd(), claudeAuthPath)
548+
const { accountLabel, accountPath } = resolveClaudeAccountPath(path, rootPath, label ?? null)
549+
const method = yield* _(resolveClaudeAuthMethod(fs, path, accountPath))
550+
return claudeAuthStatus(accountLabel, accountPath, method)
551+
})
552+
520553
export const readGrokAuthStatus = (
521554
label?: string | null | undefined
522555
): Effect.Effect<GrokAuthStatus, PlatformError, FileSystem.FileSystem | Path.Path> =>

packages/app/src/docker-git/api-client-auth.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import type {
2222
AuthCodexLoginCommand,
2323
AuthCodexLogoutCommand,
2424
AuthCodexStatusCommand,
25+
AuthClaudeStatusCommand,
2526
AuthGithubLoginCommand,
2627
AuthGithubLogoutCommand,
2728
AuthGithubStatusCommand,
@@ -203,6 +204,11 @@ export const codexStatus = (command: AuthCodexStatusCommand) => {
203204
return request("GET", `/auth/codex/status${query}`)
204205
}
205206

207+
export const claudeStatus = (command: AuthClaudeStatusCommand) => {
208+
const query = command.label === null ? "" : `?label=${encodeURIComponent(command.label)}`
209+
return request("GET", `/auth/claude/status${query}`)
210+
}
211+
206212
export const grokStatus = (command: AuthGrokStatusCommand) => {
207213
const query = command.label === null ? "" : `?label=${encodeURIComponent(command.label)}`
208214
return request("GET", `/auth/grok/status${query}`)

packages/app/src/docker-git/api-client.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ export {
2828
codexLogin,
2929
codexLogout,
3030
codexStatus,
31+
claudeStatus,
3132
githubLogin,
3233
githubLogout,
3334
githubStatus,

packages/app/src/docker-git/program-auth.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
codexLogin,
77
codexLogout,
88
codexStatus,
9+
claudeStatus,
910
createAuthTerminalSession,
1011
githubLogin,
1112
githubLogout,
@@ -52,6 +53,7 @@ export type RoutedAuthCommand = Extract<
5253
| "AuthCodexImport"
5354
| "AuthCodexStatus"
5455
| "AuthCodexLogout"
56+
| "AuthClaudeStatus"
5557
}
5658
>
5759

@@ -77,6 +79,7 @@ const routedAuthTags: Readonly<Record<string, true>> = {
7779
AuthGithubLogout: true,
7880
AuthGithubStatus: true,
7981
AuthClaudeLogin: true,
82+
AuthClaudeStatus: true,
8083
AuthGeminiLogin: true,
8184
AuthGrokLogin: true,
8285
AuthGrokLogout: true,
@@ -156,6 +159,10 @@ const handleClaudeLoginCommand = (
156159
)
157160
)
158161

162+
const handleClaudeStatusCommand = (
163+
command: Extract<OperationalCommand, { readonly _tag: "AuthClaudeStatus" }>
164+
) => withControllerReady(pipe(claudeStatus(command), Effect.flatMap((payload) => renderAuthPayload(payload))))
165+
159166
const handleGeminiLoginCommand = (
160167
command: Extract<OperationalCommand, { readonly _tag: "AuthGeminiLogin" }>
161168
) =>
@@ -214,6 +221,7 @@ export const dispatchRoutedAuthCommand = (
214221
Match.when({ _tag: "AuthGitStatus" }, handleGitStatusCommand),
215222
Match.when({ _tag: "AuthGitLogout" }, handleGitLogoutCommand),
216223
Match.when({ _tag: "AuthClaudeLogin" }, handleClaudeLoginCommand),
224+
Match.when({ _tag: "AuthClaudeStatus" }, handleClaudeStatusCommand),
217225
Match.when({ _tag: "AuthGeminiLogin" }, handleGeminiLoginCommand),
218226
Match.when({ _tag: "AuthGrokLogin" }, handleGrokLoginCommand),
219227
Match.when({ _tag: "AuthGrokStatus" }, handleGrokStatusCommand),

packages/app/src/docker-git/program-unsupported.ts

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ export type UnsupportedOperationalCommandTag =
55
| "ScrapImport"
66
| "McpPlaywrightUp"
77
| "Apply"
8-
| "AuthClaudeStatus"
98
| "AuthClaudeLogout"
109
| "AuthGeminiStatus"
1110
| "AuthGeminiLogout"
@@ -26,10 +25,6 @@ export const unsupportedOperationalCommands: Record<
2625
command: "Apply",
2726
message: "Command Apply is not available in API-only host mode."
2827
},
29-
AuthClaudeStatus: {
30-
command: "auth claude status",
31-
message: "Claude status is not routed through the controller in host API mode."
32-
},
3328
AuthClaudeLogout: {
3429
command: "auth claude logout",
3530
message: "Claude logout is not routed through the controller in host API mode."

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const runBrowserFrontendCommandMock = vi.hoisted(
1212
const runMenuCallMock = vi.hoisted(() => vi.fn(() => {}))
1313
const readCommandMock = vi.hoisted(() => vi.fn<() => Command>())
1414
const codexLoginMock = vi.hoisted(() => vi.fn(() => Effect.void))
15+
const claudeStatusMock = vi.hoisted(() => vi.fn(() => Effect.succeed({ status: { connected: false } })))
1516
const createAuthTerminalSessionMock = vi.hoisted(() => vi.fn())
1617
const attachTerminalSessionMock = vi.hoisted(() => vi.fn(() => Effect.void))
1718
const gitlabLoginMock = vi.hoisted(() => vi.fn(() => Effect.succeed({ ok: true })))
@@ -43,6 +44,11 @@ const claudeLoginCommand: Extract<Command, { readonly _tag: "AuthClaudeLogin" }>
4344
label: "work",
4445
claudeAuthPath: ".docker-git/.orch/auth/claude"
4546
}
47+
const claudeStatusCommand: Extract<Command, { readonly _tag: "AuthClaudeStatus" }> = {
48+
_tag: "AuthClaudeStatus",
49+
label: "work",
50+
claudeAuthPath: ".docker-git/.orch/auth/claude"
51+
}
4652
const geminiLoginCommand: Extract<Command, { readonly _tag: "AuthGeminiLogin" }> = {
4753
_tag: "AuthGeminiLogin",
4854
label: null,
@@ -71,6 +77,7 @@ vi.mock("../../src/docker-git/api-client.js", () => ({
7177
codexImport: vi.fn(() => Effect.succeed({ ok: true })),
7278
codexLogout: vi.fn(() => Effect.void),
7379
codexStatus: vi.fn(() => Effect.succeed({ ok: true })),
80+
claudeStatus: claudeStatusMock,
7481
createAuthTerminalSession: createAuthTerminalSessionMock,
7582
createProject: vi.fn(() => Effect.succeed(null)),
7683
downAllProjects: vi.fn(() => Effect.void),
@@ -122,6 +129,8 @@ describe("program menu dispatch", () => {
122129
readCommandMock.mockReturnValue(menuCommand)
123130
codexLoginMock.mockReset()
124131
codexLoginMock.mockImplementation(() => Effect.void)
132+
claudeStatusMock.mockReset()
133+
claudeStatusMock.mockImplementation(() => Effect.succeed({ status: { connected: false } }))
125134
createAuthTerminalSessionMock.mockReset()
126135
createAuthTerminalSessionMock.mockImplementation(() =>
127136
Effect.succeed({
@@ -212,6 +221,16 @@ describe("program menu dispatch", () => {
212221
expect(process.exitCode ?? 0).toBe(0)
213222
}))
214223

224+
it.effect("routes claude status through the controller API", () =>
225+
Effect.gen(function*(_) {
226+
readCommandMock.mockReturnValue(claudeStatusCommand)
227+
yield* _(runProgram())
228+
229+
expect(ensureControllerReadyMock).toHaveBeenCalledTimes(1)
230+
expect(claudeStatusMock).toHaveBeenCalledWith(claudeStatusCommand)
231+
expect(process.exitCode ?? 0).toBe(0)
232+
}))
233+
215234
it.effect("routes gemini login through controller auth terminal sessions", () =>
216235
Effect.gen(function*(_) {
217236
readCommandMock.mockReturnValue(geminiLoginCommand)

packages/lib/src/usecases/auth-claude.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { autoSyncState } from "./state-repo.js"
1919
import { readFileStringIfPresent, writeFileStringEnsuringParent } from "./volatile-files.js"
2020

2121
type ClaudeRuntime = FileSystem.FileSystem | Path.Path | CommandExecutor.CommandExecutor
22-
type ClaudeAuthMethod = "none" | "oauth-token" | "claude-ai-session"
22+
export type ClaudeAuthMethod = "none" | "oauth-token" | "claude-ai-session"
2323

2424
type ClaudeAccountContext = {
2525
readonly accountLabel: string
@@ -113,7 +113,7 @@ const readOauthToken = (
113113
return token.length > 0 ? token : null
114114
})
115115

116-
const resolveClaudeAuthMethod = (
116+
export const resolveClaudeAuthMethod = (
117117
fs: FileSystem.FileSystem,
118118
path: Path.Path,
119119
accountPath: string
@@ -166,7 +166,7 @@ RUN npm install -g @anthropic-ai/claude-code@latest
166166
ENTRYPOINT ["claude"]
167167
`
168168

169-
const resolveClaudeAccountPath = (path: Path.Path, rootPath: string, label: string | null): {
169+
export const resolveClaudeAccountPath = (path: Path.Path, rootPath: string, label: string | null): {
170170
readonly accountLabel: string
171171
readonly accountPath: string
172172
} => {

0 commit comments

Comments
 (0)