@dansyuqri/spark-joy 0.2.0 → 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.
- package/README.md +20 -21
- package/dist/app/assets/index-9PHU75Am.js +8 -0
- package/dist/app/assets/{index-ChCdW9F8.css → index-D3yLyUyh.css} +1 -1
- package/dist/app/index.html +2 -2
- package/dist/cli.js +81 -39
- package/dist/cli.js.map +1 -1
- package/package.json +4 -1
- package/plugin/skills/spark-joy/SKILL.md +1 -1
- package/dist/app/assets/index-CX9CpO88.js +0 -8
|
@@ -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}
|
package/dist/app/index.html
CHANGED
|
@@ -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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
|
|
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
|
|
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 =
|
|
755
|
+
const dir = join2(root, RUNS_DIR, name);
|
|
746
756
|
return {
|
|
747
757
|
name,
|
|
748
758
|
dir,
|
|
749
|
-
hunks:
|
|
750
|
-
meta:
|
|
751
|
-
plan:
|
|
752
|
-
state:
|
|
753
|
-
review:
|
|
754
|
-
links:
|
|
755
|
-
prs:
|
|
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 =
|
|
769
|
+
const dir = join2(root, RUNS_DIR);
|
|
760
770
|
await mkdir(dir, { recursive: true });
|
|
761
|
-
await writeFile(
|
|
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 =
|
|
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(
|
|
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
|
|
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
|
|
1044
|
-
import { extname as extname2, join as
|
|
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 =
|
|
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
|
|
1144
|
+
data = await readFile3(file);
|
|
1121
1145
|
} catch {
|
|
1122
|
-
file =
|
|
1123
|
-
data = await
|
|
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
|
|
1157
|
-
import { join as
|
|
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 =
|
|
1226
|
-
const st = await
|
|
1227
|
-
const content = st.isSymbolicLink() ? await readlink(full) : await
|
|
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 =
|
|
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(
|
|
@@ -1387,8 +1411,11 @@ Commands:
|
|
|
1387
1411
|
--apply Create the branches (never pushes)
|
|
1388
1412
|
--prefix <name> Branch prefix (default <branch>-split)
|
|
1389
1413
|
--force Move branches that already exist
|
|
1390
|
-
install-skill Install the Claude Code
|
|
1391
|
-
|
|
1414
|
+
install-skill Install the agent skill for Claude Code (~/.claude/skills) and for
|
|
1415
|
+
Codex, Gemini CLI, Cursor, OpenCode and others (~/.agents/skills)
|
|
1416
|
+
--agent <name> Only one: claude or agents
|
|
1417
|
+
--project Install into the repo (./.claude/skills, ./.agents/skills)
|
|
1418
|
+
--print Print the skill instead, e.g. to paste into AGENTS.md
|
|
1392
1419
|
|
|
1393
1420
|
Common options:
|
|
1394
1421
|
--run <name> Run to use (default: the current branch's run)
|
|
@@ -1413,6 +1440,8 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1413
1440
|
prefix: { type: "string" },
|
|
1414
1441
|
force: { type: "boolean" },
|
|
1415
1442
|
check: { type: "string" },
|
|
1443
|
+
agent: { type: "string" },
|
|
1444
|
+
print: { type: "boolean" },
|
|
1416
1445
|
"split-at": { type: "string" },
|
|
1417
1446
|
file: { type: "string" },
|
|
1418
1447
|
json: { type: "boolean" },
|
|
@@ -1425,7 +1454,7 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1425
1454
|
});
|
|
1426
1455
|
const [cmd, ...rest] = positionals;
|
|
1427
1456
|
if (values.version) {
|
|
1428
|
-
const pkg = JSON.parse(await
|
|
1457
|
+
const pkg = JSON.parse(await readFile5(join5(PKG_ROOT, "package.json"), "utf8"));
|
|
1429
1458
|
console.log(pkg.version);
|
|
1430
1459
|
return 0;
|
|
1431
1460
|
}
|
|
@@ -1484,7 +1513,7 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1484
1513
|
case "serve": {
|
|
1485
1514
|
const p = await run(cwd, values.run);
|
|
1486
1515
|
const port = values.port ? Number(values.port) : 4848;
|
|
1487
|
-
const s = await serve({ paths: p, appDir:
|
|
1516
|
+
const s = await serve({ paths: p, appDir: join5(PKG_ROOT, "dist", "app"), port });
|
|
1488
1517
|
console.log(`spark-joy reviewing ${p.name} at ${s.url} (Ctrl+C to stop)`);
|
|
1489
1518
|
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
1490
1519
|
await s.close();
|
|
@@ -1519,13 +1548,13 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1519
1548
|
hunks,
|
|
1520
1549
|
plan: result.plan,
|
|
1521
1550
|
prefix,
|
|
1522
|
-
indexFile:
|
|
1551
|
+
indexFile: join5(p.dir, "split.index")
|
|
1523
1552
|
});
|
|
1524
1553
|
for (const pr of stack.prs)
|
|
1525
1554
|
console.log(`${String(pr.n).padStart(2)} ${pr.branch} ${pr.lines} lines, ${pr.files} files ${pr.chunk.title}`);
|
|
1526
1555
|
console.log(`${stack.prs.length} PRs; the last one matches your changes exactly`);
|
|
1527
1556
|
if (values.check !== void 0) {
|
|
1528
|
-
const tmp = await mkdtemp(
|
|
1557
|
+
const tmp = await mkdtemp(join5(tmpdir(), "spark-joy-check-"));
|
|
1529
1558
|
try {
|
|
1530
1559
|
const results = await checkStack(meta.repoRoot, stack, values.check, tmp);
|
|
1531
1560
|
for (const r of results) {
|
|
@@ -1550,10 +1579,23 @@ nothing created; add --apply to create the branches`);
|
|
|
1550
1579
|
return 0;
|
|
1551
1580
|
}
|
|
1552
1581
|
case "install-skill": {
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1582
|
+
const skill = join5(PKG_ROOT, "plugin", "skills", "spark-joy", "SKILL.md");
|
|
1583
|
+
if (values.print) {
|
|
1584
|
+
process.stdout.write(await readFile5(skill, "utf8"));
|
|
1585
|
+
return 0;
|
|
1586
|
+
}
|
|
1587
|
+
const homes = { claude: ".claude", agents: ".agents" };
|
|
1588
|
+
const agents = values.agent ? [values.agent] : Object.keys(homes);
|
|
1589
|
+
const bad = agents.find((a) => !homes[a]);
|
|
1590
|
+
if (bad) throw new Error(`unknown agent ${bad}; use claude or agents`);
|
|
1591
|
+
const root = values.project ? await repoRoot(cwd) : homedir();
|
|
1592
|
+
for (const a of agents) {
|
|
1593
|
+
const dir = join5(root, homes[a], "skills", "spark-joy");
|
|
1594
|
+
await mkdir3(dir, { recursive: true });
|
|
1595
|
+
await copyFile(skill, join5(dir, "SKILL.md"));
|
|
1596
|
+
console.log(`installed skill to ${dir}`);
|
|
1597
|
+
}
|
|
1598
|
+
console.log('ask your agent to "review this PR in chunks" or "split my changes into PRs"');
|
|
1557
1599
|
return 0;
|
|
1558
1600
|
}
|
|
1559
1601
|
default:
|