From 3de94e9eb496b14a54b464d8a62a29378d119279 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Sat, 8 Mar 2025 04:05:10 +0530 Subject: [PATCH 1/8] Implemented hot reload in CLI --- cli/hot-reload.sh | 17 ++++++++ cli/source/utils/docker-config.ts | 67 +++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+) create mode 100755 cli/hot-reload.sh create mode 100644 cli/source/utils/docker-config.ts diff --git a/cli/hot-reload.sh b/cli/hot-reload.sh new file mode 100755 index 00000000..4af65032 --- /dev/null +++ b/cli/hot-reload.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +export NODE_ENV=development +export SKIP_CONTAINER_REBUILD=true +export TS_NODE_TRANSPILE_ONLY=true + +PKG_MGR=$(command -v yarn >/dev/null && echo "yarn" || echo "npm") + +if [ ! -d "node_modules" ]; then + echo "Installing dependencies..." + $PKG_MGR install +fi + +mkdir -p .cache + +echo "Starting CLI with optimized hot reloading..." +$PKG_MGR run dev:hot diff --git a/cli/source/utils/docker-config.ts b/cli/source/utils/docker-config.ts new file mode 100644 index 00000000..eda0aa1a --- /dev/null +++ b/cli/source/utils/docker-config.ts @@ -0,0 +1,67 @@ +import Docker from 'dockerode'; + +const docker = new Docker(); + +// Cache container for quick status check +class ContainerCache { + private cache = new Map(); + private TTL = 30000; + + has(containerId: string): boolean { + const entry = this.cache.get(containerId); + return entry !== undefined && Date.now() - entry.timestamp < this.TTL; + } + + get(containerId: string): boolean | null { + const entry = this.cache.get(containerId); + if (!entry) return null; + + if (Date.now() - entry.timestamp < this.TTL) { + return entry.status; + } + + this.cache.delete(containerId); + return null; + } + + set(containerId: string, status: boolean): void { + this.cache.set(containerId, { + status, + timestamp: Date.now() + }); + } + + clear(): void { + this.cache.clear(); + } +} + +const containerStatusCache = new ContainerCache(); + +// Container Rebuild logic +export const shouldRebuildContainers = (): boolean => { + const skipRebuild = process.env['SKIP_CONTAINER_REBUILD'] === 'true'; + const isDev = process.env['NODE_ENV'] === 'development'; + return !(skipRebuild && isDev); +}; + +export const containerExistsAndRunning = async ( + containerId: string +): Promise => { + if (containerStatusCache.has(containerId)) { + return containerStatusCache.get(containerId)!; + } + + try { + const container = docker.getContainer(containerId); + const data = await container.inspect(); + const isRunning = data.State.Running; + containerStatusCache.set(containerId, isRunning); + return isRunning; + } catch { + containerStatusCache.set(containerId, false); + return false; + } +}; + +export const clearContainerCache = () => containerStatusCache.clear(); From 13b70ba9e466fcab2d5a7949400291b9c552480a Mon Sep 17 00:00:00 2001 From: harshit078 Date: Sat, 8 Mar 2025 04:11:45 +0530 Subject: [PATCH 2/8] Updated dev scripts --- cli/package.json | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/cli/package.json b/cli/package.json index aee51722..43cf890c 100644 --- a/cli/package.json +++ b/cli/package.json @@ -10,6 +10,9 @@ "scripts": { "build": "tsc", "dev": "tsc --watch", + "dev:hot": "nodemon --watch source --ext ts,tsx --exec \"tsc --incremental --tsBuildInfoFile ./.cache/tsbuildinfo --project tsconfig.dev.json && node dist/cli.js\"", + "dev:fast": "ts-node-dev --respawn --transpile-only --ignore-watch node_modules --cache-directory .cache --prefer-ts-exts --project tsconfig.dev.json source/cli.ts", + "dev:watch": "concurrently \"tsc --incremental --tsBuildInfoFile ./.cache/tsbuildinfo --watch --preserveWatchOutput --project tsconfig.dev.json\" \"nodemon --watch dist --delay 1 -r source-map-support/register dist/cli.js\"", "test": "yarn link-fix && ava", "lint": "eslint source/**/*.{js,ts,tsx}", "lint-fix": "eslint --fix --ignore-pattern 'node_modules/*' 'source/**/*.{js,ts,tsx}'", @@ -31,15 +34,16 @@ "react-redux": "^9.1.1" }, "devDependencies": { + "@swc/core": "^1.3.100", "@types/dockerode": "^3.3.28", "@types/dockerode-compose": "^1.4.1", "@types/ink-testing-library": "^1.0.4", "@types/ramda": "^0.29.12", "@types/react": "^18.0.32", "@typescript-eslint/eslint-plugin": "^6.10.0", - "@typescript-eslint/parser": "^6.10.0", "ava": "^5.2.0", "chalk": "^5.2.0", + "concurrently": "^8.2.2", "eslint": "^8.40.0", "eslint-plugin-import": "^2.29.0", "eslint-plugin-prettier": "^5.0.1", @@ -47,9 +51,12 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-unused-imports": "^3.1.0", "ink-testing-library": "^3.0.0", + "nodemon": "^3.1.0", "prettier": "^3.0.3", + "swc": "^1.0.11", "ts-node": "^10.9.1", - "typescript": "5.3.3" + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.3" }, "ava": { "extensions": { From 781a6c976063b1425460fc11073ca17ce68d2319 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Sun, 9 Mar 2025 14:38:45 +0530 Subject: [PATCH 3/8] Updated package.json --- cli/package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/package.json b/cli/package.json index 43cf890c..5f0fb3ea 100644 --- a/cli/package.json +++ b/cli/package.json @@ -41,6 +41,7 @@ "@types/ramda": "^0.29.12", "@types/react": "^18.0.32", "@typescript-eslint/eslint-plugin": "^6.10.0", + "@typescript-eslint/parser": "^6.10.0", "ava": "^5.2.0", "chalk": "^5.2.0", "concurrently": "^8.2.2", From 2690a299d46a54d439613ad74cad61cdb5091cd3 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Tue, 11 Mar 2025 12:38:30 +0530 Subject: [PATCH 4/8] Removed hot-reload logic from the codebase --- cli/hot-reload.sh | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100755 cli/hot-reload.sh diff --git a/cli/hot-reload.sh b/cli/hot-reload.sh deleted file mode 100755 index 4af65032..00000000 --- a/cli/hot-reload.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -export NODE_ENV=development -export SKIP_CONTAINER_REBUILD=true -export TS_NODE_TRANSPILE_ONLY=true - -PKG_MGR=$(command -v yarn >/dev/null && echo "yarn" || echo "npm") - -if [ ! -d "node_modules" ]; then - echo "Installing dependencies..." - $PKG_MGR install -fi - -mkdir -p .cache - -echo "Starting CLI with optimized hot reloading..." -$PKG_MGR run dev:hot From 249936d880a51fedbfff6518e3c67bc4f1dde88b Mon Sep 17 00:00:00 2001 From: harshit078 Date: Tue, 11 Mar 2025 13:02:47 +0530 Subject: [PATCH 5/8] Improved Rebuild container logic and added app restart logic for docker container --- cli/source/utils/docker-config.ts | 50 +++++++++++++++++++++++++++++-- 1 file changed, 48 insertions(+), 2 deletions(-) diff --git a/cli/source/utils/docker-config.ts b/cli/source/utils/docker-config.ts index eda0aa1a..9afb1615 100644 --- a/cli/source/utils/docker-config.ts +++ b/cli/source/utils/docker-config.ts @@ -39,10 +39,56 @@ class ContainerCache { const containerStatusCache = new ContainerCache(); // Container Rebuild logic -export const shouldRebuildContainers = (): boolean => { +export const shouldRebuildContainers = async ( + containerId: string = 'middleware-dev' +): Promise => { const skipRebuild = process.env['SKIP_CONTAINER_REBUILD'] === 'true'; const isDev = process.env['NODE_ENV'] === 'development'; - return !(skipRebuild && isDev); + + if (skipRebuild && isDev) { + return false; + } + + try { + // Check if container exists and is running + const isRunning = await containerExistsAndRunning(containerId); + if (isRunning) { + const container = docker.getContainer(containerId); + const data = await container.inspect(); + + // If container in healthy and dev mode, avoid rebuild + if (isDev && data.State.Health?.Status === 'healthy') { + return false; + } + + if (data.State.Health?.Status === 'unhealthy') { + return true; + } + } + + return true; + } catch (error) { + return true; + } +}; + +export const shouldOnlyRestartApp = async ( + containerId: string = 'middleware-dev' +): Promise => { + const isDev = process.env['NODE_ENV'] === 'development'; + if (!isDev) return false; + + try { + const isRunning = await containerExistsAndRunning(containerId); + if (!isRunning) return false; + + const container = docker.getContainer(containerId); + const data = await container.inspect(); + + return data.State.Health?.Status === 'healthy'; + } catch { + return false; + } }; export const containerExistsAndRunning = async ( From 77a5a6f8ff7811fbf156e395953f1a360ed460a5 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Tue, 11 Mar 2025 13:17:06 +0530 Subject: [PATCH 6/8] Added container rebuild logic,app restart logic and integrated it with docker.config logic --- cli/source/app.tsx | 107 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 105 insertions(+), 2 deletions(-) diff --git a/cli/source/app.tsx b/cli/source/app.tsx index 2234fba6..5d3cd6f6 100644 --- a/cli/source/app.tsx +++ b/cli/source/app.tsx @@ -105,6 +105,7 @@ const CliUi = () => { if (appState === AppStates.PREREQ_CHECK) { exit(); } + preCheck.resetContainerCache(); await dispatch(appSlice.actions.setAppState(AppStates.TEARDOWN)); setTimeout(() => { if (!processRef.current) return; @@ -261,6 +262,7 @@ const CliUi = () => { watch_logs.includes(rdyMsg) ) ) { + await preCheck.checkContainerStatus(); await dispatch( appSlice.actions.addLog({ type: 'default', @@ -306,13 +308,26 @@ const CliUi = () => { return () => { globalThis.process.off('exit', handleExit); }; - }, [dispatch, exit, handleExit, runCommandOpts, retryToggle, appState]); + }, [ + dispatch, + exit, + handleExit, + runCommandOpts, + retryToggle, + appState, + preCheck + ]); useEffect(() => { preCheck.callDaemonCheck(); preCheck.callPortsCheck(); preCheck.callFilesCheck(); - }, []); + }, [ + preCheck.callDaemonCheck, + preCheck.callPortsCheck, + preCheck.callFilesCheck, + preCheck + ]); const logsStreamNodes = useMemo( () => logsStream.map((l) => transformLogToNode(l)), @@ -356,6 +371,73 @@ const CliUi = () => { [preCheck] ); + useEffect(() => { + if ( + appState === AppStates.INIT && + preCheck.daemon === PreCheckStates.SUCCESS && + preCheck.ports === PreCheckStates.SUCCESS && + preCheck.dockerFile === PreCheckStates.SUCCESS && + preCheck.composeFile === PreCheckStates.SUCCESS + ) { + if (preCheck.containerStatus === PreCheckStates.SUCCESS) { + if (preCheck.appOnlyRestart) { + dispatch(appSlice.actions.setAppState(AppStates.APP_RESTART)); + } else { + dispatch(appSlice.actions.setAppState(AppStates.DOCKER_READY)); + } + } else { + const checkRebuild = async () => { + const needsRebuild = await preCheck.checkRebuildNeeded(); + if (needsRebuild) { + console.log('Containers need rebuilding'); + } + }; + checkRebuild(); + } + } + }, [appState, preCheck, dispatch]); + + useEffect(() => { + return () => { + preCheck.resetContainerCache(); + }; + }, [preCheck]); + + useEffect(() => { + if (appState === AppStates.APP_RESTART) { + const restartApp = async () => { + try { + await runCommand( + 'docker', + ['exec', '-it', 'middleware-dev', '/app/scripts/restart-app.sh'], + runCommandOpts + ).promise; + + dispatch( + appSlice.actions.addLog({ + type: 'default', + line: '🚀 Application restarted 🚀', + time: new Date() + }) + ); + + dispatch(appSlice.actions.setAppState(AppStates.DOCKER_READY)); + } catch (err) { + dispatch( + appSlice.actions.addLog({ + type: 'error', + line: `Application restart failed: ${err}`, + time: new Date() + }) + ); + dispatch(appSlice.actions.setAppState(AppStates.INIT)); + } + }; + + restartApp(); + } + }, [appState, dispatch]); + return ( <> @@ -427,6 +509,13 @@ const CliUi = () => { 'Dockerfile.dev not found in the root directory. Please ensure that the file exists with the given name.' } /> + ); case AppStates.INIT: @@ -583,6 +672,20 @@ const CliUi = () => { {terminatedText} ); + case AppStates.APP_RESTART: + return ( + + + Status: Restarting application only... [Press X to abort]{' '} + + + + + + Optimized restart - avoiding full container rebuild. + + + ); default: return ( From 5f59f764d6bac1e13cdbe5c8795f508a6edbf24a Mon Sep 17 00:00:00 2001 From: harshit078 Date: Tue, 11 Mar 2025 13:51:16 +0530 Subject: [PATCH 7/8] Added App_Restart and container status in pre-checka dn app states logic --- cli/source/constants.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/cli/source/constants.ts b/cli/source/constants.ts index 26d9ed42..5b90c6d3 100644 --- a/cli/source/constants.ts +++ b/cli/source/constants.ts @@ -16,6 +16,7 @@ export enum AppStates { PREREQ_CHECK = 'PREREQ_CHECK', INIT = 'INIT', DOCKER_READY = 'DOCKER_READY', + APP_RESTART = 'APP_RESTART', TEARDOWN = 'TEARDOWN', TERMINATED = 'TERMINATED' } @@ -30,7 +31,8 @@ export enum PreCheckProperties { DAEMON = 'daemon', PORTS = 'ports', COMPOSE_FILE = 'compose file', - DOCKER_FILE = 'docker file' + DOCKER_FILE = 'docker file', + CONTAINER_STATUS = 'container status' } export enum LogSource { From 7a42b0803698fce1a12294c808d5e60110d14be0 Mon Sep 17 00:00:00 2001 From: harshit078 Date: Tue, 11 Mar 2025 15:16:27 +0530 Subject: [PATCH 8/8] Updated and added containerstatus and container rebuild logic --- cli/source/hooks/usePreCheck.ts | 61 +++++++++++++++++++++++++++++++-- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/cli/source/hooks/usePreCheck.ts b/cli/source/hooks/usePreCheck.ts index 507d73d9..5a5a585c 100644 --- a/cli/source/hooks/usePreCheck.ts +++ b/cli/source/hooks/usePreCheck.ts @@ -4,6 +4,12 @@ import { useCallback, useState } from 'react'; import fs from 'fs'; import { PreCheckStates } from '../constants.js'; +import { + containerExistsAndRunning, + shouldRebuildContainers, + clearContainerCache, + shouldOnlyRestartApp +} from '../utils/docker-config.js'; import { runCommand } from '../utils/run-command.js'; export const usePreCheck = ({ @@ -11,13 +17,15 @@ export const usePreCheck = ({ redis, frontend, sync_server, - analytics_server + analytics_server, + containerId = 'middleware-dev' }: { db: number; redis: number; frontend: number; sync_server: number; analytics_server: number; + containerId?: string; }) => { const [daemon, setDaemon] = useState(PreCheckStates.RUNNING); const [ports, setPorts] = useState(PreCheckStates.RUNNING); @@ -27,18 +35,59 @@ export const usePreCheck = ({ const [dockerFile, setDockerFile] = useState( PreCheckStates.RUNNING ); + const [containerStatus, setContainerStatus] = useState( + PreCheckStates.RUNNING + ); + const [appOnlyRestart, setAppOnlyRestart] = useState(false); const callDaemonCheck = useCallback(() => { // For Docker daemon runCommand('docker', ['info']) .promise.then(() => { setDaemon(PreCheckStates.SUCCESS); + checkContainerStatus(); }) .catch((err) => { setDaemon(PreCheckStates.FAILED); + setContainerStatus(PreCheckStates.FAILED); }); }, []); + const checkContainerStatus = useCallback(async () => { + try { + const isRunning = await containerExistsAndRunning(containerId); + if (isRunning) { + setContainerStatus(PreCheckStates.SUCCESS); + await checkOnlyRestartApp(); + } else { + setContainerStatus(PreCheckStates.FAILED); + setAppOnlyRestart(false); + } + } catch (error) { + setContainerStatus(PreCheckStates.FAILED); + setAppOnlyRestart(false); + } + }, [checkOnlyRestartApp, containerId]); + + const checkOnlyRestartApp = useCallback(async () => { + try { + const onlyRestartApp = await shouldOnlyRestartApp(containerId); + setAppOnlyRestart(onlyRestartApp); + return onlyRestartApp; + } catch (error) { + setAppOnlyRestart(false); + return false; + } + }, [containerId]); + + const resetContainerCache = useCallback(() => { + clearContainerCache(); + }, []); + + const checkRebuildNeeded = useCallback(async () => { + return await shouldRebuildContainers(containerId); + }, [containerId]); + const callPortsCheck = useCallback(async () => { // For ports const ports_array = [db, redis, frontend, sync_server, analytics_server]; @@ -57,7 +106,7 @@ export const usePreCheck = ({ setPorts(PreCheckStates.SUCCESS); } } - }, []); + }, [db, redis, frontend, sync_server, analytics_server]); const callFilesCheck = useCallback(() => { // For files @@ -81,8 +130,14 @@ export const usePreCheck = ({ ports, composeFile, dockerFile, + containerStatus, + appOnlyRestart, callDaemonCheck, callPortsCheck, - callFilesCheck + callFilesCheck, + checkContainerStatus, + checkOnlyRestartApp, + resetContainerCache, + checkRebuildNeeded }; };