vibex-sh 0.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/LICENSE +21 -0
- package/README.md +103 -0
- package/bin/vibex.js +19 -0
- package/index.js +501 -0
- package/package.json +32 -0
- package/publish.sh +106 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 vibex.sh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# vibex CLI
|
|
2
|
+
|
|
3
|
+
Zero-config observability CLI - pipe logs and visualize instantly.
|
|
4
|
+
|
|
5
|
+
## Quick Start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
# Production (default)
|
|
9
|
+
echo '{"cpu": 45, "memory": 78}' | vibex
|
|
10
|
+
|
|
11
|
+
# Local development
|
|
12
|
+
echo '{"test": 123}' | vibex --local
|
|
13
|
+
|
|
14
|
+
# Custom ports
|
|
15
|
+
echo '{"data": 123}' | vibex --web http://localhost:3000 --socket http://localhost:8080
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install -g vibex-sh
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Usage
|
|
25
|
+
|
|
26
|
+
Pipe any output to `vibex`:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# JSON logs
|
|
30
|
+
echo '{"cpu": 45, "memory": 78}' | vibex
|
|
31
|
+
|
|
32
|
+
# Script output
|
|
33
|
+
python script.py | vibex
|
|
34
|
+
node server.js | vibex
|
|
35
|
+
docker logs -f | vibex
|
|
36
|
+
|
|
37
|
+
# Reuse session
|
|
38
|
+
echo '{"more": "data"}' | vibex --session-id vibex-abc123
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
## Options
|
|
42
|
+
|
|
43
|
+
| Flag | Description | Example |
|
|
44
|
+
|------|-------------|---------|
|
|
45
|
+
| `-s, --session-id <id>` | Reuse existing session | `vibex --session-id vibex-abc123` |
|
|
46
|
+
| `-l, --local` | Use localhost (web: 3000, socket: 3001) | `vibex --local` |
|
|
47
|
+
| `--web <url>` | Web server URL | `vibex --web http://localhost:3000` |
|
|
48
|
+
| `--socket <url>` | Socket server URL | `vibex --socket http://localhost:8080` |
|
|
49
|
+
| `--server <url>` | Shorthand for `--web` (auto-derives socket) | `vibex --server http://localhost:3000` |
|
|
50
|
+
|
|
51
|
+
## Server Configuration
|
|
52
|
+
|
|
53
|
+
The CLI automatically derives the socket URL from the web URL, but you can override it:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
# Auto-derive socket (localhost:3000 → localhost:3001)
|
|
57
|
+
vibex --web http://localhost:3000
|
|
58
|
+
|
|
59
|
+
# Explicit socket URL
|
|
60
|
+
vibex --web http://localhost:3000 --socket http://localhost:8080
|
|
61
|
+
|
|
62
|
+
# Production (auto-derives socket.vibex.sh)
|
|
63
|
+
vibex --server https://vibex.sh
|
|
64
|
+
|
|
65
|
+
# Custom domain
|
|
66
|
+
vibex --web https://staging.vibex.sh --socket https://socket-staging.vibex.sh
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Priority Order
|
|
70
|
+
|
|
71
|
+
1. **Flags** (`--web`, `--socket`, `--local`, `--server`)
|
|
72
|
+
2. **Environment variables** (`VIBEX_WEB_URL`, `VIBEX_SOCKET_URL`)
|
|
73
|
+
3. **Production defaults** (`https://vibex.sh`, `https://socket.vibex.sh`)
|
|
74
|
+
|
|
75
|
+
## Environment Variables
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
export VIBEX_WEB_URL=http://localhost:3000
|
|
79
|
+
export VIBEX_SOCKET_URL=http://localhost:8080
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Examples
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# Production (default)
|
|
86
|
+
echo '{"data": 123}' | vibex
|
|
87
|
+
|
|
88
|
+
# Quick localhost
|
|
89
|
+
echo '{"data": 123}' | vibex --local
|
|
90
|
+
|
|
91
|
+
# Custom web server, auto socket
|
|
92
|
+
echo '{"data": 123}' | vibex --server http://localhost:3000
|
|
93
|
+
|
|
94
|
+
# Both custom
|
|
95
|
+
echo '{"data": 123}' | vibex --web http://localhost:3000 --socket http://localhost:8080
|
|
96
|
+
|
|
97
|
+
# Staging
|
|
98
|
+
echo '{"data": 123}' | vibex --server https://staging.vibex.sh
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
## License
|
|
102
|
+
|
|
103
|
+
MIT
|
package/bin/vibex.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { dirname, join } from 'path';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = dirname(__filename);
|
|
8
|
+
const cliPath = join(__dirname, '..', 'index.js');
|
|
9
|
+
|
|
10
|
+
// Directly import and run the main function
|
|
11
|
+
import(cliPath).then((module) => {
|
|
12
|
+
// The main function is already called in index.js
|
|
13
|
+
}).catch((error) => {
|
|
14
|
+
console.error('Error loading CLI:', error);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,501 @@
|
|
|
1
|
+
import readline from 'readline';
|
|
2
|
+
import { io } from 'socket.io-client';
|
|
3
|
+
import { program, Command } from 'commander';
|
|
4
|
+
import { readFile, writeFile, mkdir } from 'fs/promises';
|
|
5
|
+
import { existsSync, readFileSync } from 'fs';
|
|
6
|
+
import { join } from 'path';
|
|
7
|
+
import { homedir } from 'os';
|
|
8
|
+
import { spawn } from 'child_process';
|
|
9
|
+
import http from 'http';
|
|
10
|
+
import https from 'https';
|
|
11
|
+
|
|
12
|
+
function generateSessionId() {
|
|
13
|
+
const chars = 'abcdefghijklmnopqrstuvwxyz0123456789';
|
|
14
|
+
let result = 'vibex-';
|
|
15
|
+
for (let i = 0; i < 6; i++) {
|
|
16
|
+
result += chars[Math.floor(Math.random() * chars.length)];
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function normalizeSessionId(sessionId) {
|
|
22
|
+
if (!sessionId) return null;
|
|
23
|
+
// If it doesn't start with 'vibex-', add it
|
|
24
|
+
if (!sessionId.startsWith('vibex-')) {
|
|
25
|
+
return `vibex-${sessionId}`;
|
|
26
|
+
}
|
|
27
|
+
return sessionId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function deriveSocketUrl(webUrl) {
|
|
31
|
+
const url = new URL(webUrl);
|
|
32
|
+
|
|
33
|
+
// For localhost, socket is typically on port 3001
|
|
34
|
+
if (url.hostname === 'localhost' || url.hostname === '127.0.0.1') {
|
|
35
|
+
const port = url.port === '3000' || !url.port ? '3001' : String(parseInt(url.port) + 1);
|
|
36
|
+
return `${url.protocol}//${url.hostname}:${port}`;
|
|
37
|
+
}
|
|
38
|
+
// For vibex.sh domains, use socket subdomain
|
|
39
|
+
else if (url.hostname.includes('vibex.sh')) {
|
|
40
|
+
return webUrl.replace(url.hostname, `socket.${url.hostname}`);
|
|
41
|
+
}
|
|
42
|
+
// For other domains, try to use socket subdomain
|
|
43
|
+
else {
|
|
44
|
+
return webUrl.replace(url.hostname, `socket.${url.hostname}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getUrls(options) {
|
|
49
|
+
const { local, web, socket, server } = options;
|
|
50
|
+
|
|
51
|
+
// Priority 1: Explicit --web and --socket flags (highest priority)
|
|
52
|
+
if (web) {
|
|
53
|
+
return {
|
|
54
|
+
webUrl: web,
|
|
55
|
+
socketUrl: socket || deriveSocketUrl(web),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Priority 2: --server flag (shorthand for --web)
|
|
60
|
+
if (server) {
|
|
61
|
+
return {
|
|
62
|
+
webUrl: server,
|
|
63
|
+
socketUrl: socket || deriveSocketUrl(server),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Priority 3: --local flag
|
|
68
|
+
if (local) {
|
|
69
|
+
return {
|
|
70
|
+
webUrl: process.env.VIBEX_WEB_URL || 'http://localhost:3000',
|
|
71
|
+
socketUrl: process.env.VIBEX_SOCKET_URL || socket || 'http://localhost:3001',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Priority 4: Environment variables
|
|
76
|
+
if (process.env.VIBEX_WEB_URL) {
|
|
77
|
+
return {
|
|
78
|
+
webUrl: process.env.VIBEX_WEB_URL,
|
|
79
|
+
socketUrl: process.env.VIBEX_SOCKET_URL || socket || deriveSocketUrl(process.env.VIBEX_WEB_URL),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Priority 5: Production defaults
|
|
84
|
+
return {
|
|
85
|
+
webUrl: 'https://vibex.sh',
|
|
86
|
+
socketUrl: socket || 'https://socket.vibex.sh',
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getConfigPath() {
|
|
91
|
+
// Check for custom config path from environment variable
|
|
92
|
+
if (process.env.VIBEX_CONFIG_PATH) {
|
|
93
|
+
return process.env.VIBEX_CONFIG_PATH;
|
|
94
|
+
}
|
|
95
|
+
// Default: ~/.vibex/config.json
|
|
96
|
+
const configDir = join(homedir(), '.vibex');
|
|
97
|
+
return join(configDir, 'config.json');
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function getStoredToken() {
|
|
101
|
+
try {
|
|
102
|
+
const configPath = getConfigPath();
|
|
103
|
+
if (existsSync(configPath)) {
|
|
104
|
+
const config = JSON.parse(await readFile(configPath, 'utf-8'));
|
|
105
|
+
return config.token || null;
|
|
106
|
+
}
|
|
107
|
+
} catch (error) {
|
|
108
|
+
// Ignore errors (file doesn't exist or invalid JSON)
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function getStoredConfig() {
|
|
114
|
+
try {
|
|
115
|
+
const configPath = getConfigPath();
|
|
116
|
+
if (existsSync(configPath)) {
|
|
117
|
+
return JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
// Ignore errors
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
async function storeToken(token, webUrl = null) {
|
|
126
|
+
try {
|
|
127
|
+
const configPath = getConfigPath();
|
|
128
|
+
const configDir = join(homedir(), '.vibex');
|
|
129
|
+
if (!existsSync(configDir)) {
|
|
130
|
+
await mkdir(configDir, { recursive: true });
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const config = {
|
|
134
|
+
token,
|
|
135
|
+
...(webUrl && { webUrl }), // Store webUrl if provided
|
|
136
|
+
updatedAt: new Date().toISOString(),
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
140
|
+
return true;
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('Failed to store token:', error.message);
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function handleLogin(webUrl) {
|
|
148
|
+
const configPath = getConfigPath();
|
|
149
|
+
const existingConfig = getStoredConfig();
|
|
150
|
+
|
|
151
|
+
console.log('\n 🔐 Vibex CLI Authentication\n');
|
|
152
|
+
console.log(` 📁 Config location: ${configPath}`);
|
|
153
|
+
|
|
154
|
+
if (existingConfig?.token) {
|
|
155
|
+
console.log(` ⚠️ You already have a token stored. This will replace it.\n`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const tempToken = `temp_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
159
|
+
const authUrl = `${webUrl}/api/cli-auth?token=${tempToken}`;
|
|
160
|
+
|
|
161
|
+
console.log(' Opening browser for authentication...\n');
|
|
162
|
+
console.log(` If browser doesn't open, visit: ${authUrl}\n`);
|
|
163
|
+
|
|
164
|
+
// Open browser
|
|
165
|
+
const platform = process.platform;
|
|
166
|
+
let command;
|
|
167
|
+
if (platform === 'darwin') {
|
|
168
|
+
command = 'open';
|
|
169
|
+
} else if (platform === 'win32') {
|
|
170
|
+
command = 'start';
|
|
171
|
+
} else {
|
|
172
|
+
command = 'xdg-open';
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
spawn(command, [authUrl], { detached: true, stdio: 'ignore' });
|
|
176
|
+
|
|
177
|
+
// Poll for token
|
|
178
|
+
console.log(' Waiting for authentication...');
|
|
179
|
+
const maxAttempts = 60; // 60 seconds
|
|
180
|
+
let attempts = 0;
|
|
181
|
+
|
|
182
|
+
while (attempts < maxAttempts) {
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
184
|
+
attempts++;
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
const response = await httpRequest(`${webUrl}/api/cli-auth?token=${tempToken}`, {
|
|
188
|
+
method: 'GET',
|
|
189
|
+
});
|
|
190
|
+
if (response.ok) {
|
|
191
|
+
const data = await response.json();
|
|
192
|
+
if (data.success && data.token) {
|
|
193
|
+
await storeToken(data.token, webUrl);
|
|
194
|
+
const configPath = getConfigPath();
|
|
195
|
+
console.log('\n ✅ Authentication successful!');
|
|
196
|
+
console.log(` 📁 Token saved to: ${configPath}`);
|
|
197
|
+
console.log(` 💡 This token will be used automatically for future commands.\n`);
|
|
198
|
+
return data.token;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
} catch (error) {
|
|
202
|
+
// Continue polling
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
console.log('\n ⏱️ Authentication timeout. Please try again.\n');
|
|
207
|
+
process.exit(1);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function httpRequest(url, options) {
|
|
211
|
+
return new Promise((resolve, reject) => {
|
|
212
|
+
const urlObj = new URL(url);
|
|
213
|
+
const isHttps = urlObj.protocol === 'https:';
|
|
214
|
+
const httpModule = isHttps ? https : http;
|
|
215
|
+
|
|
216
|
+
const req = httpModule.request(url, options, (res) => {
|
|
217
|
+
let data = '';
|
|
218
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
219
|
+
res.on('end', () => {
|
|
220
|
+
try {
|
|
221
|
+
const parsed = JSON.parse(data);
|
|
222
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, json: () => Promise.resolve(parsed) });
|
|
223
|
+
} catch (e) {
|
|
224
|
+
resolve({ ok: res.statusCode >= 200 && res.statusCode < 300, status: res.statusCode, json: () => Promise.resolve({}) });
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
req.on('error', reject);
|
|
230
|
+
if (options.body) {
|
|
231
|
+
req.write(options.body);
|
|
232
|
+
}
|
|
233
|
+
req.end();
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
async function claimSession(sessionId, token, webUrl) {
|
|
238
|
+
if (!token) return false;
|
|
239
|
+
|
|
240
|
+
try {
|
|
241
|
+
// Normalize session ID before claiming
|
|
242
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
243
|
+
const response = await httpRequest(`${webUrl}/api/auth/claim-session-with-token`, {
|
|
244
|
+
method: 'POST',
|
|
245
|
+
headers: { 'Content-Type': 'application/json' },
|
|
246
|
+
body: JSON.stringify({
|
|
247
|
+
sessionId: normalizedSessionId,
|
|
248
|
+
token,
|
|
249
|
+
}),
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
return response.ok;
|
|
253
|
+
} catch (error) {
|
|
254
|
+
return false;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function printBanner(sessionId, webUrl) {
|
|
259
|
+
const dashboardUrl = `${webUrl}/${sessionId}`;
|
|
260
|
+
|
|
261
|
+
console.log('\n');
|
|
262
|
+
console.log(' ╔═══════════════════════════════════════╗');
|
|
263
|
+
console.log(' ║ 🔍 Vibex is watching... ║');
|
|
264
|
+
console.log(' ╚═══════════════════════════════════════╝');
|
|
265
|
+
console.log('\n');
|
|
266
|
+
console.log(` Session ID: ${sessionId}`);
|
|
267
|
+
console.log(` Dashboard: ${dashboardUrl}`);
|
|
268
|
+
console.log('\n');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async function main() {
|
|
272
|
+
// Handle login command separately - check BEFORE commander parses
|
|
273
|
+
// Check process.argv directly - look for 'login' as a standalone argument
|
|
274
|
+
// This must happen FIRST, before any commander parsing
|
|
275
|
+
const allArgs = process.argv;
|
|
276
|
+
const args = process.argv.slice(2);
|
|
277
|
+
|
|
278
|
+
// Check if 'login' appears anywhere in process.argv (works with npx too)
|
|
279
|
+
const hasLogin = allArgs.includes('login') || args.includes('login');
|
|
280
|
+
|
|
281
|
+
if (hasLogin) {
|
|
282
|
+
// Find login position to get args after it
|
|
283
|
+
const loginIndex = args.indexOf('login');
|
|
284
|
+
const loginArgs = loginIndex !== -1 ? args.slice(loginIndex + 1) : [];
|
|
285
|
+
|
|
286
|
+
// Create a separate command instance for login
|
|
287
|
+
const loginCmd = new Command();
|
|
288
|
+
loginCmd
|
|
289
|
+
.option('-l, --local', 'Use localhost')
|
|
290
|
+
.option('--web <url>', 'Web server URL')
|
|
291
|
+
.option('--server <url>', 'Shorthand for --web');
|
|
292
|
+
|
|
293
|
+
// Parse only the options (args after 'login')
|
|
294
|
+
if (loginArgs.length > 0) {
|
|
295
|
+
loginCmd.parse(['node', 'vibex', ...loginArgs], { from: 'user' });
|
|
296
|
+
} else {
|
|
297
|
+
loginCmd.parse(['node', 'vibex'], { from: 'user' });
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const options = loginCmd.opts();
|
|
301
|
+
const { webUrl } = getUrls(options);
|
|
302
|
+
await handleLogin(webUrl);
|
|
303
|
+
process.exit(0);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
program
|
|
307
|
+
.option('-s, --session-id <id>', 'Reuse existing session ID')
|
|
308
|
+
.option('-l, --local', 'Use localhost (web: 3000, socket: 3001)')
|
|
309
|
+
.option('--web <url>', 'Web server URL (e.g., http://localhost:3000)')
|
|
310
|
+
.option('--socket <url>', 'Socket server URL (e.g., http://localhost:3001)')
|
|
311
|
+
.option('--server <url>', 'Shorthand for --web (auto-derives socket URL)')
|
|
312
|
+
.option('--token <token>', 'Authentication token (or use VIBEX_TOKEN env var)')
|
|
313
|
+
.parse();
|
|
314
|
+
|
|
315
|
+
const options = program.opts();
|
|
316
|
+
|
|
317
|
+
// Normalize session ID - add 'vibex-' prefix if missing
|
|
318
|
+
const rawSessionId = options.sessionId || generateSessionId();
|
|
319
|
+
const sessionId = normalizeSessionId(rawSessionId);
|
|
320
|
+
const { webUrl, socketUrl } = getUrls(options);
|
|
321
|
+
|
|
322
|
+
// Get token from flag, env var, or stored config
|
|
323
|
+
let token = options.token || process.env.VIBEX_TOKEN || await getStoredToken();
|
|
324
|
+
|
|
325
|
+
// Auto-claim session if token is available
|
|
326
|
+
if (token && !options.sessionId) {
|
|
327
|
+
// Only auto-claim new sessions (not when reusing existing session)
|
|
328
|
+
const claimed = await claimSession(sessionId, token, webUrl);
|
|
329
|
+
if (claimed) {
|
|
330
|
+
console.log(' ✓ Session automatically claimed to your account\n');
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Print banner only once, and show how to reuse session
|
|
335
|
+
if (!options.sessionId) {
|
|
336
|
+
printBanner(sessionId, webUrl);
|
|
337
|
+
const localFlag = webUrl.includes('localhost') ? ' --local' : '';
|
|
338
|
+
const sessionSlug = sessionId.replace(/^vibex-/, ''); // Remove prefix for example
|
|
339
|
+
console.log(' 💡 Tip: Use -s to send more logs to this session');
|
|
340
|
+
console.log(` Example: echo '{"cpu": 45, "memory": 78, "timestamp": "${new Date().toISOString()}"}' | npx vibex-sh -s ${sessionSlug}${localFlag}\n`);
|
|
341
|
+
} else {
|
|
342
|
+
// When reusing a session, show minimal info
|
|
343
|
+
console.log(` 🔍 Sending logs to session: ${sessionId}`);
|
|
344
|
+
console.log(` Dashboard: ${webUrl}/${sessionId}\n`);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const socket = io(socketUrl, {
|
|
348
|
+
transports: ['websocket', 'polling'],
|
|
349
|
+
autoConnect: true,
|
|
350
|
+
// Reconnection settings for Cloud Run
|
|
351
|
+
reconnection: true,
|
|
352
|
+
reconnectionDelay: 1000,
|
|
353
|
+
reconnectionDelayMax: 5000,
|
|
354
|
+
reconnectionAttempts: Infinity, // Keep trying forever
|
|
355
|
+
timeout: 20000,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
let isConnected = false;
|
|
359
|
+
let hasJoinedSession = false;
|
|
360
|
+
const logQueue = [];
|
|
361
|
+
|
|
362
|
+
socket.on('connect', () => {
|
|
363
|
+
isConnected = true;
|
|
364
|
+
console.log(' ✓ Connected to server\n');
|
|
365
|
+
// Rejoin session on reconnect
|
|
366
|
+
socket.emit('join-session', sessionId);
|
|
367
|
+
// Wait a tiny bit for join-session to be processed
|
|
368
|
+
setTimeout(() => {
|
|
369
|
+
hasJoinedSession = true;
|
|
370
|
+
// Process any queued logs
|
|
371
|
+
while (logQueue.length > 0) {
|
|
372
|
+
const logData = logQueue.shift();
|
|
373
|
+
socket.emit('cli-emit', {
|
|
374
|
+
sessionId,
|
|
375
|
+
...logData,
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
}, 100);
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
socket.on('reconnect', (attemptNumber) => {
|
|
382
|
+
console.log(` ↻ Reconnected (attempt ${attemptNumber})\n`);
|
|
383
|
+
isConnected = true;
|
|
384
|
+
// Rejoin session after reconnection
|
|
385
|
+
socket.emit('join-session', sessionId);
|
|
386
|
+
setTimeout(() => {
|
|
387
|
+
hasJoinedSession = true;
|
|
388
|
+
// Process any queued logs
|
|
389
|
+
while (logQueue.length > 0) {
|
|
390
|
+
const logData = logQueue.shift();
|
|
391
|
+
socket.emit('cli-emit', {
|
|
392
|
+
sessionId,
|
|
393
|
+
...logData,
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}, 100);
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
socket.on('reconnect_attempt', (attemptNumber) => {
|
|
400
|
+
// Silent reconnection attempts - don't spam console
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
socket.on('reconnect_error', (error) => {
|
|
404
|
+
// Silent reconnection errors - will keep trying
|
|
405
|
+
});
|
|
406
|
+
|
|
407
|
+
socket.on('reconnect_failed', () => {
|
|
408
|
+
console.error(' ✗ Failed to reconnect after all attempts');
|
|
409
|
+
console.error(' Stream will continue, but logs may be lost until reconnection\n');
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
socket.on('connect_error', (error) => {
|
|
413
|
+
// Don't exit on first connection error - allow reconnection
|
|
414
|
+
if (!isConnected) {
|
|
415
|
+
console.error(' ✗ Connection error:', error.message);
|
|
416
|
+
console.error(' ↻ Retrying connection...\n');
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
socket.on('disconnect', (reason) => {
|
|
421
|
+
isConnected = false;
|
|
422
|
+
hasJoinedSession = false;
|
|
423
|
+
// Don't exit - allow reconnection
|
|
424
|
+
if (reason === 'io server disconnect') {
|
|
425
|
+
// Server disconnected, will reconnect automatically
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
|
|
429
|
+
const rl = readline.createInterface({
|
|
430
|
+
input: process.stdin,
|
|
431
|
+
output: process.stdout,
|
|
432
|
+
terminal: false,
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
rl.on('line', (line) => {
|
|
436
|
+
const trimmedLine = line.trim();
|
|
437
|
+
if (!trimmedLine) {
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
let logData;
|
|
442
|
+
try {
|
|
443
|
+
const parsed = JSON.parse(trimmedLine);
|
|
444
|
+
logData = {
|
|
445
|
+
type: 'json',
|
|
446
|
+
payload: parsed,
|
|
447
|
+
timestamp: Date.now(),
|
|
448
|
+
};
|
|
449
|
+
} catch (e) {
|
|
450
|
+
logData = {
|
|
451
|
+
type: 'text',
|
|
452
|
+
payload: trimmedLine,
|
|
453
|
+
timestamp: Date.now(),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// If connected and joined session, send immediately; otherwise queue it
|
|
458
|
+
if (isConnected && hasJoinedSession && socket.connected) {
|
|
459
|
+
socket.emit('cli-emit', {
|
|
460
|
+
sessionId,
|
|
461
|
+
...logData,
|
|
462
|
+
});
|
|
463
|
+
} else {
|
|
464
|
+
logQueue.push(logData);
|
|
465
|
+
}
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
rl.on('close', () => {
|
|
469
|
+
// Wait for connection and queued logs to be sent
|
|
470
|
+
const waitForQueue = () => {
|
|
471
|
+
if (logQueue.length === 0 || (!isConnected && logQueue.length > 0)) {
|
|
472
|
+
// If not connected and we have queued logs, wait a bit more
|
|
473
|
+
if (!isConnected && logQueue.length > 0) {
|
|
474
|
+
setTimeout(waitForQueue, 200);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
console.log('\n Stream ended. Closing connection...\n');
|
|
478
|
+
if (socket.connected) {
|
|
479
|
+
socket.disconnect();
|
|
480
|
+
}
|
|
481
|
+
setTimeout(() => process.exit(0), 100);
|
|
482
|
+
} else {
|
|
483
|
+
setTimeout(waitForQueue, 100);
|
|
484
|
+
}
|
|
485
|
+
};
|
|
486
|
+
|
|
487
|
+
waitForQueue();
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
process.on('SIGINT', () => {
|
|
491
|
+
console.log('\n Interrupted. Closing connection...\n');
|
|
492
|
+
socket.disconnect();
|
|
493
|
+
process.exit(0);
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
main().catch((error) => {
|
|
498
|
+
console.error('Fatal error:', error);
|
|
499
|
+
process.exit(1);
|
|
500
|
+
});
|
|
501
|
+
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "vibex-sh",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Zero-config observability CLI - pipe logs and visualize instantly",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"vibex": "./bin/vibex.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "index.js",
|
|
10
|
+
"keywords": [
|
|
11
|
+
"observability",
|
|
12
|
+
"logging",
|
|
13
|
+
"monitoring",
|
|
14
|
+
"cli",
|
|
15
|
+
"visualization",
|
|
16
|
+
"vibex"
|
|
17
|
+
],
|
|
18
|
+
"author": "Umut Gokbayrak <umut@vibex.sh>",
|
|
19
|
+
"license": "MIT",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://github.com/vibex-sh/vibex-cli.git"
|
|
23
|
+
},
|
|
24
|
+
"bugs": {
|
|
25
|
+
"url": "https://github.com/vibex-sh/vibex-cli/issues"
|
|
26
|
+
},
|
|
27
|
+
"homepage": "https://vibex.sh",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"commander": "^11.1.0",
|
|
30
|
+
"socket.io-client": "^4.7.2"
|
|
31
|
+
}
|
|
32
|
+
}
|
package/publish.sh
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# Publish script for vibex CLI
|
|
4
|
+
# Usage: ./publish.sh [patch|minor|major|version]
|
|
5
|
+
# Example: ./publish.sh patch
|
|
6
|
+
# Example: ./publish.sh 1.2.3
|
|
7
|
+
|
|
8
|
+
set -e # Exit on error
|
|
9
|
+
|
|
10
|
+
# Colors for output
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
GREEN='\033[0;32m'
|
|
13
|
+
YELLOW='\033[1;33m'
|
|
14
|
+
NC='\033[0m' # No Color
|
|
15
|
+
|
|
16
|
+
# Get current version
|
|
17
|
+
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
|
18
|
+
VERSION_CHANGED=false
|
|
19
|
+
PUBLISH_SUCCESS=false
|
|
20
|
+
|
|
21
|
+
# Rollback function
|
|
22
|
+
rollback_version() {
|
|
23
|
+
if [ "$VERSION_CHANGED" = true ] && [ "$PUBLISH_SUCCESS" = false ]; then
|
|
24
|
+
echo -e "${YELLOW}Rolling back version to ${CURRENT_VERSION}...${NC}"
|
|
25
|
+
npm version $CURRENT_VERSION --no-git-tag-version || true
|
|
26
|
+
VERSION_CHANGED=false # Mark as rolled back to prevent double rollback
|
|
27
|
+
echo -e "${GREEN}Version rolled back to ${CURRENT_VERSION}${NC}"
|
|
28
|
+
fi
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
# Trap to rollback on error
|
|
32
|
+
trap rollback_version ERR
|
|
33
|
+
trap rollback_version EXIT
|
|
34
|
+
|
|
35
|
+
echo -e "${GREEN}Current version: ${CURRENT_VERSION}${NC}"
|
|
36
|
+
|
|
37
|
+
# Determine new version
|
|
38
|
+
if [ -z "$1" ]; then
|
|
39
|
+
echo -e "${RED}Error: Version type or version number required${NC}"
|
|
40
|
+
echo "Usage: ./publish.sh [patch|minor|major|version]"
|
|
41
|
+
echo "Example: ./publish.sh patch"
|
|
42
|
+
echo "Example: ./publish.sh 1.2.3"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
|
|
46
|
+
VERSION_TYPE=$1
|
|
47
|
+
|
|
48
|
+
# Check if it's a semantic version (x.y.z) or a version type (patch/minor/major)
|
|
49
|
+
if [[ $VERSION_TYPE =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
50
|
+
NEW_VERSION=$VERSION_TYPE
|
|
51
|
+
echo -e "${GREEN}Setting version to: ${NEW_VERSION}${NC}"
|
|
52
|
+
npm version $NEW_VERSION --no-git-tag-version
|
|
53
|
+
VERSION_CHANGED=true
|
|
54
|
+
else
|
|
55
|
+
# Validate version type
|
|
56
|
+
if [[ ! "$VERSION_TYPE" =~ ^(patch|minor|major)$ ]]; then
|
|
57
|
+
echo -e "${RED}Error: Invalid version type. Use: patch, minor, or major${NC}"
|
|
58
|
+
exit 1
|
|
59
|
+
fi
|
|
60
|
+
|
|
61
|
+
echo -e "${GREEN}Bumping ${VERSION_TYPE} version...${NC}"
|
|
62
|
+
npm version $VERSION_TYPE --no-git-tag-version
|
|
63
|
+
VERSION_CHANGED=true
|
|
64
|
+
NEW_VERSION=$(node -p "require('./package.json').version")
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
echo -e "${GREEN}New version: ${NEW_VERSION}${NC}"
|
|
68
|
+
|
|
69
|
+
# Check if user is logged in to npm
|
|
70
|
+
if ! npm whoami &> /dev/null; then
|
|
71
|
+
echo -e "${RED}Error: Not logged in to npm${NC}"
|
|
72
|
+
echo "Run: npm login"
|
|
73
|
+
rollback_version
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
|
|
77
|
+
# Confirm before publishing
|
|
78
|
+
echo -e "${YELLOW}Ready to publish ${NEW_VERSION} to npm${NC}"
|
|
79
|
+
read -p "Continue? (y/N) " -n 1 -r
|
|
80
|
+
echo
|
|
81
|
+
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
|
82
|
+
echo -e "${YELLOW}Publish cancelled${NC}"
|
|
83
|
+
# Revert version change
|
|
84
|
+
rollback_version
|
|
85
|
+
exit 1
|
|
86
|
+
fi
|
|
87
|
+
|
|
88
|
+
# Publish to npm
|
|
89
|
+
echo -e "${GREEN}Publishing to npm...${NC}"
|
|
90
|
+
if npm publish; then
|
|
91
|
+
PUBLISH_SUCCESS=true
|
|
92
|
+
echo -e "${GREEN}✓ Successfully published vibex@${NEW_VERSION}${NC}"
|
|
93
|
+
else
|
|
94
|
+
echo -e "${RED}✗ Failed to publish to npm${NC}"
|
|
95
|
+
rollback_version
|
|
96
|
+
exit 1
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Optionally commit and tag (uncomment if desired)
|
|
100
|
+
# echo -e "${GREEN}Creating git commit and tag...${NC}"
|
|
101
|
+
# git add package.json
|
|
102
|
+
# git commit -m "chore: bump version to ${NEW_VERSION}"
|
|
103
|
+
# git tag "v${NEW_VERSION}"
|
|
104
|
+
# echo -e "${GREEN}✓ Git commit and tag created${NC}"
|
|
105
|
+
# echo -e "${YELLOW}Don't forget to push: git push && git push --tags${NC}"
|
|
106
|
+
|