split-exec 0.0.1

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/.gitlab-ci.yml ADDED
@@ -0,0 +1,16 @@
1
+
2
+ stages:
3
+ - publish
4
+
5
+ publish:
6
+ stage: publish
7
+ image: node:18-alpine
8
+ only:
9
+ - main
10
+ script:
11
+ - npm publish
12
+ id_tokens:
13
+ NPM_ID_TOKEN:
14
+ aud: "npm:registry.npmjs.org"
15
+ SIGSTORE_ID_TOKEN:
16
+ aud: sigstore
package/README.md ADDED
@@ -0,0 +1,65 @@
1
+ # [split-exec](https://gitlab.com/GCSBOSS/split-exec)
2
+
3
+ A lightweight Node.js TUI (Text User Interface) to run multiple commands side-by-side in a split terminal view.
4
+
5
+ **Features:**
6
+ - 🖥️ **Split Screen:** Automatically divides terminal width for 2, 3, 4+ commands.
7
+ - 📜 **Smart Scrolling:** Auto-scrolls to bottom, but pauses if you scroll up to read history.
8
+ - 🔄 **Restart:** Press `r` to instantly kill and restart any specific command.
9
+ - 🖱️ **Interactive:** Supports Mouse wheel, clicking, and keyboard navigation.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ npm install split-exec
15
+ # or globally
16
+ npm install -g split-exec
17
+
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### 1. CLI Usage
23
+
24
+ Run commands directly from your terminal. Great for on-the-fly monitoring.
25
+
26
+ ```bash
27
+ # Run 3 commands side by side
28
+ npx split-exec "ping google.com" "ping 1.1.1.1" "ls -R /"
29
+
30
+ ```
31
+
32
+ ### 2. Programmatic Usage
33
+
34
+ Create a dashboard file (e.g., `dev-dashboard.js`) for your project.
35
+
36
+ ```javascript
37
+ const { splitExec } = require('split-exec');
38
+
39
+ splitExec([
40
+ // Simple string
41
+ 'npm run server',
42
+
43
+ // Object for more control
44
+ {
45
+ title: 'Jest Tests',
46
+ cmd: 'npm',
47
+ args: ['test', '--', '--watch']
48
+ }
49
+ ]);
50
+
51
+ ```
52
+
53
+ ## Controls
54
+
55
+ | Key | Action |
56
+ | --- | --- |
57
+ | **Tab / Right** | Focus next column |
58
+ | **Shift+Tab / Left** | Focus previous column |
59
+ | **Up / Down** | Scroll history |
60
+ | **r** | **Restart** the focused command |
61
+ | **q / Esc** | Quit |
62
+
63
+ ## License
64
+
65
+ MIT
package/bin/cli.js ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+
3
+ const lib = require('../src/index.js');
4
+ const args = process.argv.slice(2);
5
+
6
+ if (args.length === 0) {
7
+ console.log('Usage: split-exec "command 1" "command 2" ...');
8
+ process.exit(1);
9
+ }
10
+
11
+ lib.splitExec(args);
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "split-exec",
3
+ "version": "0.0.1",
4
+ "description": "Run multiple commands in parallel with a split-screen TUI dashboard.",
5
+ "main": "src/index.js",
6
+ "bin": {
7
+ "split-exec": "bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "test": "echo \"Error: no test specified\" && exit 1"
11
+ },
12
+ "keywords": [
13
+ "cli",
14
+ "dashboard",
15
+ "parallel",
16
+ "terminal",
17
+ "split-screen",
18
+ "blessed"
19
+ ],
20
+ "author": "Your Name",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "blessed": "^0.1.81",
24
+ "iconv-lite": "^0.6.3"
25
+ }
26
+ }
package/src/index.js ADDED
@@ -0,0 +1,150 @@
1
+ const blessed = require('blessed');
2
+ const { spawn, execSync } = require('child_process');
3
+ const iconv = require('iconv-lite');
4
+
5
+ // 1. Detect System Encoding (Robust Version)
6
+ function getSystemEncoding() {
7
+ // Mac/Linux are almost always UTF-8
8
+ if (process.platform !== 'win32') return 'utf8';
9
+
10
+ try {
11
+ // Run 'chcp' to find active code page
12
+ // output example: "Página de código ativa: 850"
13
+ const output = execSync('chcp').toString();
14
+
15
+ // Regex: Find ALL sequences of digits
16
+ const matches = output.match(/\d+/g);
17
+
18
+ if (matches && matches.length > 0) {
19
+ // The code page is invariably the last number in the string
20
+ const pageId = matches[matches.length - 1];
21
+ const encoding = 'cp' + pageId; // e.g. 'cp850'
22
+
23
+ // Ensure iconv supports it, otherwise fallback
24
+ if (iconv.encodingExists(encoding)) {
25
+ return encoding;
26
+ }
27
+ }
28
+ } catch (e) {
29
+ // Ignore error
30
+ }
31
+
32
+ // CRITICAL FALLBACK for Portuguese/Western Europe Windows
33
+ // If we couldn't detect it, assume CP850 (Standard OEM), NOT UTF-8.
34
+ return 'cp850';
35
+ }
36
+
37
+ const SYSTEM_ENCODING = getSystemEncoding();
38
+
39
+ function start(commands) {
40
+ const screen = blessed.screen({
41
+ smartCSR: true,
42
+ title: `split-exec`,
43
+ dockBorders: true,
44
+ mouse: true
45
+ });
46
+
47
+ const configs = commands.map((item, index) => {
48
+ if (typeof item === 'string') {
49
+ return { cmd: item, args: [], title: item };
50
+ }
51
+ return {
52
+ cmd: item.cmd,
53
+ args: item.args || [],
54
+ title: item.title || `Command ${index + 1}`
55
+ };
56
+ });
57
+
58
+ const boxes = [];
59
+
60
+ configs.forEach((config, i) => {
61
+ const widthPercent = Math.floor(100 / configs.length);
62
+
63
+ const box = blessed.box({
64
+ top: 0,
65
+ left: `${i * widthPercent}%`,
66
+ width: `${widthPercent}%`,
67
+ height: '100%',
68
+ label: ` ${config.title} `,
69
+ tags: true,
70
+ keys: true,
71
+ mouse: true,
72
+ border: { type: 'line' },
73
+ style: { fg: 'white', border: { fg: '#f0f0f0' }, focus: { border: { fg: 'green' } } },
74
+ scrollable: true,
75
+ alwaysScroll: true,
76
+ scrollbar: { ch: ' ', bg: 'blue' }
77
+ });
78
+
79
+ boxes.push(box);
80
+ screen.append(box);
81
+
82
+ box.key('r', () => spawnInBox(box, config, screen));
83
+ spawnInBox(box, config, screen);
84
+ });
85
+
86
+ if (boxes.length > 0) boxes[0].focus();
87
+
88
+ screen.key(['tab', 'right'], () => { screen.focusNext(); screen.render(); });
89
+ screen.key(['S-tab', 'left'], () => { screen.focusPrevious(); screen.render(); });
90
+ screen.key(['escape', 'q', 'C-c'], () => process.exit(0));
91
+
92
+ const tip = blessed.box({
93
+ bottom: 0, right: 0, height: 1, width: 'shrink',
94
+ content: ` [TAB] Nav | [r] Restart | [q] Quit | Enc: ${SYSTEM_ENCODING} `,
95
+ style: { bg: 'blue', fg: 'white' }
96
+ });
97
+ screen.append(tip);
98
+
99
+ screen.render();
100
+ }
101
+
102
+ function spawnInBox(box, config, screen) {
103
+ if (box.activeProcess) {
104
+ try { box.activeProcess.kill(); } catch (e) {}
105
+ box.pushLine(`{yellow-fg}--- RESTARTING ---{/}`);
106
+ } else {
107
+ box.setContent('');
108
+ }
109
+
110
+ box.border.fg = 'green';
111
+ screen.render();
112
+
113
+ const write = (msg) => {
114
+ const isAtBottom = (box.childBase + box.height) >= box.getScrollHeight();
115
+ box.pushLine(msg);
116
+ if (isAtBottom) box.setScrollPerc(100);
117
+ };
118
+
119
+ try {
120
+ // Spawn with shell:true to support standard windows commands
121
+ const child = spawn(config.cmd, config.args, { shell: true });
122
+
123
+ box.activeProcess = child;
124
+
125
+ // IMPORTANT: Treat output as raw buffer
126
+ child.stdout.on('data', d => {
127
+ // Decode Buffer -> String using detected encoding
128
+ const cleanStr = iconv.decode(d, SYSTEM_ENCODING);
129
+ write(cleanStr.trim());
130
+ screen.render();
131
+ });
132
+
133
+ child.stderr.on('data', d => {
134
+ const cleanStr = iconv.decode(d, SYSTEM_ENCODING);
135
+ write(`{red-fg}${cleanStr.trim()}{/}`);
136
+ screen.render();
137
+ });
138
+
139
+ child.on('close', (code) => {
140
+ box.activeProcess = null;
141
+ box.border.fg = '#f0f0f0';
142
+ write(code === 0 ? `{green-fg}Done (0){/}` : `{red-fg}Exit (${code}){/}`);
143
+ screen.render();
144
+ });
145
+ } catch (e) {
146
+ write(`{red-fg}Error: ${e.message}{/}`);
147
+ }
148
+ }
149
+
150
+ module.exports = { splitExec: start };