@@ -26,21 +26,43 @@ export interface Finding {
2626 autofix ?: ( ) => void
2727}
2828
29+ /**
30+ * Which env-file topology this install uses. Compose mode writes a single root
31+ * `.env` (that's what `docker-compose.*.yml` reads via `env_file`), dev mode
32+ * writes the three per-app files. Checking for the wrong one reports a healthy
33+ * install as broken, so the layout is derived and every check consults it.
34+ */
35+ export type EnvLayout = 'split' | 'root' | 'none'
36+
2937export interface CheckContext {
3038 env : Record < EnvTarget , EnvFile >
39+ layout : EnvLayout
40+ /** The file holding app configuration for this layout — what coherence reads. */
41+ primary : EnvFile
3142 live : boolean
3243}
3344
45+ /** Split wins when both exist: the per-app files are what a dev run actually loads. */
46+ function detectLayout ( env : Record < EnvTarget , EnvFile > ) : EnvLayout {
47+ if ( env . sim . exists || env . realtime . exists || env . db . exists ) return 'split'
48+ return env . root . exists ? 'root' : 'none'
49+ }
50+
51+ /** Targets whose files this layout expects to exist. */
52+ function layoutTargets ( layout : EnvLayout ) : EnvTarget [ ] {
53+ if ( layout === 'split' ) return [ 'sim' , 'realtime' , 'db' ]
54+ return layout === 'root' ? [ 'root' ] : [ ]
55+ }
56+
3457export function loadCheckContext ( live : boolean ) : CheckContext {
35- return {
36- env : {
37- sim : readEnvFile ( 'sim' ) ,
38- realtime : readEnvFile ( 'realtime' ) ,
39- db : readEnvFile ( 'db' ) ,
40- root : readEnvFile ( 'root' ) ,
41- } ,
42- live,
58+ const env = {
59+ sim : readEnvFile ( 'sim' ) ,
60+ realtime : readEnvFile ( 'realtime' ) ,
61+ db : readEnvFile ( 'db' ) ,
62+ root : readEnvFile ( 'root' ) ,
4363 }
64+ const layout = detectLayout ( env )
65+ return { env, layout, primary : layout === 'root' ? env . root : env . sim , live }
4466}
4567
4668const REQUIRED_KEYS : Partial < Record < EnvTarget , string [ ] > > = {
@@ -70,8 +92,18 @@ function rel(file: EnvFile): string {
7092}
7193
7294function checkFiles ( ctx : CheckContext ) : Finding [ ] {
95+ if ( ctx . layout === 'none' ) {
96+ return [
97+ {
98+ group : 'files' ,
99+ status : 'fail' ,
100+ message : 'no env files found' ,
101+ fix : 'run: bun run setup' ,
102+ } ,
103+ ]
104+ }
73105 const findings : Finding [ ] = [ ]
74- for ( const target of [ 'sim' , 'realtime' , 'db' ] as const ) {
106+ for ( const target of layoutTargets ( ctx . layout ) ) {
75107 const file = ctx . env [ target ]
76108 if ( file . exists ) {
77109 findings . push ( { group : 'files' , status : 'pass' , message : `${ rel ( file ) } exists` } )
@@ -124,11 +156,13 @@ function autofixForMissing(
124156function checkSchema ( ctx : CheckContext ) : Finding [ ] {
125157 const findings : Finding [ ] = [ ]
126158 const production = process . env . NODE_ENV === 'production'
127- for ( const target of [ 'sim' , 'realtime' , 'db' ] as const ) {
159+ for ( const target of layoutTargets ( ctx . layout ) ) {
128160 const file = ctx . env [ target ]
129161 if ( ! file . exists ) continue
130162 const missing : string [ ] = [ ]
131- for ( const key of REQUIRED_KEYS [ target ] ?? [ ] ) {
163+ // The single root file feeds both containers, so it must satisfy the app's
164+ // requirements — a superset of realtime's.
165+ for ( const key of REQUIRED_KEYS [ target === 'root' ? 'sim' : target ] ?? [ ] ) {
132166 const value = file . vars . get ( key )
133167 if ( ! value ) {
134168 missing . push ( key )
@@ -189,6 +223,13 @@ function checkSchema(ctx: CheckContext): Finding[] {
189223}
190224
191225function checkConsistency ( ctx : CheckContext ) : Finding [ ] {
226+ // Consistency is about the same key agreeing across files; a single root
227+ // file has nothing to disagree with.
228+ if ( ctx . layout !== 'split' ) {
229+ return ctx . layout === 'root'
230+ ? [ { group : 'consistency' , status : 'skip' , message : 'single .env — nothing to mirror' } ]
231+ : [ ]
232+ }
192233 const findings : Finding [ ] = [ ]
193234 const { sim, realtime, db } = ctx . env
194235 if ( sim . exists && realtime . exists ) {
@@ -233,7 +274,7 @@ function checkConsistency(ctx: CheckContext): Finding[] {
233274
234275function checkCoherence ( ctx : CheckContext ) : Finding [ ] {
235276 const findings : Finding [ ] = [ ]
236- const sim = ctx . env . sim
277+ const sim = ctx . primary
237278 if ( ! sim . exists ) return findings
238279 if ( isTruthy ( sim . vars . get ( 'TRIGGER_DEV_ENABLED' ) ) ) {
239280 const missing = [ 'TRIGGER_SECRET_KEY' , 'TRIGGER_PROJECT_ID' ] . filter ( ( k ) => ! sim . vars . get ( k ) )
@@ -556,7 +597,7 @@ async function checkOllama(sim: EnvFile): Promise<Finding[]> {
556597 * report stays deterministic regardless of which probe settles first.
557598 */
558599async function checkLive ( ctx : CheckContext ) : Promise < Finding [ ] > {
559- const sim = ctx . env . sim
600+ const sim = ctx . primary
560601 const [ database , redis , app , realtime , ollama ] = await Promise . all ( [
561602 checkDatabase ( sim ) ,
562603 checkRedis ( sim ) ,
0 commit comments