litai-spex 1.0.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.
@@ -0,0 +1,114 @@
1
+ const chalk = require('chalk');
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const { DEFAULT_CONFIG } = require('../utils/config');
5
+
6
+ /**
7
+ * Init command handler - creates a default config.json file
8
+ */
9
+ async function initCommand() {
10
+ const configPath = path.resolve(process.cwd(), 'config.json');
11
+
12
+ console.log(chalk.cyan('\nšŸ”§ LitAI-Spex Initialize\n'));
13
+
14
+ // Check if config already exists
15
+ if (fs.existsSync(configPath)) {
16
+ console.log(chalk.yellow('āš ļø config.json already exists in this directory.'));
17
+ console.log(chalk.gray(' Delete it first if you want to create a new one.\n'));
18
+ process.exit(1);
19
+ }
20
+
21
+ // Create config file
22
+ const configContent = {
23
+ connection: {
24
+ host: '192.168.1.100',
25
+ username: 'deploy',
26
+ password: '',
27
+ privateKeyPath: '',
28
+ targetDirectory: '/home/rock/ui'
29
+ },
30
+ deploy: {
31
+ excludeDirectories: [
32
+ 'node_modules',
33
+ 'logs',
34
+ '.git',
35
+ '.idea',
36
+ '.vscode',
37
+ 'coverage',
38
+ 'dist',
39
+ '.cache'
40
+ ],
41
+ excludeFiles: [
42
+ 'package-lock.json',
43
+ '.env.local',
44
+ '.env.development',
45
+ '.DS_Store',
46
+ 'config.json',
47
+ 'Thumbs.db'
48
+ ],
49
+ excludePatterns: [
50
+ '*.log',
51
+ '*.tmp',
52
+ '*.bak',
53
+ '*.swp',
54
+ '*~'
55
+ ]
56
+ },
57
+ scripts: {
58
+ afterDeploy: 'deploy.sh'
59
+ },
60
+ project: {
61
+ repositoryUrl: 'https://github.com/username/repo.git'
62
+ }
63
+ };
64
+
65
+ fs.writeFileSync(configPath, JSON.stringify(configContent, null, 2));
66
+
67
+ console.log(chalk.green('āœ… Created config.json\n'));
68
+ console.log(chalk.gray('šŸ“ Configuration file created with default settings.'));
69
+ console.log(chalk.gray(' Edit config.json to set your connection details:\n'));
70
+ console.log(chalk.white(' {'));
71
+ console.log(chalk.white(' "connection": {'));
72
+ console.log(chalk.cyan(' "host": "your-server-ip",'));
73
+ console.log(chalk.cyan(' "username": "your-username",'));
74
+ console.log(chalk.cyan(' "password": "your-password",'));
75
+ console.log(chalk.gray(' // OR use private key:'));
76
+ console.log(chalk.cyan(' "privateKeyPath": "~/.ssh/id_rsa",'));
77
+ console.log(chalk.cyan(' "targetDirectory": "/var/www/app"'));
78
+ console.log(chalk.white(' }'));
79
+ console.log(chalk.white(' }\n'));
80
+
81
+ console.log(chalk.gray('šŸ”’ Security tip: Add config.json to .gitignore!\n'));
82
+
83
+ // Create sample deploy.sh if it doesn't exist
84
+ const deployShPath = path.resolve(process.cwd(), 'deploy.sh');
85
+ if (!fs.existsSync(deployShPath)) {
86
+ const deployShContent = `#!/bin/bash
87
+ # Deploy script - runs after files are uploaded
88
+ # This script runs on the remote server
89
+
90
+ echo "šŸš€ Running post-deployment tasks..."
91
+
92
+ # Example: Install dependencies
93
+ npm install
94
+
95
+ # Example: Build the project
96
+ # npm run build
97
+
98
+ # Example: Restart service
99
+ # sudo systemctl restart myapp
100
+
101
+ # Example: Clear cache
102
+ # rm -rf /tmp/app-cache/*
103
+
104
+ echo "āœ… Post-deployment tasks completed!"
105
+ `;
106
+
107
+ fs.writeFileSync(deployShPath, deployShContent);
108
+ console.log(chalk.green('āœ… Created sample deploy.sh\n'));
109
+ console.log(chalk.gray(' Edit deploy.sh to add your post-deployment commands.'));
110
+ console.log(chalk.gray(' Run with: litai-spex deploy -r\n'));
111
+ }
112
+ }
113
+
114
+ module.exports = { initCommand };
@@ -0,0 +1,165 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const { NodeSSH } = require('node-ssh');
4
+ const { loadConfig } = require('../utils/config');
5
+
6
+ /**
7
+ * Quick TCP port check for SSH (port 22)
8
+ * @param {string} ip - IP address
9
+ * @param {number} timeout - Timeout in ms
10
+ * @returns {Promise<object>} { success: boolean, duration: number }
11
+ */
12
+ function checkSSHPort(ip, timeout = 5000) {
13
+ return new Promise((resolve) => {
14
+ const net = require('net');
15
+ const socket = new net.Socket();
16
+ const startTime = Date.now();
17
+
18
+ socket.setTimeout(timeout);
19
+
20
+ socket.on('connect', () => {
21
+ const duration = Date.now() - startTime;
22
+ socket.destroy();
23
+ resolve({ success: true, duration });
24
+ });
25
+
26
+ socket.on('timeout', () => {
27
+ const duration = Date.now() - startTime;
28
+ socket.destroy();
29
+ resolve({ success: false, duration });
30
+ });
31
+
32
+ socket.on('error', (err) => {
33
+ const duration = Date.now() - startTime;
34
+ socket.destroy();
35
+ resolve({ success: false, duration });
36
+ });
37
+
38
+ socket.connect(22, ip);
39
+ });
40
+ }
41
+
42
+ /**
43
+ * Try to connect to SSH on given IP
44
+ * @param {string} ip - IP address
45
+ * @param {string} username - SSH username
46
+ * @param {string} password - SSH password (optional)
47
+ * @param {number} timeout - Connection timeout in ms
48
+ * @returns {Promise<object>} Connection result
49
+ */
50
+ async function trySSHConnection(ip, username, password, timeout = 10000) {
51
+ const ssh = new NodeSSH();
52
+
53
+ try {
54
+ const config = {
55
+ host: ip,
56
+ username: username,
57
+ readyTimeout: timeout,
58
+ tryKeyboard: false
59
+ };
60
+
61
+ if (password) {
62
+ config.password = password;
63
+ } else {
64
+ // Try with agent or default keys
65
+ config.agent = process.env.SSH_AUTH_SOCK;
66
+ }
67
+ console.log(config)
68
+ await ssh.connect(config);
69
+ ssh.dispose();
70
+ return { success: true, authenticated: true, error: null };
71
+ } catch (error) {
72
+ ssh.dispose();
73
+
74
+ // Check if it's an auth error (means SSH is running but wrong credentials)
75
+ if (error.message.includes('Authentication') ||
76
+ error.message.includes('authentication') ||
77
+ error.message.includes('All configured authentication methods failed')) {
78
+ return { success: true, authenticated: false, error: 'Authentication failed' };
79
+ }
80
+
81
+ return { success: false, authenticated: false, error: error.message };
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Ping command handler
87
+ * @param {string} host - Host IP or hostname
88
+ * @param {object} options - CLI options
89
+ */
90
+ async function pingCommand(host, options) {
91
+ const spinner = ora();
92
+
93
+ console.log(chalk.cyan('\nšŸ” LitAI-Spex Connection Tester\n'));
94
+
95
+ if (!host) {
96
+ console.log(chalk.red('āŒ No host specified.'));
97
+ console.log(chalk.gray('\nUsage:'));
98
+ console.log(chalk.gray(' litai-spex ping <host>'));
99
+ console.log(chalk.gray(' litai-spex ping <host> -u <username>'));
100
+ console.log(chalk.gray(' litai-spex ping <host> -u <username> -p <password>\n'));
101
+ process.exit(1);
102
+ }
103
+
104
+ try {
105
+ const timeout = options.timeout || 5000;
106
+
107
+ console.log(chalk.gray(`Target: ${host}`));
108
+ console.log(chalk.gray(`Timeout: ${timeout}ms\n`));
109
+
110
+ // Step 1: Check SSH port
111
+ spinner.start(`Checking SSH port (22) on ${host}...`);
112
+ const portCheck = await checkSSHPort(host, timeout);
113
+
114
+ if (!portCheck.success) {
115
+ spinner.fail(`SSH port is not accessible on ${host} (took ${portCheck.duration}ms)`);
116
+ console.log(chalk.yellow('\nšŸ’” Possible reasons:'));
117
+ console.log(chalk.gray(' • Host is down or unreachable'));
118
+ console.log(chalk.gray(' • Firewall blocking port 22'));
119
+ console.log(chalk.gray(' • SSH service not running'));
120
+ console.log(chalk.gray(' • Network timeout (try increasing with --timeout)\n'));
121
+ process.exit(1);
122
+ }
123
+
124
+ spinner.succeed(`SSH port is ${chalk.green('OPEN')} on ${host} ${chalk.gray(`(${portCheck.duration}ms)`)}`);
125
+ console.log(chalk.cyan(`\nā±ļø Connection time: ${portCheck.duration}ms`));
126
+ console.log(chalk.gray(` Recommended timeout for scan: ${Math.ceil(portCheck.duration * 1.5)}ms or higher\n`));
127
+
128
+ // Step 2: Try SSH connection if username provided
129
+ if (options.user) {
130
+ const username = options.user;
131
+ const password = options.password;
132
+
133
+ console.log(chalk.gray(`Attempting SSH authentication as ${username}...\n`));
134
+
135
+ spinner.start(`Connecting to ${username}@${host}...`);
136
+ const sshStartTime = Date.now();
137
+ const result = await trySSHConnection(host, username, password, timeout);
138
+ const sshDuration = Date.now() - sshStartTime;
139
+
140
+ if (result.authenticated) {
141
+ spinner.succeed(`SSH connection ${chalk.green('SUCCESSFUL')} - authenticated as ${username} ${chalk.gray(`(${sshDuration}ms)`)}`);
142
+ console.log(chalk.green('\nāœ… You can connect to this host!\n'));
143
+ } else if (result.success) {
144
+ spinner.warn(`SSH is running but ${chalk.yellow('authentication failed')} ${chalk.gray(`(${sshDuration}ms)`)}`);
145
+ console.log(chalk.yellow('\nāš ļø The host is accessible but credentials are incorrect.'));
146
+ console.log(chalk.gray('\nTry:'));
147
+ console.log(chalk.gray(` ssh ${username}@${host}`));
148
+ console.log(chalk.gray(' to test manually\n'));
149
+ } else {
150
+ spinner.fail(`SSH connection failed ${chalk.gray(`(${sshDuration}ms)`)}`);
151
+ console.log(chalk.red(`\nāŒ Error: ${result.error}\n`));
152
+ }
153
+ } else {
154
+ console.log(chalk.green('\nāœ… SSH port is accessible!'));
155
+ console.log(chalk.gray('\nšŸ’” To test SSH authentication, add: -u <username>\n'));
156
+ }
157
+
158
+ } catch (error) {
159
+ spinner.fail('Ping failed');
160
+ console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
161
+ process.exit(1);
162
+ }
163
+ }
164
+
165
+ module.exports = { pingCommand };
@@ -0,0 +1,313 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const { NodeSSH } = require('node-ssh');
4
+ const os = require('os');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+ const readline = require('readline');
8
+ const { loadConfig } = require('../utils/config');
9
+
10
+ /**
11
+ * Get local IP address
12
+ * @returns {string|null} Local IP address
13
+ */
14
+ function getLocalIP() {
15
+ const interfaces = os.networkInterfaces();
16
+
17
+ for (const name of Object.keys(interfaces)) {
18
+ for (const iface of interfaces[name]) {
19
+ // Skip internal and non-IPv4 addresses
20
+ if (iface.family === 'IPv4' && !iface.internal) {
21
+ return iface.address;
22
+ }
23
+ }
24
+ }
25
+
26
+ return null;
27
+ }
28
+
29
+ /**
30
+ * Generate IP mask from local IP
31
+ * @param {string} ip - Local IP address
32
+ * @returns {string} IP mask (e.g., "192.168.0")
33
+ */
34
+ function generateMask(ip) {
35
+ const parts = ip.split('.');
36
+ return `${parts[0]}.${parts[1]}.${parts[2]}`;
37
+ }
38
+
39
+ /**
40
+ * Generate all IPs in a subnet
41
+ * @param {string} mask - IP mask (e.g., "192.168.0")
42
+ * @param {string} localIP - Local IP to exclude
43
+ * @returns {string[]} Array of IP addresses
44
+ */
45
+ function generateIPRange(mask, localIP) {
46
+ const ips = [];
47
+ for (let i = 1; i < 255; i++) {
48
+ const ip = `${mask}.${i}`;
49
+ if (ip !== localIP) {
50
+ ips.push(ip);
51
+ }
52
+ }
53
+ return ips;
54
+ }
55
+
56
+ /**
57
+ * Try to connect to SSH on given IP
58
+ * @param {string} ip - IP address
59
+ * @param {string} username - SSH username
60
+ * @param {string} password - SSH password (optional)
61
+ * @param {number} timeout - Connection timeout in ms
62
+ * @returns {Promise<boolean>} True if SSH port is accessible
63
+ */
64
+ async function trySSHConnection(ip, username, password, timeout = 3000) {
65
+ const ssh = new NodeSSH();
66
+
67
+ try {
68
+ const config = {
69
+ host: ip,
70
+ username: username,
71
+ readyTimeout: timeout,
72
+ tryKeyboard: false
73
+ };
74
+
75
+ if (password) {
76
+ config.password = password;
77
+ } else {
78
+ // Try with agent or default keys
79
+ config.agent = process.env.SSH_AUTH_SOCK;
80
+ }
81
+
82
+ await ssh.connect(config);
83
+ ssh.dispose();
84
+ return { success: true, authenticated: true };
85
+ } catch (error) {
86
+ ssh.dispose();
87
+
88
+ // Check if it's an auth error (means SSH is running but wrong credentials)
89
+ if (error.message.includes('Authentication') ||
90
+ error.message.includes('authentication') ||
91
+ error.message.includes('All configured authentication methods failed')) {
92
+ return { success: true, authenticated: false };
93
+ }
94
+
95
+ return { success: false, authenticated: false };
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Quick TCP port check for SSH (port 22)
101
+ * @param {string} ip - IP address
102
+ * @param {number} timeout - Timeout in ms
103
+ * @returns {Promise<boolean>}
104
+ */
105
+ function checkSSHPort(ip, timeout = 1000) {
106
+ return new Promise((resolve) => {
107
+ const net = require('net');
108
+ const socket = new net.Socket();
109
+
110
+ socket.setTimeout(timeout);
111
+
112
+ socket.on('connect', () => {
113
+ socket.destroy();
114
+ resolve(true);
115
+ });
116
+
117
+ socket.on('timeout', () => {
118
+ socket.destroy();
119
+ resolve(false);
120
+ });
121
+
122
+ socket.on('error', () => {
123
+ socket.destroy();
124
+ resolve(false);
125
+ });
126
+
127
+ socket.connect(22, ip);
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Prompt user for input
133
+ * @param {string} question - Question to ask
134
+ * @returns {Promise<string>} User input
135
+ */
136
+ function prompt(question) {
137
+ const rl = readline.createInterface({
138
+ input: process.stdin,
139
+ output: process.stdout
140
+ });
141
+
142
+ return new Promise((resolve) => {
143
+ rl.question(question, (answer) => {
144
+ rl.close();
145
+ resolve(answer.trim().toLowerCase());
146
+ });
147
+ });
148
+ }
149
+
150
+ /**
151
+ * Save IP to config file
152
+ * @param {string} configPath - Path to config file
153
+ * @param {string} ip - IP to save
154
+ */
155
+ function saveToConfig(configPath, ip) {
156
+ const fullPath = path.resolve(process.cwd(), configPath);
157
+
158
+ let config = {};
159
+ if (fs.existsSync(fullPath)) {
160
+ const content = fs.readFileSync(fullPath, 'utf8');
161
+ config = JSON.parse(content);
162
+ }
163
+
164
+ if (!config.connection) {
165
+ config.connection = {};
166
+ }
167
+
168
+ config.connection.host = ip;
169
+
170
+ fs.writeFileSync(fullPath, JSON.stringify(config, null, 2));
171
+ }
172
+
173
+ /**
174
+ * Scan command handler
175
+ * @param {object} options - CLI options
176
+ */
177
+ async function scanCommand(options) {
178
+ const spinner = ora();
179
+
180
+ console.log(chalk.cyan('\nšŸ” LitAI-Spex Network Scanner\n'));
181
+
182
+ try {
183
+ // Load config for username/password
184
+ const configPath = options.config || 'config.json';
185
+ let config = {};
186
+
187
+ try {
188
+ config = loadConfig(configPath);
189
+ } catch (e) {
190
+ // Config might not exist, that's OK
191
+ }
192
+
193
+ // Get username from options or config
194
+ const username = options.user || config.connection?.username;
195
+ const password = options.password || config.connection?.password;
196
+
197
+ if (!username) {
198
+ console.log(chalk.red('āŒ No username specified.'));
199
+ console.log(chalk.gray('\nProvide it via:'));
200
+ console.log(chalk.gray(' • CLI: litai-spex scan -u <username>'));
201
+ console.log(chalk.gray(' • Config: Set "connection.username" in config.json\n'));
202
+ process.exit(1);
203
+ }
204
+
205
+ // Get local IP
206
+ spinner.start('Detecting local IP address...');
207
+ const localIP = getLocalIP();
208
+
209
+ if (!localIP) {
210
+ spinner.fail('Could not detect local IP address');
211
+ process.exit(1);
212
+ }
213
+ spinner.succeed(`Local IP: ${chalk.green(localIP)}`);
214
+
215
+ // Generate or use provided mask
216
+ const mask = options.mask || generateMask(localIP);
217
+ console.log(chalk.gray(` Scanning subnet: ${mask}.*\n`));
218
+
219
+ // Generate IP range
220
+ const ips = generateIPRange(mask, localIP);
221
+ const foundHosts = [];
222
+
223
+ console.log(chalk.cyan('šŸ“” Scanning for SSH hosts...\n'));
224
+
225
+ // Batch size for parallel scanning
226
+ const batchSize = options.threads || 1;
227
+ const timeout = options.timeout || 200;
228
+
229
+ // Progress tracking
230
+ let scanned = 0;
231
+ const total = ips.length;
232
+
233
+ // Scan in batches
234
+ for (let i = 0; i < ips.length; i += batchSize) {
235
+ const batch = ips.slice(i, i + batchSize);
236
+
237
+ const results = await Promise.all(
238
+ batch.map(async (ip) => {
239
+ // First do a quick port check
240
+ const portOpen = await checkSSHPort(ip, timeout);
241
+ scanned++;
242
+
243
+ // Update progress
244
+ const percent = Math.round((scanned / total) * 100);
245
+ const bar = 'ā–ˆ'.repeat(Math.floor(percent / 2)) + 'ā–‘'.repeat(50 - Math.floor(percent / 2));
246
+ process.stdout.clearLine(0);
247
+ process.stdout.cursorTo(0);
248
+ process.stdout.write(chalk.gray(` [${bar}] ${percent}% (${scanned}/${total})`));
249
+
250
+ if (portOpen) {
251
+ return { ip, portOpen: true };
252
+ }
253
+ return { ip, portOpen: false };
254
+ })
255
+ );
256
+
257
+ // Collect hosts with open SSH port
258
+ for (const result of results) {
259
+ if (result.portOpen) {
260
+ foundHosts.push(result.ip);
261
+ }
262
+ }
263
+ }
264
+
265
+ console.log('\n\n');
266
+
267
+ if (foundHosts.length === 0) {
268
+ console.log(chalk.yellow('āš ļø No SSH hosts found in the subnet.\n'));
269
+ process.exit(0);
270
+ }
271
+
272
+ console.log(chalk.green(`āœ… Found ${foundHosts.length} host(s) with SSH port open:\n`));
273
+
274
+ // Now try to authenticate to found hosts
275
+ for (const ip of foundHosts) {
276
+ spinner.start(`Testing SSH connection to ${ip}...`);
277
+
278
+ const result = await trySSHConnection(ip, username, password, 5000);
279
+
280
+ if (result.authenticated) {
281
+ spinner.succeed(`${chalk.green(ip)} - SSH accessible ${chalk.green('(authenticated)')}`);
282
+ } else if (result.success) {
283
+ spinner.succeed(`${chalk.green(ip)} - SSH accessible ${chalk.yellow('(auth required)')}`);
284
+ } else {
285
+ spinner.info(`${chalk.gray(ip)} - SSH port open but connection failed`);
286
+ continue;
287
+ }
288
+
289
+ // Ask user if they want to save this IP
290
+ const answer = await prompt(chalk.cyan(` Save ${ip} to config.json? (y/n): `));
291
+
292
+ if (answer === 'y' || answer === 'yes') {
293
+ try {
294
+ saveToConfig(configPath, ip);
295
+ console.log(chalk.green(` āœ… Saved ${ip} to ${configPath}\n`));
296
+ } catch (error) {
297
+ console.log(chalk.red(` āŒ Failed to save: ${error.message}\n`));
298
+ }
299
+ } else {
300
+ console.log(chalk.gray(' Skipped.\n'));
301
+ }
302
+ }
303
+
304
+ console.log(chalk.green('\nšŸŽ‰ Scan completed!\n'));
305
+
306
+ } catch (error) {
307
+ spinner.fail('Scan failed');
308
+ console.log(chalk.red(`\nāŒ Error: ${error.message}\n`));
309
+ process.exit(1);
310
+ }
311
+ }
312
+
313
+ module.exports = { scanCommand };
package/src/index.js ADDED
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { program } = require('commander');
4
+ const path = require('path');
5
+ const { deployCommand } = require('./commands/deploy');
6
+ const { initCommand } = require('./commands/init');
7
+ const { createProjectCommand } = require('./commands/create-project');
8
+ const { scanCommand } = require('./commands/scan');
9
+ const { pingCommand } = require('./commands/ping');
10
+ const pkg = require('../package.json');
11
+
12
+ program
13
+ .name('litai-spex')
14
+ .description('CLI tool for deploying files via SSH')
15
+ .version(pkg.version);
16
+
17
+ program
18
+ .command('deploy')
19
+ .description('Deploy files from current directory to remote server via SSH')
20
+ .option('-ip, --ip <host>', 'Target server IP/hostname')
21
+ .option('-u, --user <username>', 'SSH username')
22
+ .option('-p, --password <password>', 'SSH password')
23
+ .option('-dir, --directory <path>', 'Target directory on remote server')
24
+ .option('-k, --key <path>', 'Path to private key file (alternative to password)')
25
+ .option('-r, --run [script]', 'Run script after deployment (default: deploy.sh)')
26
+ .option('-c, --config <path>', 'Path to config file (default: config.json)')
27
+ .action(deployCommand);
28
+
29
+ program
30
+ .command('init')
31
+ .description('Initialize a config.json file with default settings')
32
+ .action(initCommand);
33
+
34
+ program
35
+ .command('create-project')
36
+ .description('Clone a git repository from URL specified in config or CLI')
37
+ .option('--repo <url>', 'Git repository URL (overrides config)')
38
+ .option('--name <n>', 'Directory name for the cloned project')
39
+ .option('-b, --branch <branch>', 'Branch to clone')
40
+ .option('--depth <depth>', 'Create a shallow clone with specified depth')
41
+ .option('-i, --install', 'Auto-install npm dependencies after clone')
42
+ .option('-c, --config <path>', 'Path to config file (default: config.json)')
43
+ .action(createProjectCommand);
44
+
45
+ program
46
+ .command('scan')
47
+ .description('Scan local network for SSH hosts')
48
+ .option('-m, --mask <mask>', 'IP mask to scan (e.g., 192.168.1)')
49
+ .option('-u, --user <username>', 'SSH username to test')
50
+ .option('-p, --password <password>', 'SSH password to test')
51
+ .option('-t, --timeout <ms>', 'Connection timeout in milliseconds (default: 1000)', parseInt)
52
+ .option('--threads <n>', 'Number of parallel scans (default: 20)', parseInt)
53
+ .option('-c, --config <path>', 'Path to config file (default: config.json)')
54
+ .action(scanCommand);
55
+
56
+ program
57
+ .command('ping <host>')
58
+ .description('Test SSH connectivity to a specific host')
59
+ .option('-u, --user <username>', 'SSH username to test authentication')
60
+ .option('-p, --password <password>', 'SSH password to test authentication')
61
+ .option('-t, --timeout <ms>', 'Connection timeout in milliseconds (default: 5000)', parseInt)
62
+ .action(pingCommand);
63
+
64
+ program.parse(process.argv);
65
+
66
+ // Show help if no command provided
67
+ if (!process.argv.slice(2).length) {
68
+ program.outputHelp();
69
+ }