|
1 | 1 | import { spawnSync } from 'node:child_process' |
2 | | -import { readFileSync } from 'node:fs' |
| 2 | +import { existsSync, readFileSync } from 'node:fs' |
3 | 3 | import path from 'node:path' |
4 | 4 | import { ensureProductionComposeFile } from './compose-asset' |
5 | | -import { resolveSetupContextAtRoot } from './context' |
| 5 | +import { legacyComposeProjectName } from './compose-project' |
| 6 | +import { directoryOverride, resolveSetupContextAtRoot, SETUP_CONTEXT } from './context' |
6 | 7 | import { DB_CONTAINER, type Detection, REDIS_CONTAINER, runDetection } from './detect' |
7 | | -import { archiveEnvFile, archiveFile, ROOT } from './env-files' |
| 8 | +import { archiveEnvFile, archiveFile, parseEnv, ROOT } from './env-files' |
8 | 9 | import { SetupError } from './errors' |
9 | 10 | import { forwardCommands, isLocalKubeContext } from './modes/k8s' |
10 | 11 | import { httpHealth } from './probes' |
@@ -67,7 +68,7 @@ function dockerRun(args: string[], failMessage: string, cwd: string = ROOT): voi |
67 | 68 | } |
68 | 69 | } |
69 | 70 |
|
70 | | -interface ComposeInstall { |
| 71 | +export interface ComposeInstall { |
71 | 72 | kind: 'compose' |
72 | 73 | /** Absolute path to the compose file Docker recorded for the project. */ |
73 | 74 | file: string |
@@ -158,6 +159,28 @@ function composeInstalls(): ComposeInstall[] { |
158 | 159 | return installs |
159 | 160 | } |
160 | 161 |
|
| 162 | +/** Restores a setup-managed Compose target from disk even when `down` removed every container. */ |
| 163 | +export function composeInstallFromDirectory( |
| 164 | + root: string, |
| 165 | + activeInstalls: readonly ComposeInstall[] |
| 166 | +): ComposeInstall | null { |
| 167 | + const file = path.join(root, 'docker-compose.prod.yml') |
| 168 | + if (!isSimComposeFile(file)) return null |
| 169 | + if (activeInstalls.some((install) => path.resolve(install.file) === path.resolve(file))) |
| 170 | + return null |
| 171 | + |
| 172 | + const envFile = path.join(root, '.env') |
| 173 | + const configuredProject = existsSync(envFile) |
| 174 | + ? parseEnv(readFileSync(envFile, 'utf8')).get('COMPOSE_PROJECT_NAME') |
| 175 | + : undefined |
| 176 | + return { |
| 177 | + kind: 'compose', |
| 178 | + file, |
| 179 | + dir: root, |
| 180 | + project: configuredProject || legacyComposeProjectName(root), |
| 181 | + } |
| 182 | +} |
| 183 | + |
161 | 184 | /** |
162 | 185 | * Compose args for an op on a detected install. `-p` is not optional: without it |
163 | 186 | * Compose re-derives the project from the working directory, and that name is |
@@ -203,14 +226,29 @@ function k8sInstall(detection: Detection): K8sInstall | null { |
203 | 226 | } |
204 | 227 |
|
205 | 228 | function detectInstalls(detection: Detection): Install[] { |
206 | | - const installs: Install[] = [...composeInstalls()] |
| 229 | + const compose = composeInstalls() |
| 230 | + if (SETUP_CONTEXT.kind === 'standalone' && SETUP_CONTEXT.existing) { |
| 231 | + const fromDirectory = composeInstallFromDirectory(SETUP_CONTEXT.root, compose) |
| 232 | + if (fromDirectory) compose.push(fromDirectory) |
| 233 | + } |
| 234 | + const installs: Install[] = [...compose] |
207 | 235 | const dev = devInstall(detection) |
208 | 236 | if (dev) installs.push(dev) |
209 | 237 | const k8s = k8sInstall(detection) |
210 | 238 | if (k8s) installs.push(k8s) |
211 | 239 | return installs |
212 | 240 | } |
213 | 241 |
|
| 242 | +/** Limits an explicit standalone `--dir` command to that installation. */ |
| 243 | +function scopeInstallsToInvocation(installs: Install[]): Install[] { |
| 244 | + if (SETUP_CONTEXT.kind !== 'standalone' || directoryOverride(process.argv.slice(2)) === null) { |
| 245 | + return installs |
| 246 | + } |
| 247 | + return installs.filter( |
| 248 | + (install) => install.kind === 'compose' && path.resolve(install.dir) === SETUP_CONTEXT.root |
| 249 | + ) |
| 250 | +} |
| 251 | + |
214 | 252 | function describeInstall(install: Install): string { |
215 | 253 | if (install.kind === 'compose') |
216 | 254 | return `Docker Compose (project ${install.project} in ${install.dir})` |
@@ -501,7 +539,7 @@ async function reset(install: Install | null): Promise<void> { |
501 | 539 |
|
502 | 540 | async function status(): Promise<void> { |
503 | 541 | const detection = await runDetection() |
504 | | - const installs = detectInstalls(detection) |
| 542 | + const installs = scopeInstallsToInvocation(detectInstalls(detection)) |
505 | 543 | const docker = dockerReachable() |
506 | 544 | console.log(`\n${theme.heading('◆ Sim status')}\n`) |
507 | 545 | // Every container probe goes through Docker, so when the daemon is down the |
@@ -540,7 +578,7 @@ async function status(): Promise<void> { |
540 | 578 | export async function runLifecycle(command: LifecycleCommand): Promise<void> { |
541 | 579 | if (command === 'status') return status() |
542 | 580 |
|
543 | | - const installs = detectInstalls(await runDetection()) |
| 581 | + const installs = scopeInstallsToInvocation(detectInstalls(await runDetection())) |
544 | 582 |
|
545 | 583 | // Reset stays useful with nothing running — it still archives stray .env files. |
546 | 584 | if (command === 'reset') return reset(await resolveInstall(installs)) |
|
0 commit comments