@ahmedalbarghouti/farq 0.0.1 → 0.0.3
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 +10 -20
- package/dist/index.js +57 -42
- package/dist/open-pr.d.ts +16 -1
- package/dist/open-pr.js +128 -37
- package/dist/paths.d.ts +12 -0
- package/dist/paths.js +38 -0
- package/dist/render/pr.d.ts +6 -0
- package/dist/render/pr.js +33 -8
- package/dist/template.d.ts +6 -0
- package/dist/template.js +43 -18
- package/dist/tools.d.ts +21 -0
- package/dist/tools.js +95 -0
- package/dist/ui/index.d.ts +2 -0
- package/dist/ui/index.js +2 -0
- package/dist/ui/lines.d.ts +3 -0
- package/dist/ui/lines.js +48 -0
- package/dist/ui/logger.d.ts +12 -0
- package/dist/ui/logger.js +20 -0
- package/dist/ui/spinner.d.ts +7 -0
- package/dist/ui/spinner.js +57 -0
- package/dist/ui/theme.d.ts +6 -0
- package/dist/ui/theme.js +19 -0
- package/dist/visual/chrome.js +35 -17
- package/dist/visual/cluster.d.ts +23 -0
- package/dist/visual/cluster.js +180 -0
- package/dist/visual/compose.d.ts +2 -0
- package/dist/visual/compose.js +8 -5
- package/dist/visual/diagram.d.ts +1 -0
- package/dist/visual/diagram.js +6 -2
- package/dist/visual/mockup.d.ts +2 -0
- package/dist/visual/mockup.js +9 -5
- package/dist/visual/pipeline.d.ts +5 -0
- package/dist/visual/pipeline.js +156 -69
- package/dist/visual/viewport.d.ts +16 -0
- package/dist/visual/viewport.js +19 -0
- package/package.json +2 -1
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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { truncateTitle, GITHUB_TITLE_MAX } from "../title.js";
|
|
2
|
+
import { isHostedImageUrl, LOCAL_IMAGE_NOTE } from "../tools.js";
|
|
2
3
|
const EMOJI = {
|
|
3
4
|
feature: "\u2728",
|
|
4
5
|
fix: "\uD83D\uDC1B",
|
|
@@ -17,14 +18,30 @@ export function renderPr(options) {
|
|
|
17
18
|
out.push("");
|
|
18
19
|
out.push(overflow);
|
|
19
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) {
|
|
42
|
+
out.push("");
|
|
43
|
+
out.push(LOCAL_IMAGE_NOTE);
|
|
44
|
+
}
|
|
28
45
|
}
|
|
29
46
|
out.push("");
|
|
30
47
|
out.push("### Changes");
|
|
@@ -46,3 +63,11 @@ export function renderPr(options) {
|
|
|
46
63
|
}
|
|
47
64
|
return `${title}\n\n${out.join("\n").trim()}\n`;
|
|
48
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/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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { truncateTitle, GITHUB_TITLE_MAX } from "./title.js";
|
|
4
|
+
import { stripLocalImageNote } from "./tools.js";
|
|
4
5
|
export function findPrTemplate(cwd) {
|
|
5
6
|
const single = join(cwd, ".github", "pull_request_template.md");
|
|
6
7
|
if (existsSync(single))
|
|
@@ -18,23 +19,18 @@ export function findPrTemplate(cwd) {
|
|
|
18
19
|
}
|
|
19
20
|
export function fillPrTemplate(options) {
|
|
20
21
|
const { title, overflow } = truncateTitle(options.summary.headline, GITHUB_TITLE_MAX);
|
|
22
|
+
const images = normalizeTemplateImages(options);
|
|
21
23
|
let body = options.template?.trim() || "";
|
|
22
24
|
if (!body) {
|
|
23
25
|
body = options.bodyMarkdown;
|
|
24
26
|
if (overflow)
|
|
25
27
|
body = overflow + "\n\n" + body;
|
|
26
|
-
if (
|
|
27
|
-
|
|
28
|
-
if (
|
|
29
|
-
body =
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
body =
|
|
33
|
-
"### Before / After\n\n\n\n" +
|
|
36
|
-
body;
|
|
28
|
+
if (images.length > 0) {
|
|
29
|
+
body = replaceLocalImages(body, images);
|
|
30
|
+
if (!hasMarkdownImage(body)) {
|
|
31
|
+
body = renderImageMarkdown(images) + "\n\n" + body;
|
|
37
32
|
}
|
|
33
|
+
body = stripLocalImageNote(body);
|
|
38
34
|
}
|
|
39
35
|
return { title, body };
|
|
40
36
|
}
|
|
@@ -48,19 +44,48 @@ export function fillPrTemplate(options) {
|
|
|
48
44
|
body = fillSection(body, /test\s*plan|testing|how to test/i, testPlan);
|
|
49
45
|
if (overflow)
|
|
50
46
|
body = overflow + "\n\n" + body;
|
|
51
|
-
if (
|
|
52
|
-
|
|
53
|
-
|
|
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());
|
|
54
51
|
}
|
|
55
52
|
else {
|
|
56
|
-
body +=
|
|
57
|
-
"\n\n### Before / After\n\n\n";
|
|
53
|
+
body += "\n\n" + md + "\n";
|
|
60
54
|
}
|
|
55
|
+
body = stripLocalImageNote(body);
|
|
61
56
|
}
|
|
62
57
|
return { title, body };
|
|
63
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
|
+
}
|
|
64
89
|
function fillSection(template, heading, content) {
|
|
65
90
|
const lines = template.split("\n");
|
|
66
91
|
let inSection = false;
|
package/dist/tools.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export declare class ToolNotFoundError extends Error {
|
|
2
|
+
constructor(message: string);
|
|
3
|
+
}
|
|
4
|
+
/** True when the image reference is already a hosted URL. */
|
|
5
|
+
export declare function isHostedImageUrl(ref: string): boolean;
|
|
6
|
+
/** Italic note appended for local-only image paths in PR markdown. */
|
|
7
|
+
export declare const LOCAL_IMAGE_NOTE = "_Local image path - attach the file when pasting into GitHub if it does not render._";
|
|
8
|
+
/** Strip the local-image attach note from PR markdown. */
|
|
9
|
+
export declare function stripLocalImageNote(body: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve an executable: env override, then PATH, then extra candidate paths.
|
|
12
|
+
*/
|
|
13
|
+
export declare function resolveExecutable(names: string[], options: {
|
|
14
|
+
env?: NodeJS.ProcessEnv;
|
|
15
|
+
envKey?: string;
|
|
16
|
+
extraPaths?: string[];
|
|
17
|
+
notFoundMessage: string;
|
|
18
|
+
}): string;
|
|
19
|
+
export declare function resolveGh(env?: NodeJS.ProcessEnv): string;
|
|
20
|
+
/** Reset cache (tests). */
|
|
21
|
+
export declare function resetGhCache(): void;
|
package/dist/tools.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { delimiter, join } from "node:path";
|
|
3
|
+
import { platform } from "node:os";
|
|
4
|
+
export class ToolNotFoundError extends Error {
|
|
5
|
+
constructor(message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = "ToolNotFoundError";
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
/** True when the image reference is already a hosted URL. */
|
|
11
|
+
export function isHostedImageUrl(ref) {
|
|
12
|
+
return /^https?:\/\//i.test(ref.trim());
|
|
13
|
+
}
|
|
14
|
+
/** Italic note appended for local-only image paths in PR markdown. */
|
|
15
|
+
export const LOCAL_IMAGE_NOTE = "_Local image path - attach the file when pasting into GitHub if it does not render._";
|
|
16
|
+
/** Strip the local-image attach note from PR markdown. */
|
|
17
|
+
export function stripLocalImageNote(body) {
|
|
18
|
+
return body
|
|
19
|
+
.replace(/\n*_Local image path[^\n]*_\s*/g, "\n")
|
|
20
|
+
.replace(/\n{3,}/g, "\n\n");
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve an executable: env override, then PATH, then extra candidate paths.
|
|
24
|
+
*/
|
|
25
|
+
export function resolveExecutable(names, options) {
|
|
26
|
+
const env = options.env ?? process.env;
|
|
27
|
+
if (options.envKey && env[options.envKey]) {
|
|
28
|
+
const forced = env[options.envKey];
|
|
29
|
+
if (existsSync(forced))
|
|
30
|
+
return forced;
|
|
31
|
+
throw new ToolNotFoundError(`${options.envKey} not found: ${forced}`);
|
|
32
|
+
}
|
|
33
|
+
const pathEnv = env.PATH ?? env.Path ?? "";
|
|
34
|
+
const dirs = pathEnv.split(delimiter).filter(Boolean);
|
|
35
|
+
const win = platform() === "win32";
|
|
36
|
+
const exts = win
|
|
37
|
+
? (env.PATHEXT ?? ".EXE;.CMD;.BAT;.COM")
|
|
38
|
+
.split(";")
|
|
39
|
+
.map((e) => e.toLowerCase())
|
|
40
|
+
: [""];
|
|
41
|
+
const tryName = (dir, name) => {
|
|
42
|
+
if (win) {
|
|
43
|
+
if (name.includes(".")) {
|
|
44
|
+
const p = join(dir, name);
|
|
45
|
+
return existsSync(p) ? p : null;
|
|
46
|
+
}
|
|
47
|
+
for (const ext of exts) {
|
|
48
|
+
const p = join(dir, name + ext);
|
|
49
|
+
if (existsSync(p))
|
|
50
|
+
return p;
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
const p = join(dir, name);
|
|
55
|
+
return existsSync(p) ? p : null;
|
|
56
|
+
};
|
|
57
|
+
for (const dir of dirs) {
|
|
58
|
+
for (const name of names) {
|
|
59
|
+
const hit = tryName(dir, name);
|
|
60
|
+
if (hit)
|
|
61
|
+
return hit;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const candidate of options.extraPaths ?? []) {
|
|
65
|
+
if (candidate && existsSync(candidate))
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
throw new ToolNotFoundError(options.notFoundMessage);
|
|
69
|
+
}
|
|
70
|
+
let cachedGh;
|
|
71
|
+
export function resolveGh(env = process.env) {
|
|
72
|
+
if (cachedGh)
|
|
73
|
+
return cachedGh;
|
|
74
|
+
const local = env.LOCALAPPDATA ?? "";
|
|
75
|
+
const pf = env.ProgramFiles ?? "C:\\Program Files";
|
|
76
|
+
const pf86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
77
|
+
cachedGh = resolveExecutable(["gh", "gh.exe"], {
|
|
78
|
+
env,
|
|
79
|
+
envKey: "GH_PATH",
|
|
80
|
+
extraPaths: [
|
|
81
|
+
join(pf, "GitHub CLI", "gh.exe"),
|
|
82
|
+
join(pf86, "GitHub CLI", "gh.exe"),
|
|
83
|
+
join(local, "Programs", "GitHub CLI", "gh.exe"),
|
|
84
|
+
"/usr/local/bin/gh",
|
|
85
|
+
"/opt/homebrew/bin/gh",
|
|
86
|
+
"/usr/bin/gh",
|
|
87
|
+
],
|
|
88
|
+
notFoundMessage: "gh not found — install GitHub CLI (https://cli.github.com) or set GH_PATH",
|
|
89
|
+
});
|
|
90
|
+
return cachedGh;
|
|
91
|
+
}
|
|
92
|
+
/** Reset cache (tests). */
|
|
93
|
+
export function resetGhCache() {
|
|
94
|
+
cachedGh = undefined;
|
|
95
|
+
}
|
package/dist/ui/index.js
ADDED
package/dist/ui/lines.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const SUMMARIZE = {
|
|
2
|
+
claude: [
|
|
3
|
+
"claude is reading the diff…",
|
|
4
|
+
"claude is grouping the mess…",
|
|
5
|
+
"claude is writing the blurb…",
|
|
6
|
+
],
|
|
7
|
+
opencode: [
|
|
8
|
+
"opencode is reading the diff…",
|
|
9
|
+
"opencode is grouping the mess…",
|
|
10
|
+
"opencode is writing the blurb…",
|
|
11
|
+
],
|
|
12
|
+
fake: [
|
|
13
|
+
"fake is vibing on hard mode…",
|
|
14
|
+
"fake is inventing nothing new…",
|
|
15
|
+
"fake is shipping canned vibes…",
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
const VISUAL = {
|
|
19
|
+
claude: [
|
|
20
|
+
"claude is sketching a mockup…",
|
|
21
|
+
"claude is boxing a flowchart…",
|
|
22
|
+
"chrome is taking the shot…",
|
|
23
|
+
],
|
|
24
|
+
opencode: [
|
|
25
|
+
"opencode is sketching a mockup…",
|
|
26
|
+
"opencode is boxing a flowchart…",
|
|
27
|
+
"chrome is taking the shot…",
|
|
28
|
+
],
|
|
29
|
+
fake: [
|
|
30
|
+
"fake is drawing boxes…",
|
|
31
|
+
"fake is pretending to design…",
|
|
32
|
+
"chrome is taking the shot…",
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
const OPEN = [
|
|
36
|
+
"talking to gh…",
|
|
37
|
+
"shipping the PR body…",
|
|
38
|
+
"almost there…",
|
|
39
|
+
];
|
|
40
|
+
export function linesFor(bank, provider) {
|
|
41
|
+
if (bank === "diff") {
|
|
42
|
+
return ["gathering the diff…", "asking git politely…"];
|
|
43
|
+
}
|
|
44
|
+
if (bank === "open")
|
|
45
|
+
return OPEN;
|
|
46
|
+
const p = provider ?? "claude";
|
|
47
|
+
return bank === "summarize" ? SUMMARIZE[p] : VISUAL[p];
|
|
48
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { ProviderName } from "../config.js";
|
|
2
|
+
import { type LineBank } from "./lines.js";
|
|
3
|
+
import { type SpinnerHandle } from "./spinner.js";
|
|
4
|
+
export type Ui = {
|
|
5
|
+
/** Plain note (no spinner). */
|
|
6
|
+
note: (text: string) => void;
|
|
7
|
+
/** Start a spinning stage with optional rotating witty lines. */
|
|
8
|
+
stage: (bank: LineBank, provider?: ProviderName) => SpinnerHandle;
|
|
9
|
+
error: (text: string) => void;
|
|
10
|
+
success: (text: string) => void;
|
|
11
|
+
};
|
|
12
|
+
export declare function createUi(): Ui;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { linesFor } from "./lines.js";
|
|
2
|
+
import { startSpinner } from "./spinner.js";
|
|
3
|
+
import { brand, fail, muted, ok } from "./theme.js";
|
|
4
|
+
export function createUi() {
|
|
5
|
+
return {
|
|
6
|
+
note(text) {
|
|
7
|
+
process.stderr.write(`${brand()} ${muted(text)}\n`);
|
|
8
|
+
},
|
|
9
|
+
stage(bank, provider) {
|
|
10
|
+
const lines = linesFor(bank, provider);
|
|
11
|
+
return startSpinner(lines[0] ?? "working…", lines);
|
|
12
|
+
},
|
|
13
|
+
error(text) {
|
|
14
|
+
process.stderr.write(`${brand()} ${fail(text)}\n`);
|
|
15
|
+
},
|
|
16
|
+
success(text) {
|
|
17
|
+
process.stderr.write(`${brand()} ${ok(text)}\n`);
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { brand, accent, isInteractive, muted, ok, fail } from "./theme.js";
|
|
2
|
+
const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
3
|
+
export function startSpinner(initial, rotateLines) {
|
|
4
|
+
const interactive = isInteractive();
|
|
5
|
+
let text = initial;
|
|
6
|
+
let i = 0;
|
|
7
|
+
let lineIdx = 0;
|
|
8
|
+
let timer = null;
|
|
9
|
+
let lineTimer = null;
|
|
10
|
+
let stopped = false;
|
|
11
|
+
const render = () => {
|
|
12
|
+
if (!interactive || stopped)
|
|
13
|
+
return;
|
|
14
|
+
const frame = accent(FRAMES[i % FRAMES.length]);
|
|
15
|
+
process.stderr.write(`\r\x1b[2K${brand()} ${frame} ${text}`);
|
|
16
|
+
i++;
|
|
17
|
+
};
|
|
18
|
+
if (interactive) {
|
|
19
|
+
render();
|
|
20
|
+
timer = setInterval(render, 80);
|
|
21
|
+
if (rotateLines && rotateLines.length > 1) {
|
|
22
|
+
lineTimer = setInterval(() => {
|
|
23
|
+
lineIdx = (lineIdx + 1) % rotateLines.length;
|
|
24
|
+
text = rotateLines[lineIdx];
|
|
25
|
+
}, 2000);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
process.stderr.write(`${brand()} ${muted(text)}\n`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
update(next) {
|
|
33
|
+
text = next;
|
|
34
|
+
if (!interactive)
|
|
35
|
+
process.stderr.write(`${brand()} ${muted(text)}\n`);
|
|
36
|
+
},
|
|
37
|
+
succeed(next) {
|
|
38
|
+
this.stop();
|
|
39
|
+
process.stderr.write(`${brand()} ${ok(next)}\n`);
|
|
40
|
+
},
|
|
41
|
+
fail(next) {
|
|
42
|
+
this.stop();
|
|
43
|
+
process.stderr.write(`${brand()} ${fail(next)}\n`);
|
|
44
|
+
},
|
|
45
|
+
stop() {
|
|
46
|
+
if (stopped)
|
|
47
|
+
return;
|
|
48
|
+
stopped = true;
|
|
49
|
+
if (timer)
|
|
50
|
+
clearInterval(timer);
|
|
51
|
+
if (lineTimer)
|
|
52
|
+
clearInterval(lineTimer);
|
|
53
|
+
if (interactive)
|
|
54
|
+
process.stderr.write("\r\x1b[2K");
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export declare function brand(): string;
|
|
2
|
+
export declare function accent(text: string): string;
|
|
3
|
+
export declare function ok(text: string): string;
|
|
4
|
+
export declare function fail(text: string): string;
|
|
5
|
+
export declare function muted(text: string): string;
|
|
6
|
+
export declare function isInteractive(): boolean;
|
package/dist/ui/theme.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import pc from "picocolors";
|
|
2
|
+
export function brand() {
|
|
3
|
+
return pc.dim("farq");
|
|
4
|
+
}
|
|
5
|
+
export function accent(text) {
|
|
6
|
+
return pc.cyan(text);
|
|
7
|
+
}
|
|
8
|
+
export function ok(text) {
|
|
9
|
+
return `${pc.cyan("✓")} ${pc.dim(text)}`;
|
|
10
|
+
}
|
|
11
|
+
export function fail(text) {
|
|
12
|
+
return `${pc.red("✗")} ${text}`;
|
|
13
|
+
}
|
|
14
|
+
export function muted(text) {
|
|
15
|
+
return pc.dim(text);
|
|
16
|
+
}
|
|
17
|
+
export function isInteractive() {
|
|
18
|
+
return Boolean(process.stderr.isTTY) && process.env.CI !== "true";
|
|
19
|
+
}
|
package/dist/visual/chrome.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
1
|
import { execa } from "execa";
|
|
3
2
|
import { platform } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { resolveExecutable, ToolNotFoundError, } from "../tools.js";
|
|
5
|
+
import { clampViewport } from "./viewport.js";
|
|
4
6
|
const TIMEOUT_MS = 30_000;
|
|
5
7
|
export class ChromeError extends Error {
|
|
6
8
|
constructor(message) {
|
|
@@ -9,17 +11,15 @@ export class ChromeError extends Error {
|
|
|
9
11
|
}
|
|
10
12
|
}
|
|
11
13
|
export function resolveChrome(env = process.env) {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}
|
|
17
|
-
const candidates = platform() === "win32"
|
|
14
|
+
const local = env.LOCALAPPDATA ?? "";
|
|
15
|
+
const pf = env.ProgramFiles ?? "C:\\Program Files";
|
|
16
|
+
const pf86 = env["ProgramFiles(x86)"] ?? "C:\\Program Files (x86)";
|
|
17
|
+
const extraPaths = platform() === "win32"
|
|
18
18
|
? [
|
|
19
|
-
"
|
|
20
|
-
"
|
|
21
|
-
|
|
22
|
-
"
|
|
19
|
+
join(pf, "Google", "Chrome", "Application", "chrome.exe"),
|
|
20
|
+
join(pf86, "Google", "Chrome", "Application", "chrome.exe"),
|
|
21
|
+
join(local, "Google", "Chrome", "Application", "chrome.exe"),
|
|
22
|
+
join(pf, "Chromium", "Application", "chrome.exe"),
|
|
23
23
|
]
|
|
24
24
|
: platform() === "darwin"
|
|
25
25
|
? [
|
|
@@ -32,16 +32,34 @@ export function resolveChrome(env = process.env) {
|
|
|
32
32
|
"/usr/bin/chromium",
|
|
33
33
|
"/usr/bin/chromium-browser",
|
|
34
34
|
];
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
try {
|
|
36
|
+
return resolveExecutable([
|
|
37
|
+
"google-chrome",
|
|
38
|
+
"google-chrome-stable",
|
|
39
|
+
"chromium",
|
|
40
|
+
"chromium-browser",
|
|
41
|
+
"chrome",
|
|
42
|
+
"chrome.exe",
|
|
43
|
+
], {
|
|
44
|
+
env,
|
|
45
|
+
envKey: "CHROME_PATH",
|
|
46
|
+
extraPaths,
|
|
47
|
+
notFoundMessage: "Chrome/Chromium not found — install Google Chrome or set CHROME_PATH",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
if (err instanceof ToolNotFoundError) {
|
|
52
|
+
throw new ChromeError(err.message);
|
|
53
|
+
}
|
|
54
|
+
throw err;
|
|
38
55
|
}
|
|
39
|
-
throw new ChromeError("Chrome/Chromium not found — install Google Chrome or set CHROME_PATH");
|
|
40
56
|
}
|
|
41
57
|
export async function screenshotHtml(options) {
|
|
42
58
|
const chrome = options.chromePath ?? resolveChrome();
|
|
43
|
-
const width
|
|
44
|
-
|
|
59
|
+
const { width, height } = clampViewport({
|
|
60
|
+
width: options.width,
|
|
61
|
+
height: options.height,
|
|
62
|
+
});
|
|
45
63
|
try {
|
|
46
64
|
await execa(chrome, [
|
|
47
65
|
"--headless=new",
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ChangeItem, ChangeSummary } from "../schema.js";
|
|
2
|
+
import type { Provider } from "../providers/index.js";
|
|
3
|
+
export declare const MAX_VISUAL_TOPICS = 5;
|
|
4
|
+
export type VisualTopic = {
|
|
5
|
+
id: number;
|
|
6
|
+
title: string;
|
|
7
|
+
items: ChangeItem[];
|
|
8
|
+
files: string[];
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Prefer intent clustering via the cheap model; fall back to file-overlap.
|
|
12
|
+
* Same-theme items (one feature across many files) → one topic.
|
|
13
|
+
* Truly unrelated domains → separate topics (cap 5).
|
|
14
|
+
*/
|
|
15
|
+
export declare function clusterVisualTopics(summary: ChangeSummary, options?: {
|
|
16
|
+
provider?: Provider;
|
|
17
|
+
model?: string;
|
|
18
|
+
log?: (msg: string) => void;
|
|
19
|
+
}): Promise<VisualTopic[]>;
|
|
20
|
+
/** File-overlap union-find (fallback). Exported for tests. */
|
|
21
|
+
export declare function clusterByFileOverlap(summary: ChangeSummary): VisualTopic[];
|
|
22
|
+
/** Build topics from model JSON; null if invalid. Exported for tests. */
|
|
23
|
+
export declare function topicsFromIntentJson(summary: ChangeSummary, raw: unknown): VisualTopic[] | null;
|