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
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 };
|