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 ADDED
@@ -0,0 +1,254 @@
1
+ # LitAI-Spex
2
+
3
+ A CLI tool for deploying files via SSH with configurable exclusions and post-deployment script execution.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ # Clone or download the project
9
+ cd litai-spex
10
+
11
+ # Install dependencies
12
+ npm install
13
+
14
+ # Link globally for CLI access
15
+ npm link
16
+ ```
17
+
18
+ ## Quick Start
19
+
20
+ 1. Initialize configuration in your project directory:
21
+ ```bash
22
+ litai-spex init
23
+ ```
24
+
25
+ 2. Edit `config.json` with your server details
26
+
27
+ 3. Deploy:
28
+ ```bash
29
+ litai-spex deploy
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ### Commands
35
+
36
+ #### `litai-spex init`
37
+ Creates a `config.json` file with default settings and a sample `deploy.sh` script.
38
+
39
+ #### `litai-spex deploy [options]`
40
+ Deploys files from the current directory to a remote server.
41
+
42
+ **Options:**
43
+ | Option | Description |
44
+ |--------|-------------|
45
+ | `-ip, --ip <host>` | Target server IP/hostname |
46
+ | `-u, --user <username>` | SSH username |
47
+ | `-p, --password <password>` | SSH password |
48
+ | `-k, --key <path>` | Path to private key file |
49
+ | `-dir, --directory <path>` | Target directory on remote server |
50
+ | `-r, --run [script]` | Run script after deployment (default: deploy.sh) |
51
+ | `-c, --config <path>` | Path to config file (default: config.json) |
52
+
53
+ #### `litai-spex create-project [options]`
54
+ Clones a git repository from URL specified in config or CLI.
55
+
56
+ **Options:**
57
+ | Option | Description |
58
+ |--------|-------------|
59
+ | `--repo <url>` | Git repository URL (overrides config) |
60
+ | `--name <name>` | Directory name for the cloned project |
61
+ | `-b, --branch <branch>` | Branch to clone |
62
+ | `--depth <depth>` | Create a shallow clone with specified depth |
63
+ | `-i, --install` | Auto-install npm dependencies after clone |
64
+ | `-c, --config <path>` | Path to config file (default: config.json) |
65
+
66
+ #### `litai-spex scan [options]`
67
+ Scans local network for SSH hosts and optionally saves found hosts to config.
68
+
69
+ **Options:**
70
+ | Option | Description |
71
+ |--------|-------------|
72
+ | `-m, --mask <mask>` | IP mask to scan (e.g., 192.168.1). Auto-detected if not provided |
73
+ | `-u, --user <username>` | SSH username to test (can also be set in config) |
74
+ | `-p, --password <password>` | SSH password to test (can also be set in config) |
75
+ | `-t, --timeout <ms>` | Connection timeout in milliseconds (default: 1000) |
76
+ | `--threads <n>` | Number of parallel scans (default: 20) |
77
+ | `-c, --config <path>` | Path to config file (default: config.json) |
78
+
79
+ ### Examples
80
+
81
+ **Deploy using config file:**
82
+ ```bash
83
+ litai-spex deploy
84
+ ```
85
+
86
+ **Deploy with CLI options (override config):**
87
+ ```bash
88
+ litai-spex deploy -ip 192.168.1.100 -u admin -p mypassword -dir /var/www/myapp
89
+ ```
90
+
91
+ **Deploy using SSH key:**
92
+ ```bash
93
+ litai-spex deploy -ip 192.168.1.100 -u admin -k ~/.ssh/id_rsa
94
+ ```
95
+
96
+ **Deploy and run post-deployment script:**
97
+ ```bash
98
+ litai-spex deploy -r # Uses deploy.sh from config
99
+ litai-spex deploy -r setup.sh # Uses specific script
100
+ ```
101
+
102
+ **Clone a project from config:**
103
+ ```bash
104
+ litai-spex create-project
105
+ ```
106
+
107
+ **Clone with CLI options:**
108
+ ```bash
109
+ litai-spex create-project --repo https://github.com/user/repo.git
110
+ litai-spex create-project --repo https://github.com/user/repo.git --name my-project
111
+ litai-spex create-project --repo https://github.com/user/repo.git -b develop
112
+ litai-spex create-project --repo https://github.com/user/repo.git -i # Auto-install deps
113
+ ```
114
+
115
+ **Scan local network for SSH hosts:**
116
+ ```bash
117
+ litai-spex scan -u myuser # Auto-detect subnet, scan with username
118
+ litai-spex scan -u myuser -p mypassword # Scan with credentials
119
+ litai-spex scan -m 192.168.1 -u admin # Scan specific subnet
120
+ litai-spex scan -u admin --threads 50 # Faster scan with more threads
121
+ ```
122
+
123
+ ## Configuration
124
+
125
+ ### config.json Structure
126
+
127
+ ```json
128
+ {
129
+ "connection": {
130
+ "host": "192.168.1.100",
131
+ "username": "deploy",
132
+ "password": "your-password",
133
+ "privateKeyPath": "",
134
+ "targetDirectory": "/var/www/app"
135
+ },
136
+ "deploy": {
137
+ "excludeDirectories": [
138
+ "node_modules",
139
+ "logs",
140
+ ".git",
141
+ ".idea",
142
+ ".vscode"
143
+ ],
144
+ "excludeFiles": [
145
+ "package-lock.json",
146
+ ".env.local",
147
+ ".DS_Store",
148
+ "config.json"
149
+ ],
150
+ "excludePatterns": [
151
+ "*.log",
152
+ "*.tmp",
153
+ "*.bak"
154
+ ]
155
+ },
156
+ "scripts": {
157
+ "afterDeploy": "deploy.sh"
158
+ },
159
+ "project": {
160
+ "repositoryUrl": "https://github.com/username/repo.git"
161
+ }
162
+ }
163
+ ```
164
+
165
+ ### Configuration Options
166
+
167
+ #### Connection
168
+ | Field | Description |
169
+ |-------|-------------|
170
+ | `host` | Server IP address or hostname |
171
+ | `username` | SSH username |
172
+ | `password` | SSH password (use this OR privateKeyPath) |
173
+ | `privateKeyPath` | Path to SSH private key file |
174
+ | `targetDirectory` | Remote directory to deploy to |
175
+
176
+ #### Deploy Exclusions
177
+ | Field | Description |
178
+ |-------|-------------|
179
+ | `excludeDirectories` | Array of directory names to skip |
180
+ | `excludeFiles` | Array of file names to skip |
181
+ | `excludePatterns` | Array of glob patterns to skip (e.g., `*.log`) |
182
+
183
+ #### Project
184
+ | Field | Description |
185
+ |-------|-------------|
186
+ | `repositoryUrl` | Default git repository URL for create-project command |
187
+
188
+ ### Default Exclusions
189
+
190
+ **Directories:** `node_modules`, `logs`, `.git`, `.idea`, `.vscode`
191
+
192
+ **Files:** `package-lock.json`, `.env.local`, `.DS_Store`
193
+
194
+ **Patterns:** `*.log`, `*.tmp`
195
+
196
+ ## Post-Deployment Scripts
197
+
198
+ Create a `deploy.sh` file in your project root:
199
+
200
+ ```bash
201
+ #!/bin/bash
202
+ # deploy.sh - runs on remote server after files are uploaded
203
+
204
+ echo "Installing dependencies..."
205
+ npm install --production
206
+
207
+ echo "Building project..."
208
+ npm run build
209
+
210
+ echo "Restarting service..."
211
+ sudo systemctl restart myapp
212
+
213
+ echo "Deployment complete!"
214
+ ```
215
+
216
+ Run it with:
217
+ ```bash
218
+ litai-spex deploy -r
219
+ ```
220
+
221
+ ## Security Notes
222
+
223
+ 1. **Never commit `config.json`** - Add it to `.gitignore`
224
+ 2. **Use SSH keys** when possible instead of passwords
225
+ 3. **Secure your deploy.sh** - It runs with the connected user's permissions
226
+
227
+ ## CLI Priority
228
+
229
+ Command-line options always override config file settings:
230
+
231
+ ```bash
232
+ # Uses host from config, but overrides username
233
+ litai-spex deploy -u differentuser
234
+ ```
235
+
236
+ ## Troubleshooting
237
+
238
+ ### Connection Refused
239
+ - Check if SSH is running on the server
240
+ - Verify the IP address and port
241
+ - Check firewall settings
242
+
243
+ ### Authentication Failed
244
+ - Verify username and password
245
+ - Check SSH key permissions (`chmod 600 ~/.ssh/id_rsa`)
246
+ - Ensure the key is added to `~/.ssh/authorized_keys` on the server
247
+
248
+ ### Permission Denied
249
+ - Verify the user has write access to the target directory
250
+ - Check directory permissions on the server
251
+
252
+ ## License
253
+
254
+ MIT
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "litai-spex",
3
+ "version": "1.0.0",
4
+ "description": "CLI tool for deploying files via SSH with configurable exclusions",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "litai-spex": "./src/index.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node src/index.js",
11
+ "test": "echo \"No tests yet\" && exit 0"
12
+ },
13
+ "keywords": [
14
+ "cli",
15
+ "ssh",
16
+ "deploy",
17
+ "sftp",
18
+ "upload",
19
+ "remote",
20
+ "deployment",
21
+ "devops"
22
+ ],
23
+ "author": "Litai Tech",
24
+ "license": "MIT",
25
+ "repository": {
26
+ "type": "git",
27
+ "url": "https://github.com/litai-tech/litai.spex.cli.git"
28
+ },
29
+ "homepage": "https://github.com/litai-tech/litai.spex.cli#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/litai-tech/litai.spex.cli/issues"
32
+ },
33
+ "engines": {
34
+ "node": ">=14.0.0"
35
+ },
36
+ "files": [
37
+ "src/**/*",
38
+ "README.md"
39
+ ],
40
+ "dependencies": {
41
+ "chalk": "^4.1.2",
42
+ "commander": "^11.1.0",
43
+ "node-ssh": "^13.2.0",
44
+ "ora": "^5.4.1"
45
+ }
46
+ }
@@ -0,0 +1,155 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const { execSync } = require('child_process');
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+ const { loadConfig } = require('../utils/config');
7
+
8
+ /**
9
+ * Create project command handler - clones a git repo from config
10
+ * @param {object} options - CLI options
11
+ */
12
+ async function createProjectCommand(options) {
13
+ const spinner = ora();
14
+
15
+ console.log(chalk.cyan('\n📦 LitAI-Spex Create Project\n'));
16
+
17
+ try {
18
+ // Load configuration
19
+ spinner.start('Loading configuration...');
20
+ const configPath = options.config || 'config.json';
21
+ const config = loadConfig(configPath);
22
+ spinner.succeed('Configuration loaded');
23
+
24
+ // Get repo URL from config or CLI
25
+ const repoUrl = options.repo || config.project?.repositoryUrl;
26
+
27
+ if (!repoUrl) {
28
+ console.log(chalk.red('\n❌ No repository URL specified.'));
29
+ console.log(chalk.gray('\nProvide it via:'));
30
+ console.log(chalk.gray(' • CLI: litai-spex create-project --repo <url>'));
31
+ console.log(chalk.gray(' • Config: Add "project.repositoryUrl" to config.json\n'));
32
+ console.log(chalk.white(' Example config.json:'));
33
+ console.log(chalk.gray(' {'));
34
+ console.log(chalk.gray(' "project": {'));
35
+ console.log(chalk.cyan(' "repositoryUrl": "https://github.com/user/repo.git"'));
36
+ console.log(chalk.gray(' }'));
37
+ console.log(chalk.gray(' }\n'));
38
+ process.exit(1);
39
+ }
40
+
41
+ // Determine target directory
42
+ const targetDir = options.dir || config.project?.targetDirectory || '.';
43
+ const fullTargetPath = path.resolve(process.cwd(), targetDir);
44
+
45
+ // Extract repo name for folder name if cloning to current dir
46
+ let cloneDir = fullTargetPath;
47
+ if (targetDir === '.' && !options.name) {
48
+ // Will clone into repo name folder by default
49
+ } else if (options.name) {
50
+ cloneDir = path.resolve(process.cwd(), options.name);
51
+ }
52
+
53
+ // Check if git is installed
54
+ try {
55
+ execSync('git --version', { stdio: 'pipe' });
56
+ } catch (e) {
57
+ console.log(chalk.red('\n❌ Git is not installed or not in PATH.'));
58
+ console.log(chalk.gray(' Please install Git: https://git-scm.com/downloads\n'));
59
+ process.exit(1);
60
+ }
61
+
62
+ // Display clone info
63
+ console.log(chalk.gray('\n📋 Clone Details:'));
64
+ console.log(chalk.gray(` Repository: ${repoUrl}`));
65
+ console.log(chalk.gray(` Target: ${cloneDir === fullTargetPath ? 'Current directory' : cloneDir}`));
66
+
67
+ if (options.branch) {
68
+ console.log(chalk.gray(` Branch: ${options.branch}`));
69
+ }
70
+ console.log('');
71
+
72
+ // Build git clone command
73
+ let gitCommand = 'git clone';
74
+
75
+ if (options.branch) {
76
+ gitCommand += ` -b ${options.branch}`;
77
+ }
78
+
79
+ if (options.depth) {
80
+ gitCommand += ` --depth ${options.depth}`;
81
+ }
82
+
83
+ gitCommand += ` "${repoUrl}"`;
84
+
85
+ if (options.name) {
86
+ gitCommand += ` "${options.name}"`;
87
+ }
88
+
89
+ // Clone the repository
90
+ spinner.start('Cloning repository...');
91
+
92
+ try {
93
+ execSync(gitCommand, {
94
+ cwd: process.cwd(),
95
+ stdio: 'pipe'
96
+ });
97
+ spinner.succeed('Repository cloned successfully');
98
+ } catch (error) {
99
+ spinner.fail('Clone failed');
100
+ const errorMessage = error.stderr?.toString() || error.message;
101
+ console.log(chalk.red(`\n ${errorMessage}`));
102
+ process.exit(1);
103
+ }
104
+
105
+ // Determine the cloned directory name
106
+ let clonedDirName = options.name;
107
+ if (!clonedDirName) {
108
+ // Extract from URL
109
+ clonedDirName = repoUrl
110
+ .split('/')
111
+ .pop()
112
+ .replace(/\.git$/, '');
113
+ }
114
+
115
+ const clonedPath = path.resolve(process.cwd(), clonedDirName);
116
+
117
+ // Check if package.json exists and offer to install dependencies
118
+ const packageJsonPath = path.join(clonedPath, 'package.json');
119
+ if (fs.existsSync(packageJsonPath) && options.install !== false) {
120
+ if (options.install) {
121
+ spinner.start('Installing dependencies...');
122
+ try {
123
+ execSync('npm install', {
124
+ cwd: clonedPath,
125
+ stdio: 'pipe'
126
+ });
127
+ spinner.succeed('Dependencies installed');
128
+ } catch (error) {
129
+ spinner.warn('Failed to install dependencies');
130
+ console.log(chalk.yellow(` Run 'cd ${clonedDirName} && npm install' manually`));
131
+ }
132
+ } else {
133
+ console.log(chalk.gray(`\n💡 Tip: Run 'cd ${clonedDirName} && npm install' to install dependencies`));
134
+ }
135
+ }
136
+
137
+ console.log(chalk.green(`\n✅ Project created successfully!`));
138
+ console.log(chalk.gray(` Location: ${clonedPath}\n`));
139
+
140
+ // Show next steps
141
+ console.log(chalk.cyan('📝 Next steps:'));
142
+ console.log(chalk.white(` cd ${clonedDirName}`));
143
+ if (fs.existsSync(packageJsonPath) && !options.install) {
144
+ console.log(chalk.white(' npm install'));
145
+ }
146
+ console.log('');
147
+
148
+ } catch (error) {
149
+ spinner.fail('Operation failed');
150
+ console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
151
+ process.exit(1);
152
+ }
153
+ }
154
+
155
+ module.exports = { createProjectCommand };
@@ -0,0 +1,155 @@
1
+ const chalk = require('chalk');
2
+ const ora = require('ora');
3
+ const path = require('path');
4
+ const { loadConfig, mergeWithCliOptions, validateConfig } = require('../utils/config');
5
+ const { scanDirectory, getRemoteDirectories } = require('../utils/fileScanner');
6
+ const { SSHDeployer } = require('../utils/sshDeployer');
7
+
8
+ /**
9
+ * Deploy command handler
10
+ * @param {object} options - CLI options
11
+ */
12
+ async function deployCommand(options) {
13
+ const spinner = ora();
14
+ const deployer = new SSHDeployer();
15
+
16
+ console.log(chalk.cyan('\n🚀 LitAI-Spex Deploy\n'));
17
+
18
+ try {
19
+ // Load and merge configuration
20
+ spinner.start('Loading configuration...');
21
+ const configPath = options.config || 'config.json';
22
+ let config = loadConfig(configPath);
23
+ config = mergeWithCliOptions(config, options);
24
+ spinner.succeed('Configuration loaded');
25
+
26
+ // Validate configuration
27
+ const validation = validateConfig(config);
28
+ if (!validation.isValid) {
29
+ console.log(chalk.red('\n❌ Missing required configuration:'));
30
+ validation.missing.forEach(field => {
31
+ console.log(chalk.yellow(` • ${field}`));
32
+ });
33
+ console.log(chalk.gray('\nRun `litai-spex init` to create a config file or provide options via CLI.\n'));
34
+ process.exit(1);
35
+ }
36
+
37
+ // Display connection info
38
+ console.log(chalk.gray('\n📡 Connection Details:'));
39
+ console.log(chalk.gray(` Host: ${config.connection.host}`));
40
+ console.log(chalk.gray(` User: ${config.connection.username}`));
41
+ console.log(chalk.gray(` Target: ${config.connection.targetDirectory}`));
42
+ console.log(chalk.gray(` Auth: ${config.connection.privateKeyPath ? 'Private Key' : 'Password'}\n`));
43
+
44
+ // Scan local files
45
+ spinner.start('Scanning local files...');
46
+ const sourceDir = process.cwd();
47
+ const files = scanDirectory(sourceDir, config.deploy);
48
+ const directories = getRemoteDirectories(files);
49
+ spinner.succeed(`Found ${chalk.green(files.length)} files to deploy`);
50
+
51
+ if (files.length === 0) {
52
+ console.log(chalk.yellow('\n⚠️ No files to deploy. Check your exclude settings.\n'));
53
+ process.exit(0);
54
+ }
55
+
56
+ // Display exclusions
57
+ console.log(chalk.gray('\n📋 Exclusions:'));
58
+ console.log(chalk.gray(` Directories: ${config.deploy.excludeDirectories.join(', ')}`));
59
+ console.log(chalk.gray(` Files: ${config.deploy.excludeFiles.join(', ')}`));
60
+ console.log(chalk.gray(` Patterns: ${config.deploy.excludePatterns.join(', ')}\n`));
61
+
62
+ // Connect to server
63
+ spinner.start(`Connecting to ${config.connection.host}...`);
64
+ await deployer.connect(config.connection);
65
+ spinner.succeed('Connected to server');
66
+
67
+ // Get server info
68
+ try {
69
+ const serverInfo = await deployer.getServerInfo();
70
+ console.log(chalk.gray(` Server: ${serverInfo.hostname} (${serverInfo.os})`));
71
+ console.log(chalk.gray(` Uptime: ${serverInfo.uptime}\n`));
72
+ } catch (e) {
73
+ // Server info is optional, don't fail if it doesn't work
74
+ }
75
+
76
+ // Create directories
77
+ if (directories.length > 0) {
78
+ spinner.start(`Creating ${directories.length} directories...`);
79
+ await deployer.createDirectories(config.connection.targetDirectory, directories);
80
+ spinner.succeed(`Created ${directories.length} directories`);
81
+ }
82
+
83
+ // Upload files
84
+ console.log(chalk.cyan('\n📤 Uploading files...\n'));
85
+
86
+ let lastPercent = -1;
87
+ await deployer.uploadFiles(
88
+ config.connection.targetDirectory,
89
+ files,
90
+ (current, total, filename) => {
91
+ const percent = Math.round((current / total) * 100);
92
+ if (percent !== lastPercent) {
93
+ lastPercent = percent;
94
+ process.stdout.clearLine(0);
95
+ process.stdout.cursorTo(0);
96
+ const bar = '█'.repeat(Math.floor(percent / 2)) + '░'.repeat(50 - Math.floor(percent / 2));
97
+ process.stdout.write(chalk.cyan(` [${bar}] ${percent}% - ${filename}`));
98
+ }
99
+ }
100
+ );
101
+
102
+ console.log('\n');
103
+ console.log(chalk.green(`✅ Successfully uploaded ${files.length} files\n`));
104
+
105
+ // Execute script if requested
106
+ if (options.run !== undefined) {
107
+ const scriptName = typeof options.run === 'string' ? options.run : config.scripts.afterDeploy;
108
+
109
+ console.log(chalk.cyan(`\n🔧 Executing script: ${scriptName}\n`));
110
+ spinner.start('Running script...');
111
+
112
+ try {
113
+ const result = await deployer.executeScript(config.connection.targetDirectory, scriptName);
114
+ spinner.succeed('Script executed');
115
+
116
+ if (result.stdout) {
117
+ console.log(chalk.gray('\n📋 Script output:'));
118
+ console.log(chalk.white(result.stdout));
119
+ }
120
+
121
+ if (result.stderr) {
122
+ console.log(chalk.yellow('\n⚠️ Script stderr:'));
123
+ console.log(chalk.yellow(result.stderr));
124
+ }
125
+
126
+ if (result.code !== 0) {
127
+ console.log(chalk.yellow(`\n⚠️ Script exited with code: ${result.code}`));
128
+ }
129
+ } catch (scriptError) {
130
+ spinner.fail('Script execution failed');
131
+ console.log(chalk.red(` ${scriptError.message}`));
132
+ }
133
+ }
134
+
135
+ // Disconnect
136
+ deployer.disconnect();
137
+
138
+ console.log(chalk.green('\n🎉 Deployment completed successfully!\n'));
139
+
140
+ } catch (error) {
141
+ spinner.fail('Deployment failed');
142
+ console.log(chalk.red(`\n❌ Error: ${error.message}\n`));
143
+
144
+ if (error.message.includes('ECONNREFUSED')) {
145
+ console.log(chalk.yellow(' Hint: Check if the server is running and accessible.'));
146
+ } else if (error.message.includes('Authentication')) {
147
+ console.log(chalk.yellow(' Hint: Check your username and password/key.'));
148
+ }
149
+
150
+ deployer.disconnect();
151
+ process.exit(1);
152
+ }
153
+ }
154
+
155
+ module.exports = { deployCommand };