portok 1.0.0 → 1.0.2

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/Dockerfile CHANGED
@@ -13,7 +13,7 @@ RUN npm ci
13
13
  COPY . .
14
14
 
15
15
  # Make CLI executable
16
- RUN chmod +x portok.mjs portokd.mjs
16
+ RUN chmod +x portok.js portokd.js
17
17
 
18
18
  # Default command runs tests
19
19
  CMD ["npm", "test"]
package/README.md CHANGED
@@ -15,8 +15,15 @@ A lightweight Node.js "switchboard" proxy that enables zero-downtime deployments
15
15
 
16
16
  ### Installation
17
17
 
18
+ **Global Installation (Recommended):**
19
+
18
20
  ```bash
19
- npm install
21
+ # Install globally
22
+ npm install -g portok
23
+
24
+ # Now you can use portok and portokd commands from anywhere
25
+ portok --help
26
+ portokd --help
20
27
  ```
21
28
 
22
29
  ### Start the Daemon
@@ -28,25 +35,27 @@ export INITIAL_TARGET_PORT=8080
28
35
  export ADMIN_TOKEN=your-secret-token
29
36
 
30
37
  # Start portokd
31
- node portokd.mjs
38
+ node portokd.js
32
39
  ```
33
40
 
34
41
  ### Use the CLI
35
42
 
36
43
  ```bash
37
44
  # Check status
38
- ./portok.mjs status --token your-secret-token
45
+ portok status --token your-secret-token
39
46
 
40
47
  # Switch to new port
41
- ./portok.mjs switch 8081 --token your-secret-token
48
+ portok switch 8081 --token your-secret-token
42
49
 
43
50
  # Check metrics
44
- ./portok.mjs metrics --token your-secret-token
51
+ portok metrics --token your-secret-token
45
52
 
46
53
  # Check health
47
- ./portok.mjs health --token your-secret-token
54
+ portok health --token your-secret-token
48
55
  ```
49
56
 
57
+ > **Note:** If not installed globally, use `./portok.js` instead of `portok`.
58
+
50
59
  ## Configuration
51
60
 
52
61
  All configuration is via environment variables:
@@ -190,6 +199,14 @@ Management Commands:
190
199
  add <name> Create a new service instance
191
200
  list List all configured instances and their status
192
201
 
202
+ Service Control Commands:
203
+ start <name> Start a portok service (systemctl start portok@<name>)
204
+ stop <name> Stop a portok service
205
+ restart <name> Restart a portok service
206
+ enable <name> Enable service at boot
207
+ disable <name> Disable service at boot
208
+ logs <name> Show service logs (journalctl)
209
+
193
210
  Operational Commands:
194
211
  status Show current proxy status
195
212
  metrics Show proxy metrics
@@ -208,6 +225,10 @@ Options for 'add' command:
208
225
  --target <port> Target port (default: random 8000-8999)
209
226
  --health <path> Health check path (default: /health)
210
227
  --force Overwrite existing config
228
+
229
+ Options for 'logs' command:
230
+ --follow, -f Follow log output
231
+ --lines, -n Number of lines to show (default: 50)
211
232
  ```
212
233
 
213
234
  ### Quick Start with CLI
@@ -220,12 +241,15 @@ sudo portok init
220
241
  sudo portok add api --port 3001 --target 8001
221
242
 
222
243
  # 3. Start the service
223
- sudo systemctl start portok@api
244
+ sudo portok start api
245
+
246
+ # 4. Enable at boot
247
+ sudo portok enable api
224
248
 
225
- # 4. Check status
249
+ # 5. Check status
226
250
  portok status --instance api
227
251
 
228
- # 5. List all services
252
+ # 6. List all services
229
253
  portok list
230
254
  ```
231
255
 
@@ -244,6 +268,18 @@ sudo portok init
244
268
  sudo portok add api --port 3001 --target 8001
245
269
  sudo portok add web --port 3002 --target 8002
246
270
 
271
+ # Service management
272
+ sudo portok start api
273
+ sudo portok stop api
274
+ sudo portok restart api
275
+ sudo portok enable api # Enable at boot
276
+ sudo portok disable api # Disable at boot
277
+
278
+ # View logs
279
+ portok logs api
280
+ portok logs api --follow # Follow log output
281
+ portok logs api -n 100 # Show last 100 lines
282
+
247
283
  # List all instances with status
248
284
  portok list
249
285
 
@@ -276,7 +312,7 @@ After=network.target
276
312
  Type=simple
277
313
  User=www-data
278
314
  WorkingDirectory=/opt/portok
279
- ExecStart=/usr/bin/node /opt/portok/portokd.mjs
315
+ ExecStart=/usr/bin/node /opt/portok/portokd.js
280
316
  Restart=always
281
317
  RestartSec=5
282
318
 
@@ -548,10 +584,10 @@ Run the validation benchmark to verify performance:
548
584
 
549
585
  ```bash
550
586
  # Quick validation (3s)
551
- FAST_PATH=1 node bench/validate.mjs --quick
587
+ FAST_PATH=1 node bench/validate.js --quick
552
588
 
553
589
  # Full validation (10s)
554
- FAST_PATH=1 node bench/validate.mjs
590
+ FAST_PATH=1 node bench/validate.js
555
591
 
556
592
  # Manual autocannon test
557
593
  # Direct:
@@ -3,10 +3,10 @@
3
3
  * Compares direct server performance vs proxied performance
4
4
  */
5
5
 
6
- import autocannon from 'autocannon';
7
- import { createMockServer, startDaemon, getFreePort, formatNumber } from './run.mjs';
6
+ const autocannon = require('autocannon');
7
+ const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./run.js');
8
8
 
9
- export async function run({ duration, adminToken }) {
9
+ async function run({ duration, adminToken }) {
10
10
  const shortDuration = Math.max(2, Math.floor(duration / 2));
11
11
 
12
12
  // Setup mock server for direct test
@@ -71,3 +71,5 @@ export async function run({ duration, adminToken }) {
71
71
  }
72
72
  }
73
73
 
74
+ module.exports = { run };
75
+
@@ -3,10 +3,10 @@
3
3
  * Tests scaling with increasing concurrent connections
4
4
  */
5
5
 
6
- import autocannon from 'autocannon';
7
- import { createMockServer, startDaemon, getFreePort, formatNumber } from './run.mjs';
6
+ const autocannon = require('autocannon');
7
+ const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./run.js');
8
8
 
9
- export async function run({ duration, adminToken }) {
9
+ async function run({ duration, adminToken }) {
10
10
  // Setup
11
11
  const mockServer = await createMockServer();
12
12
  const proxyPort = await getFreePort();
@@ -68,3 +68,5 @@ export async function run({ duration, adminToken }) {
68
68
  }
69
69
  }
70
70
 
71
+ module.exports = { run };
72
+
@@ -6,14 +6,14 @@
6
6
  * - Keep-alive should be at least 50% faster than no-keepalive
7
7
  * - Proxied RPS should be at least 20% of direct RPS
8
8
  *
9
- * Can be run standalone or via bench/run.mjs:
10
- * node bench/keepalive.bench.mjs --quick
9
+ * Can be run standalone or via bench/run.js:
10
+ * node bench/keepalive.bench.js --quick
11
11
  * npm run bench
12
12
  */
13
13
 
14
- import autocannon from 'autocannon';
15
- import { spawn } from 'node:child_process';
16
- import http from 'node:http';
14
+ const autocannon = require('autocannon');
15
+ const { spawn } = require('node:child_process');
16
+ const http = require('node:http');
17
17
 
18
18
  // =============================================================================
19
19
  // Utilities (used when running standalone)
@@ -62,7 +62,7 @@ async function createMockServer(port = 0) {
62
62
  }
63
63
 
64
64
  async function startDaemonWithKA(listenPort, targetPort, adminToken, keepAlive = true) {
65
- const proc = spawn('node', ['portokd.mjs'], {
65
+ const proc = spawn('node', ['portokd.js'], {
66
66
  env: {
67
67
  ...process.env,
68
68
  LISTEN_PORT: String(listenPort),
@@ -99,10 +99,10 @@ async function startDaemonWithKA(listenPort, targetPort, adminToken, keepAlive =
99
99
  }
100
100
 
101
101
  // =============================================================================
102
- // Main run function (called by bench/run.mjs)
102
+ // Main run function (called by bench/run.js)
103
103
  // =============================================================================
104
104
 
105
- export async function run({ duration, adminToken }) {
105
+ async function run({ duration, adminToken }) {
106
106
  const shortDuration = Math.max(2, Math.floor(duration / 2));
107
107
  const CONNECTIONS = 100;
108
108
  const PIPELINING = 10;
@@ -187,9 +187,7 @@ export async function run({ duration, adminToken }) {
187
187
  // Standalone execution
188
188
  // =============================================================================
189
189
 
190
- const isMain = import.meta.url === `file://${process.argv[1]}`;
191
-
192
- if (isMain) {
190
+ if (require.main === module) {
193
191
  const args = process.argv.slice(2);
194
192
  const isQuick = args.includes('--quick');
195
193
  const isJson = args.includes('--json');
@@ -209,9 +207,7 @@ if (isMain) {
209
207
  log('╚══════════════════════════════════════════════════════════════╝');
210
208
  log('');
211
209
 
212
- try {
213
- const results = await run({ duration, adminToken });
214
-
210
+ run({ duration, adminToken }).then(results => {
215
211
  // Validation
216
212
  const rpsPass = results.proxiedKeepAlive.rps >= results.direct.rps * 0.20;
217
213
  const kaEffective = results.proxiedKeepAlive.rps >= results.proxiedNoKeepAlive.rps * 1.5;
@@ -241,8 +237,11 @@ if (isMain) {
241
237
  }
242
238
 
243
239
  process.exit(0);
244
- } catch (err) {
240
+ }).catch(err => {
245
241
  console.error('Benchmark failed:', err);
246
242
  process.exit(1);
247
- }
243
+ });
248
244
  }
245
+
246
+ module.exports = { run };
247
+
@@ -3,10 +3,10 @@
3
3
  * Measures latency distribution under moderate load
4
4
  */
5
5
 
6
- import autocannon from 'autocannon';
7
- import { createMockServer, startDaemon, getFreePort } from './run.mjs';
6
+ const autocannon = require('autocannon');
7
+ const { createMockServer, startDaemon, getFreePort } = require('./run.js');
8
8
 
9
- export async function run({ duration, adminToken }) {
9
+ async function run({ duration, adminToken }) {
10
10
  // Setup
11
11
  const mockServer = await createMockServer();
12
12
  const proxyPort = await getFreePort();
@@ -45,3 +45,5 @@ export async function run({ duration, adminToken }) {
45
45
  }
46
46
  }
47
47
 
48
+ module.exports = { run };
49
+
@@ -5,8 +5,8 @@
5
5
  * Orchestrates all benchmarks and outputs formatted results
6
6
  */
7
7
 
8
- import { spawn } from 'node:child_process';
9
- import http from 'node:http';
8
+ const { spawn } = require('node:child_process');
9
+ const http = require('node:http');
10
10
 
11
11
  // =============================================================================
12
12
  // Configuration
@@ -82,7 +82,7 @@ async function createMockServer(port = 0) {
82
82
  // =============================================================================
83
83
 
84
84
  async function startDaemon(listenPort, targetPort) {
85
- const proc = spawn('node', ['portokd.mjs'], {
85
+ const proc = spawn('node', ['portokd.js'], {
86
86
  env: {
87
87
  ...process.env,
88
88
  LISTEN_PORT: String(listenPort),
@@ -116,7 +116,7 @@ async function startDaemon(listenPort, targetPort) {
116
116
  // =============================================================================
117
117
 
118
118
  async function runBenchmark(file) {
119
- const module = await import(file);
119
+ const module = require(file);
120
120
  return module.run({ duration, adminToken: ADMIN_TOKEN });
121
121
  }
122
122
 
@@ -165,12 +165,12 @@ async function main() {
165
165
 
166
166
  // Run each benchmark
167
167
  const benchmarks = [
168
- { name: 'Throughput', file: './throughput.bench.mjs' },
169
- { name: 'Latency', file: './latency.bench.mjs' },
170
- { name: 'Connections', file: './connections.bench.mjs' },
171
- { name: 'Switching', file: './switching.bench.mjs' },
172
- { name: 'Baseline', file: './baseline.bench.mjs' },
173
- { name: 'KeepAlive', file: './keepalive.bench.mjs' },
168
+ { name: 'Throughput', file: './throughput.bench.js' },
169
+ { name: 'Latency', file: './latency.bench.js' },
170
+ { name: 'Connections', file: './connections.bench.js' },
171
+ { name: 'Switching', file: './switching.bench.js' },
172
+ { name: 'Baseline', file: './baseline.bench.js' },
173
+ { name: 'KeepAlive', file: './keepalive.bench.js' },
174
174
  ];
175
175
 
176
176
  for (const bench of benchmarks) {
@@ -207,5 +207,5 @@ main().catch(err => {
207
207
  process.exit(1);
208
208
  });
209
209
 
210
- export { createMockServer, startDaemon, getFreePort, waitFor, formatNumber, ADMIN_TOKEN };
210
+ module.exports = { createMockServer, startDaemon, getFreePort, waitFor, formatNumber, ADMIN_TOKEN };
211
211
 
@@ -3,10 +3,10 @@
3
3
  * Measures switch latency and request loss during switch
4
4
  */
5
5
 
6
- import autocannon from 'autocannon';
7
- import { createMockServer, startDaemon, getFreePort, formatNumber, ADMIN_TOKEN } from './run.mjs';
6
+ const autocannon = require('autocannon');
7
+ const { createMockServer, startDaemon, getFreePort, formatNumber, ADMIN_TOKEN } = require('./run.js');
8
8
 
9
- export async function run({ duration, adminToken }) {
9
+ async function run({ duration, adminToken }) {
10
10
  // Setup two servers
11
11
  const mockServer1 = await createMockServer();
12
12
  const mockServer2 = await createMockServer();
@@ -94,3 +94,5 @@ export async function run({ duration, adminToken }) {
94
94
  }
95
95
  }
96
96
 
97
+ module.exports = { run };
98
+
@@ -3,10 +3,10 @@
3
3
  * Measures maximum requests per second with high concurrency
4
4
  */
5
5
 
6
- import autocannon from 'autocannon';
7
- import { createMockServer, startDaemon, getFreePort, formatNumber } from './run.mjs';
6
+ const autocannon = require('autocannon');
7
+ const { createMockServer, startDaemon, getFreePort, formatNumber } = require('./run.js');
8
8
 
9
- export async function run({ duration, adminToken }) {
9
+ async function run({ duration, adminToken }) {
10
10
  // Setup
11
11
  const mockServer = await createMockServer();
12
12
  const proxyPort = await getFreePort();
@@ -42,3 +42,5 @@ export async function run({ duration, adminToken }) {
42
42
  }
43
43
  }
44
44
 
45
+ module.exports = { run };
46
+
@@ -9,14 +9,14 @@
9
9
  * - p99 <= 50ms
10
10
  *
11
11
  * Usage:
12
- * node bench/validate.mjs
13
- * node bench/validate.mjs --quick
14
- * FAST_PATH=1 node bench/validate.mjs
12
+ * node bench/validate.js
13
+ * node bench/validate.js --quick
14
+ * FAST_PATH=1 node bench/validate.js
15
15
  */
16
16
 
17
- import autocannon from 'autocannon';
18
- import { spawn } from 'node:child_process';
19
- import http from 'node:http';
17
+ const autocannon = require('autocannon');
18
+ const { spawn } = require('node:child_process');
19
+ const http = require('node:http');
20
20
 
21
21
  const DURATION = parseInt(process.env.BENCH_DURATION || '10', 10);
22
22
  const CONNECTIONS = parseInt(process.env.BENCH_CONNECTIONS || '50', 10);
@@ -72,7 +72,7 @@ async function createMockServer() {
72
72
  }
73
73
 
74
74
  async function startProxy(listenPort, targetPort) {
75
- const proc = spawn('node', ['portokd.mjs'], {
75
+ const proc = spawn('node', ['portokd.js'], {
76
76
  env: {
77
77
  ...process.env,
78
78
  LISTEN_PORT: String(listenPort),
package/package.json CHANGED
@@ -1,20 +1,19 @@
1
1
  {
2
2
  "name": "portok",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "Zero-downtime deployment proxy - routes traffic through a stable port to internal app instances with health-gated switching",
5
- "type": "module",
6
- "main": "portokd.mjs",
5
+ "main": "portokd.js",
7
6
  "bin": {
8
- "portok": "./portok.mjs",
9
- "portokd": "./portokd.mjs"
7
+ "portok": "./portok.js",
8
+ "portokd": "./portokd.js"
10
9
  },
11
10
  "scripts": {
12
- "start": "node portokd.mjs",
13
- "test": "node --test --test-timeout=120000 test/*.test.mjs",
14
- "test:watch": "node --test --watch test/*.test.mjs",
15
- "bench": "node bench/run.mjs",
16
- "bench:quick": "node bench/run.mjs --quick",
17
- "bench:json": "node bench/run.mjs --json"
11
+ "start": "node portokd.js",
12
+ "test": "node --test --test-timeout=120000 test/*.test.js",
13
+ "test:watch": "node --test --watch test/*.test.js",
14
+ "bench": "node bench/run.js",
15
+ "bench:quick": "node bench/run.js --quick",
16
+ "bench:json": "node bench/run.js --json"
18
17
  },
19
18
  "keywords": [
20
19
  "proxy",
@@ -36,4 +35,3 @@
36
35
  "node": ">=20.0.0"
37
36
  }
38
37
  }
39
-
@@ -5,6 +5,10 @@
5
5
  * Communicates with the daemon via HTTP to query status, metrics, and trigger switches.
6
6
  */
7
7
 
8
+ const fs = require('fs');
9
+ const path = require('path');
10
+ const crypto = require('crypto');
11
+
8
12
  // =============================================================================
9
13
  // ANSI Colors (only if TTY)
10
14
  // =============================================================================
@@ -61,9 +65,6 @@ function parseArgs(args) {
61
65
  // Env File Parser (for --instance support)
62
66
  // =============================================================================
63
67
 
64
- import fs from 'node:fs';
65
- import path from 'node:path';
66
-
67
68
  const ENV_FILE_DIR = '/etc/portok';
68
69
 
69
70
  /**
@@ -384,7 +385,7 @@ async function cmdInit(options) {
384
385
  function generateToken(length = 32) {
385
386
  const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
386
387
  let token = '';
387
- const randomBytes = require('crypto').randomBytes(length);
388
+ const randomBytes = crypto.randomBytes(length);
388
389
  for (let i = 0; i < length; i++) {
389
390
  token += chars[randomBytes[i] % chars.length];
390
391
  }
@@ -630,6 +631,92 @@ async function cmdHealth(baseUrl, token, options) {
630
631
  }
631
632
  }
632
633
 
634
+ // =============================================================================
635
+ // Systemctl Commands (start, stop, restart, enable, disable)
636
+ // =============================================================================
637
+
638
+ const { execSync } = require('node:child_process');
639
+
640
+ async function cmdSystemctl(action, name, options) {
641
+ if (!name) {
642
+ console.error(`${colors.red}Error:${colors.reset} Instance name is required`);
643
+ console.error(`Usage: portok ${action} <name>`);
644
+ return 1;
645
+ }
646
+
647
+ // Validate name exists
648
+ const envFilePath = path.join(ENV_FILE_DIR, `${name}.env`);
649
+ if (!fs.existsSync(envFilePath)) {
650
+ console.error(`${colors.red}Error:${colors.reset} Instance '${name}' not found`);
651
+ console.error(`Make sure ${envFilePath} exists. Run ${colors.cyan}portok add ${name}${colors.reset} to create it.`);
652
+ return 1;
653
+ }
654
+
655
+ const serviceName = `portok@${name}`;
656
+ const actionVerb = {
657
+ start: 'Starting',
658
+ stop: 'Stopping',
659
+ restart: 'Restarting',
660
+ enable: 'Enabling',
661
+ disable: 'Disabling',
662
+ }[action];
663
+
664
+ console.log(`${colors.cyan}${actionVerb} ${serviceName}...${colors.reset}`);
665
+
666
+ try {
667
+ execSync(`systemctl ${action} ${serviceName}`, { stdio: 'inherit' });
668
+
669
+ const pastVerb = {
670
+ start: 'started',
671
+ stop: 'stopped',
672
+ restart: 'restarted',
673
+ enable: 'enabled',
674
+ disable: 'disabled',
675
+ }[action];
676
+
677
+ console.log(`${colors.green}✓ Service ${serviceName} ${pastVerb}${colors.reset}`);
678
+
679
+ // For start/restart, show status after
680
+ if (action === 'start' || action === 'restart') {
681
+ console.log('');
682
+ try {
683
+ execSync(`systemctl status ${serviceName} --no-pager -l`, { stdio: 'inherit' });
684
+ } catch {
685
+ // Status might return non-zero even when running, ignore
686
+ }
687
+ }
688
+
689
+ return 0;
690
+ } catch (err) {
691
+ console.error(`${colors.red}Failed to ${action} ${serviceName}${colors.reset}`);
692
+ if (err.status === 1) {
693
+ console.error(`${colors.yellow}Hint:${colors.reset} This command may require sudo privileges.`);
694
+ console.error(`Try: ${colors.cyan}sudo portok ${action} ${name}${colors.reset}`);
695
+ }
696
+ return 1;
697
+ }
698
+ }
699
+
700
+ async function cmdLogs(name, options) {
701
+ if (!name) {
702
+ console.error(`${colors.red}Error:${colors.reset} Instance name is required`);
703
+ console.error('Usage: portok logs <name> [--follow]');
704
+ return 1;
705
+ }
706
+
707
+ const serviceName = `portok@${name}`;
708
+ const followFlag = options.follow || options.f ? '-f' : '';
709
+ const lines = options.lines || options.n || '50';
710
+
711
+ try {
712
+ execSync(`journalctl -u ${serviceName} -n ${lines} ${followFlag} --no-pager`, { stdio: 'inherit' });
713
+ return 0;
714
+ } catch (err) {
715
+ console.error(`${colors.red}Failed to get logs for ${serviceName}${colors.reset}`);
716
+ return 1;
717
+ }
718
+ }
719
+
633
720
  function showHelp() {
634
721
  console.log(`
635
722
  ${colors.bold}portok${colors.reset} - CLI for portokd zero-downtime proxy daemon
@@ -643,6 +730,14 @@ ${colors.bold}COMMANDS${colors.reset}
643
730
  add <name> Create a new service instance
644
731
  list List all configured instances and their status
645
732
 
733
+ ${colors.cyan}Service Control:${colors.reset}
734
+ start <name> Start a portok service (systemctl start portok@<name>)
735
+ stop <name> Stop a portok service
736
+ restart <name> Restart a portok service
737
+ enable <name> Enable service at boot
738
+ disable <name> Disable service at boot
739
+ logs <name> Show service logs (journalctl)
740
+
646
741
  ${colors.cyan}Operations:${colors.reset}
647
742
  status Show current proxy status (activePort, drainUntil, lastSwitch)
648
743
  metrics Show proxy metrics (requests, errors, health, RPS)
@@ -662,17 +757,28 @@ ${colors.bold}OPTIONS${colors.reset}
662
757
  --health <path> Health check path (default: /health)
663
758
  --force Overwrite existing config
664
759
 
760
+ ${colors.dim}For 'logs' command:${colors.reset}
761
+ --follow, -f Follow log output
762
+ --lines, -n Number of lines to show (default: 50)
763
+
665
764
  ${colors.bold}EXAMPLES${colors.reset}
666
765
  ${colors.dim}# Initialize portok (run once, requires sudo)${colors.reset}
667
766
  sudo portok init
668
767
 
669
- ${colors.dim}# Create a new service${colors.reset}
768
+ ${colors.dim}# Create and start a new service${colors.reset}
670
769
  sudo portok add api --port 3001 --target 8001
671
- sudo portok add web --port 3002 --target 8002
770
+ sudo portok start api
771
+ sudo portok enable api
672
772
 
673
773
  ${colors.dim}# List all instances${colors.reset}
674
774
  portok list
675
775
 
776
+ ${colors.dim}# Service management${colors.reset}
777
+ sudo portok start api
778
+ sudo portok stop api
779
+ sudo portok restart api
780
+ portok logs api --follow
781
+
676
782
  ${colors.dim}# Check status of an instance${colors.reset}
677
783
  portok status --instance api
678
784
 
@@ -682,9 +788,6 @@ ${colors.bold}EXAMPLES${colors.reset}
682
788
  ${colors.dim}# Get metrics as JSON${colors.reset}
683
789
  portok metrics --instance api --json
684
790
 
685
- ${colors.dim}# Direct URL mode (without instance)${colors.reset}
686
- portok status --url http://127.0.0.1:3000 --token mysecret
687
-
688
791
  ${colors.bold}MULTI-INSTANCE${colors.reset}
689
792
  When using --instance, the CLI reads /etc/portok/<name>.env
690
793
  to resolve LISTEN_PORT and ADMIN_TOKEN for that instance.
@@ -710,7 +813,7 @@ async function main() {
710
813
  }
711
814
 
712
815
  // Management commands (don't require daemon connection)
713
- const managementCommands = ['init', 'add', 'list'];
816
+ const managementCommands = ['init', 'add', 'list', 'start', 'stop', 'restart', 'enable', 'disable', 'logs'];
714
817
 
715
818
  if (managementCommands.includes(args.command)) {
716
819
  let exitCode = 1;
@@ -724,6 +827,16 @@ async function main() {
724
827
  case 'list':
725
828
  exitCode = await cmdList(args.options);
726
829
  break;
830
+ case 'start':
831
+ case 'stop':
832
+ case 'restart':
833
+ case 'enable':
834
+ case 'disable':
835
+ exitCode = await cmdSystemctl(args.command, args.positional[0], args.options);
836
+ break;
837
+ case 'logs':
838
+ exitCode = await cmdLogs(args.positional[0], args.options);
839
+ break;
727
840
  }
728
841
  process.exit(exitCode);
729
842
  }
@@ -13,10 +13,10 @@
13
13
  * - Connection header stripping for proper keep-alive upstream
14
14
  */
15
15
 
16
- import http from 'node:http';
17
- import fs from 'node:fs';
18
- import crypto from 'node:crypto';
19
- import httpProxy from 'http-proxy';
16
+ const http = require('http');
17
+ const fs = require('fs');
18
+ const crypto = require('crypto');
19
+ const httpProxy = require('http-proxy');
20
20
 
21
21
  // =============================================================================
22
22
  // Configuration
@@ -779,7 +779,7 @@ process.on('SIGINT', () => {
779
779
  });
780
780
 
781
781
  // Export for testing
782
- export {
782
+ module.exports = {
783
783
  config,
784
784
  state,
785
785
  metrics,
@@ -791,3 +791,4 @@ export {
791
791
  server,
792
792
  upstreamAgent,
793
793
  };
794
+
@@ -3,10 +3,10 @@
3
3
  * Tests the portok CLI commands
4
4
  */
5
5
 
6
- import { describe, it, before, after } from 'node:test';
7
- import assert from 'node:assert';
8
- import { spawn, execSync } from 'node:child_process';
9
- import { createMockServer, getFreePort, waitFor } from './helpers/mock-server.mjs';
6
+ const { describe, it, before, after } = require('node:test');
7
+ const assert = require('node:assert');
8
+ const { spawn, execSync } = require('node:child_process');
9
+ const { createMockServer, getFreePort, waitFor } = require('./helpers/mock-server.js');
10
10
 
11
11
  describe('CLI', () => {
12
12
  let mockServer;
@@ -18,7 +18,7 @@ describe('CLI', () => {
18
18
  mockServer = await createMockServer({ port: 0 });
19
19
  proxyPort = await getFreePort();
20
20
 
21
- daemonProcess = spawn('node', ['portokd.mjs'], {
21
+ daemonProcess = spawn('node', ['portokd.js'], {
22
22
  env: {
23
23
  ...process.env,
24
24
  LISTEN_PORT: String(proxyPort),
@@ -64,7 +64,7 @@ describe('CLI', () => {
64
64
  };
65
65
 
66
66
  try {
67
- const output = execSync(`node portok.mjs ${args}`, {
67
+ const output = execSync(`node portok.js ${args}`, {
68
68
  encoding: 'utf-8',
69
69
  env,
70
70
  timeout: 15000,
@@ -218,3 +218,4 @@ describe('CLI', () => {
218
218
  });
219
219
  });
220
220
  });
221
+
@@ -3,11 +3,11 @@
3
3
  * Tests that existing connections drain to old port while new connections go to new port
4
4
  */
5
5
 
6
- import { describe, it, before, after } from 'node:test';
7
- import assert from 'node:assert';
8
- import http from 'node:http';
9
- import { spawn } from 'node:child_process';
10
- import { createMockServer, getFreePort, waitFor } from './helpers/mock-server.mjs';
6
+ const { describe, it, before, after } = require('node:test');
7
+ const assert = require('node:assert');
8
+ const http = require('node:http');
9
+ const { spawn } = require('node:child_process');
10
+ const { createMockServer, getFreePort, waitFor } = require('./helpers/mock-server.js');
11
11
 
12
12
  describe('Connection Draining', () => {
13
13
  let mockServer1;
@@ -24,7 +24,7 @@ describe('Connection Draining', () => {
24
24
  proxyPort = await getFreePort();
25
25
 
26
26
  // Start daemon with longer drain time for testing
27
- daemonProcess = spawn('node', ['portokd.mjs'], {
27
+ daemonProcess = spawn('node', ['portokd.js'], {
28
28
  env: {
29
29
  ...process.env,
30
30
  LISTEN_PORT: String(proxyPort),
@@ -148,7 +148,7 @@ describe('Connection Draining', () => {
148
148
  it('should clear drainUntil after drain period', async () => {
149
149
  // Use a fresh daemon with shorter drain for this test
150
150
  const shortDrainPort = await getFreePort();
151
- const shortDaemon = spawn('node', ['portokd.mjs'], {
151
+ const shortDaemon = spawn('node', ['portokd.js'], {
152
152
  env: {
153
153
  ...process.env,
154
154
  LISTEN_PORT: String(shortDrainPort),
@@ -3,15 +3,15 @@
3
3
  * Provides configurable responses, delays, and health status
4
4
  */
5
5
 
6
- import http from 'node:http';
6
+ const http = require('http');
7
7
 
8
- // Dynamically import ws to handle when not available
9
- let WebSocketServer;
8
+ // Try to load ws, handle when not available
9
+ let WebSocketServer = null;
10
10
  try {
11
- const ws = await import('ws');
11
+ const ws = require('ws');
12
12
  WebSocketServer = ws.WebSocketServer;
13
13
  } catch {
14
- WebSocketServer = null;
14
+ // ws not available
15
15
  }
16
16
 
17
17
  /**
@@ -24,7 +24,7 @@ try {
24
24
  * @param {boolean} options.enableWebSocket - Enable WebSocket support (default: true)
25
25
  * @returns {Promise<MockServer>}
26
26
  */
27
- export async function createMockServer(options = {}) {
27
+ async function createMockServer(options = {}) {
28
28
  const config = {
29
29
  port: options.port || 0,
30
30
  healthStatus: options.healthStatus ?? 200,
@@ -202,7 +202,7 @@ export async function createMockServer(options = {}) {
202
202
  * Create the portokd server for testing
203
203
  * Sets up environment and spawns the daemon
204
204
  */
205
- export async function createTestDaemon(options = {}) {
205
+ async function createTestDaemon(options = {}) {
206
206
  const {
207
207
  listenPort = 0,
208
208
  targetPort,
@@ -226,7 +226,7 @@ export async function createTestDaemon(options = {}) {
226
226
  process.env.STATE_FILE = `/tmp/portok-test-${Date.now()}.json`;
227
227
 
228
228
  // Import the daemon (will start based on env vars)
229
- const daemon = await import('../../portokd.mjs');
229
+ const daemon = require('../../portokd.js');
230
230
 
231
231
  // Wait for server to be ready
232
232
  await new Promise(resolve => setTimeout(resolve, 100));
@@ -248,7 +248,7 @@ export async function createTestDaemon(options = {}) {
248
248
  /**
249
249
  * Helper to make admin requests
250
250
  */
251
- export async function adminRequest(baseUrl, endpoint, options = {}) {
251
+ async function adminRequest(baseUrl, endpoint, options = {}) {
252
252
  const { method = 'GET', token = 'test-token-12345', body } = options;
253
253
 
254
254
  const fetchOptions = {
@@ -276,7 +276,7 @@ export async function adminRequest(baseUrl, endpoint, options = {}) {
276
276
  /**
277
277
  * Helper to wait for a condition
278
278
  */
279
- export async function waitFor(condition, timeout = 5000, interval = 100) {
279
+ async function waitFor(condition, timeout = 5000, interval = 100) {
280
280
  const startTime = Date.now();
281
281
 
282
282
  while (Date.now() - startTime < timeout) {
@@ -292,7 +292,7 @@ export async function waitFor(condition, timeout = 5000, interval = 100) {
292
292
  /**
293
293
  * Helper to get a free port
294
294
  */
295
- export async function getFreePort() {
295
+ async function getFreePort() {
296
296
  return new Promise((resolve, reject) => {
297
297
  const server = http.createServer();
298
298
  server.listen(0, '127.0.0.1', () => {
@@ -303,3 +303,11 @@ export async function getFreePort() {
303
303
  });
304
304
  }
305
305
 
306
+ module.exports = {
307
+ createMockServer,
308
+ createTestDaemon,
309
+ adminRequest,
310
+ waitFor,
311
+ getFreePort,
312
+ };
313
+
@@ -3,10 +3,10 @@
3
3
  * Tests metrics collection: counters, RPS, inflight tracking
4
4
  */
5
5
 
6
- import { describe, it, before, after } from 'node:test';
7
- import assert from 'node:assert';
8
- import { spawn } from 'node:child_process';
9
- import { createMockServer, getFreePort, waitFor } from './helpers/mock-server.mjs';
6
+ const { describe, it, before, after } = require('node:test');
7
+ const assert = require('node:assert');
8
+ const { spawn } = require('node:child_process');
9
+ const { createMockServer, getFreePort, waitFor } = require('./helpers/mock-server.js');
10
10
 
11
11
  describe('Metrics', () => {
12
12
  let mockServer;
@@ -18,7 +18,7 @@ describe('Metrics', () => {
18
18
  mockServer = await createMockServer({ port: 0 });
19
19
  proxyPort = await getFreePort();
20
20
 
21
- daemonProcess = spawn('node', ['portokd.mjs'], {
21
+ daemonProcess = spawn('node', ['portokd.js'], {
22
22
  env: {
23
23
  ...process.env,
24
24
  LISTEN_PORT: String(proxyPort),
@@ -279,7 +279,7 @@ describe('Metrics', () => {
279
279
  it('should record last proxy error details', async () => {
280
280
  // Create a daemon with invalid target port
281
281
  const badPort = await getFreePort();
282
- const badDaemon = spawn('node', ['portokd.mjs'], {
282
+ const badDaemon = spawn('node', ['portokd.js'], {
283
283
  env: {
284
284
  ...process.env,
285
285
  LISTEN_PORT: String(badPort),
@@ -3,12 +3,12 @@
3
3
  * Tests HTTP proxying, WebSocket upgrades, and streaming
4
4
  */
5
5
 
6
- import { describe, it, before, after, beforeEach } from 'node:test';
7
- import assert from 'node:assert';
8
- import http from 'node:http';
9
- import { spawn } from 'node:child_process';
10
- import { createMockServer, getFreePort, waitFor } from './helpers/mock-server.mjs';
11
- import WebSocket from 'ws';
6
+ const { describe, it, before, after, beforeEach } = require('node:test');
7
+ const assert = require('node:assert');
8
+ const http = require('node:http');
9
+ const { spawn } = require('node:child_process');
10
+ const { createMockServer, getFreePort, waitFor } = require('./helpers/mock-server.js');
11
+ const WebSocket = require('ws');
12
12
 
13
13
  describe('Proxy Core', () => {
14
14
  let mockServer;
@@ -24,7 +24,7 @@ describe('Proxy Core', () => {
24
24
  proxyPort = await getFreePort();
25
25
 
26
26
  // Start the daemon as a subprocess
27
- daemonProcess = spawn('node', ['portokd.mjs'], {
27
+ daemonProcess = spawn('node', ['portokd.js'], {
28
28
  env: {
29
29
  ...process.env,
30
30
  LISTEN_PORT: String(proxyPort),
@@ -36,6 +36,7 @@ describe('Proxy Core', () => {
36
36
  ROLLBACK_CHECK_EVERY_MS: '500',
37
37
  ROLLBACK_FAIL_THRESHOLD: '3',
38
38
  ADMIN_RATE_LIMIT: '1000',
39
+ ENABLE_XFWD: '1',
39
40
  },
40
41
  stdio: 'pipe',
41
42
  });
@@ -3,10 +3,10 @@
3
3
  * Tests automatic rollback when health checks fail consecutively
4
4
  */
5
5
 
6
- import { describe, it, before, after } from 'node:test';
7
- import assert from 'node:assert';
8
- import { spawn } from 'node:child_process';
9
- import { createMockServer, getFreePort, waitFor } from './helpers/mock-server.mjs';
6
+ const { describe, it, before, after } = require('node:test');
7
+ const assert = require('node:assert');
8
+ const { spawn } = require('node:child_process');
9
+ const { createMockServer, getFreePort, waitFor } = require('./helpers/mock-server.js');
10
10
 
11
11
  describe('Auto Rollback', () => {
12
12
  const adminToken = 'test-token-rollback';
@@ -24,7 +24,7 @@ describe('Auto Rollback', () => {
24
24
  proxyPort = await getFreePort();
25
25
 
26
26
  // Start daemon with aggressive rollback settings for testing
27
- daemonProcess = spawn('node', ['portokd.mjs'], {
27
+ daemonProcess = spawn('node', ['portokd.js'], {
28
28
  env: {
29
29
  ...process.env,
30
30
  LISTEN_PORT: String(proxyPort),
@@ -126,7 +126,7 @@ describe('Auto Rollback', () => {
126
126
 
127
127
  proxyPort = await getFreePort();
128
128
 
129
- daemonProcess = spawn('node', ['portokd.mjs'], {
129
+ daemonProcess = spawn('node', ['portokd.js'], {
130
130
  env: {
131
131
  ...process.env,
132
132
  LISTEN_PORT: String(proxyPort),
@@ -208,7 +208,7 @@ describe('Auto Rollback', () => {
208
208
 
209
209
  proxyPort = await getFreePort();
210
210
 
211
- daemonProcess = spawn('node', ['portokd.mjs'], {
211
+ daemonProcess = spawn('node', ['portokd.js'], {
212
212
  env: {
213
213
  ...process.env,
214
214
  LISTEN_PORT: String(proxyPort),
@@ -280,7 +280,7 @@ describe('Auto Rollback', () => {
280
280
 
281
281
  proxyPort = await getFreePort();
282
282
 
283
- daemonProcess = spawn('node', ['portokd.mjs'], {
283
+ daemonProcess = spawn('node', ['portokd.js'], {
284
284
  env: {
285
285
  ...process.env,
286
286
  LISTEN_PORT: String(proxyPort),
@@ -3,10 +3,10 @@
3
3
  * Tests token validation, IP allowlist, and rate limiting
4
4
  */
5
5
 
6
- import { describe, it, before, after } from 'node:test';
7
- import assert from 'node:assert';
8
- import { spawn } from 'node:child_process';
9
- import { createMockServer, getFreePort, waitFor } from './helpers/mock-server.mjs';
6
+ const { describe, it, before, after } = require('node:test');
7
+ const assert = require('node:assert');
8
+ const { spawn } = require('node:child_process');
9
+ const { createMockServer, getFreePort, waitFor } = require('./helpers/mock-server.js');
10
10
 
11
11
  describe('Security', () => {
12
12
  let mockServer;
@@ -18,7 +18,7 @@ describe('Security', () => {
18
18
  mockServer = await createMockServer({ port: 0 });
19
19
  proxyPort = await getFreePort();
20
20
 
21
- daemonProcess = spawn('node', ['portokd.mjs'], {
21
+ daemonProcess = spawn('node', ['portokd.js'], {
22
22
  env: {
23
23
  ...process.env,
24
24
  LISTEN_PORT: String(proxyPort),
@@ -187,7 +187,7 @@ describe('Security - Rate Limit Test', () => {
187
187
  proxyPort = await getFreePort();
188
188
 
189
189
  // Use rate limit of 15 - waitFor uses ~5 requests, then we make 15 more
190
- daemonProcess = spawn('node', ['portokd.mjs'], {
190
+ daemonProcess = spawn('node', ['portokd.js'], {
191
191
  env: {
192
192
  ...process.env,
193
193
  LISTEN_PORT: String(proxyPort),
@@ -3,10 +3,10 @@
3
3
  * Tests the /__switch endpoint with health checks
4
4
  */
5
5
 
6
- import { describe, it, before, after, beforeEach } from 'node:test';
7
- import assert from 'node:assert';
8
- import { spawn } from 'node:child_process';
9
- import { createMockServer, getFreePort, waitFor, adminRequest } from './helpers/mock-server.mjs';
6
+ const { describe, it, before, after, beforeEach } = require('node:test');
7
+ const assert = require('node:assert');
8
+ const { spawn } = require('node:child_process');
9
+ const { createMockServer, getFreePort, waitFor, adminRequest } = require('./helpers/mock-server.js');
10
10
 
11
11
  describe('Health-Gated Switching', () => {
12
12
  let mockServer1;
@@ -24,7 +24,7 @@ describe('Health-Gated Switching', () => {
24
24
  proxyPort = await getFreePort();
25
25
 
26
26
  // Start the daemon
27
- daemonProcess = spawn('node', ['portokd.mjs'], {
27
+ daemonProcess = spawn('node', ['portokd.js'], {
28
28
  env: {
29
29
  ...process.env,
30
30
  LISTEN_PORT: String(proxyPort),