kryptheon 0.1.34 → 0.1.36

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/LICENSE ADDED
@@ -0,0 +1,38 @@
1
+ Kryptheon Software License
2
+
3
+ Copyright (c) 2026 Kryptheon (https://kryptheon.tech). All rights reserved.
4
+
5
+ This software is proprietary. It is not open source.
6
+
7
+ 1. Use. You may install and run this software, as published by Kryptheon,
8
+ free of charge, to check software and databases that you own or are
9
+ authorised to test. Features that kryptheon.tech offers on a paid plan
10
+ are provided only under that plan and the Terms at
11
+ https://kryptheon.tech/terms.
12
+
13
+ 2. Restrictions. Without Kryptheon's written permission you may not:
14
+ (a) copy, modify, translate or create derivative works of this software,
15
+ except as needed to run it;
16
+ (b) sell, sublicense, rent, publish or otherwise distribute it, or offer
17
+ it, or anything derived from it, as a hosted or managed service;
18
+ (c) remove, disable or work around any licence, plan or usage limit, or
19
+ any part of the software that enforces them;
20
+ (d) use it, or any part of it, to build a competing product.
21
+
22
+ 3. No warranty. The software is provided "as is", without warranty of any
23
+ kind, express or implied, including fitness for a particular purpose.
24
+ It may miss problems; a check that finds nothing does not mean that
25
+ nothing is wrong. You are responsible for what you test and for keeping
26
+ your own backups.
27
+
28
+ 4. Limitation of liability. To the extent the law allows, Kryptheon is not
29
+ liable for any damages arising from the use of, or inability to use,
30
+ this software.
31
+
32
+ 5. Earlier versions. Versions published before this licence was added keep
33
+ the licence they were published with.
34
+
35
+ 6. Termination. This licence ends automatically if you break it. When it
36
+ ends, you must stop using the software and delete your copies.
37
+
38
+ Questions: company@kryptheon.tech
package/README.md CHANGED
@@ -70,6 +70,10 @@ npx kryptheon record https://your-app.example.com
70
70
  A browser opens. Use your app the way a customer would. Close the browser when
71
71
  you are done, and the recording is saved as a test.
72
72
 
73
+ Add `--phone` to record in a phone-sized window (390 x 844) - for a menu or a
74
+ layout that only changes on a phone. The recording keeps that size, so every
75
+ replay uses it too. On the dashboard it is the "Phone size" box under Record.
76
+
73
77
  On its own, a recording knows that every button and field is still there and
74
78
  that the last page looks the same. To say what must be true — "Reservation
75
79
  received" appears after booking — use the toolbar at the top of the page while
@@ -254,4 +258,4 @@ rather than refusing up front.
254
258
 
255
259
  ## Licence
256
260
 
257
- MIT
261
+ Free to use, not open source: see LICENSE. Paid features are on kryptheon.tech.
package/bin/kryptheon.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  // The kryptheon CLI. A thin wrapper so nobody has to remember Playwright's
3
3
  // command line or edit a config file. Everything here shells out to the
4
4
  // Playwright binary that ships with this package.
@@ -91,6 +91,7 @@ function usage() {
91
91
  console.log(' The same, for scripts and AI tools:');
92
92
  console.log('');
93
93
  console.log(' npx kryptheon record <url> open your app and record what you do as a test');
94
+ console.log(' ... <url> --phone the same in a phone-sized window, replayed at that size');
94
95
  console.log(' npx kryptheon check run every recorded test and report in plain language');
95
96
  console.log(' npx kryptheon accept <name> agree that one test\'s new result is the correct one');
96
97
  console.log(' npx kryptheon remove [name] list the recordings, or remove one you no longer want');
@@ -1527,7 +1528,28 @@ async function prepareFolder(url) {
1527
1528
  return makeReady(USER_DIR) ? null : 1;
1528
1529
  }
1529
1530
 
1530
- async function record(url) {
1531
+ // A phone's window: the size the recording is made at and replayed at.
1532
+ // Found on a blind test (Northstar): a mobile menu that never opened and a
1533
+ // card wider than the screen were only visible below 440px, and every
1534
+ // recording was made at desktop width.
1535
+ // Written with a comma: codegen's own help shows "1280x720", but codegen
1536
+ // itself refuses that ("use width,height") - measured on Playwright 1.62.
1537
+ const PHONE_SIZE = '390,844';
1538
+
1539
+ /**
1540
+ * What codegen is started with. At phone size, codegen writes
1541
+ * test.use({ viewport: ... }) into the recording itself, so every replay
1542
+ * opens at the size it was recorded at - nothing else has to remember it.
1543
+ */
1544
+ function codegenArgs(outFile, target, options) {
1545
+ const phone = options && options.phone;
1546
+ return ['codegen', '--target', 'playwright-test']
1547
+ .concat(phone ? ['--viewport-size=' + PHONE_SIZE] : [])
1548
+ .concat(['-o', outFile, target]);
1549
+ }
1550
+
1551
+ async function record(url, options) {
1552
+ const phone = Boolean(options && options.phone);
1531
1553
  if (!url) {
1532
1554
  console.error('');
1533
1555
  console.error(' Which address should I open?');
@@ -1571,7 +1593,8 @@ async function record(url) {
1571
1593
  const specPath = path.join(USER_DIR, outFile);
1572
1594
 
1573
1595
  console.log('');
1574
- console.log(' Opening ' + target + ' in a browser.');
1596
+ console.log(' Opening ' + target + (phone ? ' in a phone-sized window (' + PHONE_SIZE.replace(',', ' x ') + ').' : ' in a browser.'));
1597
+ if (phone) console.log(' It is replayed at the same size every time.');
1575
1598
  console.log('');
1576
1599
  console.log(' Use your app normally - click, type, sign in, whatever you want');
1577
1600
  console.log(' covered. Every step is recorded as you go.');
@@ -1579,7 +1602,7 @@ async function record(url) {
1579
1602
  console.log(' When you are done, close the browser window to save the test.');
1580
1603
  console.log('');
1581
1604
 
1582
- const child = startCodegen(['codegen', '--target', 'playwright-test', '-o', outFile, target]);
1605
+ const child = startCodegen(codegenArgs(outFile, target, { phone: phone }));
1583
1606
  if (!child) return 1;
1584
1607
 
1585
1608
  let stderr = '';
@@ -2450,10 +2473,11 @@ function recheckFromDashboard(id, opened, say) {
2450
2473
  }
2451
2474
 
2452
2475
  /** Records one flow the way `npx kryptheon record <address>` does, and names what it saved. */
2453
- function recordFromDashboard(address, say) {
2476
+ function recordFromDashboard(address, say, options) {
2477
+ const phone = Boolean(options && options.phone);
2454
2478
  const before = listSpecFiles();
2455
- say('A browser window opens on ' + address + '. Use your app, then close that window to save.');
2456
- return kryptheonChild(['record', address], say).then(({ code }) => {
2479
+ say('A ' + (phone ? 'phone-sized ' : '') + 'browser window opens on ' + address + '. Use your app, then close that window to save.');
2480
+ return kryptheonChild(['record', address].concat(phone ? ['--phone'] : []), say).then(({ code }) => {
2457
2481
  const added = listSpecFiles().filter((name) => !before.includes(name));
2458
2482
  if (!added.length) return { ok: false, exit: code, why: 'no recording was saved' };
2459
2483
  return { ok: true, exit: code, recordings: added, summary: 'saved ' + added.join(', ') };
@@ -2565,7 +2589,7 @@ async function dashboardCommand() {
2565
2589
  const app = dash.createServer(USER_DIR, {
2566
2590
  run: (id, say) => runFromDashboard(id, opened, say),
2567
2591
  recheck: (id, say) => recheckFromDashboard(id, opened, say),
2568
- record: (address, say) => recordFromDashboard(address, say),
2592
+ record: (address, say, options) => recordFromDashboard(address, say, options),
2569
2593
  db: (action, connection, say) => databaseFromDashboard(action, connection, say),
2570
2594
  words: (connection) => nightWords(connection),
2571
2595
  });
@@ -2867,7 +2891,7 @@ async function main() {
2867
2891
 
2868
2892
  switch (command) {
2869
2893
  case 'record':
2870
- return finishWith(await record(rest[0]));
2894
+ return finishWith(await record(rest.find((a) => !/^--/.test(a)), { phone: rest.includes('--phone') }));
2871
2895
  break;
2872
2896
  case 'check': {
2873
2897
  const handedOver = handToFolderCopy();
@@ -2919,6 +2943,8 @@ async function main() {
2919
2943
 
2920
2944
  // Exported so the naming logic can be checked without opening a browser.
2921
2945
  module.exports = {
2946
+ codegenArgs: codegenArgs,
2947
+ PHONE_SIZE: PHONE_SIZE,
2922
2948
  packageVersion: packageVersion,
2923
2949
  humanisePath: humanisePath,
2924
2950
  nameFromTitle: nameFromTitle,
package/kryptheon-code.js CHANGED
@@ -137,8 +137,11 @@ function calleeName(node) {
137
137
  // feeds another. It was 6, which a constant-only lookup (a template, a call,
138
138
  // its return, a map of maps) used up before reaching the strings at the
139
139
  // bottom, so a harmless label came back "unknown". Giving up always errs
140
- // towards reporting, never towards silence.
141
- const MAX_DEPTH = 12;
140
+ // towards reporting, never towards silence. It was 12 until a blind test
141
+ // (Northstar): a helper's parameter, back to its call inside a map() callback,
142
+ // to the list, a slice of a list reassigned from a filter of the fixed one -
143
+ // 13 steps. 18 leaves room for one more layer of the same.
144
+ const MAX_DEPTH = 18;
142
145
 
143
146
  function classify(node, scope, code, depth, aliases) {
144
147
  if (!node || depth > MAX_DEPTH) return 'unknown';
@@ -483,7 +486,7 @@ function fromBinding(id, scope, code, depth, aliases) {
483
486
  const binding = scope.getBinding(id.name);
484
487
  if (!binding) return 'unknown';
485
488
  if (binding.path && binding.path.type === 'CatchClause') return 'error';
486
- if (binding.kind === 'param') return fromCalls(binding, code, depth);
489
+ if (binding.kind === 'param') return fromCallback(binding, code, depth) || fromCalls(binding, code, depth);
487
490
  if (binding.kind === 'module') return fromImport(binding, depth);
488
491
  const init = binding.path && binding.path.node && binding.path.node.init;
489
492
  const later = binding.constantViolations || [];
@@ -518,6 +521,35 @@ function fromBinding(id, scope, code, depth, aliases) {
518
521
  return classify(init, binding.path.scope, code, depth + 1, aliases);
519
522
  }
520
523
 
524
+ // Array methods that hand their callback the list's own items.
525
+ const ITEM_CALLBACKS = /^(map|forEach|filter|find|findLast|findIndex|findLastIndex|some|every|flatMap|sort|toSorted)$/;
526
+
527
+ /**
528
+ * A parameter of a callback handed to list.map(...) and its kin: the item is
529
+ * whatever the list holds, the index a number, the third the list itself.
530
+ * Null when the parameter is not that.
531
+ *
532
+ * Found on a blind test (Northstar): pageRows.map(row => `...${severityClass(row.severity)}...`).
533
+ * The helper's parameter was followed to its call, and there `row` was a
534
+ * parameter of an arrow nobody names - "unknown" - though pageRows only ever
535
+ * held the file's own fixed list. The map().join() reading knew what row was;
536
+ * a name followed back into the same callback did not.
537
+ */
538
+ function fromCallback(binding, code, depth) {
539
+ const fn = binding.path && binding.path.parentPath;
540
+ if (!fn || !/Function/.test(fn.node.type)) return null;
541
+ const call = fn.parentPath;
542
+ if (!call || !/CallExpression$/.test(call.node.type) || call.node.arguments[0] !== fn.node) return null;
543
+ const method = calleeName(call.node.callee);
544
+ if (!/MemberExpression$/.test(call.node.callee.type) || !ITEM_CALLBACKS.test(method)) return null;
545
+ const index = fn.node.params.indexOf(binding.path.node);
546
+ const list = () => classify(call.node.callee.object, call.scope, code, depth + 1);
547
+ if (index === 0 || (index === 1 && /^(sort|toSorted)$/.test(method))) return list();
548
+ if (index === 1) return 'safe';
549
+ if (index === 2) return list();
550
+ return null;
551
+ }
552
+
521
553
  /**
522
554
  * A parameter is as dangerous as the worst thing any call passes it. Every
523
555
  * use of the function has to be a call this file makes: one handed on as a
@@ -155,6 +155,10 @@
155
155
  recordInput.placeholder = 'http://localhost:3000';
156
156
  recordInput.setAttribute('aria-label', 'Your app address');
157
157
  recordInput.spellcheck = false;
158
+ // Kept like the box beside it, so a page refresh does not untick it.
159
+ var phoneBox = el('input');
160
+ phoneBox.type = 'checkbox';
161
+ phoneBox.id = 'recordPhone';
158
162
  var dbInput = el('input', 'field');
159
163
  dbInput.type = 'password';
160
164
  dbInput.placeholder = 'postgresql://postgres.<project>:<password>@...pooler.supabase.com:5432/postgres';
@@ -441,7 +445,7 @@
441
445
  var go = button('sun sm', 'film', a.label, function () {
442
446
  var address = recordInput.value.trim();
443
447
  if (!address) { toast('Put your app\'s address in the box first - for example http://localhost:3000'); recordInput.focus(); return; }
444
- runJob('/api/record', { url: address }).then(function (done) {
448
+ runJob('/api/record', { url: address, phone: phoneBox.checked }).then(function (done) {
445
449
  var r = done && done.result;
446
450
  toast(r && r.ok ? 'Saved: ' + (r.recordings || []).join(', ') : 'Nothing was saved' + (r && r.why ? ' - ' + r.why : ''));
447
451
  }, function () {});
@@ -450,6 +454,11 @@
450
454
  go.disabled = busy;
451
455
  line.appendChild(go);
452
456
  wrap.appendChild(line);
457
+ var size = el('label', 'fallback');
458
+ size.setAttribute('for', 'recordPhone');
459
+ size.appendChild(phoneBox);
460
+ size.appendChild(document.createTextNode(' Phone size (390 x 844) - for menus and layouts that only change on a phone. Replayed at the same size.'));
461
+ wrap.appendChild(size);
453
462
  wrap.appendChild(el('div', 'fallback', 'or in a terminal: ' + a.command));
454
463
  return wrap;
455
464
  }
@@ -825,7 +825,9 @@ function createServer(root, options) {
825
825
  if (url.pathname === '/api/record' && typeof opts.record === 'function') {
826
826
  const address = appAddress(input.url);
827
827
  if (!address) return json(res, 400, { ok: false, why: 'that is not a web address - it should look like http://localhost:3000' });
828
- return begin('record', 'Recording ' + address, 'npx kryptheon record ' + address, (say) => opts.record(address, say));
828
+ const phone = input.phone === true;
829
+ return begin('record', 'Recording ' + address + (phone ? ' at phone size' : ''),
830
+ 'npx kryptheon record ' + address + (phone ? ' --phone' : ''), (say) => opts.record(address, say, { phone: phone }));
829
831
  }
830
832
  if (url.pathname === '/api/db' && typeof opts.db === 'function') {
831
833
  const action = String(input.action || '');
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "kryptheon",
3
- "version": "0.1.34",
3
+ "version": "0.1.36",
4
4
  "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
- "license": "MIT",
5
+ "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",
7
7
  "bin": {
8
8
  "kryptheon": "bin/kryptheon.js"
@@ -52,7 +52,7 @@
52
52
  "@playwright/test": "^1.62.1"
53
53
  },
54
54
  "scripts": {
55
- "check": "node kryptheon-store.check.js && node kryptheon-code.check.js && node kryptheon-constdata.check.js && node kryptheon-accept.check.js && node kryptheon-keys.check.js && node kryptheon-dashboard.check.js && node kryptheon-autostart.check.js && node kryptheon-changes.check.js && node kryptheon-look.check.js && node kryptheon-verify.check.js && node kryptheon-recheck.check.js && node kryptheon-reporter.check.js && node kryptheon-signature.check.js && node kryptheon-signature.stream.check.js && node kryptheon-signature.baseline.check.js && node kryptheon-signature.settle.check.js && node kryptheon-signature.number.check.js && node kryptheon-env.check.js && node kryptheon-watchdog.check.js && node kryptheon-record.check.js && node kryptheon-selectors.check.js && node kryptheon-recordings.check.js && node kryptheon-cleanup.check.js && node kryptheon-orphan.check.js && node kryptheon-project.check.js && node kryptheon-setup.check.js && node kryptheon-browser.check.js && node kryptheon-heal.check.js && node kryptheon-load.check.js && node kryptheon-package.check.js",
55
+ "check": "node kryptheon-store.check.js && node kryptheon-code.check.js && node kryptheon-constdata.check.js && node kryptheon-accept.check.js && node kryptheon-keys.check.js && node kryptheon-dashboard.check.js && node kryptheon-autostart.check.js && node kryptheon-changes.check.js && node kryptheon-look.check.js && node kryptheon-verify.check.js && node kryptheon-recheck.check.js && node kryptheon-reporter.check.js && node kryptheon-signature.check.js && node kryptheon-signature.stream.check.js && node kryptheon-signature.baseline.check.js && node kryptheon-signature.settle.check.js && node kryptheon-signature.number.check.js && node kryptheon-env.check.js && node kryptheon-watchdog.check.js && node kryptheon-phone.check.js && node kryptheon-record.check.js && node kryptheon-selectors.check.js && node kryptheon-recordings.check.js && node kryptheon-cleanup.check.js && node kryptheon-orphan.check.js && node kryptheon-project.check.js && node kryptheon-setup.check.js && node kryptheon-browser.check.js && node kryptheon-heal.check.js && node kryptheon-load.check.js && node kryptheon-package.check.js",
56
56
  "check:store": "node kryptheon-store.check.js",
57
57
  "check:code": "node kryptheon-code.check.js",
58
58
  "check:constdata": "node kryptheon-constdata.check.js",
@@ -89,12 +89,5 @@
89
89
  "end-to-end",
90
90
  "cli"
91
91
  ],
92
- "repository": {
93
- "type": "git",
94
- "url": "git+https://github.com/jhashantanu987-beep/kryptheon-cli.git"
95
- },
96
- "bugs": {
97
- "url": "https://github.com/jhashantanu987-beep/kryptheon-cli/issues"
98
- },
99
- "homepage": "https://github.com/jhashantanu987-beep/kryptheon-cli#readme"
92
+ "homepage": "https://kryptheon.tech"
100
93
  }