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.
- package/README.md +254 -0
- package/package.json +46 -0
- package/src/commands/create-project.js +155 -0
- package/src/commands/deploy.js +155 -0
- package/src/commands/init.js +114 -0
- package/src/commands/ping.js +165 -0
- package/src/commands/scan.js +313 -0
- package/src/index.js +69 -0
- package/src/utils/config.js +115 -0
- package/src/utils/fileScanner.js +101 -0
- package/src/utils/sshDeployer.js +138 -0
|
@@ -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
|
+
}
|