11import * as Command from "@effect/platform/Command"
2- import * as CommandExecutor from "@effect/platform/CommandExecutor"
2+ import type * as CommandExecutor from "@effect/platform/CommandExecutor"
33import { ExitCode } from "@effect/platform/CommandExecutor"
44import type { PlatformError } from "@effect/platform/Error"
55import { Effect , pipe } from "effect"
6- import * as Chunk from "effect/Chunk"
7- import * as Stream from "effect/Stream"
8- import { existsSync } from "node:fs"
96
107import { runCommandCapture , runCommandWithExitCodes } from "./command-runner.js"
11- import { CommandFailedError , DockerAccessError , type DockerAccessIssue , DockerCommandError } from "./errors.js"
8+ import { CommandFailedError , DockerCommandError } from "./errors.js"
9+
10+ export { classifyDockerAccessIssue , ensureDockerDaemonAccess } from "./docker-daemon-access.js"
1211
1312const composeSpec = ( cwd : string , args : ReadonlyArray < string > ) => ( {
1413 cwd,
@@ -30,148 +29,6 @@ const parseInspectNetworkEntry = (line: string): ReadonlyArray<readonly [string,
3029 return [ entry ]
3130}
3231
33- const collectUint8Array = ( chunks : Chunk . Chunk < Uint8Array > ) : Uint8Array =>
34- Chunk . reduce ( chunks , new Uint8Array ( ) , ( acc , curr ) => {
35- const next = new Uint8Array ( acc . length + curr . length )
36- next . set ( acc )
37- next . set ( curr , acc . length )
38- return next
39- } )
40-
41- const permissionDeniedPattern = / p e r m i s s i o n d e n i e d / i
42-
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-
104- // CHANGE: classify docker daemon access failure into deterministic typed reasons
105- // WHY: allow callers to render actionable recovery guidance for socket permission issues
106- // QUOTE(ТЗ): "docker-git handles Docker socket permission problems predictably"
107- // REF: issue-11
108- // SOURCE: n/a
109- // FORMAT THEOREM: ∀m: classify(m) ∈ {"PermissionDenied","DaemonUnavailable"}
110- // PURITY: CORE
111- // EFFECT: Effect<DockerAccessIssue, never, never>
112- // INVARIANT: classification is stable for equal input
113- // COMPLEXITY: O(|m|)
114- export const classifyDockerAccessIssue = ( message : string ) : DockerAccessIssue =>
115- permissionDeniedPattern . test ( message ) ? "PermissionDenied" : "DaemonUnavailable"
116-
117- // CHANGE: verify docker daemon access before compose/auth flows
118- // WHY: fail fast on socket permission errors instead of cascading into opaque command failures
119- // QUOTE(ТЗ): "permission denied to /var/run/docker.sock"
120- // REF: issue-11
121- // SOURCE: n/a
122- // FORMAT THEOREM: ∀cwd: access(cwd)=ok ∨ DockerAccessError
123- // PURITY: SHELL
124- // EFFECT: Effect<void, DockerAccessError | PlatformError, CommandExecutor>
125- // INVARIANT: non-zero docker info exit always maps to DockerAccessError
126- // COMPLEXITY: O(command)
127- export const ensureDockerDaemonAccess = (
128- cwd : string
129- ) : Effect . Effect < void , DockerAccessError | PlatformError , CommandExecutor . CommandExecutor > =>
130- Effect . scoped (
131- Effect . gen ( function * ( _ ) {
132- const primaryResult = yield * _ ( runDockerInfoCommand ( cwd ) )
133- if ( primaryResult . exitCode === 0 ) {
134- return
135- }
136-
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- )
147-
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- }
162- }
163-
164- return yield * _ (
165- Effect . fail (
166- new DockerAccessError ( {
167- issue : primaryIssue ,
168- details : primaryResult . details
169- } )
170- )
171- )
172- } )
173- )
174-
17532const runCompose = (
17633 cwd : string ,
17734 args : ReadonlyArray < string > ,
0 commit comments