kryptheon 0.1.20 → 0.1.22
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 +34 -0
- package/bin/kryptheon.js +390 -0
- package/kryptheon-archivo.woff2 +0 -0
- package/kryptheon-changes.js +222 -0
- package/kryptheon-code.js +564 -0
- package/kryptheon-dashboard-page.js +602 -0
- package/kryptheon-dashboard.html +341 -0
- package/kryptheon-dashboard.js +539 -0
- package/kryptheon-fonts-OFL.txt +98 -0
- package/kryptheon-logo.png +0 -0
- package/kryptheon-look.js +259 -0
- package/kryptheon-spacemono.woff2 +0 -0
- package/kryptheon-store.js +15 -0
- package/package.json +19 -3
package/README.md
CHANGED
|
@@ -161,6 +161,40 @@ 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
|
+
## Dashboard
|
|
165
|
+
|
|
166
|
+
```
|
|
167
|
+
npx kryptheon
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
In a terminal, with no command, this opens a page on your own machine
|
|
171
|
+
(`http://127.0.0.1:4789/`) for this project: what is broken, what needs a
|
|
172
|
+
look, every run, recent changes, and switches for each check. `Run now`
|
|
173
|
+
replays your recordings or reads your code from the page. It is only
|
|
174
|
+
reachable from this machine; press Ctrl+C to stop it. From a script, use
|
|
175
|
+
`npx kryptheon dashboard`.
|
|
176
|
+
|
|
177
|
+
It opens on the one thing to do next, and under it "Start here": read your
|
|
178
|
+
code, record your main flows, replay them, check your database - in that
|
|
179
|
+
order, each marked done only once it has actually run.
|
|
180
|
+
|
|
181
|
+
Checks that are not built yet are listed as "not available" - never as
|
|
182
|
+
passed.
|
|
183
|
+
|
|
184
|
+
## Reading your code
|
|
185
|
+
|
|
186
|
+
```
|
|
187
|
+
npx kryptheon code
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
Reads your frontend code for values put into the page as HTML - `innerHTML`,
|
|
191
|
+
`insertAdjacentHTML`, `document.write`, React's `dangerouslySetInnerHTML`,
|
|
192
|
+
Vue's `v-html`. If such a value can hold text someone typed, a `<` in it can
|
|
193
|
+
run script with the signed-in person's session. Nothing is run; every result
|
|
194
|
+
is "verification required", ranked HIGH when the value comes from a server
|
|
195
|
+
response or an error message, MEDIUM when the code alone cannot say. The fix
|
|
196
|
+
prompt for each one names the file, line and value.
|
|
197
|
+
|
|
164
198
|
## What goes where
|
|
165
199
|
|
|
166
200
|
Only your recordings go in your project, in `tests/`. Everything Kryptheon
|
package/bin/kryptheon.js
CHANGED
|
@@ -85,6 +85,10 @@ function usage() {
|
|
|
85
85
|
console.log(' npx kryptheon accept <name> agree that one test\'s new result is the correct one');
|
|
86
86
|
console.log(' npx kryptheon remove [name] list the recordings, or remove one you no longer want');
|
|
87
87
|
console.log(' npx kryptheon setup-ai tell your AI assistant to check its work');
|
|
88
|
+
console.log(' npx kryptheon verify after a change: what changed, what broke, what got fixed');
|
|
89
|
+
console.log(' npx kryptheon recheck [id] prove a fix: run the check again, and say FIXED or not');
|
|
90
|
+
console.log(' npx kryptheon code read your frontend code for text inserted as HTML');
|
|
91
|
+
console.log(' npx kryptheon dashboard open this project\'s dashboard (also: npx kryptheon, in a terminal)');
|
|
88
92
|
console.log('');
|
|
89
93
|
console.log(' add --quiet to check for one line when everything passes');
|
|
90
94
|
console.log(' npx kryptheon --version print the version you have installed');
|
|
@@ -1984,6 +1988,377 @@ function setupAi() {
|
|
|
1984
1988
|
return results.some((r) => r.action === 'failed') ? 1 : 0;
|
|
1985
1989
|
}
|
|
1986
1990
|
|
|
1991
|
+
// --- code -------------------------------------------------------------------
|
|
1992
|
+
//
|
|
1993
|
+
// A read of the project's own frontend code for text inserted as HTML
|
|
1994
|
+
// (kryptheon-code.js). Nothing is run and nothing is sent; every finding is
|
|
1995
|
+
// "verification required", so the exit code is 0 unless the read could not
|
|
1996
|
+
// happen at all - a script must never take an unproven risk for a break.
|
|
1997
|
+
|
|
1998
|
+
// How many fix prompts are printed in full. The rest are saved in the store
|
|
1999
|
+
// and shown by the dashboard; forty prompts in a terminal is a wall nobody reads.
|
|
2000
|
+
const CODE_PROMPTS_SHOWN = 5;
|
|
2001
|
+
|
|
2002
|
+
// The read itself, saved to the store. Shared by the command and the
|
|
2003
|
+
// dashboard's "Run now", so both leave the same record behind.
|
|
2004
|
+
function readCodeAndSave(opened) {
|
|
2005
|
+
const reader = require(path.join(PACKAGE_DIR, 'kryptheon-code.js'));
|
|
2006
|
+
const result = reader.scanProject(USER_DIR, { packageDir: PACKAGE_DIR });
|
|
2007
|
+
if (!result.ran) return result;
|
|
2008
|
+
try {
|
|
2009
|
+
fs.writeFileSync(opened.codeFindings, JSON.stringify({
|
|
2010
|
+
checkedAt: new Date().toISOString(),
|
|
2011
|
+
filesRead: result.filesRead,
|
|
2012
|
+
findings: result.findings,
|
|
2013
|
+
unreadable: result.unreadable,
|
|
2014
|
+
}, null, 2) + String.fromCharCode(10), 'utf8');
|
|
2015
|
+
} catch (err) {
|
|
2016
|
+
result.saveError = err.message;
|
|
2017
|
+
}
|
|
2018
|
+
return result;
|
|
2019
|
+
}
|
|
2020
|
+
|
|
2021
|
+
function codeRead() {
|
|
2022
|
+
const here = project.inspectProject(USER_DIR);
|
|
2023
|
+
if (!here.ok) {
|
|
2024
|
+
for (const line of project.noProjectLines(here)) console.error(line);
|
|
2025
|
+
return 1;
|
|
2026
|
+
}
|
|
2027
|
+
const opened = openStore();
|
|
2028
|
+
if (!opened) return 1;
|
|
2029
|
+
|
|
2030
|
+
const result = readCodeAndSave(opened);
|
|
2031
|
+
if (!result.ran) {
|
|
2032
|
+
console.error('');
|
|
2033
|
+
console.error(' I could not read your code this time: ' + result.why + '.');
|
|
2034
|
+
console.error(' Nothing was checked - this is not a pass.');
|
|
2035
|
+
console.error('');
|
|
2036
|
+
return 2;
|
|
2037
|
+
}
|
|
2038
|
+
if (result.saveError) console.error(' (could not save this read: ' + result.saveError + ')');
|
|
2039
|
+
|
|
2040
|
+
const high = result.findings.filter((f) => f.severity === 'HIGH');
|
|
2041
|
+
const medium = result.findings.filter((f) => f.severity !== 'HIGH');
|
|
2042
|
+
console.log('');
|
|
2043
|
+
console.log(' I read ' + result.filesRead + ' file' + (result.filesRead === 1 ? '' : 's') + ' of your app\'s code.');
|
|
2044
|
+
if (!result.findings.length) {
|
|
2045
|
+
console.log(' I found no place where text that could come from outside the page is');
|
|
2046
|
+
console.log(' inserted as HTML. That is what this read looks for, and only that.');
|
|
2047
|
+
} else {
|
|
2048
|
+
console.log(' ' + result.findings.length + ' place' + (result.findings.length === 1 ? '' : 's') +
|
|
2049
|
+
' where text may be inserted as HTML (' + high.length + ' HIGH, ' + medium.length + ' MEDIUM).');
|
|
2050
|
+
console.log(' I did not run anything, so each one needs a look before it is called a bug.');
|
|
2051
|
+
for (const f of high.concat(medium)) {
|
|
2052
|
+
console.log('');
|
|
2053
|
+
console.log(' ' + f.severity + ' ' + f.file + ':' + f.line + ' (verification required)');
|
|
2054
|
+
console.log(' ' + f.headline);
|
|
2055
|
+
console.log(' ' + f.sink + ': ' + f.expression);
|
|
2056
|
+
}
|
|
2057
|
+
const prompted = high.slice(0, CODE_PROMPTS_SHOWN);
|
|
2058
|
+
if (prompted.length) {
|
|
2059
|
+
console.log('');
|
|
2060
|
+
console.log(' ' + '-'.repeat(68));
|
|
2061
|
+
console.log(' For the HIGH ones, paste this into your AI tool - one at a time:');
|
|
2062
|
+
for (const f of prompted) {
|
|
2063
|
+
console.log('');
|
|
2064
|
+
f.fixPrompt.split(String.fromCharCode(10)).forEach((l) => console.log(' ' + l));
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
if (result.findings.length > prompted.length) {
|
|
2068
|
+
console.log('');
|
|
2069
|
+
console.log(' The fix for every one is saved in:');
|
|
2070
|
+
console.log(' ' + opened.codeFindings);
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
2073
|
+
if (result.unreadable.length) {
|
|
2074
|
+
console.log('');
|
|
2075
|
+
console.log(' What I could not read (unknown, not clear):');
|
|
2076
|
+
for (const u of result.unreadable) console.log(' ' + u.file + ' - ' + u.why);
|
|
2077
|
+
}
|
|
2078
|
+
console.log('');
|
|
2079
|
+
return 0;
|
|
2080
|
+
}
|
|
2081
|
+
|
|
2082
|
+
// --- verify -----------------------------------------------------------------
|
|
2083
|
+
//
|
|
2084
|
+
// The command for the end of an AI task: what changed since Kryptheon last
|
|
2085
|
+
// looked, which checks that calls for, run the ones that can run here, and
|
|
2086
|
+
// say what broke and what got fixed. "The AI said done" is not the answer;
|
|
2087
|
+
// this is. The looking itself is kryptheon-look.js, shared with the dashboard.
|
|
2088
|
+
|
|
2089
|
+
function verify() {
|
|
2090
|
+
const here = project.inspectProject(USER_DIR);
|
|
2091
|
+
if (!here.ok) {
|
|
2092
|
+
for (const line of project.noProjectLines(here)) console.error(line);
|
|
2093
|
+
return 1;
|
|
2094
|
+
}
|
|
2095
|
+
const opened = openStore();
|
|
2096
|
+
if (!opened) return 1;
|
|
2097
|
+
const looker = require(path.join(PACKAGE_DIR, 'kryptheon-look.js'));
|
|
2098
|
+
|
|
2099
|
+
const result = looker.look(USER_DIR, {
|
|
2100
|
+
beforeRun: (diff, now) => {
|
|
2101
|
+
console.log('');
|
|
2102
|
+
if (diff.first) {
|
|
2103
|
+
console.log(' First look at this project: ' + Object.keys(now.files).length + ' files noted.');
|
|
2104
|
+
console.log(' Next time, I will say exactly what changed since now.');
|
|
2105
|
+
} else if (!diff.files.length) {
|
|
2106
|
+
console.log(' Nothing changed since the last look.');
|
|
2107
|
+
} else {
|
|
2108
|
+
console.log(' WHAT CHANGED ' + diff.files.length + ' file' + (diff.files.length === 1 ? '' : 's') + ', in: ' + diff.parts.join(', '));
|
|
2109
|
+
for (const part of diff.parts) {
|
|
2110
|
+
const inPart = diff.files.filter((f) => f.part === part);
|
|
2111
|
+
console.log('');
|
|
2112
|
+
console.log(' ' + part);
|
|
2113
|
+
for (const f of inPart.slice(0, 15)) console.log(' ' + f.state.padEnd(9) + f.path);
|
|
2114
|
+
if (inPart.length > 15) console.log(' ... and ' + (inPart.length - 15) + ' more');
|
|
2115
|
+
if (part === 'dependencies') {
|
|
2116
|
+
for (const d of diff.dependencies) {
|
|
2117
|
+
console.log(' package ' + d.name + ': ' + (d.change === 'added' ? 'added ' + d.to : d.change === 'removed' ? 'removed (was ' + d.from + ')' : d.from + ' -> ' + d.to));
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
}
|
|
2122
|
+
},
|
|
2123
|
+
frontend: () => readCodeAndSave(opened),
|
|
2124
|
+
regression: () => {
|
|
2125
|
+
console.log('');
|
|
2126
|
+
console.log(' Replaying your recordings...');
|
|
2127
|
+
const r = spawnSync(process.execPath, [path.join(PACKAGE_DIR, 'bin', 'kryptheon.js'), 'check', '--quiet'], {
|
|
2128
|
+
cwd: USER_DIR,
|
|
2129
|
+
stdio: 'inherit',
|
|
2130
|
+
env: process.env,
|
|
2131
|
+
});
|
|
2132
|
+
return r.status;
|
|
2133
|
+
},
|
|
2134
|
+
}, { by: 'verify' });
|
|
2135
|
+
|
|
2136
|
+
const { diff, plan, fresh, fixed, removed, openConfirmed, after } = result;
|
|
2137
|
+
console.log('');
|
|
2138
|
+
if (plan.length) {
|
|
2139
|
+
console.log(' CHECKS');
|
|
2140
|
+
for (const step of plan) console.log(' ' + step.id.padEnd(12) + step.state);
|
|
2141
|
+
} else if (!diff.first && diff.files.length) {
|
|
2142
|
+
console.log(' CHECKS none apply to this change (' + diff.parts.join(', ') + ').');
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
console.log('');
|
|
2146
|
+
if (diff.first) {
|
|
2147
|
+
console.log(' STARTING POINT ' + after.findings.length + ' finding' + (after.findings.length === 1 ? '' : 's') +
|
|
2148
|
+
' already here (see npx kryptheon dashboard). Only what changes from now on will be called new.');
|
|
2149
|
+
} else {
|
|
2150
|
+
if (fresh.length) {
|
|
2151
|
+
console.log(' WHAT BROKE ' + fresh.length + ' new');
|
|
2152
|
+
for (const f of fresh) {
|
|
2153
|
+
console.log(' ' + f.severity + ' ' + f.status + ' ' + f.where);
|
|
2154
|
+
console.log(' ' + f.headline);
|
|
2155
|
+
}
|
|
2156
|
+
} else {
|
|
2157
|
+
console.log(' WHAT BROKE nothing new that the checks above could see.');
|
|
2158
|
+
}
|
|
2159
|
+
if (fixed.length) {
|
|
2160
|
+
console.log('');
|
|
2161
|
+
console.log(' WHAT GOT FIXED ' + fixed.length);
|
|
2162
|
+
for (const f of fixed) console.log(' ' + f.where + ' ' + f.headline);
|
|
2163
|
+
}
|
|
2164
|
+
if (removed.length) {
|
|
2165
|
+
console.log('');
|
|
2166
|
+
console.log(' GONE WITH ITS FILE ' + removed.length + ' (the file was deleted - not the same as fixed)');
|
|
2167
|
+
for (const f of removed) console.log(' ' + f.where + ' ' + f.headline);
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
if (openConfirmed.length) {
|
|
2171
|
+
console.log('');
|
|
2172
|
+
console.log(' Still broken: ' + openConfirmed.length + ' confirmed problem' + (openConfirmed.length === 1 ? '' : 's') + '. The fix prompts are in the dashboard.');
|
|
2173
|
+
}
|
|
2174
|
+
console.log('');
|
|
2175
|
+
if (result.saveError) console.error(' (could not save this look: ' + result.saveError + ')');
|
|
2176
|
+
|
|
2177
|
+
// Non-zero while anything confirmed is broken - new or not - so an AI tool
|
|
2178
|
+
// reading only the exit code is never told its work left things fine when
|
|
2179
|
+
// they are not. Findings that only need a look do not count.
|
|
2180
|
+
return openConfirmed.length || result.regressionExit === 1 ? 1 : 0;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
// --- recheck ----------------------------------------------------------------
|
|
2184
|
+
//
|
|
2185
|
+
// The end of the fix loop: prove a fix. The finding's own check runs again,
|
|
2186
|
+
// and so does everything the changes since call for, so "fixed" never means
|
|
2187
|
+
// "the code changed", and a fix that broke something else says so.
|
|
2188
|
+
|
|
2189
|
+
function replayForLoop() {
|
|
2190
|
+
console.log('');
|
|
2191
|
+
console.log(' Replaying your recordings...');
|
|
2192
|
+
const r = spawnSync(process.execPath, [path.join(PACKAGE_DIR, 'bin', 'kryptheon.js'), 'check', '--quiet'], {
|
|
2193
|
+
cwd: USER_DIR,
|
|
2194
|
+
stdio: 'inherit',
|
|
2195
|
+
env: process.env,
|
|
2196
|
+
});
|
|
2197
|
+
return r.status;
|
|
2198
|
+
}
|
|
2199
|
+
|
|
2200
|
+
const VERDICT_WORDS = {
|
|
2201
|
+
fixed: 'FIXED',
|
|
2202
|
+
'still open': 'STILL OPEN',
|
|
2203
|
+
'gone with its file': 'GONE WITH ITS FILE (not the same as fixed)',
|
|
2204
|
+
'could not confirm': 'COULD NOT CONFIRM',
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
function recheckCommand(id) {
|
|
2208
|
+
const here = project.inspectProject(USER_DIR);
|
|
2209
|
+
if (!here.ok) {
|
|
2210
|
+
for (const line of project.noProjectLines(here)) console.error(line);
|
|
2211
|
+
return 1;
|
|
2212
|
+
}
|
|
2213
|
+
const opened = openStore();
|
|
2214
|
+
if (!opened) return 1;
|
|
2215
|
+
const looker = require(path.join(PACKAGE_DIR, 'kryptheon-look.js'));
|
|
2216
|
+
const dash = require(path.join(PACKAGE_DIR, 'kryptheon-dashboard.js'));
|
|
2217
|
+
|
|
2218
|
+
if (!id) {
|
|
2219
|
+
const open = dash.buildState(USER_DIR).findings;
|
|
2220
|
+
console.log('');
|
|
2221
|
+
if (!open.length) {
|
|
2222
|
+
console.log(' Nothing is open. There is nothing to re-check.');
|
|
2223
|
+
} else {
|
|
2224
|
+
console.log(' Open findings - re-check one after fixing it: npx kryptheon recheck <id>');
|
|
2225
|
+
console.log('');
|
|
2226
|
+
for (const f of open) console.log(' ' + f.id + ' ' + f.severity.padEnd(8) + f.status.padEnd(23) + f.where + ' ' + f.headline);
|
|
2227
|
+
}
|
|
2228
|
+
console.log('');
|
|
2229
|
+
return 0;
|
|
2230
|
+
}
|
|
2231
|
+
|
|
2232
|
+
const r = looker.recheck(USER_DIR, { frontend: () => readCodeAndSave(opened), regression: replayForLoop }, String(id).trim());
|
|
2233
|
+
console.log('');
|
|
2234
|
+
if (!r.found) {
|
|
2235
|
+
console.error(' There is no open finding with the id ' + id + '.');
|
|
2236
|
+
console.error(' npx kryptheon recheck, on its own, lists the ones that are open.');
|
|
2237
|
+
console.error('');
|
|
2238
|
+
return 1;
|
|
2239
|
+
}
|
|
2240
|
+
console.log(' RE-CHECK ' + r.id + ' ' + r.where);
|
|
2241
|
+
console.log(' ' + r.headline);
|
|
2242
|
+
console.log('');
|
|
2243
|
+
console.log(' VERDICT ' + VERDICT_WORDS[r.verdict] + (r.why ? ' - ' + r.why : ''));
|
|
2244
|
+
if (r.newProblems.length) {
|
|
2245
|
+
console.log('');
|
|
2246
|
+
console.log(' NEW PROBLEMS ' + r.newProblems.length + ' appeared with this change:');
|
|
2247
|
+
for (const p of r.newProblems) console.log(' ' + p.id + ' ' + p.severity + ' ' + p.status + ' ' + p.where + ' ' + p.headline);
|
|
2248
|
+
} else if (r.verdict === 'fixed') {
|
|
2249
|
+
console.log('');
|
|
2250
|
+
console.log(' Nothing new broke that the checks could see.');
|
|
2251
|
+
}
|
|
2252
|
+
console.log('');
|
|
2253
|
+
if (r.verdict === 'could not confirm') return 2;
|
|
2254
|
+
return r.verdict === 'fixed' && !r.newProblems.length ? 0 : 1;
|
|
2255
|
+
}
|
|
2256
|
+
|
|
2257
|
+
// --- dashboard --------------------------------------------------------------
|
|
2258
|
+
//
|
|
2259
|
+
// One page on this machine for everything Kryptheon knows about this project
|
|
2260
|
+
// (kryptheon-dashboard.js). Runs until Ctrl+C.
|
|
2261
|
+
|
|
2262
|
+
const DASHBOARD_PORT = 4789;
|
|
2263
|
+
|
|
2264
|
+
function runFromDashboard(id, opened) {
|
|
2265
|
+
if (id === 'frontend') {
|
|
2266
|
+
const result = readCodeAndSave(opened);
|
|
2267
|
+
return result.ran ? { ok: true } : { ok: false, why: result.why };
|
|
2268
|
+
}
|
|
2269
|
+
if (id === 'regression') {
|
|
2270
|
+
// The same command a person types, as a separate process, so the page
|
|
2271
|
+
// stays answerable while a browser replays the flows.
|
|
2272
|
+
return new Promise((resolve) => {
|
|
2273
|
+
const child = spawn(process.execPath, [path.join(PACKAGE_DIR, 'bin', 'kryptheon.js'), 'check', '--quiet'], {
|
|
2274
|
+
cwd: USER_DIR,
|
|
2275
|
+
stdio: 'ignore',
|
|
2276
|
+
env: process.env,
|
|
2277
|
+
});
|
|
2278
|
+
child.on('error', (err) => resolve({ ok: false, why: err.message }));
|
|
2279
|
+
// A run that found broken flows is a run that finished; its results are
|
|
2280
|
+
// in the history the page reads next.
|
|
2281
|
+
child.on('close', (code) => resolve(code === 0 || code === 1 ? { ok: true, exit: code } : { ok: false, why: 'the run stopped with exit ' + code }));
|
|
2282
|
+
});
|
|
2283
|
+
}
|
|
2284
|
+
return { ok: false, why: '"' + id + '" cannot be run from the dashboard' };
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
async function dashboardCommand() {
|
|
2288
|
+
const here = project.inspectProject(USER_DIR);
|
|
2289
|
+
if (!here.ok) {
|
|
2290
|
+
for (const line of project.noProjectLines(here)) console.error(line);
|
|
2291
|
+
return 1;
|
|
2292
|
+
}
|
|
2293
|
+
const opened = openStore();
|
|
2294
|
+
if (!opened) return 1;
|
|
2295
|
+
const dash = require(path.join(PACKAGE_DIR, 'kryptheon-dashboard.js'));
|
|
2296
|
+
const app = dash.createServer(USER_DIR, {
|
|
2297
|
+
run: (id) => runFromDashboard(id, opened),
|
|
2298
|
+
// The same `recheck` a person types, in its own process, so the page stays
|
|
2299
|
+
// answerable while a flow is replayed. Its verdict is read from the store.
|
|
2300
|
+
recheck: (id) => new Promise((resolve) => {
|
|
2301
|
+
// Only a verdict written by this re-check counts: an older one for the
|
|
2302
|
+
// same finding, read after a child that failed early, would be a stale
|
|
2303
|
+
// answer shown as a fresh one.
|
|
2304
|
+
const startedAt = new Date().toISOString();
|
|
2305
|
+
const child = spawn(process.execPath, [path.join(PACKAGE_DIR, 'bin', 'kryptheon.js'), 'recheck', id], {
|
|
2306
|
+
cwd: USER_DIR,
|
|
2307
|
+
stdio: 'ignore',
|
|
2308
|
+
env: process.env,
|
|
2309
|
+
});
|
|
2310
|
+
child.on('error', (err) => resolve({ ok: false, why: err.message }));
|
|
2311
|
+
child.on('close', () => {
|
|
2312
|
+
let last = null;
|
|
2313
|
+
try {
|
|
2314
|
+
const lines = fs.readFileSync(opened.fixes, 'utf8').split(String.fromCharCode(10)).filter(Boolean);
|
|
2315
|
+
for (let i = lines.length - 1; i >= 0 && !last; i--) {
|
|
2316
|
+
const a = JSON.parse(lines[i]);
|
|
2317
|
+
if (a.id === id && a.at >= startedAt) last = a;
|
|
2318
|
+
}
|
|
2319
|
+
} catch (err) {
|
|
2320
|
+
last = null;
|
|
2321
|
+
}
|
|
2322
|
+
resolve(last || { ok: false, why: 'the re-check did not record a verdict' });
|
|
2323
|
+
});
|
|
2324
|
+
}),
|
|
2325
|
+
});
|
|
2326
|
+
let port;
|
|
2327
|
+
try {
|
|
2328
|
+
port = await app.listen(DASHBOARD_PORT);
|
|
2329
|
+
} catch (err) {
|
|
2330
|
+
// Taken - by another project's dashboard, most likely. Any free port will do.
|
|
2331
|
+
port = await app.listen(0);
|
|
2332
|
+
}
|
|
2333
|
+
console.log('');
|
|
2334
|
+
console.log(' Kryptheon dashboard for ' + path.basename(USER_DIR) + ':');
|
|
2335
|
+
console.log('');
|
|
2336
|
+
console.log(' http://127.0.0.1:' + port + '/');
|
|
2337
|
+
console.log('');
|
|
2338
|
+
console.log(' Open that address in your browser. It is only reachable from this');
|
|
2339
|
+
console.log(' machine. Press Ctrl+C here to stop it.');
|
|
2340
|
+
console.log('');
|
|
2341
|
+
|
|
2342
|
+
// While the page is open, Kryptheon notices changes by itself: once a change
|
|
2343
|
+
// has settled, it looks, runs the fast checks, and the page shows what
|
|
2344
|
+
// changed and what came of it. Recordings are replayed only from the page's
|
|
2345
|
+
// Run now - a replay while the app is down would call every flow broken.
|
|
2346
|
+
const looker = require(path.join(PACKAGE_DIR, 'kryptheon-look.js'));
|
|
2347
|
+
looker.startWatch(USER_DIR, {
|
|
2348
|
+
frontend: () => readCodeAndSave(opened),
|
|
2349
|
+
regression: () => null,
|
|
2350
|
+
}, {
|
|
2351
|
+
onLook: (r) => {
|
|
2352
|
+
if (r.diff.first) return;
|
|
2353
|
+
const when = new Date().toLocaleTimeString();
|
|
2354
|
+
console.log(' ' + when + ' ' + r.diff.files.length + ' file' + (r.diff.files.length === 1 ? '' : 's') + ' changed (' +
|
|
2355
|
+
r.diff.parts.join(', ') + ') - ' + r.fresh.length + ' new, ' + r.fixed.length + ' fixed. See the page.');
|
|
2356
|
+
},
|
|
2357
|
+
onError: (err) => console.error(' (could not look at a change: ' + err.message + ')'),
|
|
2358
|
+
});
|
|
2359
|
+
return new Promise(() => {});
|
|
2360
|
+
}
|
|
2361
|
+
|
|
1987
2362
|
// --- accept -----------------------------------------------------------------
|
|
1988
2363
|
|
|
1989
2364
|
function baselineApi() {
|
|
@@ -2243,12 +2618,27 @@ async function main() {
|
|
|
2243
2618
|
case 'accept':
|
|
2244
2619
|
return finishWith(accept(rest.join(' ').trim()));
|
|
2245
2620
|
break;
|
|
2621
|
+
case 'code':
|
|
2622
|
+
return finishWith(codeRead());
|
|
2623
|
+
case 'verify':
|
|
2624
|
+
return finishWith(verify());
|
|
2625
|
+
case 'recheck':
|
|
2626
|
+
return finishWith(recheckCommand(rest[0]));
|
|
2246
2627
|
case '-v':
|
|
2247
2628
|
case '--version':
|
|
2248
2629
|
case 'version':
|
|
2249
2630
|
console.log(packageVersion());
|
|
2250
2631
|
return finishWith(0);
|
|
2632
|
+
case 'dashboard':
|
|
2633
|
+
return finishWith(await dashboardCommand());
|
|
2251
2634
|
case undefined:
|
|
2635
|
+
// One command starts Kryptheon - in a terminal a person is looking at.
|
|
2636
|
+
// Run by a script or an AI tool there is nobody to open a page for, and
|
|
2637
|
+
// a server that never exits would hang it, so it prints the usage as it
|
|
2638
|
+
// always did.
|
|
2639
|
+
if (process.stdin.isTTY && process.stdout.isTTY) return finishWith(await dashboardCommand());
|
|
2640
|
+
usage();
|
|
2641
|
+
return finishWith(0);
|
|
2252
2642
|
case '-h':
|
|
2253
2643
|
case '--help':
|
|
2254
2644
|
case 'help':
|
|
Binary file
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
// What changed in a project since Kryptheon last looked, and what that means
|
|
2
|
+
// for which checks have to run.
|
|
3
|
+
//
|
|
4
|
+
// A snapshot is a content hash of every file that is the project's own - its
|
|
5
|
+
// source, its database migrations, its dependency list - kept in the store,
|
|
6
|
+
// never in the project. Comparing two snapshots answers the first question a
|
|
7
|
+
// person has after an AI tool says "done": what did it actually touch?
|
|
8
|
+
//
|
|
9
|
+
// Content, not timestamps and not git: an editor that saves a file unchanged
|
|
10
|
+
// is not a change, and a landing page with no git repository still has one.
|
|
11
|
+
//
|
|
12
|
+
// Each changed file is put in a part of the app (frontend, backend, database,
|
|
13
|
+
// tests, dependencies, config, docs), and each part names the checks that
|
|
14
|
+
// can speak to it. The mapping is written here, once, and deliberately
|
|
15
|
+
// plain: a path-based guess that is right for how most projects are laid
|
|
16
|
+
// out, and said as a guess ("likely affected") rather than a proof.
|
|
17
|
+
|
|
18
|
+
const fs = require('fs');
|
|
19
|
+
const path = require('path');
|
|
20
|
+
const crypto = require('crypto');
|
|
21
|
+
|
|
22
|
+
const SKIP_DIRS = new Set([
|
|
23
|
+
'node_modules', '.git', 'dist', 'build', 'out', '.next', '.nuxt', '.svelte-kit', 'coverage',
|
|
24
|
+
'.vercel', '.netlify', '.turbo', '.cache', 'test-results', 'playwright-report', '.kryptheon',
|
|
25
|
+
]);
|
|
26
|
+
const MAX_BYTES = 5 * 1024 * 1024;
|
|
27
|
+
|
|
28
|
+
// Where a file sits in the app. The first rule that matches wins, so the
|
|
29
|
+
// specific ones come first: a .sql file inside backend/ is a database change.
|
|
30
|
+
const PARTS = [
|
|
31
|
+
{ part: 'dependencies', test: (p) => /(^|\/)(package\.json|package-lock\.json|yarn\.lock|pnpm-lock\.yaml|bun\.lockb?|requirements\.txt|poetry\.lock|Gemfile\.lock|go\.sum)$/i.test(p) },
|
|
32
|
+
{ part: 'database', test: (p) => /\.sql$/i.test(p) || /(^|\/)(migrations|supabase|prisma|drizzle|db\/schema)(\/|$)/i.test(p) || /(^|\/)schema\.prisma$/i.test(p) },
|
|
33
|
+
{ part: 'tests', test: (p) => /(^|\/)(tests?|__tests__|e2e|cypress)\//i.test(p) || /\.(spec|test)\.[cm]?[jt]sx?$/i.test(p) },
|
|
34
|
+
{ part: 'docs', test: (p) => /\.(md|mdx|txt|rst)$/i.test(p) || /(^|\/)docs\//i.test(p) },
|
|
35
|
+
{ part: 'config', test: (p) => /(^|\/)(\.env\.example|\.env\.sample|tsconfig[^/]*\.json|jsconfig\.json|vercel\.json|netlify\.toml|dockerfile|docker-compose[^/]*\.ya?ml)$/i.test(p) || /\.config\.[cm]?[jt]s$/i.test(p) || /(^|\/)\.github\//i.test(p) },
|
|
36
|
+
{ part: 'backend', test: (p) => /(^|\/)(server|backend|api|routes|controllers|services|middleware|functions|workers|jobs|lib\/server)(\/|\.[cm]?[jt]s$)/i.test(p) || /(^|\/)(server|app|index)\.[cm]?[jt]s$/i.test(p) && !/(^|\/)(src|public|frontend|client|pages|components)\//i.test(p) },
|
|
37
|
+
{ part: 'frontend', test: (p) => /\.(html?|css|scss|sass|less|jsx|tsx|vue|svelte|astro)$/i.test(p) || /(^|\/)(frontend|client|public|src|pages|components|app|views|static)\//i.test(p) && /\.[cm]?[jt]s$/i.test(p) },
|
|
38
|
+
{ part: 'assets', test: (p) => /\.(png|jpe?g|gif|svg|webp|ico|woff2?|ttf|otf|mp4|webm|mp3|pdf)$/i.test(p) },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Which checks can speak to a change in each part. Ids are the dashboard's.
|
|
42
|
+
const CHECKS_FOR = {
|
|
43
|
+
frontend: ['frontend', 'regression'],
|
|
44
|
+
backend: ['backend', 'api', 'regression'],
|
|
45
|
+
database: ['data', 'regression'],
|
|
46
|
+
dependencies: ['dependency', 'build', 'regression'],
|
|
47
|
+
tests: ['regression'],
|
|
48
|
+
config: ['build', 'regression'],
|
|
49
|
+
code: ['regression'],
|
|
50
|
+
docs: [],
|
|
51
|
+
assets: [],
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function partOf(rel) {
|
|
55
|
+
const p = rel.split(path.sep).join('/');
|
|
56
|
+
const hit = PARTS.find((r) => r.test(p));
|
|
57
|
+
if (hit) return hit.part;
|
|
58
|
+
return /\.[cm]?[jt]sx?$/i.test(p) ? 'code' : 'other';
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Every file that is the project's own, as relative paths with forward slashes. */
|
|
62
|
+
function listFiles(root) {
|
|
63
|
+
const out = [];
|
|
64
|
+
(function walk(dir) {
|
|
65
|
+
let entries;
|
|
66
|
+
try {
|
|
67
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
68
|
+
} catch (err) {
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
const full = path.join(dir, entry.name);
|
|
73
|
+
if (entry.isDirectory()) {
|
|
74
|
+
if (!SKIP_DIRS.has(entry.name)) walk(full);
|
|
75
|
+
} else if (entry.isFile()) {
|
|
76
|
+
// A real .env holds secrets; its content is never hashed or kept.
|
|
77
|
+
if (/^\.env(\..+)?$/i.test(entry.name) && !/\.(example|sample)$/i.test(entry.name)) continue;
|
|
78
|
+
out.push(path.relative(root, full).split(path.sep).join('/'));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
})(root);
|
|
82
|
+
return out.sort();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** The dependency lists of package.json, kept so a change can say which package moved. */
|
|
86
|
+
function dependenciesOf(root) {
|
|
87
|
+
try {
|
|
88
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
89
|
+
return Object.assign({}, pkg.dependencies || {}, pkg.devDependencies || {});
|
|
90
|
+
} catch (err) {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The project as it is now. Files whose size and modified time match the
|
|
97
|
+
* previous snapshot keep its hash without being read again - on a large
|
|
98
|
+
* project that is the difference between milliseconds and seconds, and an
|
|
99
|
+
* unchanged size and time is the same bytes for every editor that exists.
|
|
100
|
+
*/
|
|
101
|
+
function takeSnapshot(root, previous) {
|
|
102
|
+
const before = (previous && previous.files) || {};
|
|
103
|
+
const files = {};
|
|
104
|
+
for (const rel of listFiles(root)) {
|
|
105
|
+
let stat;
|
|
106
|
+
try {
|
|
107
|
+
stat = fs.statSync(path.join(root, rel));
|
|
108
|
+
} catch (err) {
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
const old = before[rel];
|
|
112
|
+
if (old && old.size === stat.size && old.mtimeMs === stat.mtimeMs) {
|
|
113
|
+
files[rel] = old;
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
let hash;
|
|
117
|
+
if (stat.size > MAX_BYTES) {
|
|
118
|
+
hash = 'size:' + stat.size + ':' + stat.mtimeMs;
|
|
119
|
+
} else {
|
|
120
|
+
try {
|
|
121
|
+
hash = crypto.createHash('sha1').update(fs.readFileSync(path.join(root, rel))).digest('hex');
|
|
122
|
+
} catch (err) {
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
files[rel] = { hash: hash, size: stat.size, mtimeMs: stat.mtimeMs };
|
|
127
|
+
}
|
|
128
|
+
return { takenAt: new Date().toISOString(), files: files, dependencies: dependenciesOf(root) };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Packages added, removed or moved to another version between two snapshots. */
|
|
132
|
+
function dependencyChanges(before, after) {
|
|
133
|
+
const a = (before && before.dependencies) || {};
|
|
134
|
+
const b = (after && after.dependencies) || {};
|
|
135
|
+
const out = [];
|
|
136
|
+
for (const name of new Set(Object.keys(a).concat(Object.keys(b)))) {
|
|
137
|
+
if (!(name in a)) out.push({ name: name, change: 'added', to: b[name] });
|
|
138
|
+
else if (!(name in b)) out.push({ name: name, change: 'removed', from: a[name] });
|
|
139
|
+
else if (a[name] !== b[name]) out.push({ name: name, change: 'changed', from: a[name], to: b[name] });
|
|
140
|
+
}
|
|
141
|
+
return out.sort((x, y) => x.name.localeCompare(y.name));
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* What changed between two snapshots, by part of the app, and which checks
|
|
146
|
+
* that calls for. With no earlier snapshot there is nothing to compare, and
|
|
147
|
+
* that is said - it is not reported as "everything changed".
|
|
148
|
+
*/
|
|
149
|
+
function compare(before, after) {
|
|
150
|
+
if (!before) return { first: true, files: [], parts: [], checks: [], dependencies: [] };
|
|
151
|
+
const files = [];
|
|
152
|
+
const a = before.files || {};
|
|
153
|
+
const b = after.files || {};
|
|
154
|
+
for (const rel of Object.keys(b)) {
|
|
155
|
+
if (!a[rel]) files.push({ path: rel, state: 'added' });
|
|
156
|
+
else if (a[rel].hash !== b[rel].hash) files.push({ path: rel, state: 'modified' });
|
|
157
|
+
}
|
|
158
|
+
for (const rel of Object.keys(a)) if (!b[rel]) files.push({ path: rel, state: 'deleted' });
|
|
159
|
+
files.sort((x, y) => x.path.localeCompare(y.path));
|
|
160
|
+
for (const f of files) f.part = partOf(f.path);
|
|
161
|
+
|
|
162
|
+
const parts = [];
|
|
163
|
+
for (const f of files) if (!parts.includes(f.part)) parts.push(f.part);
|
|
164
|
+
const checks = [];
|
|
165
|
+
for (const part of parts) for (const id of CHECKS_FOR[part] || []) if (!checks.includes(id)) checks.push(id);
|
|
166
|
+
return {
|
|
167
|
+
first: false,
|
|
168
|
+
files: files,
|
|
169
|
+
parts: parts,
|
|
170
|
+
checks: checks,
|
|
171
|
+
dependencies: dependencyChanges(before, after),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Findings that were not there last time. Keyed by what they are about rather
|
|
177
|
+
* than by line number: a line added above a known finding moves it, and a
|
|
178
|
+
* moved finding reported as new would teach a person to ignore this list.
|
|
179
|
+
*/
|
|
180
|
+
//
|
|
181
|
+
// The detail is part of what a finding is only for the code read, where it is
|
|
182
|
+
// the expression itself. For a recorded flow or a database finding it is the
|
|
183
|
+
// account of how it broke this time - "ended up somewhere different", "could
|
|
184
|
+
// not be opened" - and a flow that is still broken in a new way is the same
|
|
185
|
+
// finding, not a fixed one and a new one.
|
|
186
|
+
function findingKey(f) {
|
|
187
|
+
return [
|
|
188
|
+
f.check || '',
|
|
189
|
+
f.where ? String(f.where).replace(/:\d+$/, '') : '',
|
|
190
|
+
f.headline ? String(f.headline).replace(/:\d+\b/g, '') : '',
|
|
191
|
+
f.check === 'frontend' ? f.detail || '' : '',
|
|
192
|
+
].join('|');
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/** A short, stable name for a finding: what a person types to re-check it. */
|
|
196
|
+
function findingId(f) {
|
|
197
|
+
return crypto.createHash('sha1').update(findingKey(f)).digest('hex').slice(0, 8);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function newFindings(before, after) {
|
|
201
|
+
const seen = new Set((before || []).map(findingKey));
|
|
202
|
+
return (after || []).filter((f) => !seen.has(findingKey(f)));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function goneFindings(before, after) {
|
|
206
|
+
const now = new Set((after || []).map(findingKey));
|
|
207
|
+
return (before || []).filter((f) => !now.has(findingKey(f)));
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
module.exports = {
|
|
211
|
+
PARTS: PARTS,
|
|
212
|
+
CHECKS_FOR: CHECKS_FOR,
|
|
213
|
+
partOf: partOf,
|
|
214
|
+
listFiles: listFiles,
|
|
215
|
+
takeSnapshot: takeSnapshot,
|
|
216
|
+
compare: compare,
|
|
217
|
+
dependencyChanges: dependencyChanges,
|
|
218
|
+
findingKey: findingKey,
|
|
219
|
+
findingId: findingId,
|
|
220
|
+
newFindings: newFindings,
|
|
221
|
+
goneFindings: goneFindings,
|
|
222
|
+
};
|