@webappwiz/cli 0.0.7 → 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 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. `--budget` caps what a run may read before it asks whether you meant it;
69
- `--estimate` answers that without having to guess a budget low enough to be
70
- refused. `--print`, `--estimate` and running are three things to do with one
71
- plan, so passing two of them is an error rather than one quietly winning. Code excuses itself from a rule with a `rule-ignore <id>: <reason>`
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 SHIPS INSTALLED
104
- arbor 1.4.0 1.3.0
105
- other 1.4.0 -
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
- overBudget,
8
- tokens,
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
- const predicted = tokens(review.bytes ?? 0);
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, cost }) => {
66
- const spent = cost === undefined ? "" : ` ${usd(cost)}`;
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 };