kryptheon 0.1.26 → 0.1.28
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/README.md +33 -7
- package/bin/kryptheon.js +199 -43
- package/kryptheon-dashboard-page.js +1026 -702
- package/kryptheon-dashboard.html +411 -378
- package/kryptheon-dashboard.js +864 -608
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -6,6 +6,30 @@ plain English what broke.
|
|
|
6
6
|
Runs entirely on your machine. No account, no API key, nothing leaves your
|
|
7
7
|
computer. It is a wrapper around [Playwright](https://playwright.dev).
|
|
8
8
|
|
|
9
|
+
## The one command
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
npx kryptheon@latest dashboard
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Run it in your project's folder. It opens a page in your browser, and
|
|
16
|
+
everything else is a button on that page:
|
|
17
|
+
|
|
18
|
+
| | how often |
|
|
19
|
+
| --- | --- |
|
|
20
|
+
| Read my code | after every change |
|
|
21
|
+
| Record a flow | one time - again only when that flow changes |
|
|
22
|
+
| Replay | after every change |
|
|
23
|
+
| Connect your database | one time |
|
|
24
|
+
| Check my database | after every change |
|
|
25
|
+
| Set up the nightly check | one time |
|
|
26
|
+
| Re-check a finding | after every change |
|
|
27
|
+
|
|
28
|
+
Every button shows the terminal command that does the same thing, in small
|
|
29
|
+
print, for when the page cannot be used. The database connection string is
|
|
30
|
+
pasted into the page, used for one run, and never saved, logged or shown
|
|
31
|
+
again; reload the page and it is gone.
|
|
32
|
+
|
|
9
33
|
## Only have a web address?
|
|
10
34
|
|
|
11
35
|
Built your app in Lovable, Bolt or v0, and there is no project folder on your
|
|
@@ -168,15 +192,17 @@ npx kryptheon
|
|
|
168
192
|
```
|
|
169
193
|
|
|
170
194
|
In a terminal, with no command, this opens a page on your own machine
|
|
171
|
-
(`http://127.0.0.1:4789/`) for this project
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
195
|
+
(`http://127.0.0.1:4789/`) for this project, in your browser by itself (the
|
|
196
|
+
address is printed too): what is broken, what needs a look, every run, recent
|
|
197
|
+
changes, and switches for each check. Every check runs from a button, one at a
|
|
198
|
+
time, with its progress shown on the page as it goes. It is only reachable
|
|
199
|
+
from this machine; press Ctrl+C to stop it. From a script, use
|
|
200
|
+
`npx kryptheon dashboard`; set `KRYPTHEON_NO_OPEN=1` to keep the browser shut.
|
|
176
201
|
|
|
177
202
|
It opens on the one thing to do next, and under it "Start here": read your
|
|
178
|
-
code, record your main flows, replay them, check your database
|
|
179
|
-
|
|
203
|
+
code, record your main flows, replay them, connect and check your database,
|
|
204
|
+
and set up the nightly check - each marked done only once it has actually run,
|
|
205
|
+
and each labelled "one time" or "after every change".
|
|
180
206
|
|
|
181
207
|
Checks that are not built yet are listed as "not available" - never as
|
|
182
208
|
passed.
|
package/bin/kryptheon.js
CHANGED
|
@@ -80,6 +80,16 @@ function usage() {
|
|
|
80
80
|
console.log('');
|
|
81
81
|
console.log(' kryptheon - record and check your app');
|
|
82
82
|
console.log('');
|
|
83
|
+
console.log(' The one command to remember:');
|
|
84
|
+
console.log('');
|
|
85
|
+
console.log(' npx kryptheon@latest dashboard');
|
|
86
|
+
console.log('');
|
|
87
|
+
console.log(' It opens a page in your browser, and everything else is a button there:');
|
|
88
|
+
console.log(' read your code, record and replay your flows, check your database, set up');
|
|
89
|
+
console.log(' the nightly check, and re-check a fix.');
|
|
90
|
+
console.log('');
|
|
91
|
+
console.log(' The same, for scripts and AI tools:');
|
|
92
|
+
console.log('');
|
|
83
93
|
console.log(' npx kryptheon record <url> open your app and record what you do as a test');
|
|
84
94
|
console.log(' npx kryptheon check run every recorded test and report in plain language');
|
|
85
95
|
console.log(' npx kryptheon accept <name> agree that one test\'s new result is the correct one');
|
|
@@ -88,7 +98,7 @@ function usage() {
|
|
|
88
98
|
console.log(' npx kryptheon verify after a change: what changed, what broke, what got fixed');
|
|
89
99
|
console.log(' npx kryptheon recheck [id] prove a fix: run the check again, and say FIXED or not');
|
|
90
100
|
console.log(' npx kryptheon code read your frontend code for text inserted as HTML');
|
|
91
|
-
console.log(' npx kryptheon dashboard
|
|
101
|
+
console.log(' npx kryptheon dashboard the page above (also: npx kryptheon, in a terminal)');
|
|
92
102
|
console.log('');
|
|
93
103
|
console.log(' add --quiet to check for one line when everything passes');
|
|
94
104
|
console.log(' npx kryptheon --version print the version you have installed');
|
|
@@ -2313,29 +2323,194 @@ function recheckCommand(id) {
|
|
|
2313
2323
|
|
|
2314
2324
|
const DASHBOARD_PORT = 4789;
|
|
2315
2325
|
|
|
2316
|
-
|
|
2326
|
+
/**
|
|
2327
|
+
* Runs a command and hands every line it prints to `say`, as it prints it -
|
|
2328
|
+
* so the page can show progress instead of a spinner. Nothing is read from
|
|
2329
|
+
* the keyboard: a command started from the page has nobody at one.
|
|
2330
|
+
*/
|
|
2331
|
+
function spawnLines(file, args, options, say) {
|
|
2332
|
+
return new Promise((resolve) => {
|
|
2333
|
+
let child;
|
|
2334
|
+
try {
|
|
2335
|
+
child = spawn(file, args, Object.assign({ cwd: USER_DIR, stdio: ['ignore', 'pipe', 'pipe'], windowsHide: true }, options || {}));
|
|
2336
|
+
} catch (err) {
|
|
2337
|
+
say('Could not start it: ' + err.message);
|
|
2338
|
+
resolve({ code: null, why: err.message });
|
|
2339
|
+
return;
|
|
2340
|
+
}
|
|
2341
|
+
const pending = { out: '', err: '' };
|
|
2342
|
+
const feed = (which) => (chunk) => {
|
|
2343
|
+
pending[which] += chunk.toString();
|
|
2344
|
+
const parts = pending[which].split(/\r?\n/);
|
|
2345
|
+
pending[which] = parts.pop();
|
|
2346
|
+
for (const line of parts) say(line);
|
|
2347
|
+
};
|
|
2348
|
+
child.stdout.on('data', feed('out'));
|
|
2349
|
+
child.stderr.on('data', feed('err'));
|
|
2350
|
+
child.on('error', (err) => {
|
|
2351
|
+
say('Could not start it: ' + err.message);
|
|
2352
|
+
resolve({ code: null, why: err.message });
|
|
2353
|
+
});
|
|
2354
|
+
child.on('close', (code) => {
|
|
2355
|
+
if (pending.out) say(pending.out);
|
|
2356
|
+
if (pending.err) say(pending.err);
|
|
2357
|
+
resolve({ code: code });
|
|
2358
|
+
});
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
|
|
2362
|
+
/** This same command, as a child, with these arguments. */
|
|
2363
|
+
function kryptheonChild(args, say) {
|
|
2364
|
+
return spawnLines(process.execPath, [path.join(PACKAGE_DIR, 'bin', 'kryptheon.js')].concat(args), {}, say);
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
/**
|
|
2368
|
+
* How kryptheon-night is started from the page: the newest one from npm, the
|
|
2369
|
+
* same as `npx kryptheon-night` in a terminal. KRYPTHEON_NIGHT_BIN points at
|
|
2370
|
+
* a copy on disk instead, for the checks. The arguments are only ever this
|
|
2371
|
+
* file's own fixed words - the connection string travels in the environment,
|
|
2372
|
+
* never on a command line, where other programs on the machine could read it.
|
|
2373
|
+
*/
|
|
2374
|
+
function nightChild(words, env, say) {
|
|
2375
|
+
const own = process.env.KRYPTHEON_NIGHT_BIN;
|
|
2376
|
+
const options = { env: env };
|
|
2377
|
+
if (own) return spawnLines(process.execPath, [own].concat(words), options, say);
|
|
2378
|
+
const npx = 'npx --yes kryptheon-night@latest ' + words.join(' ');
|
|
2379
|
+
if (process.platform === 'win32') {
|
|
2380
|
+
return spawnLines(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', npx], Object.assign({ windowsVerbatimArguments: true }, options), say);
|
|
2381
|
+
}
|
|
2382
|
+
return spawnLines('npx', ['--yes', 'kryptheon-night@latest'].concat(words), options, say);
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
/** The environment a child is given: this one, never with a connection string of its own. */
|
|
2386
|
+
function childEnv(extra) {
|
|
2387
|
+
const env = Object.assign({}, process.env, extra || {});
|
|
2388
|
+
if (!extra || !('KN_DATABASE_URL' in extra)) delete env.KN_DATABASE_URL;
|
|
2389
|
+
return env;
|
|
2390
|
+
}
|
|
2391
|
+
|
|
2392
|
+
// What each database button asks kryptheon-night to do. `--yes` only where
|
|
2393
|
+
// the page has already shown the same consent screen and been told yes.
|
|
2394
|
+
const NIGHT_WORDS = {
|
|
2395
|
+
scan: ['--yes'],
|
|
2396
|
+
install: ['install', '--yes'],
|
|
2397
|
+
night: ['night'],
|
|
2398
|
+
uninstall: ['uninstall'],
|
|
2399
|
+
};
|
|
2400
|
+
|
|
2401
|
+
const NIGHT_SAYS = { 0: 'nothing got through', 1: 'something got through - see Findings', 2: 'it could not check' };
|
|
2402
|
+
|
|
2403
|
+
function runFromDashboard(id, opened, say) {
|
|
2317
2404
|
if (id === 'frontend') {
|
|
2405
|
+
say('Reading the code in ' + USER_DIR + ' ...');
|
|
2318
2406
|
const result = readCodeAndSave(opened);
|
|
2319
|
-
|
|
2407
|
+
if (!result.ran) {
|
|
2408
|
+
say(result.why);
|
|
2409
|
+
return { ok: false, why: result.why };
|
|
2410
|
+
}
|
|
2411
|
+
const n = (result.findings || []).length;
|
|
2412
|
+
say('Read ' + (result.filesRead || 0) + ((result.filesRead || 0) === 1 ? ' file. ' : ' files. ') + (n ? n + ' thing' + (n === 1 ? '' : 's') + ' to look at.' : 'Nothing found.'));
|
|
2413
|
+
return { ok: true, summary: n ? n + ' thing' + (n === 1 ? '' : 's') + ' to look at' : 'nothing found' };
|
|
2320
2414
|
}
|
|
2321
2415
|
if (id === 'regression') {
|
|
2322
2416
|
// The same command a person types, as a separate process, so the page
|
|
2323
2417
|
// stays answerable while a browser replays the flows.
|
|
2324
|
-
return
|
|
2325
|
-
const child = spawn(process.execPath, [path.join(PACKAGE_DIR, 'bin', 'kryptheon.js'), 'check', '--quiet'], {
|
|
2326
|
-
cwd: USER_DIR,
|
|
2327
|
-
stdio: 'ignore',
|
|
2328
|
-
env: process.env,
|
|
2329
|
-
});
|
|
2330
|
-
child.on('error', (err) => resolve({ ok: false, why: err.message }));
|
|
2418
|
+
return kryptheonChild(['check'], say).then(({ code }) =>
|
|
2331
2419
|
// A run that found broken flows is a run that finished; its results are
|
|
2332
2420
|
// in the history the page reads next.
|
|
2333
|
-
|
|
2334
|
-
|
|
2421
|
+
code === 0 ? { ok: true, summary: 'every flow held' }
|
|
2422
|
+
: code === 1 ? { ok: true, summary: 'a flow broke - see Findings' }
|
|
2423
|
+
: { ok: false, why: 'the replay stopped with exit ' + code });
|
|
2335
2424
|
}
|
|
2336
2425
|
return { ok: false, why: '"' + id + '" cannot be run from the dashboard' };
|
|
2337
2426
|
}
|
|
2338
2427
|
|
|
2428
|
+
/** Re-checks one finding the way `npx kryptheon recheck <id>` does, and reads back its verdict. */
|
|
2429
|
+
function recheckFromDashboard(id, opened, say) {
|
|
2430
|
+
// Only a verdict written by this re-check counts: an older one for the
|
|
2431
|
+
// same finding, read after a child that failed early, would be a stale
|
|
2432
|
+
// answer shown as a fresh one.
|
|
2433
|
+
const startedAt = new Date().toISOString();
|
|
2434
|
+
return kryptheonChild(['recheck', id], say).then(() => {
|
|
2435
|
+
let last = null;
|
|
2436
|
+
try {
|
|
2437
|
+
const lines = fs.readFileSync(opened.fixes, 'utf8').split(String.fromCharCode(10)).filter(Boolean);
|
|
2438
|
+
for (let i = lines.length - 1; i >= 0 && !last; i--) {
|
|
2439
|
+
const a = JSON.parse(lines[i]);
|
|
2440
|
+
if (a.id === id && a.at >= startedAt) last = a;
|
|
2441
|
+
}
|
|
2442
|
+
} catch (err) {
|
|
2443
|
+
last = null;
|
|
2444
|
+
}
|
|
2445
|
+
if (!last) return { ok: false, why: 'the re-check did not record a verdict' };
|
|
2446
|
+
return Object.assign({ ok: true, summary: last.verdict }, last);
|
|
2447
|
+
});
|
|
2448
|
+
}
|
|
2449
|
+
|
|
2450
|
+
/** Records one flow the way `npx kryptheon record <address>` does, and names what it saved. */
|
|
2451
|
+
function recordFromDashboard(address, say) {
|
|
2452
|
+
const before = listSpecFiles();
|
|
2453
|
+
say('A browser window opens on ' + address + '. Use your app, then close that window to save.');
|
|
2454
|
+
return kryptheonChild(['record', address], say).then(({ code }) => {
|
|
2455
|
+
const added = listSpecFiles().filter((name) => !before.includes(name));
|
|
2456
|
+
if (!added.length) return { ok: false, exit: code, why: 'no recording was saved' };
|
|
2457
|
+
return { ok: true, exit: code, recordings: added, summary: 'saved ' + added.join(', ') };
|
|
2458
|
+
});
|
|
2459
|
+
}
|
|
2460
|
+
|
|
2461
|
+
/** One database action, through kryptheon-night, with the string in its environment only. */
|
|
2462
|
+
function databaseFromDashboard(action, connection, say) {
|
|
2463
|
+
return nightChild(NIGHT_WORDS[action], childEnv({ KN_DATABASE_URL: connection }), say).then(({ code }) => {
|
|
2464
|
+
if (code === null || code === undefined) return { ok: false, why: 'kryptheon-night could not be started' };
|
|
2465
|
+
if (action === 'scan') return { ok: code !== 2, exit: code, summary: NIGHT_SAYS[code] || 'it ended with exit ' + code };
|
|
2466
|
+
return code === 2 && action !== 'night'
|
|
2467
|
+
? { ok: false, exit: code, why: 'it could not do that - the lines above say why' }
|
|
2468
|
+
: code === 2 ? { ok: false, exit: code, why: 'nothing to read back - the lines above say why' }
|
|
2469
|
+
: { ok: true, exit: code, summary: 'done' };
|
|
2470
|
+
});
|
|
2471
|
+
}
|
|
2472
|
+
|
|
2473
|
+
/** What kryptheon-night itself would say - help, consent, warnings - without connecting. */
|
|
2474
|
+
function nightWords(connection) {
|
|
2475
|
+
const said = [];
|
|
2476
|
+
return nightChild(['words'], childEnv(connection ? { KN_DATABASE_URL: connection } : {}), (line) => said.push(line))
|
|
2477
|
+
.then(({ code }) => {
|
|
2478
|
+
for (const line of said) {
|
|
2479
|
+
if (!/^\s*\{/.test(line)) continue;
|
|
2480
|
+
try {
|
|
2481
|
+
return JSON.parse(line);
|
|
2482
|
+
} catch (err) {
|
|
2483
|
+
/* not the JSON line */
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
throw new Error('kryptheon-night did not answer (exit ' + code + ')');
|
|
2487
|
+
});
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
/**
|
|
2491
|
+
* Opens the page in the person's own browser. Not when KRYPTHEON_NO_OPEN or
|
|
2492
|
+
* CI is set - a script or a check has nobody to show a page to - and never
|
|
2493
|
+
* fatal: the address is printed either way.
|
|
2494
|
+
*/
|
|
2495
|
+
function openInBrowser(url) {
|
|
2496
|
+
// KRYPTHEON_BROWSER names the program to open it with instead (a .js file
|
|
2497
|
+
// is run with Node) - for a browser of one's own choosing, and the checks.
|
|
2498
|
+
const chosen = process.env.KRYPTHEON_BROWSER;
|
|
2499
|
+
if (process.env.KRYPTHEON_NO_OPEN || (process.env.CI && !chosen)) return false;
|
|
2500
|
+
const how = chosen ? (/\.[cm]?js$/i.test(chosen) ? [process.execPath, [chosen, url]] : [chosen, [url]])
|
|
2501
|
+
: process.platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
|
|
2502
|
+
: process.platform === 'darwin' ? ['open', [url]]
|
|
2503
|
+
: ['xdg-open', [url]];
|
|
2504
|
+
try {
|
|
2505
|
+
const child = spawn(how[0], how[1], { stdio: 'ignore', detached: true, windowsHide: true });
|
|
2506
|
+
child.on('error', () => {});
|
|
2507
|
+
child.unref();
|
|
2508
|
+
return true;
|
|
2509
|
+
} catch (err) {
|
|
2510
|
+
return false;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
|
|
2339
2514
|
async function dashboardCommand() {
|
|
2340
2515
|
const here = project.inspectProject(USER_DIR);
|
|
2341
2516
|
if (!here.ok) {
|
|
@@ -2346,34 +2521,11 @@ async function dashboardCommand() {
|
|
|
2346
2521
|
if (!opened) return 1;
|
|
2347
2522
|
const dash = require(path.join(PACKAGE_DIR, 'kryptheon-dashboard.js'));
|
|
2348
2523
|
const app = dash.createServer(USER_DIR, {
|
|
2349
|
-
run: (id) => runFromDashboard(id, opened),
|
|
2350
|
-
|
|
2351
|
-
|
|
2352
|
-
|
|
2353
|
-
|
|
2354
|
-
// same finding, read after a child that failed early, would be a stale
|
|
2355
|
-
// answer shown as a fresh one.
|
|
2356
|
-
const startedAt = new Date().toISOString();
|
|
2357
|
-
const child = spawn(process.execPath, [path.join(PACKAGE_DIR, 'bin', 'kryptheon.js'), 'recheck', id], {
|
|
2358
|
-
cwd: USER_DIR,
|
|
2359
|
-
stdio: 'ignore',
|
|
2360
|
-
env: process.env,
|
|
2361
|
-
});
|
|
2362
|
-
child.on('error', (err) => resolve({ ok: false, why: err.message }));
|
|
2363
|
-
child.on('close', () => {
|
|
2364
|
-
let last = null;
|
|
2365
|
-
try {
|
|
2366
|
-
const lines = fs.readFileSync(opened.fixes, 'utf8').split(String.fromCharCode(10)).filter(Boolean);
|
|
2367
|
-
for (let i = lines.length - 1; i >= 0 && !last; i--) {
|
|
2368
|
-
const a = JSON.parse(lines[i]);
|
|
2369
|
-
if (a.id === id && a.at >= startedAt) last = a;
|
|
2370
|
-
}
|
|
2371
|
-
} catch (err) {
|
|
2372
|
-
last = null;
|
|
2373
|
-
}
|
|
2374
|
-
resolve(last || { ok: false, why: 'the re-check did not record a verdict' });
|
|
2375
|
-
});
|
|
2376
|
-
}),
|
|
2524
|
+
run: (id, say) => runFromDashboard(id, opened, say),
|
|
2525
|
+
recheck: (id, say) => recheckFromDashboard(id, opened, say),
|
|
2526
|
+
record: (address, say) => recordFromDashboard(address, say),
|
|
2527
|
+
db: (action, connection, say) => databaseFromDashboard(action, connection, say),
|
|
2528
|
+
words: (connection) => nightWords(connection),
|
|
2377
2529
|
});
|
|
2378
2530
|
let port;
|
|
2379
2531
|
try {
|
|
@@ -2382,13 +2534,17 @@ async function dashboardCommand() {
|
|
|
2382
2534
|
// Taken - by another project's dashboard, most likely. Any free port will do.
|
|
2383
2535
|
port = await app.listen(0);
|
|
2384
2536
|
}
|
|
2537
|
+
const address = 'http://127.0.0.1:' + port + '/';
|
|
2538
|
+
const shown = openInBrowser(address);
|
|
2385
2539
|
console.log('');
|
|
2386
2540
|
console.log(' Kryptheon dashboard for ' + path.basename(USER_DIR) + ':');
|
|
2387
2541
|
console.log('');
|
|
2388
|
-
console.log('
|
|
2542
|
+
console.log(' ' + address);
|
|
2389
2543
|
console.log('');
|
|
2390
|
-
console.log('
|
|
2391
|
-
|
|
2544
|
+
console.log(shown ? ' It is opening in your browser. If it does not, open that address.'
|
|
2545
|
+
: ' Open that address in your browser.');
|
|
2546
|
+
console.log(' It is only reachable from this machine. Everything else is a button');
|
|
2547
|
+
console.log(' on that page. Press Ctrl+C here to stop it.');
|
|
2392
2548
|
console.log('');
|
|
2393
2549
|
|
|
2394
2550
|
// While the page is open, Kryptheon notices changes by itself: once a change
|