@@ -5,6 +5,7 @@ import type { PlatformError } from "@effect/platform/Error"
55import { Effect , pipe } from "effect"
66import * as Chunk from "effect/Chunk"
77import * as Stream from "effect/Stream"
8+ import { existsSync } from "node:fs"
89
910import { runCommandCapture , runCommandWithExitCodes } from "./command-runner.js"
1011import { CommandFailedError , DockerAccessError , type DockerAccessIssue , DockerCommandError } from "./errors.js"
@@ -39,6 +40,67 @@ const collectUint8Array = (chunks: Chunk.Chunk<Uint8Array>): Uint8Array =>
3940
4041const permissionDeniedPattern = / p e r m i s s i o n d e n i e d / i
4142
43+ const resolveDockerHostFallback = ( ) : string | undefined => {
44+ if ( process . env [ "DOCKER_HOST" ] !== undefined ) {
45+ return undefined
46+ }
47+
48+ const runtimeDir = process . env [ "XDG_RUNTIME_DIR" ] ?. trim ( )
49+ const uid =
50+ typeof process . getuid === "function"
51+ ? process . getuid ( ) . toString ( )
52+ : process . env [ "UID" ] ?. trim ( )
53+
54+ const candidates = Array . from (
55+ new Set (
56+ [
57+ runtimeDir ? `${ runtimeDir } /docker.sock` : undefined ,
58+ uid ? `/run/user/${ uid } /docker.sock` : undefined
59+ ] . filter ( ( value ) : value is string => value !== undefined )
60+ )
61+ )
62+
63+ for ( const candidate of candidates ) {
64+ if ( existsSync ( candidate ) ) {
65+ return `unix://${ candidate } `
66+ }
67+ }
68+
69+ return undefined
70+ }
71+
72+ const runDockerInfoCommand = (
73+ cwd : string ,
74+ env ?: Readonly < Record < string , string | undefined > >
75+ ) : Effect . Effect < { readonly exitCode : number ; readonly details : string } , PlatformError , CommandExecutor . CommandExecutor > =>
76+ Effect . scoped (
77+ Effect . gen ( function * ( _ ) {
78+ const executor = yield * _ ( CommandExecutor . CommandExecutor )
79+ const process = yield * _ (
80+ executor . start (
81+ pipe (
82+ Command . make ( "docker" , "info" ) ,
83+ Command . workingDirectory ( cwd ) ,
84+ env ? Command . env ( env ) : ( value ) => value ,
85+ Command . stdin ( "pipe" ) ,
86+ Command . stdout ( "pipe" ) ,
87+ Command . stderr ( "pipe" )
88+ )
89+ )
90+ )
91+
92+ const stderrBytes = yield * _ (
93+ pipe ( process . stderr , Stream . runCollect , Effect . map ( ( chunks ) => collectUint8Array ( chunks ) ) )
94+ )
95+ const exitCode = Number ( yield * _ ( process . exitCode ) )
96+ const stderr = new TextDecoder ( "utf-8" ) . decode ( stderrBytes ) . trim ( )
97+ return {
98+ exitCode,
99+ details : stderr . length > 0 ? stderr : `docker info failed with exit code ${ exitCode } `
100+ }
101+ } )
102+ )
103+
42104// CHANGE: classify docker daemon access failure into deterministic typed reasons
43105// WHY: allow callers to render actionable recovery guidance for socket permission issues
44106// QUOTE(ТЗ): "docker-git handles Docker socket permission problems predictably"
@@ -67,35 +129,43 @@ export const ensureDockerDaemonAccess = (
67129) : Effect . Effect < void , DockerAccessError | PlatformError , CommandExecutor . CommandExecutor > =>
68130 Effect . scoped (
69131 Effect . gen ( function * ( _ ) {
70- const executor = yield * _ ( CommandExecutor . CommandExecutor )
71- const process = yield * _ (
72- executor . start (
73- pipe (
74- Command . make ( "docker" , "info" ) ,
75- Command . workingDirectory ( cwd ) ,
76- Command . stdin ( "pipe" ) ,
77- Command . stdout ( "pipe" ) ,
78- Command . stderr ( "pipe" )
79- )
80- )
81- )
132+ const primaryResult = yield * _ ( runDockerInfoCommand ( cwd ) )
133+ if ( primaryResult . exitCode === 0 ) {
134+ return
135+ }
82136
83- const stderrBytes = yield * _ (
84- pipe ( process . stderr , Stream . runCollect , Effect . map ( ( chunks ) => collectUint8Array ( chunks ) ) )
85- )
86- const exitCode = Number ( yield * _ ( process . exitCode ) )
137+ const primaryIssue = classifyDockerAccessIssue ( primaryResult . details )
138+ if ( primaryIssue === "PermissionDenied" && process . env [ "DOCKER_HOST" ] === undefined ) {
139+ const fallbackHost = resolveDockerHostFallback ( )
140+ if ( fallbackHost !== undefined ) {
141+ const fallbackResult = yield * _ (
142+ runDockerInfoCommand ( cwd , {
143+ ...process . env ,
144+ DOCKER_HOST : fallbackHost
145+ } )
146+ )
87147
88- if ( exitCode === 0 ) {
89- return
148+ if ( fallbackResult . exitCode === 0 ) {
149+ process . env [ "DOCKER_HOST" ] = fallbackHost
150+ return
151+ }
152+
153+ return yield * _ (
154+ Effect . fail (
155+ new DockerAccessError ( {
156+ issue : classifyDockerAccessIssue ( fallbackResult . details ) ,
157+ details : fallbackResult . details
158+ } )
159+ )
160+ )
161+ }
90162 }
91163
92- const stderr = new TextDecoder ( "utf-8" ) . decode ( stderrBytes ) . trim ( )
93- const details = stderr . length > 0 ? stderr : `docker info failed with exit code ${ exitCode } `
94164 return yield * _ (
95165 Effect . fail (
96166 new DockerAccessError ( {
97- issue : classifyDockerAccessIssue ( details ) ,
98- details
167+ issue : primaryIssue ,
168+ details : primaryResult . details
99169 } )
100170 )
101171 )
0 commit comments