@@ -8,8 +8,11 @@ import {
88 readProjectConfig ,
99 runDockerComposeUpWithPortCheck
1010} from "@effect-template/lib"
11+ import * as FileSystem from "@effect/platform/FileSystem"
12+ import * as Path from "@effect/platform/Path"
1113import { runCommandCapture } from "@effect-template/lib/shell/command-runner"
1214import { CommandFailedError } from "@effect-template/lib/shell/errors"
15+ import { defaultProjectsRoot , resolvePathFromCwd } from "@effect-template/lib/usecases/path-helpers"
1316import { deleteDockerGitProject } from "@effect-template/lib/usecases/projects"
1417import type { RawOptions } from "@effect-template/lib/core/command-options"
1518import type { ProjectItem } from "@effect-template/lib/usecases/projects"
@@ -159,6 +162,52 @@ const resolveCreatedProject = (
159162 } )
160163 )
161164
165+ const normalizeAuthorizedKeys = ( value : string ) : ReadonlyArray < string > =>
166+ value
167+ . split ( / \r ? \n / u)
168+ . map ( ( line ) => line . trim ( ) )
169+ . filter ( ( line ) => line . length > 0 )
170+
171+ const mergeAuthorizedKeys = (
172+ current : ReadonlyArray < string > ,
173+ next : ReadonlyArray < string >
174+ ) : string => {
175+ const merged = [ ...current ]
176+ for ( const line of next ) {
177+ if ( ! merged . includes ( line ) ) {
178+ merged . push ( line )
179+ }
180+ }
181+ return merged . length === 0 ? "" : `${ merged . join ( "\n" ) } \n`
182+ }
183+
184+ export const seedAuthorizedKeysForCreate = (
185+ outDir : string ,
186+ authorizedKeysContents : string | undefined
187+ ) =>
188+ Effect . gen ( function * ( _ ) {
189+ const normalized = normalizeAuthorizedKeys ( authorizedKeysContents ?? "" )
190+ if ( normalized . length === 0 ) {
191+ return
192+ }
193+
194+ const fs = yield * _ ( FileSystem . FileSystem )
195+ const path = yield * _ ( Path . Path )
196+ const defaultAuthorizedKeysPath = path . join ( defaultProjectsRoot ( process . cwd ( ) ) , "authorized_keys" )
197+ const resolvedOutDir = resolvePathFromCwd ( path , process . cwd ( ) , outDir )
198+ const projectAuthorizedKeysPath = path . join ( resolvedOutDir , "authorized_keys" )
199+ const targets = Array . from ( new Set ( [ defaultAuthorizedKeysPath , projectAuthorizedKeysPath ] ) )
200+
201+ for ( const target of targets ) {
202+ const exists = yield * _ ( fs . exists ( target ) )
203+ const current = exists ? yield * _ ( fs . readFileString ( target ) ) : ""
204+ const merged = mergeAuthorizedKeys ( normalizeAuthorizedKeys ( current ) , normalized )
205+
206+ yield * _ ( fs . makeDirectory ( path . dirname ( target ) , { recursive : true } ) )
207+ yield * _ ( fs . writeFileString ( target , merged ) )
208+ }
209+ } )
210+
162211export const listProjects = ( ) =>
163212 listProjectItems . pipe (
164213 Effect . flatMap ( ( projects ) => Effect . forEach ( projects , withProjectRuntime , { concurrency : "unbounded" } ) ) ,
@@ -218,6 +267,7 @@ export const createProjectFromRequest = (
218267 ...( request . enableMcpPlaywright === undefined ? { } : { enableMcpPlaywright : request . enableMcpPlaywright } ) ,
219268 ...( request . outDir === undefined ? { } : { outDir : request . outDir } ) ,
220269 ...( request . gitTokenLabel === undefined ? { } : { gitTokenLabel : request . gitTokenLabel } ) ,
270+ ...( request . skipGithubAuth === undefined ? { } : { skipGithubAuth : request . skipGithubAuth } ) ,
221271 ...( request . codexTokenLabel === undefined ? { } : { codexTokenLabel : request . codexTokenLabel } ) ,
222272 ...( request . claudeTokenLabel === undefined ? { } : { claudeTokenLabel : request . claudeTokenLabel } ) ,
223273 ...( request . agentAutoMode === undefined ? { } : { agentAutoMode : request . agentAutoMode } ) ,
@@ -245,6 +295,8 @@ export const createProjectFromRequest = (
245295 waitForClone : request . waitForClone ?? parsed . right . waitForClone
246296 }
247297
298+ yield * _ ( seedAuthorizedKeysForCreate ( command . outDir , request . authorizedKeysContents ) )
299+
248300 yield * _ ( ensureGithubAuthForCreate ( command . config ) )
249301
250302 yield * _ (
@@ -297,13 +349,89 @@ const markDeployment = (projectId: string, phase: string, message: string) =>
297349 emitProjectEvent ( projectId , "project.deployment.status" , { phase, message } )
298350 } )
299351
352+ const syncContainerAuthorizedKeys = (
353+ project : ProjectItem
354+ ) =>
355+ Effect . gen ( function * ( _ ) {
356+ const path = yield * _ ( Path . Path )
357+ const sourcePath = path . join ( project . projectDir , "authorized_keys" )
358+
359+ yield * _ (
360+ runCommandCapture (
361+ {
362+ cwd : project . projectDir ,
363+ command : "docker" ,
364+ args : [
365+ "exec" ,
366+ project . containerName ,
367+ "sh" ,
368+ "-c" ,
369+ [
370+ "set -eu" ,
371+ `mkdir -p /home/${ project . sshUser } /.docker-git` ,
372+ `mkdir -p /home/${ project . sshUser } /.ssh`
373+ ] . join ( "; " )
374+ ]
375+ } ,
376+ [ 0 ] ,
377+ ( exitCode ) => new CommandFailedError ( { command : "docker exec prepare authorized_keys sync" , exitCode } )
378+ ) . pipe ( Effect . asVoid )
379+ )
380+
381+ yield * _ (
382+ runCommandCapture (
383+ {
384+ cwd : project . projectDir ,
385+ command : "docker" ,
386+ args : [
387+ "cp" ,
388+ sourcePath ,
389+ `${ project . containerName } :/home/${ project . sshUser } /.docker-git/authorized_keys`
390+ ]
391+ } ,
392+ [ 0 ] ,
393+ ( exitCode ) => new CommandFailedError ( { command : "docker cp authorized_keys" , exitCode } )
394+ ) . pipe ( Effect . asVoid )
395+ )
396+
397+ yield * _ (
398+ runCommandCapture (
399+ {
400+ cwd : project . projectDir ,
401+ command : "docker" ,
402+ args : [
403+ "exec" ,
404+ project . containerName ,
405+ "sh" ,
406+ "-c" ,
407+ [
408+ "set -eu" ,
409+ `cp /home/${ project . sshUser } /.docker-git/authorized_keys /home/${ project . sshUser } /.ssh/authorized_keys` ,
410+ `chown ${ project . sshUser } :${ project . sshUser } /home/${ project . sshUser } /.docker-git/authorized_keys` ,
411+ `chmod 600 /home/${ project . sshUser } /.docker-git/authorized_keys` ,
412+ `chown ${ project . sshUser } :${ project . sshUser } /home/${ project . sshUser } /.ssh/authorized_keys` ,
413+ `chmod 600 /home/${ project . sshUser } /.ssh/authorized_keys`
414+ ] . join ( "; " )
415+ ]
416+ } ,
417+ [ 0 ] ,
418+ ( exitCode ) => new CommandFailedError ( { command : "docker exec sync authorized_keys" , exitCode } )
419+ ) . pipe ( Effect . asVoid )
420+ )
421+ } )
422+
300423export const upProject = (
301- projectId : string
424+ projectId : string ,
425+ authorizedKeysContents ?: string
302426) =>
303427 Effect . gen ( function * ( _ ) {
304428 const project = yield * _ ( findProjectById ( projectId ) )
429+ yield * _ ( seedAuthorizedKeysForCreate ( project . projectDir , authorizedKeysContents ) )
305430 yield * _ ( markDeployment ( projectId , "build" , "docker compose up -d --build" ) )
306431 yield * _ ( runDockerComposeUpWithPortCheck ( project . projectDir ) )
432+ if ( ( authorizedKeysContents ?? "" ) . trim ( ) . length > 0 ) {
433+ yield * _ ( syncContainerAuthorizedKeys ( project ) )
434+ }
307435 yield * _ ( markDeployment ( projectId , "running" , "Container running" ) )
308436 } )
309437
0 commit comments