remote-deploy-cli 1.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,31 @@
1
+ name: Publish to npm
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+
8
+ permissions:
9
+ contents: read
10
+ packages: write
11
+
12
+ jobs:
13
+ publish:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - name: Checkout code
17
+ uses: actions/checkout@v4
18
+
19
+ - name: Setup Node.js
20
+ uses: actions/setup-node@v4
21
+ with:
22
+ node-version: '22'
23
+ registry-url: 'https://registry.npmjs.org'
24
+
25
+ - name: Install dependencies
26
+ run: npm ci
27
+
28
+ - name: Publish to npm
29
+ run: npm publish
30
+ env:
31
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/Dockerfile ADDED
@@ -0,0 +1,23 @@
1
+ FROM node:18-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
+ # Start the server listener by default
23
+ CMD ["node", "bin/index.js", "listen"]
package/README.md ADDED
@@ -0,0 +1,119 @@
1
+ # redep (Remote Deploy CLI)
2
+
3
+ A lightweight Node.js CLI tool for remote execution using a Client-Server architecture. Designed to simplify deployment workflows (e.g., triggering Docker Compose on a remote server).
4
+
5
+ ## Features
6
+
7
+ - **Client-Server Architecture**: Centralized control where Client sends commands and Server executes them.
8
+ - **Secure Communication**: Uses Token-based authentication (Bearer Token) to ensure only authorized clients can execute commands.
9
+ - **Configurable**: Easy configuration management for both Client and Server via CLI or Environment Variables.
10
+ - **Docker Integration**: Built-in support for `deploy fe` which pulls and restarts containers using Docker Compose.
11
+ - **Docker Support**: Can be run as a Docker container itself on the server machine.
12
+ - **Logging**: Detailed logs for tracking execution status.
13
+
14
+ ## Architecture
15
+
16
+ 1. **Client Machine**: The client that issues deployment commands (e.g., your laptop or CI runner).
17
+ 2. **Server Machine**: The server that hosts the application and executes the deployment commands (e.g., `docker compose up`).
18
+
19
+ Communication happens via HTTP/REST with a shared secret for authentication.
20
+
21
+ ## Installation
22
+
23
+ You can install this package globally using npm:
24
+
25
+ ```bash
26
+ # Clone the repository
27
+ git clone <repo-url>
28
+ cd remote-deploy-cli
29
+
30
+ # Install globally (for development/local use)
31
+ npm install -g .
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ ### 1. Configure the Server Machine
37
+
38
+ #### Option A: Running directly on Node.js
39
+
40
+ On the machine where you want to run your application (Server):
41
+
42
+ 1. **Set the Working Directory**: This is where your `docker-compose.yml` is located.
43
+ ```bash
44
+ redep config set working_dir /path/to/your/project
45
+ ```
46
+
47
+ 2. **Set a Secret Key**: A secure password for authentication.
48
+ ```bash
49
+ redep config set secret_key super-secure-secret-123
50
+ ```
51
+
52
+ 3. **Start the Listener**:
53
+ ```bash
54
+ redep listen
55
+ # Optional: Specify port (default 3000)
56
+ # redep listen --port 4000
57
+ ```
58
+
59
+ #### Option B: Running with Docker (Recommended)
60
+
61
+ You can run the server itself inside a Docker container. We provide a `docker-compose.server.yml` as an example.
62
+
63
+ 1. **Prerequisites**: Ensure Docker and Docker Compose are installed on the server machine.
64
+ 2. **Run the Server**:
65
+
66
+ ```bash
67
+ # Edit docker-compose.server.yml to map your project directory
68
+ # Then run:
69
+ docker compose -f docker-compose.server.yml up -d --build
70
+ ```
71
+
72
+ **Configuration via `docker-compose.server.yml`**:
73
+ - `volumes`:
74
+ - `/var/run/docker.sock:/var/run/docker.sock`: Required to allow the server to manage sibling containers.
75
+ - `./your-project:/workspace`: Mount your project directory containing `docker-compose.yml` to `/workspace` inside the container.
76
+ - `environment`:
77
+ - `WORKING_DIR=/workspace`: Must match the container mount path.
78
+ - `SECRET_KEY=your-secret-key`: Set the shared secret.
79
+
80
+ ### 2. Configure the Client Machine
81
+
82
+ On your local machine or CI/CD server (Client):
83
+
84
+ 1. **Set the Server URL**: The address of your server machine.
85
+ ```bash
86
+ redep config set server_url http://<server-ip>:3000
87
+ ```
88
+ Or use `SERVER_URL` env var.
89
+
90
+ 2. **Set the Secret Key**: Must match the key set on the Server.
91
+ ```bash
92
+ redep config set secret_key super-secure-secret-123
93
+ ```
94
+ Or use `SECRET_KEY` env var.
95
+
96
+ ### 3. Execute Deployment
97
+
98
+ To deploy the Frontend (triggers `docker compose up -d` on the server):
99
+
100
+ ```bash
101
+ redep deploy fe
102
+ ```
103
+
104
+ ## Security Best Practices
105
+
106
+ - **Secret Management**: Ensure your `secret_key` is strong and not committed to version control.
107
+ - **Network Security**: In production, it is recommended to run the Server behind a reverse proxy (like Nginx) with SSL/TLS (HTTPS) to encrypt the traffic, especially since the secret key is sent in the header.
108
+ - **Firewall**: Restrict access to the Server port (default 3000) to only allow IPs from known Client machines.
109
+
110
+ ## Development
111
+
112
+ - `bin/index.js`: CLI entry point.
113
+ - `lib/server/`: Server and execution logic.
114
+ - `lib/client/`: Client and command logic.
115
+ - `lib/config.js`: Configuration management using `conf`.
116
+
117
+ ## License
118
+
119
+ ISC
package/bin/index.js ADDED
@@ -0,0 +1,105 @@
1
+ #!/usr/bin/env node
2
+
3
+ import 'dotenv/config';
4
+ import { Command } from 'commander';
5
+ import { logger } from '../lib/logger.js';
6
+ import { getConfig, setConfig, getAllConfig, clearConfig } from '../lib/config.js';
7
+ import { startServer } from '../lib/server/index.js';
8
+ import { deploy } from '../lib/client/index.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('redep')
14
+ .description('Remote execution CLI for deployment')
15
+ .version('1.0.0');
16
+
17
+ // Configuration Command
18
+ const configCommand = new Command('config')
19
+ .description('Manage configuration');
20
+
21
+ configCommand
22
+ .command('set <key> <value>')
23
+ .description('Set a configuration key')
24
+ .action((key, value) => {
25
+ setConfig(key, value);
26
+ logger.success(`Configuration updated: ${key} = ${value}`);
27
+ });
28
+
29
+ configCommand
30
+ .command('get <key>')
31
+ .description('Get a configuration key')
32
+ .action((key) => {
33
+ const value = getConfig(key);
34
+ logger.info(`${key}: ${value}`);
35
+ });
36
+
37
+ configCommand
38
+ .command('list')
39
+ .description('List all configurations')
40
+ .action(() => {
41
+ const all = getAllConfig();
42
+ logger.info('Current Configuration:');
43
+ console.table(all);
44
+ });
45
+
46
+ configCommand
47
+ .command('clear')
48
+ .description('Clear all configurations')
49
+ .action(() => {
50
+ clearConfig();
51
+ logger.success('All configurations have been cleared.');
52
+ });
53
+
54
+ program.addCommand(configCommand);
55
+
56
+ // Server Command
57
+ program
58
+ .command('listen')
59
+ .description('Start the server to listen for commands')
60
+ .option('-p, --port <port>', 'Port to listen on', 3000)
61
+ .action((options) => {
62
+ const port = options.port || process.env.SERVER_PORT || getConfig('server_port') || 3000;
63
+ const secret = process.env.SECRET_KEY || getConfig('secret_key');
64
+
65
+ if (!secret) {
66
+ logger.warn('Warning: No "secret_key" set in config or SECRET_KEY env var. Communication might be insecure or fail if client requires it.');
67
+ logger.info('Run "redep config set secret_key <your-secret>" or set SECRET_KEY env var.');
68
+ }
69
+
70
+ const workingDir = process.env.WORKING_DIR || getConfig('working_dir');
71
+ if (!workingDir) {
72
+ logger.error('Error: "working_dir" is not set. Please set it using "redep config set working_dir <path>" or WORKING_DIR env var.');
73
+ process.exit(1);
74
+ }
75
+
76
+ startServer(port, secret, workingDir);
77
+ });
78
+
79
+ // Client Command
80
+ program
81
+ .command('deploy <type>')
82
+ .description('Deploy a service (e.g., "fe") to the server machine')
83
+ .action(async (type) => {
84
+ const serverUrl = process.env.SERVER_URL || getConfig('server_url');
85
+ const secret = process.env.SECRET_KEY || getConfig('secret_key');
86
+
87
+ if (!serverUrl) {
88
+ logger.error('Error: "server_url" is not set. Set SERVER_URL env var or run "redep config set server_url <url>"');
89
+ process.exit(1);
90
+ }
91
+
92
+ if (!secret) {
93
+ logger.error('Error: "secret_key" is not set. Set SECRET_KEY env var or run "redep config set secret_key <your-secret>"');
94
+ process.exit(1);
95
+ }
96
+
97
+ try {
98
+ await deploy(type, serverUrl, secret);
99
+ } catch (error) {
100
+ logger.error(`Deploy failed: ${error.message}`);
101
+ process.exit(1);
102
+ }
103
+ });
104
+
105
+ 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,26 @@
1
+ import axios from 'axios';
2
+
3
+ export const sendCommand = async (url, endpoint, secret, data) => {
4
+ try {
5
+ // Ensure url has no trailing slash and endpoint has leading slash
6
+ const cleanUrl = url.replace(/\/$/, '');
7
+ const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
8
+ const fullUrl = `${cleanUrl}${cleanEndpoint}`;
9
+
10
+ const response = await axios.post(fullUrl, data, {
11
+ headers: {
12
+ 'Authorization': `Bearer ${secret}`,
13
+ 'Content-Type': 'application/json'
14
+ }
15
+ });
16
+ return response.data;
17
+ } catch (error) {
18
+ if (error.response) {
19
+ throw new Error(`Server error (${error.response.status}): ${JSON.stringify(error.response.data)}`);
20
+ } else if (error.request) {
21
+ throw new Error(`Connection failed: No response from ${url}. Is the server running?`);
22
+ } else {
23
+ throw new Error(`Request error: ${error.message}`);
24
+ }
25
+ }
26
+ };
@@ -0,0 +1,31 @@
1
+ import { sendCommand } from './client.js';
2
+ import { logger } from '../logger.js';
3
+
4
+ export const deploy = async (type, serverUrl, secret) => {
5
+ logger.info(`Starting deployment sequence for target: ${type}`);
6
+
7
+ if (type === 'fe') {
8
+ logger.info('Sending deployment instruction to server machine...');
9
+
10
+ try {
11
+ const result = await sendCommand(serverUrl, '/deploy/fe', secret, {});
12
+
13
+ if (result.status === 'success') {
14
+ logger.success('Server successfully executed the deployment command.');
15
+ logger.log('--- Remote Output ---');
16
+ if (result.output.stdout) console.log(result.output.stdout);
17
+ if (result.output.stderr) console.log(result.output.stderr);
18
+ logger.log('---------------------');
19
+ } else {
20
+ logger.error('Server reported failure.');
21
+ console.error(result);
22
+ }
23
+ } catch (err) {
24
+ throw err;
25
+ }
26
+
27
+ } else {
28
+ logger.error(`Deployment type "${type}" is not supported yet.`);
29
+ throw new Error(`Unsupported deployment type: ${type}`);
30
+ }
31
+ };
package/lib/config.js ADDED
@@ -0,0 +1,21 @@
1
+ import Conf from 'conf';
2
+
3
+ const config = new Conf({
4
+ projectName: 'redep'
5
+ });
6
+
7
+ export const getConfig = (key) => {
8
+ return config.get(key);
9
+ };
10
+
11
+ export const setConfig = (key, value) => {
12
+ config.set(key, value);
13
+ };
14
+
15
+ export const clearConfig = () => {
16
+ config.clear();
17
+ };
18
+
19
+ export const getAllConfig = () => {
20
+ return config.store;
21
+ };
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,23 @@
1
+ import { exec } from 'child_process';
2
+ import { logger } from '../logger.js';
3
+
4
+ export const executeCommand = (command, workingDir) => {
5
+ return new Promise((resolve, reject) => {
6
+ logger.info(`Executing: ${command} in ${workingDir}`);
7
+
8
+ exec(command, { cwd: workingDir }, (error, stdout, stderr) => {
9
+ if (error) {
10
+ logger.error(`Execution error: ${error.message}`);
11
+ return reject({ error: error.message, stderr });
12
+ }
13
+
14
+ if (stderr) {
15
+ // Docker often outputs to stderr even for info, so we log it but don't fail unless error is set
16
+ logger.warn(`Stderr: ${stderr}`);
17
+ }
18
+
19
+ logger.success('Execution successful');
20
+ resolve({ stdout, stderr });
21
+ });
22
+ });
23
+ };
@@ -0,0 +1,17 @@
1
+ import { createServer } from './server.js';
2
+ import { logger } from '../logger.js';
3
+
4
+ export const startServer = (port, secret, workingDir) => {
5
+ if (!secret) {
6
+ logger.error('Cannot start server: Secret key is required for security.');
7
+ process.exit(1);
8
+ }
9
+
10
+ const app = createServer(secret, workingDir);
11
+
12
+ app.listen(port, () => {
13
+ logger.success(`Server is running on port ${port}`);
14
+ logger.info(`Working Directory: ${workingDir}`);
15
+ logger.info(`Waiting for commands...`);
16
+ });
17
+ };
@@ -0,0 +1,63 @@
1
+ import express from 'express';
2
+ import bodyParser from 'body-parser';
3
+ import cors from 'cors';
4
+ import helmet from 'helmet';
5
+ import morgan from 'morgan';
6
+ import { logger } from '../logger.js';
7
+ import { executeCommand } from './executor.js';
8
+
9
+ export const createServer = (secretKey, workingDir) => {
10
+ const app = express();
11
+
12
+ app.use(helmet());
13
+ app.use(cors());
14
+ app.use(bodyParser.json());
15
+ app.use(morgan('tiny'));
16
+
17
+ // Auth Middleware
18
+ const authenticate = (req, res, next) => {
19
+ const authHeader = req.headers['authorization'];
20
+ if (!authHeader) {
21
+ return res.status(401).json({ error: 'Missing Authorization header' });
22
+ }
23
+
24
+ const token = authHeader.replace('Bearer ', '');
25
+ if (token !== secretKey) {
26
+ return res.status(403).json({ error: 'Invalid token' });
27
+ }
28
+ next();
29
+ };
30
+
31
+ app.post('/execute', authenticate, async (req, res) => {
32
+ const { command } = req.body;
33
+
34
+ if (!command) {
35
+ return res.status(400).json({ error: 'Missing command' });
36
+ }
37
+
38
+ try {
39
+ const result = await executeCommand(command, workingDir);
40
+ res.json({ status: 'success', output: result });
41
+ } catch (error) {
42
+ res.status(500).json({ status: 'error', error: error });
43
+ }
44
+ });
45
+
46
+ // Specific endpoint for deploy fe to map the requirement exactly
47
+ app.post('/deploy/fe', authenticate, async (req, res) => {
48
+ logger.info('Received deploy fe request');
49
+
50
+ // Requirement: cd to working dir and run docker compose up -d
51
+ // We use 'docker compose pull && docker compose up -d' to ensure latest image is used
52
+ const command = 'docker compose pull && docker compose up -d';
53
+
54
+ try {
55
+ const result = await executeCommand(command, workingDir);
56
+ res.json({ status: 'success', output: result });
57
+ } catch (error) {
58
+ res.status(500).json({ status: 'error', error: error });
59
+ }
60
+ });
61
+
62
+ return app;
63
+ };
package/package.json ADDED
@@ -0,0 +1,28 @@
1
+ {
2
+ "name": "remote-deploy-cli",
3
+ "version": "1.0.0",
4
+ "main": "index.js",
5
+ "type": "module",
6
+ "bin": {
7
+ "redep": "./bin/index.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [],
13
+ "author": "",
14
+ "license": "ISC",
15
+ "description": "Remote execution CLI for deployment",
16
+ "dependencies": {
17
+ "axios": "^1.7.9",
18
+ "body-parser": "^1.20.2",
19
+ "chalk": "^5.3.0",
20
+ "commander": "^12.0.0",
21
+ "conf": "^12.0.0",
22
+ "cors": "^2.8.5",
23
+ "dotenv": "^17.2.3",
24
+ "express": "^4.18.2",
25
+ "helmet": "^7.1.0",
26
+ "morgan": "^1.10.0"
27
+ }
28
+ }
Binary file