kryptheon 0.1.31 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/kryptheon.js +43 -1
- package/kryptheon-autostart.js +148 -0
- package/kryptheon-dashboard-page.js +127 -14
- package/kryptheon-dashboard.html +26 -3
- package/kryptheon-dashboard.js +49 -0
- package/package.json +5 -3
package/bin/kryptheon.js
CHANGED
|
@@ -2321,7 +2321,9 @@ function recheckCommand(id) {
|
|
|
2321
2321
|
// One page on this machine for everything Kryptheon knows about this project
|
|
2322
2322
|
// (kryptheon-dashboard.js). Runs until Ctrl+C.
|
|
2323
2323
|
|
|
2324
|
-
|
|
2324
|
+
// KRYPTHEON_DASHBOARD_PORT is for the checks, which must not use the port of
|
|
2325
|
+
// a dashboard the person has open.
|
|
2326
|
+
const DASHBOARD_PORT = Number(process.env.KRYPTHEON_DASHBOARD_PORT) || 4789;
|
|
2325
2327
|
|
|
2326
2328
|
/**
|
|
2327
2329
|
* Runs a command and hands every line it prints to `say`, as it prints it -
|
|
@@ -2511,12 +2513,52 @@ function openInBrowser(url) {
|
|
|
2511
2513
|
}
|
|
2512
2514
|
}
|
|
2513
2515
|
|
|
2516
|
+
/**
|
|
2517
|
+
* Whether this project's dashboard is already running on the usual port -
|
|
2518
|
+
* started at sign-in, most likely - by asking it which project it shows.
|
|
2519
|
+
* Never fatal: no answer, a slow one, or another project's means no.
|
|
2520
|
+
*/
|
|
2521
|
+
function dashboardAlreadyRunning(port) {
|
|
2522
|
+
return new Promise((resolve) => {
|
|
2523
|
+
const req = require('http').get({ host: '127.0.0.1', port: port, path: '/api/state', headers: { host: '127.0.0.1:' + port }, timeout: 2000 }, (res) => {
|
|
2524
|
+
let body = '';
|
|
2525
|
+
res.setEncoding('utf8');
|
|
2526
|
+
res.on('data', (chunk) => { if (body.length < 5000000) body += chunk; });
|
|
2527
|
+
res.on('end', () => {
|
|
2528
|
+
try {
|
|
2529
|
+
const root = JSON.parse(body).project.root;
|
|
2530
|
+
const same = (a) => (process.platform === 'win32' ? path.resolve(a).toLowerCase() : path.resolve(a));
|
|
2531
|
+
resolve(res.statusCode === 200 && typeof root === 'string' && same(root) === same(USER_DIR));
|
|
2532
|
+
} catch (err) {
|
|
2533
|
+
resolve(false);
|
|
2534
|
+
}
|
|
2535
|
+
});
|
|
2536
|
+
});
|
|
2537
|
+
req.on('timeout', () => req.destroy());
|
|
2538
|
+
req.on('error', () => resolve(false));
|
|
2539
|
+
});
|
|
2540
|
+
}
|
|
2541
|
+
|
|
2514
2542
|
async function dashboardCommand() {
|
|
2515
2543
|
const here = project.inspectProject(USER_DIR);
|
|
2516
2544
|
if (!here.ok) {
|
|
2517
2545
|
for (const line of project.noProjectLines(here)) console.error(line);
|
|
2518
2546
|
return 1;
|
|
2519
2547
|
}
|
|
2548
|
+
// Running already: open that one rather than a second, which would watch
|
|
2549
|
+
// the same project twice. Exit code 0 is how the launcher knows.
|
|
2550
|
+
if (await dashboardAlreadyRunning(DASHBOARD_PORT)) {
|
|
2551
|
+
const address = 'http://127.0.0.1:' + DASHBOARD_PORT + '/';
|
|
2552
|
+
const shown = openInBrowser(address);
|
|
2553
|
+
console.log('');
|
|
2554
|
+
console.log(' The Kryptheon dashboard for ' + path.basename(USER_DIR) + ' is already running:');
|
|
2555
|
+
console.log('');
|
|
2556
|
+
console.log(' ' + address);
|
|
2557
|
+
console.log('');
|
|
2558
|
+
console.log(shown ? ' It is opening in your browser.' : ' Open that address in your browser.');
|
|
2559
|
+
console.log('');
|
|
2560
|
+
return 0;
|
|
2561
|
+
}
|
|
2520
2562
|
const opened = openStore();
|
|
2521
2563
|
if (!opened) return 1;
|
|
2522
2564
|
const dash = require(path.join(PACKAGE_DIR, 'kryptheon-dashboard.js'));
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* Start with Windows: the dashboard for one project, started when the person
|
|
5
|
+
* signs in, so it is watching whenever the laptop is on.
|
|
6
|
+
*
|
|
7
|
+
* It is one small .cmd file in the person's own Startup folder - the plainest
|
|
8
|
+
* way Windows has, seen and removable in Task Manager's Startup apps and in
|
|
9
|
+
* the folder itself. The file says what it is and runs one thing:
|
|
10
|
+
* npx kryptheon@latest dashboard, in a minimized window on the taskbar. A
|
|
11
|
+
* window, not a hidden process: something that starts by itself at sign-in
|
|
12
|
+
* should be seen, and closing it stops it.
|
|
13
|
+
*
|
|
14
|
+
* Only the file this module wrote is ever removed: it is recognised by its
|
|
15
|
+
* first lines, never by its name alone.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
|
|
22
|
+
const MARK = 'rem Kryptheon - starts the Kryptheon dashboard for this project when you sign in to Windows.';
|
|
23
|
+
const CRLF = '\r\n';
|
|
24
|
+
|
|
25
|
+
/** The Startup folder, or null where there is none (not Windows). */
|
|
26
|
+
function startupDir(env, platform) {
|
|
27
|
+
const e = env || process.env;
|
|
28
|
+
// For the checks: a folder of their own, never the real Startup folder.
|
|
29
|
+
if (e.KRYPTHEON_STARTUP_DIR) return e.KRYPTHEON_STARTUP_DIR;
|
|
30
|
+
if ((platform || process.platform) !== 'win32' || !e.APPDATA) return null;
|
|
31
|
+
return path.join(e.APPDATA, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** The folder's name as it may be shown in a file name and a window title. */
|
|
35
|
+
function plainName(root) {
|
|
36
|
+
return path.basename(root).replace(/[^A-Za-z0-9 ._-]/g, '_').slice(0, 40) || 'project';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* One file per project, named after the folder and told apart by its full
|
|
41
|
+
* path - two projects called "app" get two files. Windows paths are compared
|
|
42
|
+
* without regard to case, so the same folder spelled two ways is one file.
|
|
43
|
+
*/
|
|
44
|
+
function fileFor(root, env, platform) {
|
|
45
|
+
const dir = startupDir(env, platform);
|
|
46
|
+
if (!dir) return null;
|
|
47
|
+
const full = path.resolve(root);
|
|
48
|
+
const key = (platform || process.platform) === 'win32' ? full.toLowerCase() : full;
|
|
49
|
+
const id = crypto.createHash('sha256').update(key).digest('hex').slice(0, 8);
|
|
50
|
+
return path.join(dir, 'Kryptheon dashboard - ' + plainName(root) + ' ' + id + '.cmd');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* The file's text. The project's path appears only inside double quotes, so
|
|
55
|
+
* & ^ ( ) and spaces in it are read as themselves; % is doubled, the one
|
|
56
|
+
* character cmd still reads inside quotes. The file is UTF-8 and switches
|
|
57
|
+
* the window to UTF-8 before the path is read, for folder names in any
|
|
58
|
+
* language.
|
|
59
|
+
*/
|
|
60
|
+
function script(root) {
|
|
61
|
+
const full = path.resolve(root);
|
|
62
|
+
const quoted = '"' + full.replace(/%/g, '%%') + '"';
|
|
63
|
+
const name = plainName(root);
|
|
64
|
+
return [
|
|
65
|
+
'@echo off',
|
|
66
|
+
'chcp 65001 >nul',
|
|
67
|
+
MARK,
|
|
68
|
+
'rem ' + quoted,
|
|
69
|
+
'rem Put here by "Start with Windows" on the Kryptheon dashboard. To stop it, switch',
|
|
70
|
+
'rem that off on the dashboard, or delete this file. What it runs:',
|
|
71
|
+
'rem npx kryptheon@latest dashboard - nothing else, in a small window on the taskbar.',
|
|
72
|
+
'if "%~1"=="run" goto run',
|
|
73
|
+
// cmd /c, not start's own cmd /k: a window whose file was removed while it
|
|
74
|
+
// ran - switched off on the dashboard - closes instead of waiting at a prompt.
|
|
75
|
+
'start "Kryptheon dashboard - ' + name + '" /min cmd /d /c ""%~f0" run"',
|
|
76
|
+
'exit /b',
|
|
77
|
+
':run',
|
|
78
|
+
'title Kryptheon dashboard - ' + name,
|
|
79
|
+
'rem npx starts the command "kryptheon": never look for it in the current folder,',
|
|
80
|
+
'rem where a Kryptheon.cmd launcher may sit, and the browser is not opened at sign-in.',
|
|
81
|
+
'set NoDefaultCurrentDirectoryInExePath=1',
|
|
82
|
+
'set KRYPTHEON_LAUNCHER=1',
|
|
83
|
+
'set KRYPTHEON_NO_OPEN=1',
|
|
84
|
+
'cd /d ' + quoted + ' || goto gone',
|
|
85
|
+
'echo.',
|
|
86
|
+
'echo Kryptheon dashboard, started when you signed in, for:',
|
|
87
|
+
'echo ' + quoted,
|
|
88
|
+
'echo Closing this window stops it until you sign in again.',
|
|
89
|
+
'echo.',
|
|
90
|
+
'call npx --yes kryptheon@latest dashboard',
|
|
91
|
+
'if not errorlevel 1 exit',
|
|
92
|
+
'rem At sign-in the network may not be up yet: then the copy npx already has.',
|
|
93
|
+
'echo Trying again with the copy already on this computer...',
|
|
94
|
+
'call npx --yes --offline kryptheon@latest dashboard',
|
|
95
|
+
'if not errorlevel 1 exit',
|
|
96
|
+
'echo.',
|
|
97
|
+
'echo The Kryptheon dashboard stopped. The reason, if any, is above.',
|
|
98
|
+
'pause',
|
|
99
|
+
'exit',
|
|
100
|
+
':gone',
|
|
101
|
+
'echo.',
|
|
102
|
+
'echo This project folder is gone, so its dashboard cannot start:',
|
|
103
|
+
'echo ' + quoted,
|
|
104
|
+
'echo To stop this message, delete this file:',
|
|
105
|
+
'echo "%~f0"',
|
|
106
|
+
'pause',
|
|
107
|
+
'exit',
|
|
108
|
+
'',
|
|
109
|
+
].join(CRLF);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Whether it is on for this project, and whether it can be at all. */
|
|
113
|
+
function status(root, env, platform) {
|
|
114
|
+
const file = fileFor(root, env, platform);
|
|
115
|
+
if (!file) return { available: false, on: false, why: 'Starting with the computer is only on Windows for now.' };
|
|
116
|
+
let on = false;
|
|
117
|
+
try {
|
|
118
|
+
on = fs.readFileSync(file, 'utf8').indexOf(MARK) !== -1;
|
|
119
|
+
} catch (err) {
|
|
120
|
+
on = false;
|
|
121
|
+
}
|
|
122
|
+
return { available: true, on: on, file: file };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Switches it on (writes the file) or off (removes only a file it wrote). */
|
|
126
|
+
function setAutostart(root, on, env, platform) {
|
|
127
|
+
if (typeof on !== 'boolean') return { ok: false, why: 'on must be true or false' };
|
|
128
|
+
const file = fileFor(root, env, platform);
|
|
129
|
+
if (!file) return { ok: false, why: 'Starting with the computer is only on Windows for now.' };
|
|
130
|
+
try {
|
|
131
|
+
if (on) {
|
|
132
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
133
|
+
fs.writeFileSync(file, script(root), 'utf8');
|
|
134
|
+
} else {
|
|
135
|
+
let text = null;
|
|
136
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch (err) { text = null; }
|
|
137
|
+
if (text !== null) {
|
|
138
|
+
if (text.indexOf(MARK) === -1) return { ok: false, why: 'the file there was not written by Kryptheon, so it was left alone: ' + file };
|
|
139
|
+
fs.unlinkSync(file);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch (err) {
|
|
143
|
+
return { ok: false, why: 'could not ' + (on ? 'write' : 'remove') + ' ' + file + ': ' + err.message };
|
|
144
|
+
}
|
|
145
|
+
return Object.assign({ ok: true }, status(root, env, platform));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { startupDir, fileFor, script, status, setAutostart, MARK };
|
|
@@ -103,10 +103,24 @@
|
|
|
103
103
|
busy = on;
|
|
104
104
|
Array.prototype.forEach.call(document.querySelectorAll('[data-action]'), function (b) { b.disabled = on; });
|
|
105
105
|
}
|
|
106
|
+
// The clipboard, and when a browser refuses it, the older way: the text
|
|
107
|
+
// put in a box off the screen, selected and copied.
|
|
106
108
|
function copy(text, done) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
109
|
+
function older() {
|
|
110
|
+
var box = el('textarea');
|
|
111
|
+
box.value = text;
|
|
112
|
+
box.setAttribute('readonly', '');
|
|
113
|
+
box.style.position = 'fixed';
|
|
114
|
+
box.style.left = '-9999px';
|
|
115
|
+
document.body.appendChild(box);
|
|
116
|
+
box.select();
|
|
117
|
+
var ok = false;
|
|
118
|
+
try { ok = document.execCommand('copy'); } catch (e) { ok = false; }
|
|
119
|
+
document.body.removeChild(box);
|
|
120
|
+
toast(ok ? done : 'Could not copy - select the text instead');
|
|
121
|
+
}
|
|
122
|
+
if (!navigator.clipboard || !navigator.clipboard.writeText) return older();
|
|
123
|
+
navigator.clipboard.writeText(text).then(function () { toast(done); }, older);
|
|
110
124
|
}
|
|
111
125
|
function button(cls, iconName, text, onClick) {
|
|
112
126
|
var b = el('button', 'btn' + (cls ? ' ' + cls : ''));
|
|
@@ -155,33 +169,70 @@
|
|
|
155
169
|
// lines are what the command printed, already scrubbed of any secret.
|
|
156
170
|
var job = null;
|
|
157
171
|
var lastRecheck = null;
|
|
158
|
-
|
|
172
|
+
// Whether the printed lines are shown: null follows the job - shown while it
|
|
173
|
+
// runs or when it failed, folded away once it finished well - until the
|
|
174
|
+
// person opens or folds them.
|
|
175
|
+
var linesOpen = null;
|
|
176
|
+
// Whether the one prompt for all findings is shown in full.
|
|
177
|
+
var allPromptOpen = false;
|
|
178
|
+
|
|
179
|
+
// The panel sits in the corner of the window, over the page; the page gets
|
|
180
|
+
// room at its foot so nothing stays hidden under it.
|
|
159
181
|
function renderJob() {
|
|
160
182
|
var box = byId('job');
|
|
161
183
|
clear(box);
|
|
162
|
-
|
|
184
|
+
var main = document.querySelector('main');
|
|
185
|
+
var root = document.documentElement;
|
|
186
|
+
if (!job) { box.hidden = true; linesOpen = null; main.style.paddingBottom = ''; root.style.removeProperty('--job-room'); return; }
|
|
163
187
|
box.hidden = false;
|
|
164
|
-
|
|
188
|
+
var ok = !!(job.result && job.result.ok);
|
|
189
|
+
box.className = 'job' + (job.running ? ' on' : ok ? ' ok' : ' bad');
|
|
165
190
|
var head = el('div', 'job-head');
|
|
166
191
|
if (job.running) head.appendChild(el('span', 'spin'));
|
|
167
192
|
head.appendChild(el('b', null, job.title));
|
|
168
|
-
head.appendChild(el('span', 'tag ' + (job.running ? 'sun' :
|
|
169
|
-
job.running ? 'running' :
|
|
193
|
+
head.appendChild(el('span', 'tag ' + (job.running ? 'sun' : ok ? 'leaf' : 'ember'),
|
|
194
|
+
job.running ? 'running' : ok ? 'finished' : 'did not finish'));
|
|
170
195
|
if (!job.running) {
|
|
171
|
-
var close = el('button', 'job-close', '
|
|
196
|
+
var close = el('button', 'job-close', 'Close');
|
|
172
197
|
close.type = 'button';
|
|
173
198
|
close.addEventListener('click', function () { job = null; renderJob(); });
|
|
174
199
|
head.appendChild(close);
|
|
175
200
|
}
|
|
176
201
|
box.appendChild(head);
|
|
177
|
-
var
|
|
178
|
-
|
|
202
|
+
var open = linesOpen === null ? job.running || !ok : linesOpen;
|
|
203
|
+
var lines = null;
|
|
204
|
+
if (open) {
|
|
205
|
+
lines = el('pre', 'job-lines', (job.lines || []).filter(function (l, i, all) { return l || (i && all[i - 1]); }).slice(-60).join('\n') || 'Starting...');
|
|
206
|
+
box.appendChild(lines);
|
|
207
|
+
}
|
|
179
208
|
if (!job.running && job.result) {
|
|
180
209
|
var r = job.result;
|
|
181
|
-
box.appendChild(el('div', 'job-result',
|
|
210
|
+
box.appendChild(el('div', 'job-result', ok ? 'Result: ' + (r.summary || 'done') : 'It did not finish: ' + (r.why || 'see what it printed')));
|
|
211
|
+
}
|
|
212
|
+
if (!job.running) {
|
|
213
|
+
var acts = el('div', 'job-acts');
|
|
214
|
+
// What it found is on Findings; a recording is on Runs. Setting the
|
|
215
|
+
// nightly check up, or removing it, finds nothing to go and see.
|
|
216
|
+
var setUp = job.kind === 'db' && / (un)?install$/.test(job.command || '');
|
|
217
|
+
var next = !ok || setUp ? null : job.kind === 'record' ? { id: 'runs', label: 'See runs' } : { id: 'findings', label: 'See findings' };
|
|
218
|
+
if (next && currentView() !== next.id) {
|
|
219
|
+
var goBtn = el('button', 'go', next.label + ' →');
|
|
220
|
+
goBtn.type = 'button';
|
|
221
|
+
goBtn.addEventListener('click', function () { go(next.id); });
|
|
222
|
+
acts.appendChild(goBtn);
|
|
223
|
+
}
|
|
224
|
+
var fold = el('button', null, open ? 'Hide what it printed' : 'Show what it printed');
|
|
225
|
+
fold.type = 'button';
|
|
226
|
+
fold.setAttribute('aria-expanded', open ? 'true' : 'false');
|
|
227
|
+
fold.addEventListener('click', function () { linesOpen = !open; renderJob(); });
|
|
228
|
+
acts.appendChild(fold);
|
|
229
|
+
box.appendChild(acts);
|
|
182
230
|
}
|
|
183
231
|
if (job.command) box.appendChild(el('div', 'fallback', 'The same in a terminal: ' + job.command));
|
|
184
|
-
lines.scrollTop = lines.scrollHeight;
|
|
232
|
+
if (lines) lines.scrollTop = lines.scrollHeight;
|
|
233
|
+
main.style.paddingBottom = (box.offsetHeight + 48) + 'px';
|
|
234
|
+
// A message pops up above the panel, never over it.
|
|
235
|
+
root.style.setProperty('--job-room', (box.offsetHeight + 12) + 'px');
|
|
185
236
|
}
|
|
186
237
|
|
|
187
238
|
function getJob() {
|
|
@@ -194,6 +245,7 @@
|
|
|
194
245
|
setBusy(true);
|
|
195
246
|
return post(path, body).then(function (started) {
|
|
196
247
|
job = started.job;
|
|
248
|
+
linesOpen = null;
|
|
197
249
|
renderJob();
|
|
198
250
|
return follow();
|
|
199
251
|
}).then(function (done) {
|
|
@@ -255,6 +307,8 @@
|
|
|
255
307
|
byId('viewTitle').textContent = view.title;
|
|
256
308
|
document.title = (id === 'dashboard' ? '' : view.title + ' - ') + 'Kryptheon';
|
|
257
309
|
if (state) renderSide();
|
|
310
|
+
// Its "See findings" goes once Findings is the page on screen.
|
|
311
|
+
if (job && !job.running) renderJob();
|
|
258
312
|
window.scrollTo(0, 0);
|
|
259
313
|
}
|
|
260
314
|
window.addEventListener('hashchange', showView);
|
|
@@ -528,7 +582,7 @@
|
|
|
528
582
|
if (!connection) { toast('Paste your connection string in "Connect your database" first.'); dbInput.focus(); return; }
|
|
529
583
|
runJob('/api/db', { action: 'night', connection: connection }).then(function (done) {
|
|
530
584
|
var r = done && done.result;
|
|
531
|
-
toast(r && r.ok ? 'Read back - see
|
|
585
|
+
toast(r && r.ok ? 'Read back - see Findings' : 'Nothing to read back' + (r && r.why ? ' - ' + r.why : ''));
|
|
532
586
|
}, function () {});
|
|
533
587
|
});
|
|
534
588
|
see.setAttribute('data-action', 'db');
|
|
@@ -724,6 +778,28 @@
|
|
|
724
778
|
if (lastRecheck.newProblems.length) gone.appendChild(el('span', 'tag ember', plural(lastRecheck.newProblems.length, 'problem') + ' newly broken - marked below'));
|
|
725
779
|
body.appendChild(gone);
|
|
726
780
|
}
|
|
781
|
+
// Every fix in one paste, with the rules that keep the AI from breaking
|
|
782
|
+
// something else - whatever the filter shows, it is all of them.
|
|
783
|
+
if (state.allFixPrompt) {
|
|
784
|
+
var many = state.findings.filter(function (f) { return f.fixPrompt; }).length;
|
|
785
|
+
var all = el('div', 'fix-all');
|
|
786
|
+
var words = el('div', 'fix-all-words');
|
|
787
|
+
words.appendChild(el('b', null, 'Fix all ' + many + ' at once'));
|
|
788
|
+
words.appendChild(el('span', null, 'One prompt with every fix, and rules that stop your AI from changing anything else. Then Re-check each one.'));
|
|
789
|
+
all.appendChild(words);
|
|
790
|
+
all.appendChild(button('sun', 'copy', 'Copy one prompt for all', function () {
|
|
791
|
+
copy(state.allFixPrompt, 'One prompt for all ' + many + ' copied - paste it into your AI tool');
|
|
792
|
+
}));
|
|
793
|
+
// To read before pasting, or to select by hand. It stays open across
|
|
794
|
+
// the page's own refresh every few seconds.
|
|
795
|
+
var whole = el('details', 'fix-all-text');
|
|
796
|
+
whole.open = allPromptOpen;
|
|
797
|
+
whole.addEventListener('toggle', function () { allPromptOpen = whole.open; });
|
|
798
|
+
whole.appendChild(el('summary', null, 'See the prompt'));
|
|
799
|
+
whole.appendChild(el('pre', null, state.allFixPrompt));
|
|
800
|
+
all.appendChild(whole);
|
|
801
|
+
body.appendChild(all);
|
|
802
|
+
}
|
|
727
803
|
if (!shown.length) {
|
|
728
804
|
body.appendChild(el('p', 'empty', state.findings.length ? 'Nothing matches.' : 'Nothing found by the checks that are on and have run. That is not the same as nothing wrong - see which checks are not built yet.'));
|
|
729
805
|
return;
|
|
@@ -960,6 +1036,41 @@
|
|
|
960
1036
|
}, function () {});
|
|
961
1037
|
}
|
|
962
1038
|
|
|
1039
|
+
// Start with Windows, and what still runs when the laptop is off.
|
|
1040
|
+
function renderKeep() {
|
|
1041
|
+
var box = byId('keep');
|
|
1042
|
+
clear(box);
|
|
1043
|
+
var a = state.autostart || { available: false, on: false, why: '' };
|
|
1044
|
+
var row = el('div', 'keep-row');
|
|
1045
|
+
if (a.available) {
|
|
1046
|
+
var sw = el('label', 'switch');
|
|
1047
|
+
var input = el('input');
|
|
1048
|
+
input.type = 'checkbox';
|
|
1049
|
+
input.checked = !!a.on;
|
|
1050
|
+
input.setAttribute('aria-label', 'Start with Windows');
|
|
1051
|
+
input.addEventListener('change', function () { setAutostart(input.checked, input); });
|
|
1052
|
+
sw.appendChild(input);
|
|
1053
|
+
sw.appendChild(el('span', 'track'));
|
|
1054
|
+
row.appendChild(sw);
|
|
1055
|
+
}
|
|
1056
|
+
var text = el('div', 'keep-text');
|
|
1057
|
+
var title = el('b', null, 'Start with Windows ');
|
|
1058
|
+
if (a.available) title.appendChild(el('span', 'tag ' + (a.on ? 'leaf' : ''), a.on ? 'on' : 'off'));
|
|
1059
|
+
text.appendChild(title);
|
|
1060
|
+
text.appendChild(el('span', null, !a.available ? (a.why || 'Not on this computer.')
|
|
1061
|
+
: a.on ? 'When you sign in to Windows, this dashboard starts by itself in a small window on the taskbar, with nothing opened. To see it, double-click Kryptheon.cmd as always - it opens the one already running. Closing that taskbar window stops it until you sign in again.'
|
|
1062
|
+
: 'Switch it on and this dashboard starts by itself every time you sign in to Windows, in a small window on the taskbar. Nothing opens - it just watches for changes. Switch it off here any time.'));
|
|
1063
|
+
row.appendChild(text);
|
|
1064
|
+
box.appendChild(row);
|
|
1065
|
+
box.appendChild(el('div', 'keep-off', 'With the laptop off, nothing on it can run. The nightly database check is different: once set up in Start here, it runs inside your database itself, and its result is waiting the next time you open this page.'));
|
|
1066
|
+
}
|
|
1067
|
+
function setAutostart(on, input) {
|
|
1068
|
+
post('/api/autostart', { on: on }).then(function () {
|
|
1069
|
+
toast(on ? 'On: it starts by itself when you sign in to Windows' : 'Off: it no longer starts by itself');
|
|
1070
|
+
return load();
|
|
1071
|
+
}).catch(function (err) { input.checked = !on; toast('Not saved: ' + err.message); });
|
|
1072
|
+
}
|
|
1073
|
+
|
|
963
1074
|
function setCheck(id, on, input) {
|
|
964
1075
|
post('/api/check', { id: id, on: on }).then(function () {
|
|
965
1076
|
toast((on ? 'Switched on: ' : 'Switched off: ') + (CHECK_NAME[id] || id));
|
|
@@ -988,6 +1099,7 @@
|
|
|
988
1099
|
renderTop();
|
|
989
1100
|
renderNow();
|
|
990
1101
|
renderSteps();
|
|
1102
|
+
renderKeep();
|
|
991
1103
|
renderStatusFilter();
|
|
992
1104
|
renderFindings();
|
|
993
1105
|
renderTimeline();
|
|
@@ -1012,6 +1124,7 @@
|
|
|
1012
1124
|
return getJob().then(function (now) {
|
|
1013
1125
|
if (!now || !now.running) return;
|
|
1014
1126
|
job = now;
|
|
1127
|
+
linesOpen = null;
|
|
1015
1128
|
setBusy(true);
|
|
1016
1129
|
renderJob();
|
|
1017
1130
|
return follow().then(function () { setBusy(false); return load(); });
|
package/kryptheon-dashboard.html
CHANGED
|
@@ -265,14 +265,22 @@
|
|
|
265
265
|
@media (max-width: 600px) { .files div { grid-template-columns: minmax(0, 1fr); gap: 2px; } }
|
|
266
266
|
|
|
267
267
|
/* --- a run in progress, and what it said ---------------------------------- */
|
|
268
|
-
|
|
268
|
+
/* Pinned to the corner of the window, so it is seen from wherever the button
|
|
269
|
+
was pressed - never at the top of a page scrolled away from. */
|
|
270
|
+
.job { position: fixed; right: 24px; bottom: 24px; z-index: 9; display: flex; flex-direction: column; width: min(520px, calc(100vw - 48px)); max-height: calc(100vh - 48px); padding: 16px 18px 18px; color: var(--on-dark); background: var(--ink); border-left: 3px solid var(--orange); box-shadow: 0 18px 50px rgba(11, 14, 20, 0.35); animation: job-in 0.35s var(--ease); }
|
|
271
|
+
.job[hidden] { display: none; }
|
|
269
272
|
.job.ok { border-left-color: var(--on-dark); }
|
|
273
|
+
@keyframes job-in { from { opacity: 0; transform: translateY(16px); } }
|
|
274
|
+
@media (max-width: 600px) { .job { right: 16px; bottom: 16px; width: calc(100vw - 32px); } }
|
|
270
275
|
.job-head { display: flex; align-items: center; gap: 12px; flex-wrap: wrap; }
|
|
271
276
|
.job-head b { font-weight: 800; font-stretch: 74%; font-size: 1.1rem; text-transform: uppercase; overflow-wrap: anywhere; }
|
|
272
277
|
.job-head .tag { color: var(--on-dark); border-color: var(--line-dark); }
|
|
273
278
|
.job-close { margin-left: auto; padding: 4px 10px; font: 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; border: 1px solid var(--line-dark); }
|
|
274
|
-
.job-lines { max-height:
|
|
279
|
+
.job-lines { min-height: 0; max-height: 220px; margin: 12px 0 0; overflow: auto; font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; opacity: 0.85; }
|
|
275
280
|
.job-result { margin-top: 12px; font-weight: 700; }
|
|
281
|
+
.job-acts { display: flex; flex-wrap: wrap; gap: 10px; margin-top: 12px; }
|
|
282
|
+
.job-acts button { padding: 6px 12px; font: 10px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; color: var(--on-dark); border: 1px solid var(--line-dark); }
|
|
283
|
+
.job-acts button.go { color: var(--ink); background: var(--on-dark); border-color: var(--on-dark); }
|
|
276
284
|
.job .fallback { color: rgba(233, 237, 242, 0.6); }
|
|
277
285
|
|
|
278
286
|
/* --- boxes to type into, the words around them, and the yes -------------- */
|
|
@@ -293,9 +301,20 @@
|
|
|
293
301
|
.fallback { font: 10.5px/1.4 var(--mono); color: var(--muted); overflow-wrap: anywhere; }
|
|
294
302
|
.tag.when { color: var(--on-light); border-style: dotted; }
|
|
295
303
|
.f-quick { display: flex; flex-wrap: wrap; align-items: flex-start; gap: 10px; padding: 0 8px 16px 20px; margin-top: -6px; }
|
|
304
|
+
.keep-row { display: flex; align-items: flex-start; gap: 16px; padding: 18px 8px 18px 0; }
|
|
305
|
+
.keep-row .switch { margin-top: 2px; }
|
|
306
|
+
.keep-text { display: grid; gap: 6px; min-width: 0; font-size: 14px; }
|
|
307
|
+
.keep-text b { font-weight: 800; }
|
|
308
|
+
.keep-off { padding-top: 14px; border-top: 1px solid var(--line); font-size: 14px; opacity: 0.8; }
|
|
309
|
+
.fix-all { display: flex; flex-wrap: wrap; align-items: center; gap: 14px 20px; padding: 16px 8px 16px 20px; border-bottom: 1px solid var(--line); box-shadow: inset 3px 0 0 var(--orange); }
|
|
310
|
+
.fix-all-words { flex: 1 1 260px; display: grid; gap: 4px; font-size: 14px; }
|
|
311
|
+
.fix-all-words b { font-weight: 800; font-stretch: 74%; font-size: 1.05rem; text-transform: uppercase; }
|
|
312
|
+
.fix-all-text { flex: 1 1 100%; min-width: 0; }
|
|
313
|
+
.fix-all-text summary { width: max-content; font: 11px var(--mono); letter-spacing: 0.12em; text-transform: uppercase; cursor: pointer; opacity: 0.75; }
|
|
314
|
+
.fix-all-text pre { max-height: 320px; margin: 10px 0 0; padding: 14px 16px; overflow: auto; font: 12px/1.6 var(--mono); white-space: pre-wrap; overflow-wrap: anywhere; border: 1px solid var(--line); }
|
|
296
315
|
.rechecked { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; padding: 14px 8px 14px 20px; font-size: 14px; border-bottom: 1px solid var(--line); box-shadow: inset 3px 0 0 var(--on-light); }
|
|
297
316
|
|
|
298
|
-
.toast { position: fixed; left: 50%; bottom: 24px; z-index: 10; max-width: calc(100vw - 32px); padding: 14px 20px; font-size: 14px; color: var(--on-dark); background: var(--ink); border-left: 2px solid var(--orange); opacity: 0; transform: translate(-50%, 20px); transition: opacity 0.35s var(--ease), transform 0.35s var(--ease); pointer-events: none; }
|
|
317
|
+
.toast { position: fixed; left: 50%; bottom: calc(24px + var(--job-room, 0px)); z-index: 10; max-width: calc(100vw - 32px); padding: 14px 20px; font-size: 14px; color: var(--on-dark); background: var(--ink); border-left: 2px solid var(--orange); opacity: 0; transform: translate(-50%, 20px); transition: opacity 0.35s var(--ease), transform 0.35s var(--ease); pointer-events: none; }
|
|
299
318
|
.toast.show { opacity: 1; transform: translate(-50%, 0); }
|
|
300
319
|
|
|
301
320
|
@media (prefers-reduced-motion: reduce) {
|
|
@@ -345,6 +364,10 @@
|
|
|
345
364
|
<div class="card-head"><div><h3>Start here</h3><div class="c-sub">Do these in order. A step is done only once it has actually run.</div></div></div>
|
|
346
365
|
<div class="steps" id="steps"></div>
|
|
347
366
|
</div>
|
|
367
|
+
<div class="card gap" id="keepCard">
|
|
368
|
+
<div class="card-head"><div><h3>Keep it running</h3><div class="c-sub">So it is watching whenever your laptop is on - without opening anything</div></div></div>
|
|
369
|
+
<div class="keep" id="keep"></div>
|
|
370
|
+
</div>
|
|
348
371
|
<div class="card gap">
|
|
349
372
|
<div class="card-head"><div><h3>Connect your AI tool</h3><div class="c-sub">Optional - your assistant then checks its own work, and does not call a change done until the check passes</div></div></div>
|
|
350
373
|
<div class="ai">
|
package/kryptheon-dashboard.js
CHANGED
|
@@ -24,6 +24,7 @@ const crypto = require('crypto');
|
|
|
24
24
|
const { spawnSync } = require('child_process');
|
|
25
25
|
const store = require('./kryptheon-store.js');
|
|
26
26
|
const changes = require('./kryptheon-changes.js');
|
|
27
|
+
const autostart = require('./kryptheon-autostart.js');
|
|
27
28
|
|
|
28
29
|
/**
|
|
29
30
|
* Every check the dashboard knows the name of, in the order the person asked
|
|
@@ -284,6 +285,44 @@ function nextAction(findings, steps) {
|
|
|
284
285
|
return { kind: 'quiet', title: 'Nothing open' };
|
|
285
286
|
}
|
|
286
287
|
|
|
288
|
+
/**
|
|
289
|
+
* One prompt that fixes every open finding at once, for pasting into an AI
|
|
290
|
+
* tool, or '' when fewer than two findings have a prompt (one is its own).
|
|
291
|
+
* Each finding's own prompt goes in as it is, in the order the page shows
|
|
292
|
+
* them, most severe first. Around them are the rules that keep a fix from
|
|
293
|
+
* breaking something else: only what is listed, the smallest change, one at
|
|
294
|
+
* a time with the app checked between, nothing weakened to make a finding go
|
|
295
|
+
* away, ask rather than guess. The re-checks come once, at the end.
|
|
296
|
+
*
|
|
297
|
+
* items: [{ id, severity, headline, where, prompt }] - prompt without its
|
|
298
|
+
* re-check line.
|
|
299
|
+
*/
|
|
300
|
+
function combinedPrompt(items) {
|
|
301
|
+
const withPrompt = items.filter((f) => f.prompt);
|
|
302
|
+
if (withPrompt.length < 2) return '';
|
|
303
|
+
const n = withPrompt.length;
|
|
304
|
+
const lines = [
|
|
305
|
+
'Kryptheon checked this project and found ' + n + ' problems. Fix all ' + n + ', carefully - not breaking anything matters more than speed.',
|
|
306
|
+
'',
|
|
307
|
+
'Rules for every fix:',
|
|
308
|
+
'1. Fix only the problems listed below. Do not refactor, rename, reformat or "improve" anything else, and do not touch a file a fix does not need.',
|
|
309
|
+
'2. Make the smallest change that fixes each one. Every feature must keep working exactly as it does now for the people meant to use it.',
|
|
310
|
+
'3. Never delete or weaken a check, test, validation or security rule to make a problem go away, and never delete or rewrite existing data.',
|
|
311
|
+
'4. Do them one at a time, in the order below. After each one, make sure the app still builds and its existing tests still pass before starting the next.',
|
|
312
|
+
'5. If a fix needs a decision only I can make, or you are not sure a change is safe, stop and ask me instead of guessing.',
|
|
313
|
+
'6. When you are done, list every file you changed and why, one line each.',
|
|
314
|
+
];
|
|
315
|
+
withPrompt.forEach((f, i) => {
|
|
316
|
+
lines.push('', '======== Problem ' + (i + 1) + ' of ' + n + ' - ' + f.severity + ': ' + f.headline + (f.where ? ' (' + f.where + ')' : ''), '', f.prompt.trim());
|
|
317
|
+
});
|
|
318
|
+
lines.push('', '======== When all ' + n + ' are fixed',
|
|
319
|
+
'',
|
|
320
|
+
'Prove each fix: run these one at a time (or press Re-check on each finding in the Kryptheon dashboard). A fix counts only when it says FIXED, and a re-check also says if anything else newly broke - if it does, fix that before anything else.',
|
|
321
|
+
'');
|
|
322
|
+
withPrompt.forEach((f) => lines.push(' npx kryptheon recheck ' + f.id));
|
|
323
|
+
return lines.join('\n');
|
|
324
|
+
}
|
|
325
|
+
|
|
287
326
|
/**
|
|
288
327
|
* One sentence about the nightly run inside the database, or null when
|
|
289
328
|
* kryptheon-night has never been asked. Its four states are four different
|
|
@@ -433,9 +472,11 @@ function buildState(root, env) {
|
|
|
433
472
|
// prove the fix. Added here, where the loop lives, so every check's prompt
|
|
434
473
|
// ends the same way and the detection code itself stays untouched.
|
|
435
474
|
const attempts = readHistory(paths.fixes, 500);
|
|
475
|
+
const forAll = [];
|
|
436
476
|
for (const f of findings) {
|
|
437
477
|
f.id = changes.findingId(f);
|
|
438
478
|
f.attempts = attempts.filter((a) => a.id === f.id).map((a) => ({ at: a.at, verdict: a.verdict, why: a.why || '', newProblems: (a.newProblems || []).length }));
|
|
479
|
+
forAll.push({ id: f.id, severity: f.severity, headline: f.headline, where: f.where, prompt: f.fixPrompt || '' });
|
|
439
480
|
if (f.fixPrompt) {
|
|
440
481
|
f.fixPrompt += '\n\nWhen you have fixed it, prove it: run npx kryptheon recheck ' + f.id +
|
|
441
482
|
' (or press Re-check on this finding in the Kryptheon dashboard). It is fixed only when that says FIXED.';
|
|
@@ -497,6 +538,7 @@ function buildState(root, env) {
|
|
|
497
538
|
proven: proven,
|
|
498
539
|
checks: checks,
|
|
499
540
|
findings: findings,
|
|
541
|
+
allFixPrompt: combinedPrompt(forAll),
|
|
500
542
|
runs: history.slice().reverse().map((r) => ({ runAt: r.runAt, status: r.status, passed: r.passed, failed: r.failed, durationMs: r.durationMs })),
|
|
501
543
|
recordings: recordings,
|
|
502
544
|
steps: steps,
|
|
@@ -506,6 +548,8 @@ function buildState(root, env) {
|
|
|
506
548
|
// Part 3 is where a version gets verified. Until then this says so,
|
|
507
549
|
// rather than leaving a blank that could be read either way.
|
|
508
550
|
verification: { state: 'not verified', why: 'Kryptheon Verified is not built yet. No version of this project has been verified.' },
|
|
551
|
+
// Whether this dashboard starts by itself when the person signs in.
|
|
552
|
+
autostart: (() => { const s = autostart.status(root, env); return { available: s.available, on: s.on, why: s.why || '' }; })(),
|
|
509
553
|
};
|
|
510
554
|
}
|
|
511
555
|
|
|
@@ -751,6 +795,10 @@ function createServer(root, options) {
|
|
|
751
795
|
const r = setCheck(store.pathsFor(root, env), String(input.id || ''), input.on);
|
|
752
796
|
return json(res, r.ok ? 200 : 400, r);
|
|
753
797
|
}
|
|
798
|
+
if (url.pathname === '/api/autostart') {
|
|
799
|
+
const r = autostart.setAutostart(root, input.on, env);
|
|
800
|
+
return json(res, r.ok ? 200 : 400, { ok: r.ok, on: !!r.on, why: r.why || '' });
|
|
801
|
+
}
|
|
754
802
|
// Everything below starts a job: it answers at once with the job, and
|
|
755
803
|
// the page follows it on /api/job. One at a time - two browsers
|
|
756
804
|
// replaying against one app, or two attacks on one database, would
|
|
@@ -858,6 +906,7 @@ module.exports = {
|
|
|
858
906
|
databaseVerdict: databaseVerdict,
|
|
859
907
|
startSteps: startSteps,
|
|
860
908
|
nextAction: nextAction,
|
|
909
|
+
combinedPrompt: combinedPrompt,
|
|
861
910
|
readConfig: readConfig,
|
|
862
911
|
setCheck: setCheck,
|
|
863
912
|
createServer: createServer,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kryptheon",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.33",
|
|
4
4
|
"description": "Record what you do in your app and get plain-language reports when it breaks.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"kryptheon-spacemono.woff2",
|
|
43
43
|
"kryptheon-archivo.woff2",
|
|
44
44
|
"kryptheon-changes.js",
|
|
45
|
-
"kryptheon-look.js"
|
|
45
|
+
"kryptheon-look.js",
|
|
46
|
+
"kryptheon-autostart.js"
|
|
46
47
|
],
|
|
47
48
|
"engines": {
|
|
48
49
|
"node": ">=20.6.0"
|
|
@@ -51,11 +52,12 @@
|
|
|
51
52
|
"@playwright/test": "^1.62.1"
|
|
52
53
|
},
|
|
53
54
|
"scripts": {
|
|
54
|
-
"check": "node kryptheon-store.check.js && node kryptheon-code.check.js && node kryptheon-constdata.check.js && node kryptheon-accept.check.js && node kryptheon-keys.check.js && node kryptheon-dashboard.check.js && node kryptheon-changes.check.js && node kryptheon-look.check.js && node kryptheon-verify.check.js && node kryptheon-recheck.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",
|
|
55
|
+
"check": "node kryptheon-store.check.js && node kryptheon-code.check.js && node kryptheon-constdata.check.js && node kryptheon-accept.check.js && node kryptheon-keys.check.js && node kryptheon-dashboard.check.js && node kryptheon-autostart.check.js && node kryptheon-changes.check.js && node kryptheon-look.check.js && node kryptheon-verify.check.js && node kryptheon-recheck.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",
|
|
55
56
|
"check:store": "node kryptheon-store.check.js",
|
|
56
57
|
"check:code": "node kryptheon-code.check.js",
|
|
57
58
|
"check:constdata": "node kryptheon-constdata.check.js",
|
|
58
59
|
"check:dashboard": "node kryptheon-dashboard.check.js",
|
|
60
|
+
"check:autostart": "node kryptheon-autostart.check.js",
|
|
59
61
|
"check:changes": "node kryptheon-changes.check.js",
|
|
60
62
|
"check:look": "node kryptheon-look.check.js",
|
|
61
63
|
"check:verify": "node kryptheon-verify.check.js",
|