redep 2.0.2 → 2.1.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/.dockerignore ADDED
@@ -0,0 +1,63 @@
1
+ # Node.js artifacts
2
+ node_modules
3
+ npm-debug.log*
4
+ yarn-debug.log*
5
+ yarn-error.log*
6
+
7
+ # Build and dependency artifacts
8
+ coverage/
9
+ dist/
10
+ build/
11
+ *.tgz
12
+ *.tar.gz
13
+
14
+ # Development and IDE files
15
+ .git/
16
+ .gitignore
17
+ .gitattributes
18
+ .vscode/
19
+ .idea/
20
+ *.swp
21
+ *.swo
22
+ *~
23
+
24
+ # Environment and configuration
25
+ .env
26
+ .env.*
27
+ !.env.example
28
+ .env.local
29
+ .env.development.local
30
+ .env.test.local
31
+ .env.production.local
32
+
33
+ # Documentation and licenses
34
+ README.md
35
+ LICENSE
36
+ docs/
37
+ CHANGELOG.md
38
+ CONTRIBUTING.md
39
+
40
+ # Test files
41
+ test-target/
42
+ *.test.js
43
+ *.spec.js
44
+ __tests__/
45
+ __mocks__/
46
+
47
+ # Log files
48
+ *.log
49
+ logs/
50
+
51
+ # Docker files themselves
52
+ Dockerfile
53
+ .dockerignore
54
+
55
+ # OS and backup files
56
+ .DS_Store
57
+ Thumbs.db
58
+
59
+ # Package manager files
60
+ package-lock.json
61
+ npm-shrinkwrap.json
62
+ yarn.lock
63
+ sonar-project.js
@@ -46,6 +46,14 @@ jobs:
46
46
  tags: ${{ steps.meta.outputs.tags }}
47
47
  labels: ${{ steps.meta.outputs.labels }}
48
48
 
49
+ - name: Update Docker Hub Description
50
+ uses: peter-evans/dockerhub-description@v4
51
+ with:
52
+ username: ${{ secrets.DOCKER_USERNAME }}
53
+ password: ${{ secrets.DOCKER_PASSWORD }}
54
+ repository: nafies1/redep
55
+ readme-filepath: ./docs/DOCKER.md
56
+
49
57
  npm:
50
58
  name: Publish to npm
51
59
  runs-on: ubuntu-latest
package/Dockerfile CHANGED
@@ -1,27 +1,57 @@
1
- FROM node:25.3.0-alpine
1
+ # Stage 1: Dependencies
2
+ FROM node:20-alpine AS deps
3
+ WORKDIR /app
4
+
5
+ # Copy package files to install dependencies
6
+ COPY package.json ./
2
7
 
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
8
+ # Install only production dependencies for smaller image size
9
+ # First try with package-lock.json, fallback to npm install if not available
10
+ RUN if [ -f package-lock.json ]; then \
11
+ npm ci --only=production; \
12
+ else \
13
+ npm install --only=production; \
14
+ fi && \
15
+ npm cache clean --force
6
16
 
17
+ # Stage 2: Runtime
18
+ FROM node:20-alpine AS runner
7
19
  WORKDIR /app
8
20
 
9
- # Install app dependencies
10
- COPY package*.json ./
11
- RUN npm install --production
21
+ # Install minimal Docker CLI and dumb-init (remove compose plugin if not essential)
22
+ RUN apk add --no-cache \
23
+ docker-cli \
24
+ dumb-init && \
25
+ # Clean up apk cache to save space
26
+ rm -rf /var/cache/apk/*
27
+
28
+ # Set environment to production
29
+ ENV NODE_ENV=production
12
30
 
13
- # Bundle app source
14
- COPY . .
31
+ # Copy dependencies from deps stage
32
+ COPY --from=deps /app/node_modules ./node_modules
33
+
34
+ # Copy only essential application files (avoid copying everything)
35
+ COPY package.json ./
36
+ COPY bin/ ./bin/
37
+ COPY lib/ ./lib/
15
38
 
16
39
  # Make the CLI executable
17
- RUN chmod +x bin/index.js
40
+ RUN chmod +x bin/index.js && \
41
+ # Remove development files that might have been copied
42
+ find . -name "*.md" -delete && \
43
+ find . -name "*.test.js" -delete && \
44
+ find . -name "test" -type d -exec rm -rf {} + 2>/dev/null || true
18
45
 
19
- # Expose the default port
46
+ # Expose the port the server listens on
20
47
  EXPOSE 3000
21
48
 
22
- # Health check
23
- HEALTHCHECK --interval=30s --timeout=3s \
24
- CMD wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1
49
+ # Health check to ensure the server is responsive
50
+ HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
51
+ CMD wget --no-verbose --tries=1 --spider http://localhost:${SERVER_PORT:-3000}/health || exit 1
52
+
53
+ # Use dumb-init as the entrypoint to handle signals
54
+ ENTRYPOINT ["/usr/bin/dumb-init", "--"]
25
55
 
26
- # Start the server listener by default
27
- CMD ["node", "bin/index.js", "listen"]
56
+ # Default command to start the server
57
+ CMD ["node", "bin/index.js", "listen"]
package/README.md CHANGED
@@ -49,6 +49,8 @@ The server is the agent that runs on your remote machine and executes the deploy
49
49
 
50
50
  #### Option A: Using Docker (Recommended)
51
51
 
52
+ **Linux / macOS (Bash):**
53
+
52
54
  ```bash
53
55
  docker run -d \
54
56
  --name redep-server \
@@ -62,6 +64,21 @@ docker run -d \
62
64
  nafies1/redep:latest
63
65
  ```
64
66
 
67
+ **Windows (PowerShell):**
68
+
69
+ ```powershell
70
+ docker run -d `
71
+ --name redep-server `
72
+ --restart always `
73
+ -p 3000:3000 `
74
+ -v /var/run/docker.sock:/var/run/docker.sock `
75
+ -v ${PWD}:/app/workspace `
76
+ -e SECRET_KEY=your-super-secret-key `
77
+ -e WORKING_DIR=/app/workspace `
78
+ -e DEPLOYMENT_COMMAND="docker compose pull && docker compose up -d" `
79
+ nafies1/redep:latest
80
+ ```
81
+
65
82
  #### Option B: Using npm & PM2
66
83
 
67
84
  ```bash
@@ -94,6 +111,7 @@ redep deploy prod
94
111
  ```
95
112
 
96
113
  You will see:
114
+
97
115
  ```text
98
116
  (INFO) Connecting to http://your-server-ip:3000...
99
117
  (SUCCESS) Connected to server. requesting deployment...
@@ -108,6 +126,7 @@ You will see:
108
126
  ## ⚙️ Configuration
109
127
 
110
128
  `redep` uses a hierarchical configuration system:
129
+
111
130
  1. **Environment Variables** (Highest Priority)
112
131
  2. **Config File** (Managed via CLI)
113
132
 
@@ -118,23 +137,27 @@ See [Advanced Configuration](docs/ADVANCED_CONFIG.md) for full details on enviro
118
137
  ## 💻 Development Setup
119
138
 
120
139
  ### Prerequisites
140
+
121
141
  - Node.js >= 18
122
142
  - Docker (for testing container builds)
123
143
 
124
144
  ### Local Development
125
145
 
126
146
  1. **Clone the repository:**
147
+
127
148
  ```bash
128
149
  git clone https://github.com/nafies1/redep.git
129
150
  cd redep
130
151
  ```
131
152
 
132
153
  2. **Install dependencies:**
154
+
133
155
  ```bash
134
156
  npm install
135
157
  ```
136
158
 
137
159
  3. **Link globally (optional):**
160
+
138
161
  ```bash
139
162
  npm link
140
163
  ```
@@ -161,6 +184,7 @@ See [.github/workflows/ci-cd.yml](.github/workflows/ci-cd.yml) for details.
161
184
 
162
185
  ## 📚 Additional Documentation
163
186
 
187
+ - [Docker Guide](docs/DOCKER.md) - Full reference for Docker deployment, configuration, and security.
164
188
  - [Troubleshooting Guide](docs/TROUBLESHOOTING.md) - Solutions for common connection and auth issues.
165
189
  - [Advanced Configuration](docs/ADVANCED_CONFIG.md) - Deep dive into config options and PM2.
166
190
  - [API Reference](docs/API.md) - Socket.IO events and protocol details.
package/bin/index.js CHANGED
@@ -5,8 +5,10 @@ import { Command } from 'commander';
5
5
  import { spawn } from 'child_process';
6
6
  import crypto from 'crypto';
7
7
  import inquirer from 'inquirer';
8
+ import Table from 'cli-table3';
9
+ import chalk from 'chalk';
8
10
  import { logger } from '../lib/logger.js';
9
- import { getConfig, setConfig, getAllConfig, clearConfig } from '../lib/config.js';
11
+ import { getConfig, setConfig, getAllConfig, clearConfig, getDetailedConfig, getClientConfig, getServerConfig, validateMandatoryConfig, getMissingConfigMessage } from '../lib/config.js';
10
12
  import { startServer } from '../lib/server/index.js';
11
13
  import { deploy } from '../lib/client/index.js';
12
14
  import pkg from '../package.json' with { type: 'json' };
@@ -38,10 +40,17 @@ program
38
40
  try {
39
41
  if (type === 'client') {
40
42
  const answers = await inquirer.prompt([
43
+ {
44
+ type: 'input',
45
+ name: 'server_name',
46
+ message: 'Enter Server Name:',
47
+ default: 'prod',
48
+ validate: (input) => (input ? true : 'Server Name is required'),
49
+ },
41
50
  {
42
51
  type: 'input',
43
52
  name: 'server_url',
44
- message: 'Enter Server URL:',
53
+ message: 'Enter Server URL (Host):',
45
54
  validate: (input) => (input ? true : 'Server URL is required'),
46
55
  },
47
56
  {
@@ -52,9 +61,13 @@ program
52
61
  },
53
62
  ]);
54
63
 
55
- setConfig('server_url', answers.server_url);
56
- setConfig('secret_key', answers.secret_key);
57
- logger.success('Client configuration saved successfully.');
64
+ const servers = getConfig('servers') || {};
65
+ servers[answers.server_name] = {
66
+ url: answers.server_url,
67
+ secret: answers.secret_key,
68
+ };
69
+ setConfig('servers', servers);
70
+ logger.success(`Client configuration for '${answers.server_name}' saved successfully.`);
58
71
  } else {
59
72
  const answers = await inquirer.prompt([
60
73
  {
@@ -153,12 +166,182 @@ configCommand
153
166
  });
154
167
 
155
168
  configCommand
156
- .command('list')
157
- .description('List all configurations')
158
- .action(() => {
159
- const all = getAllConfig();
160
- logger.info('Current Configuration:');
161
- console.table(all);
169
+ .command('list [type]')
170
+ .description('List configurations (client, server, or all)')
171
+ .option('--json', 'Output as JSON')
172
+ .option('--sort <field>', 'Sort by: key, modified, source, security', 'key')
173
+ .action((type, options) => {
174
+ // Handle different list types
175
+ if (type === 'client') {
176
+ const clientConfig = getClientConfig();
177
+
178
+ if (options.json) {
179
+ console.log(JSON.stringify(clientConfig, null, 2));
180
+ return;
181
+ }
182
+
183
+ if (clientConfig.length === 0) {
184
+ logger.warn('No client servers configured. Use "redep init client" to add servers.');
185
+ return;
186
+ }
187
+
188
+ // Create table for client config
189
+ const table = new Table({
190
+ head: ['Server', 'Host', 'Secret Key', 'Description', 'Security'],
191
+ style: { head: ['bold', 'white'] },
192
+ wordWrap: true,
193
+ colWidths: [15, 35, 15, 30, 12],
194
+ });
195
+
196
+ clientConfig.forEach((item) => {
197
+ let server = item.server;
198
+ let host = item.host;
199
+ let secret = item.secret_key;
200
+ let description = item.description;
201
+ let security = item.security;
202
+
203
+ // Color coding based on security
204
+ if (item.security === 'high') {
205
+ security = chalk.green(security);
206
+ server = chalk.green(server);
207
+ } else if (item.security === 'medium') {
208
+ security = chalk.yellow(security);
209
+ server = chalk.yellow(server);
210
+ } else if (item.security === 'low') {
211
+ security = chalk.cyan(security);
212
+ server = chalk.cyan(server);
213
+ }
214
+
215
+ table.push([server, host, secret, description, security]);
216
+ });
217
+
218
+ logger.info('Client Server Configurations:');
219
+ console.log(table.toString());
220
+
221
+ } else if (type === 'server') {
222
+ const serverConfig = getServerConfig();
223
+
224
+ if (options.json) {
225
+ console.log(JSON.stringify(serverConfig, null, 2));
226
+ return;
227
+ }
228
+
229
+ // Create table for server config
230
+ const table = new Table({
231
+ head: ['Key', 'Value', 'Default', 'Source', 'Updated', 'Security'],
232
+ style: { head: ['bold', 'white'] },
233
+ wordWrap: true,
234
+ colWidths: [20, 30, 15, 15, 25, 10],
235
+ });
236
+
237
+ serverConfig.forEach((item) => {
238
+ let key = item.key;
239
+ let value = typeof item.value === 'object' ? JSON.stringify(item.value) : String(item.value || '');
240
+ let def = typeof item.defaultValue === 'object' ? JSON.stringify(item.defaultValue) : String(item.defaultValue !== undefined ? item.defaultValue : '-');
241
+ let source = item.source;
242
+ let updated = item.updatedAt === 'N/A' ? '-' : new Date(item.updatedAt).toLocaleString();
243
+ let security = item.security || 'unknown';
244
+
245
+ // Color coding
246
+ if (item.source === 'Environment') {
247
+ source = chalk.cyan(source);
248
+ key = chalk.cyan(key);
249
+ } else if (item.isModified) {
250
+ source = chalk.yellow(source);
251
+ key = chalk.yellow(key);
252
+ }
253
+
254
+ if (['critical', 'high'].includes(item.security)) {
255
+ security = chalk.red(security);
256
+ if (item.security === 'critical') value = '********';
257
+ }
258
+
259
+ table.push([key, value, def, source, updated, security]);
260
+ });
261
+
262
+ logger.info('Server Configuration:');
263
+ console.log(table.toString());
264
+
265
+ } else {
266
+ // Default behavior - show all config (backward compatibility)
267
+ const detailed = getDetailedConfig();
268
+
269
+ // Sorting Logic
270
+ detailed.sort((a, b) => {
271
+ if (options.sort === 'key') return a.key.localeCompare(b.key);
272
+ if (options.sort === 'modified') return (b.updatedAt || '').localeCompare(a.updatedAt || '');
273
+ if (options.sort === 'source') return a.source.localeCompare(b.source);
274
+ if (options.sort === 'security') {
275
+ const levels = { critical: 0, high: 1, medium: 2, low: 3, unknown: 4 };
276
+ return (levels[a.security] || 4) - (levels[b.security] || 4);
277
+ }
278
+ return 0;
279
+ });
280
+
281
+ if (options.json) {
282
+ console.log(JSON.stringify(detailed, null, 2));
283
+ return;
284
+ }
285
+
286
+ // Table Setup (existing code)
287
+ const table = new Table({
288
+ head: ['Key', 'Value', 'Default', 'Source', 'Updated', 'Sec'],
289
+ style: { head: ['bold', 'white'] },
290
+ wordWrap: true,
291
+ colWidths: [20, 30, 15, 15, 25, 10],
292
+ });
293
+
294
+ let stats = { total: 0, modified: 0, env: 0, security: 0 };
295
+
296
+ detailed.forEach((item) => {
297
+ stats.total++;
298
+ if (item.source === 'Environment') stats.env++;
299
+ if (item.isModified) stats.modified++;
300
+ if (['critical', 'high'].includes(item.security)) stats.security++;
301
+
302
+ let key = item.key;
303
+ let value = typeof item.value === 'object' ? JSON.stringify(item.value) : String(item.value || '');
304
+ let def = typeof item.defaultValue === 'object' ? JSON.stringify(item.defaultValue) : String(item.defaultValue !== undefined ? item.defaultValue : '-');
305
+ let source = item.source;
306
+ let updated = item.updatedAt === 'N/A' ? '-' : new Date(item.updatedAt).toLocaleString();
307
+ let sec = item.security || 'unknown';
308
+
309
+ // Color Coding
310
+ if (item.source === 'Environment') {
311
+ source = chalk.cyan(source);
312
+ key = chalk.cyan(key);
313
+ } else if (item.isModified) {
314
+ source = chalk.yellow(source);
315
+ key = chalk.yellow(key);
316
+ }
317
+
318
+ if (['critical', 'high'].includes(item.security)) {
319
+ sec = chalk.red(sec);
320
+ if (item.security === 'critical') value = '********';
321
+ }
322
+
323
+ table.push([key, value, def, source, updated, sec]);
324
+ });
325
+
326
+ logger.info('Current Configuration:');
327
+ console.log(table.toString());
328
+
329
+ // Summary Statistics
330
+ console.log('\nSummary Statistics:');
331
+ console.log(
332
+ `Total: ${stats.total} | Modified: ${chalk.yellow(stats.modified)} | Env Overrides: ${chalk.cyan(
333
+ stats.env
334
+ )} | Security Critical: ${chalk.red(stats.security)}`
335
+ );
336
+
337
+ // Help Section
338
+ console.log('\nLegend:');
339
+ console.log(
340
+ `${chalk.cyan('Cyan')}: Environment Override | ${chalk.yellow(
341
+ 'Yellow'
342
+ )}: Modified/File | ${chalk.red('Red')}: High Security`
343
+ );
344
+ }
162
345
  });
163
346
 
164
347
  configCommand
@@ -177,6 +360,14 @@ program
177
360
  .description('Start the server in background (daemon mode) using PM2 if available')
178
361
  .option('-p, --port <port>', 'Port to listen on')
179
362
  .action((options) => {
363
+ // Validate mandatory configuration before starting
364
+ const missingParams = validateMandatoryConfig();
365
+ if (missingParams.length > 0) {
366
+ const errorMessage = getMissingConfigMessage(missingParams);
367
+ logger.error(errorMessage);
368
+ process.exit(1);
369
+ }
370
+
180
371
  // Try to use PM2 first
181
372
  try {
182
373
  // Check if PM2 is available via API
@@ -322,49 +513,46 @@ program
322
513
  program
323
514
  .command('listen')
324
515
  .description('Start the server to listen for commands')
325
- .option('-p, --port <port>', 'Port to listen on', 3000)
516
+ .option('-p, --port <port>', 'Port to listen on')
326
517
  .action((options) => {
518
+ // Validate mandatory configuration before starting
519
+ const missingParams = validateMandatoryConfig();
520
+ if (missingParams.length > 0) {
521
+ const errorMessage = getMissingConfigMessage(missingParams);
522
+ logger.error(errorMessage);
523
+ process.exit(1);
524
+ }
525
+
327
526
  const port = options.port || getConfig('server_port') || process.env.SERVER_PORT || 3000;
328
527
  const secret = getConfig('secret_key') || process.env.SECRET_KEY;
329
528
 
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
529
  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
530
  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
531
 
353
532
  startServer(port, secret, workingDir, deploymentCommand);
354
533
  });
355
534
 
356
535
  // Client Command
357
536
  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;
537
+ .command('deploy <serverName>')
538
+ .description('Deploy to a specific server (e.g., "prod")')
539
+ .action(async (serverName) => {
540
+ const servers = getConfig('servers') || {};
541
+ let serverUrl, secret;
542
+
543
+ if (servers[serverName]) {
544
+ serverUrl = servers[serverName].url;
545
+ secret = servers[serverName].secret;
546
+ } else {
547
+ serverUrl = getConfig('server_url') || process.env.SERVER_URL;
548
+ secret = getConfig('secret_key') || process.env.SECRET_KEY;
549
+ }
363
550
 
364
551
  if (!serverUrl) {
365
552
  logger.error(
366
- 'Error: "server_url" is not set. Set SERVER_URL env var or run "redep config set server_url <url>"'
553
+ `Error: Server "${serverName}" not found in config, and global "server_url" is not set.`
367
554
  );
555
+ logger.info('Run "redep init client" to configure a server.');
368
556
  process.exit(1);
369
557
  }
370
558
 
@@ -376,7 +564,7 @@ program
376
564
  }
377
565
 
378
566
  try {
379
- await deploy(type, serverUrl, secret);
567
+ await deploy(serverName, serverUrl, secret);
380
568
  } catch (error) {
381
569
  logger.error(`Deploy failed: ${error.message}`);
382
570
  process.exit(1);
@@ -26,21 +26,29 @@ The CLI `redep start` command automatically attempts to use PM2 if installed.
26
26
 
27
27
  Clients can be configured to talk to multiple servers (e.g., `dev`, `staging`, `prod`).
28
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
- }
29
+ ### Client Server Configuration Table
30
+
31
+ | Servers | Host | Secret Key | Description |
32
+ | --------- | ----------------------------- | ---------------- | ----------------------------------- |
33
+ | `prod` | `https://deploy.example.com` | `prod-secret` | Production server with HTTPS |
34
+ | `staging` | `http://10.0.0.5:3000` | `staging-secret` | Staging server on internal network |
35
+ | `uat` | `http://uat.company.com:3000` | `uat-secret` | User Acceptance Testing environment |
36
+ | `dev` | `http://localhost:3000` | `dev-secret` | Local development server |
37
+
38
+ ### Viewing Client Configuration
39
+
40
+ Use the new `redep config list client` command to display your client configurations in a structured table format:
41
+
42
+ ```bash
43
+ # Display all client server configurations
44
+ redep config list client
45
+
46
+ # Display with custom sorting
47
+ redep config list client --sort host
42
48
  ```
43
49
 
50
+ This will show a formatted table with columns for Server Name, Host URL, and Security Level, making it easy to review all your configured deployment targets.
51
+
44
52
  ### Managing Servers via CLI
45
53
 
46
54
  ```bash
@@ -52,6 +60,51 @@ redep config set servers.dev.secret_key mysecret
52
60
  redep config get servers.dev
53
61
  ```
54
62
 
63
+ ### Viewing Configuration with New List Commands
64
+
65
+ The enhanced `redep config list` command now supports viewing client and server configurations separately:
66
+
67
+ ```bash
68
+ # View all client server configurations
69
+ redep config list client
70
+
71
+ # View server configuration only
72
+ redep config list server
73
+
74
+ # View all configurations (backward compatibility)
75
+ redep config list
76
+
77
+ # View with JSON output
78
+ redep config list client --json
79
+ redep config list server --json
80
+ ```
81
+
82
+ **Client Configuration Table Example:**
83
+
84
+ ```
85
+ ┌─────────┬──────────────────────────────┬─────────────┬─────────────────────────────────────┬──────────┐
86
+ │ Server │ Host │ Secret Key │ Description │ Security │
87
+ ├─────────┼──────────────────────────────┼─────────────┼─────────────────────────────────────┼──────────┤
88
+ │ prod │ https://deploy.example.com │ ******** │ Production environment with HTTPS │ high │
89
+ │ staging │ http://10.0.0.5:3000 │ ******** │ Staging environment for testing │ medium │
90
+ │ uat │ http://uat.company.com:3000 │ ******** │ User Acceptance Testing environment │ medium │
91
+ │ dev │ http://localhost:3000 │ ******** │ Local development server │ low │
92
+ └─────────┴──────────────────────────────┴─────────────┴─────────────────────────────────────┴──────────┘
93
+ ```
94
+
95
+ **Server Configuration Table Example:**
96
+
97
+ ```
98
+ ┌────────────────────┬──────────────────────────────┬───────────────┬───────────────┬─────────────────────────┬──────────┐
99
+ │ Key │ Value │ Default │ Source │ Updated │ Security │
100
+ ├────────────────────┼──────────────────────────────┼───────────────┼───────────────┼─────────────────────────┼──────────┤
101
+ │ server_port │ 3000 │ 3000 │ Environment │ 2026-01-22 14:28:14 │ low │
102
+ │ secret_key │ ******** │ null │ File │ 2026-01-22 14:30:22 │ critical │
103
+ │ working_dir │ /app/workspace │ null │ Environment │ - │ medium │
104
+ │ deployment_command │ docker compose up -d │ null │ File │ - │ high │
105
+ └────────────────────┴──────────────────────────────┴───────────────┴───────────────┴─────────────────────────┴──────────┘
106
+ ```
107
+
55
108
  ## Security Best Practices
56
109
 
57
110
  1. **TLS/SSL**: Always use HTTPS for the `host` URL in production. The WebSocket connection will automatically use WSS (Secure WebSocket).
package/docs/DOCKER.md ADDED
@@ -0,0 +1,189 @@
1
+ # 🐳 Docker Configuration & Usage Guide
2
+
3
+ This guide provides comprehensive instructions for running `redep` server using Docker. The Docker image is optimized for size and security, based on Alpine Linux.
4
+
5
+ ## 🚀 Quick Start
6
+
7
+ ### 1. Pull the Image
8
+
9
+ ```bash
10
+ docker pull nafies1/redep:latest
11
+ ```
12
+
13
+ ### 2. Run the Server
14
+
15
+ To run the server, you need to provide a **Secret Key** and mount the **Docker Socket** (so `redep` can execute Docker commands on the host).
16
+
17
+ #### Linux / macOS (Bash)
18
+
19
+ ```bash
20
+ docker run -d \
21
+ --name redep-server \
22
+ --restart always \
23
+ -p 3000:3000 \
24
+ -v /var/run/docker.sock:/var/run/docker.sock \
25
+ -v $(pwd)/workspace:/app/workspace \
26
+ -e SECRET_KEY=your-super-secret-key \
27
+ -e WORKING_DIR=/app/workspace \
28
+ -e DEPLOYMENT_COMMAND="docker compose up -d" \
29
+ nafies1/redep:latest
30
+ ```
31
+
32
+ #### Windows (PowerShell)
33
+
34
+ ```powershell
35
+ docker run -d `
36
+ --name redep-server `
37
+ --restart always `
38
+ -p 3000:3000 `
39
+ -v /var/run/docker.sock:/var/run/docker.sock `
40
+ -v ${PWD}/workspace:/app/workspace `
41
+ -e SECRET_KEY=your-super-secret-key `
42
+ -e WORKING_DIR=/app/workspace `
43
+ -e DEPLOYMENT_COMMAND="docker compose up -d" `
44
+ nafies1/redep:latest
45
+ ```
46
+
47
+ #### Windows (Command Prompt)
48
+
49
+ ```cmd
50
+ docker run -d ^
51
+ --name redep-server ^
52
+ --restart always ^
53
+ -p 3000:3000 ^
54
+ -v /var/run/docker.sock:/var/run/docker.sock ^
55
+ -v %cd%/workspace:/app/workspace ^
56
+ -e SECRET_KEY=your-super-secret-key ^
57
+ -e WORKING_DIR=/app/workspace ^
58
+ -e DEPLOYMENT_COMMAND="docker compose up -d" ^
59
+ nafies1/redep:latest
60
+ ```
61
+
62
+ ---
63
+
64
+ ## ⚙️ Configuration Reference
65
+
66
+ ### Environment Variables
67
+
68
+ | Variable | Description | Default | Required |
69
+ | -------------------- | ----------------------------------------------------------- | ------------ | -------- |
70
+ | `SECRET_KEY` | Secret token for authentication with the client. | - | **Yes** |
71
+ | `WORKING_DIR` | Directory inside the container where commands are executed. | - | **Yes** |
72
+ | `DEPLOYMENT_COMMAND` | Command to execute when a deployment is triggered. | - | **Yes** |
73
+ | `SERVER_PORT` | Port the server listens on inside the container. | `3000` | No |
74
+ | `NODE_ENV` | Node.js environment mode. | `production` | No |
75
+
76
+ ### Volume Mounts
77
+
78
+ | Host Path | Container Path | Purpose |
79
+ | ---------------------- | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
80
+ | `/var/run/docker.sock` | `/var/run/docker.sock` | **Required**. Allows the container to control the host's Docker daemon. |
81
+ | `./workspace` | `/app/workspace` | **Recommended**. Persist your project files (e.g., `docker-compose.yml`) so they survive container restarts. |
82
+
83
+ ---
84
+
85
+ ## 📦 Docker Compose Examples
86
+
87
+ ### Basic Setup
88
+
89
+ Create a `docker-compose.redep.yml`:
90
+
91
+ ```yaml
92
+ services:
93
+ redep-server:
94
+ image: nafies1/redep:latest
95
+ container_name: redep-server
96
+ restart: always
97
+ ports:
98
+ - '3000:3000'
99
+ volumes:
100
+ - /var/run/docker.sock:/var/run/docker.sock
101
+ - ./my-project:/app/workspace
102
+ environment:
103
+ - SECRET_KEY=change-me-to-something-secure
104
+ - WORKING_DIR=/app/workspace
105
+ - DEPLOYMENT_COMMAND=docker compose pull && docker compose up -d
106
+ ```
107
+
108
+ Run with:
109
+
110
+ ```bash
111
+ docker compose -f docker-compose.redep.yml up -d
112
+ ```
113
+
114
+ ### Advanced Setup (Traefik + Watchtower)
115
+
116
+ ```yaml
117
+ services:
118
+ redep-server:
119
+ image: nafies1/redep:latest
120
+ networks:
121
+ - web
122
+ volumes:
123
+ - /var/run/docker.sock:/var/run/docker.sock
124
+ - ./workspace:/app/workspace
125
+ environment:
126
+ - SECRET_KEY=${REDEP_SECRET}
127
+ - WORKING_DIR=/app/workspace
128
+ - DEPLOYMENT_COMMAND=docker compose up -d --build
129
+ labels:
130
+ - 'traefik.enable=true'
131
+ - 'traefik.http.routers.redep.rule=Host(`deploy.example.com`)'
132
+
133
+ networks:
134
+ web:
135
+ external: true
136
+ ```
137
+
138
+ ---
139
+
140
+ ## 🛠️ Troubleshooting
141
+
142
+ ### 1. "Docker command not found" or Permission Denied
143
+
144
+ - **Symptom**: Deployment logs show `docker: not found` or `permission denied`.
145
+ - **Cause**: The container cannot access the host's Docker socket.
146
+ - **Fix**: Ensure you passed `-v /var/run/docker.sock:/var/run/docker.sock`. On some systems, you may need to run the container with `--group-add $(getent group docker | cut -d: -f3)` or `user: root` (though root is not recommended if avoidable).
147
+
148
+ ### 2. "Connection Refused"
149
+
150
+ - **Symptom**: Client says `xhr poll error`.
151
+ - **Cause**: The server is not running, or the port is not mapped correctly.
152
+ - **Fix**: Check `docker ps`. Ensure `-p 3000:3000` matches the internal `SERVER_PORT`.
153
+
154
+ ### 3. "Directory not found"
155
+
156
+ - **Symptom**: Error regarding `WORKING_DIR`.
157
+ - **Fix**: Ensure the `WORKING_DIR` path exists inside the container. Mounting a volume automatically creates the directory.
158
+
159
+ ---
160
+
161
+ ## 🔒 Security Best Practices
162
+
163
+ 1. **Least Privilege**: The container runs as `root` by default to access the Docker socket easily. For higher security, consider using a non-root user and adding them to the docker group (requires custom image extension).
164
+ 2. **Network Isolation**: Don't expose port 3000 to the public internet directly. Use a reverse proxy (Nginx, Traefik) with SSL/TLS and IP whitelisting if possible.
165
+ 3. **Secret Management**: Do not commit your `SECRET_KEY` to version control. Use `.env` files (excluded from git) or Docker Secrets.
166
+ 4. **Socket Exposure**: Mounting `/var/run/docker.sock` gives the container full control over the host. Ensure only trusted commands are executed via `DEPLOYMENT_COMMAND`.
167
+
168
+ ---
169
+
170
+ ## 🚀 Performance Tuning
171
+
172
+ - **Base Image**: We use `node:20-alpine` for a lightweight footprint (<100MB).
173
+ - **Memory Limit**: The server is lightweight. You can limit memory usage:
174
+ ```bash
175
+ --memory="256m" --cpus="0.5"
176
+ ```
177
+ - **Layer Caching**: The Dockerfile uses multi-stage builds. Dependencies are cached in a separate layer, so rebuilding the image after code changes is fast.
178
+
179
+ ## 📋 Version Compatibility
180
+
181
+ | redep Version | Docker Tag | Node.js Version |
182
+ | ------------- | ---------- | --------------- |
183
+ | 2.x | `latest` | 20 (LTS) |
184
+ | 1.x | `v1` | 18 |
185
+
186
+ ## 🔗 Additional Resources
187
+
188
+ - [Official Docker Documentation](https://docs.docker.com/)
189
+ - [Node.js Docker Best Practices](https://github.com/nodejs/docker-node/blob/main/docs/BestPractices.md)
@@ -18,7 +18,11 @@ export const connectAndDeploy = (serverUrl, secret) => {
18
18
  });
19
19
 
20
20
  socket.on('connect_error', (err) => {
21
- logger.error(`Connection error: ${err.message}`);
21
+ let msg = err.message;
22
+ if (msg === 'xhr poll error' || msg.includes('ECONNREFUSED')) {
23
+ msg = `Could not connect to server at ${serverUrl}. Is the server running?`;
24
+ }
25
+ logger.error(`Connection error: ${msg}`);
22
26
  socket.close();
23
27
  reject(err);
24
28
  });
package/lib/config.js CHANGED
@@ -5,12 +5,78 @@ const config = new Conf({
5
5
  encryptionKey: 'redep-secure-storage', // Obfuscates the config file
6
6
  });
7
7
 
8
+ export const CONFIG_SCHEMA = {
9
+ server_port: {
10
+ default: 3000,
11
+ security: 'low',
12
+ env: 'SERVER_PORT',
13
+ description: 'Server Port',
14
+ },
15
+ secret_key: {
16
+ default: null,
17
+ security: 'critical',
18
+ env: 'SECRET_KEY',
19
+ description: 'Authentication Secret',
20
+ },
21
+ working_dir: {
22
+ default: null,
23
+ security: 'medium',
24
+ env: 'WORKING_DIR',
25
+ description: 'Working Directory',
26
+ },
27
+ deployment_command: {
28
+ default: null,
29
+ security: 'high',
30
+ env: 'DEPLOYMENT_COMMAND',
31
+ description: 'Deployment Command',
32
+ },
33
+ server_url: {
34
+ default: null,
35
+ security: 'low',
36
+ env: 'SERVER_URL',
37
+ description: 'Default Server URL',
38
+ },
39
+ servers: {
40
+ default: {},
41
+ security: 'medium',
42
+ env: null,
43
+ description: 'Server Profiles',
44
+ },
45
+ server_pid: {
46
+ default: null,
47
+ security: 'low',
48
+ env: null,
49
+ description: 'Server Process ID',
50
+ },
51
+ };
52
+
8
53
  export const getConfig = (key) => {
9
- return config.get(key);
54
+ // Check environment variables first (highest priority)
55
+ if (CONFIG_SCHEMA[key] && CONFIG_SCHEMA[key].env) {
56
+ const envValue = process.env[CONFIG_SCHEMA[key].env];
57
+ if (envValue !== undefined) {
58
+ return envValue;
59
+ }
60
+ }
61
+
62
+ // Then check stored config
63
+ const value = config.get(key);
64
+
65
+ // If value is undefined but key exists in schema, return default value
66
+ if (value === undefined && CONFIG_SCHEMA[key]) {
67
+ return CONFIG_SCHEMA[key].default;
68
+ }
69
+
70
+ return value;
10
71
  };
11
72
 
12
73
  export const setConfig = (key, value) => {
13
74
  config.set(key, value);
75
+ // Store metadata for timestamp
76
+ // We use a flat key for metadata to avoid interfering with nested config objects if possible,
77
+ // but since we want per-key tracking, we store it in a hidden _meta object.
78
+ const now = new Date().toISOString();
79
+ config.set(`_meta.${key}`, { updatedAt: now });
14
80
  };
15
81
 
16
82
  export const clearConfig = () => {
@@ -20,3 +86,141 @@ export const clearConfig = () => {
20
86
  export const getAllConfig = () => {
21
87
  return config.store;
22
88
  };
89
+
90
+ export const getDetailedConfig = () => {
91
+ const all = config.store;
92
+ const meta = all._meta || {};
93
+ const result = [];
94
+
95
+ // Process Schema Keys
96
+ Object.keys(CONFIG_SCHEMA).forEach((key) => {
97
+ const schema = CONFIG_SCHEMA[key];
98
+ const fileValue = config.get(key);
99
+ const envValue = schema.env ? process.env[schema.env] : undefined;
100
+
101
+ let currentValue = fileValue;
102
+ let source = 'File';
103
+
104
+ if (envValue !== undefined) {
105
+ currentValue = envValue;
106
+ source = 'Environment';
107
+ } else if (fileValue !== undefined) {
108
+ currentValue = fileValue;
109
+ source = 'File';
110
+ } else {
111
+ currentValue = schema.default;
112
+ source = 'Default';
113
+ }
114
+
115
+ result.push({
116
+ key,
117
+ value: currentValue,
118
+ defaultValue: schema.default,
119
+ source,
120
+ security: schema.security,
121
+ updatedAt: meta[key]?.updatedAt || 'N/A',
122
+ description: schema.description,
123
+ });
124
+ });
125
+
126
+ // Process Custom Keys (not in schema)
127
+ Object.keys(all).forEach((key) => {
128
+ if (!CONFIG_SCHEMA[key] && key !== '_meta') {
129
+ result.push({
130
+ key,
131
+ value: all[key],
132
+ defaultValue: undefined,
133
+ source: 'File', // Assumed file for custom keys
134
+ security: 'unknown',
135
+ updatedAt: meta[key]?.updatedAt || 'N/A',
136
+ description: 'Custom Configuration',
137
+ });
138
+ }
139
+ });
140
+
141
+ return result;
142
+ };
143
+
144
+ export const getClientConfig = () => {
145
+ const servers = config.get('servers') || {};
146
+ const result = [];
147
+
148
+ Object.keys(servers).forEach((serverName) => {
149
+ const server = servers[serverName];
150
+ result.push({
151
+ server: serverName,
152
+ host: server.url || 'Not configured',
153
+ secret_key: server.secret ? '********' : 'Not set',
154
+ description: getServerDescription(serverName),
155
+ security: getServerSecurityLevel(server.url),
156
+ });
157
+ });
158
+
159
+ return result;
160
+ };
161
+
162
+ export const getServerConfig = () => {
163
+ const detailed = getDetailedConfig();
164
+ // Filter hanya konfigurasi server (bukan client servers)
165
+ return detailed.filter((item) =>
166
+ ['server_port', 'secret_key', 'working_dir', 'deployment_command', 'server_pid'].includes(item.key)
167
+ );
168
+ };
169
+
170
+ function getServerDescription(serverName) {
171
+ const descriptions = {
172
+ prod: 'Production environment with high security',
173
+ staging: 'Staging environment for testing',
174
+ uat: 'User Acceptance Testing environment',
175
+ dev: 'Development environment',
176
+ test: 'Testing environment',
177
+ };
178
+ return descriptions[serverName.toLowerCase()] || 'Custom environment';
179
+ }
180
+
181
+ export const validateMandatoryConfig = () => {
182
+ const errors = [];
183
+
184
+ // Check SECRET_KEY
185
+ const secretKey = getConfig('secret_key');
186
+ if (!secretKey) {
187
+ errors.push('secret_key');
188
+ }
189
+
190
+ // Check WORKING_DIR
191
+ const workingDir = getConfig('working_dir');
192
+ if (!workingDir) {
193
+ errors.push('working_dir');
194
+ }
195
+
196
+ // Check DEPLOYMENT_COMMAND
197
+ const deploymentCommand = getConfig('deployment_command');
198
+ if (!deploymentCommand) {
199
+ errors.push('deployment_command');
200
+ }
201
+
202
+ return errors;
203
+ };
204
+
205
+ export const getMissingConfigMessage = (missingParams) => {
206
+ if (missingParams.length === 0) return null;
207
+
208
+ const paramList = missingParams.map(param => `'${param}'`).join(', ');
209
+ const instructions = missingParams.map(param => {
210
+ const envVar = CONFIG_SCHEMA[param]?.env;
211
+ if (envVar) {
212
+ return ` - Set '${param}' using 'redep config set ${param} <value>' or add '${envVar}=<value>' to your .env file`;
213
+ }
214
+ return ` - Set '${param}' using 'redep config set ${param} <value>'`;
215
+ }).join('\n');
216
+
217
+ return `Error: Required configuration parameter(s) ${paramList} are not set.\n${instructions}`;
218
+ };
219
+
220
+ function getServerSecurityLevel(host) {
221
+ if (!host) return 'unknown';
222
+ if (host.startsWith('https://')) return 'high';
223
+ if (host.startsWith('http://localhost') || host.startsWith('http://127.0.0.1')) return 'low';
224
+ if (host.startsWith('http://')) return 'medium';
225
+ return 'unknown';
226
+ };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "redep",
3
3
  "author": "nafies1",
4
- "version": "2.0.2",
4
+ "version": "2.1.0",
5
5
  "main": "index.js",
6
6
  "type": "module",
7
7
  "bin": {
@@ -37,6 +37,7 @@
37
37
  "axios": "^1.7.9",
38
38
  "body-parser": "^1.20.2",
39
39
  "chalk": "^5.3.0",
40
+ "cli-table3": "^0.6.5",
40
41
  "commander": "^12.0.0",
41
42
  "conf": "^12.0.0",
42
43
  "cors": "^2.8.5",
@@ -53,9 +54,5 @@
53
54
  "@sonar/scan": "^4.3.4",
54
55
  "prettier": "^3.8.0",
55
56
  "sonarqube-scanner": "^4.3.4"
56
- },
57
- "redep": {
58
- "secret_key": "4k94jLuEIT_jWPHJYPActmGxn2x72eR0",
59
- "working_dir": "C:\\Users\\nafie\\Documents\\trae_projects\\remote-deploy-cli"
60
57
  }
61
58
  }
Binary file
@@ -0,0 +1,14 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <title>Redep Test Deployment</title>
5
+ </head>
6
+ <body>
7
+ <h1>Redep Docker Deployment Test</h1>
8
+ <p>Server successfully deployed via redep!</p>
9
+ <p>Time: <span id="time"></span></p>
10
+ <script>
11
+ document.getElementById('time').textContent = new Date().toLocaleString();
12
+ </script>
13
+ </body>
14
+ </html>