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 +116 -7
- package/kryptheon-project.js +26 -0
- package/package.json +1 -1
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
|
-
//
|
|
44
|
-
//
|
|
45
|
-
// the
|
|
46
|
-
|
|
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 === '
|
|
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
|
-
|
|
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,
|
package/kryptheon-project.js
CHANGED
|
@@ -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,
|