redep 2.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/.env.example ADDED
@@ -0,0 +1,7 @@
1
+ # Server Configuration
2
+ SERVER_PORT=3000
3
+ SECRET_KEY=your-secret-key
4
+ WORKING_DIR=/path/to/your/project
5
+
6
+ # Client Configuration
7
+ SERVER_URL=http://localhost:3000
@@ -0,0 +1,69 @@
1
+ name: CI/CD Pipeline
2
+
3
+ on:
4
+ release:
5
+ types: [created]
6
+
7
+ permissions:
8
+ contents: read
9
+ packages: write
10
+
11
+ jobs:
12
+ docker:
13
+ name: Build and Push Docker Image
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Set up QEMU
20
+ uses: docker/setup-qemu-action@v3
21
+
22
+ - name: Set up Docker Buildx
23
+ uses: docker/setup-buildx-action@v3
24
+
25
+ - name: Login to Docker Hub
26
+ uses: docker/login-action@v3
27
+ with:
28
+ username: ${{ secrets.DOCKER_USERNAME }}
29
+ password: ${{ secrets.DOCKER_PASSWORD }}
30
+
31
+ - name: Extract metadata (tags, labels) for Docker
32
+ id: meta
33
+ uses: docker/metadata-action@v5
34
+ with:
35
+ images: nafies1/redep
36
+ tags: |
37
+ type=semver,pattern={{version}}
38
+ type=semver,pattern={{major}}.{{minor}}
39
+ type=raw,value=latest,enable=${{ !github.event.release.prerelease }}
40
+
41
+ - name: Build and push
42
+ uses: docker/build-push-action@v5
43
+ with:
44
+ context: .
45
+ push: true
46
+ tags: ${{ steps.meta.outputs.tags }}
47
+ labels: ${{ steps.meta.outputs.labels }}
48
+
49
+ npm:
50
+ name: Publish to npm
51
+ runs-on: ubuntu-latest
52
+ needs: docker # Wait for docker build to ensure code is stable/buildable
53
+ steps:
54
+ - name: Checkout code
55
+ uses: actions/checkout@v4
56
+
57
+ - name: Setup Node.js
58
+ uses: actions/setup-node@v4
59
+ with:
60
+ node-version: '22'
61
+ registry-url: 'https://registry.npmjs.org'
62
+
63
+ - name: Install dependencies
64
+ run: npm ci
65
+
66
+ - name: Publish to npm
67
+ run: npm publish
68
+ env:
69
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
@@ -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/Dockerfile ADDED
@@ -0,0 +1,27 @@
1
+ FROM node:25.3.0-alpine
2
+
3
+ # Install Docker CLI and Docker Compose plugin
4
+ # We need these to execute docker commands from the slave
5
+ RUN apk add --no-cache docker-cli docker-cli-compose
6
+
7
+ WORKDIR /app
8
+
9
+ # Install app dependencies
10
+ COPY package*.json ./
11
+ RUN npm install --production
12
+
13
+ # Bundle app source
14
+ COPY . .
15
+
16
+ # Make the CLI executable
17
+ RUN chmod +x bin/index.js
18
+
19
+ # Expose the default port
20
+ EXPOSE 3000
21
+
22
+ # Health check
23
+ HEALTHCHECK --interval=30s --timeout=3s \
24
+ CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
25
+
26
+ # Start the server listener by default
27
+ CMD ["node", "bin/index.js", "listen"]
package/README.md ADDED
@@ -0,0 +1,172 @@
1
+ # redep (Remote Deployment Tool)
2
+
3
+ > A secure, streaming deployment tool for Node.js and Docker environments. Real-time logs, multi-server support, and secure execution.
4
+
5
+ [![npm version](https://img.shields.io/npm/v/redep.svg)](https://www.npmjs.com/package/redep)
6
+ [![Docker Pulls](https://img.shields.io/docker/pulls/nafies1/redep)](https://hub.docker.com/r/nafies1/redep)
7
+ [![License](https://img.shields.io/npm/l/redep.svg)](LICENSE)
8
+ [![Build Status](https://github.com/nafies1/redep/actions/workflows/ci-cd.yml/badge.svg)](https://github.com/nafies1/redep/actions)
9
+
10
+ ## 📖 Project Overview
11
+
12
+ **redep** is a lightweight, secure Command Line Interface (CLI) designed to simplify remote deployment workflows. It solves the problem of "blind deployments" by establishing a real-time WebSocket connection between your local machine (or CI/CD runner) and your remote servers.
13
+
14
+ ### Key Features
15
+
16
+ - **📺 Real-time Streaming**: Watch your deployment logs (stdout/stderr) stream live to your terminal.
17
+ - **🔒 Secure**: Uses Token-based authentication and strictly isolated command execution.
18
+ - **⚡ Multi-Server**: Manage multiple environments (Development, UAT, Production) from a single config.
19
+ - **🐳 Docker Ready**: Comes with a production-ready Docker image for instant server setup.
20
+ - **🛠️ Flexible**: Execute any command you define (`docker compose`, `kubectl`, shell scripts, etc.).
21
+
22
+ ---
23
+
24
+ ## 🚀 Installation
25
+
26
+ ### npm Package (Client & Server)
27
+
28
+ Install globally to use the CLI on your local machine or server.
29
+
30
+ ```bash
31
+ npm install -g redep
32
+ ```
33
+
34
+ ### Docker Image (Server Only)
35
+
36
+ Pull the official image for running the server component.
37
+
38
+ ```bash
39
+ docker pull nafies1/redep:latest
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 📖 Usage Guide
45
+
46
+ ### 1. Setting up the Server
47
+
48
+ The server is the agent that runs on your remote machine and executes the deployment commands.
49
+
50
+ #### Option A: Using Docker (Recommended)
51
+
52
+ ```bash
53
+ docker run -d \
54
+ --name redep-server \
55
+ --restart always \
56
+ -p 3000:3000 \
57
+ -v /var/run/docker.sock:/var/run/docker.sock \
58
+ -v $(pwd):/app/workspace \
59
+ -e SECRET_KEY=your-super-secret-key \
60
+ -e WORKING_DIR=/app/workspace \
61
+ -e DEPLOYMENT_COMMAND="docker compose pull && docker compose up -d" \
62
+ nafies1/redep:latest
63
+ ```
64
+
65
+ #### Option B: Using npm & PM2
66
+
67
+ ```bash
68
+ # Initialize configuration
69
+ redep init server
70
+ # Follow the prompts to set port, working dir, and secret key
71
+
72
+ # Start the server
73
+ redep start
74
+ ```
75
+
76
+ ### 2. Setting up the Client
77
+
78
+ The client runs on your local machine or CI pipeline.
79
+
80
+ ```bash
81
+ # Initialize a server profile
82
+ redep init client
83
+ # ? Enter Server Name: prod
84
+ # ? Enter Server URL (Host): http://your-server-ip:3000
85
+ # ? Enter Secret Key: your-super-secret-key
86
+ ```
87
+
88
+ ### 3. Deploying
89
+
90
+ Trigger a deployment to your configured server.
91
+
92
+ ```bash
93
+ redep deploy prod
94
+ ```
95
+
96
+ You will see:
97
+ ```text
98
+ (INFO) Connecting to http://your-server-ip:3000...
99
+ (SUCCESS) Connected to server. requesting deployment...
100
+ (INFO) [10:00:01 AM] Status: Deployment Started
101
+ (INFO) [10:00:02 AM] [STDOUT] Pulling images...
102
+ (INFO) [10:00:05 AM] [STDOUT] Container recreated.
103
+ (SUCCESS) [10:00:06 AM] Status: Deployment Completed
104
+ ```
105
+
106
+ ---
107
+
108
+ ## ⚙️ Configuration
109
+
110
+ `redep` uses a hierarchical configuration system:
111
+ 1. **Environment Variables** (Highest Priority)
112
+ 2. **Config File** (Managed via CLI)
113
+
114
+ See [Advanced Configuration](docs/ADVANCED_CONFIG.md) for full details on environment variables and config management.
115
+
116
+ ---
117
+
118
+ ## 💻 Development Setup
119
+
120
+ ### Prerequisites
121
+ - Node.js >= 18
122
+ - Docker (for testing container builds)
123
+
124
+ ### Local Development
125
+
126
+ 1. **Clone the repository:**
127
+ ```bash
128
+ git clone https://github.com/nafies1/redep.git
129
+ cd redep
130
+ ```
131
+
132
+ 2. **Install dependencies:**
133
+ ```bash
134
+ npm install
135
+ ```
136
+
137
+ 3. **Link globally (optional):**
138
+ ```bash
139
+ npm link
140
+ ```
141
+
142
+ 4. **Run tests:**
143
+ Currently, we rely on manual verification using the `test-target` directory.
144
+ ```bash
145
+ # Start server
146
+ npm run start -- listen
147
+ ```
148
+
149
+ ---
150
+
151
+ ## 🔄 CI/CD Pipeline
152
+
153
+ This project uses GitHub Actions for automation:
154
+
155
+ - **Build & Push Docker Image**: Triggered on new release tags. Pushes to Docker Hub.
156
+ - **Publish npm Package**: Triggered on new release tags. Publishes to npm registry.
157
+
158
+ See [.github/workflows/ci-cd.yml](.github/workflows/ci-cd.yml) for details.
159
+
160
+ ---
161
+
162
+ ## 📚 Additional Documentation
163
+
164
+ - [Troubleshooting Guide](docs/TROUBLESHOOTING.md) - Solutions for common connection and auth issues.
165
+ - [Advanced Configuration](docs/ADVANCED_CONFIG.md) - Deep dive into config options and PM2.
166
+ - [API Reference](docs/API.md) - Socket.IO events and protocol details.
167
+
168
+ ---
169
+
170
+ ## 📄 License
171
+
172
+ This project is licensed under the ISC License.
package/bin/index.js ADDED
@@ -0,0 +1,386 @@
1
+ #!/usr/bin/env node
2
+
3
+ import 'dotenv/config';
4
+ import { Command } from 'commander';
5
+ import { spawn } from 'child_process';
6
+ import crypto from 'crypto';
7
+ import inquirer from 'inquirer';
8
+ import { logger } from '../lib/logger.js';
9
+ import { getConfig, setConfig, getAllConfig, clearConfig } from '../lib/config.js';
10
+ import { startServer } from '../lib/server/index.js';
11
+ import { deploy } from '../lib/client/index.js';
12
+ import pkg from '../package.json' with { type: 'json' };
13
+
14
+ const program = new Command();
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
29
+ program
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);
135
+
136
+ // Configuration Command
137
+ const configCommand = new Command('config').description('Manage configuration');
138
+
139
+ configCommand
140
+ .command('set <key> <value>')
141
+ .description('Set a configuration key')
142
+ .action((key, value) => {
143
+ setConfig(key, value);
144
+ logger.success(`Configuration updated: ${key} = ${value}`);
145
+ });
146
+
147
+ configCommand
148
+ .command('get <key>')
149
+ .description('Get a configuration key')
150
+ .action((key) => {
151
+ const value = getConfig(key);
152
+ logger.info(`${key}: ${value}`);
153
+ });
154
+
155
+ configCommand
156
+ .command('list')
157
+ .description('List all configurations')
158
+ .action(() => {
159
+ const all = getAllConfig();
160
+ logger.info('Current Configuration:');
161
+ console.table(all);
162
+ });
163
+
164
+ configCommand
165
+ .command('clear')
166
+ .description('Clear all configurations')
167
+ .action(() => {
168
+ clearConfig();
169
+ logger.success('All configurations have been cleared.');
170
+ });
171
+
172
+ program.addCommand(configCommand);
173
+
174
+ // Background Process Management
175
+ program
176
+ .command('start')
177
+ .description('Start the server in background (daemon mode) using PM2 if available')
178
+ .option('-p, --port <port>', 'Port to listen on')
179
+ .action((options) => {
180
+ // Try to use PM2 first
181
+ try {
182
+ // Check if PM2 is available via API
183
+ // We'll use a dynamic import or checking for the pm2 binary in a real scenario
184
+ // But here we can just try to spawn 'pm2' command
185
+
186
+ // Use dedicated server entry point for PM2 to avoid CLI/ESM issues
187
+ // Resolve absolute path to server-entry.js
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
+
195
+ // We don't pass 'listen' arg because server-entry.js starts immediately
196
+ // But we do need to ensure env vars are passed if port is customized
197
+
198
+ const env = { ...process.env };
199
+ if (options.port) {
200
+ env.SERVER_PORT = options.port;
201
+ }
202
+
203
+ const pm2 = spawn('pm2', args, {
204
+ stdio: 'inherit',
205
+ shell: true,
206
+ env: env, // Pass modified env with port
207
+ });
208
+
209
+ pm2.on('error', () => {
210
+ // Fallback to native spawn if PM2 is not found/fails
211
+ logger.info('PM2 not found, falling back to native background process...');
212
+ startNativeBackground(options);
213
+ });
214
+
215
+ pm2.on('close', (code) => {
216
+ if (code !== 0) {
217
+ logger.warn('PM2 start failed, falling back to native background process...');
218
+ startNativeBackground(options);
219
+ } else {
220
+ logger.success('Server started in background using PM2');
221
+ }
222
+ });
223
+ } catch (e) {
224
+ startNativeBackground(options);
225
+ }
226
+ });
227
+
228
+ function startNativeBackground(options) {
229
+ const existingPid = getConfig('server_pid');
230
+
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);
239
+ }
240
+ }
241
+
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
+ });
252
+
253
+ child.unref();
254
+ setConfig('server_pid', child.pid);
255
+ logger.success(`Server started in background (native) with PID ${child.pid}`);
256
+ }
257
+
258
+ program
259
+ .command('stop')
260
+ .description('Stop the background server')
261
+ .action(() => {
262
+ // Try PM2 stop first
263
+ const pm2 = spawn('pm2', ['stop', 'redep-server'], { stdio: 'ignore', shell: true });
264
+
265
+ pm2.on('close', (code) => {
266
+ if (code === 0) {
267
+ logger.success('Server stopped (PM2)');
268
+ return;
269
+ }
270
+
271
+ // Fallback to native stop
272
+ const pid = getConfig('server_pid');
273
+ if (!pid) {
274
+ logger.warn('No active server found.');
275
+ return;
276
+ }
277
+
278
+ try {
279
+ process.kill(pid);
280
+ setConfig('server_pid', null);
281
+ logger.success(`Server stopped (PID ${pid})`);
282
+ } catch (e) {
283
+ if (e.code === 'ESRCH') {
284
+ logger.warn(`Process ${pid} not found. Cleaning up config.`);
285
+ setConfig('server_pid', null);
286
+ } else {
287
+ logger.error(`Failed to stop server: ${e.message}`);
288
+ }
289
+ }
290
+ });
291
+ });
292
+
293
+ program
294
+ .command('status')
295
+ .description('Check server status')
296
+ .action(() => {
297
+ // Try PM2 status first
298
+ const pm2 = spawn('pm2', ['describe', 'redep-server'], { stdio: 'inherit', shell: true });
299
+
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);
316
+ }
317
+ }
318
+ });
319
+ });
320
+
321
+ // Server Command
322
+ program
323
+ .command('listen')
324
+ .description('Start the server to listen for commands')
325
+ .option('-p, --port <port>', 'Port to listen on', 3000)
326
+ .action((options) => {
327
+ const port = options.port || getConfig('server_port') || process.env.SERVER_PORT || 3000;
328
+ const secret = getConfig('secret_key') || process.env.SECRET_KEY;
329
+
330
+ if (!secret) {
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
+ );
334
+ logger.info('Run "redep config set secret_key <your-secret>" or set SECRET_KEY env var.');
335
+ }
336
+
337
+ const workingDir = getConfig('working_dir') || process.env.WORKING_DIR;
338
+ if (!workingDir) {
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
+ );
342
+ process.exit(1);
343
+ }
344
+
345
+ const deploymentCommand = getConfig('deployment_command') || process.env.DEPLOYMENT_COMMAND;
346
+ if (!deploymentCommand) {
347
+ logger.error(
348
+ 'Error: "deployment_command" is not set. Please set it using "redep config set deployment_command <cmd>" or DEPLOYMENT_COMMAND env var.'
349
+ );
350
+ process.exit(1);
351
+ }
352
+
353
+ startServer(port, secret, workingDir, deploymentCommand);
354
+ });
355
+
356
+ // Client Command
357
+ program
358
+ .command('deploy <type>')
359
+ .description('Deploy a service (e.g., "fe") to the server machine')
360
+ .action(async (type) => {
361
+ const serverUrl = getConfig('server_url') || process.env.SERVER_URL;
362
+ const secret = getConfig('secret_key') || process.env.SECRET_KEY;
363
+
364
+ if (!serverUrl) {
365
+ logger.error(
366
+ 'Error: "server_url" is not set. Set SERVER_URL env var or run "redep config set server_url <url>"'
367
+ );
368
+ process.exit(1);
369
+ }
370
+
371
+ if (!secret) {
372
+ logger.error(
373
+ 'Error: "secret_key" is not set. Set SECRET_KEY env var or run "redep config set secret_key <your-secret>"'
374
+ );
375
+ process.exit(1);
376
+ }
377
+
378
+ try {
379
+ await deploy(type, serverUrl, secret);
380
+ } catch (error) {
381
+ logger.error(`Deploy failed: ${error.message}`);
382
+ process.exit(1);
383
+ }
384
+ });
385
+
386
+ program.parse(process.argv);
@@ -0,0 +1,22 @@
1
+ services:
2
+ deploy-server:
3
+ build: .
4
+ container_name: deploy-server
5
+ restart: unless-stopped
6
+
7
+ env_file:
8
+ - .env
9
+
10
+ ports:
11
+ - '${SERVER_PORT}:3000'
12
+ environment:
13
+ # Override WORKING_DIR to match the internal container path
14
+ - WORKING_DIR=/workspace
15
+ volumes:
16
+ # Required: Give access to the host's Docker Daemon
17
+ - /var/run/docker.sock:/var/run/docker.sock
18
+
19
+ # Required: Mount the project directory that contains docker-compose.yml
20
+ # Host Path : Container Path
21
+ # CHANGE THIS to your actual project path
22
+ - ${WORKING_DIR}:/workspace
@@ -0,0 +1,60 @@
1
+ # Advanced Configuration
2
+
3
+ ## Server Configuration
4
+
5
+ The server can be configured via Environment Variables or the CLI configuration store. Environment variables take precedence.
6
+
7
+ | Variable | Config Key | Description | Default |
8
+ | -------------------- | -------------------- | -------------------------------------------------------------------------- | ------- |
9
+ | `SERVER_PORT` | `server_port` | The TCP port the server listens on. | `3000` |
10
+ | `SECRET_KEY` | `secret_key` | A shared secret string for authentication. **Required**. | - |
11
+ | `WORKING_DIR` | `working_dir` | The absolute path where the deployment command is executed. **Required**. | - |
12
+ | `DEPLOYMENT_COMMAND` | `deployment_command` | The shell command to execute when a deployment is triggered. **Required**. | - |
13
+
14
+ ### Using PM2
15
+
16
+ For production usage without Docker, we recommend using PM2.
17
+
18
+ ```bash
19
+ # Start with PM2
20
+ pm2 start server-entry.js --name redep-server --env SECRET_KEY=xyz --env WORKING_DIR=/app
21
+ ```
22
+
23
+ The CLI `redep start` command automatically attempts to use PM2 if installed.
24
+
25
+ ## Client Configuration
26
+
27
+ Clients can be configured to talk to multiple servers (e.g., `dev`, `staging`, `prod`).
28
+
29
+ ```json
30
+ {
31
+ "servers": {
32
+ "prod": {
33
+ "host": "https://deploy.example.com",
34
+ "secret_key": "prod-secret"
35
+ },
36
+ "staging": {
37
+ "host": "http://10.0.0.5:3000",
38
+ "secret_key": "staging-secret"
39
+ }
40
+ }
41
+ }
42
+ ```
43
+
44
+ ### Managing Servers via CLI
45
+
46
+ ```bash
47
+ # Set a server
48
+ redep config set servers.dev.host http://localhost:3000
49
+ redep config set servers.dev.secret_key mysecret
50
+
51
+ # Get a server config
52
+ redep config get servers.dev
53
+ ```
54
+
55
+ ## Security Best Practices
56
+
57
+ 1. **TLS/SSL**: Always use HTTPS for the `host` URL in production. The WebSocket connection will automatically use WSS (Secure WebSocket).
58
+ 2. **Secret Rotation**: Rotate your `SECRET_KEY` periodically.
59
+ 3. **Firewall**: Restrict access to the server port (3000) to known IP addresses (e.g., your VPN or CI/CD runner IPs).
60
+ 4. **Least Privilege**: Run the server process with a user that has only the necessary permissions (e.g., access to Docker socket and the working directory).
package/docs/API.md ADDED
@@ -0,0 +1,59 @@
1
+ # API Reference
2
+
3
+ `redep` uses `socket.io` for real-time communication. If you want to build a custom client, you can connect to the server using any Socket.IO client.
4
+
5
+ ## Connection
6
+
7
+ **URL**: `http://<server-ip>:<port>` (or `https://` if configured)
8
+
9
+ **Authentication**:
10
+ You must provide the `token` in the `auth` object during the handshake.
11
+
12
+ ```javascript
13
+ const socket = io('http://localhost:3000', {
14
+ auth: {
15
+ token: 'YOUR_SECRET_KEY',
16
+ },
17
+ });
18
+ ```
19
+
20
+ ## Events
21
+
22
+ ### Client -> Server
23
+
24
+ | Event | Data | Description |
25
+ | -------- | ------ | ------------------------------------------- |
26
+ | `deploy` | `null` | Triggers the configured deployment command. |
27
+
28
+ ### Server -> Client
29
+
30
+ | Event | Data Structure | Description |
31
+ | -------- | ----------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
32
+ | `status` | `{ status: string, message?: string, error?: string, timestamp: Date }` | Updates on the deployment state. Statuses: `started`, `completed`, `failed`. |
33
+ | `log` | `{ type: 'stdout' \| 'stderr', data: string, timestamp: Date }` | Real-time log output from the deployment process. |
34
+
35
+ ## Example Custom Client (Node.js)
36
+
37
+ ```javascript
38
+ import { io } from 'socket.io-client';
39
+
40
+ const socket = io('http://localhost:3000', {
41
+ auth: { token: 'my-secret' },
42
+ });
43
+
44
+ socket.on('connect', () => {
45
+ console.log('Connected!');
46
+ socket.emit('deploy');
47
+ });
48
+
49
+ socket.on('log', (data) => {
50
+ console.log(`[${data.type}] ${data.data}`);
51
+ });
52
+
53
+ socket.on('status', (data) => {
54
+ console.log(`Status: ${data.status}`);
55
+ if (data.status === 'completed' || data.status === 'failed') {
56
+ socket.close();
57
+ }
58
+ });
59
+ ```
@@ -0,0 +1,64 @@
1
+ # Troubleshooting Guide
2
+
3
+ ## Common Issues
4
+
5
+ ### 1. Connection Refused
6
+ **Error:** `Connection failed: connect ECONNREFUSED 127.0.0.1:3000`
7
+
8
+ **Possible Causes:**
9
+ - The server is not running.
10
+ - The server is running on a different port.
11
+ - The server is running inside Docker but the port is not mapped correctly.
12
+ - Firewall blocking the connection.
13
+
14
+ **Solutions:**
15
+ - Check if the server is running: `redep status` or `docker ps`.
16
+ - Verify the port: `redep config get server_port`.
17
+ - If using Docker, ensure `-p 3000:3000` is used.
18
+ - Check firewall settings.
19
+
20
+ ### 2. Authentication Failed
21
+ **Error:** `Authentication error: Invalid secret key` or `403 Forbidden`
22
+
23
+ **Possible Causes:**
24
+ - The `SECRET_KEY` on the client does not match the server's key.
25
+ - The `SECRET_KEY` was not set on the server.
26
+
27
+ **Solutions:**
28
+ - Verify client key: `redep config get servers.<name>.secret_key`.
29
+ - Verify server key: Check `SECRET_KEY` env var or `redep config get secret_key`.
30
+ - Regenerate keys if necessary: `redep generate secret_key` (on server) and update client.
31
+
32
+ ### 3. Deployment Command Failed
33
+ **Error:** `Deployment failed: Process exited with code 1`
34
+
35
+ **Possible Causes:**
36
+ - The `DEPLOYMENT_COMMAND` failed to execute successfully.
37
+ - Missing dependencies (e.g., `docker`, `npm`, `git`) in the server environment.
38
+ - Permission issues in `WORKING_DIR`.
39
+
40
+ **Solutions:**
41
+ - Check the streamed logs for the specific error message from the command.
42
+ - Ensure the `WORKING_DIR` exists and is writable.
43
+ - If running in Docker, ensure the necessary tools are installed or volumes are mounted correctly (e.g., `/var/run/docker.sock` for Docker commands).
44
+
45
+ ### 4. "socket hang up" or Disconnects
46
+ **Possible Causes:**
47
+ - Network instability.
48
+ - Server crashed during execution.
49
+ - Timeout settings on intermediate proxies (Nginx, etc.).
50
+
51
+ **Solutions:**
52
+ - Check server logs: `docker logs redep-server` or `pm2 logs redep-server`.
53
+ - If using a reverse proxy, ensure WebSocket support is enabled and timeouts are increased.
54
+
55
+ ### 5. Config Not Saving
56
+ **Error:** Changes to config don't persist.
57
+
58
+ **Possible Causes:**
59
+ - Permission issues with the config file location.
60
+ - Corrupted config file.
61
+
62
+ **Solutions:**
63
+ - Run `redep config path` to see where the config is stored.
64
+ - Try clearing config: `redep config clear`.
@@ -0,0 +1,59 @@
1
+ import { io } from 'socket.io-client';
2
+ import { logger } from '../logger.js';
3
+
4
+ export const connectAndDeploy = (serverUrl, secret) => {
5
+ return new Promise((resolve, reject) => {
6
+ logger.info(`Connecting to ${serverUrl}...`);
7
+
8
+ const socket = io(serverUrl, {
9
+ auth: {
10
+ token: secret,
11
+ },
12
+ reconnection: false, // Don't auto reconnect for a single command execution
13
+ });
14
+
15
+ socket.on('connect', () => {
16
+ logger.success('Connected to server. requesting deployment...');
17
+ socket.emit('deploy');
18
+ });
19
+
20
+ socket.on('connect_error', (err) => {
21
+ logger.error(`Connection error: ${err.message}`);
22
+ socket.close();
23
+ reject(err);
24
+ });
25
+
26
+ socket.on('status', (data) => {
27
+ const time = new Date(data.timestamp).toLocaleTimeString();
28
+
29
+ if (data.status === 'started') {
30
+ logger.info(`[${time}] Status: ${data.message || 'Deployment Started'}`);
31
+ } else if (data.status === 'completed') {
32
+ logger.success(`[${time}] Status: ${data.message || 'Deployment Completed'}`);
33
+ socket.close();
34
+ resolve();
35
+ } else if (data.status === 'failed') {
36
+ logger.error(`[${time}] Status: ${data.error || 'Deployment Failed'}`);
37
+ socket.close();
38
+ reject(new Error(data.error));
39
+ }
40
+ });
41
+
42
+ socket.on('log', (data) => {
43
+ const time = new Date(data.timestamp).toLocaleTimeString();
44
+ const msg = data.data.trim();
45
+ if (msg) {
46
+ if (data.type === 'stderr') {
47
+ console.error(`[${time}] [STDERR] ${msg}`);
48
+ } else {
49
+ console.log(`[${time}] [STDOUT] ${msg}`);
50
+ }
51
+ }
52
+ });
53
+
54
+ socket.on('disconnect', () => {
55
+ // If disconnected without completion/failure, it might be an issue
56
+ // But usually 'status' events close the socket.
57
+ });
58
+ });
59
+ };
@@ -0,0 +1,13 @@
1
+ import { connectAndDeploy } from './client.js';
2
+ import { logger } from '../logger.js';
3
+
4
+ export const deploy = async (serverName, serverUrl, secret) => {
5
+ logger.info(`Starting deployment sequence for target: ${serverName}`);
6
+
7
+ try {
8
+ await connectAndDeploy(serverUrl, secret);
9
+ } catch (err) {
10
+ logger.error(`Deployment failed: ${err.message}`);
11
+ throw err;
12
+ }
13
+ };
package/lib/config.js ADDED
@@ -0,0 +1,22 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({
4
+ projectName: 'redep',
5
+ encryptionKey: 'redep-secure-storage', // Obfuscates the config file
6
+ });
7
+
8
+ export const getConfig = (key) => {
9
+ return config.get(key);
10
+ };
11
+
12
+ export const setConfig = (key, value) => {
13
+ config.set(key, value);
14
+ };
15
+
16
+ export const clearConfig = () => {
17
+ config.clear();
18
+ };
19
+
20
+ export const getAllConfig = () => {
21
+ return config.store;
22
+ };
package/lib/logger.js ADDED
@@ -0,0 +1,9 @@
1
+ import chalk from 'chalk';
2
+
3
+ export const logger = {
4
+ info: (msg) => console.log(chalk.blue('ℹ') + ' ' + msg),
5
+ success: (msg) => console.log(chalk.green('✔') + ' ' + msg),
6
+ warn: (msg) => console.log(chalk.yellow('⚠') + ' ' + msg),
7
+ error: (msg) => console.error(chalk.red('✖') + ' ' + msg),
8
+ log: (msg) => console.log(msg),
9
+ };
@@ -0,0 +1,46 @@
1
+ import { spawn } from 'child_process';
2
+ import { logger } from '../logger.js';
3
+
4
+ export const spawnCommand = (command, workingDir, onStdout, onStderr) => {
5
+ return new Promise((resolve, reject) => {
6
+ logger.info(`Executing: ${command} in ${workingDir}`);
7
+
8
+ // Parse command string into command and args for spawn
9
+ // This is a simple split, for complex commands with quotes, we might need a parser or use shell: true
10
+ // Using shell: true is easier for compatibility with the existing full command strings
11
+ const child = spawn(command, {
12
+ cwd: workingDir,
13
+ shell: true,
14
+ stdio: ['ignore', 'pipe', 'pipe'],
15
+ });
16
+
17
+ if (child.stdout) {
18
+ child.stdout.on('data', (data) => {
19
+ const output = data.toString();
20
+ if (onStdout) onStdout(output);
21
+ });
22
+ }
23
+
24
+ if (child.stderr) {
25
+ child.stderr.on('data', (data) => {
26
+ const output = data.toString();
27
+ if (onStderr) onStderr(output);
28
+ });
29
+ }
30
+
31
+ child.on('error', (error) => {
32
+ logger.error(`Execution error: ${error.message}`);
33
+ reject(error);
34
+ });
35
+
36
+ child.on('close', (code) => {
37
+ if (code === 0) {
38
+ logger.success('Execution successful');
39
+ resolve();
40
+ } else {
41
+ logger.error(`Process exited with code ${code}`);
42
+ reject(new Error(`Process exited with code ${code}`));
43
+ }
44
+ });
45
+ });
46
+ };
@@ -0,0 +1,23 @@
1
+ import { createServer } from 'http';
2
+ import { createApp, setupSocket } from './server.js';
3
+ import { logger } from '../logger.js';
4
+
5
+ export const startServer = (port, secret, workingDir, deploymentCommand) => {
6
+ if (!secret) {
7
+ logger.error('Cannot start server: Secret key is required for security.');
8
+ process.exit(1);
9
+ }
10
+
11
+ const app = createApp();
12
+ const httpServer = createServer(app);
13
+
14
+ // Attach Socket.io
15
+ setupSocket(httpServer, secret, workingDir, deploymentCommand);
16
+
17
+ httpServer.listen(port, () => {
18
+ logger.success(`Server is running on port ${port}`);
19
+ logger.info(`Working Directory: ${workingDir}`);
20
+ logger.info(`Deployment Command: ${deploymentCommand}`);
21
+ logger.info(`Waiting for connections...`);
22
+ });
23
+ };
@@ -0,0 +1,70 @@
1
+ import express from 'express';
2
+ import { Server } from 'socket.io';
3
+ import helmet from 'helmet';
4
+ import cors from 'cors';
5
+ import { spawnCommand } from './executor.js';
6
+ import { logger } from '../logger.js';
7
+
8
+ export const createApp = () => {
9
+ const app = express();
10
+ app.use(helmet());
11
+ app.use(cors());
12
+
13
+ app.get('/health', (req, res) => res.status(200).json({ status: 'ok' }));
14
+
15
+ return app;
16
+ };
17
+
18
+ export const setupSocket = (httpServer, secretKey, workingDir, deploymentCommand) => {
19
+ const io = new Server(httpServer, {
20
+ cors: {
21
+ origin: '*',
22
+ methods: ['GET', 'POST'],
23
+ },
24
+ });
25
+
26
+ // Authentication Middleware
27
+ io.use((socket, next) => {
28
+ const token = socket.handshake.auth.token;
29
+ if (token === secretKey) {
30
+ next();
31
+ } else {
32
+ logger.warn(`Authentication failed for socket: ${socket.id}`);
33
+ next(new Error('Authentication error: Invalid secret key'));
34
+ }
35
+ });
36
+
37
+ io.on('connection', (socket) => {
38
+ logger.info(`Client connected: ${socket.id}`);
39
+
40
+ socket.on('deploy', async () => {
41
+ logger.info(`Starting deployment sequence triggered by ${socket.id}`);
42
+ socket.emit('status', { status: 'started', message: 'Deployment started', timestamp: new Date() });
43
+
44
+ try {
45
+ await spawnCommand(
46
+ deploymentCommand,
47
+ workingDir,
48
+ (log) => {
49
+ socket.emit('log', { type: 'stdout', data: log, timestamp: new Date() });
50
+ },
51
+ (log) => {
52
+ socket.emit('log', { type: 'stderr', data: log, timestamp: new Date() });
53
+ }
54
+ );
55
+
56
+ socket.emit('status', { status: 'completed', message: 'Deployment finished successfully', timestamp: new Date() });
57
+ logger.success('Deployment completed successfully');
58
+ } catch (error) {
59
+ socket.emit('status', { status: 'failed', error: error.message, timestamp: new Date() });
60
+ logger.error(`Deployment failed: ${error.message}`);
61
+ }
62
+ });
63
+
64
+ socket.on('disconnect', () => {
65
+ logger.info(`Client disconnected: ${socket.id}`);
66
+ });
67
+ });
68
+
69
+ return io;
70
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "redep",
3
+ "author": "nafies1",
4
+ "version": "2.0.0",
5
+ "main": "index.js",
6
+ "type": "module",
7
+ "bin": {
8
+ "redep": "./bin/index.js"
9
+ },
10
+ "scripts": {
11
+ "test": "echo \"Error: no test specified\" && exit 1",
12
+ "update": "node scripts/update.js",
13
+ "format": "prettier --write .",
14
+ "format:check": "prettier --check .",
15
+ "sonar": "node sonar-project.js"
16
+ },
17
+ "keywords": [
18
+ "deployment",
19
+ "cli",
20
+ "remote-execution",
21
+ "docker",
22
+ "streaming",
23
+ "websocket",
24
+ "devops"
25
+ ],
26
+ "homepage": "https://github.com/nafies1/redep#readme",
27
+ "bugs": {
28
+ "url": "https://github.com/nafies1/redep/issues"
29
+ },
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/nafies1/redep.git"
33
+ },
34
+ "license": "ISC",
35
+ "description": "Remote execution CLI for deployment",
36
+ "dependencies": {
37
+ "axios": "^1.7.9",
38
+ "body-parser": "^1.20.2",
39
+ "chalk": "^5.3.0",
40
+ "commander": "^12.0.0",
41
+ "conf": "^12.0.0",
42
+ "cors": "^2.8.5",
43
+ "dotenv": "^17.2.3",
44
+ "express": "^4.18.2",
45
+ "helmet": "^7.1.0",
46
+ "inquirer": "^13.2.0",
47
+ "morgan": "^1.10.0",
48
+ "pm2": "^6.0.14"
49
+ },
50
+ "devDependencies": {
51
+ "@sonar/scan": "^4.3.4",
52
+ "prettier": "^3.8.0",
53
+ "sonarqube-scanner": "^4.3.4"
54
+ },
55
+ "redep": {
56
+ "secret_key": "4k94jLuEIT_jWPHJYPActmGxn2x72eR0",
57
+ "working_dir": "C:\\Users\\nafie\\Documents\\trae_projects\\remote-deploy-cli"
58
+ }
59
+ }
@@ -0,0 +1,89 @@
1
+ import fs from 'fs';
2
+ import { execSync } from 'child_process';
3
+ import path from 'path';
4
+
5
+ // Get arguments from command line
6
+ // Usage: node scripts/update.js <updateType> <commitMessage>
7
+ const updateType = process.argv[2];
8
+ const commitMessage = process.argv[3];
9
+
10
+ // Valid update types
11
+ const VALID_TYPES = ['patch', 'minor', 'major'];
12
+
13
+ // --- Validation ---
14
+
15
+ if (!updateType || !VALID_TYPES.includes(updateType)) {
16
+ console.error(`Error: Invalid or missing updateType. Must be one of: ${VALID_TYPES.join(', ')}`);
17
+ process.exit(1);
18
+ }
19
+
20
+ if (!commitMessage) {
21
+ console.error('Error: Missing commitMessage.');
22
+ process.exit(1);
23
+ }
24
+
25
+ const packageJsonPath = path.resolve('package.json');
26
+
27
+ if (!fs.existsSync(packageJsonPath)) {
28
+ console.error('Error: package.json not found.');
29
+ process.exit(1);
30
+ }
31
+
32
+ // --- Version Update Logic ---
33
+
34
+ try {
35
+ const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
36
+
37
+ if (!packageJson.version) {
38
+ console.error('Error: package.json does not contain a "version" field.');
39
+ process.exit(1);
40
+ }
41
+
42
+ const currentVersion = packageJson.version;
43
+ const versionParts = currentVersion.split('.').map(Number);
44
+
45
+ if (versionParts.length !== 3 || versionParts.some(isNaN)) {
46
+ console.error(`Error: Invalid semantic version format in package.json: ${currentVersion}`);
47
+ process.exit(1);
48
+ }
49
+
50
+ let [major, minor, patch] = versionParts;
51
+
52
+ switch (updateType) {
53
+ case 'major':
54
+ major++;
55
+ minor = 0;
56
+ patch = 0;
57
+ break;
58
+ case 'minor':
59
+ minor++;
60
+ patch = 0;
61
+ break;
62
+ case 'patch':
63
+ patch++;
64
+ break;
65
+ }
66
+
67
+ const newVersion = `${major}.${minor}.${patch}`;
68
+ packageJson.version = newVersion;
69
+
70
+ // Write updated package.json
71
+ fs.writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + '\n');
72
+ console.log(`Version updated: ${currentVersion} -> ${newVersion}`);
73
+
74
+ // --- Git Operations ---
75
+
76
+ console.log('Staging changes...');
77
+ execSync('git add .', { stdio: 'inherit' });
78
+
79
+ console.log(`Committing with message: "${commitMessage}"...`);
80
+ execSync(`git commit -m "${commitMessage}"`, { stdio: 'inherit' });
81
+
82
+ console.log('Pushing to origin main...');
83
+ execSync('git push origin main', { stdio: 'inherit' });
84
+
85
+ console.log('Update process completed successfully!');
86
+ } catch (error) {
87
+ console.error(`Error during update process: ${error.message}`);
88
+ process.exit(1);
89
+ }
@@ -0,0 +1,28 @@
1
+ import { startServer } from './lib/server/index.js';
2
+ import { logger } from './lib/logger.js';
3
+ import { getConfig } from './lib/config.js';
4
+ import 'dotenv/config';
5
+
6
+ // Dedicated entry point for PM2 to ensure clean ESM handling
7
+ // PM2 sometimes struggles with CLI binaries directly in ESM mode
8
+
9
+ const port = process.env.SERVER_PORT || getConfig('server_port') || 3000;
10
+ const secret = process.env.SECRET_KEY || getConfig('secret_key');
11
+ const workingDir = process.env.WORKING_DIR || getConfig('working_dir');
12
+ const deploymentCommand = process.env.DEPLOYMENT_COMMAND || getConfig('deployment_command');
13
+
14
+ if (!secret) {
15
+ logger.warn('Warning: No "secret_key" set. Communication might be insecure.');
16
+ }
17
+
18
+ if (!workingDir) {
19
+ logger.error('Error: "working_dir" is not set.');
20
+ process.exit(1);
21
+ }
22
+
23
+ if (!deploymentCommand) {
24
+ logger.error('Error: "deployment_command" is not set.');
25
+ process.exit(1);
26
+ }
27
+
28
+ startServer(port, secret, workingDir, deploymentCommand);
@@ -0,0 +1,23 @@
1
+ import { createRequire } from 'module';
2
+ import pkg from './package.json' with { type: 'json' };
3
+
4
+ const require = createRequire(import.meta.url);
5
+ const sonarScannerModule = require('@sonar/scan');
6
+ const sonarScanner = sonarScannerModule.scan || sonarScannerModule.default || sonarScannerModule;
7
+
8
+ sonarScanner(
9
+ {
10
+ serverUrl: process.env.SONAR_HOST_URL || 'http://localhost:9000',
11
+ token: process.env.SONAR_TOKEN,
12
+ options: {
13
+ 'sonar.projectKey': 'remote-deploy-cli',
14
+ 'sonar.projectName': 'Remote Deploy CLI',
15
+ 'sonar.projectVersion': pkg.version,
16
+ 'sonar.sources': 'bin,lib',
17
+ 'sonar.tests': 'test',
18
+ 'sonar.javascript.lcov.reportPaths': 'coverage/lcov.info',
19
+ 'sonar.sourceEncoding': 'UTF-8',
20
+ },
21
+ },
22
+ () => process.exit()
23
+ );
Binary file