kryptheon 0.1.19 → 0.1.20

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
@@ -161,20 +161,28 @@ wanted, and one new name of the same kind. A button that is gone with nothing
161
161
  in its place, or a rename next to other new buttons, fails as it always did.
162
162
  Checks you added with the toolbar are never redirected.
163
163
 
164
- ## Files it creates in your folder
164
+ ## What goes where
165
165
 
166
- | File | What it is |
167
- | --- | --- |
168
- | `tests/` | your recordings |
169
- | `kryptheon-baselines.json` | the remembered result for each test |
170
- | `kryptheon-history.jsonl` | one line per run, used for "this was working on …" |
166
+ Only your recordings go in your project, in `tests/`. Everything Kryptheon
167
+ remembers about the project is kept outside it, so it can never end up in your
168
+ repo:
169
+
170
+ ~/.kryptheon/projects/<folder-name>-<id>/
171
+ baselines.json the remembered result for each test
172
+ history.jsonl one line per run, used for "this was working on …"
173
+ test-results/ the screenshot of the last failure
174
+
175
+ Set `KRYPTHEON_HOME` to keep them somewhere else. Each project folder gets its
176
+ own store, so two projects are never mixed. A project folder that is moved or
177
+ renamed starts a new store.
178
+
179
+ Older versions kept `kryptheon-baselines.json`, `kryptheon-history.jsonl` and
180
+ `test-results/` in the project. The first run of this version moves them out,
181
+ once, and says so.
171
182
 
172
183
  A `.env` in the same folder is loaded automatically, and anything you type into a
173
184
  password box is replaced with an environment variable rather than written into
174
- the test.
175
-
176
- Worth adding to `.gitignore`: `.env`, `kryptheon-baselines.json`,
177
- `kryptheon-history.jsonl`, `test-results/`.
185
+ the test. When there is a password, `.env` is added to an existing `.gitignore`.
178
186
 
179
187
  ## Requirements
180
188
 
package/bin/kryptheon.js CHANGED
@@ -5,8 +5,11 @@
5
5
  //
6
6
  // Two roots matter, and they are not the same once this is installed:
7
7
  // PACKAGE_DIR - the tool's own files (fixture, reporter, config, this file)
8
- // USER_DIR - the folder the command was run in, which owns tests/,
9
- // kryptheon-baselines.json, kryptheon-history.jsonl and .env
8
+ // USER_DIR - the folder the command was run in, which owns tests/ and .env
9
+ //
10
+ // Everything Kryptheon remembers about USER_DIR - baselines, history, the last
11
+ // run's screenshots - is kept in its store under ~/.kryptheon, never in the
12
+ // project (kryptheon-store.js).
10
13
 
11
14
  const fs = require('fs');
12
15
  const path = require('path');
@@ -19,6 +22,11 @@ const recordings = require('../kryptheon-recordings.js');
19
22
  const cleanup = require('../kryptheon-cleanup.js');
20
23
  const baselines = require('../kryptheon-baselines.js');
21
24
  const project = require('../kryptheon-project.js');
25
+ const store = require('../kryptheon-store.js');
26
+
27
+ // The first version that keeps its records in the store. An older copy in a
28
+ // project folder still looks for them inside the project.
29
+ const FIRST_STORE_VERSION = '0.1.20';
22
30
 
23
31
  const PACKAGE_DIR = path.join(__dirname, '..');
24
32
  const USER_DIR = process.cwd();
@@ -864,15 +872,49 @@ function takeOutSecrets(relativeFile) {
864
872
  return writeSpec(relativeFile, result.source) ? result.replacements : [];
865
873
  }
866
874
 
867
- // kryptheon leaves a password file and three files of machine state in the
868
- // project. If there is already a .gitignore, they belong in it.
869
- function keepArtefactsOutOfGit() {
875
+ // The password file is the one thing of kryptheon's in a project that must not
876
+ // be committed. If there is already a .gitignore, it belongs in it - but only
877
+ // once there is a password to protect: a .env here, or a recording that just
878
+ // had one taken out. A landing page with no sign-in has nothing to hide, and
879
+ // an edit to its .gitignore would be one more change of Kryptheon's in the repo.
880
+ function keepArtefactsOutOfGit(passwordTakenOut) {
881
+ if (!passwordTakenOut && !fs.existsSync(path.join(USER_DIR, '.env'))) return;
870
882
  const added = secrets.updateGitignore(USER_DIR);
871
883
  if (!added.length) return;
872
884
  console.log(' Added to .gitignore: ' + added.join(', '));
873
885
  console.log('');
874
886
  }
875
887
 
888
+ // The project's store, opened before anything reads a record, so whatever an
889
+ // older version left in this folder is moved out first - and said once, here,
890
+ // rather than by the fixture or the reporter, which run inside the test runner.
891
+ //
892
+ // Returns null when the store cannot be made. Carrying on would mean every
893
+ // recording is compared against nothing, which reads as "all fine" and is
894
+ // the one answer that must never be given by accident.
895
+ function openStore() {
896
+ let opened;
897
+ try {
898
+ opened = store.open(USER_DIR);
899
+ } catch (err) {
900
+ console.error('');
901
+ console.error(' Kryptheon keeps what it remembers about this project in');
902
+ console.error(' ' + store.dirFor(USER_DIR));
903
+ console.error(' and could not write there: ' + err.message);
904
+ console.error('');
905
+ console.error(' Without it every recording would look new, so nothing was checked.');
906
+ console.error(' Set KRYPTHEON_HOME to a folder you can write to, and run this again.');
907
+ console.error('');
908
+ return null;
909
+ }
910
+ const lines = store.migrationLines(opened);
911
+ if (lines.length) {
912
+ console.log('');
913
+ for (const line of lines) console.log(line);
914
+ }
915
+ return opened;
916
+ }
917
+
876
918
  function reportSecrets(replacements) {
877
919
  if (!replacements || !replacements.length) return;
878
920
  const names = replacements.map((item) => item.envName);
@@ -1170,7 +1212,7 @@ async function finaliseRecording(outFile, context) {
1170
1212
 
1171
1213
  reportTidying(tidied);
1172
1214
  reportSecrets(secrets);
1173
- keepArtefactsOutOfGit();
1215
+ keepArtefactsOutOfGit(!!(secrets && secrets.length));
1174
1216
  await offerToDropLogout(named);
1175
1217
  reportReplayRisks(named);
1176
1218
  reportFragileSelectors(named);
@@ -1321,6 +1363,7 @@ async function remove(name) {
1321
1363
  console.error("");
1322
1364
  return 1;
1323
1365
  }
1366
+ if (!openStore()) return 1;
1324
1367
 
1325
1368
  const gone = [];
1326
1369
  const failed = [];
@@ -1494,6 +1537,9 @@ async function record(url) {
1494
1537
  // gets a folder set up; before the browser, which is the expensive part.
1495
1538
  const setup = await prepareFolder(url);
1496
1539
  if (setup !== null) return setup;
1540
+ // Before an old recording can be replaced, because replacing one also drops
1541
+ // its saved result - which has to be found where it now lives.
1542
+ if (!openStore()) return 1;
1497
1543
 
1498
1544
  if (!ensureBrowser()) return 1;
1499
1545
 
@@ -1705,6 +1751,7 @@ function check(options) {
1705
1751
  for (const line of project.noProjectLines(here)) console.error(line);
1706
1752
  return 1;
1707
1753
  }
1754
+ if (!openStore()) return 1;
1708
1755
 
1709
1756
  // Deliberately not a failure. An assistant told to run this after every
1710
1757
  // change will read a non-zero exit as "something broke" and start fixing
@@ -1983,6 +2030,9 @@ function listBaselines(api) {
1983
2030
  }
1984
2031
 
1985
2032
  function accept(name) {
2033
+ // Only where there are recordings: anywhere else there is nothing to accept,
2034
+ // and no reason to make a store for a folder that is not a project.
2035
+ if (fs.existsSync(TESTS_DIR) && !openStore()) return 1;
1986
2036
  const api = baselineApi();
1987
2037
  if (!api) return 1;
1988
2038
  if (!name) return listBaselines(api);
@@ -2121,6 +2171,26 @@ function handToFolderCopy() {
2121
2171
  }
2122
2172
  }
2123
2173
 
2174
+ // A copy from before the store looks for its records inside the project. If
2175
+ // they have already moved out, it finds none, takes every recording as new,
2176
+ // and learns whatever the app does today as correct - a broken page would
2177
+ // pass. So it is not run; the one fix is to bring it up to date.
2178
+ if (project.olderVersion(theirVersion, FIRST_STORE_VERSION)) {
2179
+ const kept = store.pathsFor(USER_DIR);
2180
+ if (fs.existsSync(kept.baselines) || fs.existsSync(kept.history)) {
2181
+ console.error('');
2182
+ console.error(' This folder has kryptheon ' + theirVersion + ', which looks for its saved results');
2183
+ console.error(' inside the project. They have moved out, to');
2184
+ console.error(' ' + kept.dir);
2185
+ console.error(' so ' + theirVersion + ' would see none, treat every recording as new, and could');
2186
+ console.error(' miss a real change. Nothing was checked. Update this folder\'s copy:');
2187
+ console.error(' npm i -D kryptheon@latest');
2188
+ console.error(' then run this again.');
2189
+ console.error('');
2190
+ return 1;
2191
+ }
2192
+ }
2193
+
2124
2194
  if (theirVersion !== mine) {
2125
2195
  // Not an error: the folder's copy is the one its recordings are written
2126
2196
  // against. Said, so an older one is never mistaken for this one.
@@ -26,10 +26,13 @@ const heal = require('./kryptheon-heal.js');
26
26
 
27
27
  const MAX_ITEMS = 5; // keep the failure block readable
28
28
 
29
- // Baselines describe the user's app, so they live in the folder the command
30
- // was run from - never inside the installed package.
29
+ const store = require('./kryptheon-store.js');
30
+
31
+ // Baselines describe the user's app, so they belong to the folder the command
32
+ // was run from - but they are kept in that folder's store, outside it, so the
33
+ // project's repo never grows because Kryptheon is watching it.
31
34
  const USER_DIR = process.cwd();
32
- const BASELINE_FILE = path.join(USER_DIR, 'kryptheon-baselines.json');
35
+ const BASELINE_FILE = store.pathsFor(USER_DIR).baselines;
33
36
 
34
37
  function oneLine(text) {
35
38
  return String(text).replace(/\s+/g, ' ').trim();
@@ -78,6 +81,7 @@ function saveBaselines(file, all) {
78
81
  try {
79
82
  // Explicit, and without a byte order mark: this file is read back by the
80
83
  // same code and by people, and a mismatch would look like the app changed.
84
+ fs.mkdirSync(path.dirname(file), { recursive: true });
81
85
  fs.writeFileSync(file, JSON.stringify(all, null, 2) + '\n', { encoding: 'utf8' });
82
86
  return true;
83
87
  } catch (e) {
@@ -1,20 +1,29 @@
1
1
  // Plain-language Playwright reporter for non-technical readers.
2
2
  // Prints a short line per passing test and a readable block per failure,
3
- // and appends one JSON line per run to kryptheon-history.jsonl.
3
+ // and appends one JSON line per run to the project's history.jsonl.
4
4
 
5
5
  const fs = require('fs');
6
+ const os = require('os');
6
7
  const path = require('path');
7
8
  const recordings = require('./kryptheon-recordings.js');
8
9
  const heal = require('./kryptheon-heal.js');
10
+ const store = require('./kryptheon-store.js');
9
11
 
10
12
  // An assistant runs this after every change, so it needs a mode that says
11
13
  // nothing at all when the news is good.
12
14
  const QUIET = !!process.env.KRYPTHEON_QUIET;
13
15
 
14
- // The history belongs to whoever is running the tests, so it lives in their
15
- // folder - not inside the installed package.
16
+ // The history belongs to whoever is running the tests, so it is kept in their
17
+ // project's store - not inside the installed package, and not in the project.
16
18
  const USER_DIR = process.cwd();
17
- const HISTORY_FILE = path.join(USER_DIR, 'kryptheon-history.jsonl');
19
+ const HISTORY_FILE = store.pathsFor(USER_DIR).history;
20
+
21
+ // Said with ~ for the home folder: the full path is long, and the person only
22
+ // needs to know it is outside their project and where to look.
23
+ function shortHome(file) {
24
+ const home = os.homedir();
25
+ return file.toLowerCase().startsWith(home.toLowerCase() + path.sep) ? '~' + file.slice(home.length) : file;
26
+ }
18
27
 
19
28
  const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sept', 'Oct', 'Nov', 'Dec'];
20
29
 
@@ -962,10 +971,13 @@ class KryptheonReporter {
962
971
  };
963
972
 
964
973
  // Append only - existing lines are never rewritten.
974
+ let saved = false;
965
975
  try {
976
+ fs.mkdirSync(path.dirname(HISTORY_FILE), { recursive: true });
966
977
  fs.appendFileSync(HISTORY_FILE, JSON.stringify(entry) + '\n', 'utf8');
978
+ saved = true;
967
979
  } catch (err) {
968
- process.stdout.write('\n(could not write ' + path.basename(HISTORY_FILE) + ': ' + err.message + ')\n');
980
+ process.stdout.write('\n(could not write ' + shortHome(HISTORY_FILE) + ': ' + err.message + ')\n');
969
981
  }
970
982
 
971
983
  // "Broken" is kept for recordings that used to work. The ones that never
@@ -1011,7 +1023,7 @@ class KryptheonReporter {
1011
1023
 
1012
1024
  process.stdout.write(
1013
1025
  '\n' + recordings.summaryLines(counts).join('\n') + '\n' +
1014
- 'History saved to ' + path.basename(HISTORY_FILE) + '\n\n'
1026
+ (saved ? 'History saved outside your project, in ' + shortHome(HISTORY_FILE) + '\n' : '') + '\n'
1015
1027
  );
1016
1028
 
1017
1029
  // Once per run, not once per test: it is one fact about the app, and
@@ -139,9 +139,11 @@ function writeEnvExample(dir, names) {
139
139
  }
140
140
  }
141
141
 
142
- // Everything kryptheon leaves in a project that should not be committed: the
143
- // real password, and three files that are machine state rather than source.
144
- const GITIGNORE_ENTRIES = ['.env', 'kryptheon-baselines.json', 'kryptheon-history.jsonl', 'test-results/'];
142
+ // What must never be committed from a project: the real password. The three
143
+ // files of machine state that used to be listed here - baselines, history and
144
+ // test-results/ - are no longer written into the project at all; they live in
145
+ // its store (kryptheon-store.js), so there is nothing of Kryptheon's to ignore.
146
+ const GITIGNORE_ENTRIES = ['.env'];
145
147
 
146
148
  /**
147
149
  * Adds the missing entries to an existing .gitignore. Returns what it added.
@@ -0,0 +1,317 @@
1
+ // Where Kryptheon keeps what it remembers about a project.
2
+ //
3
+ // It used to be the project itself: kryptheon-baselines.json,
4
+ // kryptheon-history.jsonl, test-results/ and .kryptheon-last.json all appeared
5
+ // next to the person's own code. A plain landing page picked up six or seven
6
+ // files that way, and an AI assistant that commits "everything that changed"
7
+ // commits them too - the repo grows because Kryptheon is watching it.
8
+ //
9
+ // So every record now lives in one folder per project, outside it:
10
+ //
11
+ // ~/.kryptheon/projects/<folder-name>-<hash>/ (KRYPTHEON_HOME moves ~/.kryptheon)
12
+ // project.json which folder this store belongs to
13
+ // baselines.json was kryptheon-baselines.json
14
+ // history.jsonl was kryptheon-history.jsonl
15
+ // test-results/ was test-results/ (Playwright's screenshots of a failure)
16
+ // night-last.json was .kryptheon-last.json (kryptheon-night)
17
+ //
18
+ // Only tests/ - the recordings the person made - stays in the project.
19
+ //
20
+ // The hash is of the project's full path, so two folders that happen to share
21
+ // a name are never mixed. kryptheon-night carries a copy of this file and must
22
+ // land in the same folder for the same project; both packages check the ids
23
+ // below against the same fixed examples, so the two copies cannot drift apart.
24
+ //
25
+ // Records an older version left in a project are moved here the first time a
26
+ // project is opened, and only moved: nothing is thrown away, and when both
27
+ // places already hold a file the project's copy is kept in parked/ rather than
28
+ // overwriting what is here.
29
+
30
+ const fs = require('fs');
31
+ const os = require('os');
32
+ const path = require('path');
33
+ const crypto = require('crypto');
34
+
35
+ const HOME_ENV = 'KRYPTHEON_HOME';
36
+
37
+ const FILES = {
38
+ project: 'project.json',
39
+ baselines: 'baselines.json',
40
+ history: 'history.jsonl',
41
+ testResults: 'test-results',
42
+ nightLast: 'night-last.json',
43
+ };
44
+
45
+ // What older versions wrote into the project, and where each one goes now.
46
+ // append - history: lines the store does not have yet are added to it
47
+ // park - both exist: the project's copy goes to parked/, the store's stays
48
+ const LEGACY = [
49
+ { from: 'kryptheon-baselines.json', to: FILES.baselines, onBoth: 'park' },
50
+ { from: 'kryptheon-history.jsonl', to: FILES.history, onBoth: 'append' },
51
+ { from: '.kryptheon-last.json', to: FILES.nightLast, onBoth: 'park' },
52
+ { from: 'test-results', to: FILES.testResults, onBoth: 'park', dir: true },
53
+ ];
54
+
55
+ // A project with a Playwright config of its own owns its test-results/ -
56
+ // Playwright's default output folder. Moving that would take the person's own
57
+ // screenshots away, so it is left exactly where it is.
58
+ const OWN_PLAYWRIGHT = /^playwright\.config\.(js|cjs|mjs|ts|cts|mts)$/;
59
+
60
+ /** ~/.kryptheon, or wherever KRYPTHEON_HOME points. */
61
+ function home(env) {
62
+ const source = env || process.env;
63
+ const set = source[HOME_ENV];
64
+ if (set && String(set).trim()) return path.resolve(String(set).trim());
65
+ return path.join(os.homedir(), '.kryptheon');
66
+ }
67
+
68
+ /** The project folder as one exact path, whatever form it was given in. */
69
+ function canonicalRoot(dir) {
70
+ const resolved = path.resolve(dir || process.cwd());
71
+ try {
72
+ return fs.realpathSync.native(resolved);
73
+ } catch (err) {
74
+ return resolved;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * <folder-name>-<12 hex>. Pure: the same path always gives the same id.
80
+ *
81
+ * Windows paths are compared without regard to case, because C:\Users\a and
82
+ * c:\users\a are the same folder there and must be the same project.
83
+ */
84
+ function projectId(root, platform) {
85
+ const plat = platform || process.platform;
86
+ const P = plat === 'win32' ? path.win32 : path.posix;
87
+ // resolve() also drops a trailing separator and turns / into \ on Windows.
88
+ let key = P.resolve(String(root));
89
+ if (plat === 'win32') key = key.toLowerCase();
90
+ const hash = crypto.createHash('sha256').update(key, 'utf8').digest('hex').slice(0, 12);
91
+ const base = P.basename(key) || 'project';
92
+ const name = base.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|-+$/g, '').slice(0, 40) || 'project';
93
+ return name + '-' + hash;
94
+ }
95
+
96
+ /**
97
+ * The store folder for a project. Only reads: nothing is created or moved, so
98
+ * the fixture and the reporter can ask for it just by being loaded.
99
+ */
100
+ function dirFor(dir, env) {
101
+ const root = canonicalRoot(dir);
102
+ const short = path.join(home(env), 'projects', projectId(root));
103
+ const owner = readProject(path.join(short, FILES.project));
104
+ if (!owner || !owner.root || sameRoot(owner.root, root)) return short;
105
+ // Two different folders with the same short hash. Astronomically rare, but
106
+ // the one thing a store must never do is hold two projects, so the second
107
+ // one gets the full hash instead.
108
+ const full = crypto.createHash('sha256').update(root, 'utf8').digest('hex');
109
+ return short.replace(/-[0-9a-f]{12}$/, '-' + full);
110
+ }
111
+
112
+ /** Every file in a project's store, by name. Only reads, like dirFor. */
113
+ function pathsFor(dir, env) {
114
+ return pathsIn(dirFor(dir, env));
115
+ }
116
+
117
+ function pathsIn(dir) {
118
+ return {
119
+ dir: dir,
120
+ project: path.join(dir, FILES.project),
121
+ baselines: path.join(dir, FILES.baselines),
122
+ history: path.join(dir, FILES.history),
123
+ testResults: path.join(dir, FILES.testResults),
124
+ nightLast: path.join(dir, FILES.nightLast),
125
+ };
126
+ }
127
+
128
+ function exists(file) {
129
+ try {
130
+ fs.lstatSync(file);
131
+ return true;
132
+ } catch (err) {
133
+ return false;
134
+ }
135
+ }
136
+
137
+ function hasOwnPlaywright(root) {
138
+ try {
139
+ return fs.readdirSync(root).some((name) => OWN_PLAYWRIGHT.test(name));
140
+ } catch (err) {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ // rename is the whole move when both sides are on one drive. Across drives it
146
+ // fails with EXDEV, and then it is copy, check the copy, and only then remove.
147
+ function move(from, to, isDir) {
148
+ try {
149
+ fs.renameSync(from, to);
150
+ return;
151
+ } catch (err) {
152
+ // Anything else - a file held open by an editor, say - is reported and the
153
+ // record stays where it is, whole.
154
+ if (err.code !== 'EXDEV') throw err;
155
+ }
156
+ if (isDir) {
157
+ fs.cpSync(from, to, { recursive: true, errorOnExist: true, force: false });
158
+ fs.rmSync(from, { recursive: true, force: true });
159
+ return;
160
+ }
161
+ const tmp = to + '.moving';
162
+ fs.copyFileSync(from, tmp);
163
+ if (fs.statSync(tmp).size !== fs.statSync(from).size) {
164
+ fs.rmSync(tmp, { force: true });
165
+ throw new Error('the copy of ' + from + ' came out a different size');
166
+ }
167
+ fs.renameSync(tmp, to);
168
+ fs.rmSync(from, { force: true });
169
+ }
170
+
171
+ // Lines of the project's history the store does not have yet, in their order.
172
+ // Checked line by line so moving twice - say the project file could not be
173
+ // removed the first time - never writes a run down twice.
174
+ function appendHistory(from, to) {
175
+ const have = new Set(fs.readFileSync(to, 'utf8').split('\n').filter((l) => l.trim()));
176
+ const extra = fs.readFileSync(from, 'utf8').split('\n').filter((l) => l.trim() && !have.has(l));
177
+ if (extra.length) {
178
+ const current = fs.readFileSync(to, 'utf8');
179
+ const lead = current.length && !current.endsWith('\n') ? '\n' : '';
180
+ fs.appendFileSync(to, lead + extra.join('\n') + '\n', 'utf8');
181
+ }
182
+ fs.rmSync(from, { force: true });
183
+ return extra.length;
184
+ }
185
+
186
+ function stamp(now) {
187
+ return (now || new Date()).toISOString().replace(/[:.]/g, '-');
188
+ }
189
+
190
+ /**
191
+ * Moves what an older version left in the project into the store.
192
+ * Returns { moved: [names], parked: [{ name, to }], appended: [{ name, lines }],
193
+ * kept: [{ name, why }], failed: [{ name, why }] }.
194
+ */
195
+ function migrate(root, dir, options) {
196
+ const out = { moved: [], parked: [], appended: [], kept: [], failed: [] };
197
+ const ownPlaywright = hasOwnPlaywright(root);
198
+ for (const item of LEGACY) {
199
+ const from = path.join(root, item.from);
200
+ if (!exists(from)) continue;
201
+ const label = item.from + (item.dir ? '/' : '');
202
+ if (item.dir && ownPlaywright) {
203
+ out.kept.push({ name: label, why: 'this project has its own Playwright config, so it is theirs' });
204
+ continue;
205
+ }
206
+ const to = path.join(dir, item.to);
207
+ try {
208
+ if (!exists(to)) {
209
+ move(from, to, item.dir);
210
+ out.moved.push(label);
211
+ } else if (item.onBoth === 'append') {
212
+ out.appended.push({ name: label, lines: appendHistory(from, to) });
213
+ } else {
214
+ const parkedDir = path.join(dir, 'parked');
215
+ fs.mkdirSync(parkedDir, { recursive: true });
216
+ const parked = path.join(parkedDir, stamp(options && options.now) + '-' + item.from.replace(/^\./, ''));
217
+ move(from, parked, item.dir);
218
+ out.parked.push({ name: label, to: parked });
219
+ }
220
+ } catch (err) {
221
+ out.failed.push({ name: label, why: err.message });
222
+ }
223
+ }
224
+ return out;
225
+ }
226
+
227
+ /**
228
+ * The store for the project in `dir` (default: the current folder), created if
229
+ * it is not there yet, with anything an older version left in the project moved
230
+ * into it. Safe to call again: once moved, there is nothing left to move.
231
+ *
232
+ * Throws when the store cannot be created. A record that silently cannot be
233
+ * written makes every run look like the first one - which reads as "nothing to
234
+ * compare against, all fine" - so that is never swallowed here.
235
+ */
236
+ function open(dir, options) {
237
+ const env = (options && options.env) || process.env;
238
+ const root = canonicalRoot(dir);
239
+ const storeDir = dirFor(root, env);
240
+ fs.mkdirSync(storeDir, { recursive: true });
241
+ const paths = pathsIn(storeDir);
242
+ if (!exists(paths.project)) {
243
+ fs.writeFileSync(paths.project, JSON.stringify({
244
+ root: root,
245
+ name: path.basename(root),
246
+ id: path.basename(storeDir),
247
+ createdAt: new Date().toISOString(),
248
+ }, null, 2) + '\n', 'utf8');
249
+ }
250
+ return Object.assign({ root: root, id: path.basename(storeDir) }, paths, {
251
+ migration: migrate(root, storeDir, options),
252
+ });
253
+ }
254
+
255
+ function sameRoot(a, b, platform) {
256
+ const plat = platform || process.platform;
257
+ const norm = (p) => {
258
+ const r = path.resolve(String(p)).replace(/[\\/]+$/, '');
259
+ return plat === 'win32' ? r.split('/').join('\\').toLowerCase() : r;
260
+ };
261
+ return norm(a) === norm(b);
262
+ }
263
+
264
+ function readProject(file) {
265
+ try {
266
+ let raw = fs.readFileSync(file, 'utf8');
267
+ if (raw.charCodeAt(0) === 0xfeff) raw = raw.slice(1);
268
+ const parsed = JSON.parse(raw);
269
+ return parsed && typeof parsed === 'object' ? parsed : null;
270
+ } catch (err) {
271
+ return null;
272
+ }
273
+ }
274
+
275
+ /** What to tell the person about a migration, or nothing when nothing moved. */
276
+ function migrationLines(store) {
277
+ const m = store && store.migration;
278
+ if (!m) return [];
279
+ const lines = [];
280
+ const movedNames = m.moved.concat(m.appended.map((a) => a.name)).concat(m.parked.map((p) => p.name));
281
+ if (movedNames.length) {
282
+ lines.push(' Kryptheon now keeps its records outside your project, so they can never');
283
+ lines.push(' end up in your repo. Moved out of this folder:');
284
+ for (const name of movedNames) lines.push(' ' + name);
285
+ lines.push(' They are now in:');
286
+ lines.push(' ' + store.dir);
287
+ for (const p of m.parked) {
288
+ lines.push(' ' + p.name + ' was already there too, so this folder\'s copy is kept aside in:');
289
+ lines.push(' ' + p.to);
290
+ }
291
+ lines.push('');
292
+ }
293
+ for (const k of m.kept) {
294
+ lines.push(' Left ' + k.name + ' where it is: ' + k.why + '.');
295
+ lines.push('');
296
+ }
297
+ for (const f of m.failed) {
298
+ lines.push(' Could not move ' + f.name + ' out of this folder: ' + f.why);
299
+ lines.push(' It is still here. Close anything that has it open and run this again.');
300
+ lines.push('');
301
+ }
302
+ return lines;
303
+ }
304
+
305
+ module.exports = {
306
+ HOME_ENV: HOME_ENV,
307
+ FILES: FILES,
308
+ LEGACY: LEGACY,
309
+ home: home,
310
+ canonicalRoot: canonicalRoot,
311
+ projectId: projectId,
312
+ dirFor: dirFor,
313
+ pathsFor: pathsFor,
314
+ open: open,
315
+ migrate: migrate,
316
+ migrationLines: migrationLines,
317
+ };
package/package.json CHANGED
@@ -1,78 +1,80 @@
1
- {
2
- "name": "kryptheon",
3
- "version": "0.1.19",
4
- "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
- "license": "MIT",
6
- "type": "commonjs",
7
- "bin": {
8
- "kryptheon": "bin/kryptheon.js"
9
- },
10
- "exports": {
11
- ".": "./kryptheon-fixture.js",
12
- "./kryptheon-fixture": "./kryptheon-fixture.js",
13
- "./kryptheon-reporter": "./kryptheon-reporter.js",
14
- "./package.json": "./package.json",
15
- "./kryptheon-replay": "./kryptheon-replay.js",
16
- "./kryptheon-signature": "./kryptheon-signature.js",
17
- "./kryptheon-secrets": "./kryptheon-secrets.js"
18
- },
19
- "files": [
20
- "bin/",
21
- "kryptheon-fixture.js",
22
- "kryptheon-reporter.js",
23
- "kryptheon-signature.js",
24
- "kryptheon-secrets.js",
25
- "playwright.config.js",
26
- "README.md",
27
- "kryptheon-replay.js",
28
- "kryptheon-heal.js",
29
- "kryptheon-selectors.js",
30
- "kryptheon-recordings.js",
31
- "kryptheon-cleanup.js",
32
- "kryptheon-baselines.js",
33
- "kryptheon-project.js"
34
- ],
35
- "engines": {
36
- "node": ">=20.6.0"
37
- },
38
- "dependencies": {
39
- "@playwright/test": "^1.62.1"
40
- },
41
- "scripts": {
42
- "check": "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",
43
- "check:reporter": "node kryptheon-reporter.check.js",
44
- "check:signature": "node kryptheon-signature.check.js",
45
- "check:stream": "node kryptheon-signature.stream.check.js",
46
- "check:baseline": "node kryptheon-signature.baseline.check.js",
47
- "check:settle": "node kryptheon-signature.settle.check.js",
48
- "check:number": "node kryptheon-signature.number.check.js",
49
- "check:env": "node kryptheon-env.check.js",
50
- "check:watchdog": "node kryptheon-watchdog.check.js",
51
- "check:record": "node kryptheon-record.check.js",
52
- "check:selectors": "node kryptheon-selectors.check.js",
53
- "check:recordings": "node kryptheon-recordings.check.js",
54
- "check:cleanup": "node kryptheon-cleanup.check.js",
55
- "check:orphan": "node kryptheon-orphan.check.js",
56
- "check:project": "node kryptheon-project.check.js",
57
- "check:setup": "node kryptheon-setup.check.js",
58
- "check:browser": "node kryptheon-browser.check.js",
59
- "check:heal": "node kryptheon-heal.check.js",
60
- "check:load": "node kryptheon-load.check.js",
61
- "check:package": "node kryptheon-package.check.js",
62
- "prepublishOnly": "node kryptheon-package.check.js"
63
- },
64
- "keywords": [
65
- "playwright",
66
- "testing",
67
- "end-to-end",
68
- "cli"
69
- ],
70
- "repository": {
71
- "type": "git",
72
- "url": "git+https://github.com/jhashantanu987-beep/kryptheon-cli.git"
73
- },
74
- "bugs": {
75
- "url": "https://github.com/jhashantanu987-beep/kryptheon-cli/issues"
76
- },
77
- "homepage": "https://github.com/jhashantanu987-beep/kryptheon-cli#readme"
78
- }
1
+ {
2
+ "name": "kryptheon",
3
+ "version": "0.1.20",
4
+ "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "bin": {
8
+ "kryptheon": "bin/kryptheon.js"
9
+ },
10
+ "exports": {
11
+ ".": "./kryptheon-fixture.js",
12
+ "./kryptheon-fixture": "./kryptheon-fixture.js",
13
+ "./kryptheon-reporter": "./kryptheon-reporter.js",
14
+ "./package.json": "./package.json",
15
+ "./kryptheon-replay": "./kryptheon-replay.js",
16
+ "./kryptheon-signature": "./kryptheon-signature.js",
17
+ "./kryptheon-secrets": "./kryptheon-secrets.js"
18
+ },
19
+ "files": [
20
+ "bin/",
21
+ "kryptheon-fixture.js",
22
+ "kryptheon-reporter.js",
23
+ "kryptheon-signature.js",
24
+ "kryptheon-secrets.js",
25
+ "playwright.config.js",
26
+ "README.md",
27
+ "kryptheon-replay.js",
28
+ "kryptheon-heal.js",
29
+ "kryptheon-selectors.js",
30
+ "kryptheon-recordings.js",
31
+ "kryptheon-cleanup.js",
32
+ "kryptheon-baselines.js",
33
+ "kryptheon-project.js",
34
+ "kryptheon-store.js"
35
+ ],
36
+ "engines": {
37
+ "node": ">=20.6.0"
38
+ },
39
+ "dependencies": {
40
+ "@playwright/test": "^1.62.1"
41
+ },
42
+ "scripts": {
43
+ "check": "node kryptheon-store.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",
44
+ "check:store": "node kryptheon-store.check.js",
45
+ "check:reporter": "node kryptheon-reporter.check.js",
46
+ "check:signature": "node kryptheon-signature.check.js",
47
+ "check:stream": "node kryptheon-signature.stream.check.js",
48
+ "check:baseline": "node kryptheon-signature.baseline.check.js",
49
+ "check:settle": "node kryptheon-signature.settle.check.js",
50
+ "check:number": "node kryptheon-signature.number.check.js",
51
+ "check:env": "node kryptheon-env.check.js",
52
+ "check:watchdog": "node kryptheon-watchdog.check.js",
53
+ "check:record": "node kryptheon-record.check.js",
54
+ "check:selectors": "node kryptheon-selectors.check.js",
55
+ "check:recordings": "node kryptheon-recordings.check.js",
56
+ "check:cleanup": "node kryptheon-cleanup.check.js",
57
+ "check:orphan": "node kryptheon-orphan.check.js",
58
+ "check:project": "node kryptheon-project.check.js",
59
+ "check:setup": "node kryptheon-setup.check.js",
60
+ "check:browser": "node kryptheon-browser.check.js",
61
+ "check:heal": "node kryptheon-heal.check.js",
62
+ "check:load": "node kryptheon-load.check.js",
63
+ "check:package": "node kryptheon-package.check.js",
64
+ "prepublishOnly": "node kryptheon-package.check.js"
65
+ },
66
+ "keywords": [
67
+ "playwright",
68
+ "testing",
69
+ "end-to-end",
70
+ "cli"
71
+ ],
72
+ "repository": {
73
+ "type": "git",
74
+ "url": "git+https://github.com/jhashantanu987-beep/kryptheon-cli.git"
75
+ },
76
+ "bugs": {
77
+ "url": "https://github.com/jhashantanu987-beep/kryptheon-cli/issues"
78
+ },
79
+ "homepage": "https://github.com/jhashantanu987-beep/kryptheon-cli#readme"
80
+ }
@@ -42,8 +42,11 @@ module.exports = defineConfig({
42
42
  // The tests belong to the user, so they are found in their folder.
43
43
  testDir: path.join(USER_DIR, 'tests'),
44
44
  // Screenshots and other artefacts default to sitting next to the config,
45
- // which once installed means inside node_modules. Keep them with the user.
46
- outputDir: path.join(USER_DIR, 'test-results'),
45
+ // which once installed means inside node_modules. They belong to the user's
46
+ // project, so they go to its store - outside the project, where a commit of
47
+ // "everything that changed" cannot pick them up. Playwright empties this
48
+ // folder at the start of every run, so it never grows.
49
+ outputDir: require(path.join(PACKAGE_DIR, 'kryptheon-store.js')).pathsFor(USER_DIR).testResults,
47
50
  // These tests all run against one live site, so in parallel they interfere
48
51
  // with each other - a shared logged-in session, and rate limiting on the
49
52
  // real endpoints. Serial is the safe default here.