local-cmd-runner 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/bin/local-cmd.js +2 -0
- package/package.json +28 -0
- package/public/app.js +100 -0
- package/public/index.html +52 -0
- package/public/style.css +246 -0
- package/server.js +55 -0
package/bin/local-cmd.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "local-cmd-runner",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Run local shell commands from a web page",
|
|
5
|
+
"homepage": "https://github.com/codewoow/local_cmd#readme",
|
|
6
|
+
"bugs": {
|
|
7
|
+
"url": "https://github.com/codewoow/local_cmd/issues"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/codewoow/local_cmd.git"
|
|
12
|
+
},
|
|
13
|
+
"license": "ISC",
|
|
14
|
+
"author": "hero",
|
|
15
|
+
"type": "commonjs",
|
|
16
|
+
"main": "server.js",
|
|
17
|
+
"bin": {
|
|
18
|
+
"local-cmd": "bin/local-cmd.js"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"start": "node ./bin/local-cmd.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"express": "^4.19.2",
|
|
25
|
+
"socket.io": "^4.7.5"
|
|
26
|
+
},
|
|
27
|
+
"devDependencies": {}
|
|
28
|
+
}
|
package/public/app.js
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
const socket = io();
|
|
2
|
+
|
|
3
|
+
const cmdInput = document.getElementById('cmdInput');
|
|
4
|
+
const runBtn = document.getElementById('runBtn');
|
|
5
|
+
const outputEl = document.getElementById('output');
|
|
6
|
+
const presetBtns = document.querySelectorAll('.btn-preset');
|
|
7
|
+
|
|
8
|
+
let currentRunId = 0;
|
|
9
|
+
|
|
10
|
+
function runCommand(cmd) {
|
|
11
|
+
if (!cmd.trim()) return;
|
|
12
|
+
|
|
13
|
+
const runId = ++currentRunId;
|
|
14
|
+
|
|
15
|
+
// Create a block for this specific command execution
|
|
16
|
+
const block = document.createElement('div');
|
|
17
|
+
block.className = 'command-block';
|
|
18
|
+
block.id = `run_${runId}`;
|
|
19
|
+
|
|
20
|
+
const cmdHeader = document.createElement('div');
|
|
21
|
+
cmdHeader.className = 'system cmd-header';
|
|
22
|
+
cmdHeader.textContent = `$ ${cmd}`;
|
|
23
|
+
|
|
24
|
+
const outputContainer = document.createElement('div');
|
|
25
|
+
outputContainer.className = 'command-output-inner';
|
|
26
|
+
|
|
27
|
+
block.appendChild(cmdHeader);
|
|
28
|
+
block.appendChild(outputContainer);
|
|
29
|
+
|
|
30
|
+
// Prepend makes the new commands appear at the TOP
|
|
31
|
+
outputEl.prepend(block);
|
|
32
|
+
|
|
33
|
+
// Send to server
|
|
34
|
+
socket.emit('run_command', { cmd: cmd, runId: runId });
|
|
35
|
+
cmdInput.value = '';
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
runBtn.addEventListener('click', () => {
|
|
39
|
+
runCommand(cmdInput.value);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
cmdInput.addEventListener('keypress', (e) => {
|
|
43
|
+
if (e.key === 'Enter') {
|
|
44
|
+
runCommand(cmdInput.value);
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
presetBtns.forEach(btn => {
|
|
49
|
+
btn.addEventListener('click', () => {
|
|
50
|
+
const cmd = btn.getAttribute('data-cmd');
|
|
51
|
+
runCommand(cmd);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
socket.on('cmd_start', (data) => {
|
|
56
|
+
// Command started
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
function getOutputContainer(runId) {
|
|
60
|
+
// If runId mapped correctly, return it. Otherwise just use a fallback block or create one.
|
|
61
|
+
let block = document.getElementById(`run_${runId}`);
|
|
62
|
+
if (!block) {
|
|
63
|
+
block = document.createElement('div');
|
|
64
|
+
block.className = 'command-block';
|
|
65
|
+
block.id = `run_${runId}`;
|
|
66
|
+
const outputContainer = document.createElement('div');
|
|
67
|
+
outputContainer.className = 'command-output-inner';
|
|
68
|
+
block.appendChild(outputContainer);
|
|
69
|
+
outputEl.prepend(block);
|
|
70
|
+
}
|
|
71
|
+
return block.querySelector('.command-output-inner');
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
socket.on('cmd_output', (data) => {
|
|
75
|
+
const container = getOutputContainer(data.runId || data.id);
|
|
76
|
+
const span = document.createElement('span');
|
|
77
|
+
span.textContent = data.data;
|
|
78
|
+
|
|
79
|
+
if (data.type === 'stderr') {
|
|
80
|
+
span.classList.add('error');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
container.appendChild(span);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
socket.on('cmd_error', (data) => {
|
|
87
|
+
const container = getOutputContainer(data.runId || data.id);
|
|
88
|
+
const div = document.createElement('div');
|
|
89
|
+
div.textContent = `\n[Error]: ${data.error}\n`;
|
|
90
|
+
div.classList.add('error');
|
|
91
|
+
container.appendChild(div);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
socket.on('cmd_close', (data) => {
|
|
95
|
+
const container = getOutputContainer(data.runId || data.id);
|
|
96
|
+
const div = document.createElement('div');
|
|
97
|
+
div.textContent = `\n[Process exited with code ${data.code}]\n\n`;
|
|
98
|
+
div.classList.add('system');
|
|
99
|
+
container.appendChild(div);
|
|
100
|
+
});
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
7
|
+
<title>Local Command Runner</title>
|
|
8
|
+
<link rel="stylesheet" href="style.css">
|
|
9
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&family=Fira+Code&display=swap"
|
|
10
|
+
rel="stylesheet">
|
|
11
|
+
</head>
|
|
12
|
+
|
|
13
|
+
<body>
|
|
14
|
+
<div class="container">
|
|
15
|
+
<header>
|
|
16
|
+
<h1>Cmd Runner</h1>
|
|
17
|
+
<p>Execute local shell and cmd commands directly from your browser.</p>
|
|
18
|
+
</header>
|
|
19
|
+
|
|
20
|
+
<div class="presets">
|
|
21
|
+
<h3>Preset OpenClaw Commands</h3>
|
|
22
|
+
<div class="preset-buttons">
|
|
23
|
+
<button class="btn-preset" data-cmd="openclaw --help">Help</button>
|
|
24
|
+
<button class="btn-preset" data-cmd="openclaw gateway">Start</button>
|
|
25
|
+
</div>
|
|
26
|
+
</div>
|
|
27
|
+
|
|
28
|
+
<div class="card">
|
|
29
|
+
<div class="input-group">
|
|
30
|
+
<input type="text" id="cmdInput" placeholder="Enter shell or cmd command here..." autofocus>
|
|
31
|
+
<button id="runBtn" class="btn-primary">Run Command</button>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div class="terminal-container">
|
|
36
|
+
<div class="terminal-header">
|
|
37
|
+
<div class="dots">
|
|
38
|
+
<span class="dot red"></span>
|
|
39
|
+
<span class="dot yellow"></span>
|
|
40
|
+
<span class="dot green"></span>
|
|
41
|
+
</div>
|
|
42
|
+
<div class="title" id="terminalTitle">Terminal</div>
|
|
43
|
+
</div>
|
|
44
|
+
<div id="output" class="terminal-output"></div>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<script src="/socket.io/socket.io.js"></script>
|
|
49
|
+
<script src="app.js"></script>
|
|
50
|
+
</body>
|
|
51
|
+
|
|
52
|
+
</html>
|
package/public/style.css
ADDED
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
:root {
|
|
2
|
+
--bg-color: #0f172a;
|
|
3
|
+
--panel-bg: rgba(30, 41, 59, 0.7);
|
|
4
|
+
--text-primary: #f8fafc;
|
|
5
|
+
--text-secondary: #94a3b8;
|
|
6
|
+
--accent: #3b82f6;
|
|
7
|
+
--accent-hover: #2563eb;
|
|
8
|
+
--terminal-bg: #020617;
|
|
9
|
+
--terminal-text: #10b981;
|
|
10
|
+
--terminal-error: #ef4444;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
* {
|
|
14
|
+
box-sizing: border-box;
|
|
15
|
+
margin: 0;
|
|
16
|
+
padding: 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
body {
|
|
20
|
+
font-family: 'Inter', sans-serif;
|
|
21
|
+
background: linear-gradient(135deg, var(--bg-color), #000000);
|
|
22
|
+
color: var(--text-primary);
|
|
23
|
+
min-height: 100vh;
|
|
24
|
+
display: flex;
|
|
25
|
+
justify-content: center;
|
|
26
|
+
padding: 40px 20px;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
.container {
|
|
30
|
+
width: 100%;
|
|
31
|
+
max-width: 900px;
|
|
32
|
+
display: flex;
|
|
33
|
+
flex-direction: column;
|
|
34
|
+
gap: 24px;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
header {
|
|
38
|
+
text-align: center;
|
|
39
|
+
margin-bottom: 10px;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
h1 {
|
|
43
|
+
font-size: 2.5rem;
|
|
44
|
+
font-weight: 600;
|
|
45
|
+
background: -webkit-linear-gradient(#38bdf8, #818cf8);
|
|
46
|
+
-webkit-background-clip: text;
|
|
47
|
+
-webkit-text-fill-color: transparent;
|
|
48
|
+
margin-bottom: 8px;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
header p {
|
|
52
|
+
color: var(--text-secondary);
|
|
53
|
+
font-size: 1.1rem;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.presets {
|
|
57
|
+
background: var(--panel-bg);
|
|
58
|
+
backdrop-filter: blur(10px);
|
|
59
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
60
|
+
padding: 20px;
|
|
61
|
+
border-radius: 16px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.presets h3 {
|
|
65
|
+
margin-bottom: 16px;
|
|
66
|
+
font-size: 1.1rem;
|
|
67
|
+
font-weight: 500;
|
|
68
|
+
color: #e2e8f0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.preset-buttons {
|
|
72
|
+
display: flex;
|
|
73
|
+
flex-wrap: wrap;
|
|
74
|
+
gap: 12px;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.btn-preset {
|
|
78
|
+
background: rgba(255, 255, 255, 0.05);
|
|
79
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
80
|
+
color: #e2e8f0;
|
|
81
|
+
padding: 8px 16px;
|
|
82
|
+
border-radius: 8px;
|
|
83
|
+
cursor: pointer;
|
|
84
|
+
transition: all 0.2s ease;
|
|
85
|
+
font-family: 'Inter', sans-serif;
|
|
86
|
+
font-size: 0.9rem;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.btn-preset:hover {
|
|
90
|
+
background: rgba(255, 255, 255, 0.15);
|
|
91
|
+
transform: translateY(-2px);
|
|
92
|
+
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.card {
|
|
96
|
+
background: var(--panel-bg);
|
|
97
|
+
backdrop-filter: blur(10px);
|
|
98
|
+
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
99
|
+
padding: 20px;
|
|
100
|
+
border-radius: 16px;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.input-group {
|
|
104
|
+
display: flex;
|
|
105
|
+
gap: 12px;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
input[type="text"] {
|
|
109
|
+
flex: 1;
|
|
110
|
+
background: rgba(0, 0, 0, 0.3);
|
|
111
|
+
border: 1px solid rgba(255, 255, 255, 0.2);
|
|
112
|
+
color: var(--text-primary);
|
|
113
|
+
padding: 14px 20px;
|
|
114
|
+
border-radius: 8px;
|
|
115
|
+
font-size: 1rem;
|
|
116
|
+
font-family: 'Fira Code', monospace;
|
|
117
|
+
outline: none;
|
|
118
|
+
transition: border-color 0.2s;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
input[type="text"]:focus {
|
|
122
|
+
border-color: var(--accent);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.btn-primary {
|
|
126
|
+
background: var(--accent);
|
|
127
|
+
color: white;
|
|
128
|
+
border: none;
|
|
129
|
+
padding: 14px 28px;
|
|
130
|
+
border-radius: 8px;
|
|
131
|
+
font-size: 1rem;
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
cursor: pointer;
|
|
134
|
+
transition: all 0.2s ease;
|
|
135
|
+
box-shadow: 0 4px 14px rgba(59, 130, 246, 0.4);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
.btn-primary:hover {
|
|
139
|
+
background: var(--accent-hover);
|
|
140
|
+
transform: translateY(-2px);
|
|
141
|
+
box-shadow: 0 6px 20px rgba(59, 130, 246, 0.6);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.btn-primary:active {
|
|
145
|
+
transform: translateY(0);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.terminal-container {
|
|
149
|
+
background: var(--terminal-bg);
|
|
150
|
+
border-radius: 12px;
|
|
151
|
+
border: 1px solid rgba(255,255,255,0.1);
|
|
152
|
+
overflow: hidden;
|
|
153
|
+
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.5);
|
|
154
|
+
display: flex;
|
|
155
|
+
flex-direction: column;
|
|
156
|
+
min-height: 400px;
|
|
157
|
+
max-height: 600px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.terminal-header {
|
|
161
|
+
background: #1e293b;
|
|
162
|
+
padding: 10px 16px;
|
|
163
|
+
display: flex;
|
|
164
|
+
align-items: center;
|
|
165
|
+
border-bottom: 1px solid rgba(255,255,255,0.1);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
.dots {
|
|
169
|
+
display: flex;
|
|
170
|
+
gap: 8px;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.dot {
|
|
174
|
+
width: 12px;
|
|
175
|
+
height: 12px;
|
|
176
|
+
border-radius: 50%;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.red { background: #ff5f56; }
|
|
180
|
+
.yellow { background: #ffbd2e; }
|
|
181
|
+
.green { background: #27c93f; }
|
|
182
|
+
|
|
183
|
+
.terminal-header .title {
|
|
184
|
+
flex: 1;
|
|
185
|
+
text-align: center;
|
|
186
|
+
color: var(--text-secondary);
|
|
187
|
+
font-size: 0.9rem;
|
|
188
|
+
font-family: 'Inter', sans-serif;
|
|
189
|
+
margin-right: 48px; /* Offset the dots for perfect centering */
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.terminal-output {
|
|
193
|
+
padding: 16px;
|
|
194
|
+
font-family: 'Fira Code', monospace;
|
|
195
|
+
font-size: 0.95rem;
|
|
196
|
+
line-height: 1.5;
|
|
197
|
+
color: var(--terminal-text);
|
|
198
|
+
overflow-y: auto;
|
|
199
|
+
flex: 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.command-block {
|
|
203
|
+
margin-bottom: 24px;
|
|
204
|
+
border-bottom: 1px solid rgba(255,255,255,0.05);
|
|
205
|
+
padding-bottom: 16px;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.command-block:last-child {
|
|
209
|
+
border-bottom: none;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
.cmd-header {
|
|
213
|
+
font-weight: bold;
|
|
214
|
+
margin-bottom: 8px;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
.command-output-inner {
|
|
218
|
+
white-space: pre-wrap;
|
|
219
|
+
word-wrap: break-word;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.terminal-output .error {
|
|
223
|
+
color: var(--terminal-error);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
.terminal-output .system {
|
|
227
|
+
color: #38bdf8;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/* Custom Scrollbar for Terminal */
|
|
231
|
+
.terminal-output::-webkit-scrollbar {
|
|
232
|
+
width: 8px;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.terminal-output::-webkit-scrollbar-track {
|
|
236
|
+
background: rgba(255, 255, 255, 0.05);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
.terminal-output::-webkit-scrollbar-thumb {
|
|
240
|
+
background: rgba(255, 255, 255, 0.2);
|
|
241
|
+
border-radius: 4px;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
.terminal-output::-webkit-scrollbar-thumb:hover {
|
|
245
|
+
background: rgba(255, 255, 255, 0.3);
|
|
246
|
+
}
|
package/server.js
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
const { createServer } = require('http');
|
|
3
|
+
const { Server } = require('socket.io');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const path = require('path');
|
|
6
|
+
const os = require('os');
|
|
7
|
+
|
|
8
|
+
const app = express();
|
|
9
|
+
const httpServer = createServer(app);
|
|
10
|
+
const io = new Server(httpServer);
|
|
11
|
+
|
|
12
|
+
const PORT = process.env.PORT || 3000;
|
|
13
|
+
|
|
14
|
+
app.use(express.static(path.join(__dirname, 'public')));
|
|
15
|
+
|
|
16
|
+
io.on('connection', (socket) => {
|
|
17
|
+
console.log('Client connected');
|
|
18
|
+
|
|
19
|
+
socket.on('run_command', (payload) => {
|
|
20
|
+
// If the old app.js sends a string, handle it. Otherwise handle the new object format.
|
|
21
|
+
const cmd = typeof payload === 'string' ? payload : payload.cmd;
|
|
22
|
+
const runId = typeof payload === 'string' ? cmd : payload.runId;
|
|
23
|
+
console.log(`Running command: ${cmd}`);
|
|
24
|
+
|
|
25
|
+
// execute the command inside the native OS shell
|
|
26
|
+
const child = spawn(cmd, [], { shell: true });
|
|
27
|
+
|
|
28
|
+
socket.emit('cmd_start', { id: cmd, runId: runId });
|
|
29
|
+
|
|
30
|
+
child.stdout.on('data', (data) => {
|
|
31
|
+
socket.emit('cmd_output', { type: 'stdout', data: data.toString(), id: cmd, runId: runId });
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
child.stderr.on('data', (data) => {
|
|
35
|
+
socket.emit('cmd_output', { type: 'stderr', data: data.toString(), id: cmd, runId: runId });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
child.on('close', (code) => {
|
|
39
|
+
socket.emit('cmd_close', { code, id: cmd, runId: runId });
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
child.on('error', (err) => {
|
|
43
|
+
socket.emit('cmd_error', { error: err.toString(), id: cmd, runId: runId });
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
httpServer.listen(PORT, () => {
|
|
49
|
+
const url = `http://localhost:${PORT}`;
|
|
50
|
+
console.log(`Server is running at ${url}`);
|
|
51
|
+
|
|
52
|
+
// Use platform-specific command to open the browser
|
|
53
|
+
const startCmd = os.platform() === 'win32' ? 'start' : (os.platform() === 'darwin' ? 'open' : 'xdg-open');
|
|
54
|
+
spawn(startCmd, [url], { shell: true });
|
|
55
|
+
});
|