@mjasano/devtunnel 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.
@@ -0,0 +1,18 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(cat:*)",
5
+ "Bash(brew install:*)",
6
+ "Bash(npm start)",
7
+ "Bash(curl:*)",
8
+ "Bash(cloudflared tunnel:*)",
9
+ "Bash(pkill:*)",
10
+ "Bash(chmod:*)",
11
+ "Bash(node bin/cli.js:*)",
12
+ "Bash(npm whoami:*)",
13
+ "Bash(npm config set:*)",
14
+ "Bash(npm view:*)",
15
+ "Bash(npm publish:*)"
16
+ ]
17
+ }
18
+ }
package/Dockerfile ADDED
@@ -0,0 +1,41 @@
1
+ FROM node:20-slim
2
+
3
+ # Install dependencies
4
+ RUN apt-get update && apt-get install -y \
5
+ curl \
6
+ git \
7
+ build-essential \
8
+ python3 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Install cloudflared
12
+ RUN curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared \
13
+ && chmod +x /usr/local/bin/cloudflared
14
+
15
+ # Install Claude CLI
16
+ RUN npm install -g @anthropic-ai/claude-code
17
+
18
+ # Create app directory
19
+ WORKDIR /app
20
+
21
+ # Copy package files
22
+ COPY package*.json ./
23
+
24
+ # Install app dependencies
25
+ RUN npm install
26
+
27
+ # Copy app source
28
+ COPY . .
29
+
30
+ # Create workspace directory
31
+ RUN mkdir -p /workspace
32
+
33
+ # Environment variables
34
+ ENV PORT=3000
35
+ ENV WORKSPACE=/workspace
36
+
37
+ # Expose port
38
+ EXPOSE 3000
39
+
40
+ # Start the server
41
+ CMD ["node", "server.js"]
package/bin/cli.js ADDED
@@ -0,0 +1,250 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { spawn, execSync } = require('child_process');
4
+ const path = require('path');
5
+ const http = require('http');
6
+
7
+ const VERSION = '1.0.0';
8
+
9
+ // Colors
10
+ const c = {
11
+ reset: '\x1b[0m',
12
+ bold: '\x1b[1m',
13
+ dim: '\x1b[2m',
14
+ green: '\x1b[32m',
15
+ blue: '\x1b[34m',
16
+ cyan: '\x1b[36m',
17
+ yellow: '\x1b[33m',
18
+ red: '\x1b[31m',
19
+ magenta: '\x1b[35m',
20
+ };
21
+
22
+ function log(msg) {
23
+ console.log(msg);
24
+ }
25
+
26
+ function banner() {
27
+ log('');
28
+ log(`${c.cyan}${c.bold} ____ _____ _ ${c.reset}`);
29
+ log(`${c.cyan}${c.bold} | _ \\ _____ _|_ _| _ _ __ _ __ ___| | ${c.reset}`);
30
+ log(`${c.cyan}${c.bold} | | | |/ _ \\ \\/ / | || | | | '_ \\| '_ \\ / _ \\ | ${c.reset}`);
31
+ log(`${c.cyan}${c.bold} | |_| | __/> < | || |_| | | | | | | | __/ | ${c.reset}`);
32
+ log(`${c.cyan}${c.bold} |____/ \\___/_/\\_\\ |_| \\__,_|_| |_|_| |_|\\___|_| ${c.reset}`);
33
+ log('');
34
+ log(`${c.dim} Web Terminal + Tunnel Manager v${VERSION}${c.reset}`);
35
+ log('');
36
+ }
37
+
38
+ function checkCloudflared() {
39
+ try {
40
+ execSync('which cloudflared', { stdio: 'ignore' });
41
+ return true;
42
+ } catch {
43
+ return false;
44
+ }
45
+ }
46
+
47
+ function installCloudflared() {
48
+ log(`${c.yellow}cloudflared not found. Installing...${c.reset}`);
49
+
50
+ const platform = process.platform;
51
+
52
+ try {
53
+ if (platform === 'darwin') {
54
+ log(`${c.dim}Running: brew install cloudflared${c.reset}`);
55
+ execSync('brew install cloudflared', { stdio: 'inherit' });
56
+ } else if (platform === 'linux') {
57
+ log(`${c.dim}Downloading cloudflared...${c.reset}`);
58
+ execSync('curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64 -o /usr/local/bin/cloudflared && chmod +x /usr/local/bin/cloudflared', { stdio: 'inherit' });
59
+ } else {
60
+ log(`${c.red}Please install cloudflared manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/${c.reset}`);
61
+ process.exit(1);
62
+ }
63
+ log(`${c.green}cloudflared installed successfully!${c.reset}`);
64
+ } catch (err) {
65
+ log(`${c.red}Failed to install cloudflared: ${err.message}${c.reset}`);
66
+ log(`${c.yellow}Please install manually: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/${c.reset}`);
67
+ process.exit(1);
68
+ }
69
+ }
70
+
71
+ function waitForServer(port, timeout = 10000) {
72
+ return new Promise((resolve, reject) => {
73
+ const start = Date.now();
74
+
75
+ const check = () => {
76
+ const req = http.get(`http://localhost:${port}/health`, (res) => {
77
+ if (res.statusCode === 200) {
78
+ resolve();
79
+ } else {
80
+ retry();
81
+ }
82
+ });
83
+
84
+ req.on('error', retry);
85
+ req.setTimeout(1000);
86
+ };
87
+
88
+ const retry = () => {
89
+ if (Date.now() - start > timeout) {
90
+ reject(new Error('Server startup timeout'));
91
+ } else {
92
+ setTimeout(check, 500);
93
+ }
94
+ };
95
+
96
+ check();
97
+ });
98
+ }
99
+
100
+ function startServer(port) {
101
+ const serverPath = path.join(__dirname, '..', 'server.js');
102
+
103
+ const server = spawn('node', [serverPath], {
104
+ env: { ...process.env, PORT: port },
105
+ stdio: ['ignore', 'pipe', 'pipe'],
106
+ detached: false
107
+ });
108
+
109
+ server.stdout.on('data', (data) => {
110
+ const msg = data.toString().trim();
111
+ if (msg) log(`${c.dim}[server] ${msg}${c.reset}`);
112
+ });
113
+
114
+ server.stderr.on('data', (data) => {
115
+ const msg = data.toString().trim();
116
+ if (msg) log(`${c.red}[server] ${msg}${c.reset}`);
117
+ });
118
+
119
+ return server;
120
+ }
121
+
122
+ function startTunnel(port) {
123
+ return new Promise((resolve, reject) => {
124
+ const tunnel = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], {
125
+ stdio: ['ignore', 'pipe', 'pipe']
126
+ });
127
+
128
+ let resolved = false;
129
+
130
+ const handleOutput = (data) => {
131
+ const output = data.toString();
132
+ const match = output.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
133
+ if (match && !resolved) {
134
+ resolved = true;
135
+ resolve({ tunnel, url: match[0] });
136
+ }
137
+ };
138
+
139
+ tunnel.stdout.on('data', handleOutput);
140
+ tunnel.stderr.on('data', handleOutput);
141
+
142
+ tunnel.on('error', (err) => {
143
+ if (!resolved) reject(err);
144
+ });
145
+
146
+ setTimeout(() => {
147
+ if (!resolved) {
148
+ reject(new Error('Tunnel creation timeout'));
149
+ }
150
+ }, 30000);
151
+ });
152
+ }
153
+
154
+ function showSuccess(url) {
155
+ log('');
156
+ log(`${c.green}${c.bold} Ready!${c.reset}`);
157
+ log('');
158
+ log(`${c.bold} Access from anywhere:${c.reset}`);
159
+ log(` ${c.cyan}${c.bold}${url}${c.reset}`);
160
+ log('');
161
+ log(`${c.dim} Features:${c.reset}`);
162
+ log(`${c.dim} - Web terminal with session persistence${c.reset}`);
163
+ log(`${c.dim} - Expose local ports to the internet${c.reset}`);
164
+ log(`${c.dim} - Run Claude CLI and develop anywhere${c.reset}`);
165
+ log('');
166
+ log(`${c.yellow} Press Ctrl+C to stop${c.reset}`);
167
+ log('');
168
+ }
169
+
170
+ async function main() {
171
+ const args = process.argv.slice(2);
172
+ const command = args[0] || 'start';
173
+
174
+ if (command === '--help' || command === '-h') {
175
+ banner();
176
+ log(`${c.bold}Usage:${c.reset}`);
177
+ log(` devtunnel Start DevTunnel`);
178
+ log(` devtunnel start Start DevTunnel`);
179
+ log(` devtunnel --port 8080 Use custom port`);
180
+ log(` devtunnel --help Show this help`);
181
+ log(` devtunnel --version Show version`);
182
+ log('');
183
+ return;
184
+ }
185
+
186
+ if (command === '--version' || command === '-v') {
187
+ log(`devtunnel v${VERSION}`);
188
+ return;
189
+ }
190
+
191
+ // Parse port
192
+ let port = 3000;
193
+ const portIndex = args.indexOf('--port');
194
+ if (portIndex !== -1 && args[portIndex + 1]) {
195
+ port = parseInt(args[portIndex + 1]);
196
+ }
197
+
198
+ banner();
199
+
200
+ // Check cloudflared
201
+ log(`${c.dim}Checking dependencies...${c.reset}`);
202
+ if (!checkCloudflared()) {
203
+ installCloudflared();
204
+ }
205
+ log(`${c.green}✓${c.reset} cloudflared`);
206
+
207
+ // Start server
208
+ log(`${c.dim}Starting server on port ${port}...${c.reset}`);
209
+ const server = startServer(port);
210
+
211
+ try {
212
+ await waitForServer(port);
213
+ log(`${c.green}✓${c.reset} Server running`);
214
+ } catch (err) {
215
+ log(`${c.red}✗ Failed to start server: ${err.message}${c.reset}`);
216
+ server.kill();
217
+ process.exit(1);
218
+ }
219
+
220
+ // Start tunnel
221
+ log(`${c.dim}Creating tunnel...${c.reset}`);
222
+ try {
223
+ const { tunnel, url } = await startTunnel(port);
224
+ log(`${c.green}✓${c.reset} Tunnel created`);
225
+
226
+ showSuccess(url);
227
+
228
+ // Handle shutdown
229
+ const cleanup = () => {
230
+ log('');
231
+ log(`${c.dim}Shutting down...${c.reset}`);
232
+ tunnel.kill();
233
+ server.kill();
234
+ process.exit(0);
235
+ };
236
+
237
+ process.on('SIGINT', cleanup);
238
+ process.on('SIGTERM', cleanup);
239
+
240
+ } catch (err) {
241
+ log(`${c.red}✗ Failed to create tunnel: ${err.message}${c.reset}`);
242
+ server.kill();
243
+ process.exit(1);
244
+ }
245
+ }
246
+
247
+ main().catch((err) => {
248
+ log(`${c.red}Error: ${err.message}${c.reset}`);
249
+ process.exit(1);
250
+ });
@@ -0,0 +1,14 @@
1
+ services:
2
+ devtunnel:
3
+ build: .
4
+ ports:
5
+ - "3000:3000"
6
+ volumes:
7
+ - ./workspace:/workspace
8
+ - claude-config:/root/.claude
9
+ environment:
10
+ - ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
11
+ restart: unless-stopped
12
+
13
+ volumes:
14
+ claude-config:
package/package.json ADDED
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "@mjasano/devtunnel",
3
+ "version": "1.0.0",
4
+ "description": "Web terminal with tunnel manager - access your dev environment from anywhere",
5
+ "main": "server.js",
6
+ "bin": {
7
+ "devtunnel": "./bin/cli.js"
8
+ },
9
+ "scripts": {
10
+ "start": "node server.js",
11
+ "dev": "node server.js"
12
+ },
13
+ "keywords": [
14
+ "terminal",
15
+ "web-terminal",
16
+ "tunnel",
17
+ "cloudflare",
18
+ "remote-development"
19
+ ],
20
+ "author": "",
21
+ "license": "MIT",
22
+ "dependencies": {
23
+ "@homebridge/node-pty-prebuilt-multiarch": "^0.13.1",
24
+ "express": "^4.18.2",
25
+ "ws": "^8.14.2"
26
+ }
27
+ }