fnos-cli 0.1.0
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.
- package/LICENSE +201 -0
- package/README.md +300 -0
- package/bin/fnos +3 -0
- package/package.json +3 -0
- package/src/commands/auth.js +61 -0
- package/src/commands/index.js +123 -0
- package/src/constants.js +69 -0
- package/src/index.js +57 -0
- package/src/utils/client.js +119 -0
- package/src/utils/formatter.js +366 -0
- package/src/utils/logger.js +120 -0
- package/src/utils/settings.js +106 -0
package/src/constants.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command mapping configuration
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const COMMAND_MAPPING = {
|
|
6
|
+
resmon: {
|
|
7
|
+
className: 'ResourceMonitor',
|
|
8
|
+
commands: {
|
|
9
|
+
cpu: { method: 'cpu', description: 'Get CPU resource monitoring information' },
|
|
10
|
+
gpu: { method: 'gpu', description: 'Get GPU resource monitoring information' },
|
|
11
|
+
mem: { method: 'memory', description: 'Get memory resource monitoring information' },
|
|
12
|
+
disk: { method: 'disk', description: 'Get disk resource monitoring information' },
|
|
13
|
+
net: { method: 'net', description: 'Get network resource monitoring information' },
|
|
14
|
+
gen: { method: 'general', description: 'Get general resource monitoring information', params: ['items'] }
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
store: {
|
|
18
|
+
className: 'Store',
|
|
19
|
+
commands: {
|
|
20
|
+
general: { method: 'general', description: 'Get storage general information' },
|
|
21
|
+
calcSpace: { method: 'calculateSpace', description: 'Calculate storage space information' },
|
|
22
|
+
listDisk: { method: 'listDisks', description: 'List disk information', params: ['noHotSpare'] },
|
|
23
|
+
diskSmart: { method: 'getDiskSmart', description: 'Get disk SMART information', params: ['disk'] },
|
|
24
|
+
state: { method: 'getState', description: 'Get storage state information', params: ['name', 'uuid'] }
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
sysinfo: {
|
|
28
|
+
className: 'SystemInfo',
|
|
29
|
+
commands: {
|
|
30
|
+
getHostName: { method: 'getHostName', description: 'Get host name information' },
|
|
31
|
+
getTrimVersion: { method: 'getTrimVersion', description: 'Get Trim version information' },
|
|
32
|
+
getMachineId: { method: 'getMachineId', description: 'Get machine ID information' },
|
|
33
|
+
getHardwareInfo: { method: 'getHardwareInfo', description: 'Get hardware information' },
|
|
34
|
+
getUptime: { method: 'getUptime', description: 'Get system uptime information' }
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
user: {
|
|
38
|
+
className: 'User',
|
|
39
|
+
commands: {
|
|
40
|
+
info: { method: 'getInfo', description: 'Get user information' },
|
|
41
|
+
listUG: { method: 'listUserGroups', description: 'List users and groups' },
|
|
42
|
+
groupUsers: { method: 'groupUsers', description: 'Get user grouping information' },
|
|
43
|
+
isAdmin: { method: 'isAdmin', description: 'Check if current user is admin' }
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
network: {
|
|
47
|
+
className: 'Network',
|
|
48
|
+
commands: {
|
|
49
|
+
list: { method: 'list', description: 'List network information', params: ['type'] },
|
|
50
|
+
detect: { method: 'detect', description: 'Detect network interface', params: ['ifName'] }
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
file: {
|
|
54
|
+
className: 'File',
|
|
55
|
+
commands: {
|
|
56
|
+
ls: { method: 'list', description: 'List files and directories', params: ['path'] },
|
|
57
|
+
mkdir: { method: 'mkdir', description: 'Create directory', params: ['path'] },
|
|
58
|
+
rm: { method: 'remove', description: 'Remove files or directories', params: ['files', 'moveToTrashbin', 'details'] }
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
sac: {
|
|
62
|
+
className: 'SAC',
|
|
63
|
+
commands: {
|
|
64
|
+
upsStatus: { method: 'upsStatus', description: 'Get UPS status information' }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
module.exports = { COMMAND_MAPPING };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* fnos-cli - CLI client for 飞牛 fnOS system
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const { Command } = require('commander');
|
|
8
|
+
const { registerLoginCommand, registerLogoutCommand } = require('./commands/auth');
|
|
9
|
+
const { registerCommands } = require('./commands');
|
|
10
|
+
const { setLogLevel } = require('./utils/logger');
|
|
11
|
+
|
|
12
|
+
// Create program
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
// Configure program
|
|
16
|
+
program
|
|
17
|
+
.name('fnos')
|
|
18
|
+
.description('CLI client for 飞牛 fnOS system')
|
|
19
|
+
.version('1.0.0');
|
|
20
|
+
|
|
21
|
+
// Global options
|
|
22
|
+
program
|
|
23
|
+
.option('--raw', 'Output raw JSON response')
|
|
24
|
+
.option('-v, --verbose', 'Verbose output (info level)')
|
|
25
|
+
.option('-vv, --debug', 'Debug output (debug level)')
|
|
26
|
+
.option('-vvv, --silly', 'Silly output (silly level)');
|
|
27
|
+
|
|
28
|
+
// Register auth commands
|
|
29
|
+
registerLoginCommand(program);
|
|
30
|
+
registerLogoutCommand(program);
|
|
31
|
+
|
|
32
|
+
// Register dynamic commands
|
|
33
|
+
registerCommands(program);
|
|
34
|
+
|
|
35
|
+
// Hook into parse to set log level before command execution
|
|
36
|
+
program.hook('preAction', (thisCommand, actionCommand) => {
|
|
37
|
+
const options = thisCommand.opts();
|
|
38
|
+
let verboseLevel = 0;
|
|
39
|
+
if (options.verbose) verboseLevel = 1;
|
|
40
|
+
if (options.debug) verboseLevel = 2;
|
|
41
|
+
if (options.silly) verboseLevel = 3;
|
|
42
|
+
|
|
43
|
+
// Set log level for both CLI and SDK logger
|
|
44
|
+
setLogLevel(verboseLevel);
|
|
45
|
+
|
|
46
|
+
// Set environment variable to control SDK logger level
|
|
47
|
+
const sdkLogLevels = ['error', 'info', 'debug', 'silly'];
|
|
48
|
+
process.env.LOG_LEVEL = sdkLogLevels[verboseLevel];
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Parse
|
|
52
|
+
program.parse(process.argv);
|
|
53
|
+
|
|
54
|
+
// Show help if no command provided
|
|
55
|
+
if (!process.argv.slice(2).length) {
|
|
56
|
+
program.outputHelp();
|
|
57
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* FnosClient wrapper with authentication
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
const { FnosClient, ResourceMonitor, Store, SystemInfo, User, Network, File, SAC } = require('fnos');
|
|
6
|
+
const settings = require('./settings');
|
|
7
|
+
const { logger } = require('./logger');
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Create authenticated FnosClient
|
|
11
|
+
* @param {Object} options - Options object
|
|
12
|
+
* @param {string} options.endpoint - Server endpoint
|
|
13
|
+
* @param {string} options.username - Username
|
|
14
|
+
* @param {string} options.password - Password
|
|
15
|
+
* @param {number} options.timeout - Connection timeout in seconds
|
|
16
|
+
* @returns {Promise<FnosClient>} Authenticated client
|
|
17
|
+
*/
|
|
18
|
+
async function createClient(options = {}) {
|
|
19
|
+
const { endpoint, username, password, timeout = 60 } = options;
|
|
20
|
+
|
|
21
|
+
// Get credentials from settings if not provided
|
|
22
|
+
const savedCredentials = settings.getCredentials();
|
|
23
|
+
const finalEndpoint = endpoint || savedCredentials?.endpoint;
|
|
24
|
+
const finalUsername = username || savedCredentials?.username;
|
|
25
|
+
const finalPassword = password || savedCredentials?.password;
|
|
26
|
+
|
|
27
|
+
if (!finalEndpoint || !finalUsername) {
|
|
28
|
+
throw new Error('Missing credentials. Please run "fnos login" first or provide -e, -u, -p parameters.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Create client
|
|
32
|
+
const client = new FnosClient();
|
|
33
|
+
logger.info(`Connecting to ${finalEndpoint}...`);
|
|
34
|
+
|
|
35
|
+
// Connect
|
|
36
|
+
await client.connect(finalEndpoint, timeout * 1000);
|
|
37
|
+
logger.info('Connected successfully');
|
|
38
|
+
|
|
39
|
+
// Try to login with token first
|
|
40
|
+
// if (savedCredentials?.token && savedCredentials?.secret && !endpoint && !username && !password) {
|
|
41
|
+
// try {
|
|
42
|
+
// logger.info('Logging in with saved token...');
|
|
43
|
+
// await client.loginViaToken(savedCredentials.token, savedCredentials.longToken, savedCredentials.secret);
|
|
44
|
+
// logger.info('Logged in successfully with token');
|
|
45
|
+
// return client;
|
|
46
|
+
// } catch (error) {
|
|
47
|
+
// logger.warn('Token login failed, falling back to password login:', error.message);
|
|
48
|
+
// }
|
|
49
|
+
// }
|
|
50
|
+
|
|
51
|
+
// Login with password
|
|
52
|
+
if (!finalPassword) {
|
|
53
|
+
throw new Error('Password required. Please run "fnos login" first or provide -p parameter.');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
logger.info('Logging in...');
|
|
57
|
+
const loginResult = await client.login(finalUsername, finalPassword);
|
|
58
|
+
logger.info('Logged in successfully');
|
|
59
|
+
|
|
60
|
+
// Save credentials if they were provided via command line
|
|
61
|
+
if (endpoint || username || password) {
|
|
62
|
+
settings.saveCredentials({
|
|
63
|
+
endpoint: finalEndpoint,
|
|
64
|
+
username: finalUsername,
|
|
65
|
+
password: finalPassword,
|
|
66
|
+
token: loginResult.token,
|
|
67
|
+
longToken: loginResult.longToken,
|
|
68
|
+
secret: loginResult.secret
|
|
69
|
+
});
|
|
70
|
+
logger.info('Credentials saved');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return client;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Execute a command with auto-retry
|
|
78
|
+
* @param {FnosClient} client - FnosClient instance
|
|
79
|
+
* @param {Function} commandFn - Command function to execute
|
|
80
|
+
* @returns {Promise<*>} Command result
|
|
81
|
+
*/
|
|
82
|
+
async function executeCommand(client, commandFn) {
|
|
83
|
+
try {
|
|
84
|
+
return await commandFn();
|
|
85
|
+
} catch (error) {
|
|
86
|
+
if (error.message.includes('token') || error.message.includes('auth')) {
|
|
87
|
+
logger.warn('Authentication failed, trying to re-login...');
|
|
88
|
+
// Re-login logic could be added here
|
|
89
|
+
}
|
|
90
|
+
throw error;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Get SDK class instance
|
|
96
|
+
* @param {FnosClient} client - FnosClient instance
|
|
97
|
+
* @param {string} className - Name of the SDK class
|
|
98
|
+
* @returns {*} SDK class instance
|
|
99
|
+
*/
|
|
100
|
+
function getSDKInstance(client, className) {
|
|
101
|
+
const instances = {
|
|
102
|
+
ResourceMonitor: new ResourceMonitor(client),
|
|
103
|
+
Store: new Store(client),
|
|
104
|
+
SystemInfo: new SystemInfo(client),
|
|
105
|
+
User: new User(client),
|
|
106
|
+
Network: new Network(client),
|
|
107
|
+
File: new File(client),
|
|
108
|
+
SAC: new SAC(client)
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const instance = instances[className];
|
|
112
|
+
if (!instance) {
|
|
113
|
+
throw new Error(`Unknown SDK class: ${className}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return instance;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
module.exports = { createClient, executeCommand, getSDKInstance };
|
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Output formatter utility
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Format output based on data type
|
|
7
|
+
* @param {*} data - Data to format
|
|
8
|
+
* @param {boolean} raw - If true, output raw JSON
|
|
9
|
+
* @returns {string} Formatted output
|
|
10
|
+
*/
|
|
11
|
+
function formatOutput(data, raw = false) {
|
|
12
|
+
if (raw) {
|
|
13
|
+
return JSON.stringify(data, null, 2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (data === null || data === undefined) {
|
|
17
|
+
return 'No data returned';
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (Array.isArray(data)) {
|
|
21
|
+
return formatArray(data);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (typeof data === 'object') {
|
|
25
|
+
return formatObject(data);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return String(data);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Format array as table with borders
|
|
33
|
+
* @param {Array} arr - Array to format
|
|
34
|
+
* @returns {string} Formatted table
|
|
35
|
+
*/
|
|
36
|
+
function formatArray(arr) {
|
|
37
|
+
if (arr.length === 0) {
|
|
38
|
+
return 'Empty array';
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Get all keys from objects
|
|
42
|
+
const keys = Object.keys(arr[0]);
|
|
43
|
+
const columnWidths = keys.map(key => {
|
|
44
|
+
const headerWidth = key.length;
|
|
45
|
+
const maxValWidth = Math.max(...arr.map(item => {
|
|
46
|
+
const val = item[key];
|
|
47
|
+
return val ? String(val).length : 0;
|
|
48
|
+
}));
|
|
49
|
+
return Math.max(headerWidth, maxValWidth);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Helper to format value with units
|
|
53
|
+
const formatValue = (key, val) => {
|
|
54
|
+
if (val === null || val === undefined) return '';
|
|
55
|
+
if (key === 'temp') return `${val}°C`;
|
|
56
|
+
if (key === 'read' || key === 'write') return val.toString();
|
|
57
|
+
if (key === 'busy') return `${val}%`;
|
|
58
|
+
if (key === 'standby') return val ? '✓' : '✗';
|
|
59
|
+
return String(val);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Build top border
|
|
63
|
+
let output = '┌';
|
|
64
|
+
columnWidths.forEach((width, i) => {
|
|
65
|
+
output += '─'.repeat(width + 2);
|
|
66
|
+
if (i < columnWidths.length - 1) output += '┬';
|
|
67
|
+
});
|
|
68
|
+
output += '┐\n';
|
|
69
|
+
|
|
70
|
+
// Build header
|
|
71
|
+
output += '│';
|
|
72
|
+
keys.forEach((key, i) => {
|
|
73
|
+
output += ' ' + key.padEnd(columnWidths[i]) + ' │';
|
|
74
|
+
});
|
|
75
|
+
output += '\n';
|
|
76
|
+
|
|
77
|
+
// Build separator
|
|
78
|
+
output += '├';
|
|
79
|
+
columnWidths.forEach((width, i) => {
|
|
80
|
+
output += '─'.repeat(width + 2);
|
|
81
|
+
if (i < columnWidths.length - 1) output += '┼';
|
|
82
|
+
});
|
|
83
|
+
output += '┤\n';
|
|
84
|
+
|
|
85
|
+
// Build rows
|
|
86
|
+
arr.forEach(item => {
|
|
87
|
+
output += '│';
|
|
88
|
+
keys.forEach((key, i) => {
|
|
89
|
+
const val = item[key];
|
|
90
|
+
const formattedVal = formatValue(key, val);
|
|
91
|
+
output += ' ' + formattedVal.padEnd(columnWidths[i]) + ' │';
|
|
92
|
+
});
|
|
93
|
+
output += '\n';
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Build bottom border
|
|
97
|
+
output += '└';
|
|
98
|
+
columnWidths.forEach((width, i) => {
|
|
99
|
+
output += '─'.repeat(width + 2);
|
|
100
|
+
if (i < columnWidths.length - 1) output += '┴';
|
|
101
|
+
});
|
|
102
|
+
output += '┘\n';
|
|
103
|
+
|
|
104
|
+
return output;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Format object as pretty-printed JSON
|
|
109
|
+
* @param {Object} obj - Object to format
|
|
110
|
+
* @returns {string} Formatted output
|
|
111
|
+
*/
|
|
112
|
+
function formatObject(obj) {
|
|
113
|
+
const result = [];
|
|
114
|
+
|
|
115
|
+
// Helper to format bytes to GB
|
|
116
|
+
const formatBytes = (bytes) => {
|
|
117
|
+
if (!bytes || bytes === 0) return '0 GB';
|
|
118
|
+
const gb = (bytes / (1024 * 1024 * 1024)).toFixed(2);
|
|
119
|
+
return `${gb} GB`;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// Helper to format speed to MB/s
|
|
123
|
+
const formatSpeed = (speed) => {
|
|
124
|
+
if (!speed || speed === 0) return '0 MB/s';
|
|
125
|
+
const mb = (speed / (1024 * 1024)).toFixed(2);
|
|
126
|
+
return `${mb} MB/s`;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
// Special handling for resmon.gen output (has 'item' field)
|
|
130
|
+
if (obj.item !== undefined) {
|
|
131
|
+
let items;
|
|
132
|
+
if (typeof obj.item === 'string') {
|
|
133
|
+
items = obj.item.split(',').map(s => s.trim());
|
|
134
|
+
} else if (Array.isArray(obj.item)) {
|
|
135
|
+
items = obj.item;
|
|
136
|
+
} else if (typeof obj.item === 'object') {
|
|
137
|
+
// item is an object containing the actual data (storeSpeed, netSpeed, cpuBusy, memPercent)
|
|
138
|
+
const resultItems = [];
|
|
139
|
+
|
|
140
|
+
if (obj.item.memPercent !== undefined) {
|
|
141
|
+
resultItems.push(`Memory: ${obj.item.memPercent}%`);
|
|
142
|
+
}
|
|
143
|
+
if (obj.item.cpuBusy !== undefined) {
|
|
144
|
+
resultItems.push(`CPU: ${obj.item.cpuBusy}%`);
|
|
145
|
+
}
|
|
146
|
+
if (obj.item.storeSpeed) {
|
|
147
|
+
const read = formatSpeed(obj.item.storeSpeed.read);
|
|
148
|
+
const write = formatSpeed(obj.item.storeSpeed.write);
|
|
149
|
+
resultItems.push(`Storage: ↑ ${write} | ↓ ${read}`);
|
|
150
|
+
}
|
|
151
|
+
if (obj.item.netSpeed) {
|
|
152
|
+
const receive = formatSpeed(obj.item.netSpeed.receive);
|
|
153
|
+
const transmit = formatSpeed(obj.item.netSpeed.transmit);
|
|
154
|
+
resultItems.push(`Network: ↑ ${transmit} | ↓ ${receive}`);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (resultItems.length > 0) {
|
|
158
|
+
result.push(resultItems.join(' | '));
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Add metadata at the end if present
|
|
162
|
+
const metadata = {};
|
|
163
|
+
if (obj.reqid) metadata.reqid = obj.reqid;
|
|
164
|
+
if (obj.result) metadata.result = obj.result;
|
|
165
|
+
if (obj.rev) metadata.rev = obj.rev;
|
|
166
|
+
if (obj.req) metadata.req = obj.req;
|
|
167
|
+
|
|
168
|
+
if (Object.keys(metadata).length > 0) {
|
|
169
|
+
result.push('');
|
|
170
|
+
result.push('─'.repeat(40));
|
|
171
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
172
|
+
result.push(`${key}: ${value}`);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result.join('\n');
|
|
177
|
+
} else {
|
|
178
|
+
// If item is neither string nor array nor object, skip special handling
|
|
179
|
+
items = [];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const resultItems = [];
|
|
183
|
+
|
|
184
|
+
if (items.includes('memPercent') && obj.memPercent !== undefined) {
|
|
185
|
+
resultItems.push(`Memory: ${obj.memPercent}%`);
|
|
186
|
+
}
|
|
187
|
+
if (items.includes('cpuBusy') && obj.cpuBusy !== undefined) {
|
|
188
|
+
resultItems.push(`CPU: ${obj.cpuBusy}%`);
|
|
189
|
+
}
|
|
190
|
+
if (items.includes('storeSpeed') && obj.storeSpeed) {
|
|
191
|
+
const read = formatSpeed(obj.storeSpeed.read);
|
|
192
|
+
const write = formatSpeed(obj.storeSpeed.write);
|
|
193
|
+
resultItems.push(`Storage: ↑ ${write} | ↓ ${read}`);
|
|
194
|
+
}
|
|
195
|
+
if (items.includes('netSpeed') && obj.netSpeed) {
|
|
196
|
+
const receive = formatSpeed(obj.netSpeed.receive);
|
|
197
|
+
const transmit = formatSpeed(obj.netSpeed.transmit);
|
|
198
|
+
resultItems.push(`Network: ↑ ${transmit} | ↓ ${receive}`);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (resultItems.length > 0) {
|
|
202
|
+
result.push(resultItems.join(' | '));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// Add metadata at the end if present
|
|
206
|
+
const metadata = {};
|
|
207
|
+
if (obj.reqid) metadata.reqid = obj.reqid;
|
|
208
|
+
if (obj.result) metadata.result = obj.result;
|
|
209
|
+
if (obj.rev) metadata.rev = obj.rev;
|
|
210
|
+
if (obj.req) metadata.req = obj.req;
|
|
211
|
+
|
|
212
|
+
if (Object.keys(metadata).length > 0) {
|
|
213
|
+
result.push('');
|
|
214
|
+
result.push('─'.repeat(40));
|
|
215
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
216
|
+
result.push(`${key}: ${value}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return result.join('\n');
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Handle special fields first
|
|
224
|
+
const metadata = {};
|
|
225
|
+
const dataFields = {};
|
|
226
|
+
let hasMainData = false;
|
|
227
|
+
|
|
228
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
229
|
+
// Skip certain metadata fields or handle them separately
|
|
230
|
+
if (['reqid', 'result', 'rev', 'req'].includes(key)) {
|
|
231
|
+
metadata[key] = value;
|
|
232
|
+
} else if (key === 'data' && typeof value === 'object' && value !== null) {
|
|
233
|
+
// Handle 'data' field specially - don't show 'data:' prefix
|
|
234
|
+
hasMainData = true;
|
|
235
|
+
// Recursively format the data object
|
|
236
|
+
const formattedData = formatObject(value);
|
|
237
|
+
if (formattedData) {
|
|
238
|
+
result.push(formattedData);
|
|
239
|
+
}
|
|
240
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
241
|
+
if (Array.isArray(value) && value.length > 0 && typeof value[0] === 'object') {
|
|
242
|
+
// Format array as table with header
|
|
243
|
+
const title = key.charAt(0).toUpperCase() + key.slice(1);
|
|
244
|
+
result.push(`${title} (${value.length} items)`);
|
|
245
|
+
result.push(formatArray(value));
|
|
246
|
+
} else if (key === 'cpu' && typeof value === 'object') {
|
|
247
|
+
// Format CPU info as compact lines
|
|
248
|
+
const lines = [];
|
|
249
|
+
if (value.name) lines.push(`CPU: ${value.name}`);
|
|
250
|
+
if (value.core !== undefined) lines.push(`Cores: ${value.core}`);
|
|
251
|
+
if (value.thread !== undefined) lines.push(`Threads: ${value.thread}`);
|
|
252
|
+
if (value.maxFreq) lines.push(`Max Frequency: ${value.maxFreq} MHz`);
|
|
253
|
+
if (value.num !== undefined) lines.push(`CPU Count: ${value.num}`);
|
|
254
|
+
result.push(lines.join(' | '));
|
|
255
|
+
|
|
256
|
+
// Handle nested fields within cpu object
|
|
257
|
+
if (value.temp) {
|
|
258
|
+
if (Array.isArray(value.temp) && value.temp.length > 0) {
|
|
259
|
+
result.push(`Temperature: ${value.temp[0]}°C`);
|
|
260
|
+
} else if (typeof value.temp === 'object') {
|
|
261
|
+
const temps = Object.entries(value.temp).map(([k, v]) => {
|
|
262
|
+
const core = k === '0' ? '' : `Core${k}: `;
|
|
263
|
+
return `${core}${v}°C`;
|
|
264
|
+
});
|
|
265
|
+
result.push(`Temperature: ${temps.join(', ')}`);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (value.busy && value.busy.user !== undefined) {
|
|
270
|
+
const user = value.busy.user || 0;
|
|
271
|
+
const system = value.busy.system || 0;
|
|
272
|
+
const iowait = value.busy.iowait || 0;
|
|
273
|
+
const other = value.busy.other !== undefined ? value.busy.other : 0;
|
|
274
|
+
const all = value.busy.all !== undefined ? value.busy.all : (user + system + iowait + other);
|
|
275
|
+
result.push(`CPU Usage: user ${user}%, system ${system}%, iowait ${iowait}%, total ${all}%`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (value.loadavg && value.loadavg.avg1min !== undefined) {
|
|
279
|
+
const avg1 = value.loadavg.avg1min.toFixed(2);
|
|
280
|
+
const avg5 = value.loadavg.avg5min ? value.loadavg.avg5min.toFixed(2) : avg1;
|
|
281
|
+
const avg15 = value.loadavg.avg15min ? value.loadavg.avg15min.toFixed(2) : avg1;
|
|
282
|
+
result.push(`Load Average: ${avg1} (1m), ${avg5} (5m), ${avg15} (15m)`);
|
|
283
|
+
}
|
|
284
|
+
} else if (key === 'mem' && typeof value === 'object' && value.total !== undefined) {
|
|
285
|
+
// Format memory info
|
|
286
|
+
const total = formatBytes(value.total);
|
|
287
|
+
const used = formatBytes(value.used);
|
|
288
|
+
const free = formatBytes(value.free);
|
|
289
|
+
const available = formatBytes(value.available);
|
|
290
|
+
const cached = formatBytes(value.cached);
|
|
291
|
+
const buffers = formatBytes(value.buffers);
|
|
292
|
+
const percent = value.total ? ((value.used / value.total) * 100).toFixed(1) : 0;
|
|
293
|
+
result.push(`Memory: ${used} / ${total} (${percent}%) | Free: ${free} | Available: ${available} | Cached: ${cached} | Buffers: ${buffers}`);
|
|
294
|
+
} else if (key === 'swap' && typeof value === 'object' && value.total !== undefined) {
|
|
295
|
+
// Format swap info
|
|
296
|
+
const total = formatBytes(value.total);
|
|
297
|
+
const used = formatBytes(value.used);
|
|
298
|
+
const free = formatBytes(value.free);
|
|
299
|
+
const percent = value.total ? ((value.used / value.total) * 100).toFixed(1) : 0;
|
|
300
|
+
result.push(`Swap: ${used} / ${total} (${percent}%) | Free: ${free}`);
|
|
301
|
+
} else if (key === 'storeSpeed' && typeof value === 'object') {
|
|
302
|
+
// Format storage speed
|
|
303
|
+
const read = formatSpeed(value.read);
|
|
304
|
+
const write = formatSpeed(value.write);
|
|
305
|
+
result.push(`Storage Speed: ↑ ${write} | ↓ ${read}`);
|
|
306
|
+
} else if (key === 'netSpeed' && typeof value === 'object') {
|
|
307
|
+
// Format network speed
|
|
308
|
+
const receive = formatSpeed(value.receive);
|
|
309
|
+
const transmit = formatSpeed(value.transmit);
|
|
310
|
+
result.push(`Network Speed: ↑ ${transmit} | ↓ ${receive}`);
|
|
311
|
+
} else if (key === 'busy' && value.user !== undefined) {
|
|
312
|
+
// Format CPU busy usage as compact line
|
|
313
|
+
const user = value.user || 0;
|
|
314
|
+
const system = value.system || 0;
|
|
315
|
+
const iowait = value.iowait || 0;
|
|
316
|
+
const other = value.other !== undefined ? value.other : 0;
|
|
317
|
+
const all = value.all !== undefined ? value.all : (user + system + iowait + other);
|
|
318
|
+
result.push(`CPU Usage: user ${user}%, system ${system}%, iowait ${iowait}%, total ${all}%`);
|
|
319
|
+
} else if (key === 'loadavg' && value.avg1min !== undefined) {
|
|
320
|
+
// Format load average as compact line
|
|
321
|
+
const avg1 = value.avg1min.toFixed(2);
|
|
322
|
+
const avg5 = value.avg5min ? value.avg5min.toFixed(2) : avg1;
|
|
323
|
+
const avg15 = value.avg15min ? value.avg15min.toFixed(2) : avg1;
|
|
324
|
+
result.push(`Load Average: ${avg1} (1m), ${avg5} (5m), ${avg15} (15m)`);
|
|
325
|
+
} else if (key === 'temp' && typeof value === 'object') {
|
|
326
|
+
// Format temperature for CPU (might have core indices)
|
|
327
|
+
const temps = Object.entries(value).map(([k, v]) => {
|
|
328
|
+
const core = k === '0' ? '' : `Core${k}: `;
|
|
329
|
+
return `${core}${v}°C`;
|
|
330
|
+
});
|
|
331
|
+
result.push(`Temperature: ${temps.join(', ')}`);
|
|
332
|
+
} else {
|
|
333
|
+
// Handle nested objects
|
|
334
|
+
result.push(`${key}:`);
|
|
335
|
+
result.push(formatObject(value));
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
dataFields[key] = value;
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// Add data fields at the beginning
|
|
343
|
+
if (Object.keys(dataFields).length > 0) {
|
|
344
|
+
for (const [key, value] of Object.entries(dataFields)) {
|
|
345
|
+
result.unshift(`${key}: ${value}`);
|
|
346
|
+
}
|
|
347
|
+
if (!hasMainData) {
|
|
348
|
+
result.push('');
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Add metadata at the end if present
|
|
353
|
+
if (Object.keys(metadata).length > 0) {
|
|
354
|
+
if (result.length > 0) {
|
|
355
|
+
result.push('');
|
|
356
|
+
}
|
|
357
|
+
result.push('─'.repeat(40));
|
|
358
|
+
for (const [key, value] of Object.entries(metadata)) {
|
|
359
|
+
result.push(`${key}: ${value}`);
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return result.join('\n');
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
module.exports = { formatOutput };
|