groove-dev 0.27.214 → 0.27.216

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 (31) hide show
  1. package/CLAUDE.md +2 -0
  2. package/bug-reports/Screenshot_2026-08-14_at_1.18.34_PM.png +0 -0
  3. package/node_modules/@groove-dev/cli/bin/groove.js +8 -0
  4. package/node_modules/@groove-dev/cli/package.json +1 -1
  5. package/node_modules/@groove-dev/cli/src/commands/handoff.js +22 -0
  6. package/node_modules/@groove-dev/daemon/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/src/index.js +18 -1
  8. package/node_modules/@groove-dev/daemon/src/journalist.js +28 -0
  9. package/node_modules/@groove-dev/daemon/src/rotator.js +179 -0
  10. package/node_modules/@groove-dev/daemon/src/routes/agents.js +25 -0
  11. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +71 -10
  12. package/node_modules/@groove-dev/daemon/test/rotator.test.js +86 -0
  13. package/node_modules/@groove-dev/daemon/test/tunnel-manager.test.js +44 -1
  14. package/node_modules/@groove-dev/gui/dist/assets/{index-DPsim83z.css → index-B82kMi13.css} +1 -1
  15. package/node_modules/@groove-dev/gui/dist/assets/{index-BP4oE2UL.js → index-DDdFdSWx.js} +228 -223
  16. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/package.json +1 -1
  19. package/packages/cli/bin/groove.js +8 -0
  20. package/packages/cli/package.json +1 -1
  21. package/packages/cli/src/commands/handoff.js +22 -0
  22. package/packages/daemon/package.json +1 -1
  23. package/packages/daemon/src/index.js +18 -1
  24. package/packages/daemon/src/journalist.js +28 -0
  25. package/packages/daemon/src/rotator.js +179 -0
  26. package/packages/daemon/src/routes/agents.js +25 -0
  27. package/packages/daemon/src/tunnel-manager.js +71 -10
  28. package/packages/gui/dist/assets/{index-DPsim83z.css → index-B82kMi13.css} +1 -1
  29. package/packages/gui/dist/assets/{index-BP4oE2UL.js → index-DDdFdSWx.js} +228 -223
  30. package/packages/gui/dist/index.html +2 -2
  31. package/packages/gui/package.json +1 -1
@@ -14,7 +14,7 @@ import { mkdtempSync, rmSync, readFileSync, existsSync } from 'fs';
14
14
  import { tmpdir } from 'os';
15
15
  import { resolve } from 'path';
16
16
  import { createServer } from 'net';
17
- import { TunnelManager } from '../src/tunnel-manager.js';
17
+ import { TunnelManager, resolveBestAddress } from '../src/tunnel-manager.js';
18
18
 
19
19
  function makeDaemon(grooveDir) {
20
20
  const broadcasts = [];
@@ -349,3 +349,46 @@ describe('TunnelManager — wake-from-sleep recovery', () => {
349
349
  assert.equal(await mgr._waitForExit(999999, 500), true);
350
350
  });
351
351
  });
352
+
353
+ describe('resolveBestAddress — multi-homed host selection', () => {
354
+ // The failure this guards: an mDNS name advertising both a wired and a weak
355
+ // Wi-Fi interface. ssh took whichever address the resolver listed first, so
356
+ // tunnels randomly landed on the bad link and died of keepalive timeout.
357
+
358
+ it('passes literal IPs through untouched', async () => {
359
+ assert.equal(await resolveBestAddress('10.0.0.205', 22), '10.0.0.205');
360
+ assert.equal(await resolveBestAddress('::1', 22), '::1');
361
+ });
362
+
363
+ it('falls back to the name when it does not resolve', async () => {
364
+ assert.equal(
365
+ await resolveBestAddress('no-such-host-zzz.invalid', 22, 500),
366
+ 'no-such-host-zzz.invalid',
367
+ );
368
+ });
369
+
370
+ it('picks the address that actually answers when others are dead', async () => {
371
+ // 'localhost' resolves to both ::1 and 127.0.0.1 on typical systems. Bind
372
+ // only the IPv4 side: the resolver must pick 127.0.0.1, not the dead ::1.
373
+ const srv = createServer(() => {});
374
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
375
+ const port = srv.address().port;
376
+ try {
377
+ const best = await resolveBestAddress('localhost', port, 800);
378
+ // On single-address systems resolution is a passthrough — accept that too.
379
+ assert.ok(
380
+ best === '127.0.0.1' || best === 'localhost',
381
+ `picked ${best} — expected the listening 127.0.0.1 (or passthrough)`,
382
+ );
383
+ } finally { srv.close(); }
384
+ });
385
+
386
+ it('falls back to the name when no candidate answers', async () => {
387
+ // Nobody listens on this port on any of localhost's addresses.
388
+ const srv = createServer(() => {});
389
+ await new Promise((r) => srv.listen(0, '127.0.0.1', r));
390
+ const deadPort = srv.address().port;
391
+ await new Promise((r) => srv.close(r)); // free it — now guaranteed dead
392
+ assert.equal(await resolveBestAddress('localhost', deadPort, 500), 'localhost');
393
+ });
394
+ });