kryptheon 0.1.15 → 0.1.17

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/bin/kryptheon.js CHANGED
@@ -40,10 +40,14 @@ const MIN_NODE = [20, 6, 0];
40
40
  const WINDOW_POLL_MS = 5000;
41
41
  const WINDOW_TIMEOUT_MS = 30000;
42
42
  const WINDOW_HARD_LIMIT_MS = 90000;
43
- // Lower than the poll interval on purpose: a probe still running when the next
44
- // one is due is of no use, and every extra second it holds the loop is a second
45
- // the watchdog is blind.
46
- const WINDOW_PROBE_TIMEOUT_MS = 4000;
43
+ // This was 4 seconds, kept under the poll interval so a probe never held the
44
+ // loop past the next one. Measured since, on a Windows 11 machine: the probe
45
+ // takes 2.5-3 seconds with nothing else running, and while the browser was
46
+ // starting it ran past 4 seconds three times in a row. On a slower machine
47
+ // every probe ran out of time and nothing was ever learned. A probe that
48
+ // answers late is worth more than one that never answers; the budget is
49
+ // counted by the clock, so a slow tick costs nothing extra.
50
+ const WINDOW_PROBE_TIMEOUT_MS = 10000;
47
51
 
48
52
  function nodeIsTooOld(version) {
49
53
  const parts = String(version || process.versions.node).split('.').map(Number);
@@ -614,6 +618,49 @@ function windowUndetectedLines(seconds) {
614
618
  ];
615
619
  }
616
620
 
621
+ /**
622
+ * What to do with a browser whose window the watchdog could not confirm:
623
+ * 'leave' it running, or 'stop' it.
624
+ *
625
+ * Found on the first run by somebody new, on Windows 10: the browser opened,
626
+ * they used their app, and it closed under them - "almost the whole thing" -
627
+ * with nothing recorded. The probe is a fresh PowerShell each time: measured on
628
+ * a Windows 11 machine it takes 2.5-3 seconds idle, and ran past its 4 second
629
+ * limit three times in a row while the browser was starting. On a slower
630
+ * machine every probe runs out of time, the watchdog learns nothing for 90
631
+ * seconds, calls that 'unclear' - and the browser somebody was using was
632
+ * stopped on a guess.
633
+ *
634
+ * So the browser is only ever stopped on something known, and never when a
635
+ * person is at the keyboard to close it themselves:
636
+ *
637
+ * window leave - it is up
638
+ * unclear leave - nothing was learned, so nothing is done
639
+ * no-window / never-started leave with a keyboard, stop without one
640
+ *
641
+ * The watchdog exists for the case with no keyboard: an AI assistant, where
642
+ * there is no desktop and codegen would wait for ever. isTTY can be false in a
643
+ * perfectly good terminal, so false is not taken as proof of anything - that
644
+ * case keeps the old behaviour. True is taken as a person being there.
645
+ */
646
+ function whenWindowUnconfirmed(verdict, atKeyboard) {
647
+ if (verdict === 'window' || verdict === 'unclear') return 'leave';
648
+ return atKeyboard ? 'leave' : 'stop';
649
+ }
650
+
651
+ // Said once, while the person may still be recording, instead of taking the
652
+ // browser away from them.
653
+ function windowUnconfirmedLines() {
654
+ return [
655
+ '',
656
+ ' (I could not confirm the browser window from here - that check is slow',
657
+ ' on some machines - so I am leaving it alone. If it is on your screen,',
658
+ ' carry on and close it when you are done. If there is no window',
659
+ ' anywhere, press Ctrl+C to stop.)',
660
+ '',
661
+ ];
662
+ }
663
+
617
664
  // The probe kept failing to answer. Nothing was learned about the browser
618
665
  // either way, and saying "no window appeared" would be inventing a fact.
619
666
  function probeUnclearLines(answered, unanswered) {
@@ -1520,7 +1567,12 @@ async function record(url) {
1520
1567
  const outcome = watch.tick();
1521
1568
  if (!outcome) return;
1522
1569
  clearInterval(watchdog);
1523
- if (outcome.verdict === 'window') return;
1570
+ if (whenWindowUnconfirmed(outcome.verdict, !!process.stdin.isTTY) === 'leave') {
1571
+ if (outcome.verdict !== 'window') {
1572
+ for (const line of windowUnconfirmedLines()) console.log(line);
1573
+ }
1574
+ return;
1575
+ }
1524
1576
  stopChild();
1525
1577
  finalise(outcome.verdict, outcome).then(resolve);
1526
1578
  }, WINDOW_POLL_MS);
@@ -2005,6 +2057,57 @@ async function finishWith(code) {
2005
2057
  );
2006
2058
  }
2007
2059
 
2060
+ // The copy that must run `check` is the one the recordings load. Measured: in
2061
+ // a folder `record` set up, `npx kryptheon@latest check` ran npx's copy, whose
2062
+ // Playwright loads the config, while the recordings loaded the folder's copy -
2063
+ // two Playwrights, and "Requiring @playwright/test second time" before a single
2064
+ // recording ran. `npx kryptheon check` worked in the same folder only because
2065
+ // npx happened to pick the local copy.
2066
+ //
2067
+ // So when this is not the folder's copy, the folder's copy does the work.
2068
+ // Returns its exit code, or null to carry on here.
2069
+ function handToFolderCopy() {
2070
+ if (process.env.KRYPTHEON_HANDED_OVER) return null; // once, never in a loop
2071
+ const theirs = project.kryptheonCopyFor(USER_DIR);
2072
+ if (!theirs) return null;
2073
+ let same = false;
2074
+ try {
2075
+ same = fs.realpathSync(theirs) === fs.realpathSync(PACKAGE_DIR);
2076
+ } catch (err) {
2077
+ same = path.resolve(theirs) === path.resolve(PACKAGE_DIR);
2078
+ }
2079
+ if (same) return null;
2080
+
2081
+ let theirVersion = 'unknown';
2082
+ try {
2083
+ theirVersion = JSON.parse(fs.readFileSync(path.join(theirs, 'package.json'), 'utf8')).version;
2084
+ } catch (err) {
2085
+ /* said as unknown */
2086
+ }
2087
+ const mine = packageVersion();
2088
+ if (theirVersion !== mine) {
2089
+ // Not an error: the folder's copy is the one its recordings are written
2090
+ // against. Said, so an older one is never mistaken for this one.
2091
+ console.log('');
2092
+ console.log(' Running the kryptheon installed in this folder (' + theirVersion + '), because your');
2093
+ console.log(' recordings load that one. ' + (mine !== 'unknown' ? 'To move this folder to ' + mine + ':' : 'To update it:'));
2094
+ console.log(' npm i -D kryptheon@latest');
2095
+ }
2096
+ const result = spawnSync(process.execPath, [path.join(theirs, 'bin', 'kryptheon.js')].concat(process.argv.slice(2)), {
2097
+ cwd: USER_DIR,
2098
+ stdio: 'inherit',
2099
+ env: Object.assign({}, process.env, { KRYPTHEON_HANDED_OVER: '1' }),
2100
+ });
2101
+ if (result.error) {
2102
+ console.error('');
2103
+ console.error(' Could not run the kryptheon installed in this folder: ' + result.error.message);
2104
+ console.error(' Reinstalling it usually fixes this: npm i -D kryptheon@latest');
2105
+ console.error('');
2106
+ return 1;
2107
+ }
2108
+ return result.status === null ? 1 : result.status;
2109
+ }
2110
+
2008
2111
  async function main() {
2009
2112
  if (nodeIsTooOld()) {
2010
2113
  console.error('');
@@ -2021,9 +2124,11 @@ async function main() {
2021
2124
  case 'record':
2022
2125
  return finishWith(await record(rest[0]));
2023
2126
  break;
2024
- case 'check':
2127
+ case 'check': {
2128
+ const handedOver = handToFolderCopy();
2129
+ if (handedOver !== null) return finishWith(handedOver);
2025
2130
  return finishWith(check({ quiet: rest.indexOf('--quiet') !== -1 || rest.indexOf('-q') !== -1 }));
2026
- break;
2131
+ }
2027
2132
  case 'remove':
2028
2133
  case 'rm':
2029
2134
  return finishWith(await remove(rest.join(" ").trim()));
@@ -2070,6 +2175,7 @@ module.exports = {
2070
2175
  rulesBlock: rulesBlock,
2071
2176
  record: record,
2072
2177
  prepareFolder: prepareFolder,
2178
+ handToFolderCopy: handToFolderCopy,
2073
2179
  finaliseRecording: finaliseRecording,
2074
2180
  takeOutSecrets: takeOutSecrets,
2075
2181
  reportSecrets: reportSecrets,
@@ -2091,9 +2197,12 @@ module.exports = {
2091
2197
  dropOldRecordings: dropOldRecordings,
2092
2198
  askAboutExisting: askAboutExisting,
2093
2199
  askKey: askKey,
2200
+ whenWindowUnconfirmed: whenWindowUnconfirmed,
2201
+ windowUnconfirmedLines: windowUnconfirmedLines,
2094
2202
  WINDOW_TIMEOUT_MS: WINDOW_TIMEOUT_MS,
2095
2203
  WINDOW_HARD_LIMIT_MS: WINDOW_HARD_LIMIT_MS,
2096
2204
  WINDOW_POLL_MS: WINDOW_POLL_MS,
2205
+ WINDOW_PROBE_TIMEOUT_MS: WINDOW_PROBE_TIMEOUT_MS,
2097
2206
  classifyFetchError: classifyFetchError,
2098
2207
  normaliseRecordUrl: normaliseRecordUrl,
2099
2208
  isLocalAddress: isLocalAddress,
@@ -238,6 +238,31 @@ function noRecordingsLines() {
238
238
  // missing before the browser, which is a 200MB download on a new machine, and
239
239
  // offers to add it.
240
240
 
241
+ /**
242
+ * The installed kryptheon a recording saved in this folder will load - the
243
+ * nearest node_modules/kryptheon going up - or null if there is none.
244
+ *
245
+ * `check` must be run by this same copy. Found through the MCP server, then
246
+ * measured by hand in a folder `record` had set up: `npx kryptheon@latest
247
+ * check` runs npx's own copy, whose Playwright loads the config, while the
248
+ * recordings load this one's - and Playwright stops with "Requiring
249
+ * @playwright/test second time" before a single recording runs.
250
+ */
251
+ function kryptheonCopyFor(dir) {
252
+ let current = path.resolve(String(dir || ''));
253
+ for (;;) {
254
+ const candidate = path.join(current, 'node_modules', 'kryptheon');
255
+ // Whole copies only. A half-finished install - the fixture there, the rest
256
+ // not - was handed the run and answered with Node's own stack trace.
257
+ const whole = ['kryptheon-fixture.js', 'package.json', path.join('bin', 'kryptheon.js')]
258
+ .every((f) => fs.existsSync(path.join(candidate, f)));
259
+ if (whole) return candidate;
260
+ const parent = path.dirname(current);
261
+ if (parent === current) return null;
262
+ current = parent;
263
+ }
264
+ }
265
+
241
266
  /**
242
267
  * Whether kryptheon can be loaded by a recording saved in this folder.
243
268
  *
@@ -505,6 +530,7 @@ module.exports = {
505
530
  ESCAPE_HATCH: ESCAPE_HATCH,
506
531
  HOME_RECORDINGS: HOME_RECORDINGS,
507
532
  kryptheonReachableFrom: kryptheonReachableFrom,
533
+ kryptheonCopyFor: kryptheonCopyFor,
508
534
  kryptheonInstalledIn: kryptheonInstalledIn,
509
535
  projectAbove: projectAbove,
510
536
  setupPlan: setupPlan,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kryptheon",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",