🗑️ Remove unused and archived files across multiple directories and update project dependencies in package files

This commit is contained in:
2026-01-28 02:54:35 +00:00
parent 79d52f5d92
commit 1beba26249
170 changed files with 632200 additions and 209 deletions

View File

@@ -0,0 +1,396 @@
/**
* Pokedex.Online Deployment Script
*
* Deploys the pokedex.online Docker container to Synology NAS via SSH.
* - Connects to Synology using configured SSH hosts
* - Transfers files via SFTP
* - Manages Docker deployment with rollback on failure
* - Performs health check to verify deployment
*
* Usage:
* node code/utils/deploy-pokedex.js [--target internal|external] [--port 8080] [--ssl-port 8443]
* npm run deploy:pokedex -- --target external --port 8081 --ssl-port 8444
*
* Examples:
* npm run deploy:pokedex # Deploy to internal (10.0.0.81) on port 8080
* npm run deploy:pokedex -- --target external # Deploy to external (home.gregrjacobs.com)
* npm run deploy:pokedex -- --port 8081 # Deploy to internal on port 8081
* npm run deploy:pokedex -- --port 8080 --ssl-port 8443 # Deploy with HTTPS on port 8443
* npm run deploy:pokedex -- --target external --port 3000 --ssl-port 3443
*/
import { NodeSSH } from 'node-ssh';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
import http from 'http';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Configuration
const SSH_HOSTS = {
internal: {
host: '10.0.0.81',
port: 2323,
username: 'GregRJacobs',
privateKeyPath: '~/.ssh/ds3627xs_gregrjacobs',
password: 'J@Cubs88'
},
external: {
host: 'home.gregrjacobs.com',
port: 2323,
username: 'GregRJacobs',
privateKeyPath: '~/.ssh/ds3627xs_gregrjacobs',
password: 'J@Cubs88'
}
};
const REMOTE_PATH = '/volume1/docker/pokedex-online/base';
const CONTAINER_NAME = 'pokedex-online';
const SOURCE_DIR = path.resolve(__dirname, '../websites/pokedex.online/apps');
/**
* Parse command line arguments
* @returns {Object} Parsed arguments
*/
function parseArgs() {
const args = process.argv.slice(2);
const config = {
target: 'internal',
port: 8080,
sslPort: null
};
for (let i = 0; i < args.length; i++) {
if (args[i] === '--target' && args[i + 1]) {
config.target = args[i + 1];
i++;
} else if (args[i] === '--port' && args[i + 1]) {
config.port = parseInt(args[i + 1], 10);
i++;
} else if (args[i] === '--ssl-port' && args[i + 1]) {
config.sslPort = parseInt(args[i + 1], 10);
i++;
}
}
// Validate target
if (!SSH_HOSTS[config.target]) {
throw new Error(
`Invalid target: ${config.target}. Must be 'internal' or 'external'.`
);
}
// Validate port
if (isNaN(config.port) || config.port < 1 || config.port > 65535) {
throw new Error(
`Invalid port: ${config.port}. Must be between 1 and 65535.`
);
}
// Validate SSL port if provided
if (
config.sslPort !== null &&
(isNaN(config.sslPort) || config.sslPort < 1 || config.sslPort > 65535)
) {
throw new Error(
`Invalid SSL port: ${config.sslPort}. Must be between 1 and 65535.`
);
}
return config;
}
/**
* Expand tilde in file paths
* @param {string} filepath - Path potentially starting with ~
* @returns {string} Expanded path
*/
function expandTilde(filepath) {
if (filepath.startsWith('~/')) {
return path.join(process.env.HOME, filepath.slice(2));
}
return filepath;
}
/**
* Create modified docker-compose.yml with custom ports
* @param {number} port - HTTP port to map to container
* @param {number|null} sslPort - HTTPS port to map to container (optional)
* @returns {string} Modified docker-compose content
*/
function createModifiedDockerCompose(port, sslPort) {
const originalPath = path.join(SOURCE_DIR, 'docker-compose.yml');
let content = fs.readFileSync(originalPath, 'utf8');
// Replace HTTP port mapping (handle both single and double quotes)
content = content.replace(/- ['"](\d+):80['"]/, `- '${port}:80'`);
// Replace HTTPS port mapping if SSL port provided
if (sslPort !== null) {
content = content.replace(/- ['"](\d+):443['"]/, `- '${sslPort}:443'`);
} else {
// Remove HTTPS port mapping if no SSL port specified
content = content.replace(/\s*- ['"](\d+):443['"]/g, '');
}
return content;
}
/**
* Perform HTTP health check
* @param {string} host - Host to check
* @param {number} port - Port to check
* @param {number} retries - Number of retries
* @returns {Promise<boolean>} True if healthy
*/
async function healthCheck(host, port, retries = 5) {
for (let i = 0; i < retries; i++) {
try {
await new Promise((resolve, reject) => {
const req = http.get(
`http://${host}:${port}`,
{ timeout: 5000 },
res => {
if (res.statusCode === 200) {
resolve();
} else {
reject(new Error(`HTTP ${res.statusCode}`));
}
}
);
req.on('error', reject);
req.on('timeout', () => {
req.destroy();
reject(new Error('Request timeout'));
});
});
return true;
} catch (error) {
if (i < retries - 1) {
console.log(
`⏳ Health check attempt ${i + 1}/${retries} failed, retrying in 3s...`
);
await new Promise(resolve => setTimeout(resolve, 3000));
}
}
}
return false;
}
/**
* Main deployment function
*/
async function deploy() {
const ssh = new NodeSSH();
let previousImage = null;
let containerExisted = false;
try {
// Parse arguments
const config = parseArgs();
const sshConfig = SSH_HOSTS[config.target];
console.log('🚀 Starting Pokedex.Online deployment');
console.log(
`📡 Target: ${config.target} (${sshConfig.host}:${sshConfig.port})`
);
console.log(`🔌 HTTP Port: ${config.port}`);
if (config.sslPort) {
console.log(`🔒 HTTPS Port: ${config.sslPort}`);
}
// Connect to Synology
console.log('\n🔐 Connecting to Synology...');
await ssh.connect({
host: sshConfig.host,
port: sshConfig.port,
username: sshConfig.username,
privateKeyPath: expandTilde(sshConfig.privateKeyPath),
password: sshConfig.password,
tryKeyboard: true
});
console.log('✅ Connected successfully');
// Check if container exists and capture current image
console.log('\n📦 Checking for existing container...');
console.log(` Container name: ${CONTAINER_NAME}`);
try {
const result = await ssh.execCommand(
`/usr/local/bin/docker inspect --format='{{.Image}}' ${CONTAINER_NAME} || /usr/bin/docker inspect --format='{{.Image}}' ${CONTAINER_NAME}`
);
console.log(` Command exit code: ${result.code}`);
if (result.stdout) console.log(` Stdout: ${result.stdout.trim()}`);
if (result.stderr) console.log(` Stderr: ${result.stderr.trim()}`);
if (result.code === 0 && result.stdout.trim()) {
previousImage = result.stdout.trim();
containerExisted = true;
console.log(
`✅ Found existing container (image: ${previousImage.substring(0, 12)}...)`
);
} else {
console.log(' No existing container found');
}
} catch (error) {
console.log(` Error: ${error.message}`);
console.log(' No existing container found');
}
// Create remote directory
console.log('\n📁 Creating remote directory...');
const mkdirResult = await ssh.execCommand(`mkdir -p ${REMOTE_PATH}`);
console.log(` Command: mkdir -p ${REMOTE_PATH}`);
if (mkdirResult.stdout) console.log(` Output: ${mkdirResult.stdout}`);
if (mkdirResult.stderr) console.log(` Stderr: ${mkdirResult.stderr}`);
console.log(` ✅ Directory ready`);
// Create modified docker-compose.yml
const modifiedDockerCompose = createModifiedDockerCompose(
config.port,
config.sslPort
);
const tempDockerComposePath = path.join(
SOURCE_DIR,
'docker-compose.tmp.yml'
);
fs.writeFileSync(tempDockerComposePath, modifiedDockerCompose);
// Transfer files
console.log('\n📤 Transferring files...');
const filesToTransfer = [
{
local: path.join(SOURCE_DIR, 'index.html'),
remote: `${REMOTE_PATH}/index.html`
},
{
local: path.join(SOURCE_DIR, 'Dockerfile'),
remote: `${REMOTE_PATH}/Dockerfile`
},
{
local: tempDockerComposePath,
remote: `${REMOTE_PATH}/docker-compose.yml`
}
];
for (const file of filesToTransfer) {
try {
await ssh.putFile(file.local, file.remote);
console.log(`${path.basename(file.local)}`);
} catch (error) {
// If SFTP fails, fall back to cat method
console.log(
` ⚠️ SFTP failed for ${path.basename(file.local)}, using cat fallback...`
);
const fileContent = fs.readFileSync(file.local, 'utf8');
const escapedContent = fileContent.replace(/'/g, "'\\''");
const catResult = await ssh.execCommand(
`cat > '${file.remote}' << 'EOFMARKER'\n${fileContent}\nEOFMARKER`
);
if (catResult.stdout) console.log(` Output: ${catResult.stdout}`);
if (catResult.stderr) console.log(` Stderr: ${catResult.stderr}`);
if (catResult.code !== 0) {
throw new Error(
`Failed to transfer ${path.basename(file.local)}: ${catResult.stderr}`
);
}
console.log(
`${path.basename(file.local)} (${fs.statSync(file.local).size} bytes)`
);
}
}
// Clean up temp file
fs.unlinkSync(tempDockerComposePath);
// Stop existing container first to avoid port conflicts
if (containerExisted) {
console.log('\n🛑 Stopping existing container...');
const stopResult = await ssh.execCommand(
`cd ${REMOTE_PATH} && /usr/local/bin/docker compose down || /usr/local/bin/docker-compose down`
);
if (stopResult.stdout) console.log(` ${stopResult.stdout.trim()}`);
console.log(' ✅ Container stopped');
}
// Deploy with docker-compose
console.log('\n🐳 Building and starting Docker container...');
console.log(` Working directory: ${REMOTE_PATH}`);
// Try Docker Compose V2 first (docker compose), then fall back to V1 (docker-compose)
// Use full paths for Synology
console.log(' Attempting: /usr/local/bin/docker compose up -d --build');
let deployResult = await ssh.execCommand(
`cd ${REMOTE_PATH} && /usr/local/bin/docker compose up -d --build || /usr/local/bin/docker-compose up -d --build || /usr/bin/docker compose up -d --build`,
{ stream: 'both' }
);
console.log('\n 📋 Docker Output:');
if (deployResult.stdout) {
deployResult.stdout.split('\n').forEach(line => {
if (line.trim()) console.log(` ${line}`);
});
}
if (deployResult.stderr) {
console.log('\n ⚠️ Docker Stderr:');
deployResult.stderr.split('\n').forEach(line => {
if (line.trim()) console.log(` ${line}`);
});
}
console.log(` Exit code: ${deployResult.code}`);
if (deployResult.code !== 0) {
throw new Error(`Docker deployment failed: ${deployResult.stderr}`);
}
console.log('\n✅ Container started');
// Health check
console.log('\n🏥 Performing health check...');
const isHealthy = await healthCheck(sshConfig.host, config.port);
if (!isHealthy) {
throw new Error('Health check failed - container is not responding');
}
console.log('✅ Health check passed');
console.log(`\n🎉 Deployment successful!`);
console.log(`🌐 HTTP: http://${sshConfig.host}:${config.port}`);
if (config.sslPort) {
console.log(`🔒 HTTPS: https://${sshConfig.host}:${config.sslPort}`);
}
ssh.dispose();
} catch (error) {
console.error('\n❌ Deployment failed:', error.message);
// Rollback
if (previousImage) {
console.log('\n🔄 Rolling back to previous image...');
try {
await ssh.execCommand(
`cd ${REMOTE_PATH} && docker-compose down && docker tag ${previousImage} pokedex-online:latest && docker-compose up -d`
);
console.log('✅ Rollback successful');
} catch (rollbackError) {
console.error('❌ Rollback failed:', rollbackError.message);
}
} else if (containerExisted === false) {
console.log('\n🧹 Cleaning up failed deployment...');
try {
await ssh.execCommand(
`cd ${REMOTE_PATH} && docker-compose down --volumes --remove-orphans`
);
console.log('✅ Cleanup successful');
} catch (cleanupError) {
console.error('❌ Cleanup failed:', cleanupError.message);
}
}
ssh.dispose();
process.exit(1);
}
}
// Run deployment
deploy();