fnos-cli 0.2.0 → 0.3.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.
@@ -1,144 +1,23 @@
1
- /**
2
- * Command registration helper
3
- */
4
-
5
- const { COMMAND_MAPPING } = require('../constants');
6
- const { createClient, getSDKInstance } = require('../utils/client');
7
- const { formatOutput } = require('../utils/formatter');
8
- const { logger } = require('../utils/logger');
9
- const { resolveCredentials } = require('../utils/auth-helper');
10
-
11
- /**
12
- * Register all dynamic commands based on COMMAND_MAPPING
13
- * @param {Command} program - Commander program instance
14
- */
15
- function registerCommands(program) {
16
- for (const [commandName, config] of Object.entries(COMMAND_MAPPING)) {
17
- for (const [subCommandName, commandConfig] of Object.entries(config.commands)) {
18
- // Create combined command name (e.g., "resmon.cpu")
19
- const fullCommandName = `${commandName}.${subCommandName}`;
20
-
21
- // Create command
22
- const cmd = program.command(fullCommandName)
23
- .description(commandConfig.description);
24
-
25
- // Add authentication parameters
26
- cmd.option('-e, --endpoint <endpoint>', 'Server endpoint (e.g., nas-9.timandes.net:5666)');
27
- cmd.option('-u, --username <username>', 'Username');
28
- cmd.option('-p, --password <password>', 'Password');
29
-
30
- // Add parameters
31
- if (commandConfig.params) {
32
- commandConfig.params.forEach(param => {
33
- const optionName = `--${param}`;
34
- const description = `${param} parameter`;
35
- if (param === 'disk' || param === 'path' || param === 'ifName' || param === 'files') {
36
- cmd.requiredOption(optionName + ' <value>', `${description} (required)`);
37
- } else {
38
- cmd.option(optionName + ' <value>', description);
39
- }
40
- });
41
- }
42
-
43
- // Add action handler
44
- cmd.action(async (options) => {
45
- try {
46
- // Get global options from root command
47
- const globalOptions = program.opts();
48
-
49
- // Validate required parameters
50
- if (commandConfig.params) {
51
- const requiredParams = ['disk', 'path', 'ifName', 'files'];
52
- for (const param of requiredParams) {
53
- if (commandConfig.params.includes(param) && !options[param]) {
54
- console.error(`Error: --${param} is required for this command.`);
55
- process.exit(4);
56
- }
57
- }
58
- }
59
-
60
- // Resolve credentials (command line or settings file)
61
- let credentials;
62
- try {
63
- credentials = resolveCredentials(options, program);
64
- } catch (error) {
65
- if (error.message === 'PARTIAL_CREDENTIALS') {
66
- // Display friendly error message
67
- console.error('Error: When using -e/--endpoint, you must also provide -u/--username and -p/--password.');
68
- console.error('Usage: fnos %s -e <endpoint> -u <username> -p <password>', fullCommandName);
69
- process.exit(1);
70
- }
71
- throw error;
72
- }
73
-
74
- // Create client with resolved credentials
75
- const client = await createClient({
76
- credentials,
77
- timeout: 60
78
- });
79
-
80
- // Get SDK instance
81
- const sdkInstance = getSDKInstance(client, config.className);
82
-
83
- // Prepare method arguments
84
- const args = [];
85
- const timeoutFirstClasses = ['ResourceMonitor', 'User', 'SystemInfo', 'SAC'];
86
-
87
- if (commandConfig.params) {
88
- commandConfig.params.forEach(param => {
89
- if (options[param] !== undefined) {
90
- let value = options[param];
91
- // Handle comma-separated lists
92
- if (param === 'files' || param === 'items') {
93
- if (typeof value === 'string') {
94
- value = value.split(',').map(s => s.trim());
95
- } else if (!Array.isArray(value)) {
96
- value = [value];
97
- }
98
- }
99
- // Handle boolean parameters
100
- if (param === 'noHotSpare' || param === 'moveToTrashbin') {
101
- value = value === 'false' ? false : value;
102
- }
103
- // Handle numeric parameters
104
- if (param === 'type') {
105
- value = parseInt(value, 10);
106
- }
107
- args.push(value);
108
- }
109
- });
110
- }
111
-
112
- // Add timeout based on class pattern
113
- if (timeoutFirstClasses.includes(config.className)) {
114
- // Timeout first pattern: (timeout, params...)
115
- args.unshift(10000);
116
- } else {
117
- // Timeout last pattern: (params..., timeout)
118
- args.push(10000);
119
- }
120
-
121
- // Execute method
122
- logger.info(`Executing ${config.className}.${commandConfig.method}...`);
123
- const result = await sdkInstance[commandConfig.method](...args);
124
-
125
- // Format output
126
- console.log(formatOutput(result, globalOptions.raw));
127
-
128
- // Close connection
129
- client.close();
130
- process.exit(0);
131
- } catch (error) {
132
- logger.error(`Command failed: ${error.message}`);
133
- const globalOptions = program.opts();
134
- if (globalOptions.debug || globalOptions.silly) {
135
- console.error(error);
136
- }
137
- process.exit(1);
138
- }
1
+ const { COMMAND_CATALOG } = require('./catalog');
2
+ const { registerAuthenticationOptions, registerBusinessOptions } = require('./options');
3
+ const { runCommand } = require('./runner');
4
+
5
+ function registerCommands(program, dependencies = {}) {
6
+ for (const [domainName, domain] of Object.entries(COMMAND_CATALOG)) {
7
+ for (const [commandName, definition] of Object.entries(domain.commands)) {
8
+ const command = program.command(`${domainName}.${commandName}`).description(definition.description);
9
+ registerAuthenticationOptions(command);
10
+ registerBusinessOptions(command, definition.options);
11
+ command.action(async (options) => {
12
+ process.exitCode = await runCommand({
13
+ domain,
14
+ definition,
15
+ options,
16
+ globalOptions: program.opts()
17
+ }, dependencies);
139
18
  });
140
19
  }
141
20
  }
142
21
  }
143
22
 
144
- module.exports = { registerCommands };
23
+ module.exports = { registerCommands };
@@ -0,0 +1,128 @@
1
+ const { Option } = require('commander');
2
+
3
+ class CommandParameterError extends Error {
4
+ constructor(message) {
5
+ super(message);
6
+ this.name = 'CommandParameterError';
7
+ this.exitCode = 4;
8
+ }
9
+ }
10
+
11
+ function cloneDefault(value) {
12
+ if (Array.isArray(value)) return value.map((item) => cloneDefault(item));
13
+ if (value && typeof value === 'object') return { ...value };
14
+ return value;
15
+ }
16
+
17
+ function parseBoolean(name, value) {
18
+ if (typeof value === 'boolean') return value;
19
+ if (typeof value === 'string' && value.toLowerCase() === 'true') return true;
20
+ if (typeof value === 'string' && value.toLowerCase() === 'false') return false;
21
+ throw new CommandParameterError(`--${name} must be true or false.`);
22
+ }
23
+
24
+ function parseInteger(definition, value) {
25
+ if (typeof value === 'string' && !/^-?\d+$/.test(value.trim())) {
26
+ throw new CommandParameterError(`--${definition.name} must be a decimal integer.`);
27
+ }
28
+ const parsed = typeof value === 'number' ? value : Number(value);
29
+ if (!Number.isInteger(parsed)) throw new CommandParameterError(`--${definition.name} must be an integer.`);
30
+ if (definition.min !== undefined && parsed < definition.min) {
31
+ throw new CommandParameterError(`--${definition.name} must be at least ${definition.min}.`);
32
+ }
33
+ if (definition.max !== undefined && parsed > definition.max) {
34
+ throw new CommandParameterError(`--${definition.name} must be at most ${definition.max}.`);
35
+ }
36
+ if (definition.allowedValues && !definition.allowedValues.includes(parsed)) {
37
+ throw new CommandParameterError(`--${definition.name} must be one of: ${definition.allowedValues.join(', ')}.`);
38
+ }
39
+ return parsed;
40
+ }
41
+
42
+ function parseJson(name, value) {
43
+ if (value && typeof value === 'object') return value;
44
+ try {
45
+ return JSON.parse(value);
46
+ } catch {
47
+ throw new CommandParameterError(`--${name} must be valid JSON.`);
48
+ }
49
+ }
50
+
51
+ function parseValue(definition, value) {
52
+ if (definition.type === 'boolean') return parseBoolean(definition.name, value);
53
+ if (definition.type === 'integer') return parseInteger(definition, value);
54
+ if (definition.type === 'csv') {
55
+ const values = Array.isArray(value) ? value : String(value).split(',').map((item) => item.trim()).filter(Boolean);
56
+ if (values.length === 0) throw new CommandParameterError(`--${definition.name} must contain at least one value.`);
57
+ return values;
58
+ }
59
+ if (definition.type === 'json') {
60
+ const parsed = parseJson(definition.name, value);
61
+ if (!parsed || Array.isArray(parsed) || typeof parsed !== 'object') {
62
+ throw new CommandParameterError(`--${definition.name} must be a JSON object.`);
63
+ }
64
+ return parsed;
65
+ }
66
+ if (definition.type === 'json-record-array') {
67
+ const parsed = parseJson(definition.name, value);
68
+ if (!Array.isArray(parsed) || parsed.length === 0 || parsed.some((item) => !item || Array.isArray(item) || typeof item !== 'object')) {
69
+ throw new CommandParameterError(`--${definition.name} must be a non-empty JSON object array.`);
70
+ }
71
+ return parsed;
72
+ }
73
+ const parsed = String(value);
74
+ if (!parsed.trim()) throw new CommandParameterError(`--${definition.name} must not be empty.`);
75
+ return parsed;
76
+ }
77
+
78
+ function optionReadNames(definition) {
79
+ if (definition.readNames) return definition.readNames;
80
+ return [new Option(definition.flag).attributeName()];
81
+ }
82
+
83
+ function readRawValue(definition, options) {
84
+ for (const name of optionReadNames(definition)) {
85
+ if (options[name] !== undefined) return options[name];
86
+ }
87
+ return undefined;
88
+ }
89
+
90
+ function buildMethodArguments(definition, options, timeout = 10000) {
91
+ const values = definition.options.map((option) => {
92
+ const raw = readRawValue(option, options);
93
+ if (raw === undefined) {
94
+ if (option.required) throw new CommandParameterError(`--${option.name} is required.`);
95
+ return cloneDefault(option.defaultValue);
96
+ }
97
+ return parseValue(option, raw);
98
+ });
99
+ return definition.timeoutPosition === 'first' ? [timeout, ...values] : [...values, timeout];
100
+ }
101
+
102
+ function registerBusinessOptions(command, definitions) {
103
+ for (const definition of definitions) {
104
+ command.addOption(new Option(definition.flag, definition.description));
105
+ for (const legacyFlag of definition.legacyFlags || []) {
106
+ command.addOption(new Option(legacyFlag, `${definition.description} (legacy spelling)`).hideHelp());
107
+ }
108
+ }
109
+ return command;
110
+ }
111
+
112
+ function registerAuthenticationOptions(command, requiredCredentials = false) {
113
+ const add = requiredCredentials ? 'requiredOption' : 'option';
114
+ command[add]('-e, --endpoint <endpoint>', 'Server endpoint, including optional ws:// or wss:// prefix');
115
+ command[add]('-u, --username <username>', 'Username');
116
+ command[add]('-p, --password <password>', 'Password');
117
+ command.option('--twofa-code <code>', 'Six-digit two-factor code');
118
+ command.option('--trust-device', 'Trust this device after two-factor login');
119
+ command.option('--verify-ssl', 'Verify the WSS server certificate');
120
+ return command;
121
+ }
122
+
123
+ module.exports = {
124
+ CommandParameterError,
125
+ buildMethodArguments,
126
+ registerAuthenticationOptions,
127
+ registerBusinessOptions
128
+ };
@@ -0,0 +1,37 @@
1
+ const { buildMethodArguments } = require('./options');
2
+ const clientUtils = require('../utils/client');
3
+ const { formatOutput } = require('../utils/formatter');
4
+ const { resolveCredentials } = require('../utils/auth-helper');
5
+ const { errorMessage } = require('../utils/errors');
6
+
7
+ async function runCommand(context, dependencies = {}) {
8
+ const createClient = dependencies.createClient || clientUtils.createClient;
9
+ const getSDKInstance = dependencies.getSDKInstance || clientUtils.getSDKInstance;
10
+ const resolve = dependencies.resolveCredentials || resolveCredentials;
11
+ const stdout = dependencies.stdout || console.log;
12
+ const stderr = dependencies.stderr || console.error;
13
+ let client;
14
+
15
+ try {
16
+ const args = buildMethodArguments(context.definition, context.options, 10000);
17
+ const credentials = resolve(context.options);
18
+ client = await createClient({
19
+ credentials,
20
+ timeout: 60,
21
+ twofaCode: context.options.twofaCode,
22
+ trustDevice: context.options.trustDevice,
23
+ verifySsl: context.options.verifySsl
24
+ });
25
+ const instance = getSDKInstance(client, context.domain.className);
26
+ const result = await instance[context.definition.method](...args);
27
+ stdout(formatOutput(result, context.globalOptions.raw));
28
+ return 0;
29
+ } catch (error) {
30
+ stderr(`Error: ${errorMessage(error)}`);
31
+ return error.exitCode || 1;
32
+ } finally {
33
+ if (client) client.close();
34
+ }
35
+ }
36
+
37
+ module.exports = { errorMessage, runCommand };
package/src/index.js CHANGED
@@ -1,57 +1,47 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- /**
4
- * fnos-cli - CLI client for 飞牛 fnOS system
5
- */
6
-
7
3
  const { Command } = require('commander');
8
4
  const { registerLoginCommand, registerLogoutCommand } = require('./commands/auth');
9
5
  const { registerCommands } = require('./commands');
10
6
  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('0.2.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
- }
7
+ const { errorMessage } = require('./utils/errors');
8
+ const { version } = require('../package.json');
9
+
10
+ function createProgram(dependencies = {}) {
11
+ const program = new Command();
12
+ program.name('fnos').description('CLI client for 飞牛 fnOS system').version(version);
13
+ program
14
+ .option('--raw', 'Output raw JSON response')
15
+ .option('-v, --verbose', 'Verbose output (info level)')
16
+ .option('-vv, --debug', 'Debug output (debug level)')
17
+ .option('-vvv, --silly', 'Silly output (silly level)');
18
+ registerLoginCommand(program, dependencies);
19
+ registerLogoutCommand(program, dependencies);
20
+ registerCommands(program, dependencies);
21
+ program.hook('preAction', (thisCommand) => {
22
+ const options = thisCommand.opts();
23
+ const verboseLevel = options.silly ? 3 : options.debug ? 2 : options.verbose ? 1 : 0;
24
+ setLogLevel(verboseLevel);
25
+ process.env.LOG_LEVEL = ['error', 'info', 'debug', 'silly'][verboseLevel];
26
+ });
27
+ return program;
28
+ }
29
+
30
+ async function main(argv = process.argv, dependencies = {}) {
31
+ const program = createProgram(dependencies);
32
+ if (argv.slice(2).length === 0) {
33
+ program.outputHelp();
34
+ return 0;
35
+ }
36
+ await program.parseAsync(argv);
37
+ return process.exitCode || 0;
38
+ }
39
+
40
+ if (require.main === module) {
41
+ main().catch((error) => {
42
+ console.error(`Error: ${errorMessage(error)}`);
43
+ process.exitCode = error.exitCode || 1;
44
+ });
45
+ }
46
+
47
+ module.exports = { createProgram, main };
@@ -15,6 +15,13 @@ function hasPartialCredentials(options) {
15
15
  return provided.length > 0 && provided.length < 3;
16
16
  }
17
17
 
18
+ function credentialError(message) {
19
+ const error = new Error(message);
20
+ error.name = 'AuthenticationError';
21
+ error.exitCode = 3;
22
+ return error;
23
+ }
24
+
18
25
  /**
19
26
  * Validate command line credentials completeness
20
27
  * @param {Object} options - Command line options
@@ -23,10 +30,10 @@ function hasPartialCredentials(options) {
23
30
  */
24
31
  function validateCommandLineCredentials(options) {
25
32
  if (hasPartialCredentials(options)) {
26
- throw new Error('PARTIAL_CREDENTIALS');
33
+ throw credentialError('PARTIAL_CREDENTIALS');
27
34
  }
28
35
 
29
- const hasCredentials = options.endpoint && options.username && options.password;
36
+ const hasCredentials = Boolean(options.endpoint && options.username && options.password);
30
37
  return { isValid: true, hasCredentials };
31
38
  }
32
39
 
@@ -36,7 +43,7 @@ function validateCommandLineCredentials(options) {
36
43
  * @returns {Object} - Credentials object
37
44
  * @throws {Error} - If no valid credentials are found or partial credentials are provided
38
45
  */
39
- function resolveCredentials(options) {
46
+ function resolveCredentials(options, settingsStore = settings) {
40
47
  const validation = validateCommandLineCredentials(options);
41
48
 
42
49
  if (validation.hasCredentials) {
@@ -50,9 +57,9 @@ function resolveCredentials(options) {
50
57
  }
51
58
 
52
59
  // Read from settings file
53
- const credentials = settings.getCredentials();
60
+ const credentials = settingsStore.getCredentials();
54
61
  if (!credentials || !credentials.endpoint || !credentials.username || !credentials.password) {
55
- throw new Error('No valid credentials found. Please use "fnos login" or provide -e/-u/-p options.');
62
+ throw credentialError('No valid credentials found. Please use "fnos login" or provide -e/-u/-p options.');
56
63
  }
57
64
 
58
65
  return {
@@ -65,4 +72,4 @@ module.exports = {
65
72
  hasPartialCredentials,
66
73
  validateCommandLineCredentials,
67
74
  resolveCredentials
68
- };
75
+ };