kryptheon 0.1.38 → 0.1.40
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 +1 -1
- package/README.md +261 -261
- package/bin/kryptheon.js +104 -0
- package/kryptheon-dashboard-page.js +67 -0
- package/kryptheon-dashboard.html +13 -0
- package/kryptheon-dashboard.js +65 -0
- package/package.json +1 -1
package/LICENSE
CHANGED
|
@@ -5,7 +5,7 @@ Copyright (c) 2026 Kryptheon (https://kryptheon.tech). All rights reserved.
|
|
|
5
5
|
This software is proprietary. It is not open source.
|
|
6
6
|
|
|
7
7
|
1. Use. You may install and run this software, as published by Kryptheon,
|
|
8
|
-
|
|
8
|
+
at no charge, to check software and databases that you own or are
|
|
9
9
|
authorised to test. Features that kryptheon.tech offers on a paid plan
|
|
10
10
|
are provided only under that plan and the Terms at
|
|
11
11
|
https://kryptheon.tech/terms.
|
package/README.md
CHANGED
|
@@ -1,261 +1,261 @@
|
|
|
1
|
-
# kryptheon
|
|
2
|
-
|
|
3
|
-
Record a flow in your app once, replay it after every AI change, and get told in
|
|
4
|
-
plain English what broke.
|
|
5
|
-
|
|
6
|
-
Runs entirely on your machine. No account, no API key, nothing leaves your
|
|
7
|
-
computer. It is a wrapper around [Playwright](https://playwright.dev).
|
|
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
|
-
|
|
33
|
-
## Only have a web address?
|
|
34
|
-
|
|
35
|
-
Built your app in Lovable, Bolt or v0, and there is no project folder on your
|
|
36
|
-
computer? Open a terminal and run:
|
|
37
|
-
|
|
38
|
-
```
|
|
39
|
-
npx kryptheon record https://your-app.lovable.app
|
|
40
|
-
```
|
|
41
|
-
|
|
42
|
-
Before it opens anything, it looks at the folder you are in. If that folder is
|
|
43
|
-
not set up for recordings it says so and asks first, then adds a `package.json`
|
|
44
|
-
and kryptheon itself (about 20MB) — nothing else. A new terminal opens in your
|
|
45
|
-
home folder, so from there it keeps your recordings in a `kryptheon-tests`
|
|
46
|
-
folder of their own and tells you where that is.
|
|
47
|
-
|
|
48
|
-
After every change to your app, from that same folder:
|
|
49
|
-
|
|
50
|
-
```
|
|
51
|
-
npx kryptheon check
|
|
52
|
-
```
|
|
53
|
-
|
|
54
|
-
## Install
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
npm i -D kryptheon
|
|
58
|
-
```
|
|
59
|
-
|
|
60
|
-
Install it into your project, not globally — every recording imports
|
|
61
|
-
`kryptheon/kryptheon-fixture`, and your tests can only find that if kryptheon is
|
|
62
|
-
in your project's `node_modules`.
|
|
63
|
-
|
|
64
|
-
## Record
|
|
65
|
-
|
|
66
|
-
```
|
|
67
|
-
npx kryptheon record https://your-app.example.com
|
|
68
|
-
```
|
|
69
|
-
|
|
70
|
-
A browser opens. Use your app the way a customer would. Close the browser when
|
|
71
|
-
you are done, and the recording is saved as a test.
|
|
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
|
-
|
|
77
|
-
On its own, a recording knows that every button and field is still there and
|
|
78
|
-
that the last page looks the same. To say what must be true — "Reservation
|
|
79
|
-
received" appears after booking — use the toolbar at the top of the page while
|
|
80
|
-
recording:
|
|
81
|
-
|
|
82
|
-
| Button | Click it, then click… | Kryptheon then checks |
|
|
83
|
-
| --- | --- | --- |
|
|
84
|
-
| Eye | any element | it is on the page |
|
|
85
|
-
| `ab` | some text | that text is there |
|
|
86
|
-
| Box | a filled-in field | the field still holds that value |
|
|
87
|
-
| `{ }` | a part of the page | that part is laid out the same |
|
|
88
|
-
|
|
89
|
-
The round red button pauses and resumes recording. The arrow only shows how an
|
|
90
|
-
element would be found; it adds nothing to the test. One check at the end of a
|
|
91
|
-
flow — on the success message — is usually all a recording needs.
|
|
92
|
-
|
|
93
|
-
## Check
|
|
94
|
-
|
|
95
|
-
```
|
|
96
|
-
npx kryptheon check
|
|
97
|
-
```
|
|
98
|
-
|
|
99
|
-
Runs everything you have recorded:
|
|
100
|
-
|
|
101
|
-
```
|
|
102
|
-
OK Sign in (4.1s)
|
|
103
|
-
|
|
104
|
-
X Checkout
|
|
105
|
-
Could not find the button "Place order" on the page.
|
|
106
|
-
This was working on 12 Mar at 9:14 AM.
|
|
107
|
-
What to check: your last change may have renamed, hidden, or removed it.
|
|
108
|
-
Where to look:
|
|
109
|
-
- Browser was on: https://your-app.example.com/cart
|
|
110
|
-
- POST /api/orders returned 500
|
|
111
|
-
the server crashed or is unavailable - the error is in backend code, not the page
|
|
112
|
-
```
|
|
113
|
-
|
|
114
|
-
Every failure ends with a short summary you can paste straight into an AI coding
|
|
115
|
-
tool.
|
|
116
|
-
|
|
117
|
-
## Recording something that replays
|
|
118
|
-
|
|
119
|
-
A recording is only useful if it still works the second time. So:
|
|
120
|
-
|
|
121
|
-
- **Record one simple flow.** Sign in and look at a page. Open a thing and check
|
|
122
|
-
it loaded. Short beats thorough.
|
|
123
|
-
- **Sign in with an account that already exists.** Do not sign up during a
|
|
124
|
-
recording — the account exists next time, and the signup step fails.
|
|
125
|
-
- **Avoid steps that cannot repeat.** Adding data piles up a row on every run.
|
|
126
|
-
Logging out at the end means the next run does not start where this one did.
|
|
127
|
-
|
|
128
|
-
Kryptheon warns you about these after a recording, and names the exact step.
|
|
129
|
-
|
|
130
|
-
## Make your AI assistant check its own work
|
|
131
|
-
|
|
132
|
-
An assistant will happily call a change done after only reading the code. This
|
|
133
|
-
writes a rule into the files assistants already read, so it has to run the check
|
|
134
|
-
first:
|
|
135
|
-
|
|
136
|
-
```
|
|
137
|
-
npx kryptheon setup-ai
|
|
138
|
-
```
|
|
139
|
-
|
|
140
|
-
It writes `CLAUDE.md`, `AGENTS.md` and `.cursorrules`. If a file already exists,
|
|
141
|
-
it adds a marked section at the end rather than overwriting anything, and
|
|
142
|
-
running it again replaces that section instead of adding a second copy.
|
|
143
|
-
|
|
144
|
-
The rule: after any change, run `npx kryptheon check`, show the output, and do
|
|
145
|
-
not report the work as done until it passes.
|
|
146
|
-
|
|
147
|
-
For an assistant running the check after every change, `--quiet` keeps the
|
|
148
|
-
conversation readable — one line when everything passes, the full explanation
|
|
149
|
-
only when something breaks:
|
|
150
|
-
|
|
151
|
-
```
|
|
152
|
-
npx kryptheon check --quiet
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
```
|
|
156
|
-
OK 3 recordings still working.
|
|
157
|
-
```
|
|
158
|
-
|
|
159
|
-
## Automatic checks
|
|
160
|
-
|
|
161
|
-
The first time a test passes, kryptheon remembers where the browser ended up and
|
|
162
|
-
what the page was called. If either changes later, the test fails and says what
|
|
163
|
-
changed — so a recording catches regressions without you writing a single
|
|
164
|
-
assertion.
|
|
165
|
-
|
|
166
|
-
When a change is intentional, accept it for that one test:
|
|
167
|
-
|
|
168
|
-
```
|
|
169
|
-
npx kryptheon accept "Checkout"
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
`npx kryptheon accept` on its own lists what has been saved.
|
|
173
|
-
|
|
174
|
-
## Renamed buttons
|
|
175
|
-
|
|
176
|
-
A button that only changed its words has not broken anything. When a step
|
|
177
|
-
cannot find "Reserve a table" and the page shows exactly one new button in its
|
|
178
|
-
place — "Reserve here" — kryptheon clicks that one, carries on through the rest
|
|
179
|
-
of the flow, and says so:
|
|
180
|
-
|
|
181
|
-
```
|
|
182
|
-
OK Reserve a table (16.0s)
|
|
183
|
-
Renamed: The button "Reserve a table" is now called "Reserve here" - I used "Reserve here" and carried on.
|
|
184
|
-
Nothing is broken. To stop seeing this, record the flow again.
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
It only does this when the page proves it: one name gone, the one the step
|
|
188
|
-
wanted, and one new name of the same kind. A button that is gone with nothing
|
|
189
|
-
in its place, or a rename next to other new buttons, fails as it always did.
|
|
190
|
-
Checks you added with the toolbar are never redirected.
|
|
191
|
-
|
|
192
|
-
## Dashboard
|
|
193
|
-
|
|
194
|
-
```
|
|
195
|
-
npx kryptheon
|
|
196
|
-
```
|
|
197
|
-
|
|
198
|
-
In a terminal, with no command, this opens a page on your own machine
|
|
199
|
-
(`http://127.0.0.1:4789/`) for this project, in your browser by itself (the
|
|
200
|
-
address is printed too): what is broken, what needs a look, every run, recent
|
|
201
|
-
changes, and switches for each check. Every check runs from a button, one at a
|
|
202
|
-
time, with its progress shown on the page as it goes. It is only reachable
|
|
203
|
-
from this machine; press Ctrl+C to stop it. From a script, use
|
|
204
|
-
`npx kryptheon dashboard`; set `KRYPTHEON_NO_OPEN=1` to keep the browser shut.
|
|
205
|
-
|
|
206
|
-
It opens on the one thing to do next, and under it "Start here": read your
|
|
207
|
-
code, record your main flows, replay them, connect and check your database,
|
|
208
|
-
and set up the nightly check - each marked done only once it has actually run,
|
|
209
|
-
and each labelled "one time" or "after every change".
|
|
210
|
-
|
|
211
|
-
Checks that are not built yet are listed as "not available" - never as
|
|
212
|
-
passed.
|
|
213
|
-
|
|
214
|
-
## Reading your code
|
|
215
|
-
|
|
216
|
-
```
|
|
217
|
-
npx kryptheon code
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
Reads your frontend code for values put into the page as HTML - `innerHTML`,
|
|
221
|
-
`insertAdjacentHTML`, `document.write`, React's `dangerouslySetInnerHTML`,
|
|
222
|
-
Vue's `v-html`. If such a value can hold text someone typed, a `<` in it can
|
|
223
|
-
run script with the signed-in person's session. Nothing is run; every result
|
|
224
|
-
is "verification required", ranked HIGH when the value comes from a server
|
|
225
|
-
response or an error message, MEDIUM when the code alone cannot say. The fix
|
|
226
|
-
prompt for each one names the file, line and value.
|
|
227
|
-
|
|
228
|
-
## What goes where
|
|
229
|
-
|
|
230
|
-
Only your recordings go in your project, in `tests/`. Everything Kryptheon
|
|
231
|
-
remembers about the project is kept outside it, so it can never end up in your
|
|
232
|
-
repo:
|
|
233
|
-
|
|
234
|
-
~/.kryptheon/projects/<folder-name>-<id>/
|
|
235
|
-
baselines.json the remembered result for each test
|
|
236
|
-
history.jsonl one line per run, used for "this was working on …"
|
|
237
|
-
test-results/ the screenshot of the last failure
|
|
238
|
-
|
|
239
|
-
Set `KRYPTHEON_HOME` to keep them somewhere else. Each project folder gets its
|
|
240
|
-
own store, so two projects are never mixed. A project folder that is moved or
|
|
241
|
-
renamed starts a new store.
|
|
242
|
-
|
|
243
|
-
Older versions kept `kryptheon-baselines.json`, `kryptheon-history.jsonl` and
|
|
244
|
-
`test-results/` in the project. The first run of this version moves them out,
|
|
245
|
-
once, and says so.
|
|
246
|
-
|
|
247
|
-
A `.env` in the same folder is loaded automatically, and anything you type into a
|
|
248
|
-
password box is replaced with an environment variable rather than written into
|
|
249
|
-
the test. When there is a password, `.env` is added to an existing `.gitignore`.
|
|
250
|
-
|
|
251
|
-
## Requirements
|
|
252
|
-
|
|
253
|
-
Node 20.6 or later. The first run downloads a browser (about 200MB, once).
|
|
254
|
-
|
|
255
|
-
Recording opens a browser window, so it needs a machine with a screen. It will
|
|
256
|
-
always try — if no window appears within 30 seconds it stops and says so,
|
|
257
|
-
rather than refusing up front.
|
|
258
|
-
|
|
259
|
-
## Licence
|
|
260
|
-
|
|
261
|
-
|
|
1
|
+
# kryptheon
|
|
2
|
+
|
|
3
|
+
Record a flow in your app once, replay it after every AI change, and get told in
|
|
4
|
+
plain English what broke.
|
|
5
|
+
|
|
6
|
+
Runs entirely on your machine. No account, no API key, nothing leaves your
|
|
7
|
+
computer. It is a wrapper around [Playwright](https://playwright.dev).
|
|
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
|
+
|
|
33
|
+
## Only have a web address?
|
|
34
|
+
|
|
35
|
+
Built your app in Lovable, Bolt or v0, and there is no project folder on your
|
|
36
|
+
computer? Open a terminal and run:
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
npx kryptheon record https://your-app.lovable.app
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Before it opens anything, it looks at the folder you are in. If that folder is
|
|
43
|
+
not set up for recordings it says so and asks first, then adds a `package.json`
|
|
44
|
+
and kryptheon itself (about 20MB) — nothing else. A new terminal opens in your
|
|
45
|
+
home folder, so from there it keeps your recordings in a `kryptheon-tests`
|
|
46
|
+
folder of their own and tells you where that is.
|
|
47
|
+
|
|
48
|
+
After every change to your app, from that same folder:
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
npx kryptheon check
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Install
|
|
55
|
+
|
|
56
|
+
```
|
|
57
|
+
npm i -D kryptheon
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Install it into your project, not globally — every recording imports
|
|
61
|
+
`kryptheon/kryptheon-fixture`, and your tests can only find that if kryptheon is
|
|
62
|
+
in your project's `node_modules`.
|
|
63
|
+
|
|
64
|
+
## Record
|
|
65
|
+
|
|
66
|
+
```
|
|
67
|
+
npx kryptheon record https://your-app.example.com
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
A browser opens. Use your app the way a customer would. Close the browser when
|
|
71
|
+
you are done, and the recording is saved as a test.
|
|
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
|
+
|
|
77
|
+
On its own, a recording knows that every button and field is still there and
|
|
78
|
+
that the last page looks the same. To say what must be true — "Reservation
|
|
79
|
+
received" appears after booking — use the toolbar at the top of the page while
|
|
80
|
+
recording:
|
|
81
|
+
|
|
82
|
+
| Button | Click it, then click… | Kryptheon then checks |
|
|
83
|
+
| --- | --- | --- |
|
|
84
|
+
| Eye | any element | it is on the page |
|
|
85
|
+
| `ab` | some text | that text is there |
|
|
86
|
+
| Box | a filled-in field | the field still holds that value |
|
|
87
|
+
| `{ }` | a part of the page | that part is laid out the same |
|
|
88
|
+
|
|
89
|
+
The round red button pauses and resumes recording. The arrow only shows how an
|
|
90
|
+
element would be found; it adds nothing to the test. One check at the end of a
|
|
91
|
+
flow — on the success message — is usually all a recording needs.
|
|
92
|
+
|
|
93
|
+
## Check
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
npx kryptheon check
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
Runs everything you have recorded:
|
|
100
|
+
|
|
101
|
+
```
|
|
102
|
+
OK Sign in (4.1s)
|
|
103
|
+
|
|
104
|
+
X Checkout
|
|
105
|
+
Could not find the button "Place order" on the page.
|
|
106
|
+
This was working on 12 Mar at 9:14 AM.
|
|
107
|
+
What to check: your last change may have renamed, hidden, or removed it.
|
|
108
|
+
Where to look:
|
|
109
|
+
- Browser was on: https://your-app.example.com/cart
|
|
110
|
+
- POST /api/orders returned 500
|
|
111
|
+
the server crashed or is unavailable - the error is in backend code, not the page
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Every failure ends with a short summary you can paste straight into an AI coding
|
|
115
|
+
tool.
|
|
116
|
+
|
|
117
|
+
## Recording something that replays
|
|
118
|
+
|
|
119
|
+
A recording is only useful if it still works the second time. So:
|
|
120
|
+
|
|
121
|
+
- **Record one simple flow.** Sign in and look at a page. Open a thing and check
|
|
122
|
+
it loaded. Short beats thorough.
|
|
123
|
+
- **Sign in with an account that already exists.** Do not sign up during a
|
|
124
|
+
recording — the account exists next time, and the signup step fails.
|
|
125
|
+
- **Avoid steps that cannot repeat.** Adding data piles up a row on every run.
|
|
126
|
+
Logging out at the end means the next run does not start where this one did.
|
|
127
|
+
|
|
128
|
+
Kryptheon warns you about these after a recording, and names the exact step.
|
|
129
|
+
|
|
130
|
+
## Make your AI assistant check its own work
|
|
131
|
+
|
|
132
|
+
An assistant will happily call a change done after only reading the code. This
|
|
133
|
+
writes a rule into the files assistants already read, so it has to run the check
|
|
134
|
+
first:
|
|
135
|
+
|
|
136
|
+
```
|
|
137
|
+
npx kryptheon setup-ai
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
It writes `CLAUDE.md`, `AGENTS.md` and `.cursorrules`. If a file already exists,
|
|
141
|
+
it adds a marked section at the end rather than overwriting anything, and
|
|
142
|
+
running it again replaces that section instead of adding a second copy.
|
|
143
|
+
|
|
144
|
+
The rule: after any change, run `npx kryptheon check`, show the output, and do
|
|
145
|
+
not report the work as done until it passes.
|
|
146
|
+
|
|
147
|
+
For an assistant running the check after every change, `--quiet` keeps the
|
|
148
|
+
conversation readable — one line when everything passes, the full explanation
|
|
149
|
+
only when something breaks:
|
|
150
|
+
|
|
151
|
+
```
|
|
152
|
+
npx kryptheon check --quiet
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
```
|
|
156
|
+
OK 3 recordings still working.
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
## Automatic checks
|
|
160
|
+
|
|
161
|
+
The first time a test passes, kryptheon remembers where the browser ended up and
|
|
162
|
+
what the page was called. If either changes later, the test fails and says what
|
|
163
|
+
changed — so a recording catches regressions without you writing a single
|
|
164
|
+
assertion.
|
|
165
|
+
|
|
166
|
+
When a change is intentional, accept it for that one test:
|
|
167
|
+
|
|
168
|
+
```
|
|
169
|
+
npx kryptheon accept "Checkout"
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`npx kryptheon accept` on its own lists what has been saved.
|
|
173
|
+
|
|
174
|
+
## Renamed buttons
|
|
175
|
+
|
|
176
|
+
A button that only changed its words has not broken anything. When a step
|
|
177
|
+
cannot find "Reserve a table" and the page shows exactly one new button in its
|
|
178
|
+
place — "Reserve here" — kryptheon clicks that one, carries on through the rest
|
|
179
|
+
of the flow, and says so:
|
|
180
|
+
|
|
181
|
+
```
|
|
182
|
+
OK Reserve a table (16.0s)
|
|
183
|
+
Renamed: The button "Reserve a table" is now called "Reserve here" - I used "Reserve here" and carried on.
|
|
184
|
+
Nothing is broken. To stop seeing this, record the flow again.
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
It only does this when the page proves it: one name gone, the one the step
|
|
188
|
+
wanted, and one new name of the same kind. A button that is gone with nothing
|
|
189
|
+
in its place, or a rename next to other new buttons, fails as it always did.
|
|
190
|
+
Checks you added with the toolbar are never redirected.
|
|
191
|
+
|
|
192
|
+
## Dashboard
|
|
193
|
+
|
|
194
|
+
```
|
|
195
|
+
npx kryptheon
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
In a terminal, with no command, this opens a page on your own machine
|
|
199
|
+
(`http://127.0.0.1:4789/`) for this project, in your browser by itself (the
|
|
200
|
+
address is printed too): what is broken, what needs a look, every run, recent
|
|
201
|
+
changes, and switches for each check. Every check runs from a button, one at a
|
|
202
|
+
time, with its progress shown on the page as it goes. It is only reachable
|
|
203
|
+
from this machine; press Ctrl+C to stop it. From a script, use
|
|
204
|
+
`npx kryptheon dashboard`; set `KRYPTHEON_NO_OPEN=1` to keep the browser shut.
|
|
205
|
+
|
|
206
|
+
It opens on the one thing to do next, and under it "Start here": read your
|
|
207
|
+
code, record your main flows, replay them, connect and check your database,
|
|
208
|
+
and set up the nightly check - each marked done only once it has actually run,
|
|
209
|
+
and each labelled "one time" or "after every change".
|
|
210
|
+
|
|
211
|
+
Checks that are not built yet are listed as "not available" - never as
|
|
212
|
+
passed.
|
|
213
|
+
|
|
214
|
+
## Reading your code
|
|
215
|
+
|
|
216
|
+
```
|
|
217
|
+
npx kryptheon code
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Reads your frontend code for values put into the page as HTML - `innerHTML`,
|
|
221
|
+
`insertAdjacentHTML`, `document.write`, React's `dangerouslySetInnerHTML`,
|
|
222
|
+
Vue's `v-html`. If such a value can hold text someone typed, a `<` in it can
|
|
223
|
+
run script with the signed-in person's session. Nothing is run; every result
|
|
224
|
+
is "verification required", ranked HIGH when the value comes from a server
|
|
225
|
+
response or an error message, MEDIUM when the code alone cannot say. The fix
|
|
226
|
+
prompt for each one names the file, line and value.
|
|
227
|
+
|
|
228
|
+
## What goes where
|
|
229
|
+
|
|
230
|
+
Only your recordings go in your project, in `tests/`. Everything Kryptheon
|
|
231
|
+
remembers about the project is kept outside it, so it can never end up in your
|
|
232
|
+
repo:
|
|
233
|
+
|
|
234
|
+
~/.kryptheon/projects/<folder-name>-<id>/
|
|
235
|
+
baselines.json the remembered result for each test
|
|
236
|
+
history.jsonl one line per run, used for "this was working on …"
|
|
237
|
+
test-results/ the screenshot of the last failure
|
|
238
|
+
|
|
239
|
+
Set `KRYPTHEON_HOME` to keep them somewhere else. Each project folder gets its
|
|
240
|
+
own store, so two projects are never mixed. A project folder that is moved or
|
|
241
|
+
renamed starts a new store.
|
|
242
|
+
|
|
243
|
+
Older versions kept `kryptheon-baselines.json`, `kryptheon-history.jsonl` and
|
|
244
|
+
`test-results/` in the project. The first run of this version moves them out,
|
|
245
|
+
once, and says so.
|
|
246
|
+
|
|
247
|
+
A `.env` in the same folder is loaded automatically, and anything you type into a
|
|
248
|
+
password box is replaced with an environment variable rather than written into
|
|
249
|
+
the test. When there is a password, `.env` is added to an existing `.gitignore`.
|
|
250
|
+
|
|
251
|
+
## Requirements
|
|
252
|
+
|
|
253
|
+
Node 20.6 or later. The first run downloads a browser (about 200MB, once).
|
|
254
|
+
|
|
255
|
+
Recording opens a browser window, so it needs a machine with a screen. It will
|
|
256
|
+
always try — if no window appears within 30 seconds it stops and says so,
|
|
257
|
+
rather than refusing up front.
|
|
258
|
+
|
|
259
|
+
## Licence
|
|
260
|
+
|
|
261
|
+
Proprietary software, not open source - see LICENSE. It runs on your own machine, with no account and no card. The nightly check, with its morning report, alerts and history, is part of the Pro and Studio plans on kryptheon.tech.
|
package/bin/kryptheon.js
CHANGED
|
@@ -2537,6 +2537,88 @@ function openInBrowser(url) {
|
|
|
2537
2537
|
}
|
|
2538
2538
|
}
|
|
2539
2539
|
|
|
2540
|
+
/**
|
|
2541
|
+
* The person's own folder picker, on this computer: the one Windows or the
|
|
2542
|
+
* Mac shows everywhere else. Resolves to the folder, or null when it was
|
|
2543
|
+
* closed. KRYPTHEON_CHOOSE_FOLDER answers instead, for the checks - no
|
|
2544
|
+
* window is shown then.
|
|
2545
|
+
*
|
|
2546
|
+
* Found when the first person paid: the dashboard opened from Win+R is for
|
|
2547
|
+
* a kryptheon-tests folder, and nothing on it said how to point it at an
|
|
2548
|
+
* app whose code is on this computer.
|
|
2549
|
+
*/
|
|
2550
|
+
function chooseFolder() {
|
|
2551
|
+
if (process.env.KRYPTHEON_CHOOSE_FOLDER !== undefined) return Promise.resolve(process.env.KRYPTHEON_CHOOSE_FOLDER || null);
|
|
2552
|
+
return new Promise((resolve) => {
|
|
2553
|
+
let cmd;
|
|
2554
|
+
let args;
|
|
2555
|
+
if (process.platform === 'win32') {
|
|
2556
|
+
// A form kept on top owns the dialog, or it opens behind the browser.
|
|
2557
|
+
const script = [
|
|
2558
|
+
'[Console]::OutputEncoding = [Text.Encoding]::UTF8',
|
|
2559
|
+
'Add-Type -AssemblyName System.Windows.Forms',
|
|
2560
|
+
'$owner = New-Object System.Windows.Forms.Form -Property @{ TopMost = $true; ShowInTaskbar = $false }',
|
|
2561
|
+
'$f = New-Object System.Windows.Forms.FolderBrowserDialog',
|
|
2562
|
+
"$f.Description = 'Choose your app''s folder - the one with its package.json'",
|
|
2563
|
+
'$f.ShowNewFolderButton = $false',
|
|
2564
|
+
"if ($f.ShowDialog($owner) -eq 'OK') { [Console]::Out.Write($f.SelectedPath) }",
|
|
2565
|
+
].join('; ');
|
|
2566
|
+
cmd = 'powershell.exe';
|
|
2567
|
+
args = ['-NoProfile', '-STA', '-ExecutionPolicy', 'Bypass', '-Command', script];
|
|
2568
|
+
} else if (process.platform === 'darwin') {
|
|
2569
|
+
cmd = 'osascript';
|
|
2570
|
+
args = ['-e', 'POSIX path of (choose folder with prompt "Choose your app\'s folder - the one with its package.json")'];
|
|
2571
|
+
} else {
|
|
2572
|
+
return resolve(null);
|
|
2573
|
+
}
|
|
2574
|
+
let out = '';
|
|
2575
|
+
const child = spawn(cmd, args, { stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true });
|
|
2576
|
+
child.stdout.on('data', (c) => (out += c));
|
|
2577
|
+
child.on('error', () => resolve(null));
|
|
2578
|
+
child.on('close', () => {
|
|
2579
|
+
const folder = out.trim().replace(/\/$/, '');
|
|
2580
|
+
resolve(folder && fs.existsSync(folder) && fs.statSync(folder).isDirectory() ? folder : null);
|
|
2581
|
+
});
|
|
2582
|
+
});
|
|
2583
|
+
}
|
|
2584
|
+
|
|
2585
|
+
/**
|
|
2586
|
+
* A dashboard for that folder, in a window of its own - the same line as
|
|
2587
|
+
* Win+R, started there. The port this one holds is taken, so that one picks
|
|
2588
|
+
* a free port and opens its own browser tab. KRYPTHEON_OPEN_FOLDER_LOG names
|
|
2589
|
+
* a file the folder is written to instead, for the checks.
|
|
2590
|
+
*/
|
|
2591
|
+
function openFolderDashboard(folder) {
|
|
2592
|
+
const where = path.resolve(folder);
|
|
2593
|
+
const plan = project.setupPlan(where);
|
|
2594
|
+
if (plan.action === 'refuse') {
|
|
2595
|
+
return { ok: false, why: plan.reason === 'system' ? 'that is a system folder, not an app' : 'that folder cannot be used' };
|
|
2596
|
+
}
|
|
2597
|
+
if (process.env.KRYPTHEON_OPEN_FOLDER_LOG) {
|
|
2598
|
+
fs.appendFileSync(process.env.KRYPTHEON_OPEN_FOLDER_LOG, where + '\n');
|
|
2599
|
+
return { ok: true, opened: path.basename(where) };
|
|
2600
|
+
}
|
|
2601
|
+
try {
|
|
2602
|
+
let child;
|
|
2603
|
+
if (process.platform === 'win32') {
|
|
2604
|
+
// start's first quoted word is the window's title; /d is where it runs.
|
|
2605
|
+
child = spawn('cmd.exe', ['/c', 'start "Kryptheon - ' + path.basename(where).replace(/["%^&|<>]/g, '') + '" /d "' + where + '" cmd /k npx -y kryptheon@latest dashboard'],
|
|
2606
|
+
{ detached: true, stdio: 'ignore', windowsVerbatimArguments: true, env: Object.assign({}, process.env, { KRYPTHEON_FOLDER_CHOSEN: '1' }) });
|
|
2607
|
+
} else if (process.platform === 'darwin') {
|
|
2608
|
+
const line = 'cd ' + JSON.stringify(where) + ' && KRYPTHEON_FOLDER_CHOSEN=1 npx -y kryptheon@latest dashboard';
|
|
2609
|
+
child = spawn('osascript', ['-e', 'tell application "Terminal" to do script ' + JSON.stringify(line), '-e', 'tell application "Terminal" to activate'],
|
|
2610
|
+
{ detached: true, stdio: 'ignore' });
|
|
2611
|
+
} else {
|
|
2612
|
+
return { ok: false, why: 'open a terminal in that folder and run: npx kryptheon@latest dashboard' };
|
|
2613
|
+
}
|
|
2614
|
+
child.on('error', () => {});
|
|
2615
|
+
child.unref();
|
|
2616
|
+
return { ok: true, opened: path.basename(where) };
|
|
2617
|
+
} catch (err) {
|
|
2618
|
+
return { ok: false, why: 'the new window could not be opened: ' + err.message };
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
|
|
2540
2622
|
/**
|
|
2541
2623
|
* Whether this project's dashboard is already running on the usual port -
|
|
2542
2624
|
* started at sign-in, most likely - by asking it which project it shows.
|
|
@@ -2625,6 +2707,26 @@ async function dashboardCommand() {
|
|
|
2625
2707
|
if (!here.ok) {
|
|
2626
2708
|
const plan = project.setupPlan(USER_DIR);
|
|
2627
2709
|
if (plan.action === 'home') return await dashboardFromHome(plan);
|
|
2710
|
+
// A folder of the person's own with no package.json - a site exported
|
|
2711
|
+
// from a builder, or one chosen on the dashboard's "Choose its folder" -
|
|
2712
|
+
// is set up the way record sets one up: said first, asked once.
|
|
2713
|
+
if (plan.action === 'create') {
|
|
2714
|
+
for (const line of project.setupLines(plan)) console.log(line);
|
|
2715
|
+
// Without a keyboard to ask, only a window the person opened for this
|
|
2716
|
+
// folder - "Choose its folder" marks it - is taken as a yes. Anything
|
|
2717
|
+
// else run here unattended writes nothing.
|
|
2718
|
+
const question = project.setupQuestion(plan);
|
|
2719
|
+
const yes = process.stdin.isTTY
|
|
2720
|
+
? project.answeredYes(await askLine(question.text), question.yesByDefault)
|
|
2721
|
+
: process.env.KRYPTHEON_FOLDER_CHOSEN === '1';
|
|
2722
|
+
if (!yes || !makeReady(USER_DIR)) {
|
|
2723
|
+
console.log('');
|
|
2724
|
+
console.log(' Nothing was changed.');
|
|
2725
|
+
console.log('');
|
|
2726
|
+
return 1;
|
|
2727
|
+
}
|
|
2728
|
+
return await dashboardCommand();
|
|
2729
|
+
}
|
|
2628
2730
|
for (const line of project.noProjectLines(here)) console.error(line);
|
|
2629
2731
|
return 1;
|
|
2630
2732
|
}
|
|
@@ -2651,6 +2753,8 @@ async function dashboardCommand() {
|
|
|
2651
2753
|
record: (address, say, options) => recordFromDashboard(address, say, options),
|
|
2652
2754
|
db: (action, connection, say) => databaseFromDashboard(action, connection, say),
|
|
2653
2755
|
words: (connection) => nightWords(connection),
|
|
2756
|
+
chooseFolder: () => chooseFolder(),
|
|
2757
|
+
openFolder: (folder) => openFolderDashboard(folder),
|
|
2654
2758
|
});
|
|
2655
2759
|
let port;
|
|
2656
2760
|
try {
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
arrow: 'M5 12h14m-6-6l6 6-6 6',
|
|
52
52
|
sun: 'M12 16a4 4 0 100-8 4 4 0 000 8zM12 2v2m0 16v2M4.9 4.9l1.4 1.4m11.4 11.4l1.4 1.4M2 12h2m16 0h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4',
|
|
53
53
|
terminal: 'M4 5h16v14H4zM7 9l3 3-3 3M13 15h4',
|
|
54
|
+
folder: 'M3 6h6l2 2h10v11H3z',
|
|
54
55
|
dot: 'M12 12h.01',
|
|
55
56
|
};
|
|
56
57
|
function icon(name, size) {
|
|
@@ -155,6 +156,12 @@
|
|
|
155
156
|
recordInput.placeholder = 'http://localhost:3000';
|
|
156
157
|
recordInput.setAttribute('aria-label', 'Your app address');
|
|
157
158
|
recordInput.spellcheck = false;
|
|
159
|
+
// The app's web address, at the top of the page: kept like the others.
|
|
160
|
+
var appInput = el('input', 'field');
|
|
161
|
+
appInput.type = 'url';
|
|
162
|
+
appInput.placeholder = 'https://my-app.lovable.app';
|
|
163
|
+
appInput.setAttribute('aria-label', 'Your app\'s web address');
|
|
164
|
+
appInput.spellcheck = false;
|
|
158
165
|
// Kept like the box beside it, so a page refresh does not untick it.
|
|
159
166
|
var phoneBox = el('input');
|
|
160
167
|
phoneBox.type = 'checkbox';
|
|
@@ -1103,11 +1110,71 @@
|
|
|
1103
1110
|
if (!busy) { clear(run); run.appendChild(icon('play', 15)); run.appendChild(el('span', null, 'Run checks')); }
|
|
1104
1111
|
}
|
|
1105
1112
|
|
|
1113
|
+
/* ------------------------------ your app ------------------------------ */
|
|
1114
|
+
|
|
1115
|
+
// Found when the first person paid: the dashboard opened from Win+R is for
|
|
1116
|
+
// a kryptheon-tests folder, and nothing said which app it checks or how to
|
|
1117
|
+
// point it at one. Said first: the app's web address - all a Lovable app
|
|
1118
|
+
// has - and, for code on this computer, its folder.
|
|
1119
|
+
function renderApp() {
|
|
1120
|
+
var box = byId('appBox');
|
|
1121
|
+
if (!box) return;
|
|
1122
|
+
var app = state.app || {};
|
|
1123
|
+
clear(box);
|
|
1124
|
+
if (document.activeElement !== appInput && !appInput.value && app.address) appInput.value = app.address;
|
|
1125
|
+
if (!recordInput.value && app.address) recordInput.value = app.address;
|
|
1126
|
+
|
|
1127
|
+
if (!app.webOnly) {
|
|
1128
|
+
var here = el('p', 'app-here');
|
|
1129
|
+
here.appendChild(document.createTextNode('This dashboard checks the code in '));
|
|
1130
|
+
here.appendChild(el('b', null, (state.project && state.project.name) || 'this folder'));
|
|
1131
|
+
here.appendChild(el('small', 'app-path', app.folder || ''));
|
|
1132
|
+
box.appendChild(here);
|
|
1133
|
+
}
|
|
1134
|
+
var label = el('label', 'app-label', app.webOnly ? 'Your app\'s web address' : 'Its web address - optional, for recording it');
|
|
1135
|
+
box.appendChild(label);
|
|
1136
|
+
var row = el('div', 'field-row');
|
|
1137
|
+
row.appendChild(appInput);
|
|
1138
|
+
var save = button('sun sm', 'check', app.address ? 'Save' : 'Save the address', function () {
|
|
1139
|
+
post('/api/app', { address: appInput.value.trim() }).then(function (r) {
|
|
1140
|
+
appInput.value = r.address || '';
|
|
1141
|
+
if (!recordInput.value || recordInput.value === app.address) recordInput.value = r.address || '';
|
|
1142
|
+
toast(r.address ? 'Saved. Record starts from ' + r.address : 'Cleared.');
|
|
1143
|
+
load();
|
|
1144
|
+
}, function (err) { toast(err.message); });
|
|
1145
|
+
});
|
|
1146
|
+
row.appendChild(save);
|
|
1147
|
+
box.appendChild(row);
|
|
1148
|
+
box.appendChild(el('div', 'fallback', app.address
|
|
1149
|
+
? 'Saved: ' + app.address + ' - Record and the checks start from it.'
|
|
1150
|
+
: 'The address you open your app at. For a Lovable app it ends in .lovable.app. That is all Kryptheon needs.'));
|
|
1151
|
+
|
|
1152
|
+
if (app.canChooseFolder) {
|
|
1153
|
+
var other = el('div', 'app-folder');
|
|
1154
|
+
other.appendChild(el('p', null, app.webOnly
|
|
1155
|
+
? 'Is your app\'s code on this computer - from Cursor, Bolt or GitHub?'
|
|
1156
|
+
: 'Another app on this computer?'));
|
|
1157
|
+
var pick = button('', 'folder', app.webOnly ? 'Choose its folder...' : 'Open another app\'s folder...', function () {
|
|
1158
|
+
pick.disabled = true;
|
|
1159
|
+
toast('A folder picker is opening - it may be behind this window.');
|
|
1160
|
+
post('/api/choose-folder', {}).then(function (r) {
|
|
1161
|
+
toast('Opening a dashboard for ' + r.opened + ' in a new window. It opens in your browser by itself.');
|
|
1162
|
+
}, function (err) {
|
|
1163
|
+
toast(/no folder was chosen/.test(err.message) ? 'No folder chosen.' : 'It did not open: ' + err.message);
|
|
1164
|
+
}).then(function () { pick.disabled = false; });
|
|
1165
|
+
});
|
|
1166
|
+
other.appendChild(pick);
|
|
1167
|
+
other.appendChild(el('div', 'fallback', 'Its own dashboard opens in a new window, for that folder: recordings and checks then live beside your code, and Kryptheon can read it.'));
|
|
1168
|
+
box.appendChild(other);
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1106
1172
|
function render(next) {
|
|
1107
1173
|
if (!next) return;
|
|
1108
1174
|
state = next;
|
|
1109
1175
|
renderSide();
|
|
1110
1176
|
renderTop();
|
|
1177
|
+
renderApp();
|
|
1111
1178
|
renderNow();
|
|
1112
1179
|
renderSteps();
|
|
1113
1180
|
renderKeep();
|
package/kryptheon-dashboard.html
CHANGED
|
@@ -289,6 +289,15 @@
|
|
|
289
289
|
.field { flex: 1; min-width: 220px; width: 100%; padding: 10px 12px; font: 13px/1.4 var(--mono); color: var(--on-light); background: var(--white); border: 1px solid var(--on-light); border-radius: 0; }
|
|
290
290
|
.field-row .btn { margin-top: 0; }
|
|
291
291
|
.field-note { font-size: 13px; line-height: 1.55; color: var(--muted); }
|
|
292
|
+
/* --- your app: which app this dashboard checks ----------------------------- */
|
|
293
|
+
.app-card { margin-bottom: 26px; }
|
|
294
|
+
#appBox { display: grid; gap: 10px; max-width: 760px; margin-top: 16px; }
|
|
295
|
+
.app-label { font: 11px var(--mono); letter-spacing: 0.08em; text-transform: uppercase; color: var(--muted); }
|
|
296
|
+
.app-here { font-size: 15px; line-height: 1.5; }
|
|
297
|
+
.app-path { display: block; margin-top: 2px; font: 11px var(--mono); color: var(--muted); overflow-wrap: anywhere; }
|
|
298
|
+
.app-folder { display: grid; gap: 10px; margin-top: 14px; padding-top: 16px; border-top: 1px solid var(--line); }
|
|
299
|
+
.app-folder p { font-size: 15px; line-height: 1.5; }
|
|
300
|
+
.app-folder .btn { justify-self: start; }
|
|
292
301
|
.controls pre { margin: 0; padding: 14px 16px; font: 12px/1.65 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; }
|
|
293
302
|
.controls pre.help { color: var(--on-light); background: rgba(18, 22, 29, 0.05); border-left: 2px solid var(--on-light); }
|
|
294
303
|
.controls pre.warn { color: var(--orange-deep); background: rgba(255, 77, 0, 0.06); border-left: 2px solid var(--orange); }
|
|
@@ -351,6 +360,10 @@
|
|
|
351
360
|
<section class="job" id="job" hidden aria-live="polite"></section>
|
|
352
361
|
|
|
353
362
|
<section class="view" id="view-dashboard">
|
|
363
|
+
<div class="card app-card" id="appCard">
|
|
364
|
+
<div class="card-head"><div><h3>Your app</h3><div class="c-sub">What this dashboard checks</div></div></div>
|
|
365
|
+
<div id="appBox"></div>
|
|
366
|
+
</div>
|
|
354
367
|
<div class="sky" id="sky">
|
|
355
368
|
<div class="s-chip" id="skyIcon"></div>
|
|
356
369
|
<div class="s-text">
|
package/kryptheon-dashboard.js
CHANGED
|
@@ -200,6 +200,13 @@ const EVERY_CHANGE = 'after every change';
|
|
|
200
200
|
function startSteps(checks, seen) {
|
|
201
201
|
const on = (id) => (checks.find((c) => c.id === id) || {}).enabled;
|
|
202
202
|
const nightly = seen.nightly || null;
|
|
203
|
+
// With no code on this computer - the home folder's dashboard - there is
|
|
204
|
+
// no code to read, and "Read your frontend code" would only say it read
|
|
205
|
+
// nothing. The app is checked by its address.
|
|
206
|
+
return startStepList(on, seen, nightly).filter((s) => !(seen.webOnly && s.id === 'frontend'));
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function startStepList(on, seen, nightly) {
|
|
203
210
|
return [
|
|
204
211
|
{
|
|
205
212
|
id: 'frontend',
|
|
@@ -508,7 +515,9 @@ function buildState(root, env) {
|
|
|
508
515
|
};
|
|
509
516
|
});
|
|
510
517
|
|
|
518
|
+
const webOnly = isHomeKept(root, env);
|
|
511
519
|
const steps = startSteps(checks, {
|
|
520
|
+
webOnly: webOnly,
|
|
512
521
|
codeAt: code ? code.checkedAt : null,
|
|
513
522
|
recordings: recordings,
|
|
514
523
|
lastRunAt: lastRun ? lastRun.runAt : null,
|
|
@@ -542,6 +551,15 @@ function buildState(root, env) {
|
|
|
542
551
|
runs: history.slice().reverse().map((r) => ({ runAt: r.runAt, status: r.status, passed: r.passed, failed: r.failed, durationMs: r.durationMs })),
|
|
543
552
|
recordings: recordings,
|
|
544
553
|
steps: steps,
|
|
554
|
+
// Which app this dashboard is for: its web address, kept here, and whether
|
|
555
|
+
// its code is on this computer at all. The dashboard started from the home
|
|
556
|
+
// folder (Win+R) holds recordings only - the app is a web address.
|
|
557
|
+
app: {
|
|
558
|
+
address: readAppAddress(paths),
|
|
559
|
+
webOnly: webOnly,
|
|
560
|
+
folder: root,
|
|
561
|
+
canChooseFolder: process.platform === 'win32' || process.platform === 'darwin',
|
|
562
|
+
},
|
|
545
563
|
next: nextAction(findings, steps),
|
|
546
564
|
baselines: Object.keys(baselines).length,
|
|
547
565
|
changes: recentChanges(root),
|
|
@@ -613,6 +631,33 @@ const JOB_LINES = 400;
|
|
|
613
631
|
* anything that could be read as an option by the command it is passed to,
|
|
614
632
|
* and anything with a scheme other than http(s).
|
|
615
633
|
*/
|
|
634
|
+
/**
|
|
635
|
+
* The dashboard started from the home folder: the kryptheon-tests folder
|
|
636
|
+
* there, which holds recordings and nothing of the app's own code.
|
|
637
|
+
*/
|
|
638
|
+
function isHomeKept(root, env) {
|
|
639
|
+
const environment = env || process.env;
|
|
640
|
+
const home = environment.USERPROFILE || environment.HOME || require('os').homedir();
|
|
641
|
+
const tidy = (p) => (process.platform === 'win32' ? path.resolve(p).toLowerCase() : path.resolve(p));
|
|
642
|
+
return path.basename(root) === 'kryptheon-tests' && tidy(path.dirname(root)) === tidy(home);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/** The app's web address, as saved on the page - or empty. */
|
|
646
|
+
function readAppAddress(paths) {
|
|
647
|
+
const saved = readJson(path.join(paths.dir, 'app.json'));
|
|
648
|
+
return saved && typeof saved.address === 'string' ? saved.address : '';
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function saveAppAddress(paths, address) {
|
|
652
|
+
try {
|
|
653
|
+
fs.mkdirSync(paths.dir, { recursive: true });
|
|
654
|
+
fs.writeFileSync(path.join(paths.dir, 'app.json'), JSON.stringify({ address: address, savedAt: new Date().toISOString() }, null, 2));
|
|
655
|
+
return true;
|
|
656
|
+
} catch (err) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
|
|
616
661
|
function appAddress(value) {
|
|
617
662
|
if (typeof value !== 'string') return null;
|
|
618
663
|
const text = value.trim();
|
|
@@ -859,6 +904,26 @@ function createServer(root, options) {
|
|
|
859
904
|
return begin('db', DB_ACTIONS[action].title, DB_ACTIONS[action].command,
|
|
860
905
|
(say) => opts.db(action, connection, say), secretsOf(connection));
|
|
861
906
|
}
|
|
907
|
+
if (url.pathname === '/api/app') {
|
|
908
|
+
// The app's web address: saved for this project, so Record and the
|
|
909
|
+
// outside check start from it. Empty clears it.
|
|
910
|
+
const raw = typeof input.address === 'string' ? input.address.trim() : '';
|
|
911
|
+
const address = raw ? appAddress(raw) : '';
|
|
912
|
+
if (address === null) return json(res, 400, { ok: false, why: 'that is not a web address - it should look like https://my-app.lovable.app' });
|
|
913
|
+
const full = address && !/^https?:\/\//i.test(address) ? 'https://' + address : address;
|
|
914
|
+
return saveAppAddress(store.pathsFor(root, env), full)
|
|
915
|
+
? json(res, 200, { ok: true, address: full })
|
|
916
|
+
: json(res, 500, { ok: false, why: 'it could not be saved on this computer' });
|
|
917
|
+
}
|
|
918
|
+
if (url.pathname === '/api/choose-folder' && typeof opts.chooseFolder === 'function' && typeof opts.openFolder === 'function') {
|
|
919
|
+
// The person's own folder picker, on this computer; then a dashboard
|
|
920
|
+
// for that folder in a window of its own. Nothing is run in it here.
|
|
921
|
+
return Promise.resolve(opts.chooseFolder()).then((folder) => {
|
|
922
|
+
if (!folder) return json(res, 200, { ok: false, cancelled: true, why: 'no folder was chosen' });
|
|
923
|
+
const opened = opts.openFolder(folder);
|
|
924
|
+
return json(res, opened.ok ? 200 : 400, Object.assign({ folder: folder }, opened));
|
|
925
|
+
}, (err) => json(res, 500, { ok: false, why: err.message }));
|
|
926
|
+
}
|
|
862
927
|
if (url.pathname === '/api/db/words' && typeof opts.words === 'function') {
|
|
863
928
|
// What kryptheon-night itself would say about this string - its help,
|
|
864
929
|
// its consent screens, its warnings - asked of it without connecting.
|