From 2064feca0663dcd7a5db1265058789912630e7f2 Mon Sep 17 00:00:00 2001 From: TheTrueShell Date: Tue, 3 Jun 2025 22:56:55 +0100 Subject: [PATCH 1/7] Add update command logic --- index.js | 247 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) diff --git a/index.js b/index.js index 671f092..562f2c3 100755 --- a/index.js +++ b/index.js @@ -16,6 +16,7 @@ const program = new Command(); const CONFIG_DIR = path.join(os.homedir(), '.synchronizer-cli'); const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); const POINTS_FILE = path.join(CONFIG_DIR, 'points.json'); +const UPDATE_CHECK_FILE = path.join(CONFIG_DIR, 'update-check.json'); // Cache file for wallet points API responses const CACHE_FILE = path.join(CONFIG_DIR, 'wallet-points-cache.json'); @@ -3892,6 +3893,252 @@ async function setupViaEnterpriseAPIAutomatic(apiKey) { } } +/** + * Check if a new version of synchronizer-cli is available on npm + * @returns {Promise<{hasUpdate: boolean, currentVersion: string, latestVersion?: string, error?: string}>} + */ +async function checkForCLIUpdate() { + try { + const currentVersion = packageJson.version; + + // Fetch latest version from npm registry + const response = await fetch('https://registry.npmjs.org/synchronizer-cli/latest'); + + if (!response.ok) { + return { + hasUpdate: false, + currentVersion, + error: `Failed to check npm registry (${response.status})` + }; + } + + const data = await response.json(); + const latestVersion = data.version; + + // Compare versions using simple string comparison for now + // This works for semantic versioning when properly formatted + const hasUpdate = compareVersions(currentVersion, latestVersion) < 0; + + return { + hasUpdate, + currentVersion, + latestVersion, + publishedAt: data.time ? data.time[latestVersion] : null + }; + } catch (error) { + return { + hasUpdate: false, + currentVersion: packageJson.version, + error: error.message + }; + } +} + +/** + * Simple version comparison function + * Returns: -1 if v1 < v2, 0 if v1 === v2, 1 if v1 > v2 + * @param {string} v1 First version + * @param {string} v2 Second version + * @returns {number} Comparison result + */ +function compareVersions(v1, v2) { + const parts1 = v1.split('.').map(Number); + const parts2 = v2.split('.').map(Number); + + const maxLength = Math.max(parts1.length, parts2.length); + + for (let i = 0; i < maxLength; i++) { + const part1 = parts1[i] || 0; + const part2 = parts2[i] || 0; + + if (part1 < part2) return -1; + if (part1 > part2) return 1; + } + + return 0; +} + +/** + * Load update check data from file + * @returns {object} Update check data with lastChecked timestamp + */ +function loadUpdateCheckData() { + if (fs.existsSync(UPDATE_CHECK_FILE)) { + try { + return JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf8')); + } catch (error) { + return { lastChecked: null }; + } + } + return { lastChecked: null }; +} + +/** + * Save update check data to file + * @param {object} data Update check data + */ +function saveUpdateCheckData(data) { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify(data, null, 2)); +} + +/** + * Check if we should perform an update check (once per day) + * @returns {boolean} True if we should check for updates + */ +function shouldCheckForUpdates() { + const updateData = loadUpdateCheckData(); + + if (!updateData.lastChecked) { + return true; + } + + const lastChecked = new Date(updateData.lastChecked); + const now = new Date(); + const oneDayAgo = new Date(now.getTime() - 24 * 60 * 60 * 1000); + + return lastChecked < oneDayAgo; +} + +/** + * Perform automatic update check and display notification if update available + * This runs silently in the background and only shows a message if an update is found + */ +async function performAutomaticUpdateCheck() { + if (!shouldCheckForUpdates()) { + return; + } + + try { + const updateInfo = await checkForCLIUpdate(); + + // Save that we checked today + saveUpdateCheckData({ + lastChecked: new Date().toISOString(), + lastResult: updateInfo + }); + + // Only show message if there's an update available + if (updateInfo.hasUpdate) { + console.log(chalk.yellow('\nšŸ”” UPDATE AVAILABLE!')); + console.log(chalk.cyan(` Current version: ${updateInfo.currentVersion}`)); + console.log(chalk.green(` Latest version: ${updateInfo.latestVersion}`)); + console.log(chalk.gray(' Run `synchronize update` to update automatically')); + console.log(chalk.gray(' Or: npm install -g synchronizer-cli@latest\n')); + } + } catch (error) { + // Silently fail automatic checks - don't bother the user + } +} + +/** + * Manual update command - check for updates and optionally install + */ +async function updateCLI() { + console.log(chalk.blue('šŸ”„ Synchronizer CLI Update Check')); + console.log(chalk.yellow('Checking for updates...\n')); + + const updateInfo = await checkForCLIUpdate(); + + if (updateInfo.error) { + console.log(chalk.red(`āŒ Error checking for updates: ${updateInfo.error}`)); + return; + } + + console.log(chalk.cyan(`šŸ“¦ Current version: ${updateInfo.currentVersion}`)); + console.log(chalk.cyan(`šŸ“¦ Latest version: ${updateInfo.latestVersion}`)); + + if (updateInfo.publishedAt) { + console.log(chalk.gray(`šŸ“… Published: ${new Date(updateInfo.publishedAt).toLocaleDateString()}`)); + } + + if (!updateInfo.hasUpdate) { + console.log(chalk.green('\nāœ… You are running the latest version!')); + + // Update the last checked timestamp + saveUpdateCheckData({ + lastChecked: new Date().toISOString(), + lastResult: updateInfo + }); + + return; + } + + console.log(chalk.yellow(`\nšŸ”„ Update available: ${updateInfo.currentVersion} → ${updateInfo.latestVersion}`)); + + // Ask if user wants to update + const shouldUpdate = await inquirer.prompt([{ + type: 'confirm', + name: 'update', + message: 'Would you like to update now?', + default: true + }]); + + if (!shouldUpdate.update) { + console.log(chalk.gray('Update cancelled. You can update later with:')); + console.log(chalk.gray(' synchronize update')); + console.log(chalk.gray(' npm install -g synchronizer-cli@latest')); + return; + } + + // Determine which package manager to use + let packageManager = 'npm'; + let installCommand = 'npm install -g synchronizer-cli@latest'; + + // Check if pnpm is available (following user's preference for pnpm) + try { + execSync('pnpm --version', { stdio: 'ignore' }); + packageManager = 'pnpm'; + installCommand = 'pnpm add -g synchronizer-cli@latest'; + console.log(chalk.cyan('šŸš€ Using pnpm for update...')); + } catch (error) { + console.log(chalk.cyan('šŸ“¦ Using npm for update...')); + } + + try { + console.log(chalk.cyan(`\nā¬‡ļø Installing synchronizer-cli@${updateInfo.latestVersion}...`)); + console.log(chalk.gray(`Running: ${installCommand}`)); + + // Run the install command + execSync(installCommand, { + stdio: 'inherit', + env: { ...process.env } + }); + + console.log(chalk.green(`\nāœ… Successfully updated to version ${updateInfo.latestVersion}!`)); + console.log(chalk.blue('šŸŽ‰ You can now use the latest features and improvements.')); + + // Update the last checked timestamp + saveUpdateCheckData({ + lastChecked: new Date().toISOString(), + lastResult: { + ...updateInfo, + hasUpdate: false, + currentVersion: updateInfo.latestVersion + } + }); + + // Show what's new if we have release notes + console.log(chalk.gray('\nšŸ’” Check the changelog at: https://github.com/multisynq/synchronizer-cli/releases')); + + } catch (error) { + console.error(chalk.red('\nāŒ Update failed:'), error.message); + console.error(chalk.yellow('\nšŸ”§ You can try updating manually:')); + console.error(chalk.gray(` ${installCommand}`)); + console.error(chalk.gray(' Or: npm uninstall -g synchronizer-cli && npm install -g synchronizer-cli')); + + // Check if it's a permission error + if (error.message.includes('EACCES') || error.message.includes('permission denied')) { + console.error(chalk.blue('\nšŸ” Permission Error Solutions:')); + console.error(chalk.gray(' • Run with sudo: sudo ' + installCommand)); + console.error(chalk.gray(' • Configure npm to use a different directory')); + console.error(chalk.gray(' • Use a Node version manager like nvm')); + } + } +} + program.name('synchronize') .description(`šŸš€ Synchronizer v${packageJson.version} - Complete CLI Toolkit for Multisynq Synchronizer From 867e608ef2785e3a730fada731431ac3b87f8446 Mon Sep 17 00:00:00 2001 From: TheTrueShell Date: Tue, 3 Jun 2025 22:57:18 +0100 Subject: [PATCH 2/7] Add wrapper function to run a automatic update check --- index.js | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/index.js b/index.js index 562f2c3..d34b716 100755 --- a/index.js +++ b/index.js @@ -4204,9 +4204,28 @@ program.name('synchronize') .version(packageJson.version) .option('--api ', 'Automatic Enterprise API setup using API key and preferences'); -program.command('init').description('Interactive configuration').action(init); -program.command('start').description('Build and run synchronizer Docker container').action(start); -program.command('service').description('Generate systemd service file for headless service').action(installService); +// Wrapper function to add automatic update checking to commands +async function withUpdateCheck(commandFunction) { + return async function(...args) { + // Perform automatic update check before running the command + await performAutomaticUpdateCheck(); + // Run the original command + return await commandFunction.apply(this, args); + }; +} + +program.command('init').description('Interactive configuration').action(async () => { + await performAutomaticUpdateCheck(); + return await init(); +}); +program.command('start').description('Build and run synchronizer Docker container').action(async () => { + await performAutomaticUpdateCheck(); + return await start(); +}); +program.command('service').description('Generate systemd service file for headless service').action(async () => { + await performAutomaticUpdateCheck(); + return await installService(); +}); program.command('service-web').description('Generate systemd service file for web dashboard').action(async () => { try { const result = await installWebServiceFile(); From 4a4b3adca9e1caf2ca9baa569d6eae05a750e167 Mon Sep 17 00:00:00 2001 From: TheTrueShell Date: Tue, 3 Jun 2025 22:57:31 +0100 Subject: [PATCH 3/7] Add automated update check to each command --- index.js | 78 ++++++++++++++++++++++++++++++++++++++++---------------- 1 file changed, 56 insertions(+), 22 deletions(-) diff --git a/index.js b/index.js index d34b716..5156814 100755 --- a/index.js +++ b/index.js @@ -4227,6 +4227,7 @@ program.command('service').description('Generate systemd service file for headle return await installService(); }); program.command('service-web').description('Generate systemd service file for web dashboard').action(async () => { + await performAutomaticUpdateCheck(); try { const result = await installWebServiceFile(); console.log(chalk.green('āœ… Web service file generated successfully!')); @@ -4242,30 +4243,58 @@ program.command('service-web').description('Generate systemd service file for we process.exit(1); } }); -program.command('status').description('Show systemd service status and recent logs').action(showStatus); -program.command('web') - .description('Start web dashboard and metrics server with optional Enterprise API setup') - .option('-p, --port ', 'Dashboard port (default: 3000)', parseInt) - .option('-m, --metrics-port ', 'Metrics port (default: 3001)', parseInt) - .option('-a, --api ', 'Enterprise API key for automatic setup') - .option('-d, --dashboard-password ', 'Set dashboard password') - .option('-w, --wallet
', 'Wallet address') - .option('-s, --synchronizer-id ', 'Synchronizer ID (for existing configs)') - .option('-n, --synchronizer-name ', 'Synchronizer name (for display/reference)') - .action(startWebGUI); -program.command('install-docker').description('Install Docker automatically (Linux only)').action(installDocker); -program.command('fix-docker').description('Fix Docker permissions (add user to docker group)').action(fixDockerPermissions); -program.command('test-platform').description('Test Docker platform compatibility').action(testPlatform); -program.command('points').description('Show wallet lifetime points and stats').action(showPoints); -program.command('set-password').description('Set or change the dashboard password').action(setDashboardPassword); +program.command('status').description('Show systemd service status and recent logs').action(async () => { + await performAutomaticUpdateCheck(); + return await showStatus(); +}); +program.command('web').description('Start web dashboard and metrics server').action(async () => { + await performAutomaticUpdateCheck(); + return await startWebGUI(); +}); +program.command('install-docker').description('Install Docker automatically (Linux only)').action(async () => { + await performAutomaticUpdateCheck(); + return await installDocker(); +}); +program.command('fix-docker').description('Fix Docker permissions (add user to docker group)').action(async () => { + await performAutomaticUpdateCheck(); + return await fixDockerPermissions(); +}); +program.command('test-platform').description('Test Docker platform compatibility').action(async () => { + await performAutomaticUpdateCheck(); + return await testPlatform(); +}); +program.command('points').description('Show wallet lifetime points and stats').action(async () => { + await performAutomaticUpdateCheck(); + return await showPoints(); +}); +program.command('set-password').description('Set or change the dashboard password').action(async () => { + await performAutomaticUpdateCheck(); + return await setDashboardPassword(); +}); program.command('validate-key [key]') .description('Validate a synq key format and check availability with API') - .action(validateSynqKey); -program.command('nightly').description('Start synchronizer with latest nightly test Docker image').action(startNightly); -program.command('test-nightly').description('Test nightly launch with direct Docker command').action(testNightly); -program.command('check-updates').description('Check for Docker image updates manually').action(checkImageUpdates); -program.command('monitor').description('Start background monitoring for Docker image updates').action(startImageMonitoring); + .action(async (key) => { + await performAutomaticUpdateCheck(); + return await validateSynqKey(key); + }); +program.command('nightly').description('Start synchronizer with latest nightly test Docker image').action(async () => { + await performAutomaticUpdateCheck(); + return await startNightly(); +}); +program.command('test-nightly').description('Test nightly launch with direct Docker command').action(async () => { + await performAutomaticUpdateCheck(); + return await testNightly(); +}); +program.command('check-updates').description('Check for Docker image updates manually').action(async () => { + await performAutomaticUpdateCheck(); + return await checkImageUpdates(); +}); +program.command('monitor').description('Start background monitoring for Docker image updates').action(async () => { + await performAutomaticUpdateCheck(); + return await startImageMonitoring(); +}); program.command('monitor-service').description('Generate systemd service file for image monitoring').action(async () => { + await performAutomaticUpdateCheck(); try { const result = await installImageMonitoringService(); console.log(chalk.green('āœ… Image monitoring service file generated successfully!')); @@ -4279,8 +4308,13 @@ program.command('monitor-service').description('Generate systemd service file fo process.exit(1); } }); -program.command('api').description('Set up synchronizer via Enterprise API').action(setupViaEnterpriseAPI); +program.command('update').description('Check for CLI updates and install automatically').action(updateCLI); +program.command('api').description('Set up synchronizer via Enterprise API').action(async () => { + await performAutomaticUpdateCheck(); + return await setupViaEnterpriseAPI(); +}); program.command('api-auto').description('Automatic Enterprise API setup using API preferences').action(async () => { + await performAutomaticUpdateCheck(); try { const apiKey = await inquirer.prompt([{ type: 'password', From b5f8207d916deaeff48cb4fed0d3621ccc15155e Mon Sep 17 00:00:00 2001 From: TheTrueShell Date: Tue, 3 Jun 2025 23:11:31 +0100 Subject: [PATCH 4/7] Update HTML --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 5156814..dd24fd1 100755 --- a/index.js +++ b/index.js @@ -1701,7 +1701,7 @@ function generateDashboardHTML(config, metricsPort, authenticated, primaryIP) {
GET /api/check-updates - Check for Docker image updates + Check for Docker image updates (CLI: 'synchronize update-container')
POST From b94d82423b471ed621d7413e936130caf5e7ba96 Mon Sep 17 00:00:00 2001 From: TheTrueShell Date: Tue, 3 Jun 2025 23:11:48 +0100 Subject: [PATCH 5/7] Update update command reference --- index.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/index.js b/index.js index dd24fd1..4331aa5 100755 --- a/index.js +++ b/index.js @@ -4285,7 +4285,7 @@ program.command('test-nightly').description('Test nightly launch with direct Doc await performAutomaticUpdateCheck(); return await testNightly(); }); -program.command('check-updates').description('Check for Docker image updates manually').action(async () => { +program.command('update-container').description('Check for Docker image updates manually').action(async () => { await performAutomaticUpdateCheck(); return await checkImageUpdates(); }); @@ -4308,7 +4308,11 @@ program.command('monitor-service').description('Generate systemd service file fo process.exit(1); } }); -program.command('update').description('Check for CLI updates and install automatically').action(updateCLI); +program.command('update-cli').description('Check for CLI updates and install automatically').action(updateCLI); +program.command('update').description('Check for all updates (CLI and Docker containers)').action(async () => { + await performAutomaticUpdateCheck(); + return await checkAllUpdates(); +}); program.command('api').description('Set up synchronizer via Enterprise API').action(async () => { await performAutomaticUpdateCheck(); return await setupViaEnterpriseAPI(); From ee75ca16708c42a614d556809473b69fae6838f9 Mon Sep 17 00:00:00 2001 From: TheTrueShell Date: Tue, 3 Jun 2025 23:11:56 +0100 Subject: [PATCH 6/7] Add unified update function --- index.js | 131 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/index.js b/index.js index 4331aa5..44b5ec2 100755 --- a/index.js +++ b/index.js @@ -4466,3 +4466,134 @@ async function deployAll(options) { process.exit(1); } } + +/** + * Check for both CLI and Docker container updates in a unified command + */ +async function checkAllUpdates() { + console.log(chalk.blue('šŸ”„ Checking All Updates')); + console.log(chalk.yellow('Checking both CLI and Docker container updates...\n')); + + let hasAnyUpdates = false; + + // 1. Check CLI updates first + console.log(chalk.cyan('šŸ“¦ Checking CLI Updates...')); + console.log(chalk.gray('─'.repeat(50))); + + const cliUpdateInfo = await checkForCLIUpdate(); + + if (cliUpdateInfo.error) { + console.log(chalk.red(`āŒ Error checking CLI updates: ${cliUpdateInfo.error}`)); + } else { + console.log(chalk.blue(`Current CLI version: ${cliUpdateInfo.currentVersion}`)); + console.log(chalk.blue(`Latest CLI version: ${cliUpdateInfo.latestVersion}`)); + + if (cliUpdateInfo.hasUpdate) { + hasAnyUpdates = true; + console.log(chalk.yellow(`šŸ”„ CLI update available: ${cliUpdateInfo.currentVersion} → ${cliUpdateInfo.latestVersion}`)); + + const shouldUpdateCLI = await inquirer.prompt([{ + type: 'confirm', + name: 'update', + message: 'Update CLI now?', + default: true + }]); + + if (shouldUpdateCLI.update) { + // Determine package manager + let packageManager = 'npm'; + let installCommand = 'npm install -g synchronizer-cli@latest'; + + try { + execSync('pnpm --version', { stdio: 'ignore' }); + packageManager = 'pnpm'; + installCommand = 'pnpm add -g synchronizer-cli@latest'; + } catch (error) { + // Use npm + } + + try { + console.log(chalk.cyan(`ā¬‡ļø Updating CLI with ${packageManager}...`)); + execSync(installCommand, { stdio: 'inherit' }); + console.log(chalk.green('āœ… CLI updated successfully!')); + } catch (error) { + console.log(chalk.red(`āŒ CLI update failed: ${error.message}`)); + } + } + } else { + console.log(chalk.green('āœ… CLI is up to date')); + } + } + + console.log(''); // Spacing + + // 2. Check Docker container updates + console.log(chalk.cyan('🐳 Checking Docker Container Updates...')); + console.log(chalk.gray('─'.repeat(50))); + + const images = [ + { name: 'cdrakep/synqchronizer:latest', description: 'Main synchronizer image' }, + { name: 'cdrakep/synqchronizer-test-fixed:latest', description: 'Fixed nightly test image' } + ]; + + let containerUpdatesAvailable = 0; + + for (const image of images) { + console.log(chalk.gray(`Checking ${image.description}...`)); + + try { + const hasUpdate = await isNewDockerImageAvailable(image.name); + + if (hasUpdate) { + hasAnyUpdates = true; + containerUpdatesAvailable++; + console.log(chalk.yellow(`šŸ”„ Update available: ${image.name}`)); + + const shouldPull = await inquirer.prompt([{ + type: 'confirm', + name: 'pull', + message: `Pull latest version of ${image.name}?`, + default: true + }]); + + if (shouldPull.pull) { + try { + console.log(chalk.cyan(`ā¬‡ļø Pulling ${image.name}...`)); + execSync(`docker pull ${image.name}`, { stdio: 'inherit' }); + console.log(chalk.green(`āœ… Successfully updated ${image.name}`)); + } catch (error) { + console.log(chalk.red(`āŒ Failed to pull ${image.name}: ${error.message}`)); + } + } + } else { + console.log(chalk.green(`āœ… ${image.name} is up to date`)); + } + } catch (error) { + console.log(chalk.red(`āŒ Error checking ${image.name}: ${error.message}`)); + } + } + + // 3. Summary + console.log(''); // Spacing + console.log(chalk.blue('šŸ“Š Update Summary:')); + console.log(chalk.gray('─'.repeat(30))); + + if (!hasAnyUpdates) { + console.log(chalk.green('āœ… Everything is up to date!')); + console.log(chalk.gray(' • CLI: Latest version')); + console.log(chalk.gray(' • Containers: All images current')); + } else { + console.log(chalk.yellow('šŸ”„ Updates were available')); + if (cliUpdateInfo.hasUpdate) { + console.log(chalk.gray(' • CLI: Update available')); + } + if (containerUpdatesAvailable > 0) { + console.log(chalk.gray(` • Containers: ${containerUpdatesAvailable} image(s) had updates`)); + } + } + + console.log(''); + console.log(chalk.gray('šŸ’” Individual update commands:')); + console.log(chalk.gray(' synchronize update-cli # CLI updates only')); + console.log(chalk.gray(' synchronize update-container # Container updates only')); +} From 8d4d25936fb44244d73b3390f220172e51d1582b Mon Sep 17 00:00:00 2001 From: TheTrueShell Date: Tue, 3 Jun 2025 23:12:04 +0100 Subject: [PATCH 7/7] Update documentation --- index.js | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/index.js b/index.js index 44b5ec2..e8c5967 100755 --- a/index.js +++ b/index.js @@ -3420,7 +3420,7 @@ async function startImageMonitoring() { } else { console.log(chalk.yellow(`šŸ”„ Found ${updatesFound} image(s) with updates`)); if (!monitoringConfig.autoUpdate) { - console.log(chalk.gray(' Run `synchronize check-updates` to update manually')); + console.log(chalk.gray(' Run `synchronize update-container` to update manually')); } } @@ -4174,9 +4174,10 @@ program.name('synchronize') • Automated configuration with API-generated keys • Hands-free setup using API preferences (--api option) -šŸ”„ DOCKER IMAGE MONITORING: +šŸ”„ UPDATE MANAGEMENT: + • Unified update checking for CLI and Docker containers • Automatic update checking every 30-60 minutes - • Manual update checking with interactive pulls + • Manual update checking with interactive installation • Background monitoring service with systemd integration • Version tracking with CLI version / Docker version format @@ -4186,14 +4187,16 @@ program.name('synchronize') • Enhanced logging with versioned container information šŸ’” QUICK START: - synchronize init # Interactive configuration (manual) - synchronize api # Enterprise API setup (interactive) - synchronize --api # Enterprise API setup (automatic) - synchronize start # Start synchronizer container - synchronize nightly # Run fixed nightly test version - synchronize dashboard # Launch web dashboard - synchronize check-updates # Check for Docker image updates - synchronize web # Launch web dashboard (auto ports + synchronize init # Interactive configuration (manual) + synchronize api # Enterprise API setup (interactive) + synchronize --api # Enterprise API setup (automatic) + synchronize start # Start synchronizer container + synchronize nightly # Run fixed nightly test version + synchronize dashboard # Launch web dashboard + synchronize update # Check for all updates (CLI + containers) + synchronize update-cli # Check for CLI updates only + synchronize update-container # Check for container updates only + synchronize web # Launch web dashboard (auto ports) synchronize web --port 8080 # Launch with custom dashboard port synchronize web -p 8080 -m 8081 # Custom dashboard and metrics ports