[object Object]

← back to Pattern Vault

capture-session: auto-detect login (poll for logged-in marker, no keypress) — works via ! prefix, 5-min timeout

bd366e3a0660b60c343aba901b2a70037af4ec1a · 2026-07-09 19:01:30 -0700 · Steve

Files touched

Diff

commit bd366e3a0660b60c343aba901b2a70037af4ec1a
Author: Steve <steve@designerwallcoverings.com>
Date:   Thu Jul 9 19:01:30 2026 -0700

    capture-session: auto-detect login (poll for logged-in marker, no keypress) — works via ! prefix, 5-min timeout
---
 wpb-uploaders/capture-session.js | 53 +++++++++++++++++++++++++++++-----------
 1 file changed, 39 insertions(+), 14 deletions(-)

diff --git a/wpb-uploaders/capture-session.js b/wpb-uploaders/capture-session.js
index 3565c67..4c1d8f9 100644
--- a/wpb-uploaders/capture-session.js
+++ b/wpb-uploaders/capture-session.js
@@ -1,16 +1,16 @@
 #!/usr/bin/env node
 /*
  * Capture a logged-in session (Playwright storageState) for any WPB uploader platform.
- * Opens a HEADED browser to the platform's login/home page; YOU log in manually (SSO, 2FA,
- * CAPTCHA — all fine, it's a real window); then press Enter in this terminal to save the session.
+ * Opens a HEADED browser to the platform's login page; YOU log in manually (SSO, 2FA, CAPTCHA
+ * all fine). It AUTO-DETECTS when you're logged in and saves the session — no keypress needed,
+ * so it works even when launched via the `!` prefix. Times out after 5 minutes.
  *
- *   node capture-session.js <platform>       # etsy* | creativemarket | redbubble | society6 | patternbank
+ *   node capture-session.js <creativemarket|redbubble|society6|patternbank>
  *
- * (*Etsy uses OAuth, not a browser session — for Etsy, paste an ETSY_OAUTH_TOKEN instead.)
+ * (Etsy uses OAuth, not a browser session — paste an ETSY_OAUTH_TOKEN instead.)
  * Writes sessions/<platform>-state.json. Nothing is uploaded — this only captures the login.
- * MUST be run by Steve in an interactive terminal (it waits for your Enter).
  */
-const path = require('path'), fs = require('fs'), readline = require('readline');
+const path = require('path'), fs = require('fs');
 const { chromium } = require('/Users/macstudio3/.npm-global/lib/node_modules/playwright');
 
 const platform = process.argv[2];
@@ -24,23 +24,48 @@ const cfg = require(path.join(__dirname, 'platforms', `${platform}.js`));
 const OUT = cfg.sessionFile;
 const LOGIN = { creativemarket: 'https://creativemarket.com/login', redbubble: 'https://www.redbubble.com/auth/login',
   society6: 'https://society6.com/login', patternbank: 'https://patternbank.com/login' }[platform];
+const sleep = ms => new Promise(r => setTimeout(r, ms));
 
-const ask = q => new Promise(r => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question(q, a => { rl.close(); r(a); }); });
+// logged-in heuristic: off the login/auth URL AND a logged-in marker is present.
+async function isLoggedIn(pg) {
+  const u = pg.url();
+  if (/\/login|\/signin|\/auth\/login|\/sign_in/i.test(u)) return false;
+  const marker = await pg.$('a[href*="logout" i], a[href*="sign_out" i], button:has-text("Log out"), button:has-text("Sign out"), a[href*="/account" i], [aria-label*="account" i], img[alt*="avatar" i], [class*="avatar" i]').catch(() => null);
+  return !!marker;
+}
 
 (async () => {
   fs.mkdirSync(path.dirname(OUT), { recursive: true });
   console.log(`\n🔐 Capturing ${cfg.name} session → ${OUT}`);
-  console.log(`   A browser window will open at ${LOGIN}`);
-  console.log(`   Log in fully (SSO / 2FA / CAPTCHA all OK), reach your dashboard, THEN come back here.\n`);
+  console.log(`   A browser window is opening at ${LOGIN}`);
+  console.log(`   Log in fully (SSO / 2FA / CAPTCHA all OK) and reach your dashboard.`);
+  console.log(`   I'll auto-detect the login and save it — no keypress needed.\n`);
+
   const b = await chromium.launch({ headless: false, args: ['--start-maximized'] });
   const ctx = await b.newContext({ viewport: null });
   const pg = await ctx.newPage();
   await pg.goto(LOGIN, { waitUntil: 'domcontentloaded', timeout: 60000 }).catch(() => {});
 
-  await ask('   ▶ When you are fully logged in, press ENTER here to save the session… ');
-  await ctx.storageState({ path: OUT });
+  const deadline = Date.now() + 5 * 60 * 1000; // 5 min
+  let stable = 0, saved = false;
+  while (Date.now() < deadline) {
+    await sleep(3000);
+    let ok = false;
+    try { ok = await isLoggedIn(pg); } catch { /* page navigating */ }
+    if (ok) { stable++; } else { stable = 0; }
+    if (stable >= 2) { // logged-in confirmed across two checks
+      await ctx.storageState({ path: OUT });
+      saved = true;
+      break;
+    }
+    if (Math.round((deadline - Date.now()) / 1000) % 30 === 0) console.log(`   …waiting for login (${Math.round((deadline - Date.now()) / 1000)}s left)`);
+  }
   await b.close();
-  const ok = fs.existsSync(OUT) && fs.statSync(OUT).size > 200;
-  console.log(ok ? `\n✅ Saved ${cfg.name} session (${fs.statSync(OUT).size} bytes) → ${OUT}` : `\n⚠️ Session file looks empty — did the login complete?`);
-  console.log(`   Next: verify with  node run.js ${platform} <manifest>  (still fail-loud + <=4/day).`);
+
+  if (saved && fs.existsSync(OUT) && fs.statSync(OUT).size > 200) {
+    console.log(`\n✅ Saved ${cfg.name} session (${fs.statSync(OUT).size} bytes) → ${OUT}`);
+    console.log(`   Tell Claude "captured ${platform}" and it will calibrate the uploader (still fail-loud + <=4/day).`);
+  } else {
+    console.log(`\n⚠️  No login detected within 5 min — session NOT saved. Re-run and complete the login.`);
+  }
 })();

← b68bdf0 gitignore: uploader sessions + ARMED runtime flags  ·  back to Pattern Vault  ·  capture-session: robust login detect (no-password-field + do 6800cc2 →