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 +7 -0
- package/.github/workflows/ci-cd.yml +69 -0
- package/.prettierignore +9 -0
- package/.prettierrc +7 -0
- package/Dockerfile +27 -0
- package/README.md +172 -0
- package/bin/index.js +386 -0
- package/docker-compose.server.yml +22 -0
- package/docs/ADVANCED_CONFIG.md +60 -0
- package/docs/API.md +59 -0
- package/docs/TROUBLESHOOTING.md +64 -0
- package/lib/client/client.js +59 -0
- package/lib/client/index.js +13 -0
- package/lib/config.js +22 -0
- package/lib/logger.js +9 -0
- package/lib/server/executor.js +46 -0
- package/lib/server/index.js +23 -0
- package/lib/server/server.js +70 -0
- package/package.json +59 -0
- package/scripts/update.js +89 -0
- package/server-entry.js +28 -0
- package/sonar-project.js +23 -0
- package/test-target/docker-compose.yml +0 -0
package/.env.example
ADDED
|
@@ -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 }}
|
package/.prettierignore
ADDED
package/.prettierrc
ADDED
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
|
+
[](https://www.npmjs.com/package/redep)
|
|
6
|
+
[](https://hub.docker.com/r/nafies1/redep)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
[](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
|
+
}
|
package/server-entry.js
ADDED
|
@@ -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);
|
package/sonar-project.js
ADDED
|
@@ -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
|