@humanjs/generator 0.1.0 → 0.2.0

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.
@@ -4,8 +4,8 @@
4
4
  <meta charset="utf-8" />
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
6
  <title>HumanJS Generator</title>
7
- <script type="module" crossorigin src="./assets/index-DAIzvz09.js"></script>
8
- <link rel="stylesheet" crossorigin href="./assets/index-BA_iJGjh.css">
7
+ <script type="module" crossorigin src="./assets/index-pW8ERgI7.js"></script>
8
+ <link rel="stylesheet" crossorigin href="./assets/index-BhTAwphH.css">
9
9
  </head>
10
10
  <body>
11
11
  <div id="root"></div>
package/dist/index.cjs CHANGED
@@ -3,10 +3,10 @@
3
3
 
4
4
  var promises = require('fs/promises');
5
5
  var path = require('path');
6
+ var playwright$1 = require('@humanjs/playwright');
6
7
  var playwright = require('playwright');
7
8
  var fs = require('fs');
8
9
  var url = require('url');
9
- var playwright$1 = require('@humanjs/playwright');
10
10
  var child_process = require('child_process');
11
11
  var http = require('http');
12
12
  var ws = require('ws');
@@ -383,6 +383,8 @@ async function start(targetUrl) {
383
383
  const server = await createDashboardServer();
384
384
  const store = new TimelineStore();
385
385
  let personality = "careful";
386
+ let browser;
387
+ let replayController = null;
386
388
  const previewCode = () => generateCode(store.list(), { format: "spec", personality });
387
389
  const stateMessage = () => ({
388
390
  type: "state",
@@ -399,6 +401,49 @@ async function start(targetUrl) {
399
401
  server.broadcast({ type: "exported", path: path$1 });
400
402
  console.log(` Exported ${format === "spec" ? "test" : "script"} \u2192 ${path$1}`);
401
403
  };
404
+ const runReplay = async () => {
405
+ if (replayController) return;
406
+ const steps = store.list();
407
+ if (steps.length === 0) return;
408
+ const stepId = (index) => steps[index]?.id;
409
+ const controller = new AbortController();
410
+ replayController = controller;
411
+ server.broadcast({ type: "replayStarted" });
412
+ const startedAt = Date.now();
413
+ let context2;
414
+ try {
415
+ context2 = await browser.newContext({ viewport: null });
416
+ const page2 = await context2.newPage();
417
+ const result = await playwright$1.replayTimeline(page2, steps, {
418
+ personality,
419
+ signal: controller.signal,
420
+ onStep: ({ index, status, error }) => {
421
+ const id = stepId(index);
422
+ if (id) server.broadcast({ type: "replayStep", id, status, ...error ? { error } : {} });
423
+ }
424
+ });
425
+ const failedStepId = result.failedIndex === void 0 ? void 0 : stepId(result.failedIndex);
426
+ server.broadcast({
427
+ type: "replayDone",
428
+ status: result.status,
429
+ durationMs: result.durationMs,
430
+ ...failedStepId ? { failedStepId } : {}
431
+ });
432
+ } catch (cause) {
433
+ const aborted = cause instanceof Error && cause.name === "AbortError";
434
+ server.broadcast({
435
+ type: "replayDone",
436
+ status: "fail",
437
+ aborted,
438
+ durationMs: Date.now() - startedAt,
439
+ ...aborted ? {} : { error: cause instanceof Error ? cause.message : String(cause) }
440
+ });
441
+ } finally {
442
+ await context2?.close().catch(() => {
443
+ });
444
+ replayController = null;
445
+ }
446
+ };
402
447
  const handleCommand = (message) => {
403
448
  switch (message.type) {
404
449
  case "delete":
@@ -424,6 +469,14 @@ async function start(targetUrl) {
424
469
  case "export":
425
470
  void exportTimeline(message.format);
426
471
  return;
472
+ // export replies with `exported`, not a state refresh
473
+ case "replay":
474
+ void runReplay();
475
+ return;
476
+ // replay streams its own `replay*` messages
477
+ case "cancelReplay":
478
+ replayController?.abort();
479
+ return;
427
480
  }
428
481
  broadcastState();
429
482
  };
@@ -436,7 +489,7 @@ async function start(targetUrl) {
436
489
  }
437
490
  });
438
491
  });
439
- const browser = await playwright.chromium.launch({ headless: false });
492
+ browser = await playwright.chromium.launch({ headless: false });
440
493
  const context = await browser.newContext({ viewport: null });
441
494
  await attachCapture(context, (action) => {
442
495
  store.append({
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/capture/attach.ts","../src/export.ts","../src/open-browser.ts","../src/dashboard-placeholder.ts","../src/server.ts","../src/timeline-store.ts","../src/run.ts","../src/url.ts","../src/index.ts"],"names":["dirname","fileURLToPath","readFileSync","join","generateHumanJS","generatePlaywrightTest","spawn","http","createServer","WebSocketServer","resolve","WebSocket","normalize","sep","stat","createReadStream","step","path","writeFile","chromium"],"mappings":"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","file":"index.cjs","sourcesContent":["import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { BrowserContext } from 'playwright';\nimport type { CapturedAction } from './types';\n\nlet injectedSource: string | null = null;\n\n/** The bundled in-page recorder (dist/injected.js), read once and cached. */\nfunction loadInjectedScript(): string {\n if (injectedSource === null) {\n const dir = dirname(fileURLToPath(import.meta.url));\n injectedSource = readFileSync(join(dir, 'injected.js'), 'utf8');\n }\n return injectedSource;\n}\n\nfunction isCapturedAction(value: unknown): value is CapturedAction {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { type?: unknown }).type === 'string' &&\n typeof (value as { params?: unknown }).params === 'object'\n );\n}\n\n/**\n * A main-frame navigation within this window of the last in-page gesture\n * (pointerdown / keydown) is treated as a *consequence* of that gesture — a\n * clicked link, a submitted form, a typed search that updates the URL. The\n * gesture is already recorded and reproduces the navigation on replay, so we\n * don't also record a `goto` (which would navigate a second time). Navigations\n * with no recent gesture — the initial load, or the user typing a URL in the\n * address bar — are still captured.\n */\nconst NAV_AFTER_GESTURE_MS = 2500;\n\n/** Sentinel action the recorder pings on each gesture; bumps the clock, never a step. */\nconst NAV_INTENT = '__navIntent';\n\n/**\n * Attach interaction capture to a browser context: expose the `__humanjsEmit`\n * binding the recorder calls, inject the recorder into every page, and report\n * main-frame navigations as `goto` actions. Each captured action is handed to\n * `onAction` in the order it occurred.\n *\n * Must be called before the first page is created so the init script and\n * binding apply to it.\n */\nexport async function attachCapture(\n context: BrowserContext,\n onAction: (action: CapturedAction) => void,\n): Promise<void> {\n // Timestamp of the last in-page gesture (or recorded action). Used to tell a\n // user-driven navigation from one caused by an interaction we already logged.\n let lastGestureAt = 0;\n\n await context.exposeBinding('__humanjsEmit', (_source, action: unknown) => {\n if (!isCapturedAction(action)) return;\n lastGestureAt = Date.now();\n // Gesture pings only advance the clock — they aren't timeline steps.\n if (action.type === NAV_INTENT) return;\n onAction(action);\n });\n await context.addInitScript({ content: loadInjectedScript() });\n\n // Main-frame navigations become `goto` steps. Dedupe consecutive identical\n // URLs (a single navigation can fire more than once) and ignore blanks.\n let lastUrl = '';\n context.on('page', (page) => {\n page.on('framenavigated', (frame) => {\n if (frame !== page.mainFrame()) return;\n const url = frame.url();\n if (!url || url === 'about:blank' || url === lastUrl) return;\n lastUrl = url;\n // Skip navigations that are the consequence of a just-recorded gesture\n // (clicked link, form submit, search-as-you-type) — replaying the gesture\n // navigates on its own; a `goto` here would double-navigate.\n if (Date.now() - lastGestureAt < NAV_AFTER_GESTURE_MS) return;\n onAction({ type: 'goto', params: { url } });\n });\n });\n}\n","import {\n generateHumanJS,\n generatePlaywrightTest,\n type PlaywrightTestOptions,\n type Timeline,\n type TimelineEvent,\n} from '@humanjs/playwright';\n\nexport type ExportFormat = 'spec' | 'script';\n\nexport interface ExportOptions {\n /** `spec` → a `@humanjs/playwright/test` spec; `script` → a standalone HumanJS script. */\n readonly format: ExportFormat;\n readonly personality?: string;\n readonly seed?: string | null;\n readonly speed?: string;\n /** Test title (spec only); defaults to the recording name. */\n readonly title?: string;\n /** Extra `generatePlaywrightTest` options (steps / baseUrl / keepSleeps). */\n readonly playwright?: PlaywrightTestOptions;\n}\n\n// A `type`/`paste` step flagged secret (`params.secret = 'ENV_NAME'`) exports\n// its value as `process.env.ENV_NAME` instead of a literal. We can't make the\n// codegen emit a raw identifier directly, so we route the value through a\n// sentinel string and unquote it in the generated output.\nconst ENV_PREFIX = '__HUMANJS_ENV__';\nconst ENV_SENTINEL_RE = /['\"]__HUMANJS_ENV__([A-Za-z0-9_]+)__['\"]/g;\n\nfunction sanitizeEnvName(name: string): string {\n return name.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/** Replace the value of secret-flagged type/paste steps with an env sentinel. */\nfunction applySecrets(events: readonly TimelineEvent[]): TimelineEvent[] {\n return events.map((event) => {\n const secret = event.params.secret;\n if ((event.type === 'type' || event.type === 'paste') && typeof secret === 'string' && secret) {\n return { ...event, inputValue: `${ENV_PREFIX}${sanitizeEnvName(secret)}__` };\n }\n return event;\n });\n}\n\nfunction restoreSecrets(code: string): string {\n return code.replace(ENV_SENTINEL_RE, 'process.env.$1');\n}\n\n/** Pick the export format from an output filename. `.spec.ts` / `.test.ts` → spec. */\nexport function formatFromFilename(filename: string): ExportFormat {\n return /\\.(spec|test)\\.[cm]?tsx?$/i.test(filename) ? 'spec' : 'script';\n}\n\n/**\n * Generate code from a captured timeline. Wraps the events in a `Timeline` with\n * the chosen personality/seed/speed and runs the shared `@humanjs/playwright`\n * codegen, then substitutes `process.env.*` for any secret-flagged values.\n */\nexport function generateCode(events: readonly TimelineEvent[], options: ExportOptions): string {\n const timeline: Timeline = {\n version: 1,\n name: options.title,\n personality: options.personality ?? 'careful',\n seed: options.seed ?? null,\n speed: options.speed ?? 'human',\n durationMs: 0,\n events: applySecrets(events),\n };\n const code =\n options.format === 'script'\n ? generateHumanJS(timeline)\n : generatePlaywrightTest(timeline, options.playwright);\n return restoreSecrets(code);\n}\n","import { spawn } from 'node:child_process';\n\n/**\n * Best-effort: open `url` in the user's default browser for the dashboard.\n *\n * Uses the platform's native opener (`open` / `start` / `xdg-open`). Failures\n * are swallowed — the CLI always prints the URL too, so the user can open it\n * by hand if no opener is available (headless box, locked-down environment).\n */\nexport function openInBrowser(url: string): void {\n let command: string;\n let args: string[];\n if (process.platform === 'darwin') {\n command = 'open';\n args = [url];\n } else if (process.platform === 'win32') {\n command = 'cmd';\n args = ['/c', 'start', '', url];\n } else {\n command = 'xdg-open';\n args = [url];\n }\n\n try {\n const child = spawn(command, args, {\n stdio: 'ignore',\n detached: true,\n });\n child.on('error', () => {\n // No opener on this platform / not on PATH — the printed URL is the fallback.\n });\n child.unref();\n } catch {\n // spawn threw synchronously (rare) — ignore; the URL is printed regardless.\n }\n}\n","/**\n * Fallback dashboard served when the built Vite + React editor isn't present in\n * `dist/dashboard/`. It's a read-only view of the live timeline + code preview\n * over the same `state` protocol, with Export buttons — enough to exercise the\n * pipeline without the full editor.\n *\n * Single self-contained string — no external assets, no nested backticks.\n */\nexport const PLACEHOLDER_HTML = `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>HumanJS Generator</title>\n <style>\n :root { color-scheme: dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; display: grid; place-items: center;\n background: #060604; color: #f0ece5;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n }\n main { width: min(620px, 92vw); padding: 2rem; }\n h1 { font-size: 1.25rem; margin: 0 0 0.25rem; letter-spacing: 0.02em; }\n .accent { color: #f5a55c; }\n .muted { color: #8a857c; }\n .row { display: flex; align-items: center; gap: 0.6rem; margin: 1.25rem 0 0.5rem; }\n .dot { width: 9px; height: 9px; border-radius: 50%; background: #555; transition: background 0.2s; }\n .dot.on { background: #f5a55c; box-shadow: 0 0 10px #f5a55c; }\n .target { margin-top: 0.5rem; word-break: break-all; }\n ol { margin: 1rem 0 0; padding-left: 0; list-style: none; font-size: 0.85rem; }\n ol li { padding: 0.35rem 0; border-top: 1px solid #1a1a1a; display: flex; gap: 0.6rem; }\n ol li .kind { color: #f5a55c; min-width: 5.5rem; }\n ol li .detail { color: #c9c9c9; word-break: break-all; }\n .empty { color: #6a6a6a; }\n .bar { display: flex; align-items: center; gap: 0.6rem; margin-top: 1.5rem; flex-wrap: wrap; }\n button {\n font: inherit; font-size: 0.8rem; color: #060604; background: #f5a55c;\n border: 0; border-radius: 6px; padding: 0.45rem 0.8rem; cursor: pointer;\n }\n button:hover { background: #ffb87a; }\n .saved { color: #f5a55c; font-size: 0.8rem; }\n pre.code {\n margin-top: 1rem; padding: 1rem; background: #0c0b0a; border: 1px solid #1a1a1a;\n border-radius: 8px; overflow: auto; max-height: 320px; font-size: 0.8rem;\n color: #cfcfcf; white-space: pre; line-height: 1.5;\n }\n footer { margin-top: 1.75rem; font-size: 0.8rem; }\n </style>\n </head>\n <body>\n <main>\n <h1><span class=\"accent\">HumanJS</span> Generator</h1>\n <p class=\"muted\">Local dashboard &mdash; read-only fallback view.</p>\n <div class=\"row\"><span id=\"dot\" class=\"dot\"></span><span id=\"status\">connecting&hellip;</span></div>\n <p class=\"muted target\" id=\"target\"></p>\n <ol id=\"events\"><li class=\"empty\">No steps captured yet &mdash; interact with the Chromium window.</li></ol>\n <div class=\"bar\">\n <button id=\"exp-spec\" type=\"button\">Export .spec.ts</button>\n <button id=\"exp-script\" type=\"button\">Export .ts</button>\n <span id=\"saved\" class=\"saved\"></span>\n </div>\n <pre class=\"code\" id=\"code\"></pre>\n <footer class=\"muted\">The full editor (drag, relabel, selector picker, assertions) loads here once built.</footer>\n </main>\n <script>\n var dot = document.getElementById('dot');\n var status = document.getElementById('status');\n var target = document.getElementById('target');\n var events = document.getElementById('events');\n var code = document.getElementById('code');\n var saved = document.getElementById('saved');\n\n function detailFor(ev) {\n var p = ev.params || {};\n if (typeof ev.inputValue === 'string') return (p.target || '') + ' = ' + JSON.stringify(ev.inputValue);\n if (p.from) return p.from + ' -> ' + p.to;\n if (p.key) return String(p.key);\n if (p.url) return String(p.url);\n if (p.kind) return p.kind + (p.target ? ' ' + p.target : '') + (p.value ? ' ' + JSON.stringify(p.value) : '');\n if (typeof p.values !== 'undefined') return (p.target || '') + ' -> ' + JSON.stringify(p.values);\n return String(p.target || '');\n }\n\n function renderSteps(steps) {\n events.innerHTML = '';\n if (!steps.length) {\n events.innerHTML = '<li class=\"empty\">No steps captured yet &mdash; interact with the Chromium window.</li>';\n return;\n }\n for (var i = 0; i < steps.length; i++) {\n var ev = steps[i];\n var li = document.createElement('li');\n var kind = document.createElement('span');\n kind.className = 'kind';\n kind.textContent = ev.type;\n var detail = document.createElement('span');\n detail.className = 'detail';\n detail.textContent = detailFor(ev);\n li.appendChild(kind);\n li.appendChild(detail);\n events.appendChild(li);\n }\n }\n\n var ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host);\n ws.onopen = function () { dot.classList.add('on'); status.textContent = 'connected'; };\n ws.onclose = function () { dot.classList.remove('on'); status.textContent = 'disconnected'; };\n ws.onmessage = function (event) {\n try {\n var msg = JSON.parse(event.data);\n if (msg.type === 'state') {\n target.textContent = 'Recording: ' + msg.targetUrl;\n renderSteps(msg.steps || []);\n code.textContent = msg.code || '';\n } else if (msg.type === 'exported') {\n saved.textContent = 'Saved ' + msg.path;\n }\n } catch (_) { /* ignore non-JSON frames */ }\n };\n\n function requestExport(format) {\n saved.textContent = '';\n ws.send(JSON.stringify({ type: 'export', format: format }));\n }\n document.getElementById('exp-spec').onclick = function () { requestExport('spec'); };\n document.getElementById('exp-script').onclick = function () { requestExport('script'); };\n </script>\n </body>\n</html>\n`;\n","import { createReadStream } from 'node:fs';\nimport { stat } from 'node:fs/promises';\nimport { createServer, type ServerResponse } from 'node:http';\nimport { dirname, join, normalize, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { WebSocket, WebSocketServer } from 'ws';\nimport { PLACEHOLDER_HTML } from './dashboard-placeholder';\nimport type { ServerMessage } from './protocol';\n\nconst MODULE_DIR = dirname(fileURLToPath(import.meta.url));\n// The built Vite dashboard lands in dist/dashboard/ (milestone 5). Until it\n// exists, the root path falls back to the embedded placeholder.\nconst DASHBOARD_DIR = join(MODULE_DIR, 'dashboard');\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff2': 'font/woff2',\n};\n\nfunction contentTypeFor(filePath: string): string {\n const dot = filePath.lastIndexOf('.');\n const ext = dot === -1 ? '' : filePath.slice(dot).toLowerCase();\n return CONTENT_TYPES[ext] ?? 'application/octet-stream';\n}\n\nexport interface DashboardServer {\n /** Loopback URL the dashboard is served from, e.g. `http://127.0.0.1:53124`. */\n readonly url: string;\n /** Underlying WebSocket server — attach `connection` handlers for the live channel. */\n readonly wss: WebSocketServer;\n /** Send a message to every connected dashboard client. */\n broadcast(message: ServerMessage): void;\n close(): Promise<void>;\n}\n\nexport interface DashboardServerOptions {\n /** Directory of built dashboard assets to serve. Defaults to `dist/dashboard`. */\n readonly dashboardDir?: string;\n}\n\n/**\n * Start the local dashboard server, bound to loopback on an OS-assigned port.\n * Serves the built dashboard (or the placeholder) over HTTP and upgrades\n * WebSocket connections on the same port.\n */\nexport async function createDashboardServer(\n options: DashboardServerOptions = {},\n): Promise<DashboardServer> {\n const dashboardDir = options.dashboardDir ?? DASHBOARD_DIR;\n const http = createServer((req, res) => {\n void serve(dashboardDir, req.url ?? '/', res);\n });\n const wss = new WebSocketServer({ server: http });\n\n await new Promise<void>((resolve, reject) => {\n http.once('error', reject);\n // Loopback only — this is a local dev tool, never exposed to the network.\n http.listen(0, '127.0.0.1', resolve);\n });\n\n const address = http.address();\n if (address === null || typeof address === 'string') {\n throw new Error('dashboard server failed to bind to a TCP port');\n }\n const url = `http://127.0.0.1:${address.port}`;\n\n return {\n url,\n wss,\n broadcast(message) {\n const data = JSON.stringify(message);\n for (const client of wss.clients) {\n if (client.readyState === WebSocket.OPEN) client.send(data);\n }\n },\n close() {\n return new Promise<void>((resolve) => {\n for (const client of wss.clients) client.terminate();\n wss.close(() => http.close(() => resolve()));\n });\n },\n };\n}\n\nasync function serve(dashboardDir: string, rawUrl: string, res: ServerResponse): Promise<void> {\n const pathname = rawUrl.split('?')[0] ?? '/';\n const relative = pathname === '/' ? 'index.html' : pathname.replace(/^\\/+/, '');\n const filePath = normalize(join(dashboardDir, relative));\n\n // Path-traversal guard: the resolved file must stay inside the dashboard dir.\n if (filePath !== dashboardDir && !filePath.startsWith(dashboardDir + sep)) {\n res.writeHead(403).end('Forbidden');\n return;\n }\n\n try {\n const info = await stat(filePath);\n if (info.isFile()) {\n res.writeHead(200, { 'content-type': contentTypeFor(filePath) });\n createReadStream(filePath).pipe(res);\n return;\n }\n } catch {\n // No such file — fall through to the placeholder / 404.\n }\n\n // No built dashboard yet (or an unknown path): serve the placeholder at root.\n if (pathname === '/') {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(PLACEHOLDER_HTML);\n return;\n }\n res.writeHead(404).end('Not found');\n}\n","import type { TimelineEvent } from '@humanjs/playwright';\nimport type { AssertKind, Step, StepPatch } from './protocol';\n\n/**\n * The canonical, editable timeline held by the CLI. Capture appends events;\n * the dashboard issues edit commands (delete / move / update / addAssert) that\n * mutate this list. Every step carries a stable id so edits can target it.\n *\n * Steps are valid `TimelineEvent`s (plus the id, which the codegen ignores), so\n * the list can be passed straight to `generateCode`.\n */\nexport class TimelineStore {\n #steps: Step[] = [];\n #counter = 0;\n\n /** The current steps, in order. */\n list(): readonly Step[] {\n return this.#steps;\n }\n\n /** Append a freshly captured event, assigning it an id. */\n append(event: TimelineEvent): void {\n this.#steps.push({ ...event, id: this.#nextId() });\n }\n\n delete(id: string): void {\n this.#steps = this.#steps.filter((step) => step.id !== id);\n }\n\n /** Move a step to a new index (clamped to the list bounds). */\n move(id: string, toIndex: number): void {\n const from = this.#steps.findIndex((step) => step.id === id);\n if (from === -1) return;\n const [step] = this.#steps.splice(from, 1);\n if (!step) return;\n const clamped = Math.max(0, Math.min(toIndex, this.#steps.length));\n this.#steps.splice(clamped, 0, step);\n }\n\n /** Reorder the timeline to match the given id order (a full-order set from a drag). */\n reorder(ids: readonly string[]): void {\n const byId = new Map(this.#steps.map((step) => [step.id, step]));\n const next: Step[] = [];\n for (const id of ids) {\n const step = byId.get(id);\n if (step) {\n next.push(step);\n byId.delete(id);\n }\n }\n // Keep any steps the client didn't mention (e.g. captured mid-drag) at the end.\n for (const step of this.#steps) if (byId.has(step.id)) next.push(step);\n this.#steps = next;\n }\n\n update(id: string, patch: StepPatch): void {\n this.#steps = this.#steps.map((step) => (step.id === id ? applyPatch(step, patch) : step));\n }\n\n /** Insert an assertion step after `afterId` (or at the end when null). */\n addAssert(afterId: string | null, kind: AssertKind, target?: string, value?: string): void {\n const params: Record<string, unknown> = { kind };\n if (target !== undefined) params.target = target;\n if (value !== undefined) params.value = value;\n const step: Step = { id: this.#nextId(), type: 'assert', params, tMs: 0, durationMs: 0 };\n\n const index = afterId === null ? this.#steps.length - 1 : this.#findIndex(afterId);\n this.#steps.splice(index + 1, 0, step);\n }\n\n #findIndex(id: string): number {\n return this.#steps.findIndex((step) => step.id === id);\n }\n\n #nextId(): string {\n this.#counter += 1;\n return `s${this.#counter}`;\n }\n}\n\nfunction applyPatch(step: Step, patch: StepPatch): Step {\n const params: Record<string, unknown> = { ...step.params };\n if (patch.target !== undefined) params.target = patch.target;\n if (patch.label !== undefined) params.label = patch.label;\n if (patch.secret !== undefined) {\n if (patch.secret === null) delete params.secret;\n else params.secret = patch.secret;\n }\n return {\n ...step,\n params,\n ...(patch.inputValue !== undefined ? { inputValue: patch.inputValue } : {}),\n };\n}\n","import { writeFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { chromium } from 'playwright';\nimport { attachCapture } from './capture/attach';\nimport { type ExportFormat, generateCode } from './export';\nimport { openInBrowser } from './open-browser';\nimport type { ClientMessage, ServerMessage } from './protocol';\nimport { createDashboardServer } from './server';\nimport { TimelineStore } from './timeline-store';\n\n/** Built-in personalities the dashboard switcher offers. */\nconst PERSONALITIES = ['careful', 'fast', 'distracted', 'precise'] as const;\n\n/** Default output filenames per format, written to the CLI's working directory. */\nconst OUTPUT_FILE: Record<ExportFormat, string> = {\n spec: 'humanjs-recording.spec.ts',\n script: 'humanjs-recording.ts',\n};\n\n/**\n * Launch a recording session: start the local dashboard, open a real Chromium\n * window at `targetUrl`, capture interactions into an editable timeline, and let\n * the dashboard curate it (delete / reorder / relabel / edit / pick selectors /\n * add assertions / mark secrets / switch personality) with a live code preview,\n * then export a `.spec.ts` / `.ts`. Runs until the browser closes or the\n * process is interrupted.\n */\nexport async function start(targetUrl: string): Promise<void> {\n const server = await createDashboardServer();\n const store = new TimelineStore();\n let personality = 'careful';\n\n const previewCode = (): string => generateCode(store.list(), { format: 'spec', personality });\n\n const stateMessage = (): ServerMessage => ({\n type: 'state',\n targetUrl,\n steps: store.list(),\n personality,\n personalities: [...PERSONALITIES],\n code: previewCode(),\n });\n\n const broadcastState = (): void => server.broadcast(stateMessage());\n\n const exportTimeline = async (format: ExportFormat): Promise<void> => {\n const path = resolve(process.cwd(), OUTPUT_FILE[format]);\n await writeFile(path, generateCode(store.list(), { format, personality }), 'utf8');\n server.broadcast({ type: 'exported', path });\n console.log(` Exported ${format === 'spec' ? 'test' : 'script'} → ${path}`);\n };\n\n const handleCommand = (message: ClientMessage): void => {\n switch (message.type) {\n case 'delete':\n store.delete(message.id);\n break;\n case 'move':\n store.move(message.id, message.toIndex);\n break;\n case 'reorder':\n store.reorder(message.ids);\n break;\n case 'update':\n store.update(message.id, message.patch);\n break;\n case 'addAssert':\n store.addAssert(message.afterId, message.kind, message.target, message.value);\n break;\n case 'setPersonality':\n if ((PERSONALITIES as readonly string[]).includes(message.personality)) {\n personality = message.personality;\n }\n break;\n case 'export':\n void exportTimeline(message.format);\n return; // export replies with `exported`, not a state refresh\n }\n broadcastState();\n };\n\n server.wss.on('connection', (socket) => {\n socket.send(JSON.stringify(stateMessage()));\n socket.on('message', (data) => {\n try {\n handleCommand(JSON.parse(data.toString()) as ClientMessage);\n } catch {\n // Ignore malformed frames.\n }\n });\n });\n\n const browser = await chromium.launch({ headless: false });\n // `viewport: null` lets the page fill the real window — a person drives this.\n const context = await browser.newContext({ viewport: null });\n // Attach capture before the first page exists so the init script applies.\n await attachCapture(context, (action) => {\n store.append({\n type: action.type,\n params: action.params,\n tMs: 0,\n durationMs: 0,\n ...(action.inputValue === undefined ? {} : { inputValue: action.inputValue }),\n });\n broadcastState();\n });\n const page = await context.newPage();\n\n let closing = false;\n const shutdown = async (): Promise<void> => {\n if (closing) return;\n closing = true;\n await browser.close().catch(() => {});\n await server.close();\n process.exit(0);\n };\n process.on('SIGINT', () => void shutdown());\n process.on('SIGTERM', () => void shutdown());\n browser.on('disconnected', () => void shutdown());\n\n openInBrowser(server.url);\n\n try {\n await page.goto(targetUrl);\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n console.warn(` Couldn't load ${targetUrl}: ${reason}`);\n console.warn(' Navigate manually in the Chromium window.');\n }\n\n console.log('');\n console.log(' HumanJS Generator');\n console.log(` Dashboard: ${server.url}`);\n console.log(` Recording: ${targetUrl}`);\n console.log('');\n console.log(' Interact with the Chromium window. Close it or press Ctrl+C to stop.');\n console.log('');\n}\n","/**\n * Normalize a user-supplied target into a navigable URL.\n *\n * A bare host (`example.com`) gets an `https://` scheme so `page.goto` accepts\n * it; loopback hosts default to `http://` since local dev servers rarely speak\n * TLS. An explicit scheme is always left untouched.\n */\nexport function normalizeUrl(input: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(input)) return input;\n const isLoopback = /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\])(:|\\/|$)/i.test(input);\n return `${isLoopback ? 'http' : 'https'}://${input}`;\n}\n","/**\n * @humanjs/generator — visual recorder for HumanJS.\n *\n * `npx @humanjs/generator <url>` launches a real Chromium window, records the\n * clicks / typing / scrolls / navigation you perform (with role- and\n * accessible-name-first selectors), shows a live, editable timeline in a local\n * dashboard, and exports a clean humanized Playwright test.\n *\n * Milestone 2: the CLI launches the browser and the local dashboard server and\n * wires the WebSocket channel. Capture, the editor UI, and export land across\n * the following milestones (see ROADMAP.md).\n */\n\nimport { start } from './run';\nimport { normalizeUrl } from './url';\n\nconst USAGE = 'Usage: npx @humanjs/generator <url>';\n\nasync function main(argv: readonly string[]): Promise<void> {\n const [rawUrl] = argv;\n\n if (!rawUrl || rawUrl === '--help' || rawUrl === '-h') {\n // No URL (or an explicit help flag): print usage. Exit 0 for --help, 1 for\n // a missing-argument error.\n console.log(USAGE);\n process.exit(rawUrl ? 0 : 1);\n }\n\n await start(normalizeUrl(rawUrl));\n}\n\nmain(process.argv.slice(2)).catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exit(1);\n});\n"]}
1
+ {"version":3,"sources":["../src/capture/attach.ts","../src/export.ts","../src/open-browser.ts","../src/dashboard-placeholder.ts","../src/server.ts","../src/timeline-store.ts","../src/run.ts","../src/url.ts","../src/index.ts"],"names":["dirname","fileURLToPath","readFileSync","join","generateHumanJS","generatePlaywrightTest","spawn","http","createServer","WebSocketServer","resolve","WebSocket","normalize","sep","stat","createReadStream","step","path","writeFile","context","page","replayTimeline","chromium"],"mappings":"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","file":"index.cjs","sourcesContent":["import { readFileSync } from 'node:fs';\nimport { dirname, join } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport type { BrowserContext } from 'playwright';\nimport type { CapturedAction } from './types';\n\nlet injectedSource: string | null = null;\n\n/** The bundled in-page recorder (dist/injected.js), read once and cached. */\nfunction loadInjectedScript(): string {\n if (injectedSource === null) {\n const dir = dirname(fileURLToPath(import.meta.url));\n injectedSource = readFileSync(join(dir, 'injected.js'), 'utf8');\n }\n return injectedSource;\n}\n\nfunction isCapturedAction(value: unknown): value is CapturedAction {\n return (\n typeof value === 'object' &&\n value !== null &&\n typeof (value as { type?: unknown }).type === 'string' &&\n typeof (value as { params?: unknown }).params === 'object'\n );\n}\n\n/**\n * A main-frame navigation within this window of the last in-page gesture\n * (pointerdown / keydown) is treated as a *consequence* of that gesture — a\n * clicked link, a submitted form, a typed search that updates the URL. The\n * gesture is already recorded and reproduces the navigation on replay, so we\n * don't also record a `goto` (which would navigate a second time). Navigations\n * with no recent gesture — the initial load, or the user typing a URL in the\n * address bar — are still captured.\n */\nconst NAV_AFTER_GESTURE_MS = 2500;\n\n/** Sentinel action the recorder pings on each gesture; bumps the clock, never a step. */\nconst NAV_INTENT = '__navIntent';\n\n/**\n * Attach interaction capture to a browser context: expose the `__humanjsEmit`\n * binding the recorder calls, inject the recorder into every page, and report\n * main-frame navigations as `goto` actions. Each captured action is handed to\n * `onAction` in the order it occurred.\n *\n * Must be called before the first page is created so the init script and\n * binding apply to it.\n */\nexport async function attachCapture(\n context: BrowserContext,\n onAction: (action: CapturedAction) => void,\n): Promise<void> {\n // Timestamp of the last in-page gesture (or recorded action). Used to tell a\n // user-driven navigation from one caused by an interaction we already logged.\n let lastGestureAt = 0;\n\n await context.exposeBinding('__humanjsEmit', (_source, action: unknown) => {\n if (!isCapturedAction(action)) return;\n lastGestureAt = Date.now();\n // Gesture pings only advance the clock — they aren't timeline steps.\n if (action.type === NAV_INTENT) return;\n onAction(action);\n });\n await context.addInitScript({ content: loadInjectedScript() });\n\n // Main-frame navigations become `goto` steps. Dedupe consecutive identical\n // URLs (a single navigation can fire more than once) and ignore blanks.\n let lastUrl = '';\n context.on('page', (page) => {\n page.on('framenavigated', (frame) => {\n if (frame !== page.mainFrame()) return;\n const url = frame.url();\n if (!url || url === 'about:blank' || url === lastUrl) return;\n lastUrl = url;\n // Skip navigations that are the consequence of a just-recorded gesture\n // (clicked link, form submit, search-as-you-type) — replaying the gesture\n // navigates on its own; a `goto` here would double-navigate.\n if (Date.now() - lastGestureAt < NAV_AFTER_GESTURE_MS) return;\n onAction({ type: 'goto', params: { url } });\n });\n });\n}\n","import {\n generateHumanJS,\n generatePlaywrightTest,\n type PlaywrightTestOptions,\n type Timeline,\n type TimelineEvent,\n} from '@humanjs/playwright';\n\nexport type ExportFormat = 'spec' | 'script';\n\nexport interface ExportOptions {\n /** `spec` → a `@humanjs/playwright/test` spec; `script` → a standalone HumanJS script. */\n readonly format: ExportFormat;\n readonly personality?: string;\n readonly seed?: string | null;\n readonly speed?: string;\n /** Test title (spec only); defaults to the recording name. */\n readonly title?: string;\n /** Extra `generatePlaywrightTest` options (steps / baseUrl / keepSleeps). */\n readonly playwright?: PlaywrightTestOptions;\n}\n\n// A `type`/`paste` step flagged secret (`params.secret = 'ENV_NAME'`) exports\n// its value as `process.env.ENV_NAME` instead of a literal. We can't make the\n// codegen emit a raw identifier directly, so we route the value through a\n// sentinel string and unquote it in the generated output.\nconst ENV_PREFIX = '__HUMANJS_ENV__';\nconst ENV_SENTINEL_RE = /['\"]__HUMANJS_ENV__([A-Za-z0-9_]+)__['\"]/g;\n\nfunction sanitizeEnvName(name: string): string {\n return name.replace(/[^A-Za-z0-9_]/g, '_');\n}\n\n/** Replace the value of secret-flagged type/paste steps with an env sentinel. */\nfunction applySecrets(events: readonly TimelineEvent[]): TimelineEvent[] {\n return events.map((event) => {\n const secret = event.params.secret;\n if ((event.type === 'type' || event.type === 'paste') && typeof secret === 'string' && secret) {\n return { ...event, inputValue: `${ENV_PREFIX}${sanitizeEnvName(secret)}__` };\n }\n return event;\n });\n}\n\nfunction restoreSecrets(code: string): string {\n return code.replace(ENV_SENTINEL_RE, 'process.env.$1');\n}\n\n/** Pick the export format from an output filename. `.spec.ts` / `.test.ts` → spec. */\nexport function formatFromFilename(filename: string): ExportFormat {\n return /\\.(spec|test)\\.[cm]?tsx?$/i.test(filename) ? 'spec' : 'script';\n}\n\n/**\n * Generate code from a captured timeline. Wraps the events in a `Timeline` with\n * the chosen personality/seed/speed and runs the shared `@humanjs/playwright`\n * codegen, then substitutes `process.env.*` for any secret-flagged values.\n */\nexport function generateCode(events: readonly TimelineEvent[], options: ExportOptions): string {\n const timeline: Timeline = {\n version: 1,\n name: options.title,\n personality: options.personality ?? 'careful',\n seed: options.seed ?? null,\n speed: options.speed ?? 'human',\n durationMs: 0,\n events: applySecrets(events),\n };\n const code =\n options.format === 'script'\n ? generateHumanJS(timeline)\n : generatePlaywrightTest(timeline, options.playwright);\n return restoreSecrets(code);\n}\n","import { spawn } from 'node:child_process';\n\n/**\n * Best-effort: open `url` in the user's default browser for the dashboard.\n *\n * Uses the platform's native opener (`open` / `start` / `xdg-open`). Failures\n * are swallowed — the CLI always prints the URL too, so the user can open it\n * by hand if no opener is available (headless box, locked-down environment).\n */\nexport function openInBrowser(url: string): void {\n let command: string;\n let args: string[];\n if (process.platform === 'darwin') {\n command = 'open';\n args = [url];\n } else if (process.platform === 'win32') {\n command = 'cmd';\n args = ['/c', 'start', '', url];\n } else {\n command = 'xdg-open';\n args = [url];\n }\n\n try {\n const child = spawn(command, args, {\n stdio: 'ignore',\n detached: true,\n });\n child.on('error', () => {\n // No opener on this platform / not on PATH — the printed URL is the fallback.\n });\n child.unref();\n } catch {\n // spawn threw synchronously (rare) — ignore; the URL is printed regardless.\n }\n}\n","/**\n * Fallback dashboard served when the built Vite + React editor isn't present in\n * `dist/dashboard/`. It's a read-only view of the live timeline + code preview\n * over the same `state` protocol, with Export buttons — enough to exercise the\n * pipeline without the full editor.\n *\n * Single self-contained string — no external assets, no nested backticks.\n */\nexport const PLACEHOLDER_HTML = `<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>HumanJS Generator</title>\n <style>\n :root { color-scheme: dark; }\n * { box-sizing: border-box; }\n body {\n margin: 0; min-height: 100vh; display: grid; place-items: center;\n background: #060604; color: #f0ece5;\n font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;\n }\n main { width: min(620px, 92vw); padding: 2rem; }\n h1 { font-size: 1.25rem; margin: 0 0 0.25rem; letter-spacing: 0.02em; }\n .accent { color: #f5a55c; }\n .muted { color: #8a857c; }\n .row { display: flex; align-items: center; gap: 0.6rem; margin: 1.25rem 0 0.5rem; }\n .dot { width: 9px; height: 9px; border-radius: 50%; background: #555; transition: background 0.2s; }\n .dot.on { background: #f5a55c; box-shadow: 0 0 10px #f5a55c; }\n .target { margin-top: 0.5rem; word-break: break-all; }\n ol { margin: 1rem 0 0; padding-left: 0; list-style: none; font-size: 0.85rem; }\n ol li { padding: 0.35rem 0; border-top: 1px solid #1a1a1a; display: flex; gap: 0.6rem; }\n ol li .kind { color: #f5a55c; min-width: 5.5rem; }\n ol li .detail { color: #c9c9c9; word-break: break-all; }\n .empty { color: #6a6a6a; }\n .bar { display: flex; align-items: center; gap: 0.6rem; margin-top: 1.5rem; flex-wrap: wrap; }\n button {\n font: inherit; font-size: 0.8rem; color: #060604; background: #f5a55c;\n border: 0; border-radius: 6px; padding: 0.45rem 0.8rem; cursor: pointer;\n }\n button:hover { background: #ffb87a; }\n .saved { color: #f5a55c; font-size: 0.8rem; }\n pre.code {\n margin-top: 1rem; padding: 1rem; background: #0c0b0a; border: 1px solid #1a1a1a;\n border-radius: 8px; overflow: auto; max-height: 320px; font-size: 0.8rem;\n color: #cfcfcf; white-space: pre; line-height: 1.5;\n }\n footer { margin-top: 1.75rem; font-size: 0.8rem; }\n </style>\n </head>\n <body>\n <main>\n <h1><span class=\"accent\">HumanJS</span> Generator</h1>\n <p class=\"muted\">Local dashboard &mdash; read-only fallback view.</p>\n <div class=\"row\"><span id=\"dot\" class=\"dot\"></span><span id=\"status\">connecting&hellip;</span></div>\n <p class=\"muted target\" id=\"target\"></p>\n <ol id=\"events\"><li class=\"empty\">No steps captured yet &mdash; interact with the Chromium window.</li></ol>\n <div class=\"bar\">\n <button id=\"exp-spec\" type=\"button\">Export .spec.ts</button>\n <button id=\"exp-script\" type=\"button\">Export .ts</button>\n <span id=\"saved\" class=\"saved\"></span>\n </div>\n <pre class=\"code\" id=\"code\"></pre>\n <footer class=\"muted\">The full editor (drag, relabel, selector picker, assertions) loads here once built.</footer>\n </main>\n <script>\n var dot = document.getElementById('dot');\n var status = document.getElementById('status');\n var target = document.getElementById('target');\n var events = document.getElementById('events');\n var code = document.getElementById('code');\n var saved = document.getElementById('saved');\n\n function detailFor(ev) {\n var p = ev.params || {};\n if (typeof ev.inputValue === 'string') return (p.target || '') + ' = ' + JSON.stringify(ev.inputValue);\n if (p.from) return p.from + ' -> ' + p.to;\n if (p.key) return String(p.key);\n if (p.url) return String(p.url);\n if (p.kind) return p.kind + (p.target ? ' ' + p.target : '') + (p.value ? ' ' + JSON.stringify(p.value) : '');\n if (typeof p.values !== 'undefined') return (p.target || '') + ' -> ' + JSON.stringify(p.values);\n return String(p.target || '');\n }\n\n function renderSteps(steps) {\n events.innerHTML = '';\n if (!steps.length) {\n events.innerHTML = '<li class=\"empty\">No steps captured yet &mdash; interact with the Chromium window.</li>';\n return;\n }\n for (var i = 0; i < steps.length; i++) {\n var ev = steps[i];\n var li = document.createElement('li');\n var kind = document.createElement('span');\n kind.className = 'kind';\n kind.textContent = ev.type;\n var detail = document.createElement('span');\n detail.className = 'detail';\n detail.textContent = detailFor(ev);\n li.appendChild(kind);\n li.appendChild(detail);\n events.appendChild(li);\n }\n }\n\n var ws = new WebSocket((location.protocol === 'https:' ? 'wss://' : 'ws://') + location.host);\n ws.onopen = function () { dot.classList.add('on'); status.textContent = 'connected'; };\n ws.onclose = function () { dot.classList.remove('on'); status.textContent = 'disconnected'; };\n ws.onmessage = function (event) {\n try {\n var msg = JSON.parse(event.data);\n if (msg.type === 'state') {\n target.textContent = 'Recording: ' + msg.targetUrl;\n renderSteps(msg.steps || []);\n code.textContent = msg.code || '';\n } else if (msg.type === 'exported') {\n saved.textContent = 'Saved ' + msg.path;\n }\n } catch (_) { /* ignore non-JSON frames */ }\n };\n\n function requestExport(format) {\n saved.textContent = '';\n ws.send(JSON.stringify({ type: 'export', format: format }));\n }\n document.getElementById('exp-spec').onclick = function () { requestExport('spec'); };\n document.getElementById('exp-script').onclick = function () { requestExport('script'); };\n </script>\n </body>\n</html>\n`;\n","import { createReadStream } from 'node:fs';\nimport { stat } from 'node:fs/promises';\nimport { createServer, type ServerResponse } from 'node:http';\nimport { dirname, join, normalize, sep } from 'node:path';\nimport { fileURLToPath } from 'node:url';\nimport { WebSocket, WebSocketServer } from 'ws';\nimport { PLACEHOLDER_HTML } from './dashboard-placeholder';\nimport type { ServerMessage } from './protocol';\n\nconst MODULE_DIR = dirname(fileURLToPath(import.meta.url));\n// The built Vite dashboard lands in dist/dashboard/ (milestone 5). Until it\n// exists, the root path falls back to the embedded placeholder.\nconst DASHBOARD_DIR = join(MODULE_DIR, 'dashboard');\n\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.map': 'application/json; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.ico': 'image/x-icon',\n '.woff2': 'font/woff2',\n};\n\nfunction contentTypeFor(filePath: string): string {\n const dot = filePath.lastIndexOf('.');\n const ext = dot === -1 ? '' : filePath.slice(dot).toLowerCase();\n return CONTENT_TYPES[ext] ?? 'application/octet-stream';\n}\n\nexport interface DashboardServer {\n /** Loopback URL the dashboard is served from, e.g. `http://127.0.0.1:53124`. */\n readonly url: string;\n /** Underlying WebSocket server — attach `connection` handlers for the live channel. */\n readonly wss: WebSocketServer;\n /** Send a message to every connected dashboard client. */\n broadcast(message: ServerMessage): void;\n close(): Promise<void>;\n}\n\nexport interface DashboardServerOptions {\n /** Directory of built dashboard assets to serve. Defaults to `dist/dashboard`. */\n readonly dashboardDir?: string;\n}\n\n/**\n * Start the local dashboard server, bound to loopback on an OS-assigned port.\n * Serves the built dashboard (or the placeholder) over HTTP and upgrades\n * WebSocket connections on the same port.\n */\nexport async function createDashboardServer(\n options: DashboardServerOptions = {},\n): Promise<DashboardServer> {\n const dashboardDir = options.dashboardDir ?? DASHBOARD_DIR;\n const http = createServer((req, res) => {\n void serve(dashboardDir, req.url ?? '/', res);\n });\n const wss = new WebSocketServer({ server: http });\n\n await new Promise<void>((resolve, reject) => {\n http.once('error', reject);\n // Loopback only — this is a local dev tool, never exposed to the network.\n http.listen(0, '127.0.0.1', resolve);\n });\n\n const address = http.address();\n if (address === null || typeof address === 'string') {\n throw new Error('dashboard server failed to bind to a TCP port');\n }\n const url = `http://127.0.0.1:${address.port}`;\n\n return {\n url,\n wss,\n broadcast(message) {\n const data = JSON.stringify(message);\n for (const client of wss.clients) {\n if (client.readyState === WebSocket.OPEN) client.send(data);\n }\n },\n close() {\n return new Promise<void>((resolve) => {\n for (const client of wss.clients) client.terminate();\n wss.close(() => http.close(() => resolve()));\n });\n },\n };\n}\n\nasync function serve(dashboardDir: string, rawUrl: string, res: ServerResponse): Promise<void> {\n const pathname = rawUrl.split('?')[0] ?? '/';\n const relative = pathname === '/' ? 'index.html' : pathname.replace(/^\\/+/, '');\n const filePath = normalize(join(dashboardDir, relative));\n\n // Path-traversal guard: the resolved file must stay inside the dashboard dir.\n if (filePath !== dashboardDir && !filePath.startsWith(dashboardDir + sep)) {\n res.writeHead(403).end('Forbidden');\n return;\n }\n\n try {\n const info = await stat(filePath);\n if (info.isFile()) {\n res.writeHead(200, { 'content-type': contentTypeFor(filePath) });\n createReadStream(filePath).pipe(res);\n return;\n }\n } catch {\n // No such file — fall through to the placeholder / 404.\n }\n\n // No built dashboard yet (or an unknown path): serve the placeholder at root.\n if (pathname === '/') {\n res.writeHead(200, { 'content-type': 'text/html; charset=utf-8' }).end(PLACEHOLDER_HTML);\n return;\n }\n res.writeHead(404).end('Not found');\n}\n","import type { TimelineEvent } from '@humanjs/playwright';\nimport type { AssertKind, Step, StepPatch } from './protocol';\n\n/**\n * The canonical, editable timeline held by the CLI. Capture appends events;\n * the dashboard issues edit commands (delete / move / update / addAssert) that\n * mutate this list. Every step carries a stable id so edits can target it.\n *\n * Steps are valid `TimelineEvent`s (plus the id, which the codegen ignores), so\n * the list can be passed straight to `generateCode`.\n */\nexport class TimelineStore {\n #steps: Step[] = [];\n #counter = 0;\n\n /** The current steps, in order. */\n list(): readonly Step[] {\n return this.#steps;\n }\n\n /** Append a freshly captured event, assigning it an id. */\n append(event: TimelineEvent): void {\n this.#steps.push({ ...event, id: this.#nextId() });\n }\n\n delete(id: string): void {\n this.#steps = this.#steps.filter((step) => step.id !== id);\n }\n\n /** Move a step to a new index (clamped to the list bounds). */\n move(id: string, toIndex: number): void {\n const from = this.#steps.findIndex((step) => step.id === id);\n if (from === -1) return;\n const [step] = this.#steps.splice(from, 1);\n if (!step) return;\n const clamped = Math.max(0, Math.min(toIndex, this.#steps.length));\n this.#steps.splice(clamped, 0, step);\n }\n\n /** Reorder the timeline to match the given id order (a full-order set from a drag). */\n reorder(ids: readonly string[]): void {\n const byId = new Map(this.#steps.map((step) => [step.id, step]));\n const next: Step[] = [];\n for (const id of ids) {\n const step = byId.get(id);\n if (step) {\n next.push(step);\n byId.delete(id);\n }\n }\n // Keep any steps the client didn't mention (e.g. captured mid-drag) at the end.\n for (const step of this.#steps) if (byId.has(step.id)) next.push(step);\n this.#steps = next;\n }\n\n update(id: string, patch: StepPatch): void {\n this.#steps = this.#steps.map((step) => (step.id === id ? applyPatch(step, patch) : step));\n }\n\n /** Insert an assertion step after `afterId` (or at the end when null). */\n addAssert(afterId: string | null, kind: AssertKind, target?: string, value?: string): void {\n const params: Record<string, unknown> = { kind };\n if (target !== undefined) params.target = target;\n if (value !== undefined) params.value = value;\n const step: Step = { id: this.#nextId(), type: 'assert', params, tMs: 0, durationMs: 0 };\n\n const index = afterId === null ? this.#steps.length - 1 : this.#findIndex(afterId);\n this.#steps.splice(index + 1, 0, step);\n }\n\n #findIndex(id: string): number {\n return this.#steps.findIndex((step) => step.id === id);\n }\n\n #nextId(): string {\n this.#counter += 1;\n return `s${this.#counter}`;\n }\n}\n\nfunction applyPatch(step: Step, patch: StepPatch): Step {\n const params: Record<string, unknown> = { ...step.params };\n if (patch.target !== undefined) params.target = patch.target;\n if (patch.label !== undefined) params.label = patch.label;\n if (patch.secret !== undefined) {\n if (patch.secret === null) delete params.secret;\n else params.secret = patch.secret;\n }\n return {\n ...step,\n params,\n ...(patch.inputValue !== undefined ? { inputValue: patch.inputValue } : {}),\n };\n}\n","import { writeFile } from 'node:fs/promises';\nimport { resolve } from 'node:path';\nimport { type PresetName, replayTimeline } from '@humanjs/playwright';\nimport { chromium } from 'playwright';\nimport { attachCapture } from './capture/attach';\nimport { type ExportFormat, generateCode } from './export';\nimport { openInBrowser } from './open-browser';\nimport type { ClientMessage, ServerMessage } from './protocol';\nimport { createDashboardServer } from './server';\nimport { TimelineStore } from './timeline-store';\n\n/** Built-in personalities the dashboard switcher offers. */\nconst PERSONALITIES = ['careful', 'fast', 'distracted', 'precise'] as const;\n\n/** Default output filenames per format, written to the CLI's working directory. */\nconst OUTPUT_FILE: Record<ExportFormat, string> = {\n spec: 'humanjs-recording.spec.ts',\n script: 'humanjs-recording.ts',\n};\n\n/**\n * Launch a recording session: start the local dashboard, open a real Chromium\n * window at `targetUrl`, capture interactions into an editable timeline, and let\n * the dashboard curate it (delete / reorder / relabel / edit / pick selectors /\n * add assertions / mark secrets / switch personality) with a live code preview,\n * then export a `.spec.ts` / `.ts`. Runs until the browser closes or the\n * process is interrupted.\n */\nexport async function start(targetUrl: string): Promise<void> {\n const server = await createDashboardServer();\n const store = new TimelineStore();\n let personality: PresetName = 'careful';\n // Assigned once the browser launches (below); the replay handler — only\n // reachable via a dashboard command, which can't arrive before launch —\n // closes over it.\n let browser: Awaited<ReturnType<typeof chromium.launch>>;\n let replayController: AbortController | null = null;\n\n const previewCode = (): string => generateCode(store.list(), { format: 'spec', personality });\n\n const stateMessage = (): ServerMessage => ({\n type: 'state',\n targetUrl,\n steps: store.list(),\n personality,\n personalities: [...PERSONALITIES],\n code: previewCode(),\n });\n\n const broadcastState = (): void => server.broadcast(stateMessage());\n\n const exportTimeline = async (format: ExportFormat): Promise<void> => {\n const path = resolve(process.cwd(), OUTPUT_FILE[format]);\n await writeFile(path, generateCode(store.list(), { format, personality }), 'utf8');\n server.broadcast({ type: 'exported', path });\n console.log(` Exported ${format === 'spec' ? 'test' : 'script'} → ${path}`);\n };\n\n // Replay the curated timeline in a fresh, capture-free window (so it can't\n // re-record itself), streaming per-step results to the dashboard. Stops at\n // the first failure; cancellable; the window is always closed afterwards.\n const runReplay = async (): Promise<void> => {\n if (replayController) return; // a run is already in flight\n const steps = store.list();\n if (steps.length === 0) return;\n const stepId = (index: number): string | undefined => steps[index]?.id;\n const controller = new AbortController();\n replayController = controller;\n server.broadcast({ type: 'replayStarted' });\n const startedAt = Date.now();\n // Created inside the try so a newContext/newPage failure still resets state\n // and broadcasts a `replayDone` — otherwise Run would stay disabled and the\n // dashboard would be stuck \"running\".\n let context: Awaited<ReturnType<typeof browser.newContext>> | undefined;\n try {\n context = await browser.newContext({ viewport: null });\n const page = await context.newPage();\n const result = await replayTimeline(page, steps, {\n personality,\n signal: controller.signal,\n onStep: ({ index, status, error }) => {\n const id = stepId(index);\n if (id) server.broadcast({ type: 'replayStep', id, status, ...(error ? { error } : {}) });\n },\n });\n const failedStepId =\n result.failedIndex === undefined ? undefined : stepId(result.failedIndex);\n server.broadcast({\n type: 'replayDone',\n status: result.status,\n durationMs: result.durationMs,\n ...(failedStepId ? { failedStepId } : {}),\n });\n } catch (cause) {\n const aborted = cause instanceof Error && cause.name === 'AbortError';\n server.broadcast({\n type: 'replayDone',\n status: 'fail',\n aborted,\n durationMs: Date.now() - startedAt,\n ...(aborted ? {} : { error: cause instanceof Error ? cause.message : String(cause) }),\n });\n } finally {\n await context?.close().catch(() => {});\n replayController = null;\n }\n };\n\n const handleCommand = (message: ClientMessage): void => {\n switch (message.type) {\n case 'delete':\n store.delete(message.id);\n break;\n case 'move':\n store.move(message.id, message.toIndex);\n break;\n case 'reorder':\n store.reorder(message.ids);\n break;\n case 'update':\n store.update(message.id, message.patch);\n break;\n case 'addAssert':\n store.addAssert(message.afterId, message.kind, message.target, message.value);\n break;\n case 'setPersonality':\n if ((PERSONALITIES as readonly string[]).includes(message.personality)) {\n // Guarded by the includes() check above — it's one of PERSONALITIES.\n personality = message.personality as PresetName;\n }\n break;\n case 'export':\n void exportTimeline(message.format);\n return; // export replies with `exported`, not a state refresh\n case 'replay':\n void runReplay();\n return; // replay streams its own `replay*` messages\n case 'cancelReplay':\n replayController?.abort();\n return;\n }\n broadcastState();\n };\n\n server.wss.on('connection', (socket) => {\n socket.send(JSON.stringify(stateMessage()));\n socket.on('message', (data) => {\n try {\n handleCommand(JSON.parse(data.toString()) as ClientMessage);\n } catch {\n // Ignore malformed frames.\n }\n });\n });\n\n browser = await chromium.launch({ headless: false });\n // `viewport: null` lets the page fill the real window — a person drives this.\n const context = await browser.newContext({ viewport: null });\n // Attach capture before the first page exists so the init script applies.\n await attachCapture(context, (action) => {\n store.append({\n type: action.type,\n params: action.params,\n tMs: 0,\n durationMs: 0,\n ...(action.inputValue === undefined ? {} : { inputValue: action.inputValue }),\n });\n broadcastState();\n });\n const page = await context.newPage();\n\n let closing = false;\n const shutdown = async (): Promise<void> => {\n if (closing) return;\n closing = true;\n await browser.close().catch(() => {});\n await server.close();\n process.exit(0);\n };\n process.on('SIGINT', () => void shutdown());\n process.on('SIGTERM', () => void shutdown());\n browser.on('disconnected', () => void shutdown());\n\n openInBrowser(server.url);\n\n try {\n await page.goto(targetUrl);\n } catch (error) {\n const reason = error instanceof Error ? error.message : String(error);\n console.warn(` Couldn't load ${targetUrl}: ${reason}`);\n console.warn(' Navigate manually in the Chromium window.');\n }\n\n console.log('');\n console.log(' HumanJS Generator');\n console.log(` Dashboard: ${server.url}`);\n console.log(` Recording: ${targetUrl}`);\n console.log('');\n console.log(' Interact with the Chromium window. Close it or press Ctrl+C to stop.');\n console.log('');\n}\n","/**\n * Normalize a user-supplied target into a navigable URL.\n *\n * A bare host (`example.com`) gets an `https://` scheme so `page.goto` accepts\n * it; loopback hosts default to `http://` since local dev servers rarely speak\n * TLS. An explicit scheme is always left untouched.\n */\nexport function normalizeUrl(input: string): string {\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(input)) return input;\n const isLoopback = /^(localhost|127\\.0\\.0\\.1|0\\.0\\.0\\.0|\\[::1\\])(:|\\/|$)/i.test(input);\n return `${isLoopback ? 'http' : 'https'}://${input}`;\n}\n","/**\n * @humanjs/generator — visual recorder for HumanJS.\n *\n * `npx @humanjs/generator <url>` launches a real Chromium window, records the\n * clicks / typing / scrolls / navigation you perform (with role- and\n * accessible-name-first selectors), shows a live, editable timeline in a local\n * dashboard, and exports a clean humanized Playwright test.\n *\n * Milestone 2: the CLI launches the browser and the local dashboard server and\n * wires the WebSocket channel. Capture, the editor UI, and export land across\n * the following milestones (see ROADMAP.md).\n */\n\nimport { start } from './run';\nimport { normalizeUrl } from './url';\n\nconst USAGE = 'Usage: npx @humanjs/generator <url>';\n\nasync function main(argv: readonly string[]): Promise<void> {\n const [rawUrl] = argv;\n\n if (!rawUrl || rawUrl === '--help' || rawUrl === '-h') {\n // No URL (or an explicit help flag): print usage. Exit 0 for --help, 1 for\n // a missing-argument error.\n console.log(USAGE);\n process.exit(rawUrl ? 0 : 1);\n }\n\n await start(normalizeUrl(rawUrl));\n}\n\nmain(process.argv.slice(2)).catch((error: unknown) => {\n console.error(error instanceof Error ? error.message : String(error));\n process.exit(1);\n});\n"]}
package/dist/index.js CHANGED
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import { stat, writeFile } from 'fs/promises';
3
3
  import { dirname, join, normalize, sep, resolve } from 'path';
4
+ import { replayTimeline, generateHumanJS, generatePlaywrightTest } from '@humanjs/playwright';
4
5
  import { chromium } from 'playwright';
5
6
  import { createReadStream, readFileSync } from 'fs';
6
7
  import { fileURLToPath } from 'url';
7
- import { generateHumanJS, generatePlaywrightTest } from '@humanjs/playwright';
8
8
  import { spawn } from 'child_process';
9
9
  import { createServer } from 'http';
10
10
  import { WebSocketServer, WebSocket } from 'ws';
@@ -380,6 +380,8 @@ async function start(targetUrl) {
380
380
  const server = await createDashboardServer();
381
381
  const store = new TimelineStore();
382
382
  let personality = "careful";
383
+ let browser;
384
+ let replayController = null;
383
385
  const previewCode = () => generateCode(store.list(), { format: "spec", personality });
384
386
  const stateMessage = () => ({
385
387
  type: "state",
@@ -396,6 +398,49 @@ async function start(targetUrl) {
396
398
  server.broadcast({ type: "exported", path });
397
399
  console.log(` Exported ${format === "spec" ? "test" : "script"} \u2192 ${path}`);
398
400
  };
401
+ const runReplay = async () => {
402
+ if (replayController) return;
403
+ const steps = store.list();
404
+ if (steps.length === 0) return;
405
+ const stepId = (index) => steps[index]?.id;
406
+ const controller = new AbortController();
407
+ replayController = controller;
408
+ server.broadcast({ type: "replayStarted" });
409
+ const startedAt = Date.now();
410
+ let context2;
411
+ try {
412
+ context2 = await browser.newContext({ viewport: null });
413
+ const page2 = await context2.newPage();
414
+ const result = await replayTimeline(page2, steps, {
415
+ personality,
416
+ signal: controller.signal,
417
+ onStep: ({ index, status, error }) => {
418
+ const id = stepId(index);
419
+ if (id) server.broadcast({ type: "replayStep", id, status, ...error ? { error } : {} });
420
+ }
421
+ });
422
+ const failedStepId = result.failedIndex === void 0 ? void 0 : stepId(result.failedIndex);
423
+ server.broadcast({
424
+ type: "replayDone",
425
+ status: result.status,
426
+ durationMs: result.durationMs,
427
+ ...failedStepId ? { failedStepId } : {}
428
+ });
429
+ } catch (cause) {
430
+ const aborted = cause instanceof Error && cause.name === "AbortError";
431
+ server.broadcast({
432
+ type: "replayDone",
433
+ status: "fail",
434
+ aborted,
435
+ durationMs: Date.now() - startedAt,
436
+ ...aborted ? {} : { error: cause instanceof Error ? cause.message : String(cause) }
437
+ });
438
+ } finally {
439
+ await context2?.close().catch(() => {
440
+ });
441
+ replayController = null;
442
+ }
443
+ };
399
444
  const handleCommand = (message) => {
400
445
  switch (message.type) {
401
446
  case "delete":
@@ -421,6 +466,14 @@ async function start(targetUrl) {
421
466
  case "export":
422
467
  void exportTimeline(message.format);
423
468
  return;
469
+ // export replies with `exported`, not a state refresh
470
+ case "replay":
471
+ void runReplay();
472
+ return;
473
+ // replay streams its own `replay*` messages
474
+ case "cancelReplay":
475
+ replayController?.abort();
476
+ return;
424
477
  }
425
478
  broadcastState();
426
479
  };
@@ -433,7 +486,7 @@ async function start(targetUrl) {
433
486
  }
434
487
  });
435
488
  });
436
- const browser = await chromium.launch({ headless: false });
489
+ browser = await chromium.launch({ headless: false });
437
490
  const context = await browser.newContext({ viewport: null });
438
491
  await attachCapture(context, (action) => {
439
492
  store.append({