sessionsnap 0.0.1 → 0.0.3

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/README.md CHANGED
@@ -1,7 +1,5 @@
1
1
  # sessionsnap
2
2
 
3
- > **KISS — Keep It Simple, Stupid.**
4
- >
5
3
  > Authenticated browser sessions shouldn't require complex auth flows, token management, cookie injection hacks, or custom login scripts per site. Just open a browser, log in like a human, and let the tool save & reuse that session. That's it.
6
4
 
7
5
  Capture and reuse authenticated browser sessions using **manual login**. Supports both Puppeteer and Playwright.
@@ -19,16 +19,17 @@ program
19
19
  .requiredOption('--profile <name>', 'Profile name to save the session under')
20
20
  .option('--out <file>', 'Custom output path for the snapshot JSON')
21
21
  .option('--wait <minutes>', 'Max minutes to wait for login', parseFloat)
22
+ .option('--target <pattern>', 'Regex pattern to wait for in URL (e.g., "/dashboard" or "^https://.*/app")')
22
23
  .option('--record', 'Record user actions (clicks, inputs, navigations)')
23
24
  .action(async (url, opts) => {
24
25
  validateRunner(opts.runner);
25
26
  try {
26
27
  if (opts.runner === 'puppeteer') {
27
28
  const { capture } = await import('../src/puppeteer/capture.js');
28
- await capture(url, { profile: opts.profile, out: opts.out, waitMinutes: opts.wait, record: opts.record });
29
+ await capture(url, { profile: opts.profile, out: opts.out, waitMinutes: opts.wait, targetPattern: opts.target, record: opts.record });
29
30
  } else {
30
31
  const { capture } = await import('../src/playwright/capture.js');
31
- await capture(url, { profile: opts.profile, out: opts.out, waitMinutes: opts.wait, record: opts.record });
32
+ await capture(url, { profile: opts.profile, out: opts.out, waitMinutes: opts.wait, targetPattern: opts.target, record: opts.record });
32
33
  }
33
34
  } catch (err) {
34
35
  console.error(`[sessionsnap] Error: ${err.message}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sessionsnap",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Capture and reuse authenticated browser sessions (Puppeteer + Playwright)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,7 +12,7 @@
12
12
  "dependencies": {
13
13
  "commander": "^12.1.0",
14
14
  "css-selector-generator": "^3.8.0",
15
- "puppeteer": "^24.2.1"
15
+ "puppeteer-core": "^24.2.1"
16
16
  },
17
17
  "peerDependencies": {
18
18
  "playwright": ">=1.40.0"
@@ -18,10 +18,22 @@ export async function getCookieCount(page, runner) {
18
18
  return cookies.length;
19
19
  }
20
20
 
21
- export async function waitForLoginComplete(page, { runner, waitMinutes } = {}) {
21
+ export async function waitForLoginComplete(page, { runner, waitMinutes, targetPattern } = {}) {
22
22
  const timeout = (waitMinutes ?? DEFAULT_TIMEOUT_MIN) * 60 * 1000;
23
23
  const start = Date.now();
24
24
  const initialCookieCount = await getCookieCount(page, runner);
25
+
26
+ // Compile target pattern if provided
27
+ let targetRegex = null;
28
+ if (targetPattern) {
29
+ try {
30
+ targetRegex = new RegExp(targetPattern);
31
+ console.log(`[sessionsnap] Waiting for URL matching pattern: ${targetPattern}`);
32
+ } catch (err) {
33
+ console.error(`[sessionsnap] Invalid regex pattern: ${targetPattern}. Error: ${err.message}`);
34
+ throw new Error(`Invalid target pattern: ${targetPattern}`);
35
+ }
36
+ }
25
37
 
26
38
  return new Promise((resolve) => {
27
39
  const timer = setInterval(async () => {
@@ -36,6 +48,20 @@ export async function waitForLoginComplete(page, { runner, waitMinutes } = {}) {
36
48
  }
37
49
 
38
50
  const currentUrl = page.url();
51
+
52
+ // If target pattern is provided, check if URL matches
53
+ if (targetRegex) {
54
+ if (targetRegex.test(currentUrl)) {
55
+ clearInterval(timer);
56
+ console.log(`[sessionsnap] Target URL pattern matched: ${currentUrl}`);
57
+ resolve(true);
58
+ return;
59
+ }
60
+ // Continue waiting if pattern doesn't match yet
61
+ return;
62
+ }
63
+
64
+ // Default behavior: check URL and cookie heuristics
39
65
  const urlClear = !isLoginUrl(currentUrl);
40
66
 
41
67
  let cookieIncreased = false;
@@ -3,7 +3,7 @@ import { waitForLoginComplete } from '../core/heuristics.js';
3
3
  import { createSnapshot, normalizePlaywrightState } from '../core/snapshot.js';
4
4
  import { setupRecorder } from '../core/recorder.js';
5
5
 
6
- export async function capture(url, { profile, out, waitMinutes, record }) {
6
+ export async function capture(url, { profile, out, waitMinutes, targetPattern, record }) {
7
7
  const { chromium } = await import('playwright');
8
8
  const userDataDir = await ensureProfileDir(profile);
9
9
 
@@ -24,12 +24,16 @@ export async function capture(url, { profile, out, waitMinutes, record }) {
24
24
  console.log(`[sessionsnap] Navigating to: ${url}`);
25
25
  await page.goto(url, { waitUntil: 'domcontentloaded' });
26
26
 
27
- console.log('[sessionsnap] Please log in manually. Login completion will be detected automatically.');
27
+ if (targetPattern) {
28
+ console.log(`[sessionsnap] Please log in manually. Waiting for URL matching pattern: ${targetPattern}`);
29
+ } else {
30
+ console.log('[sessionsnap] Please log in manually. Login completion will be detected automatically.');
31
+ }
28
32
  if (record) {
29
33
  console.log('[sessionsnap] Recording actions... (press Ctrl+C to stop early)');
30
34
  }
31
35
 
32
- const detected = await waitForLoginComplete(page, { runner: 'playwright', waitMinutes });
36
+ const detected = await waitForLoginComplete(page, { runner: 'playwright', waitMinutes, targetPattern });
33
37
 
34
38
  if (detected) {
35
39
  console.log('[sessionsnap] Login detected! Capturing session...');
@@ -0,0 +1,108 @@
1
+ import { execSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+
4
+ /**
5
+ * Find Chrome/Chromium executable path on the system.
6
+ * Returns the path or null if not found.
7
+ */
8
+ export function findChromeExecutable() {
9
+ const platform = process.platform;
10
+
11
+ // Common Chrome/Chromium paths
12
+ const possiblePaths = [];
13
+
14
+ if (platform === 'darwin') {
15
+ // macOS
16
+ possiblePaths.push(
17
+ '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
18
+ '/Applications/Chromium.app/Contents/MacOS/Chromium',
19
+ '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary',
20
+ process.env.CHROME_PATH
21
+ );
22
+ } else if (platform === 'linux') {
23
+ // Linux
24
+ possiblePaths.push(
25
+ '/usr/bin/google-chrome',
26
+ '/usr/bin/google-chrome-stable',
27
+ '/usr/bin/chromium',
28
+ '/usr/bin/chromium-browser',
29
+ '/snap/bin/chromium',
30
+ process.env.CHROME_PATH
31
+ );
32
+ } else if (platform === 'win32') {
33
+ // Windows
34
+ const programFiles = [
35
+ process.env.PROGRAMFILES,
36
+ process.env['PROGRAMFILES(X86)'],
37
+ process.env.LOCALAPPDATA,
38
+ ].filter(Boolean);
39
+
40
+ for (const pf of programFiles) {
41
+ possiblePaths.push(
42
+ `${pf}\\Google\\Chrome\\Application\\chrome.exe`,
43
+ `${pf}\\Chromium\\Application\\chrome.exe`
44
+ );
45
+ }
46
+
47
+ // Also check common user install location
48
+ if (process.env.LOCALAPPDATA) {
49
+ possiblePaths.push(
50
+ `${process.env.LOCALAPPDATA}\\Google\\Chrome\\Application\\chrome.exe`,
51
+ `${process.env.LOCALAPPDATA}\\Chromium\\Application\\chrome.exe`
52
+ );
53
+ }
54
+
55
+ possiblePaths.push(process.env.CHROME_PATH);
56
+ }
57
+
58
+ // Filter out undefined/null values and check if file exists
59
+ for (const path of possiblePaths) {
60
+ if (path && existsSync(path)) {
61
+ return path;
62
+ }
63
+ }
64
+
65
+ // Try to find via command line (works on macOS and Linux)
66
+ if (platform === 'darwin' || platform === 'linux') {
67
+ try {
68
+ const commands = platform === 'darwin'
69
+ ? ['mdfind "kMDItemCFBundleIdentifier == \'com.google.Chrome\'" | head -1']
70
+ : ['which google-chrome', 'which chromium', 'which chromium-browser'];
71
+
72
+ for (const cmd of commands) {
73
+ try {
74
+ const result = execSync(cmd, { encoding: 'utf8', stdio: 'pipe' }).trim();
75
+ if (result && existsSync(result)) {
76
+ // On macOS, mdfind returns the .app path, need to append the executable
77
+ if (platform === 'darwin' && result.endsWith('.app')) {
78
+ return `${result}/Contents/MacOS/Google Chrome`;
79
+ }
80
+ return result;
81
+ }
82
+ } catch {
83
+ // Command failed, try next
84
+ }
85
+ }
86
+ } catch {
87
+ // Fallback failed
88
+ }
89
+ }
90
+
91
+ return null;
92
+ }
93
+
94
+ /**
95
+ * Get Chrome executable path, throwing an error if not found.
96
+ */
97
+ export function getChromeExecutable() {
98
+ const path = findChromeExecutable();
99
+ if (!path) {
100
+ throw new Error(
101
+ 'Chrome/Chromium not found. Please install Chrome or set CHROME_PATH environment variable.\n' +
102
+ ' macOS: brew install --cask google-chrome\n' +
103
+ ' Linux: sudo apt-get install google-chrome-stable\n' +
104
+ ' Or set CHROME_PATH=/path/to/chrome'
105
+ );
106
+ }
107
+ return path;
108
+ }
@@ -2,14 +2,17 @@ import { ensureProfileDir, saveSnapshot, saveScreenshot, DEFAULT_VIEWPORT } from
2
2
  import { waitForLoginComplete } from '../core/heuristics.js';
3
3
  import { createSnapshot, normalizePuppeteerCookies } from '../core/snapshot.js';
4
4
  import { setupRecorder } from '../core/recorder.js';
5
+ import { getChromeExecutable } from './browser.js';
5
6
 
6
- export async function capture(url, { profile, out, waitMinutes, record }) {
7
- const puppeteer = await import('puppeteer');
7
+ export async function capture(url, { profile, out, waitMinutes, targetPattern, record }) {
8
+ const puppeteer = await import('puppeteer-core');
8
9
  const userDataDir = await ensureProfileDir(profile);
10
+ const executablePath = getChromeExecutable();
9
11
 
10
12
  console.log('[sessionsnap] Launching browser (Puppeteer)...');
11
13
  const browser = await puppeteer.default.launch({
12
14
  headless: false,
15
+ executablePath,
13
16
  userDataDir,
14
17
  defaultViewport: DEFAULT_VIEWPORT,
15
18
  args: [`--window-size=${DEFAULT_VIEWPORT.width},${DEFAULT_VIEWPORT.height}`],
@@ -25,12 +28,16 @@ export async function capture(url, { profile, out, waitMinutes, record }) {
25
28
  console.log(`[sessionsnap] Navigating to: ${url}`);
26
29
  await page.goto(url, { waitUntil: 'domcontentloaded' });
27
30
 
28
- console.log('[sessionsnap] Please log in manually. Login completion will be detected automatically.');
31
+ if (targetPattern) {
32
+ console.log(`[sessionsnap] Please log in manually. Waiting for URL matching pattern: ${targetPattern}`);
33
+ } else {
34
+ console.log('[sessionsnap] Please log in manually. Login completion will be detected automatically.');
35
+ }
29
36
  if (record) {
30
37
  console.log('[sessionsnap] Recording actions... (press Ctrl+C to stop early)');
31
38
  }
32
39
 
33
- const detected = await waitForLoginComplete(page, { runner: 'puppeteer', waitMinutes });
40
+ const detected = await waitForLoginComplete(page, { runner: 'puppeteer', waitMinutes, targetPattern });
34
41
 
35
42
  if (detected) {
36
43
  console.log('[sessionsnap] Login detected! Capturing session...');
@@ -1,10 +1,12 @@
1
1
  import { ensureProfileDir, loadSnapshot, saveSnapshot, saveScreenshot, DEFAULT_VIEWPORT } from '../store.js';
2
2
  import { normalizePuppeteerCookies, updateSnapshot } from '../core/snapshot.js';
3
3
  import { setupRecorder } from '../core/recorder.js';
4
+ import { getChromeExecutable } from './browser.js';
4
5
 
5
6
  export async function open(url, { profile, record }) {
6
- const puppeteer = await import('puppeteer');
7
+ const puppeteer = await import('puppeteer-core');
7
8
  const userDataDir = await ensureProfileDir(profile);
9
+ const executablePath = getChromeExecutable();
8
10
 
9
11
  // Verify snapshot exists
10
12
  await loadSnapshot(profile);
@@ -12,6 +14,7 @@ export async function open(url, { profile, record }) {
12
14
  console.log('[sessionsnap] Launching browser (Puppeteer)...');
13
15
  const browser = await puppeteer.default.launch({
14
16
  headless: false,
17
+ executablePath,
15
18
  userDataDir,
16
19
  defaultViewport: DEFAULT_VIEWPORT,
17
20
  args: [`--window-size=${DEFAULT_VIEWPORT.width},${DEFAULT_VIEWPORT.height}`],
@@ -1,9 +1,11 @@
1
1
  import { ensureProfileDir, loadSnapshot, saveScreenshot, DEFAULT_VIEWPORT } from '../store.js';
2
2
  import { replayActions } from '../core/replayer.js';
3
+ import { getChromeExecutable } from './browser.js';
3
4
 
4
5
  export async function replay(actionsData, { profile, speed, headed, bail, visual }) {
5
- const puppeteer = await import('puppeteer');
6
+ const puppeteer = await import('puppeteer-core');
6
7
  const userDataDir = await ensureProfileDir(profile);
8
+ const executablePath = getChromeExecutable();
7
9
 
8
10
  // Verify snapshot exists
9
11
  await loadSnapshot(profile);
@@ -13,6 +15,7 @@ export async function replay(actionsData, { profile, speed, headed, bail, visual
13
15
  console.log('[sessionsnap] Launching browser for replay (Puppeteer)...');
14
16
  const browser = await puppeteer.default.launch({
15
17
  headless,
18
+ executablePath,
16
19
  userDataDir,
17
20
  defaultViewport: DEFAULT_VIEWPORT,
18
21
  args: [`--window-size=${DEFAULT_VIEWPORT.width},${DEFAULT_VIEWPORT.height}`],