@hover-dev/core 0.20.0 → 0.21.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.
@@ -1 +1 @@
1
- {"version":3,"file":"humanSteps.d.ts","sourceRoot":"","sources":["../../src/specs/humanSteps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAwDxE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAmBvD"}
1
+ {"version":3,"file":"humanSteps.d.ts","sourceRoot":"","sources":["../../src/specs/humanSteps.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AACH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,uEAAuE;AACvE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CAoExE;AAED;;;;;GAKG;AACH,wBAAgB,UAAU,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAmBvD"}
@@ -40,6 +40,18 @@ export function humanStep(tool, rawInput) {
40
40
  const key = String(input.key ?? '');
41
41
  return key ? `Press ${key}` : null;
42
42
  }
43
+ // Grounded control tools (MCP-first) — the target is role+name/testId/text
44
+ // ON the input itself, not an `element` string.
45
+ case 'click_control':
46
+ return `Click ${describeGrounded(input)}`;
47
+ case 'fill_control':
48
+ return `Fill ${describeGrounded(input)} with ${quote(String(input.value ?? ''))}`;
49
+ case 'select_control':
50
+ return `Select ${quote(String(input.value ?? ''))} in ${describeGrounded(input)}`;
51
+ case 'check_control':
52
+ return `${input.checked === false ? 'Uncheck' : 'Check'} ${describeGrounded(input)}`;
53
+ case 'assert_visible':
54
+ return `Expect ${describeGrounded(input)} to be visible`;
43
55
  // Diagnostic / read-only — same skip list as writeSpec.translateStep.
44
56
  case 'browser_wait_for':
45
57
  case 'browser_tabs':
@@ -91,6 +103,23 @@ function describe(raw) {
91
103
  const s = String(raw ?? '').trim();
92
104
  return s.length > 0 ? s : 'the target element';
93
105
  }
106
+ /** Human phrase for a GROUNDED target ({ role, name, testId, text }) — the shape
107
+ * the *_control tools carry, vs the old browser_* tools' `element` string. */
108
+ function describeGrounded(input) {
109
+ const role = typeof input.role === 'string' ? input.role : '';
110
+ const name = typeof input.name === 'string' ? input.name : '';
111
+ const testId = typeof input.testId === 'string' ? input.testId : '';
112
+ const text = typeof input.text === 'string' ? input.text : '';
113
+ if (role && name)
114
+ return `${role} "${name}"`;
115
+ if (name)
116
+ return `"${name}"`;
117
+ if (testId)
118
+ return `testId "${testId}"`;
119
+ if (text)
120
+ return `"${text}"`;
121
+ return 'the target element';
122
+ }
94
123
  /** Wrap in double-quotes for prose; escape internal quotes. A redacted
95
124
  * credential (stored as a `process.env.X …` expression) shows as the masked
96
125
  * `$X` instead — the prose, like the code, never reveals the secret. */
@@ -1 +1 @@
1
- {"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAatD,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;0DAE0D;AAC1D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AA2CD,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAgB,SAAQ,KAAK;aACZ,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB;;;;yEAIqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;sFAIkF;IAClF,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;qEAIiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAiDD,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD;;;;;2DAKuD;IACvD,gBAAgB,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CACnE;AAQD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAchF;AA+mBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9E;AAED;;oCAEoC;AACpC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAqB9E;AA0DD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAMxE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAQ1F"}
1
+ {"version":3,"file":"writeSpec.d.ts","sourceRoot":"","sources":["../../src/specs/writeSpec.ts"],"names":[],"mappings":"AAqBA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAatD,MAAM,MAAM,QAAQ,GAAG,SAAS,CAAC;AAEjC;;;;;;;GAOG;AACH,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AAEzD;;0DAE0D;AAC1D,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAE9D;AA2CD,MAAM,WAAW,aAAa;IAC5B,oEAAoE;IACpE,IAAI,EAAE,MAAM,CAAC;IACb,qDAAqD;IACrD,IAAI,CAAC,EAAE,MAAM,CAAC;CACf;AAED,qBAAa,eAAgB,SAAQ,KAAK;aACZ,IAAI,EAAE,MAAM;aAAkB,IAAI,EAAE,MAAM;gBAA1C,IAAI,EAAE,MAAM,EAAkB,IAAI,EAAE,MAAM;CAIvE;AAED;;;;;;;GAOG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,QAAQ,EAAE,CAAC;IAClB,UAAU,CAAC,EAAE,aAAa,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,0EAA0E;IAC1E,UAAU,CAAC,EAAE,SAAS,EAAE,CAAC;IACzB;;;;yEAIqE;IACrE,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;;sFAIkF;IAClF,WAAW,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC;IACtE;;;;qEAIiE;IACjE,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAiDD,MAAM,WAAW,eAAe;IAC9B,8EAA8E;IAC9E,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,+EAA+E;IAC/E,KAAK,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,EAAE,CAAC;IACtD;;;;;2DAKuD;IACvD,gBAAgB,CAAC,EAAE;QAAE,UAAU,EAAE,MAAM,CAAC;QAAC,cAAc,EAAE,MAAM,CAAA;KAAE,CAAC;CACnE;AAQD,wBAAsB,SAAS,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,eAAe,CAAC,CAchF;AAonBD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,eAAe,CAAC,YAAY,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,CAM9E;AAED;;oCAEoC;AACpC,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAEpD;AAED;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAqB9E;AA0DD;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAMxE;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,MAAM,CAQ1F"}
@@ -201,13 +201,17 @@ async function writeOneSpec(opts, slug, displayName, rawSteps) {
201
201
  const envVars = (opts.redactions ?? []).map(r => r.envVar);
202
202
  const detectedPrefix = authPrefixLength(cleanActions, envVars);
203
203
  const userConfigName = PLAYWRIGHT_CONFIG_NAMES.find(n => existsSync(join(opts.devRoot, n)));
204
+ // A config Hover scaffolded earlier this run (e.g. a non-login spec wrote a
205
+ // plain one first) is OURS to upgrade — treat it like "no user config", not a
206
+ // hands-off user file. ensurePlaywrightConfig adds the setup project to it.
207
+ const ownScaffold = !!userConfigName && readFileSync(join(opts.devRoot, userConfigName), 'utf-8').includes(SCAFFOLD_MARKER);
204
208
  // Already opted in: auth.setup.ts exists from a prior approval (and the config
205
209
  // already registers it), so engage AUTOMATICALLY — don't re-ask or re-edit.
206
210
  const authSetupExists = existsSync(join(dir, 'auth.setup.ts'));
207
211
  // Engage the fixture when a login is detected AND we can register the setup
208
- // project: we scaffold the config (no user config), the caller approved editing
209
- // it (opts.authFixture, Stage 4), or the fixture was already set up earlier.
210
- const engage = detectedPrefix > 0 && (!userConfigName || opts.authFixture === true || authSetupExists);
212
+ // project: we scaffold/own the config, the caller approved editing a user
213
+ // config (opts.authFixture, Stage 4), or the fixture was already set up earlier.
214
+ const engage = detectedPrefix > 0 && (!userConfigName || ownScaffold || opts.authFixture === true || authSetupExists);
211
215
  const authPrefix = engage ? detectedPrefix : 0;
212
216
  const authFile = engage ? AUTH_STATE_FILE : undefined;
213
217
  let authFixtureOffer;
@@ -1005,28 +1009,24 @@ async function ensureResetStateHelper(devRoot, keys) {
1005
1009
  ].join('\n');
1006
1010
  await writeFile(join(dir, 'resetState.ts'), source, 'utf-8');
1007
1011
  }
1008
- async function ensurePlaywrightConfig(devRoot, steps, startUrl, authFile) {
1009
- if (PLAYWRIGHT_CONFIG_NAMES.some(n => existsSync(join(devRoot, n))))
1010
- return;
1011
- const origin = firstNavigateOrigin(steps) ?? originOf(startUrl);
1012
- if (!origin)
1013
- return;
1014
- // Auth-as-fixture: register a `setup` project (matches auth.setup.ts) that the
1015
- // main project depends on, so login runs ONCE before the specs. Only emitted
1016
- // when scaffolding our own config (we never touch a user's existing one).
1012
+ const SCAFFOLD_MARKER = 'Scaffolded by Hover';
1013
+ /** The scaffolded config source. When `authFile` is set, a `setup` project runs
1014
+ * auth.setup.ts ONCE and the main `chromium` project reuses the saved
1015
+ * storageState so EVERY spec starts authenticated, not just the login flow. */
1016
+ function renderScaffoldConfig(origin, authFile) {
1017
1017
  const projects = authFile
1018
1018
  ? [
1019
1019
  ` projects: [`,
1020
1020
  ` { name: 'setup', testMatch: /.*\\.setup\\.ts$/ },`,
1021
- ` { name: 'chromium', dependencies: ['setup'] },`,
1021
+ ` { name: 'chromium', dependencies: ['setup'], use: { storageState: ${JSON.stringify(authFile)} } },`,
1022
1022
  ` ],`,
1023
1023
  ]
1024
1024
  : [];
1025
- const source = [
1025
+ return [
1026
1026
  `import { defineConfig } from '@playwright/test';`,
1027
1027
  ``,
1028
1028
  `/**`,
1029
- ` * Scaffolded by Hover so crystallized specs (which use relative URLs like`,
1029
+ ` * ${SCAFFOLD_MARKER} so crystallized specs (which use relative URLs like`,
1030
1030
  ` * page.goto("/")) resolve against a base. Override HOVER_BASE_URL in CI to`,
1031
1031
  ` * point the same specs at staging/prod.`,
1032
1032
  ` */`,
@@ -1039,7 +1039,50 @@ async function ensurePlaywrightConfig(devRoot, steps, startUrl, authFile) {
1039
1039
  `});`,
1040
1040
  ``,
1041
1041
  ].join('\n');
1042
- await writeFile(join(devRoot, 'playwright.config.ts'), source, 'utf-8');
1042
+ }
1043
+ async function ensurePlaywrightConfig(devRoot, steps, startUrl, authFile) {
1044
+ const origin = firstNavigateOrigin(steps) ?? originOf(startUrl);
1045
+ if (!origin)
1046
+ return;
1047
+ const existingName = PLAYWRIGHT_CONFIG_NAMES.find(n => existsSync(join(devRoot, n)));
1048
+ if (existingName) {
1049
+ // A config already exists. Only ever UPGRADE our OWN scaffold — and only to
1050
+ // add the auth `setup` project when a login was just lifted (authFile) and
1051
+ // it isn't there yet. This makes auth order-independent: whichever spec in
1052
+ // the run triggers auth-fixture upgrades the config, even if a non-login
1053
+ // spec scaffolded a plain config first. A user's own config is never touched
1054
+ // (that's the Stage-4 approval flow via authFixtureOffer).
1055
+ if (!authFile)
1056
+ return;
1057
+ try {
1058
+ const cur = readFileSync(join(devRoot, existingName), 'utf-8');
1059
+ if (!cur.includes(SCAFFOLD_MARKER) || cur.includes(`name: 'setup'`))
1060
+ return;
1061
+ await writeFile(join(devRoot, existingName), renderScaffoldConfig(origin, authFile), 'utf-8');
1062
+ }
1063
+ catch { /* upgrade is best-effort */ }
1064
+ return;
1065
+ }
1066
+ await writeFile(join(devRoot, 'playwright.config.ts'), renderScaffoldConfig(origin, authFile), 'utf-8');
1067
+ await ensurePlaywrightDep(devRoot);
1068
+ }
1069
+ /** When Hover scaffolds the config it also ensures `@playwright/test` is a
1070
+ * devDependency — otherwise `npx playwright test` can't run the specs locally.
1071
+ * Best-effort + idempotent: skips if already present (either dep list) or if
1072
+ * there's no package.json. Reformats to 2-space JSON (the npm norm). */
1073
+ const PLAYWRIGHT_TEST_RANGE = '^1.50.0';
1074
+ async function ensurePlaywrightDep(devRoot) {
1075
+ const pkgPath = join(devRoot, 'package.json');
1076
+ if (!existsSync(pkgPath))
1077
+ return;
1078
+ try {
1079
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
1080
+ if (pkg.dependencies?.['@playwright/test'] || pkg.devDependencies?.['@playwright/test'])
1081
+ return;
1082
+ pkg.devDependencies = { ...(pkg.devDependencies ?? {}), '@playwright/test': PLAYWRIGHT_TEST_RANGE };
1083
+ await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
1084
+ }
1085
+ catch { /* best-effort — never break Save */ }
1043
1086
  }
1044
1087
  function stripBaseUrl(url) {
1045
1088
  // http://localhost:5173/checkout → /checkout, http://localhost:5173/ → /
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hover-dev/core",
3
- "version": "0.20.0",
3
+ "version": "0.21.0",
4
4
  "description": "Hover's local Node service: agent invocation, Playwright CDP preflight, WebSocket bridge.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Hyperyond",