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.
@@ -0,0 +1,9 @@
1
+ # Ignore artifacts:
2
+ build
3
+ coverage
4
+ dist
5
+ node_modules
6
+ .git
7
+
8
+ # Ignore config files that don't need formatting
9
+ package-lock.json
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "tabWidth": 2,
5
+ "trailingComma": "es5",
6
+ "printWidth": 100
7
+ }
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
- - `<type>`: The service type to deploy. Currently supports `fe` (frontend).
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
- * **Auto-PM2**: If `pm2` is installed, it uses PM2 for process management.
131
- * **Native Fallback**: If `pm2` is missing, it uses Node.js `child_process` to detach.
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 | Env Variable | Description | Context |
208
- | :------------ | :------------ | :----------------------------------------------- | :--------------- |
209
- | `server_port` | `SERVER_PORT` | Port for the server to listen on (Default: 3000) | **Server** |
210
- | `working_dir` | `WORKING_DIR` | Directory to execute commands in | **Server** |
211
- | `server_url` | `SERVER_URL` | URL of the remote `redep` server | **Client** |
212
- | `secret_key` | `SECRET_KEY` | Shared secret for authentication | **Both** |
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
- .name('redep')
15
- .description('Remote execution CLI for deployment')
16
- .version('1.0.0');
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(/^\/([A-Za-z]:)/, '$1');
72
-
73
- const args = [
74
- 'start',
75
- scriptPath,
76
- '--name', 'redep-server'
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
- logger.warn('PM2 start failed, falling back to native background process...');
102
- startNativeBackground(options);
217
+ logger.warn('PM2 start failed, falling back to native background process...');
218
+ startNativeBackground(options);
103
219
  } else {
104
- logger.success('Server started in background using PM2');
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
- const existingPid = getConfig('server_pid');
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
- const args = ['listen'];
128
- if (options.port) {
129
- args.push('--port', options.port);
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
- const child = spawn(process.argv[0], [process.argv[1], ...args], {
133
- detached: true,
134
- stdio: 'ignore',
135
- windowsHide: true
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
- child.unref();
139
- setConfig('server_pid', child.pid);
140
- logger.success(`Server started in background (native) with PID ${child.pid}`);
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
- // Try PM2 status first
183
- const pm2 = spawn('pm2', ['describe', 'redep-server'], { stdio: 'inherit', shell: true });
297
+ // Try PM2 status first
298
+ const pm2 = spawn('pm2', ['describe', 'redep-server'], { stdio: 'inherit', shell: true });
184
299
 
185
- pm2.on('close', (code) => {
186
- if (code !== 0) {
187
- // Fallback to native status
188
- const pid = getConfig('server_pid');
189
-
190
- if (!pid) {
191
- logger.info('Server is NOT running.');
192
- return;
193
- }
194
-
195
- try {
196
- process.kill(pid, 0);
197
- logger.success(`Server is RUNNING (PID ${pid})`);
198
- } catch (e) {
199
- logger.warn(`Server is NOT running (Stale PID ${pid} found).`);
200
- setConfig('server_pid', null);
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('Warning: No "secret_key" set in config or SECRET_KEY env var. Communication might be insecure or fail if client requires it.');
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('Error: "working_dir" is not set. Please set it using "redep config set working_dir <path>" or WORKING_DIR env var.');
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('Error: "server_url" is not set. Set SERVER_URL env var or run "redep config set server_url <url>"');
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('Error: "secret_key" is not set. Set SECRET_KEY env var or run "redep config set secret_key <your-secret>"');
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
- - "${SERVER_PORT}:3000"
11
+ - '${SERVER_PORT}:3000'
12
12
  environment:
13
13
  # Override WORKING_DIR to match the internal container path
14
14
  - WORKING_DIR=/workspace
@@ -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
- 'Authorization': `Bearer ${secret}`,
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(`Server error (${error.response.status}): ${JSON.stringify(error.response.data)}`);
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 {
@@ -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
@@ -1,7 +1,7 @@
1
1
  import Conf from 'conf';
2
2
 
3
3
  const config = new Conf({
4
- projectName: 'redep'
4
+ projectName: 'redep',
5
5
  });
6
6
 
7
7
  export const getConfig = (key) => {
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
  };
@@ -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}`);
@@ -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}`);
@@ -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
- "version": "1.1.2",
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
@@ -83,7 +83,6 @@ try {
83
83
  execSync('git push origin main', { stdio: 'inherit' });
84
84
 
85
85
  console.log('Update process completed successfully!');
86
-
87
86
  } catch (error) {
88
87
  console.error(`Error during update process: ${error.message}`);
89
88
  process.exit(1);
@@ -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