@prism-d1/cli 1.9.1 → 1.10.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.
Files changed (57) hide show
  1. package/dist/assets/github-workflows/prism-eval-gate-kiro.yml +57 -5
  2. package/dist/assets/gitlab-workflows/prism-eval-gate.yml +46 -7
  3. package/dist/bin/cli.d.ts +2 -0
  4. package/dist/bin/cli.d.ts.map +1 -0
  5. package/dist/bin/cli.js +19 -0
  6. package/dist/bin/cli.js.map +1 -0
  7. package/dist/bin/prism-cli.js +12 -17
  8. package/dist/bin/prism-cli.js.map +1 -1
  9. package/dist/src/commands/_top/dashboard.d.ts +2 -0
  10. package/dist/src/commands/_top/dashboard.d.ts.map +1 -1
  11. package/dist/src/commands/_top/dashboard.js +43 -15
  12. package/dist/src/commands/_top/dashboard.js.map +1 -1
  13. package/dist/src/commands/_top/mcp.d.ts.map +1 -1
  14. package/dist/src/commands/_top/mcp.js +3 -15
  15. package/dist/src/commands/_top/mcp.js.map +1 -1
  16. package/dist/src/commands/_top/statusline.d.ts +13 -0
  17. package/dist/src/commands/_top/statusline.d.ts.map +1 -0
  18. package/dist/src/commands/_top/statusline.js +12 -0
  19. package/dist/src/commands/_top/statusline.js.map +1 -0
  20. package/dist/src/commands/_top/version.d.ts +2 -0
  21. package/dist/src/commands/_top/version.d.ts.map +1 -1
  22. package/dist/src/commands/_top/version.js +4 -0
  23. package/dist/src/commands/_top/version.js.map +1 -1
  24. package/dist/src/commands/assessment/deploy-cdk.js +1 -1
  25. package/dist/src/commands/assessment/deploy-cdk.js.map +1 -1
  26. package/dist/src/commands/enable/claude-code.d.ts +81 -0
  27. package/dist/src/commands/enable/claude-code.d.ts.map +1 -0
  28. package/dist/src/commands/enable/claude-code.js +381 -0
  29. package/dist/src/commands/enable/claude-code.js.map +1 -0
  30. package/dist/src/dashboard/page.d.ts +25 -0
  31. package/dist/src/dashboard/page.d.ts.map +1 -1
  32. package/dist/src/dashboard/page.js +66 -7
  33. package/dist/src/dashboard/page.js.map +1 -1
  34. package/dist/src/dashboard/setup-page.d.ts +1 -1
  35. package/dist/src/dashboard/setup-page.d.ts.map +1 -1
  36. package/dist/src/dashboard/setup-page.js +15 -2
  37. package/dist/src/dashboard/setup-page.js.map +1 -1
  38. package/dist/src/dashboard/snapshot.d.ts +31 -0
  39. package/dist/src/dashboard/snapshot.d.ts.map +1 -0
  40. package/dist/src/dashboard/snapshot.js +65 -0
  41. package/dist/src/dashboard/snapshot.js.map +1 -0
  42. package/dist/src/mcp/dashboard-process.d.ts +83 -0
  43. package/dist/src/mcp/dashboard-process.d.ts.map +1 -0
  44. package/dist/src/mcp/dashboard-process.js +184 -0
  45. package/dist/src/mcp/dashboard-process.js.map +1 -0
  46. package/dist/src/mcp/server.d.ts.map +1 -1
  47. package/dist/src/mcp/server.js +8 -3
  48. package/dist/src/mcp/server.js.map +1 -1
  49. package/dist/src/mcp/tools.d.ts +7 -1
  50. package/dist/src/mcp/tools.d.ts.map +1 -1
  51. package/dist/src/mcp/tools.js +100 -2
  52. package/dist/src/mcp/tools.js.map +1 -1
  53. package/dist/src/statusline/statusline.d.ts +135 -0
  54. package/dist/src/statusline/statusline.d.ts.map +1 -0
  55. package/dist/src/statusline/statusline.js +398 -0
  56. package/dist/src/statusline/statusline.js.map +1 -0
  57. package/package.json +1 -1
@@ -6,6 +6,7 @@
6
6
  * Every value from the payload is placed with textContent or a DOM attribute;
7
7
  * innerHTML is never used for data.
8
8
  */
9
+ import { createHash } from 'node:crypto';
9
10
  import { SETUP_SCRIPT, SETUP_STYLE, THEME_BOOT, THEME_SCRIPT } from './setup-page.js';
10
11
  /** Colour tokens; the dark set applies by system preference unless the toggle picks Light, and always with Dark. */
11
12
  const LIGHT_VARS = '--bg:#f7f7f8; --card:#fff; --text:#1d1d1f; --muted:#6b6b73; --line:#e3e3e8; --ok:#1a7f37; --warn:#9a6700; --bad:#cf222e; --high:#cf222e; --medium:#bf8700; --low:#3d6fa8; --accent:#0969da; --raised:var(--bg);';
@@ -48,6 +49,7 @@ a.rlink { color:var(--accent); } a.rlink:hover { text-decoration:underline; }
48
49
  .help { font-size:12px; margin-top:4px; overflow-wrap:anywhere; } .help-links { margin-top:2px; } .help-steps { margin:4px 0; padding-left:18px; } .help code { white-space:normal; overflow-wrap:anywhere; }
49
50
  .banner { border:1px solid var(--warn); border-left-width:4px; border-radius:6px; padding:10px 12px; margin:4px 0 12px; background:color-mix(in srgb, var(--warn) 10%, var(--card)); }
50
51
  .banner strong { color:var(--text); display:block; margin-bottom:4px; } .banner .fix code { white-space:normal; overflow-wrap:anywhere; } .note { color:var(--muted); font-size:12px; margin-top:10px; }
52
+ .banner.snapshot { margin:12px 20px 0; border-color:var(--accent); background:color-mix(in srgb, var(--accent) 8%, var(--card)); } .banner.snapshot strong { font-size:15px; }
51
53
  svg.trend rect.high { fill:var(--high); } svg.trend rect.medium { fill:var(--medium); } svg.trend rect.low { fill:var(--low); } svg.trend rect.zero { fill:var(--ok); }
52
54
  svg.trend line { stroke:var(--line); }
53
55
  .repo-title { font-size:12px; color:var(--muted); margin-bottom:6px; word-break:break-all; }
@@ -69,6 +71,8 @@ ${SETUP_STYLE}
69
71
  `;
70
72
  const SCRIPT = `
71
73
  'use strict';
74
+ /** Set in a static snapshot (dashboard --snapshot): the embedded data. Nothing is fetched and nothing runs. */
75
+ const SNAPSHOT = (() => { const n = document.getElementById('prism-snapshot'); try { return n ? JSON.parse(n.textContent) : null; } catch (e) { return null; } })();
72
76
  const SVG = 'http://www.w3.org/2000/svg';
73
77
  function el(tag, attrs, ...kids) {
74
78
  const n = document.createElement(tag);
@@ -187,8 +191,10 @@ function summaryPanel(sum) {
187
191
 
188
192
  const SCAN_LABEL = { ok: ['ok', 'ok'], 'not-scanned': ['warn', 'not scanned'], 'no-verify': ['warn', 'no-verify'], flagged: ['missing', 'secrets found'], none: ['skipped', '-'] };
189
193
 
190
- /** Link to a scan report. Ids come from the server's own listing; encodeURIComponent keeps them literal. */
191
- const reportLink = (id, label) => el('a', { href: '/report/' + encodeURIComponent(id), class: 'rlink' }, label);
194
+ /** Link to a scan report. Ids come from the server's own listing; encodeURIComponent keeps them literal. A snapshot has no report pages. */
195
+ const reportLink = (id, label) => SNAPSHOT
196
+ ? el('span', { title: 'Report pages open from the live dashboard (prism-cli dashboard) or prism-cli codewatch report' }, label)
197
+ : el('a', { href: '/report/' + encodeURIComponent(id), class: 'rlink' }, label);
192
198
 
193
199
  function reviewCell(rv) {
194
200
  if (rv.state === 'not-reviewed') return el('span', { class: 'fix' }, 'not reviewed');
@@ -628,6 +634,11 @@ async function load(fresh) {
628
634
  }
629
635
 
630
636
  async function getJson(path) {
637
+ if (SNAPSHOT) {
638
+ if (path === '/api/data') return SNAPSHOT.data;
639
+ if (path.startsWith('/api/usage')) return { usage: SNAPSHOT.data.usage, cacheHit: true };
640
+ throw new Error('not in this snapshot');
641
+ }
631
642
  const res = await fetch(path, { credentials: 'same-origin', cache: 'no-store' });
632
643
  if (!res.ok) throw new Error('HTTP ' + res.status);
633
644
  return res.json();
@@ -666,16 +677,23 @@ async function render() {
666
677
  PREV = CUR;
667
678
  const c = data.overview.counts;
668
679
  meta.textContent = c.repos + ' repos under ' + txt(data.scope.root_display) + ' · ' + c.codewatch + ' with codewatch' + (data.scope.truncated ? ' (scan capped)' : '')
669
- + ' · updated ' + when(data.generated_at) + (changed.size ? ' · ' + changed.size + ' value' + (changed.size === 1 ? '' : 's') + ' changed (outlined)' : '') + ' · refreshes every minute while visible';
680
+ + (SNAPSHOT ? ' · data from ' + when(data.generated_at) + ' · static snapshot, does not refresh'
681
+ : ' · updated ' + when(data.generated_at) + (changed.size ? ' · ' + changed.size + ' value' + (changed.size === 1 ? '' : 's') + ' changed (outlined)' : '') + ' · refreshes every minute while visible');
670
682
  await renderSetup(data);
671
683
  return data;
672
684
  }
673
685
  ${SETUP_SCRIPT}
674
686
  ${THEME_SCRIPT}
675
- document.getElementById('refresh').addEventListener('click', () => load(true));
676
- setInterval(() => { if (document.visibilityState === 'visible' && Date.now() - lastLoad >= AUTO_REFRESH_MS - 1000) load(false); }, AUTO_REFRESH_MS / 4);
677
- document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && Date.now() - lastLoad >= AUTO_REFRESH_MS) load(false); });
678
- load(false);
687
+ if (SNAPSHOT) {
688
+ const at = document.getElementById('snapshot-at');
689
+ if (at) at.textContent = when(SNAPSHOT.generatedAt);
690
+ load(false);
691
+ } else {
692
+ document.getElementById('refresh').addEventListener('click', () => load(true));
693
+ setInterval(() => { if (document.visibilityState === 'visible' && Date.now() - lastLoad >= AUTO_REFRESH_MS - 1000) load(false); }, AUTO_REFRESH_MS / 4);
694
+ document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'visible' && Date.now() - lastLoad >= AUTO_REFRESH_MS) load(false); });
695
+ load(false);
696
+ }
679
697
  `;
680
698
  export function renderPage(nonce, csrf = '') {
681
699
  // The nonce and the CSRF token are base64/base64url generated server-side; nothing user-derived is interpolated here.
@@ -695,4 +713,45 @@ export function renderPage(nonce, csrf = '') {
695
713
  <main id="setup" hidden></main>
696
714
  <script nonce="${nonce}">${SCRIPT}</script></body></html>`;
697
715
  }
716
+ const sha256 = (s) => `'sha256-${createHash('sha256').update(s, 'utf8').digest('base64')}'`;
717
+ /** JSON for a <script type="application/json"> block: `<` escaped, so the text can never close the element. */
718
+ export function embedJson(v) {
719
+ return JSON.stringify(v).replace(/</g, '\\u003c').replace(/\u2028/g, '\\u2028').replace(/\u2029/g, '\\u2029');
720
+ }
721
+ /** The exact element markers renderSnapshotPage writes, so a test can read the file back without parsing HTML. */
722
+ export const SNAPSHOT_MARKERS = {
723
+ script: '<script>',
724
+ data: '<script type="application/json" id="prism-snapshot">',
725
+ scriptEnd: '</script>',
726
+ style: '<style>',
727
+ styleEnd: '</style>',
728
+ };
729
+ const escapeHtml = (s) => s.replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
730
+ /**
731
+ * The same page as renderPage, as one static file: the data is embedded, the
732
+ * scripts and the style are allowed by hash in a CSP meta tag with
733
+ * default-src 'none' (no fetch, no remote resource), and the Setup page's Run
734
+ * buttons become Copy buttons. No token, no server.
735
+ */
736
+ export function renderSnapshotPage(s) {
737
+ const M = SNAPSHOT_MARKERS;
738
+ const at = escapeHtml(s.generatedAt);
739
+ const csp = `default-src 'none'; script-src ${sha256(THEME_BOOT)} ${sha256(SCRIPT)}; style-src ${sha256(STYLE)}; img-src data:; base-uri 'none'; form-action 'none'`;
740
+ return `<!DOCTYPE html>
741
+ <html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
742
+ <meta http-equiv="Content-Security-Policy" content="${csp}">
743
+ <meta name="referrer" content="no-referrer">
744
+ ${M.script}${THEME_BOOT}${M.scriptEnd}
745
+ <title>PRISM dashboard snapshot ${at}</title>${M.style}${STYLE}${M.styleEnd}</head>
746
+ <body><header><h1>PRISM dashboard</h1>
747
+ <nav class="tabs" aria-label="Views"><a href="#" data-view="dashboard">Dashboard</a><a href="#setup" data-view="setup">Setup</a></nav>
748
+ <div class="theme" role="group" aria-label="Theme"><button type="button" data-theme-set="light">Light</button><button type="button" data-theme-set="dark">Dark</button><button type="button" data-theme-set="system">System</button></div>
749
+ <span class="meta" id="meta"></span></header>
750
+ <div class="banner snapshot" role="note"><strong>Snapshot generated <time id="snapshot-at" datetime="${at}">${at}</time></strong>
751
+ <div class="fix">A static copy written by prism-cli ${escapeHtml(s.version)} dashboard --snapshot. It does not refresh and nothing on it runs: commands are copy only. For the live page run <code>prism-cli dashboard</code>.</div></div>
752
+ <main id="main"></main>
753
+ <main id="setup" hidden></main>
754
+ ${M.data}${embedJson(s)}${M.scriptEnd}
755
+ ${M.script}${SCRIPT}${M.scriptEnd}</body></html>`;
756
+ }
698
757
  //# sourceMappingURL=page.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"page.js","sourceRoot":"","sources":["../../../src/dashboard/page.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,EAAE,YAAY,EAAE,WAAW,EAAE,UAAU,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAEtF,oHAAoH;AACpH,MAAM,UAAU,GAAG,iNAAiN,CAAC;AACrO,MAAM,SAAS,GAAG,kNAAkN,CAAC;AAErO,MAAM,KAAK,GAAG;+BACiB,UAAU;8FACqD,SAAS;iDACtD,SAAS;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EAoDxD,WAAW;CACZ,CAAC;AAEF,MAAM,MAAM,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;EA2lBb,YAAY;EACZ,YAAY;;;;;CAKb,CAAC;AAEF,MAAM,UAAU,UAAU,CAAC,KAAa,EAAE,IAAI,GAAG,EAAE;IACjD,sHAAsH;IACtH,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC;QAAE,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAC;IACtE,OAAO;;mCAE0B,IAAI;iBACtB,KAAK,KAAK,UAAU;8CACS,KAAK,KAAK,KAAK;;;;;;;;iBAQ5C,KAAK,KAAK,MAAM,yBAAyB,CAAC;AAC3D,CAAC"}
1
+ {"version":3,"file":"page.js","sourceRoot":"","sources":["../../../src/dashboard/page.ts"],"names":[],"mappings":"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"}
@@ -12,5 +12,5 @@ export declare const THEME_BOOT = "(function(){try{var t=localStorage.getItem('p
12
12
  /** The header toggle: Light, Dark or System (no data-theme; follows prefers-color-scheme). Saved in localStorage only. */
13
13
  export declare const THEME_SCRIPT = "\nconst THEME_KEY = 'prism-theme';\nfunction storedTheme() { try { const t = localStorage.getItem(THEME_KEY); return t === 'light' || t === 'dark' ? t : 'system'; } catch (e) { return 'system'; } }\nfunction applyTheme(t) {\n const root = document.documentElement;\n if (t === 'light' || t === 'dark') root.setAttribute('data-theme', t); else root.removeAttribute('data-theme');\n for (const b of document.querySelectorAll('[data-theme-set]')) b.setAttribute('aria-pressed', String(b.getAttribute('data-theme-set') === t));\n}\nfunction setTheme(t) {\n const v = t === 'light' || t === 'dark' ? t : 'system';\n try { if (v === 'system') localStorage.removeItem(THEME_KEY); else localStorage.setItem(THEME_KEY, v); } catch (e) { /* storage off: applies for this page only */ }\n applyTheme(v);\n}\nfor (const b of document.querySelectorAll('[data-theme-set]')) b.addEventListener('click', () => setTheme(b.getAttribute('data-theme-set')));\napplyTheme(storedTheme());\n";
14
14
  export declare const SETUP_STYLE = "\n[hidden] { display:none !important; }\nnav.tabs { display:flex; gap:4px; }\nnav.tabs a { color:var(--muted); text-decoration:none; padding:4px 10px; border-radius:6px; border:1px solid transparent; }\nnav.tabs a[aria-current=\"page\"] { color:var(--text); background:var(--bg); border-color:var(--line); font-weight:600; }\na:focus-visible, button:focus-visible, input:focus-visible, summary:focus-visible { outline:2px solid var(--accent); outline-offset:1px; }\n.theme { display:inline-flex; border:1px solid var(--line); border-radius:6px; overflow:hidden; }\n.theme button { border:none; border-radius:0; background:var(--card); color:var(--muted); padding:4px 9px; }\n.theme button[aria-pressed=\"true\"] { background:var(--bg); color:var(--text); font-weight:600; }\n.srow { padding:8px 0; border-top:1px solid var(--line); overflow-wrap:anywhere; } .srow:first-of-type { border-top:none; }\n.srow-head { display:flex; gap:8px; align-items:baseline; flex-wrap:wrap; }\n.srow-ctl { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:6px; }\n.cmdline { display:inline-flex; gap:6px; align-items:center; flex-wrap:wrap; max-width:100%; } .cmdline code { white-space:normal; overflow-wrap:anywhere; }\n.badge { font-size:11px; padding:0 6px; border-radius:8px; border:1px solid var(--line); color:var(--muted); background:var(--raised); white-space:nowrap; }\nbutton.run { background:var(--accent); border-color:var(--accent); color:var(--card); font-weight:600; }\nbutton.run.danger { background:var(--card); border-color:var(--bad); color:var(--bad); }\nbutton:disabled { opacity:.55; cursor:not-allowed; }\ninput.url { font:inherit; padding:4px 8px; border:1px solid var(--line); border-radius:6px; background:var(--bg); color:var(--text); min-width:min(320px,100%); flex:1; }\ninput.url::placeholder { color:var(--muted); }\n.msg { color:var(--bad); font-size:12px; }\nol.steps { margin:4px 0; padding-left:20px; } ol.steps li { padding:3px 0; overflow-wrap:anywhere; }\ndl.kv { display:grid; grid-template-columns:8em minmax(0,1fr); gap:2px 10px; font-size:13px; margin:6px 0; overflow-wrap:anywhere; } dl.kv dt { color:var(--muted); } dl.kv dd { margin:0; }\ndetails.copies > summary { cursor:pointer; color:var(--muted); font-size:12px; padding:4px 0; } details.copies > div { padding:3px 0; }\ndialog.confirm { background:var(--card); color:var(--text); border:1px solid var(--line); border-radius:10px; padding:16px 18px; width:min(640px, 92vw); box-shadow:0 10px 30px rgb(0 0 0 / .35); }\ndialog.confirm::backdrop { background:rgb(0 0 0 / .5); }\ndialog.confirm h2 { margin-bottom:6px; } dialog.confirm p { margin:6px 0; }\ndialog.confirm .actions { display:flex; gap:8px; justify-content:flex-end; margin-top:14px; }\npre.cmd, pre.out { font-family:ui-monospace, SFMono-Regular, Menlo, monospace; font-size:12px; background:var(--bg); color:var(--text); border:1px solid var(--line); border-radius:6px; padding:8px 10px; white-space:pre-wrap; overflow-wrap:anywhere; margin:6px 0; }\npre.out { max-height:420px; overflow:auto; }\nlabel.extra { display:flex; gap:6px; align-items:flex-start; margin-top:10px; color:var(--bad); font-weight:600; }\n";
15
- export declare const SETUP_SCRIPT = "\nconst CSRF = (document.querySelector('meta[name=\"prism-csrf\"]') || {}).content || '';\nlet SETUP = null, POLL = null, DIALOG = null, CHECK = null, URL_DRAFT = '';\nconst OUT = { status: el('div', { class: 'srow-head' }), pre: el('pre', { class: 'out', 'aria-live': 'polite' }) };\nconst ACTION_PANEL = el('section', { class: 'card wide', hidden: '' }, el('h2', {}, 'Action output'), OUT.status, OUT.pre,\n el('p', { class: 'note' }, 'Secrets are masked. Each run is logged to ~/.prism/logs/dashboard-actions.log (time, action, repo, exit code).'));\n\nfunction currentView() { return location.hash === '#setup' ? 'setup' : 'dashboard'; }\nfunction showView() {\n const v = currentView();\n document.getElementById('main').hidden = v !== 'dashboard';\n document.getElementById('setup').hidden = v !== 'setup';\n for (const a of document.querySelectorAll('[data-view]')) {\n if (a.getAttribute('data-view') === v) a.setAttribute('aria-current', 'page'); else a.removeAttribute('aria-current');\n }\n}\nwindow.addEventListener('hashchange', showView);\nshowView();\n\n/** GET, or POST JSON with the CSRF header. referrerPolicy same-origin so the browser sends this page's Origin. */\nasync function api(path, body) {\n const init = body === undefined ? { credentials: 'same-origin', cache: 'no-store' } : {\n method: 'POST', credentials: 'same-origin', cache: 'no-store', referrerPolicy: 'same-origin',\n headers: { 'Content-Type': 'application/json', 'X-Prism-CSRF': CSRF }, body: JSON.stringify(body),\n };\n const res = await fetch(path, init);\n let json = null;\n try { json = await res.json(); } catch (e) { json = null; }\n return { ok: res.ok, status: res.status, json: json || { error: 'HTTP ' + res.status } };\n}\n\nfunction ago(iso) {\n const t = Date.parse(iso);\n if (isNaN(t)) return 'unknown';\n const s = Math.max(0, (Date.now() - t) / 1000);\n for (const [n, w] of [[86400, 'day'], [3600, 'hour'], [60, 'minute']]) {\n if (s >= n) { const k = Math.floor(s / n); return k + ' ' + w + (k === 1 ? '' : 's') + ' ago'; }\n }\n return 'just now';\n}\n\nfunction copyButton(cmd) {\n const b = el('button', { type: 'button', title: 'Copy to the clipboard' }, 'Copy');\n b.addEventListener('click', async () => {\n try { await navigator.clipboard.writeText(cmd); b.textContent = 'Copied'; } catch (e) { b.textContent = 'Select the text to copy'; }\n setTimeout(() => { b.textContent = 'Copy'; }, 1500);\n });\n return el('span', { class: 'cmdline' }, el('code', {}, cmd), b);\n}\nconst copyOnly = (cmd, why) => el('div', { class: 'srow-ctl' }, copyButton(cmd), why ? el('span', { class: 'badge' }, why) : null);\n\n/** Client-side copy of the server's collector URL rule; the server checks again. */\nfunction urlProblem(s) {\n if (!s) return 'enter the collector URL';\n if (s.length > 2048) return 'the URL is longer than 2048 characters';\n let u;\n try { u = new URL(s); } catch (e) { return 'not a valid URL'; }\n if (u.protocol !== 'https:') return 'the URL must use https';\n if (!u.hostname) return 'the URL has no hostname';\n if (u.username || u.password) return 'the URL must not contain a user name or password';\n return null;\n}\n\nconst actionDef = (id) => SETUP && (SETUP.actions || []).find((a) => a.id === id);\n\n/** A Run button for an allowlisted action; null when the action is not available here. */\nfunction runButton(id, o) {\n const a = actionDef(id);\n if (!a || !a.available) return null;\n o = o || {};\n const b = el('button', { type: 'button', class: 'run' + (a.confirm ? ' danger' : '') }, o.label || 'Run');\n b.addEventListener('click', () => {\n let url;\n if (a.url) {\n url = o.urlInput ? o.urlInput.value : '';\n const p = urlProblem(url);\n if (p) { if (o.msg) o.msg.textContent = p; return; }\n if (o.msg) o.msg.textContent = '';\n }\n confirmRun(a, { repo: o.repo, where: o.where, url });\n });\n return b;\n}\n\nfunction confirmRun(a, o) {\n if (!DIALOG) { DIALOG = el('dialog', { class: 'confirm', 'aria-labelledby': 'confirm-title' }); document.body.append(DIALOG); }\n const cmd = a.command + (o.url ? ' ' + o.url : '');\n const run = el('button', { type: 'button', class: 'run' + (a.confirm ? ' danger' : '') }, 'Run');\n const cancel = el('button', { type: 'button' }, 'Cancel');\n const kids = [el('h2', { id: 'confirm-title' }, a.label), el('p', {}, 'This will run:'), el('pre', { class: 'cmd' }, cmd),\n el('p', { class: 'fix' }, 'in ' + txt(o.where || (SETUP && SETUP.cwd) || '')),\n el('p', { class: 'note' }, a.readOnlyAws ? 'Read-only in AWS: describe, get and list calls only. Nothing in AWS is changed.' : 'Changes setup on this machine only. No AWS calls.'),\n el('p', { class: 'note' }, 'It runs without a terminal, so it cannot ask questions; it is stopped after ' + Math.max(1, Math.round(a.timeoutSec / 60)) + ' min.')];\n if (a.confirm) {\n const cb = el('input', { type: 'checkbox' });\n run.disabled = true;\n cb.addEventListener('change', () => { run.disabled = !cb.checked; });\n kids.push(el('label', { class: 'extra' }, cb, 'Yes, remove the scheduled codeburn sync push from this machine'));\n }\n kids.push(el('div', { class: 'actions' }, cancel, run));\n DIALOG.replaceChildren(...kids);\n cancel.addEventListener('click', () => DIALOG.close());\n run.addEventListener('click', () => { DIALOG.close(); startAction(a, o); });\n DIALOG.showModal();\n cancel.focus();\n}\n\nasync function startAction(a, o) {\n const body = { action: a.id };\n if (a.repo) body.repo = o.repo;\n if (a.url) body.url = o.url;\n if (a.confirm) body.confirm = true;\n ACTION_PANEL.hidden = false;\n OUT.status.replaceChildren(pill('warn', 'starting'), el('code', {}, a.command));\n OUT.pre.textContent = '';\n let r;\n try { r = await api('/api/action', body); } catch (e) { r = { ok: false, json: { error: e.message } }; }\n if (!r.ok) { OUT.status.replaceChildren(pill('error', 'refused'), txt(r.json.error)); return; }\n showStatus(r.json);\n ACTION_PANEL.scrollIntoView({ block: 'nearest' });\n poll();\n}\n\nfunction showStatus(s) {\n if (!s || s.state === 'idle') return;\n ACTION_PANEL.hidden = false;\n const p = s.state === 'running' ? pill('warn', 'running') : s.exitCode === 0 ? pill('ok', 'exit 0') : pill('error', s.timedOut ? 'timed out' : 'exit ' + s.exitCode);\n OUT.status.replaceChildren(p, el('code', {}, txt(s.command)), el('span', { class: 'fix' }, (s.repo ? 'in ' + txt(s.repo) + ' \u00B7 ' : '') + 'started ' + when(s.startedAt)));\n OUT.pre.textContent = txt(s.output) || (s.state === 'running' ? '(no output yet)' : '(no output)');\n OUT.pre.scrollTop = OUT.pre.scrollHeight;\n}\n\nfunction poll() {\n clearTimeout(POLL);\n POLL = setTimeout(async () => {\n try {\n const r = await api('/api/action/status');\n if (r.ok) showStatus(r.json);\n if (r.ok && r.json.state !== 'running') { load(); return; }\n } catch (e) { /* retry */ }\n poll();\n }, 1000);\n}\n\nfunction srow(status, label, detail, ...ctl) {\n const c = ctl.filter(Boolean);\n return el('div', { class: 'srow' }, el('div', { class: 'srow-head' }, status, el('b', {}, label)),\n detail ? el('div', { class: 'fix' }, detail) : null, c.length ? el('div', { class: 'srow-ctl' }, c) : null);\n}\n\nfunction machineSection(data) {\n const s = el('section', { class: 'card' }, el('h2', {}, 'This machine'));\n for (const i of data.machine.setup) {\n if (i.id === 'codeburn-sync' || i.id === 'aws-credentials') continue;\n let ctl = null;\n if (i.id === 'codewatch-global' && i.status !== 'ok') {\n const elsewhere = (data.machine.globalHooks || {}).state === 'elsewhere';\n ctl = elsewhere ? copyOnly('prism-cli enable codewatch --global --force', 'replaces another global hooks path')\n : runButton('codewatch-global', { label: 'Enable for every repo' });\n } else if (i.fix) {\n ctl = copyOnly(i.fix, i.id === 'gitleaks' ? 'installs software' : i.id === 'codeburn' ? 'asks before installing' : null);\n }\n s.append(srow(pill(i.status), i.label, i.detail, ctl));\n }\n s.append(el('p', { class: 'note' }, 'AWS credentials: see the AWS section.'));\n return s;\n}\n\nconst CW_PILL = { ok: 'ok', warn: 'warn', missing: 'missing', 'opted-out': 'skipped' };\nfunction reposSection(data) {\n const s = el('section', { class: 'card wide' }, el('h2', {}, 'Repos'));\n const recheck = el('button', { type: 'button' }, 'Re-check status');\n recheck.addEventListener('click', async () => { recheck.disabled = true; try { await api('/api/recheck'); } catch (e) { /* load() reports */ } await load(); });\n s.append(el('div', { class: 'srow-ctl' }, recheck, el('span', { class: 'fix' }, 'Codewatch hooks and Security Agent setup per repo, from local files.')));\n const ids = SETUP.repoIds || {}, cds = SETUP.repoCd || {}, cp = SETUP.copy || {};\n const rowOf = (r) => {\n const cw = el('div', {}, pill(CW_PILL[r.status] || 'missing', r.status === 'opted-out' ? 'opted out' : r.status + (r.status === 'ok' && r.global ? ' (global)' : '')), ' ',\n el('span', { class: 'fix' }, r.status === 'ok' ? 'reviewer ' + txt(r.reviewer || 'none') : txt(r.detail)));\n if ((r.status === 'missing' || r.status === 'warn') && ids[r.path]) cw.append(el('div', { class: 'srow-ctl' }, runButton('codewatch-repo', { repo: ids[r.path], where: r.path, label: 'Enable codewatch' })));\n const sec = r.security || { status: 'missing' };\n const sc = el('div', {}, pill(sec.status === 'missing' ? 'missing' : 'ok', sec.status === 'in-use' ? 'in use' : sec.status));\n const cd = cds[r.path] ? cds[r.path] + ' && ' : '';\n if (sec.status === 'missing') sc.append(copyOnly(sec.fix || cd + cp.enableSecurity, 'AWS'));\n else sc.append(el('details', { class: 'copies' }, el('summary', {}, 'Security Agent commands (AWS, copy only)'),\n [cp.codeReview, cp.threatModel, cp.pentest].map((c) => el('div', {}, copyButton(cd + c)))));\n return [repoName(r), cw, sc];\n };\n const rows = data.overview.setup || [];\n const shown = rows.filter((r) => !r.collapsed), hidden = rows.filter((r) => r.collapsed);\n if (shown.length) s.append(table(['Repo', 'Codewatch', 'Security Agent'], shown.map(rowOf)));\n else if (!hidden.length) s.append(el('p', { class: 'empty' }, 'No git repositories found under ' + txt(data.scope.root_display) + '.'));\n if (hidden.length) s.append(el('details', { class: 'missing-group', 'data-key': 'setup-view:other' },\n el('summary', {}, hidden.length + ' other repo' + (hidden.length === 1 ? '' : 's') + ': not set up'), table(['Repo', 'Codewatch', 'Security Agent'], hidden.map(rowOf))));\n return s;\n}\n\nfunction syncSection(data) {\n const s = el('section', { class: 'card' }, el('h2', {}, 'Usage sync (codeburn)'));\n const item = data.machine.setup.find((i) => i.id === 'codeburn-sync');\n const mode = (data.machine.codeburnSync || {}).mode || 'local';\n if (item) s.append(srow(pill(item.status), item.label, item.detail));\n if (item && item.help) s.append(helpBlock(item.help));\n s.append(el('p', { class: 'fix' }, 'Sync mode: ' + mode));\n s.append(srow(null, 'Local only', 'codeburn usage stays on this machine; no endpoint and no schedule.', runButton('codeburn-local', { label: 'Keep usage local' })));\n const input = el('input', { type: 'url', class: 'url', placeholder: 'collector URL (https, from your platform team)', maxlength: '2048', 'aria-label': 'Collector URL', spellcheck: 'false', autocomplete: 'off' });\n input.value = URL_DRAFT;\n input.addEventListener('input', () => { URL_DRAFT = input.value; });\n const msg = el('span', { class: 'msg', role: 'status' });\n s.append(srow(null, \"Sync to my team's PRISM\", 'Sets up codeburn sync for the collector URL and schedules the push. codeburn opens a browser to sign in.',\n input, runButton('codeburn-team', { label: 'Sync to team', urlInput: input, msg }), msg));\n if (actionDef('codeburn-systemd') && actionDef('codeburn-systemd').available) {\n s.append(srow(null, 'Use a systemd user timer', mode === 'team' ? 'Schedules the sync push with a systemd user timer; your crontab is not touched.' : 'Team mode only: set up sync to your team first.',\n mode === 'team' ? runButton('codeburn-systemd', { label: 'Use systemd timer' }) : null));\n }\n s.append(srow(null, 'Remove the sync schedule', 'Removes the scheduled sync push (cron entry or systemd timer). codeburn and its sync config stay.', runButton('codeburn-remove', { label: 'Remove schedule' })));\n s.append(srow(null, 'Status', 'codeburn, sync and schedule status.', runButton('codeburn-status', { label: 'Show status' })));\n s.append(srow(null, 'Platform team: add a developer', 'Creates a telemetry account in AWS (Cognito), so it is copy only.', copyOnly((SETUP.copy || {}).addDeveloper, 'AWS write')));\n return s;\n}\n\nconst STEP_PILL = { used: ['ok', 'used'], skipped: ['skipped', 'skipped'], 'not-set': ['info', 'not set'], 'n/a': ['skipped', 'n/a'] };\nfunction kv(pairs) { return el('dl', { class: 'kv' }, pairs.filter(Boolean).flatMap(([k, v]) => [el('dt', {}, k), el('dd', {}, v)])); }\n\nfunction checkResult(r) {\n if (!r) return null;\n if (!r.ok) return el('div', {}, pill('error', 'refused'), ' ', txt(r.json.error));\n const j = r.json;\n if (j.ok) {\n const m = j.matches === true ? pill('ok', 'matches the saved account') : j.matches === false ? pill('error', 'does not match saved account ' + txt(j.savedAccount)) : pill('info', 'no saved account');\n return el('div', {}, pill('ok', 'credentials work'), kv([['Account', txt(j.account)], ['Caller', txt(j.arn)], ['Region', txt(j.region || 'not set')], ['Saved', m]]), el('p', { class: 'note' }, txt(j.note)));\n }\n return el('div', {}, pill('error', j.expired ? 'expired or invalid' : 'check failed'), ' ', el('span', {}, txt(j.error)),\n el('div', { class: 'help' }, 'Fixes:', el('ul', { class: 'help-steps' }, (j.fixes || []).map((f) => el('li', {}, /^(aws|prism-cli) /.test(f) ? copyButton(f) : txt(f))))));\n}\n\nfunction awsSection(a) {\n const s = el('section', { class: 'card wide' }, el('h2', {}, 'AWS'));\n s.append(el('p', { class: 'note' }, 'From the environment and local config files only: no AWS call until you press Check now. Only setting names are read from ~/.aws; secret values are never read or shown.'));\n s.append(el('div', { class: 'srow' }, el('div', {}, 'prism-cli would use ', el('b', {}, a.profile ? 'profile ' + txt(a.profile) : txt(a.source)), ' \u00B7 ', el('span', { class: 'badge' }, txt(a.credentialLabel))),\n el('div', { class: 'fix' }, 'chosen by ' + txt(a.source) + ' \u00B7 region ' + (a.region ? txt(a.region.value) + ' (' + txt(a.region.source) + ')' : 'not set'))));\n for (const w of a.warnings || []) s.append(el('div', { class: 'callout warn', role: 'note' }, txt(w)));\n s.append(el('h3', {}, 'Resolution order'), el('ol', { class: 'steps' }, (a.steps || []).map((st) => {\n const p = STEP_PILL[st.state] || ['skipped', st.state];\n return el('li', {}, el('b', {}, txt(st.label)), ' ', pill(p[0], p[1]), ' ', el('span', { class: 'fix' }, txt(st.detail)));\n })));\n s.append(el('h3', {}, 'Saved aws connect'));\n if (a.saved) {\n const v = a.saved;\n s.append(kv([['Account', el('span', {}, txt(v.account), v.alias ? el('span', { class: 'tag' }, txt(v.alias)) : null)], ['Profile', txt(v.profile) + (v.inUse ? '' : ' (not in use)')],\n ['Region', txt(v.region)], ['Verified', v.verifiedAt ? el('span', { title: when(v.verifiedAt) }, ago(v.verifiedAt)) : 'unknown'], ['File', txt(v.path) + ' (' + v.scope + ')'],\n ['Admin / org', 'not stored by aws connect (it prints them when it runs)']]));\n } else s.append(el('p', { class: 'empty-line' }, 'None saved. ', copyButton((SETUP.copy || {}).awsConnect)));\n const btn = el('button', { type: 'button', class: 'run' }, 'Check now');\n const out = el('div', { 'aria-live': 'polite' }, checkResult(CHECK));\n btn.addEventListener('click', async () => {\n btn.disabled = true;\n out.replaceChildren(el('span', { class: 'fix' }, 'checking...'));\n try { CHECK = await api('/api/check-now', {}); } catch (e) { CHECK = { ok: false, json: { error: e.message } }; }\n out.replaceChildren(checkResult(CHECK));\n btn.disabled = false;\n if (CHECK.ok && CHECK.json.updated) load();\n });\n s.append(el('h3', {}, 'Check now'), el('div', { class: 'srow-ctl' }, btn, el('span', { class: 'fix' }, 'One read-only call: aws sts get-caller-identity, as aws connect does it.')), out);\n s.append(el('h3', {}, 'Read-only AWS commands'),\n srow(null, 'Account check', 'Every call is a describe, get or list; the result is saved to .prism/results/.', runButton('aws-check', { label: 'Run aws check' })),\n srow(null, 'Team stack info', 'Reads the PRISM stack outputs (collector URL, user pool).', runButton('team-info', { label: 'Run team info' })));\n const cp = SETUP.copy || {};\n s.append(el('h3', {}, 'Copy only (writes to AWS or needs a terminal)'),\n srow(null, 'Choose and save the AWS profile', 'Asks questions on a terminal.', copyOnly(cp.awsConnect, 'terminal')),\n srow(null, 'Set up the Security Agent in a repo', 'Creates AWS resources.', copyOnly(cp.enableSecurity, 'AWS write')),\n srow(null, 'Add a developer telemetry account', 'Creates a Cognito user.', copyOnly(cp.addDeveloper, 'AWS write')),\n srow(null, 'Security Agent runs', 'Uploads code and starts AWS jobs.', copyOnly(cp.codeReview, 'AWS write'), copyOnly(cp.threatModel, 'AWS write'), copyOnly(cp.pentest, 'AWS write')));\n return s;\n}\n\nasync function renderSetup(data) {\n try { const r = await api('/api/setup'); if (r.ok) SETUP = r.json; } catch (e) { /* keep the last good copy */ }\n const root = document.getElementById('setup');\n const openState = new Map([...root.querySelectorAll('details[data-key]')].map((d) => [d.getAttribute('data-key'), d.open]));\n root.replaceChildren();\n if (!SETUP) { root.append(el('section', { class: 'card wide' }, pill('error'), ' could not load the setup data')); return; }\n root.append(machineSection(data), syncSection(data), reposSection(data), awsSection(SETUP.aws || {}), ACTION_PANEL);\n for (const d of root.querySelectorAll('details[data-key]')) { const k = d.getAttribute('data-key'); if (openState.has(k)) d.open = openState.get(k); }\n}\n// A reload while an action runs picks its output back up.\napi('/api/action/status').then((r) => { if (r.ok && r.json.state === 'running') { showStatus(r.json); poll(); } }).catch(() => {});\n";
15
+ export declare const SETUP_SCRIPT = "\nconst CSRF = (document.querySelector('meta[name=\"prism-csrf\"]') || {}).content || '';\nlet SETUP = null, POLL = null, DIALOG = null, CHECK = null, URL_DRAFT = '';\nconst OUT = { status: el('div', { class: 'srow-head' }), pre: el('pre', { class: 'out', 'aria-live': 'polite' }) };\nconst ACTION_PANEL = el('section', { class: 'card wide', hidden: '' }, el('h2', {}, 'Action output'), OUT.status, OUT.pre,\n el('p', { class: 'note' }, 'Secrets are masked. Each run is logged to ~/.prism/logs/dashboard-actions.log (time, action, repo, exit code).'));\n\nfunction currentView() { return location.hash === '#setup' ? 'setup' : 'dashboard'; }\nfunction showView() {\n const v = currentView();\n document.getElementById('main').hidden = v !== 'dashboard';\n document.getElementById('setup').hidden = v !== 'setup';\n for (const a of document.querySelectorAll('[data-view]')) {\n if (a.getAttribute('data-view') === v) a.setAttribute('aria-current', 'page'); else a.removeAttribute('aria-current');\n }\n}\nwindow.addEventListener('hashchange', showView);\nshowView();\n\n/** GET, or POST JSON with the CSRF header. referrerPolicy same-origin so the browser sends this page's Origin. */\nasync function api(path, body) {\n // A static snapshot answers the two reads it embeds and refuses everything else; it never fetches.\n if (SNAPSHOT) {\n if (body === undefined && path === '/api/setup') return { ok: true, status: 200, json: SNAPSHOT.setup };\n if (body === undefined && path === '/api/action/status') return { ok: true, status: 200, json: { state: 'idle' } };\n return { ok: false, status: 0, json: { error: 'this is a static snapshot: nothing runs from it' } };\n }\n const init = body === undefined ? { credentials: 'same-origin', cache: 'no-store' } : {\n method: 'POST', credentials: 'same-origin', cache: 'no-store', referrerPolicy: 'same-origin',\n headers: { 'Content-Type': 'application/json', 'X-Prism-CSRF': CSRF }, body: JSON.stringify(body),\n };\n const res = await fetch(path, init);\n let json = null;\n try { json = await res.json(); } catch (e) { json = null; }\n return { ok: res.ok, status: res.status, json: json || { error: 'HTTP ' + res.status } };\n}\n\nfunction ago(iso) {\n const t = Date.parse(iso);\n if (isNaN(t)) return 'unknown';\n const s = Math.max(0, (Date.now() - t) / 1000);\n for (const [n, w] of [[86400, 'day'], [3600, 'hour'], [60, 'minute']]) {\n if (s >= n) { const k = Math.floor(s / n); return k + ' ' + w + (k === 1 ? '' : 's') + ' ago'; }\n }\n return 'just now';\n}\n\nfunction copyButton(cmd) {\n const b = el('button', { type: 'button', title: 'Copy to the clipboard' }, 'Copy');\n b.addEventListener('click', async () => {\n try { await navigator.clipboard.writeText(cmd); b.textContent = 'Copied'; } catch (e) { b.textContent = 'Select the text to copy'; }\n setTimeout(() => { b.textContent = 'Copy'; }, 1500);\n });\n return el('span', { class: 'cmdline' }, el('code', {}, cmd), b);\n}\nconst copyOnly = (cmd, why) => el('div', { class: 'srow-ctl' }, copyButton(cmd), why ? el('span', { class: 'badge' }, why) : null);\n\n/** Client-side copy of the server's collector URL rule; the server checks again. */\nfunction urlProblem(s) {\n if (!s) return 'enter the collector URL';\n if (s.length > 2048) return 'the URL is longer than 2048 characters';\n let u;\n try { u = new URL(s); } catch (e) { return 'not a valid URL'; }\n if (u.protocol !== 'https:') return 'the URL must use https';\n if (!u.hostname) return 'the URL has no hostname';\n if (u.username || u.password) return 'the URL must not contain a user name or password';\n return null;\n}\n\nconst actionDef = (id) => SETUP && (SETUP.actions || []).find((a) => a.id === id);\n\n/** A Run button for an allowlisted action; null when the action is not available here. */\nfunction runButton(id, o) {\n const a = actionDef(id);\n if (!a || !a.available) return null;\n o = o || {};\n if (SNAPSHOT) {\n const cd = o.where && SETUP.repoCd && SETUP.repoCd[o.where];\n return copyOnly((cd ? cd + ' && ' : '') + a.command + (a.url ? ' <collector-url>' : ''), 'snapshot: copy only');\n }\n const b = el('button', { type: 'button', class: 'run' + (a.confirm ? ' danger' : '') }, o.label || 'Run');\n b.addEventListener('click', () => {\n let url;\n if (a.url) {\n url = o.urlInput ? o.urlInput.value : '';\n const p = urlProblem(url);\n if (p) { if (o.msg) o.msg.textContent = p; return; }\n if (o.msg) o.msg.textContent = '';\n }\n confirmRun(a, { repo: o.repo, where: o.where, url });\n });\n return b;\n}\n\nfunction confirmRun(a, o) {\n if (!DIALOG) { DIALOG = el('dialog', { class: 'confirm', 'aria-labelledby': 'confirm-title' }); document.body.append(DIALOG); }\n const cmd = a.command + (o.url ? ' ' + o.url : '');\n const run = el('button', { type: 'button', class: 'run' + (a.confirm ? ' danger' : '') }, 'Run');\n const cancel = el('button', { type: 'button' }, 'Cancel');\n const kids = [el('h2', { id: 'confirm-title' }, a.label), el('p', {}, 'This will run:'), el('pre', { class: 'cmd' }, cmd),\n el('p', { class: 'fix' }, 'in ' + txt(o.where || (SETUP && SETUP.cwd) || '')),\n el('p', { class: 'note' }, a.readOnlyAws ? 'Read-only in AWS: describe, get and list calls only. Nothing in AWS is changed.' : 'Changes setup on this machine only. No AWS calls.'),\n el('p', { class: 'note' }, 'It runs without a terminal, so it cannot ask questions; it is stopped after ' + Math.max(1, Math.round(a.timeoutSec / 60)) + ' min.')];\n if (a.confirm) {\n const cb = el('input', { type: 'checkbox' });\n run.disabled = true;\n cb.addEventListener('change', () => { run.disabled = !cb.checked; });\n kids.push(el('label', { class: 'extra' }, cb, 'Yes, remove the scheduled codeburn sync push from this machine'));\n }\n kids.push(el('div', { class: 'actions' }, cancel, run));\n DIALOG.replaceChildren(...kids);\n cancel.addEventListener('click', () => DIALOG.close());\n run.addEventListener('click', () => { DIALOG.close(); startAction(a, o); });\n DIALOG.showModal();\n cancel.focus();\n}\n\nasync function startAction(a, o) {\n const body = { action: a.id };\n if (a.repo) body.repo = o.repo;\n if (a.url) body.url = o.url;\n if (a.confirm) body.confirm = true;\n ACTION_PANEL.hidden = false;\n OUT.status.replaceChildren(pill('warn', 'starting'), el('code', {}, a.command));\n OUT.pre.textContent = '';\n let r;\n try { r = await api('/api/action', body); } catch (e) { r = { ok: false, json: { error: e.message } }; }\n if (!r.ok) { OUT.status.replaceChildren(pill('error', 'refused'), txt(r.json.error)); return; }\n showStatus(r.json);\n ACTION_PANEL.scrollIntoView({ block: 'nearest' });\n poll();\n}\n\nfunction showStatus(s) {\n if (!s || s.state === 'idle') return;\n ACTION_PANEL.hidden = false;\n const p = s.state === 'running' ? pill('warn', 'running') : s.exitCode === 0 ? pill('ok', 'exit 0') : pill('error', s.timedOut ? 'timed out' : 'exit ' + s.exitCode);\n OUT.status.replaceChildren(p, el('code', {}, txt(s.command)), el('span', { class: 'fix' }, (s.repo ? 'in ' + txt(s.repo) + ' \u00B7 ' : '') + 'started ' + when(s.startedAt)));\n OUT.pre.textContent = txt(s.output) || (s.state === 'running' ? '(no output yet)' : '(no output)');\n OUT.pre.scrollTop = OUT.pre.scrollHeight;\n}\n\nfunction poll() {\n clearTimeout(POLL);\n POLL = setTimeout(async () => {\n try {\n const r = await api('/api/action/status');\n if (r.ok) showStatus(r.json);\n if (r.ok && r.json.state !== 'running') { load(); return; }\n } catch (e) { /* retry */ }\n poll();\n }, 1000);\n}\n\nfunction srow(status, label, detail, ...ctl) {\n const c = ctl.filter(Boolean);\n return el('div', { class: 'srow' }, el('div', { class: 'srow-head' }, status, el('b', {}, label)),\n detail ? el('div', { class: 'fix' }, detail) : null, c.length ? el('div', { class: 'srow-ctl' }, c) : null);\n}\n\nfunction machineSection(data) {\n const s = el('section', { class: 'card' }, el('h2', {}, 'This machine'));\n for (const i of data.machine.setup) {\n if (i.id === 'codeburn-sync' || i.id === 'aws-credentials') continue;\n let ctl = null;\n if (i.id === 'codewatch-global' && i.status !== 'ok') {\n const elsewhere = (data.machine.globalHooks || {}).state === 'elsewhere';\n ctl = elsewhere ? copyOnly('prism-cli enable codewatch --global --force', 'replaces another global hooks path')\n : runButton('codewatch-global', { label: 'Enable for every repo' });\n } else if (i.fix) {\n ctl = copyOnly(i.fix, i.id === 'gitleaks' ? 'installs software' : i.id === 'codeburn' ? 'asks before installing' : null);\n }\n s.append(srow(pill(i.status), i.label, i.detail, ctl));\n }\n s.append(el('p', { class: 'note' }, 'AWS credentials: see the AWS section.'));\n return s;\n}\n\nconst CW_PILL = { ok: 'ok', warn: 'warn', missing: 'missing', 'opted-out': 'skipped' };\nfunction reposSection(data) {\n const s = el('section', { class: 'card wide' }, el('h2', {}, 'Repos'));\n const recheck = el('button', { type: 'button', disabled: SNAPSHOT ? '' : null, title: SNAPSHOT ? 'Not in a static snapshot' : null }, 'Re-check status');\n recheck.addEventListener('click', async () => { recheck.disabled = true; try { await api('/api/recheck'); } catch (e) { /* load() reports */ } await load(); });\n s.append(el('div', { class: 'srow-ctl' }, recheck, el('span', { class: 'fix' }, 'Codewatch hooks and Security Agent setup per repo, from local files.')));\n const ids = SETUP.repoIds || {}, cds = SETUP.repoCd || {}, cp = SETUP.copy || {};\n const rowOf = (r) => {\n const cw = el('div', {}, pill(CW_PILL[r.status] || 'missing', r.status === 'opted-out' ? 'opted out' : r.status + (r.status === 'ok' && r.global ? ' (global)' : '')), ' ',\n el('span', { class: 'fix' }, r.status === 'ok' ? 'reviewer ' + txt(r.reviewer || 'none') : txt(r.detail)));\n if ((r.status === 'missing' || r.status === 'warn') && ids[r.path]) cw.append(el('div', { class: 'srow-ctl' }, runButton('codewatch-repo', { repo: ids[r.path], where: r.path, label: 'Enable codewatch' })));\n const sec = r.security || { status: 'missing' };\n const sc = el('div', {}, pill(sec.status === 'missing' ? 'missing' : 'ok', sec.status === 'in-use' ? 'in use' : sec.status));\n const cd = cds[r.path] ? cds[r.path] + ' && ' : '';\n if (sec.status === 'missing') sc.append(copyOnly(sec.fix || cd + cp.enableSecurity, 'AWS'));\n else sc.append(el('details', { class: 'copies' }, el('summary', {}, 'Security Agent commands (AWS, copy only)'),\n [cp.codeReview, cp.threatModel, cp.pentest].map((c) => el('div', {}, copyButton(cd + c)))));\n return [repoName(r), cw, sc];\n };\n const rows = data.overview.setup || [];\n const shown = rows.filter((r) => !r.collapsed), hidden = rows.filter((r) => r.collapsed);\n if (shown.length) s.append(table(['Repo', 'Codewatch', 'Security Agent'], shown.map(rowOf)));\n else if (!hidden.length) s.append(el('p', { class: 'empty' }, 'No git repositories found under ' + txt(data.scope.root_display) + '.'));\n if (hidden.length) s.append(el('details', { class: 'missing-group', 'data-key': 'setup-view:other' },\n el('summary', {}, hidden.length + ' other repo' + (hidden.length === 1 ? '' : 's') + ': not set up'), table(['Repo', 'Codewatch', 'Security Agent'], hidden.map(rowOf))));\n return s;\n}\n\nfunction syncSection(data) {\n const s = el('section', { class: 'card' }, el('h2', {}, 'Usage sync (codeburn)'));\n const item = data.machine.setup.find((i) => i.id === 'codeburn-sync');\n const mode = (data.machine.codeburnSync || {}).mode || 'local';\n if (item) s.append(srow(pill(item.status), item.label, item.detail));\n if (item && item.help) s.append(helpBlock(item.help));\n s.append(el('p', { class: 'fix' }, 'Sync mode: ' + mode));\n s.append(srow(null, 'Local only', 'codeburn usage stays on this machine; no endpoint and no schedule.', runButton('codeburn-local', { label: 'Keep usage local' })));\n const input = el('input', { type: 'url', class: 'url', placeholder: 'collector URL (https, from your platform team)', maxlength: '2048', 'aria-label': 'Collector URL', spellcheck: 'false', autocomplete: 'off' });\n input.value = URL_DRAFT;\n input.addEventListener('input', () => { URL_DRAFT = input.value; });\n const msg = el('span', { class: 'msg', role: 'status' });\n s.append(srow(null, \"Sync to my team's PRISM\", 'Sets up codeburn sync for the collector URL and schedules the push. codeburn opens a browser to sign in.',\n input, runButton('codeburn-team', { label: 'Sync to team', urlInput: input, msg }), msg));\n if (actionDef('codeburn-systemd') && actionDef('codeburn-systemd').available) {\n s.append(srow(null, 'Use a systemd user timer', mode === 'team' ? 'Schedules the sync push with a systemd user timer; your crontab is not touched.' : 'Team mode only: set up sync to your team first.',\n mode === 'team' ? runButton('codeburn-systemd', { label: 'Use systemd timer' }) : null));\n }\n s.append(srow(null, 'Remove the sync schedule', 'Removes the scheduled sync push (cron entry or systemd timer). codeburn and its sync config stay.', runButton('codeburn-remove', { label: 'Remove schedule' })));\n s.append(srow(null, 'Status', 'codeburn, sync and schedule status.', runButton('codeburn-status', { label: 'Show status' })));\n s.append(srow(null, 'Platform team: add a developer', 'Creates a telemetry account in AWS (Cognito), so it is copy only.', copyOnly((SETUP.copy || {}).addDeveloper, 'AWS write')));\n return s;\n}\n\nconst STEP_PILL = { used: ['ok', 'used'], skipped: ['skipped', 'skipped'], 'not-set': ['info', 'not set'], 'n/a': ['skipped', 'n/a'] };\nfunction kv(pairs) { return el('dl', { class: 'kv' }, pairs.filter(Boolean).flatMap(([k, v]) => [el('dt', {}, k), el('dd', {}, v)])); }\n\nfunction checkResult(r) {\n if (!r) return null;\n if (!r.ok) return el('div', {}, pill('error', 'refused'), ' ', txt(r.json.error));\n const j = r.json;\n if (j.ok) {\n const m = j.matches === true ? pill('ok', 'matches the saved account') : j.matches === false ? pill('error', 'does not match saved account ' + txt(j.savedAccount)) : pill('info', 'no saved account');\n return el('div', {}, pill('ok', 'credentials work'), kv([['Account', txt(j.account)], ['Caller', txt(j.arn)], ['Region', txt(j.region || 'not set')], ['Saved', m]]), el('p', { class: 'note' }, txt(j.note)));\n }\n return el('div', {}, pill('error', j.expired ? 'expired or invalid' : 'check failed'), ' ', el('span', {}, txt(j.error)),\n el('div', { class: 'help' }, 'Fixes:', el('ul', { class: 'help-steps' }, (j.fixes || []).map((f) => el('li', {}, /^(aws|prism-cli) /.test(f) ? copyButton(f) : txt(f))))));\n}\n\nfunction awsSection(a) {\n const s = el('section', { class: 'card wide' }, el('h2', {}, 'AWS'));\n s.append(el('p', { class: 'note' }, 'From the environment and local config files only: no AWS call until you press Check now. Only setting names are read from ~/.aws; secret values are never read or shown.'));\n s.append(el('div', { class: 'srow' }, el('div', {}, 'prism-cli would use ', el('b', {}, a.profile ? 'profile ' + txt(a.profile) : txt(a.source)), ' \u00B7 ', el('span', { class: 'badge' }, txt(a.credentialLabel))),\n el('div', { class: 'fix' }, 'chosen by ' + txt(a.source) + ' \u00B7 region ' + (a.region ? txt(a.region.value) + ' (' + txt(a.region.source) + ')' : 'not set'))));\n for (const w of a.warnings || []) s.append(el('div', { class: 'callout warn', role: 'note' }, txt(w)));\n s.append(el('h3', {}, 'Resolution order'), el('ol', { class: 'steps' }, (a.steps || []).map((st) => {\n const p = STEP_PILL[st.state] || ['skipped', st.state];\n return el('li', {}, el('b', {}, txt(st.label)), ' ', pill(p[0], p[1]), ' ', el('span', { class: 'fix' }, txt(st.detail)));\n })));\n s.append(el('h3', {}, 'Saved aws connect'));\n if (a.saved) {\n const v = a.saved;\n s.append(kv([['Account', el('span', {}, txt(v.account), v.alias ? el('span', { class: 'tag' }, txt(v.alias)) : null)], ['Profile', txt(v.profile) + (v.inUse ? '' : ' (not in use)')],\n ['Region', txt(v.region)], ['Verified', v.verifiedAt ? el('span', { title: when(v.verifiedAt) }, ago(v.verifiedAt)) : 'unknown'], ['File', txt(v.path) + ' (' + v.scope + ')'],\n ['Admin / org', 'not stored by aws connect (it prints them when it runs)']]));\n } else s.append(el('p', { class: 'empty-line' }, 'None saved. ', copyButton((SETUP.copy || {}).awsConnect)));\n const btn = el('button', { type: 'button', class: 'run' }, 'Check now');\n const out = el('div', { 'aria-live': 'polite' }, checkResult(CHECK));\n btn.addEventListener('click', async () => {\n btn.disabled = true;\n out.replaceChildren(el('span', { class: 'fix' }, 'checking...'));\n try { CHECK = await api('/api/check-now', {}); } catch (e) { CHECK = { ok: false, json: { error: e.message } }; }\n out.replaceChildren(checkResult(CHECK));\n btn.disabled = false;\n if (CHECK.ok && CHECK.json.updated) load();\n });\n if (SNAPSHOT) btn.disabled = true;\n s.append(el('h3', {}, 'Check now'), el('div', { class: 'srow-ctl' }, btn, el('span', { class: 'fix' }, SNAPSHOT\n ? 'Disabled in a static snapshot, which makes no calls. Run prism-cli dashboard for a live check, or run aws sts get-caller-identity yourself.'\n : 'One read-only call: aws sts get-caller-identity, as aws connect does it.')), out);\n s.append(el('h3', {}, 'Read-only AWS commands'),\n srow(null, 'Account check', 'Every call is a describe, get or list; the result is saved to .prism/results/.', runButton('aws-check', { label: 'Run aws check' })),\n srow(null, 'Team stack info', 'Reads the PRISM stack outputs (collector URL, user pool).', runButton('team-info', { label: 'Run team info' })));\n const cp = SETUP.copy || {};\n s.append(el('h3', {}, 'Copy only (writes to AWS or needs a terminal)'),\n srow(null, 'Choose and save the AWS profile', 'Asks questions on a terminal.', copyOnly(cp.awsConnect, 'terminal')),\n srow(null, 'Set up the Security Agent in a repo', 'Creates AWS resources.', copyOnly(cp.enableSecurity, 'AWS write')),\n srow(null, 'Add a developer telemetry account', 'Creates a Cognito user.', copyOnly(cp.addDeveloper, 'AWS write')),\n srow(null, 'Security Agent runs', 'Uploads code and starts AWS jobs.', copyOnly(cp.codeReview, 'AWS write'), copyOnly(cp.threatModel, 'AWS write'), copyOnly(cp.pentest, 'AWS write')));\n return s;\n}\n\nasync function renderSetup(data) {\n try { const r = await api('/api/setup'); if (r.ok) SETUP = r.json; } catch (e) { /* keep the last good copy */ }\n const root = document.getElementById('setup');\n const openState = new Map([...root.querySelectorAll('details[data-key]')].map((d) => [d.getAttribute('data-key'), d.open]));\n root.replaceChildren();\n if (!SETUP) { root.append(el('section', { class: 'card wide' }, pill('error'), ' could not load the setup data')); return; }\n root.append(machineSection(data), syncSection(data), reposSection(data), awsSection(SETUP.aws || {}), ACTION_PANEL);\n for (const d of root.querySelectorAll('details[data-key]')) { const k = d.getAttribute('data-key'); if (openState.has(k)) d.open = openState.get(k); }\n}\n// A reload while an action runs picks its output back up.\napi('/api/action/status').then((r) => { if (r.ok && r.json.state === 'running') { showStatus(r.json); poll(); } }).catch(() => {});\n";
16
16
  //# sourceMappingURL=setup-page.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"setup-page.d.ts","sourceRoot":"","sources":["../../../src/dashboard/setup-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uGAAuG;AACvG,eAAO,MAAM,UAAU,gKAAgK,CAAC;AAExL,0HAA0H;AAC1H,eAAO,MAAM,YAAY,m9BAexB,CAAC;AAEF,eAAO,MAAM,WAAW,inGA8BvB,CAAC;AAEF,eAAO,MAAM,YAAY,qpkBA8RxB,CAAC"}
1
+ {"version":3,"file":"setup-page.d.ts","sourceRoot":"","sources":["../../../src/dashboard/setup-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uGAAuG;AACvG,eAAO,MAAM,UAAU,gKAAgK,CAAC;AAExL,0HAA0H;AAC1H,eAAO,MAAM,YAAY,m9BAexB,CAAC;AAEF,eAAO,MAAM,WAAW,inGA8BvB,CAAC;AAEF,eAAO,MAAM,YAAY,qlmBA2SxB,CAAC"}
@@ -78,6 +78,12 @@ showView();
78
78
 
79
79
  /** GET, or POST JSON with the CSRF header. referrerPolicy same-origin so the browser sends this page's Origin. */
80
80
  async function api(path, body) {
81
+ // A static snapshot answers the two reads it embeds and refuses everything else; it never fetches.
82
+ if (SNAPSHOT) {
83
+ if (body === undefined && path === '/api/setup') return { ok: true, status: 200, json: SNAPSHOT.setup };
84
+ if (body === undefined && path === '/api/action/status') return { ok: true, status: 200, json: { state: 'idle' } };
85
+ return { ok: false, status: 0, json: { error: 'this is a static snapshot: nothing runs from it' } };
86
+ }
81
87
  const init = body === undefined ? { credentials: 'same-origin', cache: 'no-store' } : {
82
88
  method: 'POST', credentials: 'same-origin', cache: 'no-store', referrerPolicy: 'same-origin',
83
89
  headers: { 'Content-Type': 'application/json', 'X-Prism-CSRF': CSRF }, body: JSON.stringify(body),
@@ -127,6 +133,10 @@ function runButton(id, o) {
127
133
  const a = actionDef(id);
128
134
  if (!a || !a.available) return null;
129
135
  o = o || {};
136
+ if (SNAPSHOT) {
137
+ const cd = o.where && SETUP.repoCd && SETUP.repoCd[o.where];
138
+ return copyOnly((cd ? cd + ' && ' : '') + a.command + (a.url ? ' <collector-url>' : ''), 'snapshot: copy only');
139
+ }
130
140
  const b = el('button', { type: 'button', class: 'run' + (a.confirm ? ' danger' : '') }, o.label || 'Run');
131
141
  b.addEventListener('click', () => {
132
142
  let url;
@@ -228,7 +238,7 @@ function machineSection(data) {
228
238
  const CW_PILL = { ok: 'ok', warn: 'warn', missing: 'missing', 'opted-out': 'skipped' };
229
239
  function reposSection(data) {
230
240
  const s = el('section', { class: 'card wide' }, el('h2', {}, 'Repos'));
231
- const recheck = el('button', { type: 'button' }, 'Re-check status');
241
+ const recheck = el('button', { type: 'button', disabled: SNAPSHOT ? '' : null, title: SNAPSHOT ? 'Not in a static snapshot' : null }, 'Re-check status');
232
242
  recheck.addEventListener('click', async () => { recheck.disabled = true; try { await api('/api/recheck'); } catch (e) { /* load() reports */ } await load(); });
233
243
  s.append(el('div', { class: 'srow-ctl' }, recheck, el('span', { class: 'fix' }, 'Codewatch hooks and Security Agent setup per repo, from local files.')));
234
244
  const ids = SETUP.repoIds || {}, cds = SETUP.repoCd || {}, cp = SETUP.copy || {};
@@ -319,7 +329,10 @@ function awsSection(a) {
319
329
  btn.disabled = false;
320
330
  if (CHECK.ok && CHECK.json.updated) load();
321
331
  });
322
- s.append(el('h3', {}, 'Check now'), el('div', { class: 'srow-ctl' }, btn, el('span', { class: 'fix' }, 'One read-only call: aws sts get-caller-identity, as aws connect does it.')), out);
332
+ if (SNAPSHOT) btn.disabled = true;
333
+ s.append(el('h3', {}, 'Check now'), el('div', { class: 'srow-ctl' }, btn, el('span', { class: 'fix' }, SNAPSHOT
334
+ ? 'Disabled in a static snapshot, which makes no calls. Run prism-cli dashboard for a live check, or run aws sts get-caller-identity yourself.'
335
+ : 'One read-only call: aws sts get-caller-identity, as aws connect does it.')), out);
323
336
  s.append(el('h3', {}, 'Read-only AWS commands'),
324
337
  srow(null, 'Account check', 'Every call is a describe, get or list; the result is saved to .prism/results/.', runButton('aws-check', { label: 'Run aws check' })),
325
338
  srow(null, 'Team stack info', 'Reads the PRISM stack outputs (collector URL, user pool).', runButton('team-info', { label: 'Run team info' })));
@@ -1 +1 @@
1
- {"version":3,"file":"setup-page.js","sourceRoot":"","sources":["../../../src/dashboard/setup-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uGAAuG;AACvG,MAAM,CAAC,MAAM,UAAU,GAAG,6JAA6J,CAAC;AAExL,0HAA0H;AAC1H,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;CAe3B,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8B1B,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8R3B,CAAC"}
1
+ {"version":3,"file":"setup-page.js","sourceRoot":"","sources":["../../../src/dashboard/setup-page.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,uGAAuG;AACvG,MAAM,CAAC,MAAM,UAAU,GAAG,6JAA6J,CAAC;AAExL,0HAA0H;AAC1H,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;CAe3B,CAAC;AAEF,MAAM,CAAC,MAAM,WAAW,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA8B1B,CAAC;AAEF,MAAM,CAAC,MAAM,YAAY,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CA2S3B,CAAC"}
@@ -0,0 +1,31 @@
1
+ /**
2
+ * `prism-cli dashboard --snapshot <file>` and the MCP tool prism_snapshot: the
3
+ * dashboard written as one static HTML file (renderSnapshotPage), with the
4
+ * same /api/data payload and /api/setup answer the live page fetches. No
5
+ * server, no port, no token. The file is written with mode 0600 because,
6
+ * like the live page, it shows local paths and the saved AWS account.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ import { type PayloadOptions } from './payload.js';
10
+ export interface SnapshotOptions {
11
+ cwd: string;
12
+ home: string;
13
+ root?: string;
14
+ version: string;
15
+ usage: NonNullable<PayloadOptions['usage']>;
16
+ pathEnv?: string;
17
+ machineDeps?: PayloadOptions['machineDeps'];
18
+ env?: NodeJS.ProcessEnv;
19
+ now?: () => Date;
20
+ }
21
+ export declare function buildSnapshotHtml(o: SnapshotOptions): Promise<string>;
22
+ /** Writes (or overwrites) `file` with mode 0600; returns its size in bytes. */
23
+ export declare function writeSnapshot(file: string, html: string): number;
24
+ /** True when a browser can be opened here: never over SSH; on Linux only with a display. */
25
+ export declare function canOpen(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): boolean;
26
+ /**
27
+ * Opens the URL (or file) in the default browser. argv only, detached; failure
28
+ * is non-fatal and goes to stderr, never stdout (the MCP server's protocol stream).
29
+ */
30
+ export declare function openBrowser(url: string, spawnFn?: typeof spawn, platform?: NodeJS.Platform): void;
31
+ //# sourceMappingURL=snapshot.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.d.ts","sourceRoot":"","sources":["../../../src/dashboard/snapshot.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAE3C,OAAO,EAAgB,KAAK,cAAc,EAAE,MAAM,cAAc,CAAC;AAOjE,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,WAAW,CAAC,cAAc,CAAC,OAAO,CAAC,CAAC,CAAC;IAC5C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,cAAc,CAAC,aAAa,CAAC,CAAC;IAC5C,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,wBAAsB,iBAAiB,CAAC,CAAC,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,CAgB3E;AAED,+EAA+E;AAC/E,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKhE;AAED,4FAA4F;AAC5F,wBAAgB,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,EAAE,QAAQ,EAAE,MAAM,CAAC,QAAQ,GAAG,OAAO,CAIlF;AAED;;;GAGG;AACH,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,OAAO,KAAa,EAAE,QAAQ,GAAE,MAAM,CAAC,QAA2B,GAAG,IAAI,CAW1H"}
@@ -0,0 +1,65 @@
1
+ /**
2
+ * `prism-cli dashboard --snapshot <file>` and the MCP tool prism_snapshot: the
3
+ * dashboard written as one static HTML file (renderSnapshotPage), with the
4
+ * same /api/data payload and /api/setup answer the live page fetches. No
5
+ * server, no port, no token. The file is written with mode 0600 because,
6
+ * like the live page, it shows local paths and the saved AWS account.
7
+ */
8
+ import { spawn } from 'node:child_process';
9
+ import { chmodSync, writeFileSync } from 'node:fs';
10
+ import { buildPayload } from './payload.js';
11
+ import { renderSnapshotPage } from './page.js';
12
+ import { ActionRunner, cliEntry, repoId } from './actions.js';
13
+ import { setupApi } from './setup-api.js';
14
+ import { repoRootOf } from '../aws/connection.js';
15
+ import { systemdUserAvailable } from '../utils/otel-sync-schedule.js';
16
+ export async function buildSnapshotHtml(o) {
17
+ const data = await buildPayload({ cwd: o.cwd, root: o.root, home: o.home, pathEnv: o.pathEnv, machineDeps: o.machineDeps, usage: o.usage });
18
+ const repos = data.repos.map((r) => ({ id: repoId(r.path), name: r.name, path: r.path }));
19
+ // Only the action catalog is read (labels, commands, availability); nothing is started.
20
+ const runner = new ActionRunner({
21
+ execPath: process.execPath, entry: cliEntry(), cwd: o.cwd, home: o.home, platform: process.platform,
22
+ repos: async () => repos, systemdAvailable: () => systemdUserAvailable(),
23
+ });
24
+ const api = setupApi({
25
+ runner, cwd: o.cwd,
26
+ repoPaths: async () => repos.map((r) => r.path),
27
+ identity: { env: o.env ?? process.env, home: o.home, repoRoot: repoRootOf(o.cwd) },
28
+ refresh: async () => data,
29
+ });
30
+ const setup = await api.getApi['/api/setup']();
31
+ return renderSnapshotPage({ data, setup, generatedAt: (o.now ?? (() => new Date()))().toISOString(), version: o.version });
32
+ }
33
+ /** Writes (or overwrites) `file` with mode 0600; returns its size in bytes. */
34
+ export function writeSnapshot(file, html) {
35
+ writeFileSync(file, html, { mode: 0o600 });
36
+ // An existing file keeps its old mode through writeFileSync.
37
+ chmodSync(file, 0o600);
38
+ return Buffer.byteLength(html, 'utf8');
39
+ }
40
+ /** True when a browser can be opened here: never over SSH; on Linux only with a display. */
41
+ export function canOpen(env, platform) {
42
+ if (env.SSH_CONNECTION || env.SSH_TTY)
43
+ return false;
44
+ if (platform === 'darwin' || platform === 'win32')
45
+ return true;
46
+ return !!(env.DISPLAY || env.WAYLAND_DISPLAY);
47
+ }
48
+ /**
49
+ * Opens the URL (or file) in the default browser. argv only, detached; failure
50
+ * is non-fatal and goes to stderr, never stdout (the MCP server's protocol stream).
51
+ */
52
+ export function openBrowser(url, spawnFn = spawn, platform = process.platform) {
53
+ const [cmd, args] = platform === 'darwin' ? ['open', [url]]
54
+ : platform === 'win32' ? ['rundll32', ['url.dll,FileProtocolHandler', url]]
55
+ : ['xdg-open', [url]];
56
+ try {
57
+ const child = spawnFn(cmd, args, { stdio: 'ignore', detached: true });
58
+ child.on('error', () => process.stderr.write(' (could not open a browser; open the URL above)\n'));
59
+ child.unref();
60
+ }
61
+ catch {
62
+ process.stderr.write(' (could not open a browser; open the URL above)\n');
63
+ }
64
+ }
65
+ //# sourceMappingURL=snapshot.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot.js","sourceRoot":"","sources":["../../../src/dashboard/snapshot.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,83 @@
1
+ /**
2
+ * The live dashboard behind the MCP tools prism_dashboard and
3
+ * prism_dashboard_stop.
4
+ *
5
+ * prism_dashboard starts `prism-cli dashboard` as a child of the MCP server:
6
+ * argv with shell:false, not detached, with `--exit-on-stdin-close`. The
7
+ * server keeps all its own protections (bound to 127.0.0.1, a constant; a
8
+ * per-run token in the URL; Origin and CSRF checks on POST). This module adds
9
+ * no bind option and no other way in.
10
+ *
11
+ * The child ends with the MCP server, three ways: the server kills it when
12
+ * its stdin closes or it gets SIGINT/SIGTERM; a process 'exit' hook kills it;
13
+ * and the child itself exits when its stdin pipe closes, which also covers
14
+ * the MCP server being killed outright.
15
+ *
16
+ * One instance per user: ~/.prism/run/dashboard.json (dir 0700, file 0600)
17
+ * holds the pid, port and URL. The URL holds the token, so prism-cli writes it
18
+ * only there and returns it to the client; it never logs it. The client's own
19
+ * transcript does hold it (the user asked to see it); the token is useless once
20
+ * the server stops, and the server is reachable only from this machine. A state file whose
21
+ * pid is dead, or whose port does not answer as a PRISM dashboard, is ignored.
22
+ */
23
+ import { type ChildProcess, type SpawnOptions } from 'node:child_process';
24
+ export declare const RUN_SCHEMA = "prism.dashboard-run.v1";
25
+ export declare const START_TIMEOUT_MS = 15000;
26
+ export declare const STOP_TIMEOUT_MS = 3000;
27
+ export declare const PROBE_TIMEOUT_MS = 1000;
28
+ /** The URL line `prism-cli dashboard` prints; only a 127.0.0.1 URL is accepted. */
29
+ export declare const URL_LINE: RegExp;
30
+ export declare const runStatePath: (home: string) => string;
31
+ export interface RunState {
32
+ schema: string;
33
+ pid: number;
34
+ port: number;
35
+ url: string;
36
+ startedAt: string;
37
+ }
38
+ export type SpawnFn = (file: string, args: string[], opts: SpawnOptions) => ChildProcess;
39
+ export interface DashboardDeps {
40
+ home: string;
41
+ cwd: string;
42
+ root?: string;
43
+ env: NodeJS.ProcessEnv;
44
+ /** The CLI to run: node and its entry script (defaultCommand()). */
45
+ command?: {
46
+ file: string;
47
+ args: string[];
48
+ };
49
+ spawn?: SpawnFn;
50
+ probe?: (port: number) => Promise<boolean>;
51
+ alive?: (pid: number) => boolean;
52
+ timeoutMs?: number;
53
+ }
54
+ /** node + dist/bin/prism-cli.js; from source (tsx), the .ts entry with this process's loader flags. */
55
+ export declare function defaultCommand(): {
56
+ file: string;
57
+ args: string[];
58
+ };
59
+ /** True when `pid` is a process this user can signal. */
60
+ export declare function pidAlive(pid: number): boolean;
61
+ /** True when 127.0.0.1:<port> answers an untokened GET / with the dashboard's own 401. */
62
+ export declare function probeDashboard(port: number): Promise<boolean>;
63
+ /** The recorded instance when its pid is alive and its port answers as a dashboard; else null. */
64
+ export declare function liveInstance(d: DashboardDeps): Promise<RunState | null>;
65
+ /** Kills every dashboard this process started (sync: safe in an 'exit' handler). */
66
+ export declare function killStartedDashboards(home?: string): void;
67
+ export interface StartResult {
68
+ url: string;
69
+ port: number;
70
+ pid: number;
71
+ reused: boolean;
72
+ }
73
+ /** The running instance's URL, else starts one and waits for its URL line. */
74
+ export declare function startDashboard(d: DashboardDeps): Promise<StartResult>;
75
+ export interface StopResult {
76
+ stopped: boolean;
77
+ note: string;
78
+ }
79
+ /** Stops the instance in the state file (one started by prism_dashboard), never any other dashboard. */
80
+ export declare function stopDashboard(d: DashboardDeps): Promise<StopResult>;
81
+ /** What to tell the user when the browser is not opened here. */
82
+ export declare function remoteNote(port: number): string;
83
+ //# sourceMappingURL=dashboard-process.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dashboard-process.d.ts","sourceRoot":"","sources":["../../../src/mcp/dashboard-process.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAAsB,KAAK,YAAY,EAAE,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAQ9F,eAAO,MAAM,UAAU,2BAA2B,CAAC;AACnD,eAAO,MAAM,gBAAgB,QAAS,CAAC;AACvC,eAAO,MAAM,eAAe,OAAQ,CAAC;AACrC,eAAO,MAAM,gBAAgB,OAAQ,CAAC;AACtC,mFAAmF;AACnF,eAAO,MAAM,QAAQ,QAA2E,CAAC;AAEjG,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,WAAkD,CAAC;AAE5F,MAAM,WAAW,QAAQ;IAAG,MAAM,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE;AAEvG,MAAM,MAAM,OAAO,GAAG,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,KAAK,YAAY,CAAC;AAEzF,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,GAAG,EAAE,MAAM,CAAC,UAAU,CAAC;IACvB,oEAAoE;IACpE,OAAO,CAAC,EAAE;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC;IAC3C,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,uGAAuG;AACvG,wBAAgB,cAAc,IAAI;IAAE,IAAI,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,EAAE,CAAA;CAAE,CAIjE;AAED,yDAAyD;AACzD,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAE7C;AAED,0FAA0F;AAC1F,wBAAgB,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAa7D;AASD,kGAAkG;AAClG,wBAAsB,YAAY,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAK7E;AAUD,oFAAoF;AACpF,wBAAgB,qBAAqB,CAAC,IAAI,CAAC,EAAE,MAAM,GAAG,IAAI,CAMzD;AAED,MAAM,WAAW,WAAW;IAAG,GAAG,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAC;IAAC,GAAG,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,OAAO,CAAA;CAAE;AAExF,8EAA8E;AAC9E,wBAAsB,cAAc,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAiC3E;AAED,MAAM,WAAW,UAAU;IAAG,OAAO,EAAE,OAAO,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE;AAE9D,wGAAwG;AACxG,wBAAsB,aAAa,CAAC,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,UAAU,CAAC,CAiBzE;AAED,iEAAiE;AACjE,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAE/C"}