@sharnix/agent 1.0.7 → 1.0.8
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/index.js +100 -60
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -10,7 +10,7 @@ const { randomBytes, createHash } = require('crypto');
|
|
|
10
10
|
|
|
11
11
|
const RELAY_BASE = (process.env.SHARNIX_URL || 'https://relay.sharnix.com').replace(/\/$/, '');
|
|
12
12
|
const WS_BASE = RELAY_BASE.replace(/^http/, 'ws');
|
|
13
|
-
|
|
13
|
+
let API_KEY = process.env.SHARNIX_API_KEY || process.env.SHARNIX_KEY || '';
|
|
14
14
|
|
|
15
15
|
// ── Config persistence (declared early so dispatch below can rely on it) ──────
|
|
16
16
|
const CONFIG_DIR = path.join(os.homedir(), '.sharnix');
|
|
@@ -34,10 +34,10 @@ if (has('--help') || has('-h')) {
|
|
|
34
34
|
sharnix — tunnel your local app through Sharnix
|
|
35
35
|
|
|
36
36
|
Usage:
|
|
37
|
-
npx @sharnix/agent
|
|
38
|
-
npx @sharnix/agent
|
|
39
|
-
npx @sharnix/agent
|
|
40
|
-
|
|
37
|
+
npx @sharnix/agent --port <port> Tunnel a local port (auto-runs setup if no key)
|
|
38
|
+
npx @sharnix/agent --port <port> --share Tunnel and print a share link on connect
|
|
39
|
+
npx @sharnix/agent setup Re-run setup explicitly (writes MCP config too)
|
|
40
|
+
npx @sharnix/agent setup --print-url Print auth URL and exit (legacy non-blocking mode)
|
|
41
41
|
|
|
42
42
|
Options:
|
|
43
43
|
--port, -p <n> Local port to forward (default: 3000)
|
|
@@ -47,23 +47,17 @@ if (has('--help') || has('-h')) {
|
|
|
47
47
|
--help, -h Show this message
|
|
48
48
|
|
|
49
49
|
Environment:
|
|
50
|
-
SHARNIX_API_KEY API key
|
|
51
|
-
|
|
50
|
+
SHARNIX_API_KEY API key (also: SHARNIX_KEY). If unset and ~/.sharnix/key.json
|
|
51
|
+
doesn't exist, the CLI will start the device flow automatically.
|
|
52
|
+
SHARNIX_URL Override relay base URL (default: https://relay.sharnix.com)
|
|
52
53
|
`);
|
|
53
54
|
process.exit(0);
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
// ──
|
|
57
|
+
// ── Dispatch ──────────────────────────────────────────────────────────────────
|
|
57
58
|
if (args[0] === 'setup') {
|
|
58
59
|
runSetup(has('--print-url')).catch((err) => { console.error(`\n Error: ${err.message}\n`); process.exit(1); });
|
|
59
60
|
} else {
|
|
60
|
-
if (!API_KEY) {
|
|
61
|
-
console.error('\n Error: SHARNIX_API_KEY is not set.\n');
|
|
62
|
-
console.error(' On your local machine: npx @sharnix/agent setup');
|
|
63
|
-
console.error(' On a remote machine: SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000');
|
|
64
|
-
console.error('\n Get your key from relay.sharnix.com/app/settings → API Keys\n');
|
|
65
|
-
process.exit(1);
|
|
66
|
-
}
|
|
67
61
|
if (isNaN(port) || port < 1 || port > 65535) {
|
|
68
62
|
console.error(`\n Error: invalid port "${get('--port') || get('-p')}"\n`);
|
|
69
63
|
process.exit(1);
|
|
@@ -266,70 +260,116 @@ process.on('SIGINT', () => {
|
|
|
266
260
|
process.exit(0);
|
|
267
261
|
});
|
|
268
262
|
|
|
269
|
-
// ──
|
|
270
|
-
function
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
263
|
+
// ── Key resolution + headless detection ──────────────────────────────────────
|
|
264
|
+
function resolveApiKey() {
|
|
265
|
+
if (API_KEY) return API_KEY;
|
|
266
|
+
for (const name of ['key.json', 'sharnix-key.json']) {
|
|
267
|
+
const data = loadJson(path.join(CONFIG_DIR, name));
|
|
268
|
+
if (data?.apiKey) return data.apiKey;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
275
271
|
}
|
|
276
272
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
273
|
+
function isHeadlessEnv() {
|
|
274
|
+
if (!process.stdout.isTTY) return true;
|
|
275
|
+
if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) return true;
|
|
276
|
+
if (process.env.CI) return true;
|
|
277
|
+
if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
|
|
278
|
+
return false;
|
|
279
|
+
}
|
|
282
280
|
|
|
283
|
-
|
|
284
|
-
|
|
281
|
+
// Device-flow bootstrap — returns the API key without writing MCP config.
|
|
282
|
+
// Shared by both auto-bootstrap (main path) and the explicit `setup` command.
|
|
283
|
+
async function obtainKeyViaDeviceFlow() {
|
|
285
284
|
const r = await fetch(`${RELAY_BASE}/api/v1/setup-cli`, { method: 'POST' });
|
|
286
|
-
if (!r.ok) throw new Error(
|
|
285
|
+
if (!r.ok) throw new Error(`Could not reach ${RELAY_BASE}. Check your connection.`);
|
|
287
286
|
const { code, authUrl } = await r.json();
|
|
288
|
-
process.stdout.write(' done\n\n');
|
|
289
287
|
|
|
290
|
-
|
|
291
|
-
// Non-blocking mode for agents running on remote machines
|
|
292
|
-
console.log(' Authorization URL (open this in your browser):\n');
|
|
293
|
-
console.log(` ${authUrl}\n`);
|
|
294
|
-
console.log(' After authorizing, run the tunnel with:');
|
|
295
|
-
console.log(` SHARNIX_API_KEY=<your-key> npx @sharnix/agent --port 3000\n`);
|
|
296
|
-
console.log(' Your key will be shown on the authorization page after you click "Authorize".\n');
|
|
297
|
-
process.exit(0);
|
|
298
|
-
}
|
|
288
|
+
const headless = isHeadlessEnv();
|
|
299
289
|
|
|
300
|
-
console.log('
|
|
301
|
-
console.log(
|
|
290
|
+
console.log('\n ──────────────────────────────────────────────────────────');
|
|
291
|
+
console.log(' No Sharnix key found — let\'s set one up. (10-second flow.)');
|
|
292
|
+
console.log(' ──────────────────────────────────────────────────────────\n');
|
|
293
|
+
console.log(' Open this URL in any browser to authorize (your phone works fine):\n');
|
|
294
|
+
console.log(` ${authUrl}\n`);
|
|
302
295
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
296
|
+
if (!headless) {
|
|
297
|
+
try {
|
|
298
|
+
const { spawn } = require('child_process');
|
|
299
|
+
if (process.platform === 'win32') {
|
|
300
|
+
spawn('cmd', ['/c', 'start', '', authUrl], { detached: true, stdio: 'ignore' });
|
|
301
|
+
} else {
|
|
302
|
+
const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
303
|
+
spawn(cmd, [authUrl], { detached: true, stdio: 'ignore' });
|
|
304
|
+
}
|
|
305
|
+
} catch {}
|
|
306
|
+
}
|
|
312
307
|
|
|
313
|
-
|
|
314
|
-
process.stdout.write(' Waiting for browser authorization');
|
|
315
|
-
let apiKey = null;
|
|
308
|
+
process.stdout.write(' Waiting for authorization');
|
|
316
309
|
const deadline = Date.now() + 10 * 60 * 1000;
|
|
317
310
|
while (Date.now() < deadline) {
|
|
318
|
-
await new Promise((r) => setTimeout(r,
|
|
311
|
+
await new Promise((r) => setTimeout(r, 3000));
|
|
319
312
|
process.stdout.write('.');
|
|
320
313
|
const poll = await fetch(`${RELAY_BASE}/api/v1/setup-cli/${code}`);
|
|
321
314
|
if (!poll.ok) { process.stdout.write('\n'); throw new Error('Setup session expired.'); }
|
|
322
315
|
const data = await poll.json();
|
|
323
|
-
if (data.done) {
|
|
316
|
+
if (data.done) { process.stdout.write(' ✓\n'); return data.key; }
|
|
324
317
|
}
|
|
325
318
|
process.stdout.write('\n');
|
|
326
|
-
|
|
319
|
+
throw new Error('Setup timed out after 10 minutes.');
|
|
320
|
+
}
|
|
327
321
|
|
|
328
|
-
|
|
322
|
+
function saveKeyToDisk(apiKey) {
|
|
323
|
+
saveJson(path.join(CONFIG_DIR, 'key.json'), { apiKey, createdAt: new Date().toISOString() });
|
|
324
|
+
}
|
|
329
325
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
326
|
+
// ── Tunnel main ───────────────────────────────────────────────────────────────
|
|
327
|
+
async function main() {
|
|
328
|
+
console.log(`\n Sharnix Agent → localhost:${port}`);
|
|
329
|
+
|
|
330
|
+
// Resolve a key, or trigger device flow inline if none.
|
|
331
|
+
API_KEY = resolveApiKey();
|
|
332
|
+
if (!API_KEY) {
|
|
333
|
+
try {
|
|
334
|
+
API_KEY = await obtainKeyViaDeviceFlow();
|
|
335
|
+
saveKeyToDisk(API_KEY);
|
|
336
|
+
console.log(` ✓ Key saved to ${path.join(CONFIG_DIR, 'key.json')}\n`);
|
|
337
|
+
} catch (err) {
|
|
338
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
339
|
+
process.exit(1);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
try {
|
|
344
|
+
creds = await bootstrap();
|
|
345
|
+
connect();
|
|
346
|
+
} catch (err) {
|
|
347
|
+
console.error(`\n Error: ${err.message}\n`);
|
|
348
|
+
process.exit(1);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// ── Setup: explicit `setup` subcommand — bootstrap + MCP config write ────────
|
|
353
|
+
async function runSetup(printUrlOnly = false) {
|
|
354
|
+
console.log('\n Sharnix Setup\n');
|
|
355
|
+
|
|
356
|
+
if (printUrlOnly) {
|
|
357
|
+
// Legacy non-blocking mode — start device flow, print URL, exit.
|
|
358
|
+
process.stdout.write(' Starting setup…');
|
|
359
|
+
const r = await fetch(`${RELAY_BASE}/api/v1/setup-cli`, { method: 'POST' });
|
|
360
|
+
if (!r.ok) throw new Error('Could not reach relay.sharnix.com. Check your internet connection.');
|
|
361
|
+
const { authUrl } = await r.json();
|
|
362
|
+
process.stdout.write(' done\n\n');
|
|
363
|
+
console.log(' Authorization URL (open this in your browser):\n');
|
|
364
|
+
console.log(` ${authUrl}\n`);
|
|
365
|
+
console.log(' After authorizing, just run the tunnel — the CLI will read the saved key:');
|
|
366
|
+
console.log(' npx @sharnix/agent --port 3000\n');
|
|
367
|
+
process.exit(0);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const apiKey = await obtainKeyViaDeviceFlow();
|
|
371
|
+
console.log('\n API key received!\n');
|
|
372
|
+
saveKeyToDisk(apiKey);
|
|
333
373
|
|
|
334
374
|
// Detect and write MCP configs
|
|
335
375
|
const mcpBlock = {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sharnix/agent",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.8",
|
|
4
4
|
"description": "Tunnel your local app through Sharnix — share previews with one command",
|
|
5
5
|
"keywords": ["tunnel", "preview", "sharing", "localhost", "sharnix"],
|
|
6
6
|
"homepage": "https://relay.sharnix.com",
|