@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.
Files changed (2) hide show
  1. package/index.js +100 -60
  2. 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
- const API_KEY = process.env.SHARNIX_API_KEY || '';
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 setup First-time setup on your local machine
38
- npx @sharnix/agent setup --print-url Print auth URL and exit (for remote/agent use)
39
- npx @sharnix/agent --port <port> Tunnel a local port
40
- SHARNIX_API_KEY=shx_... npx @sharnix/agent --port 3000
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 from relay.sharnix.com/app/settings
51
- SHARNIX_URL Override relay base URL
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
- // ── Setup command (device-flow bootstrap, no API key required) ────────────────
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
- // ── Tunnel main ───────────────────────────────────────────────────────────────
270
- function main() {
271
- console.log(`\n Sharnix Agent → localhost:${port}\n`);
272
- bootstrap()
273
- .then((c) => { creds = c; connect(); })
274
- .catch((err) => { console.error(`\n Error: ${err.message}\n`); process.exit(1); });
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
- // ── Setup: device-flow bootstrap ─────────────────────────────────────────────
278
- async function runSetup(printUrlOnly = false) {
279
- const { spawn } = require('child_process');
280
-
281
- console.log('\n Sharnix Setup\n');
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
- // Start device flow
284
- process.stdout.write(' Starting setup…');
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('Could not reach relay.sharnix.com. Check your internet connection.');
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
- if (printUrlOnly) {
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(' Opening your browser to authorize…');
301
- console.log(` URL: ${authUrl}\n`);
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
- // Open browser cross-platform
304
- try {
305
- if (process.platform === 'win32') {
306
- spawn('cmd', ['/c', 'start', '', authUrl], { detached: true, stdio: 'ignore' });
307
- } else {
308
- const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
309
- spawn(cmd, [authUrl], { detached: true, stdio: 'ignore' });
310
- }
311
- } catch {}
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
- // Poll until authorized
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, 2000));
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) { apiKey = data.key; break; }
316
+ if (data.done) { process.stdout.write(' ✓\n'); return data.key; }
324
317
  }
325
318
  process.stdout.write('\n');
326
- if (!apiKey) throw new Error('Setup timed out. Run npx @sharnix/agent setup again.');
319
+ throw new Error('Setup timed out after 10 minutes.');
320
+ }
327
321
 
328
- console.log('\n API key received!\n');
322
+ function saveKeyToDisk(apiKey) {
323
+ saveJson(path.join(CONFIG_DIR, 'key.json'), { apiKey, createdAt: new Date().toISOString() });
324
+ }
329
325
 
330
- // Save key to ~/.sharnix/config.json
331
- if (!fs.existsSync(CONFIG_DIR)) fs.mkdirSync(CONFIG_DIR, { recursive: true });
332
- saveJson(path.join(CONFIG_DIR, 'sharnix-key.json'), { apiKey, createdAt: new Date().toISOString() });
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.7",
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",