4runr-os 2.10.69 → 2.10.70

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1606,7 +1606,7 @@ async function handleGatewayConnect(ctx) {
1606
1606
  level: 'info',
1607
1607
  message: 'Attempting to restart Docker containers...',
1608
1608
  });
1609
- const redisUrl = tryTuiAutostartDockerComposeStack(bundleCheck.path, activityLog, getCurrentTime);
1609
+ const redisUrl = await tryTuiAutostartDockerComposeStack(bundleCheck.path, activityLog, getCurrentTime);
1610
1610
  if (redisUrl) {
1611
1611
  await new Promise((r) => setTimeout(r, 3000));
1612
1612
  // Re-verify
@@ -2267,6 +2267,8 @@ async function isGatewayHealthy(url) {
2267
2267
  }
2268
2268
  }
2269
2269
  const TUI_LOCAL_REDIS_DEFAULT = 'redis://127.0.0.1:6379';
2270
+ const TUI_DB_CONTAINERS = ['4runr-postgres', '4runr-redis'];
2271
+ const TUI_COMPOSE_PROJECT = '4runr';
2270
2272
  function tuiAutostartDockerStackDisabledByEnv() {
2271
2273
  if (process.env['FOURRUNR_NO_AUTO_REDIS'] === '1' || process.env['FOURRUNR_NO_AUTO_REDIS'] === 'true') {
2272
2274
  return true;
@@ -2279,24 +2281,90 @@ function tuiAutostartDockerStackDisabledByEnv() {
2279
2281
  }
2280
2282
  return false;
2281
2283
  }
2282
- /**
2283
- * Start Postgres + Redis together via the same compose file the Gateway uses.
2284
- * Previously we only ran a standalone Redis container, so Postgres never came up and Prisma stayed in memory mode.
2285
- */
2286
- function tryTuiAutostartDockerComposeStack(bundlePath, activityLog, getCurrentTimeFn) {
2287
- if (tuiAutostartDockerStackDisabledByEnv()) {
2284
+ function tuiDockerShell() {
2285
+ return process.platform === 'win32' ? (process.env['ComSpec'] || 'cmd.exe') : '/bin/sh';
2286
+ }
2287
+ function tuiIsContainerRunning(containerName) {
2288
+ try {
2289
+ const state = execSync(`docker inspect --format="{{.State.Running}}" ${containerName}`, {
2290
+ encoding: 'utf-8',
2291
+ stdio: 'pipe',
2292
+ timeout: 5000,
2293
+ windowsHide: true,
2294
+ shell: tuiDockerShell(),
2295
+ }).trim();
2296
+ return state === 'true';
2297
+ }
2298
+ catch {
2299
+ return false;
2300
+ }
2301
+ }
2302
+ function tuiStartStoppedContainers(activityLog, getCurrentTimeFn) {
2303
+ const stopped = TUI_DB_CONTAINERS.filter((name) => !tuiIsContainerRunning(name));
2304
+ if (stopped.length === 0) {
2305
+ return;
2306
+ }
2307
+ try {
2308
+ execSync(`docker start ${stopped.join(' ')}`, {
2309
+ stdio: 'ignore',
2310
+ windowsHide: true,
2311
+ timeout: 60000,
2312
+ shell: tuiDockerShell(),
2313
+ });
2288
2314
  activityLog.push({
2289
2315
  timestamp: getCurrentTimeFn(),
2290
2316
  level: 'info',
2291
- message: 'Autostart Docker stack: skipped (FOURRUNR_NO_AUTO_DOCKER / FOURRUNR_NO_AUTO_REDIS / FOURRUNR_SPAWN_REDIS=0).',
2317
+ message: `Docker: started stopped container(s): ${stopped.join(', ')}`,
2292
2318
  });
2293
- return undefined;
2294
2319
  }
2295
- if (String(process.env['REDIS_URL'] || '').trim() !== '') {
2320
+ catch {
2321
+ // May not exist yet — compose up will create them
2322
+ }
2323
+ }
2324
+ async function tuiWaitForContainerHealthy(containerName, timeoutMs) {
2325
+ const start = Date.now();
2326
+ while (Date.now() - start < timeoutMs) {
2327
+ try {
2328
+ const health = execSync(`docker inspect --format="{{.State.Health.Status}}" ${containerName}`, {
2329
+ encoding: 'utf-8',
2330
+ stdio: 'pipe',
2331
+ timeout: 5000,
2332
+ windowsHide: true,
2333
+ shell: tuiDockerShell(),
2334
+ }).trim();
2335
+ if (health === 'healthy') {
2336
+ return true;
2337
+ }
2338
+ if (health === '' || health === '<no value>') {
2339
+ const running = execSync(`docker inspect --format="{{.State.Running}}" ${containerName}`, {
2340
+ encoding: 'utf-8',
2341
+ stdio: 'pipe',
2342
+ timeout: 5000,
2343
+ windowsHide: true,
2344
+ shell: tuiDockerShell(),
2345
+ }).trim();
2346
+ if (running === 'true') {
2347
+ return true;
2348
+ }
2349
+ }
2350
+ }
2351
+ catch {
2352
+ // keep waiting
2353
+ }
2354
+ await new Promise((r) => setTimeout(r, 1000));
2355
+ }
2356
+ return false;
2357
+ }
2358
+ /**
2359
+ * Start Postgres + Redis together via the same compose file the Gateway uses.
2360
+ * Watchdog stops both when 4r exits — always ensure both are back up on connect.
2361
+ */
2362
+ async function tryTuiAutostartDockerComposeStack(bundlePath, activityLog, getCurrentTimeFn) {
2363
+ if (tuiAutostartDockerStackDisabledByEnv()) {
2296
2364
  activityLog.push({
2297
2365
  timestamp: getCurrentTimeFn(),
2298
2366
  level: 'info',
2299
- message: 'Autostart Docker stack: skipped (REDIS_URL already set manage Postgres yourself if needed).',
2367
+ message: 'Autostart Docker stack: skipped (FOURRUNR_NO_AUTO_DOCKER / FOURRUNR_NO_AUTO_REDIS / FOURRUNR_SPAWN_REDIS=0).',
2300
2368
  });
2301
2369
  return undefined;
2302
2370
  }
@@ -2309,21 +2377,59 @@ function tryTuiAutostartDockerComposeStack(bundlePath, activityLog, getCurrentTi
2309
2377
  });
2310
2378
  return undefined;
2311
2379
  }
2312
- const dockerShell = process.platform === 'win32' ? (process.env['ComSpec'] || 'cmd.exe') : '/bin/sh';
2380
+ const dockerShell = tuiDockerShell();
2381
+ const allRunning = TUI_DB_CONTAINERS.every((name) => tuiIsContainerRunning(name));
2382
+ if (allRunning) {
2383
+ activityLog.push({
2384
+ timestamp: getCurrentTimeFn(),
2385
+ level: 'info',
2386
+ message: 'Docker: Postgres + Redis already running (4runr-postgres :5432, 4runr-redis :6379).',
2387
+ });
2388
+ return String(process.env['REDIS_URL'] || '').trim() || TUI_LOCAL_REDIS_DEFAULT;
2389
+ }
2390
+ if (String(process.env['REDIS_URL'] || '').trim() !== '') {
2391
+ activityLog.push({
2392
+ timestamp: getCurrentTimeFn(),
2393
+ level: 'info',
2394
+ message: 'REDIS_URL is set in environment — still starting Postgres + Redis Docker stack for persistence.',
2395
+ });
2396
+ }
2313
2397
  try {
2314
- execSync(`docker compose -f "${composeFile}" up -d`, {
2315
- cwd: bundlePath,
2316
- stdio: 'ignore',
2317
- windowsHide: true,
2318
- timeout: 120000,
2319
- shell: dockerShell,
2398
+ tuiStartStoppedContainers(activityLog, getCurrentTimeFn);
2399
+ if (!TUI_DB_CONTAINERS.every((name) => tuiIsContainerRunning(name))) {
2400
+ execSync(`docker compose -p ${TUI_COMPOSE_PROJECT} -f "${composeFile}" up -d`, {
2401
+ cwd: bundlePath,
2402
+ stdio: 'ignore',
2403
+ windowsHide: true,
2404
+ timeout: 120000,
2405
+ shell: dockerShell,
2406
+ });
2407
+ }
2408
+ activityLog.push({
2409
+ timestamp: getCurrentTimeFn(),
2410
+ level: 'info',
2411
+ message: 'Waiting for Postgres + Redis to become healthy...',
2320
2412
  });
2413
+ // Postgres cold start after watchdog stop can take 30–60s
2414
+ const pgOk = await tuiWaitForContainerHealthy('4runr-postgres', 60000);
2415
+ const redisOk = await tuiWaitForContainerHealthy('4runr-redis', 15000);
2416
+ if (!pgOk || !redisOk) {
2417
+ const down = [!pgOk ? '4runr-postgres' : null, !redisOk ? '4runr-redis' : null]
2418
+ .filter(Boolean)
2419
+ .join(', ');
2420
+ activityLog.push({
2421
+ timestamp: getCurrentTimeFn(),
2422
+ level: 'warning',
2423
+ message: `Autostart Docker stack: ${down} did not become healthy in time. Gateway may fall back to memory DB.`,
2424
+ });
2425
+ return undefined;
2426
+ }
2321
2427
  activityLog.push({
2322
2428
  timestamp: getCurrentTimeFn(),
2323
2429
  level: 'success',
2324
2430
  message: 'Docker: Postgres + Redis started via docker-compose.local.yml (4runr-postgres :5432, 4runr-redis :6379).',
2325
2431
  });
2326
- return TUI_LOCAL_REDIS_DEFAULT;
2432
+ return String(process.env['REDIS_URL'] || '').trim() || TUI_LOCAL_REDIS_DEFAULT;
2327
2433
  }
2328
2434
  catch (e) {
2329
2435
  activityLog.push({
@@ -2387,10 +2493,7 @@ async function startLocalGatewayAndWait(bundlePath, url, activityLog, getCurrent
2387
2493
  });
2388
2494
  shutdownAutostartedGateway(portNum);
2389
2495
  await new Promise((r) => setTimeout(r, 400));
2390
- const tuiRedisUrl = tryTuiAutostartDockerComposeStack(bundlePath, activityLog, getCurrentTimeFn);
2391
- if (tuiRedisUrl) {
2392
- await new Promise((r) => setTimeout(r, 1500));
2393
- }
2496
+ const tuiRedisUrl = await tryTuiAutostartDockerComposeStack(bundlePath, activityLog, getCurrentTimeFn);
2394
2497
  const passThroughRedis = process.env['REDIS_URL'];
2395
2498
  if (!tuiRedisUrl && !String(passThroughRedis || '').trim()) {
2396
2499
  activityLog.push({
@@ -2414,6 +2517,9 @@ async function startLocalGatewayAndWait(bundlePath, url, activityLog, getCurrent
2414
2517
  };
2415
2518
  if (tuiRedisUrl) {
2416
2519
  childEnv['REDIS_URL'] = tuiRedisUrl;
2520
+ childEnv['DATABASE_URL'] =
2521
+ process.env['DATABASE_URL'] ||
2522
+ 'postgresql://4runr:4runr_dev_pass@localhost:5432/4runr_local';
2417
2523
  }
2418
2524
  const commonOpts = {
2419
2525
  cwd: bundlePath,