remote-deploy-cli 1.2.0 → 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/README.md +40 -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 +25 -26
- 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 +5 -4
- package/package.json +8 -2
- package/scripts/update.js +0 -1
- package/sonar-project.js +8 -4
- package/test-target/docker-compose.yml +0 -0
package/README.md
CHANGED
|
@@ -66,20 +66,24 @@ 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:**
|
|
75
|
+
|
|
74
76
|
- `<type>`: The service type to deploy.
|
|
75
77
|
- `fe`: Frontend (pre-configured to run `docker compose pull && docker compose up -d`).
|
|
76
78
|
- `custom`: Custom command (configured on server via `deployment_command`).
|
|
77
79
|
|
|
78
80
|
**Requirements:**
|
|
81
|
+
|
|
79
82
|
- `SERVER_URL` must be configured.
|
|
80
83
|
- `SECRET_KEY` must match the server's key.
|
|
81
84
|
|
|
82
85
|
**Example:**
|
|
86
|
+
|
|
83
87
|
```bash
|
|
84
88
|
# Deploy frontend service
|
|
85
89
|
redep deploy fe
|
|
@@ -89,6 +93,7 @@ redep deploy custom
|
|
|
89
93
|
```
|
|
90
94
|
|
|
91
95
|
**Expected Output:**
|
|
96
|
+
|
|
92
97
|
```
|
|
93
98
|
[INFO] Deploying fe to http://192.168.1.50:3000...
|
|
94
99
|
[SUCCESS] Deployment triggered successfully.
|
|
@@ -117,14 +122,17 @@ These commands are executed on the **remote server** (VPS, VM, etc.).
|
|
|
117
122
|
Starts the server in the foreground. Useful for debugging or running inside Docker.
|
|
118
123
|
|
|
119
124
|
**Syntax:**
|
|
125
|
+
|
|
120
126
|
```bash
|
|
121
127
|
redep listen [--port <number>]
|
|
122
128
|
```
|
|
123
129
|
|
|
124
130
|
**Options:**
|
|
131
|
+
|
|
125
132
|
- `-p, --port`: Specify port (default: 3000).
|
|
126
133
|
|
|
127
134
|
**Example:**
|
|
135
|
+
|
|
128
136
|
```bash
|
|
129
137
|
redep listen --port 4000
|
|
130
138
|
```
|
|
@@ -132,19 +140,23 @@ redep listen --port 4000
|
|
|
132
140
|
### `start` (Background)
|
|
133
141
|
|
|
134
142
|
Starts the server in background mode (Daemon).
|
|
135
|
-
|
|
136
|
-
|
|
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.
|
|
137
146
|
|
|
138
147
|
**Syntax:**
|
|
148
|
+
|
|
139
149
|
```bash
|
|
140
150
|
redep start [--port <number>]
|
|
141
151
|
```
|
|
142
152
|
|
|
143
153
|
**Related Commands:**
|
|
154
|
+
|
|
144
155
|
- `redep stop`: Stops the background server.
|
|
145
156
|
- `redep status`: Checks if the server is running.
|
|
146
157
|
|
|
147
158
|
**Example:**
|
|
159
|
+
|
|
148
160
|
```bash
|
|
149
161
|
redep start
|
|
150
162
|
# [SUCCESS] Server started in background using PM2
|
|
@@ -185,6 +197,20 @@ services:
|
|
|
185
197
|
|
|
186
198
|
---
|
|
187
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
|
+
|
|
188
214
|
## Command Interactions
|
|
189
215
|
|
|
190
216
|
### Workflow: Deployment
|
|
@@ -193,11 +219,11 @@ services:
|
|
|
193
219
|
sequenceDiagram
|
|
194
220
|
participant Client (CI/Local)
|
|
195
221
|
participant Server (Remote)
|
|
196
|
-
|
|
222
|
+
|
|
197
223
|
Note over Client: User runs "redep deploy fe"
|
|
198
224
|
Client->>Client: Read Config (URL, Secret)
|
|
199
225
|
Client->>Server: POST /deploy { type: "fe" } (Auth: Bearer Token)
|
|
200
|
-
|
|
226
|
+
|
|
201
227
|
Note over Server: Verify Secret Key
|
|
202
228
|
alt Invalid Key
|
|
203
229
|
Server-->>Client: 403 Forbidden
|
|
@@ -213,27 +239,30 @@ sequenceDiagram
|
|
|
213
239
|
|
|
214
240
|
## Configuration Reference
|
|
215
241
|
|
|
216
|
-
| Config Key
|
|
217
|
-
|
|
|
218
|
-
| `server_port`
|
|
219
|
-
| `working_dir`
|
|
220
|
-
| `deployment_command` | `DEPLOYMENT_COMMAND` | Custom command for `deploy custom`
|
|
221
|
-
| `server_url`
|
|
222
|
-
| `secret_key`
|
|
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** |
|
|
223
249
|
|
|
224
250
|
---
|
|
225
251
|
|
|
226
252
|
## Troubleshooting
|
|
227
253
|
|
|
228
254
|
### `Error: "working_dir" is not set`
|
|
255
|
+
|
|
229
256
|
- **Context**: Server
|
|
230
257
|
- **Fix**: Run `redep config set working_dir /path` or check `docker-compose.yml` environment.
|
|
231
258
|
|
|
232
259
|
### `Connection Refused`
|
|
260
|
+
|
|
233
261
|
- **Context**: Client
|
|
234
262
|
- **Fix**: Ensure server is running (`redep status` or `docker ps`) and port 3000 is open.
|
|
235
263
|
|
|
236
264
|
### `403 Forbidden`
|
|
265
|
+
|
|
237
266
|
- **Context**: Client
|
|
238
267
|
- **Fix**: Re-check `SECRET_KEY` on both machines. They must match exactly.
|
|
239
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,13 +3,13 @@ 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 ---');
|
|
@@ -17,34 +17,33 @@ export const deploy = async (type, serverUrl, secret) => {
|
|
|
17
17
|
if (result.output.stderr) console.log(result.output.stderr);
|
|
18
18
|
logger.log('---------------------');
|
|
19
19
|
} else if (type === 'custom') {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
logger.error('Server reported failure.');
|
|
33
|
-
console.error(result);
|
|
34
|
-
}
|
|
35
|
-
} catch (err) {
|
|
36
|
-
throw err;
|
|
37
|
-
}
|
|
38
|
-
} else {
|
|
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 {
|
|
39
32
|
logger.error('Server reported failure.');
|
|
40
33
|
console.error(result);
|
|
41
34
|
}
|
|
42
35
|
} catch (err) {
|
|
43
36
|
throw err;
|
|
44
37
|
}
|
|
45
|
-
|
|
46
38
|
} else {
|
|
47
|
-
logger.error(
|
|
48
|
-
|
|
39
|
+
logger.error('Server reported failure.');
|
|
40
|
+
console.error(result);
|
|
49
41
|
}
|
|
50
|
-
}
|
|
42
|
+
} catch (err) {
|
|
43
|
+
throw err;
|
|
44
|
+
}
|
|
45
|
+
} else {
|
|
46
|
+
logger.error(`Deployment type "${type}" is not supported yet.`);
|
|
47
|
+
throw new Error(`Unsupported deployment type: ${type}`);
|
|
48
|
+
}
|
|
49
|
+
};
|
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
|
@@ -47,7 +47,7 @@ export const createServer = (secretKey, workingDir) => {
|
|
|
47
47
|
// Specific endpoint for deploy fe to map the requirement exactly
|
|
48
48
|
app.post('/deploy/fe', authenticate, async (req, res) => {
|
|
49
49
|
logger.info('Received deploy fe request');
|
|
50
|
-
|
|
50
|
+
|
|
51
51
|
// Requirement: cd to working dir and run docker compose up -d
|
|
52
52
|
// We use 'docker compose pull && docker compose up -d' to ensure latest image is used
|
|
53
53
|
const command = 'docker compose pull && docker compose up -d';
|
|
@@ -69,9 +69,10 @@ export const createServer = (secretKey, workingDir) => {
|
|
|
69
69
|
|
|
70
70
|
if (!deploymentCommand) {
|
|
71
71
|
logger.error('No DEPLOYMENT_COMMAND set');
|
|
72
|
-
return res.status(400).json({
|
|
73
|
-
status: 'error',
|
|
74
|
-
error:
|
|
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>"',
|
|
75
76
|
});
|
|
76
77
|
}
|
|
77
78
|
|
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": {
|
|
@@ -14,7 +15,6 @@
|
|
|
14
15
|
"sonar": "node sonar-project.js"
|
|
15
16
|
},
|
|
16
17
|
"keywords": [],
|
|
17
|
-
"author": "",
|
|
18
18
|
"license": "ISC",
|
|
19
19
|
"description": "Remote execution CLI for deployment",
|
|
20
20
|
"dependencies": {
|
|
@@ -27,11 +27,17 @@
|
|
|
27
27
|
"dotenv": "^17.2.3",
|
|
28
28
|
"express": "^4.18.2",
|
|
29
29
|
"helmet": "^7.1.0",
|
|
30
|
+
"inquirer": "^13.2.0",
|
|
30
31
|
"morgan": "^1.10.0",
|
|
31
32
|
"pm2": "^6.0.14"
|
|
32
33
|
},
|
|
33
34
|
"devDependencies": {
|
|
35
|
+
"@sonar/scan": "^4.3.4",
|
|
34
36
|
"prettier": "^3.8.0",
|
|
35
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"
|
|
36
42
|
}
|
|
37
43
|
}
|
package/scripts/update.js
CHANGED
package/sonar-project.js
CHANGED
|
@@ -1,15 +1,19 @@
|
|
|
1
|
-
|
|
1
|
+
import { createRequire } from 'module';
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
const require = createRequire(import.meta.url);
|
|
4
|
+
const sonarScannerModule = require('@sonar/scan');
|
|
5
|
+
const sonarScanner = sonarScannerModule.scan || sonarScannerModule.default || sonarScannerModule;
|
|
6
|
+
|
|
7
|
+
sonarScanner(
|
|
4
8
|
{
|
|
5
9
|
serverUrl: process.env.SONAR_HOST_URL || 'http://localhost:9000',
|
|
6
10
|
token: process.env.SONAR_TOKEN,
|
|
7
11
|
options: {
|
|
8
12
|
'sonar.projectKey': 'remote-deploy-cli',
|
|
9
13
|
'sonar.projectName': 'Remote Deploy CLI',
|
|
10
|
-
'sonar.projectVersion':
|
|
14
|
+
'sonar.projectVersion': pkg.version,
|
|
11
15
|
'sonar.sources': 'bin,lib',
|
|
12
|
-
'sonar.tests': 'test',
|
|
16
|
+
'sonar.tests': 'test',
|
|
13
17
|
'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info',
|
|
14
18
|
'sonar.sourceEncoding': 'UTF-8',
|
|
15
19
|
},
|
|
Binary file
|