@@ -16,6 +16,71 @@ export function docker(args: string[]): void {
1616 }
1717}
1818
19+ function dockerOutput ( args : string [ ] ) : string | null {
20+ const result = spawnSync ( 'docker' , args , { encoding : 'utf8' } )
21+ return result . status === 0 ? result . stdout . trim ( ) : null
22+ }
23+
24+ interface ManagedContainer {
25+ running : boolean
26+ dsn : string
27+ }
28+
29+ /**
30+ * Recovers everything needed to reach an existing managed container from Docker
31+ * itself, so re-running the wizard is idempotent.
32+ *
33+ * Both facts used to be unrecoverable: the password is generated at creation and
34+ * only lived in the env files the run wrote, and the host port varies (5433 when
35+ * 5432 is taken). Reading them back turns "a container already exists" from a
36+ * fatal name collision into a reuse.
37+ */
38+ function inspectManagedContainer ( ) : ManagedContainer | null {
39+ const running = dockerOutput ( [ 'inspect' , DB_CONTAINER , '--format' , '{{.State.Running}}' ] )
40+ if ( running === null ) return null
41+
42+ const env = dockerOutput ( [
43+ 'inspect' ,
44+ DB_CONTAINER ,
45+ '--format' ,
46+ '{{range .Config.Env}}{{println .}}{{end}}' ,
47+ ] )
48+ const password = env
49+ ?. split ( '\n' )
50+ . find ( ( line ) => line . startsWith ( 'POSTGRES_PASSWORD=' ) )
51+ ?. slice ( 'POSTGRES_PASSWORD=' . length )
52+ if ( ! password ) return null
53+
54+ // `docker port` only reports a published port while the container runs; the
55+ // static config carries it either way.
56+ const hostPort = dockerOutput ( [
57+ 'inspect' ,
58+ DB_CONTAINER ,
59+ '--format' ,
60+ '{{(index .HostConfig.PortBindings "5432/tcp" 0).HostPort}}' ,
61+ ] )
62+ if ( ! hostPort ) return null
63+
64+ return {
65+ running : running === 'true' ,
66+ dsn : `postgresql://postgres:${ password } @localhost:${ hostPort } /simstudio` ,
67+ }
68+ }
69+
70+ /** Starts the container if needed and returns its DSN, or null if it won't answer. */
71+ async function reuseManagedContainer ( container : ManagedContainer ) : Promise < string | null > {
72+ if ( ! container . running ) docker ( [ 'start' , DB_CONTAINER ] )
73+ const spin = p . spinner ( )
74+ spin . start ( `Reusing existing ${ DB_CONTAINER } container…` )
75+ const healthy = await waitFor ( async ( ) => ( await pgProbe ( container . dsn ) ) . ok , 30_000 , 1500 )
76+ spin . stop (
77+ healthy
78+ ? `Postgres running in ${ DB_CONTAINER } on :${ new URL ( container . dsn ) . port } `
79+ : `${ glyph . warn } ${ DB_CONTAINER } exists but is not answering`
80+ )
81+ return healthy ? container . dsn : null
82+ }
83+
1984async function probeWithSpinner ( dsn : string , label : string ) : Promise < boolean > {
2085 const spin = p . spinner ( )
2186 spin . start ( label )
@@ -57,7 +122,32 @@ async function promptExternalDsn(): Promise<string> {
57122 }
58123}
59124
125+ /**
126+ * Provisions the managed container, reconciling with one that already exists
127+ * rather than colliding on the name. Recreating is always an explicit choice —
128+ * the data volume outlives the container, so a silent recreate would quietly
129+ * re-point setup at data the user may not expect.
130+ */
60131async function startManagedContainer ( detection : Detection ) : Promise < string > {
132+ const existing = inspectManagedContainer ( )
133+ if ( existing ) {
134+ const reused = await reuseManagedContainer ( existing )
135+ if ( reused ) return reused
136+
137+ const recreate = await p . confirm ( {
138+ message : `${ DB_CONTAINER } exists but is not answering. Remove and recreate it? Its data volume is kept.` ,
139+ initialValue : true ,
140+ } )
141+ if ( ! recreate ) {
142+ throw new SetupError ( `the existing ${ DB_CONTAINER } container is not usable.` , [
143+ `inspect: ${ theme . command ( `docker logs ${ DB_CONTAINER } ` ) } ` ,
144+ `remove it: ${ theme . command ( `docker rm -f ${ DB_CONTAINER } ` ) } ` ,
145+ `start clean: ${ theme . command ( 'docker volume rm sim-postgres-data' ) } drops its data too` ,
146+ ] )
147+ }
148+ docker ( [ 'rm' , '-f' , DB_CONTAINER ] )
149+ }
150+
61151 const password = generateSecret ( ) . slice ( 0 , 24 )
62152 const hostPort = detection . postgresPortOpen ? 5433 : 5432
63153 const dsn = `postgresql://postgres:${ password } @localhost:${ hostPort } /simstudio`
@@ -96,26 +186,6 @@ async function startManagedContainer(detection: Detection): Promise<string> {
96186 return dsn
97187}
98188
99- async function restartManagedContainer ( existingDsn : string | undefined ) : Promise < string > {
100- if ( ! existingDsn ) {
101- throw new SetupError (
102- `found a stopped ${ DB_CONTAINER } container but no DATABASE_URL to reach it (the generated password lived in your env files).` ,
103- [
104- `remove it: ${ theme . command ( `docker rm ${ DB_CONTAINER } ` ) } ` ,
105- `also drop its data volume if you don't need it: ${ theme . command ( 'docker volume rm sim-postgres-data' ) } ` ,
106- 'then re-run the wizard to provision a fresh one' ,
107- ]
108- )
109- }
110- docker ( [ 'start' , DB_CONTAINER ] )
111- const spin = p . spinner ( )
112- spin . start ( `Starting existing ${ DB_CONTAINER } container…` )
113- const healthy = await waitFor ( async ( ) => ( await pgProbe ( existingDsn ) ) . ok , 30_000 , 1500 )
114- spin . stop ( healthy ? `${ DB_CONTAINER } running` : `${ glyph . fail } ${ DB_CONTAINER } did not come up` )
115- if ( ! healthy ) throw new Error ( `${ DB_CONTAINER } started but is not answering on ${ existingDsn } ` )
116- return existingDsn
117- }
118-
119189/**
120190 * The mode-B database ladder: reuse a working DSN, offer (never silently adopt)
121191 * a Postgres already on 5432, start/reuse the wizard-managed pgvector
@@ -127,8 +197,12 @@ export async function resolveDatabase(detection: Detection, existingDsn?: string
127197 return existingDsn
128198 }
129199
130- if ( detection . dbContainer ?. managed && detection . dbContainer . state === 'stopped' ) {
131- return restartManagedContainer ( existingDsn )
200+ // Any managed container, running or stopped — a running one used to fall
201+ // through to `docker run` and die on the name collision.
202+ if ( detection . dbContainer ?. managed ) {
203+ const existing = inspectManagedContainer ( )
204+ const reused = existing && ( await reuseManagedContainer ( existing ) )
205+ if ( reused ) return reused
132206 }
133207
134208 if (
0 commit comments