@sharnix/agent 1.0.7 → 1.0.9

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 +128 -69
  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);
@@ -181,18 +175,37 @@ function connect() {
181
175
  }
182
176
 
183
177
  async function createShareLink() {
178
+ let orgs;
184
179
  try {
185
- const orgs = await api('GET', '/api/v1/orgs');
186
- if (!orgs.length) return;
187
- const orgSlug = creds.orgSlug || orgs[0].slug;
188
- const result = await api('POST', `/api/v1/orgs/${orgSlug}/tunnels/${tunnelId}/links`, {
189
- permission: 'read-only',
190
- label: label || 'Shared via agent',
191
- });
192
- console.log(` Share link : ${result.url}`);
193
- console.log(` Permission : read-only\n`);
180
+ orgs = await api('GET', '/api/v1/orgs');
194
181
  } catch (err) {
195
182
  console.error(` Could not create share link: ${err.message}`);
183
+ return;
184
+ }
185
+ if (!orgs.length) return;
186
+ const orgSlug = creds.orgSlug || orgs[0].slug;
187
+
188
+ // The relay inserts the tunnel row from an async handler triggered by our WS REGISTER.
189
+ // The HTTP POST below can arrive before that insert completes, yielding tunnel_not_found.
190
+ // Retry briefly on that specific error; surface any other error immediately.
191
+ const delays = [400, 800, 1500, 2500];
192
+ for (let attempt = 0; ; attempt++) {
193
+ try {
194
+ const result = await api('POST', `/api/v1/orgs/${orgSlug}/tunnels/${tunnelId}/links`, {
195
+ permission: 'read-only',
196
+ label: label || 'Shared via agent',
197
+ });
198
+ console.log(` Share link : ${result.url}`);
199
+ console.log(` Permission : read-only\n`);
200
+ return;
201
+ } catch (err) {
202
+ if (err.message === 'tunnel_not_found' && attempt < delays.length) {
203
+ await new Promise((r) => setTimeout(r, delays[attempt]));
204
+ continue;
205
+ }
206
+ console.error(` Could not create share link: ${err.message}`);
207
+ return;
208
+ }
196
209
  }
197
210
  }
198
211
 
@@ -266,70 +279,116 @@ process.on('SIGINT', () => {
266
279
  process.exit(0);
267
280
  });
268
281
 
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); });
282
+ // ── Key resolution + headless detection ──────────────────────────────────────
283
+ function resolveApiKey() {
284
+ if (API_KEY) return API_KEY;
285
+ for (const name of ['key.json', 'sharnix-key.json']) {
286
+ const data = loadJson(path.join(CONFIG_DIR, name));
287
+ if (data?.apiKey) return data.apiKey;
288
+ }
289
+ return null;
275
290
  }
276
291
 
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');
292
+ function isHeadlessEnv() {
293
+ if (!process.stdout.isTTY) return true;
294
+ if (process.env.SSH_CLIENT || process.env.SSH_TTY || process.env.SSH_CONNECTION) return true;
295
+ if (process.env.CI) return true;
296
+ if (process.platform === 'linux' && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) return true;
297
+ return false;
298
+ }
282
299
 
283
- // Start device flow
284
- process.stdout.write(' Starting setup…');
300
+ // Device-flow bootstrap — returns the API key without writing MCP config.
301
+ // Shared by both auto-bootstrap (main path) and the explicit `setup` command.
302
+ async function obtainKeyViaDeviceFlow() {
285
303
  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.');
304
+ if (!r.ok) throw new Error(`Could not reach ${RELAY_BASE}. Check your connection.`);
287
305
  const { code, authUrl } = await r.json();
288
- process.stdout.write(' done\n\n');
289
306
 
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
- }
307
+ const headless = isHeadlessEnv();
299
308
 
300
- console.log(' Opening your browser to authorize…');
301
- console.log(` URL: ${authUrl}\n`);
309
+ console.log('\n ──────────────────────────────────────────────────────────');
310
+ console.log(' No Sharnix key found — let\'s set one up. (10-second flow.)');
311
+ console.log(' ──────────────────────────────────────────────────────────\n');
312
+ console.log(' Open this URL in any browser to authorize (your phone works fine):\n');
313
+ console.log(` ${authUrl}\n`);
302
314
 
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 {}
315
+ if (!headless) {
316
+ try {
317
+ const { spawn } = require('child_process');
318
+ if (process.platform === 'win32') {
319
+ spawn('cmd', ['/c', 'start', '', authUrl], { detached: true, stdio: 'ignore' });
320
+ } else {
321
+ const cmd = process.platform === 'darwin' ? 'open' : 'xdg-open';
322
+ spawn(cmd, [authUrl], { detached: true, stdio: 'ignore' });
323
+ }
324
+ } catch {}
325
+ }
312
326
 
313
- // Poll until authorized
314
- process.stdout.write(' Waiting for browser authorization');
315
- let apiKey = null;
327
+ process.stdout.write(' Waiting for authorization');
316
328
  const deadline = Date.now() + 10 * 60 * 1000;
317
329
  while (Date.now() < deadline) {
318
- await new Promise((r) => setTimeout(r, 2000));
330
+ await new Promise((r) => setTimeout(r, 3000));
319
331
  process.stdout.write('.');
320
332
  const poll = await fetch(`${RELAY_BASE}/api/v1/setup-cli/${code}`);
321
333
  if (!poll.ok) { process.stdout.write('\n'); throw new Error('Setup session expired.'); }
322
334
  const data = await poll.json();
323
- if (data.done) { apiKey = data.key; break; }
335
+ if (data.done) { process.stdout.write(' ✓\n'); return data.key; }
324
336
  }
325
337
  process.stdout.write('\n');
326
- if (!apiKey) throw new Error('Setup timed out. Run npx @sharnix/agent setup again.');
338
+ throw new Error('Setup timed out after 10 minutes.');
339
+ }
327
340
 
328
- console.log('\n API key received!\n');
341
+ function saveKeyToDisk(apiKey) {
342
+ saveJson(path.join(CONFIG_DIR, 'key.json'), { apiKey, createdAt: new Date().toISOString() });
343
+ }
344
+
345
+ // ── Tunnel main ───────────────────────────────────────────────────────────────
346
+ async function main() {
347
+ console.log(`\n Sharnix Agent → localhost:${port}`);
348
+
349
+ // Resolve a key, or trigger device flow inline if none.
350
+ API_KEY = resolveApiKey();
351
+ if (!API_KEY) {
352
+ try {
353
+ API_KEY = await obtainKeyViaDeviceFlow();
354
+ saveKeyToDisk(API_KEY);
355
+ console.log(` ✓ Key saved to ${path.join(CONFIG_DIR, 'key.json')}\n`);
356
+ } catch (err) {
357
+ console.error(`\n Error: ${err.message}\n`);
358
+ process.exit(1);
359
+ }
360
+ }
361
+
362
+ try {
363
+ creds = await bootstrap();
364
+ connect();
365
+ } catch (err) {
366
+ console.error(`\n Error: ${err.message}\n`);
367
+ process.exit(1);
368
+ }
369
+ }
329
370
 
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() });
371
+ // ── Setup: explicit `setup` subcommand — bootstrap + MCP config write ────────
372
+ async function runSetup(printUrlOnly = false) {
373
+ console.log('\n Sharnix Setup\n');
374
+
375
+ if (printUrlOnly) {
376
+ // Legacy non-blocking mode — start device flow, print URL, exit.
377
+ process.stdout.write(' Starting setup…');
378
+ const r = await fetch(`${RELAY_BASE}/api/v1/setup-cli`, { method: 'POST' });
379
+ if (!r.ok) throw new Error('Could not reach relay.sharnix.com. Check your internet connection.');
380
+ const { authUrl } = await r.json();
381
+ process.stdout.write(' done\n\n');
382
+ console.log(' Authorization URL (open this in your browser):\n');
383
+ console.log(` ${authUrl}\n`);
384
+ console.log(' After authorizing, just run the tunnel — the CLI will read the saved key:');
385
+ console.log(' npx @sharnix/agent --port 3000\n');
386
+ process.exit(0);
387
+ }
388
+
389
+ const apiKey = await obtainKeyViaDeviceFlow();
390
+ console.log('\n API key received!\n');
391
+ saveKeyToDisk(apiKey);
333
392
 
334
393
  // Detect and write MCP configs
335
394
  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.9",
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",