@ahmedalbarghouti/farq 0.0.2 → 0.1.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 +51 -13
- package/dist/concurrency.d.ts +2 -0
- package/dist/concurrency.js +16 -0
- package/dist/config.d.ts +16 -0
- package/dist/config.js +25 -0
- package/dist/git.d.ts +7 -3
- package/dist/git.js +36 -27
- package/dist/index.js +116 -33
- package/dist/open-pr.d.ts +6 -1
- package/dist/open-pr.js +42 -16
- package/dist/providers/claude.d.ts +1 -0
- package/dist/providers/claude.js +49 -32
- package/dist/providers/fake.d.ts +5 -2
- package/dist/providers/fake.js +26 -15
- package/dist/providers/index.d.ts +1 -1
- package/dist/providers/index.js +1 -1
- package/dist/render/pr.d.ts +6 -0
- package/dist/render/pr.js +29 -7
- package/dist/schema.d.ts +40 -7
- package/dist/schema.js +14 -1
- package/dist/summarize.js +7 -0
- package/dist/template.d.ts +6 -0
- package/dist/template.js +40 -19
- package/dist/ui/index.d.ts +1 -0
- package/dist/ui/index.js +1 -0
- package/dist/ui/lines.d.ts +19 -1
- package/dist/ui/lines.js +81 -39
- package/dist/ui/logger.d.ts +9 -2
- package/dist/ui/logger.js +15 -4
- package/dist/ui/spinner.d.ts +15 -2
- package/dist/ui/spinner.js +105 -22
- package/dist/ui/theme.d.ts +1 -0
- package/dist/ui/theme.js +3 -0
- package/dist/visual/chrome.js +21 -2
- package/dist/visual/cluster.d.ts +29 -0
- package/dist/visual/cluster.js +204 -0
- package/dist/visual/compose.d.ts +8 -1
- package/dist/visual/compose.js +17 -32
- package/dist/visual/css-scope.d.ts +6 -0
- package/dist/visual/css-scope.js +162 -0
- package/dist/visual/design.d.ts +103 -0
- package/dist/visual/design.js +327 -0
- package/dist/visual/diagram.d.ts +8 -0
- package/dist/visual/diagram.js +36 -14
- package/dist/visual/mockup.d.ts +16 -11
- package/dist/visual/mockup.js +45 -22
- package/dist/visual/pipeline.d.ts +35 -1
- package/dist/visual/pipeline.js +191 -69
- package/dist/visual/viewport.d.ts +16 -0
- package/dist/visual/viewport.js +19 -0
- package/package.json +1 -1
package/dist/open-pr.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { execa } from "execa";
|
|
5
5
|
import { fillPrTemplate, findPrTemplate } from "./template.js";
|
|
@@ -104,14 +104,19 @@ export async function openPullRequest(options) {
|
|
|
104
104
|
reason: `Already on ${defaultBranch} — skipping PR create`,
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
|
-
|
|
107
|
+
const paths = options.imagePaths && options.imagePaths.length > 0
|
|
108
|
+
? options.imagePaths
|
|
109
|
+
: options.imagePath
|
|
110
|
+
? [options.imagePath]
|
|
111
|
+
: [];
|
|
112
|
+
let imageUrls = [];
|
|
108
113
|
let warning;
|
|
109
|
-
if (
|
|
114
|
+
if (paths.length > 0) {
|
|
110
115
|
try {
|
|
111
|
-
|
|
116
|
+
imageUrls = await uploadPrImages(cwd, paths, branch);
|
|
112
117
|
}
|
|
113
118
|
catch (err) {
|
|
114
|
-
warning = `Image upload failed — attach manually: ${
|
|
119
|
+
warning = `Image upload failed — attach manually: ${paths.join(", ")} (${err instanceof Error ? err.message : String(err)})`;
|
|
115
120
|
}
|
|
116
121
|
}
|
|
117
122
|
const template = findPrTemplate(cwd);
|
|
@@ -119,7 +124,11 @@ export async function openPullRequest(options) {
|
|
|
119
124
|
template,
|
|
120
125
|
summary: options.summary,
|
|
121
126
|
bodyMarkdown: options.bodyMarkdown,
|
|
122
|
-
imageUrl,
|
|
127
|
+
imageUrl: imageUrls[0] ?? null,
|
|
128
|
+
images: imageUrls.map((url, i) => ({
|
|
129
|
+
url,
|
|
130
|
+
title: options.imageTitles?.[i],
|
|
131
|
+
})),
|
|
123
132
|
});
|
|
124
133
|
const dir = mkdtempSync(join(tmpdir(), "farq-pr-"));
|
|
125
134
|
const bodyFile = join(dir, "body.md");
|
|
@@ -149,7 +158,8 @@ export async function openPullRequest(options) {
|
|
|
149
158
|
warning,
|
|
150
159
|
title,
|
|
151
160
|
body,
|
|
152
|
-
imageUrl,
|
|
161
|
+
imageUrl: imageUrls[0] ?? null,
|
|
162
|
+
imageUrls,
|
|
153
163
|
};
|
|
154
164
|
}
|
|
155
165
|
const created = await gh(cwd, [
|
|
@@ -173,7 +183,8 @@ export async function openPullRequest(options) {
|
|
|
173
183
|
warning,
|
|
174
184
|
title,
|
|
175
185
|
body,
|
|
176
|
-
imageUrl,
|
|
186
|
+
imageUrl: imageUrls[0] ?? null,
|
|
187
|
+
imageUrls,
|
|
177
188
|
};
|
|
178
189
|
}
|
|
179
190
|
catch (err) {
|
|
@@ -225,8 +236,18 @@ export async function pruneOrphanedFarqAssets(cwd, keepTag) {
|
|
|
225
236
|
}
|
|
226
237
|
return deleted;
|
|
227
238
|
}
|
|
228
|
-
/** Best-effort: prerelease asset URL for
|
|
239
|
+
/** Best-effort: prerelease asset URL for one PNG. */
|
|
229
240
|
export async function uploadPrImage(cwd, imagePath, branch) {
|
|
241
|
+
const urls = await uploadPrImages(cwd, [imagePath], branch);
|
|
242
|
+
const url = urls[0];
|
|
243
|
+
if (!url)
|
|
244
|
+
throw new Error("no asset URL returned");
|
|
245
|
+
return url;
|
|
246
|
+
}
|
|
247
|
+
/** Best-effort: upload many PNGs to one prerelease; URLs in input order. */
|
|
248
|
+
export async function uploadPrImages(cwd, imagePaths, branch) {
|
|
249
|
+
if (imagePaths.length === 0)
|
|
250
|
+
return [];
|
|
230
251
|
const tag = assetTagForBranch(branch);
|
|
231
252
|
try {
|
|
232
253
|
await gh(cwd, ["release", "delete", tag, "--yes", "--cleanup-tag"], {
|
|
@@ -240,7 +261,7 @@ export async function uploadPrImage(cwd, imagePath, branch) {
|
|
|
240
261
|
"release",
|
|
241
262
|
"create",
|
|
242
263
|
tag,
|
|
243
|
-
|
|
264
|
+
...imagePaths,
|
|
244
265
|
"--title",
|
|
245
266
|
`farq assets (${branch})`,
|
|
246
267
|
"--notes",
|
|
@@ -253,12 +274,17 @@ export async function uploadPrImage(cwd, imagePath, branch) {
|
|
|
253
274
|
tag,
|
|
254
275
|
"--json",
|
|
255
276
|
"assets",
|
|
256
|
-
"--jq",
|
|
257
|
-
".assets[0].url",
|
|
258
277
|
]);
|
|
259
|
-
const
|
|
260
|
-
|
|
261
|
-
|
|
278
|
+
const parsed = JSON.parse(stdout);
|
|
279
|
+
const assets = parsed.assets ?? [];
|
|
280
|
+
const urls = imagePaths.map((p) => {
|
|
281
|
+
const name = basename(p);
|
|
282
|
+
const hit = assets.find((a) => a.name === name);
|
|
283
|
+
if (!hit?.url) {
|
|
284
|
+
throw new Error(`no asset URL for ${name}`);
|
|
285
|
+
}
|
|
286
|
+
return hit.url;
|
|
287
|
+
});
|
|
262
288
|
await pruneOrphanedFarqAssets(cwd, tag);
|
|
263
|
-
return
|
|
289
|
+
return urls;
|
|
264
290
|
}
|
package/dist/providers/claude.js
CHANGED
|
@@ -1,58 +1,75 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
2
|
const TIMEOUT_MS = 120_000;
|
|
3
|
+
/**
|
|
4
|
+
* farq only ever asks for JSON back, so the user's MCP servers, skills, hooks
|
|
5
|
+
* and project CLAUDE.md are pure startup cost — and a source of drift in the
|
|
6
|
+
* output. Skipping them roughly halves per-call latency.
|
|
7
|
+
*/
|
|
8
|
+
const FAST_ARGS = ["--strict-mcp-config", "--setting-sources", ""];
|
|
9
|
+
/** Tried in order; each falls back to the next when the CLI rejects a flag. */
|
|
10
|
+
const ARG_VARIANTS = [
|
|
11
|
+
FAST_ARGS,
|
|
12
|
+
[],
|
|
13
|
+
// Empty allowedTools hangs on some versions, so it stays the last resort.
|
|
14
|
+
["--allowedTools", ""],
|
|
15
|
+
];
|
|
16
|
+
export const UNSUPPORTED_FLAG = /unknown option|unknown argument|unrecognized|allowedTools|setting-sources|strict-mcp-config/i;
|
|
3
17
|
export async function complete(prompt, options = {}) {
|
|
4
18
|
const baseArgs = ["-p", "--output-format", "json", "--max-turns", "1"];
|
|
5
19
|
if (options.model)
|
|
6
20
|
baseArgs.push("--model", options.model);
|
|
7
|
-
// Prefer without allowedTools first — empty allowedTools hangs/fails on some versions.
|
|
8
|
-
const attempts = [[...baseArgs], [...baseArgs, "--allowedTools", ""]];
|
|
9
21
|
let lastErr;
|
|
10
|
-
for (
|
|
22
|
+
for (let i = 0; i < ARG_VARIANTS.length; i++) {
|
|
23
|
+
const canRetry = i < ARG_VARIANTS.length - 1;
|
|
24
|
+
const args = [...baseArgs, ...ARG_VARIANTS[i]];
|
|
25
|
+
let result;
|
|
11
26
|
try {
|
|
12
|
-
|
|
27
|
+
result = await execa("claude", args, {
|
|
13
28
|
input: prompt,
|
|
14
29
|
timeout: TIMEOUT_MS,
|
|
15
30
|
reject: false,
|
|
16
31
|
stdout: "pipe",
|
|
17
32
|
stderr: "pipe",
|
|
18
33
|
});
|
|
19
|
-
if (result.timedOut) {
|
|
20
|
-
throw new Error("claude did not respond — check that it's authenticated (`claude` login)");
|
|
21
|
-
}
|
|
22
|
-
const text = (result.stdout || result.stderr || "").trim();
|
|
23
|
-
if (!text && result.exitCode !== 0) {
|
|
24
|
-
throw new Error(`claude failed (exit ${result.exitCode}): ${(result.stderr || "no output").trim()}`);
|
|
25
|
-
}
|
|
26
|
-
try {
|
|
27
|
-
return parseClaudeOutput(text);
|
|
28
|
-
}
|
|
29
|
-
catch (parseErr) {
|
|
30
|
-
// Retry next arg set only when allowedTools looks unsupported
|
|
31
|
-
if (args.includes("--allowedTools") &&
|
|
32
|
-
/allowedTools|unknown option|unexpected/i.test(`${result.stderr ?? ""} ${text}`)) {
|
|
33
|
-
lastErr = parseErr;
|
|
34
|
-
continue;
|
|
35
|
-
}
|
|
36
|
-
throw parseErr;
|
|
37
|
-
}
|
|
38
34
|
}
|
|
39
35
|
catch (err) {
|
|
40
36
|
const e = err;
|
|
41
|
-
if (e.timedOut
|
|
42
|
-
throw
|
|
37
|
+
if (e.timedOut)
|
|
38
|
+
throw notResponding();
|
|
39
|
+
if (canRetry && UNSUPPORTED_FLAG.test(e.message ?? "")) {
|
|
40
|
+
lastErr = err;
|
|
41
|
+
continue;
|
|
43
42
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
43
|
+
throw err instanceof Error ? err : new Error(String(err));
|
|
44
|
+
}
|
|
45
|
+
if (result.timedOut)
|
|
46
|
+
throw notResponding();
|
|
47
|
+
const text = (result.stdout || result.stderr || "").trim();
|
|
48
|
+
const diagnostics = `${result.stderr ?? ""} ${text}`;
|
|
49
|
+
if (!text && result.exitCode !== 0) {
|
|
50
|
+
const err = new Error(`claude failed (exit ${result.exitCode}): ${(result.stderr || "no output").trim()}`);
|
|
51
|
+
if (canRetry && UNSUPPORTED_FLAG.test(diagnostics)) {
|
|
52
|
+
lastErr = err;
|
|
53
|
+
continue;
|
|
51
54
|
}
|
|
55
|
+
throw err;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
return parseClaudeOutput(text);
|
|
59
|
+
}
|
|
60
|
+
catch (parseErr) {
|
|
61
|
+
if (canRetry && UNSUPPORTED_FLAG.test(diagnostics)) {
|
|
62
|
+
lastErr = parseErr;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
throw parseErr;
|
|
52
66
|
}
|
|
53
67
|
}
|
|
54
68
|
throw lastErr instanceof Error ? lastErr : new Error("claude failed");
|
|
55
69
|
}
|
|
70
|
+
function notResponding() {
|
|
71
|
+
return new Error("claude did not respond — check that it's authenticated (`claude` login)");
|
|
72
|
+
}
|
|
56
73
|
function parseClaudeOutput(stdout) {
|
|
57
74
|
try {
|
|
58
75
|
const envelope = JSON.parse(stdout);
|
package/dist/providers/fake.d.ts
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { ChangeSummary } from "../schema.js";
|
|
2
2
|
export declare const FAKE_SUMMARY: ChangeSummary;
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
3
|
+
export declare const FAKE_MOCKUP_CSS = ".page{padding:32px;background:var(--fq-surface)}\n.card{max-width:340px;padding:24px;background:var(--fq-surface-alt);border:1px solid var(--fq-border);border-radius:var(--fq-radius)}\n.card h1{margin:0 0 8px;font-family:var(--fq-font-display);font-size:20px}\n.card p{margin:0 0 8px;color:var(--fq-text-muted)}\n.card p.status{margin:0;color:var(--fq-positive)}";
|
|
4
|
+
export declare const FAKE_BEFORE_BODY = "<div class=\"page\"><div class=\"card\"><h1>Order #1042</h1><p>Total: $48.00</p></div></div>";
|
|
5
|
+
export declare const FAKE_AFTER_BODY = "<div class=\"page\"><div class=\"card\"><h1>Order #1042</h1><p>Total: $48.00</p><p class=\"status\">Refund status: pending</p></div></div>";
|
|
6
|
+
export declare const FAKE_DIAGRAM_CSS = ".cols{display:flex;gap:24px}\n.col{flex:1;padding:16px;border:1px solid var(--fq-border);border-radius:var(--fq-radius)}\n.step{padding:8px 12px;margin-bottom:8px;background:var(--fq-surface-alt);border-radius:var(--fq-radius-sm)}\n.step--new{background:var(--fq-accent-soft);color:var(--fq-accent)}";
|
|
7
|
+
export declare const FAKE_DIAGRAM_BODY = "<div class=\"cols\"><div class=\"col\"><div class=\"step\">Fetch order</div><div class=\"step\">Return payload</div></div><div class=\"col\"><div class=\"step\">Fetch order</div><div class=\"step step--new\">Attach refund status</div><div class=\"step\">Return payload</div></div></div>";
|
|
5
8
|
export type CompleteOptions = {
|
|
6
9
|
model?: string;
|
|
7
10
|
};
|
package/dist/providers/fake.js
CHANGED
|
@@ -18,25 +18,36 @@ export const FAKE_SUMMARY = {
|
|
|
18
18
|
],
|
|
19
19
|
breaking_changes: [],
|
|
20
20
|
visual_notes: "Show a simple status badge on the order card.",
|
|
21
|
+
visual_topics: [{ title: "Refund status on orders", item_indices: [0, 1] }],
|
|
21
22
|
};
|
|
22
|
-
export const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
export const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
23
|
+
export const FAKE_MOCKUP_CSS = `.page{padding:32px;background:var(--fq-surface)}
|
|
24
|
+
.card{max-width:340px;padding:24px;background:var(--fq-surface-alt);border:1px solid var(--fq-border);border-radius:var(--fq-radius)}
|
|
25
|
+
.card h1{margin:0 0 8px;font-family:var(--fq-font-display);font-size:20px}
|
|
26
|
+
.card p{margin:0 0 8px;color:var(--fq-text-muted)}
|
|
27
|
+
.card p.status{margin:0;color:var(--fq-positive)}`;
|
|
28
|
+
export const FAKE_BEFORE_BODY = `<div class="page"><div class="card"><h1>Order #1042</h1><p>Total: $48.00</p></div></div>`;
|
|
29
|
+
export const FAKE_AFTER_BODY = `<div class="page"><div class="card"><h1>Order #1042</h1><p>Total: $48.00</p><p class="status">Refund status: pending</p></div></div>`;
|
|
30
|
+
export const FAKE_DIAGRAM_CSS = `.cols{display:flex;gap:24px}
|
|
31
|
+
.col{flex:1;padding:16px;border:1px solid var(--fq-border);border-radius:var(--fq-radius)}
|
|
32
|
+
.step{padding:8px 12px;margin-bottom:8px;background:var(--fq-surface-alt);border-radius:var(--fq-radius-sm)}
|
|
33
|
+
.step--new{background:var(--fq-accent-soft);color:var(--fq-accent)}`;
|
|
34
|
+
export const FAKE_DIAGRAM_BODY = `<div class="cols"><div class="col"><div class="step">Fetch order</div><div class="step">Return payload</div></div><div class="col"><div class="step">Fetch order</div><div class="step step--new">Attach refund status</div><div class="step">Return payload</div></div></div>`;
|
|
33
35
|
export async function complete(prompt, _options = {}) {
|
|
34
|
-
if (prompt.includes("
|
|
36
|
+
if (prompt.includes("before_body")) {
|
|
35
37
|
return JSON.stringify({
|
|
36
38
|
feasible: true,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
39
|
+
css: FAKE_MOCKUP_CSS,
|
|
40
|
+
before_body: FAKE_BEFORE_BODY,
|
|
41
|
+
after_body: FAKE_AFTER_BODY,
|
|
42
|
+
stage_width: 620,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
if (prompt.includes("flowchart")) {
|
|
46
|
+
return JSON.stringify({
|
|
47
|
+
feasible: true,
|
|
48
|
+
css: FAKE_DIAGRAM_CSS,
|
|
49
|
+
body: FAKE_DIAGRAM_BODY,
|
|
50
|
+
stage_width: 900,
|
|
40
51
|
});
|
|
41
52
|
}
|
|
42
53
|
return JSON.stringify(FAKE_SUMMARY);
|
|
@@ -19,4 +19,4 @@ export declare function detectProviders(): Promise<{
|
|
|
19
19
|
}>;
|
|
20
20
|
export declare function resolveProvider(options?: ResolveProviderOptions): Promise<Provider>;
|
|
21
21
|
export declare function getProvider(name: ProviderName): Provider;
|
|
22
|
-
export { FAKE_SUMMARY,
|
|
22
|
+
export { FAKE_SUMMARY, FAKE_MOCKUP_CSS, FAKE_BEFORE_BODY, FAKE_AFTER_BODY, FAKE_DIAGRAM_CSS, FAKE_DIAGRAM_BODY, } from "./fake.js";
|
package/dist/providers/index.js
CHANGED
|
@@ -37,4 +37,4 @@ export function getProvider(name) {
|
|
|
37
37
|
return { name, complete: fake.complete };
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
export { FAKE_SUMMARY,
|
|
40
|
+
export { FAKE_SUMMARY, FAKE_MOCKUP_CSS, FAKE_BEFORE_BODY, FAKE_AFTER_BODY, FAKE_DIAGRAM_CSS, FAKE_DIAGRAM_BODY, } from "./fake.js";
|
package/dist/render/pr.d.ts
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
import type { ChangeSummary } from "../schema.js";
|
|
2
|
+
export type PrImage = {
|
|
3
|
+
path: string;
|
|
4
|
+
title?: string;
|
|
5
|
+
};
|
|
2
6
|
export type RenderPrOptions = {
|
|
3
7
|
summary: ChangeSummary;
|
|
8
|
+
/** @deprecated prefer images */
|
|
4
9
|
imagePath?: string | null;
|
|
5
10
|
imageAlt?: string;
|
|
11
|
+
images?: PrImage[];
|
|
6
12
|
};
|
|
7
13
|
export declare function renderPr(options: RenderPrOptions): string;
|
package/dist/render/pr.js
CHANGED
|
@@ -18,13 +18,27 @@ export function renderPr(options) {
|
|
|
18
18
|
out.push("");
|
|
19
19
|
out.push(overflow);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
21
|
+
const images = normalizeImages(options);
|
|
22
|
+
if (images.length > 0) {
|
|
23
|
+
const anyLocal = images.some((img) => !isHostedImageUrl(img.path));
|
|
24
|
+
if (images.length === 1) {
|
|
25
|
+
out.push("");
|
|
26
|
+
out.push("### Before / After");
|
|
27
|
+
out.push("");
|
|
28
|
+
const alt = images[0].title ?? options.imageAlt ?? "before / after";
|
|
29
|
+
out.push(``);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
out.push("");
|
|
33
|
+
out.push("### Visuals");
|
|
34
|
+
for (const img of images) {
|
|
35
|
+
out.push("");
|
|
36
|
+
out.push(`#### ${img.title ?? "Before / After"}`);
|
|
37
|
+
out.push("");
|
|
38
|
+
out.push(``);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (anyLocal) {
|
|
28
42
|
out.push("");
|
|
29
43
|
out.push(LOCAL_IMAGE_NOTE);
|
|
30
44
|
}
|
|
@@ -49,3 +63,11 @@ export function renderPr(options) {
|
|
|
49
63
|
}
|
|
50
64
|
return `${title}\n\n${out.join("\n").trim()}\n`;
|
|
51
65
|
}
|
|
66
|
+
function normalizeImages(options) {
|
|
67
|
+
if (options.images && options.images.length > 0)
|
|
68
|
+
return options.images;
|
|
69
|
+
if (options.imagePath) {
|
|
70
|
+
return [{ path: options.imagePath, title: options.imageAlt }];
|
|
71
|
+
}
|
|
72
|
+
return [];
|
|
73
|
+
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -7,18 +7,32 @@ export declare const ChangeItemSchema: z.ZodObject<{
|
|
|
7
7
|
why_it_matters: z.ZodOptional<z.ZodString>;
|
|
8
8
|
files: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
9
9
|
}, "strip", z.ZodTypeAny, {
|
|
10
|
+
description: string;
|
|
10
11
|
files: string[];
|
|
11
12
|
category: "feature" | "fix" | "improvement" | "refactor" | "chore" | "docs" | "perf" | "security";
|
|
12
13
|
title: string;
|
|
13
|
-
description: string;
|
|
14
14
|
why_it_matters?: string | undefined;
|
|
15
15
|
}, {
|
|
16
|
+
description: string;
|
|
16
17
|
category: "feature" | "fix" | "improvement" | "refactor" | "chore" | "docs" | "perf" | "security";
|
|
17
18
|
title: string;
|
|
18
|
-
description: string;
|
|
19
19
|
files?: string[] | undefined;
|
|
20
20
|
why_it_matters?: string | undefined;
|
|
21
21
|
}>;
|
|
22
|
+
/**
|
|
23
|
+
* Grouping of items into visual topics, returned by the same call that writes
|
|
24
|
+
* the summary so the visual pipeline does not need its own round-trip.
|
|
25
|
+
*/
|
|
26
|
+
export declare const VisualTopicSchema: z.ZodObject<{
|
|
27
|
+
title: z.ZodString;
|
|
28
|
+
item_indices: z.ZodArray<z.ZodNumber, "many">;
|
|
29
|
+
}, "strip", z.ZodTypeAny, {
|
|
30
|
+
title: string;
|
|
31
|
+
item_indices: number[];
|
|
32
|
+
}, {
|
|
33
|
+
title: string;
|
|
34
|
+
item_indices: number[];
|
|
35
|
+
}>;
|
|
22
36
|
export declare const ChangeSummarySchema: z.ZodObject<{
|
|
23
37
|
headline: z.ZodString;
|
|
24
38
|
overview: z.ZodString;
|
|
@@ -29,46 +43,65 @@ export declare const ChangeSummarySchema: z.ZodObject<{
|
|
|
29
43
|
why_it_matters: z.ZodOptional<z.ZodString>;
|
|
30
44
|
files: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
31
45
|
}, "strip", z.ZodTypeAny, {
|
|
46
|
+
description: string;
|
|
32
47
|
files: string[];
|
|
33
48
|
category: "feature" | "fix" | "improvement" | "refactor" | "chore" | "docs" | "perf" | "security";
|
|
34
49
|
title: string;
|
|
35
|
-
description: string;
|
|
36
50
|
why_it_matters?: string | undefined;
|
|
37
51
|
}, {
|
|
52
|
+
description: string;
|
|
38
53
|
category: "feature" | "fix" | "improvement" | "refactor" | "chore" | "docs" | "perf" | "security";
|
|
39
54
|
title: string;
|
|
40
|
-
description: string;
|
|
41
55
|
files?: string[] | undefined;
|
|
42
56
|
why_it_matters?: string | undefined;
|
|
43
57
|
}>, "many">;
|
|
44
58
|
breaking_changes: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
45
59
|
visual_notes: z.ZodOptional<z.ZodString>;
|
|
60
|
+
visual_topics: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
61
|
+
title: z.ZodString;
|
|
62
|
+
item_indices: z.ZodArray<z.ZodNumber, "many">;
|
|
63
|
+
}, "strip", z.ZodTypeAny, {
|
|
64
|
+
title: string;
|
|
65
|
+
item_indices: number[];
|
|
66
|
+
}, {
|
|
67
|
+
title: string;
|
|
68
|
+
item_indices: number[];
|
|
69
|
+
}>, "many">>;
|
|
46
70
|
}, "strip", z.ZodTypeAny, {
|
|
47
71
|
headline: string;
|
|
48
72
|
overview: string;
|
|
49
73
|
items: {
|
|
74
|
+
description: string;
|
|
50
75
|
files: string[];
|
|
51
76
|
category: "feature" | "fix" | "improvement" | "refactor" | "chore" | "docs" | "perf" | "security";
|
|
52
77
|
title: string;
|
|
53
|
-
description: string;
|
|
54
78
|
why_it_matters?: string | undefined;
|
|
55
79
|
}[];
|
|
56
80
|
breaking_changes: string[];
|
|
57
81
|
visual_notes?: string | undefined;
|
|
82
|
+
visual_topics?: {
|
|
83
|
+
title: string;
|
|
84
|
+
item_indices: number[];
|
|
85
|
+
}[] | undefined;
|
|
58
86
|
}, {
|
|
59
87
|
headline: string;
|
|
60
88
|
overview: string;
|
|
61
89
|
items: {
|
|
90
|
+
description: string;
|
|
62
91
|
category: "feature" | "fix" | "improvement" | "refactor" | "chore" | "docs" | "perf" | "security";
|
|
63
92
|
title: string;
|
|
64
|
-
description: string;
|
|
65
93
|
files?: string[] | undefined;
|
|
66
94
|
why_it_matters?: string | undefined;
|
|
67
95
|
}[];
|
|
68
96
|
breaking_changes?: string[] | undefined;
|
|
69
97
|
visual_notes?: string | undefined;
|
|
98
|
+
visual_topics?: {
|
|
99
|
+
title: string;
|
|
100
|
+
item_indices: number[];
|
|
101
|
+
}[] | undefined;
|
|
70
102
|
}>;
|
|
71
103
|
export type ChangeSummary = z.infer<typeof ChangeSummarySchema>;
|
|
72
104
|
export type ChangeItem = z.infer<typeof ChangeItemSchema>;
|
|
105
|
+
export type VisualTopicHint = z.infer<typeof VisualTopicSchema>;
|
|
73
106
|
/** Compact schema description embedded in AI prompts. */
|
|
74
|
-
export declare const CHANGE_SUMMARY_SCHEMA_PROMPT = "{\n \"headline\": \"string <=140 \u2014 one-line summary (PR title)\",\n \"overview\": \"string \u2014 1-3 sentences in requested tone\",\n \"items\": [{\n \"category\": \"feature|fix|improvement|refactor|chore|docs|perf|security\",\n \"title\": \"string <=120\",\n \"description\": \"string\",\n \"why_it_matters\": \"string (optional; required for client tone)\",\n \"files\": [\"string\"]\n }],\n \"breaking_changes\": [\"string\"],\n \"visual_notes\": \"string (optional)\"\n}";
|
|
107
|
+
export declare const CHANGE_SUMMARY_SCHEMA_PROMPT = "{\n \"headline\": \"string <=140 \u2014 one-line summary (PR title)\",\n \"overview\": \"string \u2014 1-3 sentences in requested tone\",\n \"items\": [{\n \"category\": \"feature|fix|improvement|refactor|chore|docs|perf|security\",\n \"title\": \"string <=120\",\n \"description\": \"string\",\n \"why_it_matters\": \"string (optional; required for client tone)\",\n \"files\": [\"string\"]\n }],\n \"breaking_changes\": [\"string\"],\n \"visual_notes\": \"string (optional)\",\n \"visual_topics\": [{\n \"title\": \"string <=80 \u2014 human topic name\",\n \"item_indices\": [0]\n }]\n}";
|
package/dist/schema.js
CHANGED
|
@@ -16,12 +16,21 @@ export const ChangeItemSchema = z.object({
|
|
|
16
16
|
why_it_matters: z.string().min(1).optional(),
|
|
17
17
|
files: z.array(z.string()).default([]),
|
|
18
18
|
});
|
|
19
|
+
/**
|
|
20
|
+
* Grouping of items into visual topics, returned by the same call that writes
|
|
21
|
+
* the summary so the visual pipeline does not need its own round-trip.
|
|
22
|
+
*/
|
|
23
|
+
export const VisualTopicSchema = z.object({
|
|
24
|
+
title: z.string().min(1).max(80),
|
|
25
|
+
item_indices: z.array(z.number().int().nonnegative()).min(1),
|
|
26
|
+
});
|
|
19
27
|
export const ChangeSummarySchema = z.object({
|
|
20
28
|
headline: z.string().min(1).max(140),
|
|
21
29
|
overview: z.string().min(1),
|
|
22
30
|
items: z.array(ChangeItemSchema).min(1),
|
|
23
31
|
breaking_changes: z.array(z.string()).default([]),
|
|
24
32
|
visual_notes: z.string().min(1).optional(),
|
|
33
|
+
visual_topics: z.array(VisualTopicSchema).optional(),
|
|
25
34
|
});
|
|
26
35
|
/** Compact schema description embedded in AI prompts. */
|
|
27
36
|
export const CHANGE_SUMMARY_SCHEMA_PROMPT = `{
|
|
@@ -35,5 +44,9 @@ export const CHANGE_SUMMARY_SCHEMA_PROMPT = `{
|
|
|
35
44
|
"files": ["string"]
|
|
36
45
|
}],
|
|
37
46
|
"breaking_changes": ["string"],
|
|
38
|
-
"visual_notes": "string (optional)"
|
|
47
|
+
"visual_notes": "string (optional)",
|
|
48
|
+
"visual_topics": [{
|
|
49
|
+
"title": "string <=80 — human topic name",
|
|
50
|
+
"item_indices": [0]
|
|
51
|
+
}]
|
|
39
52
|
}`;
|
package/dist/summarize.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { extractJson } from "./extract-json.js";
|
|
2
2
|
import { CHANGE_SUMMARY_SCHEMA_PROMPT, ChangeSummarySchema, } from "./schema.js";
|
|
3
|
+
import { MAX_VISUAL_TOPICS } from "./visual/cluster.js";
|
|
3
4
|
export class SummarizeError extends Error {
|
|
4
5
|
exitCode = 2;
|
|
5
6
|
constructor(message) {
|
|
@@ -59,6 +60,12 @@ Rules:
|
|
|
59
60
|
- Report only what the diff shows. Do not invent work.
|
|
60
61
|
- ${toneRules}
|
|
61
62
|
${convention}
|
|
63
|
+
visual_topics — how the items get grouped into before/after visuals:
|
|
64
|
+
- Every item index must appear in exactly one topic. Use 0-based indices into your own items array.
|
|
65
|
+
- If all items advance the SAME feature or story, return exactly ONE topic containing every index, even across many files, tests, docs and plumbing.
|
|
66
|
+
- Only split when a reviewer would call them separate product changes (say, a UI redesign AND an unrelated billing API).
|
|
67
|
+
- At most ${MAX_VISUAL_TOPICS} topics. Fewer is better; one is right when unsure.
|
|
68
|
+
|
|
62
69
|
Schema:
|
|
63
70
|
${CHANGE_SUMMARY_SCHEMA_PROMPT}
|
|
64
71
|
|
package/dist/template.d.ts
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ChangeSummary } from "./schema.js";
|
|
2
2
|
export declare function findPrTemplate(cwd: string): string | null;
|
|
3
|
+
export type TemplateImage = {
|
|
4
|
+
url: string;
|
|
5
|
+
title?: string;
|
|
6
|
+
};
|
|
3
7
|
export declare function fillPrTemplate(options: {
|
|
4
8
|
template: string | null;
|
|
5
9
|
summary: ChangeSummary;
|
|
6
10
|
bodyMarkdown: string;
|
|
11
|
+
/** @deprecated prefer images */
|
|
7
12
|
imageUrl?: string | null;
|
|
13
|
+
images?: TemplateImage[];
|
|
8
14
|
}): {
|
|
9
15
|
title: string;
|
|
10
16
|
body: string;
|
package/dist/template.js
CHANGED
|
@@ -19,23 +19,16 @@ export function findPrTemplate(cwd) {
|
|
|
19
19
|
}
|
|
20
20
|
export function fillPrTemplate(options) {
|
|
21
21
|
const { title, overflow } = truncateTitle(options.summary.headline, GITHUB_TITLE_MAX);
|
|
22
|
+
const images = normalizeTemplateImages(options);
|
|
22
23
|
let body = options.template?.trim() || "";
|
|
23
24
|
if (!body) {
|
|
24
25
|
body = options.bodyMarkdown;
|
|
25
26
|
if (overflow)
|
|
26
27
|
body = overflow + "\n\n" + body;
|
|
27
|
-
if (
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
body = body.replace(re, "");
|
|
32
|
-
}
|
|
33
|
-
else {
|
|
34
|
-
body =
|
|
35
|
-
"### Before / After\n\n\n\n" +
|
|
38
|
-
body;
|
|
28
|
+
if (images.length > 0) {
|
|
29
|
+
body = replaceLocalImages(body, images);
|
|
30
|
+
if (!hasMarkdownImage(body)) {
|
|
31
|
+
body = renderImageMarkdown(images) + "\n\n" + body;
|
|
39
32
|
}
|
|
40
33
|
body = stripLocalImageNote(body);
|
|
41
34
|
}
|
|
@@ -51,20 +44,48 @@ export function fillPrTemplate(options) {
|
|
|
51
44
|
body = fillSection(body, /test\s*plan|testing|how to test/i, testPlan);
|
|
52
45
|
if (overflow)
|
|
53
46
|
body = overflow + "\n\n" + body;
|
|
54
|
-
if (
|
|
55
|
-
|
|
56
|
-
|
|
47
|
+
if (images.length > 0) {
|
|
48
|
+
const md = renderImageMarkdown(images);
|
|
49
|
+
if (/before\s*\/\s*after|screenshots?|visuals?/i.test(body)) {
|
|
50
|
+
body = fillSection(body, /before\s*\/\s*after|screenshots?|visuals?/i, md.replace(/^###[^\n]+\n+/, "").trim());
|
|
57
51
|
}
|
|
58
52
|
else {
|
|
59
|
-
body +=
|
|
60
|
-
"\n\n### Before / After\n\n\n";
|
|
53
|
+
body += "\n\n" + md + "\n";
|
|
63
54
|
}
|
|
64
55
|
body = stripLocalImageNote(body);
|
|
65
56
|
}
|
|
66
57
|
return { title, body };
|
|
67
58
|
}
|
|
59
|
+
function normalizeTemplateImages(options) {
|
|
60
|
+
if (options.images && options.images.length > 0)
|
|
61
|
+
return options.images;
|
|
62
|
+
if (options.imageUrl)
|
|
63
|
+
return [{ url: options.imageUrl }];
|
|
64
|
+
return [];
|
|
65
|
+
}
|
|
66
|
+
function hasMarkdownImage(body) {
|
|
67
|
+
return /!\[[^\]]*\]\([^)]+\)/.test(body);
|
|
68
|
+
}
|
|
69
|
+
function replaceLocalImages(body, images) {
|
|
70
|
+
let i = 0;
|
|
71
|
+
return body.replace(/!\[([^\]]*)\]\((?!https?:\/\/)[^)]+\)/g, (_m, alt) => {
|
|
72
|
+
const img = images[Math.min(i, images.length - 1)];
|
|
73
|
+
i += 1;
|
|
74
|
+
return ``;
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
function renderImageMarkdown(images) {
|
|
78
|
+
if (images.length === 1) {
|
|
79
|
+
const alt = images[0].title ?? "before / after";
|
|
80
|
+
return `### Before / After\n\n`;
|
|
81
|
+
}
|
|
82
|
+
const parts = ["### Visuals"];
|
|
83
|
+
for (const img of images) {
|
|
84
|
+
const alt = img.title ?? "before / after";
|
|
85
|
+
parts.push("", `#### ${alt}`, "", ``);
|
|
86
|
+
}
|
|
87
|
+
return parts.join("\n");
|
|
88
|
+
}
|
|
68
89
|
function fillSection(template, heading, content) {
|
|
69
90
|
const lines = template.split("\n");
|
|
70
91
|
let inSection = false;
|
package/dist/ui/index.d.ts
CHANGED
package/dist/ui/index.js
CHANGED