11import * as Command from "@effect/platform/Command"
2- import type * as CommandExecutor from "@effect/platform/CommandExecutor"
2+ import * 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"
68
79import { runCommandCapture , runCommandWithExitCodes } from "./command-runner.js"
8- import { CommandFailedError , DockerCommandError } from "./errors.js"
10+ import { CommandFailedError , DockerAccessError , type DockerAccessIssue , DockerCommandError } from "./errors.js"
911
1012const composeSpec = ( cwd : string , args : ReadonlyArray < string > ) => ( {
1113 cwd,
@@ -27,6 +29,79 @@ const parseInspectNetworkEntry = (line: string): ReadonlyArray<readonly [string,
2729 return [ entry ]
2830}
2931
32+ const collectUint8Array = ( chunks : Chunk . Chunk < Uint8Array > ) : Uint8Array =>
33+ Chunk . reduce ( chunks , new Uint8Array ( ) , ( acc , curr ) => {
34+ const next = new Uint8Array ( acc . length + curr . length )
35+ next . set ( acc )
36+ next . set ( curr , acc . length )
37+ return next
38+ } )
39+
40+ const permissionDeniedPattern = / p e r m i s s i o n d e n i e d / i
41+
42+ // CHANGE: classify docker daemon access failure into deterministic typed reasons
43+ // WHY: allow callers to render actionable recovery guidance for socket permission issues
44+ // QUOTE(ТЗ): "docker-git handles Docker socket permission problems predictably"
45+ // REF: issue-11
46+ // SOURCE: n/a
47+ // FORMAT THEOREM: ∀m: classify(m) ∈ {"PermissionDenied","DaemonUnavailable"}
48+ // PURITY: CORE
49+ // EFFECT: Effect<DockerAccessIssue, never, never>
50+ // INVARIANT: classification is stable for equal input
51+ // COMPLEXITY: O(|m|)
52+ export const classifyDockerAccessIssue = ( message : string ) : DockerAccessIssue =>
53+ permissionDeniedPattern . test ( message ) ? "PermissionDenied" : "DaemonUnavailable"
54+
55+ // CHANGE: verify docker daemon access before compose/auth flows
56+ // WHY: fail fast on socket permission errors instead of cascading into opaque command failures
57+ // QUOTE(ТЗ): "permission denied to /var/run/docker.sock"
58+ // REF: issue-11
59+ // SOURCE: n/a
60+ // FORMAT THEOREM: ∀cwd: access(cwd)=ok ∨ DockerAccessError
61+ // PURITY: SHELL
62+ // EFFECT: Effect<void, DockerAccessError | PlatformError, CommandExecutor>
63+ // INVARIANT: non-zero docker info exit always maps to DockerAccessError
64+ // COMPLEXITY: O(command)
65+ export const ensureDockerDaemonAccess = (
66+ cwd : string
67+ ) : Effect . Effect < void , DockerAccessError | PlatformError , CommandExecutor . CommandExecutor > =>
68+ Effect . scoped (
69+ 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+ )
82+
83+ const stderrBytes = yield * _ (
84+ pipe ( process . stderr , Stream . runCollect , Effect . map ( ( chunks ) => collectUint8Array ( chunks ) ) )
85+ )
86+ const exitCode = Number ( yield * _ ( process . exitCode ) )
87+
88+ if ( exitCode === 0 ) {
89+ return
90+ }
91+
92+ const stderr = new TextDecoder ( "utf-8" ) . decode ( stderrBytes ) . trim ( )
93+ const details = stderr . length > 0 ? stderr : `docker info failed with exit code ${ exitCode } `
94+ return yield * _ (
95+ Effect . fail (
96+ new DockerAccessError ( {
97+ issue : classifyDockerAccessIssue ( details ) ,
98+ details
99+ } )
100+ )
101+ )
102+ } )
103+ )
104+
30105const runCompose = (
31106 cwd : string ,
32107 args : ReadonlyArray < string > ,
0 commit comments