@webappwiz/cli 0.0.6 → 0.0.8
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 +13 -8
- package/index-4xj59vmx.js +135 -0
- package/{index-3k3rtw49.js → index-fh8js68z.js} +5 -17
- package/{index-3p0t2exn.js → index-hz6bwqfy.js} +4 -0
- package/index-r14q0cmx.js +262 -0
- package/{index-k6gswc4n.js → index-rz4spnkb.js} +157 -45
- package/index.js +12 -15
- package/judge.d.ts +9 -37
- package/judge.js +3 -4
- package/mode.d.ts +4 -6
- package/package.json +3 -3
- package/progress.d.ts +64 -0
- package/report.d.ts +14 -30
- package/rules.js +1 -1
- package/signoff.d.ts +0 -6
- package/signoff.js +2 -2
- package/webappwiz.js +12 -15
- package/cost.d.ts +0 -45
- package/index-jbrs6gqt.js +0 -418
package/README.md
CHANGED
|
@@ -59,16 +59,17 @@ of files.
|
|
|
59
59
|
|
|
60
60
|
```bash
|
|
61
61
|
bunx @webappwiz/cli judge . --agent haiku
|
|
62
|
-
bunx @webappwiz/cli judge . --estimate # what would this read, and cost
|
|
63
62
|
bunx @webappwiz/cli judge . --print # print the prompts, spawn nothing
|
|
64
63
|
bunx @webappwiz/cli judge . --since main # only what changed
|
|
64
|
+
bunx @webappwiz/cli judge . --ci # plain lines, no live block
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
Each rule's code half runs first, free, and only what it escalates reaches an
|
|
68
|
-
agent.
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
68
|
+
agent. On a terminal a run draws a live status line (a bar over the calls,
|
|
69
|
+
how many are out, and the tokens spent so far), then dumps the report in one
|
|
70
|
+
block; `--ci`, or any output that is not a terminal, prints line by line as
|
|
71
|
+
reviews finish instead. `--print` and running are two things to do with one
|
|
72
|
+
plan, so passing both is an error rather than one quietly winning. Code excuses itself from a rule with a `rule-ignore <id>: <reason>`
|
|
72
73
|
comment above the line, or `rule-ignore-file <id>: <reason>` for the file.
|
|
73
74
|
|
|
74
75
|
## update
|
|
@@ -100,11 +101,15 @@ bunx @webappwiz/cli skills update ./project
|
|
|
100
101
|
```
|
|
101
102
|
|
|
102
103
|
```
|
|
103
|
-
SKILL
|
|
104
|
-
arbor
|
|
105
|
-
|
|
104
|
+
SKILL SHIPS INSTALLED
|
|
105
|
+
arbor 1.4.0 1.3.0
|
|
106
|
+
webappwiz 1.4.0 -
|
|
106
107
|
```
|
|
107
108
|
|
|
109
|
+
Two ship: `arbor`, which lands an agent's work from its own worktree, and
|
|
110
|
+
`webappwiz`, which sends an agent to the package's catalogue before it writes
|
|
111
|
+
infrastructure by hand.
|
|
112
|
+
|
|
108
113
|
`add` installs one skill by name. `update` refreshes the ones a project already
|
|
109
114
|
has and never installs a new one: which skills a project uses is its own
|
|
110
115
|
business, and a skill nobody chose should not arrive by way of an update. `ls`
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
// changed.ts
|
|
2
|
+
import { NodePs } from "webappwiz/system";
|
|
3
|
+
async function changed(dir, ref, opts = {}) {
|
|
4
|
+
const ps = opts.ps ?? new NodePs;
|
|
5
|
+
const files = new Set;
|
|
6
|
+
for (const argv of [
|
|
7
|
+
["diff", "--name-only", "--diff-filter=d", "--relative", ref],
|
|
8
|
+
["ls-files", "--others", "--exclude-standard"]
|
|
9
|
+
]) {
|
|
10
|
+
for (const line of (await git(ps, dir, argv)).split(`
|
|
11
|
+
`)) {
|
|
12
|
+
if (line !== "") {
|
|
13
|
+
files.add(line);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return files;
|
|
18
|
+
}
|
|
19
|
+
async function diff(dir, ref, opts = {}) {
|
|
20
|
+
const ps = opts.ps ?? new NodePs;
|
|
21
|
+
const patch = await git(ps, dir, ["diff", "--relative", ref]);
|
|
22
|
+
const added = (await git(ps, dir, ["ls-files", "--others", "--exclude-standard"])).split(`
|
|
23
|
+
`).filter((line) => line !== "");
|
|
24
|
+
return { patch: patch.trim(), added };
|
|
25
|
+
}
|
|
26
|
+
async function git(ps, dir, argv) {
|
|
27
|
+
const { exitCode, stdout, stderr } = await ps.spawnCapture([
|
|
28
|
+
"git",
|
|
29
|
+
"-C",
|
|
30
|
+
dir,
|
|
31
|
+
...argv
|
|
32
|
+
]);
|
|
33
|
+
if (exitCode !== 0) {
|
|
34
|
+
throw new Error(`git ${argv[0]} failed in ${dir}: ${stderr.trim() || `exit ${exitCode}`}`);
|
|
35
|
+
}
|
|
36
|
+
return stdout;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// mode.ts
|
|
40
|
+
function mode({ print, agent, exec }) {
|
|
41
|
+
const named = [
|
|
42
|
+
print === true ? "--print" : undefined,
|
|
43
|
+
agent === undefined ? undefined : "--agent",
|
|
44
|
+
exec === undefined ? undefined : "--exec"
|
|
45
|
+
].filter((flag) => flag !== undefined);
|
|
46
|
+
if (named.length > 1) {
|
|
47
|
+
throw new Error(`${named.join(" and ")} are different things to do with one run, so pass one`);
|
|
48
|
+
}
|
|
49
|
+
return print === true ? "print" : "run";
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// table.ts
|
|
53
|
+
import { color } from "webappwiz/log";
|
|
54
|
+
var table = (rows) => {
|
|
55
|
+
const width = (cell) => color.strip(cell).length;
|
|
56
|
+
const widths = rows[0]?.map((_, i) => Math.max(...rows.map((row) => width(row[i] ?? ""))));
|
|
57
|
+
return rows.map((row) => row.map((cell, i) => cell.padEnd((widths?.[i] ?? 0) + cell.length - width(cell))).join(" ").trimEnd());
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
// report.ts
|
|
61
|
+
import { color as color2 } from "webappwiz/log";
|
|
62
|
+
var count = (total, word) => `${total} ${word}${total === 1 ? "" : "s"}`;
|
|
63
|
+
var divider = (name) => {
|
|
64
|
+
if (name === undefined) {
|
|
65
|
+
return color2.dim("-".repeat(72));
|
|
66
|
+
}
|
|
67
|
+
const opening = `--- ${name} `;
|
|
68
|
+
return color2.dim(opening.padEnd(Math.max(72, opening.length + 3), "-"));
|
|
69
|
+
};
|
|
70
|
+
var compact = new Intl.NumberFormat("en", { notation: "compact" });
|
|
71
|
+
var tokens = (bytes) => Math.ceil(bytes / 4);
|
|
72
|
+
function planned({
|
|
73
|
+
files,
|
|
74
|
+
rules,
|
|
75
|
+
calls,
|
|
76
|
+
estimate,
|
|
77
|
+
concurrency,
|
|
78
|
+
agent
|
|
79
|
+
}) {
|
|
80
|
+
const rows = [
|
|
81
|
+
[color2.dim("files"), String(files)],
|
|
82
|
+
[color2.dim("rules"), String(rules)],
|
|
83
|
+
[color2.dim("calls"), String(calls)],
|
|
84
|
+
...concurrency === undefined ? [] : [[color2.dim("workers"), String(concurrency)]],
|
|
85
|
+
[color2.dim("reading"), `${compact.format(estimate)}+ tokens`]
|
|
86
|
+
];
|
|
87
|
+
if (agent !== undefined) {
|
|
88
|
+
rows.push([color2.dim("agent"), agent]);
|
|
89
|
+
}
|
|
90
|
+
return ["", ...table(rows).map((line) => ` ${line}`), ""];
|
|
91
|
+
}
|
|
92
|
+
function finished({
|
|
93
|
+
rules,
|
|
94
|
+
files,
|
|
95
|
+
violations,
|
|
96
|
+
took,
|
|
97
|
+
tokens: tokens2,
|
|
98
|
+
worker,
|
|
99
|
+
workerTokens,
|
|
100
|
+
done,
|
|
101
|
+
total
|
|
102
|
+
}) {
|
|
103
|
+
const heading = `${color2.gray(`[${done}/${total}]`)} ` + color2.gray(`(${count(rules.length, "rule")}, ${count(files, "file")})`);
|
|
104
|
+
const spent = tokens2 === undefined ? "" : ` ${compact.format(tokens2)} tokens` + (workerTokens === undefined ? "" : ` (w${worker + 1}: ${compact.format(workerTokens)})`);
|
|
105
|
+
const tail = `${color2.gray(`in ${took.human()}${spent}`)}`;
|
|
106
|
+
if (violations.length === 0) {
|
|
107
|
+
return [`${color2.green("✓")} ${heading}: clean ${tail}`];
|
|
108
|
+
}
|
|
109
|
+
return [
|
|
110
|
+
`${color2.red("✗")} ${heading}: ${count(violations.length, "problem")} ${tail}`,
|
|
111
|
+
...violations.flatMap(finding)
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
function finding(violation) {
|
|
115
|
+
const level = violation.level === "error" ? color2.red("error") : color2.yellow(violation.level);
|
|
116
|
+
const lines = [
|
|
117
|
+
` ${color2.bold(`${violation.file}:${violation.line}`)} ${level} ${violation.message} ${color2.gray(`(${violation.id})`)}`
|
|
118
|
+
];
|
|
119
|
+
if (violation.code !== "") {
|
|
120
|
+
lines.push(color2.gray(` │ ${violation.code}`));
|
|
121
|
+
}
|
|
122
|
+
return lines;
|
|
123
|
+
}
|
|
124
|
+
function summary(violations, took, tokens2) {
|
|
125
|
+
const spent = tokens2 === undefined ? "" : ` ${compact.format(tokens2)} tokens total`;
|
|
126
|
+
const elapsed = color2.gray(`in ${took.human()}${spent}`);
|
|
127
|
+
if (violations.length === 0) {
|
|
128
|
+
return `${color2.green("✓ no violations")} ${elapsed}`;
|
|
129
|
+
}
|
|
130
|
+
const errors = violations.filter((violation) => violation.level === "error").length;
|
|
131
|
+
const line = `✖ ${count(violations.length, "problem")} (${count(errors, "error")}, ${count(violations.length - errors, "warning")})`;
|
|
132
|
+
return `${errors > 0 ? color2.red(line) : color2.yellow(line)} ${elapsed}`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export { changed, diff, mode, table, count, divider, compact, tokens, planned, finished, summary };
|
|
@@ -1,13 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
|
-
ask,
|
|
3
2
|
count,
|
|
4
3
|
diff,
|
|
5
4
|
divider,
|
|
6
5
|
mode,
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
usd
|
|
10
|
-
} from "./index-jbrs6gqt.js";
|
|
6
|
+
tokens
|
|
7
|
+
} from "./index-4xj59vmx.js";
|
|
11
8
|
|
|
12
9
|
// signoff.ts
|
|
13
10
|
import {
|
|
@@ -21,14 +18,12 @@ import { SystemClock } from "webappwiz/time";
|
|
|
21
18
|
class Signoff {
|
|
22
19
|
rules;
|
|
23
20
|
defaultAgent;
|
|
24
|
-
confirmer;
|
|
25
21
|
log;
|
|
26
22
|
ps;
|
|
27
23
|
clock;
|
|
28
24
|
constructor(rules, defaultAgent, opts = {}) {
|
|
29
25
|
this.rules = rules;
|
|
30
26
|
this.defaultAgent = defaultAgent;
|
|
31
|
-
this.confirmer = opts.confirmer ?? ask;
|
|
32
27
|
this.log = opts.log ?? new ConsoleLogger;
|
|
33
28
|
this.ps = opts.ps ?? new NodePs;
|
|
34
29
|
this.clock = opts.clock ?? new SystemClock;
|
|
@@ -46,14 +41,7 @@ class Signoff {
|
|
|
46
41
|
}
|
|
47
42
|
const agent = agentCommand(opts.exec === undefined ? { agent: opts.agent ?? this.defaultAgent } : opts);
|
|
48
43
|
const review = this.review(patch, added, opts.since);
|
|
49
|
-
|
|
50
|
-
this.log.info(`weighing ${opts.since}..working tree against ` + `${count(this.rules.length, "rule")}, reading ` + `${predicted}+ tokens with ${agent.label}`);
|
|
51
|
-
if (predicted > opts.budget) {
|
|
52
|
-
this.log.info(overBudget(predicted, opts.budget));
|
|
53
|
-
if (!await this.confirmer.confirm("Run anyway?")) {
|
|
54
|
-
throw new Error("over budget");
|
|
55
|
-
}
|
|
56
|
-
}
|
|
44
|
+
this.log.info(`weighing ${opts.since}..working tree against ` + `${count(this.rules.length, "rule")}, reading ` + `${tokens(review.bytes ?? 0)}+ tokens with ${agent.label}`);
|
|
57
45
|
this.say(await this.judge(review, agent, dir));
|
|
58
46
|
}
|
|
59
47
|
async judge(review, agent, dir) {
|
|
@@ -62,8 +50,8 @@ class Signoff {
|
|
|
62
50
|
ps: this.ps,
|
|
63
51
|
clock: this.clock
|
|
64
52
|
});
|
|
65
|
-
harness.events.on("finished", ({ took,
|
|
66
|
-
const spent =
|
|
53
|
+
harness.events.on("finished", ({ took, tokens: read }) => {
|
|
54
|
+
const spent = read === undefined ? "" : ` ${read} tokens`;
|
|
67
55
|
this.log.info(color.gray(`read in ${took.human()}${spent}`));
|
|
68
56
|
});
|
|
69
57
|
return await harness.run([review], agent, { cwd: dir });
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
CommentsSayWhyNotWhat,
|
|
6
6
|
DevServersFindAPort,
|
|
7
7
|
DocCommentsAddressUsers,
|
|
8
|
+
ExportLeadsTheFile,
|
|
8
9
|
FakesOverMocks,
|
|
9
10
|
MatchersOverTestLogic,
|
|
10
11
|
NamedOptionsLast,
|
|
@@ -17,17 +18,20 @@ import {
|
|
|
17
18
|
ResourcesAreDisposable,
|
|
18
19
|
SimpleTestSetup,
|
|
19
20
|
TestsNotWeakened,
|
|
21
|
+
TestsOwnTheirState,
|
|
20
22
|
VisualWorkTested
|
|
21
23
|
} from "@webappwiz/rules/catalog";
|
|
22
24
|
var JUDGE_RULES = defineRules({
|
|
23
25
|
rules: [
|
|
24
26
|
new NoEmDashes,
|
|
25
27
|
new OneClassPerFile,
|
|
28
|
+
new ExportLeadsTheFile,
|
|
26
29
|
new ParametersDeclareFields,
|
|
27
30
|
new ClassesOverFunctionExports,
|
|
28
31
|
new ObjectsOverCallbacks,
|
|
29
32
|
new NamedOptionsLast,
|
|
30
33
|
new SimpleTestSetup,
|
|
34
|
+
new TestsOwnTheirState,
|
|
31
35
|
new FakesOverMocks,
|
|
32
36
|
new MatchersOverTestLogic,
|
|
33
37
|
new CommentsSayWhyNotWhat,
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import {
|
|
2
|
+
changed,
|
|
3
|
+
compact,
|
|
4
|
+
count,
|
|
5
|
+
divider,
|
|
6
|
+
finished,
|
|
7
|
+
mode,
|
|
8
|
+
planned,
|
|
9
|
+
summary,
|
|
10
|
+
table,
|
|
11
|
+
tokens
|
|
12
|
+
} from "./index-4xj59vmx.js";
|
|
13
|
+
|
|
14
|
+
// judge.ts
|
|
15
|
+
import {
|
|
16
|
+
agentCommand,
|
|
17
|
+
Files,
|
|
18
|
+
Harness,
|
|
19
|
+
prompt as reviewPrompt
|
|
20
|
+
} from "@webappwiz/rules";
|
|
21
|
+
import { ConsoleLogger, color as color2 } from "webappwiz/log";
|
|
22
|
+
import {
|
|
23
|
+
NodeFs,
|
|
24
|
+
NodeGlob,
|
|
25
|
+
NodePs
|
|
26
|
+
} from "webappwiz/system";
|
|
27
|
+
import { SystemClock } from "webappwiz/time";
|
|
28
|
+
|
|
29
|
+
// progress.ts
|
|
30
|
+
import { color } from "webappwiz/log";
|
|
31
|
+
import { Duration, SystemTimer } from "webappwiz/time";
|
|
32
|
+
var terminal = () => ({
|
|
33
|
+
tty: process.stdout.isTTY === true,
|
|
34
|
+
write: (text) => process.stdout.write(text)
|
|
35
|
+
});
|
|
36
|
+
var BAR = 20;
|
|
37
|
+
var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
38
|
+
function render(view, frame = 0) {
|
|
39
|
+
const spin = view.files === 0 ? " " : FRAMES[frame % FRAMES.length] ?? " ";
|
|
40
|
+
const filled = Math.round(BAR * view.done / Math.max(1, view.total));
|
|
41
|
+
const bar = color.green("█".repeat(filled)) + color.dim("░".repeat(BAR - filled));
|
|
42
|
+
const judging = view.files === 0 ? "" : ` · judging ${count(view.files, "file")}`;
|
|
43
|
+
const spent = view.tokens === undefined ? "" : ` · ${compact.format(view.tokens)} tokens`;
|
|
44
|
+
const found = view.done === 0 ? "" : ` · ${view.problems === 0 ? "clean so far" : count(view.problems, "problem")}`;
|
|
45
|
+
return `${color.green(spin)} ${bar} ${color.gray(`${view.done}/${view.total} calls${judging}${spent}${found}`)}`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class Progress {
|
|
49
|
+
screen;
|
|
50
|
+
total;
|
|
51
|
+
done = 0;
|
|
52
|
+
files = 0;
|
|
53
|
+
problems = 0;
|
|
54
|
+
tokens;
|
|
55
|
+
drawn = false;
|
|
56
|
+
frame = 0;
|
|
57
|
+
ticking;
|
|
58
|
+
constructor(screen, total, opts = {}) {
|
|
59
|
+
this.screen = screen;
|
|
60
|
+
this.total = total;
|
|
61
|
+
const timer = opts.timer ?? new SystemTimer;
|
|
62
|
+
this.ticking = timer.setInterval(() => {
|
|
63
|
+
this.frame += 1;
|
|
64
|
+
this.draw();
|
|
65
|
+
}, Duration.ms(100));
|
|
66
|
+
}
|
|
67
|
+
started(files) {
|
|
68
|
+
this.files += files;
|
|
69
|
+
this.draw();
|
|
70
|
+
}
|
|
71
|
+
finished(files, spent, problems = 0) {
|
|
72
|
+
this.files -= files;
|
|
73
|
+
this.done += 1;
|
|
74
|
+
this.problems += problems;
|
|
75
|
+
if (spent !== undefined) {
|
|
76
|
+
this.tokens = (this.tokens ?? 0) + spent;
|
|
77
|
+
}
|
|
78
|
+
this.draw();
|
|
79
|
+
}
|
|
80
|
+
stop() {
|
|
81
|
+
this.ticking.dispose();
|
|
82
|
+
this.erase();
|
|
83
|
+
}
|
|
84
|
+
erase() {
|
|
85
|
+
if (this.drawn) {
|
|
86
|
+
this.screen.write("\x1B[1A\r\x1B[0J");
|
|
87
|
+
this.drawn = false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
draw() {
|
|
91
|
+
const line = render({
|
|
92
|
+
done: this.done,
|
|
93
|
+
total: this.total,
|
|
94
|
+
files: this.files,
|
|
95
|
+
tokens: this.tokens,
|
|
96
|
+
problems: this.problems
|
|
97
|
+
}, this.frame);
|
|
98
|
+
this.erase();
|
|
99
|
+
this.screen.write(`${line}
|
|
100
|
+
`);
|
|
101
|
+
this.drawn = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// judge.ts
|
|
106
|
+
var estimated = (reviews) => tokens(reviews.reduce((bytes, review) => bytes + review.bytes, 0));
|
|
107
|
+
var isFileRule = (rule) => ("files" in rule);
|
|
108
|
+
var title = (rule) => /^#\s+(.+)$/m.exec(rule.document)?.[1]?.trim() ?? rule.id;
|
|
109
|
+
|
|
110
|
+
class JudgeCommands {
|
|
111
|
+
rules;
|
|
112
|
+
signoffRules;
|
|
113
|
+
screen;
|
|
114
|
+
log;
|
|
115
|
+
fs;
|
|
116
|
+
ps;
|
|
117
|
+
clock;
|
|
118
|
+
glob;
|
|
119
|
+
constructor(rules, opts = {}) {
|
|
120
|
+
this.rules = rules;
|
|
121
|
+
this.signoffRules = opts.signoffRules ?? [];
|
|
122
|
+
this.screen = opts.screen ?? terminal();
|
|
123
|
+
this.log = opts.log ?? new ConsoleLogger;
|
|
124
|
+
this.fs = opts.fs ?? new NodeFs;
|
|
125
|
+
this.ps = opts.ps ?? new NodePs;
|
|
126
|
+
this.clock = opts.clock ?? new SystemClock;
|
|
127
|
+
this.glob = opts.glob ?? new NodeGlob;
|
|
128
|
+
}
|
|
129
|
+
ls() {
|
|
130
|
+
const rows = [["id", "rule", "set", "level", "files"].map(color2.dim)];
|
|
131
|
+
for (const rule of this.rules.rules) {
|
|
132
|
+
rows.push([rule.id, title(rule), "judge", rule.level, rule.files]);
|
|
133
|
+
}
|
|
134
|
+
for (const rule of this.signoffRules) {
|
|
135
|
+
rows.push([rule.id, title(rule), "signoff", "", ""]);
|
|
136
|
+
}
|
|
137
|
+
this.log.info(table(rows).join(`
|
|
138
|
+
`));
|
|
139
|
+
}
|
|
140
|
+
show(opts) {
|
|
141
|
+
const all = [...this.rules.rules, ...this.signoffRules];
|
|
142
|
+
const rule = all.find((candidate) => candidate.id === opts.id);
|
|
143
|
+
if (!rule) {
|
|
144
|
+
throw new Error(`no rule "${opts.id}". Known ids: ${all.map((candidate) => candidate.id).join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
const rows = [
|
|
147
|
+
[color2.dim("id"), rule.id],
|
|
148
|
+
[color2.dim("rule"), title(rule)]
|
|
149
|
+
];
|
|
150
|
+
if (isFileRule(rule)) {
|
|
151
|
+
rows.push([color2.dim("level"), rule.level], [color2.dim("files"), rule.files]);
|
|
152
|
+
}
|
|
153
|
+
this.log.info(table(rows).join(`
|
|
154
|
+
`));
|
|
155
|
+
this.log.info("");
|
|
156
|
+
this.log.info(rule.document.trim());
|
|
157
|
+
}
|
|
158
|
+
async judge(opts) {
|
|
159
|
+
const how = mode(opts);
|
|
160
|
+
const config = this.rules;
|
|
161
|
+
const rules = config.rules;
|
|
162
|
+
const dir = opts.dir.replace(/\/+$/, "") || "/";
|
|
163
|
+
const only = opts.since === undefined ? undefined : await changed(dir, opts.since, { ps: this.ps });
|
|
164
|
+
if (only?.size === 0) {
|
|
165
|
+
this.log.info(`nothing has changed since ${opts.since}`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
const files = new Files({ log: this.log, fs: this.fs, glob: this.glob });
|
|
169
|
+
const reviews = await files.plan(rules, dir, {
|
|
170
|
+
chunk: opts.chunk,
|
|
171
|
+
only
|
|
172
|
+
});
|
|
173
|
+
if (how === "print") {
|
|
174
|
+
for (const review of reviews) {
|
|
175
|
+
this.log.info(`
|
|
176
|
+
${divider(`${review.label} (${count(review.files.length, "file")})`)}
|
|
177
|
+
`);
|
|
178
|
+
this.log.info(reviewPrompt(review));
|
|
179
|
+
}
|
|
180
|
+
this.log.info(`
|
|
181
|
+
${divider()}`);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
const read = new Set(reviews.flatMap((review) => review.files)).size;
|
|
185
|
+
const agent = this.agent(config, opts);
|
|
186
|
+
const concurrency = opts["concurrency-override"] ?? config.concurrency;
|
|
187
|
+
const started = this.clock.now();
|
|
188
|
+
this.log.info(planned({
|
|
189
|
+
files: read,
|
|
190
|
+
rules: rules.length,
|
|
191
|
+
calls: reviews.length,
|
|
192
|
+
estimate: estimated(reviews),
|
|
193
|
+
concurrency,
|
|
194
|
+
agent: agent.label
|
|
195
|
+
}).join(`
|
|
196
|
+
`));
|
|
197
|
+
const found = [];
|
|
198
|
+
const byWorker = new Map;
|
|
199
|
+
let spent;
|
|
200
|
+
const progress = opts.ci !== true && this.screen.tty ? new Progress(this.screen, reviews.length) : undefined;
|
|
201
|
+
const deferred = [];
|
|
202
|
+
const harness = new Harness({
|
|
203
|
+
log: this.log,
|
|
204
|
+
ps: this.ps,
|
|
205
|
+
clock: this.clock
|
|
206
|
+
});
|
|
207
|
+
harness.events.on("started", ({ at }) => progress?.started(reviews[at]?.files.length ?? 0));
|
|
208
|
+
harness.events.on("finished", (review) => {
|
|
209
|
+
const at = reviews[review.at];
|
|
210
|
+
if (!at) {
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
let workerTokens;
|
|
214
|
+
if (review.tokens !== undefined) {
|
|
215
|
+
workerTokens = (byWorker.get(review.worker) ?? 0) + review.tokens;
|
|
216
|
+
byWorker.set(review.worker, workerTokens);
|
|
217
|
+
spent = (spent ?? 0) + review.tokens;
|
|
218
|
+
}
|
|
219
|
+
const violations2 = files.violations(at, review.findings, dir);
|
|
220
|
+
found[review.at] = violations2;
|
|
221
|
+
progress?.finished(at.files.length, review.tokens, violations2.length);
|
|
222
|
+
const lines = finished({
|
|
223
|
+
rules: review.rules,
|
|
224
|
+
files: at.files.length,
|
|
225
|
+
violations: violations2,
|
|
226
|
+
took: review.took,
|
|
227
|
+
tokens: review.tokens,
|
|
228
|
+
worker: review.worker,
|
|
229
|
+
workerTokens,
|
|
230
|
+
done: review.done,
|
|
231
|
+
total: review.total
|
|
232
|
+
});
|
|
233
|
+
if (progress) {
|
|
234
|
+
deferred[review.done - 1] = lines;
|
|
235
|
+
} else {
|
|
236
|
+
for (const line of lines) {
|
|
237
|
+
this.log.info(line);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
try {
|
|
242
|
+
await harness.run(reviews, agent, { cwd: dir, concurrency });
|
|
243
|
+
} finally {
|
|
244
|
+
progress?.stop();
|
|
245
|
+
}
|
|
246
|
+
for (const line of deferred.flat()) {
|
|
247
|
+
this.log.info(line);
|
|
248
|
+
}
|
|
249
|
+
const violations = found.flat();
|
|
250
|
+
this.log.info("");
|
|
251
|
+
this.log.info(summary(violations, this.clock.now().subtract(started), spent));
|
|
252
|
+
const errors = violations.filter((violation) => violation.level === "error").length;
|
|
253
|
+
if (errors > 0) {
|
|
254
|
+
throw new Error(count(errors, "error"));
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
agent(config, opts) {
|
|
258
|
+
return agentCommand(opts.exec === undefined ? { agent: opts.agent ?? config.agent } : opts);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export { JudgeCommands };
|