kryptheon 0.1.11 → 0.1.13

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 CHANGED
@@ -6,6 +6,27 @@ 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
+ ## Only have a web address?
10
+
11
+ Built your app in Lovable, Bolt or v0, and there is no project folder on your
12
+ computer? Open a terminal and run:
13
+
14
+ ```
15
+ npx kryptheon record https://your-app.lovable.app
16
+ ```
17
+
18
+ Before it opens anything, it looks at the folder you are in. If that folder is
19
+ not set up for recordings it says so and asks first, then adds a `package.json`
20
+ and kryptheon itself (about 20MB) — nothing else. A new terminal opens in your
21
+ home folder, so from there it keeps your recordings in a `kryptheon-tests`
22
+ folder of their own and tells you where that is.
23
+
24
+ After every change to your app, from that same folder:
25
+
26
+ ```
27
+ npx kryptheon check
28
+ ```
29
+
9
30
  ## Install
10
31
 
11
32
  ```
package/bin/kryptheon.js CHANGED
@@ -1116,7 +1116,9 @@ async function finaliseRecording(outFile, context) {
1116
1116
  const named = await nameRecording(outFile);
1117
1117
  console.log('');
1118
1118
  console.log(' Saved to ' + named);
1119
- console.log(' Run it any time with: kryptheon check');
1119
+ // npx, not the bare name: somebody who started with `npx kryptheon record`
1120
+ // has no `kryptheon` command, and would be told it does not exist.
1121
+ console.log(' Run it any time with: npx kryptheon check');
1120
1122
  console.log('');
1121
1123
 
1122
1124
  reportTidying(tidied);
@@ -1300,6 +1302,122 @@ async function remove(name) {
1300
1302
  return failed.length ? 1 : 0;
1301
1303
  }
1302
1304
 
1305
+ // --- setting the folder up --------------------------------------------------
1306
+ //
1307
+ // See the setup section of kryptheon-project.js for why this exists. That file
1308
+ // decides; this one asks, writes and installs.
1309
+
1310
+ function writeOwnPackageJson(dir) {
1311
+ // 'wx': never over the top of one that appeared since the folder was looked
1312
+ // at. Somebody's package.json is not ours to replace.
1313
+ try {
1314
+ fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify({ private: true }, null, 2) + '\n', { flag: 'wx' });
1315
+ return true;
1316
+ } catch (err) {
1317
+ return err.code === 'EEXIST';
1318
+ }
1319
+ }
1320
+
1321
+ function installKryptheonIn(dir) {
1322
+ console.log(' Installing kryptheon here - this usually takes under a minute.');
1323
+ const args = ['install', '--save-dev', 'kryptheon', '--no-audit', '--no-fund', '--loglevel=error'];
1324
+ // npm is npm.cmd on Windows, and Node will not start a .cmd directly
1325
+ // (spawnSync EINVAL), so there it goes through cmd.exe. Every argument is a
1326
+ // fixed string; nothing anybody typed reaches this command line.
1327
+ const result = process.platform === 'win32'
1328
+ ? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', 'npm'].concat(args), { cwd: dir, encoding: 'utf8' })
1329
+ : spawnSync('npm', args, { cwd: dir, encoding: 'utf8' });
1330
+ // Judged by what is on disk afterwards, not by npm's exit code alone.
1331
+ const ok = !result.error && result.status === 0 && project.kryptheonInstalledIn(dir);
1332
+ if (ok) {
1333
+ console.log(' Installed.');
1334
+ console.log('');
1335
+ }
1336
+ return ok;
1337
+ }
1338
+
1339
+ // A folder `record` is about to use, made ready: its own package.json, its own
1340
+ // copy of kryptheon. Returns false, having said why, if either did not happen.
1341
+ function makeReady(dir) {
1342
+ if (!project.hasPackageJson(dir) && !writeOwnPackageJson(dir)) {
1343
+ console.error('');
1344
+ console.error(' Could not create package.json in: ' + dir);
1345
+ console.error(' This folder may be read-only. Make a new folder of your own and');
1346
+ console.error(' run the same command from there.');
1347
+ console.error('');
1348
+ return false;
1349
+ }
1350
+ if (!project.kryptheonInstalledIn(dir) && !installKryptheonIn(dir)) {
1351
+ for (const line of project.installFailedLines()) console.error(line);
1352
+ return false;
1353
+ }
1354
+ return true;
1355
+ }
1356
+
1357
+ // Run again from inside the new folder rather than moving this process there:
1358
+ // every path in this file is worked out once, from the folder the command
1359
+ // started in.
1360
+ async function recordFromHome(plan, url) {
1361
+ try {
1362
+ fs.mkdirSync(plan.into, { recursive: true });
1363
+ } catch (err) {
1364
+ console.error('');
1365
+ console.error(' Could not make the folder: ' + plan.into);
1366
+ console.error(' Make a new folder of your own, open a terminal in it, and run the');
1367
+ console.error(' same command again.');
1368
+ console.error('');
1369
+ return 1;
1370
+ }
1371
+ if (!makeReady(plan.into)) return 1;
1372
+
1373
+ const child = spawn(process.execPath, [__filename, 'record', url], { cwd: plan.into, stdio: 'inherit' });
1374
+ // Ctrl-C reaches the child too, and the child is the one that saves what was
1375
+ // recorded. This process waits for it rather than exiting under it.
1376
+ const wait = () => {};
1377
+ process.on('SIGINT', wait);
1378
+ process.on('SIGTERM', wait);
1379
+ const code = await new Promise((resolve) => {
1380
+ child.on('error', () => resolve(1));
1381
+ child.on('close', (status) => resolve(status === null ? 1 : status));
1382
+ });
1383
+ process.removeListener('SIGINT', wait);
1384
+ process.removeListener('SIGTERM', wait);
1385
+
1386
+ if (code === 0) for (const line of project.homeDoneLines(plan.into)) console.log(line);
1387
+ return code;
1388
+ }
1389
+
1390
+ // Before the browser: it is a 200MB download on a new machine, and a
1391
+ // recording that cannot be checked afterwards is somebody's five minutes
1392
+ // thrown away. Returns null to carry on recording, or an exit code to stop.
1393
+ async function prepareFolder(url) {
1394
+ const plan = project.setupPlan(USER_DIR);
1395
+ if (plan.action === 'ready') return null;
1396
+
1397
+ const refusing = plan.action === 'refuse';
1398
+ for (const line of project.setupLines(plan, url)) (refusing ? console.error : console.log)(line);
1399
+ if (refusing) return 1;
1400
+
1401
+ // Asked where there is a keyboard. Where there is not, the question's own
1402
+ // default is taken rather than refusing: isTTY is false in plenty of
1403
+ // perfectly good terminals (see noWindowLines), and a command that refuses
1404
+ // there can never work for the person who ran it. The one question whose
1405
+ // default is no - a second project inside somebody's first - stays no.
1406
+ const question = project.setupQuestion(plan);
1407
+ const yes = process.stdin.isTTY
1408
+ ? project.answeredYes(await askLine(question.text), question.yesByDefault)
1409
+ : question.yesByDefault;
1410
+ if (!yes) {
1411
+ console.log('');
1412
+ console.log(' Nothing was changed, and nothing was recorded.');
1413
+ console.log('');
1414
+ return 1;
1415
+ }
1416
+
1417
+ if (plan.action === 'home') return await recordFromHome(plan, url);
1418
+ return makeReady(USER_DIR) ? null : 1;
1419
+ }
1420
+
1303
1421
  async function record(url) {
1304
1422
  if (!url) {
1305
1423
  console.error('');
@@ -1320,6 +1438,11 @@ async function record(url) {
1320
1438
  return 1;
1321
1439
  }
1322
1440
 
1441
+ // After the address is known to answer, so a typo is not the thing that
1442
+ // gets a folder set up; before the browser, which is the expensive part.
1443
+ const setup = await prepareFolder(url);
1444
+ if (setup !== null) return setup;
1445
+
1323
1446
  if (!ensureBrowser()) return 1;
1324
1447
 
1325
1448
  const existingBefore = listSpecFiles();
@@ -1429,20 +1552,10 @@ async function record(url) {
1429
1552
  // Recordings import the fixture by package name, which only resolves if
1430
1553
  // kryptheon is in the user's own node_modules. Running through a bare `npx`
1431
1554
  // puts the package somewhere the tests cannot see.
1432
- // Deliberately a filesystem walk, not require.resolve: this file lives inside
1433
- // the kryptheon package, and a package with a name and an "exports" map can
1434
- // always resolve itself by name, so require.resolve would answer "yes" even
1435
- // when the user's tests have no way to find it.
1555
+ // One walk, shared with record's setup, so that record and check can never
1556
+ // disagree about whether a folder is ready.
1436
1557
  function fixtureResolvesForUser() {
1437
- let dir = USER_DIR;
1438
- for (;;) {
1439
- if (fs.existsSync(path.join(dir, 'node_modules', 'kryptheon', 'kryptheon-fixture.js'))) {
1440
- return true;
1441
- }
1442
- const parent = path.dirname(dir);
1443
- if (parent === dir) return false;
1444
- dir = parent;
1445
- }
1558
+ return project.kryptheonReachableFrom(USER_DIR);
1446
1559
  }
1447
1560
 
1448
1561
  // Only a problem if a recording actually asks for it: specs written against a
@@ -1956,6 +2069,7 @@ module.exports = {
1956
2069
  applyRules: applyRules,
1957
2070
  rulesBlock: rulesBlock,
1958
2071
  record: record,
2072
+ prepareFolder: prepareFolder,
1959
2073
  finaliseRecording: finaliseRecording,
1960
2074
  takeOutSecrets: takeOutSecrets,
1961
2075
  reportSecrets: reportSecrets,