kryptheon 0.1.37 → 0.1.39

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 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';
@@ -624,12 +631,10 @@
624
631
  { id: 'claude-code', name: 'Claude Code', tested: true,
625
632
  steps: ['Run this once, in any terminal. That is the whole set-up.'],
626
633
  code: 'claude mcp add kryptheon -- npx -y kryptheon-mcp', file: 'terminal' },
634
+ // No "Add to Cursor" button: it was never seen working end to end, and
635
+ // the person asked for it to go. By hand, as for every other tool.
627
636
  { id: 'cursor', name: 'Cursor', tested: false,
628
- // Cursor's own install page, as on kryptheon.tech: it opens Cursor, and
629
- // where Cursor does not open - not installed, or blocked - it shows the
630
- // config to paste, rather than a click that does nothing at all.
631
- oneClick: { href: 'https://cursor.com/en/install-mcp?name=kryptheon&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsImtyeXB0aGVvbi1tY3AiXX0=', label: 'Add to Cursor' },
632
- steps: ['Press Add to Cursor. Cursor\'s page opens and hands it to Cursor, which asks you to confirm - say yes. Cursor has to be installed on this computer.', 'Or by hand: paste the config into mcp.json in the .cursor folder of your home folder.'],
637
+ steps: ['Open Cursor Settings, then MCP, and press Add new MCP server - Cursor opens its mcp.json.', 'Paste the config and save. It lives in the .cursor folder of your home folder.'],
633
638
  code: MCP_JSON, file: '~/.cursor/mcp.json' },
634
639
  { id: 'vscode', name: 'VS Code', tested: false,
635
640
  oneClick: { href: 'vscode:mcp/install?' + encodeURIComponent(JSON.stringify({ name: 'kryptheon', command: 'npx', args: ['-y', 'kryptheon-mcp'] })), label: 'Install in VS Code' },
@@ -1105,11 +1110,71 @@
1105
1110
  if (!busy) { clear(run); run.appendChild(icon('play', 15)); run.appendChild(el('span', null, 'Run checks')); }
1106
1111
  }
1107
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
+
1108
1172
  function render(next) {
1109
1173
  if (!next) return;
1110
1174
  state = next;
1111
1175
  renderSide();
1112
1176
  renderTop();
1177
+ renderApp();
1113
1178
  renderNow();
1114
1179
  renderSteps();
1115
1180
  renderKeep();
@@ -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">
@@ -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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kryptheon",
3
- "version": "0.1.37",
3
+ "version": "0.1.39",
4
4
  "description": "Record what you do in your app and get plain-language reports when it breaks.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "type": "commonjs",