@dansyuqri/spark-joy 0.2.1 → 0.2.2

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.
@@ -1 +1 @@
1
- :root{--bg:#fbfaf8;--panel:#fff;--ink:#1d1d1f;--muted:#6b6b72;--line:#e4e2dd;--accent:#c2410c;--accent-ink:#fff;--add-bg:#e9f8ec;--add-ink:#116329;--del-bg:#fdeceb;--del-ink:#a40e26;--hunk-bg:#f1f0fb;--note-bg:#fff8e5;--done:#15803d;--partial:#b45309;--todo:#9ca3af;--mono:ui-monospace, "SF Mono", Menlo, Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial;--bg:#151518;--panel:#1c1c20;--ink:#ececef;--muted:#9a9aa3;--line:#2e2e34;--accent:#fb923c;--accent-ink:#1a1a1a;--add-bg:#12301c;--add-ink:#7ee2a0;--del-bg:#3a1619;--del-ink:#ff9aa5;--hunk-bg:#23233a;--note-bg:#2f2a18;--done:#4ade80;--partial:#fbbf24;--todo:#6b7280}}*{box-sizing:border-box}body{background:var(--bg);color:var(--ink);margin:0;font:14px/1.5 system-ui,-apple-system,Segoe UI,sans-serif}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code,kbd{font-family:var(--mono);font-size:.92em}kbd{border:1px solid var(--line);border-radius:4px;padding:0 4px}button{font:inherit;border:1px solid var(--line);background:var(--panel);color:var(--ink);cursor:pointer;border-radius:6px;padding:6px 12px}button:hover:not(:disabled){border-color:var(--muted)}button:disabled{opacity:.4;cursor:default}button.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink);font-weight:600}textarea{font:inherit;border:1px solid var(--line);background:var(--panel);width:100%;min-height:70px;color:var(--ink);resize:vertical;border-radius:6px;padding:8px}.muted{color:var(--muted)}.small{font-size:12px}.warn{color:var(--partial)}.pre{white-space:pre-wrap}.empty{max-width:640px;margin:80px auto;padding:0 16px}.topbar{z-index:10;background:var(--panel);border-bottom:1px solid var(--line);align-items:center;gap:20px;padding:10px 20px;display:flex;position:sticky;top:0}.brand{white-space:nowrap;font-weight:700}.spark{color:var(--accent)}.run{white-space:nowrap;text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.tabs{gap:4px;display:flex}.tabs a{color:var(--muted);border-radius:6px;padding:4px 10px}.tabs a.on{background:var(--hunk-bg);color:var(--ink);font-weight:600}.progress{font-variant-numeric:tabular-nums;align-items:center;gap:8px;display:flex}.progress .bar{background:var(--line);border-radius:3px;width:120px;height:6px;overflow:hidden}.progress .bar div{background:var(--done);height:100%;transition:width .2s}.saving{width:70px;color:var(--muted);font-size:12px}.saving.error{color:var(--del-ink)}.status{vertical-align:-2px;justify-content:center;align-self:center;width:1.2em;display:inline-flex}.status.done{color:var(--done)}.status.partial{color:var(--partial)}.status.todo{color:var(--todo)}.badge{border:1px solid var(--line);color:var(--muted);white-space:nowrap;border-radius:10px;margin-left:6px;padding:0 6px;font-size:11px;display:inline-block}.risk{vertical-align:middle;border-radius:50%;width:8px;height:8px;margin-left:8px;display:inline-block}.risk-high{background:var(--del-ink)}.risk-med{background:var(--partial)}.issues{border:1px solid var(--line);background:var(--panel);border-radius:8px;margin:12px 0;padding:8px 12px}.issues.has-errors{border-color:var(--del-ink)}.issues li.error{color:var(--del-ink)}.issues li.warning{color:var(--partial)}.page{max-width:1200px;padding:24px 32px 80px}.overview{max-width:800px;font-size:15px}.strategies{grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px;display:grid}.strategy{background:var(--panel);border:1px solid var(--line);border-radius:10px;flex-direction:column;padding:16px;display:flex}.strategy.on{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}.strategy h3{margin:0 0 4px}.strategy .stats{color:var(--muted);font-size:12px}.chunk-list{flex:1;padding-left:0;list-style:none}.chunk-list .n{min-width:1.6em;color:var(--muted);font-variant-numeric:tabular-nums}.chunk-list li{align-items:center;gap:4px;padding:3px 0;display:flex}.chunk-list li .title{flex:1}.chunk-list .branch{color:var(--muted);font-size:11px;display:block}.strategy .hint{color:var(--muted);margin:10px 0 0;font-size:12px}.size{font-variant-numeric:tabular-nums;color:var(--muted);margin-left:6px;font-size:12px}.features li{margin-bottom:6px}.review{grid-template-columns:300px 1fr;align-items:start;display:grid}.sidebar{border-right:1px solid var(--line);max-height:calc(100vh - 53px);padding:16px 8px 16px 16px;position:sticky;top:53px;overflow:auto}.sidebar ol{margin:8px 0;padding:0;list-style:none}.sidebar li a{color:var(--ink);border-radius:6px;align-items:baseline;gap:6px;padding:5px 8px;display:flex}.sidebar li a:hover{background:var(--hunk-bg);text-decoration:none}.sidebar li.on a{background:var(--hunk-bg);font-weight:600}.sidebar li.skim{border-top:1px dashed var(--line);margin-top:8px;padding-top:8px}.sidebar .n{color:var(--muted);font-variant-numeric:tabular-nums;min-width:1.4em}.sidebar .title{flex:1}.chunk-head h1{margin:0 0 2px;font-size:20px}.chunk-head .intent{max-width:820px;margin:6px 0 0;font-size:15px}.focus{max-width:820px;color:var(--muted);margin:6px 0 0;padding-left:18px}.diff{margin-top:16px}.file{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin-bottom:14px;overflow:hidden}.file>summary{cursor:pointer;border-bottom:1px solid var(--line);align-items:center;gap:4px;padding:8px 12px;display:flex}.file:not([open])>summary{border-bottom:none}.file .path{font-family:var(--mono);font-weight:600}.file .counts{font-family:var(--mono);margin-left:auto;font-size:12px}.add{color:var(--add-ink)}.del{color:var(--del-ink)}.file table{border-collapse:collapse;width:100%;font-family:var(--mono);table-layout:fixed;font-size:12.5px}.file col.no-col{width:52px}.file td.no{text-align:right;color:var(--muted);-webkit-user-select:none;user-select:none;cursor:pointer;vertical-align:top;padding:0 8px}.file td.no:hover{color:var(--accent)}.file td.code{white-space:pre-wrap;word-break:break-all;padding:0 8px}.sign{width:1.2em;color:var(--muted);-webkit-user-select:none;user-select:none;display:inline-block}tr.add td{background:var(--add-bg)}tr.del td{background:var(--del-bg)}tr.meta td{color:var(--muted);font-style:italic}tr.hunk-header td{background:var(--hunk-bg);color:var(--muted);padding:3px 8px}.hid{color:var(--ink);font-weight:700}tr.placeholder td{color:var(--muted);padding:8px;font-family:system-ui,sans-serif}tr.note-row td{background:var(--bg);padding:6px 12px 6px 112px}.line-note{background:var(--note-bg);border:1px solid var(--line);border-radius:6px;padding:8px 10px;font-family:system-ui,sans-serif;font-size:13px}.line-note .text{white-space:pre-wrap;cursor:text}.line-note .actions{gap:6px;margin-top:6px;display:flex}.line-note .actions button{padding:2px 8px;font-size:12px}.chunk-foot{background:var(--bg);border-top:1px solid var(--line);align-items:flex-end;gap:8px;padding:10px 0;display:flex;position:sticky;bottom:0}.chunk-foot textarea{flex:1;height:34px;min-height:34px;padding:6px 8px;transition:height .1s}.chunk-foot textarea:focus,.chunk-foot textarea:not(:placeholder-shown){height:80px}.totals{margin:0 0 8px;font-size:18px}.chunk-refs a{font-variant-numeric:tabular-nums;margin-left:8px;font-size:12px}.features{padding:0;list-style:none}.note-group{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin-bottom:10px;padding:10px 16px}.note-group p{margin:6px 0}.export textarea{min-height:300px;font-family:var(--mono);font-size:12px}@media (width<=900px){.review{grid-template-columns:1fr}.sidebar{border-right:none;border-bottom:1px solid var(--line);max-height:none;position:static}.page{padding:16px}.run{display:none}}.file{scroll-margin-top:64px}.map-page{flex-direction:column;height:calc(100vh - 53px);display:flex}.crumbs{color:var(--muted);padding:10px 20px;font-size:13px}.map-canvas{cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none;flex:1;position:relative;overflow:hidden}.map-canvas:active{cursor:grabbing}.empty-map{padding:20px}.zoom-in{transform-box:fill-box;animation:.22s ease-out zoom-in}@keyframes zoom-in{0%{opacity:0}}.node{cursor:pointer}.node rect{fill:var(--panel);stroke:var(--line);stroke-width:1.2px}.node:hover rect{stroke:var(--accent)}.node.neighbour rect{fill:var(--bg);stroke-dasharray:4 3}.node.neighbour text{opacity:.7}.node text{fill:var(--ink);font-size:13px}.node .title{font-weight:600}.node.file .title{font-family:var(--mono);font-size:12.5px}.node .n{fill:var(--muted);font-weight:400}.node .meta{fill:var(--muted);font-size:11px}.node .status.done{color:var(--done)}.node .status.partial{color:var(--partial)}.node .status.todo{color:var(--todo)}.node .risk-high{fill:var(--del-ink)}.node .risk-med{fill:var(--partial)}.edge path{fill:none;stroke:var(--muted);opacity:.7;transition:opacity .15s}.edge.link path{stroke-dasharray:5 4}.edge.faded{opacity:.15}.arrowhead{fill:var(--muted)}.edge-label rect{fill:var(--bg);stroke:var(--line)}.edge-label text{fill:var(--ink);font-size:11px}
1
+ :root{--bg:#fbfaf8;--panel:#fff;--ink:#1d1d1f;--muted:#6b6b72;--line:#e4e2dd;--accent:#c2410c;--accent-ink:#fff;--add-bg:#e9f8ec;--add-ink:#116329;--del-bg:#fdeceb;--del-ink:#a40e26;--hunk-bg:#f1f0fb;--note-bg:#fff8e5;--done:#15803d;--partial:#b45309;--todo:#9ca3af;--mono:ui-monospace, "SF Mono", Menlo, Consolas, monospace;--lightningcss-light:initial;--lightningcss-dark: ;color-scheme:light dark}@media (prefers-color-scheme:dark){:root{--lightningcss-light: ;--lightningcss-dark:initial;--bg:#151518;--panel:#1c1c20;--ink:#ececef;--muted:#9a9aa3;--line:#2e2e34;--accent:#fb923c;--accent-ink:#1a1a1a;--add-bg:#12301c;--add-ink:#7ee2a0;--del-bg:#3a1619;--del-ink:#ff9aa5;--hunk-bg:#23233a;--note-bg:#2f2a18;--done:#4ade80;--partial:#fbbf24;--todo:#6b7280}}*{box-sizing:border-box}body{background:var(--bg);color:var(--ink);margin:0;font:14px/1.5 system-ui,-apple-system,Segoe UI,sans-serif}a{color:var(--accent);text-decoration:none}a:hover{text-decoration:underline}code,kbd{font-family:var(--mono);font-size:.92em}kbd{border:1px solid var(--line);border-radius:4px;padding:0 4px}button{font:inherit;border:1px solid var(--line);background:var(--panel);color:var(--ink);cursor:pointer;border-radius:6px;padding:6px 12px}button:hover:not(:disabled){border-color:var(--muted)}button:disabled{opacity:.4;cursor:default}button.primary{background:var(--accent);border-color:var(--accent);color:var(--accent-ink);font-weight:600}textarea{font:inherit;border:1px solid var(--line);background:var(--panel);width:100%;min-height:70px;color:var(--ink);resize:vertical;border-radius:6px;padding:8px}.muted{color:var(--muted)}.small{font-size:12px}.warn{color:var(--partial)}.pre{white-space:pre-wrap}.empty{max-width:640px;margin:80px auto;padding:0 16px}.topbar{z-index:10;background:var(--panel);border-bottom:1px solid var(--line);align-items:center;gap:20px;padding:10px 20px;display:flex;position:sticky;top:0}.brand{white-space:nowrap;font-weight:700}.spark{color:var(--accent)}.run{white-space:nowrap;text-overflow:ellipsis;flex:1;min-width:0;overflow:hidden}.tabs{gap:4px;display:flex}.tabs a{color:var(--muted);border-radius:6px;padding:4px 10px}.tabs a.on{background:var(--hunk-bg);color:var(--ink);font-weight:600}.progress{font-variant-numeric:tabular-nums;align-items:center;gap:8px;display:flex}.progress .bar{background:var(--line);border-radius:3px;width:120px;height:6px;overflow:hidden}.progress .bar div{background:var(--done);height:100%;transition:width .2s}.saving{width:70px;color:var(--muted);font-size:12px}.saving.error{color:var(--del-ink)}.status{vertical-align:-2px;justify-content:center;align-self:center;width:1.2em;display:inline-flex}.status.done{color:var(--done)}.status.partial{color:var(--partial)}.status.todo{color:var(--todo)}.badge{border:1px solid var(--line);color:var(--muted);white-space:nowrap;border-radius:10px;margin-left:6px;padding:0 6px;font-size:11px;display:inline-block}.risk{vertical-align:middle;border-radius:50%;width:8px;height:8px;margin-left:8px;display:inline-block}.risk-high{background:var(--del-ink)}.risk-med{background:var(--partial)}.issues{border:1px solid var(--line);background:var(--panel);border-radius:8px;margin:12px 0;padding:8px 12px}.issues.has-errors{border-color:var(--del-ink)}.issues li.error{color:var(--del-ink)}.issues li.warning{color:var(--partial)}.page{max-width:1200px;padding:24px 32px 80px}.overview{max-width:800px;font-size:15px}.strategies{grid-template-columns:repeat(auto-fit,minmax(340px,1fr));gap:16px;display:grid}.strategy{background:var(--panel);border:1px solid var(--line);border-radius:10px;flex-direction:column;padding:16px;display:flex}.strategy.on{border-color:var(--accent);box-shadow:0 0 0 1px var(--accent)}.strategy h3{margin:0 0 4px}.strategy .stats{color:var(--muted);font-size:12px}.chunk-list{flex:1;padding-left:0;list-style:none}.chunk-list .n{min-width:1.6em;color:var(--muted);font-variant-numeric:tabular-nums}.chunk-list li{align-items:center;gap:4px;padding:3px 0;display:flex}.chunk-list li .title{flex:1}.chunk-list .branch{color:var(--muted);font-size:11px;display:block}.strategy .hint{color:var(--muted);margin:10px 0 0;font-size:12px}.size{font-variant-numeric:tabular-nums;color:var(--muted);margin-left:6px;font-size:12px}.features li{margin-bottom:6px}.review{grid-template-columns:300px 1fr;align-items:start;display:grid}.sidebar{border-right:1px solid var(--line);max-height:calc(100vh - 53px);padding:16px 8px 16px 16px;position:sticky;top:53px;overflow:auto}.sidebar ol{margin:8px 0;padding:0;list-style:none}.sidebar li a{color:var(--ink);border-radius:6px;align-items:baseline;gap:6px;padding:5px 8px;display:flex}.sidebar li a:hover{background:var(--hunk-bg);text-decoration:none}.sidebar li.on a{background:var(--hunk-bg);font-weight:600}.sidebar li.skim{border-top:1px dashed var(--line);margin-top:8px;padding-top:8px}.sidebar .n{color:var(--muted);font-variant-numeric:tabular-nums;min-width:1.4em}.sidebar .title{flex:1}.chunk-head h1{margin:0 0 2px;font-size:20px}.chunk-head .intent{max-width:820px;margin:6px 0 0;font-size:15px}.focus{max-width:820px;color:var(--muted);margin:6px 0 0;padding-left:18px}.diff{margin-top:16px}.file{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin-bottom:14px;overflow:hidden}.file>summary{cursor:pointer;border-bottom:1px solid var(--line);align-items:center;gap:4px;padding:8px 12px;display:flex}.file:not([open])>summary{border-bottom:none}.file .path{font-family:var(--mono);font-weight:600}.file .counts{font-family:var(--mono);margin-left:auto;font-size:12px}.add{color:var(--add-ink)}.del{color:var(--del-ink)}.file table{border-collapse:collapse;width:100%;font-family:var(--mono);table-layout:fixed;font-size:12.5px}.file col.no-col{width:52px}.file td.no{text-align:right;color:var(--muted);-webkit-user-select:none;user-select:none;cursor:pointer;vertical-align:top;padding:0 8px}.file td.no:hover{color:var(--accent)}.file td.code{white-space:pre-wrap;word-break:break-all;padding:0 8px}.sign{width:1.2em;color:var(--muted);-webkit-user-select:none;user-select:none;display:inline-block}tr.add td{background:var(--add-bg)}tr.del td{background:var(--del-bg)}tr.meta td{color:var(--muted);font-style:italic}tr.hunk-header td{background:var(--hunk-bg);color:var(--muted);padding:3px 8px}.hid{color:var(--ink);font-weight:700}tr.placeholder td{color:var(--muted);padding:8px;font-family:system-ui,sans-serif}tr.note-row td{background:var(--bg);padding:6px 12px 6px 112px}.line-note{background:var(--note-bg);border:1px solid var(--line);border-radius:6px;padding:8px 10px;font-family:system-ui,sans-serif;font-size:13px}.line-note .text{white-space:pre-wrap;cursor:text}.line-note .actions{gap:6px;margin-top:6px;display:flex}.line-note .actions button{padding:2px 8px;font-size:12px}.chunk-foot{background:var(--bg);border-top:1px solid var(--line);align-items:flex-end;gap:8px;padding:10px 0;display:flex;position:sticky;bottom:0}.chunk-foot textarea{flex:1;height:34px;min-height:34px;padding:6px 8px;transition:height .1s}.chunk-foot textarea:focus,.chunk-foot textarea:not(:placeholder-shown){height:80px}.totals{margin:0 0 8px;font-size:18px}.chunk-refs a{font-variant-numeric:tabular-nums;margin-left:8px;font-size:12px}.features{padding:0;list-style:none}.note-group{background:var(--panel);border:1px solid var(--line);border-radius:8px;margin-bottom:10px;padding:10px 16px}.note-group p{margin:6px 0}.export textarea{min-height:300px;font-family:var(--mono);font-size:12px}@media (width<=900px){.review{grid-template-columns:1fr}.sidebar{border-right:none;border-bottom:1px solid var(--line);max-height:none;position:static}.page{padding:16px}.run{display:none}}.file{scroll-margin-top:64px}.map-page{flex-direction:column;height:calc(100vh - 53px);display:flex}.crumbs{color:var(--muted);padding:10px 20px;font-size:13px}.map-canvas{cursor:grab;touch-action:none;-webkit-user-select:none;user-select:none;flex:1;position:relative;overflow:hidden}.map-canvas:active{cursor:grabbing}.empty-map{padding:20px}.zoom-in{transform-box:fill-box;animation:.22s ease-out zoom-in}@keyframes zoom-in{0%{opacity:0}}.node{cursor:pointer}.node rect{fill:var(--panel);stroke:var(--line);stroke-width:1.2px}.node:hover rect{stroke:var(--accent)}.node.neighbour rect{fill:var(--bg);stroke-dasharray:4 3}.node.neighbour text{opacity:.7}.node text{fill:var(--ink);font-size:13px}.node .title{font-weight:600}.node.file .title{font-family:var(--mono);font-size:12.5px}.node .n{fill:var(--muted);font-weight:400}.node .meta{fill:var(--muted);font-size:11px}.node .status.done{color:var(--done)}.node .status.partial{color:var(--partial)}.node .status.todo{color:var(--todo)}.node .risk-high{fill:var(--del-ink)}.node .risk-med{fill:var(--partial)}.edge path{fill:none;stroke:var(--muted);opacity:.7;transition:opacity .15s}.edge.link path{stroke-dasharray:5 4}.edge.faded{opacity:.15}.arrowhead{fill:var(--muted)}.edge-label rect{fill:var(--bg);stroke:var(--line)}.edge-label text{fill:var(--ink);font-size:11px}tr.gap-row td,tr.foreign-row td{background:var(--hunk-bg);color:var(--muted);padding:2px 8px;font-size:12px}tr.gap-row button{margin-right:6px;padding:1px 8px;font-size:12px}tr.line.expanded td.code,tr.line.expanded td.no{opacity:.75}tr.line.expanded td.no{cursor:default}.file summary .view-file{margin-left:8px;padding:1px 8px;font-size:12px}
@@ -5,8 +5,8 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
6
  <title>spark-joy review</title>
7
7
  <link rel="icon" href="./favicon.svg" type="image/svg+xml" />
8
- <script type="module" crossorigin src="/assets/index-_XxERk0m.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-ChCdW9F8.css">
8
+ <script type="module" crossorigin src="/assets/index-9PHU75Am.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-D3yLyUyh.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="root"></div>
package/dist/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  // cli/index.ts
2
- import { copyFile, mkdir as mkdir3, mkdtemp, readFile as readFile4, rm as rm2, writeFile as writeFile3 } from "fs/promises";
2
+ import { copyFile, mkdir as mkdir3, mkdtemp, readFile as readFile5, rm as rm2, writeFile as writeFile3 } from "fs/promises";
3
3
  import { homedir, tmpdir } from "os";
4
- import { join as join4, relative } from "path";
4
+ import { join as join5, relative } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { parseArgs } from "util";
7
7
 
@@ -667,6 +667,8 @@ function computeLinks(hunks) {
667
667
 
668
668
  // cli/lib/git.ts
669
669
  import { execFile } from "child_process";
670
+ import { lstat, readFile } from "fs/promises";
671
+ import { join } from "path";
670
672
  import { promisify } from "util";
671
673
  var exec = promisify(execFile);
672
674
  async function git(cwd, args, input, env) {
@@ -734,31 +736,39 @@ async function generatedByAttr(cwd, files) {
734
736
  }
735
737
  return result;
736
738
  }
739
+ async function fileText(root, meta, path) {
740
+ const source = meta.source ?? (meta.working ? "working" : "pr");
741
+ if (source === "working" || source === "uncommitted") {
742
+ if ((await lstat(join(root, path))).isSymbolicLink()) throw new Error(`${path} is a symlink`);
743
+ return readFile(join(root, path), "utf8");
744
+ }
745
+ return git(root, ["show", `${source === "staged" ? "" : meta.headSha}:${path}`]);
746
+ }
737
747
 
738
748
  // cli/lib/runs.ts
739
- import { mkdir, readFile, readdir, rename, stat, writeFile } from "fs/promises";
740
- import { join } from "path";
749
+ import { mkdir, readFile as readFile2, readdir, rename, stat, writeFile } from "fs/promises";
750
+ import { join as join2 } from "path";
741
751
  import { z as z2 } from "zod";
742
752
  var RUNS_DIR = ".spark-joy";
743
753
  var slug = (s) => s.replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "run";
744
754
  function runPaths(root, name) {
745
- const dir = join(root, RUNS_DIR, name);
755
+ const dir = join2(root, RUNS_DIR, name);
746
756
  return {
747
757
  name,
748
758
  dir,
749
- hunks: join(dir, "hunks.json"),
750
- meta: join(dir, "meta.json"),
751
- plan: join(dir, "plan.json"),
752
- state: join(dir, "review-state.json"),
753
- review: join(dir, "review.md"),
754
- links: join(dir, "links.json"),
755
- prs: join(dir, "prs.md")
759
+ hunks: join2(dir, "hunks.json"),
760
+ meta: join2(dir, "meta.json"),
761
+ plan: join2(dir, "plan.json"),
762
+ state: join2(dir, "review-state.json"),
763
+ review: join2(dir, "review.md"),
764
+ links: join2(dir, "links.json"),
765
+ prs: join2(dir, "prs.md")
756
766
  };
757
767
  }
758
768
  async function ensureRunsDir(root) {
759
- const dir = join(root, RUNS_DIR);
769
+ const dir = join2(root, RUNS_DIR);
760
770
  await mkdir(dir, { recursive: true });
761
- await writeFile(join(dir, ".gitignore"), "*\n");
771
+ await writeFile(join2(dir, ".gitignore"), "*\n");
762
772
  }
763
773
  var exists = (p) => stat(p).then(() => true, () => false);
764
774
  async function resolveRun(root, name, branch) {
@@ -774,11 +784,11 @@ async function resolveRun(root, name, branch) {
774
784
  return runPaths(root, runs[0].name);
775
785
  }
776
786
  async function listRuns(root) {
777
- const dir = join(root, RUNS_DIR);
787
+ const dir = join2(root, RUNS_DIR);
778
788
  const names = await readdir(dir).catch(() => []);
779
789
  const runs = [];
780
790
  for (const name of names) {
781
- const s = await stat(join(dir, name, "hunks.json")).catch(() => void 0);
791
+ const s = await stat(join2(dir, name, "hunks.json")).catch(() => void 0);
782
792
  if (s) runs.push({ name, mtime: s.mtimeMs });
783
793
  }
784
794
  return runs.sort((a, b) => b.mtime - a.mtime);
@@ -789,7 +799,7 @@ async function writeJson(path, data) {
789
799
  await rename(tmp, path);
790
800
  }
791
801
  async function readJson(path) {
792
- return JSON.parse(await readFile(path, "utf8"));
802
+ return JSON.parse(await readFile2(path, "utf8"));
793
803
  }
794
804
  var readHunks = async (p) => z2.array(Hunk).parse(await readJson(p.hunks));
795
805
  var readMeta = async (p) => Meta.parse(await readJson(p.meta));
@@ -1040,8 +1050,8 @@ function formatLinks(hunks, links) {
1040
1050
 
1041
1051
  // cli/lib/server.ts
1042
1052
  import { createServer } from "http";
1043
- import { readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
1044
- import { extname as extname2, join as join2, normalize, sep } from "path";
1053
+ import { readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
1054
+ import { extname as extname2, join as join3, normalize, sep } from "path";
1045
1055
  var TYPES = {
1046
1056
  ".html": "text/html; charset=utf-8",
1047
1057
  ".js": "text/javascript; charset=utf-8",
@@ -1051,6 +1061,7 @@ var TYPES = {
1051
1061
  ".map": "application/json"
1052
1062
  };
1053
1063
  var MAX_BODY = 20 * 1024 * 1024;
1064
+ var MAX_FILE = 2 * 1024 * 1024;
1054
1065
  async function body(req) {
1055
1066
  const chunks = [];
1056
1067
  let size = 0;
@@ -1097,6 +1108,19 @@ async function serve(opts) {
1097
1108
  return send(res, 403, { error: "cross-origin request refused" });
1098
1109
  }
1099
1110
  if (url.pathname === "/api/run" && req.method === "GET") return send(res, 200, await loadRun(paths));
1111
+ if (url.pathname === "/api/file" && req.method === "GET") {
1112
+ const path = url.searchParams.get("path") ?? "";
1113
+ const [meta, hunks] = await Promise.all([readMeta(paths), readHunks(paths)]);
1114
+ const hs = hunks.filter((h) => h.file === path);
1115
+ if (!hs.length || hs[0].status === "deleted" || hs.some((h) => h.flags.includes("binary")))
1116
+ return send(res, 404, { error: "no text for that file" });
1117
+ const text = await fileText(meta.repoRoot, meta, path).catch(() => void 0);
1118
+ if (text === void 0) return send(res, 404, { error: "no text for that file" });
1119
+ if (text.length > MAX_FILE || text.includes("\0")) return send(res, 404, { error: "file too large or binary" });
1120
+ const lines = text.split("\n");
1121
+ if (lines.at(-1) === "") lines.pop();
1122
+ return send(res, 200, { lines: lines.map((l) => l.replace(/\r$/, "")) });
1123
+ }
1100
1124
  if (url.pathname === "/api/state" && req.method === "PUT") {
1101
1125
  const state = ReviewState.parse(await body(req));
1102
1126
  state.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
@@ -1113,14 +1137,14 @@ async function serve(opts) {
1113
1137
  if (url.pathname.startsWith("/api/")) return send(res, 404, { error: "not found" });
1114
1138
  if (req.method !== "GET") return send(res, 405, { error: "method not allowed" });
1115
1139
  const rel = normalize(decodeURIComponent(url.pathname)).replace(/^([/\\])+/, "");
1116
- let file = join2(appDir, rel);
1140
+ let file = join3(appDir, rel);
1117
1141
  if (!file.startsWith(appDir + sep) && file !== appDir) return send(res, 403, { error: "forbidden" });
1118
1142
  let data;
1119
1143
  try {
1120
- data = await readFile2(file);
1144
+ data = await readFile3(file);
1121
1145
  } catch {
1122
- file = join2(appDir, "index.html");
1123
- data = await readFile2(file);
1146
+ file = join3(appDir, "index.html");
1147
+ data = await readFile3(file);
1124
1148
  }
1125
1149
  res.writeHead(200, { "content-type": TYPES[extname2(file)] ?? "application/octet-stream" });
1126
1150
  res.end(data);
@@ -1153,8 +1177,8 @@ async function serve(opts) {
1153
1177
 
1154
1178
  // cli/lib/stack.ts
1155
1179
  import { execFile as execFile2 } from "child_process";
1156
- import { lstat, readFile as readFile3, readlink, rm } from "fs/promises";
1157
- import { join as join3 } from "path";
1180
+ import { lstat as lstat2, readFile as readFile4, readlink, rm } from "fs/promises";
1181
+ import { join as join4 } from "path";
1158
1182
  import { promisify as promisify2 } from "util";
1159
1183
  var execP = promisify2(execFile2);
1160
1184
  var ZERO = "0".repeat(40);
@@ -1222,9 +1246,9 @@ async function targetEntry(root, meta, path) {
1222
1246
  const m = /^(\d+) ([0-9a-f]+) 0\t/.exec(await git(root, ["ls-files", "-s", "-z", "--", path]));
1223
1247
  if (m) e = { mode: m[1], sha: m[2] };
1224
1248
  } else {
1225
- const full = join3(root, path);
1226
- const st = await lstat(full);
1227
- const content = st.isSymbolicLink() ? await readlink(full) : await readFile3(full);
1249
+ const full = join4(root, path);
1250
+ const st = await lstat2(full);
1251
+ const content = st.isSymbolicLink() ? await readlink(full) : await readFile4(full);
1228
1252
  const mode = st.isSymbolicLink() ? "120000" : st.mode & 73 ? "100755" : "100644";
1229
1253
  const sha = st.isSymbolicLink() ? (await git(root, ["hash-object", "-w", "--stdin"], content)).trim() : (await git(root, ["hash-object", "-w", "--", path])).trim();
1230
1254
  e = { mode, sha };
@@ -1330,7 +1354,7 @@ async function createBranches(root, stack, force) {
1330
1354
  async function checkStack(root, stack, cmd, tmp) {
1331
1355
  const results = [];
1332
1356
  for (const pr of stack.prs) {
1333
- const dir = join3(tmp, `pr-${pr.n}`);
1357
+ const dir = join4(tmp, `pr-${pr.n}`);
1334
1358
  await git(root, ["worktree", "add", "--detach", "--quiet", dir, pr.commit]);
1335
1359
  try {
1336
1360
  const r = await execP(cmd, { cwd: dir, shell: true, maxBuffer: 64 * 1024 * 1024 }).then(
@@ -1430,7 +1454,7 @@ async function main(argv, cwd = process.cwd()) {
1430
1454
  });
1431
1455
  const [cmd, ...rest] = positionals;
1432
1456
  if (values.version) {
1433
- const pkg = JSON.parse(await readFile4(join4(PKG_ROOT, "package.json"), "utf8"));
1457
+ const pkg = JSON.parse(await readFile5(join5(PKG_ROOT, "package.json"), "utf8"));
1434
1458
  console.log(pkg.version);
1435
1459
  return 0;
1436
1460
  }
@@ -1489,7 +1513,7 @@ async function main(argv, cwd = process.cwd()) {
1489
1513
  case "serve": {
1490
1514
  const p = await run(cwd, values.run);
1491
1515
  const port = values.port ? Number(values.port) : 4848;
1492
- const s = await serve({ paths: p, appDir: join4(PKG_ROOT, "dist", "app"), port });
1516
+ const s = await serve({ paths: p, appDir: join5(PKG_ROOT, "dist", "app"), port });
1493
1517
  console.log(`spark-joy reviewing ${p.name} at ${s.url} (Ctrl+C to stop)`);
1494
1518
  await new Promise((resolve) => process.once("SIGINT", resolve));
1495
1519
  await s.close();
@@ -1524,13 +1548,13 @@ async function main(argv, cwd = process.cwd()) {
1524
1548
  hunks,
1525
1549
  plan: result.plan,
1526
1550
  prefix,
1527
- indexFile: join4(p.dir, "split.index")
1551
+ indexFile: join5(p.dir, "split.index")
1528
1552
  });
1529
1553
  for (const pr of stack.prs)
1530
1554
  console.log(`${String(pr.n).padStart(2)} ${pr.branch} ${pr.lines} lines, ${pr.files} files ${pr.chunk.title}`);
1531
1555
  console.log(`${stack.prs.length} PRs; the last one matches your changes exactly`);
1532
1556
  if (values.check !== void 0) {
1533
- const tmp = await mkdtemp(join4(tmpdir(), "spark-joy-check-"));
1557
+ const tmp = await mkdtemp(join5(tmpdir(), "spark-joy-check-"));
1534
1558
  try {
1535
1559
  const results = await checkStack(meta.repoRoot, stack, values.check, tmp);
1536
1560
  for (const r of results) {
@@ -1555,9 +1579,9 @@ nothing created; add --apply to create the branches`);
1555
1579
  return 0;
1556
1580
  }
1557
1581
  case "install-skill": {
1558
- const skill = join4(PKG_ROOT, "plugin", "skills", "spark-joy", "SKILL.md");
1582
+ const skill = join5(PKG_ROOT, "plugin", "skills", "spark-joy", "SKILL.md");
1559
1583
  if (values.print) {
1560
- process.stdout.write(await readFile4(skill, "utf8"));
1584
+ process.stdout.write(await readFile5(skill, "utf8"));
1561
1585
  return 0;
1562
1586
  }
1563
1587
  const homes = { claude: ".claude", agents: ".agents" };
@@ -1566,9 +1590,9 @@ nothing created; add --apply to create the branches`);
1566
1590
  if (bad) throw new Error(`unknown agent ${bad}; use claude or agents`);
1567
1591
  const root = values.project ? await repoRoot(cwd) : homedir();
1568
1592
  for (const a of agents) {
1569
- const dir = join4(root, homes[a], "skills", "spark-joy");
1593
+ const dir = join5(root, homes[a], "skills", "spark-joy");
1570
1594
  await mkdir3(dir, { recursive: true });
1571
- await copyFile(skill, join4(dir, "SKILL.md"));
1595
+ await copyFile(skill, join5(dir, "SKILL.md"));
1572
1596
  console.log(`installed skill to ${dir}`);
1573
1597
  }
1574
1598
  console.log('ask your agent to "review this PR in chunks" or "split my changes into PRs"');