@dansyuqri/spark-joy 0.2.2 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/app/assets/{index-9PHU75Am.js → index-CDcwMAja.js} +1 -1
- package/dist/app/assets/index-pgxDJMuQ.css +1 -0
- package/dist/app/index.html +2 -2
- package/dist/cli.js +71 -24
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
- package/plugin/skills/spark-joy/SKILL.md +25 -9
- package/dist/app/assets/index-D3yLyUyh.css +0 -1
|
@@ -0,0 +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;color:#fff;white-space:nowrap;border-radius:9px;margin-left:8px;padding:0 6px;font-size:11px;font-weight:600;line-height:17px;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}.risk-reason{border-left:3px solid var(--partial);margin:4px 0 0;padding-left:8px;font-size:13px}.risk-reason-high{border-left-color:var(--del-ink)}.rubric{color:var(--muted);margin:0 0 12px;font-size:13px}.rubric p{margin:4px 0}.rubric .risk{margin:0 6px 0 0}
|
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-CDcwMAja.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-pgxDJMuQ.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 readFile6, rm as rm2, writeFile as writeFile3 } from "fs/promises";
|
|
3
3
|
import { homedir, tmpdir } from "os";
|
|
4
|
-
import { join as
|
|
4
|
+
import { join as join6, relative } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
6
|
import { parseArgs } from "util";
|
|
7
7
|
|
|
@@ -66,6 +66,8 @@ var Chunk = z.object({
|
|
|
66
66
|
dependsOn: z.array(z.string()).default([]),
|
|
67
67
|
reviewFocus: z.array(z.string()).default([]),
|
|
68
68
|
risk: Risk.default("low"),
|
|
69
|
+
// Why this chunk is med or high risk, against the agreed rubric.
|
|
70
|
+
riskReason: z.string().min(1).optional(),
|
|
69
71
|
// Split mode: the branch this chunk becomes.
|
|
70
72
|
branch: z.string().min(1).optional()
|
|
71
73
|
});
|
|
@@ -119,6 +121,23 @@ var ReviewState = z.object({
|
|
|
119
121
|
updatedAt: z.string().optional()
|
|
120
122
|
});
|
|
121
123
|
var emptyState = () => ReviewState.parse({});
|
|
124
|
+
var RUBRIC_FILE = ".spark-joy.md";
|
|
125
|
+
function parseRubric(text) {
|
|
126
|
+
const rubric = { high: [], med: [] };
|
|
127
|
+
let inRisk = false;
|
|
128
|
+
for (const line of text.split(/\r?\n/)) {
|
|
129
|
+
const heading = /^#{1,6}\s+(.*)$/.exec(line);
|
|
130
|
+
if (heading) {
|
|
131
|
+
inRisk = /^risk$/i.test(heading[1].trim());
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const m = inRisk && /^\s*[-*]\s*(high|med|medium)\s*:\s*(.+)$/i.exec(line);
|
|
135
|
+
if (!m) continue;
|
|
136
|
+
const items = m[2].split(",").map((s) => s.trim()).filter(Boolean);
|
|
137
|
+
rubric[m[1].toLowerCase() === "high" ? "high" : "med"].push(...items);
|
|
138
|
+
}
|
|
139
|
+
return rubric.high.length || rubric.med.length ? rubric : void 0;
|
|
140
|
+
}
|
|
122
141
|
|
|
123
142
|
// shared/derive.ts
|
|
124
143
|
var isIgnorable = (h) => h.flags.some((f) => IGNORABLE_FLAGS.includes(f));
|
|
@@ -188,7 +207,8 @@ var BUDGET = {
|
|
|
188
207
|
focusWords: 8,
|
|
189
208
|
featureTitle: 4,
|
|
190
209
|
featureDescription: 8,
|
|
191
|
-
linkLabel: 3
|
|
210
|
+
linkLabel: 3,
|
|
211
|
+
riskReason: 8
|
|
192
212
|
};
|
|
193
213
|
var PADDING = [
|
|
194
214
|
[/,\s*not\b|\bnot just\b|\brather than\b|\binstead of\b/i, "drop the contrast; say only what it does"],
|
|
@@ -264,6 +284,10 @@ function validatePlan(raw, hunks, budget = CHUNK_LINE_BUDGET) {
|
|
|
264
284
|
if (c.hunks.length === 0) error(cw, "chunk has no hunks");
|
|
265
285
|
tooLong(cw, "title", c.title, BUDGET.chunkTitle);
|
|
266
286
|
tooLong(cw, "intent", c.intent, BUDGET.intent);
|
|
287
|
+
if (c.risk === "low") {
|
|
288
|
+
if (c.riskReason) warn(cw, "riskReason on a low-risk chunk; drop it");
|
|
289
|
+
} else if (!c.riskReason) error(cw, `${c.risk} risk needs a riskReason: which rubric item, and where`);
|
|
290
|
+
else tooLong(cw, "riskReason", c.riskReason, BUDGET.riskReason);
|
|
267
291
|
if (c.reviewFocus.length > BUDGET.focusItems)
|
|
268
292
|
error(cw, `${c.reviewFocus.length} reviewFocus items; max ${BUDGET.focusItems}`);
|
|
269
293
|
c.reviewFocus.forEach((q, n) => tooLong(cw, `reviewFocus ${n + 1}`, q, BUDGET.focusWords));
|
|
@@ -948,7 +972,8 @@ function exportMarkdown(meta, hunks, plan, state) {
|
|
|
948
972
|
for (const c of view.chunks) {
|
|
949
973
|
const status = hunkStatus(c.hunks, reviewed);
|
|
950
974
|
const changed = status === "partial" ? " \xB7 changed since review" : "";
|
|
951
|
-
|
|
975
|
+
const risk = c.chunk.risk !== "low" ? ` \xB7 ${c.chunk.risk}: ${c.chunk.riskReason ?? "unexplained"}` : "";
|
|
976
|
+
out.push(`- ${MARK[status]} ${c.index + 1}. ${c.chunk.title} (${c.size})${risk}${changed}`);
|
|
952
977
|
const note = state.chunkNotes[chunkNoteKey(view.strategy.id, c.chunk.id)]?.trim();
|
|
953
978
|
if (note) out.push(...note.split("\n").map((l) => ` > ${l}`));
|
|
954
979
|
for (const n of notesInHunks(c.hunks, state.lineNotes))
|
|
@@ -1048,10 +1073,19 @@ function formatLinks(hunks, links) {
|
|
|
1048
1073
|
return [...pairs].sort(([a], [b]) => a.localeCompare(b)).map(([pair, items]) => [pair, ...items.map((i) => ` ${i}`)].join("\n")).join("\n");
|
|
1049
1074
|
}
|
|
1050
1075
|
|
|
1076
|
+
// cli/lib/rubric.ts
|
|
1077
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
1078
|
+
import { join as join3 } from "path";
|
|
1079
|
+
async function readRubric(root) {
|
|
1080
|
+
const text = await readFile3(join3(root, RUBRIC_FILE), "utf8").catch(() => void 0);
|
|
1081
|
+
return text === void 0 ? void 0 : parseRubric(text);
|
|
1082
|
+
}
|
|
1083
|
+
var formatRubric = (r) => [r.high.length ? `high: ${r.high.join(", ")}` : "", r.med.length ? `med: ${r.med.join(", ")}` : ""].filter(Boolean).join("\n");
|
|
1084
|
+
|
|
1051
1085
|
// cli/lib/server.ts
|
|
1052
1086
|
import { createServer } from "http";
|
|
1053
|
-
import { readFile as
|
|
1054
|
-
import { extname as extname2, join as
|
|
1087
|
+
import { readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
|
|
1088
|
+
import { extname as extname2, join as join4, normalize, sep } from "path";
|
|
1055
1089
|
var TYPES = {
|
|
1056
1090
|
".html": "text/html; charset=utf-8",
|
|
1057
1091
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -1084,6 +1118,7 @@ async function loadRun(paths) {
|
|
|
1084
1118
|
readState(paths),
|
|
1085
1119
|
readLinks(paths)
|
|
1086
1120
|
]);
|
|
1121
|
+
const rubric = await readRubric(meta.repoRoot);
|
|
1087
1122
|
const validation = raw === void 0 ? void 0 : validatePlan(raw, hunks);
|
|
1088
1123
|
return {
|
|
1089
1124
|
meta,
|
|
@@ -1092,7 +1127,8 @@ async function loadRun(paths) {
|
|
|
1092
1127
|
issues: validation?.issues ?? [],
|
|
1093
1128
|
hasPlan: raw !== void 0,
|
|
1094
1129
|
links,
|
|
1095
|
-
state
|
|
1130
|
+
state,
|
|
1131
|
+
rubric
|
|
1096
1132
|
};
|
|
1097
1133
|
}
|
|
1098
1134
|
async function serve(opts) {
|
|
@@ -1137,14 +1173,14 @@ async function serve(opts) {
|
|
|
1137
1173
|
if (url.pathname.startsWith("/api/")) return send(res, 404, { error: "not found" });
|
|
1138
1174
|
if (req.method !== "GET") return send(res, 405, { error: "method not allowed" });
|
|
1139
1175
|
const rel = normalize(decodeURIComponent(url.pathname)).replace(/^([/\\])+/, "");
|
|
1140
|
-
let file =
|
|
1176
|
+
let file = join4(appDir, rel);
|
|
1141
1177
|
if (!file.startsWith(appDir + sep) && file !== appDir) return send(res, 403, { error: "forbidden" });
|
|
1142
1178
|
let data;
|
|
1143
1179
|
try {
|
|
1144
|
-
data = await
|
|
1180
|
+
data = await readFile4(file);
|
|
1145
1181
|
} catch {
|
|
1146
|
-
file =
|
|
1147
|
-
data = await
|
|
1182
|
+
file = join4(appDir, "index.html");
|
|
1183
|
+
data = await readFile4(file);
|
|
1148
1184
|
}
|
|
1149
1185
|
res.writeHead(200, { "content-type": TYPES[extname2(file)] ?? "application/octet-stream" });
|
|
1150
1186
|
res.end(data);
|
|
@@ -1177,8 +1213,8 @@ async function serve(opts) {
|
|
|
1177
1213
|
|
|
1178
1214
|
// cli/lib/stack.ts
|
|
1179
1215
|
import { execFile as execFile2 } from "child_process";
|
|
1180
|
-
import { lstat as lstat2, readFile as
|
|
1181
|
-
import { join as
|
|
1216
|
+
import { lstat as lstat2, readFile as readFile5, readlink, rm } from "fs/promises";
|
|
1217
|
+
import { join as join5 } from "path";
|
|
1182
1218
|
import { promisify as promisify2 } from "util";
|
|
1183
1219
|
var execP = promisify2(execFile2);
|
|
1184
1220
|
var ZERO = "0".repeat(40);
|
|
@@ -1246,9 +1282,9 @@ async function targetEntry(root, meta, path) {
|
|
|
1246
1282
|
const m = /^(\d+) ([0-9a-f]+) 0\t/.exec(await git(root, ["ls-files", "-s", "-z", "--", path]));
|
|
1247
1283
|
if (m) e = { mode: m[1], sha: m[2] };
|
|
1248
1284
|
} else {
|
|
1249
|
-
const full =
|
|
1285
|
+
const full = join5(root, path);
|
|
1250
1286
|
const st = await lstat2(full);
|
|
1251
|
-
const content = st.isSymbolicLink() ? await readlink(full) : await
|
|
1287
|
+
const content = st.isSymbolicLink() ? await readlink(full) : await readFile5(full);
|
|
1252
1288
|
const mode = st.isSymbolicLink() ? "120000" : st.mode & 73 ? "100755" : "100644";
|
|
1253
1289
|
const sha = st.isSymbolicLink() ? (await git(root, ["hash-object", "-w", "--stdin"], content)).trim() : (await git(root, ["hash-object", "-w", "--", path])).trim();
|
|
1254
1290
|
e = { mode, sha };
|
|
@@ -1354,7 +1390,7 @@ async function createBranches(root, stack, force) {
|
|
|
1354
1390
|
async function checkStack(root, stack, cmd, tmp) {
|
|
1355
1391
|
const results = [];
|
|
1356
1392
|
for (const pr of stack.prs) {
|
|
1357
|
-
const dir =
|
|
1393
|
+
const dir = join5(tmp, `pr-${pr.n}`);
|
|
1358
1394
|
await git(root, ["worktree", "add", "--detach", "--quiet", dir, pr.commit]);
|
|
1359
1395
|
try {
|
|
1360
1396
|
const r = await execP(cmd, { cwd: dir, shell: true, maxBuffer: 64 * 1024 * 1024 }).then(
|
|
@@ -1376,6 +1412,7 @@ function prsMarkdown(meta, stack) {
|
|
|
1376
1412
|
out.push(`## ${pr.n}. ${c.title}`, "");
|
|
1377
1413
|
out.push(`\`${pr.branch}\` \u2192 \`${i === 0 ? target : stack.prs[i - 1].branch}\` \xB7 ${pr.lines} lines`, "");
|
|
1378
1414
|
out.push(c.intent, "");
|
|
1415
|
+
if (c.risk !== "low") out.push(`**${c.risk === "high" ? "High" : "Medium"} risk:** ${c.riskReason ?? "unexplained"}`, "");
|
|
1379
1416
|
for (const q of c.reviewFocus) out.push(`- ${q}`);
|
|
1380
1417
|
if (c.reviewFocus.length) out.push("");
|
|
1381
1418
|
const deps = c.dependsOn.map((d) => stack.prs.find((p) => p.chunk.id === d)?.n).filter((n) => n && n < pr.n - 1);
|
|
@@ -1400,6 +1437,7 @@ Commands:
|
|
|
1400
1437
|
show <ids...> Print hunks in full; accepts h003, h003..h010, or a parent id like h012
|
|
1401
1438
|
--file <path> Print every hunk of a file
|
|
1402
1439
|
links Cross-file links: which hunk uses a name another hunk defines
|
|
1440
|
+
rubric Print the agreed risk rubric (.spark-joy.md at the repo root)
|
|
1403
1441
|
validate Check plan.json against the hunks
|
|
1404
1442
|
--json Machine-readable output
|
|
1405
1443
|
serve Open the review app
|
|
@@ -1454,7 +1492,7 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1454
1492
|
});
|
|
1455
1493
|
const [cmd, ...rest] = positionals;
|
|
1456
1494
|
if (values.version) {
|
|
1457
|
-
const pkg = JSON.parse(await
|
|
1495
|
+
const pkg = JSON.parse(await readFile6(join6(PKG_ROOT, "package.json"), "utf8"));
|
|
1458
1496
|
console.log(pkg.version);
|
|
1459
1497
|
return 0;
|
|
1460
1498
|
}
|
|
@@ -1475,6 +1513,7 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1475
1513
|
if (r.untracked.length)
|
|
1476
1514
|
console.log(`note: ${r.untracked.length} untracked file(s) are not included; \`git add -N <file>\` to include them`);
|
|
1477
1515
|
if (await readPlanRaw(r.paths)) console.log("note: plan.json already exists and may be stale; run `spark-joy validate`");
|
|
1516
|
+
console.log(await readRubric(r.meta.repoRoot) ? `risk rubric: ${RUBRIC_FILE}` : "no risk rubric yet");
|
|
1478
1517
|
return 0;
|
|
1479
1518
|
}
|
|
1480
1519
|
case "outline": {
|
|
@@ -1495,11 +1534,19 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1495
1534
|
console.log(formatLinks(await readHunks(p), await readLinks(p)));
|
|
1496
1535
|
return 0;
|
|
1497
1536
|
}
|
|
1537
|
+
case "rubric": {
|
|
1538
|
+
const rubric = await readRubric(await repoRoot(cwd));
|
|
1539
|
+
console.log(rubric ? formatRubric(rubric) : `no risk rubric yet; agree one with the user and write ${RUBRIC_FILE}`);
|
|
1540
|
+
return 0;
|
|
1541
|
+
}
|
|
1498
1542
|
case "validate": {
|
|
1499
1543
|
const p = await run(cwd, values.run);
|
|
1500
1544
|
const raw = await readPlanRaw(p);
|
|
1501
1545
|
if (raw === void 0) throw new Error(`no plan yet; write ${p.plan}`);
|
|
1502
1546
|
const result = validatePlan(raw, await readHunks(p));
|
|
1547
|
+
const risky = result.plan?.strategies.some((s) => s.chunks.some((c) => c.risk !== "low"));
|
|
1548
|
+
if (risky && !await readRubric(await repoRoot(cwd)))
|
|
1549
|
+
result.issues.push({ level: "warning", where: "risk", message: `no risk rubric; agree one with the user and write ${RUBRIC_FILE}` });
|
|
1503
1550
|
if (values.json) {
|
|
1504
1551
|
console.log(JSON.stringify({ ok: result.ok, issues: result.issues }, null, 2));
|
|
1505
1552
|
} else {
|
|
@@ -1513,7 +1560,7 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1513
1560
|
case "serve": {
|
|
1514
1561
|
const p = await run(cwd, values.run);
|
|
1515
1562
|
const port = values.port ? Number(values.port) : 4848;
|
|
1516
|
-
const s = await serve({ paths: p, appDir:
|
|
1563
|
+
const s = await serve({ paths: p, appDir: join6(PKG_ROOT, "dist", "app"), port });
|
|
1517
1564
|
console.log(`spark-joy reviewing ${p.name} at ${s.url} (Ctrl+C to stop)`);
|
|
1518
1565
|
await new Promise((resolve) => process.once("SIGINT", resolve));
|
|
1519
1566
|
await s.close();
|
|
@@ -1548,13 +1595,13 @@ async function main(argv, cwd = process.cwd()) {
|
|
|
1548
1595
|
hunks,
|
|
1549
1596
|
plan: result.plan,
|
|
1550
1597
|
prefix,
|
|
1551
|
-
indexFile:
|
|
1598
|
+
indexFile: join6(p.dir, "split.index")
|
|
1552
1599
|
});
|
|
1553
1600
|
for (const pr of stack.prs)
|
|
1554
1601
|
console.log(`${String(pr.n).padStart(2)} ${pr.branch} ${pr.lines} lines, ${pr.files} files ${pr.chunk.title}`);
|
|
1555
1602
|
console.log(`${stack.prs.length} PRs; the last one matches your changes exactly`);
|
|
1556
1603
|
if (values.check !== void 0) {
|
|
1557
|
-
const tmp = await mkdtemp(
|
|
1604
|
+
const tmp = await mkdtemp(join6(tmpdir(), "spark-joy-check-"));
|
|
1558
1605
|
try {
|
|
1559
1606
|
const results = await checkStack(meta.repoRoot, stack, values.check, tmp);
|
|
1560
1607
|
for (const r of results) {
|
|
@@ -1579,9 +1626,9 @@ nothing created; add --apply to create the branches`);
|
|
|
1579
1626
|
return 0;
|
|
1580
1627
|
}
|
|
1581
1628
|
case "install-skill": {
|
|
1582
|
-
const skill =
|
|
1629
|
+
const skill = join6(PKG_ROOT, "plugin", "skills", "spark-joy", "SKILL.md");
|
|
1583
1630
|
if (values.print) {
|
|
1584
|
-
process.stdout.write(await
|
|
1631
|
+
process.stdout.write(await readFile6(skill, "utf8"));
|
|
1585
1632
|
return 0;
|
|
1586
1633
|
}
|
|
1587
1634
|
const homes = { claude: ".claude", agents: ".agents" };
|
|
@@ -1590,9 +1637,9 @@ nothing created; add --apply to create the branches`);
|
|
|
1590
1637
|
if (bad) throw new Error(`unknown agent ${bad}; use claude or agents`);
|
|
1591
1638
|
const root = values.project ? await repoRoot(cwd) : homedir();
|
|
1592
1639
|
for (const a of agents) {
|
|
1593
|
-
const dir =
|
|
1640
|
+
const dir = join6(root, homes[a], "skills", "spark-joy");
|
|
1594
1641
|
await mkdir3(dir, { recursive: true });
|
|
1595
|
-
await copyFile(skill,
|
|
1642
|
+
await copyFile(skill, join6(dir, "SKILL.md"));
|
|
1596
1643
|
console.log(`installed skill to ${dir}`);
|
|
1597
1644
|
}
|
|
1598
1645
|
console.log('ask your agent to "review this PR in chunks" or "split my changes into PRs"');
|