gm-plugkit 2.0.1222 → 2.0.1223

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gm-plugkit",
3
- "version": "2.0.1222",
3
+ "version": "2.0.1223",
4
4
  "description": "Bootstrap and daemon-spawn tool for gm plugkit binary. Downloads the correct platform binary, verifies SHA256, and starts the spool watcher daemon. Includes plugkit-wasm-wrapper for WASM-based spool watching.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -651,9 +651,48 @@ function ensureGitignored(cwd, entry) {
651
651
  } catch (_) {}
652
652
  }
653
653
 
654
+ function isProcessAliveSync(pid) {
655
+ if (!pid || typeof pid !== 'number' || pid <= 0) return false;
656
+ try {
657
+ process.kill(pid, 0);
658
+ return true;
659
+ } catch (e) {
660
+ return e && e.code === 'EPERM';
661
+ }
662
+ }
663
+
664
+ function readSingletonLockPid(profileDir) {
665
+ const lock = path.join(profileDir, 'SingletonLock');
666
+ try {
667
+ let target;
668
+ try {
669
+ target = fs.readlinkSync(lock);
670
+ } catch (_) {
671
+ try { target = fs.readFileSync(lock, 'utf-8'); } catch (__) { return null; }
672
+ }
673
+ if (!target) return null;
674
+ const m = String(target).match(/-(\d+)\s*$/);
675
+ if (m) return parseInt(m[1], 10);
676
+ const m2 = String(target).match(/(\d+)/);
677
+ if (m2) return parseInt(m2[1], 10);
678
+ } catch (_) {}
679
+ return null;
680
+ }
681
+
654
682
  function isProfileLocked(profileDir) {
655
683
  const lock = path.join(profileDir, 'SingletonLock');
656
- return fs.existsSync(lock);
684
+ if (!fs.existsSync(lock)) return false;
685
+ const holderPid = readSingletonLockPid(profileDir);
686
+ if (holderPid != null && !isProcessAliveSync(holderPid)) {
687
+ try { fs.unlinkSync(lock); } catch (_) {}
688
+ try { fs.unlinkSync(path.join(profileDir, 'SingletonCookie')); } catch (_) {}
689
+ try { fs.unlinkSync(path.join(profileDir, 'SingletonSocket')); } catch (_) {}
690
+ logEvent('bootstrap', 'browser-profile.lock-cleared', {
691
+ profileDir, dead_pid: holderPid,
692
+ });
693
+ return false;
694
+ }
695
+ return true;
657
696
  }
658
697
 
659
698
  function acquireProfileDir(cwd) {
@@ -669,6 +708,31 @@ function acquireProfileDir(cwd) {
669
708
  return fallback;
670
709
  }
671
710
 
711
+ function cleanDeadProfileFragments(cwd) {
712
+ try {
713
+ const gmDir = path.join(cwd, '.gm');
714
+ if (!fs.existsSync(gmDir)) return { cleaned: 0 };
715
+ let cleaned = 0;
716
+ for (const name of fs.readdirSync(gmDir)) {
717
+ const m = name.match(/^browser-profile-(\d+)$/);
718
+ if (!m) continue;
719
+ const pid = parseInt(m[1], 10);
720
+ if (!isProcessAliveSync(pid)) {
721
+ try {
722
+ fs.rmSync(path.join(gmDir, name), { recursive: true, force: true });
723
+ cleaned++;
724
+ } catch (_) {}
725
+ }
726
+ }
727
+ if (cleaned > 0) {
728
+ logEvent('bootstrap', 'browser-profile.hygiene', { cwd, cleaned });
729
+ }
730
+ return { cleaned };
731
+ } catch (_) {
732
+ return { cleaned: 0 };
733
+ }
734
+ }
735
+
672
736
  function findFreePortSync() {
673
737
  const r = spawnSync(process.execPath, ['-e', `
674
738
  const net = require('net');
@@ -722,10 +786,31 @@ function getOrCreateBrowserSession(cwd, claudeSessionId, pw) {
722
786
  const ports = readJsonFile(portsFile, {});
723
787
  const sessions = readJsonFile(sessionsFile, {});
724
788
  const existing = ports[claudeSessionId];
725
- if (existing && existing.port && isPortAliveSync(existing.port)) {
726
- const pwIds = sessions[claudeSessionId] || [];
727
- if (pwIds.length > 0) return pwIds[0];
789
+ if (existing && existing.port) {
790
+ const wantProfile = path.join(cwd, '.gm', 'browser-profile');
791
+ const pidOk = existing.pid && isProcessAliveSync(existing.pid);
792
+ const profileOk = !existing.profileDir || existing.profileDir === wantProfile || existing.profileDir.startsWith(path.join(cwd, '.gm', 'browser-profile'));
793
+ const portOk = isPortAliveSync(existing.port);
794
+ if (pidOk && profileOk && portOk) {
795
+ const pwIds = sessions[claudeSessionId] || [];
796
+ if (pwIds.length > 0) return pwIds[0];
797
+ } else {
798
+ const reason = !pidOk ? 'pid-dead' : !profileOk ? 'profile-drift' : 'port-dead';
799
+ logEvent('hook', 'deviation.browser-profile-collision', {
800
+ sid: claudeSessionId,
801
+ stale_pid: existing.pid || null,
802
+ stale_port: existing.port || null,
803
+ stale_profile: existing.profileDir || null,
804
+ want_profile: wantProfile,
805
+ reason,
806
+ });
807
+ delete ports[claudeSessionId];
808
+ delete sessions[claudeSessionId];
809
+ try { writeJsonFile(portsFile, ports); } catch (_) {}
810
+ try { writeJsonFile(sessionsFile, sessions); } catch (_) {}
811
+ }
728
812
  }
813
+ cleanDeadProfileFragments(cwd);
729
814
  const chrome = findChrome();
730
815
  if (!chrome) throw new Error('Chrome not found. Please install Google Chrome.');
731
816
  const profileDir = acquireProfileDir(cwd);
@@ -1385,7 +1470,12 @@ function makeHostFunctions(instanceRef) {
1385
1470
  });
1386
1471
  }
1387
1472
  const pwSessionId = getOrCreateBrowserSession(cwd, sessionId, pw);
1388
- const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', '-e', body], 60000);
1473
+ const portsAfter = readJsonFile(browserPortsFile(cwd), {});
1474
+ const livePort = portsAfter[sessionId] && portsAfter[sessionId].port;
1475
+ const directArgs = (livePort && isPortAliveSync(livePort))
1476
+ ? [`--direct=localhost:${livePort}`]
1477
+ : [];
1478
+ const r = runPlaywriter(pw, ['-s', pwSessionId, '--timeout', '14000', ...directArgs, '-e', body], 60000);
1389
1479
  return writeWasmJson(instanceRef.value, {
1390
1480
  ok: r.status === 0,
1391
1481
  stdout: scrubBrowserRunnerText(r.stdout || ''),