@@ -20,6 +20,7 @@ import { runCommandCapture } from "@effect-template/lib/shell/command-runner"
2020import { CommandFailedError } from "@effect-template/lib/shell/errors"
2121import { defaultProjectsRoot , resolvePathFromCwd } from "@effect-template/lib/usecases/path-helpers"
2222import { deleteDockerGitProject } from "@effect-template/lib/usecases/projects"
23+ import { autoPullState } from "@effect-template/lib/usecases/state-repo"
2324import type { RawOptions } from "@effect-template/lib/core/command-options"
2425import type { CreateCommand as LibCreateCommand } from "@effect-template/lib/core/domain"
2526import type { ProjectItem } from "@effect-template/lib/usecases/projects"
@@ -216,6 +217,43 @@ const toProjectDetails = (
216217
217218const dbProjectDetails = ( project : ProjectItem ) : ProjectDetails => toProjectDetails ( project , dbProjectSummary ( project ) )
218219
220+ const toProjectInventoryReadError = ( error : unknown ) : ApiInternalError =>
221+ new ApiInternalError ( {
222+ message : `Failed to read docker-git project inventory: ${ String ( error ) } ` ,
223+ cause : error
224+ } )
225+
226+ /**
227+ * Refreshes controller project inventory from the shared state repository.
228+ *
229+ * @returns Effect that completes after best-effort state auto-pull.
230+ *
231+ * @pure false
232+ * @effect FileSystem, Path, CommandExecutor through autoPullState.
233+ * @invariant Respects DOCKER_GIT_STATE_AUTO_PULL=false and never fails.
234+ * @precondition Controller state root may or may not be a git repository.
235+ * @postcondition If auto-pull is enabled and succeeds, local inventory includes latest remote state.
236+ * @complexity O(1) git remote round-trip when enabled.
237+ * @throws Never - failures are logged by autoPullState.
238+ */
239+ // CHANGE: refresh shared state before project inventory reads
240+ // WHY: shell and web both read `/projects`; stale controller state caused issue #372
241+ // QUOTE(ТЗ): "project not synchronized"
242+ // REF: issue-372
243+ // SOURCE: https://github.com/ProverCoderAI/docker-git/issues/372
244+ // FORMAT THEOREM: remote_has(p) and pull_enabled -> p in inventory_after_refresh
245+ // PURITY: SHELL
246+ // EFFECT: Effect<void, never, FileSystem | Path | CommandExecutor>
247+ // INVARIANT: refresh failure cannot masquerade as an empty project list
248+ // COMPLEXITY: O(git pull)
249+ export const refreshProjectStateForInventory = ( ) => autoPullState
250+
251+ const readProjectItemsForInventory = ( ) =>
252+ refreshProjectStateForInventory ( ) . pipe (
253+ Effect . zipRight ( listProjectItems ) ,
254+ Effect . mapError ( toProjectInventoryReadError )
255+ )
256+
219257const runtimeProjectDetails = ( project : ProjectItem ) =>
220258 Effect . gen ( function * ( _ ) {
221259 const runtimeByProject = yield * _ ( loadProjectRuntimeByProject ( [ project ] ) )
@@ -255,7 +293,7 @@ const findProjectById = (projectId: string) =>
255293 Effect . gen ( function * ( _ ) {
256294 const path = yield * _ ( Path . Path )
257295 const aliases = projectIdAliases ( path , projectId )
258- const projects = yield * _ ( listProjectItems )
296+ const projects = yield * _ ( readProjectItemsForInventory ( ) )
259297 const project = projects . find ( ( item ) => item . projectDir === projectId )
260298 ?? projects . find ( ( item ) => aliases . has ( item . projectDir ) || aliases . has ( path . resolve ( item . projectDir ) ) )
261299 if ( project ) {
@@ -268,7 +306,7 @@ export const getProjectItemById = (projectId: string) => findProjectById(project
268306
269307const findProjectByKey = ( projectKey : string ) =>
270308 Effect . gen ( function * ( _ ) {
271- const projects = yield * _ ( listProjectItems )
309+ const projects = yield * _ ( readProjectItemsForInventory ( ) )
272310 const matches = projects . filter ( ( item ) => projectShortKey ( item . projectDir ) === projectKey )
273311 if ( matches . length === 0 ) {
274312 return yield * _ ( Effect . fail ( new ApiNotFoundError ( { message : `Project key not found: ${ projectKey } ` } ) ) )
@@ -587,9 +625,8 @@ const startCreateProjectJob = (
587625 } )
588626
589627export const listProjects = ( ) =>
590- listProjectItems . pipe (
591- Effect . map ( ( projects ) => projects . map ( ( project ) => dbProjectSummary ( project ) ) ) ,
592- Effect . catchAll ( ( ) => Effect . succeed ( [ ] as ReadonlyArray < ProjectSummary > ) )
628+ readProjectItemsForInventory ( ) . pipe (
629+ Effect . map ( ( projects ) => projects . map ( ( project ) => dbProjectSummary ( project ) ) )
593630 )
594631
595632export const applyAllProjects = ( activeOnly : boolean ) =>
@@ -650,6 +687,7 @@ export const createProjectFromRequest = (
650687 request : CreateProjectRequest
651688) =>
652689 Effect . gen ( function * ( _ ) {
690+ yield * _ ( refreshProjectStateForInventory ( ) )
653691 const prepared = yield * _ ( prepareCreateProjectRequest ( request ) . pipe ( Effect . mapError ( toProjectApiError ) ) )
654692 if ( request . async === true ) {
655693 return yield * _ ( startCreateProjectJob ( prepared ) )
0 commit comments