1+ import type * as CommandExecutor from "@effect/platform/CommandExecutor"
12import type { PlatformError } from "@effect/platform/Error"
23import * as FileSystem from "@effect/platform/FileSystem"
34import * as Path from "@effect/platform/Path"
45import { Effect } from "effect"
56
67import { computeLocalControllerRevision , controllerRevisionEnvKey } from "./controller-revision.js"
8+ import { runCommandWithCapturedOutput } from "./frontend-lib/shell/command-runner.js"
79import { findExistingUpwards } from "./frontend-lib/usecases/path-helpers.js"
810import type { ControllerBootstrapError } from "./host-errors.js"
911
@@ -18,6 +20,9 @@ export type ControllerComposeFiles = {
1820 readonly gpuOverlayPath : string | null
1921}
2022
23+ const skillerSubmodulePath = "third_party/skiller-desktop-skills-manager"
24+ const skillerPackagePath = `${ skillerSubmodulePath } /package.json`
25+
2126const controllerBootstrapError = ( message : string ) : ControllerBootstrapError => ( {
2227 _tag : "ControllerBootstrapError" ,
2328 message
@@ -85,6 +90,85 @@ const mapComposePathError = (error: PlatformError): ControllerBootstrapError =>
8590const mapControllerRevisionError = ( error : PlatformError ) : ControllerBootstrapError =>
8691 controllerBootstrapError ( `Failed to compute docker-git controller revision.\nDetails: ${ String ( error ) } ` )
8792
93+ const skillerSubmoduleCommand = [
94+ "submodule" ,
95+ "update" ,
96+ "--init" ,
97+ "--checkout" ,
98+ skillerSubmodulePath
99+ ]
100+
101+ const formatSkillerSubmoduleFailure = ( rootDir : string , exitCode : number , output : string ) : ControllerBootstrapError =>
102+ controllerBootstrapError (
103+ [
104+ "Failed to initialize Skiller submodule before building docker-git controller." ,
105+ `Command: git ${ skillerSubmoduleCommand . join ( " " ) } ` ,
106+ `Working directory: ${ rootDir } ` ,
107+ `Exit code: ${ exitCode } ` ,
108+ output . trim ( ) . length > 0 ? `Output:\n${ output . trim ( ) } ` : "Output: n/a"
109+ ] . join ( "\n" )
110+ )
111+
112+ const runSkillerSubmoduleInit = (
113+ rootDir : string
114+ ) : Effect . Effect < void , ControllerBootstrapError , CommandExecutor . CommandExecutor > =>
115+ runCommandWithCapturedOutput (
116+ {
117+ cwd : rootDir ,
118+ command : "git" ,
119+ args : skillerSubmoduleCommand
120+ } ,
121+ [ 0 ] ,
122+ ( exitCode , output ) => formatSkillerSubmoduleFailure ( rootDir , exitCode , output )
123+ ) . pipe (
124+ Effect . mapError ( ( error ) : ControllerBootstrapError =>
125+ error . _tag === "ControllerBootstrapError"
126+ ? error
127+ : controllerBootstrapError (
128+ `Failed to initialize Skiller submodule before building docker-git controller.\nDetails: ${ String ( error ) } `
129+ )
130+ )
131+ )
132+
133+ // CHANGE: initialize the pinned Skiller submodule before controller Docker builds
134+ // WHY: the API image copies `third_party`, so an empty submodule makes the patch/build step fail
135+ // QUOTE(ТЗ): "исправь проблему"
136+ // REF: user-message-2026-05-24-controller-skiller-submodule
137+ // SOURCE: n/a
138+ // FORMAT THEOREM: forall root: missing(root/skillerPackagePath) -> init(root) -> exists(root/skillerPackagePath) or typed error
139+ // PURITY: SHELL
140+ // EFFECT: Effect<void, ControllerBootstrapError, FileSystem | Path | CommandExecutor>
141+ // INVARIANT: controller revision and Docker build context are computed only after Skiller source exists
142+ // COMPLEXITY: O(1) filesystem probes plus O(git submodule update)
143+ export const ensureSkillerSubmoduleInitialized = (
144+ rootDir : string
145+ ) : Effect . Effect < void , ControllerBootstrapError , FileSystem . FileSystem | Path . Path | CommandExecutor . CommandExecutor > =>
146+ Effect . gen ( function * ( _ ) {
147+ const fs = yield * _ ( FileSystem . FileSystem )
148+ const path = yield * _ ( Path . Path )
149+ const packagePath = path . join ( rootDir , skillerPackagePath )
150+ const existsBeforeInit = yield * _ ( fs . exists ( packagePath ) . pipe ( Effect . mapError ( mapComposePathError ) ) )
151+ if ( existsBeforeInit ) {
152+ return
153+ }
154+
155+ yield * _ ( Effect . log ( "Initializing Skiller submodule for docker-git controller build." ) )
156+ yield * _ ( runSkillerSubmoduleInit ( rootDir ) )
157+
158+ const existsAfterInit = yield * _ ( fs . exists ( packagePath ) . pipe ( Effect . mapError ( mapComposePathError ) ) )
159+ if ( existsAfterInit ) {
160+ return
161+ }
162+
163+ return yield * _ (
164+ Effect . fail (
165+ controllerBootstrapError (
166+ `Skiller submodule initialization completed but ${ packagePath } was not found.`
167+ )
168+ )
169+ )
170+ } )
171+
88172export const composeFilesForMode = (
89173 composePath : string ,
90174 gpuOverlayPath : string | null
@@ -126,13 +210,13 @@ type ComposePathAndGpuMode = {
126210 readonly buildSkillerMode : ControllerBuildSkillerMode
127211}
128212
129- const withComposePathAndGpuMode = < A > (
213+ const withComposePathAndGpuMode = < A , R > (
130214 effect : ( input : ComposePathAndGpuMode ) => Effect . Effect <
131215 A ,
132216 ControllerBootstrapError ,
133- FileSystem . FileSystem | Path . Path
217+ R
134218 >
135- ) : Effect . Effect < A , ControllerBootstrapError , FileSystem . FileSystem | Path . Path > =>
219+ ) : Effect . Effect < A , ControllerBootstrapError , FileSystem . FileSystem | Path . Path | R > =>
136220 composeFilePath ( ) . pipe (
137221 Effect . mapError ( mapComposePathError ) ,
138222 Effect . flatMap ( ( composePath ) =>
@@ -170,8 +254,16 @@ const persistControllerRevision = (revision: string): Effect.Effect<void> =>
170254export const prepareControllerRevision = ( ) : Effect . Effect <
171255 string ,
172256 ControllerBootstrapError ,
173- FileSystem . FileSystem | Path . Path
257+ FileSystem . FileSystem | Path . Path | CommandExecutor . CommandExecutor
174258> =>
175259 withComposePathAndGpuMode ( ( { buildSkillerMode, composePath, gpuMode } ) =>
176- computeControllerRevision ( composePath , gpuMode , buildSkillerMode )
177- ) . pipe ( Effect . tap ( ( revision ) => persistControllerRevision ( revision ) ) )
260+ Effect . gen ( function * ( _ ) {
261+ const path = yield * _ ( Path . Path )
262+ if ( buildSkillerMode === "1" ) {
263+ yield * _ ( ensureSkillerSubmoduleInitialized ( path . dirname ( composePath ) ) )
264+ }
265+ return yield * _ ( computeControllerRevision ( composePath , gpuMode , buildSkillerMode ) )
266+ } )
267+ ) . pipe (
268+ Effect . tap ( ( revision ) => persistControllerRevision ( revision ) )
269+ )
0 commit comments