Compare commits

...

3 Commits

Author SHA1 Message Date
c2f2f1e2ee feat(update): add version check to skip update when already latest
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s
Release / build-and-release (push) Successful in 48s
CI / Build All Platforms (push) Successful in 53s
Now `nupst update` checks current version against latest release before
downloading anything.

Behavior:
- Fetches latest version from Gitea API
- Compares with current version
- Shows "Already up to date!" if versions match
- Only downloads/installs if newer version available

Example output when up to date:
  Checking for updates...
  Current version: v4.0.8
  Latest version:  v4.0.8

  ✓ Already up to date!
2025-10-19 22:50:03 +00:00
936f86c346 fix(update): rewrite nupst update for v4 (download install script instead of git pull)
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 6s
Release / build-and-release (push) Successful in 45s
CI / Build All Platforms (push) Successful in 50s
The update command was still using v3 logic (git pull, setup.sh) which
doesn't work for v4 binary distribution.

Now it simply:
1. Downloads install.sh from main branch
2. Runs it (handles download, stop, replace, restart automatically)

This is much simpler and matches how v4 is distributed. No more git,
no more setup.sh, just download the latest binary.
2025-10-19 21:54:05 +00:00
7ff1a7da36 feat(cli): replace ugly ASCII boxes with beautiful colored status output
All checks were successful
CI / Type Check & Lint (push) Successful in 6s
CI / Build Test (Current Platform) (push) Successful in 5s
Release / build-and-release (push) Successful in 48s
CI / Build All Platforms (push) Successful in 52s
Replaced all ASCII box characters (┌─┐│└┘) with modern, clean colored
output using the existing color theme and symbols.

Changes in ts/systemd.ts:
- displayServiceStatus(): Parse systemctl output and show key info
  with colored symbols (● for running, ○ for stopped)
- displaySingleUpsStatus(): Clean output with battery/runtime colors
  - Green >60%, yellow 30-60%, red <30% for battery
  - Power status with colored symbols and text
  - Clean indented layout without boxes

Example new output:
  ● Service: active (running)
    PID: 7606  Memory: 41.5M  CPU: 279ms

  UPS Devices (2):
    ● Test UPS (SNMP v1) - Online
      Battery: 100% ✓  Runtime: 48 min
      Host: 192.168.187.140:161

Much cleaner and more readable than ASCII boxes!
2025-10-19 21:50:31 +00:00
3 changed files with 129 additions and 129 deletions

View File

@@ -1,6 +1,6 @@
{
"name": "@serve.zone/nupst",
"version": "4.0.5",
"version": "4.0.8",
"exports": "./mod.ts",
"tasks": {
"dev": "deno run --allow-all mod.ts",

View File

@@ -129,81 +129,53 @@ export class ServiceHandler {
try {
// Check if running as root
this.checkRootAccess(
'This command must be run as root to update NUPST and refresh the systemd service.',
'This command must be run as root to update NUPST.',
);
const boxWidth = 45;
logger.logBoxTitle('NUPST Update Process', boxWidth);
logger.logBoxLine('Updating NUPST from repository...');
// Determine the installation directory (assuming it's either /opt/nupst or the current directory)
const { existsSync } = await import('fs');
let installDir = '/opt/nupst';
if (!existsSync(installDir)) {
// If not installed in /opt/nupst, use the current directory
const { dirname } = await import('path');
installDir = dirname(dirname(process.argv[1])); // Go up two levels from the executable
logger.logBoxLine(`Using local installation directory: ${installDir}`);
}
console.log('');
logger.info('Checking for updates...');
try {
// 1. Update the repository
logger.logBoxLine('Pulling latest changes from git repository...');
execSync(`cd ${installDir} && git fetch origin && git reset --hard origin/main`, {
stdio: 'pipe',
// Get current version
const currentVersion = this.nupst.getVersion();
// Fetch latest version from Gitea API
const apiUrl = 'https://code.foss.global/api/v1/repos/serve.zone/nupst/releases/latest';
const response = execSync(`curl -sSL ${apiUrl}`).toString();
const release = JSON.parse(response);
const latestVersion = release.tag_name; // e.g., "v4.0.7"
logger.dim(`Current version: ${currentVersion}`);
logger.dim(`Latest version: ${latestVersion}`);
console.log('');
// Compare versions (both are in format "v4.0.7")
if (currentVersion === latestVersion) {
logger.success('Already up to date!');
console.log('');
return;
}
logger.info(`New version available: ${latestVersion}`);
logger.dim('Downloading and installing...');
console.log('');
// Download and run the install script
// This handles everything: download binary, stop service, replace, restart
const installUrl = 'https://code.foss.global/serve.zone/nupst/raw/branch/main/install.sh';
execSync(`curl -sSL ${installUrl} | bash`, {
stdio: 'inherit', // Show install script output to user
});
// 2. Run the install.sh script
logger.logBoxLine('Running install.sh to update NUPST...');
execSync(`cd ${installDir} && bash ./install.sh`, { stdio: 'pipe' });
// 3. Run the setup.sh script with force flag to update Node.js and dependencies
logger.logBoxLine('Running setup.sh to update Node.js and dependencies...');
execSync(`cd ${installDir} && bash ./setup.sh --force`, { stdio: 'pipe' });
// 4. Refresh the systemd service
logger.logBoxLine('Refreshing systemd service...');
// First check if service exists
let serviceExists = false;
try {
const output = execSync('systemctl list-unit-files | grep nupst.service').toString();
serviceExists = output.includes('nupst.service');
console.log('');
logger.success(`Updated to ${latestVersion}`);
console.log('');
} catch (error) {
// If grep fails (service not found), serviceExists remains false
serviceExists = false;
}
if (serviceExists) {
// Stop the service if it's running
const isRunning =
execSync('systemctl is-active nupst.service || true').toString().trim() === 'active';
if (isRunning) {
logger.logBoxLine('Stopping nupst service...');
execSync('systemctl stop nupst.service');
}
// Reinstall the service
logger.logBoxLine('Reinstalling systemd service...');
await this.nupst.getSystemd().install();
// Restart the service if it was running
if (isRunning) {
logger.logBoxLine('Restarting nupst service...');
execSync('systemctl start nupst.service');
}
} else {
logger.logBoxLine('Systemd service not installed, skipping service refresh.');
logger.logBoxLine('Run "nupst enable" to install the service.');
}
logger.logBoxLine('Update completed successfully!');
logger.logBoxEnd();
} catch (error) {
logger.logBoxLine('Error during update process:');
logger.logBoxLine(`${error instanceof Error ? error.message : String(error)}`);
logger.logBoxEnd();
console.log('');
logger.error('Update failed');
logger.dim(`${error instanceof Error ? error.message : String(error)}`);
console.log('');
process.exit(1);
}
} catch (error) {

View File

@@ -3,6 +3,7 @@ import { promises as fs } from 'node:fs';
import { execSync } from 'node:child_process';
import { NupstDaemon } from './daemon.ts';
import { logger } from './logger.ts';
import { theme, symbols, getBatteryColor, getRuntimeColor, formatPowerStatus } from './colors.ts';
/**
* Class for managing systemd service
@@ -171,18 +172,50 @@ WantedBy=multi-user.target
private displayServiceStatus(): void {
try {
const serviceStatus = execSync('systemctl status nupst.service').toString();
const boxWidth = 45;
logger.logBoxTitle('Service Status', boxWidth);
// Process each line of the status output
serviceStatus.split('\n').forEach((line) => {
logger.logBoxLine(line);
});
logger.logBoxEnd();
const lines = serviceStatus.split('\n');
// Parse key information from systemctl output
let isActive = false;
let pid = '';
let memory = '';
let cpu = '';
for (const line of lines) {
if (line.includes('Active:')) {
isActive = line.includes('active (running)');
} else if (line.includes('Main PID:')) {
const match = line.match(/Main PID:\s+(\d+)/);
if (match) pid = match[1];
} else if (line.includes('Memory:')) {
const match = line.match(/Memory:\s+([\d.]+[A-Z])/);
if (match) memory = match[1];
} else if (line.includes('CPU:')) {
const match = line.match(/CPU:\s+([\d.]+(?:ms|s))/);
if (match) cpu = match[1];
}
}
// Display beautiful status
console.log('');
if (isActive) {
console.log(`${symbols.running} ${theme.success('Service:')} ${theme.statusActive('active (running)')}`);
} else {
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('inactive')}`);
}
if (pid || memory || cpu) {
const details = [];
if (pid) details.push(`PID: ${theme.dim(pid)}`);
if (memory) details.push(`Memory: ${theme.dim(memory)}`);
if (cpu) details.push(`CPU: ${theme.dim(cpu)}`);
console.log(` ${details.join(' ')}`);
}
console.log('');
} catch (error) {
const boxWidth = 45;
logger.logBoxTitle('Service Status', boxWidth);
logger.logBoxLine('Service is not running');
logger.logBoxEnd();
console.log('');
console.log(`${symbols.stopped} ${theme.dim('Service:')} ${theme.statusInactive('not installed')}`);
console.log('');
}
}
@@ -199,7 +232,7 @@ WantedBy=multi-user.target
// Check if we have the new multi-UPS config format
if (config.upsDevices && Array.isArray(config.upsDevices) && config.upsDevices.length > 0) {
logger.log(`Found ${config.upsDevices.length} UPS device(s) in configuration`);
console.log(theme.info(`UPS Devices (${config.upsDevices.length}):`));
// Show status for each UPS
for (const ups of config.upsDevices) {
@@ -207,6 +240,7 @@ WantedBy=multi-user.target
}
} else if (config.snmp) {
// Legacy single UPS configuration
console.log(theme.info('UPS Devices (1):'));
const legacyUps = {
id: 'default',
name: 'Default UPS',
@@ -217,15 +251,16 @@ WantedBy=multi-user.target
await this.displaySingleUpsStatus(legacyUps, snmp);
} else {
logger.error('No UPS devices found in configuration');
console.log('');
console.log(`${symbols.warning} ${theme.warning('No UPS devices configured')}`);
console.log(` ${theme.dim('Run')} ${theme.command('nupst ups add')} ${theme.dim('to add a device')}`);
console.log('');
}
} catch (error) {
const boxWidth = 45;
logger.logBoxTitle('UPS Status', boxWidth);
logger.logBoxLine(
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
);
logger.logBoxEnd();
console.log('');
console.log(`${symbols.error} ${theme.error('Failed to retrieve UPS status')}`);
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
console.log('');
}
}
@@ -235,24 +270,6 @@ WantedBy=multi-user.target
* @param snmp SNMP manager
*/
private async displaySingleUpsStatus(ups: any, snmp: any): Promise<void> {
const boxWidth = 45;
logger.logBoxTitle(`Connecting to UPS: ${ups.name}`, boxWidth);
logger.logBoxLine(`ID: ${ups.id}`);
logger.logBoxLine(`Host: ${ups.snmp.host}:${ups.snmp.port}`);
logger.logBoxLine(`UPS Model: ${ups.snmp.upsModel || 'cyberpower'}`);
if (ups.groups && ups.groups.length > 0) {
// Get group names if available
const config = this.daemon.getConfig();
const groupNames = ups.groups.map((groupId: string) => {
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
return group ? group.name : groupId;
});
logger.logBoxLine(`Groups: ${groupNames.join(', ')}`);
}
logger.logBoxEnd();
try {
// Create a test config with a short timeout
const testConfig = {
@@ -262,32 +279,43 @@ WantedBy=multi-user.target
const status = await snmp.getUpsStatus(testConfig);
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
logger.logBoxLine(`Power Status: ${status.powerStatus}`);
logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`);
logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`);
// Determine status symbol based on power status
let statusSymbol = symbols.unknown;
if (status.powerStatus === 'online') {
statusSymbol = symbols.running;
} else if (status.powerStatus === 'onBattery') {
statusSymbol = symbols.warning;
}
// Show threshold status
logger.logBoxLine('');
logger.logBoxLine('Thresholds:');
logger.logBoxLine(
` Battery: ${status.batteryCapacity}% / ${ups.thresholds.battery}% ${
status.batteryCapacity < ups.thresholds.battery ? '⚠️' : '✓'
}`,
);
logger.logBoxLine(
` Runtime: ${status.batteryRuntime} min / ${ups.thresholds.runtime} min ${
status.batteryRuntime < ups.thresholds.runtime ? '⚠️' : '✓'
}`,
);
// Display UPS name and power status
console.log(` ${statusSymbol} ${theme.highlight(ups.name)} - ${formatPowerStatus(status.powerStatus)}`);
// Display battery with color coding
const batteryColor = getBatteryColor(status.batteryCapacity);
const batterySymbol = status.batteryCapacity >= ups.thresholds.battery ? symbols.success : symbols.warning;
console.log(` Battery: ${batteryColor(status.batteryCapacity + '%')} ${batterySymbol} Runtime: ${getRuntimeColor(status.batteryRuntime)(status.batteryRuntime + ' min')}`);
// Display host info
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
// Display groups if any
if (ups.groups && ups.groups.length > 0) {
const config = this.daemon.getConfig();
const groupNames = ups.groups.map((groupId: string) => {
const group = config.groups?.find((g: { id: string }) => g.id === groupId);
return group ? group.name : groupId;
});
console.log(` ${theme.dim(`Groups: ${groupNames.join(', ')}`)}`);
}
console.log('');
logger.logBoxEnd();
} catch (error) {
logger.logBoxTitle(`UPS Status: ${ups.name}`, boxWidth);
logger.logBoxLine(
`Failed to retrieve UPS status: ${error instanceof Error ? error.message : String(error)}`,
);
logger.logBoxEnd();
// Display error for this UPS
console.log(` ${symbols.error} ${theme.highlight(ups.name)} - ${theme.error('Connection failed')}`);
console.log(` ${theme.dim(error instanceof Error ? error.message : String(error))}`);
console.log(` ${theme.dim(`Host: ${ups.snmp.host}:${ups.snmp.port}`)}`);
console.log('');
}
}