@webappwiz/cli 0.0.10 → 0.0.11

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/index-pyjg1rtk.js DELETED
@@ -1,373 +0,0 @@
1
- // judge.ts
2
- import {
3
- agentCommand,
4
- Files,
5
- Harness,
6
- prompt as reviewPrompt
7
- } from "@webappwiz/rules";
8
- import { ConsoleLogger, color as color4 } from "webappwiz/log";
9
- import {
10
- NodeFs,
11
- NodeGlob,
12
- NodePs as NodePs2
13
- } from "webappwiz/system";
14
- import { SystemClock } from "webappwiz/time";
15
-
16
- // changed.ts
17
- import { NodePs } from "webappwiz/system";
18
- async function changed(dir, ref, opts = {}) {
19
- const ps = opts.ps ?? new NodePs;
20
- const files = new Set;
21
- for (const argv of [
22
- ["diff", "--name-only", "--diff-filter=d", "--relative", ref],
23
- ["ls-files", "--others", "--exclude-standard"]
24
- ]) {
25
- for (const line of (await git(ps, dir, argv)).split(`
26
- `)) {
27
- if (line !== "") {
28
- files.add(line);
29
- }
30
- }
31
- }
32
- return files;
33
- }
34
- async function git(ps, dir, argv) {
35
- const { exitCode, stdout, stderr } = await ps.spawnCapture([
36
- "git",
37
- "-C",
38
- dir,
39
- ...argv
40
- ]);
41
- if (exitCode !== 0) {
42
- throw new Error(`git ${argv[0]} failed in ${dir}: ${stderr.trim() || `exit ${exitCode}`}`);
43
- }
44
- return stdout;
45
- }
46
-
47
- // mode.ts
48
- function mode({ print, agent, exec }) {
49
- const named = [
50
- print === true ? "--print" : undefined,
51
- agent === undefined ? undefined : "--agent",
52
- exec === undefined ? undefined : "--exec"
53
- ].filter((flag) => flag !== undefined);
54
- if (named.length > 1) {
55
- throw new Error(`${named.join(" and ")} are different things to do with one run, so pass one`);
56
- }
57
- return print === true ? "print" : "run";
58
- }
59
-
60
- // progress.ts
61
- import { color as color3 } from "webappwiz/log";
62
- import { Duration, SystemTimer } from "webappwiz/time";
63
-
64
- // report.ts
65
- import { color as color2 } from "webappwiz/log";
66
-
67
- // table.ts
68
- import { color } from "webappwiz/log";
69
- var table = (rows) => {
70
- const width = (cell) => color.strip(cell).length;
71
- const widths = rows[0]?.map((_, i) => Math.max(...rows.map((row) => width(row[i] ?? ""))));
72
- return rows.map((row) => row.map((cell, i) => cell.padEnd((widths?.[i] ?? 0) + cell.length - width(cell))).join(" ").trimEnd());
73
- };
74
-
75
- // report.ts
76
- var count = (total, word) => `${total} ${word}${total === 1 ? "" : "s"}`;
77
- var divider = (name) => {
78
- if (name === undefined) {
79
- return color2.dim("-".repeat(72));
80
- }
81
- const opening = `--- ${name} `;
82
- return color2.dim(opening.padEnd(Math.max(72, opening.length + 3), "-"));
83
- };
84
- var compact = new Intl.NumberFormat("en", { notation: "compact" });
85
- var tokens = (bytes) => Math.ceil(bytes / 4);
86
- function planned({
87
- files,
88
- rules,
89
- calls,
90
- estimate,
91
- concurrency,
92
- agent
93
- }) {
94
- const rows = [
95
- [color2.dim("files"), String(files)],
96
- [color2.dim("rules"), String(rules)],
97
- [color2.dim("calls"), String(calls)],
98
- ...concurrency === undefined ? [] : [[color2.dim("workers"), String(concurrency)]],
99
- [color2.dim("reading"), `${compact.format(estimate)}+ tokens`]
100
- ];
101
- if (agent !== undefined) {
102
- rows.push([color2.dim("agent"), agent]);
103
- }
104
- return ["", ...table(rows).map((line) => ` ${line}`), ""];
105
- }
106
- function finished({
107
- rules,
108
- files,
109
- violations,
110
- took,
111
- tokens: tokens2,
112
- worker,
113
- workerTokens,
114
- done,
115
- total
116
- }) {
117
- const heading = `${color2.gray(`[${done}/${total}]`)} ` + color2.gray(`(${count(rules.length, "rule")}, ${count(files, "file")})`);
118
- const spent = tokens2 === undefined ? "" : ` ${compact.format(tokens2)} tokens` + (workerTokens === undefined ? "" : ` (w${worker + 1}: ${compact.format(workerTokens)})`);
119
- const tail = `${color2.gray(`in ${took.human()}${spent}`)}`;
120
- if (violations.length === 0) {
121
- return [`${color2.green("✓")} ${heading}: clean ${tail}`];
122
- }
123
- return [
124
- `${color2.red("✗")} ${heading}: ${count(violations.length, "problem")} ${tail}`,
125
- ...violations.flatMap(finding)
126
- ];
127
- }
128
- function finding(violation) {
129
- const level = violation.level === "error" ? color2.red("error") : color2.yellow(violation.level);
130
- const lines = [
131
- ` ${color2.bold(`${violation.file}:${violation.line}`)} ${level} ${violation.message} ${color2.gray(`(${violation.id})`)}`
132
- ];
133
- if (violation.code !== "") {
134
- lines.push(color2.gray(` │ ${violation.code}`));
135
- }
136
- return lines;
137
- }
138
- function summary(violations, took, tokens2) {
139
- const spent = tokens2 === undefined ? "" : ` ${compact.format(tokens2)} tokens total`;
140
- const elapsed = color2.gray(`in ${took.human()}${spent}`);
141
- if (violations.length === 0) {
142
- return `${color2.green("✓ no violations")} ${elapsed}`;
143
- }
144
- const errors = violations.filter((violation) => violation.level === "error").length;
145
- const line = `✖ ${count(violations.length, "problem")} (${count(errors, "error")}, ${count(violations.length - errors, "warning")})`;
146
- return `${errors > 0 ? color2.red(line) : color2.yellow(line)} ${elapsed}`;
147
- }
148
-
149
- // progress.ts
150
- var terminal = () => ({
151
- tty: process.stdout.isTTY === true,
152
- write: (text) => process.stdout.write(text)
153
- });
154
- var BAR = 20;
155
- var FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
156
- function render(view, frame = 0) {
157
- const spin = view.files === 0 ? " " : FRAMES[frame % FRAMES.length] ?? " ";
158
- const filled = Math.round(BAR * view.done / Math.max(1, view.total));
159
- const bar = color3.green("█".repeat(filled)) + color3.dim("░".repeat(BAR - filled));
160
- const judging = view.files === 0 ? "" : ` · judging ${count(view.files, "file")}`;
161
- const spent = view.tokens === undefined ? "" : ` · ${compact.format(view.tokens)} tokens`;
162
- const found = view.done === 0 ? "" : ` · ${view.problems === 0 ? "clean so far" : count(view.problems, "problem")}`;
163
- return `${color3.green(spin)} ${bar} ${color3.gray(`${view.done}/${view.total} calls${judging}${spent}${found}`)}`;
164
- }
165
-
166
- class Progress {
167
- screen;
168
- total;
169
- done = 0;
170
- files = 0;
171
- problems = 0;
172
- tokens;
173
- drawn = false;
174
- frame = 0;
175
- ticking;
176
- constructor(screen, total, opts = {}) {
177
- this.screen = screen;
178
- this.total = total;
179
- const timer = opts.timer ?? new SystemTimer;
180
- this.ticking = timer.setInterval(() => {
181
- this.frame += 1;
182
- this.draw();
183
- }, Duration.ms(100));
184
- }
185
- started(files) {
186
- this.files += files;
187
- this.draw();
188
- }
189
- finished(files, spent, problems = 0) {
190
- this.files -= files;
191
- this.done += 1;
192
- this.problems += problems;
193
- if (spent !== undefined) {
194
- this.tokens = (this.tokens ?? 0) + spent;
195
- }
196
- this.draw();
197
- }
198
- stop() {
199
- this.ticking.dispose();
200
- this.erase();
201
- }
202
- erase() {
203
- if (this.drawn) {
204
- this.screen.write("\x1B[1A\r\x1B[0J");
205
- this.drawn = false;
206
- }
207
- }
208
- draw() {
209
- const line = render({
210
- done: this.done,
211
- total: this.total,
212
- files: this.files,
213
- tokens: this.tokens,
214
- problems: this.problems
215
- }, this.frame);
216
- this.erase();
217
- this.screen.write(`${line}
218
- `);
219
- this.drawn = true;
220
- }
221
- }
222
-
223
- // judge.ts
224
- var estimated = (reviews) => tokens(reviews.reduce((bytes, review) => bytes + review.bytes, 0));
225
- var title = (rule) => /^#\s+(.+)$/m.exec(rule.document)?.[1]?.trim() ?? rule.id;
226
-
227
- class JudgeCommands {
228
- rules;
229
- screen;
230
- log;
231
- fs;
232
- ps;
233
- clock;
234
- glob;
235
- constructor(rules, opts = {}) {
236
- this.rules = rules;
237
- this.screen = opts.screen ?? terminal();
238
- this.log = opts.log ?? new ConsoleLogger;
239
- this.fs = opts.fs ?? new NodeFs;
240
- this.ps = opts.ps ?? new NodePs2;
241
- this.clock = opts.clock ?? new SystemClock;
242
- this.glob = opts.glob ?? new NodeGlob;
243
- }
244
- ls() {
245
- const rows = [["id", "rule", "level", "files"].map(color4.dim)];
246
- for (const rule of this.rules.rules) {
247
- rows.push([rule.id, title(rule), rule.level, rule.files]);
248
- }
249
- this.log.info(table(rows).join(`
250
- `));
251
- }
252
- show(opts) {
253
- const all = this.rules.rules;
254
- const rule = all.find((candidate) => candidate.id === opts.id);
255
- if (!rule) {
256
- throw new Error(`no rule "${opts.id}". Known ids: ${all.map((candidate) => candidate.id).join(", ")}`);
257
- }
258
- const rows = [
259
- [color4.dim("id"), rule.id],
260
- [color4.dim("rule"), title(rule)],
261
- [color4.dim("level"), rule.level],
262
- [color4.dim("files"), rule.files]
263
- ];
264
- this.log.info(table(rows).join(`
265
- `));
266
- this.log.info("");
267
- this.log.info(rule.document.trim());
268
- }
269
- async judge(opts) {
270
- const how = mode(opts);
271
- const config = this.rules;
272
- const rules = config.rules;
273
- const dir = opts.dir.replace(/\/+$/, "") || "/";
274
- const only = opts.since === undefined ? undefined : await changed(dir, opts.since, { ps: this.ps });
275
- if (only?.size === 0) {
276
- this.log.info(`nothing has changed since ${opts.since}`);
277
- return;
278
- }
279
- const files = new Files({ log: this.log, fs: this.fs, glob: this.glob });
280
- const reviews = await files.plan(rules, dir, {
281
- chunk: opts.chunk,
282
- only
283
- });
284
- if (how === "print") {
285
- for (const review of reviews) {
286
- this.log.info(`
287
- ${divider(`${review.label} (${count(review.files.length, "file")})`)}
288
- `);
289
- this.log.info(reviewPrompt(review));
290
- }
291
- this.log.info(`
292
- ${divider()}`);
293
- return;
294
- }
295
- const read = new Set(reviews.flatMap((review) => review.files)).size;
296
- const agent = this.agent(config, opts);
297
- const concurrency = opts["concurrency-override"] ?? config.concurrency;
298
- const started = this.clock.now();
299
- this.log.info(planned({
300
- files: read,
301
- rules: rules.length,
302
- calls: reviews.length,
303
- estimate: estimated(reviews),
304
- concurrency,
305
- agent: agent.label
306
- }).join(`
307
- `));
308
- const found = [];
309
- const byWorker = new Map;
310
- let spent;
311
- const progress = opts.ci !== true && this.screen.tty ? new Progress(this.screen, reviews.length) : undefined;
312
- const deferred = [];
313
- const harness = new Harness({
314
- log: this.log,
315
- ps: this.ps,
316
- clock: this.clock
317
- });
318
- harness.events.on("started", ({ at }) => progress?.started(reviews[at]?.files.length ?? 0));
319
- harness.events.on("finished", (review) => {
320
- const at = reviews[review.at];
321
- if (!at) {
322
- return;
323
- }
324
- let workerTokens;
325
- if (review.tokens !== undefined) {
326
- workerTokens = (byWorker.get(review.worker) ?? 0) + review.tokens;
327
- byWorker.set(review.worker, workerTokens);
328
- spent = (spent ?? 0) + review.tokens;
329
- }
330
- const violations2 = files.violations(at, review.findings, dir);
331
- found[review.at] = violations2;
332
- progress?.finished(at.files.length, review.tokens, violations2.length);
333
- const lines = finished({
334
- rules: review.rules,
335
- files: at.files.length,
336
- violations: violations2,
337
- took: review.took,
338
- tokens: review.tokens,
339
- worker: review.worker,
340
- workerTokens,
341
- done: review.done,
342
- total: review.total
343
- });
344
- if (progress) {
345
- deferred[review.done - 1] = lines;
346
- } else {
347
- for (const line of lines) {
348
- this.log.info(line);
349
- }
350
- }
351
- });
352
- try {
353
- await harness.run(reviews, agent, { cwd: dir, concurrency });
354
- } finally {
355
- progress?.stop();
356
- }
357
- for (const line of deferred.flat()) {
358
- this.log.info(line);
359
- }
360
- const violations = found.flat();
361
- this.log.info("");
362
- this.log.info(summary(violations, this.clock.now().subtract(started), spent));
363
- const errors = violations.filter((violation) => violation.level === "error").length;
364
- if (errors > 0) {
365
- throw new Error(count(errors, "error"));
366
- }
367
- }
368
- agent(config, opts) {
369
- return agentCommand(opts.exec === undefined ? { agent: opts.agent ?? config.agent } : opts);
370
- }
371
- }
372
-
373
- export { table, JudgeCommands };
package/judge.d.ts DELETED
@@ -1,63 +0,0 @@
1
- import { type RuleSet } from "@webappwiz/rules";
2
- import { type Logger } from "webappwiz/log";
3
- import { type Fs, type Glob, type Ps } from "webappwiz/system";
4
- import { type Clock } from "webappwiz/time";
5
- import { type Screen } from "./progress.js";
6
- export interface ShowOptions {
7
- /** The rule to print, as `rules ls` lists it. */
8
- id: string;
9
- }
10
- export interface JudgeOptions {
11
- /** The directory to check, and what paths in the report are relative to. */
12
- dir: string;
13
- agent?: string;
14
- exec?: string;
15
- /** Print the prompts to the logger and spawn nothing. */
16
- print?: boolean;
17
- /** Files per review. */
18
- chunk: number;
19
- /** Narrows the run to what git says has changed since this ref. */
20
- since?: string;
21
- /** Agent calls in flight at once, over the config's `concurrency`. */
22
- "concurrency-override"?: number;
23
- /** Line-by-line output with no live block, the way a log wants it. */
24
- ci?: boolean;
25
- }
26
- /** What a `JudgeCommands` runs through, and what else it lists. */
27
- export interface JudgeCommandsOptions {
28
- /** Where live progress draws; this process's terminal by default. */
29
- screen?: Screen;
30
- log?: Logger;
31
- fs?: Fs;
32
- ps?: Ps;
33
- clock?: Clock;
34
- glob?: Glob;
35
- }
36
- export declare class JudgeCommands {
37
- private rules;
38
- private screen;
39
- private log;
40
- private fs;
41
- private ps;
42
- private clock;
43
- private glob;
44
- constructor(rules: RuleSet, opts?: JudgeCommandsOptions);
45
- /** Lists every rule there is, one row each, ids first for citing. */
46
- ls(): void;
47
- /**
48
- * Prints one rule in full: what it covers, and the document an agent is
49
- * given, verbatim. Take the id from `rules ls` or from a finding.
50
- */
51
- show(opts: ShowOptions): void;
52
- /**
53
- * Runs the rules over a directory with the agent you name, as `agent` or
54
- * `exec`, falling back to the config's. Exits 1 on any error. Under `print`
55
- * it spawns nothing and prints the prompts instead, for an agent that would
56
- * rather hand them to subagents of its own.
57
- *
58
- * `since` narrows the run to what git says has changed.
59
- */
60
- judge(opts: JudgeOptions): Promise<void>;
61
- /** The agent a command runs with: what it was told, else the config's. */
62
- private agent;
63
- }
package/judge.js DELETED
@@ -1,6 +0,0 @@
1
- import {
2
- JudgeCommands
3
- } from "./index-pyjg1rtk.js";
4
- export {
5
- JudgeCommands
6
- };
package/mode.d.ts DELETED
@@ -1,21 +0,0 @@
1
- /** What a command does with the plan it has made. */
2
- export type Mode = "print" | "run";
3
- /** The flags that choose between the two, on top of the two that name an
4
- * agent. */
5
- export interface ModeOptions {
6
- /** A model to ask. */
7
- agent?: string;
8
- /** A command to hand the prompt to instead. */
9
- exec?: string;
10
- /** Print to the logger and spawn nothing. */
11
- print?: boolean;
12
- }
13
- /**
14
- * Which of the two a caller asked for, with running the default: a command
15
- * given nothing but a directory is one somebody means to run.
16
- *
17
- * Naming two is an error rather than a quiet winner. All of these say what to
18
- * do with one plan, and letting one silently beat the other would leave a
19
- * caller unsure which of the two things they asked for they got.
20
- */
21
- export declare function mode({ print, agent, exec }: ModeOptions): Mode;
package/progress.d.ts DELETED
@@ -1,64 +0,0 @@
1
- import { type Timer } from "webappwiz/time";
2
- /**
3
- * Where live progress draws. `tty` is whether a line can be redrawn in
4
- * place: without one, judge stays line-by-line and never writes here.
5
- */
6
- export interface Screen {
7
- tty: boolean;
8
- write(text: string): void;
9
- }
10
- /** The terminal this process writes to. */
11
- export declare const terminal: () => Screen;
12
- /** A run as the status line shows it: how far along, what it is on, what it
13
- * has spent, and what it has found. */
14
- export interface RunView {
15
- /** Calls finished. */
16
- done: number;
17
- /** Calls the run will make in all. */
18
- total: number;
19
- /** Files the calls out right now are reading. Every review names at least
20
- * one, so zero here is the same as nothing running. */
21
- files: number;
22
- /** Tokens spent so far, when any agent has reported usage. */
23
- tokens?: number;
24
- /** Violations found so far; said aloud once the first call is home. */
25
- problems: number;
26
- }
27
- /** The spinner's walk, one step per tick while any call is out. */
28
- export declare const FRAMES: string[];
29
- /**
30
- * The status line. Pure, so what it says is testable without a terminal:
31
- * the `Progress` around it only draws and redraws it. `frame` indexes the
32
- * spinner's walk; with nothing running there is nothing to spin.
33
- */
34
- export declare function render(view: RunView, frame?: number): string;
35
- /** What a `Progress` paces its spinner with; the real one by default. */
36
- export interface ProgressOptions {
37
- timer?: Timer;
38
- }
39
- /**
40
- * The live line a run draws while agents are out: progress over the calls
41
- * and the tokens they have spent, redrawn on every event and spun on a
42
- * tick between them. `stop` takes the line down, and whatever prints next
43
- * lands where it was.
44
- */
45
- export declare class Progress {
46
- private screen;
47
- private total;
48
- private done;
49
- private files;
50
- private problems;
51
- private tokens;
52
- private drawn;
53
- private frame;
54
- private ticking;
55
- constructor(screen: Screen, total: number, opts?: ProgressOptions);
56
- /** A call went out over this many files. */
57
- started(files: number): void;
58
- /** A call came home: the files it read, what it spent, what it found. */
59
- finished(files: number, spent?: number, problems?: number): void;
60
- /** Takes the line down for good; call it before printing the report. */
61
- stop(): void;
62
- private erase;
63
- private draw;
64
- }
package/report.d.ts DELETED
@@ -1,69 +0,0 @@
1
- import type { Violation } from "@webappwiz/rules";
2
- import type { Duration } from "webappwiz/time";
3
- export declare const count: (total: number, word: string) => string;
4
- /**
5
- * Where one printed document stops and the next starts, named so a reader
6
- * knows which one they are in without scrolling back.
7
- *
8
- * What `--print` writes is pages of markdown with headings of its own, so a
9
- * heading is not enough to mark a boundary. Unnamed, this is the closing line
10
- * the last document wants as much as the others want an opening one.
11
- */
12
- export declare const divider: (name?: string) => string;
13
- /** How every token figure prints: "14K", not "14,000". */
14
- export declare const compact: Intl.NumberFormat;
15
- /**
16
- * What a plan costs to read, at the four-bytes-a-token rule of thumb. Rough on
17
- * purpose: an estimate that needed a tokenizer, or an API call to count, would
18
- * be one more thing to install and one more thing to be wrong about, and the
19
- * decision it informs is only ever "is this the order of magnitude I meant".
20
- */
21
- export declare const tokens: (bytes: number) => number;
22
- /**
23
- * The plan, before the first agent starts. Counts calls rather than reviews
24
- * because a call is what a run spawns, and it is the denominator of the
25
- * `[n/total]` headings below.
26
- */
27
- export interface Planned {
28
- files: number;
29
- rules: number;
30
- calls: number;
31
- /** Tokens the plan can see, which is a floor on what the run reads. */
32
- estimate: number;
33
- /** Agent calls in flight at once, which is what the wall clock turns on. */
34
- concurrency?: number;
35
- /** The command the run will spawn. */
36
- agent?: string;
37
- }
38
- export declare function planned({ files, rules, calls, estimate, concurrency, agent, }: Planned): string[];
39
- /** A finished review as the report prints it: what the call covered, what it
40
- * found, and what it read. */
41
- export interface Finished {
42
- /** The ids of the rules the review checked. */
43
- rules: string[];
44
- /** How many files this review's agent was told to read. */
45
- files: number;
46
- violations: Violation[];
47
- /** How long this review's agent took. */
48
- took: Duration;
49
- /** Tokens this review's call touched, when the agent reported usage. */
50
- tokens?: number;
51
- /** Which worker ran the call, 0-based. */
52
- worker: number;
53
- /** Tokens that worker has touched so far, this review included. */
54
- workerTokens?: number;
55
- done: number;
56
- total: number;
57
- }
58
- /**
59
- * A finished review, as it should print the moment its agent returns: a status
60
- * line sizing the call, then one finding per violation.
61
- */
62
- export declare function finished({ rules, files, violations, took, tokens, worker, workerTokens, done, total, }: Finished): string[];
63
- /**
64
- * One violation: a location a reader can click, what the code does that the
65
- * rule forbids, and the line it happens on. The heading above names only the
66
- * glob, so the finding says which of its rules this one breaks.
67
- */
68
- export declare function finding(violation: Violation): string[];
69
- export declare function summary(violations: Violation[], took: Duration, tokens?: number): string;
package/rules.d.ts DELETED
@@ -1,10 +0,0 @@
1
- /**
2
- * Every rule webappwiz judges itself by, named one by one. There is no preset
3
- * to spread and nothing runs implicitly: a rule is here or it does not run.
4
- *
5
- * A constant rather than a config file, because rules reach the harness as
6
- * objects. A project with its own rules writes its own list and hands it to
7
- * `JudgeCommands` or to `Check` directly, rather than pointing a flag at a
8
- * module for one of these to import.
9
- */
10
- export declare const JUDGE_RULES: import("@webappwiz/rules").RuleSet;
package/rules.js DELETED
@@ -1,6 +0,0 @@
1
- import {
2
- JUDGE_RULES
3
- } from "./index-htwb54c6.js";
4
- export {
5
- JUDGE_RULES
6
- };