remote-deploy-cli 1.1.2 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +9 -0
- package/.prettierrc +7 -0
- package/README.md +50 -11
- package/bin/index.js +193 -70
- package/docker-compose.server.yml +2 -2
- package/lib/client/client.js +6 -4
- package/lib/client/index.js +22 -4
- package/lib/config.js +1 -1
- package/lib/logger.js +1 -1
- package/lib/server/executor.js +2 -2
- package/lib/server/index.js +2 -2
- package/lib/server/server.js +29 -1
- package/package.json +16 -3
- package/scripts/update.js +0 -1
- package/sonar-project.js +22 -0
- package/test-target/docker-compose.yml +0 -0
package/.prettierignore
ADDED
package/.prettierrc
ADDED
package/README.md
CHANGED
|
@@ -66,24 +66,34 @@ These commands are executed on your **local machine** or **CI/CD runner**.
|
|
|
66
66
|
Triggers a deployment on the remote server.
|
|
67
67
|
|
|
68
68
|
**Syntax:**
|
|
69
|
+
|
|
69
70
|
```bash
|
|
70
71
|
redep deploy <type>
|
|
71
72
|
```
|
|
72
73
|
|
|
73
74
|
**Parameters:**
|
|
74
|
-
|
|
75
|
+
|
|
76
|
+
- `<type>`: The service type to deploy.
|
|
77
|
+
- `fe`: Frontend (pre-configured to run `docker compose pull && docker compose up -d`).
|
|
78
|
+
- `custom`: Custom command (configured on server via `deployment_command`).
|
|
75
79
|
|
|
76
80
|
**Requirements:**
|
|
81
|
+
|
|
77
82
|
- `SERVER_URL` must be configured.
|
|
78
83
|
- `SECRET_KEY` must match the server's key.
|
|
79
84
|
|
|
80
85
|
**Example:**
|
|
86
|
+
|
|
81
87
|
```bash
|
|
82
88
|
# Deploy frontend service
|
|
83
89
|
redep deploy fe
|
|
90
|
+
|
|
91
|
+
# Deploy custom command
|
|
92
|
+
redep deploy custom
|
|
84
93
|
```
|
|
85
94
|
|
|
86
95
|
**Expected Output:**
|
|
96
|
+
|
|
87
97
|
```
|
|
88
98
|
[INFO] Deploying fe to http://192.168.1.50:3000...
|
|
89
99
|
[SUCCESS] Deployment triggered successfully.
|
|
@@ -112,14 +122,17 @@ These commands are executed on the **remote server** (VPS, VM, etc.).
|
|
|
112
122
|
Starts the server in the foreground. Useful for debugging or running inside Docker.
|
|
113
123
|
|
|
114
124
|
**Syntax:**
|
|
125
|
+
|
|
115
126
|
```bash
|
|
116
127
|
redep listen [--port <number>]
|
|
117
128
|
```
|
|
118
129
|
|
|
119
130
|
**Options:**
|
|
131
|
+
|
|
120
132
|
- `-p, --port`: Specify port (default: 3000).
|
|
121
133
|
|
|
122
134
|
**Example:**
|
|
135
|
+
|
|
123
136
|
```bash
|
|
124
137
|
redep listen --port 4000
|
|
125
138
|
```
|
|
@@ -127,19 +140,23 @@ redep listen --port 4000
|
|
|
127
140
|
### `start` (Background)
|
|
128
141
|
|
|
129
142
|
Starts the server in background mode (Daemon).
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
|
|
144
|
+
- **Auto-PM2**: If `pm2` is installed, it uses PM2 for process management.
|
|
145
|
+
- **Native Fallback**: If `pm2` is missing, it uses Node.js `child_process` to detach.
|
|
132
146
|
|
|
133
147
|
**Syntax:**
|
|
148
|
+
|
|
134
149
|
```bash
|
|
135
150
|
redep start [--port <number>]
|
|
136
151
|
```
|
|
137
152
|
|
|
138
153
|
**Related Commands:**
|
|
154
|
+
|
|
139
155
|
- `redep stop`: Stops the background server.
|
|
140
156
|
- `redep status`: Checks if the server is running.
|
|
141
157
|
|
|
142
158
|
**Example:**
|
|
159
|
+
|
|
143
160
|
```bash
|
|
144
161
|
redep start
|
|
145
162
|
# [SUCCESS] Server started in background using PM2
|
|
@@ -155,6 +172,9 @@ redep config set working_dir /path/to/project
|
|
|
155
172
|
|
|
156
173
|
# Set Secret Key
|
|
157
174
|
redep config set secret_key my-secret-key
|
|
175
|
+
|
|
176
|
+
# Set Custom Deployment Command (Optional)
|
|
177
|
+
redep config set deployment_command "git pull && npm install && pm2 restart app"
|
|
158
178
|
```
|
|
159
179
|
|
|
160
180
|
#### Running with Docker (Recommended)
|
|
@@ -172,10 +192,25 @@ services:
|
|
|
172
192
|
environment:
|
|
173
193
|
- WORKING_DIR=/workspace
|
|
174
194
|
- SECRET_KEY=secure-key
|
|
195
|
+
- DEPLOYMENT_COMMAND=docker compose restart app
|
|
175
196
|
```
|
|
176
197
|
|
|
177
198
|
---
|
|
178
199
|
|
|
200
|
+
### `generate`
|
|
201
|
+
|
|
202
|
+
Helper to generate configuration values.
|
|
203
|
+
|
|
204
|
+
- `generate secret_key`: Generates a secure 32-character secret key and saves it to config.
|
|
205
|
+
- `generate working_dir`: Sets the current directory as the `working_dir`.
|
|
206
|
+
|
|
207
|
+
### `init`
|
|
208
|
+
|
|
209
|
+
Interactive initialization for configuration.
|
|
210
|
+
|
|
211
|
+
- `init client`: Prompts for `server_url` and `secret_key`.
|
|
212
|
+
- `init server`: Prompts for `server_port`, `working_dir`, `deployment_command`, and `secret_key` (auto-generates if empty).
|
|
213
|
+
|
|
179
214
|
## Command Interactions
|
|
180
215
|
|
|
181
216
|
### Workflow: Deployment
|
|
@@ -184,11 +219,11 @@ services:
|
|
|
184
219
|
sequenceDiagram
|
|
185
220
|
participant Client (CI/Local)
|
|
186
221
|
participant Server (Remote)
|
|
187
|
-
|
|
222
|
+
|
|
188
223
|
Note over Client: User runs "redep deploy fe"
|
|
189
224
|
Client->>Client: Read Config (URL, Secret)
|
|
190
225
|
Client->>Server: POST /deploy { type: "fe" } (Auth: Bearer Token)
|
|
191
|
-
|
|
226
|
+
|
|
192
227
|
Note over Server: Verify Secret Key
|
|
193
228
|
alt Invalid Key
|
|
194
229
|
Server-->>Client: 403 Forbidden
|
|
@@ -204,26 +239,30 @@ sequenceDiagram
|
|
|
204
239
|
|
|
205
240
|
## Configuration Reference
|
|
206
241
|
|
|
207
|
-
| Config Key
|
|
208
|
-
|
|
|
209
|
-
| `server_port`
|
|
210
|
-
| `working_dir`
|
|
211
|
-
| `
|
|
212
|
-
| `
|
|
242
|
+
| Config Key | Env Variable | Description | Context |
|
|
243
|
+
| :------------------- | :------------------- | :----------------------------------------------- | :--------- |
|
|
244
|
+
| `server_port` | `SERVER_PORT` | Port for the server to listen on (Default: 3000) | **Server** |
|
|
245
|
+
| `working_dir` | `WORKING_DIR` | Directory to execute commands in | **Server** |
|
|
246
|
+
| `deployment_command` | `DEPLOYMENT_COMMAND` | Custom command for `deploy custom` | **Server** |
|
|
247
|
+
| `server_url` | `SERVER_URL` | URL of the remote `redep` server | **Client** |
|
|
248
|
+
| `secret_key` | `SECRET_KEY` | Shared secret for authentication | **Both** |
|
|
213
249
|
|
|
214
250
|
---
|
|
215
251
|
|
|
216
252
|
## Troubleshooting
|
|
217
253
|
|
|
218
254
|
### `Error: "working_dir" is not set`
|
|
255
|
+
|
|
219
256
|
- **Context**: Server
|
|
220
257
|
- **Fix**: Run `redep config set working_dir /path` or check `docker-compose.yml` environment.
|
|
221
258
|
|
|
222
259
|
### `Connection Refused`
|
|
260
|
+
|
|
223
261
|
- **Context**: Client
|
|
224
262
|
- **Fix**: Ensure server is running (`redep status` or `docker ps`) and port 3000 is open.
|
|
225
263
|
|
|
226
264
|
### `403 Forbidden`
|
|
265
|
+
|
|
227
266
|
- **Context**: Client
|
|
228
267
|
- **Fix**: Re-check `SECRET_KEY` on both machines. They must match exactly.
|
|
229
268
|
|
package/bin/index.js
CHANGED
|
@@ -3,21 +3,138 @@
|
|
|
3
3
|
import 'dotenv/config';
|
|
4
4
|
import { Command } from 'commander';
|
|
5
5
|
import { spawn } from 'child_process';
|
|
6
|
+
import crypto from 'crypto';
|
|
7
|
+
import inquirer from 'inquirer';
|
|
6
8
|
import { logger } from '../lib/logger.js';
|
|
7
9
|
import { getConfig, setConfig, getAllConfig, clearConfig } from '../lib/config.js';
|
|
8
10
|
import { startServer } from '../lib/server/index.js';
|
|
9
11
|
import { deploy } from '../lib/client/index.js';
|
|
12
|
+
import pkg from '../package.json' assert { type: 'json' };
|
|
10
13
|
|
|
11
14
|
const program = new Command();
|
|
12
15
|
|
|
16
|
+
program.name('redep').description(pkg.description).version(pkg.version);
|
|
17
|
+
|
|
18
|
+
// Helper to generate secure token
|
|
19
|
+
const generateSecureToken = (length = 32) => {
|
|
20
|
+
return crypto
|
|
21
|
+
.randomBytes(Math.ceil(length * 0.75))
|
|
22
|
+
.toString('base64')
|
|
23
|
+
.slice(0, length)
|
|
24
|
+
.replace(/\+/g, '-')
|
|
25
|
+
.replace(/\//g, '_');
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
// Init Command
|
|
13
29
|
program
|
|
14
|
-
.
|
|
15
|
-
.description('
|
|
16
|
-
.
|
|
30
|
+
.command('init <type>')
|
|
31
|
+
.description('Initialize configuration for client or server')
|
|
32
|
+
.action(async (type) => {
|
|
33
|
+
if (type !== 'client' && type !== 'server') {
|
|
34
|
+
logger.error('Type must be either "client" or "server"');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
try {
|
|
39
|
+
if (type === 'client') {
|
|
40
|
+
const answers = await inquirer.prompt([
|
|
41
|
+
{
|
|
42
|
+
type: 'input',
|
|
43
|
+
name: 'server_url',
|
|
44
|
+
message: 'Enter Server URL:',
|
|
45
|
+
validate: (input) => (input ? true : 'Server URL is required'),
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
type: 'input',
|
|
49
|
+
name: 'secret_key',
|
|
50
|
+
message: 'Enter Secret Key:',
|
|
51
|
+
validate: (input) => (input ? true : 'Secret Key is required'),
|
|
52
|
+
},
|
|
53
|
+
]);
|
|
54
|
+
|
|
55
|
+
setConfig('server_url', answers.server_url);
|
|
56
|
+
setConfig('secret_key', answers.secret_key);
|
|
57
|
+
logger.success('Client configuration saved successfully.');
|
|
58
|
+
} else {
|
|
59
|
+
const answers = await inquirer.prompt([
|
|
60
|
+
{
|
|
61
|
+
type: 'input',
|
|
62
|
+
name: 'server_port',
|
|
63
|
+
message: 'Enter Server Port:',
|
|
64
|
+
default: '3000',
|
|
65
|
+
validate: (input) => (!isNaN(input) ? true : 'Port must be a number'),
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
type: 'input',
|
|
69
|
+
name: 'working_dir',
|
|
70
|
+
message: 'Enter Working Directory:',
|
|
71
|
+
validate: (input) => (input ? true : 'Working Directory is required'),
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
type: 'input',
|
|
75
|
+
name: 'deployment_command',
|
|
76
|
+
message: 'Enter Custom Deployment Command (Optional):',
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
type: 'input',
|
|
80
|
+
name: 'secret_key',
|
|
81
|
+
message: 'Enter Secret Key (Leave empty to generate):',
|
|
82
|
+
},
|
|
83
|
+
]);
|
|
84
|
+
|
|
85
|
+
let secret = answers.secret_key;
|
|
86
|
+
if (!secret) {
|
|
87
|
+
secret = generateSecureToken();
|
|
88
|
+
logger.info(`Generated Secret Key: ${secret}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
setConfig('server_port', answers.server_port);
|
|
92
|
+
setConfig('working_dir', answers.working_dir);
|
|
93
|
+
if (answers.deployment_command) {
|
|
94
|
+
setConfig('deployment_command', answers.deployment_command);
|
|
95
|
+
}
|
|
96
|
+
setConfig('secret_key', secret);
|
|
97
|
+
logger.success('Server configuration saved successfully.');
|
|
98
|
+
}
|
|
99
|
+
} catch (error) {
|
|
100
|
+
logger.error(`Initialization failed: ${error.message}`);
|
|
101
|
+
process.exit(1);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// Generate Command
|
|
106
|
+
const generateCommand = new Command('generate').description('Generate configuration values');
|
|
107
|
+
|
|
108
|
+
generateCommand
|
|
109
|
+
.command('secret_key')
|
|
110
|
+
.description('Generate a new secret key')
|
|
111
|
+
.action(() => {
|
|
112
|
+
try {
|
|
113
|
+
const secret = generateSecureToken();
|
|
114
|
+
setConfig('secret_key', secret);
|
|
115
|
+
logger.success(`Secret Key generated and saved: ${secret}`);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
logger.error(`Generation failed: ${error.message}`);
|
|
118
|
+
}
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
generateCommand
|
|
122
|
+
.command('working_dir')
|
|
123
|
+
.description('Set working directory to current path')
|
|
124
|
+
.action(() => {
|
|
125
|
+
try {
|
|
126
|
+
const cwd = process.cwd();
|
|
127
|
+
setConfig('working_dir', cwd);
|
|
128
|
+
logger.success(`Working Directory set to: ${cwd}`);
|
|
129
|
+
} catch (error) {
|
|
130
|
+
logger.error(`Failed to set working directory: ${error.message}`);
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
program.addCommand(generateCommand);
|
|
17
135
|
|
|
18
136
|
// Configuration Command
|
|
19
|
-
const configCommand = new Command('config')
|
|
20
|
-
.description('Manage configuration');
|
|
137
|
+
const configCommand = new Command('config').description('Manage configuration');
|
|
21
138
|
|
|
22
139
|
configCommand
|
|
23
140
|
.command('set <key> <value>')
|
|
@@ -65,20 +182,19 @@ program
|
|
|
65
182
|
// Check if PM2 is available via API
|
|
66
183
|
// We'll use a dynamic import or checking for the pm2 binary in a real scenario
|
|
67
184
|
// But here we can just try to spawn 'pm2' command
|
|
68
|
-
|
|
185
|
+
|
|
69
186
|
// Use dedicated server entry point for PM2 to avoid CLI/ESM issues
|
|
70
187
|
// Resolve absolute path to server-entry.js
|
|
71
|
-
const scriptPath = new URL('../server-entry.js', import.meta.url).pathname.replace(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
188
|
+
const scriptPath = new URL('../server-entry.js', import.meta.url).pathname.replace(
|
|
189
|
+
/^\/([A-Za-z]:)/,
|
|
190
|
+
'$1'
|
|
191
|
+
);
|
|
192
|
+
|
|
193
|
+
const args = ['start', scriptPath, '--name', 'redep-server'];
|
|
194
|
+
|
|
79
195
|
// We don't pass 'listen' arg because server-entry.js starts immediately
|
|
80
196
|
// But we do need to ensure env vars are passed if port is customized
|
|
81
|
-
|
|
197
|
+
|
|
82
198
|
const env = { ...process.env };
|
|
83
199
|
if (options.port) {
|
|
84
200
|
env.SERVER_PORT = options.port;
|
|
@@ -87,7 +203,7 @@ program
|
|
|
87
203
|
const pm2 = spawn('pm2', args, {
|
|
88
204
|
stdio: 'inherit',
|
|
89
205
|
shell: true,
|
|
90
|
-
env: env // Pass modified env with port
|
|
206
|
+
env: env, // Pass modified env with port
|
|
91
207
|
});
|
|
92
208
|
|
|
93
209
|
pm2.on('error', () => {
|
|
@@ -98,46 +214,45 @@ program
|
|
|
98
214
|
|
|
99
215
|
pm2.on('close', (code) => {
|
|
100
216
|
if (code !== 0) {
|
|
101
|
-
|
|
102
|
-
|
|
217
|
+
logger.warn('PM2 start failed, falling back to native background process...');
|
|
218
|
+
startNativeBackground(options);
|
|
103
219
|
} else {
|
|
104
|
-
|
|
220
|
+
logger.success('Server started in background using PM2');
|
|
105
221
|
}
|
|
106
222
|
});
|
|
107
|
-
|
|
108
223
|
} catch (e) {
|
|
109
224
|
startNativeBackground(options);
|
|
110
225
|
}
|
|
111
226
|
});
|
|
112
227
|
|
|
113
228
|
function startNativeBackground(options) {
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (existingPid) {
|
|
117
|
-
try {
|
|
118
|
-
process.kill(existingPid, 0);
|
|
119
|
-
logger.warn(`Server is already running with PID ${existingPid}`);
|
|
120
|
-
return;
|
|
121
|
-
} catch (e) {
|
|
122
|
-
// Process doesn't exist, clear stale PID
|
|
123
|
-
setConfig('server_pid', null);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
229
|
+
const existingPid = getConfig('server_pid');
|
|
126
230
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
231
|
+
if (existingPid) {
|
|
232
|
+
try {
|
|
233
|
+
process.kill(existingPid, 0);
|
|
234
|
+
logger.warn(`Server is already running with PID ${existingPid}`);
|
|
235
|
+
return;
|
|
236
|
+
} catch (e) {
|
|
237
|
+
// Process doesn't exist, clear stale PID
|
|
238
|
+
setConfig('server_pid', null);
|
|
130
239
|
}
|
|
240
|
+
}
|
|
131
241
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
242
|
+
const args = ['listen'];
|
|
243
|
+
if (options.port) {
|
|
244
|
+
args.push('--port', options.port);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const child = spawn(process.argv[0], [process.argv[1], ...args], {
|
|
248
|
+
detached: true,
|
|
249
|
+
stdio: 'ignore',
|
|
250
|
+
windowsHide: true,
|
|
251
|
+
});
|
|
137
252
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
253
|
+
child.unref();
|
|
254
|
+
setConfig('server_pid', child.pid);
|
|
255
|
+
logger.success(`Server started in background (native) with PID ${child.pid}`);
|
|
141
256
|
}
|
|
142
257
|
|
|
143
258
|
program
|
|
@@ -146,13 +261,13 @@ program
|
|
|
146
261
|
.action(() => {
|
|
147
262
|
// Try PM2 stop first
|
|
148
263
|
const pm2 = spawn('pm2', ['stop', 'redep-server'], { stdio: 'ignore', shell: true });
|
|
149
|
-
|
|
264
|
+
|
|
150
265
|
pm2.on('close', (code) => {
|
|
151
266
|
if (code === 0) {
|
|
152
267
|
logger.success('Server stopped (PM2)');
|
|
153
268
|
return;
|
|
154
269
|
}
|
|
155
|
-
|
|
270
|
+
|
|
156
271
|
// Fallback to native stop
|
|
157
272
|
const pid = getConfig('server_pid');
|
|
158
273
|
if (!pid) {
|
|
@@ -179,28 +294,28 @@ program
|
|
|
179
294
|
.command('status')
|
|
180
295
|
.description('Check server status')
|
|
181
296
|
.action(() => {
|
|
182
|
-
|
|
183
|
-
|
|
297
|
+
// Try PM2 status first
|
|
298
|
+
const pm2 = spawn('pm2', ['describe', 'redep-server'], { stdio: 'inherit', shell: true });
|
|
184
299
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
}
|
|
300
|
+
pm2.on('close', (code) => {
|
|
301
|
+
if (code !== 0) {
|
|
302
|
+
// Fallback to native status
|
|
303
|
+
const pid = getConfig('server_pid');
|
|
304
|
+
|
|
305
|
+
if (!pid) {
|
|
306
|
+
logger.info('Server is NOT running.');
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
process.kill(pid, 0);
|
|
312
|
+
logger.success(`Server is RUNNING (PID ${pid})`);
|
|
313
|
+
} catch (e) {
|
|
314
|
+
logger.warn(`Server is NOT running (Stale PID ${pid} found).`);
|
|
315
|
+
setConfig('server_pid', null);
|
|
202
316
|
}
|
|
203
|
-
|
|
317
|
+
}
|
|
318
|
+
});
|
|
204
319
|
});
|
|
205
320
|
|
|
206
321
|
// Server Command
|
|
@@ -211,15 +326,19 @@ program
|
|
|
211
326
|
.action((options) => {
|
|
212
327
|
const port = options.port || getConfig('server_port') || process.env.SERVER_PORT || 3000;
|
|
213
328
|
const secret = getConfig('secret_key') || process.env.SECRET_KEY;
|
|
214
|
-
|
|
329
|
+
|
|
215
330
|
if (!secret) {
|
|
216
|
-
logger.warn(
|
|
331
|
+
logger.warn(
|
|
332
|
+
'Warning: No "secret_key" set in config or SECRET_KEY env var. Communication might be insecure or fail if client requires it.'
|
|
333
|
+
);
|
|
217
334
|
logger.info('Run "redep config set secret_key <your-secret>" or set SECRET_KEY env var.');
|
|
218
335
|
}
|
|
219
336
|
|
|
220
337
|
const workingDir = getConfig('working_dir') || process.env.WORKING_DIR;
|
|
221
338
|
if (!workingDir) {
|
|
222
|
-
logger.error(
|
|
339
|
+
logger.error(
|
|
340
|
+
'Error: "working_dir" is not set. Please set it using "redep config set working_dir <path>" or WORKING_DIR env var.'
|
|
341
|
+
);
|
|
223
342
|
process.exit(1);
|
|
224
343
|
}
|
|
225
344
|
|
|
@@ -235,12 +354,16 @@ program
|
|
|
235
354
|
const secret = getConfig('secret_key') || process.env.SECRET_KEY;
|
|
236
355
|
|
|
237
356
|
if (!serverUrl) {
|
|
238
|
-
logger.error(
|
|
357
|
+
logger.error(
|
|
358
|
+
'Error: "server_url" is not set. Set SERVER_URL env var or run "redep config set server_url <url>"'
|
|
359
|
+
);
|
|
239
360
|
process.exit(1);
|
|
240
361
|
}
|
|
241
362
|
|
|
242
363
|
if (!secret) {
|
|
243
|
-
logger.error(
|
|
364
|
+
logger.error(
|
|
365
|
+
'Error: "secret_key" is not set. Set SECRET_KEY env var or run "redep config set secret_key <your-secret>"'
|
|
366
|
+
);
|
|
244
367
|
process.exit(1);
|
|
245
368
|
}
|
|
246
369
|
|
|
@@ -3,12 +3,12 @@ services:
|
|
|
3
3
|
build: .
|
|
4
4
|
container_name: deploy-server
|
|
5
5
|
restart: unless-stopped
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
env_file:
|
|
8
8
|
- .env
|
|
9
9
|
|
|
10
10
|
ports:
|
|
11
|
-
-
|
|
11
|
+
- '${SERVER_PORT}:3000'
|
|
12
12
|
environment:
|
|
13
13
|
# Override WORKING_DIR to match the internal container path
|
|
14
14
|
- WORKING_DIR=/workspace
|
package/lib/client/client.js
CHANGED
|
@@ -9,14 +9,16 @@ export const sendCommand = async (url, endpoint, secret, data) => {
|
|
|
9
9
|
|
|
10
10
|
const response = await axios.post(fullUrl, data, {
|
|
11
11
|
headers: {
|
|
12
|
-
|
|
13
|
-
'Content-Type': 'application/json'
|
|
14
|
-
}
|
|
12
|
+
Authorization: `Bearer ${secret}`,
|
|
13
|
+
'Content-Type': 'application/json',
|
|
14
|
+
},
|
|
15
15
|
});
|
|
16
16
|
return response.data;
|
|
17
17
|
} catch (error) {
|
|
18
18
|
if (error.response) {
|
|
19
|
-
throw new Error(
|
|
19
|
+
throw new Error(
|
|
20
|
+
`Server error (${error.response.status}): ${JSON.stringify(error.response.data)}`
|
|
21
|
+
);
|
|
20
22
|
} else if (error.request) {
|
|
21
23
|
throw new Error(`Connection failed: No response from ${url}. Is the server running?`);
|
|
22
24
|
} else {
|
package/lib/client/index.js
CHANGED
|
@@ -3,19 +3,38 @@ import { logger } from '../logger.js';
|
|
|
3
3
|
|
|
4
4
|
export const deploy = async (type, serverUrl, secret) => {
|
|
5
5
|
logger.info(`Starting deployment sequence for target: ${type}`);
|
|
6
|
-
|
|
6
|
+
|
|
7
7
|
if (type === 'fe') {
|
|
8
8
|
logger.info('Sending deployment instruction to server machine...');
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
try {
|
|
11
11
|
const result = await sendCommand(serverUrl, '/deploy/fe', secret, {});
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
if (result.status === 'success') {
|
|
14
14
|
logger.success('Server successfully executed the deployment command.');
|
|
15
15
|
logger.log('--- Remote Output ---');
|
|
16
16
|
if (result.output.stdout) console.log(result.output.stdout);
|
|
17
17
|
if (result.output.stderr) console.log(result.output.stderr);
|
|
18
18
|
logger.log('---------------------');
|
|
19
|
+
} else if (type === 'custom') {
|
|
20
|
+
logger.info('Sending custom deployment instruction to server machine...');
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
const result = await sendCommand(serverUrl, '/deploy/custom', secret, {});
|
|
24
|
+
|
|
25
|
+
if (result.status === 'success') {
|
|
26
|
+
logger.success('Server successfully executed the custom deployment command.');
|
|
27
|
+
logger.log('--- Remote Output ---');
|
|
28
|
+
if (result.output.stdout) console.log(result.output.stdout);
|
|
29
|
+
if (result.output.stderr) console.log(result.output.stderr);
|
|
30
|
+
logger.log('---------------------');
|
|
31
|
+
} else {
|
|
32
|
+
logger.error('Server reported failure.');
|
|
33
|
+
console.error(result);
|
|
34
|
+
}
|
|
35
|
+
} catch (err) {
|
|
36
|
+
throw err;
|
|
37
|
+
}
|
|
19
38
|
} else {
|
|
20
39
|
logger.error('Server reported failure.');
|
|
21
40
|
console.error(result);
|
|
@@ -23,7 +42,6 @@ export const deploy = async (type, serverUrl, secret) => {
|
|
|
23
42
|
} catch (err) {
|
|
24
43
|
throw err;
|
|
25
44
|
}
|
|
26
|
-
|
|
27
45
|
} else {
|
|
28
46
|
logger.error(`Deployment type "${type}" is not supported yet.`);
|
|
29
47
|
throw new Error(`Unsupported deployment type: ${type}`);
|
package/lib/config.js
CHANGED
package/lib/logger.js
CHANGED
|
@@ -5,5 +5,5 @@ export const logger = {
|
|
|
5
5
|
success: (msg) => console.log(chalk.green('✔') + ' ' + msg),
|
|
6
6
|
warn: (msg) => console.log(chalk.yellow('⚠') + ' ' + msg),
|
|
7
7
|
error: (msg) => console.error(chalk.red('✖') + ' ' + msg),
|
|
8
|
-
log: (msg) => console.log(msg)
|
|
8
|
+
log: (msg) => console.log(msg),
|
|
9
9
|
};
|
package/lib/server/executor.js
CHANGED
|
@@ -4,13 +4,13 @@ import { logger } from '../logger.js';
|
|
|
4
4
|
export const executeCommand = (command, workingDir) => {
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
6
6
|
logger.info(`Executing: ${command} in ${workingDir}`);
|
|
7
|
-
|
|
7
|
+
|
|
8
8
|
exec(command, { cwd: workingDir }, (error, stdout, stderr) => {
|
|
9
9
|
if (error) {
|
|
10
10
|
logger.error(`Execution error: ${error.message}`);
|
|
11
11
|
return reject({ error: error.message, stderr });
|
|
12
12
|
}
|
|
13
|
-
|
|
13
|
+
|
|
14
14
|
if (stderr) {
|
|
15
15
|
// Docker often outputs to stderr even for info, so we log it but don't fail unless error is set
|
|
16
16
|
logger.warn(`Stderr: ${stderr}`);
|
package/lib/server/index.js
CHANGED
|
@@ -6,9 +6,9 @@ export const startServer = (port, secret, workingDir) => {
|
|
|
6
6
|
logger.error('Cannot start server: Secret key is required for security.');
|
|
7
7
|
process.exit(1);
|
|
8
8
|
}
|
|
9
|
-
|
|
9
|
+
|
|
10
10
|
const app = createServer(secret, workingDir);
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
app.listen(port, () => {
|
|
13
13
|
logger.success(`Server is running on port ${port}`);
|
|
14
14
|
logger.info(`Working Directory: ${workingDir}`);
|
package/lib/server/server.js
CHANGED
|
@@ -5,6 +5,7 @@ import helmet from 'helmet';
|
|
|
5
5
|
import morgan from 'morgan';
|
|
6
6
|
import { logger } from '../logger.js';
|
|
7
7
|
import { executeCommand } from './executor.js';
|
|
8
|
+
import { getConfig } from '../config.js';
|
|
8
9
|
|
|
9
10
|
export const createServer = (secretKey, workingDir) => {
|
|
10
11
|
const app = express();
|
|
@@ -46,7 +47,7 @@ export const createServer = (secretKey, workingDir) => {
|
|
|
46
47
|
// Specific endpoint for deploy fe to map the requirement exactly
|
|
47
48
|
app.post('/deploy/fe', authenticate, async (req, res) => {
|
|
48
49
|
logger.info('Received deploy fe request');
|
|
49
|
-
|
|
50
|
+
|
|
50
51
|
// Requirement: cd to working dir and run docker compose up -d
|
|
51
52
|
// We use 'docker compose pull && docker compose up -d' to ensure latest image is used
|
|
52
53
|
const command = 'docker compose pull && docker compose up -d';
|
|
@@ -59,5 +60,32 @@ export const createServer = (secretKey, workingDir) => {
|
|
|
59
60
|
}
|
|
60
61
|
});
|
|
61
62
|
|
|
63
|
+
// Endpoint for custom deployment command
|
|
64
|
+
app.post('/deploy/custom', authenticate, async (req, res) => {
|
|
65
|
+
logger.info('Received custom deploy request');
|
|
66
|
+
|
|
67
|
+
// Get command from environment variable or config
|
|
68
|
+
const deploymentCommand = getConfig('deployment_command') || process.env.DEPLOYMENT_COMMAND;
|
|
69
|
+
|
|
70
|
+
if (!deploymentCommand) {
|
|
71
|
+
logger.error('No DEPLOYMENT_COMMAND set');
|
|
72
|
+
return res.status(400).json({
|
|
73
|
+
status: 'error',
|
|
74
|
+
error:
|
|
75
|
+
'No DEPLOYMENT_COMMAND configured. Set it via env var or "redep config set deployment_command <cmd>"',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
logger.info(`Executing custom command: ${deploymentCommand}`);
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await executeCommand(deploymentCommand, workingDir);
|
|
83
|
+
res.json({ status: 'success', output: result });
|
|
84
|
+
} catch (error) {
|
|
85
|
+
logger.error(`Deployment failed: ${error}`);
|
|
86
|
+
res.status(500).json({ status: 'error', error: error.message || error });
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
|
|
62
90
|
return app;
|
|
63
91
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "remote-deploy-cli",
|
|
3
|
-
"
|
|
3
|
+
"author": "nafies1",
|
|
4
|
+
"version": "1.3.0",
|
|
4
5
|
"main": "index.js",
|
|
5
6
|
"type": "module",
|
|
6
7
|
"bin": {
|
|
@@ -8,10 +9,12 @@
|
|
|
8
9
|
},
|
|
9
10
|
"scripts": {
|
|
10
11
|
"test": "echo \"Error: no test specified\" && exit 1",
|
|
11
|
-
"update": "node scripts/update.js"
|
|
12
|
+
"update": "node scripts/update.js",
|
|
13
|
+
"format": "prettier --write .",
|
|
14
|
+
"format:check": "prettier --check .",
|
|
15
|
+
"sonar": "node sonar-project.js"
|
|
12
16
|
},
|
|
13
17
|
"keywords": [],
|
|
14
|
-
"author": "",
|
|
15
18
|
"license": "ISC",
|
|
16
19
|
"description": "Remote execution CLI for deployment",
|
|
17
20
|
"dependencies": {
|
|
@@ -24,7 +27,17 @@
|
|
|
24
27
|
"dotenv": "^17.2.3",
|
|
25
28
|
"express": "^4.18.2",
|
|
26
29
|
"helmet": "^7.1.0",
|
|
30
|
+
"inquirer": "^13.2.0",
|
|
27
31
|
"morgan": "^1.10.0",
|
|
28
32
|
"pm2": "^6.0.14"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"@sonar/scan": "^4.3.4",
|
|
36
|
+
"prettier": "^3.8.0",
|
|
37
|
+
"sonarqube-scanner": "^4.3.4"
|
|
38
|
+
},
|
|
39
|
+
"redep": {
|
|
40
|
+
"secret_key": "4k94jLuEIT_jWPHJYPActmGxn2x72eR0",
|
|
41
|
+
"working_dir": "C:\\Users\\nafie\\Documents\\trae_projects\\remote-deploy-cli"
|
|
29
42
|
}
|
|
30
43
|
}
|
package/scripts/update.js
CHANGED
package/sonar-project.js
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
|
+
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const sonarScannerModule = require('@sonar/scan');
|
|
5
|
+
const sonarScanner = sonarScannerModule.scan || sonarScannerModule.default || sonarScannerModule;
|
|
6
|
+
|
|
7
|
+
sonarScanner(
|
|
8
|
+
{
|
|
9
|
+
serverUrl: process.env.SONAR_HOST_URL || 'http://localhost:9000',
|
|
10
|
+
token: process.env.SONAR_TOKEN,
|
|
11
|
+
options: {
|
|
12
|
+
'sonar.projectKey': 'remote-deploy-cli',
|
|
13
|
+
'sonar.projectName': 'Remote Deploy CLI',
|
|
14
|
+
'sonar.projectVersion': pkg.version,
|
|
15
|
+
'sonar.sources': 'bin,lib',
|
|
16
|
+
'sonar.tests': 'test',
|
|
17
|
+
'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info',
|
|
18
|
+
'sonar.sourceEncoding': 'UTF-8',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
() => process.exit()
|
|
22
|
+
);
|
|
Binary file
|