@mneme-ai/xray 2.163.0 → 2.174.0
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/dist/clone.d.ts +2 -7
- package/dist/clone.d.ts.map +1 -1
- package/dist/clone.js +15 -2
- package/dist/clone.js.map +1 -1
- package/dist/engine.js +2 -2
- package/dist/engine.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +98 -1
- package/dist/server.js.map +1 -1
- package/dist/track.d.ts +59 -0
- package/dist/track.d.ts.map +1 -0
- package/dist/track.js +182 -0
- package/dist/track.js.map +1 -0
- package/dist/tracker_server.d.ts +94 -0
- package/dist/tracker_server.d.ts.map +1 -0
- package/dist/tracker_server.js +235 -0
- package/dist/tracker_server.js.map +1 -0
- package/dist/triage.d.ts +66 -0
- package/dist/triage.d.ts.map +1 -0
- package/dist/triage.js +171 -0
- package/dist/triage.js.map +1 -0
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -2
- package/public/card.js +40 -4
- package/public/index.html +257 -27
- package/public/local-scan.js +153 -0
- package/public/report.html +14 -2
package/public/index.html
CHANGED
|
@@ -66,11 +66,25 @@
|
|
|
66
66
|
.g-C{background:linear-gradient(135deg,#eaa83a,#c9821a)} .g-D{background:linear-gradient(135deg,#f0742e,#d4571a)}
|
|
67
67
|
.g-F{background:linear-gradient(135deg,#f43f5e,#be123c)}
|
|
68
68
|
.top .repo{font-size:22px;font-weight:640;letter-spacing:-.02em;color:var(--ink);word-break:break-all;line-height:1.2}
|
|
69
|
+
.top .repobr{font-size:13px;font-weight:600;color:var(--a);background:var(--a-soft);border-radius:6px;padding:1px 7px;vertical-align:middle}
|
|
70
|
+
.top .repourl{display:inline-block;font-size:12.5px;color:var(--a);text-decoration:none;margin-top:3px;word-break:break-all;font-family:ui-monospace,Menlo,monospace}
|
|
71
|
+
.top .repourl:hover{text-decoration:underline}
|
|
69
72
|
.top .head{color:var(--sub);font-size:14px;margin-top:4px}
|
|
70
73
|
.trustbar{display:flex;align-items:center;gap:14px;padding:13px 30px;background:#f0fdf4;border-bottom:1px solid #dcfce7;flex-wrap:wrap}
|
|
71
74
|
.hgauge{display:inline-flex;align-items:center;gap:8px;font-weight:680;color:#15803d;font-size:14px;white-space:nowrap}
|
|
72
75
|
.hdot{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)}
|
|
73
76
|
.htext{font-size:12.5px;color:#3f6b4e;line-height:1.5}.htext b{color:#14532d;font-weight:640}
|
|
77
|
+
.membrane{display:flex;border-bottom:1px solid #eef0f2;background:#fafbfc;flex-wrap:wrap}
|
|
78
|
+
.mp{flex:1;min-width:180px;padding:11px 18px;border-right:1px solid #eef0f2;display:flex;flex-direction:column;gap:3px}.mp:last-child{border-right:0}
|
|
79
|
+
.mpk{font-size:10.5px;letter-spacing:.06em;color:#8a909a;font-weight:700}
|
|
80
|
+
.mpv{font-size:12.5px;color:#2b2f36;font-weight:560;display:flex;align-items:center;gap:7px}
|
|
81
|
+
.triage{padding:14px 30px 4px}
|
|
82
|
+
.triage-h{font-size:13px;font-weight:680;color:#b45309;margin-bottom:9px}.triage-sub{font-weight:400;color:#9aa0a6;font-size:11.5px}
|
|
83
|
+
.triage.clearall{padding:13px 30px;color:#15803d;font-weight:560;font-size:13px;background:#f0fdf4;border-bottom:1px solid #dcfce7}
|
|
84
|
+
.ti{display:flex;gap:11px;align-items:flex-start;padding:8px 0;border-top:1px solid #f1f2f4}
|
|
85
|
+
.ti-sev{flex:0 0 auto;font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:3px 8px;border-radius:6px;min-width:62px;text-align:center}
|
|
86
|
+
.ti.critical .ti-sev{background:#fee2e2;color:#b91c1c}.ti.warn .ti-sev{background:#fef3c7;color:#b45309}.ti.info .ti-sev{background:#e0e7ff;color:#4338ca}
|
|
87
|
+
.ti-body{flex:1;min-width:0}.ti-f{font-size:13px;color:#1f2329}.ti-p{font-size:11px;color:#9aa0a6;margin-top:2px;font-family:ui-monospace,Menlo,monospace}
|
|
74
88
|
.rows{padding:6px 30px 16px}
|
|
75
89
|
.row{display:flex;gap:18px;padding:17px 0;border-bottom:1px solid var(--line2);align-items:baseline}
|
|
76
90
|
.row:last-child{border-bottom:0}
|
|
@@ -81,7 +95,8 @@
|
|
|
81
95
|
.row .v .big{font-weight:660;color:var(--ink);font-size:16px}
|
|
82
96
|
.muted{color:var(--sub)}
|
|
83
97
|
.spark{font-family:ui-monospace,Menlo,monospace;letter-spacing:1px;color:var(--a)}
|
|
84
|
-
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
98
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;max-height:148px;overflow-y:auto}
|
|
99
|
+
.chips::-webkit-scrollbar{width:8px}.chips::-webkit-scrollbar-thumb{background:#d7dbe0;border-radius:8px}
|
|
85
100
|
.chip{font-size:12px;padding:4px 10px;border-radius:8px;background:var(--soft);border:1px solid var(--line);color:#5c616b;font-weight:450}
|
|
86
101
|
.chip.bad{background:#fff1f3;border-color:#ffe0e6;color:var(--red)}
|
|
87
102
|
.chip.warn{background:#fff8ed;border-color:#fde6cc;color:#b45309}
|
|
@@ -131,6 +146,8 @@
|
|
|
131
146
|
.listfoot{display:flex;justify-content:space-between;align-items:center;padding:14px 4px 0;font-size:13px}
|
|
132
147
|
.moreb{height:38px;padding:0 18px;border:1px solid var(--line);background:#fff;border-radius:10px;cursor:pointer;font-size:13px;font-weight:540;transition:border-color .15s}
|
|
133
148
|
.moreb:hover{border-color:var(--ink)}
|
|
149
|
+
.moreb:disabled{opacity:.4;cursor:not-allowed;border-color:var(--line)}
|
|
150
|
+
.pagenav{display:inline-flex;gap:8px}
|
|
134
151
|
/* share + buttons */
|
|
135
152
|
.share{display:flex;flex-wrap:wrap;align-items:center;gap:10px;padding:18px 30px;border-top:1px solid var(--line2)}
|
|
136
153
|
.badgeimg{height:20px}
|
|
@@ -151,14 +168,50 @@
|
|
|
151
168
|
.paste-hint{align-self:center;font-size:12.5px;color:var(--sub)}
|
|
152
169
|
.paste-hint b{color:var(--ink2);font-weight:560}
|
|
153
170
|
/* local bridge panel */
|
|
154
|
-
.localbox{max-width:560px;margin:22px auto 0;border:1px solid var(--line);border-radius:var(--rs);padding:18px 20px;background:var(--soft);display:
|
|
155
|
-
.localbox.on{display:block}
|
|
171
|
+
.localbox{max-width:560px;margin:22px auto 0;border:1px solid var(--line);border-radius:var(--rs);padding:18px 20px;background:var(--soft);display:block;text-align:left}
|
|
156
172
|
.localbox .lh{display:flex;align-items:center;gap:9px;font-size:13.5px;font-weight:560;color:var(--ink2)}
|
|
157
|
-
.localdot{width:9px;height:9px;border-radius:50%;background
|
|
173
|
+
.localdot{width:9px;height:9px;border-radius:50%;background:#c8ccd2;box-shadow:0 0 0 3px rgba(140,140,150,.12)}
|
|
174
|
+
.localbox.connected .localdot{background:var(--green);box-shadow:0 0 0 3px rgba(22,163,74,.14)}
|
|
175
|
+
.pickbtn{height:44px;padding:0 18px;border:1.5px solid var(--a);border-radius:10px;background:var(--a);color:#fff;cursor:pointer;font-size:13.5px;font-weight:600}
|
|
176
|
+
.cmdrow{display:flex;gap:8px;align-items:center;margin-top:8px;flex-wrap:wrap}
|
|
177
|
+
.cmdrow code{background:#0b0b0f;color:#e6e6ea;border-radius:8px;padding:8px 12px;font-size:12.5px;font-family:ui-monospace,Menlo,monospace}
|
|
178
|
+
.orline{display:flex;align-items:center;gap:10px;margin:14px 0;color:var(--sub);font-size:12px}
|
|
179
|
+
.orline::before,.orline::after{content:"";flex:1;height:1px;background:var(--line)}
|
|
158
180
|
.localrow{display:flex;gap:8px;margin-top:12px}
|
|
159
181
|
.localrow input{flex:1;height:44px;padding:0 14px;border:1px solid var(--line);border-radius:10px;font-size:13.5px;font-family:ui-monospace,Menlo,monospace}
|
|
160
182
|
.localrow button{height:44px;padding:0 16px;border:0;border-radius:10px;background:var(--ink);color:#fff;cursor:pointer;font-size:13.5px}
|
|
161
|
-
.localhint{font-size:12px;color:var(--sub);margin-top:9px}
|
|
183
|
+
.localhint{font-size:12px;color:var(--sub);margin-top:9px;line-height:1.5}
|
|
184
|
+
.bridgebox{margin-top:14px;border-top:1px solid var(--line);padding-top:12px}
|
|
185
|
+
.bridgebox summary{cursor:pointer;font-size:12.5px;color:var(--a);font-weight:560;list-style:none}
|
|
186
|
+
.lscard{margin-top:12px;border:1px solid var(--line);border-radius:12px;background:#fff;padding:14px 16px;text-align:left}
|
|
187
|
+
.lscard .lstop{display:flex;align-items:center;gap:14px;margin-bottom:8px}
|
|
188
|
+
.lscard .lsgrade{width:46px;height:46px;border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:23px;font-weight:700;color:#fff;flex-shrink:0;letter-spacing:-.02em}
|
|
189
|
+
.lscard .lsh{font-size:15px;color:var(--ink)}
|
|
190
|
+
.lscard .lsrow{display:flex;gap:12px;padding:7px 0;border-top:1px solid var(--line2);font-size:13px}
|
|
191
|
+
.lscard .lsk{width:110px;color:var(--sub);font-weight:560;flex-shrink:0}
|
|
192
|
+
.lscard .lsv{color:var(--ink2)} .lscard .lsv.bad{color:var(--red)} .lscard .lsv.ok{color:var(--green)}
|
|
193
|
+
.lscard .lshits{margin:6px 0;font-size:12px;color:var(--red);font-family:ui-monospace,Menlo,monospace;line-height:1.6}
|
|
194
|
+
.lscard .lsok{margin:6px 0;font-size:12.5px;color:var(--green)}
|
|
195
|
+
.lscard .pill{display:inline-block;font-size:11px;background:var(--soft);border:1px solid var(--line);border-radius:20px;padding:1px 8px;color:var(--ink2)}
|
|
196
|
+
.lscard .lsnote{margin-top:10px;font-size:11.5px;color:var(--sub);line-height:1.5;border-top:1px solid var(--line2);padding-top:9px}
|
|
197
|
+
/* SHOWCASE — selling points; the graphic sits to the SIDE (grid areas, no markup move) */
|
|
198
|
+
.showcase{max-width:1060px;margin:64px auto 0;padding:0 20px;text-align:left;
|
|
199
|
+
display:grid;column-gap:36px;row-gap:18px;align-items:center;
|
|
200
|
+
grid-template-columns:1fr 360px;grid-template-areas:"title art" "sub art" "cards cards"}
|
|
201
|
+
.showcase .sctitle{grid-area:title;font-size:30px;font-weight:700;letter-spacing:-.02em;color:var(--ink);margin:0;line-height:1.2}
|
|
202
|
+
.showcase .sctitle .hl{color:var(--a)}
|
|
203
|
+
.showcase .scsub{grid-area:sub;font-size:15px;color:var(--sub);margin:0;line-height:1.6}
|
|
204
|
+
.showcase .bigart{grid-area:art;display:flex;justify-content:center;align-self:center;opacity:.95}
|
|
205
|
+
.showcase .sc3{grid-area:cards;display:grid;grid-template-columns:repeat(3,1fr);gap:16px;margin-top:14px}
|
|
206
|
+
.showcase .sccard{background:var(--soft);border:1px solid var(--line);border-radius:16px;padding:20px}
|
|
207
|
+
.showcase .scico{font-size:26px;margin-bottom:8px}
|
|
208
|
+
.showcase .sccard b{display:block;font-size:15px;color:var(--ink);margin-bottom:6px}
|
|
209
|
+
.showcase .sccard span{font-size:13px;color:var(--sub);line-height:1.55}
|
|
210
|
+
@media(max-width:820px){
|
|
211
|
+
.showcase{grid-template-columns:1fr;grid-template-areas:"title" "sub" "art" "cards"}
|
|
212
|
+
.showcase .sc3{grid-template-columns:1fr}
|
|
213
|
+
.showcase .bigart svg{width:300px;height:auto}
|
|
214
|
+
}
|
|
162
215
|
.picker{display:none;margin-top:12px;border:1px solid var(--line);border-radius:10px;background:#fff;overflow:hidden}
|
|
163
216
|
.picker.on{display:block}
|
|
164
217
|
.pkbar{display:flex;align-items:center;gap:8px;padding:10px 12px;border-bottom:1px solid var(--line2);font-size:12.5px}
|
|
@@ -169,6 +222,33 @@
|
|
|
169
222
|
.pkrow:last-child{border-bottom:0}.pkrow:hover{background:var(--soft)}
|
|
170
223
|
.pkrow .ic{width:18px;text-align:center}
|
|
171
224
|
.pkrow .repobadge{margin-left:auto;font-size:10.5px;color:var(--a);background:var(--a-soft);border-radius:6px;padding:2px 7px;font-weight:600}
|
|
225
|
+
/* ── REAL-TIME TRACKING panel — clean + clear ── */
|
|
226
|
+
.track{max-width:760px;margin:18px auto 0;background:var(--soft);border:1px solid var(--line);border-top:2px solid var(--a);border-radius:var(--rs);padding:16px 18px;box-shadow:var(--sh-sm)}
|
|
227
|
+
.track .thead{display:flex;align-items:center;gap:8px;font-size:15px;color:var(--ink)}
|
|
228
|
+
.track .ticon{font-size:17px}
|
|
229
|
+
.track .tnew{font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;color:var(--a);background:var(--a-soft);border-radius:6px;padding:2px 7px}
|
|
230
|
+
.track .trow{display:flex;align-items:center;gap:12px;flex-wrap:wrap;margin-top:12px}
|
|
231
|
+
.track .tlabel{font-size:13px;color:var(--sub);font-weight:560}
|
|
232
|
+
.track select{height:38px;border:1px solid var(--line);border-radius:9px;padding:0 12px;font-size:13.5px;background:#fff;color:var(--ink2);max-width:260px}
|
|
233
|
+
.track .tbtn{height:38px;padding:0 16px;border-radius:9px;border:1.5px solid var(--a);background:var(--a);color:#fff;font-weight:600;font-size:13.5px;cursor:pointer}
|
|
234
|
+
.track .tbtn.off{background:#fff;color:var(--a)}
|
|
235
|
+
.track .tbtn.stop{border-color:var(--red);background:#fff;color:var(--red)}
|
|
236
|
+
.track .live{display:inline-flex;align-items:center;gap:7px;font-size:13px;color:var(--green);font-weight:600}
|
|
237
|
+
.track .dot{width:9px;height:9px;border-radius:50%;background:var(--green);box-shadow:0 0 0 0 rgba(22,163,74,.5);animation:pulse 1.8s infinite}
|
|
238
|
+
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(22,163,74,.45)}70%{box-shadow:0 0 0 8px rgba(22,163,74,0)}100%{box-shadow:0 0 0 0 rgba(22,163,74,0)}}
|
|
239
|
+
.track .drift{margin-top:12px;border-radius:10px;padding:10px 14px;font-size:13.5px;font-weight:560;display:none}
|
|
240
|
+
.track .drift.degraded{background:#fff1f3;border:1px solid #fecdd6;color:var(--red)}
|
|
241
|
+
.track .drift.improved{background:#f0fdf4;border:1px solid #bbf7d0;color:var(--green)}
|
|
242
|
+
.track .drift.changed,.track .drift.stable{background:var(--a-soft);border:1px solid #dfe2ff;color:var(--a)}
|
|
243
|
+
.track .tl{margin-top:10px;display:flex;gap:6px;align-items:center;flex-wrap:wrap;font-family:ui-monospace,Menlo,monospace;font-size:12px}
|
|
244
|
+
.track .tl .pill{padding:2px 8px;border-radius:20px;border:1px solid var(--line);background:#fff;color:var(--ink2)}
|
|
245
|
+
.track .tl .pill.deg{border-color:#fecdd6;color:var(--red)} .track .tl .pill.imp{border-color:#bbf7d0;color:var(--green)}
|
|
246
|
+
.track .thelp{margin-top:8px;font-size:12px;color:var(--sub);line-height:1.5}
|
|
247
|
+
.track .tfeat{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:12px}
|
|
248
|
+
@media(max-width:620px){.track .tfeat{grid-template-columns:1fr}}
|
|
249
|
+
.track .tf{background:#fff;border:1px solid var(--line);border-radius:10px;padding:10px 12px;font-size:12px}
|
|
250
|
+
.track .tf b{display:block;font-size:12.5px;color:var(--ink);margin-bottom:3px}
|
|
251
|
+
.track .tf span{color:var(--sub);line-height:1.45}
|
|
172
252
|
footer{padding:40px 0 70px;text-align:center;color:#aab0b8;font-size:13px}
|
|
173
253
|
.spin{display:inline-block;width:15px;height:15px;border:2px solid currentColor;border-top-color:transparent;
|
|
174
254
|
border-radius:50%;animation:s .7s linear infinite;vertical-align:-2px;margin-right:7px;opacity:.9}
|
|
@@ -198,8 +278,8 @@
|
|
|
198
278
|
</style>
|
|
199
279
|
</head>
|
|
200
280
|
<body>
|
|
201
|
-
<!-- grand meteor-impact illustration —
|
|
202
|
-
<div class="comet comet-a" aria-hidden="true">
|
|
281
|
+
<!-- grand meteor-impact illustration — moved to the bottom showcase -->
|
|
282
|
+
<div class="comet comet-a" aria-hidden="true" style="display:none">
|
|
203
283
|
<svg viewBox="0 0 360 420" width="360" height="420">
|
|
204
284
|
<defs>
|
|
205
285
|
<linearGradient id="mtail" x1="1" y1="0" x2="0" y2="1">
|
|
@@ -258,10 +338,10 @@
|
|
|
258
338
|
<span><b>X-Ray</b> — instant health report: dependencies, secrets, risk & who-to-ask.</span>
|
|
259
339
|
<span><b>📦 AI Pack</b> — bundle the whole repo into one text to paste into any AI chat.</span>
|
|
260
340
|
</div>
|
|
261
|
-
<p class="hint">Every number is <b>reproducible</b> from git, code & package metadata and sealed with an <b>offline-verifiable</b> signature. <span class="muted">Private folder?
|
|
341
|
+
<p class="hint">Every number is <b>reproducible</b> from git, code & package metadata and sealed with an <b>offline-verifiable</b> signature. <span class="muted">Private repo or a local folder with no public URL? Run <code>npx @mneme-ai/xray bridge</code> on your machine — a <b>Scan a local folder</b> box appears here + your code never leaves your computer.</span></p>
|
|
262
342
|
<details class="keybox">
|
|
263
343
|
<summary>🔖 Optional — save your reports</summary>
|
|
264
|
-
<div class="keyhelp">A “key” is
|
|
344
|
+
<div class="keyhelp">A “key” is just a password you make up (no sign-up). <b>How it works:</b> ① type a key + Save · ② X-Ray any repo above — it's filed under your key automatically · ③ open <b>My repos</b> below to find it anytime, on any device with the same key.</div>
|
|
265
345
|
<div class="keyrow">
|
|
266
346
|
<input id="key" placeholder="make up a password — e.g. my-secret-123" autocomplete="off" />
|
|
267
347
|
<button id="savekey" type="button">Save</button>
|
|
@@ -270,39 +350,145 @@
|
|
|
270
350
|
<div id="keymsg" class="keymsg" style="display:none"></div>
|
|
271
351
|
</details>
|
|
272
352
|
<div class="localbox" id="localbox">
|
|
273
|
-
<div class="lh"><span class="localdot"></span>
|
|
353
|
+
<div class="lh"><span class="localdot" id="localdot"></span><b>Or scan a folder on this computer</b> <span class="muted" id="localhead">— nothing is uploaded</span></div>
|
|
274
354
|
<div class="localrow">
|
|
275
|
-
<
|
|
276
|
-
<
|
|
277
|
-
<button id="localgo" type="button">Scan</button>
|
|
278
|
-
<button id="localpack" type="button" class="ghostbtn">📦 Pack</button>
|
|
355
|
+
<button id="pickfolder" type="button" class="pickbtn">📂 Choose a folder…</button>
|
|
356
|
+
<span class="muted" id="picknote">No install, no terminal — your browser asks which folder, the scan runs right here (secrets · dependencies · size).</span>
|
|
279
357
|
</div>
|
|
280
|
-
<div
|
|
281
|
-
<
|
|
358
|
+
<div id="localout"></div>
|
|
359
|
+
<details class="bridgebox">
|
|
360
|
+
<summary>Want the FULL report on a local repo (git history, bus factor, vitality)?</summary>
|
|
361
|
+
<div class="localhint">Run this once on your machine — a tiny local agent reads git + files locally and shows the <b>signed</b> result here. Nothing is uploaded.
|
|
362
|
+
<div class="cmdrow"><code>npx @mneme-ai/xray bridge</code><button id="copybridge" type="button" class="ghostbtn">Copy</button></div>
|
|
363
|
+
</div>
|
|
364
|
+
<div class="localrow" id="bridgerow" style="display:none">
|
|
365
|
+
<input id="localpath" placeholder="/absolute/path/to/your/repo" autocomplete="off" spellcheck="false" />
|
|
366
|
+
<button id="localbrowse" type="button" class="ghostbtn">📂 Browse</button>
|
|
367
|
+
<button id="localgo" type="button">Scan</button>
|
|
368
|
+
<button id="localpack" type="button" class="ghostbtn">📦 Pack</button>
|
|
369
|
+
</div>
|
|
370
|
+
<div class="picker" id="picker"></div>
|
|
371
|
+
</details>
|
|
282
372
|
</div>
|
|
283
373
|
<p class="err" id="err" style="display:none"></p>
|
|
284
374
|
</header>
|
|
285
375
|
|
|
286
376
|
<div id="out"></div>
|
|
287
377
|
<div id="packout"></div>
|
|
378
|
+
<div id="track" class="track" style="display:none"></div>
|
|
288
379
|
|
|
289
380
|
<div class="board">
|
|
290
381
|
<div class="tabs"><button id="tabBoard" class="tab on">Recently X-rayed</button><button id="tabMine" class="tab" style="display:none">My repos</button></div>
|
|
291
382
|
<p class="boardnote" id="boardnote">🌍 <b>Public</b> — recent X-Rays of <b>public</b> repos, visible to everyone. Private repos never appear here.</p>
|
|
292
383
|
<div id="board"></div>
|
|
293
384
|
</div>
|
|
385
|
+
|
|
386
|
+
<!-- SHOWCASE — the selling points (real: this is what Live tracking does) + the grand graphic -->
|
|
387
|
+
<section class="showcase">
|
|
388
|
+
<h2 class="sctitle">From a one-shot check to an <span class="hl">AI Auditor for your team</span></h2>
|
|
389
|
+
<p class="scsub">Turn on Live tracking and X-Ray watches a branch 24/7 — every push, human or AI, is re-scanned and the drift surfaces here. No re-click.</p>
|
|
390
|
+
<div class="sc3">
|
|
391
|
+
<div class="sccard"><div class="scico">🛡</div><b>AI Code Drift Detection</b><span>The moment anyone — or an AI — pushes, catch a newly-leaked secret, a destructive CI command, or a grade drop. Malicious or off-architecture code is flagged on arrival.</span></div>
|
|
392
|
+
<div class="sccard"><div class="scico">🔁</div><b>Continuous Autonomous Audit</b><span>Re-checked on every push (or every 30s) — a 24/7 security & health audit that runs itself. Nobody has to remember to click.</span></div>
|
|
393
|
+
<div class="sccard"><div class="scico">🕰</div><b>Time-Machine Indexing</b><span>A signed timeline of how code-health drifted, commit by commit — see exactly when a risk entered and who pushed it.</span></div>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="bigart" aria-hidden="true">
|
|
396
|
+
<svg viewBox="0 0 360 420" width="440" height="513">
|
|
397
|
+
<defs>
|
|
398
|
+
<linearGradient id="mtail2" x1="1" y1="0" x2="0" y2="1"><stop offset="0" stop-color="#0b0b0f" stop-opacity=".5"/><stop offset="1" stop-color="#0b0b0f" stop-opacity="0"/></linearGradient>
|
|
399
|
+
<radialGradient id="dust2" cx="50%" cy="60%"><stop offset="0" stop-color="#5b5bf6" stop-opacity=".18"/><stop offset="1" stop-color="#5b5bf6" stop-opacity="0"/></radialGradient>
|
|
400
|
+
<linearGradient id="rock2" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="#2b2c4a"/><stop offset="1" stop-color="#0b0b0f"/></linearGradient>
|
|
401
|
+
</defs>
|
|
402
|
+
<path d="M-20 322 Q90 300 150 330 Q175 344 200 330 Q280 300 380 320" fill="none" stroke="#5b5bf6" stroke-width="2" opacity=".4"/>
|
|
403
|
+
<ellipse cx="172" cy="332" rx="96" ry="24" fill="url(#dust2)"/>
|
|
404
|
+
<path d="M300 24 Q236 96 196 250 Q244 110 296 40 Z" fill="url(#mtail2)"/>
|
|
405
|
+
<g transform="translate(196 250)">
|
|
406
|
+
<polygon points="-34,-6 -18,-34 14,-40 36,-18 30,18 2,34 -28,22" fill="url(#rock2)"/>
|
|
407
|
+
<g stroke="#5b5bf6" stroke-width="1.3" opacity=".6" fill="none"><path d="M-18 -34 L2 -6 L36 -18"/><path d="M2 -6 L-28 22"/><path d="M2 -6 L2 34"/><path d="M2 -6 L30 18"/></g>
|
|
408
|
+
<polygon points="-18,-34 14,-40 2,-6" fill="#34356a"/>
|
|
409
|
+
</g>
|
|
410
|
+
<g>
|
|
411
|
+
<polygon points="86,178 110,150 128,170 116,210 92,206" fill="url(#rock2)"/><polygon points="248,170 276,150 296,184 272,206 250,194" fill="url(#rock2)"/>
|
|
412
|
+
<polygon points="150,128 168,104 186,124 178,156 154,152" fill="url(#rock2)"/>
|
|
413
|
+
<polygon points="44,256 64,242 74,266 56,278" fill="#0b0b0f"/><polygon points="300,250 318,238 326,262 306,272" fill="#0b0b0f"/>
|
|
414
|
+
<polygon points="208,150 222,134 234,150 226,170" fill="#0b0b0f" opacity=".82"/>
|
|
415
|
+
</g>
|
|
416
|
+
<g fill="#5b5bf6" opacity=".5"><polygon points="130,96 140,90 142,102 132,104"/><polygon points="232,108 242,102 244,114 234,116"/><polygon points="70,150 79,145 81,156 71,158"/><polygon points="286,138 295,133 297,144 287,146"/></g>
|
|
417
|
+
</svg>
|
|
418
|
+
</div>
|
|
419
|
+
</section>
|
|
294
420
|
</div>
|
|
295
421
|
<footer>Mneme — the trust & cost layer for code. Deterministic · Signed · Local-first.<br/><span id="copyr">© Mneme</span></footer>
|
|
296
422
|
|
|
297
423
|
<script src="/card.js"></script>
|
|
424
|
+
<script src="/local-scan.js"></script>
|
|
298
425
|
<script>
|
|
299
426
|
const gC = g => "g-" + (("ABCDEF".includes(g)) ? g : "C");
|
|
300
427
|
const esc = s => String(s==null?"":s).replace(/[&<>]/g, c => ({"&":"&","<":"<",">":">"}[c]));
|
|
301
428
|
|
|
302
|
-
function
|
|
429
|
+
function renderCard(signed){
|
|
303
430
|
document.getElementById("out").innerHTML = window.MnemeXRay.xrayCardHTML(signed, { share: true });
|
|
304
431
|
mountShare(signed.report);
|
|
305
432
|
}
|
|
433
|
+
function render(signed){
|
|
434
|
+
renderCard(signed);
|
|
435
|
+
mountTracking(signed.report);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── REAL-TIME TRACKING (branch-aware · poll+webhook · live SSE · drift) ──
|
|
439
|
+
const brank = n => /^(main)$/.test(n)?0 : /^(master)$/.test(n)?1 : /^(develop|dev)$/.test(n)?2 : 9;
|
|
440
|
+
function mountTracking(r){
|
|
441
|
+
const panel = document.getElementById("track");
|
|
442
|
+
if(!r || r.subject.kind !== "git-url"){ panel.style.display="none"; return; }
|
|
443
|
+
const gitUrl = r.subject.ref;
|
|
444
|
+
panel.style.display="block";
|
|
445
|
+
panel.innerHTML = `<div class="thead"><span class="ticon">🛰</span><b>Live tracking</b><span class="tnew">new</span></div>
|
|
446
|
+
<div class="thelp">Pick a branch and press <b>Track live</b> — when you, a teammate, or an AI pushes, this report <b>re-scans itself</b> and shows what changed. No re-click. <span class="muted">(What you get → see “AI Auditor for your team” below.)</span></div>
|
|
447
|
+
<div class="trow">
|
|
448
|
+
<span class="tlabel">Branch</span>
|
|
449
|
+
<select id="tbranch"><option value="">loading branches…</option></select>
|
|
450
|
+
<button id="ttoggle" class="tbtn off">● Track live</button>
|
|
451
|
+
<span id="tstate" class="tlabel"></span>
|
|
452
|
+
</div>
|
|
453
|
+
<div id="tdrift" class="drift"></div>
|
|
454
|
+
<div id="ttl" class="tl"></div>
|
|
455
|
+
<div id="tnote" class="thelp" style="display:none"></div>`;
|
|
456
|
+
fetch("/api/branches?url="+encodeURIComponent(gitUrl)).then(x=>x.json()).then(d=>{
|
|
457
|
+
const sel=document.getElementById("tbranch"); if(!sel) return;
|
|
458
|
+
const names=((d&&d.branches)||[]).map(b=>b.name);
|
|
459
|
+
if(!names.length){ sel.innerHTML='<option value="">default branch</option>'; return; }
|
|
460
|
+
names.sort((a,b)=>brank(a)-brank(b)||a.localeCompare(b));
|
|
461
|
+
sel.innerHTML=names.map(n=>`<option value="${esc(n)}"${n===r.subject.branch?" selected":""}>${esc(n)}</option>`).join("");
|
|
462
|
+
}).catch(()=>{ const sel=document.getElementById("tbranch"); if(sel) sel.innerHTML='<option value="">default branch</option>'; });
|
|
463
|
+
|
|
464
|
+
let es=null; const history=[];
|
|
465
|
+
const toggle=document.getElementById("ttoggle");
|
|
466
|
+
const stateEl=()=>document.getElementById("tstate");
|
|
467
|
+
const noteEl=()=>document.getElementById("tnote");
|
|
468
|
+
function stop(){ if(es){es.close();es=null;} toggle.className="tbtn off"; toggle.textContent="● Track live"; stateEl().textContent="stopped"; const n=noteEl(); if(n) n.style.display="none"; const sel=document.getElementById("tbranch"); if(sel) sel.disabled=false; }
|
|
469
|
+
function showDrift(d){ const el=document.getElementById("tdrift"); if(!el||!d) return; el.className="drift "+d.drift; el.style.display="block"; el.textContent=((d.highlights&&d.highlights[0])||("changed → grade "+d.gradeTo))+" · just now"; }
|
|
470
|
+
function pushTL(d){ history.push(d); const el=document.getElementById("ttl"); if(!el) return; el.innerHTML='<span class="tlabel">Timeline</span> '+history.slice(-14).map(h=>{const c=h.drift==="degraded"?"deg":h.drift==="improved"?"imp":""; return '<span class="pill '+c+'">'+esc(h.gradeTo)+(h.newSecretLeaks>0?" 🔴":"")+'</span>';}).join(" "); }
|
|
471
|
+
function startSSE(id, branch){
|
|
472
|
+
es=new EventSource("/api/track/"+id+"/stream");
|
|
473
|
+
toggle.className="tbtn stop"; toggle.textContent="Stop"; toggle.disabled=false;
|
|
474
|
+
const sel=document.getElementById("tbranch"); if(sel) sel.disabled=true; // lock branch while LIVE
|
|
475
|
+
stateEl().innerHTML='<span class="live"><span class="dot"></span>LIVE · '+esc(branch||"default")+'</span>';
|
|
476
|
+
const n=noteEl(); if(n){ n.style.display="block"; n.innerHTML='✓ <b>Now monitoring '+esc(branch||"the default branch")+'.</b> Leave this tab open to watch changes appear here live. The monitor keeps running on the server even if you close the tab — come back and X-Ray this same repo to reconnect. <span class="muted">Switching repo or branch starts a separate monitor; a refresh reconnects this view.</span>'; }
|
|
477
|
+
es.addEventListener("update", ev=>{ try{ const p=JSON.parse(ev.data); renderCard(p.signed); showDrift(p.delta); pushTL(p.delta);}catch{} });
|
|
478
|
+
// EventSource auto-reconnects on error; keep the LIVE state.
|
|
479
|
+
}
|
|
480
|
+
toggle.addEventListener("click", async ()=>{
|
|
481
|
+
if(es){ stop(); return; }
|
|
482
|
+
const branch=document.getElementById("tbranch").value||undefined;
|
|
483
|
+
toggle.disabled=true; toggle.textContent="starting…";
|
|
484
|
+
try{
|
|
485
|
+
const res=await fetch("/api/track",{method:"POST",headers:{"content-type":"application/json"},body:JSON.stringify({gitUrl,branch})});
|
|
486
|
+
const data=await res.json();
|
|
487
|
+
if(!res.ok){ stateEl().textContent=data.error||"failed"; toggle.disabled=false; toggle.textContent="● Track live"; return; }
|
|
488
|
+
startSSE(data.trackId, branch);
|
|
489
|
+
}catch(e){ stateEl().textContent=String(e); toggle.disabled=false; toggle.textContent="● Track live"; }
|
|
490
|
+
});
|
|
491
|
+
}
|
|
306
492
|
|
|
307
493
|
// share row: permalink + an embeddable badge (the viral loop)
|
|
308
494
|
function mountShare(r){
|
|
@@ -367,7 +553,7 @@ document.getElementById("savekey").addEventListener("click", ()=>{
|
|
|
367
553
|
});
|
|
368
554
|
|
|
369
555
|
let tab="board", offset=0, loaded=[];
|
|
370
|
-
const PAGE=
|
|
556
|
+
const PAGE=5;
|
|
371
557
|
function fmtDate(s){ if(!s) return "—"; const d=new Date(s); return isNaN(d)?"—":d.toLocaleDateString(undefined,{year:"numeric",month:"short",day:"numeric"}); }
|
|
372
558
|
function rowHTML(b){
|
|
373
559
|
const lock = b.visibility==="private" ? '<span class="lock" title="private — only you can open this">🔒</span>' : "";
|
|
@@ -397,16 +583,18 @@ async function loadList(reset=true){
|
|
|
397
583
|
}
|
|
398
584
|
try{
|
|
399
585
|
const data=await fetchPage(offset);
|
|
400
|
-
|
|
401
|
-
if(
|
|
402
|
-
el.innerHTML=`<div class="bitem muted">${tab==="mine"?"Nothing here yet — run an <b>X-Ray</b> or <b>📦 AI Pack</b> above and it appears here automatically. (Private folder? use the
|
|
586
|
+
const items=data.items||[];
|
|
587
|
+
if((data.total||0)===0){
|
|
588
|
+
el.innerHTML=`<div class="bitem muted">${tab==="mine"?"Nothing here yet — run an <b>X-Ray</b> or <b>📦 AI Pack</b> above and it appears here automatically. (Private folder? use the Choose-a-folder panel.)":"No reports yet — be the first."}</div>`;
|
|
403
589
|
return;
|
|
404
590
|
}
|
|
405
|
-
const
|
|
406
|
-
|
|
407
|
-
|
|
591
|
+
const start=data.offset+1, end=data.offset+items.length;
|
|
592
|
+
const hasPrev=data.offset>0, hasNext=(data.offset+data.limit)<data.total;
|
|
593
|
+
el.innerHTML = `<div class="listbox">${items.map(rowHTML).join("")}</div>`
|
|
594
|
+
+ `<div class="listfoot"><span class="muted">${start}–${end} of ${data.total} repos</span><span class="pagenav"><button class="moreb" id="prevb"${hasPrev?"":" disabled"}>← Prev</button><button class="moreb" id="nextb"${hasNext?"":" disabled"}>Next →</button></span></div>`;
|
|
408
595
|
el.querySelectorAll(".bitem[data-fp]").forEach(n=>n.addEventListener("click",()=>openReport(n.dataset.fp, n.dataset.priv==="1")));
|
|
409
|
-
const
|
|
596
|
+
const pb=document.getElementById("prevb"); if(pb&&hasPrev) pb.addEventListener("click",()=>{ offset=Math.max(0,offset-PAGE); loadList(false); window.scrollTo({top:document.getElementById("board").offsetTop-80,behavior:"smooth"}); });
|
|
597
|
+
const nb=document.getElementById("nextb"); if(nb&&hasNext) nb.addEventListener("click",()=>{ offset+=PAGE; loadList(false); window.scrollTo({top:document.getElementById("board").offsetTop-80,behavior:"smooth"}); });
|
|
410
598
|
}catch{ el.innerHTML='<div class="bitem muted">Could not load.</div>'; }
|
|
411
599
|
}
|
|
412
600
|
async function openReport(fp, isPrivate){
|
|
@@ -453,9 +641,51 @@ async function detectBridge(){
|
|
|
453
641
|
try{
|
|
454
642
|
const ctrl=new AbortController(); const t=setTimeout(()=>ctrl.abort(),1200);
|
|
455
643
|
const r=await fetch(BRIDGE+"/bridge/ping",{signal:ctrl.signal}); clearTimeout(t);
|
|
456
|
-
if(r.ok){
|
|
457
|
-
|
|
644
|
+
if(r.ok){ // full-report bridge is up → light up the green dot + the bridge row
|
|
645
|
+
document.getElementById("localbox").classList.add("connected");
|
|
646
|
+
const h=document.getElementById("localhead"); if(h) h.textContent="— local agent connected · full signed report available";
|
|
647
|
+
const br=document.getElementById("bridgerow"); if(br) br.style.display="flex";
|
|
648
|
+
}
|
|
649
|
+
}catch{ /* no local agent — the zero-install Choose-folder path still works */ }
|
|
650
|
+
}
|
|
651
|
+
// ---- ZERO-INSTALL local scan (File System Access API) — no terminal, no upload ----
|
|
652
|
+
const copyBtn=document.getElementById("copybridge");
|
|
653
|
+
if(copyBtn) copyBtn.addEventListener("click",async()=>{ try{ await navigator.clipboard.writeText("npx @mneme-ai/xray bridge"); const o=copyBtn.textContent; copyBtn.textContent="Copied ✓"; setTimeout(()=>copyBtn.textContent=o,1200);}catch{} });
|
|
654
|
+
function renderLocalScan(r){
|
|
655
|
+
const s=r.summary, out=document.getElementById("localout"); if(!out) return;
|
|
656
|
+
const sec=s.secrets, secCls=sec.totalFindings>0?"bad":"ok";
|
|
657
|
+
const langs=s.langs.map(([e,n])=>`<span class="pill">${esc(e)} ${n}</span>`).join(" ");
|
|
658
|
+
const g=s.git, vit = !g.has ? "" : (s.dormantDays>365?"dormant":s.dormantDays>120?"slowing":"active");
|
|
659
|
+
const gitRows = g.has ? `
|
|
660
|
+
<div class="lsrow"><span class="lsk">Bus factor</span><span class="lsv ${g.authors===1?"bad":""}"><b>${g.authors}</b> author${g.authors===1?" — single point of failure":"s"} · top author ${Math.round(g.topShare*100)}% of ${g.commits} commits</span></div>
|
|
661
|
+
<div class="lsrow"><span class="lsk">Vitality</span><span class="lsv ${vit==="dormant"?"bad":""}"><b>${vit}</b> · ${s.ageDays}d span · last activity ${s.dormantDays}d ago</span></div>`
|
|
662
|
+
: `<div class="lsrow"><span class="lsk">Git history</span><span class="lsv muted">— no .git reflog in this folder (bus factor / vitality unavailable)</span></div>`;
|
|
663
|
+
out.innerHTML=`<div class="lscard">
|
|
664
|
+
<div class="lstop"><div class="lsgrade g-${("ABCDEF".includes(s.grade)?s.grade:"C")}">${esc(s.grade)}</div>
|
|
665
|
+
<div><div class="lsh"><b>📂 ${esc(r.folder)}</b></div><div class="muted" style="font-size:12px">scanned in your browser · ${r.files} files · nothing uploaded</div></div></div>
|
|
666
|
+
<div class="lsrow"><span class="lsk">Secrets</span><span class="lsv ${secCls}"><b>${sec.totalFindings}</b> in production code${sec.excludedTestHits?` · ${sec.excludedTestHits} in tests (excluded)`:""}</span></div>
|
|
667
|
+
${sec.hits.length?`<div class="lshits">${sec.hits.slice(0,6).map(h=>`<div>🔴 ${esc(h.kind)} — ${esc(h.file)}:${h.line}</div>`).join("")}</div>`:`<div class="lsok">✓ no leaked credentials in production code</div>`}
|
|
668
|
+
${gitRows}
|
|
669
|
+
<div class="lsrow"><span class="lsk">Dependencies</span><span class="lsv"><b>${s.deps.total}</b> total · ${s.deps.deps} deps · ${s.deps.devDeps} dev</span></div>
|
|
670
|
+
<div class="lsrow"><span class="lsk">Complexity</span><span class="lsv"><b>${(s.symbols||0).toLocaleString()}</b> symbols · ${s.filesScanned} files · ${s.loc.toLocaleString()} lines</span></div>
|
|
671
|
+
<div class="lsrow"><span class="lsk">Languages</span><span class="lsv">${langs||"—"}</span></div>
|
|
672
|
+
<div class="lsnote">Computed <b>in your browser</b> (unsigned, nothing uploaded). Secrets · dependencies · complexity from your files; bus factor & vitality from <code>.git</code> reflog. The full <b>signed</b> report (dep mortality, hotspots, coupling) needs a public URL above or the bridge.</div>
|
|
673
|
+
</div>`;
|
|
458
674
|
}
|
|
675
|
+
const pick=document.getElementById("pickfolder");
|
|
676
|
+
if(pick) pick.addEventListener("click", async ()=>{
|
|
677
|
+
const note=document.getElementById("picknote");
|
|
678
|
+
if(!window.MnemeLocalScan || !window.MnemeLocalScan.supported){
|
|
679
|
+
if(note) note.textContent="Your browser does not support in-page folder access (try Chrome or Edge), or use the FULL report option below.";
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
pick.disabled=true; const o=pick.textContent; pick.innerHTML='<span class="spin"></span>Scanning locally…';
|
|
683
|
+
try{
|
|
684
|
+
const r=await window.MnemeLocalScan.pickAndScan();
|
|
685
|
+
renderLocalScan(r);
|
|
686
|
+
}catch(e){ if(String(e&&e.name)!=="AbortError" && String(e&&e.message)!=="UNSUPPORTED" && note) note.textContent="Couldn't read that folder: "+String(e&&e.message||e); }
|
|
687
|
+
finally{ pick.disabled=false; pick.textContent=o; }
|
|
688
|
+
});
|
|
459
689
|
document.getElementById("localgo").addEventListener("click", async ()=>{
|
|
460
690
|
const p=document.getElementById("localpath").value.trim();
|
|
461
691
|
const err=document.getElementById("err"), btn=document.getElementById("localgo");
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* In-browser LOCAL FOLDER scan — zero install, no terminal, NOTHING uploaded.
|
|
3
|
+
*
|
|
4
|
+
* A website cannot read your disk on its own (browser security). The File System
|
|
5
|
+
* Access API is the honest exception: the user clicks "Choose folder", the OS
|
|
6
|
+
* shows a native picker, and ONLY the folder they grant is readable — and only by
|
|
7
|
+
* code running in their own tab. We read the files in the browser, run the
|
|
8
|
+
* deterministic file-content analyzers (secrets · dependencies · size), and render
|
|
9
|
+
* the result locally. The bytes never leave the machine.
|
|
10
|
+
*
|
|
11
|
+
* Honest scope: git-history signals (bus factor · vitality · hotspots · coupling)
|
|
12
|
+
* need git, which a browser cannot run — those need the public URL or the bridge.
|
|
13
|
+
* This is the instant, no-install "what's in this folder" scan.
|
|
14
|
+
*/
|
|
15
|
+
(function (g) {
|
|
16
|
+
// ── pure, deterministic analyzers (unit-testable, no DOM) ──────────────────
|
|
17
|
+
const SECRET_PATTERNS = [
|
|
18
|
+
{ kind: "AWS access key", re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
19
|
+
{ kind: "AWS secret key", re: /\b[A-Za-z0-9/+]{40}\b(?=.*aws|.*secret)/i },
|
|
20
|
+
{ kind: "GitHub token", re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/ },
|
|
21
|
+
{ kind: "OpenAI key", re: /\bsk-[A-Za-z0-9]{20,}\b/ },
|
|
22
|
+
{ kind: "Anthropic key", re: /\bsk-ant-[A-Za-z0-9_-]{20,}\b/ },
|
|
23
|
+
{ kind: "Slack token", re: /\bxox[baprs]-[A-Za-z0-9-]{10,}\b/ },
|
|
24
|
+
{ kind: "Google API key", re: /\bAIza[0-9A-Za-z_-]{35}\b/ },
|
|
25
|
+
{ kind: "private key block", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----/ },
|
|
26
|
+
{ kind: "JWT", re: /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\b/ },
|
|
27
|
+
];
|
|
28
|
+
// test/fixture/doc files are sample data, not leaks — excluded from the count
|
|
29
|
+
const TEST_RE = /(^|\/)(test|tests|__tests__|fixtures?|examples?|docs?|spec|mocks?)(\/|$)|\.(test|spec)\.|\.md$|\.lock$/i;
|
|
30
|
+
const TEXT_EXT = /\.(ts|tsx|js|jsx|mjs|cjs|py|go|rs|java|c|h|cpp|cc|rb|php|cs|kt|swift|scala|sh|bash|env|json|ya?ml|toml|ini|cfg|xml|gradle|properties|tf|sql|vue|svelte)$/i;
|
|
31
|
+
const SKIP_DIR = /(^|\/)(node_modules|\.git|dist|build|vendor|\.next|coverage|__pycache__|\.venv|target)(\/|$)/;
|
|
32
|
+
|
|
33
|
+
function scanSecretsText(text, relPath) {
|
|
34
|
+
const isTest = TEST_RE.test(relPath || "");
|
|
35
|
+
const hits = [];
|
|
36
|
+
const lines = String(text).split("\n");
|
|
37
|
+
for (let i = 0; i < lines.length; i++) {
|
|
38
|
+
for (const p of SECRET_PATTERNS) {
|
|
39
|
+
if (p.re.test(lines[i])) { hits.push({ kind: p.kind, file: relPath, line: i + 1, isTest }); }
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return hits;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function parseDeps(pkgJsonText) {
|
|
46
|
+
try {
|
|
47
|
+
const p = JSON.parse(pkgJsonText);
|
|
48
|
+
const d = Object.keys(p.dependencies || {});
|
|
49
|
+
const dev = Object.keys(p.devDependencies || {});
|
|
50
|
+
return { total: d.length + dev.length, deps: d.length, devDeps: dev.length, names: [...d, ...dev].slice(0, 60) };
|
|
51
|
+
} catch { return { total: 0, deps: 0, devDeps: 0, names: [] }; }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// structural complexity — count declared symbols (deterministic, regex-based)
|
|
55
|
+
const SYMBOL_RE = /\b(function\s+\w+|class\s+\w+|interface\s+\w+|def\s+\w+|func\s+\w+|fn\s+\w+|export\s+(?:const|function|class|default)|=>\s*[{(])/g;
|
|
56
|
+
function countSymbols(text) { const m = String(text).match(SYMBOL_RE); return m ? m.length : 0; }
|
|
57
|
+
|
|
58
|
+
// REAL git signals from .git/logs/HEAD (the reflog — plain text the browser can read).
|
|
59
|
+
// Each line: <old> <new> <Name> <email> <unixTs> <tz>\t<message>. Honest: this is
|
|
60
|
+
// HEAD-movement history (commits/resets/merges), a sound approximation of authorship
|
|
61
|
+
// + activity window without running git or parsing packfiles.
|
|
62
|
+
function parseGitLog(text) {
|
|
63
|
+
const authors = {}; let commits = 0, firstTs = 0, lastTs = 0;
|
|
64
|
+
for (const line of String(text || "").split("\n")) {
|
|
65
|
+
const m = line.match(/^[0-9a-f]+\s+[0-9a-f]+\s+(.+?)\s+<[^>]*>\s+(\d+)\s/);
|
|
66
|
+
if (!m) continue;
|
|
67
|
+
commits++;
|
|
68
|
+
authors[m[1]] = (authors[m[1]] || 0) + 1;
|
|
69
|
+
const ts = parseInt(m[2], 10) * 1000;
|
|
70
|
+
if (!firstTs || ts < firstTs) firstTs = ts;
|
|
71
|
+
if (ts > lastTs) lastTs = ts;
|
|
72
|
+
}
|
|
73
|
+
const names = Object.keys(authors);
|
|
74
|
+
const top = names.length ? Math.max(...names.map((n) => authors[n])) : 0;
|
|
75
|
+
return { commits, authors: names.length, topShare: commits ? top / commits : 0, firstTs, lastTs, has: commits > 0 };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const GRADES = ["A", "B", "C", "D", "F"];
|
|
79
|
+
/** A deterministic grade from the signals available IN-BROWSER. Honest: lighter
|
|
80
|
+
* than the full server grade (no dep-mortality / hotspots), but real. */
|
|
81
|
+
function gradeLocal(s) {
|
|
82
|
+
let pen = 0;
|
|
83
|
+
if (s.secrets.totalFindings > 0) pen += s.secrets.totalFindings >= 3 ? 4 : s.secrets.totalFindings >= 1 ? 3 : 0; // a leaked secret is severe
|
|
84
|
+
if (s.git.has) { if (s.git.authors === 1) pen += 1; if (s.git.topShare > 0.8) pen += 1; if (s.ageDays > 0 && s.dormantDays > 365) pen += 1; }
|
|
85
|
+
return GRADES[Math.min(pen, 4)];
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Compose a deterministic local report from already-read files + .git reflog. Pure. */
|
|
89
|
+
function summarize(files, gitLog, nowMs) {
|
|
90
|
+
// files: [{ rel, text }]
|
|
91
|
+
let loc = 0, scanned = 0, testHits = 0, symbols = 0;
|
|
92
|
+
const prodHits = [];
|
|
93
|
+
const langs = {};
|
|
94
|
+
let deps = { total: 0, deps: 0, devDeps: 0, names: [] };
|
|
95
|
+
for (const f of files) {
|
|
96
|
+
if (SKIP_DIR.test(f.rel)) continue;
|
|
97
|
+
if (/(^|\/)package\.json$/.test(f.rel) && !/node_modules/.test(f.rel)) deps = parseDeps(f.text);
|
|
98
|
+
if (!TEXT_EXT.test(f.rel)) continue;
|
|
99
|
+
scanned++;
|
|
100
|
+
loc += f.text.split("\n").length;
|
|
101
|
+
symbols += countSymbols(f.text);
|
|
102
|
+
const ext = (f.rel.match(/\.([a-z0-9]+)$/i) || [, "?"])[1].toLowerCase();
|
|
103
|
+
langs[ext] = (langs[ext] || 0) + 1;
|
|
104
|
+
for (const h of scanSecretsText(f.text, f.rel)) { if (h.isTest) testHits++; else prodHits.push(h); }
|
|
105
|
+
}
|
|
106
|
+
const git = parseGitLog(gitLog);
|
|
107
|
+
const now = nowMs || (git.lastTs || 0);
|
|
108
|
+
const ageDays = git.has && git.firstTs ? Math.max(0, Math.round((git.lastTs - git.firstTs) / 86400000)) : 0;
|
|
109
|
+
const dormantDays = git.has && git.lastTs && now ? Math.max(0, Math.round((now - git.lastTs) / 86400000)) : 0;
|
|
110
|
+
const s = {
|
|
111
|
+
filesScanned: scanned, loc, symbols, deps, git, ageDays, dormantDays,
|
|
112
|
+
secrets: { totalFindings: prodHits.length, excludedTestHits: testHits, hits: prodHits.slice(0, 20) },
|
|
113
|
+
langs: Object.entries(langs).sort((a, b) => b[1] - a[1]).slice(0, 8),
|
|
114
|
+
};
|
|
115
|
+
s.grade = gradeLocal(s);
|
|
116
|
+
return s;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
g.MnemeLocalScan = { scanSecretsText, parseDeps, countSymbols, parseGitLog, gradeLocal, summarize, _patterns: SECRET_PATTERNS };
|
|
120
|
+
|
|
121
|
+
// ── File System Access glue (browser-only; needs a user gesture + HTTPS) ────
|
|
122
|
+
g.MnemeLocalScan.supported = typeof g.showDirectoryPicker === "function";
|
|
123
|
+
|
|
124
|
+
async function readDir(dirHandle, prefix, out, cap) {
|
|
125
|
+
for await (const [name, handle] of dirHandle.entries()) {
|
|
126
|
+
const rel = prefix ? prefix + "/" + name : name;
|
|
127
|
+
if (SKIP_DIR.test(rel) || name.startsWith(".")) continue;
|
|
128
|
+
if (out.length >= cap) return;
|
|
129
|
+
if (handle.kind === "directory") { await readDir(handle, rel, out, cap); }
|
|
130
|
+
else if (TEXT_EXT.test(rel) || /(^|\/)package\.json$/.test(rel)) {
|
|
131
|
+
try { const file = await handle.getFile(); if (file.size <= 2 * 1024 * 1024) out.push({ rel, text: await file.text() }); } catch { /* skip unreadable */ }
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** Open the OS folder picker, read text files in-browser, return the summary +
|
|
137
|
+
* the folder name. Throws on user-cancel (caller catches). cap bounds the walk. */
|
|
138
|
+
g.MnemeLocalScan.pickAndScan = async function pickAndScan(cap = 4000) {
|
|
139
|
+
if (!g.MnemeLocalScan.supported) throw new Error("UNSUPPORTED");
|
|
140
|
+
const dir = await g.showDirectoryPicker(); // native picker — user grants ONE folder
|
|
141
|
+
const files = [];
|
|
142
|
+
await readDir(dir, "", files, cap);
|
|
143
|
+
// REAL git signals: read .git/logs/HEAD (plain text) for authors / commits / age
|
|
144
|
+
let gitLog = "";
|
|
145
|
+
try {
|
|
146
|
+
const gitDir = await dir.getDirectoryHandle(".git");
|
|
147
|
+
const logsDir = await gitDir.getDirectoryHandle("logs");
|
|
148
|
+
const headFile = await logsDir.getFileHandle("HEAD");
|
|
149
|
+
gitLog = await (await headFile.getFile()).text();
|
|
150
|
+
} catch { /* not a git repo, or no reflog — file signals only */ }
|
|
151
|
+
return { folder: dir.name || "folder", files: files.length, summary: summarize(files, gitLog, Date.now()) };
|
|
152
|
+
};
|
|
153
|
+
})(typeof window !== "undefined" ? window : globalThis);
|
package/public/report.html
CHANGED
|
@@ -22,11 +22,22 @@
|
|
|
22
22
|
.grade{width:72px;height:72px;border-radius:16px;display:grid;place-items:center;font-size:36px;font-weight:740;color:#fff;flex:none;box-shadow:inset 0 -2px 6px rgba(0,0,0,.12)}
|
|
23
23
|
.g-A{background:linear-gradient(135deg,#1fb255,#15863f)}.g-B{background:linear-gradient(135deg,#7bb736,#5c8f1e)}
|
|
24
24
|
.g-C{background:linear-gradient(135deg,#eaa83a,#c9821a)}.g-D{background:linear-gradient(135deg,#f0742e,#d4571a)}.g-F{background:linear-gradient(135deg,#f43f5e,#be123c)}
|
|
25
|
-
.top .repo{font-size:22px;font-weight:640;letter-spacing:-.02em;color:var(--ink);word-break:break-all;line-height:1.2}.top .head{color:var(--sub);font-size:14px;margin-top:4px}
|
|
25
|
+
.top .repo{font-size:22px;font-weight:640;letter-spacing:-.02em;color:var(--ink);word-break:break-all;line-height:1.2}.top .repobr{font-size:13px;font-weight:600;color:var(--a);background:var(--a-soft);border-radius:6px;padding:1px 7px;vertical-align:middle}.top .repourl{display:inline-block;font-size:12.5px;color:var(--a);text-decoration:none;margin-top:3px;word-break:break-all;font-family:ui-monospace,Menlo,monospace}.top .repourl:hover{text-decoration:underline}.top .head{color:var(--sub);font-size:14px;margin-top:4px}
|
|
26
26
|
.trustbar{display:flex;align-items:center;gap:14px;padding:13px 30px;background:#f0fdf4;border-bottom:1px solid #dcfce7;flex-wrap:wrap}
|
|
27
27
|
.hgauge{display:inline-flex;align-items:center;gap:8px;font-weight:680;color:#15803d;font-size:14px}
|
|
28
28
|
.hdot{width:10px;height:10px;border-radius:50%;background:#16a34a;box-shadow:0 0 0 4px rgba(22,163,74,.16)}
|
|
29
29
|
.htext{font-size:12.5px;color:#3f6b4e;line-height:1.5}.htext b{color:#14532d;font-weight:640}
|
|
30
|
+
.membrane{display:flex;border-bottom:1px solid #eef0f2;background:#fafbfc}
|
|
31
|
+
.mp{flex:1;padding:11px 18px;border-right:1px solid #eef0f2;display:flex;flex-direction:column;gap:3px}.mp:last-child{border-right:0}
|
|
32
|
+
.mpk{font-size:10.5px;letter-spacing:.06em;color:#8a909a;font-weight:700}
|
|
33
|
+
.mpv{font-size:12.5px;color:#2b2f36;font-weight:560;display:flex;align-items:center;gap:7px}
|
|
34
|
+
.triage{padding:14px 30px 4px}
|
|
35
|
+
.triage-h{font-size:13px;font-weight:680;color:#b45309;margin-bottom:9px}.triage-sub{font-weight:400;color:#9aa0a6;font-size:11.5px}
|
|
36
|
+
.triage.clearall{padding:13px 30px;color:#15803d;font-weight:560;font-size:13px;background:#f0fdf4;border-bottom:1px solid #dcfce7}
|
|
37
|
+
.ti{display:flex;gap:11px;align-items:flex-start;padding:8px 0;border-top:1px solid #f1f2f4}
|
|
38
|
+
.ti-sev{flex:0 0 auto;font-size:10px;font-weight:700;letter-spacing:.04em;text-transform:uppercase;padding:3px 8px;border-radius:6px;min-width:62px;text-align:center}
|
|
39
|
+
.ti.critical .ti-sev{background:#fee2e2;color:#b91c1c}.ti.warn .ti-sev{background:#fef3c7;color:#b45309}.ti.info .ti-sev{background:#e0e7ff;color:#4338ca}
|
|
40
|
+
.ti-body{flex:1;min-width:0}.ti-f{font-size:13px;color:#1f2329}.ti-p{font-size:11px;color:#9aa0a6;margin-top:2px;font-family:ui-monospace,Menlo,monospace}
|
|
30
41
|
.rows{padding:6px 30px 16px}
|
|
31
42
|
.row{display:flex;gap:18px;padding:17px 0;border-bottom:1px solid var(--line2);align-items:baseline}.row:last-child{border-bottom:0}
|
|
32
43
|
.row .k{width:138px;flex:none;display:flex;flex-direction:column;gap:4px;font-size:12.5px;letter-spacing:.05em;text-transform:uppercase;color:var(--ink);font-weight:700}
|
|
@@ -34,7 +45,8 @@
|
|
|
34
45
|
.spark{font-family:ui-monospace,Menlo,monospace;letter-spacing:1px;color:var(--a)}
|
|
35
46
|
.muted{color:var(--sub)}
|
|
36
47
|
.row .v{font-size:15px;color:var(--ink2)}.row .v .big{font-weight:660;color:var(--ink);font-size:16px}
|
|
37
|
-
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px}
|
|
48
|
+
.chips{display:flex;flex-wrap:wrap;gap:6px;margin-top:8px;max-height:148px;overflow-y:auto}
|
|
49
|
+
.chips::-webkit-scrollbar{width:8px}.chips::-webkit-scrollbar-thumb{background:#d7dbe0;border-radius:8px}
|
|
38
50
|
.chip{font-size:12px;padding:4px 10px;border-radius:8px;background:var(--soft);border:1px solid var(--line);color:#5c616b}
|
|
39
51
|
.chip.bad{background:#fff1f3;border-color:#ffe0e6;color:var(--red)}.chip.warn{background:#fff8ed;border-color:#fde6cc;color:#b45309}
|
|
40
52
|
.foot{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:16px 30px;background:var(--soft);font-size:13px;color:var(--sub);flex-wrap:wrap}
|